Jump to content
  • 0

Mod Organizer Automation


jacko

Question

Hello,

 

I'm trying to automate mod installation for MO using Autoit. It's going fine so far, but I'd rather have a better experience than needing to take control of the mouse and click all the buttons. But MO doesn't have any keyboard shortcuts that I can see, doesn't seem to have any standard windows "controls" that can be hooked into with Autoit (or probably COM either), and it can't be clicked on at all without using the mouse (attempting to send click events fail).

 

  • If I am wrong on any of these counts please let me know, I am new to this stuff and just messing around.
  • Is there is some kind of command line access to install and manage mods?
  • Do keyboard shortcuts exist?
  • Is there some other way to automate mod tasks in MO?

I noticed there was a way to create python plugins for MO, where can I find out more information? I only saw one example but I didn't see anything that referenced mod installation, would a plugin work? I have some python experience but I would need some guidance on getting set up.

 

Thanks bye

 

Link to comment
Share on other sites

14 answers to this question

Recommended Posts

  • 0

Yes, you can install mods from a python plugin.

In the mod organizer code repository there is code for a plugin that takes a list of mods from a csv file, downloads them from nexus and then installs them. I didn't release it because it has a lot of potential to piss off nexus admins but left it in the repository as a sample for installing mods from python plugins.

 

You can see the code here:

https://sourceforge.net/p/modorganizer/code/ci/default/tree/source/plugins/installerBatch/pyBatchInstaller.py

 

It did work at some point, I don't promise it still does (due to changes in the interface). Should get you started though.

  • +1 1
Link to comment
Share on other sites

  • 0

Depends on the plugin. "tool"-plugins will appear in that dropdown.

"installer"-plugins may add a new supported file type (that type will than appear in the downloads list and in the "install mod" dialog) and will then be invoked when such a file is opened.

An installer-plugin may also use an already supported file type and can then "choose" if it supports a specific file (based on the name or content, but the latter is currently not supported in python plugin)

 

There are more ways to have your plugin do something: You can also write plugins to add more checks to the warnings dialog (exclamation mark in the upper right) and you can set up a plugin to automatically be run when a binary is run.

 

Further plugin types are added as necessary, so if you want to write a plugin to be notified every time after a mod was installed or after a download was completed or when the profile changes, ... then let me know.

  • +1 1
Link to comment
Share on other sites

  • 0

Hey thanks for the replies. I'm looking at the source of that plugin now. I don't have any C or QT knowledge, but I'm trying to understand what's going on more or less. I was expecting an inherited class with methods to override for plugin functionality, but searching for the parent object mobase.IPluginInstallerCustom was tricky since there was no import statement I could follow back, but I think I finally found it in some C code that appears to match the method names (but I have no idea what it does). Also I tried searching all files for a reference of "tool" and found a C file that mentioned IPluginToolWrapper which was also the same file that had IPluginInstallerCustomWrapper which was very similar to the mobase.IPluginInstallerCustom that the plugin inherits from, but when I replace mobase.IPluginInstallerCustom with IPluginTool I predictably get a not found error on MO startup.

 

In summary, I have no idea what's going on. Not because of anything you did but just because I'm a hobbyist programmer so my knowledge is really limited. It's also kinda difficult because the plugin doesn't appear to work so I can't edit and check what changed and hack my way through things. I'd simply like to make a plugin that accepts a text file listing mod locations on the computer and it would go through and install them. I'll keep trying tomorrow to make something work. bye

 

edit: I suppose to keep it simple, how do I make a plugin a "tool"? Then we can go from there.

Edited by jacko
Link to comment
Share on other sites

  • 0

You probably don't want to read the source code. What you want is the headers in uibase starting with an I. Those are the interface classes and most have been exported in some way to python. The python class mobase.IPluginInstallerCustom corresponds to the C++ interface IPluginInstallerCustom found in iplugininstallercustom.h IPluginInstallerCustom derives from IPluginInstaller (in iplugininstaller.h) which derives from IPlugin (in - surprise! - iplugin.h) Your plugin has to implement the member functions declared "virtual" from each interface.
 
