pulse-rb 1.4.0 → 1.4.2
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 +231 -99
- package/dist/index.js +31 -15
- package/package.json +1 -1
- package/pulse/helpers/log.lua +6 -1
- package/pulse/runtime.lua +94 -41
package/adapters/windui.lua
CHANGED
|
@@ -2,23 +2,28 @@
|
|
|
2
2
|
-- Wind UI adapter for Pulse components.
|
|
3
3
|
-- Docs: footagesus.github.io/treehub-web/docs/windui
|
|
4
4
|
-- Version: 1.6.64-fix (pinned)
|
|
5
|
-
|
|
6
|
-
--
|
|
7
|
-
--
|
|
8
|
-
--
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
--
|
|
6
|
+
-- Supports two load modes:
|
|
7
|
+
-- 1. Combined (rb publish): inlined into pulse.lua — _PULSE_LAYOUT, Components, _Pages, Pulse
|
|
8
|
+
-- are already in scope as upvalues. publish.ts strips everything above [PULSE_INLINE_START].
|
|
9
|
+
-- 2. Separate (legacy): loaded via its own loadstring(_P, _PULSE_LAYOUT) call.
|
|
10
|
+
-- _P carries the bundle table; we spread it into the local env.
|
|
11
|
+
local _isSeparate = type(select(1,...)) == "table"
|
|
12
|
+
if _isSeparate then
|
|
13
|
+
local _P = select(1,...)
|
|
14
|
+
local _PULSE_LAYOUT = select(2,...) or {}
|
|
11
15
|
local _env = (getfenv and getfenv(1)) or _ENV or _G
|
|
12
16
|
for _k, _v in pairs(_P) do _env[_k] = _v end
|
|
13
17
|
end
|
|
14
18
|
_PULSE_LAYOUT = _PULSE_LAYOUT or {}
|
|
15
19
|
|
|
16
|
-
--
|
|
17
|
-
--
|
|
18
|
-
--
|
|
19
|
-
|
|
20
|
-
local
|
|
21
|
-
local
|
|
20
|
+
-- [PULSE_INLINE_START]
|
|
21
|
+
-- Everything below this marker is included verbatim when bundled inline.
|
|
22
|
+
-- In combined mode: Components, _Pages, Pulse are direct upvalues from runtime.lua.
|
|
23
|
+
-- In separate mode: they were spread into the local env by the block above.
|
|
24
|
+
local _BundleComponents = Components or {}
|
|
25
|
+
local _BundlePages = _Pages or {}
|
|
26
|
+
local _BundlePulse = Pulse or {}
|
|
22
27
|
|
|
23
28
|
local _WINDUI_URL = "https://github.com/Footagesus/WindUI/releases/download/1.6.64-fix/main.lua"
|
|
24
29
|
local _PULSE_LOGO = "https://pulse-rb.vercel.app/img/logo.svg"
|
|
@@ -184,6 +189,9 @@ function _UIAdapter:CreateWindow(title, w, h, opts)
|
|
|
184
189
|
local tabProxy = { _wind = windTab }
|
|
185
190
|
function tabProxy:AddLeftGroupbox(title, gbIcon) return _mkSection(leftCol, title, gbIcon) end
|
|
186
191
|
function tabProxy:AddRightGroupbox(title, gbIcon) return _mkSection(rightCol, title, gbIcon) end
|
|
192
|
+
-- Raw column access — no Section wrapper (used by plain=true groupboxes)
|
|
193
|
+
function tabProxy:GetLeftColumn() return { _container = leftCol } end
|
|
194
|
+
function tabProxy:GetRightColumn() return { _container = rightCol } end
|
|
187
195
|
return tabProxy
|
|
188
196
|
end
|
|
189
197
|
|
|
@@ -587,14 +595,25 @@ task.spawn(function()
|
|
|
587
595
|
local _execVer = ""
|
|
588
596
|
pcall(function()
|
|
589
597
|
if type(rawget(_G, "identifyexecutor")) == "function" then
|
|
590
|
-
local n, v = identifyexecutor
|
|
591
|
-
_execName = tostring(n or "Unknown")
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
+
local ok, n, v = pcall(identifyexecutor)
|
|
599
|
+
if ok then _execName = tostring(n or "Unknown"); _execVer = tostring(v or "") end
|
|
600
|
+
end
|
|
601
|
+
if _execName == "Unknown" and type(rawget(_G, "getexecutorname")) == "function" then
|
|
602
|
+
local ok, n = pcall(getexecutorname)
|
|
603
|
+
if ok and n then _execName = tostring(n) end
|
|
604
|
+
end
|
|
605
|
+
if _execName == "Unknown" then
|
|
606
|
+
if rawget(_G, "syn") or rawget(_G, "Synapse") then _execName = "Synapse X"
|
|
607
|
+
elseif rawget(_G, "KRNL_LOADED") then _execName = "Krnl"
|
|
608
|
+
elseif rawget(_G, "DELTA_KEY") then _execName = "Delta"
|
|
609
|
+
elseif rawget(_G, "WAVE_LOADED") then _execName = "Wave"
|
|
610
|
+
elseif rawget(_G, "SOLARA_LOADED") then _execName = "Solara"
|
|
611
|
+
elseif rawget(_G, "XENO_LOADED") then _execName = "Xeno"
|
|
612
|
+
elseif rawget(_G, "Seliware") or rawget(_G, "seliware") then _execName = "Seliware"
|
|
613
|
+
elseif rawget(_G, "Celery") or rawget(_G, "celery") then _execName = "Celery"
|
|
614
|
+
elseif rawget(_G, "is_sirhurt_closure") then _execName = "Sirhurt"
|
|
615
|
+
elseif rawget(_G, "EXECUTOR_NAME") then _execName = tostring(rawget(_G, "EXECUTOR_NAME"))
|
|
616
|
+
end
|
|
598
617
|
end
|
|
599
618
|
end)
|
|
600
619
|
|
|
@@ -605,84 +624,131 @@ task.spawn(function()
|
|
|
605
624
|
if info and info.Name then _gameName = info.Name end
|
|
606
625
|
end)
|
|
607
626
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
local
|
|
614
|
-
|
|
615
|
-
|
|
627
|
+
-- Home tab — raw columns, no Section wrappers for a clean look.
|
|
628
|
+
-- Create the tab directly so we can get the raw VStack columns from _mkColumns.
|
|
629
|
+
local _homeWindTab
|
|
630
|
+
pcall(function() _homeWindTab = _windWindow:Tab({ Title = "Home", Icon = "house" }) end)
|
|
631
|
+
if not _UIAdapter._firstTab then _UIAdapter._firstTab = _homeWindTab end
|
|
632
|
+
local _homeLeft, _homeRight = _mkColumns(_homeWindTab)
|
|
633
|
+
|
|
634
|
+
-- Left: player info + description + discord (no Section box)
|
|
635
|
+
if _homeLeft then
|
|
636
|
+
-- Capture the returned paragraph element so we can try avatar injection via its Frame
|
|
637
|
+
local _paraRef = nil
|
|
616
638
|
pcall(function()
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
639
|
+
_paraRef = _homeLeft:Paragraph({
|
|
640
|
+
Title = "Welcome, " .. _dspName,
|
|
641
|
+
Desc = "@" .. _usrName,
|
|
620
642
|
})
|
|
621
643
|
end)
|
|
622
|
-
end
|
|
623
|
-
_welcomeGb._container:Paragraph({
|
|
624
|
-
Title = _dspName,
|
|
625
|
-
Desc = "@" .. _usrName,
|
|
626
|
-
})
|
|
627
644
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
645
|
+
-- Avatar injection: WindUI paragraph elements wrap a Roblox Frame internally.
|
|
646
|
+
-- Grab that Frame's parent (the list container) and add an ImageLabel there.
|
|
647
|
+
if _avatarUrl ~= "" and _paraRef then
|
|
648
|
+
task.delay(0.3, function()
|
|
649
|
+
pcall(function()
|
|
650
|
+
local paraFrame = nil
|
|
651
|
+
for _, k in ipairs({"Frame","Object","Instance","Container","Label","Content"}) do
|
|
652
|
+
local ok, v = pcall(function() return _paraRef[k] end)
|
|
653
|
+
if ok and type(v) == "userdata" then
|
|
654
|
+
local _, isGui = pcall(function() return v:IsA("GuiObject") end)
|
|
655
|
+
if isGui then paraFrame = v; break end
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
local contentFrame = paraFrame and paraFrame.Parent
|
|
659
|
+
if not (contentFrame and type(contentFrame) == "userdata") then return end
|
|
660
|
+
if contentFrame:FindFirstChild("_PulseAvatar") then return end
|
|
661
|
+
local img = Instance.new("ImageLabel")
|
|
662
|
+
img.Name = "_PulseAvatar"; img.Image = _avatarUrl
|
|
663
|
+
img.Size = UDim2.fromOffset(64, 64); img.BackgroundTransparency = 1
|
|
664
|
+
img.ScaleType = Enum.ScaleType.Crop; img.LayoutOrder = -999
|
|
665
|
+
Instance.new("UICorner", img).CornerRadius = UDim.new(1, 0)
|
|
666
|
+
img.Parent = contentFrame
|
|
667
|
+
end)
|
|
668
|
+
end)
|
|
669
|
+
end
|
|
632
670
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
671
|
+
if L.description and L.description ~= "" then
|
|
672
|
+
pcall(function() _homeLeft:Divider() end)
|
|
673
|
+
pcall(function() _homeLeft:Paragraph({ Title = "About", Desc = L.description }) end)
|
|
674
|
+
end
|
|
675
|
+
if L.discord and L.discord ~= "" then
|
|
676
|
+
pcall(function() _homeLeft:Divider() end)
|
|
677
|
+
pcall(function()
|
|
678
|
+
_homeLeft:Button({
|
|
679
|
+
Title = "Join Discord",
|
|
680
|
+
Desc = "Tap to copy the invite link",
|
|
681
|
+
Callback = function()
|
|
682
|
+
local ok = false
|
|
683
|
+
if not ok then pcall(function() setclipboard(L.discord); ok=true end) end
|
|
684
|
+
if not ok then pcall(function() toclipboard(L.discord); ok=true end) end
|
|
685
|
+
if not ok then pcall(function() writeclipboard(L.discord); ok=true end) end
|
|
686
|
+
_PulseNotify(ok and "Discord link copied!" or "No clipboard API", 3)
|
|
687
|
+
end,
|
|
688
|
+
})
|
|
689
|
+
end)
|
|
690
|
+
end
|
|
646
691
|
end
|
|
647
692
|
|
|
648
|
-
-- Right
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
if type(L.unsupported) == "table" then
|
|
674
|
-
for _, v in ipairs(L.unsupported) do _unsupList[#_unsupList+1] = tostring(v) end
|
|
675
|
-
elseif type(L.unsupported) == "string" then
|
|
676
|
-
for part in L.unsupported:gmatch("[^,]+") do
|
|
677
|
-
_unsupList[#_unsupList+1] = part:match("^%s*(.-)%s*$")
|
|
678
|
-
end
|
|
679
|
-
end
|
|
680
|
-
if #_unsupList > 0 then
|
|
681
|
-
local _unsupGb = _homeTab:AddRightGroupbox("Not Supported", "alert-triangle")
|
|
682
|
-
_unsupGb._container:Paragraph({
|
|
683
|
-
Title = "Not compatible with:",
|
|
684
|
-
Desc = table.concat(_unsupList, ", "),
|
|
693
|
+
-- Right: script info + executor + game (no Section box)
|
|
694
|
+
if _homeRight then
|
|
695
|
+
local _verStr = (L.version and L.version ~= "") and ("v" .. L.version) or ""
|
|
696
|
+
local _authStr = (L.author and L.author ~= "") and ("by " .. L.author) or ""
|
|
697
|
+
local _metaParts = {}
|
|
698
|
+
if _verStr ~= "" then _metaParts[#_metaParts+1] = _verStr end
|
|
699
|
+
if _authStr ~= "" then _metaParts[#_metaParts+1] = _authStr end
|
|
700
|
+
pcall(function()
|
|
701
|
+
_homeRight:Paragraph({
|
|
702
|
+
Title = L.title or "Hub",
|
|
703
|
+
Desc = #_metaParts > 0 and table.concat(_metaParts, " · ") or "—",
|
|
704
|
+
})
|
|
705
|
+
end)
|
|
706
|
+
pcall(function() _homeRight:Divider() end)
|
|
707
|
+
local _execStr = _execName .. (_execVer ~= "" and (" " .. _execVer) or "")
|
|
708
|
+
pcall(function() _homeRight:Paragraph({ Title = "Executor", Desc = _execStr }) end)
|
|
709
|
+
pcall(function() _homeRight:Paragraph({ Title = "Game", Desc = _gameName }) end)
|
|
710
|
+
pcall(function() _homeRight:Divider() end)
|
|
711
|
+
-- Server info: live player count + rejoin
|
|
712
|
+
local _playerCount = 0
|
|
713
|
+
pcall(function() _playerCount = #game:GetService("Players"):GetPlayers() end)
|
|
714
|
+
pcall(function()
|
|
715
|
+
_homeRight:Paragraph({
|
|
716
|
+
Title = "Server",
|
|
717
|
+
Desc = tostring(_playerCount) .. " player" .. (_playerCount == 1 and "" or "s") .. " · " .. tostring(game.PlaceId),
|
|
685
718
|
})
|
|
719
|
+
end)
|
|
720
|
+
pcall(function()
|
|
721
|
+
_homeRight:Button({
|
|
722
|
+
Title = "Rejoin Server",
|
|
723
|
+
Desc = "Leave and rejoin the current place",
|
|
724
|
+
Callback = function()
|
|
725
|
+
pcall(function()
|
|
726
|
+
game:GetService("TeleportService"):TeleportToPlaceInstance(
|
|
727
|
+
game.PlaceId, game.JobId,
|
|
728
|
+
game:GetService("Players").LocalPlayer
|
|
729
|
+
)
|
|
730
|
+
end)
|
|
731
|
+
end,
|
|
732
|
+
})
|
|
733
|
+
end)
|
|
734
|
+
if L.unsupported and L.unsupported ~= "" then
|
|
735
|
+
local _unsupList = {}
|
|
736
|
+
if type(L.unsupported) == "table" then
|
|
737
|
+
for _, v in ipairs(L.unsupported) do _unsupList[#_unsupList+1] = tostring(v) end
|
|
738
|
+
elseif type(L.unsupported) == "string" then
|
|
739
|
+
for part in L.unsupported:gmatch("[^,]+") do
|
|
740
|
+
_unsupList[#_unsupList+1] = part:match("^%s*(.-)%s*$")
|
|
741
|
+
end
|
|
742
|
+
end
|
|
743
|
+
if #_unsupList > 0 then
|
|
744
|
+
pcall(function() _homeRight:Divider() end)
|
|
745
|
+
pcall(function()
|
|
746
|
+
_homeRight:Paragraph({
|
|
747
|
+
Title = "Not compatible with:",
|
|
748
|
+
Desc = table.concat(_unsupList, ", "),
|
|
749
|
+
})
|
|
750
|
+
end)
|
|
751
|
+
end
|
|
686
752
|
end
|
|
687
753
|
end
|
|
688
754
|
|
|
@@ -691,7 +757,14 @@ task.spawn(function()
|
|
|
691
757
|
local tab = _win:AddTab(page.title, page.icon)
|
|
692
758
|
for _, gb in ipairs(page.layout or {}) do
|
|
693
759
|
local container
|
|
694
|
-
if gb.
|
|
760
|
+
if gb.plain then
|
|
761
|
+
-- No Section wrapper — widgets go directly onto the column VStack
|
|
762
|
+
if gb.side == "left" then
|
|
763
|
+
if tab.GetLeftColumn then container = tab:GetLeftColumn() end
|
|
764
|
+
else
|
|
765
|
+
if tab.GetRightColumn then container = tab:GetRightColumn() end
|
|
766
|
+
end
|
|
767
|
+
elseif gb.side == "left" then
|
|
695
768
|
container = tab:AddLeftGroupbox(gb.title, gb.icon)
|
|
696
769
|
else
|
|
697
770
|
container = tab:AddRightGroupbox(gb.title, gb.icon)
|
|
@@ -784,6 +857,19 @@ task.spawn(function()
|
|
|
784
857
|
key = _PULSE_TOGGLE_KEY,
|
|
785
858
|
action = function() _UIAdapter:ToggleWindow() end,
|
|
786
859
|
})
|
|
860
|
+
gb_menu._container:Button({
|
|
861
|
+
Title = "Rejoin",
|
|
862
|
+
Icon = "refresh-cw",
|
|
863
|
+
Desc = "Leave and rejoin the current place",
|
|
864
|
+
Callback = function()
|
|
865
|
+
pcall(function()
|
|
866
|
+
game:GetService("TeleportService"):TeleportToPlaceInstance(
|
|
867
|
+
game.PlaceId, game.JobId,
|
|
868
|
+
game:GetService("Players").LocalPlayer
|
|
869
|
+
)
|
|
870
|
+
end)
|
|
871
|
+
end,
|
|
872
|
+
})
|
|
787
873
|
|
|
788
874
|
-- Auto-load saved config on startup
|
|
789
875
|
task.spawn(function()
|
|
@@ -799,14 +885,19 @@ task.spawn(function()
|
|
|
799
885
|
local _devTab
|
|
800
886
|
pcall(function() _devTab = _windWindow:Tab({ Title = "Dev", Icon = "bug" }) end)
|
|
801
887
|
if _devTab then
|
|
802
|
-
local
|
|
803
|
-
pcall(function()
|
|
804
|
-
|
|
888
|
+
local _devLeft, _devRight
|
|
889
|
+
pcall(function()
|
|
890
|
+
local _devHs = _devTab:HStack({ AutoSpace = true })
|
|
891
|
+
_devLeft = _devHs:VStack({})
|
|
892
|
+
_devRight = _devHs:VStack({})
|
|
893
|
+
end)
|
|
894
|
+
_devLeft = _devLeft or _devTab
|
|
895
|
+
_devRight = _devRight or _devTab
|
|
805
896
|
|
|
806
|
-
-- Tools section
|
|
897
|
+
-- Tools section (left)
|
|
807
898
|
local _toolSect
|
|
808
899
|
pcall(function()
|
|
809
|
-
_toolSect =
|
|
900
|
+
_toolSect = _devLeft:Section({ Title="Tools", Icon="wrench", Box=true, BoxBorder=true, Opened=true })
|
|
810
901
|
end)
|
|
811
902
|
|
|
812
903
|
local _TOOLS = {
|
|
@@ -864,7 +955,7 @@ task.spawn(function()
|
|
|
864
955
|
-- Scanner section
|
|
865
956
|
local _scanSect
|
|
866
957
|
pcall(function()
|
|
867
|
-
_scanSect =
|
|
958
|
+
_scanSect = _devRight:Section({ Title="Scanner", Icon="scan", Box=true, BoxBorder=true, Opened=true })
|
|
868
959
|
end)
|
|
869
960
|
|
|
870
961
|
if _scanSect then
|
|
@@ -976,7 +1067,7 @@ task.spawn(function()
|
|
|
976
1067
|
-- Info section — framework version + signal counts for debugging
|
|
977
1068
|
local _infoSect
|
|
978
1069
|
pcall(function()
|
|
979
|
-
_infoSect =
|
|
1070
|
+
_infoSect = _devLeft:Section({ Title="Pulse Info", Icon="info", Box=true, BoxBorder=true, Opened=true })
|
|
980
1071
|
end)
|
|
981
1072
|
if _infoSect then
|
|
982
1073
|
local nPages = #_BundlePages
|
|
@@ -1000,6 +1091,47 @@ task.spawn(function()
|
|
|
1000
1091
|
end
|
|
1001
1092
|
end
|
|
1002
1093
|
|
|
1094
|
+
-- Heartbeat Monitor section — tick counts per component (left)
|
|
1095
|
+
-- Uses Pulse.Monitor which is independent of the log system.
|
|
1096
|
+
local _monSect
|
|
1097
|
+
pcall(function()
|
|
1098
|
+
_monSect = _devLeft:Section({ Title="Heartbeat Monitor", Icon="activity", Box=true, BoxBorder=true, Opened=true })
|
|
1099
|
+
end)
|
|
1100
|
+
if _monSect then
|
|
1101
|
+
local _monPara
|
|
1102
|
+
pcall(function()
|
|
1103
|
+
_monPara = _monSect:Paragraph({
|
|
1104
|
+
Title = "raw / cond (updates 2s)",
|
|
1105
|
+
Desc = "raw = every HB fire · cond = when() passed",
|
|
1106
|
+
})
|
|
1107
|
+
end)
|
|
1108
|
+
-- Reads _rawHb/_condHb plain-Lua counters set in runtime._hbBind.
|
|
1109
|
+
-- No dependency on Pulse.Monitor — works regardless of log system state.
|
|
1110
|
+
task.spawn(function()
|
|
1111
|
+
while _windWindow do
|
|
1112
|
+
task.wait(2)
|
|
1113
|
+
pcall(function()
|
|
1114
|
+
local lines = {}
|
|
1115
|
+
for cname, comp in pairs(_BundleComponents) do
|
|
1116
|
+
if type(comp) == "table" and comp._rawHb ~= nil then
|
|
1117
|
+
lines[#lines+1] = cname
|
|
1118
|
+
.. " raw=" .. tostring(comp._rawHb)
|
|
1119
|
+
.. " cond=" .. tostring(comp._condHb)
|
|
1120
|
+
end
|
|
1121
|
+
end
|
|
1122
|
+
if _monPara then
|
|
1123
|
+
if #lines == 0 then
|
|
1124
|
+
_monPara:SetDesc("No heartbeat components registered")
|
|
1125
|
+
else
|
|
1126
|
+
table.sort(lines)
|
|
1127
|
+
_monPara:SetDesc(table.concat(lines, "\n"))
|
|
1128
|
+
end
|
|
1129
|
+
end
|
|
1130
|
+
end)
|
|
1131
|
+
end
|
|
1132
|
+
end)
|
|
1133
|
+
end
|
|
1134
|
+
|
|
1003
1135
|
-- Console section — controls for the Roblox developer console (F9).
|
|
1004
1136
|
-- "Clear" wipes the output so only your next prints are visible.
|
|
1005
1137
|
-- "Print Logs" re-dumps all buffered Pulse.Log entries so you can read
|
|
@@ -1007,7 +1139,7 @@ task.spawn(function()
|
|
|
1007
1139
|
-- "Copy Logs" copies the full log buffer to clipboard.
|
|
1008
1140
|
local _consoleSect
|
|
1009
1141
|
pcall(function()
|
|
1010
|
-
_consoleSect =
|
|
1142
|
+
_consoleSect = _devRight:Section({ Title="Console (F9)", Icon="terminal", Box=true, BoxBorder=true, Opened=true })
|
|
1011
1143
|
end)
|
|
1012
1144
|
if _consoleSect and _BundlePulse and _BundlePulse.Log then
|
|
1013
1145
|
local _Log = _BundlePulse.Log
|
package/dist/index.js
CHANGED
|
@@ -56,7 +56,7 @@ var RB_VERSION, RB_HOME, IRONBREW2_DIR, IRONBREW2_REPO, ALWAYS_EXCLUDE, VALID_UI
|
|
|
56
56
|
var init_constants = __esm({
|
|
57
57
|
"src/constants.ts"() {
|
|
58
58
|
init_cjs_shims();
|
|
59
|
-
RB_VERSION = "1.4.
|
|
59
|
+
RB_VERSION = "1.4.2";
|
|
60
60
|
RB_HOME = path.join(os.homedir(), ".rb");
|
|
61
61
|
path.join(RB_HOME, "bin");
|
|
62
62
|
IRONBREW2_DIR = path.join(RB_HOME, "ironbrew2");
|
|
@@ -1363,7 +1363,7 @@ end)
|
|
|
1363
1363
|
if (opts.dev) parts.push(`,dev=true`);
|
|
1364
1364
|
parts.push(`}
|
|
1365
1365
|
`);
|
|
1366
|
-
parts.push(`local _P=loadstring(game:HttpGet("${base}/
|
|
1366
|
+
parts.push(`local _P=loadstring(game:HttpGet("${base}/pulse.lua"))(_PULSE_LAYOUT)
|
|
1367
1367
|
`);
|
|
1368
1368
|
if (opts.dev) {
|
|
1369
1369
|
parts.push(`_PULSE_DEV = true
|
|
@@ -1373,8 +1373,21 @@ end)
|
|
|
1373
1373
|
}
|
|
1374
1374
|
parts.push(`for _k,_v in pairs(_P) do _G[_k]=_v end
|
|
1375
1375
|
`);
|
|
1376
|
-
parts.push(
|
|
1377
|
-
`
|
|
1376
|
+
parts.push(
|
|
1377
|
+
`do
|
|
1378
|
+
local _rs=game:GetService("RunService")
|
|
1379
|
+
local _lp_=game:GetService("Players").LocalPlayer
|
|
1380
|
+
local _uis_=game:GetService("UserInputService")
|
|
1381
|
+
_rs.Heartbeat:Connect(function(a,b) if _PulseRunHeartbeat then _PulseRunHeartbeat(a,b) end end)
|
|
1382
|
+
_rs.RenderStepped:Connect(function(a,b) if _PulseRunRenderStepped then _PulseRunRenderStepped(a,b) end end)
|
|
1383
|
+
_rs.Stepped:Connect(function(a,b) if _PulseRunStepped then _PulseRunStepped(a,b) end end)
|
|
1384
|
+
_lp_.CharacterAdded:Connect(function(c) if _PulseRunCharAdded then _PulseRunCharAdded(c) end end)
|
|
1385
|
+
_lp_.CharacterRemoving:Connect(function(c) if _PulseRunCharRemoving then _PulseRunCharRemoving(c) end end)
|
|
1386
|
+
_uis_.InputBegan:Connect(function(i,g) if _PulseRunInputBegan then _PulseRunInputBegan(i,g) end end)
|
|
1387
|
+
_uis_.InputEnded:Connect(function(i,g) if _PulseRunInputEnded then _PulseRunInputEnded(i,g) end end)
|
|
1388
|
+
end
|
|
1389
|
+
`
|
|
1390
|
+
);
|
|
1378
1391
|
parts.push(`local signal,computed,defineComponent,on=_P.signal,_P.computed,_P.defineComponent,_P.on
|
|
1379
1392
|
`);
|
|
1380
1393
|
parts.push(`local toggle,slider,dropdown,multidropdown=_P.toggle,_P.slider,_P.dropdown,_P.multidropdown
|
|
@@ -2982,7 +2995,9 @@ async function cmdPublish(_args) {
|
|
|
2982
2995
|
process.exit(1);
|
|
2983
2996
|
}
|
|
2984
2997
|
const bundleParts = [
|
|
2985
|
-
`-- Pulse v${RB_VERSION}
|
|
2998
|
+
`-- Pulse v${RB_VERSION} (runtime + helpers + UI adapter)`,
|
|
2999
|
+
`-- Single loadstring \u2014 no executor sandbox split between runtime and UI.`,
|
|
3000
|
+
`local _PULSE_LAYOUT = ... or {}`,
|
|
2986
3001
|
fs.readFileSync(runtimeFile, "utf8").trimEnd()
|
|
2987
3002
|
];
|
|
2988
3003
|
if (fs.existsSync(helpersDir)) {
|
|
@@ -2991,21 +3006,22 @@ async function cmdPublish(_args) {
|
|
|
2991
3006
|
bundleParts.push(fs.readFileSync(pathe.join(helpersDir, f), "utf8").trimEnd());
|
|
2992
3007
|
}
|
|
2993
3008
|
}
|
|
3009
|
+
const winduiPath = pathe.join(ADAPTERS, "windui.lua");
|
|
3010
|
+
if (fs.existsSync(winduiPath)) {
|
|
3011
|
+
const winduiSrc = fs.readFileSync(winduiPath, "utf8");
|
|
3012
|
+
const markerTag = "-- [PULSE_INLINE_START]";
|
|
3013
|
+
const markerIdx = winduiSrc.indexOf(markerTag);
|
|
3014
|
+
const winduiCore = markerIdx >= 0 ? winduiSrc.slice(markerIdx + markerTag.length).trimStart() : winduiSrc;
|
|
3015
|
+
bundleParts.push("-- [windui adapter inline]");
|
|
3016
|
+
bundleParts.push(winduiCore.trimEnd());
|
|
3017
|
+
}
|
|
2994
3018
|
bundleParts.push(
|
|
2995
|
-
"return {signal=signal,computed=computed,defineComponent=defineComponent,on=on,toggle=toggle,slider=slider,dropdown=dropdown,multidropdown=multidropdown,button=button,keybind=keybind,label=label,separator=separator,groupbox=groupbox,definePage=definePage,Pulse=Pulse,Signal=Signal,Computed=Computed,Component=Component,Components=Components,Store=Store,PulseEvent=PulseEvent,_PulseGetChar=_PulseGetChar,_PulseGetHRP=_PulseGetHRP,_PulseGetHumanoid=_PulseGetHumanoid,_PulseGetAlive=_PulseGetAlive,_PulseRS=_PulseRS,_PulseUIS=_PulseUIS,_PulseDestroy=_PulseDestroy,_PULSE_DEFAULTS=_PULSE_DEFAULTS,_Pages=_Pages}"
|
|
3019
|
+
"return {signal=signal,computed=computed,defineComponent=defineComponent,on=on,toggle=toggle,slider=slider,dropdown=dropdown,multidropdown=multidropdown,button=button,keybind=keybind,label=label,separator=separator,groupbox=groupbox,definePage=definePage,Pulse=Pulse,Signal=Signal,Computed=Computed,Component=Component,Components=Components,Store=Store,PulseEvent=PulseEvent,_PulseGetChar=_PulseGetChar,_PulseGetHRP=_PulseGetHRP,_PulseGetHumanoid=_PulseGetHumanoid,_PulseGetAlive=_PulseGetAlive,_PulseRS=_PulseRS,_PulseUIS=_PulseUIS,_PulseDestroy=_PulseDestroy,_PULSE_DEFAULTS=_PULSE_DEFAULTS,_Pages=_Pages,_PulseRunHeartbeat=_PulseRunHeartbeat,_PulseRunRenderStepped=_PulseRunRenderStepped,_PulseRunStepped=_PulseRunStepped,_PulseRunCharAdded=_PulseRunCharAdded,_PulseRunCharRemoving=_PulseRunCharRemoving,_PulseRunInputBegan=_PulseRunInputBegan,_PulseRunInputEnded=_PulseRunInputEnded}"
|
|
2996
3020
|
);
|
|
2997
3021
|
const bundleContent = Buffer.from(bundleParts.join("\n") + "\n", "utf8");
|
|
2998
3022
|
const uploads = [
|
|
2999
|
-
{ remote: `${VERSION_PATH}/
|
|
3023
|
+
{ remote: `${VERSION_PATH}/pulse.lua`, content: bundleContent }
|
|
3000
3024
|
];
|
|
3001
|
-
for (const adapter of ["windui"]) {
|
|
3002
|
-
const p = pathe.join(ADAPTERS, `${adapter}.lua`);
|
|
3003
|
-
if (!fs.existsSync(p)) continue;
|
|
3004
|
-
uploads.push({
|
|
3005
|
-
remote: `${VERSION_PATH}/adapters/${adapter}.lua`,
|
|
3006
|
-
content: Buffer.from(fs.readFileSync(p, "utf8"), "utf8")
|
|
3007
|
-
});
|
|
3008
|
-
}
|
|
3009
3025
|
pSection(`Uploading to R2 ${gray("(v" + RB_VERSION + " \xB7 " + uploads.length + " files)")}`);
|
|
3010
3026
|
for (const { remote, content } of uploads) {
|
|
3011
3027
|
try {
|
package/package.json
CHANGED
package/pulse/helpers/log.lua
CHANGED
|
@@ -73,7 +73,12 @@ do
|
|
|
73
73
|
if #_buf > _bufMax then table.remove(_buf, 1) end
|
|
74
74
|
|
|
75
75
|
if _console then
|
|
76
|
-
|
|
76
|
+
-- Defer print outside the caller's thread (e.g. Heartbeat) so the
|
|
77
|
+
-- output always reaches the developer console regardless of context.
|
|
78
|
+
local _ln, _nw = line, n >= 3
|
|
79
|
+
task.spawn(function()
|
|
80
|
+
if _nw then warn(_ln) else print(_ln) end
|
|
81
|
+
end)
|
|
77
82
|
end
|
|
78
83
|
for _, fn in ipairs(_subs) do pcall(fn, entry) end
|
|
79
84
|
if _file then
|
package/pulse/runtime.lua
CHANGED
|
@@ -186,17 +186,6 @@ local function Component(name)
|
|
|
186
186
|
end)
|
|
187
187
|
end
|
|
188
188
|
|
|
189
|
-
-- Respawn listener (bound to connection tracker)
|
|
190
|
-
function c:onRespawn(fn)
|
|
191
|
-
local key = "__respawn_" .. name
|
|
192
|
-
local existing = self._connections[key]
|
|
193
|
-
if existing then
|
|
194
|
-
pcall(function() existing:Disconnect() end)
|
|
195
|
-
end
|
|
196
|
-
self._connections[key] = game:GetService("Players").LocalPlayer
|
|
197
|
-
.CharacterAdded:Connect(fn)
|
|
198
|
-
end
|
|
199
|
-
|
|
200
189
|
-- Lifecycle
|
|
201
190
|
function c:mount()
|
|
202
191
|
if self._mounted then return end
|
|
@@ -418,43 +407,106 @@ local function _needComp(name)
|
|
|
418
407
|
return _currentComponent
|
|
419
408
|
end
|
|
420
409
|
|
|
421
|
-
|
|
410
|
+
-- ── Sandboxing-safe event runner ─────────────────────────────────────────────
|
|
411
|
+
-- Executor loadstring() environments sandbox Roblox event :Connect() — calls
|
|
412
|
+
-- from inside the bundle never fire. Fix: store callbacks in plain Lua tables
|
|
413
|
+
-- here; the compiler emits user-scope Roblox connections that call _PulseRun*,
|
|
414
|
+
-- routing events into these runners from the non-sandboxed user script.
|
|
415
|
+
|
|
416
|
+
local _cbSeq = 0
|
|
417
|
+
local function _nextId() _cbSeq = _cbSeq + 1; return _cbSeq end
|
|
418
|
+
|
|
419
|
+
local _HbCbs = {} -- Heartbeat
|
|
420
|
+
local _RsCbs = {} -- RenderStepped
|
|
421
|
+
local _StCbs = {} -- Stepped
|
|
422
|
+
local _CaCbs = {} -- CharacterAdded / respawn
|
|
423
|
+
local _CrCbs = {} -- CharacterRemoving
|
|
424
|
+
local _IbCbs = {} -- InputBegan
|
|
425
|
+
local _IeCbs = {} -- InputEnded
|
|
426
|
+
|
|
427
|
+
-- Mock RBXScriptConnection: removing from the list on :Disconnect().
|
|
428
|
+
local function _mockConn(id, list)
|
|
429
|
+
return { Disconnect = function() list[id] = nil end }
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
local function _runFrameCbs(list, a, b)
|
|
433
|
+
for _, e in pairs(list) do
|
|
434
|
+
if e and e.comp then
|
|
435
|
+
e.comp._rawHb = (e.comp._rawHb or 0) + 1
|
|
436
|
+
local pass = not e.when or e.when()
|
|
437
|
+
if pass and e.every then
|
|
438
|
+
local t = type(e.every) == "table" and e.every() or e.every
|
|
439
|
+
local now = tick()
|
|
440
|
+
if (now - e.last) < t then pass = false else e.last = now end
|
|
441
|
+
end
|
|
442
|
+
if pass then
|
|
443
|
+
e.comp._condHb = (e.comp._condHb or 0) + 1
|
|
444
|
+
if Pulse and Pulse.Log then
|
|
445
|
+
Pulse.Log.throttle(e.comp._name, 5, "debug", "tick (condition passed)")
|
|
446
|
+
end
|
|
447
|
+
local ok, err = pcall(e.cb, a, b)
|
|
448
|
+
if not ok then
|
|
449
|
+
warn("[Pulse] "..tostring(e.comp._name).." error: "..tostring(err))
|
|
450
|
+
if Pulse and Pulse.Log then
|
|
451
|
+
Pulse.Log.error(e.comp._name, "event error: "..tostring(err))
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
local function _runEventCbs(list, a, b)
|
|
460
|
+
for _, e in pairs(list) do
|
|
461
|
+
if e then pcall(e.cb, a, b) end
|
|
462
|
+
end
|
|
463
|
+
end
|
|
422
464
|
|
|
423
|
-
|
|
465
|
+
-- Public runners — called by compiler-generated user-scope connections.
|
|
466
|
+
_PulseRunHeartbeat = function(a,b) _runFrameCbs(_HbCbs, a,b) end
|
|
467
|
+
_PulseRunRenderStepped = function(a,b) _runFrameCbs(_RsCbs, a,b) end
|
|
468
|
+
_PulseRunStepped = function(a,b) _runFrameCbs(_StCbs, a,b) end
|
|
469
|
+
_PulseRunCharAdded = function(c) _runEventCbs(_CaCbs, c) end
|
|
470
|
+
_PulseRunCharRemoving = function(c) _runEventCbs(_CrCbs, c) end
|
|
471
|
+
_PulseRunInputBegan = function(i,g) _runEventCbs(_IbCbs, i,g) end
|
|
472
|
+
_PulseRunInputEnded = function(i,g) _runEventCbs(_IeCbs, i,g) end
|
|
473
|
+
|
|
474
|
+
local function _hbBind(list, optsOrFn, fn)
|
|
424
475
|
local comp = _needComp("event")
|
|
425
476
|
local opts, cb
|
|
426
477
|
if type(optsOrFn) == "function" then cb=optsOrFn; opts={}
|
|
427
478
|
else opts=optsOrFn; cb=fn end
|
|
428
|
-
local
|
|
429
|
-
comp
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
local now=tick(); if (now-last)<t then return end; last=now
|
|
434
|
-
end
|
|
435
|
-
-- Throttled trace: confirms heartbeat is running when condition passes.
|
|
436
|
-
if Pulse and Pulse.Log then
|
|
437
|
-
Pulse.Log.throttle(comp._name, 5, "debug", "tick (condition passed)")
|
|
438
|
-
end
|
|
439
|
-
local ok,err=pcall(cb,a,b)
|
|
440
|
-
if not ok then
|
|
441
|
-
warn("[Pulse] "..comp._name.." event error: "..tostring(err))
|
|
442
|
-
if Pulse and Pulse.Log then
|
|
443
|
-
Pulse.Log.error(comp._name, "tick error: "..tostring(err))
|
|
444
|
-
end
|
|
445
|
-
end
|
|
446
|
-
end))
|
|
479
|
+
local id = _nextId()
|
|
480
|
+
comp._rawHb = comp._rawHb or 0
|
|
481
|
+
comp._condHb = comp._condHb or 0
|
|
482
|
+
list[id] = { comp=comp, when=opts.when, every=opts.every, last=0, cb=cb }
|
|
483
|
+
comp:bind("ev_"..id, _mockConn(id, list))
|
|
447
484
|
end
|
|
448
485
|
|
|
449
486
|
local _on = {}
|
|
450
|
-
_on.heartbeat = function(a,b) _hbBind(
|
|
451
|
-
_on.renderStepped = function(a,b) _hbBind(
|
|
452
|
-
_on.stepped = function(a,b) _hbBind(
|
|
453
|
-
_on.inputBegan = function(fn)
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
_on.
|
|
487
|
+
_on.heartbeat = function(a,b) _hbBind(_HbCbs, a,b) end
|
|
488
|
+
_on.renderStepped = function(a,b) _hbBind(_RsCbs, a,b) end
|
|
489
|
+
_on.stepped = function(a,b) _hbBind(_StCbs, a,b) end
|
|
490
|
+
_on.inputBegan = function(fn)
|
|
491
|
+
local c=_needComp("inputBegan"); local id=_nextId()
|
|
492
|
+
_IbCbs[id]={cb=fn}; c:bind("ib_"..id, _mockConn(id,_IbCbs))
|
|
493
|
+
end
|
|
494
|
+
_on.inputEnded = function(fn)
|
|
495
|
+
local c=_needComp("inputEnded"); local id=_nextId()
|
|
496
|
+
_IeCbs[id]={cb=fn}; c:bind("ie_"..id, _mockConn(id,_IeCbs))
|
|
497
|
+
end
|
|
498
|
+
_on.characterAdded = function(fn)
|
|
499
|
+
local c=_needComp("characterAdded"); local id=_nextId()
|
|
500
|
+
_CaCbs[id]={cb=fn}; c:bind("ca_"..id, _mockConn(id,_CaCbs))
|
|
501
|
+
end
|
|
502
|
+
_on.characterRemoving = function(fn)
|
|
503
|
+
local c=_needComp("characterRemoving"); local id=_nextId()
|
|
504
|
+
_CrCbs[id]={cb=fn}; c:bind("cr_"..id, _mockConn(id,_CrCbs))
|
|
505
|
+
end
|
|
506
|
+
_on.respawn = function(fn)
|
|
507
|
+
local c=_needComp("respawn"); local id=_nextId()
|
|
508
|
+
_CaCbs[id]={cb=fn}; c:bind("__respawn_"..c._name, _mockConn(id,_CaCbs))
|
|
509
|
+
end
|
|
458
510
|
_on.signal = function(sig,fn)
|
|
459
511
|
local comp = _needComp("signal")
|
|
460
512
|
comp:watch(sig, function(v)
|
|
@@ -464,7 +516,7 @@ _on.signal = function(sig,fn)
|
|
|
464
516
|
fn(v)
|
|
465
517
|
end)
|
|
466
518
|
end
|
|
467
|
-
_on.after = function(s,fn)
|
|
519
|
+
_on.after = function(s,fn) _needComp("after"):task(s,fn) end
|
|
468
520
|
|
|
469
521
|
local function _defineComponent(name, setup)
|
|
470
522
|
local comp = Component(name)
|
|
@@ -490,6 +542,7 @@ local function _groupbox(side, title, opts)
|
|
|
490
542
|
return { type="groupbox", side=side, title=title,
|
|
491
543
|
icon=(opts and opts.icon) or "",
|
|
492
544
|
mount=(opts and opts.mount) or nil,
|
|
545
|
+
plain=(opts and opts.plain) or false,
|
|
493
546
|
widgets=(opts and opts.widgets) or {} }
|
|
494
547
|
end
|
|
495
548
|
|