0 of 0

File information

Last updated

Original upload

Created by

Merlord

Uploaded by

Merlord

Virus scan

Safe to use

15 comments

  1. Seallv
    Seallv
    • member
    • 0 kudos
    Thank you mod authors for making this mod! It makes my Morrowind playtrough much easier on my fingers and on my wrist.

    I cant imagine Morrowind designers thought that this is the best way to play the game - repetady for 30seconds to minute spam a mouse button on each enemy. And when skills get higher then the same is true for packs of 2-3-4 enemies at a time.

    I get the 3D video game implementation into D&D combat system - where you need to roll the dice to know if your attack has been successful or not and that is great, but after a while my fingers start hurting and if i lay back in to the chair, my wrist also start hurting.

    This mod solves this problem. And also works perfectly so far, past ~ 20 hours of gameplay i havent noticed anything that wouldnt work perfectly.
  2. cantorsdust
    cantorsdust
    • premium
    • 48 kudos
    I've edited the mod with the following changes:
    1) Expands the hotkeys to include mouse buttons in the combination
    2) Allows the option to auto-attack when holding the hotkey, instead of toggling the auto-attack on and off.
    3) Allows the option to ignore control keys (shift, alt, ctrl, and super).  This allows one to auto-attack while holding shift to sprint or when not holding shift.

    This allows me to, for example, bind my auto-attack key to the mouse, and just hold the mouse button when I want to attack, and release it to stop attacking.  I can attack while sprinting, walking, sneaking, or standing still without worrying about whether I'm holding shift or ctrl.

    Users who wish to use these changes will need to replace their config.lua, main.lua, mcm.lua, and util.lua files in the mod with the following:

    config.lua:
    Spoiler:  
    Show

    
    local inMemConfig

    local this = {}

    --Static Config (stored right here)
    this.static = {
        modName = "Auto Attack",
        modDescription =
        [[A mod that allows you to attack automatically with a hotkey.]],
        useKeyOptionIndex = 5,
        deviceMouse = 1,
        deviceKeyboard = 0,
        buttonDown = 128,
    }

    --MCM Config (stored as JSON)
    this.configPath = "autoAttack"
    this.mcmDefault = {
        enabled = true,
        holding = false,
        controlKeysMatter = true,
        logLevel = "INFO",
        ---@type mwseKeyMouseCombo
        hotKey = {
            keyCode = tes3.scanCode.q,
            isAltDown = true
            --mouseButton = 0
        },
        maxSwing = 100,
        displayMessages = true
    }
    this.save = function(newConfig)
        inMemConfig = newConfig
        mwse.saveConfig(this.configPath, inMemConfig)
    end

    this.mcm = setmetatable({}, {
        __index = function(_, key)
            inMemConfig = inMemConfig or mwse.loadConfig(this.configPath, this.mcmDefault)
            return inMemConfig[key]
        end,
        __newindex = function(_, key, value)
            inMemConfig = inMemConfig or mwse.loadConfig(this.configPath, this.mcmDefault)
            inMemConfig[key] = value
            mwse.saveConfig(this.configPath, inMemConfig)
        end
    })


    -- local persistentDefault =
    -- }

    -- this.persistent = setmetatable({}, {
    --     __index = function(_, key)
    --         if not tes3.player then return end
    --         tes3.player.data[this.configPath] = tes3.player.data[this.configPath] or persistentDefault
    --         return tes3.player.data[this.configPath][key]
    --     end,
    --     __newindex = function(_, key, value)
    --         if not tes3.player then return end
    --         tes3.player.data[this.configPath] = tes3.player.data[this.configPath] or persistentDefault
    --         tes3.player.data[this.configPath][key] = value
    --     end
    -- })

    return this



    main.lua:
    Spoiler:  
    Show


    require("mer.autoAttack.mcm")

    local util = require("mer.autoAttack.util")
    local config = util.config
    local logger = util.createLogger("main")
    local attackToggled = false


    --[[
    --original function, edited below
    local function onKeyDown(e)
        if not config.mcm.enabled then return end
        if not tes3.player then return end
        if not tes3.player.mobile then return end
        if not tes3.player.mobile.weaponDrawn then return end
        if util.isKeyPressed(e, config.mcm.hotKey) then
            attackToggled = not attackToggled
            logger:debug("Toggling Auto-Attack %s", attackToggled and "on" or "off")
            if config.mcm.displayMessages then
                tes3.messageBox("Auto-Attack %s.", attackToggled and "On" or "Off")
            end
        end
    end
    ]]--

    --[[
    plan:
    add separate isKeyUp to detect when hotkey is released
    then change onKeyDown to just turn on, not toggle attack
    and add onKeyUp to turn off attack
    ]]--

    ---@param e keyDownEventData|mouseButtonDownEventData
    local function onKeyDown(e)
        if not config.mcm.enabled then return end
        if not tes3.player then return end
        if not tes3.player.mobile then return end
        if not tes3.player.mobile.weaponDrawn then return end
        if tes3.menuMode() then return end
        if util.isKeyPressed(e, config.mcm.hotKey) then
            --attackToggled = not attackToggled
            if config.mcm.holding then
                attackToggled = true --turned false using onKeyUp
            else
                attackToggled = not attackToggled
            end
            logger:debug("Toggling Auto-Attack %s", attackToggled and "on" or "off")
            if config.mcm.displayMessages then
                tes3.messageBox("Auto-Attack %s.", attackToggled and "On" or "Off")
            end
        end
    end

    ---@param e keyUpEventData|mouseButtonUpEventData
    local function onKeyUp(e)
        if not config.mcm.enabled then return end
        if not config.mcm.holding then return end --if you run this code when holding isn't enabled it still toggles off your attack, so this needs to be disabled
        if not tes3.player then return end
        if not tes3.player.mobile then return end
        if not tes3.player.mobile.weaponDrawn then return end
        if tes3.menuMode() then return end
        if util.isKeyReleased(e, config.mcm.hotKey) then
            --attackToggled = not attackToggled
            attackToggled = false
            logger:debug("Toggling Auto-Attack %s", attackToggled and "on" or "off")
            if config.mcm.displayMessages then
                tes3.messageBox("Auto-Attack %s.", attackToggled and "On" or "Off")
            end
        end
    end


    local function getAttackKeyConfig()
        local inputController = tes3.worldController.inputController
        ---@type tes3inputConfig
        local attackConfig = inputController.inputMaps[config.static.useKeyOptionIndex]
        return attackConfig
    end

    local function getPlayerAtMaxSwing()
        local attackSwing = tes3.mobilePlayer.animationController:calculateAttackSwing()
        local atMaxSwing = attackSwing >= math.max(1, config.mcm.maxSwing) / 100
        logger:trace("At Max Swing: %s", atMaxSwing)
        return atMaxSwing
    end

    ---@param e simulateEventData
    local function onSimulate(e)
        if not config.mcm.enabled then return end
        if not attackToggled then return end
        if not tes3.player.mobile then return end
        if not tes3.player.mobile.weaponDrawn then return end

        local inputController = tes3.worldController.inputController
        local attackConfig = getAttackKeyConfig()
        local attackSwing = tes3.player.mobile.actionData.attackSwing
        logger:trace("AttackSwing: %s", attackSwing)

        --Mouse
        if attackConfig.device == config.static.deviceMouse then
            local state = getPlayerAtMaxSwing() and 0 or config.static.buttonDown
            logger:trace("Setting state to %s", state)
            inputController.mouseState.buttons[attackConfig.code + 1] = state
        --Keyboard
        elseif attackConfig.device == config.static.deviceKeyboard then
            local state = getPlayerAtMaxSwing() and 0 or config.static.buttonDown
            logger:trace("Setting state to %s", state)
            inputController.keyboardState[attackConfig.code + 1] = state
        end
    end


    local function initialise()
        event.register(tes3.event.keyDown, onKeyDown)
        event.register(tes3.event.mouseButtonDown, onKeyDown)
        event.register(tes3.event.keyUp, onKeyUp)
        event.register(tes3.event.mouseButtonUp, onKeyUp)
        event.register(tes3.event.simulate, onSimulate)
        logger:info("Initialised: %s", util.getVersion())
    end
    event.register(tes3.event.initialized, initialise)



    mcm.lua:
    Spoiler:  
    Show


    local util = require("mer.autoAttack.util")
    local config = util.config
    local mcmConfig = mwse.loadConfig(config.configPath, config.mcmDefault)

    local LINKS_LIST = {
        {
            text = "Release history",
            url = "https://github.com/jhaakma/auto-attack/releases"
        },
        -- {
        --     text = "Wiki",
        --     url = "https://github.com/jhaakma/auto-attack/wiki"
        -- },
        {
            text = "Nexus Page",
            url = "https://www.nexusmods.com/morrowind/mods/51348"
        },
        {
            text = "Buy me a coffee",
            url = "https://ko-fi.com/merlord"
        },
    }
    local CREDITS_LIST = {
        {
            text = "Made by Merlord",
            url = "https://www.nexusmods.com/users/3040468?tab=user+files",
        },
    }

    local function addSideBar(component)
        local versionText = string.format(config.static.modName)
        component.sidebar:createCategory(versionText)
        component.sidebar:createInfo{ text = config.static.modDescription}

        local linksCategory = component.sidebar:createCategory("Links")
        for _, link in ipairs(LINKS_LIST) do
            linksCategory:createHyperLink{ text = link.text, url = link.url }
        end
        local creditsCategory = component.sidebar:createCategory("Credits")
        for _, credit in ipairs(CREDITS_LIST) do
            creditsCategory:createHyperLink{ text = credit.text, url = credit.url }
        end
    end

    local function registerMCM()
        local template = mwse.mcm.createTemplate{ name = config.static.modName,}
        template.onClose = function()
            config.save(mcmConfig)
        end
        template:register()

        local page = template:createSideBarPage{ label = "Settings"}
        addSideBar(page)

        page:createYesNoButton{
            label = "Enable Mod",
            description = "Turn this mod on or off",
            variable = mwse.mcm.createTableVariable{ id = "enabled", table = mcmConfig }
        }
        
        page:createYesNoButton{
            label = "Enable Holding",
            description = "If enabled, auto-attack by holding hotkey.  If disabled, auto-attack by toggling hotkey.",
            variable = mwse.mcm.createTableVariable{ id = "holding", table = mcmConfig }
        }
        
        page:createYesNoButton{
            label = "Enable Control Keys",
            description = "If enabled, can use shift, control, alt, and super as part of hotkeys.  If disabled, these are ignored, only main key is used.",
            variable = mwse.mcm.createTableVariable{ id = "controlKeysMatter", table = mcmConfig }
        }

        page:createOnOffButton{
            label = "Display Messages",
            description = "Displays a message box whenever auto-attack is toggled on or off.",
            variable = mwse.mcm.createTableVariable{ id = "displayMessages", table = mcmConfig }
        }

        page:createKeyBinder{
            label = "Toggle Auto Attack Hot key",
            description = "The key and/or mouse combo to toggle auto attacking on or off.",
            allowMouse = true,
            allowCombinations = true,
            ---@type mwseKeyMouseCombo
            variable = mwse.mcm.createTableVariable{ id = "hotKey", table = mcmConfig }
        }

        page:createSlider{
            label = "Max Swing: %s%%",
            description = "Determines how far back to pull the weapon before releasing while auto-attacking.",
            variable = mwse.mcm.createTableVariable{ id = "maxSwing", table = mcmConfig },
            min = 0,
            max = 100,
            jump = 10,
            step = 1
        }

        page:createDropdown{
            label = "Log Level",
            description = "Set the logging level for mwse.log. Keep on INFO unless you are debugging.",
            options = {
                { label = "TRACE", value = "TRACE"},
                { label = "DEBUG", value = "DEBUG"},
                { label = "INFO", value = "INFO"},
                { label = "ERROR", value = "ERROR"},
                { label = "NONE", value = "NONE"},
            },
            variable =  mwse.mcm.createTableVariable{ id = "logLevel", table = mcmConfig },
            callback = function(self)
                for _, logger in pairs(util.loggers) do
                    logger:setLogLevel(self.variable.value)
                end
            end
        }
    end
    event.register("modConfigReady", registerMCM)



    util.lua:
    Spoiler:  
    Show


    local Util = {}

    Util.config = require("mer.autoAttack.config")
    Util.messageBox = require("mer.autoAttack.messageBox")
    Util.loggers = {}
    do --logger
        local logLevel = Util.config.mcm.logLevel
        local logger = require("logging.logger")
        Util.log = logger.new{
            name = Util.config.static.modName,
            logLevel = logLevel
        }
        Util.createLogger = function(serviceName)
            local logger = logger.new{
                name = string.format("%s: %s", Util.config.static.modName, serviceName),
                logLevel = logLevel
            }
            Util.loggers[serviceName] = logger
            return logger
        end
    end

    ---@param pressed keyDownEventData|mouseButtonDownEventData
    ---@param stored keyDownEventData|mouseButtonDownEventData
    function Util.isKeyPressed(pressed, stored)
        if Util.config.mcm.controlKeysMatter then
            return ( tes3.isKeyEqual({ expected = stored, actual = pressed }) )
            --[[
            return (
                pressed.keyCode == stored.keyCode
                 and not not pressed.isShiftDown == not not stored.isShiftDown
                 and not not pressed.isControlDown == not not stored.isControlDown
                 and not not pressed.isAltDown == not not stored.isAltDown
                 and not not pressed.isSuperDown == not not stored.isSuperDown
            )
            ]]--
        else
            pressed.isShiftDown = false
            pressed.isControlDown = false
            pressed.isAltDown = false
            pressed.isSuperDown = false
            stored.isShiftDown = false
            stored.isControlDown = false
            stored.isAltDown = false
            stored.isSuperDown = false
            return ( tes3.isKeyEqual({ expected = stored, actual = pressed }) )
            --[[
            return (
                pressed.keyCode == stored.keyCode
            )
            ]]--
        end
    end

    ---@param released keyUpEventData|mouseButtonUpEventData
    ---@param stored keyUpEventData|mouseButtonUpEventData
    function Util.isKeyReleased(released, stored)
        if Util.config.mcm.controlKeysMatter then
            return ( tes3.isKeyEqual({ expected = stored, actual = released }) )
            --[[
            return (
                released.keyCode == stored.keyCode
                 and not not released.isShiftDown == not not stored.isShiftDown
                 and not not released.isControlDown == not not stored.isControlDown
                 and not not released.isAltDown == not not stored.isAltDown
                 and not not released.isSuperDown == not not stored.isSuperDown
            )
            ]]--
        else
            released.isShiftDown = false
            released.isControlDown = false
            released.isAltDown = false
            released.isSuperDown = false
            stored.isShiftDown = false
            stored.isControlDown = false
            stored.isAltDown = false
            stored.isSuperDown = false
            return ( tes3.isKeyEqual({ expected = stored, actual = released }) )
            --[[
            return (
                released.keyCode == stored.keyCode
            )
            ]]--
        end
    end

    function Util.getVersion()
        local versionFile = io.open("Data Files/MWSE/mods/mer/autoAttack/version.txt", "r")
        if not versionFile then return end
        local version = ""
        for line in versionFile:lines() do -- Loops over all the lines in an open text file
            version = line
        end
        return version
    end

    return Util

    1. Merlord
      Merlord
      • premium
      • 353 kudos
      This is great, I'll update the mod with these changes. Thanks!
    2. cantorsdust
      cantorsdust
      • premium
      • 48 kudos
      In further testing, I've noted that using a mouse button for your hotkey doesn't get saved correctly.  autoAttack.json, the config file, correctly saves the hotkey, but when loaded it defaults to shift-Q, the default value.  I didn't actually change anything in the MCM code for the hotkey except for adding allowMouse = true, so I'm not sure why this isn't saving correctly.  The type of config saved would be a mwseKeyMouseCombo instead of a mwseKeyCombo but that's never actually mentioned in any of the lua or .json files and mwseKeyMouseCombo inherits mwseKeyCombo.

      For now, for users, if you use a mouse button in your hotkey, you'll need to reset it every time you run the game.  You can change this by manually coding it into config.lua but most people aren't going to do this.


      Fixed, see post below. Above code doesn't have this error.
    3. Merlord
      Merlord
      • premium
      • 353 kudos
      Thanks I'll get that fixed too
    4. cantorsdust
      cantorsdust
      • premium
      • 48 kudos
      For users, I've edited my comments above with an updated version of the files.  The underlying base MCM bug is fixed, and now the version above does everything as intended.  You do need to run MWSE-Update.exe to make sure your MWSE.exe is updated to today's, 8/15/2024, version of MWSE.  Earlier versions before 8/15/2024 will have a bug that makes the MCM not correctly save a hotkey using a mouse button.
  3. bolbasor
    bolbasor
    • member
    • 0 kudos
    I'd love to have something like this for OMW :sad:
  4. barame3526
    barame3526
    • member
    • 5 kudos
    After all these years I can finally play with one-handed short blade instead of picking 2H axe/blunt every time! Thank you!
  5. SuccubusNirriti
    SuccubusNirriti
    • member
    • 1 kudos
    Thank you very much for saving my COMP mouse 45 clicks per monster. Bless you. 
  6. shulf
    shulf
    • member
    • 2 kudos
    I asked ChatGPT to change the script so that auto-attack goes on hold instead of switch, so I could use the AutoHotkey to assign the key to the left mouse button for comfortable fighting with low-damage weapons like short blades. And it worked, lol. You can ask a ChatGPT to customize mods without coding knowledge. But it couldn't add exceptions for throwing weapons to the mod. It would be awesome if you could add a mod version that works by holding down a key, with the option to assign a mouse button and an exception for long-range weapons.
  7. Keeptrucking
    Keeptrucking
    • supporter
    • 0 kudos
    You have no idea how much I love you for this.
  8. jezzr33
    jezzr33
    • supporter
    • 0 kudos
    Thanks for this, do you think it is possible to do this with casting (such as the hotkey casting in Morrowind Code Patch)? I would do it myself if it isn't a dead end.
  9. arcvoodal
    arcvoodal
    • member
    • 3 kudos
    Is it possible to bind mouse-click at the auto attack?
  10. bigDunm3r
    bigDunm3r
    • supporter
    • 0 kudos
    Thank you Merlord! It works like a charm and it's amazing!