/*
 * file: loadingmusic2.as
 *
 * Loading Music II.
 *
 * Ported from AMXx plugin "Loading Music II v1.2.9"
 * (c) Copyright 2006-2007, Simon Logic 'slspam@land.ru'.
 */
/*
 * About: MIT License
 *
 * Copyright (c) 2021 Anggara Yama Putra
 *
 * 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.
 */


/*
 * namespace: AN::LOADINGMUSIC2
 *
 * --- C++
 * namespace AN::LOADINGMUSIC2 {...}
 * ---
 */
namespace AN{namespace LOADINGMUSIC2{

/*
 * string: _VERSION
 */
const string _VERSION = "2021-03-28.0001";

/*
 * obj: g_LibDebug
 *
 * Global instance of <AN::LIBDEBUG::CLibDebug>.
 */
const AN::LIBDEBUG::CLibDebug g_LibDebug( "LMII." + _VERSION );

/*
 * enum: LoopMode
 *
 * --- C++
 * enum AN::LOADINGMUSIC2::LoopMode {...}
 * ---
 *
 * LOOP_NO          - Play mp3/wav until a player starts to play.
 * LOOP_MP3_SPEC    - Play mp3 once till the end; play wav as described above.
 * LOOP_4EVER       - Play mp3/wav forever (strongly do not recommend if
 *                    <AN::LOADINGMUSIC2::TrackLoadingFlags::FL_FORCE_MP3_ON_CONNECT>
 *                    is not set).
 */
enum LoopMode
{
    LOOP_NO         = 0
    , LOOP_MP3_SPEC = 1
    , LOOP_4EVER    = 2
}

/*
 * enum: ForceMode
 *
 * --- C++
 * enum AN::LOADINGMUSIC2::ForceMode {...}
 * ---
 *
 * FORCE_NONE   - Allow to play any tracks.
 * FORCE_MP3    - Only MP3 tracks are allowed to play.
 * FORCE_WAV    - Only WAV tracks are allowed to play.
 */
enum ForceMode
{
    FORCE_NONE  = 0
    , FORCE_MP3 = 1
    , FORCE_WAV = 2
}

/*
 * enum: TrackLoadingFlags
 *
 * --- C++
 * enum AN::LOADINGMUSIC2::TrackLoadingFlags {...}
 * ---
 *
 * FL_FORCE_MP3_ON_CONNECT  - (Flag 'a') play mp3 files only on player connection/spectating; otherwise
 *                            wav files are possible (strongly do not recommend to turn this flag
 *                            OFF because you may get an unstoppable ambient cyclic sound during
 *                            the game').
 * FL_PLAY_ON_SPECTATE      - (Flag 'b') play mp3 on spectate; otherwise stop playing track when player
 *                            goes to spectator.
 * FL_FORCE_WAV_ON_SELECT   - (Flag 'c')(CS specific) play wav files only on team select; otherwise
 *                            connection track will continue playing (under CS it can be turned off
 *                            by the game; try using flag 'd' to fix this issue).
 * FL_RESTART_ON_SELECT     - (Flag 'd')(CS specific) restart a track on team select to fix a CS bug;
 *                            has minor priority than flag 'c'.
 * FL_DONT_PLAY_ON_DEAD     - (Flag 'e') don't play a spectator track on dead players (consider to use it
 *                            if you enabled flag 'b').
 */
enum TrackLoadingFlags
{
    FL_FORCE_MP3_ON_CONNECT     = (1 << 0)
    , FL_PLAY_ON_SPECTATE       = (1 << 1)
    , FL_FORCE_WAV_ON_SELECT    = (1 << 2)
    , FL_RESTART_ON_SELECT      = (1 << 3)
    , FL_DONT_PLAY_ON_DEAD      = (1 << 4)
}

/*
 * enum: SoundEngineTypes
 *
 * --- C++
 * enum AN::LOADINGMUSIC2::SoundEngineTypes {...}
 * ---
 *
 * SE_NONE  - None.
 * SE_MP3   - mp3: client cmd 'mp3 play'.
 * SE_SPK   - wav: client cmd 'spk'.
 * SE_EMIT  - wav: EmitSound().
 */
enum SoundEngineTypes
{
    SE_NONE  = 0,
    SE_MP3,
    SE_SPK,
    SE_EMIT
}

/*
 * float: ON_TEAMSELECT_DELAY_WAV
 *
 * delay to start a track on team select (experimental values)...
 */
const float ON_TEAMSELECT_DELAY_WAV = 0.2;

/*
 * float: ON_TEAMSELECT_DELAY_MP3
 *
 * delay to start a track on team select (experimental values)...
 */
const float ON_TEAMSELECT_DELAY_MP3 = 0.3;

/*
 * int: EMIT_WAV_CHANNEL
 *
 * do not change until you know what you're doing!
 */
const SOUND_CHANNEL EMIT_WAV_CHANNEL = CHAN_STATIC;

/*
 * string: SOUND_DIR
 *
 * it's also a precache dir.
 */
const string SOUND_DIR = "sound";

/*
 * string: WAV_EXTENSION
 *
 * .WAV file extension.
 */
const string WAV_EXTENSION = "wav";

/*
 * struct: AN::LOADINGMUSIC2::CTrackStruct
 *
 * --- C++
 * final class AN::LOADINGMUSIC2::CTrackStruct {...}
 * ---
 *
 * track struct.
 */
final class CTrackStruct
{

    /*
     * bool: m_bIsWav
     *
     * file is wav.
     */
    bool m_bIsWav;
    /*
     * bool: m_bShouldPrecache
     *
     * file should be precached.
     */
    bool m_bShouldPrecache;
    /*
     * bool: m_bIsPrecached
     *
     * file is cached.
     */
    bool m_bIsPrecached;
    /*
     * bool: m_bIsExists
     *
     * file exists on server.
     */
    bool m_bIsExists;
    /*
     * string: m_szFilePath
     *
     * Track file path.
     */
    string m_szFilePath;

    /*
     * method: opCmp
     *
     * Compare operand to let this struct searchable with given file path.
     */
    int opCmp (const CTrackStruct &in tTrack) const
    {
        return this.m_szFilePath.ICompare( tTrack.m_szFilePath );
    }

}

/*
 * struct: AN::LOADINGMUSIC2::CPlayerStruct
 *
 * --- C++
 * final class AN::LOADINGMUSIC2::CPlayerStruct {...}
 * ---
 *
 * player struct.
 */
final class CPlayerStruct
{

