379 lines
13 KiB
Python
379 lines
13 KiB
Python
import sys
|
|
from PyQt6.QtCore import Qt
|
|
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QWidget, QLabel, QPushButton, QFileDialog, QInputDialog, QMessageBox, QCheckBox
|
|
from PyQt6.QtGui import QFont
|
|
from Crypto.Cipher import AES
|
|
import base64
|
|
import gzip
|
|
import zlib
|
|
import pyperclip
|
|
import hashlib
|
|
import platform
|
|
|
|
if platform.system() == "Windows":
|
|
from winmica import EnableMica, BackdropType, is_mica_supported
|
|
|
|
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")
|
|
self.apply_mica(self)
|
|
|
|
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_to_gdlvl_click)
|
|
self.convertToGmd = QPushButton("Convert to GMD")
|
|
self.convertToGmd.clicked.connect(self.on_convert_to_gmd_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 apply_mica(self, widget):
|
|
if platform.system() == "Windows" and is_mica_supported():
|
|
widget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
|
|
hwnd = int(widget.winId())
|
|
EnableMica(hwnd, BackdropType.MICA)
|
|
|
|
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:")
|
|
self.apply_mica(dialog)
|
|
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")
|
|
self.apply_mica(msg)
|
|
msg.exec()
|
|
|
|
def on_level_encode_click(self):
|
|
dialog = QInputDialog(self)
|
|
dialog.setWindowTitle("Level encoder")
|
|
dialog.setLabelText("Enter the level data to encode:")
|
|
self.apply_mica(dialog)
|
|
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")
|
|
self.apply_mica(msg)
|
|
msg.exec()
|
|
|
|
def on_convert_to_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, is_official_level=False))
|
|
|
|
def on_convert_to_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, is_official_level=False))
|
|
|
|
def on_gjp_decode_click(self):
|
|
dialog = QInputDialog(self)
|
|
dialog.setWindowTitle("GJP decoder")
|
|
dialog.setLabelText("Enter the GJP to decode:")
|
|
self.apply_mica(dialog)
|
|
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")
|
|
self.apply_mica(msg)
|
|
msg.exec()
|
|
|
|
def on_gjp_encode_click(self):
|
|
dialog = QInputDialog(self)
|
|
dialog.setWindowTitle("GJP encoder")
|
|
dialog.setLabelText("Enter the password to encode:")
|
|
self.apply_mica(dialog)
|
|
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")
|
|
self.apply_mica(msg)
|
|
msg.exec()
|
|
|
|
def on_gjp2_encode_click(self):
|
|
dialog = QInputDialog(self)
|
|
dialog.setWindowTitle("GJP2 encoder")
|
|
dialog.setLabelText("Enter the password to encode:")
|
|
self.apply_mica(dialog)
|
|
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")
|
|
self.apply_mica(msg)
|
|
msg.exec()
|
|
|
|
app = QApplication(sys.argv)
|
|
window = MainWindow()
|
|
window.show()
|
|
sys.exit(app.exec()) |