Misc Plugin dev questions[Was: Finding internal objects (Inventory Items, etc.)]

Started by Denzil Quixode, Wed 09/09/2009 15:08:39

Previous topic - Next topic

Denzil Quixode

The IAGSEngine interface available to runtime plugins lets you loop over every AGSCharacter* quite easily:

Code: ags
void IterateCharacters() {
	int i;
	for (i = 0; i < engine->GetNumCharacters(); i++) {
		AGSCharacter* c = engine->GetCharacter(i);
		// do something with the AGSCharacter pointer ...
	}
}


However, I would really like to be able to do similar things for other AGS objects than Characters (edit: and also Room Objects). This is as far as I've got for InventoryItem:

Code: ags
typedef int (*GETNUMINVENTORYITEMS)();

void IterateInventoryItems() {
	GETNUMINVENTORYITEMS GetNumInventoryItems = (GETNUMINVENTORYITEMS)engine->GetScriptFunctionAddress("Game::get_InventoryItemCount");
	if (!GetNumInventoryItems) {
		engine->AbortGame("Cannot find Game.InventoryItemCount static property");
	}
	for (i = 0; i < GetNumInventoryItems(); i++) {
		// ... but how to get the pointer for the "i-th" InventoryItem?
	}
}


All I want is the pointer, so I can pass it to other functions and methods that take an InventoryItem. I'm focusing on InventoryItem as an example, but there's other things like Dialogs. Is there any existing way to get the pointers? I don't mind it being a bit weird, as long as it works  :=

Pumaman

In theory you should be able to use:
engine->GetScriptFunctionAddress("inventory");

to return an array of the inventory item pointers, though I haven't tested this myself.

Denzil Quixode

Aha! That's ideal, if it works - I will experiment and post my findings, in case there's anyone in the future who wants to know the same thing.

(Much better than the hacky way I had just thought of - first set the "activeinv" field of the AGSCharacter struct to the number of each inventory item, and then use Player::get_ActiveInventory to pull out the pointer :P then set the activeinv field back to what it originally was at the end...)

Denzil Quixode

OK, it took me a bit of time to twig... engine->GetScriptFunctionAddress("inventory") does indeed return an address, pointing at what looks like an array of 64-bit integers. All I have to do is offset that address by the ID of the inventory item I want to get the InventoryItem* pointer - the addresses I get with this method are consistent with what I get back from the "activeinv" trick I mentioned in the previous post.

(It still feels a little bit fragile, as I am hard-coding this to expect an array of 8-byte records at the "inventory" address and I know that things like this could always change in future versions of AGS.)

Anyway, to go back to the code snippets in the first post, here's is how I would apply it to that:

Code: ags
typedef int (*GETNUMINVENTORYITEMS)();

void IterateInventoryItems() {
	GETNUMINVENTORYITEMS GetNumInventoryItems = (GETNUMINVENTORYITEMS)engine->GetScriptFunctionAddress("Game::get_InventoryItemCount");
	long long* inventory = (long long*)engine->GetScriptFunctionAddress("inventory");
	if (!GetNumInventoryItems) {
		engine->AbortGame("Cannot find Game.InventoryItemCount static property");
	}
	if (!inventory) {
		engine->AbortGame("Cannot find inventory array");
	}
	for (i = 1; i <= GetNumInventoryItems(); i++) {
		void* item = (void*)&inventory[i];
		// ...
	}
}

Denzil Quixode

Thanks so much for taking the time to help! It is very gratifying to solve these things so quickly  :D

I have a follow-up question, now...

Is there a way to use engine->GetScriptFunctionAddress() to get hold of struct methods like Character.Say() and Character.Think(), which take a variable number of input parameters (used for formatting values into the string)? I really only want to be able to call them with a single string, and not actually use the string formatting feature at all, but I need to know the number of parameters for GetScriptFunctionAddress ("Character::Say^...?")

Pumaman

For variable parameters, the number to pass is the number of fixed parameters plus 100.

For example:
"Character::Say^101"
"Overlay::SetText^104"

Denzil Quixode

Excellent ;D Thanks! I've got a good idea of how to call pretty much any of the built in functions now... very useful.

Now I have a Runtime Plugin problem. Is there a good example somewhere of using your own custom "PropertyGridObjectList" for a ContentDocument? All I need at the moment is to be able to pick from a dropdown of options, for each of the ContentDocument tabs spawned by the plugin.

Denzil Quixode

After some fiddling I think I'm halfway there now - but I still have the problem that the PropertyChanged() method on my IEditorComponent gets the name of the property that got changed (and its old value), but I can't seem to see a way to determine which ContentDocument tab the property change was for.