    /*
     * int: m_iTrackNumber
     *
     * track number playing on player.
     */
    int m_iTrackNumber;
    /*
     * int: m_iSoundEngine
     *
     * Sound engine currently use for playing current track.
     */
    SoundEngineTypes m_iSoundEngine;

}

/*
 * class: AN::LOADINGMUSIC2::CLoadingMusic
 *
 * --- C++
 * final class AN::LOADINGMUSIC2::CLoadingMusic {...}
 * ---
 *
 * Loading Music module.
 */
final class CLoadingMusic
{

    /*
     * constructor: CLoadingMusic
     *
     * Default constructor.
     */
    CLoadingMusic ()
    {
        this.m_szPlaybackMode       = "-1";
        this.m_iLoopMode            = LOOP_NO;
        this.m_flPlayOnConnectDelay = 0.0f;
        this.m_szFlags              = "abc";
    }

    /*
     * constructor: CLoadingMusic
     *
     * Default constructor.
     *
     * Arguments:
     * szPlaybackMode           - Track playback mode, see <AN::LOADINGMUSIC2::CLoadingMusic::set_m_szPlaybackMode> for details.
     * iLoopMode                - Loop mode playback, see <AN::LOADINGMUSIC2::LoopMode> for details.
     * flPlayOnConnectDelay     - Delay before play the track (in seconds) on connecting players.
     * szFlags                  - Track loading flag(s), see <AN::LOADINGMUSIC2::TrackLoadingFlags> for details.
     */
    CLoadingMusic (const string &in szPlaybackMode, const LoopMode iLoopMode, const float flPlayOnConnectDelay, const string &in szFlags)
    {
        this.m_szPlaybackMode       = szPlaybackMode;
        this.m_iLoopMode            = iLoopMode;
        this.m_flPlayOnConnectDelay = flPlayOnConnectDelay;
        this.m_szFlags              = szFlags;
    }

    /*
     * destructor: CLoadingMusic
     *
     * Default destructor.
     */
    ~CLoadingMusic ()
    {
        const string szDebugMsg = "~CLoadingMusic()...";
        g_LibDebug.verboselogln( szDebugMsg );

        this.Stop();
        this.ClearTrackList();

        g_LibDebug.logln( szDebugMsg +"GOOD" );
    }

    /*
     * method: Start
     *
     * Start the module.
     */
    void Start ()
    {
        const string szDebugMsg = "Start()...";
        g_LibDebug.verboselogln( szDebugMsg );

        if (g_LibModule.IsPlugin())
            g_Hooks.RegisterHook( Hooks::Player::ClientConnected, @m_pClientConnectedHook );
        g_Hooks.RegisterHook( Hooks::Player::ClientDisconnect, @m_pClientDisconnectHook );
        g_Hooks.RegisterHook( Hooks::Player::PlayerSpawn, @m_pPlayerSpawnHook );
        g_Hooks.RegisterHook( Hooks::Player::PlayerEnteredObserver, @m_pPlayerEnteredObserverHook );

        g_LibDebug.logln( szDebugMsg +"GOOD" );
    }

    /*
     * method: Stop
     *
     * Stop the module.
     */
    void Stop ()
    {
        const string szDebugMsg = "Stop()...";
        g_LibDebug.verboselogln( szDebugMsg );

        if (g_LibModule.IsPlugin())
            g_Hooks.RemoveHook( Hooks::Player::ClientConnected, @m_pClientConnectedHook );
        g_Hooks.RemoveHook( Hooks::Player::ClientDisconnect, @m_pClientDisconnectHook );
        g_Hooks.RemoveHook( Hooks::Player::PlayerSpawn, @m_pPlayerSpawnHook );
        g_Hooks.RemoveHook( Hooks::Player::PlayerEnteredObserver, @m_pPlayerEnteredObserverHook );

        for (uint i = 0; i < m_rgPlayers.length(); ++i)
        {
            auto@ pPlayer = g_PlayerFuncs.FindPlayerByIndex( i );
            if (pPlayer !is null)
                StopTrack( pPlayer.edict(), true );
        }

        g_LibDebug.logln( szDebugMsg +"GOOD" );
    }

    /*
     * method: AddTrack
     *
     * Add a track file.
     *
     * Arguments:
     * szFilePath       - Track file path (Overwritten same track file, if any).
     * bAllowPrecache   - Allow precaching the file (Must be precached within `MapInit()` to avoid crashes).
     *
     * Returns:
     * true     - Successfully added.
     * false    - Failed to add, see debug messages for details.
     */
    bool AddTrack (const string &in szFilePath, const bool bAllowPrecache = true)
    {
        const string szDebugMsg = g_LibString.GetFormattedString( "AddTrack(szFilePath='%1', bAllowPrecache=%2)...", {szFilePath,bAllowPrecache} );
        g_LibDebug.verboselogln( szDebugMsg );

        CTrackStruct tTrack;
        if (!ParseTrack( szFilePath, bAllowPrecache, tTrack ))
        {
            g_LibDebug.logln( szDebugMsg +"Failed to parse track" );
            return false;
        }

        auto iFound = m_rgTracks.find( tTrack );
        if (iFound < 0)
        {
            g_LibDebug.logln( szDebugMsg +"New track" );
            m_rgTracks.insertLast( tTrack );
            /*if (tTrack.m_bIsWav)
                m_uiWavCount++;
            else
                m_uiMp3Count++;*/
        }
        else
        {
            g_LibDebug.logln( szDebugMsg +"Existing track #"+iFound );
            m_rgTracks[iFound] = tTrack;
        }

        g_LibDebug.verboselogln( szDebugMsg +"m_uiWavCount = "+m_uiWavCount );
        g_LibDebug.verboselogln( szDebugMsg +"m_uiMp3Count = "+m_uiMp3Count );
        g_LibDebug.logln( szDebugMsg +"GOOD" );
        return true;
    }

    /*
     * method: GetTrackList
     *
     * Get read-only track list.
     */
    CTrackStruct[] GetTrackList () const
    {
        return this.m_rgTracks;
    }

    /*
     * method: ClearTrackList
     *
     * Remove all tracks from the list.
     */
    void ClearTrackList ()
    {
        const string szDebugMsg = g_LibString.GetFormattedString( "ClearTrackList()...", {} );
        g_LibDebug.verboselogln( szDebugMsg );

        this.m_rgTracks.resize( 0 );
        this.m_rgTracks.resize( 1 );

        g_LibDebug.logln( szDebugMsg +"GOOD" );
    }

