[vJASS] [Snippet] Mouse Utility

MyPad

Spell Reviewer
Level 20
Joined
May 9, 2014
Messages
1,624
This is a snippet that makes mouse coordinate detection easier for the user. It allows the user to determine the mouse's map coordinates in runtime without having to ever depend on events that the user would usually have to listen to. This also has the added bonus of returning an individual instance.

JASS:
library MouseUtils
/*
    -------------------
        MouseUtils
         - MyPad
         
         1.0.2.2
    -------------------
    
    ----------------------------------------------------------------------------
        A simple snippet that allows one to
        conveniently use the mouse natives
        as they were meant to be...
        
     -------------------
    |    API            |
     -------------------
    
        struct UserMouse extends array
            static method operator [] (player p) -> thistype
                - Returns the player's id + 1
                
            static method getCurEventType() -> integer
                - Returns the custom event that got executed.
                
            method operator player -> player
                - Returns Player(this - 1)
                
            readonly real mouseX
            readonly real mouseY
                - Returns the current mouse coordinates.
                
            readonly method operator isMouseClicked -> boolean
                - Determines whether any mouse key has been clicked,
                  and will return true on the first mouse key.
                  
            method isMouseButtonClicked(mousebuttontype mouseButton)
                - Returns true if the mouse button hasn't been
                  released yet.
            method setMousePos(real x, y) (introduced in 1.0.2.2)
                - Sets the mouse position for a given player.
                  
            static method registerCode(code c, integer ev) -> triggercondition
                - Lets code run upon the execution of a certain event.
                - Returns a triggercondition that can be removed later.
                
            static method unregisterCallback(triggercondition trgHndl, integer ev)
                - Removes a generated triggercondition from the trigger.
                
        functions:
            GetPlayerMouseX(player p) -> real
            GetPlayerMouseY(player p) -> real
                - Returns the coordinates of the mouse of the player.
                
            OnMouseEvent(code func, integer eventId) -> triggercondition
                - See UserMouse.registerCode
                
            GetMouseEventType() -> integer
                - See UserMouse.getCurEventType
                
            UnregisterMouseCallback(triggercondition t, integer eventId)
                - See UserMouse.unregisterCallback
            SetUserMousePos(player p, real x, real y)
                - See UserMouse.setMousePos
    
  Unique Global Constants:
   IMPL_LOCK (Introduced in v.1.0.2.2)
    - Enables or disables the lock option
     -------------------
    |    Credits        |
     -------------------
     
        -   Pyrogasm for pointing out a comparison logic flaw
            in operator isMouseClicked.
            
        -   Illidan(Evil)X for the useful enum handles that
            grant more functionality to this snippet.
        
        -   TriggerHappy for the suggestion to include 
            associated events and callbacks to this snippet.
            
        -   Quilnez for pointing out a bug related to the
            method isMouseButtonClicked not working as intended
            in certain situations.
            
    ----------------------------------------------------------------------------
*/
//  Arbitrary constants
globals
    constant integer EVENT_MOUSE_UP     = 1024
    constant integer EVENT_MOUSE_DOWN   = 2048
 constant integer EVENT_MOUSE_MOVE   = 3072
 
 private constant boolean IMPL_LOCK = false
endglobals
private module Init
    private static method onInit takes nothing returns nothing
        call thistype.init()
    endmethod
