Game Development Community

dev|Pro Game Development Curriculum

TSE Interior Decals

by Erik Madison · 02/08/2005 (3:34 pm) · 15 comments

This code uses the GameEntity entity to place and pass decals on/in your interior files (difs)
Basically, you're telling the entity that you will be creating an fxInteriorDecal class, and tell
it which dataBlock to use. In our case, for decals, we don't need a datablock, so we've created
a blank, generic datablock (shown below). In QuArK at least, you'll have an arrow thingy on your
entity icon, which you point at the surface getting the decal. This is used mostly because you may
have some tight geometry going on, and the code needs to know _exactly_ which surface you mean to
cover. You will also add a 'specific' to the entity (generic fielddata) telling it which texture
to use.

Keep an eye on the below version info, as I probably will update this fairly often.
Version 0.95
Updated release, I did forget a function
Known bugs: Decal occasionally flickers, getting somewhat brighter. Probably an error in how
I'm calling the GFX stuff.
Texture name must be fully qualified. A waste of bandwidth, and the cause of carpal.

We begin with ver 0.9 in case I forgot something (rushing out the door). Those who expect
plug/play, are better off waiting until at least 1.0

You will need to recompile map2dif, as I changed a few things it uses via engine.lib This WILL NOT
break existing interiors, they will load just fine along with your new graffiti covered urban hells.
Placement is not all that intuitive. You get a small icon with an arrow, and you cannot see your decal
until it is ingame. I don't know Delphi enough to patch up QuArK to show it better, but it's not to awful
difficult once you get used to it. HalfLife 1 wasnt any better at this either :)

InteriorInstance.cpp

At the top...
[b]#include "game/fx/fxInteriorDecal.h"[/b]
At the end of onAdd()
addToScene();
   [b]addChildren();[/b]
   return true;
Inside PackUpdate()
if (stream->writeFlag((mask & TransformMask) != 0)) {
         mathWrite(*stream, mObjToWorld);
         mathWrite(*stream, mObjScale);
		     [b]updateChildren();  // done server side. Client will redo its data later[/b]
      }
New functions, add anywhere
Note: addChildren is entirely based on door code, which will hopefully soon be
re-released in a more generic, useful and working manner.

[b] void InteriorInstance::addChildren()
 {

	if (bool(mInteriorRes) == false)
		return;
 
	if (isGhost())  // only add on the server!
		return;

	// First thing to do, add a group with our name
    SimObject* myObj = Sim::findObject("MissionGroup");
	SimGroup* myGroup = myObj->getGroup();
	
	// Load all the interior game objects...
	U32 i;
	for(i = 0; i < mInteriorRes->getNumGameEntities(); i++)
	{
		ItrGameEntity *pEntity = mInteriorRes->getGameEntity(i);
		ConsoleObject *obj = ConsoleObject::create(pEntity->mGameClass);
		GameBase *gb = dynamic_cast<GameBase*>(obj);

		if(!gb)
		{
			Con::errorf("Invalid game class for entity: %s", pEntity->mGameClass);
			delete obj;
			continue;
		} 
		gb->setField("dataBlock", pEntity->mDataBlock);
		gb->setModStaticFields(true);
		for(U32 j = 0; j < pEntity->mDictionary.size(); j++)
		{
			gb->setDataField(StringTable->insert(pEntity->mDictionary[j].name),
				NULL, pEntity->mDictionary[j].value); 
		}
		gb->setModStaticFields(false);
		Point3F origin = pEntity->mPos; // This is the offset from the parent

		origin.convolve(mObjScale);
		getTransform().mulP(origin);
		MatrixF xform(true);
		xform.setColumn(3, origin);
		gb->setTransform(xform);
		
		if(!gb->registerObject())
		{
			Con::errorf("Failed to register pEntity: %s: %s", pEntity->mGameClass, pEntity->mDataBlock);
			delete gb;
			continue;
		} 
		// Save the server id, for later updating
		pEntity->mId = gb->getId();

		myGroup->addObject(gb);
	}
}