    /*
     * method: PlayTrack
     *
     * Play a track to given player.
     *
     * Arguments:
     * pEdict       - Player edict pointer.
     * iStartTrack  - (Re)play on given track number (-1 = Random/Custom/Selected track).
     * bAllowEmit   - Use <EmitSoundDyn> to play.
     * iForceMode   - Only play on given track format (<FORCE_NONE> = Play any formats).
     */
    void PlayTrack (edict_t@ pEdict, const int iStartTrack = -1, bool bAllowEmit = true, ForceMode iForceMode = FORCE_NONE)
    {
        const string szDebugMsg = g_LibString.GetFormattedString( "PlayTrack(iStartTrack=%1, bAllowEmit=%2, iForceMode=%3)...", {iStartTrack,bAllowEmit,iForceMode} );
        g_LibDebug.verboselogln( szDebugMsg );

        if (pEdict is null)
        {
            g_LibDebug.verboselogln( szDebugMsg +"pEdict is null" );
            return;
        }
        const int iEdictIndex = g_EngineFuncs.IndexOfEdict( @pEdict );
        g_LibDebug.verboselogln( szDebugMsg +"iEdictIndex = "+iEdictIndex );

        int iTrackToPlay = -1;
        if (iStartTrack >= 0)
        {
            iTrackToPlay = iStartTrack; // we're forced to restart a track
        }
        else
        {
            if (m_rgPlayers[iEdictIndex].m_iTrackNumber >= 0)
            {
                g_LibDebug.logln( g_LibString.GetFormattedString(szDebugMsg +"Already playing [%1] on track #%2", {iEdictIndex,m_rgPlayers[iEdictIndex].m_iTrackNumber}) );
                return; // already playing
            }

            g_LibDebug.verboselogln( szDebugMsg +"m_iTrackSelection = "+m_iTrackSelection );
            if (m_iTrackSelection < 0)
            {
                g_LibDebug.verboselogln( szDebugMsg +"m_uiTrackCount = "+m_uiTrackCount );
                // play random track
                if (m_uiTrackCount > 0)
                {
                    iTrackToPlay = Math.RandomLong( 1, m_uiTrackCount );
                }
                else
                {
                    g_LibDebug.verboselogln( szDebugMsg +"Track list is empty" );
                    return;
                }
                g_LibDebug.verboselogln( szDebugMsg +"iTrackToPlay = "+iTrackToPlay );

                if (iForceMode != FORCE_NONE)
                {
                    const int iStart = iTrackToPlay;
                    int iSkip;
                    if(iForceMode == FORCE_MP3)
                    {
                        if (m_uiMp3Count <= 0)
                        {
                            iTrackToPlay = -1; // there is no mp3 file in playlist
                        }
                        else
                        {
                            iSkip = Math.RandomLong( 1, m_uiMp3Count );

                            for (;;)
                            {
                                if (!m_rgTracks[iTrackToPlay].m_bIsWav)
                                    if (--iSkip <= 0)
                                        break;

                                if (++iTrackToPlay > int(m_uiTrackCount))
                                    iTrackToPlay = 1;

                                if (iTrackToPlay == iStart)
                                {
                                    iTrackToPlay = -1; // there is no mp3 file in playlist
                                    break;
                                }
                            }
                        }
                    }
                    else
                    {
                        if (m_uiWavCount <= 0)
                        {
                            iTrackToPlay = -1; // there is no wav file in playlist
                        }
                        else
                        {
                            iSkip = Math.RandomLong( 1, m_uiWavCount );

                            for (;;)
                            {
                                if (m_rgTracks[iTrackToPlay].m_bIsWav)
                                    if (--iSkip <= 0)
                                        break;

                                if (++iTrackToPlay > int(m_uiTrackCount))
                                    iTrackToPlay = 1;

                                if (iTrackToPlay == iStart)
                                {
                                    iTrackToPlay = -1; // there is no wav file in playlist
                                    break;
                                }
                            }
                        }
                    }
                }
            }
            else
            if (m_iTrackSelection == 0)
            {
                // play custom track
                iTrackToPlay = 0;
            }
            else
            if (m_iTrackSelection <= int(m_uiTrackCount))
            {
                // play track by its index
                iTrackToPlay = m_iTrackSelection;
            }
        }

        g_LibDebug.verboselogln( szDebugMsg +"iTrackToPlay = "+iTrackToPlay );
        g_LibDebug.verboselogln( szDebugMsg +"iForceMode = "+iForceMode );
        g_LibDebug.verboselogln( szDebugMsg +"m_bIsWav = "+m_rgTracks[iTrackToPlay].m_bIsWav );

        if (iTrackToPlay < 0
            || (iForceMode == FORCE_MP3 && m_rgTracks[iTrackToPlay].m_bIsWav)
            || (iForceMode == FORCE_WAV && !m_rgTracks[iTrackToPlay].m_bIsWav)
            )
        {
            g_LibDebug.verboselogln( szDebugMsg +"No track to play" );
            m_rgPlayers[iEdictIndex].m_iTrackNumber = -1;
            g_LibDebug.verboselogln( szDebugMsg +"m_iTrackNumber = "+m_rgPlayers[iEdictIndex].m_iTrackNumber );
            return;
        }

        if (g_LibDebug.IsEnabled())
        {
            g_LibDebug.logln( g_LibString.GetFormattedString(szDebugMsg +"Play on [%1] track #%2: '%3'", {iEdictIndex,iTrackToPlay,m_rgTracks[iTrackToPlay].m_szFilePath}) );
        }

        g_LibDebug.verboselogln( szDebugMsg +"m_iLoopMode = "+m_iLoopMode );

        // NOTE: 'play' command inserts ambient sound with attenuation
        // NOTE: 'spk' command plays on STATIC channel

        string szClientCmd;
        if (m_rgTracks[iTrackToPlay].m_bIsWav)
        {
            // NOTE: just leave as is because it can be useful in future,
            // so 'bAllowEmit' is not used right now

            /*
                if (bAllowEmit && m_rgTracks[iTrackToPlay].m_bIsPrecached && g_LibPlayer.IsConnected(@pEdict))
                {
                    // NOTE: emit_sound is heard by ALL players
                    g_SoundSystem.PlaySound( @pEdict, EMIT_WAV_CHANNEL, m_rgTracks[iTrackToPlay].m_szFilePath, VOL_NORM, ATTN_NONE, 0, PITCH_NORM );
                    m_rgPlayers[iEdictIndex].m_iSoundEngine = SE_EMIT;
                    #if defined _DEBUG
                    log_amx("emit_sound(iEdictIndex=%d, chan=%d, file='%s')", iEdictIndex, EMIT_WAV_CHANNEL, m_rgTracks[iTrackToPlay].m_szFilePath);
                    #endif
                }
                else
                {
                    #if defined _DEBUG
                    m_rgPlayers[iEdictIndex].m_iSoundEngine = SE_SPK
                    log_amx("spk/play %s", m_rgTracks[iTrackToPlay].m_szFilePath)
                    #endif
                }
            */

            // HACK: set SE_EMIT in honour to stop wav playback by emit_sound()
            // (see StopTrack() for reference)
            m_rgPlayers[iEdictIndex].m_iSoundEngine = bAllowEmit ? SE_EMIT : SE_SPK;
            szClientCmd = 'spk "sound/'+m_rgTracks[iTrackToPlay].m_szFilePath+'"';
        }
        else
        {
            if (m_iLoopMode == LOOP_4EVER)
                szClientCmd = 'mp3 loop "'+m_rgTracks[iTrackToPlay].m_szFilePath+'"';
            else
                szClientCmd = 'mp3 play "'+m_rgTracks[iTrackToPlay].m_szFilePath+'"';
            m_rgPlayers[iEdictIndex].m_iSoundEngine = SE_MP3;
        }

        g_LibDebug.verboselogln( szDebugMsg +"m_iSoundEngine = "+m_rgPlayers[iEdictIndex].m_iSoundEngine );

        g_LibDebug.verboselogln( g_LibString.GetFormattedString(szDebugMsg +"szClientCmd = '%1'", {szClientCmd}) );
        if (!szClientCmd.IsEmpty())
        {
            g_LibPlayer.ClientCommand( @pEdict, szClientCmd );
        }

        m_rgPlayers[iEdictIndex].m_iTrackNumber = iTrackToPlay;
        g_LibDebug.verboselogln( szDebugMsg +"m_iTrackNumber = "+m_rgPlayers[iEdictIndex].m_iTrackNumber );

        g_LibDebug.logln( szDebugMsg +"GOOD" );
    }

