OBSE Plugin Tutorial

Post » Wed Jul 06, 2011 12:01 am

Hi everyone,

today i'm going to explain how the example_plugin provided with OBSE's source works . I think this will be useful for all coders, since i think that doesn't exist a tutorial explaining how to make obse plugins. You'll learn a lot of functions that are used in the process of making a plugin and how to make a plugin. Since i just finished writing this tutorial i would appreciate some feedback, and please report me any error.

Lesson 1

The obse_plugin_example directory contains 5 files :

- obse_plugin_example.sln is a file generated by visual c++ , we don't care about this
- obse_plugin_example.vcproj : same as above, you have to open this file with Visual C++ in order to see the entire plugin's structure.

- dllmain.c : this file cointains instructions that are executed every time the DLL is loaded or unloaded. This is optional, I can't understand why they made this file since it only returns TRUE every time it's called. However when making OBSE plugins you don't have to add instructions to this file, so take it as it is and add it in your plugin projects.

exports.def : this is a definition file, used by the linker to get some useful informations while linking the DLL. It exports two important functions , OBSEPlugin_Query and OBSEPlugin_Load. Since those are functions that are always in OBSE's plugins, we don't have to edit this file.

- main.cc : this is the most important file. This tutorial is mainly focused on this file since it determines your plugin's features.

So let's start anolysing main.cc :

Spoiler
#include "obse/PluginAPI.h"#include "obse/CommandTable.h"#if OBLIVION#include "obse/GameAPI.h"/*	As of 0020, ExtractArgsix() and ExtractFormatStringArgs() are no longer directly included in plugin builds.	They are available instead through the OBSEScriptInterface.	To make it easier to update plugins to account for this, the following can be used.	It requires that g_scriptInterface is assigned correctly when the plugin is first loaded.*/#define ENABLE_EXTRACT_ARGS_MACROS 1	// #define this as 0 if you prefer not to use this#if ENABLE_EXTRACT_ARGS_MACROSOBSEScriptInterface * g_scriptInterface = NULL;	// make sure you assign to this#define ExtractArgsix(...) g_scriptInterface->ExtractArgsix(__VA_ARGS__)#define ExtractFormatStringArgs(...) g_scriptInterface->ExtractFormatStringArgs(__VA_ARGS__)#endif#else#include "obse_editor/EditorAPI.h"#endif


those are the first few lines. As you can see, there are 3 #include that you'll use in almost all your plugins ( PluginAPI.h , CommandTable.h , GameAPI.h ) . #if OBLIVION is an instructions for the preprocessor : if your plugin adds new scripting functions you'll use this, instead if your plugin modifies the editor you won't use this and use #include EditorAPI.h instead. ( this is the supposition i made reading comments into obse's source code, i will confirm this in the next days ) .

#define ENABLE_EXTRACT_ARGS_MACROS 1	// #define this as 0 if you prefer not to use this#if ENABLE_EXTRACT_ARGS_MACROSOBSEScriptInterface * g_scriptInterface = NULL;	// make sure you assign to this#define ExtractArgsix(...) g_scriptInterface->ExtractArgsix(__VA_ARGS__)#define ExtractFormatStringArgs(...) g_scriptInterface->ExtractFormatStringArgs(__VA_ARGS__)#endif


You have to know how the preprocessor works to understand this code, read this guide if you want to learn more http://www.cplusplus.com/doc/tutorial/preprocessor/ . Assuming that now you know the preprocessor instructions, i'll explain the other lines.

OBSEScriptInterface * g_scriptInterface = NULL;

this line creates a new OBSEScriptInterface, it will be instantiated later in the code, so don't worry about this right now.

#define ExtractArgsix(...) g_scriptInterface->ExtractArgsix(__VA_ARGS__)#define ExtractFormatStringArgs(...) g_scriptInterface->ExtractFormatStringArgs(__VA_ARGS__)


This is just a way to make to code more readable, so that instead of writing g_scriptInterface->ExtractArgsix(__VA_ARGS__) you just need to type ExtractArgsix() in the code. It's the same for ExtractFormatStringArgs() . Those functions are defined in the struct named OBSEScriptInterface ( g_scriptInterface is an instance of this struct ) . I will explain what those functions do later when they'll be invoked in the code.

Lesson 2

Alright, this is the chunk of code i'm going to anolyse this afternoon.

Spoiler
#include "obse/ParamInfos.h"#include "obse/Script.h"#include "obse/GameObjects.h"#include IDebugLog		gLog("obse_plugin_example.log");PluginHandle				g_pluginHandle = kPluginHandle_Invalid;OBSESerializationInterface	* g_serialization = NULL;OBSEArrayVarInterface		* g_arrayIntfc = NULL;OBSEScriptInterface			* g_scriptIntfc = NULL;/*********************	Array API Example *********************/typedef OBSEArrayVarInterface::Array	OBSEArray;typedef OBSEArrayVarInterface::Element	OBSEElement;// helper function for creating an OBSE StringMap from a std::mapOBSEArray* StringMapFromStdMap(const std::map& data, Script* callingScript){	// create empty string map	OBSEArray* arr = g_arrayIntfc->CreateStringMap(NULL, NULL, 0, callingScript);	// add each key-value pair	for (std::map::const_iterator iter = data.begin(); iter != data.end(); ++iter) {		g_arrayIntfc->SetElement(arr, iter->first.c_str(), iter->second);	}	return arr;}// helper function for creating an OBSE Map from a std::mapOBSEArray* MapFromStdMap(const std::map& data, Script* callingScript){	OBSEArray* arr = g_arrayIntfc->CreateMap(NULL, NULL, 0, callingScript);	for (std::map::const_iterator iter = data.begin(); iter != data.end(); ++iter) {		g_arrayIntfc->SetElement(arr, iter->first, iter->second);	}	return arr;}// helper function for creating OBSE Array from std::vectorOBSEArray* ArrayFromStdVector(const std::vector& data, Script* callingScript){	OBSEArray* arr = g_arrayIntfc->CreateArray(&data[0], data.size(), callingScript);	return arr;}


We can see there are more #include :

- #include "obse/ParamInfos.h" defines all the parameter types that can be passed to a cs or obse function. You must always include this.
- #include "obse/Script.h" as the name says, this header defines the structure of a script, so that it can be used inside your plugins. Like above, it must be included.
- #include "obse/GameObjects.h" this header defines all ingame objects, so if you want to handle them in your plugins you must include this header file.
- #include it's a standard C++ library, obviously it contains useful functions for managing strings.

IDebugLog   gLog("obse_plugin_example.log");


This line defines gLog object ( it is declared into IDegubLog.h ) and gives the dubeg log file a name. In this case the file will be named obse_plugin_example.log, if you don't provided any name it'll be named debug.log by default. We will learn how to use gLog later in the code.

PluginHandle            g_pluginHandle = kPluginHandle_Invalid;OBSESerializationInterface   * g_serialization = NULL;OBSEArrayVarInterface      * g_arrayIntfc = NULL;OBSEScriptInterface         * g_scriptIntfc = NULL;