endmodule
struct UserMouse extends array
 static if IMPL_LOCK then
  //  Determines the minimum interval that a mouse move event detector
  //  will be deactivated. (Globally-based)
  //  You can configure it to any amount you like.
  private static constant real INTERVAL           = 0.031250000
  
  //  Determines how many times a mouse move event detector can fire
  //  before being deactivated. (locally-based)
  //  You can configure this to any integer value. (Preferably positive)
  private static constant integer MOUSE_COUNT_MAX  = 16
  
  // Determines the amount to be deducted from mouseEventCount
  // per INTERVAL. Runs independently of resetTimer
  private static constant integer MOUSE_COUNT_LOSS = 8
  private static constant boolean IS_INSTANT   = INTERVAL <= 0.
 endif
    private static integer currentEventType         = 0
    private static integer updateCount               = 0
    private static trigger stateDetector             = null
 static if IMPL_LOCK and not IS_INSTANT then
  private static timer resetTimer                 = null
 endif
    private static trigger array evTrigger
    
    private static integer array mouseButtonStack
 
 static if IMPL_LOCK and not IS_INSTANT then
  private integer  mouseEventCount
  private timer mouseEventReductor
 endif
    private thistype next
    private thistype prev
    
    private thistype resetNext
    private thistype resetPrev
    private trigger posDetector
 private integer mouseClickCount
    
    readonly real mouseX
    readonly real mouseY
    
    //  Converts the enum type mousebuttontype into an integer
    private static method toIndex takes mousebuttontype mouseButton returns integer
        return GetHandleId(mouseButton)
    endmethod
    
    static method getCurEventType takes nothing returns integer
        return currentEventType
    endmethod
    
    static method operator [] takes player p returns thistype
        if thistype(GetPlayerId(p) + 1).posDetector != null then
            return GetPlayerId(p) + 1
        endif
        return 0
    endmethod
        
    method operator player takes nothing returns player
        return Player(this - 1)
    endmethod
    method operator isMouseClicked takes nothing returns boolean
        return .mouseClickCount > 0
    endmethod
    method isMouseButtonClicked takes mousebuttontype mouseButton returns boolean
        return UserMouse.mouseButtonStack[(this - 1)*3 + UserMouse.toIndex(mouseButton)] > 0
 endmethod
    method setMousePos takes integer x, integer y returns nothing
        if GetLocalPlayer() == this.player then
            call BlzSetMousePos(x, y)
        endif
    endmethod
 static if IMPL_LOCK then
 private static method getMouseEventReductor takes timer t returns thistype
  local thistype this = thistype(0).next
  loop
   exitwhen this.mouseEventReductor == t or this == 0
   set this = this.next
  endloop
  return this
 endmethod
    private static method onMouseUpdateListener takes nothing returns nothing
        local thistype this = thistype(0).resetNext
        set updateCount     = 0
        
        loop
            exitwhen this == 0
            set updateCount = updateCount + 1
                        
            set this.mouseEventCount = 0
            call EnableTrigger(this.posDetector)
            
            set this.resetNext.resetPrev = this.resetPrev
            set this.resetPrev.resetNext = this.resetNext
            
            set this                     = this.resetNext
  endloop
  
  if updateCount > 0 then
   static if not IS_INSTANT then
    call TimerStart(resetTimer, INTERVAL, false, function thistype.onMouseUpdateListener)
   else
    call onMouseUpdateListener() 
   endif
  else
   static if not IS_INSTANT then
    call TimerStart(resetTimer, 0.00, false, null)
    call PauseTimer(resetTimer)
   endif
        endif
 endmethod
 
 private static method onMouseReductListener takes nothing returns nothing
  local thistype this  = getMouseEventReductor(GetExpiredTimer())
  if this.mouseEventCount <= 0 then
   call PauseTimer(this.mouseEventReductor)
  else
   set this.mouseEventCount = IMaxBJ(this.mouseEventCount - MOUSE_COUNT_LOSS, 0)
   call TimerStart(this.mouseEventReductor, INTERVAL, false, function thistype.onMouseReductListener)
  endif
 endmethod
 endif
    private static method onMouseUpOrDown takes nothing returns nothing
        local thistype this = thistype[GetTriggerPlayer()]
        local integer index = (this - 1)*3 + UserMouse.toIndex(BlzGetTriggerPlayerMouseButton())
        local boolean releaseFlag   = false
        
        if GetTriggerEventId() == EVENT_PLAYER_MOUSE_DOWN then
            set this.mouseClickCount    = IMinBJ(this.mouseClickCount + 1, 3)
            set releaseFlag          = UserMouse.mouseButtonStack[index] <= 0
            set UserMouse.mouseButtonStack[index]  = IMinBJ(UserMouse.mouseButtonStack[index] + 1, 1)
           
            if releaseFlag then
                set currentEventType = EVENT_MOUSE_DOWN
                call TriggerEvaluate(evTrigger[EVENT_MOUSE_DOWN])
            endif
        else
            set this.mouseClickCount = IMaxBJ(this.mouseClickCount - 1, 0)
            set releaseFlag          = UserMouse.mouseButtonStack[index] > 0
            set UserMouse.mouseButtonStack[index]  = IMaxBJ(UserMouse.mouseButtonStack[index] - 1, 0)
            
            if releaseFlag then
                set currentEventType = EVENT_MOUSE_UP
                call TriggerEvaluate(evTrigger[EVENT_MOUSE_UP])
            endif
        endif
    endmethod
    
    private static method onMouseMove takes nothing returns nothing
  local thistype this   = thistype[GetTriggerPlayer()]
  local boolean started  = false
                
        set this.mouseX      = BlzGetTriggerPlayerMouseX()
        set this.mouseY      = BlzGetTriggerPlayerMouseY()
  
  static if IMPL_LOCK then
   set this.mouseEventCount  = this.mouseEventCount + 1
   if this.mouseEventCount <= 1 then
    call TimerStart(this.mouseEventReductor, INTERVAL, false, function thistype.onMouseReductListener)
   endif
  endif
  set currentEventType   = EVENT_MOUSE_MOVE
  call TriggerEvaluate(evTrigger[EVENT_MOUSE_MOVE])  
  static if IMPL_LOCK then
   if this.mouseEventCount >= thistype.MOUSE_COUNT_MAX then
    call DisableTrigger(this.posDetector)                  
   
    if thistype(0).resetNext == 0 then
     static if not IS_INSTANT then
      call TimerStart(resetTimer, INTERVAL, false, function thistype.onMouseUpdateListener)
      // Mouse event reductor should be paused
     else
      set started  = true
     endif
     call PauseTimer(this.mouseEventReductor)
    endif
    
    set this.resetNext              = 0
    set this.resetPrev              = this.resetNext.resetPrev
    set this.resetPrev.resetNext    = this
    set this.resetNext.resetPrev    = this  
      
    if started then
     call onMouseUpdateListener()
    endif
   endif
  endif
    endmethod
        
    private static method init takes nothing returns nothing
        local thistype this = 1
        local player p      = this.player
  
  static if IMPL_LOCK and not IS_INSTANT then
   set resetTimer  = CreateTimer()
  endif
        set stateDetector   = CreateTrigger()
        
        set evTrigger[EVENT_MOUSE_UP]   = CreateTrigger()
        set evTrigger[EVENT_MOUSE_DOWN] = CreateTrigger()
        set evTrigger[EVENT_MOUSE_MOVE] = CreateTrigger()
        
        call TriggerAddCondition( stateDetector, Condition(function thistype.onMouseUpOrDown))
        loop
            exitwhen integer(this) > bj_MAX_PLAYER_SLOTS
            
            if GetPlayerController(p) == MAP_CONTROL_USER and GetPlayerSlotState(p) == PLAYER_SLOT_STATE_PLAYING then
                set this.next             = 0
                set this.prev             = thistype(0).prev
                set thistype(0).prev.next = this
                set thistype(0).prev      = this
                
                set this.posDetector         = CreateTrigger()
                static if IMPL_LOCK and not IS_INSTANT then
                    set this.mouseEventReductor  = CreateTimer()
                endif
                call TriggerRegisterPlayerEvent( this.posDetector, p, EVENT_PLAYER_MOUSE_MOVE )
                call TriggerAddCondition( this.posDetector, Condition(function thistype.onMouseMove))                
                
                call TriggerRegisterPlayerEvent( stateDetector, p, EVENT_PLAYER_MOUSE_UP )
                call TriggerRegisterPlayerEvent( stateDetector, p, EVENT_PLAYER_MOUSE_DOWN )
            endif
            
            set this = this + 1
            set p    = this.player
        endloop
    endmethod
    
    static method registerCode takes code handlerFunc, integer eventId returns triggercondition
        return TriggerAddCondition(evTrigger[eventId], Condition(handlerFunc))
    endmethod
    
    static method unregisterCallback takes triggercondition whichHandler, integer eventId returns nothing
        call TriggerRemoveCondition(evTrigger[eventId], whichHandler)
    endmethod
    
    implement Init
