Scriptname zbfBondageShell extends Quest Conditional

zbfConfig Property Config Auto

Actor Property PlayerRef Auto
zbfPlayerControl Property PlayerControl Auto

Idle Property zbfIdleHandsBound Auto
Idle Property zbfIdleFree Auto

Keyword Property zbfEffectNoInventory Auto
keyword Property zbfEffectOpenMouth Auto
keyword Property zbfEffectWrist Auto
keyword Property zbfEffectNoSneak Auto
keyword Property zbfEffectNoMove Auto
keyword Property zbfEffectNoActivate Auto
keyword Property zbfEffectNoFighting Auto
keyword Property zbfEffectSetExpression Auto
keyword Property zbfEffectSlowMove Auto
Keyword Property zbfEffectNoMagic Auto
Keyword Property zbfEffectRefresh Auto
Keyword Property zbfEffectGagSound Auto

Keyword[] Property zbfAnimHands Auto
Keyword[] Property zbfAnimMouth Auto
Idle[] Property HandsBoundIdles Auto

Keyword Property zbfWornAnkles Auto
Keyword Property zbfWornBelt Auto
Keyword Property zbfWornBlindfold Auto
Keyword Property zbfWornCollar Auto
Keyword Property zbfWornGag Auto
Keyword Property zbfWornHood Auto
Keyword Property zbfWornPermitOral Auto
Keyword Property zbfWornWrist Auto

Sound[] Property GagSoundDefault Auto
Sound[] Property GagSoundAlt Auto
Sound[] Property GagSoundFrustrated Auto
Sound Property GagSoundCustomFemale Auto

Sound Property GagSoundCustomMale Auto
Sound[] Property GagSoundMale01 Auto

Sound Property GagSoundSilent Auto

GlobalVariable Property zbfSettingUpdateInterval Auto
GlobalVariable Property zbfSettingSpeedMult Auto
GlobalVariable Property zbfSettingDisableEffects Auto


; --------------------------------------------------------------
; --------------------------------------------------------------
; Ai reference counting
;
; Use RetainAi to increase reference count and bring the player
; under ai control.
; Use ReleaseAi to release ai control. If the ai ref ends up at
; zero, then ai control is released.
;
; This system allows multiple modules to request ai control of
; the player, without stepping on each other's toes. Make sure
; to match each call to retain to a call to release, or control
; will not be released.
;
Int iAiRef = 0

; Ai reference counter, start ai control
Function RetainAi()
	iAiRef += 1
	Game.SetPlayerAiDriven(true)
EndFunction

; Ai reference counter, release ai control
Function ReleaseAi()
	If iAiRef > 1
		iAiRef -= 1
	Else
		Game.SetPlayerAiDriven(false)
		iAiRef = 0
	EndIf
EndFunction

; Returns the ai reference counter
Int Function GetAiRef()
	Return iAiRef
EndFunction



; --------------------------------------------------------------
; --------------------------------------------------------------
; Animation selection
;
; These functions should be used to select appropriate idles to play 
; depending on what bindings are applied.
; 
; The purpose of this set of functions is to use the promoted idles to
; set up animation names and allow a mod to play animations based on
; applied bindings without having to micro manage the details.
; 
; Keywords: zbfAnimHands001-zbfAnimHands007 are supported.
; 

; Hand binding enums
; 
Int Property iBindWrists = 1 AutoReadOnly
Int Property iBindArmbinder = 5 AutoReadOnly
Int Property iBindYoke = 7 AutoReadOnly
Int Property iBindUnbound = -1 AutoReadOnly		; No bindings identified.

; Returns the substring used for the binding type.
; 
; This functions is typically not useful to call directly. Instead call GetAnimationName
; to get a fully qualified animation name.
; 
; Example:
; GetBindTypeSubString(iBindWrists) will return "Wri"
; GetBindTypeSubString(iBindUnbound) will return ""
; 
String Function GetBindTypeSubString(Int iBindType)
	If iBindType == iBindWrists
		Return "Wri"
	ElseIf iBindType == iBindArmbinder
		Return "Armb"
	ElseIf iBindType == iBindYoke
		Return "Yoke"
	EndIf
	Return "" ; Same as iBindUnbound
EndFunction

