From fe6e55a8910bcb3dbf074247fb62aa3f33860f52 Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Mon, 24 Aug 2026 12:19:48 +0200 Subject: [PATCH] trucks and addons --- constants.py | 64 +++++++++ loader.py | 399 +++++++++++++++++++++++++++++++++++++++++++-------- output.py | 383 ++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 755 insertions(+), 91 deletions(-) create mode 100644 constants.py diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..779e682 --- /dev/null +++ b/constants.py @@ -0,0 +1,64 @@ +COUNTRYS = {} + +DLC_NAMES = { + None: "Base Game", + "dlc_1_1": "Season 1: Search & Recover
Kola Peninsula, Russia", + "dlc_1_2": "Clasico Pack", + "dlc_2_1": "Season 2: Explore & Expand
Yukon, Canada", + "dlc_2_2": "Season 2: Explore & Expand", + "dlc_3": "Season 3: Locate & Deliver
Wisconsin, USA", + "DLC_trial_6": "GMC Brigadier", + "dlc_4": "Season 4: New Frontiers
Amur, Russia", + "dlc_4_1": "Anniversary DLC", + "dlc_5": "Season 5: Build & Dispatch
Don, Russia", + "dlc_6": "Season 6: Haul & Hustle
Maine, USA", + "dlc_7": "Season 7: Compete & Conquer
Tennessee, USA", + "dlc_8": "Season 8: Grand Harvest
Belozersk Glades, Central Asia", + "dlc_9": "Season 9: Renew & Rebuild
Ontario, Canada", + "dlc_10": "Season 10: Fix & Connect
British Columbia, Canada", + "dlc_11": "Season 11: Light & Cameras
Scandinavia, Northern Europe", + "dlc_12": "Season 12: Public Energy
North Carolina, USA", + "dlc_12_5": "The Atom", + "dlc_13": "Season 13: Dig & Drill
Almaty Region, Central Asia", + "dlc_13_5": "Rezvani Tank", + "dlc_14": "Season 14: Reap & Sow
Austria, Central Europe", + "dlc_14_1": "Kenworth Dual Pack", + "dlc_14_2": "Mack Dual Pack", + "dlc_15": "Season 15: Oil & Dirt
Quebec, Canada", + "dlc_15_5": "Freightliner & Western Star Dual Pack", + "dlc_16": "Season 16: High Voltage
Washington, USA", + "dlc_16_5": "Dragon Wagons Dual Pack", + "dlc_17": "Season 17: Repair & Rescue
Zurdania, Western Asia", + "dlc_17_5": "Mercedes-Benz Trucks Dual Pack 1", + "dlc_18": "Season 18: Patch & Power
Chiapas, Mexico", + "dlc_navistar": "Navistart 5000-MV Tractor", + "dlc_high_roller": "High Roller Pack", + "dlc_ws_49x": "Western Star 49X", + "dlc_tatra": "TATRA Dual Pack", + "dlc_jeep": "Jeep Dual Pack", + "dlc_crocodile": "Crocodile Pack", + "dlc_land_rover": "Land Rover Dual Pack", + "dlc_ws_wolf": "Western Star Wolf Pack", + "dlc_rezvani": "Rezvani Hercules", + "dlc_mastodon": "The Mastodon", + "dlc_w990": "Kenworth W990", +} + +DLC_OVERRIDES = { + "UI_VEHICLE_NAVISTAR_5000_NAME": "dlc_navistar", + "UI_VEHICLE_KHAN_39_MARSHALL_NAME": "dlc_high_roller", + "UI_VEHICLE_WESTERN_STAR_49X_NAME": "dlc_ws_49x", + "UI_VEHICLE_TATRA_805_NAME": "dlc_tatra", + "UI_VEHICLE_TATRA_T813_NAME": "dlc_tatra", + "UI_VEHICLE_JEEP_CJ7_RENEGADE_NAME": "dlc_jeep", + "UI_VEHICLE_JEEP_WRANGLER_NAME": "dlc_jeep", + "UI_VEHICLE_STEP_3364_CROCODILE_NAME": "dlc_crocodile", + "UI_VEHICLE_LAND_ROVER_DEFENDER_110_NAME": "dlc_land_rover", + "UI_VEHICLE_LAND_ROVER_DEFENDER_90_NAME": "dlc_land_rover", + "UI_VEHICLE_WESTERN_STAR_57X_NAME": "dlc_ws_wolf", + "UI_VEHICLE_WESTERN_STAR_NF1424_NAME": "dlc_ws_wolf", + "UI_VEHICLE_WESTERN_STAR_NF1430_NAME": "dlc_ws_wolf", + "UI_VEHICLE_REZVANI_NAME": "dlc_rezvani", + "UI_VEHICLE_ZIKZ_612H_MASTODONT_NAME": "dlc_mastodon", + "UI_VEHICLE_KENWORTH_W990_NAME": "dlc_w990", +} diff --git a/loader.py b/loader.py index 959c0e2..1431a69 100644 --- a/loader.py +++ b/loader.py @@ -1,4 +1,5 @@ from __future__ import annotations +from constants import DLC_OVERRIDES from decimal import Decimal, ROUND_HALF_UP from enum import Enum from html import parser as html_parser @@ -58,6 +59,37 @@ locatestarttagend_tolerant = re.compile( endtagfind = re.compile(r"") +class TruckInfo(str, Enum): + ALWAYS = "UI_TRUCK_INFO_ALWAYS" + CAPABLE = "UI_TRUCK_INFO_CAPABLE" + NOT_AVAILABLE = "UI_TRUCK_INFO_NA" + SWITCHABLE = "UI_TRUCK_INFO_ON_OFF" + + +class TruckAddonCategory(str, Enum): + ALL_WHEEL_DRIVE = "awd" + BUBBLE_HEAD = "bubble_head" + BUMPER = "bumper" + CURTAIN = "curtain" + DIFF_LOCK = "diff_lock" + EXHAUST = "exhaust" + FRAME_ADDON = "frame_addons" + FENDERS = "fenders" + FENDER_FRONT = "fender_front" + FENDER_REAR = "fender_rear" + GRILL = "grill" + HOOD_ORNAMENT = "hood_ornament" + MIRROR_MOUNT = "mirror_mount" + MISCELLANEOUS = "miscellenious" + REAR = "rear" + SNORKEL = "snorkel" + STICKER = "sticker" + STICKER_EXTERIOR = "sticker_exterior" + STICKER_WINDSHIELD = "sticker_windshield" + TOP = "top" + WHEEL_ADDON = "wheel_addon" + + def get_initial_pak_path(): return os.path.join( str(os.path.expanduser("~")), @@ -157,11 +189,12 @@ class SnowrunnerClass: self.pak = pak_loader self.UiDesc = "" self.UiName = "" - self.Price = -1 + self.Price = 0 self.UnlockByExploration = False self.UnlockByObjective = False self.UnlockByRank = -1 self.GameData = None + self.DLC = None def load_desc(self, data: SnowRunnerXNLNode): if "region:default" in data.children: @@ -174,7 +207,10 @@ class SnowrunnerClass: self.UiName = data.get_attr("uiname") def load_game_data(self, data: SnowRunnerXNLNode): - self.GameData = self.pak.load_class(SnowrunnerGameObjectType.GAME_DATA, data) + if self.GameData is None: + self.GameData = self.pak.load_class(SnowrunnerGameObjectType.GAME_DATA, data, dlc_name=self.DLC) + else: + self.GameData.update_data(data) self.Price = self.GameData.Price self.UnlockByExploration = self.GameData.UnlockByExploration self.UnlockByObjective = self.GameData.UnlockByObjective @@ -201,7 +237,11 @@ class SnowrunnerClass: self.__children_handled.add(name.lower()) def load_child( - self, data: SnowRunnerXNLNode, name: str, object_type_override: SnowrunnerGameObjectType | None = None + self, + data: SnowRunnerXNLNode, + name: str, + object_type_override: SnowrunnerGameObjectType | None = None, + always_as_list: bool = False, ): self.__children_handled.add(name.lower()) @@ -216,11 +256,22 @@ class SnowrunnerClass: self.load_game_data(data.children[name.lower()]) return + if always_as_list and not isinstance(data.children[name.lower()], list): + data.children[name.lower()] = [data.children[name.lower()]] + 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()]]) + setattr( + self, + name, + [self.pak.load_class(object_type, x, dlc_name=self.DLC) for x in data.children[name.lower()]], + ) else: - setattr(self, name, self.pak.load_class(object_type, data.children[name.lower()])) + setattr( + self, + name, + self.pak.load_class(object_type, data.children[name.lower()], dlc_name=self.DLC), + ) def loading_finished(self, data: SnowRunnerXNLNode): attr_set = set(data.attrs.keys()) @@ -239,6 +290,9 @@ class SnowrunnerClass: ) sys.exit(1) + if self.UiName in DLC_OVERRIDES: + self.DLC = DLC_OVERRIDES[self.UiName] + def get_translated_ui_desc(self, language: LanguageLoader) -> str: if self.UiDesc in language: return language[self.UiDesc] @@ -305,22 +359,30 @@ class AddonSlots(SnowrunnerClass): super().__init__(pak_loader=pak_loader) self.InitialOffset = "(0;0;0)" self.Quantity = 0 + self.UseTrailerFrame = False def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "InitialOffset") self.load_attr(data, "Quantity") + self.load_attr(data, "UseTrailerFrame") + self.ignore_attr("OffsetStep") + self.ignore_attr("ParentFrames") self.loading_finished(data) class AddonSockets(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) - self.DefaultAddon = "" + self.DefaultAddon = None self.Socket = None def load_data(self, data: SnowRunnerXNLNode) -> None: - self.load_attr(data, "DefaultAddon") - self.load_child(data, "Socket") + if "defaultaddon" in data.attrs: + self.DefaultAddon = self.pak.get_object( + object_type=SnowrunnerGameObjectType.TRUCK_ADDON, name=data.attrs["defaultaddon"], lazy=True + ) + self.ignore_attr("defaultaddon") + self.load_child(data, "Socket", always_as_list=True) self.ignore_attr("ParentFrame") self.ignore_attr("RequiredAddonIfNoConflicts") self.loading_finished(data) @@ -364,6 +426,10 @@ class Body(SnowrunnerClass): self.ModelFrame = None self.Body = [] + @property + def Weight(self) -> int: + return self.Mass + sum(x.Weight for x in self.Body) + def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "AllowedPenetrationDepth") self.load_attr(data, "NoSoftContacts") @@ -397,8 +463,7 @@ class Body(SnowrunnerClass): self.ignore_attr("ExplicitParentFrame") self.ignore_attr("NetSync") self.ignore_attr("ParentFrame") - - self.load_child(data, "Body") + self.load_child(data, "Body", always_as_list=True) self.ignore_child("Constraint") self.ignore_child("Motor") self.ignore_child("SideMirror") @@ -489,18 +554,17 @@ class Engine(SnowrunnerClass): class EngineVariants(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) - self.Engines = [] + self.Engine = [] + + def get_engine(self, name: str) -> Engine | None: + for e in self.Engine: + if e.Name == name: + return e + + return None 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.load_child(data, "Engine", always_as_list=True) self.loading_finished(data) @@ -523,10 +587,11 @@ class GameData(SnowrunnerClass): self.Country = "" self.ExcludeAddons = "" self.ExcludedCargoTypes = "" + self.InstallSocket = None self.IsCustomizable = False self.IsDoubleTrailer = False self.IsQuest = False - self.Price = -1 + self.Price = 0 self.UnlockByRank = -1 self.UnlockByExploration = False self.UnlockByObjective = False @@ -544,7 +609,9 @@ class GameData(SnowrunnerClass): def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "CameraPreset") - self.load_attr(data, "Category") + if "category" in data.attrs: + self.Category = TruckAddonCategory(data.attrs["category"]) + self.ignore_attr("Category") self.load_attr(data, "Country") self.load_attr(data, "ExcludeAddons") self.load_attr(data, "ExcludedCargoTypes") @@ -577,8 +644,12 @@ class GameData(SnowrunnerClass): self.ignore_attr("ResetTruckAfterGateway") self.children = data.children - self.load_child(data, "AddonSockets") + self.load_child(data, "AddonSockets", always_as_list=True) self.load_child(data, "UiDesc") + if "installsocket" in data.children: + self.InstallSocket = data.children["installsocket"].attrs.get("type", self.InstallSocket) + self.ignore_child("InstallSocket") + self.ignore_child("AddonSlots") self.ignore_child("AddonType") self.ignore_child("CenteringAngularVelocity") @@ -586,7 +657,6 @@ class GameData(SnowrunnerClass): 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") @@ -617,6 +687,9 @@ class GameData(SnowrunnerClass): self.ignore_child("longlogsaligndata") self.loading_finished(data) + def update_data(self, data: SnowRunnerXNLNode) -> None: + self.load_data(data) + class Gear(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): @@ -645,7 +718,7 @@ class Gearbox(SnowrunnerClass): self.GearboxParams = None self.ReverseGear = None self.HighGear = None - self.Gears = [] + self.Gear = [] def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "AWDConsumptionModifier") @@ -660,10 +733,12 @@ class Gearbox(SnowrunnerClass): 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, "Gear", always_as_list=True) self.load_child(data, "ReverseGear", object_type_override=SnowrunnerGameObjectType.GEAR) self.load_child(data, "HighGear", object_type_override=SnowrunnerGameObjectType.GEAR) + self.Gear = list(sorted(self.Gear, key=lambda gear: gear.AngVel)) + self.loading_finished(data) @@ -690,8 +765,15 @@ class GearboxVariants(SnowrunnerClass): super().__init__(pak_loader=pak_loader) self.Gearbox = [] + def get_gearbox(self, name: str) -> Gearbox | None: + for e in self.Gearbox: + if e.Name == name: + return e + + return None + def load_data(self, data: SnowRunnerXNLNode) -> None: - self.load_child(data, "Gearbox") + self.load_child(data, "Gearbox", always_as_list=True) self.loading_finished(data) def update_data(self, data: SnowRunnerXNLNode) -> None: @@ -700,7 +782,7 @@ class GearboxVariants(SnowrunnerClass): 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)) + self.Gearbox.append(self.pak.load_class(SnowrunnerGameObjectType.GEARBOX, upd, dlc_name=self.DLC)) elif upd is None: return else: @@ -766,6 +848,15 @@ class PhysicsModel(SnowrunnerClass): self.Mesh = "" self.Body = None + @property + def Weight(self) -> int: + if not self.Body: + return 0 + if isinstance(self.Body, list): + return sum(x.Weight for x in self.Body) + else: + return self.Body.Weight + def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "_noinherit") self.load_attr(data, "Mesh") @@ -871,8 +962,15 @@ class SuspensionSetVariants(SnowrunnerClass): super().__init__(pak_loader=pak_loader) self.SuspensionSet = [] + def get_suspension_set(self, name: str) -> SuspensionSet | None: + for e in self.SuspensionSet: + if e.Name == name: + return e + + return None + def load_data(self, data: SnowRunnerXNLNode) -> None: - self.load_child(data, "SuspensionSet") + self.load_child(data, "SuspensionSet", always_as_list=True) self.loading_finished(data) @@ -891,6 +989,98 @@ class Truck(SnowrunnerClass): self.Type = "Truck" self.WaterMass = None + @property + def TireCount(self) -> int: + return len(self.TruckData.Wheels.Wheel) + + @property + def TireCountFront(self) -> int: + return len([x for x in self.TruckData.Wheels.Wheel if x.Location == "front"]) + + @property + def TireCountRear(self) -> int: + return len([x for x in self.TruckData.Wheels.Wheel if x.Location == "rear"]) + + @property + def DriveTrain(self) -> str: + undriven_axles = len([x for x in self.TruckData.Wheels.Wheel if x.Torque == "none"]) + return f"{self.TireCount}x{self.TireCount - undriven_axles}" + + @property + def Weight(self) -> int: + wheel_mass = ( + self.TruckData.Wheels.DefaultTire.Mass * self.TireCountFront + + self.TruckData.Wheels.DefaultTire.Mass + * self.TruckData.Wheels.DefaultTire.RearMassScale + * self.TireCountRear + ) + default_addon_mass = sum(x.PhysicsModel.Weight for x in self.get_default_truck_addons()) + + return int(self.PhysicsModel.Weight + wheel_mass + default_addon_mass) + + def get_shop_price(self) -> int: + return ( + self.Price + + self.TruckData.DefaultEngine.Price + + self.TruckData.DefaultGearbox.Price + + self.TruckData.DefaultSuspension.Price + + self.TruckData.DefaultWinch.Price + + self.TruckData.Wheels.DefaultTire.Price + + sum(x.DefaultAddon.Price for x in self.GameData.AddonSockets if x.DefaultAddon is not None) + ) + + def get_addon_socket_names(self) -> list[str]: + return [s.Names for s in [y for x in self.GameData.AddonSockets for y in x.Socket]] + + def get_available_truck_addons(self, category: TruckAddonCategory) -> list[TruckAddon]: + return [ + ta + for ta in self.pak.get_object_list(SnowrunnerGameObjectType.TRUCK_ADDON) + if ta.GameData.InstallSocket in self.get_addon_socket_names() and ta.GameData.Category == category + ] + + def get_default_truck_addons(self) -> list[TruckAddon]: + return [x.DefaultAddon for x in self.GameData.AddonSockets if x.DefaultAddon is not None] + + @property + def AWDInfo(self) -> TruckInfo: + torques = {x.Torque.lower() for x in self.TruckData.Wheels.Wheel} + if len(torques) == 1 and "default" in torques: + return TruckInfo.ALWAYS + elif "connectable" in torques: + return TruckInfo.CAPABLE + elif "full" in torques: + return TruckInfo.SWITCHABLE + elif "none" in torques: + return TruckInfo.NOT_AVAILABLE + else: + raise ValueError(f"Truck {self.UiName} AWD Wheel Torques {torques} is not supported") + + @property + def DiffLockInfo(self) -> TruckInfo: + if self.TruckData.DiffLockType == "Always": + return TruckInfo.ALWAYS + elif ( + self.TruckData.DiffLockType == "Installed" + or self.TruckData.DiffLockType == "Switchable" + or self.TruckData.DiffLockType == "Connected" + or self.TruckData.DiffLockType == "Uninstalled" + or self.TruckData.DiffLockType == "None" + ): # DiffLockType seems to have no meaning. At all. We have to figure it out ourselfs. + diff_lock_addons = self.get_available_truck_addons(TruckAddonCategory.DIFF_LOCK) + if len(diff_lock_addons) == 0: + return TruckInfo.NOT_AVAILABLE + elif len(diff_lock_addons) == 1: + return TruckInfo.SWITCHABLE + else: + installed_addon = list(set(diff_lock_addons) & set(self.get_default_truck_addons())) + if installed_addon[0].TruckData.DiffLockInstalled: + return TruckInfo.SWITCHABLE + else: + return TruckInfo.CAPABLE + else: + raise ValueError(f"Truck {self.UiName} DiffLockType {self.TruckData.DiffLockType} is not supported") + def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "AttachType") self.load_attr(data, "IsDeprecated") @@ -944,6 +1134,7 @@ class TruckAddon(SnowrunnerClass): self.PhysicsModel = None self.TruckData = None self.AddonType = None + self.AddonSlots = None self.RequiredAddonType = None self.IsChassisFullOcclusion = False self.FuelMass = None @@ -966,7 +1157,7 @@ class TruckAddon(SnowrunnerClass): 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") + self.load_child(data.children["gamedata"], "AddonSlots") if "addontype" in data.children["gamedata"].children: self.load_child(data.children["gamedata"], "AddonType") if "requiredaddon" in data.children["gamedata"].children: @@ -1007,12 +1198,25 @@ class TruckAddon(SnowrunnerClass): class TruckData(SnowrunnerClass): + truck_types = { + "HEAVY": "CODEX_TYPE_TRUCK_6_HEADER", + "HEAVY_DUTY": "CODEX_TYPE_TRUCK_4_HEADER", + "SCOUT": "CODEX_TYPE_TRUCK_3_HEADER", + "HIGHWAY": "CODEX_TYPE_TRUCK_2_HEADER", + "OFFROAD": "CODEX_TYPE_TRUCK_5_HEADER", + } + 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.DefaultEngine = None + self.DefaultGearbox = None + self.DefaultSuspension = None + self.DefaultTire = None + self.DefaultWinch = None self.DiffLockInstalled = False self.DiffLockType = "" self.Engines = [] @@ -1041,12 +1245,12 @@ class TruckData(SnowrunnerClass): self.load_attr(data, "Responsiveness") self.load_attr(data, "SetHandbrakeOnWinchRelease") self.load_attr(data, "SteerSpeed") - self.load_attr(data, "TruckType") + if "trucktype" in data.attrs: + self.TruckType = self.truck_types[data.attrs["trucktype"]] + self.ignore_attr("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, "CompatibleWheels", always_as_list=True) self.load_child(data, "Damage") self.load_child(data, "Wheels") @@ -1058,6 +1262,9 @@ class TruckData(SnowrunnerClass): self.pak.get_object(object_type=SnowrunnerGameObjectType.ENGINE_VARIANTS, name=n.strip()) for n in data.children["enginesocket"].attrs["type"].split(",") ] + self.DefaultEngine = list( + {x.get_engine(data.children["enginesocket"].attrs["default"]) for x in self.Engines} - {None} + )[0] self.ignore_child("EngineSocket") if "gearboxsocket" in data.children: @@ -1065,6 +1272,9 @@ class TruckData(SnowrunnerClass): self.pak.get_object(object_type=SnowrunnerGameObjectType.GEARBOX_VARIANTS, name=n.strip()) for n in data.children["gearboxsocket"].attrs["type"].split(",") ] + self.DefaultGearbox = list( + {x.get_gearbox(data.children["gearboxsocket"].attrs["default"]) for x in self.Gearboxes} - {None} + )[0] self.ignore_child("GearboxSocket") if "suspensionsocket" in data.children: @@ -1072,6 +1282,10 @@ class TruckData(SnowrunnerClass): self.pak.get_object(object_type=SnowrunnerGameObjectType.SUSPENSION_SET_VARIANTS, name=n.strip()) for n in data.children["suspensionsocket"].attrs["type"].split(",") ] + self.DefaultSuspension = list( + {x.get_suspension_set(data.children["suspensionsocket"].attrs["default"]) for x in self.Suspensions} + - {None} + )[0] self.ignore_child("SuspensionSocket") if "winchupgradesocket" in data.children: @@ -1079,6 +1293,9 @@ class TruckData(SnowrunnerClass): self.pak.get_object(object_type=SnowrunnerGameObjectType.WINCH_VARIANTS, name=n.strip()) for n in data.children["winchupgradesocket"].attrs["type"].split(",") ] + self.DefaultWinch = list( + {x.get_winch(data.children["winchupgradesocket"].attrs["default"]) for x in self.Winches} - {None} + )[0] self.ignore_child("WinchUpgradeSocket") self.ignore_attr("_noinherit") @@ -1150,9 +1367,7 @@ class TruckTires(SnowrunnerClass): 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.load_child(data, "TruckTire", always_as_list=True) self.loading_finished(data) def update_data(self, data: SnowRunnerXNLNode) -> None: @@ -1161,7 +1376,7 @@ class TruckTires(SnowrunnerClass): 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)) + self.TruckTire.append(self.pak.load_class(SnowrunnerGameObjectType.TRUCK_TIRE, upd, dlc_name=self.DLC)) elif upd is None: return else: @@ -1202,6 +1417,13 @@ class TruckWheels(SnowrunnerClass): self.WidthRear = -1.0 self.TruckTires = None + def get_truck_tire(self, name: str) -> TruckTire | None: + for x in self.TruckTires.TruckTire: + if x.Name == name: + return x + + return None + def load_data(self, data: SnowRunnerXNLNode) -> None: self.load_attr(data, "DamageCapacity") self.load_attr(data, "Mass") @@ -1276,12 +1498,21 @@ class Wheels(SnowrunnerClass): def __init__(self, pak_loader: InitialPakLoader): super().__init__(pak_loader=pak_loader) self.Wheel = None + self.DefaultTire = None + self.DefaultWheelType = None def load_data(self, data: SnowRunnerXNLNode) -> None: - self.ignore_attr("DefaultWheelType") + if "defaultwheeltype" in data.attrs: + self.DefaultWheelType = self.pak.get_object( + SnowrunnerGameObjectType.TRUCK_WHEELS, name=data.attrs["defaultwheeltype"] + ) + self.ignore_attr("DefaultWheelType") + if "defaulttire" in data.attrs: + self.DefaultTire = self.DefaultWheelType.get_truck_tire(data.attrs["defaulttire"]) + self.ignore_attr("DefaultTire") + self.ignore_attr("DefaultRim") - self.ignore_attr("DefaultTire") - self.load_child(data, "Wheel") + self.load_child(data, "Wheel", always_as_list=True) self.loading_finished(data) @@ -1349,8 +1580,15 @@ class WinchVariants(SnowrunnerClass): super().__init__(pak_loader=pak_loader) self.Winch = [] + def get_winch(self, name: str) -> Winch | None: + for e in self.Winch: + if e.Name == name: + return e + + return None + def load_data(self, data: SnowRunnerXNLNode) -> None: - self.load_child(data, "Winch") + self.load_child(data, "Winch", always_as_list=True) self.load_child(data, "WinchParams") self.loading_finished(data) @@ -1366,6 +1604,15 @@ class LanguageLoader(dict): value = parts[-1].replace('"', "").replace("\\", '"') self[key] = value + def get_sanitized(self, key: str, default: str | None = None) -> str: + if key not in self: + if default is not None: + return default + else: + raise KeyError(key) + + return " ".join(x[0] + x[1:].lower() for x in re.split(r"[-_ ]+", self[key])) + TYPE_CLASS_MATRIX = { "addonslots": AddonSlots, @@ -1449,6 +1696,7 @@ class SnowrunnerGameObjectType(str, Enum): WINCH_PARAMS = "winchparams" WINCH_VARIANTS = "winchvariants" + @property def snowrunner_class(self): return TYPE_CLASS_MATRIX[self.value] @@ -1532,6 +1780,7 @@ class InitialPakLoader: self.pak_zip = get_initial_pak_zipfile(get_initial_pak_path()) self.is_loaded = False self.languages = {} + self.dlc_names = {x: {} for x in SnowrunnerGameObjectType} self.raw_data = {x: {} for x in SnowrunnerGameObjectType} self.game_data = {x: {} for x in SnowrunnerGameObjectType} self.templates = {x: {} for x in SnowrunnerGameObjectType} @@ -1550,6 +1799,7 @@ class InitialPakLoader: self, object_type: SnowrunnerGameObjectType, data: SnowRunnerXNLNode, + dlc_name: str, parent: str | None = None, lazy: bool = False, ) -> SnowrunnerClass | list[SnowrunnerClass] | None: @@ -1557,37 +1807,40 @@ class InitialPakLoader: 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 = object_type.snowrunner_class.from_template(pak_loader=self, template=template) + ret.DLC = dlc_name 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) + ret = object_type.snowrunner_class(pak_loader=self) else: - ret = object_type.snowrunner_class().from_template( + ret = object_type.snowrunner_class.from_template( pak_loader=self, template=self.get_template(object_type=object_type, name=data.attrs["_template"]) ) + ret.DLC = dlc_name 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)) + ret.append(object_type.snowrunner_class(pak_loader=self)) else: ret.append( - object_type.snowrunner_class().from_template( + object_type.snowrunner_class.from_template( pak_loader=self, template=self.get_template(object_type=object_type, name=sub_data.attrs["_template"]), ) ) + ret[-1].DLC = dlc_name ret[-1].load_data(sub_data) return ret - def load_templates(self, data: dict, local: bool = False) -> None: + def load_templates(self, data: dict, dlc_name: str, local: bool = False) -> None: if local: self.templates_local = {x: {} for x in SnowrunnerGameObjectType} @@ -1610,18 +1863,26 @@ class InitialPakLoader: continue if local: self.templates_local[category][tp_name] = self.load_class( - object_type=object_type, data=sub_data + object_type=object_type, + data=sub_data, + dlc_name=dlc_name, ) else: - self.templates[category][tp_name] = self.load_class(object_type=object_type, data=sub_data) + self.templates[category][tp_name] = self.load_class( + object_type=object_type, data=sub_data, dlc_name=dlc_name + ) 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 + object_type=object_type, + data=sub_data, + dlc_name=dlc_name, ) else: - self.templates[category][tp_name] = self.load_class(object_type=object_type, data=sub_data) + self.templates[category][tp_name] = self.load_class( + object_type=object_type, data=sub_data, dlc_name=dlc_name + ) elif category in self.ignore_categories: pass # Ignore for now @@ -1657,6 +1918,9 @@ class InitialPakLoader: logger.error(f"Unknown Game Data Object requested: {name} in {object_type.name}") sys.exit(1) + def get_object_list(self, object_type: SnowrunnerGameObjectType) -> list: + return list(self.game_data[object_type].values()) + def get_sorted_object_list( self, object_type: SnowrunnerGameObjectType, language: LanguageLoader ) -> list[tuple[str, SnowrunnerClass]]: @@ -1664,12 +1928,13 @@ class InitialPakLoader: 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): + def store_raw(self, ref: str, dlc_name: 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 + self.dlc_names[SnowrunnerGameObjectType(list(cat_in_matrix)[0])][ref] = dlc_name elif cat_in_ignore: pass # not sure what to do with this, yet else: @@ -1686,6 +1951,8 @@ class InitialPakLoader: for member in self.pak_zip.infolist(): if not member.filename.endswith(".xml"): continue + + dlc_name = member.filename.split("\\")[2] if "_dlc" in member.filename else None filename_ref = member.filename.split("\\")[-1].replace(".xml", "") logger.info(f" - {member.filename}") with self.pak_zip.open(member) as d_file: @@ -1695,13 +1962,13 @@ class InitialPakLoader: if len(data) == 1 and "_templates" in data: logger.info(f" - -> Loading templates from {member.filename}") - self.load_templates(data["_templates"].children) + self.load_templates(data["_templates"].children, dlc_name=dlc_name) continue elif len(data) > 3: logger.error(f"Multiple keys?! ({list(data.keys())}") sys.exit(1) - self.store_raw(filename_ref, data) + self.store_raw(filename_ref, dlc_name, data) deferred_loads = [] logger.info("Loading GameData") @@ -1715,9 +1982,13 @@ class InitialPakLoader: logger.info(f" - {ref}") if "_templates" in data: - self.load_templates(data["_templates"].children, local=True) + self.load_templates( + data["_templates"].children, local=True, dlc_name=self.dlc_names[object_type][ref] + ) - self.game_data[object_type][ref] = self.load_class(object_type, data[category]) + self.game_data[object_type][ref] = self.load_class( + object_type, data[category], dlc_name=self.dlc_names[object_type][ref] + ) del self.raw_data[object_type] @@ -1726,9 +1997,15 @@ class InitialPakLoader: for object_type, ref, data in deferred_loads: logger.info(f" - {ref}") if "_templates" in data: - self.load_templates(data["_templates"].children, local=True) + self.load_templates( + data["_templates"].children, local=True, dlc_name=self.dlc_names[object_type][ref] + ) sr_class = self.load_class( - object_type, data[category], parent=data["_parent"].get_attr("file"), lazy=True + object_type, + data[category], + parent=data["_parent"].get_attr("file"), + lazy=True, + dlc_name=self.dlc_names[object_type][ref], ) if sr_class is None: still_deferred_loads.append((object_type, ref, data)) @@ -1743,7 +2020,9 @@ class InitialPakLoader: 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]) + self.game_data[object_type][ref] = self.load_class( + object_type, data[category], dlc_name=self.dlc_names[object_type][ref] + ) deferred_loads = [] for cat, v in self.raw_data.items(): diff --git a/output.py b/output.py index f0f00b6..8879051 100644 --- a/output.py +++ b/output.py @@ -1,6 +1,14 @@ from __future__ import annotations import os -from loader import InitialPakLoader, LanguageLoader, SnowrunnerGameObjectType, SnowrunnerClass, GameData +from constants import DLC_NAMES +from loader import ( + InitialPakLoader, + LanguageLoader, + SnowrunnerGameObjectType, + TruckAddonCategory, + GameData, + SnowrunnerClass, +) from logging import getLogger logger = getLogger(__name__) @@ -13,13 +21,17 @@ def get_head(language: LanguageLoader, category: str, name: str | None = None) - SnowRunnerDB - {category}""" + (f""" - {name}""" if name is not None else "") + f""" + + +
@@ -44,50 +56,154 @@ def write_html( def write_trucks(pak: InitialPakLoader, destination_directory: str, language: LanguageLoader): - index_table = "" - - truck_types = {} + index_content = "

Trucks

\n" + index_content += f" \n" + index_content += ( + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + ) + index_content += f" \n" 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)}

" + truck_content += f"
NameClassDLCUnlock byRegionPriceWeightDrivetrainAWDDiffLock
\n" + try: + truck_content += f" \n" + except KeyError: + print(f"DEBUG: DLC {truck.DLC} for Truck {truck.get_translated_ui_name(language)}") + exit(1) + truck_content += f" \n" + truck_content += f" \n" + truck_content += f" \n" + truck_content += f" \n" + truck_content += f" \n" + truck_content += f" \n" + truck_content += f" \n" + truck_content += f" \n" + truck_content += f"
DLC{DLC_NAMES[truck.DLC]}
Weight{truck.Weight}kg
AWD{language[truck.AWDInfo.value]}
DiffLock{language[truck.DiffLockInfo.value]}
Def Engine{truck.TruckData.DefaultEngine.get_translated_ui_name(language)}
Def Gearbox{truck.TruckData.DefaultGearbox.get_translated_ui_name(language)}
Def Suspension{truck.TruckData.DefaultSuspension.get_translated_ui_name(language)}
Def Winch{truck.TruckData.DefaultWinch.get_translated_ui_name(language)}
Def Tire{truck.TruckData.Wheels.DefaultTire.get_translated_ui_name(language)}
\n" # 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" + engines = [x for y in truck.TruckData.Engines for x in y.Engine] + truck_content += f"
NamePriceTorqueBase Fuel ConsumptionUnlock By
\n" + truck_content += ( + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + 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" + f" " + f" " + f" " + f" " + f" " + f" \n" ) - truck_content += f"
NamePriceUnlock ByBase Fuel ConsumptionTorquePower-to-Weight
{engine.get_translated_ui_name(language)}{engine.GameData.Price}" - f"{engine.Torque}{engine.FuelConsumption}{engine.GameData.get_unlock_str()}
{engine.get_translated_ui_name(language)}{engine.GameData.Price}" + f" {engine.GameData.get_unlock_str()}{engine.FuelConsumption}{engine.Torque}{engine.Torque / truck.Weight:.2f}
\n" + truck_content += f" \n" # Gearboxes + gearboxes = [y for x in truck.TruckData.Gearboxes for y in x.Gearbox] + max_gear_count = max(len(x.Gear) for x in gearboxes) + truck_content += f"