Those functions will be called by Mod Organizer as needed. If your plugin needs to access information from MO, you go through the "organizer" object passed to the init function. The interface for that object is called IOrganizer and declared in imoinfo.h (woops) This lets you create empty mod, install mods, get information about the current profile and so on. You can also get access to further interfaces like the download manager, an interface to retrieve nexus information and so on. The "hello world" plugin would look like this:  

from PyQt4.QtGui import QIcon, QMessageBox

class HelloWorld(mobase.IPluginTool):
    def init(self, organizer):
        return True
    def name(self):
        return "Hello World"
    def author(self):
        return "Tannin"
    def description(self):
        return "Gives a friendly greeting"
    def version(self):
       return mobase.VersionInfo(1, 0, 0, mobase.ReleaseType.final)
    def isActive(self):
        return True
    def settings(self):
        return []
    def displayName(self):
        return "Hello World"
    def tooltip(self):
        return "Says "Hello World""
    def icon(self):
        return QIcon(":/pyCfg/pycfgicon")
    def setParentWidget(self, widget): 
       self.__parentWidget = widget
    def display(self):
	QMessageBox.information(self.__parentWidget, "Hello", "Hello World")

def createPlugin():
        return HelloWorld() 
  • +1 1
Link to comment
Share on other sites

  • 0

Now we are making bacon. Thanks for the help things are making sense now, your C code is well commented. I was able to install a mod from its archive quite easily. I stuck the code under display method and it seems to work just fine. Then I wanted to make it so the mod had nexus information so it could be updated directly from MO, so I tried adding setNexusID but then it gives me 

Plugin "Hello World" failed: Traceback (most recent call last):  File "C:/Steam/SteamApps/common/Skyrim/ModOrganizer/plugins/pyTest.py", line 42, in display    aaj_mod = self._mo.getMod("Appropriately Attired Jarls")TypeError: No Python class registered for C++ class class MOBase::IModInterface

Here is my code so far:

from PyQt4.QtGui import QIcon, QMessageBoxclass HelloWorld(mobase.IPluginTool):    def init(self, organizer):        self._mo = organizer        return True    def name(self):        return "Hello World"    def author(self):        return "Tannin"    def description(self):        return "Gives a friendly greeting"    def version(self):        return mobase.VersionInfo(1, 0, 0, mobase.ReleaseType.final)    def isActive(self):        return True    def settings(self):        return []    def displayName(self):        return "Hello World"    def tooltip(self):        return "Says "Hello World""    def icon(self):        return QIcon(":/pyCfg/pycfgicon")    def setParentWidget(self, widget):        self.__parentWidget = widget    def display(self):        mystr = self._mo.downloadsPath()        mod_path = self._mo.downloadsPath() + "/Appropriately Attired Jarls-23793-1-1-1.7z"        self._mo.installMod(mod_path)        aaj_mod = self._mo.getMod("Appropriately Attired Jarls")        aaj_mod.setNexusID(23793)        #QMessageBox.information(self.__parentWidget, "Hello", mod_path)def createPlugin():        return HelloWorld()
Link to comment
Share on other sites

  • 0

Yeah, sorry, it turned out that interface wasn't exposed to python yet. This is fixed in 1.2.1.

Also, the installMod function now returns the newly created mod. this means you don't have to call self._mo.getMod anymore because if the user renames the mod that call would fail.

  • +1 1
Link to comment
Share on other sites

  • 0

OK so now I am moving on to installing multiple mods at a time and trying to set each mod's id and version, but I'm getting an error trying to set version string. Here is my display method:

def display(self):        mods = ["Appropriately Attired Jarls-23793-1-1-1.7z",                "Argonian Decapitation Fix-22624-1-0.rar"]        for mod_file_name in mods:            mod_path = self._mo.downloadsPath() + "/" + mod_file_name            mod_interface = self._mo.installMod(mod_path)            # change to parse filename?            if 'Jarls' in mod_path:                mod_interface.setNexusID(23793)                mod_interface.setVersion('1.1.1')            elif 'Argonian' in mod_path:                mod_interface.setNexusID(22624)                mod_interface.setVersion('1.0')

I get this error:

Plugin "Batch Installer" failed: Traceback (most recent call last):  File "C:/Steam/SteamApps/common/Skyrim/ModOrganizer/plugins/pyTest.py", line 49, in display    mod_interface.setVersion('1.1.1')Boost.Python.ArgumentError: Python argument types in    IModInterface.setVersion(IModInterface, str)did not match C++ signature:    setVersion(struct IModInterfaceWrapper {lvalue}, class MOBase::VersionInfo)    setVersion(class MOBase::IModInterface {lvalue}, class MOBase::VersionInfo)

I'm getting the impression it expects something other than a string for version, and I went looking through the code and came upon versioninfo.h which seems to maybe imply that there exists code to parse version strings into version datatypes. Is this available to me? This leads me to another question, if there already exists a parser for filenames to get mod name/id/version from them or will I have to roll my own?

 

thanks bye

 

edit: also, is there a way to "lookup" an ID in the nexus and get back mod name, latest version and other info?

Edited by jacko
Link to comment
Share on other sites

  • 0

Yes, you have to use the versioninfo class like this:

mod_interface.setVersion(mobase.VersionInfo(1, 1, 1, mobase.ReleaseType.final))

The following may also work, I'm not sure:

mod_interface.setVersion(mobase.VersionInfo(1, 1, 1))

Parsing the filename into mod name/id/version is not reliably possible unless you plan to write an AI ;) The way nexus mangles these file names makes correct parsing impossible, you can only guess and computers are notoriously bad at that.

 

That being said, the regular expression I currently use in MO is fairly successful, you are of course welcome to use it:

^([a-zA-Z0-9_- ]*?)([-_ ][VvRr]?[0-9_]+)?-([1-9][0-9]+)-([0-9-]*).*

mod name will be in match group 1, mod id in group 3, version (with - instead of .) in group 4

 

Getting information about a mod from nexus is currently not possible but I'll see if I can add this for the next release.

  • +1 1
Link to comment
Share on other sites

  • 0

Great stuff thanks. I decided to forgo parsing filenames. Here is my working code with test data:

def display(self):        # todo: import from csv or json        mods = [            {'file_name':   'Appropriately Attired Jarls-23793-1-1-1.7z',             'mod_name':    'Appropriately Attired Jarls',             'nexus_id':    23793,             'version':     '1.1.1',            },            {'file_name':   'Argonian Decapitation Fix-22624-1-0.rar',             'mod_name':    'Argonian Decapitation Fix',             'nexus_id':    22624,             'version':     '1.0',            },        ]        for mod_dict in mods:            mod_path = self._mo.downloadsPath() + "/" + mod_dict['file_name']            mod_interface = self._mo.installMod(mod_path)            mod_interface.setNexusID(mod_dict['nexus_id'])            # mobase.VersionInfo expects version ints & ReleaseType, init params now            version_data = [0, 0, 0, mobase.ReleaseType.final]            # update zeroed data with actual version data            for i,v in enumerate( int(v) for v in mod_dict['version'].split('.') ):                version_data[i] = v            mod_interface.setVersion(mobase.VersionInfo(*version_data))

My question would be how can I handle versions that have letters in them? Snooping around in versioninfo.h I see 

  /**   * @brief constructor   * @param versionString the string to construct from   **/  VersionInfo(const QString &versionString, VersionScheme scheme = SCHEME_DISCOVER);

Which seems to indicate (along with other mentions in the code) that not only is there a built-in version parser, but that it can detect version 'schemes' with letters and even dates. I previously tried 

mod_interface.setVersion(mobase.VersionInfo(version_string))

Thinking maybe since the name VersionInfo appeared 2 times it was some kind of overloaded method that would activate if I gave it only a string (half-remembered high-school java class) but no luck.

 

 

I think this will be the last piece I need to make a simple batch installer. However there are other MO features I'd like to access programmatically if possible instead of having to use a clicker on the GUI.

  • Start my plugin (this is hardest one for clicker and would need OCR to be reliable)
  • Create new profile
  • Hide "Quick Install" dialogue for simple mods, "Silent Install"
  • Activate mod

Thanks bye

Edited by jacko
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...

Important Information

By using this site, you agree to our Guidelines, Privacy Policy, and Terms of Use.