Edit: Another question  :-[ - I can successfully add a tab-specific menu or toolbar by assigning to the MainMenu and ToolbarCommands fields in the ContentDocument, but my IEditorComponent does not seem to receive the event when the items are actually selected. Is there a way to get them?

Pumaman

Quote from: Denzil Quixode on Sun 13/09/2009 19:08:35
After some fiddling I think I'm halfway there now - but I still have the problem that the PropertyChanged() method on my IEditorComponent gets the name of the property that got changed (and its old value), but I can't seem to see a way to determine which ContentDocument tab the property change was for.

3.2 beta 5 adds the IGUIController.ActivePane property to allow you to find this out.

QuoteEdit: Another question  :-[ - I can successfully add a tab-specific menu or toolbar by assigning to the MainMenu and ToolbarCommands fields in the ContentDocument, but my IEditorComponent does not seem to receive the event when the items are actually selected. Is there a way to get them?

Assuming your content panel inherits from the AGS.Types.EditorContentPanel class, there is a OnCommandClick method that you can override to get these notifications.

Denzil Quixode

QuoteAssuming your content panel inherits from the AGS.Types.EditorContentPanel class, there is a OnCommandClick method that you can override to get these notifications.
Ah, cool! That works perfectly. (I was only trying the CommandClick on the IEditorComponent...)
Quote3.2 beta 5 adds the IGUIController.ActivePane property to allow you to find this out.
Nice! :D

Denzil Quixode

Okay... I promise this is the last one  :=


Is it possible for an editor plugin to request to be notified whenever any room has just been saved? Sort of like an equivalent for IEditorComponent.BeforeSaveGame() for the current room instead of the whole game. I noticed there is a "RoomModifiedChanged" event on _editor.RoomController.CurrentRoom, is there a way to use that for any room?

(The reason I ask is this: I would really like the plugin to be able to know the script names of each hotspot and object in all of the rooms, at the time that IEditorComponent.BeforeSaveGame() is called. All the other (non-room-specific) script names for things are quite easy to get hold of, but these are a challenge. Now... I know that the way that the AGS Editor works, you only have one room open at a time. Loading every room with _editor.RoomController.LoadRoom() in turn and then using _editor.RoomController.CurrentRoom to get at this info does seem to work OK - if done inside IEditorComponent.RefreshDataFromGame(), before the user will have had a chance to open a room editor themselves. Doing it at any other point is a bad idea since the user won't be expecting the inevitable "Do you want to save changes to this room?" message, and will probably just cancel it (then immediately uninstall the plugin).

So, my idea was for the plugin to maintain its own set of simple records about all the rooms. First, these records would get created and populated in IEditorComponent.RefreshDataFromGame(), then whenever a room is saved, the plugin would replace only the record for the current room. If there is a much simpler way to do all this, please let me know.)


Edit: Promise already broken... I have 2 questions related to the order that things happen, internally:

1) I have an Editor plugin and a Runtime plugin, working as a team. When you save changes to a game in the AGS Editor, the Editor plugin's IEditorComponent.BeforeSaveGame() currently always seems to get run before AGS_EditorSaveGame() in the Runtime plugin - which is good, that's what I want, but is it unwise to rely on it always happening that way around?

2) I hope this one makes sense: In a Runtime Plugin, does it work to call engine->DecrementManagedObjectRefCount() on a managed object (like e.g. an Overlay) during the plugin's handler for the AGSE_SAVEGAME event? My worry is if it is "too late" at that point, and the managed object is already serialized, so when the game gets loaded again the object gets deserialized into memory with a nonzero RefCount, causing a memory leak. Or is it even dangerous to call DecrementManagedObjectRefCount from this context?

Pumaman

Quote from: Denzil Quixode on Thu 17/09/2009 04:53:17
Is it possible for an editor plugin to request to be notified whenever any room has just been saved? Sort of like an equivalent for IEditorComponent.BeforeSaveGame() for the current room instead of the whole game. I noticed there is a "RoomModifiedChanged" event on _editor.RoomController.CurrentRoom, is there a way to use that for any room?

Not currently, no. I'll add this to my list.

Quote1) I have an Editor plugin and a Runtime plugin, working as a team. When you save changes to a game in the AGS Editor, the Editor plugin's IEditorComponent.BeforeSaveGame() currently always seems to get run before AGS_EditorSaveGame() in the Runtime plugin - which is good, that's what I want, but is it unwise to rely on it always happening that way around?

Yes, currently all editor plugins will be called before run-time plugins.

Quote2) I hope this one makes sense: In a Runtime Plugin, does it work to call engine->DecrementManagedObjectRefCount() on a managed object (like e.g. an Overlay) during the plugin's handler for the AGSE_SAVEGAME event? My worry is if it is "too late" at that point, and the managed object is already serialized, so when the game gets loaded again the object gets deserialized into memory with a nonzero RefCount, causing a memory leak. Or is it even dangerous to call DecrementManagedObjectRefCount from this context?

The script interpreter is in an inconsistent state during the save, so I would strongly advise not calling any methods like that in the middle of the AGSE_SAVEGAME event, as it could well lead to memory corruption.

Denzil Quixode

Quote from: Pumaman on Mon 21/09/2009 21:56:33
Not currently, no. I'll add this to my list.
Thanks!

QuoteThe script interpreter is in an inconsistent state during the save, so I would strongly advise not calling any methods like that in the middle of the AGSE_SAVEGAME event, as it could well lead to memory corruption.

Okay, good thing I asked, I will avoid that then.

