Skip to content

Writing a Plugin with COEF

JRoush edited this page Nov 27, 2011 · 27 revisions

Table of Contents

Background

COEF builds off of the Oblivion Script Extender and it's plugin system. Before continuing here, it is a good idea to look through PluginAPI.h and the Example Plugin provided with OBSE. This article assumes the reader is already familiar with the basics of writing plugins.

In a nutshell, OBSE will load 3rd-party executable code in the form of plugin dlls. The original intent was to allow custom script functions, but since plugins can patch the engine itself they can be used to do virtually anything.

OBSE runs with both Oblivion itself and the TES IV Construction Set, so a single plugin can modify both (but not at the same time; if both run at once then OBSE will actually load each plugin twice, once for each executable). Oblivion and CS actually share a lot of common code - so much so that a single API can be used for both programs. The few classes and functions that appear only in the game are marked with an #ifdef OBLIVION ... #endif compiler directive, and those that appear only in the CS are marked with #ifndef OBLIVION ... #endif. This applies to both the OBSE class definitions and the COEF API.

All OBSE plugins (including COEF-based ones) must export two functions: OBSEPlugin_Query(), and OBSEPlugin_Load(). Query() is called to let the plugin do version checks and other validation - if it returns false, OBSE will unload the plugin. Otherwise Load() is called, in which the plugin is expected to do whatever it was written to do (e.g. add new script functions). OBSE also provides some useful services to loaded plugins (inter-plugin messaging, saving in the obse co-save, event notifications, etc).

When it runs, OBSE attaches itself to the Oblivion.exe/TESConstructionSet.exe process. This allows it to patch the executable image in memory, adding hooks or changing instructions to make things like new script functions possible. It can also call functions and reference objects in the executable directly by their memory address. OBSE plugins can do these things too, and this is where their real power comes from.

COEF provides support for patching the executable through the memaddr class in the Utilities library. This class isn't required to patch the game, but it makes it easier. The COEF API is basically a map of C++ names to memory addresses, so that plugin authors can actually call functions and reference objects in the executable directly by name.

Using the API

For a dll to use the COEF API, it must dynamically link to Oblivion.exe or TESConstructionSet.exe. This is handled at compile time by telling the linker to add API\Oblivion.lib or API\TESConstructionSet.lib as dependencies. In Visual Studio, this means opening the Project->Properties menu, navigating to Configuration Properties->Linker->Additional Dependencies and adding the appropriate lib file.

In order to provide a clean API, COEF has to make some changes to the Oblivion and CS executable images in memory. Specifically, it must build a table mapping names from the API to literal memory addresses in the executable. This information is stored in the *.eed files that come in the API/ directory. Loading these files and building the map is handled by an internal module called ExportInjector.

Before a dll that uses the API can be loaded at runtime, ExportInjector must be loaded first. This means that a single dll cannot both load ExportInjector and use the API. The loading itself is done by the following line of C++ code:

 LoadLibrary("Data\\obse\\Plugins\\COEF\\API\\ExportInjector.dll");

This is usually placed in the OBSE Query() function, so that plugin loading can abort if ExportInjector is not found or fails to load (see A 3-DLL Approach below). Note that this requires some COEF files to be present at run time.

Plugins that use the COEF API require the player to install COEF on their computer.

Players don't need everything, though, just ExportInjector and the *.eed files. These are available as standalone COEF Releases. Only plugin authors need to download the complete COEF source.

Using Components

For a dll to use COEF Components, it must dynamically link to Components.Game.dll or Components.CS.dll. The procedure is basically the same as linking to the Game/CS executables to use the API (see above).

At compile time, Components\Components.Game.lib or Components\Components.CS.lib must be added as linker dependencies. At run time, the Components dll must be loaded before any dlls that use it. This is done by the following line of C++ code (for game mode; substitute "CS" for "Game" in CS mode):

 LoadLibrary("Data\\obse\\Plugins\\COEF\\Components\\Components.Game.dll");

As with the API, plugins that use COEF Components require the player to install COEF on their computer.

A 3-DLL Approach

Oblivion and the CS share a lot of code in common, but not *all* of it. There are enough differences that code intended to run with the game must be compiled separately from code intended to run with the CS. Also (more importantly), there are usually some big differences in what a plugin actually *does* in each case. In fact, many plugins don't do anything in CS mode.

For these reasons, it is common for OBSE plugins to have two dlls for the game and CS, each compiled separately. Not all plugins need to do this; those that do little or nothing to the CS can get away with only a game-mode dll. But any plugin that makes nontrivial changes will probably need two dlls.

COEF-based plugins have an additional requirement: ExportInjector.dll must be loaded before any dll that references the API. Likewise, Components.Game/CS.dll must be loaded before anything that references the Component library. The easiest way to guarantee this is a third dll that holds plugin code that common to both the game and CS, including the OBSE Query() and Load() functions. ExportInjector (and the components library if necessary) can be loaded during Query(), so that the plugin can abort safely abort loading if it isn't found. This "loader" dll can then load the game or CS "submodule" dlls as necessary.

An example Query function:

extern "C" bool _declspec(dllexport) OBSEPlugin_Query(const OBSEInterface* obse, PluginInfo* info)
{
	// fill out plugin info structure, do version checks, open debugging log, etc.    
     // ...
      
    // load COEF ExportInjector library
    if ( !LoadLibrary("Data\\obse\\Plugins\\COEF\\API\\ExportInjector.dll") )
    {
        _FATALERROR("Could not load ExportInjector.dll.  Check that this file is installed correctly.");
        return false;
    }

    // load COEF Components library
    _MESSAGE("Loading COEF Components ...");
    const char* clibname =  ;
    if ( !LoadLibrary(  obse->isEditor ? "Data\\obse\\Plugins\\COEF\\Components\\Components.CS.dll" 
                                       : "Data\\obse\\Plugins\\COEF\\Components\\Components.Game.dll"))
    {
        _FATALERROR("Could not load Components.dll.  Check that this file is installed correctly.");
        return false;
    }

    // load Plugin Submodule 
    if ( !LoadLibrary(  obse->isEditor ? "Data\\obse\\plugins\\ExamplePluginFolder\\Submodule.CS.dll" 
                                       : "Data\\obse\\plugins\\ExamplePluginFolder\\Submodule.Game.dll"))
    {
        _FATALERROR("Could not load submodule.  Check that this file is installed correctly.");
        return false;
    }

	// all checks pass, this plugin should continue to loading stage
	return true;
}

This setup isn't exactly elegant, but experimentation has shown it to be much easier to work with than the alternatives. The main complication is passing OBSE-related information (like the Array and String variable interfaces) to the submodule so that custom script functions can be written to take advantage of the COEF API.

Example Plugins

There are three example projects available that demonstrate how to use COEF:

Basic Example
Setting up a COEF plugin in Visual Studio, loading ExportInjector, and using the API.
Intermediate Example
Saving/Loading data in the OBSE cosave, creating a public interface to pass to other plugins.
Advanced Example
Using COEF Components.
Details and instructions are in the project Readme files.