Gearboxes

\n" + truck_content += f"

General

\n" + truck_content += f" \n" + truck_content += ( + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + ) + truck_content += f" \n" + for gearbox in sorted(gearboxes, key=lambda x: x.get_translated_ui_name(language)): + truck_content += f" \n" + truck_content += f" \n" + truck_content += f" \n" + truck_content += f" \n" + truck_content += ( + f" \n" + ) + truck_content += ( + f" \n" + ) + truck_content += ( + f" \n" + ) + truck_content += ( + f" \n" + ) + truck_content += ( + f" \n" + ) + truck_content += f" \n" + truck_content += f" \n" + truck_content += f"
NamePriceUnlock ByFine-TunableL-LL+HGears
{gearbox.get_translated_ui_name(language)}{gearbox.Price}{gearbox.GameData.get_unlock_str()}{'✓' if gearbox.GearboxParams.IsManualLowGear else '˙'}{'✓' if gearbox.GearboxParams.IsLowerMinusGearExists else '˙'}{'✓' if gearbox.GearboxParams.IsLowerGearExists else '˙'}{'✓' if gearbox.GearboxParams.IsLowerPlusGearExists else '˙'}{'✓' if gearbox.GearboxParams.IsHighGearExists else '˙'}{len(gearbox.Gear)}
\n" + + truck_content += f"

