Skip to content

Instantly share code, notes, and snippets.

@AndrewCarvalho
Last active November 25, 2020 03:22
Show Gist options
  • Save AndrewCarvalho/5b66cbdea135431a8d4e226888ebc7d4 to your computer and use it in GitHub Desktop.
Save AndrewCarvalho/5b66cbdea135431a8d4e226888ebc7d4 to your computer and use it in GitHub Desktop.

FMOD Constants Generator For Unity

A CodeDom based code generation tool for reading the GUIDs.txt output by FMOD Studio and creating a FMODConstants.cs file containing nested classes that organize Events, Busses and Banks into easily accessible nexted classes containing their string paths and GUID struct properties.

Also included is a short CodeGeneration static class containing some CodeDom helper methods.

Usage

To use, select the GUIDs.txt (the name doesn't matter so renamed files will also work) and select Tools > LaundryBear > Generate Fmod Events Constants File and the tool will generate a FMODConstants.cs file in the same folder as the GUIDs.txt file. You can then move this to wherever makes sense in your project structure.

The generator creates a single FMODConstants.cs file containing an FMODConstants class, with three nested classes for Events, Busses and Banks. Each of the nested classes then conaints additional nested classes based on the event, bus and bank paths in the FMOD Studio project.

Example

Given the GUID.txt file:

{c656f701-b0b3-4f6c-af7e-3ade7e01ffbd} event:/Music/Scene1/AmbientMusicEvent

{c656f701-b0b3-4f6c-af7e-3ade7e01ffbd} event:/Music/Scene2/AmbientMusicEvent

{c656f701-b0b3-4f6c-af7e-3ade7e01ffbd} event:/SFX/Scene1/Rock/RockSmashEvent

{c656f701-b0b3-4f6c-af7e-3ade7e01ffbd} event:/SFX/ErrorEvent

{c656f701-b0b3-4f6c-af7e-3ade7e01ffbd} event:/SFX/UI/CancelEvent

{c656f701-b0b3-4f6c-af7e-3ade7e01ffbd} bus:/

{c656f701-b0b3-4f6c-af7e-3ade7e01ffbd} bus:/Reverb

{c656f701-b0b3-4f6c-af7e-3ade7e01ffbd} bank:/Music Bank

{c656f701-b0b3-4f6c-af7e-3ade7e01ffbd} bank:/Sounds Bank

The tool would generate a file containing the following constant strings containing the full path and System.GUID properties containing the FMOD GUIDs for each line.

string FMODConstants.Events.Music.Scene1.AmbientEventPath;
System.GUID FMODConstants.Events.Music.Scene1.AmbientEvent;
string FMODConstants.Events.Music.Scene2.AmbientEventPath;
System.GUID FMODConstants.Events.Music.Scene2.AmbientEvent;
string FMODConstants.Events.SFX.Scene1.RockSmashEventPath;
System.GUID FMODConstants.Events.SFX.Scene1.Rock.RockSmashEvent;
string FMODConstants.Events.SFX.ErrorEventPath;
System.GUID FMODConstants.Events.SFX.ErrorEvent;
string FMODConstants.Events.SFX.UI.CancelEventPath;
System.GUID FMODConstants.Events.UI.CancelEvent;
string FMODConstants.Busses.MasterPath;
System.GUID FMODConstants.Busses.Master;
string FMODConstants.Busses.ReverbPath;
System.GUID FMODConstants.Busses.Reverb;
string FMODConstants.Banks.MasterBankPath;
System.GUID FMODConstants.Banks.MasterBank;
string FMODConstants.Banks.SoundsBankPath;
System.GUID FMODConstants.Banks.SoundsBank;

And wold be used at runtime using the call:

FMODUnity.RuntimeManager.PlayOneShot( FMODConstants.Events.SFX.Scene1.RockSmashEventPath );

Features

  • Automatic generation of Paths and GUIDs in a structured way
  • Does not auto remap GUIDs if evenst/busses/banks are renamed, but will at least give you compile errors
  • No more need to hardcoded string literals!

License

Copyright( c) 2017 Andrew Carvalho, Laundry Bear Games Inc.

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.
//Copyright( c) 2017 Andrew Carvalho, Laundry Bear Games Inc.
//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.
using System;
using System.CodeDom;
using System.Reflection;
using System.Text;
public static class CodeGeneration
{
public const string VALID_NAME_CHARS = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890_";
public static CodeTypeDeclaration CreateSealedPrivateClass( string name )
{
CodeTypeDeclaration codeClass = new CodeTypeDeclaration( name );
codeClass.TypeAttributes = TypeAttributes.Sealed;
codeClass.Attributes &= MemberAttributes.AccessMask;
codeClass.Attributes |= MemberAttributes.Public;
// the below creates a private constructor preventing instantiation. Abstract can't be used since the class is sealed.
codeClass.Members.Add( new CodeConstructor
{
Attributes = MemberAttributes.Private | MemberAttributes.Final
} );
return codeClass;
}
public static CodeTypeDeclaration CreateSealedPrivateStruct( string name )
{
CodeTypeDeclaration codeClass = new CodeTypeDeclaration( name );
codeClass.IsStruct = true;
return codeClass;
}
public static CodeMemberField CreatePrivateMember( Type type, string name )
{
CodeMemberField member = new CodeMemberField( type, name );
member.Attributes &= ~MemberAttributes.AccessMask;
member.Attributes |= ~MemberAttributes.Private;
return member;
}
public static CodeMemberField CreateConstMember( Type type, string name )
{
CodeMemberField member = new CodeMemberField( type, name );
member.Attributes &= ~MemberAttributes.AccessMask;
member.Attributes &= ~MemberAttributes.ScopeMask;
member.Attributes |= MemberAttributes.Public;
member.Attributes |= MemberAttributes.Const;
return member;
}
public static CodeMemberProperty CreateReadonlyPropertyMember( Type type, string name )
{
CodeMemberProperty member = new CodeMemberProperty();
member.Name = name;
member.Type = new CodeTypeReference( type.FullName );
member.Attributes &= ~MemberAttributes.AccessMask;
member.Attributes |= MemberAttributes.Static;
member.Attributes |= MemberAttributes.Public;
return member;
}
public static CodeSnippetExpression ConstructBitshiftExpression( int shiftDirection, int shiftAmount )
{
string bitshiftOperator = System.Math.Sign(shiftDirection) > 0 ? "<<" : ">>";
return new CodeSnippetExpression( string.Format( "1 {0} {1}", bitshiftOperator, shiftAmount ) );
}
public static string ConvertToValidIdentifier( string name, string replaceString = "" )
{
var sb = new StringBuilder(name.Length + 1);
if ( !char.IsLetter( name[0] ) )
sb.Append( replaceString );
var makeUpper = false;
foreach ( var ch in name )
{
if ( IsValidCharacter( ch ) )
{
sb.Append( makeUpper
? char.ToUpperInvariant( ch )
: ch );
makeUpper = false;
}
else if ( char.IsWhiteSpace( ch ) )
{
makeUpper = true;
}
else
{
sb.Append( replaceString );
}
}
return sb.ToString();
}
private static bool IsValidCharacter( char character )
{
return VALID_NAME_CHARS.Contains( char.ToString( character ) );
}
}
//Copyright( c) 2017 Andrew Carvalho, Laundry Bear Games Inc.
//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.
using Microsoft.CSharp;
using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using UnityEditor;
using UnityEngine;
public class GenerateFmodGuids : Editor
{
public class FmodConstant
{
public enum ConstantType { Event, Bus, Bank }
public ConstantType FmodType { get; private set; }
public Guid Guid { get; private set; }
public string Path { get; private set; }
public FmodConstant( Guid guid, string path, ConstantType type )
{
Guid = guid;
Path = path;
FmodType = type;
}
public string GetParentPath()
{
return Path.Substring( 0, Path.LastIndexOf( @"/" ) );
if ( Path.EndsWith( @"/" ) )
{
return Path.Substring( 0, Path.Length - 1 );
}
return System.IO.Path.GetDirectoryName( Path );
}
public string GetEventName()
{
string eventName = System.IO.Path.GetFileName( Path );
// empty bus is valid and is a special case for the master bus
if ( string.IsNullOrEmpty( eventName ) && FmodType == ConstantType.Bus )
{
eventName = "Master";
}
return eventName;
}
}
private const int GUID_INDEX = 0;
private const int PATH_INDEX = 1;
private const string FMOD_PATH_EVENT_KEY = "event";
private const string FMOD_PATH_BUS_KEY = "bus";
private const string FMOD_PATH_BANK_KEY = "bank";
[MenuItem( "Tools/Laundry Bear/Generate Fmod Events Constants File" )]
public static void CreateEventsFile()
{
string guidFilePath = AssetDatabase.GetAssetPath( Selection.activeObject.GetInstanceID() );
CreateLayersClass( guidFilePath );
}
[MenuItem( "Tools/Laundry Bear/Generate Fmod Events Constants File", true )]
public static bool ValidateCreateEventsFile()
{
return Selection.activeObject is TextAsset;
}
private static FmodConstant[] GetConstantList( string path )
{
List<FmodConstant> guidPathList = new List<FmodConstant>();
using ( StreamReader reader = new StreamReader( path ) )
{
while ( !reader.EndOfStream )
{
string guidPath = reader.ReadLine();
string[] splitGuidPath = guidPath.Split( new char[] { ' ' }, 2 );
Guid guid = new Guid( splitGuidPath[ GUID_INDEX ] );
string guidTypeName = splitGuidPath[ PATH_INDEX ].Split( new char[] { ':' }, 2 )[0];
switch ( guidTypeName )
{
case FMOD_PATH_EVENT_KEY:
guidPathList.Add( new FmodConstant( guid, splitGuidPath[PATH_INDEX].Trim(), FmodConstant.ConstantType.Event ) );
break;
case FMOD_PATH_BUS_KEY:
guidPathList.Add( new FmodConstant( guid, splitGuidPath[PATH_INDEX].Trim(), FmodConstant.ConstantType.Bus ) );
break;
case FMOD_PATH_BANK_KEY:
guidPathList.Add( new FmodConstant( guid, splitGuidPath[PATH_INDEX].Trim(), FmodConstant.ConstantType.Bank ) );
break;
default:
break;
}
}
}
return guidPathList.ToArray();
}
static void CreateLayersClass( string path )
{
CodeCompileUnit codeCompileUnit = new CodeCompileUnit();
CodeNamespace codeNamespace = new CodeNamespace();
FmodConstant[] constants = GetConstantList( path );
// create classes for events, busses and banks
CodeTypeDeclaration codeEventClass = CodeGeneration.CreateSealedPrivateClass( CodeGeneration.ConvertToValidIdentifier( "Events" ) );
CodeTypeDeclaration codeBusClass = CodeGeneration.CreateSealedPrivateClass( CodeGeneration.ConvertToValidIdentifier( "Busses" ) );
CodeTypeDeclaration codeBankClass = CodeGeneration.CreateSealedPrivateClass( CodeGeneration.ConvertToValidIdentifier( "Banks" ) );
// create a dictionary to hold all the class declarations
Dictionary<string, CodeTypeDeclaration> classDictionary = new Dictionary<string, CodeTypeDeclaration>();
foreach ( FmodConstant constant in constants )
{
CodeTypeDeclaration codeClass = null;
switch ( constant.FmodType )
{
case FmodConstant.ConstantType.Event:
codeClass = codeEventClass;
break;
case FmodConstant.ConstantType.Bus:
codeClass = codeBusClass;
break;
case FmodConstant.ConstantType.Bank:
codeClass = codeBankClass;
break;
}
if ( codeClass == null )
{
continue;
}
CodeTypeDeclaration containingClass = GetOrCreateClassDeclaration( constant.GetParentPath(), classDictionary, codeClass );
string backingGuidName = string.Format( "sm_{0}", constant.GetEventName() );
backingGuidName = CodeGeneration.ConvertToValidIdentifier( backingGuidName );
CodeMemberField codeGuidBackingFieldMember = CodeGeneration.CreatePrivateMember( typeof( Guid ), CodeGeneration.ConvertToValidIdentifier( backingGuidName ) );
codeGuidBackingFieldMember.Attributes &= ~MemberAttributes.AccessMask;
codeGuidBackingFieldMember.Attributes &= ~MemberAttributes.ScopeMask;
codeGuidBackingFieldMember.Attributes |= MemberAttributes.Private;
codeGuidBackingFieldMember.Attributes |= MemberAttributes.Static;
codeGuidBackingFieldMember.InitExpression = new CodeObjectCreateExpression( typeof( Guid ).FullName, new CodeExpression[] { new CodePrimitiveExpression( constant.Guid.ToString() ) } );
containingClass.Members.Add( codeGuidBackingFieldMember );
CodeMemberProperty codeGuidPropertyMember = CodeGeneration.CreateReadonlyPropertyMember( typeof ( Guid ), CodeGeneration.ConvertToValidIdentifier( constant.GetEventName() ) );
codeGuidPropertyMember.GetStatements.Add( new CodeMethodReturnStatement( new CodeFieldReferenceExpression() { FieldName = backingGuidName } ) );
containingClass.Members.Add( codeGuidPropertyMember );
CodeMemberField codePathMember = CodeGeneration.CreateConstMember( typeof ( string ), CodeGeneration.ConvertToValidIdentifier( string.Format( "{0}{1}", constant.GetEventName(), "Path" ) ) );
codePathMember.InitExpression = new CodePrimitiveExpression( constant.Path );
containingClass.Members.Add( codePathMember );
}
CodeTypeDeclaration codeConstantsClass = CodeGeneration.CreateSealedPrivateClass( "FMODConstants" );
codeConstantsClass.Members.Add( codeEventClass );
codeConstantsClass.Members.Add( codeBusClass );
codeConstantsClass.Members.Add( codeBankClass );
codeNamespace.Types.Add( codeConstantsClass );
codeCompileUnit.Namespaces.Add( codeNamespace );
string outputPath = AssetDatabase.GetAssetPath(Selection.activeObject);
outputPath = Path.GetDirectoryName( outputPath ) + "/FmodConstants.cs";
using ( var stream = new StreamWriter( outputPath, append: false ) )
{
var tw = new IndentedTextWriter(stream);
var codeProvider = new CSharpCodeProvider();
codeProvider.GenerateCodeFromCompileUnit( codeCompileUnit, tw, new CodeGeneratorOptions() { BracingStyle = "C", BlankLinesBetweenMembers = false } );
}
AssetDatabase.ImportAsset( path, ImportAssetOptions.ForceUpdate );
AssetDatabase.Refresh();
Debug.Log( "Completed FmodConstants.cs generation." );
}
private static CodeTypeDeclaration GetOrCreateClassDeclaration( string path, Dictionary<string, CodeTypeDeclaration> classDictionary, CodeTypeDeclaration containingClass )
{
if ( path.IndexOf( @"/" ) < 0 )
{
return containingClass;
}
CodeTypeDeclaration classDeclaration;
classDictionary.TryGetValue( path, out classDeclaration );
if ( classDeclaration == null )
{
string parentPath = path.Substring( 0, path.LastIndexOf(@"/") );
int start = path.LastIndexOf(@"/");
string localPath = path.Substring( start, path.Length - start );
classDeclaration = new CodeTypeDeclaration( CodeGeneration.ConvertToValidIdentifier( localPath ) );
// if this class has an encapsuating class
if ( parentPath.IndexOf( @"/" ) > -1 )
{
// get encapsulating class
CodeTypeDeclaration parentClassDeclaration = GetOrCreateClassDeclaration( parentPath, classDictionary, containingClass );
// add this class as a member
parentClassDeclaration.Members.Add( classDeclaration );
}
else
{
// no encapsulating class, add it to the containing class argument
containingClass.Members.Add( classDeclaration );
}
classDictionary.Add( path, classDeclaration );
}
return classDeclaration;
}
private static string GetTrimmedPathFromFullPath( string fmodPath )
{
return fmodPath.Split( new char[] { ':' }, 2 )[1];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment