├── screenshot.png
├── .gitignore
├── README.md
└── holocure_save_tool.py
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aclich/Holocure_save_editor/HEAD/screenshot.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | venv/
3 | release/
4 | __pycache__/
5 | *.dat
6 | *.json
7 | *.ini
8 | *.zip
9 | *.ipynb
10 | *.ico
11 | dist/
12 | build/
13 | *.spec
14 | test/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Holocure Save Tool
2 | A simple save Tool for Holocure fan game.
3 | Download (exe): https://github.com/aclich/Holocure_save_editor/releases/download/v0.0.3-C/HoloCure_Save_Tool_v0.0.3_C.zip
4 | - Support game version 0.4.1663293877
5 | - Add scroll bar in editor. (issue https://github.com/aclich/Holocure_save_editor/issues/6#issue-1376130532)
6 | - Fix Issue https://github.com/aclich/Holocure_save_editor/issues/8
7 |
8 |
9 | ### Known Issue:
10 | - Not compatible with older game version of save.
11 |
12 | ### v0.0.3 Expected Update:
13 | - Basic compatible with previous save version. 🚀
14 | - Optimize UI ⏸️
15 |
16 | #### Virus detected issue
17 | The released exe is packed by pyinstaller, and it will cause **false positives** in some anti-virus software. Please don't worry if you downloaded or used it. To see my full explanation, please refer to https://github.com/aclich/Holocure_save_editor/issues/4#issuecomment-1174153928.
18 |
19 | ## Feature:
20 | #### Save editing:
21 | - Holocoin
22 | - Shop items
23 | - Charactor gacha level
24 | - Tears
25 | - Lock, Unlock items/weapons/collabs/outfits/stages
26 |
27 | #### Save Inheritance (Transfer):
28 | - Help player Inherit save from other PC to current PC.
29 |
30 | #### Screenshot:
31 |
32 |
33 | ## How to Use (exe)
34 | 0. Select which tool to use `(editor or Inheritance tool)`
35 | - Editor:
36 | 1. Select the save file (Default path: 'C:\\Users\\\\AppData\\Local\\HoloCure\\save.dat')
37 | 2. Change the data in editor
38 | 3. Save the data and replace the original save file (please manually backup the original save file)
39 | - Inheritance Tool:
40 | 1. Select save from other PC (Or the save you want to inheritance)
41 | 2. Select save in current PC (Already selected in default, make sure the save file exists. If not, run the game first.)
42 | 3. Click Run.
43 |
44 | ## Run from source code
45 | ### Requirements
46 | - python > 3.6
47 | ### Steps
48 | 0. Install python if haven't (version > 3.6)
49 | 1. git clone https://github.com/aclich/Holocure_save_editor.git
50 | 2. cd Holocure_save_editor
51 | 3. python holocure_save_editor.py
52 |
53 | - - -
54 |
55 | ***Note**
56 | Build Command:
57 | >```
58 | >pyinstaller.exe --upx-dir --noconsole --onefile --name HoloCure_Save_Tool_{VERSION} --icon .\holocure_save_tool.py --clean
59 | >```
--------------------------------------------------------------------------------
/holocure_save_tool.py:
--------------------------------------------------------------------------------
1 | VERSION = '0.0.3-C'
2 | ABOUT_MSG=f'''HoloCure bad bad Save Tool
3 | Version: {VERSION}
4 | Date: 2022/09/18
5 | Author: Aclich
6 | Require: python > 3.6, tkinter
7 | Source code: https://github.com/aclich/Holocure_save_editor
8 |
9 | Change Log:
10 | - Update for new save content🤯.
11 |
12 | Tested Game version
13 | 0.4.1663293877
14 | 0.4.1662728581
15 |
16 | Known issue:
17 | Not compatible with older version of save file.
18 | '''
19 |
20 |
21 | import json, base64, os
22 | import tkinter as tk
23 | from tkinter import CENTER, DISABLED, messagebox
24 | from tkinter.filedialog import askopenfilename, asksaveasfilename
25 | from typing import List, Tuple
26 |
27 | LVL_KEYS = [] #['characters', 'tears']
28 | NUMB_KEYS = []
29 | CHK_KEYS = ['specUnlock', 'refund', 'challenge', 'GROff', 'growth']
30 | UNKNOW_KEYS = ['unlockedCharacters', 'eliminate', 'completedStages']
31 | LIST_MAP = {'unlockedItems': ['BodyPillow', 'FullMeal', 'PikiPikiPiman', 'SuccubusHorn', 'Headphones', 'UberSheep',
32 | 'HolyMilk', 'Sake', 'FaceMask', 'CreditCard', 'GorillasPaw', 'InjectionAsacoco',
33 | 'IdolCostume', 'Plushie', 'StudyGlasses', 'SuperChattoTime', 'EnergyDrink', 'Halu',
34 | 'Membership', 'GWSPill', 'ChickensFeather', 'Bandaid', 'Limiter', 'PiggyBank'],
35 | 'unlockedWeapons': ['PsychoAxe', 'Glowstick', 'SpiderCooking', 'Tailplug', 'BLBook', 'EliteLava',
36 | 'HoloBomb', 'HoloLaser', 'CuttingBoard', 'IdolSong', 'CEOTears', 'WamyWater',
37 | 'XPotato'],
38 | 'seenCollabs': ['BreatheInAsacoco', 'DragonBeam', 'EliteCooking', 'FlatBoard',
39 | 'MiComet', 'BLLover', 'LightBeam', 'IdolConcert', 'StreamOfTears',
40 | 'MariLamy', 'BrokenDreams', 'RapDog'],
41 | 'unlockedStages': ['STAGE 1', 'STAGE 2', 'STAGE 1 (HARD)'],
42 | 'unlockedOutfits': ['default', 'ameAlt1', 'kiaraAlt1', 'ameAlt1', 'inaAlt1', 'guraAlt1', 'calliAlt1',
43 | 'kiaraAlt1', 'irysAlt1', 'baeAlt1', 'sanaAlt1', 'faunaAlt1', 'mumeiAlt1', 'kroniiAlt1',
44 | 'kurokami']
45 | }
46 | ORIG_LIST_MAP, ORIG_CHK_KEYS = LIST_MAP, CHK_KEYS
47 | GEOMETRY='+600+200'
48 | InitPath = os.path.join(os.environ['LOCALAPPDATA'], 'HoloCure')
49 | AskSavePath = lambda init_path=InitPath: askopenfilename(title='select save data',
50 | initialdir=init_path,
51 | filetypes=[['.dat save', '*.dat']])
52 |
53 | def PopError(func):
54 | def wrap(self: tk.Toplevel, *args, **kwargs):
55 | _return = None
56 | try:
57 | _return = func(*args, **kwargs)
58 | except Exception as e:
59 | messagebox.showerror(title=e.__class__.__name__, message=f'{e}')
60 | self.deiconify()
61 | return _return
62 | return wrap
63 |
64 | class SaveEditor(object):
65 | def __init__(self) -> None:
66 | pass
67 |
68 | def _update_keys(self) -> None:
69 | global NUMB_KEYS, LVL_KEYS, LIST_MAP, UNKNOW_KEYS
70 | NUMB_KEYS, LVL_KEYS, LIST_MAP, CHK_KEYS = [], [], ORIG_LIST_MAP, ORIG_CHK_KEYS
71 | for k, v in self.save_js.items():
72 | if isinstance(v, list) and len(v) > 0 and all((isinstance(l, list) and isinstance(l[1], (float, int)) for l in v)):
73 | LVL_KEYS += [k]
74 | elif k not in (*LIST_MAP.keys(), *CHK_KEYS, *UNKNOW_KEYS, *LVL_KEYS) and isinstance(v, (float, int)):
75 | NUMB_KEYS += [k]
76 | elif k not in (*LVL_KEYS, *LIST_MAP) and isinstance(v, list) and len(v) and isinstance(v[0], str):
77 | LIST_MAP.update({k:v})
78 | else:
79 | UNKNOW_KEYS += [k]
80 |
81 | for rm_key in [k for k in LIST_MAP if k not in self.save_js]:
82 | _ = LIST_MAP.pop(rm_key, None)
83 |
84 | for rm_key in [k for k in CHK_KEYS if k not in self.save_js]:
85 | _ = CHK_KEYS.remove(rm_key)
86 |
87 | def load_file(self, file_path: str) -> Tuple[str, int, dict]:
88 | self._decrypt_str = ''.join([chr(b) for b in base64.b64decode(open(file_path, 'rb').read())])
89 | self._trunc_point = self._decrypt_str.find('{"') if self._decrypt_str.find('{ "') == -1 else self._decrypt_str.find('{ "')
90 | self.save_js = json.loads(self._decrypt_str[self._trunc_point:self._decrypt_str.rfind('}')+1])
91 | self._update_keys()
92 | return self._decrypt_str, self._trunc_point, self.save_js
93 |
94 | def save_file(self, file_path: str):
95 | out_str = f"{self._decrypt_str[:self._trunc_point]}{json.dumps(self.save_js)}"
96 | open(file_path, 'wb+').write(base64.b64encode(bytes((ord(s) for s in out_str))))
97 |
98 | def inerit_save(self, orig_path: str, curr_path: str):
99 | _, _, orig_save_js = self.load_file(orig_path)
100 | self._decrypt_str, self._trunc_point, _ = self.load_file(curr_path)
101 | self.save_js = orig_save_js
102 | self.save_file(curr_path)
103 |
104 | class mainApp(tk.Tk):
105 | def __init__(self):
106 | super().__init__()
107 | self.title(f'HoloCure Save Tool {VERSION}')
108 | self.resizable(0, 0)
109 | self.geometry(GEOMETRY)
110 | self._create_component()
111 | self._layout()
112 |
113 | def _create_component(self):
114 | self.editor_btn = tk.Button(self, text="Save Editor", command=lambda: editorPage(self))
115 | self.inherit_btn = tk.Button(self, text="Save Inheritance", command=lambda: saveInheritPage(self))
116 | self.about_btn = tk.Button(self, text='About', command=lambda: aboutpop(self))
117 |
118 | def _layout(self):
119 | for btn in (self.editor_btn, self.inherit_btn, self.about_btn):
120 | btn.pack(padx=120, pady=(10, 10))
121 |
122 | class editorPage(tk.Toplevel):
123 | def __init__(self, mainapp:mainApp, **kwargs):
124 | super().__init__(mainapp, **kwargs)
125 | self.title('Holocure Save Editor')
126 | self.editor = SaveEditor()
127 | self._open_save(self)
128 | if self.file_path == '':
129 | self.destroy()
130 | return
131 | self.geometry(f"690x750{GEOMETRY}")
132 | self.resizable(0, 100)
133 |
134 | ### Mousewheel Events
135 | def _bind_mouse_wheel(self, event):
136 | self.bind_all("", self._on_mouse_wheel)
137 |
138 | def _unbind_mouse_wheel(self, event):
139 | self.canvas.unbind_all('')
140 |
141 | def _on_mouse_wheel(self, event):
142 | self.canvas.yview_scroll(-1 * int((event.delta / 120)), "units")
143 | ### End of Mousewheel Events
144 |
145 | def _create_component(self):
146 | ###Create Scrollable Frame
147 | self.ground_frame = tk.Frame(self)
148 | self.ground_frame.pack(fill=tk.BOTH, expand=1)
149 |
150 | self.canvas = tk.Canvas(self.ground_frame)
151 | self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=1)
152 |
153 | self.vertical_bar = tk.Scrollbar(self.ground_frame, orient=tk.VERTICAL, command=self.canvas.yview)
154 | self.vertical_bar.pack(side=tk.RIGHT, fill=tk.Y)
155 |
156 | self.canvas.configure(yscrollcommand=self.vertical_bar.set)
157 | self.canvas.bind('', lambda e: self.canvas.configure(scrollregion=self.canvas.bbox('all')))
158 |
159 | self.scroll_frame = tk.Frame(self.canvas)
160 | self.canvas.create_window((0,0), window=self.scroll_frame, anchor='nw')
161 |
162 | self.canvas.bind('', self._bind_mouse_wheel)
163 | self.canvas.bind('', self._unbind_mouse_wheel)
164 | ###End of Create Scrollable Frame
165 |
166 | self.mskframe = miskFrame(self)
167 | self.chkframes = [unlockFrame(self, key_name=k) for k in LIST_MAP.keys()]
168 | self.LevelFrames = [LevelFrame(self, key_name=k) for k in LVL_KEYS]
169 |
170 | self.open_btn = tk.Button(self.scroll_frame, text='Open', justify=CENTER, command=lambda: self._open_save(self))
171 | self.save_btn = tk.Button(self.scroll_frame, text='Save', justify=CENTER, command=lambda: self._save_as(self))
172 |
173 | def _layout(self):
174 | t_col, _row = 2, 0
175 | self.mskframe.grid(column=0, columnspan=t_col, row=_row, sticky='news', pady=(0,10))
176 | for _row, frame in enumerate(self.chkframes, 1):
177 | frame.grid(column=0, columnspan=t_col, row=_row, sticky='news', pady=(0,5))
178 | for _row, frame in enumerate(self.LevelFrames, _row+1):
179 | frame.grid(column=0, columnspan=t_col, row=_row, sticky='news', pady=(0,10))
180 | self.open_btn.grid(column=0, row=_row+1)
181 | self.save_btn.grid(column=1, row=_row+1, pady=10)
182 | self.withdraw()
183 | self.deiconify()
184 |
185 | @PopError
186 | def _open_save(self):
187 | self.file_path = AskSavePath()
188 | if self.file_path == '':
189 | return
190 | self.editor.load_file(self.file_path)
191 | if hasattr(self, 'ground_frame'):
192 | self.ground_frame.destroy()
193 | self._create_component()
194 | self._layout()
195 |
196 | @PopError
197 | def _save_as(self):
198 | save_file_path = asksaveasfilename(defaultextension='.dat',
199 | initialdir=os.path.dirname(os.path.abspath(self.file_path)),
200 | initialfile='save.dat',
201 | filetypes=[['.dat save', '*.dat']])
202 | #misk data
203 | self.editor.save_js.update({key: float(var.get()) for key, var in self.mskframe.var_map.items()})
204 |
205 | #unlocks data
206 | self.editor.save_js.update({frame.key_name: [key for key, val in frame.check_map.items() if val.get()]
207 | for frame in self.chkframes})
208 |
209 | #Levels data
210 | self.editor.save_js.update({frame.key_name: [[chr, float(lv.get())] for chr, lv in frame.chr_var.items()]
211 | for frame in self.LevelFrames})
212 |
213 | self.editor.save_file(save_file_path)
214 | messagebox.showinfo(title='info', message=f"Saved!\n raw_data:\n{self.editor.save_js}")
215 |
216 | class miskFrame(tk.Frame):
217 | def __init__(self, parent: editorPage, **kwargs):
218 | super().__init__(parent.scroll_frame, **kwargs)
219 | self.parent = parent
220 | self.vcmd = (self.register(self._valid_num), '%P')
221 | self._create_component()
222 | self._layout()
223 |
224 | def _create_component(self):
225 | self.title_label = tk.Label(self, text='HoloCure Save Editor', justify=CENTER, bg='#36C6FF')
226 |
227 | self.var_map = {k: tk.IntVar(value=self.parent.editor.save_js[k])
228 | for k in [*NUMB_KEYS, *CHK_KEYS]}
229 | self.lb_map = {k: tk.Label(self, text=f'{k}: ') for k in NUMB_KEYS}
230 | self.en_map = {k: tk.Entry(self, textvariable=self.var_map[k],
231 | validatecommand=self.vcmd, width=5) for k in NUMB_KEYS}
232 | self.ck_map = {k: tk.Checkbutton(self, text=f'{k}',
233 | variable=self.var_map[k], onvalue=1.0, offvalue=0.0) for k in CHK_KEYS}
234 |
235 | def _layout(self):
236 | col = row = 0
237 | self.title_label.grid(column=col, row=row, columnspan=100, sticky='nwes')
238 | row += 1
239 | for i, key in zip(range(0,len(self.lb_map)*2, 2), self.lb_map):
240 | col, _row = i%10, i//10+row
241 | self.lb_map[key].grid(column=col, row=_row, sticky='e', pady=5)
242 | self.en_map[key].grid(column=col+1, row=_row, sticky='w', padx=(0, 10), pady=5)
243 | col, row = 0, _row+1
244 | for i, key in enumerate(self.ck_map):
245 | self.ck_map[key].grid(column=i%6, row=row+i//6, columnspan=1, sticky='w', ipady=10)
246 |
247 | def _valid_num(self, P):
248 | if str.isdigit(P):
249 | return True
250 | return False
251 |
252 | class unlockFrame(tk.Frame):
253 | def __init__(self, parent: editorPage, key_name:str, **kwargs):
254 | super().__init__(parent.scroll_frame, **kwargs)
255 | self.key_name = key_name
256 | self.parent = parent
257 | self.items_name = LIST_MAP[key_name]
258 | self._create_component()
259 | self._layout()
260 |
261 | def _create_component(self):
262 | self.sub_lb = tk.Label(self, text=f"{self.key_name}:")
263 | self.check_map = {k:tk.IntVar(value=1 if k in self.parent.editor.save_js[self.key_name] else 0)
264 | for k in self.items_name}
265 | self.ck_map = {k:tk.Checkbutton(self, text=k, variable=self.check_map[k], onvalue=1, offvalue=0) for k in self.items_name}
266 |
267 | def _layout(self):
268 | col = row = 0
269 | self.sub_lb.grid(column=col,columnspan=3 ,row=row, sticky="nsw")
270 | row += 1
271 | for i, check_box in enumerate(self.ck_map.values()):
272 | check_box.grid(column=i%6, row=row+i//6, sticky='w', pady=(0,3), padx=(0, 5))
273 |
274 | class LevelFrame(tk.Frame):
275 | def __init__(self, parent:editorPage, key_name:str, **kwargs):
276 | super().__init__(parent.scroll_frame, **kwargs)
277 | self.parent, self.key_name = parent, key_name
278 | self.chr_list: list = self.parent.editor.save_js[key_name]
279 | self._create_component()
280 | self._layout()
281 |
282 | def _create_component(self):
283 | self.sub_lb = tk.Label(self, text=self.key_name)
284 | self.chr_var = {k: tk.IntVar(value=lv) for k, lv in self.chr_list}
285 | self.chr_lb = {k:tk.Label(self, text=f"{k}: ") for k, _ in self.chr_list}
286 | self.chr_ent = {k: tk.Entry(self, width=5,
287 | textvariable=self.chr_var[k]) for k, _ in self.chr_list}
288 |
289 | def _layout(self):
290 | row = col = 0
291 | self.sub_lb.grid(column=col, columnspan=3, row=row, sticky='w')
292 | row += 1
293 | for i, chr in zip(range(0,len(self.chr_lb)*2,2), self.chr_lb):
294 | col, _row = i%14, i//14+row
295 | self.chr_lb[chr].grid(column=col, row=_row, sticky='w', pady=(0,5))
296 | self.chr_ent[chr].grid(column=col+1, row=_row, sticky='e', pady=(0,5), padx=(0,5))
297 |
298 | class saveInheritPage(tk.Toplevel):
299 | def __init__(self, mainapp:mainApp, **kwargs):
300 | super().__init__(mainapp, **kwargs)
301 | self.title('Save Inheritace')
302 | self.geometry(GEOMETRY)
303 | self.resizable(0, 0)
304 | self.editor = SaveEditor()
305 | self._create_component()
306 | self._layout()
307 |
308 | def _create_component(self):
309 | self.var_map = {k: tk.StringVar(value=v)
310 | for k, v in {'orig': '',
311 | 'curr': os.path.join(InitPath, 'save.dat').replace('\\', '/')
312 | }.items()
313 | }
314 | self.lb_map = {k: tk.Label(self, textvariable=self.var_map[k]) for k in {'orig', 'curr'}}
315 | self.open_orig_btn = tk.Button(self, text='Select Save From Other PC',
316 | command=lambda: self._select_save('orig'), width=25)
317 | self.open_curr_btn = tk.Button(self, text='Select Save In This PC',
318 | command=lambda: self._select_save('curr'), width=25)
319 | self.inherit_btn = tk.Button(self, text='Run', command=lambda: self._run_inherit(self))
320 | self.btn_map = {
321 | 'orig': self.open_orig_btn,
322 | 'curr': self.open_curr_btn
323 | }
324 |
325 | def _layout(self):
326 | for i, k in enumerate(('orig','curr')):
327 | self.btn_map[k].grid(column=0, row=i, sticky='nsw', pady=(10,0), padx=(10,0))
328 | self.lb_map[k].grid(column=1, row=i, sticky='nsw', pady=(10,0), padx=(5,10))
329 | self.inherit_btn.grid(column=0, row=2, sticky='nsw', pady=(10,10), padx=10)
330 |
331 | def _select_save(self, key: str):
332 | _file_path = AskSavePath(self.var_map[key].get()) if self.var_map[key].get() else AskSavePath()
333 | self.var_map[key].set(_file_path)
334 | self.deiconify()
335 |
336 | @PopError
337 | def _run_inherit(self):
338 | _orig_save = self.var_map['orig'].get()
339 | _curr_save = self.var_map['curr'].get()
340 | self.editor.inerit_save(_orig_save, _curr_save)
341 | messagebox.showinfo(title='Inheritence success', message=f'Success!\nRaw data:\n{self.editor.save_js}')
342 |
343 | class aboutpop(tk.Toplevel):
344 | def __init__(self, mainapp, **kwargs):
345 | super().__init__(mainapp, **kwargs)
346 | self.title('About')
347 | self.geometry(GEOMETRY)
348 | self.resizable(0, 0)
349 | self._create_component()
350 | self._layout()
351 |
352 | def _create_component(self):
353 | self.txt = tk.Text(self, wrap='word', height=len(ABOUT_MSG.split('\n')), bg=mainapp.cget('bg'), font=('Arial', 10))
354 | self.txt.insert('end', ABOUT_MSG)
355 | self.txt.config(state=DISABLED)
356 | self.ok_btn = tk.Button(self, text='OK', command=lambda: self.destroy())
357 |
358 | def _layout(self):
359 | self.txt.pack()
360 | self.ok_btn.pack(anchor=CENTER)
361 |
362 | if __name__ == "__main__":
363 | mainapp = mainApp()
364 | mainapp.mainloop()
--------------------------------------------------------------------------------