"""
The MIT License (MIT)
Copyright (c) 2023-present AbstractUmbra
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
import random
from pathlib import Path
from typing import TYPE_CHECKING, Any, Generic, TypeVar
from .crypt import decrypt, encrypt
from .enums import BestiaryEntry, ExtraUnlock, Item, Moon, Scrap, ShipUnlock
from .item import GrabbableScrap
from .utils import MISSING, SaveValue, _to_json, resolve_save_path # type: ignore[reportPrivateUsage] we allow this here.
from .vector import Vector
if TYPE_CHECKING:
from os import PathLike
from types import TracebackType
from typing_extensions import Self
from .types_.challenge_file import ChallengeFile as ChallengeFileType
from .types_.config_file import ConfigFile as ConfigFileType
from .types_.save_file import (
SaveFile as SaveFileType,
)
from .types_.shared import *
SaveT = TypeVar("SaveT", "SaveFileType", "ConfigFileType", "ChallengeFileType")
TEMP_FILE = Path("./_previously_decrypted_file.json")
TIPS = [
"LC_MoveObjectsTip",
"LC_StorageTip",
"LC_LightningTip",
"LCTip_SecureDoors",
"LC_EclipseTip",
"LCTip_SellScrap",
"LCTip_UseManual",
"LC_IntroTip1",
]
__all__ = (
"SaveFile",
"ConfigFile",
"ChallengeFile",
)
class _BaseSaveFile(Generic[SaveT]):
_inner_data: SaveT
_file_type: str
_extra_data: dict[str, Any]
_written: bool
_skip_parsing: bool
__slots__ = (
"_inner_data",
"_file_type",
"_extra_data",
"_written",
"_skip_parsing",
"_raw_data",
)
def __init__(self, data: bytes, /) -> None:
self._skip_parsing = False
self._written = False
self._raw_data: bytes = data
self._parse_file()
def __enter__(self) -> Self:
return self
def __exit__(
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
) -> None:
if not self._written and not exc_type:
self.write()
@classmethod
def from_path(cls, path: Path | PathLike[Any] | str) -> Self:
if not isinstance(path, Path):
path = Path(path)
if not path.exists():
raise FileNotFoundError("The passed file is not found.")
with path.open("rb") as fp:
return cls(fp.read())
def _parse_file(self) -> None:
if self._skip_parsing:
return
data = decrypt(data=self._raw_data)
self._validate_contents(data)
self._inner_data = data
def _upsert_value(self, key_name: str, value: Any) -> None:
if value is MISSING:
return # If the value is the sentinel type, do nothing and move onto the next
if isinstance(value, int):
_type = "int"
elif isinstance(value, list):
if value and isinstance(value[0], int):
_type = "System.Int32[],mscorlib"
elif value and isinstance(value[0], dict):
_type = "UnityEngine.Vector3[],UnityEngine.CoreModule"
else:
raise ValueError("Unexpected or unknown array type passed for `value`")
elif isinstance(value, bool):
_type = "bool"
else:
raise ValueError("Unexpected type passed for `value`: %r (%s)", value, type(value))
try:
self._inner_data[key_name]["value"] = value
except KeyError:
self._inner_data[key_name] = {"__type": _type, "value": value}
def _dump(self, path: Path, /) -> None:
decrypted_result = _to_json(self._inner_data)
encoded = decrypted_result.encode()
with TEMP_FILE.open("wb") as fp:
fp.write(encoded)
encrypted_result = encrypt(data=encoded)
with path.open("wb") as fp:
fp.write(encrypted_result)
def write(self, *, path: Path | None = None) -> None:
for key, value in self._extra_data.items():
self._upsert_value(key, value)
if path:
self._dump(path)
self._written = True
def _validate_contents(self, data: SaveT, /) -> None:
raise NotImplementedError
[docs]class SaveFile(_BaseSaveFile["SaveFileType"]):
"""
A Lethal Company save file.
Parameters
-----------
data: :class:`bytes`
The data read from the save file.
"""
# late init variable types
_extra_data: dict[str, Any]
_credits: int
_current_planet_id: int
_deadline: float
_deaths: int
_elapsed_days: int
_quotas_met: int
_quota_threshold: int
_seed: int
_steps_taken: int
__slots__ = (
"_inner_data",
"_extra_data",
"_credits",
"_current_planet_id",
"_current_quota_progress",
"_deadline",
"_deaths",
"_elapsed_days",
"_quotas_met",
"_quota_threshold",
"_seed",
"_steps_taken",
# these values need a richer interface
"_enemy_scans",
"_ship_item_save_data",
"_unlocked_ship_objects",
"_scrap",
"__ship_grabbable_items",
"__ship_grabbable_item_positions",
"__ship_scrap",
)
[docs] @classmethod
def resolve_from_file(cls, save_file_number: SaveValue, /) -> SaveFile:
"""
A class method to find, open and return a save file from the default save file location (Windows) with the given file number.
Parameters
-----------
save_file_number: Literal[``1``, ``2``, ``3``]
The numbered save to open.
"""
path = resolve_save_path(save_file_number)
return cls.from_path(path)
def _validate_contents(self, data: SaveFileType, /) -> None:
_required_keys = (
"GroupCredits",
"DeadlineTime",
"Stats_StepsTaken",
"Stats_DaysSpent",
"ProfitQuota",
"CurrentPlanetID",
)
if any(key not in data for key in _required_keys):
raise ValueError("This doesn't appear to be a valid Lethal Company save file!")
def _generate_seed(self, *, max: int = 99999999, min: int = 10000000) -> int:
return random.randint(min, max)
def _parse_file(self) -> None:
super()._parse_file()
self._credits = self._inner_data["GroupCredits"]["value"]
self._current_planet_id = self._inner_data["CurrentPlanetID"]["value"]
self._current_quota_progress = self._inner_data["QuotaFulfilled"]["value"]
self._deadline = self._inner_data["DeadlineTime"]["value"]
self._deaths = self._inner_data["Stats_Deaths"]["value"]
self._elapsed_days = self._inner_data["Stats_DaysSpent"]["value"]
self._quotas_met = self._inner_data["QuotasPassed"]["value"]
self._quota_threshold = self._inner_data["ProfitQuota"]["value"]
self._seed = self._inner_data["RandomSeed"]["value"]
self._steps_taken = self._inner_data["Stats_StepsTaken"]["value"]
# TODO: richer interface here.
self._enemy_scans = self._inner_data.get("EnemyScans", {"__type": "System.Int32[],mscorlib", "value": []})
self._ship_item_save_data = self._inner_data.get(
"shipItemSaveData", {"__type": "System.Int32[],mscorlib", "value": []}
)
self._unlocked_ship_objects = self._inner_data.get(
"UnlockedShipObjects", {"__type": "System.Int32[],mscorlib", "value": []}
)
self.__ship_grabbable_items = self._inner_data.get(
"shipGrabbableItemIDs", {"__type": "System.Int32[],mscorlib", "value": []}
)
self.__ship_grabbable_item_positions = self._inner_data.get(
"shipGrabbableItemPos", {"__type": "UnityEngine.Vector3[],UnityEngine.CoreModule", "value": []}
)
self.__ship_scrap = self._inner_data.get("shipScrapValues", {"__type": "System.Int32[],mscorlib", "value": []})
self._parse_scrap_mapping()
# this key is mostly laziness for now
# we'll serialise anything in here into the final payload
# for now this will just be how we add the UnlockedStored_X keys
self._extra_data = {}
def _parse_scrap_mapping(self) -> None:
# shipGrabbableItems contains all touchable items on the ship, including tools which have no value
# shipScrapValues are an array of values assigned to each piece of scrap
# it works because GrabbableItems[1]: ScrapValues[1], each index aligns and that's how the values are assigned, like a zip
# once the scrapvalues runs out of elements, the rest of the items are treated as no value, like tools
self._scrap: list[GrabbableScrap] = []
for item, value, pos in zip(
self.__ship_grabbable_items["value"], self.__ship_scrap["value"], self.__ship_grabbable_item_positions["value"]
):
self._scrap.append(GrabbableScrap(item, value, pos))
self.__ship_grabbable_items["value"] = self.__ship_grabbable_items["value"][len(self._scrap) :]
self.__ship_grabbable_item_positions["value"] = self.__ship_grabbable_item_positions["value"][len(self._scrap) :]
@property
def credits(self) -> int:
"""
Get the current credits value within the save.
Returns
--------
:class:`int`
"""
return self._credits
@property
def current_moon(self) -> Moon:
"""
Get the current planet within the save.
Returns
--------
:class:`~great_asset.Moon`
"""
return Moon(self._current_planet_id)
@property
def steps_taken(self) -> int:
"""
Get the current amount of steps taken within the save file.
Returns
--------
:class:`int`
"""
return self._steps_taken
@property
def deaths(self) -> int:
"""
Get the current amount of deaths within the save file.
Returns
--------
:class:`int`
"""
return self._deaths
@property
def elapsed_days(self) -> int:
"""
Get the current amount of elapsed days within the save file.
Returns
--------
:class:`int`
"""
return self._elapsed_days
@property
def deadline(self) -> int:
"""
Get the current deadline in time as days within the save file.
.. warning:
I round this value using :meth:`round` so it may not be fully accurate.
If you need the raw value, try :property:`raw_deadline`.
Returns
--------
:class:`int`
"""
return round(self._deadline / 1080)
@property
def raw_deadline(self) -> float:
"""
Get the current deadline in time's raw value within the save file.
Returns
--------
:class:`float`
"""
return self._deadline
@property
def profit_quota(self) -> int:
"""
Get the current profit quota (value to reach) within the save file.
Returns
--------
:class:`int`
"""
return self._quota_threshold
@property
def quotas_passed(self) -> int:
"""
Get the current total quotas met from within the save file.
Returns
--------
:class:`int`
"""
return self._quotas_met
@property
def current_quota_progress(self) -> int:
"""
Get the current quota progress from within the save file.
Returns
--------
:class:`int`
"""
return self._current_quota_progress
@property
def current_seed(self) -> int:
"""
Get the current seed from within the save file.
Returns
--------
:class:`int`
"""
return self._seed
[docs] def update_credits(self, new_credits: int, /) -> None:
"""Update the credits value within the save file.
Parameters
-----------
new_credits: :class:`int`
The new credits value.
"""
self._upsert_value("GroupCredits", new_credits)
self._credits = new_credits
[docs] def update_current_moon(self, moon: Moon, /) -> None:
"""
Update the current planet within the save file.
Parameters
-----------
planet: :class:`~great_asset.Moon`
The planet to update to.
"""
self._upsert_value("CurrentPlanetID", moon.value)
self._current_planet_id = moon.value
[docs] def update_steps_taken(self, new_steps: int, /) -> None:
"""
Update the current amount of steps taken within the save file.
Parameters
-----------
new_steps: :class:`int`
The amount of steps to have taken.
"""
self._upsert_value("Stats_StepsTaken", new_steps)
self._steps_taken = new_steps
[docs] def update_deaths(self, new_deaths: int, /) -> None:
"""
Update the current deaths within the save file.
Parameters
-----------
new_deaths: :class:`int`
The new value for total deaths.
"""
self._upsert_value("Stats_Deaths", new_deaths)
self._deaths = new_deaths
[docs] def update_elapsed_days(self, new_elapsed_days: int, /) -> None:
"""
Update the elapsed days count within the save file.
Parameters
-----------
new_elapsed_days: :class:`int`
The elapsed days value.
"""
self._upsert_value("Stats_DaysSpent", new_elapsed_days)
self._elapsed_days = new_elapsed_days
[docs] def update_deadline(self, new_deadline: int | float, /) -> None:
"""
Update the deadline time within the save file.
Parameters
-----------
new_deadline: :class:`int` | :class:`float`
New deadline/time remaining in days or minutes.
"""
if not isinstance(new_deadline, float):
new_deadline = new_deadline * 1080
self._upsert_value("DeadlineTime", new_deadline)
self._deadline = new_deadline
[docs] def update_profit_quota(self, new_profit_quota: int, /) -> None:
"""
Update the target quota value within the save file.
Parameters
-----------
new_profit_quota: :class:`int`
The profit quota to set.
"""
self._upsert_value("ProfitQuota", new_profit_quota)
self._quota_threshold = new_profit_quota
[docs] def update_quotas_met(self, new_quotas_met: int, /) -> None:
"""
Updates the quotas met within the save file.
Parameters
-----------
new_quotas_met: :class:`int`
The quotas met to set.
"""
self._upsert_value("QuotasPassed", new_quotas_met)
self._quotas_met = new_quotas_met
[docs] def update_current_quota_progress(self, new_quota_progress: int, /) -> None:
"""
Updates the current quota progress within the save file.
Parameters
-----------
new_quota_progress: :class:`int`
The quota progress to set.
"""
self._upsert_value("QuotaFulfilled", new_quota_progress)
self._current_quota_progress = new_quota_progress
[docs] def update_current_seed(self, new_seed: int | None = None, /) -> None:
"""
Updates the current seed within the save file.
Parameters
-----------
new_seed: :class:`int` | :class:`None`
The new seed to update to. If ``None`` is passed or no value is given a random one will be generated.
"""
seed = new_seed or self._generate_seed()
self._upsert_value("RandomSeed", seed)
self._seed = seed
[docs] def unlock_ship_upgrades(self, *items: ShipUnlock) -> None:
"""
Unlock upgrades for the ship.
Parameters
-----------
*items: :class:`~great_asset.ShipUnlock`
The items to unlock in the ship.
.. note::
The items unlocked will be added into storage, please access them from the terminal to place them.
"""
for item in items:
self._unlocked_ship_objects["value"].append(item.serialised_value)
self._extra_data[f"ShipUnlockStored_{item.serialised_name}"] = True
[docs] def unlock_all_ship_upgrades(self) -> None:
"""
Unlocks all possible ship upgrades.
"""
return self.unlock_ship_upgrades(*ShipUnlock.all())
[docs] def remove_ship_upgrades(self, *items: ShipUnlock) -> None:
"""
Remove upgrades from the ship.
Parameters
-----------
*items: :class:`~great_asset.ShipUnlock`
The items to remove from the ship.
"""
for item in items:
try:
self._unlocked_ship_objects["value"].remove(item.serialised_value)
except ValueError:
pass
self._extra_data[f"ShipUnlockStored_{item.serialised_name}"] = False
[docs] def unlock_bestiary_entries(self, *entries: BestiaryEntry) -> None:
"""
Unlock bestiary entries on the ship terminal.
Parameters
-----------
*entries: :class:`~great_asset.BestiaryEntry`
The entries to unlock.
"""
self._enemy_scans["value"] = list({e.value for e in entries})
[docs] def unlock_all_bestiary_entries(self) -> None:
"""
Unlock all possible bestiary entries.
"""
return self.unlock_bestiary_entries(*BestiaryEntry.all())
[docs] def spawn_items(self, *items: tuple[Item | Scrap, Vector | None], value_min: int = 30, value_max: int = 90) -> None:
"""
Spawn items within the world or ship.
Parameters
-----------
*items: tuple[:class:`~great_asset.Item` | :class:`~great_asset.Scrap`, :class:`~great_asset.Vector` | :class:`None`]
A series of tuples with the item and it's position to spawn in.
Using ``None`` as the second value will spawn at a default area near the door of the ship internally.
"""
cupboard_position = self._inner_data.get("ShipUnlockPos_Cupboard")
for item, position in items:
vec = position or Vector.in_cupboard(cupboard_position=cupboard_position)
if isinstance(item, Scrap):
value = random.randint(value_min, value_max)
self._scrap.append(GrabbableScrap(item.value, value, vec.serialise()))
continue
self.__ship_grabbable_items["value"].append(item.value)
self.__ship_grabbable_item_positions["value"].append(vec.serialise())
[docs] def get_current_items(self) -> list[Item]:
"""
Get the current items that exist within the ship in the save file.
Returns
--------
list[:class:`~great_asset.Item`]
"""
return [Item(item) for item in self.__ship_grabbable_items["value"]]
[docs] def get_current_scrap(self) -> list[Scrap]:
"""
Get the current scrap that exist within the ship in the save file.
Returns
--------
list[:class:`~great_asset.Scrap`]
"""
return [Scrap(item.id) for item in self._scrap]
[docs] def write(self, *, path: Path | None = None) -> None:
"""
A function to write the save file data to the internal data structure.
Parameters
-----------
dump_to_file: :class:`bool`
Whether to overwrite the passed file with the changes. Defaults to ``True``.
overwrite: :class:`bool`
Whether to overwrite the existing save file with the changes. Defaults to ``True``.
Irrelevant if ``dump_to_file`` is ``False``.
Creates a file with the same name with the file extension of ``".over"``.
"""
# manually handle the more complex types:
self._upsert_value("UnlockedShipObjects", list(set(self._unlocked_ship_objects["value"])))
self._inner_data["EnemyScans"] = self._enemy_scans
self._inner_data["shipItemSaveData"] = self._ship_item_save_data
for item in self._scrap:
self.__ship_scrap["value"].insert(0, item.value)
self.__ship_grabbable_items["value"].insert(0, item.id)
self.__ship_grabbable_item_positions["value"].insert(0, item.pos)
self._inner_data["shipScrapValues"] = self.__ship_scrap
self._inner_data["shipGrabbableItemIDs"] = self.__ship_grabbable_items
self._inner_data["shipGrabbableItemPos"] = self.__ship_grabbable_item_positions
super().write(path=path)
[docs]class ConfigFile(_BaseSaveFile["ConfigFileType"]):
_extra_data: dict[str, Any]
# late init types
arachnophobia_mode: bool
y_axis_inverted: bool
screen_mode: int
fps_mode: int
current_mic: str
push_to_talk: bool
mic_enabled: bool
mouse_sensitivity: int
master_volume: float
gamma: float
finished_setup: bool
start_online: bool
_player_xp: IntValue
_player_level: IntValue
_times_landed: IntValue
_is_host_public: BoolValue
_host_name: StringValue
_played_extrance_0: BoolValue
_played_entrance_1: BoolValue
_tips: dict[str, BoolValue]
def _validate_contents(self, data: ConfigFileType, /) -> None:
_required_keys = ("SelectedFile", "FPSCap", "Gamma", "LookSens", "ScreenMode")
if any(key not in data for key in _required_keys):
raise ValueError("This doesn't appear to be a valid Lethal Company config file!")
def _parse_file(self) -> None:
super()._parse_file()
self.arachnophobia_mode = self._inner_data["SpiderSafeMode"]["value"]
self.y_axis_inverted = self._inner_data["InvertYAxis"]["value"]
self.screen_mode = self._inner_data["ScreenMode"]["value"]
self.fps_mode = self._inner_data["FPSCap"]["value"]
self.current_mic = self._inner_data["CurrentMic"]["value"]
self.push_to_talk = self._inner_data["PushToTalk"]["value"]
self.mic_enabled = self._inner_data["MicEnabled"]["value"]
self.mouse_sensitivity = self._inner_data["LookSens"]["value"]
self.master_volume = self._inner_data["MasterVolume"]["value"]
self.gamma = self._inner_data["Gamma"]["value"]
self.finished_setup = self._inner_data["PlayerFinishedSetup"]["value"]
self.start_online = self._inner_data["StartInOnlineMode"]["value"]
self._player_xp = self._inner_data.get("PlayerXPNum", MISSING)
self._player_level = self._inner_data.get("PlayerLevel", MISSING)
self._times_landed = self._inner_data.get("TimesLanded", MISSING)
self._is_host_public = self._inner_data.get("HostSettings_Public", MISSING)
self._host_name = self._inner_data.get("HostSettings_Name", MISSING)
self._played_entrance_0 = self._inner_data.get("PlayedDungeonEntrance0", MISSING)
self._played_entrance_1 = self._inner_data.get("PlayedDungeonEntrance1", MISSING)
self._tips = {tip: self._inner_data.get(tip, MISSING) for tip in TIPS}
self._extra_data = {}
@property
def player_xp(self) -> int:
if self._player_xp is not MISSING:
return self._player_xp["value"]
return 0
@property
def player_level(self) -> int:
if self._player_xp is not MISSING:
return self._player_level["value"]
return 0
@property
def times_landed(self) -> int:
if self._times_landed is not MISSING:
return self._times_landed["value"]
return 0
@property
def is_host_public(self) -> bool:
if self._is_host_public is not MISSING:
return self._is_host_public["value"]
return False
@property
def host_name(self) -> str:
if self._host_name is not MISSING:
return self._host_name["value"]
return "No name set"
@property
def played_entrances(self) -> list[int]:
return [val["value"] for val in [self._played_entrance_0, self._played_entrance_1] if val is not MISSING]
@property
def tips(self) -> dict[str, bool]:
return {tip: value["value"] for tip, value in self._tips.items() if value is not MISSING}
def set_player_xp(self, value: int) -> None:
if 0 <= value <= 999:
self._player_xp = {"__type": "int", "value": value}
def set_player_level(self, value: int) -> None:
if value > 0:
self._player_level = {"__type": "int", "value": (value - 1) % 5}
def set_host_public(self, value: bool) -> None:
self._is_host_public = {"__type": "bool", "value": value}
def set_host_name(self, value: str) -> None:
self._host_name = {"__type": "string", "value": value[40:]}
def set_arachnophobia_mode(self, value: bool) -> None:
self.arachnophobia_mode = value
def set_inverted_y_axis(self, value: bool) -> None:
self.y_axis_inverted = value
def set_screen_mode(self, value: int) -> None:
self.screen_mode = value
def set_fps_mode(self, value: int) -> None:
self.fps_mode = value
def set_push_to_talk(self, value: bool) -> None:
self.push_to_talk = value
def set_mic_enabled(self, value: bool) -> None:
self.mic_enabled = value
def set_mouse_sensitivity(self, value: int) -> None:
self.mouse_sensitivity = value
def set_master_volume(self, value: float) -> None:
if 0 <= value <= 1:
self.master_volume = value
def set_gamma(self, value: float) -> None:
if 0 <= value <= 1:
self.gamma = value
def write(self, *, path: Path | None = None) -> None:
self._upsert_value("SpiderSafeMode", self.arachnophobia_mode)
self._upsert_value("InvertYAxis", self.y_axis_inverted)
self._upsert_value("ScreenMode", self.screen_mode)
self._upsert_value("FPSCap", self.fps_mode)
self._upsert_value("CurrentMic", self.current_mic)
self._upsert_value("PushToTalk", self.push_to_talk)
self._upsert_value("MicEnabled", self.mic_enabled)
self._upsert_value("LookSens", self.mouse_sensitivity)
self._upsert_value("MasterVolume", self.master_volume)
self._upsert_value("Gamma", self.gamma)
self._upsert_value("PlayerFinishedSetup", self.finished_setup)
self._upsert_value("StartInOnlineMode", self.start_online)
self._upsert_value("PlayerXPNum", self._player_xp)
self._upsert_value("PlayerLevel", self._player_level)
self._upsert_value("TimesLanded", self._times_landed)
self._upsert_value("HostSettings_Public", self._is_host_public)
self._upsert_value("HostSettings_Name", self._host_name)
self._upsert_value("PlayedDungeonEntrance0", self._played_entrance_0)
self._upsert_value("PlayedDungeonEntrance1", self._played_entrance_1)
for idx, tip in enumerate(self._tips):
self._upsert_value(TIPS[idx], tip)
super().write(path=path)
[docs]class ChallengeFile(_BaseSaveFile["ChallengeFileType"]):
_profit_earned: int
_finished_challenge: bool
_submitted_score: bool
_file_game_version: int
_challenge_week_number: int
_set_challenge_file_money: bool
__slots__ = (
"_profit_earned",
"_finished_challenge",
"_submitted_score",
"_file_game_version",
"_challenge_week_number",
"_set_challenge_file_money",
)
def _validate_contents(self, data: ChallengeFileType) -> None:
_required_keys = ("ProfitEarned", "FinishedChallenge", "SubmittedScore")
if any(key not in data for key in _required_keys):
raise ValueError("This doesn't appear to be a valid Challenge file.")
def _parse_file(self) -> None:
super()._parse_file()
self._profit_earned = self._inner_data["ProfitEarned"]["value"]
self._finished_challenge = self._inner_data["FinishedChallenge"]["value"]
self._submitted_score = self._inner_data["SubmittedScore"]["value"]
self._file_game_version = self._inner_data.get("FileGameVers", {"__type": "int", "value": 48})["value"]
self._challenge_week_number = self._inner_data.get("FileGameVers", {"__type": "int", "value": 4})["value"]
self._set_challenge_file_money = self._inner_data.get("SetChallengeFileMoney", {"__type": "bool", "value": False})[
"value"
]
self._extra_data = {}
@property
def profit_earned(self) -> int:
return self._profit_earned
@property
def finished_challenge(self) -> bool:
return self._finished_challenge
@property
def submitted_score(self) -> int:
return self._submitted_score
@property
def file_game_version(self) -> int:
return self._file_game_version
@property
def challenge_week_number(self) -> int:
return self._challenge_week_number
@property
def set_challenge_file_money(self) -> bool:
return self._set_challenge_file_money