Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions ddmd_settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
[
{
"config_gl2": false
}
]
{
"config_gl2": false
}
2 changes: 0 additions & 2 deletions game/mod_content.rpy
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,8 @@ image ddmd_transfer_icon_hover = "sdc_system/settings_app/transferHover.png"
image ddmd_time_clock = DynamicDisplayable(ddmd_app.get_current_time)

default persistent.mod_list_disclaimer_accepted = False
define modSearchCriteria = ""

default persistent.self_extract = None
define tempFolderName = ""

default persistent.transfer_warning = False

Expand Down
41 changes: 25 additions & 16 deletions game/mod_dir_browser.rpy
Original file line number Diff line number Diff line change
@@ -1,31 +1,40 @@
## Copyright 2023 Azariel Del Carmen (GanstaKingofSA)

init python:
import errno

def can_access(path, drive=False):
try:
if not renpy.windows or drive:
return os.access(path, os.R_OK)
else:
for x in os.listdir(path):
if drive:
if os.name == "nt" and len(path) == 2 and path[1] == ":":
return os.path.isdir(path)
return False

if os.name == "nt":
try:
for entry in os.listdir(path):
break
except OSError as e:
if e.errno == 13 or e.errno == 2 or e.errno == 22:
return False
raise
return True
return True
except WindowsError as e:
if e.errno == errno.EACCES or e.winerror == 59:
return False
raise
else:
return os.access(path, os.R_OK)

def get_network_drives():
temp = subprocess.run("powershell (Get-WmiObject -ClassName Win32_MappedLogicalDisk).Name", check=True, shell=True, stdout=subprocess.PIPE).stdout.decode("utf-8").replace("\r\n", "").split(":")
temp.pop(-1)
return temp
result = subprocess.check_output("net use", shell=True)
output_lines = result.strip().split('\r\n')[1:]
drives = [line.split()[1].rstrip(':') for line in output_lines if line.startswith('OK')]
return drives

def get_physical_drives(net_drives):
temp = subprocess.run("powershell (Get-PSDrive -PSProvider FileSystem).Name", check=True, shell=True, stdout=subprocess.PIPE).stdout.decode("utf-8").split("\r\n")
temp.pop(-1)
temp = subprocess.check_output("powershell (Get-PSDrive -PSProvider FileSystem).Name", shell=True)
temp = temp.decode("utf-8").strip().split("\r\n")

for x in temp:
for x in temp[:]:
if x in net_drives:
temp.remove(x)

return temp

screen pc_directory(loc=None, ml=False, mac=False):
Expand Down
208 changes: 137 additions & 71 deletions game/mod_installer.rpy
Original file line number Diff line number Diff line change
@@ -1,107 +1,173 @@

init python in ddmd_mod_installer:
from store import persistent
from zipfile import ZipFile
from zipfile import ZipFile, BadZipfile
import os
import shutil
import tempfile

tempFolderName = ""
modFolderName = ""

def valid_zip(filePath):
"""
Returns whether the given ZIP file is a valid Ren'Py/DDLC mod ZIP file.
filePath - the direct path to the ZIP file.
"""
zip_contents = []
def inInvalidDir(path):
for x in ("lib", "renpy"):
if os.path.normpath(x) in os.path.normpath(path):
return True
if path.endswith(".app"):
return True
return False

with ZipFile(filePath, "r") as temp_zip:
zip_contents = temp_zip.namelist()
def check_mod_validity(zipPath, copy):
if not copy:
# ZIP file check
with ZipFile(zipPath, "r") as temp_zip:
zip_contents = temp_zip.namelist()

for x in zip_contents:
if x.endswith((".rpa", ".rpyc", ".rpy")):
del zip_contents
return True
mod_files = zip_contents
else:
# Folder check
mod_files = []

return False
for mod_src, dirs, files in os.walk(zipPath):
for mod_f in files:
mod_files.append(os.path.join(mod_src, mod_f))

def inInvalidDir(path):
for x in ("lib", "renpy"):
if x + "/" in path.replace("\\", "/"):
return True
# Check if mod files contain Ren'Py files
if any(file.endswith((".rpa", ".rpyc", ".rpy")) for file in mod_files):
return True
return False

def install_mod(zipPath, copy=False):
global tempFolderName
def get_top_level_folder(mod_dir):
top_level_folder = None

for entry in os.listdir(mod_dir):
if os.path.isdir(os.path.join(mod_dir, entry)):
top_level_folder = entry
break

if not tempFolderName:
return top_level_folder

def identify_mod_format(mod_dir):
"""
Identifies the format of the mod package based on its content and structure.
mod_dir: The path to the mod package directory.
Returns: A string indicating the format of the mod package.
"""
top_level_folder = get_top_level_folder(mod_dir)

