Changing the hard coded paths to script driven ones
by Gregory "Centove" McLean · 11/30/2006 (1:20 pm) · 3 comments
First up, terrrain data.
In the engine/terrain/terrData.cc we find these hard coded paths:
Now lets go about changing this to get this information from the mission file, to do this we will be adding a new datablock member in the TerrainBlock.
Open up the terrData.h file and find the TerrainBlock class we will want to add a filename member to instruct the engine where to find this texture.
We want to change this chunk of code from:
Now over to the terrData.cc file we need a few changes there to make this active.
First we want to initialize the new member, so in the constructor we add the new member. New code is in in bold.
We change the onAdd method to use this member, if present, else we fall back to the original hardcoded default and issue a warning that we are doing so.
Now we want it to get loaded from the scripts when we execute/load the mission, so off to the initPersistFields method. It really don't matter where in here, I put it in the media group as thats what it is.
Now the terrain block is a networked object so we want to push this information out to the clients. Since this is in the packUpdate/unpackUpdate the only thing to pay attention to is that they must be in the same order in both functions or odd things will happen. We also want to only send this out on initial load of the datablock so we stay in the Init block.
This will get the path written to the clients.
Now we want to get it from the server on the client, again stay in the init block and put it in the same place as the pack function. In our case we write it just after the empty squares bit so we want to read it after that in the unpacking.
Now rebuild the engine and while its churning away on that lets add the texture to the mission file. If you don't nothing should explode as it falls back to the hard coded (and hopefully working) default.
open up the mission file in question, I'm using the stronghold mission from the starter.fps, find the TerrainBlock definition:
If you notice we are specifying the same as the hard coded default, but the point is that we can now specify it in the terrain block and it can be anywhere the script engine can see it. Hence we can re-arrange things however we like and it won't matter as we only have to change the path in one place.
Now launch the game and start the mission. It should work just as before. Move the texture somewhere else and update the path then start the mission again. Once again it should work as it did before.
Now we will tackle the other sticky issue with the terrain textures being saved as absolute paths in the terrain file. For this we will assume that you keep your mission terrain files all in the same directory, but fear not we preserve the original behavior with some slight modifications.
What we are going to do here is establish a Material path member using the same procedures as above. So we add the mMaterialPath member to the terrain block class. So in the engine/terrain/terrData.h file in the TerrainBlock class add a member for it, it don't really matter where, I stuck it above the mTerrFileName member.
Initialize it in the constructor.
Add it to the persistFields block so that it gets loaded from the scriptengine, I add it to the media group as that makes the most sense.
Next we want to add it to the networking code, keep in mind it has to be in the same order in both the sending and receiving.
Send
Receive
Now lets make use of it, we need to find where its loading textures and alter that. In the TerrainBlock::initMMXBlender is where this happens. What we want to do is if the mMaterialPath is set, take the mMaterialFileName entry, lop off the path so we get just the texture name, glue our custom path on and try and load that. If that fails, try and load the texture from the saved path. If that fails, look in the same directory as the terrain file. If that fails try a default texture from the terrain file path. If we still can't get a texture, make a white 256x256 texture and use that.
Lets see what that looks like in code, we are in the terrData.cc file and in the TerrainBlock::initMMXBlender() method. We want to change this chunk of code from this:
To This:
Rebuild your project, and while thats happening stick the new member in the terrainblock definition in the mission file, once again I'm using the stronghold mission file.
Launch the game and check that everything works as before, now you can move stuff around and only have to change the paths to things in one location.
The other two instances of hard coded paths that this resource deals with are fairly trivial to address. First up is the GuiInspector.
On the dynamic fields, there is a delete button next to the field to delete that field, its a BitmapButton so it has a bitmap which is hard coded. In the constructRenameControl method we find this:
We fix this instance by creating a new profile that has that path in it over in the scripts arena. We change that above call to this:
Then we add a new profile in the defaultProfiles.cs file (or where ever you define all your default profiles)
Rebuild your project, launch it and try one of the editors (gui editor comes to mind) and you'll see the inspector works as before. However if you want to move the images around now you can and avoid the crashing when it can't find an image it wants.
The next two are the DirectoryTree and TreeView controls, both have a hard coded icon list that they use, lets move this stuff out to the scripting side of things where the rest of this stuff is handled. Since they are both controls, we will need to add a new member to the gui profiles. This is done in the engine/gui/core/guiTypes files.
Lets add the member:
guiTypes.h
Initialize it.
guiTypes.cc
Add it to the persistent fields.
Make use of the new fields:
engine/gui/controls/guiDirectoryTreeCtrl.cc
guiTreeViewCtrl.cc
Rebuild your project, while thats rebuilding, add the members to your profiles:
defaultProfiles.cs (or where ever you do this sort of thing)
Launch your game and pull up one of the editors, nothing should have changed, even if you didn't add the members to the profiles. However its now possible to add them to the profiles and not worry about paths lurking in the c++ code to trip you up.
The remaining two instances are in the interiors.cc and fxSunlight.cc. I'm sure the same techniques can be used to push those settings out into the scripting arena where the rest of the stuff is. As I don't use those two items that much I haven't really looked into how/where to do this.
In the engine/terrain/terrData.cc we find these hard coded paths:
bool TerrainBlock::onAdd()
{
if(!Parent::onAdd())
return false;
{...}
if (!buildMaterialMap())
return false;
mTextureCallbackKey = TextureManager::registerEventCallback(terrainTextureEventCB, this);
mDynLightTexture = TextureHandle("common/lighting/lightFalloffMono", BitmapTexture, true);
{...}Now lets go about changing this to get this information from the mission file, to do this we will be adding a new datablock member in the TerrainBlock.
Open up the terrData.h file and find the TerrainBlock class we will want to add a filename member to instruct the engine where to find this texture.
We want to change this chunk of code from:
StringTableEntry *mMaterialFileName; ///< Array from the file. TextureHandle mDynLightTexture; U8 *mBaseMaterialMap; Material *materialMap;To:
StringTableEntry *mMaterialFileName; ///< Array from the file. StringTableEntry mDynLightTextureFileName; ///< Dynamic Light texture filename. TextureHandle mDynLightTexture; U8 *mBaseMaterialMap; Material *materialMap;
Now over to the terrData.cc file we need a few changes there to make this active.
First we want to initialize the new member, so in the constructor we add the new member. New code is in in bold.
TerrainBlock::TerrainBlock()
{
{...}
mBaseMaterialMap = NULL;
mMaterialFileName = NULL;
[b] mDynLightTextureFileName = NULL;[/b]
mTypeMask = TerrainObjectType | StaticObjectType | StaticRenderedObjectType;
mNetFlags.set(Ghostable | ScopeAlways);
mCollideEmpty = false;
{...}
}We change the onAdd method to use this member, if present, else we fall back to the original hardcoded default and issue a warning that we are doing so.
bool TerrainBlock::onAdd()
{
if(!Parent::onAdd())
return false;
{...}
if (!buildMaterialMap())
return false;
mTextureCallbackKey = TextureManager::registerEventCallback(terrainTextureEventCB, this);
[b]
if (dStrlen(mDynLightTextureFileName) >= 1) {
mDynLightTexture = TextureHandle(mDynLightTextureFileName, BitmapTexture, true);
}
if (!mDynLightTexture) {
Con::warnf("TerrainBlock: Loading hardcoded dynamic light texture.");
mDynLightTexture = TextureHandle("client/lighting/lightFalloffMono", BitmapTexture, true);
}
[/b]
if (dglDoesSupportVertexBuffer())
mVertexBuffer = glAllocateVertexBufferEXT(VertexBufferSize,GL_V12MTVFMT_EXT,true);
elseNow we want it to get loaded from the scripts when we execute/load the mission, so off to the initPersistFields method. It really don't matter where in here, I put it in the media group as thats what it is.
void TerrainBlock::initPersistFields()
{
Parent::initPersistFields();
addGroup("Media");
{...}
[b]
addField("DynLightTextureFileName", TypeFilename, Offset(mDynLightTextureFileName, TerrainBlock));
[/b]
endGroup{"Media"};
{...}Now the terrain block is a networked object so we want to push this information out to the clients. Since this is in the packUpdate/unpackUpdate the only thing to pay attention to is that they must be in the same order in both functions or odd things will happen. We also want to only send this out on initial load of the datablock so we stay in the Init block.
This will get the path written to the clients.
U32 TerrainBlock::packUpdate(NetConnection *, U32 mask, BitStream *stream)
{
if(stream->writeFlag(mask & InitMask))
{
{...}
// write out the empty rle vector
stream->write(mEmptySquareRuns.size());
for(U32 i = 0; i < mEmptySquareRuns.size(); i++)
stream->write(mEmptySquareRuns[i]);
[b]
stream->writeString(mDynLightTextureFileName);
[/b]
}
else // normal update
{...}
}Now we want to get it from the server on the client, again stay in the init block and put it in the same place as the pack function. In our case we write it just after the empty squares bit so we want to read it after that in the unpacking.
void TerrainBlock::unpackUpdate(NetConnection *, BitStream *stream)
{
if(stream->readFlag()) // init
{
{...}
// read in the empty rle
U32 size;
stream->read(&size);
mEmptySquareRuns.setSize(size);
for(U32 i = 0; i < size; i++)
stream->read(&mEmptySquareRuns[i]);
[b]
mDynLightTextureFileName = stream->readSTString();
[/b]
{...}
}Now rebuild the engine and while its churning away on that lets add the texture to the mission file. If you don't nothing should explode as it falls back to the hard coded (and hopefully working) default.
open up the mission file in question, I'm using the stronghold mission from the starter.fps, find the TerrainBlock definition:
new TerrainBlock(Terrain) {
canSaveDynamicFields = "1";
rotation = "1 0 0 0";
scale = "1 1 1";
detailTexture = "~/data/terrains/details/snowdetail04.png";
terrainFile = "./stronghold.ter";
squareSize = "8";
bumpScale = "8";
bumpOffset = "0.01";
zeroBumpScale = "8";
tile = "1";
locked = "true";
position = "-1024 -1024 0";
};Now we want to add the "DynLightTextureFileName" setting to get things loaded from where we want.new TerrainBlock(Terrain) {
canSaveDynamicFields = "1";
rotation = "1 0 0 0";
scale = "1 1 1";
detailTexture = "~/data/terrains/details/snowdetail04.png";
[b]
DynLightTextureFilename = "common/lighting/lightFalloffMono";
[/b]
terrainFile = "./stronghold.ter";
squareSize = "8";
bumpScale = "8";
bumpOffset = "0.01";
zeroBumpScale = "8";
tile = "1";
locked = "true";
position = "-1024 -1024 0";
};If you notice we are specifying the same as the hard coded default, but the point is that we can now specify it in the terrain block and it can be anywhere the script engine can see it. Hence we can re-arrange things however we like and it won't matter as we only have to change the path in one place.
Now launch the game and start the mission. It should work just as before. Move the texture somewhere else and update the path then start the mission again. Once again it should work as it did before.
Now we will tackle the other sticky issue with the terrain textures being saved as absolute paths in the terrain file. For this we will assume that you keep your mission terrain files all in the same directory, but fear not we preserve the original behavior with some slight modifications.
What we are going to do here is establish a Material path member using the same procedures as above. So we add the mMaterialPath member to the terrain block class. So in the engine/terrain/terrData.h file in the TerrainBlock class add a member for it, it don't really matter where, I stuck it above the mTerrFileName member.
StringTableEntry mMaterialPath; ///< Path to the textures for the terrain
Initialize it in the constructor.
mMaterialPath = NULL;
Add it to the persistFields block so that it gets loaded from the scriptengine, I add it to the media group as that makes the most sense.
addField("MaterialPath", TypeFilename, Offset(mMaterialPath, TerrainBlock));Next we want to add it to the networking code, keep in mind it has to be in the same order in both the sending and receiving.
Send
void terrainBlock::packUpdate(NetConnection *, U32 mask, BitStream *stream)
{
...
stream->write(mEmptySquareRuns.size());
for(U32 i = 0; i < mEmptySquareRuns.size(); i++)
stream->write(mEmptySquareRuns[i]);
stream->writeString(mDynLightTextureFileName);
stream->writeString(mMaterialPath);
...
}Receive
void TerrainBlock::unpackUpdate(NetConnection *, BitStream *stream)
{
...
// read in the empty rle
U32 size;
stream->read(&size);
mEmptySquareRuns.setSize(size);
for(U32 i = 0; i < size; i++)
stream->read(&mEmptySquareRuns[i]);
mDynLightTextureFileName = stream->readSTString();
mMaterialPath = stream->readSTString();
...
}Now lets make use of it, we need to find where its loading textures and alter that. In the TerrainBlock::initMMXBlender is where this happens. What we want to do is if the mMaterialPath is set, take the mMaterialFileName entry, lop off the path so we get just the texture name, glue our custom path on and try and load that. If that fails, try and load the texture from the saved path. If that fails, look in the same directory as the terrain file. If that fails try a default texture from the terrain file path. If we still can't get a texture, make a white 256x256 texture and use that.
Lets see what that looks like in code, we are in the terrData.cc file and in the TerrainBlock::initMMXBlender() method. We want to change this chunk of code from this:
bool matsValid = true;
for(i = 0; i < validMaterials; i++)
{
AssertFatal(mMaterialFileName[i] && *mMaterialFileName[i], "Error, something wacky here");
StringTableEntry fn = mMaterialFileName[i];
GBitmap* pBitmap = TextureManager::loadBitmapInstance(fn);
if (!pBitmap)
{
dStrcpyl(fileBuf, sizeof(fileBuf), mFile.getFilePath(), "/", fn, NULL);
pBitmap = TextureManager::loadBitmapInstance(fileBuf);
if(pBitmap)
mMaterialFileName[i] = StringTable->insert(fileBuf, true);
if(!pBitmap)
{
// Try the default..
Con::errorf("Missing terrain texture: %s",fileBuf);
matsValid = false;
dStrcpyl(fileBuf, sizeof(fileBuf), mFile.getFilePath(), "/","default",NULL);
pBitmap = TextureManager::loadBitmapInstance(fileBuf);
if(!pBitmap)
pBitmap = new GBitmap(256,256);
}
}
pBitmap->extrudeMipLevels();To This:
bool matsValid = true;
for(i = 0; i < validMaterials; i++)
{
AssertFatal(mMaterialFileName[i] && *mMaterialFileName[i], "Error, something wacky here");
StringTableEntry fn = mMaterialFileName[i];
GBitmap *pBitmap = NULL;
// If the terrainblock in the mission file specifies a materialpath we
// will attempt to use that, and load our materials from there.
if (dStrlen(mMaterialPath) >= 1)
{
// we got a path, build a path that hopefully the texture manager
// can use.
dStrcpyl(fileBuf, sizeof(fileBuf), mMaterialPath, dStrrchr(fn,'/'), NULL);
pBitmap = TextureManager::loadBitmapInstance(fileBuf);
if (pBitmap)
mMaterialFileName[i] = StringTable->insert(fileBuf, true);
}
// If that failed above, try the path in the terrain file.
if (!pBitmap)
pBitmap = TextureManager::loadBitmapInstance(fn);
// If that failed, try the path to the missionfile/<texture name>
if (!pBitmap)
{
dStrcpyl(fileBuf, sizeof(fileBuf), mFile.getFilePath(),dStrrchr(fn, '/'), NULL);
pBitmap = TextureManager::loadBitmapInstance(fileBuf);
if(pBitmap)
mMaterialFileName[i] = StringTable->insert(fileBuf, true);
// If all the above fails, try a default texture.
if(!pBitmap)
{
// Try the default..
Con::errorf("Missing terrain texture: %s, trying default",fileBuf);
matsValid = false;
dStrcpyl(fileBuf, sizeof(fileBuf), mFile.getFilePath(), "/","default",NULL);
pBitmap = TextureManager::loadBitmapInstance(fileBuf);
if(!pBitmap)
pBitmap = new GBitmap(256,256);
}
}
pBitmap->extrudeMipLevels();Rebuild your project, and while thats happening stick the new member in the terrainblock definition in the mission file, once again I'm using the stronghold mission file.
new TerrainBlock(Terrain) {
canSaveDynamicFields = "1";
rotation = "1 0 0 0";
scale = "1 1 1";
detailTexture = "~/data/terrains/details/snowdetail04.png";
DynLightTextureFilename = "client/lighting/lightFalloffMono";
[b]
MaterialPath = "~/data/terrains/orctown/";
[/b]
terrainFile = "./stronghold.ter";
squareSize = "8";
bumpScale = "8";
bumpOffset = "0.01";
zeroBumpScale = "8";
tile = "1";
locked = "true";
position = "-1024 -1024 0";
};Launch the game and check that everything works as before, now you can move stuff around and only have to change the paths to things in one location.
The other two instances of hard coded paths that this resource deals with are fairly trivial to address. First up is the GuiInspector.
On the dynamic fields, there is a delete button next to the field to delete that field, its a BitmapButton so it has a bitmap which is hard coded. In the constructRenameControl method we find this:
GuiControl* GuiInspectorDynamicField::constructRenameControl()
{...}
delButt->setField("Bitmap", "common/ui/inspector_delete");
{...}We fix this instance by creating a new profile that has that path in it over in the scripts arena. We change that above call to this:
dSprintf(szBuffer, 512, "%d.%s = \"\";%d.inspect(%d);", mTarget->getId(), getFieldName(), mParent->getContentCtrl()->getId(), mTarget->getId());
delButt->setField("profile", "GuiInspectorDeleteButtonProfile");Then we add a new profile in the defaultProfiles.cs file (or where ever you define all your default profiles)
if (!isObject(GuiInspectorDeleteButtonProfile))
new GuiControlProfile(GuiInspectorDeleteButtonProfile : GuiButtonProfile)
{
bitmap = "common/ui/inspector_delete";
};Rebuild your project, launch it and try one of the editors (gui editor comes to mind) and you'll see the inspector works as before. However if you want to move the images around now you can and avoid the crashing when it can't find an image it wants.
The next two are the DirectoryTree and TreeView controls, both have a hard coded icon list that they use, lets move this stuff out to the scripting side of things where the rest of this stuff is handled. Since they are both controls, we will need to add a new member to the gui profiles. This is done in the engine/gui/core/guiTypes files.
Lets add the member:
guiTypes.h
class GuiControlProfile : public SimObject
{
{...}
// bitmap members
StringTableEntry mBitmapName; ///< Bitmap file name for the bitmap of the control
[b]
StringTableEntry mIconList; ///< A colon ':' seperated list of icons for controls that have that functionality.
[/b]
{...}
}Initialize it.
guiTypes.cc
GuiControlProfile::GuiControlProfile(void) :
mFontColor(mFontColors[BaseColor]),
mFontColorHL(mFontColors[ColorHL]),
mFontColorNA(mFontColors[ColorNA]),
mFontColorSEL(mFontColors[ColorSEL])
{
{...}
mBitmapName = NULL;
[b]
mIconList = NULL;
[/b]
{...}
}Add it to the persistent fields.
void GuiControlProfile::initPersistFields()
{
Parent::initPersistFields();
...
addField("bitmap", TypeFilename, Offset(mBitmapName, GuiControlProfile));
[b]
addField("icons", TypeString, Offset(mIconList, GuiControlProfile));
[/b]
...
}Make use of the new fields:
engine/gui/controls/guiDirectoryTreeCtrl.cc
bool GuiDirectoryTreeCtrl::onAdd()
{
if( !Parent::onAdd() )
return false;
// Specify our icons
if (mProfile->mIconList) {
buildIconTable(mProfile->mIconList);
} else {
Con::warnf("GuiDirectoryTreeCtrl: Using hardcoded icon defaults.");
buildIconTable(NULL);
}
return true;
}guiTreeViewCtrl.cc
bool GuiTreeViewCtrl::onWake()
{
if(!Parent::onWake() || !mProfile->constructBitmapArray())
return false;
// If destroy on sleep, then we have to give things a chance to rebuild.
if(mDestroyOnSleep)
{
destroyTree();
Con::executef(this, 1, "onWake");
// (Re)build our icon table.
const char * res = Con::executef(this, 1, "onDefineIcons");
// If no icons were defined in script then use defaults.
if(!(dAtob(res)))
{
if (mProfile->mIconList) {
buildIconTable(mProfile->mIconList);
} else {
Con::warnf("GuiTreeViewCtrl: Using hard coded icon defaults.");
buildIconTable(NULL);
}
}
}And in the inspectObject method as well.void GuiTreeViewCtrl::inspectObject(SimObject *obj, bool okToEdit)
{
destroyTree();
mFlags.set(IsEditable, okToEdit);
//build our icon table
const char * res = Con::executef(this, 1, "onDefineIcons");
if(!(dAtob(res)))
{
[b]
if (mProfile->mIconList) {
buildIconTable(mProfile->mIconList);
} else {
Con::warnf("GuiTreeViewCtrl: Using hard coded icon defaults.");
buildIconTable(NULL);
}
[/b]
}
addInspectorDataItem(NULL, obj);
}Rebuild your project, while thats rebuilding, add the members to your profiles:
defaultProfiles.cs (or where ever you do this sort of thing)
if(!isObject(GuiTreeViewProfile)) new GuiControlProfile (GuiTreeViewProfile : GuiBaseTreeViewProfile)
{
fontColorSEL= "250 250 250";
fillColorHL = "0 60 150";
fontColorNA = "240 240 240";
bitmap = "creator/ui/shll_treeView";
icons = "creator/ui/default:creator/ui/simgroup:creator/ui/simgroup_closed:creator/ui/simgroup_selected:creator/ui/simgroup_selected_closed:creator/ui/audio:creator/ui/camera:creator/ui/fxfoliage:creator/ui/fxlight:creator/ui/fxshapereplicator:creator/ui/fxsunlight:creator/ui/hidden:creator/ui/interior:creator/ui/lightning:creator/ui/shll_icon_passworded_hi:creator/ui/shll_icon_passworded:creator/ui/mission_area:creator/ui/particle:creator/ui/path:creator/ui/pathmarker:creator/ui/physical_area:creator/ui/precipitation:creator/ui/shape:creator/ui/sky:creator/ui/static_shape:creator/ui/sun:creator/ui/terrain:creator/ui/trigger:creator/ui/water:creator/ui/default";
};
if(!isObject(GuiDirectoryTreeProfile)) new GuiControlProfile ( GuiDirectoryTreeProfile : GuiTreeViewProfile )
{
fontColor = "40 40 40";
fontColorSEL= "250 250 250 175";
fillColorHL = "0 60 150";
fontColorNA = "240 240 240";
bitmap = "creator/ui/shll_treeView";
icons = "creator/ui/folder:creator/ui/folder:creator/ui/folder_closed";
fontType = "Arial";
fontSize = 14;
};Launch your game and pull up one of the editors, nothing should have changed, even if you didn't add the members to the profiles. However its now possible to add them to the profiles and not worry about paths lurking in the c++ code to trip you up.
The remaining two instances are in the interiors.cc and fxSunlight.cc. I'm sure the same techniques can be used to push those settings out into the scripting arena where the rest of the stuff is. As I don't use those two items that much I haven't really looked into how/where to do this.
#2
12/14/2006 (8:31 pm)
Awesome resource Gregory. I got the terrain portion in without a problem. However, and this may be due to how tired I am, I can't locate the places I should make the changes for interiors. I'm looking through engine\interior\interior.cc and engine\interior\interiorInstance.cc. Have any recommendations on which functions I should be looking at?
#3
I don't use interiors that much so I'm not familiar with the load sequence on them, I imagine somewhere in there it loads the datablock from the mission file, and thats where you would want to put the new members which will tell it where to find the textures. So I would look for the persist fields stuff that relates to the interior and add the members there. Then find where its loading the hard coded texture and use the new member to load the correct texture.
12/15/2006 (6:42 am)
@Michael:I don't use interiors that much so I'm not familiar with the load sequence on them, I imagine somewhere in there it loads the datablock from the mission file, and thats where you would want to put the new members which will tell it where to find the textures. So I would look for the persist fields stuff that relates to the interior and add the members there. Then find where its loading the hard coded texture and use the new member to load the correct texture.

Torque 3D Owner Caylo Gypsyblood