diff --git a/.github/workflows/pyinstaller-linux.yml b/.github/workflows/pyinstaller-linux.yml index 382fbc3..891933d 100644 --- a/.github/workflows/pyinstaller-linux.yml +++ b/.github/workflows/pyinstaller-linux.yml @@ -21,6 +21,7 @@ jobs: pip install pyinstaller pip install jsoncparser pip install certifi + pip install pyyaml - name: Run PyInstaller run: | pyinstaller --noconfirm --onedir --console --name "Snark" --add-data "version.txt:." --add-data "activities.txt:." --add-data "logo128.png:." --add-data "icon-win32.ico:." --add-data "icon-linux.png:." --add-data "LICENSE:." --add-data "README.md:." --add-data "save:save/" --add-data "themes:themes/" --add-data "third_party:third_party/" --add-data "images:images/" --add-data "logs:logs/" "GUI.py" diff --git a/.github/workflows/pyinstaller.yml b/.github/workflows/pyinstaller.yml index 1b2637c..5a2cc35 100644 --- a/.github/workflows/pyinstaller.yml +++ b/.github/workflows/pyinstaller.yml @@ -21,6 +21,7 @@ jobs: pip install pyinstaller pip install jsoncparser pip install certifi + pip install pyyaml - name: Run PyInstaller run: | pyinstaller --noconfirm --onedir --console --name "Snark" --add-data "version.txt:." --add-data "activities.txt:." --add-data "logo128.png:." --add-data "icon-win32.ico:." --add-data "icon-linux.png:." --add-data "LICENSE:." --add-data "README.md:." --add-data "save:save/" --add-data "themes:themes/" --add-data "third_party:third_party/" --add-data "images:images/" --add-data "logs:logs/" "GUI.py" diff --git a/GUI.py b/GUI.py index d366fcd..dc3773d 100644 --- a/GUI.py +++ b/GUI.py @@ -577,11 +577,13 @@ def changeTheme(self, a): self.decMenu.changeTheme(thCol) self.optMenu.changeTheme(thCol) self.compSetMenu.changeTheme(thCol) + self.scrMenu.changeTheme(thCol) self.setupMenu.master.config(bg=thCol["bg"]) self.cmpMenu.master.config(bg=thCol["bg"]) self.abtMenu.master.config(bg=thCol["bg"]) self.optMenu.master.config(bg=thCol["bg"]) self.compSetMenu.master.config(bg=thCol["bg"]) + self.scrMenu.master.config(bg=thCol["bg"]) self.frame.config(bg=thCol["bg"]) self.header.config(bg=thCol["bg"]) for w in self.header.winfo_children(): diff --git a/README.md b/README.md index ca22067..d5cdf04 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@

Why should I use Snark over Crowbar?

It's easier to configure and use!

-

In Crowbar, to decompile a model you would have to fiddle around with settings in order to get your model decompiled properly for GoldSRC. With Snark, you don't need to do any extra configuration, just select an MDL file, select the output folder, set the appropriate preset for the compiler used, hit the decompile button and you're set!

+

In Crowbar, to decompile a model you would have to fiddle around with settings in order to get your model decompiled properly for GoldSRC. With Snark, you don't need to do any extra configuration, just select an MDL file, select the output folder, set the appropriate preset for the compiler you'd like to use, hit the decompile button and you're set!

Screenshot of decompile menu

On top of that, the compiling GUI includes all the command-line options as toggleable checks with tooltips to tell you what they do, so you don't need to check any sort of documentation, official or unofficial to use the advanced features StudioMDL offers its users.

