├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── dist ├── LICENSE ├── README.md ├── docs │ ├── api.html │ ├── buildPatches.html │ ├── managePatchersModal.html │ ├── overview.html │ ├── patcherHelpers.json │ ├── patcherModules.html │ ├── patcherSchema.json │ ├── patcherSettings.json │ ├── patcherVariables.json │ ├── topics.json │ └── upfSettings.html ├── images │ └── banner.jpg ├── index.js ├── module.json ├── partials │ ├── buildPatches.html │ ├── ignorePlugins.html │ ├── managePatchersModal.html │ └── settings.html └── src │ ├── Directives │ └── ignorePlugins.js │ ├── Runners │ ├── managePatchersButton.js │ ├── managePatchersHotkey.js │ ├── managePatchersMenuItem.js │ ├── upfLoader.js │ └── upfSettingsTab.js │ ├── Services │ ├── idCacheService.js │ ├── patchBuilder.js │ ├── patchPluginWorker.js │ ├── patcherService.js │ └── patcherWorker.js │ ├── Views │ ├── buildPatches.js │ ├── managePatchersModal.js │ └── upfSettings.js │ └── helpers.js ├── docs ├── api.html ├── buildPatches.html ├── managePatchersModal.html ├── overview.html ├── patcherHelpers.json ├── patcherModules.html ├── patcherSchema.json ├── patcherSettings.json ├── patcherVariables.json ├── topics.json └── upfSettings.html ├── gulpfile.js ├── images └── banner.jpg ├── index.js ├── module.json ├── package-lock.json ├── package.json ├── partials ├── buildPatches.html ├── ignorePlugins.html ├── managePatchersModal.html └── settings.html ├── src ├── Directives │ └── ignorePlugins.js ├── Runners │ ├── managePatchersButton.js │ ├── managePatchersHotkey.js │ ├── managePatchersMenuItem.js │ ├── upfLoader.js │ └── upfSettingsTab.js ├── Services │ ├── idCacheService.js │ ├── patchBuilder.js │ ├── patchPluginWorker.js │ ├── patcherService.js │ └── patcherWorker.js ├── Views │ ├── buildPatches.js │ ├── managePatchersModal.js │ └── upfSettings.js └── helpers.js └── update_dist_branch.bat /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.png binary 3 | *.jpg binary 4 | *.ico binary 5 | *.icns binary -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | *.zip 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Colin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zedit-unified-patching-framework 2 | A zEdit module which provides a framework for dynamic patch generation, similar to SkyProc/SUM. 3 | 4 | ## patchers 5 | Check out the following patchers to get an idea of how to use UPF: 6 | 7 | ### Shared / General 8 | - Example Patcher - [github](https://github.com/z-edit/zedit-example-patcher) 9 | 10 | ### Fallout 4 11 | - Pra's zEdit Patchers - [nexus](https://www.nexusmods.com/fallout4/mods/33858) 12 | - Fallout Genetics - everyone is beautiful - [nexus](https://www.nexusmods.com/fallout4/mods/35459) 13 | - Save Your Finger - [nexus](https://www.nexusmods.com/fallout4/mods/38781) 14 | - No Lowered Suppressor and Bayonet Range - [nexus](https://www.nexusmods.com/fallout4/mods/38817) 15 | - No Lowered Automatic Weapon Damage - [nexus](https://www.nexusmods.com/fallout4/mods/38805) 16 | - Cherrys New Calibers Patcher - [nexus](https://www.nexusmods.com/fallout4/mods/46059) 17 | - Cherry's Damage Threshold Framework Patcher - [nexus](https://www.nexusmods.com/fallout4/mods/46358) 18 | - No Exterior Lights [nexus](https://www.nexusmods.com/fallout4/mods/34210) 19 | 20 | ### Skyrim 21 | - No Dragon LODs - [github](https://github.com/hishutup/hishy-no-dragon-lods), [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/13541/) 22 | - NPC Enchant Fix - [github](https://github.com/z-edit/hishy-npc-enchant-fix), [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/13543) 23 | - Cell Encounter Level in Name - [github](https://github.com/z-edit/hishy-cell-encounter-level-in-name), [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/13542) 24 | - Khajiit Ears Show - [github](https://github.com/hishutup/hishy-khajiit-ears-show), [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/13544) 25 | - Enchantment Restriction Remover - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/17370/) 26 | - True Equipment Overhaul (TEO) - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/18157) 27 | - True Unleveled Skyrim (TUS) - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/18342) 28 | - Hunterborn Patcher - [github](https://github.com/Hazado/Hunterborn-Creature-Patcher), [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/17993) 29 | - dualSheathRedux - [github](https://github.com/Qudix/dualSheathRedux) 30 | - loadScreenRemover - [github](https://github.com/Qudix/loadScreenRemover), [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/18279/) 31 | - oppositeAnimationDisabler - [github](https://github.com/Qudix/oppositeAnimationDisabler), [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/18281) 32 | - Know Your Enemy - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/13807) 33 | - Reproccer Reborn - [github](https://github.com/jdsmith2816/reproccer-reborn), [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/17913) 34 | - Skyrim Material Patcher - [github](https://github.com/z-edit/zedit-skyrim-material-patcher) 35 | - SORT - Scripted Overrides that Rename Things - [nexus](https://www.nexusmods.com/skyrim/mods/87820/) 36 | - Know Your Enemy - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/13807) 37 | - Randomized Birthstones Skyrim - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/23838) 38 | - Engarde - [nexus](https://www.nexusmods.com/skyrim/mods/97404) 39 | - Experience Mod - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/23589) 40 | - zEdit patchers warehouse - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/23254) 41 | - ENB Light Patcher - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/22574) 42 | - Keys Have Weight - [nexus](https://www.nexusmods.com/skyrim/mods/95168) 43 | - No Distant LOD for NPCs - [nexus](https://www.nexusmods.com/skyrim/mods/95175) 44 | - Animated Armory - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/25969) 45 | - NPC Stat Rescaler - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/24254) 46 | - Challenging Spell Learning - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/20521) 47 | - XP Editor - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/24356) 48 | - Limited Perk Trees - [nexus](https://www.nexusmods.com/skyrim/mods/95540) 49 | - Pick Your Poison - [nexus](https://www.nexusmods.com/skyrim/mods/96473) 50 | - Speed and Reach Fix - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/29847) 51 | - Enemy Releveler - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/32211) 52 | - Action Speed - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/35097) 53 | - Breakdown Recipe Generator - [github](https://github.com/Hazado/breakdownRecipeBuilder), [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/38273) 54 | - The Alchemist's Cookbook - [github](https://github.com/epic-crab/zedit-potion-recipes-patcher), [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/45866) 55 | 56 | ## installation 57 | 58 | 1. Download the [latest release archive](https://github.com/matortheeternal/zedit-unified-patching-framework/releases). 59 | 2. Extract the archive to zEdit's `modules` folder. 60 | 3. Run zEdit. 61 | 62 | ## dev notes 63 | The `dist` branch contains the `dist` subtree, allowing it to be a submodule of the [zEdit](https://github.com/matortheeternal/zedit) repo. The branch can be updated by running the command `git subtree split --branch dist --prefix dist/` on master. 64 | -------------------------------------------------------------------------------- /dist/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Colin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dist/README.md: -------------------------------------------------------------------------------- 1 | # zedit-unified-patching-framework 2 | A zEdit module which provides a framework for dynamic patch generation, similar to SkyProc/SUM. 3 | 4 | ## patchers 5 | Check out the following patchers to get an idea of how to use UPF: 6 | 7 | - Example Patcher - [github](https://github.com/z-edit/zedit-example-patcher) 8 | - No Dragon LODs - [github](https://github.com/hishutup/hishy-no-dragon-lods), [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/13541/) 9 | - NPC Enchant Fix - [github](https://github.com/z-edit/hishy-npc-enchant-fix), [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/13543) 10 | - Cell Encounter Level in Name - [github](https://github.com/z-edit/hishy-cell-encounter-level-in-name), [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/13542) 11 | - Khajiit Ears Show - [github](https://github.com/hishutup/hishy-khajiit-ears-show), [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/13544) 12 | - Enchantment Restriction Remover - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/17370/) 13 | - Total Equipment Overhaul (TEO) - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/18157) 14 | - True Unleveled Skyrim (TUS) - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/18342) 15 | - Hunterborn Patcher - [github](https://www.nexusmods.com/skyrimspecialedition/mods/17993) 16 | - dualSheathRedux - [github](https://github.com/Qudix/dualSheathRedux) 17 | - loadScreenRemover - [github](https://github.com/Qudix/loadScreenRemover), [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/18279/) 18 | - oppositeAnimationDisabler - [github](https://github.com/Qudix/oppositeAnimationDisabler), [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/18281) 19 | - Know Your Enemy - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/13807) 20 | - Reproccer Reborn - [github](https://github.com/jdsmith2816/reproccer-reborn), [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/17913) 21 | - Skyrim Material Patcher - [github](https://github.com/z-edit/zedit-skyrim-material-patcher) 22 | - Pra's zEdit Patchers - [nexus](https://www.nexusmods.com/fallout4/mods/33858) 23 | - SORT - Scripted Overrides that Rename Things - [nexus](https://www.nexusmods.com/skyrim/mods/87820/) 24 | - Know Your Enemy - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/13807) 25 | - Randomized Birthstones Skyrim - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/23838) 26 | - Engarde - [nexus](https://www.nexusmods.com/skyrim/mods/97404) 27 | - Experience Mod - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/23589) 28 | - zEdit patchers warehouse - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/23254) 29 | - ENB Light Patcher - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/22574) 30 | - Keys Have Weight - [nexus](https://www.nexusmods.com/skyrim/mods/95168) 31 | - Fallout Genetics - everyone is beautiful - [nexus](https://www.nexusmods.com/fallout4/mods/35459) 32 | - No Distant LOD for NPCs - [nexus](https://www.nexusmods.com/skyrim/mods/95175) 33 | - Animated Armory - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/25969) 34 | - Save Your Finger - [nexus](https://www.nexusmods.com/fallout4/mods/38781) 35 | - No Lowered Suppressor and Bayonet Range - [nexus](https://www.nexusmods.com/fallout4/mods/38817) 36 | - No Lowered Automatic Weapon Damage - [nexus](https://www.nexusmods.com/fallout4/mods/38805) 37 | - NPC Stat Rescaler - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/24254) 38 | - Challenging Spell Learning - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/20521) 39 | - XP Editor - [nexus](https://www.nexusmods.com/skyrimspecialedition/mods/24356) 40 | - Limited Perk Trees - [nexus](https://www.nexusmods.com/skyrim/mods/95540) 41 | - Pick Your Poison - [nexus](https://www.nexusmods.com/skyrim/mods/96473) 42 | 43 | ## installation 44 | 45 | 1. Download the [latest release archive](https://github.com/matortheeternal/zedit-unified-patching-framework/releases). 46 | 2. Extract the archive to zEdit's `modules` folder. 47 | 3. Run zEdit. 48 | 49 | ## dev notes 50 | The `dist` branch contains the `dist` subtree, allowing it to be a submodule of the [zEdit](https://github.com/matortheeternal/zedit) repo. The branch can be updated by running the command `git subtree split --branch dist --prefix dist/` on master. 51 | -------------------------------------------------------------------------------- /dist/docs/api.html: -------------------------------------------------------------------------------- 1 |

Patcher Modules

2 |

UPF patchers are Patcher Modules, and must specify the UPF module loader in their module.json file. All patcher modules should register a patcher using the registerPatcher function.

3 | 4 |

Patcher Schema

5 |

Patchers can be implemented using JavaScript classes or objects. You can choose which to use at your own discretion. Regardless of your choice, your patcher must conform to the following schema:

6 | 7 | 8 | 9 |

Patcher Helpers

10 |

Helpers which you can use at any point in your patcher's execution.

11 | 12 | 13 | 14 |

Special Patcher Settings

15 |

There are several special patcher settings that are used by UPF internally.

16 | 17 | 18 | 19 |

Patcher Locals

20 |

The locals object is passed to all patcher functions. The locals object allows you to persist variables across all steps of your patcher's execution. It's recommended to use the locals object over using variables defined directly in your module because the locals object does not persist between patcher executions.

-------------------------------------------------------------------------------- /dist/docs/buildPatches.html: -------------------------------------------------------------------------------- 1 |

The build patches tab allows you to:

2 | 3 | 11 | 12 |

Patch plugins

13 |

With UPF, the results of multiple patchers can be generated in a single plugin. In this documentation, we use the term "patch plugin" to refer to a plugin which is generated by one or more patchers.

14 | 15 |

Creating patch plugins

16 |

You can click the Add Plugin button to create a new patch plugin.

17 | 18 |

Renaming patch plugins

19 |

You can edit a patch plugin's filename by clicking on it. NOTE: most tools require plugin filenames to end in a known extension such as .esp or .esl.

20 | 21 |

Building patching plugins

22 |

You can click the Build button to build a patch plugin. NOTE: If the patch plugin is loaded it will be nuked - all records, groups, and masters will be removed from it. If the plugin is not loaded but exists in your game data folder it will be deleted.

23 | 24 |

You can build all patch plugins by clicking the Build All button.

25 | 26 |

Deleting patch plugins

27 |

Patch plugins which have no patchers assigned to them will be automatically removed from the manage patchers modal when you close it.

28 | 29 |

Patchers

30 | 31 |

Toggling individual patchers

32 |

You can disable/enable individual patchers by toggling their corresponding checkbox. Disabled patchers will not be run when the patch plugin they are assigned to is built.

33 | 34 |

Disabled patchers

35 |

Patchers will be disabled if a required file is not loaded or is unavailable to the patch plugin it is assigned to due to load order. Disabled patchers will appear red, and will display a tooltip explaining why they were disabled when you hover over them.

36 | 37 |

Assigning patchers to patch plugins

38 |

You can move a patcher to a different patch plugin using drag and drop.

-------------------------------------------------------------------------------- /dist/docs/managePatchersModal.html: -------------------------------------------------------------------------------- 1 |

The manage patchers modal provides an interface to run patchers and edit patcher settings.

2 | 3 |

You can open the manage patchers modal once you've started a zEdit session by:

4 | 5 |
    6 |
  1. Clicking the icon in the title bar.
  2. 7 |
  3. Right-clicking in the tree view to open the context menu and clicking the Manage Patchers option.
  4. 8 |
  5. Clicking on the Manage Patchers button on the UPF settings tab.
  6. 9 |
  7. Pressing Ctrl + P.
  8. 10 |
11 | 12 |

Navigation

13 |

You can read further documentation on the tabs available in the manager patchers modal at the pages below:

14 | 15 | 16 | -------------------------------------------------------------------------------- /dist/docs/overview.html: -------------------------------------------------------------------------------- 1 |

The Unified Patching Framework (UPF) is a core module provided with zEdit. It provides an API for patchers - programs which generate patch plugins. It's similar to SkyProc, SUM, and MXPF, though there are some key differences.

2 | 3 |

UPF works with all games zEdit supports. UPF Patchers are only loaded when the user starts the application in Edit mode.

4 | 5 |

Language

6 |

UPF patchers use the same framework used for zEdit Modules. Like zEdit modules, they are coded in ES6 JavaScript and can use AngularJS, HTML, and CSS to create user interfaces.

7 | 8 |

ES6 JavaScript is a modern language which empowers developers to use functional and object oriented programming through classes, anonymous functions, and more.

9 | 10 |

API

11 |

UPF patchers use the xelib API. This is the same API that's used throughout zEdit. The API is very carefully designed, and offers a wide range of both generic and specific functions which make it easy to edit and build plugin files.

12 | 13 |

UPF patchers are built with the UPF Patcher API, which provides a highly structured system for defining patcher behavior. The system allows users to manage all of their patchers and patcher settings from a single modal - the manage patchers modal. Each patcher registers its own tab on the modal from which users can edit settings associated with it.

14 | 15 |

