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 |
12 |
--------------------------------------------------------------------------------
/res/icons/arrow_right.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
--------------------------------------------------------------------------------
/res/icons/arrow_up.svg:
--------------------------------------------------------------------------------
1 |
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 |