endstruct
function GetPlayerMouseX takes player p returns real
    return UserMouse.mouseX
endfunction
function GetPlayerMouseY takes player p returns real
    return UserMouse.mouseY
endfunction
function OnMouseEvent takes code func, integer eventId returns triggercondition
    return UserMouse.registerCode(func, eventId)
endfunction
function GetMouseEventType takes nothing returns integer
    return UserMouse.getCurEventType()
endfunction
function UnregisterMouseCallback takes triggercondition whichHandler, integer eventId returns nothing
    call UserMouse.unregisterCallback(whichHandler, eventId)
endfunction
function SetUserMousePos takes player p, integer x, integer y returns nothing
    call UserMouse.setMousePos(x, y)
endfunction
endlibrary
 
Last edited:
Level 22
Joined
Feb 27, 2007
Messages
3,784
Cool snippet. There are some choices I don't understand, though:
  • Is the mouse move event really that inefficient that even with a relatively small amount of code to run (onMouseMove) you want to disable it after 4 move events in rapid succession? Why did you feel this was necessary? Why is 4 events the number? Why resume all instances simultaneously with a global timer? Why is the lockout 0.03? I'd just like to know more about this in general.

  • You're using the 0th instance as kind of a 'data holding' instance, right? I bumbled through the parts that use resetNext/Prev and eventually found this to be really confusing:
    JASS:
    set this.resetNext              = 0
    set this.resetPrev              = this.resetNext.resetPrev
    set this.resetPrev.resetNext    = this
    set this.resetNext.resetPrev    = this
    
    //would be clearer as
    set this.resetNext              = thistype(0) //I thought initially this 0 was a 'nothing' 0 not a 'zeroth instance', which made the next line seem useless
    set this.resetPrev              = thistype(0).resetPrev
    set thistype(0).resetNext       = this
    set thistype(0).resetPrev       = this

  • Your mouse click counting logic seems backward based on the output of isMouseClicked. And for that matter what exactly is that method supposed to tell you? It seems to me it would return true when a player is holding down the button but has not released it to 'finish' the click.
    JASS:
    if GetTriggerEventId() == EVENT_PLAYER_MOUSE_UP then
         set this.mouseClickCount = this.mouseClickCount + 1  //goes up by 1 when button is released
    else
         set this.mouseClickCount = this.mouseClickCount - 1  //goes down by 1 when button is pressed
    endif
    
    method operator isMouseClicked takes nothing returns boolean
        return .mouseClickCount > 0 //as per above logic the value of this variable is always 0 .. -1 .. 0 .. -1
    endmethod
 