UPF handles the majority of the patching process out of the box, requiring developers to only write the code which is specific to their patcher. Developers can easily blacklist files to never be processed by their patcher, or require files to always be loaded for their patcher to be run.

16 | 17 |

UPF allows users to specify the filename of the plugin a patcher should generate, and allows single plugin files to be generated from multiple patchers. UPF's default behavior is to direct the output of all patchers to a single plugin file: zPatch.esp. Developers can override this behavior to have their patcher generate a unique plugin file by default.

18 | 19 |

UPF allows users to disable individual patchers without uninstalling them, and to customize the plugin files ignored by specific patchers.

20 | 21 |

Read more

22 |

You can read more about UPF on the following pages:

23 | 24 | -------------------------------------------------------------------------------- /dist/docs/patcherHelpers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "loadRecords", 4 | "type": "function", 5 | "args": [{ 6 | "name": "search", 7 | "type": "string", 8 | "description": "Records to search for. See xelib.GetRecords for more information." 9 | }, { 10 | "name": "includeOverrides", 11 | "type": "boolean", 12 | "description": "Pass true to load both master and override records. Default false." 13 | }], 14 | "returns": { 15 | "type": "array of handle" 16 | }, 17 | "description": "Helper function which allows you to load records from the files your patcher is targeting." 18 | }, 19 | { 20 | "name": "copyToPatch", 21 | "type": "function", 22 | "args": [{ 23 | "name": "rec", 24 | "type": "handle", 25 | "description": "Record to copy." 26 | }, { 27 | "name": "asNew", 28 | "type": "boolean", 29 | "description": "Whether or not to copy the record to the patch file as a new record. Default false." 30 | }], 31 | "returns": { 32 | "type": "handle", 33 | "description": "Handle for the record copied to the patch plugin." 34 | }, 35 | "description": "Helper function for copying records to your patch plugin without using a process block. Useful for copying globals and other individual records. It's recommended to prefer process blocks over this function." 36 | }, 37 | { 38 | "name": "allSettings", 39 | "type": "object", 40 | "description": "Contains the settings of all patchers, with each patcher's settings in a property corresponding to their id. Use this if you need to change your patcher's behavior when a user is using another patcher." 41 | }, 42 | { 43 | "name": "logMessage", 44 | "type": "function", 45 | "args": [{ 46 | "name": "message", 47 | "type": "string" 48 | }], 49 | "description": "Call this function to print a message to the progress modal's log." 50 | }, 51 | { 52 | "name": "cacheRecord", 53 | "type": "function", 54 | "args": [{ 55 | "name": "rec", 56 | "type": "handle" 57 | }, { 58 | "name": "id", 59 | "type": "string" 60 | }], 61 | "returns": { 62 | "type": "handle" 63 | }, 64 | "description": "Uses record consistency caching to make certain the input record `rec` stays at the same Form ID when the patch gets regenerated. This function should be used on all records created by UPF patchers, excluding overrides. The `id` should be a unique string value for the record. It is recommended to use a unique prefix for `id` to avoid collisions with other patchers. The record's editor ID will be set to `id` if the record has an Editor ID field." 65 | }, 66 | { 67 | "name": "addProgress", 68 | "type": "function", 69 | "args": [{ 70 | "name": "amount", 71 | "type": "number" 72 | }], 73 | "description": "Only available when `customProgress` is set in your patcher's execute block. Adds `amount` to the progress bar." 74 | } 75 | ] -------------------------------------------------------------------------------- /dist/docs/patcherModules.html: -------------------------------------------------------------------------------- 1 |

Patcher modules are added by UPF. Patcher modules are limited modules which use the UPF patcher loader.

2 | 3 |

Module Loader

4 |

Patcher module are defined by the "moduleLoader": "UPF" line in their module.json file.

5 | 6 |

Variables

7 |

Patcher modules are given access to the following variables:

8 | 9 | -------------------------------------------------------------------------------- /dist/docs/patcherSchema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "info", 4 | "type": "object", 5 | "description": "Your patcher module information. You should use the `info` variable as the value here." 6 | }, 7 | { 8 | "name": "gameModes", 9 | "type": "array of integer", 10 | "description": "Array of the game modes your patcher can be used with. Game modes are defined in xelib's [gameModes enumeration](docs://Development/APIs/xelib/Setup)." 11 | }, 12 | { 13 | "name": "settings", 14 | "type": "object", 15 | "description": "The patcher settings schema is used to register a settings tab for your patcher in the [manage patchers modal](docs://Modal Views/Manage Patchers Modal).", 16 | "items": [ 17 | { 18 | "name": "label", 19 | "type": "string", 20 | "description": "The label used for the tab in the manage patchers modal." 21 | }, 22 | { 23 | "name": "hide", 24 | "type": "boolean", 25 | "icons": ["optional"], 26 | "description": "Set to true to not display a settings tab in the manage patchers modal for your patcher." 27 | }, 28 | { 29 | "name": "templateUrl", 30 | "type": "string", 31 | "description": "URL to the HTML template to use for the settings tab. You'll want to use the `patcherUrl` global in this URL. E.g. `${patcherUrl}/partials/settings.html`." 32 | }, 33 | { 34 | "name": "controller", 35 | "type": "function", 36 | "icons": ["optional"], 37 | "description": "Controller function for use on the settings tab. Can use dependency injection per AngularJS." 38 | }, 39 | { 40 | "name": "defaultSettings", 41 | "type": "object", 42 | "description": "Object containing the default values for your patcher's settings. Settings can be any type. You should provide a default value for every setting your patcher supports." 43 | } 44 | ] 45 | }, 46 | { 47 | "name": "requiredFiles", 48 | "type": "function", 49 | "icons": ["optional"], 50 | "args": [], 51 | "returns": { 52 | "type": "array of string" 53 | }, 54 | "description": "Return an array of filenames which must be loaded for the patcher to run. Your patcher will be disabled if any file from this array is not loaded. Your patcher will also be disabled if a required file is loaded after the patch plugin the patcher has been assigned to." 55 | }, 56 | { 57 | "name": "requiredFiles", 58 | "type": "array of string", 59 | "icons": ["optional"], 60 | "legacy": true, 61 | "description": "Legacy option to provide required files as an array directly." 62 | }, 63 | { 64 | "name": "getFilesToPatch", 65 | "type": "function", 66 | "icons": ["optional"], 67 | "args": [{ 68 | "name": "filenames", 69 | "type": "array of string", 70 | "description": "The filenames of all files available for your patcher to patch." 71 | }], 72 | "returns": { 73 | "type": "array of string" 74 | }, 75 | "description": "Function which allows you to exclude certain files from patching. The array of filenames you return from the function will be the base file selection used by the patcher. You can use [Array.prototype.subtract](docs://Development/APIs/Polyfills) to remove files from the passed filenames array easily." 76 | }, 77 | { 78 | "name": "execute", 79 | "type": "function", 80 | "args": [{ 81 | "name": "patch", 82 | "type": "handle", 83 | "description": "Handle for the patch plugin your patcher is using." 84 | }, { 85 | "name": "helpers", 86 | "type": "object", 87 | "description": "Patcher helpers." 88 | }, { 89 | "name": "settings", 90 | "type": "object", 91 | "description": "Your patcher's settings." 92 | }, { 93 | "name": "locals", 94 | "type": "object", 95 | "description": "Patcher locals." 96 | }], 97 | "returns": { 98 | "type": "object", 99 | "description": "An Executor object." 100 | }, 101 | "description": "This function gets called when your patcher is executed. It should return an Executor object as described by the schema below.", 102 | "itemsLabel": "Executor", 103 | "items": [ 104 | { 105 | "name": "customProgress", 106 | "type": "function", 107 | "icons": ["optional"], 108 | "args": [{ 109 | "name": "filesToPatch", 110 | "type": "array of string", 111 | "description": "The filenames of the files the patcher will be run on." 112 | }], 113 | "returns": { 114 | "type": "number" 115 | }, 116 | "description": "Provide a function here to manage the progress bar manually. The function should return the max progress for the patcher." 117 | }, 118 | { 119 | "name": "initialize", 120 | "type": "function", 121 | "icons": ["optional"], 122 | "args": [{ 123 | "name": "patchFile", 124 | "type": "handle", 125 | "description": "Handle for the patch plugin your patcher is using." 126 | }, { 127 | "name": "helpers", 128 | "type": "object", 129 | "description": "Patcher helpers." 130 | }, { 131 | "name": "settings", 132 | "type": "object", 133 | "description": "Your patcher's settings." 134 | }, { 135 | "name": "locals", 136 | "type": "object", 137 | "description": "Patcher locals." 138 | }], 139 | "description": "Called before processing. Perform anything that needs to be done at the beginning of the patcher's execution in this function. This step is often used to load records which but need to be used in the patching process." 140 | }, 141 | { 142 | "name": "process", 143 | "type": "array of object", 144 | "description": "Array of process blocks which are executed sequentially. See the process block schema below for more information.", 145 | "itemsLabel": "Process Block Schema", 146 | "items": [ 147 | { 148 | "name": "load", 149 | "type": "object", 150 | "description": "Loaded records which pass `filter` will be copied to the patch plugin, and then passed to the `patch` function.", 151 | "itemsLabel": "Load Options Object", 152 | "items": [ 153 | { 154 | "name": "signature", 155 | "type": "string", 156 | "description": "Record signature to load. You can view record signatures by top level group names on the tree view and in record headers." 157 | }, 158 | { 159 | "name": "overrides", 160 | "type": "boolean", 161 | "icons": ["optional"], 162 | "description": "Pass true to include override records. Override records are not included by default." 163 | }, 164 | { 165 | "name": "filter", 166 | "type": "function", 167 | "icons": ["optional"], 168 | "args": [{ 169 | "name": "record", 170 | "type": "handle" 171 | }], 172 | "returns": { 173 | "type": "boolean" 174 | }, 175 | "description": "Filter function. Called for each loaded record. Return false to skip patching a record." 176 | } 177 | ] 178 | }, 179 | { 180 | "name": "load", 181 | "type": "function", 182 | "legacy": true, 183 | "args": [{ 184 | "name": "plugin", 185 | "type": "handle", 186 | "description": "Handle for the plugin to patch." 187 | }, { 188 | "name": "helpers", 189 | "type": "object", 190 | "description": "Patcher helpers." 191 | }, { 192 | "name": "settings", 193 | "type": "object", 194 | "description": "Your patcher's settings." 195 | }, { 196 | "name": "locals", 197 | "type": "object", 198 | "description": "Patcher locals." 199 | }], 200 | "returns": { 201 | "type": "object", 202 | "description": "A Load Options object." 203 | }, 204 | "description": "Legacy support for using a function instead of providing a load options object directly. Return null or undefined to skip loading records from a plugin, else return a load options object." 205 | }, 206 | { 207 | "name": "records", 208 | "type": "function", 209 | "icons": ["optional"], 210 | "args": [{ 211 | "name": "filesToPatch", 212 | "type": "array of handle", 213 | "description": "Array of file handles corresponding to the plugins being patched." 214 | }, { 215 | "name": "helpers", 216 | "type": "object", 217 | "description": "Patcher helpers." 218 | }, { 219 | "name": "settings", 220 | "type": "object", 221 | "description": "Your patcher's settings." 222 | }, { 223 | "name": "locals", 224 | "type": "object", 225 | "description": "Patcher locals." 226 | }], 227 | "returns": { 228 | "type": "array of handle", 229 | "description": "Array of records to patch." 230 | }, 231 | "description": "A function which can be used instead of `load`. The `records` function allows you to return a custom array of records to patch." 232 | }, 233 | { 234 | "name": "patch", 235 | "type": "function", 236 | "icons": ["optional"], 237 | "args": [{ 238 | "name": "record", 239 | "type": "handle", 240 | "description": "Handle for the patch record." 241 | }, { 242 | "name": "helpers", 243 | "type": "object", 244 | "description": "Patcher helpers." 245 | }, { 246 | "name": "settings", 247 | "type": "object", 248 | "description": "Your patcher's settings." 249 | }, { 250 | "name": "locals", 251 | "type": "object", 252 | "description": "Patcher locals." 253 | }], 254 | "description": "Called for each record copied to the patch plugin. This is the step where you set values on the record." 255 | } 256 | ] 257 | }, 258 | { 259 | "name": "finalize", 260 | "type": "function", 261 | "icons": ["optional"], 262 | "args": [{ 263 | "name": "patchFile", 264 | "type": "handle", 265 | "description": "Handle for the patch plugin your patcher is using." 266 | }, { 267 | "name": "helpers", 268 | "type": "object", 269 | "description": "Patcher helpers." 270 | }, { 271 | "name": "settings", 272 | "type": "object", 273 | "description": "Your patcher's settings." 274 | }, { 275 | "name": "locals", 276 | "type": "object", 277 | "description": "Patcher locals." 278 | }], 279 | "description": "Called after processing. Can be used to perform any cleanup/final steps once your patcher has finished executing. Note that UPF automatically removes ITPO records and unused masters, so you don't need to do that here." 280 | } 281 | ] 282 | }, 283 | { 284 | "name": "execute", 285 | "type": "object", 286 | "legacy": true, 287 | "description": "Legacy support for providing an Executor object directly instead of returning it from a function. Using this format means you must access `patchFile`, `helpers`, `settings`, and `locals` from arguments passed to the `initialize`, `finalize`, `load`, and `patch` function calls. This syntax is not recommended." 288 | } 289 | ] -------------------------------------------------------------------------------- /dist/docs/patcherSettings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "ignoredFiles", 4 | "type": "array of string", 5 | "description": "Array of filenames to ignore when patching. The `ignore-plugins` directive sets this value." 6 | }, 7 | { 8 | "name": "processDeletedRecords", 9 | "type": "boolean", 10 | "description": "If set to true deleted records will not be automatically excluded when loading records in process blocks or when using the `helpers.loadRecords` function." 11 | } 12 | ] -------------------------------------------------------------------------------- /dist/docs/patcherVariables.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "registerPatcher", 4 | "type": "function", 5 | "args": [{ 6 | "name": "patcher", 7 | "type": "object", 8 | "description": "Patcher to register." 9 | }], 10 | "description": "Function to register your patcher. Call this function with a patcher object or class as an argument. See the patcher schema for more details." 11 | }, 12 | { 13 | "name": "fh", 14 | "type": "object", 15 | "description": "The [fileHelpers API](docs://Development/APIs/File Helpers)." 16 | }, 17 | { 18 | "name": "info", 19 | "type": "object", 20 | "description": "Object containing information about your patcher module. Basically just your module.json." 21 | }, 22 | { 23 | "name": "patcherPath", 24 | "type": "string", 25 | "description": "Absolute path for the folder where your patcher module is installed on the user's machine. Should be prepended to paths when loading/saving files." 26 | }, 27 | { 28 | "name": "patcherUrl", 29 | "type": "string", 30 | "description": "`file://` URL for the folder where your patcher module is installed on the user's machine. Should be prepended to any HTML template/resource URLs." 31 | }, 32 | { 33 | "name": "xelib", 34 | "type": "object", 35 | "description": "The [xelib API](docs://Development/APIs/xelib)." 36 | } 37 | ] -------------------------------------------------------------------------------- /dist/docs/topics.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "path": "Modules/Core Modules", 3 | "topic": { 4 | "label": "Unified Patching Framework", 5 | "templateUrl": "/docs/overview.html" 6 | } 7 | }, { 8 | "path": "Modules", 9 | "topic": { 10 | "label": "Patcher Modules", 11 | "templateUrl": "/docs/patcherModules.html" 12 | } 13 | }, { 14 | "path": "Modal Views", 15 | "topic": { 16 | "label": "Manage Patchers Modal", 17 | "templateUrl": "/docs/managePatchersModal.html" 18 | } 19 | }, { 20 | "path": "Modal Views/Manage Patchers Modal", 21 | "topic": { 22 | "label": "Build Patches Tab", 23 | "templateUrl": "/docs/buildPatches.html" 24 | } 25 | }, { 26 | "path": "Modal Views/Settings Modal", 27 | "topic": { 28 | "label": "UPF Settings Tab", 29 | "templateUrl": "/docs/upfSettings.html" 30 | } 31 | }, { 32 | "path": "Development/APIs", 33 | "topic": { 34 | "label": "UPF Patcher API", 35 | "templateUrl": "/docs/api.html" 36 | } 37 | }] -------------------------------------------------------------------------------- /dist/docs/upfSettings.html: -------------------------------------------------------------------------------- 1 |