This code declares four objects, their names are quite self-explanatory . We'll find them later, for now just know that they exist.

typedef OBSEArrayVarInterface::Array   OBSEArray;typedef OBSEArrayVarInterface::Element   OBSEElement;


Now we'll begin working on arrays. Remember, typedef are only used to make the code more readable, in this case instead of writing OBSEArrayVarInterface::Array every time you want to declare an array you'll just have to type OBSEArray. It's the same for OBSEElement.

// helper function for creating an OBSE StringMap from a std::mapOBSEArray* StringMapFromStdMap(const std::map& data, Script* callingScript){   // create empty string map   OBSEArray* arr = g_arrayIntfc->CreateStringMap(NULL, NULL, 0, callingScript);   // add each key-value pair   for (std::map::const_iterator iter = data.begin(); iter != data.end(); ++iter) {      g_arrayIntfc->SetElement(arr, iter->first.c_str(), iter->second);   }   return arr;}


This is the first function defined in the code. As you can see, it returns an OBSEArray* ( it's a pointer because in C++ array identifiers are pointers that contain the address of the beginning of the array ). This function takes 2 params :

- const std::map& data :
this is not very easy to understand for beginners. You have to know what templates are in order to fully understand this line. Here is a good guide if you have doubts about this http://www.cplusplus.com/doc/tutorial/templates/. However, this line declares a param which is a map containing strings and OBSEElement.

- Script* callingScript :
as the name tells, this object will contain a reference to the script that called this function.

Then the function creates a new map named arr. As we can see, the map is empty ( CreateStringMap takes 4 params : an array of strings, an array of OBSEElements, the initial size and a reference to the calling script ) . Of course you have to know what a map is, basically it's a collection of elements associated to a key ( in this case a string ).

Now that we have an empty map, we have to fill it. For doing so we use a for cycle , let's take a look at it since it's not very easy to understand :

- std::map::const_iterator iter = data.begin() creates a new iterator that is used to iterate every element into the data map. At the beginning this iterator points to the first element of the map.
- iter != data.end() , of course this means " keep on cycling until the map is over"
- ++iter : with this function iter points to the next element ( i should say to the next couple key-element ).

During each cycle we take a key and the corresponding element from data and put it into our new map ( arr ) . This is done by using the function SetElement.

SetElement(arr, iter->first.c_str(), iter->second);


The first param is the map where you want to put the key and the element, the second param is the key corresponding to the current iter ( converted to a string using c_str() function) and the third param is the elem corresponding to the current iter.
Eventually the function returns an handle to the newly created map.

Maybe you're asking yourself : what is the meaning of this function? It takes a map and converts its keys to strings, no matter what type they are before. In fact it is called StringMapFromStdMap.


The next function is very similar to StringMapFromStdMap, it simply takes a map created with default C++ libraries and converts it to a OBSE map. You can see it in this code :

// helper function for creating an OBSE Map from a std::mapOBSEArray* MapFromStdMap(const std::map& data, Script* callingScript){   OBSEArray* arr = g_arrayIntfc->CreateMap(NULL, NULL, 0, callingScript);   for (std::map::const_iterator iter = data.begin(); iter != data.end(); ++iter) {      g_arrayIntfc->SetElement(arr, iter->first, iter->second);   }   return arr;}


What's the difference between a standard map and an OBSE map? Obse maps are used inside obse plugins and have more functionalities. The code of this function should be undestandable now, since it's almost equal to the other function.

Eventually, we have ArrayFromStdVector() function. Let's take a look at its code :

// helper function for creating OBSE Array from std::vectorOBSEArray* ArrayFromStdVector(const std::vector& data, Script* callingScript){   OBSEArray* arr = g_arrayIntfc->CreateArray(&data[0], data.size(), callingScript);   return arr;}


This function takes a different parameter : instead of a map it takes a vector. This vector named data contains the OBSEElements that we want to add to our OBSEArray. So the aim of this function is creating a custom OBSEArray from a standard array. This is done by using the function CreateArray(&data[0], data.size(), callingScript) which takes as parameters the original array, its size and the calling script. I don't think this function needs further explainations, if you have any doubt please ask me.

This is the end of lesson 2 .This wasn't easy, if you have questions ask me in this thread.

Lesson 3

Let's continue with this code:

Spoiler
/**************************** Serialization routines***************************/std::string	g_strData;static void ResetData(void){	g_strData.clear();}static void ExamplePlugin_SaveCallback(void * reserved){	// write out the string	g_serialization->OpenRecord('STR ', 0);	g_serialization->WriteRecordData(g_strData.c_str(), g_strData.length());	// write out some other data	g_serialization->WriteRecord('ASDF', 1234, "hello world", 11);}static void ExamplePlugin_LoadCallback(void * reserved){	UInt32	type, version, length;	ResetData();	char	buf[512];	while(g_serialization->GetNextRecordInfo(&type, &version, &length))	{		_MESSAGE("record %08X (%.4s) %08X %08X", type, &type, version, length);		switch(type)		{			case 'STR ':				g_serialization->ReadRecordData(buf, length);				buf[length] = 0;				_MESSAGE("got string %s", buf);				g_strData = http://forums.bethsoft.com/index.php?/topic/1207389-obse-plugin-tutorial/buf;				break;			case'ASDF':				g_serialization->ReadRecordData(buf, length);				buf[length] = 0;				_MESSAGE("ASDF chunk = %s", buf);				break;			default:				_MESSAGE("Unknown chunk type $08X", type);		}	}}static void ExamplePlugin_PreloadCallback(void * reserved){	_MESSAGE("Preload Callback start");	ExamplePlugin_LoadCallback(reserved);	_MESSAGE("Preload Callback finished");}static void ExamplePlugin_NewGameCallback(void * reserved){	ResetData();}


Serialization functions are used to manage save files. In fact, obse plugins can create a separate file save and store informations inside it. Today we'll learn how do save data and how to read previously saved data.
Firstly we declare a new string named g_strData .

static void ResetData(void){   g_strData.clear();}


This is an auxiliary function used later, it simply empties g_strData. We'll se at the end of the lesson where this function is invoked.

static void ExamplePlugin_SaveCallback(void * reserved){   // write out the string   g_serialization->OpenRecord('STR ', 0);   g_serialization->WriteRecordData(g_strData.c_str(), g_strData.length());   // write out some other data   g_serialization->WriteRecord('ASDF', 1234, "hello world", 11);}


This function is called by OBSE every time the player saves the game. Later we'll see how to teach OBSE using this functions at the right time. The are two ways to write records to your save file:

- the easiest is using WriteRecordData() function. It's best when you're saving only one record . If you want to save multiple records, keep on reading . This function takes 4 params :
- the record type ( it's a string that identifies what kind of record you're writing. You can give any name to this, should be something that identifies the kind of data you're saving)
- the plugin version, it's an integer used to specify which version of your plugin wrote the current record.
- a buffer storing your data, in this case g_strData.c_str() ( i.e. a string )
- the buffer lenght

- if you want to save multiple records, you should you the combination OpenRecord()/WriteRecordData(). When starting to write a new record, call OpenRecord, passing as parameters the record type and version. Then call WriteRecordData as many times as needed to fill in the data for the record passing as parameters the data buffer and its lenght. To start the next record, just call OpenRecord again. Calling OpenRecord or exiting your save callback will automatically close the record.

It this case, the plugin creates two records :
- a STR record containing g_strData ( we'll see during lesson 4 what g_strData contains. )
- an ASDF record containing the string "hello world"

static void ExamplePlugin_LoadCallback(void * reserved){   UInt32   type, version, length;   ResetData();   char   buf[512];   while(g_serialization->GetNextRecordInfo(&type, &version, &length))   {      _MESSAGE("record %08X (%.4s) %08X %08X", type, &type, version, length);      switch(type)      {         case 'STR ':            g_serialization->ReadRecordData(buf, length);            buf[length] = 0;            _MESSAGE("got string %s", buf);            g_strData = http://forums.bethsoft.com/index.php?/topic/1207389-obse-plugin-tutorial/buf;            break;         case'ASDF':            g_serialization->ReadRecordData(buf, length);            buf[length] = 0;            _MESSAGE("ASDF chunk = %s", buf);            break;         default:            _MESSAGE("Unknown chunk type $08X", type);      }   }}


This function is invoked by Obse every time a game save is loaded. Let's see how to read our save files . First of all we declare 3 integers ( UInt32 is just another way to say 32 bit integer ) named type, version and lenght and a string that can contain up to 512 chars named buf. Function GetNextRecordInfo(&type, &version, &length) returns 1 if there are more records, 0 if records are finished. So we can use it inside a while cycle condition, so that we'll keep on reading the save file until the records are finished. After the call of GetNextRecordInfo() type,version and lenght contain the corresponding values of the current record.

_MESSAGE("record %08X (%.4s) %08X %08X", type, &type, version, length);


Then we write to debug log record's type, version and lenght using _MESSAGE() function.

Eventually we have to make a switch :

- if record's type is STR, we use ReadRecordData passing as parameters buf ( that will contain record's data ) and the record's lenght. Have you noticed that buf[length] = 0; ? We must write this line because ReadRecordData extracts raw data from the record, and since every string must end with '/0' ( or 0 without quotes ) char that line enables us using string functions with buf. Then we print buf to debug log .

- if record's type is ASDF we print a different message to debug log.

So you must remember this structure used for reading save files and always write a switch that performs different istruction basing on the current record's type.

static void ExamplePlugin_PreloadCallback(void * reserved){   _MESSAGE("Preload Callback start");   ExamplePlugin_LoadCallback(reserved);   _MESSAGE("Preload Callback finished");}


LoadCallback is called a moment after the loading of a game save, PreloadCallback is invoked before the loading of a game save. This is useful if you want to perform some action before the game is loaded, for example updating models befor the player can see them.
In this case, Preload_Callback simply prints two messages to debug log and invokes LoadCallback.

static void ExamplePlugin_NewGameCallback(void * reserved){   ResetData();}


This last function is invoked every time a new game is started or a game is loaded with no save file. Usually it resets all of your plugin's data structures.

Now there's just one more question about serialization : how can OBSE know that he must invoke those function at the right time ? To answer this question we must anolyse a few lines that we can find at the end of plugin's source code :

		g_serialization->SetSaveCallback(g_pluginHandle, ExamplePlugin_SaveCallback);		g_serialization->SetLoadCallback(g_pluginHandle, ExamplePlugin_LoadCallback);		g_serialization->SetNewGameCallback(g_pluginHandle, ExamplePlugin_NewGameCallback);#if 0	// enable below to test Preload callback, don't use unless you actually need it		g_serialization->SetPreloadCallback(g_pluginHandle, ExamplePlugin_PreloadCallback);#endif


Those functions are very easy to understand, i'll explain one and you will have no problems with the others.

g_serialization->SetSaveCallback(g_pluginHandle, ExamplePlugin_SaveCallback);


SetSaveCallback() takes two parameters :
- the plugin's handle ( we defined it in lesson 2 )
- the name of the function that defines what to do every time a game save is loaded.

That's all about serialization, and the end of lesson 3. See you tomorrow ;)

Lesson 4

Here we are, today I'm going to explain the core functions, i.e. the ones which are invoked when a command is executed. I'll post the whole code here, but one lesson isn't enough to anolyse it, so we'll finish during lesson 5.

Spoiler
/*********************** Command handlers**********************/#if OBLIVIONbool Cmd_TestExtractArgsix_Execute(COMMAND_ARGS){	UInt32 i = 0;	char str[0x200] = { 0 };	*result = 0.0;	if (ExtractArgsix(paramInfo, arg1, opcodeOffsetPtr, scriptObj, eventList, &i, str)) {		Console_Print("TestExtractArgsix >> int: %d str: %s", i, str);		*result = 1.0;	}	else {		Console_Print("TestExtractArgsix >> couldn't extract arguments");	}	return true;}bool Cmd_TestExtractFormatString_Execute(COMMAND_ARGS){	char str[0x200] = { 0 };	int i = 0;	TESForm* form = NULL;	*result = 0.0;	if (ExtractFormatStringArgs(0, str, paramInfo, arg1, opcodeOffsetPtr, scriptObj, eventList, 		SIZEOF_FMT_STRING_PARAMS + 2, &i, &form)) {			Console_Print("TestExtractFormatString >> str: %s int: %d form: %08X", str, i, form ? form->refID : 0);			*result = 1.0;	}	else {		Console_Print("TestExtractFormatString >> couldn't extract arguments.");	}	return true;}bool Cmd_ExamplePlugin_0019Additions_Execute(COMMAND_ARGS){	// tests and demonstrates 0019 additions to plugin API	// args:	//	an array ID as an integer	//	a function script with the signature {int, string, refr} returning a string	// return:	//	an array containing the keys and values of the original array	UInt32 arrID = 0;	TESForm* funcForm = NULL;	if (ExtractArgsix(paramInfo, arg1, opcodeOffsetPtr, scriptObj, eventList, &arrID, &funcForm)) {		// look up the array		 OBSEArray* arr = g_arrayIntfc->LookupArrayByID(arrID);		 if (arr) {			 // get contents of array			 UInt32 size = g_arrayIntfc->GetArraySize(arr);			 if (size != -1) {				 OBSEElement* elems = new OBSEElement[size];				 OBSEElement* keys = new OBSEElement[size];				 if (g_arrayIntfc->GetElements(arr, elems, keys)) {					 OBSEArray* newArr = g_arrayIntfc->CreateArray(NULL, 0, scriptObj);					 for (UInt32 i = 0; i < size; i++) {						 g_arrayIntfc->SetElement(newArr, i*2, elems[i]);						 g_arrayIntfc->SetElement(newArr, i*2+1, keys[i]);					 }					 // return the new array					 g_arrayIntfc->AssignCommandResult(newArr, result);				 }				 delete[] elems;				 delete[] keys;			 }		 }		 if (funcForm) {			 Script* func = OBLIVION_CAST(funcForm, TESForm, Script);			 if (func) {				 // call the function				 OBSEElement funcResult;				 if (g_scriptIntfc->CallFunction(func, thisObj, NULL, &funcResult, 3, 123456, "a string", *g_thePlayer)) {					 if (funcResult.GetType() == funcResult.kType_String) {						 Console_Print("Function script returned string %s", funcResult.String());					 }					 else {						 Console_Print("Function did not return a string");					 }				 }				 else {					 Console_Print("Could not call function script");				 }			 }			 else {				 Console_Print("Could not extract function script argument");			 }		 }	}	return true;}bool Cmd_ExamplePlugin_MakeArray_Execute(COMMAND_ARGS){	// Create an array of the format	// { 	//	 0:"Zero"	//	 1:1.0	//	 2:PlayerRef	//	 3:StringMap { "A":"a", "B":123.456, "C":"manually set" }	//	 4:"Appended"	//	}	// create the inner StringMap array	std::map stringMap;	stringMap["A"] = "a";	stringMap["B"] = 123.456;	// create the outer array	std::vector vec;	vec.push_back("Zero");	vec.push_back(1.0);	vec.push_back(*g_thePlayer);		// convert our map to an OBSE StringMap and store in outer array	OBSEArray* stringMapArr = StringMapFromStdMap(stringMap, scriptObj);	vec.push_back(stringMapArr);	// manually set another element in stringmap	g_arrayIntfc->SetElement(stringMapArr, "C", "manually set");	// convert outer array	OBSEArray* arr = ArrayFromStdVector(vec, scriptObj);	// append another element to array	g_arrayIntfc->AppendElement(arr, "appended");	if (!arr)		Console_Print("Couldn't create array");	// return the array	if (!g_arrayIntfc->AssignCommandResult(arr, result))		Console_Print("Couldn't assign array to command result.");	// result contains the new ArrayID; print it	Console_Print("Returned array ID %.0f", *result);	return true;}bool Cmd_PluginTest_Execute(COMMAND_ARGS){	_MESSAGE("plugintest");	*result = 42;	Console_Print("plugintest running");	return true;}bool Cmd_ExamplePlugin_PrintString_Execute(COMMAND_ARGS){	Console_Print("PrintString: %s", g_strData.c_str());	return true;}bool Cmd_ExamplePlugin_SetString_Execute(COMMAND_ARGS){	char	data[512];	if(ExtractArgs(PASS_EXTRACT_ARGS, &data))	{		g_strData = http://forums.bethsoft.com/index.php?/topic/1207389-obse-plugin-tutorial/data;		Console_Print("Set string %s in script %08x", data, scriptObj->refID);	}	ExtractFormattedString(ScriptFormatStringArgs(0, 0, 0, 0), data);	return true;}#endif


As you can see each function takes only one parameter : COMMAND_ARGS. We can find its definition in CommandTable.h :

#define COMMAND_ARGS	ParamInfo * paramInfo, void * arg1, TESObjectREFR * thisObj, UInt32 arg3, Script * scriptObj, ScriptEventList * eventList, double * result, UInt32 * opcodeOffsetPtr


So the only purpose of this line is , as always, making the code more readable. We can see that "inside" COMMAND_ARGS there are various variables, we'll see in a moment what they represent.

bool Cmd_TestExtractArgsix_Execute(COMMAND_ARGS){	UInt32 i = 0;	char str[0x200] = { 0 };	*result = 0.0;	if (ExtractArgsix(paramInfo, arg1, opcodeOffsetPtr, scriptObj, eventList, &i, str)) {		Console_Print("TestExtractArgsix >> int: %d str: %s", i, str);		*result = 1.0;	}	else {		Console_Print("TestExtractArgsix >> couldn't extract arguments");	}	return true;}


This is the first function, it returns a boolean and defines 2 local variables : an integer and a string . Then it sets result to 0.0 . result is one of the variables contained in COMMAND_ARGS, and it contains the value returned from the command we are creating. I had some troubles understanding the parameters passed to ExtractArgsix , so i wrote ianpatt ( one of the OBSE's developers ) and he kindly answered :

ExtractArgsix is our reimplementation of Bethesda's equivalent internal function (called ExtractArgs as a total guess). They both do effectively the same thing, but our version doesn't perform type checks (see the ParamType enum in CommandTable.h for a list of types and the checks they need to pass in vanilla ExtractArgs).

You don't need to understand the arguments being passed to ExtractArgs to write a standard plugin. However:
paramInfo - a pointer to the ParamInfo array for this command
scriptDataIn - a pointer to the start of the bytecode for the script
scriptDataOffset - the current offset (in bytes) in to the script
scriptObj - the script itself
eventList - not the greatest name, a pointer to the script state of the current object (contains local variables and some other things)


So you don't really need to fully understand the arguments , but now you have at least an idea of what they are. So this function extracts the arguments passed to our command and puts them in the last arguments , in this case i and str ( the two local variables we defined earlier ) .If the extraction was successful, we print to console the content of the arguments and we set result to 1 . Otherwise we print to console a message saying that an error occurred , and our new command will return 0.

bool Cmd_TestExtractFormatString_Execute(COMMAND_ARGS){	char str[0x200] = { 0 };	int i = 0;	TESForm* form = NULL;	*result = 0.0;	if (ExtractFormatStringArgs(0, str, paramInfo, arg1, opcodeOffsetPtr, scriptObj, eventList, 		SIZEOF_FMT_STRING_PARAMS + 2, &i, &form)) {			Console_Print("TestExtractFormatString >> str: %s int: %d form: %08X", str, i, form ? form->refID : 0);			*result = 1.0;	}	else {		Console_Print("TestExtractFormatString >> couldn't extract arguments.");	}	return true;}


The previous command purpose was testing the behaviour of ExtractArgsix; with this function we want to see hwo ExtractFormatStringArgs works. Now we have three local variables : a string, an integer and a TESForm. Here is ianpatt's explaination to ExtractFormatStringArgs :

ExtractFormatStringArgs is a version of ExtractArgs that handles printf-style formatting. Arguments:
fmtStringPos - index of the argument containing the format string
buffer - output pointer, will be filled with the formatted string
paramInfo, scriptDataIn, scriptDataOffset, scriptObj, eventList - same as above
maxParams - the maximum number of parameters that can be passed to the function, needed so we can do cleanup


In this case , fmtStringPos is 0, buffer is str and maxParams is set to SIZEOF_FMT_STRING_PARAMS + 2 ( we have two additional parameters after the formatted string, i and form) . So each time you use a formatted string as an argument for your commands you must set maxParams to SIZEOF_FMT_STRING_PARAMS plus the number of additional arguments. If the extraction was successful, it prints to console the content of str, i and form and returns 1.
form ? form->refID : 0 means that if form contains something the functions must print form's refID, otherwise it must print 0.

bool Cmd_ExamplePlugin_0019Additions_Execute(COMMAND_ARGS){	// tests and demonstrates 0019 additions to plugin API	// args:	//	an array ID as an integer	//	a function script with the signature {int, string, refr} returning a string	// return:	//	an array containing the keys and values of the original array	UInt32 arrID = 0;	TESForm* funcForm = NULL;	if (ExtractArgsix(paramInfo, arg1, opcodeOffsetPtr, scriptObj, eventList, &arrID, &funcForm)) {		// look up the array		 OBSEArray* arr = g_arrayIntfc->LookupArrayByID(arrID);		 if (arr) {			 // get contents of array			 UInt32 size = g_arrayIntfc->GetArraySize(arr);			 if (size != -1) {				 OBSEElement* elems = new OBSEElement[size];				 OBSEElement* keys = new OBSEElement[size];				 if (g_arrayIntfc->GetElements(arr, elems, keys)) {					 OBSEArray* newArr = g_arrayIntfc->CreateArray(NULL, 0, scriptObj);					 for (UInt32 i = 0; i < size; i++) {						 g_arrayIntfc->SetElement(newArr, i*2, elems[i]);						 g_arrayIntfc->SetElement(newArr, i*2+1, keys[i]);					 }					 // return the new array					 g_arrayIntfc->AssignCommandResult(newArr, result);				 }				 delete[] elems;				 delete[] keys;			 }		 }		 if (funcForm) {			 Script* func = OBLIVION_CAST(funcForm, TESForm, Script);			 if (func) {				 // call the function				 OBSEElement funcResult;				 if (g_scriptIntfc->CallFunction(func, thisObj, NULL, &funcResult, 3, 123456, "a string", *g_thePlayer)) {					 if (funcResult.GetType() == funcResult.kType_String) {						 Console_Print("Function script returned string %s", funcResult.String());					 }					 else {						 Console_Print("Function did not return a string");					 }				 }				 else {					 Console_Print("Could not call function script");				 }			 }			 else {				 Console_Print("Could not extract function script argument");			 }		 }	}	return true;}


As you can see from the comments, this command takes as arguments an array ID and a functions script with a specific signature. First of all we extract the array ID and funcForm. Then we set arr so that it points to the array identified by arrID. Why don't we use a simple array as argument, instead of this array ID? Simply because OBSE versions up to 0019 don't support array as ExtractArgsix arguments. So we have to associate an integer to the array and use it as a reference to our array. Now we can use arrays directly, here is an example :

Array* arr;		ExtractArgs(PASS_EXTRACT_ARGS, &arr);


PASS_EXTRACT_ARGS contains all the arguments that we always have to use with ExtractArgs, and as you can see, i used directly an array that will contain the command's parameter array. OBSE's developers suggest to use ExtractArgs, in 99% of cases this will be enough.

Let's back to our code. After the definition of arr, we get its size and put it into an integer called size. Then we create two arrays that will contain the elements and the keys of arr. Then, if the extraction of elements and keys is successful , we create a new array called newArr and initialize it with a simple for cycle so that it will contain elements and keys from arr. Eventually , with g_arrayIntfc->AssignCommandResult(newArr, result); , we assign newArr as command result, so that this command will return the new array.

In the second piece of code we'll execute the script passed as argument to our command.

 Script* func = OBLIVION_CAST(funcForm, TESForm, Script);


This code makes a cast : func will contain funcForm "trasformed" into a Script variable. This is possible because Script inherits from TESForm , so the cast is legitimate ( google "Liskov substitution principle" if you want to know more).

Now that func is considered as a script, we can execute it using g_scriptIntfc->CallFunction(func, thisObj, NULL, &funcResult, 3, 123456, "a string", *g_thePlayer) . Here is an explanation of its arguments :

-func : the function that we want to execute
-thisObj: a reference to the object which called the function
-NULL: a reference to a container, if needed. In this case we don't need it.
-&funcResult : this variable will contain the result of the function we're calling
-3 : the number of the arguments passed to the function
-123456 : the first argument of this function is an integer, this is a random one
-"a string": the second argument of this function is a string, and this is a random one
-*g_thePlayer : the third argument of this function is a reference, in this case we use a reference to Player.

If the function execution was successful and the returned type was a string , we print to console the string, otherwise we print to console what happened . Those messages are quite self-explanatory, so i won't write about them. If you have doubts, ask me.

bool Cmd_ExamplePlugin_MakeArray_Execute(COMMAND_ARGS){	// Create an array of the format	// { 	//	 0:"Zero"	//	 1:1.0	//	 2:PlayerRef	//	 3:StringMap { "A":"a", "B":123.456, "C":"manually set" }	//	 4:"Appended"	//	}	// create the inner StringMap array	std::map stringMap;	stringMap["A"] = "a";	stringMap["B"] = 123.456;	// create the outer array	std::vector vec;	vec.push_back("Zero");	vec.push_back(1.0);	vec.push_back(*g_thePlayer);		// convert our map to an OBSE StringMap and store in outer array	OBSEArray* stringMapArr = StringMapFromStdMap(stringMap, scriptObj);	vec.push_back(stringMapArr);	// manually set another element in stringmap	g_arrayIntfc->SetElement(stringMapArr, "C", "manually set");	// convert outer array	OBSEArray* arr = ArrayFromStdVector(vec, scriptObj);	// append another element to array	g_arrayIntfc->AppendElement(arr, "appended");	if (!arr)		Console_Print("Couldn't create array");	// return the array	if (!g_arrayIntfc->AssignCommandResult(arr, result))		Console_Print("Couldn't assign array to command result.");	// result contains the new ArrayID; print it	Console_Print("Returned array ID %.0f", *result);	return true;}


Here is another command defined in this plugin. As yo can see from the comments, it creates an array with the specified format. I don't think this function needs to be explained, it is well commented and it has not anything new. The only new function is push_back(). It is used to add an element to a vector , so for example

vec.push_back(stringMapArr);


adds stringMapArr to vec's next index.

As always, if you don't understand something, PM me or write a question in this thread.

bool Cmd_PluginTest_Execute(COMMAND_ARGS){	_MESSAGE("plugintest");	*result = 42;	Console_Print("plugintest running");	return true;}


Another very simple command, it just prints two messages to debug log and to console, returning always as result 42.

bool Cmd_ExamplePlugin_SetString_Execute(COMMAND_ARGS){	char	data[512];	if(ExtractArgs(PASS_EXTRACT_ARGS, &data))	{		g_strData = http://forums.bethsoft.com/index.php?/topic/1207389-obse-plugin-tutorial/data;		Console_Print("Set string %s in script %08x", data, scriptObj->refID);	}	ExtractFormattedString(ScriptFormatStringArgs(0, 0, 0, 0), data);	return true;}


This command takes a string as argument and puts it into g_strData ( a string that we declared earlier in the code). Then it prints data and the refID of the calling script.

bool Cmd_ExamplePlugin_PrintString_Execute(COMMAND_ARGS){	Console_Print("PrintString: %s", g_strData.c_str());	return true;}


This command simply prints to console g_strData.

That's all for lesson 4, today we've learned how to implement new commands, tomorrow i'll explain how to bind those functions to a name that can be used in our normal CS scripts.

Lesson 5

Here is the code :

Spoiler
/*************************** Command definitions**************************/static CommandInfo kPluginTestCommand ={	"plugintest",	"",	0,	"test command for obse plugin",	0,		// requires parent obj	0,		// doesn't have params	NULL,	// no param table	HANDLER(Cmd_PluginTest_Execute)};static ParamInfo kParams_ExamplePlugin_0019Additions[2] ={	{ "array var", kParamType_Integer, 0 },	{ "function script", kParamType_InventoryObject, 0 },};DEFINE_COMMAND_PLUGIN(ExamplePlugin_SetString, "sets a string", 0, 1, kParams_OneString)DEFINE_COMMAND_PLUGIN(ExamplePlugin_PrintString, "prints a string", 0, 0, NULL)DEFINE_COMMAND_PLUGIN(ExamplePlugin_MakeArray, test, 0, 0, NULL);DEFINE_COMMAND_PLUGIN(ExamplePlugin_0019Additions, "tests 0019 API", 0, 2, kParams_ExamplePlugin_0019Additions);static ParamInfo kParams_TestExtractArgsix[2] ={	{	"int",		kParamType_Integer,	0	},	{	"string",	kParamType_String,	0	},};static ParamInfo kParams_TestExtractFormatString[SIZEOF_FMT_STRING_PARAMS + 2] ={	FORMAT_STRING_PARAMS,	{	"int",		kParamType_Integer,	0	},	{	"object",	kParamType_InventoryObject,	0	},};DEFINE_COMMAND_PLUGIN(TestExtractArgsix, "tests 0020 changes to arg extraction", 0, 2, kParams_TestExtractArgsix);DEFINE_COMMAND_PLUGIN(TestExtractFormatString, "tests 0020 changes to format string extraction", 0, 					  SIZEOF_FMT_STRING_PARAMS+2, kParams_TestExtractFormatString);/*************************	Messaging API example*************************/OBSEMessagingInterface* g_msg;void MessageHandler(OBSEMessagingInterface::Message* msg){	switch (msg->type)	{	case OBSEMessagingInterface::kMessage_ExitGame:		_MESSAGE("Plugin Example received ExitGame message");		break;	case OBSEMessagingInterface::kMessage_ExitToMainMenu:		_MESSAGE("Plugin Example received ExitToMainMenu message");		break;	case OBSEMessagingInterface::kMessage_PostLoad:		_MESSAGE("Plugin Example received PostLoad mesage");		break;	case OBSEMessagingInterface::kMessage_LoadGame:	case OBSEMessagingInterface::kMessage_SaveGame:		_MESSAGE("Plugin Example received save/load message with file path %s", msg->data);		break;	case OBSEMessagingInterface::kMessage_Precompile: 		{			ScriptBuffer* buffer = (ScriptBuffer*)msg->data;					_MESSAGE("Plugin Example received precompile message. Script Text:\n%s", buffer->scriptText);			break;		}	case OBSEMessagingInterface::kMessage_PreLoadGame:		_MESSAGE("Plugin Example received pre-loadgame message with file path %s", msg->data);		break;	case OBSEMessagingInterface::kMessage_ExitGame_Console:		_MESSAGE("Plugin Example received quit game from console message");		break;	default:		_MESSAGE("Plugin Example received unknown message");		break;	}}


During lesson 5 we'll learn how to bind commands to a struct that defines their features and we'll see how the Messaging API works.

static CommandInfo kPluginTestCommand ={	"plugintest",	"",	0,	"test command for obse plugin",	0,		// requires parent obj	0,		// doesn't have params	NULL,	// no param table	HANDLER(Cmd_PluginTest_Execute)};


There are two ways to define the command's struct, and this is the first. We define a new CommandInfo variable named kPluginTestCommand ( you can give any name to this, just try to use reasonable names ) . Let's the variables contained in kPluginTestCommand :

- "plugintest" : this is the name that you'll use in CS when you want to use this function. So if you want to use this command in a CS script you'll have to type plugintest();
- "" : this is the command' short name, you can leave it empty.
- 0 : the command's opcode, in this phase you should always set this to 0. We'll understand during lesson 6 what opcodes are.
- "test command for obse plugin" : a short description of the command
- 0 : this variable state wheter the command needs a parent object or not. If set to 0, the command will need a parent object, otherwise it won't.
- 0 : the number of parameters passed to the command
- NULL : the param table, in this case in isn't needed.

DEFINE_COMMAND_PLUGIN(ExamplePlugin_SetString, "sets a string", 0, 1, kParams_OneString)


This is the second way to define a command. The name of the command will be the name of the function that defines its behaviour , in this case ExamplePlugin_SetString. Here we have a param table , i.e. a variable that defines what kind of params will be passed to the command. You can find the complete list of default params types in ParamInfos.h.

DEFINE_COMMAND_PLUGIN(ExamplePlugin_0019Additions, "tests 0019 API", 0, 2, kParams_ExamplePlugin_0019Additions);


ExamplePlugin_0019Additions requires a couple of params that isn't defined in ParamInfos.h, so we have to write a custom ParamInfo struct :

static ParamInfo kParams_TestExtractArgsix[2] ={	{	"int",		kParamType_Integer,	0	},	{	"string",	kParamType_String,	0	},};


Since we have 2 params , we create an array of ParamInfo that contains informations about the params. Each param is defined by three variables, let's anolyse one :
- "int" : a mnemonic name
- kParamType_Integer , a ParamInfo variable that defines the param type
- 0, if set to 1 means that the param is optional, otherwise if it's 0 means that this is a required param. In this case both params are required.

As you can see, the remaining part of the code works in the same way, so just read it to make sure you understood everything.

/*************************	Messaging API example*************************/OBSEMessagingInterface* g_msg;void MessageHandler(OBSEMessagingInterface::Message* msg){	switch (msg->type)	{	case OBSEMessagingInterface::kMessage_ExitGame:		_MESSAGE("Plugin Example received ExitGame message");		break;	case OBSEMessagingInterface::kMessage_ExitToMainMenu:		_MESSAGE("Plugin Example received ExitToMainMenu message");		break;	case OBSEMessagingInterface::kMessage_PostLoad:		_MESSAGE("Plugin Example received PostLoad mesage");		break;	case OBSEMessagingInterface::kMessage_LoadGame:	case OBSEMessagingInterface::kMessage_SaveGame:		_MESSAGE("Plugin Example received save/load message with file path %s", msg->data);		break;	case OBSEMessagingInterface::kMessage_Precompile: 		{			ScriptBuffer* buffer = (ScriptBuffer*)msg->data;					_MESSAGE("Plugin Example received precompile message. Script Text:\n%s", buffer->scriptText);			break;		}	case OBSEMessagingInterface::kMessage_PreLoadGame:		_MESSAGE("Plugin Example received pre-loadgame message with file path %s", msg->data);		break;	case OBSEMessagingInterface::kMessage_ExitGame_Console:		_MESSAGE("Plugin Example received quit game from console message");		break;	default:		_MESSAGE("Plugin Example received unknown message");		break;	}}


This is an example that shows of messagging API works. We must write a function that has one argument ( OBSEMessagingInterface::Message* msg ) and which can have any name. We'll see during lesson 6 how to teach OBSE using this a the message handler. This code simply switches msg->type and performs different operations depending on the type of the message. Let's see one example to understand them all :

	case OBSEMessagingInterface::kMessage_ExitGame:		_MESSAGE("Plugin Example received ExitGame message");		break;


If the recieved message was a kMessage_ExitGame , we know that the game is closing and we print to the log file "Plugin Example received ExitGame message".

You can perform any action depending on the type of the message you received, just remember to always write break; at the end of each instruction block.
Now try to read the other cases yourself, you shouldn't have problem understanding them. As always, if you have doubts, write me.

That's all for lesson 5, i think this was a pretty simple lesson. The tutorial is almost finished, and lesson 6 won't be hard. So be happy, you almost know how to code OBSE plugins! :D

Lesson 6

Here we are , this is the last lesson ! Here is the last chunk of code :

Spoiler
extern "C" {bool OBSEPlugin_Query(const OBSEInterface * obse, PluginInfo * info){	_MESSAGE("query");	// fill out the info structure	info->infoVersion = PluginInfo::kInfoVersion;	info->name = "obse_plugin_example";	info->version = 1;	// version checks	if(!obse->isEditor)	{		if(obse->obseVersion < OBSE_VERSION_INTEGER)		{			_ERROR("OBSE version too old (got %08X expected at least %08X)", obse->obseVersion, OBSE_VERSION_INTEGER);			return false;		}#if OBLIVION		if(obse->oblivionVersion != OBLIVION_VERSION)		{			_ERROR("incorrect Oblivion version (got %08X need %08X)", obse->oblivionVersion, OBLIVION_VERSION);			return false;		}#endif		g_serialization = (OBSESerializationInterface *)obse->QueryInterface(kInterface_Serialization);		if(!g_serialization)		{			_ERROR("serialization interface not found");			return false;		}		if(g_serialization->version < OBSESerializationInterface::kVersion)		{			_ERROR("incorrect serialization version found (got %08X need %08X)", g_serialization->version, OBSESerializationInterface::kVersion);			return false;		}		g_arrayIntfc = (OBSEArrayVarInterface*)obse->QueryInterface(kInterface_ArrayVar);		if (!g_arrayIntfc)		{			_ERROR("Array interface not found");			return false;		}		g_scriptIntfc = (OBSEScriptInterface*)obse->QueryInterface(kInterface_Script);			}	else	{		// no version checks needed for editor	}	// version checks pass	return true;}bool OBSEPlugin_Load(const OBSEInterface * obse){	_MESSAGE("load");	g_pluginHandle = obse->GetPluginHandle();	/***************************************************************************	 *		 *	READ THIS!	 *		 *	Before releasing your plugin, you need to request an opcode range from	 *	the OBSE team and set it in your first SetOpcodeBase call. If you do not	 *	do this, your plugin will create major compatibility issues with other	 *	plugins, and may not load in future versions of OBSE. See	 *	obse_readme.txt for more information.	 *		 **************************************************************************/	// register commands	obse->SetOpcodeBase(0x2000);	obse->RegisterCommand(&kPluginTestCommand);	obse->RegisterCommand(&kCommandInfo_ExamplePlugin_SetString);	obse->RegisterCommand(&kCommandInfo_ExamplePlugin_PrintString);	// commands returning array must specify return type; type is optional for other commands	obse->RegisterTypedCommand(&kCommandInfo_ExamplePlugin_MakeArray, kRetnType_Array);	obse->RegisterTypedCommand(&kCommandInfo_ExamplePlugin_0019Additions, kRetnType_Array);	obse->RegisterCommand(&kCommandInfo_TestExtractArgsix);	obse->RegisterCommand(&kCommandInfo_TestExtractFormatString);	// set up serialization callbacks when running in the runtime	if(!obse->isEditor)	{		// NOTE: SERIALIZATION DOES NOT WORK USING THE DEFAULT OPCODE BASE IN RELEASE BUILDS OF OBSE		// it works in debug builds		g_serialization->SetSaveCallback(g_pluginHandle, ExamplePlugin_SaveCallback);		g_serialization->SetLoadCallback(g_pluginHandle, ExamplePlugin_LoadCallback);		g_serialization->SetNewGameCallback(g_pluginHandle, ExamplePlugin_NewGameCallback);#if 0	// enable below to test Preload callback, don't use unless you actually need it		g_serialization->SetPreloadCallback(g_pluginHandle, ExamplePlugin_PreloadCallback);#endif		// register to use string var interface		// this allows plugin commands to support '%z' format specifier in format string arguments		OBSEStringVarInterface* g_Str = (OBSEStringVarInterface*)obse->QueryInterface(kInterface_StringVar);		g_Str->Register(g_Str);		// get an OBSEScriptInterface to use for argument extraction		g_scriptInterface = (OBSEScriptInterface*)obse->QueryInterface(kInterface_Script);	}	// register to receive messages from OBSE	OBSEMessagingInterface* msgIntfc = (OBSEMessagingInterface*)obse->QueryInterface(kInterface_Messaging);	msgIntfc->RegisterListener(g_pluginHandle, "OBSE", MessageHandler);	g_msg = msgIntfc;	// get command table, if needed	OBSECommandTableInterface* cmdIntfc = (OBSECommandTableInterface*)obse->QueryInterface(kInterface_CommandTable);	if (cmdIntfc) {#if 0	// enable the following for loads of log output		for (const CommandInfo* cur = cmdIntfc->Start(); cur != cmdIntfc->End(); ++cur) {			_MESSAGE("%s",cur->longName);		}#endif	}	else {		_MESSAGE("Couldn't read command table");	}	return true;}


As you can see , there are two main functions : bool OBSEPlugin_Query(const OBSEInterface * obse, PluginInfo * info) and bool OBSEPlugin_Load(const OBSEInterface * obse) . The former is used to initialize some variables we used before and to make some version checks, the latter registers our commands and performs other operations that we're going to anolyse. They are defined inside

extern "C" {  ...  } 


because they must be available to OBSE to initialize and load the plugin.

Let's begin with OBSEPlugin_Query().

bool OBSEPlugin_Query(const OBSEInterface * obse, PluginInfo * info){	_MESSAGE("query");	// fill out the info structure	info->infoVersion = PluginInfo::kInfoVersion;	info->name = "obse_plugin_example";	info->version = 1;


Here we print to debug log the string "query" and we fill the plugin info structure. In your plugins you must edit info->name using your plugin's name and info->version writing a number that represents your plugin's version.

	// version checks	if(!obse->isEditor)	{		if(obse->obseVersion < OBSE_VERSION_INTEGER)		{			_ERROR("OBSE version too old (got %08X expected at least %08X)", obse->obseVersion, OBSE_VERSION_INTEGER);			return false;		}#if OBLIVION		if(obse->oblivionVersion != OBLIVION_VERSION)		{			_ERROR("incorrect Oblivion version (got %08X need %08X)", obse->oblivionVersion, OBLIVION_VERSION);			return false;		}#endif


These are all version checks, you don't have to udit them, just use them as they are.

		g_serialization = (OBSESerializationInterface *)obse->QueryInterface(kInterface_Serialization);		if(!g_serialization)		{			_ERROR("serialization interface not found");			return false;		}		if(g_serialization->version < OBSESerializationInterface::kVersion)		{			_ERROR("incorrect serialization version found (got %08X need %08X)", g_serialization->version, OBSESerializationInterface::kVersion);			return false;		}


Here we initialize our g_serializationj variable ( we used it in lesson 3 ) and then we make some other version checks.

		g_arrayIntfc = (OBSEArrayVarInterface*)obse->QueryInterface(kInterface_ArrayVar);		if (!g_arrayIntfc)		{			_ERROR("Array interface not found");			return false;		}		g_scriptIntfc = (OBSEScriptInterface*)obse->QueryInterface(kInterface_Script);			}	else	{		// no version checks needed for editor	}	// version checks pass	return true;}


Here we initialize g_arrayIntfc ( we used this variable during lesson 4 to handle arrays ) and g_scriptIntfc ( always lesson 4, we used it to execute a script from inside the plugin ) .

As you can see , the entire code of OBSEPlugin_Query() doesn't need much modification , it is almost the same in all plugins. This function returns true if everything went well.

bool OBSEPlugin_Load(const OBSEInterface * obse){	_MESSAGE("load");	g_pluginHandle = obse->GetPluginHandle();


This is the last important function . Firstly we print a string in the debug log containing "load". The we initialize the g_pluginHandle.

	/***************************************************************************	 *		 *	READ THIS!	 *		 *	Before releasing your plugin, you need to request an opcode range from	 *	the OBSE team and set it in your first SetOpcodeBase call. If you do not	 *	do this, your plugin will create major compatibility issues with other	 *	plugins, and may not load in future versions of OBSE. See	 *	obse_readme.txt for more information.	 *		 **************************************************************************/	// register commands	obse->SetOpcodeBase(0x2000);


As you can see from the comments, this is a particular function : you must set here you first opcode. Opcodes are numers that are associated to each command, so it's a big issue if two commands have the same opcode . In order to avoid this problems, you must ask OBSE's team a base opcode that you'll use as argument in this function. They keep a record of free opcodes, so just tell them how many new commands you want to implement with your plugin and they'll give you an opcode range.

	obse->RegisterCommand(&kPluginTestCommand);	obse->RegisterCommand(&kCommandInfo_ExamplePlugin_SetString);	obse->RegisterCommand(&kCommandInfo_ExamplePlugin_PrintString);	// commands returning array must specify return type; type is optional for other commands	obse->RegisterTypedCommand(&kCommandInfo_ExamplePlugin_MakeArray, kRetnType_Array);	obse->RegisterTypedCommand(&kCommandInfo_ExamplePlugin_0019Additions, kRetnType_Array);	obse->RegisterCommand(&kCommandInfo_TestExtractArgsix);	obse->RegisterCommand(&kCommandInfo_TestExtractFormatString);


Here we register all the commands that we created in our plugin. The sintax is simple :

obse->RegisterCommand(&Name_of_CommandInfo_structure_we_created_during_lesson_5);

Just remember to use obse->RegisterTypedCommand() when the command you're registering returns an array.

	// set up serialization callbacks when running in the runtime	if(!obse->isEditor)	{		// NOTE: SERIALIZATION DOES NOT WORK USING THE DEFAULT OPCODE BASE IN RELEASE BUILDS OF OBSE		// it works in debug builds		g_serialization->SetSaveCallback(g_pluginHandle, ExamplePlugin_SaveCallback);		g_serialization->SetLoadCallback(g_pluginHandle, ExamplePlugin_LoadCallback);		g_serialization->SetNewGameCallback(g_pluginHandle, ExamplePlugin_NewGameCallback);#if 0	// enable below to test Preload callback, don't use unless you actually need it		g_serialization->SetPreloadCallback(g_pluginHandle, ExamplePlugin_PreloadCallback);#endif


We've already anolysed this code during lesson 3, now you know where it is .

		// register to use string var interface		// this allows plugin commands to support '%z' format specifier in format string arguments		OBSEStringVarInterface* g_Str = (OBSEStringVarInterface*)obse->QueryInterface(kInterface_StringVar);		g_Str->Register(g_Str);		// get an OBSEScriptInterface to use for argument extraction		g_scriptInterface = (OBSEScriptInterface*)obse->QueryInterface(kInterface_Script);	}


These lines are well-commented , i don't think they need explainations.

	// register to receive messages from OBSE	OBSEMessagingInterface* msgIntfc = (OBSEMessagingInterface*)obse->QueryInterface(kInterface_Messaging);	msgIntfc->RegisterListener(g_pluginHandle, "OBSE", MessageHandler);	g_msg = msgIntfc;


Here we initialize the g_msg ( messaging interface ) that we used during lesson 5. As you can see, msgIntfc is used as a support variable, if you want you can directly use g_msg. RegisterListener() takes as arguments the plugin's handle, "OBSE" and the name of the function that handles the messages.

	OBSECommandTableInterface* cmdIntfc = (OBSECommandTableInterface*)obse->QueryInterface(kInterface_CommandTable);	if (cmdIntfc) {#if 0	// enable the following for loads of log output		for (const CommandInfo* cur = cmdIntfc->Start(); cur != cmdIntfc->End(); ++cur) {			_MESSAGE("%s",cur->longName);		}#endif	}	else {		_MESSAGE("Couldn't read command table");	}	return true;}


These line are used to get the command table, in this plugin it isn't used. It is used to get access to OBSE's internal command table.

That's all folks! I hope this tutorial helped you getting more confident with the creation of OBSE plugins, the next step for you is reading the comments inside PluginAPI.h, i think you won't find any difficulty understanding them. And eventually start coding your own plugins, using chunks of code from obse_plugin and adding your own functions.

Thank you for reading my lessons and good luck with your coding !
User avatar
Jessica Phoenix
 
Posts: 3420
Joined: Sat Jun 24, 2006 8:49 am

Post » Wed Jul 06, 2011 1:40 am

Looks quite comprehensive! You should consider porting this to the wiki. At any rate, keep up the bad work :thumbsup:
User avatar
Laura Elizabeth
 
Posts: 3454
Joined: Wed Oct 11, 2006 7:34 pm

Post » Wed Jul 06, 2011 2:10 am

Indeed. I didn't go through it all, but it looks very detailed. Please post it to the http://cs.elderscrolls.com/constwiki/index.php/Main_Page because forum threads fall off the first few pages and do get purged after a while.
User avatar
Prisca Lacour
 
Posts: 3375
Joined: Thu Mar 15, 2007 9:25 am


Return to IV - Oblivion