531 lines
21 KiB
C#
531 lines
21 KiB
C#
|
using System.Collections;
|
|||
|
using System.Collections.Generic;
|
|||
|
using UnityEngine;
|
|||
|
using System;
|
|||
|
|
|||
|
//plans for the future:
|
|||
|
//create a dictionary driven option
|
|||
|
//make it compatible with enums
|
|||
|
//use pooling audio channels instead when developing version2.0
|
|||
|
|
|||
|
//namespace Lemon.GenericLib.Audio {
|
|||
|
public class AudioManager2D : MonoBehaviour
|
|||
|
{
|
|||
|
public static AudioManager2D Instance;
|
|||
|
|
|||
|
//there are 4 seperate audio streams here, make sure they are assigned
|
|||
|
[Header("audio sources")]
|
|||
|
[SerializeField] private AudioSource soundAudioSrc;
|
|||
|
[SerializeField] private AudioSource bgmAudioSource;
|
|||
|
[SerializeField] private AudioSource uiAudioSource;
|
|||
|
[SerializeField] private AudioSource ambientAudioSource;
|
|||
|
[SerializeField] float masterVolume = 1;
|
|||
|
[SerializeField] bool changeOtherSources = false;
|
|||
|
[SerializeField] bool playOnStart = false;
|
|||
|
[SerializeField] string startingAudio = "";
|
|||
|
[SerializeField] AudioSource[] extraAudioThreads; //best for sound effects
|
|||
|
|
|||
|
[Header("extra effects config")]
|
|||
|
[SerializeField] float fadeAudioEffectSpeed = 1;
|
|||
|
|
|||
|
[Header("audio to play")]
|
|||
|
[SerializeField] Audio[] audioArray;
|
|||
|
|
|||
|
|
|||
|
//the index for the audio array to track what is playing
|
|||
|
//-1 means nothing is playing in that
|
|||
|
private int currentSoundIndex = -1;
|
|||
|
private int currentBGMIndex = -1;
|
|||
|
private int currentUISoundIndex = -1;
|
|||
|
private int currentAmbientIndex = -1;
|
|||
|
|
|||
|
|
|||
|
//singleton pattern
|
|||
|
private void Awake()
|
|||
|
{
|
|||
|
if (Instance == null)
|
|||
|
{
|
|||
|
Instance = this;
|
|||
|
}
|
|||
|
|
|||
|
if (Instance != this)
|
|||
|
{
|
|||
|
Destroy(gameObject);
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
private void Start()
|
|||
|
{
|
|||
|
if (playOnStart)
|
|||
|
{
|
|||
|
Play(startingAudio);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
//does not support extra threads
|
|||
|
//called when changed volume
|
|||
|
public void SetVolume(float vol)
|
|||
|
{
|
|||
|
|
|||
|
this.masterVolume = vol / 100;
|
|||
|
UpdateAudioSrcVolume(currentSoundIndex, Audio.AudioType.Sound);
|
|||
|
UpdateAudioSrcVolume(currentBGMIndex, Audio.AudioType.BGM);
|
|||
|
UpdateAudioSrcVolume(currentUISoundIndex, Audio.AudioType.UISounds);
|
|||
|
UpdateAudioSrcVolume(currentAmbientIndex, Audio.AudioType.Ambient);
|
|||
|
|
|||
|
//this also needs to change audio in audio sources outside
|
|||
|
//not the best method
|
|||
|
if (!changeOtherSources) return;
|
|||
|
var audioSrcList = FindObjectsOfType<AudioSource>();
|
|||
|
foreach (var audioSrc in audioSrcList)
|
|||
|
{
|
|||
|
if (audioSrc != soundAudioSrc && audioSrc != bgmAudioSource && audioSrc != uiAudioSource && audioSrc != ambientAudioSource)
|
|||
|
audioSrc.volume = masterVolume;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
//does not support extra threads
|
|||
|
private void UpdateAudioSrcVolume(int audioIndex = 0, Audio.AudioType type = Audio.AudioType.BGM)
|
|||
|
{
|
|||
|
|
|||
|
if (audioIndex >= 0)
|
|||
|
{
|
|||
|
switch (audioArray[audioIndex].audioType)
|
|||
|
{
|
|||
|
case Audio.AudioType.Sound:
|
|||
|
audioArray[audioIndex].updateVolume(soundAudioSrc, this.masterVolume);
|
|||
|
break;
|
|||
|
case Audio.AudioType.BGM:
|
|||
|
audioArray[audioIndex].updateVolume(bgmAudioSource, this.masterVolume);
|
|||
|
break;
|
|||
|
case Audio.AudioType.UISounds:
|
|||
|
audioArray[audioIndex].updateVolume(uiAudioSource, this.masterVolume);
|
|||
|
break;
|
|||
|
case Audio.AudioType.Ambient:
|
|||
|
audioArray[audioIndex].updateVolume(ambientAudioSource, this.masterVolume);
|
|||
|
break;
|
|||
|
default:
|
|||
|
Debug.LogError($"the audio clip {audioArray[audioIndex].audioName} does not have a audio type assigned \n or there is no update volume option for that type yet");
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
switch (type)
|
|||
|
{
|
|||
|
case Audio.AudioType.Sound:
|
|||
|
soundAudioSrc.volume = masterVolume;
|
|||
|
break;
|
|||
|
case Audio.AudioType.BGM:
|
|||
|
bgmAudioSource.volume = masterVolume;
|
|||
|
break;
|
|||
|
case Audio.AudioType.UISounds:
|
|||
|
uiAudioSource.volume = masterVolume;
|
|||
|
break;
|
|||
|
case Audio.AudioType.Ambient:
|
|||
|
ambientAudioSource.volume = masterVolume;
|
|||
|
break;
|
|||
|
default:
|
|||
|
Debug.LogError($"the audio clip {audioArray[audioIndex].audioName} does not have a audio type assigned \n or there is no update volume option for that type yet");
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
//force all audio to stop
|
|||
|
public void StopAllAudio()
|
|||
|
{
|
|||
|
//this is the solution to manage fading for now
|
|||
|
StopCoroutine("FadeAudioEffect");
|
|||
|
soundAudioSrc.Stop();
|
|||
|
bgmAudioSource.Stop();
|
|||
|
uiAudioSource.Stop();
|
|||
|
ambientAudioSource.Stop();
|
|||
|
foreach (var audioSrc in extraAudioThreads)
|
|||
|
{
|
|||
|
audioSrc.Stop();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
public void StopSound()
|
|||
|
{
|
|||
|
soundAudioSrc.Stop();
|
|||
|
}
|
|||
|
public void StopBGM()
|
|||
|
{
|
|||
|
bgmAudioSource.Stop();
|
|||
|
}
|
|||
|
public void StopUISound()
|
|||
|
{
|
|||
|
uiAudioSource.Stop();
|
|||
|
}
|
|||
|
public void StopAmbient()
|
|||
|
{
|
|||
|
ambientAudioSource.Stop();
|
|||
|
}
|
|||
|
|
|||
|
public void StopExtra()
|
|||
|
{
|
|||
|
foreach (var audioSrc in extraAudioThreads)
|
|||
|
{
|
|||
|
audioSrc.Stop();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
public void Stop(int audioTypeIndex)
|
|||
|
{
|
|||
|
if (audioTypeIndex == (int)Audio.AudioType.Sound)
|
|||
|
{
|
|||
|
soundAudioSrc.Stop();
|
|||
|
}
|
|||
|
else if (audioTypeIndex == (int)Audio.AudioType.BGM)
|
|||
|
{
|
|||
|
bgmAudioSource.Stop();
|
|||
|
}
|
|||
|
else if (audioTypeIndex == (int)Audio.AudioType.UISounds)
|
|||
|
{
|
|||
|
uiAudioSource.Stop();
|
|||
|
}
|
|||
|
else if (audioTypeIndex == (int)Audio.AudioType.Ambient)
|
|||
|
{
|
|||
|
ambientAudioSource.Stop();
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
Debug.LogError($"the audio index {audioTypeIndex} cis not a valid audio type\nPlease use 1 - 4 ");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
public void SetFadeEffectSpeed(float newEffectSpeed)
|
|||
|
{
|
|||
|
fadeAudioEffectSpeed = newEffectSpeed;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
//does not support extra threads
|
|||
|
public void PlayFade(string audioName)
|
|||
|
{
|
|||
|
|
|||
|
for (int i = 0; i < audioArray.Length; i++)
|
|||
|
{
|
|||
|
//Debug.Log(audioName + " " + audioArray[i].audioName);
|
|||
|
if (audioArray[i].audioName.ToUpper() == audioName.ToUpper())
|
|||
|
{
|
|||
|
|
|||
|
switch (audioArray[i].audioType)
|
|||
|
{
|
|||
|
case Audio.AudioType.Sound:
|
|||
|
|
|||
|
StartCoroutine(FadeAudioEffect(soundAudioSrc, fadeAudioEffectSpeed, audioArray[i].defaultVolume * masterVolume));
|
|||
|
audioArray[i].Play(soundAudioSrc, masterVolume);
|
|||
|
currentSoundIndex = i;
|
|||
|
break;
|
|||
|
case Audio.AudioType.BGM:
|
|||
|
|
|||
|
StartCoroutine(FadeAudioEffect(bgmAudioSource, fadeAudioEffectSpeed, audioArray[i].defaultVolume * masterVolume));
|
|||
|
audioArray[i].Play(bgmAudioSource, masterVolume);
|
|||
|
currentBGMIndex = i;
|
|||
|
break;
|
|||
|
case Audio.AudioType.UISounds:
|
|||
|
|
|||
|
StartCoroutine(FadeAudioEffect(uiAudioSource, fadeAudioEffectSpeed, audioArray[i].defaultVolume * masterVolume));
|
|||
|
audioArray[i].Play(uiAudioSource, masterVolume);
|
|||
|
currentUISoundIndex = i;
|
|||
|
break;
|
|||
|
case Audio.AudioType.Ambient:
|
|||
|
|
|||
|
StartCoroutine(FadeAudioEffect(ambientAudioSource, fadeAudioEffectSpeed, audioArray[i].defaultVolume * masterVolume));
|
|||
|
audioArray[i].Play(ambientAudioSource, masterVolume);
|
|||
|
currentAmbientIndex = i;
|
|||
|
break;
|
|||
|
default:
|
|||
|
Debug.LogError($"the audio clip {audioArray[i].audioName} does not have a audio type assigned \n or there is no play option for that type yet");
|
|||
|
break;
|
|||
|
}
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
}
|
|||
|
Debug.LogError($"the audio clip {audioName} cannot be found\n please check that your spelling is correct");
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
//does not support extra threads
|
|||
|
public void PlayFade(string audioName, float fadeAudioEffectSpeed)
|
|||
|
{
|
|||
|
|
|||
|
for (int i = 0; i < audioArray.Length; i++)
|
|||
|
{
|
|||
|
//Debug.Log(audioName + " " + audioArray[i].audioName);
|
|||
|
if (audioArray[i].audioName.ToUpper() == audioName.ToUpper())
|
|||
|
{
|
|||
|
|
|||
|
switch (audioArray[i].audioType)
|
|||
|
{
|
|||
|
case Audio.AudioType.Sound:
|
|||
|
StopCoroutine("FadeAudioEffect");
|
|||
|
StartCoroutine(FadeAudioEffect(soundAudioSrc, fadeAudioEffectSpeed, audioArray[i].defaultVolume * masterVolume));
|
|||
|
audioArray[i].Play(soundAudioSrc, masterVolume);
|
|||
|
currentSoundIndex = i;
|
|||
|
break;
|
|||
|
case Audio.AudioType.BGM:
|
|||
|
StopCoroutine("FadeAudioEffect");
|
|||
|
StartCoroutine(FadeAudioEffect(bgmAudioSource, fadeAudioEffectSpeed, audioArray[i].defaultVolume * masterVolume));
|
|||
|
audioArray[i].Play(bgmAudioSource, masterVolume);
|
|||
|
currentBGMIndex = i;
|
|||
|
break;
|
|||
|
case Audio.AudioType.UISounds:
|
|||
|
StopCoroutine("FadeAudioEffect");
|
|||
|
StartCoroutine(FadeAudioEffect(uiAudioSource, fadeAudioEffectSpeed, audioArray[i].defaultVolume * masterVolume));
|
|||
|
audioArray[i].Play(uiAudioSource, masterVolume);
|
|||
|
currentUISoundIndex = i;
|
|||
|
break;
|
|||
|
case Audio.AudioType.Ambient:
|
|||
|
StopCoroutine("FadeAudioEffect");
|
|||
|
StartCoroutine(FadeAudioEffect(ambientAudioSource, fadeAudioEffectSpeed, audioArray[i].defaultVolume * masterVolume));
|
|||
|
audioArray[i].Play(ambientAudioSource, masterVolume);
|
|||
|
currentAmbientIndex = i;
|
|||
|
break;
|
|||
|
default:
|
|||
|
Debug.LogError($"the audio clip {audioArray[i].audioName} does not have a audio type assigned \n or there is no play option for that type yet");
|
|||
|
break;
|
|||
|
}
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
}
|
|||
|
Debug.LogError($"the audio clip {audioName} cannot be found\n please check that your spelling is correct");
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
//does not support extra threads
|
|||
|
//0 = sound, 1 is BGM, 2 is UI, 3 is ambient
|
|||
|
public void StopFade(int audioTypeIndex)
|
|||
|
{
|
|||
|
|
|||
|
if (audioTypeIndex == (int)Audio.AudioType.Sound)
|
|||
|
{
|
|||
|
StartCoroutine(FadeAudioEffect(soundAudioSrc, fadeAudioEffectSpeed, soundAudioSrc.volume, false));
|
|||
|
}
|
|||
|
else if (audioTypeIndex == (int)Audio.AudioType.BGM)
|
|||
|
{
|
|||
|
StartCoroutine(FadeAudioEffect(bgmAudioSource, fadeAudioEffectSpeed, bgmAudioSource.volume, false));
|
|||
|
}
|
|||
|
else if (audioTypeIndex == (int)Audio.AudioType.UISounds)
|
|||
|
{
|
|||
|
StartCoroutine(FadeAudioEffect(uiAudioSource, fadeAudioEffectSpeed, uiAudioSource.volume, false));
|
|||
|
}
|
|||
|
else if (audioTypeIndex == (int)Audio.AudioType.Ambient)
|
|||
|
{
|
|||
|
StartCoroutine(FadeAudioEffect(ambientAudioSource, fadeAudioEffectSpeed, ambientAudioSource.volume, false));
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
Debug.LogError($"the audio index {audioTypeIndex} cis not a valid audio type\nPlease use 1 - 4 ");
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
//does not support extra threads
|
|||
|
public void StopFade(int audioTypeIndex, float fadeAudioEffectSpeed)
|
|||
|
{
|
|||
|
|
|||
|
if (audioTypeIndex == (int)Audio.AudioType.Sound)
|
|||
|
{
|
|||
|
StartCoroutine(FadeAudioEffect(soundAudioSrc, fadeAudioEffectSpeed, soundAudioSrc.volume, false));
|
|||
|
}
|
|||
|
else if (audioTypeIndex == (int)Audio.AudioType.BGM)
|
|||
|
{
|
|||
|
StartCoroutine(FadeAudioEffect(bgmAudioSource, fadeAudioEffectSpeed, bgmAudioSource.volume, false));
|
|||
|
}
|
|||
|
else if (audioTypeIndex == (int)Audio.AudioType.UISounds)
|
|||
|
{
|
|||
|
StartCoroutine(FadeAudioEffect(uiAudioSource, fadeAudioEffectSpeed, uiAudioSource.volume, false));
|
|||
|
}
|
|||
|
else if (audioTypeIndex == (int)Audio.AudioType.Ambient)
|
|||
|
{
|
|||
|
StartCoroutine(FadeAudioEffect(ambientAudioSource, fadeAudioEffectSpeed, ambientAudioSource.volume, false));
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
Debug.LogError($"the audio index {audioTypeIndex} cis not a valid audio type\nPlease use 1 - 4 ");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
IEnumerator FadeAudioEffect(AudioSource audioSource, float effectSpeed, float MaxVolume, bool increment = true)
|
|||
|
{
|
|||
|
float currentVolume = 0;
|
|||
|
if (increment)
|
|||
|
{
|
|||
|
currentVolume = 0;
|
|||
|
while (currentVolume < MaxVolume)
|
|||
|
{
|
|||
|
audioSource.volume = currentVolume;
|
|||
|
currentVolume += Time.deltaTime * 0.01f * effectSpeed;
|
|||
|
yield return null;
|
|||
|
}
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
Debug.Log("fading");
|
|||
|
currentVolume = MaxVolume;
|
|||
|
while (currentVolume > 0)
|
|||
|
{
|
|||
|
audioSource.volume = currentVolume;
|
|||
|
currentVolume -= Time.deltaTime * 0.01f * effectSpeed;
|
|||
|
yield return null;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
//use audio name to play audio, I know it is a string but what choice we have, maybe change to scriptable object when we have time
|
|||
|
public void Play(string audioName)
|
|||
|
{
|
|||
|
|
|||
|
for (int i = 0; i < audioArray.Length; i++)
|
|||
|
{
|
|||
|
//Debug.Log(audioName + " " + audioArray[i].audioName);
|
|||
|
if (audioArray[i].audioName.ToUpper() == audioName.ToUpper())
|
|||
|
{
|
|||
|
|
|||
|
switch (audioArray[i].audioType)
|
|||
|
{
|
|||
|
case Audio.AudioType.Sound:
|
|||
|
StopCoroutine("FadeAudioEffect");
|
|||
|
audioArray[i].Play(soundAudioSrc, masterVolume);
|
|||
|
currentSoundIndex = i;
|
|||
|
break;
|
|||
|
case Audio.AudioType.BGM:
|
|||
|
StopCoroutine("FadeAudioEffect");
|
|||
|
audioArray[i].Play(bgmAudioSource, masterVolume);
|
|||
|
currentBGMIndex = i;
|
|||
|
break;
|
|||
|
case Audio.AudioType.UISounds:
|
|||
|
StopCoroutine("FadeAudioEffect");
|
|||
|
audioArray[i].Play(uiAudioSource, masterVolume);
|
|||
|
currentUISoundIndex = i;
|
|||
|
break;
|
|||
|
case Audio.AudioType.Ambient:
|
|||
|
StopCoroutine("FadeAudioEffect");
|
|||
|
audioArray[i].Play(ambientAudioSource, masterVolume);
|
|||
|
currentAmbientIndex = i;
|
|||
|
break;
|
|||
|
case Audio.AudioType.Extra:
|
|||
|
|
|||
|
if (audioArray[i].extraThreadIndex < 0 || audioArray[i].extraThreadIndex >= extraAudioThreads.Length)
|
|||
|
{
|
|||
|
Debug.LogError($"the audio clip {audioArray[i].audioName} has a invalid extra audio thread index of {audioArray[i].extraThreadIndex}");
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
StopCoroutine("FadeAudioEffect");
|
|||
|
audioArray[i].Play(extraAudioThreads[audioArray[i].extraThreadIndex], masterVolume);
|
|||
|
}
|
|||
|
break;
|
|||
|
default:
|
|||
|
Debug.LogError($"the audio clip {audioArray[i].audioName} does not have a audio type assigned \n or there is no play option for that type yet");
|
|||
|
break;
|
|||
|
}
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
public void Play(int index)
|
|||
|
{
|
|||
|
if (index < 0 || index > audioArray.Length) {
|
|||
|
Debug.LogError($"Error: Invalid Audio Index of {index}");
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
switch (audioArray[index].audioType)
|
|||
|
{
|
|||
|
case Audio.AudioType.Sound:
|
|||
|
StopCoroutine("FadeAudioEffect");
|
|||
|
audioArray[index].Play(soundAudioSrc, masterVolume);
|
|||
|
currentSoundIndex = index;
|
|||
|
break;
|
|||
|
case Audio.AudioType.BGM:
|
|||
|
StopCoroutine("FadeAudioEffect");
|
|||
|
audioArray[index].Play(bgmAudioSource, masterVolume);
|
|||
|
currentBGMIndex = index;
|
|||
|
break;
|
|||
|
case Audio.AudioType.UISounds:
|
|||
|
StopCoroutine("FadeAudioEffect");
|
|||
|
audioArray[index].Play(uiAudioSource, masterVolume);
|
|||
|
currentUISoundIndex = index;
|
|||
|
break;
|
|||
|
case Audio.AudioType.Ambient:
|
|||
|
StopCoroutine("FadeAudioEffect");
|
|||
|
audioArray[index].Play(ambientAudioSource, masterVolume);
|
|||
|
currentAmbientIndex = index;
|
|||
|
break;
|
|||
|
case Audio.AudioType.Extra:
|
|||
|
|
|||
|
if (audioArray[index].extraThreadIndex < 0 || audioArray[index].extraThreadIndex >= extraAudioThreads.Length)
|
|||
|
{
|
|||
|
Debug.LogError($"the audio clip {audioArray[index].audioName} has a invalid extra audio thread index of {audioArray[index].extraThreadIndex}");
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
StopCoroutine("FadeAudioEffect");
|
|||
|
audioArray[index].Play(extraAudioThreads[audioArray[index].extraThreadIndex], masterVolume);
|
|||
|
}
|
|||
|
break;
|
|||
|
default:
|
|||
|
Debug.LogError($"the audio clip {audioArray[index].audioName} does not have a audio type assigned \n or there is no play option for that type yet");
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
//store audio data
|
|||
|
[Serializable]
|
|||
|
public class Audio
|
|||
|
{
|
|||
|
public string audioName = "";
|
|||
|
public enum AudioType { Sound, BGM, UISounds, Ambient, Extra }
|
|||
|
public int extraThreadIndex;
|
|||
|
public AudioType audioType = AudioType.Sound;
|
|||
|
public AudioClip clip;
|
|||
|
public bool loop = false;
|
|||
|
public float defaultVolume = 1;
|
|||
|
|
|||
|
|
|||
|
public void Play(AudioSource audioSrc, float volume)
|
|||
|
{
|
|||
|
audioSrc.clip = clip;
|
|||
|
audioSrc.volume = defaultVolume * volume;
|
|||
|
audioSrc.loop = loop;
|
|||
|
|
|||
|
|
|||
|
audioSrc.Play();
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
public void updateVolume(AudioSource audioSrc, float volume)
|
|||
|
{
|
|||
|
Debug.Log("update");
|
|||
|
audioSrc.volume = defaultVolume * volume;
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
//}
|
|||
|
|
|||
|
|