local classlib = require("core.class")
local MLT = require("design.MayaLuaTablesLibrary")
local debuglib = require("level.contextactionlibrary_debug")
local minDelay = 0.1
local InterruptTypes = {
  "Preempt",
  "Disengage",
  "BreakOut",
  "CommandShotStart",
  "CommandShotEnd",
  "LoopDisengage"
}
local BranchEvents = {
  ApproachStart = "BranchOnApproachStart",
  Enter = "BranchOnEnter",
  Loop = "BranchOnLoop",
  Exit = "BranchOnExit",
  Disengage = "BranchOnDisengage",
  Interrupt = "BranchOnInterrupt",
  CommandShot = "BranchOnCommandShot"
}
local BanterEvents = {
  ApproachStart = "BanterOnApproachStart",
  Enter = "BanterOnEnter",
  Loop = "BanterOnLoop",
  Exit = "BanterOnExit",
  Disengage = "BanterOnDisengage",
  Interrupt = "BanterOnInterrupt",
  CommandShot = "BanterOnCommandShot"
}
local BanterEvents_IsCritical = {
  ApproachStart = "BanterOnApproachStart_IsCritical",
  Enter = "BanterOnEnter_IsCritical",
  Loop = "BanterOnLoop_IsCritical",
  Exit = "BanterOnExit_IsCritical",
  Disengage = "BanterOnDisengage_IsCritical",
  Interrupt = "BanterOnInterrupt_IsCritical",
  CommandShot = "BanterOnCommandShot_IsCritical"
}
local BanterEvents_HasPlayed = {
  ApproachStart = "BanterOnApproachStart_HasPlayed",
  Enter = "BanterOnEnter_HasPlayed",
  Loop = "BanterOnLoop_HasPlayed",
  Exit = "BanterOnExit_HasPlayed",
  Disengage = "BanterOnDisengage_HasPlayed",
  Interrupt = "BanterOnInterrupt_HasPlayed",
  CommandShot = "BanterOnCommandShot_HasPlayed"
}
local CallbackEvents = {
  ApproachStart = "CallbackOnApproachStart",
  Enter = "CallbackOnEnter",
  Loop = "CallbackOnLoop",
  Exit = "CallbackOnExit",
  Disengage = "CallbackOnDisengage",
  Interrupt = "CallbackOnInterrupt",
  CommandShot = "CallbackOnCommandShot"
}
BranchEvents.Disengage = BranchEvents.Exit
CallbackEvents.LoopDisengage = CallbackEvents.Disengage
local SpeedNameToTweakName = {
  Walk = "NAV_SPEED_WALK",
  Jog = "NAV_SPEED_JOG",
  Run = "NAV_SPEED_RUN",
  Sprint = "NAV_SPEED_SPRINT"
}
local PuppeteerWrapper = classlib.Class("PuppeteerWrapper")
function PuppeteerWrapper:init(puppeteer, level, go, attributes, persistentState)
  self.puppeteer = puppeteer
  self.level = level
  self.go = go
  self.attributes = attributes
  self.persistentState = persistentState
  self.stageName = nil
  self.pendingEnvironmentEvent = nil
  self.interruptHandlers = {}
  self.setupApproach = false
  self.seenObject = false
  self.engaged = false
  self.actionChance = 0
  self.queuedEnd = nil
  self.thisLookAtEntry = nil
  self.enabled = nil
  self.syncStarted = false
  self.commandShotEnding = false
  self.framesStuckInCommandShotState = 0
  debuglib.RegisterContextAction(self)
end
function PuppeteerWrapper:Valid()
  return self.puppeteer ~= nil
end
function PuppeteerWrapper:Clear()
  self.cooldownTimeRemaining = nil
  self.loopTimeRemaining = nil
  self.loopActionFreqTimeRemaining = nil
  self.interruptHandlers = {}
  if self.puppeteer ~= nil then
    self.puppeteer:Clear()
  end
  game.SubObject.Sleep(self.go)
end
local PuppeteerDisengageOrExit, OnPuppeteerEngaged, OnPuppeteerArrival, OnPuppeteerEnterPreempted, OnPuppeteerStartLoop, OnPuppeteerExit, OnPuppeteerDisengage, OnPuppeteerInterrupt, RestartPuppeteer, OnPuppeteerCommandShotStart, OnPuppeteerCommandShotEnd, SetLoopActionFreq, TriggerLoopAction, TriggerPlayBanter, AddPropsForSync, HideProps, ShowProps, PlayBanterForEvent, ResyncToLoop, LoopDone, SetupAnimDrivers, DetermineDirection, DetermineDistance, EnterBranchDone, ExitBranchDone, EarlyExit, InterruptPuppeteer
local HandleInterrupt = function(wrapper, event)
  local fn = wrapper.interruptHandlers[event] or wrapper.defaultInterruptHandler
  if fn and wrapper.puppeteer then
    if wrapper.puppeteer ~= nil then
      wrapper.puppeteer:Clear()
    end
    fn(wrapper)
  end
end
local SetupEvent_BranchDone = function(wrapper)
  local stage = wrapper.stageName
  local func
  if stage == "Enter" then
    function func()
      EnterBranchDone(wrapper)
    end
  elseif stage == "EnterPreempt" then
    function func()
      OnPuppeteerExit(wrapper)
    end
  elseif stage == "LoopAction" then
    function func()
      ResyncToLoop(wrapper)
    end
  elseif stage == "QueuedEnd" then
    function func()
      ResyncToLoop(wrapper)
    end
  elseif stage == "Exit" then
    function func()
      ExitBranchDone(wrapper)
    end
  elseif stage == "LoopDisengage" then
    function func()
      ResyncToLoop(wrapper)
    end
  elseif stage == "Disengage" then
    function func()
      ExitBranchDone(wrapper)
    end
  end
  if func ~= nil and wrapper.puppeteer ~= nil then
    wrapper.puppeteer:OnEvent(func, "CA_BranchDone")
  end
end
local SetInterruptHandlers = function(wrapper)
  local puppeteer = wrapper.puppeteer
  if puppeteer ~= nil then
    for _, event in ipairs(InterruptTypes) do
      if wrapper.interruptHandlers[event] then
        puppeteer:OnEvent(function()
          HandleInterrupt(wrapper, event)
        end, event)
      end
    end
  end