Fuel Consumption

\n" + truck_content += f" \n" + truck_content += ( + f" \n" + f" \n" + f" \n" + f" \n" + ) + for i in range(max_gear_count): + gear_num = str(i + 1) if i > 0 else "L+/1" + truck_content += f" \n" + truck_content += f" \n" + truck_content += f" \n" + + for gearbox in sorted(gearboxes, key=lambda x: x.get_translated_ui_name(language)): + truck_content += f" \n" + truck_content += f" \n" + truck_content += f" \n" + truck_content += f" \n" + for i in range(max_gear_count): + if i >= len(gearbox.Gear): + truck_content += f" \n" + else: + truck_content += f" \n" + truck_content += f" \n" + + truck_content += f"
NameFuel ConsumptionAWD Fuel Modifier{gear_num}
{gearbox.get_translated_ui_name(language)}{gearbox.FuelConsumption}{gearbox.AWDConsumptionModifier}-{gearbox.Gear[i].FuelModifier}
\n" + # Suspensions truck_content += f"

Suspensions

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

Winches

\n" # Wheels - truck_content += f"

Wheels

\n" + truck_content += f"

Tires

\n" wheel_cat = {} scales = set() for cw in truck.TruckData.CompatibleWheels: @@ -124,6 +240,124 @@ def write_trucks(pak: InitialPakLoader, destination_directory: str, language: La scales = ", ".join(['%d"' % x for x in sorted(scales)]) truck_content += f"

