--[[
---+------------------------------------+---
-- | 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