This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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