snowrunner-data-extract/loader.py
2026-08-16 11:44:59 +02:00

1752 lines
63 KiB
Python

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