Perfect Async Mouse Screen XY v1.1

This bundle is marked as pending. It has not been reviewed by a staff member yet.

Perfect Async Mouse Screen XY! (v1.1)

mousexy-gif.475506

Recent Updates - Version 1.1:
:: Added a way to get stabilized coords!
:: Added 8 getter functions for ease of integration! GetMouseFrameX/Y, GetMouseSaneX/Y, and their stabilized variations!
:: The InitMouseTracker function now takes a bool arg. If true, it will create the Demo Graphics (the crosshair and text).

It's in the name!

This system is as basic as it can be. Just some code that tracks the mouse coordinates on the screen.
Immune to lag. Immune to bumpy terrain. Offers arbitrary resolution.


This solution uses frames to do the tracking, but I have figured out a way for the frames to keep tracking
under any* circumstances while NOT interfering with any** user inputs.
(*: Drag Selection pauses the tracking temporarily. Disable it if you can.)
(**: Well, any inputs in 24 out of 25 timer ticks. Virtually any, precisely.)


How? Read the (extensive) comments in the code below!

If you Use or Modify ANY part of this asset, please give me credit...
And provide a link to this page, if you can!


Lua:
--[[
    ---+------------------------------------+---
    -- | Perfect Async Mouse Screen XY v1.1 | --
    ---+------------------------------------+---
 
    --> https://www.hiveworkshop.com/threads/perfect-async-mouse-screen-xy.354135/
 
    - by ModdieMads, with special thanks to:
        - #modding channel on the HiveWorkshop Discord. The best part of modding WC3!
        - @Tasyen, for the herculean job of writing The Big UI-Frame Tutorial (https://www.hiveworkshop.com/pastebin/e23909d8468ff4942ccea268fbbcafd1.20598).
        - @Water, for the support while we're trying to push WC3 to its limits.
        - @Eikonium, for providing the DebugUtils and the IngameConsole. Big W.
        - @Vinz, for the crosshair texture!
 
 
    +-------------------------------------------------------------------------------------------+
    | Provides mouse screen coordinates with precision, elegance and asynchronism!                |
    |                                                                                            |
    |                                                                                            |
    | Functions you can call:                                                                    |
    |    :: InitMouseTracker() -- inits the MouseTracker and starts it.                            |
    |    :: PauseMouseTracker() -- pauses and hides the mouse tracker.                            |
    |    :: ResumeMouseTracker() -- resumes it!                                                    |
    |                                                                                            |
    | Includes:                                                                                    |
    |   :: Exposes 2 pairs of coords for easy reading:                                             |
    |        1. The frame-space coords (origin at 0.4,0.3)                                        |
    |        2. Sane coords (top-left=(0,0), bottom-right=(1,1))!                                |
    |                                                                                            |
    |   :: A real-life application of a cybernetic device!                                        |
    |   :: Advancements in the understanding of frames, timers and the mouse inside WC3!        |
    |                                                                                            |
    +-------------------------------------------------------------------------------------------+
]]--

-- ATTENTION!
--     The system has 8 getters for ease of use:
--            GetMouseFrameX(), GetMouseFrameY(),
--            GetMouseSaneX(), GetMouseSaneY()
--
--    These are stabilized coords. It is done by using a buffer and averaging it.
--            GetMouseFrameXStable(), GetMouseFrameYStable(),
--            GetMouseSaneXStable(), GetMouseSaneYStable()
--
--
-- If you don't need them, you should remove the lines that set them. Might save you some microseconds.


-- The 3 pairs of coordinates this system can provide.
-- They do as they are named, nothing too fancy.
-- They are explained in more detail down below.

mouseFrameX = 0.0;
mouseFrameY = 0.0;

-- (You should probably be using this one the most...)
mouseSaneX = 0.0;
mouseSaneY = 0.0;

do
    -- Hello again, interloper! Welcome to another crazy project by me, Moddie!

    -- Today, we breakthrough the impossible!
    -- For years, perfect mouse tracking in a screen was the golden relic hidden inside the deepest abyss!
    -- But now, it is ours! From me and all the giants whose shoulders I have stood on, to you.
 
    -- IMPORTANT NOTE:
    -- Please don't take any of this too seriously. I am playing up a character.
    -- I aspire to be an entertainer, if nothing else.


    -- For today's show, the impossible is now possible!
    -- Perfect mouse tracking in screen space, in an asynchronous fashion!

    -- Read on, interloper, and bask yourself in the blazing light of the future!

    local globalFrame = 0;
 
    -- We begin hot! This here are the parameters for a Cybernetic Quadratic Lattice.
    -- Cybernetic not in the sense of science fiction, but in the etymology's.
    -- Cybernetic as in derived from principles of circular causal processes.
    -- As in, self-centering quadratic lattice.
 
    -- Fun fact: Supposedly, this is a Grandmaster-level coding technique.
    -- Meaning that if you know how to build one and how to use it, you are in the 0.1 percentile of coders! yay.
    -- (This is a joke. Nobody thinks that.)
 
    -- I shall explain in further detail what it does when we get to the functions.
 
    local TRACKER_ERROR_BOUND = .15;
 
    local baseSize = .002;
    local TRACKER_LEVELS = 6;
    local trackerTilesGaps = {3, 3, 3, 1, 1, 1};
    local trackerTilesClms = {9, 9, 7, 3, 3, 3};
    local trackerTilesSizes = {baseSize, 3.0*baseSize, 3.0*3.0*baseSize, 7.0*3.0*3.0*baseSize, 3.0*7.0*3.0*3.0*baseSize, 3.0*3.0*7.0*3.0*3.0*baseSize};
 
    -- These are the arrays for the frames used in the lattice. It uses SIMPLEFRAMES, in fact.
    local trackerTilesButtons = {};
    local trackerTilesTooltips = {};
    local trackerTilesN = 0;
 
    -- These two are for internal use of the Tracker only.
    local trackerRawX = 0.0;
    local trackerRawY = 0.0;
 
    -- These two should always be integer powers of 2.
    -- Changing them will make the stabilization stronger/weaker, and the coords update lag bigger/smaller.
    local TRACKER_BUFFER_N = 16;
    local TRACKER_BUFFER_PERIOD_FRAMES = 16;
 
 
    local curTrackerBufferInd = -1;
    local trackerXBuffer = {};
    local trackerYBuffer = {};
 
    local trackerFlickerFrame = 0;
 
    -- The variables for the Demo Graphics. Remove these only if you remove all the Demo Graphics code.
    -- The crosshair, the stabilized crosshair and text for the demo.
    local mouseTargetFrame = nil;
    local mouseStableTargetFrame = nil;
    local mouseTextFrame = nil;
 
    local mouseTextFrameX, mouseTextFrameY, mouseTargetFrameX, mouseTargetFrameY;
 
    local screenWid;
    local screenHei;
    local screenAspectRatio;
 
    local timTick;
 
    -- Ah, the gamedev bread-and-butter: linear interpolation.
    local function Lerp(v0, v1, t)
        return v0 + (v1-v0)*t;
    end
 
    local function Clamp(v, v0, v1)
        return v < v0 and v0 or (v > v1 and v1 or v);
    end
 
    local function Abs(val)
        return val < 0 and -val or val;
    end
 
    local function Sign(val)
        return val < 0 and -1 or 1;
    end
 
    -- Manual modulo, since WC3 really does not like the percentile glyph.
    local function Mod(val, div)
        return val - (val // div)*div;
    end
 
    local function Fill(arr, arrLen, val)
        for i=1,arrLen do
            arr[i] = val;
        end
 
        return arr;
    end
 
    local function Mean(arr)
        local sum = 0;
        local arrLen = #arr;
        for i=1,arrLen do
            sum = sum + arr[i];
        end
 
        return sum/arrLen;
    end
 
 
    local function AvgMovingBuffer(arr, fromInd)
        -- You can use this instead of Mean when sampling the buffer.
        -- The lines are already added on the Getter functions. Just switch them up.
 
        -- This makes more recent values in the buffer have more weight.
        local sum = 0.0;
        local divSum = 0.0;
        local arrLen = #arr;
        local factor = 1.0;
        for i=1,arrLen do
            factor = Mod(arrLen - i + fromInd - 1, arrLen) + 1;
            divSum = divSum + factor;
            sum = sum + factor*arr[i];
        end
 
        return sum/divSum;
    end
 
    -- The Getters this system provides. 8 of them!
    function GetMouseFrameX()
        return mouseFrameX;
    end
 
    function GetMouseFrameY()
        return mouseFrameY;
    end
 
    function GetMouseSaneX()
        return mouseSaneX;
    end
 
    function GetMouseSaneY()
        return mouseSaneY;
    end
 
    function GetMouseFrameXStable()
        --return AvgMovingBuffer(trackerXBuffer, curTrackerBufferInd) + .4;
        return Mean(trackerXBuffer) + .4;
    end
 
    function GetMouseFrameYStable()
        --return AvgMovingBuffer(trackerYBuffer, curTrackerBufferInd) + .3;
        return Mean(trackerYBuffer) + .3;
    end
 
    function GetMouseSaneXStable()
        --return 1.666667*(AvgMovingBuffer(trackerXBuffer, curTrackerBufferInd) + .3*screenAspectRatio)/screenAspectRatio;
        return 1.666667*(Mean(trackerXBuffer) + .3*screenAspectRatio)/screenAspectRatio;
    end
 
    function GetMouseSaneYStable()
        --return (1.0 - 1.666667*(AvgMovingBuffer(trackerYBuffer, curTrackerBufferInd) + .3));
        return (1.0 - 1.666667*(Mean(trackerYBuffer) + .3));
    end
 
 
    -- Ok, below now is the first function concerning the Cybernetic Quadratic Lattice, or as I call it, the Tracker.
 
    -- A brief introduction:
    -- The quadratic lattice is a grid of tiles laid out from the center to the border.
    -- In this case, because we want a self-centering process, the pattern is hollowed out in the very center.
 
    -- The tiles grow in size following a pattern determined by trackerTilesGaps, trackerTilesClms and trackerTilesSizes.
    -- It's called quadratic because the tiles are squares and grow by squaring up on the previous size.
 
    -- Every time a tile is hit, the whole lattice is moved to the coordinates of the hit.
    -- Because the self-governing gradient of a quadratic lattice always points to the center, this process will always center
    -- the lattice around the hit point, given non-catastrophic conditions.
 
    local function MoveTracker(x, y)
        -- MoveTracker moves the entire lattice to the x,y pair provided. (in raw coordinates, meaning center of the screen is 0,0)
 
        -- First calculate the sane coordinates, as sanity is a treasured resource.
        mouseSaneX = 1.666667*(x+.3*screenAspectRatio)/screenAspectRatio;
        mouseSaneY = (1.0 - 1.666667*(y+.3));
 
        -- Check if the tracker was catastrophically shaken.
        -- If it left the screen, reposition it on the center and try again.
        if mouseSaneX > (1.0+TRACKER_ERROR_BOUND) or mouseSaneY > (1.0+TRACKER_ERROR_BOUND) or mouseSaneX < -TRACKER_ERROR_BOUND or mouseSaneY < -TRACKER_ERROR_BOUND then
            do return MoveTracker(0.0,0.0) end;
        end
 
 
        trackerRawX = x;
        trackerRawY = y;
 
        mouseFrameX = x + .4;
        mouseFrameY = y + .3;
    
        local curSize, curGap, gapInd0, gapInd1, clms, clmCenter, ind;
 
        -- Here is the centering loops.
        -- They may look a little scary, but they are big pushovers.
        ind = 0;
        for lvl=1,TRACKER_LEVELS do
            curSize = trackerTilesSizes[lvl];
            curGap = trackerTilesGaps[lvl];
            clms = trackerTilesClms[lvl];
    
            clmCenter = clms >> 1;
            gapInd0 = (clms - curGap) >> 1;
            gapInd1 = clms - gapInd0;
    
            for i=0, clms-1 do
                for i2=0,clms-1 do            
                    if not (i >= gapInd0 and i < gapInd1 and i2 >= gapInd0 and i2 < gapInd1) then                
                        ind = ind + 1;
                
                        -- Re-center the Tracker tiles. It really is doing just that.
                        BlzFrameSetAbsPoint(
                            trackerTilesButtons[ind], FRAMEPOINT_CENTER,
                            mouseFrameX + curSize*(i2 - clmCenter),
                            mouseFrameY - curSize*(i - clmCenter)
                        );
                
                    end
                end
            end
        end
    end
 
    -- Also very simple. Just sets visibility for all tiles in the tracker.
    local function SetTrackerVisible(val)
        for i=1,trackerTilesN do
            BlzFrameSetVisible(trackerTilesButtons[i], val);
        end
    end
 
    -- Now here we have more meat.
    -- What this does is check if the mouse is on top of a tile.
    -- It does so by using the tooltip trick, explained by Tasyen in his tutorial.
    -- Basically, if the mouse is over a tooltipped SIMPLEBUTTON, the tooltip will say it's visible (even if it isn't drawing anything on the screen)
 
 
    local function UpdateTracker()
        local curSize, curGap, gapInd0, gapInd1, clms, clmCenter, ind;
 
        ind = 0;
        for lvl=1,TRACKER_LEVELS do
            curSize = trackerTilesSizes[lvl];
            curGap = trackerTilesGaps[lvl];
            clms = trackerTilesClms[lvl];
    
            clmCenter = clms >> 1;
            gapInd0 = (clms - curGap) >> 1;
            gapInd1 = clms - gapInd0;
    
            for i=0, clms-1 do
                for i2=0,clms-1 do            
                    if not (i >= gapInd0 and i < gapInd1 and i2 >= gapInd0 and i2 < gapInd1) then
                        ind = ind + 1;
                
                        -- If mouse on top of the tile at ind...
                        if BlzFrameIsVisible(trackerTilesTooltips[ind]) then
                
                            -- Immediately hide the tooltip, important to prevent double-procs of the hit detection.
                            BlzFrameSetVisible(trackerTilesTooltips[ind], false);
                    
                            -- You know what, better be safe and hide the whole tracker.
                            SetTrackerVisible(false);
                    
                    
                            -- And here is the golden nugget!
                            -- I have made a big discovery regarding frames! (I think, maybe it was already known)                    
                            -- Read on to unlock the mystery! And remember, the answer is 25!
                    
                            -- I think this could be called FlickerDelay, but since this really turned out to be a Magic Number..
                            -- Why not make it true to its nature?
                            trackerFlickerFrame = globalFrame + 25;
                    
                            -- After the hit is detected, re-center the tracker on the position of the hit tile to
                            -- initiate the cybernetic process.
                            MoveTracker(
                                trackerRawX + curSize*(i2 - clmCenter),
                                trackerRawY - curSize*(i - clmCenter)
                            );
                    
                            do return true end;
                        end
                    end
                end
            end
        end
 
        -- Nothing was hit, so return false.
        return false;
    end
 
 
    -- Creates a tile for the tracker.
    local function CreateTrackerButton(size)
        local button = BlzCreateSimpleFrame('Tile', BlzGetOriginFrame(ORIGIN_FRAME_SIMPLE_UI_PARENT, 0), 0);
 
        -- Important for the tracker to stay above buttons and the top UI bar.
        BlzFrameSetLevel(button, 5);
        BlzFrameSetSize(button, size, size);
 
        return button;
    end
 
    -- Tooltip for the hit detection.
    local function CreateTrackerTooltip(button)
        local tooltip = BlzCreateFrameByType('SIMPLEFRAME', '', button, '', 0);
 
        BlzFrameSetTooltip(button, tooltip);
        BlzFrameSetEnable(tooltip, false);
        BlzFrameSetVisible(tooltip, false);
 
        return tooltip;
    end
 
 
    -- This only runs once. Here we create all the tiles. Very basic stuff.
    local function CreateTracker()
        local button, curSize, curGap, gapInd0, gapInd1, clms, ind;
 
        ind = 0;
        for lvl=1,TRACKER_LEVELS do
            curSize = trackerTilesSizes[lvl];
            curGap = trackerTilesGaps[lvl];
            clms = trackerTilesClms[lvl];
    
            gapInd0 = (clms - curGap) >> 1;
            gapInd1 = clms - gapInd0;
    
            for i=0, clms-1 do
                for i2=0,clms-1 do
                    if not (i >= gapInd0 and i < gapInd1 and i2 >= gapInd0 and i2 < gapInd1) then
            
                        button = CreateTrackerButton(curSize);
                
                        ind = ind + 1;
                        trackerTilesButtons[ind] = button;
                        trackerTilesTooltips[ind] = CreateTrackerTooltip(button);
                
                    end        
                end
            end
        end
 
        trackerTilesN = ind;
        MoveTracker(0.0, 0.0);
    end
 
    -- Well, creates the Demo Graphics.
    local function CreateDemoGraphics()
        -- The main crosshair. Follows the cursor closely. Opaque.
        mouseTargetFrameX = .4;
        mouseTargetFrameY = .3;
        mouseTargetFrame = BlzCreateFrameByType('BACKDROP', '', BlzGetFrameByName('ConsoleUIBackdrop',0), '', 0);
        BlzFrameSetTexture(mouseTargetFrame, 'war3mapImported\\CrossHairSelectionSmall', 0, true);
        BlzFrameSetAbsPoint(mouseTargetFrame, FRAMEPOINT_CENTER, mouseTargetFrameX, mouseTargetFrameY);
        BlzFrameSetEnable(mouseTargetFrame, false);
        BlzFrameSetSize(mouseTargetFrame, 0.04, 0.04);
 
 
        -- The stabilized crosshair. Slightly transparent, and lags behind because of the buffering.
        mouseStableTargetFrame = BlzCreateFrameByType('BACKDROP', '', BlzGetFrameByName('ConsoleUIBackdrop',0), '', 0);
        BlzFrameSetTexture(mouseStableTargetFrame, 'war3mapImported\\CrossHairSelectionSmall', 0, true);
        BlzFrameSetAlpha(mouseStableTargetFrame, 180);
        BlzFrameSetAbsPoint(mouseStableTargetFrame, FRAMEPOINT_CENTER, mouseTargetFrameX, mouseTargetFrameY);
        BlzFrameSetEnable(mouseStableTargetFrame, false);
        BlzFrameSetSize(mouseStableTargetFrame, 0.04, 0.04);
 
        -- The text.
        mouseTextFrameX = .4;
        mouseTextFrameY = .3;
        mouseTextFrame = BlzCreateFrameByType('TEXT', '', BlzGetFrameByName('ConsoleUIBackdrop',0), '', 0);
        BlzFrameSetText(mouseTextFrame, '');        
        BlzFrameSetAbsPoint(mouseTextFrame, FRAMEPOINT_CENTER, mouseTextFrameX, mouseTextFrameY);
        BlzFrameSetEnable(mouseTextFrame, false);
        BlzFrameSetScale(mouseTextFrame, 2);
    end
 
    local function SetDemoGraphicsVisible(val)
        BlzFrameSetVisible(mouseStableTargetFrame, val);
        BlzFrameSetVisible(mouseTargetFrame, val);
        BlzFrameSetVisible(mouseTextFrame, val);
    end

    -- Change the position and the text of the Demo Graphics.
    local function UpdateDemoGraphics()
        -- This updates the text and the crosshair on the screen. Basic stuff.
        BlzFrameSetText(mouseTextFrame, R2S(mouseFrameX) .. ' , ' .. R2S(mouseFrameY));
 
        -- What? I am not addicted to lerp smoothing...
        mouseTextFrameX = Lerp(mouseTextFrameX, mouseFrameX + (mouseSaneX < .9 and .09 or -.09), .5);
        mouseTextFrameY = Lerp(mouseTextFrameY, mouseFrameY + (mouseSaneY > .1 and .03 or -.05), .5);
 
        BlzFrameSetAbsPoint(mouseTextFrame, FRAMEPOINT_CENTER, mouseTextFrameX, mouseTextFrameY);
 
        -- Two times is not an addiction.
        mouseTargetFrameX = Lerp(mouseTargetFrameX, mouseFrameX, .75);
        mouseTargetFrameY = Lerp(mouseTargetFrameY, mouseFrameY, .75);

        BlzFrameSetAbsPoint(mouseTargetFrame, FRAMEPOINT_CENTER, mouseTargetFrameX, mouseTargetFrameY);
 
        -- The stabilized crosshair will be slightly transparent, and lag behind because of the buffering.
        BlzFrameSetAbsPoint(mouseStableTargetFrame, FRAMEPOINT_CENTER, GetMouseFrameXStable(), GetMouseFrameYStable());
    end
 
 
    -- Now here is the beating heart of the system.
    -- And what a beat it has! This Tick is proc-ing every 0.001 seconds.
    local function TimerTick()
        globalFrame = globalFrame+1;
 
        -- I think this is a good spot to explain the Flicker technique, and what the number 25 has to do with frames.
 
        -- I discovered that it takes ~255 ticks of a 0.0 timer to change the state of a tile's tooltip after the tile has been moved.
        -- So, if you set the tile on a new position right under the mouse (or away from it)
        --        it takes ~25 ticks of a 0.001 timer for the tooltip to update its visible state.
 
        -- Nothing extraordinary, I know, but the key part of the discovery is that the visibility update happens even if the tile is INVISIBLE!
        -- Which means that after you move your tile, YOU DON'T NEED to make it visible until after 25 timer-ticks after!
 
        -- The big result of this discovery is that all you need to do is flicker the tiles for the smallest amount of frames possible to check if the mouse
        --        is on top of them! Which means....... THE FRAMES DON'T HAVE ENOUGH TIME TO MESS WITH YOUR MOUSE INPUTS!!
        -- HA!
 
        -- I guess it's time to rectify a little lie I've told you earlier, interloper.
        -- You see, it's not completely true that the tracker does not mess with mouse inputs.
        -- It can block inputs, but only in 1 out of 25 frames. Very rare, but it can happen.
        -- In practice, I can almost never feel it happening.
        -- Not sure if there's a way to solve it, though.
        -- But hey, this whole thing was thought to be impossible, so who truly knows?
 
        -- Back to the code.
        -- If it's time to flicker...
        if globalFrame == trackerFlickerFrame then
            -- Turn on the tracker!
            SetTrackerVisible(true);
        end
 
 
        -- This if block here is just in case the player is using a window and changed its size.
        -- Now because we are at laser speed, we need to tick-limit this kind of stuff.
        -- This block only runs once every 512 ticks.
        if (globalFrame&511)==1 then
            if BlzGetLocalClientWidth() ~= screenWid then
                MoveTracker(0,0);
            end
    
            screenWid = BlzGetLocalClientWidth();
            screenHei = BlzGetLocalClientHeight();
    
            screenAspectRatio = screenWid/screenHei;
        end
 
 
        if (globalFrame&(TRACKER_BUFFER_PERIOD_FRAMES-1))==1 then
            curTrackerBufferInd = (curTrackerBufferInd + 1)&(TRACKER_BUFFER_N-1);
    
            trackerXBuffer[curTrackerBufferInd + 1] = trackerRawX;
            trackerYBuffer[curTrackerBufferInd + 1] = trackerRawY;
        end
 
        -- Here the tracker is updating itself after it has been flickered on.
        -- Once the player hovers a tile with the mouse, the tracker instantly goes dark, and re-sets the flicker frame,
        --         freeing up the screen for any inputs!
        if trackerFlickerFrame <= globalFrame then
            UpdateTracker();
        end
 
 
 
        -- This block here only runs once every 32 ticks, and it only concerns
        --         the demo graphics. You can safely remove this
        --        if-block when integrating this into your map.
        if (globalFrame&31)==1 and mouseTargetFrame ~= nil then
            UpdateDemoGraphics()
        end
    end
 
 
 
 
    function ResumeMouseTracker()
        screenWid = BlzGetLocalClientWidth();
        screenHei = BlzGetLocalClientHeight();
 
        screenAspectRatio = screenWid/screenHei;
 
        BlzSetMousePos(screenWid>>1,screenHei>>1);
 
        MoveTracker(0,0);
        SetTrackerVisible(true);
 
        TimerStart(timTick, .001, true, TimerTick);
 
        if mouseTargetFrame ~= nil then
            SetDemoGraphicsVisible(true);
        end
 
    end
 
    function PauseMouseTracker()
        PauseTimer(timTick);
        SetTrackerVisible(false);
 
        if mouseTargetFrame ~= nil then
            SetDemoGraphicsVisible(false);
        end
    end
 
 
    -- That is the function that needs to be called to get the whole thing started!
    function InitMouseTracker(willCreateDemoGraphics)
        --IngameConsole.forceStartIngameConsole(GetLocalPlayer());
        -- Load the .toc with the .fdf for the tile's SIMPLEFRAME. It's named after Mouse XY.
        BlzLoadTOCFile('mousexy.toc');
 
        DisplayTextToPlayer(GetLocalPlayer(), 0, 0, "|cff66ddffPerfect Async Mouse Screen Tracker by|r |cffffdd00@ModdieMads!|r");
        DisplayTextToPlayer(GetLocalPlayer(), 0, 0, "|cffffcc22This project is hosted on HiveWorkshop! Find me there!|r");
        DisplayTextToPlayer(GetLocalPlayer(), 0, 0, "|cffff3333If you Use or Modify ANY part of this, you|r |cffff9933MUST give me CREDIT!!|r");
        DisplayTextToPlayer(GetLocalPlayer(), 0, 0, "|cffff9933And link back to my HiveWorkshop page!!|r");
 
 
        -- Some boilerplate ahead.
        screenWid = BlzGetLocalClientWidth();
        screenHei = BlzGetLocalClientHeight();
 
        screenAspectRatio = screenWid/screenHei;
 
        BlzSetMousePos(screenWid>>1, screenHei>>1);
        CreateTracker();
 
        Fill(trackerXBuffer, TRACKER_BUFFER_N, 0.0);
        Fill(trackerYBuffer, TRACKER_BUFFER_N, 0.0);
 
        timTick = CreateTimer();
        TimerStart(timTick, .001, true, TimerTick);
 
 
        -- The crosshairs and the text that follow the cursor.
        -- You can remove all the lines pertaining to the Demo Graphics
        --         when importing this code into your map.
        if willCreateDemoGraphics then
            CreateDemoGraphics();
        end
 

    end
 
 
    -- And here we are. At the very end, once more.
    -- It was a pleasure having you on this tour. Hope you come back for future updates.
    -- Have a good one and travel well, dear interloper.
 
    -- If you have any questions, reach me out on Discord! @moddiemads
 
end

Installation Instructions:
:: Read the code! The header, at least. It has useful info!
:: Open the provided .w3x. Take a look at the script file and the trigger.
:: Just add the script file to your map, then call InitMouseTracker() whenever you want to activate it.
:: :: - GUI users can do it with the Custom Script action.


Credits and Shoutouts:
:: - by ModdieMads, with special thanks to:
:: :: - #modding channel on the HiveWorkshop Discord. The best part of modding WC3!
:: :: - @Tasyen, for the herculean job of writing The Big UI-Frame Tutorial (https://www.hiveworkshop.com/pastebin/e23909d8468ff4942ccea268fbbcafd1.20598).
:: :: - @Water, for the support while we're trying to push WC3 to its limits.
:: :: - @Eikonium, for providing the DebugUtils and the IngameConsole. Big W.
:: :: - @Vinz, for the crosshair texture!
Contents

Perfect Async Mouse Screen XY (Map)

I have no idea how frames work, but I can give some tips:

1) 512 and 32 should be user-configurable at the top as "constants", since they were seemingly just numbers that worked for you.
2) Lua's Math library is usable in War3, not sure why you wouldn't take advantage of it instead of using fully custom functions
3) Variable names are mostly fine, but it could pay off in the long term to be a bit more descriptive. Someone could technically run luamin on a war3map.lua file if they are concerned about bundle size.
 