MyPad

Spell Reviewer
Level 20
Joined
May 9, 2014
Messages
1,624
Is the mouse move event really that inefficient that even with a relatively small amount of code to run (onMouseMove) you want to disable it after 4 move events in rapid succession? Why did you feel this was necessary? Why is 4 events the number? Why resume all instances simultaneously with a global timer? Why is the lockout 0.03? I'd just like to know more about this in general.

When I made the snippet, I was using BJDebugMsg("") in it. Having to see a lot of messages caused the game to suffer, so I thought that it might become troublesome/resource intensive at any point, and chose to implement it such that at most 4 mouse move events can fire per a given interval, which is 0.03 by default.

The reason why all instances that have reached the global cap are resumed simultaneously, is to mimic the deterministic behavior of the game, making sure that all instances are synced at the same time.

I did not really test out the snippet when I uploaded it, but looking back at it now, I would say that the mouse move event is not really inefficient, just that DisplayTextToPlayers clutter up the game and the log and slow it down upon every time the mouse is moved. I will clarify with an update on this that the lockout and the number of move events allowed per interval are configurable, but set to safe values by default.

I might have missed out a question, so I may reply in another message.

Your mouse click counting logic seems backward based on the output of isMouseClicked. And for that matter what exactly is that method supposed to tell you? It seems to me it would return true when a player is holding down the button but has not released it to 'finish' the click.

