54 |
55 |
75 |
76 |
104 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/data/types.py:
--------------------------------------------------------------------------------
1 | from enum import IntEnum
2 | from json import load
3 |
4 |
5 | # The possible serialisation types the game uses and their binary value (as int)
6 | class Types(IntEnum):
7 | NullValue = 0,
8 | Bool_True = 1,
9 | Bool_False = 2,
10 | Int32 = 3,
11 | Int64 = 4,
12 | Single = 5,
13 | Double = 6,
14 | Byte = 7,
15 | Char = 8,
16 | String = 9,
17 | String_Indexed = 10,
18 | String_Empty = 11,
19 | Json = 12,
20 | Vector2 = 13,
21 | Vector3 = 14,
22 | Quaternion = 15,
23 | Int32_0 = 16,
24 | Int32_1 = 17,
25 | Single_0 = 18,
26 | Single_1 = 19,
27 | Vector2_00 = 20,
28 | Vector2_11 = 21,
29 | Vector3_000 = 22,
30 | Vector3_111 = 23,
31 | Quaternion_0001 = 24,
32 | GenericList = 100,
33 | Array = 101,
34 | ByteArray = 102,
35 | SmartSerialized = 250
36 |
37 |
38 | # The localisation is english and I have to say I don't know if they are all correct and
39 | # they are not complete, considering I mostly was in it for the items and perks and so on
40 | # But still they may contain spoilers considering quite a few dialogues and so on are in
41 | # them so read it at your own risk
42 | with open("./data/locals.json", encoding="utf8") as f:
43 | id_to_name = load(f)
44 |
45 | # Generic game information
46 | with open("./data/html/items.json", encoding="utf8") as f:
47 | gamedata = load(f)
48 |
49 | with open("./data/data.json", encoding="utf8") as f:
50 | jsongamedata = load(f)
51 |
52 | # Load a list with default items, if those items have special attributes which would otherwise not work
53 | # (using the default item)
54 | with open("./data/new_item_data.json", encoding="utf8") as f:
55 | item_fallback_data = load(f)
56 |
57 | # A example item in the case of the inventory being empty and people wanting to add items to it
58 | fallback_item = {
59 | "type": 250,
60 | "v": {
61 | "value": {
62 | "type": 3,
63 | "v": 6
64 | },
65 | "linked_id": {
66 | "type": 3,
67 | "v": -1
68 | },
69 | "self_chance": {
70 | "type": 250,
71 | "v": {
72 | "_simpified_float": {
73 | "type": 18,
74 | "v": 0
75 | },
76 | "_simplified": {
77 | "type": 2,
78 | "v": False
79 | },
80 | "_expression": {
81 | "type": 11,
82 | "v": ""
83 | },
84 | "default_value": {
85 | "type": 18,
86 | "v": 0
87 | }
88 | }
89 | },
90 | "15320842": {
91 | "type": 100,
92 | "v": []
93 | },
94 | "equipped_as": {
95 | "type": 250,
96 | "v": {
97 | "1826761547": {
98 | "type": 16,
99 | "v": 0
100 | }
101 | }
102 | },
103 | "_params": {
104 | "type": 250,
105 | "v": {
106 | "_hp": {
107 | "type": 18,
108 | "v": 0
109 | },
110 | "_progress": {
111 | "type": 18,
112 | "v": 0
113 | },
114 | "_durability": {
115 | "type": 19,
116 | "v": 1
117 | },
118 | "_res_v": {
119 | "type": 100,
120 | "v": []
121 | },
122 | "_money": {
123 | "type": 18,
124 | "v": 0
125 | },
126 | "_res_type": {
127 | "type": 100,
128 | "v": []
129 | }
130 | }
131 | },
132 | "chance_group": {
133 | "type": 3,
134 | "v": -1
135 | },
136 | "sub_name": {
137 | "type": 11,
138 | "v": ""
139 | },
140 | "is_unique": {
141 | "type": 2,
142 | "v": False
143 | },
144 | "max_value": {
145 | "type": 0,
146 | "v": None
147 | },
148 | "id": {
149 | "type": 10,
150 | "v": "wooden_plank"
151 | },
152 | "min_value": {
153 | "type": 0,
154 | "v": None
155 | },
156 | "common_chance": {
157 | "type": 250,
158 | "v": {
159 | "_simpified_float": {
160 | "type": 18,
161 | "v": 0
162 | },
163 | "_simplified": {
164 | "type": 2,
165 | "v": False
166 | },
167 | "_expression": {
168 | "type": 11,
169 | "v": ""
170 | },
171 | "default_value": {
172 | "type": 18,
173 | "v": 0
174 | }
175 | }
176 | },
177 | "multiquality_items": {
178 | "type": 100,
179 | "v": []
180 | },
181 | "_serialize_depth": {
182 | "type": 16,
183 | "v": 0
184 | },
185 | "1068875674": {
186 | "type": 11,
187 | "v": ""
188 | }
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Graveyard-Keeper-Savefile-Editor
2 | ================================
3 | [](https://opensource.org/licenses/MIT)
4 |
5 | * [Introduction](#introduction)
6 | * [Currently Editable](#currently-editable)
7 | * [Screenshots](#screenshots)
8 | * [Installation](#installation)
9 | + [General](#general)
10 | + [Installing the Python version](#installing-the-python-version)
11 | * [Save File Locations](#save-file-locations)
12 | * [Usage](#usage)
13 | * [Changelog](#changelog)
14 | * [Additional Thanks](#additional-thanks-go-to)
15 | * [Manually editing saves](#manually-editing-saves)
16 | * [The application is not working?](#the-application-is-not-working-)
17 | * [Notice](#notice)
18 | * [Building](#building)
19 |
20 | ## Introduction
21 |
22 | This is a save editor for [Graveyard Keeper](https://store.steampowered.com/app/599140/Graveyard_Keeper/).
23 |
24 | The Python version works on Windows, Linux and macOS.
25 | For Windows a `.exe` version available too.
26 |
27 | It can load and save `*.dat` files.
28 | Additionally you can export the loaded save to JSON, which has a similar structure to the original `.json` save files.
29 | (The difference is, that information about types of variables is saved in the file, meaning every value is wrapped in an object).
30 | When exporting to JSON you can also choose `.html` to have a website where you can relatively easily edit the save in the browser console and then export it again.
31 |
32 | **The Application uses a webbrowser as a GUI** (by default Chrome in App Mode, but during development FireFox was tried too - f.e. Opera should work too, but watch out for your Opera version - see [Issue #54](https://github.com/NetroScript/Graveyard-Keeper-Savefile-Editor/issues/54)).
33 |
34 | I want to add I am not responsible if you break your save file, I try to mantain this editor in a way, in which it can't break save files (or not by mistake) but it is still possible that this editor produces a bugged save file (if you for example add an item in a quality in which the item doesn't exist. - although in non expert mode the editor will prevent saving your file when a non existant item is selected).
35 |
36 | That is why you always should **backup your save files**. (In the settings of the editor you can also set the number of backups the editor should create (which are zipped) - on saving the editor all backups are shifted by one, so if set it to 3 backups and save 4 times, your oldest backup is lost).
37 |
38 | But I want to mention the last time someone asked for help due to a save broken by the save editor was in september 2018 and since then I also took additional measures.
39 |
40 | **This Editor supports the DLCs (Stranger Sins + Game Of Crone + Better Save Soul) - in the editor you have toggles to enable or disable DLC Support**
41 |
42 | ## Currently Editable
43 |
44 | * Money
45 | * Red, green, blue technology points
46 | * HP
47 | * Energy
48 | * Current Time
49 | * Your inventory size
50 | * Your inventory items
51 | * Inventories of all (or at least most) storage units
52 | * Your relationships with NPC's (only if you interacted with them before and have more than 0)
53 | * Additionally utilities like:
54 | * Removing all drops
55 | * Setting the worker efficiency to any value you want
56 | * Turning the graves into perfect graves
57 | * Complete the entire tech tree (state is pre DLC)
58 | * Fix if the donkey is stuck
59 | * Reset the morgue body counter should it be broken
60 | * Remove stuck church goes
61 | * Reset your dungeon
62 |
63 | ## Screenshots
64 |
65 | 
66 |
67 | ## Installation
68 |
69 | ### General
70 | If you have Python installed, you can directly use the master branch (download [here](https://github.com/NetroScript/Graveyard-Keeper-Savefile-Editor/archive/refs/heads/master.zip)) and go over to [Installing the Python version](#installing-the-python-version).
71 | This method of installation works on any platform (Windows, Linux, macOS).
72 |
73 | **If you are on Windows and do not have Python installed**, you can download a `.zip` file containing a `.exe` instead. You can find this `.zip` file on the [releases page](https://github.com/NetroScript/Graveyard-Keeper-Savefile-Editor/releases). The downloaded archive has a bundled version of Python with all necessary modules.
74 |
75 | ### Installing the Python version
76 |
77 | * Download this as zip and extract it any folder
78 | * Get Python (>=3.4)
79 | * If you are using a macOS/Linux system where `python` is by default Python 2.x, replace `python` with `python3`. Additionally according to users you might also need to install `python-tk` if it is not yet included in your distribution.
80 | * Install dependencies using `python -m pip install -r requirements.txt`.
81 | * Execute the file with `python main.py` (in the console with the folder where main.py resides as working directory - to simplify this just create a `run.bat` (Windows) or `run.sh` (Linux) in the directory with the same content)
82 | * Enjoy
83 |
84 |
85 | ## Save File Locations
86 |
87 | Linux / Ubuntu:
88 |
89 | * `/home/$USER/.config/unity3d/Lazy Bear Games/Graveyard Keeper/`
90 |
91 | MacOS:
92 |
93 | * `/Users/$USER/Library/Application Support/unity.LazyBearGames.GraveyardKeeper/`
94 |
95 | Windows:
96 |
97 | * `C:\Users\%username%\AppData\LocalLow\Lazy Bear Games\Graveyard Keeper`
98 |
99 |
100 | **Warning**:
101 | The application supports variables in the path starting with 0.1.17. If you use an earlier version manually fill in your user name for the variables.
102 |
103 | ## Usage
104 |
105 | Considering the application has a GUI it should be self explanatory.
106 | If a red border appears around an item, it means the save editor doesn't have this item indexed. Either because the item doesn't exist, or it was added in an update which wasn't included yet in the editor.
107 | Same if an item doesn't have a preview image.
108 |
109 | If you want to manually edit save files, when you change values, watch out if you change the type of the variable. F.e. if you would change a value from 0 to 1, the type would change and you would need to manually change the type to the correct one.
110 | For information about the possible types, check `types.py` in the `data` folder.
111 |
112 |
113 | ## Changelog
114 |
115 | Check it [here](https://github.com/NetroScript/Graveyard-Keeper-Savefile-Editor/blob/master/changelog.md).
116 |
117 |
118 | ## Additional Thanks go to
119 |
120 | * Reddit user [aMannus](https://www.reddit.com/user/aMannus) for supplying me a save to implement worker efficiency (40%)
121 | * GitHub user [DeadElle](https://github.com/DeadElle) for supplying me a save with 65% worker efficiency
122 | * All contributers to this repository
123 |
124 | ## Manually editing saves
125 |
126 | If you want to know how to manually edit saves or see some additional infos about the saves you can check out the following document [here](https://github.com/NetroScript/Graveyard-Keeper-Savefile-Editor/blob/master/saves.md).
127 |
128 |
129 | ## The application is not working?
130 |
131 | Supply me a screenshot with the console output (or the copied text).
132 | If you are using the compiled windows version and a black window appears and then disappears it means the application crashes because of some error.
133 | To view the error code to be able to send it to me:
134 | In the folder where you have the .exe file, `Shift + Rightclick` in a free space and in the context menu there should be an option like `Open Command Prompt here` or `Open Powershell here`, click that, begin writing `Graveyard` and then press tab to autocomplete and enter to execute - now you should start the application using that console window. This time the window won't close after execution, meaning you have time to make a screenshot of the error.
135 |
136 | ## Notice
137 |
138 | This repository contains content which I do not own.
139 | Notably all the image files in the `/data/html/rsc` folder. These are by [Lazy Bear Games](http://lazybeargames.com/).
140 | Additionally [jQuery](https://jquery.com/) and [Materialize](https://materializecss.com/) are also used. Their original licenses are still included.
141 | This application uses extracted strings like f.e. localisation files. These are also by [Lazy Bear Games](http://lazybeargames.com/).
142 | If you find any bugs / mistakes, feel free to open issues, or if you know how to fix it yourself, feel free to create a pull request.
143 |
144 | ## Building
145 |
146 | For those interested, here is the command I use to generate the folder which I then zip and upload as release:
147 |
148 | (I have this saved as build.bat in the same folder)
149 |
150 | ```batch
151 | py -3.8 -m eel main.py "./data/html" -n "Graveyard Keeper Savefile Editor" -i "./data/html/favicon.ico" --exclude PyQt5 --exclude win32com --exclude pydoc --exclude lib2to3 -y --additional-hooks-dir=hooks
152 |
153 | copy "%cd%\data\hashes" "%cd%\dist\Graveyard Keeper Savefile Editor\data\hashes" /Y
154 | copy "%cd%\data\locals.json" "%cd%\dist\Graveyard Keeper Savefile Editor\data\locals.json" /Y
155 | copy "%cd%\data\data.json" "%cd%\dist\Graveyard Keeper Savefile Editor\data\data.json" /Y
156 | copy "%cd%\data\version" "%cd%\dist\Graveyard Keeper Savefile Editor\data\version" /Y
157 | copy "%cd%\data\itemversion" "%cd%\dist\Graveyard Keeper Savefile Editor\data\itemversion" /Y
158 | copy "%cd%\data\new_item_data.json" "%cd%\dist\Graveyard Keeper Savefile Editor\data\new_item_data.json" /Y
159 |
160 | pause
161 | ```
162 |
163 | In the hooks directory there is a file called `hook-eel.py` with the content:
164 |
165 | ```py
166 | from PyInstaller.utils.hooks import collect_all
167 |
168 | datas, binaries, hiddenimports = collect_all('eel')
169 | ```
--------------------------------------------------------------------------------
/changelog.md:
--------------------------------------------------------------------------------
1 | 0.1.24
2 | =====
3 |
4 | Improved:
5 |
6 | * It is now possible to have a worker zombie of any efficiency
7 | * At the same time you can now decide how many skulls the bodies in the graves should have
8 | * Utility functions can now be collapsed and expanded
9 | * Loading of .info files (more robust display of the save files, should an error with a single file happen, only that save is missing instead of all)
10 |
11 | Fixed:
12 |
13 | * Before when upgrading to the highest grave decoration, you still had to manually "repair" the top part, this is now fixed and it directly display correctly
14 | * When saving new indexed strings now, they get added correctly to the save file (Unless you used the application to export your save to JSON and then edit yourself and then reimported it, this problem doesn't matter for you)
15 |
16 | 0.1.23
17 | =====
18 |
19 | Added:
20 |
21 | * Support for items in the new Better Save Soul DLC (considering Autocomplete and icons)
22 |
23 | 0.1.22
24 | =====
25 |
26 | Added:
27 |
28 | * Preview for items in bags
29 | * Utilities
30 | * Reset dungeon button
31 |
32 | Improved:
33 |
34 | * Greatly improved item deleting and adding, by default it should work much better now, the only problem which might remain is when items are added which have special meta data (like bags), these items need special cases, but for those a working save with the item is needed. Currently supported with special meta data are `bag_universal` and `bag_universal_big`
35 |
36 | 0.1.21
37 | =====
38 |
39 | Added:
40 |
41 | * Camp storage as editable Inventory
42 |
43 | Improved:
44 |
45 | * In the settings you can now disable and enable your owned DLC's. If a DLC is disabled their item names will not show up in the autocomplete. Additionally based on DLC or non DLC some functionality differs (like perfect decoration for graves)
46 |
47 | 0.1.20
48 | =====
49 |
50 | Added:
51 |
52 | * Support for the Game of Crone DLC (meaning items, localisation and npcs)
53 | * When exporting as JSON you can now also select `.html` which generates a HTML file where you can relatively easily edit the JSON in your browser
54 | * Utilities
55 | * Remove church visitors stuck in the church
56 |
57 | 0.1.19
58 | =====
59 |
60 | Improved:
61 |
62 | * In non expert mode you are now warned (red border) about non existant items and you will be unable to save if any exist
63 | * Items which did not show correctly before (like organs) are now shown again
64 | * In the big view of an item you can see the entire item again, instead of only a part of it (for non square items)
65 | * When using the update function for the GUI now the entire GUI is replaced instead of only parts meaning bigger updates can be done using that
66 |
67 | 0.1.18
68 | =====
69 |
70 | Added:
71 |
72 | * Utilities
73 | * Reset morgue body count to 0
74 |
75 | Improved:
76 |
77 | * Now the remove drops button doesn't bug your morgue (when removing bodies from the floor it will decrease the morgue counter)
78 | * Replaced some NPC images with better ones and added some
79 | * Hints / Warnings are now hidden by default and you can hover to display them so they don't clutter the UI as much with text
80 | * Now the application requests focus when opening file dialogs, so now they don't hide behind tons of windows anymore
81 |
82 | 0.1.17
83 | =====
84 |
85 | Improved:
86 |
87 | * The editor now tries to automatically get the save file location on the first start of the application
88 | * The save file location field now allows path variables
89 | * The Open button in the settings menu will now start in the directory which is currently in the field
90 | * If you want to use your context menu (to f.e. inspect elements for debugging) you can now press `ALT` once to enable it again
91 |
92 | 0.1.16
93 | =====
94 |
95 | Added:
96 |
97 | * New items (of version 1.200 / DLC)
98 |
99 | Improved:
100 |
101 | * Instead of having an image for every item a spritesheet is used now which speeds up interface loading times
102 |
103 | 0.1.15
104 | =====
105 |
106 | Improved:
107 |
108 | * Utilities
109 | * Remove drops button now also removes red, green and blue points
110 |
111 | 0.1.14
112 | =====
113 |
114 | Added:
115 |
116 | * Utilities
117 | * Fix for a stuck donkey
118 |
119 | 0.1.13
120 | =====
121 |
122 | Added:
123 |
124 | * Utilities
125 | * Complete tech tree
126 |
127 | Improved:
128 |
129 | * You can now set in settings how many backups are kept, additionally those are zipped now to safe space
130 | * Added hint considering usage of Perfect Body / Grave / ...
131 | * Greatly decreased the time until the GUI is interactable when loading a save
132 |
133 | 0.1.12
134 | =====
135 |
136 | Added:
137 |
138 | * Utilities
139 | * Set the efficiency of all workers to 40%
140 | * Turn all bodies in your graveyard (in graves) into perfect bodies
141 | * Turn all decorations of your grave into the highest possible grade
142 | * Turn empty graves into a grave with a perfect body and the best decoration
143 |
144 | 0.1.11
145 | =====
146 |
147 | Improved:
148 |
149 | * BIG Item update - I now took the time to extract all possible item ids, their localisation in english and the item sprite (That doesn't mean you should use all new items, because now the editor also includes f.e. placeholder items which work in the inventory but you should only use items you really want)
150 | * You can now click on the icon of a day to switch to the day (instead of only being able to enter a number)
151 |
152 | Added:
153 |
154 | * Expert Mode, you can now disable all checks and warnings (although because of the item update at the same time, I doubt that you will need it anymore)
155 |
156 | 0.1.10
157 | =====
158 |
159 | Improved:
160 |
161 | * Browser automatically closes now when the application updated the items
162 | * If over 99 gold coins are set no red underline is shown anymore (and it now also loads higher amounts correclty)
163 | * Should there be no chrome installation or an error happens while opening chrome the default browser will be used now
164 | * Load a .png file as icon if the .ico file is failing (On some Linux distros)
165 |
166 | 0.1.9
167 | =====
168 |
169 | Fixed:
170 |
171 | * Items / Images (removed / renamed / added items, images) (tool related items)
172 |
173 | Added:
174 |
175 | * The application can now notify you (and update itself) about item changes (previously you only had item changes on the windows version if a complete new version was releases - now track of a itemversion is kept and it will notify you about changes and you can update manually or automatically. Those changes include: wrong item names, missing / wrong item icon, missing / wrong item which you can add to inventories)
176 |
177 | Improved:
178 |
179 | * Some CSS
180 | * Items which were previously undefined in the editor (now not a single item is shown as undefined)
181 |
182 | 0.1.8
183 | =====
184 |
185 | Fixed:
186 |
187 | * Items / Images (removed / renamed / added items, images)
188 |
189 | Added:
190 |
191 | * You can now edit the ingame time
192 | * You can now edit the "tool" inventory which was added
193 |
194 | Improved:
195 |
196 | * Added some hashes so looking at the generated .json is easier
197 | * Changed the displayed ingame days in the slot loader because for some strange reason the game subtracts 1.5 from the saved value (this editor now does that too)
198 |
199 | 0.1.7
200 | =====
201 |
202 | Fixed:
203 |
204 | * Items / Images (removed / renamed / added items, images)
205 | * Small bug considering the existance of multiple entries in a list, see more here https://github.com/NetroScript/Graveyard-Keeper-Savefile-Editor/commit/8c0bd69a8df75e09202be00586afee39ed0b7e6f
206 | * Fixing wrong character in object leading to errors when adding items to external storage
207 | * Wrong usage of JQuery leading to the state of the checkbox being read incorrectly
208 |
209 | Added:
210 |
211 | * In the settings menu you can now choose your own port. Additionally you are now able to edit the settings again without deleting the settings file.
212 | * You can now remove item drops from the map. (Intended to reduce lag if you have tons of them somewhere)
213 |
214 | Improved:
215 |
216 | * Wait for input on exception so it is easier to report errors
217 | * Save NaN as null in the JSON so it can be parsed by strict parsers
218 | * More comments for the source code
219 | * Loading circle in the editor so the user knows when he can edit a specific save
220 | * Removed content which wasn't working (perks - now you can only look at them :v)
221 |
222 | 0.1.6
223 | =====
224 |
225 | Fixed:
226 |
227 | * Items which contain `(` in their names
228 | * Many items
229 | * Removing / adding item names according to their qualities
230 | * Deleting item images which are not really needed
231 | * Adding new items to the editor autocomplete (Wooden planks, Fake coins, Instructions for the key, Cleric's Beginner's Guide)
232 | * Adding some missing images (of items)
233 | * Fixing some names of items
234 | * Save issues when the inventory was empty
235 |
236 | 0.1.5
237 | =====
238 |
239 | Fixed:
240 |
241 | * A mistake on my side which partly breaks your save file, which was introduced in 9c8be820dba71381a4e4fce4ed64813a39881400 - It is only a small change but an own release because it breaks save files. If your save file was broken by this bug, leave me a message because the fix is rather simple (it is just adding a byte at a specific location).
242 |
243 |
244 | 0.1.4
245 | =====
246 |
247 | Added:
248 |
249 | * Support for modifying NPC Relationship values
250 |
251 |
252 | 0.1.3
253 | =====
254 |
255 | Fixed:
256 |
257 | * Bug causing some strings to be saved again in the wrong way. It is strongly advised to update because otherwise you will lose f.e. Comfort of Faith technology after every modification
258 |
259 |
260 | 0.1.2
261 | =====
262 |
263 | Added:
264 |
265 | * You can now edit and view every storage unit (like trunks, chests, ...)
266 | * The information about a new update available now also displays the changes
267 |
268 | Fixed:
269 |
270 | * Some HTML formatting
271 | * Some items names / icons, including but not limited to restoration tools, coal, clay, jointing, water, simple iron parts, complex iron parts, ...
272 |
273 | 0.1.1
274 | =====
275 |
276 | Added:
277 |
278 | * Support for item qualities
279 | * Rightful Citizen Papers so you can add them to your save if they were missing
280 |
281 | Fixed:
282 |
283 | * Added some item id's
284 | * Fixed a typo in the Github URL leading to 404
285 | * The church quality and graveyard quality being swapped
286 | * Error which happens when using the application on an early savegame
287 |
--------------------------------------------------------------------------------
/data/encode.py:
--------------------------------------------------------------------------------
1 | from data.tools import BinaryWriter
2 | from data.types import Types
3 |
4 |
5 | class Encoder:
6 |
7 | # On Initialisation we need our Hashes object to be able to translate our converted strings back into the numerical
8 | # values
9 | def __init__(self):
10 | self.hashes = {}
11 |
12 | # Encode a file, supply the path to the file and the data of the save here
13 | def encode(self, file_path, data):
14 | # The hashes of the save are stored in the data, so we set them here
15 | self.hashes = data["hashes"]
16 | # Begin writing to our output file
17 | with open(file_path, "wb") as fi:
18 | print("Beginning to encode to: " + file_path)
19 | # Create a instance of the Binary Writer with the instance of our file
20 | bw = BinaryWriter(fi)
21 | bw.write("int64", 0)
22 | print("Writing header")
23 | bw.write("int64", data["header"]["offset"])
24 | bw.write("int32", data["header"]["version"])
25 |
26 | # This is part of the header, I assume the Lazy Bear Games put this in to be able to add more header
27 | # information at a later points. Currently (Game version 1.034) this information is not filled
28 | for i in range(15):
29 | bw.write("int32", 0)
30 | print("Writing gamedata")
31 | self.serialize(data["savedata"], bw, data["serializer"])
32 | print("Writing strings")
33 | self.insertserializer(bw, data["serializer"])
34 | print("Finished saving file: " + file_path)
35 |
36 | # A recursive function to turn an object into the binary data in the form of basic values
37 | def serialize(self, data, stream, serializer):
38 | if data is None:
39 | stream.write("int32", -1)
40 | else:
41 | # Save how many properties our object has
42 | stream.write("int32", len(data))
43 | #print(len(data))
44 | i = 0
45 |
46 | # Iterate the properties of our object
47 | for key in data:
48 | # If we had our string before turn the string back into the numerical hash
49 | if type(key) is str and key in self.hashes:
50 | hash = int(self.hashes[key])
51 | else:
52 | hash = int(key)
53 | #print(hash)
54 | # Write the hash to the file
55 | stream.write("int32", hash)
56 | # Start writing the contents of the object to the file
57 | self.serializedata(data[key], stream, serializer, hash)
58 | i+=1
59 |
60 | # Serialize 1 object / value / list / ....
61 | def serializedata(self, data, stream, serializer, hash):
62 |
63 | # In the case of our object being a specific object like a Vector we check if the type of object changed
64 | if type(data) is dict and "type" in data:
65 | if data["type"] == "Vector2":
66 | if data["x"] == 0 and data["y"] == 0:
67 | curtype = Types.Vector2_00.value
68 | elif data["x"] == 1 and data["y"] == 1:
69 | curtype = Types.Vector2_11.value
70 | else:
71 | curtype = Types.Vector2.value
72 | elif data["type"] == "Vector3":
73 | if data["x"] == 0 and data["y"] == 0 and data["z"] == 0:
74 | curtype = Types.Vector3_000.value
75 | elif data["x"] == 1 and data["y"] == 1 and data["z"] == 1:
76 | curtype = Types.Vector3_111.value
77 | else:
78 | curtype = Types.Vector3.value
79 | elif data["type"] == "Quaternion":
80 | if data["x"] == 0 and data["y"] == 0 and data["z"] == 0 and data["n"] == 1:
81 | curtype = Types.Quaternion_0001.value
82 | else:
83 | curtype = Types.Quaternion.value
84 |
85 | # Otherwise if it is our default notation we just use the type which is stored there
86 | if "v" in data:
87 | curtype = data["type"]
88 | data = data["v"]
89 |
90 | # Now we write the type as Byte to the file
91 | stream.write("uint8", curtype)
92 | # Depending on the current type we call the function recursively again or just pass onto the next value
93 | # (In the case of simple values like Null)
94 | if curtype == Types.SmartSerialized:
95 | # Call it again recursively
96 | self.serialize(data, stream, serializer)
97 | elif curtype == Types.NullValue:
98 | pass
99 | elif curtype == Types.String:
100 | self.insertstring(data,stream, False)
101 | elif curtype == Types.String_Empty:
102 | pass
103 | elif curtype == Types.String_Indexed:
104 |
105 | # Here we check additionally if all strings which have a type of Indexed String are actually in the string
106 | # array, if those are not in there, they are added
107 | try:
108 | indx = serializer.index(data)
109 | except ValueError:
110 | indx = -1
111 |
112 | # Check if the string is encoded as a special object
113 | for i in range(len(serializer)):
114 | if type(serializer[i]) == dict:
115 | if data == serializer[i]["string"]:
116 | indx = i
117 | break
118 |
119 | # If it is not encoded as a special object, add it at the end of the string list
120 | if indx == -1:
121 | indx = len(serializer)
122 | serializer.append(data)
123 |
124 | # Save the index of the string within the string array
125 | stream.write("int32", indx)
126 | elif curtype == Types.Int32_0:
127 | pass
128 | elif curtype == Types.Int32_1:
129 | pass
130 | elif curtype == Types.Int32:
131 | stream.write("int32", data)
132 | elif curtype == Types.Int64:
133 | stream.write("int64", data)
134 | elif curtype == Types.Single:
135 | try:
136 | stream.write("float", float(data))
137 | except TypeError:
138 | stream.write("float", float(0))
139 | elif curtype == Types.Single_0:
140 | pass
141 | elif curtype == Types.Single_1:
142 | pass
143 | elif curtype == Types.Double:
144 | stream.write("double", data)
145 | elif curtype == Types.Byte:
146 | return stream.write("uint8", data)
147 | elif curtype == Types.Char:
148 | return stream.write("char", data)
149 | elif curtype == Types.Bool_True:
150 | pass
151 | elif curtype == Types.Bool_False:
152 | pass
153 | elif curtype == Types.Vector2:
154 | stream.write("float", data["x"])
155 | stream.write("float", data["y"])
156 | elif curtype == Types.Vector2_00:
157 | pass
158 | elif curtype == Types.Vector2_11:
159 | pass
160 | elif curtype == Types.Vector3:
161 | stream.write("float", data["x"])
162 | stream.write("float", data["y"])
163 | stream.write("float", data["z"])
164 | elif curtype == Types.Vector3_000:
165 | pass
166 | elif curtype == Types.Vector3_111:
167 | pass
168 | elif curtype == Types.Quaternion:
169 | stream.write("float", data["x"])
170 | stream.write("float", data["y"])
171 | stream.write("float", data["z"])
172 | stream.write("float", data["n"])
173 | elif curtype == Types.Quaternion_0001:
174 | pass
175 | elif curtype == Types.ByteArray:
176 | # Save the length of our Array to the file
177 | stream.write("int32", len(data))
178 | for i in range(len(data)):
179 | stream.write("uint8", data[i])
180 | elif curtype == Types.GenericList:
181 | # Save the length of our Array to the file
182 | stream.write("int32", len(data))
183 | # Recursively save every item in the list as new object
184 | for i in range(len(data)):
185 | self.serializedata(data[i], stream, serializer, hash)
186 | elif curtype == Types.Array:
187 | # Save the length of our Array to the file
188 | stream.write("int32", len(data))
189 | # Recursively save every item in the list as new object
190 | for i in range(len(data)):
191 | self.serializedata(data[i], stream, serializer, hash)
192 | # If it is an unknown data type
193 | else:
194 | print("Datatype: " + str(curtype) + " can not be parsed")
195 |
196 | # Function to save a string to the file
197 | def insertstring(self, string, stream, encrypt=False):
198 | # If the string is non existant write a -1
199 | if string is None:
200 | stream.write("int32", -1)
201 | return
202 |
203 | # If the string was modified while loading (because of Unicode character) we save the original length and byte
204 | # buffer to the file
205 | if type(string) == dict:
206 | stream.write("int32", string["length"])
207 | for byte in string["buffer"]:
208 | stream.write("int8", byte)
209 | else:
210 | # Otherwise we just save the string length and then write every character as single Byte
211 | stream.write("int32", len(string))
212 | for char in string:
213 | n = ord(char)
214 | # For the String array at the end of the file we also encode the string
215 | if encrypt:
216 | if n <= 255 and n != 0 and n != 109:
217 | n ^= 0x6D
218 | stream.write("uint8", n)
219 |
220 | # We save our string array at the end of the file
221 | def insertserializer(self, stream, serializer):
222 | # The position of the end of the file
223 | pos = stream.file.tell()
224 | # Then we go to the beginning and save the position of the current end of the file to be able to read the
225 | # serialized Strings first
226 | stream.file.seek(0)
227 | stream.write("int64", pos)
228 | # we now go back to the end of the file
229 | stream.file.seek(pos)
230 | # Write how many strings are contained in the string array
231 | stream.write("int32", len(serializer))
232 | # Save every string encrypted to the file
233 | for i in range(len(serializer)):
234 | self.insertstring(serializer[i], stream, True)
235 |
--------------------------------------------------------------------------------
/data/decode.py:
--------------------------------------------------------------------------------
1 | from data.tools import BinaryReader
2 | from data.types import Types
3 | from data.corruptionfix import prefix, postfix
4 |
5 |
6 | class Decoder:
7 |
8 | # On Initialisation we need our Hashes object to be able to translate the numerical hashes to more readable strings
9 | def __init__(self, hashes):
10 | self.hashes = hashes
11 |
12 | # Decode a file, supply the path to the file here
13 | def decode(self, file):
14 | print("Beginning to decode file: " + file)
15 | with open(file, "rb") as f:
16 | # Create a instance of the Binary Reader with the path to our file
17 | b = BinaryReader(f)
18 | print("Extracting String Array")
19 | sd = self.extractserializer(b)
20 | header = dict()
21 | print("Extracting header")
22 | header["offset"] = b.read("int64")
23 | header["version"] = b.read("int32")
24 |
25 | # This is part of the header, I assume the Lazy Bear Games put this in to be able to add more header
26 | # information at a later points. Currently (Game version 1.034) this information is not filled
27 | for i in range(15):
28 | b.read("int32")
29 | # print(header)
30 | # print(sd)
31 | print("Extracting game objects")
32 |
33 | # Deserialize the main save
34 | x = self.deserialize(b, sd, {})
35 |
36 | # Create the output object
37 | obj = {
38 | "savedata": x,
39 | "header": {
40 | "offset": header["offset"],
41 | "version": header["version"]
42 | },
43 | "serializer": sd,
44 | "hashes": self.hashes.name_to_hash
45 | }
46 | print("Loaded file: " + file)
47 | return obj
48 |
49 | # A recursive function to which you supply a data object
50 | # For that a layer is supplied, so the function can build the object layer by layer without needing a global object
51 | # The serializer is the extracted string data
52 | def deserialize(self, stream, serializer, layer):
53 |
54 | # Information how many objects are stored in this dataset
55 | n = stream.read("int32")
56 |
57 | if n == -1:
58 | return
59 | # print(num)
60 | # print("Following things are Serialized")
61 |
62 | # Iterate the stored datasets / objects
63 | for i in range(n):
64 | # Extract the hash of the object, this is equal to the property name of the object (f.e. toplevel this would
65 | # be something like the property Inventory of the object GameSave
66 | hash = str(stream.read("int32"))
67 | # Create a copy of the hash
68 | nam = hash
69 |
70 | # If we have a string for the hash we assign this string to nam
71 | if hash in self.hashes.hash_to_name:
72 | nam = self.hashes.hash_to_name[hash]
73 | # print("Hash: " + str(nam))
74 | # pass
75 |
76 | # In the current layer we create a property with either the hash or the string as key
77 | layer[nam] = dict()
78 | # print("Deserialized Object:")
79 |
80 | # After extracting the information about the current object we now extract it's data
81 | x = self.deserializedata(stream, serializer, layer[nam], hash)
82 |
83 | layer[nam] = x
84 | return layer
85 |
86 | # Deserialize 1 object / value / list / ....
87 | def deserializedata(self, stream, serializer, layer, hash):
88 |
89 | # We extract a Byte which determines which type of object we have
90 | curtype = stream.read("uint8")
91 |
92 | # We assign the type to the hash to be able get the data type just from the hash (in the encoding function)
93 | self.hashes.hash_to_type[hash] = curtype
94 | # print("Datatype: " + str(curtype))
95 |
96 | # Depending on the Type we extract the data in different ways
97 | if curtype == Types.SmartSerialized:
98 | # As child we have again an object
99 | return {"v": self.deserialize(stream, serializer, layer), "type": curtype}
100 | elif curtype == Types.NullValue:
101 | return {"v": None, "type": curtype}
102 | elif curtype == Types.String:
103 | return {"v": self.extractstring(stream, False), "type": curtype}
104 | elif curtype == Types.String_Empty:
105 | return {"v": "", "type": curtype}
106 | elif curtype == Types.String_Indexed:
107 | return {"v": serializer[stream.read("int32")], "type": curtype}
108 | elif curtype == Types.Int32_0:
109 | return {"v": 0, "type": curtype}
110 | elif curtype == Types.Int32_1:
111 | return {"v": 1, "type": curtype}
112 | elif curtype == Types.Int32:
113 | return {"v": stream.read("int32"), "type": curtype}
114 | elif curtype == Types.Int64:
115 | return {"v": stream.read("int64"), "type": curtype}
116 | elif curtype == Types.Single:
117 | return {"v": stream.read("float"), "type": curtype}
118 | elif curtype == Types.Single_0:
119 | return {"v": 0, "type": curtype}
120 | elif curtype == Types.Single_1:
121 | return {"v": 1, "type": curtype}
122 | elif curtype == Types.Double:
123 | return {"v": stream.read("double"), "type": curtype}
124 | elif curtype == Types.Byte:
125 | return {"v": (stream.read("uint8")).to_bytes(1, byteorder="little"), "type": curtype}
126 | elif curtype == Types.Char:
127 | return {"v": stream.read("char"), "type": curtype}
128 | elif curtype == Types.Bool_True:
129 | return {"v": True, "type": curtype}
130 | elif curtype == Types.Bool_False:
131 | return {"v": False, "type": curtype}
132 | elif curtype == Types.Vector2:
133 | return {"x": stream.read("float"), "y": stream.read("float"), "type": "Vector2"}
134 | elif curtype == Types.Vector2_00:
135 | return {"x": 0, "y": 0, "type": "Vector2"}
136 | elif curtype == Types.Vector2_11:
137 | return {"x": 1, "y": 1, "type": "Vector2"}
138 | elif curtype == Types.Vector3:
139 | return {"x": stream.read("float"), "y": stream.read("float"), "z": stream.read("float"), "type": "Vector3"}
140 | elif curtype == Types.Vector3_000:
141 | return {"x": 0, "y": 0, "z": 0, "type": "Vector3"}
142 | elif curtype == Types.Vector3_111:
143 | return {"x": 1, "y": 1, "z": 1, "type": "Vector3"}
144 | elif curtype == Types.Quaternion:
145 | return {"x" :stream.read("float"), "y": stream.read("float"), "z": stream.read("float"), "n": stream.read("float"), "type": "Quaternion"}
146 | elif curtype == Types.Quaternion_0001:
147 | return {"x": 0, "y": 0, "z": 0, "n": 1, "type": "Quaternion"}
148 | elif curtype == Types.ByteArray:
149 | # Length of the saved array
150 | amount = stream.read("int32")
151 | arr = []
152 | for i in range(amount):
153 | arr.append((stream.read("uint8")).to_bytes(1, byteorder="little"))
154 | return {"v": arr, "type": curtype}
155 | elif curtype == Types.GenericList:
156 | # Length of the saved array
157 | amount = stream.read("int32")
158 | arr = []
159 | for i in range(amount):
160 | # Iterate and extract the data of an array
161 | arr.append(self.deserializedata(stream, serializer, {}, hash))
162 |
163 | return {"v": arr, "type": curtype}
164 | elif curtype == Types.Array:
165 | # Length of the saved array
166 | amount = stream.read("int32")
167 | arr = []
168 | for i in range(amount):
169 | # Iterate and extract the data of an array
170 | arr.append(self.deserializedata(stream, serializer, {}, hash))
171 |
172 | return {"v": arr, "type": curtype}
173 | # If it is an unknown data type
174 | else:
175 | print("Datatype: " + str(curtype) + " can not be parsed")
176 |
177 | # Extract the array of strings which is at the end of the save file
178 | def extractserializer(self, stream):
179 | # The position in bytes of the serialized string array
180 | pos = stream.read("int64")
181 | # our current position in the bytes
182 | pos2 = stream.file.tell()
183 | # print(pos2)
184 | # We go to the position in the file stream where the strings are stored
185 | stream.file.seek(pos)
186 | # we read how many strings are stored
187 | n = stream.read("int32")
188 | data = []
189 | # print(n)
190 |
191 | # We extract every string and push it into our serializer array
192 | for i in range(n):
193 | data.append(self.extractstring(stream, True))
194 |
195 | # We return to our start position (because after this function the object data will be read)
196 | stream.file.seek(pos2)
197 | return data
198 |
199 | # Function to extract a single string
200 | def extractstring(self, stream, encrypt=False):
201 |
202 | # We extract the length of the string (in Characters, not Bytes, but some Characters have multiple Bytes thats
203 | # why we have the prefix and postfix functions)
204 | n = stream.read("int32")
205 | # print(n)
206 | if(n == -1):
207 | return None
208 |
209 | buffer = []
210 | out = ""
211 | # store where the string starts in the file stream
212 | beginning = stream.file.tell()
213 | # Create a buffer of bytes in the length of the string
214 | for i in range(n):
215 | # buffer.append(int.from_bytes(stream.read("char"), byteorder="little"))
216 | buffer.append(stream.read("int8"))
217 |
218 |
219 | # We now add buffers and the length of the read to a special object should specific unicode values be detected,
220 | # so we can save the original buffer when encoding again
221 | specialread = prefix(buffer, stream, beginning, len(buffer))
222 |
223 | # print(num4)
224 | # in the case of the string being encrypted (which is the case in the string array at the end of the save file)
225 | if encrypt:
226 | for i in range(len(buffer)):
227 | num5 = buffer[i]
228 | # Replace the numerical buffer value with the character
229 | if num5 <= 255 and num5 is not 0 and num5 is not 109:
230 | buffer[i] = chr(abs(num5 ^ 0x6D))
231 | else:
232 | buffer[i] = chr(abs(num5))
233 | else:
234 | for i in range(len(buffer)):
235 | # Replace the numerical buffer value with the character
236 | buffer[i] = chr(abs(buffer[i]))
237 |
238 | # We combine every char we extracted to our output string
239 | out = ''.join(buffer)
240 |
241 | # Fixing our output string
242 | out = postfix(out, stream, buffer)
243 |
244 | # In the case of a unicode read we return a special object, otherwise we return just the string
245 | if type(specialread) == dict:
246 | out = {"string": out, "length": specialread["length"], "buffer": specialread["buffer"]}
247 |
248 | #print(out)
249 |
250 | return out
251 |
--------------------------------------------------------------------------------
/saves.md:
--------------------------------------------------------------------------------
1 | Information about additional edits which are possible in the JSON file
2 | ======================================================================
3 |
4 | World game objects
5 | ------------------
6 |
7 | In the file save (exportet as JSON) you have in `savedata->map->v->_wgos->v` a list with all "World Game Objects" of the game.
8 | Those include all interactable objects and more. The game saves the location of those. These include f.e. spots where hiccup grass grows, meaning you could change its location next to your house or even duplicate the objects (is what I assume, I didn't try it)
9 | Other interesting things you could change there are f.e. location of built objects, teleport spots (?, ids teleport_hatch, teleport_inside, teleport_outside, teleport_point - 1 teleport_hatch / teleport_inside / teleport_outside corresponds with 1 teleport_point because they have the same custom tag, meaning you probably can create new teleport points f.e. 1 infront of your house to the lighthouse) or NPC objects.
10 | Meaning if your NPC is broken you could try replacing it's data with the data from a save where it is not broken. (As far as I can tell no achievement or progress related data is stored in them, but I only took a look at a early game save).
11 |
12 | A list of a 10 Minute save with all the possible world game object ids and their amounts would be (as JSON):
13 |
14 | ```JSON
15 | {"swamp_table_constr":1,"swamp_bridge":1,"teleport_outside":13,"teleport_point":41,"mushroom_1":72,"fence_stone_h":14,"fence_wood_anc_b_1":20,"fence_wood_anc_r_2":3,"fence_wood_anc_f_1":16,"fence_wood_anc_b_0":5,"fence_wood_anc_b_10":4,"fence_wood_anc_f_0":2,"fence_wood_anc_r_10":2,"fence_wood_anc_r_4":1,"fence_wood_anc_r_1":6,"fence_wood_anc_r_0":1,"fence_wood_anc_l_10":3,"fence_wood_anc_l_2":2,"fence_wood_anc_l_1":6,"fence_wood_anc_f_10":3,"fence_wood_anc_f_6":6,"fence_wood_anc_f_4":4,"fence_wood_anc_b_5":2,"fence_wood_anc_b_4":3,"fence_wood_anc_b_2":3,"bat_test":10,"bonfire_burning":2,"mining_builddesk_broken":1,"d_obj_spiral_stair_stn_in_1":3,"teleport_inside":21,"teleport_hatch":5,"garden_builddesk_broken":1,"fence_wood_anc_l_0":5,"fence_wood_anc_l_4":5,"graveyard_zone_lvlup2":1,"graveyard_builddesk":1,"mf_wood_builddesk":1,"morgue_builddesk_broken":1,"church_builddesk":1,"cellar_builddesk":1,"tree_2_6":49,"tree_tiny_2":89,"tree_2_2":114,"tree_tiny_1":107,"tree_1_3":99,"bush_4":32,"bush_horizontal_3":42,"garden_grapes_broken":8,"flower_small_1":68,"flower_small_3":75,"flower_small_2":84,"flower_small_4":54,"flower_small_5":78,"flower_small_6":50,"flower_small_8":59,"flower_small_9":55,"roof_1":4,"beehouse_broken":4,"beegarden_table_broken":1,"tree_garden_builddesk":1,"stone_2":40,"tree_huge_1":10,"tree_old_2":23,"bush_5":219,"flower_small_7":83,"tree_old_1":24,"tree_old_3":21,"cremation_builddesk":1,"old_wood_stump_1":26,"old_wood_swamp_set_1":23,"old_wood_swamp_set_2":28,"wood_obstacle_steep_el":1,"wood_obstacle_v":1,"bush_horizontal_1":17,"bush_1_berry":7,"stone_4":30,"stone_3":19,"decor_stone":21,"stone_5":20,"stone_1":27,"land_clay_spot_1":2,"church_0":1,"npc_royal_box":1,"tree_mid_strip_1":17,"tree_3_1":43,"tree_mid_strip_2":17,"water_well":3,"mf_grindstone_1":2,"village_wc_Face":3,"house_1":1,"mf_box_stuff":4,"cooking_bonfire":2,"garden_of_stones_place":1,"witch_hut":1,"npc_witch":1,"church_closed_door":1,"morgue_1":1,"ruin_0":3,"morgue_throw_in_broken":1,"iron_ore":32,"stone_ore":3,"pile_of_sand":5,"lying_rock":1,"vegit_bracken_1":7,"vegit_bracken_2":43,"tree_2_1":35,"tree_2_4":73,"tree_3_4":23,"tree_3_3":34,"tree_1_2":76,"tree_2_5":50,"tree_3_2":39,"tree_tiny_3":83,"tree_1_1":55,"tree_1_4":54,"tree_3_3_stump":2,"tree_3_4_stump":3,"tree_1_1_stump":3,"bush_plump_2":36,"bush_2":100,"bush_horizontal_2":21,"bush_2_berry":10,"bush_3_berry":14,"mushroom_2":47,"hiccup_grass_3":4,"hiccup_grass_2":6,"hiccup_grass_1":8,"gate_wood_new_f_0":1,"fence_stone_v":5,"grave_ground":13,"lantern_obj":9,"tree_2_1_stump":8,"mf_timber_1":1,"mf_stones_1":1,"bridge_stn_broken":1,"tree_3_1_stump":4,"roof_2":3,"swamp_fishing_spot":1,"throw_body_river":1,"Tavern":1,"lantern_2":1,"mf_stones_1_decor":2,"mf_ore_1_complete_decor":6,"village_wc_Rgh":8,"stone_workshop_1":1,"village_house_1":2,"village_wc_Face_2":3,"village_house_2":1,"tree_3_4_bees_done":2,"tree_2_4_stump":2,"tree_2_5_stump":3,"tree_3_1_bees":1,"village_mill":1,"village_farmer_house_sml":1,"mole":3,"mill_broken_obj":1,"mf_anvil_2_decor":2,"mf_anvil_1_decor":1,"mf_anvil_3_decor":1,"mf_wood_panel_2_complete":1,"mf_coal_1_decor":2,"smithy_1":1,"barrel":14,"village_house_3":1,"garden_pumpkin_ready_village":4,"beehouse_decor":6,"garden_pumpkin":4,"tree_1_2_stump":1,"decor_tree_apple_1_flower":3,"tree_3_1_bees_done":3,"village_house_4":2,"npc_beekeeper":1,"garden_cabbage_ready_village":18,"village_house_5":1,"garden_carrot_ready_village":3,"garden_carrot":17,"garden_beet":14,"village_house_6":1,"garden_beet_ready_village":1,"garden_onion":11,"garden_grapes":3,"garden_lentils":9,"garden_cabbage_ready":2,"garden_cabbage":13,"village_house_7":1,"ruins_el_2":9,"garden_onion_ready_village":1,"garden_lentils_ready_village":1,"decor_tree_apple_2_green":2,"decor_tree_apple_3_red":1,"decor_tree_apple_1_red":4,"decor_tree_apple_2_flower":3,"camp_barrel_wagon":3,"village_farmer_house_mid":1,"village_henhouse":2,"village_hut_1":1,"egg_seller":1,"camp_horse_parking":1,"camp_tent_db_1":2,"camp_tent_df_1":1,"camp_tent_vf_1":2,"camp_tent_vf_2":1,"camp_wagon_chest_d":2,"camp_wagon_stuff":3,"camp_wagon_stuff_2":1,"campfire":2,"tree_1_4_stump":1,"storage_1":1,"decor_land_clay_spot_1":2,"mf_potter_wheel_1_decor":1,"npc_potter":1,"npc_shepherd":1,"npc_shepherds_wife":1,"farm":1,"sealman_house":1,"mf_timber_1_decor":2,"mf_beam_gantry_1_decor":2,"tree_2_2_stump":1,"garden_cannabis":11,"decor_mf_vine_press":1,"npc_dig":1,"village_pithos":1,"ruins_pillar_2":5,"burn_0":1,"tree_3_2_stump":1,"bonfire_smoldering":1,"npc_guard_9":1,"npc_guard_10":1,"forpost":1,"hatch_from_morgue":1,"mine":1,"sawmill_1":1,"big_broken_bridge":1,"lighthouse":1,"tree_1_3_stump":1,"ruins_viaduct_1":2,"ruins_viaduct_5":1,"ruins_viaduct_6":3,"ruins_viaduct_7":1,"ruins_viaduct_2":1,"ruins_pillar_1":7,"ruins_pillar_5":3,"ruins_pillar_6":3,"ruins_el_1":7,"turnpike_close":3,"nameplate_2":1,"npc_actor":1,"npc_gypsy":1,"npc_wood_cutter":1,"donkey":1,"npc_miller":1,"npc_blacksmith":1,"npc_farmer":1,"npc_tavern owner":1,"npc_captain":1,"npc_mrs chain":1,"npc_guard":1,"npc_guard_3":1,"npc_guard_4":1,"npc_guard_5":1,"npc_carpenter":1,"npc_engineer":1,"npc_farmers son":1,"talking_skull":1,"npc_redneck_1":1,"npc_redneck_2":1,"npc_redneck_3":1,"npc_redneck_4":1,"npc_redneck_5":1,"npc_redneck_6":1,"npc_guard_2":1,"npc_citizen_1":1,"npc_citizen_2":1,"npc_citizen_3":1,"npc_citizen_4":1,"npc_citizen_5":1,"npc_citizen_6":1,"npc_guard_torch":1,"npc_citizen_woman_1":1,"npc_citizen_woman_2":2,"npc_guard_6":1,"npc_guard_7":1,"npc_guard_8":1,"npc_alice":1,"npc_satyr":1,"npc_lilya":1,"npc_ghost":1,"npc_hunchback":1,"decor_tree_apple_2_red":4,"decor_tree_apple_3_green":4,"decor_tree_apple_1_green":6,"tree_apple_1_3_1":3,"tree_apple_1_3_3":2,"tree_apple_1_3_stump":3,"tree_apple_1_3_0":2,"fence_wood_anc_f_2":1,"garden_empty":1,"npc_light_keeper":1,"telescope":1,"tree_2_6_stump":1,"tree_3_3_squirrel":1,"village_lake_fishing_spot":1,"portal_marble":1,"marble_heap_mid_1":1,"marble_heap_mid_2":1,"marble_stand_inactive":1,"decor_tree_apple_3_flower":1,"steep_coal":2,"mf_stone_pile_1":1,"sea_fishing_spot":1,"witch_pylon":1,"mf_chocks_1_decor":1,"scarecrow":1,"river_fishing_spot":1,"idle_points_stock":3,"mining_hut":1,"steep_end_blue_L_obstruction":1,"steep_yellow_blockage":2,"steep_yellow_blockage_inactive":1,"steep_yellow_blockage_R_o":1,"tree_3_2_bees_done":2,"tree_3_3_bees_done":1,"tree_old_3_stump":1,"steep_marble":1,"steep_iron":1,"steep_stone":1,"mine_zombie":1,"barrel01_broken":5,"barrel03_broken":3,"tree_big_sawmill":1,"zombie_sawmill_unfinished_placer":1,"waterfall_fishing_spot":1,"bed":2,"cupboard":1,"oven":1,"hatch":2,"cooking_table":1,"cooking_stand":1,"chest":2,"stranger":1,"storage_builddesk":1,"box_pallet":1,"cashbox":1,"working_table":1,"empty":3,"dungeon_stairs":3,"barrel02_broken":6,"blockage_H_low":1,"mf_preparation_1":1,"mf_alchemy_survey":1,"church_candle":4,"bookcase_F_damaged":2,"bookcase_F_broken":2,"morgue_throw_out_broken":1,"d_obj_spiral_stair_stn_out_1":1,"flour_bag":3,"blockage_V_low":1,"dungeon_enter":1,"blockage_V_high":1,"blockage_H_high":2,"dungeon_obj_rack02":1,"dungeon_obj_vase02":3,"dungeon_obj_bench01_broken":1,"dungeon_grille_closed":2,"dungeon_grille_opened":2,"dungeon_obj_vase01_broken":3,"dungeon_obj_vase03":1,"dungeon_obj_vase01":1,"dungeon_obj_vase04":2,"dungeon_obj_table02":1,"dungeon_obj_chair_02_broken":1,"table_cultist_quest":1,"floor_grid_fire":1,"wall_cellar_1tile":1,"bookcase_F_broken_custom_zombie":1,"alchemy_builddesk":1,"sacrifice_builddesk":1,"zombie_in_mortuary":1,"church_pulpit":1,"church_bench":2,"church_altar":1,"donat_box_inside":1,"tavern_door03":2,"tavern_table":5,"bed_no_sleep_1":1,"obj_church_bookcase":1,"dungeon_obj_table05":1,"bed_no_sleep_2":1,"dungeon_obj_table01":1,"tavern_writers_table":1,"tavern_cupboard":1,"mining_hut_bed":1,"nameplate_1":1,"npc_bishop":1,"npc_actress":1,"npc_merchant":1,"npc_cultist":1,"npc_inquisitor":1,"npc_astrologer":1,"crafting_skull_3":1,"mf_chocks_1":1}
16 | ```
17 |
18 | With this data (because all of them contain a location) it should be possible to recreate your current world map if you extract the base map (the underground) + the sprites of the id's.
19 |
20 | You can also generate this list by yourself, if you export as JSON and then select the `.html` export. If you open that in the browser, the code to generate that list is shown in the console.
21 |
22 | Achievements
23 | ------------
24 |
25 | The number progress of achievements is stored in `savedata->achievements->v`
26 | If you f.e. started to catch 200 fish, you could find the entry with "fish_200" in `_completed->v` and at the same index in `_completed_n->v` you can set it f.e. to 199 and then fish 1 more fish to get the achievement.
27 | Same is possible with other achievements.
28 |
29 | Workers
30 | -------
31 |
32 | References to your worker zombies are stored in `savedata->workers`. They are still saved within the wgo object.
33 |
34 |
35 | They have inventories and sub inventories. F.e. they have an inventory which contains `portable_backpack` - this backpack can contain other items. (Which Porter Workers are currently transporting)
36 |
37 |
38 | It is also possible to get information about the workers you have within a save. If you want to know for example what the efficiencies of your workers are, you can use the following code. (In the provided HTML export, because that contains the utility function used and the correct variables):
39 |
40 | ```JavaScript
41 | // Iterate all the objects in the world
42 | wgo.forEach((entry) => {
43 | // Save the ID of our object to a name for easier access
44 | const name = entry.v.obj_id.v;
45 |
46 | // If the ID is a worker zombie, we print out the object and the efficiency, and all used items of the worker
47 | if(name === "worker_zombie_1")
48 | {
49 | console.log(entry)
50 | // getValueForParamKey is one of the provided utility functions in a HTML exported save
51 | console.log(getValueForParamKey(entry.v["-1126421579"].v._params, "working_k"))
52 | // For every inventory item object, we map the object to the actual item ID
53 | console.log(entry.v["-1126421579"].v.inventory.v.map(item => item.v.id.v))
54 | }
55 | });
56 | ```
57 |
58 | Unlocked Technologies
59 | ---------------------
60 |
61 | In `savedata->unlocked_techs` are your unlocked technologies (obviously), but the editor doesn't feature anything there because you can simply give you red, green and blue points to get them.
62 |
63 |
64 | ___________________________________________________________
65 |
66 | If someone else wants to extend this information about additional data in the save, feel free to do so.
--------------------------------------------------------------------------------
/data/html/no settings.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Graveyard Keeper Savefile Editor | Welcome
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
Hey, seems like you are using this Application for the first time, meaning you now have the chance to set
25 | some options. If the application automatically set a save folder please also check it to be sure that it is correct.
54 | Graveyard Keeper WIKI for having such great resources which I could use to check my code and
55 | already extracting the game icons + naming them, so I didn't have to try to extract the sprite atlas
56 | information (at least for earlier versions of the editor).
57 |
58 |
59 | Eel for making it easy for me to create a HTML GUI in a python script.
60 |
61 |
62 | JQuery and
63 | Materialize for making it easier to create the HTML content.
64 |
65 |
66 |
67 |
68 |
Notes:
69 |
70 |
71 |
72 |
73 | If you have a save file which could help (for example a more up to date body of a perfect worker or the entire tech tree with all DLCs),
74 | feel free to upload your save to an issue. I will take the data from that save then to improve
75 | the several features of this save editor.
76 |
77 |
78 | Every loaded save file is kept in memory, meaning you can expect almost up to 200 MB per save file which you load.
79 |
80 |
81 | No sprites / images used, were created by me, they are from
82 | Lazy Bear Games
83 |
84 |
85 | Additionally the localization file is extracted from
86 | Lazy Bear Games
87 |
88 |
89 | You are able change all the values in the editor, but especially values like items can't be perfectly recreated and there
90 | might be bugs with them if you add new items (Because I can't reproduce the specific item types which
91 | may contain specific values, the script uses the last item in your inventory, duplicates it and replace
92 | it's item ID, amount and durability). Additionally like the Wiki mentions in the article about save
93 | file editing, it might be that some of your changes don't work because you didn't fulfill previous
94 | requirements.
95 |
96 |
97 | If you change the energy or your hp to a value above 100, your maximal hp / energy is set to the same value, if you want
98 | to reset it again, just set your hp or energy again to 100.
99 |
100 |
101 | If you find bugs or have feature requests (like an idea to add a functionality) feel free to report them on the
102 | GitHub Page.
103 |