From 1780362b902a1f5fa87a2f332364ea33b2b7bd2b Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Sun, 16 Aug 2026 11:44:59 +0200 Subject: [PATCH] mv --- loader.py | 1751 ++++++++++++++++++++++++++++++++++++++++++++++ main.py | 15 + output.py | 160 +++++ requirements.txt | 1 + 4 files changed, 1927 insertions(+) create mode 100644 loader.py create mode 100644 main.py create mode 100644 output.py create mode 100644 requirements.txt diff --git a/loader.py b/loader.py new file mode 100644 index 0000000..959c0e2 --- /dev/null +++ b/loader.py @@ -0,0 +1,1751 @@ +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}") diff --git a/main.py b/main.py new file mode 100644 index 0000000..66d39e4 --- /dev/null +++ b/main.py @@ -0,0 +1,15 @@ +from __future__ import annotations +from loader import InitialPakLoader +from output import create_html +import logging +import sys + + +def main(): + logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) + pak = InitialPakLoader() + create_html(pak=pak, destination_directory="output/") + + +if __name__ == "__main__": + main() diff --git a/output.py b/output.py new file mode 100644 index 0000000..f0f00b6 --- /dev/null +++ b/output.py @@ -0,0 +1,160 @@ +from __future__ import annotations +import os +from loader import InitialPakLoader, LanguageLoader, SnowrunnerGameObjectType, SnowrunnerClass, GameData +from logging import getLogger + +logger = getLogger(__name__) + + +def get_head(language: LanguageLoader, category: str, name: str | None = None) -> str: + return f"""\n\n""" + ( + f""" + + SnowRunnerDB - {category}""" + + (f""" - {name}""" if name is not None else "") + + f""" + + + + +
+ +
+
+ """ + ) + + +def get_footer(): + return f""" +
+ \n\n + """ + + +def write_html( + file_path: str | os.PathLike, language: LanguageLoader, content: str, category: str, name: str | None = None +): + with open(file_path, "w") as f: + f.write(get_head(language=language, category=category, name=name)) + f.write(content) + f.write(get_footer()) + + +def write_trucks(pak: InitialPakLoader, destination_directory: str, language: LanguageLoader): + index_table = "" + + truck_types = {} + + for reference, truck in pak.get_sorted_object_list(SnowrunnerGameObjectType.TRUCK, language): + if truck.Type != "Truck": + continue + + if truck.TruckData.TruckType not in truck_types: + truck_types[truck.TruckData.TruckType] = [(reference, truck)] + else: + truck_types[truck.TruckData.TruckType].append((reference, truck)) + + truck_file = os.path.join(destination_directory, reference + ".html") + truck_content = f"

{truck.get_translated_ui_name(language)}

" + + # Engines + truck_content += f"

Engines

\n" + engines = [x for y in truck.TruckData.Engines for x in y.Engines] + truck_content += f" \n" + truck_content += f" \n" + # Power2Weight + # Power2Fuel + for engine in sorted(engines, key=lambda x: x.Torque): + truck_content += ( + f" " + f"" + f"" + f"" + f"" + f"\n" + ) + truck_content += f"
NamePriceTorqueBase Fuel ConsumptionUnlock By
{engine.get_translated_ui_name(language)}{engine.GameData.Price}" + f"{engine.Torque}{engine.FuelConsumption}{engine.GameData.get_unlock_str()}
\n" + + # Gearboxes + truck_content += f"

Gearboxes

\n" + # Suspensions + truck_content += f"

Suspensions

\n" + # Winches + truck_content += f"

Winches

\n" + + # Wheels + truck_content += f"

Wheels

\n" + wheel_cat = {} + scales = set() + for cw in truck.TruckData.CompatibleWheels: + scales.add(cw.get_size_in_inches()) + damage_cap = cw.TruckWheels.DamageCapacity + width = int(cw.TruckWheels.Width * cw.Scale * 100) + mass = cw.TruckWheels.Mass + # FIXME: cw.TruckWheels.GameData.ReadMassScale (affects the truck mass for dualies) + for tt in cw.TruckWheels.TruckTires.TruckTire: + if tt.Mass > 0: + mass = tt.Mass + if tt.Width > 0: + width = int(tt.Width * cw.Scale * 100) + + cat = tt.WheelFriction.get_translated_ui_name(language) + if cat not in wheel_cat: + wheel_cat[cat] = {} + wheel_cat[cat][tt.UiName] = (damage_cap, width, mass, tt) + # Mud_Performance= + # Truck_Weight / (Tire_Count×(Tire_Diameter×Width×Softness)) * Tire_Mud_Rating + + for cat in sorted(wheel_cat): + truck_content += f"

{cat}

\n" + truck_content += f" \n" + truck_content += f" \n" + truck_content += f" \n" + for damage_cap, width, mass, tt in sorted( + wheel_cat[cat].values(), key=lambda v: v[3].get_translated_ui_name(language) + ): + truck_content += f" \n" + + truck_content += f"
 Friction
NamePriceMassWidthAsphaltOff-RoadMud
{tt.get_translated_ui_name(language)}{tt.GameData.Price}{mass}kg{width}cm{tt.WheelFriction.BodyFrictionAsphalt}{tt.WheelFriction.BodyFriction}{tt.WheelFriction.SubstanceFriction}
\n" + + scales = ", ".join(['%d"' % x for x in sorted(scales)]) + truck_content += f"

Available Tire Sizes: {scales}

\n" + + write_html( + truck_file, + language=language, + content=truck_content, + category="Trucks", + name=truck.get_translated_ui_name(language), + ) + + index_content = "

Trucks

\n" + for truck_type in sorted(truck_types): + index_content += f"

{truck_type}

\n" + index_content += f" \n" + index_content += f" \n" + for reference, truck in sorted(truck_types[truck_type], key=lambda v: v[1].get_translated_ui_name(language)): + index_content += f" \n" + index_content += f"
Name
{truck.get_translated_ui_name(language)}
\n" + + write_html( + os.path.join(destination_directory, "index.html"), language=language, content=index_content, category="Trucks" + ) + + +def create_html(pak: InitialPakLoader, destination_directory: str = "output/"): + if not os.path.exists(destination_directory): + os.mkdir(destination_directory) + + logger.info("Writing HTML Output") + for language_name, language_data in pak.languages.items(): + logger.info(f" - {language_name}") + language_path = os.path.join(destination_directory, language_name) + if not os.path.exists(language_path): + os.mkdir(language_path) + + write_trucks(pak=pak, destination_directory=language_path, language=language_data) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6cf4eb4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +charset-normalizer \ No newline at end of file