Oops. That was a nice logical catch. I suppose I can change the conditional expression to be more reasonable. Before all else, I interpret clicking as the moment a mouse button has been depressed or EVENT_PLAYER_MOUSE_DOWN. Since one cannot know exactly which mouse button was clicked, I had to go with the method a'la counter. When the counter is greater than 1, the snippet will tell you that at least one button has been clicked and not released yet.
 
Since one cannot know exactly which mouse button was clicked
You very much can.
JASS:
type mousebuttontype    extends     handle

constant native ConvertMouseButtonType      takes integer i returns mousebuttontype

constant mousebuttontype    MOUSE_BUTTON_TYPE_LEFT          = ConvertMouseButtonType(1)
constant mousebuttontype    MOUSE_BUTTON_TYPE_MIDDLE        = ConvertMouseButtonType(2)
constant mousebuttontype    MOUSE_BUTTON_TYPE_RIGHT         = ConvertMouseButtonType(3)

native BlzGetTriggerPlayerMouseButton              takes nothing returns mousebuttontype
 
Level 22
Joined
Feb 27, 2007
Messages
3,784
Oh okay cool I get where you were coming from with the lockout now. You addressed everything I was wondering about, will have to mess with it now. Glad to see I even made the credits list with that catch :]


Something else I forgot to ask above: why write your onInit in a module like that? Habit from larger projects? If it were complex or involved some other part of the code in the init process I’d get it but you actually typed more letters to do the same thing. If you had just named your struct’s Init method onInit instead you wouldn’t have needed to write the module and implement it since all onInit does is call Init!

<insert blah blah about extra code overhead from JASSHelper that nobody functionally has to worrying about here>
 

MyPad

Spell Reviewer
Level 20
Joined
May 9, 2014
Messages
1,624
You very much can.
JASS:
type mousebuttontype    extends    handle

constant native ConvertMouseButtonType      takes integer i returns mousebuttontype

constant mousebuttontype    MOUSE_BUTTON_TYPE_LEFT          = ConvertMouseButtonType(1)
constant mousebuttontype    MOUSE_BUTTON_TYPE_MIDDLE        = ConvertMouseButtonType(2)
constant mousebuttontype    MOUSE_BUTTON_TYPE_RIGHT         = ConvertMouseButtonType(3)

native BlzGetTriggerPlayerMouseButton              takes nothing returns mousebuttontype

Well, isn't that quite a catch? Syntax highlighter keeps hiding those stuff which could really have proven useful here.
Looks like I have some more work to do.

Something else I forgot to ask above: why write your onInit in a module like that?

In writing up libraries, I find it more convenient to have an initialization module that calls a specific function named init. That way, all I have to do is just implement the module below the init function, and now I have a module initialization function. This has become my habit with certain structs as of late.
 

TriggerHappy

Spell Moderator
Level 38
Joined
Jun 23, 2007
Messages
4,028
If you are trying to make an API which allows more convenient usage of the mouse natives I would suggest the following:

JASS:
// These should return a cached value
function GetPlayerMouseX takes player p returns real
function GetPlayerMouseY takes player p returns real

// Simply add the boolexpr to a global trigger and return the result of TriggerAddCondition
function OnMouseMove takes boolexpr func returns triggercondition
function OnMouseClick takes boolexpr func, boolean up returns triggercondition

