Skip to content

Instantly share code, notes, and snippets.

@GregLando113
Created June 13, 2024 10:46
Show Gist options
  • Save GregLando113/b1b2ef61c8f1aed26ca532214799129a to your computer and use it in GitHub Desktop.
Save GregLando113/b1b2ef61c8f1aed26ca532214799129a to your computer and use it in GitHub Desktop.
ModOrganizer2 Delta Patcher Plugin
'''
@name MO2 Mod Delta Patcher
@author KAOS (xILARTH)
@description Allows you to automate creating zipfiles containing all mod changes
to allow you to keep multiple installs of the same mod playthough up-to-date.
This way you dont have to manually cherry-pick the new mods, and you dont have
to copy over GB's of redundant assets.
@usage - On the install you wish to update, Use the Gen Manifest tool to create a json file
of all your mods of that install.
- Copy that file over to the device with the install you want to update from.
- On the install you are updating from, use the Generate Delta Zipfile tool and
point it to the manifest file. The tool will then compare the manifest to your
active mods and create a zipfile of the difference, including your profile and
overwrite folders.
- Copy the zipfile to the install you wish to update, use to copy the mods and
profiles over to update as you see fit.
'''
import os
import pathlib
import sys
from PyQt6.QtCore import QCoreApplication, qCritical, QFileInfo
from PyQt6.QtGui import QIcon, QFileSystemModel
from PyQt6.QtWidgets import QFileDialog, QMessageBox, QProgressDialog
import mobase
import json, datetime
import zipfile
PLUGIN_VERSION = mobase.VersionInfo(1, 0, 0, 0)
class UnknownOutputPreferenceException(Exception):
"""Thrown if the user hasn't specified whether to output to a separate mod"""
pass
class MakeDeltaManifestTool(mobase.IPluginTool):
def __init__(self):
self.__organizer = None
self.__parentWidget = None
super(MakeDeltaManifestTool, self).__init__()
def init(self, organizer):
self.__organizer = organizer
if sys.version_info < (3, 0):
qCritical("Mod Delta Generation plugin requires a Python 3 interpreter, but is running on a Python 2 interpreter.")
QMessageBox.critical(self.__parentWidget, "Incompatible Python version.", "This version of the FNIS Integration plugin requires a Python 3 interpreter, but Mod Organizer has provided a Python 2 interpreter. You should check for an updated version, including in the Mod Organizer 2 Development Discord Server.")
return False
return True
def name(self):
return "Mod Delta Generation: Gen Manifest"
def localizedName(self):
return "Mod Delta Generation: Gen Manifest"
def author(self):
return "xILARTH"
def description(self):
return "Generate a manifest file that another install can use to know what mods you need."
def version(self):
return PLUGIN_VERSION
def requirements(self):
return []
def settings(self):
return []
def displayName(self):
return "Delta: Gen Manifest"
def tooltip(self):
return "Generate a manifest file that another install can use to know what mods you need."
def icon(self):
return QIcon("plugins/MakeDelta.ico")
def setParentWidget(self, widget):
self.__parentWidget = widget
def display(self):
output_name, _ = QFileDialog.getSaveFileName(self.__parentWidget, "Save Mod Delta Manifest", filter="*.json")
modList = self.__organizer.modList().allMods()
manifest = {
'version': PLUGIN_VERSION.canonicalString(),
'mods': modList,
}
with open(output_name,'w') as fd:
json.dump(manifest, fd, indent=4)
# self.__organizer.modList().setActive(logOutputModName, True)
pass
class MakeDeltaZipfileTool(mobase.IPluginTool):
def __init__(self):
self.__organizer = None
self.__parentWidget = None
super(MakeDeltaZipfileTool, self).__init__()
def init(self, organizer):
self.__organizer = organizer
if sys.version_info < (3, 0):
qCritical("Mod Delta Generation plugin requires a Python 3 interpreter, but is running on a Python 2 interpreter.")
QMessageBox.critical(self.__parentWidget, "Incompatible Python version.", "This version of the FNIS Integration plugin requires a Python 3 interpreter, but Mod Organizer has provided a Python 2 interpreter. You should check for an updated version, including in the Mod Organizer 2 Development Discord Server.")
return False
return True
def name(self):
return "Mod Delta Generation: Gen Delta Zipfile"
def localizedName(self):
return "Mod Delta Generation: Gen Delta Zipfile"
def author(self):
return "xILARTH"
def description(self):
return "Generate a zipfile of your profile and mods that another install does not have given their manifest."
def version(self):
return PLUGIN_VERSION
def requirements(self):
return []
def settings(self):
return []
def displayName(self):
return "Delta: Generate Delta Zipfile"
def tooltip(self):
return "Generate a zipfile of your profile and mods that another install does not have given their manifest."
def icon(self):
return QIcon("plugins/MakeDelta.ico")
def setParentWidget(self, widget):
self.__parentWidget = widget
def display(self):
recvr_manifest, _ = QFileDialog.getOpenFileName(self.__parentWidget, "Select the manifest file of the other device.", filter="JSON Manifest (*.json)")
recvr_manifest_path = pathlib.Path(recvr_manifest)
with open(recvr_manifest_path,'r') as fd:
recvr_manifest_json = json.load(fd)
recvr_manifest_version = mobase.VersionInfo(recvr_manifest_json['version'])
# TODO: Bound check file is compatible version
# recvr version not greater then current version
# recvr version major matches
# build a delta of your active mods that the reciever does not have given its manifest.
recvr_mods = recvr_manifest_json['mods']
my_mod_list = self.__organizer.modList()
my_mods = [mod for mod in my_mod_list.allMods() if (my_mod_list.state(mod) & mobase.ModState.ACTIVE)]
mod_delta = [ mod for mod in my_mods if mod not in recvr_mods]
if not mod_delta:
QMessageBox.information(self.__parentWidget, "MO2 Make Delta", "No new mods to add to manifest install.")
return
#TODO: Build delta zipfile
zip_filename, _ = QFileDialog.getSaveFileName(self.__parentWidget, "Delta zipfile save path",filter='*.zip')
zip_path = pathlib.Path(zip_filename)
zip_mod_base = pathlib.Path('mods')
mo_mods_path = pathlib.Path(self.__organizer.modsPath())
progress = QProgressDialog(parent=self.__parentWidget)
progress.setLabelText("Starting Copy...")
progress.open()
with zipfile.ZipFile(zip_path,'w') as zip:
for mod in mod_delta:
local_mod_path = pathlib.Path(mo_mods_path, mod)
zip_mod_path = pathlib.Path(zip_mod_base, mod)
modfiles = [x for x in local_mod_path.rglob('*')]
copy_prog = 0
progress.reset()
progress.setMaximum(len(modfiles))
for modfile in modfiles:
savefile = modfile.relative_to(local_mod_path)
progress.setLabelText(f'{mod}\n{str(savefile)}')
zip.write(modfile, pathlib.Path(zip_mod_path, savefile))
copy_prog += 1
progress.setValue(copy_prog)
profile_path = pathlib.Path(self.__organizer.profilePath())
for file in profile_path.rglob('*'):
zip.write(file, pathlib.Path('profile', file.relative_to(profile_path)))
overwrite_path = pathlib.Path(self.__organizer.overwritePath())
for file in overwrite_path.rglob('*'):
zip.write(file, pathlib.Path('overwrite', file.relative_to(overwrite_path)))
QMessageBox.information(self.__parentWidget, "MO2 Make Delta", f"Delta file successfully built at {str(zip_path.absolute())}")
def createPlugins():
return [
MakeDeltaManifestTool(),
MakeDeltaZipfileTool()
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment