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:
parent
6839022091
commit
2c4010c2b5
|
@ -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()
|
Loading…
Reference in New Issue