Level 9
Joined
Jun 28, 2023
Messages
51
Hey! Thanks for stopping by!
1) 512 and 32 should be user-configurable at the top as "constants", since they were seemingly just numbers that worked for you.
They are not just numbers that work. They are selected to not overwhelm the timer, which is ticking every 0.001s.
Recently, it was discovered that timers in WC3 are unstable when periodic with a tiny duration.
@Water has been documenting the whole affair in the #modding discord, but in short, timers accelerate their ticking rate proportionally to game time.

This means that if they get overwhelmed, they might become a time bomb and crash the whole game after 5-10-30min in.
512 and 32 were chosen based on those discoveries.
2) Lua's Math library is usable in War3, not sure why you wouldn't take advantage of it instead of using fully custom functions
I know, but the LUA math library inside WC3 is faulty. It was recently discovered and published on #modding that math.ceil(0.) == 1
Also, the math.tan() is extremely noisy next to the singularities. Probably because all that it is doing is calculating sin(), then cos() then dividing them.

For this reason, whenever possible I am trying to use literal functions instead of native ones, so that I don't get stuck figuring out a bug not caused by me.

3) Variable names are mostly fine, but it could pay off in the long term to be a bit more descriptive. Someone could technically run luamin on a war3map.lua file if they are concerned about bundle size.
I find long variable names to be a headache, and I was hoping my extensive commentary would be enough to clarify what the code is doing.
Any under-named variable you could point out to me?
 
