Dialogue: Requests

This is a small sample of the most root-level script for playing all forms of dialogue. It is written in the exp language created by John Haggerty.

Exp Cheatsheet:

  • aslongas - keeps the current scope alive "as long as" the expression remains true
  • whenever - Runs the following statement repeatedly, once each time the expression is true, ending the statement when it becomes false, and waiting for it to become true again when the statement ends
  • meantime - Whenever may be paired with "meantime" to provide a statement that will be run when the expression is false
  • thread - Exp "threads" are asynchronous function calls. They are akin to coroutines in some other languages: they take turns executing and so are not truly asynchronous.
  • localthread - A localthread is a thread that is guaranteed to end when the scope it is defined in is exited. You can customize the scope it is attached to with the @ symbol.
  • cleanup - After a cleanup block is passed during execution, the statement (or block of statements) after cleanup is guaranteed to run when the scope is exited.
#if SERVER
shared struct DialogueLine
{
    FDialogueDataLine dialogueData // mostly a duplicate of this struct that is unreal-friendly. ugh.
    string barkRuleId
    string dataTablePathName
    string dataTableRowName
    EBarkPriority priority // used for cancelling or reordering within the queue when a new line is requested.
    EBarkCancelBehavior cancelCurrentOption // how should this line be cancelled, given priority?
    EBarkCancelBehavior removeAllOption // how should this line be removed, given priority?
    bool allowWhenDead
    bool isForcedRadio // is this disembodied dialogue over the radio, without a physical speaker?
    AActor? speakerOverride
    AActor? speaker
    bool isQueued
    
    array<DialogueLine> associatedLines // if this line is cancelled, which other lines should be removed in the queue?

    watchable bool LineCancelled = false
    watchable bool LineFinished = false

    #if DEV
        bool isTemp = false // used for temp, text-only dialogue which has special behaviors.
    #endif
}

// Most dialogue in the game is queued, so you hear it one line at a time. However, there are multiple queues.
maywait fn APlayer::PlayQueuedLine( DialogueLine currentLine, DialogueQueue dialogueQueue, EDialogueQueueType queueType )
{
    // get a unique id to sync things like subtitles
    int dialogueId = GetUniqueDialogueId()

    bool isTemp = false
#if DEV
    isTemp = currentLine.isTemp
#endif

    // find out who is actually delivering the line, if given a broad category...
    ERadioEmitterType radioType = currentLine.dialogueData.forcedRadio ? ERadioEmitterType.ImmediateScripted : ERadioEmitterType.Invalid
    currentLine.speaker = GetSpeakerAndSetType( currentLine.dialogueData, isTemp, currentLine.speakerOverride, radioType )
    AActor? barker = currentLine.speaker // copy for exp syntax convenience.
    
    // endwhen inPlay or IsAlive()
    bool stopOnSpeakerDeath = ShouldDialogueStopWhenSpeakerDead( currentLine.dialogueData, currentLine.allowWhenDead )

    cleanup
    {
        if ( dialogueQueue.dialogueLines.size > 0 )
        {
            bool stoppedDueToSpeakerDeath = stopOnSpeakerDeath && (!barker || !barker.IsAlive())
            if ( stoppedDueToSpeakerDeath || currentLine.LineCancelled )
            {
                foreach ( line in currentLine.associatedLines )
                    line.LineCancelled = true
            }
            dialogueQueue.dialogueLines.removeOrdered( 0 )
        }

        // handle if this line is meant to end early or late in relation to the next line
        thread WaitOffsetAndStopLine( currentLine, dialogueId )
    }

    if ( !barker || !barker.inPlay )
        return

    // function ending on death or external cancelling
    if ( stopOnSpeakerDeath )
    {
        aslongas @fn barker.IsAlive()
    }
    else
    {
        aslongas @fn barker.inPlay
    }

    aslongas @fn !currentLine.LineCancelled

    // wait for dialogue to clear
    array<DialogueLine> linesToWaitFor
    foreach ( line in CurrentlyPlayingDialogueLines )
    {
        if ( line.speaker == barker || dialogueQueue.dialogueLines.contains(line) )
            linesToWaitFor.push( line )
    }

    foreach ( line in linesToWaitFor )
        waitwhile CurrentlyPlayingDialogueLines.contains( line )

    CurrentlyPlayingDialogueLines.push( currentLine )
    
    // make sure the speaker is always relevant during this time so they don't cut out at huge distances.
    int speakerIndex = RegisterSpeakerActor( barker )
    assert( speakerIndex >= 0 )
    
    // get the localized duration of the dialogue on the server so we are in sync with the client
    UAkAudioEvent? audioWwise = currentLine.dialogueData.audioWwise
    float duration = GetDialogueDuration( audioWwise, currentLine.dialogueData.subtitle, currentLine.barkRuleId )
    float durationWithOffset = GetDialogueDurationWithOffset( duration, currentLine.dialogueData.subtitle, currentLine.dialogueData.desiredOffset )

    ESubtitleType subtitleType = queueType == EDialogueQueueType.Main ? ESubtitleType.QueuedMain : ESubtitleType.QueuedOther

    FGameplayTag speakerTag = currentLine.dialogueData.SpeakerTag
    if ( IsInPlay(currentLine.speakerOverride) && !isTemp )
        speakerTag = GetActorSpeakerTag( currentLine.speakerOverride! )

    // Play the dialogue by adding it to a replicated array of requests
    ServerPlayDialogue(
    currentLine.dataTablePathName,
    currentLine.dataTableRowName,
    subtitleType,
    dialogueId,
    barker,
    speakerTag,
    "",
    isTemp ? currentLine.dialogueData.subtitle.ToString() : "",
    duration )

    wait durationWithOffset
}
#endif // SERVER