end
local SetupAllEvents = function(wrapper, skipBranchDone)
  local puppeteer = wrapper.puppeteer
  SetInterruptHandlers(wrapper)
  if puppeteer ~= nil then
    puppeteer:OnEvent(function()
      ShowProps(wrapper)
    end, "CA_ShowProps")
    puppeteer:OnEvent(function()
      HideProps(wrapper)
    end, "CA_HideProps")
    puppeteer:OnEvent(function()
      TriggerPlayBanter(wrapper)
    end, "CA_PlayBanter")
    puppeteer:OnEvent(function()
      LoopDone(wrapper)
    end, "CA_LoopDone")
    puppeteer:OnEvent(function()
      EarlyExit(wrapper)
    end, "CA_EarlyExit")
    puppeteer:OnEvent(function()
      HandleInterrupt(wrapper, "Disengage")
    end, "CA_Disengage")
    puppeteer:OnEvent(function()
      InterruptPuppeteer(wrapper)
    end, "CA_Interrupt")
  end
  if not skipBranchDone then
    SetupEvent_BranchDone(wrapper)
  end
end
function AddPropsForSync(wrapper)
  local props = wrapper.attributes.Props
  local slaveTable = {}
  if props then
    for _, v in ipairs(props) do
      if v.Parent then
        v:Unparent()
      end
      table.insert(slaveTable, {Slave = v})
    end
  end
  return slaveTable
end
function HideProps(wrapper)
  local props = wrapper.attributes.Props
  if props then
    for _, v in ipairs(props) do
      if v.Parent ~= wrapper.go then
        wrapper.go:AddChild(v)
      end
      v:Hide()
    end
  end
  SetupAllEvents(wrapper)
end
function ShowProps(wrapper)
  local props = wrapper.attributes.Props
  if props then
    for _, v in ipairs(props) do
      v:Show()
    end
  end
  SetupAllEvents(wrapper)
end
local IsValidString = function(testString)
  return testString ~= nil and testString ~= ""
end
local CalculateApproachSpeed = function(puppet, attributes)
  local speedName = attributes.ApproachSpeed or "MaxSpeed"
  local tweakName = SpeedNameToTweakName[speedName]
  local speed
  if tweakName then
    speed = puppet:LookupFloatConstant(tweakName)
  end
  speed = speed or puppet:GetMaxSpeed()
  return speed
end
local BranchNameForEvent = function(event, attributes)
  local branchEvent = BranchEvents[event]
  local branchName = branchEvent and attributes[branchEvent]
  if branchName then
    return branchName
  end
end
local PlayBranch = function(wrapper, newBranch)
  if newBranch then
    if wrapper.syncStarted == false then
      local slaveTable = AddPropsForSync(wrapper)
      local slaveNum = #slaveTable
      if 0 < slaveNum then
        wrapper.puppeteer:Sync(newBranch, false, slaveTable, "approachJoint")
      else
        wrapper.puppeteer:Sync(newBranch, true, {}, "approachJoint")
      end
      wrapper.syncStarted = true
    else
      wrapper.puppeteer:StartBranch(newBranch)
    end
  end
end
local EnterStage = function(event, wrapper)
  local puppeteer = wrapper.puppeteer
  local go = wrapper.go
  local level = wrapper.level
  local attributes = wrapper.attributes
  if event ~= "Disengage" and not wrapper.IsEarlyExit then
    PlayBranch(wrapper, BranchNameForEvent(event, attributes))
  else
    PlayBanterForEvent(event, wrapper)
  end
  if (event == "Exit" or event == "Enter") and not BranchNameForEvent(event, attributes) then
    PlayBanterForEvent(event, wrapper)
  end
  wrapper.stageName = event
  if puppeteer ~= nil then
    puppeteer:SetStageName(event)
  end
  if event == "Exit" and not BranchNameForEvent("Enter", attributes) and not BranchNameForEvent("Loop", attributes) then
    return wrapper.puppeteer ~= nil
  end
  local callbackEvent = CallbackEvents[event]
  local callback = callbackEvent and attributes[callbackEvent]
  if callback then
    MLT.ExecuteCallbacksForEvent(level, go, callback, callbackEvent)
  end
  return wrapper.puppeteer ~= nil
end
local DestroyPuppeteer = function(wrapper)
  local attributes = wrapper.attributes
  local go = wrapper.go
  wrapper:Clear()
  wrapper.engaged = false
  local props = wrapper.attributes.Props
  if props then
    for _, v in ipairs(props) do
      if v.Parent ~= wrapper.go then
        wrapper.go:AddChild(v)
      end
      v:Hide()
    end
  end
  local puppeteer = wrapper.puppeteer
  if puppeteer ~= nil then
    local puppet = puppeteer.Puppet
    if puppet ~= nil then
      puppet:ClearLocomotionSlowDownBehavior()
      puppet:ClearDecelerationOverride()
      puppet:ClearFocus()
      if wrapper.stageName == "ApproachStart" or attributes.UseSteadyOnExit == false then
        puppeteer:SetStageName("ApproachOrMovingBreakOut")
      end
      if (attributes.SetCreatureAsUnavailable == true or attributes.SetCreatureAsOccupied == true) and puppet.RemoveAvailabilityRequest then
        puppet:RemoveAvailabilityRequest("ContextAction")
      end
    end
  end
  if wrapper.attributes.DisableContextBehaviorBanter and wrapper.persistentState.EnableContextBehaviorBanter then
    wrapper.persistentState.EnableContextBehaviorBanter = false
    game.Audio.SetBanterFact("ContextBehaviorBanterEnabled", true)
  end
  wrapper.approachBanterPlayed = false
  if attributes.BehaviorBanterLoop and attributes.BehaviorBanterLoop ~= "" and wrapper.persistentState.BehaviorBanterLoop then
    wrapper.persistentState.BehaviorBanterLoop = false
    game.Audio.SetBanterFact(attributes.BehaviorBanterLoop, false)
  end
  wrapper.queuedEnd = nil
  wrapper.puppeteer = nil
  wrapper.pendingEnvironmentEvent = nil
  if wrapper.stageName == "ApproachStart" then
    if wrapper.thisLookAtEntry then
      wrapper.thisLookAtEntry:Remove()
      wrapper.thisLookAtEntry = nil
    end
    if puppeteer ~= nil and puppeteer.Puppet ~= nil and puppeteer.Puppet:IsContextBehaviorNameEqualTo("POST_UP_BEHAVIOR_CONTEXT_CONFIG") then
      puppeteer.Puppet:ForceMove("BRA_PostUpDisabledWhileApproaching")
    end
  end
  if puppeteer ~= nil then
    puppeteer:DetachPuppet()
  end
  if wrapper.enabled then
    if wrapper.persistentState.enabled and wrapper.stageName == "ApproachStart" and attributes.ResetWhenDisengageInApproach and not game.Player.FindPlayer():GetTraversePath() then
      RestartPuppeteer(wrapper)
    else
      local cooldownTimeRemaining = attributes.CooldownAfterUse or 0
      if 0 < cooldownTimeRemaining then
        wrapper.cooldownTimeRemaining = cooldownTimeRemaining
        game.SubObject.Wake(go)
      else
        game.SubObject.Sleep(go)
      end
    end
  end
  if wrapper.IsEarlyExit then
    wrapper.IsEarlyExit = false
  end
  wrapper.stageName = nil
