trucks and addons

This commit is contained in:
Stefan Harmuth 2026-08-24 12:19:48 +02:00
parent 1780362b90
commit fe6e55a891
3 changed files with 755 additions and 91 deletions

64
constants.py Normal file
View File

@ -0,0 +1,64 @@
COUNTRYS = {}
DLC_NAMES = {
None: "Base Game",
"dlc_1_1": "Season 1: Search & Recover<br>Kola Peninsula, Russia",
"dlc_1_2": "Clasico Pack",
"dlc_2_1": "Season 2: Explore & Expand<br>Yukon, Canada",
"dlc_2_2": "Season 2: Explore & Expand",
"dlc_3": "Season 3: Locate & Deliver<br>Wisconsin, USA",
"DLC_trial_6": "GMC Brigadier",
"dlc_4": "Season 4: New Frontiers<br>Amur, Russia",
"dlc_4_1": "Anniversary DLC",
"dlc_5": "Season 5: Build & Dispatch<br>Don, Russia",
"dlc_6": "Season 6: Haul & Hustle<br>Maine, USA",
"dlc_7": "Season 7: Compete & Conquer<br>Tennessee, USA",
"dlc_8": "Season 8: Grand Harvest<br>Belozersk Glades, Central Asia",
"dlc_9": "Season 9: Renew & Rebuild<br>Ontario, Canada",
"dlc_10": "Season 10: Fix & Connect<br>British Columbia, Canada",
"dlc_11": "Season 11: Light & Cameras<br>Scandinavia, Northern Europe",
"dlc_12": "Season 12: Public Energy<br>North Carolina, USA",
"dlc_12_5": "The Atom",
"dlc_13": "Season 13: Dig & Drill<br>Almaty Region, Central Asia",
"dlc_13_5": "Rezvani Tank",
"dlc_14": "Season 14: Reap & Sow<br>Austria, Central Europe",
"dlc_14_1": "Kenworth Dual Pack",
"dlc_14_2": "Mack Dual Pack",
"dlc_15": "Season 15: Oil & Dirt<br>Quebec, Canada",
"dlc_15_5": "Freightliner & Western Star Dual Pack",
"dlc_16": "Season 16: High Voltage<br>Washington, USA",
"dlc_16_5": "Dragon Wagons Dual Pack",
"dlc_17": "Season 17: Repair & Rescue<br>Zurdania, Western Asia",
"dlc_17_5": "Mercedes-Benz Trucks Dual Pack 1",
"dlc_18": "Season 18: Patch & Power<br>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",
}

