TorqueScript Function Scoping Rules
by Charlie Sibbach · in Torque Game Engine · 08/19/2008 (8:30 pm) · 8 replies
Hey all, I'm just hoping to put together a tutorial resource (or maybe another sufficiently motivated person could do so) on the intricacies of TorqueScript's function scoping rules. I just keep running into gotchas, and I wonder about the precedence of the different types. So I'd like to ask a few questions, and then I'll see about compiling this info into a better place. I've been looking all over GarageGames for info on this, and I think they/we really need to put out some enhanced docs on Torquescript, as there are a lot of technical aspects to it that are simply not covered in the docs. The only info on Scoping is connected to Variables, at least in the official docs; the various books are a bit better but in no way complete. If I hadn't stumbled across it, I'd never know you could access an object via a pathname down the SimGroup (nameToID("ServerGroup/ResourceGroup/MyObject") for instance), which I've found to be VITAL when dealing with potential namespace conflicts.
Some of the questions I have, which are either on my mind or currently frustrating me. Some of these are rhetorical, but I'd like a second opinion to check my understanding.
1. What names can a function be scoped to, and when? I can scope a function to a built-in class type, I can scope to a datablock type, and I can scope to an individual named object or an individual named datablock. However, this doesn't always work. If I have a GuiControl named "MyGui", I can scope to MyGui::onWake() which is built in, and I can scope to MyGui::myRandomFunction() which has no connection to the engine. However, I am running into problems with a SimGroup named "PilotFile"; I create "function PilotFile::myRandomFunction(%this)" I'm getting an error when I try $Pilot (a reference to a SimGroup named PilotFile).myRandomFunction();. What's the catch, what are the rules?
2. Precedence rules. There's at least two levels here, let's start with Datablocks. I have a datablock ItemData(MyItemData). I can't remember what the ItemData callbacks are off hand, let's assume there's one for OnCollect(%theDatablock, %theObject); I can define OnCollect as connected to either ItemData (the datablock class) or to MyItemData (a named instance of ItemData). Which gets called and when?
3. Similar to 2, but for normal objects. I run into this with GUIs all the time. Stuff like GuiPopupMenuCtrl::onSelect vs LinksSystem1Popup::onSelect. Which gets used? Is the engine smart enough to use the most specific function if both are defined?
4. ScriptObject and the Class member. If I have
I can now scope to ScriptObject, MyScriptObject, or to MyScriptObjectClass. What are the precedence rules?
That's about it for my major questions. I'll include some example code and results to support my issue with 1 below:
(0): Unknown command setCargoInStorage.
Object pilotFile(1963) SimGroup -> SimSet -> SimObject
If I do this instead:
Now for the truly strange bit. If I recompile the file back to the first way (without exiting) so that I have only this again:
But this doesn't:
Some of the questions I have, which are either on my mind or currently frustrating me. Some of these are rhetorical, but I'd like a second opinion to check my understanding.
1. What names can a function be scoped to, and when? I can scope a function to a built-in class type, I can scope to a datablock type, and I can scope to an individual named object or an individual named datablock. However, this doesn't always work. If I have a GuiControl named "MyGui", I can scope to MyGui::onWake() which is built in, and I can scope to MyGui::myRandomFunction() which has no connection to the engine. However, I am running into problems with a SimGroup named "PilotFile"; I create "function PilotFile::myRandomFunction(%this)" I'm getting an error when I try $Pilot (a reference to a SimGroup named PilotFile).myRandomFunction();. What's the catch, what are the rules?
2. Precedence rules. There's at least two levels here, let's start with Datablocks. I have a datablock ItemData(MyItemData). I can't remember what the ItemData callbacks are off hand, let's assume there's one for OnCollect(%theDatablock, %theObject); I can define OnCollect as connected to either ItemData (the datablock class) or to MyItemData (a named instance of ItemData). Which gets called and when?
3. Similar to 2, but for normal objects. I run into this with GUIs all the time. Stuff like GuiPopupMenuCtrl::onSelect vs LinksSystem1Popup::onSelect. Which gets used? Is the engine smart enough to use the most specific function if both are defined?
4. ScriptObject and the Class member. If I have
new ScriptObject(MyScriptObject)
{
class = "MyScriptObjectClass";
};I can now scope to ScriptObject, MyScriptObject, or to MyScriptObjectClass. What are the precedence rules?
That's about it for my major questions. I'll include some example code and results to support my issue with 1 below:
$Pilot = new SimGroup(PilotFile) {....};
function PilotFile::setCargoInStorage(%this) {}
$Pilot.setCargoInStorage(); // Broken!(0): Unknown command setCargoInStorage.
Object pilotFile(1963) SimGroup -> SimSet -> SimObject
If I do this instead:
function SimGroup::setCargoInStorage(%this) {}
$Pilot.setCargoInStorage(); // No problemNow for the truly strange bit. If I recompile the file back to the first way (without exiting) so that I have only this again:
function PilotFile::setCargoInStorage(%this) {}
$Pilot.setCargoInStorage(); // Still works!But this doesn't:
$Pilot.getCargoInStorage(); // same scoping, different function.
#2
I did a bit more testing:
And got these results:
==>$TestGui.testFunction();
TestGuiControl testFunction
==>$TestPilot.testFunction();
SimGroup testFunction
Out of the three object types I've tested, only SimGroup has not allowed scoping to an individual name. Unfortunately I don't have time for a source dive right now, is name-scoping something that is put in on a per-type basis?
08/19/2008 (9:09 pm)
More follow up:I did a bit more testing:
$TestPilot = new SimGroup(PilotFile)
{
};
function SimGroup::testFunction(%this)
{
echo("SimGroup testFunction");
}
function PilotFile::testFunction(%this)
{
echo("PilotFile testFunction");
}
$TestGui = new GuiControl(TestGuiControl)
{
};
function GuiControl::testFunction(%this)
{
echo("GuiControl testFunction");
}
function TestGuiControl::testFunction(%this)
{
echo("TestGuiControl testFunction");
}And got these results:
==>$TestGui.testFunction();
TestGuiControl testFunction
==>$TestPilot.testFunction();
SimGroup testFunction
Out of the three object types I've tested, only SimGroup has not allowed scoping to an individual name. Unfortunately I don't have time for a source dive right now, is name-scoping something that is put in on a per-type basis?
#3
Name->class->superClass->Type.
In the case where a function is found at a particular namespace, that function will be executed. Versions of the function that exist in "higher" namespaces can be executed using the Parent namespace:
I had thought that scoping functions to object names always worked, so I'm surprised by your results relating to SimGroups. I would be interested to see if the context of the call (on an ID or a Name) makes a difference. For example, if you typed this in the console:
08/19/2008 (9:37 pm)
As far as I know, Torque tries to resolve scoping going from the "lowest" namespace level up to the "highest" or most general:Name->class->superClass->Type.
In the case where a function is found at a particular namespace, that function will be executed. Versions of the function that exist in "higher" namespaces can be executed using the Parent namespace:
function MyObject::someFunction(%this)
{
Parent::someFunction();
}Using the Parent namespace will cause Torque to continue to move "up" the namespace tree searching for another version of the function, again stopping when one is found. That namespace could then also call the function on the Parent namespace.I had thought that scoping functions to object names always worked, so I'm surprised by your results relating to SimGroups. I would be interested to see if the context of the call (on an ID or a Name) makes a difference. For example, if you typed this in the console:
PilotFile.testFunction();Alternately try this:
new SimGroup(PilotFile)
{
};
$TestPilot = "PilotFile";Then try executing: $TestPilot.testFunction();If the value (Name or ID) stored in the global makes a difference for function scoping I would consider that a bug in TorqueScript's scoping rules.
#4
It doesn't matter if I call it on the name of the object or on the ID, I get only the SimGroup function, and not the function scoped to the name PilotFile. Still, it is ignoring a function that should by rights be of higher precedence. If I take out SimGroup::testFunction(), I always get an error, BTW.
Another question, since I've seen it used this way, if you have multiple objects called X, and you have a function X::foo(), all the objects named X should use X::foo() and not a higher-order parent?
08/19/2008 (10:15 pm)
Mark, here's some more results using the above ; apparently, it IS context sensitive:==>$TestPilot2 = "PilotFile"; ==>$TestPilot2.testFunction(); SimGroup testFunction ==>$TestPilot.testFunction(); SimGroup testFunction ==>PilotFile.testFunction(); SimGroup testFunction
It doesn't matter if I call it on the name of the object or on the ID, I get only the SimGroup function, and not the function scoped to the name PilotFile. Still, it is ignoring a function that should by rights be of higher precedence. If I take out SimGroup::testFunction(), I always get an error, BTW.
Another question, since I've seen it used this way, if you have multiple objects called X, and you have a function X::foo(), all the objects named X should use X::foo() and not a higher-order parent?
#5
08/19/2008 (11:03 pm)
Giving multiple objects the same name is very bad and should not be done in any circumstance. I believe the actual effect is that only the last object that was created with the Name "X" will "own" the name. Unless the name "X" is a "class" name or some other namespace "group" in which case, yes, all objects that are assigned that class will be scoped to that function.
#6
With any luck, tonight I'll derive a new class PilotFile from SimGroup, with no extra additions. That way I can scope to the PilotFile object type, and bypass the problems of individual names.
However, this did make me think of a simple way to avoid namespace pollution with newly created objects in my game:
Pretty much impossible to get a conflict that way!
08/20/2008 (1:27 am)
Oh, I agree that multiple objects with the same name is very bad, but at times it's nigh unavoidable, either through accident or neglect. In this particular case, it could be a useful, exploitable behavior, to effectively turn a set of SimGroups into a virtual class without the extra functionality of the Class keyword. Of course, that is if named SimGroups could have scoped functions at all... I'll have a very big caveat about this in any case. I have the case where I'm allowing what will undoubtedly be a very inexperienced set of coders to have direct access to and be writing script, and the subtleties of namespace crossover will probably be lost. With any luck, tonight I'll derive a new class PilotFile from SimGroup, with no extra additions. That way I can scope to the PilotFile object type, and bypass the problems of individual names.
However, this did make me think of a simple way to avoid namespace pollution with newly created objects in my game:
if(!nameToID(%newName)) new Whatever(%newName);
Pretty much impossible to get a conflict that way!
#7
I think the only way to really know is to write some test script as you have done (or start looking thru the C++ code). The TorqueScript core is complex and i'm not sure how many people really understand it. I've done alot of work with it and i don't fully understand it.
I believe your issue with SimGroup is that it doesn't link the object name to the namespace. This is an extra step that SimObject derived classes need to do to support it. You used to see code like this in the onAdd()...
08/20/2008 (1:44 am)
@Charlie - This is some interesting stuff to delve into... i don't think many have.I think the only way to really know is to write some test script as you have done (or start looking thru the C++ code). The TorqueScript core is complex and i'm not sure how many people really understand it. I've done alot of work with it and i don't fully understand it.
I believe your issue with SimGroup is that it doesn't link the object name to the namespace. This is an extra step that SimObject derived classes need to do to support it. You used to see code like this in the onAdd()...
const char *name = getName();
if(name && name[0] && getClassRep())
{
Namespace *parent = getClassRep()->getNameSpace();
if(Con::linkNamespaces(parent->mName, name))
mNameSpace = Con::lookupNamespace(name);
}Looking at TGEA it seems that this is now done automatically in SimObject, so *all* SimObject should link the object name into the namespace list... at least it seems like that from reading the code. In TGE as of an old repo i have here it seems GuiControl, GameBase, TCPObject, ScriptObject, and ***ScriptGroup*** do this. Notice ScriptGroup... its a SimGroup that registers the namespaces for scripting... give it a try.
#8
Doing a little delving of my own, it seems to me that your assessment is accurate. Looks like it was a coincidence that I chose two out of three of my tests from that limited list of classes linked into the namespace. This being the case, I think we've answered all my original questions as to scoping. I will try to update at least the TDN wiki with a summary of this.
It's interesting to know as well that TGEA does this differently. My next project will be on TGEA, and I assumed the changes were limited to the rendering and material code, that changes to the greater engine had been back ported to the standard engine. Well, you know what happens when you assume.
08/20/2008 (3:24 am)
ScriptGroup, eh? Sounds exactly like what I need. I will give it a shot, indeed. Doing a little delving of my own, it seems to me that your assessment is accurate. Looks like it was a coincidence that I chose two out of three of my tests from that limited list of classes linked into the namespace. This being the case, I think we've answered all my original questions as to scoping. I will try to update at least the TDN wiki with a summary of this.
It's interesting to know as well that TGEA does this differently. My next project will be on TGEA, and I assumed the changes were limited to the rendering and material code, that changes to the greater engine had been back ported to the standard engine. Well, you know what happens when you assume.
Torque Owner Charlie Sibbach
$TestObject = new ScriptObject(TestObject) { class = "TestObjectClass"; }; function TestObjectClass::testFunction(%this) { echo("TestObjectClass testFunction"); } function ScriptObject::testFunction(%this) { echo("ScriptObject testFunction"); } function TestObject::testFunction(%this) { echo("TestObject testFunction"); }I loaded the game, and typed in $TestObject.testFunction(), and I would get the most specific function: TestObject testFunction. If I commented out that code, I'd get the Class function, and finally the ScriptObject function. However, when I first did it, I got some anomalous errors which I can't reproduce, so I'm not 100% confident. I have quit and restart many times, so it shouldn't be some kind of linkage issue.
Just putting this out there to confirm or deny.
Still can't scope to PilotFile (new SimGroup(PilotFile)), but can scope to SimGroup, and I can scope to MyGuiControl (new GuiControl(MyGuiControl)), which is decended from SimGroup.