• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[Wurst] MouseUtils

Status
Not open for further replies.
Level 6
Joined
Aug 28, 2015
Messages
213
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.

Frotty
Chaosy


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.


Wurst:
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())
Wurst:
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:

Chaosy

Tutorial Reviewer
Level 40
Joined
Jun 9, 2011
Messages
13,183
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.
 
Level 6
Joined
Aug 28, 2015
Messages
213
With some hashmap usage you could remove all of these and do something like.

mouseStatus.currentEvent = hash.get(mb)
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.


You didn't even incorporate all my feedback from your question thread..
Okay, some more feedback then. I don't think you need all "DragPath leftPath, DragPath rightPath, DragPath middlePath" because once you hit another mouse button, the last path is over right? Or do you need retroactive access to the last path done with each button?
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.

I think you should just save all mouse events in a stack and then reconstruct a path from that as needed.

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".

Right now onMouseEvent is quite messy with many duplicated lines which should be cleaned up.
But even worse, why do you have 3 timers and doing nothing with them?! You are just doing a "doAfter", you need exactly 0 timers.


clickStatus.leftTimer.doAfter(resetTime) ->
should just be


doAfter(resetTime) ->
. Doing weird stuff like this for seemingly no purpose makes me wonder if you read the package hotdoc or online doc page WurstScript • Closure Timers.

Also stuff like this is bad style:


  • let vec = vec3(0, 0, 0)
    Use descriptive names, use ZERO3 for the zero length vector instead of vec3(0,0,0)
  • This function

    function onMouseStopMove(integer playerId)
    starts with "on" but isn't of callback nature. Also only used once. Inline it.
  • Use ternary if it makes sense, e.g. DragVec constructor

    vec = withTerrainZ ? v.withTerrainZ() : v.toVec3()
    .
  • This

    this.playerId = playerId
    should throw a warning. You are assigning a field to itself here, there is no parameter.
  • int and integer are fine even when I prefer int, but definitely don't mix both in the same file

  • function getPath() returns vec3 array
    why? You should just work on the list. Also why are you using HashList?
this should be all fixed with the new update please let me know if I forgot something.

Edit:
Updated the code.
 
Last edited:
Level 23
Joined
Jan 1, 2009
Messages
1,608
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.

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.

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.
 
Level 6
Joined
Aug 28, 2015
Messages
213
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:
Level 23
Joined
Jan 1, 2009
Messages
1,608
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:
Status
Not open for further replies.
Top