├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .vscode └── tasks.json ├── DOCUMENTATION.md ├── Example Patch └── Patch │ └── RaceMenu.bsa │ └── interface │ ├── racemenu │ └── buttonart.json │ └── racesex_menu.json ├── LICENSE ├── OfficialPatches.md ├── README.md ├── build.bat ├── build.py ├── fomod ├── Image.jpg ├── ModuleConfig.xml └── info.xml ├── requirements.txt ├── res ├── 7-zip │ ├── 7z.dll │ ├── 7z.exe │ ├── 7z_History.txt │ ├── 7z_License.txt │ └── 7z_readme.txt ├── default_configs │ └── config.json ├── ffdec │ ├── ffdec.jar │ ├── ffdec_orig.bat │ ├── flashlib │ │ ├── airglobal.swc │ │ └── playerglobal32_0.swc │ ├── lib │ │ ├── JavactiveX.jar │ │ ├── LZMA.jar │ │ ├── avi.jar │ │ ├── avi.montemedia.license.txt │ │ ├── cmykjpeg.jar │ │ ├── ddsreader.jar │ │ ├── ffdec_lib.jar │ │ ├── ffdec_lib.license.txt │ │ ├── flamingo-6.2.jar │ │ ├── flamingo.license.txt │ │ ├── flashdebugger.jar │ │ ├── gif.jar │ │ ├── gif.license.txt │ │ ├── gnujpdf.jar │ │ ├── jargs.jar │ │ ├── jlayer-1.0.2.jar │ │ ├── jlayer.license.txt │ │ ├── jna-3.5.1.jar │ │ ├── jna-platform-3.5.1.jar │ │ ├── jna.license.txt │ │ ├── jpacker.jar │ │ ├── jpacker.license.txt │ │ ├── jpproxy.jar │ │ ├── jpproxy.muffin.license.txt │ │ ├── jsyntaxpane-0.9.5.jar │ │ ├── jsyntaxpane.license.txt │ │ ├── minimal-json-0.9.5.jar │ │ ├── minimal-json.license.txt │ │ ├── nellymoser.jar │ │ ├── nellymoser.license.txt │ │ ├── sfntly.jar │ │ ├── sfntly.license.txt │ │ ├── substance-6.2.jar │ │ ├── substance-flamingo-6.2.jar │ │ ├── substance-flamingo.license.txt │ │ ├── substance.license.txt │ │ ├── tablelayout.jar │ │ ├── tga.jar │ │ ├── tga.license.txt │ │ ├── treetable.jar │ │ ├── trident-6.2.jar │ │ ├── trident.license.txt │ │ ├── ttf.doubletype.license.txt │ │ ├── ttf.fontastic.license.txt │ │ ├── ttf.jar │ │ ├── vlcj-4.7.3.jar │ │ └── vlcj-natives-4.7.0.jar │ └── license.txt ├── glob.dll ├── glob │ ├── dllmain.cpp │ ├── framework.h │ ├── glob_cpp.sln │ ├── glob_cpp.vcxproj │ ├── pch.cpp │ └── pch.h ├── icons │ ├── arrow_down.svg │ ├── arrow_left.svg │ ├── arrow_right.svg │ ├── arrow_up.svg │ ├── checkmark.svg │ ├── grip.png │ └── icon.ico ├── jre.7z ├── resources.qrc ├── style.qss └── xdelta │ ├── license.txt │ └── xdelta.exe ├── src ├── app.py ├── core │ ├── __init__.py │ ├── archive │ │ ├── __init__.py │ │ ├── archive.py │ │ ├── rar.py │ │ ├── sevenzip.py │ │ └── zip.py │ ├── bsa │ │ ├── __init__.py │ │ ├── bsa_archive.py │ │ ├── datatypes.py │ │ ├── file_name_block.py │ │ ├── file_record.py │ │ ├── folder_record.py │ │ ├── header.py │ │ └── utilities.py │ ├── config │ │ ├── __init__.py │ │ ├── _base_config.py │ │ └── config.py │ ├── patcher │ │ ├── __init__.py │ │ ├── ffdec.py │ │ ├── patcher.py │ │ └── xdelta.py │ └── utilities │ │ ├── __init__.py │ │ ├── exception_handler.py │ │ ├── filesystem.py │ │ ├── glob.py │ │ ├── licenses.py │ │ ├── path_splitter.py │ │ ├── process_runner.py │ │ ├── qt_res_provider.py │ │ ├── status_update.py │ │ ├── stdout_handler.py │ │ ├── thread.py │ │ └── xml_utils.py ├── main.py └── ui │ ├── __init__.py │ ├── main_widget.py │ ├── main_window.py │ ├── patcher_widget.py │ └── widgets │ ├── __init__.py │ ├── browse_edit.py │ └── error_dialog.py └── version.yml /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | *.code-workspace 3 | **__pycache__ 4 | dist 5 | build 6 | tests 7 | **.old 8 | src/*.xml 9 | src/*.json 10 | src/resources_rc.py 11 | interface 12 | DIP_with_fomod 13 | *.bsa 14 | res/ffdec/ffdec.bat 15 | DIP_*.7z 16 | mypy.ini 17 | DIP.log 18 | version.txt 19 | main.dist 20 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "compile-qrc", 6 | "type": "shell", 7 | "command": "pyside6-rcc", 8 | "args": [ 9 | "${workspaceFolder:Dynamic-Interface-Patcher}/res/resources.qrc", 10 | "-o", 11 | "${workspaceFolder:Dynamic-Interface-Patcher}/src/resources_rc.py" 12 | ], 13 | "problemMatcher": [], 14 | "detail": "Compiles Qt resource file." 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | # [NEW] Auto Patch Creator 2 | ### Dynamic Interface Construction Kit (DICK) 3 | 4 |

5 | 6 |
7 | 8 | 9 |

10 | 11 | This tool automatically creates a DIP-suitable patch from finished SWF files. Instructions on usage can be found in its [README.md](https://github.com/Cutleast/Dynamic-Interface-Construction-Kit). 12 | 13 | # Manually creating Patches 14 | 15 | **This documentation always refers to the latest version of the patcher!** 16 | 17 | *Latest version as time of writing: v1.0* 18 | 19 | Patches are done in two major steps. At first they are created in FFDec itself and then they get documented in a json files for the automated patcher. 20 | A patch consists of two parts; a "Patch" folder that has the same folder structure as the mod to patch (including BSA files as folders). It contains the specifications and instructions for the patcher and there is a "Shapes" folder containing the shapes that will replace the shapes. 21 | 22 | **NOTE:** You can view the status of official patches [here](https://www.nexusmods.com/skyrimspecialedition/mods/92345/?tab=forum&topic_id=12944454). It would be very great if you'd read through that **before** starting to create a patch yourself. We love to see the community take action but it doesn't help anyone if two people are working on a patch at the same time and we end up with two. 23 | 24 | ### Requirements for creating patches 25 | 26 | - [JPEXS Free Flash Decompiler](https://github.com/jindrapetrik/jpexs-decompiler) 27 | - you have to know how to use it, of course 28 | - (Skyrim) UI modding in general 29 | - basic knowledge about JSON syntax 30 | 31 | ### Things that can be patched automatically using the patcher 32 | 33 | - Shapes (svg files recommended; use png files at your own risk!) 34 | - Everything else via the JSON files 35 | 36 | # Patch folder structure 37 | 38 | The root folder name should contain "DIP" for the patcher to auto detect it and the "Patch" folder has the same structure as the mod that gets patched. This includes BSA archives as folders. For example, to patch `racesex_menu.swf` from the RaceMenu mod the path of the respective JSON file looks like this: 39 | 40 | `/RaceMenu.bsa/interface/racesex_menu.json` 41 | 42 | And a complete RaceMenu patch could look like this: 43 | 44 | `Example patch`: 45 | 46 | ``` 47 | data (in Skyrim's installation directory) 48 | └── Example DIP Patch (root folder) 49 | ├── Patch 50 | | └── RaceMenu.bsa 51 | | └── interface 52 | | ├── racemenu 53 | | | └── buttonart.json 54 | | └── racesex_menu.json 55 | └── Shapes 56 | ├── shape_1.svg 57 | └── shape_2.svg 58 | ``` 59 | 60 | # Patch file structure 61 | 62 | A Patch JSON file consists of two major parts: 63 | 64 | - the shapes, their file paths and the ids they replace 65 | 66 | and 67 | 68 | - the swf itself, where everything else can be modified 69 | 70 | There's also an optional "optional" tag to indicate that the original SWF doesn't have to exist for the patch to succeed. 71 | DIP will then ignore this patch file if the original SWF is missing instead of throwing an error. 72 | 73 | #### SWF (XML) Patch structure 74 | 75 | The patcher converts the SWF files to XML files and modifies them according to the changes specified in the `swf` part of the JSON file. 76 | Therefore this part of the JSON has a very similar structure and it is recommended to familiarize yourself with the general structure of the SWF file when it is converted to an XML file (FFDec has an export feature for this). 77 | Since not all changes should be applied to every element in the file, filters are required to use. There are three different "prefixes" to differentiate between filters, changes and parent elements: 78 | 79 | | Type of key | Prefix | 80 | | ----------- | ------ | 81 | | Filters | # | 82 | | Changes | ~ | 83 | | Parents | None | 84 | 85 |
86 | 87 | `patch.json`: 88 | 89 | ```json 90 | { 91 | "shapes": [ 92 | { 93 | "id": "1,2,5,7,9", 94 | "fileName": "example.svg" // Path relative to "Shapes" folder 95 | } 96 | ], 97 | "optional": true, // this tag itself is optional and indicates that the original file doesn't have to exist for the patch to succeed 98 | // '#' for filters | '~' for changes | '' for parent elements 99 | "swf": { 100 | "displayRect": { 101 | "~Xmax": "25600", 102 | "~Xmin": "0", 103 | "~Ymax": "14400", 104 | "~Ymin": "0" 105 | }, 106 | "tags": [ 107 | { 108 | "#type": "DefineSpriteTag", 109 | "#spriteId": "3", 110 | "subTags": [ 111 | { 112 | "#type": "PlaceObject2Tag", 113 | "#characterId": "2", 114 | "#depth": "1", 115 | "~placeFlagHasMatrix": "true" 116 | }, 117 | { 118 | "#type": "PlaceObject2Tag", 119 | "#characterId": "2", 120 | "#depth": "1", 121 | "matrix": { 122 | "~hasScale": "true", 123 | "~scaleX": "0", 124 | "~scaleY": "0" 125 | } 126 | } 127 | ] 128 | }, 129 | { 130 | "#type": "DefineEditTextTag", 131 | "#characterID": "5", 132 | "textColor": { 133 | "~type": "RGBA", 134 | "~alpha": "255", 135 | "~blue": "255", 136 | "~green": "255", 137 | "~red": "255" 138 | } 139 | } 140 | ] 141 | } 142 | } 143 | ``` 144 | 145 | # Patcher Commandline Usage 146 | 147 | ```bash 148 | Usage: DIP.exe [-h] [-d] [-b] [patchpath] [originalpath] 149 | 150 | Dynamic Interface Patcher (c) Cutleast 151 | 152 | Positional Arguments: 153 | patchpath Path to patch that gets automatically run. An original mod path must also be given! 154 | originalpath Path to original mod that gets automatically patched. A patch path must also be given! 155 | 156 | Options: 157 | -h, --help Show this help message and exit 158 | -d, --debug Enables debug mode so that debug files get outputted. 159 | -b, --repack-bsa Enables experimental repacking of original BSA file(s). 160 | ``` 161 | -------------------------------------------------------------------------------- /Example Patch/Patch/RaceMenu.bsa/interface/racemenu/buttonart.json: -------------------------------------------------------------------------------- 1 | { 2 | "swf": { 3 | "displayRect": { 4 | "~Xmax": "25600", 5 | "~Xmin": "0", 6 | "~Ymax": "14400", 7 | "~Ymin": "0" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Example Patch/Patch/RaceMenu.bsa/interface/racesex_menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "shapes": [ 3 | { 4 | "id": "1,2,5,7,9", 5 | "fileName": "example.svg" 6 | } 7 | ], 8 | // '#' for filters | '~' for changes | '' for parent elements 9 | "swf": { 10 | "displayRect": { 11 | "~Xmax": "25600", 12 | "~Xmin": "0", 13 | "~Ymax": "14400", 14 | "~Ymin": "0" 15 | }, 16 | "tags": [ 17 | { 18 | "#type": "DefineSpriteTag", 19 | "#spriteId": "3", 20 | "subTags": [ 21 | { 22 | "#type": "PlaceObject2Tag", 23 | "#characterId": "2", 24 | "#depth": "1", 25 | "~placeFlagHasMatrix": "true" 26 | }, 27 | { 28 | "#type": "PlaceObject2Tag", 29 | "#characterId": "2", 30 | "#depth": "1", 31 | "matrix": { 32 | "~hasScale": "true", 33 | "~scaleX": "0", 34 | "~scaleY": "0" 35 | } 36 | } 37 | ] 38 | }, 39 | { 40 | "#type": "DefineEditTextTag", 41 | "#characterID": "5", 42 | "textColor": { 43 | "~type": "RGBA", 44 | "~alpha": "255", 45 | "~blue": "255", 46 | "~green": "255", 47 | "~red": "255" 48 | } 49 | } 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /OfficialPatches.md: -------------------------------------------------------------------------------- 1 | # Released 2 | 3 | ### RaceMenu 4 | * [Nordic UI](https://www.nexusmods.com/skyrimspecialedition/mods/97348) 5 | * [Horizons UI (Transparent & Opaque)](https://www.nexusmods.com/skyrimspecialedition/mods/97354) 6 | * [Dear Diary Dark Mode (White & Warm Text)](https://www.nexusmods.com/skyrimspecialedition/mods/97349) 7 | * [Dear Diary (Light Mode)](https://www.nexusmods.com/skyrimspecialedition/mods/97355) 8 | * [Untarnished UI](https://www.nexusmods.com/skyrimspecialedition/mods/97347) 9 | 10 | ### Minimap 11 | * [Nordic UI](https://www.nexusmods.com/skyrimspecialedition/mods/97356) 12 | * [Untarnished UI](https://www.nexusmods.com/skyrimspecialedition/mods/97357) 13 | 14 | # Work in Progress 15 | 16 | ### RaceMenu 17 | * Edge UI 18 | * New Horizons UI 19 | 20 | # Planned/Coming soon 21 | None (see below) 22 | 23 | # Cancelled/Discontinued indefinitely 24 | Due to other projects in our timeline, we will not be able to make these patches available in the near future. Feel free to create one or more of them, but make sure to notify us so we can remove the patch from this list. 25 | 26 | ### RaceMenu 27 | * Dragonbreaker UI 28 | 29 | ### Minimap 30 | * Dear Diary Dark Mode 31 | * Dear Diary 32 | * Horizons UI 33 | 34 | ### Enhanced Character Edit (ECE) <-- maybe 35 | * NORDIC UI 36 | * Untarnished UI 37 | * Dear Diary Dark Mode 38 | * Dear Diary 39 | 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |
4 | 5 | 6 |
7 | 8 | # Please Note!!! 9 | 10 | **I take no responsibility for any problems that may occur or any assets that get redistributed without permission!** 11 | 12 | # Description 13 | 14 | This is a dynamic patching tool for ui mods with strict permissions like RaceMenu or MiniMap. 15 | **No assets or files by the original mod authors get redistributed! Patching takes place exclusively locally and redistribution of the patched files is strictly prohibited according to the respective permissions.** 16 | The tool requires a compatible patch to work. Those can be found on the Skyrim nexus on Nexus Mods by searching for something like "DIP Patch". 17 | More info on creating patches can be found in the [documentation](./DOCUMENTATION.md). 18 | 19 | # Features 20 | 21 | - Fully automated patching 22 | - Automatic extraction of BSA 23 | - Can be installed as a mod in MO2 or Vortex 24 | - Commandline arguments for auto patching 25 | 26 | # Official Patches 27 | 28 | See [here](./OfficialPatches.md) for a list of released and planned patches. 29 | 30 | # Contributing 31 | 32 | ### 1. Feedback (Suggestions/Issues) 33 | 34 | If you encountered an issue/error or you have a suggestion, create an issue under the "Issues" tab above. 35 | 36 | ### 2. Code contributions 37 | 38 | 1. Install Python 3.11 (Make sure that you add it to PATH!) 39 | 2. Clone repository 40 | 3. Open terminal in repository folder 41 | 4. Type in following command to install all requirements (Using a virtual environment is strongly recommended!): 42 | `pip install -r requirements.txt` 43 | 44 | ### 3. Execute from source 45 | 46 | 1. Open terminal in src folder 47 | 2. Execute main file 48 | `python main.py` 49 | 50 | ### 4. Compile and build executable 51 | 52 | 1. Follow the steps on this page [Nuitka.net](https://nuitka.net/doc/user-manual.html#usage) to install a C Compiler 53 | 2. Run `build.bat` with activated virtual environment from the root folder of this repo. 54 | 3. The executable and all dependencies are built in the main.dist-Folder. 55 | 56 | # How it works 57 | 58 | 1. Copy original mod to a temp folder. (Extract BSAs if required) 59 | 2. Patch shapes. 60 | 3. Convert SWFs to XMLs. 61 | 4. Patch XMLs. 62 | 5. Convert XMLs back to SWFs. 63 | 6. Copy patched mod back to current directory. 64 | 65 | # Credits 66 | 67 | - Qt by The [Qt Company Ltd](https://qt.io) 68 | - [bethesda-structs](https://github.com/stephen-bunn/bethesda-structs) by [Stephen Bunn](https://github.com/stephen-bunn) 69 | - [FFDec](https://github.com/jindrapetrik/jpexs-decompiler) by [Jindra Petřík](https://github.com/jindrapetrik) 70 | -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | python build.py -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | import os 6 | import shutil 7 | from pathlib import Path 8 | from xml.etree import ElementTree as ET 9 | 10 | # Application details 11 | APPNAME = "Dynamic Interface Patcher" 12 | VERSION = "2.1.5" 13 | AUTHOR = "Cutleast" 14 | LICENSE = "GNU General Public License v3.0" 15 | DIST_FOLDER = Path("main.dist").resolve() 16 | FOMOD_FOLDER = Path("fomod").resolve() 17 | OUTPUT_FOLDER = Path("DIP_with_fomod").resolve() / "fomod" 18 | OBSOLETE_ITEMS: list[Path] = [DIST_FOLDER / "lib" / "qtpy" / "tests"] 19 | CONSOLE_MODE = "force" # "attach": Attaches to console it was started with (if any), "force": starts own console window, "disable": disables console completely 20 | ADDITIONAL_ITEMS: dict[Path, Path] = { 21 | Path("res") / "7-zip": DIST_FOLDER / "7-zip", 22 | Path("res") / "ffdec": DIST_FOLDER / "ffdec", 23 | Path("res") / "jre.7z": DIST_FOLDER / "jre.7z", 24 | Path("res") / "xdelta": DIST_FOLDER / "xdelta", 25 | Path("res") / "glob.dll": DIST_FOLDER / "glob.dll", 26 | } 27 | 28 | cmd = f'.venv\\scripts\\nuitka \ 29 | --msvc="latest" \ 30 | --standalone \ 31 | --windows-console-mode={CONSOLE_MODE} \ 32 | --enable-plugin=pyside6 \ 33 | --remove-output \ 34 | --company-name="{AUTHOR}" \ 35 | --product-name="{APPNAME}" \ 36 | --file-version="{VERSION.split("-")[0]}" \ 37 | --product-version="{VERSION.split("-")[0]}" \ 38 | --file-description="{APPNAME}" \ 39 | --copyright="{LICENSE}" \ 40 | --nofollow-import-to=tkinter \ 41 | --windows-icon-from-ico="./res/icons/icon.ico" \ 42 | --output-filename="DIP.exe" \ 43 | "./src/main.py"' 44 | 45 | if DIST_FOLDER.is_dir(): 46 | shutil.rmtree(DIST_FOLDER) 47 | print("Deleted dist folder.") 48 | 49 | os.system(cmd) 50 | 51 | print(f"Copying {len(ADDITIONAL_ITEMS)} additional item(s)...") 52 | for item, dest in ADDITIONAL_ITEMS.items(): 53 | if item.is_dir(): 54 | shutil.copytree(item, dest, dirs_exist_ok=True, copy_function=os.link) 55 | elif item.is_file(): 56 | os.makedirs(dest.parent, exist_ok=True) 57 | os.link(item, dest) 58 | else: 59 | print(f"{str(item)!r} does not exist!") 60 | continue 61 | 62 | print(f"Copied {str(item)!r} to {str(dest.relative_to(DIST_FOLDER))!r}.") 63 | 64 | for item in OBSOLETE_ITEMS: 65 | if item.is_file(): 66 | os.remove(item) 67 | elif item.is_dir(): 68 | shutil.rmtree(item) 69 | 70 | print(f"Removed item {str(item.relative_to(DIST_FOLDER))!r} from dist folder.") 71 | 72 | print("Packing with FOMOD...") 73 | if OUTPUT_FOLDER.is_dir(): 74 | shutil.rmtree(OUTPUT_FOLDER) 75 | print("Deleted already existing output folder.") 76 | 77 | print("Copying FOMOD...") 78 | shutil.copytree(FOMOD_FOLDER, OUTPUT_FOLDER, dirs_exist_ok=True) 79 | 80 | 81 | def update_fomod_version(info_xml_path: Path, new_version: str) -> None: 82 | if not info_xml_path.is_file(): 83 | print(f"The file {info_xml_path} does not exist!") 84 | return 85 | 86 | try: 87 | tree = ET.parse(info_xml_path) 88 | root = tree.getroot() 89 | 90 | version_element = root.find(".//Version") 91 | if version_element is None: 92 | print(f"Found no element in {info_xml_path}.") 93 | return 94 | 95 | version_element.text = new_version 96 | tree.write(info_xml_path, encoding="utf-8", xml_declaration=True) 97 | 98 | print(f"Updated version in {info_xml_path} to {new_version}.") 99 | except ET.ParseError as e: 100 | print(f"Failed to parse {info_xml_path}: {e}") 101 | 102 | 103 | update_fomod_version(OUTPUT_FOLDER / "info.xml", VERSION) 104 | 105 | print("Copying DIP...") 106 | shutil.copytree(DIST_FOLDER, OUTPUT_FOLDER / "DIP", dirs_exist_ok=True) 107 | 108 | print("Packing into 7-zip archive...") 109 | if Path(f"DIP_v{VERSION}.7z").is_file(): 110 | os.remove(f"DIP_v{VERSION}.7z") 111 | print("Deleted already existing 7-zip archive.") 112 | 113 | cmd = f"res\\7-zip\\7z.exe \ 114 | a \ 115 | DIP_v{VERSION}.7z \ 116 | {OUTPUT_FOLDER}" 117 | os.system(cmd) 118 | 119 | print("Done!") 120 | -------------------------------------------------------------------------------- /fomod/Image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/fomod/Image.jpg -------------------------------------------------------------------------------- /fomod/ModuleConfig.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/fomod/ModuleConfig.xml -------------------------------------------------------------------------------- /fomod/info.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/fomod/info.xml -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # GUI 2 | qtawesome 3 | PySide6 4 | QtPy 5 | 6 | # Distribution 7 | cx_freeze # seems to be fine with AV 8 | nuitka # severe issues with AV (trojan) 9 | pyinstaller==6.1 # 3 detecions on VT 10 | pyinstaller-versionfile # for versioninfo 11 | 12 | # File System and Archiving 13 | lz4 14 | virtual_glob 15 | rarfile 16 | py7zr 17 | 18 | # Utilities 19 | jstyleson 20 | pyperclip 21 | -------------------------------------------------------------------------------- /res/7-zip/7z.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/7-zip/7z.dll -------------------------------------------------------------------------------- /res/7-zip/7z.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/7-zip/7z.exe -------------------------------------------------------------------------------- /res/7-zip/7z_License.txt: -------------------------------------------------------------------------------- 1 | 7-Zip 2 | ~~~~~ 3 | License for use and distribution 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | 7-Zip Copyright (C) 1999-2023 Igor Pavlov. 7 | 8 | The licenses for files are: 9 | 10 | 1) 7z.dll: 11 | - The "GNU LGPL" as main license for most of the code 12 | - The "GNU LGPL" with "unRAR license restriction" for some code 13 | - The "BSD 3-clause License" for some code 14 | 2) All other files: the "GNU LGPL". 15 | 16 | Redistributions in binary form must reproduce related license information from this file. 17 | 18 | Note: 19 | You can use 7-Zip on any computer, including a computer in a commercial 20 | organization. You don't need to register or pay for 7-Zip. 21 | 22 | 23 | GNU LGPL information 24 | -------------------- 25 | 26 | This library is free software; you can redistribute it and/or 27 | modify it under the terms of the GNU Lesser General Public 28 | License as published by the Free Software Foundation; either 29 | version 2.1 of the License, or (at your option) any later version. 30 | 31 | This library is distributed in the hope that it will be useful, 32 | but WITHOUT ANY WARRANTY; without even the implied warranty of 33 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 34 | Lesser General Public License for more details. 35 | 36 | You can receive a copy of the GNU Lesser General Public License from 37 | http://www.gnu.org/ 38 | 39 | 40 | 41 | 42 | BSD 3-clause License 43 | -------------------- 44 | 45 | The "BSD 3-clause License" is used for the code in 7z.dll that implements LZFSE data decompression. 46 | That code was derived from the code in the "LZFSE compression library" developed by Apple Inc, 47 | that also uses the "BSD 3-clause License": 48 | 49 | ---- 50 | Copyright (c) 2015-2016, Apple Inc. All rights reserved. 51 | 52 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 53 | 54 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 55 | 56 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer 57 | in the documentation and/or other materials provided with the distribution. 58 | 59 | 3. Neither the name of the copyright holder(s) nor the names of any contributors may be used to endorse or promote products derived 60 | from this software without specific prior written permission. 61 | 62 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 63 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 64 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 65 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 66 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 67 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 68 | ---- 69 | 70 | 71 | 72 | 73 | unRAR license restriction 74 | ------------------------- 75 | 76 | The decompression engine for RAR archives was developed using source 77 | code of unRAR program. 78 | All copyrights to original unRAR code are owned by Alexander Roshal. 79 | 80 | The license for original unRAR code has the following restriction: 81 | 82 | The unRAR sources cannot be used to re-create the RAR compression algorithm, 83 | which is proprietary. Distribution of modified unRAR sources in separate form 84 | or as a part of other software is permitted, provided that it is clearly 85 | stated in the documentation and source comments that the code may 86 | not be used to develop a RAR (WinRAR) compatible archiver. 87 | 88 | 89 | -- 90 | Igor Pavlov 91 | -------------------------------------------------------------------------------- /res/7-zip/7z_readme.txt: -------------------------------------------------------------------------------- 1 | 7-Zip 23.01 2 | ----------- 3 | 4 | 7-Zip is a file archiver for Windows. 5 | 6 | 7-Zip Copyright (C) 1999-2023 Igor Pavlov. 7 | 8 | The main features of 7-Zip: 9 | 10 | - High compression ratio in the new 7z format 11 | - Supported formats: 12 | - Packing / unpacking: 7z, XZ, BZIP2, GZIP, TAR, ZIP and WIM. 13 | - Unpacking only: APFS, AR, ARJ, Base64, CAB, CHM, CPIO, CramFS, DMG, EXT, FAT, GPT, HFS, 14 | IHEX, ISO, LZH, LZMA, MBR, MSI, NSIS, NTFS, QCOW2, RAR, 15 | RPM, SquashFS, UDF, UEFI, VDI, VHD, VHDX, VMDK, XAR and Z. 16 | - Fast compression and decompression 17 | - Self-extracting capability for 7z format 18 | - Strong AES-256 encryption in 7z and ZIP formats 19 | - Integration with Windows Shell 20 | - Powerful File Manager 21 | - Powerful command line version 22 | - Localizations for 90 languages 23 | 24 | 25 | 7-Zip is free software distributed under the GNU LGPL (except for unRar code). 26 | Read License.txt for more information about license. 27 | 28 | 29 | This distribution package contains the following files: 30 | 31 | 7zFM.exe - 7-Zip File Manager 32 | 7-zip.dll - Plugin for Windows Shell 33 | 7-zip32.dll - Plugin for Windows Shell (32-bit plugin for 64-bit system) 34 | 7zg.exe - GUI module 35 | 7z.exe - Command line version 36 | 7z.dll - 7-Zip engine module 37 | 7z.sfx - SFX module (Windows version) 38 | 7zCon.sfx - SFX module (Console version) 39 | 40 | License.txt - License information 41 | readme.txt - This file 42 | History.txt - History of 7-Zip 43 | 7-zip.chm - User's Manual in HTML Help format 44 | descript.ion - Description for files 45 | 46 | Lang\en.ttt - English (base) localization file 47 | Lang\*.txt - Localization files 48 | 49 | 50 | --- 51 | End of document 52 | -------------------------------------------------------------------------------- /res/default_configs/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "debug_mode": false, 3 | "repack_bsa": false, 4 | "silent": false, 5 | "output_folder": null 6 | } -------------------------------------------------------------------------------- /res/ffdec/ffdec.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/ffdec.jar -------------------------------------------------------------------------------- /res/ffdec/ffdec_orig.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | rem This is a comment, it starts with "rem". 3 | 4 | rem Set following to higher value if you want more memory: 5 | rem You need 64 bit OS and 64 bit java to set it to higher values 6 | set MEMORY=256m 7 | 8 | rem Uncomment following when you encounter StackOverFlowErrors. 9 | rem If the app then terminates with OutOfMemory you can experiment with lower value. 10 | rem set STACK_SIZE=32m 11 | 12 | rem Hide VLC error output 13 | set VLC_VERBOSE=-1 14 | 15 | if not "%STACK_SiZE%"=="" set STACK_SIZE_PARAM= -Xss%STACK_SiZE% 16 | if not "%MEMORY%"=="" set MEMORY_PARAM=-Xmx%MEMORY% 17 | 18 | java %MEMORY_PARAM%%STACK_SIZE_PARAM%-Djna.nosys=true -jar "%~dp0\ffdec.jar" %* 19 | -------------------------------------------------------------------------------- /res/ffdec/flashlib/airglobal.swc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/flashlib/airglobal.swc -------------------------------------------------------------------------------- /res/ffdec/flashlib/playerglobal32_0.swc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/flashlib/playerglobal32_0.swc -------------------------------------------------------------------------------- /res/ffdec/lib/JavactiveX.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/JavactiveX.jar -------------------------------------------------------------------------------- /res/ffdec/lib/LZMA.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/LZMA.jar -------------------------------------------------------------------------------- /res/ffdec/lib/avi.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/avi.jar -------------------------------------------------------------------------------- /res/ffdec/lib/avi.montemedia.license.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/avi.montemedia.license.txt -------------------------------------------------------------------------------- /res/ffdec/lib/cmykjpeg.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/cmykjpeg.jar -------------------------------------------------------------------------------- /res/ffdec/lib/ddsreader.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/ddsreader.jar -------------------------------------------------------------------------------- /res/ffdec/lib/ffdec_lib.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/ffdec_lib.jar -------------------------------------------------------------------------------- /res/ffdec/lib/ffdec_lib.license.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /res/ffdec/lib/flamingo-6.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/flamingo-6.2.jar -------------------------------------------------------------------------------- /res/ffdec/lib/flamingo.license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2005-2010 Flamingo Kirill Grouchnikov. All Rights Reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | o Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | 9 | o Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | o Neither the name of Flamingo Kirill Grouchnikov nor the names of 14 | its contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 19 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 20 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 21 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 22 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 23 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 24 | OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 25 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 26 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 27 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /res/ffdec/lib/flashdebugger.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/flashdebugger.jar -------------------------------------------------------------------------------- /res/ffdec/lib/gif.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/gif.jar -------------------------------------------------------------------------------- /res/ffdec/lib/gif.license.txt: -------------------------------------------------------------------------------- 1 | Created by Elliot Kroo on 2009-04-25. 2 | 3 | This work is licensed under the Creative Commons Attribution 3.0 Unported 4 | License. To view a copy of this license, visit 5 | http://creativecommons.org/licenses/by/3.0/ or send a letter to Creative 6 | Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA. -------------------------------------------------------------------------------- /res/ffdec/lib/gnujpdf.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/gnujpdf.jar -------------------------------------------------------------------------------- /res/ffdec/lib/jargs.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/jargs.jar -------------------------------------------------------------------------------- /res/ffdec/lib/jlayer-1.0.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/jlayer-1.0.2.jar -------------------------------------------------------------------------------- /res/ffdec/lib/jlayer.license.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993, 1994 Tobias Bading (bading@cs.tu-berlin.de) 2 | Berlin University of Technology 3 | ----------------------------------------------------------------------- 4 | This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU Library General Public License as published 6 | by the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Library General Public License for more details. 13 | 14 | You should have received a copy of the GNU Library General Public 15 | License along with this program; if not, write to the Free Software 16 | Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 17 | ---------------------------------------------------------------------- -------------------------------------------------------------------------------- /res/ffdec/lib/jna-3.5.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/jna-3.5.1.jar -------------------------------------------------------------------------------- /res/ffdec/lib/jna-platform-3.5.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/jna-platform-3.5.1.jar -------------------------------------------------------------------------------- /res/ffdec/lib/jna.license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007 Timothy Wall, All Rights Reserved 2 | 3 | This library is free software; you can redistribute it and/or 4 | modify it under the terms of the GNU Lesser General Public 5 | License as published by the Free Software Foundation; either 6 | version 2.1 of the License, or (at your option) any later version. 7 | 8 | This library is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | Lesser General Public License for more details. 12 | -------------------------------------------------------------------------------- /res/ffdec/lib/jpacker.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/jpacker.jar -------------------------------------------------------------------------------- /res/ffdec/lib/jpacker.license.txt: -------------------------------------------------------------------------------- 1 | Packer version 3.0 (final) 2 | Copyright 2004-2007, Dean Edwards 3 | Web: http://dean.edwards.name/ 4 | 5 | This software is licensed under the MIT license 6 | Web: http://www.opensource.org/licenses/mit-license 7 | 8 | Ported to Java by Pablo Santiago based on C# version by Jesse Hansen, 9 | Web: http://jpacker.googlecode.com/ 10 | Email: pablo.santiago@gmail.com 11 | -------------------------------------------------------------------------------- /res/ffdec/lib/jpproxy.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/jpproxy.jar -------------------------------------------------------------------------------- /res/ffdec/lib/jpproxy.muffin.license.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 1996-2003 Mark R. Boyns 2 | 3 | Muffin is free software; you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation; either version 2 of the License, or 6 | (at your option) any later version. 7 | 8 | Muffin is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU General Public License for more details. 12 | 13 | You should have received a copy of the GNU General Public License 14 | along with Muffin; see the file COPYING. If not, write to the 15 | Free Software Foundation, Inc., 16 | 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. -------------------------------------------------------------------------------- /res/ffdec/lib/jsyntaxpane-0.9.5.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/jsyntaxpane-0.9.5.jar -------------------------------------------------------------------------------- /res/ffdec/lib/jsyntaxpane.license.txt: -------------------------------------------------------------------------------- 1 | Copyright 2008 Ayman Al-Sairafi ayman.alsairafi@gmail.com 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License 6 | at http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. -------------------------------------------------------------------------------- /res/ffdec/lib/minimal-json-0.9.5.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/minimal-json-0.9.5.jar -------------------------------------------------------------------------------- /res/ffdec/lib/minimal-json.license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, 2014 EclipseSource 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /res/ffdec/lib/nellymoser.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/nellymoser.jar -------------------------------------------------------------------------------- /res/ffdec/lib/nellymoser.license.txt: -------------------------------------------------------------------------------- 1 | NellyMoser ASAO codec 2 | Copyright (C) 2007-2008 UAB "DKD" 3 | Copyright (C) 2007-2008 Joseph Artsimovich 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 2.1 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with FFmpeg; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -------------------------------------------------------------------------------- /res/ffdec/lib/sfntly.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/sfntly.jar -------------------------------------------------------------------------------- /res/ffdec/lib/sfntly.license.txt: -------------------------------------------------------------------------------- 1 | Copyright 2010 Google Inc. All Rights Reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /res/ffdec/lib/substance-6.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/substance-6.2.jar -------------------------------------------------------------------------------- /res/ffdec/lib/substance-flamingo-6.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/substance-flamingo-6.2.jar -------------------------------------------------------------------------------- /res/ffdec/lib/substance-flamingo.license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2005-2010 Flamingo / Substance Kirill Grouchnikov. All Rights Reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | o Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | 9 | o Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | o Neither the name of Flamingo Kirill Grouchnikov nor the names of 14 | its contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 19 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 20 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 21 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 22 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 23 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 24 | OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 25 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 26 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 27 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /res/ffdec/lib/substance.license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2005-2010, Kirill Grouchnikov and contributors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of the Kirill Grouchnikov and contributors nor 13 | the names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 18 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 19 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 20 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 26 | THE POSSIBILITY OF SUCH DAMAGE. 27 | 28 | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 29 | 30 | The original artwork used in the Quaqua color chooser implementation 31 | has been replaced by images from Famfam Silk collection available 32 | under Creative Commons Attribution-ShareAlike 2.5 license and 33 | images created dynamically by the Substance core code. 34 | -------------------------------------------------------------------------------- /res/ffdec/lib/tablelayout.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/tablelayout.jar -------------------------------------------------------------------------------- /res/ffdec/lib/tga.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/tga.jar -------------------------------------------------------------------------------- /res/ffdec/lib/treetable.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/treetable.jar -------------------------------------------------------------------------------- /res/ffdec/lib/trident-6.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/trident-6.2.jar -------------------------------------------------------------------------------- /res/ffdec/lib/trident.license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2005-2010 Trident Kirill Grouchnikov. All Rights Reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | o Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | 9 | o Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | o Neither the name of Trident Kirill Grouchnikov nor the names of 14 | its contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 19 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 20 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 21 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 22 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 23 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 24 | OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 25 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 26 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 27 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /res/ffdec/lib/ttf.doubletype.license.txt: -------------------------------------------------------------------------------- 1 | This program is free software; you can redistribute it and/or modify 2 | it under the terms of the GNU General Public License as published by 3 | the Free Software Foundation; either version 2 of the License, or 4 | (at your option) any later version. 5 | 6 | This Program is distributed in the hope that it will be useful, 7 | but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | GNU General Public License for more details. 10 | 11 | You should have received a copy of the GNU General Public License 12 | along with this program; if not, write to the Free Software 13 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 14 | 15 | In addition, as a special exception, e.e d3si9n gives permission to 16 | link the code of this program with any Java Platform that is available 17 | to public with free of charge, including but not limited to 18 | Sun Microsystem's JAVA(TM) 2 RUNTIME ENVIRONMENT (J2RE), 19 | and distribute linked combinations including the two. 20 | You must obey the GNU General Public License in all respects for all 21 | of the code used other than Java Platform. If you modify this file, 22 | you may extend this exception to your version of the file, but you are not 23 | obligated to do so. If you do not wish to do so, delete this exception 24 | statement from your version. -------------------------------------------------------------------------------- /res/ffdec/lib/ttf.fontastic.license.txt: -------------------------------------------------------------------------------- 1 | Fontastic 2 | A font file writer to create TTF and WOFF (Webfonts). 3 | http://code.andreaskoller.com/libraries/fontastic 4 | 5 | Copyright (C) 2013 Andreas Koller http://andreaskoller.com 6 | 7 | This library is free software; you can redistribute it and/or 8 | modify it under the terms of the GNU Lesser General Public 9 | License as published by the Free Software Foundation; either 10 | version 2.1 of the License, or (at your option) any later version. 11 | 12 | This library is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | Lesser General Public License for more details. 16 | 17 | You should have received a copy of the GNU Lesser General 18 | Public License along with this library; if not, write to the 19 | Free Software Foundation, Inc., 59 Temple Place, Suite 330, 20 | Boston, MA 02111-1307 USA -------------------------------------------------------------------------------- /res/ffdec/lib/ttf.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/ttf.jar -------------------------------------------------------------------------------- /res/ffdec/lib/vlcj-4.7.3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/vlcj-4.7.3.jar -------------------------------------------------------------------------------- /res/ffdec/lib/vlcj-natives-4.7.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/ffdec/lib/vlcj-natives-4.7.0.jar -------------------------------------------------------------------------------- /res/glob.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/glob.dll -------------------------------------------------------------------------------- /res/glob/dllmain.cpp: -------------------------------------------------------------------------------- 1 | // Thanks to SkyHorizon for the code! Check out his profile on GitHub: https://github.com/SkyHorizon3 2 | 3 | #include "pch.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define DLLEXPORT extern "C" __declspec(dllexport) 11 | 12 | BOOL APIENTRY DllMain(HMODULE hModule, 13 | DWORD ul_reason_for_call, 14 | LPVOID lpReserved) 15 | { 16 | switch (ul_reason_for_call) 17 | { 18 | case DLL_PROCESS_ATTACH: 19 | case DLL_THREAD_ATTACH: 20 | case DLL_THREAD_DETACH: 21 | case DLL_PROCESS_DETACH: 22 | break; 23 | } 24 | return TRUE; 25 | } 26 | 27 | std::string pattern_to_regex(const std::string &pattern) 28 | { 29 | std::string regex_pattern; 30 | for (char c : pattern) 31 | { 32 | switch (c) 33 | { 34 | case '*': 35 | regex_pattern += ".*"; 36 | break; 37 | case '?': 38 | regex_pattern += "."; 39 | break; 40 | case '.': 41 | regex_pattern += "\\."; 42 | break; 43 | default: 44 | regex_pattern += c; 45 | } 46 | } 47 | return regex_pattern; 48 | } 49 | 50 | static std::vector stringResult; 51 | static std::vector returnValues; 52 | 53 | DLLEXPORT void glob_clear() 54 | { 55 | returnValues.clear(); 56 | stringResult.clear(); 57 | } 58 | 59 | DLLEXPORT const char **glob_cpp(const char *pattern, const char *basePath, bool recursive, size_t *out_size) 60 | { 61 | glob_clear(); 62 | 63 | std::regex pattern_regex(pattern_to_regex(pattern)); 64 | 65 | auto checkFile = [&pattern_regex](const std::filesystem::directory_entry &entry) 66 | { 67 | if (entry.is_regular_file()) 68 | { 69 | const auto &fileName = entry.path().filename().string(); 70 | return std::regex_match(fileName, pattern_regex); 71 | } 72 | return false; 73 | }; 74 | 75 | if (recursive) 76 | { 77 | for (const auto &entry : std::filesystem::recursive_directory_iterator(basePath)) 78 | { 79 | if (checkFile(entry)) 80 | { 81 | stringResult.emplace_back(entry.path().string()); 82 | } 83 | } 84 | } 85 | else 86 | { 87 | for (const auto &entry : std::filesystem::directory_iterator(basePath)) 88 | { 89 | if (checkFile(entry)) 90 | { 91 | stringResult.emplace_back(entry.path().string()); 92 | } 93 | } 94 | } 95 | 96 | for (const auto &str : stringResult) 97 | { 98 | returnValues.emplace_back(str.c_str()); 99 | } 100 | 101 | *out_size = returnValues.size(); 102 | return returnValues.data(); 103 | } -------------------------------------------------------------------------------- /res/glob/framework.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define WIN32_LEAN_AND_MEAN 4 | 5 | #include 6 | -------------------------------------------------------------------------------- /res/glob/glob_cpp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.12.35527.113 d17.12 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "glob_cpp", "glob_cpp.vcxproj", "{EC716006-FF99-42C6-ACFD-56E37542C438}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x64 = Debug|x64 11 | Debug|x86 = Debug|x86 12 | Release|x64 = Release|x64 13 | Release|x86 = Release|x86 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {EC716006-FF99-42C6-ACFD-56E37542C438}.Debug|x64.ActiveCfg = Debug|x64 17 | {EC716006-FF99-42C6-ACFD-56E37542C438}.Debug|x64.Build.0 = Debug|x64 18 | {EC716006-FF99-42C6-ACFD-56E37542C438}.Debug|x86.ActiveCfg = Debug|Win32 19 | {EC716006-FF99-42C6-ACFD-56E37542C438}.Debug|x86.Build.0 = Debug|Win32 20 | {EC716006-FF99-42C6-ACFD-56E37542C438}.Release|x64.ActiveCfg = Release|x64 21 | {EC716006-FF99-42C6-ACFD-56E37542C438}.Release|x64.Build.0 = Release|x64 22 | {EC716006-FF99-42C6-ACFD-56E37542C438}.Release|x86.ActiveCfg = Release|Win32 23 | {EC716006-FF99-42C6-ACFD-56E37542C438}.Release|x86.Build.0 = Release|Win32 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /res/glob/glob_cpp.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 17.0 23 | Win32Proj 24 | {ec716006-ff99-42c6-acfd-56e37542c438} 25 | globcpp 26 | 10.0 27 | 28 | 29 | 30 | DynamicLibrary 31 | true 32 | v143 33 | Unicode 34 | 35 | 36 | DynamicLibrary 37 | false 38 | v143 39 | true 40 | Unicode 41 | 42 | 43 | DynamicLibrary 44 | true 45 | v143 46 | Unicode 47 | 48 | 49 | DynamicLibrary 50 | false 51 | v143 52 | true 53 | Unicode 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | Level3 76 | true 77 | WIN32;_DEBUG;GLOBCPP_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 78 | true 79 | Use 80 | pch.h 81 | 82 | 83 | Windows 84 | true 85 | false 86 | 87 | 88 | 89 | 90 | Level3 91 | true 92 | true 93 | true 94 | WIN32;NDEBUG;GLOBCPP_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 95 | true 96 | Use 97 | pch.h 98 | 99 | 100 | Windows 101 | true 102 | true 103 | true 104 | false 105 | 106 | 107 | 108 | 109 | Level3 110 | true 111 | _DEBUG;GLOBCPP_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 112 | true 113 | Use 114 | pch.h 115 | stdc17 116 | true 117 | stdcpp17 118 | 119 | 120 | Windows 121 | true 122 | false 123 | 124 | 125 | 126 | 127 | Level3 128 | true 129 | true 130 | true 131 | NDEBUG;GLOBCPP_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 132 | true 133 | Use 134 | pch.h 135 | stdcpp17 136 | stdc17 137 | 138 | 139 | Windows 140 | true 141 | true 142 | true 143 | false 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | Create 154 | Create 155 | Create 156 | Create 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /res/glob/pch.cpp: -------------------------------------------------------------------------------- 1 | #include "pch.h" 2 | -------------------------------------------------------------------------------- /res/glob/pch.h: -------------------------------------------------------------------------------- 1 | #ifndef PCH_H 2 | #define PCH_H 3 | 4 | #include "framework.h" 5 | 6 | #endif //PCH_H 7 | -------------------------------------------------------------------------------- /res/icons/arrow_down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/arrow_left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /res/icons/arrow_right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /res/icons/arrow_up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /res/icons/checkmark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/grip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/icons/grip.png -------------------------------------------------------------------------------- /res/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/icons/icon.ico -------------------------------------------------------------------------------- /res/jre.7z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/jre.7z -------------------------------------------------------------------------------- /res/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | default_configs/config.json 5 | icons/arrow_down.svg 6 | icons/arrow_left.svg 7 | icons/arrow_right.svg 8 | icons/arrow_up.svg 9 | icons/checkmark.svg 10 | icons/grip.png 11 | icons/icon.ico 12 | style.qss 13 | 14 | -------------------------------------------------------------------------------- /res/style.qss: -------------------------------------------------------------------------------- 1 | /* General */ 2 | QWidget, 3 | QWidget#regular { 4 | background: #08ffffff; 5 | color: #ffffff; 6 | font-size: 14px; 7 | border: 0px solid; 8 | border-radius: 8px; 9 | } 10 | QWidget#root, 11 | QWidget#primary, 12 | QMainWindow, 13 | QDialog { 14 | background: #202020; 15 | spacing: 15px; 16 | border-radius: 8px; 17 | } 18 | QWidget#secondary { 19 | background: #303030; 20 | border-radius: 8px; 21 | } 22 | QWidget#transparent { 23 | background: transparent; 24 | } 25 | QWidget:disabled { 26 | color: #66ffffff; 27 | } 28 | QWidget#warning { 29 | background: #a6ff3c00; 30 | } 31 | 32 | 33 | /* Menubar, Menu and Actions */ 34 | QMenuBar, 35 | QMenu { 36 | background: #202020; 37 | color: #ffffff; 38 | } 39 | QMenu::right-arrow { 40 | image: url(:/icons/arrow_right.svg); 41 | width: 24px; 42 | height: 24px; 43 | } 44 | QMenuBar::item { 45 | padding: 4px; 46 | margin: 4px; 47 | background: transparent; 48 | border-radius: 4px; 49 | } 50 | QMenuBar::item:selected:!disabled { 51 | background: #1cffffff; 52 | } 53 | QMenuBar::item:disabled { 54 | color: #66ffffff; 55 | } 56 | QMenu { 57 | padding: 4px; 58 | border-radius: 8px; 59 | } 60 | QMenu::separator, 61 | QToolBar::separator { 62 | background: #1cffffff; 63 | margin: 3px; 64 | } 65 | QMenu::separator { 66 | height: 1px; 67 | } 68 | QToolBar::separator { 69 | width: 1px; 70 | } 71 | QMenu::item { 72 | background: transparent; 73 | color: #ffffff; 74 | border-radius: 4px; 75 | margin: 2px; 76 | padding: 6px; 77 | } 78 | QMenu::item:selected:!disabled { 79 | background: #1cffffff; 80 | } 81 | QMenu::item:disabled { 82 | color: #66ffffff; 83 | } 84 | QAction, 85 | QToolButton { 86 | background: #1cffffff; 87 | color: #ffffff; 88 | padding: 8px; 89 | border-radius: 8px; 90 | } 91 | QAction:selected:!disabled { 92 | background: #1cffffff; 93 | } 94 | QAction:disabled { 95 | color: #66ffffff; 96 | } 97 | QToolButton::menu-button:hover, 98 | QToolButton::menu-button:selected, 99 | QToolButton::menu-button:pressed { 100 | background: #0cffffff; 101 | } 102 | QToolButton::menu-arrow { 103 | image: url(:/icons/arrow_down.svg); 104 | } 105 | QCheckBox#menu_checkbox { 106 | padding: 8px; 107 | /* margin: 2px; */ 108 | } 109 | QCheckBox#menu_checkbox:hover { 110 | background: #1cffffff; 111 | } 112 | 113 | 114 | /* Labels */ 115 | QLabel { 116 | background: transparent; 117 | selection-background-color: #4994e0; 118 | } 119 | QLabel#warning_label { 120 | color: #fff08d3b; 121 | } 122 | QLabel#critical_label { 123 | color: #ffd12525; 124 | } 125 | QLabel#title_label { 126 | font-size: 34px; 127 | } 128 | QLabel#subtitle_label { 129 | font-size: 28px; 130 | } 131 | QLabel#relevant_label { 132 | font-size: 22px; 133 | } 134 | QLabel#status_label, QTextEdit#protocol { 135 | font-family: Consolas; 136 | font-size: 13px; 137 | } 138 | 139 | 140 | /* Buttons */ 141 | QPushButton, QToolButton { 142 | padding: 7px; 143 | margin: 2px; 144 | border: 1px solid #0cffffff; 145 | border-radius: 4px; 146 | } 147 | QPushButton#accent_button { 148 | background: #4994e0; 149 | color: #000000; 150 | } 151 | QToolButton#accent_button { 152 | border-bottom-color: #4994e0; 153 | } 154 | QPushButton#accent_button:hover { 155 | background: #63a2e2; 156 | } 157 | QPushButton#accent_button:disabled { 158 | background: #66ffffff; 159 | } 160 | QPushButton:hover, QToolButton:hover { 161 | background: #15ffffff; 162 | } 163 | QPushButton:pressed, QToolButton:pressed { 164 | background: transparent; 165 | } 166 | QPushButton:checked, QToolButton:checked { 167 | border-color: #4994e0; 168 | } 169 | QPushButton:checked:hover, QToolButton:checked:hover { 170 | border-color: #63a2e2; 171 | background: transparent; 172 | } 173 | QToolButton { 174 | background: transparent; 175 | border-color: transparent; 176 | width: 20px; 177 | height: 20px; 178 | } 179 | 180 | QLineEdit QPushButton { 181 | background: transparent; 182 | border-color: transparent; 183 | margin: 0px; 184 | } 185 | QLineEdit QPushButton:hover { 186 | background: #15ffffff; 187 | } 188 | QLineEdit QPushButton:checked { 189 | border-color: #4994e0; 190 | } 191 | 192 | 193 | /* Input Fields */ 194 | QSpinBox, 195 | QDoubleSpinBox, 196 | QLineEdit, 197 | QPlainTextEdit, 198 | QTextEdit, 199 | QDateTimeEdit, 200 | QComboBox { 201 | selection-background-color: #4994e0; 202 | padding: 9px; 203 | border-radius: 4px; 204 | border: 2px solid transparent; 205 | } 206 | QLineEdit:hover:!focus, 207 | QSpinBox:hover:!focus, 208 | QDoubleSpinBox:hover:!focus, 209 | QPlainTextEdit:editable:hover:!focus, 210 | QTextEdit:editable:hover:!focus, 211 | QDateTimeEdit:hover:!focus, 212 | QComboBox:editable:hover:!focus { 213 | background: #15ffffff; 214 | } 215 | QLineEdit:focus, 216 | QSpinBox:focus, 217 | QDoubleSpinBox:focus, 218 | QPlainTextEdit:editable:focus, 219 | QTextEdit:editable:focus, 220 | QDateTimeEdit:focus, 221 | QComboBox:editable:focus { 222 | border-bottom: 2px solid #4994e0; 223 | } 224 | QComboBox:!editable:hover { 225 | background: #1cffffff; 226 | } 227 | QTextEdit, QPlainTextEdit { 228 | border-radius: 8px; 229 | } 230 | 231 | 232 | /* Spinbox */ 233 | QSpinBox::up-button, QSpinBox::down-button, 234 | QDoubleSpinBox::up-button, QDoubleSpinBox::down-button { 235 | border: 0px; 236 | border-radius: 4px; 237 | } 238 | QSpinBox::up-button:hover, QSpinBox::down-button:hover, 239 | QDoubleSpinBox::up-button:hover, QDoubleSpinBox::down-button:hover { 240 | background: #1cffffff; 241 | } 242 | QSpinBox::up-button, 243 | QDoubleSpinBox::up-button { 244 | image: url(:/icons/arrow_up.svg); 245 | } 246 | QSpinBox::down-button, 247 | QDoubleSpinBox::down-button { 248 | image: url(:/icons/arrow_down.svg); 249 | } 250 | 251 | 252 | /* Dropdowns */ 253 | QComboBox::drop-down { 254 | subcontrol-origin: padding; 255 | subcontrol-position: right; 256 | padding-right: 8px; 257 | border-radius: 4px; 258 | border: 0px; 259 | } 260 | QComboBox::down-arrow { 261 | image: url(:/icons/arrow_down.svg); 262 | width: 24px; 263 | height: 24px; 264 | } 265 | QComboBox QAbstractItemView, 266 | QAbstractItemView#completer_popup { 267 | border-radius: 4px; 268 | border: 0px solid; 269 | background-color: #202020; 270 | color: #ffffff; 271 | padding: 4px; 272 | } 273 | QComboBox QAbstractItemView::item, 274 | QAbstractItemView#completer_popup::item { 275 | background-color: transparent; 276 | color: #ffffff; 277 | border-radius: 4px; 278 | margin: 2px; 279 | padding: 4px; 280 | } 281 | QComboBox QAbstractItemView::item:selected, 282 | QAbstractItemView#completer_popup::item:selected { 283 | background: #15ffffff; 284 | color: #4994e0; 285 | } 286 | 287 | QStatusBar { 288 | background: transparent; 289 | } 290 | 291 | 292 | /* Tooltips */ 293 | QToolTip { 294 | background: #202020; 295 | color: #ffffff; 296 | spacing: 5px; 297 | border: 0px; 298 | border-radius: 4px; 299 | margin: 5px; 300 | } 301 | 302 | 303 | /* Scrollbar */ 304 | QScrollBar { 305 | background: transparent; 306 | } 307 | QScrollBar, QScrollBar::handle { 308 | border-radius: 3px; 309 | width: 6px; 310 | } 311 | QScrollBar::handle { 312 | background: #15ffffff; 313 | min-height: 24px; 314 | } 315 | QScrollBar::handle:hover { 316 | background: #1cffffff; 317 | } 318 | QScrollBar::handle:pressed { 319 | background: #202020; 320 | } 321 | QScrollBar::up-arrow { 322 | image: url(:/icons/arrow_up.svg); 323 | } 324 | QScrollBar::down-arrow { 325 | image: url(:/icons/arrow_down.svg); 326 | } 327 | QScrollBar::add-line, 328 | QScrollBar::sub-line { 329 | width: 0px; 330 | height: 0px; 331 | } 332 | QScrollBar::add-page, 333 | QScrollBar::sub-page { 334 | background: transparent; 335 | } 336 | 337 | 338 | /* Progressbar */ 339 | QProgressBar { 340 | padding: 0px; 341 | height: 2px; 342 | } 343 | QProgressBar::chunk { 344 | background: #4994e0; 345 | border-radius: 8px; 346 | } 347 | QProgressBar[failed="true"]::chunk { 348 | background: #a6ff3c00; 349 | } 350 | 351 | 352 | /* Radiobuttons & Checkboxes */ 353 | QRadioButton, 354 | QCheckBox { 355 | background: transparent; 356 | } 357 | QRadioButton::indicator, 358 | QCheckBox::indicator, 359 | QListWidget::indicator, 360 | QTableView::indicator, 361 | QTreeView::indicator { 362 | background: #0affffff; 363 | width: 12px; 364 | height: 12px; 365 | border: 0px solid; 366 | border-radius: 4px; 367 | padding: 2px; 368 | } 369 | QRadioButton::indicator, 370 | QCheckBox::indicator { 371 | width: 16px; 372 | height: 16px; 373 | } 374 | QRadioButton::indicator:hover, 375 | QCheckBox::indicator:hover, 376 | QListWidget::indicator:hover, 377 | QTableView::indicator:hover, 378 | QTreeView::indicator:hover { 379 | background: #1cffffff; 380 | } 381 | QRadioButton::indicator:checked, 382 | QCheckBox::indicator:checked, 383 | QListWidget::indicator:checked, 384 | QTableView::indicator:checked, 385 | QTreeView::indicator:checked { 386 | background: #4994e0; 387 | } 388 | QRadioButton::indicator:hover:checked, 389 | QCheckBox::indicator:hover:checked, 390 | QListWidget::indicator:hover:checked, 391 | QTableView::indicator:hover:checked, 392 | QTreeView::indicator:hover:checked { 393 | background: #63a2e2; 394 | } 395 | QRadioButton::indicator:disabled:checked, 396 | QCheckBox::indicator:disabled:checked, 397 | QListWidget::indicator:disabled:checked, 398 | QTableView::indicator:disabled:checked, 399 | QTreeView::indicator:disabled:checked { 400 | background: #1cffffff; 401 | } 402 | QCheckBox::indicator:checked, 403 | QListWidget::indicator:checked, 404 | QTableView::indicator:checked, 405 | QTreeView::indicator:checked { 406 | image: url(:/icons/checkmark.svg); 407 | } 408 | QRadioButton::indicator { 409 | border-radius: 10px; 410 | } 411 | 412 | 413 | /* List Widget */ 414 | QListWidget { 415 | background: transparent; 416 | border: 1px solid #1cffffff; 417 | alternate-background-color: #08ffffff; 418 | } 419 | QListWidget::item { 420 | color: #ffffff; 421 | border: 0px; 422 | padding: 3px; 423 | } 424 | QListWidget::item:selected, 425 | QListWidget::item:hover { 426 | background: #15ffffff; 427 | } 428 | QListWidget::item:selected { 429 | color: #4994e0; 430 | } 431 | QListWidget#side_menu { 432 | padding: 4px; 433 | } 434 | 435 | 436 | /* Statusbar */ 437 | QStatusBar { 438 | font-family: Consolas; 439 | font-size: 13px; 440 | border-bottom-left-radius: 0px; 441 | border-bottom-right-radius: 0px; 442 | } 443 | QStatusBar QPushButton { 444 | background: transparent; 445 | border: 0px; 446 | margin-right: 3px; 447 | } 448 | QStatusBar::item { 449 | border: 0px; 450 | } 451 | 452 | 453 | /* Tree View & Table View */ 454 | QTreeView, QTableView { 455 | background: transparent; 456 | alternate-background-color: #08ffffff; 457 | selection-background-color: #15ffffff; 458 | border: 1px solid #1cffffff; 459 | border-radius: 8px; 460 | } 461 | QTreeView::item, QTableView::item { 462 | border: 0px solid; 463 | padding: 3px; 464 | } 465 | QTreeView::item:disabled, 466 | QTableView::item:disabled { 467 | color: #66ffffff; 468 | } 469 | QTreeView::item:selected, 470 | QTreeView::item:hover, 471 | QTableView::item:selected, 472 | QTableView::item:hover { 473 | background: #15ffffff; 474 | } 475 | QTreeView::item:selected, 476 | QTableView::item:selected { 477 | color: #4994e0; 478 | } 479 | QTreeView#download_list { 480 | selection-background-color: transparent; 481 | selection-color: #ffffff; 482 | } 483 | QTreeView#download_list::item:selected, 484 | QTreeView#download_list::item:hover { 485 | background: transparent; 486 | } 487 | 488 | 489 | /* Header View */ 490 | QHeaderView { 491 | background: transparent; 492 | color: #ffffff; 493 | border-top-left-radius: 8px; 494 | border-top-right-radius: 8px; 495 | } 496 | QHeaderView::section { 497 | background: transparent; 498 | padding: 5px; 499 | border: 1px solid #0cffffff; 500 | } 501 | QHeaderView::section:first { 502 | border-top-left-radius: 8px; 503 | } 504 | QHeaderView::section:last { 505 | border-top-right-radius: 8px; 506 | } 507 | QHeaderView::down-arrow { 508 | image: url(:/icons/arrow_down.svg); 509 | width: 20px; 510 | height: 20px; 511 | } 512 | QHeaderView::up-arrow { 513 | image: url(:/icons/arrow_up.svg); 514 | width: 20px; 515 | height: 20px; 516 | } 517 | 518 | 519 | /* Splitter */ 520 | QSplitter { 521 | background: transparent; 522 | } 523 | QSplitter::handle { 524 | border-radius: 4px; 525 | image: url(:/icons/grip.png); 526 | height: 15px; 527 | padding-left: 2px; 528 | padding-right: 2px; 529 | margin-left: 2px; 530 | margin-right: 2px; 531 | } 532 | 533 | 534 | /* TextBrowser */ 535 | QTextBrowser { 536 | background: #202020; 537 | color: #ffffff; 538 | 539 | padding-left: 20%; 540 | padding-right: 20%; 541 | 542 | border: 0px; 543 | } 544 | 545 | /* Tab Widget & Tab Bar */ 546 | QTabWidget::pane { 547 | background: transparent; 548 | border-radius: 8px; 549 | padding: 4px; 550 | } 551 | QTabBar { 552 | background: #08ffffff; 553 | } 554 | QTabBar::tab { 555 | background: transparent; 556 | border: 0px solid; 557 | height: 25px; 558 | padding: 4px; 559 | spacing: 4px; 560 | margin: 4px; 561 | border-radius: 4px; 562 | } 563 | QTabBar::tab:hover:!selected { 564 | background: #1cffffff; 565 | } 566 | QTabBar::tab:selected { 567 | background: #15ffffff; 568 | color: #4994e0; 569 | } 570 | QTabBar::close-button { 571 | image: url(:/icons/close.svg); 572 | width: 20px; 573 | height: 20px; 574 | padding: 3px; 575 | border-radius: 1px; 576 | } 577 | QTabBar::close-button:hover { 578 | background: #1cffffff; 579 | } 580 | QTabWidget::tab-bar#centered_tab { 581 | alignment: center; 582 | } 583 | QTabWidget#centered_tab QTabBar::tab { 584 | padding-left: 25px; 585 | padding-right: 25px; 586 | } 587 | -------------------------------------------------------------------------------- /res/xdelta/license.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /res/xdelta/xdelta.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cutleast/Dynamic-Interface-Patcher/b7fc17ecbf15c00696e83cd1dedf41401be03465/res/xdelta/xdelta.exe -------------------------------------------------------------------------------- /src/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | import logging 6 | import os 7 | import sys 8 | from argparse import Namespace 9 | from pathlib import Path 10 | 11 | from PySide6.QtCore import Signal 12 | from PySide6.QtGui import QIcon 13 | from PySide6.QtWidgets import QApplication 14 | 15 | from core.config.config import Config 16 | from core.patcher.patcher import Patcher 17 | from core.utilities.exception_handler import ExceptionHandler 18 | from core.utilities.qt_res_provider import read_resource 19 | from core.utilities.stdout_handler import StdoutHandler 20 | from ui.main_window import MainWindow 21 | 22 | 23 | class App(QApplication): 24 | """ 25 | Main application class. 26 | """ 27 | 28 | APP_NAME: str = "Dynamic Interface Patcher" 29 | APP_VERSION: str = "2.1.5" 30 | 31 | args: Namespace 32 | config: Config 33 | 34 | log: logging.Logger = logging.getLogger("App") 35 | stdout_handler: StdoutHandler 36 | exception_handler: ExceptionHandler 37 | 38 | app_path: Path = ( 39 | Path(sys.executable if getattr(sys, "frozen", False) else __file__) 40 | .resolve() 41 | .parent 42 | ) 43 | cwd_path: Path = Path(os.getcwd()) 44 | 45 | patcher: Patcher 46 | 47 | ready_signal = Signal() 48 | """ 49 | This signal gets emitted when the application is ready. 50 | """ 51 | 52 | def __init__(self, args: Namespace): 53 | super().__init__() 54 | 55 | self.args = args 56 | self.config = Config(Path(os.getcwd()) / "config") 57 | self.patcher = Patcher() 58 | 59 | log_format = "[%(asctime)s.%(msecs)03d]" 60 | log_format += "[%(levelname)s]" 61 | log_format += "[%(name)s.%(funcName)s]: " 62 | log_format += "%(message)s" 63 | self.log_format = logging.Formatter(log_format, datefmt="%d.%m.%Y %H:%M:%S") 64 | self.stdout_handler = StdoutHandler(self) 65 | self.exception_handler = ExceptionHandler(self) 66 | self.log_str = logging.StreamHandler(self.stdout_handler) 67 | self.log_str.setFormatter(self.log_format) 68 | self.log_level = 10 # Debug level 69 | self.log.setLevel(self.log_level) 70 | root_log = logging.getLogger() 71 | root_log.addHandler(self.log_str) 72 | root_log.setLevel(self.log_level) 73 | 74 | self.apply_args_to_config() 75 | 76 | if self.config.debug_mode: 77 | self.config.print_settings_to_log() 78 | 79 | self.setApplicationName(self.APP_NAME) 80 | self.setApplicationDisplayName(self.APP_NAME) 81 | self.setApplicationVersion(self.APP_VERSION) 82 | self.setStyleSheet(read_resource(":/style.qss")) 83 | self.setWindowIcon(QIcon(":/icons/icon.ico")) 84 | 85 | self.log.info(f"Current working directory: {self.cwd_path}") 86 | self.log.info(f"Executable location: {self.app_path}") 87 | self.log.info("Program started!") 88 | 89 | self.root = MainWindow() 90 | 91 | def apply_args_to_config(self) -> None: 92 | if self.args.debug: 93 | self.config.debug_mode = True 94 | self.log.info("Debug mode enabled.") 95 | 96 | if self.args.silent: 97 | self.config.silent = True 98 | 99 | if self.args.repack_bsa: 100 | self.config.repack_bsas = True 101 | 102 | if self.args.output_path: 103 | self.config.output_folder = Path(self.args.output_path) 104 | 105 | def exec(self) -> int: 106 | silent: bool = ( 107 | self.args.patchpath and self.args.originalpath 108 | ) and self.args.silent 109 | 110 | if not silent: 111 | self.root.show() 112 | 113 | self.ready_signal.emit() 114 | 115 | retcode: int = super().exec() 116 | 117 | self.log.info("Exiting application...") 118 | self.clean() 119 | 120 | return retcode 121 | 122 | def clean(self) -> None: 123 | """ 124 | Cleans up temporary application files. 125 | """ 126 | 127 | self.patcher.clean() 128 | -------------------------------------------------------------------------------- /src/core/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | -------------------------------------------------------------------------------- /src/core/archive/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | from .archive import Archive 6 | 7 | __add__ = [Archive] 8 | -------------------------------------------------------------------------------- /src/core/archive/archive.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | import logging 6 | import os 7 | from pathlib import Path 8 | 9 | from PySide6.QtWidgets import QApplication 10 | from virtual_glob import InMemoryPath, glob 11 | 12 | from core.utilities.process_runner import run_process 13 | 14 | 15 | class Archive: 16 | """ 17 | Base class for archives. 18 | 19 | **Do not instantiate directly, use Archive.load_archive() instead!** 20 | """ 21 | 22 | log = logging.getLogger("Archiver") 23 | 24 | __files: list[str] | None = None 25 | 26 | bin_path: Path 27 | 28 | def __init__(self, path: Path): 29 | self.bin_path = QApplication.instance().app_path / "7-zip" / "7z.exe" 30 | self.path = path 31 | 32 | @property 33 | def files(self) -> list[str]: 34 | """ 35 | Gets a list of files in the archive. 36 | 37 | Returns: 38 | list[str]: List of filenames, relative to archive root. 39 | """ 40 | 41 | raise NotImplementedError 42 | 43 | def get_files(self) -> list[str]: 44 | """ 45 | Alias method for `Archive.files` property. 46 | 47 | Returns: 48 | list[str]: List of filenames, relative to archive root. 49 | """ 50 | 51 | return self.files 52 | 53 | def extract_all(self, dest: Path, full_paths: bool = True) -> None: 54 | """ 55 | Extracts archive content. 56 | 57 | Args: 58 | dest (Path): Folder to extract archive content to. 59 | full_paths (bool, optional): 60 | Toggles whether paths within archive are retained. Defaults to True. 61 | 62 | Raises: 63 | RuntimeError: When the 7-zip commandline returns a non-zero exit code. 64 | """ 65 | 66 | cmd: list[str] = [ 67 | str(self.bin_path), 68 | "x" if full_paths else "e", 69 | str(self.path), 70 | f"-o{dest}", 71 | "-aoa", 72 | "-y", 73 | ] 74 | 75 | run_process(cmd) 76 | 77 | def extract(self, filename: str, dest: Path, full_paths: bool = True) -> None: 78 | """ 79 | Extracts a single file. 80 | 81 | Args: 82 | filename (str): Filename of file to extract. 83 | dest (Path): Folder to extract file to. 84 | full_paths (bool, optional): 85 | Toggles whether path within archives is retained. Defaults to True. 86 | 87 | Raises: 88 | RuntimeError: When the 7-zip commandline returns a non-zero exit code. 89 | """ 90 | 91 | cmd: list[str] = [ 92 | str(self.bin_path), 93 | "x" if full_paths else "e", 94 | f"-o{dest}", 95 | "-aoa", 96 | "-y", 97 | "--", 98 | str(self.path), 99 | filename, 100 | ] 101 | 102 | run_process(cmd) 103 | 104 | def extract_files( 105 | self, filenames: list[str], dest: Path, full_paths: bool = True 106 | ) -> None: 107 | """ 108 | Extracts multiple files. 109 | 110 | Args: 111 | filenames (list[str]): List of filenames to extract. 112 | dest (Path): Folder to extract files to. 113 | full_paths (bool, optional): 114 | Toggles whether paths within archive are retained. Defaults to True. 115 | 116 | Raises: 117 | RuntimeError: When the 7-zip commandline returns a non-zero exit code. 118 | """ 119 | 120 | if not len(filenames): 121 | return 122 | 123 | cmd: list[str] = [ 124 | str(self.bin_path), 125 | "x" if full_paths else "e", 126 | f"-o{dest}", 127 | "-aoa", 128 | "-y", 129 | str(self.path), 130 | ] 131 | 132 | # Write filenames to a txt file to workaround commandline length limit 133 | filenames_txt = self.path.with_suffix(".txt") 134 | with open(filenames_txt, "w", encoding="utf8") as file: 135 | file.write("\n".join(filenames)) 136 | cmd.append(f"@{filenames_txt}") 137 | 138 | try: 139 | run_process(cmd) 140 | except RuntimeError: 141 | os.remove(filenames_txt) 142 | raise 143 | 144 | def glob(self, pattern: str) -> list[str]: 145 | """ 146 | Gets a list of file paths that match a specified pattern. 147 | 148 | Args: 149 | pattern (str): Pattern that matches everything that fnmatch supports 150 | 151 | Returns: 152 | list: List of matching filenames. 153 | """ 154 | 155 | # Workaround case-sensitivity 156 | files: dict[str, str] = {file.lower(): file for file in self.files} 157 | 158 | fs: InMemoryPath = InMemoryPath.from_list(list(files.keys())) 159 | matches: list[str] = [files[p.path] for p in glob(fs, pattern)] 160 | 161 | return matches 162 | 163 | @staticmethod 164 | def load_archive(archive_path: Path) -> "Archive": 165 | """ 166 | Loads archive with fitting handler class. 167 | 168 | Currently supported archive types: RAR, 7z, ZIP 169 | 170 | Args: 171 | archive_path (Path): Path to archive file. 172 | 173 | Raises: 174 | NotImplementedError: When the archive type is not supported. 175 | 176 | Returns: 177 | Archive: Correct initialized handler class to use. 178 | """ 179 | 180 | from .rar import RARArchive 181 | from .sevenzip import SevenZipArchive 182 | from .zip import ZIPARchive 183 | 184 | match archive_path.suffix.lower(): 185 | case ".rar": 186 | return RARArchive(archive_path) 187 | case ".7z": 188 | return SevenZipArchive(archive_path) 189 | case ".zip": 190 | return ZIPARchive(archive_path) 191 | case suffix: 192 | raise NotImplementedError( 193 | f"Archive format {suffix!r} not yet supported!" 194 | ) 195 | -------------------------------------------------------------------------------- /src/core/archive/rar.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | import rarfile # type: ignore 6 | 7 | from .archive import Archive 8 | 9 | 10 | class RARArchive(Archive): 11 | """ 12 | Class for RAR Archives. 13 | """ 14 | 15 | __files: list[str] | None = None 16 | 17 | @property 18 | def files(self) -> list[str]: 19 | if self.__files is None: 20 | self.__files = [ 21 | file.filename 22 | for file in rarfile.RarFile(self.path).infolist() 23 | if file.is_file() 24 | ] 25 | 26 | return self.__files 27 | -------------------------------------------------------------------------------- /src/core/archive/sevenzip.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | import py7zr 6 | 7 | from .archive import Archive 8 | 9 | 10 | class SevenZipArchive(Archive): 11 | """ 12 | Class for 7z Archives. 13 | """ 14 | 15 | __files: list[str] | None = None 16 | 17 | @property 18 | def files(self) -> list[str]: 19 | if self.__files is None: 20 | self.__files = [ 21 | file.filename 22 | for file in py7zr.SevenZipFile(self.path).files 23 | if not file.is_directory 24 | ] 25 | 26 | return self.__files 27 | -------------------------------------------------------------------------------- /src/core/archive/zip.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | import zipfile 6 | 7 | from .archive import Archive 8 | 9 | 10 | class ZIPARchive(Archive): 11 | """ 12 | Class for ZIP Archives. 13 | """ 14 | 15 | __files: list[str] | None = None 16 | 17 | @property 18 | def files(self) -> list[str]: 19 | if self.__files is None: 20 | self.__files = [ 21 | file.filename 22 | for file in zipfile.ZipFile(self.path).filelist 23 | if not file.is_dir() 24 | ] 25 | 26 | return self.__files 27 | -------------------------------------------------------------------------------- /src/core/bsa/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | from .bsa_archive import BSAArchive 6 | from .header import Header 7 | 8 | __add__ = [BSAArchive, Header] 9 | -------------------------------------------------------------------------------- /src/core/bsa/bsa_archive.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | import os 6 | from io import BytesIO 7 | from pathlib import Path 8 | 9 | import lz4.frame 10 | from virtual_glob import InMemoryPath, glob 11 | 12 | from core.utilities.filesystem import is_dir, is_file 13 | 14 | from .datatypes import Hash, Integer, String 15 | from .file_name_block import FileNameBlock 16 | from .file_record import FileRecord, FileRecordBlock 17 | from .folder_record import FolderRecord 18 | from .header import Header 19 | from .utilities import create_folder_list 20 | 21 | 22 | class BSAArchive: 23 | """ 24 | Contains parsed archive data. 25 | """ 26 | 27 | def __init__(self, archive_path: Path): 28 | self.path = archive_path 29 | 30 | self.parse() 31 | 32 | def match_names(self): 33 | """ 34 | Matches file names to their records. 35 | """ 36 | 37 | result: dict[str, FileRecord] = {} 38 | 39 | index = 0 40 | for file_record_block in self.file_record_blocks: 41 | for file_record in file_record_block.file_records: 42 | # result[self.file_name_block.file_names[index]] = file_record 43 | file_path = file_record_block.name 44 | file_name = self.file_name_block.file_names[index] 45 | file = str(Path(file_path) / file_name).replace("\\", "/") 46 | result[file] = file_record 47 | index += 1 48 | 49 | return result 50 | 51 | def process_compression_flags(self): 52 | """ 53 | Processes compression flags in files. 54 | """ 55 | 56 | for file_record in self.files.values(): 57 | has_compression_flag = file_record.has_compression_flag() 58 | compressed_archive = self.header.archive_flags[ 59 | self.header.ArchiveFlags.CompressedArchive 60 | ] 61 | 62 | if has_compression_flag: 63 | file_record.compressed = not compressed_archive 64 | else: 65 | file_record.compressed = compressed_archive 66 | 67 | def parse(self): 68 | with self.path.open("rb") as stream: 69 | self.header = Header().parse(stream) 70 | self.folders = [ 71 | FolderRecord().parse(stream) for i in range(self.header.folder_count) 72 | ] 73 | self.file_record_blocks = [ 74 | FileRecordBlock().parse(stream, self.folders[i].count) 75 | for i in range(self.header.folder_count) 76 | ] 77 | self.file_name_block = FileNameBlock().parse(stream, self.header.file_count) 78 | 79 | self.files = self.match_names() 80 | self.process_compression_flags() 81 | 82 | return self 83 | 84 | def glob(self, pattern: str): 85 | """ 86 | Returns a list of file paths that 87 | match the . 88 | 89 | Parameters: 90 | pattern: str, everything that fnmatch supports 91 | 92 | Returns: 93 | list of matching filenames 94 | """ 95 | 96 | fs = InMemoryPath.from_list(list(self.files.keys())) 97 | matches = [p.path for p in glob(fs, pattern)] 98 | 99 | return matches 100 | 101 | def extract(self, dest_folder: Path): 102 | """ 103 | Extracts archive content to `dest_folder`. 104 | """ 105 | 106 | for file in self.files: 107 | self.extract_file(file, dest_folder) 108 | 109 | def extract_file(self, filename: str | Path, dest_folder: Path): 110 | """ 111 | Extracts `filename` from archive to `dest_folder`. 112 | """ 113 | 114 | filename = str(filename).replace("\\", "/") 115 | 116 | if filename not in self.files: 117 | raise FileNotFoundError(f"{filename!r} is not in archive!") 118 | 119 | file_record = self.files[filename] 120 | 121 | # Go to file raw data 122 | with self.path.open("rb") as stream: 123 | stream.seek(file_record.offset) 124 | 125 | file_size = file_record.size 126 | 127 | if self.header.archive_flags[self.header.ArchiveFlags.EmbedFileNames]: 128 | filename = String.parse(stream, String.StrType.BString) 129 | file_size -= ( 130 | len(filename) + 1 131 | ) # Subtract file name length + Uint8 prefix 132 | 133 | if file_record.compressed: 134 | original_size = Integer.parse(stream, Integer.IntType.ULong) 135 | data = stream.read(file_size - 4) 136 | data = lz4.frame.decompress(data) 137 | else: 138 | data = stream.read(file_size) 139 | 140 | destination = dest_folder / filename 141 | os.makedirs(destination.parent, exist_ok=True) 142 | with open(destination, "wb") as file: 143 | file.write(data) 144 | 145 | if not is_file(destination): 146 | raise Exception( 147 | f"Failed to extract file '{filename}' from archive '{self.path}'!" 148 | ) 149 | 150 | def get_file_stream(self, filename: str | Path): 151 | """ 152 | Instead of extracting the file this returns a file stream to the file data. 153 | """ 154 | 155 | filename = str(filename) 156 | 157 | if filename not in self.files: 158 | raise FileNotFoundError("File is not in archive!") 159 | 160 | file_record = self.files[filename] 161 | 162 | with self.path.open("rb") as stream: 163 | # Go to file raw data 164 | stream.seek(file_record.offset) 165 | 166 | file_size = file_record.size 167 | 168 | if self.header.archive_flags[self.header.ArchiveFlags.EmbedFileNames]: 169 | filename = String.parse(stream, String.StrType.BString) 170 | file_size -= ( 171 | len(filename) + 1 172 | ) # Subtract file name length + Uint8 prefix 173 | 174 | if file_record.compressed: 175 | original_size = Integer.parse(stream, Integer.IntType.ULong) 176 | data = stream.read(file_size - 4) 177 | data = lz4.frame.decompress(data) 178 | else: 179 | data = stream.read(file_size) 180 | 181 | return BytesIO(data) 182 | 183 | @staticmethod 184 | def create_file_flags(folders: list[Path]): 185 | file_flags: dict[Header.FileFlags, bool] = {} 186 | 187 | for folder in folders: 188 | root_folder_name = folder.parts[0].lower() 189 | sub_folder_name = folder.parts[1].lower() if len(folder.parts) > 1 else None 190 | 191 | match root_folder_name: 192 | case "meshes": 193 | file_flags[Header.FileFlags.Meshes] = True 194 | case "textures": 195 | file_flags[Header.FileFlags.Textures] = True 196 | case "interface": 197 | file_flags[Header.FileFlags.Menus] = True 198 | case "sounds": 199 | file_flags[Header.FileFlags.Sounds] = True 200 | if sub_folder_name == "voice": 201 | file_flags[Header.FileFlags.Voices] = True 202 | 203 | case _: 204 | file_flags[Header.FileFlags.Miscellaneous] = True 205 | 206 | return file_flags 207 | 208 | @staticmethod 209 | def create_archive( 210 | input_folder: Path, 211 | output_file: Path, 212 | archive_flags: dict[Header.ArchiveFlags, bool] = None, 213 | file_flags: dict[Header.FileFlags, bool] = None, 214 | ): 215 | """ 216 | Creates an archive from `input_folder`. 217 | """ 218 | 219 | if not is_dir(input_folder): 220 | raise ValueError(f"{str(input_folder)!r} must be an existing directory!") 221 | 222 | # Get elements and prepare folder and file structure 223 | files: list[Path] = [] 224 | for element in os.listdir(input_folder): 225 | if os.path.isdir(input_folder / element): 226 | files += create_folder_list(input_folder / element) 227 | file_name_block = FileNameBlock() 228 | file_name_block.file_names = [file.name for file in files] 229 | folders: dict[Path, list[Path]] = {} 230 | for file in files: 231 | folder = file.parent 232 | 233 | if folder in folders: 234 | folders[folder].append(file) 235 | else: 236 | folders[folder] = [file] 237 | 238 | if not folders or not file_name_block.file_names: 239 | raise Exception("No elements to pack!") 240 | 241 | # Create header 242 | header = Header() 243 | header.file_count = len(files) 244 | header.folder_count = len(folders) 245 | header.total_file_name_length = len(file_name_block.dump()) 246 | header.total_folder_name_length = len( 247 | String.dump([str(folder) for folder in folders], String.StrType.List) 248 | ) 249 | header.file_flags = BSAArchive.create_file_flags(list(folders.keys())) 250 | 251 | if archive_flags is not None: 252 | header.archive_flags.update(archive_flags) 253 | if file_flags is not None: 254 | header.file_flags.update(file_flags) 255 | 256 | if ( 257 | header.archive_flags[Header.ArchiveFlags.EmbedFileNames] 258 | and header.archive_flags[Header.ArchiveFlags.CompressedArchive] 259 | ): 260 | print( 261 | "WARNING! Use Embedded File Names and Compresion at the same time at your own risk!" 262 | ) 263 | 264 | # Create record and block structure 265 | folder_records: list[FolderRecord] = [] 266 | file_record_blocks: list[FileRecordBlock] = [] 267 | file_records: dict[FileRecord, str] = {} 268 | current_offset = 36 # Start with header size 269 | current_offset += len(folders) * 24 # Add estimated size of all folder records 270 | 271 | for folder, _files in folders.items(): 272 | folder_name = str(folder).replace("/", "\\") 273 | 274 | folder_record = FolderRecord() 275 | folder_record.name_hash = Hash.calc_hash(folder_name) 276 | folder_record.count = len(_files) 277 | folder_record.offset = current_offset 278 | folder_records.append(folder_record) 279 | 280 | file_record_block = FileRecordBlock() 281 | file_record_block.name = folder_name 282 | file_record_block.file_records = [] 283 | 284 | for file in _files: 285 | file_record = FileRecord() 286 | file_record.name_hash = Hash.calc_hash(file.name.lower()) 287 | file_record.size = os.path.getsize(input_folder / file) 288 | file_records[file_record] = str(file).replace("/", "\\") 289 | file_record_block.file_records.append(file_record) 290 | 291 | # Add name length of file record block (+ Uint8 and null-terminator) 292 | current_offset += len(file_record_block.name) + 2 293 | # Add estimated size of all file records from current file record block 294 | current_offset += len(file_record_block.file_records) * 16 295 | 296 | file_record_blocks.append(file_record_block) 297 | 298 | current_offset += len(file_name_block.dump()) # Add size of file name block 299 | 300 | # Write file 301 | with output_file.open("wb") as output_stream: 302 | # Write Placeholder for Record Structure 303 | output_stream.write(b"\x00" * current_offset) 304 | 305 | for file_record, file_name in file_records.items(): 306 | if header.archive_flags[Header.ArchiveFlags.EmbedFileNames]: 307 | output_stream.write(String.dump(file_name, String.StrType.BString)) 308 | file_record.size += len(file_name) + 1 309 | 310 | if header.archive_flags[Header.ArchiveFlags.CompressedArchive]: 311 | with (input_folder / file_name).open("rb") as file: 312 | data = file.read() 313 | 314 | compressed_data: bytes = lz4.frame.compress(data) 315 | 316 | output_stream.write( 317 | Integer.dump(file_record.size, Integer.IntType.ULong) 318 | ) 319 | output_stream.write(compressed_data) 320 | file_record.size = ( 321 | len(compressed_data) + 4 322 | ) # Compressed size + ULong prefix 323 | 324 | # Readd file name length after reducing file size to compressed size 325 | if header.archive_flags[Header.ArchiveFlags.EmbedFileNames]: 326 | file_record.size += len(file_name) + 1 327 | else: 328 | with (input_folder / file_name).open("rb") as file: 329 | while data := file.read(1024 * 1024): 330 | output_stream.write(data) 331 | 332 | # Calculate file offsets 333 | for file_record, file_name in file_records.items(): 334 | file_record.offset = current_offset 335 | current_offset += file_record.size # Add file size 336 | 337 | output_stream.seek(0) 338 | output_stream.write(header.dump()) 339 | output_stream.write( 340 | b"".join(folder_record.dump() for folder_record in folder_records) 341 | ) 342 | output_stream.write( 343 | b"".join( 344 | file_record_block.dump() for file_record_block in file_record_blocks 345 | ) 346 | ) 347 | output_stream.write(file_name_block.dump()) 348 | -------------------------------------------------------------------------------- /src/core/bsa/datatypes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | import os 6 | import struct 7 | from enum import Enum, IntEnum, auto 8 | from io import BufferedReader, BytesIO 9 | 10 | from .utilities import read_data, get_stream 11 | 12 | 13 | class Integer: 14 | """ 15 | Class for all types of signed and unsigned integers. 16 | """ 17 | 18 | class IntType(Enum): 19 | UInt8 = (1, False) 20 | """Unsigned Integer of size 1.""" 21 | 22 | UInt16 = (2, False) 23 | """Unsigned Integer of size 2.""" 24 | 25 | UInt32 = (4, False) 26 | """Unsigned Integer of size 4.""" 27 | 28 | UInt64 = (8, False) 29 | """Unsigned Integer of size 8.""" 30 | 31 | UShort = (2, False) 32 | """Same as UInt16.""" 33 | 34 | ULong = (4, False) 35 | """Same as UInt32.""" 36 | 37 | Int8 = (1, True) 38 | """Signed Integer of Size 1.""" 39 | 40 | Int16 = (2, True) 41 | """Signed Integer of Size 2.""" 42 | 43 | Int32 = (4, True) 44 | """Signed Integer of Size 4.""" 45 | 46 | Int64 = (8, True) 47 | """Signed Integer of Size 8.""" 48 | 49 | Short = (2, True) 50 | """Same as Int16.""" 51 | 52 | Long = (4, True) 53 | """Same as Int32.""" 54 | 55 | @staticmethod 56 | def parse(data: BufferedReader | bytes, type: IntType): 57 | size, signed = type.value 58 | 59 | return int.from_bytes( 60 | get_stream(data).read(size), byteorder="little", signed=signed 61 | ) 62 | 63 | @staticmethod 64 | def dump(value: int, type: IntType): 65 | size, signed = type.value 66 | 67 | return value.to_bytes(size, byteorder="little", signed=signed) 68 | 69 | 70 | class Float: 71 | """ 72 | Class for all types of floats. 73 | """ 74 | 75 | @staticmethod 76 | def parse(data: BufferedReader | bytes, size: int = 4) -> float: 77 | return struct.unpack("f", get_stream(data).read(size))[0] 78 | 79 | @staticmethod 80 | def dump(value: float, size: int = 4): 81 | return struct.pack("f", value) 82 | 83 | 84 | class String: 85 | """ 86 | Class for all types of chars and strings. 87 | """ 88 | 89 | class StrType(Enum): 90 | Char = auto() 91 | """8-bit character.""" 92 | 93 | WChar = auto() 94 | """16-bit character.""" 95 | 96 | String = auto() 97 | """Not-terminated string.""" 98 | 99 | WString = auto() 100 | """Not-terminated string prefixed by UInt16.""" 101 | 102 | BZString = auto() 103 | """Null-terminated string prefixed by UInt8.""" 104 | 105 | BString = auto() 106 | """Not-terminated string prefixed by UInt8.""" 107 | 108 | List = auto() 109 | """List of strings separated by `\\x00`.""" 110 | 111 | @staticmethod 112 | def parse(data: BufferedReader | bytes, type: StrType, size: int = None): 113 | stream = get_stream(data) 114 | 115 | match type: 116 | case type.Char: 117 | text = read_data(stream, 1) 118 | 119 | case type.WChar: 120 | text = read_data(stream, 2) 121 | 122 | case type.String: 123 | if size is None: 124 | raise ValueError( 125 | f"'size' must not be None when 'type' is 'String'!" 126 | ) 127 | 128 | text = read_data(stream, size) 129 | 130 | case type.WString: 131 | size = Integer.parse(stream, Integer.IntType.UInt16) 132 | text = read_data(stream, size) 133 | 134 | case type.BZString | type.BString: 135 | size = Integer.parse(stream, Integer.IntType.UInt8) 136 | text = read_data(stream, size).strip(b"\x00") 137 | 138 | case type.List: 139 | strings: list[str] = [] 140 | 141 | while len(strings) < size: 142 | string = b"" 143 | while (char := stream.read(1)) != b"\x00" and char: 144 | string += char 145 | 146 | if string: 147 | strings.append(string.decode()) 148 | 149 | return strings 150 | 151 | return text.decode() 152 | 153 | @staticmethod 154 | def dump(value: list[str] | str, type: StrType) -> bytes: 155 | match type: 156 | case type.Char | type.WChar | type.String: 157 | return value.encode() 158 | 159 | case type.WString: 160 | text = value.encode() 161 | size = Integer.dump(len(text), Integer.IntType.UInt16) 162 | return size + text 163 | 164 | case type.BString: 165 | text = value.encode() 166 | size = Integer.dump(len(text), Integer.IntType.UInt8) 167 | return size + text 168 | 169 | case type.BZString: 170 | text = value.encode() + b"\x00" 171 | size = Integer.dump(len(text), Integer.IntType.UInt8) 172 | return size + text 173 | 174 | case type.List: 175 | data = b"\x00".join(v.encode() for v in value) + b"\x00" 176 | 177 | return data 178 | 179 | 180 | class Flags(IntEnum): 181 | """ 182 | Class for all types of flags. 183 | """ 184 | 185 | @classmethod 186 | def parse( 187 | cls, data: BufferedReader | bytes, type: Integer.IntType 188 | ) -> dict["Flags", bool]: 189 | # Convert the bytestring to an integer 190 | value = Integer.parse(data, type) 191 | 192 | parsed_flags = {} 193 | 194 | for flag in cls: 195 | parsed_flags[flag] = bool(value & flag.value) 196 | 197 | return parsed_flags 198 | 199 | @classmethod 200 | def dump(cls, flags: dict["Flags", bool], type: Integer.IntType) -> bytes: 201 | # Convert the parsed flags back into an integer 202 | value = 0 203 | for flag in cls: 204 | if flags.get(flag, False): 205 | value |= flag 206 | 207 | # Convert the integer back to a bytestring 208 | return Integer.dump(value, type) 209 | 210 | 211 | class Hex: 212 | """ 213 | Class for all types of hexadecimal strings. 214 | """ 215 | 216 | @staticmethod 217 | def parse(data: BufferedReader | bytes, size: int): 218 | return read_data(data, size).hex() 219 | 220 | @staticmethod 221 | def dump(value: str, type: Integer.IntType): 222 | number = int(value, base=16) 223 | 224 | return Integer.dump(number, type) 225 | 226 | 227 | class Hash: 228 | """ 229 | Class for all types of hashes. 230 | """ 231 | 232 | @staticmethod 233 | def parse(data: BufferedReader | bytes): 234 | return Integer.parse(data, Integer.IntType.UInt64) 235 | 236 | @staticmethod 237 | def dump(value: int): 238 | return Integer.dump(value, Integer.IntType.UInt64) 239 | 240 | @staticmethod 241 | def calc_hash(filename: str) -> int: 242 | """ 243 | Returns TES4's two hash values for filename. 244 | Based on TimeSlips code, fixed for names < 3 characters 245 | and updated to Python 3. 246 | 247 | This original code is from here: 248 | https://en.uesp.net/wiki/Oblivion_Mod:Hash_Calculation 249 | """ 250 | 251 | name, ext = os.path.splitext( 252 | filename.lower() 253 | ) # --"bob.dds" >> root = "bob", ext = ".dds" 254 | 255 | # Create the hashBytes array equivalent 256 | hash_bytes = [ 257 | ord(name[-1]) if len(name) > 0 else 0, 258 | ord(name[-2]) if len(name) >= 3 else 0, 259 | len(name), 260 | ord(name[0]) if len(name) > 0 else 0, 261 | ] 262 | 263 | # Convert the byte array to a single 32-bit integer 264 | hash1: int = struct.unpack("I", bytes(hash_bytes))[0] 265 | 266 | # Apply extensions-specific bit manipulation 267 | if ext == ".kf": 268 | hash1 |= 0x80 269 | elif ext == ".nif": 270 | hash1 |= 0x8000 271 | elif ext == ".dds": 272 | hash1 |= 0x8080 273 | elif ext == ".wav": 274 | hash1 |= 0x80000000 275 | 276 | hash2 = 0 277 | for i in range(1, len(name) - 2): 278 | hash2 = hash2 * 0x1003F + ord(name[i]) 279 | 280 | hash3 = 0 281 | for char in ext: 282 | hash3 = hash3 * 0x1003F + ord(char) 283 | 284 | uint_mask = 0xFFFFFFFF 285 | combined_hash = ((hash2 + hash3) & uint_mask) << 32 | hash1 286 | 287 | return combined_hash 288 | -------------------------------------------------------------------------------- /src/core/bsa/file_name_block.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | import os 6 | from io import BufferedReader 7 | 8 | from .datatypes import String 9 | 10 | 11 | class FileNameBlock: 12 | """ 13 | Class for file name block. 14 | """ 15 | 16 | file_names: list[str] 17 | 18 | def parse(self, stream: BufferedReader, count: int): 19 | self.file_names = String.parse(stream, String.StrType.List, count) 20 | 21 | return self 22 | 23 | def dump(self) -> bytes: 24 | data = String.dump(self.file_names, String.StrType.List) 25 | 26 | return data 27 | -------------------------------------------------------------------------------- /src/core/bsa/file_record.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | from io import BufferedReader 6 | 7 | from .datatypes import Hash, Integer, String 8 | 9 | 10 | class FileRecordBlock: 11 | """ 12 | Class for file record block. 13 | """ 14 | 15 | name: str 16 | file_records: list["FileRecord"] 17 | 18 | def parse(self, stream: BufferedReader, count: int): 19 | self.name = String.parse(stream, String.StrType.BZString) 20 | self.file_records = [FileRecord().parse(stream) for i in range(count)] 21 | 22 | return self 23 | 24 | def dump(self) -> bytes: 25 | data = b"" 26 | 27 | data += String.dump(self.name, String.StrType.BZString) 28 | data += b"".join(file_record.dump() for file_record in self.file_records) 29 | 30 | return data 31 | 32 | 33 | class FileRecord: 34 | """ 35 | Class for file record. 36 | """ 37 | 38 | name_hash: int 39 | size: int 40 | offset: int 41 | 42 | compressed: bool = None 43 | 44 | def has_compression_flag(self) -> bool: 45 | # Mask for the 30th bit (0x40000000) 46 | mask = 0x40000000 47 | 48 | # Use bitwise AND to check if the 30th bit is set 49 | is_set = self.size & mask 50 | 51 | return is_set != 0 52 | 53 | @staticmethod 54 | def apply_compression_flag(size: int) -> int: 55 | """ 56 | Applies compression flag to `size`. 57 | """ 58 | 59 | # Mask for the 30th bit (0x40000000) 60 | mask = 0x40000000 61 | 62 | # Use bitwise OR to set the 30th bit 63 | size |= mask 64 | 65 | return size 66 | 67 | def parse(self, stream: BufferedReader): 68 | self.name_hash = Hash.parse(stream) 69 | self.size = Integer.parse(stream, Integer.IntType.ULong) 70 | self.offset = Integer.parse(stream, Integer.IntType.ULong) 71 | 72 | return self 73 | 74 | def dump(self): 75 | data = b"" 76 | 77 | data += Hash.dump(self.name_hash) 78 | if self.compressed: 79 | self.size = self.apply_compression_flag(self.size) 80 | data += Integer.dump(self.size, Integer.IntType.ULong) 81 | data += Integer.dump(self.offset, Integer.IntType.ULong) 82 | 83 | return data 84 | -------------------------------------------------------------------------------- /src/core/bsa/folder_record.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | from io import BufferedReader 6 | 7 | from .datatypes import Hash, Integer 8 | 9 | 10 | class FolderRecord: 11 | """ 12 | Class for folder records. 13 | """ 14 | 15 | name_hash: int 16 | count: int 17 | padding: int = 0 18 | offset: int 19 | padding2: int = 0 20 | 21 | def parse(self, stream: BufferedReader): 22 | self.name_hash = Hash.parse(stream) 23 | self.count = Integer.parse(stream, Integer.IntType.ULong) 24 | self.padding = Integer.parse(stream, Integer.IntType.ULong) 25 | self.offset = Integer.parse(stream, Integer.IntType.ULong) 26 | self.padding2 = Integer.parse(stream, Integer.IntType.ULong) 27 | 28 | return self 29 | 30 | def dump(self) -> bytes: 31 | data = b"" 32 | 33 | data += Hash.dump(self.name_hash) 34 | data += Integer.dump(self.count, Integer.IntType.ULong) 35 | data += Integer.dump(self.padding, Integer.IntType.ULong) 36 | data += Integer.dump(self.offset, Integer.IntType.ULong) 37 | data += Integer.dump(self.padding2, Integer.IntType.ULong) 38 | 39 | return data 40 | -------------------------------------------------------------------------------- /src/core/bsa/header.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | from io import BufferedReader 6 | 7 | from .datatypes import Flags, Integer 8 | 9 | 10 | class Header: 11 | """ 12 | Class for archive header. 13 | """ 14 | 15 | class ArchiveFlags(Flags): 16 | IncludeDirectoryNames = 0x1 17 | IncludeFileNames = 0x2 18 | CompressedArchive = 0x4 19 | RetainDirectoryNames = 0x8 20 | RetainFileNames = 0x10 21 | RetainFileNameOffsets = 0x20 22 | Xbox360archive = 0x40 23 | RetainStringsDuringStartup = 0x80 24 | EmbedFileNames = 0x100 25 | XMemCodec = 0x200 26 | 27 | class FileFlags(Flags): 28 | Meshes = 0x1 29 | Textures = 0x2 30 | Menus = 0x4 31 | Sounds = 0x8 32 | Voices = 0x10 33 | Shaders = 0x20 34 | Trees = 0x40 35 | Fonts = 0x80 36 | Miscellaneous = 0x100 37 | 38 | file_id: bytes = b"BSA\x00" 39 | version: int = 0x69 # Skyrim SE as default version 40 | offset: int = 0x24 # Header has fix size of 36 bytes 41 | archive_flags: dict[ArchiveFlags, bool] = { 42 | ArchiveFlags.IncludeDirectoryNames: True, 43 | ArchiveFlags.IncludeFileNames: True, 44 | ArchiveFlags.CompressedArchive: True, 45 | ArchiveFlags.RetainDirectoryNames: True, 46 | ArchiveFlags.RetainFileNames: True, 47 | ArchiveFlags.RetainFileNameOffsets: True, 48 | ArchiveFlags.Xbox360archive: False, 49 | ArchiveFlags.RetainStringsDuringStartup: False, 50 | ArchiveFlags.EmbedFileNames : False, 51 | ArchiveFlags.XMemCodec : False, 52 | } 53 | folder_count: int 54 | file_count: int 55 | total_folder_name_length: int 56 | total_file_name_length: int 57 | file_flags: dict[str, bool] = { 58 | FileFlags.Meshes: False, 59 | FileFlags.Textures: False, 60 | FileFlags.Menus: False, 61 | FileFlags.Sounds: False, 62 | FileFlags.Voices: False, 63 | FileFlags.Shaders: False, 64 | FileFlags.Trees: False, 65 | FileFlags.Fonts: False, 66 | FileFlags.Miscellaneous: False, 67 | } 68 | padding: int = 0 69 | 70 | def parse(self, stream: BufferedReader): 71 | self.file_id = stream.read(4) 72 | self.version = Integer.parse(stream, Integer.IntType.ULong) 73 | 74 | if self.version != 105: 75 | raise Exception("Archive format is not supported!") 76 | 77 | self.offset = Integer.parse(stream, Integer.IntType.ULong) 78 | self.archive_flags = Header.ArchiveFlags.parse(stream, Integer.IntType.ULong) 79 | self.folder_count = Integer.parse(stream, Integer.IntType.ULong) 80 | self.file_count = Integer.parse(stream, Integer.IntType.ULong) 81 | self.total_folder_name_length = Integer.parse(stream, Integer.IntType.ULong) 82 | self.total_file_name_length = Integer.parse(stream, Integer.IntType.ULong) 83 | self.file_flags = Header.FileFlags.parse(stream, Integer.IntType.UShort) 84 | self.padding = Integer.parse(stream, Integer.IntType.UShort) 85 | 86 | return self 87 | 88 | def dump(self): 89 | data = b"" 90 | 91 | data += self.file_id 92 | data += Integer.dump(self.version, Integer.IntType.ULong) 93 | data += Integer.dump(self.offset, Integer.IntType.ULong) 94 | data += Header.ArchiveFlags.dump(self.archive_flags, Integer.IntType.ULong) 95 | data += Integer.dump(self.folder_count, Integer.IntType.ULong) 96 | data += Integer.dump(self.file_count, Integer.IntType.ULong) 97 | data += Integer.dump(self.total_folder_name_length, Integer.IntType.ULong) 98 | data += Integer.dump(self.total_file_name_length, Integer.IntType.ULong) 99 | data += Header.FileFlags.dump(self.file_flags, Integer.IntType.UShort) 100 | data += Integer.dump(self.padding, Integer.IntType.UShort) 101 | 102 | return data 103 | -------------------------------------------------------------------------------- /src/core/bsa/utilities.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | import os 6 | from io import BufferedReader, BytesIO 7 | from pathlib import Path 8 | 9 | 10 | def create_folder_list(folder: Path): 11 | """ 12 | Creates a list with all files 13 | with relative paths to `folder` and returns it. 14 | """ 15 | 16 | files: list[Path] = [] 17 | 18 | for root, _, _files in os.walk(folder): 19 | for f in _files: 20 | path = os.path.join(root, f) 21 | files.append(Path(path).relative_to(folder.parent)) 22 | 23 | return files 24 | 25 | 26 | def get_stream(data: BufferedReader | bytes) -> BytesIO: 27 | if isinstance(data, bytes): 28 | return BytesIO(data) 29 | 30 | return data 31 | 32 | 33 | def read_data(data: BufferedReader | bytes, size: int) -> bytes: 34 | """ 35 | Returns `size` bytes from `data`. 36 | """ 37 | 38 | if isinstance(data, bytes): 39 | return data[:size] 40 | else: 41 | return data.read(size) 42 | -------------------------------------------------------------------------------- /src/core/config/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | -------------------------------------------------------------------------------- /src/core/config/_base_config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | import logging 6 | import os 7 | from pathlib import Path 8 | from typing import Any, Iterable 9 | 10 | import jstyleson as json # type: ignore 11 | 12 | from core.utilities.filesystem import is_file 13 | from core.utilities.qt_res_provider import load_json_resource 14 | 15 | 16 | class BaseConfig: 17 | """ 18 | Base class for app configurations. 19 | """ 20 | 21 | log: logging.Logger = logging.getLogger("BaseConfig") 22 | 23 | _config_path: Path 24 | _default_settings: dict[str, Any] 25 | _settings: dict[str, Any] 26 | 27 | def __init__(self, config_path: Path): 28 | self._config_path = config_path 29 | 30 | # Load default config values from resources 31 | self._default_settings = load_json_resource( 32 | f":/default_configs/{config_path.name}" 33 | ) 34 | 35 | self.load() 36 | 37 | def load(self) -> None: 38 | """ 39 | Loads configuration from JSON File, if existing. 40 | """ 41 | 42 | if is_file(self._config_path): 43 | with self._config_path.open("r", encoding="utf8") as file: 44 | self._settings = self._default_settings | json.load(file) 45 | 46 | for key in self._settings: 47 | if key not in self._default_settings: 48 | self.log.warning( 49 | f"Unknown setting detected in {self._config_path.name}: {key!r}" 50 | ) 51 | else: 52 | self._settings = self._default_settings.copy() 53 | 54 | def save(self) -> None: 55 | """ 56 | Saves non-default configuration values to JSON File, creating it if not existing. 57 | """ 58 | 59 | changed_values: dict[str, Any] = { 60 | key: item 61 | for key, item in self._settings.items() 62 | if item != self._default_settings.get(key) 63 | } 64 | 65 | # Create config folder if it doesn't exist 66 | os.makedirs(self._config_path.parent, exist_ok=True) 67 | 68 | with self._config_path.open("w", encoding="utf8") as file: 69 | json.dump(changed_values, file, indent=4, ensure_ascii=False) 70 | 71 | @staticmethod 72 | def validate_value(value: Any, valid_values: Iterable[Any]) -> None: 73 | """ 74 | Validates a value by checking it against an iterable of valid values. 75 | 76 | Args: 77 | value (Any): Value to validate. 78 | valid_values (Iterable[Any]): Iterable containing valid values. 79 | 80 | Raises: 81 | ValueError: When the value is not a valid value. 82 | """ 83 | 84 | if value not in list(valid_values): 85 | raise ValueError(f"{value!r} is not a valid value!") 86 | 87 | @staticmethod 88 | def validate_type(value: Any, type: type) -> None: 89 | """ 90 | Validates if value is of a certain type. 91 | 92 | Args: 93 | value (Any): Value to validate. 94 | type (type): Type the value should have. 95 | 96 | Raises: 97 | TypeError: When the value is not of the specified type. 98 | """ 99 | 100 | if not isinstance(value, type): 101 | raise TypeError(f"Value must be of type {type}!") 102 | 103 | def print_settings_to_log(self) -> None: 104 | """ 105 | Prints current settings to log. 106 | """ 107 | 108 | self.log.info("Current Configuration:") 109 | for key, item in self._settings.items(): 110 | self.log.info(f"{key:>25} = {item!r}") 111 | 112 | # Context Manager methods 113 | def __enter__(self) -> "BaseConfig": 114 | return self 115 | 116 | def __exit__(self, exc_type, exc_value, exc_tb) -> None: 117 | self.save() 118 | -------------------------------------------------------------------------------- /src/core/config/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | from pathlib import Path 6 | from typing import Optional 7 | 8 | from core.utilities.filesystem import is_dir, is_file 9 | 10 | from ._base_config import BaseConfig 11 | 12 | 13 | class Config(BaseConfig): 14 | """ 15 | Class for managing settings. 16 | """ 17 | 18 | def __init__(self, config_folder: Path): 19 | super().__init__(config_folder / "config.json") 20 | 21 | @property 22 | def debug_mode(self) -> bool: 23 | """ 24 | Toggles whether debug files get outputted. 25 | """ 26 | 27 | return self._settings["debug_mode"] 28 | 29 | @debug_mode.setter 30 | def debug_mode(self, value: bool): 31 | Config.validate_type(value, bool) 32 | 33 | self._settings["debug_mode"] = value 34 | 35 | @property 36 | def repack_bsas(self) -> bool: 37 | """ 38 | Toggles whether to repack BSAs after patching. 39 | """ 40 | 41 | return self._settings["repack_bsa"] 42 | 43 | @repack_bsas.setter 44 | def repack_bsas(self, value: bool): 45 | Config.validate_type(value, bool) 46 | 47 | self._settings["repack_bsa"] = value 48 | 49 | @property 50 | def silent(self) -> bool: 51 | """ 52 | Toggles whether GUI is shown. 53 | """ 54 | 55 | return self._settings["silent"] 56 | 57 | @silent.setter 58 | def silent(self, value: bool): 59 | Config.validate_type(value, bool) 60 | 61 | self._settings["silent"] = value 62 | 63 | @property 64 | def output_folder(self) -> Optional[Path]: 65 | """ 66 | Specifies output path for patched files. 67 | """ 68 | 69 | if self._settings["output_folder"] is not None: 70 | return Path(self._settings["output_folder"]).resolve() 71 | 72 | @output_folder.setter 73 | def output_folder(self, path: Path): 74 | Config.validate_type(path, Path) 75 | 76 | if not is_dir(path.parent): 77 | raise FileNotFoundError(path) 78 | elif is_file(path): 79 | raise NotADirectoryError(path) 80 | 81 | self._settings["output_folder"] = str(path.resolve()) 82 | -------------------------------------------------------------------------------- /src/core/patcher/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | -------------------------------------------------------------------------------- /src/core/patcher/ffdec.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | import logging 6 | from pathlib import Path 7 | 8 | from PySide6.QtWidgets import QApplication 9 | 10 | from core.utilities.filesystem import is_file 11 | from core.utilities.process_runner import run_process 12 | 13 | 14 | class FFDecInterface: 15 | """ 16 | Class for FFDec commandline interface. 17 | """ 18 | 19 | log: logging.Logger = logging.getLogger("FFDecInterface") 20 | bin_path: Path 21 | 22 | def __init__(self): 23 | self.bin_path = QApplication.instance().app_path / "ffdec" / "ffdec.bat" 24 | 25 | def replace_shapes(self, swf_file: Path, shapes: dict[Path, list[int]]) -> None: 26 | """ 27 | Replaces shapes in an SWF file. 28 | 29 | Args: 30 | swf_file (Path): Path to SWF file 31 | shapes (dict[Path, list[int]]): Dictionary mapping SVG paths and list of indexes 32 | """ 33 | 34 | self.log.info(f"Patching shapes of file {swf_file.name!r}...") 35 | 36 | cmds: list[str] = [] 37 | for shape, indexes in shapes.items(): 38 | shape = shape.resolve() 39 | 40 | if not is_file(shape): 41 | self.log.error( 42 | f"Failed to patch shape {shape.name}: File does not exist!" 43 | ) 44 | continue 45 | 46 | if shape.suffix not in [".svg", ".png", ".jpg", ".jpeg"]: 47 | self.log.warning( 48 | f"File type '{shape.suffix}' ({shape.name}) is not supported or tested and may lead to issues!" 49 | ) 50 | 51 | for index in indexes: 52 | line = f"""{index}\n{shape}\n""" 53 | cmds.append(line) 54 | 55 | cmdfile: Path = swf_file.parent / "shapes.txt" 56 | with open(cmdfile, "w", encoding="utf8") as file: 57 | file.writelines(cmds) 58 | 59 | cmd: list[str] = [ 60 | str(self.bin_path), 61 | "-replace", 62 | str(swf_file), 63 | str(swf_file), 64 | str(cmdfile.resolve()), 65 | ] 66 | run_process(cmd) 67 | 68 | self.log.info("Shapes patched.") 69 | 70 | def swf2xml(self, swf_file: Path) -> Path: 71 | """ 72 | Converts an SWF file to an XML file. 73 | 74 | Args: 75 | swf_file (Path): SWF file to convert to XML. 76 | 77 | Returns: 78 | Path: to converted XML file. 79 | """ 80 | 81 | self.log.info(f"Converting {swf_file.name!r} to XML...") 82 | 83 | out_path: Path = swf_file.with_suffix(".xml") 84 | 85 | cmd: list[str] = [ 86 | str(self.bin_path), 87 | "-swf2xml", 88 | str(swf_file), 89 | str(out_path), 90 | ] 91 | run_process(cmd) 92 | 93 | self.log.info("Converted to XML.") 94 | 95 | return out_path 96 | 97 | def xml2swf(self, xml_file: Path) -> Path: 98 | """ 99 | Converts an XML file to an SWF file. 100 | 101 | Args: 102 | xml_file (Path): XML file to convert to SWF. 103 | 104 | Returns: 105 | Path: to converted SWF file. 106 | """ 107 | 108 | self.log.info(f"Converting {xml_file.name!r} to SWF...") 109 | 110 | out_path: Path = xml_file.with_suffix(".swf") 111 | 112 | cmd: list[str] = [ 113 | str(self.bin_path), 114 | "-xml2swf", 115 | str(xml_file), 116 | str(out_path), 117 | ] 118 | run_process(cmd) 119 | 120 | self.log.info("Converted to SWF.") 121 | 122 | return out_path 123 | -------------------------------------------------------------------------------- /src/core/patcher/xdelta.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | import logging 6 | import os 7 | from pathlib import Path 8 | 9 | from PySide6.QtWidgets import QApplication 10 | 11 | from core.utilities.process_runner import run_process 12 | 13 | 14 | class XDeltaInterface: 15 | """ 16 | Class for xdelta commandline interface. 17 | """ 18 | 19 | log: logging.Logger = logging.getLogger("xdelta") 20 | bin_path: Path 21 | 22 | def __init__(self): 23 | self.bin_path = QApplication.instance().app_path / "xdelta" / "xdelta.exe" 24 | 25 | def patch_file(self, original_file_path: Path, xdelta_file_path: Path): 26 | self.log.info(f"Patching {original_file_path.name!r} with xdelta...") 27 | 28 | output_file_path: Path = original_file_path.with_suffix(".patched") 29 | cmd: list[str] = [ 30 | str(self.bin_path), 31 | "-d", 32 | "-s", 33 | str(original_file_path), 34 | str(xdelta_file_path), 35 | str(output_file_path), 36 | ] 37 | self.log.debug(" ".join(cmd)) 38 | run_process(cmd) 39 | 40 | os.remove(original_file_path) 41 | os.rename(output_file_path, original_file_path) 42 | 43 | self.log.info(f"{original_file_path.name!r} patched.") 44 | -------------------------------------------------------------------------------- /src/core/utilities/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | -------------------------------------------------------------------------------- /src/core/utilities/exception_handler.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | import logging 6 | import sys 7 | from traceback import format_exception 8 | from types import TracebackType 9 | from typing import Callable 10 | from winsound import MessageBeep as alert 11 | 12 | from PySide6.QtCore import QObject 13 | from PySide6.QtWidgets import QApplication 14 | 15 | from ui.widgets.error_dialog import ErrorDialog 16 | 17 | 18 | class ExceptionHandler(QObject): 19 | """ 20 | Redirects uncatched exceptions to an ErrorDialog instead of crashing the entire app. 21 | """ 22 | 23 | log: logging.Logger = logging.getLogger("ExceptionHandler") 24 | __sys_excepthook: ( 25 | Callable[[type[BaseException], BaseException, TracebackType | None], None] 26 | | None 27 | ) = None 28 | 29 | __parent: QApplication 30 | 31 | def __init__(self, parent: QApplication): 32 | super().__init__(parent) 33 | 34 | self.__parent = parent 35 | 36 | self.bind_hook() 37 | 38 | def bind_hook(self) -> None: 39 | """ 40 | Binds ExceptionHandler to `sys.excepthook`. 41 | """ 42 | 43 | if self.__sys_excepthook is None: 44 | self.__sys_excepthook = sys.excepthook 45 | sys.excepthook = self.__exception_hook 46 | 47 | def unbind_hook(self) -> None: 48 | """ 49 | Unbinds ExceptionHandler and restores original `sys.excepthook`. 50 | """ 51 | 52 | if self.__sys_excepthook is not None: 53 | sys.excepthook = self.__sys_excepthook 54 | self.__sys_excepthook = None 55 | 56 | def __exception_hook( 57 | self, 58 | exc_type: type[BaseException], 59 | exc_value: BaseException, 60 | exc_traceback: TracebackType | None, 61 | ) -> None: 62 | """ 63 | Redirects uncatched exceptions and shows them in an ErrorDialog. 64 | """ 65 | 66 | # Pass through if exception is KeyboardInterrupt (Ctrl + C) 67 | if issubclass(exc_type, KeyboardInterrupt): 68 | sys.__excepthook__(exc_type, exc_value, exc_traceback) 69 | return 70 | 71 | traceback = "".join(format_exception(exc_type, exc_value, exc_traceback)) 72 | self.log.critical("An uncaught exception occured:\n" + traceback) 73 | 74 | error_message = self.tr("An unexpected error occured: ") + str(exc_value) 75 | detailed_msg = traceback 76 | 77 | error_dialog = ErrorDialog( 78 | parent=self.__parent.activeModalWidget(), 79 | title=self.tr("Error"), 80 | text=error_message, 81 | details=detailed_msg, 82 | ) 83 | 84 | # Play system alarm sound 85 | alert() 86 | 87 | choice = error_dialog.exec() 88 | 89 | if choice == ErrorDialog.StandardButton.No: 90 | self.__parent.exit() 91 | -------------------------------------------------------------------------------- /src/core/utilities/filesystem.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | 4 | This module contains workaround functions for MO2 and Win 11 24H2. 5 | See this issue for more information: 6 | https://github.com/ModOrganizer2/modorganizer/issues/2174 7 | """ 8 | 9 | from pathlib import Path 10 | 11 | from PySide6.QtCore import QDir, QFile 12 | 13 | from .process_runner import run_process 14 | 15 | 16 | def file_path_to_qpath(path: str | Path) -> QFile: 17 | """ 18 | Creates a `QFile` from a file path. 19 | 20 | Args: 21 | path (str | Path): Path to file 22 | 23 | Returns: 24 | QFile: QFile object 25 | """ 26 | 27 | return QFile(str(path)) 28 | 29 | 30 | def folder_path_to_qpath(path: str | Path) -> QDir: 31 | """ 32 | Creates a `QDir` from a folder path. 33 | 34 | Args: 35 | path (str | Path): Path to folder 36 | 37 | Returns: 38 | QDir: QDir object 39 | """ 40 | 41 | return QDir(str(path)) 42 | 43 | 44 | def is_dir(path: Path) -> bool: 45 | """ 46 | Checks if a folder exists. Doesn't use `Path.is_dir()` since 47 | its known to be broken with Win 11 24H2. 48 | 49 | Args: 50 | path (Path): Path to check 51 | 52 | Returns: 53 | bool: True if the path exists, False otherwise 54 | """ 55 | 56 | return folder_path_to_qpath(path).exists() 57 | 58 | 59 | def is_file(path: Path) -> bool: 60 | """ 61 | Checks if a file exists. Doesn't use `Path.is_file()` since 62 | its known to be broken with Win 11 24H2. 63 | 64 | Args: 65 | path (Path): Path to check 66 | 67 | Returns: 68 | bool: True if the path exists, False otherwise 69 | """ 70 | 71 | qfile: QFile = file_path_to_qpath(path) 72 | 73 | return not is_dir(path) and qfile.exists() 74 | 75 | 76 | def mkdir(path: Path) -> None: 77 | """ 78 | Creates a directory. Doesn't use `Path.mkdir()` since 79 | its known to be broken with Win 11 24H2. 80 | 81 | Args: 82 | path (Path): Path to create 83 | """ 84 | 85 | if not is_dir(path) and not is_file(path): 86 | run_process(["mkdir", str(path).replace("/", "\\")]) 87 | elif is_file(path): 88 | raise FileExistsError(f"{str(path)!r} already exists!") 89 | -------------------------------------------------------------------------------- /src/core/utilities/glob.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | 4 | This module contains a workaround function for MO2 and Win 11 24H2. 5 | See this issue for more information: 6 | https://github.com/ModOrganizer2/modorganizer/issues/2174 7 | """ 8 | 9 | import ctypes 10 | from pathlib import Path 11 | 12 | lib = ctypes.CDLL("./glob.dll") 13 | 14 | ENCODING: str = "cp1252" 15 | """ 16 | The encoding used by the underlying C++ code. 17 | """ 18 | 19 | # Define function signatures 20 | lib.glob_cpp.argtypes = [ 21 | ctypes.POINTER(ctypes.c_char), 22 | ctypes.POINTER(ctypes.c_char), 23 | ctypes.c_bool, 24 | ctypes.POINTER(ctypes.c_size_t), 25 | ] 26 | lib.glob_cpp.restype = ctypes.POINTER(ctypes.c_char_p) 27 | lib.glob_clear.argtypes = [] 28 | lib.glob_clear.restype = None 29 | 30 | 31 | def glob(path: Path, pattern: str, recursive: bool = True) -> list[Path]: 32 | """ 33 | A glob.glob-like function that is implemented in C++ to workaround issues with MO2's 34 | VFS on Windows 11 24H2. 35 | 36 | Args: 37 | path (Path): Base path to search in. 38 | pattern (str): Glob pattern. 39 | recursive (bool, optional): Whether to search recursively. Defaults to True. 40 | 41 | Returns: 42 | list[str]: List of matching filenames 43 | """ 44 | 45 | count = ctypes.c_size_t() 46 | 47 | encoded_path: bytes = str(path).encode(ENCODING) 48 | encoded_pattern: bytes = pattern.encode(ENCODING) 49 | 50 | result_ptr = lib.glob_cpp( 51 | encoded_pattern, encoded_path, recursive, ctypes.byref(count) 52 | ) 53 | 54 | results: list[Path] = [ 55 | Path(result_ptr[i].decode(ENCODING)) for i in range(count.value) 56 | ] 57 | 58 | return results 59 | -------------------------------------------------------------------------------- /src/core/utilities/licenses.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | LICENSES = { 6 | "7-zip": "https://www.7-zip.org/license.txt", 7 | "cx_freeze": "https://github.com/marcelotduarte/cx_Freeze/blob/main/LICENSE.md", 8 | "jstyleson": "https://github.com/linjackson78/jstyleson/blob/master/LICENSE", 9 | "lz4": "https://github.com/python-lz4/python-lz4/blob/master/LICENSE", 10 | "py7zr": "https://github.com/miurahr/py7zr/blob/master/LICENSE", 11 | "pyperclip": "https://github.com/asweigart/pyperclip/blob/master/LICENSE.txt", 12 | "Qt6": "https://doc.qt.io/qt-6/lgpl.html", 13 | "qtawesome": "https://github.com/spyder-ide/qtawesome/blob/master/LICENSE.txt", 14 | "qtpy": "https://github.com/spyder-ide/qtpy/blob/master/LICENSE.txt", 15 | "PySide6": "https://code.qt.io/cgit/pyside/pyside-setup.git/tree/LICENSES?h=6.6.1", 16 | "rarfile": "https://github.com/markokr/rarfile/blob/master/LICENSE", 17 | "unrar": "https://github.com/matiasb/python-unrar/blob/master/LICENSE.txt", 18 | "virtual_glob": "https://pypi.org/project/virtual_glob/", 19 | "xdelta": "http://www.apache.org/licenses/LICENSE-2.0", 20 | } 21 | -------------------------------------------------------------------------------- /src/core/utilities/path_splitter.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | from pathlib import Path 6 | from typing import Optional 7 | 8 | 9 | def split_path_with_bsa(path: Path) -> tuple[Optional[Path], Optional[Path]]: 10 | """ 11 | Splits a path containing a BSA file and returns bsa path and file path. 12 | 13 | For example: 14 | ``` 15 | path = 'C:/Modding/RaceMenu/RaceMenu.bsa/interface/racesex_menu.swf' 16 | ``` 17 | ==> 18 | ``` 19 | ( 20 | 'C:/Modding/RaceMenu/RaceMenu.bsa', 21 | 'interface/racesex_menu.swf' 22 | ) 23 | ``` 24 | 25 | Args: 26 | path (Path): Path to split. 27 | 28 | Returns: 29 | tuple[Optional[Path], Optional[Path]]: 30 | BSA path or None and relative file path or None 31 | """ 32 | 33 | bsa_path: Optional[Path] = None 34 | file_path: Optional[Path] = None 35 | 36 | parts: list[str] = [] 37 | 38 | for part in path.parts: 39 | parts.append(part) 40 | 41 | if part.endswith(".bsa"): 42 | bsa_path = Path("/".join(parts)) 43 | parts.clear() 44 | 45 | if parts: 46 | file_path = Path("/".join(parts)) 47 | 48 | return (bsa_path, file_path) 49 | -------------------------------------------------------------------------------- /src/core/utilities/process_runner.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | import logging 6 | import subprocess 7 | 8 | log: logging.Logger = logging.getLogger("ProcessRunner") 9 | 10 | 11 | def run_process(command: list[str]) -> None: 12 | """ 13 | Executes an external command. 14 | 15 | Args: 16 | command (list[str]): Executable + Arguments to run 17 | 18 | Raises: 19 | RuntimeError: when the process returns a non-zero exit code. 20 | """ 21 | 22 | output: str = "" 23 | 24 | with subprocess.Popen( 25 | command, 26 | shell=True, 27 | stdin=subprocess.PIPE, 28 | stdout=subprocess.PIPE, 29 | stderr=subprocess.PIPE, 30 | text=True, 31 | encoding="utf8", 32 | errors="ignore", 33 | ) as process: 34 | if process.stderr is not None: 35 | output = process.stderr.read() 36 | 37 | if process.returncode: 38 | log.debug(f"Command: {command}") 39 | log.error(output) 40 | raise RuntimeError(f"Process returned non-zero exit code: {process.returncode}") 41 | -------------------------------------------------------------------------------- /src/core/utilities/qt_res_provider.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | from typing import Any 6 | 7 | import jstyleson as json # type: ignore[import-untyped] 8 | from PySide6.QtCore import QFile, QTextStream 9 | 10 | 11 | def read_resource(name: str) -> str: 12 | """ 13 | Reads the content of the specified resource. 14 | 15 | Args: 16 | name (str): Resource path to file to read. 17 | 18 | Raises: 19 | FileNotFoundError: When the resource does not exist. 20 | 21 | Returns: 22 | str: Content of the resource. 23 | """ 24 | 25 | file: QFile = QFile(name) 26 | 27 | if not file.exists(): 28 | raise FileNotFoundError(name) 29 | 30 | file.open(QFile.OpenModeFlag.ReadOnly | QFile.OpenModeFlag.Text) 31 | stream: QTextStream = QTextStream(file) 32 | 33 | content: str = stream.readAll() 34 | 35 | file.close() 36 | 37 | return content 38 | 39 | 40 | def load_json_resource(name: str) -> Any: 41 | """ 42 | Loads a resource a and deserializes it. 43 | 44 | Args: 45 | name (str): Resource path to file to load. 46 | 47 | Returns: 48 | Any: Deserialized content of the resource. 49 | """ 50 | 51 | return json.loads(read_resource(name)) 52 | -------------------------------------------------------------------------------- /src/core/utilities/status_update.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | from enum import Enum, auto 6 | 7 | 8 | class StatusUpdate(Enum): 9 | """ 10 | Enum for status updates, transmitted via Qt signals. 11 | """ 12 | 13 | Ready = auto() 14 | """ 15 | When Patcher or Patch Creator are ready. 16 | """ 17 | 18 | Running = auto() 19 | """ 20 | When Patcher or Patch Creator are running. 21 | """ 22 | 23 | Failed = auto() 24 | """ 25 | When Patcher or Patch Creator failed. 26 | """ 27 | 28 | Successful = auto() 29 | """ 30 | When Patcher or Patch Creator succeeded. 31 | """ 32 | -------------------------------------------------------------------------------- /src/core/utilities/stdout_handler.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | import os 6 | import sys 7 | from pathlib import Path 8 | from typing import Any, TextIO 9 | 10 | from PySide6.QtCore import QObject, Signal 11 | 12 | 13 | class StdoutHandler(QObject): 14 | """ 15 | Redirector class for sys.stdout. 16 | 17 | Redirects sys.stdout to self.output_signal [QtCore.Signal]. 18 | """ 19 | 20 | output_signal: Signal = Signal(str) 21 | _stream: TextIO 22 | _content: str 23 | 24 | log_file: Path = Path(os.getcwd()) / "DIP.log" 25 | __file_stream: TextIO 26 | 27 | def __init__(self, parent: QObject) -> None: 28 | super().__init__(parent) 29 | 30 | self._stream = sys.stdout 31 | sys.stdout = self 32 | self._content = "" 33 | 34 | self.__prepare_log_file() 35 | 36 | def __prepare_log_file(self) -> None: 37 | self.log_file.write_text("", encoding="utf8") 38 | 39 | self.__file_stream = self.log_file.open("a", encoding="utf8") 40 | 41 | def write(self, text: str) -> None: 42 | if self._stream is not None: 43 | self._stream.write(text) 44 | 45 | self._content += text 46 | self.__file_stream.write(text) 47 | self.output_signal.emit(text) 48 | 49 | def close(self) -> None: 50 | try: 51 | self.__file_stream.close() 52 | except Exception: 53 | pass 54 | 55 | def __getattr__(self, name: str) -> Any: 56 | return getattr(self._stream, name) 57 | 58 | def __del__(self) -> None: 59 | self.close() 60 | 61 | try: 62 | sys.stdout = self._stream 63 | except AttributeError: 64 | pass 65 | -------------------------------------------------------------------------------- /src/core/utilities/thread.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | from typing import Callable, Generic, Optional, TypeVar 6 | 7 | from PySide6.QtCore import QEventLoop, QThread 8 | from PySide6.QtWidgets import QWidget 9 | 10 | T = TypeVar("T") 11 | 12 | 13 | class Thread(QThread, Generic[T]): 14 | """ 15 | QThread that optionally blocks the caller thread's execution 16 | by executing its own event loop while the thread is running. 17 | """ 18 | 19 | __target: Callable[[], T] 20 | __event_loop: QEventLoop 21 | 22 | __return_result: T = None 23 | 24 | def __init__( 25 | self, 26 | target: Callable[[], T], 27 | name: str | None = None, 28 | parent: QWidget | None = None, 29 | ) -> None: 30 | super().__init__(parent) 31 | 32 | self.__target = target 33 | 34 | if name is not None: 35 | self.setObjectName(name) 36 | 37 | self.__event_loop = QEventLoop(self) 38 | self.finished.connect(self.__event_loop.quit) 39 | 40 | def start(self, block: bool = True) -> Optional[T]: 41 | """ 42 | Starts the thread and waits for it to finish, blocking the execution of MainThread, 43 | while keeping the Qt application responsive. 44 | 45 | Args: 46 | block (bool, optional): Blocks the caller thread's execution. Defaults to True. 47 | 48 | Returns: 49 | The return value of the target function. **None if block is False.** 50 | """ 51 | 52 | super().start() 53 | 54 | if block: 55 | self.__event_loop.exec() 56 | return self.__return_result 57 | 58 | def run(self) -> None: 59 | """ 60 | Runs the target function, storing its return value. 61 | 62 | The return value can be accessed with `start()`. 63 | """ 64 | 65 | try: 66 | self.__return_result = self.__target() 67 | except Exception as ex: 68 | self.__return_result = ex 69 | 70 | def get_result(self) -> T | Exception: 71 | """ 72 | Returns the return value of the target function. 73 | 74 | Returns: 75 | The return value or the exception of the target function. 76 | """ 77 | 78 | return self.__return_result 79 | -------------------------------------------------------------------------------- /src/core/utilities/xml_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | import xml.etree.ElementTree as ET 6 | from xml.dom.minidom import Document, parseString 7 | 8 | 9 | def split_frames(xml_element: ET.Element) -> ET.Element: 10 | """ 11 | Split frames in an XML element recursively and return the 12 | modified XML element with frames. 13 | 14 | Args: 15 | xml_element (ET.Element): XML element to split. 16 | 17 | Returns: 18 | ET.Element: Modified XML element with frames. 19 | """ 20 | 21 | new_frame = ET.Element("frame") 22 | current_frame: int = 1 23 | new_frame.set("frameId", str(current_frame)) 24 | new_frame_subtags = ET.Element("subTags") 25 | new_frame.append(new_frame_subtags) 26 | 27 | frame_delimiters: list[ET.Element] = xml_element.findall( 28 | "./item[@type='ShowFrameTag']" 29 | ) 30 | 31 | # Iterate over all child elements 32 | children: list[ET.Element] = xml_element.findall("./") 33 | for child in children: 34 | # Split child recursively 35 | child = split_frames(child) 36 | 37 | # If child is not a frame delimiter 38 | if len(frame_delimiters) > 1: 39 | # Remove child from xml_element 40 | xml_element.remove(child) 41 | 42 | # If child is not a frame delimiter 43 | if child.get("type") != "ShowFrameTag": 44 | # Append child to current frame 45 | new_frame_subtags.append(child) 46 | 47 | # If child is a frame delimiter 48 | elif child in frame_delimiters: 49 | xml_element.append(new_frame) 50 | # Create new frame 51 | new_frame = ET.Element("frame") 52 | current_frame += 1 53 | new_frame.set("frameId", str(current_frame)) 54 | new_frame_subtags = ET.Element("subTags") 55 | new_frame.append(new_frame_subtags) 56 | 57 | return xml_element 58 | 59 | 60 | def unsplit_frames(xml_element: ET.Element) -> ET.Element: 61 | """ 62 | This function is a reverse of `split_frames`. 63 | 64 | Args: 65 | xml_element (ET.Element): XML element to revert. 66 | 67 | Returns: 68 | ET.Element: Reverted XML element. 69 | """ 70 | 71 | children: list[ET.Element] = xml_element.findall("./") 72 | for child in children: 73 | frames: list[ET.Element] = child.findall("./frame") 74 | 75 | for frame in frames: 76 | child.remove(frame) 77 | 78 | for frame_child in frame.findall("./subTags/"): 79 | child.append(frame_child) 80 | 81 | frame_tag = ET.Element("item") 82 | frame_tag.set("type", "ShowFrameTag") 83 | child.append(frame_tag) 84 | 85 | unsplit_frames(child) 86 | 87 | return xml_element 88 | 89 | 90 | def beautify_xml(xml_string: str) -> str: 91 | """ 92 | Beautify an XML string. 93 | 94 | Args: 95 | xml_string (str): XML string to beautify. 96 | 97 | Returns: 98 | str: Beautified XML string. 99 | """ 100 | 101 | try: 102 | dom: Document = parseString(xml_string) 103 | pretty_xml: str = dom.toprettyxml() 104 | 105 | lines: list[str] = pretty_xml.splitlines() 106 | lines = [line + "\n" for line in lines if line.strip()] 107 | return "".join(lines) 108 | except Exception as ex: 109 | print(ex) 110 | 111 | return xml_string 112 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | import sys 6 | from argparse import ArgumentParser, Namespace 7 | 8 | import resources_rc # noqa: F401 9 | from app import App 10 | 11 | 12 | def __init_argparser() -> ArgumentParser: 13 | """ 14 | Initializes commandline argument parser. 15 | """ 16 | 17 | parser = ArgumentParser( 18 | prog=sys.executable, 19 | description=f"{App.APP_NAME} v{App.APP_VERSION} (c) Cutleast " 20 | "- An automated patcher for UI (swf) files.", 21 | ) 22 | parser.add_argument( 23 | "-d", 24 | "--debug", 25 | help="Enables debug mode so that debug files get outputted.", 26 | action="store_true", 27 | ) 28 | parser.add_argument( 29 | "patchpath", 30 | nargs="?", 31 | default="", 32 | help="Path to patch that gets automatically run. An original mod path must also be given!", 33 | ) 34 | parser.add_argument( 35 | "originalpath", 36 | nargs="?", 37 | default="", 38 | help="Path to original mod that gets automatically patched. A patch path must also be given!", 39 | ) 40 | parser.add_argument( 41 | "-b", 42 | "--repack-bsa", 43 | help="Enables experimental repacking of original BSA file(s).", 44 | action="store_true", 45 | ) 46 | parser.add_argument( 47 | "-o", 48 | "--output-path", 49 | help="Specifies output path for patched files.", 50 | ) 51 | parser.add_argument( 52 | "-s", 53 | "--silent", 54 | help="Toggles whether the GUI is shown while patching automatically.", 55 | action="store_true", 56 | ) 57 | 58 | return parser 59 | 60 | 61 | if __name__ == "__main__": 62 | parser: ArgumentParser = __init_argparser() 63 | arg_namespace: Namespace = parser.parse_args() 64 | 65 | sys.exit(App(arg_namespace).exec()) 66 | -------------------------------------------------------------------------------- /src/ui/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | -------------------------------------------------------------------------------- /src/ui/main_widget.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | import pyperclip as clipboard 6 | import qtawesome as qta 7 | from PySide6.QtCore import Signal 8 | from PySide6.QtGui import QTextCursor 9 | from PySide6.QtWidgets import ( 10 | QApplication, 11 | QHBoxLayout, 12 | QProgressBar, 13 | QPushButton, 14 | QSizePolicy, 15 | QTabWidget, 16 | QTextEdit, 17 | QVBoxLayout, 18 | QWidget, 19 | ) 20 | 21 | from core.utilities.status_update import StatusUpdate 22 | from core.utilities.stdout_handler import StdoutHandler 23 | 24 | from .patcher_widget import PatcherWidget 25 | 26 | 27 | class MainWidget(QWidget): 28 | """ 29 | Class for main widget. 30 | """ 31 | 32 | tab_widget: QTabWidget 33 | patcher_widget: PatcherWidget 34 | 35 | vlayout: QVBoxLayout 36 | 37 | incr_progress_signal = Signal() 38 | 39 | def __init__(self): 40 | super().__init__() 41 | 42 | self.__init_ui() 43 | 44 | self.patcher_widget.status_signal.connect(self.__handle_status_update) 45 | 46 | def __init_ui(self) -> None: 47 | self.vlayout = QVBoxLayout() 48 | self.setLayout(self.vlayout) 49 | 50 | self.__init_header() 51 | self.__init_protocol_widget() 52 | self.__init_progress_bar() 53 | self.__init_footer() 54 | 55 | def __init_header(self) -> None: 56 | self.tab_widget = QTabWidget() 57 | self.tab_widget.tabBar().setExpanding(True) 58 | self.tab_widget.tabBar().setDocumentMode(True) 59 | self.vlayout.addWidget(self.tab_widget) 60 | 61 | self.patcher_widget = PatcherWidget() 62 | self.tab_widget.addTab(self.patcher_widget, "Patcher") 63 | self.tab_widget.addTab(QWidget(), "Patch Creator") 64 | self.tab_widget.setTabToolTip(1, "Work in Progress...") 65 | self.tab_widget.setTabEnabled(1, False) 66 | 67 | def __init_protocol_widget(self) -> None: 68 | self.protocol_widget = QTextEdit() 69 | self.protocol_widget.setReadOnly(True) 70 | self.protocol_widget.setObjectName("protocol") 71 | self.vlayout.addWidget(self.protocol_widget, 1) 72 | 73 | stdout_handler: StdoutHandler = QApplication.instance().stdout_handler 74 | stdout_handler.output_signal.connect(self.__handle_stdout) 75 | stdout_handler.output_signal.emit(stdout_handler._content) 76 | 77 | def __init_progress_bar(self) -> None: 78 | self.progress_bar = QProgressBar() 79 | self.progress_bar.setRange(0, 1) 80 | self.progress_bar.setTextVisible(False) 81 | self.progress_bar.setFixedHeight(4) 82 | 83 | def incr_progress(): 84 | self.progress_bar.setValue(self.progress_bar.value() + 1) 85 | 86 | self.incr_progress_signal.connect(incr_progress) 87 | self.vlayout.addWidget(self.progress_bar) 88 | 89 | def __init_footer(self) -> None: 90 | hlayout = QHBoxLayout() 91 | self.vlayout.addLayout(hlayout) 92 | 93 | self.run_button = QPushButton("Run!") 94 | self.run_button.setObjectName("accent_button") 95 | self.run_button.setSizePolicy( 96 | QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred 97 | ) 98 | self.run_button.clicked.connect(self.run) 99 | self.patcher_widget.valid_signal.connect(self.run_button.setEnabled) 100 | hlayout.addWidget(self.run_button) 101 | 102 | copy_log_button = QPushButton() 103 | copy_log_button.setIcon(qta.icon("mdi6.content-copy", color="#ffffff")) 104 | copy_log_button.setToolTip("Copy Log to Clipboard") 105 | copy_log_button.clicked.connect( 106 | lambda: clipboard.copy(self.protocol_widget.toPlainText()) 107 | ) 108 | hlayout.addWidget(copy_log_button) 109 | 110 | def __handle_stdout(self, text: str) -> None: 111 | self.protocol_widget.insertPlainText(text) 112 | self.protocol_widget.moveCursor(QTextCursor.MoveOperation.End) 113 | 114 | def run(self) -> None: 115 | self.tab_widget.currentWidget().run() 116 | 117 | def cancel(self) -> None: 118 | self.tab_widget.currentWidget().cancel() 119 | 120 | def __handle_status_update(self, status_update: StatusUpdate) -> None: 121 | match status_update: 122 | case StatusUpdate.Running: 123 | self.progress_bar.setRange(0, 0) 124 | 125 | self.progress_bar.setProperty("failed", False) 126 | 127 | self.run_button.setText("Cancel") 128 | self.run_button.setObjectName("") 129 | self.run_button.clicked.disconnect() 130 | self.run_button.clicked.connect(self.cancel) 131 | 132 | self.tab_widget.setDisabled(True) 133 | 134 | case StatusUpdate.Successful | StatusUpdate.Ready | StatusUpdate.Failed: 135 | self.progress_bar.setRange(0, 1) 136 | self.progress_bar.setValue(1) 137 | 138 | if status_update == StatusUpdate.Failed: 139 | self.progress_bar.setProperty("failed", True) 140 | 141 | self.run_button.setText("Run!") 142 | self.run_button.setObjectName("accent_button") 143 | self.run_button.clicked.disconnect() 144 | self.run_button.clicked.connect(self.run) 145 | 146 | self.tab_widget.setDisabled(False) 147 | 148 | self.update() 149 | 150 | def update(self) -> None: 151 | self.style().unpolish(self) 152 | self.style().polish(self) 153 | 154 | self.progress_bar.style().unpolish(self.progress_bar) 155 | self.progress_bar.style().polish(self.progress_bar) 156 | 157 | self.run_button.style().unpolish(self.run_button) 158 | self.run_button.style().polish(self.run_button) 159 | 160 | super().update() 161 | -------------------------------------------------------------------------------- /src/ui/main_window.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | import os 6 | 7 | import qtawesome as qta 8 | from PySide6.QtCore import Qt 9 | from PySide6.QtGui import QColor 10 | from PySide6.QtWidgets import ( 11 | QApplication, 12 | QDialog, 13 | QHBoxLayout, 14 | QLabel, 15 | QListWidget, 16 | QMainWindow, 17 | QMessageBox, 18 | QTabWidget, 19 | QVBoxLayout, 20 | QWidget, 21 | ) 22 | 23 | from core.utilities.licenses import LICENSES 24 | 25 | from .main_widget import MainWidget 26 | 27 | 28 | class MainWindow(QMainWindow): 29 | """ 30 | Class for main patcher window. 31 | """ 32 | 33 | def __init__(self): 34 | super().__init__() 35 | 36 | self.setCentralWidget(MainWidget()) 37 | self.resize(1000, 600) 38 | 39 | # Menu Bar 40 | help_menu = self.menuBar().addMenu("Help") 41 | 42 | about_action = help_menu.addAction("About") 43 | about_action.setIcon(qta.icon("fa5s.info-circle", color="#ffffff")) 44 | about_action.triggered.connect(self.about) 45 | 46 | about_qt_action = help_menu.addAction("About Qt") 47 | about_qt_action.triggered.connect(self.about_qt) 48 | 49 | # Fix link color 50 | palette = self.palette() 51 | palette.setColor(palette.ColorRole.Link, QColor("#4994e0")) 52 | self.setPalette(palette) 53 | 54 | docs_label = QLabel( 55 | "\ 56 | Interested in creating own patches? \ 57 | Read the documentation \ 58 | \ 59 | here.\ 60 | " 61 | ) 62 | docs_label.setTextFormat(Qt.TextFormat.RichText) 63 | docs_label.setAlignment(Qt.AlignmentFlag.AlignRight) 64 | docs_label.setOpenExternalLinks(True) 65 | self.statusBar().addPermanentWidget(docs_label) 66 | 67 | def about(self): 68 | """ 69 | Displays about dialog. 70 | """ 71 | 72 | dialog = QDialog(QApplication.activeModalWidget()) 73 | dialog.setWindowTitle("About") 74 | 75 | vlayout = QVBoxLayout() 76 | dialog.setLayout(vlayout) 77 | 78 | tab_widget = QTabWidget() 79 | tab_widget.tabBar().setExpanding(True) 80 | tab_widget.setObjectName("centered_tab") 81 | vlayout.addWidget(tab_widget) 82 | 83 | about_tab = QWidget() 84 | about_tab.setObjectName("transparent") 85 | tab_widget.addTab(about_tab, "About") 86 | 87 | hlayout = QHBoxLayout() 88 | about_tab.setLayout(hlayout) 89 | 90 | hlayout.addSpacing(25) 91 | 92 | icon = self.windowIcon() 93 | pixmap = icon.pixmap(128, 128) 94 | icon_label = QLabel() 95 | icon_label.setPixmap(pixmap) 96 | hlayout.addWidget(icon_label) 97 | 98 | hlayout.addSpacing(15) 99 | 100 | vlayout = QVBoxLayout() 101 | hlayout.addLayout(vlayout, 1) 102 | 103 | hlayout.addSpacing(25) 104 | vlayout.addSpacing(25) 105 | 106 | title_label = QLabel( 107 | f"{QApplication.applicationName()} v{QApplication.applicationVersion()}" 108 | ) 109 | title_label.setObjectName("title_label") 110 | vlayout.addWidget(title_label) 111 | 112 | text = """ 113 | Created by Cutleast (NexusMods 114 | | GitHub | Ko-Fi) 115 |