#if CLIENT
// This is the most root-level function that actually plays the audio and subtitles.
// It will finish either when the audio finishes, or the server ends it early.
maywait fn APlayer::PlayDialogueThread_Client(
    int dataTableLookupVal,
    string dataTableRowName,
    ESubtitleType subtitleType,
    int dialogueId,
    AActor? speaker,
    FGameplayTag speakerTag,
    string barkDebugText,
    string devSubtitle,
    float duration )
{
    // Get the dialogue information client side
    UAkAudioEvent? audioEvent
    string subtitle
    bool isForcedRadio

    string dataTablePath = GetDatatableLookupPath(dataTableLookupVal)

    if (dataTablePath != "")
    {
        FDialogueDataLine dialogueData = GetCachedDialogueLineInfo(dataTablePath, dataTableRowName)

        // This is for when the speakerTag is missing; TODO: in the future, speakerTags should just be grabbed from dialogueData
        // all the time since they should be guarenteed by the bark request, but that'd require reworking dev barks
        if (speakerTag.Equals(GameplayTag_None))
            speakerTag = dialogueData.SpeakerTag

        audioEvent = dialogueData.audioWwise
        subtitle = dialogueData.subtitle
        isForcedRadio = dialogueData.forcedRadio // Is this ever inconsistent with the data table? i.e. realtime radio filter
    }
    #if DEV
    else if (devSubtitle != "")
    {
        subtitle = devSubtitle
    }
    else
    {
        // PrintWarning("dataTable lookup value was missed")
        // TODO: Check back with the server to find out what actually was missed?
    }
    #endif

    aslongas @fn CurrentlyPlayingDialogueLines_Client.contains( dialogueId )

    if ( !IsInPlay( speaker ) )
    {
        Printl( "Incoming server dialogue is playing on an actor we don't have: {audioEvent} - debugText {barkDebugText}" )
        return
    }
    assert( speaker )

    UDialogueSpeakerComponent? audioComp

    cleanup
    {
        if ( audioComp )
            audioComp.Stop()

        if ( this.inPlay )
            FinishSubtitleById( dialogueId ) // Tell the subtitle to clean itself up
    }

    waitfor this.inPlay // due to race condition when dialogue is played on map load
    aslongas @fn this.inPlay

    if ( speaker != this )
        aslongas @fn speaker.inPlay

    // speech bubble setup for debugging battle chatter
    #if DEV
    if ( speaker is AVillain && barkDebugText != "" )
    {
        thread SetupClientSpeechBubble( barkDebugText, audioEvent, speaker, subtitle, duration )
    }
    #endif // DEV

    // Play a subtitle if our user settings allow...
    if ( ShouldAddSubtitle(speaker, subtitle, isForcedRadio, subtitleType) )
        thread AddSubtitleWidget( speakerTag, subtitle, isForcedRadio, subtitleType, dialogueId )

    // ok finally playing the audio...
    if ( audioEvent )
    {
        audioComp = GetDialogueAudioComponent( speaker )
        assert( audioComp )

        audioComp.PostAudioEvent( audioEvent, ESoundNetDestination.LOCAL )
    }
    
    wait duration
}
#endif // CLIENT