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

addon/PixelState/PixelState.lua7.9 KB

-- Encodes player state into a row of solid-color blocks at the top-left of the
-- screen so an external program can read it by sampling pixel colors.
--
-- Each block is BLOCK_SIZE x BLOCK_SIZE physical pixels. Sample its center.
-- Every channel is one byte (0-255). 16-bit values are split high byte, low byte.
--
--   Block 0: 255, 0, 255                  sync marker
--   Block 1: x high, x low, facing        x is map position 0-1 scaled to 0-65535
--   Block 2: y high, y low, flags         facing is 0-2pi scaled to 0-255
--   Block 3: map id high, map id low, player health %
--   Block 4: player power %, target health %, checksum
--   Block 5: player level, target level, flags2
--            target level is 0 with no target and 255 for a skull (??) target
--            flags2: 1 = target within melee reach (CheckInteractDistance, ~10 yd),
--                    2 = target can be attacked, 4 = player is moving
--                    (GetUnitSpeed)
--   Block 6: 255, 255, 255 while a text box has keyboard focus (chat or any
--            edit box), 0, 0, 0 otherwise. Only full-on and full-off, so it
--            reads true under any gamma, brightness or contrast setting.
--
-- Flags: 1 = in combat, 2 = has target, 4 = target dead, 8 = player dead,
-- 16 = target in range of RANGE_SPELL, 32 = the game said "facing the wrong
-- way" within the last FACING_ERROR_SECONDS, 64 = auto-attacking or wanding,
-- 128 = casting or channelling.
-- Percentages are scaled to 0-255. Position and facing read 0 where the game
-- hides them, such as inside instances.
-- Checksum is the sum of every other channel in blocks 1-5, modulo 256. Reject
-- a frame when it does not match, since it means a sample caught a partial redraw
-- or the colors were shifted by gamma, HDR, or color filters.

local BLOCK_SIZE = 4
local BLOCK_COUNT = 7
-- the spell whose range decides "in range": the external reader stops walking
-- and starts casting once the target is inside it
local RANGE_SPELL = "Shadow Bolt"
-- newer clients moved the global into C_Spell and return a boolean instead of 1
local isSpellInRange = C_Spell and C_Spell.IsSpellInRange or IsSpellInRange
-- the API has no "is the target in front of me", so the game's own error is
-- the signal; it is held long enough for a reader polling every ~100 ms
local FACING_ERROR_SECONDS = 1
local FACING_ERRORS = {
    [ERR_BADATTACKFACING] = true,
    [SPELL_FAILED_UNIT_NOT_INFRONT] = true,
}
local facingErrorAt = 0
-- The strip is redrawn this often rather than every frame, so all the squares
-- hold the same values while a reader samples them one pixel at a time.
local UPDATE_SECONDS = 0.05
-- Attack and Shoot are toggles, so the reader must know whether one is on
-- before pressing it, or the press switches it off
local ATTACK_SPELL_ID = 6603
local WAND_SPELL = "Shoot"
local isCurrentSpell = C_Spell and C_Spell.IsCurrentSpell or IsCurrentSpell
local isAutoRepeatSpell = C_Spell and C_Spell.IsAutoRepeatSpell or IsAutoRepeatSpell

local frame = CreateFrame("Frame", "PixelStateFrame", UIParent)
frame:SetIgnoreParentScale(true)
frame:SetFrameStrata("TOOLTIP")
frame:SetPoint("TOPLEFT")
frame:SetSize(BLOCK_SIZE * BLOCK_COUNT, BLOCK_SIZE)

local blocks = {}
for i = 0, BLOCK_COUNT - 1 do
    local texture = frame:CreateTexture(nil, "OVERLAY")
    texture:SetSize(BLOCK_SIZE, BLOCK_SIZE)
    texture:SetPoint("TOPLEFT", i * BLOCK_SIZE, 0)
    blocks[i] = texture
end

local function setBlock(index, red, green, blue)
    blocks[index]:SetColorTexture(red / 255, green / 255, blue / 255)
end

-- Clients with secret values (such as WoW Forever) withhold some numbers from
-- addons in combat or always, and arithmetic on one raises an error that would
-- stop the whole strip. A withheld number reads 0.
local isSecret = issecretvalue or function() return false end
local function plain(value)
    if isSecret(value) then
        return 0
    end
    return value
