diff --git a/lihzahrd/world.py b/lihzahrd/world.py index c34a951..7cb3b5d 100644 --- a/lihzahrd/world.py +++ b/lihzahrd/world.py @@ -1,5 +1,9 @@ import math import struct +import uuid +import enum +import datetime +import typing class Rect: @@ -13,84 +17,464 @@ class Rect: return f"Rect(left={self.left}, right={self.right}, top={self.top}, bottom={self.bottom})" -class World: - @classmethod - def create_from_file(cls, f): - version = cls.int4(f) - relogic = cls.string(f, 7) - filetype = cls.byte(f) - revision = cls.uint4(f) - favorite = cls.uint8(f) != 0 - pointers = [cls.int4(f) for _ in range(cls.int2(f))] - tileframeimportant_size = math.ceil(cls.int2(f) / 8) - tileframeimportant = [cls.bool(f) for _ in range(tileframeimportant_size)] - worldname = cls.string(f) - worldid = cls.int4(f) - # Not working from here on - bounds = cls.rect(f) - worldsize = (cls.int4(f), cls.int4(f)) - ... +class FileReader: + def __init__(self, file): + self.file = file - @staticmethod - def bit(f): - data = f.read(1) - return (data & 0b1000_0000, - data & 0b0100_0000, - data & 0b0010_0000, - data & 0b0001_0000, - data & 0b0000_1000, - data & 0b0000_0100, - data & 0b0000_0010, - data & 0b0000_0001) + def bool(self): + return struct.unpack("?", self.file.read(1))[0] - @staticmethod - def bool(f): - return struct.unpack("?", f.read(1))[0] + def int1(self): + return struct.unpack("B", self.file.read(1))[0] - @staticmethod - def byte(f): - return struct.unpack("B", f.read(1))[0] + def uint1(self): + return struct.unpack("B", self.file.read(1))[0] - @staticmethod - def int2(f): - return struct.unpack("h", f.read(2))[0] + def int2(self): + return struct.unpack("h", self.file.read(2))[0] - @staticmethod - def int4(f): - return struct.unpack("i", f.read(4))[0] + def uint2(self): + return struct.unpack("H", self.file.read(2))[0] - @staticmethod - def uint4(f): - return struct.unpack("I", f.read(4))[0] + def int4(self): + return struct.unpack("i", self.file.read(4))[0] - @staticmethod - def int8(f): - return struct.unpack("q", f.read(8))[0] + def uint4(self): + return struct.unpack("I", self.file.read(4))[0] - @staticmethod - def uint8(f): - return struct.unpack("Q", f.read(8))[0] + def int8(self): + return struct.unpack("q", self.file.read(8))[0] - @staticmethod - def single(f): - return struct.unpack("f", f.read(4))[0] + def uint8(self): + return struct.unpack("Q", self.file.read(8))[0] - @staticmethod - def double(f): - return struct.unpack("d", f.read(8))[0] + def single(self): + return struct.unpack("f", self.file.read(4))[0] - @staticmethod - def rect(f): - left, right, top, bottom = struct.unpack("iiii", f.read(16)) + def double(self): + return struct.unpack("d", self.file.read(8))[0] + + def bit(self): + data = struct.unpack("B", self.file.read(1))[0] + return (bool(data & 0b1000_0000), + bool(data & 0b0100_0000), + bool(data & 0b0010_0000), + bool(data & 0b0001_0000), + bool(data & 0b0000_1000), + bool(data & 0b0000_0100), + bool(data & 0b0000_0010), + bool(data & 0b0000_0001)) + + def rect(self): + left, right, top, bottom = struct.unpack("iiii", self.file.read(16)) return Rect(left, right, top, bottom) - @staticmethod - def string(f, size=None): + def string(self, size=None): if size is None: - size = World.byte(f) - return str(f.read(size), encoding="latin1") + size = self.uint1() + return str(self.file.read(size), encoding="latin1") + + def uuid(self): + # TODO: convert to uuid + # https://docs.microsoft.com/en-us/dotnet/api/system.guid.tobytearray?view=netframework-4.8 + uuid_bytes = self.file.read(16) + return uuid_bytes + + def datetime(self): + # TODO: convert to datetime + # https://docs.microsoft.com/it-it/dotnet/api/system.datetime.kind?view=netframework-4.8#System_DateTime_Kind + datetime_bytes = self.file.read(8) + return datetime_bytes + + +class Version: + """A Terraria version.""" + + _version_ids = { + 71: "1.2.0.3.1", + 77: "1.2.2", + 104: "1.2.3", + 140: "1.3.0.1", + 151: "1.3.0.4", + 153: "1.3.0.5", + 154: "1.3.0.6", + 155: "1.3.0.7", + 156: "1.3.0.8", + 170: "1.3.2", + 174: "1.3.3", + 178: "1.3.4", + 194: "1.3.5.3" + } + + def __init__(self, data: typing.Union[int, str]): + if isinstance(data, int): + self.id = data + else: + for version in self._version_ids: + if self._version_ids[version] == data: + self.id = version + break + else: + raise ValueError("No such version") + + @property + def name(self): + # TODO: Add all versions + try: + return self._version_ids[self.id] + except KeyError: + return "Unknown" + + def __repr__(self): + return f"Version({self.id})" + + def __str__(self): + return self.name + + def __eq__(self, other): + return self.id == other + + def __gt__(self, other): + return self.id > other + + def __lt__(self, other): + return self.id < other + + +class GeneratorInfo: + """Information about the world generator.""" + + def __init__(self, seed, version): + self.seed = seed + """The seed this world was generated with.""" + + self.version = version + """The version of the generator that created this world.""" + + +class Coordinates: + """A pair of coordinates.""" + def __init__(self, x, y): + self.x = x + self.y = y + + def __repr__(self): + return f"Coordinates({self.x}, {self.y})" + + def __str__(self): + return f"{self.x}, {self.y}" + + +class MoonStyle(enum.IntEnum): + """All possible moon styles.""" + WHITE = 0 + ORANGE = 1 + RINGED_GREEN = 2 + + +class FourPartSplit: + """A world property split in four parts, separated by three vertical lines at a certain x coordinate.""" + def __init__(self, separators: typing.List[int], properties: typing.List): + self.separators: typing.List[int] = separators + """The three x coordinates of the vertical separators, in increasing order.""" + + self.properties: typing.List = properties + """The four properties, in order: + + - The far left property, the one between the left world edge and the first separator. + - The nearby left property, between the first and the second separator. + - The nearby right property, between the second and the third separator. + - The far right property, between the third separator and the right world edge.""" + + def __repr__(self): + return f"FourPartSplit({repr(self.separators)}, {repr(self.properties)})" + + def __str__(self): + return f"{self.far_left} [{self.separators[0]}] {self.nearby_left} [{self.separators[1]}] {self.nearby_right} [{self.separators[2]}] {self.far_right}" + + def get_property_at_x(self, x: int): + if x < self.separators[0]: + return self.properties[0] + elif x < self.separators[1]: + return self.properties[1] + elif x < self.separators[2]: + return self.properties[2] + else: + return self.properties[3] + + @property + def far_left(self): + return self.properties[0] + + @far_left.setter + def far_left(self, value): + self.properties[0] = value + + @property + def nearby_left(self): + return self.properties[1] + + @nearby_left.setter + def nearby_left(self, value): + self.properties[1] = value + + @property + def nearby_right(self): + return self.properties[2] + + @nearby_right.setter + def nearby_right(self, value): + self.properties[2] = value + + @property + def far_right(self): + return self.properties[2] + + @far_right.setter + def far_right(self, value): + self.properties[2] = value + + +class WorldStyles: + """The styles of various world elements.""" + def __init__(self, + moon: MoonStyle, + trees: FourPartSplit, + moss: FourPartSplit,): + self.moon: MoonStyle = moon + self.trees: FourPartSplit = trees + self.moss: FourPartSplit = moss + + +class WorldBackgrounds: + """The backgrounds of various world biomes.""" + def __init__(self, + bg_underground_snow, + bg_underground_jungle, + bg_hell, + bg_forest, + bg_corruption, + bg_jungle, + bg_snow, + bg_hallow, + bg_crimson, + bg_desert, + bg_ocean, + bg_cloud): + self.bg_underground_snow = bg_underground_snow + self.bg_underground_jungle = bg_underground_jungle + self.bg_hell = bg_hell + self.bg_forest = bg_forest + self.bg_corruption = bg_corruption + self.bg_jungle = bg_jungle + self.bg_snow = bg_snow + self.bg_hallow = bg_hallow + self.bg_crimson = bg_crimson + self.bg_desert = bg_desert + self.bg_ocean = bg_ocean + self.bg_cloud = bg_cloud + + + +class World: + """The Python representation of a Terraria world.""" + def __init__(self, + version: Version, + savefile_type: int, + revision: int, + is_favorite: bool, + name: str, + generator: GeneratorInfo, + uuid_: uuid.UUID, + id_: int, + bounds: Rect, + size: Coordinates, + is_expert: bool, + created_on, + ): + + self.version: Version = version + """The game version when this savefile was last saved.""" + + self.savefile_type = savefile_type + """The format of the save file. Should be 2 for all versions following 1.2.""" + + self.revision: int = revision + """The number of times this world was saved.""" + + self.is_favorite: bool = is_favorite + """If the world is marked as favorite or not.""" + + self.name: str = name + """The name the world was given at creation. Doesn't always match the filename.""" + + self.generator: GeneratorInfo = generator + """Information about the generation of this world.""" + + self.uuid: uuid.UUID = uuid_ + """The Universally Unique ID of this world.""" + + self.id: int = id_ + """The world id. Used to name the minimap file.""" + + self.bounds: Rect = bounds + """The world size in pixels.""" + + self.size: Coordinates = size + """The world size in tiles.""" + + self.is_expert: bool = is_expert + """If the world is in expert mode or not.""" + + self.created_on = created_on + """The date and time this world was created in.""" + + @classmethod + def create_from_file(cls, file): + f = FileReader(file) + + version = Version(f.int4()) + relogic = f.string(7) + savefile_type = f.uint1() + if version != Version("1.3.5.3") or relogic != "relogic" or savefile_type != 2: + raise NotImplementedError("This parser can only read Terraria 1.3.5.3 save files.") + + revision = f.uint4() + is_favorite = f.uint8() != 0 + + pointers = [f.int4() for _ in range(f.int2())] + tileframeimportant_size = math.ceil(f.int2() / 8) + tileframeimportant = [] + for _ in range(tileframeimportant_size): + current_bit = f.bit() + tileframeimportant = [*tileframeimportant, *current_bit] + + name = f.string() + seed = f.string() + generator_version = f.int4() + uuid_ = f.uuid() + id_ = f.int8() + bounds = f.rect() + world_size = Coordinates(y=f.int4(), x=f.int4()) + is_expert = f.bool() + created_on = f.datetime() + + world_styles = WorldStyles(moon=MoonStyle(f.uint1()), + trees=FourPartSplit(separators=[f.int4(), f.int4(), f.int4()], + properties=[f.int4(), f.int4(), f.int4(), f.int4()]), + moss=FourPartSplit(separators=[f.int4(), f.int4(), f.int4()], + properties=[f.int4(), f.int4(), f.int4(), f.int4()])) + + bg_underground_snow = f.int4() + bg_underground_jungle = f.int4() + bg_hell = f.int4() + spawn_point = (f.int4(), f.int4()) + underground_level = f.double() + cavern_level = f.double() + current_time = f.double() + is_daytime = f.bool() + moon_phase = f.uint4() + blood_moon = f.bool() + eclipse = f.bool() + dungeon_point = (f.int4(), f.int4()) + is_crimson = f.bool() + defeated_eye_of_cthulhu = f.bool() # Possibly. I'm not sure. + defeated_eater_of_worlds = f.bool() # Possibly. I'm not sure. + defeated_skeletron = f.bool() # Possibly. I'm not sure. + defeated_queen_bee = f.bool() + defeated_the_twins = f.bool() + defeated_the_destroyer = f.bool() + defeated_skeletron_prime = f.bool() + defeated_any_mechnical_boss = f.bool() + defeated_plantera = f.bool() + defeated_golem = f.bool() + defeated_king_slime = f.bool() + saved_goblin_tinkerer = f.bool() + saved_wizard = f.bool() + saved_mechanic = f.bool() + defeated_goblin_army = f.bool() + defeated_clown = f.bool() + defeated_frost_moon = f.bool() + defeated_pirates = f.bool() + smashed_shadow_orb = f.bool() + spawn_meteor = f.bool() + smashed_shadow_orb_mod3 = f.int4() + smashed_altars_count = f.int4() + is_hardmode = f.bool() + invasion_delay = f.int4() + invasion_size = f.int4() + invasion_type = f.int4() + invasion_position = f.double() + time_left_slime_rain = f.double() + cooldown_sundial = f.uint1() + is_raining = f.bool() + time_left_rain = f.int4() + max_rain = f.single() # ??? + hardmode_ore_1 = f.int4() + hardmode_ore_2 = f.int4() + hardmode_ore_3 = f.int4() + bg_forest = f.int1() + bg_corruption = f.int1() + bg_jungle = f.int1() + bg_snow = f.int1() + bg_hallow = f.int1() + bg_crimson = f.int1() + bg_desert = f.int1() + bg_ocean = f.int1() + bg_cloud = f.int4() # ??? + cloud_number = f.int2() # ??? + wind_speed = f.single() # ??? + angler_today_quest_completed_by_count = f.uint1() + angler_today_quest_completed_by = [] + for _ in range(angler_today_quest_completed_by_count): + angler_today_quest_completed_by.append(f.string()) + saved_angler = f.bool() + angler_today_quest_target = f.int4() + saved_stylist = f.bool() + saved_tax_collector = f.bool() + invasion_size_start = f.int4() # ??? + cultist_delay = f.int4() # ??? + ... + mob_types_count = f.int2() + mob_kills = {} + for mob_id in range(mob_types_count): + mob_kills[mob_id] = f.int4() + fast_forward_time = f.bool() + defeated_duke_fishron = f.bool() + defeated_moon_lord = f.bool() + defeated_pumpking = f.bool() + defeated_mourning_wood = f.bool() + defeated_ice_queen = f.bool() + defeated_santa_nk1 = f.bool() + defeated_everscream = f.bool() + defeated_pillar_solar = f.bool() + defeated_pillar_vortex = f.bool() + defeated_pillar_nebula = f.bool() + defeated_pillar_stardust = f.bool() + solar_pillar_active = f.bool() + vortex_pillar_active = f.bool() + nebula_pillar_active = f.bool() + stardust_pillar_active = f.bool() + lunar_events_active = f.bool() + party_center_active = f.bool() + party_natural_active = f.bool() + party_cooldown = f.int4() + partying_npcs_count = f.int4() + partying_npcs = [] + for _ in range(partying_npcs_count): + partying_npcs.append(f.int4()) + is_sandstorm = f.bool() + time_left_sandstorm = f.int4() + sandstorm_severity = f.single() # ??? + sandstorm_intended_severity = f.single() # ??? + saved_bartender = f.bool() + defeated_old_ones_army_tier_1 = f.bool() + defeated_old_ones_army_tier_2 = f.bool() + defeated_old_ones_army_tier_3 = f.bool() + # Tile data starts here + ... if __name__ == "__main__": - with open("sampleworld.wld", "rb") as f: + with open("Small_Example.wld", "rb") as f: w = World.create_from_file(f)