Last edited:
I read through the entire code and I have to say, although you have tons of comments, or may because you do, it's quite hard to follow. I don't have anything against you writing an entertaining story about how you came up with this system, but that shouldn't be in place of an understandable documentation. I don't think someone who experiences an issue with your system a year from now will care about your superhero origin story.

When you write something like this
Lua:
"-- And here is the golden nugget!
-- I have made a big discovery regarding frames! (I think, maybe it was already known)							
-- Read on to unlock the mystery! And remember, the answer is 25!"
Yea, that's maybe more exciting to read, but it doesn't really help me understand what's going on. On the contrary, it just confuses me further.

So, I think it would be a good idea to separate your story from the rest and provide a plain-old, boring documentation that explains how it works. I also don't think it's a good idea to scatter the explanation of the method throughout the code. I didn't understand that the mouse tracker frame intercepting the mouse clicks is a problem that you're trying to solve until the very bottom. So, I would change the documentation as such:

  • At the top, how to use it. It's pretty straight-forward, so not much needed.
  • In a separate script, explain the method and how you got there. List all the problems that needed to be addressed first so that people actually understand why you did what you did.
  • Within the code, just do minimal comments and refer to stuff explained earlier in your method section.

This problem is amplified by you using confusing variable names, such as globalFrame and trackerFlickerFrame for what seem to be counters. It took me a while to figure out these do not refer to framehandles, but to monitor frames. The way you're handling those counters is so confusing. globalFrame seems to be unbound and just count up without ever looping back, but basically every 25 ticks, you're executing the UpdateTracker function, so that's every 0.025 seconds?