; Scans the actor for worn keywords and returns the first found type of bindings.
; 
; When zbfEffectWrist is worn, but no zbfAnimHands, then this function will return bound
; wrists. 
; 
; Example:
; If the player is wearing an item with zbfAnimHandsArmbinder attached, then 
; GetBindTypeFromWornKeywords(Game.GetPlayer()) will return iBindArmbinder
; 
Int Function GetBindTypeFromWornKeywords(Actor akActor)
	If akActor == None
		Return iBindUnbound
	EndIf

	If akActor.WornHasKeyword(zbfAnimHands[1])
		Return iBindWrists
	ElseIf akActor.WornHasKeyword(zbfAnimHands[5])
		Return iBindArmbinder
	ElseIf akActor.WornHasKeyword(zbfAnimHands[2])
		Return iBindWrists
	ElseIf akActor.WornHasKeyword(zbfAnimHands[8])
		Return iBindWrists
	ElseIf akActor.WornHasKeyword(zbfAnimHands[7])
		Return iBindYoke
	EndIf
	If akActor.WornHasKeyword(zbfEffectWrist)
		Return iBindWrists
	EndIf
	Return iBindUnbound ; Feel free to override with whatever feels right...
EndFunction

; Checks if the actor has the specified bind type equipped.
; 
; WARNING: 
; Note that for efficiency reasons, only the promoted animations will be matched.
; This means, that HasBindType(akActor, GetBindTypeFromWornKeywords(akActor)) does
; not have to return true.
;
Bool Function HasBindType(Actor akActor, Int iBindType)
	If iBindType == iBindWrists
		Return akActor.WornHasKeyword(zbfAnimHands[1])
	ElseIf iBindType == iBindArmbinder
		Return akActor.WornHasKeyword(zbfAnimHands[5])
	ElseIf iBindType == iBindYoke
		Return akActor.WornHasKeyword(zbfAnimHands[7])
	EndIf
	Return False
EndFunction


; Type of animation
; 
; Each animation has a different number of allowed indices
; 
; Poses - 15
; Horny - 3
; Struggle - 15 (1-6, 11, 12 defined, rest are currently missing)
; Offset - 1
Int Property iAnimTypePose = 0 AutoReadOnly
Int Property iAnimTypeHorny = 1 AutoReadOnly
Int Property iAnimTypeStruggle = 2 AutoReadOnly
Int Property iAnimTypeOffset = 3 AutoReadOnly

; Returns the substring used for the animation type
; 
; Animation type is the primary classification of non-SexLab animations. See GetAnimationName
; for a full description of how animation names are put together.
; 
String Function GetAnimTypeSubString(Int iAnimType)
	If iAnimType == iAnimTypePose
		Return "Pose"
	ElseIf iAnimType == iAnimTypeHorny
		Return "Horny"
	ElseIf iAnimType == iAnimTypeStruggle
		Return "Struggle"
	ElseIf iAnimType == iAnimTypeStruggle
		Return "Offset"
	EndIf
	Return ""
EndFunction

; Puts together a fully qualified animation name
; 
; iBindType - One of the hand binding enums
; iAnimType - One of the animation types
; iAnimIndex - Index of the animation to play
; 
; Animation names are put together by four parts mandatory substrings and one optional substring.
; Zap - Three letter identifier always present
; <bindings> - Type of bindings, eg. Wri, Yoke etc
; <animType> - Type of animation, general category. Eg. Struggle, Pose
; <animIndex> - There are several animations of each category. The index in this category.
; <SexLab index> - SexLab actor and stage index, eg. "_A2_S3" (actor 2, stage 3)
; 
; This function supports putting together the first four categories. A few examples will hopefully
; explain what an animation name looks like.
; 
; Example:
; GetAnimationName(iBindWrists, iAnimTypeStruggle, 2) will return "ZapWriStruggle02"
; GetAnimationName(iBindArmbinder, iAnimTypeHorny, 1) will return "ZapArmbHorny01"
; GetAnimationName(iBindArmbinder, iAnimTypeOffset, 1) will return "ZapArmbOffset01"
; 
String Function GetAnimationName(Int iBindType, Int iAnimType, Int iAnimIndex)
	Return "Zap" + GetBindTypeSubString(iBindType) + GetAnimTypeSubString(iAnimType) + GetAnimationIndex(iAnimIndex)
EndFunction