if os.path.isdir(os.path.join(mod_dir, top_level_folder, "characters")) and os.path.isdir(os.path.join(mod_dir, top_level_folder, "game")):
return 1 # Standard Format
elif os.path.isdir(os.path.join(mod_dir, top_level_folder, "game")):
return 2 # Standard Format (No Characters Folder)
elif os.path.isdir(os.path.join(mod_dir, "characters")) and os.path.isdir(os.path.join(mod_dir, "game")):
return 3 # Standard Format without top folder
elif os.path.isdir(os.path.join(mod_dir, "game")):
if any(f.endswith((".rpa", ".rpyc", ".rpy")) for f in os.listdir(os.path.join(mod_dir, "game"))):
return 5 # Possible Format 2 or 4 (game folder with RPAs or RPYC/RPY files)
else:
return 4 # Standard Format without top folder (No Characters Folder)
elif any(f.endswith(".rpa") for f in os.listdir(mod_dir)):
return 6 # Possible Format 1 (RPAs without game folder)
elif os.path.isdir(os.path.join(mod_dir, "mod_assets")) or any(f.endswith((".rpyc", ".rpy")) for f in os.listdir(mod_dir)):
return 7 # Possible Format 3 (RPYC/RPY files without game folder)
return -1 # Unknown Format

def extract_mod_from_zip(zipPath):
mod_dir = tempfile.mkdtemp(prefix="NewDDML_", suffix="_TempArchive")
try:
with ZipFile(zipPath, "r") as tempzip:
tempzip.extractall(mod_dir)
return mod_dir
except BadZipFile:
shutil.rmtree(mod_dir)
return None

def move_mod_files(mod_folder_path, mod_dir_path, mod_format_id, copy):
"""
Moves or copies the mod files from the mod directory or mod package folder to the mod folder.
mod_folder_path: Path to the mod folder.
mod_dir_path: Path to the mod directory or mod package folder.
mod_format_id: Integer value indicating the mod format ID.
copy: Boolean value indicating whether to copy the files (macOS only).
"""
if mod_format_id in (1, 2, 3, 4, 5):
characters_dir = os.path.join(mod_dir_path, "characters")
game_dir = os.path.join(mod_dir_path, "game")
if copy:
# Copy characters folder (if applicable)
if os.path.isdir(characters_dir):
shutil.copytree(characters_dir, mod_folder_path)
# Copy game folder to mod_folder_path
shutil.copytree(game_dir, mod_folder_path)
else:
# Move characters folder (if applicable)
if os.path.isdir(characters_dir):
shutil.move(characters_dir, mod_folder_path)
# Move game folder to mod_folder_path
shutil.move(os.path.join(mod_dir_path, "game"), mod_folder_path)
elif mod_format_id in (6, 7):
game_folder_path = os.path.join(mod_folder_path, "game")
# Create game folder in mod_folder_path
os.makedirs(game_folder_path)
# Move all files from mod_dir_path to game folder in mod_folder_path
for entry in os.listdir(mod_dir_path):
entry_path = os.path.join(mod_dir_path, entry)
if os.path.isfile(entry_path):
shutil.move(entry_path, game_folder_path)
elif os.path.isdir(entry_path):
shutil.move(entry_path, os.path.join(game_folder_path, entry))

def install_mod(zipPath, modFolderName, copy=False):
if not modFolderName:
renpy.show_screen("ddmd_dialog", message="Error: The folder name cannot be blank.")
return
elif tempFolderName.lower() in ("ddlc mode", "stock mode", "ddlc", "stock"):
tempFolderName = ""
renpy.show_screen("ddmd_dialog", message="Error: %s is a reserved folder name. Please try another folder name." % tempFolderName)
elif modFolderName.lower() in ("ddlc mode", "stock mode", "ddlc", "stock"):
modFolderName = ""
renpy.show_screen("ddmd_dialog", message="Error: %s is a reserved folder name. Please try another folder name." % modFolderName)
return
elif os.path.exists(os.path.join(persistent.ddml_basedir, "game/mods/" + tempFolderName)):
tempFolderName = ""
elif os.path.exists(os.path.join(persistent.ddml_basedir, "game/mods/" + modFolderName)):
modFolderName = ""
renpy.show_screen("ddmd_dialog", message="Error: This mod folder already exists. Please try another folder name.")
return
else:
renpy.show_screen("ddmd_progress", message="Installing mod. Please wait.")
folderPath = os.path.join(persistent.ddml_basedir, "game/mods", tempFolderName)
folderPath = os.path.join(persistent.ddml_basedir, "game/mods", modFolderName)
try:
if not check_mod_validity(zipPath, copy):
raise Exception("Given file/folder is an invalid DDLC Mod Package. Please select a different file/folder.")

if not copy:
if not valid_zip(zipPath):
raise Exception("Given ZIP file is a invalid DDLC Mod ZIP Package. Please select a different ZIP file.")
return

mod_dir = tempfile.mkdtemp(prefix="NewDDML_", suffix="_TempArchive")

with ZipFile(zipPath, "r") as tempzip:
tempzip.extractall(mod_dir)