    /*
     * method: StopTrack
     *
     * Stop playing track to given player.
     *
     * Arguments:
     * pEdict       - Player edict pointer.
     * bForceStop   - Force stop any tracks from playing, ignoring loop mode.
     */
    void StopTrack (edict_t@ pEdict, bool bForceStop = false)
    {
        const string szDebugMsg = g_LibString.GetFormattedString( "StopTrack(bForceStop=%1)...", {bForceStop} );
        g_LibDebug.verboselogln( szDebugMsg );

        if (pEdict is null)
        {
            g_LibDebug.verboselogln( szDebugMsg +"pEdict is null" );
            return;
        }
        const int iEdictIndex = g_EngineFuncs.IndexOfEdict( @pEdict );
        g_LibDebug.verboselogln( szDebugMsg +"iEdictIndex = "+iEdictIndex );

        RemoveSchedulePlayTrack( @pEdict );

        const int iCurrentTrack = m_rgPlayers[iEdictIndex].m_iTrackNumber;
        g_LibDebug.verboselogln( szDebugMsg +"iCurrentTrack = "+iCurrentTrack );

        if (iCurrentTrack >= 0 && !g_LibPlayer.IsConnected(@pEdict))
        {
            g_LibDebug.verboselogln( g_LibString.GetFormattedString(szDebugMsg +"Player [%1] is not connected", {iEdictIndex}) );
            m_rgPlayers[iEdictIndex].m_iTrackNumber = -1;
            m_rgPlayers[iEdictIndex].m_iSoundEngine = SE_NONE;
            g_LibDebug.verboselogln( szDebugMsg +"m_iTrackNumber = "+m_rgPlayers[iEdictIndex].m_iTrackNumber );
            g_LibDebug.verboselogln( szDebugMsg +"m_iSoundEngine = "+m_rgPlayers[iEdictIndex].m_iSoundEngine );
            return;
        }

        if (iCurrentTrack < 0)
        {
            g_LibDebug.verboselogln( szDebugMsg +"No track to stop" );
            m_rgPlayers[iEdictIndex].m_iSoundEngine = SE_NONE;
            g_LibDebug.verboselogln( szDebugMsg +"m_iSoundEngine = "+m_rgPlayers[iEdictIndex].m_iSoundEngine );
            return;
        }

        LoopMode iLoopMode;

        if (bForceStop)
            iLoopMode = LOOP_NO;
        else
            iLoopMode = m_iLoopMode;

        g_LibDebug.verboselogln( szDebugMsg +"iLoopMode = "+iLoopMode );
        if (iLoopMode != LOOP_4EVER)
        {
            string szClientCmd;

            g_LibDebug.verboselogln( szDebugMsg +"m_iSoundEngine = "+m_rgPlayers[iEdictIndex].m_iSoundEngine );
            g_LibDebug.verboselogln( szDebugMsg +"m_bIsWav = "+m_rgTracks[iCurrentTrack].m_bIsWav );
            if (m_rgTracks[iCurrentTrack].m_bIsWav)
            {
                // TODO: discover how to use 'soundfade' command
                if (m_rgPlayers[iEdictIndex].m_iSoundEngine == SE_EMIT)
                {
                    const string szFilePath = m_rgTracks[iCurrentTrack].m_szFilePath;
                    g_SoundSystem.PlaySound( @pEdict, EMIT_WAV_CHANNEL, szFilePath, 0, 0, SND_STOP, 0 );
                    g_LibDebug.logln( g_LibString.GetFormattedString(szDebugMsg +"PlaySound(iEdictIndex=%1, chan=%2, szFilePath='%3', SND_STOP)", {iEdictIndex,EMIT_WAV_CHANNEL,szFilePath}) );
                }
                else
                {
                    // this should never be called in current release
                    szClientCmd = "stopsound";
                }
                m_rgPlayers[iEdictIndex].m_iTrackNumber = -1;
            }
            else
            if (iLoopMode != LOOP_MP3_SPEC)
            {
                szClientCmd = "mp3 stop";//"cd fadeout";
                m_rgPlayers[iEdictIndex].m_iTrackNumber = -1;
            }

            if (!szClientCmd.IsEmpty())
            {
                g_LibDebug.logln( g_LibString.GetFormattedString(szDebugMsg +"szClientCmd = '%1'", {szClientCmd}) );
                g_LibPlayer.ClientCommand( @pEdict, szClientCmd );
            }
        }

        g_LibDebug.verboselogln( szDebugMsg +"m_iTrackNumber = "+m_rgPlayers[iEdictIndex].m_iTrackNumber );

        if (m_rgPlayers[iEdictIndex].m_iTrackNumber < 0)
        {
            m_rgPlayers[iEdictIndex].m_iSoundEngine = SE_NONE;
            g_LibDebug.verboselogln( szDebugMsg +"m_iSoundEngine = "+m_rgPlayers[iEdictIndex].m_iSoundEngine );
        }

        g_LibDebug.logln( szDebugMsg +"GOOD" );
    }