397
loader.py
View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
from constants import DLC_OVERRIDES
from decimal import Decimal, ROUND_HALF_UP from decimal import Decimal, ROUND_HALF_UP
from enum import Enum from enum import Enum
from html import parser as html_parser from html import parser as html_parser
@ -58,6 +59,37 @@ locatestarttagend_tolerant = re.compile(
endtagfind = re.compile(r"</\s*([a-zA-Z_][-.a-zA-Z0-9:_]*)\s*>") endtagfind = re.compile(r"</\s*([a-zA-Z_][-.a-zA-Z0-9:_]*)\s*>")
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(): def get_initial_pak_path():
return os.path.join( return os.path.join(
str(os.path.expanduser("~")), str(os.path.expanduser("~")),
@ -157,11 +189,12 @@ class SnowrunnerClass:
self.pak = pak_loader self.pak = pak_loader
self.UiDesc = "" self.UiDesc = ""
self.UiName = "" self.UiName = ""
self.Price = -1 self.Price = 0
self.UnlockByExploration = False self.UnlockByExploration = False
self.UnlockByObjective = False self.UnlockByObjective = False
self.UnlockByRank = -1 self.UnlockByRank = -1
self.GameData = None self.GameData = None
self.DLC = None
def load_desc(self, data: SnowRunnerXNLNode): def load_desc(self, data: SnowRunnerXNLNode):
if "region:default" in data.children: if "region:default" in data.children:
@ -174,7 +207,10 @@ class SnowrunnerClass:
self.UiName = data.get_attr("uiname") self.UiName = data.get_attr("uiname")
def load_game_data(self, data: SnowRunnerXNLNode): 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.Price = self.GameData.Price
self.UnlockByExploration = self.GameData.UnlockByExploration self.UnlockByExploration = self.GameData.UnlockByExploration
self.UnlockByObjective = self.GameData.UnlockByObjective self.UnlockByObjective = self.GameData.UnlockByObjective
@ -201,7 +237,11 @@ class SnowrunnerClass:
self.__children_handled.add(name.lower()) self.__children_handled.add(name.lower())
def load_child( 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()) self.__children_handled.add(name.lower())
@ -216,11 +256,22 @@ class SnowrunnerClass:
self.load_game_data(data.children[name.lower()]) self.load_game_data(data.children[name.lower()])
return 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 object_type = SnowrunnerGameObjectType(name.lower()) if object_type_override is None else object_type_override
if isinstance(data.children[name.lower()], list): 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: 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): def loading_finished(self, data: SnowRunnerXNLNode):
attr_set = set(data.attrs.keys()) attr_set = set(data.attrs.keys())
@ -239,6 +290,9 @@ class SnowrunnerClass:
) )
sys.exit(1) sys.exit(1)
if self.UiName in DLC_OVERRIDES:
self.DLC = DLC_OVERRIDES[self.UiName]
def get_translated_ui_desc(self, language: LanguageLoader) -> str: def get_translated_ui_desc(self, language: LanguageLoader) -> str:
if self.UiDesc in language: if self.UiDesc in language:
return language[self.UiDesc] return language[self.UiDesc]
@ -305,22 +359,30 @@ class AddonSlots(SnowrunnerClass):
super().__init__(pak_loader=pak_loader) super().__init__(pak_loader=pak_loader)
self.InitialOffset = "(0;0;0)" self.InitialOffset = "(0;0;0)"
self.Quantity = 0 self.Quantity = 0
self.UseTrailerFrame = False
def load_data(self, data: SnowRunnerXNLNode) -> None: def load_data(self, data: SnowRunnerXNLNode) -> None:
self.load_attr(data, "InitialOffset") self.load_attr(data, "InitialOffset")
self.load_attr(data, "Quantity") self.load_attr(data, "Quantity")
self.load_attr(data, "UseTrailerFrame")
self.ignore_attr("OffsetStep")
self.ignore_attr("ParentFrames")
self.loading_finished(data) self.loading_finished(data)
class AddonSockets(SnowrunnerClass): class AddonSockets(SnowrunnerClass):
def __init__(self, pak_loader: InitialPakLoader): def __init__(self, pak_loader: InitialPakLoader):
super().__init__(pak_loader=pak_loader) super().__init__(pak_loader=pak_loader)
self.DefaultAddon = "" self.DefaultAddon = None
self.Socket = None self.Socket = None
def load_data(self, data: SnowRunnerXNLNode) -> None: def load_data(self, data: SnowRunnerXNLNode) -> None:
self.load_attr(data, "DefaultAddon") if "defaultaddon" in data.attrs:
self.load_child(data, "Socket") 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("ParentFrame")
self.ignore_attr("RequiredAddonIfNoConflicts") self.ignore_attr("RequiredAddonIfNoConflicts")
self.loading_finished(data) self.loading_finished(data)
@ -364,6 +426,10 @@ class Body(SnowrunnerClass):
self.ModelFrame = None self.ModelFrame = None
self.Body = [] 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: def load_data(self, data: SnowRunnerXNLNode) -> None:
self.load_attr(data, "AllowedPenetrationDepth") self.load_attr(data, "AllowedPenetrationDepth")
self.load_attr(data, "NoSoftContacts") self.load_attr(data, "NoSoftContacts")
@ -397,8 +463,7 @@ class Body(SnowrunnerClass):
self.ignore_attr("ExplicitParentFrame") self.ignore_attr("ExplicitParentFrame")
self.ignore_attr("NetSync") self.ignore_attr("NetSync")
self.ignore_attr("ParentFrame") self.ignore_attr("ParentFrame")
self.load_child(data, "Body", always_as_list=True)
self.load_child(data, "Body")
self.ignore_child("Constraint") self.ignore_child("Constraint")
self.ignore_child("Motor") self.ignore_child("Motor")
self.ignore_child("SideMirror") self.ignore_child("SideMirror")
@ -489,18 +554,17 @@ class Engine(SnowrunnerClass):
class EngineVariants(SnowrunnerClass): class EngineVariants(SnowrunnerClass):
def __init__(self, pak_loader: InitialPakLoader): def __init__(self, pak_loader: InitialPakLoader):
super().__init__(pak_loader=pak_loader) 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: def load_data(self, data: SnowRunnerXNLNode) -> None:
if "engine" in data.children: self.load_child(data, "Engine", always_as_list=True)
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) self.loading_finished(data)
@ -523,10 +587,11 @@ class GameData(SnowrunnerClass):
self.Country = "" self.Country = ""
self.ExcludeAddons = "" self.ExcludeAddons = ""
self.ExcludedCargoTypes = "" self.ExcludedCargoTypes = ""
self.InstallSocket = None
self.IsCustomizable = False self.IsCustomizable = False
self.IsDoubleTrailer = False self.IsDoubleTrailer = False
self.IsQuest = False self.IsQuest = False
self.Price = -1 self.Price = 0
self.UnlockByRank = -1 self.UnlockByRank = -1
self.UnlockByExploration = False self.UnlockByExploration = False
self.UnlockByObjective = False self.UnlockByObjective = False
@ -544,7 +609,9 @@ class GameData(SnowrunnerClass):
def load_data(self, data: SnowRunnerXNLNode) -> None: def load_data(self, data: SnowRunnerXNLNode) -> None:
self.load_attr(data, "CameraPreset") 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, "Country")
self.load_attr(data, "ExcludeAddons") self.load_attr(data, "ExcludeAddons")
self.load_attr(data, "ExcludedCargoTypes") self.load_attr(data, "ExcludedCargoTypes")
@ -577,8 +644,12 @@ class GameData(SnowrunnerClass):
self.ignore_attr("ResetTruckAfterGateway") self.ignore_attr("ResetTruckAfterGateway")
self.children = data.children self.children = data.children
self.load_child(data, "AddonSockets") self.load_child(data, "AddonSockets", always_as_list=True)
self.load_child(data, "UiDesc") 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("AddonSlots")
self.ignore_child("AddonType") self.ignore_child("AddonType")
self.ignore_child("CenteringAngularVelocity") self.ignore_child("CenteringAngularVelocity")
@ -586,7 +657,6 @@ class GameData(SnowrunnerClass):
self.ignore_child("CustomizationCameras") self.ignore_child("CustomizationCameras")
self.ignore_child("GearboxParams") self.ignore_child("GearboxParams")
self.ignore_child("InstallSlot") self.ignore_child("InstallSlot")
self.ignore_child("InstallSocket")
self.ignore_child("LoadArea") self.ignore_child("LoadArea")
self.ignore_child("LongLogsAlignTarget") self.ignore_child("LongLogsAlignTarget")
self.ignore_child("RequiredAddon") self.ignore_child("RequiredAddon")
@ -617,6 +687,9 @@ class GameData(SnowrunnerClass):
self.ignore_child("longlogsaligndata") self.ignore_child("longlogsaligndata")
self.loading_finished(data) self.loading_finished(data)
def update_data(self, data: SnowRunnerXNLNode) -> None:
self.load_data(data)
class Gear(SnowrunnerClass): class Gear(SnowrunnerClass):
def __init__(self, pak_loader: InitialPakLoader): def __init__(self, pak_loader: InitialPakLoader):
@ -645,7 +718,7 @@ class Gearbox(SnowrunnerClass):
self.GearboxParams = None self.GearboxParams = None
self.ReverseGear = None self.ReverseGear = None
self.HighGear = None self.HighGear = None
self.Gears = [] self.Gear = []
def load_data(self, data: SnowRunnerXNLNode) -> None: def load_data(self, data: SnowRunnerXNLNode) -> None:
self.load_attr(data, "AWDConsumptionModifier") self.load_attr(data, "AWDConsumptionModifier")
@ -660,10 +733,12 @@ class Gearbox(SnowrunnerClass):
self.load_child(data, "GameData") self.load_child(data, "GameData")
if "gamedata" in data.children: if "gamedata" in data.children:
self.load_child(data.children["gamedata"], "GearboxParams") 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, "ReverseGear", object_type_override=SnowrunnerGameObjectType.GEAR)
self.load_child(data, "HighGear", 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) self.loading_finished(data)
@ -690,8 +765,15 @@ class GearboxVariants(SnowrunnerClass):
super().__init__(pak_loader=pak_loader) super().__init__(pak_loader=pak_loader)
self.Gearbox = [] 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: 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) self.loading_finished(data)
def update_data(self, data: SnowRunnerXNLNode) -> None: 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"]): for orig, upd in zip_longest(self.Gearbox, data.children["gearbox"]):
if orig is None: 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: elif upd is None:
return return
else: else:
@ -766,6 +848,15 @@ class PhysicsModel(SnowrunnerClass):
self.Mesh = "" self.Mesh = ""
self.Body = None 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: def load_data(self, data: SnowRunnerXNLNode) -> None:
self.load_attr(data, "_noinherit") self.load_attr(data, "_noinherit")
self.load_attr(data, "Mesh") self.load_attr(data, "Mesh")
@ -871,8 +962,15 @@ class SuspensionSetVariants(SnowrunnerClass):
super().__init__(pak_loader=pak_loader) super().__init__(pak_loader=pak_loader)
self.SuspensionSet = [] 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: 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) self.loading_finished(data)
@ -891,6 +989,98 @@ class Truck(SnowrunnerClass):
self.Type = "Truck" self.Type = "Truck"
self.WaterMass = None 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: def load_data(self, data: SnowRunnerXNLNode) -> None:
self.load_attr(data, "AttachType") self.load_attr(data, "AttachType")
self.load_attr(data, "IsDeprecated") self.load_attr(data, "IsDeprecated")
@ -944,6 +1134,7 @@ class TruckAddon(SnowrunnerClass):
self.PhysicsModel = None self.PhysicsModel = None
self.TruckData = None self.TruckData = None
self.AddonType = None self.AddonType = None
self.AddonSlots = None
self.RequiredAddonType = None self.RequiredAddonType = None
self.IsChassisFullOcclusion = False self.IsChassisFullOcclusion = False
self.FuelMass = None self.FuelMass = None
@ -966,7 +1157,7 @@ class TruckAddon(SnowrunnerClass):
if "installsocket" in data.children["gamedata"].children: if "installsocket" in data.children["gamedata"].children:
self.load_child(data.children["gamedata"], "InstallSocket") self.load_child(data.children["gamedata"], "InstallSocket")
if "addonslots" in data.children["gamedata"].children: 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: if "addontype" in data.children["gamedata"].children:
self.load_child(data.children["gamedata"], "AddonType") self.load_child(data.children["gamedata"], "AddonType")
if "requiredaddon" in data.children["gamedata"].children: if "requiredaddon" in data.children["gamedata"].children:
@ -1007,12 +1198,25 @@ class TruckAddon(SnowrunnerClass):
class TruckData(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): def __init__(self, pak_loader: InitialPakLoader):
super().__init__(pak_loader=pak_loader) super().__init__(pak_loader=pak_loader)
self.AllWheelDriveInstalled = False self.AllWheelDriveInstalled = False
self.BackSteerSpeed = 0.0 self.BackSteerSpeed = 0.0
self.CompatibleWheels = [] self.CompatibleWheels = []
self.Damage = None self.Damage = None
self.DefaultEngine = None
self.DefaultGearbox = None
self.DefaultSuspension = None
self.DefaultTire = None
self.DefaultWinch = None
self.DiffLockInstalled = False self.DiffLockInstalled = False
self.DiffLockType = "" self.DiffLockType = ""
self.Engines = [] self.Engines = []
@ -1041,12 +1245,12 @@ class TruckData(SnowrunnerClass):
self.load_attr(data, "Responsiveness") self.load_attr(data, "Responsiveness")
self.load_attr(data, "SetHandbrakeOnWinchRelease") self.load_attr(data, "SetHandbrakeOnWinchRelease")
self.load_attr(data, "SteerSpeed") 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, "WaterCapacity")
self.load_attr(data, "WheelRepairsCapacity") self.load_attr(data, "WheelRepairsCapacity")
self.load_child(data, "CompatibleWheels") self.load_child(data, "CompatibleWheels", always_as_list=True)
if self.CompatibleWheels and not isinstance(self.CompatibleWheels, list):
self.CompatibleWheels = [self.CompatibleWheels]
self.load_child(data, "Damage") self.load_child(data, "Damage")
self.load_child(data, "Wheels") self.load_child(data, "Wheels")
@ -1058,6 +1262,9 @@ class TruckData(SnowrunnerClass):
self.pak.get_object(object_type=SnowrunnerGameObjectType.ENGINE_VARIANTS, name=n.strip()) self.pak.get_object(object_type=SnowrunnerGameObjectType.ENGINE_VARIANTS, name=n.strip())
for n in data.children["enginesocket"].attrs["type"].split(",") 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") self.ignore_child("EngineSocket")
if "gearboxsocket" in data.children: if "gearboxsocket" in data.children:
@ -1065,6 +1272,9 @@ class TruckData(SnowrunnerClass):
self.pak.get_object(object_type=SnowrunnerGameObjectType.GEARBOX_VARIANTS, name=n.strip()) self.pak.get_object(object_type=SnowrunnerGameObjectType.GEARBOX_VARIANTS, name=n.strip())
for n in data.children["gearboxsocket"].attrs["type"].split(",") 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") self.ignore_child("GearboxSocket")
if "suspensionsocket" in data.children: 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()) self.pak.get_object(object_type=SnowrunnerGameObjectType.SUSPENSION_SET_VARIANTS, name=n.strip())
for n in data.children["suspensionsocket"].attrs["type"].split(",") 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") self.ignore_child("SuspensionSocket")
if "winchupgradesocket" in data.children: if "winchupgradesocket" in data.children:
@ -1079,6 +1293,9 @@ class TruckData(SnowrunnerClass):
self.pak.get_object(object_type=SnowrunnerGameObjectType.WINCH_VARIANTS, name=n.strip()) self.pak.get_object(object_type=SnowrunnerGameObjectType.WINCH_VARIANTS, name=n.strip())
for n in data.children["winchupgradesocket"].attrs["type"].split(",") 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_child("WinchUpgradeSocket")
self.ignore_attr("_noinherit") self.ignore_attr("_noinherit")
@ -1150,9 +1367,7 @@ class TruckTires(SnowrunnerClass):
self.TruckTire = None self.TruckTire = None
def load_data(self, data: SnowRunnerXNLNode) -> None: def load_data(self, data: SnowRunnerXNLNode) -> None:
self.load_child(data, "TruckTire") self.load_child(data, "TruckTire", always_as_list=True)
if self.TruckTire and not isinstance(self.TruckTire, list):
self.TruckTire = [self.TruckTire]
self.loading_finished(data) self.loading_finished(data)
def update_data(self, data: SnowRunnerXNLNode) -> None: 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"]): for orig, upd in zip_longest(self.TruckTire, data.children["trucktire"]):
if orig is None: 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: elif upd is None:
return return
else: else:
@ -1202,6 +1417,13 @@ class TruckWheels(SnowrunnerClass):
self.WidthRear = -1.0 self.WidthRear = -1.0
self.TruckTires = None 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: def load_data(self, data: SnowRunnerXNLNode) -> None:
self.load_attr(data, "DamageCapacity") self.load_attr(data, "DamageCapacity")
self.load_attr(data, "Mass") self.load_attr(data, "Mass")
@ -1276,12 +1498,21 @@ class Wheels(SnowrunnerClass):
def __init__(self, pak_loader: InitialPakLoader): def __init__(self, pak_loader: InitialPakLoader):
super().__init__(pak_loader=pak_loader) super().__init__(pak_loader=pak_loader)
self.Wheel = None self.Wheel = None
self.DefaultTire = None
self.DefaultWheelType = None
def load_data(self, data: SnowRunnerXNLNode) -> None: def load_data(self, data: SnowRunnerXNLNode) -> None:
if "defaultwheeltype" in data.attrs:
self.DefaultWheelType = self.pak.get_object(
SnowrunnerGameObjectType.TRUCK_WHEELS, name=data.attrs["defaultwheeltype"]
)
self.ignore_attr("DefaultWheelType") self.ignore_attr("DefaultWheelType")
self.ignore_attr("DefaultRim") if "defaulttire" in data.attrs:
self.DefaultTire = self.DefaultWheelType.get_truck_tire(data.attrs["defaulttire"])
self.ignore_attr("DefaultTire") self.ignore_attr("DefaultTire")
self.load_child(data, "Wheel")
self.ignore_attr("DefaultRim")
self.load_child(data, "Wheel", always_as_list=True)
self.loading_finished(data) self.loading_finished(data)
@ -1349,8 +1580,15 @@ class WinchVariants(SnowrunnerClass):
super().__init__(pak_loader=pak_loader) super().__init__(pak_loader=pak_loader)
self.Winch = [] 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: 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.load_child(data, "WinchParams")
self.loading_finished(data) self.loading_finished(data)
@ -1366,6 +1604,15 @@ class LanguageLoader(dict):
value = parts[-1].replace('"', "").replace("\\", '"') value = parts[-1].replace('"', "").replace("\\", '"')
self[key] = value 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 = { TYPE_CLASS_MATRIX = {
"addonslots": AddonSlots, "addonslots": AddonSlots,
@ -1449,6 +1696,7 @@ class SnowrunnerGameObjectType(str, Enum):
WINCH_PARAMS = "winchparams" WINCH_PARAMS = "winchparams"
WINCH_VARIANTS = "winchvariants" WINCH_VARIANTS = "winchvariants"
@property
def snowrunner_class(self): def snowrunner_class(self):
return TYPE_CLASS_MATRIX[self.value] return TYPE_CLASS_MATRIX[self.value]
@ -1532,6 +1780,7 @@ class InitialPakLoader:
self.pak_zip = get_initial_pak_zipfile(get_initial_pak_path()) self.pak_zip = get_initial_pak_zipfile(get_initial_pak_path())
self.is_loaded = False self.is_loaded = False
self.languages = {} self.languages = {}
self.dlc_names = {x: {} for x in SnowrunnerGameObjectType}
self.raw_data = {x: {} for x in SnowrunnerGameObjectType} self.raw_data = {x: {} for x in SnowrunnerGameObjectType}
self.game_data = {x: {} for x in SnowrunnerGameObjectType} self.game_data = {x: {} for x in SnowrunnerGameObjectType}
self.templates = {x: {} for x in SnowrunnerGameObjectType} self.templates = {x: {} for x in SnowrunnerGameObjectType}
@ -1550,6 +1799,7 @@ class InitialPakLoader:
self, self,
object_type: SnowrunnerGameObjectType, object_type: SnowrunnerGameObjectType,
data: SnowRunnerXNLNode, data: SnowRunnerXNLNode,
dlc_name: str,
parent: str | None = None, parent: str | None = None,
lazy: bool = False, lazy: bool = False,
) -> SnowrunnerClass | list[SnowrunnerClass] | None: ) -> SnowrunnerClass | list[SnowrunnerClass] | None:
@ -1557,37 +1807,40 @@ class InitialPakLoader:
template = self.get_object(object_type=object_type, name=parent, lazy=lazy) template = self.get_object(object_type=object_type, name=parent, lazy=lazy)
if template is None: if template is None:
return 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) ret.update_data(data)
return ret return ret
if not isinstance(data, list): if not isinstance(data, list):
if "_template" not in data.attrs: if "_template" not in data.attrs:
ret = object_type.snowrunner_class()(pak_loader=self) ret = object_type.snowrunner_class(pak_loader=self)
else: 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"]) pak_loader=self, template=self.get_template(object_type=object_type, name=data.attrs["_template"])
) )
ret.DLC = dlc_name
ret.load_data(data) ret.load_data(data)
else: else:
ret = [] ret = []
for sub_data in data: for sub_data in data:
if "_template" not in sub_data.attrs: 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: else:
ret.append( ret.append(
object_type.snowrunner_class().from_template( object_type.snowrunner_class.from_template(
pak_loader=self, pak_loader=self,
template=self.get_template(object_type=object_type, name=sub_data.attrs["_template"]), template=self.get_template(object_type=object_type, name=sub_data.attrs["_template"]),
) )
) )
ret[-1].DLC = dlc_name
ret[-1].load_data(sub_data) ret[-1].load_data(sub_data)
return ret 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: if local:
self.templates_local = {x: {} for x in SnowrunnerGameObjectType} self.templates_local = {x: {} for x in SnowrunnerGameObjectType}
@ -1610,18 +1863,26 @@ class InitialPakLoader:
continue continue
if local: if local:
self.templates_local[category][tp_name] = self.load_class( 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: 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: if defer:
for tp_name, sub_data in defer: for tp_name, sub_data in defer:
if local: if local:
self.templates_local[category][tp_name] = self.load_class( 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: 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: elif category in self.ignore_categories:
pass # Ignore for now pass # Ignore for now
@ -1657,6 +1918,9 @@ class InitialPakLoader:
logger.error(f"Unknown Game Data Object requested: {name} in {object_type.name}") logger.error(f"Unknown Game Data Object requested: {name} in {object_type.name}")
sys.exit(1) 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( def get_sorted_object_list(
self, object_type: SnowrunnerGameObjectType, language: LanguageLoader self, object_type: SnowrunnerGameObjectType, language: LanguageLoader
) -> list[tuple[str, SnowrunnerClass]]: ) -> 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)), 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_matrix = set(data.keys()) & set(SnowrunnerGameObjectType.values())
cat_in_ignore = set(data.keys()) & self.ignore_categories cat_in_ignore = set(data.keys()) & self.ignore_categories
if cat_in_matrix: if cat_in_matrix:
self.raw_data[SnowrunnerGameObjectType(list(cat_in_matrix)[0])][ref] = data 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: elif cat_in_ignore:
pass # not sure what to do with this, yet pass # not sure what to do with this, yet
else: else:
@ -1686,6 +1951,8 @@ class InitialPakLoader:
for member in self.pak_zip.infolist(): for member in self.pak_zip.infolist():
if not member.filename.endswith(".xml"): if not member.filename.endswith(".xml"):
continue continue
dlc_name = member.filename.split("\\")[2] if "_dlc" in member.filename else None
filename_ref = member.filename.split("\\")[-1].replace(".xml", "") filename_ref = member.filename.split("\\")[-1].replace(".xml", "")
logger.info(f" - {member.filename}") logger.info(f" - {member.filename}")
with self.pak_zip.open(member) as d_file: with self.pak_zip.open(member) as d_file:
@ -1695,13 +1962,13 @@ class InitialPakLoader:
if len(data) == 1 and "_templates" in data: if len(data) == 1 and "_templates" in data:
logger.info(f" - -> Loading templates from {member.filename}") 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 continue
elif len(data) > 3: elif len(data) > 3:
logger.error(f"Multiple keys?! ({list(data.keys())}") logger.error(f"Multiple keys?! ({list(data.keys())}")
sys.exit(1) sys.exit(1)
self.store_raw(filename_ref, data) self.store_raw(filename_ref, dlc_name, data)
deferred_loads = [] deferred_loads = []
logger.info("Loading GameData") logger.info("Loading GameData")
@ -1715,9 +1982,13 @@ class InitialPakLoader:
logger.info(f" - {ref}") logger.info(f" - {ref}")
if "_templates" in data: 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] del self.raw_data[object_type]
@ -1726,9 +1997,15 @@ class InitialPakLoader:
for object_type, ref, data in deferred_loads: for object_type, ref, data in deferred_loads:
logger.info(f" - {ref}") logger.info(f" - {ref}")
if "_templates" in data: 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( 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: if sr_class is None:
still_deferred_loads.append((object_type, ref, data)) still_deferred_loads.append((object_type, ref, data))
@ -1743,7 +2020,9 @@ class InitialPakLoader:
logger.info(f" - {object_type.name}, {ref}") logger.info(f" - {object_type.name}, {ref}")
if "_templates" in data: if "_templates" in data:
self.load_templates(data["_templates"].children, local=True) 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 = [] deferred_loads = []
for cat, v in self.raw_data.items(): for cat, v in self.raw_data.items():

383
output.py
View File

@ -1,6 +1,14 @@
from __future__ import annotations from __future__ import annotations
import os 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 from logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
@ -13,13 +21,17 @@ def get_head(language: LanguageLoader, category: str, name: str | None = None) -
<title>SnowRunnerDB - {category}""" <title>SnowRunnerDB - {category}"""
+ (f""" - {name}""" if name is not None else "") + (f""" - {name}""" if name is not None else "")
+ f"""</title> + f"""</title>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<link href="https://cdn.datatables.net/v/dt/dt-3.0.1/datatables.min.css" rel="stylesheet">
<script src="https://cdn.datatables.net/v/dt/dt-3.0.1/datatables.min.js"></script>
</head> </head>
<body> <body>
<div id='navi_lang'></div> <div id='navi_lang'></div>
<div id='categories'> <div id='categories'>
<ul id='navi_cat'> <ul id='navi_cat'>
<li><a href="/{language.Name}/trucks/">Trucks</a></li> <li><a href="/{language.Name}/trucks/">Trucks</a></li>
<li><a href="/{language.Name}/frame_addons/">Frame Addons</a></li>
</ul> </ul>
</div> </div>
<div id='content'> <div id='content'>
@ -44,50 +56,154 @@ def write_html(
def write_trucks(pak: InitialPakLoader, destination_directory: str, language: LanguageLoader): def write_trucks(pak: InitialPakLoader, destination_directory: str, language: LanguageLoader):
index_table = "" index_content = " <h1>Trucks</h1>\n"
index_content += f" <table id='truck_index' class='display'>\n"
truck_types = {} index_content += (
f" <thead><tr>\n"
f" <th>Name</th>\n"
f" <th>Class</th>\n"
f" <th>DLC</th>\n"
f" <th>Unlock by</th>\n"
f" <th>Region</th>\n"
f" <th>Price</th>\n"
f" <th>Weight</th>\n"
f" <th>Drivetrain</th>\n"
f" <th>AWD</th>\n"
f" <th>DiffLock</th>\n"
f" </tr></thead>\n"
)
index_content += f" <tbody>\n"
for reference, truck in pak.get_sorted_object_list(SnowrunnerGameObjectType.TRUCK, language): for reference, truck in pak.get_sorted_object_list(SnowrunnerGameObjectType.TRUCK, language):
if truck.Type != "Truck": if truck.Type != "Truck":
continue 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_file = os.path.join(destination_directory, reference + ".html")
truck_content = f" <h1>{truck.get_translated_ui_name(language)}</h1>" truck_content = f" <h1>{truck.get_translated_ui_name(language)}</h1>"
truck_content += f" <table>\n"
try:
truck_content += f" <tr><td>DLC</td><td>{DLC_NAMES[truck.DLC]}</td></tr>\n"
except KeyError:
print(f"DEBUG: DLC {truck.DLC} for Truck {truck.get_translated_ui_name(language)}")
exit(1)
truck_content += f" <tr><td>Weight</td><td>{truck.Weight}kg</td></tr>\n"
truck_content += f" <tr><td>AWD</td><td>{language[truck.AWDInfo.value]}</td></tr>\n"
truck_content += f" <tr><td>DiffLock</td><td>{language[truck.DiffLockInfo.value]}</td></tr>\n"
truck_content += f" <tr><td>Def Engine</td><td>{truck.TruckData.DefaultEngine.get_translated_ui_name(language)}</td></tr>\n"
truck_content += f" <tr><td>Def Gearbox</td><td>{truck.TruckData.DefaultGearbox.get_translated_ui_name(language)}</td></tr>\n"
truck_content += f" <tr><td>Def Suspension</td><td>{truck.TruckData.DefaultSuspension.get_translated_ui_name(language)}</td></tr>\n"
truck_content += f" <tr><td>Def Winch</td><td>{truck.TruckData.DefaultWinch.get_translated_ui_name(language)}</td></tr>\n"
truck_content += f" <tr><td>Def Tire</td><td>{truck.TruckData.Wheels.DefaultTire.get_translated_ui_name(language)}</td></tr>\n"
truck_content += f" </table>\n"
# Engines # Engines
truck_content += f" <h2>Engines</h2>\n" truck_content += f" <h2>Engines</h2>\n"
engines = [x for y in truck.TruckData.Engines for x in y.Engines] engines = [x for y in truck.TruckData.Engines for x in y.Engine]
truck_content += f" <table>\n" truck_content += f" <table id='engines' data-order='[[2, \"asc\"]]'>\n"
truck_content += f" <tr><th>Name</th><th>Price</th><th>Torque</th><th>Base Fuel Consumption</th><th>Unlock By</th></tr>\n" truck_content += (
f" <thead><tr>\n"
f" <th>Name</th>\n"
f" <th>Price</th>\n"
f" <th>Unlock By</th>\n"
f" <th>Base Fuel Consumption</th>\n"
f" <th>Torque</th>\n"
f" <th>Power-to-Weight</th>\n"
f" </tr></thead>\n"
)
truck_content += f" <tbody>\n"
# Power2Weight # Power2Weight
# Power2Fuel # Power2Fuel
for engine in sorted(engines, key=lambda x: x.Torque): for engine in sorted(engines, key=lambda x: x.Torque):
truck_content += ( truck_content += (
f" <tr>" f" <tr>"
f"<td>{engine.get_translated_ui_name(language)}</td>" f" <td>{engine.get_translated_ui_name(language)}</td>"
f"<td>{engine.GameData.Price}</th>" f" <td>{engine.GameData.Price}</th>"
f"<td>{engine.Torque}</td>" f" <td>{engine.GameData.get_unlock_str()}</td>"
f"<td>{engine.FuelConsumption}</td>" f" <td>{engine.FuelConsumption}</td>"
f"<td>{engine.GameData.get_unlock_str()}</td>" f" <td>{engine.Torque}</td>"
f"</tr>\n" f" <td>{engine.Torque / truck.Weight:.2f}</td>"
f" </tr>\n"
) )
truck_content += f" </table>\n" truck_content += f" </tbody></table>\n"
# Gearboxes # 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" <h2>Gearboxes</h2>\n" truck_content += f" <h2>Gearboxes</h2>\n"
truck_content += f" <h3>General</h3>\n"
truck_content += f" <table id='gearboxes_general'>\n"
truck_content += (
f" <thead><tr>\n"
f" <th>Name</th>\n"
f" <th>Price</th>\n"
f" <th>Unlock By</th>\n"
f" <th>Fine-Tunable</th>\n"
f" <th>L-</th>\n"
f" <th>L</th>\n"
f" <th>L+</th>\n"
f" <th>H</th>\n"
f" <th>Gears</th>\n"
f" </tr></thead>\n"
)
truck_content += f" <tbody>\n"
for gearbox in sorted(gearboxes, key=lambda x: x.get_translated_ui_name(language)):
truck_content += f" <tr>\n"
truck_content += f" <td>{gearbox.get_translated_ui_name(language)}</td>\n"
truck_content += f" <td>{gearbox.Price}</td>\n"
truck_content += f" <td>{gearbox.GameData.get_unlock_str()}</td>\n"
truck_content += (
f" <td>{'&check;' if gearbox.GearboxParams.IsManualLowGear else '&dot;'}</td>\n"
)
truck_content += (
f" <td>{'&check;' if gearbox.GearboxParams.IsLowerMinusGearExists else '&dot;'}</td>\n"
)
truck_content += (
f" <td>{'&check;' if gearbox.GearboxParams.IsLowerGearExists else '&dot;'}</td>\n"
)
truck_content += (
f" <td>{'&check;' if gearbox.GearboxParams.IsLowerPlusGearExists else '&dot;'}</td>\n"
)
truck_content += (
f" <td>{'&check;' if gearbox.GearboxParams.IsHighGearExists else '&dot;'}</td>\n"
)
truck_content += f" <td>{len(gearbox.Gear)}</td>\n"
truck_content += f" </tr>\n"
truck_content += f" </tbody></table>\n"
truck_content += f" <h3>Fuel Consumption</h3>\n"
truck_content += f" <table id='gearboxes_fuel'>\n"
truck_content += (
f" <thead><tr>\n"
f" <th>Name</th>\n"
f" <th>Fuel Consumption</th>\n"
f" <th>AWD Fuel Modifier</th>\n"
)
for i in range(max_gear_count):
gear_num = str(i + 1) if i > 0 else "L+/1"
truck_content += f" <th>{gear_num}</th>\n"
truck_content += f" </tr></thead>\n"
truck_content += f" <tbody>\n"
for gearbox in sorted(gearboxes, key=lambda x: x.get_translated_ui_name(language)):
truck_content += f" <tr>\n"
truck_content += f" <td>{gearbox.get_translated_ui_name(language)}</td>\n"
truck_content += f" <td>{gearbox.FuelConsumption}</td>\n"
truck_content += f" <td>{gearbox.AWDConsumptionModifier}</td>\n"
for i in range(max_gear_count):
if i >= len(gearbox.Gear):
truck_content += f" <td>-</td>\n"
else:
truck_content += f" <td>{gearbox.Gear[i].FuelModifier}</td>\n"
truck_content += f" </tr>\n"
truck_content += f" </tbody></table>\n"
# Suspensions # Suspensions
truck_content += f" <h2>Suspensions</h2>\n" truck_content += f" <h2>Suspensions</h2>\n"
# Winches
truck_content += f" <h2>Winches</h2>\n"
# Wheels # Wheels
truck_content += f" <h2>Wheels</h2>\n" truck_content += f" <h2>Tires</h2>\n"
wheel_cat = {} wheel_cat = {}
scales = set() scales = set()
for cw in truck.TruckData.CompatibleWheels: 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)]) scales = ", ".join(['%d"' % x for x in sorted(scales)])
truck_content += f" <h4>Available Tire Sizes: {scales}</h4>\n" truck_content += f" <h4>Available Tire Sizes: {scales}</h4>\n"
# Winches
truck_content += f" <h2>Winches</h2>\n"
# Diff. Lock
diff_lock_addons = truck.get_available_truck_addons(TruckAddonCategory.DIFF_LOCK)
if diff_lock_addons:
truck_content += f" <h2>Diff. Lock</h2>\n"
truck_content += f" <table id='diff_lock'>\n"
truck_content += (
f" <thead><tr>\n"
f" <th>Name</th>\n"
f" <th>Price</th>\n"
f" <th>Unlock By</th>\n"
f" </tr></thead><tbody>\n"
)
for addon in sorted(diff_lock_addons, key=lambda v: v.get_translated_ui_name(language)):
truck_content += (
f" <tr>\n"
f" <td>{addon.get_translated_ui_name(language)}</td>\n"
f" <td>{addon.Price}</td>\n"
f" <td>{addon.GameData.get_unlock_str()}</td>\n"
f" </tr>\n"
)
truck_content += f" </tbody></table>\n"
# Spare Wheel (if any)
# Snorkel (if any)
snorkel_addons = truck.get_available_truck_addons(TruckAddonCategory.SNORKEL)
if snorkel_addons:
truck_content += f" <h2>Snorkel</h2>\n"
truck_content += f" <table id='snorkel'>\n"
truck_content += (
f" <thead><tr>\n"
f" <th>Name</th>\n"
f" <th>Price</th>\n"
f" <th>Unlock By</th>\n"
f" <th>Mass</th>\n"
f" </tr></thead><tbody>\n"
)
for addon in sorted(snorkel_addons, key=lambda v: v.get_translated_ui_name(language)):
truck_content += (
f" <tr>\n"
f" <td>{addon.get_translated_ui_name(language)}</td>\n"
f" <td>{addon.Price}</td>\n"
f" <td>{addon.GameData.get_unlock_str()}</td>\n"
f" <td>{addon.PhysicsModel.Weight}kg</td>\n"
f" </tr>\n"
)
truck_content += f" </tbody></table>\n"
# AWD
awd_addons = truck.get_available_truck_addons(TruckAddonCategory.ALL_WHEEL_DRIVE)
if awd_addons:
truck_content += f" <h2>AWD</h2>\n"
truck_content += f" <table id='awd'>\n"
truck_content += (
f" <thead><tr>\n"
f" <th>Name</th>\n"
f" <th>Price</th>\n"
f" <th>Unlock By</th>\n"
f" <th>Mass</th>\n"
f" </tr></thead><tbody>\n"
)
for addon in sorted(awd_addons, key=lambda v: v.get_translated_ui_name(language)):
truck_content += (
f" <tr>\n"
f" <td>{addon.get_translated_ui_name(language)}</td>\n"
f" <td>{addon.Price}</td>\n"
f" <td>{addon.GameData.get_unlock_str()}</td>\n"
f" <td>{addon.PhysicsModel.Weight}kg</td>\n"
f" </tr>\n"
)
truck_content += f" </tbody></table>\n"
# Frame Addons
truck_content += f" <h2>Frame Addons</h2>\n"
truck_content += f" <table id='frame_addons'>\n"
truck_content += (
f" <thead><tr>\n"
f" <th>Name</th>\n"
f" <th>Price</th>\n"
f" <th>Unlock By</th>\n"
f" <th>Mass</th>\n"
f" <th>Cargo Slots</th>\n"
f" <th>Repair Points</th>\n"
f" <th>Spare Wheels</th>\n"
f" <th>Fuel Capacity</th>\n"
f" <th>Water Capacity</th>\n"
f" </tr></thead><tbody>\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" <tr>\n"
truck_content += f" <td>{truck_addon.get_translated_ui_name(language)}</td>\n"
truck_content += f" <td>{truck_addon.Price}</td>\n"
truck_content += f" <td>{truck_addon.GameData.get_unlock_str()}</td>\n"
truck_content += f" <td>{truck_addon.PhysicsModel.Weight}kg</td>\n"
truck_content += f" <td>{truck_addon.AddonSlots.Quantity if truck_addon.AddonSlots is not None else '&nbsp;'}</td>\n"
truck_content += f" <td>{truck_addon.TruckData.RepairsCapacity if truck_addon.TruckData is not None and truck_addon.TruckData.RepairsCapacity > 0 else '&nbsp;'}</td>\n"
truck_content += f" <td>{truck_addon.TruckData.WheelRepairsCapacity if truck_addon.TruckData is not None and truck_addon.TruckData.WheelRepairsCapacity > 0 else '&nbsp;'}</td>\n"
truck_content += f" <td>{truck_addon.TruckData.FuelCapacity if truck_addon.TruckData is not None and truck_addon.TruckData.FuelCapacity > 0 else '&nbsp;'}</td>\n"
truck_content += f" <td>{truck_addon.TruckData.WaterCapacity if truck_addon.TruckData is not None and truck_addon.TruckData.WaterCapacity > 0 else '&nbsp;'}</td>\n"
truck_content += f" </tr>\n"
truck_content += f" </tbody></table>\n"
truck_content += f" <script>\n"
truck_content += " document.addEventListener('DOMContentLoaded', function() {new DataTable('#engines', {info: false, paging: false, searching: false});});\n"
truck_content += " document.addEventListener('DOMContentLoaded', function() {new DataTable('#gearboxes_general', {info: false, paging: false, searching: false});});\n"
truck_content += " document.addEventListener('DOMContentLoaded', function() {new DataTable('#gearboxes_fuel', {info: false, paging: false, searching: false});});\n"
truck_content += " document.addEventListener('DOMContentLoaded', function() {new DataTable('#diff_lock', {info: false, paging: false, searching: false});});\n"
truck_content += " document.addEventListener('DOMContentLoaded', function() {new DataTable('#snorkel', {info: false, paging: false, searching: false});});\n"
truck_content += " document.addEventListener('DOMContentLoaded', function() {new DataTable('#awd', {info: false, paging: false, searching: false});});\n"
truck_content += " document.addEventListener('DOMContentLoaded', function() {new DataTable('#frame_addons', {info: false, paging: false, searching: false});});\n"
truck_content += f" </script>\n"
write_html( write_html(
truck_file, truck_file,
language=language, language=language,
@ -132,20 +366,99 @@ def write_trucks(pak: InitialPakLoader, destination_directory: str, language: La
name=truck.get_translated_ui_name(language), name=truck.get_translated_ui_name(language),
) )
index_content = " <h1>Trucks</h1>\n" index_content += (
for truck_type in sorted(truck_types): f" <tr>\n"
index_content += f" <h2>{truck_type}</h2>\n" f" <td><a href='{reference}.html'>{truck.get_translated_ui_name(language)}</a></td>\n"
index_content += f" <table>\n" f" <td>{language.get_sanitized(truck.TruckData.TruckType)}</td>\n"
index_content += f" <tr><th>Name</th></tr>\n" f" <td>{DLC_NAMES[truck.DLC]}</td>\n"
for reference, truck in sorted(truck_types[truck_type], key=lambda v: v[1].get_translated_ui_name(language)): f" <td>{truck.GameData.get_unlock_str()}</td>\n"
index_content += f" <tr><td><a href='{reference}.html'>{truck.get_translated_ui_name(language)}</a></th></tr>\n" f" <td>{truck.GameData.Country}</td>\n"
index_content += f" </table>\n" f" <td>{truck.get_shop_price()}</td>\n"
f" <td>{truck.Weight}</td>\n"
f" <td>{truck.DriveTrain}</td>\n"
f" <td>{language[truck.AWDInfo.value]}</td>\n"
f" <td>{language[truck.DiffLockInfo.value]}</td>\n"
f" </tr>\n"
)
index_content += f" </tbody></table>\n"
index_content += " <script>document.addEventListener('DOMContentLoaded', function() {new DataTable('#truck_index', {paging: false});});</script>\n"
write_html( write_html(
os.path.join(destination_directory, "index.html"), language=language, content=index_content, category="Trucks" 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 = " <h1>Frame Addons</h1>\n"
index_content += f" <table id='frame_addons' class='display'>\n"
index_content += (
f" <thead><tr>\n"
f" <th>Name</th>\n"
f" <th>Price</th>\n"
f" <th>Unlock By</th>\n"
f" <th>Mass</th>\n"
f" <th>Cargo Slots</th>\n"
f" <th>Repair Points</th>\n"
f" <th>Spare Wheels</th>\n"
f" <th>Fuel Capacity</th>\n"
f" <th>Water Capacity</th>\n"
f" <th>Available for</th>\n"
f" </tr></thead>\n"
)
index_content += f" <tbody>\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" <tr>\n"
index_content += f" <td>{frame_addon.get_translated_ui_name(language)}</td>\n"
index_content += f" <td>{frame_addon.Price}</td>\n"
index_content += f" <td>{frame_addon.GameData.get_unlock_str()}</td>\n"
index_content += f" <td>{frame_addon.PhysicsModel.Weight}kg</td>\n"
index_content += f" <td>{frame_addon.AddonSlots.Quantity if frame_addon.AddonSlots is not None else '&nbsp;'}</td>\n"
index_content += f" <td>{frame_addon.TruckData.RepairsCapacity if frame_addon.TruckData is not None and frame_addon.TruckData.RepairsCapacity > 0 else '&nbsp;'}</td>\n"
index_content += f" <td>{frame_addon.TruckData.WheelRepairsCapacity if frame_addon.TruckData is not None and frame_addon.TruckData.WheelRepairsCapacity > 0 else '&nbsp;'}</td>\n"
index_content += f" <td>{frame_addon.TruckData.FuelCapacity if frame_addon.TruckData is not None and frame_addon.TruckData.FuelCapacity > 0 else '&nbsp;'}</td>\n"
index_content += f" <td>{frame_addon.TruckData.WaterCapacity if frame_addon.TruckData is not None and frame_addon.TruckData.WaterCapacity > 0 else '&nbsp;'}</td>\n"
truck_links = ", ".join(
f'<a href="../trucks/{ref}.html">{truck.get_translated_ui_name(language)}</a>'
for ref, truck in available_trucks
)
index_content += f" <td>{truck_links}</td>\n"
index_content += f" </tr>\n"
index_content += f" </tbody>\n"
index_content += f" </table>\n"
index_content += " <script>document.addEventListener('DOMContentLoaded', function() {new DataTable('#frame_addons', {paging: false});});</script>\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/"): def create_html(pak: InitialPakLoader, destination_directory: str = "output/"):
if not os.path.exists(destination_directory): if not os.path.exists(destination_directory):
os.mkdir(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): if not os.path.exists(language_path):
os.mkdir(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)