The UPF settings tab allows you to open the Manage Patchers Modal or reload patchers from disk.

2 | 3 |

You can reload UPF patchers by pressing Alt + F5 when a modal view is not active.

4 | 5 |

Note: reloading patchers does not update patcher settings changes, including GUI changes to your patcher's settings tab in the Manager Patchers Modal.

-------------------------------------------------------------------------------- /dist/images/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-edit/zedit-unified-patching-framework/b0fbe649e1cbcee7606d1df5e9ae1d41eafd82ba/dist/images/banner.jpg -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function(args) { 2 | let {fh, modulePath} = args, 3 | helpers = require('./src/helpers')(args), 4 | srcPath = fh.path(modulePath, 'src'); 5 | 6 | fh.getFiles(srcPath, { 7 | matching: '**/*.js' 8 | }).forEach(filePath => { 9 | let filename = fh.getFileName(filePath); 10 | if (filename === 'helpers.js') return; 11 | require(filePath)(args, helpers); 12 | }); 13 | }; 14 | 15 | -------------------------------------------------------------------------------- /dist/module.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "unifiedPatchingFramework", 3 | "name": "Unified Patching Framework", 4 | "author": "Mator", 5 | "version": "2.0.0", 6 | "repo": "https://github.com/matortheeternal/zedit-unified-patching-framework", 7 | "released": "9/7/2017", 8 | "updated": "6/24/2019", 9 | "description": "A framework for dynamic patchers, similar to SkyProc/SUM." 10 | } 11 | -------------------------------------------------------------------------------- /dist/partials/buildPatches.html: -------------------------------------------------------------------------------- 1 | 43 | 44 |

Build Patches

45 | 46 |
47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | {{::$parent.item.name}} 55 | 56 | 57 | (Patching {{$parent.item.filesToPatch.length}} files) 58 | 59 | 60 |
61 |
62 | 63 |
64 | No patchers found. 65 |
-------------------------------------------------------------------------------- /dist/partials/ignorePlugins.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ignoring {{ignoredPlugins.length}} Plugins 4 | 5 | 6 |
7 |
8 | 9 | 10 |
11 |
-------------------------------------------------------------------------------- /dist/partials/managePatchersModal.html: -------------------------------------------------------------------------------- 1 | 21 | 22 | 42 | -------------------------------------------------------------------------------- /dist/partials/settings.html: -------------------------------------------------------------------------------- 1 | 9 | 10 |

Unified Patching Framework

