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.
@@ -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 the bundle table (_P) as an argument; we write every key into
9
- -- this chunk's own environment so names like "Pulse" resolve correctly below.
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) opts.signal(v) end,
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) opts.signal(v) end,
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 = component[w.signal]
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)