local LD = require("design.LevelDesignLibrary")
local monitors, TUT, CCOS, crank, thisLevel, player, playerPuppeteer, interactZone, interactZoneRear
local interactDirection = "front"
local tableCamera
local usingThisCrank = false
local crankSolved = false
local winCyclesOnPinned, crankStartState, state, drivenObject, drivenObjectName, crankDetachedBehavior, lockWhenFullyCranked
ledgePushEvent = nil
local startCycle, animSynchedObjectNames
local animSynchedCrankObjects = {}
local gearObjectNames, totalRewindTime, rewindRate, startAnimFrame, straightCrankAnimMonitor, synchBranchOverride, stuckAttemptCooldown, interactFX, numCycles
local currentCycle = 0
local animCyclesPerLoop, drivenObjFrameChangePerInterval, numPausePositions, minStopCycleStart, minStopCycle, minDrivenObjectFrame, maxStopCycleStart, maxStopCycle, maxDrivenObjectFrame
local cycleStopFrames = {}
local pausePositions = {}
local onEnterCompleteCallbacks, onForwardCallbacks, onBackwardCallbacks, onLockedAttemptCallbacks, rewindDelayWhenFullyCranked
local fps = 30
local nextStartCycle, endCycle, crankTweakName, crankType, crankSubType, driveObjectInReverse
local rewindFeedback = "FFB_GENERIC_RUMBLE_LOW"
local atEndFeedback = "FFB_MEDIUM"
local cameraType
local drivenObjectAnimState = "init"
local crankMotion = "idle"
local bUseDefaultRecenter = true
local camApproachCamera = "ENV_Crank_Approach"
local camOneShotApproachObject, cameraWorldSpaceForward
local bSkipInteractApproachYaw = false
local cameraSubmitDuration = 5
local InteractApproachTable, cameraRecenterOverride, cameraOverrideYaw, cameraOverridePitch, drivenObjAnimRate
local easingEnabled = true
local easeTime
local easeAnim = false
local animStarted = false
local targetAnimRate
local currentAnimRate = 0
local rateData = {}
local calBridgeTutorial = false
local CCOS_LoadLibrary = function()
  if CCOS == nil then
    CCOS = require("camera.camera_oneshot")
  end
end
local TUT_LoadLibrary = function()
  if TUT == nil then
    TUT = require("game.GlobalTutorials")
  end
end
local monitors_LoadLibrary = function()
  if monitors == nil then
    monitors = require("level.MonitorLibrary")
  end
end
function OnScriptLoaded(level, obj)
  cameraWorldSpaceForward = engine.Vector.New(0, 0, 1)
  crank = obj
  thisLevel = level
  player = game.Player.FindPlayer()
  if obj:FindJointIndex("promptJoint") ~= nil then
    interactZone = LD.CreateInteractZone_Standard_180(obj, "promptJoint")
    interactZone:SetXZRange(3)
    interactZone:SetHintXZRange(4)
    LD.EnableInteractZoneGlint(interactZone)
  end
  if obj:FindJointIndex("promptJointRear") ~= nil then
    interactZoneRear = LD.CreateInteractZone_Standard_180(obj, "promptJointRear")
    interactZoneRear:SetXZRange(3)
    interactZoneRear:SetHintXZRange(4)
    LD.EnableInteractZoneGlint(interactZoneRear)
  end
  interactFX = obj:FindSingleGOByName("crankInteract_FX")
  crankTweakName = crank:GetLuaTableAttribute("crankTweakName")
  animCyclesPerLoop = crank:GetLuaTableAttribute("animCyclesPerLoop")
  numPausePositions = crank:GetLuaTableAttribute("numPausePositions")
  crankStartState = crank:GetLuaTableAttribute("crankStartState")
  crankDetachedBehavior = crank:GetLuaTableAttribute("crankDetachedBehavior")
  totalRewindTime = crank:GetLuaTableAttribute("totalRewindTime")
  lockWhenFullyCranked = crank:GetLuaTableAttribute("lockWhenFullyCranked")
  local winCycles = crank:FindLuaTableAttribute("winCyclesOnPinned")
  animSynchedObjectNames = crank:GetLuaTableAttribute("animSynchedObjectNames")
  gearObjectNames = crank:GetLuaTableAttribute("gearObjectNames")
  numCycles = crank:GetLuaTableAttribute("numCycles")
  startCycle = crank:GetLuaTableAttribute("startCycle")
  drivenObjectName = crank:GetLuaTableAttribute("drivenObjectName")
  rewindDelayWhenFullyCranked = crank:GetLuaTableAttribute("rewindDelayWhenFullyCranked")
  tableCamera = crank:GetLuaTableAttribute("camera")
  ledgePushEvent = crank:FindLuaTableAttribute("ledgePushEvent")
  if ledgePushEvent and ledgePushEvent == "" then
    ledgePushEvent = nil
  end
  if winCycles ~= nil then
    winCycles = LD.ConvertStringListToTable(winCycles)
    winCyclesOnPinned = {}
    for i = 1, #winCycles do
      if string.match(winCycles[i], "%a") then
        engine.Warning("Entered an invalid win cycle: '" .. tostring(winCycles[i]) .. "' on crank: " .. tostring(obj.Parent))
      else
        winCyclesOnPinned[winCycles[i]] = winCycles[i]
      end
    end
  end
  if crankTweakName == "WheelCrank" then
    stuckAttemptCooldown = 1.5
  elseif crankTweakName == "ChainPulley" then
    stuckAttemptCooldown = 2
  else
    stuckAttemptCooldown = 1.75
  end
  game.SubObject.Sleep(obj)
