using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using FMOD; using FMOD.Studio; using FMODUnity; using Ktyl.Util; using UnityEngine; using UnityEngine.Animations.Rigging; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Users; using UnityEngine.Networking; using Debug = UnityEngine.Debug; using STOP_MODE = FMOD.Studio.STOP_MODE; #if UNITY_EDITOR using UnityEditor; #endif [CreateAssetMenu(menuName = "KernelPanic/Dialogue/Dialogue System")] public partial class DialogueSystem : ScriptableObject { [SerializeField] private DialogueSettings _settings; // https://stackoverflow.com/questions/2282476/actiont-vs-delegate-event public event EventHandler onDialogueLine; // a list of dialogue keys that have already been spoken private readonly List _usedClips = new List(); private EVENT_CALLBACK _dialogueCallback; private EventInstance _radioInstance; private EventInstance _bazInstance; private string _currentControlScheme; private void OnEnable() { _usedClips.Clear(); _dialogueCallback = DialogueEventCallback; } [AOT.MonoPInvokeCallback(typeof(EVENT_CALLBACK))] private static RESULT DialogueEventCallback(EVENT_CALLBACK_TYPE type, IntPtr instancePtr, IntPtr parameterPtr) { var instance = new EventInstance(instancePtr); // retrieve user data instance.getUserData(out IntPtr stringPtr); // get string obejct var stringHandle = GCHandle.FromIntPtr(stringPtr); var key = stringHandle.Target as string; switch (type) { case EVENT_CALLBACK_TYPE.CREATE_PROGRAMMER_SOUND: { MODE soundMode = MODE.LOOP_NORMAL | MODE.CREATECOMPRESSEDSAMPLE | MODE.NONBLOCKING; var parameter = (PROGRAMMER_SOUND_PROPERTIES) Marshal.PtrToStructure(parameterPtr, typeof(PROGRAMMER_SOUND_PROPERTIES)); if (key.Contains(".")) { Sound dialogueSound; var soundResult = RuntimeManager.CoreSystem.createSound(Application.streamingAssetsPath + "/" + key, soundMode, out dialogueSound); if (soundResult == RESULT.OK) { parameter.sound = dialogueSound.handle; parameter.subsoundIndex = -1; Marshal.StructureToPtr(parameter, parameterPtr, false); } } else { SOUND_INFO dialogueSoundInfo; var keyResult = RuntimeManager.StudioSystem.getSoundInfo(key, out dialogueSoundInfo); if (keyResult != RESULT.OK) { break; } Sound dialogueSound; var soundResult = RuntimeManager.CoreSystem.createSound(dialogueSoundInfo.name_or_data, soundMode | dialogueSoundInfo.mode, ref dialogueSoundInfo.exinfo, out dialogueSound); if (soundResult == RESULT.OK) { parameter.sound = dialogueSound.handle; parameter.subsoundIndex = dialogueSoundInfo.subsoundindex; Marshal.StructureToPtr(parameter, parameterPtr, false); } } break; } case EVENT_CALLBACK_TYPE.DESTROY_PROGRAMMER_SOUND: { var parameter = (PROGRAMMER_SOUND_PROPERTIES) Marshal.PtrToStructure(parameterPtr, typeof(PROGRAMMER_SOUND_PROPERTIES)); var sound = new Sound(parameter.sound); sound.release(); break; } case EVENT_CALLBACK_TYPE.DESTROYED: { stringHandle.Free(); break; } } return RESULT.OK; } public float GetLineDuration(string key) => _settings.GetDialogueClip(key).length; public void PlayLineBaz(string key, Transform bazObject) => PlayLine(ref _bazInstance, _settings.BazDialogueKey, key, speakerObject: bazObject); public void PlayLineRadio(string key) => PlayLine(ref _radioInstance, _settings.RadioDialogueKey, key); // noRepeat locks this key off from further use. further attempts to use the key will be discarded private void PlayLine(ref EventInstance eventInstance, string speakerKey, string key, bool noRepeat = true, Transform speakerObject = null ) { if (noRepeat) { if (_usedClips.Contains(key)) return; _usedClips.Add(key); } if ( eventInstance.isValid() ) { eventInstance.stop( STOP_MODE.IMMEDIATE ); eventInstance.release(); } eventInstance = RuntimeManager.CreateInstance(speakerKey); GCHandle stringHandle = GCHandle.Alloc(key, GCHandleType.Pinned); eventInstance.setUserData(GCHandle.ToIntPtr(stringHandle)); DialogueLine dl; var line = DialogueDatabase.ReadDialogue(key); // process templates in string line = ProcessTemplates(line); dl.text = line; var clip = _settings.GetDialogueClip(key); dl.duration = clip.length; eventInstance.setCallback(_dialogueCallback); if ( speakerObject ) { RuntimeManager.AttachInstanceToGameObject( eventInstance, speakerObject, (Rigidbody) null ); } eventInstance.start(); onDialogueLine?.Invoke(this, dl); } private string ProcessTemplates(string text) { var spriteIndices = _currentControlScheme == InputSchemes.GAMEPAD ? _settings.GamepadInputPrompts : _settings.KeyboardInputPrompts; text = text.Replace("[BLINK]", $""); text = text.Replace("[BOOST]", $""); text = text.Replace("[TIME_FREEZE]", $""); return text; } public void UpdateControlPrompts(PlayerInput playerInput) { var controlScheme = playerInput.currentControlScheme; if (controlScheme != InputSchemes.PCMR && controlScheme != InputSchemes.GAMEPAD) { Debug.LogError($"could not set unknown control scheme {controlScheme}"); return; } _currentControlScheme = playerInput.currentControlScheme; } } public struct DialogueLine { public string text; public float duration; }