Initial hook-in of the sound asset's integration into sfxEmitter, as well as some fixups for editor/workflow usage.

This commit is contained in:
JeffR 2021-08-31 00:54:05 -05:00
parent 6fc67a11bb
commit 56b0a0cb85
6 changed files with 214 additions and 138 deletions

View file

@ -40,6 +40,10 @@
#include "assets/assetPtr.h"
#endif
#ifndef _SFXSOURCE_H_
#include "sfx/sfxSource.h"
#endif
// Debug Profiling.
#include "platform/profiler.h"
#include "sfx/sfxTypes.h"
@ -159,7 +163,7 @@ void SoundAsset::initPersistFields()
addField("maxDistance", TypeF32, Offset(mProfileDesc.mMaxDistance, SoundAsset), "Max distance for sound.");
addField("coneInsideAngle", TypeS32, Offset(mProfileDesc.mConeInsideAngle, SoundAsset), "Cone inside angle.");
addField("coneOutsideAngle", TypeS32, Offset(mProfileDesc.mConeOutsideAngle, SoundAsset), "Cone outside angle.");
addField("coneOutsideVolume", TypeS32, Offset(mProfileDesc.mConeOutsideVolume, SoundAsset), "Cone outside volume.");
addField("coneOutsideVolume", TypeF32, Offset(mProfileDesc.mConeOutsideVolume, SoundAsset), "Cone outside volume.");
addField("rolloffFactor", TypeF32, Offset(mProfileDesc.mRolloffFactor, SoundAsset), "Rolloff factor.");
addField("scatterDistance", TypePoint3F, Offset(mProfileDesc.mScatterDistance, SoundAsset), "Randomization to the spacial position of the sound.");
addField("sourceGroup", TypeSFXSourceName, Offset(mProfileDesc.mSourceGroup, SoundAsset), "Group that sources playing with this description should be put into.");
@ -181,13 +185,7 @@ void SoundAsset::initializeAsset(void)
if (mSoundFile == StringTable->EmptyString())
return;
//ResourceManager::get().getChangedSignal.notify(this, &SoundAsset::_onResourceChanged);
//Ensure our path is expando'd if it isn't already
mSoundPath = getOwned() ? expandAssetFilePath(mSoundFile) : mSoundPath;
mSoundPath = expandAssetFilePath(mSoundPath);
loadSound();
}
@ -208,7 +206,6 @@ void SoundAsset::onAssetRefresh(void)
//Update
mSoundPath = getOwned() ? expandAssetFilePath(mSoundFile) : mSoundPath;
loadSound();
}
@ -225,7 +222,7 @@ bool SoundAsset::loadSound()
else
{// = new SFXProfile(mProfileDesc, mSoundFile, mPreload);
mSFXProfile.setDescription(&mProfileDesc);
mSFXProfile.setSoundFileName(mSoundFile);
mSFXProfile.setSoundFileName(mSoundPath);
mSFXProfile.setPreload(mPreload);
}
@ -254,11 +251,98 @@ void SoundAsset::setSoundFile(const char* pSoundFile)
refreshAsset();
}
StringTableEntry SoundAsset::getAssetIdByFileName(StringTableEntry fileName)
{
if (fileName == StringTable->EmptyString())
return StringTable->EmptyString();
StringTableEntry materialAssetId = "";
AssetQuery query;
U32 foundCount = AssetDatabase.findAssetType(&query, "SoundAsset");
if (foundCount != 0)
{
for (U32 i = 0; i < foundCount; i++)
{
SoundAsset* soundAsset = AssetDatabase.acquireAsset<SoundAsset>(query.mAssetList[i]);
if (soundAsset && soundAsset->getSoundPath() == fileName)
{
materialAssetId = soundAsset->getAssetId();
AssetDatabase.releaseAsset(query.mAssetList[i]);
break;
}
AssetDatabase.releaseAsset(query.mAssetList[i]);
}
}
return materialAssetId;
}
U32 SoundAsset::getAssetById(StringTableEntry assetId, AssetPtr<SoundAsset>* materialAsset)
{
(*materialAsset) = assetId;
if (materialAsset->notNull())
{
return (*materialAsset)->mLoadedState;
}
else
{
//Well that's bad, loading the fallback failed.
Con::warnf("MaterialAsset::getAssetById - Finding of asset with id %s failed with no fallback asset", assetId);
return AssetErrCode::Failed;
}
}
U32 SoundAsset::getAssetByFileName(StringTableEntry fileName, AssetPtr<SoundAsset>* soundAsset)
{
AssetQuery query;
U32 foundAssetcount = AssetDatabase.findAssetType(&query, "SoundAsset");
if (foundAssetcount == 0)
{
//Well that's bad, loading the fallback failed.
Con::warnf("MaterialAsset::getAssetByMaterialName - Finding of asset associated with filename %s failed with no fallback asset", fileName);
return AssetErrCode::Failed;
}
else
{
for (U32 i = 0; i < foundAssetcount; i++)
{
SoundAsset* tSoundAsset = AssetDatabase.acquireAsset<SoundAsset>(query.mAssetList[i]);
if (tSoundAsset && tSoundAsset->getSoundPath() == fileName)
{
soundAsset->setAssetId(query.mAssetList[i]);
AssetDatabase.releaseAsset(query.mAssetList[i]);
return (*soundAsset)->mLoadedState;
}
AssetDatabase.releaseAsset(query.mAssetList[i]); //cleanup if that's not the one we needed
}
}
//No good match
return AssetErrCode::Failed;
}
DefineEngineMethod(SoundAsset, getSoundPath, const char*, (), , "")
{
return object->getSoundPath();
}
DefineEngineMethod(SoundAsset, playSound, S32, (Point3F position), (Point3F::Zero),
"Gets the number of materials for this shape asset.\n"
"@return Material count.\n")
{
if (object->getSfxProfile())
{
MatrixF transform;
transform.setPosition(position);
SFXSource* source = SFX->playOnce(object->getSfxProfile(), &transform, NULL, -1);
return source->getId();
}
else
return 0;
}
IMPLEMENT_CONOBJECT(GuiInspectorTypeSoundAssetPtr);
ConsoleDocClass(GuiInspectorTypeSoundAssetPtr,
@ -276,12 +360,63 @@ void GuiInspectorTypeSoundAssetPtr::consoleInit()
GuiControl * GuiInspectorTypeSoundAssetPtr::constructEditControl()
{
return nullptr;
// Create base filename edit controls
GuiControl* retCtrl = Parent::constructEditControl();
if (retCtrl == NULL)
return retCtrl;
// Change filespec
char szBuffer[512];
dSprintf(szBuffer, sizeof(szBuffer), "AssetBrowser.showDialog(\"SoundAsset\", \"AssetBrowser.changeAsset\", %s, \"\");",
getIdString());
mBrowseButton->setField("Command", szBuffer);
setDataField(StringTable->insert("targetObject"), NULL, mInspector->getInspectObject()->getIdString());
// Create "Open in Editor" button
mEditButton = new GuiBitmapButtonCtrl();
dSprintf(szBuffer, sizeof(szBuffer), "AssetBrowser.editAsset(%d.getText());", retCtrl->getId());
mEditButton->setField("Command", szBuffer);
char bitmapName[512] = "ToolsModule:SFXEmitter_image";
mEditButton->setBitmap(StringTable->insert(bitmapName));
mEditButton->setDataField(StringTable->insert("Profile"), NULL, "GuiButtonProfile");
mEditButton->setDataField(StringTable->insert("tooltipprofile"), NULL, "GuiToolTipProfile");
mEditButton->setDataField(StringTable->insert("hovertime"), NULL, "1000");
mEditButton->setDataField(StringTable->insert("tooltip"), NULL, "Test play this sound");
mEditButton->registerObject();
addObject(mEditButton);
return retCtrl;
}
bool GuiInspectorTypeSoundAssetPtr::updateRects()
{
return false;
S32 dividerPos, dividerMargin;
mInspector->getDivider(dividerPos, dividerMargin);
Point2I fieldExtent = getExtent();
Point2I fieldPos = getPosition();
mCaptionRect.set(0, 0, fieldExtent.x - dividerPos - dividerMargin, fieldExtent.y);
mEditCtrlRect.set(fieldExtent.x - dividerPos + dividerMargin, 1, dividerPos - dividerMargin - 34, fieldExtent.y);
bool resized = mEdit->resize(mEditCtrlRect.point, mEditCtrlRect.extent);
if (mBrowseButton != NULL)
{
mBrowseRect.set(fieldExtent.x - 32, 2, 14, fieldExtent.y - 4);
resized |= mBrowseButton->resize(mBrowseRect.point, mBrowseRect.extent);
}
if (mEditButton != NULL)
{
RectI shapeEdRect(fieldExtent.x - 16, 2, 14, fieldExtent.y - 4);
resized |= mEditButton->resize(shapeEdRect.point, shapeEdRect.extent);
}
return resized;
}
IMPLEMENT_CONOBJECT(GuiInspectorTypeSoundAssetId);

View file

@ -122,6 +122,9 @@ public:
bool isLoop() { return mProfileDesc.mIsLooping; }
bool is3D() { return mProfileDesc.mIs3D; }
static StringTableEntry getAssetIdByFileName(StringTableEntry fileName);
static U32 getAssetById(StringTableEntry assetId, AssetPtr<SoundAsset>* materialAsset);
static U32 getAssetByFileName(StringTableEntry fileName, AssetPtr<SoundAsset>* matAsset);
protected:
virtual void initializeAsset(void);
@ -143,7 +146,7 @@ class GuiInspectorTypeSoundAssetPtr : public GuiInspectorTypeFileName
typedef GuiInspectorTypeFileName Parent;
public:
GuiBitmapButtonCtrl* mSoundButton;
GuiBitmapButtonCtrl* mEditButton;
DECLARE_CONOBJECT(GuiInspectorTypeSoundAssetPtr);
static void consoleInit();
@ -168,14 +171,14 @@ public:
/// Declares a sound asset
/// This establishes the assetId, asset and legacy filepath fields, along with supplemental getter and setter functions
/// </Summary>
#define DECLARE_SOUNDASSET(className, name, profile) public: \
#define DECLARE_SOUNDASSET(className, name) public: \
Resource<SFXResource> m##name;\
StringTableEntry m##name##Name; \
StringTableEntry m##name##AssetId;\
AssetPtr<SoundAsset> m##name##Asset = NULL;\
SFXProfile* m##name##Profile = &profile;\
SFXProfile* m##name##Profile = NULL;\
public: \
const StringTableEntry get##name##File() const { return m##name##Name); }\
const StringTableEntry get##name##File() const { return m##name##Name; }\
void set##name##File(const FileName &_in) { m##name##Name = StringTable->insert(_in.c_str());}\
const AssetPtr<SoundAsset> & get##name##Asset() const { return m##name##Asset; }\
void set##name##Asset(const AssetPtr<SoundAsset> &_in) { m##name##Asset = _in;}\
@ -206,7 +209,7 @@ public: \
}\
else\
{\
StringTableEntry assetId = SoundAsset::getAssetIdByFilename(_in);\
StringTableEntry assetId = SoundAsset::getAssetIdByFileName(_in);\
if (assetId != StringTable->EmptyString())\
{\
m##name##AssetId = assetId;\
@ -232,9 +235,9 @@ public: \
m##name = NULL;\
}\
\
if (m##name##Asset.notNull() && m##name##Asset->getStatus() != ShapeAsset::Ok)\
if (m##name##Asset.notNull() && m##name##Asset->getStatus() != SoundAsset::Ok)\
{\
Con::errorf("%s(%s)::_set%s() - sound asset failure\"%s\" due to [%s]", macroText(className), getName(), macroText(name), _in, ShapeAsset::getAssetErrstrn(m##name##Asset->getStatus()).c_str());\
Con::errorf("%s(%s)::_set%s() - sound asset failure\"%s\" due to [%s]", macroText(className), getName(), macroText(name), _in, SoundAsset::getAssetErrstrn(m##name##Asset->getStatus()).c_str());\
return false; \
}\
else if (bool(m##name) == NULL)\

View file

@ -1412,8 +1412,10 @@ void AssetImporter::processImportAssets(AssetImportObject* assetItem)
{
processShapeAsset(item);
}
/*else if (item->assetType == String("SoundAsset"))
SoundAsset::prepareAssetForImport(this, item);*/
else if (item->assetType == String("SoundAsset"))
{
processSoundAsset(item);
}
else if (item->assetType == String("MaterialAsset"))
{
processMaterialAsset(item);
@ -1462,8 +1464,10 @@ void AssetImporter::processImportAssets(AssetImportObject* assetItem)
{
processShapeAsset(childItem);
}
/*else if (item->assetType == String("SoundAsset"))
SoundAsset::prepareAssetForImport(this, item);*/
else if (childItem->assetType == String("SoundAsset"))
{
processSoundAsset(childItem);
}
else if (childItem->assetType == String("MaterialAsset"))
{
processMaterialAsset(childItem);
@ -2046,93 +2050,12 @@ void AssetImporter::processShapeMaterialInfo(AssetImportObject* assetItem, S32 m
void AssetImporter::processSoundAsset(AssetImportObject* assetItem)
{
dSprintf(importLogBuffer, sizeof(importLogBuffer), "Preparing Image for Import: %s", assetItem->assetName.c_str());
dSprintf(importLogBuffer, sizeof(importLogBuffer), "Preparing Sound for Import: %s", assetItem->assetName.c_str());
activityLog.push_back(importLogBuffer);
if ((activeImportConfig->GenerateMaterialOnImport && assetItem->parentAssetItem == nullptr)/* || assetItem->parentAssetItem != nullptr*/)
{
//find our suffix match, if any
String noSuffixName = assetItem->assetName;
String suffixType;
String suffix = parseImageSuffixes(assetItem->assetName, &suffixType);
if (suffix.isNotEmpty())
{
assetItem->imageSuffixType = suffixType;
S32 suffixPos = assetItem->assetName.find(suffix, 0, String::NoCase | String::Left);
noSuffixName = assetItem->assetName.substr(0, suffixPos);
}
//We try to automatically populate materials under the naming convention: materialName: Rock, image maps: Rock_Albedo, Rock_Normal, etc
AssetImportObject* materialAsset = findImportingAssetByName(noSuffixName);
if (materialAsset != nullptr && materialAsset->assetType != String("MaterialAsset"))
{
//We may have a situation where an asset matches the no-suffix name, but it's not a material asset. Ignore this
//asset item for now
materialAsset = nullptr;
}
//If we didn't find a matching material asset in our current items, we'll make one now
if (materialAsset == nullptr)
{
if (!assetItem->filePath.isEmpty())
{
materialAsset = addImportingAsset("MaterialAsset", assetItem->filePath, nullptr, noSuffixName);
}
}
//Not that, one way or another, we have the generated material asset, lets move on to associating our image with it
if (materialAsset != nullptr && materialAsset != assetItem->parentAssetItem)
{
if (assetItem->parentAssetItem != nullptr)
{
//If the image had an existing parent, it gets removed from that parent's child item list
assetItem->parentAssetItem->childAssetItems.remove(assetItem);
}
else
{
//If it didn't have one, we're going to pull it from the importingAssets list
importingAssets.remove(assetItem);
}
//Now we can add it to the correct material asset
materialAsset->childAssetItems.push_back(assetItem);
assetItem->parentAssetItem = materialAsset;
assetHeirarchyChanged = true;
}
//Now to do some cleverness. If we're generating a material, we can parse like assets being imported(similar filenames) but different suffixes
//If we find these, we'll just populate into the original's material
//if we need to append the diffuse suffix and indeed didn't find a suffix on the name, do that here
if (suffixType.isEmpty())
{
if (activeImportConfig->UseDiffuseSuffixOnOriginImage)
{
String diffuseToken = StringUnit::getUnit(activeImportConfig->DiffuseTypeSuffixes, 0, ",;\t");
assetItem->assetName = assetItem->assetName + diffuseToken;
assetItem->cleanAssetName = assetItem->assetName;
}
else
{
//We need to ensure that our image asset doesn't match the same name as the material asset, so if we're not trying to force the diffuse suffix
//we'll give it a generic one
if ((materialAsset && materialAsset->assetName.compare(assetItem->assetName) == 0) || activeImportConfig->AlwaysAddImageSuffix)
{
assetItem->assetName = assetItem->assetName + activeImportConfig->AddedImageSuffix;
assetItem->cleanAssetName = assetItem->assetName;
}
}
//Assume for abledo if it has no suffix matches
assetItem->imageSuffixType = "Albedo";
}
}
assetItem->processed = true;
}
//
// Validation
//

View file

@ -94,22 +94,23 @@ ColorI SFXEmitter::smRenderColorRangeSphere( 200, 0, 0, 90 );
SFXEmitter::SFXEmitter()
: SceneObject(),
mSource( NULL ),
mTrack( NULL ),
mUseTrackDescriptionOnly( false ),
mLocalProfile( &mDescription ),
mPlayOnAdd( true )
{
mTypeMask |= MarkerObjectType;
mNetFlags.set( Ghostable | ScopeAlways );
mDescription.mIs3D = true;
mDescription.mIsLooping = true;
mDescription.mIsStreaming = false;
mDescription.mFadeInTime = -1.f;
mDescription.mFadeOutTime = -1.f;
mLocalProfile.mFilename = StringTable->EmptyString();
mLocalProfile._registerSignals();
INIT_SOUNDASSET(Sound);
mObjBox.minExtents.set( -1.f, -1.f, -1.f );
mObjBox.maxExtents.set( 1.f, 1.f, 1.f );
}
@ -174,15 +175,17 @@ void SFXEmitter::consoleInit()
void SFXEmitter::initPersistFields()
{
addGroup( "Media" );
addField( "track", TypeSFXTrackName, Offset( mTrack, SFXEmitter),
INITPERSISTFIELD_SOUNDASSET(Sound, SFXEmitter, "");
/*addField("track", TypeSFXTrackName, Offset(mTrack, SFXEmitter),
"The track which the emitter should play.\n"
"@note If assigned, this field will take precedence over a #fileName that may also be assigned to the "
"emitter." );
addField( "fileName", TypeStringFilename, Offset( mLocalProfile.mFilename, SFXEmitter),
"The sound file to play.\n"
"Use @b either this property @b or #track. If both are assigned, #track takes precendence. The primary purpose of this "
"field is to avoid the need for the user to define SFXTrack datablocks for all sounds used in a level." );
"field is to avoid the need for the user to define SFXTrack datablocks for all sounds used in a level." );*/
endGroup( "Media");
@ -287,12 +290,13 @@ U32 SFXEmitter::packUpdate( NetConnection *con, U32 mask, BitStream *stream )
stream->writeAffineTransform( mObjToWorld );
// track
if( stream->writeFlag( mDirty.test( Track ) ) )
sfxWrite( stream, mTrack );
PACK_SOUNDASSET(con, Sound);
//if (stream->writeFlag(mDirty.test(Track)))
// sfxWrite( stream, mTrack );
// filename
if( stream->writeFlag( mDirty.test( Filename ) ) )
stream->writeString( mLocalProfile.mFilename );
//if( stream->writeFlag( mDirty.test( Filename ) ) )
// stream->writeString( mLocalProfile.mFilename );
// volume
if( stream->writeFlag( mDirty.test( Volume ) ) )
@ -397,7 +401,8 @@ void SFXEmitter::unpackUpdate( NetConnection *conn, BitStream *stream )
}
// track
if ( _readDirtyFlag( stream, Track ) )
UNPACK_SOUNDASSET(conn, Sound);
/*if (_readDirtyFlag(stream, Track))
{
String errorStr;
if( !sfxReadAndResolve( stream, &mTrack, errorStr ) )
@ -406,7 +411,7 @@ void SFXEmitter::unpackUpdate( NetConnection *conn, BitStream *stream )
// filename
if ( _readDirtyFlag( stream, Filename ) )
mLocalProfile.mFilename = stream->readSTString();
mLocalProfile.mFilename = stream->readSTString();*/
// volume
if ( _readDirtyFlag( stream, Volume ) )
@ -586,8 +591,8 @@ void SFXEmitter::inspectPostApply()
// Parent will call setScale so sync up scale with distance.
F32 maxDistance = mDescription.mMaxDistance;
if( mUseTrackDescriptionOnly && mTrack )
maxDistance = mTrack->getDescription()->mMaxDistance;
if( mUseTrackDescriptionOnly && mSoundAsset )
maxDistance = mSoundAsset->getSfxDescription()->mMaxDistance;
mObjScale.set( maxDistance, maxDistance, maxDistance );
@ -608,8 +613,8 @@ bool SFXEmitter::onAdd()
mDescription.validate();
// Read an old 'profile' field for backwards-compatibility.
if( !mTrack )
/*
if(mSoundAsset.isNull() || !mSoundAsset->getSfxProfile())
{
static const char* sProfile = StringTable->insert( "profile" );
const char* profileName = getDataField( sProfile, NULL );
@ -643,7 +648,7 @@ bool SFXEmitter::onAdd()
// Remove the old 'channel' field.
setDataField( sChannel, NULL, "" );
}
}
}*/
}
else
{
@ -683,6 +688,12 @@ void SFXEmitter::_update()
// we can restore it.
SFXStatus prevState = mSource ? mSource->getStatus() : SFXStatusNull;
if (mSoundAsset.notNull() )
{
mLocalProfile = *mSoundAsset->getSfxProfile();
mDescription = *mSoundAsset->getSfxDescription();
}
// Make sure all the settings are valid.
mDescription.validate();
@ -695,12 +706,12 @@ void SFXEmitter::_update()
SFX_DELETE( mSource );
// Do we have a track?
if( mTrack )
if( mSoundAsset && mSoundAsset->getSfxProfile() )
{
mSource = SFX->createSource( mTrack, &transform, &velocity );
mSource = SFX->createSource(mSoundAsset->getSfxProfile(), &transform, &velocity );
if( !mSource )
Con::errorf( "SFXEmitter::_update() - failed to create sound for track %i (%s)",
mTrack->getId(), mTrack->getName() );
mSoundAsset->getSfxProfile()->getId(), mSoundAsset->getSfxProfile()->getName() );
// If we're supposed to play when the emitter is
// added to the scene then also restart playback
@ -739,12 +750,12 @@ void SFXEmitter::_update()
// is toggled on a local profile sound. It makes the
// editor feel responsive and that things are working.
if( gEditingMission &&
!mTrack &&
(mSoundAsset.isNull() || !mSoundAsset->getSfxProfile()) &&
mPlayOnAdd &&
mDirty.test( IsLooping ) )
prevState = SFXStatusPlaying;
bool useTrackDescriptionOnly = ( mUseTrackDescriptionOnly && mTrack );
bool useTrackDescriptionOnly = ( mUseTrackDescriptionOnly && mSoundAsset.notNull() && mSoundAsset->getSfxProfile());
// The rest only applies if we have a source.
if( mSource )
@ -1087,8 +1098,8 @@ SFXStatus SFXEmitter::_getPlaybackStatus() const
bool SFXEmitter::is3D() const
{
if( mTrack != NULL )
return mTrack->getDescription()->mIs3D;
if( mSoundAsset.notNull() && mSoundAsset->getSfxProfile() != NULL )
return mSoundAsset->getSfxProfile()->getDescription()->mIs3D;
else
return mDescription.mIs3D;
}
@ -1124,8 +1135,8 @@ void SFXEmitter::setScale( const VectorF &scale )
{
F32 maxDistance;
if( mUseTrackDescriptionOnly && mTrack )
maxDistance = mTrack->getDescription()->mMaxDistance;
if( mUseTrackDescriptionOnly && mSoundAsset.notNull() && mSoundAsset->getSfxProfile())
maxDistance = mSoundAsset->getSfxProfile()->getDescription()->mMaxDistance;
else
{
// Use the average of the three coords.

View file

@ -36,6 +36,7 @@
#include "gfx/gfxStateBlock.h"
#endif
#include "T3D/assets/SoundAsset.h"
class SFXSource;
class SFXTrack;
@ -103,13 +104,11 @@ class SFXEmitter : public SceneObject
/// The current dirty flags.
BitSet32 mDirty;
DECLARE_SOUNDASSET(SFXEmitter, Sound);
DECLARE_SOUNDASSET_NET_SETGET(SFXEmitter, Sound, DirtyUpdateMask);
/// The sound source for the emitter.
SFXSource *mSource;
/// The selected track or null if the local
/// profile should be used.
SFXTrack *mTrack;
/// Whether to leave sound setup exclusively to the assigned mTrack and not
/// override part of the track's description with emitter properties.
bool mUseTrackDescriptionOnly;

View file

@ -38,7 +38,7 @@ function AssetBrowser::onSoundAssetEditorDropped(%this, %assetDef, %position)
%newSFXEmitter = new SFXEmitter()
{
position = %pos;
fileName = %assetDef.getSoundPath();
soundAsset = %assetDef.getAssetId();
pitch = %assetDef.pitchAdjust;
volume = %assetDef.volumeAdjust;
};
@ -50,4 +50,9 @@ function AssetBrowser::onSoundAssetEditorDropped(%this, %assetDef, %position)
EWorldEditor.isDirty = true;
}
function AssetBrowser::editSoundAsset(%this, %assetDef)
{
%soundSource = %assetDef.playSound();
}