Screenshot of compile menu diff --git a/helpers.py b/helpers.py index ed8a64f..ba8d3d7 100644 --- a/helpers.py +++ b/helpers.py @@ -21,6 +21,9 @@ def callback(self, opt): self.command(opt) self.name.set(self.text) +""" +Widget class that displays an output of anything, including terminal outputs or logs, while disallowing the user to edit the output. +""" class Console(): def __init__(self, master, startText:str='', columnn=0, roww=0, widthh=100, heightt=20): @@ -28,18 +31,49 @@ def __init__(self, master, startText:str='', columnn=0, roww=0, widthh=100, heig self.row = roww self.width = widthh self.height = heightt + self.lines = [] self.miniTerminal = Text(master, width=self.width, height=self.height) ys = Scrollbar(master, orient='vertical', command=self.miniTerminal.yview) self.miniTerminal['yscrollcommand'] = ys.set self.miniTerminal.insert('1.0', startText) + if startText.find("\n") != -1: + self.lines = startText.split('\n') + else: + self.lines.append(startText) self.miniTerminal['state'] = 'disabled' + # Sets the output of the console to something else. def setOutput(self, replace): self.miniTerminal['state'] = 'normal' self.miniTerminal.delete('1.0', 'end') self.miniTerminal.insert('1.0', replace) self.miniTerminal['state'] = 'disabled' + if replace.find("\n") != -1: + self.lines = replace.split('\n') + else: + self.lines.append(replace) + + # Appends a new line to the bottom of the console. + def append(self, e): + self.miniTerminal['state'] = 'normal' + self.miniTerminal.insert('end', e) + self.lines.append(e) + self.miniTerminal['state'] = 'disabled' + + # Clears the output completely. + def clear(self): + self.lines = [] + self.miniTerminal['state'] = 'normal' + self.miniTerminal.delete('1.0', 'end') + self.miniTerminal['state'] = 'disabled' + + # Checks if the output is empty and returns true if it is. + def isEmpty(self): + if len(self.lines) == 0: + return True + return False + # Helper function that formats outputted text generated by the subprocess library to be usable for the class. def subprocessHelper(self, query): tOutputTmp = query.split('\n') tOutputTmp.pop(0) @@ -57,9 +91,11 @@ def subprocessHelper(self, query): tOutput += tOutputTmp[count] + '\n' return tOutput + # Show the console def show(self): self.miniTerminal.grid(column=self.column, row=self.row, sticky=(N, S, E, W), columnspan=69) + # Hide the console def hide(self): self.miniTerminal.grid_remove() diff --git a/interp.py b/interp.py index 49192d3..de2d06a 100644 --- a/interp.py +++ b/interp.py @@ -2,6 +2,9 @@ import jsonc from tkinter.filedialog import askopenfilename, askdirectory import os +import sys +import subprocess +import yaml # Class that gets information from the script header (the lines of text sandwiched inbetween the dashes) class SSTVer: @@ -33,7 +36,9 @@ class SSTGlobal: def __init__(self, dat:dict, opt:dict): self.name = dat["name"] - self.globalOut = dat["globalOutput"] + self.defProf = dat.get("defComp", None) + self.defDec = dat.get("defDecPreset", None) + self.globalOut = dat.get("globalOutput", None) if self.globalOut.startswith('askFolder'): # Getting start directory startDir = opt["startFolder"] @@ -48,10 +53,11 @@ def __init__(self, dat:dict, opt:dict): # Reader for SST (Snark ScripT) files class SSTReader: - def __init__(self, sst:str, opt:dict): + def __init__(self, sst:str, opt:dict, logger, options:dict): sstf = open(sst, 'r') scr = sstf.readlines() count = -1 + error = False for l in scr: count += 1 scr[count] = l.replace('\n', '') @@ -63,12 +69,109 @@ def __init__(self, sst:str, opt:dict): elif header.format == 'json': print(header.script) parsedSCR = json.loads(header.script) - scrG = SSTGlobal(parsedSCR, opt) - print("Data from SST file: ") - print(f"Version: {header.version}") - print(f"Format: {header.format}") - print(f"Name: {scrG.name}") - suffix = "" - if parsedSCR["globalOutput"].startswith("askFolder"): - suffix = "(generated path from askFolder)" - print(f"Global Output Folder: {scrG.globalOut} {suffix}") \ No newline at end of file + elif header.format == 'yaml' or header.format == 'yml': + print(header.script) + parsedSCR = yaml.safe_load(header.script) + else: + logger.append("Fatal error: Format specified in header is not json, jsonc, or yaml/yml.") + error = True + if not error: + scrG = SSTGlobal(parsedSCR, opt) + print("Data from SST file: ") + print(f"Version: {header.version}") + print(f"Format: {header.format}") + print(f"Name: {scrG.name}") + suffix = "" + if parsedSCR["globalOutput"].startswith("askFolder"): + suffix = "(generated path from askFolder)" + print(f"Global Output Folder: {scrG.globalOut} {suffix}") + intrp = SSTInterp(logger, parsedSCR["tasks"], scrG, header, options) + + +class SSTInterp: + + def __init__(self, logger, dat, globalVs, header, options): + self.logger = logger + self.dat = dat + self.globalVs = globalVs + self.header = header + self.options = options + self.tskCnt = 0 + # This variable stores all available tasks and tells the Interpreter how to handle them + self.cmds = { + # This command is for compiling models + "compile": { + "func": self.cmpHnd, + # First array: Type?, Accepted inputs, Options + # Second array: + "args": [["file", "file", ".mdl"], ["output", "folder"]] + }, + # This command is for decompiling models + "decompile": { + "func": self.dCMPHnd, + "args": [["file", "file", ".mdl"], ["output", "folder"]] + }, + "view": { + "func": self.hlmvHnd, + "args": [["file", "file+task", ".mdl"]] + } + } + logger.append(f"Starting Script \'{globalVs.name}\'") + + def taskToValue(self, tsk:str, grab:str): + for t in self.dat: + if t["name"] == tsk: + return t[grab] + # If a value could not be found, return nothing. + return None + + # Functions for each instruction defined in self.cmds + def cmpHnd(self): + # This is a stub for now. + pass + + def dCOMPHnd(self): + # This is a stub for now. + pass + + def hlmvHnd(self, dat): + file = "" + genByTask = False + if dat["file"].endswith(".mdl"): + file = dat["file"] + else: + file = self.taskToValue(dat["file"], "output") + genByTask = True + + if file != None: + if not genByTask: + self.hlmvTsk(file, False) + else: + self.hlmvTsk(file, True, dat["file"]) + else: + self.logger.append(f"Error: Couldn't find model generated by compile task \'{dat["file"]}\'") + self.logger.append(f"From: view task \'{dat["name"]}\' (index {self.tskCnt})") + + def hlmvTsk(self, f, genByTask:bool, task=""): + if genByTask: + self.logger.append(f"Viewing model generated by compile task: \'{task}\'") + else: + self.logger.append(f"Viewing model \'{os.path.basename(f)}\'") + # If "Half-Life Asset Manager" is selected + if self.options["gsMV"]["selectedMV"] == 1: + if sys.platform == "linux": + a = subprocess.getoutput(f"XDG_SESSION_TYPE=x11 hlam \"{f}\"") + else: + a = subprocess.getoutput(f"\"C:/Program Files (x86)/Half-Life Asset Manager/hlam.exe\" \"{f}\"") + # If "Other" option is selected + elif self.options["gsMV"]["selectedMV"] > 1: + if sys.platform == "linux": + path = self.options["gsMV"]["csPath"] + path = os.path.expanduser(path) + if path.endswith(".exe"): + a = subprocess.getoutput(f"wine \"{path}\" \"{f}\"") + else: + a = subprocess.getoutput(f"\"{path}\" \"{f}\"") + else: + path = self.options["gsMV"]["csPath"] + a = subprocess.getoutput(f"\"{path}\" \"{f}\"") \ No newline at end of file diff --git a/menus.py b/menus.py index 14b334b..9120ab9 100644 --- a/menus.py +++ b/menus.py @@ -647,9 +647,9 @@ def startDecomp(self): tOutput = subprocess.getoutput(f'./third_party/mdldec -a \"{mdl}\"') elif sys.platform == 'win32': if gotArgs: - tOutput = subprocess.getoutput(f'\"{os.getcwd()}/third_party/mdldec.exe -a {cmdArgs} \" \"{mdl}\"') + tOutput = subprocess.getoutput(f'\"{os.getcwd()}/third_party/mdldec.exe\" -a {cmdArgs} \"{mdl}\"') else: - tOutput = subprocess.getoutput(f'\"{os.getcwd()}/third_party/mdldec.exe -a \" \"{mdl}\"') + tOutput = subprocess.getoutput(f'\"{os.getcwd()}/third_party/mdldec.exe\" -a \"{mdl}\"') # I don't have a Mac so I can't compile mdldec to Mac targets :( # So instead I have to use wine for Mac systems """elif sys.platform == 'darwin': @@ -815,7 +815,7 @@ def __init__(self, template, master, startHidden:bool=False): self.angleSBtt = ToolTip(self.angleChk, "Overrides the blend angle of vertex normals, Valve recommends keeping this value at 2 (the default) according to the HLSDK docs.", background=thme["tt"], foreground=thme["txt"]) self.angleChkTT = ToolTip(self.hitboxChk, "Dumps hitbox information to the console when enabled.", background=thme["tt"], foreground=thme["txt"]) self.keepBonesChkTT = ToolTip(self.keepBonesChk, "Tells the compiler to keep all bones, including unweighted bones.", background=thme["tt"], foreground=thme["txt"]) - self.ignoreChkTT = ToolTip(self.ignoreChk, "Tells the compiler to ignore all warnings, useful for if you want to quickly test a model that isn't complete yet.", background=thme["tt"], foreground=thme["txt"]) + self.ignoreChkTT = ToolTip(self.ignoreChk, "Tells the compiler to ignore all warnings, useful for when you want to quickly test a model that isn't complete yet.", background=thme["tt"], foreground=thme["txt"]) self.bNormChkTT = ToolTip(self.bNormChk, "Tags bad normals in the console.", background=thme["tt"], foreground=thme["txt"]) self.flipChkTT = ToolTip(self.flipChk, "Tells the compiler to flip all triangles in the model.", background=thme["tt"], foreground=thme["txt"]) self.groupChkTT = ToolTip(self.groupChk, "Sets the maximum group size for sequences in KB", background=thme["tt"], foreground=thme["txt"]) @@ -1847,7 +1847,8 @@ def __init__(self, template, master, startHidden:bool=False): EXE_LOCATION = os.path.dirname( os.path.realpath( __file__ ) ) self.scr_dir = os.path.join(EXE_LOCATION, "scripts") for s in os.listdir(self.scr_dir): - self.scripts.append(s) + if not s.startswith("template"): + self.scripts.append(s) self.scr_list = Listbox(master, width=self.widthFix, selectmode=SINGLE) count = -1 @@ -1864,7 +1865,7 @@ def __init__(self, template, master, startHidden:bool=False): def readScript(self): selected_scr = self.scripts[int(self.scr_list.curselection()[0])] - a = SSTReader(os.path.join(self.scr_dir, selected_scr), self.options) + a = SSTReader(os.path.join(self.scr_dir, selected_scr), self.options, self.console) def applyTheme(self, master): style= ttk.Style() diff --git a/scripts/batch-compile.sst b/scripts/batch-compile.sst index a78929a..5ff9d9e 100644 --- a/scripts/batch-compile.sst +++ b/scripts/batch-compile.sst @@ -4,7 +4,8 @@ format json - { "name": "Batch Compile", - "globalOutput": "askFolder", + "globalOutput": "askFolder,Select a folder full of compilable models", + "defComp": "ask", "tasks": [ { "name": "Compile MDL", diff --git a/scripts/batch-decompile.sst b/scripts/batch-decompile.sst index a7fec6f..4a06b9a 100644 --- a/scripts/batch-decompile.sst +++ b/scripts/batch-decompile.sst @@ -4,7 +4,8 @@ format json - { "name": "Batch Decompile", - "globalOutput": "askFolder,Select a folder full of models", + "globalOutput": "askFolder,Select a folder full of compiled models", + "defDecPreset": "ask", "tasks": [ { "name": "Decompile MDL", diff --git a/scripts/template.sst b/scripts/template.sst index 9cf6370..99dd2a7 100644 --- a/scripts/template.sst +++ b/scripts/template.sst @@ -4,8 +4,12 @@ format jsonc - { "name": "Template Script", - // You can specify a global output folder for every task + // You can specify a global output folder for every task. "globalOutput": "path/to/output/folder", + // You can also specify a default compiler for the compile task to use, + "defComp": "Svengine", + // and a default preset for the decompiler to use. + "defDecPreset": "Svengine", "tasks": [ { // This specifies the name of the task, this will show up in the console when the task has been started. @@ -15,14 +19,27 @@ format jsonc "task": "decompile", "file": "absolute/path/to/mdl/file", // You can use this key to specify an output folder for this specific task. - "output": "path/to/output/folder" + "output": "path/to/output/folder", + // Specify arguments for the decompiler, write them how you would if you were interfacing with it through a terminal. + // Supported options: -l, -m, -t, -u, -V + // More info for them available on the wiki's Scripting 101 page. + "args": "-m -t", + // Specify a decompiler preset just for this task, you can do this instead of specifying arguments directly. + // Available options are: GoldSRC, Svengine, DoomMusic and Xash3D + "usePreset": "Svengine" }, { "name": "Compile MDL", "task": "compile", "file": "absolute/path/to/qc/file", // Please note that with compile tasks, the output will not be put in a subfolder of the output folder. - "output": "path/to/output/folder" + "output": "path/to/output/folder", + // Specify arguments for the compiler, write them how you would if you were interfacing with it through a terminal. + // Supported options: -t, -r, -a, -h, -i, -n, -f, -g, -p, -k (only for Sven Co-op's StudioMDL). + // More info for them available on the wiki's Scripting 101 page. + "args": "-k", + // Specify a compiler to use just for this task. + "useComp": "Svengine" }, { "name": "View Compiled Model", diff --git a/version.txt b/version.txt index 0b16ff8..824a641 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v0.2.2-(OS)-alpha +v0.2.3-(OS)-alpha