from __future__ import annotations from decimal import Decimal, ROUND_HALF_UP from enum import Enum from html import parser as html_parser from itertools import zip_longest from zipfile import ZipFile import logging import os import re import sys LOAD_LIST = "pak.load_list" LANGUAGES = { "english": ("en", r"[strings]\strings_english.str"), "korean": ("kr", r"[strings]\strings_korean.str"), } DEFAULT_LANGUAGE = "english" logger = logging.getLogger(__name__) html_parser.starttagopen = re.compile("<[a-zA-Z_]") html_parser.endtagopen = re.compile("]*)(?:[\t\n\r\f ]|/(?!>))*") html_parser.locatetagend = re.compile( r""" [a-zA-Z_][^\t\n\r\f />]* # tag name [\t\n\r\f /]* # optional whitespace before attribute name (?:(?<=['"\t\n\r\f /])[^\t\n\r\f />][^\t\n\r\f /=>]* # attribute name (?:[\t\n\r\f ]*=[\t\n\r\f ]* # value indicator (?:'[^']*' # LITA-enclosed value |"[^"]*" # LIT-enclosed value |(?!['"])[^>\t\n\r\f ]* # bare value ) )? [\t\n\r\f /]* # possibly followed by a space )* >? """, re.VERBOSE, ) locatestarttagend_tolerant = re.compile( r""" <[a-zA-Z_][^\t\n\r\f />\x00]* # tag name (?:[\s/]* # optional whitespace before attribute name (?:(?<=['"\s/])[^\s/>][^\s/=>]* # attribute name (?:\s*=+\s* # value indicator (?:'[^']*' # LITA-enclosed value |"[^"]*" # LIT-enclosed value |(?!['"])[^>\s]* # bare value ) \s* # possibly followed by a space )?(?:\s|/(?!>))* )* )? \s* # trailing whitespace """, re.VERBOSE, ) endtagfind = re.compile(r"") def get_initial_pak_path(): return os.path.join( str(os.path.expanduser("~")), ".steam", "steam", "steamapps", "common", "SnowRunner", "preload", "paks", "client", "initial.pak", ) def get_initial_pak_zipfile(path: str) -> ZipFile: try: return ZipFile(file=path, mode="r") except (FileNotFoundError, PermissionError) as e: logger.error(f"Unable to open initial.pak: {e}") sys.exit(1) class SnowRunnerXNLNode: def __init__(self, name: str, attrs: dict): self.name: str = name self.attrs: dict = attrs self.parent: SnowRunnerXNLNode | None = None self.children: dict[str, SnowRunnerXNLNode | list[SnowRunnerXNLNode]] = {} self.try_cast_attrs() def try_cast_attrs(self): for k, v in self.attrs.items(): if v in ["true", "false"]: self.attrs[k] = True if v == "true" else False continue try: self.attrs[k] = int(v) continue except ValueError: pass try: self.attrs[k] = float(v) except ValueError: pass def add_child(self, child: SnowRunnerXNLNode): if child.name not in self.children: self.children[child.name] = child else: if isinstance(self.children[child.name], list): self.children[child.name].append(child) else: self.children[child.name] = [self.children[child.name], child] def has_attr(self, name: str) -> bool: return name in self.attrs def get_attr(self, name: str): return self.attrs[name] class SnowRunnerXMLParser(html_parser.HTMLParser): def __init__(self): super().__init__(convert_charrefs=False) self.parsed_data = {} self.current_child = None def handle_starttag(self, tag, attrs): this_node = SnowRunnerXNLNode(name=tag, attrs={k: v for k, v in attrs}) if self.current_child is not None: this_node.parent = self.current_child self.current_child = this_node def handle_endtag(self, tag): if self.current_child.parent is None: self.parsed_data[self.current_child.name] = self.current_child self.current_child = None else: self.current_child.parent.add_child(self.current_child) self.current_child = self.current_child.parent def handle_data(self, data): # There should never be any data inside tags as this pretends to be XML. # However, Snowrunner's datafiles are not valid XML in the first place and might # contain random data (most likely typos). Just ignore those. return class SnowrunnerClass: def __init__(self, pak_loader: InitialPakLoader): self.__attrs_handled = set() self.__children_handled = set() self.pak = pak_loader self.UiDesc = "" self.UiName = "" self.Price = -1 self.UnlockByExploration = False self.UnlockByObjective = False self.UnlockByRank = -1 self.GameData = None def load_desc(self, data: SnowRunnerXNLNode): if "region:default" in data.children: data = data.children["region:default"] if data.has_attr("uidesc"): self.UiDesc = data.get_attr("uidesc") if data.has_attr("uiname"): self.UiName = data.get_attr("uiname") def load_game_data(self, data: SnowRunnerXNLNode): self.GameData = self.pak.load_class(SnowrunnerGameObjectType.GAME_DATA, data) self.Price = self.GameData.Price self.UnlockByExploration = self.GameData.UnlockByExploration self.UnlockByObjective = self.GameData.UnlockByObjective self.UnlockByRank = self.GameData.UnlockByRank if "uidesc" in data.children: self.load_desc(data.children["uidesc"]) def load_data(self, data: SnowRunnerXNLNode) -> None: raise NotImplementedError() def update_data(self, data: SnowRunnerXNLNode) -> None: raise NotImplementedError() def ignore_attr(self, name: str) -> None: self.__attrs_handled.add(name.lower()) def load_attr(self, data: SnowRunnerXNLNode, name: str): if data.has_attr(name.lower()): setattr(self, name, data.get_attr(name.lower())) self.__attrs_handled.add(name.lower()) def ignore_child(self, name: str): self.__children_handled.add(name.lower()) def load_child( self, data: SnowRunnerXNLNode, name: str, object_type_override: SnowrunnerGameObjectType | None = None ): self.__children_handled.add(name.lower()) if name.lower() not in data.children: return if name == "UiDesc": self.load_desc(data.children[name.lower()]) return if name == "GameData": self.load_game_data(data.children[name.lower()]) return object_type = SnowrunnerGameObjectType(name.lower()) if object_type_override is None else object_type_override if isinstance(data.children[name.lower()], list): setattr(self, name, [self.pak.load_class(object_type, x) for x in data.children[name.lower()]]) else: setattr(self, name, self.pak.load_class(object_type, data.children[name.lower()])) def loading_finished(self, data: SnowRunnerXNLNode): attr_set = set(data.attrs.keys()) if "_template" in attr_set: attr_set.remove("_template") if len(attr_set - self.__attrs_handled) > 0: logger.error( f"{self.__class__.__name__}(): Unhandled Attributes: {[attr_set - self.__attrs_handled]}, (Handled: {[self.__attrs_handled]}, Exist: {[attr_set]}" ) sys.exit(1) child_set = set(data.children.keys()) if len(child_set - self.__children_handled) > 0: logger.error( f"{self.__class__.__name__}(): Unhandled Children: {[child_set - self.__children_handled]}, (Handled: {[self.__children_handled]}, Exist: {[child_set]}" ) sys.exit(1) def get_translated_ui_desc(self, language: LanguageLoader) -> str: if self.UiDesc in language: return language[self.UiDesc] return f"" def get_translated_ui_name(self, language: LanguageLoader) -> str: if self.UiName in language: return language[self.UiName] return f"" def print(self, level: int = 0): for k, v in sorted(vars(self).items()): if k.startswith("_") or k == "pak": continue if not isinstance(v, list): if not isinstance(v, SnowrunnerClass): print(" " * level * 2 + f"{k}: {v}") else: print(" " * level * 2 + f"{k}:") v.print(level=level + 1) else: print(" " * level * 2 + f"{k}:") for vv in v: if not isinstance(vv, SnowrunnerClass): print(" " * (level + 1) * 2 + f"{v}") else: vv.print(level + 2) @classmethod def from_template(cls, pak_loader: InitialPakLoader, template: SnowrunnerClass) -> SnowrunnerClass: r = cls(pak_loader=pak_loader) for k, v in vars(template).items(): if not isinstance(v, list): setattr( r, k, ( v if not isinstance(v, SnowrunnerClass) else v.__class__.from_template(pak_loader=pak_loader, template=v) ), ) else: setattr( r, k, [ ( x if not isinstance(x, SnowrunnerClass) else x.__class__.from_template(pak_loader=pak_loader, template=x) ) for x in v ], ) return r class AddonSlots(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.InitialOffset = "(0;0;0)" self.Quantity = 0 def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "InitialOffset") self.load_attr(data, "Quantity") self.loading_finished(data) class AddonSockets(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.DefaultAddon = "" self.Socket = None def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "DefaultAddon") self.load_child(data, "Socket") self.ignore_attr("ParentFrame") self.ignore_attr("RequiredAddonIfNoConflicts") self.loading_finished(data) class AddonType(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.Name = "" self.Type = "" self.TypeUiName = "" def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "Name") self.load_attr(data, "Type") self.load_attr(data, "TypeUiName") self.loading_finished(data) class Body(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.AllowedPenetrationDepth = -1 self.NoSoftContacts = False self.Friction = -1.0 self.Mass = 0 self.Name = "" self.IsCapsuleCDT = True self.DamageMult = -1.0 self.RollingFrictionMultiplier = -1.0 self.ImpactType = "" self.LinearDamping = -1.0 self.AngularDamping = -1.0 self.UseDirtTine = False self.DisableShadows = False self.AttachToGround = False self.PlantingGroups = "" self.DebrisType = "" self.Collisions = "" self.CenterOfMassOffset = "(0;0;0)" self.ModelFrame = None self.Body = [] def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "AllowedPenetrationDepth") self.load_attr(data, "NoSoftContacts") self.load_attr(data, "Friction") self.load_attr(data, "Mass") self.load_attr(data, "Name") self.load_attr(data, "IsCapsuleCDT") self.load_attr(data, "DamageMult") self.load_attr(data, "RollingFrictionMultiplier") self.load_attr(data, "ImpactType") self.load_attr(data, "LinearDamping") self.load_attr(data, "AngularDamping") self.load_attr(data, "UseDirtTint") self.load_attr(data, "DisableShadows") self.load_attr(data, "AttachToGround") self.load_attr(data, "PlantingGroups") self.load_attr(data, "DebrisType") self.load_attr(data, "Collisions") self.load_attr(data, "CenterOfMassOffset") self.ignore_attr("CenterOfMassOffseet") # Seriously, Saber? Not even a Schema Check? self.load_attr(data, "ModelFrame") self.load_attr(data, "ForceBodyParams") self.load_attr(data, "NoFoliageCollisions") self.load_attr(data, "NoClientCollisions") self.load_attr(data, "NoCameraCollision") self.load_attr(data, "GravityFactor") self.load_attr(data, "MaxLimit") self.load_attr(data, "MinLimit") self.load_attr(data, "NetSyncUseGravity") self.load_attr(data, "NoPackWheels") self.ignore_attr("ExplicitParentFrame") self.ignore_attr("NetSync") self.ignore_attr("ParentFrame") self.load_child(data, "Body") self.ignore_child("Constraint") self.ignore_child("Motor") self.ignore_child("SideMirror") self.ignore_child("Sunshield") self.loading_finished(data) class CargoType(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_child(data, "UiDesc") self.loading_finished(data) class CompatibleWheels(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.Scale = -1.0 self.TruckWheels = [] def get_size_in_inches(self) -> int: return int(Decimal(self.Scale / 0.0127).to_integral(ROUND_HALF_UP)) def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "Scale") if "type" in data.attrs: self.TruckWheels = self.pak.get_object( object_type=SnowrunnerGameObjectType.TRUCK_WHEELS, name=data.attrs["type"] ) self.ignore_attr("type") self.ignore_attr("OffsetZ") self.ignore_attr("RearOffsetZ") self.loading_finished(data) class Damage(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.Capacity = 0 self.Multiplier = None def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "Capacity") self.ignore_attr("SensationMin") self.ignore_attr("SensationMax") self.ignore_attr("ParentFrame") self.load_child(data, "Multiplier") self.ignore_child("DamageArea") self.loading_finished(data) class Engine(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.BrakesDelay = -1.0 self.CriticalDamageThreshold = -1.0 self.DamageCapacity = -1.0 self.DamagedConsumptionModifier = -1.0 self.DamagedMinTorqueMultiplier = -1.0 self.DamagedMaxTorqueMultiplier = -1.0 self.EngineResponsiveness = -1.0 self.FuelConsumption = -1.0 self.Name = "" self.Torque = -1 self.MaxDeltaAngVel = -1.0 def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "BrakesDelay") self.load_attr(data, "CriticalDamageThreshold") self.load_attr(data, "DamageCapacity") self.load_attr(data, "DamagedConsumptionModifier") self.load_attr(data, "DamagedMinTorqueMultiplier") self.load_attr(data, "DamagedMaxTorqueMultiplier") self.load_attr(data, "EngineResponsiveness") self.load_attr(data, "FuelConsumption") self.load_attr(data, "Name") self.load_attr(data, "Torque") self.load_attr(data, "MaxDeltaAngVel") self.load_child(data, "UiDesc") self.load_child(data, "GameData") self.loading_finished(data) class EngineVariants(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.Engines = [] def load_data(self, data: SnowRunnerXNLNode) -> None: if "engine" in data.children: if isinstance(data.children["engine"], list): self.Engines = [ self.pak.load_class(SnowrunnerGameObjectType.ENGINE, x) for x in data.children["engine"] ] else: self.Engines = [self.pak.load_class(SnowrunnerGameObjectType.ENGINE, data.children["engine"])] self.ignore_child("Engine") self.loading_finished(data) class FuelMass(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.Body = None def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_child(data, "Body") self.loading_finished(data) class GameData(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.AddonSockets = None self.CameraPreset = None self.Category = None self.Country = "" self.ExcludeAddons = "" self.ExcludedCargoTypes = "" self.IsCustomizable = False self.IsDoubleTrailer = False self.IsQuest = False self.Price = -1 self.UnlockByRank = -1 self.UnlockByExploration = False self.UnlockByObjective = False self.children = {} def get_unlock_str(self) -> str: if self.UnlockByExploration: return "Explore / Upgrade" elif self.UnlockByObjective: return "Task" elif self.UnlockByRank > 1: return f"Rank {int(self.UnlockByRank)}" else: return "Always" def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "CameraPreset") self.load_attr(data, "Category") self.load_attr(data, "Country") self.load_attr(data, "ExcludeAddons") self.load_attr(data, "ExcludedCargoTypes") self.load_attr(data, "IsCustomizable") self.load_attr(data, "IsDoubleTrailer") self.load_attr(data, "IsQuest") self.load_attr(data, "Price") self.load_attr(data, "SaddleType") self.load_attr(data, "SoundByGroups") self.load_attr(data, "UnlockByRank") self.load_attr(data, "UnlockByExploration") self.load_attr(data, "UnlockByObjective") self.load_attr(data, "ShowPackingsToppers") self.load_attr(data, "SoundForEachConstraint") self.load_attr(data, "WheelToPack") self.load_attr(data, "GaragePoints") self.load_attr(data, "LoadPoints") self.load_attr(data, "UnpackOnTrailerDetach") self.load_attr(data, "ManualLoads") self.load_attr(data, "OriginalAddon") self.load_attr(data, "RecreateOnZoneChange") self.load_attr(data, "TrialsToUnlock") self.load_attr(data, "IncludedCargoTypes") self.ignore_attr("_noinherit") self.ignore_attr("AddonUnlockByObjective") self.ignore_attr("FrameAlign") self.ignore_attr("FrameAlignOffset") self.ignore_attr("LegacyAlignementTruckAfterGateway") self.ignore_attr("Recallable") self.ignore_attr("ResetTruckAfterGateway") self.children = data.children self.load_child(data, "AddonSockets") self.load_child(data, "UiDesc") self.ignore_child("AddonSlots") self.ignore_child("AddonType") self.ignore_child("CenteringAngularVelocity") self.ignore_child("CraneSocket") self.ignore_child("CustomizationCameras") self.ignore_child("GearboxParams") self.ignore_child("InstallSlot") self.ignore_child("InstallSocket") self.ignore_child("LoadArea") self.ignore_child("LongLogsAlignTarget") self.ignore_child("RequiredAddon") self.ignore_child("RequiredAddonType") self.ignore_child("TractorCargoSlotsOverride") self.ignore_child("WatchTower") self.ignore_child("WinchParams") self.ignore_child("WinchSocket") self.ignore_child("sounds") self.ignore_child("soundcranecabin") self.ignore_child("soundpoweredgroupstart") self.ignore_child("soundpoweredgroupstop") self.ignore_child("soundikloop") self.ignore_child("cranesourcesocket") self.ignore_child("soundpoweredgrouploop") self.ignore_child("soundikstart") self.ignore_child("soundikstop") self.ignore_child("soundopenstart") self.ignore_child("soundgrabbermovestart") self.ignore_child("soundgrabberaction") self.ignore_child("soundgrabbermoveloop") self.ignore_child("soundgrabbermovestop") self.ignore_child("soundclose") self.ignore_child("soundopenloop") self.ignore_child("soundopenstop") self.ignore_child("spawnloadorigin") self.ignore_child("constraintsounds") self.ignore_child("longlogsaligndata") self.loading_finished(data) class Gear(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.AngVel = -1.0 self.FuelModifier = -1.0 def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "AngVel") self.load_attr(data, "FuelModifier") self.loading_finished(data) class Gearbox(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.AWDConsumptionModifier = -1.0 self.CriticalDamageThreshold = -1.0 self.DamageCapacity = -1 self.DamagedConsumptionModifier = -1.0 self.FuelConsumption = -1.0 self.IdleFuelModifier = -1.0 self.MinBreakFreq = -1.0 self.MaxBreakFreq = -1.0 self.Name = "" self.GearboxParams = None self.ReverseGear = None self.HighGear = None self.Gears = [] def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "AWDConsumptionModifier") self.load_attr(data, "CriticalDamageThreshold") self.load_attr(data, "DamageCapacity") self.load_attr(data, "DamagedConsumptionModifier") self.load_attr(data, "FuelConsumption") self.load_attr(data, "IdleFuelModifier") self.load_attr(data, "MinBreakFreq") self.load_attr(data, "MaxBreakFreq") self.load_attr(data, "Name") self.load_child(data, "GameData") if "gamedata" in data.children: self.load_child(data.children["gamedata"], "GearboxParams") self.load_child(data, "Gear") self.load_child(data, "ReverseGear", object_type_override=SnowrunnerGameObjectType.GEAR) self.load_child(data, "HighGear", object_type_override=SnowrunnerGameObjectType.GEAR) self.loading_finished(data) class GearboxParams(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.IsHighGearExists = False self.IsLowerGearExists = False self.IsLowerPlusGearExists = False self.IsLowerMinusGearExists = False self.IsManualLowGear = False def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "IsHighGearExists") self.load_attr(data, "IsLowerGearExists") self.load_attr(data, "IsLowerPlusGearExists") self.load_attr(data, "IsLowerMinusGearExists") self.load_attr(data, "IsManualLowGear") self.loading_finished(data) class GearboxVariants(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.Gearbox = [] def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_child(data, "Gearbox") self.loading_finished(data) def update_data(self, data: SnowRunnerXNLNode) -> None: if not isinstance(data.children["gearbox"], list): data.children["gearbox"] = [data.children["gearbox"]] for orig, upd in zip_longest(self.Gearbox, data.children["gearbox"]): if orig is None: self.Gearbox.append(self.pak.load_class(SnowrunnerGameObjectType.GEARBOX, upd)) elif upd is None: return else: orig.load_data(upd) class InstallSlot(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.CargoAddonSubtype = "" self.CargoLength = 0 self.CargoType = "" self.CargoValue = 1 self.ManualLoads = 0 self.NoPackWheels = False self.Offset = "(0;0;0)" def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "CargoAddonSubtype") self.load_attr(data, "CargoLength") self.load_attr(data, "CargoType") self.load_attr(data, "CargoValue") self.load_attr(data, "ManualLoads") self.load_attr(data, "NoPackWheels") self.load_attr(data, "Offset") self.loading_finished(data) class InstallSocket(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.Offset = "(0;0;0)" self.ParentFrame = None self.Type = None def load_data(self, data: SnowRunnerXNLNode) -> None: self.ignore_attr("_noinherit") self.ignore_attr("Price") # SERIOUSLY, SABER self.ignore_attr("UnlockByRank") # VALIDATE self.ignore_attr("UnlockByExploration") # YOUR INPUTS self.load_attr(data, "CameraPreset") self.load_attr(data, "Type") self.load_attr(data, "ParentFrame") self.load_attr(data, "Offset") self.loading_finished(data) class Multiplier(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.Multiplier = -1.0 self.Type = "" def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "Multiplier") self.load_attr(data, "Type") self.loading_finished(data) class PhysicsModel(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.Mesh = "" self.Body = None def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "_noinherit") self.load_attr(data, "Mesh") self.load_child(data, "Body") self.ignore_child("FarmingBoundingBox1") self.ignore_child("FarmingBoundingBox2") self.ignore_child("FarmingBoundingBox3") self.ignore_child("FarmingBoundingBox4") self.ignore_child("FarmingBoundingBox5") self.ignore_child("FarmingBoundingBox6") self.ignore_child("FarmingBoundingBox7") self.ignore_child("FarmingBoundingBox8") self.ignore_child("NetSync") self.loading_finished(data) class RequiredAddon(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.Types = [] def load_data(self, data: SnowRunnerXNLNode) -> None: self.ignore_attr("_noinherit") self.load_attr(data, "Types") # FIXME: split types by "," and match to loaded TruckAddons self.loading_finished(data) class Socket(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.InCockpit = False self.Names = "" self.NamesBlock = "" self.RequiredHitches = None def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "InCockpit") self.load_attr(data, "Names") self.load_attr(data, "NamesBlock") self.ignore_attr("AddonShift") self.ignore_attr("Dir") self.ignore_attr("Offset") self.ignore_attr("ParentFrame") self.ignore_attr("RequiredHitches") self.ignore_attr("UpDir") self.ignore_child("AddonsShift") self.ignore_child("ExtraParent") self.loading_finished(data) class Suspension(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.BrokenSuspensionMin = 0.0 self.BrokenSuspensionMax = -1.0 self.Damping = -1.0 self.DeviationMax = 0.0 self.Height = -1.0 self.Strength = -1.0 self.SuspensionMin = -100.0 self.SuspensionMax = -100.0 self.WheelType = "top" def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "BrokenSuspensionMin") self.load_attr(data, "BrokenSuspensionMax") self.load_attr(data, "Damping") self.load_attr(data, "DeviationMax") self.load_attr(data, "Height") self.load_attr(data, "Strength") self.load_attr(data, "SuspensionMin") self.load_attr(data, "SuspensionMax") self.load_attr(data, "WheelType") self.loading_finished(data) class SuspensionSet(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.BrokenWheelDamageMultiplier = -1 self.CriticalDamageThreshold = -1.0 self.DamageCapacity = -1 self.DeviationDelta = 0.0 self.Name = "" self.UiName = "" self.Suspension = [] def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "BrokenWheelDamageMultiplier") self.load_attr(data, "CriticalDamageThreshold") self.load_attr(data, "DamageCapacity") self.load_attr(data, "DeviationDelta") self.load_attr(data, "Name") self.load_attr(data, "UiName") self.load_child(data, "GameData") self.load_child(data, "Suspension") self.loading_finished(data) class SuspensionSetVariants(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.SuspensionSet = [] def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_child(data, "SuspensionSet") self.loading_finished(data) class Truck(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.AttachType = "None" self.Country = "" self.ExcludeAddons = None self.ExcludedCargoTypes = None self.IsDeprecated = False self.IsDoubleTrailer = False self.IsQuest = False self.PhysicsModel = None self.TruckData = None self.Type = "Truck" self.WaterMass = None def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "AttachType") self.load_attr(data, "IsDeprecated") self.load_attr(data, "Type") self.ignore_attr("FarmingTrailerType") self.load_child(data, "FuelMass") self.load_child(data, "GameData") self.load_child(data, "PhysicsModel") self.load_child(data, "TruckData") self.load_child(data, "WaterMass", object_type_override=SnowrunnerGameObjectType.FUEL_MASS) if "gamedata" in data.children: self.Country = self.GameData.Country self.ExcludeAddons = self.GameData.ExcludeAddons self.ExcludedCargoTypes = self.GameData.ExcludedCargoTypes self.IsDoubleTrailer = self.GameData.IsDoubleTrailer self.IsQuest = self.GameData.IsQuest self.ignore_child("ActionGroups") self.ignore_child("AutomaticIK") self.ignore_child("BreakableLight") self.ignore_child("ControlledConstraints") self.ignore_child("Headlight") self.ignore_child("LandMark") self.ignore_child("MainHeadlight") self.ignore_child("ModelAttachments") self.ignore_child("MudExtrude") self.ignore_child("NetSync") self.ignore_child("PoweredConstraints") self.ignore_child("RepairsHide") self.ignore_child("Rotator") self.ignore_child("Shaker") self.ignore_child("SFX") self.ignore_child("Snorkel") self.ignore_child("WheelRepairsHide") self.loading_finished(data) def update_data(self, data: SnowRunnerXNLNode) -> None: self.load_data(data) class TruckAddon(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.CameraPreset = "" self.Category = "" self.ExcludedCargoTypes = None self.InstallSlot = None self.InstallSocket = None self.PhysicsModel = None self.TruckData = None self.AddonType = None self.RequiredAddonType = None self.IsChassisFullOcclusion = False self.FuelMass = None self.WaterMass = None def load_data(self, data: SnowRunnerXNLNode) -> None: self.ignore_attr("_noinherit") self.load_attr(data, "IsChassisFullOcclusion") self.load_child(data, "FuelMass") self.load_child(data, "WaterMass", object_type_override=SnowrunnerGameObjectType.FUEL_MASS) self.load_child(data, "GameData") self.load_child(data, "PhysicsModel") self.load_child(data, "TruckData") if "gamedata" in data.children: self.CameraPreset = self.GameData.CameraPreset self.Category = self.GameData.Category self.ExcludedCargoTypes = self.GameData.ExcludedCargoTypes if "installslot" in data.children["gamedata"].children: self.load_child(data.children["gamedata"], "InstallSlot") if "installsocket" in data.children["gamedata"].children: self.load_child(data.children["gamedata"], "InstallSocket") if "addonslots" in data.children["gamedata"].children: self.load_child(data.children["gamedata"], "AddonType") if "addontype" in data.children["gamedata"].children: self.load_child(data.children["gamedata"], "AddonType") if "requiredaddon" in data.children["gamedata"].children: self.load_child(data.children["gamedata"], "RequiredAddon") if "requiredaddontype" in data.children["gamedata"].children: self.load_child( data.children["gamedata"].children["requiredaddontype"], "RequiredAddonType", object_type_override=SnowrunnerGameObjectType.ADDON_TYPE, ) self.ignore_child("Exhaust") self.ignore_child("ExplicitParents") self.ignore_child("FuelHide") self.ignore_child("MainHeadLight") self.ignore_child("ModelAttachments") self.ignore_child("RepairsHide") self.ignore_child("Rotator") self.ignore_child("Snorkel") self.ignore_child("WheelRepairsHide") self.ignore_child("LoadCheckpoint") self.ignore_child("headlight") self.ignore_child("headlightray") self.ignore_child("actioncategories") self.ignore_child("automaticik") self.ignore_child("poweredconstraints") self.ignore_child("addoncamera") self.ignore_child("controlledik") self.ignore_child("controlledconstraints") self.ignore_child("vibrator") self.ignore_child("actiongroups") self.ignore_child("shakers") self.ignore_child("landmark") self.loading_finished(data) def update_data(self, data: SnowRunnerXNLNode) -> None: self.load_data(data) class TruckData(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.AllWheelDriveInstalled = False self.BackSteerSpeed = 0.0 self.CompatibleWheels = [] self.Damage = None self.DiffLockInstalled = False self.DiffLockType = "" self.Engines = [] self.EngineStartDelay = -1.0 self.FuelCapacity = 0 self.FuelTankDamageCapacity = 0 self.Gearboxes = [] self.RepairsCapacity = 0 self.Responsiveness = 0.0 self.SteerSpeed = 0.0 self.Suspensions = [] self.TruckType = "" self.WaterCapacity = 0 self.WheelRepairsCapacity = 0 self.Winches = [] self.Wheels = None def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "AllWheelDriveInstalled") self.load_attr(data, "BackSteerSpeed") self.load_attr(data, "DiffLockInstalled") self.load_attr(data, "DiffLockType") self.load_attr(data, "EngineStartDelay") self.load_attr(data, "FuelCapacity") self.load_attr(data, "RepairsCapacity") self.load_attr(data, "Responsiveness") self.load_attr(data, "SetHandbrakeOnWinchRelease") self.load_attr(data, "SteerSpeed") self.load_attr(data, "TruckType") self.load_attr(data, "WaterCapacity") self.load_attr(data, "WheelRepairsCapacity") self.load_child(data, "CompatibleWheels") if self.CompatibleWheels and not isinstance(self.CompatibleWheels, list): self.CompatibleWheels = [self.CompatibleWheels] self.load_child(data, "Damage") self.load_child(data, "Wheels") if "fueltank" in data.children: self.FuelTankDamageCapacity = int(data.children["fueltank"].attrs["damagecapacity"]) if "enginesocket" in data.children: self.Engines = [ self.pak.get_object(object_type=SnowrunnerGameObjectType.ENGINE_VARIANTS, name=n.strip()) for n in data.children["enginesocket"].attrs["type"].split(",") ] self.ignore_child("EngineSocket") if "gearboxsocket" in data.children: self.Gearboxes = [ self.pak.get_object(object_type=SnowrunnerGameObjectType.GEARBOX_VARIANTS, name=n.strip()) for n in data.children["gearboxsocket"].attrs["type"].split(",") ] self.ignore_child("GearboxSocket") if "suspensionsocket" in data.children: self.Suspensions = [ self.pak.get_object(object_type=SnowrunnerGameObjectType.SUSPENSION_SET_VARIANTS, name=n.strip()) for n in data.children["suspensionsocket"].attrs["type"].split(",") ] self.ignore_child("SuspensionSocket") if "winchupgradesocket" in data.children: self.Winches = [ self.pak.get_object(object_type=SnowrunnerGameObjectType.WINCH_VARIANTS, name=n.strip()) for n in data.children["winchupgradesocket"].attrs["type"].split(",") ] self.ignore_child("WinchUpgradeSocket") self.ignore_attr("_noinherit") self.ignore_attr("EngineIconMesh") self.ignore_attr("EngineIconScale") self.ignore_attr("EngineMarkerOffset") self.ignore_attr("ExhaustStartTime") self.ignore_attr("FuelTankMarkerOffset") self.ignore_attr("GearboxMarkerOffset") self.ignore_attr("HideDistance") self.ignore_attr("ShadowHideDistance") self.ignore_attr("SuspensionMarkerOffset") self.ignore_attr("TruckImage") self.ignore_child("Axles") self.ignore_child("Camera") self.ignore_child("Constraint") self.ignore_child("Dashboard") self.ignore_child("Driver") self.ignore_child("ExtraWheels") # FIXME: Do we need to handle wheels on dead axles? self.ignore_child("Exhaust") self.ignore_child("Foot") self.ignore_child("FuelTank") self.ignore_child("Intake") self.ignore_child("LimitedFluid") self.ignore_child("Messages") self.ignore_child("OcclusionMap") self.ignore_child("Shafts") self.ignore_child("Shaker") self.ignore_child("Shakers") self.ignore_child("Snorkel") self.ignore_child("Sounds") self.ignore_child("SoundsDamage") self.ignore_child("SoundsWheels") self.ignore_child("Steam") self.ignore_child("SteeringRack") self.ignore_child("SteeringWheel") self.loading_finished(data) class TruckTire(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.Mass = -1 self.Name = "" self.RearMassScale = 1 self.WheelFriction = None self.WheelSoftness = None self.Width = -1.0 self.BodyFrictionAsphalt = None def load_data(self, data: SnowRunnerXNLNode) -> None: self.ignore_attr("Mesh") self.ignore_attr("RightSideMesh") self.load_attr(data, "BodyFrictionAsphalt") # FIXME: -sigh- self.load_attr(data, "Mass") self.load_attr(data, "Name") self.load_attr(data, "RearMassScale") self.load_attr(data, "Width") self.load_child(data, "GameData") self.load_child(data, "WheelFriction") self.load_child(data, "WheelSoftness") self.ignore_child("WheelTracks") self.loading_finished(data) class TruckTires(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.TruckTire = None def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_child(data, "TruckTire") if self.TruckTire and not isinstance(self.TruckTire, list): self.TruckTire = [self.TruckTire] self.loading_finished(data) def update_data(self, data: SnowRunnerXNLNode) -> None: if not isinstance(data.children["trucktire"]): data.children["trucktire"] = [data.children["trucktire"]] for orig, upd in zip_longest(self.TruckTire, data.children["trucktire"]): if orig is None: self.TruckTire.append(self.pak.load_class(SnowrunnerGameObjectType.TRUCK_TIRE, upd)) elif upd is None: return else: orig.load_data(upd) class TruckWheel(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.Mass = -1 self.Radius = 0 self.Width = -1.0 self.WheelFriction = None self.WheelSoftness = None def load_data(self, data: SnowRunnerXNLNode) -> None: self.ignore_attr("Mesh") self.load_attr(data, "Mass") self.load_attr(data, "Radius") self.load_attr(data, "Width") self.ignore_child("WheelTracks") self.load_child(data, "WheelFriction") self.load_child(data, "WheelSoftness") self.loading_finished(data) def update_data(self, data: SnowRunnerXNLNode) -> None: self.load_data(data) class TruckWheels(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.DamageCapacity = 0 self.Mass = 0 self.Radius = 0 self.RadiusRear = 0 self.Width = -1.0 self.WidthRear = -1.0 self.TruckTires = None def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "DamageCapacity") self.load_attr(data, "Mass") self.load_attr(data, "Radius") self.load_attr(data, "RadiusRear") self.load_attr(data, "Width") self.load_attr(data, "WidthRear") self.ignore_child("TruckRims") self.load_child(data, "TruckTires") self.loading_finished(data) def update_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "DamageCapacity") self.load_attr(data, "Mass") self.load_attr(data, "Radius") self.load_attr(data, "RadiusRear") self.load_attr(data, "Width") self.load_attr(data, "WidthRear") if "trucktires" not in data.children: return self.TruckTires.update_data(data.children["trucktires"]) class Wheel(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.ConnectedToHandbrake = False self.Location = "" self.SteeringAngle = 0.0 self.SuspensionDamping = -1.0 self.SuspensionHeight = -1.0 self.SuspensionMin = -1.0 self.SuspensionMax = -1.0 self.SuspensionStrength = -1.0 self.Torque = 0 self.Type = None def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "ConnectedToHandbrake") self.load_attr(data, "Location") self.load_attr(data, "SteeringAngle") self.load_attr(data, "SuspensionDamping") self.load_attr(data, "SuspensionHeight") self.load_attr(data, "SuspensionMax") self.load_attr(data, "SuspensionMin") self.load_attr(data, "SuspensionStrength") self.load_attr(data, "Torque") if "type" in data.attrs: self.Type = self.pak.get_object(object_type=SnowrunnerGameObjectType.TRUCK_WHEEL, name=data.attrs["type"]) self.ignore_attr("type") self.ignore_attr("CamberAnglePhysics") self.ignore_attr("CamberAngleRender") self.ignore_attr("CamberSuspensionMultiplier") self.ignore_attr("MeshType") self.ignore_attr("ParentFrame") self.ignore_attr("PaentFrame") # FIXME: SAAAAAAAAAABEEEEEEEEEEEERR!!!!!!!! self.ignore_attr("Pos") self.ignore_attr("PosInLocalFrame") self.ignore_attr("RightSide") self.ignore_attr("SkipPlacement") self.ignore_attr("SteeringCastor") self.ignore_attr("SteeringJointOffset") self.ignore_attr("UiTorque") self.loading_finished(data) class Wheels(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.Wheel = None def load_data(self, data: SnowRunnerXNLNode) -> None: self.ignore_attr("DefaultWheelType") self.ignore_attr("DefaultRim") self.ignore_attr("DefaultTire") self.load_child(data, "Wheel") self.loading_finished(data) class WheelFriction(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader) -> None: super().__init__(pak_loader=pak_loader) self.BodyFriction: float = -1.0 self.BodyFrictionAsphalt: float = -1.0 self.SubstanceFriction: float = -1.0 self.IsIgnoreIce: bool = False self.Rear = False self.UiName = "" def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "BodyFriction") self.load_attr(data, "BodyFrictionAsphalt") self.load_attr(data, "SubstanceFriction") self.load_attr(data, "IsIgnoreIce") self.load_attr(data, "Rear") self.load_attr(data, "UiName") self.loading_finished(data) class WheelSoftness(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.SoftForceScale: float = -1.0 self.RadiusOffset: float = -1.0 def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "RadiusOffset") self.load_attr(data, "SoftForceScale") self.loading_finished(data) class Winch(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.Length = -1 self.Name = "" self.IsEngineIgnitionRequired = True self.StrengthMult = -1.0 def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "Length") self.load_attr(data, "Name") self.load_attr(data, "IsEngineIgnitionRequired") self.load_attr(data, "StrengthMult") self.load_child(data, "GameData") if "gamedata" in data.children: self.load_child(data.children["gamedata"], "WinchParams") self.loading_finished(data) class WinchParams(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) def load_data(self, data: SnowRunnerXNLNode) -> None: self.loading_finished(data) class WinchVariants(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.Winch = [] def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_child(data, "Winch") self.load_child(data, "WinchParams") self.loading_finished(data) class LanguageLoader(dict): def __init__(self, name: str, ident: str, data: str): super().__init__() self.Ident = ident self.Name = name for line in data.splitlines(): parts = line.split("\t") key = parts[0].replace('"', "") value = parts[-1].replace('"', "").replace("\\", '"') self[key] = value TYPE_CLASS_MATRIX = { "addonslots": AddonSlots, "addonsockets": AddonSockets, "addontype": AddonType, "body": Body, "cargotype": CargoType, "compatiblewheels": CompatibleWheels, "damage": Damage, "engine": Engine, "enginevariants": EngineVariants, "fuelmass": FuelMass, "gamedata": GameData, "gear": Gear, "gearbox": Gearbox, "gearboxparams": GearboxParams, "gearboxvariants": GearboxVariants, "installslot": InstallSlot, "installsocket": InstallSocket, "multiplier": Multiplier, "physicsmodel": PhysicsModel, "requiredaddon": RequiredAddon, "socket": Socket, "suspension": Suspension, "suspensionset": SuspensionSet, "suspensionsetvariants": SuspensionSetVariants, "truck": Truck, "truckdata": TruckData, "truckaddon": TruckAddon, "trucktire": TruckTire, "trucktires": TruckTires, "truckwheel": TruckWheel, "truckwheels": TruckWheels, "wheel": Wheel, "wheels": Wheels, "wheelfriction": WheelFriction, "wheelsoftness": WheelSoftness, "winch": Winch, "winchparams": WinchParams, "winchvariants": WinchVariants, } class SnowrunnerGameObjectType(str, Enum): ADDON_SLOTS = "addonslots" ADDON_SOCKETS = "addonsockets" ADDON_TYPE = "addontype" BODY = "body" CARGO_TYPE = "cargotype" COMPATIBLE_WHEELS = "compatiblewheels" DAMAGE = "damage" ENGINE = "engine" ENGINE_VARIANTS = "enginevariants" FUEL_MASS = "fuelmass" GAME_DATA = "gamedata" GEAR = "gear" GEARBOX = "gearbox" GEARBOX_PARAMS = "gearboxparams" GEARBOX_VARIANTS = "gearboxvariants" INSTALL_SLOT = "installslot" INSTALL_SOCKET = "installsocket" MULTIPLIER = "multiplier" PHYSICS_MODEL = "physicsmodel" REQUIRED_ADDON = "requiredaddon" SOCKET = "socket" SUSPENSION = "suspension" SUSPENSION_SET = "suspensionset" SUSPENSION_SET_VARIANTS = "suspensionsetvariants" TRUCK = "truck" TRUCK_ADDON = "truckaddon" TRUCK_DATA = "truckdata" TRUCK_TIRE = "trucktire" TRUCK_TIRES = "trucktires" TRUCK_WHEEL = "truckwheel" TRUCK_WHEELS = "truckwheels" WHEEL = "wheel" WHEELS = "wheels" WHEEL_FRICTION = "wheelfriction" WHEEL_SOFTNESS = "wheelsoftness" WINCH = "winch" WINCH_PARAMS = "winchparams" WINCH_VARIANTS = "winchvariants" def snowrunner_class(self): return TYPE_CLASS_MATRIX[self.value] @classmethod def values(cls) -> list[str]: return [x.value for x in SnowrunnerGameObjectType] @classmethod def names(cls) -> list[str]: return [x.name for x in SnowrunnerGameObjectType] class InitialPakLoader: game_data_load_order = [ "cargotype", "enginevariants", "gearboxvariants", "suspensionsetvariants", "winchvariants", "truckaddon", "truckwheel", "truckwheels", "truck", ] ignore_categories = { "ambientsounds", "breaktypes", "categorylist", # FIXME: Maybe? "chunksbreak", "cockpit", "collarf", "combineparticles", "combinesky", "constraint", "daytimeconfig", "daytimestate", "debristypes", "distributionbrushes", "drivecamera", "drivecharacter", "drivecharacterskeleton", "editorzones", "flare", "garagesounds", "globalsounds", "grassbrand", "ignition", "impacttypes", "headlight", "light", "lightbar", "material", "materialoverrides", "materialtype", "mainheadlight", "model", "modelbrand", "motor", "mudguard", "mudtype", "musicpresets", "mutators", "overlaybrand", "physicsmodel", "plantbrand", "rotator", "shafts", "sounds", "soundsdamage", "soundswheels", "staticlight", "truckset", # FIXME: Maybe? "watertype", "weatherparticles", "wheeltracks", "winchuidrawparams", } def __init__(self): self.pak_zip = get_initial_pak_zipfile(get_initial_pak_path()) self.is_loaded = False self.languages = {} self.raw_data = {x: {} for x in SnowrunnerGameObjectType} self.game_data = {x: {} for x in SnowrunnerGameObjectType} self.templates = {x: {} for x in SnowrunnerGameObjectType} self.templates_local = {x: {} for x in SnowrunnerGameObjectType} self.load() def load_language(self, l_name: str): try: with self.pak_zip.open(LANGUAGES[l_name][1]) as l_file: self.languages[l_name] = LanguageLoader(l_name, LANGUAGES[l_name][0], l_file.read().decode("utf-16le")) except FileNotFoundError: logger.error(f"Language File Not Found for '{l_name}': {LANGUAGES[l_name]}") def load_class( self, object_type: SnowrunnerGameObjectType, data: SnowRunnerXNLNode, parent: str | None = None, lazy: bool = False, ) -> SnowrunnerClass | list[SnowrunnerClass] | None: if parent is not None: template = self.get_object(object_type=object_type, name=parent, lazy=lazy) if template is None: return None ret = object_type.snowrunner_class().from_template(pak_loader=self, template=template) ret.update_data(data) return ret if not isinstance(data, list): if "_template" not in data.attrs: ret = object_type.snowrunner_class()(pak_loader=self) else: ret = object_type.snowrunner_class().from_template( pak_loader=self, template=self.get_template(object_type=object_type, name=data.attrs["_template"]) ) ret.load_data(data) else: ret = [] for sub_data in data: if "_template" not in sub_data.attrs: ret.append(object_type.snowrunner_class()(pak_loader=self)) else: ret.append( object_type.snowrunner_class().from_template( pak_loader=self, template=self.get_template(object_type=object_type, name=sub_data.attrs["_template"]), ) ) ret[-1].load_data(sub_data) return ret def load_templates(self, data: dict, local: bool = False) -> None: if local: self.templates_local = {x: {} for x in SnowrunnerGameObjectType} for category in data: if isinstance(data[category], list): for x in data[category][1:]: data[category][0].children |= x.children data[category] = data[category][0] tp_data = data[category].children if category in ["requiredaddon", "truck", "_parent"]: continue # not sure, yet - needs more analysis if category in SnowrunnerGameObjectType.values(): object_type = SnowrunnerGameObjectType(category) defer = [] for tp_name, sub_data in tp_data.items(): if not isinstance(sub_data, list) and "_template" in sub_data.attrs: defer.append((tp_name, sub_data)) continue if local: self.templates_local[category][tp_name] = self.load_class( object_type=object_type, data=sub_data ) else: self.templates[category][tp_name] = self.load_class(object_type=object_type, data=sub_data) if defer: for tp_name, sub_data in defer: if local: self.templates_local[category][tp_name] = self.load_class( object_type=object_type, data=sub_data ) else: self.templates[category][tp_name] = self.load_class(object_type=object_type, data=sub_data) elif category in self.ignore_categories: pass # Ignore for now else: logger.error(f"Unknown Template Category: {category}") sys.exit(1) def get_template(self, object_type: SnowrunnerGameObjectType, name: str) -> SnowrunnerClass: name = name.lower() if object_type in self.templates_local and name in self.templates_local[object_type]: return self.templates_local[object_type][name] elif object_type in self.templates and name in self.templates[object_type]: return self.templates[object_type][name] else: logger.error(f"Unknown Template requested: {name} in {object_type.name}") sys.exit(1) def get_object( self, object_type: SnowrunnerGameObjectType, name: str, lazy: bool = False ) -> SnowrunnerClass | None: # name = name.lower() if object_type in self.game_data: if name in self.game_data[object_type]: return self.game_data[object_type][name] for v in self.game_data[object_type].values(): if getattr(v, "Name", "") == name: return v if lazy: return None logger.error(f"Unknown Game Data Object requested: {name} in {object_type.name}") sys.exit(1) def get_sorted_object_list( self, object_type: SnowrunnerGameObjectType, language: LanguageLoader ) -> list[tuple[str, SnowrunnerClass]]: return list( sorted(self.game_data[object_type].items(), key=lambda v: v[1].get_translated_ui_name(language)), ) def store_raw(self, ref: str, data: dict): cat_in_matrix = set(data.keys()) & set(SnowrunnerGameObjectType.values()) cat_in_ignore = set(data.keys()) & self.ignore_categories if cat_in_matrix: self.raw_data[SnowrunnerGameObjectType(list(cat_in_matrix)[0])][ref] = data elif cat_in_ignore: pass # not sure what to do with this, yet else: logger.error(f"Don't know what to do with:\n {list(data.keys())}") sys.exit(1) def load(self): logger.info("Loading Languages") for l_name in LANGUAGES: logger.info(f" - {l_name}") self.load_language(l_name) logger.info("Loading data files") for member in self.pak_zip.infolist(): if not member.filename.endswith(".xml"): continue filename_ref = member.filename.split("\\")[-1].replace(".xml", "") logger.info(f" - {member.filename}") with self.pak_zip.open(member) as d_file: parser = SnowRunnerXMLParser() parser.feed(d_file.read().decode()) data = parser.parsed_data if len(data) == 1 and "_templates" in data: logger.info(f" - -> Loading templates from {member.filename}") self.load_templates(data["_templates"].children) continue elif len(data) > 3: logger.error(f"Multiple keys?! ({list(data.keys())}") sys.exit(1) self.store_raw(filename_ref, data) deferred_loads = [] logger.info("Loading GameData") for category in self.game_data_load_order: logger.info(f" - {category}") object_type = SnowrunnerGameObjectType(category) for ref, data in self.raw_data[object_type].items(): if "_parent" in data: deferred_loads.append((object_type, ref, data)) continue logger.info(f" - {ref}") if "_templates" in data: self.load_templates(data["_templates"].children, local=True) self.game_data[object_type][ref] = self.load_class(object_type, data[category]) del self.raw_data[object_type] while deferred_loads: still_deferred_loads = [] for object_type, ref, data in deferred_loads: logger.info(f" - {ref}") if "_templates" in data: self.load_templates(data["_templates"].children, local=True) sr_class = self.load_class( object_type, data[category], parent=data["_parent"].get_attr("file"), lazy=True ) if sr_class is None: still_deferred_loads.append((object_type, ref, data)) else: self.game_data[object_type][ref] = sr_class if len(deferred_loads) != len(still_deferred_loads): deferred_loads = still_deferred_loads else: logger.warning("Deferred Loading failed. Probably non-existing parents. Loading without parents") for object_type, ref, data in still_deferred_loads: logger.info(f" - {object_type.name}, {ref}") if "_templates" in data: self.load_templates(data["_templates"].children, local=True) self.game_data[object_type][ref] = self.load_class(object_type, data[category]) deferred_loads = [] for cat, v in self.raw_data.items(): if v: logger.error(f"Unhandled Category: {cat}")