11 | 12 |
13 | 14 | 15 |
-------------------------------------------------------------------------------- /dist/src/Directives/ignorePlugins.js: -------------------------------------------------------------------------------- 1 | module.exports = function({ngapp, moduleUrl}) { 2 | ngapp.directive('ignorePlugins', () => ({ 3 | restrict: 'E', 4 | scope: { 5 | patcherId: '@' 6 | }, 7 | templateUrl: `${moduleUrl}/partials/ignorePlugins.html`, 8 | controller: 'ignorePluginsController' 9 | })); 10 | 11 | ngapp.controller('ignorePluginsController', function($scope, patcherService) { 12 | // helper functions 13 | let updateIgnoredFiles = function() { 14 | let settings = patcherService.settings[$scope.patcherId]; 15 | settings.ignoredFiles = $scope.ignoredPlugins 16 | .filter((item) => !item.invalid) 17 | .map((item) => item.filename); 18 | }; 19 | 20 | let getValid = function(item, itemIndex) { 21 | let filename = item.filename, 22 | isRequired = $scope.requiredPlugins.includes(filename), 23 | duplicate = $scope.ignoredPlugins.find((item, index) => { 24 | return item.filename === filename && index < itemIndex; 25 | }); 26 | return !isRequired && !duplicate; 27 | }; 28 | 29 | // scope functions 30 | $scope.toggleExpanded = function() { 31 | if ($scope.ignoredPlugins.length === 0) return; 32 | $scope.expanded = !$scope.expanded; 33 | }; 34 | 35 | $scope.addIgnoredPlugin = function() { 36 | if (!$scope.expanded) $scope.expanded = true; 37 | $scope.ignoredPlugins.push({ filename: 'Plugin.esp' }); 38 | $scope.onChange(); 39 | }; 40 | 41 | $scope.removeIgnoredPlugin = function(index) { 42 | $scope.ignoredPlugins.splice(index, 1); 43 | $scope.onChange(); 44 | }; 45 | 46 | $scope.onChange = function() { 47 | $scope.ignoredPlugins.forEach((item, index) => { 48 | item.invalid = !getValid(item, index); 49 | }); 50 | updateIgnoredFiles(); 51 | }; 52 | 53 | // initialization 54 | if (!$scope.patcherId) 55 | throw 'ignorePlugins Directive: patcher-id is required.'; 56 | 57 | let patcher = patcherService.getPatcher($scope.patcherId), 58 | ignored = patcherService.getIgnoredFiles(patcher); 59 | 60 | $scope.requiredPlugins = patcherService.getRequiredFiles(patcher); 61 | $scope.ignoredPlugins = ignored.map(filename => ({ filename })); 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /dist/src/Runners/managePatchersButton.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ngapp}, {openManagePatchersModal}) => 2 | ngapp.run(function($rootScope, buttonService) { 3 | let managePatchersButton = { 4 | class: 'fa fa-puzzle-piece', 5 | title: 'Manage Patchers', 6 | hidden: true, 7 | onClick: openManagePatchersModal 8 | }; 9 | buttonService.addButton(managePatchersButton); 10 | 11 | // make button visible when edit mode is started 12 | $rootScope.$on('filesLoaded', function() { 13 | if ($rootScope.appMode.id !== 'edit') return; 14 | $rootScope.$applyAsync(() => managePatchersButton.hidden = false); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /dist/src/Runners/managePatchersHotkey.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ngapp}, {openManagePatchersModal}) => 2 | ngapp.run(function(hotkeyService) { 3 | hotkeyService.addHotkeys('editView', { 4 | p: [{ 5 | modifiers: ['ctrlKey', 'shiftKey'], 6 | callback: openManagePatchersModal 7 | }], 8 | f5: [{ 9 | modifiers: ['altKey'], 10 | callback: scope => { 11 | if (scope.$root.modalActive) return; 12 | scope.$emit('reloadPatchers') 13 | } 14 | }] 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /dist/src/Runners/managePatchersMenuItem.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ngapp}, {openManagePatchersModal}) => 2 | ngapp.run(function(contextMenuService) { 3 | // add manage patchers context menu item to tree view context menu 4 | let menuItems = contextMenuService.getContextMenu('treeView'); 5 | let automateIndex = menuItems.findIndex(item => { 6 | return item.id === 'Automate'; 7 | }); 8 | 9 | menuItems.splice(automateIndex + 1, 0, { 10 | id: 'Manage Patchers', 11 | visible: () => true, 12 | build: (scope, items) => { 13 | items.push({ 14 | label: 'Manage Patchers', 15 | hotkey: 'Ctrl+Shift+P', 16 | callback: () => openManagePatchersModal(scope) 17 | }); 18 | } 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /dist/src/Runners/upfLoader.js: -------------------------------------------------------------------------------- 1 | module.exports = function({moduleService, ngapp}) { 2 | moduleService.deferLoader('UPF'); 3 | 4 | ngapp.run(function($rootScope, patcherService) { 5 | let upfLoader = function({module, fh, moduleService}) { 6 | moduleService.executeModule(module, { 7 | registerPatcher: patcherService.registerPatcher, 8 | fh: fh, 9 | info: module.info, 10 | patcherUrl: fh.pathToFileUrl(module.path), 11 | patcherPath: module.path 12 | }); 13 | moduleService.loadDocs(module.path); 14 | }; 15 | 16 | moduleService.registerLoader('UPF', upfLoader); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /dist/src/Runners/upfSettingsTab.js: -------------------------------------------------------------------------------- 1 | module.exports = function({ngapp, moduleUrl}) { 2 | ngapp.run(function(settingsService) { 3 | settingsService.registerSettings({ 4 | appModes: ['edit'], 5 | label: 'Unified Patching Framework', 6 | templateUrl: `${moduleUrl}/partials/settings.html`, 7 | controller: 'upfSettingsController' 8 | }); 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /dist/src/Services/idCacheService.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ngapp}) => 2 | ngapp.service('idCacheService', function(patcherService) { 3 | let prepareIdCache = function(patchFile) { 4 | let cache = patcherService.cache, 5 | fileName = xelib.Name(patchFile); 6 | if (!cache.hasOwnProperty(fileName)) cache[fileName] = {}; 7 | return cache[fileName]; 8 | }; 9 | 10 | let updateNextFormId = function(patchFile, idCache) { 11 | let formIds = Object.values(idCache), 12 | maxFormId = formIds.reduce((a, b) => Math.max(a, b), 0x7FF); 13 | xelib.SetNextObjectID(patchFile, maxFormId + 1); 14 | }; 15 | 16 | this.cacheRecord = function(patchFile) { 17 | let patchOrd = xelib.GetFileLoadOrder(patchFile) * 0x1000000, 18 | idCache = prepareIdCache(patchFile), 19 | usedIds = {}; 20 | 21 | updateNextFormId(patchFile, idCache); 22 | 23 | return function(rec, id) { 24 | if (!xelib.IsMaster(rec)) return; 25 | if (usedIds.hasOwnProperty(id)) 26 | throw new Error(`cacheRecord: ${id} is not unique.`); 27 | if (idCache.hasOwnProperty(id)) { 28 | xelib.SetFormID(rec, patchOrd + idCache[id], false, false); 29 | } else { 30 | idCache[id] = xelib.GetFormID(rec, false, true); 31 | } 32 | if (xelib.HasElement(rec, 'EDID')) xelib.SetValue(rec, 'EDID', id); 33 | usedIds[id] = true; 34 | return rec; 35 | }; 36 | }; 37 | }); 38 | -------------------------------------------------------------------------------- /dist/src/Services/patchBuilder.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ngapp}) => 2 | ngapp.service('patchBuilder', function($rootScope, $timeout, patcherService, patchPluginWorker, errorService, progressService) { 3 | let cache = {}; 4 | 5 | let build = (patchPlugin) => patchPluginWorker.run(cache, patchPlugin); 6 | 7 | let getExecutor = function(patcher) { 8 | return patcher.execute.constructor === Function ? 9 | patcher.execute(0, {}, {}, {}) : patcher.execute; 10 | }; 11 | 12 | let getProcessSize = function(process, files) { 13 | return process.reduce((sum, block) => { 14 | let patch = block.patch ? 1 : 0; 15 | if (block.records) return sum + 1 + patch; 16 | if (block.load) return sum + files.length * (2 + patch); 17 | return sum; 18 | }, 0); 19 | }; 20 | 21 | let getMaxProgress = function(patchPlugin) { 22 | return patchPlugin.patchers.filterOnKey('active') 23 | .map(p => patcherService.getPatcher(p.id)) 24 | .reduce((sum, patcher) => { 25 | let {customProgress, process} = getExecutor(patcher), 26 | files = patcher.filesToPatch; 27 | if (customProgress) return sum + customProgress(files); 28 | return sum + 2 + getProcessSize(process, files); 29 | }, 1); 30 | }; 31 | 32 | let getTotalMaxProgress = function(patchPlugins) { 33 | return patchPlugins.reduce((sum, patchPlugin) => { 34 | return sum + getMaxProgress(patchPlugin); 35 | }, 0); 36 | }; 37 | 38 | let openProgressModal = function(maxProgress) { 39 | $rootScope.$broadcast('closeModal'); 40 | progressService.showProgress({ 41 | determinate: true, 42 | echo: true, 43 | title: 'Running Patchers', 44 | message: 'Initializing...', 45 | current: 0, 46 | max: maxProgress 47 | }); 48 | }; 49 | 50 | let getActivePatchPlugins = function(patchPlugins) { 51 | return patchPlugins.filter(patchPlugin => !patchPlugin.disabled); 52 | }; 53 | 54 | let progressDone = function(patchPlugins, success) { 55 | let pluginsStr = `${patchPlugins.length} patch plugins`; 56 | progressService.progressTitle(success ? 57 | `${pluginsStr} built successfully` : 58 | `${pluginsStr} failed to build`); 59 | progressService.progressMessage(success ? 'All Done!' : 'Error'); 60 | progressService.allowClose(); 61 | }; 62 | 63 | // public functions 64 | this.buildPatchPlugins = function(patchPlugins) { 65 | let activePatchPlugins = getActivePatchPlugins(patchPlugins), 66 | maxProgress = getTotalMaxProgress(activePatchPlugins); 67 | if (activePatchPlugins.length === 0) return; 68 | xelib.CreateHandleGroup(); 69 | openProgressModal(maxProgress); 70 | $timeout(function() { 71 | patcherService.loadCache(); 72 | let success = errorService.try(() => 73 | activePatchPlugins.forEach(build)); 74 | success ? patcherService.saveCache() : patcherService.loadCache(); 75 | progressDone(activePatchPlugins, success); 76 | cache = {}; 77 | xelib.FreeHandleGroup(); 78 | $rootScope.$broadcast('reloadGUI'); 79 | }, 50); 80 | }; 81 | }); 82 | -------------------------------------------------------------------------------- /dist/src/Services/patchPluginWorker.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ngapp, fh}) => 2 | ngapp.service('patchPluginWorker', function(progressService, patcherWorker) { 3 | this.run = function(cache, patchPlugin) { 4 | let start = new Date(); 5 | 6 | let progressTitle = function(title) { 7 | progressService.progressTitle(title); 8 | }; 9 | 10 | let patcherProgress = function(message) { 11 | progressService.addProgress(1); 12 | progressService.progressMessage(message); 13 | }; 14 | 15 | let preparePatchFile = function(filename) { 16 | if (!xelib.HasElement(0, filename)) { 17 | let dataPath = xelib.GetGlobal('DataPath'); 18 | fh.jetpack.cwd(dataPath).remove(filename); 19 | } 20 | let patchFile = xelib.AddElement(0, filename); 21 | xelib.NukeFile(patchFile); 22 | xelib.AddAllMasters(patchFile); 23 | return patchFile; 24 | }; 25 | 26 | let cleanPatchFile = function(patchFile) { 27 | patcherProgress('Removing ITPOs and cleaning masters.'); 28 | try { 29 | xelib.RemoveIdenticalRecords(patchFile, false, true); 30 | } catch (x) { 31 | progressService.logMessage('Removing ITPOs failed: ' + x.message); 32 | } 33 | xelib.CleanMasters(patchFile); 34 | }; 35 | 36 | // MAIN WORKER EXECUTION 37 | let patchFileName = patchPlugin.filename, 38 | patchFile = preparePatchFile(patchFileName); 39 | patchPlugin.patchers.forEach(function(patcher) { 40 | if (!patcher.active) return; 41 | progressTitle(`Building ${patchFileName} ~ Running ${patcher.name}`); 42 | patcherWorker.run(cache, patchFileName, patchFile, patcher); 43 | }); 44 | cleanPatchFile(patchFile); 45 | console.log(`Generated ${patchFileName} in ${new Date() - start}ms`); 46 | }; 47 | }); 48 | -------------------------------------------------------------------------------- /dist/src/Services/patcherService.js: -------------------------------------------------------------------------------- 1 | module.exports = function({ngapp, moduleUrl, fh}) { 2 | ngapp.service('patcherService', function($rootScope, $cacheFactory, settingsService) { 3 | const disabledHintBase = 4 | 'This patcher is disabled because the following required' + 5 | '\r\nfiles are not available to the patch plugin:'; 6 | 7 | let service = this, 8 | patchers = [], 9 | tabs = [{ 10 | label: 'Build Patches', 11 | templateUrl: `${moduleUrl}/partials/buildPatches.html`, 12 | controller: 'buildPatchesController' 13 | }]; 14 | 15 | // private functions 16 | let getAvailableFiles = function(patcher) { 17 | let patchFileName = service.settings[patcher.info.id].patchFileName; 18 | return xelib.GetLoadedFileNames().itemsBefore(patchFileName); 19 | }; 20 | 21 | let getPatcherEnabled = function(patcher) { 22 | return service.settings[patcher.info.id].enabled; 23 | }; 24 | 25 | let getMissingRequirements = function(patcher) { 26 | return service.getRequiredFiles(patcher) 27 | .subtract(patcher.availableFiles); 28 | }; 29 | 30 | let getPatcherDisabled = function(patcher) { 31 | return getMissingRequirements(patcher).length > 0; 32 | }; 33 | 34 | let getDisabledHint = function(patcher) { 35 | return getMissingRequirements(patcher).reduce((hint, filename) => { 36 | return `${hint}\r\n - ${filename}`; 37 | }, disabledHintBase); 38 | }; 39 | 40 | let getDefaultSettings = function(patcher) { 41 | let defaultSettings = patcher.settings.defaultSettings || {}; 42 | return Object.assign({ 43 | patchFileName: 'zPatch.esp', 44 | ignoredFiles: [], 45 | enabled: true 46 | }, defaultSettings); 47 | }; 48 | 49 | let buildSettings = function(settings) { 50 | let defaults = { cache: {} }; 51 | patchers.forEach(function(patcher) { 52 | let patcherSettings = {}; 53 | patcherSettings[patcher.info.id] = getDefaultSettings(patcher); 54 | Object.deepAssign(defaults, patcherSettings); 55 | }); 56 | return Object.deepAssign(defaults, settings); 57 | }; 58 | 59 | let buildTabs = function() { 60 | patchers.forEach(function(patcher) { 61 | if (!patcher.settings.hide) tabs.push(patcher.settings); 62 | }); 63 | }; 64 | 65 | let getFilesToPatchHint = function(patcher) { 66 | let filesToPatch = patcher.filesToPatch, 67 | hint = filesToPatch.slice(0, 40).join(', '); 68 | if (filesToPatch.length > 40) hint += '...'; 69 | return hint.wordwrap(); 70 | }; 71 | 72 | let createPatchPlugin = function(patchPlugins, patchFileName) { 73 | let patchPlugin = { filename: patchFileName, patchers: [] }; 74 | patchPlugins.push(patchPlugin); 75 | return patchPlugin; 76 | }; 77 | 78 | let getPatchPlugin = function(patcher, patchPlugins) { 79 | let patchFileName = service.settings[patcher.info.id].patchFileName; 80 | return patchPlugins.find(patchPlugin => { 81 | return patchPlugin.filename === patchFileName; 82 | }) || createPatchPlugin(patchPlugins, patchFileName); 83 | }; 84 | 85 | // public functions 86 | this.getPatcher = function(id) { 87 | return patchers.find(patcher => patcher.info.id === id); 88 | }; 89 | 90 | this.registerPatcher = function(patcher) { 91 | if (service.getPatcher(patcher.info.id)) return; 92 | patchers.push(patcher); 93 | }; 94 | 95 | this.reloadPatchers = function() { 96 | let patcherIds = patchers.map(patcher => patcher.info.id); 97 | patchers = []; 98 | patcherIds.forEach(id => { 99 | let patcherPath = fh.jetpack.path(`modules\\${id}`); 100 | moduleService.loadModule(patcherPath); 101 | }); 102 | }; 103 | 104 | this.updateForGameMode = function(gameMode) { 105 | patchers = patchers.filter(patcher => { 106 | return patcher.gameModes.includes(gameMode); 107 | }); 108 | }; 109 | 110 | this.loadSettings = function() { 111 | let profileName = settingsService.currentProfile; 112 | service.settingsPath = `profiles/${profileName}/patcherSettings.json`; 113 | let settings = fh.loadJsonFile(service.settingsPath) || {}; 114 | service.settings = buildSettings(settings); 115 | service.saveSettings(); 116 | buildTabs(); 117 | }; 118 | 119 | this.loadCache = function() { 120 | let profileName = settingsService.currentProfile; 121 | service.cachePath = `profiles/${profileName}/patcherCache.json`; 122 | service.cache = fh.loadJsonFile(service.cachePath) || {}; 123 | }; 124 | 125 | this.saveSettings = function() { 126 | fh.saveJsonFile(service.settingsPath, service.settings); 127 | }; 128 | 129 | this.saveCache = function() { 130 | fh.saveJsonFile(service.cachePath, service.cache); 131 | }; 132 | 133 | this.getTabs = function() { 134 | return tabs.map(tab => ({ 135 | label: tab.label, 136 | templateUrl: tab.templateUrl, 137 | controller: tab.controller 138 | })); 139 | }; 140 | 141 | this.getRequiredFiles = function(patcher) { 142 | if (!patcher.requiredFiles) return []; 143 | if (patcher.requiredFiles.constructor === Function) 144 | return patcher.requiredFiles() || []; 145 | return patcher.requiredFiles; 146 | }; 147 | 148 | this.getIgnoredFiles = function(patcher) { 149 | return service.settings[patcher.info.id].ignoredFiles; 150 | }; 151 | 152 | this.getFilesToPatch = function(patcher) { 153 | let filesToPatch = patcher.availableFiles.slice(); 154 | if (patcher.getFilesToPatch) 155 | filesToPatch = patcher.getFilesToPatch(filesToPatch); 156 | return filesToPatch.subtract(service.getIgnoredFiles(patcher)); 157 | }; 158 | 159 | this.updateFilesToPatch = function() { 160 | patchers.forEach(patcher => { 161 | patcher.availableFiles = getAvailableFiles(patcher); 162 | patcher.filesToPatch = service.getFilesToPatch(patcher); 163 | }); 164 | }; 165 | 166 | this.getPatchPlugins = function() { 167 | let patchPlugins = []; 168 | patchers.forEach(patcher => { 169 | let patchPlugin = getPatchPlugin(patcher, patchPlugins), 170 | disabled = getPatcherDisabled(patcher); 171 | patchPlugin.patchers.push({ 172 | id: patcher.info.id, 173 | name: patcher.info.name, 174 | active: !disabled && getPatcherEnabled(patcher), 175 | disabled: disabled, 176 | disabledHint: disabled ? getDisabledHint(patcher) : '', 177 | filesToPatch: patcher.filesToPatch, 178 | filesToPatchHint: getFilesToPatchHint(patcher) 179 | }); 180 | }); 181 | return patchPlugins; 182 | }; 183 | 184 | $rootScope.$on('reloadPatchers', () => { 185 | tabs.forEach(tab => { 186 | if (!tab.templateUrl) return; 187 | $cacheFactory.get('templates').remove(tab.templateUrl); 188 | }); 189 | tabs = []; 190 | service.reloadPatchers(); 191 | service.loadSettings(); 192 | }); 193 | }); 194 | 195 | // register for events 196 | ngapp.run(function($rootScope, patcherService) { 197 | $rootScope.$on('filesLoaded', () => { 198 | if ($rootScope.appMode.id !== 'edit') return; 199 | patcherService.loadSettings(); 200 | }); 201 | 202 | $rootScope.$on('sessionStarted', (e, selectedProfile) => { 203 | patcherService.updateForGameMode(selectedProfile.gameMode); 204 | }); 205 | }); 206 | }; 207 | -------------------------------------------------------------------------------- /dist/src/Services/patcherWorker.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ngapp}) => 2 | ngapp.service('patcherWorker', function(patcherService, progressService, idCacheService, interApiService) { 3 | this.run = function(cache, patchFileName, patchFile, patcherInfo) { 4 | let filesToPatch, customProgress, patcher, settings, 5 | helpers, locals = {}; 6 | 7 | // helper functions 8 | let progressMessage = (title) => progressService.progressMessage(title); 9 | let logMessage = (msg) => progressService.logMessage(msg); 10 | let addProgress = (num) => progressService.addProgress(num); 11 | 12 | let patcherProgress = function(message) { 13 | if (!customProgress) addProgress(1); 14 | progressMessage(message); 15 | }; 16 | 17 | let getFile = function(filename) { 18 | if (!cache[filename]) 19 | cache[filename] = { handle: xelib.FileByName(filename) }; 20 | return cache[filename]; 21 | }; 22 | 23 | let filterDeletedRecords = function(records) { 24 | if (settings.processDeletedRecords) return records; 25 | return records.filter(function(record) { 26 | return !xelib.GetRecordFlag(record, 'Deleted'); 27 | }); 28 | }; 29 | 30 | let getPreviousOverrides = function(records) { 31 | return records.map(function(record) { 32 | return xelib.GetPreviousOverride(record, patchFile); 33 | }); 34 | }; 35 | 36 | let getRecords = function(filename, search, overrides) { 37 | let file = getFile(filename), 38 | cacheKey = `${search}_${+overrides}`; 39 | if (!file[cacheKey]) 40 | file[cacheKey] = filterDeletedRecords(getPreviousOverrides( 41 | xelib.GetRecords(file.handle, search, overrides) 42 | )); 43 | return file[cacheKey]; 44 | }; 45 | 46 | let getRecordsContext = function({signature, overrides}, filename) { 47 | let recordType = xelib.NameFromSignature(signature); 48 | if (overrides) recordType = `${recordType} override`; 49 | return `${recordType} records from ${filename}`; 50 | }; 51 | 52 | let loadRecords = function(filename, {signature, overrides}, recordsContext) { 53 | patcherProgress(`Loading ${recordsContext}.`); 54 | return getRecords(filename, signature, overrides); 55 | }; 56 | 57 | let filterRecords = function(records, filterFn, recordsContext) { 58 | patcherProgress(`Filtering ${records.length} ${recordsContext}`); 59 | return filterFn ? records.filter(filterFn) : records; 60 | }; 61 | 62 | let getLoadOpts = function(load, plugin) { 63 | return load.constructor === Function ? 64 | load(plugin, helpers, settings, locals) : load; 65 | }; 66 | 67 | let getRecordsToPatch = function(load, filename) { 68 | let loadOpts = getLoadOpts(load, getFile(filename)); 69 | if (!loadOpts || !loadOpts.signature) { 70 | if (!customProgress) addProgress(2); 71 | return []; 72 | } 73 | let recordsContext = getRecordsContext(loadOpts, filename), 74 | records = loadRecords(filename, loadOpts, recordsContext); 75 | return filterRecords(records, loadOpts.filter, recordsContext); 76 | }; 77 | 78 | let patchRecords = function(load, patch, filename, recordsToPatch) { 79 | let loadOpts = getLoadOpts(load, getFile(filename)), 80 | recordsContext = getRecordsContext(loadOpts, filename); 81 | patcherProgress(`Patching ${recordsToPatch.length} ${recordsContext}`); 82 | recordsToPatch.forEach(function(record) { 83 | let patchRecord = xelib.CopyElement(record, patchFile, false); 84 | patch(patchRecord, helpers, settings, locals); 85 | }); 86 | }; 87 | 88 | let getPatcherHelpers = function() { 89 | return Object.assign({ 90 | loadRecords: function(search, includeOverrides = false) { 91 | return filesToPatch.reduce(function(records, fn) { 92 | let a = getRecords(fn, search, includeOverrides); 93 | return records.concat(a); 94 | }, []); 95 | }, 96 | copyToPatch: function(rec, asNew = false) { 97 | return xelib.CopyElement(rec, patchFile, asNew); 98 | }, 99 | allSettings: patcherService.settings, 100 | logMessage: logMessage, 101 | cacheRecord: idCacheService.cacheRecord(patchFile) 102 | }, interApiService.getApi('UPF')); 103 | }; 104 | 105 | let loadAndPatch = function(load, patch) { 106 | filesToPatch.forEach(filename => { 107 | let recordsToPatch = getRecordsToPatch(load, filename); 108 | if (patch) patchRecords(load, patch, filename, recordsToPatch); 109 | }); 110 | }; 111 | 112 | let recordsAndPatch = function(records, patch, label = 'records') { 113 | patcherProgress(`Getting ${label}`); 114 | let r = records(filesToPatch, helpers, settings, locals); 115 | if (!patch) return; 116 | patcherProgress(`Patching ${r ? r.length : 0} ${label}`); 117 | r && r.forEach(record => { 118 | let patchRecord = xelib.CopyElement(record, patchFile, false); 119 | patch(patchRecord, helpers, settings, locals); 120 | }); 121 | }; 122 | 123 | let executeBlock = function({init, skip, load, records, label, patch}) { 124 | if (skip && skip()) return; 125 | if (init) init(patchFile, helpers, settings, locals); 126 | if (records) return recordsAndPatch(records, patch, label); 127 | if (load) loadAndPatch(load, patch); 128 | }; 129 | 130 | let initialize = function(exec) { 131 | patcherProgress('Initializing...'); 132 | if (!exec.initialize) return; 133 | exec.initialize(patchFile, helpers, settings, locals); 134 | }; 135 | 136 | let process = function(exec) { 137 | if (!exec.process) return; 138 | exec.process.forEach(executeBlock); 139 | }; 140 | 141 | let finalize = function(exec) { 142 | patcherProgress('Finalizing...'); 143 | if (!exec.finalize) return; 144 | exec.finalize(patchFile, helpers, settings, locals); 145 | }; 146 | 147 | let getExecutor = function() { 148 | return patcher.execute.constructor === Function ? 149 | patcher.execute(patchFile, helpers, settings, locals) : 150 | patcher.execute; 151 | }; 152 | 153 | let patcherId = patcherInfo.id; 154 | filesToPatch = patcherInfo.filesToPatch; 155 | patcher = patcherService.getPatcher(patcherId); 156 | helpers = getPatcherHelpers(); 157 | settings = patcherService.settings[patcherId]; 158 | executor = getExecutor(); 159 | customProgress = executor.customProgress; 160 | if (customProgress) 161 | Object.assign(helpers, { addProgress, progressMessage }); 162 | 163 | initialize(executor); 164 | process(executor); 165 | finalize(executor); 166 | }; 167 | }); 168 | -------------------------------------------------------------------------------- /dist/src/Views/buildPatches.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ngapp}) => 2 | ngapp.controller('buildPatchesController', function($scope, $q, patcherService, patchBuilder) { 3 | // helper functions 4 | let getUsedFileNames = function() { 5 | let patchFileNames = $scope.patchPlugins.map(function(patchPlugin) { 6 | return patchPlugin.filename; 7 | }); 8 | return xelib.GetLoadedFileNames(false).concat(patchFileNames); 9 | }; 10 | 11 | let getNewPatchFilename = function() { 12 | let usedFileNames = getUsedFileNames(), 13 | patchFileName = 'Patch.esp', 14 | counter = 1; 15 | while (usedFileNames.includes(patchFileName)) { 16 | patchFileName = `Patch ${++counter}.esp`; 17 | } 18 | return patchFileName; 19 | }; 20 | 21 | let getDisabledHint = function(patchPlugin) { 22 | if (patchPlugin.filename.length === 0) 23 | return 'Patch plugin filename cannot be empty.'; 24 | let enabledPatchers = patchPlugin.patchers.filter(p => p.active); 25 | if (enabledPatchers.length === 0) 26 | return 'No patchers to build.'; 27 | }; 28 | 29 | // scope functions 30 | $scope.patcherToggled = function(patcher) { 31 | $scope.settings[patcher.id].enabled = patcher.active; 32 | $scope.updatePatchStatuses(); 33 | }; 34 | 35 | $scope.patchFileNameChanged = function(patchPlugin) { 36 | patchPlugin.patchers.forEach(function(patcher) { 37 | $scope.settings[patcher.id].patchFileName = patchPlugin.filename; 38 | }); 39 | }; 40 | 41 | $scope.addPatchPlugin = function() { 42 | $scope.patchPlugins.push({ 43 | filename: getNewPatchFilename(), 44 | patchers: [] 45 | }); 46 | }; 47 | 48 | $scope.removePatchPlugin = (index) => $scope.patchPlugins.splice(index, 1); 49 | 50 | $scope.buildPatchPlugin = function(patchPlugin) { 51 | patcherService.saveSettings(); 52 | patchBuilder.buildPatchPlugins([patchPlugin]); 53 | }; 54 | 55 | $scope.buildAllPatchPlugins = function() { 56 | patcherService.saveSettings(); 57 | patchBuilder.buildPatchPlugins($scope.patchPlugins); 58 | }; 59 | 60 | $scope.updatePatchStatuses = function() { 61 | $scope.patchPlugins.forEach(patchPlugin => { 62 | patchPlugin.disabledHint = getDisabledHint(patchPlugin); 63 | patchPlugin.disabled = !!patchPlugin.disabledHint; 64 | }); 65 | }; 66 | 67 | // event handlers 68 | $scope.$on('buildAllPatches', $scope.buildAllPatchPlugins); 69 | $scope.$on('addPatchPlugin', $scope.addPatchPlugin); 70 | $scope.$on('itemsReordered', $scope.updatePatchStatuses, true); 71 | 72 | // initialization 73 | patcherService.updateFilesToPatch(); 74 | $scope.patchPlugins = patcherService.getPatchPlugins(); 75 | $scope.updatePatchStatuses(); 76 | }); 77 | -------------------------------------------------------------------------------- /dist/src/Views/managePatchersModal.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ngapp}) => 2 | ngapp.controller('managePatchersModalController', function($scope, patcherService) { 3 | // helper functions 4 | let selectTab = function(tab) { 5 | $scope.tabs.forEach((tab) => tab.selected = false); 6 | $scope.currentTab = tab; 7 | $scope.currentTab.selected = true; 8 | $scope.onBuildPatchesTab = $scope.currentTab.label === 'Build Patches'; 9 | }; 10 | 11 | // initialize scope variables 12 | $scope.settings = patcherService.settings; 13 | $scope.tabs = patcherService.getTabs(); 14 | $scope.noPatchers = $scope.tabs.length === 1; 15 | selectTab($scope.tabs[0]); 16 | 17 | // scope functions 18 | $scope.buildAllPatches = () => $scope.$broadcast('buildAllPatches'); 19 | $scope.addPatchPlugin = () => $scope.$broadcast('addPatchPlugin'); 20 | 21 | $scope.closeModal = function() { 22 | patcherService.saveSettings(); 23 | $scope.$emit('closeModal'); 24 | }; 25 | 26 | $scope.onTabClick = function(e, tab) { 27 | e.stopPropagation(); 28 | if (tab === $scope.currentTab) return; 29 | selectTab(tab); 30 | }; 31 | }); 32 | -------------------------------------------------------------------------------- /dist/src/Views/upfSettings.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ngapp}) => 2 | ngapp.controller('upfSettingsController', function($timeout, $scope) { 3 | $scope.bannerStyle = { 4 | 'background': `url('${moduleUrl}/images/banner.jpg')`, 5 | 'background-size': 'cover' 6 | }; 7 | 8 | $scope.managePatchers = function() { 9 | $scope.saveSettings(false); 10 | $timeout(() => openManagePatchersModal($scope)); 11 | }; 12 | 13 | $scope.reloadPatchers = () => $scope.$emit('reloadPatchers'); 14 | }); 15 | -------------------------------------------------------------------------------- /dist/src/helpers.js: -------------------------------------------------------------------------------- 1 | module.exports = function({moduleUrl}) { 2 | let openManagePatchersModal = function(scope) { 3 | scope.$emit('openModal', 'managePatchers', { 4 | basePath: `${moduleUrl}/partials` 5 | }); 6 | }; 7 | 8 | return { openManagePatchersModal }; 9 | }; 10 | -------------------------------------------------------------------------------- /docs/api.html: -------------------------------------------------------------------------------- 1 |

Patcher Modules

2 |

UPF patchers are Patcher Modules, and must specify the UPF module loader in their module.json file. All patcher modules should register a patcher using the registerPatcher function.

3 | 4 |

Patcher Schema

5 |

Patchers can be implemented using JavaScript classes or objects. You can choose which to use at your own discretion. Regardless of your choice, your patcher must conform to the following schema:

6 | 7 | 8 | 9 |

Patcher Helpers

10 |

Helpers which you can use at any point in your patcher's execution.

11 | 12 | 13 | 14 |

Special Patcher Settings

15 |

There are several special patcher settings that are used by UPF internally.

16 | 17 | 18 | 19 |

Patcher Locals

20 |

The locals object is passed to all patcher functions. The locals object allows you to persist variables across all steps of your patcher's execution. It's recommended to use the locals object over using variables defined directly in your module because the locals object does not persist between patcher executions.

-------------------------------------------------------------------------------- /docs/buildPatches.html: -------------------------------------------------------------------------------- 1 |

The build patches tab allows you to:

2 | 3 | 11 | 12 |

Patch plugins

13 |

With UPF, the results of multiple patchers can be generated in a single plugin. In this documentation, we use the term "patch plugin" to refer to a plugin which is generated by one or more patchers.

14 | 15 |

Creating patch plugins

16 |

You can click the Add Plugin button to create a new patch plugin.

17 | 18 |

Renaming patch plugins

19 |

You can edit a patch plugin's filename by clicking on it. NOTE: most tools require plugin filenames to end in a known extension such as .esp or .esl.

20 | 21 |

Building patching plugins

22 |

You can click the Build button to build a patch plugin. NOTE: If the patch plugin is loaded it will be nuked - all records, groups, and masters will be removed from it. If the plugin is not loaded but exists in your game data folder it will be deleted.

23 | 24 |

You can build all patch plugins by clicking the Build All button.

25 | 26 |

Deleting patch plugins

27 |

Patch plugins which have no patchers assigned to them will be automatically removed from the manage patchers modal when you close it.

28 | 29 |

Patchers

30 | 31 |

Toggling individual patchers

32 |

You can disable/enable individual patchers by toggling their corresponding checkbox. Disabled patchers will not be run when the patch plugin they are assigned to is built.

33 | 34 |

Disabled patchers

35 |

Patchers will be disabled if a required file is not loaded or is unavailable to the patch plugin it is assigned to due to load order. Disabled patchers will appear red, and will display a tooltip explaining why they were disabled when you hover over them.

36 | 37 |

Assigning patchers to patch plugins

38 |

You can move a patcher to a different patch plugin using drag and drop.

-------------------------------------------------------------------------------- /docs/managePatchersModal.html: -------------------------------------------------------------------------------- 1 |

The manage patchers modal provides an interface to run patchers and edit patcher settings.

2 | 3 |

You can open the manage patchers modal once you've started a zEdit session by:

4 | 5 |
    6 |
  1. Clicking the icon in the title bar.
  2. 7 |
  3. Right-clicking in the tree view to open the context menu and clicking the Manage Patchers option.
  4. 8 |
  5. Clicking on the Manage Patchers button on the UPF settings tab.
  6. 9 |
  7. Pressing Ctrl + P.
  8. 10 |
11 | 12 |

Navigation

13 |

You can read further documentation on the tabs available in the manager patchers modal at the pages below:

14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/overview.html: -------------------------------------------------------------------------------- 1 |

The Unified Patching Framework (UPF) is a core module provided with zEdit. It provides an API for patchers - programs which generate patch plugins. It's similar to SkyProc, SUM, and MXPF, though there are some key differences.

2 | 3 |

UPF works with all games zEdit supports. UPF Patchers are only loaded when the user starts the application in Edit mode.

4 | 5 |

Language

6 |

UPF patchers use the same framework used for zEdit Modules. Like zEdit modules, they are coded in ES6 JavaScript and can use AngularJS, HTML, and CSS to create user interfaces.

7 | 8 |

ES6 JavaScript is a modern language which empowers developers to use functional and object oriented programming through classes, anonymous functions, and more.

9 | 10 |

API

11 |

UPF patchers use the xelib API. This is the same API that's used throughout zEdit. The API is very carefully designed, and offers a wide range of both generic and specific functions which make it easy to edit and build plugin files.

12 | 13 |

UPF patchers are built with the UPF Patcher API, which provides a highly structured system for defining patcher behavior. The system allows users to manage all of their patchers and patcher settings from a single modal - the manage patchers modal. Each patcher registers its own tab on the modal from which users can edit settings associated with it.

14 | 15 |

UPF handles the majority of the patching process out of the box, requiring developers to only write the code which is specific to their patcher. Developers can easily blacklist files to never be processed by their patcher, or require files to always be loaded for their patcher to be run.

16 | 17 |

UPF allows users to specify the filename of the plugin a patcher should generate, and allows single plugin files to be generated from multiple patchers. UPF's default behavior is to direct the output of all patchers to a single plugin file: zPatch.esp. Developers can override this behavior to have their patcher generate a unique plugin file by default.

18 | 19 |

UPF allows users to disable individual patchers without uninstalling them, and to customize the plugin files ignored by specific patchers.

20 | 21 |

Read more

22 |

You can read more about UPF on the following pages:

23 | 24 | -------------------------------------------------------------------------------- /docs/patcherHelpers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "loadRecords", 4 | "type": "function", 5 | "args": [{ 6 | "name": "search", 7 | "type": "string", 8 | "description": "Records to search for. See xelib.GetRecords for more information." 9 | }, { 10 | "name": "includeOverrides", 11 | "type": "boolean", 12 | "description": "Pass true to load both master and override records. Default false." 13 | }], 14 | "returns": { 15 | "type": "array of handle" 16 | }, 17 | "description": "Helper function which allows you to load records from the files your patcher is targeting." 18 | }, 19 | { 20 | "name": "copyToPatch", 21 | "type": "function", 22 | "args": [{ 23 | "name": "rec", 24 | "type": "handle", 25 | "description": "Record to copy." 26 | }, { 27 | "name": "asNew", 28 | "type": "boolean", 29 | "description": "Whether or not to copy the record to the patch file as a new record. Default false." 30 | }], 31 | "returns": { 32 | "type": "handle", 33 | "description": "Handle for the record copied to the patch plugin." 34 | }, 35 | "description": "Helper function for copying records to your patch plugin without using a process block. Useful for copying globals and other individual records. It's recommended to prefer process blocks over this function." 36 | }, 37 | { 38 | "name": "allSettings", 39 | "type": "object", 40 | "description": "Contains the settings of all patchers, with each patcher's settings in a property corresponding to their id. Use this if you need to change your patcher's behavior when a user is using another patcher." 41 | }, 42 | { 43 | "name": "logMessage", 44 | "type": "function", 45 | "args": [{ 46 | "name": "message", 47 | "type": "string" 48 | }], 49 | "description": "Call this function to print a message to the progress modal's log." 50 | }, 51 | { 52 | "name": "cacheRecord", 53 | "type": "function", 54 | "args": [{ 55 | "name": "rec", 56 | "type": "handle" 57 | }, { 58 | "name": "id", 59 | "type": "string" 60 | }], 61 | "returns": { 62 | "type": "handle" 63 | }, 64 | "description": "Uses record consistency caching to make certain the input record `rec` stays at the same Form ID when the patch gets regenerated. This function should be used on all records created by UPF patchers, excluding overrides. The `id` should be a unique string value for the record. It is recommended to use a unique prefix for `id` to avoid collisions with other patchers. The record's editor ID will be set to `id` if the record has an Editor ID field." 65 | }, 66 | { 67 | "name": "addProgress", 68 | "type": "function", 69 | "args": [{ 70 | "name": "amount", 71 | "type": "number" 72 | }], 73 | "description": "Only available when `customProgress` is set in your patcher's execute block. Adds `amount` to the progress bar." 74 | } 75 | ] -------------------------------------------------------------------------------- /docs/patcherModules.html: -------------------------------------------------------------------------------- 1 |

Patcher modules are added by UPF. Patcher modules are limited modules which use the UPF patcher loader.

2 | 3 |

Module Loader

4 |

Patcher module are defined by the "moduleLoader": "UPF" line in their module.json file.

5 | 6 |

Variables

7 |

Patcher modules are given access to the following variables:

8 | 9 | -------------------------------------------------------------------------------- /docs/patcherSchema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "info", 4 | "type": "object", 5 | "description": "Your patcher module information. You should use the `info` variable as the value here." 6 | }, 7 | { 8 | "name": "gameModes", 9 | "type": "array of integer", 10 | "description": "Array of the game modes your patcher can be used with. Game modes are defined in xelib's [gameModes enumeration](docs://Development/APIs/xelib/Setup)." 11 | }, 12 | { 13 | "name": "settings", 14 | "type": "object", 15 | "description": "The patcher settings schema is used to register a settings tab for your patcher in the [manage patchers modal](docs://Modal Views/Manage Patchers Modal).", 16 | "items": [ 17 | { 18 | "name": "label", 19 | "type": "string", 20 | "description": "The label used for the tab in the manage patchers modal." 21 | }, 22 | { 23 | "name": "hide", 24 | "type": "boolean", 25 | "icons": ["optional"], 26 | "description": "Set to true to not display a settings tab in the manage patchers modal for your patcher." 27 | }, 28 | { 29 | "name": "templateUrl", 30 | "type": "string", 31 | "description": "URL to the HTML template to use for the settings tab. You'll want to use the `patcherUrl` global in this URL. E.g. `${patcherUrl}/partials/settings.html`." 32 | }, 33 | { 34 | "name": "controller", 35 | "type": "function", 36 | "icons": ["optional"], 37 | "description": "Controller function for use on the settings tab. Can use dependency injection per AngularJS." 38 | }, 39 | { 40 | "name": "defaultSettings", 41 | "type": "object", 42 | "description": "Object containing the default values for your patcher's settings. Settings can be any type. You should provide a default value for every setting your patcher supports." 43 | } 44 | ] 45 | }, 46 | { 47 | "name": "requiredFiles", 48 | "type": "function", 49 | "icons": ["optional"], 50 | "args": [], 51 | "returns": { 52 | "type": "array of string" 53 | }, 54 | "description": "Return an array of filenames which must be loaded for the patcher to run. Your patcher will be disabled if any file from this array is not loaded. Your patcher will also be disabled if a required file is loaded after the patch plugin the patcher has been assigned to." 55 | }, 56 | { 57 | "name": "requiredFiles", 58 | "type": "array of string", 59 | "icons": ["optional"], 60 | "legacy": true, 61 | "description": "Legacy option to provide required files as an array directly." 62 | }, 63 | { 64 | "name": "getFilesToPatch", 65 | "type": "function", 66 | "icons": ["optional"], 67 | "args": [{ 68 | "name": "filenames", 69 | "type": "array of string", 70 | "description": "The filenames of all files available for your patcher to patch." 71 | }], 72 | "returns": { 73 | "type": "array of string" 74 | }, 75 | "description": "Function which allows you to exclude certain files from patching. The array of filenames you return from the function will be the base file selection used by the patcher. You can use [Array.prototype.subtract](docs://Development/APIs/Polyfills) to remove files from the passed filenames array easily." 76 | }, 77 | { 78 | "name": "execute", 79 | "type": "function", 80 | "args": [{ 81 | "name": "patch", 82 | "type": "handle", 83 | "description": "Handle for the patch plugin your patcher is using." 84 | }, { 85 | "name": "helpers", 86 | "type": "object", 87 | "description": "Patcher helpers." 88 | }, { 89 | "name": "settings", 90 | "type": "object", 91 | "description": "Your patcher's settings." 92 | }, { 93 | "name": "locals", 94 | "type": "object", 95 | "description": "Patcher locals." 96 | }], 97 | "returns": { 98 | "type": "object", 99 | "description": "An Executor object." 100 | }, 101 | "description": "This function gets called when your patcher is executed. It should return an Executor object as described by the schema below.", 102 | "itemsLabel": "Executor", 103 | "items": [ 104 | { 105 | "name": "customProgress", 106 | "type": "function", 107 | "icons": ["optional"], 108 | "args": [{ 109 | "name": "filesToPatch", 110 | "type": "array of string", 111 | "description": "The filenames of the files the patcher will be run on." 112 | }], 113 | "returns": { 114 | "type": "number" 115 | }, 116 | "description": "Provide a function here to manage the progress bar manually. The function should return the max progress for the patcher." 117 | }, 118 | { 119 | "name": "initialize", 120 | "type": "function", 121 | "icons": ["optional"], 122 | "args": [{ 123 | "name": "patchFile", 124 | "type": "handle", 125 | "description": "Handle for the patch plugin your patcher is using." 126 | }, { 127 | "name": "helpers", 128 | "type": "object", 129 | "description": "Patcher helpers." 130 | }, { 131 | "name": "settings", 132 | "type": "object", 133 | "description": "Your patcher's settings." 134 | }, { 135 | "name": "locals", 136 | "type": "object", 137 | "description": "Patcher locals." 138 | }], 139 | "description": "Called before processing. Perform anything that needs to be done at the beginning of the patcher's execution in this function. This step is often used to load records which but need to be used in the patching process." 140 | }, 141 | { 142 | "name": "process", 143 | "type": "array of object", 144 | "description": "Array of process blocks which are executed sequentially. See the process block schema below for more information.", 145 | "itemsLabel": "Process Block Schema", 146 | "items": [ 147 | { 148 | "name": "skip", 149 | "type": "function", 150 | "icons": ["optional"], 151 | "args": [], 152 | "returns": { 153 | "type": "boolean" 154 | }, 155 | "description": "Skip function. Called to determine whether or not the process block should be skipped. Return true to skip the process block." 156 | }, 157 | { 158 | "name": "load", 159 | "type": "object", 160 | "description": "Loaded records which pass `filter` will be copied to the patch plugin, and then passed to the `patch` function.", 161 | "itemsLabel": "Load Options Object", 162 | "items": [ 163 | { 164 | "name": "signature", 165 | "type": "string", 166 | "description": "Record signature to load. You can view record signatures by top level group names on the tree view and in record headers." 167 | }, 168 | { 169 | "name": "overrides", 170 | "type": "boolean", 171 | "icons": ["optional"], 172 | "description": "Pass true to include override records. Override records are not included by default." 173 | }, 174 | { 175 | "name": "filter", 176 | "type": "function", 177 | "icons": ["optional"], 178 | "args": [{ 179 | "name": "record", 180 | "type": "handle" 181 | }], 182 | "returns": { 183 | "type": "boolean" 184 | }, 185 | "description": "Filter function. Called for each loaded record. Return false to skip patching a record." 186 | } 187 | ] 188 | }, 189 | { 190 | "name": "load", 191 | "type": "function", 192 | "legacy": true, 193 | "args": [{ 194 | "name": "plugin", 195 | "type": "handle", 196 | "description": "Handle for the plugin to patch." 197 | }, { 198 | "name": "helpers", 199 | "type": "object", 200 | "description": "Patcher helpers." 201 | }, { 202 | "name": "settings", 203 | "type": "object", 204 | "description": "Your patcher's settings." 205 | }, { 206 | "name": "locals", 207 | "type": "object", 208 | "description": "Patcher locals." 209 | }], 210 | "returns": { 211 | "type": "object", 212 | "description": "A Load Options object." 213 | }, 214 | "description": "Legacy support for using a function instead of providing a load options object directly. Return null or undefined to skip loading records from a plugin, else return a load options object." 215 | }, 216 | { 217 | "name": "records", 218 | "type": "function", 219 | "icons": ["optional"], 220 | "args": [{ 221 | "name": "filesToPatch", 222 | "type": "array of handle", 223 | "description": "Array of file handles corresponding to the plugins being patched." 224 | }, { 225 | "name": "helpers", 226 | "type": "object", 227 | "description": "Patcher helpers." 228 | }, { 229 | "name": "settings", 230 | "type": "object", 231 | "description": "Your patcher's settings." 232 | }, { 233 | "name": "locals", 234 | "type": "object", 235 | "description": "Patcher locals." 236 | }], 237 | "returns": { 238 | "type": "array of handle", 239 | "description": "Array of records to patch." 240 | }, 241 | "description": "A function which can be used instead of `load`. The `records` function allows you to return a custom array of records to patch." 242 | }, 243 | { 244 | "name": "patch", 245 | "type": "function", 246 | "icons": ["optional"], 247 | "args": [{ 248 | "name": "record", 249 | "type": "handle", 250 | "description": "Handle for the patch record." 251 | }, { 252 | "name": "helpers", 253 | "type": "object", 254 | "description": "Patcher helpers." 255 | }, { 256 | "name": "settings", 257 | "type": "object", 258 | "description": "Your patcher's settings." 259 | }, { 260 | "name": "locals", 261 | "type": "object", 262 | "description": "Patcher locals." 263 | }], 264 | "description": "Called for each record copied to the patch plugin. This is the step where you set values on the record." 265 | } 266 | ] 267 | }, 268 | { 269 | "name": "finalize", 270 | "type": "function", 271 | "icons": ["optional"], 272 | "args": [{ 273 | "name": "patchFile", 274 | "type": "handle", 275 | "description": "Handle for the patch plugin your patcher is using." 276 | }, { 277 | "name": "helpers", 278 | "type": "object", 279 | "description": "Patcher helpers." 280 | }, { 281 | "name": "settings", 282 | "type": "object", 283 | "description": "Your patcher's settings." 284 | }, { 285 | "name": "locals", 286 | "type": "object", 287 | "description": "Patcher locals." 288 | }], 289 | "description": "Called after processing. Can be used to perform any cleanup/final steps once your patcher has finished executing. Note that UPF automatically removes ITPO records and unused masters, so you don't need to do that here." 290 | } 291 | ] 292 | }, 293 | { 294 | "name": "execute", 295 | "type": "object", 296 | "legacy": true, 297 | "description": "Legacy support for providing an Executor object directly instead of returning it from a function. Using this format means you must access `patchFile`, `helpers`, `settings`, and `locals` from arguments passed to the `initialize`, `finalize`, `load`, and `patch` function calls. This syntax is not recommended." 298 | } 299 | ] 300 | -------------------------------------------------------------------------------- /docs/patcherSettings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "ignoredFiles", 4 | "type": "array of string", 5 | "description": "Array of filenames to ignore when patching. The `ignore-plugins` directive sets this value." 6 | }, 7 | { 8 | "name": "processDeletedRecords", 9 | "type": "boolean", 10 | "description": "If set to true deleted records will not be automatically excluded when loading records in process blocks or when using the `helpers.loadRecords` function." 11 | } 12 | ] -------------------------------------------------------------------------------- /docs/patcherVariables.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "registerPatcher", 4 | "type": "function", 5 | "args": [{ 6 | "name": "patcher", 7 | "type": "object", 8 | "description": "Patcher to register." 9 | }], 10 | "description": "Function to register your patcher. Call this function with a patcher object or class as an argument. See the patcher schema for more details." 11 | }, 12 | { 13 | "name": "fh", 14 | "type": "object", 15 | "description": "The [fileHelpers API](docs://Development/APIs/File Helpers)." 16 | }, 17 | { 18 | "name": "info", 19 | "type": "object", 20 | "description": "Object containing information about your patcher module. Basically just your module.json." 21 | }, 22 | { 23 | "name": "patcherPath", 24 | "type": "string", 25 | "description": "Absolute path for the folder where your patcher module is installed on the user's machine. Should be prepended to paths when loading/saving files." 26 | }, 27 | { 28 | "name": "patcherUrl", 29 | "type": "string", 30 | "description": "`file://` URL for the folder where your patcher module is installed on the user's machine. Should be prepended to any HTML template/resource URLs." 31 | }, 32 | { 33 | "name": "xelib", 34 | "type": "object", 35 | "description": "The [xelib API](docs://Development/APIs/xelib)." 36 | } 37 | ] -------------------------------------------------------------------------------- /docs/topics.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "path": "Modules/Core Modules", 3 | "topic": { 4 | "label": "Unified Patching Framework", 5 | "templateUrl": "/docs/overview.html" 6 | } 7 | }, { 8 | "path": "Modules", 9 | "topic": { 10 | "label": "Patcher Modules", 11 | "templateUrl": "/docs/patcherModules.html" 12 | } 13 | }, { 14 | "path": "Modal Views", 15 | "topic": { 16 | "label": "Manage Patchers Modal", 17 | "templateUrl": "/docs/managePatchersModal.html" 18 | } 19 | }, { 20 | "path": "Modal Views/Manage Patchers Modal", 21 | "topic": { 22 | "label": "Build Patches Tab", 23 | "templateUrl": "/docs/buildPatches.html" 24 | } 25 | }, { 26 | "path": "Modal Views/Settings Modal", 27 | "topic": { 28 | "label": "UPF Settings Tab", 29 | "templateUrl": "/docs/upfSettings.html" 30 | } 31 | }, { 32 | "path": "Development/APIs", 33 | "topic": { 34 | "label": "UPF Patcher API", 35 | "templateUrl": "/docs/api.html" 36 | } 37 | }] -------------------------------------------------------------------------------- /docs/upfSettings.html: -------------------------------------------------------------------------------- 1 |