    /*
     * method: SchedulePlayTrack
     *
     * Schedules <PlayTrack> with given player.
     *
     * Arguments:
     * flDelay          - Delay time in second(s).
     * pEdict           - Player edict pointer.
     * bRestartTrack    - Replay current track.
     * bAllowEmit       - Use <EmitSoundDyn> to play.
     * iForceMode       - Only play on given track format (<FORCE_NONE> = Play any formats).
     *
     * Returns:
     * CScheduledFunction@  - The scheduler instance pointer.
     * null                 - Failed to create the scheduler, see debug messages for details.
     */
    CScheduledFunction@ SchedulePlayTrack (const float flDelay, edict_t@ pEdict, bool bRestartTrack = true, bool bAllowEmit = true, ForceMode iForceMode = FORCE_NONE)
    {
        const string szDebugMsg = g_LibString.GetFormattedString( "SchedulePlayTrack(flDelay=%1, bRestartTrack=%2, bAllowEmit=%3, iForceMode=%4)...", {flDelay,bRestartTrack,bAllowEmit,iForceMode} );
        g_LibDebug.verboselogln( szDebugMsg );

        if (pEdict is null)
        {
            g_LibDebug.verboselogln( szDebugMsg +"pEdict is null" );
            return null;
        }
        const int iEdictIndex = g_EngineFuncs.IndexOfEdict( @pEdict );
        g_LibDebug.verboselogln( szDebugMsg +"iEdictIndex = "+iEdictIndex );

        @m_rgpPlayTrackSchedFuncs[iEdictIndex] = g_Scheduler.SetTimeout( @this, "PlayScheduledTrack", flDelay, @pEdict, bRestartTrack, bAllowEmit, int(iForceMode) );
        g_LibDebug.verboselogln( g_LibString.GetFormattedString(szDebugMsg +"m_rgpPlayTrackSchedFuncs[%1] is %2", {iEdictIndex,(m_rgpPlayTrackSchedFuncs[iEdictIndex] is null ? "null" : "occupied")}) );

        g_LibDebug.logln( szDebugMsg +"GOOD" );
        return m_rgpPlayTrackSchedFuncs[iEdictIndex];
    }

    /*
     * method: RemoveSchedulePlayTrack
     *
     * Removes scheduled <PlayTrack> with given player.
     *
     * Arguments:
     * pEdict   - Player edict pointer.
     *
     * Returns:
     * true     - Successfully removed.
     * false    - Failed to remove, see debug messages for details.
     */
    bool RemoveSchedulePlayTrack (edict_t@ pEdict)
    {
        const string szDebugMsg = g_LibString.GetFormattedString( "RemoveSchedulePlayTrack()...", {} );
        g_LibDebug.verboselogln( szDebugMsg );

        if (pEdict is null)
        {
            g_LibDebug.verboselogln( szDebugMsg +"pEdict is null" );
            return false;
        }
        const int iEdictIndex = g_EngineFuncs.IndexOfEdict( @pEdict );
        g_LibDebug.verboselogln( szDebugMsg +"iEdictIndex = "+iEdictIndex );

        auto@ pSched = m_rgpPlayTrackSchedFuncs[iEdictIndex];
        if (pSched is null)
        {
            g_LibDebug.verboselogln( szDebugMsg +"m_rgpPlayTrackSchedFuncs["+iEdictIndex+"] is null" );
            return false;
        }

        g_Scheduler.RemoveTimer( @pSched );
        @pSched = @m_rgpPlayTrackSchedFuncs[iEdictIndex] = null;

        g_LibDebug.logln( szDebugMsg +"GOOD" );
        return @pSched is null;
    }

    /*
     * method: PrecacheCustomTrack
     *
     * Precache the custom track set from <AN::LOADINGMUSIC2::CLoadingMusic::m_szPlaybackMode>.
     *
     * Returns:
     * true     - Successfully precached.
     * false    - Failed to precache, see debug messages for details.
     */
    bool PrecacheCustomTrack ()
    {
        const string szDebugMsg = g_LibString.GetFormattedString( "PrecacheCustomTrack()...", {} );
        g_LibDebug.verboselogln( szDebugMsg );

        g_LibDebug.verboselogln( szDebugMsg +"m_iTrackSelection = "+m_iTrackSelection );
        if (m_iTrackSelection != 0)
        {
            g_LibDebug.logln( szDebugMsg +"Not using custom track" );
            return false;
        }

        if (!PrecacheTrack( m_rgTracks[0] ))
        {
            g_LibDebug.logln( szDebugMsg +"Failed to precache custom track" );
            return false;
        }

        g_LibDebug.logln( szDebugMsg +"GOOD" );
        return true;
    }

    /*
     * method: get_m_szPlaybackMode
     *
     * Gets playback mode.
     *
     * Returns:
     * See <AN::LOADINGMUSIC2::CLoadingMusic::set_m_szPlaybackMode> for details.
     */
    string get_m_szPlaybackMode () const property
    {
        if (this.m_iTrackSelection == 0)
        {
            const string szFilePath = this.m_rgTracks[0].m_szFilePath;
            if (!szFilePath.IsEmpty())
                return szFilePath;
        }
        return this.m_iTrackSelection;
    }

    /*
     * method: set_m_szPlaybackMode
     *
     * Sets playback mode.
     *
     * -1           - random order.
     * 0            - play nothing (disable plugin).
     * N            - play single file from playlist (N=1..<total of parsed tracks>).
     * filename     - name of file to play ignoring the playlist (it's
     *                a single-file playback mode); if this file is under precache dir
     *                it won't be precached yet.
     */
    void set_m_szPlaybackMode (string &in szInput) property
    {
        szInput.Trim();
        //FIXME: g_LibString null pointer crashes.
        //if (g_LibString.IsInteger( szInput ))
        if (IsInteger( szInput ))
        {
            if (!this.m_rgTracks[0].m_szFilePath.IsEmpty())
                this.m_rgTracks[0] = CTrackStruct();

            this.m_iTrackSelection = Math.clamp( -1, m_rgTracks.length()-1, atoi(szInput) );
        }
        else
        {
            this.m_iTrackSelection = 0;
            this.SetTrack( 0, szInput, false );
        }
    }

    /*
     * method: get_m_szFlags
     *
     * An AMXx flag string interpretation of <AN::LOADINGMUSIC2::m_uiTrackLoadingFlags>.
     *
     * Returns:
     * See <AN::LOADINGMUSIC2::TrackLoadingFlags> for details.
     */
    string get_m_szFlags () const property
    {
        return g_LibBit.ToFlags( this.m_uiTrackLoadingFlags );
    }