Available Tire Sizes: {scales}

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

Winches

\n" + + # Diff. Lock + diff_lock_addons = truck.get_available_truck_addons(TruckAddonCategory.DIFF_LOCK) + if diff_lock_addons: + truck_content += f"

Diff. Lock

\n" + truck_content += f" \n" + truck_content += ( + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + ) + for addon in sorted(diff_lock_addons, key=lambda v: v.get_translated_ui_name(language)): + truck_content += ( + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + ) + truck_content += f"
NamePriceUnlock By
{addon.get_translated_ui_name(language)}{addon.Price}{addon.GameData.get_unlock_str()}
\n" + + # Spare Wheel (if any) + # Snorkel (if any) + snorkel_addons = truck.get_available_truck_addons(TruckAddonCategory.SNORKEL) + if snorkel_addons: + truck_content += f"

Snorkel

\n" + truck_content += f" \n" + truck_content += ( + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + ) + for addon in sorted(snorkel_addons, key=lambda v: v.get_translated_ui_name(language)): + truck_content += ( + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + ) + truck_content += f"
NamePriceUnlock ByMass
{addon.get_translated_ui_name(language)}{addon.Price}{addon.GameData.get_unlock_str()}{addon.PhysicsModel.Weight}kg
\n" + + # AWD + awd_addons = truck.get_available_truck_addons(TruckAddonCategory.ALL_WHEEL_DRIVE) + if awd_addons: + truck_content += f"

