Tutorial : How to make custom, managed objects using the AGS plugin API (v1.1)

Started by Calin Leafshade, Fri 27/05/2011 15:30:31

Previous topic - Next topic

Calin Leafshade

I made a tutorial on how to make custom structs in C++!

I would appreciate it if some C++ type person could go through it and maybe tell me if I messed up anywhere with my definitions and explanations.

GO!

What you will need:

- A c++ compiler and some basic knowledge on how to use it. I suggest using Visual C++ Express from Microsoft since it is nice and comprehensive but also free.
- The template here: http://www.thethoughtradar.com/AGS/strippedplugintemplate.zip
- No... that's pretty much it.

What *is* a managed object

Without getting too technical about memory usage, a managed object (struct) is basically something that you can declare a pointer to. Built-in examples include DynamicSprite, Character, DrawingSurface and so on.

Why would I want one?

One advantage is to allow your code to be neater and more object oriented. Consider the following example:

Code: ags

struct Particle{
	int posX;
	int posY;
	int posZ;
	int colorRed;
	int colorBlue;
	int colorGreen;
	int colorAlpha;
	int spriteNumber;
};


Do you see a problem here? Wouldn't it be nicer if we could do this instead:

Code: ags

struct Particle{
	Point3D *position;
	Color *color;
	int spriteNumber;
};


Then, when accessing the members of the struct it looks like this:

Code: ags

//'part' is an instance of the Particle struct
part.color.red = 10;
part.position.x = 32;


Another advantage is that it allows you to pass whole structs from functions like this:

Code: ags

Color *GetColorFromAGSColor(int agsColor){

Color *toReturn = Color.CreateColor(agsColor);
return *toReturn;

}


Ofcourse this function is pretty stupid, you get the idea..

Without the color struct you would need 3 different functions for the red, green and blue components (or a selector of some kind) which is inefficient and messy.

Advantage number 3 is that it allows you to nest structs within one another like the Particle example above.

Ok Fine, I am still reading, show me how

Make a project!

First you'll need to get the plugin API header and example file that is linked above. This is a custom version of the demo given on the AGS site which removes all the superfluous stuff in there for demo purposes. Make a new  empty project in VC++ as a Win32 DLL and name it something beginning with the letters AGS. AGS won't load your plugin unless it starts with AGS.

Import the header and the source file (the header is the .h and the source file is the .cpp) into the project.

In MSVC++ this will compile out of the box with no editing. Try and compile it. If it compiles and produces a .dll file in the 'Debug' folder of your project folder then you are good to start coding.

Defining the struct!

Near the top of the source file you will find a section that looks like this:

Code: ags

const char *ourScriptHeader =
	"//script header for plugin\r\n"
	"//This is my script header\r\n";


This is the header that we will give to AGS. The syntax used is just AGS script so you can put anything in here that you could put in AGS script.
the '\r\n' parts just tell the compiler that its a new line. Look up 'Escape Characters' for more information.

Ok so lets define the simple struct that we are going to be making:

Code: ags

const char *ourScriptHeader =
	"managed struct Point2D {\r\n"
	"		import attribute int X;\r\n"
	"		import attribute int Y;\r\n"
	"		import void Add(Point2D *toAdd);\r\n"
	"		import void Subtract(Point2D *toSub);\r\n"
	"       import static Point2D* Create(int X, int Y);\r\n"
	"};\r\n";


Let's go line by line.