    /*
     * method: set_m_szFlags
     *
     * An AMXx flag string interpretation of <AN::LOADINGMUSIC2::m_uiTrackLoadingFlags>.
     *
     * See <AN::LOADINGMUSIC2::TrackLoadingFlags> for details.
     */
    void set_m_szFlags (string &in szInput) property
    {
        this.m_uiTrackLoadingFlags = g_LibBit.FromFlags( szInput );
    }

    /*
     * method: get_m_uiTrackCount
     *
     * Gets added track counts.
     */
    uint get_m_uiTrackCount () const property
    {
        return this.m_uiWavCount + this.m_uiMp3Count;
    }

    /*
     * method: get_m_uiWavCount
     *
     * number of wav tracks (for better randomization).
     */
    uint get_m_uiWavCount () const property
    {
        uint uiTotal = 0;
        for (uint i = 1; i < m_rgTracks.length(); ++i)
            if (m_rgTracks[i].m_bIsWav) ++uiTotal;
        return uiTotal;
    }

    /*
     * method: get_m_uiMp3Count
     *
     * number of mp3 tracks (for better randomization).
     */
    uint get_m_uiMp3Count () const property
    {
        uint uiTotal = 0;
        for (uint i = 1; i < m_rgTracks.length(); ++i)
            if (!m_rgTracks[i].m_bIsWav) ++uiTotal;
        return uiTotal;
    }

    /*
     * method: SetTrack
     */
    private bool SetTrack (const uint &in uiTrackNumber, const string &in szFilePath, const bool bAllowPrecache)
    {
        const string szDebugMsg = g_LibString.GetFormattedString( "SetTrack(uiTrackNumber=%1, szFilePath='%2', bAllowPrecache=%3)...", {uiTrackNumber,szFilePath,bAllowPrecache} );
        g_LibDebug.verboselogln( szDebugMsg );

        CTrackStruct tTrack;
        if (!ParseTrack( szFilePath, bAllowPrecache, tTrack ))
        {
            g_LibDebug.verboselogln( szDebugMsg +"Failed to parse track" );
            return false;
        }

        g_LibDebug.verboselogln( szDebugMsg +"length = "+m_rgTracks.length() );
        if (uiTrackNumber >= m_rgTracks.length())
        {
            g_LibDebug.log( szDebugMsg +"Index out of bound" );
            return false;
        }

        m_rgTracks[uiTrackNumber] = tTrack;

        g_LibDebug.verboselogln( szDebugMsg +"m_uiWavCount = "+m_uiWavCount );
        g_LibDebug.verboselogln( szDebugMsg +"m_uiMp3Count = "+m_uiMp3Count );
        g_LibDebug.logln( szDebugMsg +"GOOD" );
        return true;
    }

    /*
     * method: ParseTrack
     */
    private bool ParseTrack (string &in szFilePath, const bool bAllowPrecache, CTrackStruct &out tTrack)
    {
        szFilePath.Trim();
        const string szDebugMsg = g_LibString.GetFormattedString( "ParseTrack(szFilePath='%1', bAllowPrecache=%2)...", {szFilePath,bAllowPrecache} );
        g_LibDebug.verboselogln( szDebugMsg );

        if (szFilePath.IsEmpty())
        {
            g_LibDebug.logln( szDebugMsg +"szFilePath is empty" );
            return false;
        }

        tTrack.m_szFilePath = szFilePath;
        tTrack.m_bIsWav = HasWAVExtension( szFilePath );

        g_LibDebug.verboselogln( szDebugMsg +"m_bIsWav = "+tTrack.m_bIsWav );
        g_LibDebug.logln( g_LibString.GetFormattedString(szDebugMsg +"m_szFilePath = '%1'", {tTrack.m_szFilePath}) );

        const uint uiSoundDirLen = SOUND_DIR.Length();
        if ((szFilePath.Length() > uiSoundDirLen)
            && (szFilePath[uiSoundDirLen] == '\\' || szFilePath[uiSoundDirLen] == '/')
            )
        {
            // it's possible that file is placed within precache dir = > check it...
            tTrack.m_bShouldPrecache = szFilePath.ICompareN( SOUND_DIR, uiSoundDirLen ) == 0;

            if (tTrack.m_bShouldPrecache)
            {
                // Treated it as WAV track.
                if (g_LibFile.GetFileExtension( szFilePath ).IsEmpty())
                {
                    tTrack.m_bIsWav = true;
                    tTrack.m_szFilePath = szFilePath + ".wav";

                    g_LibDebug.verboselogln( szDebugMsg +"m_bIsWav = "+tTrack.m_bIsWav );
                    g_LibDebug.logln( g_LibString.GetFormattedString(szDebugMsg +"m_szFilePath = '%1'", {tTrack.m_szFilePath}) );
                }

                // NOTE: short filename is required for wav files only
                if (tTrack.m_bIsWav)
                {
                    tTrack.m_szFilePath = tTrack.m_szFilePath.SubString( uiSoundDirLen + 1 );
                    g_LibDebug.logln( g_LibString.GetFormattedString(szDebugMsg +"m_szFilePath = '%1'", {tTrack.m_szFilePath}) );
                }
            }
        }

        // normalize path
        tTrack.m_szFilePath = tTrack.m_szFilePath.Replace( '\\', '/' );
        g_LibDebug.logln( g_LibString.GetFormattedString(szDebugMsg +"m_szFilePath = '%1'", {tTrack.m_szFilePath}) );

        if (!bAllowPrecache)
        {
            g_LibDebug.logln( szDebugMsg +"Not allowed to precache" );
            tTrack.m_bIsExists = true; // assume client has this file
            tTrack.m_bIsPrecached = false;
        }
        else
        {
            PrecacheTrack( tTrack );
        }

        g_LibDebug.logln( szDebugMsg +"GOOD" );
        return true;
    }