You want feedback on your system, and to do that, we have to dig ourselves into a pretty complicated topic, so don't make it any harder than it needs to be.

So, if my interpretation is correct, every update, you have to check through 208 frames to find the mouse cursor. Then you move those frames to the new position. That loop should take roughly ~1 microsecond per frame, so that's ~200 microseconds per iteration, every 25k microseconds, so the map script will spend ~2% of its budget on tracking the mouse position (if the map script can take max about 50% of the CPU time). I guess that's fine, but definitely try to reduce the number of frames if you can.

You could also make a mode of your library where the mouse cursor isn't continuously tracked, but its position is only determined on request, maybe through a binary search of both its x- and y-coordinates.

I don't understand that stuff with the flickerframe and the 25 and whether that's really necessary, but this is without a doubt an ingenious way to solve the mouse-tracking problem. What I like most about it is that you don't even need to to any interpolation. You just naturally work around that god-awful, stupid quirk of the mouse natives to only fire once every 0.1 seconds and then fire multiple times at once. With my Screen2World function

Lua:
    ---@param x number
    ---@param y number
    ---@param P integer
    ---@return number, number
    local function Screen2World(x, y, P)
        local eyeX, eyeY, eyeZ, angleOfAttack, rotation

        if FORCED_CAMERAS then
            eyeX, eyeY, eyeZ, angleOfAttack, rotation = GetPlayerCameraParameters(P)
        else
            eyeX = GetCameraEyePositionX()
            eyeY = GetCameraEyePositionY()
            eyeZ = GetCameraEyePositionZ()
            angleOfAttack = -GetCameraField(CAMERA_FIELD_ANGLE_OF_ATTACK)
            rotation = GetCameraField(CAMERA_FIELD_ROTATION)
        end
    
        local cosAttack = cos(angleOfAttack)
        local sinAttack = sin(angleOfAttack)
        local cosRot = cos(rotation)
        local sinRot = sin(rotation)

        local a = (x - 0.4)/0.7408
        local b = (0.42625 - 0.1284*cosAttack - y)/0.7408

        --This is the unit vector pointing towards the mouse cursor in the camera's coordinate system.
        local nx = 1/math.sqrt(1 + a^2 + b^2)
        local ny = math.sqrt(1 - (1 + b^2)*nx^2)
        local nz = math.sqrt(1 - nx^2 - ny^2)
        if a > 0 then
            ny = -ny
        end
        if b < 0 then
            nz = -nz
        end

        --Constructs the unit vector pointing from the camera eye position to the mouse cursor.
		local nxPrime = cosAttack*cosRot*nx - sinRot*ny - sinAttack*cosRot*nz
		local nyPrime = cosAttack*sinRot*nx + cosRot*ny - sinAttack*sinRot*nz
		local nzPrime = sinAttack*nx + cosAttack*nz

        --Try to find intersection point of vector with terrain.
        local zGuess = GetLocZ(eyeX, eyeY)
        local xGuess = eyeX + nxPrime*(eyeZ - zGuess)/nzPrime
        local yGuess = eyeY + nyPrime*(eyeZ - zGuess)/nzPrime
        local zWorld = GetLocZ(xGuess, yGuess)
        local deltaZ = zWorld - zGuess
        zGuess = zWorld

        local zWorldOld, deltaZOld
        local i = 0

        while abs(deltaZ) > 1 and i < 50 do
            zWorldOld = zWorld
            deltaZOld = deltaZ

            xGuess = eyeX + nxPrime*(eyeZ - zGuess)/nzPrime
            yGuess = eyeY + nyPrime*(eyeZ - zGuess)/nzPrime

            zWorld = GetLocZ(xGuess, yGuess)
            deltaZ = zWorld - zGuess
            zGuess = (deltaZOld*zWorld - deltaZ*zWorldOld)/(deltaZOld - deltaZ)
            i = i + 1
        end

        return xGuess, yGuess
    end