end

local function splitBytes(value)
    return math.floor(value / 256), value % 256
end

local function percentByte(current, maximum)
    current, maximum = plain(current), plain(maximum)
    if maximum == 0 then
        return 0
    end
    return math.floor(current / maximum * 255)
end

local function readPosition()
    local mapID = C_Map.GetBestMapForUnit("player")
    if not mapID then
        return 0, 0, 0
    end
    local position = C_Map.GetPlayerMapPosition(mapID, "player")
    if not position then
        return mapID, 0, 0
    end
    return mapID, math.floor(plain(position.x) * 65535), math.floor(plain(position.y) * 65535)
end

local function readFlags()
    local flags = 0
    if UnitAffectingCombat("player") then
        flags = flags + 1
    end
    if UnitExists("target") then
        flags = flags + 2
        if UnitIsDead("target") then
            flags = flags + 4
        end
        local inRange = isSpellInRange(RANGE_SPELL, "target")
        if inRange == true or inRange == 1 then
            flags = flags + 16
        end
        if GetTime() - facingErrorAt < FACING_ERROR_SECONDS then
            flags = flags + 32
        end
    end
    if UnitIsDeadOrGhost("player") then
        flags = flags + 8
    end
    if isCurrentSpell(ATTACK_SPELL_ID) or isAutoRepeatSpell(WAND_SPELL) then
        flags = flags + 64
    end
    if UnitCastingInfo("player") or UnitChannelInfo("player") then
        flags = flags + 128
    end
    return flags
end

-- 1 UI unit equals 1 physical pixel when the frame scale is 768 / screen height,
-- because the UI coordinate space is always 768 units tall.
local function matchPhysicalPixels()
    local _, screenHeight = GetPhysicalScreenSize()
    frame:SetScale(768 / screenHeight)
end

local function update()
    local mapID, x, y = readPosition()
    local facing = math.floor(plain(GetPlayerFacing() or 0) / (2 * math.pi) * 255)
    local flags = readFlags()
    local health = percentByte(UnitHealth("player"), UnitHealthMax("player"))
    local power = percentByte(UnitPower("player"), UnitPowerMax("player"))
    local targetHealth = percentByte(UnitHealth("target"), UnitHealthMax("target"))
    local level = plain(UnitLevel("player"))
    local targetLevel = 0
    local flags2 = 0
    if plain(GetUnitSpeed("player")) > 0 then
        flags2 = flags2 + 4
    end
    if UnitExists("target") then
        targetLevel = plain(UnitLevel("target"))
        if targetLevel < 1 then
            targetLevel = 255
        end
        if CheckInteractDistance("target", 3) then
            flags2 = flags2 + 1
        end
        if UnitCanAttack("player", "target") then
            flags2 = flags2 + 2
        end
    end

    local xHigh, xLow = splitBytes(x)
    local yHigh, yLow = splitBytes(y)
    local mapHigh, mapLow = splitBytes(mapID)
    local checksum = (xHigh + xLow + facing + yHigh + yLow + flags + mapHigh + mapLow
        + health + power + targetHealth + level + targetLevel + flags2) % 256

    setBlock(1, xHigh, xLow, facing)
    setBlock(2, yHigh, yLow, flags)
    setBlock(3, mapHigh, mapLow, health)
    setBlock(4, power, targetHealth, checksum)
    setBlock(5, level, targetLevel, flags2)
    if GetCurrentKeyBoardFocus() then
        setBlock(6, 255, 255, 255)
    else
        setBlock(6, 0, 0, 0)
    end
end

local sinceUpdate = UPDATE_SECONDS
local function onUpdate(_, elapsed)
    sinceUpdate = sinceUpdate + elapsed
    if sinceUpdate >= UPDATE_SECONDS then
        sinceUpdate = 0
        update()
    end
end

local function onEvent(_, event, _, message)
    if event == "UI_ERROR_MESSAGE" then
        if FACING_ERRORS[message] then
            facingErrorAt = GetTime()
        end
        return
    end
    matchPhysicalPixels()
end

setBlock(0, 255, 0, 255)
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:RegisterEvent("DISPLAY_SIZE_CHANGED")
frame:RegisterEvent("UI_ERROR_MESSAGE")
frame:SetScript("OnEvent", onEvent)
frame:SetScript("OnUpdate", onUpdate)

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