1. The contestants were to create water structures for the 20th Terraining Contest. Choose one in the public poll!
    Dismiss Notice
  2. Join other hivers in a friendly concept-art contest. The contestants have to create a genie coming out of its container. We wish you the best of luck!
    Dismiss Notice
  3. The Melee Mapping Contest #4: 2v2 - Results are out! Step by to congratulate the winners!
    Dismiss Notice
  4. We're hosting the 15th Mini-Mapping Contest with YouTuber Abelhawk! The contestants are to create a custom map that uses the hidden content within Warcraft 3 or is inspired by any of the many secrets within the game.
    Dismiss Notice
  5. Check out the Staff job openings thread.
    Dismiss Notice

[Wurst] MouseUtils

Discussion in 'The Lab' started by apsyll, Mar 16, 2019.

  1. apsyll

    apsyll

    Joined:
    Aug 28, 2015
    Messages:
    184
    Resources:
    1
    Maps:
    1
    Resources:
    1
    I started creating a mouse control/helper system for my project that uses the mouse as input device heavily.
    It is at this stage working but not fully polished.
    you can check if a player presses a specific button, get the amount of multy clicks (double/tripperl/quadrupel... clicks), the mouse position a drag path of each button and the current mouse event.
    Like always feedback ideas and suggestions are more than welcome.
    credits:

    Frotty
    Chaosy

    changelog

    V.0.9b
    Improved the code more. using castTo
    Add more comments.
    V.0.9
    Streamlined the whole coda again more also separated the packages more clearly
    added hot Docs
    V.0.8b
    Split the code in two packages:
    -MouseUtils
    -DragPath
    cleaned up the onMouseEvent function:
    -using a new mousebutton.toKey function to access the currentEvent via hashlist.
    changed multiple if to switches.
    using Players package(FrEntity) to initialize the tuple types for the playing players.

    code

    Code (WurstScript):

    package DragPath
    import MouseUtils
    import ClosureEvents
    import ClosureTimers

    /**
        This package helps with the managing of mouse clicks double and tripple clicks and drags.
        This package saves the count how many times a button was multy clicked for each player
        and safes the drag path the cursor moved across the map while holding one or more mouse buttons down.
    */

    public constant MOUSE_PATH_POINT_DISTANCE = 1000        // the minimum distance the mouse has to move from its last path point to create a new point
    public constant MOUSE_BUTTON_RESET_TIME = 0.2           // the maximum time between two clicks to count as multi click (in seconds)

    public tuple clickStatus(int leftClicks, int rightClicks, int middleClicks, DragPath leftPath, DragPath rightPath, DragPath middlePath, real leftCD, real rightCD, real middleCD)
    public clickStatus array clickStatuses

    /**clickStatuses(Amount of LEFT multi clicks, Amount of RIGHT multi clicks, Amount of MIDDLE multi clicks, the path drawn on the map with pressed left button, the path drawn on the map with pressed right button, the path drawn on the map with pressed middle button, Game time the LEFT multi click reset cooldown started, Game time the RIGHT multi click reset cooldown started, Game time the MIDDLE multi click reset cooldown started)*/
    public function player.getMouseClicks() returns clickStatus
        return clickStatuses[this.getId()]

    /**sets the multi click amount of the given mouse button to the given amount*/
    public function player.setClickCounter(mousebuttontype wichButton, int count)
        if wichButton == MOUSE_BUTTON_TYPE_LEFT
            clickStatuses[this.getId()].leftClicks = count
        else if wichButton == MOUSE_BUTTON_TYPE_RIGHT
            clickStatuses[this.getId()].rightClicks = count
        else if wichButton == MOUSE_BUTTON_TYPE_MIDDLE
            clickStatuses[this.getId()].middleClicks = count

    /**returns the amount of multi clicks of the given mouse button*/
    public function player.getClickCounter(mousebuttontype wichButton) returns int
        if wichButton == MOUSE_BUTTON_TYPE_LEFT
            return clickStatuses[this.getId()].leftClicks
        else if wichButton == MOUSE_BUTTON_TYPE_RIGHT
            return clickStatuses[this.getId()].rightClicks
        else if wichButton == MOUSE_BUTTON_TYPE_MIDDLE
            return clickStatuses[this.getId()].middleClicks
        return 0

    /**Checks the distance between the last point and mouse position and adds a new DragPoint to the path if over MOUSE_PATH_POINT_DISTANCE*/
    function mouseStatus.onMouseMoved(int playerId)
        if this.left
            clickStatuses[playerId].leftPath.addDragPoint(this.mousePosition)
        if this.right
            clickStatuses[playerId].rightPath.addDragPoint(this.mousePosition)
        if this.middle
            clickStatuses[playerId].middlePath.addDragPoint(this.mousePosition)

    /**Increases the click counter and adds a new path for based on the currentEvent*/        
    function mouseStatus.onMousePressed(int playerId)
        switch this.currentEvent
            case MouseEvent.PRESSED_LEFT
                clickStatuses[playerId].leftClicks ++
                clickStatuses[playerId].leftPath = new DragPath(this.mousePosition)
            case MouseEvent.PRESSED_RIGHT
                clickStatuses[playerId].rightClicks ++
                clickStatuses[playerId].rightPath = new DragPath(this.mousePosition)
            case MouseEvent.PRESSED_MIDDLE
                clickStatuses[playerId].middleClicks ++
                clickStatuses[playerId].middlePath = new DragPath(this.mousePosition)
            default

    /**Checks if a new button release was done after this one*/
    function clickStatus.isClicking(mousebuttontype wichButton) returns bool
        let subtractedTime = getElapsedGameTime() - MOUSE_BUTTON_RESET_TIME
        if wichButton == MOUSE_BUTTON_TYPE_LEFT
            return this.leftCD <= subtractedTime
        else if wichButton == MOUSE_BUTTON_TYPE_MIDDLE
            return this.middleCD <= subtractedTime
        else if wichButton == MOUSE_BUTTON_TYPE_RIGHT
            return this.rightCD <= subtractedTime
        return false

    /**Destroys the path and starts the counter reset based on the currentEvent*/
    function mouseStatus.onMouseReleased(int playerId)
        DragPath currentPath = null
        clickStatus clickStatus = clickStatuses[playerId]
        switch this.currentEvent
            case MouseEvent.RELEASED_LEFT
                clickStatus.leftCD = getElapsedGameTime()
                currentPath = clickStatus.leftPath
            case MouseEvent.RELEASED_RIGHT
                clickStatus.rightCD = getElapsedGameTime()
                currentPath = clickStatus.rightPath
            case MouseEvent.RELEASED_MIDDLE
                clickStatus.middleCD = getElapsedGameTime()
                currentPath = clickStatus.middlePath
            default
        clickStatuses[playerId] = clickStatus
        doAfter(MOUSE_BUTTON_RESET_TIME)->
            let buttonType = ConvertMouseButtonType((this.currentEvent castTo int) -3)
            if not clickStatus.isClicking(buttonType)
                playerFromIndex(playerId).setClickCounter(buttonType, 0)
        if currentPath != null
            currentPath.onButtonRelease(this.mousePosition)
            destroy currentPath //destroys the Path after releasing atm.

    class DragPoint
        let pathPoint = ZERO3
        construct(real x, real y, real z)
            pathPoint = vec3(x, y, z)
       
        construct(vec2 currentPosition, bool withTerrainZ)
            pathPoint = withTerrainZ ? currentPosition.withTerrainZ() : currentPosition.toVec3()

        construct(vec3 currentPosition)
            pathPoint = currentPosition
       
        function getVector() returns vec3
            return pathPoint

    /**The path drawn on the map*/    
    public class DragPath
        vec2 oldMousePosition
        LinkedList<DragPoint> path = new LinkedList<DragPoint>

        construct(vec2 currentMousePosition)
            path.add(new DragPoint(currentMousePosition, true))
            this.oldMousePosition = currentMousePosition
       
        /**returns true if a new point is set*/
        function addDragPoint(vec2 currentMousePosition)
            if currentMousePosition.distanceToSq(this.oldMousePosition) >= MOUSE_PATH_POINT_DISTANCE
                path.add(new DragPoint(currentMousePosition, true))
                this.oldMousePosition = currentMousePosition
       
        /**Returns a LinkedList of DragPoints where you can iterate over*/
        function getPath() returns LinkedList<DragPoint>
            return this.path

        /**Returns the specific DragPoint of index in the path*/
        function getIndexDragPoint(int index) returns DragPoint
            return path.get(index)

        function onButtonRelease(vec2 currentMousePosition)
            addDragPoint(currentMousePosition)
       
        ondestroy
            path.clear()

    /**This Class manages the mouse input to path setup*/      
    class ClickListener extends MouseListener

        override function onEvent(mouseStatus mouseStatus, int playerId)
            if  mouseStatus.currentEvent castTo int >= 7
                mouseStatus.onMouseMoved(playerId)
            else if mouseStatus.currentEvent castTo int >=4
                mouseStatus.onMouseReleased(playerId)
            else if mouseStatus.currentEvent castTo int >= 0
                mouseStatus.onMousePressed(playerId)
           
    init
        doAfter(0.02)->
            for p in ALL_PLAYERS
                clickStatuses[p.getId()] = clickStatus(0, 0, 0, null, null, null, 0.00, 0.00, 0.00)
        let mouseEvent = new ClickListener()
        EventListener.add(EVENT_PLAYER_MOUSE_DOWN, () -> mouseEvent.onMouseEvent())
        EventListener.add(EVENT_PLAYER_MOUSE_UP, () -> mouseEvent.onMouseEvent())
        EventListener.add(EVENT_PLAYER_MOUSE_MOVE, () -> mouseEvent.onMouseEvent())

     
    Code (WurstScript):

    package MouseUtils
    import ClosureEvents
    import ClosureTimers
    import public Players
    import public GameTimer

    /**
    This Util helps to handle the mouse natives and gives acces to all mouse informations outside mouse trigger events
    You can extend the functionallity by overriding the MouseEvents.onEvent function.
    */

    public enum MouseEvent
        STOPPED
        PRESSED_LEFT
        PRESSED_MIDDLE
        PRESSED_RIGHT
        RELEASED_LEFT
        RELEASED_MIDDLE
        RELEASED_RIGHT
        DRAGGED_LEFT
        DRAGGED_MIDDLE
        DRAGGED_RIGHT
        MOVED

    public constant MOUSE_MOVE_RESET_TIME = 1.00 / 24.00    // the time after mouse movement to set current event to stop (in seconds)

    /**mouseStatus(Most recent mouse event, Is the left button pressed, Is the right button pressed, Is the middle button pressed, Is the mouse moving, Current mouse pointer position on the map, Game time where the mouse started the move cooldown)*/
    public function player.getMouseStatus() returns mouseStatus
        return mouseStatuses[this.getId()]

    /**returns the mouse position as vec2 on the map*/
    public function player.getMousePosition() returns vec2
        return mouseStatuses[this.getId()].mousePosition

    /**The reversed function to ConvertMouseButtonType(i)*/
    public function mousebuttontype.convertMouseButtonIndex() returns int
        if this == MOUSE_BUTTON_TYPE_LEFT
            return 1
        if this == MOUSE_BUTTON_TYPE_MIDDLE
            return 2
        if this == MOUSE_BUTTON_TYPE_RIGHT
            return 3  
        return 0

    /**Shows the current Event in game*/
    public function mouseStatus.printCurrentEvent()
        Log.debug(MOUSE_EVENT_STRINGS[this.currentEvent castTo int])
       
    public tuple mouseStatus(MouseEvent currentEvent, boolean left, boolean right, boolean middle, boolean move, vec2 mousePosition, real moveCD)
    public mouseStatus array mouseStatuses
       
    constant string array MOUSE_EVENT_STRINGS = [
        "STOPPED",
        "PRESSED_LEFT",
        "PRESSED_MIDDLE",
        "PRESSED_RIGHT",
        "RELEASED_LEFT",
        "RELEASED_MIDDLE",
        "RELEASED_RIGHT",
        "DRAGGED_LEFT",
        "DRAGGED_MIDDLE",
        "DRAGGED_RIGHT",
        "MOVED"]

    EventListener firstEventListener = null

    public function addMouseListener(EventListener listener) returns EventListener
        if firstEventListener != null
            firstEventListener.prev = listener
            listener.next = firstEventListener

        firstEventListener = listener
        return listener

    public function removeMouseListener(EventListener listener)
        if firstEventListener == listener
            firstEventListener = null
        destroy listener
       
    /**Gives back the index of the EventList for this mousebutton*/
    function mousebuttontype.toEventListIndex(eventid whichEvent ) returns int
        var i = this.convertMouseButtonIndex()
        if whichEvent == EVENT_PLAYER_MOUSE_UP
            i += 3
        return i
    /**This Class is used to listen to mouse events and set the new values to the MouseStatuses*/
    public class MouseListener
        /**Overrite this function if you want to add more functionallity afterward*/
        function onEvent(mouseStatus mouseStatus, int playerId)
       
        function isMoving(real startTime) returns bool
            return (getElapsedGameTime() - MOUSE_MOVE_RESET_TIME) <= startTime

        function setMoveEvent(mouseStatus mouseStatus) returns MouseEvent
            let eventindex = mouseStatus.currentEvent castTo int
            if eventindex <= 3 and eventindex > 0
                return (eventindex + 6) castTo MouseEvent
            if eventindex <= 6
                return MouseEvent.MOVED
            return mouseStatus.currentEvent

        /**This function is called from the EventListener if you want to add more functionallity to the system use the onEvent(mouseStatus, playerId) function*/
        function onMouseEvent()
            let triggerPlayer = GetTriggerPlayer()
            let playerId = triggerPlayer.getId()
            var mouseStatus = mouseStatuses[playerId]
            let id = GetTriggerEventId()
            mouseStatus.mousePosition = vec2(BlzGetTriggerPlayerMouseX(),BlzGetTriggerPlayerMouseY())
           
            if id == EVENT_PLAYER_MOUSE_MOVE
                //prevent the event to fire inside a menu
                if mouseStatus.mousePosition.x != 0 or mouseStatus.mousePosition.y != 0
                    mouseStatus.move = true
                    mouseStatus.currentEvent = setMoveEvent(mouseStatus)
                    mouseStatus.moveCD = getElapsedGameTime()
                    doAfter(MOUSE_MOVE_RESET_TIME) ->
                        if not isMoving(mouseStatus.moveCD)
                            mouseStatus.currentEvent = MouseEvent.STOPPED
            else
                let mb = BlzGetTriggerPlayerMouseButton()
                mouseStatus.currentEvent = mb.toEventListIndex(id) castTo MouseEvent
                if mb == MOUSE_BUTTON_TYPE_LEFT
                    mouseStatus.left = (id == EVENT_PLAYER_MOUSE_DOWN)
                else if mb == MOUSE_BUTTON_TYPE_RIGHT
                    mouseStatus.right = (id == EVENT_PLAYER_MOUSE_DOWN)
                else if mb == MOUSE_BUTTON_TYPE_MIDDLE
                    mouseStatus.middle = (id == EVENT_PLAYER_MOUSE_DOWN)
            onEvent( mouseStatus, playerId)
            mouseStatuses[playerId] = mouseStatus
           
            var listener = firstEventListener
            while listener != null
                listener.onEvent()
                listener = listener.next
    init
        doAfter(0.02) ->
        for p in ALL_PLAYERS
            mouseStatuses[p.getId()] = mouseStatus(MouseEvent.STOPPED, false, false, false, false, ZERO2, 0.00)
     
     
    Last edited: Mar 25, 2019
  2. Chaosy

    Chaosy

    Joined:
    Jun 9, 2011
    Messages:
    10,567
    Resources:
    17
    Maps:
    1
    Spells:
    10
    Tutorials:
    6
    Resources:
    17
    There is way too much copy paste code here.

    if id == EVENT_PLAYER_MOUSE_DOWN
    if mb == MOUSE_BUTTON_TYPE_LEFT
    mouseStatus.currentEvent = MOUSE_PRESSED_LEFT
    mouseStatus.left = true
    else if mb == MOUSE_BUTTON_TYPE_RIGHT
    mouseStatus.currentEvent = MOUSE_PRESSED_RIGHT
    mouseStatus.right = true
    else if mb == MOUSE_BUTTON_TYPE_MIDDLE
    mouseStatus.currentEvent = MOUSE_PRESSED_MIDDLE
    mouseStatus.middle = true
    mouseStatus.mousePressed(playerId)
    else if id == EVENT_PLAYER_MOUSE_UP
    if mb == MOUSE_BUTTON_TYPE_LEFT
    mouseStatus.currentEvent = MOUSE_RELEASED_LEFT
    mouseStatus.left = false
    else if mb == MOUSE_BUTTON_TYPE_RIGHT
    mouseStatus.currentEvent = MOUSE_RELEASED_RIGHT
    mouseStatus.right = false
    else if mb == MOUSE_BUTTON_TYPE_MIDDLE
    mouseStatus.currentEvent = MOUSE_RELEASED_MIDDLE
    mouseStatus.middle = false

    With some hashmap usage you could remove all of these and do something like.

    mouseStatus.currentEvent = hash.get(mb)
    that'd save you quite a few rows as this method can be applied on more than one location in your code.

    Secondly, look into switch cases, they are way cleaner than a lot of else ifs

    I'd also reason that classes should be in their respective files.
     
  3. Frotty

    Frotty

    Wurst Reviewer

    Joined:
    Jan 1, 2009
    Messages:
    1,393
    Resources:
    11
    Models:
    3
    Tools:
    1
    Maps:
    5
    Tutorials:
    1
    Wurst:
    1
    Resources:
    11
    You didn't even incorporate all my feedback from your question thread..
     
  4. apsyll

    apsyll

    Joined:
    Aug 28, 2015
    Messages:
    184
    Resources:
    1
    Maps:
    1
    Resources:
    1
    I did this as much as possible sadly you can't use mousebuttontype as key but I just added a small function as a helper.


    I use this system on a map that uses the mouse as main input device due to this I need the possibility to create two paths simultaneously, so I really want to start for each mouse button a path on click and finish it at release.
    I do actually want to use the paths retroactive I just have added the destroy line on release because I don't have written a good alternative yet.

    This sounds actually like a good idea.
    I will keep this in mind and will overthink my approach of how I handle it atm...
    But I like at my system that all paths are now easily accessible via the player, so when I use it later for my systems I can use it "straight out of the box".

    this should be all fixed with the new update please let me know if I forgot something.

    Edit:
    Updated the code.
     
    Last edited: Mar 19, 2019
  5. Frotty

    Frotty

    Wurst Reviewer

    Joined:
    Jan 1, 2009
    Messages:
    1,393
    Resources:
    11
    Models:
    3
    Tools:
    1
    Maps:
    5
    Tutorials:
    1
    Wurst:
    1
    Resources:
    11
    Please add some sample usage code, maybe map. Also there are still quite a few remains of old names e.g. from the KeyUtils you cloned this from.
    In sense of publishing this as library or e.g. frentity PR, I think having KeyUtils import DragPath is the wrong way. MouseUtils, like KeyUtils, should be the lightweight lib, and the drag stuff should build on top imho.
    Right now DragPath can be used alone, but has zero function, and MouseUtils cannot used standalone, but has all the function.
    The API should be visible at the top of a package at first glance. Why is clickTuple in DragPath but mouseTuple not?

    Regarding conventions:
    • Don't name "tuples" "xxxTuple". Do you name your classes "XXXClass" ? I hope not. Use descriptive names. In KeyUtils I used "keyStatuses" for a reason.
    • I would rename
      MouseCurrentEvent
      to
      MouseEvent
      , the variable will indicate "current" in this case and e.g. if you save the enum values somewhere, the event is no more current.
    • comments like
      //          Core Code
      are not benefiting anyone I believe. Add some proper hotdoc and a small package doc with demo instead.

    Regarding lightweightness: Stuff like
    function mouseResetLoop()
    is probably unnecessary for most maps, because they don't need a specific MOUSE_STOPPED event.
    You can just save the timestamp (getElapsedGameTime()) and then add an extension func to the mouse status tuple that compares the timestamp to the current time, to determine whether or not it's still moving.
    The extension can then be user space (doAfter() -> isMoving()) or built ontop e.g. in DragPath. This library could be stripped down a lot for general purpose needs.

    If you use indices you don't need a hashmap. Just initialize a constant array.

    The same can be applied to
    MOUSE_BUTTON_TYPE_XXX
    btw. Add to a constant array and refer via index. Or use
    ConvertMouseButtonType

    This way you can get rid of some more ifs (of course don't overdo it, if it doesn't make sense).

    That's it for now. Make sure to explain more of your use case and rationale earlier in the future.
     
  6. apsyll

    apsyll

    Joined:
    Aug 28, 2015
    Messages:
    184
    Resources:
    1
    Maps:
    1
    Resources:
    1
    Updated the code again.
    My to do list for V.1.0:
    • Demo Code + Map
    • More Streamlining
    • Adding the Drag event properly so it returns when you press a button and move the mouse around.
     
    Last edited: Mar 28, 2019
  7. Frotty

    Frotty

    Wurst Reviewer

    Joined:
    Jan 1, 2009
    Messages:
    1,393
    Resources:
    11
    Models:
    3
    Tools:
    1
    Maps:
    5
    Tutorials:
    1
    Wurst:
    1
    Resources:
    11
    Just took a quick look, overall looks much better already:

    MOUSE_EVENT_LIST
    is unnecessary. just use
    castTo
    if you want to cast from int to enum and back.

    class MouseEvents
    is a terrible name. It implies plurality and collides with "MouseEvent". If it's a listener, name is "MouseListener" and then "ClickListener".
    It is also not used anywhere in the package and not explained.

    print("Left")
    ->
    Log.debug("Left")
    or "info". Using log makes the logging configurable by the user.

    All the "var"s e.g. in
    function onMouseEvent()
    should be "let"s if constant.

    vec2(0., 0.)
    ->
    ZERO2


    After this the core lib should be pretty slim
     
    Last edited: Mar 24, 2019