; Returns the keyword specified idle on the actor
; 
; If the actor has zbfAnimHandsWrists, the function will return ZaZAPOA001 (same as ZapWriOffset01) etc.
; 
Idle Function GetAnimationIdle(Actor akActor)
	Idle kIdleToPlay = zbfIdleFree

	; Retrieve an idle to play
	Int i = zbfAnimHands.Length
	While i > 1 ; Don't check 0th index
		i -= 1
		If akActor.WornHasKeyword(zbfAnimHands[i])
			Return HandsBoundIdles[i]
		EndIf
	EndWhile
	Return kIdleToPlay
EndFunction

; Returns the SexLab animation tag connected to the bind type
; 
String Function GetSexLabBoundTag(Int iBindType)
	String tags = ""
	If iBindType == iBindWrists
		tags = "Wrists"
	ElseIf iBindType == iBindArmbinder
		tags = "Armbinder"
	ElseIf iBindType == iBindYoke
		tags = "Yoke"
	EndIf
	Return tags
EndFunction

; Returns a list of required SexLab tags based on worn keywords
; 
; akActor - the actor to scan
; 
; Example:
; Wearing iBindWrists will return ["Wrists"]
; Wearing iBindArmbinder will return ["Armbinder"]
; 
String[] Function GetRequiredSexLabTags(Actor akActor)
	String[] list = New String[1]
	If akActor == None
		Return list
	EndIf

	list[0] = GetSexLabBoundTag(GetBindTypeFromWornKeywords(akActor))
	Return list
EndFunction

; Returns a list of blocked SexLab tags based on worn keywords
; 
; akActor - the actor to scan
; 
; Example:
; Wearing zbfWornGag (only) will return ["Oral"]
; Wearing zbfWornGag, zbfPermitOral will return []
; Wearing zbfWornBelt, zbfWornHood will return ["Oral", "Vaginal", "Anal"]
; 
; Corner case is zbfWornGag, zbfPermitOral, zbfWornHood which returns ["Oral"] - permit oral only affects gags
; 
String[] Function GetBlockedSexLabTags(Actor akActor)
	String[] list = New String[3]
	If akActor == None
		Return list
	EndIf

	Int j = 0
	If akActor.WornHasKeyword(zbfWornBelt)
		list[j] = "Anal"
		j += 1

		list[j] = "Vaginal"
		j += 1
	EndIf

	If akActor.WornHasKeyword(zbfWornHood) || (akActor.WornHasKeyword(zbfWornGag) && !akActor.WornHasKeyword(zbfWornPermitOral))
		list[j] = "Oral"
		j += 1
	EndIf

	Return list
EndFunction

; Modifies a list of SexLab strip slots to remove the specified slots
; 
; Can be combined with vanilla SexLab strip slots to further limit stripping in custom animations.
; 
Function ModifySexLabStripSlots(Bool[] abSlots, Bool abKeepWrists = True, Bool abKeepAnkles = True, Bool abKeepGag = True, Bool abKeepCollar = True, Bool abKeepBelt = True, Bool abKeepBlindfold = True)
	Log("ModifySexLabStripSlotsFromKeywords", "Wrists = " + abKeepWrists + ", Gag = " + abKeepGag + ", Collar = " + abKeepCollar + ", Blindfold = " + abKeepBlindfold)

	If abKeepWrists
		abSlots[59 - 30] = False
	EndIf
	If abKeepAnkles
		abSlots[53 - 30] = False
	EndIf
	If abKeepGag
		abSlots[44 - 30] = False
	EndIf
	If abKeepCollar
		abSlots[45 - 30] = False
	EndIf
	If abKeepBelt
		abSlots[49 - 30] = False
	EndIf
	If abKeepBlindfold
		abSlots[55 - 30] = False
	EndIf
EndFunction

; Automatically modifies a strip list based on worn keywords.
; 
Function ModifySexLabStripSlotsFromKeywords(Bool[] abSlots, Actor akActor)
	Log("ModifySexLabStripSlotsFromKeywords", "On actor " + akActor)

	ModifySexLabStripSlots( \
		abSlots, \
		akActor.WornHasKeyword(zbfWornWrist), \
		akActor.WornHasKeyword(zbfWornAnkles), \
		akActor.WornHasKeyword(zbfWornGag), \
		akActor.WornHasKeyword(zbfWornCollar), \
		akActor.WornHasKeyword(zbfWornBelt), \
		akActor.WornHasKeyword(zbfWornBlindfold) \
	)
