snowrunner-data-extract/output.py
2026-08-24 12:19:48 +02:00

482 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import os
from constants import DLC_NAMES
from loader import (
InitialPakLoader,
LanguageLoader,
SnowrunnerGameObjectType,
TruckAddonCategory,
GameData,
SnowrunnerClass,
)
from logging import getLogger
logger = getLogger(__name__)
def get_head(language: LanguageLoader, category: str, name: str | None = None) -> str:
return f"""<!DOCTYPE html>\n<html lang='{language.Ident}'>\n""" + (
f"""
<head>
<title>SnowRunnerDB - {category}"""
+ (f""" - {name}""" if name is not None else "")
+ f"""</title>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<link href="https://cdn.datatables.net/v/dt/dt-3.0.1/datatables.min.css" rel="stylesheet">
<script src="https://cdn.datatables.net/v/dt/dt-3.0.1/datatables.min.js"></script>
</head>
<body>
<div id='navi_lang'></div>
<div id='categories'>
<ul id='navi_cat'>
<li><a href="/{language.Name}/trucks/">Trucks</a></li>
<li><a href="/{language.Name}/frame_addons/">Frame Addons</a></li>
</ul>
</div>
<div id='content'>
"""
)
def get_footer():
return f"""
</div>
</body>\n</html>\n
"""
def write_html(
file_path: str | os.PathLike, language: LanguageLoader, content: str, category: str, name: str | None = None
):
with open(file_path, "w") as f:
f.write(get_head(language=language, category=category, name=name))
f.write(content)
f.write(get_footer())
def write_trucks(pak: InitialPakLoader, destination_directory: str, language: LanguageLoader):
index_content = " <h1>Trucks</h1>\n"
index_content += f" <table id='truck_index' class='display'>\n"
index_content += (
f" <thead><tr>\n"
f" <th>Name</th>\n"
f" <th>Class</th>\n"
f" <th>DLC</th>\n"
f" <th>Unlock by</th>\n"
f" <th>Region</th>\n"
f" <th>Price</th>\n"
f" <th>Weight</th>\n"
f" <th>Drivetrain</th>\n"
f" <th>AWD</th>\n"
f" <th>DiffLock</th>\n"
f" </tr></thead>\n"
)
index_content += f" <tbody>\n"
for reference, truck in pak.get_sorted_object_list(SnowrunnerGameObjectType.TRUCK, language):
if truck.Type != "Truck":
continue
truck_file = os.path.join(destination_directory, reference + ".html")
truck_content = f" <h1>{truck.get_translated_ui_name(language)}</h1>"
truck_content += f" <table>\n"
try:
truck_content += f" <tr><td>DLC</td><td>{DLC_NAMES[truck.DLC]}</td></tr>\n"
except KeyError:
print(f"DEBUG: DLC {truck.DLC} for Truck {truck.get_translated_ui_name(language)}")
exit(1)
truck_content += f" <tr><td>Weight</td><td>{truck.Weight}kg</td></tr>\n"
truck_content += f" <tr><td>AWD</td><td>{language[truck.AWDInfo.value]}</td></tr>\n"
truck_content += f" <tr><td>DiffLock</td><td>{language[truck.DiffLockInfo.value]}</td></tr>\n"
truck_content += f" <tr><td>Def Engine</td><td>{truck.TruckData.DefaultEngine.get_translated_ui_name(language)}</td></tr>\n"
truck_content += f" <tr><td>Def Gearbox</td><td>{truck.TruckData.DefaultGearbox.get_translated_ui_name(language)}</td></tr>\n"
truck_content += f" <tr><td>Def Suspension</td><td>{truck.TruckData.DefaultSuspension.get_translated_ui_name(language)}</td></tr>\n"
truck_content += f" <tr><td>Def Winch</td><td>{truck.TruckData.DefaultWinch.get_translated_ui_name(language)}</td></tr>\n"
truck_content += f" <tr><td>Def Tire</td><td>{truck.TruckData.Wheels.DefaultTire.get_translated_ui_name(language)}</td></tr>\n"
truck_content += f" </table>\n"
# Engines
truck_content += f" <h2>Engines</h2>\n"
engines = [x for y in truck.TruckData.Engines for x in y.Engine]
truck_content += f" <table id='engines' data-order='[[2, \"asc\"]]'>\n"
truck_content += (
f" <thead><tr>\n"
f" <th>Name</th>\n"
f" <th>Price</th>\n"
f" <th>Unlock By</th>\n"
f" <th>Base Fuel Consumption</th>\n"
f" <th>Torque</th>\n"
f" <th>Power-to-Weight</th>\n"
f" </tr></thead>\n"
)
truck_content += f" <tbody>\n"
# Power2Weight
# Power2Fuel
for engine in sorted(engines, key=lambda x: x.Torque):
truck_content += (
f" <tr>"
f" <td>{engine.get_translated_ui_name(language)}</td>"
f" <td>{engine.GameData.Price}</th>"
f" <td>{engine.GameData.get_unlock_str()}</td>"
f" <td>{engine.FuelConsumption}</td>"
f" <td>{engine.Torque}</td>"
f" <td>{engine.Torque / truck.Weight:.2f}</td>"
f" </tr>\n"
)
truck_content += f" </tbody></table>\n"
# Gearboxes
gearboxes = [y for x in truck.TruckData.Gearboxes for y in x.Gearbox]
max_gear_count = max(len(x.Gear) for x in gearboxes)
truck_content += f" <h2>Gearboxes</h2>\n"
truck_content += f" <h3>General</h3>\n"
truck_content += f" <table id='gearboxes_general'>\n"
truck_content += (
f" <thead><tr>\n"
f" <th>Name</th>\n"
f" <th>Price</th>\n"
f" <th>Unlock By</th>\n"
f" <th>Fine-Tunable</th>\n"
f" <th>L-</th>\n"
f" <th>L</th>\n"
f" <th>L+</th>\n"
f" <th>H</th>\n"
f" <th>Gears</th>\n"
f" </tr></thead>\n"
)
truck_content += f" <tbody>\n"
for gearbox in sorted(gearboxes, key=lambda x: x.get_translated_ui_name(language)):
truck_content += f" <tr>\n"
truck_content += f" <td>{gearbox.get_translated_ui_name(language)}</td>\n"
truck_content += f" <td>{gearbox.Price}</td>\n"
truck_content += f" <td>{gearbox.GameData.get_unlock_str()}</td>\n"
truck_content += (
f" <td>{'&check;' if gearbox.GearboxParams.IsManualLowGear else '&dot;'}</td>\n"
)
truck_content += (
f" <td>{'&check;' if gearbox.GearboxParams.IsLowerMinusGearExists else '&dot;'}</td>\n"
)
truck_content += (
f" <td>{'&check;' if gearbox.GearboxParams.IsLowerGearExists else '&dot;'}</td>\n"
)
truck_content += (
f" <td>{'&check;' if gearbox.GearboxParams.IsLowerPlusGearExists else '&dot;'}</td>\n"
)
truck_content += (
f" <td>{'&check;' if gearbox.GearboxParams.IsHighGearExists else '&dot;'}</td>\n"
)
truck_content += f" <td>{len(gearbox.Gear)}</td>\n"
truck_content += f" </tr>\n"
truck_content += f" </tbody></table>\n"
truck_content += f" <h3>Fuel Consumption</h3>\n"
truck_content += f" <table id='gearboxes_fuel'>\n"
truck_content += (
f" <thead><tr>\n"
f" <th>Name</th>\n"
f" <th>Fuel Consumption</th>\n"
f" <th>AWD Fuel Modifier</th>\n"
)
for i in range(max_gear_count):
gear_num = str(i + 1) if i > 0 else "L+/1"
truck_content += f" <th>{gear_num}</th>\n"
truck_content += f" </tr></thead>\n"
truck_content += f" <tbody>\n"
for gearbox in sorted(gearboxes, key=lambda x: x.get_translated_ui_name(language)):
truck_content += f" <tr>\n"
truck_content += f" <td>{gearbox.get_translated_ui_name(language)}</td>\n"
truck_content += f" <td>{gearbox.FuelConsumption}</td>\n"
truck_content += f" <td>{gearbox.AWDConsumptionModifier}</td>\n"
for i in range(max_gear_count):
if i >= len(gearbox.Gear):
truck_content += f" <td>-</td>\n"
else:
truck_content += f" <td>{gearbox.Gear[i].FuelModifier}</td>\n"
truck_content += f" </tr>\n"
truck_content += f" </tbody></table>\n"
# Suspensions
truck_content += f" <h2>Suspensions</h2>\n"
# Wheels
truck_content += f" <h2>Tires</h2>\n"
wheel_cat = {}
scales = set()
for cw in truck.TruckData.CompatibleWheels:
scales.add(cw.get_size_in_inches())
damage_cap = cw.TruckWheels.DamageCapacity
width = int(cw.TruckWheels.Width * cw.Scale * 100)
mass = cw.TruckWheels.Mass
# FIXME: cw.TruckWheels.GameData.ReadMassScale (affects the truck mass for dualies)
for tt in cw.TruckWheels.TruckTires.TruckTire:
if tt.Mass > 0:
mass = tt.Mass
if tt.Width > 0:
width = int(tt.Width * cw.Scale * 100)
cat = tt.WheelFriction.get_translated_ui_name(language)
if cat not in wheel_cat:
wheel_cat[cat] = {}
wheel_cat[cat][tt.UiName] = (damage_cap, width, mass, tt)
# Mud_Performance=
# Truck_Weight / (Tire_Count×(Tire_Diameter×Width×Softness)) * Tire_Mud_Rating
for cat in sorted(wheel_cat):
truck_content += f" <h3>{cat}</h3>\n"
truck_content += f" <table>\n"
truck_content += f" <tr><th colspan='4'>&nbsp;</th><th colspan='3'>Friction</th></tr>\n"
truck_content += f" <tr><th>Name</th><th>Price</th><th>Mass</th><th>Width</th><th>Asphalt</th><th>Off-Road</th><th>Mud</th></tr>\n"
for damage_cap, width, mass, tt in sorted(
wheel_cat[cat].values(), key=lambda v: v[3].get_translated_ui_name(language)
):
truck_content += f" <tr><td>{tt.get_translated_ui_name(language)}</td><td>{tt.GameData.Price}</td><td>{mass}kg</td><td>{width}cm</td><td>{tt.WheelFriction.BodyFrictionAsphalt}</td><td>{tt.WheelFriction.BodyFriction}</td><td>{tt.WheelFriction.SubstanceFriction}</td></tr>\n"
truck_content += f" </table>\n"
scales = ", ".join(['%d"' % x for x in sorted(scales)])
truck_content += f" <h4>Available Tire Sizes: {scales}</h4>\n"
# Winches
truck_content += f" <h2>Winches</h2>\n"
# Diff. Lock
diff_lock_addons = truck.get_available_truck_addons(TruckAddonCategory.DIFF_LOCK)
if diff_lock_addons:
truck_content += f" <h2>Diff. Lock</h2>\n"
truck_content += f" <table id='diff_lock'>\n"
truck_content += (
f" <thead><tr>\n"
f" <th>Name</th>\n"
f" <th>Price</th>\n"
f" <th>Unlock By</th>\n"
f" </tr></thead><tbody>\n"
)
for addon in sorted(diff_lock_addons, key=lambda v: v.get_translated_ui_name(language)):
truck_content += (
f" <tr>\n"
f" <td>{addon.get_translated_ui_name(language)}</td>\n"
f" <td>{addon.Price}</td>\n"
f" <td>{addon.GameData.get_unlock_str()}</td>\n"
f" </tr>\n"
)
truck_content += f" </tbody></table>\n"
# Spare Wheel (if any)
# Snorkel (if any)
snorkel_addons = truck.get_available_truck_addons(TruckAddonCategory.SNORKEL)
if snorkel_addons:
truck_content += f" <h2>Snorkel</h2>\n"
truck_content += f" <table id='snorkel'>\n"
truck_content += (
f" <thead><tr>\n"
f" <th>Name</th>\n"
f" <th>Price</th>\n"
f" <th>Unlock By</th>\n"
f" <th>Mass</th>\n"
f" </tr></thead><tbody>\n"
)
for addon in sorted(snorkel_addons, key=lambda v: v.get_translated_ui_name(language)):
truck_content += (
f" <tr>\n"
f" <td>{addon.get_translated_ui_name(language)}</td>\n"
f" <td>{addon.Price}</td>\n"
f" <td>{addon.GameData.get_unlock_str()}</td>\n"
f" <td>{addon.PhysicsModel.Weight}kg</td>\n"
f" </tr>\n"
)
truck_content += f" </tbody></table>\n"
# AWD
awd_addons = truck.get_available_truck_addons(TruckAddonCategory.ALL_WHEEL_DRIVE)
if awd_addons:
truck_content += f" <h2>AWD</h2>\n"
truck_content += f" <table id='awd'>\n"
truck_content += (
f" <thead><tr>\n"
f" <th>Name</th>\n"
f" <th>Price</th>\n"
f" <th>Unlock By</th>\n"
f" <th>Mass</th>\n"
f" </tr></thead><tbody>\n"
)
for addon in sorted(awd_addons, key=lambda v: v.get_translated_ui_name(language)):
truck_content += (
f" <tr>\n"
f" <td>{addon.get_translated_ui_name(language)}</td>\n"
f" <td>{addon.Price}</td>\n"
f" <td>{addon.GameData.get_unlock_str()}</td>\n"
f" <td>{addon.PhysicsModel.Weight}kg</td>\n"
f" </tr>\n"
)
truck_content += f" </tbody></table>\n"
# Frame Addons
truck_content += f" <h2>Frame Addons</h2>\n"
truck_content += f" <table id='frame_addons'>\n"
truck_content += (
f" <thead><tr>\n"
f" <th>Name</th>\n"
f" <th>Price</th>\n"
f" <th>Unlock By</th>\n"
f" <th>Mass</th>\n"
f" <th>Cargo Slots</th>\n"
f" <th>Repair Points</th>\n"
f" <th>Spare Wheels</th>\n"
f" <th>Fuel Capacity</th>\n"
f" <th>Water Capacity</th>\n"
f" </tr></thead><tbody>\n"
)
for truck_addon in sorted(
truck.get_available_truck_addons(TruckAddonCategory.FRAME_ADDON),
key=lambda v: v.get_translated_ui_name(language),
):
truck_content += f" <tr>\n"
truck_content += f" <td>{truck_addon.get_translated_ui_name(language)}</td>\n"
truck_content += f" <td>{truck_addon.Price}</td>\n"
truck_content += f" <td>{truck_addon.GameData.get_unlock_str()}</td>\n"
truck_content += f" <td>{truck_addon.PhysicsModel.Weight}kg</td>\n"
truck_content += f" <td>{truck_addon.AddonSlots.Quantity if truck_addon.AddonSlots is not None else '&nbsp;'}</td>\n"
truck_content += f" <td>{truck_addon.TruckData.RepairsCapacity if truck_addon.TruckData is not None and truck_addon.TruckData.RepairsCapacity > 0 else '&nbsp;'}</td>\n"
truck_content += f" <td>{truck_addon.TruckData.WheelRepairsCapacity if truck_addon.TruckData is not None and truck_addon.TruckData.WheelRepairsCapacity > 0 else '&nbsp;'}</td>\n"
truck_content += f" <td>{truck_addon.TruckData.FuelCapacity if truck_addon.TruckData is not None and truck_addon.TruckData.FuelCapacity > 0 else '&nbsp;'}</td>\n"
truck_content += f" <td>{truck_addon.TruckData.WaterCapacity if truck_addon.TruckData is not None and truck_addon.TruckData.WaterCapacity > 0 else '&nbsp;'}</td>\n"
truck_content += f" </tr>\n"
truck_content += f" </tbody></table>\n"
truck_content += f" <script>\n"
truck_content += " document.addEventListener('DOMContentLoaded', function() {new DataTable('#engines', {info: false, paging: false, searching: false});});\n"
truck_content += " document.addEventListener('DOMContentLoaded', function() {new DataTable('#gearboxes_general', {info: false, paging: false, searching: false});});\n"
truck_content += " document.addEventListener('DOMContentLoaded', function() {new DataTable('#gearboxes_fuel', {info: false, paging: false, searching: false});});\n"
truck_content += " document.addEventListener('DOMContentLoaded', function() {new DataTable('#diff_lock', {info: false, paging: false, searching: false});});\n"
truck_content += " document.addEventListener('DOMContentLoaded', function() {new DataTable('#snorkel', {info: false, paging: false, searching: false});});\n"
truck_content += " document.addEventListener('DOMContentLoaded', function() {new DataTable('#awd', {info: false, paging: false, searching: false});});\n"
truck_content += " document.addEventListener('DOMContentLoaded', function() {new DataTable('#frame_addons', {info: false, paging: false, searching: false});});\n"
truck_content += f" </script>\n"
write_html(
truck_file,
language=language,
content=truck_content,
category="Trucks",
name=truck.get_translated_ui_name(language),
)
index_content += (
f" <tr>\n"
f" <td><a href='{reference}.html'>{truck.get_translated_ui_name(language)}</a></td>\n"
f" <td>{language.get_sanitized(truck.TruckData.TruckType)}</td>\n"
f" <td>{DLC_NAMES[truck.DLC]}</td>\n"
f" <td>{truck.GameData.get_unlock_str()}</td>\n"
f" <td>{truck.GameData.Country}</td>\n"
f" <td>{truck.get_shop_price()}</td>\n"
f" <td>{truck.Weight}</td>\n"
f" <td>{truck.DriveTrain}</td>\n"
f" <td>{language[truck.AWDInfo.value]}</td>\n"
f" <td>{language[truck.DiffLockInfo.value]}</td>\n"
f" </tr>\n"
)
index_content += f" </tbody></table>\n"
index_content += " <script>document.addEventListener('DOMContentLoaded', function() {new DataTable('#truck_index', {paging: false});});</script>\n"
write_html(
os.path.join(destination_directory, "index.html"), language=language, content=index_content, category="Trucks"
)
def write_frame_addons(pak: InitialPakLoader, destination_directory: str, language: LanguageLoader):
index_content = " <h1>Frame Addons</h1>\n"
index_content += f" <table id='frame_addons' class='display'>\n"
index_content += (
f" <thead><tr>\n"
f" <th>Name</th>\n"
f" <th>Price</th>\n"
f" <th>Unlock By</th>\n"
f" <th>Mass</th>\n"
f" <th>Cargo Slots</th>\n"
f" <th>Repair Points</th>\n"
f" <th>Spare Wheels</th>\n"
f" <th>Fuel Capacity</th>\n"
f" <th>Water Capacity</th>\n"
f" <th>Available for</th>\n"
f" </tr></thead>\n"
)
index_content += f" <tbody>\n"
for _, frame_addon in pak.get_sorted_object_list(SnowrunnerGameObjectType.TRUCK_ADDON, language=language):
if frame_addon.GameData.Category != TruckAddonCategory.FRAME_ADDON:
continue
if (
"TRANSLATION NOT FOUND" in frame_addon.get_translated_ui_name(language)
or len(frame_addon.get_translated_ui_name(language))
> 100 # This is a hidden frame addon where somebody swapped UI_NAME and UI_DESC
):
continue # FIXME: How To better detect Hidden Frame Addons not shown in game?
# Also e.g. Mack Defense has a Spare Wheel Addon that's always installed and not shown in game?!
available_trucks = [
x
for x in pak.get_sorted_object_list(SnowrunnerGameObjectType.TRUCK, language=language)
if x[1].Type == "Truck" and frame_addon.GameData.InstallSocket in x[1].get_addon_socket_names()
]
if not available_trucks:
continue
index_content += f" <tr>\n"
index_content += f" <td>{frame_addon.get_translated_ui_name(language)}</td>\n"
index_content += f" <td>{frame_addon.Price}</td>\n"
index_content += f" <td>{frame_addon.GameData.get_unlock_str()}</td>\n"
index_content += f" <td>{frame_addon.PhysicsModel.Weight}kg</td>\n"
index_content += f" <td>{frame_addon.AddonSlots.Quantity if frame_addon.AddonSlots is not None else '&nbsp;'}</td>\n"
index_content += f" <td>{frame_addon.TruckData.RepairsCapacity if frame_addon.TruckData is not None and frame_addon.TruckData.RepairsCapacity > 0 else '&nbsp;'}</td>\n"
index_content += f" <td>{frame_addon.TruckData.WheelRepairsCapacity if frame_addon.TruckData is not None and frame_addon.TruckData.WheelRepairsCapacity > 0 else '&nbsp;'}</td>\n"
index_content += f" <td>{frame_addon.TruckData.FuelCapacity if frame_addon.TruckData is not None and frame_addon.TruckData.FuelCapacity > 0 else '&nbsp;'}</td>\n"
index_content += f" <td>{frame_addon.TruckData.WaterCapacity if frame_addon.TruckData is not None and frame_addon.TruckData.WaterCapacity > 0 else '&nbsp;'}</td>\n"
truck_links = ", ".join(
f'<a href="../trucks/{ref}.html">{truck.get_translated_ui_name(language)}</a>'
for ref, truck in available_trucks
)
index_content += f" <td>{truck_links}</td>\n"
index_content += f" </tr>\n"
index_content += f" </tbody>\n"
index_content += f" </table>\n"
index_content += " <script>document.addEventListener('DOMContentLoaded', function() {new DataTable('#frame_addons', {paging: false});});</script>\n"
write_html(
os.path.join(destination_directory, "index.html"),
language=language,
content=index_content,
category="Frame Addons",
)
def create_html(pak: InitialPakLoader, destination_directory: str = "output/"):
if not os.path.exists(destination_directory):
os.mkdir(destination_directory)
logger.info("Writing HTML Output")
for language_name, language_data in pak.languages.items():
logger.info(f" - {language_name}")
language_path = os.path.join(destination_directory, language_name)
if not os.path.exists(language_path):
os.mkdir(language_path)
truck_path = os.path.join(destination_directory, language_name, "trucks")
if not os.path.exists(truck_path):
os.mkdir(truck_path)
write_trucks(pak=pak, destination_directory=truck_path, language=language_data)
frame_addon_path = os.path.join(destination_directory, language_name, "frame_addons")
if not os.path.exists(frame_addon_path):
os.mkdir(frame_addon_path)
write_frame_addons(pak=pak, destination_directory=frame_addon_path, language=language_data)