The first line of the struct definition tells AGS that its a *managed* struct called Point2D
The second and third lines define 'attributes' X and Y. Attributes are variables with accessors functions so they are got and set by functions in the script and not directly. (I'll show you how later)
The fourth and fifth lines define new functions for us to use. Notice that they take a single variable which is another Point2D pointer.
The sixth line defines a static function called create which allows us to create a new instance of our struct. AGS doesnt allow dynamic creation of managed objects from within the script so we have to use a function in C++ to create one for us and then return he whole struct to AGSScript.
And the last line just closes the struct, duh.

STRUCT DEFINED! LEVEL UP!

Define our C++ class to handle this

What we have done above just defines the struct in AGS. This is useless to C++. C++ spits on this information and asks for a proper definition. What an asshole.

SO lets do that before it gets violent and starts chucking chairs around.

Right click the project in the Solution Explorer in the top right of your screen and goto Add -> Class. Your screen may look different to mine since I'm using VS Studio not Express.



Now click Add (C++ Class should already be selected)

Enter 'Point2D' (no quotes) into class name and press OK or Next or To Infinity And Beyond! or whatever it asks you to press.. yes I closed the window.

You will be presented with a new source file and a new header file (Point2D.cpp and Point2D.h respectively). Open the header file if it's not already open.

It will currently look like this (or similar):

Code: ags

#pragma once
class Point2D
{
public:
	Point2D(void);
	~Point2D(void);
};


exciting huh.

The header file is just like a struct definition. It defines what the class *is* and what it *does* but not *how* it does it.

Let's define stuff.

Code: ags

#pragma once
class Point2D
{
public:
	Point2D(int x, int y);
	void Add(Point2D *obj);
	void Subtract(Point2D *obj);
	~Point2D(void);
	int X;
	int Y;
};


Ok line by line again:

Line 1: #pragma defines are used internally by the compiler for stuff. This one just tells the compiler to only include this header once.. you can ignore it.
2: This just defines the class name.
3: no, shut up.
4: This is really turning into a c++ primer but basically this means that all the declarations below this line are public and accessable from outside the class.
5: Now we're getting somewhere. This defines the 'constructor' of the class. This is the method which is called every time you make a new instance of this class. Notice there is no return type.
6 & 7: These are methods which will be contained in the class.
8: This is the DEconstructor and it's called everytime an instance of your class dies. YES DIES, YOU MONSTER.
9 & 10: This are variables in your class.
11: Closes the goddamn class.

(Dear C++ developers, Yes I know I could override the + and - operators but that is beyond the scope of this article and also I would have to look it up)

Right so we've defined the class. We should *implement* it now.

Open the Point2D.cpp source file which should look like this (similar):

Code: ags

#include "Point2D.h"

Point2D::Point3D(void)
{
}

Point2D::~Point3D(void)
{
}


#include "Point2D.h" just tells the compiled to include the header file in this class.

The 'Point2D::' parts just mean that what follows are part of the Point2D class.

So let's implement stuff.

Code: ags

#include "Point2D.h"

Point2D::Point2D(int x, int y)
{
	X = x;
	Y = y;
}

void Point2D::Add(Point2D *obj)
{
	X += obj->X;
	Y += obj->Y;
}

void Point2D::Subtract(Point2D *obj)
{
	X -= obj->X;
	Y -= obj->Y;
}

Point2D::~Point2D(void)
{
}


Most of this is self-explanatory but lets look at important things.

Firstly I have changed the constructor to match the definition in the header.

X = x; might look silly but notice that the cases are different.
The capital X refers to the X we defined in the header while the lower case x is the one passed by the constructor.
Basically it means (Make the object's X equal the one i was given).

The other two methods just add (or subtract) the passed object's X & Y values to the classes X & Y values.

The last method is just an empty deconstructor.. we dont have any memory that needs to be released in this class so nothing needs to be done here (look it up in a C++ book)

Including the Point2D files in the main source file

Go back to the main source file and add the following line just after #include "agsplugin.h"

Code: ags

#include "Point2D.h"


This simply includes your Point2D class into the main source file.

Define accessor interfaces

Ok this part is a little tricky so pay attention.

AGS needs two interfaces to interact with your object. Specifically a 'ObjectReader' and a 'ManagedObject' interface.

ManagedObject Interface

Insert this code into your main source file just below the script header:

Code: ags

class ManagedPoint2DInterface : public IAGSScriptManagedObject {
public:
	ManagedPoint2DInterface() {};

	virtual int Dispose(const char *address, bool force) {
		delete address; // free the memory and return 1 to tell AGS we did.
		return 1;
	}
	virtual const char *GetType() {
		return typeName;
	}
	virtual int Serialize(const char *address, char *buffer, int bufsize) {
		
			memcpy(buffer, address, sizeof (Point2D));
			return sizeof(Point2D);
	}

	static const char* typeName;
};

const char* ManagedPoint2DInterface::typeName = "Point2D";
ManagedPoint2DInterface gManagedPoint2DInterface;



This is a new class that derives from IAGSScriptManagedObject. The specifics of the mechanics of this are beyond the scope of this article but lets look at what happens.

The 'Dispose' method is run by AGS whenever an instance of your class has been mercilessly slaughtered once it's gone out of scope.
It passes 2 arguments, *address and force.
*address is simply a pointer to the object that it being destroyed. If you want to do anything to that object then you can cast the pointer to the class and work with it.
force is a bool which tells you whether or not this is your last opportunity to release the memory (on a game restore or something) If it is 'true' then you must delete the pointer or you will have a memory leak.
If you do delete the pointer then you must return 1 if not then return 0.

In the case of our very simple struct we just delete the pointer and return 1.

*GetType returns a pointer to a string with the name of the struct that this interface deals with. In our case it just returns a pointer to a static string "Point2D" which is assigned just after the class.

The Seralize method is where we save our object when the game is saved. Since our object is just a simple type we can just memcpy our object into the buffer provided and return how many bytes we wrote.

The last line defines a global reference to the interface that we can use to actually process our objects.

For most simple types you can copy the code above and just swap out all the names so don't worry if you dont understand the code too much.

ObjectReader Interface

The Reader interface is used to read an object back from the save file when a game is restored. It looks like this:

Code: ags

class ManagedPoint2DReader : public IAGSManagedObjectReader {
public:
	ManagedPoint2DReader() {}

	virtual void Unserialize(int key, const char *serializedData, int dataSize) {
		Point2D *newPoint = new Point2D(0,0);
		memcpy(newPoint, serializedData, sizeof(Point2D));
		engine->RegisterUnserializedObject(key, newPoint, &gManagedPoint2DInterface);
	}
};

ManagedPoint2DReader gManagedPoint2DReader;


This interface is much simpler and just defines a single method which Unserializes your objects from a save file.

The first line of the method creates a pointer to a new Point2D and the next line assigns the serializedData memory to it which effectively loads your Point2D from the save file. AGS does all the hard work for you.
The second line registers the Object with the script engine which allows the scripts to access it again. The 'key' argument ensures that the memory is assigned to the correct pointer in the script.
The last line makes a global reference to the interface that AGS can use to deserialise our custom struct.

Putting it all together

Ok thats all the hard stuff out the way, now we just need to stich all the elements together!

Registering the object with AGS

In the main source file you should find a function called AGS_EngineStartup(IAGSEngine *lpEngine). This function is run, surprisingly enough, when the engine starts up. Add the following line to the end of the function:

Code: ags

engine->AddManagedObjectReader(ManagedPoint2DInterface::typeName, &gManagedPoint2DReader);


This does pretty much what it says. It let's AGS know that there is a new managed object in town and it better make accomodations for it or else shit might go down.

Making Accessor Functions

Just above the AGS_EngineStartup function add the following:

Code: ags

void Point2D_set_X(Point2D* obj, int val) {
	obj->X = val;
}

int Point2D_get_X(Point2D* obj){
	return obj->X;
}

void Point2D_set_Y(Point2D* obj, int val) {
	obj->Y = val;
}

int Point2D_get_Y(Point2D* obj){
	return obj->Y;
}

void AddPoint(Point2D *obj, Point2D* obj2){
	obj->Add(obj2);
}

void SubtractPoint(Point2D *obj, Point2D* obj2){
	obj->Subtract(obj2);
}

Point2D* CreatePoint2D(int X, int Y) {
	Point2D *newPoint = new Point2D(X, Y);
	engine->RegisterManagedObject(newPoint, &gManagedPoint2DInterface); // the second parameter is the ManagedObject interface that is responsible for disposing of the object
	return newPoint;
}


I hope that the first four of those functions are pretty straight forward. They just get or set a value in our Point2D object, a pointer to which is always passed at the first parameter to the function (i named it obj).
The 5th and 6th functions just call the Add and Subtract methods of our class.
The 7th function is our static Create function from the struct. It makes a new Point2D (passing the x and y variables it was given), registers that new object with AGS and tells it how to dispose of that object if it needs to and then returns the constructed point to your ags script.

All good? Excellent.

And FINALLY registering those accessor functions with AGS

The final step is to link our accessor functions with the functions defined in the script which is done in the AGS_EngineStartup function.

Just at the end of that function add the following:

Code: ags

	engine->RegisterScriptFunction("Point2D::Create^2", CreatePoint2D);
	engine->RegisterScriptFunction("Point2D::Add^1", AddPoint);
	engine->RegisterScriptFunction("Point2D::Subtract^1", SubtractPoint);
	engine->RegisterScriptFunction("Point2D::set_X", Point2D_set_X);
	engine->RegisterScriptFunction("Point2D::get_X", Point2D_get_X);
	engine->RegisterScriptFunction("Point2D::set_Y", Point2D_set_Y);
	engine->RegisterScriptFunction("Point2D::get_Y", Point2D_get_Y);


The RegisterScriptFunction method takes two values. The first is the name of the AGS function we are registering which we need to look at in more detail.

Firstly notice the 'Point2D::' part. This tells AGS that the following function is part of the Point2D struct.
The number following the ^ symbol tells AGS how many parameters the function takes *not including* the object itself. For instance the 'Add' function takes the object that the function is acting on plus 1 more (the object to add to it in this case) so we append the function name with ^1.

"What about those get_ and set_ functions? They aren't in our struct. WHAT IS GOING ON!?"
Remember we talked about 'attributes' earlier? This is where they come into play. When you define an attribute AGS will use two magic functions to deal with that attribute. They are called get_varName and set_varName. If i try to retrieve a varible's value like this:

Code: ags

Point2D* p = Point2D.Create(1,1);
Display("%d", p.X); // <--- here


AGS will run the Point2D::get_X method and return whatever value it returns. Likewise if I try and *set* a value it will run the set_X method and pass the value I give to it.
So those funny functions are used internally by AGS to access our values.

And thats it!

Compile and add your plugin to your game and you will have a brand new custom object to play with. Lucky you!

A final word on 'linking'

When you compile your plugin under the default settings it will 'dyamically' link to the libraries used by the MSVC++ compiler.. this isnt ideal since it will mean that your users will need that library installed on their computer. So let's fix that.

Firstly change your compiler to 'Release' (if you're using MSVC++, If you aren't i cant help you). You can do this here:


Yours might look different. Tell me and I'll update the tutorial.

Now right click your project in the Solution Explorer and click Properties.



Change the highlighted setting to "Multi-threaded (/MT)" This will include the basic libraries in with your plugin and save alot of people some trouble.

Recompile and your plugin will now be in the 'Release' folder of your project folder. Changing to Release also heavily optimises your plugins code and doesn't include all the debugging symbols and stuff. So hooray for that.

Have fun*!

*Fun involves C++ programming. Might not actually be fun.

Change History

1.0 - initial

1.1 - Fixed Unserialize method.

Dualnames

Worked on Strangeland, Primordia, Hob's Barrow, The Cat Lady, Mage's Initiation, Until I Have You, Downfall, Hunie Pop, and every game in the Wadjet Eye Games catalogue (porting)

Monsieur OUXX

Reviving this topic because I'm trying to follow the tutorial and also because it deserves it :)

I'm trying to follow the steps in Visual C++ Express 2010.
I have issues with the "out of the box" compiling.

Here is what I do :
- File-->New-->Project
- VisualC++ --> Win32 Project
- I give it a name starting with "AGS". Actually I call it "agsplugin" to match the name of the .cpp file given by Calin (by the way, does it have to be "AGS" upper case?)
- "Welcome to the Win32 application wizard" --> Next --> Application type = "DLL"
- I tick "Empty project" because I assume that the .cpp file provided by Calin is meant to be the main source file.
- In the newly created project tree : Add-->Existing Item.  Then I add the files provided by Calin for download at the beginning of this thread (the .h in the "headers" folder, and the .cpp in the "source" folder).

Then when I compile, it has issues with the STRINGIFY macro.
It says :
Code: AGS

1>c:\...\agsplugin\agsplugin\agsplugin.cpp(133): error C2146: syntax error : missing ')' before identifier 'STRINGIFY'
1>c:\...\agsplugin\agsplugin\agsplugin.cpp(133): error C2059: syntax error : ')'


The faulty line is :
Code: AGS

engine->AbortGame("Plugin needs engine version " STRINGIFY(MIN_ENGINE_VERSION) " or newer.");

And indeed, I wonder how a macro would manage to concatenate C strings in that manner, but I'm going to trust Calin, as I've seen some crazy witchcraft achieved with macros.

So, what's the catch? To I need to include to some obscure new Microsoft header?
 

Crimson Wizard

Quote from: Calin Leafshade
*Fun involves C++ programming. Might not actually be fun.
Hahaha, depends on how you look at that :D

From what I see in the AGS code right now, there's only one place where this macro is defined: AGSBlend plugin (by Steven Poulton :D).

I believe you should add these declarations:
Code: "AGSBlend.cpp"

#define STRINGIFY(s) STRINGIFY_X(s)
#define STRINGIFY_X(s) #s


EDIT: or use C sprintf function to format string

Code: ""

char longstr[100];
sprintf(longstr, "Plugin needs engine version %d or newer.", MIN_ENGINE_VERSION);
engine->AbortGame(longstr);

WARNING: unsafe code, do not use that at home (hypothetically may cause overflow).

Calin Leafshade

Sorry, I must've stripped out the macro but forgot it was still used there. Either solution given by CW is fine tho.

Monsieur OUXX

Since we're there, there's a teeny weeny copy-paste mistake in Calin's original post: You wrote "Point2D::Point3D" instead of "Point2D::Point2D" (and again with the destructor).

Please don't forget my question: Is it important if "AGS" at the beginning of the plugin's name is upper case or lower case?
 

Calin Leafshade

Quote from: Monsieur OUXX on Sun 17/06/2012 19:14:00
Please don't forget my question: Is ti important if "AGS" at the beginning of the plugin's name is upper case or lower case?

I don't think it matters, certainly not in windows. However, the convention is that they should be lower case.

Monsieur OUXX

I also forgot to mention that "AGS_EngineDebugHook" is supposed to return an int, and in the present version there is no "return" statement.
I've added "return 0;" as a workaround, but I'm not sure what it's supposed to return. I'd rather return the correct value as the .cpp file I'm producing is meant to be distributed in my next plugin (I need it clean).
 

Calin Leafshade

That is for handling debug hooks (surprisingly). You should return 1 if the plugin has handled debugging the script line and 0 if it has not.

Its only run if you have requested the hook anyway.

geork

Hello!

Sorry to drag this post up again - I didn't want to start a new thread however for the same reasons as Monsieur OUXX in 2012...

I have completed all the coding parts of Calin's tutorial, but I get the following compiler error:
Quote1>c:\program files (x86)\ags - test 3.3.0\dll\ags_template.cpp(69): error C2065: 'engine' : undeclared identifier
1>c:\program files (x86)\ags - test 3.3.0\dll\ags_template.cpp(69): error C2227: left of '->RegisterUnserializedObject' must point to class/struct/union/generic type
1>          type is ''unknown-type''

However, I think I am pointing to a class, since that line looks like:
Code: cpp
engine->RegisterUnserializedObject(key,newlight,&gManagedPXLightInterface);

And the ManagedPXLightInterface class is declared and an instance of it (gManagedPXLightInterface) is created:
Code: cpp
class ManagedPXLightInterface : public IAGSScriptManagedObject{
public:
	ManagedPXLightInterface(){}
	virtual int Dispose(const char *address, bool force){
		delete address;
		return 1;
	}
	virtual const char *GetType(){
		return typeName;
	}
	virtual int Serialize(const char *address, char *buffer, int bufsize){
		memcpy(buffer,address,sizeof(PXLight));
		return sizeof(PXLight);
	}
	static const char* typeName;
};
const char* ManagedPXLightInterface::typeName = "PXLight";
ManagedPXLightInterface gManagedPXLightInterface;


My code is almost exactly copied from Calin's, with the only difference being that the cpp file is the plugin template, and I've changed the struct/class name

Many thanks in advance!

EDIT: Sorry, I should mention I am using VC++ 2010 Express

Calin Leafshade

The problem is that "engine" is not declared or not in scope.

I forget exactly how the interface gets the engine but this is a scoping issue.

geork

Oops; - I should really learn which way is left :P

I've put it under the 'RUN TIME' section now and no compiling errors.

Thanks very much :)

eri0o

I am so sorry for necroing this topic. Does someone has a backup of the file:  http://www.thethoughtradar.com/AGS/strippedplugintemplate.zip ? (the link is broken for me)

Dualnames

I doubt it, but if you'r looking to create your own plugins, I can gladly share code for that, making a dummy project.
Worked on Strangeland, Primordia, Hob's Barrow, The Cat Lady, Mage's Initiation, Until I Have You, Downfall, Hunie Pop, and every game in the Wadjet Eye Games catalogue (porting)

SMF spam blocked by CleanTalk