EndFunction

; Automatically fetches a list of SexLab strip slots.
; 
; Does not consider SexLab configuration, and will completely strip the actor of all gear.
; 
Bool[] Function GetSexLabStripSlots(Actor akActor)
	Log("GetSexLabStripSlots", "On actor " + akActor)

	Bool[] list = New Bool[33]
	ModifySexLabStripSlotsFromKeywords(list, akActor)
	Return list
EndFunction

; Returns a SexLab animation name given a SexLab base entry and a bind type
; 
; Does not verify that the requested animation exists. This is also just a convenience
; wrapper so that a caller does not have to check if vanilla or ZAP animation names should
; be used.
; 
; This function will return weird (unusable) results if called with aiStage set, but aiActorIndex ignored.
; SexLab always needs _Ax_Sy defined at the end.
; 
; Base names used by ZAP should never include the _Ax_Sy prefix. Those parameter are included as a 
; convenience.
; 
String Function GetSexLabAnimationName(zbfSexLabBaseEntry akEntry, Int aiBindType, Int aiActorIndex = 0, Int aiStage = 0)
	String sStageSubString = ""
	String sActorSubString = ""
	String sBase = ""

	If aiActorIndex > 0
		sActorSubString = "_" + aiActorIndex As String
	EndIf

	If aiStage > 0
		sStageSubString = "_" + aiStage As String
	EndIf

	If aiBindType >= 0
		sBase = "Zap" + GetBindTypeSubString(aiBindType) + akEntry.BaseName
	Else
		sBase = akEntry.VanillaBaseName
	EndIf

	Return sBase + sActorSubString + sStageSubString
EndFunction


; --------------------------------------------------------------
; --------------------------------------------------------------
; Actor control
;
; These functions are primarily used by ZAP internally to alter
; actor behavior (play idles, change expression, etc).
; 

; Updates control settings if akActor is the player. No effect on npcs.
; 
Function ApplyPlayerControls(Actor akActor)
	If akActor != PlayerRef
		Return
	EndIf

	PlayerControl.SetDisabledControls( \
		abMovement = akActor.WornHasKeyword(zbfEffectNoMove), \
		abFighting = akActor.WornHasKeyword(zbfEffectNoFighting), \
		abSneaking = akActor.WornHasKeyword(zbfEffectNoSneak), \
		abMenu = akActor.WornHasKeyword(zbfEffectNoInventory), \
		abActivate = akActor.WornHasKeyword(zbfEffectNoActivate))
EndFunction

; Applies the correct expression on the player based on worn keywords.
; 
Function ApplyExpressionModifier(Actor akActor)
	Int expressionStrength = 0
	Int expressionType = -1

	; ExpressionType and Amount
	If akActor.WornHasKeyword(zbfEffectSetExpression)
		expressionStrength = 75
		expressionType = 9
	EndIf

	; Override with gag mouth open
	If akActor.WornHasKeyword(zbfEffectOpenMouth)
		expressionStrength = 100
		expressionType = 16
	EndIf

	; Use custom gag expression? (Again, overrides)
	If akActor.WornHasKeyword(zbfEffectOpenMouth) && akActor.WornHasKeyword(zbfAnimMouth[3])
		expressionType = 17
	EndIf

	If expressionType == 17
		akActor.ClearExpressionOverride()
		MfgConsoleFunc.SetPhonemeModifier(akActor, 0, 1, 100)
		MfgConsoleFunc.SetPhonemeModifier(akActor, 0, 11, 70)
	ElseIf expressionType > 0
		;MfgConsoleFunc.ResetPhonemeModifier(akActor)
		MfgConsoleFunc.SetPhonemeModifier(akActor, 0, 1, 0)
		MfgConsoleFunc.SetPhonemeModifier(akActor, 0, 11, 0)
		akActor.SetExpressionOverride(aiMood = expressionType, aiStrength = expressionStrength)
	Else
		;MfgConsoleFunc.ResetPhonemeModifier(akActor)
		MfgConsoleFunc.SetPhonemeModifier(akActor, 0, 1, 0)
		MfgConsoleFunc.SetPhonemeModifier(akActor, 0, 11, 0)
		akActor.ClearExpressionOverride()
	EndIf
EndFunction