mod_dir = extract_mod_from_zip(zipPath)
if mod_dir is None:
raise Exception("Invalid mod structure. Please select a different ZIP file.")
else:
validMod = False
for mod_src, dirs, files in os.walk(zipPath):
for mod_f in files:
if mod_f.endswith((".rpa", ".rpyc", ".rpy")):
validMod = True
if validMod:
mod_dir = zipPath
else:
raise Exception("Given Mod Folder is a invalid DDLC Mod Folder Package. Please select a different mod folder.")
return

mod_dir = zipPath

mod_format_id = identify_mod_format(mod_dir)
print("Mod Format ID: %d" % mod_format_id)
if mod_format_id == -1:
raise Exception("Mod is packaged in a way that is unknown to DDMD. Re-download the mod that follows a proper DDLC mod package standard or contact 'bronya_rand' with the mod in question.")

os.makedirs(folderPath)
os.makedirs(os.path.join(folderPath, "game"))

for mod_src, dirs, files in os.walk(mod_dir):
dst_dir = mod_src.replace(mod_dir, folderPath)
for d in dirs:
if d == "characters":
shutil.move(os.path.join(mod_src, d), os.path.join(dst_dir, d))
for f in files:
if f.endswith((".rpa", ".rpyc", ".rpy")):
if not inInvalidDir(mod_src):
mod_dir = mod_src
break

for mod_src, dirs, files in os.walk(mod_dir):
dst_dir = mod_src.replace(mod_dir, folderPath + "/game")
for mod_d in dirs:
shutil.move(os.path.join(mod_src, mod_d), os.path.join(dst_dir, mod_d))
for mod_f in files:
shutil.move(os.path.join(mod_src, mod_f), os.path.join(dst_dir, mod_f))

if mod_format_id in (1, 2):
top_level_folder = get_top_level_folder(mod_dir)
mod_dir_path = os.path.join(mod_dir, top_level_folder)
move_mod_files(folderPath, mod_dir_path, mod_format_id, copy)
else:
move_mod_files(folderPath, mod_dir, mod_format_id, copy)

renpy.hide_screen("ddmd_progress")
renpy.show_screen("ddmd_dialog", message="%s has been installed successfully." % modFolderName)
except BadZipfile:
renpy.hide_screen("ddmd_progress")
renpy.show_screen("ddmd_dialog", message="%s has been installed successfully." % tempFolderName)
renpy.show_screen("ddmd_dialog", message="Error: Invalid ZIP file. Please select a different ZIP file.")
except OSError as err:
if os.path.exists(folderPath):
shutil.rmtree(folderPath)
renpy.hide_screen("ddmd_progress")
renpy.show_screen("ddmd_dialog", message="A error has occured during installation.", message2=str(err))
renpy.show_screen("ddmd_dialog", message="An error has occured during installation.", message2=str(err))
except Exception as err:
if os.path.exists(folderPath):
shutil.rmtree(folderPath)
renpy.hide_screen("ddmd_progress")
renpy.show_screen("ddmd_dialog", message="A error has occured during installation.", message2=str(err))
tempFolderName = ""
renpy.show_screen("ddmd_dialog", message="An unexpected error has occured during installation.", message2=str(err))
5 changes: 3 additions & 2 deletions game/mod_list.rpy
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,14 @@ screen mod_search(xs=480, ys=220):
xalign .5
yalign .5
spacing 8
default modSearchCriteria = ""

label _("Search For?"):
text_size 20
xalign 0.5

input:
value VariableInputValue("modSearchCriteria")
value ScreenVariableInputValue("modSearchCriteria")
length 24
allow "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz[[]] "

Expand All @@ -70,7 +71,7 @@ screen mod_search(xs=480, ys=220):
spacing 100

textbutton _("OK") action [Hide("mod_search", Dissolve(0.25)), Function(search_script, modSearchCriteria)]
textbutton _("Clear") action SetVariable("modSearchCriteria", "")
textbutton _("Clear") action SetScreenVariable("modSearchCriteria", "")

screen mod_list_info(mod):
zorder 102
Expand Down
5 changes: 3 additions & 2 deletions game/mod_prompt.rpy
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,19 @@ screen mod_name_input(zipPath, copy=False, xs=480, ys=220):
xalign .5
yalign .5
spacing 8
default tempFolderName = ""

label _("Enter the name you wish to call this mod."):
text_size int(18 * res_scale)
xalign 0.5

input default "" value FieldInputValue(ddmd_mod_installer, "tempFolderName") length 24 allow "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz 0123456789:-"
input default "" value ScreenVariableInputValue("tempFolderName") length 24 allow "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz 0123456789:-"

hbox:
xalign 0.5
spacing 100

textbutton _("OK") action [Hide("mod_name_input"), Function(ddmd_mod_installer.install_mod, zipPath=zipPath, copy=copy)]
textbutton _("OK") action [Hide("mod_name_input"), Function(ddmd_mod_installer.install_mod, zipPath=zipPath, modFolderName=tempFolderName, copy=copy)]

screen ddmd_progress(message, xs=480, ys=220):
modal True
Expand Down
Loading