116 | Icon by Wuerfelhusten (NexusMods) 117 |

118 | Licensed under GNU General Public License v3.0 119 | """ 120 | 121 | credits_label = QLabel(text) 122 | credits_label.setTextFormat(Qt.TextFormat.RichText) 123 | credits_label.setOpenExternalLinks(True) 124 | vlayout.addWidget(credits_label) 125 | 126 | vlayout.addSpacing(25) 127 | 128 | licenses_tab = QListWidget() 129 | tab_widget.addTab(licenses_tab, "Used Software") 130 | 131 | licenses_tab.addItems(LICENSES.keys()) 132 | 133 | licenses_tab.itemDoubleClicked.connect( 134 | lambda item: os.startfile(LICENSES[item.text()]) 135 | ) 136 | 137 | dialog.exec() 138 | 139 | def about_qt(self): 140 | """ 141 | Displays about Qt dialog. 142 | """ 143 | 144 | QMessageBox.aboutQt(QApplication.activeModalWidget(), "About Qt") 145 | -------------------------------------------------------------------------------- /src/ui/patcher_widget.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | import logging 6 | import os 7 | from argparse import Namespace 8 | from pathlib import Path 9 | from typing import Optional 10 | 11 | import qtawesome as qta 12 | from PySide6.QtCore import Qt, Signal 13 | from PySide6.QtWidgets import ( 14 | QApplication, 15 | QCheckBox, 16 | QComboBox, 17 | QFileDialog, 18 | QHBoxLayout, 19 | QLabel, 20 | QMessageBox, 21 | QPushButton, 22 | QSizePolicy, 23 | QVBoxLayout, 24 | QWidget, 25 | ) 26 | 27 | from core.config.config import Config 28 | from core.patcher.patcher import Patcher 29 | from core.utilities.filesystem import is_dir 30 | from core.utilities.status_update import StatusUpdate 31 | from core.utilities.thread import Thread 32 | 33 | 34 | class PatcherWidget(QWidget): 35 | """ 36 | Class for patcher widget. 37 | """ 38 | 39 | log: logging.Logger = logging.getLogger("Patcher") 40 | 41 | args: Namespace 42 | config: Config 43 | patcher: Patcher 44 | cwd_path: Path 45 | 46 | patcher_thread: Optional[Thread] = None 47 | 48 | status_signal = Signal(StatusUpdate) 49 | valid_signal = Signal(bool) 50 | 51 | def __init__(self): 52 | super().__init__() 53 | 54 | self.args = QApplication.instance().args 55 | self.config = QApplication.instance().config 56 | self.patcher = QApplication.instance().patcher 57 | self.cwd_path = QApplication.instance().cwd_path 58 | 59 | self.__init_ui() 60 | 61 | QApplication.instance().ready_signal.connect(self.__on_app_ready) 62 | 63 | self.status_signal.emit(StatusUpdate.Ready) 64 | 65 | def __on_app_ready(self): 66 | self.__validate() 67 | 68 | if (patch_path := self.args.patchpath) and ( 69 | original_path := self.args.originalpath 70 | ): 71 | self.log.info("Patching automatically...") 72 | self.patch_path_entry.setCurrentText(patch_path) 73 | self.mod_path_entry.setCurrentText(original_path) 74 | self.run() 75 | 76 | def __init_ui(self) -> None: 77 | self.setObjectName("transparent") 78 | 79 | vlayout = QVBoxLayout() 80 | self.setLayout(vlayout) 81 | 82 | patch_path_layout = QHBoxLayout() 83 | vlayout.addLayout(patch_path_layout) 84 | patch_path_label = QLabel("Path to DIP Patch:") 85 | patch_path_label.setFixedWidth(175) 86 | patch_path_layout.addWidget(patch_path_label) 87 | self.patch_path_entry = QComboBox() 88 | self.patch_path_entry.setSizePolicy( 89 | QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred 90 | ) 91 | self.patch_path_entry.setEditable(True) 92 | self.patch_path_entry.currentTextChanged.connect(lambda _: self.__validate()) 93 | patch_path_layout.addWidget(self.patch_path_entry) 94 | patch_path_button = QPushButton() 95 | patch_path_button.setIcon(qta.icon("fa.folder-open", color="#ffffff")) 96 | 97 | def browse_patch_path(): 98 | file_dialog = QFileDialog(QApplication.activeModalWidget()) 99 | file_dialog.setWindowTitle("Browse DIP Patch...") 100 | path = ( 101 | Path(self.patch_path_entry.currentText()) 102 | if self.patch_path_entry.currentText() 103 | else Path(".") 104 | ) 105 | path = path.resolve() 106 | file_dialog.setDirectory(str(path.parent)) 107 | file_dialog.setFileMode(QFileDialog.FileMode.Directory) 108 | if file_dialog.exec(): 109 | folder = file_dialog.selectedFiles()[0] 110 | folder = os.path.normpath(folder) 111 | self.patch_path_entry.setCurrentText(folder) 112 | 113 | patch_path_button.clicked.connect(browse_patch_path) 114 | patch_path_layout.addWidget(patch_path_button) 115 | 116 | mod_path_layout = QHBoxLayout() 117 | vlayout.addLayout(mod_path_layout) 118 | mod_path_label = QLabel("Path to Skyrim's Data folder:") 119 | mod_path_label.setFixedWidth(175) 120 | mod_path_layout.addWidget(mod_path_label) 121 | self.mod_path_entry = QComboBox() 122 | self.mod_path_entry.setSizePolicy( 123 | QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred 124 | ) 125 | self.mod_path_entry.setEditable(True) 126 | self.mod_path_entry.currentTextChanged.connect(lambda _: self.__validate()) 127 | mod_path_layout.addWidget(self.mod_path_entry) 128 | mod_path_button = QPushButton() 129 | mod_path_button.setIcon(qta.icon("fa.folder-open", color="#ffffff")) 130 | 131 | def browse_mod_path(): 132 | file_dialog = QFileDialog(QApplication.activeModalWidget()) 133 | file_dialog.setWindowTitle("Browse Data folder...") 134 | path = ( 135 | Path(self.mod_path_entry.currentText()) 136 | if self.mod_path_entry.currentText() 137 | else Path(".") 138 | ) 139 | path = path.resolve() 140 | file_dialog.setDirectory(str(path.parent)) 141 | file_dialog.setFileMode(QFileDialog.FileMode.Directory) 142 | if file_dialog.exec(): 143 | folder = file_dialog.selectedFiles()[0] 144 | folder = os.path.normpath(folder) 145 | self.mod_path_entry.setCurrentText(folder) 146 | 147 | mod_path_button.clicked.connect(browse_mod_path) 148 | mod_path_layout.addWidget(mod_path_button) 149 | 150 | self.repack_checkbox = QCheckBox( 151 | "Repack BSA(s) (Warning! The original BSA(s) get(s) overwritten!) (Experimental, use at your own risk!)" 152 | ) 153 | self.repack_checkbox.setChecked(self.config.repack_bsas) 154 | self.repack_checkbox.checkStateChanged.connect(self.__on_repack_changed) 155 | vlayout.addWidget(self.repack_checkbox) 156 | 157 | self.__init_entries() 158 | 159 | def __on_repack_changed(self, check_state: Qt.CheckState) -> None: 160 | self.config.repack_bsas = self.repack_checkbox.isChecked() 161 | 162 | def __init_entries(self) -> None: 163 | self.patch_path_entry.addItems(self.patcher.get_patches()) 164 | 165 | # If the current working directory is the data folder, 166 | # set the mod path to the parent folder 167 | parent_folder = self.cwd_path.parent 168 | if parent_folder.parts[-1].lower() == "data": 169 | self.mod_path_entry.setCurrentText(str(parent_folder)) 170 | elif self.cwd_path.parts[-1].lower() == "data": 171 | self.mod_path_entry.setCurrentText(str(self.cwd_path)) 172 | 173 | def __validate(self) -> None: 174 | patch_path = Path(self.patch_path_entry.currentText()).resolve() 175 | mod_path = Path(self.mod_path_entry.currentText()).resolve() 176 | 177 | if not self.patcher.check_patch(patch_path): 178 | self.valid_signal.emit(False) 179 | return 180 | 181 | if not is_dir(mod_path): 182 | self.valid_signal.emit(False) 183 | return 184 | 185 | self.valid_signal.emit(True) 186 | 187 | def run(self) -> None: 188 | patch_path = Path(self.patch_path_entry.currentText()).resolve() 189 | mod_path = Path(self.mod_path_entry.currentText()).resolve() 190 | 191 | self.status_signal.emit(StatusUpdate.Running) 192 | 193 | self.patcher_thread = Thread( 194 | lambda: self.patcher.patch(patch_path, mod_path), 195 | "PatcherThread", 196 | self, 197 | ) 198 | self.patcher_thread.finished.connect(self.on_done) 199 | self.patcher_thread.start(block=False) 200 | 201 | def on_done(self) -> None: 202 | # Check if thread was terminated externally 203 | if self.patcher_thread is None: 204 | self.status_signal.emit(StatusUpdate.Failed) 205 | return 206 | 207 | duration: float | Exception = self.patcher_thread.get_result() 208 | self.patcher_thread = None 209 | 210 | if isinstance(duration, Exception): 211 | self.log.error(f"Failed to patch: {duration}", exc_info=duration) 212 | self.status_signal.emit(StatusUpdate.Failed) 213 | 214 | if self.config.silent: 215 | QApplication.instance().exit(1) 216 | else: 217 | message_box = QMessageBox(QApplication.activeModalWidget()) 218 | message_box.setWindowTitle("Patch failed!") 219 | message_box.setText(f"Failed to patch: {duration}") 220 | message_box.setStandardButtons(QMessageBox.StandardButton.Ok) 221 | message_box.exec() 222 | 223 | if self.args.patchpath and self.args.originalpath: 224 | QApplication.instance().exit(1) 225 | else: 226 | return 227 | 228 | self.status_signal.emit(StatusUpdate.Successful) 229 | 230 | if self.args.patchpath and self.args.originalpath and not self.config.silent: 231 | message_box = QMessageBox(QApplication.activeModalWidget()) 232 | message_box.setWindowTitle(f"Patch completed in {duration:.3f} second(s)!") 233 | message_box.setText("Patch successfully completed.") 234 | message_box.setStandardButtons(QMessageBox.StandardButton.Ok) 235 | message_box.exec() 236 | QApplication.instance().exit() 237 | 238 | elif not self.config.silent: 239 | message_box = QMessageBox(QApplication.activeModalWidget()) 240 | message_box.setWindowTitle(f"Patch completed in {duration:.3f} second(s)!") 241 | message_box.setText("Patch successfully completed.") 242 | message_box.setStandardButtons( 243 | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Yes 244 | ) 245 | message_box.setDefaultButton(QMessageBox.StandardButton.Yes) 246 | message_box.button(QMessageBox.StandardButton.Yes).setText("Close DIP") 247 | message_box.button(QMessageBox.StandardButton.No).setText("Ok") 248 | choice = message_box.exec() 249 | 250 | # Handle the user's choice 251 | if choice == QMessageBox.StandardButton.Yes: 252 | # Close DIP 253 | QApplication.instance().exit() 254 | 255 | else: 256 | QApplication.instance().exit() 257 | 258 | def cancel(self): 259 | if self.patcher_thread is not None: 260 | self.patcher_thread.terminate() 261 | self.patcher_thread = None 262 | 263 | self.mod_path_entry.setEnabled(True) 264 | self.patch_path_entry.setEnabled(True) 265 | self.log.warning("Patch incomplete!") 266 | -------------------------------------------------------------------------------- /src/ui/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | -------------------------------------------------------------------------------- /src/ui/widgets/browse_edit.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | import os 6 | 7 | import qtawesome as qta 8 | from PySide6.QtCore import Qt 9 | from PySide6.QtWidgets import QFileDialog, QHBoxLayout, QLineEdit, QPushButton 10 | 11 | 12 | class BrowseLineEdit(QLineEdit): 13 | """ 14 | Custom QLineEdit with a "Browse" button to open a QFileDialog. 15 | """ 16 | 17 | __browse_button: QPushButton 18 | __file_dialog: QFileDialog 19 | 20 | def __init__(self, *args, **kwargs) -> None: 21 | super().__init__(*args, **kwargs) 22 | 23 | self.__file_dialog = QFileDialog() 24 | 25 | hlayout: QHBoxLayout = QHBoxLayout(self) 26 | hlayout.setContentsMargins(0, 0, 0, 0) 27 | 28 | # Push Browse Button to the right-hand side 29 | hlayout.addStretch() 30 | 31 | self.__browse_button = QPushButton() 32 | self.__browse_button.setIcon( 33 | qta.icon( 34 | "fa5s.folder-open", 35 | color=self.palette().text().color(), 36 | scale_factor=1.5, 37 | ) 38 | ) 39 | self.__browse_button.clicked.connect(self.__browse) 40 | self.__browse_button.setCursor(Qt.CursorShape.ArrowCursor) 41 | hlayout.addWidget(self.__browse_button) 42 | 43 | def configureFileDialog(self, *args, **kwargs) -> None: 44 | """ 45 | Redirects `args` and `kwargs` to constructor of `QFileDialog`. 46 | """ 47 | 48 | self.__file_dialog = QFileDialog(*args, **kwargs) 49 | 50 | def setFileMode(self, mode: QFileDialog.FileMode) -> None: 51 | """ 52 | Redirects `mode` to `QFileDialog.setFileMode()`. 53 | """ 54 | 55 | self.__file_dialog.setFileMode(mode) 56 | 57 | def __browse(self) -> None: 58 | current_text: str = self.text().strip() 59 | 60 | if current_text: 61 | current_path = os.path.normpath(current_text) 62 | if self.__file_dialog.fileMode() == QFileDialog.FileMode.Directory: 63 | self.__file_dialog.setDirectory(current_path) 64 | else: 65 | self.__file_dialog.setDirectory(os.path.dirname(current_path)) 66 | self.__file_dialog.selectFile(os.path.basename(current_path)) 67 | 68 | if self.__file_dialog.exec(): 69 | selected_files = self.__file_dialog.selectedFiles() 70 | 71 | if selected_files: 72 | file = os.path.normpath(selected_files.pop()) 73 | self.setText(file) 74 | 75 | 76 | def test(): 77 | from PySide6.QtWidgets import QApplication 78 | 79 | app = QApplication() 80 | 81 | edit = BrowseLineEdit() 82 | edit.setFileMode(QFileDialog.FileMode.AnyFile) 83 | edit.show() 84 | 85 | app.exec() 86 | 87 | 88 | if __name__ == "__main__": 89 | test() 90 | -------------------------------------------------------------------------------- /src/ui/widgets/error_dialog.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Cutleast 3 | """ 4 | 5 | import pyperclip as clipboard 6 | import qtawesome as qta 7 | from PySide6.QtCore import Qt 8 | from PySide6.QtWidgets import QApplication, QLabel, QMessageBox, QPushButton, QWidget 9 | 10 | 11 | class ErrorDialog(QMessageBox): 12 | """ 13 | Custom error messagebox with short text and detailed text functionality. 14 | 15 | Args: 16 | parent (QWidget | None): Parent window. `None` sets active modal widget as parent. 17 | title (str): Window title. 18 | text (str): Short message. 19 | details (str, optional): Long message that is displayed when details are shown. 20 | yesno (bool, optional): 21 | Toggles 'Continue' and 'Cancel' buttons or only an 'ok' button. Defaults to True. 22 | """ 23 | 24 | def __init__( 25 | self, 26 | parent: QWidget | None, 27 | title: str, 28 | text: str, 29 | details: str = "", 30 | yesno: bool = True, 31 | ): 32 | if parent is None: 33 | parent = QApplication.activeModalWidget() 34 | 35 | super().__init__(parent) 36 | 37 | # Basic configuration 38 | self.setWindowTitle(title) 39 | self.setIcon(QMessageBox.Icon.Critical) 40 | self.setText(text) 41 | 42 | icon_color = self.palette().text().color() 43 | 44 | # Show 'continue' and 'cancel' button 45 | if yesno: 46 | self.setStandardButtons( 47 | QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No 48 | ) 49 | self.button(QMessageBox.StandardButton.Yes).setText("Continue") 50 | self.button(QMessageBox.StandardButton.No).setText("Exit") 51 | 52 | # Only show 'ok' button 53 | else: 54 | self.setStandardButtons(QMessageBox.StandardButton.Ok) 55 | 56 | # Add details button if details are given 57 | if details: 58 | self.details_button: QPushButton = self.addButton( 59 | "Show Details...", QMessageBox.ButtonRole.AcceptRole 60 | ) 61 | self.details_button.setIcon(qta.icon("fa5s.chevron-down", color=icon_color)) 62 | 63 | self.copy_button: QPushButton = self.addButton( 64 | "", QMessageBox.ButtonRole.YesRole 65 | ) 66 | self.copy_button.setText("") 67 | self.copy_button.setIcon(qta.icon("mdi6.content-copy")) 68 | self.copy_button.clicked.disconnect() 69 | self.copy_button.clicked.connect(lambda: clipboard.copy(details)) 70 | self.copy_button.clicked.connect( 71 | lambda: self.copy_button.setIcon(qta.icon("fa5s.check")) 72 | ) 73 | 74 | self._details = False 75 | label: QLabel = self.findChild(QLabel) 76 | 77 | def toggle_details(): 78 | # toggle details 79 | if not self._details: 80 | self._details = True 81 | self.details_button.setText("Hide Details...") 82 | self.details_button.setIcon( 83 | qta.icon("fa5s.chevron-up", color=icon_color) 84 | ) 85 | self.setInformativeText( 86 | f"