    /*
     * method: PrecacheTrack
     */
    private bool PrecacheTrack (CTrackStruct@ pTrack)
    {
        const string szDebugMsg = g_LibString.GetFormattedString( "PrecacheTrack()...", {} );
        g_LibDebug.verboselogln( szDebugMsg );

        if (pTrack is null)
        {
            g_LibDebug.verboselogln( szDebugMsg +"pTrack is null" );
            return false;
        }

        string szFilePath = pTrack.m_szFilePath;
        g_LibDebug.verboselogln( g_LibString.GetFormattedString(szDebugMsg +"szFilePath = '%1'", {szFilePath}) );

        if (szFilePath.IsEmpty())
        {
            g_LibDebug.logln( szDebugMsg +"szFilePath is empty" );
            return false;
        }

        const bool bShouldPrecache = pTrack.m_bShouldPrecache;
        g_LibDebug.verboselogln( szDebugMsg +"bShouldPrecache = "+bShouldPrecache );

        /* NOTE: if sound file does not exist in current $MODDIR it can
        be still searched in other mods when playing by client side,
        thus i do not block adding a track to playlist
        */
        if (bShouldPrecache)
        {
            // TODO: Can't check files outside of hardcoded readable paths.
            if (true /*g_LibFile.IsExists(szFilePath)*/)
            {
                if (pTrack.m_bIsWav)
                {
                    g_LibDebug.logln( szDebugMsg +"PrecacheSound" );
                    g_SoundSystem.PrecacheSound( szFilePath );
                }
                else
                {
                    g_LibDebug.logln( szDebugMsg +"PrecacheGeneric" );
                    g_Game.PrecacheGeneric( szFilePath );
                }
                pTrack.m_bIsExists = true;
                pTrack.m_bIsPrecached = true;
            }
            else
            {
                g_LibDebug.logln( g_LibString.GetFormattedString(szDebugMsg +"File not found: '%1'", {szFilePath}) );
                pTrack.m_bIsExists = false;
                pTrack.m_bIsPrecached = false;
            }
        }
        else
        {
            g_LibDebug.logln( szDebugMsg +"No precache" );
            pTrack.m_bIsExists = true; // assume client has this file
            pTrack.m_bIsPrecached = false;
        }

        g_LibDebug.logln( szDebugMsg +"GOOD" );
        return pTrack.m_bIsPrecached;
    }

    /*
     * method: PlayScheduledTrack
     */
    private void PlayScheduledTrack (edict_t@ pEdict, bool bRestartTrack, bool bAllowEmit, int iForceMode) const
    {
        const string szDebugMsg = g_LibString.GetFormattedString( "PlayScheduledTrack(bRestartTrack=%1, bAllowEmit=%2, iForceMode=%3)...", {bRestartTrack,bAllowEmit,iForceMode} );
        g_LibDebug.verboselogln( szDebugMsg );

        if (pEdict is null)
        {
            g_LibDebug.verboselogln( szDebugMsg +"pEdict is null" );
            return;
        }
        const int iEdictIndex = g_EngineFuncs.IndexOfEdict( @pEdict );
        g_LibDebug.verboselogln( szDebugMsg +"iEdictIndex = "+iEdictIndex );

        RemoveSchedulePlayTrack( @pEdict );

        if (bRestartTrack)
        {
            // restore playback under CS
            PlayTrack( @pEdict, m_rgPlayers[iEdictIndex].m_iTrackNumber, bAllowEmit, ForceMode(iForceMode) );
        }
        else
        {
            if (m_rgPlayers[iEdictIndex].m_iTrackNumber < 0)
                PlayTrack( @pEdict, -1, true, ForceMode(iForceMode) );
        }

        g_LibDebug.logln( szDebugMsg +"GOOD" );
    }

    // HACK: Copied from g_LibString.
    /*
     * method: IsInteger
     */
    private bool IsInteger (const string &in szInput) const
    {
        return Regex::Match( szInput, Regex::Regex( '^[-]?\\d+$', Regex::FlagType(Regex::ECMAScript | Regex::icase) ) );
    }

    /*
     * method: HasWAVExtension
     */
    private bool HasWAVExtension (const string &in szFilePath) const
    {
        return g_LibFile.GetFileExtension( szFilePath ).ICompareN( WAV_EXTENSION, WAV_EXTENSION.Length()-1 ) == 0;
    }

    /*
     * method: OnClientConnected
     *
     * ClientConnected hook.
     */
    private HookReturnCode OnClientConnected (edict_t@ pEdict, const string& in szPlayerName, const string& in szIPAddress, bool& out bDisallowJoin, string& out szRejectReason)
    {
        const int iEdictIndex = g_EngineFuncs.IndexOfEdict( @pEdict );
        const string szDebugMsg = g_LibString.GetFormattedString( "OnClientConnected(iEdictIndex=%1)...", {iEdictIndex} );
        g_LibDebug.verboselogln( szDebugMsg );

        m_rgPlayers[iEdictIndex].m_iTrackNumber = -1;
        g_LibDebug.verboselogln( szDebugMsg +"m_iTrackNumber = "+m_rgPlayers[iEdictIndex].m_iTrackNumber );

        if (g_LibPlayer.IsBot( @pEdict ))
        {
            g_LibDebug.verboselogln( szDebugMsg +"Player is a bot" );
            return HOOK_CONTINUE;
        }

        g_LibDebug.verboselogln( szDebugMsg +"m_uiTrackLoadingFlags = "+m_uiTrackLoadingFlags );

        ForceMode iForceMode = g_LibBit.IsSet( m_uiTrackLoadingFlags, FL_FORCE_MP3_ON_CONNECT ) ? FORCE_MP3 : FORCE_NONE;
        g_LibDebug.verboselogln( szDebugMsg +"iForceMode = "+iForceMode );

        const float fDelay = m_flPlayOnConnectDelay;
        g_LibDebug.verboselogln( szDebugMsg +"fDelay = "+fDelay );

        if (fDelay >= 0.1)
        {
            SchedulePlayTrack( fDelay, @pEdict, false, false, iForceMode );
        }
        else
        {
            PlayTrack( @pEdict, -1, false, iForceMode );
        }

        g_LibDebug.verboselogln( szDebugMsg +"GOOD" );
        return HOOK_CONTINUE;
    }

    /*
     * method: OnClientDisconnect
     *
     * ClientDisconnect hook.
     */
    private HookReturnCode OnClientDisconnect (CBasePlayer@ pPlayer)
    {
        const string szDebugMsg = g_LibString.GetFormattedString( "OnClientDisconnect()...", {} );
        g_LibDebug.verboselogln( szDebugMsg );

        if (pPlayer is null)
        {
            g_LibDebug.verboselogln( szDebugMsg +"pPlayer is null" );
            return HOOK_CONTINUE;
        }
        const int iPlayerIndex = pPlayer.entindex();
        g_LibDebug.verboselogln( szDebugMsg +"iPlayerIndex = "+iPlayerIndex );

        if (g_LibPlayer.IsBot( @pPlayer ))
        {
            g_LibDebug.verboselogln( szDebugMsg +"Player is a bot" );
            return HOOK_CONTINUE;
        }

        StopTrack( pPlayer.edict() ); // has no effect on client but reinit plugin structures

        g_LibDebug.verboselogln( szDebugMsg +"GOOD" );
        return HOOK_CONTINUE;
    }

