1. The poll for our 11th Music Contest is up! Help us choose the most awesome cinematic tracks by casting a vote!
    Dismiss Notice
  2. Melee Mapping contest #3 - Poll is up! Vote for the best 4v4 melee maps!
    Dismiss Notice
  3. The 30th edition of the Modeling Contest is finally up! The Portable Buildings need your attention, so come along and have a blast!
    Dismiss Notice
  4. The Aftermath has been revealed for the 19th Terraining Contest! Be sure to check out the Results and see what came out of it.
    Dismiss Notice

[vJASS] [Snippet] Mouse Utility

Discussion in 'JASS Resources' started by MyPad, Nov 28, 2018.

  1. MyPad

    MyPad

    Spell Reviewer

    Joined:
    May 9, 2014
    Messages:
    1,137
    Resources:
    3
    Models:
    1
    Icons:
    1
    JASS:
    1
    Resources:
    3
    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.

    Code (vJASS):

    library MouseUtils
    /*
        -------------------
            MouseUtils
             - MyPad
           
             1.0.2.0
        -------------------
       
        ----------------------------------------------------------------------------
            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.
                     
                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
                   
         -------------------
        |    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.
               
        ----------------------------------------------------------------------------
    */

    //  Arbitrary constants
    globals
        constant integer EVENT_MOUSE_UP     = 1024
        constant integer EVENT_MOUSE_DOWN   = 2048
        constant integer EVENT_MOUSE_MOVE   = 3072
    endglobals
    private module Init
        private static method onInit takes nothing returns nothing
            call thistype.init()
        endmethod
    endmodule
    struct UserMouse extends array
        //  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.03125
       
        //  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
       
        private static integer currentEventType         = 0
        private static integer updateCount              = 0
        private static timer resetTimer                 = null
        private static trigger stateDetector            = null
       
        private static trigger array evTrigger
       
        private static integer array mouseButtonStack
       
        private integer  mouseEventCount
       
        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
        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
                call PauseTimer(resetTimer)
            endif
        endmethod
       
        private static method onMouseUpOrDown takes nothing returns nothing
            local thistype this = thistype[GetTriggerPlayer()]
            local integer index = (this - 1)*3 + UserMouse.toIndex(BlzGetTriggerPlayerMouseButton())
            if GetTriggerEventId() == EVENT_PLAYER_MOUSE_DOWN then
                set this.mouseClickCount    = this.mouseClickCount + 1
                set UserMouse.mouseButtonStack[index]  = UserMouse.mouseButtonStack[index] + 1
             
                set currentEventType = EVENT_MOUSE_DOWN
                call TriggerEvaluate(evTrigger[EVENT_MOUSE_DOWN])
            else      
                set this.mouseClickCount = this.mouseClickCount - 1
                set UserMouse.mouseButtonStack[index]  = UserMouse.mouseButtonStack[index] - 1
                set currentEventType = EVENT_MOUSE_UP
                call TriggerEvaluate(evTrigger[EVENT_MOUSE_UP])
            endif
        endmethod
       
        private static method onMouseMove takes nothing returns nothing
            local thistype this = thistype[GetTriggerPlayer()]
                   
            set this.mouseX     = BlzGetTriggerPlayerMouseX()
            set this.mouseY     = BlzGetTriggerPlayerMouseY()
                   
            set this.mouseEventCount = this.mouseEventCount + 1
            set currentEventType = EVENT_MOUSE_MOVE
            call TriggerEvaluate(evTrigger[EVENT_MOUSE_MOVE])
            if this.mouseEventCount >= thistype.MOUSE_COUNT_MAX then
                call DisableTrigger(this.posDetector)                
           
                if thistype(0).resetNext == 0 then
                    call TimerStart(resetTimer, INTERVAL, true, function thistype.onMouseUpdateListener)
                endif
               
                set this.resetNext              = 0
                set this.resetPrev              = this.resetNext.resetPrev
                set this.resetPrev.resetNext    = this
                set this.resetNext.resetPrev    = this
            endif
        endmethod
           
        private static method init takes nothing returns nothing
            local thistype this = 1
            local player p      = this.player
           
            set resetTimer      = CreateTimer()
            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()
                    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[p].mouseX
    endfunction
    function GetPlayerMouseY takes player p returns real
        return UserMouse[p].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
    endlibrary
     
     
    Last edited: Dec 11, 2018
  2. Pyrogasm

    Pyrogasm

    Joined:
    Feb 27, 2007
    Messages:
    1,761
    Resources:
    0
    Resources:
    0
    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:
      Code (vJASS):
      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.
      Code (vJASS):
      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
     
  3. MyPad

    MyPad

    Spell Reviewer

    Joined:
    May 9, 2014
    Messages:
    1,137
    Resources:
    3
    Models:
    1
    Icons:
    1
    JASS:
    1
    Resources:
    3
    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.

    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.
     
  4. Illidan(Evil)X

    Illidan(Evil)X

    Joined:
    Oct 24, 2004
    Messages:
    645
    Resources:
    150
    Models:
    109
    Icons:
    27
    Skins:
    2
    Maps:
    12
    Resources:
    150
    You very much can.
    Code (vJASS):
    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
     
  5. Pyrogasm

    Pyrogasm

    Joined:
    Feb 27, 2007
    Messages:
    1,761
    Resources:
    0
    Resources:
    0
    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>
     
  6. MyPad

    MyPad

    Spell Reviewer

    Joined:
    May 9, 2014
    Messages:
    1,137
    Resources:
    3
    Models:
    1
    Icons:
    1
    JASS:
    1
    Resources:
    3
    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.

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

    TriggerHappy

    Code Moderator

    Joined:
    Jun 23, 2007
    Messages:
    3,538
    Resources:
    22
    Spells:
    11
    Tutorials:
    2
    JASS:
    9
    Resources:
    22
    If you are trying to make an API which allows more convenient usage of the mouse natives I would suggest the following:

    Code (vJASS):

    // 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.
     
  8. MyPad

    MyPad

    Spell Reviewer

    Joined:
    May 9, 2014
    Messages:
    1,137
    Resources:
    3
    Models:
    1
    Icons:
    1
    JASS:
    1
    Resources:
    3
    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.
     

    Attached Files:

  9. TriggerHappy

    TriggerHappy

    Code Moderator

    Joined:
    Jun 23, 2007
    Messages:
    3,538
    Resources:
    22
    Spells:
    11
    Tutorials:
    2
    JASS:
    9
    Resources:
    22
    Tested and works properly. This is a very convenient way to start using the mouse natives without creating any boilerplate yourself.

    Approved.