you can get a world mouse position that's just as accurate as the native position, remains accurate on camera movement, and doesn't have that stupid behavior.
 
Level 9
Joined
Jun 28, 2023
Messages
51
Wow, thanks for the detailed response!! Let's go!

Antares said:
I read through the entire code and I have to say, although you have tons of comments, or may because you do, it's quite hard to follow. I don't have anything against you writing an entertaining story about how you came up with this system, but that shouldn't be in place of an understandable documentation.

I don't think someone who experiences an issue with your system a year from now will care about your superhero origin story.
Hehe, fair snark about the excessive flair of the comments.
I surely hope someone will use this a year from now, and I hope to make myself available to them in case they have any questions. I have the feeling that in the age of AI IP-laundromats, humans removing themselves from their work is ill-advised, if not self-sacrificial. If anyone would like some extra explanation, they can always reach me here or on Discord.

I am very grateful for you taking the time to read and understand my code, and I apologize if my ramblings were annoying to read through.
All I wanted is to add some seasoning to the task of reading source code, which is often a thankless one.


Antares said:
I don't think someone who experiences an issue with your system a year from now will care about your superhero origin story.
The part about caring, however, is exactly why I chose to write more to entertain than to document.
It is so hard to get people to care enough to read something, even when it is required to get something working at all!

