Files
gdutils/GDUtils.py
T
2026-10-20 17:57:50 -07:00

358 lines
12 KiB
Python

import sys
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QWidget, QLabel, QPushButton, QFileDialog, QInputDialog, QMessageBox, QCheckBox
from PySide6.QtGui import QFont
from Crypto.Cipher import AES
import base64
import gzip
import zlib
import pyperclip
import hashlib
MAC_SAVE_KEY = (
b"\x69\x70\x75\x39\x54\x55\x76\x35\x34\x79\x76\x5d\x69\x73\x46\x4d"
b"\x68\x35\x40\x3b\x74\x2e\x35\x77\x33\x34\x45\x32\x52\x79\x40\x7b"
)
def xor(string: str, key: int) -> str:
return ("").join(chr(ord(char) ^ key) for char in string)
def remove_pad(data: bytes) -> bytes:
last = data[-1]
if last < 16:
data = data[:-last]
return data
def add_pad(data: bytes) -> bytes:
len_r = len(data) % 16
if len_r:
to_add = 16 - len_r
data += to_add.to_bytes(1, "little") * to_add
return data
def decrypt_mac_save_data(data: bytes) -> str:
cipher = AES.new(MAC_SAVE_KEY, AES.MODE_ECB)
return remove_pad(cipher.decrypt(data)).decode()
def encrypt_mac_save_data(data: str) -> bytes:
cipher = AES.new(MAC_SAVE_KEY, AES.MODE_ECB)
return cipher.encrypt(add_pad(data.encode()))
def decrypt_save_data(data: str) -> str:
base64_decoded = base64.urlsafe_b64decode(xor(data, key=11).encode())
decompressed = gzip.decompress(base64_decoded)
return decompressed.decode()
def encrypt_save_data(data: str) -> str:
gzipped = gzip.compress(data.encode())
base64_encoded = base64.urlsafe_b64encode(gzipped)
return xor(base64_encoded.decode(), key=11)
def encode_level(level_string: str, is_official_level: bool) -> str:
gzipped = gzip.compress(level_string.encode())
base64_encoded = base64.urlsafe_b64encode(gzipped)
if is_official_level:
base64_encoded = base64_encoded[13:]
return base64_encoded.decode()
def decode_level(level_data: str, is_official_level: bool) -> str:
if is_official_level:
level_data = 'H4sIAAAAAAAAA' + level_data
base64_decoded = base64.urlsafe_b64decode(level_data.encode())
decompressed = zlib.decompress(base64_decoded, 15 | 32)
return decompressed.decode()
def xor_cipher(input_str: str, key: str) -> str:
result = ""
for i in range(len(input_str)):
byte = ord(input_str[i])
x_key = ord(key[i % len(key)])
result += chr(byte ^ x_key)
return result
def encode_gjp(password: str) -> str:
encoded = xor_cipher(password, "37526")
encoded_base64 = base64.b64encode(encoded.encode()).decode()
encoded_base64 = encoded_base64.replace("+", "-")
encoded_base64 = encoded_base64.replace("/", "_")
return encoded_base64
def decode_gjp(gjp: str) -> str:
decoded_base64 = base64.b64decode(gjp.encode()).decode()
decoded = xor_cipher(decoded_base64, "37526")
return decoded
def encode_gjp2(password: str = "", salt: str = "mI29fmAnxgTs") -> str:
password += salt
hash = hashlib.sha1(password.encode()).hexdigest()
return hash
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("GDUtils")
titleLabel = QLabel("GDUtils")
font = QFont()
font.setPointSize(18)
font.setBold(True)
titleLabel.setFont(font)
saveFileLabel = QLabel("Save file")
self.saveFileDecode = QPushButton("Decode")
self.saveFileDecode.clicked.connect(self.on_save_decode_click)
self.saveFileEncode = QPushButton("Encode")
self.saveFileEncode.clicked.connect(self.on_save_encode_click)
self.saveFileIsMacos = QCheckBox("macOS save?")
saveFileButtons = QHBoxLayout()
saveFileButtons.addStretch()
saveFileButtons.addWidget(self.saveFileDecode, alignment=Qt.AlignmentFlag.AlignHCenter)
saveFileButtons.addWidget(self.saveFileEncode, alignment=Qt.AlignmentFlag.AlignHCenter)
saveFileButtons.addWidget(self.saveFileIsMacos, alignment=Qt.AlignmentFlag.AlignHCenter)
saveFileButtons.addStretch()
levelDataLabel = QLabel("Level data")
self.levelDataDecode = QPushButton("Decode")
self.levelDataDecode.clicked.connect(self.on_level_decode_click)
self.levelDataEncode = QPushButton("Encode")
self.levelDataEncode.clicked.connect(self.on_level_encode_click)
self.levelDataIsOfficial = QCheckBox("Official level?")
levelDataButtons = QHBoxLayout()
levelDataButtons.addStretch()
levelDataButtons.addWidget(self.levelDataDecode, alignment=Qt.AlignmentFlag.AlignHCenter)
levelDataButtons.addWidget(self.levelDataEncode, alignment=Qt.AlignmentFlag.AlignHCenter)
levelDataButtons.addWidget(self.levelDataIsOfficial, alignment=Qt.AlignmentFlag.AlignHCenter)
levelDataButtons.addStretch()
gdLvlGDShareLabel = QLabel("GDLVL/GDShare")
self.convertToGdLvl = QPushButton("Convert to GDLVL")
self.convertToGdLvl.clicked.connect(self.on_convert_from_gmd_click)
self.convertToGmd = QPushButton("Convert to GMD")
self.convertToGmd.clicked.connect(self.on_convert_from_gdlvl_click)
gdLvlGDShareButtons = QHBoxLayout()
gdLvlGDShareButtons.addStretch()
gdLvlGDShareButtons.addWidget(self.convertToGdLvl, alignment=Qt.AlignmentFlag.AlignHCenter)
gdLvlGDShareButtons.addWidget(self.convertToGmd, alignment=Qt.AlignmentFlag.AlignHCenter)
gdLvlGDShareButtons.addStretch()
gjpLabel = QLabel("GJP")
self.gjpDecode = QPushButton("Decode")
self.gjpDecode.clicked.connect(self.on_gjp_decode_click)
self.gjpEncode = QPushButton("Encode")
self.gjpEncode.clicked.connect(self.on_gjp_encode_click)
gjpButtons = QHBoxLayout()
gjpButtons.addStretch()
gjpButtons.addWidget(self.gjpDecode, alignment=Qt.AlignmentFlag.AlignHCenter)
gjpButtons.addWidget(self.gjpEncode, alignment=Qt.AlignmentFlag.AlignHCenter)
gjpButtons.addStretch()
gjp2Label = QLabel("GJP2")
self.gjp2Encode = QPushButton("Encode")
self.gjp2Encode.clicked.connect(self.on_gjp2_encode_click)
layout = QVBoxLayout()
layout.addWidget(titleLabel, alignment=Qt.AlignmentFlag.AlignHCenter)
layout.addWidget(saveFileLabel, alignment=Qt.AlignmentFlag.AlignHCenter)
layout.addLayout(saveFileButtons)
layout.addWidget(levelDataLabel, alignment=Qt.AlignmentFlag.AlignHCenter)
layout.addLayout(levelDataButtons)
layout.addWidget(gdLvlGDShareLabel, alignment=Qt.AlignmentFlag.AlignHCenter)
layout.addLayout(gdLvlGDShareButtons)
layout.addWidget(gjpLabel, alignment=Qt.AlignmentFlag.AlignHCenter)
layout.addLayout(gjpButtons)
layout.addWidget(gjp2Label, alignment=Qt.AlignmentFlag.AlignHCenter)
layout.addWidget(self.gjp2Encode, alignment=Qt.AlignmentFlag.AlignHCenter)
layout.addStretch()
container = QWidget()
container.setLayout(layout)
self.setCentralWidget(container)
def on_save_decode_click(self):
file_path, _ = QFileDialog.getOpenFileName(
self,
"Select a GD save file",
"",
"DAT Files (*.dat)"
)
if not file_path:
return
output_file_path, _ = QFileDialog.getSaveFileName(
self,
"Select a output location for the decoded save file",
file_path.removesuffix(".dat") + ".xml",
"XML Files (*.xml)"
)
if not output_file_path:
return
with open(file_path, "r") as f:
data = f.read()
with open(output_file_path, "w") as f:
if self.saveFileIsMacos.isChecked():
f.write(decrypt_mac_save_data(data))
else:
f.write(decrypt_save_data(data))
def on_save_encode_click(self):
file_path, _ = QFileDialog.getOpenFileName(
self,
"Select a decoded GD save file",
"",
"XML Files (*.xml)"
)
if not file_path:
return
output_file_path, _ = QFileDialog.getSaveFileName(
self,
"Select a output location for the encoded save file",
file_path.removesuffix(".xml") + ".dat",
"DAT Files (*.dat)"
)
if not output_file_path:
return
with open(file_path, "r") as f:
data = f.read()
if self.saveFileIsMacos.isChecked():
with open(output_file_path, "wb") as f:
f.write(encrypt_mac_save_data(data))
else:
with open(output_file_path, "w") as f:
f.write(encrypt_save_data(data))
def on_level_decode_click(self):
dialog = QInputDialog(self)
dialog.setWindowTitle("Level decoder")
dialog.setLabelText("Enter the level data to decode:")
ok = dialog.exec()
data = dialog.textValue()
if ok and data:
pyperclip.copy(decode_level(data, self.levelDataIsOfficial.isChecked()))
msg = QMessageBox()
msg.setWindowTitle("Level decoder")
msg.setText("The decoded level data has been copied to your clipboard")
msg.exec()
def on_level_encode_click(self):
dialog = QInputDialog(self)
dialog.setWindowTitle("Level encoder")
dialog.setLabelText("Enter the level data to encode:")
ok = dialog.exec()
data = dialog.textValue()
if ok and data:
pyperclip.copy(encode_level(data, self.levelDataIsOfficial.isChecked()))
msg = QMessageBox()
msg.setWindowTitle("Level encoder")
msg.setText("The encoded level data has been copied to your clipboard")
msg.exec()
def on_convert_from_gdlvl_click(self):
file_path, _ = QFileDialog.getOpenFileName(
self,
"Select a GDLVL file",
"",
"GDLVL Files (*.gdlvl)"
)
if not file_path:
return
output_file_path, _ = QFileDialog.getSaveFileName(
self,
"Select a location for the output GMD file",
file_path.removesuffix(".gdlvl") + ".gmd",
"GMD Files (*.gmd)"
)
if not output_file_path:
return
with open(file_path, "r") as f:
data = f.read()
with open(output_file_path, "w") as f:
f.write(decode_level(data, False))
def on_convert_from_gmd_click(self):
file_path, _ = QFileDialog.getOpenFileName(
self,
"Select a GMD file",
"",
"GMD Files (*.gmd)"
)
if not file_path:
return
output_file_path, _ = QFileDialog.getSaveFileName(
self,
"Select a location for the output GDLVL file",
file_path.removesuffix(".gmd") + ".gdlvl",
"GDLVL Files (*.gdlvl)"
)
if not output_file_path:
return
with open(file_path, "r") as f:
data = f.read()
with open(output_file_path, "w") as f:
f.write(encode_level(data, False))
def on_gjp_decode_click(self):
dialog = QInputDialog(self)
dialog.setWindowTitle("GJP decoder")
dialog.setLabelText("Enter the GJP to decode:")
ok = dialog.exec()
data = dialog.textValue()
if ok and data:
pyperclip.copy(decode_gjp(data))
msg = QMessageBox()
msg.setWindowTitle("GJP decoder")
msg.setText("The decoded GJP has been copied to your clipboard")
msg.exec()
def on_gjp_encode_click(self):
dialog = QInputDialog(self)
dialog.setWindowTitle("GJP encoder")
dialog.setLabelText("Enter the password to encode:")
ok = dialog.exec()
data = dialog.textValue()
if ok and data:
pyperclip.copy(encode_gjp(data))
msg = QMessageBox()
msg.setWindowTitle("GJP encoder")
msg.setText("The encoded GJP has been copied to your clipboard")
msg.exec()
def on_gjp2_encode_click(self):
dialog = QInputDialog(self)
dialog.setWindowTitle("GJP2 encoder")
dialog.setLabelText("Enter the password to encode:")
ok = dialog.exec()
data = dialog.textValue()
if ok and data:
pyperclip.copy(encode_gjp2(data))
msg = QMessageBox()
msg.setWindowTitle("GJP2 encoder")
msg.setText("The encoded GJP2 has been copied to your clipboard")
msg.exec()
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())