Rewrite, version 1.0.0

Mods with highest priority are loaded first (reverse load-order), saving
each file path of the linked files in memory for quick "if exists"
checks. Files that already exist simply do not get replaced, as the
pre-existing files are higher priority. The game is linked at the very
end.

This all results in major speedup.

Some features are still pending reimplementation.
This commit is contained in:
Fierelier 2020-10-03 22:53:32 +02:00
parent 6839022091
commit 2c4010c2b5
1 changed files with 157 additions and 321 deletions

View File

@ -1,358 +1,194 @@
#!/usr/bin/env python3
#Imports and variables
class uml:
version = 0
versionSub = 11
versionSub2 = 1
versionBranch = "beta (dev)"
versionString = str(version) + "." + str(versionSub) + "." +str(versionSub2)+ " " + versionBranch
import sys
import os
import shutil
import webbrowser
import time
import subprocess
import configparser
oldexcepthook = sys.excepthook
def newexcepthook(type,value,traceback):
oldexcepthook(type,value,traceback)
input("Press ENTER to quit.")
sys.excepthook = newexcepthook
p = os.path.join
pUp = os.path.dirname
scriptPath = pUp(os.path.realpath(__file__))
s = False
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
s = os.path.realpath(sys.executable)
else:
s = os.path.realpath(__file__)
sp = pUp(s)
appPath = False
appName = False
originalAppPath = False
tmpAppPath = False
modPath = False
originalModPath = False
api = True
cloneMethod = "hardlink" # hardlink, copy or reflink
version = (1,0,0,"beta (dev)")
pathGame = ""
pathTemp = ""
pathOrig = ""
pathMods = ""
stateModded = False
listLinked = []
listLinkedFolders = []
#Modloader
def testAccess(path):
def filterDd(text):
while text[-1] in ['"',"'"," ",os.path.sep]: text = text[:-1]
while text[0] in ['"',"'"," "]: text = text[1:]
return text
def questionYesNo(text):
while True:
response = input(text).lower()
if response == "y": return True
if response == "n": return False
def questionMultChoice(options):
length = len(options)
index = 0
while index < length:
print(str(index + 1)+ ": " +options[index])
index = index + 1
response = input("Choice: ")
try:
os.rename(path,path)
return True
response = int(response)
except:
return False
def walkMods(modDir):
for root,dirs,files in walklevel(modDir):
for dir in dirs:
if dir[0] == "-": continue
if dir[0] == "[" and dir[-1:] == "]":
for mod in walkMods(p(root,dir)): yield mod
continue
yield p(root,dir)
def cloneMods(modDir):
for mod in walkMods(modDir):
modName = mod.replace(modPath + os.sep,"")
print("Applying Mod: " +modName)
if os.path.isfile(p(mod,"uml_installscript.py")) == True:
file = open(p(mod,"uml_installscript.py"))
exec(file.read(),globals(),locals())
file.close()
else:
cloneFolder(mod,tmpAppPath,True,False,True)
def loadMods(output = False, fast = False):
if fast == False:
if areModsLoaded():
if unloadMods() == False:
if output: print("Unloading mods failed!")
return False
if fast == True:
if areModsLoaded() == False:
if output: print("Can not fast-load mods when mods aren't loaded.")
return False
if fast == False:
if areModsLoaded():
if output: print("Mods are already loaded and could not be unloaded.")
return False
print("Testing access...")
if testAccess(appPath) == False:
if output: print("Can't access folder! Is it in use?")
return
print("Cloning app folder...")
cloneFolder(appPath,tmpAppPath,False)
print("Cloning mods...")
cloneMods(modPath)
os.rename(appPath,originalAppPath)
os.rename(tmpAppPath,appPath)
else:
print("Testing access...")
if testAccess(appPath) == False:
if output: print("Can't access folder! Is it in use?")
return
os.rename(appPath,tmpAppPath)
cloneMods(modPath)
os.rename(tmpAppPath,appPath)
#ctypes.windll.kernel32.SetFileAttributesW(originalAppPath,2)
if output: print("\nMods have been loaded!")
return True
if response < 1: return False
if response > index: return False
return options[response - 1]
def unloadMods(output = False):
if areModsLoaded() == False:
if output: print("Mods are already unloaded.")
def getModState():
if os.path.isdir(pathOrig):
return True
print("Testing access...")
if testAccess(appPath) == False:
if output: print("Can't access folder! Is it in use?")
return
print("Removing cloned app folder...")
try:
shutil.rmtree(appPath)
except:
if output: print("Can't delete folder! Is it in use?")
return
tries = 0
while tries < 100:
try:
os.rename(originalAppPath,appPath)
tries + 1
except:
time.sleep(0.1)
tries + 1
else:
break
#ctypes.windll.kernel32.SetFileAttributesW(appPath,128)
if output: print("\nUnloading mods successful.")
return True
def openModsFolder():
if areModsLoaded():
webbrowser.open("file://" +originalModPath)
else:
webbrowser.open("file://" +modPath)
def areModsLoaded():
if os.path.isdir(originalAppPath):
return True
return False
def clone(src,dst):
if cloneMethod == "hardlink":
os.link(src,dst)
def getModlist(path,sort = True):
modList = []
for root,dirs,files in os.walk(path):
for file in dirs:
ffile = p(root,file)
lfile = ffile.replace(path + os.path.sep,"",1)
if lfile[0] == "-": continue
if lfile[0] == "[" and lfile[-1] == "]":
modList = modList + getModlist(ffile,False)
continue
modList.append(ffile)
break
if cloneMethod == "copy":
shutil.copyfile(src,dst)
if cloneMethod == "reflink": # use when using btrfs or similar
subprocess.call(["cp","-n","--reflink=always",src,dst])
if sort == True: return sorted(modList)
return modList
def cloneFolder(src,dst,replace = False,ignoreMods = True, isMod = False):
maxCount = 0
count = 0
def linkFile(src,dst):
os.link(src,dst)
def cloneFolder(src,dst):
global listLinked
global listLinkedFolders
for root,dirs,files in os.walk(src):
newRoot = root.replace(src,dst)
if ignoreMods == True:
if root.replace(modPath,"") != root: continue
maxCount = maxCount + len(files)
sys.stdout.write("\r" +str(count)+ "/" +str(maxCount))
sys.stdout.flush()
for root,dirs,files in os.walk(src):
newRoot = root.replace(src,dst)
if ignoreMods == True:
if root.replace(modPath,"") != root: continue
if os.path.isdir(newRoot) == False: os.makedirs(newRoot)
for file in dirs:
ffile = p(root,file)
lfile = ffile.replace(src + os.path.sep,"",1)
nfile = p(dst,lfile)
if nfile in listLinkedFolders: continue
os.makedirs(nfile)
listLinkedFolders.append(nfile)
for file in files:
count = count + 1
sys.stdout.write("\r" +str(count)+ "/" +str(maxCount))
sys.stdout.flush()
if isMod == True:
if file[:4] == "uml_": continue
fullFile = p(root,file)
newFile = p(newRoot,file)
if os.path.isfile(newFile):
if replace == True:
os.remove(newFile)
clone(fullFile,newFile)
else:
clone(fullFile,newFile)
ffile = p(root,file)
lfile = ffile.replace(src + os.path.sep,"",1)
nfile = p(dst,lfile)
if nfile in listLinked: continue
linkFile(ffile,nfile)
listLinked.append(nfile)
def loadMods():
print("")
global listLinked
global listLinkedFolders
global stateModded
os.makedirs(pathTemp)
print("Getting mod-list...")
listMods = getModlist(pathMods)
for ffile in reversed(listMods):
lfile = ffile.replace(pathMods + os.path.sep,"",1)
print("Load mod: " +lfile)
cloneFolder(ffile,pathTemp)
print("Linking game...")
cloneFolder(pathGame,pathTemp)
os.rename(pathGame,pathOrig)
os.rename(pathTemp,pathGame)
listLinked = []
listLinkedFolders = []
stateModded = True
def walklevel(some_dir, level=0):
some_dir = some_dir.rstrip(os.path.sep)
assert os.path.isdir(some_dir)
num_sep = some_dir.count(os.path.sep)
for root, dirs, files in os.walk(some_dir):
yield root, dirs, files
num_sep_this = root.count(os.path.sep)
if num_sep + level <= num_sep_this:
del dirs[:]
def unloadMods():
global stateModded
print("")
print("Removing modded game...")
shutil.rmtree(pathGame)
print("Renaming original game...")
os.rename(pathOrig,pathGame)
stateModded = False
def title(string):
if api == True: return
if os.name == "nt":
os.system("title " +string)
else:
sys.stdout.write("\x1b]2;" +string+ "\x07")
def clear():
os.system('cls' if os.name=='nt' else 'clear')
def cleanUp():
if os.path.isdir(tmpAppPath):
shutil.rmtree(tmpAppPath)
def checkUp():
if os.path.isdir(modPath) == False:
while True:
clear()
print("You selected the following path: '" +appPath+ "'")
choice = input("Do you wish to set up that folder for mod-use? (y/n)\n")
if choice == "y":
os.makedirs(modPath)
#ctypes.windll.kernel32.SetFileAttributesW(modPath,2)
return
if choice == "n": sys.exit(-1)
def mainMenu():
clear()
if areModsLoaded() == False:
print("Welcome to Fier's Universal Modloader.")
print("Mods are not loaded.")
print("")
print("Please choose an action:")
print("1) Load Mods")
print("2) Open Mods-Folder")
choice = input("Choice: ")
clear()
if choice == "1": loadMods(True); input("Press ENTER to continue.")
if choice == "2": openModsFolder()
else:
print("Welcome to Fier's Universal Modloader.")
print("Mods are loaded.")
print("")
print("Please choose an action:")
print("1) Reload Mods")
print("2) Fast-Load Mods")
print("3) Unload Mods")
print("4) Open Mods-Folder")
choice = input("Choice: ")
clear()
if choice == "1": loadMods(True); input("Press ENTER to continue.")
if choice == "2": loadMods(True,True); input("Press ENTER to continue.")
if choice == "3": unloadMods(True); input("Press ENTER to continue.")
if choice == "4": openModsFolder()
def requestAppPath():
while True:
clear()
dir = input("Please insert a folder via Drag&Drop:\n")
if (dir[0] == '"' and dir[-1] == '"') or (dir[0] == "'" and dir[-1] == "'"): dir = dir[1:-1]
if dir == "console":
console()
def main():
global pathGame, pathMods, pathTemp, pathOrig, stateModded
pathGame = ""
if len(sys.argv) > 1:
pathGame = sys.argv[1]
while pathGame == "" or os.path.isdir(pathGame) == False:
pathGame = filterDd(input("Path to game - you may drag & drop:\n"))
pathMods = pathGame + " - umlMods"
pathTemp = pathGame + " - umlTemp"
pathOrig = pathGame + " - umlOriginal"
if not os.path.isdir(pathMods):
if questionYesNo("\nIt doesn't look like you modded this game before.\nDo you want to create the mod folder? (y/n)\n") == True:
os.makedirs(pathMods)
else:
if os.path.isdir(dir) == True:
return dir
def console():
clear()
print("UniversalModloader Console")
print("Version: " +uml.versionString)
sys.exit()
if os.path.isdir(pathTemp):
print("\nRemoving temporary folder...")
shutil.rmtree(pathTemp)
stateModded = getModState()
while True:
print("")
cmd = input(os.getcwd()+ ">")
print("Modded: " +str(stateModded))
choice = False
if cmd == "exit":
return
if stateModded == False: choice = questionMultChoice([
"Load mods",
"Open mod-folder"
])
if cmd.startswith("cd "):
try:
os.chdir(cmd[3:])
continue
except Exception as e:
print(e)
continue
if stateModded == True: choice = questionMultChoice([
"Reload mods",
"Unload mods",
"Open mod-folder"
])
curModState = getModState()
if curModState != stateModded:
stateModded = curModState
print("\nMod-state has changed, refreshing menu.")
continue
if cmd.startswith("pyc "):
try:
exec(cmd[4:])
continue
except Exception as e:
print(e)
continue
if cmd.startswith("mc "):
try:
cloneFolder(cmd[3:],cmd[3:] + " - clone",True,False)
continue
except Exception as e:
print(e)
continue
os.system(cmd)
def setupVariables():
global appPath
global appName
global originalAppPath
global tmpAppPath
global modPath
global originalModPath
if len(sys.argv) < 2:
appPath = requestAppPath()
else:
appPath = sys.argv[1]
if appPath == "console":
console()
if os.path.isdir(appPath) == False:
raise NameError("Folder not found: " +appPath)
if os.path.isdir(appPath.replace(" - umlOriginal","")): appPath = appPath.replace(" - umlOriginal","")
appName = appPath.replace(pUp(appPath)+ os.sep,"")
originalAppPath = appPath + " - umlOriginal"
tmpAppPath = appPath + " - umlTemp"
modPath = p(appPath + " - umlMods")
originalModPath = modPath
def init():
global api
api = False
title("Fier's Universal Modloader - " +uml.versionString)
setupVariables()
title("Fier's Universal Modloader - " +uml.versionString+ " : " +appName)
cleanUp()
checkUp()
while True:
mainMenu()
if choice == False: continue
if choice == "Load mods": loadMods()
if choice == "Reload mods": unloadMods(); loadMods()
if choice == "Unload mods": unloadMods()
if choice == "Open mod-folder": webbrowser.open("file://" +pathMods)
if __name__ == "__main__":
init()
main()