GCD Goblin for WoW Forever

@paysch / gcd-goblinv1.0.1Public

World of Warcraft turbo keypress for Rebind: hold a key to auto repeat it, record a rotation and replay it with one key, plus anti-AFK.

Files

lib/rotation.luau5.4 KB

-- lib/rotation.luau
-- A rotation is a Rebind macro recording trimmed to the ability keys, kept in
-- the script's data folder across restarts and played back once, start to
-- finish, unless the caller stops it first.

local rotation = {}

local PATH = "@data/rotation.json"
-- how often playback checks whether to keep going
local SLICE_MS = 25

-- macro actions: { action = "down" | "up", code, delay }, delay in ms before it
local actions = if rb.file.exists(PATH) then rb.file.read_json(PATH) else {}
local recording = false
-- ability presses seen while recording, for the recording panel's count
local seen = 0

-- How long one pass takes, in ms; 0 when nothing is recorded.
function rotation.length()
  local total = 0
  for _, action in actions do
    total += action.delay
  end
  return total
end

function rotation.ready()
  return #actions > 0
end

function rotation.recording()
  return recording
end

function rotation.seen()
  return seen
end

function rotation.start()
  -- the mouse is recorded for its wheel; stop() drops pointer movement
  rb.macro.record({ ignore_mouse = false })
  recording = true
  seen = 0
end

-- Counts an ability press or wheel turn while recording; returns whether it
-- counted.
function rotation.saw()
  if recording then
    seen += 1
  end
  return recording
end

-- Ends the recording and keeps the saved rotation as it was.
function rotation.cancel()
  rb.macro.finish()
  recording = false
end

-- Kept as pressed, so Ctrl+C plays back as Ctrl+C, but only while an ability
-- press starts during them: a stray Shift on its own never plays back.
local MODIFIERS = { LCtrl = true, RCtrl = true, LShift = true, RShift = true, LAlt = true, RAlt = true }

-- Ends the recording and saves what the game received from the keys in
-- `keys` (a set of key names), the modifiers held with them, and the mouse
-- wheel. Movement, the pointer, other mouse buttons and the menu keys are
-- left out.
--
-- The recorder sees only physical input, so a held turbo key arrives as one
-- long press although turbo sent the game a run of taps. taps(key, held_ms)
-- answers with the taps the game got for that hold, as { at, length } in ms
-- from the press, and each press is replaced by them.
--
-- A release whose press came before recording is dropped, and a key still
-- held at the end is released there. The first action starts at once.
-- Returns the ability presses and wheel turns kept; with none, the saved
-- rotation stays as it was.
function rotation.stop(keys, taps)
  local take = rb.macro.finish()
  recording = false

  -- on the recording's clock: presses as { code, start, held }, and wheel
  -- turns (the actions with an amount) as { action, at }
  local presses, wheel, open = {}, {}, {}
  local now = 0
  for _, action in take do
    now += action.delay
    local code = action.code
    if action.amount then
      table.insert(wheel, { action = action, at = now })
    elseif code and (keys[code] or MODIFIERS[code]) then
      if action.action == "down" and not open[code] then
        open[code] = now
      elseif action.action == "up" and open[code] then
        table.insert(presses, { code = code, start = open[code], held = now - open[code] })
        open[code] = nil
      end
    end
  end
  for code, start in open do
    table.insert(presses, { code = code, start = start, held = now - start })
  end

  local function ability_starts_during(modifier)
    for _, press in presses do
      if keys[press.code] and press.start >= modifier.start and press.start <= modifier.start + modifier.held then
        return true
      end
    end
    return false
  end

  -- the actions with their times; ties keep the order they were made in, so a
  -- zero-length tap's press stays ahead of its release
  local events, kept = {}, 0
  local function add(at, action)
    table.insert(events, { at = at, order = #events, action = action })
  end
  for _, press in presses do
    local ability = keys[press.code] ~= nil
    if ability or ability_starts_during(press) then
      local pressed = if ability then taps(press.code, press.held) else { { at = 0, length = press.held } }
      for _, tap in pressed do
        add(press.start + tap.at, { action = "down", code = press.code })
        add(press.start + tap.at + tap.length, { action = "up", code = press.code })
      end
      if ability then
        kept += 1
      end
    end
  end
  for _, turn in wheel do
    add(turn.at, table.clone(turn.action))
    kept += 1
  end
  if kept == 0 then
    return 0
  end
  table.sort(events, function(a, b)
    return if a.at == b.at then a.order < b.order else a.at < b.at
  end)

  local saved, last = {}, events[1].at
  for _, event in events do
    event.action.delay = event.at - last
    table.insert(saved, event.action)
    last = event.at
  end
  actions = saved
  rb.file.write_json(PATH, actions)
  return kept
end

-- Plays the rotation once until it ends or keep_going() turns false.
-- Coroutine only. Stopping can cut a press between its down and up, so every
-- key the rotation uses is released after.
function rotation.play(keep_going)
  local handle = rb.macro.play(actions)
  while handle:is_playing() do
    if not keep_going() then
      handle:stop()
      for _, action in actions do
        if action.code then
          rb.hid.up(action.code)
        end
      end
      return
    end
    rb.sleep(SLICE_MS)
  end
end

return rotation

61 files, 595.0 KB. 53 have no text preview.