Get information from datablocks without loading them.
by Richard Ranft · 07/13/2014 (10:28 am) · 17 comments
I wanted to be able to select a player model from a list and use the GuiObjectView to preview the selection. I discovered that the datablocks aren't loaded until level load and so are unavailable when choosing your first level (probably later ones, but I never checked). I also discovered that loading the datablocks when I showed the dialog was slow.
What I needed was a way to go and find my datablocks, then sift through them for their name and shapefile fields. This is the heart of what I came up with:
After I aggregated my list I dynamically populated the scroll list for selection, so any time I add a new player-usable model/datablock combo it becomes available automatically.
What I needed was a way to go and find my datablocks, then sift through them for their name and shapefile fields. This is the heart of what I came up with:
$PlayerDatablocks = new SimSet()
{
class = "PlayerDatablockList";
};
function PlayerDatablockList::init(%this)
{
%this.clearDatablocks();
%this.loadData();
}
function PlayerDatablockList::addDatablock(%this, %datablock)
{
if(!%this.isMember(%datablock))
%this.add(%datablock);
}
function PlayerDatablockList::clearDatablocks(%this)
{
while(isObject(%obj = %this.getObject(0)))
{
%this.remove(%obj);
}
%this.clear();
}
function PlayerDatablockList::loadData(%this, %path)
{
// find main.cs
%search = "art/datablocks/aiPlayer/*.cs";
// search for scripts and load them
for( %file = findFirstFile( %search ); %file !$= ""; %file = findNextFile( %search ) )
{
%this.readDatablockInfo(%file);
}
}
function PlayerDatablockList::readDatablockInfo( %this, %fileName )
{
%infoObject = new ScriptObject();
%infoObject.shapeFileInfo = "";
%infoObject.datablockName = "";
%file = new FileObject();
%validFile = false;
if ( %file.openForRead( %fileName ) )
{
%validFile = true;
%inInfoBlock = false;
%dbNameFound = false;
while ( !%file.isEOF() )
{
%line = %file.readLine();
%line = trim( %line );
%tempLine = strreplace(%line, "datablock PlayerData(", "");
if( %line !$= %tempLine ) // deduction by negatives - this is the start of a PlayerData datablock
%inInfoBlock = true;
else if( %inInfoBlock && %line $= "};" )
{
%inInfoBlock = false;
%infoObject = %infoObject @ %line;
break;
}
if( %inInfoBlock )
{
if (!%dbNameFound)
{
// find datablock name
%schloc = strchr(%tempLine, ":");
%dbName = trim(strreplace(%tempLine, %schloc, ""));
%infoObject.datablockName = %dbName;
%dbNameFound = true;
}
else
{
%tempLine = strreplace(%line, "shapeFile = ", "");
if (%line !$= %tempLine) // found the shapefile entry
{
%sfName = trim(strreplace(%tempLine, """, ""));
%sfName = strreplace(%sfName, ";", "");
%infoObject.shapeFileInfo = trim(%sfName);
}
}
}
}
%file.close();
}
else
{
%validFile = false;
error("Datablock file " @ %fileName @ " not found.");
}
%file.delete();
%this.addDatablock(%infoObject);
}
$PlayerDatablocks.init();This can be modified to scrape other information from files, or whatever.After I aggregated my list I dynamically populated the scroll list for selection, so any time I add a new player-usable model/datablock combo it becomes available automatically.
About the author
I was a soldier, then a computer technician, an electrician, a technical writer, game programmer, and now software test/tools developer. I've been a hobbyist programmer since the age of 13.
#2
You also need to grab it in core/scripts/server/server.cs:
07/13/2014 (10:56 am)
Damn.... You also need to grab it in core/scripts/server/server.cs:
function createAndConnectToLocalServer( %serverType, %level )
{
if( !createServer( %serverType, %level ) )
return false;
%conn = new GameConnection( ServerConnection );
RootGroup.add( ServerConnection );
%conn.setConnectArgs( $pref::Player::Name, $Game::SelectedPlayerClass);
%conn.setJoinPassword( $Client::Password );
%result = %conn.connectLocal();
if( %result !$= "" )
{
%conn.delete();
destroyServer();
return false;
}
return true;
}Sorry all - it was a few months ago when I did this bit... forgot about some of it....
#3
07/13/2014 (1:33 pm)
Neat-o, now I imagine this could also be extended to other data formats like checking on the server if a map has a certain game-mode specific simgroup before loading it?
#4
It is useful for anything where you want to look through a text file for information, really - you could use it implement a simple flat-file database with a little more work. Using SQLite or MySQL or some such would be more powerful, but might be overkill. Sometimes a super simple solution is enough.
07/13/2014 (2:27 pm)
I think there is already support for that sort of thing in the mission loading code - you can specify a number of properties in the mission file itself, like game type. Perhaps it could expand on that, though.It is useful for anything where you want to look through a text file for information, really - you could use it implement a simple flat-file database with a little more work. Using SQLite or MySQL or some such would be more powerful, but might be overkill. Sometimes a super simple solution is enough.
#5
07/13/2014 (3:22 pm)
Instant bookmark. I'm doing a bit of level work atm, but when I get back to my GUI I'll definitely give this a go. As a matter of fact, I have login, level selection, and game mode selection GUI's but on the 'character' tab it's empty atm waiting for something just like this! Thanks! I'll also upload a vid of that T3D drag-drop inventory for ya when I get to the GUI again Richard =)
#6
07/13/2014 (5:08 pm)
I should probably push my whole character model selection setup to github....
#7
07/13/2014 (5:37 pm)
@Richard Awesome, I'll have to look for that.
#8
Just a thought ... before making it part of any "Standard" build;
Would be cool if the character selection "list" was created dynamically -- rather than having it hard coded for specific models.
Then on a new build you just setup a couple actors (datablocks, etc) as possible characters and voila!
07/13/2014 (6:13 pm)
This is great, thanks Richard!Just a thought ... before making it part of any "Standard" build;
Would be cool if the character selection "list" was created dynamically -- rather than having it hard coded for specific models.
Then on a new build you just setup a couple actors (datablocks, etc) as possible characters and voila!
#9


I don't think there's any danger of this becoming any sort of standard. I wouldn't want that anyway - every game has different needs and this wouldn't fit every game.
07/13/2014 (7:12 pm)
It is dynamically created - by scanning a particular folder for the datablock files and then getting the names of the datablocks in those files.function PlayerDatablockList::loadData(%this, %path)
{
// find aiPlayer datablocks
%search = "art/datablocks/aiPlayer/*.cs";
// search for scripts and load them
for( %file = findFirstFile( %search ); %file !$= ""; %file = findNextFile( %search ) )
{
%this.readDatablockInfo(%file);
}
}The purpose of the PlayerDatablockList script object is just that - to find player datablocks and store a list of them, along with other information from them, without actually loading the datablocks. You then iterate through the list and generate the necessary GUI elements:function ChooseLevelDlg::onDialogPush(%this)
{
ChooseLevelWindow->clPlayerBox->PreviewScroll->PlayerPreviews.setVisible(false);
ChooseLevelWindow->clPlayerBox->PreviewScroll->PlayerPreviews.clear();
ChooseLevelWindow->clPlayerBox->PreviewScroll->PlayerPreviews.position = "0 0";
%count = $PlayerDatablocks.getCount();
for (%i = 0; %i < %count; %i++)
{
%this.addPlayerButton($PlayerDatablocks.getObject(%i));
}
ChooseLevelWindow->clPlayerBox->PreviewScroll->PlayerPreviews.refresh();
ChooseLevelWindow->clPlayerBox->PreviewScroll->PlayerPreviews.setVisible(true);
cldlgPlayerPreview.cameraRotation = "0 0 3.14";
}
function ChooseLevelDlg::addPlayerButton(%this, %data)
{
%button = new GuiButtonCtrl()
{
extent = "118 30";
text = %data.datablockName;
};
%command = "cldlgPlayerPreview.setModel("@%data@".shapeFileInfo);cldlgPlayerPreview.cameraRotation = "0 0 3.14";";
%command = %command @ "$Game::SelectedPlayerClass = "@%data.datablockName@";";
%button.command = %command;
%button.selectedDatablock = %data.datablockName;
ChooseLevelWindow->clPlayerBox->PreviewScroll->PlayerPreviews.add(%button);
}For those unfamiliar with it, the "->" operator lets you point to member objects ("child" objects) by internal name. Very handy.

I don't think there's any danger of this becoming any sort of standard. I wouldn't want that anyway - every game has different needs and this wouldn't fit every game.
#11
07/14/2014 (8:57 am)
I'll have to drop my other changes some time soon - the Twillex-driven rollouts are only partially ready. I'm going to add drag-drop to them so you can drag to them from other controls and rearrange the buttons within each rollout. I had to add an onResize() callback to GameTSCtrl to handle positioning the rollouts when the game was resized.
#12
07/14/2014 (4:06 pm)
Or you could move the exec() of the effected torquescript files (that declare your datablocks) out of server starting script code to when main menu is suppose to load instead. ;)
#13
My way is rarely the "best" way, but I usually try a few things before I settle on a solution.
07/14/2014 (5:44 pm)
I did that - very slow. UI is supposed to be responsive - if it takes 2 or 3 seconds for a simple dialog to load it's really annoying. And loading datablocks on the client isn't best practice. And you really don't need the whole datablock loaded for a simple preview of the model.My way is rarely the "best" way, but I usually try a few things before I settle on a solution.
#14
07/16/2014 (4:01 am)
cool stuff! thanks for sharing.
#15
I use to load and then unload datablocks in my game's "shop" i'm quite happy with that, but is always useful to see a more elegant way!
Thanks!
07/17/2014 (1:28 am)
Bookmarked :DI use to load and then unload datablocks in my game's "shop" i'm quite happy with that, but is always useful to see a more elegant way!
Thanks!
#16
07/19/2014 (10:18 am)
That's awesome Richard, but what happens if you only release .dso files. You can't read the files anymore ;)
#17
You can still use a text file with the relevant data extracted from the actual datablocks....
Or you could get TAML working and then use the binary TAML format. Added bonus - you could use a TAML visitor object to search even faster under the right conditions.
There might be a way to decode a .dso without actually executing it....
The main take-away is that you can read through text files and extract what you need. It is probably more useful for beginners than all of you old-timers.... ;p
07/25/2014 (7:21 am)
@Jeff -You can still use a text file with the relevant data extracted from the actual datablocks....
Or you could get TAML working and then use the binary TAML format. Added bonus - you could use a TAML visitor object to search even faster under the right conditions.
There might be a way to decode a .dso without actually executing it....
The main take-away is that you can read through text files and extract what you need. It is probably more useful for beginners than all of you old-timers.... ;p

Torque Owner Richard Ranft
Roostertail Games
function ChooseLevelDlgGoBtn::onMouseUp( %this ) { if ($Game::SelectedPlayerClass $= "") { // get first in container %btn = ChooseLevelWindow->clPlayerBox->PreviewScroll->PlayerPreviews.getObject(0); $Game::SelectedPlayerClass = %btn.selectedDatablock; } // So we can't fire the button when loading is in progress. if ( isObject( ServerGroup ) ) return; // Launch the chosen level with the editor open? if ( ChooseLevelDlg.launchInEditor ) { activatePackage( "BootEditor" ); ChooseLevelDlg.launchInEditor = false; StartLevel("", "SinglePlayer"); } else { StartLevel(); } }function JoinServerDlg::join(%this) { cancelServerQuery(); %index = JS_serverList.getSelectedId(); // The server info index is stored in the row along with the // rest of displayed info. if( setServerInfo( %index ) ) { Canvas.setContent("LoadingGui"); LoadingProgress.setValue(1); LoadingProgressTxt.setValue("WAITING FOR SERVER"); Canvas.repaint(); %conn = new GameConnection(ServerConnection); %conn.setConnectArgs($pref::Player::Name, $Game::SelectedPlayerClass); %conn.setJoinPassword($Client::Password); %conn.connect($ServerInfo::Address); } }Then, in your game you have to catch the $Game::SelectedPlayerClass variable when you hit your game's <game>::spawnPlayer() function.