end
local CreateOrRestartPuppeteer = function(level, go, attributes, persistentState, existing_wrapper)
  local characterType = attributes.AvailableToCharacterType or "ANY"
  local advertisement = string.format("GENERIC_%s", characterType)
  local debugName
  if attributes.DebugName ~= nil and attributes.DebugName ~= "" then
    debugName = string.format("CA: %s - %s", characterType, attributes.DebugName)
  else
    debugName = string.format("%s: %s", characterType, go.Parent.Parent:GetName())
  end
  for _, name in pairs(BanterEvents_HasPlayed) do
    attributes[name] = false
  end
  BanterEvents_HasPlayed.LoopAction = "BanterOnLoopAction_HasPlayed"
  attributes[BanterEvents_HasPlayed.LoopAction] = false
  local puppeteer = game.Puppeteer.NewEngagement(go, debugName)
  local wrapper = existing_wrapper
  if wrapper then
    wrapper:Clear()
    wrapper:init(puppeteer, level, go, attributes, persistentState)
  else
    wrapper = PuppeteerWrapper.New(puppeteer, level, go, attributes, persistentState)
  end
  wrapper.defaultInterruptHandler = DestroyPuppeteer
  puppeteer:SetType("ContextAction")
  puppeteer:Advertise(advertisement)
  puppeteer:OnEngaged(function()
    OnPuppeteerEngaged(wrapper)
  end)
  wrapper.enabled = true
  return wrapper
end
local function SetupApproach(wrapper)
  local puppeteer = wrapper.puppeteer
  local puppet = puppeteer.Puppet
  local attributes = wrapper.attributes
  if wrapper.IsEarlyExit then
    wrapper.IsEarlyExit = false
  end
  local approachBranch
  if IsValidString(attributes.BranchOnEnter) then
    approachBranch = attributes.BranchOnEnter
  elseif IsValidString(attributes.BranchOnLoop) then
    approachBranch = attributes.BranchOnLoop
  else
    approachBranch = attributes.BranchOnExit
  end
  local approachSpeed = CalculateApproachSpeed(puppet, attributes)
  local arrivalRadius = attributes.ApproachArrivalRadius
  local approachIgnoreNavmesh = attributes.ApproachIgnoreNavmesh
  local approachParams = {
    speed = approachSpeed,
    branch_name = approachBranch,
    joint_name = "approachJoint",
    stop = false,
    ignore_navmesh = approachIgnoreNavmesh
  }
  if (attributes.BranchOnEnter and attributes.BranchOnEnter ~= "" or attributes.BranchOnExit and attributes.BranchOnExit ~= "" or attributes.BranchOnLoop and attributes.BranchOnExit ~= "" and wrapper.loopTimeRemaining ~= 0) and attributes.SettleOnArrival then
    approachParams.stop = true
    approachParams.stop_distance = arrivalRadius
    approachParams.close_range_distance = arrivalRadius
    approachParams.focus_end_direction = true
    approachParams.complete_radius = arrivalRadius
  end
  if attributes.CausalAction then
    puppet:SetDecelerationOverride(1.25)
    puppet:SetLocomotionSlowDownBehavior(1.3, 2.2, 4, 1.9, 3)
  end
  puppeteer:Approach(approachParams)
  if approachParams.stop == true then
    puppeteer:OnEvent(function()
      if puppet and game.AIUtil.Distance(puppet, wrapper.go) < attributes.ApproachArrivalRadius then
        OnPuppeteerArrival(wrapper)
      else
        SetupApproach(wrapper)
      end
    end, "ContextActionEarlyStopExit")
    puppeteer:OnComplete(function()
      OnPuppeteerArrival(wrapper)
    end)
  else
    puppeteer:OnArrival(function()
      OnPuppeteerArrival(wrapper)
    end, {
      branch_name = approachBranch,
      joint_name = "approachJoint",
      radius = arrivalRadius
    })
  end
  wrapper.syncStarted = false
  wrapper.setupApproach = true
  puppet:SetActuator({StartDistance = 0.1})
  wrapper.interruptHandlers = {
    Preempt = DestroyPuppeteer,
    Disengage = DestroyPuppeteer,
    BreakOut = OnPuppeteerInterrupt,
    CommandShotStart = OnPuppeteerCommandShotStart,
    CommandShotEnd = OnPuppeteerCommandShotEnd
  }
  SetInterruptHandlers(wrapper)
end
function OnPuppeteerEngaged(wrapper)
  if not EnterStage("ApproachStart", wrapper) then
    return
  end
  local puppeteer = wrapper.puppeteer
  local puppet = puppeteer.Puppet
  local go = wrapper.go
  local attributes = wrapper.attributes
  local availabilityState
  if attributes.SetCreatureAsUnavailable == true then
    availabilityState = availabilityState or {}
    availabilityState.AvailableForSync = false
  end
  if attributes.SetCreatureAsOccupied == true then
    availabilityState = availabilityState or {}
    availabilityState.Unoccupied = false
  end
  if availabilityState and puppet.SetNewAvailabilityRequest then
    if puppet.IsAvailableInLevel then
      puppet:SetNewAvailabilityRequest("ContextAction", availabilityState)
    else
      puppet:SetNewAvailabilityRequest("ContextAction", false)
    end
  end
  if attributes.DisableContextBehaviorBanter then
    wrapper.persistentState.EnableContextBehaviorBanter = game.Audio.CanBanterConversationPlay("Get_IsContextBehaviorBanterEnabled")
    game.Audio.SetBanterFact("ContextBehaviorBanterEnabled", false)
  end
  wrapper.approachBanterPlayed = false
  game.SubObject.Wake(go)
  wrapper.engaged = true
  if attributes.ApproachMovementPriority == "ForcePushAway" then
    puppeteer:SetUserFlag("ForcePushAway")
  elseif attributes.ApproachMovementPriority == "ForcePushAwayCloseRange" then
    puppeteer:SetUserFlag("ForcePushAwayCloseRange")
  end
  SetupApproach(wrapper)