; Updates properties on the actor which relates to movement
; 
Function ApplyMovementModifiers(Actor akActor)
	If akActor.WornHasKeyword(zbfEffectSlowMove)
		akActor.SetAv("SpeedMult", zbfSettingSpeedMult.GetValue())
	Else
		akActor.SetAv("SpeedMult", 100.0)
	EndIf
	akActor.ModAv("CarryWeight", -0.02)
	akActor.ModAv("CarryWeight", 0.02)
EndFunction

; Updates offset animation based on worn keywords.
; 
; Offset animations are specified with the zbfAnimHands* keywords. If multiple such keywords are worn,
; one is automatically selected.
; 
; Internally uses ::GetAnimationIdle to retrieve the idle to play.
; Will break other animations playing on the actor.
; 
Function ApplyOffsetIdle(Actor akActor)
	Idle kIdleToPlay = zbfIdleFree
	If akActor.WornHasKeyword(zbfEffectWrist)
		kIdleToPlay = GetAnimationIdle(akActor)
		If (kIdleToPlay == zbfIdleFree) || (kIdleToPlay == HandsBoundIdles[iBindWrists])
			kIdleToPlay = HandsBoundIdles[Config.iDefaultOffset]
		EndIf
	EndIf
	akActor.PlayIdle(kIdleToPlay)
EndFunction

; Prevents magic use and shouts by forcing unequip of magic and shouts.
; 
Function ApplySilenceEffect(Actor akActor)
	If akActor.WornHasKeyword(zbfEffectNoMagic)
		Spell kSpell = akActor.GetEquippedSpell(0)
		if kSpell != None
			akActor.UnequipSpell(kSpell, 0)
		EndIf
		kSpell = akActor.GetEquippedSpell(1)
		if kSpell != None
			akActor.UnequipSpell(kSpell, 1)
		EndIf

		Shout kShout = akActor.GetEquippedShout()
		If kShout != None
			akActor.UnequipShout(kShout)
		EndIf
	EndIf
EndFunction

; Returns a mask of all the currently applied effects on akActor.
; 
; 0x1		Offset idle
; 0x2		Expression
; 0x4		Player controls
; 0x8		Movement
; 0x10		Silence
; 0x20		Refresh offset idle
; 0x40		Gag sound
; 
Int Function WornEffectMask(Actor akActor)
	Bool bHasWrist
	Bool bHasExpression
	Int iMask = 0
	Int i

	bHasWrist = akActor.WornHasKeyword(zbfEffectWrist)
	bHasExpression = akActor.WornHasKeyword(zbfEffectOpenMouth) || akActor.WornHasKeyword(zbfEffectSetExpression)

	; Get the offset animation index specified by keywords
	Int iAnimHands = 0
	If bHasWrist
		i = zbfAnimHands.Length
		While (i > 2) && (iAnimHands == 0)
			i -= 1
			If akActor.WornHasKeyword(zbfAnimHands[i])
				iAnimHands = i
			EndIf
		EndWhile
		If iAnimHands == 0
			iAnimHands = Config.iDefaultOffset
		EndIf
	EndIf

	; Player controls
	Int iPC = 0
	If akActor == PlayerRef
		iPC += 0x01 * akActor.WornHasKeyword(zbfEffectNoMove) As Int
		iPC += 0x02 * akActor.WornHasKeyword(zbfEffectNoFighting) As Int
		iPC += 0x04 * akActor.WornHasKeyword(zbfEffectNoSneak) As Int
		iPC += 0x08 * akActor.WornHasKeyword(zbfEffectNoInventory) As Int
		iPC += 0x10 * akActor.WornHasKeyword(zbfEffectNoActivate) As Int
	EndIf

	; Compose the mask now, bits 1-8
	iMask += 0x1 * bHasWrist As Int
	iMask += 0x2 * bHasExpression As Int
	iMask += 0x4 * (iPC != 0) As Int
	iMask += 0x8 * akActor.WornHasKeyword(zbfEffectSlowMove) As Int
	iMask += 0x10 * akActor.WornHasKeyword(zbfEffectNoMagic) As Int
	iMask += 0x20 * (akActor.WornHasKeyword(zbfEffectRefresh) && bHasWrist) As Int
	iMask += 0x40 * (akActor.WornHasKeyword(zbfEffectGagSound) && Config.bGagSoundAuto) As Int

	; Add player controls bits, 9-16 (not all are needed, but this space is reserved)
	iMask += Math.LeftShift(iPC, 8)

	; Offset animation bits, 17-20
	iMask += Math.LeftShift(iAnimHands, 16)

	Return iMask