void InteriorInstance::updateChildren()
{
	U32 i;

	// Load all the interior game objects...
	for(i = 0; i < mInteriorRes->getNumGameEntities(); i++)
	{
		ItrGameEntity *pEntity = mInteriorRes->getGameEntity(i);
		// This finds and works on the server object
		GameBase *obj = dynamic_cast<GameBase*>(Sim::findObject(pEntity->mId));
		if (!obj)
		{
			Con::errorf("We seem to have lost a child! (InteriorInstance::updateChildren())");
			return;
		}

		Point3F origin = pEntity->mPos;
		Point3F normal(0, 1, 0);
		origin.convolve(mObjScale);
		getTransform().mulP(origin);
		MatrixF xform(true);
		xform.setColumn(3, origin);
		obj->setTransform(xform);
		// Here we let the client know we're changing things
		obj->inspectPostApply();
	}

}[/b]

InteriorInstance.h
Somewhere...
protected:
   bool onAdd();
   void onRemove();
[b]   void addChildren();
   void updateChildren();
[/b]
   void inspectPreApply();
   void inspectPostApply();

InteriorResObjects.cpp
Somewhere...
ItrGameEntity::ItrGameEntity()
{
[b]   mId = 0;[/b]
   mDataBlock = "";
   mGameClass = "";
   mPos.set(0, 0, 0);
}

Read and write functions work better if we reverse a few things
bool ItrGameEntity::read(Stream& stream)
{
   mDataBlock = stream.readSTString();
   mGameClass = stream.readSTString();
[b]   mDictionary.read(stream);
   mathRead(stream, &mPos);
[/b]
   return (stream.getStatus() == Stream::Ok);
}

bool ItrGameEntity::write(Stream& stream) const
{
   stream.writeString(mDataBlock);
   stream.writeString(mGameClass);
[b]   mDictionary.write(stream);
   mathWrite(stream, mPos);
[/b]
   return (stream.getStatus() == Stream::Ok);
}
InteriorResObjects.h
Changing the struct size, I need more info...
struct InteriorDictEntry
{
   char name[32];
   char value[[b]128[b/]];
};