// Ability to remove callbacks (TriggerRemoveCondition)
function RemoveMouseEventCallback takes triggercondition callback returns nothing

Keeping your struct syntax would be fine but I'd really like normal JASS API too.

I also don't think there should be any need for the lockout. It might be useful as an optional feature though.
 

MyPad

Spell Reviewer
Level 20
Joined
May 9, 2014
Messages
1,624
Was too busy last week, so I only got around to update it now.
The lockout behavior is going to be kept, just in case anyone used the previous version.

The attached map demonstrates a sample of what the snippet can allow one to perform.
  • JASS Syntax has been added to the next version, along with suggested functions.
  • New integer constants have been introduced for callback-related functions, EVENT_MOUSE_UP, EVENT_MOUSE_DOWN, and EVENT_MOUSE_MOVE.
 

Attachments

  • Mouse Utilities.w3x
    21.8 KB · Views: 227
Level 22
Joined
Feb 27, 2007
Messages
3,784
Okay I've been messing with this for a bit and I have some... gripes/confusion about the lockout you implemented.
  • The lockout is based on sheer number of inputs, not inputs in a given time. This means that if I move for 15 inputs (one less than the default lockout number) and then stop moving my mouse, the next time I move it I'm instantly locked out for the duration after one move event even though I'm clearly not overloading the system with input. A method where inputs with a frequency higher than X would be ignored seems to make more logical sense.

  • Setting the lockout time to 0 doesn't instantly re-enable the mouse tracking. I do not understand why this should be since the 0.00 timer callback is a frequent method method to run new threads and it definitely does not delay anything in those situations. I tested this by destroying/creating an effect on the mouse position and then whipping my mouse around the screen at a relatively constant rate. There are visible gaps in the pattern of created effects (not because I went faster there) that appear regularly as they should after the appropriate number of move events to fire the lockout. I tested by commenting out the lockout if block entirely and the resulting effects did not have any visible gaps. I'm really confused about this.

  • Having a static if block to enable/disable the lockout entirely seems a good addition. Is this code really so taxing that having it on constantly could be an issue? I am aware I can effectively create this by setting the lockout count to something ridiculously high.

Additional comment: Moving the camera field with the arrow keys does not trigger mouse move events even though the coordinates of the mouse are changing. I know there's nothing you can do about this but it seems like an issue Blizzard should fix with the mouse move natives. Scrolling the camera field also doesn't fire mouse move. This also sounds like a pain in the butt to solve and involves hooking into all the camera functions.
 

MyPad

Spell Reviewer
Level 20
Joined
May 9, 2014
Messages
1,624
Released v.1.0.2.2 (Apologies for the messed up indenting.)

The lockout is based on sheer number of inputs, not inputs in a given time.

The new version addresses this with a timer per player, which decreases the likelihood of locking within a given amount of time by decrementing the total count with a new MOUSE_COUNT_LOSS variable.

Setting the lockout time to 0 doesn't instantly re-enable the mouse tracking.

This will hopefully be addressed in the new version.

Having a static if block to enable/disable the lockout entirely seems a good addition. Is this code really so taxing that having it on constantly could be an issue? I am aware I can effectively create this by setting the lockout count to something ridiculously high.


I had gripes on having to introduce static ifs to this, but got around to it. The new version now has IMPL_LOCK, shorthand for implement lock (or implicit lock).

Additional comment: Moving the camera field with the arrow keys does not trigger mouse move events even though the coordinates of the mouse are changing. I know there's nothing you can do about this but it seems like an issue Blizzard should fix with the mouse move natives. Scrolling the camera field also doesn't fire mouse move. This also sounds like a pain in the butt to solve and involves hooking into all the camera functions.


Yep, kinda tricky to implement. There is this native which could prove to be of use to others (BlzSetMousePos). The new version includes a method that uses that function.
 
Top