diff --git a/README.md b/README.md
index 90c4273..08663bc 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,8 @@ If any mod you have uses these files, change the values in it instead of using t
Caller IDs for all factions:
-
+
All numbers are documented [on GitHub](https://gist.github.com/begin-theadventure/d35f8602dd15762bf2e8648728272ca5).
+
+Thanks to `begin-theadventure` for creating this description for me.
diff --git a/icon.png b/icon.png
new file mode 100644
index 0000000..549619a
Binary files /dev/null and b/icon.png differ
diff --git a/manifest.json b/manifest.json
new file mode 100644
index 0000000..cf19d75
--- /dev/null
+++ b/manifest.json
@@ -0,0 +1,7 @@
+{
+ "name": "EnableFactionIDCards",
+ "version_number": "1.0.1",
+ "website_url": "https://github.com/NachosChipeados/N-EnableFactionIDCards",
+ "dependencies": [],
+ "description": "Enables ID cards for all factions."
+}
diff --git a/mods/Nachos.EnableIDCards/mod.json b/mods/Nachos.EnableIDCards/mod.json
new file mode 100644
index 0000000..a01e3b1
--- /dev/null
+++ b/mods/Nachos.EnableIDCards/mod.json
@@ -0,0 +1,6 @@
+{
+ "Name": "Nachos.EnableIDCards",
+ "Description": "-Enables ID cards for all factions.\ncl_conversation.gnut: line 96, caller_id_number 01-36 - changes the banner on the ID Card.\ncl_faction_dialogue.gnut: line 250 & 256 return changed to true - enables the ID Card.\nConflicts with mods using these files.",
+ "Version": "1.0.1",
+ "LoadPriority": 5
+}
diff --git a/mods/Nachos.EnableIDCards/mod/scripts/vscripts/conversation/cl_conversation.gnut b/mods/Nachos.EnableIDCards/mod/scripts/vscripts/conversation/cl_conversation.gnut
new file mode 100644
index 0000000..79f1e3e
--- /dev/null
+++ b/mods/Nachos.EnableIDCards/mod/scripts/vscripts/conversation/cl_conversation.gnut
@@ -0,0 +1,1605 @@
+untyped
+
+global function ClDialogue_Init
+
+// call to attempt to start playing a conversation. will return false if it doesn't play, not that we can do anything about it.
+global function ServerCallback_PlayConversation
+global function ServerCallback_PlayTitanConversation
+global function ServerCallback_PlaySquadConversation
+global function SetDialogueDebugLevel
+global function GetDialogueDebugLevel
+global function VerifyConversationAliases
+global function ClDebugPlayConversation
+global function CancelConversation
+global function AddSpeakerToBlacklist
+global function RemoveSpeakerFromBlacklist
+global function AbortConversationDueToPriority
+
+global function CreateWaveform
+global function DestroyWaveform
+global function DestroyWaveform_Immediate
+
+global function PlayConversationToLocalClient
+global function ResetSquadConversationDebounceTimers
+
+global function PlayOneLinerConversationOnEntWithPriority //R2 MP style dialogue ( one liner, sound alias needs to be dynamically generated on the client ) should use this function
+global function PlayAnnouncerLineThroughDeathWithPriority //Faction Leader announcers are an exception since sometimes they need to persist through death ( e.g. win announcement shouldn't cancel when you die )
+
+/*
+ ServerCallback_PlayConversation( conversationType, priority )
+*/
+
+const WAVEFORM_FADE_DURATION = 1.0
+const RADIO_SPEECH_DELAY = 0.4
+const PLAYER_HEARS_RADIO = false
+const FRIENDLY_GRUNT_MINIMAP_MATERIAL = $"vgui/HUD/threathud_friendly_soldier"
+
+struct
+{
+ int nextVoiceIndex = 0
+ int DebugLevel = 0 // no debug = 0
+
+ table squadConversationDebounceTimers
+ array squadConversationPriorities
+
+ table aiTalkers
+ string lastWaveformTalker
+ var waveformRUI
+ table callerIDs
+} file
+
+function ClDialogue_Init()
+{
+ file.squadConversationPriorities = [
+ VO_PRIORITY_AI_CHATTER_LOWEST,
+ VO_PRIORITY_AI_CHATTER_LOW,
+ VO_PRIORITY_AI_CHATTER,
+ VO_PRIORITY_AI_CHATTER_HIGH
+ ]
+
+ file.aiTalkers = {
+ [ TEAM_IMC ] = {},
+ [ TEAM_MILITIA ] = {},
+ [ TEAM_BOTH ] = {},
+ }
+
+ InitGlobals()
+
+ RegisterSignal( "CancelConversation" )
+ RegisterSignal( "ConversationOver" )
+ RegisterSignal( "vdu_close" )
+ RegisterSignal( "vdu_open" )
+ RegisterSignal( "WaveformRuiExtended" )
+
+ AddCreateCallback( "npc_soldier", AI_Dialogue_General_Init )
+
+ //Debug stuff for QA
+ #document( "ClDebugPlayConversation", " Play conversation specified on this client." )
+ AddCallback_KillReplayStarted( ResetSquadConversationDebounceTimers )
+ AddCallback_KillReplayEnded( ResetSquadConversationDebounceTimers )
+
+ AddCallback_EntitiesDidLoad( EntitiesDidLoad )
+
+ #if SP
+ var dataTable = GetDataTable( $"datatable/caller_ids.rpak" )
+ #elseif MP
+ var dataTable = GetDataTable( $"datatable/caller_ids_mp.rpak" )
+ #endif
+ int rows = GetDatatableRowCount( dataTable )
+ for ( int i = 0 ; i < rows ; i++ )
+ {
+ string title = GetDataTableString( dataTable, i, GetDataTableColumnByName( dataTable, "title" ) )
+ asset image = GetDataTableAsset( dataTable, i, GetDataTableColumnByName( dataTable, "image" ) )
+ file.callerIDs[ title ] <- image
+ }
+
+ file.callerIDs[ "default" ] <- $"rui/hud/caller_ids/caller_id_01"
+}
+
+void function EntitiesDidLoad()
+{
+ VerifyConversationAliases()
+}
+
+
+void function AI_Dialogue_General_Init( entity guy )
+{
+ int team = guy.GetTeam()
+ guy.s.spawnTeam <- team // so they use native tongue if they switch teams
+
+ if ( !( team in file.aiTalkers ) )
+ return
+
+ local dialogue = {}
+ dialogue.voiceIndex <- file.nextVoiceIndex
+ file.nextVoiceIndex = (file.nextVoiceIndex + 1) % VOICE_COUNT
+
+ dialogue.enabled <- true
+
+ dialogue.hasBeenAlerted <- false
+ dialogue.currentConversationPriority <- 0 // track priority of current conversation for this ai
+
+ guy.s.dialogue <- dialogue
+
+ file.aiTalkers[ team ][ guy ] <- guy
+}
+
+
+function InitGlobals()
+{
+ if ( reloadingScripts )
+ return
+
+ level.ConversationIndices <- {}
+ level.CurrentPriority <- 0
+ level.AnnouncementPriority <- 1000
+ level.debugType <- ""
+
+ level.DefaultLineInterval <- 0.45 // default interval between lines in a conversations
+
+ level.ConversationIntervalMin <- 0.5 // interval between conversations in the form of delay before last vdu will close
+ level.ConversationIntervalMax <- 1.0
+
+ level.speakerBlacklist <- {}
+}
+
+function GetDialogueDebugLevel()
+{
+ return file.DebugLevel
+}
+
+function SetDialogueDebugLevel( int level )
+{
+ file.DebugLevel = level
+}
+
+
+int function GetTeamForConversation( entity player )
+{
+ if ( IsFFAGame() )
+ return TEAM_MILITIA
+
+ return player.GetTeam()
+}
+
+function PlayConversationToLocalClient( string convAlias )
+{
+ #if FACTION_DIALOGUE_ENABLED
+ return
+ #endif
+
+ if ( IsWatchingReplay() )
+ {
+ if ( file.DebugLevel > 1 )
+ printt( "Watching kill replay, not attempting conversation" )
+
+ return
+ }
+
+ local priority = GetConversationPriority( convAlias )
+ entity player = GetLocalClientPlayer()
+
+ if ( IsLobby() ) // TEMP: Too many MP assumptions that fail in lobby so just play directly
+ thread ClRunConversation( player, convAlias )
+ else
+ thread ClAttemptConversation( convAlias, player, priority )
+
+}
+
+void function ServerCallback_PlayTitanConversation( int conversationIndex )
+{
+ entity player = GetLocalClientPlayer()
+ string conversationType = GetConversationName( conversationIndex )
+ TitanCockpit_PlayDialog( GetLocalViewPlayer(), conversationType )
+}
+
+void function ServerCallback_PlayConversation( int conversationIndex )
+{
+ entity player = GetLocalClientPlayer()
+ string conversationType = GetConversationName( conversationIndex )
+ PlayConversationToLocalClient( conversationType )
+}
+
+void function ServerCallback_PlaySquadConversation( int conversationIndex, eHandle1, eHandle2, eHandle3, eHandle4 )
+{
+ #if GRUNT_CHATTER_MP_ENABLED
+ return
+ #endif
+
+ entity ai = GetEntityFromEncodedEHandle( eHandle1 )
+ if ( !IsAlive( ai ) )
+ return
+
+ local squad = []
+ squad.append( ai )
+
+ if ( eHandle2 != null )
+ {
+ entity ent = GetEntityFromEncodedEHandle( eHandle2 )
+ if ( IsValid( ent ) )
+ squad.append( ent )
+ }
+
+ if ( eHandle3 != null )
+ {
+ entity ent = GetEntityFromEncodedEHandle( eHandle3 )
+ if ( IsValid( ent ) )
+ squad.append( ent )
+ }
+ if ( eHandle4 != null )
+ {
+ entity ent = GetEntityFromEncodedEHandle( eHandle4 )
+ if ( IsValid( ent ) )
+ squad.append( ent )
+ }
+
+ bool foundNonSoldier = false
+ bool dialogueNotInitialized = false
+ foreach ( soldier in squad )
+ {
+ if ( !( "dialogue" in soldier.s ) )
+ dialogueNotInitialized = true
+
+ if ( soldier.GetSignifierName() == "npc_soldier" )
+ continue
+
+ foundNonSoldier = true
+ break
+ }
+
+ string conversationType = GetConversationName( conversationIndex )
+
+ if ( foundNonSoldier )
+ {
+ printt( "ABORTING CONVERSATION: Found non soldier in conversation: " + conversationType )
+ foreach ( soldier in squad )
+ {
+ printt( "Soldier is " + soldier.GetSignifierName() )
+ }
+// Assert( 0, "See above, found non soldier in conversation" )
+ return
+ }
+
+ if ( dialogueNotInitialized ) //JFS. Kill replay shenenigans with create callback not being run
+ {
+ printt( "ABORTING CONVERSATION: .s.dialogue not initialized: " + conversationType )
+ return
+ }
+
+ int team = ai.GetTeam()
+ Assert( team in file.aiTalkers, "Unknown AI team " + team )
+ local priority = GetConversationPriority( conversationType )
+
+ if ( file.DebugLevel > 1 )
+ {
+ printt( "Attempting squad conversation " + conversationType )
+ }
+
+ local currentConversationPriority = GetSquadConversationPriority( squad )
+
+ if ( priority <= currentConversationPriority ) // cancel if squad conversation is higher priority
+ {
+ if ( file.DebugLevel > 1 )
+ printt( "Priority of conversationType " + conversationType + " is " + priority + ", which is not higher than CurrentConversationPriority of " + currentConversationPriority + ", cancelling squad conversation " )
+
+ return
+ }
+
+ local activeTimer = GetSquadConversationDebounceTimer( conversationType )
+ if ( Time() < activeTimer )
+ {
+ if ( file.DebugLevel > 1 )
+ printt( "Can't play conversation " + conversationType + " because debounce timer hasn't expired, cancelling squad conversation" )
+
+ return
+ }
+
+ // if currently running a conversation, cancel because we have determined this is higher priority
+ if ( currentConversationPriority )
+ {
+ CancelSquadConversation( squad )
+ }
+
+ UpdateSquadConversationDebounceTimer( conversationType )
+
+ entity player = GetLocalViewPlayer()
+ thread ClRunSquadConversation( player, conversationType, squad )
+}
+
+void function TryCreateSquadConversationDebounceTimer( conversationType )
+{
+ if ( !(conversationType in file.squadConversationDebounceTimers) )
+ file.squadConversationDebounceTimers[ conversationType ] <- 0.0
+}
+
+void function UpdateSquadConversationDebounceTimer( string conversationType )
+{
+ TryCreateSquadConversationDebounceTimer( conversationType )
+
+ float debounceTime = GetConversationDebounce( conversationType )
+ file.squadConversationDebounceTimers[ conversationType ] = Time() + debounceTime
+}
+
+void function ResetSquadConversationDebounceTimers()
+{
+ file.squadConversationDebounceTimers.clear()
+}
+
+float function GetSquadConversationDebounceTimer( conversationType )
+{
+ TryCreateSquadConversationDebounceTimer( conversationType )
+
+ return expect float( file.squadConversationDebounceTimers[ conversationType ] )
+}
+
+function GetSquadConversationPriority( squad )
+{
+ local highest = 0
+ foreach ( guy in squad )
+ {
+ // handle deletion
+ if ( !IsValid( guy ) )
+ continue
+
+ if ( guy.s.dialogue.currentConversationPriority > highest )
+ {
+ highest = guy.s.dialogue.currentConversationPriority
+ }
+ }
+
+ return highest
+}
+
+function ClDebugPlayConversation( string conversationType )
+{
+ thread ClRunConversation( GetLocalViewPlayer(), conversationType )
+}
+
+function ClAttemptConversation( string conversationType, entity player, priority )
+{
+ // no conversations during scripted VDUs
+ if ( IsLockedVDU() )
+ return
+
+ if ( file.DebugLevel > 0 )
+ {
+ printt( "Attempting conversation " + conversationType )
+ }
+
+ // compare new priority to current priority
+ if ( AbortConversationDueToPriority( priority ) )
+ {
+ if ( file.DebugLevel > 1 )
+ printt( "Discarding conversation \"" + conversationType + "\" of priority " + priority +
+ " , which is less than either existing level priority " + level.CurrentPriority +
+ " or is less than existing announcement priority " + level.AnnouncementPriority +
+ " on player: " + player )
+ return
+ }
+
+ //if ( IsLockedVDU() )
+ //{
+ // if ( file.DebugLevel > 1 )
+ // printt( "VDU is locked on player " + player + ", discarding conversation " + conversationType )
+ // return
+ //}
+
+ int team = GetTeamForConversation( player )
+
+ #if SP
+ float startTime = Time()
+ QueueItem queueItem
+ queueItem = QueueAndWait( PRIORITY_NORMAL, CUTOFF_NEVER, conversationType )
+
+ OnThreadEnd(
+ function() : ( queueItem )
+ {
+ RemoveFromQueue( queueItem )
+ }
+ )
+
+ if ( Time() - startTime > 1.0 )
+ return
+ #endif
+
+ // cancel any playing conversation
+ if ( IsMultiplayer() )
+ {
+ CancelConversation( player )
+ }
+ else
+ {
+ #if HAS_BOSS_AI
+ CancelBossConversation()
+ #endif
+ }
+
+ level.CurrentPriority = priority
+
+ if ( file.DebugLevel > 1 )
+ printt( "Playing conversation \"" + conversationType + "\" of priority " + priority )
+
+ thread ClRunConversation( player, conversationType )
+}
+
+
+void function CancelSquadConversation( squad )
+{
+ if ( file.DebugLevel > 1 )
+ printt( "Cancelling squad conversation" )
+
+ FinishSquadConversation( squad )
+}
+
+void function FinishSquadConversation( squad )
+{
+ local currentConversationPriority = GetSquadConversationPriority( squad )
+
+ foreach ( guy in squad )
+ {
+ if ( IsValid( guy ) )
+ {
+ guy.Signal( "CancelConversation" )
+ guy.s.dialogue.currentConversationPriority = 0
+ }
+ }
+}
+
+void function CancelConversation( entity speakingEnt )
+{
+ if ( file.DebugLevel > 0 )
+ {
+ if ( level.CurrentPriority )
+ printt( "Cancelling conversation of priority " + level.CurrentPriority )
+ }
+
+ clGlobal.levelEnt.Signal( "CancelConversation" )
+ speakingEnt.Signal( "CancelConversation" ) //Speaking Ent can be different from listening player, i.e. clientPlayer
+
+ // signaling "CancelConversation" to the player should cause him to be clear level.CurrentPriority.
+ // This happend in the OnThreadEnd for RunConversation(...)
+ Assert( !level.CurrentPriority )
+}
+
+
+function AbortConversationDueToPriority( priority )
+{
+ // will return true if the conversation should not play
+ if ( priority > level.CurrentPriority )
+ return false
+
+ // conversations above level.AnnouncementPriority interupt conversations of the same priority level
+ if ( priority < level.AnnouncementPriority )
+ return true
+
+ return priority < level.CurrentPriority
+}
+
+function GetTeamForConversationFromSquad( player, squad )
+{
+ expect entity( player )
+
+ if ( squad )
+ return squad[0].s.spawnTeam
+
+ return GetTeamForConversation( player )
+}
+
+function ClRunConversation( entity player, string conversationType )
+{
+ OnThreadEnd(
+ function() : ( player )
+ {
+ level.CurrentPriority = 0
+ if ( IsValid( player ) )
+ player.Signal( "ConversationOver" )
+ if ( file.DebugLevel > 1 )
+ printt( "ConversationOver signal sent" )
+ }
+ )
+
+ level.debugType = conversationType
+
+ clGlobal.levelEnt.EndSignal( "CancelConversation" )
+ player.EndSignal( "OnDestroy" )
+
+ // returns a pseudo random conversation for this team
+ local conversation = ClSelectRandomConversation( conversationType, GetTeamForConversation( player ) )
+ // removes all but one alias choice in the conversation
+ conversation = RemoveChoicesFromConversation( conversation )
+
+ // temp debug print
+ local numRadio = 0
+ foreach ( elem in conversation )
+ {
+ if ( elem.dialogType == "radio" )
+ numRadio++
+ }
+
+// printt( " Starting conversation " + conversationType + " with " + numRadio + " lines for " + player.GetPlayerName() )
+
+// Dump( conversation, 2 )
+ foreach ( index, elem in conversation )
+ {
+ if ( file.DebugLevel > 1 )
+ {
+ printt( " Running elem " + elem )
+ }
+
+ ClRunConversationElement( player, elem ) // in the future indexed NPC speakers need to be passed as well for AI dialogue
+
+ if ( file.DebugLevel > 1 )
+ {
+ printt( " Ended elem " + elem )
+ }
+ }
+}
+
+function RandomizeSquadVoices( squad )
+{
+ //local squadCopy = clone squad
+ //squadCopy.randomize()
+ //for ( int i = 0; i < squadCopy.len(); i++ )
+ //{
+ // squadCopy[i].s.dialogue.voiceIndex = i % VOICE_COUNT
+ // printt( "index is " + squadCopy[i].s.dialogue.voiceIndex )
+ //}
+
+ int offset = RandomInt( VOICE_COUNT )
+ for ( int i = 0; i < squad.len(); i++ )
+ {
+ squad[i].s.dialogue.voiceIndex = ( i + offset ) % VOICE_COUNT
+// printt( "index is " + squadCopy[i].s.dialogue.voiceIndex )
+ }
+}
+
+function ClRunSquadConversation( entity player, string conversationType, squad )
+{
+ OnThreadEnd(
+ function () : ( squad )
+ {
+ FinishSquadConversation( squad )
+ }
+ )
+
+ // so the squad members each has a different voice
+ RandomizeSquadVoices( squad )
+
+ squad[0].EndSignal( "CancelConversation" )
+ player.EndSignal( "OnDestroy" )
+
+ local priority = GetConversationPriority( conversationType )
+ squad[0].s.dialogue.currentConversationPriority = priority
+
+ level.debugType = conversationType
+
+ // returns a pseudo random conversation for this team
+ local conversation = ClSelectRandomConversation( conversationType, GetTeamForConversationFromSquad( player, squad ) )
+ // removes all but one alias choice in the conversation
+ conversation = RemoveChoicesFromConversation( conversation )
+
+
+ foreach ( index, elem in conversation )
+ {
+ if ( file.DebugLevel > 1 )
+ {
+ printt( " Running elem " + elem )
+ }
+
+ ClRunSquadConversationElement( player, elem, squad ) // in the future indexed NPC speakers need to be passed as well for AI dialogue
+
+ if ( file.DebugLevel > 1 )
+ {
+ printt( " Ended elem " + elem )
+ }
+ }
+}
+
+function ClSelectRandomConversation( string conversationType, team )
+{
+ ConversationStruct convStruct = GetConversationStruct( conversationType )
+ array conversationArray
+
+ expect int( team )
+
+ Assert( team in convStruct.conversationTable )
+ conversationArray = convStruct.conversationTable[ team ]
+
+ if ( conversationArray.len() == 0 )
+ {
+ conversationArray = convStruct.conversationTable[ TEAM_BOTH ]
+ Assert( conversationArray.len() > 0, "Conversation " + conversationType + " isn't available for team ID: " + team )
+ }
+
+ Assert( IsArray( conversationArray ) )
+
+ // we cycle through a permutation of the conversations.
+
+ // level.ConversationIndices is a map from array to index.
+ // Thus it is actually indexed by the array itself.
+ if ( !( conversationArray in level.ConversationIndices ) || level.ConversationIndices[ conversationArray ] >= conversationArray.len() )
+ {
+ // randomize it each time we use up all choices in the conversation
+ conversationArray.randomize()
+ level.ConversationIndices[ conversationArray ] <- 0
+ }
+
+ local conversation = conversationArray[ level.ConversationIndices[ conversationArray ] ]
+
+ // next time we'll play the next conversation in the system
+ level.ConversationIndices[ conversationArray ]++
+
+ return conversation
+}
+
+function RemoveChoicesFromConversation( conversation )
+{
+ // removes all but one alias choice, per team, in the conversation
+ local _conversation = []
+ foreach ( elem in conversation )
+ {
+ local _elem = RemoveChoicesFromElem( elem )
+ _conversation.append( _elem )
+ }
+
+ return _conversation
+}
+
+function RemoveChoicesFromElem( elem )
+{
+ local _elem = {}
+ _elem.dialogType <- elem.dialogType
+
+ switch ( elem.dialogType )
+ {
+ case "radio":
+ // elem.choices should be an array of lines to choose from randomly
+ Assert( IsArray( elem.choices ) )
+
+ if ( elem.choices.len() )
+ {
+ _elem.alias <- elem.choices.getrandom()
+ }
+ else
+ {
+ _elem.alias <- null
+ }
+
+ if ( "delay" in elem )
+ _elem.delay <- elem.delay
+ break
+
+ case "multiple":
+ local min = 0
+ local max = elem.choices.len()
+
+ if ( "min" in elem )
+ min = elem.min
+ if ( "max" in elem )
+ {
+ max = elem.max
+ if ( max > elem.choices.len() )
+ max = elem.choices.len()
+ }
+ int count = RandomIntRange( min, max + 1 )
+ Assert( count >= min )
+ Assert( count <= max )
+
+ if ( file.DebugLevel > 1 )
+ printt( "parsing dialogtype multiple. count: " + count )
+
+ if ( elem.randomize )
+ elem.choices.randomize()
+
+ local _choices = []
+ for ( int i = 0; i < count; i++ )
+ {
+ local subelem = RemoveChoicesFromElem( elem.choices[i] )
+ _choices.append( subelem )
+ }
+ _elem.choices <- _choices
+ break
+
+ default:
+ _elem = elem
+ break
+ }
+
+ return _elem
+}
+
+function ClRunConversationElement( entity player, elem )
+{
+ if ( "chance" in elem )
+ {
+ local rnd = RandomFloat( 1.0 )
+ if ( rnd >= elem.chance )
+ {
+ if ( file.DebugLevel > 1 )
+ printt( " Skipping random elem: " + rnd + " >= " + elem.chance )
+
+ return
+ }
+ }
+
+ Assert( IsValid( player ) )
+
+ switch ( elem.dialogType )
+ {
+ case "radio":
+ case "fx":
+ local duration = 0
+ if ( elem.alias == null )
+ {
+ CodeWarning( "Sound alias for " + level.debugType + " not found!\n" )
+ duration = 1
+ }
+ else
+ {
+ Assert( IsString( elem.alias ) )
+ if ( SpeakerIsBlacklisted( elem.alias ) )
+ {
+ if ( file.DebugLevel > 1 )
+ printt( "Speaker is blacklisted, not playing radio alias: " + elem.alias )
+
+ return
+ }
+
+ duration = DoGeneralRadioSound( elem.alias, null, player )
+ }
+
+ if ( "delay" in elem )
+ duration += elem.delay
+ else
+ duration += level.DefaultLineInterval
+ wait duration
+ break
+
+ case "music":
+ Assert( IsString( elem.alias ) )
+ local duration
+ duration = DoPlayerMusic( player, elem.alias )
+ if ( "halt_conversation" in elem )
+ wait duration
+ break
+
+ case "dispatch":
+ Assert( false ) // Can't run this script because "squad" is undefined
+ /*
+ Assert( squad != null, "Can't do a speech conversation without using PlaySquadConversation" )
+ // choose an AI to say this
+ local guy = squad[0]
+
+ //SpeakingGuy = guy
+
+ if ( !IsAlive( guy ) )
+ {
+ // We failed to find a guy to speak. Could be that guys have died or walked too far away since the conversation started.
+ // for now just bail.
+
+ if ( file.DebugLevel > 1 )
+ printl( " Bailing: no guy left to talk" )
+
+ CancelConversation( player )
+ break
+ }
+
+ // elem.choices should be an array of lines to choose from randomly
+ Assert( IsArray( elem.choices ) )
+
+ local dialogueChoice = elem.choices.getrandom()
+ local startTime = Time()
+
+ if ( typeof dialogueChoice == "string" )
+ {
+ waitthread DoGuySound( guy, guy, dialogueChoice, 0 )
+ }
+ else
+ {
+ AssertVoiceAliasDataIsValid( dialogueChoice )
+
+ if ( file.DebugLevel > 1 )
+ printt( "Speaking ai: " + guy.GetEntIndex() + " voice index " + guy.s.dialogue.voiceIndex )
+ local aliases = GetAliases( guy, dialogueChoice )
+ DoGuySpeechLine( guy, aliases )
+ }
+
+ //printt( "time passed in dialogue " + ( Time() - startTime ) )
+ wait 0.3 // delay between speech lines
+
+// if ( IsAlive( guy ) )
+// guy.Signal( "FinishedLine", { alias = aliases.radioAlias } )
+ */
+ break
+
+
+ case "wait":
+ wait RandomFloatRange( elem.durationMin, elem.durationMax )
+ break
+
+ case "flag_set":
+ if ( file.DebugLevel > 1 )
+ printt( " Setting flag " + elem.flag )
+ FlagSet( expect string( elem.flag ) )
+ break
+
+ case "flag_clear":
+ if ( file.DebugLevel > 1 )
+ printt( " Clearing flag " + elem.flag )
+ FlagClear( expect string( elem.flag ) )
+ break
+
+ case "flag_wait":
+ if ( file.DebugLevel > 1 )
+ printt( " Waiting on flag " + elem.flag )
+
+ FlagWait( expect string( elem.flag ) )
+
+ if ( file.DebugLevel > 1 )
+ printt( " Flag wait complete" )
+ break
+
+ case "function":
+ if ( file.DebugLevel > 1 )
+ printt( " Calling function " + elem["func"] )
+
+ elem[ "func" ]()
+ break
+
+ case "thread":
+ if ( file.DebugLevel > 1 )
+ printt( " Calling thread " + elem["func"] )
+
+ local func = elem[ "func" ]
+ thread func()
+ break
+
+ #if HAS_BOSS_AI
+ case "boss_titan_conversation":
+ RunBossConversation( player, expect string( elem.speaker ), expect string( elem.event ) )
+ break
+ #endif
+
+ case "multiple":
+ foreach ( subelem in elem.choices )
+ {
+ // choices and randomization etc removed in RemoveChoicesFromElem( ... )
+ ClRunConversationElement( player, subelem )
+ }
+ break
+
+ default:
+ Assert( false, "Invalid conversation element " + elem.dialogType )
+ }
+}
+
+function ClRunSquadConversationElement( entity player, elem, squad = null )
+{
+ Assert( IsValid( player ) )
+
+ switch ( elem.dialogType )
+ {
+ case "speech":
+ Assert( squad != null, "Can't do a speech conversation without using PlaySquadConversation" )
+ // choose an AI to say this
+ entity guy = expect entity( ChooseSpeakingAI( squad, elem.speakerIndex ) )
+
+ //SpeakingGuy = guy
+
+ if ( !guy )
+ {
+ // We failed to find a guy to speak. Could be that guys have died or walked too far away since the conversation started.
+ // for now just bail.
+
+ if ( file.DebugLevel > 1 )
+ printl( " Bailing: no guy left to talk" )
+
+ CancelSquadConversation( squad )
+ break
+ }
+
+ if ( !IsAlive( guy ) || guy.ContextAction_IsMeleeExecution() )
+ {
+ // this guy is dead. we should wait a moment and then start an "are you there?" dialogType conversation.
+ // for now just bail.
+
+ if ( file.DebugLevel > 1 )
+ printl( " Bailing: next guy is dead" )
+
+ CancelSquadConversation( squad )
+ break
+ }
+
+ // elem.choices should be an array of lines to choose from randomly
+ Assert( IsArray( elem.choices ) )
+
+ local dialogueChoice = elem.choices.getrandom() //Seems like this is unnecessary since we remove other choices earlier in ClRunSquadConversation, but it's too late in the project to chance changing this...
+
+ AssertVoiceAliasDataIsValid( dialogueChoice )
+
+ if ( file.DebugLevel > 1 )
+ printt( "Speaking ai: " + guy.GetEntIndex() + " voice index " + guy.s.dialogue.voiceIndex )
+ local aliases = GetAliases( guy, dialogueChoice )
+ DoGuySpeechLine( guy, aliases )
+ //wait RandomFloatRange( 0.1, 0.2 )// delay between speech lines. Taking out for now, might bring back later.
+
+
+ break
+
+ case "dispatch":
+ Assert( squad != null, "Can't do a speech conversation without using PlaySquadConversation" )
+ // choose an AI to say this
+ entity guy = expect entity( squad[0] )
+
+ //SpeakingGuy = guy
+
+ if ( !IsAlive( guy ) )
+ {
+ // We failed to find a guy to speak. Could be that guys have died or walked too far away since the conversation started.
+ // for now just bail.
+
+ if ( file.DebugLevel > 1 )
+ printl( " Bailing: no guy left to talk" )
+
+ CancelSquadConversation( squad )
+ break
+ }
+
+ // elem.choices should be an array of lines to choose from randomly
+ Assert( IsArray( elem.choices ) )
+
+ local dialogueChoice = elem.choices.getrandom() //Seems like this is unnecessary since we remove other choices earlier in ClRunSquadConversation, but it's too late in the project to chance changing this...
+ local startTime = Time()
+
+ if ( typeof dialogueChoice == "string" )
+ {
+ waitthread DoGuySound( guy, guy, dialogueChoice, 0 )
+ }
+ else
+ {
+ AssertVoiceAliasDataIsValid( dialogueChoice )
+
+ if ( file.DebugLevel > 1 )
+ printt( "Speaking ai: " + guy.GetEntIndex() + " voice index " + guy.s.dialogue.voiceIndex )
+ local aliases = GetAliases( guy, dialogueChoice )
+ DoGuySpeechLine( guy, aliases )
+ }
+
+ //printt( "time passed in dialogue " + ( Time() - startTime ) )
+ wait 0.3 // delay between speech lines
+
+// if ( IsAlive( guy ) )
+// guy.Signal( "FinishedLine", { alias = aliases.radioAlias } )
+
+ break
+
+ case "multiple":
+ foreach ( subelem in elem.choices )
+ {
+ // choices and randomization etc removed in RemoveChoicesFromElem( ... )
+ ClRunSquadConversationElement( player, subelem, squad )
+ }
+ break
+
+ default:
+ Assert( false, "Invalid conversation element " + elem.dialogType )
+ }
+}
+
+function DoGeneralRadioSound( alias, sourceGuy, entity player )
+{
+ if ( file.DebugLevel > 1 )
+ {
+ printt( "Playing radio sound alias: " + alias + " to " + player.GetPlayerName() )
+ }
+
+ //printt( "playing alias: " + alias + " to " + player.GetPlayerName() )
+ local duration = GetSoundDuration( alias )
+ EmitSoundOnEntity( player, alias )
+ thread EndPlayerSound( player, sourceGuy, alias, duration )
+
+ return duration
+}
+
+function EndPlayerSound( player, sourceGuy, alias, delay = 0 )
+{
+ // this function is threaded but it needs to end with the conversation
+ player.EndSignal( "ConversationOver" )
+ player.EndSignal( "OnDestroy" )
+
+ if ( sourceGuy )
+ EndSignal( sourceGuy, "OnDeath" )
+
+ OnThreadEnd(
+ function () : ( player, alias )
+ {
+ if( IsValid( player ) )
+ StopSoundOnEntity( player, alias )
+ }
+ )
+
+ // necessary because of the OnThreadEnd
+ wait delay // this is for delayed radio playback of AI dialogue
+}
+
+function DoPlayerMusic( player, alias )
+{
+ if ( file.DebugLevel > 1 )
+ {
+ printt( "Playing music alias: " + alias + " to " + player )
+ }
+
+ local duration = GetSoundDuration( alias )
+ EmitSoundOnEntity( player, alias )
+ return duration
+}
+
+function GetAvailableTalkers( team )
+{
+ local Array = []
+ local ai = file.aiTalkers[ team ]
+
+ foreach ( ent in clone ai )
+ {
+ if ( IsValid( ent ) )
+ {
+ Array.append( ent )
+ continue
+ }
+
+ delete ai[ ent ]
+ }
+
+ return Array
+}
+
+function ChooseSpeakingAI( squad, speakerIndex )
+{
+ // originator has to be alive
+ if ( !IsAlive( expect entity( squad[0] ) ) )
+ return null
+
+ if ( speakerIndex >= squad.len() )
+ {
+ // find some other dude in the squad
+ for ( local i = squad.len() - 1; i > 0; i-- ) //Dont' want to pick squad[0] since he originated the conversation
+ {
+ if ( IsAlive( expect entity( squad[i] ) ) )
+ return squad[i]
+ }
+
+ return null
+ }
+
+ if ( !IsAlive( expect entity( squad[ speakerIndex ] ) ) )
+ return null
+
+ return squad[ speakerIndex ]
+
+/*
+ entity player = GetLocalViewPlayer()
+ local guys = GetAvailableTalkers( squad[0].GetTeam() )
+
+ if ( RandomInt( 3 ) == 0 )
+ guys.randomize()
+ else
+ guys = ArrayClosest( guys, player.GetOrigin() )
+
+ local bestOptions = []
+ local options = []
+
+ // get two nearest AI not already in speakerIndexChoices
+ foreach ( guy in guys )
+ {
+ if ( !GuyIsEligibleForDialogue( guy ) )
+ continue
+
+ bool alreadyUsed = false
+ bool sameVoice = false
+ foreach ( otherguy in speakerIndexChoices )
+ {
+ if ( guy == otherguy )
+ {
+ alreadyUsed = true
+ break
+ }
+ if ( guy.s.dialogue.voiceIndex == otherguy.s.dialogue.voiceIndex )
+ sameVoice = true
+ }
+
+ if ( alreadyUsed )
+ continue
+
+ if ( !sameVoice )
+ bestOptions.append( guy )
+
+ options.append( guy )
+ if ( options.len() >= 2 )
+ break
+ }
+
+ if ( bestOptions.len() >= 2 )
+ options = bestOptions
+
+ if ( options.len() <= 0 )
+ return null
+
+ local guy = options.getrandom()
+
+ speakerIndexChoices[speakerIndex] <- guy
+
+ return guy
+ */
+}
+
+function DoGuySound( guy, sourceGuy, alias, delay )
+{
+ expect entity( guy )
+
+ Assert( IsAlive( guy ) )
+
+ OnThreadEnd(
+ function () : (guy, alias)
+ {
+ if ( !IsValid( guy ) )
+ return
+
+ //Let the AI have a chance to finish speaking
+ if ( !IsAlive( guy ) || guy.ContextAction_IsMeleeExecution() )
+ StopSoundOnEntity( guy, alias )
+ }
+ )
+
+ EndSignal( guy, "OnDeath" )
+ EndSignal( guy, "OnDestroy" )
+ EndSignal( guy, "OnSyncedMeleeVictim" )
+
+ if ( sourceGuy && sourceGuy != guy )
+ sourceGuy.EndSignal( "OnDeath" )
+
+ wait delay
+
+ EmitSoundOnEntity( guy, alias )
+
+ wait GetSoundDuration( alias )
+}
+
+function DoGuySoundSilentWait( guy, sourceGuy, alias )
+{
+ expect entity( guy )
+
+ Assert( IsAlive( guy ) )
+
+ guy.EndSignal( "OnDeath" )
+ guy.EndSignal( "OnDestroy" )
+ if ( sourceGuy && sourceGuy != guy )
+ {
+ sourceGuy.EndSignal( "OnDeath" )
+ sourceGuy.EndSignal( "OnDestroy" )
+ }
+
+ local duration = GetSoundDuration( alias )
+ wait duration
+}
+
+function DoGuySpeechLine( guy, aliases )
+{
+ AssertGuyIsDialogueReady( guy )
+
+ local radioAlias = aliases.radioAlias
+
+ if ( file.DebugLevel > 1 )
+ {
+ printt( " Guy " + guy.GetTargetName() + ": " + radioAlias )
+ DebugDrawLine( GetLocalViewPlayer().GetOrigin(), guy.GetOrigin(), 255,255,255, true, 3.0 )
+ DebugDrawText( guy.GetOrigin() + Vector(0,0,60), radioAlias, true, 6.0 )
+ }
+
+ waitthread DoGuySound( guy, guy, radioAlias, 0 )
+}
+
+/*
+function DoGeneralRadioSound_Nonblocking( alias, sourceGuy, delay )
+{
+ if ( PLAYER_HEARS_RADIO )
+ {
+ thread DoPlayerSound( GetLocalViewPlayer(), sourceGuy, alias, delay )
+ }
+ else
+ {
+ local guys = GetAvailableEnemyTalkers()
+ local playerOrigin = GetLocalViewPlayer().GetOrigin()
+ guys = ArrayClosest( guys, playerOrigin )
+
+ local guyCount = 0
+ foreach ( guy in guys )
+ {
+ if ( guyCount >= MaxRadioPlayGuys )
+ break
+
+ if ( DistanceSqr( guy.GetOrigin(), playerOrigin ) > RadioPlayDistance * RadioPlayDistance )
+ break
+
+ if ( guy == sourceGuy )
+ continue
+
+ if ( !GuyIsEligibleForDialogue( guy ) )
+ continue
+
+ thread DoGuyRadioSound( guy, sourceGuy, alias, delay )
+ guyCount++
+ }
+ }
+}
+*/
+
+function GuyIsEligibleForDialogue( guy )
+{
+ expect entity( guy )
+
+ if ( !("dialogue" in guy.s ) )
+ return false
+
+ if ( !IsAlive( guy ) )
+ return false
+
+ if ( DistanceSqr( guy.GetOrigin(), GetLocalViewPlayer().GetOrigin() ) > MAX_VOICE_DIST_SQRD )
+ return false
+
+ if ( !guy.s.dialogue.enabled )
+ return false
+
+ return true
+}
+
+function AssertVoiceAliasDataIsValid( aliasData )
+{
+ // NONE OF THE FOLLOWING ASSERTS SHOULD HIT IF YOU USE AI_Dialogue_AliasAllVoices OR AI_Dialogue_AliasSingleVoice for aliasData.
+
+ Assert( IsArray( aliasData ) )
+ Assert( aliasData.len() == VOICE_COUNT )
+ // each voice should have a soundalias for the radio sound
+ Assert( IsString( aliasData[0] ) )
+}
+
+
+function GetAliases( guy, dialogue, radioDelayOverride = null )
+{
+ local aliases = {}
+
+ aliases.radioAlias <- null
+ aliases.radioDelay <- null
+
+ aliases.radioAlias = dialogue[ guy.s.dialogue.voiceIndex ]
+ aliases.radioDelay = 0
+
+ if ( radioDelayOverride != null )
+ {
+ aliases.radioDelay = radioDelayOverride
+ }
+
+ return aliases
+}
+
+function AssertGuyIsDialogueReady( guy )
+{
+ Assert( "dialogue" in guy.s, guy + " not set up for dialogue; call AI_Dialogue_Scripted_Init on him if this is a scripted conversation" )
+}
+
+function VerifyConversationAliases()
+{
+ if ( !GetDeveloperLevel() )
+ return
+
+ local e = {}
+ e.count <- 0
+ e.failed <- {}
+ e.tried <- {}
+ local conv
+ foreach ( conversationName, convStruct in GetAllConversationData() )
+ {
+ foreach ( conv in convStruct.conversationTable[ TEAM_IMC ] )
+ {
+ VerifyConversation( conv, e )
+ }
+ foreach ( conv in convStruct.conversationTable[ TEAM_MILITIA ] )
+ {
+ VerifyConversation( conv, e )
+ }
+ }
+
+ if ( e.failed.len() )
+ {
+ local failed = []
+ foreach ( alias in e.failed )
+ {
+ failed.append( alias )
+ }
+ failed.sort( SortAlphabetize )
+ foreach ( alias in failed )
+ {
+ CodeWarning( "Sound alias " + alias + " not found!\n" )
+ }
+
+ }
+}
+
+function VerifyConversation( convArray, e )
+{
+ foreach ( conv in convArray )
+ {
+ if ( !( "choices" in conv ) )
+ continue
+
+ if ( conv.dialogType == "temp_text" )
+ continue
+
+ foreach ( Array in conv.choices )
+ {
+ if ( typeof Array == "string" )
+ {
+ VerifyConversationAlias( Array, e )
+ continue
+ }
+
+ foreach ( aliases in Array )
+ {
+ if ( typeof aliases == "array" )
+ {
+ foreach ( alias in aliases )
+ {
+ VerifyConversationAlias( alias, e )
+ }
+ }
+ else
+ {
+ VerifyConversationAlias( aliases, e )
+ }
+ }
+ }
+ }
+}
+
+function VerifyConversationAlias( alias, e )
+{
+ if ( alias in e.tried )
+ return
+ e.tried[ alias ] <- alias
+
+ local result = DoesAliasExist( alias )
+
+ if ( !result )
+ {
+ if ( !( alias in e.failed ) )
+ e.failed[ alias ] <- alias
+ }
+}
+
+function AddSpeakerToBlacklist( character )
+{
+ if ( character in level.speakerBlacklist )
+ return
+
+ level.speakerBlacklist[ character ] <- true
+}
+
+function RemoveSpeakerFromBlacklist( character )
+{
+ if ( !(character in level.speakerBlacklist ) )
+ return
+
+ delete level.speakerBlacklist[ character ]
+}
+
+function SpeakerIsBlacklisted( alias )
+{
+ foreach( speaker, _ in level.speakerBlacklist )
+ {
+ if ( alias.find( speaker ) != null )
+ return true
+ }
+
+ return false
+}
+
+
+var function CreateWaveform( string title, int team, float duration, entity speaker = null, bool radioIntercept = false )
+{
+ var waveformRUI = file.waveformRUI
+ float timeExtension = 0.2 // FADEIN_TIME in waveform.rui
+
+ if ( title != file.lastWaveformTalker || file.waveformRUI == null )
+ {
+ if ( file.waveformRUI != null )
+ {
+ DestroyWaveform( file.waveformRUI, false )
+ }
+
+ waveformRUI = RuiCreate( $"ui/waveform.rpak", clGlobal.topoFullScreen, RUI_DRAW_HUD, 0 )
+ timeExtension = 0.0
+
+ entity player = GetLocalClientPlayer()
+ if ( IsValid( player ) )
+ {
+ if ( team == TEAM_MILITIA )
+ EmitSoundOnEntity( player, "ui_callerid_chime_friendly" )
+ else if ( team == TEAM_IMC )
+ EmitSoundOnEntity( player, "ui_callerid_chime_enemy" )
+ }
+ }
+ else
+ {
+ Signal( clGlobal.levelEnt, "WaveformRuiExtended" )
+ }
+
+ RuiSetFloat( waveformRUI, "soundStartTime", Time() - timeExtension )
+ RuiSetFloat( waveformRUI, "soundDuration", duration + timeExtension )
+ RuiSetFloat( waveformRUI, "fadeOutDuration", WAVEFORM_FADE_DURATION )
+ RuiSetString( waveformRUI, "speakerName", title )
+ RuiSetBool( waveformRUI, "intercepting", radioIntercept )
+ RuiSetBool( waveformRUI, "isMP", IsMultiplayer() )
+ RuiSetResolutionToScreenSize( waveformRUI )
+
+ file.lastWaveformTalker = title
+
+ if ( team == TEAM_MILITIA )
+ RuiSetFloat3( waveformRUI, "tintColor", HIGHLIGHT_COLOR_FRIENDLY )
+ else if ( team == TEAM_IMC )
+ RuiSetFloat3( waveformRUI, "tintColor", HIGHLIGHT_COLOR_ENEMY )
+ else
+ RuiSetFloat3( waveformRUI, "tintColor", HIGHLIGHT_COLOR_NEUTRAL )
+
+ asset image = GetImageForName( title )
+ RuiSetImage( waveformRUI, "bgImage", image )
+
+ if ( speaker != null && !speaker.IsPhaseShifted() )
+ {
+ int attachment = speaker.LookupAttachment( "HEADFOCUS" )
+
+ if ( attachment <= 0 )
+ attachment = speaker.LookupAttachment( "REF" )
+
+ RuiSetBool( waveformRUI, "hasConnectingLine", true )
+ RuiTrackFloat3( waveformRUI, "connectingLineWorldPos", speaker, RUI_TRACK_POINT_FOLLOW, attachment )
+ }
+
+ printt( "RUI TRACKING SOUND METER!" )
+ RuiTrackFloat( waveformRUI, "level", null, RUI_TRACK_SOUND_METER, 0 )
+
+ file.waveformRUI = waveformRUI
+ return waveformRUI
+}
+
+void function DestroyWaveform_Immediate( var rui )
+{
+ DestroyWaveform( rui, false )
+}
+
+void function DestroyWaveform( var rui, bool doWait = true )
+{
+ EndSignal( clGlobal.levelEnt, "WaveformRuiExtended" )
+
+ // usually wait before destroying so waveform can die down; otherwise just kill it
+ if ( doWait )
+ wait WAVEFORM_FADE_DURATION
+
+ if ( rui == file.waveformRUI )
+ file.waveformRUI = null
+ RuiDestroyIfAlive( rui )
+}
+
+
+//Battle Chatter, Grunt Chatter_MP, Spectre Chatter_MP and TitanOS dialogue should go into this. They all fall into the
+//"One liner, exact sound alias needs to be generated dynamically on the client" category of dialogue.
+//Faction Leader stuff used to use this, but since we need some of them to persist through death they now call PlayAnnouncerLineThroughDeathWithPriority() instead
+void function PlayOneLinerConversationOnEntWithPriority( string conversationName, string soundAlias, entity ent, int priority )
+{
+ bool printDebug = GetDialogueDebugLevel() > 0
+ if ( printDebug )
+ printt( "PlayOneLinerConversationOnEntWithPriority, ConversationName: " + conversationName )
+
+ if ( AbortConversationDueToPriority( priority ) )
+ {
+ if ( printDebug )
+ printt( "Aborting conversation: " + conversationName + " due to higher priority conversation going on" )
+ return
+ }
+
+ CancelConversation( ent )
+
+ SetConversationLastPlayedTime( conversationName, Time() )
+
+ thread PlayOneLinerConversationOnEntWithPriority_internal( soundAlias, ent, priority ) //Only thread this off once we've done the priority check since threading is expensive
+
+}
+
+void function PlayOneLinerConversationOnEntWithPriority_internal( string soundAlias, entity ent, int priority )
+{
+ ent.EndSignal( "CancelConversation" )
+ ent.EndSignal( "OnDeath" )
+
+ clGlobal.levelEnt.EndSignal( "CancelConversation" )
+
+ OnThreadEnd(
+ function() : ( soundAlias, ent )
+ {
+ //printt( "OnThreadEnd of PlayOneLinerConversationOnEntWithPriority_internal, should try to end " + soundAlias + " onEnt: " + ent )
+ level.CurrentPriority = 0
+ if( IsValid( ent ) )
+ StopSoundOnEntity( ent, soundAlias )
+ }
+ )
+
+ level.CurrentPriority = priority
+
+ bool printDebug = GetDialogueDebugLevel() > 0
+ if ( printDebug )
+ printt( "PlayOneLinerConversationOnEntWithPriority_internal, soundAlias: " + soundAlias )
+
+ var handle = EmitSoundOnEntity( ent, soundAlias )
+
+ WaitSignal( handle, "OnSoundFinished" )
+}
+
+void function PlayAnnouncerLineThroughDeathWithPriority( string conversationName, string soundAlias, int priority, string waveformName = "" ) //Used primarily with Faction Leader announcements that might need to go through death, e.g. win announcement
+{
+ bool printDebug = GetDialogueDebugLevel() > 0
+ if ( printDebug )
+ printt( "PlayAnnouncerLineThroughDeathWithPriority, ConversationName: " + conversationName )
+
+ if ( AbortConversationDueToPriority( priority ) )
+ {
+ if ( printDebug )
+ printt( "Aborting conversation: " + conversationName + " due to higher priority conversation going on" )
+ return
+ }
+
+ entity localClientPlayer = GetLocalClientPlayer()
+
+ CancelConversation( localClientPlayer )
+
+ SetConversationLastPlayedTime( conversationName, Time() )
+
+ thread PlayAnnouncerLineThroughDeathWithPriority_internal( soundAlias, localClientPlayer, priority, waveformName ) //Only thread this off once we've done the priority check since threading is expensive
+
+}
+
+void function PlayAnnouncerLineThroughDeathWithPriority_internal( string soundAlias, entity localClientPlayer, int priority, string waveformName )
+{
+ clGlobal.levelEnt.EndSignal( "CancelConversation" )
+
+ var rui
+ if ( waveformName != "" )
+ {
+ rui = CreateWaveform( waveformName, TEAM_MILITIA, 10.0 )
+ }
+
+ OnThreadEnd(
+ function() : ( rui, soundAlias, localClientPlayer )
+ {
+ //printt( "OnThreadEnd of PlayAnnouncerLineThroughDeathWithPriority_internal, should try to end " + soundAlias + " onLocalClientPlayer: " + localClientPlayer )
+ level.CurrentPriority = 0
+ if( IsValid( localClientPlayer ) )
+ StopSoundOnEntity( localClientPlayer, soundAlias )
+ if( rui != null )
+ {
+ thread DestroyWaveform( rui )
+ }
+ }
+ )
+
+ level.CurrentPriority = priority
+
+ var handle = EmitSoundOnEntity( localClientPlayer, soundAlias )
+
+ SetPlayThroughKillReplay( handle )
+
+ WaitSignal( handle, "OnSoundFinished" )
+}
+
+asset function GetImageForName( string title )
+{
+ if ( title in file.callerIDs )
+ return file.callerIDs[ title ]
+ return file.callerIDs[ "default" ]
+}
diff --git a/mods/Nachos.EnableIDCards/mod/scripts/vscripts/conversation/cl_faction_dialogue.gnut b/mods/Nachos.EnableIDCards/mod/scripts/vscripts/conversation/cl_faction_dialogue.gnut
new file mode 100644
index 0000000..e02274f
--- /dev/null
+++ b/mods/Nachos.EnableIDCards/mod/scripts/vscripts/conversation/cl_faction_dialogue.gnut
@@ -0,0 +1,272 @@
+global function ServerCallback_PlayFactionDialogue
+global function ServerCallback_ForcePlayFactionDialogue
+global function GenerateFactionDialogueAlias
+global function PlayFactionDialogueOnLocalClientPlayer
+global function ServerCallback_SpawnFactionCommanderInDropship
+
+global function IsFactionLeaderOverriden
+global function GetOverrideFactionLeader
+global function SetOverrideFactionLeader
+global function ClearOverrideFactionLeader
+
+global struct OverrideFactionLeaderStruct //Note that this is meant for game mode specific faction leader overrides like Frontier Defence. The alternative for this is to make an actual faction but have a column that specifies which game mode it is overriden for, etc, but then you also have to do work to make it not show up as a faction you can pick, make sure you earn the faction XP for the faction you had before, add persistence data, etc.
+{
+ string dialoguePrefix
+ string factionName
+ asset factionLogo
+ bool useWaveForm
+ void functionref( int shipEHandle, float dropshipSpawnTime ) dropshipIntroOverride
+}
+
+struct
+{
+ #if DEV
+ int factionLeaderAnimIndex = -1
+ bool forceEasterEgg = false
+ #endif
+ bool factionLeaderOverriden = false
+ OverrideFactionLeaderStruct& overrideFactionLeaderData
+}
+file
+
+#if DEV
+ global function Dev_SetFactionLeaderAnimIndex
+ global function Dev_ForceEasterEgg
+#endif
+
+string function GenerateFactionDialogueAlias( entity player, string conversationSuffix )
+{
+ string factionPrefix
+ if ( IsFactionLeaderOverriden() )
+ {
+ factionPrefix = file.overrideFactionLeaderData.dialoguePrefix //Mainly for Frontier Defence, where it has its own announcer. Don't want to override GetFactionChoice() because that's used by FactionXP etc.
+ }
+ else
+ {
+ string factionChoice = GetFactionChoice( player )
+ factionPrefix = factionLeaderData[ factionChoice ].dialoguePrefix
+ }
+
+ string result = "diag_" + factionPrefix + "_" + conversationSuffix
+ return result
+
+}
+
+void function ServerCallback_PlayFactionDialogue( int conversationIndex )
+{
+ string conversationName = GetConversationName( conversationIndex )
+
+ PlayFactionDialogueOnLocalClientPlayer( conversationName )
+}
+
+void function ServerCallback_ForcePlayFactionDialogue( int conversationIndex )
+{
+ string conversationName = GetConversationName( conversationIndex )
+
+ ForcePlayFactionDialogueOnLocalClientPlayer( conversationName )
+}
+
+void function PlayFactionDialogueOnLocalClientPlayer( string conversationName )
+{
+ bool printDebug = GetDialogueDebugLevel() > 0
+
+ if ( printDebug )
+ printt( "PlayFactionDialogueOnLocalClientPlayer: " + conversationName )
+
+ entity listeningPlayer = GetLocalClientPlayer()
+
+ if ( !ShouldPlayFactionDialogue( conversationName, listeningPlayer ) )
+ return
+
+ string faction = GetFactionChoice( listeningPlayer )
+ if ( !ConversationEnabledForFaction( faction, conversationName ) )
+ return
+
+ //Check for priority. TODO: Cancel existing conversations if needed
+ int priority = GetConversationPriority( conversationName )
+ //Generate alias from playerEHandle and number of altAliases we have
+ string soundAlias = GenerateFactionDialogueAlias( listeningPlayer, conversationName )
+
+ bool useWaveForm = ShouldUseWaveForm( faction )
+ string waveformName = ""
+ if ( useWaveForm )
+ waveformName = GetFactionCharacterName( faction )
+ PlayAnnouncerLineThroughDeathWithPriority( conversationName, soundAlias, priority, waveformName )
+}
+
+void function ForcePlayFactionDialogueOnLocalClientPlayer( string conversationName )
+{
+ bool printDebug = GetDialogueDebugLevel() > 0
+
+ if ( printDebug )
+ printt( "ForcePlayFactionDialogueOnLocalClientPlayer: " + conversationName )
+
+ entity listeningPlayer = GetLocalClientPlayer()
+
+ string faction = GetFactionChoice( listeningPlayer )
+ if ( !ConversationEnabledForFaction( faction, conversationName ) )
+ return
+
+ //Check for priority. TODO: Cancel existing conversations if needed
+ int priority = GetConversationPriority( conversationName )
+ //Generate alias from playerEHandle and number of altAliases we have
+ string soundAlias = GenerateFactionDialogueAlias( listeningPlayer, conversationName )
+
+ bool useWaveForm = ShouldUseWaveForm( faction )
+ string waveformName = ""
+ if ( useWaveForm )
+ waveformName = GetFactionCharacterName( faction )
+ PlayAnnouncerLineThroughDeathWithPriority( conversationName, soundAlias, priority, waveformName )
+}
+
+void function ServerCallback_SpawnFactionCommanderInDropship( int shipEHandle, float dropshipSpawnTime ) //Awkward that it's here, could be in cl_classic_mp.nut but it uses data that is read in from sh_faction_dialogue.nut
+{
+ if ( IsFactionLeaderOverriden() )
+ {
+ thread file.overrideFactionLeaderData.dropshipIntroOverride( shipEHandle, dropshipSpawnTime )
+ }
+ else
+ {
+ thread ServerCallback_SpawnFactionCommanderInDropship_threaded( shipEHandle, dropshipSpawnTime )
+ }
+
+}
+
+void function ServerCallback_SpawnFactionCommanderInDropship_threaded( int shipEHandle, float dropshipSpawnTime ) //Awkward that it's here, could be in cl_classic_mp.nut but it uses data that is read in from sh_faction_dialogue.nut
+{
+ entity dropShip = GetEntityFromEncodedEHandle( shipEHandle )
+ entity localViewPlayer = GetLocalViewPlayer()
+ string faction = GetFactionChoice( localViewPlayer )
+ FactionLeaderDataStruct factionLeaderInfo = factionLeaderData[ faction ]
+
+ entity factionLeader = CreatePropDynamic( GetFactionModel( faction ) )
+ factionLeader.SetParent( dropShip, "ORIGIN" )
+ factionLeader.MarkAsNonMovingAttachment()
+ factionLeader.SetSkin( GetFactionModelSkin( faction ) )
+
+ entity prop = null
+ if ( factionLeaderInfo.propModelName != $"" )
+ {
+ prop = CreatePropDynamic( factionLeaderInfo.propModelName )
+ prop.MarkAsNonMovingAttachment()
+ prop.SetParent( factionLeader, factionLeaderInfo.propAttachment )
+ }
+
+ array dropshipAnimList = factionLeaderInfo.dropshipAnimList
+ bool useEasterEgg = RandomFloat( 100 ) <= 1.0
+ #if DEV
+ useEasterEgg = useEasterEgg || file.forceEasterEgg
+ #endif
+ if ( factionLeaderInfo.easterEggDropshipAnimList.len() > 0 && useEasterEgg )
+ {
+ dropshipAnimList = factionLeaderInfo.easterEggDropshipAnimList
+ }
+ /*foreach( dropshipAnim in dropshipAnimList )
+ {
+ printt( "dropshipAnim: " + dropshipAnim )
+ }
+ */
+
+ string dropshipAnim = dropshipAnimList.getrandom()
+ #if DEV
+ if ( file.factionLeaderAnimIndex > -1 )
+ {
+ int numberOfAnims = dropshipAnimList.len()
+ Assert( numberOfAnims > 0 )
+ if ( ( numberOfAnims - 1 ) >= file.factionLeaderAnimIndex )
+ {
+ dropshipAnim = dropshipAnimList[ file.factionLeaderAnimIndex ]
+ printt( "------------------" )
+ printt( "Selecting anim: " + dropshipAnim )
+ printt( "------------------" )
+ }
+ else
+ {
+ printt( "------------------" )
+ printt( "Tried to select anim number " + file.factionLeaderAnimIndex + " but only anims 0 - " + ( dropshipAnimList.len() - 1 ) + " available" )
+ printt( "------------------" )
+
+ }
+ }
+ #endif
+ //printt( "Picked anim: " + dropshipAnim )
+
+ AddAnimEvent( factionLeader, "set_skin_2", ModelSetSkin2 ) //Marvin specific
+ thread PlayAnim( factionLeader, dropshipAnim, dropShip, "ORIGIN" )
+ factionLeader.Anim_SetStartTime( dropshipSpawnTime )
+ factionLeader.LerpSkyScale( 0.9, 0.1 )
+ SetTeam( factionLeader, localViewPlayer.GetTeam() )
+
+ dropShip.EndSignal( "OnDestroy" )
+
+ OnThreadEnd(
+ function() : ( factionLeader, prop )
+ {
+ if ( IsValid( factionLeader ) ) //Need to check for this because if you disconnect it will delete that prop, then call this OnThreadEnd
+ factionLeader.Destroy()
+
+ if ( IsValid( prop ) )
+ prop.Destroy()
+ }
+ )
+
+ WaitForever()
+ //printt( "animation name: " + factionLeaderInfo.dropshipAnimName + ", initialTime: " + initialTime + ", propAttachment: " + factionLeaderInfo.propAttachment )
+}
+
+void function ModelSetSkin2( entity model ) //Marvin specific
+{
+ model.SetSkin( 2 )
+}
+
+bool function IsFactionLeaderOverriden()
+{
+ return file.factionLeaderOverriden
+}
+
+ OverrideFactionLeaderStruct function GetOverrideFactionLeader()
+ {
+ Assert ( IsFactionLeaderOverriden() )
+ return file.overrideFactionLeaderData
+ }
+
+void function SetOverrideFactionLeader( OverrideFactionLeaderStruct overrideFactionLeaderData )
+{
+ file.factionLeaderOverriden = true
+ file.overrideFactionLeaderData = overrideFactionLeaderData
+
+}
+void function ClearOverrideFactionLeader()
+{
+ file.factionLeaderOverriden = false
+ OverrideFactionLeaderStruct newBlankData
+ file.overrideFactionLeaderData = newBlankData
+
+}
+
+bool function ShouldUseWaveForm( string factionName )
+{
+ if ( !FactionUsesWaveform( factionName ) )
+ return true
+
+ if ( IsWatchingReplay() ) // no waveforms in kill replay, confusing
+ return false
+
+ if ( IsFactionLeaderOverriden() && !file.overrideFactionLeaderData.useWaveForm )
+ return true
+
+ return true
+
+}
+
+#if DEV
+void function Dev_SetFactionLeaderAnimIndex( int value )
+{
+ file.factionLeaderAnimIndex = value
+}
+
+void function Dev_ForceEasterEgg()
+{
+ file.forceEasterEgg = true
+}
+#endif