Near the end...
class ItrGameEntity
{
   public:
	    [b]U32               mId;[/b]
      StringTableEntry  mDataBlock;
      StringTableEntry  mGameClass;
      Point3F           mPos;
      InteriorDict mDictionary;

Small change in map2dif,
entitytypes.cpp
else if (pToker->tokenICmp("game_class")) {
         pToker->advanceToken(false, true);
         AssertISV(dStrlen(pToker->getToken()) < 256, avar("Error: %s is an invalid target name.  Must be less than 256", pToker->getToken()));
         dStrcpy(mGameClass, pToker->getToken());
      }
[b]     else if (pToker->tokenICmp("dataBlock")) {
         pToker->advanceToken(false, true);
         AssertISV(dStrlen(pToker->getToken()) < 256, avar("Error: %s is an invalid target name.  Must be less than 256", pToker->getToken()));
         dStrcpy(mDatablock, pToker->getToken());
      }[/b]
      else {
2 new files to add, I placed them in engine/game/fx/
Note: Not everything here is pretty, but I was really extending my skills here.
I tried to use the new GFX calls whenever I could figure them out, unfortunately
I had to drop back to PrimBuilder for most of the work. As I learn and/or am aided by
my betters, I will keep this updated with proper TSE code.

fxInteriorDecal.cpp
[b]
//-----------------------------------------------------------------------------
// Torque Game Engine
// This file is based completely on fxRenderObject, changed only as/when I needed
// to. 
// Written by Melvyn May, Started on 9th September 2002.
//
// "My code is written for the Torque community, so do your worst with it,
//	just don't rip-it-off and call it your own without even thanking me".
//
//	- Melv.
//
//-----------------------------------------------------------------------------

#include "console/consoleTypes.h"
#include "core/bitStream.h"
#include "math/mathIO.h"
#include "game/gameConnection.h"
#include "console/simBase.h"
#include "sceneGraph/sceneGraph.h"
#include "sceneGraph/sgUtil.h"
#include "fxInteriorDecal.h"
#include "gfx/primBuilder.h"


//----------------------------------------------------------------------------

IMPLEMENT_CO_DATABLOCK_V1(fxInteriorDecalData);

fxInteriorDecalData::fxInteriorDecalData()
{
   mTextureName =  StringTable->insert("");
}

bool fxInteriorDecalData::onAdd()
{
   if (!Parent::onAdd())
      return false;

   return true;
}

void fxInteriorDecalData::initPersistFields()
{
   Parent::initPersistFields();

   addField( "Texture",		TypeFilename,	Offset( mTextureName,		fxInteriorDecalData ) );
}


//--------------------------------------------------------------------------
void fxInteriorDecalData::packData(BitStream* stream)
{
   Parent::packData(stream);
   stream->writeString(mTextureName);
}

void fxInteriorDecalData::unpackData(BitStream* stream)
{
   Parent::unpackData(stream);
   mTextureName = StringTable->insert(stream->readSTString());
}


//------------------------------------------------------------------------------

IMPLEMENT_CO_NETOBJECT_V1(fxInteriorDecal);


//------------------------------------------------------------------------------
// Class: fxInteriorDecal
//------------------------------------------------------------------------------

fxInteriorDecal::fxInteriorDecal()
{
	// Setup NetObject.
	mTypeMask |= StaticRenderedObjectType;
	mNetFlags.set(Ghostable);

	// Texture Handle.
	mTextureHandle = NULL;
	// Texture Name.
	mTextureName = StringTable->insert("");

	mAngle = 0;

	mDatablock = NULL;

}

//------------------------------------------------------------------------------

fxInteriorDecal::~fxInteriorDecal()
{
}

//------------------------------------------------------------------------------

bool fxInteriorDecal::onAdd()
{
	if(!Parent::onAdd()) return(false);

	addToScene();

	// Set initial bounding box.
	//
	// NOTE:-	Set this box to completely encapsulate your object.
	//			You must reset the world box and set the render transform
	//			after changing this.
	mObjBox.min.set( -1, -0.1f, -1 );
	mObjBox.max.set(  1, +0.1f,  1 );
	// Reset the World Box.
	resetWorldBox();
	// Set the Render Transform.
	setRenderTransform(mObjToWorld);

	setViewPosition();

	return(true);
}

//------------------------------------------------------------------------------

void fxInteriorDecal::setViewPosition()
{
   // Find the wall, snap to it	
  //  Not pretty, but I know of no other way. Even this, I needed forum help with. Suggestions?
	Point3F normal(0, 0, 0);
	F32 nAngle = dAtof(mAngle);
	if (nAngle == -1)
		normal.z = 1;
	else if (nAngle == -2)
		normal.z = -1;
	else {
		F32 theta = mDegToRad(nAngle);
		normal.x = 1 * cos(theta);
		normal.y = 1 * sin(theta);
	}

   Point3F startPos = getPosition();
   Point3F endPos = startPos + normal;

   RayInfo rayInfo;
   Point3F origin = getPosition();

   if (getContainer())
	   if(getContainer()->castRay( startPos, endPos, InteriorObjectType, &rayInfo ) )
       {	   
	       normal = rayInfo.normal;
	       origin = rayInfo.point;
       } 

   // setup texture verts
   Point3F vecX, vecY;
   if(mFabs(normal.z) > 0.9f)
      mCross(normal, Point3F(0.0f, 1.0f, 0.0f), &vecX);
   else
      mCross(normal, Point3F(0.0f, 0.0f, 1.0f), &vecX);

   mCross(vecX, normal, &vecY);

   normal.normalizeSafe();
   Point3F position = Point3F(origin.x + (normal.x * 0.008), origin.y + (normal.y * 0.008), origin.z + (normal.z * 0.008));

   vecX.normalizeSafe();
   vecY.normalizeSafe();

   vecX *= 1/*decalData->sizeX*/;
   vecY *= 1/*decalData->sizeY*/;

   point[0] = position + vecX + vecY;
   point[1] = position + vecX - vecY;
   point[2] = position - vecX - vecY;
   point[3] = position - vecX + vecY;
}
void fxInteriorDecal::onRemove()
{
	// Remove from Scene.
	removeFromScene();

	// Do Parent.
	Parent::onRemove();
}

//------------------------------------------------------------------------------

void fxInteriorDecal::inspectPostApply()
{
	// Set Parent
	Parent::inspectPostApply();

	// Set Mask
	setMaskBits(DataMask);
}

//------------------------------------------------------------------------------

bool fxInteriorDecal::prepRenderImage(	SceneState* state, const U32 stateKey, const U32 startZone,
										const bool modifyBaseZoneState)
{
	// Return if last state.
	if (isLastState(state, stateKey)) return false;
	// Set Last State.
	setLastState(state, stateKey);

   // Is Object Rendered?
   if (state->isObjectRendered(this))
   {	   
		// Yes, so get a SceneRenderImage.
		SceneRenderImage* image = new SceneRenderImage;
		// Populate it.
		image->obj = this;
		image->isTranslucent = true;
		image->sortType = SceneRenderImage::BeginSort; //::normal

		// Insert it into the scene images.
		state->insertRenderImage(image);
   }

   return false;
}

//------------------------------------------------------------------------------

void fxInteriorDecal::renderObject(SceneState* state, SceneRenderImage*)
{
    GFX->setBaseRenderState();

    // set render states
    GFX->setTextureStageColorOp( 0, GFXTOPModulate );
    GFX->setTextureStageColorOp( 1, GFXTOPDisable );
    GFX->setTexture( 0, mTextureHandle );
    GFX->setZWriteEnable( false );

    // set up register combiners
    GFX->setTextureStageAlphaOp( 0, GFXTOPModulate );
    GFX->setTextureStageAlphaOp( 1, GFXTOPDisable );
    GFX->setTextureStageAlphaArg1( 0, GFXTATexture );
    GFX->setTextureStageAlphaArg2( 0, GFXTADiffuse );

    // turn on alpha test
    GFX->setAlphaTestEnable( true );
    GFX->setAlphaRef( 1 );
    GFX->setAlphaFunc( GFXCmpGreaterEqual );

    // alpha blend
    GFX->setAlphaBlendEnable( true );
    GFX->setSrcBlend( GFXBlendSrcAlpha );
    GFX->setDestBlend( GFXBlendOne );
    GFX->setDestBlend( GFXBlendInvSrcAlpha);

	ColorF fillCol(1,1,1,1);

	PrimBuild::begin(GFXTriangleFan, 4); {
		PrimBuild::texCoord2f(0, 0); PrimBuild::vertex3fv(point[0]);
        PrimBuild::texCoord2f(0, 1); PrimBuild::vertex3fv(point[1]);
        PrimBuild::texCoord2f(1, 1); PrimBuild::vertex3fv(point[2]);
        PrimBuild::texCoord2f(1, 0); PrimBuild::vertex3fv(point[3]);
    } 
	PrimBuild::end();   
	
	// restore states
	GFX->setZWriteEnable( true );
	GFX->setAlphaTestEnable( false );
	GFX->setBaseRenderState();
}
//------------------------------------------------------------------------------
void fxInteriorDecal::initPersistFields()
{
   Parent::initPersistFields();

   addField("texture", TypeFilename, Offset(mTextureName, fxInteriorDecal));
   addField("angle", TypeFilename, Offset(mAngle, fxInteriorDecal));
}
//------------------------------------------------------------------------------

U32 fxInteriorDecal::packUpdate(NetConnection * con, U32 mask, BitStream * stream)
{
	// Pack Parent.
	U32 retMask = Parent::packUpdate(con, mask, stream);

    if (stream->writeFlag(mask & DataMask))
    {
		// Write Object Transform.
	    stream->writeAffineTransform(mObjToWorld);
		// Write Texture Name.
		stream->writeString(mTextureName);
		// Angle
		stream->writeString(mAngle);
	}
	// Were done ...
	return(retMask);
}

//------------------------------------------------------------------------------

void fxInteriorDecal::unpackUpdate(NetConnection * con, BitStream * stream)
{
	// Unpack Parent.
	Parent::unpackUpdate(con, stream);

	MatrixF		ObjectMatrix;

	if (stream->readFlag())
    {
		// Read Object Transform.
		stream->readAffineTransform(&ObjectMatrix);
		// Read Texture Name.
		mTextureName = StringTable->insert(stream->readSTString());
		// Read angle
		mAngle = StringTable->insert(stream->readSTString());

		// Set Transform.
		setTransform(ObjectMatrix);
		// Load the texture, if possible and if needed
		if (!mTextureHandle)
			if (*mTextureName)
				if(!mTextureHandle.set(mTextureName, &GFXDefaultStaticDiffuseProfile))      				
					Con::errorf("Unable to load texture: %s for decal!", mTextureName);
	}
	// Set viewing position
   	setViewPosition();
}
[/b]

And the header...
fxInteriorDecal.h
[b]
//-----------------------------------------------------------------------------
// Torque Game Engine
// 
// This file is based completely on fxRenderObject, changed only as/when I needed
// to.
// Written by Melvyn May, 9th September 2002.
//-----------------------------------------------------------------------------

#ifndef _FXINTERIORDECAL_H_
#define _FXINTERIORDECAL_H_

#ifndef _SCENEOBJECT_H_
#include "sim/sceneObject.h"
#endif
#include "game/gameBase.h"
#include "sim/decalmanager.h"
#include "gfx/gfxTextureHandle.h"
#include "gfx/GFXDevice.h"


struct fxInteriorDecalData: public GameBaseData {
   typedef GameBaseData Parent;

  public:
   StringTableEntry				mTextureName;

   fxInteriorDecalData();
   DECLARE_CONOBJECT(fxInteriorDecalData);
   bool onAdd();
   static void initPersistFields();
   virtual void packData  (BitStream* stream);
   virtual void unpackData(BitStream* stream);
};
//------------------------------------------------------------------------------
// Class: fxInteriorDecal
//------------------------------------------------------------------------------
class fxInteriorDecal : public GameBase
{
private:
	typedef GameBase		Parent;

protected:
	GFXTexHandle 					mTextureHandle;
	Point3F							point[4];

	// Fields.
public:
    StringTableEntry				mTextureName;
    Point3F							mPos;
    MatrixF							Transform;
    fxInteriorDecalData*            mDatablock;
	StringTableEntry				mAngle;    // easier to set this way

public:
	fxInteriorDecal();
	~fxInteriorDecal();
   enum
   {
      DataMask       = Parent::NextFreeMask << 0,
      NextFreeMask   = Parent::NextFreeMask << 1
   };
	// SceneObject
	void renderObject(SceneState*, SceneRenderImage*);
	virtual bool prepRenderImage(SceneState*, const U32 stateKey, const U32 startZone,
								const bool modifyBaseZoneState = false);

	// SimObject
	static void  initPersistFields();
	bool onAdd();
	void onRemove();
	void inspectPostApply();
    void setViewPosition();

	// NetObject
	U32 packUpdate(NetConnection *, U32, BitStream *);
	void unpackUpdate(NetConnection *, BitStream *);

	// Declare Console Object.
	DECLARE_CONOBJECT(fxInteriorDecal);
};

#endif // _fxInteriorDecal_H_
[/b]

2 small things need to be done to your scripts.
First off, we need a blank datablock, since this class is based on gamebasedata.
Originally, I did it differently, but later decided to keep everything as generic
as possible, thus keeping all GameEntity features similar in nature. Doing this,
I was able to avoid breaking old compiled difs.
anyfile.cs
datablock fxInteriorDecalData(GenericDecalData)
{
  // A blank datablock, simply so we can properly create this gamebase
  // derived object
  blank = 0;
};

The next change is actually optional. It simply prevents the decal from
being selectable in the editor, which is a good thing since any changes you
make will have no lasting effect. Up to you though...
common/editor.cs
In function editor::onAdd
// Terrain Editor
   exec("./TerrainEditorVSettingsGui.gui");

   // Ignore Replicated fxStatic Instances.
   EWorldEditor.ignoreObjClass("fxShapeReplicatedStatic");
   [b]EWorldEditor.ignoreObjClass("fxInteriorDecal");[/b]
   
   // do gui initialization...
   EditorGui.init();

Last thing to add, is changes to QuArK. If you use something else, well
you'll have to figure it out yourself.
dataTorque.qrk
Special entities.qtxfolder =
      {
        [b]game_entity:e =
        {
          origin = "0 0 0"
          angle = "360"
          ;desc = "Decal"
          datablock = "GenericDecalData"
          game_class = "fxInteriorDecal"
        }[/b]      
        MirrorSurface:e =
        {
          ;desc = "Mirror Surface Entity"
          origin = "0 0 0"
          alpha_level = "0"
        }


If all went well, your map file should look like this....
// Entity 9
// Entities:g[1] -> game_entity:e[9]
{
 "classname" "game_entity"
 "origin" "-382 -148 40"
 "angle" "360"
 "datablock" "GenericDecalData"
 "game_class" "fxInteriorDecal"
 "texture" "starter.fps/data/decals/decalgraffiti001b"
}

#1
02/08/2005 (2:29 pm)
Feel free to toss suggestions at me, fix crap you see, etc. I'm not too proud to admit you probably know more than I :)
#2
02/08/2005 (2:51 pm)
do u know if this works with cartography shop
#3
02/08/2005 (3:32 pm)
@Ken - Erik will have to back me up on this one, but his resource doesn't seem to require changes to existing TSE map2dif code. If this is the case then it should work fine with our exporter for CartShop.
#4
02/08/2005 (6:02 pm)
Well, it doesnt affect the map2dif itself, but it does alter a write() call, which is compiled into engine.lib, and used by map2dif. Dunno how that affects CS, but if anything, its minor.

BTW, this learning experience makes model instance loading a breeze. I don't even have to munge GFX for that :)
Gimmie 2-3 days, and I should have that working as well.
#5
02/08/2005 (7:01 pm)
@Erik

If the write your talking about is in PackUpdate then that shouldn't be a problem. PackUpdate is only used for network updates and not in map2dif.

Also Matt pointed out that map2dif doesn't parse the "datablock" property for game entities (look at GameEntity::parseEntityDescription() in entityTypes.cpp). I'm not sure how this effects your resource other than maybe your dummy datablock isn't needed?

Still it does seem like GameEntity is perfectly suited for doing DTS instances. If you get that working i'll be sure to directly support it in our CartShop exporter at least until TSE HEAD has equivalent functionality.
#6
02/09/2005 (12:03 am)
This can turn out to be one of the most usefull resources ever released! Great work Erik!

Do I need to modify the .fgd file to get this to work in Hammer?

Nick
#7
02/09/2005 (2:24 am)
Updated I did forget to add the map2dif function, as I did know datablocks weren't parsed in. Also, I reversed 2 things in read/write resobjects, which gets rid of some file garbage. So, those are what are needed for a CS exporter to be aware of.

@Nick Yes, you will need something in .fgd in order to see the new entity. You could probably cut/paste most of the entity from an old HL1 fgd
#8
02/09/2005 (2:29 am)
Pretty cool! I'd love to see a decal system integrated into both engines one day.

One of Half-life's coolest features was Randomly tileable wall-sets* & decals I think. :)
And it's ground-level stuff which uses will be there for years to come.

off topic/ *A set of textures usually 5-6 of the same texture with subtle visual differeces to break up monotony and tiling. So basically, when you apply one texture to a wall it tiles the set randomly instead of just one texture. A bit more work on the artist, but the payoff is worth it. This is a very over-looked feature from half-life as it's doing it's job so well that no-one notices! /off topic
#9
02/09/2005 (3:02 am)
Well shoot, that was easy. The basics are working fine for loading static shapes and items via the interior. And just like the decals, they move when I move the rooms around. An hour or two after work today, and I should have them done as well.

@Timothy yeah, I had forgotten about the funky textures in HL. Thats pretty easy I would think, but it relys more on the material system to handle. I'd need a bit more GFX knowledge to get those in.
#10
02/09/2005 (5:20 am)
Sorry to bug you again but I have the halflife.fgd that came with Hammer and I don't see a GameEntity? Got any clues?

Nick
#11
02/09/2005 (9:28 am)
I'm looking closer at this and confused by the "angle" property in the decal game_entity. It seems you use -1 and -2 as magic numbers to point up or down else angle is used to create a normal around the z axis. Is this correct Erik? How does this get set in QuArK? Manually?

For spotlights Torque uses two entities. The spot_light entity and its target entity. The origin for these two entities are used to form the spotlight direction normal. This is perhaps a more general solution, but it comes at the expense of a little more complexity in setting it up for the scene.

Both of these methods are limited versions of a Projector Textures in Unreal. This might be where this resource should head.
#12
02/09/2005 (12:13 pm)
QuArK outputs the -1 and -2, its built into their angle gizmo. I just used what they had since at the moment, QuArK is the 'official' editor for Torque. I don't know the full history of TGE spotlights, but I'd assume they added the additional work due to using a builder that wasn't quite as advanced as we have now.
If CS has a more elegant way of setting angles up, feel free to adjust as needed. Doing so though, may confuse some builders. IMHO, internal code needs to be based upon the 'official' tools.
I'll check out your link in a few though, Im always open to new ideas.

BTW, rudimentary model and object instancing is already done, via this resource. Simply change TSStatic to be based on GameBase, rather than SceneObject. Set the editor game_class to TSStatic and the DataBlock field to the shapefile. For items, game_class is Item, DataBlock is whatever, Crossbow for my tests. A bit more work is needed to make items fully usable, but they do show up and move with the interiors as is. The extra data added to TSStatic is extremely small, and seldom sent across the wire, so its a decent tradeoff. Getting them to line up perfectly in the editor though, will be a chore in itself. Im researching how to load them in QuArK right now, since it already supports mdl, md2, and md3 models.
#13
02/10/2005 (1:00 am)
@Erik - Absolutely... the code should always conform to the official toolset. CartShop has no concept of an entity with a direction, so I need to use another method. Most likely just placing the decal entity near a brush face then the exporter will point it to the closest one it finds.... this should work in most every case.
#14
12/20/2005 (1:28 pm)
I cant seem to get this to work with the latest HEAD version of TSE. I followed the above procedure exactly, and it compiles except for a few errors:

../engine\interior\interiorResObjects.h(34) : error C2065: 'b' : undeclared identifier
../engine\interior\interiorResObjects.h(34) : error C2059: syntax error : ']'
../engine\interior\interiorResObjects.h(34) : error C2238: unexpected token(s) preceding ';'
../engine\interior\interiorResObjects.h(119) : error C2337: 'b' : attribute not found; it is neither a built-in nor a custom attribute that is accessible in the current namespace
interiorRes.cpp

I changed char value[128[b/]]; to char value[128];
and [b]U32 mId; to U32 mId; and it compiled perfectly.
However my existing .difs in my game now flicker terribly between their curretn texture and a white texture, and when I go inside the .dif, they flicker pink for some reason.
I can see the new entity in quark, and I can export .difs ok, but when I try to load a dif with a new decal entity in it the engine crashes with no errors or warnings.
Any ideas?
Thanks,
JS
#15
01/28/2007 (7:17 pm)
Is anyone using this for 4.2 ? I just compiled her up and I am getting a runtime error. Contrary to instructions, I am using an unmodified map2dif_plus.exe.