//----------------------------------------------------------------------------- // Copyright (c) 2012 GarageGames, LLC // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. //----------------------------------------------------------------------------- //~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~~// // Arcane-FX for MIT Licensed Open Source version of Torque 3D from GarageGames // Copyright (C) 2015 Faust Logic, Inc. //~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~~// #include "platform/platform.h" #include "T3D/gameBase/gameBase.h" #include "console/consoleTypes.h" #include "console/engineAPI.h" #include "console/consoleInternal.h" #include "core/stream/bitStream.h" #include "sim/netConnection.h" #include "T3D/gameBase/gameConnection.h" #include "math/mathIO.h" #include "T3D/gameBase/moveManager.h" #include "T3D/gameBase/gameProcess.h" #ifdef TORQUE_DEBUG_NET_MOVES #include "T3D/aiConnection.h" #endif #ifdef TORQUE_AFX_ENABLED #include "afx/arcaneFX.h" #endif //---------------------------------------------------------------------------- // Ghost update relative priority values static F32 sUpFov = 1.0; static F32 sUpDistance = 0.4f; static F32 sUpVelocity = 0.4f; static F32 sUpSkips = 0.2f; static F32 sUpInterest = 0.2f; //---------------------------------------------------------------------------- IMPLEMENT_CO_DATABLOCK_V1(GameBaseData); ConsoleDocClass( GameBaseData, "@brief Scriptable, demo-able datablock. Used by GameBase objects.\n\n" "@see GameBase\n" "@ingroup gameObjects\n" ); IMPLEMENT_CALLBACK( GameBaseData, onAdd, void, ( GameBase* obj ), ( obj ), "@brief Called when the object is added to the scene.\n\n" "@param obj the GameBase object\n\n" "@tsexample\n" "datablock GameBaseData(MyObjectData)\n" "{\n" " category = \"Misc\";\n" "};\n\n" "function MyObjectData::onAdd( %this, %obj )\n" "{\n" " echo( \"Added \" @ %obj.getName() @ \" to the scene.\" );\n" "}\n\n" "function MyObjectData::onNewDataBlock( %this, %obj )\n" "{\n" " echo( \"Assign \" @ %this.getName() @ \" datablock to \" %obj.getName() );\n" "}\n\n" "function MyObjectData::onRemove( %this, %obj )\n" "{\n" " echo( \"Removed \" @ %obj.getName() @ \" to the scene.\" );\n" "}\n\n" "function MyObjectData::onMount( %this, %obj, %mountObj, %node )\n" "{\n" " echo( %obj.getName() @ \" mounted to \" @ %mountObj.getName() );\n" "}\n\n" "function MyObjectData::onUnmount( %this, %obj, %mountObj, %node )\n" "{\n" " echo( %obj.getName() @ \" unmounted from \" @ %mountObj.getName() );\n" "}\n\n" "@endtsexample\n" ); IMPLEMENT_CALLBACK( GameBaseData, onNewDataBlock, void, ( GameBase* obj ), ( obj ), "@brief Called when the object has a new datablock assigned.\n\n" "@param obj the GameBase object\n\n" "@see onAdd for an example\n" ); IMPLEMENT_CALLBACK( GameBaseData, onRemove, void, ( GameBase* obj ), ( obj ), "@brief Called when the object is removed from the scene.\n\n" "@param obj the GameBase object\n\n" "@see onAdd for an example\n" ); IMPLEMENT_CALLBACK( GameBaseData, onMount, void, ( SceneObject* obj, SceneObject* mountObj, S32 node ), ( obj, mountObj, node ), "@brief Called when the object is mounted to another object in the scene.\n\n" "@param obj the GameBase object being mounted\n" "@param mountObj the object we are mounted to\n" "@param node the mountObj node we are mounted to\n\n" "@see onAdd for an example\n" ); IMPLEMENT_CALLBACK( GameBaseData, onUnmount, void, ( SceneObject* obj, SceneObject* mountObj, S32 node ), ( obj, mountObj, node ), "@brief Called when the object is unmounted from another object in the scene.\n\n" "@param obj the GameBase object being unmounted\n" "@param mountObj the object we are unmounted from\n" "@param node the mountObj node we are unmounted from\n\n" "@see onAdd for an example\n" ); IMPLEMENT_CALLBACK( GameBase, setControl, void, ( bool controlled ), ( controlled ), "@brief Called when the client controlling the object changes.\n\n" "@param controlled true if a client now controls this object, false if no " "client controls this object.\n" ); GameBaseData::GameBaseData() { mCategory = ""; mPacked = false; } GameBaseData::GameBaseData(const GameBaseData& other, bool temp_clone) : SimDataBlock(other, temp_clone) { mPacked = other.mPacked; mCategory = other.mCategory; //mReloadSignal = other.mReloadSignal; // DO NOT copy the mReloadSignal member. } void GameBaseData::inspectPostApply() { Parent::inspectPostApply(); // Tell interested parties ( like objects referencing this datablock ) // that we have been modified and they might want to rebuild... mReloadSignal.trigger(); } bool GameBaseData::onAdd() { if (!Parent::onAdd()) return false; return true; } void GameBaseData::initPersistFields() { addGroup("Scripting"); addField( "category", TypeCaseString, Offset(mCategory, GameBaseData ), "The group that this datablock will show up in under the \"Scripted\" " "tab in the World Editor Library." ); endGroup("Scripting"); Parent::initPersistFields(); } bool GameBaseData::preload(bool server, String &errorStr) { if (!Parent::preload(server, errorStr)) return false; mPacked = false; return true; } void GameBaseData::unpackData(BitStream* stream) { Parent::unpackData(stream); mPacked = true; } //---------------------------------------------------------------------------- bool UNPACK_DB_ID(BitStream * stream, U32 & id) { if (stream->readFlag()) { id = stream->readRangedU32(DataBlockObjectIdFirst,DataBlockObjectIdLast); return true; } return false; } bool PACK_DB_ID(BitStream * stream, U32 id) { if (stream->writeFlag(id)) { stream->writeRangedU32(id,DataBlockObjectIdFirst,DataBlockObjectIdLast); return true; } return false; } bool PRELOAD_DB(U32 & id, SimDataBlock ** data, bool server, const char * clientMissing, const char * serverMissing) { if (server) { if (*data) id = (*data)->getId(); else if (server && serverMissing) { Con::errorf(ConsoleLogEntry::General,serverMissing); return false; } } else { if (id && !Sim::findObject(id,*data) && clientMissing) { Con::errorf(ConsoleLogEntry::General,clientMissing); return false; } } return true; } //---------------------------------------------------------------------------- bool GameBase::gShowBoundingBox = false; //---------------------------------------------------------------------------- IMPLEMENT_CO_NETOBJECT_V1(GameBase); ConsoleDocClass( GameBase, "@brief Base class for game objects which use datablocks, networking, are " "editable, and need to process ticks.\n\n" "@ingroup gameObjects\n" ); GameBase::GameBase() : mDataBlock( NULL ), mControllingClient( NULL ), mCurrentWaterObject( NULL ) { mNetFlags.set(Ghostable); mTypeMask |= GameBaseObjectType; mProcessTag = 0; // From ProcessObject mIsGameBase = true; #ifdef TORQUE_DEBUG_NET_MOVES mLastMoveId = 0; mTicksSinceLastMove = 0; mIsAiControlled = false; #endif mCameraFov = 90.f; } GameBase::~GameBase() { #ifdef TORQUE_AFX_ENABLED if (mScope_registered) arcaneFX::unregisterScopedObject(this); #endif } //---------------------------------------------------------------------------- bool GameBase::onAdd() { if ( !Parent::onAdd() ) return false; // Datablock must be initialized on the server. // Client datablock are initialized by the initial update. #ifdef TORQUE_AFX_ENABLED if (isClientObject()) { if (mScope_id > 0 && !mScope_registered) arcaneFX::registerScopedObject(this); } else { if ( mDataBlock && !onNewDataBlock( mDataBlock, false ) ) return false; } #else if ( isServerObject() && mDataBlock && !onNewDataBlock( mDataBlock, false ) ) return false; #endif setProcessTick( true ); return true; } void GameBase::onRemove() { #ifdef TORQUE_AFX_ENABLED if (mScope_registered) arcaneFX::unregisterScopedObject(this); #endif // EDITOR FEATURE: Remove us from the reload signal of our datablock. if ( mDataBlock ) mDataBlock->mReloadSignal.remove( this, &GameBase::_onDatablockModified ); Parent::onRemove(); } bool GameBase::onNewDataBlock( GameBaseData *dptr, bool reload ) { // EDITOR FEATURE: Remove us from old datablock's reload signal and // add us to the new one. if ( !reload ) { if ( mDataBlock ) mDataBlock->mReloadSignal.remove( this, &GameBase::_onDatablockModified ); if ( dptr ) dptr->mReloadSignal.notify( this, &GameBase::_onDatablockModified ); } mDataBlock = dptr; if ( !mDataBlock ) return false; #ifdef TORQUE_AFX_ENABLED // Don't set mask when new datablock is a temp-clone. if (mDataBlock->isTempClone()) return true; #endif setMaskBits(DataBlockMask); return true; } void GameBase::_onDatablockModified() { AssertFatal( mDataBlock, "GameBase::onDatablockModified - mDataBlock is NULL." ); onNewDataBlock( mDataBlock, true ); } void GameBase::inspectPostApply() { Parent::inspectPostApply(); setMaskBits(ExtendedInfoMask); } //---------------------------------------------------------------------------- void GameBase::processTick(const Move * move) { #ifdef TORQUE_DEBUG_NET_MOVES if (!move) mTicksSinceLastMove++; const char * srv = isClientObject() ? "client" : "server"; const char * who = ""; if (isClientObject()) { if (this == (GameBase*)GameConnection::getConnectionToServer()->getControlObject()) who = " player"; else who = " ghost"; if (mIsAiControlled) who = " ai"; } if (isServerObject()) { if (dynamic_cast(getControllingClient())) { who = " ai"; mIsAiControlled = true; } else if (getControllingClient()) { who = " player"; mIsAiControlled = false; } else { who = ""; mIsAiControlled = false; } } U32 moveid = mLastMoveId+mTicksSinceLastMove; if (move) moveid = move->id; if (getTypeMask() & GameBaseHiFiObjectType) { if (move) Con::printf("Processing (%s%s id %i) move %i",srv,who,getId(), move->id); else Con::printf("Processing (%s%s id %i) move %i (%i)",srv,who,getId(),mLastMoveId+mTicksSinceLastMove,mTicksSinceLastMove); } if (move) { mLastMoveId = move->id; mTicksSinceLastMove=0; } #endif } void GameBase::interpolateTick(F32 dt) { // PATHSHAPE updateRenderChangesByParent(); // PATHSHAPE END } //---------------------------------------------------------------------------- F32 GameBase::getUpdatePriority(CameraScopeQuery *camInfo, U32 updateMask, S32 updateSkips) { TORQUE_UNUSED(updateMask); // Calculate a priority used to decide if this object // will be updated on the client. All the weights // are calculated 0 -> 1 Then weighted together at the // end to produce a priority. Point3F pos; getWorldBox().getCenter(&pos); pos -= camInfo->pos; F32 dist = pos.len(); if (dist == 0.0f) dist = 0.001f; pos *= 1.0f / dist; // Weight based on linear distance, the basic stuff. F32 wDistance = (dist < camInfo->visibleDistance)? 1.0f - (dist / camInfo->visibleDistance): 0.0f; // Weight by field of view, objects directly in front // will be weighted 1, objects behind will be 0 F32 dot = mDot(pos,camInfo->orientation); bool inFov = dot > camInfo->cosFov * 1.5f; F32 wFov = inFov? 1.0f: 0; // Weight by linear velocity parallel to the viewing plane // (if it's the field of view, 0 if it's not). F32 wVelocity = 0.0f; if (inFov) { Point3F vec; mCross(camInfo->orientation,getVelocity(),&vec); wVelocity = (vec.len() * camInfo->fov) / (camInfo->fov * camInfo->visibleDistance); if (wVelocity > 1.0f) wVelocity = 1.0f; } // Weight by interest. F32 wInterest; if (getTypeMask() & (PlayerObjectType || VehicleObjectType )) wInterest = 0.75f; else if (getTypeMask() & ProjectileObjectType) { // Projectiles are more interesting if they // are heading for us. wInterest = 0.30f; dot = -mDot(pos,getVelocity()); if (dot > 0.0f) wInterest += 0.20 * dot; } else { if (getTypeMask() & ItemObjectType) wInterest = 0.25f; else // Everything else is less interesting. wInterest = 0.0f; } // Weight by updateSkips F32 wSkips = updateSkips * 0.5; // Calculate final priority, should total to about 1.0f (plus children) // return wFov * sUpFov + wDistance * sUpDistance + wVelocity * sUpVelocity + wSkips * sUpSkips + wInterest * sUpInterest + getNumChildren(); } //---------------------------------------------------------------------------- bool GameBase::setDataBlock(GameBaseData* dptr) { if (isGhost() || isProperlyAdded()) { if (mDataBlock != dptr) return onNewDataBlock(dptr,false); } else mDataBlock = dptr; return true; } //-------------------------------------------------------------------------- void GameBase::scriptOnAdd() { // Script onAdd() must be called by the leaf class after // everything is ready. if (mDataBlock && !isGhost()) mDataBlock->onAdd_callback( this ); } void GameBase::scriptOnNewDataBlock() { // Script onNewDataBlock() must be called by the leaf class // after everything is loaded. if (mDataBlock && !isGhost()) mDataBlock->onNewDataBlock_callback( this ); } void GameBase::scriptOnRemove() { // Script onRemove() must be called by leaf class while // the object state is still valid. if (!isGhost() && mDataBlock) mDataBlock->onRemove_callback( this ); } //---------------------------------------------------------------------------- void GameBase::setControllingClient(GameConnection* client) { if (isClientObject()) { if (mControllingClient) setControl_callback( 0 ); if (client) setControl_callback( 1 ); } mControllingClient = client; } U32 GameBase::getPacketDataChecksum(GameConnection * connection) { // just write the packet data into a buffer // then we can CRC the buffer. This should always let us // know when there is a checksum problem. static U8 buffer[1500] = { 0, }; BitStream stream(buffer, sizeof(buffer)); writePacketData(connection, &stream); U32 byteCount = stream.getPosition(); U32 ret = CRC::calculateCRC(buffer, byteCount, 0xFFFFFFFF); dMemset(buffer, 0, byteCount); return ret; } void GameBase::writePacketData(GameConnection*, BitStream*) { } void GameBase::readPacketData(GameConnection*, BitStream*) { } U32 GameBase::packUpdate( NetConnection *connection, U32 mask, BitStream *stream ) { U32 retMask = Parent::packUpdate( connection, mask, stream ); if ( stream->writeFlag( mask & ScaleMask ) ) { // Only write one bit if the scale is one. if ( stream->writeFlag( mObjScale != Point3F::One ) ) mathWrite( *stream, mObjScale ); } if ( stream->writeFlag( ( mask & DataBlockMask ) && mDataBlock != NULL ) ) { stream->writeRangedU32( mDataBlock->getId(), DataBlockObjectIdFirst, DataBlockObjectIdLast ); if ( stream->writeFlag( mNetFlags.test( NetOrdered ) ) ) stream->writeInt( mOrderGUID, 16 ); } #ifdef TORQUE_DEBUG_NET_MOVES stream->write(mLastMoveId); stream->writeFlag(mIsAiControlled); #endif #ifdef TORQUE_AFX_ENABLED if (stream->writeFlag(mask & ScopeIdMask)) { if (stream->writeFlag(mScope_refs > 0)) stream->writeInt(mScope_id, SCOPE_ID_BITS); } #endif return retMask; } void GameBase::unpackUpdate(NetConnection *con, BitStream *stream) { Parent::unpackUpdate( con, stream ); // ScaleMask if ( stream->readFlag() ) { if ( stream->readFlag() ) { VectorF scale; mathRead( *stream, &scale ); setScale( scale ); } else setScale( Point3F::One ); } // DataBlockMask if ( stream->readFlag() ) { GameBaseData *dptr = 0; SimObjectId id = stream->readRangedU32( DataBlockObjectIdFirst, DataBlockObjectIdLast ); if ( stream->readFlag() ) mOrderGUID = stream->readInt( 16 ); if ( !Sim::findObject( id, dptr ) || !setDataBlock( dptr ) ) con->setLastError( "Invalid packet GameBase::unpackUpdate()" ); } #ifdef TORQUE_DEBUG_NET_MOVES stream->read(&mLastMoveId); mTicksSinceLastMove = 0; mIsAiControlled = stream->readFlag(); #endif #ifdef TORQUE_AFX_ENABLED if (stream->readFlag()) { mScope_id = (stream->readFlag()) ? (U16) stream->readInt(SCOPE_ID_BITS) : 0; mScope_refs = 0; } #endif } void GameBase::onMount( SceneObject *obj, S32 node ) { deleteNotify( obj ); // Are we mounting to a GameBase object? GameBase *gbaseObj = dynamic_cast( obj ); if ( gbaseObj && gbaseObj->getControlObject() != this && gbaseObj->getControllingObject() != this) processAfter( gbaseObj ); if (!isGhost()) { setMaskBits(MountedMask); mDataBlock->onMount_callback( this, obj, node ); } } void GameBase::onUnmount( SceneObject *obj, S32 node ) { clearNotify(obj); GameBase *gbaseObj = dynamic_cast( obj ); if ( gbaseObj && gbaseObj->getControlObject() != this ) clearProcessAfter(); if (!isGhost()) { setMaskBits(MountedMask); mDataBlock->onUnmount_callback( this, obj, node ); } } bool GameBase::setDataBlockProperty( void *obj, const char *index, const char *db) { if( db == NULL || !db[ 0 ] ) { Con::errorf( "GameBase::setDataBlockProperty - Can't unset datablock on GameBase objects" ); return false; } GameBase* object = static_cast< GameBase* >( obj ); GameBaseData* data; if( Sim::findObject( db, data ) ) return object->setDataBlock( data ); Con::errorf( "GameBase::setDatablockProperty - Could not find data block \"%s\"", db ); return false; } MoveList* GameBase::getMoveList() { return mControllingClient ? mControllingClient->mMoveList : NULL; } //---------------------------------------------------------------------------- DefineEngineMethod( GameBase, getDataBlock, S32, (),, "@brief Get the datablock used by this object.\n\n" "@return the datablock this GameBase is using." "@see setDataBlock()\n") { return object->getDataBlock()? object->getDataBlock()->getId(): 0; } //---------------------------------------------------------------------------- DefineEngineMethod( GameBase, setDataBlock, bool, ( GameBaseData* data ),, "@brief Assign this GameBase to use the specified datablock.\n\n" "@param data new datablock to use\n" "@return true if successful, false if failed." "@see getDataBlock()\n") { return ( data && object->setDataBlock(data) ); } //---------------------------------------------------------------------------- void GameBase::initPersistFields() { addGroup( "Game" ); addProtectedField( "dataBlock", TYPEID< GameBaseData >(), Offset(mDataBlock, GameBase), &setDataBlockProperty, &defaultProtectedGetFn, "Script datablock used for game objects." ); endGroup( "Game" ); Parent::initPersistFields(); } void GameBase::consoleInit() { #ifdef TORQUE_DEBUG Con::addVariable( "GameBase::boundingBox", TypeBool, &gShowBoundingBox, "@brief Toggles on the rendering of the bounding boxes for certain types of objects in scene.\n\n" "@ingroup GameBase" ); #endif } DefineEngineMethod( GameBase, applyImpulse, bool, ( Point3F pos, VectorF vel ),, "@brief Apply an impulse to this object as defined by a world position and velocity vector.\n\n" "@param pos impulse world position\n" "@param vel impulse velocity (impulse force F = m * v)\n" "@return Always true\n" "@note Not all objects that derrive from GameBase have this defined.\n") { object->applyImpulse(pos,vel); return true; } DefineEngineMethod( GameBase, applyRadialImpulse, void, ( Point3F origin, F32 radius, F32 magnitude ),, "@brief Applies a radial impulse to the object using the given origin and force.\n\n" "@param origin World point of origin of the radial impulse.\n" "@param radius The radius of the impulse area.\n" "@param magnitude The strength of the impulse.\n" "@note Not all objects that derrive from GameBase have this defined.\n") { object->applyRadialImpulse( origin, radius, magnitude ); } // PATHSHAPE // Console Methods for attach children. can't put them in sceneobject because // // we want the processafter functions//////////////////////////////////////////// DefineEngineMethod(GameBase, attachChild, bool, (GameBase* _subObject), (nullAsType()), "(SceneObject subObject)" "attach an object to this one, preserving its present transform.") { if (_subObject != nullptr) { if (_subObject->getParent() != object){ Con::errorf("Object is (%d)", _subObject->getId()); _subObject->clearProcessAfter(); _subObject->processAfter(object); return object->attachChild(_subObject); } else return false; } else { Con::errorf("Couldn't addObject()!"); return false; } } DefineEngineMethod(GameBase, detachChild, bool, (GameBase* _subObject), (nullAsType()), "(SceneObject subObject)" "attach an object to this one, preserving its present transform.") { if (_subObject != nullptr) { _subObject->clearProcessAfter(); return _subObject->attachToParent(NULL); } else { return false; } }//end // PATHSHAPE END