revert: modules

This commit is contained in:
RedDeadDepresso
2024-07-25 02:31:51 +01:00
parent 758d77fe4b
commit be99cd3ff0
21 changed files with 628 additions and 530 deletions

View File

@@ -36,17 +36,17 @@ class Config(QConfig):
""" Config of application """
# core
gamePath = ConfigItem("Core", "GamePath", "", FolderValidator())
gamePath = ConfigItem("Core", "GamePath", "C:/Program Files (x86)/Steam/steamapps/common/Koikatsu Party", FolderValidator())
# createBackup
backupEnable = ConfigItem(
"CreateBackup", "Enable", False, BoolValidator()
)
backupPath = ConfigItem(
"CreateBackup", "OutputPath", "", FolderValidator()
"CreateBackup", "OutputPath", "C:/Backup", FolderValidator()
)
filename = ConfigItem(
"CreateBackup", "Filename", "",
"CreateBackup", "Filename", "koikatsu_backup",
)
userData = ConfigItem(
"CreateBackup", "UserData", False, BoolValidator()
@@ -73,7 +73,7 @@ class Config(QConfig):
installEnable = ConfigItem(
"InstallChara", "Enable", False, BoolValidator()
)
gamePath = ConfigItem(
installPath = ConfigItem(
"InstallChara", "InputPath", "", FolderValidator())
fileConflicts = OptionsConfigItem(
"InstallChara", "FileConflicts", "Skip", OptionsValidator(["Skip", "Replace", "Rename"])
@@ -86,8 +86,8 @@ class Config(QConfig):
removeEnable = ConfigItem(
"RemoveChara", "Enable", False, BoolValidator()
)
gamePath = ConfigItem(
"InstallChara", "InputPath", "", FolderValidator())
removePath = ConfigItem(
"RemoveChara", "InputPath", "", FolderValidator())
# main window
micaEnabled = ConfigItem("MainWindow", "MicaEnabled", isWin11(), BoolValidator())

View File

@@ -1,169 +0,0 @@
import os
import shutil
import datetime
import patoolib
import customtkinter
import subprocess
import time
from app.common.logger import logger
class FileManager:
def __init__(self, config):
self.config = config
def findAllFiles(self, directory):
fileList = []
archiveList = []
archiveExtensions = [".rar", ".zip", ".7z"]
for root, dirs, files in os.walk(directory):
for file in files:
filePath = os.path.join(root, file)
fileSize = os.path.getsize(filePath)
_, fileExtension = os.path.splitext(filePath)
if fileExtension in archiveExtensions:
archiveList.append((filePath, fileSize, fileExtension))
else:
fileList.append((filePath, fileSize, fileExtension))
fileList.sort(key=lambda x: x[1])
archiveList.sort(key=lambda x: x[1])
return fileList, archiveList
def copyAndPaste(self, type, sourcePath, destinationFolder):
sourcePath = sourcePath[0]
baseName = os.path.basename(sourcePath)
destinationPath = os.path.join(destinationFolder, baseName)
conflicts = self.config.install_chara["FileConflicts"]
alreadyExists = os.path.exists(destinationPath)
if alreadyExists and conflicts == "Skip":
logger.skipped(type, baseName)
return
elif alreadyExists and conflicts == "Replace":
logger.replaced(type, baseName)
elif alreadyExists and conflicts == "Rename":
maxRetries = 3
for attempt in range(maxRetries):
try:
filename, fileExtension = os.path.splitext(sourcePath)
newName = datetime.datetime.now().strftime('%Y%m%d%H%M%S%f')
newSourcePath = f"{filename}_{newName}{fileExtension}"
os.rename(sourcePath, newSourcePath)
sourcePath = newSourcePath
logger.renamed(type, baseName)
filename, fileExtension = os.path.splitext(destinationPath)
destinationPath = f"{filename}_{newName}{fileExtension}"
break # Exit the loop if renaming is successful
except PermissionError:
if attempt < maxRetries - 1:
time.sleep(1) # Wait for 1 second before retrying
else:
logger.error(type, f"Failed to rename {baseName} after {maxRetries} attempts.")
return
try:
shutil.copy(sourcePath, destinationPath)
print(f"File copied successfully from {sourcePath} to {destinationPath}")
if not alreadyExists:
logger.success(type, baseName)
except FileNotFoundError:
logger.error(type, f"{baseName} does not exist.")
except PermissionError:
logger.error(type, f"Permission denied for {baseName}")
except Exception as e:
logger.error(type, f"An error occurred: {e}")
def findAndRemove(self, type, sourcePath, destinationFolder):
sourcePath = sourcePath[0]
baseName = os.path.basename(sourcePath)
destinationPath = os.path.join(destinationFolder, baseName)
if os.path.exists(destinationPath):
try:
os.remove(destinationPath)
logger.removed(type, baseName)
except OSError as e:
logger.error(type, baseName)
def createArchive(self, folders, archivePath):
# Specify the full path to the 7zip executable
pathTo7zip = patoolib.util.find_program("7z")
if not pathTo7zip:
logger.error("SCRIPT", "7zip not found. Unable to create backup")
raise Exception()
if os.path.exists(archivePath+".7z"):
os.remove(archivePath+".7z")
pathTo7zip = f'"{pathTo7zip}"'
archivePath = f'"{archivePath}"'
excludeFolders = [
'"Sideloader Modpack"',
'"Sideloader Modpack - Studio"',
'"Sideloader Modpack - KK_UncensorSelector"',
'"Sideloader Modpack - Maps"',
'"Sideloader Modpack - KK_MaterialEditor"',
'"Sideloader Modpack - Fixes"',
'"Sideloader Modpack - Exclusive KK KKS"',
'"Sideloader Modpack - Exclusive KK"',
'"Sideloader Modpack - Animations"'
]
# Create a string of folder names to exclude
excludeString = ''
for folder in excludeFolders:
excludeString += f'-xr!{folder} '
# Create a string of folder names to include
includeString = ''
for folder in folders:
includeString += f'"{folder}" '
# Construct the 7zip command
command = f'{pathTo7zip} a -t7z {archivePath} {includeString} {excludeString}'
# Call the command
process = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# Print the output
for line in process.stdout.decode().split('\n'):
if line.strip() != "":
logger.info("7-Zip", line)
# Check the return code
if process.returncode not in [0, 1]:
raise Exception()
def extractArchive(self, archivePath):
try:
archiveName = os.path.basename(archivePath)
logger.info("ARCHIVE", f"Extracting {archiveName}")
extractPath = os.path.join(f"{os.path.splitext(archivePath)[0]}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S%f')}")
patoolib.extract_archive(archivePath, outdir=extractPath)
return extractPath
except patoolib.util.PatoolError as e:
text = f"There is an error with the archive {archiveName} but it is impossible to detect the cause. Maybe it requires a password?"
while self.config.install_chara["Password"] == "Request Password":
try:
dialog = customtkinter.CTkInputDialog(text=text, title="Enter Password")
password = dialog.get_input()
if password is not None or "":
patoolib.extract_archive(archivePath, outdir=extractPath, password=password)
return extractPath
else:
break
except:
text = f"Wrong password or {archiveName} is corrupted. Please enter password again or click Cancel"
logger.skipped("ARCHIVE", archiveName)
fileManager = FileManager()

View File

@@ -29,8 +29,8 @@ class Logger:
# Status Text: INFO, SUCCESS, ERROR, SKIPPED, REPLACED, RENAMED, REMOVED
self.status = ['&nbsp;&nbsp;&nbsp;&nbsp;INFO', '&nbsp;&nbsp;SUCCESS', '&nbsp;&nbsp;&nbsp;ERROR',
'&nbsp;SKIPPED', '&nbsp;REPLACED', '&nbsp;RENAMED', '&nbsp;REMOVED']
# Status Color: Blue, Red, Green, Orange,
self.statusColor = ['#2d8cf0', '#ed3f14', '#f90', '#f90', '#f90', '#f90', '#00c12b']
# Status Color: Blue, Red, Green, Orange
self.statusColor = ['#2d8cf0', '#00c12b', '#ed3f14', '#f90', '#f90', '#f90', '#f90']
# Status HTML: <b style="color:$color">status</b>
self.statusHtml = [
@@ -69,17 +69,20 @@ class Logger:
def colorize(self, line):
adding = line
print(line)
for i, s in enumerate(self.text):
if s in line:
print(s)
print(self.statusColor[i])
adding = (f'''
<div style="font-family: Consolas, monospace;color:{self.statusColor[i]};">
{line}
</div>
''')
break
self.logs += adding
self.logger_signal.emit(adding)
return
def info(self, category: str, message: str) -> None:
"""

View File

@@ -32,6 +32,7 @@ class ThreadManager:
logger.colorize(line)
def stop(self):
if self._script is not None:
self._script.terminate()
self._script = None

View File

@@ -1,27 +0,0 @@
import os
from app.common.file_manager import fileManager
from app.modules.handler import Handler
class CreateBackup(Handler):
def __str__(self) -> str:
return "Create Backup"
def loadConfig(self, config):
super().loadConfig(config)
folders = ["mods", "UserData", "BepInEx"]
self.folders = [self.gamePath[f] for f in folders if self.config[f]]
self.outputPath = self.config["OutputPath"]
self.filename = self.config["Filename"]
def handle(self, request):
outputPath = os.path.join(self.outputPath, self.filename)
fileManager.createArchive(self.folders, outputPath)
self.setNext(request)

View File

@@ -1,108 +0,0 @@
import os
import re as regex
import codecs
import shutil
from app.common.logger import logger
from app.modules.handler import Handler
class FilterConvertKKS(Handler):
def __str__(self) -> str:
return "Filter Convert KKS"
def loadConfig(self, config):
super().loadConfig(config)
self.convert = self.config["Convert"]
def getList(self, folderPath):
newList = []
for root, dirs, files in os.walk(folderPath):
for filename in files:
if regex.match(r".*(\.png)$", filename):
newList.append(os.path.join(root, filename))
return newList
def checkPng(self, cardPath):
with codecs.open(cardPath, "rb") as card:
data = card.read()
cardType = 0
if data.find(b"KoiKatuChara") != -1:
cardType = 1
if data.find(b"KoiKatuCharaSP") != -1:
cardType = 2
elif data.find(b"KoiKatuCharaSun") != -1:
cardType = 3
logger.info(f"[{cardType}]", f"{cardPath}")
return cardType
def convertKk(self, cardName, cardPath, destinationPath):
with codecs.open(cardPath, mode="rb") as card:
data = card.read()
replaceList = [
[b"\x15\xe3\x80\x90KoiKatuCharaSun", b"\x12\xe3\x80\x90KoiKatuChara"],
[b"Parameter\xa7version\xa50.0.6", b"Parameter\xa7version\xa50.0.5"],
[b"version\xa50.0.6\xa3sex", b"version\xa50.0.5\xa3sex"],
]
for text in replaceList:
data = data.replace(text[0], text[1])
newFilePath = os.path.normpath(os.path.join(destinationPath, f"KKS2KK_{cardName}"))
with codecs.open(newFilePath, "wb") as newCard:
newCard.write(data)
def handle(self, request):
path = self.config.fc_kks["InputPath"]
kksCardList = []
kksFolder = "_KKS_card_"
kksFolder2 = "_KKS_to_KK_"
pngList = self.getList(path)
count = len(pngList)
if count > 0:
logger.info("SCRIPT", "0: unknown / 1: kk / 2: kksp / 3: kks")
for png in pngList:
if self.checkPng(png) == 3:
kksCardList.append(png)
else:
logger.success("SCRIPT", f"no PNG found")
return
count = len(kksCardList)
if count > 0:
print(kksCardList)
targetFolder = os.path.normpath(os.path.join(path, kksFolder))
targetFolder2 = os.path.normpath(os.path.join(path, kksFolder2))
if not os.path.isdir(targetFolder):
os.mkdir(targetFolder)
if self.convert:
logger.info("SCRIPT", f"Conversion to KK is [{self.convert}]")
if not os.path.isdir(targetFolder2):
os.mkdir(targetFolder2)
for cardPath in kksCardList:
source = cardPath
card = os.path.basename(cardPath)
target = os.path.normpath(os.path.join(targetFolder, card))
# copy & convert before move
if self.convert:
self.convertKk(card, source, targetFolder2)
# move file
shutil.move(source, target)
if self.convert:
logger.success("SCRIPT", f"[{count}] cards moved to [{kksFolder}] folder, converted and save to [{kksFolder2}] folder")
else:
logger.success("SCRIPT", f"[{count}] cards moved to [{kksFolder}] folder")
else:
logger.success("SCRIPT", f"no KKS card found")
self.setNext(request)

View File

@@ -1,16 +0,0 @@
class Handler:
def __str__(self) -> str:
return "Handler"
def loadConfig(self, config):
self.gamePath = config.get("Core", "GamePath")
key = str(self).replace(" ", "")
self.config = config.get(key)
def handle(self, request):
pass
def setNext(self, request):
request.removeHandler()
request.process()

View File

@@ -1,57 +0,0 @@
import os
import codecs
from app.common.file_manager import fileManager
from app.common.logger import logger
from app.modules.handler import Handler
class InstallChara(Handler):
def __str__(self) -> str:
return "Install Chara"
def loadConfig(self, config):
super().loadConfig(config)
self.inputPath = self.config["InputPath"]
def resolvePng(self, imagePath):
with codecs.open(imagePath[0], "rb") as card:
data = card.read()
if data.find(b"KoiKatuChara") != -1:
if data.find(b"KoiKatuCharaSP") != -1 or data.find(b"KoiKatuCharaSun") != -1:
basename = os.path.basename(imagePath[0])
logger.error("CHARA", f"{basename} is a KKS card")
return
fileManager.copyAndPaste("CHARA", imagePath, self.gamePath["chara"])
elif data.find(b"KoiKatuClothes") != -1:
fileManager.copyAndPaste("COORD",imagePath, self.gamePath["coordinate"])
else:
fileManager.copyAndPaste("OVERLAYS", imagePath, self.gamePath["Overlays"])
def handle(self, request, folderPath=None):
if folderPath is None:
folderPath = self.inputPath
foldername = os.path.basename(folderPath)
logger.log_msg("FOLDER", foldername)
fileList, archiveList = fileManager.findAllFiles(folderPath)
for file in fileList:
extension = file[2]
match extension:
case ".zipmod":
fileManager.copyAndPaste("MODS", file, self.gamePath["mods"])
case ".png":
self.resolvePng(file)
case _:
basename = os.path.basename(file[0])
logger.error("UKNOWN", f"Cannot classify {basename}")
for archive in archiveList:
extractPath = fileManager.extractArchive(archive[0])
if extractPath is not None:
self.handle(extractPath)
self.setNext(request)

View File

@@ -1,48 +0,0 @@
import os
import codecs
from app.common.file_manager import fileManager
from app.common.logger import logger
from app.modules.handler import Handler
class RemoveChara(Handler):
def __str__(self) -> str:
return "Remove Chara"
def loadConfig(self, config):
super().configLoad(config)
self.inputPath = self.config["InputPath"]
def resolvePng(self, imagePath):
with codecs.open(imagePath[0], "rb") as card:
data = card.read()
if data.find(b"KoiKatuChara") != -1:
if data.find(b"KoiKatuCharaSP") != -1 or data.find(b"KoiKatuCharaSun") != -1:
return
fileManager.findAndRemove("CHARA", imagePath, self.gamePath["chara"])
elif data.find(b"KoiKatuClothes") != -1:
fileManager.findAndRemove("COORD",imagePath, self.gamePath["coordinate"])
else:
fileManager.findAndRemove("OVERLAYS", imagePath, self.gamePath["Overlays"])
def handle(self, request):
foldername = os.path.basename(self.inputPath)
logger.info("FOLDER", foldername)
fileList, archiveList = fileManager.findAllFiles(self.inputPath)
for file in fileList:
extension = file[2]
match extension:
case ".zipmod":
fileManager.findAndRemove("MODS", file, self.game_path["mods"])
case ".png":
self.resolvePng(file)
case _:
pass
logger.line()
self.setNext(request)

View File

@@ -1,76 +0,0 @@
import os
from app.common.config import cfg
from app.common.logger import logger
from app.modules.handler import Handler
from app.modules.create_backup import CreateBackup
from app.modules.fc_kks import FilterConvertKKS
from app.modules.install_chara import InstallChara
from app.modules.remove_chara import RemoveChara
class Request:
def __init__(self):
self._handlers = [CreateBackup(), FilterConvertKKS(), InstallChara(), RemoveChara()]
self._config = cfg.toDict()
self._isValid = True
logger.info("SCRIPT", "Validating config")
self.validateGamepath()
self._handlers = [x for x in self.handlers if self.isTaskEnabled(x)]
if not self._handlers:
logger.error("SCRIPT", "No task enabled")
if not self._isValid:
raise Exception()
def validatePath(self, path, errorMsg):
if not os.path.exists(path):
logger.error("SCRIPT", errorMsg)
self._isValid = False
return False
return True
def validateGamepath(self):
base = self.config['Core']['GamePath']
self.config['Core']['GamePath'] = {
"base": base,
"UserData": os.path.join(base, "UserData"),
"BepInEx": os.path.join(base, "BepInEx"),
"mods": os.path.join(base, "mods"),
"chara": os.path.join(base, "UserData\\chara\\female"),
"coordinate": os.path.join(base, "UserData\\coordinate"),
"Overlays": os.path.join(base, "UserData\\Overlays")
}
for directory, path in self.config['Core']['GamePath'].items():
self.validatePath(path, f"Game path not valid. Missing {directory} directory.")
def isTaskEnabled(self, handler: Handler):
task = str(handler).replace(" ", "")
taskConfig = self.config[task]
if not taskConfig["Enable"]:
return False
if (path := taskConfig.get("InputPath")):
self.validatePath(path, f"Invalid path for {str(handler)}: {path}")
if (path := taskConfig.get("OutputPath")):
self.validatePath(path, f"Invalid path for {str(handler)}: {path}")
if self._isValid:
handler.loadConfig(self.config)
return True
def removeHandler(self) -> Handler:
if len(self._handlers) > 1:
return self._handlers.pop(0)
def process(self):
if self._handlers:
self._handlers[0].handle()

View File

@@ -8,7 +8,7 @@ from qfluentwidgets import FluentIcon as FIF
from qfluentwidgets import InfoBar, DisplayLabel
from PySide6.QtCore import Qt, QUrl
from PySide6.QtGui import QDesktopServices
from PySide6.QtWidgets import QWidget
from PySide6.QtWidgets import QWidget, QFileDialog
from ..common.config import cfg, HELP_URL, FEEDBACK_URL, AUTHOR, VERSION, YEAR, isWin11
from ..components.line_edit_card import LineEditSettingCard
@@ -307,23 +307,29 @@ class SettingInterface(ScrollArea):
parent=self
)
# def __onDownloadFolderCardClicked(self):
# """ download folder card clicked slot """
# folder = QFileDialog.getExistingDirectory(
# self, self.tr("Choose folder"), "./")
# if not folder or cfg.get(cfg.downloadFolder) == folder:
# return
def __onFolderCardClicked(self, item, card):
""" download folder card clicked slot """
folder = QFileDialog.getExistingDirectory(
self, self.tr("Choose folder"), "./")
if not folder or cfg.get(item) == folder:
return
# cfg.set(cfg.downloadFolder, folder)
# self.downloadFolderCard.setContent(folder)
cfg.set(item, folder)
card.setContent(folder)
def __connectSignalToSlot(self):
""" connect signal to slot """
cfg.appRestartSig.connect(self.__showRestartTooltip)
# music in the pc
# self.downloadFolderCard.clicked.connect(
# self.__onDownloadFolderCardClicked)
# gamePath
self.gamePathCard.clicked.connect(lambda: self.__onFolderCardClicked(cfg.gamePath, self.gamePathCard))
# backup
self.backupPathCard.clicked.connect(lambda: self.__onFolderCardClicked(cfg.backupPath, self.backupPathCard))
# fckks
self.fckksPathCard.clicked.connect(lambda: self.__onFolderCardClicked(cfg.fccksPath, self.fckksPathCard))
# install
self.installPathCard.clicked.connect(lambda: self.__onFolderCardClicked(cfg.installPath, self.installPathCard))
# remove
self.removePathCard.clicked.connect(lambda: self.__onFolderCardClicked(cfg.removePath, self.removePathCard))
# personalization
self.themeCard.optionChanged.connect(lambda ci: setTheme(cfg.get(ci)))

26
modules/create_backup.py Normal file
View File

@@ -0,0 +1,26 @@
import os
class CreateBackup:
def __init__(self, config, file_manager):
"""Initializes the Bounty module.
Args:
config (Config): BAAuto Config instance
"""
self.config = config
self.file_manager = file_manager
self.game_path = self.config.game_path
folders = ["mods", "UserData", "BepInEx"]
self.folders = [self.game_path[f] for f in folders if self.config.create_backup[f]]
self.filename = self.config.create_backup["Filename"]
self.output_path = self.config.create_backup["OutputPath"]
def logic_wrapper(self):
output_path = os.path.join(self.output_path, self.filename)
self.file_manager.create_archive(self.folders, output_path)

107
modules/fc_kks.py Normal file
View File

@@ -0,0 +1,107 @@
import os
import re as regex
import codecs
import shutil
from util.logger import Logger
class FilterConvertKKS:
def __init__(self, config, file_manager):
"""Initializes the Bounty module.
Args:
config (Config): BAAuto Config instance
"""
self.config = config
self.file_manager = file_manager
self.convert = self.config.fc_kks["Convert"]
def get_list(self, folder_path):
new_list = []
for root, dirs, files in os.walk(folder_path):
for filename in files:
if regex.match(r".*(\.png)$", filename):
new_list.append(os.path.join(root, filename))
return new_list
def check_png(self, card_path):
with codecs.open(card_path, "rb") as card:
data = card.read()
card_type = 0
if data.find(b"KoiKatuChara") != -1:
card_type = 1
if data.find(b"KoiKatuCharaSP") != -1:
card_type = 2
elif data.find(b"KoiKatuCharaSun") != -1:
card_type = 3
Logger.log_info(f"[{card_type}]", f"{card_path}")
return card_type
def convert_kk(self, card_name, card_path, destination_path):
with codecs.open(card_path, mode="rb") as card:
data = card.read()
replace_list = [
[b"\x15\xe3\x80\x90KoiKatuCharaSun", b"\x12\xe3\x80\x90KoiKatuChara"],
[b"Parameter\xa7version\xa50.0.6", b"Parameter\xa7version\xa50.0.5"],
[b"version\xa50.0.6\xa3sex", b"version\xa50.0.5\xa3sex"],
]
for text in replace_list:
data = data.replace(text[0], text[1])
new_file_path = os.path.normpath(os.path.join(destination_path, f"KKS2KK_{card_name}"))
# print(f"new_file_path {new_file_path}")
with codecs.open(new_file_path, "wb") as new_card:
new_card.write(data)
def logic_wrapper(self):
path = self.config.fc_kks["InputPath"]
kks_card_list = []
kks_folder = "_KKS_card_"
kks_folder2 = "_KKS_to_KK_"
png_list = self.get_list(path)
count = len(png_list)
if count > 0:
Logger.log_info("SCRIPT", "0: unknown / 1: kk / 2: kksp / 3: kks")
for png in png_list:
if self.check_png(png) == 3:
kks_card_list.append(png)
else:
Logger.log_success("SCRIPT", f"no PNG found")
return
count = len(kks_card_list)
if count > 0:
print(kks_card_list)
target_folder = os.path.normpath(os.path.join(path, kks_folder))
target_folder2 = os.path.normpath(os.path.join(path, kks_folder2))
if not os.path.isdir(target_folder):
os.mkdir(target_folder)
if self.convert:
Logger.log_info("SCRIPT", f"Conversion to KK is [{self.convert}]")
if not os.path.isdir(target_folder2):
os.mkdir(target_folder2)
for card_path in kks_card_list:
source = card_path
card = os.path.basename(card_path)
target = os.path.normpath(os.path.join(target_folder, card))
# copy & convert before move
if self.convert:
self.convert_kk(card, source, target_folder2)
# move file
shutil.move(source, target)
if self.convert:
Logger.log_success("SCRIPT", f"[{count}] cards moved to [{kks_folder}] folder, converted and save to [{kks_folder2}] folder")
else:
Logger.log_success("SCRIPT", f"[{count}] cards moved to [{kks_folder}] folder")
else:
Logger.log_success("SCRIPT", f"no KKS card found")

56
modules/install_chara.py Normal file
View File

@@ -0,0 +1,56 @@
import os
import codecs
from util.logger import Logger
class InstallChara:
def __init__(self, config, file_manager):
"""Initializes the Bounty module.
Args:
config (Config): BAAuto Config instance
"""
self.config = config
self.file_manager = file_manager
self.game_path = self.config.game_path
self.input_path = self.config.install_chara["InputPath"]
def resolve_png(self, image_path):
with codecs.open(image_path[0], "rb") as card:
data = card.read()
if data.find(b"KoiKatuChara") != -1:
if data.find(b"KoiKatuCharaSP") != -1 or data.find(b"KoiKatuCharaSun") != -1:
basename = os.path.basename(image_path[0])
Logger.log_error("CHARA", f"{basename} is a KKS card")
return
self.file_manager.copy_and_paste("CHARA", image_path, self.game_path["chara"])
elif data.find(b"KoiKatuClothes") != -1:
self.file_manager.copy_and_paste("COORD",image_path, self.game_path["coordinate"])
else:
self.file_manager.copy_and_paste("OVERLAYS", image_path, self.game_path["Overlays"])
def logic_wrapper(self, folder_path=None):
if folder_path is None:
folder_path = self.input_path
foldername = os.path.basename(folder_path)
Logger.log_msg("FOLDER", foldername)
file_list, compressed_file_list = self.file_manager.find_all_files(folder_path)
for file in file_list:
file_extension = file[2]
match file_extension:
case ".zipmod":
self.file_manager.copy_and_paste("MODS", file, self.game_path["mods"])
case ".png":
self.resolve_png(file)
case _:
basename = os.path.basename(file[0])
Logger.log_error("UKNOWN", f"Cannot classify {basename}")
print("[MSG]")
for compressed in compressed_file_list:
extract_path = self.file_manager.extract_archive(compressed[0])
if extract_path is not None:
self.logic_wrapper(extract_path)

47
modules/remove_chara.py Normal file
View File

@@ -0,0 +1,47 @@
import os
import codecs
from util.logger import Logger
class RemoveChara:
def __init__(self, config, file_manager):
"""Initializes the Bounty module.
Args:
config (Config): BAAuto Config instance
"""
self.config = config
self.file_manager = file_manager
self.game_path = self.config.game_path
self.input_path = self.config.remove_chara["InputPath"]
def resolve_png(self, image_path):
with codecs.open(image_path[0], "rb") as card:
data = card.read()
if data.find(b"KoiKatuChara") != -1:
if data.find(b"KoiKatuCharaSP") != -1 or data.find(b"KoiKatuCharaSun") != -1:
return
self.file_manager.find_and_remove("CHARA", image_path, self.game_path["chara"])
elif data.find(b"KoiKatuClothes") != -1:
self.file_manager.find_and_remove("COORD",image_path, self.game_path["coordinate"])
else:
self.file_manager.find_and_remove("OVERLAYS", image_path, self.game_path["Overlays"])
def logic_wrapper(self):
foldername = os.path.basename(self.input_path)
Logger.log_msg("FOLDER", foldername)
file_list, archive_list = self.file_manager.find_all_files(self.input_path)
for file in file_list:
file_extension = file[2]
match file_extension:
case ".zipmod":
self.file_manager.find_and_remove("MODS", file, self.game_path["mods"])
case ".png":
self.resolve_png(file)
case _:
pass
print("[MSG]")

View File

@@ -1,5 +1,69 @@
from app.modules.request import Request
import sys
import traceback
try:
with open('traceback.log', 'w') as f:
pass
from util.config import Config
from util.logger import Logger
from util.file_manager import FileManager
from modules.install_chara import InstallChara
from modules.remove_chara import RemoveChara
from modules.fc_kks import FilterConvertKKS
from modules.create_backup import CreateBackup
request = Request()
request.process()
class Script:
def __init__(self, config, file_manager):
"""Initializes the primary azurlane-auto instance with the passed in
Config instance;
Args:
config (Config): BAAuto Config instance
"""
self.config = config
self.file_manager = file_manager
self.modules = {
'InstallChara': None,
'RemoveChara': None,
'CreateBackup': None,
'FilterConvertKKS': None,
}
if self.config.install_chara['Enable']:
self.modules['InstallChara'] = InstallChara(self.config, self.file_manager)
if self.config.remove_chara['Enable']:
self.modules['RemoveChara'] = RemoveChara(self.config, self.file_manager)
if self.config.create_backup['Enable']:
self.modules['CreateBackup'] = CreateBackup(self.config, self.file_manager)
if self.config.fc_kks["Enable"]:
self.modules['FilterConvertKKS'] = FilterConvertKKS(self.config, self.file_manager)
def run(self):
for task in self.config.tasks:
if self.modules[task]:
Logger.log_info("SCRIPT", f'Start Task: {task}')
try:
self.modules[task].logic_wrapper()
except:
Logger.log_error("SCRIPT", f'Task error: {task}. For more info, check the traceback.log file.')
with open('traceback.log', 'a') as f:
f.write(f'[{task}]\n')
traceback.print_exc(None, f, True)
f.write('\n')
sys.exit(1)
sys.exit(0)
except:
print(f'[ERROR] Script Initialisation Error. For more info, check the traceback.log file.')
with open('traceback.log', 'w') as f:
f.write(f'Script Initialisation Error\n')
traceback.print_exc(None, f, True)
f.write('\n')
sys.exit(1)
if __name__ == "__main__":
config = Config('app/config/config.json')
file_manager = FileManager(config)
script = Script(config, file_manager)
script.run()

0
util/__init__.py Normal file
View File

90
util/config.py Normal file
View File

@@ -0,0 +1,90 @@
import sys
import json
import os
from util.logger import Logger
class Config:
def __init__(self, config_file):
Logger.log_info("SCRIPT", "Initializing config module")
self.config_file = config_file
self.ok = False
self.initialized = False
self.config_data = None
self.changed = False
self.read()
def read(self):
backup_config = self._deepcopy_dict(self.__dict__)
try:
with open(self.config_file, 'r') as json_file:
self.config_data = json.load(json_file)
except FileNotFoundError:
Logger.log_error("SCRIPT", f"Config file '{self.config_file}' not found.")
sys.exit(1)
except json.JSONDecodeError:
Logger.log_error("SCRIPT", f"Invalid JSON format in '{self.config_file}'.")
sys.exit(1)
self.validate()
if self.ok and not self.initialized:
Logger.log_info("SCRIPT", "Starting KKAFIO!")
self.initialized = True
self.changed = True
elif not self.ok and not self.initialized:
Logger.log_error("SCRIPT", "Invalid config. Please check your config file.")
sys.exit(1)
elif not self.ok and self.initialized:
Logger.log_warning("SCRIPT", "Config change detected, but with problems. Rolling back config.")
self._rollback_config(backup_config)
elif self.ok and self.initialized:
if backup_config != self.__dict__:
Logger.log_warning("SCRIPT", "Config change detected. Hot-reloading.")
self.changed = True
def validate(self):
Logger.log_info("SCRIPT", "Validating config")
self.ok = True
self.tasks = ["CreateBackup", "FilterConvertKKS", "InstallChara", "RemoveChara"]
self.create_gamepath()
for task in self.tasks:
if self.config_data[task]["Enable"]:
if "InputPath" in self.config_data[task]:
path = self.config_data[task]["InputPath"]
elif "OutputPath" in self.config_data[task]:
path = self.config_data[task]["OutputPath"]
if not os.path.exists(path):
Logger.log_error("SCRIPT", f"Path invalid for task {task}")
raise Exception()
self.install_chara = self.config_data.get("InstallChara", {})
self.create_backup = self.config_data.get("CreateBackup", {})
self.remove_chara = self.config_data.get("RemoveChara", {})
self.fc_kks = self.config_data.get("FilterConvertKKS", {})
def create_gamepath(self):
base = self.config_data["Core"]["GamePath"]
self.game_path = {
"base": base,
"UserData": os.path.join(base, "UserData"),
"BepInEx": os.path.join(base, "BepInEx"),
"mods": os.path.join(base, "mods"),
"chara": os.path.join(base, "UserData\\chara\\female"),
"coordinate": os.path.join(base, "UserData\coordinate"),
"Overlays": os.path.join(base, "UserData\Overlays")
}
for path in self.game_path.values():
if not os.path.exists(path):
Logger.log_error("SCRIPT", "Game path not valid")
raise Exception()
def _deepcopy_dict(self, dictionary):
from copy import deepcopy
return deepcopy(dictionary)
def _rollback_config(self, config):
for key, value in config.items():
setattr(self, key, value)

165
util/file_manager.py Normal file
View File

@@ -0,0 +1,165 @@
import os
import shutil
import datetime
import patoolib
import customtkinter
import subprocess
import time
from util.logger import Logger
class FileManager:
def __init__(self, config):
self.config = config
def find_all_files(self, directory):
file_list = []
compressed_file_list = []
compressed_extensions = [".rar", ".zip", ".7z"]
for root, dirs, files in os.walk(directory):
for file in files:
file_path = os.path.join(root, file)
file_size = os.path.getsize(file_path)
_, file_extension = os.path.splitext(file_path)
if file_extension in compressed_extensions:
compressed_file_list.append((file_path, file_size, file_extension))
else:
file_list.append((file_path, file_size, file_extension))
file_list.sort(key=lambda x: x[1])
compressed_file_list.sort(key=lambda x: x[1])
return file_list, compressed_file_list
def copy_and_paste(self, type, source_path, destination_folder):
source_path = source_path[0]
base_name = os.path.basename(source_path)
destination_path = os.path.join(destination_folder, base_name)
conflicts = self.config.install_chara["FileConflicts"]
already_exists = os.path.exists(destination_path)
if already_exists and conflicts == "Skip":
Logger.log_skipped(type, base_name)
return
elif already_exists and conflicts == "Replace":
Logger.log_replaced(type, base_name)
elif already_exists and conflicts == "Rename":
max_retries = 3
for attempt in range(max_retries):
try:
filename, file_extension = os.path.splitext(source_path)
new_name = datetime.datetime.now().strftime('%Y%m%d%H%M%S%f')
new_source_path = f"{filename}_{new_name}{file_extension}"
os.rename(source_path, new_source_path)
source_path = new_source_path
Logger.log_renamed(type, base_name)
filename, file_extension = os.path.splitext(destination_path)
destination_path = f"{filename}_{new_name}{file_extension}"
break # Exit the loop if renaming is successful
except PermissionError:
if attempt < max_retries - 1:
time.sleep(1) # Wait for 1 second before retrying
else:
Logger.log_error(type, f"Failed to rename {base_name} after {max_retries} attempts.")
return
try:
shutil.copy(source_path, destination_path)
print(f"File copied successfully from {source_path} to {destination_path}")
if not already_exists:
Logger.log_success(type, base_name)
except FileNotFoundError:
Logger.log_error(type, f"{base_name} does not exist.")
except PermissionError:
Logger.log_error(type, f"Permission denied for {base_name}")
except Exception as e:
Logger.log_error(type, f"An error occurred: {e}")
def find_and_remove(self, type, source_path, destination_folder):
source_path = source_path[0]
base_name = os.path.basename(source_path)
destination_path = os.path.join(destination_folder, base_name)
if os.path.exists(destination_path):
try:
os.remove(destination_path)
Logger.log_removed(type, base_name)
except OSError as e:
Logger.log_error(type, base_name)
def create_archive(self, folders, archive_path):
# Specify the full path to the 7zip executable
path_to_7zip = patoolib.util.find_program("7z")
if not path_to_7zip:
Logger.log_error("SCRIPT", "7zip not found. Unable to create backup")
raise Exception()
if os.path.exists(archive_path+".7z"):
os.remove(archive_path+".7z")
path_to_7zip = f'"{path_to_7zip}"'
archive_path = f'"{archive_path}"'
exclude_folders = [
'"Sideloader Modpack"',
'"Sideloader Modpack - Studio"',
'"Sideloader Modpack - KK_UncensorSelector"',
'"Sideloader Modpack - Maps"',
'"Sideloader Modpack - KK_MaterialEditor"',
'"Sideloader Modpack - Fixes"',
'"Sideloader Modpack - Exclusive KK KKS"',
'"Sideloader Modpack - Exclusive KK"',
'"Sideloader Modpack - Animations"'
]
# Create a string of folder names to exclude
exclude_string = ''
for folder in exclude_folders:
exclude_string += f'-xr!{folder} '
# Create a string of folder names to include
include_string = ''
for folder in folders:
include_string += f'"{folder}" '
# Construct the 7zip command
command = f'{path_to_7zip} a -t7z {archive_path} {include_string} {exclude_string}'
# Call the command
process = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# Print the output
for line in process.stdout.decode().split('\n'):
if line.strip() != "":
Logger.log_info("7-Zip", line)
# Check the return code
if process.returncode not in [0, 1]:
raise Exception()
def extract_archive(self, archive_path):
try:
archive_name = os.path.basename(archive_path)
Logger.log_info("ARCHIVE", f"Extracting {archive_name}")
extract_path = os.path.join(f"{os.path.splitext(archive_path)[0]}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S%f')}")
patoolib.extract_archive(archive_path, outdir=extract_path)
return extract_path
except patoolib.util.PatoolError as e:
text = f"There is an error with the archive {archive_name} but it is impossible to detect the cause. Maybe it requires a password?"
while self.config.install_chara["Password"] == "Request Password":
try:
dialog = customtkinter.CTkInputDialog(text=text, title="Enter Password")
password = dialog.get_input()
if password is not None or "":
patoolib.extract_archive(archive_path, outdir=extract_path, password=password)
return extract_path
else:
break
except:
text = f"Wrong password or {archive_name} is corrupted. Please enter password again or click Cancel"
Logger.log_skipped("ARCHIVE", archive_name)

34
util/logger.py Normal file
View File

@@ -0,0 +1,34 @@
class Logger(object):
@classmethod
def log_msg(cls, type, msg):
print(f"[MSG][{type}] {msg}")
@classmethod
def log_success(cls, type, msg):
print(f"[SUCCESS][{type}] {msg}")
@classmethod
def log_skipped(cls, type, msg):
print(f"[SKIPPED][{type}] {msg}")
@classmethod
def log_replaced(cls, type, msg):
print(f"[REPLACED][{type}] {msg}")
@classmethod
def log_renamed(cls, type, msg):
print(f"[RENAMED][{type}] {msg}")
@classmethod
def log_removed(cls, type, msg):
print(f"[REMOVED][{type}] {msg}")
@classmethod
def log_error(cls, type, msg):
print(f"[ERROR][{type}] {msg}")
@classmethod
def log_info(cls, type, msg):
print(f"[INFO][{type}] {msg}")