Unlike on consoles, where the save files for these games are usually plain, on PC they are now compressed with a custom method that uses the YKCMP_V1 header, also seen in other NIS games. On top of that, they are obfuscated with a XOR operation on the whole thing.
I've written a script that gets rid of both the obfuscation as well as decompressing the save file. It works for save files from both Disgaea PC as well as Disgaea 2 PC.
In version 2 I implemented a kind of pseudo-compression which does not actually compress the file but in fact makes it bigger, but it at least allows us to re-pack a previously decompressed save into a format that the games accept. So it's now possible to load modified save games.
https://pastebin.com/R1ZHA6W2
Savegames from Disgaea 1 / 2 for PC
-
- Posts: 27
- Joined: Wed Aug 13, 2014 6:43 pm
Savegames from Disgaea 1 / 2 for PC
Last edited by HenryEx on Wed Aug 09, 2017 5:29 pm, edited 1 time in total.
-
- Posts: 27
- Joined: Wed Aug 13, 2014 6:43 pm
Re: Savegames from Disgaea 1 / 2 for PC
Script V1:
Code: Select all
# Disgaea PC / Disgaea 2 PC save game decompression
# De-XORs and decompresses save files from these games.
# Leaves headers intact, so actual save data starts at offset 0x44.
#
# Written by HenryEx
# version 1
#
# script for QuickBMS http://quickbms.aluigi.org
################################################
# set up virtual memory file for save data
print "Preparing..."
get FILENAME filename 0
get FILESIZE asize 0
log MEMORY_FILE 0 FILESIZE 0 # MEMORY_FILE is the working copy for decryption
log MEMORY_FILE2 0 0 # MEMORY_FILE2 will be the target for decompression
putvarchr MEMORY_FILE2 0x100000 0
log MEMORY_FILE2 0 0 # reset MF2
################################################
# Decrypt save
print "Starting Decryption..."
getDString HEADSTART 0x20
get XORKEY long
get XORUNK1 short
get XORUNK2 short
get XORCHUNKS long # num of XOR'd integers in file
get FSIZE_COMP_A long # savedata size with YKCMP header minus padding (for XOR ints)
SavePos FSTART
print "XORing Memory File..."
# XOR save file with key
for i = 0 < XORCHUNKS
xmath POS "(i * 4) + FSTART" # get current file position
getvarchr DATA MEMORY_FILE POS long
math DATA u^ XORKEY
putvarchr MEMORY_FILE POS DATA long
next i
################################################
# Decompress save
print "Start Decompression..."
goto FSTART MEMORY_FILE
idstring MEMORY_FILE "YKCMP_V1"
get ARCHIVE_VERSION long MEMORY_FILE
if ARCHIVE_VERSION != 4
print "[!] Unexpected archive version: %ARCHIVE_VERSION%! Exiting..."
CleanExit
endif
get FSIZE_COMP_B long MEMORY_FILE # comp. filesize without 0x30 decryption header, minus padding
get FSIZE_TARGET long MEMORY_FILE # target filesize without all headers after decomp.
SavePos FSTART
# set up decompression and prepare MF2
print "Setting up Memory File 2..."
putDString HEADSTART 0x20 MEMORY_FILE2
put XORKEY long MEMORY_FILE2
put XORUNK1 short MEMORY_FILE2
put XORUNK2 short MEMORY_FILE2
put XORCHUNKS long MEMORY_FILE2
put FSIZE_COMP_A long MEMORY_FILE2
putct "YKCMP_V1" string -1 MEMORY_FILE2
put ARCHIVE_VERSION long MEMORY_FILE2
put FSIZE_COMP_B long MEMORY_FILE2
put FSIZE_TARGET long MEMORY_FILE2
set FBYTES long FSIZE_COMP_B
math FBYTES + 0x30 # num. of compressed bytes + XOR & YKCMP header
math POS = FSTART # offset after the YKCMP_V1 header (save data start) for MF2
# start decompressing into MF2
print "Decompressing save data..."
for i = 68 < FBYTES # i works as byte offset, start at offset 0x44
goto i MEMORY_FILE
get A_BYTE byte MEMORY_FILE
if A_BYTE >= 0xE0 # read data like XX XY YY
get B_BYTE byte MEMORY_FILE
get C_BYTE byte MEMORY_FILE
set READLEN long A_BYTE
math READLEN & 0x1F # remove 0xE0 from X
math READLEN < 4
xmath READLEN "READLEN + (B_BYTE > 4)"
math READLEN + 3
set SEEKBACK long B_BYTE
math SEEKBACK & 0x0F
math SEEKBACK < 8
math SEEKBACK + C_BYTE
math SEEKBACK + 1
# print "Offset %i|h4%: byte %A_BYTE|2h% is >= 0xE0! Next bytes %B_BYTE|2h% %C_BYTE|2h%. Look back by %SEEKBACK% and copy %READLEN% bytes to %POS|6h%!"
math i + 2 # advance counter of processed bytes
elif A_BYTE >= 0xC0 # read data like XX YY
get B_BYTE byte MEMORY_FILE
math READLEN = A_BYTE
math READLEN & 0x3F # remove 0xC0 from X
math READLEN + 2
math SEEKBACK = B_BYTE
math SEEKBACK + 1
# print "Offset %i|h4%: byte %A_BYTE|2h% is >= 0xC0! Next byte %B_BYTE|2h%. Look back by %SEEKBACK% and copy %READLEN% bytes to %POS|6h%!"
math i + 1 # advance counter of processed bytes
elif A_BYTE >= 0x80 # read data like XY
math READLEN = A_BYTE
math READLEN > 4
math READLEN & 3 # remove 0x80 from X
math READLEN + 1
math SEEKBACK = A_BYTE
math SEEKBACK & 0x0F
math SEEKBACK + 1
# print "Offset %i|h4%: byte %A_BYTE|2h% is >= 0x80! Look back by %SEEKBACK% and copy %READLEN% bytes to %POS|6h%!"
else # byte is < 0x80, straight copy next bytes MF1 -> MF2
# print "Offset %i|h4%: byte %A_BYTE|2h% is < 0x80! Straight copy %A_BYTE% bytes to %POS|6h%!"
for j = 0 < A_BYTE
math i + 1
getvarchr DATA MEMORY_FILE1 i byte
put DATA byte MEMORY_FILE2
next j
endif
if A_BYTE >= 0x80 # Copy bytes within MF2 via lookback
math POS - SEEKBACK
for j = 0 < READLEN
getvarchr DATA MEMORY_FILE2 POS byte
put DATA byte MEMORY_FILE2
math POS + 1
next j
endif
get POS asize MEMORY_FILE2
next i
# check if filesize matches?
math ENDSIZE = POS
math POS - 68
if FSIZE_TARGET != POS
print "WARNING! Target filesize doesn't match real filesize!"
endif
################################################
# Save file to disk
string FILENAME P= "dec_%FILENAME%"
print "Exporting decompressed save to %FILENAME%"
log FILENAME 0 ENDSIZE MEMORY_FILE2
CleanExit
-
- Posts: 27
- Joined: Wed Aug 13, 2014 6:43 pm
Re: Savegames from Disgaea 1 / 2 for PC
Script V2, supporting re-packing saves:
Code: Select all
# Disgaea PC / Disgaea 2 PC save game decompression
# De-XORs and decompresses save files from these games.
# Leaves headers intact, so actual save data starts at offset 0x44.
#
# Saves decompressed with this script can be repacked as well with
# pseudo-compression to allow loading modified save files.
#
# Written by HenryEx
# version 2
#
# script for QuickBMS http://quickbms.aluigi.org
################################################
# set up virtual memory file for save data
print "Preparing..."
get FILENAME filename 0
get FILESIZE asize 0
if FILESIZE < 0x44
print "[!] Error: File too small! Exiting..."
CleanExit
endif
log MEMORY_FILE 0 0 # MEMORY_FILE is the working copy for de-/encryption
log MEMORY_FILE2 0 0 # MEMORY_FILE2 will be the target for de-/recompression
putvarchr MEMORY_FILE 0x100000 0
putvarchr MEMORY_FILE2 0x100000 0
log MEMORY_FILE 0 0 # reset MF1
log MEMORY_FILE2 0 0 # reset MF2
getDString HEADSTART 0x20
get XORKEY long
get XORUNK1 short
get XORUNK2 short
get XORCHUNKS long # num of XOR'd integers in file
get FSIZE_COMP_A long # savedata size with YKCMP header minus padding (for XOR ints)
SavePos FSTART 0
getDString MAGIC 8 0 # check for YKCMP_V1 string if already de-crypted / -compressed
if MAGIC = "YKCMP_V1"
################################################
# Pseudo re-compress save
print "File seems to be a decompressed save, will attempt to pseudo re-compress and encrypt it."
get ARCHIVE_VERSION long 0
if ARCHIVE_VERSION != 4
print "[!] Unexpected archive version: %ARCHIVE_VERSION%! Exiting..."
CleanExit
endif
get FSIZE_COMP_B long 0 # comp. filesize without 0x30 decryption header, minus padding
get FSIZE_TARGET long 0 # target filesize without all headers after decomp.
SavePos FSTART 0
# set up compression stuff and prepare MF2
print "Setting up file in memory..."
putct "YKCMP_V1" string -1 MEMORY_FILE2
put ARCHIVE_VERSION long MEMORY_FILE2
put FSIZE_COMP_B long MEMORY_FILE2 # place holder, needs to be updated after recompression
math FSIZE_TARGET = FILESIZE
math FSIZE_TARGET - 0x44
put FSIZE_TARGET long MEMORY_FILE2
set FBYTES long FSIZE_TARGET
math POS = FSTART # starting position to read bytes from, should be 0x44
# start pseudo compressing into MF2
print "Pseudo re-compressing save data..."
append # append mode ON
for FBYTES = FBYTES != 0 # loop while num of bytes to process is not 0
if FBYTES > 0x7F
set READLEN byte 0x7F
math FBYTES - 0x7F
else
set READLEN byte FBYTES
math FBYTES = 0
endif
put READLEN byte MEMORY_FILE2 # put byte length to straight copy
log MEMORY_FILE2 POS READLEN 0 # append [READLEN] bytes to MF2
math POS + READLEN # increment read offset
next
append # append mode OFF
get FSIZE_COMP_B asize MEMORY_FILE2
math FSIZE_COMP_A = FSIZE_COMP_B
putvarchr MEMORY_FILE2 0xC FSIZE_COMP_B long # update header value
################################################
# Encrypt save file in MF1
print "Setting up encryption..."
xmath PAD "4 - ( FSIZE_COMP_A % 4 )" # num of bytes for padding
if PAD > 0
for i = 0 < PAD
put 0 byte MEMORY_FILE2 # pad file with 0 for 32-bit alignment
next i
endif
xmath FSIZE "FSIZE_COMP_A + PAD"
xmath XORCHUNKS "FSIZE / 4"
# print "Padding needed: %PAD%. Padded filesize is %FSIZE%. Xor chunks: %XORCHUNKS%."
# Set up MF1
putDString HEADSTART 0x20 MEMORY_FILE
put XORKEY long MEMORY_FILE
put XORUNK1 short MEMORY_FILE
put XORUNK2 short MEMORY_FILE
put XORCHUNKS long MEMORY_FILE
put FSIZE_COMP_A long MEMORY_FILE
SavePos FSTART MEMORY_FILE
append # append mode ON
log MEMORY_FILE 0 FSIZE MEMORY_FILE2 # put recomp. save after encryption header
append # append mode OFF
print "Encrypting File..."
# XOR save file with key
for i = 0 < XORCHUNKS
xmath POS "(i * 4) + FSTART" # get current file position
getvarchr DATA MEMORY_FILE POS long
math DATA u^ XORKEY
putvarchr MEMORY_FILE POS DATA long
next i
get ENDSIZE asize MEMORY_FILE
################################################
# Save file to disk
string FILENAME $ "save" # last occurrence + searched string
print "Exporting re-compressed save to %FILENAME%"
log FILENAME 0 ENDSIZE MEMORY_FILE
CleanExit
else
################################################
# Decrypt save
print "Decrypting File..."
# XOR save file with key
log MEMORY_FILE 0 FILESIZE 0
for i = 0 < XORCHUNKS
xmath POS "(i * 4) + FSTART" # get current file position
getvarchr DATA MEMORY_FILE POS long
math DATA u^ XORKEY
putvarchr MEMORY_FILE POS DATA long
next i
goto FSTART MEMORY_FILE
getDString MAGIC 8 MEMORY_FILE
if MAGIC != "YKCMP_V1"
print "[!] Unexpected magic string: %MAGIC%! Decryption might have failed. Exiting..."
CleanExit
endif
################################################
# Decompress save
print "Begin Decompression..."
get ARCHIVE_VERSION long MEMORY_FILE
if ARCHIVE_VERSION != 4
print "[!] Unexpected archive version: %ARCHIVE_VERSION%! Exiting..."
CleanExit
endif
get FSIZE_COMP_B long MEMORY_FILE # comp. filesize without 0x30 decryption header, minus padding
get FSIZE_TARGET long MEMORY_FILE # target filesize without all headers after decomp.
SavePos FSTART 0
# set up decompression and prepare MF2
print "Setting up file in memory..."
putDString HEADSTART 0x20 MEMORY_FILE2
put XORKEY long MEMORY_FILE2
put XORUNK1 short MEMORY_FILE2
put XORUNK2 short MEMORY_FILE2
put XORCHUNKS long MEMORY_FILE2
put FSIZE_COMP_A long MEMORY_FILE2
putct "YKCMP_V1" string -1 MEMORY_FILE2
put ARCHIVE_VERSION long MEMORY_FILE2
put FSIZE_COMP_B long MEMORY_FILE2
put FSIZE_TARGET long MEMORY_FILE2
set FBYTES long FSIZE_COMP_B
math FBYTES + 0x30 # num. of compressed bytes + XOR & YKCMP header
math POS = FSTART # offset after the YKCMP_V1 header (save data start) for MF2
# start decompressing into MF2
print "Decompressing save data..."
for i = 68 < FBYTES # i works as byte offset, start at offset 0x44
goto i MEMORY_FILE
get A_BYTE byte MEMORY_FILE
if A_BYTE >= 0xE0 # read data like XX XY YY
get B_BYTE byte MEMORY_FILE
get C_BYTE byte MEMORY_FILE
set READLEN long A_BYTE
math READLEN & 0x1F # remove 0xE0 from X
math READLEN < 4
xmath READLEN "READLEN + (B_BYTE > 4)"
math READLEN + 3
set SEEKBACK long B_BYTE
math SEEKBACK & 0x0F
math SEEKBACK < 8
math SEEKBACK + C_BYTE
math SEEKBACK + 1
# print "Offset %i|h4%: byte %A_BYTE|2h% is >= 0xE0! Next bytes %B_BYTE|2h% %C_BYTE|2h%. Look back by %SEEKBACK% and copy %READLEN% bytes to %POS|6h%!"
math i + 2 # advance counter of processed bytes
elif A_BYTE >= 0xC0 # read data like XX YY
get B_BYTE byte MEMORY_FILE
math READLEN = A_BYTE
math READLEN & 0x3F # remove 0xC0 from X
math READLEN + 2
math SEEKBACK = B_BYTE
math SEEKBACK + 1
# print "Offset %i|h4%: byte %A_BYTE|2h% is >= 0xC0! Next byte %B_BYTE|2h%. Look back by %SEEKBACK% and copy %READLEN% bytes to %POS|6h%!"
math i + 1 # advance counter of processed bytes
elif A_BYTE >= 0x80 # read data like XY
math READLEN = A_BYTE
math READLEN > 4
math READLEN & 3 # remove 0x80 from X
math READLEN + 1
math SEEKBACK = A_BYTE
math SEEKBACK & 0x0F
math SEEKBACK + 1
# print "Offset %i|h4%: byte %A_BYTE|2h% is >= 0x80! Look back by %SEEKBACK% and copy %READLEN% bytes to %POS|6h%!"
else # byte is < 0x80, straight copy next bytes MF1 -> MF2
# print "Offset %i|h4%: byte %A_BYTE|2h% is < 0x80! Straight copy %A_BYTE% bytes to %POS|6h%!"
for j = 0 < A_BYTE
math i + 1
getvarchr DATA MEMORY_FILE1 i byte
put DATA byte MEMORY_FILE2
next j
endif
if A_BYTE >= 0x80 # Copy bytes within MF2 via lookback
math POS - SEEKBACK
for j = 0 < READLEN
getvarchr DATA MEMORY_FILE2 POS byte
put DATA byte MEMORY_FILE2
math POS + 1
next j
endif
get POS asize MEMORY_FILE2
next i
# check if filesize matches?
math ENDSIZE = POS
math POS - 68
if FSIZE_TARGET != POS
print "WARNING! Target filesize doesn't match real filesize!"
endif
################################################
# Save file to disk
string FILENAME P= "dec_%FILENAME%"
print "Exporting decompressed save to %FILENAME%"
log FILENAME 0 ENDSIZE MEMORY_FILE2
CleanExit
endif
-
- Site Admin
- Posts: 12984
- Joined: Wed Jul 30, 2014 9:32 pm
Re: Savegames from Disgaea 1 / 2 for PC
I have the impression that the FSIZE_COMP_B / FBYTES field includes 20 additional bytes (the size of the header), in fact the output is larger than the declared decompressed size.
If you use "FSIZE_COMP_B -= 20" you will get an output matching the expected size.
If you use "FSIZE_COMP_B -= 20" you will get an output matching the expected size.