AWD

\n" + truck_content += f" \n" + truck_content += ( + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + ) + for addon in sorted(awd_addons, key=lambda v: v.get_translated_ui_name(language)): + truck_content += ( + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + ) + truck_content += f"
NamePriceUnlock ByMass
{addon.get_translated_ui_name(language)}{addon.Price}{addon.GameData.get_unlock_str()}{addon.PhysicsModel.Weight}kg
\n" + + # Frame Addons + + truck_content += f"

Frame Addons

\n" + truck_content += f" \n" + truck_content += ( + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + ) + for truck_addon in sorted( + truck.get_available_truck_addons(TruckAddonCategory.FRAME_ADDON), + key=lambda v: v.get_translated_ui_name(language), + ): + truck_content += f" \n" + truck_content += f" \n" + truck_content += f" \n" + truck_content += f" \n" + truck_content += f" \n" + truck_content += f" \n" + truck_content += f" \n" + truck_content += f" \n" + truck_content += f" \n" + truck_content += f" \n" + truck_content += f" \n" + truck_content += f"
NamePriceUnlock ByMassCargo SlotsRepair PointsSpare WheelsFuel CapacityWater Capacity
{truck_addon.get_translated_ui_name(language)}{truck_addon.Price}{truck_addon.GameData.get_unlock_str()}{truck_addon.PhysicsModel.Weight}kg{truck_addon.AddonSlots.Quantity if truck_addon.AddonSlots is not None else ' '}{truck_addon.TruckData.RepairsCapacity if truck_addon.TruckData is not None and truck_addon.TruckData.RepairsCapacity > 0 else ' '}{truck_addon.TruckData.WheelRepairsCapacity if truck_addon.TruckData is not None and truck_addon.TruckData.WheelRepairsCapacity > 0 else ' '}{truck_addon.TruckData.FuelCapacity if truck_addon.TruckData is not None and truck_addon.TruckData.FuelCapacity > 0 else ' '}{truck_addon.TruckData.WaterCapacity if truck_addon.TruckData is not None and truck_addon.TruckData.WaterCapacity > 0 else ' '}
\n" + + truck_content += f" \n" + write_html( truck_file, language=language, @@ -132,20 +366,99 @@ def write_trucks(pak: InitialPakLoader, destination_directory: str, language: La 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" + index_content += ( + f" \n" + f" {truck.get_translated_ui_name(language)}\n" + f" {language.get_sanitized(truck.TruckData.TruckType)}\n" + f" {DLC_NAMES[truck.DLC]}\n" + f" {truck.GameData.get_unlock_str()}\n" + f" {truck.GameData.Country}\n" + f" {truck.get_shop_price()}\n" + f" {truck.Weight}\n" + f" {truck.DriveTrain}\n" + f" {language[truck.AWDInfo.value]}\n" + f" {language[truck.DiffLockInfo.value]}\n" + f" \n" + ) + + index_content += f" \n" + index_content += " \n" write_html( os.path.join(destination_directory, "index.html"), language=language, content=index_content, category="Trucks" ) +def write_frame_addons(pak: InitialPakLoader, destination_directory: str, language: LanguageLoader): + index_content = "

