├── models ├── __init__.py ├── raw_save_file.py ├── save_file.py └── lua_state.py ├── schemas ├── __init__.py ├── version_id.py ├── ctrls.py ├── sav_16.py ├── sav_14.py └── sav_15.py ├── requirements.txt ├── constant.py ├── bin_utils.py ├── util.py ├── README.md ├── LICENSE ├── gamedata.py ├── .gitignore ├── main.py └── pluto.ui /models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /schemas/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click 2 | construct 3 | luabins_py==1.4.0 4 | PyQt5 5 | pyinstaller 6 | lz4 7 | pywin32 -------------------------------------------------------------------------------- /constant.py: -------------------------------------------------------------------------------- 1 | FILE_SIGNATURE = b"\x53\x47\x42\x31" 2 | SAVE_DATA_V14_LENGTH = 3145720 3 | SAVE_DATA_V15_LENGTH = 3145720 4 | SAVE_DATA_V16_LENGTH = 3145728 5 | SAV15_UNCOMPRESSED_SIZE = 9388032 6 | SAV16_UNCOMPRESSED_SIZE = 9388032 7 | -------------------------------------------------------------------------------- /bin_utils.py: -------------------------------------------------------------------------------- 1 | def rpad_bytes(byte_data: bytes, target_length: int) -> bytes: 2 | byte_length = len(byte_data) 3 | 4 | if byte_length > target_length: 5 | return byte_data 6 | 7 | return byte_data + b'\0' * (target_length - byte_length) 8 | -------------------------------------------------------------------------------- /schemas/version_id.py: -------------------------------------------------------------------------------- 1 | from construct import * 2 | 3 | from constant import FILE_SIGNATURE 4 | 5 | version_identifier_schema = Struct( 6 | "signature" / Const(FILE_SIGNATURE), 7 | "checksum" / Padding(4), 8 | "version" / Int32ul, 9 | GreedyBytes, 10 | ) 11 | -------------------------------------------------------------------------------- /util.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import data 4 | 5 | 6 | def load_data_file_as_binary(filename): 7 | with open(get_path_to_data_file(filename), mode='rb') as file_: 8 | return file_.read() 9 | 10 | 11 | def get_path_to_data_file(filename): 12 | return os.path.join(os.path.basename(data.__path__[0]), filename) 13 | -------------------------------------------------------------------------------- /schemas/ctrls.py: -------------------------------------------------------------------------------- 1 | from construct import * 2 | 3 | ctrls_schema = Struct( 4 | "signature" / Const(b"\x53\x47\x42\x31"), 5 | "body" / Padded( 6 | 2048, 7 | Struct( 8 | "total_key_count" / Int32ul, 9 | "key_mappings" / Array( 10 | this.total_key_count, 11 | Struct( 12 | "key_bound" / Int32ul, 13 | "name" / PascalString(Int32ul, "utf8"), 14 | "key_count" / Int32ul, 15 | "keyboard_keys" / Array( 16 | this.key_count, 17 | Int32ul 18 | ), 19 | "gamepad_keys" / Int32ul, 20 | "mouse_keys" / Int32ul, 21 | "gamepad_enabled" / Byte, 22 | "use_shift" / Byte 23 | 24 | ) 25 | ) 26 | ) 27 | ) 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hades Save Editor 2 | A project to enable easy editing of Hades savegames. 3 | 4 | Given the engine similarities, I suspect it may work for bastion and transistor too, with some minor tweaks to the save schema. 5 | 6 | WIP. Basic UI, with editable currencies. Will probably break your game and ruin your life. 7 | 8 | 9 | ### Getting started 10 | 11 | Ensure you have Python 3+ installed. Clone this repo or download the source as a [zip](https://github.com/zsennenga/hades_save_editor/archive/master.zip) and extract it. In the extracted folder, 12 | 13 | 1. (Optional) Setup a [virtual environment](https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/#installing-pip) 14 | 15 | 2. Install the dependencies via pip 16 | 17 | pip3 install -r requirements.txt 18 | 19 | 3. Start the editor 20 | 21 | python3 main.py 22 | 4. Once the editor is running, load up your save file 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Zachary Ennenga 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /schemas/sav_16.py: -------------------------------------------------------------------------------- 1 | import zlib 2 | 3 | from construct import * 4 | 5 | from constant import FILE_SIGNATURE 6 | 7 | sav16_save_data_schema = Struct( 8 | "version" / Int32ul, 9 | "timestamp" / Int64ul, 10 | "location" / PascalString(Int32ul, "utf8"), 11 | "runs" / Int32ul, 12 | "active_meta_points" / Int32ul, 13 | "active_shrine_points" / Int32ul, 14 | "god_mode_enabled" / Byte, 15 | "hell_mode_enabled" / Byte, 16 | "lua_keys" / PrefixedArray( 17 | Int32ul, 18 | PascalString(Int32ul, "utf8") 19 | ), 20 | "current_map_name" / PascalString(Int32ul, "utf8"), 21 | "start_next_map" / PascalString(Int32ul, "utf8"), 22 | "lua_state" / PrefixedArray(Int32ul, Byte) 23 | ) 24 | 25 | sav16_schema = Struct( 26 | "signature" / Const(FILE_SIGNATURE), 27 | "checksum_offset" / Tell, 28 | "checksum" / Padding(4), 29 | "save_data" / RawCopy( 30 | sav16_save_data_schema 31 | ), 32 | "checksum" / Pointer( 33 | this.checksum_offset, 34 | Checksum( 35 | Int32ul, 36 | lambda data: zlib.adler32(data, 1), 37 | this.save_data.data 38 | ) 39 | ) 40 | 41 | ) 42 | -------------------------------------------------------------------------------- /schemas/sav_14.py: -------------------------------------------------------------------------------- 1 | import zlib 2 | 3 | from construct import * 4 | 5 | from constant import FILE_SIGNATURE, SAVE_DATA_V14_LENGTH 6 | 7 | sav14_save_data_schema = Struct( 8 | "version" / Int32ul, 9 | "location" / PascalString(Int32ul, "utf8"), 10 | "runs" / Int32ul, 11 | "active_meta_points" / Int32ul, 12 | "active_shrine_points" / Int32ul, 13 | "god_mode_enabled" / Byte, 14 | "hell_mode_enabled" / Byte, 15 | "lua_keys" / PrefixedArray( 16 | Int32ul, 17 | PascalString(Int32ul, "utf8") 18 | ), 19 | "current_map_name" / PascalString(Int32ul, "utf8"), 20 | "start_next_map" / PascalString(Int32ul, "utf8"), 21 | "lua_state" / PrefixedArray(Int32ul, Byte) 22 | ) 23 | 24 | sav14_schema = Struct( 25 | "signature" / Const(FILE_SIGNATURE), 26 | "checksum_offset" / Tell, 27 | "checksum" / Padding(4), 28 | "save_data" / RawCopy( 29 | Padded( 30 | SAVE_DATA_V14_LENGTH, 31 | sav14_save_data_schema 32 | ) 33 | ), 34 | "checksum" / Pointer( 35 | this.checksum_offset, 36 | Checksum( 37 | Int32ul, 38 | lambda data: zlib.adler32(data, 1), 39 | this.save_data.data 40 | ) 41 | ) 42 | 43 | ) 44 | -------------------------------------------------------------------------------- /schemas/sav_15.py: -------------------------------------------------------------------------------- 1 | import zlib 2 | 3 | from construct import * 4 | 5 | from constant import FILE_SIGNATURE, SAVE_DATA_V15_LENGTH 6 | 7 | sav15_save_data_schema = Struct( 8 | "version" / Int32ul, 9 | "location" / PascalString(Int32ul, "utf8"), 10 | "runs" / Int32ul, 11 | "active_meta_points" / Int32ul, 12 | "active_shrine_points" / Int32ul, 13 | "god_mode_enabled" / Byte, 14 | "hell_mode_enabled" / Byte, 15 | "lua_keys" / PrefixedArray( 16 | Int32ul, 17 | PascalString(Int32ul, "utf8") 18 | ), 19 | "current_map_name" / PascalString(Int32ul, "utf8"), 20 | "start_next_map" / PascalString(Int32ul, "utf8"), 21 | "lua_state" / PrefixedArray(Int32ul, Byte) 22 | ) 23 | 24 | sav15_schema = Struct( 25 | "signature" / Const(FILE_SIGNATURE), 26 | "checksum_offset" / Tell, 27 | "checksum" / Padding(4), 28 | "save_data" / RawCopy( 29 | Padded( 30 | SAVE_DATA_V15_LENGTH, 31 | sav15_save_data_schema 32 | ) 33 | ), 34 | "checksum" / Pointer( 35 | this.checksum_offset, 36 | Checksum( 37 | Int32ul, 38 | lambda data: zlib.adler32(data, 1), 39 | this.save_data.data 40 | ) 41 | ) 42 | 43 | ) 44 | -------------------------------------------------------------------------------- /gamedata.py: -------------------------------------------------------------------------------- 1 | ''' Contains Hades gamedata ''' 2 | 3 | # Based on data in Weaponsets.lua 4 | HeroMeleeWeapons = { 5 | "SwordWeapon": "Stygian Blade", 6 | "SpearWeapon": "Eternal Spear", 7 | "ShieldWeapon": "Shield of Chaos", 8 | "BowWeapon": "Heart-Seeking Bow", 9 | "FistWeapon": "Twin Fists of Malphon", 10 | "GunWeapon": "Adamant Rail", 11 | } 12 | 13 | 14 | # Based on data from TraitData.lua 15 | AspectTraits = { 16 | "SwordCriticalParryTrait": "Nemesis", 17 | "SwordConsecrationTrait": "Arthur", 18 | "ShieldRushBonusProjectileTrait": "Chaos", 19 | "ShieldLoadAmmoTrait": "Beowulf", 20 | # "ShieldBounceEmpowerTrait": "", 21 | "ShieldTwoShieldTrait": "Zeus", 22 | "SpearSpinTravel": "Guan Yu", 23 | "GunGrenadeSelfEmpowerTrait": "Eris", 24 | "FistVacuumTrait": "Talos", 25 | "FistBaseUpgradeTrait": "Zagreus", 26 | "FistWeaveTrait": "Demeter", 27 | "FistDetonateTrait": "Gilgamesh", 28 | "SwordBaseUpgradeTrait": "Zagreus", 29 | "BowBaseUpgradeTrait": "Zagreus", 30 | "SpearBaseUpgradeTrait": "Zagreus", 31 | "ShieldBaseUpgradeTrait": "Zagreus", 32 | "GunBaseUpgradeTrait": "Zagreus", 33 | "DislodgeAmmoTrait": "Poseidon", 34 | # "SwordAmmoWaveTrait": "", 35 | "GunManualReloadTrait": "Hestia", 36 | "GunLoadedGrenadeTrait": "Lucifer", 37 | "BowMarkHomingTrait": "Chiron", 38 | "BowLoadAmmoTrait": "Hera", 39 | # "BowStoredChargeTrait": "", 40 | "BowBondTrait": "Rama", 41 | # "BowBeamTrait": "", 42 | "SpearWeaveTrait": "Hades", 43 | "SpearTeleportTrait": "Achilles", 44 | } 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .idea/ 132 | data/ 133 | debug.py -------------------------------------------------------------------------------- /models/raw_save_file.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any 2 | 3 | from bin_utils import rpad_bytes 4 | from constant import SAVE_DATA_V14_LENGTH, SAVE_DATA_V15_LENGTH 5 | from schemas.sav_14 import sav14_schema, sav14_save_data_schema 6 | from schemas.sav_15 import sav15_schema, sav15_save_data_schema 7 | from schemas.sav_16 import sav16_schema, sav16_save_data_schema 8 | from schemas.version_id import version_identifier_schema 9 | 10 | 11 | class RawSaveFile: 12 | def __init__( 13 | self, 14 | version: int, 15 | save_data: Dict[Any, Any], 16 | ): 17 | self.version = version 18 | self.save_data = save_data 19 | self.lua_state_bytes = save_data['lua_state'] 20 | 21 | @classmethod 22 | def from_file(cls, path: str) -> 'RawSaveFile': 23 | with open(path, 'rb') as f: 24 | input_bytes = f.read() 25 | version = version_identifier_schema.parse(input_bytes).version 26 | 27 | if version == 14: 28 | parsed_schema = sav14_schema.parse(input_bytes) 29 | elif version == 15: 30 | parsed_schema = sav15_schema.parse(input_bytes) 31 | elif version == 16: 32 | parsed_schema = sav16_schema.parse(input_bytes) 33 | else: 34 | raise Exception(f"Unsupported version {version}") 35 | 36 | return RawSaveFile( 37 | version, 38 | dict(parsed_schema.save_data.value) 39 | ) 40 | 41 | def to_file(self, path: str) -> None: 42 | if self.version == 14: 43 | sav14_schema.build_file( 44 | { 45 | 'save_data': { 46 | 'data': rpad_bytes( 47 | sav14_save_data_schema.build( 48 | self.save_data 49 | ), 50 | SAVE_DATA_V14_LENGTH 51 | ) 52 | } 53 | }, 54 | filename=path, 55 | ) 56 | elif self.version == 15: 57 | sav15_schema.build_file( 58 | { 59 | 'save_data': { 60 | 'data': rpad_bytes( 61 | sav15_save_data_schema.build( 62 | self.save_data 63 | ), 64 | SAVE_DATA_V15_LENGTH 65 | ) 66 | } 67 | }, 68 | filename=path, 69 | ) 70 | elif self.version == 16: 71 | sav15_schema.build_file( 72 | { 73 | 'save_data': { 74 | 'data': sav16_save_data_schema.build( 75 | self.save_data 76 | ) 77 | } 78 | }, 79 | filename=path, 80 | ) 81 | else: 82 | raise Exception(f"Unsupported version {self.version}") 83 | -------------------------------------------------------------------------------- /models/save_file.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List, Optional 3 | 4 | from models.lua_state import LuaState 5 | from models.raw_save_file import RawSaveFile 6 | 7 | 8 | class HadesSaveFile: 9 | def __init__( 10 | self, 11 | version: int, 12 | location: str, 13 | runs: int, 14 | active_meta_points: int, 15 | active_shrine_points: int, 16 | god_mode_enabled: bool, 17 | hell_mode_enabled: bool, 18 | lua_keys: List[str], 19 | current_map_name: str, 20 | start_next_map: str, 21 | lua_state: LuaState, 22 | raw_save_file: Optional[RawSaveFile] = None 23 | ): 24 | self.version = version 25 | self.location = location 26 | self.runs = runs 27 | self.active_meta_points = active_meta_points 28 | self.active_shrine_points = active_shrine_points 29 | self.god_mode_enabled = god_mode_enabled 30 | self.hell_mode_enabled = hell_mode_enabled 31 | self.lua_keys = lua_keys 32 | self.current_map_name = current_map_name 33 | self.start_next_map = start_next_map 34 | self.lua_state = lua_state 35 | self.timestamp = int(datetime.utcnow().timestamp()) 36 | 37 | # Unusued, for debugging 38 | self.raw_save_file = raw_save_file 39 | 40 | @classmethod 41 | def from_file(cls, path): 42 | raw_save_file = RawSaveFile.from_file(path) 43 | lua_state = LuaState.from_bytes( 44 | version=raw_save_file.version, 45 | input_bytes=bytes(raw_save_file.lua_state_bytes) 46 | ) 47 | 48 | # Unused, for debugging 49 | lua_state.raw_save_file = raw_save_file 50 | 51 | return HadesSaveFile( 52 | version=raw_save_file.version, 53 | location=raw_save_file.save_data['location'], 54 | runs=raw_save_file.save_data['runs'], 55 | active_meta_points=raw_save_file.save_data['active_meta_points'], 56 | active_shrine_points=raw_save_file.save_data['active_shrine_points'], 57 | god_mode_enabled=raw_save_file.save_data['god_mode_enabled'], 58 | hell_mode_enabled=raw_save_file.save_data['hell_mode_enabled'], 59 | lua_keys=raw_save_file.save_data['lua_keys'], 60 | current_map_name=raw_save_file.save_data['current_map_name'], 61 | start_next_map=raw_save_file.save_data['start_next_map'], 62 | lua_state=lua_state, 63 | raw_save_file=raw_save_file 64 | ) 65 | 66 | def to_file(self, path): 67 | if self.version == 14: 68 | RawSaveFile( 69 | version=14, 70 | save_data={ 71 | 'version': self.version, 72 | 'location': self.location, 73 | 'runs': self.runs, 74 | 'active_meta_points': self.active_meta_points, 75 | 'active_shrine_points': self.active_shrine_points, 76 | 'god_mode_enabled': self.god_mode_enabled, 77 | 'hell_mode_enabled': self.hell_mode_enabled, 78 | 'lua_keys': self.lua_keys, 79 | 'current_map_name': self.current_map_name, 80 | 'start_next_map': self.start_next_map, 81 | 'lua_state': self.lua_state.to_bytes(), 82 | } 83 | ).to_file(path) 84 | elif self.version == 15: 85 | RawSaveFile( 86 | version=15, 87 | save_data={ 88 | 'version': self.version, 89 | 'location': self.location, 90 | 'runs': self.runs, 91 | 'active_meta_points': self.active_meta_points, 92 | 'active_shrine_points': self.active_shrine_points, 93 | 'god_mode_enabled': self.god_mode_enabled, 94 | 'hell_mode_enabled': self.hell_mode_enabled, 95 | 'lua_keys': self.lua_keys, 96 | 'current_map_name': self.current_map_name, 97 | 'start_next_map': self.start_next_map, 98 | 'lua_state': self.lua_state.to_bytes(), 99 | } 100 | ).to_file(path) 101 | elif self.version == 16: 102 | RawSaveFile( 103 | version=16, 104 | save_data={ 105 | 'version': self.version, 106 | 'timestamp': self.timestamp, 107 | 'location': self.location, 108 | 'runs': self.runs, 109 | 'active_meta_points': self.active_meta_points, 110 | 'active_shrine_points': self.active_shrine_points, 111 | 'god_mode_enabled': self.god_mode_enabled, 112 | 'hell_mode_enabled': self.hell_mode_enabled, 113 | 'lua_keys': self.lua_keys, 114 | 'current_map_name': self.current_map_name, 115 | 'start_next_map': self.start_next_map, 116 | 'lua_state': self.lua_state.to_bytes(), 117 | } 118 | ).to_file(path) 119 | else: 120 | raise Exception(f"Unsupported version {self.version}") 121 | -------------------------------------------------------------------------------- /models/lua_state.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from io import BytesIO 3 | from typing import Dict, Any, List 4 | 5 | from luabins import decode_luabins, encode_luabins 6 | import lz4.block 7 | 8 | from constant import SAV15_UNCOMPRESSED_SIZE, SAV16_UNCOMPRESSED_SIZE 9 | 10 | 11 | class _LuaStateProperty: 12 | def __init__(self, key: str, default: Any): 13 | self.key = key 14 | 15 | self.default = default 16 | 17 | def __get__(self, obj: 'LuaState', objtype): 18 | return obj._get_nested_key(self.key, self.default) 19 | 20 | def __set__(self, obj: 'LuaState', value: Any): 21 | return obj._set_nested_key(self.key, value) 22 | 23 | 24 | class LuaState: 25 | def __init__( 26 | self, 27 | version: int, 28 | raw_lua_state: List[Dict[Any, Any]] 29 | ): 30 | self.version = version 31 | 32 | self._active_state: Dict[Any, Any] = raw_lua_state[0] 33 | 34 | # For debugging purposes only 35 | self._raw_save_file = None 36 | self._raw_lua_state_dicts = raw_lua_state 37 | 38 | #with open("debug.txt", "w") as f: 39 | # # f.write(json.dumps(raw_lua_state, indent=2)) 40 | 41 | @classmethod 42 | def from_bytes(cls, version: int, input_bytes: bytes) -> 'LuaState': 43 | decompressed_bytes: bytes = input_bytes 44 | if version == 15: 45 | decompressed_bytes: bytes = lz4.block.decompress(input_bytes, uncompressed_size=SAV15_UNCOMPRESSED_SIZE) 46 | elif version == 16: 47 | decompressed_bytes: bytes = lz4.block.decompress(input_bytes, uncompressed_size=SAV16_UNCOMPRESSED_SIZE) 48 | 49 | return LuaState.from_dict( 50 | version, 51 | decode_luabins(BytesIO(decompressed_bytes)) 52 | ) 53 | 54 | @classmethod 55 | def from_dict(cls, version: int, input_dicts: List[Dict[Any, Any]]) -> 'LuaState': 56 | return LuaState( 57 | version, 58 | input_dicts 59 | ) 60 | 61 | darkness = _LuaStateProperty("GameState.Resources.MetaPoints", 0.0) 62 | gems = _LuaStateProperty("GameState.Resources.Gems", 0.0) 63 | diamonds = _LuaStateProperty("GameState.Resources.SuperGems", 0.0) 64 | nectar = _LuaStateProperty("GameState.Resources.GiftPoints", 0.0) 65 | ambrosia = _LuaStateProperty("GameState.Resources.SuperGiftPoints", 0.0) 66 | chthonic_key = _LuaStateProperty("GameState.Resources.LockKeys", 0.0) 67 | titan_blood = _LuaStateProperty("GameState.Resources.SuperLockKeys", 0.0) 68 | hell_mode = _LuaStateProperty("GameState.Flags.HardMode", False) 69 | easy_mode_level = _LuaStateProperty("GameState.EasyModeLevel", 0.0) 70 | 71 | gift_record = _LuaStateProperty("CurrentRun.GiftRecord", {}) 72 | npc_interactions = _LuaStateProperty("CurrentRun.NPCInteractions", {}) 73 | trigger_record = _LuaStateProperty("CurrentRun.TriggerRecord", {}) 74 | activation_record = _LuaStateProperty("CurrentRun.ActivationRecord", {}) 75 | use_record = _LuaStateProperty("CurrentRun.UseRecord", {}) 76 | text_lines = _LuaStateProperty("CurrentRun.TextLinesRecord", {}) 77 | 78 | def _parse_nested_path_reference( 79 | self, 80 | path: str 81 | ) -> (Dict[Any, Any], str): 82 | (path_components, key) = self._split_path_into_key_and_components(path) 83 | state = self._active_state 84 | 85 | for component in path_components: 86 | if component not in state: 87 | return None, None 88 | state = state.get(component) 89 | 90 | return state, key 91 | 92 | def _split_path_into_key_and_components(self, path: str): 93 | components = path.split(".") 94 | path_components = components[:-1] 95 | key = components[-1] 96 | 97 | return path_components, key 98 | 99 | def _get_nested_key(self, path: str, default: Any) -> Any: 100 | """ 101 | Sets a (potentially nested) key in a dict, such as the Game State. 102 | 103 | :param path: Target key. Nested keys can be denotated by "." 104 | For example, using the gamestate as our obj, "Resources.Gems" will map to gamestate['Resources']['Gems'] 105 | The final key does not need to exist, but any intervening dicts must. 106 | In our example, 'Gems' may not exist, but 'Resources' must. 107 | :param default: Default value if it isn't found 108 | :return: None 109 | """ 110 | (reference, key) = self._parse_nested_path_reference(path) 111 | 112 | if reference is None or key not in reference: 113 | return default 114 | 115 | return reference[key] 116 | 117 | def _set_nested_key(self, path: str, value: Any) -> None: 118 | """ 119 | Sets a (potentially nested) key in a dict, such as the Game State. 120 | 121 | :param path: Target key. Nested keys can be denotated by "." 122 | For example, using the gamestate as our obj, "Resources.Gems" will map to gamestate['Resources']['Gems'] 123 | The final key does not need to exist, but any intervening dicts must. 124 | In our example, 'Gems' may not exist, but 'Resources' must. 125 | :param value: value to set 126 | :return: None 127 | """ 128 | (reference, key) = self._parse_nested_path_reference(path) 129 | reference[key] = value 130 | 131 | def to_bytes(self) -> bytes: 132 | if self.version <= 14: 133 | return encode_luabins(self.to_dicts()) 134 | else: 135 | return lz4.block.compress(encode_luabins(self.to_dicts()), store_size=False) 136 | 137 | 138 | def to_dicts(self) -> List[Dict[Any, Any]]: 139 | return [ 140 | copy.deepcopy(self._active_state) 141 | ] 142 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from pathlib import Path 4 | 5 | from PyQt5 import QtWidgets, uic 6 | from PyQt5.QtCore import QStandardPaths 7 | from PyQt5.QtWidgets import QFileDialog, QPushButton, QLineEdit, QMessageBox, QDialog, QLabel, QCheckBox, QWidget 8 | 9 | from models.save_file import HadesSaveFile 10 | import gamedata 11 | 12 | mainWin = None 13 | 14 | def resource_path(relative_path): 15 | """ Get absolute path to resource, works for dev and for PyInstaller """ 16 | try: 17 | # PyInstaller creates a temp folder and stores path in _MEIPASS 18 | base_path = sys._MEIPASS 19 | except Exception: 20 | base_path = os.path.abspath(".") 21 | 22 | return os.path.join(base_path, relative_path) 23 | 24 | 25 | def except_hook(cls, exception, traceback): 26 | 27 | mainWin.error_widget.setVisible(1) 28 | mainWin.error_label.setText("{}: {}".format(exception.__class__.__name__, str(exception))) 29 | sys.__excepthook__(cls, exception, traceback) 30 | 31 | 32 | def _easy_mode_level_from_damage_reduction(damage_reduction: int) -> int: 33 | # Make sure the damage reduction is between 20-80 as that is what the game uses, out of range might not be safe 34 | damage_reduction = max(20, min(80, damage_reduction)) 35 | easy_mode_level = (damage_reduction - 20) / 2 36 | return easy_mode_level 37 | 38 | 39 | def _damage_reduction_from_easy_mode_level(easy_mode_level: int) -> int: 40 | # Easy mode level is half the amount of damage reduction added to the base damage reduction from enabling god mode 41 | # easy mode level 10 => 20 + (10 * 2) = 40% damage reduction 42 | damage_reduction = (easy_mode_level * 2) + 20 43 | return damage_reduction 44 | 45 | 46 | class App(QDialog): 47 | def __init__(self, application): 48 | super().__init__() 49 | self.app = application 50 | 51 | self.file_path = None 52 | self.save_file: HadesSaveFile = None 53 | self.dirty = False 54 | 55 | uic.loadUi(resource_path('pluto.ui'), self) # Load the .ui file 56 | self.error_widget = self.findChild(QWidget, "errorWidget") 57 | self.error_label = self.findChild(QLabel, "errorValue") 58 | 59 | self.ui_state = self.findChild(QLabel, "state") 60 | 61 | self.error_widget.setVisible(0) 62 | 63 | self.load_button = self.findChild(QPushButton, "load") 64 | self.load_button.clicked.connect(self.open_file_name_dialog) 65 | 66 | self.save_button = self.findChild(QPushButton, "save") 67 | self.save_button.clicked.connect(self.write_file) 68 | 69 | self.save_button = self.findChild(QPushButton, "export") 70 | self.save_button.clicked.connect(self.export_runs_as_csv) 71 | 72 | self.exit_button = self.findChild(QPushButton, "exit") 73 | self.exit_button.clicked.connect(self.safe_quit) 74 | 75 | self.interaction_reset = self.findChild(QPushButton, "interactionReset") 76 | self.interaction_reset.clicked.connect(self.reset_gift_record) 77 | 78 | self.path_label = self.findChild(QLabel, "pathValue") 79 | self.version_label = self.findChild(QLabel, "versionValue") 80 | self.run_label = self.findChild(QLabel, "runValue") 81 | self.location_label = self.findChild(QLabel, "locationValue") 82 | 83 | self.darkness_field = self.findChild(QLineEdit, "darknessEdit") 84 | self.gems_field = self.findChild(QLineEdit, "gemsEdit") 85 | self.diamonds_field = self.findChild(QLineEdit, "diamondsEdit") 86 | self.nectar_field = self.findChild(QLineEdit, "nectarEdit") 87 | self.ambrosia_field = self.findChild(QLineEdit, "ambrosiaEdit") 88 | self.keys_field = self.findChild(QLineEdit, "chthonicKeyEdit") 89 | self.titan_blood_field = self.findChild(QLineEdit, "titanBloodEdit") 90 | self.god_mode_damage_reduction_field = self.findChild(QLineEdit, "godModeDamageReductionEdit") 91 | 92 | self.hell_mode = self.findChild(QCheckBox, "hellModeCheckbox") 93 | 94 | self.show() # Show the GUI 95 | 96 | def open_file_name_dialog(self): 97 | options = QFileDialog.Options() 98 | options |= QFileDialog.DontUseNativeDialog 99 | fileName, _ = QFileDialog.getOpenFileName( 100 | self, 101 | "QFileDialog.getOpenFileName()", 102 | f"{QStandardPaths.standardLocations(QStandardPaths.DocumentsLocation)[0]}/Saved Games/Hades", 103 | "All Files (*);;Hades Save Files (*.sav)", 104 | "Hades Save Files (*.sav)", 105 | options=options 106 | ) 107 | if not fileName: 108 | # If user cancels we get an empty string 109 | return 110 | 111 | self.file_path = fileName 112 | self.save_file = HadesSaveFile.from_file(self.file_path) 113 | 114 | self.error_widget.setVisible(0) 115 | 116 | self.path_label.setText(fileName) 117 | self.version_label.setText(str(self.save_file.version)) 118 | self.run_label.setText(str(self.save_file.runs)) 119 | self.location_label.setText(str(self.save_file.location)) 120 | 121 | self.darkness_field.setText(str(self.save_file.lua_state.darkness)) 122 | self.gems_field.setText(str(self.save_file.lua_state.gems)) 123 | self.diamonds_field.setText(str(self.save_file.lua_state.diamonds)) 124 | self.nectar_field.setText(str(self.save_file.lua_state.nectar)) 125 | self.ambrosia_field.setText(str(self.save_file.lua_state.ambrosia)) 126 | self.keys_field.setText(str(self.save_file.lua_state.chthonic_key)) 127 | self.titan_blood_field.setText(str(self.save_file.lua_state.titan_blood)) 128 | self.hell_mode.setChecked(bool(self.save_file.lua_state.hell_mode)) 129 | self.god_mode_damage_reduction_field.setText(str(_damage_reduction_from_easy_mode_level(self.save_file.lua_state.easy_mode_level))) 130 | 131 | self.dirty = True 132 | self.ui_state.setText("Loaded!") 133 | 134 | def write_file(self): 135 | self.save_file.lua_state.darkness = float(self.darkness_field.text()) 136 | self.save_file.lua_state.gems = float(self.gems_field.text()) 137 | self.save_file.lua_state.diamonds = float(self.diamonds_field.text()) 138 | self.save_file.lua_state.nectar = float(self.nectar_field.text()) 139 | self.save_file.lua_state.ambrosia = float(self.ambrosia_field.text()) 140 | self.save_file.lua_state.chthonic_key = float(self.keys_field.text()) 141 | self.save_file.lua_state.titan_blood = float(self.titan_blood_field.text()) 142 | self.save_file.lua_state.hell_mode = bool(self.hell_mode.isChecked()) 143 | self.save_file.hell_mode_enabled = bool(self.hell_mode.isChecked()) 144 | self.save_file.lua_state.easy_mode_level = _easy_mode_level_from_damage_reduction(float(self.god_mode_damage_reduction_field.text())) 145 | 146 | self.save_file.to_file(self.file_path) 147 | self.dirty = False 148 | self.ui_state.setText("Saved!") 149 | 150 | def reset_gift_record(self): 151 | self.save_file.lua_state.gift_record = {} 152 | self.save_file.lua_state.npc_interactions = {} 153 | self.save_file.lua_state.trigger_record = {} 154 | self.save_file.lua_state.activation_record = {} 155 | self.save_file.lua_state.use_record = {} 156 | self.save_file.lua_state.text_lines = {} 157 | self.ui_state.setText("Reset NPC gifting status") 158 | 159 | def safe_quit(self): 160 | if self.dirty: 161 | qm = QMessageBox 162 | ret = qm.question(self, '', "You haven't saved since your last load. Really exit?", qm.Yes | qm.No) 163 | if ret == qm.No: 164 | return 165 | 166 | self.app.quit() 167 | 168 | def _get_aspect_from_trait_cache(self, trait_cache): 169 | for trait in trait_cache: 170 | if trait in gamedata.AspectTraits: 171 | return f"Aspect of {gamedata.AspectTraits[trait]}" 172 | return "Redacted" # This is what it says in game 173 | 174 | def _get_weapon_from_weapons_cache(self, weapons_cache): 175 | for weapon_name in gamedata.HeroMeleeWeapons.keys(): 176 | if weapon_name in weapons_cache: 177 | return gamedata.HeroMeleeWeapons[weapon_name] 178 | return "Unknown weapon" 179 | 180 | def export_runs_as_csv(self): 181 | if not self.file_path: 182 | self.ui_state.setText("Export failed, no savegame loaded!") 183 | return 184 | 185 | runs = self.save_file.lua_state.to_dicts()[0]["GameState"]["RunHistory"] 186 | 187 | csvfilename = "runs.csv" 188 | 189 | import csv 190 | with open(csvfilename, "w", newline='') as csvfile: 191 | run_writer = csv.writer(csvfile, dialect='excel') 192 | run_writer.writerow(["Attempt", "Heat", "Weapon", "Form", "Elapsed time (seconds)", "Outcome", "Godmode", "Godmode damage reduction"]) 193 | 194 | for key, run in runs.items(): 195 | run_writer.writerow([ 196 | # Attempt 197 | int(key), 198 | # Heat 199 | run.get("ShrinePointsCache", ""), # This seems to be heat 200 | # Weapon 201 | self._get_weapon_from_weapons_cache(run["WeaponsCache"]) if "WeaponsCache" in run else "", 202 | # Form 203 | self._get_aspect_from_trait_cache(run["TraitCache"]) if "TraitCache" in run else "", 204 | # Run duration (seconds) 205 | run["GameplayTime"] if "GameplayTime" in run else "", 206 | # Outcome 207 | "Escaped" if run.get("Cleared", False) else "", 208 | # Godmode 209 | "EasyModeLevel" in run, 210 | # Godmode damage reduction 211 | _damage_reduction_from_easy_mode_level(run["EasyModeLevel"]) if "EasyModeLevel" in run else "" 212 | ]) 213 | 214 | self.ui_state.setText(f"Exported to {csvfilename}") 215 | 216 | 217 | if __name__ == "__main__": 218 | app = QtWidgets.QApplication(sys.argv) 219 | mainWin = App(app) 220 | 221 | import sys 222 | 223 | sys.excepthook = except_hook 224 | 225 | sys.exit(app.exec_()) 226 | -------------------------------------------------------------------------------- /pluto.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Pluto 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1024 10 | 768 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | Pluto - Hades Save Editor 21 | 22 | 23 | 24 | 25 | 700 26 | 20 27 | 301 28 | 131 29 | 30 | 31 | 32 | 33 | 34 | 35 | Load 36 | 37 | 38 | 39 | 40 | 41 | 42 | Save 43 | 44 | 45 | 46 | 47 | 48 | 49 | Export runs to CSV 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 890 59 | 720 60 | 113 61 | 32 62 | 63 | 64 | 65 | Exit 66 | 67 | 68 | 69 | 70 | 71 | 30 72 | 230 73 | 160 74 | 23 75 | 76 | 77 | 78 | 79 | 80 | 81 | Darkness: 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 30 94 | 260 95 | 160 96 | 23 97 | 98 | 99 | 100 | 101 | 102 | 103 | Gems: 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 30 116 | 290 117 | 201 118 | 22 119 | 120 | 121 | 122 | 123 | 124 | 125 | true 126 | 127 | 128 | 129 | 0 130 | 0 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | Diamonds: 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 30 148 | 320 149 | 180 150 | 23 151 | 152 | 153 | 154 | 155 | 156 | 157 | Nectar: 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 0 166 | 0 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 30 177 | 350 178 | 160 179 | 23 180 | 181 | 182 | 183 | 184 | 185 | 186 | Ambrosia: 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 30 199 | 380 200 | 160 201 | 23 202 | 203 | 204 | 205 | 206 | 207 | 208 | Chthonic Key: 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 30 221 | 410 222 | 160 223 | 23 224 | 225 | 226 | 227 | 228 | 229 | 230 | Titan Blood: 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 30 243 | 510 244 | 260 245 | 23 246 | 247 | 248 | 249 | 250 | 251 | 252 | God mode damage reduction: 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 20 265 | 20 266 | 671 267 | 51 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 0 288 | 0 289 | 290 | 291 | 292 | 293 | 30 294 | 0 295 | 296 | 297 | 298 | 299 | 10 300 | 16777215 301 | 302 | 303 | 304 | Path 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 20 333 | 70 334 | 671 335 | 51 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | Version 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 20 515 | 170 516 | 671 517 | 51 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | Location 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 20 697 | 120 698 | 671 699 | 51 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | Runs 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | 847 | 848 | 849 | 850 | 851 | 852 | 853 | 854 | 855 | 856 | 857 | 858 | 859 | 860 | 861 | 862 | 863 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 20 879 | 700 880 | 671 881 | 51 882 | 883 | 884 | 885 | 886 | 887 | 888 | true 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 | 902 | 903 | 904 | true 905 | 906 | 907 | 908 | 0 909 | 0 910 | 911 | 912 | 913 | 914 | 30 915 | 0 916 | 917 | 918 | 919 | 920 | 10 921 | 16777215 922 | 923 | 924 | 925 | Error 926 | 927 | 928 | 929 | 930 | 931 | 932 | true 933 | 934 | 935 | 936 | 937 | 938 | 939 | 940 | 941 | 942 | 943 | 944 | 945 | 946 | 947 | 948 | 949 | 950 | 951 | 952 | 953 | 954 | 955 | 956 | 957 | 30 958 | 440 959 | 131 960 | 31 961 | 962 | 963 | 964 | Hell Mode 965 | 966 | 967 | 968 | 969 | 970 | 30 971 | 480 972 | 299 973 | 23 974 | 975 | 976 | 977 | Enable Gifting to all NPCs 978 | 979 | 980 | 981 | 982 | 983 | 700 984 | 170 985 | 301 986 | 51 987 | 988 | 989 | 990 | 991 | 992 | 993 | true 994 | 995 | 996 | 997 | 998 | 999 | 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 | 1009 | true 1010 | 1011 | 1012 | 1013 | 1014 | 1015 | 1016 | 1017 | 1018 | 1019 | 1020 | 1021 | 1022 | 1023 | 1024 | 1025 | 1026 | 1027 | 1028 | 1029 | 1030 | 1031 | 1032 | 1033 | 1034 | 1035 | --------------------------------------------------------------------------------