When is the right time to use engine->GetManagedAddressByKey() to "link up" related managed objects as the player loads up a savegame? The Plugin docs say "after de-serialization", but there doesn't seem to be a good way to say "OK, I want to do this bit right after de-serialization" - AGSE_RESTOREGAME and the Unserialize() method of the Object reader class are both called before/during de-serialization, and they are the only relevant places, as far as I can make out.

Pumaman

That's an interesting point.  GetManagedObjectAddressByKey will work from within an Unserialize() method, but only if the object you want has already been deserialized... and since you can't control the order in which objects are (de)serialized there's no reliable way to do this.

I'll look into adding some sort of AGSE_AFTERRESTOREGAME event to provide an entry point for you to do this.

Denzil Quixode

That would be very useful! Could there be an AGSE_BEFORESAVE, as well? (The way I'm storing managed objects means I don't always know immediately when I should be decrementing the reference counts, but I can force a full "sweep" at a moment when it isn't likely to cause a noticeable pause, which is why I was asking about calling DecrementRefCount in AGSE_SAVEGAME before - AGSE_BEFORESAVE would be good for that.)

Denzil Quixode

I have a question about what happens, or should happen, when a plugin function returns a pointer to a custom script managed object, and that pointer does not get assigned to a variable. It seems like this causes problems with reference counting - but maybe I am just doing something wrong...?

So, say I have a plugin that adds to the script header:

Code: ags
struct MyLib {
  // ... static methods ...
  import static ResultObject* PerformAction();
};


MyLib.PerformAction() is a plugin function that returns an object that contains information about the result of this action. Sometimes it might be useful to look at this object, to make sure the results are what is expected:

Code: ags
function my_ags_function()
{
  ResultObject* result = MyLib.PerformAction();
  // ... examine fields of "result" ...
}


...and that works fine. I've put a breakpoint in the Dispose() method of the IAGSScriptManagedObject that controls ResultObjects, and it seems to consistently get called as my_ags_function() completes.

But sometimes you might not care about anything in the ResultObject, so you wouldn't bother to assign the return value to a result variable:

Code: ags
function another_ags_function()
{
  MyLib.PerformAction();
}


The problem is that in this case, the Dispose() breakpoint for the ResultObject doesn't seem to get hit until the game is terminated. This could be a big problem if MyLib.PerformAction() gets called from repeatedly_execute() or something like that - hundreds of ResultObjects would get created every minute, eventually taking up way too much memory. I'm not sure how to combat this.

It also seems to happen if I want to use one function call directly inside the parameter for another function call. For example, say I have another function void MyLib.ErrorIfResultIsBad(ResultObject* resultObj); which checks a ResultObject and shows an error if there is a problem, or otherwise does nothing, and I want to use it like this:

Code: ags
function yet_another_ags_function()
{
  MyLib.ErrorIfResultIsBad( MyLib.PerformAction() );
}


...the Dispose() for the ResultObject returned by MyLib.ReturnAction() here again does not seem to get called until the game is quit.

Pumaman

Quote from: Denzil Quixode on Wed 30/09/2009 03:51:19
I have a question about what happens, or should happen, when a plugin function returns a pointer to a custom script managed object, and that pointer does not get assigned to a variable. It seems like this causes problems with reference counting - but maybe I am just doing something wrong...?

If the returned object is assigned to a variable, it will get disposed when that variable goes out of scope (like in your local variable example).

If the returned object is not assigned to a variable, it will get disposed the next time AGS runs its garbage collection sweep (which depends on how intense the script is and how many managed objects are being created).

If you want to debug this to have peace of mind that it works, put something like this in repeatedly_execute:

String newString = "hello";
newString.Append("world");

because the return value of Append is not used, it will keep creating more and more managed objects (Strings) which will cause AGS to run its garbage collection sweep fairly quickly.

Denzil Quixode

Ah, okay, I was just impatient then :) Great! Thank you for clarifying, I see what's going on now.

(I suppose it could conceivably be a problem if the plugin allocated a lot of memory for each of these objects, right? Since AGS knows the number of managed objects, but not how much memory they are using, individually. That isn't the case here, though - I'm just thinking out loud.)

Pumaman

Yes, potentially. But the assumption is that if an engine/plugin function is allocating a large managed object (eg. dynamic sprite, drawing surface), the chances are that the script will need to use what is returned, and it won't just be an optional ignorable return value.

Pumaman

Quote from: Denzil Quixode on Wed 23/09/2009 19:47:29
That would be very useful! Could there be an AGSE_BEFORESAVE, as well? (The way I'm storing managed objects means I don't always know immediately when I should be decrementing the reference counts, but I can force a full "sweep" at a moment when it isn't likely to cause a noticeable pause, which is why I was asking about calling DecrementRefCount in AGSE_SAVEGAME before - AGSE_BEFORESAVE would be good for that.)

AGSE_PRESAVEGAME and AGSE_POSTRESTOREGAME have been added to AGS 3.2 RC 1. See the plugin API page for an updated plugin reference.

SMF spam blocked by CleanTalk