EndFunction

; Plays equipped effects on an actor.
; 
; akActor is the actor to run effects on
; aiMask specifies the set of effects to consider. See WornEffectMask.
; 
Function ApplyModifiersMask(Actor akActor, Int aiMask)
	If zbfSettingDisableEffects.GetValueInt() == 0
		If Math.LogicalAnd(aiMask, 0x4)
			ApplyPlayerControls(akActor)
		EndIf

		If Math.LogicalAnd(aiMask, 0x8)
			ApplyMovementModifiers(akActor)
		EndIf

		If Math.LogicalAnd(aiMask, 0x10)
			ApplySilenceEffect(akActor)
		EndIf

		If Math.LogicalAnd(aiMask, 0x2)
			ApplyExpressionModifier(akActor)
		EndIf

		If Math.LogicalAnd(aiMask, 0x1)
			ApplyOffsetIdle(akActor)
		EndIf
	EndIf
EndFunction


; --------------------------------------------------------------
; --------------------------------------------------------------
; Gag sounds
;

; Returns the next time to play a gag sound based on settings and a random factor.
; 
Float Function NextGagSound()
	If Config.bGagSoundRepeat == False
		Return Utility.GetCurrentRealTime() + 10000000.0
	EndIf

	Return Utility.GetCurrentRealTime() + Utility.RandomFloat(1.0 / Config.fGagSoundFreqVariance, 1.0 * Config.fGagSoundFreqVariance) * Config.fGagSoundFrequency
EndFunction

; Plays a random gag sound.
; 
; Uses the config menu setting to pick a sound set to play from.
; 
; This should be the go to function for playing gag moans and sounds. It honors the 
; user configuration in MCM which is what a user would expect.
; 
; akActor - Actor to play the sound from
; afVolumeMult - Extra multiplier for the volume. 1.0 is default volume, 0.5 is half volume, and so on.
; 
Function PlayGagSound(Actor akActor, Float afVolumeMult = 1.0)
	Int gender = akActor.GetActorBase().GetSex()

	If (Config.idxGagSoundFemale > 0) && (gender == 1)
		Int iSoundHandle = PickRandomGagSoundFemale(Config.idxGagSoundFemale).Play(akActor)
		Sound.SetInstanceVolume(iSoundHandle, afVolumeMult * Config.fGagSoundVolume / 100.0)
	ElseIf (Config.idxGagSoundMale > 0) && (gender == 0)
		Int iSoundHandle = PickRandomGagSoundMale(Config.idxGagSoundMale).Play(akActor)
		Sound.SetInstanceVolume(iSoundHandle, afVolumeMult * Config.fGagSoundVolume / 100.0)
	EndIf
EndFunction

; Returns number of available gag sound types
; 
Int Function GetGagSoundCount(Actor akActor)
	Int gender = akActor.GetActorBase().GetSex()
	If gender == 1
		Return 4
	EndIf
	Return 2
EndFunction

; Returns a "best effort" random sound to play provided a set input variable.
; 
; Specify 0 to play using the user configured set.
; 
; aiGender - Gender to get sound for (0 = male, otherwise female)
; aiSet - The sound set to retrieve.
; 
; Female sets
; 0 - Use the user config set
; 1 - Custom gag sounds (loose files, user modifiable)
; 2 - Alternative 1
; 3 - Alternative 2
; 4 - Frustrated/angry gag sounds
; 
; Male sets
; 0 - Use the user config set
; 1 - Custom gag sounds (loose files, user modifiable)
; 2 - Normal gag sounds
; 
; Note:
; The custom gag sounds should almost never be explicitly selected. This set is normally left empty, and the
; user of the mod may fill with his/her own content (or leave empty). If anything, the most common option
; should be to use the MCM selected value.
; 
; Example:
; Sound s = zbf.PickRandomGagSound(akActor.GetActorBase().GetSex(), 3) ; Retrieves a best effort frustrated/angry gag sound
; 
Sound Function PickRandomGagSound(Int aiGender, Int aiSet = 0)
	If aiGender == 0
		Return PickRandomGagSoundMale(aiSet)
	EndIf
	Return PickRandomGagSoundFemale(aiSet)
EndFunction

