pulse-rb 1.3.2 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adapters/windui.lua +711 -6
- package/bin/rb.js +0 -0
- package/dist/index.js +117 -42
- package/package.json +11 -10
- package/pulse/helpers/log.lua +6 -1
- package/pulse/runtime.lua +112 -28
- package/templates/component.ts +11 -0
- package/templates/globals.lua +10 -26
- package/templates/page_home.ts +3 -6
package/adapters/windui.lua
CHANGED
|
@@ -5,13 +5,20 @@
|
|
|
5
5
|
|
|
6
6
|
-- Executor loadstring() environments are sandboxed — each chunk has its OWN environment
|
|
7
7
|
-- table, separate from _G. Global reads in this chunk go through getfenv(1), not _G.
|
|
8
|
-
-- The compiler passes
|
|
9
|
-
|
|
10
|
-
local _P = ...
|
|
8
|
+
-- The compiler passes (_P, _PULSE_LAYOUT) as arguments.
|
|
9
|
+
local _P, _PULSE_LAYOUT = ...
|
|
11
10
|
if type(_P) == "table" then
|
|
12
11
|
local _env = (getfenv and getfenv(1)) or _ENV or _G
|
|
13
12
|
for _k, _v in pairs(_P) do _env[_k] = _v end
|
|
14
13
|
end
|
|
14
|
+
_PULSE_LAYOUT = _PULSE_LAYOUT or {}
|
|
15
|
+
|
|
16
|
+
-- Capture bundle table references as upvalues immediately so the startup block
|
|
17
|
+
-- can access them without global lookups (which are environment-table-dependent
|
|
18
|
+
-- in executor sandboxes and may fail in task.spawn callbacks).
|
|
19
|
+
local _BundleComponents = type(_P) == "table" and _P.Components or {}
|
|
20
|
+
local _BundlePages = type(_P) == "table" and _P._Pages or {}
|
|
21
|
+
local _BundlePulse = type(_P) == "table" and _P.Pulse or Pulse
|
|
15
22
|
|
|
16
23
|
local _WINDUI_URL = "https://github.com/Footagesus/WindUI/releases/download/1.6.64-fix/main.lua"
|
|
17
24
|
local _PULSE_LOGO = "https://pulse-rb.vercel.app/img/logo.svg"
|
|
@@ -177,6 +184,9 @@ function _UIAdapter:CreateWindow(title, w, h, opts)
|
|
|
177
184
|
local tabProxy = { _wind = windTab }
|
|
178
185
|
function tabProxy:AddLeftGroupbox(title, gbIcon) return _mkSection(leftCol, title, gbIcon) end
|
|
179
186
|
function tabProxy:AddRightGroupbox(title, gbIcon) return _mkSection(rightCol, title, gbIcon) end
|
|
187
|
+
-- Raw column access — no Section wrapper (used by plain=true groupboxes)
|
|
188
|
+
function tabProxy:GetLeftColumn() return { _container = leftCol } end
|
|
189
|
+
function tabProxy:GetRightColumn() return { _container = rightCol } end
|
|
180
190
|
return tabProxy
|
|
181
191
|
end
|
|
182
192
|
|
|
@@ -210,7 +220,12 @@ function _UIAdapter:addToggle(gb, id, opts)
|
|
|
210
220
|
Desc = opts.tip or nil,
|
|
211
221
|
Value = opts.signal(),
|
|
212
222
|
Flag = id,
|
|
213
|
-
Callback = function(v)
|
|
223
|
+
Callback = function(v)
|
|
224
|
+
if _BundlePulse and _BundlePulse.Log then
|
|
225
|
+
_BundlePulse.Log.debug("ui", "toggle "..tostring(id).." → "..tostring(v))
|
|
226
|
+
end
|
|
227
|
+
opts.signal(v)
|
|
228
|
+
end,
|
|
214
229
|
})
|
|
215
230
|
_widgets[id] = elem
|
|
216
231
|
_G.Toggles = _G.Toggles or {}
|
|
@@ -240,7 +255,12 @@ function _UIAdapter:addSlider(gb, id, opts)
|
|
|
240
255
|
Step = step,
|
|
241
256
|
Value = { Min = min, Max = max, Default = def },
|
|
242
257
|
Flag = id,
|
|
243
|
-
Callback = function(v)
|
|
258
|
+
Callback = function(v)
|
|
259
|
+
if _BundlePulse and _BundlePulse.Log then
|
|
260
|
+
_BundlePulse.Log.debug("ui", "slider "..tostring(id).." → "..tostring(v))
|
|
261
|
+
end
|
|
262
|
+
opts.signal(v)
|
|
263
|
+
end,
|
|
244
264
|
})
|
|
245
265
|
_widgets[id] = elem
|
|
246
266
|
_G.Options = _G.Options or {}
|
|
@@ -357,7 +377,7 @@ function _UIAdapter:mount(component, gb)
|
|
|
357
377
|
if not ui then return end
|
|
358
378
|
for _, w in ipairs(ui) do
|
|
359
379
|
local t = w.type
|
|
360
|
-
local sig =
|
|
380
|
+
local sig = w.signal -- PulseSignal stored directly by .bind(sig) in TS widget builders
|
|
361
381
|
if t == "toggle" then
|
|
362
382
|
self:addToggle(gb, w.id, { label = w.label, signal = sig, tip = w.tip })
|
|
363
383
|
elseif t == "slider" then
|
|
@@ -509,3 +529,688 @@ function Pulse.UI.section(container, title, icon, opened)
|
|
|
509
529
|
function proxy:section(...) return Pulse.UI.section(self, ...) end
|
|
510
530
|
return proxy
|
|
511
531
|
end
|
|
532
|
+
|
|
533
|
+
-- ── Startup — create window, render pages, build Settings tab ─────────────────
|
|
534
|
+
-- Runs after user code has registered all components and pages (task.spawn defers
|
|
535
|
+
-- execution to the next frame, by which point all top-level TS calls have run).
|
|
536
|
+
|
|
537
|
+
task.spawn(function()
|
|
538
|
+
task.wait(0) -- yield one frame so definePage/defineComponent calls finish
|
|
539
|
+
|
|
540
|
+
local L = _PULSE_LAYOUT
|
|
541
|
+
|
|
542
|
+
-- Resolve toggle key
|
|
543
|
+
local _tkName = (L.toggleKey and L.toggleKey ~= "") and L.toggleKey or "RightControl"
|
|
544
|
+
local _tk = pcall(function() return Enum.KeyCode[_tkName] end) and Enum.KeyCode[_tkName]
|
|
545
|
+
_PULSE_TOGGLE_KEY = _tk
|
|
546
|
+
_PULSE_DEFAULT_THEME = L.theme or "Dark"
|
|
547
|
+
|
|
548
|
+
-- Create the Wind UI window
|
|
549
|
+
local _win = _UIAdapter:CreateWindow(
|
|
550
|
+
L.title or "Hub",
|
|
551
|
+
L.sizeW or 950,
|
|
552
|
+
L.sizeH or 600,
|
|
553
|
+
{
|
|
554
|
+
icon = (L.icon and L.icon ~= "") and L.icon or nil,
|
|
555
|
+
author = L.author or nil,
|
|
556
|
+
theme = L.theme or "Dark",
|
|
557
|
+
folder = (L.folder and L.folder ~= "") and L.folder or "Hub",
|
|
558
|
+
acrylic = L.acrylic or false,
|
|
559
|
+
transparency = L.transparency or 0.8,
|
|
560
|
+
toggleKey = _tk,
|
|
561
|
+
openButtonMobileOnly = L.openButtonMobileOnly or false,
|
|
562
|
+
openButtonIcon = (L.openButtonIcon and L.openButtonIcon ~= "") and L.openButtonIcon or nil,
|
|
563
|
+
}
|
|
564
|
+
)
|
|
565
|
+
_UIAdapter:SetToggleKey(_tk)
|
|
566
|
+
|
|
567
|
+
-- ── Home tab ──────────────────────────────────────────────────────────────
|
|
568
|
+
-- Always the first tab: player welcome, script info, executor/game context.
|
|
569
|
+
|
|
570
|
+
-- Player info
|
|
571
|
+
local _PS2 = game:GetService("Players")
|
|
572
|
+
local _LP2 = _PS2.LocalPlayer
|
|
573
|
+
local _dspName = (_LP2 and _LP2.DisplayName) or "Player"
|
|
574
|
+
local _usrName = (_LP2 and _LP2.Name) or ""
|
|
575
|
+
|
|
576
|
+
-- Avatar thumbnail (GetUserThumbnailAsync yields — fine inside task.spawn)
|
|
577
|
+
local _avatarUrl = ""
|
|
578
|
+
pcall(function()
|
|
579
|
+
if _LP2 then
|
|
580
|
+
_avatarUrl = _PS2:GetUserThumbnailAsync(
|
|
581
|
+
_LP2.UserId,
|
|
582
|
+
Enum.ThumbnailType.HeadShot,
|
|
583
|
+
Enum.ThumbnailSize.Size100x100
|
|
584
|
+
)
|
|
585
|
+
end
|
|
586
|
+
end)
|
|
587
|
+
|
|
588
|
+
-- Executor detection
|
|
589
|
+
local _execName = "Unknown"
|
|
590
|
+
local _execVer = ""
|
|
591
|
+
pcall(function()
|
|
592
|
+
if type(rawget(_G, "identifyexecutor")) == "function" then
|
|
593
|
+
local ok, n, v = pcall(identifyexecutor)
|
|
594
|
+
if ok then _execName = tostring(n or "Unknown"); _execVer = tostring(v or "") end
|
|
595
|
+
end
|
|
596
|
+
if _execName == "Unknown" and type(rawget(_G, "getexecutorname")) == "function" then
|
|
597
|
+
local ok, n = pcall(getexecutorname)
|
|
598
|
+
if ok and n then _execName = tostring(n) end
|
|
599
|
+
end
|
|
600
|
+
if _execName == "Unknown" then
|
|
601
|
+
if rawget(_G, "syn") or rawget(_G, "Synapse") then _execName = "Synapse X"
|
|
602
|
+
elseif rawget(_G, "KRNL_LOADED") then _execName = "Krnl"
|
|
603
|
+
elseif rawget(_G, "DELTA_KEY") then _execName = "Delta"
|
|
604
|
+
elseif rawget(_G, "WAVE_LOADED") then _execName = "Wave"
|
|
605
|
+
elseif rawget(_G, "SOLARA_LOADED") then _execName = "Solara"
|
|
606
|
+
elseif rawget(_G, "XENO_LOADED") then _execName = "Xeno"
|
|
607
|
+
elseif rawget(_G, "Seliware") or rawget(_G, "seliware") then _execName = "Seliware"
|
|
608
|
+
elseif rawget(_G, "Celery") or rawget(_G, "celery") then _execName = "Celery"
|
|
609
|
+
elseif rawget(_G, "is_sirhurt_closure") then _execName = "Sirhurt"
|
|
610
|
+
elseif rawget(_G, "EXECUTOR_NAME") then _execName = tostring(rawget(_G, "EXECUTOR_NAME"))
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
end)
|
|
614
|
+
|
|
615
|
+
-- Game name (GetProductInfo yields)
|
|
616
|
+
local _gameName = "PlaceId " .. tostring(game.PlaceId)
|
|
617
|
+
pcall(function()
|
|
618
|
+
local info = game:GetService("MarketplaceService"):GetProductInfo(game.PlaceId)
|
|
619
|
+
if info and info.Name then _gameName = info.Name end
|
|
620
|
+
end)
|
|
621
|
+
|
|
622
|
+
-- Home tab — raw columns, no Section wrappers for a clean look.
|
|
623
|
+
-- Create the tab directly so we can get the raw VStack columns from _mkColumns.
|
|
624
|
+
local _homeWindTab
|
|
625
|
+
pcall(function() _homeWindTab = _windWindow:Tab({ Title = "Home", Icon = "house" }) end)
|
|
626
|
+
if not _UIAdapter._firstTab then _UIAdapter._firstTab = _homeWindTab end
|
|
627
|
+
local _homeLeft, _homeRight = _mkColumns(_homeWindTab)
|
|
628
|
+
|
|
629
|
+
-- Left: player info + description + discord (no Section box)
|
|
630
|
+
if _homeLeft then
|
|
631
|
+
-- Capture the returned paragraph element so we can try avatar injection via its Frame
|
|
632
|
+
local _paraRef = nil
|
|
633
|
+
pcall(function()
|
|
634
|
+
_paraRef = _homeLeft:Paragraph({
|
|
635
|
+
Title = "Welcome, " .. _dspName,
|
|
636
|
+
Desc = "@" .. _usrName,
|
|
637
|
+
})
|
|
638
|
+
end)
|
|
639
|
+
|
|
640
|
+
-- Avatar injection: WindUI paragraph elements wrap a Roblox Frame internally.
|
|
641
|
+
-- Grab that Frame's parent (the list container) and add an ImageLabel there.
|
|
642
|
+
if _avatarUrl ~= "" and _paraRef then
|
|
643
|
+
task.delay(0.3, function()
|
|
644
|
+
pcall(function()
|
|
645
|
+
local paraFrame = nil
|
|
646
|
+
for _, k in ipairs({"Frame","Object","Instance","Container","Label","Content"}) do
|
|
647
|
+
local ok, v = pcall(function() return _paraRef[k] end)
|
|
648
|
+
if ok and type(v) == "userdata" then
|
|
649
|
+
local _, isGui = pcall(function() return v:IsA("GuiObject") end)
|
|
650
|
+
if isGui then paraFrame = v; break end
|
|
651
|
+
end
|
|
652
|
+
end
|
|
653
|
+
local contentFrame = paraFrame and paraFrame.Parent
|
|
654
|
+
if not (contentFrame and type(contentFrame) == "userdata") then return end
|
|
655
|
+
if contentFrame:FindFirstChild("_PulseAvatar") then return end
|
|
656
|
+
local img = Instance.new("ImageLabel")
|
|
657
|
+
img.Name = "_PulseAvatar"; img.Image = _avatarUrl
|
|
658
|
+
img.Size = UDim2.fromOffset(64, 64); img.BackgroundTransparency = 1
|
|
659
|
+
img.ScaleType = Enum.ScaleType.Crop; img.LayoutOrder = -999
|
|
660
|
+
Instance.new("UICorner", img).CornerRadius = UDim.new(1, 0)
|
|
661
|
+
img.Parent = contentFrame
|
|
662
|
+
end)
|
|
663
|
+
end)
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
if L.description and L.description ~= "" then
|
|
667
|
+
pcall(function() _homeLeft:Divider() end)
|
|
668
|
+
pcall(function() _homeLeft:Paragraph({ Title = "About", Desc = L.description }) end)
|
|
669
|
+
end
|
|
670
|
+
if L.discord and L.discord ~= "" then
|
|
671
|
+
pcall(function() _homeLeft:Divider() end)
|
|
672
|
+
pcall(function()
|
|
673
|
+
_homeLeft:Button({
|
|
674
|
+
Title = "Join Discord",
|
|
675
|
+
Desc = "Tap to copy the invite link",
|
|
676
|
+
Callback = function()
|
|
677
|
+
local ok = false
|
|
678
|
+
if not ok then pcall(function() setclipboard(L.discord); ok=true end) end
|
|
679
|
+
if not ok then pcall(function() toclipboard(L.discord); ok=true end) end
|
|
680
|
+
if not ok then pcall(function() writeclipboard(L.discord); ok=true end) end
|
|
681
|
+
_PulseNotify(ok and "Discord link copied!" or "No clipboard API", 3)
|
|
682
|
+
end,
|
|
683
|
+
})
|
|
684
|
+
end)
|
|
685
|
+
end
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
-- Right: script info + executor + game (no Section box)
|
|
689
|
+
if _homeRight then
|
|
690
|
+
local _verStr = (L.version and L.version ~= "") and ("v" .. L.version) or ""
|
|
691
|
+
local _authStr = (L.author and L.author ~= "") and ("by " .. L.author) or ""
|
|
692
|
+
local _metaParts = {}
|
|
693
|
+
if _verStr ~= "" then _metaParts[#_metaParts+1] = _verStr end
|
|
694
|
+
if _authStr ~= "" then _metaParts[#_metaParts+1] = _authStr end
|
|
695
|
+
pcall(function()
|
|
696
|
+
_homeRight:Paragraph({
|
|
697
|
+
Title = L.title or "Hub",
|
|
698
|
+
Desc = #_metaParts > 0 and table.concat(_metaParts, " · ") or "—",
|
|
699
|
+
})
|
|
700
|
+
end)
|
|
701
|
+
pcall(function() _homeRight:Divider() end)
|
|
702
|
+
local _execStr = _execName .. (_execVer ~= "" and (" " .. _execVer) or "")
|
|
703
|
+
pcall(function() _homeRight:Paragraph({ Title = "Executor", Desc = _execStr }) end)
|
|
704
|
+
pcall(function() _homeRight:Paragraph({ Title = "Game", Desc = _gameName }) end)
|
|
705
|
+
pcall(function() _homeRight:Divider() end)
|
|
706
|
+
-- Server info: live player count + rejoin
|
|
707
|
+
local _playerCount = 0
|
|
708
|
+
pcall(function() _playerCount = #game:GetService("Players"):GetPlayers() end)
|
|
709
|
+
pcall(function()
|
|
710
|
+
_homeRight:Paragraph({
|
|
711
|
+
Title = "Server",
|
|
712
|
+
Desc = tostring(_playerCount) .. " player" .. (_playerCount == 1 and "" or "s") .. " · " .. tostring(game.PlaceId),
|
|
713
|
+
})
|
|
714
|
+
end)
|
|
715
|
+
pcall(function()
|
|
716
|
+
_homeRight:Button({
|
|
717
|
+
Title = "Rejoin Server",
|
|
718
|
+
Desc = "Leave and rejoin the current place",
|
|
719
|
+
Callback = function()
|
|
720
|
+
pcall(function()
|
|
721
|
+
game:GetService("TeleportService"):TeleportToPlaceInstance(
|
|
722
|
+
game.PlaceId, game.JobId,
|
|
723
|
+
game:GetService("Players").LocalPlayer
|
|
724
|
+
)
|
|
725
|
+
end)
|
|
726
|
+
end,
|
|
727
|
+
})
|
|
728
|
+
end)
|
|
729
|
+
if L.unsupported and L.unsupported ~= "" then
|
|
730
|
+
local _unsupList = {}
|
|
731
|
+
if type(L.unsupported) == "table" then
|
|
732
|
+
for _, v in ipairs(L.unsupported) do _unsupList[#_unsupList+1] = tostring(v) end
|
|
733
|
+
elseif type(L.unsupported) == "string" then
|
|
734
|
+
for part in L.unsupported:gmatch("[^,]+") do
|
|
735
|
+
_unsupList[#_unsupList+1] = part:match("^%s*(.-)%s*$")
|
|
736
|
+
end
|
|
737
|
+
end
|
|
738
|
+
if #_unsupList > 0 then
|
|
739
|
+
pcall(function() _homeRight:Divider() end)
|
|
740
|
+
pcall(function()
|
|
741
|
+
_homeRight:Paragraph({
|
|
742
|
+
Title = "Not compatible with:",
|
|
743
|
+
Desc = table.concat(_unsupList, ", "),
|
|
744
|
+
})
|
|
745
|
+
end)
|
|
746
|
+
end
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
-- Render user pages (upvalue _BundlePages — avoids global lookup in executor sandbox)
|
|
751
|
+
for _, page in ipairs(_BundlePages) do
|
|
752
|
+
local tab = _win:AddTab(page.title, page.icon)
|
|
753
|
+
for _, gb in ipairs(page.layout or {}) do
|
|
754
|
+
local container
|
|
755
|
+
if gb.plain then
|
|
756
|
+
-- No Section wrapper — widgets go directly onto the column VStack
|
|
757
|
+
if gb.side == "left" then
|
|
758
|
+
if tab.GetLeftColumn then container = tab:GetLeftColumn() end
|
|
759
|
+
else
|
|
760
|
+
if tab.GetRightColumn then container = tab:GetRightColumn() end
|
|
761
|
+
end
|
|
762
|
+
elseif gb.side == "left" then
|
|
763
|
+
container = tab:AddLeftGroupbox(gb.title, gb.icon)
|
|
764
|
+
else
|
|
765
|
+
container = tab:AddRightGroupbox(gb.title, gb.icon)
|
|
766
|
+
end
|
|
767
|
+
if gb.mount then
|
|
768
|
+
local comp = _BundleComponents[gb.mount]
|
|
769
|
+
if comp then
|
|
770
|
+
local ok, err = pcall(function() _UIAdapter:mount(comp, container) end)
|
|
771
|
+
if not ok then
|
|
772
|
+
warn("[Pulse] mount '" .. gb.mount .. "' error: " .. tostring(err))
|
|
773
|
+
if _BundlePulse and _BundlePulse.Log then
|
|
774
|
+
_BundlePulse.Log.error("ui", "mount '"..gb.mount.."' failed: "..tostring(err))
|
|
775
|
+
end
|
|
776
|
+
else
|
|
777
|
+
if _BundlePulse and _BundlePulse.Log then
|
|
778
|
+
local nw = comp._ui and #comp._ui or 0
|
|
779
|
+
_BundlePulse.Log.debug("ui", "mounted '"..gb.mount.."' ("..nw.." widgets)")
|
|
780
|
+
end
|
|
781
|
+
end
|
|
782
|
+
else
|
|
783
|
+
warn("[Pulse] component '" .. gb.mount .. "' not found in registry")
|
|
784
|
+
if _BundlePulse and _BundlePulse.Log then
|
|
785
|
+
_BundlePulse.Log.error("ui", "component '"..gb.mount.."' not in registry")
|
|
786
|
+
end
|
|
787
|
+
end
|
|
788
|
+
else
|
|
789
|
+
for _, w in ipairs(gb.widgets or {}) do
|
|
790
|
+
pcall(function() _UIAdapter:mount({ _ui = {w}, _name = gb.title }, container) end)
|
|
791
|
+
end
|
|
792
|
+
end
|
|
793
|
+
end
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
-- ── Settings tab ──────────────────────────────────────────────────────────
|
|
797
|
+
local _settingsTab = _win:AddTab("Settings", "settings")
|
|
798
|
+
|
|
799
|
+
-- Appearance groupbox
|
|
800
|
+
local gb_appear = _settingsTab:AddLeftGroupbox("Appearance")
|
|
801
|
+
local _themeValues = _UIAdapter:GetThemeNames()
|
|
802
|
+
or { "Amber","CottonCandy","Crimson","Dark","Emerald","Indigo","Light",
|
|
803
|
+
"Mellowsi","Midnight","MonokaiPro","Plant","Rainbow","Red","Rose","Sky","Violet" }
|
|
804
|
+
gb_appear._container:Dropdown({
|
|
805
|
+
Title = "Theme",
|
|
806
|
+
Values = _themeValues,
|
|
807
|
+
Value = _PULSE_DEFAULT_THEME,
|
|
808
|
+
Flag = "Settings_theme",
|
|
809
|
+
Callback = function(v) _UIAdapter:SetTheme(v) end,
|
|
810
|
+
})
|
|
811
|
+
|
|
812
|
+
-- Config groupbox
|
|
813
|
+
local gb_cfg = _settingsTab:AddLeftGroupbox("Configuration")
|
|
814
|
+
_UIAdapter:addParagraph(gb_cfg,
|
|
815
|
+
"Config is stored in your executor's filesystem.",
|
|
816
|
+
"Save writes the current state. Load restores the last saved state."
|
|
817
|
+
)
|
|
818
|
+
gb_cfg._container:Button({
|
|
819
|
+
Title = "Save Config",
|
|
820
|
+
Icon = "save",
|
|
821
|
+
Callback = function()
|
|
822
|
+
if _UIAdapter.Config then
|
|
823
|
+
local ok, err = pcall(function() _UIAdapter.Config:Save() end)
|
|
824
|
+
_PulseNotify(ok and "Config saved." or ("Save failed: " .. tostring(err)), 3)
|
|
825
|
+
else
|
|
826
|
+
_PulseNotify("Config manager unavailable.", 3)
|
|
827
|
+
end
|
|
828
|
+
end,
|
|
829
|
+
})
|
|
830
|
+
gb_cfg._container:Button({
|
|
831
|
+
Title = "Load Config",
|
|
832
|
+
Icon = "folder-open",
|
|
833
|
+
Callback = function()
|
|
834
|
+
if _UIAdapter.Config then
|
|
835
|
+
local ok, err = pcall(function() _UIAdapter.Config:Load() end)
|
|
836
|
+
_PulseNotify(ok and "Config loaded." or ("Load failed: " .. tostring(err)), 3)
|
|
837
|
+
else
|
|
838
|
+
_PulseNotify("Config manager unavailable.", 3)
|
|
839
|
+
end
|
|
840
|
+
end,
|
|
841
|
+
})
|
|
842
|
+
|
|
843
|
+
-- Menu settings groupbox
|
|
844
|
+
local gb_menu = _settingsTab:AddRightGroupbox("Menu Settings")
|
|
845
|
+
_UIAdapter:addButton(gb_menu, {
|
|
846
|
+
label = "Destroy UI",
|
|
847
|
+
action = function() _PulseDestroy() end,
|
|
848
|
+
tip = "Unloads the script and removes all UI",
|
|
849
|
+
})
|
|
850
|
+
_UIAdapter:addKeybind(gb_menu, "Settings_MenuKeybind", {
|
|
851
|
+
label = "Menu Toggle",
|
|
852
|
+
key = _PULSE_TOGGLE_KEY,
|
|
853
|
+
action = function() _UIAdapter:ToggleWindow() end,
|
|
854
|
+
})
|
|
855
|
+
gb_menu._container:Button({
|
|
856
|
+
Title = "Rejoin",
|
|
857
|
+
Icon = "refresh-cw",
|
|
858
|
+
Desc = "Leave and rejoin the current place",
|
|
859
|
+
Callback = function()
|
|
860
|
+
pcall(function()
|
|
861
|
+
game:GetService("TeleportService"):TeleportToPlaceInstance(
|
|
862
|
+
game.PlaceId, game.JobId,
|
|
863
|
+
game:GetService("Players").LocalPlayer
|
|
864
|
+
)
|
|
865
|
+
end)
|
|
866
|
+
end,
|
|
867
|
+
})
|
|
868
|
+
|
|
869
|
+
-- Auto-load saved config on startup
|
|
870
|
+
task.spawn(function()
|
|
871
|
+
task.wait(1.5)
|
|
872
|
+
if _UIAdapter.Config then
|
|
873
|
+
local ok = pcall(function() _UIAdapter.Config:Load() end)
|
|
874
|
+
if ok and _BundlePulse and _BundlePulse.Log then _BundlePulse.Log.info("settings", "config auto-loaded") end
|
|
875
|
+
end
|
|
876
|
+
end)
|
|
877
|
+
|
|
878
|
+
-- ── Dev tab (--dev builds only) ───────────────────────────────────────────
|
|
879
|
+
if L.dev then
|
|
880
|
+
local _devTab
|
|
881
|
+
pcall(function() _devTab = _windWindow:Tab({ Title = "Dev", Icon = "bug" }) end)
|
|
882
|
+
if _devTab then
|
|
883
|
+
local _devLeft, _devRight
|
|
884
|
+
pcall(function()
|
|
885
|
+
local _devHs = _devTab:HStack({ AutoSpace = true })
|
|
886
|
+
_devLeft = _devHs:VStack({})
|
|
887
|
+
_devRight = _devHs:VStack({})
|
|
888
|
+
end)
|
|
889
|
+
_devLeft = _devLeft or _devTab
|
|
890
|
+
_devRight = _devRight or _devTab
|
|
891
|
+
|
|
892
|
+
-- Tools section (left)
|
|
893
|
+
local _toolSect
|
|
894
|
+
pcall(function()
|
|
895
|
+
_toolSect = _devLeft:Section({ Title="Tools", Icon="wrench", Box=true, BoxBorder=true, Opened=true })
|
|
896
|
+
end)
|
|
897
|
+
|
|
898
|
+
local _TOOLS = {
|
|
899
|
+
{ name="Hydroxide", desc="Remote spy with web interface", url=nil,
|
|
900
|
+
load=function()
|
|
901
|
+
local function webImport(f)
|
|
902
|
+
loadstring(game:HttpGetAsync(("https://raw.githubusercontent.com/Upbolt/Hydroxide/revision/%s.lua"):format(f)), f..".lua")()
|
|
903
|
+
end
|
|
904
|
+
webImport("init"); webImport("ui/main")
|
|
905
|
+
end },
|
|
906
|
+
{ name="Infinite Yield", desc="Admin / exploit command panel", url="https://raw.githubusercontent.com/DarkNetworks/Infinite-Yield/main/latest.lua" },
|
|
907
|
+
{ name="Dex Explorer", desc="Browse and inspect the live game tree",url="https://raw.githubusercontent.com/infyiff/backup/main/dex.lua" },
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if _toolSect then
|
|
911
|
+
for _, tool in ipairs(_TOOLS) do
|
|
912
|
+
local _launched = false
|
|
913
|
+
local _btn
|
|
914
|
+
pcall(function()
|
|
915
|
+
_btn = _toolSect:Button({
|
|
916
|
+
Title = tool.name,
|
|
917
|
+
Desc = tool.desc,
|
|
918
|
+
Callback = function()
|
|
919
|
+
if _launched then _PulseNotify("Already running: "..tool.name, 2); return end
|
|
920
|
+
pcall(function() _btn:SetTitle(tool.name.." · loading…") end)
|
|
921
|
+
task.spawn(function()
|
|
922
|
+
local ok, err = pcall(function()
|
|
923
|
+
if tool.load then
|
|
924
|
+
tool.load()
|
|
925
|
+
else
|
|
926
|
+
local src = game:HttpGet(tool.url)
|
|
927
|
+
local fn, ce = loadstring(src)
|
|
928
|
+
if not fn then error(ce) end
|
|
929
|
+
fn()
|
|
930
|
+
end
|
|
931
|
+
end)
|
|
932
|
+
if ok then
|
|
933
|
+
_launched = true
|
|
934
|
+
pcall(function() _btn:SetTitle("✓ "..tool.name) end)
|
|
935
|
+
_PulseNotify(tool.name.." launched", 3)
|
|
936
|
+
else
|
|
937
|
+
pcall(function() _btn:SetTitle(tool.name.." · failed") end)
|
|
938
|
+
_PulseNotify(tool.name.." failed: "..tostring(err):sub(1,60), 4)
|
|
939
|
+
task.delay(3, function()
|
|
940
|
+
if not _launched then pcall(function() _btn:SetTitle(tool.name) end) end
|
|
941
|
+
end)
|
|
942
|
+
end
|
|
943
|
+
end)
|
|
944
|
+
end,
|
|
945
|
+
})
|
|
946
|
+
end)
|
|
947
|
+
end
|
|
948
|
+
end
|
|
949
|
+
|
|
950
|
+
-- Scanner section
|
|
951
|
+
local _scanSect
|
|
952
|
+
pcall(function()
|
|
953
|
+
_scanSect = _devRight:Section({ Title="Scanner", Icon="scan", Box=true, BoxBorder=true, Opened=true })
|
|
954
|
+
end)
|
|
955
|
+
|
|
956
|
+
if _scanSect then
|
|
957
|
+
local _lastScan = ""
|
|
958
|
+
local _statusPara
|
|
959
|
+
pcall(function()
|
|
960
|
+
_statusPara = _scanSect:Paragraph({
|
|
961
|
+
Title = "Game Scanner",
|
|
962
|
+
Desc = "Auto-scan runs on inject. Results copied to clipboard.",
|
|
963
|
+
})
|
|
964
|
+
end)
|
|
965
|
+
|
|
966
|
+
local function _devCopy(text)
|
|
967
|
+
local ok = false
|
|
968
|
+
if not ok then pcall(function() setclipboard(text); ok=true end) end
|
|
969
|
+
if not ok then pcall(function() toclipboard(text); ok=true end) end
|
|
970
|
+
if not ok then pcall(function() writeclipboard(text); ok=true end) end
|
|
971
|
+
return ok
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
local _ALIASES = {
|
|
975
|
+
{ "Players.LocalPlayer.PlayerGui.", "PG." },
|
|
976
|
+
{ "Players.LocalPlayer.", "LP." },
|
|
977
|
+
{ "ReplicatedStorage.", "RS." },
|
|
978
|
+
{ "ReplicatedFirst.", "RF." },
|
|
979
|
+
{ "workspace.", "WS." },
|
|
980
|
+
}
|
|
981
|
+
local function _compress(obj)
|
|
982
|
+
local parts = {}; local cur = obj
|
|
983
|
+
while cur and cur ~= game do table.insert(parts, 1, cur.Name); cur = cur.Parent end
|
|
984
|
+
local s = table.concat(parts, ".")
|
|
985
|
+
for _, pair in ipairs(_ALIASES) do
|
|
986
|
+
if s:sub(1, #pair[1]) == pair[1] then return pair[2]..s:sub(#pair[1]+1) end
|
|
987
|
+
end
|
|
988
|
+
return s
|
|
989
|
+
end
|
|
990
|
+
|
|
991
|
+
local function _doScan()
|
|
992
|
+
pcall(function() _statusPara:SetDesc("Scanning…") end)
|
|
993
|
+
task.spawn(function()
|
|
994
|
+
local out = {}
|
|
995
|
+
local RS = game:GetService("ReplicatedStorage")
|
|
996
|
+
local gameName = "?"
|
|
997
|
+
pcall(function()
|
|
998
|
+
gameName = game:GetService("MarketplaceService"):GetProductInfo(game.PlaceId).Name
|
|
999
|
+
end)
|
|
1000
|
+
table.insert(out, ("[%s] pid:%d"):format(gameName, game.PlaceId))
|
|
1001
|
+
|
|
1002
|
+
local remotes = {}
|
|
1003
|
+
local function _scanRemotes(inst, depth)
|
|
1004
|
+
if depth > 8 then return end
|
|
1005
|
+
local ok, ch = pcall(function() return inst:GetChildren() end)
|
|
1006
|
+
if not ok then return end
|
|
1007
|
+
for _, c in ipairs(ch) do
|
|
1008
|
+
local cls = c.ClassName
|
|
1009
|
+
if cls == "RemoteEvent" or cls == "RemoteFunction" then
|
|
1010
|
+
table.insert(remotes, { p=_compress(c), t=cls=="RemoteEvent" and "RE" or "RF" })
|
|
1011
|
+
end
|
|
1012
|
+
if c:IsA("Folder") or c:IsA("Configuration") or c:IsA("Model") then
|
|
1013
|
+
_scanRemotes(c, depth+1)
|
|
1014
|
+
end
|
|
1015
|
+
end
|
|
1016
|
+
end
|
|
1017
|
+
pcall(_scanRemotes, RS, 0)
|
|
1018
|
+
table.sort(remotes, function(a,b) return a.p < b.p end)
|
|
1019
|
+
table.insert(out, ("REMOTES (%d)"):format(#remotes))
|
|
1020
|
+
for i, r in ipairs(remotes) do
|
|
1021
|
+
if i > 50 then table.insert(out, (" ...+%d more"):format(#remotes-50)); break end
|
|
1022
|
+
table.insert(out, (" %-52s %s"):format(r.p, r.t))
|
|
1023
|
+
end
|
|
1024
|
+
|
|
1025
|
+
local txt = table.concat(out, "\n")
|
|
1026
|
+
_lastScan = txt
|
|
1027
|
+
local copied = _devCopy(txt)
|
|
1028
|
+
local summary = ("%d remotes%s"):format(#remotes, copied and " · copied" or "")
|
|
1029
|
+
pcall(function()
|
|
1030
|
+
_statusPara:SetTitle("Scan "..os.date("%H:%M:%S"))
|
|
1031
|
+
_statusPara:SetDesc(summary)
|
|
1032
|
+
end)
|
|
1033
|
+
if copied then _PulseNotify("Scan complete · "..summary, 4) end
|
|
1034
|
+
end)
|
|
1035
|
+
end
|
|
1036
|
+
|
|
1037
|
+
pcall(function()
|
|
1038
|
+
_scanSect:Button({
|
|
1039
|
+
Title="Scan Now", Desc="Scan remotes and copy to clipboard",
|
|
1040
|
+
Callback = _doScan,
|
|
1041
|
+
})
|
|
1042
|
+
end)
|
|
1043
|
+
pcall(function()
|
|
1044
|
+
_scanSect:Button({
|
|
1045
|
+
Title="Copy Last Results", Desc="Re-copy most recent scan",
|
|
1046
|
+
Callback = function()
|
|
1047
|
+
if _lastScan == "" then _PulseNotify("No scan yet", 2); return end
|
|
1048
|
+
local ok = _devCopy(_lastScan)
|
|
1049
|
+
_PulseNotify(ok and "Copied" or "No clipboard function", 2)
|
|
1050
|
+
end,
|
|
1051
|
+
})
|
|
1052
|
+
end)
|
|
1053
|
+
|
|
1054
|
+
-- Auto-scan on inject
|
|
1055
|
+
task.spawn(function()
|
|
1056
|
+
if not game:IsLoaded() then game.Loaded:Wait() end
|
|
1057
|
+
task.wait(2)
|
|
1058
|
+
_doScan()
|
|
1059
|
+
end)
|
|
1060
|
+
end
|
|
1061
|
+
|
|
1062
|
+
-- Info section — framework version + signal counts for debugging
|
|
1063
|
+
local _infoSect
|
|
1064
|
+
pcall(function()
|
|
1065
|
+
_infoSect = _devLeft:Section({ Title="Pulse Info", Icon="info", Box=true, BoxBorder=true, Opened=true })
|
|
1066
|
+
end)
|
|
1067
|
+
if _infoSect then
|
|
1068
|
+
local nPages = #_BundlePages
|
|
1069
|
+
local nComps = 0
|
|
1070
|
+
for _ in pairs(_BundleComponents) do nComps = nComps + 1 end
|
|
1071
|
+
pcall(function()
|
|
1072
|
+
_infoSect:Paragraph({
|
|
1073
|
+
Title = "Framework",
|
|
1074
|
+
Desc = ("pages=%d components=%d theme=%s"):format(nPages, nComps, L.theme or "?"),
|
|
1075
|
+
})
|
|
1076
|
+
end)
|
|
1077
|
+
for cname, comp in pairs(_BundleComponents) do
|
|
1078
|
+
if type(comp) == "table" and comp._ui then
|
|
1079
|
+
pcall(function()
|
|
1080
|
+
_infoSect:Paragraph({
|
|
1081
|
+
Title = cname,
|
|
1082
|
+
Desc = ("%d widget(s)"):format(#comp._ui),
|
|
1083
|
+
})
|
|
1084
|
+
end)
|
|
1085
|
+
end
|
|
1086
|
+
end
|
|
1087
|
+
end
|
|
1088
|
+
|
|
1089
|
+
-- Heartbeat Monitor section — tick counts per component (left)
|
|
1090
|
+
-- Uses Pulse.Monitor which is independent of the log system.
|
|
1091
|
+
local _monSect
|
|
1092
|
+
pcall(function()
|
|
1093
|
+
_monSect = _devLeft:Section({ Title="Heartbeat Monitor", Icon="activity", Box=true, BoxBorder=true, Opened=true })
|
|
1094
|
+
end)
|
|
1095
|
+
if _monSect then
|
|
1096
|
+
local _monPara
|
|
1097
|
+
pcall(function()
|
|
1098
|
+
_monPara = _monSect:Paragraph({
|
|
1099
|
+
Title = "raw / cond (updates 2s)",
|
|
1100
|
+
Desc = "raw = every HB fire · cond = when() passed",
|
|
1101
|
+
})
|
|
1102
|
+
end)
|
|
1103
|
+
-- Reads _rawHb/_condHb plain-Lua counters set in runtime._hbBind.
|
|
1104
|
+
-- No dependency on Pulse.Monitor — works regardless of log system state.
|
|
1105
|
+
task.spawn(function()
|
|
1106
|
+
while _windWindow do
|
|
1107
|
+
task.wait(2)
|
|
1108
|
+
pcall(function()
|
|
1109
|
+
local lines = {}
|
|
1110
|
+
for cname, comp in pairs(_BundleComponents) do
|
|
1111
|
+
if type(comp) == "table" and comp._rawHb ~= nil then
|
|
1112
|
+
lines[#lines+1] = cname
|
|
1113
|
+
.. " raw=" .. tostring(comp._rawHb)
|
|
1114
|
+
.. " cond=" .. tostring(comp._condHb)
|
|
1115
|
+
end
|
|
1116
|
+
end
|
|
1117
|
+
if _monPara then
|
|
1118
|
+
if #lines == 0 then
|
|
1119
|
+
_monPara:SetDesc("No heartbeat components registered")
|
|
1120
|
+
else
|
|
1121
|
+
table.sort(lines)
|
|
1122
|
+
_monPara:SetDesc(table.concat(lines, "\n"))
|
|
1123
|
+
end
|
|
1124
|
+
end
|
|
1125
|
+
end)
|
|
1126
|
+
end
|
|
1127
|
+
end)
|
|
1128
|
+
end
|
|
1129
|
+
|
|
1130
|
+
-- Console section — controls for the Roblox developer console (F9).
|
|
1131
|
+
-- "Clear" wipes the output so only your next prints are visible.
|
|
1132
|
+
-- "Print Logs" re-dumps all buffered Pulse.Log entries so you can read
|
|
1133
|
+
-- them cleanly after a clear without any engine noise in the way.
|
|
1134
|
+
-- "Copy Logs" copies the full log buffer to clipboard.
|
|
1135
|
+
local _consoleSect
|
|
1136
|
+
pcall(function()
|
|
1137
|
+
_consoleSect = _devRight:Section({ Title="Console (F9)", Icon="terminal", Box=true, BoxBorder=true, Opened=true })
|
|
1138
|
+
end)
|
|
1139
|
+
if _consoleSect and _BundlePulse and _BundlePulse.Log then
|
|
1140
|
+
local _Log = _BundlePulse.Log
|
|
1141
|
+
|
|
1142
|
+
-- Clear console: try executor functions, fall back to blank lines separator.
|
|
1143
|
+
local function _clearConsole()
|
|
1144
|
+
local cleared = false
|
|
1145
|
+
if not cleared then pcall(function() consoleclear(); cleared=true end) end
|
|
1146
|
+
if not cleared then pcall(function() clearoutput(); cleared=true end) end
|
|
1147
|
+
if not cleared then pcall(function()
|
|
1148
|
+
game:GetService("LogService"):ClearOutput(); cleared=true
|
|
1149
|
+
end) end
|
|
1150
|
+
if not cleared then
|
|
1151
|
+
-- visual separator when no clear API is available
|
|
1152
|
+
print(("\n"):rep(40))
|
|
1153
|
+
print("──────────── console cleared ────────────")
|
|
1154
|
+
end
|
|
1155
|
+
end
|
|
1156
|
+
|
|
1157
|
+
local function _copyLogs()
|
|
1158
|
+
local txt = _Log.dump()
|
|
1159
|
+
local ok = false
|
|
1160
|
+
if not ok then pcall(function() setclipboard(txt); ok=true end) end
|
|
1161
|
+
if not ok then pcall(function() toclipboard(txt); ok=true end) end
|
|
1162
|
+
if not ok then pcall(function() writeclipboard(txt); ok=true end) end
|
|
1163
|
+
_PulseNotify(ok and "Logs copied to clipboard" or "No clipboard API available", 3)
|
|
1164
|
+
end
|
|
1165
|
+
|
|
1166
|
+
local function _printLogs()
|
|
1167
|
+
local entries = _Log.getNewEntries(0)
|
|
1168
|
+
print("──────────── Pulse Logs (" .. #entries .. ") ────────────")
|
|
1169
|
+
for _, e in ipairs(entries) do
|
|
1170
|
+
if e.n >= 3 then warn(e.line) else print(e.line) end
|
|
1171
|
+
end
|
|
1172
|
+
print("─────────────────────────────────────────")
|
|
1173
|
+
end
|
|
1174
|
+
|
|
1175
|
+
pcall(function()
|
|
1176
|
+
_consoleSect:Button({
|
|
1177
|
+
Title = "Clear Console",
|
|
1178
|
+
Desc = "Wipe F9 output — then use Print Logs to see only yours",
|
|
1179
|
+
Callback = _clearConsole,
|
|
1180
|
+
})
|
|
1181
|
+
end)
|
|
1182
|
+
pcall(function()
|
|
1183
|
+
_consoleSect:Button({
|
|
1184
|
+
Title = "Print Logs",
|
|
1185
|
+
Desc = "Re-print all Pulse.Log entries to F9 console",
|
|
1186
|
+
Callback = _printLogs,
|
|
1187
|
+
})
|
|
1188
|
+
end)
|
|
1189
|
+
pcall(function()
|
|
1190
|
+
_consoleSect:Button({
|
|
1191
|
+
Title = "Copy Logs",
|
|
1192
|
+
Desc = "Copy full log buffer to clipboard",
|
|
1193
|
+
Callback = _copyLogs,
|
|
1194
|
+
})
|
|
1195
|
+
end)
|
|
1196
|
+
pcall(function()
|
|
1197
|
+
_consoleSect:Button({
|
|
1198
|
+
Title = "Clear Log Buffer",
|
|
1199
|
+
Desc = "Reset the in-memory Pulse.Log buffer",
|
|
1200
|
+
Callback = function()
|
|
1201
|
+
_Log.clear()
|
|
1202
|
+
_PulseNotify("Log buffer cleared", 2)
|
|
1203
|
+
end,
|
|
1204
|
+
})
|
|
1205
|
+
end)
|
|
1206
|
+
end
|
|
1207
|
+
end
|
|
1208
|
+
end
|
|
1209
|
+
|
|
1210
|
+
-- Re-select first tab (Home) — Settings is last so Wind UI leaves it active
|
|
1211
|
+
task.defer(function()
|
|
1212
|
+
if _UIAdapter._firstTab then
|
|
1213
|
+
pcall(function() _UIAdapter._firstTab:Select() end)
|
|
1214
|
+
end
|
|
1215
|
+
end)
|
|
1216
|
+
end)
|