The UPF settings tab allows you to open the Manage Patchers Modal or reload patchers from disk.

2 | 3 |

You can reload UPF patchers by pressing Alt + F5 when a modal view is not active.

4 | 5 |

Note: reloading patchers does not update patcher settings changes, including GUI changes to your patcher's settings tab in the Manager Patchers Modal.

-------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'), 2 | gulp = require('gulp'), 3 | clean = require('gulp-clean'), 4 | rename = require('gulp-rename'), 5 | zip = require('gulp-zip'); 6 | 7 | let files = ['index.js', 'module.json', 'LICENSE', 'README.md']; 8 | let folders = ['src/**/*.js', 'partials/*.html', 'images/*', 'docs/*']; 9 | 10 | let loadModuleInfo = function() { 11 | let text = fs.readFileSync('module.json', 'utf8'); 12 | return JSON.parse(text); 13 | }; 14 | 15 | gulp.task('clean', function() { 16 | return gulp.src('dist', { read: false }).pipe(clean()); 17 | }); 18 | 19 | gulp.task('build', gulp.series('clean', function(done) { 20 | files.forEach(filename => { 21 | gulp.src(filename).pipe(gulp.dest('dist')); 22 | }); 23 | 24 | folders.forEach(path => { 25 | let name = path.split('/').shift(); 26 | gulp.src(path).pipe(gulp.dest(`dist/${name}`)); 27 | }); 28 | 29 | done(); 30 | })); 31 | 32 | gulp.task('release', function() { 33 | let {id, version} = loadModuleInfo(), 34 | zipFileName = `${id}-v${version}.zip`; 35 | 36 | console.log(`Packaging ${zipFileName}`); 37 | 38 | return gulp.src('dist/**/*', { base: 'dist/'}) 39 | .pipe(rename((path) => path.dirname = `${id}/${path.dirname}`)) 40 | .pipe(zip(zipFileName)) 41 | .pipe(gulp.dest('.')); 42 | }); 43 | 44 | gulp.task('watch', function() { 45 | files.forEach(filename => gulp.watch(filename, ['build'])); 46 | folders.forEach(folder => gulp.watch(folder, ['build'])); 47 | }); 48 | 49 | gulp.task('default', gulp.series('build', 'watch')); 50 | -------------------------------------------------------------------------------- /images/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-edit/zedit-unified-patching-framework/b0fbe649e1cbcee7606d1df5e9ae1d41eafd82ba/images/banner.jpg -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = function(args) { 2 | let {fh, modulePath} = args, 3 | helpers = require('./src/helpers')(args), 4 | srcPath = fh.path(modulePath, 'src'); 5 | 6 | fh.getFiles(srcPath, { 7 | matching: '**/*.js' 8 | }).forEach(filePath => { 9 | let filename = fh.getFileName(filePath); 10 | if (filename === 'helpers.js') return; 11 | require(filePath)(args, helpers); 12 | }); 13 | }; 14 | 15 | -------------------------------------------------------------------------------- /module.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "unifiedPatchingFramework", 3 | "name": "Unified Patching Framework", 4 | "author": "Mator", 5 | "version": "2.0.0", 6 | "repo": "https://github.com/matortheeternal/zedit-unified-patching-framework", 7 | "released": "9/7/2017", 8 | "updated": "6/24/2019", 9 | "description": "A framework for dynamic patchers, similar to SkyProc/SUM." 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zedit-unified-patching-framework", 3 | "productName": "zEdit Unified Patching Framework", 4 | "description": "A zEdit module which provides a framework for dynamic patch generation, similar to SkyProc/SUM.", 5 | "version": "2.0.0", 6 | "author": "Mator", 7 | "copyright": "© 2019, Mator", 8 | "repository": "https://github.com/matortheeternal/zedit-unified-patching-framework", 9 | "license": "MIT", 10 | "scripts": { 11 | "build": "gulp build", 12 | "release": "gulp release" 13 | }, 14 | "dependencies": {}, 15 | "devDependencies": { 16 | "angular": "^1.6.6", 17 | "gulp": "^4.0.0", 18 | "gulp-clean": "^0.3.2", 19 | "gulp-include": "^2.3.1", 20 | "gulp-rename": "^1.2.2", 21 | "gulp-zip": "^4.1.0", 22 | "nan": "^2.10.0", 23 | "xelib": "github:matortheeternal/xelib" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /partials/buildPatches.html: -------------------------------------------------------------------------------- 1 | 43 | 44 |

Build Patches

45 | 46 |
47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | {{::$parent.item.name}} 55 | 56 | 57 | (Patching {{$parent.item.filesToPatch.length}} files) 58 | 59 | 60 |
61 |
62 | 63 |
64 | No patchers found. 65 |
-------------------------------------------------------------------------------- /partials/ignorePlugins.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ignoring {{ignoredPlugins.length}} Plugins 4 | 5 | 6 |
7 |
8 | 9 | 10 |
11 |
-------------------------------------------------------------------------------- /partials/managePatchersModal.html: -------------------------------------------------------------------------------- 1 | 21 | 22 | 42 | -------------------------------------------------------------------------------- /partials/settings.html: -------------------------------------------------------------------------------- 1 | 9 | 10 |

Unified Patching Framework

11 | 12 |
13 | 14 | 15 |
-------------------------------------------------------------------------------- /src/Directives/ignorePlugins.js: -------------------------------------------------------------------------------- 1 | module.exports = function({ngapp, moduleUrl}) { 2 | ngapp.directive('ignorePlugins', () => ({ 3 | restrict: 'E', 4 | scope: { 5 | patcherId: '@' 6 | }, 7 | templateUrl: `${moduleUrl}/partials/ignorePlugins.html`, 8 | controller: 'ignorePluginsController' 9 | })); 10 | 11 | ngapp.controller('ignorePluginsController', function($scope, patcherService) { 12 | // helper functions 13 | let updateIgnoredFiles = function() { 14 | let settings = patcherService.settings[$scope.patcherId]; 15 | settings.ignoredFiles = $scope.ignoredPlugins 16 | .filter((item) => !item.invalid) 17 | .map((item) => item.filename); 18 | }; 19 | 20 | let getValid = function(item, itemIndex) { 21 | let filename = item.filename, 22 | isRequired = $scope.requiredPlugins.includes(filename), 23 | duplicate = $scope.ignoredPlugins.find((item, index) => { 24 | return item.filename === filename && index < itemIndex; 25 | }); 26 | return !isRequired && !duplicate; 27 | }; 28 | 29 | // scope functions 30 | $scope.toggleExpanded = function() { 31 | if ($scope.ignoredPlugins.length === 0) return; 32 | $scope.expanded = !$scope.expanded; 33 | }; 34 | 35 | $scope.addIgnoredPlugin = function() { 36 | if (!$scope.expanded) $scope.expanded = true; 37 | $scope.ignoredPlugins.push({ filename: 'Plugin.esp' }); 38 | $scope.onChange(); 39 | }; 40 | 41 | $scope.removeIgnoredPlugin = function(index) { 42 | $scope.ignoredPlugins.splice(index, 1); 43 | $scope.onChange(); 44 | }; 45 | 46 | $scope.onChange = function() { 47 | $scope.ignoredPlugins.forEach((item, index) => { 48 | item.invalid = !getValid(item, index); 49 | }); 50 | updateIgnoredFiles(); 51 | }; 52 | 53 | // initialization 54 | if (!$scope.patcherId) 55 | throw 'ignorePlugins Directive: patcher-id is required.'; 56 | 57 | let patcher = patcherService.getPatcher($scope.patcherId), 58 | ignored = patcherService.getIgnoredFiles(patcher); 59 | 60 | $scope.requiredPlugins = patcherService.getRequiredFiles(patcher); 61 | $scope.ignoredPlugins = ignored.map(filename => ({ filename })); 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /src/Runners/managePatchersButton.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ngapp}, {openManagePatchersModal}) => 2 | ngapp.run(function($rootScope, buttonService) { 3 | let managePatchersButton = { 4 | class: 'fa fa-puzzle-piece', 5 | title: 'Manage Patchers', 6 | hidden: true, 7 | onClick: openManagePatchersModal 8 | }; 9 | buttonService.addButton(managePatchersButton); 10 | 11 | // make button visible when edit mode is started 12 | $rootScope.$on('filesLoaded', function() { 13 | if ($rootScope.appMode.id !== 'edit') return; 14 | $rootScope.$applyAsync(() => managePatchersButton.hidden = false); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/Runners/managePatchersHotkey.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ngapp}, {openManagePatchersModal}) => 2 | ngapp.run(function(hotkeyService) { 3 | hotkeyService.addHotkeys('editView', { 4 | p: [{ 5 | modifiers: ['ctrlKey', 'shiftKey'], 6 | callback: openManagePatchersModal 7 | }], 8 | f5: [{ 9 | modifiers: ['altKey'], 10 | callback: scope => { 11 | if (scope.$root.modalActive) return; 12 | scope.$emit('reloadPatchers') 13 | } 14 | }] 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/Runners/managePatchersMenuItem.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ngapp}, {openManagePatchersModal}) => 2 | ngapp.run(function(contextMenuService) { 3 | // add manage patchers context menu item to tree view context menu 4 | let menuItems = contextMenuService.getContextMenu('treeView'); 5 | let automateIndex = menuItems.findIndex(item => { 6 | return item.id === 'Automate'; 7 | }); 8 | 9 | menuItems.splice(automateIndex + 1, 0, { 10 | id: 'Manage Patchers', 11 | visible: () => true, 12 | build: (scope, items) => { 13 | items.push({ 14 | label: 'Manage Patchers', 15 | hotkey: 'Ctrl+Shift+P', 16 | callback: () => openManagePatchersModal(scope) 17 | }); 18 | } 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/Runners/upfLoader.js: -------------------------------------------------------------------------------- 1 | module.exports = function({moduleService, ngapp}) { 2 | moduleService.deferLoader('UPF'); 3 | 4 | ngapp.run(function($rootScope, patcherService) { 5 | let upfLoader = function({module, fh, moduleService}) { 6 | moduleService.executeModule(module, { 7 | registerPatcher: patcherService.registerPatcher, 8 | fh: fh, 9 | info: module.info, 10 | patcherUrl: fh.pathToFileUrl(module.path), 11 | patcherPath: module.path 12 | }); 13 | moduleService.loadDocs(module.path); 14 | }; 15 | 16 | moduleService.registerLoader('UPF', upfLoader); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /src/Runners/upfSettingsTab.js: -------------------------------------------------------------------------------- 1 | module.exports = function({ngapp, moduleUrl}) { 2 | ngapp.run(function(settingsService) { 3 | settingsService.registerSettings({ 4 | appModes: ['edit'], 5 | label: 'Unified Patching Framework', 6 | templateUrl: `${moduleUrl}/partials/settings.html`, 7 | controller: 'upfSettingsController' 8 | }); 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /src/Services/idCacheService.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ngapp}) => 2 | ngapp.service('idCacheService', function(patcherService) { 3 | let prepareIdCache = function(patchFile) { 4 | let cache = patcherService.cache, 5 | fileName = xelib.Name(patchFile); 6 | if (!cache.hasOwnProperty(fileName)) cache[fileName] = {}; 7 | return cache[fileName]; 8 | }; 9 | 10 | let updateNextFormId = function(patchFile, idCache) { 11 | let formIds = Object.values(idCache), 12 | maxFormId = formIds.reduce((a, b) => Math.max(a, b), 0x7FF); 13 | xelib.SetNextObjectID(patchFile, maxFormId + 1); 14 | }; 15 | 16 | this.cacheRecord = function(patchFile) { 17 | let patchOrd = xelib.GetFileLoadOrder(patchFile) * 0x1000000, 18 | idCache = prepareIdCache(patchFile), 19 | usedIds = {}; 20 | 21 | updateNextFormId(patchFile, idCache); 22 | 23 | return function(rec, id) { 24 | if (!xelib.IsMaster(rec)) return; 25 | if (usedIds.hasOwnProperty(id)) 26 | throw new Error(`cacheRecord: ${id} is not unique.`); 27 | if (idCache.hasOwnProperty(id)) { 28 | xelib.SetFormID(rec, patchOrd + idCache[id], false, false); 29 | } else { 30 | idCache[id] = xelib.GetFormID(rec, false, true); 31 | } 32 | if (xelib.HasElement(rec, 'EDID')) xelib.SetValue(rec, 'EDID', id); 33 | usedIds[id] = true; 34 | return rec; 35 | }; 36 | }; 37 | }); 38 | -------------------------------------------------------------------------------- /src/Services/patchBuilder.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ngapp}) => 2 | ngapp.service('patchBuilder', function($rootScope, $timeout, patcherService, patchPluginWorker, errorService, progressService) { 3 | let cache = {}; 4 | 5 | let build = (patchPlugin) => patchPluginWorker.run(cache, patchPlugin); 6 | 7 | let getExecutor = function(patcher) { 8 | let helpers = {}, 9 | settings = patcherService.settings[patcher.id], 10 | locals = {}; 11 | return patcher.execute.constructor === Function 12 | ? patcher.execute(0, helpers, settings, locals) 13 | : patcher.execute; 14 | }; 15 | 16 | let getProcessSize = function(process, files) { 17 | return process.reduce((sum, block) => { 18 | if (block.skip && block.skip()) return sum; 19 | let patch = block.patch ? 1 : 0; 20 | if (block.records) return sum + 1 + patch; 21 | if (block.load) return sum + files.length * (2 + patch); 22 | return sum; 23 | }, 0); 24 | }; 25 | 26 | let getMaxProgress = function(patchPlugin) { 27 | return patchPlugin.patchers.filterOnKey('active') 28 | .map(p => patcherService.getPatcher(p.id)) 29 | .reduce((sum, patcher) => { 30 | let {customProgress, process} = getExecutor(patcher), 31 | files = patcher.filesToPatch; 32 | if (customProgress) return sum + customProgress(files); 33 | return sum + 2 + getProcessSize(process, files); 34 | }, 1); 35 | }; 36 | 37 | let getTotalMaxProgress = function(patchPlugins) { 38 | return patchPlugins.reduce((sum, patchPlugin) => { 39 | return sum + getMaxProgress(patchPlugin); 40 | }, 0); 41 | }; 42 | 43 | let openProgressModal = function(maxProgress) { 44 | $rootScope.$broadcast('closeModal'); 45 | progressService.showProgress({ 46 | determinate: true, 47 | echo: true, 48 | title: 'Running Patchers', 49 | message: 'Initializing...', 50 | current: 0, 51 | max: maxProgress 52 | }); 53 | }; 54 | 55 | let getActivePatchPlugins = function(patchPlugins) { 56 | return patchPlugins.filter(patchPlugin => !patchPlugin.disabled); 57 | }; 58 | 59 | let progressDone = function(patchPlugins, success) { 60 | let pluginsStr = `${patchPlugins.length} patch plugins`; 61 | progressService.progressTitle(success ? 62 | `${pluginsStr} built successfully` : 63 | `${pluginsStr} failed to build`); 64 | progressService.progressMessage(success ? 'All Done!' : 'Error'); 65 | progressService.allowClose(); 66 | }; 67 | 68 | // public functions 69 | this.buildPatchPlugins = function(patchPlugins) { 70 | let activePatchPlugins = getActivePatchPlugins(patchPlugins), 71 | maxProgress = getTotalMaxProgress(activePatchPlugins); 72 | if (activePatchPlugins.length === 0) return; 73 | xelib.CreateHandleGroup(); 74 | openProgressModal(maxProgress); 75 | $timeout(function() { 76 | patcherService.loadCache(); 77 | let success = errorService.try(() => 78 | activePatchPlugins.forEach(build)); 79 | success ? patcherService.saveCache() : patcherService.loadCache(); 80 | progressDone(activePatchPlugins, success); 81 | cache = {}; 82 | xelib.FreeHandleGroup(); 83 | $rootScope.$broadcast('reloadGUI'); 84 | }, 50); 85 | }; 86 | }); 87 | -------------------------------------------------------------------------------- /src/Services/patchPluginWorker.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ngapp, fh}) => 2 | ngapp.service('patchPluginWorker', function(progressService, patcherWorker) { 3 | this.run = function(cache, patchPlugin) { 4 | let start = new Date(); 5 | 6 | let progressTitle = function(title) { 7 | progressService.progressTitle(title); 8 | }; 9 | 10 | let patcherProgress = function(message) { 11 | progressService.addProgress(1); 12 | progressService.progressMessage(message); 13 | }; 14 | 15 | let preparePatchFile = function(filename) { 16 | if (!xelib.HasElement(0, filename)) { 17 | let dataPath = xelib.GetGlobal('DataPath'); 18 | fh.jetpack.cwd(dataPath).remove(filename); 19 | } 20 | let patchFile = xelib.AddElement(0, filename); 21 | xelib.NukeFile(patchFile); 22 | xelib.AddAllMasters(patchFile); 23 | return patchFile; 24 | }; 25 | 26 | let cleanPatchFile = function(patchFile) { 27 | patcherProgress('Removing ITPOs and cleaning masters.'); 28 | try { 29 | xelib.RemoveIdenticalRecords(patchFile, false, true); 30 | } catch (x) { 31 | progressService.logMessage('Removing ITPOs failed: ' + x.message); 32 | } 33 | xelib.CleanMasters(patchFile); 34 | }; 35 | 36 | // MAIN WORKER EXECUTION 37 | let patchFileName = patchPlugin.filename, 38 | patchFile = preparePatchFile(patchFileName); 39 | patchPlugin.patchers.forEach(function(patcher) { 40 | if (!patcher.active) return; 41 | progressTitle(`Building ${patchFileName} ~ Running ${patcher.name}`); 42 | patcherWorker.run(cache, patchFileName, patchFile, patcher); 43 | }); 44 | cleanPatchFile(patchFile); 45 | console.log(`Generated ${patchFileName} in ${new Date() - start}ms`); 46 | }; 47 | }); 48 | -------------------------------------------------------------------------------- /src/Services/patcherService.js: -------------------------------------------------------------------------------- 1 | module.exports = function({ngapp, moduleUrl, fh}) { 2 | ngapp.service('patcherService', function($rootScope, $cacheFactory, settingsService) { 3 | const disabledHintBase = 4 | 'This patcher is disabled because the following required' + 5 | '\r\nfiles are not available to the patch plugin:'; 6 | 7 | let service = this, 8 | patchers = [], 9 | tabs = [{ 10 | label: 'Build Patches', 11 | templateUrl: `${moduleUrl}/partials/buildPatches.html`, 12 | controller: 'buildPatchesController' 13 | }]; 14 | 15 | // private functions 16 | let getAvailableFiles = function(patcher) { 17 | let patchFileName = service.settings[patcher.info.id].patchFileName; 18 | return xelib.GetLoadedFileNames().itemsBefore(patchFileName); 19 | }; 20 | 21 | let getPatcherEnabled = function(patcher) { 22 | return service.settings[patcher.info.id].enabled; 23 | }; 24 | 25 | let getMissingRequirements = function(patcher) { 26 | return service.getRequiredFiles(patcher) 27 | .subtract(patcher.availableFiles); 28 | }; 29 | 30 | let getPatcherDisabled = function(patcher) { 31 | return getMissingRequirements(patcher).length > 0; 32 | }; 33 | 34 | let getDisabledHint = function(patcher) { 35 | return getMissingRequirements(patcher).reduce((hint, filename) => { 36 | return `${hint}\r\n - ${filename}`; 37 | }, disabledHintBase); 38 | }; 39 | 40 | let getDefaultSettings = function(patcher) { 41 | let defaultSettings = patcher.settings.defaultSettings || {}; 42 | return Object.assign({ 43 | patchFileName: 'zPatch.esp', 44 | ignoredFiles: [], 45 | enabled: true 46 | }, defaultSettings); 47 | }; 48 | 49 | let buildSettings = function(settings) { 50 | let defaults = { cache: {} }; 51 | patchers.forEach(function(patcher) { 52 | let patcherSettings = {}; 53 | patcherSettings[patcher.info.id] = getDefaultSettings(patcher); 54 | Object.deepAssign(defaults, patcherSettings); 55 | }); 56 | return Object.deepAssign(defaults, settings); 57 | }; 58 | 59 | let buildTabs = function() { 60 | patchers.forEach(function(patcher) { 61 | if (!patcher.settings.hide) tabs.push(patcher.settings); 62 | }); 63 | }; 64 | 65 | let getFilesToPatchHint = function(patcher) { 66 | let filesToPatch = patcher.filesToPatch, 67 | hint = filesToPatch.slice(0, 40).join(', '); 68 | if (filesToPatch.length > 40) hint += '...'; 69 | return hint.wordwrap(); 70 | }; 71 | 72 | let createPatchPlugin = function(patchPlugins, patchFileName) { 73 | let patchPlugin = { filename: patchFileName, patchers: [] }; 74 | patchPlugins.push(patchPlugin); 75 | return patchPlugin; 76 | }; 77 | 78 | let getPatchPlugin = function(patcher, patchPlugins) { 79 | let patchFileName = service.settings[patcher.info.id].patchFileName; 80 | return patchPlugins.find(patchPlugin => { 81 | return patchPlugin.filename === patchFileName; 82 | }) || createPatchPlugin(patchPlugins, patchFileName); 83 | }; 84 | 85 | // public functions 86 | this.getPatcher = function(id) { 87 | return patchers.find(patcher => patcher.info.id === id); 88 | }; 89 | 90 | this.registerPatcher = function(patcher) { 91 | if (service.getPatcher(patcher.info.id)) return; 92 | patchers.push(patcher); 93 | }; 94 | 95 | this.reloadPatchers = function() { 96 | let patcherIds = patchers.map(patcher => patcher.info.id); 97 | patchers = []; 98 | patcherIds.forEach(id => { 99 | let patcherPath = fh.jetpack.path(`modules\\${id}`); 100 | moduleService.loadModule(patcherPath); 101 | }); 102 | }; 103 | 104 | this.updateForGameMode = function(gameMode) { 105 | patchers = patchers.filter(patcher => { 106 | return patcher.gameModes.includes(gameMode); 107 | }); 108 | }; 109 | 110 | this.loadSettings = function() { 111 | let profileName = settingsService.currentProfile; 112 | service.settingsPath = `profiles/${profileName}/patcherSettings.json`; 113 | let settings = fh.loadJsonFile(service.settingsPath) || {}; 114 | service.settings = buildSettings(settings); 115 | service.saveSettings(); 116 | buildTabs(); 117 | }; 118 | 119 | this.loadCache = function() { 120 | let profileName = settingsService.currentProfile; 121 | service.cachePath = `profiles/${profileName}/patcherCache.json`; 122 | service.cache = fh.loadJsonFile(service.cachePath) || {}; 123 | }; 124 | 125 | this.saveSettings = function() { 126 | fh.saveJsonFile(service.settingsPath, service.settings); 127 | }; 128 | 129 | this.saveCache = function() { 130 | fh.saveJsonFile(service.cachePath, service.cache); 131 | }; 132 | 133 | this.getTabs = function() { 134 | return tabs.map(tab => ({ 135 | label: tab.label, 136 | templateUrl: tab.templateUrl, 137 | controller: tab.controller 138 | })); 139 | }; 140 | 141 | this.getRequiredFiles = function(patcher) { 142 | if (!patcher.requiredFiles) return []; 143 | if (patcher.requiredFiles.constructor === Function) 144 | return patcher.requiredFiles() || []; 145 | return patcher.requiredFiles; 146 | }; 147 | 148 | this.getIgnoredFiles = function(patcher) { 149 | return service.settings[patcher.info.id].ignoredFiles; 150 | }; 151 | 152 | this.getFilesToPatch = function(patcher) { 153 | let filesToPatch = patcher.availableFiles.slice(); 154 | if (patcher.getFilesToPatch) 155 | filesToPatch = patcher.getFilesToPatch(filesToPatch); 156 | return filesToPatch.subtract(service.getIgnoredFiles(patcher)); 157 | }; 158 | 159 | this.updateFilesToPatch = function() { 160 | patchers.forEach(patcher => { 161 | patcher.availableFiles = getAvailableFiles(patcher); 162 | patcher.filesToPatch = service.getFilesToPatch(patcher); 163 | }); 164 | }; 165 | 166 | this.getPatchPlugins = function() { 167 | let patchPlugins = []; 168 | patchers.forEach(patcher => { 169 | let patchPlugin = getPatchPlugin(patcher, patchPlugins), 170 | disabled = getPatcherDisabled(patcher); 171 | patchPlugin.patchers.push({ 172 | id: patcher.info.id, 173 | name: patcher.info.name, 174 | active: !disabled && getPatcherEnabled(patcher), 175 | disabled: disabled, 176 | disabledHint: disabled ? getDisabledHint(patcher) : '', 177 | filesToPatch: patcher.filesToPatch, 178 | filesToPatchHint: getFilesToPatchHint(patcher) 179 | }); 180 | }); 181 | return patchPlugins; 182 | }; 183 | 184 | $rootScope.$on('reloadPatchers', () => { 185 | tabs.forEach(tab => { 186 | if (!tab.templateUrl) return; 187 | $cacheFactory.get('templates').remove(tab.templateUrl); 188 | }); 189 | tabs = []; 190 | service.reloadPatchers(); 191 | service.loadSettings(); 192 | }); 193 | }); 194 | 195 | // register for events 196 | ngapp.run(function($rootScope, patcherService) { 197 | $rootScope.$on('filesLoaded', () => { 198 | if ($rootScope.appMode.id !== 'edit') return; 199 | patcherService.loadSettings(); 200 | }); 201 | 202 | $rootScope.$on('sessionStarted', (e, selectedProfile) => { 203 | patcherService.updateForGameMode(selectedProfile.gameMode); 204 | }); 205 | }); 206 | }; 207 | -------------------------------------------------------------------------------- /src/Services/patcherWorker.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ngapp}) => 2 | ngapp.service('patcherWorker', function(patcherService, progressService, idCacheService, interApiService) { 3 | let {FileByName, GetRecordFlag, GetPreviousOverride, 4 | GetRecords, NameFromSignature, CopyElement} = xelib; 5 | 6 | this.run = function(cache, patchFileName, patchFile, patcherInfo) { 7 | let filesToPatch, customProgress, patcher, settings, 8 | helpers, locals = {}; 9 | 10 | // helper functions 11 | let progressMessage = (title) => progressService.progressMessage(title); 12 | let logMessage = (msg) => progressService.logMessage(msg); 13 | let addProgress = (num) => progressService.addProgress(num); 14 | 15 | let patcherProgress = function(message) { 16 | if (!customProgress) addProgress(1); 17 | progressMessage(message); 18 | }; 19 | 20 | let getFile = function(filename) { 21 | if (!cache[filename]) 22 | cache[filename] = { handle: FileByName(filename) }; 23 | return cache[filename]; 24 | }; 25 | 26 | let filterDeletedRecords = function(records) { 27 | if (settings.processDeletedRecords) return records; 28 | return records.filter(function(record) { 29 | return !GetRecordFlag(record, 'Deleted'); 30 | }); 31 | }; 32 | 33 | let getPreviousOverrides = function(records) { 34 | return records.map(function(record) { 35 | return GetPreviousOverride(record, patchFile); 36 | }); 37 | }; 38 | 39 | let getRecords = function(filename, search, overrides) { 40 | let file = getFile(filename), 41 | cacheKey = `${search}_${+overrides}`; 42 | if (!file[cacheKey]) 43 | file[cacheKey] = GetRecords(file.handle, search, overrides); 44 | return filterDeletedRecords(getPreviousOverrides(file[cacheKey])); 45 | }; 46 | 47 | let getRecordsContext = function({signature, overrides}, filename) { 48 | let recordType = NameFromSignature(signature); 49 | if (overrides) recordType = `${recordType} override`; 50 | return `${recordType} records from ${filename}`; 51 | }; 52 | 53 | let loadRecords = function(filename, loadOpts, recordsContext) { 54 | let {signature, overrides} = loadOpts; 55 | patcherProgress(`Loading ${recordsContext}.`); 56 | return getRecords(filename, signature, overrides); 57 | }; 58 | 59 | let filterRecords = function(records, filterFn, recordsContext) { 60 | patcherProgress(`Filtering ${records.length} ${recordsContext}`); 61 | return filterFn ? records.filter(filterFn) : records; 62 | }; 63 | 64 | let getLoadOpts = function(load, plugin) { 65 | return load.constructor === Function ? 66 | load(plugin, helpers, settings, locals) : load; 67 | }; 68 | 69 | let getRecordsToPatch = function(load, filename) { 70 | let loadOpts = getLoadOpts(load, getFile(filename)); 71 | if (!loadOpts || !loadOpts.signature) { 72 | if (!customProgress) addProgress(2); 73 | return []; 74 | } 75 | let recordsContext = getRecordsContext(loadOpts, filename), 76 | records = loadRecords(filename, loadOpts, recordsContext); 77 | return filterRecords(records, loadOpts.filter, recordsContext); 78 | }; 79 | 80 | let patchRecords = function(load, patch, filename, recordsToPatch) { 81 | let loadOpts = getLoadOpts(load, getFile(filename)), 82 | recordsContext = getRecordsContext(loadOpts, filename); 83 | patcherProgress(`Patching ${recordsToPatch.length} ${recordsContext}`); 84 | recordsToPatch.forEach(function(record) { 85 | let patchRecord = CopyElement(record, patchFile, false); 86 | patch(patchRecord, helpers, settings, locals); 87 | }); 88 | }; 89 | 90 | let getPatcherHelpers = function() { 91 | return Object.assign({ 92 | loadRecords: function(search, includeOverrides = false) { 93 | return filesToPatch.reduce(function(records, fn) { 94 | let a = getRecords(fn, search, includeOverrides); 95 | return records.concat(a); 96 | }, []); 97 | }, 98 | copyToPatch: function(rec, asNew = false) { 99 | return CopyElement(rec, patchFile, asNew); 100 | }, 101 | allSettings: patcherService.settings, 102 | logMessage: logMessage, 103 | cacheRecord: idCacheService.cacheRecord(patchFile) 104 | }, interApiService.getApi('UPF')); 105 | }; 106 | 107 | let loadAndPatch = function(load, patch) { 108 | filesToPatch.forEach(filename => { 109 | let recordsToPatch = getRecordsToPatch(load, filename); 110 | if (patch) patchRecords(load, patch, filename, recordsToPatch); 111 | }); 112 | }; 113 | 114 | let recordsAndPatch = function(records, patch, label = 'records') { 115 | patcherProgress(`Getting ${label}`); 116 | let r = records(filesToPatch, helpers, settings, locals); 117 | if (!patch) return; 118 | patcherProgress(`Patching ${r ? r.length : 0} ${label}`); 119 | r && r.forEach(record => { 120 | let patchRecord = CopyElement(record, patchFile, false); 121 | patch(patchRecord, helpers, settings, locals); 122 | }); 123 | }; 124 | 125 | let executeBlock = function(block) { 126 | let {init, skip, load, records, label, patch} = block; 127 | if (skip && skip()) return; 128 | if (init) init(patchFile, helpers, settings, locals); 129 | if (records) return recordsAndPatch(records, patch, label); 130 | if (load) loadAndPatch(load, patch); 131 | }; 132 | 133 | let initialize = function(exec) { 134 | patcherProgress('Initializing...'); 135 | if (!exec.initialize) return; 136 | exec.initialize(patchFile, helpers, settings, locals); 137 | }; 138 | 139 | let process = function(exec) { 140 | if (!exec.process) return; 141 | exec.process.forEach(executeBlock); 142 | }; 143 | 144 | let finalize = function(exec) { 145 | patcherProgress('Finalizing...'); 146 | if (!exec.finalize) return; 147 | exec.finalize(patchFile, helpers, settings, locals); 148 | }; 149 | 150 | let getExecutor = function() { 151 | return patcher.execute.constructor === Function ? 152 | patcher.execute(patchFile, helpers, settings, locals) : 153 | patcher.execute; 154 | }; 155 | 156 | let patcherId = patcherInfo.id; 157 | filesToPatch = patcherInfo.filesToPatch; 158 | patcher = patcherService.getPatcher(patcherId); 159 | helpers = getPatcherHelpers(); 160 | settings = patcherService.settings[patcherId]; 161 | executor = getExecutor(); 162 | customProgress = executor.customProgress; 163 | if (customProgress) 164 | Object.assign(helpers, { addProgress, progressMessage }); 165 | 166 | initialize(executor); 167 | process(executor); 168 | finalize(executor); 169 | }; 170 | }); 171 | -------------------------------------------------------------------------------- /src/Views/buildPatches.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ngapp}) => 2 | ngapp.controller('buildPatchesController', function($scope, $q, patcherService, patchBuilder) { 3 | // helper functions 4 | let getUsedFileNames = function() { 5 | let patchFileNames = $scope.patchPlugins.map(function(patchPlugin) { 6 | return patchPlugin.filename; 7 | }); 8 | return xelib.GetLoadedFileNames(false).concat(patchFileNames); 9 | }; 10 | 11 | let getNewPatchFilename = function() { 12 | let usedFileNames = getUsedFileNames(), 13 | patchFileName = 'Patch.esp', 14 | counter = 1; 15 | while (usedFileNames.includes(patchFileName)) { 16 | patchFileName = `Patch ${++counter}.esp`; 17 | } 18 | return patchFileName; 19 | }; 20 | 21 | let getDisabledHint = function(patchPlugin) { 22 | if (patchPlugin.filename.length === 0) 23 | return 'Patch plugin filename cannot be empty.'; 24 | let enabledPatchers = patchPlugin.patchers.filter(p => p.active); 25 | if (enabledPatchers.length === 0) 26 | return 'No patchers to build.'; 27 | }; 28 | 29 | // scope functions 30 | $scope.patcherToggled = function(patcher) { 31 | $scope.settings[patcher.id].enabled = patcher.active; 32 | $scope.updatePatchStatuses(); 33 | }; 34 | 35 | $scope.patchFileNameChanged = function(patchPlugin) { 36 | patchPlugin.patchers.forEach(function(patcher) { 37 | $scope.settings[patcher.id].patchFileName = patchPlugin.filename; 38 | }); 39 | }; 40 | 41 | $scope.addPatchPlugin = function() { 42 | $scope.patchPlugins.push({ 43 | filename: getNewPatchFilename(), 44 | patchers: [] 45 | }); 46 | }; 47 | 48 | $scope.removePatchPlugin = (index) => $scope.patchPlugins.splice(index, 1); 49 | 50 | $scope.buildPatchPlugin = function(patchPlugin) { 51 | patcherService.saveSettings(); 52 | patchBuilder.buildPatchPlugins([patchPlugin]); 53 | }; 54 | 55 | $scope.buildAllPatchPlugins = function() { 56 | patcherService.saveSettings(); 57 | patchBuilder.buildPatchPlugins($scope.patchPlugins); 58 | }; 59 | 60 | $scope.updatePatchStatuses = function() { 61 | $scope.patchPlugins.forEach(patchPlugin => { 62 | patchPlugin.disabledHint = getDisabledHint(patchPlugin); 63 | patchPlugin.disabled = !!patchPlugin.disabledHint; 64 | }); 65 | }; 66 | 67 | // event handlers 68 | $scope.$on('buildAllPatches', $scope.buildAllPatchPlugins); 69 | $scope.$on('addPatchPlugin', $scope.addPatchPlugin); 70 | $scope.$on('itemsReordered', $scope.updatePatchStatuses, true); 71 | 72 | // initialization 73 | patcherService.updateFilesToPatch(); 74 | $scope.patchPlugins = patcherService.getPatchPlugins(); 75 | $scope.updatePatchStatuses(); 76 | }); 77 | -------------------------------------------------------------------------------- /src/Views/managePatchersModal.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ngapp}) => 2 | ngapp.controller('managePatchersModalController', function($scope, patcherService) { 3 | // helper functions 4 | let selectTab = function(tab) { 5 | $scope.tabs.forEach((tab) => tab.selected = false); 6 | $scope.currentTab = tab; 7 | $scope.currentTab.selected = true; 8 | $scope.onBuildPatchesTab = $scope.currentTab.label === 'Build Patches'; 9 | }; 10 | 11 | // initialize scope variables 12 | $scope.settings = patcherService.settings; 13 | $scope.tabs = patcherService.getTabs(); 14 | $scope.noPatchers = $scope.tabs.length === 1; 15 | selectTab($scope.tabs[0]); 16 | 17 | // scope functions 18 | $scope.buildAllPatches = () => $scope.$broadcast('buildAllPatches'); 19 | $scope.addPatchPlugin = () => $scope.$broadcast('addPatchPlugin'); 20 | 21 | $scope.closeModal = function() { 22 | patcherService.saveSettings(); 23 | $scope.$emit('closeModal'); 24 | }; 25 | 26 | $scope.onTabClick = function(e, tab) { 27 | e.stopPropagation(); 28 | if (tab === $scope.currentTab) return; 29 | selectTab(tab); 30 | }; 31 | }); 32 | -------------------------------------------------------------------------------- /src/Views/upfSettings.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ngapp}) => 2 | ngapp.controller('upfSettingsController', function($timeout, $scope) { 3 | $scope.bannerStyle = { 4 | 'background': `url('${moduleUrl}/images/banner.jpg')`, 5 | 'background-size': 'cover' 6 | }; 7 | 8 | $scope.managePatchers = function() { 9 | $scope.saveSettings(false); 10 | $timeout(() => openManagePatchersModal($scope)); 11 | }; 12 | 13 | $scope.reloadPatchers = () => $scope.$emit('reloadPatchers'); 14 | }); 15 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | module.exports = function({moduleUrl}) { 2 | let openManagePatchersModal = function(scope) { 3 | scope.$emit('openModal', 'managePatchers', { 4 | basePath: `${moduleUrl}/partials` 5 | }); 6 | }; 7 | 8 | return { openManagePatchersModal }; 9 | }; 10 | -------------------------------------------------------------------------------- /update_dist_branch.bat: -------------------------------------------------------------------------------- 1 | call npm run build 2 | call git subtree split --branch dist --prefix dist/ 3 | call git checkout origin dist 4 | call git push origin dist 5 | call git checkout origin master --------------------------------------------------------------------------------