; Returns a random female gag sound to play provided a set input variable.
; 
; Specify 0 to play using the user configured set.
; 
; Sets:
; 0 - Use the MCM config set
; 1 - Custom gag sounds
; 2 - Alternative 1
; 3 - Alternative 2
; 4 - Frustrated/angry gag sounds
; 
Sound Function PickRandomGagSoundFemale(Int aiSet = 0)
	If aiSet == 0
		aiSet = Config.idxGagSoundFemale ; Ooh, very naughty. Changing input parameters...!
	EndIf

	If aiSet == 2
		Return GagSoundDefault[Utility.RandomInt(0, GagSoundDefault.Length - 1)]
	ElseIf aiSet == 3
		Return GagSoundAlt[Utility.RandomInt(0, GagSoundAlt.Length - 1)]
	ElseIf aiSet == 4
		Return GagSoundFrustrated[Utility.RandomInt(0, GagSoundFrustrated.Length - 1)]
	ElseIf aiSet == 1
		Return GagSoundCustomFemale
	EndIf
	Return GagSoundSilent ; Invalid selection
EndFunction

; Returns a random sound to play provided a set input variable.
; 
; Specify 0 to play using the user configured set.
; 
; Not that no male sounds are defined. This function is just here for symmetry with the female sounds.
; 
; Sets:
; 0 - Use the MCM config set
; 1 - Custom gag sounds
; 2 - Basic male gag sounds
; 
Sound Function PickRandomGagSoundMale(Int aiSet = 0)
	If aiSet == 0
		aiSet = Config.idxGagSoundMale ; Ooh, very naughty. Changing input parameters...!
	EndIf

	If aiSet == 1
		Return GagSoundCustomMale
	ElseIf aiSet == 2
		Return GagSoundMale01[Utility.RandomInt(0, GagSoundMale01.Length - 1)]
	EndIf
	Return GagSoundSilent
EndFunction


; --------------------------------------------------------------
; --------------------------------------------------------------
; Other misc public functions
;

; Returns the update interval for the character
; 
; In the future, the update interval can be different for player and npc characters.
; Use this function instead of the deprecated older function.
Float Function GetUpdateIntervalForActor(Actor akActor)
	If akActor == PlayerRef
		Return Config.fUpdateIntervalPlayer
	EndIf
	Return Config.fUpdateIntervalNpc
EndFunction


; --------------------------------------------------------------
; --------------------------------------------------------------
; Mod protected functions
;
; WARNING! Don't use these function unless you know what you're doing. They should typically 
; never be called outside of ZAP.
; 

; Removes all ai references and releases player from ai control
; 
; Used by MCM to restore player controls.
; 
Function ReleaseAllAiRefs()
	iAiRef = 0
	Game.SetPlayerAiDriven(false)
EndFunction

; Helper function to turn an int into a zero padded string
; 
; This function supports only numbers up to 99
; 
; Example:
; GetAnimationIndex(1) --> "01"
; GetAnimationIndex(11) --> "11"
String Function GetAnimationIndex(Int iIndex)
	If iIndex < 10
		Return "0" + (iIndex As String)
	EndIf
	Return iIndex As String
EndFunction

Int Property iDebugLevel Auto Hidden
Int iError = 0
Int iWarning = 1
Int iInfo = 2
String sFilePrefix = "zbf"
Function Log(String asMethod, String asMessage, Int aiLevel = 2, Bool abCondition = True)
	If abCondition && (aiLevel <= iDebugLevel)
		Debug.Trace(sFilePrefix + " (" + asMethod + "): " + asMessage)
	EndIf
EndFunction


; --------------------------------------------------------------
; --------------------------------------------------------------
; Player control functions
; 
; These functions are all used by zbfPlayerControl, and should not be called directly.
;

; Array containing all registered player controls
; 
zbfPlayerControl[] Property PlayerControlRegs Auto Hidden

Int iLocked
Function Lock()
	Int iSafety = 0
	While iLocked > 0 && iSafety < 50
		Utility.Wait(0.5)
		iSafety += 1
	EndWhile
	Log("Lock", "Lock function did not release right away!", iError, iSafety >= 50)
	iLocked = 1
EndFunction

Function Unlock()
	iLocked = 0
EndFunction