As much as I myself like hitting Ctrl+F and searching for keywords while following a tutorial, this often leads to more problems as you incentivize yourself to take in only the most surface-level factoids of what you are studying, instead of trying to understand the fundamentals of it.

I was burned by my skimming habit just now while developing this system. I insisted for far too long in just skimming Tasyen's tutorials instead of diving into them. If I had taken the time to read it all from top to bottom, I would probably have saved a few hours making this.


Antares said:
At the top, how to use it. It's pretty straight-forward, so not much needed.
In a separate script, explain the method and how you got there. List all the problems that needed to be addressed first so that people actually understand why you did what you did.
Within the code, just do minimal comments and refer to stuff explained earlier in your method section.
I feel like that approach is too close to a manual of operations for it to have any hope of being successful today.

I am not saying it is bad to do things like that, or even that my method of writing documentation is good. I am just saying that I myself wouldn't read documentation that is "minimal", since I can read the code that is in front of me already.

If it is the case that WC3 modders prefer to read documentation that is written with as little soul as possible then I shall adapt. But let it be stated that 99% won't read anything as a principle, much less greyed-out comments.


Antares said:
This problem is amplified by you using confusing variable names, such as globalFrame and trackerFlickerFrame for what seem to be counters. It took me a while to figure out these do not refer to framehandles, but to monitor frames. The way you're handling those counters is so confusing. globalFrame seems to be unbound and just count up without ever looping back, but basically every 25 ticks, you're executing the UpdateTracker function, so that's every 0.025 seconds?

