Explanations on HoloPatcher Internal Logic - NickHugi/PyKotor GitHub Wiki

This page describes the internal logic and some nuances of the patcher. If you're just getting started with HoloPatcher, please visit Installing Mods with HoloPatcher

Generally speaking, HoloPatcher is composed of three main parts: the UI/Interface, the ConfigReader, and the Patcher.

UI/Interface

source code @ Tools/HoloPatcher/src

This is a simple GUI interface to HoloPatcher. What you'll find here:

The main purpose of this giant script is to run this function and ensure a streamlined user experience. All boils down to these two lines of code:

# Construct the installer class instance object
installer = ModInstaller(namespace_mod_path, self.gamepaths.get(), ini_file_path, self.logger)
# Start the install
installer.install()

Note:

  • When the --install option is passed, the install() call is executed in the same thread. When normal execution through the UI is used (i.e. a user presses 'install'), a new thread will be created. This is done for proper handling of stdout/stderr.
  • When the mod does not have a namespaces.ini, holopatcher creates one internally with a single 'changes.ini' entry. This is how the top combobox works and why it'll always have an entry despite a mod not providing a namespaces.ini.

ConfigReader

source code @ pykotor.tslpatcher.reader

the ConfigReader is responsible for parsing a changes.ini and accumulating the patches to execute. This happens immediately when the mod is loaded by the user or when swapping options in the namespaces comboboxes. As such, any errors/exceptions/crashes that happen in reader code will always be before the patcher modifies game files.

Patcher

source code @ pykotor.tslpatcher.patcher

The patcher itself handles all the errors/output/modifications of the patches accumulated by the ConfigReader.

PatchLists

source code @ pykotor.tslpatcher.mods

Each file represents a different patch list such as GFFList(https://github.com/NickHugi/PyKotor/blob/master/Libraries/PyKotor/src/pykotor/tslpatcher/mods/gff.py) CompileList(https://github.com/NickHugi/PyKotor/blob/master/Libraries/PyKotor/src/pykotor/tslpatcher/mods/nss.py) SSFList(https://github.com/NickHugi/PyKotor/blob/master/Libraries/PyKotor/src/pykotor/tslpatcher/mods/ssf.py) etc.

As can be seen each class inherits PatcherModifications. This causes the following behavior:

  • Each patch list will always contain the same TSLPatcher exclamation-point variables. Some patch lists may have different handling of each variable (example1, example2), this shows that everything follows more-or-less the same patch routine, which we will talk about in the next section:

The Patch Routine

source code @ pykotor.tslpatcher.patcher

The patch routine will execute all loaded changes.ini patches in a single thread.

Patchlist Priority Order

The following is the patchlist order of operations (earliest executed to last-executed)

patches_list: list[PatcherModifications] = [
    *config.install_list,  # Note: TSLPatcher executes [InstallList] after [TLKList]
    *self.get_tlk_patches(config),
    *config.patches_2da,
    *config.patches_gff,
    *config.patches_nss,
    *config.patches_ncs,   # Note: TSLPatcher executes [CompileList] after [HACKList]
    *config.patches_ssf,
]

The priority order has been changed for various reasons, mostly relating to useability. For example, if a mod wanted to overwrite a whole dialog.tlk for some reason it makes sense that InstallList patch should run before TLKList. As for the compilelist vs hacklist discrepancy, it makes more sense that users would want to compile a script and then potentially edit the NCS.

We doubt these priority order changes will affect the output of any mods. If you discover one, please report an issue.

Final validations Before Modifications.

  • Patcher will once again check if changes.ini is found on disk

  • Patcher will determine if the kotor directory is valid. Uses various heuristics of what's known about the files to safely determine if it's TSL or k1.

  • Prepare the [CompileList]: Before the patch loop runs, the patcher will first copy all the files in the namespace tslpatchdata folder matching '.nss' extension to a temporary directory. If there is a 'nwscript.nss', it will automatically append a patch to [InstallList] the nwscript.nss to the Override folder. This is done because some versions of nwnnsscomp.exe will rely on nwscript.nss being in Override rather than tslpatchdata. Specifically the KOTOR Tool version of nwnnsscomp.exe

The Patch Loop

source code @ pykotor.tslpatcher.patcher

HoloPatcher is finally ready to start applying the patches and modifying the installation. A simple for patch in all_patches loop runs, wrapped in a try-except. The try-except behavior is directly what TSLPatcher itself will do. Anytime a specific patch fails, it'll log the error and continue the next one.

Step 1: The patch routine first determines whether the mod is intending to be installed into a capsule, and if the file/resource to be patched already exists in the KOTOR path.

  • If the resource exists, back it up to a timestamped directory in the backup folder.
  • If the resource does not exist, write the patch's intended filepath into the remove these files.txt file.
  • If the patch intends to install into a capsule (.mod/.erf/.rim/.sav) and the capsule DOES NOT exist, throw a FileNotFoundError (matches tslpatcher behavior)

Step 2: Log the operation, such as `patching existing file in the 'path' folder'.

Step 3: Lookup the resource to patch: Determine where to find the source file that should be patched.

  • Check if file should be replaced or doesn't exist at output. If either condition passes, load from the mod path
  • Otherwise, load the file to be patched from the destination if it exists.
    • If the resource is encapsulated, it's a file and load it directly as a file from the destination
    • If destination is intended to be inside of a capsule, pull the resource from the capsule.
  • Log error on failure (IO exceptions, permission issues, etc.)

Step 4: Patch the resource found in step 3.: Apply the modifications to the source file determined by step 3.

  • If holopatcher determined that there's nothing to write back to disk (e.g. [CompileList] was called on an include file), continue to the next patch and stop here.

Step 5. Handle !OverrideType: A widely unknown TSLPatcher feature is configurable nature of override handling. If a file is being installed into a capsule, and that file already exists in Override, there are 3 actions that the patcher can be configured with:

class OverrideType:
    """Possible actions for how the patcher should behave when patching a file to a ERF/MOD/RIM while that filename already exists in the Override folder."""

    IGNORE = "ignore"  # Do nothing: don't even check (TSLPatcher default)
    WARN   = "warn"    # Log a warning (HoloPatcher default)
    RENAME = "rename"  # Rename the file in the Override folder with the 'old_' prefix. Also logs a warning.

source code @ tslpatcher.mods.template

Step 6: Save the resource: Save the resource to the KOTOR path on disk. !DefaultDestination and !Destination and !Filename/!SaveAs configure this.

Step 7: Repeat from step 1 for the next patch, until all patches have been completed.

All patches complete, cleanup

Step 8: Cleanup post-processed scripts: If SaveProcessedScripts=0 or not available in the changes.ini, cleanup the temp folder created in the Final Validations.

Step 8: Calculate the total patches completed.