add gs
This commit is contained in:
+272
@@ -0,0 +1,272 @@
|
||||
local VirtualInput = require(script.Parent.VirtualInput)
|
||||
local XPath = require(script.Parent.XPath)
|
||||
|
||||
local Element = {}
|
||||
Element.__index = Element
|
||||
|
||||
function Element.new(argument)
|
||||
local self = {}
|
||||
if type(argument) == "string" then
|
||||
self.path = XPath.new(argument)
|
||||
elseif type(argument) == "table" and argument.__type == "XPath" then
|
||||
self.path = argument
|
||||
elseif type(argument) == "userdata" then
|
||||
self.path = XPath.new(argument)
|
||||
self.rbxInstance = argument
|
||||
else
|
||||
error("invalid parameter for element")
|
||||
end
|
||||
|
||||
setmetatable(self, Element)
|
||||
local scrollNums = self:_scrollingFrames(self.rbxInstance)
|
||||
|
||||
self.isInScrollingFrame = scrollNums ~= 0
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function Element:getAttribute(name)
|
||||
return self:getRbxInstance()[name]
|
||||
end
|
||||
|
||||
function Element:getLocation()
|
||||
return self:getRbxInstance().AbsolutePosition
|
||||
end
|
||||
|
||||
function Element:getRect()
|
||||
local topLeft = self:getLocation()
|
||||
local bottomRight = self:getSize() + topLeft
|
||||
return Rect.new(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y)
|
||||
end
|
||||
|
||||
function Element:getSize()
|
||||
return self:getRbxInstance().AbsoluteSize
|
||||
end
|
||||
|
||||
function Element:getCenter()
|
||||
return self:getLocation()+self:getSize()/2
|
||||
end
|
||||
|
||||
function Element:getText()
|
||||
return self:getRbxInstance().Text
|
||||
end
|
||||
|
||||
function Element:getAnchor()
|
||||
return self:getLocation()+self.anchor
|
||||
end
|
||||
|
||||
-- Set anchor at offset from absolute position
|
||||
function Element:setAnchor(offsetX, offsetY)
|
||||
if(offsetX > self:getSize().x or offsetY > self:getSize().y or offsetX < 0 or offsetY < 0) then
|
||||
error("Attempt to set anchor beyond element's bounds")
|
||||
else
|
||||
self.anchor = Vector2.new(offsetX, offsetY)
|
||||
end
|
||||
end
|
||||
|
||||
function Element:isDisplayed()
|
||||
return self:getRbxInstance().Visible
|
||||
end
|
||||
|
||||
function Element:isSelected()
|
||||
return self:getRbxInstance().Selected
|
||||
end
|
||||
|
||||
function Element:getRbxInstance()
|
||||
return self:waitForRbxInstance(self.path.waitDelay, self.path.waitTimeout)
|
||||
end
|
||||
|
||||
function Element:waitForRbxInstance(timeout, delay)
|
||||
if self.rbxInstance == nil and self.path ~= nil then
|
||||
self.path:setWait(timeout, delay)
|
||||
self.rbxInstance = self.path:waitForFirstInstance()
|
||||
end
|
||||
|
||||
if self.rbxInstance and not self.anchor then
|
||||
if pcall(function() local size = self.rbxInstance.AbsoluteSize end) then
|
||||
self.anchor = self.rbxInstance.AbsoluteSize/2
|
||||
else
|
||||
self.anchor = nil
|
||||
end
|
||||
end
|
||||
|
||||
return self.rbxInstance
|
||||
end
|
||||
|
||||
function Element:_override(class)
|
||||
for k, v in pairs(class) do
|
||||
if not k:find("^_") then
|
||||
self[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Element:centralizeInstance()
|
||||
self:_centralizeInScrollingFrame(self:getRbxInstance())
|
||||
end
|
||||
|
||||
function Element:centralize()
|
||||
local instance = self:getRbxInstance()
|
||||
if instance then
|
||||
self:centralizeInstance()
|
||||
else
|
||||
self:centralizeWithInfiniteScrolling()
|
||||
end
|
||||
end
|
||||
|
||||
function Element:_scrollingFrames(instance)
|
||||
if instance == nil or instance == game then return 0 end
|
||||
local num = self:_scrollingFrames(instance.Parent)
|
||||
if instance.ClassName == "ScrollingFrame" then num = num + 1 end
|
||||
return num
|
||||
end
|
||||
|
||||
function Element:_centralizeInScrollingFrame(child, parent)
|
||||
if child == game then return end
|
||||
parent = parent or child.Parent
|
||||
if parent == game then return end
|
||||
|
||||
if parent.ClassName == "ScrollingFrame" then
|
||||
self:_centralizeInScrollingFrame(parent, parent.Parent)
|
||||
-- this is computational error tolerate.
|
||||
local threshold = 2
|
||||
|
||||
--first scroll down to make child appears neas screen,
|
||||
--so that we can access child.AbsolutPosition property
|
||||
local isChildInScreen = false
|
||||
while not isChildInScreen do
|
||||
|
||||
local prevChildPosition = child.AbsolutePosition
|
||||
local prevCanvasPosition = parent.CanvasPosition
|
||||
-- when scroll too much at one time, the element may move out side of screen immediately
|
||||
-- its AbsoluteSize will not update. limit to 300
|
||||
local scrollDistance = Vector2.new(math.min(300, parent.AbsoluteSize.X), math.min(300, parent.AbsoluteSize.Y))
|
||||
parent.CanvasPosition = parent.CanvasPosition + scrollDistance
|
||||
wait()
|
||||
local deltaCanvas = (parent.CanvasPosition - prevCanvasPosition)
|
||||
local isBottom = deltaCanvas.Magnitude <= threshold
|
||||
local deltaChild = child.AbsolutePosition - prevChildPosition
|
||||
isChildInScreen = isBottom or deltaChild.Magnitude > threshold
|
||||
end
|
||||
--second scroll to centerize the child, at most twice.
|
||||
for _ = 1, 2 do
|
||||
local frameCenter = parent.AbsolutePosition + parent.AbsoluteSize/2
|
||||
local childCenter = child.AbsolutePosition + child.AbsoluteSize/2
|
||||
local delta = childCenter - frameCenter
|
||||
if delta.Magnitude <= threshold then break end
|
||||
parent.CanvasPosition = parent.CanvasPosition + delta
|
||||
wait()
|
||||
end
|
||||
else
|
||||
self:_centralizeInScrollingFrame(child, parent.Parent)
|
||||
end
|
||||
end
|
||||
|
||||
function Element:_scrollToFindInstance(scrollingFrame, absPath)
|
||||
-- first reset scrollingFrame to zero position
|
||||
scrollingFrame.CanvasPosition = Vector2.new(0, 0)
|
||||
local width = scrollingFrame.AbsoluteSize.X
|
||||
local height = scrollingFrame.AbsoluteSize.Y
|
||||
|
||||
local isBottom = false
|
||||
local instance
|
||||
local threshold = 2
|
||||
while not isBottom do
|
||||
wait(0.1)
|
||||
--if find the element then return
|
||||
instance = absPath:getFirstInstance()
|
||||
if instance then return instance end
|
||||
--scroll
|
||||
local oldPosition = scrollingFrame.CanvasPosition
|
||||
scrollingFrame.CanvasPosition = scrollingFrame.CanvasPosition +
|
||||
Vector2.new(math.min(width, 300), math.min(height, 300))
|
||||
--wait for content to refresh
|
||||
local delta = scrollingFrame.CanvasPosition - oldPosition
|
||||
isBottom = delta.Magnitude < threshold
|
||||
--if it is the bottom, then return not found
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function Element:centralizeWithInfiniteScrolling()
|
||||
local instances, lastSeenIndex = self.path:getInstances()
|
||||
if #instances > 0 then self:centralizeInstance() end
|
||||
|
||||
local lastSeenPath = self.path:copy()
|
||||
while #lastSeenPath.data > lastSeenIndex do
|
||||
table.remove(lastSeenPath.data)
|
||||
end
|
||||
|
||||
local lastSeenInstance = lastSeenPath:getFirstInstance()
|
||||
local lastScrollingFrame = nil
|
||||
while true do
|
||||
if lastSeenInstance.ClassName == "ScrollingFrame" then
|
||||
lastScrollingFrame = lastSeenInstance
|
||||
break
|
||||
end
|
||||
lastSeenInstance = lastSeenInstance.Parent
|
||||
if lastSeenInstance == game then break end
|
||||
end
|
||||
if lastScrollingFrame == nil then return end
|
||||
if self:_scrollToFindInstance(lastScrollingFrame, self.path) == nil then return end
|
||||
self:_centralizeInScrollingFrame(self:getRbxInstance())
|
||||
end
|
||||
|
||||
function Element:setPluginWindow()
|
||||
local window = self.rbxInstance:FindFirstAncestorOfClass("DockWidgetPluginGui")
|
||||
VirtualInput.setCurrentWindow(window)
|
||||
end
|
||||
|
||||
function Element:click(repeatCount)
|
||||
self:centralize()
|
||||
self:setPluginWindow()
|
||||
|
||||
repeatCount = repeatCount or 1
|
||||
VirtualInput.Mouse.multiClick(self:getAnchor(), repeatCount)
|
||||
end
|
||||
|
||||
function Element:rightClick()
|
||||
self:centralize()
|
||||
self:setPluginWindow()
|
||||
|
||||
VirtualInput.Mouse.rightClick(self:getAnchor())
|
||||
end
|
||||
|
||||
function Element:mouseWheel(num)
|
||||
self:centralize()
|
||||
VirtualInput.Mouse.mouseWheel(self:getAnchor(), num)
|
||||
end
|
||||
|
||||
function Element:mouseDrag(xOffset, yOffset, duration)
|
||||
self:centralize()
|
||||
local posTo = self:getAnchor() + Vector2.new(xOffset, yOffset)
|
||||
VirtualInput.Mouse.mouseDrag(self:getAnchor(), posTo, duration, true)
|
||||
end
|
||||
|
||||
function Element:mouseDragTo(posTo, duration)
|
||||
self:centralize()
|
||||
VirtualInput.Mouse.mouseDrag(self:getAnchor(), posTo, duration, true)
|
||||
end
|
||||
|
||||
function Element:sendKey(key)
|
||||
self:setPluginWindow()
|
||||
VirtualInput.Keyboard.hitKey(key)
|
||||
end
|
||||
|
||||
function Element:sendText(str)
|
||||
self:click()
|
||||
wait(0)
|
||||
VirtualInput.Text.sendText(str)
|
||||
end
|
||||
|
||||
function Element:tap()
|
||||
self:centralize()
|
||||
VirtualInput.Touch.tap(self:getAnchor())
|
||||
end
|
||||
|
||||
function Element:touchScroll(xOffset, yOffset, duration, multitouchId)
|
||||
self:centralize()
|
||||
VirtualInput.Touch.touchScroll(self:getAnchor(), xOffset, yOffset, duration, true, multitouchId)
|
||||
end
|
||||
|
||||
return Element
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
return function()
|
||||
local XPath = require(script.Parent.XPath)
|
||||
local Element = require(script.Parent.Element)
|
||||
|
||||
local function makeInstance(className, props, children)
|
||||
local instance = Instance.new(className)
|
||||
if children then
|
||||
for _, child in ipairs(children) do
|
||||
child.Parent = instance
|
||||
end
|
||||
end
|
||||
if props then
|
||||
for k, v in pairs(props) do
|
||||
instance[k] = v
|
||||
end
|
||||
end
|
||||
return instance
|
||||
end
|
||||
|
||||
local root = makeInstance("Folder",
|
||||
{
|
||||
Name = "root",
|
||||
Parent = workspace
|
||||
}, {
|
||||
makeInstance("Frame", {Name = "Frame"}, {
|
||||
makeInstance("TextLabel", {
|
||||
Text = "Label1"
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
describe("element creation", function()
|
||||
it("valid element creation", function()
|
||||
local path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button2, .ClassName = Frame]")
|
||||
local validElement = Element.new(path:cat(XPath.new("TextLabel")))
|
||||
|
||||
expect(validElement:getRbxInstance()).to.be.ok()
|
||||
end)
|
||||
it("invalid element creation", function()
|
||||
local path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button2, .ClassName = Frame]")
|
||||
local invalidElement = Element.new(path:cat(XPath.new("TextLabel2")))
|
||||
|
||||
expect(invalidElement:getRbxInstance()).to.never.be.ok()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
local VirtualInputUtils = require(script.Parent.Parent.VirtualInputUtils)
|
||||
|
||||
local VirtualInputManager = game:GetService("VirtualInputManager")
|
||||
|
||||
local GamePad = {}
|
||||
GamePad.__index = GamePad
|
||||
local gamePadDeviceId = 123
|
||||
|
||||
GamePad.KeyCode = {
|
||||
ButtonX = Enum.KeyCode.ButtonX,
|
||||
ButtonY = Enum.KeyCode.ButtonY,
|
||||
ButtonA = Enum.KeyCode.ButtonA,
|
||||
ButtonB = Enum.KeyCode.ButtonB,
|
||||
ButtonR1 = Enum.KeyCode.ButtonR1,
|
||||
ButtonL1 = Enum.KeyCode.ButtonL1,
|
||||
ButtonR2 = Enum.KeyCode.ButtonR2,
|
||||
ButtonL2 = Enum.KeyCode.ButtonL2,
|
||||
ButtonR3 = Enum.KeyCode.ButtonR3,
|
||||
ButtonL3 = Enum.KeyCode.ButtonL3,
|
||||
ButtonStart = Enum.KeyCode.ButtonStart,
|
||||
ButtonSelect = Enum.KeyCode.ButtonSelect,
|
||||
DPadLeft = Enum.KeyCode.DPadLeft,
|
||||
DPadRight = Enum.KeyCode.DPadRight,
|
||||
DPadUp = Enum.KeyCode.DPadUp,
|
||||
DPadDown = Enum.KeyCode.DPadDown,
|
||||
Thumbstick1 = Enum.KeyCode.Thumbstick1,
|
||||
Thumbstick2 = Enum.KeyCode.Thumbstick2,
|
||||
}
|
||||
|
||||
function GamePad.new()
|
||||
local self = {deviceId = gamePadDeviceId}
|
||||
gamePadDeviceId = gamePadDeviceId + 1
|
||||
setmetatable(self, GamePad)
|
||||
VirtualInputManager:HandleGamepadConnect(self.deviceId)
|
||||
return self
|
||||
end
|
||||
|
||||
function GamePad:disconnect()
|
||||
VirtualInputManager:HandleGamepadDisconnect(self.deviceId)
|
||||
end
|
||||
|
||||
function GamePad:pressButton(button)
|
||||
VirtualInputManager:HandleGamepadButtonInput(self.deviceId, button, 1);
|
||||
end
|
||||
|
||||
function GamePad:releaseButton(button)
|
||||
VirtualInputManager:HandleGamepadButtonInput(self.deviceId, button, 0);
|
||||
end
|
||||
|
||||
function GamePad:hitButton(button)
|
||||
self:pressButton(button)
|
||||
self:releaseButton(button)
|
||||
end
|
||||
|
||||
function GamePad:moveStickTo(stick, vec2)
|
||||
VirtualInputManager:HandleGamepadAxisInput(self.deviceId, stick, vec2.x, vec2.y, 0)
|
||||
end
|
||||
|
||||
function GamePad:smoothMoveStickTo(stick, from, to, duration)
|
||||
duration = duration or 0
|
||||
if duration == 0 then
|
||||
self:moveStickTo(stick, to)
|
||||
return
|
||||
end
|
||||
local passed = 0
|
||||
local function run(dt)
|
||||
local ratio = passed / duration
|
||||
passed = passed + dt
|
||||
if ratio < 1 then
|
||||
local pos = from + (to - from) * ratio
|
||||
self:moveStickTo(stick, pos)
|
||||
return false
|
||||
else
|
||||
self:moveStickTo(stick, to)
|
||||
return true
|
||||
end
|
||||
end
|
||||
VirtualInputUtils.__syncRun(run)
|
||||
end
|
||||
|
||||
function GamePad:swingStick(stick, pos, duration)
|
||||
duration = duration or 0
|
||||
local origin = Vector2.new(0, 0)
|
||||
self:moveStickTo(stick, origin)
|
||||
self:smoothMoveStickTo(stick, origin, pos, duration / 2)
|
||||
self:smoothMoveStickTo(stick, pos, origin, duration / 2)
|
||||
end
|
||||
|
||||
function GamePad:swingLeft(stick, duration)
|
||||
self:swingStick(stick, Vector2.new(-1, 0), duration)
|
||||
end
|
||||
|
||||
function GamePad:swingRight(stick, duration)
|
||||
self:swingStick(stick, Vector2.new(1, 0), duration)
|
||||
end
|
||||
|
||||
function GamePad:swingTop(stick, duration)
|
||||
self:swingStick(stick, Vector2.new(0, 1), duration)
|
||||
end
|
||||
|
||||
function GamePad:swingDown(stick, duration)
|
||||
self:swingStick(stick, Vector2.new(0, -1), duration)
|
||||
end
|
||||
|
||||
return GamePad
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
local VirtualInputUtils = require(script.Parent.Parent.VirtualInputUtils)
|
||||
|
||||
local VirtualInputManager = game:GetService("VirtualInputManager")
|
||||
|
||||
local Keyboard = {}
|
||||
|
||||
function Keyboard.SendKeyEvent(isPressed, keyCode, isRepeated)
|
||||
VirtualInputManager:SendKeyEvent(isPressed, keyCode, isRepeated, VirtualInputUtils.getCurrentWindow())
|
||||
end
|
||||
|
||||
function Keyboard.pressKey(keyCode)
|
||||
Keyboard.SendKeyEvent(true, keyCode, false)
|
||||
end
|
||||
|
||||
function Keyboard.releaseKey(keyCode)
|
||||
Keyboard.SendKeyEvent(false, keyCode, false)
|
||||
end
|
||||
|
||||
function Keyboard.hitKey(keyCode)
|
||||
Keyboard.pressKey(keyCode)
|
||||
Keyboard.releaseKey(keyCode)
|
||||
end
|
||||
|
||||
return Keyboard
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
local VirtualInputUtils = require(script.Parent.Parent.VirtualInputUtils)
|
||||
|
||||
local VirtualInputManager = game:GetService("VirtualInputManager")
|
||||
local InputVisualizer = require(script.Parent.Parent.InputVisualizer):new()
|
||||
|
||||
local Mouse = {}
|
||||
|
||||
function Mouse.sendMouseButtonEvent(x, y, button, isDown, repeatCount)
|
||||
x, y = VirtualInputUtils.__handleGuiInset(x, y)
|
||||
VirtualInputManager:SendMouseButtonEvent(x, y, button, isDown, VirtualInputUtils.getCurrentWindow(), repeatCount or 0)
|
||||
end
|
||||
|
||||
function Mouse.SendMouseMoveEvent(x, y)
|
||||
x, y = VirtualInputUtils.__handleGuiInset(x, y)
|
||||
VirtualInputManager:SendMouseMoveEvent(x, y, VirtualInputUtils.getCurrentWindow())
|
||||
end
|
||||
|
||||
function Mouse.SendMouseWheelEvent(x, y, isForwardScroll)
|
||||
x, y = VirtualInputUtils.__handleGuiInset(x, y)
|
||||
VirtualInputManager:SendMouseWheelEvent(x, y, isForwardScroll, VirtualInputUtils.getCurrentWindow())
|
||||
end
|
||||
|
||||
function Mouse.mouseWheel(vec2, num)
|
||||
local forward = false
|
||||
if num < 0 then
|
||||
forward = true
|
||||
num = -num
|
||||
end
|
||||
for _ = 1, num do
|
||||
Mouse.SendMouseWheelEvent(vec2.x, vec2.y, forward)
|
||||
end
|
||||
end
|
||||
|
||||
local function click(vec2, count, clickType)
|
||||
InputVisualizer:click(vec2, VirtualInputUtils.getCurrentWindow())
|
||||
Mouse.sendMouseButtonEvent(vec2.x, vec2.y, clickType, true, count)
|
||||
Mouse.sendMouseButtonEvent(vec2.x, vec2.y, clickType, false, count)
|
||||
end
|
||||
|
||||
local function multiClick(vec2, count, clickType)
|
||||
local waiting = true
|
||||
local repeatCount = 0
|
||||
return function()
|
||||
if waiting then
|
||||
waiting = false
|
||||
return false
|
||||
elseif count >= 1 then
|
||||
click(vec2, repeatCount, clickType)
|
||||
count = count - 1
|
||||
repeatCount = repeatCount + 1
|
||||
waiting = true
|
||||
return false
|
||||
elseif count == 0 then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Mouse.click(vec2)
|
||||
VirtualInputUtils.__syncRun(multiClick(vec2, 1, 0))
|
||||
end
|
||||
|
||||
function Mouse.multiClick(vec2, count)
|
||||
VirtualInputUtils.__syncRun(multiClick(vec2, count, 0))
|
||||
end
|
||||
|
||||
function Mouse.rightClick(vec2)
|
||||
VirtualInputUtils.__syncRun(multiClick(vec2, 1, 1))
|
||||
end
|
||||
|
||||
function Mouse.mouseLeftDown(vec2)
|
||||
Mouse.sendMouseButtonEvent(vec2.x, vec2.y, 0, true)
|
||||
end
|
||||
|
||||
function Mouse.mouseLeftUp(vec2)
|
||||
Mouse.sendMouseButtonEvent(vec2.x, vec2.y, 0, false)
|
||||
end
|
||||
|
||||
function Mouse.mouseRightDown(vec2)
|
||||
Mouse.sendMouseButtonEvent(vec2.x, vec2.y, 1, true)
|
||||
end
|
||||
|
||||
function Mouse.mouseRightUp(vec2)
|
||||
Mouse.sendMouseButtonEvent(vec2.x, vec2.y, 1, false)
|
||||
end
|
||||
|
||||
function Mouse.mouseMove(vec2)
|
||||
Mouse.SendMouseMoveEvent(vec2.X, vec2.Y)
|
||||
end
|
||||
|
||||
local function drag(posFrom, posTo, duration)
|
||||
local passed = 0
|
||||
local started = false
|
||||
return function(dt)
|
||||
if not started then
|
||||
Mouse.mouseLeftDown(posFrom)
|
||||
started = true
|
||||
else
|
||||
passed = passed + dt
|
||||
if duration and passed < duration then
|
||||
local percent = passed / duration
|
||||
local pos = (posTo - posFrom) * percent + posFrom
|
||||
Mouse.mouseMove(pos)
|
||||
else
|
||||
Mouse.mouseMove(posTo)
|
||||
Mouse.mouseLeftUp(posTo)
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function Mouse.mouseDrag(posFrom, posTo, duration)
|
||||
VirtualInputUtils.__syncRun(drag(posFrom, posTo, duration))
|
||||
end
|
||||
|
||||
return Mouse
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
local VirtualInputUtils = require(script.Parent.Parent.VirtualInputUtils)
|
||||
|
||||
local VirtualInputManager = game:GetService("VirtualInputManager")
|
||||
|
||||
local Text = {}
|
||||
|
||||
function Text.sendText(str)
|
||||
VirtualInputManager:sendTextInputCharacterEvent(str, VirtualInputUtils.getCurrentWindow())
|
||||
end
|
||||
|
||||
return Text
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
local VirtualInputUtils = require(script.Parent.Parent.VirtualInputUtils)
|
||||
|
||||
local VirtualInputManager = game:GetService("VirtualInputManager")
|
||||
|
||||
local Touch = {}
|
||||
local defaultTouchId = 123456
|
||||
|
||||
function Touch.SendTouchEvent(touchId, state, x, y)
|
||||
x, y = VirtualInputUtils.__handleGuiInset(x, y)
|
||||
VirtualInputManager:SendTouchEvent(touchId, state, x, y)
|
||||
end
|
||||
|
||||
function Touch.touchStart(vec2, multitouchId)
|
||||
local touchId = defaultTouchId + (multitouchId or 0)
|
||||
Touch.SendTouchEvent(touchId, 0, vec2.x, vec2.y)
|
||||
end
|
||||
|
||||
function Touch.touchMove(vec2, multitouchId)
|
||||
local touchId = defaultTouchId + (multitouchId or 0)
|
||||
Touch.SendTouchEvent(touchId, 1, vec2.x, vec2.y)
|
||||
end
|
||||
|
||||
function Touch.touchStop(vec2, multitouchId)
|
||||
local touchId = defaultTouchId + (multitouchId or 0)
|
||||
Touch.SendTouchEvent(touchId, 2, vec2.x, vec2.y)
|
||||
end
|
||||
|
||||
local function smoothSwipe(posFrom, posTo, duration, multitouchId)
|
||||
local passed = 0
|
||||
local started = false
|
||||
local touchId = defaultTouchId + (multitouchId or 0)
|
||||
return function(dt)
|
||||
if not started then
|
||||
Touch.touchStart(posFrom, touchId)
|
||||
started = true
|
||||
else
|
||||
passed = passed + dt
|
||||
if duration and passed < duration then
|
||||
local percent = passed / duration
|
||||
local pos = (posTo - posFrom) * percent + posFrom
|
||||
Touch.touchMove(pos, touchId)
|
||||
else
|
||||
Touch.touchMove(posTo, touchId)
|
||||
Touch.touchStop(posTo, touchId)
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function Touch.swipe(posFrom, posTo, duration, async, multitouchId)
|
||||
local touchId = defaultTouchId + (multitouchId or 0)
|
||||
if async == true then
|
||||
VirtualInputUtils.__asyncRun(smoothSwipe(posFrom, posTo, duration, touchId))
|
||||
else
|
||||
VirtualInputUtils.__syncRun(smoothSwipe(posFrom, posTo, duration, touchId))
|
||||
end
|
||||
end
|
||||
|
||||
function Touch.touchScroll(startPos, xOffset, yOffset, duration, async, multitouchId)
|
||||
local posTo = startPos + Vector2.new(xOffset, yOffset)
|
||||
Touch.swipe(startPos, posTo, duration, async, multitouchId)
|
||||
end
|
||||
|
||||
function Touch.tap(vec2)
|
||||
Touch.touchStart(vec2)
|
||||
Touch.touchStop(vec2)
|
||||
end
|
||||
|
||||
return Touch
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
local InputVisualizer = {}
|
||||
InputVisualizer.__index = InputVisualizer
|
||||
|
||||
local TweenService = game:GetService("TweenService")
|
||||
local Debris = game:GetService("Debris")
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
|
||||
function InputVisualizer.new()
|
||||
local self = {}
|
||||
local state, guiRoot = pcall(function() return game.CoreGui.Parent.CoreGui end)
|
||||
if state == false then
|
||||
local LocalPlayer = game.Players.LocalPlayer
|
||||
while LocalPlayer == nil do
|
||||
LocalPlayer = game.Players.LocalPlayer
|
||||
wait()
|
||||
end
|
||||
guiRoot = LocalPlayer.PlayerGui
|
||||
end
|
||||
|
||||
local GuiName = "InputVisualizer"
|
||||
if guiRoot:FindFirstChild(GuiName) == nil then
|
||||
local screenGui = Instance.new("ScreenGui")
|
||||
screenGui.Name = GuiName
|
||||
screenGui.DisplayOrder = 1000000
|
||||
screenGui.Parent = guiRoot
|
||||
end
|
||||
guiRoot = guiRoot[GuiName]
|
||||
self.guiRoot = guiRoot
|
||||
|
||||
setmetatable(self, InputVisualizer)
|
||||
return self
|
||||
end
|
||||
|
||||
function InputVisualizer:onInputBegan(input)
|
||||
if input.UserInputType == Enum.UserInputType.MouseButton1 then
|
||||
self:click(Vector2.new(input.Position.X, input.Position.Y))
|
||||
elseif input.UserInputType == Enum.UserInputType.Touch then
|
||||
self:click(Vector2.new(input.Position.X, input.Position.Y))
|
||||
end
|
||||
end
|
||||
|
||||
function InputVisualizer:click(vec2, pluginGui)
|
||||
local delay = 0.5
|
||||
local image = nil
|
||||
if image == nil then
|
||||
image = Instance.new("ImageLabel")
|
||||
image.Image = "rbxassetid://1549893588"
|
||||
image.BackgroundTransparency = 1
|
||||
image.Parent = pluginGui or self.guiRoot
|
||||
image.Size = UDim2.new(0, 20, 0, 20)
|
||||
image.Name = "MouseClick"
|
||||
image.ZIndex = 10
|
||||
end
|
||||
|
||||
image.Visible = true
|
||||
image.Position = UDim2.new(0, vec2.X-image.Size.X.Offset/2, 0, vec2.Y-image.Size.Y.Offset/2)
|
||||
image.ImageTransparency = 0
|
||||
local goal = {ImageTransparency = 1}
|
||||
local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, false)
|
||||
local tween = TweenService:Create(image, tweenInfo, goal)
|
||||
tween:Play()
|
||||
Debris:AddItem(image, delay)
|
||||
end
|
||||
|
||||
function InputVisualizer:enable()
|
||||
self.handler = UserInputService.InputBegan:connect(
|
||||
function(input, gameProcessed)
|
||||
self:onInputBegan(input, gameProcessed)
|
||||
end)
|
||||
end
|
||||
|
||||
function InputVisualizer:disable()
|
||||
if self.handler then
|
||||
self.handler:Disconnect()
|
||||
end
|
||||
self.handler = nil
|
||||
end
|
||||
|
||||
return InputVisualizer
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
local RemoteRhodium = {}
|
||||
local HttpService = game:getService("HttpService")
|
||||
|
||||
local rootPath = nil
|
||||
|
||||
local function split(s, delimiter)
|
||||
local result = {};
|
||||
while(s:len()>0) do
|
||||
local pos, stop = string.find(s,delimiter,1,true)
|
||||
if pos == nil then
|
||||
table.insert(result,s)
|
||||
s = ""
|
||||
else
|
||||
table.insert(result,string.sub(s,1,pos-1))
|
||||
s = string.sub(s,stop+1)
|
||||
if s == "" then table.insert(result,"") end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function RemoteRhodium.setCommandPath(p)
|
||||
rootPath = p
|
||||
end
|
||||
|
||||
local function onCommand(command)
|
||||
assert(type(command) == "string", "command should be a string")
|
||||
|
||||
local op = command:find("(", 1, true)
|
||||
local cp = command:reverse():find(")", 1, true)
|
||||
if cp ~= nil then
|
||||
cp = #command - cp + 1
|
||||
end
|
||||
|
||||
local args = {}
|
||||
if op then
|
||||
assert(cp, "invalid syntex, expecting \")\"")
|
||||
local argStr = command:sub(op+1, cp-1)
|
||||
if #argStr > 0 then
|
||||
args = HttpService:JSONDecode("["..argStr.."]")
|
||||
end
|
||||
command = command:sub(1, op-1)
|
||||
end
|
||||
|
||||
local subPathTab = split(command, ".")
|
||||
|
||||
local instance = rootPath
|
||||
for i = 1, #subPathTab do
|
||||
local p = subPathTab[i]
|
||||
instance = instance[p]
|
||||
if instance == nil then
|
||||
error("can not find " .. p)
|
||||
end
|
||||
if type(instance) == "userdata" and instance.ClassName == "ModuleScript" then
|
||||
instance = require(instance)
|
||||
end
|
||||
end
|
||||
if type(instance) ~= "function" then
|
||||
error("target is not a function")
|
||||
end
|
||||
return instance(unpack(args))
|
||||
end
|
||||
|
||||
function RemoteRhodium.setCommandPath(p)
|
||||
rootPath = p
|
||||
end
|
||||
|
||||
local success, RhodiumService = pcall(function()
|
||||
return game:getService("RhodiumService")
|
||||
end)
|
||||
|
||||
if success then
|
||||
RhodiumService.onCommand = onCommand
|
||||
end
|
||||
|
||||
return RemoteRhodium
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
local Keyboard = require(script.Parent.InputTypes.Keyboard)
|
||||
local Mouse = require(script.Parent.InputTypes.Mouse)
|
||||
local Touch = require(script.Parent.InputTypes.Touch)
|
||||
local Text = require(script.Parent.InputTypes.Text)
|
||||
local GamePad = require(script.Parent.InputTypes.GamePad)
|
||||
|
||||
local VirtualInputUtils = require(script.Parent.VirtualInputUtils)
|
||||
|
||||
local VirtualInput = {
|
||||
Keyboard = Keyboard,
|
||||
Mouse = Mouse,
|
||||
Touch = Touch,
|
||||
Text = Text,
|
||||
GamePad = GamePad,
|
||||
|
||||
setCurrentWindow = VirtualInputUtils.setCurrentWindow,
|
||||
getCurrentWindow = VirtualInputUtils.getCurrentWindow,
|
||||
}
|
||||
|
||||
return VirtualInput
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
return function()
|
||||
describe("VirtualInput", function()
|
||||
it("should load", function()
|
||||
local VirtualInput = require(script.Parent.VirtualInput)
|
||||
|
||||
expect(VirtualInput).to.be.ok()
|
||||
expect(VirtualInput.Keyboard).to.be.ok()
|
||||
expect(VirtualInput.Mouse).to.be.ok()
|
||||
expect(VirtualInput.Touch).to.be.ok()
|
||||
expect(VirtualInput.GamePad).to.be.ok()
|
||||
expect(VirtualInput.Text).to.be.ok()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
local RunService = game:GetService("RunService")
|
||||
local GuiService = game:GetService("GuiService")
|
||||
|
||||
local runSet = {}
|
||||
local signals = {}
|
||||
|
||||
RunService.Heartbeat:Connect(function(dt)
|
||||
local finishedList = {}
|
||||
for runable, _ in pairs(runSet) do
|
||||
local finished = runable(dt)
|
||||
if finished then
|
||||
table.insert(finishedList, runable)
|
||||
end
|
||||
end
|
||||
|
||||
for _, toDelete in ipairs(finishedList) do
|
||||
runSet[toDelete] = nil
|
||||
end
|
||||
|
||||
for signal, _ in pairs(signals) do
|
||||
if signal == false then
|
||||
signals[signal] = nil
|
||||
else
|
||||
signal:Fire(dt)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
local VirtualInputUtils = {}
|
||||
|
||||
local currentWindow = nil
|
||||
|
||||
function VirtualInputUtils.setCurrentWindow(window)
|
||||
local old = currentWindow
|
||||
currentWindow = window
|
||||
return old
|
||||
end
|
||||
|
||||
function VirtualInputUtils.getCurrentWindow()
|
||||
return currentWindow
|
||||
end
|
||||
|
||||
function VirtualInputUtils.__asyncRun(runable)
|
||||
runSet[runable] = true
|
||||
end
|
||||
|
||||
function VirtualInputUtils.__syncRun(runable)
|
||||
local signal = Instance.new("BindableEvent")
|
||||
signals[signal] = true
|
||||
local dt = 0
|
||||
while true do
|
||||
local finished = runable(dt)
|
||||
if finished then break end
|
||||
dt = signal.Event:Wait()
|
||||
end
|
||||
signals[signal] = false
|
||||
end
|
||||
|
||||
function VirtualInputUtils.__handleGuiInset(x, y)
|
||||
local guiOffset, _ = GuiService:GetGuiInset()
|
||||
return x + guiOffset.X, y + guiOffset.Y
|
||||
end
|
||||
|
||||
return VirtualInputUtils
|
||||
+622
@@ -0,0 +1,622 @@
|
||||
local XPath = {}
|
||||
XPath.__index = XPath
|
||||
XPath.__type = "XPath"
|
||||
|
||||
local specialChars = [[\.=[],]]
|
||||
local specialCharMap = {}
|
||||
for i = 1, #specialChars do
|
||||
local ch = specialChars:sub(i, i)
|
||||
specialCharMap[ch] = true
|
||||
end
|
||||
|
||||
function XPath.addSlash(str)
|
||||
local tab = {}
|
||||
for i = 1, str:len() do
|
||||
local ch = str:sub(i, i)
|
||||
if specialCharMap[ch] then
|
||||
table.insert(tab, "\\")
|
||||
end
|
||||
table.insert(tab, ch)
|
||||
end
|
||||
return table.concat(tab)
|
||||
end
|
||||
|
||||
function XPath.removeSlash(str)
|
||||
local tab = {}
|
||||
local isBackSlash = false
|
||||
for i = 1, str:len() do
|
||||
local ch = str:sub(i, i)
|
||||
if ch == "\\" and isBackSlash == false then
|
||||
isBackSlash = true
|
||||
else
|
||||
if isBackSlash == true then
|
||||
isBackSlash = false
|
||||
end
|
||||
table.insert(tab, ch)
|
||||
end
|
||||
end
|
||||
return table.concat(tab)
|
||||
end
|
||||
|
||||
local function splitByCharWithSlash(s, token)
|
||||
local result = {}
|
||||
if s == nil or s == "" then return result end
|
||||
local isBackSlash = false
|
||||
local lastIndex = 1
|
||||
for i = 1, s:len() do
|
||||
local ch = s:sub(i, i)
|
||||
if ch == "\\" and isBackSlash == false then
|
||||
isBackSlash = true
|
||||
else
|
||||
if isBackSlash == true then
|
||||
isBackSlash = false
|
||||
else
|
||||
if ch == token then
|
||||
table.insert(result, (s:sub(lastIndex, i-1)))
|
||||
lastIndex = i+1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
table.insert(result, (s:sub(lastIndex, s:len())))
|
||||
return result
|
||||
end
|
||||
|
||||
local function deepCopy(t)
|
||||
local t2 = {}
|
||||
for k, v in pairs(t) do
|
||||
if type(v) == "table" then
|
||||
t2[k] = deepCopy(v)
|
||||
else
|
||||
t2[k] = v
|
||||
end
|
||||
end
|
||||
return t2
|
||||
end
|
||||
|
||||
function XPath.new(obj, root)
|
||||
local self = {data = {}, root = root, waitDelay = 0.2, waitTimeOut = 2}
|
||||
setmetatable(self, XPath)
|
||||
|
||||
if type(obj) == "string" then
|
||||
self:fromString(obj)
|
||||
elseif type(obj) == "userdata" then
|
||||
local current = obj
|
||||
while current do
|
||||
local name = current.Name
|
||||
if current.ClassName == "DataModel" then
|
||||
name = "game"
|
||||
end
|
||||
table.insert(self.data, 1, {name = name})
|
||||
current = current.Parent
|
||||
end
|
||||
elseif getmetatable(obj).__type == XPath.__type then
|
||||
return obj:copy()
|
||||
else
|
||||
error("unknown parameter ", obj)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function XPath:size()
|
||||
return #self.data
|
||||
end
|
||||
|
||||
function XPath:mergeFilter(index, additionalFilter)
|
||||
if index > self:size() then error("bad index") end
|
||||
local filter = self.data[index].filter or {}
|
||||
local filterDict = {}
|
||||
for _, item in ipairs(filter) do
|
||||
filterDict[item.key] = item.value
|
||||
end
|
||||
if additionalFilter then
|
||||
for _, item in ipairs(additionalFilter) do
|
||||
filterDict[item.key] = tostring(item.value)
|
||||
end
|
||||
end
|
||||
local newFilter = {}
|
||||
for k, v in pairs(filterDict) do
|
||||
table.insert(newFilter, {key = k, value = v})
|
||||
end
|
||||
self.data[index].filter = newFilter
|
||||
return self
|
||||
end
|
||||
|
||||
function XPath:fromString(str)
|
||||
local inBracket = false
|
||||
local isBackSlash = false
|
||||
local data = {}
|
||||
local lastIndex = 1
|
||||
str = str .. "."
|
||||
for i = 1, str:len() do
|
||||
local ch = str:sub(i, i)
|
||||
if ch == "\\" and isBackSlash == false then
|
||||
isBackSlash = true
|
||||
else
|
||||
if isBackSlash == true then
|
||||
isBackSlash = false
|
||||
else
|
||||
if ch == "." then
|
||||
if not inBracket then
|
||||
table.insert(data, {name = str:sub(lastIndex, i-1)})
|
||||
lastIndex = i+1
|
||||
end
|
||||
elseif ch == "[" then
|
||||
if inBracket == true then
|
||||
error("no nested bracket allowed: " .. str)
|
||||
end
|
||||
inBracket = true
|
||||
elseif ch == "]" then
|
||||
if not inBracket then
|
||||
error("unbalanced brackets: " .. str)
|
||||
end
|
||||
inBracket = false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if inBracket == true then error("unbalanced brackets: " .. str) end
|
||||
|
||||
for i = 1, #data do
|
||||
local name, filters = data[i].name:match("%s*(.*[^\\])%[(.*[^\\])%]%s*")
|
||||
if name == nil then
|
||||
filters = ""
|
||||
name = data[i].name
|
||||
end
|
||||
if name ~= nil then
|
||||
data[i].name = XPath.removeSlash(name)
|
||||
local filterArray = splitByCharWithSlash(filters, ",")
|
||||
local filterObjs = {}
|
||||
for _, filter in ipairs(filterArray) do
|
||||
local key, value = filter:match("^%s*(.-[^\\])%s*=%s*(.-)%s*$")
|
||||
if key then
|
||||
table.insert(filterObjs, {key = key, value = value})
|
||||
end
|
||||
end
|
||||
data[i].filter = filterObjs
|
||||
end
|
||||
end
|
||||
self.data = data
|
||||
end
|
||||
|
||||
function XPath:copy()
|
||||
local result = deepCopy(self)
|
||||
setmetatable(result, XPath)
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
function XPath:parent()
|
||||
local newOne = self:copy()
|
||||
if #newOne.data <= 1 then
|
||||
--error("this is the root")
|
||||
return newOne
|
||||
else
|
||||
table.remove(newOne.data, #newOne.data)
|
||||
end
|
||||
return newOne
|
||||
end
|
||||
|
||||
function XPath:_itemToString(item)
|
||||
local result = XPath.addSlash(item.name)
|
||||
if item.filter and #item.filter > 0 then
|
||||
local filter = {}
|
||||
for _, v in ipairs(item.filter) do
|
||||
table.insert(filter, v.key .. " = " ..v.value)
|
||||
end
|
||||
result = result .. "[" .. table.concat(filter, ", ") .. "]"
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function XPath:toString(arg)
|
||||
if arg == nil then
|
||||
local tab = {}
|
||||
for _, item in ipairs(self.data) do
|
||||
table.insert(tab, self:_itemToString(item))
|
||||
end
|
||||
return table.concat(tab, ".")
|
||||
elseif type(arg) == "number" then
|
||||
if arg < 0 then arg = self:size() + arg + 1 end
|
||||
if arg > self:size() or arg < 1 then error("invalid index") end
|
||||
return self:_itemToString(self.data[arg])
|
||||
elseif type(arg) == "table" then
|
||||
return self:_itemToString(arg)
|
||||
end
|
||||
end
|
||||
|
||||
function XPath:hasChild(child)
|
||||
return child:relative(self) ~= nil
|
||||
end
|
||||
|
||||
function XPath:relative(root)
|
||||
if self:size()<#root.data then return nil end
|
||||
local newRoot = root:copy()
|
||||
local newSelf = self:copy()
|
||||
|
||||
while #newRoot.data > 0 do
|
||||
if newRoot.data[1].name ~= newSelf.data[1].name then return nil end
|
||||
table.remove(newRoot.data, 1)
|
||||
table.remove(newSelf.data, 1)
|
||||
end
|
||||
return newSelf
|
||||
end
|
||||
|
||||
|
||||
function XPath:cat(path)
|
||||
local newOne = self:copy()
|
||||
for _, k in ipairs(path.data) do
|
||||
table.insert(newOne.data, k)
|
||||
end
|
||||
return newOne
|
||||
end
|
||||
|
||||
function XPath:clearFilter()
|
||||
for i = 1, self:size() do
|
||||
self.data[i].filter = nil
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
local function getProperty(instance, property)
|
||||
local state, result = pcall(function() return instance[property] end )
|
||||
return state == true and result or nil
|
||||
end
|
||||
|
||||
local function propertyEqual(lhs, rhs)
|
||||
return tostring(lhs) == tostring(rhs)
|
||||
end
|
||||
|
||||
local function propertyMatch(prop, expr)
|
||||
prop = tostring(prop)
|
||||
expr = tostring(expr)
|
||||
return expr == "*" or prop == expr
|
||||
end
|
||||
|
||||
local function findChildrenByName(instance, name)
|
||||
local children = instance:GetChildren()
|
||||
local result = {}
|
||||
for _, child in ipairs(children) do
|
||||
if propertyMatch(getProperty(child, "Name"), name) then
|
||||
table.insert(result, child)
|
||||
end
|
||||
end
|
||||
-- for game.Players, their is no child "LocalPlayer", but you can access it.
|
||||
if #result == 0 then
|
||||
local instance = getProperty(instance, name)
|
||||
if instance then
|
||||
table.insert(result, instance)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local function findChildren(instances, name)
|
||||
local result = {}
|
||||
for _, instance in ipairs(instances) do
|
||||
local children = findChildrenByName(instance, name)
|
||||
for _, child in ipairs(children) do
|
||||
table.insert(result, child)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local function findCandidates(instances, path)
|
||||
for _, name in ipairs(path) do
|
||||
instances = findChildren(instances, name)
|
||||
end
|
||||
return instances
|
||||
end
|
||||
|
||||
local function passFilter(instance, filter)
|
||||
local key = filter.key
|
||||
local path, propertyName = key:match("^%.?(.*[^\\])%.(%w-)$")
|
||||
if propertyName == nil then
|
||||
path = ""
|
||||
propertyName = key:match("^%.?(%w-)$")
|
||||
end
|
||||
local pathData = splitByCharWithSlash(path, ".")
|
||||
for i = 1, #pathData do pathData[i] = XPath.removeSlash(pathData[i]) end
|
||||
local canditates = findCandidates({instance}, pathData)
|
||||
for _, canditate in ipairs(canditates) do
|
||||
local property = getProperty(canditate, propertyName)
|
||||
if propertyMatch(property, XPath.removeSlash(filter.value)) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function passFilters(instance, filters)
|
||||
for _, filter in ipairs(filters) do
|
||||
if not passFilter(instance, filter) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function applyFilters(canditates, filters)
|
||||
if filters == nil then return canditates end
|
||||
local result = {}
|
||||
for _, canditate in ipairs(canditates) do
|
||||
if passFilters(canditate, filters) then
|
||||
table.insert(result, canditate)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function XPath:getFirstInstance()
|
||||
local instances = self:getInstances()
|
||||
if #instances == 0 then return nil else return instances[1] end
|
||||
end
|
||||
|
||||
function XPath:getInstances()
|
||||
if self:size() <1 then error("instance " .. self:toString() .. " does not exist") end
|
||||
|
||||
local rootInstance = nil
|
||||
local rootName = self.data[1].name
|
||||
|
||||
if rootName == "game" then
|
||||
rootInstance = game
|
||||
elseif rootName == "PluginGuiService" then
|
||||
rootInstance = game:GetService("PluginGuiService")
|
||||
end
|
||||
|
||||
if self.root == nil and rootInstance == nil then
|
||||
error("instance " .. self:toString() .. " does not exist")
|
||||
end
|
||||
|
||||
local instances = {self.root or rootInstance}
|
||||
local i = self.root and 1 or 2
|
||||
while i <= self:size() do
|
||||
local name = self.data[i].name
|
||||
local filters = self.data[i].filter
|
||||
local candidates = findChildren(instances, name)
|
||||
instances = applyFilters(candidates, filters)
|
||||
if #instances == 0 then return instances, i-1 end
|
||||
i = i + 1
|
||||
end
|
||||
return instances, i-1
|
||||
end
|
||||
|
||||
function XPath:setWait(timeOut, delay)
|
||||
self.waitDelay = delay or self.waitDelay
|
||||
self.waitTimeOut = timeOut or self.waitTimeOut
|
||||
return self
|
||||
end
|
||||
|
||||
function XPath:waitFor(execute, condition, delay, timeOut)
|
||||
delay = delay or self.waitDelay
|
||||
timeOut = timeOut or self.waitTimeOut
|
||||
timeOut = tick()+timeOut
|
||||
while true do
|
||||
local result = execute()
|
||||
if condition(result) then
|
||||
return result, true
|
||||
end
|
||||
wait(delay)
|
||||
if tick() > timeOut then return result, false end
|
||||
end
|
||||
end
|
||||
|
||||
function XPath:waitForFirstInstance()
|
||||
-- return self:waitForInstances(function(instances) return #instances >0 end)[1]
|
||||
local instances = self:waitForNInstances(1)
|
||||
if instances ~= nil and #instances > 0 then return instances[1] end
|
||||
return nil
|
||||
end
|
||||
|
||||
function XPath:waitForInstances(condition)
|
||||
if type(condition) ~= "function" then error("arg #1 should be a function") end
|
||||
return self:waitFor(function()
|
||||
return self:getInstances()
|
||||
end, condition)
|
||||
end
|
||||
|
||||
function XPath:waitForDisappear()
|
||||
local _, state = self:waitForInstances(function(instances) return #instances == 0 end)
|
||||
return state == true
|
||||
end
|
||||
|
||||
function XPath:waitForNInstances(n)
|
||||
return self:waitForInstances(function(instances)
|
||||
return #instances >= n
|
||||
end)
|
||||
end
|
||||
|
||||
local function makeInstance(className, props, children)
|
||||
local instance = Instance.new(className)
|
||||
if children then
|
||||
for _, child in ipairs(children) do
|
||||
child.Parent = instance
|
||||
end
|
||||
end
|
||||
if props then
|
||||
for k, v in pairs(props) do
|
||||
instance[k] = v
|
||||
end
|
||||
end
|
||||
return instance
|
||||
end
|
||||
|
||||
local function test()
|
||||
local function closeTo(lhs, rhs, err)
|
||||
return math.abs(lhs-rhs) <= err
|
||||
end
|
||||
|
||||
local specialChars = [[special chars !"#$%&'()*+,-./:;<=>?@[]\^_`{|}~]]
|
||||
local convertedSpecialChars = [[special chars !"#$%&'()*+\,-\./:;<\=>?@\[\]\\^_`{|}~]]
|
||||
|
||||
local root
|
||||
if game.Workspace:FindFirstChild("root") == nil then
|
||||
root = makeInstance("Folder",
|
||||
{
|
||||
Name = "root",
|
||||
Parent = game.Workspace
|
||||
}, {
|
||||
makeInstance("Frame", {Name = "Frame"}, {
|
||||
makeInstance("TextButton", {
|
||||
Text = "Button1"
|
||||
}),
|
||||
makeInstance("TextLabel", {
|
||||
Text = "Label1"
|
||||
}),
|
||||
makeInstance("ImageButton", {
|
||||
|
||||
})
|
||||
}),
|
||||
makeInstance("Frame", {Name = "Frame"}, {
|
||||
makeInstance("TextButton", {
|
||||
Text = "Button2"
|
||||
}),
|
||||
makeInstance("TextLabel", {
|
||||
Text = "Label2"
|
||||
})
|
||||
}),
|
||||
makeInstance("Frame", {Name = "Frame"}, {
|
||||
makeInstance("TextLabel", {Text = "Label3"}),
|
||||
makeInstance("Frame", {Name = specialChars}, {
|
||||
makeInstance("TextButton", {Name = "TextButton3"})
|
||||
}),
|
||||
makeInstance("TextLabel", {Name = "SpecialCharLabel", Text = specialChars})
|
||||
})
|
||||
})
|
||||
end
|
||||
|
||||
local containerPath = XPath.new("game.Workspace.root.Frame")
|
||||
|
||||
local getContainerDetail = function(root)
|
||||
return {
|
||||
textButton = XPath.new("TextButton", root),
|
||||
textLabel = XPath.new("TextLabel", root),
|
||||
}
|
||||
end
|
||||
|
||||
local createSearch = function(container, relativePath, property, value)
|
||||
local rootPath = container:copy()
|
||||
local filter = {{key = "." .. relativePath:toString().."."..property, value = value}}
|
||||
rootPath:mergeFilter(rootPath:size(), filter)
|
||||
return rootPath
|
||||
end
|
||||
|
||||
local rootPath = createSearch(containerPath, getContainerDetail().textButton, "Text", "Button2")
|
||||
print("createSearch:", rootPath:toString())
|
||||
local rootInstance = rootPath:waitForFirstInstance()
|
||||
assert(rootInstance)
|
||||
|
||||
local containerDetail = getContainerDetail(rootInstance)
|
||||
|
||||
local label2 = containerDetail.textLabel:waitForFirstInstance()
|
||||
assert(label2.Text == "Label2")
|
||||
|
||||
print("relative path test")
|
||||
|
||||
local path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button2]")
|
||||
local instance = path:getFirstInstance()
|
||||
assert(instance)
|
||||
local relativePath = XPath.new("TextButton", instance)
|
||||
local instance = relativePath:getFirstInstance()
|
||||
assert(instance.Text == "Button2")
|
||||
|
||||
local path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button2, .ClassName = Frame].TextLabel")
|
||||
local instance = path:getFirstInstance()
|
||||
assert(instance.Text=="Label2")
|
||||
local newPathString = XPath.new(instance):toString()
|
||||
assert(newPathString=="game.Workspace.root.Frame.TextLabel")
|
||||
|
||||
local pathStr = "game.Workspace.root.Frame[."..convertedSpecialChars..".TextButton3.Name = TextButton3].TextLabel"
|
||||
local path = XPath.new(pathStr)
|
||||
assert(path:toString()==pathStr)
|
||||
local instance = path:getFirstInstance()
|
||||
assert(instance.Text=="Label3")
|
||||
|
||||
local pathStr = "game.Workspace.root.Frame[.SpecialCharLabel.Text = "..convertedSpecialChars.."].TextLabel"
|
||||
local path = XPath.new(pathStr)
|
||||
assert(path:toString()==pathStr)
|
||||
local instance = path:getFirstInstance()
|
||||
assert(instance.Text=="Label3")
|
||||
|
||||
local pathStr = "game.Workspace.root.Frame."..convertedSpecialChars..".TextButton3"
|
||||
local path = XPath.new(pathStr)
|
||||
local newPathStr = path:toString()
|
||||
assert(newPathStr==pathStr)
|
||||
local instance = path:getFirstInstance()
|
||||
assert(instance.Name=="TextButton3")
|
||||
|
||||
local rootPath = XPath.new("game.Workspace.root")
|
||||
local path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button2, .ClassName = Frame].TextLabel")
|
||||
local relativePath = path:relative(rootPath)
|
||||
relativePath:clearFilter()
|
||||
|
||||
print("testing getFirstInstance()")
|
||||
print(path:toString())
|
||||
local instance = path:getFirstInstance()
|
||||
assert(instance.Text == "Label2")
|
||||
|
||||
print("testing wildcard *")
|
||||
path = XPath.new("game.Workspace.root.*[.TextButton.Text = Button2].TextLabel")
|
||||
instance = path:getFirstInstance()
|
||||
assert(instance.Text == "Label2")
|
||||
|
||||
path = XPath.new("game.Workspace.root.*[.ImageButton.Name = *].TextLabel")
|
||||
instance = path:getFirstInstance()
|
||||
assert(instance.Text == "Label1")
|
||||
|
||||
local timeBefore = nil
|
||||
print("testing timeout waitForNInstances() ")
|
||||
path = XPath.new("game.Workspace.root.Frame.TextLabel")
|
||||
timeBefore = tick()
|
||||
local instances, state = path:setWait(2):waitForNInstances(5)
|
||||
assert(closeTo(tick() - timeBefore, 2, 0.5))
|
||||
assert(state == false)
|
||||
|
||||
print("testing normal waitForNInstances() ")
|
||||
timeBefore = tick()
|
||||
spawn(function()
|
||||
wait(2)
|
||||
makeInstance("Frame", {
|
||||
Name = "Frame",
|
||||
Parent = game.Workspace.root
|
||||
}, {
|
||||
makeInstance("TextButton", {
|
||||
Text = "Button3"
|
||||
}),
|
||||
makeInstance("TextLabel", {
|
||||
Text = "Label3"
|
||||
})
|
||||
})
|
||||
end)
|
||||
local instances = path:setWait(5):waitForNInstances(5)
|
||||
assert(closeTo(tick() - timeBefore, 2, 0.5))
|
||||
assert(#instances >= 5)
|
||||
|
||||
print("testing getFirstInstance() ")
|
||||
path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button3]")
|
||||
instance = path:getFirstInstance()
|
||||
assert(instance.TextButton.Text == "Button3")
|
||||
|
||||
print("testing timeout waitForDisappear() ")
|
||||
timeBefore = tick()
|
||||
local notExist = path:setWait(2):waitForDisappear()
|
||||
assert(notExist == false)
|
||||
assert(closeTo(tick() - timeBefore, 2, 0.5))
|
||||
|
||||
print("testing normal waitForDisappear() ")
|
||||
timeBefore = tick()
|
||||
spawn(function()
|
||||
wait(2)
|
||||
instance:Destroy()
|
||||
end)
|
||||
local notExist = path:setWait(5):waitForDisappear()
|
||||
assert(closeTo(tick() - timeBefore, 2, 0.5))
|
||||
assert(notExist == true)
|
||||
|
||||
print("test finised")
|
||||
end
|
||||
|
||||
--test()
|
||||
|
||||
return XPath
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
return function()
|
||||
local XPath = require(script.Parent.XPath)
|
||||
|
||||
local function makeInstance(className, props, children)
|
||||
local instance = Instance.new(className)
|
||||
if children then
|
||||
for _, child in ipairs(children) do
|
||||
child.Parent = instance
|
||||
end
|
||||
end
|
||||
if props then
|
||||
for k, v in pairs(props) do
|
||||
instance[k] = v
|
||||
end
|
||||
end
|
||||
return instance
|
||||
end
|
||||
|
||||
local specialChars = [[special chars !"#$%&'()*+,-./:;<=>?@[]\^_`{|}~]]
|
||||
local convertedSpecialChars = [[special chars !"#$%&'()*+\,-\./:;<\=>?@\[\]\\^_`{|}~]]
|
||||
|
||||
local root = makeInstance("Folder",
|
||||
{
|
||||
Name = "root",
|
||||
Parent = workspace
|
||||
}, {
|
||||
makeInstance("Frame", {Name = "Frame"}, {
|
||||
makeInstance("TextButton", {
|
||||
Text = "Button1"
|
||||
}),
|
||||
makeInstance("TextLabel", {
|
||||
Text = "Label1"
|
||||
}),
|
||||
makeInstance("ImageButton", {
|
||||
|
||||
})
|
||||
}),
|
||||
makeInstance("Frame", {Name = "Frame"}, {
|
||||
makeInstance("TextButton", {
|
||||
Text = "Button2"
|
||||
}),
|
||||
makeInstance("TextLabel", {
|
||||
Text = "Label2"
|
||||
})
|
||||
}),
|
||||
makeInstance("Frame", {Name = "Frame"}, {
|
||||
makeInstance("TextLabel", {Text = "Label3"}),
|
||||
makeInstance("Frame", {Name = specialChars}, {
|
||||
makeInstance("TextButton", {Name = "TextButton3"})
|
||||
}),
|
||||
makeInstance("TextLabel", {Name = "SpecialCharLabel", Text = specialChars})
|
||||
})
|
||||
})
|
||||
|
||||
describe("basic tests", function()
|
||||
it("getFirstInstance should work", function()
|
||||
local path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button2, .ClassName = Frame].TextLabel")
|
||||
local instance = path:getFirstInstance()
|
||||
expect(instance.Text).to.equal("Label2")
|
||||
expect(XPath.new(instance):toString()).to.equal("game.Workspace.root.Frame.TextLabel")
|
||||
end)
|
||||
it("wildcard should work", function()
|
||||
local path = XPath.new("game.Workspace.root.*[.TextButton.Text = Button2].TextLabel")
|
||||
local instance = path:getFirstInstance()
|
||||
expect(instance.Text).to.equal("Label2")
|
||||
end)
|
||||
it("wildcard on property should work", function()
|
||||
local path = XPath.new("game.Workspace.root.*[.ImageButton.Name = *].TextLabel")
|
||||
local instance = path:getFirstInstance()
|
||||
expect(instance.Text).to.equal("Label1")
|
||||
end)
|
||||
end)
|
||||
describe("should work with special chars", function()
|
||||
it("should work with special chars in path", function()
|
||||
local pathStr = "game.Workspace.root.Frame."..convertedSpecialChars..".TextButton3"
|
||||
local path = XPath.new(pathStr)
|
||||
expect(path:toString()).to.equal(pathStr)
|
||||
local instance = path:getFirstInstance()
|
||||
expect(instance.Name).to.equal("TextButton3")
|
||||
end)
|
||||
it("should work with special chars in filter keys", function()
|
||||
local pathStr = "game.Workspace.root.Frame[."..convertedSpecialChars..".TextButton3.Name = TextButton3].TextLabel"
|
||||
local path = XPath.new(pathStr)
|
||||
expect(path:toString()).to.equal(pathStr)
|
||||
local instance = path:getFirstInstance()
|
||||
expect(instance.Text).to.equal("Label3")
|
||||
end)
|
||||
it("should work with special chars in filter values", function()
|
||||
local pathStr = "game.Workspace.root.Frame[.SpecialCharLabel.Text = "..convertedSpecialChars.."].TextLabel"
|
||||
local path = XPath.new(pathStr)
|
||||
expect(path:toString()).to.equal(pathStr)
|
||||
local instance = path:getFirstInstance()
|
||||
expect(instance.Text).to.equal("Label3")
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
local Element = require(script.Element)
|
||||
local VirtualInput = require(script.VirtualInput)
|
||||
local XPath = require(script.XPath)
|
||||
|
||||
local Rhodium = {
|
||||
Element = Element,
|
||||
VirtualInput = VirtualInput,
|
||||
XPath = XPath,
|
||||
}
|
||||
|
||||
return Rhodium
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
return function()
|
||||
describe("Rhodium", function()
|
||||
it("should load", function()
|
||||
local Rhodium = require(script.Parent)
|
||||
|
||||
expect(Rhodium).to.be.ok()
|
||||
expect(Rhodium.Element).to.be.ok()
|
||||
expect(Rhodium.XPath).to.be.ok()
|
||||
expect(Rhodium.VirtualInput).to.be.ok()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
Reference in New Issue
Block a user