You want feedback on your system, and to do that, we have to dig ourselves into a pretty complicated topic, so don't make it any harder than it needs to be.
I fully agree that calling it globalFrame and trackerFlickerFrame in a system about WC3 UI Frames was a horrible decision. I am used to think about games in frames, and didn't even see the problem until you pointed it out right now. Will change it. Would love to know if anything else about the code confused you.

About the functions, the UpdateTracker is not run every 25 ticks. It runs 25 ticks after the last time the tracker was moved. If the tracker is never hit, UpdateTracker will also not be called. Important difference.

If I made it harder to understand the code was purely out of incompetence, I swear.
The thing is, I think it is futile to see difficulty as a one-dimensional quantity, since it is clearly at least two-dimensional.
There's the difficulty in doing something, and the difficulty in wanting to do something. Those two are always tied, but not the same.

When I spread out the explanations and try to "hype up" the revealing of some information, I am just trying to use the same tricks that thumbnails and headlines use on us every day, in an attempt to lower the difficulty of summoning motivation to keep on studying.
If this technique is having the opposite effect, then oh well, back to the drawing board for me.


Antares said:
So, if my interpretation is correct, every update, you have to check through 208 frames to find the mouse cursor. Then you move those frames to the new position. That loop should take roughly ~1 microsecond per frame, so that's ~200 microseconds per iteration, every 25k microseconds, so the map script will spend ~2% of its budget on tracking the mouse position (if the map script can take max about 50% of the CPU time). I guess that's fine, but definitely try to reduce the number of frames if you can.
Not fully correct. If the tracker has been flickered on, then until the tracker locks the mouse position, it does the 208-frames check. However, the frame-check has an early exit, so only in the absolute worst case that it will do 208 iterations. In realitly, the tracker rarely takes more than 8 iterations to lock-in the mouse cursor. You can check this by creating a lattice with a thousand frames, and see that the bottleneck is WC3 handling of the UI, instead of the tracker checking them.

If the player is constantly moving the mouse at high speed, the check will happen on 8 out of every 8*25 ticks or something. The system couldn't dent my fps even in the worst case, and I have a potato PC. If performance is an issue on your end, I would love to dig out why.


Antares said:
You could also make a mode of your library where the mouse cursor isn't continuously tracked, but its position is only determined on request, maybe through a binary search of both its x- and y-coordinates.
This was my first idea. It turned out to be not worth it. The tracker mechanism was too fast to weigh anything down, and doing it constantly means there is minimal delay in reading the mouse coordinates.


Antares said:
I don't understand that stuff with the flickerframe and the 25 and whether that's really necessary,
The flickering is absolutely necessary and is the big breakthrough of this method. By making the tiles visible only when absolutely necessary, the system can't block mouse inputs or lower the fps.
 
Last edited:
Not fully correct. If the tracker has been flickered on, then until the tracker locks the mouse position, it does the 208-frames check. However, the frame-check has an early exit, so only in the absolute worst case that it will do 208 iterations. In realitly, the tracker rarely takes more than 8 iterations to lock-in the mouse cursor. You can check this by creating a lattice with a thousand frames, and see that the bottleneck is WC3 handling of the UI, instead of the tracker checking them.

If the player is constantly moving the mouse at high speed, the check will happen on 8 out of every 8*25 ticks or something. The system couldn't dent my fps even in the worst case, and I have a potato PC. If performance is an issue on your end, I would love to dig out why.
Then there is something not working as intended. When the mouse is not moving, the UpdateTracker function loops through all 208 frames on every single tick.
 
Level 9
Joined
Jun 28, 2023
Messages
51
Then there is something not working as intended. When the mouse is not moving, the UpdateTracker function loops through all 208 frames on every single tick.
Oh, yeah... There is the case where the mouse is sitting idle on the screen.

Hehe, well, I think you can appreciate what happened. Earlier in production, I was tying the check to the MOUSE_MOVE event.
That was creating a horrible and laggy experience because of the mouse API, so after I removed it I forgot to lower the ticking frequency for when the mouse is idle.

Well, if this proves anything, is that the system is really lightweight, because I couldn't feel any change in fps or responsiveness when the mouse was idle.
 
I feel like that approach is too close to a manual of operations for it to have any hope of being successful today.