    /*
     * method: OnPlayerSpawn
     *
     * PlayerSpawn hook.
     */
    private HookReturnCode OnPlayerSpawn (CBasePlayer@ pPlayer)
    {
        const string szDebugMsg = g_LibString.GetFormattedString( "OnPlayerSpawn()...", {} );
        g_LibDebug.verboselogln( szDebugMsg );

        if (pPlayer is null)
        {
            g_LibDebug.verboselogln( szDebugMsg +"pPlayer is null" );
            return HOOK_CONTINUE;
        }
        const int iPlayerIndex = pPlayer.entindex();
        g_LibDebug.verboselogln( szDebugMsg +"iPlayerIndex = "+iPlayerIndex );

        if (g_LibPlayer.IsBot( @pPlayer ))
        {
            g_LibDebug.verboselogln( szDebugMsg +"Player is a bot" );
            return HOOK_CONTINUE;
        }

        StopTrack( pPlayer.edict() );

        g_LibDebug.verboselogln( szDebugMsg +"GOOD" );
        return HOOK_CONTINUE;
    }

    /*
     * method: OnPlayerEnteredObserver
     *
     * PlayerEnteredObserver hook.
     */
    private HookReturnCode OnPlayerEnteredObserver (CBasePlayer@ pPlayer)
    {
        const string szDebugMsg = g_LibString.GetFormattedString( "OnPlayerEnteredObserver()...", {} );
        g_LibDebug.verboselogln( szDebugMsg );

        if (pPlayer is null)
        {
            g_LibDebug.verboselogln( szDebugMsg +"pPlayer is null" );
            return HOOK_CONTINUE;
        }
        const int iPlayerIndex = pPlayer.entindex();
        g_LibDebug.verboselogln( szDebugMsg +"iPlayerIndex = "+iPlayerIndex );

        if (g_LibPlayer.IsBot( @pPlayer ))
        {
            g_LibDebug.verboselogln( szDebugMsg +"Player is a bot" );
            return HOOK_CONTINUE;
        }

        auto@ pEdict = pPlayer.edict();
        bool bDontPlay = true;

        g_LibDebug.verboselogln( szDebugMsg +"m_uiTrackLoadingFlags = "+m_uiTrackLoadingFlags );
        if (g_LibBit.IsSet( m_uiTrackLoadingFlags, FL_PLAY_ON_SPECTATE ))
        {
            if (g_LibBit.IsSet( m_uiTrackLoadingFlags, FL_DONT_PLAY_ON_DEAD ))
            {
                const bool bIsSpectator = g_LibBit.IsSet( pPlayer.pev.flags, FL_SPECTATOR ),
                bHasObserver = pPlayer.GetObserver() !is null,
                bIsObserver = bHasObserver && pPlayer.GetObserver().IsObserver(),
                bHasCorpse = bHasObserver && pPlayer.GetObserver().HasCorpse();

                g_LibDebug.verboselogln( szDebugMsg +"FL_SPECTATOR = "+bIsSpectator );
                g_LibDebug.verboselogln( szDebugMsg +"bHasObserver = "+bHasObserver );
                g_LibDebug.verboselogln( szDebugMsg +"bIsObserver = "+bIsObserver );
                g_LibDebug.verboselogln( szDebugMsg +"bHasCorpse = "+bHasCorpse );

                // check if player a real spectator
                if (bIsSpectator || ( bIsObserver && !bHasCorpse ))
                    bDontPlay = false;
            }
            else
            {
                bDontPlay = false;
            }
        }

        g_LibDebug.verboselogln( szDebugMsg +"bDontPlay = "+bDontPlay );
        if (bDontPlay)
        {
            StopTrack( @pEdict );
        }
        else
        {
            g_LibDebug.verboselogln( g_LibString.GetFormattedString(szDebugMsg +"m_rgpPlayTrackSchedFuncs[%1] is %2", {iPlayerIndex,(m_rgpPlayTrackSchedFuncs[iPlayerIndex] is null ? "null" : "occupied")}) );
            if (m_rgpPlayTrackSchedFuncs[iPlayerIndex] !is null)
            {
                g_LibDebug.logln( szDebugMsg +"Already launched a track" );
                return HOOK_CONTINUE; // already launched a track
            }
            StopTrack( @pEdict, true );
            SchedulePlayTrack( 0.1, @pEdict, false, true, FORCE_MP3 );
        }

        g_LibDebug.verboselogln( szDebugMsg +"GOOD" );
        return HOOK_CONTINUE;
    }

    /*
     * int: m_iLoopMode
     *
     * Customizes loop mode playback.
     *
     * See <AN::LOADINGMUSIC2::LoopMode> for details.
     */
    LoopMode    m_iLoopMode         = LOOP_NO;

    /*
     * float: m_flPlayOnConnectDelay
     *
     * if your clients often complain about silence while connecting to your
     * server try to set this cvar to non-zero number to delay (in sec)
     * playback after connection event has been triggered.
     */
    float   m_flPlayOnConnectDelay  = 0.0;

    /*
     * uint: m_uiTrackLoadingFlags
     *
     * Track loading flags bitfield.
     *
     * See <AN::LOADINGMUSIC2::TrackLoadingFlags> for details.
     */
    uint    m_uiTrackLoadingFlags   = FL_FORCE_MP3_ON_CONNECT | FL_PLAY_ON_SPECTATE | FL_FORCE_WAV_ON_SELECT;

    /*
     * int: m_iTrackSelection
     *
     * -1   - random.
     * 0    - depends on custom track data.
     */
    int     m_iTrackSelection       = -1;

    /*
     * array: m_rgTracks
     *
     * slot 0 is reserved for custom play track.
     */
    private CTrackStruct[]              m_rgTracks( 1 );

    /*
     * array: m_rgPlayers
     */
    private CPlayerStruct[]             m_rgPlayers( g_Engine.maxClients+1 );

    /*
     * array: m_rgpPlayTrackSchedFuncs
     */
    private CScheduledFunction@[]       m_rgpPlayTrackSchedFuncs( g_Engine.maxClients+1 );

    /*
     * ptr: m_pClientConnectedHook
     */
    private ClientConnectedHook@        m_pClientConnectedHook          = ClientConnectedHook(this.OnClientConnected);

    /*
     * ptr: m_pClientDisconnectHook
     */
    private ClientDisconnectHook@       m_pClientDisconnectHook         = ClientDisconnectHook(this.OnClientDisconnect);

    /*
     * ptr: m_pPlayerSpawnHook
     */
    private PlayerSpawnHook@            m_pPlayerSpawnHook              = PlayerSpawnHook(this.OnPlayerSpawn);

    /*
     * ptr: m_pPlayerEnteredObserverHook
     */
    private PlayerEnteredObserverHook@  m_pPlayerEnteredObserverHook    = PlayerEnteredObserverHook(this.OnPlayerEnteredObserver);

}

}}
