Plugin Styles:
Creating plugins compatible with Neoloader means following certain design principles, but the effort will provide benefits to your plugin's users. Different templates are provided to suite the needs of different plugins. In all cases, though, the plugins to be managed through Neoloader need a declaration file - this is an INI file containing data that informs Neoloader of what a plugin is and how/when to load it. The formatting of the declaration file can be explored here.
Plugin Style |
Style Traits |
Neoloader Default Plugin Style |
A _typical_ plugin made to get the most out of Neoloader will use a three-file system. In this setup, main.lua - the code that is run at standard load time - contains only the necessary commands to see if Neoloader exists and to register the plugin. The actual code to be executed is stored in another file, which is called during Neoloader's "Init" stage if various checks all return valid, based on the data provided in your declaration file. Obviously, while it is referred to as "Three-file" default, there is no reason to assume you must use ONLY three files; there is simply a minimum of three files expected in this system, and your code can call whatever files it wants from your plugin's core file. |
Hybrid Plugin Style | Sometimes, plugins might want to run at standard execution time while still letting Neoloader manage their enabled/disabled state. Plugins designed this way should not be provided as function libraries, as Neoloader cannot control the execution time of these plugins. However, for plugins that will be at the end of a dependency tree, where nothing will be dependent on the code being executed, this is much less of a worry. While the default style above is reccomended, this method is also provided. In this method, the only files are main.lua and the declaration. The path value of the declaration file is empty, telling Neoloader there is nothing to run; instead, main.lua will institute its own checks after registry when executed by the default plugin loader at standard execution time. A properly designed main.lua will make sure to run its code ONLY if Neoloader says the load state should be enabled for this particular plugin (or if Neoloader is not available). |
Compatibility-Patched Plugin Style | Plugins that were not designed for Neoloader can still be made manageable through its interface using a utility called NeoPatcher. NeoPatcher applies a mix of the hybrid and default styles in order to make plugins manageable without breaking their compatibility. In a compatibility patch, a plugin's main.lua will be renamed to something else (core_patched.lua in the case of NeoPatcher), and a new main.lua, prerun.lua, and declaration file are created. The Declaration's path value will point to prerun.lua, but in this case main.lua itself institutes the checks necessary to access or deny core_patched.lua from running; the end result is the plugin running at its usual time and environment, but only if Neoloader says its supposed to load (or if Neoloader is not present). Prerun.lua will freeze the mod (in case anything sets the patched mod as a dependency), and unfreeze it after the PLUGINS_LOADED game event occurs. Thus, maximum compatibility without actually changing the original code is achieved. |
Representations of each are provided in the Examples folder distributed with this documentation (todo; previous examples can be found here).
Converting standard plugins to be Neo-compatible:
Ordinary plugins are usually unable to run with Neoloader right out of the gate. This is due to a select few differences in the game's environment. Due to this, tools like NeoPatcher must adjust values in their declaration, and then handle the checking process themselves when the standard loader takes over. An example of how this works is provided in the examples folder distributed with this documentation (see 1_ Compatibility Style Example in the original examples, linked above). Listed below are known differences and possible fixes, should you wish to update the plugin instead of just patching it.
Area of incompatibility |
Why the issue exists |
Potential fixes |
File Access |
During the standard loader's execution, loadfile/dofile will pre-pend the active working directory to any path provided. During Neoloader's execution, however, because it is loading using the interface system, it does not have this alteration provided. Plugins that expect prepended paths will obviously break when loaded during the Init loading phase. | Replace dofile with lib.resolve_file, which is capable of testing for multiple locations of where to find a file. ***Some functionality of dofile is currently missing from lib.resolve_file, such as passing arguments as local values. You can manually locate the correct path to files using lib.find_file instead. |
Creating Global Variables |
Plugins typically expect global values to be creatable when they run. While it is reasonable to expect Neo plugins to also have global creation access, it is possible for Neoloader to delay a plugin from activating until after the PLUGINS_LOADED event occurs, depending on the configuration of the user's system and dependency fulfillment. It should be noted that in stock Neoloader, this will not occur, as plugin activation requires an auth token and is not triggered in neomgr. | Either test for globals to be locked (lib.get_gstate().statelock == true) or use Declare() when using globals. The latter function is part of the base game, and is not reliant on Neoloader. |
Interface Incompatibility |
This is less likely to be an issue for most plugin authors, but Neoloader does make it much easier to register custom interfaces, which may not provide 1 to 1 compatibility with the DefaultUI (especially when it comes to IUP structures) | Test for the default interface to be loaded (lib.get_gstate().ifmgr == "vo-if") or check specifically for the IUP structures to be accessed. Interfaces may provide some form of compatibility layer, too, so it may be prudent to check for commonplace APIs or Neoloader libraries that can solve this problem. |
Function Libraries:
Plugins have historically been designed to be Monolithic, containing all functionality not provided by the base game. This has lead to sometimes duplicated code and plugins that don't mesh well with each other. However, Neoloader aims to provide a standardized system for facilitating communication between different plugins through the public function class. When a Neoloader plugin is registered, it is given a table where it can store public functions other plugins can then call. Multiple versions of the same plugin can even be loaded at the same time, as they each get their own table. Through this, function libraries can provide common code usable between many different plugins easily.
There are two officially recognised styles that this type of plugin can take: Distributable and Standalone.
Library Style |
Details |
Functional limitations |
Distributable Library |
The Distributable Library is designed to be as self-contained as possible, and is distributed alongside plugins that rely on them. This makes more sense for small libraries focused on specific functionality. |
Distributable libraries cannot assume their path; their declaration file must not provide a path, and if paths are needed for files, they must be found through lib.get_path(). The declaration file is registered by the plugin that is distributing the library. |
Standalone Library |
The Standalone Library is designed to be downloaded by the end user and is loaded as a plugin. This makes more sense for major libraries containing many files and/or a wide range of functionality. |
Standalone libraries are declared and defined just like normal plugins. |
There is no reason a library cannot have both Distributable and Standalone releases, too, for instance if a library has a "lite" function set that is easily downloaded due to its small size, with an optional heavier function library that has more UI components or better on-demand fulfillment of plugin conflict resolution. For libraries like this, the Distributable version would have an always-lower version than the standalone version, and should be clearly marked.
In the end, it is up to both the library designer and the implementor to properly decide on best practices for determining which should be used and how to use them.
Function Libraries will use the lib.set_class() function to store their table of public functions and data, and plugins reliant on this data will use either lib.execute() or lib.get_class() to retrieve this data. Please refer to the LME API document to determine how to use each and which will serve you best.
Versioning:
As mentioned in the prior section, plugins can (and should) have version numbers associated with them. Neoloader is capable of processing any version string provided the following rules are met:
For example, take the semantic versioning string "1.0.2 -beta". This would break down into the following relevant data:
Major |
Minor |
Patch |
Metadata |
1 |
0 |
2 |
beta |
This same data would be obtained if someone provided the version string "v1.0r2 -beta": in this case, the preceding v is dropped, 1 and 0 are seperated by a non-numeric value, and 0 and 2 are also seperated by a non-numeric value. As long as a hyphen is not used, Neoloader will be able to process the version string appropriately.
Obtaining a tabled breakdown of a version string, and comparing between two version strings, are done by the lib.get_whole_ver() and lib.compare_sem_ver().
While plugins can call on specific versions of other plugins through the various functions provided by the LME API, it is important to note that version zero is mostly reserved; when a plugin asks for the class table of a plugin and provides "0" as the version to call on, Neoloader will retrieve and pass on the class of the plugin that has the highest version present (provided it is set as loaded, of course). Version "0.0" would not trigger this, as it would not be version "0", nor would version "0 -dist".
It is suggested that versions provided to Neoloader be in Semantic Versioning - this is not for Neoloader's sake, but for plugin authors and end-users, so they can better recognise differences in provided plugins. However, in the end, as long as plugins use the same formatting style between different versions of the same plugin, then Neoloader will be able to process them correctly.
Dependency Declaration:
Neoloader allows plugins to declare dependencies on other plugins, and will determine the appropriate time to load them as a result. Dependencies can be declared two different ways; the resultant plugin will often define something as a Hard or a Soft dependency based on which method is used.
Dependency Style | Definition |
Hard Dependency | This plugin requires this dependency to be completely fulfilled before it can run |
Soft Dependency | This plugin requires this dependency for additional functionality |
The first method, Hard Dependency, will be much more common. You declare hard dependencies directly in a plugin's declaration file, and the plugin itself will never load unless they are fulfilled. Neoloader will figure out what order plugins should load in to have their dependencies fulfilled, as long as the dependencies are present and set to load.
The second method, Soft Dependency, is meant to delay specific functions from running until a dependency is fulfilled. This is best for when you want to optionally provide functionality based on the presence or lack of a library, but the functionality provided is not necessary for the working state of your plugin. Soft Dependencies are set through using the lib.require() function.