I am not saying it is bad to do things like that, or even that my method of writing documentation is good. I am just saying that I myself wouldn't read documentation that is "minimal", since I can read the code that is in front of me already.

If it is the case that WC3 modders prefer to read documentation that is written with as little soul as possible then I shall adapt. But let it be stated that 99% won't read anything as a principle, much less greyed-out comments.
I get where you're coming from, but you're wrong that the documentation has to be soulless. Writing manuals is an art form, just like writing a good story. And if someone delivers a great manual, like for example Eikonium, you appreciate the creator for doing that. I put a lot of effort into my manuals and I have been lauded before for doing that.

I also don't expect you to get rid of your story altogether. Put it anywhere you like, but (and I don't know how @Wrda sees this) I would make it a requirement that you separate it from your code so that one can read your code and just your code if they want to do that.

If I made it harder to understand the code was purely out of incompetence, I swear.
The thing is, I think it is futile to see difficulty as a one-dimensional quantity, since it is clearly at least two-dimensional.
There's the difficulty in doing something, and the difficulty in wanting to do something. Those two are always tied, but not the same.

When I spread out the explanations and try to "hype up" the revealing of some information, I am just trying to use the same tricks that thumbnails and headlines use on us every day, in an attempt to lower the difficulty of summoning motivation to keep on studying.
If this technique is having the opposite effect, then oh well, back to the drawing board for me.
The "motivation" from reading a manual comes from progressing in your understanding, understanding A, which leads to understanding B etc., not from being hyped for some reveal later on. When you feel like you hit an impasse because it's written in a cryptic way, that's where the motivation tanks.

Oh, yeah... There is the case where the mouse is sitting idle on the screen.

Hehe, well, I think you can appreciate what happened. Earlier in production, I was tying the check to the MOUSE_MOVE event.
That was creating a horrible and laggy experience because of the mouse API, so after I removed it I forgot to lower the ticking frequency for when the mouse is idle.

Well, if this proves anything, is that the system is really lightweight, because I couldn't feel any change in fps or responsiveness when the mouse was idle.
Then you're at ~20% CPU capacity when the mouse is resting. That's waaayyy to much! Yes, it won't lag in an empty test map, but it might in a map where a lot of other stuff is going on.

Optimizing your system also allows you or someone else to do a JASS translation, where you're talking about another ~5x drop in performance, so you'll be at 10% CPU capacity even on a moving mouse, which is way too much for a simple mouse-tracking system.

But Lua is certainly fast enough to handle your system, given that you fix that resting mouse issue.
 
Level 9
Joined
Jun 28, 2023
Messages
51
Then you're at ~20% CPU capacity when the mouse is resting. That's waaayyy to much! Yes, it won't lag in an empty test map, but it might in a map where a lot of other stuff is going on.

Optimizing your system also allows you or someone else to do a JASS translation, where you're talking about another ~5x drop in performance, so you'll be at 10% CPU capacity even on a moving mouse, which is way too much for a simple mouse-tracking system.

But Lua is certainly fast enough to handle your system, given that you fix that resting mouse issue.
Hmm... That CPU capacity estimation is not being reflected here on my end.
I am cruising with stable 233 fps (and I even have the demo graphics enabled!)

Maybe I need more testing, but I can't for the life of me lower my fps with the mouse either idle or moving.

Also, I am fairly sure I can adapt this to JASS with way less than a 5x drop in performance. The bottleneck is all on the WC3 natives back here (measured by os.clock())
 

Wrda

Spell Reviewer
Level 28
Joined
Nov 18, 2012
Messages
2,010
I get where you're coming from, but you're wrong that the documentation has to be soulless. Writing manuals is an art form, just like writing a good story. And if someone delivers a great manual, like for example Eikonium, you appreciate the creator for doing that. I put a lot of effort into my manuals and I have been lauded before for doing that.

I also don't expect you to get rid of your story altogether. Put it anywhere you like, but (and I don't know how @Wrda sees this) I would make it a requirement that you separate it from your code so that one can read your code and just your code if they want to do that.

I agree with Antares. Not only you show already show dedication while writing the documentation of this resource (disregarding the wall of china text at the moment), but also spent a lot of time writing it, and perfecting it. That's the opposite of soulless. Don't undermine your own work.
I had seen this resource some hours after you mentioned it on discord, the load of waffle was worse before, I have to say.
Currently, the first few scrolling moments look very nice, then the Darth Load of Waffle Vader after do. You could put your "HOW? This uses frames to do etc. blah blah blah" section just before it, I guess. To wander between Lord Waffle and the cryptic text is confusing to the Reviewerebels and to the local habitats.
To alternate between the load of text that looks like a "lore" and the code is confusing to reviewers and probably even more to the people who might want to understand how it works.
10/10 for creative writing, perfect for a lore of a map, not for an explanation of a function or something of the likes. Like Antares said, separate it from the code, and not scattered around.
The comments at a certain point become hilarious, I'll give you that, good laughs.

I assure you that math.fmod and math.abs are working as expected. No need such paranoia from the generalization that the lua library can't be trusted :p Can always test it to prove me wrong ;)
Dividing the width from height as you are doing risks division by 0 if the user tabs out of Wc3, if I remember this well.
Very promising system.

PS: My PC went for an upgrade, still didn't have it back yet, so I can't review meanwhile, just checking things out on my laptop.
 
This system breaks the standard save-load system in a single player game. Trying to load saved map will cause Warcraft to crash. The reason is that custom frames must be recreated after loading a saved game.

I haven't dived deep into the code, but here's how I see the simplest way to fix it.

Declare timTick variable as global in order to have access to the timer from outside the scope (edit: my bad, this is actually not necessary if the trigger will be initialized in the same scope)
Lua:
timTick = nil

Create a trigger for which we register loading event, and in which we turn off the timTick timer and reinitialize the system.
Lua:
local reloader = CreateTrigger()
TriggerRegisterGameEvent(reloader, EVENT_GAME_LOADED)
TriggerAddCondition(reloader, Condition(function()
    DestroyTimer(timTick)
    InitMouseTracker(true)
end))
 
Last edited:
Top