end
function OnPuppeteerArrival(wrapper)
  if not EnterStage("Enter", wrapper) then
    return
  end
  if wrapper.puppeteer and wrapper.puppeteer.Puppet then
    wrapper.puppeteer.Puppet:ClearFocus()
  end
  if BranchNameForEvent("Enter", wrapper.attributes) then
    wrapper.interruptHandlers = {
      Preempt = OnPuppeteerEnterPreempted,
      Disengage = OnPuppeteerEnterPreempted,
      BreakOut = OnPuppeteerInterrupt,
      CommandShotStart = OnPuppeteerCommandShotStart,
      CommandShotEnd = OnPuppeteerCommandShotEnd
    }
    SetupAllEvents(wrapper)
  else
    EnterBranchDone(wrapper)
  end
end
function EnterBranchDone(wrapper)
  local loopDuration = wrapper.attributes.LoopDuration or 0
  if loopDuration == 0 then
    OnPuppeteerExit(wrapper)
  else
    OnPuppeteerStartLoop(wrapper)
  end
end
function OnPuppeteerEnterPreempted(wrapper)
  OnPuppeteerDisengage(wrapper)
end
function OnPuppeteerStartLoop(wrapper)
  if not EnterStage("Loop", wrapper) then
    return
  end
  if wrapper.attributes.BehaviorBanterLoop and wrapper.attributes.BehaviorBanterLoop ~= "" then
    wrapper.persistentState.BehaviorBanterLoop = true
    game.Audio.SetBanterFact(wrapper.attributes.BehaviorBanterLoop, true)
  end
  local go = wrapper.go
  local attributes = wrapper.attributes
  local loopDuration = attributes.LoopDuration
  if loopDuration ~= 0 then
    if 0 < loopDuration then
      wrapper.loopTimeRemaining = loopDuration
    end
    local delayTime = wrapper.attributes.LoopActionInitialDelay or 0
    if delayTime < 0 then
      SetLoopActionFreq(wrapper)
    else
      if delayTime <= minDelay then
        delayTime = minDelay
      end
      wrapper.loopActionFreqTimeRemaining = delayTime
      wrapper.actionChance = 1
    end
  else
    wrapper.loopTimeRemaining = nil
  end
  game.SubObject.Wake(go)
  wrapper.interruptHandlers = {
    Preempt = OnPuppeteerDisengage,
    Disengage = OnPuppeteerDisengage,
    BreakOut = OnPuppeteerInterrupt,
    CommandShotStart = OnPuppeteerCommandShotStart,
    CommandShotEnd = OnPuppeteerCommandShotEnd
  }
  SetupAllEvents(wrapper, true)
end
function SetLoopActionFreq(wrapper)
  local freqMin = wrapper.attributes.LoopActionFreqMin
  local freqMax = wrapper.attributes.LoopActionFreqMax
  if 0 <= freqMin and 0 < freqMax then
    wrapper.loopActionFreqTimeRemaining = math.random() * (freqMax - freqMin) + freqMin
  else
    wrapper.loopActionFreqTimeRemaining = nil
  end
