• 🏆 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!

[vJASS] [Snippet] Mouse Utility

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.3.1
    -------------------
    
    ----------------------------------------------------------------------------
        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.
        -   Lt_Hawkeye for pointing out a bug related to the
            registration of callback functions not working
            as intended. (The triggers that would hold these
            functions did not exist yet at those moments)
            
    ----------------------------------------------------------------------------
*/
//  Arbitrary constants
globals
    constant integer EVENT_MOUSE_UP     = 0x400
    constant integer EVENT_MOUSE_DOWN   = 0x800
    constant integer EVENT_MOUSE_MOVE   = 0xC00
    //  Introduced in v1.0.2.3
    //  Commented out in v1.0.2.4
    // private constant real STARTUP_DELAY = 0.00
    // private constant boolean NO_DELAY   = false
    //  Introduced in v1.0.2.2
    private constant boolean IMPL_LOCK  = true
endglobals
private module Init
    private static method invokeTimerInit takes nothing returns nothing
        call PauseTimer(GetExpiredTimer())
        call DestroyTimer(GetExpiredTimer())
        call thistype.timerInit()
    endmethod
    private static method onInit takes nothing returns nothing
        set evTrigger[EVENT_MOUSE_UP]   = CreateTrigger()
        set evTrigger[EVENT_MOUSE_DOWN] = CreateTrigger()
        set evTrigger[EVENT_MOUSE_MOVE] = CreateTrigger()
        call TimerStart(CreateTimer(), 0.00, false, function thistype.invokeTimerInit)
    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
        private integer  mouseEventCount
        private timer mouseEventReductor
    endif
    private static trigger array evTrigger
    private static integer array mouseButtonStack
 
    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 initCallback 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()
        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
    
    private static method timerInit takes nothing returns nothing
        call thistype.initCallback()
    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
function SetUserMousePos takes player p, integer x, integer y returns nothing
    call UserMouse[p].setMousePos(x, y)
endfunction
endlibrary

  • v1.0.0 - Release

  • v1.1.0 - Changed GetPlayerMouseX, GetPlayerMouseY and SetUserMousePos to actually take a player parameter.
    • Fixed the formatting of the script from the previous version.
    • Unfortunately, this changelog was created in this version, and previous versions were written at least 2 years ago, so I cannot track down the changes made in between these versions.
  • v1.2.0 - Added an option to initialize the system at a later point than usual, to hopefully address occasional critical errors regarding mouse events.

  • v1.3.0 - Removed the option to initialize the system at the module initialization phase, defaulting to the timer based initialization.
    • The system can no longer be initialized at any point after the 0-second timer phase. However, it will still initialize at the moment the game has started (0-second timer expiration after map init).
  • v1.3.1- Now allows event registration at the map initialization phase.
    • To clarify, the rest of the system will still initialize upon the start of the game.
    • Changed version setup within the changelog.
      • v.1.0.2.2 -> v.1.1.0
      • v.1.0.2.3 -> v.1.2.0
      • v.1.0.2.4 -> v.1.3.0
 
Last edited:
Level 38
Joined
Feb 27, 2007
Messages
4,951
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
 
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 38
Joined
Feb 27, 2007
Messages
4,951
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>
 
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.
 
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.
 
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: 311
Level 38
Joined
Feb 27, 2007
Messages
4,951
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.
 
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.
 
This snippet seems to cause people to randomly get critical errors on load. When I implemented it into my map for a spell, one or two people(sometimes more) would always randomly drop with a critical error before loading into the game. I know this snippet was responsible because once i disabled it and nulled this line of code from my spell(disabling the mouse functions; it was also the only line in the map where i used the snippet):

vJASS:
//set data.a = bj_RADTODEG * Atan2(GetPlayerMouseY(data.p) - data.y1, GetPlayerMouseX(data.p) - data.x1)

I called it every .03125 seconds in a timer loop; but i think that should be supported by the snippet and the spell worked perfectly fine even with mouseutils enabled; the only issue was people were randomly getting critical errors on load.
 