{details}

" 87 | ) 88 | label.setTextInteractionFlags( 89 | Qt.TextInteractionFlag.TextSelectableByMouse 90 | ) 91 | label.setCursor(Qt.CursorShape.IBeamCursor) 92 | else: 93 | self._details = False 94 | self.details_button.setText("Show Details...") 95 | self.details_button.setIcon( 96 | qta.icon("fa5s.chevron-down", color=icon_color) 97 | ) 98 | self.setInformativeText("") 99 | label.setTextInteractionFlags( 100 | Qt.TextInteractionFlag.NoTextInteraction 101 | ) 102 | label.setCursor(Qt.CursorShape.ArrowCursor) 103 | 104 | # update messagebox size 105 | self.adjustSize() 106 | 107 | self.details_button.clicked.disconnect() 108 | self.details_button.clicked.connect(toggle_details) 109 | -------------------------------------------------------------------------------- /version.yml: -------------------------------------------------------------------------------- 1 | CompanyName: Cutleast 2 | FileDescription: Dynamic Interface Patcher 3 | InternalName: Dynamic Interface Patcher 4 | LegalCopyright: Copyright (c) Cutleast 5 | OriginalFilename: DIP.exe 6 | ProductName: Dynamic Interface Patcher 7 | Translation: 8 | - langID: 0 9 | charsetID: 1200 10 | - langID: 1033 11 | charsetID: 1252 12 | --------------------------------------------------------------------------------