Frame Addons

\n" + index_content += f" \n" + index_content += ( + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + f" \n" + ) + index_content += f" \n" + + for _, frame_addon in pak.get_sorted_object_list(SnowrunnerGameObjectType.TRUCK_ADDON, language=language): + if frame_addon.GameData.Category != TruckAddonCategory.FRAME_ADDON: + continue + + if ( + "TRANSLATION NOT FOUND" in frame_addon.get_translated_ui_name(language) + or len(frame_addon.get_translated_ui_name(language)) + > 100 # This is a hidden frame addon where somebody swapped UI_NAME and UI_DESC + ): + continue # FIXME: How To better detect Hidden Frame Addons not shown in game? + # Also e.g. Mack Defense has a Spare Wheel Addon that's always installed and not shown in game?! + + available_trucks = [ + x + for x in pak.get_sorted_object_list(SnowrunnerGameObjectType.TRUCK, language=language) + if x[1].Type == "Truck" and frame_addon.GameData.InstallSocket in x[1].get_addon_socket_names() + ] + if not available_trucks: + continue + + index_content += f" \n" + index_content += f" \n" + index_content += f" \n" + index_content += f" \n" + index_content += f" \n" + index_content += f" \n" + index_content += f" \n" + index_content += f" \n" + index_content += f" \n" + index_content += f" \n" + + truck_links = ", ".join( + f'{truck.get_translated_ui_name(language)}' + for ref, truck in available_trucks + ) + index_content += f" \n" + + index_content += f" \n" + + index_content += f" \n" + index_content += f"
NamePriceUnlock ByMassCargo SlotsRepair PointsSpare WheelsFuel CapacityWater CapacityAvailable for
{frame_addon.get_translated_ui_name(language)}{frame_addon.Price}{frame_addon.GameData.get_unlock_str()}{frame_addon.PhysicsModel.Weight}kg{frame_addon.AddonSlots.Quantity if frame_addon.AddonSlots is not None else ' '}{frame_addon.TruckData.RepairsCapacity if frame_addon.TruckData is not None and frame_addon.TruckData.RepairsCapacity > 0 else ' '}{frame_addon.TruckData.WheelRepairsCapacity if frame_addon.TruckData is not None and frame_addon.TruckData.WheelRepairsCapacity > 0 else ' '}{frame_addon.TruckData.FuelCapacity if frame_addon.TruckData is not None and frame_addon.TruckData.FuelCapacity > 0 else ' '}{frame_addon.TruckData.WaterCapacity if frame_addon.TruckData is not None and frame_addon.TruckData.WaterCapacity > 0 else ' '}{truck_links}
\n" + index_content += " \n" + + write_html( + os.path.join(destination_directory, "index.html"), + language=language, + content=index_content, + category="Frame Addons", + ) + + def create_html(pak: InitialPakLoader, destination_directory: str = "output/"): if not os.path.exists(destination_directory): os.mkdir(destination_directory) @@ -157,4 +470,12 @@ def create_html(pak: InitialPakLoader, destination_directory: str = "output/"): if not os.path.exists(language_path): os.mkdir(language_path) - write_trucks(pak=pak, destination_directory=language_path, language=language_data) + truck_path = os.path.join(destination_directory, language_name, "trucks") + if not os.path.exists(truck_path): + os.mkdir(truck_path) + write_trucks(pak=pak, destination_directory=truck_path, language=language_data) + + frame_addon_path = os.path.join(destination_directory, language_name, "frame_addons") + if not os.path.exists(frame_addon_path): + os.mkdir(frame_addon_path) + write_frame_addons(pak=pak, destination_directory=frame_addon_path, language=language_data)