; Registers a player control object in the repository
; 
Function Register(zbfPlayerControl akItem)
	Lock()

	If PlayerControlRegs.Length < 10
		PlayerControlRegs = New zbfPlayerControl[10]
		PlayerControlRegs[0] = PlayerControl ; Shameless self register
	EndIf

	Int i = PlayerControlRegs.Find(akItem)
	If i < 0
		i = PlayerControlRegs.Find(None) ; First empty element
		PlayerControlRegs[i] = akItem
	EndIf

	Unlock()
	Log("Register", "Registered mod " + akItem.ModName + " in slot " + i + ".")
EndFunction

; 
; 0x1 - Movement
; 0x2 - Fighting
; 0x4 - Sneaking
; 0x8 - Menu
; 0x10 - Activation
; 
Function ReapplyPlayerControls()
	Int iControlMask = 0
	Int i = 0
	While i < PlayerControlRegs.Length
		If PlayerControlRegs[i] != None
			iControlMask = Math.LogicalOr(PlayerControlRegs[i].iControlMask, iControlMask)
		EndIf
		i += 1
	EndWhile

	Game.EnablePlayerControls( \
		abMovement = Math.LogicalAnd(iControlMask, 0x1) == 0, \
		abFighting = Math.LogicalAnd(iControlMask, 0x2) == 0, \
		abSneaking = Math.LogicalAnd(iControlMask, 0x4) == 0, \
		abMenu = Math.LogicalAnd(iControlMask, 0x8) == 0, \
		abActivate = Math.LogicalAnd(iControlMask, 0x10) == 0, \
		abCamSwitch = False, \
		abLooking = False, \
		abJournalTabs = False)

	Game.DisablePlayerControls( \
		abMovement = Math.LogicalAnd(iControlMask, 0x1) != 0, \
		abFighting = Math.LogicalAnd(iControlMask, 0x2) != 0, \
		abSneaking = Math.LogicalAnd(iControlMask, 0x4) != 0, \
		abMenu = Math.LogicalAnd(iControlMask, 0x8) != 0, \
		abActivate = Math.LogicalAnd(iControlMask, 0x10) != 0)
EndFunction


; --------------------------------------------------------------
; --------------------------------------------------------------
; Deprecated functions
;
; Old deprecated functions. Avoid using these.
; 

Bool Function HasOffsetIdle(Actor akActor)
	Return akActor.WornHasKeyword(zbfEffectWrist)
EndFunction

Bool Function HasExpressionModifier(Actor akActor)
	Return akActor.WornHasKeyword(zbfEffectOpenMouth) || akActor.WornHasKeyword(zbfEffectSetExpression)
EndFunction

Bool Function HasPlayerControls(Actor akActor)
	Bool hasPc = akActor.WornHasKeyword(zbfEffectNoInventory) || akActor.WornHasKeyword(zbfEffectNoActivate) || akActor.WornHasKeyword(zbfEffectNoFighting) || akActor.WornHasKeyword(zbfEffectNoSneak) || akActor.WornHasKeyword(zbfEffectNoMove)

	Return hasPc
EndFunction

Bool Function HasMovementModifier(Actor akActor)
	Return akActor.WornHasKeyword(zbfEffectSlowMove)
EndFunction

Bool Function HasSilenceEffect(Actor akActor)
	Return akActor.WornHasKeyword(zbfEffectNoMagic)
EndFunction

Bool Function HasRefreshModifier(Actor akActor)
	Return akActor.WornHasKeyword(zbfEffectRefresh)
EndFunction

Bool Function HasGagSound(Actor akActor)
	Return akActor.WornHasKeyword(zbfEffectGagSound)
EndFunction

Function ApplyAllModifiers(Actor akActor, Bool abApplyMouth = True, Bool abApplyMovement = True, Bool abApplySilence = True, Bool abApplyPlayerControls = True, Bool abApplyOffsetIdle = True)
	If (zbfSettingDisableEffects.GetValue() as Int) == 0
		If abApplyPlayerControls
			ApplyPlayerControls(akActor)
		EndIf

		If abApplyMovement
			ApplyMovementModifiers(akActor)
		EndIf

		If abApplySilence
			ApplySilenceEffect(akActor)
		EndIf

		If abApplyMouth
			ApplyExpressionModifier(akActor)
		EndIf

		If abApplyOffsetIdle
			ApplyOffsetIdle(akActor)
		EndIf
	EndIf
EndFunction

Float Function GetUpdateInterval()
	Return 0.5
EndFunction