Last edited:
Sure.

Here is the working stable version where I disabled the snippet and said lines of code: https://drive.google.com/file/d/1KDuxeQc6CkK4VeGsr2pxc8VTYlPMQ4TZ/view?usp=sharing

Here is the one that causes people to crash on load: https://drive.google.com/file/d/1QD283vv8Q6z2vveso1w-nm714taBLZWM/view?usp=sharing

Only difference is MouseUtils being disabled and said nulled lines of code.

You can find MouseUtils in Libraries folder. Trigger called MouseUtils.

And you can find the lines of codes i nulled in Spells>Items Folder. Trigger called LAAAAASERRRS.

My theory it's related to some kind of desync on map init with GetLocalPlayer in your snippet, if that's what your using, though I admit I didn't study your snippet super well. It affects players randomly, regardless of player slot choice.
 
Last edited:
From a few tests here and there, I might've figured out a reason why the snippet caused these critical errors; creating the mouse events before the game actually starts (around the 0 second timer mark) causes the associated listening triggers to intercept requests from the game to change the position of the mouse. I suppose it wouldn't have been too much of a problem at the time this snippet was written, but that has led to the following update:

JASS:
library MouseUtils
/*
    -------------------
        MouseUtils
         - MyPad
         
         1.0.2.3
    -------------------
    
    ----------------------------------------------------------------------------
        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     = 0x400
    constant integer EVENT_MOUSE_DOWN   = 0x800
    constant integer EVENT_MOUSE_MOVE   = 0xC00
    //  Introduced in v1.0.2.3
    private constant real STARTUP_DELAY = 0.00
    private constant boolean NO_DELAY   = false
    //  Introduced in v1.0.2.2
    private constant boolean IMPL_LOCK  = true
endglobals
private module Init
    static if not NO_DELAY then
    private static method invokeTimerInit takes nothing returns nothing
        call PauseTimer(GetExpiredTimer())
        call DestroyTimer(GetExpiredTimer())
        call thistype.timerInit()
    endmethod
    endif
    private static method onInit takes nothing returns nothing
        static if not NO_DELAY then
            local timer temp    = CreateTimer()
            static if thistype.timerInitDelay.exists then
                call TimerStart(temp, thistype.timerInitDelay(), false, function thistype.invokeTimerInit)
            else
                call TimerStart(temp, 0.0, false, function thistype.invokeTimerInit)
            endif
            set temp            = null
        else
            call thistype.init()
        endif
    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
        private integer  mouseEventCount
        private timer mouseEventReductor
    endif
    private static trigger array evTrigger
    private static integer array mouseButtonStack
 
    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
            call BJDebugMsg("Mouse moved: " + I2S(this.mouseEventCount))
            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 initCallback 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 if NO_DELAY then
    private static method init takes nothing returns nothing
        call thistype.initCallback()
    endmethod
    else
    private static constant method timerInitDelay takes nothing returns real
        return STARTUP_DELAY
    endmethod
    private static method timerInit takes nothing returns nothing
        call thistype.initCallback()
    endmethod
    endif
    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
function SetUserMousePos takes player p, integer x, integer y returns nothing
    call UserMouse[p].setMousePos(x, y)
endfunction
endlibrary

The difference between this release and the most recent version so far is the ability to initialize the script at a later point in the game. Hopefully, with a timer-based initialization flow, the scenario above might be averted.
 
I think so. Try 0.01 instead. That's the value I went with while testing the snippet.
Tried that; still crash. And worse than 1.0.2.2. :\
Steps to reproduce: Move the mouse around in loading screen. Doesn't always crash you but will increase the likeliness.
For some reason you never crash when testing maps in single player though.
 
Level 20
Joined
Jul 10, 2009
Messages
474
Adding the event TriggerRegisterPlayerEvent(trigger, player, EVENT_PLAYER_MOUSE_MOVE ) to a trigger at map init has a huge chance to produce a desync upon any player moving the mouse during loading screen, even when no action is added to the trigger. The desync can lead to a subsequent crash (for some players).
To my knowledge, the game begins syncing that stuff after it finishes loading.

-> Creating the triggers at 0. time elapsed should fix the issue.

See this thread for further information.
@Teo_omant was so kind to report the reason for the crash.
 
Level 13
Joined
Oct 18, 2013
Messages
690
Whats with this?
method operator player -> player
- Returns Player(this - 1)
Player(0), which is red returns -1. Why?
 
Whats with this?
method operator player -> player
- Returns Player(this - 1)
Player(0), which is red returns -1. Why?
The static method operator returns the player ID value, plus 1 as a UserMouse instance. UserMouse[Player(0)].player will return the player itself, not Player(-1).

I included it as a convenience method if the user decides to use the UserMouse object instead, and allow a two-way conversion between player handles and UserMouse instances.

Tried that; still crash. And worse than 1.0.2.2. :\
Steps to reproduce: Move the mouse around in loading screen. Doesn't always crash you but will increase the likeliness.
For some reason you never crash when testing maps in single player though.
That's unfortunate to hear. So far, with NO_DELAY set to true, I've experienced some of the wc3 game windows crashing while loading in LAN multiplayer. However, I've yet to encounter a crash with NO_DELAY set to false in the same test environment.
 
Level 20
Joined
Jul 10, 2009
Messages
474
That's unfortunate to hear. So far, with NO_DELAY set to true, I've experienced some of the wc3 game windows crashing while loading in LAN multiplayer. However, I've yet to encounter a crash with NO_DELAY set to false in the same test environment.
As mentioned, just calling TriggerRegisterPlayerEvent(trigger, player, EVENT_PLAYER_MOUSE_MOVE) at game start instead of map init should fix the issue ;)
Same goes for the other mouse events.
 
Level 11
Joined
Jul 4, 2016
Messages
626
So, I tried using the version of the snippet from the opening post, buts its not working when trying to register a mouse event. So, I tested it by pasting the version from the OP into the map example (since this is working) and that also stopped working.
I also put some display messages to make sure the init module was actually firing and it was.

It appears the current version does not work.
 
Quite the odd occurrence there. I also tested the script and found out that I forgot to factor in the possibility that other systems will likely subscribe to the events during the module, struct, library or scope initialization phase. To fix this, I moved the initialization of the trigger callbacks "evTrigger" into the module initializer itself, not the timer initialization function.

Updated the snippet to allow proper event registration during the initialization phase.
 
Is there a way to manually trigger update current mouse-to-world-position for a player?
I don't want to move the on-screen-mouse-location, just the stored mouse world-position.

Background: I have a unit-locked-camera, a dash ability (moving unit towards mouse-location over a short time) and an attack (fire projectile towards mouse-location).
If you hold the mouse still close to the unit in front of the character, dash, then attack, the attack will go backwards due to the mouse-location has not been updated by moving the mouse, but the camera has moved, making the player think it will attack forward (towards the new mouse world-location, after the camera moved).
 
Level 38
Joined
Feb 27, 2007
Messages
4,951
I commented on that problem 3 years ago and unfortunately there isn't a good solution, really.
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.
JASS:
function SetUserMousePos takes player p, integer x, integer y returns nothing
    call UserMouse[p].setMousePos(x, y)
endfunction
You can't manually update the mouse position because the natives that return the mouse coordinates (BlzGetTriggerPlayerMouseX/Y()) are event responses and so only have values when the trigger itself is actually... triggered. Your only real solution is to manually set the mouse position to be in front of the unit after the dash. It will probably be necessary to check that the player didn't actually move their mouse during the dash (in which case they might really want to attack backwards). If they didn't move their mouse, set the new mouse position to be a little in front of the unit? Honestly seems like a solution with problems.

I can't imagine it's the best option but maybe Trackables would help you here, since those probably activate when the camera is repositioned to move the mouse over them without the mouse moving. But Trackables are permanent and can't be removed so....
 
I commented on that problem 3 years ago and unfortunately there isn't a good solution, really.


JASS:
function SetUserMousePos takes player p, integer x, integer y returns nothing
    call UserMouse[p].setMousePos(x, y)
endfunction
You can't manually update the mouse position because the natives that return the mouse coordinates (BlzGetTriggerPlayerMouseX/Y()) are event responses and so only have values when the trigger itself is actually... triggered. Your only real solution is to manually set the mouse position to be in front of the unit after the dash. It will probably be necessary to check that the player didn't actually move their mouse during the dash (in which case they might really want to attack backwards). If they didn't move their mouse, set the new mouse position to be a little in front of the unit? Honestly seems like a solution with problems.

I can't imagine it's the best option but maybe Trackables would help you here, since those probably activate when the camera is repositioned to move the mouse over them without the mouse moving. But Trackables are permanent and can't be removed so....
Alright, thanks for the answer.
I can track relative position from hero and on-dash-start, start updating mouse-position (SetUserMousePos with low update-rate) until dash finished or mouse got manually moved.
Because you can register and unregister events in this system, probably can use that so I don't have to run the callback on-mouse-movement the whole game, only during dashes (or other ability-movement)... Could get messed up due to elevation, but you can just move your mouse to make it stop.
Seems like a decent solution.
 
Here is my LockCamExtension.
You register the unit that you want to "lock the camera to" and then it keeps the relative X/Y.
Use GetMouseX(Player), GetMouseY(Player) to the the mouse x/y relative to the registered unit for that player.

It isn't really a "general solution" with high code quality, but rather something that works for me.
Maybe useful for someone else.

Note that you'd need to lock the camera yourself.

JASS:
library MouseUtilsLockCamExtension requires MouseUtils
    struct LockCamExtension extends array
        private static unit array lockedCamUnit
        private static real array relativeX
        private static real array relativeY
        static method get takes player p returns integer
            return GetPlayerId(p)
        endmethod
        static method onMouseMovement takes nothing returns nothing
            local player p = GetTriggerPlayer()
            local integer index   = get(p)
            local unit u = lockedCamUnit[index]
            set relativeX[index] = GetPlayerMouseX(p) - GetUnitX(u)
            set relativeY[index] = GetPlayerMouseY(p) - GetUnitY(u)
            //call BJDebugMsg("OnMouseMovement: " + I2S(GetPlayerId(p)) + ", this=" + I2S(this) + ", x=" + R2S(.relativeX))
            set p = null
            set u = null
        endmethod
        static method registerUnit takes unit u returns nothing
            local integer index = get(GetOwningPlayer(u))
            //call BJDebugMsg("Registering for: " + I2S(GetPlayerId(GetOwningPlayer(u))) + ", " + GetUnitName(u))
            set lockedCamUnit[index] = u
        endmethod
        static method mx takes integer index returns real
            return GetUnitX(lockedCamUnit[index]) + relativeX[index]
        endmethod
        static method my takes integer index returns real
            return GetUnitY(lockedCamUnit[index]) + relativeY[index]
        endmethod
    endstruct
    function registerLockCamUnit takes unit u returns nothing
        call LockCamExtension.registerUnit(u)
    endfunction
    function initLockCamExtension takes nothing returns nothing
        call UserMouse.registerCode(function LockCamExtension.onMouseMovement, EVENT_MOUSE_MOVE)
    endfunction
    function GetMouseX takes player p returns real
        //call BJDebugMsg("GetMouseX: " + I2S(GetPlayerId(p)))
        return LockCamExtension.mx(LockCamExtension.get(p))
    endfunction
    function GetMouseY takes player p returns real
        return LockCamExtension.my(LockCamExtension.get(p))
    endfunction
endlibrary
 
Last edited:
Top