2031 lines
73 KiB
Python
2031 lines
73 KiB
Python
from __future__ import annotations
|
|
from constants import DLC_OVERRIDES
|
|
from decimal import Decimal, ROUND_HALF_UP
|
|
from enum import Enum
|
|
from html import parser as html_parser
|
|
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*>")
|
|
|
|
|
|
class TruckInfo(str, Enum):
|
|
ALWAYS = "UI_TRUCK_INFO_ALWAYS"
|
|
CAPABLE = "UI_TRUCK_INFO_CAPABLE"
|
|
NOT_AVAILABLE = "UI_TRUCK_INFO_NA"
|
|
SWITCHABLE = "UI_TRUCK_INFO_ON_OFF"
|
|
|
|
|
|
class TruckAddonCategory(str, Enum):
|
|
ALL_WHEEL_DRIVE = "awd"
|
|
BUBBLE_HEAD = "bubble_head"
|
|
BUMPER = "bumper"
|
|
CURTAIN = "curtain"
|
|
DIFF_LOCK = "diff_lock"
|
|
EXHAUST = "exhaust"
|
|
FRAME_ADDON = "frame_addons"
|
|
FENDERS = "fenders"
|
|
FENDER_FRONT = "fender_front"
|
|
FENDER_REAR = "fender_rear"
|
|
GRILL = "grill"
|
|
HOOD_ORNAMENT = "hood_ornament"
|
|
MIRROR_MOUNT = "mirror_mount"
|
|
MISCELLANEOUS = "miscellenious"
|
|
REAR = "rear"
|
|
SNORKEL = "snorkel"
|
|
STICKER = "sticker"
|
|
STICKER_EXTERIOR = "sticker_exterior"
|
|
STICKER_WINDSHIELD = "sticker_windshield"
|
|
TOP = "top"
|
|
WHEEL_ADDON = "wheel_addon"
|
|
|
|
|
|
def get_initial_pak_path():
|
|
return os.path.join(
|
|
str(os.path.expanduser("~")),
|
|
".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 = 0
|
|
self.UnlockByExploration = False
|
|
self.UnlockByObjective = False
|
|
self.UnlockByRank = -1
|
|
self.GameData = None
|
|
self.DLC = None
|
|
|
|
def load_desc(self, data: SnowRunnerXNLNode):
|
|
if "region:default" in data.children:
|
|
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):
|
|
if self.GameData is None:
|
|
self.GameData = self.pak.load_class(SnowrunnerGameObjectType.GAME_DATA, data, dlc_name=self.DLC)
|
|
else:
|
|
self.GameData.update_data(data)
|
|
self.Price = self.GameData.Price
|
|
self.UnlockByExploration = self.GameData.UnlockByExploration
|
|
self.UnlockByObjective = self.GameData.UnlockByObjective
|
|
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,
|
|
always_as_list: bool = False,
|
|
):
|
|
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
|
|
|
|
if always_as_list and not isinstance(data.children[name.lower()], list):
|
|
data.children[name.lower()] = [data.children[name.lower()]]
|
|
|
|
object_type = SnowrunnerGameObjectType(name.lower()) if object_type_override is None else object_type_override
|
|
if isinstance(data.children[name.lower()], list):
|
|
setattr(
|
|
self,
|
|
name,
|
|
[self.pak.load_class(object_type, x, dlc_name=self.DLC) for x in data.children[name.lower()]],
|
|
)
|
|
else:
|
|
setattr(
|
|
self,
|
|
name,
|
|
self.pak.load_class(object_type, data.children[name.lower()], dlc_name=self.DLC),
|
|
)
|
|
|
|
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)
|
|
|
|
if self.UiName in DLC_OVERRIDES:
|
|
self.DLC = DLC_OVERRIDES[self.UiName]
|
|
|
|
def get_translated_ui_desc(self, language: LanguageLoader) -> str:
|
|
if self.UiDesc in language:
|
|
return language[self.UiDesc]
|
|
|
|
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
|
|
self.UseTrailerFrame = False
|
|
|
|
def load_data(self, data: SnowRunnerXNLNode) -> None:
|
|
self.load_attr(data, "InitialOffset")
|
|
self.load_attr(data, "Quantity")
|
|
self.load_attr(data, "UseTrailerFrame")
|
|
self.ignore_attr("OffsetStep")
|
|
self.ignore_attr("ParentFrames")
|
|
self.loading_finished(data)
|
|
|
|
|
|
class AddonSockets(SnowrunnerClass):
|
|
def __init__(self, pak_loader: InitialPakLoader):
|
|
super().__init__(pak_loader=pak_loader)
|
|
self.DefaultAddon = None
|
|
self.Socket = None
|
|
|
|
def load_data(self, data: SnowRunnerXNLNode) -> None:
|
|
if "defaultaddon" in data.attrs:
|
|
self.DefaultAddon = self.pak.get_object(
|
|
object_type=SnowrunnerGameObjectType.TRUCK_ADDON, name=data.attrs["defaultaddon"], lazy=True
|
|
)
|
|
self.ignore_attr("defaultaddon")
|
|
self.load_child(data, "Socket", always_as_list=True)
|
|
self.ignore_attr("ParentFrame")
|
|
self.ignore_attr("RequiredAddonIfNoConflicts")
|
|
self.loading_finished(data)
|
|
|
|
|
|
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 = []
|
|
|
|
@property
|
|
def Weight(self) -> int:
|
|
return self.Mass + sum(x.Weight for x in self.Body)
|
|
|
|
def load_data(self, data: SnowRunnerXNLNode) -> None:
|
|
self.load_attr(data, "AllowedPenetrationDepth")
|
|
self.load_attr(data, "NoSoftContacts")
|
|
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", always_as_list=True)
|
|
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.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:
|
|
self.load_child(data, "Engine", always_as_list=True)
|
|
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.InstallSocket = None
|
|
self.IsCustomizable = False
|
|
self.IsDoubleTrailer = False
|
|
self.IsQuest = False
|
|
self.Price = 0
|
|
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")
|
|
if "category" in data.attrs:
|
|
self.Category = TruckAddonCategory(data.attrs["category"])
|
|
self.ignore_attr("Category")
|
|
self.load_attr(data, "Country")
|
|
self.load_attr(data, "ExcludeAddons")
|
|
self.load_attr(data, "ExcludedCargoTypes")
|
|
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", always_as_list=True)
|
|
self.load_child(data, "UiDesc")
|
|
if "installsocket" in data.children:
|
|
self.InstallSocket = data.children["installsocket"].attrs.get("type", self.InstallSocket)
|
|
self.ignore_child("InstallSocket")
|
|
|
|
self.ignore_child("AddonSlots")
|
|
self.ignore_child("AddonType")
|
|
self.ignore_child("CenteringAngularVelocity")
|
|
self.ignore_child("CraneSocket")
|
|
self.ignore_child("CustomizationCameras")
|
|
self.ignore_child("GearboxParams")
|
|
self.ignore_child("InstallSlot")
|
|
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)
|
|
|
|
def update_data(self, data: SnowRunnerXNLNode) -> None:
|
|
self.load_data(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.Gear = []
|
|
|
|
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", always_as_list=True)
|
|
self.load_child(data, "ReverseGear", object_type_override=SnowrunnerGameObjectType.GEAR)
|
|
self.load_child(data, "HighGear", object_type_override=SnowrunnerGameObjectType.GEAR)
|
|
|
|
self.Gear = list(sorted(self.Gear, key=lambda gear: gear.AngVel))
|
|
|
|
self.loading_finished(data)
|
|
|
|
|
|
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 get_gearbox(self, name: str) -> Gearbox | None:
|
|
for e in self.Gearbox:
|
|
if e.Name == name:
|
|
return e
|
|
|
|
return None
|
|
|
|
def load_data(self, data: SnowRunnerXNLNode) -> None:
|
|
self.load_child(data, "Gearbox", always_as_list=True)
|
|
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, dlc_name=self.DLC))
|
|
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
|
|
|
|
@property
|
|
def Weight(self) -> int:
|
|
if not self.Body:
|
|
return 0
|
|
if isinstance(self.Body, list):
|
|
return sum(x.Weight for x in self.Body)
|
|
else:
|
|
return self.Body.Weight
|
|
|
|
def load_data(self, data: SnowRunnerXNLNode) -> None:
|
|
self.load_attr(data, "_noinherit")
|
|
self.load_attr(data, "Mesh")
|
|
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 get_suspension_set(self, name: str) -> SuspensionSet | None:
|
|
for e in self.SuspensionSet:
|
|
if e.Name == name:
|
|
return e
|
|
|
|
return None
|
|
|
|
def load_data(self, data: SnowRunnerXNLNode) -> None:
|
|
self.load_child(data, "SuspensionSet", always_as_list=True)
|
|
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
|
|
|
|
@property
|
|
def TireCount(self) -> int:
|
|
return len(self.TruckData.Wheels.Wheel)
|
|
|
|
@property
|
|
def TireCountFront(self) -> int:
|
|
return len([x for x in self.TruckData.Wheels.Wheel if x.Location == "front"])
|
|
|
|
@property
|
|
def TireCountRear(self) -> int:
|
|
return len([x for x in self.TruckData.Wheels.Wheel if x.Location == "rear"])
|
|
|
|
@property
|
|
def DriveTrain(self) -> str:
|
|
undriven_axles = len([x for x in self.TruckData.Wheels.Wheel if x.Torque == "none"])
|
|
return f"{self.TireCount}x{self.TireCount - undriven_axles}"
|
|
|
|
@property
|
|
def Weight(self) -> int:
|
|
wheel_mass = (
|
|
self.TruckData.Wheels.DefaultTire.Mass * self.TireCountFront
|
|
+ self.TruckData.Wheels.DefaultTire.Mass
|
|
* self.TruckData.Wheels.DefaultTire.RearMassScale
|
|
* self.TireCountRear
|
|
)
|
|
default_addon_mass = sum(x.PhysicsModel.Weight for x in self.get_default_truck_addons())
|
|
|
|
return int(self.PhysicsModel.Weight + wheel_mass + default_addon_mass)
|
|
|
|
def get_shop_price(self) -> int:
|
|
return (
|
|
self.Price
|
|
+ self.TruckData.DefaultEngine.Price
|
|
+ self.TruckData.DefaultGearbox.Price
|
|
+ self.TruckData.DefaultSuspension.Price
|
|
+ self.TruckData.DefaultWinch.Price
|
|
+ self.TruckData.Wheels.DefaultTire.Price
|
|
+ sum(x.DefaultAddon.Price for x in self.GameData.AddonSockets if x.DefaultAddon is not None)
|
|
)
|
|
|
|
def get_addon_socket_names(self) -> list[str]:
|
|
return [s.Names for s in [y for x in self.GameData.AddonSockets for y in x.Socket]]
|
|
|
|
def get_available_truck_addons(self, category: TruckAddonCategory) -> list[TruckAddon]:
|
|
return [
|
|
ta
|
|
for ta in self.pak.get_object_list(SnowrunnerGameObjectType.TRUCK_ADDON)
|
|
if ta.GameData.InstallSocket in self.get_addon_socket_names() and ta.GameData.Category == category
|
|
]
|
|
|
|
def get_default_truck_addons(self) -> list[TruckAddon]:
|
|
return [x.DefaultAddon for x in self.GameData.AddonSockets if x.DefaultAddon is not None]
|
|
|
|
@property
|
|
def AWDInfo(self) -> TruckInfo:
|
|
torques = {x.Torque.lower() for x in self.TruckData.Wheels.Wheel}
|
|
if len(torques) == 1 and "default" in torques:
|
|
return TruckInfo.ALWAYS
|
|
elif "connectable" in torques:
|
|
return TruckInfo.CAPABLE
|
|
elif "full" in torques:
|
|
return TruckInfo.SWITCHABLE
|
|
elif "none" in torques:
|
|
return TruckInfo.NOT_AVAILABLE
|
|
else:
|
|
raise ValueError(f"Truck {self.UiName} AWD Wheel Torques {torques} is not supported")
|
|
|
|
@property
|
|
def DiffLockInfo(self) -> TruckInfo:
|
|
if self.TruckData.DiffLockType == "Always":
|
|
return TruckInfo.ALWAYS
|
|
elif (
|
|
self.TruckData.DiffLockType == "Installed"
|
|
or self.TruckData.DiffLockType == "Switchable"
|
|
or self.TruckData.DiffLockType == "Connected"
|
|
or self.TruckData.DiffLockType == "Uninstalled"
|
|
or self.TruckData.DiffLockType == "None"
|
|
): # DiffLockType seems to have no meaning. At all. We have to figure it out ourselfs.
|
|
diff_lock_addons = self.get_available_truck_addons(TruckAddonCategory.DIFF_LOCK)
|
|
if len(diff_lock_addons) == 0:
|
|
return TruckInfo.NOT_AVAILABLE
|
|
elif len(diff_lock_addons) == 1:
|
|
return TruckInfo.SWITCHABLE
|
|
else:
|
|
installed_addon = list(set(diff_lock_addons) & set(self.get_default_truck_addons()))
|
|
if installed_addon[0].TruckData.DiffLockInstalled:
|
|
return TruckInfo.SWITCHABLE
|
|
else:
|
|
return TruckInfo.CAPABLE
|
|
else:
|
|
raise ValueError(f"Truck {self.UiName} DiffLockType {self.TruckData.DiffLockType} is not supported")
|
|
|
|
def load_data(self, data: SnowRunnerXNLNode) -> None:
|
|
self.load_attr(data, "AttachType")
|
|
self.load_attr(data, "IsDeprecated")
|
|
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.AddonSlots = 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"], "AddonSlots")
|
|
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):
|
|
truck_types = {
|
|
"HEAVY": "CODEX_TYPE_TRUCK_6_HEADER",
|
|
"HEAVY_DUTY": "CODEX_TYPE_TRUCK_4_HEADER",
|
|
"SCOUT": "CODEX_TYPE_TRUCK_3_HEADER",
|
|
"HIGHWAY": "CODEX_TYPE_TRUCK_2_HEADER",
|
|
"OFFROAD": "CODEX_TYPE_TRUCK_5_HEADER",
|
|
}
|
|
|
|
def __init__(self, pak_loader: InitialPakLoader):
|
|
super().__init__(pak_loader=pak_loader)
|
|
self.AllWheelDriveInstalled = False
|
|
self.BackSteerSpeed = 0.0
|
|
self.CompatibleWheels = []
|
|
self.Damage = None
|
|
self.DefaultEngine = None
|
|
self.DefaultGearbox = None
|
|
self.DefaultSuspension = None
|
|
self.DefaultTire = None
|
|
self.DefaultWinch = None
|
|
self.DiffLockInstalled = False
|
|
self.DiffLockType = ""
|
|
self.Engines = []
|
|
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")
|
|
if "trucktype" in data.attrs:
|
|
self.TruckType = self.truck_types[data.attrs["trucktype"]]
|
|
self.ignore_attr("trucktype")
|
|
self.load_attr(data, "WaterCapacity")
|
|
self.load_attr(data, "WheelRepairsCapacity")
|
|
self.load_child(data, "CompatibleWheels", always_as_list=True)
|
|
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.DefaultEngine = list(
|
|
{x.get_engine(data.children["enginesocket"].attrs["default"]) for x in self.Engines} - {None}
|
|
)[0]
|
|
self.ignore_child("EngineSocket")
|
|
|
|
if "gearboxsocket" in data.children:
|
|
self.Gearboxes = [
|
|
self.pak.get_object(object_type=SnowrunnerGameObjectType.GEARBOX_VARIANTS, name=n.strip())
|
|
for n in data.children["gearboxsocket"].attrs["type"].split(",")
|
|
]
|
|
self.DefaultGearbox = list(
|
|
{x.get_gearbox(data.children["gearboxsocket"].attrs["default"]) for x in self.Gearboxes} - {None}
|
|
)[0]
|
|
self.ignore_child("GearboxSocket")
|
|
|
|
if "suspensionsocket" in data.children:
|
|
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.DefaultSuspension = list(
|
|
{x.get_suspension_set(data.children["suspensionsocket"].attrs["default"]) for x in self.Suspensions}
|
|
- {None}
|
|
)[0]
|
|
self.ignore_child("SuspensionSocket")
|
|
|
|
if "winchupgradesocket" in data.children:
|
|
self.Winches = [
|
|
self.pak.get_object(object_type=SnowrunnerGameObjectType.WINCH_VARIANTS, name=n.strip())
|
|
for n in data.children["winchupgradesocket"].attrs["type"].split(",")
|
|
]
|
|
self.DefaultWinch = list(
|
|
{x.get_winch(data.children["winchupgradesocket"].attrs["default"]) for x in self.Winches} - {None}
|
|
)[0]
|
|
self.ignore_child("WinchUpgradeSocket")
|
|
|
|
self.ignore_attr("_noinherit")
|
|
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", always_as_list=True)
|
|
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, dlc_name=self.DLC))
|
|
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 get_truck_tire(self, name: str) -> TruckTire | None:
|
|
for x in self.TruckTires.TruckTire:
|
|
if x.Name == name:
|
|
return x
|
|
|
|
return None
|
|
|
|
def load_data(self, data: SnowRunnerXNLNode) -> None:
|
|
self.load_attr(data, "DamageCapacity")
|
|
self.load_attr(data, "Mass")
|
|
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
|
|
self.DefaultTire = None
|
|
self.DefaultWheelType = 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")
|
|
if "defaulttire" in data.attrs:
|
|
self.DefaultTire = self.DefaultWheelType.get_truck_tire(data.attrs["defaulttire"])
|
|
self.ignore_attr("DefaultTire")
|
|
|
|
self.ignore_attr("DefaultRim")
|
|
self.load_child(data, "Wheel", always_as_list=True)
|
|
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 get_winch(self, name: str) -> Winch | None:
|
|
for e in self.Winch:
|
|
if e.Name == name:
|
|
return e
|
|
|
|
return None
|
|
|
|
def load_data(self, data: SnowRunnerXNLNode) -> None:
|
|
self.load_child(data, "Winch", always_as_list=True)
|
|
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
|
|
|
|
def get_sanitized(self, key: str, default: str | None = None) -> str:
|
|
if key not in self:
|
|
if default is not None:
|
|
return default
|
|
else:
|
|
raise KeyError(key)
|
|
|
|
return " ".join(x[0] + x[1:].lower() for x in re.split(r"[-_ ]+", self[key]))
|
|
|
|
|
|
TYPE_CLASS_MATRIX = {
|
|
"addonslots": AddonSlots,
|
|
"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"
|
|
|
|
@property
|
|
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.dlc_names = {x: {} for x in SnowrunnerGameObjectType}
|
|
self.raw_data = {x: {} for x in SnowrunnerGameObjectType}
|
|
self.game_data = {x: {} for x in SnowrunnerGameObjectType}
|
|
self.templates = {x: {} for x in SnowrunnerGameObjectType}
|
|
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,
|
|
dlc_name: str,
|
|
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.DLC = dlc_name
|
|
ret.update_data(data)
|
|
return ret
|
|
|
|
if not isinstance(data, list):
|
|
if "_template" not in data.attrs:
|
|
ret = object_type.snowrunner_class(pak_loader=self)
|
|
else:
|
|
ret = object_type.snowrunner_class.from_template(
|
|
pak_loader=self, template=self.get_template(object_type=object_type, name=data.attrs["_template"])
|
|
)
|
|
|
|
ret.DLC = dlc_name
|
|
ret.load_data(data)
|
|
else:
|
|
ret = []
|
|
for sub_data in data:
|
|
if "_template" not in sub_data.attrs:
|
|
ret.append(object_type.snowrunner_class(pak_loader=self))
|
|
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].DLC = dlc_name
|
|
ret[-1].load_data(sub_data)
|
|
|
|
return ret
|
|
|
|
def load_templates(self, data: dict, dlc_name: str, 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,
|
|
dlc_name=dlc_name,
|
|
)
|
|
else:
|
|
self.templates[category][tp_name] = self.load_class(
|
|
object_type=object_type, data=sub_data, dlc_name=dlc_name
|
|
)
|
|
if defer:
|
|
for tp_name, sub_data in defer:
|
|
if local:
|
|
self.templates_local[category][tp_name] = self.load_class(
|
|
object_type=object_type,
|
|
data=sub_data,
|
|
dlc_name=dlc_name,
|
|
)
|
|
else:
|
|
self.templates[category][tp_name] = self.load_class(
|
|
object_type=object_type, data=sub_data, dlc_name=dlc_name
|
|
)
|
|
|
|
elif category in self.ignore_categories:
|
|
pass # Ignore for now
|
|
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_object_list(self, object_type: SnowrunnerGameObjectType) -> list:
|
|
return list(self.game_data[object_type].values())
|
|
|
|
def get_sorted_object_list(
|
|
self, object_type: SnowrunnerGameObjectType, language: LanguageLoader
|
|
) -> list[tuple[str, SnowrunnerClass]]:
|
|
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, dlc_name: str, data: dict):
|
|
cat_in_matrix = set(data.keys()) & set(SnowrunnerGameObjectType.values())
|
|
cat_in_ignore = set(data.keys()) & self.ignore_categories
|
|
|
|
if cat_in_matrix:
|
|
self.raw_data[SnowrunnerGameObjectType(list(cat_in_matrix)[0])][ref] = data
|
|
self.dlc_names[SnowrunnerGameObjectType(list(cat_in_matrix)[0])][ref] = dlc_name
|
|
elif cat_in_ignore:
|
|
pass # not sure what to do with this, yet
|
|
else:
|
|
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
|
|
|
|
dlc_name = member.filename.split("\\")[2] if "_dlc" in member.filename else None
|
|
filename_ref = member.filename.split("\\")[-1].replace(".xml", "")
|
|
logger.info(f" - {member.filename}")
|
|
with self.pak_zip.open(member) as d_file:
|
|
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, dlc_name=dlc_name)
|
|
continue
|
|
elif len(data) > 3:
|
|
logger.error(f"Multiple keys?! ({list(data.keys())}")
|
|
sys.exit(1)
|
|
|
|
self.store_raw(filename_ref, dlc_name, 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, dlc_name=self.dlc_names[object_type][ref]
|
|
)
|
|
|
|
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]
|
|
|
|
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, dlc_name=self.dlc_names[object_type][ref]
|
|
)
|
|
sr_class = self.load_class(
|
|
object_type,
|
|
data[category],
|
|
parent=data["_parent"].get_attr("file"),
|
|
lazy=True,
|
|
dlc_name=self.dlc_names[object_type][ref],
|
|
)
|
|
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], dlc_name=self.dlc_names[object_type][ref]
|
|
)
|
|
deferred_loads = []
|
|
|
|
for cat, v in self.raw_data.items():
|
|
if v:
|
|
logger.error(f"Unhandled Category: {cat}")
|