end
function OnFirstPreStart(level, obj)
  SoundInitCrank()
  SoundInitDrivenObject()
  SetDrivenObject(drivenObjectName)
  easeTime = GetEaseTimeFromCrankType()
  if crankStartState == "Disabled" then
    state = "disabled"
  elseif crankStartState == "Locked" then
    state = "locked"
  else
    state = "enabled"
  end
  if crankTweakName == "WheelCrank" then
    crankType = "Wheel"
  elseif crankTweakName == "ChainPulley" then
    crankType = "Chain"
  else
    crankType = "Line"
    local crankName = obj:GetName()
    if string.find(crankName, "pushblock_s_") ~= nil or string.find(crankName, "handleattachpoint_side_") ~= nil then
      crankSubType = "PushBlock_Sideways"
    elseif string.find(crankName, "pushblock") ~= nil or string.find(crankName, "handleattachpoint_") ~= nil then
      crankSubType = "PushBlock"
    elseif string.find(crankName, "tbar_s_") ~= nil then
      crankSubType = "Tbar_Sideways"
    elseif string.find(crankName, "tbar") ~= nil then
      crankSubType = "Tbar"
    else
      crankSubType = ""
    end
  end
  minStopCycle = 0
  minStopCycleStart = minStopCycle
  minDrivenObjectFrame = 0
  maxStopCycle = numCycles
  maxStopCycleStart = maxStopCycle
  if CrankIsDrivingAnObject() then
    maxDrivenObjectFrame = drivenObject.AnimLengthFrames
    drivenObjFrameChangePerInterval = drivenObject.AnimLengthFrames / numCycles
  end
  currentCycle = startCycle
  if CrankIsDrivingAnObject() then
    rewindRate = drivenObject.AnimLengthFrames / fps * (1 / totalRewindTime)
    for i = 1, numCycles do
      cycleStopFrames[i] = math.ceil(i * drivenObjFrameChangePerInterval)
    end
    cycleStopFrames[#cycleStopFrames] = drivenObject.AnimLengthFrames
    if (crankDetachedBehavior == "rewind_pause positions" or crankDetachedBehavior == "fast forward_pause positions") and 0 < numPausePositions then
      DeterminePausePositions()
    end
    if 0 < currentCycle then
      drivenObject:JumpAnimToFrame(cycleStopFrames[currentCycle])
    end
    drivenObject:PauseAnim()
    crank:StartAnim(GetCurrentIdleAnim())
  end
  if not CrankIsLocked() and not CrankIsSolved() then
    HandleAnimationForDetachOrUnlock()
  end
  if currentCycle > numCycles then
    engine.Warning("Start cycle is set to be higher than total cycles on crank: ", crank.Parent:GetName())
  end
end
function OnPreStart(level, obj)
  SoundInitCrank()
  SoundInitDrivenObject()
  AssignCamera(tableCamera)
  if CrankIsDrivingAnObject() then
    SetAnimSynchedObjects(animSynchedObjectNames)
    if crankType == "Line" then
      AutoSetAnimSynchedCranksInHierarchy()
    end
    SetGearObjects(gearObjectNames)
  end
  if state == "disabled" or state == "solved" then
    Disable()
    Patch_0_RestoreStateIfSolved()
  elseif state == "locked" then
    Lock()
  else
    Enable()
  end
end
function Patch_0_RestoreStateIfSolved()
  if lockWhenFullyCranked == true then
    if crankDetachedBehavior == "rewind" or crankDetachedBehavior == "rewind_pause positions" or crankDetachedBehavior == "stationary" then
      if currentCycle == maxStopCycle then
        state = "solved"
      end
    elseif (crankDetachedBehavior == "fast forward" or crankDetachedBehavior == "fast forward_pause positions") and currentCycle == minStopCycle then
      state = "solved"
    end
  end
end
function OnStart(level, obj)
  Patch_0_RestoreSoundStateIfUnwinding()
end
function Patch_0_RestoreSoundStateIfUnwinding()
  if CrankIsDrivingAnObject() and not CrankIsLocked() and not CrankIsSolved() then
    if crankDetachedBehavior == "rewind" then
      if drivenObject.AnimFrame > minDrivenObjectFrame then
        SetCrankMotion("rewind")
      end
    elseif crankDetachedBehavior == "fast forward" then
      if drivenObject.AnimFrame < maxDrivenObjectFrame then
        SetCrankMotion("fast forward")
      end
    elseif crankDetachedBehavior == "rewind_pause positions" then
      if DrivenObjectAtValidPausePosition() == false then
        SetCrankMotion("rewind")
      end
    elseif crankDetachedBehavior == "fast forward_pause positions" and DrivenObjectAtValidPausePosition() == false then
      SetCrankMotion("fast forward")
    end
  end
end
function OnUpdate(level, obj)
  if camOneShotApproachObject ~= nil then
    camOneShotApproachObject:Update()
  end
  if PlayerIsAttached() then
    SubmitCrankCamera(cameraType, PlayerIsAttached())
    if CrankIsDrivingAnObject() and easeAnim and animStarted then
      local rData = GetTableAverage(rateData)
      local unitTime = level:GetUnitTime()
      if rData + targetAnimRate * (unitTime / easeTime) < targetAnimRate and (crankMotion == "forward" and crankType == "Line" or crankMotion == "backward" and crankType ~= "Line") or rData + targetAnimRate * (unitTime / easeTime) > targetAnimRate and (crankMotion == "backward" and crankType == "Line" or crankMotion == "forward" and crankType ~= "Line") then
        currentAnimRate = currentAnimRate + targetAnimRate * (unitTime / easeTime)
        rateData[#rateData + 1] = currentAnimRate
        drivenObject:SetAnimRate(currentAnimRate)
      else
        animStarted = false
        easeAnim = false
        currentAnimRate = targetAnimRate
        drivenObject:SetAnimRate(currentAnimRate)
        currentAnimRate = 0
        rateData = {}
      end
    end
  elseif camOneShotApproachObject == nil then
    game.SubObject.Sleep(obj)
  end
end
function ShowDebugTable()
  if (player.WorldPosition - crank.WorldPosition):Length() < 7 or PlayerIsAttached() then
    local debugTable = {}
    debugTable.Y = 15
    debugTable.Title = "Crank Info"
    debugTable.TitleColor = engine.Vector.New(255, 0, 128)
    if 0 < #animSynchedCrankObjects then
      local synchedCranks = "Anim synched cranks: "
      for _, c in pairs(animSynchedCrankObjects) do
        synchedCranks = synchedCranks .. tostring(c) .. " ||"
      end
      debugTable[#debugTable + 1] = {synchedCranks}
    end
    debugTable[#debugTable + 1] = {
      "State: " .. state
    }
    debugTable[#debugTable + 1] = {
      "Current Cycle: " .. currentCycle .. " of " .. numCycles
    }
    debugTable[#debugTable + 1] = {
      "Detach Behavior: " .. tostring(crankDetachedBehavior)
    }
    if CrankIsDrivingAnObject() then
      debugTable[#debugTable + 1] = {
        "Driven Obj Frame: " .. tostring(drivenObject.AnimFrame)
      }
    end
    debugTable[#debugTable + 1] = {
      "Min Cycle: " .. tostring(minStopCycle)
    }
    debugTable[#debugTable + 1] = {
      "Max Cycle: " .. tostring(maxStopCycle)
    }
    debugTable[#debugTable + 1] = {
      "Crank Anim: '" .. tostring(crank:GetAnimationName()) .. "'"
    }
    debugTable[#debugTable + 1] = {
      "Crank Frame: " .. tostring(crank.AnimFrame)
    }
    debugTable[#debugTable + 1] = {
      "Crank Frames Total: " .. tostring(crank.AnimLengthFrames)
    }
    if winCyclesOnPinned ~= nil and 0 < #winCyclesOnPinned then
      local winCycles = ""
      for _, value in pairs(winCyclesOnPinned) do
        winCycles = winCycles .. value .. ", "
      end
      debugTable[#debugTable + 1] = {
        "Win cycles: " .. tostring(winCycles)
      }
    end
    local stopFrameValues = ""
    for i = 1, #cycleStopFrames do
      stopFrameValues = stopFrameValues .. cycleStopFrames[i] .. ", "
    end
    debugTable[#debugTable + 1] = {
      "Cycle Stop Frames: " .. tostring(stopFrameValues)
    }
    if 0 < #pausePositions then
      local pausePosValues = ""
      for i = 1, #pausePositions do
        pausePosValues = pausePosValues .. pausePositions[i] .. ", "
      end
      debugTable[#debugTable + 1] = {
        "Pause Pos Frames: " .. tostring(pausePosValues)
      }
    end
    debugTable[#debugTable + 1] = {
      "Crank Motion: " .. crankMotion
    }
    debugTable[#debugTable + 1] = {
      "Driven Obj Motion: " .. drivenObjectAnimState
    }
    engine.DrawDebugTable(debugTable)
  end
end
function OnUseWorld(level, obj)
  if interactZone ~= nil and interactZone:PlayerCanInteract() then
    player:RequestInteract(obj, interactZone)
    interactDirection = "front"
  end
  if interactZoneRear ~= nil and interactZoneRear:PlayerCanInteract() then
    player:RequestInteract(obj, interactZoneRear)
    interactDirection = "rear"
  end
end
function ExecuteLuaCallback(attrName, eventDescription)
  eventDescription = eventDescription or "Crank Event"
  local luaAttr = crank:FindLuaTableAttribute(attrName)
  LD.ExtractAndExecuteCallbacksForEvent(thisLevel, crank, luaAttr, eventDescription)
end
function RegisterOnPlayerEnterCompleteCallbacks(fn)
  if onEnterCompleteCallbacks == nil then
    onEnterCompleteCallbacks = {}
  end
  onEnterCompleteCallbacks[#onEnterCompleteCallbacks + 1] = fn
end
function FireOnPlayerEnterCompleteCallbacks()
  if onEnterCompleteCallbacks ~= nil then
    for _, fn in pairs(onEnterCompleteCallbacks) do
      fn()
    end
  end
end
function RegisterLockedAttemptCallback(fn)
  if onLockedAttemptCallbacks == nil then
    onLockedAttemptCallbacks = {}
  end
  onLockedAttemptCallbacks[#onLockedAttemptCallbacks + 1] = fn
end
function FireLockedAttemptCallback(currentCycle, direction)
  if onLockedAttemptCallbacks ~= nil then
    for _, fn in pairs(onLockedAttemptCallbacks) do
      fn()
    end
  end
end
function RegisterForwardPullCallback(fn)
  if onForwardCallbacks == nil then
    onForwardCallbacks = {}
  end
  onForwardCallbacks[#onForwardCallbacks + 1] = fn
end
function FireForwardCallback(newCycle)
  if onForwardCallbacks ~= nil then
    for _, fn in pairs(onForwardCallbacks) do
      fn(newCycle)
    end
  end
end
function RegisterBackwardPullCallback(fn)
  if onBackwardCallbacks == nil then
    onBackwardCallbacks = {}
  end
  onBackwardCallbacks[#onBackwardCallbacks + 1] = fn
end
function FireBackwardCallback(newCycle)
  if onBackwardCallbacks ~= nil then
    for _, fn in pairs(onBackwardCallbacks) do
      fn(newCycle)
    end
  end
end
function OnInteractStart(level, obj, creature)
  game.SubObject.Wake(crank)
  ExecuteLuaCallback("onInteractStartEvent", "Interact Start")
  playerPuppeteer = game.Puppeteer.NewForce(obj, "crankInteractive" .. obj:GetName() .. "Hero", player)
  local synchJoint
  if interactDirection == "front" then
    if obj:FindJointIndex("synchJoint") ~= nil then
      synchJoint = "synchJoint"
    else
      synchJoint = "synchJointFront"
    end
    driveObjectInReverse = false
  elseif interactDirection == "rear" then
    synchJoint = "synchJointRear"
    driveObjectInReverse = true
  end
  local syncBranch
  if synchBranchOverride ~= nil then
    syncBranch = synchBranchOverride
    synchBranchOverride = nil
  else
    syncBranch = "BRA_" .. crankTweakName .. "_Enter01"
  end
  playerPuppeteer:WeaponEquip({weaponMode = "Bare"})
  local approachData = LD.GetGlobalApproachData()
  approachData.branch_name = syncBranch
  approachData.joint_name = synchJoint
  player:SetAccelerationOverride(approachData.acceleration_override)
  player:SetDecelerationOverride(approachData.deceleration_override)
  player:ForceLookAtToObject(crank)
  playerPuppeteer:Approach(approachData)
  playerPuppeteer:OnComplete(function()
    player:ClearForcedLookAtToObject()
    OnPuppeteerArrival()
    ShowTutorial_OnFirstUse()
    if player.ClearFocus then
      player:ClearFocus()
    end
    player:ClearDecelerationOverride()
    player:ClearAccelerationOverride()
    local cappedPercent = math.fmod(currentCycle / animCyclesPerLoop, 1)
    local crankSlaves = {
      {
        Slave = crank,
        NumberLoopSteps = animCyclesPerLoop,
        ForceSlaveStartPos = cappedPercent
      }
    }
    if 0 < #animSynchedCrankObjects then
      for _, crank in pairs(animSynchedCrankObjects) do
        crankSlaves[#crankSlaves + 1] = {Slave = crank}
      end
    end
    playerPuppeteer:Sync(syncBranch, true, crankSlaves, synchJoint)
    playerPuppeteer:OnComplete(function()
      playerPuppeteer = nil
    end)
    ExecuteLuaCallback("attachedEvent", "Attached to crank")
  end)
  local playerPosition = player:GetWorldPosition()
  local interactPosition = obj:GetWorldPosition()
  local toInteract = interactPosition - playerPosition
  local interactFwd = obj:GetWorldForward()
  local interactAngle
  if toInteract:Dot(interactFwd) > 0 then
    print("Approaching from front")
    interactAngle = LD.GetAngleBetweenVector(-interactFwd, cameraWorldSpaceForward)
  else
    print("Approaching from behind")
    interactAngle = LD.GetAngleBetweenVector(interactFwd, cameraWorldSpaceForward)
  end
  if bSkipInteractApproachYaw == false and cameraOverrideYaw == nil then
    InteractApproachTable = {Yaw = interactAngle}
  elseif cameraOverrideYaw ~= nil then
    if InteractApproachTable == nil then
      InteractApproachTable = {}
    end
    InteractApproachTable.Yaw = cameraOverrideYaw
  end
  if cameraOverridePitch ~= nil then
    if InteractApproachTable == nil then
      InteractApproachTable = {}
    end
    InteractApproachTable.Pitch = cameraOverridePitch
  end
  CCOS_LoadLibrary()
  camOneShotApproachObject = CCOS.OneShotCamera.New(camApproachCamera, cameraSubmitDuration, InteractApproachTable)
  camOneShotApproachObject:Start()
  if bUseDefaultRecenter == false then
    if cameraRecenterOverride ~= nil then
      game.Camera.Recenter(cameraRecenterOverride)
    end
  elseif crankType == "Chain" then
    game.Camera.Recenter({
      TimeStart = 0,
      RotationSpace = 1,
      TimeDuration = 1.3,
      LockRecenter = 0.2,
      YawRange = -1,
      TriggerLeft = InteractApproachTable.Yaw,
      TriggerRight = InteractApproachTable.Yaw,
      ReturnLeft = InteractApproachTable.Yaw,
      ReturnRight = InteractApproachTable.Yaw,
      PitchRange = -1,
      ReturnUp = -6,
      ReturnDown = -6
    })
  end
  PlayAttachSound()
end
function ShowTutorial_OnFirstUse()
  if crankTweakName ~= "Cal100BridgePush" then
    if crankType == "Wheel" then
      if LD.GetEntityVariable("TUT_FirstWheelCrankTutorial") == false and PlayerIsAttached() then
        TUT_LoadLibrary()
        TUT.CrankControls_Wheel_Tutorial()
      end
    elseif crankType == "Chain" then
      if LD.GetEntityVariable("TUT_FirstChainCrankTutorial") == false and PlayerIsAttached() then
        TUT_LoadLibrary()
        TUT.CrankControls_Chain_Tutorial()
      end
    elseif crankType == "Line" and LD.GetEntityVariable("TUT_FirstLineCrankTutorial") == false and PlayerIsAttached() then
      TUT_LoadLibrary()
      if crankSubType == "PushBlock_Sideways" then
        TUT.CrankControls_Line_Sideways_Tutorial()
      else
        TUT.CrankControls_Line_Tutorial()
      end
    end
  end
end
function HideTutorial()
  TUT_LoadLibrary()
  LD.CallFunctionAfterDelay(TUT.HideTutorial, 0.5)
end
function ClearTutorialAndSetState(var)
  if crankTweakName ~= "Cal100BridgePush" then
    if crankType == "Wheel" then
      if LD.GetEntityVariable("TUT_FirstWheelCrankTutorial") == false then
        HideTutorial()
        if var then
          LD.SetEntityVariable("TUT_FirstWheelCrankTutorial", true)
        end
      end
    elseif crankType == "Chain" then
      if LD.GetEntityVariable("TUT_FirstChainCrankTutorial") == false then
        HideTutorial()
        if var then
          LD.SetEntityVariable("TUT_FirstChainCrankTutorial", true)
        end
      end
    elseif crankType == "Line" and LD.GetEntityVariable("TUT_FirstLineCrankTutorial") == false then
      HideTutorial()
      if var then
        LD.SetEntityVariable("TUT_FirstLineCrankTutorial", true)
      end
    end
  end
end
function OnPuppeteerArrival()
  usingThisCrank = true
  game.SubObject.Wake(crank)
  if crankMotion == "rewind" then
    local _, cycle = GetNearestBackwardStopFrame()
    currentCycle = cycle
    HandleAnimationForLockOrReEnter()
  elseif crankMotion == "fast forward" then
    local _, cycle = GetNearestForwardStopFrame()
    currentCycle = cycle
    HandleAnimationForLockOrReEnter()
  end
  CheckCrankStateForMoveEvent()
  SetCurrentCycle_AnimSynchedCranks(currentCycle)
end
function RequestInteractOverride(newSynchBranch)
  synchBranchOverride = newSynchBranch
  player:RequestInteract(crank, interactZone)
end
function DisablePlayerInput()
  StopAllLoopingSounds(crankMotion)
  game.UI.Idle(true, false)
end
function EnablePlayerInput()
  game.UI.Idle(false, false)
end
function Enable()
  state = "enabled"
  if interactZone ~= nil then
    interactZone:Enable()
  end
  if interactZoneRear ~= nil then
    interactZoneRear:Enable()
  end
end
function Disable()
  if state ~= "solved" then
    state = "disabled"
  end
  if interactZone ~= nil then
    interactZone:Disable()
  end
  if interactZoneRear ~= nil then
    interactZoneRear:Disable()
  end
  StopAllLoopingSounds(crankMotion)
end
function Lock()
  if state ~= "solved" then
    if not CrankIsLocked() then
      PlayOnLockedSound()
    end
    state = "locked"
    if PlayerIsAttached() then
      if winCyclesOnPinned ~= nil and winCyclesOnPinned[tostring(currentCycle)] ~= nil then
        RemovePlayerImmediate()
        return
      end
      AddMarker("CrankAtStart")
      AddMarker("CrankAtEnd")
    else
      HandleAnimationForLockOrReEnter()
    end
  end
end
function CrankComplete()
  crankSolved = true
  state = "solved"
  if not CrankIsLocked() then
    PlayOnLockedSound()
  end
  if PlayerIsAttached() then
    AddMarker("CrankAtStart")
    AddMarker("CrankAtEnd")
  end
  if interactZone ~= nil then
    interactZone:Disable()
  end
  if interactZoneRear ~= nil then
    interactZoneRear:Disable()
  end
  SetCrankMotion("idle")
  CleanupDrivenObjectAnim()
  crank:StartAnim(GetCurrentIdleAnim(), 0.2)
end
function RemovePlayer()
  AddMarker("CrankComplete")
  player:TriggerMoveEvent("kLEAutoExit")
end
function RemovePlayerImmediate()
  player:TriggerMoveEvent("kLEAutoExitImmediate")
end
function Unlock()
  if state ~= "solved" then
    state = "enabled"
    HandleAnimationForDetachOrUnlock()
    PlayOnUnlockedSound()
    CheckCrankStateForMoveEvent()
  end
end
function OnInteractAbort(level, obj, creature)
  HandlePlayerDetached()
end
function OnExitInteraction(level, obj, creature)
  HandlePlayerDetached()
end
function HandlePlayerDetached()
  ClearTutorialAndSetState(false)
  if straightCrankAnimMonitor ~= nil then
    straightCrankAnimMonitor:Stop()
    straightCrankAnimMonitor:Terminate()
    straightCrankAnimMonitor = nil
  end
  usingThisCrank = false
  easeAnim = false
  animStarted = false
  camOneShotApproachObject = nil
  PlayDetachSound()
  RemoveMarker("CrankAtEnd")
  RemoveMarker("CrankAtStart")
  RemoveMarker("CrankComplete")
  RemoveMarker("StuckAttemptOnCooldown")
  ExecuteLuaCallback("detachedEvent", "Detached from crank")
  if ledgePushEvent ~= nil and currentCycle == maxStopCycle then
    return
  end
  if state ~= "solved" then
    if currentCycle == maxStopCycle and 0 < rewindDelayWhenFullyCranked then
      LD.CallFunctionAfterDelay(function()
        HandleAnimationForDetachOrUnlock()
        HandleAnimationForDetachOrUnlock_AnimSynchedCranks()
      end, rewindDelayWhenFullyCranked)
    else
      HandleAnimationForDetachOrUnlock()
      HandleAnimationForDetachOrUnlock_AnimSynchedCranks()
    end
  end
end
function OnLockedExitInteraction(level, obj, creature)
  if state == "locked" then
    HandleAnimationForLockOrReEnter()
  end
end
function OnInteractFinish(level, obj, creature)
end
function LuaHook_OnPlayerEnterComplete(level, obj, creature)
  FireOnPlayerEnterCompleteCallbacks()
  if crankTweakName == "Cal100BridgePush" then
    calBridgeTutorial = true
    TUT_LoadLibrary()
    TUT.CrankControls_CalBridge_Tutorial()
  end
end
function LuaHook_OnCrankFromStationary(level, obj, creature)
  if easingEnabled then
    easeAnim = true
    currentAnimRate = 0
    rateData = {}
  end
end
function LuaHook_OnPlayerEnter(level, obj, creature)
end
function CheckCrankStateForMoveEvent()
  if PlayerIsAttached() then
    if state == "locked" then
      AddMarker("CrankAtStart")
      AddMarker("CrankAtEnd")
    elseif currentCycle == minStopCycle and currentCycle == maxStopCycle then
      AddMarker("CrankAtStart")
      AddMarker("CrankAtEnd")
    elseif currentCycle <= minStopCycle then
      if DrivenObjectMotionIsReversed() then
        AddMarker("CrankAtEnd")
        RemoveMarker("CrankAtStart")
      else
        AddMarker("CrankAtStart")
        RemoveMarker("CrankAtEnd")
      end
    elseif currentCycle >= maxStopCycle then
      if ledgePushEvent ~= nil then
        player:TriggerMoveEvent(ledgePushEvent)
        Disable()
      elseif DrivenObjectMotionIsReversed() then
        AddMarker("CrankAtStart")
        RemoveMarker("CrankAtEnd")
      else
        AddMarker("CrankAtEnd")
        RemoveMarker("CrankAtStart")
      end
    else
      RemoveMarker("CrankAtStart")
      RemoveMarker("CrankAtEnd")
    end
  end
end
function AddMarker(marker)
  if PlayerIsAttached() and player:HasMarker(marker) == false then
    player:AddMarker(marker)
  end
end
function RemoveMarker(marker)
  if player:HasMarker(marker) then
    player:RemoveMarker(marker)
  end
end
function LuaHook_OnForward(level, obj, creature)
  startAnimFrame = crank.AnimFrame
  FireForwardCallback(currentCycle + 1)
  if interactFX then
    LD.ShowFX(interactFX)
  end
end
function LuaHook_OnDrivenObjectForwardStartPlayer(level, obj, creature)
  if crankType == "Line" then
    drivenObject:PauseAnim()
  end
end
function LuaHook_OnDrivenObjectForwardStart(level, obj, creature)
  ClearTutorialAndSetState(true)
  if calBridgeTutorial then
    calBridgeTutorial = false
    HideTutorial()
  end
  animStarted = true
  if DrivenObjectMotionIsReversed() then
    DecrementCurrentCycle()
    DecrementCurrentCycle_AnimSynchedCranks()
  else
    IncrementCurrentCycle()
    IncrementCurrentCycle_AnimSynchedCranks()
  end
  if currentCycle == maxStopCycle then
    if lockWhenFullyCranked and (crankDetachedBehavior == "rewind" or crankDetachedBehavior == "rewind_pause positions" or crankDetachedBehavior == "stationary") then
      RemovePlayer()
    end
  elseif currentCycle == minStopCycle and lockWhenFullyCranked and (crankDetachedBehavior == "fast forward" or crankDetachedBehavior == "fast forward_pause positions") then
    RemovePlayer()
  end
  SetCrankMotion("backward")
  PlayCrankMotionSound()
  CheckCrankStateForMoveEvent()
  if CrankIsDrivingAnObject() then
    local tempAnimRate = GetAnimRateFromCrankFramesData("Down")
    if DrivenObjectMotionIsReversed() then
      tempAnimRate = tempAnimRate * -1
    end
    drivenObjAnimRate = tempAnimRate
    if easeAnim then
      targetAnimRate = tempAnimRate
      tempAnimRate = 0
      rateData = {tempAnimRate}
    end
    if crankType == "Line" then
      drivenObject:PlayAnimToEnd(tempAnimRate)
      if straightCrankAnimMonitor ~= nil then
        straightCrankAnimMonitor:Stop()
        straightCrankAnimMonitor:Terminate()
        straightCrankAnimMonitor = nil
      end
      monitors_LoadLibrary()
      straightCrankAnimMonitor = monitors.CreateAnimFrameMonitor(crank)
      straightCrankAnimMonitor:OnFrame(crank.AnimLengthFrames, function()
        DrivenObjectAnimationComplete_Monitor(straightCrankAnimMonitor)
      end)
    else
      StartDrivenObjectAnimation(currentCycle, tempAnimRate)
    end
    ExecuteLuaCallback("drivenObjForwardEvent", "Driven object began animating forward")
  end
end
function LuaHook_OnDrivenObjectForwardComplete(level, obj, creature)
  easeAnim = false
  animStarted = false
  currentAnimRate = 0
  if currentCycle == maxStopCycle and maxDrivenObjectFrame == cycleStopFrames[maxStopCycle] then
    StopAllLoopingSounds(crankMotion)
    if crankDetachedBehavior == "fast forward" or crankDetachedBehavior == "fast forward_pause positions" then
      SetCrankMotion("idle")
      drivenObjectAnimState = "beginning"
      LD.AddControllerRumble({EffectName = atEndFeedback, Duration = 1})
      ExecuteLuaCallback("atBeginningEvent", "Reached beginning of anim")
    else
      if lockWhenFullyCranked then
        CrankComplete()
      end
      SetCrankMotion("idle")
      drivenObjectAnimState = "end"
      LD.AddControllerRumble({EffectName = atEndFeedback, Duration = 1})
      ExecuteLuaCallback("atEndEvent", "Reached end of anim")
    end
  end
end
function LuaHook_DownLockedAttempt(level, obj, creature)
  PlayOnStuckAtEndSound()
  FireLockedAttemptCallback(currentCycle, "Backward")
  AddMarker("StuckAttemptOnCooldown")
  LD.CallFunctionAfterDelay(function()
    RemoveMarker("StuckAttemptOnCooldown")
  end, stuckAttemptCooldown)
end
function LuaHook_OnBackward()
  startAnimFrame = crank.AnimFrame
  FireBackwardCallback(currentCycle - 1)
  if interactFX then
    LD.ShowFX(interactFX)
  end
end
function LuaHook_OnDrivenObjectBackwardStartPlayer(level, obj, creature)
  if crankType == "Line" then
    drivenObject:PauseAnim()
  end
end
function LuaHook_OnDrivenObjectBackwardStart(level, obj, creature)
  ClearTutorialAndSetState(true)
  animStarted = true
  if DrivenObjectMotionIsReversed() then
    IncrementCurrentCycle()
    IncrementCurrentCycle_AnimSynchedCranks()
  else
    DecrementCurrentCycle()
    DecrementCurrentCycle_AnimSynchedCranks()
  end
  if currentCycle == minStopCycle then
    if lockWhenFullyCranked and (crankDetachedBehavior == "fast forward" or crankDetachedBehavior == "fast forward_pause positions") then
      RemovePlayer()
    end
  elseif currentCycle == maxStopCycle and lockWhenFullyCranked and (crankDetachedBehavior == "rewind" or crankDetachedBehavior == "rewind_pause positions" or crankDetachedBehavior == "stationary") then
    RemovePlayer()
  end
  SetCrankMotion("forward")
  PlayCrankMotionSound()
  CheckCrankStateForMoveEvent()
  if CrankIsDrivingAnObject() then
    local tempAnimRate = GetAnimRateFromCrankFramesData("Up")
    if not DrivenObjectMotionIsReversed() then
      tempAnimRate = tempAnimRate * -1
    end
    drivenObjAnimRate = tempAnimRate
    if easeAnim then
      targetAnimRate = tempAnimRate
      tempAnimRate = 0
      rateData = {tempAnimRate}
    end
    if crankType == "Line" then
      drivenObject:PlayAnimToEnd(tempAnimRate)
      if straightCrankAnimMonitor ~= nil then
        straightCrankAnimMonitor:Stop()
        straightCrankAnimMonitor:Terminate()
        straightCrankAnimMonitor = nil
      end
      monitors_LoadLibrary()
      straightCrankAnimMonitor = monitors.CreateAnimFrameMonitor(crank)
      straightCrankAnimMonitor:OnFrame(crank.AnimLengthFrames, function()
        DrivenObjectAnimationComplete_Monitor(straightCrankAnimMonitor)
      end)
    else
      StartDrivenObjectAnimation(currentCycle, tempAnimRate)
    end
    ExecuteLuaCallback("drivenObjBackwardEvent", "Driven object began animating backward")
  end
end
function LuaHook_OnDrivenObjectBackwardComplete(level, obj, creature)
  easeAnim = false
  animStarted = false
  if currentCycle == minStopCycle and (minDrivenObjectFrame == cycleStopFrames[minStopCycle] or minDrivenObjectFrame == 0) then
    StopAllLoopingSounds(crankMotion)
    if crankDetachedBehavior == "fast forward" or crankDetachedBehavior == "fast forward_pause positions" then
      if lockWhenFullyCranked then
        CrankComplete()
      end
      SetCrankMotion("idle")
      drivenObjectAnimState = "end"
      LD.AddControllerRumble({EffectName = atEndFeedback, Duration = 1})
      ExecuteLuaCallback("atEndEvent", "Reached end of anim")
    else
      SetCrankMotion("idle")
      drivenObjectAnimState = "beginning"
      LD.AddControllerRumble({EffectName = atEndFeedback, Duration = 1})
      ExecuteLuaCallback("atBeginningEvent", "Reached beginning of anim")
    end
  end
end
function LuaHook_UpLockedAttempt(level, obj, creature)
  PlayOnStuckAtStartSound()
  FireLockedAttemptCallback(currentCycle, "Forward")
  AddMarker("StuckAttemptOnCooldown")
  LD.CallFunctionAfterDelay(function()
    RemoveMarker("StuckAttemptOnCooldown")
  end, stuckAttemptCooldown)
end
function LuaHook_OnStopCrank(level, obj, creature)
  if crankMotion ~= "idle" then
    StopAllLoopingSounds(crankMotion)
    SetCrankMotion("idle")
  end
  if currentCycle ~= minStopCycle and currentCycle ~= maxStopCycle then
    drivenObjectAnimState = "middle"
  end
  ExecuteLuaCallback("onPauseEvent", "DrivenObject animation paused.")
  local currCycle = cycleStopFrames[currentCycle] or 0
  if PlayerIsAttached() and CrankIsDrivingAnObject() then
    CleanupDrivenObjectAnim()
  end
end
function StartDrivenObjectAnimation(newCycle, rate)
  if CrankIsDrivingAnObject() then
    local frame = cycleStopFrames[newCycle] or 0
    drivenObject:PlayAnimToFrame(frame, rate)
    drivenObject:OnAnimationDone(crank, "DrivenObjectAnimationComplete", {Force = true})
    local timeInOneIncrement = totalRewindTime / numCycles
    local framesInOneIncrement = drivenObject.AnimLengthFrames / numCycles
    local framesRemainingInCurrentIncrement = math.abs(drivenObject.AnimFrame - frame)
    local timeRemainingInCrankAnim = timeInOneIncrement * (framesRemainingInCurrentIncrement / framesInOneIncrement)
    if state ~= "locked" and not PlayerIsAttached() then
      game.Blender.Trigger({
        Name = rewindFeedback,
        Duration = timeRemainingInCrankAnim,
        TweenIn = {Time = 0},
        TweenOut = {Time = 0.25}
      })
    end
    return timeRemainingInCrankAnim
  end
end
function CleanupDrivenObjectAnim()
  if CrankIsDrivingAnObject() and drivenObjAnimRate ~= nil then
    local currCycleFrame = cycleStopFrames[currentCycle] or 0
    if drivenObject.AnimFrame == currCycleFrame then
      drivenObject:SetAnimRate(0)
      drivenObject:PauseAnim()
    elseif 0 < drivenObjAnimRate then
      if currCycleFrame < drivenObject.AnimFrame then
        drivenObject:PlayAnimToFrame(currCycleFrame, -drivenObjAnimRate)
      else
        drivenObject:PlayAnimToFrame(currCycleFrame, drivenObjAnimRate)
      end
    elseif currCycleFrame < drivenObject.AnimFrame then
      drivenObject:PlayAnimToFrame(currCycleFrame, drivenObjAnimRate)
    else
      drivenObject:PlayAnimToFrame(currCycleFrame, -drivenObjAnimRate)
    end
  end
end
function DrivenObjectAnimationComplete_Monitor(monitor)
  if monitor ~= nil then
    monitor:Stop()
    monitor:Terminate()
    monitor = nil
  end
  DrivenObjectAnimationComplete()
end
function DrivenObjectAnimationComplete()
  if crankMotion == "fast forward" or crankMotion == "rewind" then
    StopAllLoopingSounds(crankMotion)
    SetCrankMotion("idle")
  end
  if drivenObject ~= nil then
    drivenObject:ClearAllAnimCallbacks()
  end
  if state ~= "locked" and not PlayerIsAttached() then
    LD.AddScreenShake({
      EffectName = "FSE_SHAKE_VIBRATE_CRYSTAL_INTACT"
    })
    LD.AddControllerRumble({EffectName = "FFB_GIANT"})
    if crankDetachedBehavior == "stationary" or crankDetachedBehavior == "rewind" or crankDetachedBehavior == "rewind_pause positions" then
      if currentCycle == minStopCycle then
        ExecuteLuaCallback("atBeginningEvent", "Reached beginning of anim")
      elseif currentCycle == maxStopCycle then
        ExecuteLuaCallback("atEndEvent", "Reached end of anim")
      end
    elseif currentCycle == minStopCycle then
      ExecuteLuaCallback("atEndEvent", "Reached end of anim")
    elseif currentCycle == maxStopCycle then
      ExecuteLuaCallback("atBeginningEvent", "Reached beginning of anim")
    end
  end
end
function StartCrankAnim(currCycle, newCycle)
  if crankType ~= "Line" then
    crank:StartAnim("env" .. crankTweakName .. "ReleaseDown")
    if newCycle < currCycle then
      SetCrankMotion("rewind")
      AnimateCycleReverse(math.abs(currCycle - newCycle), currCycle, newCycle)
    elseif currCycle < newCycle then
      SetCrankMotion("fast forward")
      AnimateCycleForward(math.abs(currCycle - newCycle), currCycle, newCycle)
    end
  end
  PlayCrankMotionSound()
end
function PlayNextCycle()
  crank:ClearAllAnimCallbacks()
  StartCrankAnim(nextStartCycle, endCycle)
end
function AnimateCycleReverse(totalCyclesRemaining, currCycle, newCycle)
  if totalCyclesRemaining > animCyclesPerLoop then
    endCycle = newCycle
    nextStartCycle = currCycle
    local currentCrankFrame
    if currCycle % animCyclesPerLoop == 0 then
      nextStartCycle = currCycle - animCyclesPerLoop
      currentCrankFrame = crank.AnimLengthFrames
      if not CrankIsLocked() then
        crank:JumpAnimToFrame(currentCrankFrame or 0)
      end
    else
      nextStartCycle = currCycle - currCycle % animCyclesPerLoop
      currentCrankFrame = currCycle % animCyclesPerLoop * (crank.AnimLengthFrames / animCyclesPerLoop)
      if not CrankIsLocked() then
        crank:JumpAnimToFrame(currentCrankFrame or 0)
      end
    end
    local newCrankFrame = newCycle % animCyclesPerLoop * (crank.AnimLengthFrames / animCyclesPerLoop)
    local crankFramesToAnimate = totalCyclesRemaining * (crank.AnimLengthFrames / animCyclesPerLoop)
    local currentDrivenObjectFrame = cycleStopFrames[currCycle] or 0
    local newDrivenObjectFrame = cycleStopFrames[newCycle] or 0
    local timeToAnimate = math.abs(currentDrivenObjectFrame - newDrivenObjectFrame) / drivenObject.AnimLengthFrames * totalRewindTime
    local crankAnimRate = crankFramesToAnimate / timeToAnimate / fps
    crank:PlayAnimToFrame(newCrankFrame, -crankAnimRate)
    crank:OnAnimationDone(crank, "PlayNextCycle")
  else
    local currentCrankFrame
    if currCycle % animCyclesPerLoop == 0 then
      currentCrankFrame = crank.AnimLengthFrames
      if not CrankIsLocked() then
        crank:JumpAnimToFrame(currentCrankFrame or 0)
      end
    else
      currentCrankFrame = currCycle % animCyclesPerLoop * (crank.AnimLengthFrames / animCyclesPerLoop)
      if not CrankIsLocked() then
        crank:JumpAnimToFrame(currentCrankFrame or 0)
      end
    end
    local newCrankFrame = newCycle % animCyclesPerLoop * (crank.AnimLengthFrames / animCyclesPerLoop)
    local crankFramesToAnimate = totalCyclesRemaining * (crank.AnimLengthFrames / animCyclesPerLoop)
    local currentDrivenObjectFrame = cycleStopFrames[currCycle] or 0
    local newDrivenObjectFrame = cycleStopFrames[newCycle] or 0
    local timeToAnimate = math.abs(currentDrivenObjectFrame - newDrivenObjectFrame) / drivenObject.AnimLengthFrames * totalRewindTime
    local crankAnimRate = crankFramesToAnimate / timeToAnimate / fps
    crank:PlayAnimToFrame(newCrankFrame, -crankAnimRate)
    crank:OnAnimationDone(crank, "CrankAnimComplete")
  end
end
function AnimateCycleForward(totalCyclesRemaining, currCycle, newCycle)
  if totalCyclesRemaining >= animCyclesPerLoop or newCycle % animCyclesPerLoop < currCycle % animCyclesPerLoop then
    nextStartCycle = newCycle
    endCycle = newCycle
    local currentCrankFrame
    if currCycle % animCyclesPerLoop == 0 then
      nextStartCycle = currCycle + animCyclesPerLoop
      currentCrankFrame = 0
      if not CrankIsLocked() then
        crank:JumpAnimToFrame(currentCrankFrame or 0)
      end
    else
      nextStartCycle = currCycle + (animCyclesPerLoop - currCycle % animCyclesPerLoop)
      currentCrankFrame = currCycle % animCyclesPerLoop * (crank.AnimLengthFrames / animCyclesPerLoop)
      if not CrankIsLocked() then
        crank:JumpAnimToFrame(currentCrankFrame or 0)
      end
    end
    local newCrankFrame = crank.AnimLengthFrames
    local crankFramesToAnimate = totalCyclesRemaining * (crank.AnimLengthFrames / animCyclesPerLoop)
    local currentDrivenObjectFrame = cycleStopFrames[currCycle] or 0
    local newDrivenObjectFrame = cycleStopFrames[newCycle] or 0
    local timeToAnimate = math.abs(currentDrivenObjectFrame - newDrivenObjectFrame) / drivenObject.AnimLengthFrames * totalRewindTime
    local crankAnimRate = crankFramesToAnimate / timeToAnimate / fps
    crank:PlayAnimToFrame(newCrankFrame, crankAnimRate)
    crank:OnAnimationDone(crank, "PlayNextCycle")
  else
    local currentCrankFrame
    if currCycle % animCyclesPerLoop == 0 then
      currentCrankFrame = 0
      if not CrankIsLocked() then
        crank:JumpAnimToFrame(currentCrankFrame or 0)
      end
    else
      currentCrankFrame = currCycle % animCyclesPerLoop * (crank.AnimLengthFrames / animCyclesPerLoop)
      if not CrankIsLocked() then
        crank:JumpAnimToFrame(currentCrankFrame or 0)
      end
    end
    local newCrankFrame = crank.AnimFrame + totalCyclesRemaining * (crank.AnimLengthFrames / animCyclesPerLoop)
    local crankFramesToAnimate = totalCyclesRemaining * (crank.AnimLengthFrames / animCyclesPerLoop)
    local currentDrivenObjectFrame = cycleStopFrames[currCycle] or 0
    local newDrivenObjectFrame = cycleStopFrames[newCycle] or 0
    local timeToAnimate = math.abs(currentDrivenObjectFrame - newDrivenObjectFrame) / drivenObject.AnimLengthFrames * totalRewindTime
    local crankAnimRate = crankFramesToAnimate / timeToAnimate / fps
    crank:PlayAnimToFrame(newCrankFrame, crankAnimRate)
    crank:OnAnimationDone(crank, "CrankAnimComplete")
  end
end
function CrankAnimComplete()
  StopAllLoopingSounds(crankMotion)
  crank:ClearAllAnimCallbacks()
end
function HandleAnimationForLockOrReEnter()
  crank:ClearAllAnimCallbacks()
  game.FX.StopEffect({EffectName = rewindFeedback})
  if crankDetachedBehavior == "stationary" or crankDetachedBehavior == "rewind" then
    if CrankIsDrivingAnObject() and CrankIsMoving() then
      local timeRemaining, _, cycle
      local animRate = 1
      if DrivenObjectMotionIsReversed() then
        _, cycle = GetNearestForwardStopFrame()
        animRate = -1
      else
        _, cycle = GetNearestBackwardStopFrame()
      end
      if DrivenObjectAtValidStopPosition() then
        drivenObject:PauseAnim()
        StopRewindSound()
        StopRewindSound_AnimSynchedCranks()
      else
        timeRemaining = StartDrivenObjectAnimation(cycle, -rewindRate * animRate)
      end
      currentCycle = cycle
      crank:StartAnim(GetCurrentIdleAnim(), timeRemaining)
    else
      if CrankIsDrivingAnObject() then
        drivenObject:PauseAnim()
      end
      crank:StartAnim(GetCurrentIdleAnim())
      StopRewindSound()
      StopRewindSound_AnimSynchedCranks()
    end
  elseif crankDetachedBehavior == "fast forward" then
    if CrankIsDrivingAnObject() and CrankIsMoving() then
      local timeRemaining, _, cycle
      local animRate = 1
      if DrivenObjectMotionIsReversed() then
        _, cycle = GetNearestForwardStopFrame()
        animRate = -1
      else
        _, cycle = GetNearestForwardStopFrame()
      end
      if DrivenObjectAtValidStopPosition() then
        drivenObject:PauseAnim()
        StopFastForwardSound()
        StopFastForwardSound_AnimSynchedCranks()
      else
        timeRemaining = StartDrivenObjectAnimation(cycle, rewindRate * animRate)
      end
      currentCycle = cycle
      crank:StartAnim(GetCurrentIdleAnim(), timeRemaining)
    else
      if CrankIsDrivingAnObject() then
        drivenObject:PauseAnim()
      end
      crank:StartAnim(GetCurrentIdleAnim())
      StopFastForwardSound()
      StopFastForwardSound_AnimSynchedCranks()
    end
  end
end
function HandleAnimationForDetachOrUnlock()
  crank:ClearAllAnimCallbacks()
  if crankMotion == "forward" or crankMotion == "backward" then
    StopForwardBackwardLoops()
  end
  local attachedToAnimSynchedCrank = PlayerIsAttached_AnimSynchedCranks()
  if PlayerIsAttached() == false and attachedToAnimSynchedCrank == false then
    if state == "locked" or crankDetachedBehavior == "stationary" then
      if CrankIsDrivingAnObject() then
        if DrivenObjectAtValidStopPosition() == false and animStarted then
          local cycle
          local fFrame, fCycle = GetNearestForwardStopFrame()
          local bFrame, bCycle = GetNearestBackwardStopFrame()
          local animRate = 1
          if math.abs(drivenObject.AnimFrame - fFrame) < math.abs(drivenObject.AnimFrame - bFrame) then
            cycle = fCycle
            SetCrankMotion("fast forward")
          else
            animRate = -1
            cycle = bCycle
            SetCrankMotion("rewind")
          end
          currentCycle = cycle
          StartDrivenObjectAnimation(cycle, rewindRate * animRate)
        else
          IdleCrank()
        end
      end
    elseif crankDetachedBehavior == "rewind_pause positions" then
      if CrankIsDrivingAnObject() then
        if DrivenObjectAtValidPausePosition() == false and animStarted == false and CurrentCycleIsValidPauseCycle() == false then
          SetCrankMotion("rewind")
          local lastCycle = currentCycle
          local currentCycleFraction = currentCycle % animCyclesPerLoop / animCyclesPerLoop
          local _, cycle
          local animRate = 1
          if DrivenObjectMotionIsReversed() then
            _, cycle = GetNearestForwardPausePosition()
            animRate = -1
          else
            _, cycle = GetNearestBackwardPausePosition()
          end
          StartDrivenObjectAnimation(cycle, -rewindRate * animRate)
          currentCycle = cycle
          StartCrankAnim(lastCycle, currentCycle)
          PlayRewindSound()
          ExecuteLuaCallback("onRewindEvent", "Driven object rewinding")
        else
          IdleCrank()
        end
      end
    elseif crankDetachedBehavior == "fast forward_pause positions" then
      if CrankIsDrivingAnObject() then
        if DrivenObjectAtValidPausePosition() == false and animStarted == false and CurrentCycleIsValidPauseCycle() == false then
          SetCrankMotion("fast forward")
          local lastCycle = currentCycle
          local currentCycleFraction = currentCycle % animCyclesPerLoop / animCyclesPerLoop
          local _, cycle
          local animRate = 1
          if DrivenObjectMotionIsReversed() then
            _, cycle = GetNearestBackwardPausePosition()
            animRate = -1
          else
            _, cycle = GetNearestForwardPausePosition()
          end
          StartDrivenObjectAnimation(cycle, rewindRate * animRate)
          currentCycle = cycle
          StartCrankAnim(lastCycle, currentCycle)
          PlayFastForwardSound()
          ExecuteLuaCallback("onFastForwardEvent", "Driven object fast forwarding")
        else
          IdleCrank()
        end
      end
    elseif crankDetachedBehavior == "rewind" then
      if CrankIsDrivingAnObject() then
        if drivenObject.AnimFrame > minDrivenObjectFrame and animStarted == false and currentCycle ~= minStopCycle then
          SetCrankMotion("rewind")
          local lastCycle = currentCycle
          local animRate = 1
          if DrivenObjectMotionIsReversed() then
            animRate = -1
            currentCycle = maxStopCycle
          else
            currentCycle = minStopCycle or 0
          end
          StartCrankAnim(lastCycle, currentCycle)
          local drivenObjectFrame = cycleStopFrames[currentCycle] or 0
          StartDrivenObjectAnimation(currentCycle, -rewindRate * animRate)
          PlayRewindSound()
          ExecuteLuaCallback("onRewindEvent", "Driven object rewinding")
        else
          IdleCrank()
        end
      end
    elseif crankDetachedBehavior == "fast forward" and CrankIsDrivingAnObject() then
      if drivenObject.AnimFrame < maxDrivenObjectFrame and animStarted == false and currentCycle ~= maxStopCycle then
        SetCrankMotion("fast forward")
        local lastCycle = currentCycle
        local animRate = 1
        if DrivenObjectMotionIsReversed() then
          animRate = -1
          currentCycle = minStopCycle
        else
          currentCycle = maxStopCycle
        end
        StartCrankAnim(lastCycle, currentCycle)
        local drivenObjectFrame = cycleStopFrames[currentCycle]
        StartDrivenObjectAnimation(currentCycle, rewindRate * animRate)
        PlayFastForwardSound()
        ExecuteLuaCallback("onFastForwardEvent", "Driven object fast forwarding")
      else
        IdleCrank()
      end
    end
  end
end
function IdleCrank()
  crankMotion = "idle"
  crank:StartAnim(GetCurrentIdleAnim())
  CleanupDrivenObjectAnim()
end
function GetCurrentIdleAnim()
  if crankTweakName == "Cal100BridgePush" then
    return "envCal100BridgeIdle"
  elseif crankType == "Line" then
    return "envStraightCrankIdle00"
  else
    return "env" .. crankTweakName .. "Idle0" .. currentCycle % animCyclesPerLoop
  end
end
function GetAnimRateFromCrankFramesData(direction)
  if direction == "Down" then
    local crankFramesRemaining = crank.AnimLengthFrames / animCyclesPerLoop
    local diff = math.abs(crank.AnimFrame - startAnimFrame)
    if 0 < diff then
      crankFramesRemaining = crankFramesRemaining - diff
    end
    local currentCycleStopFrame = cycleStopFrames[currentCycle] or 0
    local myFramesRemaining = math.abs(drivenObject.AnimFrame - currentCycleStopFrame)
    return myFramesRemaining / crankFramesRemaining
  elseif direction == "Up" then
    local crankFramesRemaining = crank.AnimLengthFrames / animCyclesPerLoop
    local diff = math.abs(crank.AnimFrame - startAnimFrame)
    if 0 < diff then
      crankFramesRemaining = crankFramesRemaining - diff
    end
    local currentCycleStopFrame = cycleStopFrames[currentCycle] or 0
    local myFramesRemaining = math.abs(drivenObject.AnimFrame - currentCycleStopFrame)
    return myFramesRemaining / crankFramesRemaining
  end
end
function DrivenObjectMotionIsReversed()
  return driveObjectInReverse
end
function DrivenObjectAtValidPausePosition()
  if crankDetachedBehavior == "rewind" or crankDetachedBehavior == "rewind_pause positions" then
    if drivenObject.AnimFrame == 0 then
      return true
    end
  elseif (crankDetachedBehavior == "fast forward" or crankDetachedBehavior == "fast forward_pause positions") and drivenObject.AnimFrame == drivenObject.AnimLengthFrames then
    return true
  end
  for _, frame in pairs(pausePositions) do
    if drivenObject.AnimFrame == frame then
      return true
    end
  end
  return false
end
function CurrentCycleIsValidPauseCycle()
  if currentCycle % (numCycles / numPausePositions) == 0 then
    return true
  else
    return false
  end
end
function CrankAtValidStopFrame()
  if crank.AnimFrame == 0 then
    return true
  end
  if crank.AnimFrame % (crank.AnimLengthFrames / animCyclesPerLoop) == 0 then
    return true
  end
  return false
end
function DrivenObjectAtValidStopPosition()
  if drivenObject.AnimFrame == 0 then
    return true
  end
  for i = 1, #cycleStopFrames do
    if drivenObject.AnimFrame == cycleStopFrames[i] then
      return true
    end
  end
  return false
end
function GetNearestForwardStopFrame()
  if CrankIsDrivingAnObject() then
    local currentFrame = drivenObject.AnimFrame
    if DrivenObjectMotionIsReversed() and currentFrame == 0 then
      return 0, 0
    end
    for i = 1, #cycleStopFrames do
      if currentFrame <= cycleStopFrames[i] then
        local newFrame = cycleStopFrames[i]
        local newCycle = GetCycleFromFrameValue(newFrame)
        return newFrame, newCycle
      end
    end
  end
end
function GetNearestBackwardStopFrame()
  if CrankIsDrivingAnObject() then
    local currentFrame = drivenObject.AnimFrame
    for i = #cycleStopFrames, 1, -1 do
      if currentFrame >= cycleStopFrames[i] then
        local newFrame = cycleStopFrames[i]
        local newCycle = GetCycleFromFrameValue(newFrame)
        return newFrame, newCycle
      end
    end
    return 0, 0
  end
end
function GetNearestForwardPausePosition()
  if CrankIsDrivingAnObject() then
    local currentFrame = drivenObject.AnimFrame
    for i = 1, #pausePositions do
      if currentFrame <= pausePositions[i] then
        local newFrame = pausePositions[i]
        local newCycle = GetCycleFromFrameValue(newFrame)
        return newFrame, newCycle
      end
    end
  end
end
function GetNearestBackwardPausePosition()
  if CrankIsDrivingAnObject() then
    local currentFrame = drivenObject.AnimFrame
    for i = #pausePositions, 1, -1 do
      if currentFrame >= pausePositions[i] then
        local newFrame = pausePositions[i]
        local newCycle = GetCycleFromFrameValue(newFrame)
        return newFrame, newCycle
      end
    end
    return 0, 0
  end
end
function GetCycleFromFrameValue(frameVal)
  if frameVal == 0 then
    return 0
  end
  for i = 1, #cycleStopFrames do
    if frameVal == cycleStopFrames[i] then
      return i
    end
  end
end
function RecalculateStopFrames()
  if CrankIsDrivingAnObject() then
    drivenObjFrameChangePerInterval = drivenObject.AnimLengthFrames / numCycles
    maxDrivenObjectFrame = drivenObject.AnimLengthFrames
    cycleStopFrames = {}
    for i = 1, numCycles do
      cycleStopFrames[i] = math.ceil(i * drivenObjFrameChangePerInterval)
    end
    cycleStopFrames[#cycleStopFrames] = drivenObject.AnimLengthFrames
    if (crankDetachedBehavior == "rewind_pause positions" or crankDetachedBehavior == "fast forward_pause positions") and 0 < numPausePositions then
      DeterminePausePositions()
    end
    rewindRate = drivenObject.AnimLengthFrames / fps * (1 / totalRewindTime)
  end
end
function SetEaseEnabled(val)
  easingEnabled = val
end
function SetTotalCycles(numPulls)
  numCycles = numPulls
  maxStopCycle = numPulls
  RecalculateStopFrames()
end
function SetMaxStopCycle(maxCycle)
  maxStopCycle = maxCycle
  maxDrivenObjectFrame = cycleStopFrames[maxStopCycle] or 0
  CheckCrankStateForMoveEvent()
end
function SetMinStopCycle(minCycle)
  minStopCycle = minCycle
  minDrivenObjectFrame = cycleStopFrames[minStopCycle] or 0
  CheckCrankStateForMoveEvent()
end
function SetRewindTime(time)
  totalRewindTime = time
  rewindRate = drivenObject.AnimLengthFrames / fps * (1 / time)
end
function SetLockWhenFullyCranked(val)
  lockWhenFullyCranked = val
end
function SetDrivenObject(newObjectName)
  if type(newObjectName) == "string" then
    if newObjectName == "" then
      goto lbl_65
    end
    local newObj = crank
    if newObjectName == "_Parent" then
      if crank.Parent ~= nil and crank.Parent.IsRefNode then
        newObj = crank.Parent
      end
      if newObj.Parent ~= nil then
        drivenObject = newObj.Parent
        RecalculateStopFrames()
      else
        engine.Warning("Can't set parent as driven object for crank '" .. GetCrankName() .. "' because it has no parent object")
      end
    elseif newObjectName == "_Grandparent" then
      local parentCounter = 0
      while newObj.Parent ~= nil do
        newObj = newObj.Parent
        if not newObj.IsRefNode then
          parentCounter = parentCounter + 1
        end
        if parentCounter == 2 then
          break
        end
      end
      drivenObject = newObj
      RecalculateStopFrames()
    else
      drivenObject = thisLevel:GetGameObject(newObjectName)
      RecalculateStopFrames()
    end
  else
    drivenObject = newObjectName
    RecalculateStopFrames()
  end
  ::lbl_65::
end
function ClearDrivenObject()
  drivenObject = nil
end
function SetDetachedBehavior_Stationary()
  SetDetachedBehavior("stationary")
end
function SetDetachedBehavior_Rewind()
  SetDetachedBehavior("rewind")
end
function SetDetachedBehavior_FastForward()
  SetDetachedBehavior("fast forward")
end
function SetDetachedBehavior_Rewind_PausePositions()
  SetDetachedBehavior("rewind_pause positions")
  DeterminePausePositions()
end
function SetDetachedBehavior_FastForward_PausePositions()
  SetDetachedBehavior("fast forward_pause positions")
  DeterminePausePositions()
end
function DeterminePausePositions()
  for i = 1, numPausePositions do
    if numCycles % numPausePositions == 0 then
      local increment = numCycles / numPausePositions
      pausePositions[#pausePositions + 1] = math.ceil(i * increment * drivenObjFrameChangePerInterval)
    else
      engine.Error("numCycles must be evenly divisible by numPausePositions in the toolbox for crank: ", GetCrankName())
    end
  end
end
function SetDetachedBehavior(newDetachedBehavior)
  if newDetachedBehavior == "stationary" or newDetachedBehavior == "rewind" or newDetachedBehavior == "fast forward" or newDetachedBehavior == "fast forward_pause positions" or newDetachedBehavior == "rewind_pause positions" then
    crankDetachedBehavior = newDetachedBehavior
    if not CrankIsLocked() then
      HandleAnimationForDetachOrUnlock()
      HandleAnimationForDetachOrUnlock_AnimSynchedCranks()
    end
  else
    engine.Warning("Crank behavior on " .. crank:GetName() .. " cannot be set to " .. tostring(newDetachedBehavior) .. ". Options are 'stationary,' 'fast forward,' and 'rewind'")
  end
end
function RemoveAnimSynchedObject(obj)
  if drivenObject ~= nil and obj == drivenObject then
    ClearDrivenObject()
    return
  end
  if type(obj) == "string" then
    game.GameObject.AnimSync(GameObjects[obj])
  else
    game.GameObject.AnimSync(obj)
  end
end
function SetAnimSynchedObjects(synchedObjects)
  if synchedObjects ~= nil then
    local objNameTable = LD.ConvertStringListToTable(synchedObjects)
    for _, objName in ipairs(objNameTable) do
      local obj = GameObjects[objName]
      if obj ~= nil then
        if obj.IsRefNode then
          obj = obj.Child
        end
        if obj.AnimFrame ~= nil then
          game.GameObject.AnimSync(obj, drivenObject)
        else
          engine.Warning("Can't anim synch object '" .. obj:GetName() .. "' to driven object '" .. drivenObject:GetName() .. [[
' because
 ]] .. obj:GetName() .. " has no animation.  Add animation keys or remove from the anim synched objects list on crank '" .. GetCrankName() .. "' in level '" .. thisLevel.Name .. "'")
        end
      else
        engine.Warning("Can't find object '" .. objName .. "' to anim synch to driven object '" .. drivenObject:GetName() .. "' on crank " .. GetCrankName() .. "' in level '" .. thisLevel.Name .. "'.  Please make sure '" .. objName .. "' is a valid game object")
      end
    end
  end
end
function SetGearObjects(gearObjNames)
  gearObjectNames = gearObjNames
  if gearObjectNames ~= nil then
    local objNameTable = LD.ConvertStringListToTable(gearObjectNames)
    for _, objName in ipairs(objNameTable) do
      local obj = GameObjects[objName]
      if obj ~= nil then
        if obj:FindSingleGOByName("WallGears_root") ~= nil then
          obj = obj:FindSingleGOByName("WallGears_root")
        end
        if obj.IsRefNode then
          game.GameObject.AnimSync(obj.Child, drivenObject)
        else
          game.GameObject.AnimSync(obj, drivenObject)
        end
        if obj.LuaObjectScript ~= nil then
          obj.LuaObjectScript.RegisterEmbedCallback(Lock)
          obj.LuaObjectScript.RegisterUnembedCallback(Unlock)
        else
          engine.Warning("Can't use lock object for '" .. obj:GetName() .. "' to lock '" .. drivenObject:GetName() .. [[
' because
 ]] .. obj:GetName() .. " has no lua script to setup embed/unembed callbacks.  Make sure the object has the correct script to be used with '" .. GetCrankName() .. "' in level '" .. thisLevel.Name .. "'")
        end
      else
        engine.Warning("Can't find object '" .. objName .. "' to as lock object for '" .. drivenObject:GetName() .. "' on crank " .. GetCrankName() .. "' in level '" .. thisLevel.Name .. "'.  Please make sure '" .. objName .. "' is a valid game object")
      end
    end
  end
end
function SetAnimSynchedCrank(crankObject)
  if crankObject.IsRefNode then
    if 0 < #animSynchedCrankObjects then
      for i = 1, #animSynchedCrankObjects do
        if crankObject.Child == animSynchedCrankObjects[i] then
          return
        end
      end
    end
    animSynchedCrankObjects[#animSynchedCrankObjects + 1] = crankObject.Child
  else
    if 0 < #animSynchedCrankObjects then
      for i = 1, #animSynchedCrankObjects do
        if crankObject == animSynchedCrankObjects[i] then
          return
        end
      end
    end
    animSynchedCrankObjects[#animSynchedCrankObjects + 1] = crankObject
  end
end
function AutoSetAnimSynchedCranksInHierarchy()
  if crank.Parent ~= nil and crank.Parent.Parent ~= nil then
    local grandParent = crank.Parent.Parent
    if grandParent.LuaObjectScript ~= nil and grandParent.LuaObjectScript.ObjectIsCrank ~= nil then
      SetAnimSynchedCrank(grandParent)
    end
    for _, c in pairs(crank.Children) do
      if c.LuaObjectScript ~= nil and c.LuaObjectScript.ObjectIsCrank ~= nil and c ~= crank then
        SetAnimSynchedCrank(c)
      end
    end
    for _, c in pairs(grandParent.Children) do
      if c.LuaObjectScript ~= nil and c.LuaObjectScript.ObjectIsCrank ~= nil and c.Child ~= crank then
        SetAnimSynchedCrank(c)
      end
    end
  end
end
function DecrementCurrentCycle_AnimSynchedCranks()
  if 0 < #animSynchedCrankObjects then
    for _, crank in pairs(animSynchedCrankObjects) do
      crank.LuaObjectScript.DecrementCurrentCycle()
    end
  end
end
function IncrementCurrentCycle_AnimSynchedCranks()
  if 0 < #animSynchedCrankObjects then
    for _, crank in pairs(animSynchedCrankObjects) do
      crank.LuaObjectScript.IncrementCurrentCycle()
    end
  end
end
function HandleAnimationForDetachOrUnlock_AnimSynchedCranks()
  if 0 < #animSynchedCrankObjects then
    for _, crank in pairs(animSynchedCrankObjects) do
      crank.LuaObjectScript.HandleAnimationForDetachOrUnlock()
    end
  end
end
function PlayerIsAttached_AnimSynchedCranks()
  if 0 < #animSynchedCrankObjects then
    for _, crank in pairs(animSynchedCrankObjects) do
      if crank.LuaObjectScript.PlayerIsAttached() then
        return true
      end
    end
  end
  return false
end
function SetCurrentCycle_AnimSynchedCranks(newCycle)
  if 0 < #animSynchedCrankObjects then
    for _, crank in pairs(animSynchedCrankObjects) do
      crank.LuaObjectScript.SetCurrentCycle(newCycle)
    end
  end
end
function GetEaseTimeFromCrankType()
  if crankTweakName == "WheelCrank" then
    return 0.45
  elseif crankTweakName == "ChainPulley" then
    return 0.4
  elseif crankTweakName == "StraightCrank" then
    return 1.05
  elseif crankTweakName == "StraightCrankSideways" then
    return 0.8
  elseif crankTweakName == "Cal100BridgePush" then
    return 1
  else
    return 1
  end
end
function SetCrankMotion(motion)
  local mCrankMotion = motion
  if string.lower(crankType) == "line" then
    if motion == "backward" then
      mCrankMotion = "forward"
    elseif motion == "forward" then
      mCrankMotion = "backward"
    end
  end
  crankMotion = mCrankMotion
end
function GetMaxStopCycle()
  return maxStopCycle
end
function GetMinStopCycle()
  return minStopCycle
end
function SetCurrentCycle(val)
  currentCycle = val
end
function GetCurrentCycle()
  return currentCycle
end
function IncrementCurrentCycle()
  currentCycle = currentCycle + 1
end
function DecrementCurrentCycle()
  currentCycle = currentCycle - 1
end
function CrankIsLocked()
  if state == "locked" then
    return true
  end
  return false
end
function CrankIsEnabled()
  if state == "enabled" then
    return true
  end
  return false
end
function CrankIsSolved()
  if state == "solved" then
    return true
  end
  return false
end
function GetDrivenObjectAnimPercentage()
  if drivenObject ~= nil then
    return drivenObject.AnimPercent
  end
  return nil
end
function CrankIsDrivingAnObject()
  if drivenObject == nil then
    return false
  end
  return true
end
function DrivenObjectHasAnimation()
  if CrankIsDrivingAnObject() and drivenObject.AnimFrame == nil then
    return false
  end
  return true
end
function ObjectIsCrank()
  return true
end
function GetCrankName()
  if crank.Parent ~= nil and crank.Parent.IsRefNode then
    return crank.Parent:GetName()
  end
  return crank:GetName()
end
function PlayerIsAttached()
  return usingThisCrank
end
function CrankIsMoving()
  if crankMotion ~= "idle" then
    return true
  else
    return false
  end
end
function PlayerIsCranking()
  if PlayerIsAttached() then
    if crankMotion ~= "idle" then
      return true
    else
      return false
    end
  end
end
function GetTableAverage(T)
  if 0 < #T then
    local t = 0
    for _, k in pairs(T) do
      t = t + k
    end
    return t / #T
  end
end
function OverrideCameraInteractApproach(strAproachCamera)
  camApproachCamera = strAproachCamera
end
function OverrideDefaultCameraRecenter(bVal, recenterOverride)
  if bVal == true then
    bUseDefaultRecenter = false
    if recenterOverride ~= nil then
      cameraRecenterOverride = recenterOverride
    end
  end
end
function OverrideDefaultCameraYaw(bVal, overrideYaw)
  if bVal == true and overrideYaw ~= nil then
    cameraOverrideYaw = overrideYaw
  end
end
function OverrideDefaultCameraPitch(bVal, overridePitch)
  if bVal == true and overridePitch ~= nil then
    cameraOverridePitch = overridePitch
  end
end
function DestroyApproachCamera()
  if camOneShotApproachObject ~= nil then
    camOneShotApproachObject = nil
  end
end
function AssignCamera(inputCamera)
  cameraType = inputCamera
end
function SubmitCrankCamera(cameraType, attached)
  if attached == true then
    if crankType == "Line" then
      if cameraType == "Left Justified" then
        game.Camera.SubmitCameraByName("PLYR_Crank_Wheel_L")
      elseif cameraType == "Right Justified" then
        game.Camera.SubmitCameraByName("PLYR_Crank_Wheel_R")
      elseif cameraType == "Centered" then
        game.Camera.SubmitCameraByName("PLYR_Crank_Wheel_C")
      elseif cameraType ~= nil then
        if string.len(cameraType) > 0 then
          game.Camera.SubmitCameraByName(cameraType)
        else
          game.Camera.SubmitCameraByName("PLYR_Crank_Wheel_L")
        end
      else
        game.Camera.SubmitCameraByName("PLYR_Crank_Wheel_L")
      end
      return
    elseif crankType == "Wheel" then
      if cameraType == "Left Justified" then
        game.Camera.SubmitCameraByName("PLYR_Crank_Wheel_L")
      elseif cameraType == "Right Justified" then
        game.Camera.SubmitCameraByName("PLYR_Crank_Wheel_R")
      elseif cameraType == "Centered" then
        game.Camera.SubmitCameraByName("PLYR_Crank_Wheel_C")
      elseif cameraType ~= nil then
        if string.len(cameraType) > 0 then
          game.Camera.SubmitCameraByName(cameraType)
        else
          return
        end
      else
        return
      end
    elseif crankType == "Chain" then
      if cameraType == "Left Justified" then
        game.Camera.SubmitCameraByName("PLYR_Crank_Chain_L")
      elseif cameraType == "Right Justified" then
        game.Camera.SubmitCameraByName("PLYR_Crank_Chain_R")
      elseif cameraType == "Centered" then
        game.Camera.SubmitCameraByName("PLYR_Crank_Chain_C")
      elseif cameraType ~= nil then
        if string.len(cameraType) > 0 then
          game.Camera.SubmitCameraByName(cameraType)
        else
          return
        end
      else
        return
      end
    end
  end
end
local drivenObjectEmitter, crankEmitter, AnimGO, crankSoundAnimFrameMonitor, drivenObjectSoundAnimFrameMonitor
local lastFrameAngVel = 0
local currentFrameAngVel = 0
local angularVelocity = 0
local sound_isDrivenObjectPaused = false
local crankSound = {
  OnAttach = "",
  OnAttachFrame = -1,
  OnDetach = "",
  OnDetachFrame = -1,
  OnStart = "",
  OnStartFrame = -1,
  OnReturnToStart = "",
  OnReturnToStartFrame = -1,
  OnForward = "",
  OnForwardFrame = -1,
  OnForwardLoop = "",
  OnForwardLoopFrame = -1,
  OnFirstForward = "",
  OnFirstForwardFrame = -1,
  OnBackward = "",
  OnBackwardFrame = -1,
  OnBackwardLoop = "",
  OnBackwardLoopFrame = -1,
  OnFirstBackward = "",
  OnFirstBackwardFrame = -1,
  OnFastForward = "",
  OnFastForwardFrame = -1,
  OnRewind = "",
  OnRewindFrame = -1,
  OnStartFromEnd = "",
  OnStartFromEndFrame = -1,
  OnEnd = "",
  OnEndFrame = -1,
  OnStuckAtEnd = "",
  OnStuckAtEndFrame = -1,
  OnStuckAtStart = "",
  OnStuckAtStartFrame = -1,
  OnJammed = "",
  OnJammedFrame = -1,
  OnStuckWhileJammed = "",
  OnStuckWhileJammedFrame = -1,
  OnUnlocked = "",
  OnUnlockedFrame = -1,
  OnLocked = "",
  OnLockedFrame = -1,
  OnStuckWhileLocked = "",
  OnStuckWhileLockedFrame = -1,
  OnStop = "",
  OnStopAnimFrame = -1
}
local drivenObjectSound = {
  UseExplicitSoundMotion = false,
  UseInvertedEndPoints = false,
  OnAttach = "",
  OnAttachFrame = -1,
  OnDetach = "",
  OnDetachFrame = -1,
  OnStart = "",
  OnStartFrame = -1,
  OnReturnToStart = "",
  OnReturnToStartFrame = -1,
  OnForward = "",
  OnForwardFrame = -1,
  OnBackward = "",
  OnBackwardFrame = -1,
  OnFastForward = "",
  OnFastForwardFrame = -1,
  OnRewind = "",
  OnRewindFrame = -1,
  OnStartFromEnd = "",
  OnStartFromEndFrame = -1,
  OnEnd = "",
  OnEndFrame = -1,
  OnStuckAtEnd = "",
  OnStuckAtEndFrame = -1,
  OnStuckAtStart = "",
  OnStuckAtStartFrame = -1,
  OnJammed = "",
  OnJammedFrame = -1,
  OnStuckWhileJammed = "",
  OnStuckWhileJammedFrame = -1,
  OnUnlocked = "",
  OnUnlockedFrame = -1,
  OnLocked = "",
  OnLockedFrame = -1,
  OnStuckWhileLocked = "",
  OnStuckWhileLockedFrame = -1
}
local wheelCrankSound = {
  OnStart = "SND_MECH_Crank_Wheel_Metal_Stop_On_Beginning",
  OnReturnToStart = "SND_MECH_Crank_Wheel_Metal_Return_To_Start",
  OnForward = "SND_MECH_Crank_Wheel_Metal_Spin_Cont_Forward",
  OnForwardLoop = "SND_MECH_Crank_Wheel_Metal_Spin_Forward_LP",
  OnFirstForward = "SND_MECH_Crank_Wheel_Metal_Spin_Start_Forward",
  OnBackward = "SND_MECH_Crank_Wheel_Metal_Spin_Cont_Backward",
  OnBackwardLoop = "SND_MECH_Crank_Wheel_Metal_Spin_Backward_LP",
  OnFirstBackward = "SND_MECH_Crank_Wheel_Metal_Spin_Start_Backward",
  OnFastForward = "SND_MECH_Crank_Wheel_Metal_Release_Fast_Forward_LP",
  OnRewind = "SND_MECH_Crank_Wheel_Metal_Release_Rewind_LP",
  OnStartFromEnd = "SND_MECH_Crank_Wheel_Metal_Start_From_Beginning",
  OnEnd = "SND_MECH_Crank_Wheel_Metal_Stop_On_End",
  OnStuckAtEnd = "SND_MECH_Crank_Wheel_Metal_Stuck_At_End",
  OnStuckAtStart = "SND_MECH_Crank_Wheel_Metal_Stuck_At_Beginning",
  OnUnlocked = "SND_MECH_Crank_Wheel_Metal_On_Unlocked",
  OnLocked = "SND_MECH_Crank_Wheel_Metal_On_Locked"
}
local chainCrankSound = {
  OnForward = "SND_MECH_Pulley_Chain_Chains",
  OnBackward = "SND_MECH_Pulley_Chain_Chains",
  OnFastForward = "SND_MECH_Pulley_Chain_Moving_LP",
  OnRewind = "SND_MECH_Pulley_Chain_Moving_LP",
  OnStop = "SND_MECH_Pulley_Chain_Retract",
  OnStuckAtEnd = "SND_MECH_Pulley_Chain_Hit_Ceiling",
  OnStuckAtStart = "SND_MECH_Pulley_Chain_Hit_Ground"
}
function SoundInitCrank()
  lastFrameAngVel = engine.Vector.New(-0.707, 0, -0.707)
  currentFrameAngVel = lastFrameAngVel
  crankEmitter = crank:FindSingleSoundEmitterByName("SNDBasicCrankObject")
  if crankType == "Wheel" then
    CrankSoundSetup(wheelCrankSound)
  elseif crankType == "Chain" then
    CrankSoundSetup(chainCrankSound)
  end
end
function SoundInitDrivenObject()
  if drivenObject ~= nil then
    local objectName = string.gsub(string.gsub(tostring(drivenObject), "'", ""), "GameObject go", "")
    drivenObjectEmitter = drivenObject:FindSingleSoundEmitterByName("SND" .. objectName)
  end
  if DrivenObjectHasAnimation() then
    AnimGO = drivenObject or crank
  end
  SetUpSoundMonitors()
end
function CrankSoundSetup(sounds)
  if sounds ~= nil then
    if sounds.SoundEmitter ~= nil then
      crankEmitter = crank:FindSingleSoundEmitterByName(sounds.SoundEmitter)
    end
    for key, value in pairs(crankSound) do
      for newKey, newValue in pairs(sounds) do
        if newKey == key and newValue ~= nil and newValue ~= value then
          crankSound[key] = newValue
        end
      end
      LD.SoundDebug(tostring(key) .. ": " .. tostring(crankSound[key]))
    end
    SetUpSoundMonitors()
  end
end
function DrivenObjectSoundSetup(sounds)
  if sounds ~= nil then
    if sounds.SoundEmitter ~= nil then
      drivenObjectEmitter = sounds.SoundEmitter
    end
    for key, value in pairs(drivenObjectSound) do
      for newKey, newValue in pairs(sounds) do
        if newKey == key and newValue ~= nil and newValue ~= value then
          drivenObjectSound[key] = newValue
        end
      end
      LD.SoundDebug(tostring(key) .. ": " .. tostring(drivenObjectSound[key]))
    end
    if sounds.UseExplicitSoundMotion then
      sound_isDrivenObjectPaused = true
    end
    SetUpSoundMonitors()
  end
end
function PlayCrankMotionSound()
  if crankMotion == "forward" then
    print("crank forward")
    PlayForwardSound()
  elseif crankMotion == "backward" then
    print("crank backward")
    PlayBackwardSound()
  elseif crankMotion == "fast forward" then
    print("crank f.forward")
    PlayFastForwardSound()
  elseif crankMotion == "rewind" then
    print("crank rewind")
    PlayRewindSound()
  end
end
function StopAllLoopingSounds(motion)
  print(motion)
  if motion == "forward" then
    StopForwardSound()
    PlayCrankOnStopSound()
  end
  if motion == "backward" then
    StopBackwardSound()
    PlayCrankOnStopSound()
  end
  if motion == "fast forward" then
    StopFastForwardSound()
    StopFastForwardSound_AnimSynchedCranks()
    PlayCrankOnStopSound()
  end
  if motion == "rewind" then
    StopRewindSound()
    StopRewindSound_AnimSynchedCranks()
    PlayCrankOnStopSound()
  end
end
function StopForwardBackwardLoops()
  StopBackwardSound()
  StopForwardSound()
end
function PlayOnStartSound()
  if AnimGO.AnimFrame < 3 then
    LD.PlaySoundOnFrame(crankEmitter, AnimGO, crankSound.OnStart, crankSound.OnStartFrame, "forward")
  end
  if not drivenObjectSound.UseInvertedEndPoints then
    if AnimGO.AnimFrame < 3 then
      LD.PlaySoundOnFrame(drivenObjectEmitter, AnimGO, drivenObjectSound.OnStart, drivenObjectSound.OnStartFrame, "forward")
    end
  elseif AnimGO.AnimFrame > AnimGO.AnimLengthFrames - 3 then
    LD.PlaySoundOnFrame(drivenObjectEmitter, AnimGO, drivenObjectSound.OnStartFromEnd, drivenObjectSound.OnStartFromEndFrame, "backward")
  end
end
function PlayOnStartFromEndSound()
  if AnimGO.AnimFrame > AnimGO.AnimLengthFrames - 3 then
    LD.PlaySoundOnFrame(crankEmitter, AnimGO, crankSound.OnStartFromEnd, crankSound.OnStartFromEndFrame, "backward")
  end
  if not drivenObjectSound.UseInvertedEndPoints then
    if AnimGO.AnimFrame > AnimGO.AnimLengthFrames - 3 then
      LD.PlaySoundOnFrame(drivenObjectEmitter, AnimGO, drivenObjectSound.OnStartFromEnd, drivenObjectSound.OnStartFromEndFrame, "backward")
    end
  elseif AnimGO.AnimFrame < 3 then
    LD.PlaySoundOnFrame(drivenObjectEmitter, AnimGO, drivenObjectSound.OnStart, drivenObjectSound.OnStartFrame, "forward")
  end
end
function PlayForwardSound()
  PlayOnStartSound()
  if crankMotion ~= "backward" and not IsEmpty(crankSound.OnFirstForward) then
    LD.PlaySoundOnFrame(crankEmitter, AnimGO, crankSound.OnFirstForward, crankSound.OnFirstForwardFrame, "forward")
    LD.PlaySoundOnFrame(crankEmitter, AnimGO, crankSound.OnForwardLoop, crankSound.OnForwardLoopFrame, "forward")
  else
    LD.PlaySoundOnFrame(crankEmitter, AnimGO, crankSound.OnForward, crankSound.OnForwardFrame, "forward")
  end
  if IsLoopingSound(drivenObjectSound.OnForward) and drivenObjectSound.UseExplicitSoundMotion then
    if sound_isDrivenObjectPaused then
      sound_isDrivenObjectPaused = false
      LD.PlaySoundOnFrame(drivenObjectEmitter, AnimGO, drivenObjectSound.OnForward, drivenObjectSound.OnForwardFrame, "forward", true)
    end
  else
    LD.PlaySoundOnFrame(drivenObjectEmitter, AnimGO, drivenObjectSound.OnForward, drivenObjectSound.OnForwardFrame, "forward")
  end
end
function StopForwardSound()
  StopSoundIfLooping(crankEmitter, crankSound.OnForward)
  StopSoundIfLooping(crankEmitter, crankSound.OnForwardLoop)
  StopSoundIfLooping(drivenObjectEmitter, drivenObjectSound.OnForward)
  if drivenObjectSound.UseExplicitSoundMotion then
    sound_isDrivenObjectPaused = true
  end
end
function PlayBackwardSound()
  PlayOnStartFromEndSound()
  if crankMotion ~= "forward" and not IsEmpty(crankSound.OnFirstBackward) then
    LD.PlaySoundOnFrame(crankEmitter, AnimGO, crankSound.OnFirstBackward, crankSound.OnFirstBackwardFrame, "forward")
    LD.PlaySoundOnFrame(crankEmitter, AnimGO, crankSound.OnBackwardLoop, crankSound.OnBackwardLoopFrame, "forward")
  else
    LD.PlaySoundOnFrame(crankEmitter, AnimGO, crankSound.OnBackward, crankSound.OnBackwardFrame, "backward")
  end
  if IsLoopingSound(drivenObjectSound.OnBackward) and drivenObjectSound.UseExplicitSoundMotion then
    if sound_isDrivenObjectPaused then
      sound_isDrivenObjectPaused = false
      LD.PlaySoundOnFrame(drivenObjectEmitter, AnimGO, drivenObjectSound.OnBackward, drivenObjectSound.OnBackwardFrame, "backward", true)
    end
  else
    LD.PlaySoundOnFrame(drivenObjectEmitter, AnimGO, drivenObjectSound.OnBackward, drivenObjectSound.OnBackwardFrame, "backward")
  end
end
function StopBackwardSound()
  StopSoundIfLooping(crankEmitter, crankSound.OnBackward)
  StopSoundIfLooping(crankEmitter, crankSound.OnBackwardLoop)
  StopSoundIfLooping(drivenObjectEmitter, drivenObjectSound.OnBackward)
  if drivenObjectSound.UseExplicitSoundMotion then
    sound_isDrivenObjectPaused = true
  end
end
function PlayCrankOnEndSound()
  if crankMotion == "forward" or crankMotion == "backward" then
    StopForwardBackwardLoops()
  end
  if crankType == "Line" then
    StopAllLoopingSounds()
  end
  LD.PlaySound(crankEmitter, crankSound.OnEnd)
end
function PlayDrivenObjectOnEndSound()
  LD.PlaySound(drivenObjectEmitter, drivenObjectSound.OnEnd)
  if drivenObjectSound.UseExplicitSoundMotion then
    sound_isDrivenObjectPaused = true
  end
end
function PlayCrankOnReturnToStartSound()
  if crankMotion == "forward" or crankMotion == "backward" then
    StopForwardBackwardLoops()
  end
  if crankType == "Line" then
    StopAllLoopingSounds()
  end
  LD.PlaySound(crankEmitter, crankSound.OnReturnToStart)
end
function PlayDrivenObjectOnReturnToStartSound()
  LD.PlaySound(drivenObjectEmitter, drivenObjectSound.OnReturnToStart)
  if drivenObjectSound.UseExplicitSoundMotion then
    sound_isDrivenObjectPaused = true
  end
end
function PlayCrankOnStopSound()
  LD.PlaySoundOnFrame(crankEmitter, AnimGO, crankSound.OnStop, crankSound.OnStopAnimFrame)
end
function SetUpSoundMonitors()
  if AnimGO ~= nil and AnimGO ~= crank then
    if crankSoundAnimFrameMonitor ~= nil then
      crankSoundAnimFrameMonitor:Stop()
      crankSoundAnimFrameMonitor:Terminate()
      crankSoundAnimFrameMonitor = nil
    end
    if drivenObjectSoundAnimFrameMonitor ~= nil then
      drivenObjectSoundAnimFrameMonitor:Stop()
      drivenObjectSoundAnimFrameMonitor:Terminate()
      drivenObjectSoundAnimFrameMonitor = nil
    end
    monitors_LoadLibrary()
    crankSoundAnimFrameMonitor = monitors.CreateAnimFrameMonitor(AnimGO)
    drivenObjectSoundAnimFrameMonitor = monitors.CreateAnimFrameMonitor(AnimGO)
    SetUpOnReturnToStartMonitor()
    SetUpOnEndSoundMonitor()
  end
end
function SetUpOnReturnToStartMonitor()
  if not IsEmpty(crankSound.OnReturnToStart) then
    if crankSound.OnReturnToStartFrame < 1 then
      crankSound.OnReturnToStartFrame = 3
    end
    crankSoundAnimFrameMonitor:OnFrameBackward(crankSound.OnReturnToStartFrame, PlayCrankOnReturnToStartSound)
  end
  if not IsEmpty(drivenObjectSound.OnReturnToStart) then
    if drivenObjectSound.OnReturnToStartFrame < 1 then
      drivenObjectSound.OnReturnToStartFrame = 3
    end
    drivenObjectSoundAnimFrameMonitor:OnFrameBackward(drivenObjectSound.OnReturnToStartFrame, PlayDrivenObjectOnReturnToStartSound)
  end
end
function SetUpOnEndSoundMonitor()
  if not IsEmpty(crankSound.OnEnd) then
    if crankSound.OnEndFrame < 1 then
      crankSound.OnEndFrame = AnimGO.AnimLengthFrames - 3
    end
    crankSoundAnimFrameMonitor:OnFrameForward(crankSound.OnEndFrame, PlayCrankOnEndSound)
  end
  if not IsEmpty(drivenObjectSound.OnEnd) then
    if drivenObjectSound.OnEndFrame < 1 then
      drivenObjectSound.OnEndFrame = AnimGO.AnimLengthFrames - 3
    end
    drivenObjectSoundAnimFrameMonitor:OnFrameForward(drivenObjectSound.OnEndFrame, PlayDrivenObjectOnEndSound)
  end
end
function PlayFastForwardSound()
  PlayOnStartSound()
  LD.PlaySoundOnFrame(crankEmitter, AnimGO, crankSound.OnFastForward, crankSound.OnFastForwardFrame, "forward")
  if IsLoopingSound(drivenObjectSound.OnFastForward) and drivenObjectSound.UseExplicitSoundMotion then
    if sound_isDrivenObjectPaused then
      sound_isDrivenObjectPaused = false
      LD.PlaySoundOnFrame(drivenObjectEmitter, AnimGO, drivenObjectSound.OnFastForward, drivenObjectSound.OnFastForwardFrame, "forward", true)
    end
  else
    LD.PlaySoundOnFrame(drivenObjectEmitter, AnimGO, drivenObjectSound.OnFastForward, drivenObjectSound.OnFastForwardFrame, "forward")
  end
end
function StopFastForwardSound()
  StopSoundIfLooping(crankEmitter, crankSound.OnFastForward)
  StopSoundIfLooping(drivenObjectEmitter, drivenObjectSound.OnFastForward)
  if drivenObjectSound.UseExplicitSoundMotion then
    sound_isDrivenObjectPaused = true
  end
end
function StopFastForwardSound_AnimSynchedCranks()
  if 0 < #animSynchedCrankObjects then
    for _, crank in pairs(animSynchedCrankObjects) do
      crank.LuaObjectScript.StopFastForwardSound()
    end
  end
end
function PlayRewindSound()
  PlayOnStartFromEndSound()
  LD.PlaySoundOnFrame(crankEmitter, AnimGO, crankSound.OnRewind, crankSound.OnRewindFrame, "backward")
  if IsLoopingSound(drivenObjectSound.OnRewind) and drivenObjectSound.UseExplicitSoundMotion then
    if sound_isDrivenObjectPaused then
      sound_isDrivenObjectPaused = false
      LD.PlaySoundOnFrame(drivenObjectEmitter, AnimGO, drivenObjectSound.OnRewind, drivenObjectSound.OnRewindFrame, "backward", true)
    end
  else
    LD.PlaySoundOnFrame(drivenObjectEmitter, AnimGO, drivenObjectSound.OnRewind, drivenObjectSound.OnRewindFrame, "backward")
  end
end
function StopRewindSound()
  StopSoundIfLooping(crankEmitter, crankSound.OnRewind)
  StopSoundIfLooping(drivenObjectEmitter, drivenObjectSound.OnRewind)
  if drivenObjectSound.UseExplicitSoundMotion then
    sound_isDrivenObjectPaused = true
  end
end
function StopRewindSound_AnimSynchedCranks()
  if 0 < #animSynchedCrankObjects then
    for _, crank in pairs(animSynchedCrankObjects) do
      crank.LuaObjectScript.StopRewindSound()
    end
  end
end
function PlayAttachSound()
  LD.PlaySoundOnFrame(crankEmitter, player, crankSound.OnAttach, crankSound.OnAttachFrame, "forward")
  LD.PlaySoundOnFrame(drivenObjectEmitter, player, drivenObjectSound.OnAttach, drivenObjectSound.OnAttachFrame, "forward")
end
function PlayDetachSound()
  if crankDetachedBehavior == "rewind" and drivenObjectAnimState == "beginning" then
    print("Detach sound did not fire, because the crank was not moved")
  elseif crankDetachedBehavior == "fast forward" and drivenObjectAnimState == "end" then
    print("Detach sound did not fire, because the crank was not moved")
  else
    LD.PlaySoundOnFrame(crankEmitter, player, crankSound.OnDetach, crankSound.OnDetachFrame, "forward")
    LD.PlaySoundOnFrame(drivenObjectEmitter, player, drivenObjectSound.OnDetach, drivenObjectSound.OnDetachFrame, "forward")
  end
end
function PlayOnStuckAtStartSound()
  if drivenObject == nil and not CrankIsLocked() then
    StopAllLoopingSounds()
  end
  LD.PlaySoundOnFrame(crankEmitter, player, crankSound.OnStuckAtStart, crankSound.OnStuckAtStartFrame, "forward")
  LD.PlaySoundOnFrame(drivenObjectEmitter, player, drivenObjectSound.OnStuckAtStart, drivenObjectSound.OnStuckAtStartFrame, "forward")
end
function PlayOnStuckAtEndSound()
  if drivenObject == nil and not CrankIsLocked() then
    StopAllLoopingSounds()
  end
  LD.PlaySoundOnFrame(crankEmitter, player, crankSound.OnStuckAtEnd, crankSound.OnStuckAtEndFrame, "forward")
  LD.PlaySoundOnFrame(drivenObjectEmitter, player, drivenObjectSound.OnStuckAtEnd, drivenObjectSound.OnStuckAtEndFrame, "forward")
end
function PlayOnJammedSound()
  LD.PlaySoundOnFrame(crankEmitter, player, crankSound.OnJammed, crankSound.OnJammedFrame, "forward")
  LD.PlaySoundOnFrame(drivenObjectEmitter, player, drivenObjectSound.OnJammed, drivenObjectSound.OnJammedFrame, "forward")
end
function PlayOnStuckWhileJammedSound()
  LD.PlaySoundOnFrame(crankEmitter, player, crankSound.OnStuckWhileJammed, crankSound.OnStuckWhileJammedFrame, "forward")
  LD.PlaySoundOnFrame(drivenObjectEmitter, player, drivenObjectSound.OnStuckWhileJammed, drivenObjectSound.OnStuckWhileJammedFrame, "forward")
end
function PlayOnLockedSound()
  LD.PlaySoundOnFrame(crankEmitter, player, crankSound.OnLocked, crankSound.OnLockedFrame, "forward")
  LD.PlaySoundOnFrame(drivenObjectEmitter, player, drivenObjectSound.OnLocked, drivenObjectSound.OnLockedFrame, "forward")
end
function PlayOnUnlockedSound()
  LD.PlaySoundOnFrame(crankEmitter, player, crankSound.OnUnlocked, crankSound.OnUnlockedFrame, "forward")
  LD.PlaySoundOnFrame(drivenObjectEmitter, player, drivenObjectSound.OnUnlocked, drivenObjectSound.OnUnlockedFrame, "forward")
end
function PlayOnStuckWhileLockedSound()
  LD.PlaySoundOnFrame(crankEmitter, player, crankSound.OnStuckWhileLocked, crankSound.OnStuckWhileLockedFrame, "forward")
  LD.PlaySoundOnFrame(drivenObjectEmitter, player, drivenObjectSound.OnStuckWhileLocked, drivenObjectSound.OnStuckWhileLockedFrame, "forward")
end
function PlaySoundOnCrank(sound)
  LD.PlaySound(crankEmitter, sound)
end
function PlaySoundOnDrivenObject(sound)
  LD.PlaySound(drivenObjectEmitter, sound)
end
function StopSoundIfLooping(emitter, sound)
  if IsLoopingSound(sound) then
    LD.StopSound(emitter, sound)
  end
end
function IsLoopingSound(soundEvent)
  if soundEvent ~= nil and type(soundEvent) == "string" then
    if string.match(string.lower(soundEvent), "_lp") then
      return true
    else
      return false
    end
  else
    return false
  end
end
function GetWheelAngularVelocityRTPC()
  if crankType == "Wheel" then
    currentFrameAngVel = crank:GetWorldJointForward(crank:GetJointIndex("JOWheel1"))
    angularVelocity = math.floor((lastFrameAngVel - currentFrameAngVel):Length() * 10000) / 100
    lastFrameAngVel = currentFrameAngVel
    return angularVelocity
  else
    return 0
  end
end
function IsEmpty(str)
  return str == nil or str == ""
end
function ShowSoundDebugTable()
  if engine.VFSGetBool("/Sound/Emitter Data") == 1 and (player.WorldPosition - crank.WorldPosition):Length() < 5 then
    CrankDebugTable()
    DrivenObjectDebugTable()
  end
end
function CrankDebugTable()
  local debugTable = {}
  debugTable.Title = "Sound Crank Info"
  debugTable.TitleColor = engine.Vector.New(255, 0, 128)
  debugTable.X, debugTable.Y = 60, 1
  table.insert(debugTable, {
    "SoundEmitter:",
    tostring(crankEmitter)
  })
  for k, v in pairs(crankSound) do
    table.insert(debugTable, {k, v})
  end
  engine.DrawDebugTable(debugTable)
end
function DrivenObjectDebugTable()
  local debugTable = {}
  debugTable.Title = "Sound DrivenObject Info"
  debugTable.TitleColor = engine.Vector.New(255, 0, 128)
  debugTable.X, debugTable.Y = 150, 1
  table.insert(debugTable, {
    "SoundEmitter:",
    tostring(drivenObjectEmitter)
  })
  for k, v in pairs(drivenObjectSound) do
    table.insert(debugTable, {k, v})
  end
  engine.DrawDebugTable(debugTable)
end
function OnSaveCheckpoint(level, obj)
  return {
    state = state,
    crankType = crankType,
    crankSolved = crankSolved,
    crankSubType = crankSubType,
    drivenObject = drivenObject,
    drivenObjFrameChangePerInterval = drivenObjFrameChangePerInterval,
    currentCycle = currentCycle,
    numPausePositions = numPausePositions,
    minStopCycle = minStopCycle,
    maxStopCycle = maxStopCycle,
    minStopCycleStart = minStopCycleStart,
    maxStopCycleStart = maxStopCycleStart,
    minDrivenObjectFrame = minDrivenObjectFrame,
    maxDrivenObjectFrame = maxDrivenObjectFrame,
    rewindRate = rewindRate,
    easeTime = easeTime,
    crankStartState = crankStartState,
    crankDetachedBehavior = crankDetachedBehavior,
    lockWhenFullyCranked = lockWhenFullyCranked,
    animSynchedObjectNames = animSynchedObjectNames,
    gearObjectNames = gearObjectNames,
    cycleStopFrames = cycleStopFrames,
    pausePositions = pausePositions,
    totalRewindTime = totalRewindTime,
    tableCamera = tableCamera
  }
end
function OnRestoreCheckpoint(level, obj, savedInfo)
  state = savedInfo.state
  crankType = savedInfo.crankType
  crankSolved = savedInfo.crankSolved
  crankSubType = savedInfo.crankSubType
  drivenObject = savedInfo.drivenObject
  drivenObjFrameChangePerInterval = savedInfo.drivenObjFrameChangePerInterval
  currentCycle = savedInfo.currentCycle
  numPausePositions = savedInfo.numPausePositions
  minStopCycle = savedInfo.minStopCycle
  maxStopCycle = savedInfo.maxStopCycle
  minStopCycleStart = savedInfo.minStopCycleStart
  maxStopCycleStart = savedInfo.maxStopCycleStart
  minDrivenObjectFrame = savedInfo.minDrivenObjectFrame
  maxDrivenObjectFrame = savedInfo.maxDrivenObjectFrame
  rewindRate = savedInfo.rewindRate
  easeTime = savedInfo.easeTime
  crankStartState = savedInfo.crankStartState
  crankDetachedBehavior = savedInfo.crankDetachedBehavior
  lockWhenFullyCranked = savedInfo.lockWhenFullyCranked
  animSynchedObjectNames = savedInfo.animSynchedObjectNames
  gearObjectNames = savedInfo.gearObjectNames
  cycleStopFrames = savedInfo.cycleStopFrames
  pausePositions = savedInfo.pausePositions
  totalRewindTime = savedInfo.totalRewindTime
  tableCamera = savedInfo.tableCamera
end