end
function TriggerLoopAction(wrapper)
  if wrapper.stageName == "LoopAction" then
    return
  end
  local attributes = wrapper.attributes
  local puppeteer = wrapper.puppeteer
  local selectedBranch = attributes.LoopFidgetBranch
  local actionBranch = attributes.LoopActionBranch
  if selectedBranch and 0 < #selectedBranch then
    selectedBranch = selectedBranch[math.random(1, #selectedBranch)]
  end
  if actionBranch and 0 < #actionBranch then
    actionBranch = actionBranch[math.random(1, #actionBranch)]
  end
  if math.random() < wrapper.actionChance then
    if IsValidString(actionBranch) then
      selectedBranch = actionBranch
    end
    wrapper.actionChance = attributes.LoopActionToFidgetRatio
  end
  if IsValidString(selectedBranch) then
    wrapper.stageName = "LoopAction"
    puppeteer:SetStageName("LoopAction")
    SetupAnimDrivers(wrapper)
    PlayBranch(wrapper, selectedBranch)
    wrapper.interruptHandlers = {
      Preempt = OnPuppeteerDisengage,
      Disengage = OnPuppeteerDisengage,
      BreakOut = OnPuppeteerInterrupt,
      CommandShotStart = OnPuppeteerCommandShotStart,
      CommandShotEnd = OnPuppeteerCommandShotEnd
    }
    SetupAllEvents(wrapper)
  end
  wrapper.loopActionFreqTimeRemaining = nil
end
local OnBanterPlayed = function(wrapper)
  local event = wrapper.stageName
  if wrapper.attributes.OnlyPlayBanterOnce and event ~= nil then
    local attibuteIdx = BanterEvents_HasPlayed[event]
    if attibuteIdx ~= nil then
      local hasPlayed = wrapper.attributes[attibuteIdx]
      if not hasPlayed then
        wrapper.attributes[attibuteIdx] = true
      end
    end
  end
end
function TriggerPlayBanter(wrapper)
  if wrapper.stageName == "LoopAction" then
    if not wrapper.attributes.OnlyPlayBanterOnce or wrapper.attributes.OnlyPlayBanterOnce and not wrapper.attributes[BanterEvents_HasPlayed.LoopAction] then
      local attributes = wrapper.attributes
      local actionBanterIsCritical = attributes.LoopActionBanter_IsCritical
      local actionBanter = attributes.LoopActionBanter
      if IsValidString(actionBanter) then
        if actionBanterIsCritical then
          game.Audio.PlayBanter(actionBanter, nil, nil, false)
          OnBanterPlayed(wrapper)
        else
          game.Audio.PlayBanterNonCritical(actionBanter, nil, nil, false, function()
            OnBanterPlayed(wrapper)
          end)
        end
      end
    end
  else
    PlayBanterForEvent(wrapper.stageName, wrapper)
  end
  SetupAllEvents(wrapper)
end
function EarlyExit(wrapper)
  if wrapper.puppeteer and wrapper.puppeteer.Puppet then
    if not wrapper.IsEarlyExit then
      wrapper.IsEarlyExit = true
      wrapper.puppeteer.Puppet:TriggerMoveEvent("kLE_EarlyExit")
      OnPuppeteerExit(wrapper)
    end
  else
    OnPuppeteerDisengage(wrapper)
  end
end
function LoopDone(wrapper)
  local puppeteer = wrapper.puppeteer
  if puppeteer ~= nil and puppeteer.Puppet and wrapper.stageName == "LoopDisengage" then
    if not EnterStage("Exit", wrapper) then
      puppeteer.Puppet:TriggerMoveEvent("kLE_Disengage")
      PlayBanterForEvent("Disengage", wrapper)
    end
    wrapper.interruptHandlers = {BreakOut = OnPuppeteerInterrupt}
  else
    wrapper.interruptHandlers = {
      Preempt = OnPuppeteerDisengage,
      Disengage = OnPuppeteerDisengage,
      BreakOut = OnPuppeteerInterrupt,
      CommandShotStart = OnPuppeteerCommandShotStart,
      CommandShotEnd = OnPuppeteerCommandShotEnd
    }
  end
  SetupAllEvents(wrapper, true)
end
function ResyncToLoop(wrapper)
  local puppeteer = wrapper.puppeteer
  if wrapper.queuedEnd ~= nil then
    puppeteer:SetStageName("ExitReady")
    wrapper.stageName = "ExitReady"
    PuppeteerDisengageOrExit(wrapper, wrapper.queuedEnd)
    wrapper.queuedEnd = nil
  else
    local loopBranch = BranchNameForEvent("Loop", wrapper.attributes)
    PlayBranch(wrapper, loopBranch)
    SetLoopActionFreq(wrapper)
    puppeteer:SetStageName("Loop")
    wrapper.stageName = "Loop"
    wrapper.interruptHandlers = {
      Preempt = OnPuppeteerDisengage,
      Disengage = OnPuppeteerDisengage,
      BreakOut = OnPuppeteerInterrupt,
      CommandShotStart = OnPuppeteerCommandShotStart,
      CommandShotEnd = OnPuppeteerCommandShotEnd
    }
    SetInterruptHandlers(wrapper)
  end
end
function SetupAnimDrivers(wrapper)
  local FindFreya = function()
    local objArray = game.World.FindGameObjectsByMarker("freya00")
    for _, obj in ipairs(objArray) do
      if obj:GetCreature() ~= nil and obj:GetCreature():GetAI() ~= nil then
        return obj:GetCreature():GetAI()
      end
    end
    return nil
  end
  local FindBaldur = function()
    local objArray = game.World.FindGameObjectsByMarker("baldur00")
    for _, obj in ipairs(objArray) do
      if obj:GetCreature() ~= nil and obj:GetCreature():GetAI() ~= nil then
        return obj:GetCreature():GetAI()
      end
    end
    return nil
  end
  local FindSindri = function()
    local objArray = game.World.FindGameObjectsByMarker("sindri00")
    for _, obj in ipairs(objArray) do
      if obj:GetCreature() ~= nil and obj:GetCreature():GetAI() ~= nil then
        return obj:GetCreature():GetAI()
      end
    end
    return nil
  end
  local FindBrok = function()
    local objArray = game.World.FindGameObjectsByMarker("brok00")
    for _, obj in ipairs(objArray) do
      if obj:GetCreature() ~= nil and obj:GetCreature():GetAI() ~= nil then
        return obj:GetCreature():GetAI()
      end
    end
    return nil
  end
  local referenceTarget
  local referenceObjectName = wrapper.attributes.LoopActionReferenceObject
  if referenceObjectName == "Kratos" then
    referenceTarget = game.Player.FindPlayer()
  elseif referenceObjectName == "Son" then
    referenceTarget = game.AI.FindSon()
  elseif referenceObjectName == "Freya" then
    referenceTarget = FindFreya()
  elseif referenceObjectName == "Brok" then
    referenceTarget = FindBrok()
  elseif referenceObjectName == "Sindri" then
    referenceTarget = FindSindri()
  elseif referenceObjectName == "Baldur" then
    referenceTarget = FindBaldur()
  elseif referenceObjectName ~= nil and referenceObjectName ~= "" then
    referenceTarget = wrapper.go.Level:FindGameObject(referenceObjectName)
  else
    return
  end
  DetermineDirection(wrapper, referenceTarget)
  DetermineDistance(wrapper, referenceTarget)
end
function DetermineDirection(wrapper, referenceTarget)
  local puppet = wrapper.puppeteer.Puppet
  local animDriver = puppet:GetAnimDriver("ContextAction_Direction")
  local animDriver6way = puppet:GetAnimDriver("ContextAction_SixDirection")
  if referenceTarget ~= nil then
    local forward = puppet:GetWorldForward()
    local to = referenceTarget:GetWorldPosition() - puppet:GetWorldPosition()
    local angle = game.AIUtil.AngleBetweenXZ(forward, to)
    if animDriver then
      animDriver.Value = 1
      if -60 <= angle and angle <= 60 then
        animDriver.Value = 1
      elseif angle <= -60 and -120 <= angle then
        animDriver.Value = 4
      elseif 60 <= angle and angle <= 120 then
        animDriver.Value = 2
      else
        animDriver.Value = 3
      end
    end
    if animDriver6way then
      animDriver6way.Value = 1
      if 0 <= angle and angle < 60 then
        animDriver6way.Value = 1
      elseif 60 <= angle and angle < 100 then
        animDriver6way.Value = 2
      elseif 100 <= angle and angle <= 180 then
        animDriver6way.Value = 3
      elseif -180 <= angle and angle < -100 then
        animDriver6way.Value = 4
      elseif -100 <= angle and angle < -60 then
        animDriver6way.Value = 5
      else
        animDriver6way.Value = 6
      end
    end
  end
end
function DetermineDistance(wrapper, referenceTarget)
  local puppet = wrapper.puppeteer.Puppet
  local animDriver = puppet:GetAnimDriver("ContextAction_Distance")
  local distance = (referenceTarget:GetWorldPosition() - puppet:GetWorldPosition()):Length()
  if animDriver then
    animDriver.Value = 1
    if 0 <= distance and distance < 5 then
      animDriver.Value = 1
    elseif 10 <= distance and distance < 25 then
      animDriver.Value = 2
    else
      animDriver.Value = 3
    end
  end
end
function PuppeteerDisengageOrExit(wrapper, stageName)
  local puppeteer = wrapper.puppeteer
  if puppeteer == nil then
    DestroyPuppeteer(wrapper)
    return
  end
  local puppet = puppeteer.Puppet
  if puppet == nil then
    DestroyPuppeteer(wrapper)
    return
  end
  if stageName ~= "BreakOut" then
    if wrapper.stageName == "QueuedEnd" then
      return
    elseif wrapper.stageName == "LoopAction" then
      wrapper:Clear()
      wrapper.queuedEnd = stageName
      wrapper.stageName = "QueuedEnd"
      wrapper.puppeteer:SetStageName("QueuedEnd")
      if wrapper.queuedEnd ~= "Exit" then
        puppet:TriggerMoveEvent("kLE_QueuedEnd")
      end
      wrapper.interruptHandlers = {BreakOut = OnPuppeteerInterrupt}
      SetupAllEvents(wrapper)
      return
    end
  end
  if not EnterStage(stageName, wrapper) then
    wrapper.interruptHandlers = {BreakOut = OnPuppeteerInterrupt}
    SetupAllEvents(wrapper)
    return
  end
  if wrapper.attributes.DisableContextBehaviorBanter and wrapper.persistentState.EnableContextBehaviorBanter then
    wrapper.persistentState.EnableContextBehaviorBanter = false
    game.Audio.SetBanterFact("ContextBehaviorBanterEnabled", true)
  end
  if wrapper.attributes.BehaviorBanterLoop and wrapper.attributes.BehaviorBanterLoop ~= "" and wrapper.persistentState.BehaviorBanterLoop then
    wrapper.persistentState.BehaviorBanterLoop = false
    game.Audio.SetBanterFact(wrapper.attributes.BehaviorBanterLoop, false)
  end
  if BranchNameForEvent("Exit", wrapper.attributes) and puppeteer then
    wrapper.interruptHandlers = {
      BreakOut = OnPuppeteerInterrupt,
      Preempt = OnPuppeteerDisengage,
      CommandShotStart = OnPuppeteerCommandShotStart,
      CommandShotEnd = OnPuppeteerCommandShotEnd
    }
    SetupAllEvents(wrapper)
  else
    ExitBranchDone(wrapper)
  end
end
function ExitBranchDone(wrapper)
  local puppet = wrapper.puppeteer.Puppet
  local stageName = wrapper.stageName
  if puppet:IsContextBehaviorNameEqualTo("WAIT_AND_EXPLORE_BEHAVIOR_CONTEXT_CONFIG") and (stageName == "Exit" or stageName == "Disengage") and wrapper.attributes.BranchOnLoop ~= "BRA_CA_Idle" and not game.Combat.GetCombatStatus() then
    puppet:ForceMove("BRA_POI_Wait_And_Explore_Disengage")
  elseif not wrapper.IsEarlyExit then
    puppet:TriggerMoveEvent("kLE_Disengage")
  end
  if not BranchNameForEvent("Enter", wrapper.attributes) and not BranchNameForEvent("Loop", wrapper.attributes) then
    local callbackEvent = CallbackEvents.Exit
    local callback = callbackEvent and wrapper.attributes[callbackEvent]
    if callback then
      MLT.ExecuteCallbacksForEvent(wrapper.level, wrapper.go, callback, callbackEvent)
    end
  end
  DestroyPuppeteer(wrapper)
end
function OnPuppeteerExit(wrapper)
  PuppeteerDisengageOrExit(wrapper, "Exit")
end
function OnPuppeteerDisengage(wrapper)
  if wrapper.stageName == "Disengage" then
    wrapper.interruptHandlers = {BreakOut = OnPuppeteerInterrupt}
    SetupAllEvents(wrapper)
    return
  end
  if wrapper.stageName == "Enter" then
    PuppeteerDisengageOrExit(wrapper, "EnterPreempt")
  elseif wrapper.stageName == "Loop" then
    PuppeteerDisengageOrExit(wrapper, "LoopDisengage")
  else
    PuppeteerDisengageOrExit(wrapper, "Disengage")
  end
end
function OnPuppeteerInterrupt(wrapper)
  if wrapper.stageName == "Interrupt" then
    return
  end
  PlayBanterForEvent("Interrupt", wrapper)
  if not EnterStage("Interrupt", wrapper) then
    return
  end
  local puppeteer = wrapper.puppeteer
  local puppet = puppeteer.Puppet
  if puppet ~= nil then
    local puppetBlackboard = puppet:GetPrivateBlackboard()
    if puppetBlackboard then
      local inCombat = puppetBlackboard:GetBoolean("InCombat")
      if inCombat and not wrapper.attributes.PostUp and wrapper.attributes.AvailableDuringCombat then
        puppet:TriggerMoveEvent("kLE_CombatDisengage")
      else
        puppet:TriggerMoveEvent("kLE_Disengage")
      end
    else
      puppet:TriggerMoveEvent("kLE_Disengage")
    end
    local environmentEvent = wrapper.pendingEnvironmentEvent
    if environmentEvent then
      DestroyPuppeteer(wrapper)
      puppet:CallScript("OnEnvironmentEvent", environmentEvent)
      return
    end
  end
  DestroyPuppeteer(wrapper)
end
function OnPuppeteerCommandShotStart(wrapper)
  if wrapper.attributes.BreakOutOnBowFire then
    DestroyPuppeteer(wrapper)
    return
  end
  wrapper.stageName = "CommandShot"
  wrapper.puppeteer:SetStageName("CommandShot")
  wrapper.commandShotEnding = false
  SetupAllEvents(wrapper, true)
end
function OnPuppeteerCommandShotEnd(wrapper)
  wrapper.commandShotEnding = true
  if wrapper.persistentState.enabled == false and wrapper.attributes.PostUp ~= true then
    OnPuppeteerInterrupt(wrapper)
    return
  end
  if wrapper.stageName == "Interrupt" or wrapper.stageName == "Disengage" or wrapper.stageName == "LoopDisengage" or wrapper.stageName == "EnterPreempt" or wrapper.persistentState.enabled == false then
    OnPuppeteerDisengage(wrapper)
    return
  end
  if wrapper.attributes.PostUp then
    ResyncToLoop(wrapper)
  else
    SetupApproach(wrapper)
  end
end
local attributeList = {
  "BranchOnApproachStart",
  "BranchOnEnter",
  "BranchOnLoop",
  "BranchOnExit",
  "BranchOnDisengage",
  "BranchOnInterrupt",
  "BranchOnCommandShot",
  "BanterOnApproachStart",
  "BanterOnEnter",
  "BanterOnLoop",
  "BanterOnExit",
  "BanterOnDisengage",
  "BanterOnInterrupt",
  "BanterOnCommandShot",
  "BanterOnApproachStart_IsCritical",
  "BanterOnEnter_IsCritical",
  "BanterOnLoop_IsCritical",
  "BanterOnExit_IsCritical",
  "BanterOnDisengage_IsCritical",
  "BanterOnInterrupt_IsCritical",
  "BanterOnCommandShot_IsCritical",
  "CallbackOnApproachStart",
  "CallbackOnEnter",
  "CallbackOnLoop",
  "CallbackOnExit",
  "CallbackOnDisengage",
  "CallbackOnInterrupt",
  "CallbackOnCommandShot",
  "ApproachArrivalRadius",
  "ApproachBanterTriggerRadius",
  "ApproachIgnoreNavmesh",
  "ApproachMovementPriority",
  "ApproachSpeed",
  "AvailableDuringCombat",
  "AvailableOutsideCombat",
  "AvailableRange",
  "AvailableToCharacterType",
  "BehaviorBanterLoop",
  "BreakOutOnBowFire",
  "BreakOutOnReaction",
  "CausalAction",
  "CooldownAfterUse",
  "DebugName",
  "DisableContextBehaviorBanter",
  "DisengageIfPlayerFurtherThan",
  "EnabledAtLevelStart",
  "LoopActionBanter",
  "LoopActionBanter_IsCritical",
  "LoopActionFreqMax",
  "LoopActionFreqMin",
  "LoopActionInitialDelay",
  "LoopActionReferenceObject",
  "LoopActionToFidgetRatio",
  "LoopDuration",
  "OnlyPlayBanterOnce",
  "PostUp",
  "Priority",
  "ResetWhenDisengageInApproach",
  "SetCreatureAsOccupied",
  "SetCreatureAsUnavailable",
  "SettleOnArrival",
  "SonAttackType",
  "UseOffCameraEarlyExit",
  "UseSteadyOnExit",
  "ForceLoopDuration",
  LoopActionBranch = "list",
  LoopFidgetBranch = "list",
  Props = "list"
}
local CollectLuaTableAttributes = function(level, go)
  local attributes = go:GetLuaTableAttributes(attributeList)
  attributes.CallbackOnApproachStart = MLT.ExtractCallbacksForEvent(level, go, attributes.CallbackOnApproachStart)
  attributes.CallbackOnEnter = MLT.ExtractCallbacksForEvent(level, go, attributes.CallbackOnEnter)
  attributes.CallbackOnLoop = MLT.ExtractCallbacksForEvent(level, go, attributes.CallbackOnLoop)
  attributes.CallbackOnExit = MLT.ExtractCallbacksForEvent(level, go, attributes.CallbackOnExit)
  attributes.CallbackOnDisengage = MLT.ExtractCallbacksForEvent(level, go, attributes.CallbackOnDisengage)
  attributes.CallbackOnInterrupt = MLT.ExtractCallbacksForEvent(level, go, attributes.CallbackOnInterrupt)
  attributes.CallbackOnCommandShot = MLT.ExtractCallbacksForEvent(level, go, attributes.CallbackOnCommandShot)
  if attributes.Props then
    local objTable
    for _, objName in ipairs(attributes.Props) do
      local obj = GameObjects[objName]
      if obj ~= nil and obj.IsRefNode then
        obj = obj.Child
      end
      objTable = objTable or {}
      table.insert(objTable, obj)
    end
    attributes.Props = objTable
  end
  return attributes
end
local CreatePuppeteer = function(level, go, attributes, persistentState)
  return CreateOrRestartPuppeteer(level, go, attributes, persistentState)
end
function RestartPuppeteer(wrapper)
  return CreateOrRestartPuppeteer(wrapper.level, wrapper.go, wrapper.attributes, wrapper.persistentState, wrapper)
end
local CancelQueuedEnd = function(wrapper)
  wrapper.queuedEnd = nil
end
local UpdatePuppeteer = function(wrapper)
  if wrapper == nil then
    return
  end
  if wrapper.engaged and not wrapper:Valid() then
    local callbackEvent = CallbackEvents.Interrupt
    local callback = callbackEvent and wrapper.attributes[callbackEvent]
    if callback then
      MLT.ExecuteCallbacksForEvent(wrapper.level, wrapper.go, callback, callbackEvent)
    end
    DestroyPuppeteer(wrapper)
  end
  if wrapper.engaged and wrapper:Valid() then
    if wrapper.puppeteer.Puppet ~= nil then
      if not game.Combat.GetCombatStatus() and wrapper.stageName == "CommandShot" and wrapper.commandShotEnding == false and not wrapper.puppeteer.Puppet:HasMarker("CommandShot") then
        if wrapper.framesStuckInCommandShotState > 10 then
          OnPuppeteerCommandShotEnd(wrapper)
          wrapper.framesStuckInCommandShotState = 0
        else
          wrapper.framesStuckInCommandShotState = wrapper.framesStuckInCommandShotState + 1
        end
      else
        wrapper.framesStuckInCommandShotState = 0
      end
      local puppetBlackboard = wrapper.puppeteer.Puppet:GetPrivateBlackboard()
      if puppetBlackboard ~= nil and puppetBlackboard:IsValid() and puppetBlackboard:Exists("InCombat") and puppetBlackboard:IsBoolean("InCombat") then
        local inCombat = puppetBlackboard:GetBoolean("InCombat")
        if inCombat and not wrapper.attributes.AvailableDuringCombat then
          HandleInterrupt(wrapper, "BreakOut")
          return
        elseif not inCombat and not wrapper.attributes.AvailableOutsideCombat then
          HandleInterrupt(wrapper, "BreakOut")
          return
        end
      end
    end
    if wrapper.queuedEnd == nil and wrapper.stageName ~= "LoopDisengage" and wrapper.stageName ~= "EnterPreempt" then
      if (wrapper.attributes.ForceLoopDuration or BranchNameForEvent("Loop", wrapper.attributes)) and wrapper.stageName == "Loop" or wrapper.stageName == "LoopAction" then
        if wrapper.loopTimeRemaining then
          wrapper.loopTimeRemaining = wrapper.loopTimeRemaining - wrapper.level:GetUnitTime()
          if 0 >= wrapper.loopTimeRemaining then
            OnPuppeteerDisengage(wrapper)
          end
        end
        if wrapper.loopActionFreqTimeRemaining ~= nil then
          if 0 >= wrapper.loopActionFreqTimeRemaining then
            TriggerLoopAction(wrapper)
          else
            wrapper.loopActionFreqTimeRemaining = wrapper.loopActionFreqTimeRemaining - wrapper.level:GetUnitTime()
          end
        end
      end
      if wrapper.stageName == "ApproachStart" and not wrapper.approachBanterPlayed and wrapper.attributes[BanterEvents.ApproachStart] ~= nil and wrapper.attributes[BanterEvents.ApproachStart] ~= "" then
        local approachStartBanterDistance = wrapper.attributes.ApproachBanterTriggerRadius
        if approachStartBanterDistance and 0 < approachStartBanterDistance then
          local creature = wrapper.puppeteer.Puppet
          if creature ~= nil and creature ~= false then
            local distance = game.AIUtil.Distance(creature, wrapper.go)
            if approachStartBanterDistance > distance then
              PlayBanterForEvent("ApproachStart", wrapper)
              wrapper.approachBanterPlayed = true
            end
          end
        end
      end
      local disengageDistance = wrapper.attributes.DisengageIfPlayerFurtherThan or 0
      if 0 < disengageDistance then
        local player = game.Player.FindPlayer()
        local distance = game.AIUtil.Distance(player, wrapper.go)
        if disengageDistance < distance then
          if wrapper.puppeteer and wrapper.puppeteer.Puppet and not wrapper.puppeteer.Puppet:GetAI():CheckDecision("tweak_Decision_OnCamera") then
            HandleInterrupt(wrapper, "BreakOut")
          else
            HandleInterrupt(wrapper, "Disengage")
          end
        end
      end
    end
  elseif wrapper.cooldownTimeRemaining then
    wrapper.cooldownTimeRemaining = wrapper.cooldownTimeRemaining - wrapper.level:GetUnitTime()
    if 0 >= wrapper.cooldownTimeRemaining then
      RestartPuppeteer(wrapper)
    end
  end
end
function PlayBanterForEvent(event, wrapper)
  if not wrapper.attributes.OnlyPlayBanterOnce or wrapper.attributes.OnlyPlayBanterOnce and not wrapper.attributes[BanterEvents_HasPlayed[event]] then
    local banterEvent = BanterEvents[event]
    local banterIsCritical = wrapper.attributes[BanterEvents_IsCritical[event]]
    local banterName = banterEvent and wrapper.attributes[banterEvent]
    if banterName then
      if banterIsCritical then
        game.Audio.PlayBanter(banterName, nil, nil, false)
        OnBanterPlayed(wrapper)
      else
        game.Audio.PlayBanterNonCritical(banterName, nil, nil, false, function()
          OnBanterPlayed(wrapper)
        end)
      end
    end
  end
end
local DisablePuppeteer = function(wrapper)
  if wrapper.stageName == "Interrupt" or wrapper.stageName == "Disengage" or wrapper.stageName == "LoopDisengage" or wrapper.stageName == "EnterPreempt" then
    return
  end
  if wrapper.puppeteer == nil then
    DestroyPuppeteer(wrapper)
    return
  end
  if wrapper.stageName == "QueuedEnd" then
    HandleInterrupt(wrapper, "BreakOut")
  elseif wrapper.puppeteer.Puppet ~= nil and wrapper.attributes.UseOffCameraEarlyExit == true and not wrapper.puppeteer.Puppet:GetAI():CheckDecision("tweak_Decision_OnCamera") and wrapper.stageName ~= nil and wrapper.stageName ~= "Interrupt" then
    HandleInterrupt(wrapper, "BreakOut")
  else
    HandleInterrupt(wrapper, "Preempt")
  end
end
function InterruptPuppeteer(wrapper)
  HandleInterrupt(wrapper, "BreakOut")
  PlayBanterForEvent("Interrupt", wrapper)
end
local OnEnvironmentEvent = function(wrapper, event)
  if wrapper.attributes.BreakOutOnReaction then
    wrapper.pendingEnvironmentEvent = event
    HandleInterrupt(wrapper, "BreakOut")
    PlayBanterForEvent("Interrupt", wrapper)
  end
end
local ForceLoopAction = function(wrapper)
  wrapper.actionChance = 1
  TriggerLoopAction()
end
local LuaHook_CA_BranchDone = function(ai, branch)
  if ai.OwnedPOI then
    ai.OwnedPOI:SendEvent("CA_BranchDone")
  end
end
local LuaHook_CA_LoopDone = function(ai, branch)
  if ai.OwnedPOI then
    ai.OwnedPOI:SendEvent("CA_LoopDone")
  end
end
local LuaHook_CA_PlayBanter = function(ai, branch)
  if ai.OwnedPOI then
    ai.OwnedPOI:SendEvent("CA_PlayBanter")
  end
end
local LuaHook_CA_ShowProps = function(ai, branch)
  if ai.OwnedPOI then
    ai.OwnedPOI:SendEvent("CA_ShowProps")
  end
end
local LuaHook_CA_HideProps = function(ai, branch)
  if ai.OwnedPOI then
    ai.OwnedPOI:SendEvent("CA_HideProps")
  end
end
local LuaHook_ShouldBranchToEarlyExit = function(ai, branch)
  if not ai.OwnedPOI or ai.OwnedPOI and (ai.OwnedPOI:GetStageName() == "Disable" or ai.OwnedPOI:GetStageName() == "Disengage" or ai.OwnedPOI:GetStageName() == "EnterPreempt" or ai.OwnedPOI:GetStageName() == "LoopDisengage") then
    if ai.OwnedPOI then
      ai.OwnedPOI:SendEvent("CA_EarlyExit")
    else
      return true
    end
  end
end
local LuaHook_ShouldBranchToNav = function(ai, branch)
  if not ai.OwnedPOI or ai.OwnedPOI and (ai.OwnedPOI:GetStageName() == "Disable" or ai.OwnedPOI:GetStageName() == "Disengage" or ai.OwnedPOI:GetStageName() == "EnterPreempt" or ai.OwnedPOI:GetStageName() == "LoopDisengage") then
    if ai.OwnedPOI then
      ai.OwnedPOI:SendEvent("CA_Disengage")
    else
      return true
    end
  end
end
local InstallHooks = function()
  _G.LuaHook_CA_BranchDone = LuaHook_CA_BranchDone
  _G.LuaHook_CA_LoopDone = LuaHook_CA_LoopDone
  _G.LuaHook_CA_PlayBanter = LuaHook_CA_PlayBanter
  _G.LuaHook_CA_ShowProps = LuaHook_CA_ShowProps
  _G.LuaHook_CA_HideProps = LuaHook_CA_HideProps
  _G.LuaHook_ShouldBranchToNav = LuaHook_ShouldBranchToNav
  _G.LuaHook_ShouldBranchToEarlyExit = LuaHook_ShouldBranchToEarlyExit
end
return {
  InstallHooks = InstallHooks,
  CollectLuaTableAttributes = CollectLuaTableAttributes,
  ForceLoopAction = ForceLoopAction,
  CreatePuppeteer = CreatePuppeteer,
  UpdatePuppeteer = UpdatePuppeteer,
  DisablePuppeteer = DisablePuppeteer,
  OnEnvironmentEvent = OnEnvironmentEvent,
  InterruptPuppeteer = InterruptPuppeteer,
  CancelQueuedEnd = CancelQueuedEnd
}
