From 9a1ff1e24c60775cc90c9da1c5dae432e6367484 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 1 Apr 2025 21:31:51 +1300 Subject: [PATCH 001/101] Fix buttons firing when registered Buttons that were held down when registered were firing instantly --- scripts/vscripts/alyxlib/controls/input.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/vscripts/alyxlib/controls/input.lua b/scripts/vscripts/alyxlib/controls/input.lua index 5a13faf..00826fc 100644 --- a/scripts/vscripts/alyxlib/controls/input.lua +++ b/scripts/vscripts/alyxlib/controls/input.lua @@ -305,8 +305,8 @@ function Input:ListenToButton(kind, hand, button, presses, callback, context) kind = kind, multiple_press_count = 0, - press_time = -1, - release_time = 0, + press_time = vlua.select(kind == "press", 0, -1), + release_time = vlua.select(kind == "release", 0, -1), prev_press_time = 0, } From 0981f9bdba306f51a10b411c40888adaee75e6b6 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 1 Apr 2025 21:32:20 +1300 Subject: [PATCH 002/101] Add IsFakeVREnabled --- scripts/vscripts/alyxlib/globals.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/vscripts/alyxlib/globals.lua b/scripts/vscripts/alyxlib/globals.lua index a4a54ab..f9c14d0 100644 --- a/scripts/vscripts/alyxlib/globals.lua +++ b/scripts/vscripts/alyxlib/globals.lua @@ -261,6 +261,14 @@ function IsVREnabled() return GlobalSys:CommandLineCheck('-vr') end +--- +---Gets if the game was started with `+vr_enable_fake_vr 1`. +--- +---@return boolean +function IsFakeVREnabled() + return GlobalSys:CommandLineInt("+vr_enable_fake_vr", 0) == 1 +end + --- ---Prints all arguments with spaces between instead of tabs. --- From bf5342bd9becd1e8fd143047fd9f5ed26fa6b8cd Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 1 Apr 2025 21:32:56 +1300 Subject: [PATCH 003/101] Handle nil ents on EntStr --- scripts/vscripts/alyxlib/debug/common.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/vscripts/alyxlib/debug/common.lua b/scripts/vscripts/alyxlib/debug/common.lua index 4549c9d..352ae55 100644 --- a/scripts/vscripts/alyxlib/debug/common.lua +++ b/scripts/vscripts/alyxlib/debug/common.lua @@ -792,6 +792,10 @@ end ---@param ent EntityHandle ---@return string function Debug.EntStr(ent) + if ent == nil then + return "[nil, nil]" + end + return "[" .. ent:GetClassname() .. ", " .. ent:GetName() .. "]" end From 02caba490d5e1e7b5f16d401a1f54552678a65c7 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 1 Apr 2025 21:34:50 +1300 Subject: [PATCH 004/101] Add missing debug newline --- scripts/vscripts/alyxlib/debug/controller.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/vscripts/alyxlib/debug/controller.lua b/scripts/vscripts/alyxlib/debug/controller.lua index 86d78fa..6f33622 100644 --- a/scripts/vscripts/alyxlib/debug/controller.lua +++ b/scripts/vscripts/alyxlib/debug/controller.lua @@ -46,6 +46,7 @@ Convars:RegisterCommand("alyxlib_start_print_controller_button_presses", functio if buttonsPressed[h][i] ~= nil then buttonsPressed[h][i] = nil Msg(desc .. " controller: [".. i .."] " .. Input:GetButtonDescription(i) .. " Released\n") + msgPrinted = true end end end From 4f64d6d5b9beec4446c3c7beef567291ac960599 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 1 Apr 2025 21:36:02 +1300 Subject: [PATCH 005/101] Improve info display for ent_find_by_address --- scripts/vscripts/alyxlib/debug/commands.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/vscripts/alyxlib/debug/commands.lua b/scripts/vscripts/alyxlib/debug/commands.lua index 6db7061..19b3661 100644 --- a/scripts/vscripts/alyxlib/debug/commands.lua +++ b/scripts/vscripts/alyxlib/debug/commands.lua @@ -455,11 +455,11 @@ RegisterAlyxLibCommand("ent_find_by_address", function (_, tblpart, colon, hash) local foundEnt = Debug.FindEntityByHandleString(tblpart, colon, hash) if foundEnt then - Msg("Info for " .. tostring(foundEnt).."\n") - Msg("\tClassname" .. foundEnt:GetClassname().."\n") - Msg("\tName" .. foundEnt:GetName().."\n") - Msg("\tParent" .. foundEnt:GetMoveParent().."\n") - Msg("\tModel" .. foundEnt:GetModelName()) + Msg("Info for " .. tostring(foundEnt) .."\n") + Msg("\tClassname: " .. foundEnt:GetClassname() .."\n") + Msg("\tName: " .. foundEnt:GetName().."\n") + Msg("\tParent: " .. (tostring(foundEnt:GetMoveParent() or "[none]")) .."\n") + Msg("\tModel: " .. foundEnt:GetModelName()) else Msg("Could not find any entity matching '" .. hash .. "'") end From cf49f7399eac942c8011143a52de8b7d18ce970e Mon Sep 17 00:00:00 2001 From: FrostSource Date: Sun, 6 Apr 2025 20:18:46 +1200 Subject: [PATCH 006/101] Add custom data for PlayerEventWeaponSwitch --- scripts/vscripts/alyxlib/player/events.lua | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/vscripts/alyxlib/player/events.lua b/scripts/vscripts/alyxlib/player/events.lua index ed7046f..78cf0f0 100644 --- a/scripts/vscripts/alyxlib/player/events.lua +++ b/scripts/vscripts/alyxlib/player/events.lua @@ -648,6 +648,8 @@ end ListenToGameEvent("player_drop_resin_in_backpack", listenEventPlayerDropResinInBackpack, nil) ---@class PlayerEventWeaponSwitch : GameEventWeaponSwitch +---@field item EntityHandle|nil # The handle of the weapon being switched to or nil if no weapon. +---@field item_class string # Classname of the entity that was switched to. ---Track weapon equipped ---@param data GameEventWeaponSwitch @@ -658,10 +660,13 @@ local function listenEventWeaponSwitch(data) Player.PreviouslyEquipped = Player.CurrentlyEquipped + ---@type EntityHandle + local weaponHandle = nil + if data.item == "hand_use_controller" then Player.CurrentlyEquipped = PLAYER_WEAPON_HAND else - local weaponHandle = Entities:FindBestMatching("", data.item, Player.PrimaryHand:GetPalmPosition(), 64) + weaponHandle = Entities:FindBestMatching("", data.item, Player.PrimaryHand:GetPalmPosition(), 64) if data.item == "hlvr_weapon_energygun" then Player.CurrentlyEquipped = PLAYER_WEAPON_ENERGYGUN Player.Items.weapons.energygun = weaponHandle @@ -689,6 +694,9 @@ local function listenEventWeaponSwitch(data) savePlayerData() -- Registered callback + local newdata = vlua.clone(data)--[[@as PlayerEventWeaponSwitch]] + newdata.item = weaponHandle + newdata.item_class = data.item eventCallback(data.game_event_name, data) end ListenToGameEvent("weapon_switch", listenEventWeaponSwitch, nil) From 2af55126f6e5a2384659920a9c462fbcbb1bbe1a Mon Sep 17 00:00:00 2001 From: FrostSource Date: Sun, 6 Apr 2025 21:56:56 +1200 Subject: [PATCH 007/101] Fix incorrect data sent --- scripts/vscripts/alyxlib/player/events.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vscripts/alyxlib/player/events.lua b/scripts/vscripts/alyxlib/player/events.lua index 78cf0f0..f71ab6c 100644 --- a/scripts/vscripts/alyxlib/player/events.lua +++ b/scripts/vscripts/alyxlib/player/events.lua @@ -697,7 +697,7 @@ local function listenEventWeaponSwitch(data) local newdata = vlua.clone(data)--[[@as PlayerEventWeaponSwitch]] newdata.item = weaponHandle newdata.item_class = data.item - eventCallback(data.game_event_name, data) + eventCallback(data.game_event_name, newdata) end ListenToGameEvent("weapon_switch", listenEventWeaponSwitch, nil) From b85b4380ba74e313f79b852f98c9ba60ea7313b6 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Mon, 7 Apr 2025 16:39:07 +1200 Subject: [PATCH 008/101] Add missing Debug.GetSourceLine --- scripts/vscripts/alyxlib/debug/common.lua | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scripts/vscripts/alyxlib/debug/common.lua b/scripts/vscripts/alyxlib/debug/common.lua index 352ae55..9858617 100644 --- a/scripts/vscripts/alyxlib/debug/common.lua +++ b/scripts/vscripts/alyxlib/debug/common.lua @@ -895,4 +895,13 @@ function Debug.ToOrdinalString(n) return n .. (suffixes[lastDigit] or "th") end +--- +---Get the script name and line number of a function or traceback level. +--- +---@param f integer|function # Level or function +---@return string +function Debug.GetSourceLine(f) + return debug.getinfo(f, "S").short_src..":"..tostring(debug.getinfo(f, "l").currentline) +end + return Debug.version \ No newline at end of file From 53248c4a2a0079287715bb0baa51afeda1d6bd75 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Mon, 7 Apr 2025 16:40:21 +1200 Subject: [PATCH 009/101] Set primary held item to weapon on weapon switch --- scripts/vscripts/alyxlib/player/events.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/vscripts/alyxlib/player/events.lua b/scripts/vscripts/alyxlib/player/events.lua index f71ab6c..0c6058a 100644 --- a/scripts/vscripts/alyxlib/player/events.lua +++ b/scripts/vscripts/alyxlib/player/events.lua @@ -691,6 +691,8 @@ local function listenEventWeaponSwitch(data) Player:UpdateWeaponsExistence() + Player.PrimaryHand.ItemHeld = weaponHandle + savePlayerData() -- Registered callback From 96f6fb55f2ee33adea54fbed186adcfe9e2e8d3b Mon Sep 17 00:00:00 2001 From: FrostSource Date: Mon, 7 Apr 2025 16:40:54 +1200 Subject: [PATCH 010/101] Save more player data --- scripts/vscripts/alyxlib/player/events.lua | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/scripts/vscripts/alyxlib/player/events.lua b/scripts/vscripts/alyxlib/player/events.lua index 0c6058a..e16ab7a 100644 --- a/scripts/vscripts/alyxlib/player/events.lua +++ b/scripts/vscripts/alyxlib/player/events.lua @@ -107,6 +107,10 @@ local player_weapon_to_ammotype = --- local function savePlayerData() Storage.SaveTable(Player, "PlayerItems", Player.Items) + + -- Weapons aren't re-equipped on load so we need to save this + Storage.SaveString(Player, "PlayerCurrentlyEquipped", Player.CurrentlyEquipped) + if Player and Player.LeftHand then Storage.SaveEntity(Player, "LeftWristItem", Player.LeftHand.WristItem) end @@ -117,6 +121,15 @@ end local function loadPlayerData() Player.Items = Storage.LoadTable(Player, "PlayerItems", Player.Items) + + Player.CurrentlyEquipped = Storage.LoadString(Player, "PlayerCurrentlyEquipped", Player.CurrentlyEquipped) + + if Player and Player.LeftHand then + Player.LeftHand.WristItem = Storage.LoadEntity(Player, "LeftWristItem", Player.LeftHand.WristItem) + end + if Player and Player.RightHand then + Player.RightHand.WristItem = Storage.LoadEntity(Player, "RightWristItem", Player.RightHand.WristItem) + end end ---Callback logic for every player event. From c474047ab2e9ed8ae68e389ba5fadf42484e0fd5 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 8 Apr 2025 23:58:27 +1200 Subject: [PATCH 011/101] Add initial working debug menu --- .../layout/custom_game/alyxlib_debug_menu.xml | 70 ++ .../scripts/custom_game/alyxlib_debug_menu.js | 603 ++++++++++++++++++ panorama/scripts/custom_game/panorama_lua.js | 8 +- panorama/scripts/custom_game/panoramadoc.js | 21 +- .../styles/custom_game/alyxlib_debug_menu.css | 287 +++++++++ scripts/vscripts/alyxlib/controls/input.lua | 8 +- scripts/vscripts/alyxlib/debug/debug_menu.lua | 394 ++++++++++++ scripts/vscripts/alyxlib/init.lua | 1 + 8 files changed, 1381 insertions(+), 11 deletions(-) create mode 100644 panorama/layout/custom_game/alyxlib_debug_menu.xml create mode 100644 panorama/scripts/custom_game/alyxlib_debug_menu.js create mode 100644 panorama/styles/custom_game/alyxlib_debug_menu.css create mode 100644 scripts/vscripts/alyxlib/debug/debug_menu.lua diff --git a/panorama/layout/custom_game/alyxlib_debug_menu.xml b/panorama/layout/custom_game/alyxlib_debug_menu.xml new file mode 100644 index 0000000..4f90d1b --- /dev/null +++ b/panorama/layout/custom_game/alyxlib_debug_menu.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/panorama/scripts/custom_game/alyxlib_debug_menu.js b/panorama/scripts/custom_game/alyxlib_debug_menu.js new file mode 100644 index 0000000..27221dd --- /dev/null +++ b/panorama/scripts/custom_game/alyxlib_debug_menu.js @@ -0,0 +1,603 @@ +"use strict"; + +///TODO: Add pop up for warnings and errors + +if(false)p=require("./panoramadoc"); + +function FireOutput(outputName, ...args) { + if (args === undefined) args = []; + const formattedArgs = args.map(arg => + typeof arg === "string" ? `'${arg}'` : String(arg) + ); + const callString = `${outputName}(${formattedArgs.join(",")})`; + $.DispatchEvent("ClientUI_FireOutputStr", 0, callString); + + $.Msg(callString); +} + +/** + * Maps category id to category object. + * @type {Category[]} + */ +let categories = []; + +const numberOfVisibleCategories = 5; + +let categoryBarCycleIndex = 0; + +/** + * @type {Category} + */ +let currentlySelectedCategory = null; + +/** + * @type {Panel} + */ +let currentlyActiveButton = null; + +function TurnButtonIntoDebugMenuButton(button) +{ + if (button == null) return; + + button.SetPanelEvent("onmouseover", () => currentlyActiveButton = button); + button.SetPanelEvent("onmouseout", () => { + if (currentlyActiveButton == button) currentlyActiveButton = null; + }); +} + +/** + * Creates a new debug menu button. + * @param {Panel} parent Panel that the button will be added to. + * @param {function} callback Function to call when the button is pressed. + * @param {string} _class Class to apply to the button. + * @param {string} [id] Unique ID for the button. + * @returns {Panel} The newly created button. + */ +function CreateDebugMenuButton(parent, callback, _class, id) +{ + let button = $.CreatePanel("Button", parent, id); + button.AddClass(_class); + if (callback !== null && callback !== undefined) + button.SetPanelEvent("onactivate", callback); + + TurnButtonIntoDebugMenuButton(button); + + return button; +} + +/** + * @interface + * @typedef {Object} SubMenuItem + * @property {(text: string) => void} SetText + * @property {(panel: Panel) => void} AddToPanel + */ + +class Category +{ + constructor(id, name) + { + this.id = id; + this.name = name; + + /** + * @type {SubMenuItem[]} + */ + this.items = []; + + // Main submenu panel + this.panel = null; + // Content panel where items are added + this.content = null; + // Root panel that this category is attached to + this.root = null; + + // Create content panels + this.panel = $.CreatePanel("Panel", $("#CategoriesContainer"), this.id); + this.panel.AddClass("submenu"); + this.panel.AddClass("scroll"); + this.content = $.CreatePanel("Panel", this.panel, `${this.id}_content`); + this.content.AddClass("content"); + + // Create category button + // this.button = $.CreatePanel("Button", $("#CategoryBar"), `${this.id}_button`); + // this.button.AddClass("CategoryButton"); + // this.button.SetPanelEvent("onactivate", () => SetCategoryVisible(this.id)); + this.button = CreateDebugMenuButton($("#CategoryBar"), () => SetCategoryVisible(this.id), "CategoryButton", `${this.id}_button`); + let label = $.CreatePanel("Label", this.button, `${this.id}_label`); + label.text = this.name; + } + + // AddToPanel(panel) + // { + // if (this.panel == null) + // { + // this.panel = $.CreatePanel("Panel", panel, this.id); + // this.panel.AddClass("submenu"); + // this.panel.AddClass("scroll"); + // this.content = $.CreatePanel("Panel", this.panel, `${this.id}_content`); + // this.content.AddClass("content"); + // } + // else + // this.panel.SetParent(panel); + + // this.root = panel; + // } + + SetVisible(visible) + { + if (visible) + { + this.panel.AddClass("Visible"); + this.button.AddClass("Selected"); + } + else + { + this.panel.RemoveClass("Visible"); + this.button.RemoveClass("Selected"); + } + } + + SetBarButtonVisible(visible) + { + if (visible) + this.button.visible = true; + else + this.button.visible = false; + } + + /** + * Adds a button to this category. + * @param {string} id Unique identifier for the button. + * @param {string} text Text to display on the button. + * @param {function} callback Function to call when the button is pressed. + * @example + * let button = myCategory.AddButton("my_button", "My Button", () => { + * $.Msg("Button pressed!"); + * }); + */ + // _AddButtonInternal(id, text, callback) + // { + // if (this.content == null) + // { + // // Display warning + // $.Msg(`You must call AddToPanel() before adding buttons to a category!`); + // return; + // } + // // + + // // let button = $.CreatePanel("Button", this.content, `${id}_button`); + // // button.AddClass("ButtonTest"); + // // if (callback != null) + // // button.SetPanelEvent("onactivate", callback); + // let button = CreateDebugMenuButton(this.content, callback, "ButtonTest", `${this.id}_${id}`); + + // let buttonLabel = $.CreatePanel("Label", button, `${id}_label`); + // buttonLabel.AddClass("button_label"); + // buttonLabel.text = text; + + // let buttonBullet = $.CreatePanel("Image", button, `${id}_bullet`); + // buttonBullet.AddClass("button_bullet"); + // buttonBullet.SetImage("s2r://panorama/images/game_menu_ui/btn_bullet_child_page_png.vtex") + + // } + + SetItemText(id, text) + { + // let option = this.content.FindChildTraverse(`${this.id}_${id}`); + // if (option === null) + // { + // // Display warning + // $.Msg(`Option ${id} does not exist! Did you call AddToPanel()?`); + // return; + // } + + // option + + // Find item with id in this.options + let combinedId = `${this.id}_${id}`; + let item = this.items.find(o => o.id === combinedId); + if (item === undefined) + { + this.items.forEach((o) => $.Msg(o.id)); + // Display warning + $.Msg(`Item ${id} does not exist!`); + return; + } + + // text-transform: uppercase; doesn't affect js set text? + text = text.toLocaleUpperCase(); + + item.SetText(text); + } + + AddButton(id, text) + { + // this._AddButtonInternal(id, text, () => { + // // $.DispatchEvent("ClientUI_FireOutputStr", 0, `_DebugMenuCallbackButton('${id}')`); + // FireOutput("_DebugMenuCallbackButton", id); + // }); + + let button = new SubMenuButton(`${this.id}_${id}`, text, () => { + FireOutput("_DebugMenuCallbackButton", id); + }); + button.AddToPanel(this.content); + this.items.push(button); + } + + // _AddToggleInternal(id, text, callback, startsOn) + // { + // let toggle = new SubMenuToggle(id, text, callback, startsOn); + // toggle.AddToPanel(this.content); + // } + + AddToggle(id, text, startsOn) + { + // this._AddToggleInternal(id, text, startsOn, (on) => { + // // $.DispatchEvent("ClientUI_FireOutputStr", 0, `_DebugMenuCallbackToggle('${this.id}',${on})`); + // FireOutput("_DebugMenuCallbackToggle", id, on); + // }); + + let toggle = new SubMenuToggle(`${this.id}_${id}`, text, startsOn, (on) => { + FireOutput("_DebugMenuCallbackToggle", id, on); + }); + toggle.AddToPanel(this.content); + this.items.push(toggle); + } + + AddSeparator() + { + let rowDivider = $.CreatePanel("Panel", this.content, undefined); + rowDivider.AddClass("row_divider"); + + let rowDividerLabel = $.CreatePanel("Panel", rowDivider, undefined); + rowDividerLabel.AddClass("button_label"); + + let rowDividerLine = $.CreatePanel("Panel", rowDividerLabel, undefined); + rowDividerLine.AddClass("row_divider_line"); + + let rowDividerBullet = $.CreatePanel("Panel", rowDivider, undefined); + rowDividerBullet.AddClass("button_bullet"); + } +} + +class SubMenuButton +{ + constructor(id, text, callback) + { + this.id = id; + this.text = text; + this.callback = callback; + } + + AddToPanel(panel) + { + this.panel = CreateDebugMenuButton(panel, this.callback, "ButtonTest", this.id); + + let buttonLabel = $.CreatePanel("Label", this.panel, `${this.id}_label`); + buttonLabel.AddClass("button_label"); + buttonLabel.text = this.text; + + let buttonBullet = $.CreatePanel("Image", this.panel, `${this.id}_bullet`); + buttonBullet.AddClass("button_bullet"); + buttonBullet.SetImage("s2r://panorama/images/game_menu_ui/btn_bullet_child_page_png.vtex") + } + + SetText(text) + { + if (this.panel === undefined) return; + + this.text = text; + let label = this.panel.FindChildTraverse(`${this.id}_label`); + label.text = text; + } +} + +class SubMenuToggle +{ + + constructor(id, text, startsOn, callback) + { + this.id = id; + this.text = text; + this.callback = callback; + + this.isOn = startsOn; + } + + /** + * Adds this toggle button to a panel. + * @param {Panel} panel The panel to add this button to. + */ + AddToPanel(panel) + { + if (this.panel == null) + { + // this.panel = $.CreatePanel("Button", panel, `${this.id}_button`); + // this.panel.AddClass("ButtonTest"); + // if (this.callback != null) + // this.panel.SetPanelEvent("onactivate", () => this.Toggle()); + // this.panel.SetPanelEvent("onactivate", this.callback); + + this.panel = CreateDebugMenuButton(panel, () => this.Toggle(), "ButtonTest", this.id); + + this.panel.AddClass("custom_switch"); + if (!this.isOn) + this.panel.AddClass("switch_off"); + + let row = $.CreatePanel("Panel", this.panel, undefined); + row.AddClass("row"); + + let switchButton = $.CreatePanel("Panel", row, undefined); + switchButton.AddClass("switch_button"); + + // Seems too much of a hassle to change Valve's styles so keeping two different labels + let labelOn = $.CreatePanel("Label", switchButton, `${this.id}_label_on`); + labelOn.AddClass("switch_label"); + labelOn.AddClass("switch_label_on") + labelOn.text = `${this.text}`; + let labelOff = $.CreatePanel("Label", switchButton, `${this.id}_label_off`); + labelOff.AddClass("switch_label"); + labelOff.AddClass("switch_label_off") + labelOff.text = `${this.text}`; + + let switchButtonImage = $.CreatePanel("Panel", row, undefined); + switchButtonImage.AddClass("switch_button_image"); + let switchImage = $.CreatePanel("Panel", switchButtonImage, undefined); + switchImage.AddClass("switch_image"); + } + else + { + this.panel.SetParent(panel); + } + + this.root = panel; + } + + /** + * Sets the text of the toggle button on/off labels. + * @param {string} text The new text. + */ + SetText(text) + { + if (this.panel === null) return; + + this.text = text; + let labels = this.panel.FindChildrenWithClassTraverse("switch_label"); + for (const label of labels) + { + label.text = text; + } + } + + /** + * Sets the state of this toggle button. + * If the toggle button has a callback, it is called with the new state. + * @param {boolean} on If the toggle button should be on or off. + */ + SetState(on) + { + this.isOn = on; + if (this.isOn) + { + this.panel.AddClass("switch_on") + this.panel.RemoveClass("switch_off") + } + else + { + this.panel.RemoveClass("switch_on") + this.panel.AddClass("switch_off") + } + + if (this.callback != null) + { + this.callback(this.isOn); + } + } + + /** + * Toggles the state of this toggle button. + * If the toggle button has a callback, it is called with the new state. + */ + Toggle() + { + this.SetState(!this.isOn); + } +} + +/** + * Shows a specific category and hides all others. + * @param {string} id ID of the category to show. + */ +function SetCategoryVisible(id) +{ + for (const category of categories) + { + if (category.id == id) + { + // category.AddClass("Visible"); + category.SetVisible(true); + currentlySelectedCategory = category; + } + else + { + // category.RemoveClass("Visible"); + category.SetVisible(false); + } + } +} + + +/** + * Cycles the visible category in the direction given. + * @param {number} direction -1 to cycle left, 1 to cycle right. + * @see alyxlib_debug_menu.xml CategoryCycler for its usage. + */ +function CycleCategories(direction) +{ + direction = Math.sign(direction); + if (direction === 0) return; + + const currentIndex = categories.indexOf(currentlySelectedCategory); + const previousCategory = categories[(currentIndex + direction + categories.length) % categories.length]; + SetCategoryVisible(previousCategory.id); + + UpdateCategoryBarVisibility(); +} + +/** + * Updates the visibility of the category bar buttons so that the selected category is + * within the visible range. + */ +function UpdateCategoryBarVisibility() +{ + const selectedIndex = categories.indexOf(currentlySelectedCategory); + if (selectedIndex < categoryBarCycleIndex) { + categoryBarCycleIndex = selectedIndex; + } else if (selectedIndex >= categoryBarCycleIndex + numberOfVisibleCategories) { + categoryBarCycleIndex = selectedIndex - numberOfVisibleCategories + 1; + } + + categories.forEach((category, index) => { + const isVisible = index >= categoryBarCycleIndex && index < categoryBarCycleIndex + numberOfVisibleCategories; + category.SetBarButtonVisible(isVisible); + }) +} + +/** + * Creates a new category and sets it as the currently selected category if no category is currently selected. + * + * @param {string} id - The unique identifier for the category. + * @param {string} name - The display name for the category. + * @returns {Category} The newly created category. + */ + +function CreateCategory(id, name) +{ + let category = new Category(id, name); + + categories.push(category); + + if (currentlySelectedCategory == null) + SetCategoryVisible(category.id); + + return category; +} + +/** + * Finds a category by its ID. + * @param {string} id - The unique identifier for the category. + * @returns {Category} The category with the given ID, or null if none is found. + */ +function GetCategory(id) +{ + for (const category of categories) + { + if (category.id == id) return category; + } + + return null; +} + +/** + * Sends the _CloseMenu command to Lua. + * @see alyxlib_debug_menu.xml CloseMenuButton for its usage. + */ +function CloseMenu() +{ + FireOutput("_CloseMenu"); +} + +/** + * Virtually clicks the currently active button. + */ +function ClickHoveredButton() +{ + if (currentlyActiveButton !== null) + { + $.DispatchEvent("Activated", currentlyActiveButton, "mouse"); + } +} + +function ParseCommand(command, args) +{ + command = command.toLowerCase(); + + switch (command) + { + case "addcategory": { + let id = args[0]; + let name = args[1]; + CreateCategory(id, name); + break; + } + + case "addbutton": { + let category = GetCategory(args[0]); + if (category === null) + { + $.Msg(`Category ${args[0]} does not exist!`); + break; + } + + let buttonId = args[1]; + let buttonText = args[2]; + $.Msg("ID: " + buttonId + ", Text: " + buttonText); + category.AddButton(buttonId, buttonText); + break; + } + + case "addtoggle": { + let category = GetCategory(args[0]); + if (category === null) + { + $.Msg(`Category ${args[0]} does not exist!`); + break; + } + + let toggleId = args[1]; + let toggleText = args[2] || args[1]; + let toggleStartsOn = args[3] === "true"; + category.AddToggle(toggleId, toggleText, toggleStartsOn); + break; + } + + case "addseparator": { + let category = GetCategory(args[0]); + if (category === null) + { + $.Msg(`Category ${args[0]} does not exist!`); + break; + } + + category.AddSeparator(); + break; + } + + case "setitemtext": { + let category = GetCategory(args[0]); + if (category === null) + { + $.Msg(`Category ${args[0]} does not exist!`); + break; + } + + let id = args[1]; + let text = args[2]; + category.SetItemText(id, text); + break; + } + + case "clickhoveredbutton": { + ClickHoveredButton(); + break; + } + } +} + +(function() +{ + // Modify preset layout buttons to work with controller trigger + TurnButtonIntoDebugMenuButton($("#CloseMenuButton")); + TurnButtonIntoDebugMenuButton($("#CycleCategoryLeftButton")); + TurnButtonIntoDebugMenuButton($("#CycleCategoryRightButton")); +})(); \ No newline at end of file diff --git a/panorama/scripts/custom_game/panorama_lua.js b/panorama/scripts/custom_game/panorama_lua.js index ca34b6e..d9925a5 100644 --- a/panorama/scripts/custom_game/panorama_lua.js +++ b/panorama/scripts/custom_game/panorama_lua.js @@ -22,12 +22,13 @@ if(false)p=require("./panoramadoc"); * @param {null} _ Unused variable. * @param {string} encoded String of data separated by a pipe. */ -function LuaCallback(_,encoded) +function LuaCallback(_, encoded) { - $.Msg(encoded) + // $.Msg(encoded) let decoded = encoded.split("|"); let panel = $.GetContextPanel(); //$.Msg(decoded[0], panel.BHasClass(decoded[0])) + // Check if the data being sent belongs to this panel if (panel.BHasClass(decoded[0])) { @@ -58,7 +59,6 @@ function LuaCallback(_,encoded) (function() { - $.Msg("panorama_lua.js loaded") - $.RegisterForUnhandledEvent('AddStyleToEachChild',LuaCallback); + $.RegisterForUnhandledEvent('AddStyleToEachChild', LuaCallback); })(); diff --git a/panorama/scripts/custom_game/panoramadoc.js b/panorama/scripts/custom_game/panoramadoc.js index 4bbecfe..9157982 100644 --- a/panorama/scripts/custom_game/panoramadoc.js +++ b/panorama/scripts/custom_game/panoramadoc.js @@ -40,7 +40,7 @@ * "oncontextmenu_hide" | "oncontextmenu_show" | "oncontextmenu_query" | "oncontextmenu_refresh" | * "ondatechanged" | "onmodalaccepted" | "onmodalcanceled" | "onmodalprompt" | "onmodalselect" | * "onmodalsubmit" | "onselectionchange" | "onselectquery" | "onsliding" | "onsubmenuswitch" | -* "ontooltiphide" | "ontooltipshow")} Event +* "ontooltiphide" | "ontooltipshow")} PanoramaEvent */ @@ -470,7 +470,13 @@ class Panel { GetChild(int){} GetChildIndex(unknown){} Children(){} - FindChildrenWithClassTraverse(className){} + + /** + * Find children with a given class name. + * @param {string} className Class to search for + * @returns {Panel[]} Panels with the class + */ + FindChildrenWithClassTraverse(className){return} /** * Gets the parent panel. @@ -536,7 +542,16 @@ class Panel { SetReadyForDisplay(bool){} SetPositionInPixels(float1, float2, float3){} Data(unknown){} - SetPanelEvent(unknown){} + /** + * Sets an event handler for a Panorama UI panel. + * @param {Event} event The event to listen for. + * @param {function} callback The function to execute when the event is triggered. + * @example + * myButton.SetPanelEvent("onactivate", () => { + * $.Msg("Button pressed!"); + * }); + */ + SetPanelEvent(event, callback){} RunScriptInPanelContext(unknown){} rememberchildfocus(bool){} paneltype(){} diff --git a/panorama/styles/custom_game/alyxlib_debug_menu.css b/panorama/styles/custom_game/alyxlib_debug_menu.css new file mode 100644 index 0000000..b4069a7 --- /dev/null +++ b/panorama/styles/custom_game/alyxlib_debug_menu.css @@ -0,0 +1,287 @@ +#background_panel.bg +{ + /*opacity-mask: url("s2r://panorama/images/game_menu_ui/menu_type_mask_psd.vtex");*/ + flow-children: down; + width: 100%; + background-color: gradient( radial, 50% 50%, 0% 0%, 100% 100%, from( #553c18 ), color-stop( 0.97, #000000 ), to( #000000 ) ); + box-shadow: inset #995e0011 2px 2px 4px 4px; + opacity: .9; +} +#background_panel .row .col_body +{ + /*overflow: scroll;*/ +} +Panel.submenu +{ + width: 100%; + border-right: 1px solid #FFBE5544; + flow-children: down; + visibility: collapse; + height: 100%; +} +Panel.submenu.scroll +{ + overflow: squish scroll; +} +Panel.submenu.Visible +{ + visibility: visible; +} +Panel.submenu Panel.content +{ + width: 100%; + flow-children: down; + height: fit-children; +} +Panel.submenu.scroll Panel.content +{ + padding-right: 50px; +} +TextEntry +{ + width: 100%; + flow-children: none; + border: 1px solid #FFBE5588; + vertical-align: center; + text-transform: uppercase; + text-align: right; + font-size: 44px; + height: 100px; + margin-left: 24px; + margin-right: 24px; +} +TextEntry Label +{ + width: 100%; + vertical-align: center; + text-transform: uppercase; + text-align: right; + font-size: 44px; + margin-left: 24px; + margin-right: 24px; +} + +TextEntry:hover +{ + color: #fff; + background-color: #FFBE5588; + transition-property: background-color; + transition-duration: .38s; + transition-timing-function: cubic-bezier( 0.785, 0.385, 0.555, 1.505 ); + +} + +TextEntry:active +{ + color: #000000; + background-color: #ffd800; +} + +Button.custom_button_with_subtitle.error_msg.disabled, Button.custom_button_with_subtitle:not(.error_msg) +{ + flow-children: down; + height: 44px; +} +Button.custom_button_with_subtitle.error_msg.disabled Label.button_label,Button.custom_button_with_subtitle:not(.error_msg) Label.button_label +{ + padding: 0; + margin: 0; +} +Button.custom_button_with_subtitle.error_msg.disabled Label.button_label.sub,Button.custom_button_with_subtitle:not(.error_msg) Label.button_label.sub +{ + padding: 0; + margin: 0; + font-size: 20px; +} +Button.custom_button_with_subtitle.error_msg:not(.disabled) Label.button_label.sub +{ + visibility: collapse; +} + +#CategoryCycler +{ + flow-children: right; + width: 100%; + /* background-color: green; */ +} + +.CycleCategory +{ + /* margin-left: 9px; */ + flow-children: left; + /* horizontal-align: right; */ + width: 100%; +} +.CycleCategoryLeft +{ + flow-children: left; + horizontal-align: left; + overflow: clip clip; + /* background-color: red; */ + width: 10%; +} +.CycleCategoryRight +{ + flow-children: left; + horizontal-align: right; + overflow: clip clip; + /* background-color: blue; */ + width: 10%; +} +.cycle_image +{ + /* background-color: yellow; */ + horizontal-align: left; + margin: 0px 40px 10px 10px; +} + +.CategoryBar +{ + flow-children: right; + /* background-color: yellow; */ + width: fill-parent-flow(1); + height: 100%; + opacity: 0.5; +} +.CategoryButton +{ + width: 20%; + height: 100%; + /* background-color: red; */ + horizontal-align: left; + + border-bottom: 5px solid #FFBE55; + margin: 0px 5px 0px 5px; +} +.CategoryButton.Selected +{ + color: #fff; + background-color: #FFBE5588; +} +.CategoryButton Label +{ + text-align: left; + horizontal-align: center; + /* white-space: nowrap; */ + /* text-overflow: ellipsis; */ + overflow: clip; + font-size: 30px; + text-align: center; + /* background-color: green; */ +} + +Button.disabled Label +{ + color: #FFBE55; + blur: gaussian(1.5); + brightness: 0.75; + opacity: .7; +} + +Button:active +{ + sound: "PanoUI.Click"; +} + +Button.custom_switch:active +{ + sound: "PanoUI.ToggleOption"; +} + +.custom_switch +{ + width: 100%; + min-height: 116px; + /* This is confusing. Why can't I apply this style further up the heirarchy? */ + flow-children: right; + horizontal-align: right; + text-align: right; + /* Hover 'Off' values */ + background-color: #FFBE5500; + transition-property: background-color; + transition-duration: 0s; +} + +.custom_switch Label +{ + vertical-align: center; + horizontal-align: right; + font-size: 44px; + text-transform: uppercase; + /* text-shadow: 0px 0px 64px 3 #f4ec8e33; */ + text-align: right; +} + +.custom_switch.mini_option Label +{ + /*font-size: 70px;*/ +} + +.custom_switch:hover +{ + color: #fff; + background-color: #FFBE5588; + transition-property: background-color; + transition-duration: .38s; + transition-timing-function: cubic-bezier( 0.785, 0.385, 0.555, 1.505 ); + +} + +.custom_switch:active +{ + background-color: #FFBE55; + color: #000; + transition-property: background-color; + transition-duration: 0s; +} + +Button +{ + min-height: 80px; +} + +.button_bullet +{ + padding-right: 5px; +} + +.button_label +{ + padding-right: 30px; +} + + +.submenu Label +{ + font-size: 33px; +} + + +.single_controller_on .custom_switch.single_controller:hover:not(.disabled) .switch_image, +.custom_switch:hover:not(.disabled) .switch_image +{ + background-image: url("s2r://panorama/images/game_menu_ui/component_switch_on_hover_png.vtex"); + brightness: 1.0; +} + +.single_controller_off .custom_switch.single_controller:hover:not(.disabled) .switch_image, +.custom_switch.switch_off:hover:not(.disabled) .switch_image +{ + background-image: url("s2r://panorama/images/game_menu_ui/component_switch_off_hover_png.vtex"); + brightness: 1.0; +} + + +.single_controller_off .custom_switch.single_controller:active:not(.disabled) .switch_image, +.custom_switch.switch_off:active:not(.disabled) .switch_image +{ + background-image: url("s2r://panorama/images/game_menu_ui/component_switch_off_press_png.vtex"); + brightness: 1.0; +} + +.single_controller_on .custom_switch.single_controller:active:not(.disabled) .switch_image, +.custom_switch:active:not(.disabled) .switch_image +{ + background-image: url("s2r://panorama/images/game_menu_ui/component_switch_on_press_png.vtex"); + brightness: 1.0; +} diff --git a/scripts/vscripts/alyxlib/controls/input.lua b/scripts/vscripts/alyxlib/controls/input.lua index 00826fc..a1521c5 100644 --- a/scripts/vscripts/alyxlib/controls/input.lua +++ b/scripts/vscripts/alyxlib/controls/input.lua @@ -119,10 +119,10 @@ InputHandPrimary = 2 InputHandSecondary = 3 ---@alias InputHandKind ----| `INPUT_HAND_LEFT` ----| `INPUT_HAND_RIGHT` ----| `INPUT_HAND_PRIMARY` ----| `INPUT_HAND_SECONDARY` +---| `InputHandLeft` +---| `InputHandRight` +---| `InputHandPrimary` +---| `InputHandSecondary` ---| 0 # Left Hand. ---| 1 # Right Hand. ---| 2 # Primary Hand. diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua new file mode 100644 index 0000000..f285aeb --- /dev/null +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -0,0 +1,394 @@ + + +RegisterAlyxLibCommand("alyxlib_debug_menu_show", function (name, ...) + DebugMenu:ShowMenu() +end, "", 0) + +---@class DebugMenu +DebugMenu = {} + +---@class DebugMenuCategory +---@field id string +---@field name string +---@field items DebugMenuItem[] + +---@class DebugMenuItem +---@field categoryId string +---@field id string +---@field text string +---@field callback function +---@field type "button"|"toggle"|"separator" +---@field default any + +---@type CPointClientUIWorldPanel +DebugMenu.panel = nil + +---@type DebugMenuCategory[] +DebugMenu.categories = {} + +-- ---All options added to all categories +-- ---@type table +-- DebugMenu.options = {} + +-- local options = DebugMenu.options + +local buttonActivatethink = function() + local buttonPressesToActivate = 5 + local buttonPresses = 0 + local timeToResetBetweenPresses = 0.6 + local buttonPressed = false + local timeSinceLastButtonPress = 0 + + Player:SetContextThink("debug_menu_activate", function() + if Time() - timeSinceLastButtonPress > timeToResetBetweenPresses then + buttonPresses = 0 + timeSinceLastButtonPress = math.huge + print("RESET") + end + + if Player:IsDigitalActionOnForHand(Player.SecondaryHand.Literal, DIGITAL_INPUT_TOGGLE_MENU) then + if not buttonPressed then + buttonPressed = true + timeSinceLastButtonPress = Time() + buttonPresses = buttonPresses + 1 + + print(buttonPresses) + if buttonPresses >= buttonPressesToActivate then + DebugMenu:ShowMenu() + buttonPresses = 0 + return nil + end + end + else + if buttonPressed then + buttonPressed = false + end + end + return 0 + end, 0) +end + +local debugPanelScriptScope = { + _DebugMenuCallbackButton = function(id) + local item = DebugMenu:GetItem(id) + if not item then + warn("Unknown item for panorama callback'"..id.."'") + return + end + + if item.type ~= "button" then + warn("Option '"..id.."' is not a button!") + return + end + + if item then + item.callback() + end + end, + + _DebugMenuCallbackToggle = function(id, on) + local item = DebugMenu:GetItem(id) + if not item then + warn("Unknown item for panorama callback'"..id.."'") + return + end + + if item.type ~= "toggle" then + warn("Option '"..id.."' is not a toggle!") + return + end + + if item then + item.callback(on) + -- Hack for keeping state after close + item.startsOn = on + end + end, + + _CloseMenu = function() + DebugMenu:CloseMenu() + end, +} + + + +local function updateDebugMenu() + if not DebugMenu.panel then + return + end + + local panel = DebugMenu.panel + + for categoryId, category in pairs(DebugMenu.categories) do + Panorama:Send(panel, "AddCategory", category.id, category.name) + + for _, item in ipairs(category.items) do + if item.type == "toggle" then + Panorama:Send(panel, "AddToggle", item.categoryId, item.id, item.text, item.default) + elseif item.type == "button" then + Panorama:Send(panel, "AddButton", item.categoryId, item.id, item.text) + elseif item.type == "separator" then + Panorama:Send(panel, "AddSeparator", item.categoryId) + else + warn("Unknown item type '"..item.type.."'") + end + end + end +end + + +function DebugMenu:ShowMenu() + -- if spawnMenu ~= nil then + -- --return + -- HideMenu() + -- end + + local menu = SpawnEntityFromTableSynchronous("point_clientui_world_panel", { + targetname = "spawnmenu", + dialog_layout_name = "file://{resources}/layout/custom_game/alyxlib_debug_menu.xml", + width = 16,--24, + height = 12,--16 + panel_dpi = 64, + ignore_input = 0, + lit = 0, + interact_distance = 8, + + vertical_align = "1", + -- orientation = "0", + horizontal_align = "1", + }) + + + if not Player.HMDAvatar then + local localPlayer = Entities:GetLocalPlayer() + local eyePos = localPlayer:EyePosition() + local dir = localPlayer:EyeAngles():Forward() + local a = VectorToAngles(dir) + a = RotateOrientation(a, QAngle(0,-90,90)) + menu:SetQAngle(a) + menu:SetOrigin(eyePos + dir * 16) + else + -- menu:SetParent(Player.HMDAvatar,"") + -- menu:SetLocalOrigin(Vector(16)) + -- menu:SetLocalAngles(0,-90,90) + -- menu:SetParent(nil, "") + + menu:SetParent(Player.LeftHand, "") + menu:SetLocalAngles(40,-10,10) + menu:SetLocalOrigin(Vector(0,8,-2)) + + for _, child in ipairs(Player.HMDAvatar:GetChildrenMemSafe()) do + if child:GetModelName() == "models/props/handposes/handpose_cough.vmdl" then + child:EntFire("Disable") + end + end + + -- local collision = SpawnEntityFromTableSynchronous("func_clip_interaction_layer", { + -- targetname = "col", + -- InteractsWith = "LeftHand", + -- InteractsAs = "LeftHand", + -- -- model="models/alyxlib/debug_menu_collision2.vmdl", + -- model="models/alyxlib/debug_menu_collision.vmdl", + -- }) + + -- local collision2 = SpawnEntityFromTableSynchronous("prop_dynamic", { + -- targetname = "col1", + -- -- InteractsWith = "LeftHand", + -- -- InteractsAs = "LeftHand", + -- model="models/alyxlib/debug_menu_collision2.vmdl", + -- -- model="models/alyxlib/debug_menu_collision.vmdl", + -- }) + -- collision2:SetAbsScale(5) + -- -- collision:SetSize(Vector(-16, -16, -16), Vector(16, 16, 16)) + -- collision2:SetParent(menu, "") + -- collision2:ResetLocal() + -- collision2:SetLocalOrigin(Vector(0,0,2.7)) + + -- -- collision:SetModel("models/alyxlib/debug_menu_collision2.vmdl") + -- collision:SetAbsScale(5) + -- -- collision:SetSize(Vector(-16, -16, -16), Vector(16, 16, 16)) + -- collision:SetParent(menu, "") + -- collision:ResetLocal() + -- collision:SetLocalOrigin(Vector(0,0,2.7)) + end + + -- EntFireByHandle(thisEntity, menu, "AddOutput", "CustomOutput0>spawnmenu_script>RunScriptCode>>>") + -- for addon,loaded in pairs(loadedAddons) do + -- menu:AddCSSClasses("addon_"..addon) + -- end + menu:AddCSSClasses("Visible") + + local scope = menu:GetOrCreatePrivateScriptScope() + vlua.tableadd(scope, debugPanelScriptScope) + + menu:AddOutput("CustomOutput0", "!self", "RunScriptCode") + + Panorama:InitPanel(menu, "alyxlib_debug_menu") + self.panel = menu + + updateDebugMenu() + + Input:ListenToButton("press", InputHandPrimary, DIGITAL_INPUT_MENU_INTERACT, 1, function (params) + self:ClickHoveredButton() + end, self) + +end + +function DebugMenu:ClickHoveredButton() + if self.panel then + Panorama:Send(self.panel, "ClickHoveredButton") + end +end + +function DebugMenu:CloseMenu() + if self.panel then + self.panel:Kill() + self.panel = nil + + Input:StopListeningByContext(self) + + buttonActivatethink() + end +end + +---Get a debug menu item by id. +---@param id string +---@return DebugMenuItem? +function DebugMenu:GetItem(id) + for _, category in ipairs(self.categories) do + for _, item in ipairs(category.items) do + if item.id == id then + return item + end + end + end +end + +---Get a debug menu category by id. +---@param id string +---@return DebugMenuCategory? +function DebugMenu:GetCategory(id) + for _, category in ipairs(self.categories) do + if category.id == id then + return category + end + end +end + +function DebugMenu:AddCategory(id, name) + if self:GetCategory(id) then + warn("Category '"..id.."' already exists!") + return + end + + table.insert(self.categories, { + id = id, + name = name, + items = {}, + }) +end + +---Add a separator line to a category. +---@param categoryId string # The category ID to add the separator to +function DebugMenu:AddSeparator(categoryId) + local category = self:GetCategory(categoryId) + if not category then + warn("Cannot add separator: Category '"..categoryId.."' does not exist!") + return + end + + table.insert(category.items, { + categoryId = categoryId, + type = "separator", + }) +end + +---Add a button to a category. +---@param categoryId string # The category ID to add the button to +---@param buttonId string # The unique ID for this button +---@param text string # The text to display on this button +---@param command string|function # The console command or function to run when this button is pressed +function DebugMenu:AddButton(categoryId, buttonId, text, command) + local category = self:GetCategory(categoryId) + if not category then + warn("Cannot add button '"..buttonId.."': Category '"..categoryId.."' does not exist!") + return + end + + local callback + if type(command) == "string" then + callback = function() + SendToConsole(command) + end + elseif type(command) == "function" then + callback = command + end + + table.insert(category.items, { + categoryId = categoryId, + id = buttonId, + text = text, + callback = callback, + type = "button", + }) +end + +---Add a toggle to a category. +---@param categoryId string # The category ID to add the toggle to +---@param toggleId string # The unique ID for this toggle +---@param text string # The text to display on this toggle +---@param command string|function # The console command or function to run when this toggle is toggled (will run with 1 if it's on, 0 if it's off) +---@param startsOn? boolean # Whether the toggle is on by default +function DebugMenu:AddToggle(categoryId, toggleId, text, command, startsOn) + local category = self:GetCategory(categoryId) + if not category then + warn("Cannot add toggle '"..toggleId.."': Category '"..categoryId.."' does not exist!") + return + end + + local callback + if type(command) == "string" then + callback = function(on) + SendToConsole(command .. " " .. (on and 1 or 0)) + end + elseif type(command) == "function" then + callback = command + end + + table.insert(category.items, { + categoryId = categoryId, + id = toggleId, + text = text, + callback = callback, + type = "toggle", + default = startsOn or false, + }) +end + +function DebugMenu:SetItemText(categoryId, itemId, text) + local item = self:GetItem(itemId) + if not item then + warn("Cannot set item text '"..itemId.."': Item does not exist!") + return + end + + item.text = text + + if self.panel then + Panorama:Send(self.panel, "SetItemText", categoryId, itemId, text) + end +end + +ListenToPlayerEvent("player_activate", function() + Player:Delay(function() + buttonActivatethink() + end, 0.2) +end) + +-- AlyxLib defaults + +DebugMenu:AddCategory("alyxlib", "AlyxLib") + +DebugMenu:AddToggle("alyxlib", "alyxlib_noclip_vr", "NoClip VR", "noclip_vr") + +DebugMenu:AddToggle("alyxlib", "alyxlib_godmode", "God Mode", "god") \ No newline at end of file diff --git a/scripts/vscripts/alyxlib/init.lua b/scripts/vscripts/alyxlib/init.lua index 5a19946..09c087a 100644 --- a/scripts/vscripts/alyxlib/init.lua +++ b/scripts/vscripts/alyxlib/init.lua @@ -92,6 +92,7 @@ if IsVREnabled() then else alyxlib_require "alyxlib.debug.novr" end +alyxlib_require "alyxlib.debug.debug_menu" -- Common third-party libraries From 55591c18ce2d9ab72e5cf47d2f233d91ee198970 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Wed, 9 Apr 2025 00:19:11 +1200 Subject: [PATCH 012/101] Clean up code --- .../scripts/custom_game/alyxlib_debug_menu.js | 95 ++-------------- .../styles/custom_game/alyxlib_debug_menu.css | 35 ------ scripts/vscripts/alyxlib/debug/debug_menu.lua | 106 +++++++----------- scripts/vscripts/alyxlib/player/core.lua | 21 ++++ 4 files changed, 71 insertions(+), 186 deletions(-) diff --git a/panorama/scripts/custom_game/alyxlib_debug_menu.js b/panorama/scripts/custom_game/alyxlib_debug_menu.js index 27221dd..21825df 100644 --- a/panorama/scripts/custom_game/alyxlib_debug_menu.js +++ b/panorama/scripts/custom_game/alyxlib_debug_menu.js @@ -4,6 +4,13 @@ if(false)p=require("./panoramadoc"); +/** + * Fires a Panorama output with the given name and arguments. + * The output is routed to the panel's input 'RunScriptCode'. + * + * @param {string} outputName - The name of the output to fire. + * @param {...*} args - The arguments to pass to the output. + */ function FireOutput(outputName, ...args) { if (args === undefined) args = []; const formattedArgs = args.map(arg => @@ -99,30 +106,11 @@ class Category this.content.AddClass("content"); // Create category button - // this.button = $.CreatePanel("Button", $("#CategoryBar"), `${this.id}_button`); - // this.button.AddClass("CategoryButton"); - // this.button.SetPanelEvent("onactivate", () => SetCategoryVisible(this.id)); this.button = CreateDebugMenuButton($("#CategoryBar"), () => SetCategoryVisible(this.id), "CategoryButton", `${this.id}_button`); let label = $.CreatePanel("Label", this.button, `${this.id}_label`); label.text = this.name; } - // AddToPanel(panel) - // { - // if (this.panel == null) - // { - // this.panel = $.CreatePanel("Panel", panel, this.id); - // this.panel.AddClass("submenu"); - // this.panel.AddClass("scroll"); - // this.content = $.CreatePanel("Panel", this.panel, `${this.id}_content`); - // this.content.AddClass("content"); - // } - // else - // this.panel.SetParent(panel); - - // this.root = panel; - // } - SetVisible(visible) { if (visible) @@ -145,61 +133,14 @@ class Category this.button.visible = false; } - /** - * Adds a button to this category. - * @param {string} id Unique identifier for the button. - * @param {string} text Text to display on the button. - * @param {function} callback Function to call when the button is pressed. - * @example - * let button = myCategory.AddButton("my_button", "My Button", () => { - * $.Msg("Button pressed!"); - * }); - */ - // _AddButtonInternal(id, text, callback) - // { - // if (this.content == null) - // { - // // Display warning - // $.Msg(`You must call AddToPanel() before adding buttons to a category!`); - // return; - // } - // // - - // // let button = $.CreatePanel("Button", this.content, `${id}_button`); - // // button.AddClass("ButtonTest"); - // // if (callback != null) - // // button.SetPanelEvent("onactivate", callback); - // let button = CreateDebugMenuButton(this.content, callback, "ButtonTest", `${this.id}_${id}`); - - // let buttonLabel = $.CreatePanel("Label", button, `${id}_label`); - // buttonLabel.AddClass("button_label"); - // buttonLabel.text = text; - - // let buttonBullet = $.CreatePanel("Image", button, `${id}_bullet`); - // buttonBullet.AddClass("button_bullet"); - // buttonBullet.SetImage("s2r://panorama/images/game_menu_ui/btn_bullet_child_page_png.vtex") - - // } - SetItemText(id, text) { - // let option = this.content.FindChildTraverse(`${this.id}_${id}`); - // if (option === null) - // { - // // Display warning - // $.Msg(`Option ${id} does not exist! Did you call AddToPanel()?`); - // return; - // } - - // option - // Find item with id in this.options let combinedId = `${this.id}_${id}`; let item = this.items.find(o => o.id === combinedId); if (item === undefined) { this.items.forEach((o) => $.Msg(o.id)); - // Display warning $.Msg(`Item ${id} does not exist!`); return; } @@ -212,11 +153,6 @@ class Category AddButton(id, text) { - // this._AddButtonInternal(id, text, () => { - // // $.DispatchEvent("ClientUI_FireOutputStr", 0, `_DebugMenuCallbackButton('${id}')`); - // FireOutput("_DebugMenuCallbackButton", id); - // }); - let button = new SubMenuButton(`${this.id}_${id}`, text, () => { FireOutput("_DebugMenuCallbackButton", id); }); @@ -224,19 +160,8 @@ class Category this.items.push(button); } - // _AddToggleInternal(id, text, callback, startsOn) - // { - // let toggle = new SubMenuToggle(id, text, callback, startsOn); - // toggle.AddToPanel(this.content); - // } - AddToggle(id, text, startsOn) { - // this._AddToggleInternal(id, text, startsOn, (on) => { - // // $.DispatchEvent("ClientUI_FireOutputStr", 0, `_DebugMenuCallbackToggle('${this.id}',${on})`); - // FireOutput("_DebugMenuCallbackToggle", id, on); - // }); - let toggle = new SubMenuToggle(`${this.id}_${id}`, text, startsOn, (on) => { FireOutput("_DebugMenuCallbackToggle", id, on); }); @@ -312,12 +237,6 @@ class SubMenuToggle { if (this.panel == null) { - // this.panel = $.CreatePanel("Button", panel, `${this.id}_button`); - // this.panel.AddClass("ButtonTest"); - // if (this.callback != null) - // this.panel.SetPanelEvent("onactivate", () => this.Toggle()); - // this.panel.SetPanelEvent("onactivate", this.callback); - this.panel = CreateDebugMenuButton(panel, () => this.Toggle(), "ButtonTest", this.id); this.panel.AddClass("custom_switch"); diff --git a/panorama/styles/custom_game/alyxlib_debug_menu.css b/panorama/styles/custom_game/alyxlib_debug_menu.css index b4069a7..bf672a6 100644 --- a/panorama/styles/custom_game/alyxlib_debug_menu.css +++ b/panorama/styles/custom_game/alyxlib_debug_menu.css @@ -212,11 +212,6 @@ Button.custom_switch:active text-align: right; } -.custom_switch.mini_option Label -{ - /*font-size: 70px;*/ -} - .custom_switch:hover { color: #fff; @@ -255,33 +250,3 @@ Button { font-size: 33px; } - - -.single_controller_on .custom_switch.single_controller:hover:not(.disabled) .switch_image, -.custom_switch:hover:not(.disabled) .switch_image -{ - background-image: url("s2r://panorama/images/game_menu_ui/component_switch_on_hover_png.vtex"); - brightness: 1.0; -} - -.single_controller_off .custom_switch.single_controller:hover:not(.disabled) .switch_image, -.custom_switch.switch_off:hover:not(.disabled) .switch_image -{ - background-image: url("s2r://panorama/images/game_menu_ui/component_switch_off_hover_png.vtex"); - brightness: 1.0; -} - - -.single_controller_off .custom_switch.single_controller:active:not(.disabled) .switch_image, -.custom_switch.switch_off:active:not(.disabled) .switch_image -{ - background-image: url("s2r://panorama/images/game_menu_ui/component_switch_off_press_png.vtex"); - brightness: 1.0; -} - -.single_controller_on .custom_switch.single_controller:active:not(.disabled) .switch_image, -.custom_switch:active:not(.disabled) .switch_image -{ - background-image: url("s2r://panorama/images/game_menu_ui/component_switch_on_press_png.vtex"); - brightness: 1.0; -} diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index f285aeb..4f179de 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -1,9 +1,19 @@ +--[[ + v1.0.0 + https://github.com/FrostSource/alyxlib + The debug menu allows for easier VR testing by offering a customizable in-game menu. +]] RegisterAlyxLibCommand("alyxlib_debug_menu_show", function (name, ...) DebugMenu:ShowMenu() -end, "", 0) +end, "Forces the debug menu to show") +RegisterAlyxLibConvar("alyxlib_debug_menu_hand", "1", "Hand to attach the debug menu to, 0 = Secondary : 1 = Primary") + +--- +---The debug menu allows for easier VR testing by offering a customizable in-game menu. +--- ---@class DebugMenu DebugMenu = {} @@ -20,19 +30,14 @@ DebugMenu = {} ---@field type "button"|"toggle"|"separator" ---@field default any +---The panel entity. ---@type CPointClientUIWorldPanel DebugMenu.panel = nil ---@type DebugMenuCategory[] DebugMenu.categories = {} --- ---All options added to all categories --- ---@type table --- DebugMenu.options = {} - --- local options = DebugMenu.options - -local buttonActivatethink = function() +local listenForMenuActivationThink = function() local buttonPressesToActivate = 5 local buttonPresses = 0 local timeToResetBetweenPresses = 0.6 @@ -68,6 +73,11 @@ local buttonActivatethink = function() end, 0) end +--- +---The scope of the debug menu script. +--- +---These functions handle Panorama callbacks. +--- local debugPanelScriptScope = { _DebugMenuCallbackButton = function(id) local item = DebugMenu:GetItem(id) @@ -110,8 +120,7 @@ local debugPanelScriptScope = { end, } - - +---Forces the debug menu panel to add all categories and items. local function updateDebugMenu() if not DebugMenu.panel then return @@ -136,12 +145,10 @@ local function updateDebugMenu() end end - +--- +---Creates and displays the debug menu panel on the player's chosen hand. +--- function DebugMenu:ShowMenu() - -- if spawnMenu ~= nil then - -- --return - -- HideMenu() - -- end local menu = SpawnEntityFromTableSynchronous("point_clientui_world_panel", { targetname = "spawnmenu", @@ -158,7 +165,6 @@ function DebugMenu:ShowMenu() horizontal_align = "1", }) - if not Player.HMDAvatar then local localPlayer = Entities:GetLocalPlayer() local eyePos = localPlayer:EyePosition() @@ -168,54 +174,22 @@ function DebugMenu:ShowMenu() menu:SetQAngle(a) menu:SetOrigin(eyePos + dir * 16) else - -- menu:SetParent(Player.HMDAvatar,"") - -- menu:SetLocalOrigin(Vector(16)) - -- menu:SetLocalAngles(0,-90,90) - -- menu:SetParent(nil, "") + local hand = Convars:GetInt("alyxlib_debug_menu_hand") == 1 and Player.PrimaryHand or Player.SecondaryHand - menu:SetParent(Player.LeftHand, "") + menu:SetParent(hand, "") menu:SetLocalAngles(40,-10,10) menu:SetLocalOrigin(Vector(0,8,-2)) - for _, child in ipairs(Player.HMDAvatar:GetChildrenMemSafe()) do - if child:GetModelName() == "models/props/handposes/handpose_cough.vmdl" then - child:EntFire("Disable") - end - end + -- Cough handpose gets in the way for close menus + Player:SetCoughHandEnabled(false) + + -- Handle distant button presses + Input:ListenToButton("press", InputHandPrimary, DIGITAL_INPUT_MENU_INTERACT, 1, function (params) + self:ClickHoveredButton() + end, self) - -- local collision = SpawnEntityFromTableSynchronous("func_clip_interaction_layer", { - -- targetname = "col", - -- InteractsWith = "LeftHand", - -- InteractsAs = "LeftHand", - -- -- model="models/alyxlib/debug_menu_collision2.vmdl", - -- model="models/alyxlib/debug_menu_collision.vmdl", - -- }) - - -- local collision2 = SpawnEntityFromTableSynchronous("prop_dynamic", { - -- targetname = "col1", - -- -- InteractsWith = "LeftHand", - -- -- InteractsAs = "LeftHand", - -- model="models/alyxlib/debug_menu_collision2.vmdl", - -- -- model="models/alyxlib/debug_menu_collision.vmdl", - -- }) - -- collision2:SetAbsScale(5) - -- -- collision:SetSize(Vector(-16, -16, -16), Vector(16, 16, 16)) - -- collision2:SetParent(menu, "") - -- collision2:ResetLocal() - -- collision2:SetLocalOrigin(Vector(0,0,2.7)) - - -- -- collision:SetModel("models/alyxlib/debug_menu_collision2.vmdl") - -- collision:SetAbsScale(5) - -- -- collision:SetSize(Vector(-16, -16, -16), Vector(16, 16, 16)) - -- collision:SetParent(menu, "") - -- collision:ResetLocal() - -- collision:SetLocalOrigin(Vector(0,0,2.7)) end - -- EntFireByHandle(thisEntity, menu, "AddOutput", "CustomOutput0>spawnmenu_script>RunScriptCode>>>") - -- for addon,loaded in pairs(loadedAddons) do - -- menu:AddCSSClasses("addon_"..addon) - -- end menu:AddCSSClasses("Visible") local scope = menu:GetOrCreatePrivateScriptScope() @@ -228,18 +202,22 @@ function DebugMenu:ShowMenu() updateDebugMenu() - Input:ListenToButton("press", InputHandPrimary, DIGITAL_INPUT_MENU_INTERACT, 1, function (params) - self:ClickHoveredButton() - end, self) - end +--- +---Clicks the active button on the debug menu panel (the one highlighted by the finger). +--- +---This is handled automatically in most cases. +--- function DebugMenu:ClickHoveredButton() if self.panel then Panorama:Send(self.panel, "ClickHoveredButton") end end +--- +---Closes the debug menu panel. +--- function DebugMenu:CloseMenu() if self.panel then self.panel:Kill() @@ -247,7 +225,9 @@ function DebugMenu:CloseMenu() Input:StopListeningByContext(self) - buttonActivatethink() + Player:SetCoughHandEnabled(true) + + listenForMenuActivationThink() end end @@ -381,7 +361,7 @@ end ListenToPlayerEvent("player_activate", function() Player:Delay(function() - buttonActivatethink() + listenForMenuActivationThink() end, 0.2) end) diff --git a/scripts/vscripts/alyxlib/player/core.lua b/scripts/vscripts/alyxlib/player/core.lua index 37ae577..0aa30ce 100644 --- a/scripts/vscripts/alyxlib/player/core.lua +++ b/scripts/vscripts/alyxlib/player/core.lua @@ -1038,6 +1038,27 @@ function CBasePlayer:SetAnchorOriginAroundPlayer(pos) self.HMDAnchor:SetAbsOrigin(pos + (self.HMDAnchor:GetAbsOrigin() - self:GetAbsOrigin())) end +--- +---Sets the enabled state of the cough handpose attached to the HMD avatar. +--- +---@param enabled boolean # True if the cough handpose should be enabled +function CBasePlayer:SetCoughHandEnabled(enabled) + if not self.HMDAvatar then + return + end + + for _, child in ipairs(self.HMDAvatar:GetChildrenMemSafe()) do + if child:GetModelName() == "models/props/handposes/handpose_cough.vmdl" then + if enabled then + child:EntFire("Enable") + else + child:EntFire("Disable") + end + break + end + end +end + -- Other player libraries require "alyxlib.player.hands" require "alyxlib.player.events" From 363243f9e4a0f9584e23c2a1ef0f6c9a9b0c5e4c Mon Sep 17 00:00:00 2001 From: FrostSource Date: Wed, 9 Apr 2025 14:13:40 +1200 Subject: [PATCH 013/101] Update debug menu Add text changing. Add hot reload refresh. Restructure script. --- .../scripts/custom_game/alyxlib_debug_menu.js | 20 ++ scripts/vscripts/alyxlib/debug/debug_menu.lua | 284 ++++++++++++------ 2 files changed, 218 insertions(+), 86 deletions(-) diff --git a/panorama/scripts/custom_game/alyxlib_debug_menu.js b/panorama/scripts/custom_game/alyxlib_debug_menu.js index 21825df..23e7ac2 100644 --- a/panorama/scripts/custom_game/alyxlib_debug_menu.js +++ b/panorama/scripts/custom_game/alyxlib_debug_menu.js @@ -111,6 +111,15 @@ class Category label.text = this.name; } + /** + * Deletes this category and all of its items. + */ + Delete() + { + this.panel.DeleteAsync(0); + this.button.DeleteAsync(0); + } + SetVisible(visible) { if (visible) @@ -510,6 +519,12 @@ function ParseCommand(command, args) ClickHoveredButton(); break; } + + case "removeallcategories": { + categories.forEach((category) => category.Delete()); + categories = []; + break; + } } } @@ -519,4 +534,9 @@ function ParseCommand(command, args) TurnButtonIntoDebugMenuButton($("#CloseMenuButton")); TurnButtonIntoDebugMenuButton($("#CycleCategoryLeftButton")); TurnButtonIntoDebugMenuButton($("#CycleCategoryRightButton")); + + // Tells Lua that the menu has been reloaded so it can repopulate the menu + // This helps with hot reloading panel changes + $.Schedule(0.1, () => FireOutput("_DebugMenuReloaded")); + })(); \ No newline at end of file diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index 4f179de..e9aefaa 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -37,41 +37,7 @@ DebugMenu.panel = nil ---@type DebugMenuCategory[] DebugMenu.categories = {} -local listenForMenuActivationThink = function() - local buttonPressesToActivate = 5 - local buttonPresses = 0 - local timeToResetBetweenPresses = 0.6 - local buttonPressed = false - local timeSinceLastButtonPress = 0 - - Player:SetContextThink("debug_menu_activate", function() - if Time() - timeSinceLastButtonPress > timeToResetBetweenPresses then - buttonPresses = 0 - timeSinceLastButtonPress = math.huge - print("RESET") - end - - if Player:IsDigitalActionOnForHand(Player.SecondaryHand.Literal, DIGITAL_INPUT_TOGGLE_MENU) then - if not buttonPressed then - buttonPressed = true - timeSinceLastButtonPress = Time() - buttonPresses = buttonPresses + 1 - - print(buttonPresses) - if buttonPresses >= buttonPressesToActivate then - DebugMenu:ShowMenu() - buttonPresses = 0 - return nil - end - end - else - if buttonPressed then - buttonPressed = false - end - end - return 0 - end, 0) -end +local debugMenuOpen = false --- ---The scope of the debug menu script. @@ -118,32 +84,13 @@ local debugPanelScriptScope = { _CloseMenu = function() DebugMenu:CloseMenu() end, -} - ----Forces the debug menu panel to add all categories and items. -local function updateDebugMenu() - if not DebugMenu.panel then - return - end - local panel = DebugMenu.panel - - for categoryId, category in pairs(DebugMenu.categories) do - Panorama:Send(panel, "AddCategory", category.id, category.name) - - for _, item in ipairs(category.items) do - if item.type == "toggle" then - Panorama:Send(panel, "AddToggle", item.categoryId, item.id, item.text, item.default) - elseif item.type == "button" then - Panorama:Send(panel, "AddButton", item.categoryId, item.id, item.text) - elseif item.type == "separator" then - Panorama:Send(panel, "AddSeparator", item.categoryId) - else - warn("Unknown item type '"..item.type.."'") - end + _DebugMenuReloaded = function() + if DebugMenu:IsOpen() then + DebugMenu:Refresh() end end -end +} --- ---Creates and displays the debug menu panel on the player's chosen hand. @@ -151,7 +98,7 @@ end function DebugMenu:ShowMenu() local menu = SpawnEntityFromTableSynchronous("point_clientui_world_panel", { - targetname = "spawnmenu", + targetname = "alyxlib_debug_menu", dialog_layout_name = "file://{resources}/layout/custom_game/alyxlib_debug_menu.xml", width = 16,--24, height = 12,--16 @@ -174,19 +121,33 @@ function DebugMenu:ShowMenu() menu:SetQAngle(a) menu:SetOrigin(eyePos + dir * 16) else - local hand = Convars:GetInt("alyxlib_debug_menu_hand") == 1 and Player.PrimaryHand or Player.SecondaryHand - - menu:SetParent(hand, "") - menu:SetLocalAngles(40,-10,10) - menu:SetLocalOrigin(Vector(0,8,-2)) + if Convars:GetInt("alyxlib_debug_menu_hand") == 1 then + menu:SetParent(Player.PrimaryHand, "constraint1") + menu:ResetLocal() + menu:SetLocalAngles(0, 180, 0) + menu:SetLocalOrigin(Vector(4, -9, 0)) + -- menu:SetLocalAngles(0,0,0) + -- menu:SetLocalAngles(40,-10,10) + -- menu:SetLocalOrigin(Vector(0,8,-2)) + else + menu:SetParent(Player.SecondaryHand, "constraint1") + menu:ResetLocal() + menu:SetLocalAngles(0, 0, 0) + menu:SetLocalOrigin(Vector(4, 9, 0)) + -- menu:SetLocalAngles(40,-10,10) + -- menu:SetLocalOrigin(Vector(0,8,-2)) + end -- Cough handpose gets in the way for close menus Player:SetCoughHandEnabled(false) -- Handle distant button presses - Input:ListenToButton("press", InputHandPrimary, DIGITAL_INPUT_MENU_INTERACT, 1, function (params) - self:ClickHoveredButton() - end, self) + Input:ListenToButton("press", + Convars:GetInt("alyxlib_debug_menu_hand") == 1 and InputHandSecondary or InputHandPrimary, + DIGITAL_INPUT_MENU_INTERACT, 1, + function (params) + self:ClickHoveredButton() + end, self) end @@ -200,19 +161,11 @@ function DebugMenu:ShowMenu() Panorama:InitPanel(menu, "alyxlib_debug_menu") self.panel = menu - updateDebugMenu() - -end + menu:Delay(function() + debugMenuOpen = true + end, 0.2) ---- ----Clicks the active button on the debug menu panel (the one highlighted by the finger). ---- ----This is handled automatically in most cases. ---- -function DebugMenu:ClickHoveredButton() - if self.panel then - Panorama:Send(self.panel, "ClickHoveredButton") - end + self:SendCategoriesToPanel() end --- @@ -223,17 +176,40 @@ function DebugMenu:CloseMenu() self.panel:Kill() self.panel = nil + debugMenuOpen = false + Input:StopListeningByContext(self) Player:SetCoughHandEnabled(true) - listenForMenuActivationThink() + self:StartListeningForMenuActivation() + end +end + +--- +---Returns whether the debug menu is currently open. +--- +---@return boolean +function DebugMenu:IsOpen() + return self.panel ~= nil and debugMenuOpen +end + +--- +---Clicks the active button on the debug menu panel (the one highlighted by the finger). +--- +---This is handled automatically in most cases. +--- +function DebugMenu:ClickHoveredButton() + if self.panel then + Panorama:Send(self.panel, "ClickHoveredButton") end end +--- ---Get a debug menu item by id. ----@param id string ----@return DebugMenuItem? +--- +---@param id string # The item ID +---@return DebugMenuItem? # The item if it exists function DebugMenu:GetItem(id) for _, category in ipairs(self.categories) do for _, item in ipairs(category.items) do @@ -244,9 +220,11 @@ function DebugMenu:GetItem(id) end end +--- ---Get a debug menu category by id. ----@param id string ----@return DebugMenuCategory? +--- +---@param id string # The category ID +---@return DebugMenuCategory? # The category if it exists function DebugMenu:GetCategory(id) for _, category in ipairs(self.categories) do if category.id == id then @@ -255,6 +233,11 @@ function DebugMenu:GetCategory(id) end end +--- +---Add a category to the debug menu. +--- +---@param id string # The unique ID for this category +---@param name string # The display name for this category function DebugMenu:AddCategory(id, name) if self:GetCategory(id) then warn("Category '"..id.."' already exists!") @@ -268,7 +251,9 @@ function DebugMenu:AddCategory(id, name) }) end +--- ---Add a separator line to a category. +--- ---@param categoryId string # The category ID to add the separator to function DebugMenu:AddSeparator(categoryId) local category = self:GetCategory(categoryId) @@ -283,7 +268,9 @@ function DebugMenu:AddSeparator(categoryId) }) end +--- ---Add a button to a category. +--- ---@param categoryId string # The category ID to add the button to ---@param buttonId string # The unique ID for this button ---@param text string # The text to display on this button @@ -313,7 +300,9 @@ function DebugMenu:AddButton(categoryId, buttonId, text, command) }) end +--- ---Add a toggle to a category. +--- ---@param categoryId string # The category ID to add the toggle to ---@param toggleId string # The unique ID for this toggle ---@param text string # The text to display on this toggle @@ -345,6 +334,12 @@ function DebugMenu:AddToggle(categoryId, toggleId, text, command, startsOn) }) end +--- +---Set the text of an item. +--- +---@param categoryId string # The category ID +---@param itemId any # The item ID +---@param text any # The new text function DebugMenu:SetItemText(categoryId, itemId, text) local item = self:GetItem(itemId) if not item then @@ -359,9 +354,101 @@ function DebugMenu:SetItemText(categoryId, itemId, text) end end +--- +---Forces the debug menu panel to add all categories and items. +--- +---This should only be used if modifying the menu in a non-standard way. +--- +function DebugMenu:SendCategoriesToPanel() + if not self.panel then + return + end + + local panel = self.panel + + for categoryId, category in pairs(DebugMenu.categories) do + Panorama:Send(panel, "AddCategory", category.id, category.name) + + for _, item in ipairs(category.items) do + if item.type == "toggle" then + Panorama:Send(panel, "AddToggle", item.categoryId, item.id, item.text, item.default) + elseif item.type == "button" then + Panorama:Send(panel, "AddButton", item.categoryId, item.id, item.text) + elseif item.type == "separator" then + Panorama:Send(panel, "AddSeparator", item.categoryId) + else + warn("Unknown item type '"..item.type.."'") + end + end + end +end + +--- +---Clears all categories and items from the debug menu panel. +--- +function DebugMenu:ClearMenu() + if not self.panel then + return + end + + Panorama:Send(self.panel, "RemoveAllCategories") +end + +--- +---Forces the debug menu panel to refresh by removing and re-adding all categories and items. +--- +function DebugMenu:Refresh() + if self.panel then + self:ClearMenu() + self:SendCategoriesToPanel() + end +end + +--- +---Starts listening for the debug menu activation button. +--- +function DebugMenu:StartListeningForMenuActivation() + local buttonPressesToActivate = 5 + local buttonPresses = 0 + local timeToResetBetweenPresses = 0.6 + local buttonPressed = false + local timeSinceLastButtonPress = 0 + + Player:SetContextThink("debug_menu_activate", function() + if Time() - timeSinceLastButtonPress > timeToResetBetweenPresses then + buttonPresses = 0 + timeSinceLastButtonPress = math.huge + end + + if Player:IsDigitalActionOnForHand(Player.SecondaryHand.Literal, DIGITAL_INPUT_TOGGLE_MENU) then + if not buttonPressed then + buttonPressed = true + timeSinceLastButtonPress = Time() + buttonPresses = buttonPresses + 1 + + if buttonPresses >= buttonPressesToActivate then + self:ShowMenu() + buttonPresses = 0 + -- Stop think + return nil + end + end + else + if buttonPressed then + buttonPressed = false + end + end + return 0 + end, 0) +end + +function DebugMenu:StopListeningForMenuActivation() + Player:SetContextThink("debug_menu_activate", nil, 0) +end + ListenToPlayerEvent("player_activate", function() Player:Delay(function() - listenForMenuActivationThink() + DebugMenu:StartListeningForMenuActivation() end, 0.2) end) @@ -371,4 +458,29 @@ DebugMenu:AddCategory("alyxlib", "AlyxLib") DebugMenu:AddToggle("alyxlib", "alyxlib_noclip_vr", "NoClip VR", "noclip_vr") -DebugMenu:AddToggle("alyxlib", "alyxlib_godmode", "God Mode", "god") \ No newline at end of file +DebugMenu:AddToggle("alyxlib", "alyxlib_godmode", "God Mode", "god") + +local isRecordingDemo = false +local currentDemo = "" + +DebugMenu:AddButton("alyxlib", "alyxlib_demo_recording", "Start Recording Demo", function() + if isRecordingDemo then + SendToConsole("stop") + currentDemo = "" + isRecordingDemo = false + DebugMenu:SetItemText("alyxlib", "alyxlib_demo_recording", "Start Recording Demo") + -- Panorama:Send(DebugMenu.panel, "SetItemText", "alyxlib", "alyxlib_demo_recording", "Start Recording Demo") + else + -- Panorama:Send(DebugMenu.panel, "SetItemText", "alyxlib", "alyxlib_demo_recording", "Stop Recording Demo") + local localtime = LocalTime() + -- remove all whitespace and slashes` + local sanitizedMap = GetMapName():gsub("%s+", ""):gsub("/", "_") + currentDemo = "demo_" .. sanitizedMap .. "_" .. localtime.Hours .. "-" .. localtime.Minutes .. "-" .. localtime.Seconds + SendToConsole("record " .. currentDemo) + isRecordingDemo = true + DebugMenu:SetItemText("alyxlib", "alyxlib_demo_recording", "Stop Recording Demo") + -- Player:Delay(function() + -- DebugMenu:Refresh() + -- end, 0.5) + end +end) \ No newline at end of file From 8afb985f7aae26b6fafc7d4da855b76899d6492a Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 10 Jun 2025 01:09:08 +1200 Subject: [PATCH 014/101] Fix toggle state not saving --- scripts/vscripts/alyxlib/debug/debug_menu.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index e9aefaa..205eff8 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -77,7 +77,7 @@ local debugPanelScriptScope = { if item then item.callback(on) -- Hack for keeping state after close - item.startsOn = on + item.default = on end end, From d535a2febc060b8ee860cfd1cce0149a8ba2ee3e Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 10 Jun 2025 01:42:08 +1200 Subject: [PATCH 015/101] Add text labels --- .../scripts/custom_game/alyxlib_debug_menu.js | 23 +++++++++++++++++ .../styles/custom_game/alyxlib_debug_menu.css | 10 ++++++++ scripts/vscripts/alyxlib/debug/debug_menu.lua | 25 ++++++++++++++++++- 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/panorama/scripts/custom_game/alyxlib_debug_menu.js b/panorama/scripts/custom_game/alyxlib_debug_menu.js index 23e7ac2..4af8d92 100644 --- a/panorama/scripts/custom_game/alyxlib_debug_menu.js +++ b/panorama/scripts/custom_game/alyxlib_debug_menu.js @@ -178,6 +178,15 @@ class Category this.items.push(toggle); } + AddLabel(id, text) + { + let label = $.CreatePanel("Label", this.content, `${this.id}_${id}`); + label.AddClass("custom_label"); + label.text = text; + + this.items.push(label); + } + AddSeparator() { let rowDivider = $.CreatePanel("Panel", this.content, undefined); @@ -489,6 +498,20 @@ function ParseCommand(command, args) break; } + case "addlabel": { + let category = GetCategory(args[0]); + if (category === null) + { + $.Msg(`Category ${args[0]} does not exist!`); + break; + } + + let labelId = args[1]; + let labelText = args[2] || args[1]; + category.AddLabel(labelId, labelText); + break; + } + case "addseparator": { let category = GetCategory(args[0]); if (category === null) diff --git a/panorama/styles/custom_game/alyxlib_debug_menu.css b/panorama/styles/custom_game/alyxlib_debug_menu.css index bf672a6..21c4e26 100644 --- a/panorama/styles/custom_game/alyxlib_debug_menu.css +++ b/panorama/styles/custom_game/alyxlib_debug_menu.css @@ -250,3 +250,13 @@ Button { font-size: 33px; } + +Label.custom_label +{ + text-align: center; + horizontal-align: center; + font-size: 30px; + padding-left: 24px; + padding-right: 24px; + +} diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index 205eff8..4976e44 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -334,6 +334,27 @@ function DebugMenu:AddToggle(categoryId, toggleId, text, command, startsOn) }) end +--- +---Add a center aligned label to a category. +--- +---@param categoryId string # The category ID to add the label to +---@param labelId string # The unique ID for this label +---@param text string # The text to display on this label +function DebugMenu:AddLabel(categoryId, labelId, text) + local category = self:GetCategory(categoryId) + if not category then + warn("Cannot add label '"..labelId.."': Category '"..categoryId.."' does not exist!") + return + end + + table.insert(category.items, { + categoryId = categoryId, + id = labelId, + text = text, + type = "label", + }) +end + --- ---Set the text of an item. --- @@ -374,6 +395,8 @@ function DebugMenu:SendCategoriesToPanel() Panorama:Send(panel, "AddToggle", item.categoryId, item.id, item.text, item.default) elseif item.type == "button" then Panorama:Send(panel, "AddButton", item.categoryId, item.id, item.text) + elseif item.type == "label" then + Panorama:Send(panel, "AddLabel", item.categoryId, item.id, item.text) elseif item.type == "separator" then Panorama:Send(panel, "AddSeparator", item.categoryId) else @@ -446,7 +469,7 @@ function DebugMenu:StopListeningForMenuActivation() Player:SetContextThink("debug_menu_activate", nil, 0) end -ListenToPlayerEvent("player_activate", function() +ListenToPlayerEvent("vr_player_ready", function() Player:Delay(function() DebugMenu:StartListeningForMenuActivation() end, 0.2) From 6df1904192b551c4152dfcda4d60e11f77494046 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 10 Jun 2025 01:48:46 +1200 Subject: [PATCH 016/101] Add custom category ordering --- scripts/vscripts/alyxlib/debug/debug_menu.lua | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index 4976e44..61b0bbf 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -225,10 +225,11 @@ end --- ---@param id string # The category ID ---@return DebugMenuCategory? # The category if it exists +---@return number? # The index of the category in the categories table function DebugMenu:GetCategory(id) - for _, category in ipairs(self.categories) do + for index, category in ipairs(self.categories) do if category.id == id then - return category + return category, index end end end @@ -375,6 +376,27 @@ function DebugMenu:SetItemText(categoryId, itemId, text) end end +--- +---Sets the index of a category in the debug menu. +---Categories are ordered by their index, starting from 1. +--- +---This is an advanced function and should be used with caution. +--- +---@param categoryId any +---@param index any +function DebugMenu:SetCategoryIndex(categoryId, index) + local category, currentIndex = self:GetCategory(categoryId) + if not category then + warn("Cannot set category index '"..categoryId.."': Category does not exist!") + return + end + + index = math.max(1, math.min(index, #self.categories)) -- Clamp index to valid range + + table.remove(self.categories, currentIndex) + table.insert(self.categories, index, category) +end + --- ---Forces the debug menu panel to add all categories and items. --- From e264645e60c5a24fe9b61776f8d1d7b2be05b146 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 10 Jun 2025 01:49:36 +1200 Subject: [PATCH 017/101] Cleanup --- scripts/vscripts/alyxlib/debug/debug_menu.lua | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index 61b0bbf..4f2805c 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -514,18 +514,13 @@ DebugMenu:AddButton("alyxlib", "alyxlib_demo_recording", "Start Recording Demo", currentDemo = "" isRecordingDemo = false DebugMenu:SetItemText("alyxlib", "alyxlib_demo_recording", "Start Recording Demo") - -- Panorama:Send(DebugMenu.panel, "SetItemText", "alyxlib", "alyxlib_demo_recording", "Start Recording Demo") else - -- Panorama:Send(DebugMenu.panel, "SetItemText", "alyxlib", "alyxlib_demo_recording", "Stop Recording Demo") local localtime = LocalTime() - -- remove all whitespace and slashes` + -- remove all whitespace and slashes local sanitizedMap = GetMapName():gsub("%s+", ""):gsub("/", "_") currentDemo = "demo_" .. sanitizedMap .. "_" .. localtime.Hours .. "-" .. localtime.Minutes .. "-" .. localtime.Seconds SendToConsole("record " .. currentDemo) isRecordingDemo = true DebugMenu:SetItemText("alyxlib", "alyxlib_demo_recording", "Stop Recording Demo") - -- Player:Delay(function() - -- DebugMenu:Refresh() - -- end, 0.5) end end) \ No newline at end of file From aa27a8e19fef04e610cba5cc3b6f68bd33ec108d Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 10 Jun 2025 01:51:29 +1200 Subject: [PATCH 018/101] Only listen for debug menu on -dev --- scripts/vscripts/alyxlib/debug/debug_menu.lua | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index 4f2805c..8bb5a31 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -491,11 +491,13 @@ function DebugMenu:StopListeningForMenuActivation() Player:SetContextThink("debug_menu_activate", nil, 0) end -ListenToPlayerEvent("vr_player_ready", function() - Player:Delay(function() - DebugMenu:StartListeningForMenuActivation() - end, 0.2) -end) +if Convars:GetInt("developer") > 0 then + ListenToPlayerEvent("vr_player_ready", function() + Player:Delay(function() + DebugMenu:StartListeningForMenuActivation() + end, 0.2) + end) +end -- AlyxLib defaults From 7d0ca5bd8389b333142e7ab5df6f439408a1de9b Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 10 Jun 2025 01:53:36 +1200 Subject: [PATCH 019/101] Check weapon validity on update --- scripts/vscripts/alyxlib/player/core.lua | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/scripts/vscripts/alyxlib/player/core.lua b/scripts/vscripts/alyxlib/player/core.lua index 37ae577..5876e26 100644 --- a/scripts/vscripts/alyxlib/player/core.lua +++ b/scripts/vscripts/alyxlib/player/core.lua @@ -957,11 +957,16 @@ function CBasePlayer:UpdateWeaponsExistence() end end - for i = #weapons.genericpistols, 1, -1 do - local generic = weapons.genericpistols[i] - local swt = Entities:FindByName(nil, "wpnswitch_" .. generic:GetName()) - if not swt then - table.remove(weapons.genericpistols, i) + if #weapons.genericpistols > 0 then + for i = #weapons.genericpistols, 1, -1 do + local generic = weapons.genericpistols[i] + -- Server change seems to destroy weapons before this is run + if IsValidEntity(generic) then + local swt = Entities:FindByName(nil, "wpnswitch_" .. generic:GetName()) + if not swt then + table.remove(weapons.genericpistols, i) + end + end end end end From ab496cd157b2356b9eb378a3db8f4ee2ed2fec98 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 10 Jun 2025 01:57:31 +1200 Subject: [PATCH 020/101] Add GetAttachmentNameForward --- scripts/vscripts/alyxlib/extensions/entity.lua | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scripts/vscripts/alyxlib/extensions/entity.lua b/scripts/vscripts/alyxlib/extensions/entity.lua index 90c93dc..56baaf3 100644 --- a/scripts/vscripts/alyxlib/extensions/entity.lua +++ b/scripts/vscripts/alyxlib/extensions/entity.lua @@ -443,4 +443,13 @@ function CBaseAnimating:GetAttachmentNameAngles(name) return self:GetAttachmentAngles(self:ScriptLookupAttachment(name)) end +--- +---Gets the forward vector of a named attachment. +--- +---@param name string +---@return Vector +function CBaseAnimating:GetAttachmentNameForward(name) + return self:GetAttachmentForward(self:ScriptLookupAttachment(name)) +end + return version \ No newline at end of file From f1a2266662fa77b1a3c035c82eea95740f4f9a4f Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 10 Jun 2025 01:58:00 +1200 Subject: [PATCH 021/101] Check null in Debug.EntStr --- scripts/vscripts/alyxlib/debug/common.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/vscripts/alyxlib/debug/common.lua b/scripts/vscripts/alyxlib/debug/common.lua index 9858617..c624e36 100644 --- a/scripts/vscripts/alyxlib/debug/common.lua +++ b/scripts/vscripts/alyxlib/debug/common.lua @@ -796,6 +796,10 @@ function Debug.EntStr(ent) return "[nil, nil]" end + if ent:IsNull() then + return "[invalid, invalid]" + end + return "[" .. ent:GetClassname() .. ", " .. ent:GetName() .. "]" end From 07f8dfef3cc8c2030bff7ee6192ad67fbdbf80d7 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 10 Jun 2025 01:58:39 +1200 Subject: [PATCH 022/101] Check hand validity in haptics --- scripts/vscripts/alyxlib/controls/haptics.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/vscripts/alyxlib/controls/haptics.lua b/scripts/vscripts/alyxlib/controls/haptics.lua index 1e734e0..a684089 100644 --- a/scripts/vscripts/alyxlib/controls/haptics.lua +++ b/scripts/vscripts/alyxlib/controls/haptics.lua @@ -39,6 +39,11 @@ function HapticSequenceClass:Fire(hand) hand = Entities:GetLocalPlayer():GetHMDAvatar():GetVRHand(hand) end + if not IsValidEntity(hand) then + warn("Invalid hand entity for haptic sequence!") + return + end + local ref = { increment = 0, prevTime = Time(), From 574b10c925dcdc99521c22f17955855f1a4613c5 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 10 Jun 2025 02:07:21 +1200 Subject: [PATCH 023/101] Check entity validity in thinks Entity can be null in some cases of destruction while code is still running. --- scripts/vscripts/alyxlib/class.lua | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/vscripts/alyxlib/class.lua b/scripts/vscripts/alyxlib/class.lua index 41579f9..8a91d11 100644 --- a/scripts/vscripts/alyxlib/class.lua +++ b/scripts/vscripts/alyxlib/class.lua @@ -469,15 +469,19 @@ end ---Resume the entity think function. ---@luadoc-ignore function EntityClass:ResumeThink() - self:SetContextThink("__EntityThink", function() return self:Think() end, 0) - self.IsThinking = true + if not self:IsNull() then + self:SetContextThink("__EntityThink", function() return self:Think() end, 0) + self.IsThinking = true + end end ---Pause the entity think function. ---@luadoc-ignore function EntityClass:PauseThink() - self:SetContextThink("__EntityThink", nil, 0) - self.IsThinking = false + if not self:IsNull() then + self:SetContextThink("__EntityThink", nil, 0) + self.IsThinking = false + end end ---Define a function to redirected to `output` on spawn. From ef75ebadacc61113e40fcef7f67eeba3c1db01a6 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 10 Jun 2025 12:52:59 +1200 Subject: [PATCH 024/101] Add GetChild function --- .../vscripts/alyxlib/extensions/entity.lua | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/scripts/vscripts/alyxlib/extensions/entity.lua b/scripts/vscripts/alyxlib/extensions/entity.lua index 56baaf3..b2d5619 100644 --- a/scripts/vscripts/alyxlib/extensions/entity.lua +++ b/scripts/vscripts/alyxlib/extensions/entity.lua @@ -45,6 +45,52 @@ function CBaseEntity:GetTopChildren() return children end +--- +---Get the first child in the hierarchy that has targetname or classname. +--- +---@param name string # The name or classname to look for, supports wildcard '*' +---@return EntityHandle? +function CBaseEntity:GetChild(name) + local usePattern = name:find("%*") ~= nil + local pattern + + ---@TODO Consider moving wildcard logic to a utility function + if usePattern then + -- Escape pattern special characters, then replace '*' with '.*' + pattern = "^" .. name + :gsub("([%^%$%(%)%%%.%[%]%+%-%?])", "%%%1") + :gsub("%*", ".*") .. "$" + end + + local child = self:FirstMoveChild() + while IsValidEntity(child) do + local childName = child:GetName() + local className = child:GetClassname() + + local match = false + if usePattern then + match = childName:match(pattern) or className:match(pattern) + else + match = childName == name or className == name + end + + if match then + return child + end + + -- Check children recursively + local result = child:GetChild(name) + if result then + return result + end + + child = child:NextMovePeer() + end + + return nil +end + + --- ---Send an input to this entity. --- From c99b62a895f3523929df6ee9c9366ce1f3920b1f Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 10 Jun 2025 12:53:11 +1200 Subject: [PATCH 025/101] Add alias for Debug.EntStr as entstr --- scripts/vscripts/alyxlib/debug/common.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/vscripts/alyxlib/debug/common.lua b/scripts/vscripts/alyxlib/debug/common.lua index c624e36..f878861 100644 --- a/scripts/vscripts/alyxlib/debug/common.lua +++ b/scripts/vscripts/alyxlib/debug/common.lua @@ -803,6 +803,9 @@ function Debug.EntStr(ent) return "[" .. ent:GetClassname() .. ", " .. ent:GetName() .. "]" end +---@diagnostic disable-next-line: lowercase-global +entstr = Debug.EntStr + --- ---Dumps a list of convars and their values to the console. --- From 67e6ef5578b9f38b129972f347d15c48af096eb9 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 10 Jun 2025 16:33:15 +1200 Subject: [PATCH 026/101] Update version numbers --- scripts/vscripts/alyxlib/class.lua | 4 ++-- scripts/vscripts/alyxlib/controls/haptics.lua | 4 ++-- scripts/vscripts/alyxlib/controls/input.lua | 4 ++-- scripts/vscripts/alyxlib/debug/commands.lua | 4 ++-- scripts/vscripts/alyxlib/debug/common.lua | 4 ++-- scripts/vscripts/alyxlib/debug/controller.lua | 4 ++-- scripts/vscripts/alyxlib/extensions/entity.lua | 4 ++-- scripts/vscripts/alyxlib/globals.lua | 4 ++-- scripts/vscripts/alyxlib/player/core.lua | 4 ++-- scripts/vscripts/alyxlib/player/events.lua | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/scripts/vscripts/alyxlib/class.lua b/scripts/vscripts/alyxlib/class.lua index 8a91d11..2ddbd3d 100644 --- a/scripts/vscripts/alyxlib/class.lua +++ b/scripts/vscripts/alyxlib/class.lua @@ -1,5 +1,5 @@ --[[ - v2.3.0 + v2.3.1 https://github.com/FrostSource/alyxlib If not using `vscripts/alyxlib/core.lua`, load this file at game start using the following line: @@ -74,7 +74,7 @@ end ``` ]] -local version = "v2.3.0" +local version = "v2.3.1" require "alyxlib.storage" require "alyxlib.globals" diff --git a/scripts/vscripts/alyxlib/controls/haptics.lua b/scripts/vscripts/alyxlib/controls/haptics.lua index a684089..31022b5 100644 --- a/scripts/vscripts/alyxlib/controls/haptics.lua +++ b/scripts/vscripts/alyxlib/controls/haptics.lua @@ -1,5 +1,5 @@ --[[ - v1.0.1 + v1.0.2 https://github.com/FrostSource/alyxlib Haptic sequences allow for more complex vibrations than the one-shot pulses that the base API provides. @@ -28,7 +28,7 @@ local HapticSequenceClass = { pulseWidth_us = 0, } HapticSequenceClass.__index = HapticSequenceClass -HapticSequenceClass.version = "v1.0.1" +HapticSequenceClass.version = "v1.0.2" --- ---Start the haptic sequence on a given hand. diff --git a/scripts/vscripts/alyxlib/controls/input.lua b/scripts/vscripts/alyxlib/controls/input.lua index 00826fc..3a6d7d5 100644 --- a/scripts/vscripts/alyxlib/controls/input.lua +++ b/scripts/vscripts/alyxlib/controls/input.lua @@ -1,5 +1,5 @@ --[[ - v4.0.1 + v4.0.2 https://github.com/FrostSource/alyxlib Simplifies the tracking of digital action presses/releases and analog values. @@ -15,7 +15,7 @@ ---@class Input Input = {} Input.__index = Input -Input.version = "v4.0.1" +Input.version = "v4.0.2" --- ---If the input system should start automatically on player spawn. diff --git a/scripts/vscripts/alyxlib/debug/commands.lua b/scripts/vscripts/alyxlib/debug/commands.lua index 19b3661..fe7a812 100644 --- a/scripts/vscripts/alyxlib/debug/commands.lua +++ b/scripts/vscripts/alyxlib/debug/commands.lua @@ -1,5 +1,5 @@ --[[ - v1.1.0 + v1.1.1 https://github.com/FrostSource/alyxlib If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: @@ -7,7 +7,7 @@ require "alyxlib.debug.commands" ]] -local version = "v1.1.0" +local version = "v1.1.1" local alyxlibCommands = {} diff --git a/scripts/vscripts/alyxlib/debug/common.lua b/scripts/vscripts/alyxlib/debug/common.lua index f878861..bb61c5a 100644 --- a/scripts/vscripts/alyxlib/debug/common.lua +++ b/scripts/vscripts/alyxlib/debug/common.lua @@ -1,5 +1,5 @@ --[[ - v2.1.0 + v2.2.0 https://github.com/FrostSource/alyxlib Debug utility functions. @@ -13,7 +13,7 @@ require "alyxlib.extensions.entity" require "alyxlib.math.common" Debug = {} -Debug.version = "v2.1.0" +Debug.version = "v2.2.0" --- ---Finds the first entity whose name, class or model matches `pattern`. diff --git a/scripts/vscripts/alyxlib/debug/controller.lua b/scripts/vscripts/alyxlib/debug/controller.lua index 6f33622..580ef58 100644 --- a/scripts/vscripts/alyxlib/debug/controller.lua +++ b/scripts/vscripts/alyxlib/debug/controller.lua @@ -1,5 +1,5 @@ --[[ - v1.0.1 + v1.0.2 https://github.com/FrostSource/alyxlib Allows quick debugging of VR controllers. @@ -11,7 +11,7 @@ -- Used for button descriptions require "alyxlib.controls.input" -local version = "v1.0.1" +local version = "v1.0.2" Convars:RegisterCommand("alyxlib_start_print_controller_button_presses", function (_) diff --git a/scripts/vscripts/alyxlib/extensions/entity.lua b/scripts/vscripts/alyxlib/extensions/entity.lua index b2d5619..c94ea6b 100644 --- a/scripts/vscripts/alyxlib/extensions/entity.lua +++ b/scripts/vscripts/alyxlib/extensions/entity.lua @@ -1,5 +1,5 @@ --[[ - v2.6.1 + v2.7.0 https://github.com/FrostSource/alyxlib Provides base entity extension methods. @@ -9,7 +9,7 @@ require "alyxlib.extensions.entity" ]] -local version = "v2.6.1" +local version = "v2.7.0" --- ---Get the entities parented to this entity. Including children of children. diff --git a/scripts/vscripts/alyxlib/globals.lua b/scripts/vscripts/alyxlib/globals.lua index f9c14d0..c7acdaa 100644 --- a/scripts/vscripts/alyxlib/globals.lua +++ b/scripts/vscripts/alyxlib/globals.lua @@ -1,5 +1,5 @@ --[[ - v2.6.0 + v2.7.0 https://github.com/FrostSource/alyxlib Provides common global functions used throughout extravaganza libraries. @@ -14,7 +14,7 @@ -- These are expected by globals require 'alyxlib.utils.common' -local _version = "v2.6.0" +local _version = "v2.7.0" --- ---A registered AlyxLib addon. diff --git a/scripts/vscripts/alyxlib/player/core.lua b/scripts/vscripts/alyxlib/player/core.lua index 5876e26..a043d74 100644 --- a/scripts/vscripts/alyxlib/player/core.lua +++ b/scripts/vscripts/alyxlib/player/core.lua @@ -1,5 +1,5 @@ --[[ - v4.2.0 + v4.2.1 https://github.com/FrostSource/alyxlib Player script allows for more advanced player manipulation and easier @@ -100,7 +100,7 @@ require "alyxlib.globals" require "alyxlib.extensions.entity" require "alyxlib.storage" -local version = "v4.2.0" +local version = "v4.2.1" ----------------------------- -- Class extension members -- diff --git a/scripts/vscripts/alyxlib/player/events.lua b/scripts/vscripts/alyxlib/player/events.lua index e16ab7a..86c21c5 100644 --- a/scripts/vscripts/alyxlib/player/events.lua +++ b/scripts/vscripts/alyxlib/player/events.lua @@ -1,11 +1,11 @@ --[[ - v2.1.3 + v2.2.0 https://github.com/FrostSource/alyxlib ]] -local version = "v2.1.3" +local version = "v2.2.0" ---@class __PlayerRegisteredEventData ---@field callback function From 4f6068377affe19726497a219fc641face10fe34 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 10 Jun 2025 16:33:46 +1200 Subject: [PATCH 027/101] Add check_lua_versions.sh to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7067ef5..39a0086 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ __pycache__ /test_release/ __test* *.bak +check_lua_versions.sh From a44762a611768b880cd84f5af5079021e86227a6 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 10 Jun 2025 16:33:46 +1200 Subject: [PATCH 028/101] Add check_lua_versions.sh to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7067ef5..39a0086 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ __pycache__ /test_release/ __test* *.bak +check_lua_versions.sh From aa5a6b82686a16dd626f296ad96b640bfe0fb5bb Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 10 Jun 2025 22:38:42 +1200 Subject: [PATCH 029/101] Add template files for AlyxLib installation app --- deployment_manifest.json | 136 +++++++++++++++++++++++++++++ templates/gitignore.txt | 12 +++ templates/resource_manifest.txt | 9 ++ templates/script_init_local.txt | 2 + templates/script_init_main.txt | 10 +++ templates/script_init_workshop.txt | 3 + templates/soundevents.txt | 3 + templates/test_delete.txt | 2 + templates/vscode_settings.txt | 28 ++++++ 9 files changed, 205 insertions(+) create mode 100644 deployment_manifest.json create mode 100644 templates/gitignore.txt create mode 100644 templates/resource_manifest.txt create mode 100644 templates/script_init_local.txt create mode 100644 templates/script_init_main.txt create mode 100644 templates/script_init_workshop.txt create mode 100644 templates/soundevents.txt create mode 100644 templates/test_delete.txt create mode 100644 templates/vscode_settings.txt diff --git a/deployment_manifest.json b/deployment_manifest.json new file mode 100644 index 0000000..bdb4831 --- /dev/null +++ b/deployment_manifest.json @@ -0,0 +1,136 @@ +{ + "Categories": { + + "vscript": [ + { + "type": "symlink", + "description": "AlyxLib library symlink", + "source": "{AlyxLib}/scripts/vscripts/alyxlib", + "destination": "{AddonContent}/scripts/vscripts/alyxlib" + }, + { + "type": "symlink", + "description": "gameinit.lua symlink", + "source": "{AlyxLib}/scripts/vscripts/game/gameinit.lua", + "destination": "{AddonContent}/scripts/vscripts/game/gameinit.lua" + }, + { + "type": "copy", + "description": "Addon main init.lua file", + "source": "{AlyxLib}/templates/script_init_main.txt", + "destination": "{AddonContent}/scripts/vscripts/{ModName}/init.lua" + }, + { + "type": "template", + "description": "Workshop mod init file", + "source": "{AlyxLib}/templates/script_init_workshop.txt", + "destination": "{AddonContent}/scripts/vscripts/mods/init/0000000000.lua", + "replacements": "{ModName}", + "rules": [ + { + "type": "FileNameDoesNotExist", + "target": "destination", + "value": "rx:^\\d+\\.lua$", + "description": "Possible workshop init file already exists so a new one will not be created." + } + ] + }, + { + "type": "template", + "description": "Local mod init file", + "source": "{AlyxLib}/templates/script_init_local.txt", + "destination": "{AddonContent}/scripts/vscripts/mods/init/{AddonFolderName}.lua", + "replacements": "{ModName}" + }, + { + "type": "symlink", + "description": "Scripts symlink in addon game folder", + "source": "{AddonContent}/scripts", + "destination": "{AddonGame}/scripts" + } + ], + + "editor-vscode": [ + { + "type": "symlink", + "description": "AlyxLib snippets symlink for VSCode", + "source": "{AlyxLib}/.vscode/alyxlib.code-snippets", + "destination": "{AddonContent}/.vscode/alyxlib.code-snippets" + }, + { + "type": "symlink", + "description": "VScript snippets symlink for VSCode", + "source": "{AlyxLib}/.vscode/vlua_snippets.code-snippets", + "destination": "{AddonContent}/.vscode/vlua_snippets.code-snippets" + }, + { + "type": "copy", + "description": "AlyxLib VSCode settings file", + "source": "{AlyxLib}/templates/vscode_settings.txt", + "destination": "{AddonContent}/.vscode/settings.json", + "rules": [ + { + "type": "FileDoesNotExist", + "target": "destination", + "description": "VSCode settings file already exists, so it will not be replaced" + } + ] + } + ], + + "panorama": [ + { + "type": "copy", + "source": "{AlyxLib}/panorama/scripts/custom_game/panorama_lua.js", + "destination": "{AddonContent}/panorama/scripts/custom_game/panorama_lua.js" + }, + { + "type": "copy", + "source": "{AlyxLib}/panorama/scripts/custom_game/panoramadoc.js", + "destination": "{AddonContent}/panorama/scripts/custom_game/panoramadoc.js" + } + ], + + "sounds": [ + { + "type": "copy", + "description": "Addon soundevents file", + "source": "{AlyxLib}/templates/soundevents.txt", + "destination": "{AddonContent}/soundevents/{AddonFolderName}_soundevents.vsndevts" + }, + { + "type": "delete", + "description": "Delete default soundevents file", + "source": "{AddonContent}/soundevents/addon_template_soundevents.vsndevts", + "rules": [ + { + "type": "Hash", + "value": "768e1cb207576e41b92718e0559f876095618a23f7a116a829a7b5d578591eeb", + "description": "Default soundevents file has been modified, so it won't be deleted" + } + ] + }, + + { + "type": "template", + "description": "Addon soundevents manifest file", + "source": "{AlyxLib}/templates/resource_manifest.txt", + "destination": "{AddonContent}/resourcemanifests/{AddonFolderName}_addon_resources.vrman", + "replacements": "{AddonFolderName}" + }, + { + "type": "delete", + "description": "Delete default soundevents manifest file", + "source": "{AddonContent}/resourcemanifests/addon_template_addon_resources.vrman", + "rules": [ + { + "type": "Hash", + "value": "495d7301afadbed3eece2d16250608b4e4c9529fd3d34a8f93fbf61479c6ab13", + "description": "Default soundevents manifest file has been modified, so it won't be deleted" + } + ] + } + ] + + } +} \ No newline at end of file diff --git a/templates/gitignore.txt b/templates/gitignore.txt new file mode 100644 index 0000000..5aca9bd --- /dev/null +++ b/templates/gitignore.txt @@ -0,0 +1,12 @@ +# Source 2 ignores + +/_bakeresourcecache/ +*bakeresourcecache.vpk +__pycache__ + +# AlyxLib ignores + +scripts/vscripts/alyxlib +scripts/vscripts/game/gameinit.lua +.vscode/alyxlib.code-snippets +.vscode/vlua_snippets.code-snippets \ No newline at end of file diff --git a/templates/resource_manifest.txt b/templates/resource_manifest.txt new file mode 100644 index 0000000..6e99652 --- /dev/null +++ b/templates/resource_manifest.txt @@ -0,0 +1,9 @@ + +{{ + resourceManifest = + [ + [ + "soundevents/{0}_soundevents.vsndevts", + ], + ] +}} \ No newline at end of file diff --git a/templates/script_init_local.txt b/templates/script_init_local.txt new file mode 100644 index 0000000..2a6c868 --- /dev/null +++ b/templates/script_init_local.txt @@ -0,0 +1,2 @@ +-- This file was automatically generated by AlyxLib. +require("{0}.init") \ No newline at end of file diff --git a/templates/script_init_main.txt b/templates/script_init_main.txt new file mode 100644 index 0000000..e451170 --- /dev/null +++ b/templates/script_init_main.txt @@ -0,0 +1,10 @@ +-- This file was automatically generated by AlyxLib. + +-- alyxlib can only run on server +if IsServer() then + -- Load alyxlib before using it, in case this mod loads before the alyxlib mod. + require("alyxlib.init") + + -- execute code or load mod libraries here + +end \ No newline at end of file diff --git a/templates/script_init_workshop.txt b/templates/script_init_workshop.txt new file mode 100644 index 0000000..9cbf785 --- /dev/null +++ b/templates/script_init_workshop.txt @@ -0,0 +1,3 @@ +-- This file was automatically generated by AlyxLib. +-- Rename this file to the ID of your workshop item after upload. +require("{0}.init") \ No newline at end of file diff --git a/templates/soundevents.txt b/templates/soundevents.txt new file mode 100644 index 0000000..6df5591 --- /dev/null +++ b/templates/soundevents.txt @@ -0,0 +1,3 @@ + +{ +} diff --git a/templates/test_delete.txt b/templates/test_delete.txt new file mode 100644 index 0000000..bed55ec --- /dev/null +++ b/templates/test_delete.txt @@ -0,0 +1,2 @@ +-- This file was automatically generated by AlyxLib. +require("test_addon_name.init") \ No newline at end of file diff --git a/templates/vscode_settings.txt b/templates/vscode_settings.txt new file mode 100644 index 0000000..4510211 --- /dev/null +++ b/templates/vscode_settings.txt @@ -0,0 +1,28 @@ +{ + "Lua.workspace.library": [ + "${addons}/HLA-VScript/module/library" + ], + "Lua.runtime.version": "LuaJIT", + "Lua.runtime.builtin": { + "coroutine": "enable", + "debug": "enable", + "io": "disable", + "math": "enable", + "os": "disable", + "package": "enable", + "string": "enable", + "table": "enable", + "utf8": "disable", + "bit": "enable", + "bit32": "disable", + "jit": "disable" + }, + "Lua.type.weakUnionCheck": true, + "Lua.type.weakNilCheck": true, + "Lua.diagnostics.disable": [ + "inject-field" + ], + "Lua.workspace.checkThirdParty": false, + "Lua.diagnostics.ignoredFiles": "Enable", + "Lua.workspace.useGitIgnore": false +} \ No newline at end of file From 94d9c2a268d0c59ed8d875b7edb7ab72964253d9 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 10 Jun 2025 22:38:58 +1200 Subject: [PATCH 030/101] Fix potential nil reference in weapon switch event --- scripts/vscripts/alyxlib/player/events.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/vscripts/alyxlib/player/events.lua b/scripts/vscripts/alyxlib/player/events.lua index 86c21c5..2ea0460 100644 --- a/scripts/vscripts/alyxlib/player/events.lua +++ b/scripts/vscripts/alyxlib/player/events.lua @@ -704,7 +704,10 @@ local function listenEventWeaponSwitch(data) Player:UpdateWeaponsExistence() - Player.PrimaryHand.ItemHeld = weaponHandle + -- This event can fire before the player has a hand + if Player.PrimaryHand then + Player.PrimaryHand.ItemHeld = weaponHandle + end savePlayerData() From 03aaeb0cefc323b6f3605ce00d10103bc36fcd69 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 10 Jun 2025 22:43:18 +1200 Subject: [PATCH 031/101] Update AlyxLib version to v1.4.0 --- scripts/vscripts/alyxlib/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vscripts/alyxlib/init.lua b/scripts/vscripts/alyxlib/init.lua index 5a19946..aa1acc6 100644 --- a/scripts/vscripts/alyxlib/init.lua +++ b/scripts/vscripts/alyxlib/init.lua @@ -16,7 +16,7 @@ local version = "v1.2.1" ---ID of AlyxLib in the Steam workshop ALYXLIB_WORKSHOP_ID = "3329679071" ---The current version of AlyxLib as a whole -ALYXLIB_VERSION = "v1.3.1" +ALYXLIB_VERSION = "v1.4.0" print("Initializing AlyxLib system ".. ALYXLIB_VERSION .." ...") From 4b7ed933109faa09360bb592f4d898dbdc856fe8 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Wed, 11 Jun 2025 20:42:54 +1200 Subject: [PATCH 032/101] Add 'remove' property to symlink entries in deployment manifest --- deployment_manifest.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/deployment_manifest.json b/deployment_manifest.json index bdb4831..6011ea4 100644 --- a/deployment_manifest.json +++ b/deployment_manifest.json @@ -6,13 +6,15 @@ "type": "symlink", "description": "AlyxLib library symlink", "source": "{AlyxLib}/scripts/vscripts/alyxlib", - "destination": "{AddonContent}/scripts/vscripts/alyxlib" + "destination": "{AddonContent}/scripts/vscripts/alyxlib", + "remove": true }, { "type": "symlink", "description": "gameinit.lua symlink", "source": "{AlyxLib}/scripts/vscripts/game/gameinit.lua", - "destination": "{AddonContent}/scripts/vscripts/game/gameinit.lua" + "destination": "{AddonContent}/scripts/vscripts/game/gameinit.lua", + "remove": true }, { "type": "copy", From c396e5094467325ed3dfbabd17ac351576c88f45 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 12 Jun 2025 12:26:48 +1200 Subject: [PATCH 033/101] Test new logo --- README.md | 7 +++---- assets/256x256.png | Bin 0 -> 50250 bytes 2 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 assets/256x256.png diff --git a/README.md b/README.md index 7a13437..4c82bba 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,13 @@

- + + AlyxLib Logo

-  -
[![License](https://img.shields.io/badge/License-MIT-04663E)](#license) diff --git a/assets/256x256.png b/assets/256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..b6264180547c052bc00e7595e0380789cbe12f52 GIT binary patch literal 50250 zcmV)UK(N1wP)4)O00&^-!2tqr01m+Y7!DAC z18@ND$8dlE9DoCGKZXMY-~b$e`!O6K00-az+>hY^0k{uAL?g7X$8Z4tw%`B(xDNpZ z()M^ri6Ib#hM3TnXjC`S@+b-UXGEhqrqKeXA!|_$(;vUlri1~>8B4JpPjm9_(Fk*puid z4|*>B)Vde2?`KAzIr3$9ylwSA%zxyNf1Ul%@(*~QJ^Ul|(}$c-Ke6@#^ojMX9sDWQ zE&$#>e#ob{pLg(Qx1V>=7k0h-urKX?&mrI1^WMY0J^jAJzBm2e!~c8g_uu-#%w0GB zbavS#dlvn`K`*2qJm^`horuprVG^Bx!U%o; zx(fO5x-$LHWFXoHm-+PbPsQCLGD05Zq>%rWpASGkaDV_T0x+Z&@0EQ1kRZC~q2=(x zqh9O4t+@Uxd*&x_WVQ6X?%3mN25<3^!e@YTX)Iq=6io0hH%Yn%fF@;m2Zfe zu%$U)n_H-bwOV6gdSPbAUt2V~wi&f90~}onn0y_)=*t-9FFf>6@1sM*t#^-%2LBAC z{$`+!bJ(%hvh!Zc{=N=y$UYPP`PD0yMrSWy61`&SSmW=OjFab0jz-TJA0y8gAB~hL2^drYEqn|x_ zn7(ISiT=m20ln)to_@as!hJQhe{*nv0CWkQV<*1<@&94+)k90ipHO zS@`(c5AFWYnoH`<@XERAo!3VTQ(NjyGSh6eu3<>})uB@Kaeo-S8I_~6m#suE85yIG z^L_8w(Ghga(2#d19)eMC*rVl9k5q>+t&Vs!2mneYc56f!A`qX57WxqlJ;p&XQx6cA zE(l>96)2``X4x;mI+XoQEyDDtb+8>+OHjt!FsjE$0Gs($Ms6Qksm)oX*7tUjdt zQhySibLa!S|A_qB#SK!sY<}nNdz%|K?4Fyi-CCcSx_s-0p7qt~H!Zo~u5*^X8h_-- zqv!|MROp9R`}<%C{7u0D0?;8)(nJxR_vw{y8H&Do#Ax&4^{35$YUTUquKvuoqelIj zu+`exYK2#gj{4`9$AW*HT#nAH4$%|GhN1_S%4DQEOi^h#!k*9ZE@G%h5L2cxQ@}C9 zTny|lQ{5PjD#!pr*il~ei&0JXDf%_doWieUL`qO4 zM!%>hi46apwiwZ9d7PvG^03FwDVL}>JlZOiMxxTlC_bc8X*^=pTK`33E4|kpyc&O| zK3lu7UL*I;OozX_`!&li-uw@fZ)|+v;3J!#d{~A2_T#=yq(k;Rx-Iq^xi7*20#E?- zTL+cscaB&(^~E)RKlkbNpKtu-TUXX=&8-kcKd4TW-a0Z=ewG)YV|<@hhsxAL5SB3F zDgf7O`NF4;o=OGJwXj1rkVZbn#YN+Q#P6by_#}=el5ZO%&mX+HTq7q(VHzVQ?;ly$ zBXDslJ&@(lYMKaWWo6f?=9OtD`~dJA5m^zyzUXZKC~9UC8cS*02rSFMn$&sP-V&J;N3 z=8Tpj8bgst{^dV&8X#~)Fl>#40-3l-4IIw}uFKy+5{)Eca*i*E0zX$FoaFHQLPZ#C zA`)|A3qg@Ofp8UqeUnW?f)Ma~F&&%h*%6X|^);Zz!bA|(A}|lY$78)q?4X%TXygxa z{1nmnCru7)dY3s_06uAzJSL%F+6owId&6T0#)iGM%U1=bjW2=s)u!kFGQWA_4ZAOR z?ZvfstbZ8b{oW( zNGv4+DjR3u8nLO1$i z6bXXFG$DeyW(3^Q0ijnNCTOS<9zMB>oasaJ+qGS_yZ3zWV_&cR>!Hu`{^Oxl`iBb- z5COO!6M%@;J-X@p9{Jw!YiGZF@Ov6t{&00OYW;fY1btPxL=N`LEngV7nBt1Kp^Ysn z8bwGc%SxP9+|aOcu6uG_ll?{9QY{FCl%x%ZmJLz$>J^d;by!H~5?S>^=hi%|uE)xp zM+8w&^o63BAn2w+&?JrYLWX7^<$VP~K~W|F;bTK;X$X9ZkRc>UBR!S;no<-KC{2{C zhHQcbG3PgQk&nkFMDB3hKJD+~4P)7xkTR6rf}1#*7=n-|`VY#0s#UUVVnuXDP+7RN zzGugVnM>YwA${+m$I~xAJK(ktMnKU0G0^IMNdV~2P7Cm*M?HG>vNwEhVSeV;>Ja^x zau6L;!jaFgP;h!0>&r$%M;uEXu1iyTu%joZ6yNxmvaZG7+nufe+2S@$Pai_o{Vf&S zRtDm}>?=v)2SlZDKiPTqb7c3I4GDUaBSGYL<4PtHfi${limJ+C~3R*fz5UtXK5 zT|52ft9~)_PwQWRKKG206wK~lMBsiy0Jv{HyzIHd3l|^yym0r;KX0|rFUCj9r&R+w z%-9ANoJwn=g6nJJ8@JYTPR4a~DcRe=5sm;&GBAD;kvg-|?b+{9Y~0hxucV1i5MOsL z&$T(ZI>aRj)ogBQIJ5ezZ4oFTxkPF#%D*o48~*IEN`kboL6aO`oI{jN0`jF1GY<|o zSMNtObq%tvPoGc0{G*y^YQ!hb`9f?_$F+YGOJ67^Ff;_-_!2ld7;Sxh&&_}Mee-RH zzY5Mjr9z|denSNA7X%>s*{P-4MTb4RcGq>k^-8TvJ-_)lT#bAxj9Xk|N49GW$%QAy zd!!aIm4;RVQo z#7&bDM^}&|0kl8AixXUlfnsWimxz*(R@#b>@Z?1FV9%fZ?4Db1{?o!gAO33kiAR?@ zAJ}~t{$>dPB@rT9zF&#H{lG^xw%zj6(rD?Mqf7AdL8-(ryhO27=dolsT%VP`X^!Ab z*OxjlQMxXVff1^!)YGLd9CIWsmV2TDOUFg(^>UpW`SLezN!QGmlNIeLR64TVZqlGO;nsh z+$lJ2$|(7~mMz4b53#hQ+@$KgA!)m?39z!m8mT~dVALcMsm@XT8=D_L@X_RQIKm%l zTu|G%>Ds9`t$P9eZ{7n)7LR+Zy(-YU%uQ>O@7NVC`hwx)P9zlmJ-JZ%o z7%q$R|6;M0sD(+S3BZV+C?QGkOjZoh*jd=wx zDq%xF26=KvmFf>eyo6kKn}*b4b5GB(Y2-miDpTe)v9e|entcMBlz^?2%6R?g>O}J! zGdExPi>WjiMA4yK2#drZk>56gBvAziwvdf#MbzW*ChIVx+-=X-f{OTy3-IvmNGeFu9 zi1MM!b8O>Zp-sWEA-LzeI)3(elFtX)_-5XN8K^PI8DnHtoPsl+ZXj#CNgIXxK8T8= zP+k!`4uji3r1*m|MW_@YG`2E&)bMcq>h1q|#z*M+PZ;`JGZOyI5P*m_arA>n9yP!5 z@}EsEDSa>~2NQ^gwsLC^n@%awM@WmhdYvN0wP~buej<^IkXYQ7=Q{{#FLtX5&aZv< zDiB{v_tZ5|D}z=WXnRnpCGAcsf>9MYpqujh6-Ijee{i&PwfHjJe?X3CGR z;|}CO)ek6D9L2}2H~^c7Ca^}B4eTXIQqP+!jaab&$?@jcJZu##j@!@!SL4QpNK20m z59tmuju6J!qr(&QNE+0BHvOqre+s_&#wxG1P`E(%bGXkx0Q$w_hnj!A_~MZ<`k#J) z#^NI)?@e2>M9_{2gO7;P+flqwsKyyi4{e&wI1M7!|f02!e|y9?sEiyUiT)izV*5{R|Ee`fe%Y$-a1F&mQQg@=GAi_FGt3Z#G<#* zxk>DIcLH$S0GXr?%o#hDI(zaoqw~u~Vx3uB42E0Ueslv_I=;IAiRS7=#O~{bsQYqF zSo*fe`rPh*Nq>HF!3!^m8bi`wZ`lM=<4vkQNf)TjB(z)dnnb(-g_KQ0&Y}EEDakkv z?OQvWOA53~MZw60@w*VtoH8cU6_2CmAhlMser`LrsQ%rJIJ3@4oGd#^69xy;et65epB2HE z2P8dS`6b`)p9$t-?eEv349TfC&>jl!Cvp!|K0+5nAU$5N2zIhwLECvS1MHj#xMz9L z3YM*)$4_m%^YYm@9rDl!nj)N16M_A;fa!gL0Fa+NWF(xrI7 z3X#xYKcfWPcRb@ z`JE6KYC%$3WBzpVdG8zS@jO6Y`Eb??k1L#fD8ft|M4T5vhsUGQWlQLH zcD(w~2h*sz-&QZ)ZwUas;@ zZKpYlpxnwPAegD)0`gpGwFad#BcnnjR0M#Ibl`+aKs5)26i1N(Mw%PaG=~SV^x}R~ zR(43JDID@_6mcg|fNZ?Vj-A$Abbb<2#Ih*XzsvsRHQ5)qHk7vGGvh*fVaYypoPp{O zj0;$KH1bqJ^%OFtJb?0WG~t!dSIEbowo?8c?sxdM-wlWZz2Z6Lse5m_DDdF9n6G52 zUn)gf3Xms)q->;>sAvvJvDNHhF0=IVJo( zMKC&9KWg^28!n?Ce$q;W{Qb@*u-_2?`m57%?cP7Xb7Z*me6Q>={NTkpLJv`jz>lcRqoTaD!B10mK4IK;z^nSkesRl{s9K&M9~|A= zwh6eg_BP1!0ou)&vEz+vGe}(Qb|zDL4Xr{XrQ!o^b3HgZGuSl)6TQS;&~!`mPpq1M z88(ZQ4&TnLnguDFO45eXetaY*XkrTDnv5n+LaYrHO)lP-gEC1Q3o-#iLIp*@H#`^{ z@sFPV>&@Swm%d}D_<{B=>{kSU(i)1UZaXz7mCp4&zM3(KlQTeM*2s0l5nly`M9PT7 zr2wjeee~q%#2$SE%krPl_|&4nJ*K2;=+upDi|WpiwqFbHuha;X*Ic7#CelGh%aLA+ z#zz*}{Or-wcM57LI=K&_^=< z{>^}?PeA-olb z>~Nf{^BL}Mn$af4n~L@CMd#1rt`w4lF$WUQn?*!)MIqM*-w8+W z1X9oA)+ew|&6uyQZbh{uDeF2G56#}kL7z{MkBPr@RrD{@H~jA_=tZyIZvuq&8v+17 zK6)Z-w7%~9;RwZ{&oJAkKuvy*$i`BZeld|E(oY(3r07qSE0CCce{swh%P5#Y8YP-1 zj5*p^XhGM3Vcnje(D^g`;P%HX4xGAwfz`fW{444&b^rZb=kFT2kN^ZE<3vhc#01;s zC+4sE$w^`xw*3J1IRS`B3(*_T3mOaLqd^59gn1Ybk)~V}QMm`b@dqpYz9oIH0#v3S zSUy=T18{1LZ5>_TR7i93dQ7n~#PQgIR2pvq7$c)%pQ%`TR|j=;e#yxdgG_h>B%8B? zmQ0$mwrObT#hf1jamSw%mterDPvFMx$2$S(IQ(RP>cz##7bVA21jV@!87(1E*x}OY zdIClFbLYoBF85@>W`BluF$E?jzD#T~n(p6*ubZs~Oj=EvdViGT_=5oS^PS z*<%W5cZ)fZbC99qr@NPa`mBN&W(T6I@pC&04~vedu^G6Ldx>#b&(S{rUQIwRar}@b zrriUDIGnHpsZHAVG?Vd?$B6!(-&EU4Q)jb7_k(3Aee= zq6POU0iai&Rc_X5AHXHQr1p2CTEyd4ez7P*h2W8l?9-_!hFcpE83ad9l+`BM=dX}g z`Tk=_>flJJ5_ypD8njt!xnbx%RRMPF5~A=2FJaDUWrVM8grUqN=H^S!W&b0;8nB>`G0Q(PT||Ah07Uy5+4SXWvk*k=TQ zl6o+E=hbiZhTt(66G3n><~97d+Rr|>fOC2(v$_CeDtM$edxSg{4Z%#F=B{z= zy|}zL=v{e0sTsC4Ime$bcL7p9eIZSdM6>hL6Q#g4aVhfa^NKXOiI8W$+kg-Aa~@22 zN5jm~Z-)9wAB6cQe*tDrzZ7Pl{%x2#^&2pE+E<}|@+V;ar1!zDHP3}T^Q&NChVPBX ztJ(x9Pk|sLDsU=kqsXZbV19#S)B6XI91)>FpD(J9&{@+{AD1>aLv$o6oAV2ag~rdS zb$)j|v3*4D5M|zx6*FS;O(-N{Y*>)e!xUOc}@bx9(LAoH^om_72%PXld)s9qVXxj) zQ#f+{d8O9;{5!A@!!(RMXhk04dMVTQy(R@Ol=e4`Jg#SBwNZVoT2lpF(R)7Gya3Yf6f2E@U#8}g5^gs zf>LFNc?^Lk4V>cPi+yg~) zMcl#%NL$}+Z-oO$*F*%AK>-H^Nxy%}AWP%FvC7yPwDlrrZ-$6!H&^}Di%R(HrDa_Z0b_(C zhVu&#fx6E4_-*lDeNIkc7ujJM-$f5yF|{6EFOdFpxb* zCHBXcfk}J}W^X_m>-hRPoI3u4*6Gu6`^hIxZq|}j-@s|e3`NF<6x=`S4aUzyQXIaB z{K(VxDo*aToAC`h=?_nwV6*&hO!t?ZndkLK5!MJ#TRJYEDv4gY z19c2QZ2fnFW-PzHw(iS)-lQd7m;*Ie&!YUGXi)}+ccSyB{n+jO;%R;zBl_HK3_~Y8 zL)iYgeKa;>rl?B?-!pV{kRAZftH6?%zYkh3{x;0)852g=m;)m15=dR&ZKPx;;J!a9 zR2QtXKvIMuKk|Q2(B=+^#I2X>IXE^1xp5_eY8Aa|>c&4kjO@iQ;k`xxn7HOOrgpt8 z@bIK3OZic0-DlI=A}Xjyg_>H(uEjP1NQ?Y&eSYf(ApGM7V!tpN&_XRF{@rHxC5BIh z8>Zxa-&Nw7GuaY@Jsj4zfNQoi)iZk@H>dN2kJ; zNd&mnfO<%w&Yp$SMvbUcKCJwvZ6k@bsDghIzVYmOcm#Aj0|yT(M?Dh!=YJ3yVM%sn zdr-bpNmSiL*PCc(k zLD$blqc|pj$iLJ&uDoKr$_>90@*?V&5j=I_n#&$kIM4yXULgRKHqf5CHvTK}!nF`a z9v9~^_x(qrQZM2qys?gjr}1t)24VEXX%7I2U$zGAKZoQm3CPposeL@7Bvp!%@1JX!W7&#wH<2BmK9~7E&TFNI}?MU5qSoFu+<9$Bab`-=AZa}Xv`C# z3h{7ZT}G`tqnQm(u7#~)6r8^}GH`)ShT5O0NF=iX)8x6XWGRnlzms?_lE>McBkeii z%4$$)`HkA#zct@>_+G)yMZXEx^?^Qp`=+U57x9G{*b7t!(fM7Qo{uqJ22to!?)r;l?S5Iv zQ~CKtm7g#fZ~_3-cJpZ$o#s9nyX|)M-396rh%T>hb3!KxFTxPYxb6zr3@a~qJCvuk z!F;m`Tk3WA<{h`f#T#ycpKrYzuH3N&uAANi8)j!=CnF50kREDD3mt&5mS7*2pZOlx zv#?$*_99g@EdnQ>$Mw?}NC5jdpFa^QP1Cs{_SM3z*o6cTU+*4IMwW0=P#I;|5|OhS z=PqA#W8mTwfFGY$#f4|;=z!{bzRQeD%mXv0#$x=70Ji-E-VsZlwTXu zBxeWP7atQ6b1cg$ZW|TBwGlXeeB?xeQJGbyn{97M==vn){;5DB#rJ4)b-7hpFU<`WDA_-iNK@ihUDI zJO_Ynn2kO2Nk4nG4&Ql8MM~&>2Sg*OSp7J>7pu4rl3)PH%I_x-sn2l@L3=*sHV3*Q zv1NC#q(ZC`+OU3JkprN{sMkmo1(m9I4k;~LlUlF?hD9d;Wa`ex_=p}&c&aZC`*jcl zJBBfPXC9p3@YLi1m`2=%nE}-KYe8}3UqR$x@!b=B=hu;s!3>u@-$Z2|9&p>Y;DPUY zEQiz|S$_Rge=%puticEU8mTa3)vcF;4&y3;56JTbNA~>?Mn~siNpu$+ zb^CYW*#CJiJouf@hew?K6gcjE=fGj#{v52l@)t02=iN+VFAFZeJ#4~PZodVt+=;B<4zhUK^f!2wu$n1Qu5UxaFd8Ov%%U z7W>81C3So;2pT8%Xvos> z00N_a9mdC6Fh05m)^58Bj{5!Q;l!`L4IcFF7r?__`D8fcBj>@yU;Yf0vIoDL+6)&m zLNF6GMV~H5%*tyJ5yOZHYAcR_+VOwKt%LGCtMK6Deu?n{&mzbIcM2B#V4)xmqcHn1 z!*i7Qsatd@x?hF_s69O3)A9&B@aDVfCoblG0*gifxNbAIYvx?vM?*N|YmXP)^Vm{i z+9}`LQ|L!nW$kJTi;W&F)dHnMs=eD}0gKpAEAK;N|FreKbq>GrDJ!m@>DSkUMC)*7 zfk!C@mh$7fPW4-jHvaK#6IvIJ+-|2*wJC;|NNsXn3TDOk}^lfXmbWv4e& zO!KBJ&=}wO5aK+7-Z;Mv5dt>CDWVOLxL$Gq)juQka0*P!1TCMNB$i+@E_}Jgy>mj3BUX#BZz*t`;nTZGhkPI;id5krXp6uK-fd z&2?3d>?Q02YO(u0{~VZ|Evay8{*FB5K*7pID9tnxE*I(SE{G^kJrwHfJxC#W4NBKXh-XqBmq<=U=)2x)z@7`f{}7>SjT0I9 zQX=>n%HUT#RET2Bru0RDw+9KBRgrO$8 zjCFGH*hVM4kf?Rvq*c44xPmUAxa3~D*DqHz7Qshc8UVrVxybKxqaZsbq<{(%gn_F8 zzEA~_Kz>iz5x5P3U&nV?pJ?0(wL3Nd;oC2uVwIOm0fdVZYaR%VgB}H-HU`oA4BAAy zlSs#Oo&DZH6Qi6X9YQzDAAwsHIQcc5b()q|apG7R!Hpe=|BDTP^E|{3 z5P(Yrk~RsxZn>0D_DE^?TJa`G<+Rrl_#gDb3dM%FMLzT#cgkVbm zDf$tM;Kq%Ijg8SbSQQ|(0);$6w6U>hEM1Sj06KX6?3D>!o`?7PNN{tG^y0IK4?xOI z2uvd2jUzon_!4F60bvqB#C{_%QubiwWvMX}X! z6I^7d2N8MNW-?PA1n9u8pVawMi#oOQPSUTOhyk$ZBX zdV`#pD93*EWUjx|W3@&`lwVzzcbD@U@4KfmJPVq;_g+zYvdi&ZzkVvtBhiSJ5QIeB zqX~sJ7W!4c^R>JJ7=0E9@= z3Qu8+vy7DPuQ%e5i_41Q%UW<5Ml>5-it7$P_7>9k6eEUYaG;E5NTuuA{STP_W~&KF zf}qo-u`ZE?WSjse0KyQ+b^btm0(|w?6Nt|v3Cm1xsQ48aVj_I$kRwFesfJ`Wo2$3T z@BCesxky1n7(VqSFh`fkjm(i0Yn>tIK8rhF#S+6}0BZ9s1KLPRk`dX*yX!l9?$h_f zjMHH@|3gbW;>}H+oakc!_JF}k0E~j|+&=wGT;W@L+g5z31sScA##B~+Hx^T)=gXs# zt0Z^aNmz(2?j>|FtIJ+=d~ECBI31`l2xxC3NNVUv`1EB1Pa6Z}@7hCv*xSZT48Yu0 zfI+AP!fqg92`DsRIjsHrH=7!RVskpXW}z)g854Ir4t{Qh`q1Fk#S@+bP2>aLx*-&L zF|r7{MsgEbQ1~v2EcF8cr;cCYdMJ$jk8L>pMm84pryhWZh2Zjte+vEB^M#veP{zQ) z5`fPg8H|kwC!mH9|0RPF;=QVRqPF=)S|V^?5QX-W-@zldMfwh|*X~N)I=WehkV5(n zBmj}eqPGdd*hmAOvhX7&kL%b0!Lj-8SgHcN_$rqODld8q_+x9-`j6j=o6nveR7^&> zp^@Wr<$14%=3G^{2`R;GUcsPEtAl{#Xp1F~qFj&xYtUK!5nVQqFtQsxP3#djKp5;3 z_`~C*6&};_i88zskX3Ss(d&%?Nnc~@)<|ma$(nrDENRAKHCtyh{k3!Jwe#zfjJ@63 zU7x9Qrgz)5Iet1$!37lsB$|G2ph};|Qh`8zWV`yPhyf6vjii8Kcj-u2cKX?(KjP^( zg_T#uvy3SAV5m8dAc^E2!ompF8XkxGAx{Jx$)FgiF)3@giG8;p&Xxkat~q?a=ezqv z*iR|*_p>bju1;?C@8pW~JN}~nWaOYUR+inV13UN_cT(qtI3z}|>_TB5wRCu+3aESlPZ0!eBfz}WVgO72 z?n8isk&NzXqOaT$TXR+|RUl+yMfG`af%+65OjCFT5S50m#C)p2nUv9UFG6~sh`kNj z_wQ7-5#(C=x+#g!@C1Rzwy6_oYjD=6fxRJ0n;5s~;751!D8-S~Ki z<94-#EJJ{86WH5e*SXI=0XiC-sHMoamsd6AkmVwjY5)%$qJl645JBsRXT!*ePd1We z9BN8ImZ0noA$_)7cuDv0@WY|L{$Xrxd_%akOECFd+G?*Ka_Ao1ZcW1CD|)_HHgD@6y=0ITqLOH^ciM=a0JmK=Fgw6N z2!NnQm2scx{73Q)K;*Vx8VQqzIq_H92dHD+H#P6$RwTk_*LX_|E;gjNHi2wEVB6S# zSI2i-vZkQsM*1^JsW2%|nSCq+c%e;==(@73kpg%J!i2#1|1;hR{_;cQD?ssWA|8`Z z^?Odg0QY$!(3Nv#>|`kjgr|Gx3D1JLV7<&6P{*amM#i}7*bbxyz+Ybk)JO!f|Fj=} z5IVn^O5CZ$kCA(Zr{CAH?*fNO}R-64^zM zfXUNe1$OKj9gq@)*fK18KGgg&geRRLgJU2z-MFJR*YV_+6h|I#Heuh7SFobGQY+H$ z+XR~#NcUHy9*i#ans;8a;dqX7`&Ask%&x}cf~pejK`unGVD0EOi}M>%5nPU_2!|aP zfa50n?*jP?tp0<5++2i?JBXRF%ao}y{85t~l`&?^&t0GMVdSiL0WA-Ub7i)lo{x;| zL#hS*&fX{=M%531v>Gt-yt83;VVIEwEGiMlRdp@Ssl75{uA>~ULx0doB2s%O$d2f^ zCZM?hbasQ>FM@#HcR;Loy8;m*@n5STuIb5PoXBgGsz8qK@&R*UKq^2JjSl(8@j7Nm znU>>>z2pPvg=QwreqO__s4=Rv&DQI{X3*V(ba#4x*?!6UwX+Y}{ka*C_A!pkb4Q#G zLk~FxVp5s7I&~Fy^W8H)3scQT`?1>nu=%Na9yEp-@*j0Nc%ctsoeRv@fj~*fMA88p z>iUl?EFwxp+wxMzF1p4-q2lDa;GXv)z?4c4q3RB`qQBX&O zVd*B~s!&M9c2nYWWF5Er$$9@a$Ipx(up!4!pQ~f`?bmm58?s{kUA;b+@THLigN_AI z0nYa36_(*I!XC`elwk5D?*bkHBy9rPZXsKg@qGM=uGzB-ZkpKzVFzl0PSq%T0kqin z>X~nX*;%CQ7bRFL^?)vDc zCSc6bb+DUkNmr!VH^6ZFu<&_ZX!!v3M(YIFy=fpK)PP7Z^;?bRF%(+Z7Gm96G|z&%%2JfqxPA3IAN}D}vR=R- z@G~PfKo|nyD5#8725^Hxt>C3An=Leqc%3`OM-Z>(8(CH%hqF-=hfdNWz7xsk?yc92 z4xfkM{pyT`e*Gdyg)ioT)IBBYYrQh<=jNGpd+HB>$y49J=z%W~KcZCm`LlFxB+i*) zhpw602~9R%EheAdFuk3f8Wavp4}FwL0X2_xa_U*otg-8&k}4afU9_5GYT+mQr6bzW z%dpOO*OT6%pj$0YbABCS+FP80tTWL`g@ta%m2oe|R3|18Y}>ZyctZQ%G-NOYpg#BL z;ZaWYOVZe^C)1_q$~XjyHsq?a8PLC>XAm6Ug)U!TzxQ`VLLx(k7ntBxJ@frQhezWj zh%(H2WLZ0Wu3* zsd!Gd9BrFjm(JEKg2ZmU@|UizPeXqbgbO)Bm+pHvdsQ*QvX4-6&-@YWeeE9vU}pR5 zi89}xAQ8R}metpd^`~mGLFeqpaoP#}R)4Vf3=s7K9L}GT2(Wb{Vx)%5So{p=->+Yw z^HaBFoNp=`X&Z?Stwl!c7rH&?@x+7Lny10giBA;u0zk$10gWrz#4k0bxc4ly;HLR$ zSRes-JLdr1wVB;6fLmvFO7&cD)%1cjIqc#uz!<7B#P=Y2#2FBV89Kbm3!sWaCTs#3 zdjTeF1QANd@;f>|Rr@T+d5(&5`np?QE>>Z(*F|h|NYn%*#7te3xw8

Jb#% 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 literal 0 HcmV?d00001 From 4003e693dbd74ec25c51a719e7fd3fa0d7975626 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 12 Jun 2025 19:45:48 +1200 Subject: [PATCH 034/101] Update init file version --- scripts/vscripts/alyxlib/init.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/vscripts/alyxlib/init.lua b/scripts/vscripts/alyxlib/init.lua index aa1acc6..ec00156 100644 --- a/scripts/vscripts/alyxlib/init.lua +++ b/scripts/vscripts/alyxlib/init.lua @@ -1,5 +1,5 @@ --[[ - v1.2.1 + v1.2.2 https://github.com/FrostSource/alyxlib The main initializer script loads any standard libraries that it can find. @@ -11,7 +11,7 @@ ]] -- Version of this file -local version = "v1.2.1" +local version = "v1.2.2" ---ID of AlyxLib in the Steam workshop ALYXLIB_WORKSHOP_ID = "3329679071" From 300c0e378535681120cdb6418d6e2d7c34cd7370 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 12 Jun 2025 19:46:03 +1200 Subject: [PATCH 035/101] Update version.json --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index b74ddb0..a8fa482 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "1.1.0" + "version": "1.4.0" } \ No newline at end of file From 3b47b31412fa39211cda38015db1f1069fd7d7f1 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 12 Jun 2025 20:09:57 +1200 Subject: [PATCH 036/101] Remove header docs Documentation is being moved to the wiki for lighter file sizes --- scripts/vscripts/alyxlib/player/core.lua | 92 +----------------------- 1 file changed, 3 insertions(+), 89 deletions(-) diff --git a/scripts/vscripts/alyxlib/player/core.lua b/scripts/vscripts/alyxlib/player/core.lua index a043d74..a2b53d1 100644 --- a/scripts/vscripts/alyxlib/player/core.lua +++ b/scripts/vscripts/alyxlib/player/core.lua @@ -1,106 +1,20 @@ --[[ - v4.2.1 + v4.2.2 https://github.com/FrostSource/alyxlib Player script allows for more advanced player manipulation and easier entity access for player related entities by extending the player class. - If not using `vscripts/alyxlib/core.lua`, load this file at game start using the following line: + If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: - ```lua require "alyxlib.player.core" - ``` - - This module returns the version string. - - ======================================== Usage ======================================== - - Common method for referencing the player and related entities is: - - ```lua - local player = Entities:GetLocalPlayer() - local hmd_avatar = player:GetHMDAvatar() - local left_hand = hmd_avatar:GetVRHand(0) - local right_hand = hmd_avatar:GetVRHand(1) - ``` - - This script simplifies the above code significantly by automatically - caching player entity handles when the player spawns and introducing - the global variable 'Player' which references the base player entity: - - ```lua - local player = Player - local hmd_avatar = Player.HMDAvatar - local left_hand = Player.LeftHand - local right_hand = Player.RightHand - ``` - - Since this script extends the player entity class directly you can mix and match your scripting style - without worrying that you're referencing the wrong player table. - - ```lua - Entities:GetLocalPlayer() == Player - ``` - - ======================================== Player Callbacks ======================================== - - Many game events related to the player are used to track player activity and callbacks can be - registered to hook into them the same way you would a game event: - - ```lua - RegisterPlayerEventCallback("vr_player_ready", function(params) - ---@cast params PLAYER_EVENT_VR_PLAYER_READY - if params.game_loaded then - -- Load params - end - end) - ``` - - Although most of the player events are named after the same game events, the data that is passed to - the callback is pre-processed and extended to provide better context for the event: - - ```lua - ---@param params PLAYER_EVENT_ITEM_PICKUP - RegisterPlayerEventCallback("item_pickup", function(params) - if params.hand == Player.PrimaryHand and params.item_name == "@gun" then - params.item:DoNotDrop(true) - end - end) - ``` - - ======================================== Tracking Items ======================================== - - The script attempts to track player items, both inventory and physically held objects. - These can be accessed through several new player tables and variables. - - Below are a few of the new variables that point an entity handle that the player has interacted with: - - ```lua - Player.PrimaryHand.WristItem - Player.PrimaryHand.ItemHeld - Player.PrimaryHand.LastItemDropped - ``` - - The player might not be holding anything so remember to nil check: - - ```lua - local item = Player.PrimaryHand.ItemHeld - if item then - local primary_held_name = item:GetName() - end - ``` - - The `Player.Items` table keeps track of the ammo and resin the player has in the backpack. - One addition value tracked is `resin_found` which is the amount of resin the player has - collected regardless of removing from backpack or spending on upgrades. - ]] require "alyxlib.utils.common" require "alyxlib.globals" require "alyxlib.extensions.entity" require "alyxlib.storage" -local version = "v4.2.1" +local version = "v4.2.2" ----------------------------- -- Class extension members -- From d8fa8116cf15033b0363b537fb286168eb1ec373 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 19 Jun 2025 15:19:30 +1200 Subject: [PATCH 037/101] Fix direct entity reference in class inherit Entity handles do not contain thisEntity, thisEntity contains the entity handle. --- scripts/vscripts/alyxlib/class.lua | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/scripts/vscripts/alyxlib/class.lua b/scripts/vscripts/alyxlib/class.lua index 2ddbd3d..76bc6e6 100644 --- a/scripts/vscripts/alyxlib/class.lua +++ b/scripts/vscripts/alyxlib/class.lua @@ -301,9 +301,16 @@ end function inherit(script, entity) local fenv = entity or getfenv(2) if fenv.thisEntity == nil then - fenv = getfenv(3) - if fenv.thisEntity == nil then - error("Could not inherit '"..script.."' because thisEntity could not be found!") + -- If given exact entity, get scope of it + if IsEntity(entity) then + ---@cast entity -nil + fenv = entity:GetOrCreatePrivateScriptScope() + else + -- Check further up environment + fenv = getfenv(3) + if fenv.thisEntity == nil then + error("Could not inherit '"..tostring(script).."' because thisEntity could not be found!") + end end end local self = fenv.thisEntity From ccd89492383852938674c82db1ee26619214f6d8 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 19 Jun 2025 15:30:38 +1200 Subject: [PATCH 038/101] Add Debug.Try Safely calls a function without errors --- scripts/vscripts/alyxlib/debug/common.lua | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/scripts/vscripts/alyxlib/debug/common.lua b/scripts/vscripts/alyxlib/debug/common.lua index bb61c5a..ac488f8 100644 --- a/scripts/vscripts/alyxlib/debug/common.lua +++ b/scripts/vscripts/alyxlib/debug/common.lua @@ -911,4 +911,19 @@ function Debug.GetSourceLine(f) return debug.getinfo(f, "S").short_src..":"..tostring(debug.getinfo(f, "l").currentline) end +--- +---Safely calls a function while handling any errors. +--- +---If an error occurs, a warning will be printed to the console. +--- +---@param action function # The function to call +---@param ... any # Optional arguments to pass +function Debug.Try(action, ...) + local success, result = pcall(action, ...) + + if not success then + warn(result) + end +end + return Debug.version \ No newline at end of file From 7e2a8f5a987d59f8663f7e8185be28c6f49d1338 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 19 Jun 2025 15:33:01 +1200 Subject: [PATCH 039/101] Add CBasePlayer.LastGrabHand When the player grabs an item, the hand that grabbed is assigned to this member. This can be useful when dealing with grab outputs like 'OnPlayerPickup' without using events. --- scripts/vscripts/alyxlib/player/core.lua | 3 +++ scripts/vscripts/alyxlib/player/events.lua | 2 ++ 2 files changed, 5 insertions(+) diff --git a/scripts/vscripts/alyxlib/player/core.lua b/scripts/vscripts/alyxlib/player/core.lua index a2b53d1..3ac0cd6 100644 --- a/scripts/vscripts/alyxlib/player/core.lua +++ b/scripts/vscripts/alyxlib/player/core.lua @@ -59,6 +59,9 @@ CBasePlayer.LastItemGrabbed = nil ---**The classname of the last entity grabbed by the player. In case the entity no longer exists.** ---@type string CBasePlayer.LastClassGrabbed = "" +---**The player hand that last grabbed an object.** +---@type CPropVRHand +CBasePlayer.LastGrabHand = nil ---@alias PLAYER_WEAPON_HAND "hand_use_controller" ---@alias PLAYER_WEAPON_ENERGYGUN "hlvr_weapon_energygun" diff --git a/scripts/vscripts/alyxlib/player/events.lua b/scripts/vscripts/alyxlib/player/events.lua index 2ea0460..16fee2b 100644 --- a/scripts/vscripts/alyxlib/player/events.lua +++ b/scripts/vscripts/alyxlib/player/events.lua @@ -280,6 +280,8 @@ local function listenEventItemPickup(data) local hand = Player.Hands[handId + 1] local otherhand = Player.Hands[(1 - handId) + 1] + Player.LastGrabHand = hand + ---@type EntityHandle local ent_held From a59cfc022c4508aca48480109487f780ce953689 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 19 Jun 2025 15:33:46 +1200 Subject: [PATCH 040/101] Fix error when no optional callback is assigned --- scripts/vscripts/alyxlib/helpers/easyconvars.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/vscripts/alyxlib/helpers/easyconvars.lua b/scripts/vscripts/alyxlib/helpers/easyconvars.lua index f63a7fc..7f8c8c5 100644 --- a/scripts/vscripts/alyxlib/helpers/easyconvars.lua +++ b/scripts/vscripts/alyxlib/helpers/easyconvars.lua @@ -83,7 +83,9 @@ local function callCallback(registeredData, value, ...) local result = nil if registeredData.type == EASYCONVARS_COMMAND then - result = registeredData.callback(value, ...) + if type(registeredData.callback) == "function" then + result = registeredData.callback(value, ...) + end else if registeredData.type == EASYCONVARS_TOGGLE and value == nil then registeredData.value = valueToBoolStr(not truthy(registeredData.value)) @@ -91,7 +93,9 @@ local function callCallback(registeredData, value, ...) registeredData.value = convertToSafeVal(value) end - result = registeredData.callback(registeredData.value, oldValue) + if type(registeredData.callback) == "function" then + result = registeredData.callback(registeredData.value, oldValue) + end end if result ~= nil then From 8a0af4b112fe8da528d1dec518a62c9bcd670f3e Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 19 Jun 2025 15:42:51 +1200 Subject: [PATCH 041/101] Add CBaseEntity:IterateChildren Iterate over all children in a memory efficient and safe way. --- .../vscripts/alyxlib/extensions/entity.lua | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/scripts/vscripts/alyxlib/extensions/entity.lua b/scripts/vscripts/alyxlib/extensions/entity.lua index c94ea6b..76db68e 100644 --- a/scripts/vscripts/alyxlib/extensions/entity.lua +++ b/scripts/vscripts/alyxlib/extensions/entity.lua @@ -29,6 +29,38 @@ function CBaseEntity:GetChildrenMemSafe() return childrenArray end +---Returns a `function` that iterates on all children of this entity. +---The `function` returns the next child every time it is called until no more children exist, +---in which case `nil` is returned. +--- +---Useful in `for` loops: +--- +--- for child in thisEntity:IterateChildren() do +--- print(Debug.EntStr(child)) +--- end +--- +---This function is memory safe. +--- +---@return fun(...:any):EntityHandle # The new iterator function +function CBaseEntity:IterateChildren() + local function traverse(entity) + coroutine.yield(entity) + local child = entity:FirstMoveChild() + while child do + traverse(child) + child = child:NextMovePeer() + end + end + + return coroutine.wrap(function() + local child = self:FirstMoveChild() + while child do + traverse(child) + child = child:NextMovePeer() + end + end) +end + --- ---Get the top level entities parented to this entity. Not children of children. --- From 8af7fe946f05c8435ecc02ea8c3a33bd46f7cc75 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 19 Jun 2025 15:43:46 +1200 Subject: [PATCH 042/101] CBaseEntity:ClearParent Simple wrapping for self:SetParent(nil, nil) --- scripts/vscripts/alyxlib/extensions/entity.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/vscripts/alyxlib/extensions/entity.lua b/scripts/vscripts/alyxlib/extensions/entity.lua index 76db68e..42746ca 100644 --- a/scripts/vscripts/alyxlib/extensions/entity.lua +++ b/scripts/vscripts/alyxlib/extensions/entity.lua @@ -530,4 +530,11 @@ function CBaseAnimating:GetAttachmentNameForward(name) return self:GetAttachmentForward(self:ScriptLookupAttachment(name)) end +--- +---Unparents this entity if it is parented. +--- +function CBaseEntity:ClearParent() + self:SetParent(nil, nil) +end + return version \ No newline at end of file From a83e39543465a982c4a806c0019226479410f255 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 19 Jun 2025 15:45:27 +1200 Subject: [PATCH 043/101] Update comments --- scripts/vscripts/alyxlib/init.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/vscripts/alyxlib/init.lua b/scripts/vscripts/alyxlib/init.lua index ec00156..6b1297d 100644 --- a/scripts/vscripts/alyxlib/init.lua +++ b/scripts/vscripts/alyxlib/init.lua @@ -35,6 +35,7 @@ end ---Loads a library if it exists and prints version. ---@param path string # The path to the library +---@param required? boolean # If not true, any errors will be suppressed local function alyxlib_require(path, required) if required then print_version(path, require(path)) From 6d99457b508aac77002201421176db556949ea64 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Wed, 25 Jun 2025 00:33:27 +1200 Subject: [PATCH 044/101] WR:Random returns generic tables --- scripts/vscripts/alyxlib/math/weighted_random.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/vscripts/alyxlib/math/weighted_random.lua b/scripts/vscripts/alyxlib/math/weighted_random.lua index 6b0edb0..5f7cc32 100644 --- a/scripts/vscripts/alyxlib/math/weighted_random.lua +++ b/scripts/vscripts/alyxlib/math/weighted_random.lua @@ -1,5 +1,5 @@ --[[ - v1.2.3 + v1.2.4 https://github.com/FrostSource/alyxlib Weighted random allows you to assign chances to tables keys. @@ -9,7 +9,7 @@ require "alyxlib.math.weighted_random" ]] -local version = "v1.2.3" +local version = "v1.2.4" --- ---A list of tables with associated weights. @@ -117,7 +117,7 @@ end --- ---Pick a random table from the list of weighted tables. --- ----@return WeightedRandomItem # The chosen table. +---@return WeightedRandomItem|table # The chosen table. function WR:Random() local weight_sum = self:TotalWeight() local weight_remaining From b9246f819778b422fcb85c2244fa6aca2928f08f Mon Sep 17 00:00:00 2001 From: FrostSource Date: Wed, 25 Jun 2025 00:34:13 +1200 Subject: [PATCH 045/101] Add optional delay for QuickThink --- scripts/vscripts/alyxlib/extensions/entity.lua | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/vscripts/alyxlib/extensions/entity.lua b/scripts/vscripts/alyxlib/extensions/entity.lua index 42746ca..c6252e3 100644 --- a/scripts/vscripts/alyxlib/extensions/entity.lua +++ b/scripts/vscripts/alyxlib/extensions/entity.lua @@ -1,5 +1,5 @@ --[[ - v2.7.0 + v2.7.1 https://github.com/FrostSource/alyxlib Provides base entity extension methods. @@ -9,7 +9,7 @@ require "alyxlib.extensions.entity" ]] -local version = "v2.7.0" +local version = "v2.7.1" --- ---Get the entities parented to this entity. Including children of children. @@ -446,10 +446,11 @@ end ---Quickly start a think function on the entity with a random name and no delay. --- ---@param func fun(...):number? # The think function. +---@param delay? number # Delay before starting the think. ---@return string # The name of the think for stopping later if desired. -function CBaseEntity:QuickThink(func) +function CBaseEntity:QuickThink(func, delay) local name = DoUniqueString("QuickThink") - self:SetContextThink(name, func, 0) + self:SetContextThink(name, func, delay or 0) return name end From 835c2845eed69bb85960c99d8203b69a8a57a1e7 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Wed, 25 Jun 2025 00:36:01 +1200 Subject: [PATCH 046/101] Fix Debug.PrintList not aligning items --- scripts/vscripts/alyxlib/debug/common.lua | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/vscripts/alyxlib/debug/common.lua b/scripts/vscripts/alyxlib/debug/common.lua index ac488f8..bfb4190 100644 --- a/scripts/vscripts/alyxlib/debug/common.lua +++ b/scripts/vscripts/alyxlib/debug/common.lua @@ -394,13 +394,20 @@ function Debug.PrintTableShallow(tbl) print("}") end +--- +---Prints an ordered table as a numbered list in the console. +--- +---@param tbl table # Table to print. +---@param prefix? string # Optional prefix for each line. function Debug.PrintList(tbl, prefix) local m = 0 prefix = prefix or "" + + -- Pre-determine alignment padding for key, value in pairs(tbl) do m = max(m, #tostring(key)+1) end - m = 0 + local frmt = "%"..m.."s %s" for key, value in pairs(tbl) do if type(key) == "number" then From 49f4e19e5c5d78abbcc36d1b8eab78f334e85aaa Mon Sep 17 00:00:00 2001 From: FrostSource Date: Wed, 25 Jun 2025 00:36:22 +1200 Subject: [PATCH 047/101] Add Debug.PrintSimpleTable --- scripts/vscripts/alyxlib/debug/common.lua | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/scripts/vscripts/alyxlib/debug/common.lua b/scripts/vscripts/alyxlib/debug/common.lua index bfb4190..e781bc8 100644 --- a/scripts/vscripts/alyxlib/debug/common.lua +++ b/scripts/vscripts/alyxlib/debug/common.lua @@ -418,6 +418,20 @@ function Debug.PrintList(tbl, prefix) end end +--- +---Prints all the values in a table, one value per line, without any numbering or padding. +--- +---@param tbl table # Table to print. +function Debug.PrintSimpleTable(tbl) + if type(tbl) ~= "table" then + return warn("Parameter 'tbl' is not a table " .. Debug.GetSourceLine(3)) + end + + for _, value in pairs(tbl) do + print(value) + end +end + --- ---Prints a value and its type in an easy to read format. --- From 63632491bed376b930c03610e2f7fc95a74ce85a Mon Sep 17 00:00:00 2001 From: FrostSource Date: Wed, 25 Jun 2025 00:38:59 +1200 Subject: [PATCH 048/101] Change panorama deployment type to symlink --- deployment_manifest.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/deployment_manifest.json b/deployment_manifest.json index 6011ea4..fe064bb 100644 --- a/deployment_manifest.json +++ b/deployment_manifest.json @@ -82,12 +82,14 @@ "panorama": [ { - "type": "copy", + "type": "symlink", + "description": "Panorama Lua integration symlink", "source": "{AlyxLib}/panorama/scripts/custom_game/panorama_lua.js", "destination": "{AddonContent}/panorama/scripts/custom_game/panorama_lua.js" }, { - "type": "copy", + "type": "symlink", + "description": "Panorama code completion symlink", "source": "{AlyxLib}/panorama/scripts/custom_game/panoramadoc.js", "destination": "{AddonContent}/panorama/scripts/custom_game/panoramadoc.js" } From 1baa9b486725ce856600828420a11555290a4ba7 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Wed, 25 Jun 2025 01:40:16 +1200 Subject: [PATCH 049/101] Add panorama files to gitignore template --- templates/gitignore.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/templates/gitignore.txt b/templates/gitignore.txt index 5aca9bd..e736d30 100644 --- a/templates/gitignore.txt +++ b/templates/gitignore.txt @@ -9,4 +9,6 @@ __pycache__ scripts/vscripts/alyxlib scripts/vscripts/game/gameinit.lua .vscode/alyxlib.code-snippets -.vscode/vlua_snippets.code-snippets \ No newline at end of file +.vscode/vlua_snippets.code-snippets +panorama/scripts/custom_game/panorama_lua.js +panorama/scripts/custom_game/panoramadoc.js \ No newline at end of file From d766b3421f1bc090cadce43ba3b696cabf2863ec Mon Sep 17 00:00:00 2001 From: FrostSource Date: Mon, 30 Jun 2025 06:11:06 +1200 Subject: [PATCH 050/101] Use IterateChildren in GetChildrenMemSafe This also fixes the array being ordered differently than GetChildren() --- scripts/vscripts/alyxlib/extensions/entity.lua | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/scripts/vscripts/alyxlib/extensions/entity.lua b/scripts/vscripts/alyxlib/extensions/entity.lua index c6252e3..5c02849 100644 --- a/scripts/vscripts/alyxlib/extensions/entity.lua +++ b/scripts/vscripts/alyxlib/extensions/entity.lua @@ -20,11 +20,8 @@ local version = "v2.7.1" ---@return EntityHandle[] function CBaseEntity:GetChildrenMemSafe() local childrenArray = {} - local child = self:FirstMoveChild() - while child do - table.insert(childrenArray, child) - vlua.extend(childrenArray, child:GetChildrenMemSafe()) - child = child:NextMovePeer() + for child in self:IterateChildren() do + table.insert(childrenArray, 1, child) end return childrenArray end From d22f55235984545d2f3fc239e27f0a6dc17aed9c Mon Sep 17 00:00:00 2001 From: FrostSource Date: Mon, 30 Jun 2025 12:18:11 +1200 Subject: [PATCH 051/101] Improve global precache printing --- scripts/vscripts/alyxlib/precache.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vscripts/alyxlib/precache.lua b/scripts/vscripts/alyxlib/precache.lua index bd1b160..961378a 100644 --- a/scripts/vscripts/alyxlib/precache.lua +++ b/scripts/vscripts/alyxlib/precache.lua @@ -72,7 +72,7 @@ function _PrecacheGlobalItems(context) if #AlyxLibGlobalPrecacheList > 0 then devprints("Globally precaching", #AlyxLibGlobalPrecacheList, "resources...") for _, item in ipairs(AlyxLibGlobalPrecacheList) do - devprints("\nPrecaching", item.type, item.path) + devprints("\tPrecaching", item.type, item.path) if item.type == "model" then PrecacheModel(item.path, context) elseif item.type == "entity" then From 4150f2d4fcf883d0839ac3ced94bcdfcf38fb507 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Mon, 30 Jun 2025 12:18:44 +1200 Subject: [PATCH 052/101] Make Debug.PrintEntityList param optional --- scripts/vscripts/alyxlib/debug/common.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vscripts/alyxlib/debug/common.lua b/scripts/vscripts/alyxlib/debug/common.lua index e781bc8..0962164 100644 --- a/scripts/vscripts/alyxlib/debug/common.lua +++ b/scripts/vscripts/alyxlib/debug/common.lua @@ -92,7 +92,7 @@ end ---Property patterns do not need to be functions. --- ---@param list EntityHandle[] # List of entities to print. ----@param properties string[] # List of property patterns to search for. +---@param properties? string[] # List of property patterns to search for. function Debug.PrintEntityList(list, properties) if #list == 0 then From c8ed491cb86f4a73a5488ca0d9089b5d0b847d96 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Mon, 30 Jun 2025 12:19:32 +1200 Subject: [PATCH 053/101] Add IterateChildrenBreadthFirst --- .../vscripts/alyxlib/extensions/entity.lua | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/scripts/vscripts/alyxlib/extensions/entity.lua b/scripts/vscripts/alyxlib/extensions/entity.lua index 5c02849..868a781 100644 --- a/scripts/vscripts/alyxlib/extensions/entity.lua +++ b/scripts/vscripts/alyxlib/extensions/entity.lua @@ -58,6 +58,50 @@ function CBaseEntity:IterateChildren() end) end +--- +---Returns a `function` that iterates over all children of this entity in **breadth-first order**. +---The `function` returns the next child every time it is called until no more children exist, +---in which case `nil` is returned. +--- +---Useful in `for` loops: +--- +--- for child in thisEntity:IterateChildrenBreadthFirst() do +--- print(Debug.EntStr(child)) +--- end +--- +---Unlike [IterateChildren](lua://CBaseEntity.IterateChildren), this visits all immediate children first, +---then their children, and so on. +--- +---This function is memory safe. +--- +---@return fun(...:any):EntityHandle # The new iterator function +function CBaseEntity:IterateChildrenBreadthFirst() + return coroutine.wrap(function() + local queue = {} + + -- start with direct children + local child = self:FirstMoveChild() + while child do + table.insert(queue, child) + child = child:NextMovePeer() + end + + -- process the queue + while #queue > 0 do + local entity = table.remove(queue, 1) -- pop front + coroutine.yield(entity) + + -- enqueue this entity's direct children + local subchild = entity:FirstMoveChild() + while subchild do + table.insert(queue, subchild) + subchild = subchild:NextMovePeer() + end + end + end) +end + + --- ---Get the top level entities parented to this entity. Not children of children. --- From 9bcd68996a2a1fc6ae452d8b60c297f32cdf93ca Mon Sep 17 00:00:00 2001 From: FrostSource Date: Mon, 30 Jun 2025 12:20:14 +1200 Subject: [PATCH 054/101] Make GetFirstChildWith* functions use breadth-first traversal --- .../vscripts/alyxlib/extensions/entity.lua | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/scripts/vscripts/alyxlib/extensions/entity.lua b/scripts/vscripts/alyxlib/extensions/entity.lua index 868a781..3cff36a 100644 --- a/scripts/vscripts/alyxlib/extensions/entity.lua +++ b/scripts/vscripts/alyxlib/extensions/entity.lua @@ -177,32 +177,38 @@ function CBaseEntity:EntFire(action, value, delay, activator, caller) end --- ----Get the first child in this entity's hierarchy with a given classname. +---Get the first child in this entity's hierarchy with a given `classname`. +---Searches using **breadth-first traversal**, so it finds the closest matching child first. --- ---This function is memory safe. --- ----@param classname string # Classname to find. ----@return EntityHandle|nil # The child found. +---@param classname string # Classname to search for. +---@return EntityHandle? # The first matching child found, or `nil` if none exists. function CBaseEntity:GetFirstChildWithClassname(classname) - for _, child in ipairs(self:GetChildrenMemSafe()) do + for child in self:IterateChildrenBreadthFirst() do if child:GetClassname() == classname then return child end end + return nil end --- ----Get the first child in this entity's hierarchy with a given target name. +---Get the first child in this entity's hierarchy with a given `name`. +---Searches using **breadth-first traversal**, so it finds the closest matching child first. +--- +---This function is memory safe. --- ----@param name string # Targetname to find. ----@return EntityHandle|nil # The child found. +---@param name string # Targetname to search for. +---@return EntityHandle? # The first matching child found, or `nil` if none exists. function CBaseEntity:GetFirstChildWithName(name) - for _, child in ipairs(self:GetChildren()) do + for child in self:IterateChildrenBreadthFirst() do if child:GetName() == name then return child end end + return nil end From 52f4a5fbee7c2104167e4e42d6a24298d4b25657 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Mon, 30 Jun 2025 12:20:50 +1200 Subject: [PATCH 055/101] Use table.insert in GetParents --- scripts/vscripts/alyxlib/extensions/entity.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vscripts/alyxlib/extensions/entity.lua b/scripts/vscripts/alyxlib/extensions/entity.lua index 3cff36a..88206f5 100644 --- a/scripts/vscripts/alyxlib/extensions/entity.lua +++ b/scripts/vscripts/alyxlib/extensions/entity.lua @@ -367,7 +367,7 @@ function CBaseEntity:GetParents() local parents = {} local parent = self:GetMoveParent() while parent ~= nil do - parents[#parents+1] = parent + table.insert(parents, parent) parent = parent:GetMoveParent() end return parents From f127eeaa760ecfbf4e642133a9cb7c20fb8224d8 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Mon, 30 Jun 2025 12:21:03 +1200 Subject: [PATCH 056/101] Improve documentation --- .../vscripts/alyxlib/extensions/entity.lua | 86 ++++++++++--------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/scripts/vscripts/alyxlib/extensions/entity.lua b/scripts/vscripts/alyxlib/extensions/entity.lua index 88206f5..5f97d71 100644 --- a/scripts/vscripts/alyxlib/extensions/entity.lua +++ b/scripts/vscripts/alyxlib/extensions/entity.lua @@ -26,7 +26,8 @@ function CBaseEntity:GetChildrenMemSafe() return childrenArray end ----Returns a `function` that iterates on all children of this entity. +--- +---Returns a `function` that iterates over all children of this entity. ---The `function` returns the next child every time it is called until no more children exist, ---in which case `nil` is returned. --- @@ -215,7 +216,7 @@ end --- ---Set entity pitch, yaw, roll from a `QAngle`. --- ----@param qangle QAngle +---@param qangle QAngle # The rotation to set (pitch, yaw, roll). function CBaseEntity:SetQAngle(qangle) self:SetAngles(qangle.x, qangle.y, qangle.z) end @@ -223,7 +224,7 @@ end --- ---Set entity local pitch, yaw, roll from a `QAngle`. --- ----@param qangle QAngle +---@param qangle QAngle # The rotation to set (pitch, yaw, roll). function CBaseEntity:SetLocalQAngle(qangle) self:SetLocalAngles(qangle.x, qangle.y, qangle.z) end @@ -231,16 +232,16 @@ end --- ---Set entity pitch, yaw or roll. Supply `nil` for any parameter to leave it unchanged. --- ----@param pitch number|nil ----@param yaw number|nil ----@param roll number|nil +---@param pitch? number # Pitch angle, or nil to leave unchanged. +---@param yaw? number # Pitch angle, or nil to leave unchanged. +---@param roll? number # Pitch angle, or nil to leave unchanged. function CBaseEntity:SetAngle(pitch, yaw, roll) local angles = self:GetAngles() self:SetAngles(pitch or angles.x, yaw or angles.y, roll or angles.z) end --- ----Resets local origin and angle to [0,0,0] +---Resets local origin and angle to [0,0,0]. --- function CBaseEntity:ResetLocal() self:SetLocalOrigin(Vector()) @@ -250,7 +251,7 @@ end --- ---Get the bounding size of the entity. --- ----@return Vector +---@return Vector # Bounding size of the entity as a Vector. function CBaseEntity:GetSize() return self:GetBoundingMaxs() - self:GetBoundingMins() end @@ -259,24 +260,25 @@ end ---Get the biggest bounding box axis of the entity. ---This will be `size.x`, `size.y` or `size.z`. --- ----@return number +---@return number # The largest bounding value. function CBaseEntity:GetBiggestBounding() local size = self:GetSize() return math.max(size.x, size.y, size.z) end --- ----Get the radius of the entity bounding box. This is half the size of the sphere. +---Get the radius of the entity's bounding box. +---This is half the size of the bounding box along its largest axis. --- ----@return number +---@return number # The bounding radius value. function CBaseEntity:GetRadius() return self:GetSize():Length() * 0.5 end --- ----Get the volume of the entity bounds in inches cubed. +---Get the volume of the entity bounds in cubic inches. --- ----@return number +---@return number # The volume of the entity bounds. function CBaseEntity:GetVolume() local size = self:GetSize() * self:GetAbsScale() return size.x * size.y * size.z @@ -285,8 +287,8 @@ end --- ---Get each corner of the entity's bounding box. --- ----@param rotated? boolean # If the corners should be rotated with the entity angle. ----@return Vector[] +---@param rotated? boolean # If true, corners are rotated by the entity's angles. +---@return Vector[] # List of 8 corner positions. function CBaseEntity:GetBoundingCorners(rotated) local bounds = self:GetBounds() local origin = self:GetOrigin() @@ -353,8 +355,8 @@ end --- ---Delay some code using this entity. --- ----@param func fun() ----@param delay number? +---@param func fun() # The function to delay. +---@param delay? number # Optional delay in seconds (default 0). function CBaseEntity:Delay(func, delay) self:SetContextThink(DoUniqueString("delay"), function() func() end, delay or 0) end @@ -362,7 +364,7 @@ end --- ---Get all parents in the hierarchy upwards. --- ----@return EntityHandle[] +---@return EntityHandle[] # List of parent entities, from immediate parent up to the root. function CBaseEntity:GetParents() local parents = {} local parent = self:GetMoveParent() @@ -386,20 +388,20 @@ function CBaseEntity:DoNotDrop(enabled) end --- ----Get all criteria as a table. +---Get all criteria on this entity as a table. --- ----@return CriteriaTable +---@return CriteriaTable # A table of criteria key-value pairs. function CBaseEntity:GetCriteria() local c = {} self:GatherCriteria(c) return c end - --- ----Get all entities which are owned by this entity +---Get all entities owned by this entity. --- ---**Note:** This searches all entities in the map and should be used sparingly. ----@return EntityHandle[] +--- +---@return EntityHandle[] # List of owned entities. function CBaseEntity:GetOwnedEntities() local ents = {} local ent = Entities:First() @@ -413,9 +415,9 @@ function CBaseEntity:GetOwnedEntities() end --- ----Set the alpha modulation of this entity, plus any children that can have their alpha set. +---Set the alpha modulation of this entity, plus any children that support [SetRenderAlpha](lua://CBaseModelEntity.SetRenderAlpha). --- ----@param alpha integer +---@param alpha integer # Alpha value (0 = fully transparent, 255 = fully opaque). function CBaseModelEntity:SetRenderAlphaAll(alpha) for _, child in ipairs(self:GetChildren()) do if child.SetRenderAlpha then @@ -426,9 +428,9 @@ function CBaseModelEntity:SetRenderAlphaAll(alpha) end --- ----Center the entity at a new position. +---Moves the entity so that its center is at the given position. --- ----@param position Vector +---@param position Vector # The new center position. function CBaseEntity:SetCenter(position) local center = self:GetCenter() local origin = self:GetOrigin() @@ -441,10 +443,10 @@ end --- ----Set the origin of this entity around one of its attachment points. +---Set the entity's origin so that the specified attachment point aligns with the given world position. --- ----@param position Vector ----@param attachment string +---@param position Vector # The target world position for the attachment point. +---@param attachment string # The name of the attachment point to align. function CBaseAnimating:SetOriginByAttachment(position, attachment) local offset = self:GetAttachmentOrigin(self:ScriptLookupAttachment(attachment)) local origin = self:GetOrigin() @@ -543,37 +545,37 @@ function CEntityInstance:RedirectOutputFunc(output, func, entity) end --- ----Gets the position in front of the entity's eyes at the specified position. +---Gets the position in front of the entity’s eyes at the specified distance. --- ----@param distance number ----@return Vector +---@param distance number # How far in front of the eyes to get the position. +---@return Vector # The world position in front of the eyes. function CBaseEntity:DistanceFromEyes(distance) return self:EyePosition() + self:EyeAngles():Forward() * distance end --- ----Gets the origin of a named attachment. +---Gets the world origin position of a named attachment point. --- ----@param name string ----@return Vector +---@param name string # The name of the attachment. +---@return Vector # The world position of the attachment. function CBaseAnimating:GetAttachmentNameOrigin(name) return self:GetAttachmentOrigin(self:ScriptLookupAttachment(name)) end --- ----Gets the angles of a named attachment. +---Gets the world angles (rotation) of a named attachment point. --- ----@param name string ----@return Vector +---@param name string # The name of the attachment. +---@return Vector # The world rotation angles of the attachment. function CBaseAnimating:GetAttachmentNameAngles(name) return self:GetAttachmentAngles(self:ScriptLookupAttachment(name)) end --- ----Gets the forward vector of a named attachment. +---Gets the forward direction vector of a named attachment. --- ----@param name string ----@return Vector +---@param name string # The name of the attachment. +---@return Vector # The forward unit vector of the attachment in world space. function CBaseAnimating:GetAttachmentNameForward(name) return self:GetAttachmentForward(self:ScriptLookupAttachment(name)) end From b151e7f2ffdf469d521545dbc808443efd3d5ecb Mon Sep 17 00:00:00 2001 From: FrostSource Date: Mon, 30 Jun 2025 12:25:17 +1200 Subject: [PATCH 057/101] Correct angle interpolation and optimize updates Animations get the angle difference so QAngles are animated correctly --- scripts/vscripts/alyxlib/helpers/animation.lua | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/scripts/vscripts/alyxlib/helpers/animation.lua b/scripts/vscripts/alyxlib/helpers/animation.lua index 266993c..e147dad 100644 --- a/scripts/vscripts/alyxlib/helpers/animation.lua +++ b/scripts/vscripts/alyxlib/helpers/animation.lua @@ -148,24 +148,28 @@ Animation.Curves = { ---@return fun(time:number):boolean # New animation function function Animation:CreateAnimation(entity, getter, setter, targetValue, curveFunc) local startValue = getter(entity) + local adjustedTarget = targetValue local special = nil if IsVector(targetValue) then special = Vector elseif IsQAngle(targetValue) then special = QAngle + adjustedTarget = QAngle( + startValue.x + AngleDiff(targetValue.x, startValue.x), + startValue.y + AngleDiff(targetValue.y, startValue.y), + startValue.z + AngleDiff(targetValue.z, startValue.z) + ) end return function(t) local currentValue = getter(entity) if not special then - currentValue = curveFunc(startValue, targetValue, t) + currentValue = curveFunc(startValue, adjustedTarget, t) else - currentValue = special( - curveFunc(startValue.x, targetValue.x, t), - curveFunc(startValue.y, targetValue.y, t), - curveFunc(startValue.z, targetValue.z, t) - ) + currentValue.x = curveFunc(startValue.x, adjustedTarget.x, t) + currentValue.y = curveFunc(startValue.y, adjustedTarget.y, t) + currentValue.z = curveFunc(startValue.z, adjustedTarget.z, t) end setter(entity, currentValue) From 0d2abaf420b0b5fb955f82e5b199d376feaa20ae Mon Sep 17 00:00:00 2001 From: FrostSource Date: Mon, 30 Jun 2025 12:26:47 +1200 Subject: [PATCH 058/101] Add new animation curves easeInOutQuad easeOutBack easeOutQuart easeOutCubic --- .../vscripts/alyxlib/helpers/animation.lua | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/scripts/vscripts/alyxlib/helpers/animation.lua b/scripts/vscripts/alyxlib/helpers/animation.lua index e147dad..4510fee 100644 --- a/scripts/vscripts/alyxlib/helpers/animation.lua +++ b/scripts/vscripts/alyxlib/helpers/animation.lua @@ -132,6 +132,32 @@ Animation.Curves = { end end, + -- Smooth start and end, fast middle + easeInOutQuad = function(a, b, t) + if t < 0.5 then + return a + (b - a) * (2 * t * t) + else + return a + (b - a) * (1 - 2 * (1 - t) * (1 - t)) + end + end, + + -- Sharp snap with slight overshoot + easeOutBack = function(a, b, t) + local c1 = 1.70158 + local c3 = c1 + 1 + return a + (b - a) * (c3 * (t - 1)^3 + c1 * (t - 1)^2 + 1) + end, + + -- Fast start, gentle slow finish + easeOutQuart = function(a, b, t) + return a + (b - a) * (1 - (1 - t)^4) + end, + + -- Quick start, smooth deceleration + easeOutCubic = function(a, b, t) + return a + (b - a) * (1 - (1 - t)^3) + end, + } --- From 862734c2fd1a24c07444f611af6208bf1e00e73b Mon Sep 17 00:00:00 2001 From: FrostSource Date: Mon, 30 Jun 2025 12:27:17 +1200 Subject: [PATCH 059/101] Add CPropVRHand:Grab --- scripts/vscripts/alyxlib/player/hands.lua | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/scripts/vscripts/alyxlib/player/hands.lua b/scripts/vscripts/alyxlib/player/hands.lua index 3ac48ac..7c63542 100644 --- a/scripts/vscripts/alyxlib/player/hands.lua +++ b/scripts/vscripts/alyxlib/player/hands.lua @@ -45,6 +45,22 @@ function CPropVRHand:Drop() end end +---Grab the entity +---@param ent EntityHandle|string +function CPropVRHand:Grab(ent) + if type(ent) == "string" then + print('search for ' .. ent) + local name = ent + ent = Entities:FindByName(nil, name) + print(ent) + if ent == nil then + return warn("Could not find entity to grab with name " .. name) + end + end + + ent:Grab(self) +end + ---Get the rendered glove entity for this hand, i.e. the first `hlvr_prop_renderable_glove` class. ---@return EntityHandle|nil function CPropVRHand:GetGlove() From 36c163223266b24da7510f9e0cfbc149cf905a3c Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 1 Jul 2025 16:15:12 +1200 Subject: [PATCH 060/101] Add ent_rename command --- scripts/vscripts/alyxlib/debug/commands.lua | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/scripts/vscripts/alyxlib/debug/commands.lua b/scripts/vscripts/alyxlib/debug/commands.lua index fe7a812..8adad09 100644 --- a/scripts/vscripts/alyxlib/debug/commands.lua +++ b/scripts/vscripts/alyxlib/debug/commands.lua @@ -465,6 +465,25 @@ RegisterAlyxLibCommand("ent_find_by_address", function (_, tblpart, colon, hash) end end, "Prints info for an entity by its table address", 0) +--- +---Renames the first entity found using `pattern`. +--- +RegisterAlyxLibCommand("ent_rename", function(_, pattern, newName) + local ent = Debug.FindEntityByPattern(pattern) + if not ent then + warn("Could not find entity with pattern '"..pattern.."'") + return + end + + if newName == nil or newName == "" then + Msg("Removing name from " .. Debug.EntStr(ent)) + ent:SetEntityName("") + else + Msg("Renaming " .. Debug.EntStr(ent) .. " to '" .. newName .. "'") + ent:SetEntityName(newName) + end +end, "Renames the first entity found using a pattern to a new name", 0) + local symbols = {"and","break","do","else","elseif","end","false","for","function","if","in","local","nil","not","or","repeat","return","then","true","until","while"} -- if IsInToolsMode() then From 7518738fbf8815f3913037f10354bc2b349910b4 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Tue, 1 Jul 2025 16:15:35 +1200 Subject: [PATCH 061/101] Add CPropVRHand:GetHandUseController --- scripts/vscripts/alyxlib/player/hands.lua | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/scripts/vscripts/alyxlib/player/hands.lua b/scripts/vscripts/alyxlib/player/hands.lua index 7c63542..8af207a 100644 --- a/scripts/vscripts/alyxlib/player/hands.lua +++ b/scripts/vscripts/alyxlib/player/hands.lua @@ -98,6 +98,19 @@ function CPropVRHand:GetPalmPosition() end end +--- +---Gets the 'hand_use_controller' entity associated with this hand. +--- +---@return EntityHandle +function CPropVRHand:GetHandUseController() + for _, controller in ipairs(Entities:FindAllByClassname("hand_use_controller")) do + if controller:GetOwner() == self then + return controller + end +---@diagnostic disable-next-line: missing-return + end +end + ---Forces the player to drop this entity if held. ---@param self CBaseEntity function CBaseEntity:Drop() From faa3a04293d78992ad17e6b670c1b2d8fad89f2f Mon Sep 17 00:00:00 2001 From: FrostSource Date: Wed, 2 Jul 2025 16:11:55 +1200 Subject: [PATCH 062/101] Add debug menu scroll helpers Valve does not allow raytrace clicking outside the main menu so these buttons allow scrolling by hovering over the buttons as a work around --- .../layout/custom_game/alyxlib_debug_menu.xml | 19 ++++++++-- .../scripts/custom_game/alyxlib_debug_menu.js | 36 +++++++++++++++++++ .../styles/custom_game/alyxlib_debug_menu.css | 34 ++++++++++++++++++ 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/panorama/layout/custom_game/alyxlib_debug_menu.xml b/panorama/layout/custom_game/alyxlib_debug_menu.xml index 4f90d1b..9028795 100644 --- a/panorama/layout/custom_game/alyxlib_debug_menu.xml +++ b/panorama/layout/custom_game/alyxlib_debug_menu.xml @@ -57,12 +57,27 @@ + + - - + + diff --git a/panorama/scripts/custom_game/alyxlib_debug_menu.js b/panorama/scripts/custom_game/alyxlib_debug_menu.js index 4af8d92..6777238 100644 --- a/panorama/scripts/custom_game/alyxlib_debug_menu.js +++ b/panorama/scripts/custom_game/alyxlib_debug_menu.js @@ -551,6 +551,32 @@ function ParseCommand(command, args) } } +let scrollHelperScheduleCancel = false; +let scrollHelperScheduleEvent = ""; + +function ScrollHelperSchedule() { + if (scrollHelperScheduleCancel || currentlySelectedCategory === null) { + scrollHelperScheduleCancel = false; + return; + } + $.DispatchEvent(scrollHelperScheduleEvent, currentlySelectedCategory.panel); + $.Schedule(0.1, ScrollHelperSchedule); +} +function ScrollHelperClick() { + if (currentlySelectedCategory === null) return; + + switch (scrollHelperScheduleEvent){ + case "ScrollDown": + $.DispatchEvent("ScrollToBottom", currentlySelectedCategory.panel); + break; + + case "ScrollUp": + $.DispatchEvent("ScrollToTop", currentlySelectedCategory.panel); + break; + } + +} + (function() { // Modify preset layout buttons to work with controller trigger @@ -562,4 +588,14 @@ function ParseCommand(command, args) // This helps with hot reloading panel changes $.Schedule(0.1, () => FireOutput("_DebugMenuReloaded")); + // Scroll helpers for sub-menus + // Valve kindly didn't allow us to raytrace click panels like the main menu + // so this is a work around for scrolling + $('#ScrollHelperDown').SetPanelEvent("onmouseover", () =>{ $.Schedule(0.1, ScrollHelperSchedule); scrollHelperScheduleEvent="ScrollDown"}); + $('#ScrollHelperDown').SetPanelEvent("onmouseout", () => scrollHelperScheduleCancel = true); + $('#ScrollHelperDown').SetPanelEvent("onactivate", ScrollHelperClick); + $('#ScrollHelperUp').SetPanelEvent("onmouseover", () =>{ $.Schedule(0.1, ScrollHelperSchedule); scrollHelperScheduleEvent="ScrollUp"}); + $('#ScrollHelperUp').SetPanelEvent("onmouseout", () => scrollHelperScheduleCancel = true); + $('#ScrollHelperUp').SetPanelEvent("onactivate", ScrollHelperClick); + })(); \ No newline at end of file diff --git a/panorama/styles/custom_game/alyxlib_debug_menu.css b/panorama/styles/custom_game/alyxlib_debug_menu.css index 21c4e26..104aa21 100644 --- a/panorama/styles/custom_game/alyxlib_debug_menu.css +++ b/panorama/styles/custom_game/alyxlib_debug_menu.css @@ -260,3 +260,37 @@ Label.custom_label padding-right: 24px; } + +.col_body +{ + /* Allows the scroll helper to show */ + min-height: 631px; +} + +.scroll_helper { + width: 100%; + height: 53px; + flow-children:down; + border-top: 2px solid #FFBE55; +} +.scroll_helper.down .chevron_arrow{ + transform: rotateZ( 90deg ); +} +.scroll_helper.up .chevron_arrow{ + transform: rotateZ( -90deg ); +} + +.chevron_arrow{ + margin-left: 10px; + margin-right: 10px; + background-color: #FFBE55; + height: 40px; + width: 40px; + horizontal-align: center; + vertical-align: center; + + opacity-mask: url("s2r://panorama/images/upgrade_station_ui/crafting_station_chevron_mask_psd.vtex"); +} +.ScrollDownButton:hover .chevron_arrow{ + background-color:#fff; +} \ No newline at end of file From 4fe21b2adf6b00f5934280967a9e397bd6aab414 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Wed, 2 Jul 2025 16:17:04 +1200 Subject: [PATCH 063/101] Update menu attachment on primary hand change --- scripts/vscripts/alyxlib/debug/debug_menu.lua | 65 +++++++++++-------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index 8bb5a31..149b6db 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -92,12 +92,30 @@ local debugPanelScriptScope = { end } +--- +---Updates the physical menu by attaching it to the correct hand. +--- +function DebugMenu:UpdateMenuAttachment() + local hand = Convars:GetBool("alyxlib_debug_menu_hand") and Player.PrimaryHand or Player.SecondaryHand + if hand == Player.RightHand then + self.panel:SetParent(hand, "constraint1") + self.panel:ResetLocal() + self.panel:SetLocalAngles(0, 180, 0) + self.panel:SetLocalOrigin(Vector(4, -9, 0)) + else + self.panel:SetParent(hand, "constraint1") + self.panel:ResetLocal() + self.panel:SetLocalAngles(0, 0, 0) + self.panel:SetLocalOrigin(Vector(4, 9, 0)) + end +end + --- ---Creates and displays the debug menu panel on the player's chosen hand. --- function DebugMenu:ShowMenu() - local menu = SpawnEntityFromTableSynchronous("point_clientui_world_panel", { + self.panel = SpawnEntityFromTableSynchronous("point_clientui_world_panel", { targetname = "alyxlib_debug_menu", dialog_layout_name = "file://{resources}/layout/custom_game/alyxlib_debug_menu.xml", width = 16,--24, @@ -118,25 +136,10 @@ function DebugMenu:ShowMenu() local dir = localPlayer:EyeAngles():Forward() local a = VectorToAngles(dir) a = RotateOrientation(a, QAngle(0,-90,90)) - menu:SetQAngle(a) - menu:SetOrigin(eyePos + dir * 16) + self.panel:SetQAngle(a) + self.panel:SetOrigin(eyePos + dir * 16) else - if Convars:GetInt("alyxlib_debug_menu_hand") == 1 then - menu:SetParent(Player.PrimaryHand, "constraint1") - menu:ResetLocal() - menu:SetLocalAngles(0, 180, 0) - menu:SetLocalOrigin(Vector(4, -9, 0)) - -- menu:SetLocalAngles(0,0,0) - -- menu:SetLocalAngles(40,-10,10) - -- menu:SetLocalOrigin(Vector(0,8,-2)) - else - menu:SetParent(Player.SecondaryHand, "constraint1") - menu:ResetLocal() - menu:SetLocalAngles(0, 0, 0) - menu:SetLocalOrigin(Vector(4, 9, 0)) - -- menu:SetLocalAngles(40,-10,10) - -- menu:SetLocalOrigin(Vector(0,8,-2)) - end + self:UpdateMenuAttachment() -- Cough handpose gets in the way for close menus Player:SetCoughHandEnabled(false) @@ -149,19 +152,22 @@ function DebugMenu:ShowMenu() self:ClickHoveredButton() end, self) + handChangedListener = ListenToPlayerEvent("primary_hand_changed", function() + self:UpdateMenuAttachment() + end) + end - menu:AddCSSClasses("Visible") + self.panel:AddCSSClasses("Visible") - local scope = menu:GetOrCreatePrivateScriptScope() + local scope = self.panel:GetOrCreatePrivateScriptScope() vlua.tableadd(scope, debugPanelScriptScope) - menu:AddOutput("CustomOutput0", "!self", "RunScriptCode") + self.panel:AddOutput("CustomOutput0", "!self", "RunScriptCode") - Panorama:InitPanel(menu, "alyxlib_debug_menu") - self.panel = menu + Panorama:InitPanel(self.panel, "alyxlib_debug_menu") - menu:Delay(function() + self.panel:Delay(function() debugMenuOpen = true end, 0.2) @@ -178,6 +184,11 @@ function DebugMenu:CloseMenu() debugMenuOpen = false + if handChangedListener ~= nil then + StopListeningToPlayerEvent(handChangedListener) + handChangedListener = nil + end + Input:StopListeningByContext(self) Player:SetCoughHandEnabled(true) @@ -465,7 +476,9 @@ function DebugMenu:StartListeningForMenuActivation() timeSinceLastButtonPress = math.huge end - if Player:IsDigitalActionOnForHand(Player.SecondaryHand.Literal, DIGITAL_INPUT_TOGGLE_MENU) then + local hand = Convars:GetBool("alyxlib_debug_menu_hand") and Player.SecondaryHand or Player.PrimaryHand + + if Player:IsDigitalActionOnForHand(hand.Literal, DIGITAL_INPUT_TOGGLE_MENU) then if not buttonPressed then buttonPressed = true timeSinceLastButtonPress = Time() From 76a37eb29fe7b4f7560bc959371d85aefccadda3 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Wed, 2 Jul 2025 16:18:04 +1200 Subject: [PATCH 064/101] Add new debug menu buttons Left Handed Give 999 Ammo Give 999 Resin --- scripts/vscripts/alyxlib/debug/debug_menu.lua | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index 149b6db..0940786 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -520,6 +520,20 @@ DebugMenu:AddToggle("alyxlib", "alyxlib_noclip_vr", "NoClip VR", "noclip_vr") DebugMenu:AddToggle("alyxlib", "alyxlib_godmode", "God Mode", "god") +DebugMenu:AddToggle("alyxlib", "alyxlib_lefthanded", "Left Handed", "hlvr_left_hand_primary") + +DebugMenu:AddSeparator("alyxlib") + +DebugMenu:AddButton("alyxlib", "alyxlib_giveammo", "Give 999 Ammo", function() + SendToConsole("hlvr_setresources 999 999 999 " .. Player:GetResin()) +end) + +DebugMenu:AddButton("alyxlib", "alyxlib_giveresin", "Give 999 Resin", function() + SendToConsole("hlvr_addresources 0 0 0 " .. (999 - Player:GetResin())) +end) + +DebugMenu:AddSeparator("alyxlib") + local isRecordingDemo = false local currentDemo = "" From 48a07788209908cfb43588b21813a7991fcc7e8d Mon Sep 17 00:00:00 2001 From: FrostSource Date: Wed, 2 Jul 2025 16:18:55 +1200 Subject: [PATCH 065/101] Lower presses to activate debug menu from 5 to 3 --- scripts/vscripts/alyxlib/debug/debug_menu.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index 0940786..be94ed7 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -464,7 +464,7 @@ end ---Starts listening for the debug menu activation button. --- function DebugMenu:StartListeningForMenuActivation() - local buttonPressesToActivate = 5 + local buttonPressesToActivate = 3 local buttonPresses = 0 local timeToResetBetweenPresses = 0.6 local buttonPressed = false From 9654a06e53e741fc3447040123ca1041ab2b4542 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Wed, 2 Jul 2025 16:19:24 +1200 Subject: [PATCH 066/101] Try to get default value from string commands --- scripts/vscripts/alyxlib/debug/debug_menu.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index be94ed7..c2e42d7 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -329,6 +329,7 @@ function DebugMenu:AddToggle(categoryId, toggleId, text, command, startsOn) local callback if type(command) == "string" then + startsOn = startsOn or Convars:GetBool(command) or false callback = function(on) SendToConsole(command .. " " .. (on and 1 or 0)) end From aa4d43b192022d4ce66676735543460716cbb66d Mon Sep 17 00:00:00 2001 From: FrostSource Date: Wed, 2 Jul 2025 16:19:56 +1200 Subject: [PATCH 067/101] Check for ListenToPlayerEvent or ListenToGameEvent --- scripts/vscripts/alyxlib/debug/debug_menu.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index c2e42d7..e7a93f7 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -38,6 +38,7 @@ DebugMenu.panel = nil DebugMenu.categories = {} local debugMenuOpen = false +local handChangedListener = nil --- ---The scope of the debug menu script. @@ -506,11 +507,12 @@ function DebugMenu:StopListeningForMenuActivation() end if Convars:GetInt("developer") > 0 then - ListenToPlayerEvent("vr_player_ready", function() + local listenFunc = ListenToPlayerEvent or ListenToGameEvent + listenFunc("vr_player_ready", function() Player:Delay(function() DebugMenu:StartListeningForMenuActivation() end, 0.2) - end) + end, nil) end -- AlyxLib defaults From 5af3ca6e280a997b6fc3785b98f40a23eca00bc4 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Wed, 2 Jul 2025 16:21:36 +1200 Subject: [PATCH 068/101] Fix input hand not changing from left handed is_primary_left was being treated as a boolean but it's an integer, so secondary hand was never being converted back to primary hand --- scripts/vscripts/alyxlib/controls/input.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vscripts/alyxlib/controls/input.lua b/scripts/vscripts/alyxlib/controls/input.lua index a1521c5..cae8ae6 100644 --- a/scripts/vscripts/alyxlib/controls/input.lua +++ b/scripts/vscripts/alyxlib/controls/input.lua @@ -157,7 +157,7 @@ end ListenToGameEvent("primary_hand_changed", function(data) ---@cast data GameEventPrimaryHandChanged - updatePrimaryHandId(data.is_primary_left and 0 or 1) + updatePrimaryHandId(1 - data.is_primary_left) end, nil) --- From a501285717bec435a0dbd0055b329f6fbb672ef9 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Wed, 2 Jul 2025 16:21:49 +1200 Subject: [PATCH 069/101] Add debug menu title --- panorama/layout/custom_game/alyxlib_debug_menu.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panorama/layout/custom_game/alyxlib_debug_menu.xml b/panorama/layout/custom_game/alyxlib_debug_menu.xml index 9028795..3551557 100644 --- a/panorama/layout/custom_game/alyxlib_debug_menu.xml +++ b/panorama/layout/custom_game/alyxlib_debug_menu.xml @@ -22,7 +22,7 @@ - From d698d7d613b10fa9dae58869bc95bb4b6e95a799 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Wed, 2 Jul 2025 16:22:25 +1200 Subject: [PATCH 070/101] Increase interact distance from 8 to 12 --- scripts/vscripts/alyxlib/debug/debug_menu.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index e7a93f7..6372c09 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -124,7 +124,7 @@ function DebugMenu:ShowMenu() panel_dpi = 64, ignore_input = 0, lit = 0, - interact_distance = 8, + interact_distance = 12, vertical_align = "1", -- orientation = "0", From 329c24d0dbf1eefd9bb101b82f97bb75aaedd6d1 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 3 Jul 2025 15:01:54 +1200 Subject: [PATCH 071/101] Turn scroll helpers into menu buttons --- panorama/scripts/custom_game/alyxlib_debug_menu.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/panorama/scripts/custom_game/alyxlib_debug_menu.js b/panorama/scripts/custom_game/alyxlib_debug_menu.js index 6777238..53ac5e4 100644 --- a/panorama/scripts/custom_game/alyxlib_debug_menu.js +++ b/panorama/scripts/custom_game/alyxlib_debug_menu.js @@ -583,6 +583,8 @@ function ScrollHelperClick() { TurnButtonIntoDebugMenuButton($("#CloseMenuButton")); TurnButtonIntoDebugMenuButton($("#CycleCategoryLeftButton")); TurnButtonIntoDebugMenuButton($("#CycleCategoryRightButton")); + TurnButtonIntoDebugMenuButton($("#ScrollHelperDown")); + TurnButtonIntoDebugMenuButton($("#ScrollHelperUp")); // Tells Lua that the menu has been reloaded so it can repopulate the menu // This helps with hot reloading panel changes From 1d64f55cee7e6d1782ce7a8aecc531acf824d3d5 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 3 Jul 2025 15:02:29 +1200 Subject: [PATCH 072/101] Dynamically scale category text to fit tab better --- .../scripts/custom_game/alyxlib_debug_menu.js | 15 +++++++++++++++ .../styles/custom_game/alyxlib_debug_menu.css | 8 +++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/panorama/scripts/custom_game/alyxlib_debug_menu.js b/panorama/scripts/custom_game/alyxlib_debug_menu.js index 53ac5e4..f4b31da 100644 --- a/panorama/scripts/custom_game/alyxlib_debug_menu.js +++ b/panorama/scripts/custom_game/alyxlib_debug_menu.js @@ -84,6 +84,9 @@ class Category constructor(id, name) { this.id = id; + /** + * @type {string} + */ this.name = name; /** @@ -109,6 +112,18 @@ class Category this.button = CreateDebugMenuButton($("#CategoryBar"), () => SetCategoryVisible(this.id), "CategoryButton", `${this.id}_button`); let label = $.CreatePanel("Label", this.button, `${this.id}_label`); label.text = this.name; + + // Scale text size to fit button + + // Width of CategoryButton + let containerWidth = 150; + // Good factor for AlyxLib text + let baseFactor = 5; + // Calculate a scaled factor that grows slowly + let factor = baseFactor * Math.max(label.text.length / 17, 1); // never less than 1, so no shrinking below base + // Clamp to avoid too small or too big + factor = Math.min(Math.max(factor, baseFactor), 10); + label.style.fontSize = `${containerWidth / factor}px`; } /** diff --git a/panorama/styles/custom_game/alyxlib_debug_menu.css b/panorama/styles/custom_game/alyxlib_debug_menu.css index 104aa21..c673844 100644 --- a/panorama/styles/custom_game/alyxlib_debug_menu.css +++ b/panorama/styles/custom_game/alyxlib_debug_menu.css @@ -145,7 +145,7 @@ Button.custom_button_with_subtitle.error_msg:not(.disabled) Label.button_label.s } .CategoryButton { - width: 20%; + width: 150px; height: 100%; /* background-color: red; */ horizontal-align: left; @@ -162,11 +162,13 @@ Button.custom_button_with_subtitle.error_msg:not(.disabled) Label.button_label.s { text-align: left; horizontal-align: center; - /* white-space: nowrap; */ - /* text-overflow: ellipsis; */ + white-space: normal; + text-overflow: ellipsis; + line-height: 20px; overflow: clip; font-size: 30px; text-align: center; + margin: 5px; /* background-color: green; */ } From baed9ea6c64d8ec436c0e6c84d7132a44359a2c4 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 3 Jul 2025 16:25:29 +1200 Subject: [PATCH 073/101] Make newly added categories flash --- .../scripts/custom_game/alyxlib_debug_menu.js | 9 ++++++ .../styles/custom_game/alyxlib_debug_menu.css | 32 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/panorama/scripts/custom_game/alyxlib_debug_menu.js b/panorama/scripts/custom_game/alyxlib_debug_menu.js index f4b31da..f36e9ea 100644 --- a/panorama/scripts/custom_game/alyxlib_debug_menu.js +++ b/panorama/scripts/custom_game/alyxlib_debug_menu.js @@ -4,6 +4,8 @@ if(false)p=require("./panoramadoc"); +let panelReady = false; + /** * Fires a Panorama output with the given name and arguments. * The output is routed to the panel's input 'RunScriptCode'. @@ -110,6 +112,11 @@ class Category // Create category button this.button = CreateDebugMenuButton($("#CategoryBar"), () => SetCategoryVisible(this.id), "CategoryButton", `${this.id}_button`); + + // Animate this new tab if being added after the menu is open + if (panelReady) + this.button.AddClass("flash"); + let label = $.CreatePanel("Label", this.button, `${this.id}_label`); label.text = this.name; @@ -615,4 +622,6 @@ function ScrollHelperClick() { $('#ScrollHelperUp').SetPanelEvent("onmouseout", () => scrollHelperScheduleCancel = true); $('#ScrollHelperUp').SetPanelEvent("onactivate", ScrollHelperClick); + $.Schedule(1.0, () => panelReady = true); + })(); \ No newline at end of file diff --git a/panorama/styles/custom_game/alyxlib_debug_menu.css b/panorama/styles/custom_game/alyxlib_debug_menu.css index c673844..3fdf5fd 100644 --- a/panorama/styles/custom_game/alyxlib_debug_menu.css +++ b/panorama/styles/custom_game/alyxlib_debug_menu.css @@ -171,6 +171,38 @@ Button.custom_button_with_subtitle.error_msg:not(.disabled) Label.button_label.s margin: 5px; /* background-color: green; */ } +.CategoryButton.flash { + animation-name: FlashOnce; + animation-duration: 0.5s; + animation-timing-function: ease; + animation-iteration-count: 1; +} + +@define flashColor: #FFBE5588; + +@keyframes 'FlashOnce' +{ + 0% { + background-color: flashColor; + transform: translateY(0); + } + 30% { + background-color: flashColor; + transform: translateY(-20px); + } + 50% { + background-color: #FFBE5544; + transform: translateY(5px); + } + 70% { + background-color: #FFBE5511; + transform: translateY(-8px); + } + 100% { + background-color: transparent; + transform: translateY(0); + } +} Button.disabled Label { From d6a015bb49e43f1c3e0b186e34e075eb416da975 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 3 Jul 2025 16:26:48 +1200 Subject: [PATCH 074/101] Update SetCategoryIndex immediately --- .../scripts/custom_game/alyxlib_debug_menu.js | 28 +++++++++++++++++++ scripts/vscripts/alyxlib/debug/debug_menu.lua | 4 +++ 2 files changed, 32 insertions(+) diff --git a/panorama/scripts/custom_game/alyxlib_debug_menu.js b/panorama/scripts/custom_game/alyxlib_debug_menu.js index f36e9ea..5bf9767 100644 --- a/panorama/scripts/custom_game/alyxlib_debug_menu.js +++ b/panorama/scripts/custom_game/alyxlib_debug_menu.js @@ -570,6 +570,34 @@ function ParseCommand(command, args) categories = []; break; } + + case "setcategoryindex": { + let category = GetCategory(args[0]); + if (category === null) + { + $.Msg(`Category ${args[0]} does not exist!`); + break; + } + + const index = parseInt(args[1]); + const categoryBar = $("#CategoryBar"); + const childToMove = category.button; + if (index <= 0){ + // Move to front + let firstChild = categoryBar.GetChild(0); + categoryBar.MoveChildBefore(childToMove, firstChild); + } else { + // Move after the previous child + let prevChild = categoryBar.GetChild(index - 1); + categoryBar.MoveChildAfter(childToMove, prevChild) + } + + const currentPos = categories.indexOf(category); + if (currentPos !== -1 && index >= 0 && index < categories.length) { + categories.splice(currentPos, 1); + categories.splice(index, 0, category); + } + } } } diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index 6372c09..b1019b0 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -408,6 +408,10 @@ function DebugMenu:SetCategoryIndex(categoryId, index) table.remove(self.categories, currentIndex) table.insert(self.categories, index, category) + + if self.panel then + Panorama:Send(self.panel, "SetCategoryIndex", categoryId, index-1) + end end --- From 7b8d2bc85259999a01a92fc22853a50f4a71197c Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 3 Jul 2025 16:27:49 +1200 Subject: [PATCH 075/101] Add SendCategoryToPanel --- scripts/vscripts/alyxlib/debug/debug_menu.lua | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index b1019b0..b1abdcc 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -415,18 +415,18 @@ function DebugMenu:SetCategoryIndex(categoryId, index) end --- ----Forces the debug menu panel to add all categories and items. +---Sends a category and all its elements to the panel. --- ---This should only be used if modifying the menu in a non-standard way. --- -function DebugMenu:SendCategoriesToPanel() +---@param category DebugMenuCategory +function DebugMenu:SendCategoryToPanel(category) if not self.panel then return end local panel = self.panel - for categoryId, category in pairs(DebugMenu.categories) do Panorama:Send(panel, "AddCategory", category.id, category.name) for _, item in ipairs(category.items) do @@ -442,6 +442,20 @@ function DebugMenu:SendCategoriesToPanel() warn("Unknown item type '"..item.type.."'") end end +end + +--- +---Forces the debug menu panel to add all categories and items. +--- +---This should only be used if modifying the menu in a non-standard way. +--- +function DebugMenu:SendCategoriesToPanel() + if not self.panel then + return + end + + for categoryId, category in pairs(DebugMenu.categories) do + self:SendCategoryToPanel(category) end end From c486459e810d8df2ef218e482de88d6294cf1005 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 3 Jul 2025 16:28:23 +1200 Subject: [PATCH 076/101] Ignore debug menu activation in novr --- scripts/vscripts/alyxlib/debug/debug_menu.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index b1abdcc..04723a5 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -484,6 +484,8 @@ end ---Starts listening for the debug menu activation button. --- function DebugMenu:StartListeningForMenuActivation() + if Player.HMDAvatar == nil then return end + local buttonPressesToActivate = 3 local buttonPresses = 0 local timeToResetBetweenPresses = 0.6 From 88c049ff530c288ada336edf6786b05926ac6603 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 3 Jul 2025 16:51:10 +1200 Subject: [PATCH 077/101] Make switches smaller --- panorama/styles/custom_game/alyxlib_debug_menu.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/panorama/styles/custom_game/alyxlib_debug_menu.css b/panorama/styles/custom_game/alyxlib_debug_menu.css index 3fdf5fd..7cbd793 100644 --- a/panorama/styles/custom_game/alyxlib_debug_menu.css +++ b/panorama/styles/custom_game/alyxlib_debug_menu.css @@ -225,7 +225,8 @@ Button.custom_switch:active .custom_switch { width: 100%; - min-height: 116px; + min-height: 80px; + max-height: 90px; /* This is confusing. Why can't I apply this style further up the heirarchy? */ flow-children: right; horizontal-align: right; From f16d8a48a4081e46c663082504a465468bcedd5b Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 3 Jul 2025 16:51:26 +1200 Subject: [PATCH 078/101] Make scroll helpers smaller --- .../styles/custom_game/alyxlib_debug_menu.css | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/panorama/styles/custom_game/alyxlib_debug_menu.css b/panorama/styles/custom_game/alyxlib_debug_menu.css index 7cbd793..a17c33e 100644 --- a/panorama/styles/custom_game/alyxlib_debug_menu.css +++ b/panorama/styles/custom_game/alyxlib_debug_menu.css @@ -299,14 +299,19 @@ Label.custom_label .col_body { /* Allows the scroll helper to show */ - min-height: 631px; + min-height: 657px; } .scroll_helper { width: 100%; - height: 53px; + height: 36px; + min-height: 0px; flow-children:down; - border-top: 2px solid #FFBE55; +} +.scroll_helper.down { + border-top: 2px solid #FFBE5577; +}.scroll_helper.up { + border-bottom: 2px solid #FFBE5577; } .scroll_helper.down .chevron_arrow{ transform: rotateZ( 90deg ); @@ -319,8 +324,8 @@ Label.custom_label margin-left: 10px; margin-right: 10px; background-color: #FFBE55; - height: 40px; - width: 40px; + height: 32px; + width: 32px; horizontal-align: center; vertical-align: center; From 17e20706d99761008f18cf95e64ebe13ccf2d704 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 3 Jul 2025 16:53:55 +1200 Subject: [PATCH 079/101] Add debug_menu_extras A normally hidden menu tab that has extra debug options with more complex development commands --- scripts/vscripts/alyxlib/debug/debug_menu.lua | 10 ++++++ .../alyxlib/debug/debug_menu_extras.lua | 34 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 scripts/vscripts/alyxlib/debug/debug_menu_extras.lua diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index 04723a5..b23c63a 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -575,4 +575,14 @@ DebugMenu:AddButton("alyxlib", "alyxlib_demo_recording", "Start Recording Demo", isRecordingDemo = true DebugMenu:SetItemText("alyxlib", "alyxlib_demo_recording", "Stop Recording Demo") end +end) + +DebugMenu:AddSeparator("alyxlib") + +DebugMenu:AddButton("alyxlib", "alyxlib_enableextras", "Enable Extras Tab", function() + require "alyxlib.debug.debug_menu_extras" + -- Update the panel immediately + local id = "alyxlib_extras" + DebugMenu:SendCategoryToPanel(DebugMenu:GetCategory(id)) + DebugMenu:SetCategoryIndex(id, 2) end) \ No newline at end of file diff --git a/scripts/vscripts/alyxlib/debug/debug_menu_extras.lua b/scripts/vscripts/alyxlib/debug/debug_menu_extras.lua new file mode 100644 index 0000000..da77469 --- /dev/null +++ b/scripts/vscripts/alyxlib/debug/debug_menu_extras.lua @@ -0,0 +1,34 @@ +--[[ + v1.0.0 + https://github.com/FrostSource/alyxlib + + Extra debug menu category which can be enabled via the AlyxLib debug menu tab. +]] + +local version = "v1.0.0" + +require "alyxlib.debug.debug_menu" + +local id = "alyxlib_extras" + +DebugMenu:AddCategory(id, "AlyxLib Extras") + +DebugMenu:AddToggle(id, id.."_showtriggers", "Show Triggers", "showtriggers") + +DebugMenu:AddToggle(id, id.."_luxels", "Mat Luxels", "mat_luxels") + +DebugMenu:AddToggle(id, id.."_fullbright", "Fullbright", "mat_fullbright") + +DebugMenu:AddToggle(id, id.."_visfreeze", "Freeze Vis", function(on) + if on then + SendToConsole("vis_debug_show 1") + Player:Delay(function() + SendToConsole("vis_debug_lock 1") + end) + else + SendToConsole("vis_debug_show 0") + SendToConsole("vis_debug_lock 0") + end +end) + +return version \ No newline at end of file From 7c62f13161ab1249c7f2d2306de850a2153bdce9 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 3 Jul 2025 16:54:07 +1200 Subject: [PATCH 080/101] Indentation --- scripts/vscripts/alyxlib/debug/debug_menu.lua | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index b23c63a..d31924c 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -427,21 +427,21 @@ function DebugMenu:SendCategoryToPanel(category) local panel = self.panel - Panorama:Send(panel, "AddCategory", category.id, category.name) - - for _, item in ipairs(category.items) do - if item.type == "toggle" then - Panorama:Send(panel, "AddToggle", item.categoryId, item.id, item.text, item.default) - elseif item.type == "button" then - Panorama:Send(panel, "AddButton", item.categoryId, item.id, item.text) - elseif item.type == "label" then - Panorama:Send(panel, "AddLabel", item.categoryId, item.id, item.text) - elseif item.type == "separator" then - Panorama:Send(panel, "AddSeparator", item.categoryId) - else - warn("Unknown item type '"..item.type.."'") - end + Panorama:Send(panel, "AddCategory", category.id, category.name) + + for _, item in ipairs(category.items) do + if item.type == "toggle" then + Panorama:Send(panel, "AddToggle", item.categoryId, item.id, item.text, item.default) + elseif item.type == "button" then + Panorama:Send(panel, "AddButton", item.categoryId, item.id, item.text) + elseif item.type == "label" then + Panorama:Send(panel, "AddLabel", item.categoryId, item.id, item.text) + elseif item.type == "separator" then + Panorama:Send(panel, "AddSeparator", item.categoryId) + else + warn("Unknown item type '"..item.type.."'") end + end end --- From e12932d6304482f9193202fe7e7a6c2b91613dbc Mon Sep 17 00:00:00 2001 From: FrostSource Date: Fri, 11 Jul 2025 16:34:20 +1200 Subject: [PATCH 081/101] Add debug menu slider and cycler --- .../scripts/custom_game/alyxlib_debug_menu.js | 447 +++++++++++++++++- .../styles/custom_game/alyxlib_debug_menu.css | 63 ++- scripts/vscripts/alyxlib/debug/debug_menu.lua | 253 +++++++++- 3 files changed, 735 insertions(+), 28 deletions(-) diff --git a/panorama/scripts/custom_game/alyxlib_debug_menu.js b/panorama/scripts/custom_game/alyxlib_debug_menu.js index 5bf9767..39f26b5 100644 --- a/panorama/scripts/custom_game/alyxlib_debug_menu.js +++ b/panorama/scripts/custom_game/alyxlib_debug_menu.js @@ -21,7 +21,7 @@ function FireOutput(outputName, ...args) { const callString = `${outputName}(${formattedArgs.join(",")})`; $.DispatchEvent("ClientUI_FireOutputStr", 0, callString); - $.Msg(callString); + // $.Msg(callString); } /** @@ -66,14 +66,32 @@ function CreateDebugMenuButton(parent, callback, _class, id) { let button = $.CreatePanel("Button", parent, id); button.AddClass(_class); - if (callback !== null && callback !== undefined) - button.SetPanelEvent("onactivate", callback); - TurnButtonIntoDebugMenuButton(button); + TurnButtonIntoDebugMenuButton(button, callback); return button; } +/** + * Create a panel, optionally with a set of classes. + * @param {string} type Type of panel (e.g. Panel, Button). + * @param {Panel} parent Parent of this new panel. + * @param {string?} id Id of this new panel. + * @param {string?} classes Classes for this panel. + * @returns {Panel} + */ +function CreatePanel(type, parent, id, classes) +{ + id = id || ''; + const panel = $.CreatePanel(type, parent, id); + if (classes !== undefined) { + for (let _class of classes.split(' ')) { + panel.AddClass(_class); + } + } + return panel; +} + /** * @interface * @typedef {Object} SubMenuItem @@ -220,8 +238,45 @@ class Category let rowDividerLine = $.CreatePanel("Panel", rowDividerLabel, undefined); rowDividerLine.AddClass("row_divider_line"); - let rowDividerBullet = $.CreatePanel("Panel", rowDivider, undefined); - rowDividerBullet.AddClass("button_bullet"); + } + + /** + * Adds a new slider to this category. + * @param {string} id The id for this slider. + * @param {string} text Text to display in the slider. + * @param {string} convar The convar to tie this slider to. + * @param {number} min Minimum value this slider can have. + * @param {number} max Maximum value this slider can have. + * @param {number} value Starting value for this slider. + * @param {boolean} isPercentage Value is displayed as a percentage instead of raw value. + * @param {number} truncate Number of decimal places the value can be set to (-1 for no truncating). + * @param {number} increment Increment value to snap to. + */ + AddSlider(id, text, convar, min, max, value, isPercentage = true, truncate = -1, increment = 0) + { + + let slider = new SubMenuSlider(`${this.id}_${id}`, convar, text, min, max, isPercentage, (value) => { + FireOutput("_DebugMenuCallbackSlider", id, value); + }, value, truncate, increment); + slider.AddToPanel(this.content); + this.items.push(slider); + } + + /** + * Adds a new value cycler to this category. + * @param {string} id String id for this cycle. + * @param {string} convar Convar to tie this cycle to (currently unsued in JS). + * @param {SubMenuCycleItem[]} values Text/value pairs for this cycle. + * @param {string?} selectedValue Starting selected value. + */ + AddCycle(id, convar, values, selectedValue) + { + let cycle = new SubMenuCycle(`${this.id}_${id}`, convar, values, (index) => { + FireOutput("_DebugMenuCallbackCycle", id, index + 1); + }); + cycle.AddToPanel(this.content); + cycle.SetSelectedValueNoFire(selectedValue); + this.items.push(cycle); } } @@ -363,6 +418,278 @@ class SubMenuToggle } } +class SubMenuSlider +{ + constructor(id, convar, text, min, max, isPercentage, callback, currentValue = 0, truncate = -1, increment = 0) { + this.id = id; + this.convar = convar; + this.text = text; + this.min = min; + this.max = max; + this.isPercentage = false; + this.callback = callback; + + this.truncate = truncate; + this.increment = increment; + + this.value = currentValue;//Clamp(currentValue || 0, this.min, this.max); + /**@type {Panel} */ + this.panel = null; + /**@type {Slider} */ + this.slider = null; + } + + AddToPanel(panel) { + this.panel = CreatePanel("Panel", panel, this.id, "SubMenuSlider"); + this.slider = $.CreatePanelWithProperties("Slider", this.panel, "Slider", { class:"OptionsSlider", direction:"horizontal", }); + this.slider.min = this.min; + this.slider.max = this.max; + const container = CreatePanel("Panel", this.slider, null, "SliderContainer"); + const row = CreatePanel("Panel", container, "SliderRow", "SliderRow"); + const textLabel = CreatePanel("Label", row, "Title", "slider_label"); + textLabel.text = this.text; + CreatePanel("Panel", row, null, "slider_divider"); + const valueLabel = CreatePanel("Label", row, "Value", "slider_value"); + + this.SetValue(this.value); + + // This only works in VR + TurnButtonIntoDebugMenuButton(this.panel, () => { + const pos = GetAffordancePosition(); + if (pos !== null) { + // Slider does not have actualxoffset or actuallayoutwidth THANKS AGAIN VALVE + // These magic numbers are estimates of where the slider is in relation to the parent panel + const xoffset = this.panel.actualxoffset + 25; + const width = this.panel.actuallayoutwidth - 25; + const val = RemapValueClamped(pos.x, + xoffset, + width, + this.slider.min, + this.slider.max + ); + // Changing value automatically fires "onvaluechanged" + this.slider.value = val; + } + }); + + this.slider.SetPanelEvent("onvaluechanged", () => { + let prevValue = this.value; + this._SetValueInternal(this.slider.value); + if (this.value !== prevValue) + this.callback(this.value); + }); + } + + /** + * Sets the slider to a given value. + * @param {number} value + */ + SetValue(value) { + this._SetValueInternal(value); + this.slider.SetValueNoEvents(this.value); + // this.slider.value = this.value; + } + + /** + * Sets the internal value of the slider and updates labels. + * The visible slider is not updated. + * @param {number} value + */ + _SetValueInternal(value) { + const valueLabel = this.panel.FindChildTraverse("Value"); + if (this.increment > 0) value = Math.round(value / this.increment) * this.increment; + if (this.truncate > -1) value = parseFloat(value.toFixed(this.truncate)); + value = Clamp(value, this.min, this.max); + this.value = value; + + if (this.isPercentage) + valueLabel.text = this.GetValueAsPercentage().toFixed(0); + else + valueLabel.text = this.value.toFixed(this.truncate); + } + + /** + * + * @returns {number} + */ + GetValueAsPercentage() { + return RemapValueClamped(this.value, this.min, this.max, 0, 100); + } + + SetText(text) { + const /**@type {Label} */ title = this.panel.FindChildTraverse("Title"); + if (title) + title.text = text; + } +} + +/** + * @typedef {Object} SubMenuCycleItem + * @property {string} text + * @property {string?} value + */ + +class SubMenuCycle +{ + /** + * + * @param {string} id + * @param {string} convar + * @param {SubMenuCycleItem[]} values Maximum of 6 items + * @param {function} callback + * @param {number} selectedIndex + */ + constructor(id, convar, values, callback, selectedIndex) { + this.id = id; + this.convar = convar; + this.values = values;//values.slice(0, 6); + this.callback = callback; + + /** @type {Panel} */ + this.panel = null; + + this.selectedIndex = selectedIndex || 0; + } + + AddToPanel(panel) { + /// Recreate GameMenuOptionCyclePanel hierarchy to preserve styles, with dynamically added items + + this.panel = CreatePanel("Panel", panel, this.id, "cycler"); + const row = CreatePanel("Panel", this.panel, null, "row"); + const btnLeft = CreateDebugMenuButton(row, () => this.CycleLeft(), "cycle_button_left", "button_left"); + CreatePanel("Panel", btnLeft, null, "cycle_image cycle_image_left"); + const btnRight = CreateDebugMenuButton(row, () => this.CycleRight(), "cycle_button_right", "button_right"); + const col = CreatePanel("Panel", btnRight, null, "cycle_button_right_col"); + for (let [index,item] of this.values.entries()) { + /**@type {Label} */ + const text = CreatePanel("Label", col, "item"+index, "cycle_label"); + text.text = item.text; + } + CreatePanel("Label", col, "custom", "cycle_label").text = "Custom"; + const dotRow = CreatePanel("Panel", col, null, "dot_row"); + for (let [index,item] of this.values.entries()) { + /**@type {Label} */ + const dot = CreatePanel("Label", dotRow, "dot"+index, "cycle_dots"); + dot.text = " ■ "; + } + CreatePanel("Panel", dotRow); + const imgCont = CreatePanel("Panel", btnRight, null, "cycle_button_right_image"); + CreatePanel("Panel", imgCont, null, "cycle_image cycle_image_right"); + CreatePanel("Panel", this.panel); + + this.SetSelectedIndexNoFire(this.selectedIndex); + } + + /** + * Cycles to the left, wrapping around to the right if below `0`. + */ + CycleLeft() { + if (this.selectedIndex == -1) this.selectedIndex = 0; + this.SetSelectedIndex(this.selectedIndex - 1); + } + + /** + * Cycles to the right, wrapping around to the left if above `this.values.length`. + */ + CycleRight() { + this.SetSelectedIndex(this.selectedIndex + 1); + } + + /** + * Sets the selected option without firing the callback. + * @param {number} index The index of the option to select, starting from 0. + */ + SetSelectedIndexNoFire(index) { + index = (index + this.values.length) % this.values.length; + + for (let i = 0; i < this.values.length; i++) { + if (i == index) { + this.SetSelectedValueNoFire(this.values[i].value); + return; + } + } + } + + /** + * Sets the selected option. + * @param {number} index The index of the option to select, starting from 0. + */ + SetSelectedIndex(index) { + this.SetSelectedIndexNoFire(index); + this.callback(this.selectedIndex); + } + + SetSelectedValueNoFire(value) { + let foundValue = false; + + // Find the matching value index + for (let i = 0; i < this.values.length; i++) { + const _value = this.values[i].value; + const item = this.panel.FindChildTraverse("item" + i); + const dot = this.panel.FindChildTraverse("dot" + ((this.values.length-1) - i)); + if (_value === value) { + item.visible = true; + dot.SetHasClass("cycle_dots_selected", true); + foundValue = true; + this.selectedIndex = i; + } else { + item.visible = false; + dot.SetHasClass("cycle_dots_selected", false); + } + } + + // Reveal custom value if needed + const custom = this.panel.FindChildTraverse("custom"); + if (foundValue) { + custom.visible = false; + } else { + this.selectedIndex = -1; + custom.text = `Custom (${value})`; + custom.visible = true; + } + } + + SetSelectedValue(value) { + this.SetSelectedValueNoFire(value); + this.callback(this.selectedIndex); + } + + /** + * Gets the left cycle button. + * @returns {Button} + */ + GetLeftButton() { + return this.panel.FindChildTraverse("button_left"); + } + + /** + * Gets the right cycle button. + * @returns {Button} + */ + GetRightButton() { + return this.panel.FindChildTraverse("button_right"); + } + + /** + * Gets the currently selected item. + * @returns {Panel} + */ + GetSelectedItem() { + for (let i = 0; i < this.values.length; i++) { + const item = this.panel.FindChildTraverse("item" + i); + if (item.visible) return item; + } + } + + /** + * Gets the currently selected index. + * @returns {number} + */ + GetSelectedIndex() { + return this.selectedIndex; + } +} + /** * Shows a specific category and hides all others. * @param {string} id ID of the category to show. @@ -466,6 +793,59 @@ function CloseMenu() FireOutput("_CloseMenu"); } +/** + * Gets the position of the left or right 'affordance' circle for the VR finger interacting with the menu. + * @returns {{x:number,y:number}?} + */ +function GetAffordancePosition() { + const left = $('#vr_affordance_left'); + if (left.visible) + return { x: left.actualxoffset, y: left.actualyoffset }; + + const right = $('#vr_affordance_left'); + if (right.visible) + return { x: right.actualxoffset, y: right.actualyoffset }; + + return null; +} + +/** + * Remaps a number from one range to another. + * @param {number} value - The input value to remap. + * @param {number} low1 - Lower bound of the input range. + * @param {number} high1 - Upper bound of the input range. + * @param {number} low2 - Lower bound of the output range. + * @param {number} high2 - Upper bound of the output range. + * @returns {number} The remapped value in the output range. + */ +function RemapValue(value, low1, high1, low2, high2) { + return low2 + (high2 - low2) * (value - low1) / (high1 - low1); +} + +/** + * Remaps a number from one range to another while clamping within the range. + * @param {number} value - The input value to remap. + * @param {number} low1 - Lower bound of the input range. + * @param {number} high1 - Upper bound of the input range. + * @param {number} low2 - Lower bound of the output range. + * @param {number} high2 - Upper bound of the output range. + * @returns {number} The remapped value in the output range. + */ +function RemapValueClamped(value, low1, high1, low2, high2){ + return RemapValue(Clamp(value, low1, high1), low1, high1, low2, high2); +} + +/** + * Clamps a number between a minimum and maximum value. + * @param {number} value - The value to clamp. + * @param {number} min - The minimum allowable value. + * @param {number} max - The maximum allowable value. + * @returns {number} The clamped value. + */ +function Clamp(value, min, max) { + return Math.min(Math.max(value, min), max); +} + /** * Virtually clicks the currently active button. */ @@ -473,10 +853,17 @@ function ClickHoveredButton() { if (currentlyActiveButton !== null) { - $.DispatchEvent("Activated", currentlyActiveButton, "mouse"); + const button = currentlyActiveButton; + // $.Msg(`Pressing ${button.id} : ${button.paneltype}`); + $.DispatchEvent("Activated", button, "mouse"); } } +/** + * Parses the incoming Lua command. + * @param {string} command + * @param {string[]} args + */ function ParseCommand(command, args) { command = command.toLowerCase(); @@ -500,7 +887,6 @@ function ParseCommand(command, args) let buttonId = args[1]; let buttonText = args[2]; - $.Msg("ID: " + buttonId + ", Text: " + buttonText); category.AddButton(buttonId, buttonText); break; } @@ -546,6 +932,51 @@ function ParseCommand(command, args) break; } + case "addslider": { + const category = GetCategory(args[0]); + if (category === null) + { + $.Msg(`Category ${args[0]} does not exist!`); + break; + } + + const id = args[1]; + const text = args[2] || args[3]; + const convar = args[3]; + const min = parseFloat(args[4]); + const max = parseFloat(args[5]); + const value = parseFloat(args[6]); + const isPercentage = args[7] == "true"; + const truncate = parseInt(args[8]); + const increment = parseFloat(args[9]); + category.AddSlider(id, text, convar, min, max, value, isPercentage, truncate, increment); + break; + } + + case "addcycle": + const category = GetCategory(args[0]); + if (category === null) + { + $.Msg(`Category ${args[0]} does not exist!`); + break; + } + + const id = args[1]; + const convar = args[2]; + const currentValue = args[3]; + const rawValues = args.slice(4); + /**@type {SubMenuCycleItem[]} */ + const values = []; + for (let i = 0; i < rawValues.length; i+=2) { + values.push({ + text: rawValues[i], + value: rawValues[i+1] + }); + } + + category.AddCycle(id, convar, values, currentValue); + break; + case "setitemtext": { let category = GetCategory(args[0]); if (category === null) diff --git a/panorama/styles/custom_game/alyxlib_debug_menu.css b/panorama/styles/custom_game/alyxlib_debug_menu.css index a17c33e..57cf28d 100644 --- a/panorama/styles/custom_game/alyxlib_debug_menu.css +++ b/panorama/styles/custom_game/alyxlib_debug_menu.css @@ -128,12 +128,11 @@ Button.custom_button_with_subtitle.error_msg:not(.disabled) Label.button_label.s /* background-color: blue; */ width: 10%; } -.cycle_image +/* .cycle_image { - /* background-color: yellow; */ horizontal-align: left; margin: 0px 40px 10px 10px; -} +} */ .CategoryBar { @@ -281,10 +280,10 @@ Button } -.submenu Label +/* .submenu Label { font-size: 33px; -} +} */ Label.custom_label { @@ -333,4 +332,58 @@ Label.custom_label } .ScrollDownButton:hover .chevron_arrow{ background-color:#fff; +} + +.cycler +{ + width: 100%; + + flow-children: right; + horizontal-align: right; + text-align: right; + background-color: #FFBE5500; + transition-property: background-color; + transition-duration: 0s; +} + +/* Bring styles over from HLVR_SettingsSlider to custom .SubMenuSlider */ + +.SubMenuSlider +{ + width: 100%; + flow-children: none; +} + +.SubMenuSlider:hover +{ + color: #fff; + /*background-color: #FFBE5588;*/ + transition-property: background-color; + transition-duration: .38s; + transition-timing-function: cubic-bezier( 0.785, 0.385, 0.555, 1.505 ); +} + +.SubMenuSlider:active +{ + color: #000000; + background-color: #ffd800; +} + +.SubMenuSlider:hover .OptionsSlider #SliderTrackProgress +{ + width: 100%; + height: 85%; + vertical-align: center; + border: 1px solid #FFBE5588; + background-color: #FFBE5588; +} + +.Background .SubMenuSlider +{ + opacity: .1; +} + +/* Make slider value fit even with many decimal places */ +.slider_value { + text-overflow: shrink; } \ No newline at end of file diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index d31924c..27030f3 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -27,8 +27,15 @@ DebugMenu = {} ---@field id string ---@field text string ---@field callback function ----@field type "button"|"toggle"|"separator" ----@field default any +---@field type "button"|"toggle"|"separator"|"slider"|"cycle" # Type of menu element this item is. +---@field default any|function # The default value sent to the menu. If this is a function the return value will be used. +---@field min number # Minimum value of this slider. +---@field max number # Maxmimum value of this slider. +---@field isPercentage boolean # If true, this slider displays its value as a percentage of min/max. +---@field convar string # The console variable associated with this element. +---@field values any[] # Text/value pairs for this cycler. +---@field truncate number # Used for slider +---@field increment number # Used for slider ---The panel entity. ---@type CPointClientUIWorldPanel @@ -40,6 +47,13 @@ DebugMenu.categories = {} local debugMenuOpen = false local handChangedListener = nil +---Command to test trace button presses +if not IsVREnabled() then + Convars:RegisterCommand("_debug_menu_test_button_press", function() + DebugMenu:ClickHoveredButton() + end, "", FCVAR_HIDDEN) +end + --- ---The scope of the debug menu script. --- @@ -58,7 +72,7 @@ local debugPanelScriptScope = { return end - if item then + if item.callback then item.callback() end end, @@ -75,11 +89,58 @@ local debugPanelScriptScope = { return end - if item then - item.callback(on) - -- Hack for keeping state after close + -- Update default if user is tracking manually + if item.default ~= nil and type(item.default) ~= "function" then item.default = on end + + if item.callback then + item.callback(on) + end + end, + + _DebugMenuCallbackSlider = function(id, value) + local item = DebugMenu:GetItem(id) + if not item then + warn("Unknown item for panorama callback'"..id.."'") + return + end + + if item.type ~= "slider" then + warn("Option '"..id.."' is not a slider!") + return + end + + -- Update default if user is tracking manually + if item.default ~= nil and type(item.default) ~= "function" then + item.default = value + end + + if item.callback then + item.callback(value, item) + end + end, + + _DebugMenuCallbackCycle = function(id, index) + local item = DebugMenu:GetItem(id) + if not item then + return warn("Unknown item for panorama callback'"..id.."'") + end + + if item.type ~= "cycle" then + return warn("Option '"..id.."' is not a cycle!") + end + + local value = item.values[index] + + -- Update default if user is tracking manually + if item.default ~= nil and type(item.default) ~= "function" then + item.default = value + end + + if item.callback then + item.callback(index, value, item) + end end, _CloseMenu = function() @@ -131,7 +192,7 @@ function DebugMenu:ShowMenu() horizontal_align = "1", }) - if not Player.HMDAvatar then + if not Player.HMDAvatar or IsFakeVREnabled() then local localPlayer = Entities:GetLocalPlayer() local eyePos = localPlayer:EyePosition() local dir = localPlayer:EyeAngles():Forward() @@ -139,6 +200,8 @@ function DebugMenu:ShowMenu() a = RotateOrientation(a, QAngle(0,-90,90)) self.panel:SetQAngle(a) self.panel:SetOrigin(eyePos + dir * 16) + + SendToConsole("bind r _debug_menu_test_button_press") else self:UpdateMenuAttachment() @@ -194,7 +257,11 @@ function DebugMenu:CloseMenu() Player:SetCoughHandEnabled(true) - self:StartListeningForMenuActivation() + if Player.HMDAvatar then + self:StartListeningForMenuActivation() + else + SendToConsole("unbind r") + end end end @@ -320,7 +387,7 @@ end ---@param toggleId string # The unique ID for this toggle ---@param text string # The text to display on this toggle ---@param command string|function # The console command or function to run when this toggle is toggled (will run with 1 if it's on, 0 if it's off) ----@param startsOn? boolean # Whether the toggle is on by default +---@param startsOn? boolean|fun():boolean # Whether the toggle is on by default function DebugMenu:AddToggle(categoryId, toggleId, text, command, startsOn) local category = self:GetCategory(categoryId) if not category then @@ -369,12 +436,124 @@ function DebugMenu:AddLabel(categoryId, labelId, text) }) end +--- +---Add value slider to a category. +--- +---@param categoryId string # The ID of the category to add this slider to +---@param sliderId string # A unique ID for this slider +---@param text string # Display text for the slider +---@param min number # Minimum allowed value +---@param max number # Maximum allowed value +---@param isPercentage boolean # If true, value will be displayed as a percentage (0-100) +---@param command string|fun(value:number,slider:DebugMenuItem) # Convar name or callback function +---@param truncate? number # Number of decimal places (0 = integer, -1 = no truncating) +---@param increment? number # Snap increment (0 disables snapping) +---@param defaultValue? number|fun():number # Starting value. Set nil to use the convar value whenever the menu opens +function DebugMenu:AddSlider(categoryId, sliderId, text, min, max, isPercentage, command, truncate, increment, defaultValue) + local category = self:GetCategory(categoryId) + if not category then + warn("Cannot add toggle '"..sliderId.."': Category '"..categoryId.."' does not exist!") + return + end + + local callback + local convar = "" + if type(command) == "string" then + if command == "" then + error("Command must not be a blank string", 2) + end + convar = command + + ---@param value number + ---@param slider DebugMenuItem + callback = function(value, slider) + Convars:SetStr(slider.convar, tostring(value)) + end + elseif type(command) == "function" then + callback = command + end + + table.insert(category.items, { + categoryId = categoryId, + id = sliderId, + text = text, + callback = callback, + type = "slider", + default = defaultValue, + min = min, + max = max, + convar = convar, + isPercentage = isPercentage or false, + truncate = truncate or -1, + increment = increment or 0 + }) +end + +--- +---Add a value cycler to a category. +--- +---Cyclers allow users to choose from a set of values. +--- +---@param categoryId string # The id of the category to add this cycle to +---@param cycleId string # The unique id for this new cycle +---@param values {text:string,value:any}[] # List of text/value pairs for this cycle +---@param command string|fun(index:number, item:{text:string,value:any?}, cycle:DebugMenuItem) # Convar name or function callback +---@param defaultValue? any|fun():any # Value for this cycle to start with +function DebugMenu:AddCycle(categoryId, cycleId, values, command, defaultValue) + local category = self:GetCategory(categoryId) + if not category then + warn("Cannot add toggle '"..cycleId.."': Category '"..categoryId.."' does not exist!") + return + end + + if type(values) ~= "table" or #values == 0 then + error("Cycle values must be a table with at least 1 value", 2) + end + + for k,v in ipairs(values) do + v.value = tostring(v.value or (k - 1)) + end + + local callback + local convar = "" + if type(command) == "string" then + if command == "" then + error("Command must not be a blank string", 2) + end + convar = command + + ---@param index number + ---@param item {text:string,value:any?} + ---@param cycle DebugMenuItem + callback = function(index, item, cycle) + Convars:SetStr(cycle.convar, tostring(item.value)) + end + elseif type(command) == "function" then + callback = command + end + + table.insert(category.items, { + categoryId = categoryId, + id = cycleId, + callback = callback, + type = "cycle", + values = values, + default = defaultValue, + convar = convar + }) +end + --- ---Set the text of an item. --- ----@param categoryId string # The category ID ----@param itemId any # The item ID ----@param text any # The new text +---Only works on the following types: +--- - button +--- - toggle +--- - slider +--- +---@param categoryId string # The ID of the category that contains the item +---@param itemId string # The ID of the item to modify +---@param text string # The new text function DebugMenu:SetItemText(categoryId, itemId, text) local item = self:GetItem(itemId) if not item then @@ -395,8 +574,8 @@ end --- ---This is an advanced function and should be used with caution. --- ----@param categoryId any ----@param index any +---@param categoryId string # Id of the category to change. +---@param index number # New index for the category. function DebugMenu:SetCategoryIndex(categoryId, index) local category, currentIndex = self:GetCategory(categoryId) if not category then @@ -414,6 +593,16 @@ function DebugMenu:SetCategoryIndex(categoryId, index) end end +---Resolves the default value of an element by running any value getter functions. +---@param default any +---@return any +local function resolveDefault(default) + if type(default) == "function" then + return default() + end + return default +end + --- ---Sends a category and all its elements to the panel. --- @@ -431,13 +620,41 @@ function DebugMenu:SendCategoryToPanel(category) for _, item in ipairs(category.items) do if item.type == "toggle" then - Panorama:Send(panel, "AddToggle", item.categoryId, item.id, item.text, item.default) + Panorama:Send(panel, "AddToggle", item.categoryId, item.id, item.text, resolveDefault(item.default)) + elseif item.type == "button" then Panorama:Send(panel, "AddButton", item.categoryId, item.id, item.text) + elseif item.type == "label" then Panorama:Send(panel, "AddLabel", item.categoryId, item.id, item.text) + elseif item.type == "separator" then Panorama:Send(panel, "AddSeparator", item.categoryId) + + elseif item.type == "slider" then + local default = resolveDefault(item.default) + if default == nil then + default = Convars:GetFloat(item.convar) or item.min + end + Panorama:Send(panel, "AddSlider", item.categoryId, item.id, item.text, item.convar, item.min, item.max, default, item.isPercentage, item.truncate, item.increment) + + elseif item.type == "cycle" then + -- Flatten values into an array of text + local values = {} + local index = 1 + for i = 1, #item.values do + values[index] = item.values[i].text + values[index+1] = item.values[i].value or (i - 1) + index = index + 2 + end + + local default = resolveDefault(item.default) + -- Use convar value if default isn't set + if default == nil and item.convar ~= "" then + default = Convars:GetStr(item.convar) + end + + Panorama:Send(panel, "AddCycle", item.categoryId, item.id, item.convar, default, values) else warn("Unknown item type '"..item.type.."'") end @@ -529,6 +746,12 @@ end if Convars:GetInt("developer") > 0 then local listenFunc = ListenToPlayerEvent or ListenToGameEvent listenFunc("vr_player_ready", function() + -- Kill existing panel on load to avoid missing logic errors + local panel = Entities:FindByName(nil, "alyxlib_debug_menu") + if panel then + panel:Kill() + end + Player:Delay(function() DebugMenu:StartListeningForMenuActivation() end, 0.2) From 8bf68defe79fe173771f15a7566a3120d39a4b2c Mon Sep 17 00:00:00 2001 From: FrostSource Date: Fri, 11 Jul 2025 16:35:02 +1200 Subject: [PATCH 082/101] Reference panoramadoc instead of require --- panorama/scripts/custom_game/alyxlib_debug_menu.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/panorama/scripts/custom_game/alyxlib_debug_menu.js b/panorama/scripts/custom_game/alyxlib_debug_menu.js index 39f26b5..4d94470 100644 --- a/panorama/scripts/custom_game/alyxlib_debug_menu.js +++ b/panorama/scripts/custom_game/alyxlib_debug_menu.js @@ -1,9 +1,8 @@ +/// "use strict"; ///TODO: Add pop up for warnings and errors -if(false)p=require("./panoramadoc"); - let panelReady = false; /** From 694801cc501e81097534003de211e4bbfde1fb1b Mon Sep 17 00:00:00 2001 From: FrostSource Date: Fri, 11 Jul 2025 16:35:59 +1200 Subject: [PATCH 083/101] Warn elements that can't change text --- panorama/scripts/custom_game/alyxlib_debug_menu.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/panorama/scripts/custom_game/alyxlib_debug_menu.js b/panorama/scripts/custom_game/alyxlib_debug_menu.js index 4d94470..f17fd37 100644 --- a/panorama/scripts/custom_game/alyxlib_debug_menu.js +++ b/panorama/scripts/custom_game/alyxlib_debug_menu.js @@ -43,7 +43,7 @@ let currentlySelectedCategory = null; */ let currentlyActiveButton = null; -function TurnButtonIntoDebugMenuButton(button) +function TurnButtonIntoDebugMenuButton(button, callback) { if (button == null) return; @@ -51,6 +51,12 @@ function TurnButtonIntoDebugMenuButton(button) button.SetPanelEvent("onmouseout", () => { if (currentlyActiveButton == button) currentlyActiveButton = null; }); + + if (callback !== null && callback !== undefined) + if (button.paneltype == "HLVR_SettingsSlider") + button.SetPanelEvent("onvaluechanged", callback); + else + button.SetPanelEvent("onactivate", callback); } /** @@ -188,11 +194,15 @@ class Category let item = this.items.find(o => o.id === combinedId); if (item === undefined) { - this.items.forEach((o) => $.Msg(o.id)); $.Msg(`Item ${id} does not exist!`); return; } + if (!item.SetText) { + $.Msg(`Item ${id} does not support SetItemText!`); + return; + } + // text-transform: uppercase; doesn't affect js set text? text = text.toLocaleUpperCase(); From 1ebffe312c9c3a54de8dcecd2031dc8f4cc12a90 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Fri, 11 Jul 2025 16:36:23 +1200 Subject: [PATCH 084/101] Add backing convar for noclip_vr --- scripts/vscripts/alyxlib/debug/vr.lua | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/vscripts/alyxlib/debug/vr.lua b/scripts/vscripts/alyxlib/debug/vr.lua index d5cdd13..5537df4 100644 --- a/scripts/vscripts/alyxlib/debug/vr.lua +++ b/scripts/vscripts/alyxlib/debug/vr.lua @@ -102,8 +102,8 @@ Convars:RegisterCommand("add_hand_attachment", function (_, classname, handName) hand:AddHandAttachment(ent) end, "", 0) - -local noclipVREnabled = false +---Hidden convar state that can be retrieved by other scripts +RegisterAlyxLibConvar("noclip_vr_enabled", "0", "True if noclip_vr is enabled (readonly)", FCVAR_HIDDEN) ---Tracks initial button press to prevent repeated logic execution while held local quickTurnFlag = false @@ -167,11 +167,13 @@ RegisterAlyxLibConvar("noclip_vr_boost_speed", "5", "Speed of the VR noclip move local movetype = Convars:GetInt('hlvr_movetype_default') RegisterAlyxLibCommand("noclip_vr", function (_, on) + local noclipVREnabled if on == nil then - noclipVREnabled = not noclipVREnabled + noclipVREnabled = not Convars:GetBool("noclip_vr_enabled") else noclipVREnabled = truthy(on) end + Convars:SetBool("noclip_vr_enabled", noclipVREnabled) if noclipVREnabled then movetype = Convars:GetInt("hlvr_movetype_default") From 629243d6a49a16c3c3def43f3ca907406342fe09 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Fri, 11 Jul 2025 16:36:53 +1200 Subject: [PATCH 085/101] Fix panorama table flattening --- scripts/vscripts/alyxlib/panorama/core.lua | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/scripts/vscripts/alyxlib/panorama/core.lua b/scripts/vscripts/alyxlib/panorama/core.lua index 250234c..e73dc3b 100644 --- a/scripts/vscripts/alyxlib/panorama/core.lua +++ b/scripts/vscripts/alyxlib/panorama/core.lua @@ -56,15 +56,18 @@ function Panorama:Send(panelEntity, ...) local dataString = id .. "|" local data = {...} local i = 1 - local dataLength = #data + + local flattenedData = {} -- Flatten nested tables into data - while i <= dataLength do - if type(data[i]) == "table" then - data = vlua.extend(vlua.slice(data, 1, dataLength), data[i]) + for _, value in ipairs(data) do + if type(value) == "table" then + data = vlua.extend(flattenedData, value) + else + table.insert(flattenedData, value) end - i = i + 1 end + local dataLength = #flattenedData -- Put all values into a single pipe separated string for index, value in ipairs(data) do From 19aaa7709fcc0051de1ce35aec916f138105bf60 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Fri, 11 Jul 2025 16:42:58 +1200 Subject: [PATCH 086/101] Update EasyConvars to support Convars accessor Existing members registered with EasyConvars will need to be updated to the new function signatures. Display functions are no longer supported for convars and will need to be merged into the postUpdate --- .../vscripts/alyxlib/helpers/easyconvars.lua | 176 +++++++++++------- 1 file changed, 109 insertions(+), 67 deletions(-) diff --git a/scripts/vscripts/alyxlib/helpers/easyconvars.lua b/scripts/vscripts/alyxlib/helpers/easyconvars.lua index 7f8c8c5..c1e40d1 100644 --- a/scripts/vscripts/alyxlib/helpers/easyconvars.lua +++ b/scripts/vscripts/alyxlib/helpers/easyconvars.lua @@ -24,6 +24,7 @@ EasyConvars.version = "v1.2.0" ---@field name string # Name of the convar ---@field desc? string # Description of the convar/command. Is displayed below the current value when called without a parameter. ---@field value string # Raw value of the convar. +---@field prevValue string # Previous raw value of the convar. ---@field callback? fun(val:string, ...):any? # Optional callback function whenever the convar is changed. ---@field initializer? fun():any # Optional initializer function which will set the default value on player spawn. ---@field persistent boolean # If the value is saved to player on change. @@ -44,7 +45,7 @@ EASYCONVARS_COMMAND = "command" ---Toggles have a value of "1" or "0" and display their state in the console. EASYCONVARS_TOGGLE = "toggle" ----The table of all registered convars. +---The table of all registered convars, (name -> data) ---@type table EasyConvars.registered = {} @@ -102,15 +103,8 @@ local function callCallback(registeredData, value, ...) registeredData.value = convertToSafeVal(result) end - EasyConvars:Save(registeredData.name) -end - ----Default display function for convars ----@param reg EasyConvarsRegisteredData -local function defaultDisplayFuncConvar(reg) - Msg(reg.name .. " = " .. tostring(reg.value) .. "\n") - if reg.desc ~= nil and reg.desc ~= "" then - Msg(reg.desc .. "\n") + if registeredData.persistent then + EasyConvars:Save(registeredData.name) end end @@ -120,6 +114,60 @@ local function defaultDisplayFuncToggle(reg) Msg(reg.name .. (truthy(reg.value) and " ON" or " OFF") .. "\n") end +---Standard callback for all command and toggle cvars. +---@param name string # Name of the command called +---@param ... string # Values given by the user through the console +local function commandCallback(name, ...) + local cvar = EasyConvars.registered[name] + if cvar == nil then + return warn("Could not find registered data for cvar "..name) + end + + local prevVal = cvar.value + local args = {...} + + callCallback(cvar, args[1], vlua.slice(args, 1)) + + if prevVal ~= cvar.value then + cvar.wasChangedByUser = true + end + + -- Display the new toggled state ON/OFF + if cvar.type == EASYCONVARS_TOGGLE then + if type(cvar.displayFunc) == "function" then + cvar.displayFunc(cvar) + end + end +end + +---Sets the value of an EasyConvar. +---@param registeredData EasyConvarsRegisteredData +---@param value string +---@param ... any +local function setCvarValue(registeredData, value, ...) + if registeredData.type == EASYCONVARS_CONVAR then + Convars:SetStr(registeredData.name, convertToSafeVal(value)) + else + callCallback(registeredData, value, ...) + end +end + +---Listener for all FCVAR_NOTIFY changes. +---@param params GameEventServerCvar +ListenToGameEvent("server_cvar", function (params) + local cvar = EasyConvars.registered[params.cvarname] + if cvar == nil then return end + + callCallback(cvar, params.cvarvalue, cvar.prevValue) + + cvar.prevValue = cvar.value + cvar.value = params.cvarvalue + + if cvar.prevValue ~= cvar.value then + cvar.wasChangedByUser = true + end +end, nil) + ---Creates a convar of any type. --- ---For simple creation use one of the following: @@ -133,23 +181,19 @@ end ---@param onUpdate? (fun(val:string, ...):any?)|(fun(newVal:string, oldVal:string):any) # Optional callback function. ---@param helpText? string # Description of the convar ---@param flags? CVarFlags|integer # Flag for the convar ----@param displayFunc? fun(reg: EasyConvarsRegisteredData) # The function called when the convar is called without any parameters. By default it just prints the value. ----@overload fun(name: string, defaultValue: any, onUpdate: fun(newVal: string, oldVal: string):any?, helpText: string, flags: integer, displayFunc: function) --- function EasyConvars:Register(name, default, onUpdate, helpText, flags, displayFunc, postUpdate, commandOnly) -function EasyConvars:Register(ctype, name, defaultValue, onUpdate, helpText, flags, displayFunc) +---@return EasyConvarsRegisteredData # The new registered cvar data +function EasyConvars:Register(ctype, name, defaultValue, onUpdate, helpText, flags) - -- GlobalSys:CommandLineStr("-"..name, GlobalSys:CommandLineCheck("-"..name) and "1" or tostring(default or "0")) local launchVal = GlobalSys:CommandLineStr("-"..name, GlobalSys:CommandLineCheck("-"..name) and "1" or nil) self.registered[name] = { type = ctype, callback = onUpdate, value = launchVal, + prevValue = launchVal, persistent = false, - -- isCommand = commandOnly == true, name = name, desc = helpText, wasChangedByUser = false, - displayFunc = displayFunc or vlua.select(ctype == EASYCONVARS_CONVAR, defaultDisplayFuncConvar, defaultDisplayFuncToggle), } local reg = self.registered[name] @@ -171,36 +215,18 @@ function EasyConvars:Register(ctype, name, defaultValue, onUpdate, helpText, fla helpText = helpText or "" flags = flags or 0 - -- reg.callback = onUpdate - - Convars:RegisterCommand(name, function (_, ...) - local args = {...} - - -- Display current value - if reg.type == EASYCONVARS_CONVAR and #args == 0 then - if type(reg.displayFunc) == "function" then - reg.displayFunc(reg) - end - -- Early exit - return - end - - local prevVal = reg.value - local prevTruthy = truthy(reg.value) - - callCallback(reg, args[1], ...) + -- Notify is required to listen for value changes + if bit.band(flags, FCVAR_NOTIFY) == 0 then + flags = bit.bor(flags, FCVAR_NOTIFY) + end - if prevVal ~= reg.value then - reg.wasChangedByUser = true - end + if ctype == EASYCONVARS_COMMAND or ctype == EASYCONVARS_TOGGLE then + Convars:RegisterCommand(name, commandCallback, helpText, flags) + else + Convars:RegisterConvar(name, reg.value, helpText, flags) + end - -- Display the new toggled state - if reg.type == EASYCONVARS_TOGGLE then - if type(reg.displayFunc) == "function" then - reg.displayFunc(reg) - end - end - end, helpText, flags) + return reg end --- @@ -234,7 +260,6 @@ end ---@param name string # The name of the convar to save. function EasyConvars:Save(name) if not self.registered[name] then return end - if self.registered[name].persistent == false then return end local saver = Player or GetListenServerHost() if not saver then @@ -255,7 +280,8 @@ end ---@param name string # The name of the convar to load. ---@return boolean # Returns true if the convar was loaded successfully. function EasyConvars:Load(name) - if not self.registered[name] then return false end + local cvar = self.registered[name] + if not cvar then return false end local loader = Player or GetListenServerHost() if not loader then @@ -265,16 +291,15 @@ function EasyConvars:Load(name) self._isLoading = true - local val = loader:LoadString("easyconvar_"..name, nil) - if val ~= nil then - self.registered[name].persistent = true - -- If it has a callback, execute to run any necessary code - callCallback(self.registered[name], val) + local loadedValue = loader:LoadString("easyconvar_"..name, nil) + if loadedValue ~= nil then + cvar.persistent = true + setCvarValue(cvar, loadedValue) end self._isLoading = false - return val ~= nil + return loadedValue ~= nil end --- @@ -305,11 +330,10 @@ end ---@param name string # Name of the convar ---@param defaultValue? "0"|any|fun():any # Default value of the convar, or an initializer function ---@param helpText? string # Description of the convar ----@param flags? `nil`|CVarFlags|integer # Flags for the convar +---@param flags? CVarFlags|integer # Flags for the convar ---@param postUpdate? fun(newVal:string, oldVal:string): any # Update function called after the value has been changed ----@param displayFunc? fun(reg: EasyConvarsRegisteredData) # Optional custom display function -function EasyConvars:RegisterConvar(name, defaultValue, helpText, flags, postUpdate, displayFunc) - self:Register(EASYCONVARS_CONVAR,name, defaultValue, postUpdate, helpText, flags, displayFunc) +function EasyConvars:RegisterConvar(name, defaultValue, helpText, flags, postUpdate) + self:Register(EASYCONVARS_CONVAR,name, defaultValue, postUpdate, helpText, flags) end --- @@ -320,12 +344,14 @@ end ---@param helpText? string # Description of the command ---@param flags? CVarFlags|integer # Flags for the command function EasyConvars:RegisterCommand(name, callback, helpText, flags) - self:Register(EASYCONVARS_COMMAND, name, nil, callback, helpText, flags, nil) + self:Register(EASYCONVARS_COMMAND, name, nil, callback, helpText, flags) end --- ---Registers a new toggle convar. --- +---This is a command that has an on/off state like `god` or `notarget`. +--- ---@param name string # Name of the convar ---@param defaultValue? "0"|any|fun():any # Default value of the convar ---@param helpText? string # Description of the convar @@ -333,7 +359,17 @@ end ---@param postUpdate? fun(newVal:string, oldVal:string): any # Update function called after the value has been changed ---@param displayFunc? fun(reg: EasyConvarsRegisteredData) # Optional custom display function function EasyConvars:RegisterToggle(name, defaultValue, helpText, flags, postUpdate, displayFunc) - EasyConvars:Register(EASYCONVARS_TOGGLE, name, defaultValue, postUpdate, helpText, flags, displayFunc) + local cvar = EasyConvars:Register(EASYCONVARS_TOGGLE, name, defaultValue, postUpdate, helpText, flags) + cvar.displayFunc = displayFunc or defaultDisplayFuncToggle +end + +--- +---Gets the [EasyConvarsRegisteredData](lua://EasyConvarsRegisteredData) table for a given cvar name. +--- +---@param name string # The name of the registered EasyConvar to get the data for +---@return EasyConvarsRegisteredData? # The data associated with `name` +function EasyConvars:GetConvarData(name) + return self.registered[name] end --- @@ -342,8 +378,14 @@ end ---@param name string # Name of the convar ---@return string? # The value of the convar as a string or nil if convar does not exist function EasyConvars:GetStr(name) - if not self.registered[name] then return nil end - return self.registered[name].value + local cvar = self.registered[name] + if not cvar then return nil end + -- return self.registered[name].value + if cvar.type == EASYCONVARS_CONVAR then + return Convars:GetStr(name) + else + return cvar.value + end end --- @@ -384,7 +426,7 @@ end function EasyConvars:SetStr(name, value) local reg = self.registered[name] if not reg then return nil end - callCallback(reg, value) + setCvarValue(reg, value) end --- @@ -395,7 +437,7 @@ end function EasyConvars:SetBool(name, value) local reg = self.registered[name] if not reg then return nil end - callCallback(reg, valueToBoolStr(value)) + setCvarValue(reg, valueToBoolStr(value)) end --- @@ -406,7 +448,7 @@ end function EasyConvars:SetFloat(name, value) local reg = self.registered[name] if not reg then return nil end - callCallback(reg, tostring(value)) + setCvarValue(reg, tostring(value)) end --- @@ -417,7 +459,7 @@ end function EasyConvars:SetInt(name, value) local reg = self.registered[name] if not reg then return nil end - callCallback(reg, tostring(math.floor(value))) + setCvarValue(reg, tostring(math.floor(value))) end --- @@ -464,7 +506,7 @@ function EasyConvars:SetIfUnchanged(name, value) local reg = self.registered[name] if not reg then return end if not reg.wasChangedByUser then - callCallback(reg, value) + setCvarValue(reg, value) end end @@ -493,7 +535,7 @@ listener("player_activate", function (params) data.wasChangedByUser = true devprints2("EasyConvars", name, "initializer won't be used because it has a user value of", tostring(data.value)) else - data.value = convertToSafeVal(data.initializer()) + setCvarValue(data, convertToSafeVal(data.initializer())) data.defaultValue = data.value devprints2("EasyConvars", name, "initializer value was", data.value) end From 65422e69000747a2639ffc7ceb503eedf191b052 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Fri, 11 Jul 2025 16:43:30 +1200 Subject: [PATCH 087/101] Remove prints --- scripts/vscripts/alyxlib/player/hands.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/vscripts/alyxlib/player/hands.lua b/scripts/vscripts/alyxlib/player/hands.lua index 8af207a..dc5f1f0 100644 --- a/scripts/vscripts/alyxlib/player/hands.lua +++ b/scripts/vscripts/alyxlib/player/hands.lua @@ -49,10 +49,8 @@ end ---@param ent EntityHandle|string function CPropVRHand:Grab(ent) if type(ent) == "string" then - print('search for ' .. ent) local name = ent ent = Entities:FindByName(nil, name) - print(ent) if ent == nil then return warn("Could not find entity to grab with name " .. name) end From 2c836adbc3a8cdc144da12eccea854a35bea4d1a Mon Sep 17 00:00:00 2001 From: FrostSource Date: Fri, 11 Jul 2025 16:46:55 +1200 Subject: [PATCH 088/101] Add existing panel types, refactor to classes --- panorama/scripts/custom_game/panoramadoc.js | 199 +++++++++++++++----- 1 file changed, 148 insertions(+), 51 deletions(-) diff --git a/panorama/scripts/custom_game/panoramadoc.js b/panorama/scripts/custom_game/panoramadoc.js index 9157982..9980027 100644 --- a/panorama/scripts/custom_game/panoramadoc.js +++ b/panorama/scripts/custom_game/panoramadoc.js @@ -236,13 +236,15 @@ class $ { /** * Global selector function used to find panels by id. * @param {string} searchString String used to find a panel. - * @returns {Panel} + * @returns {AllPanelTypes} * @example * let panel = $('#MyPanelID') */ // @ts-ignore function $(searchString){return} +/**@typedef {Panel|Label|Button|TextButton|RadioButton|ToggleButton|DropDown|ProgressBar|CircularProgressBar|Countdown|TextEntry|SlottedSlider|Slider|NumberEntry|Image|Carousel|Grid|Movie|HTML} AllPanelTypes */ + /** * Panel class. */ @@ -409,6 +411,12 @@ class Panel { */ isValid; + /** + * Unknown. + * @type {string} + */ + paneltype; + /** * Adds a class to this panel. * @param {string} className Name of class to add. @@ -491,6 +499,11 @@ class Panel { SetParent(panel){} FindChild(str){} + /** + * + * @param {string} str Id to look for. + * @returns {Panel?} + */ FindChildTraverse(str){} FindChildInLayoutFile(str){} FindPanelInLayoutFile(str){} @@ -542,6 +555,7 @@ class Panel { SetReadyForDisplay(bool){} SetPositionInPixels(float1, float2, float3){} Data(unknown){} + /** * Sets an event handler for a Panorama UI panel. * @param {Event} event The event to listen for. @@ -552,15 +566,13 @@ class Panel { * }); */ SetPanelEvent(event, callback){} + RunScriptInPanelContext(unknown){} rememberchildfocus(bool){} - paneltype(){} - - - /** - * Label members. - */ +} +class Label extends Panel +{ /** * Text of the panel. * @type {string} @@ -584,35 +596,75 @@ class Panel { * @param {string} text */ SetAlreadyLocalizedText(text){} +} - /** - * ToggleButton members. - */ +class Button extends Panel +{ +} + +class TextButton extends Button +{ + text; +} + +class RadioButton extends Panel +{ + GetSelectedButton(){} + group; +} +class ToggleButton extends Panel +{ /** * Set if the button is selected or not. * @param {boolean} selected */ SetSelected(selected){} +} - +class DropDown extends Panel +{ /** - * DropDown members. + * Adds a panel to the drop down. + * @param {Panel} panel The panel to add. */ + AddOption(panel){} + /** + * Checks if this drop down has an option with a given id. + * @param {string} id Id of the panel to search for. + * @returns {boolean} True if this drop down as the panel. + */ + HasOption(id){} + + /** + * + * @param {unknown} unknown + */ + RemoveOption(unknown){} - AddOption(){} - HasOption(){} - RemoveOption(){} + /** + * Removes all options from this drop down. + */ RemoveAllOptions(){} + + /** + * Gets the currently selected panel. + * @return {Panel?} The currently selected panel. + */ GetSelected(){} FindDropDownMenuChild(){} AccessDropDownMenu(){} - + /** - * ProgressBar members. + * Sets a panel as selected either by object reference or id. + * @param {Panel|string} panel The panel or id to set as selected. */ + SetSelected(panel){} +} +class ProgressBar extends Panel +{ /** * @type {number} */ @@ -628,11 +680,39 @@ class Panel { */ max; + // exist? + // hasNotches; + // valuePerNotch; +} + +class CircularProgressBar extends Panel +{ + /** + * @type {number} + */ + value; + + /** + * @type {number} + */ + min; /** - * TextEntry members. + * @type {number} */ + max; +} +class Countdown extends Panel +{ + startTime = 0; + endTime = 0; + updateInterval = 1; + timeDialogVariable = 'countdown_time'; +} + +class TextEntry extends Panel +{ SetMaxChars(){} GetMaxCharCount(){} GetCursorOffset(){} @@ -640,42 +720,63 @@ class Panel { ClearSelection(){} SelectAll(){} RaiseChangeEvents(){} - +} - /** - * SlottedSlider/Slider members. - */ +class SlottedSlider extends Slider +{ +} + +class Slider extends Panel +{ + value; + min; + max; increment; default; mousedown; - SetDirection(){} - SetShowDefaultValue(){} - SetRequiresSelection(){} - SetValueNoEvents(){} - - - /** - * Image members. - */ + SetDirection(unknown){} + SetShowDefaultValue(showDefaultValue){} + SetRequiresSelection(requiresSelection){} + SetValueNoEvents(value){} +} - SetImage(){} - SetScaling(){} - +class NumberEntry extends Panel +{ + value; + min; + max; + increment; +} - /** - * Carousel members. - */ +class Image extends Panel +{ + SetImage(path){} + SetScaling(unknown){} +} +class Carousel extends Panel +{ SetSelectedChild(){} GetFocusChild(){} GetFocusIndex(){} - +} - /** - * Movie members. - */ +class Grid extends Panel +{ + verticalcount; + horizontalcount; + focusmargin; + scrolldirection; + scrollprogress; + SetIgnoreFastMotion(){} + GetFocusedChildVisibleIndex(){} + ScrollPanelToLeftEdge(){} + MoveFocusToTopLeft(){} +} +class Movie extends Panel +{ SetMovie(){} SetControls(){} SetTitle(){} @@ -685,18 +786,14 @@ class Panel { SetRepeat(){} SetPlaybackVolume(){} BAdjustingVolume(){} - - } -// class Label extends Panel -// { -// /** -// * Text of the panel, if the panel is a Label. -// * @type {string} -// */ -// text; -// } +class HTML extends Panel +{ + SetURL(){} + RunJavascript(){} + SetIgnoreCursor(){} +} // /** // * @alias $ From 6f0e459c03ea73c29c391ab2c6e2218a22f91efe Mon Sep 17 00:00:00 2001 From: FrostSource Date: Fri, 11 Jul 2025 17:23:34 +1200 Subject: [PATCH 089/101] Add title to separator --- .../scripts/custom_game/alyxlib_debug_menu.js | 84 ++++++++++++++++--- scripts/vscripts/alyxlib/debug/debug_menu.lua | 8 +- 2 files changed, 80 insertions(+), 12 deletions(-) diff --git a/panorama/scripts/custom_game/alyxlib_debug_menu.js b/panorama/scripts/custom_game/alyxlib_debug_menu.js index f17fd37..6d60077 100644 --- a/panorama/scripts/custom_game/alyxlib_debug_menu.js +++ b/panorama/scripts/custom_game/alyxlib_debug_menu.js @@ -236,17 +236,28 @@ class Category this.items.push(label); } - AddSeparator() + /** + * Adds a new separator to the menu with optional text. + * @param {string} id Id for this separator. + * @param {string?} text Text displayed on the separator. + */ + AddSeparator(id, text = "") { - let rowDivider = $.CreatePanel("Panel", this.content, undefined); - rowDivider.AddClass("row_divider"); - - let rowDividerLabel = $.CreatePanel("Panel", rowDivider, undefined); - rowDividerLabel.AddClass("button_label"); - - let rowDividerLine = $.CreatePanel("Panel", rowDividerLabel, undefined); - rowDividerLine.AddClass("row_divider_line"); + const separator = new SubMenuSeparator(`${this.id}_${id}`, text); + separator.AddToPanel(this.content); + this.items.push(separator); + } + /** + * **CURRENTLY NOT SUPPORTED IN DEBUG_MENU.LUA** + * Adds a solid header to the category. + * @param {string} title Text displayed in the header. + */ + AddHeader(title) + { + const header = CreatePanel("Panel", this.content, null, "header"); + const label = CreatePanel("Label", header, null); + label.text = "This be header"; } /** @@ -699,6 +710,57 @@ class SubMenuCycle } } +class SubMenuSeparator +{ + /** + * Creates a new sub menu separator instance. + * @param {string} id Id for this separator. + * @param {string?} text Text to display with this separator. + */ + constructor(id, text = "") + { + this.id = id; + this.text = text; + + /**@type {Panel} */ + this.panel = null; + } + + /** + * Creates all required elements as children of `panel`. + * @param {Panel} panel Panel to add this separator to. + */ + AddToPanel(panel) + { + this.panel = CreatePanel("Panel", panel, this.id, "options_divider"); + const label = CreatePanel("Label", this.panel, null); + label.text = this.text; + CreatePanel("Panel", this.panel, null, "horizontal_line"); + } + + /** + * Sets or removes the text displayed with this separator. + * @param {string?} text The text to display with this separator. + */ + SetText(text = "") + { + this.text = text; + const label = this.panel.GetChild(0); + if (label) { + label.text = text; + } + } + + /** + * Gets the text displayed with this separator. + * @returns {string} + */ + GetText() + { + return this.text; + } +} + /** * Shows a specific category and hides all others. * @param {string} id ID of the category to show. @@ -937,7 +999,9 @@ function ParseCommand(command, args) break; } - category.AddSeparator(); + const id = args[1]; + const text = args[2]; + category.AddSeparator(id, text); break; } diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index 27030f3..5dd2793 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -335,7 +335,9 @@ end ---Add a separator line to a category. --- ---@param categoryId string # The category ID to add the separator to -function DebugMenu:AddSeparator(categoryId) +---@param separatorId? string # Optional ID for the separator if you want to modify it later +---@param text? string # Optional title text to display on the separator +function DebugMenu:AddSeparator(categoryId, separatorId, text) local category = self:GetCategory(categoryId) if not category then warn("Cannot add separator: Category '"..categoryId.."' does not exist!") @@ -345,6 +347,8 @@ function DebugMenu:AddSeparator(categoryId) table.insert(category.items, { categoryId = categoryId, type = "separator", + id = separatorId or DoUniqueString("separator"), + text = text or "" }) end @@ -629,7 +633,7 @@ function DebugMenu:SendCategoryToPanel(category) Panorama:Send(panel, "AddLabel", item.categoryId, item.id, item.text) elseif item.type == "separator" then - Panorama:Send(panel, "AddSeparator", item.categoryId) + Panorama:Send(panel, "AddSeparator", item.categoryId, item.id, item.text) elseif item.type == "slider" then local default = resolveDefault(item.default) From af6ce3e6e3114ddf316b185a2a12a10d19f8d02d Mon Sep 17 00:00:00 2001 From: FrostSource Date: Fri, 11 Jul 2025 17:24:30 +1200 Subject: [PATCH 090/101] Fix scroll helpers locking up --- .../scripts/custom_game/alyxlib_debug_menu.js | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/panorama/scripts/custom_game/alyxlib_debug_menu.js b/panorama/scripts/custom_game/alyxlib_debug_menu.js index 6d60077..87c0073 100644 --- a/panorama/scripts/custom_game/alyxlib_debug_menu.js +++ b/panorama/scripts/custom_game/alyxlib_debug_menu.js @@ -1107,15 +1107,40 @@ function ParseCommand(command, args) let scrollHelperScheduleCancel = false; let scrollHelperScheduleEvent = ""; +let scrollHelperSpeed = 0.1; +/** + * Scroll logic for the scroll helper schedule. + */ function ScrollHelperSchedule() { - if (scrollHelperScheduleCancel || currentlySelectedCategory === null) { + if (scrollHelperScheduleCancel || scrollHelperScheduleEvent === "" || currentlySelectedCategory === null) { scrollHelperScheduleCancel = false; return; } + $.DispatchEvent(scrollHelperScheduleEvent, currentlySelectedCategory.panel); - $.Schedule(0.1, ScrollHelperSchedule); + $.Schedule(scrollHelperSpeed, ScrollHelperSchedule); } + +/** + * Start scrolling the category page in a direction. + * @param {"ScrollDown"|"ScrollUp"|string} eventName Name of the event to fire on the current category. + */ +function StartScrollHelper(eventName) { + if (scrollHelperScheduleEvent !== eventName) { + scrollHelperScheduleEvent = eventName; + $.Schedule(scrollHelperSpeed, ScrollHelperSchedule); + } +} + +/** + * Stop scroll the category page. + */ +function StopScrollHelper() { + scrollHelperScheduleCancel = true; + scrollHelperScheduleEvent = ""; +} + function ScrollHelperClick() { if (currentlySelectedCategory === null) return; @@ -1147,11 +1172,11 @@ function ScrollHelperClick() { // Scroll helpers for sub-menus // Valve kindly didn't allow us to raytrace click panels like the main menu // so this is a work around for scrolling - $('#ScrollHelperDown').SetPanelEvent("onmouseover", () =>{ $.Schedule(0.1, ScrollHelperSchedule); scrollHelperScheduleEvent="ScrollDown"}); - $('#ScrollHelperDown').SetPanelEvent("onmouseout", () => scrollHelperScheduleCancel = true); + $('#ScrollHelperDown').SetPanelEvent("onmouseover", () => StartScrollHelper("ScrollDown")); + $('#ScrollHelperDown').SetPanelEvent("onmouseout", () => StopScrollHelper()); $('#ScrollHelperDown').SetPanelEvent("onactivate", ScrollHelperClick); - $('#ScrollHelperUp').SetPanelEvent("onmouseover", () =>{ $.Schedule(0.1, ScrollHelperSchedule); scrollHelperScheduleEvent="ScrollUp"}); - $('#ScrollHelperUp').SetPanelEvent("onmouseout", () => scrollHelperScheduleCancel = true); + $('#ScrollHelperUp').SetPanelEvent("onmouseover", () => StartScrollHelper("ScrollUp")); + $('#ScrollHelperUp').SetPanelEvent("onmouseout", () => StopScrollHelper()); $('#ScrollHelperUp').SetPanelEvent("onactivate", ScrollHelperClick); $.Schedule(1.0, () => panelReady = true); From 26ddcbcae88e7f46481d0423f9b62982fe126169 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Fri, 11 Jul 2025 17:24:55 +1200 Subject: [PATCH 091/101] Update alyxlib tab defaults --- scripts/vscripts/alyxlib/debug/debug_menu.lua | 58 +++++++++++++------ 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index 5dd2793..85f32b7 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -764,35 +764,53 @@ end -- AlyxLib defaults -DebugMenu:AddCategory("alyxlib", "AlyxLib") +local categoryId = "alyxlib" -DebugMenu:AddToggle("alyxlib", "alyxlib_noclip_vr", "NoClip VR", "noclip_vr") +DebugMenu:AddCategory(categoryId, "AlyxLib") -DebugMenu:AddToggle("alyxlib", "alyxlib_godmode", "God Mode", "god") +DebugMenu:AddSeparator(categoryId, nil, "Basic") -DebugMenu:AddToggle("alyxlib", "alyxlib_lefthanded", "Left Handed", "hlvr_left_hand_primary") +DebugMenu:AddToggle(categoryId, "noclip_vr", "NoClip VR", "noclip_vr", function () + return Convars:GetBool("noclip_vr_enabled") +end) +DebugMenu:AddLabel(categoryId, "noclip_vr_label", "Hold movement trigger to boost") +DebugMenu:AddSlider(categoryId, "noclip_vr_speed", "NoClip VR Speed", 0.5, 10, false, "noclip_vr_speed", 2) +DebugMenu:AddSlider(categoryId, "noclip_vr_boost_speed", "NoClip VR Boost Speed", 0.5, 10, false, "noclip_vr_boost_speed", 2) + +DebugMenu:AddToggle(categoryId, "buddha", "Buddha Mode", "buddha") + +DebugMenu:AddToggle(categoryId, "lefthanded", "Left Handed", "hlvr_left_hand_primary") -DebugMenu:AddSeparator("alyxlib") +DebugMenu:AddToggle(categoryId, "gameinstructor", "Game Instructor Hints", +function(on) + Convars:SetBool("gameinstructor_enable", on) + Convars:SetBool("sv_gameinstructor_disable", not on) +end, +function() + return Convars:GetBool("gameinstructor_enable") and not Convars:GetBool("sv_gameinstructor_disable") +end) + +DebugMenu:AddSeparator(categoryId, nil, "Equipment") -DebugMenu:AddButton("alyxlib", "alyxlib_giveammo", "Give 999 Ammo", function() +DebugMenu:AddButton(categoryId, "giveammo", "Give 999 Ammo", function() SendToConsole("hlvr_setresources 999 999 999 " .. Player:GetResin()) end) -DebugMenu:AddButton("alyxlib", "alyxlib_giveresin", "Give 999 Resin", function() +DebugMenu:AddButton(categoryId, "giveresin", "Give 999 Resin", function() SendToConsole("hlvr_addresources 0 0 0 " .. (999 - Player:GetResin())) end) -DebugMenu:AddSeparator("alyxlib") +DebugMenu:AddSeparator(categoryId, nil, "Session") local isRecordingDemo = false local currentDemo = "" -DebugMenu:AddButton("alyxlib", "alyxlib_demo_recording", "Start Recording Demo", function() +DebugMenu:AddButton(categoryId, "demo_recording", "Start Recording Demo", function() if isRecordingDemo then SendToConsole("stop") currentDemo = "" isRecordingDemo = false - DebugMenu:SetItemText("alyxlib", "alyxlib_demo_recording", "Start Recording Demo") + DebugMenu:SetItemText(categoryId, "demo_recording", "Start Recording Demo") else local localtime = LocalTime() -- remove all whitespace and slashes @@ -800,16 +818,20 @@ DebugMenu:AddButton("alyxlib", "alyxlib_demo_recording", "Start Recording Demo", currentDemo = "demo_" .. sanitizedMap .. "_" .. localtime.Hours .. "-" .. localtime.Minutes .. "-" .. localtime.Seconds SendToConsole("record " .. currentDemo) isRecordingDemo = true - DebugMenu:SetItemText("alyxlib", "alyxlib_demo_recording", "Stop Recording Demo") + DebugMenu:SetItemText(categoryId, "demo_recording", "Stop Recording Demo") end end) -DebugMenu:AddSeparator("alyxlib") +DebugMenu:AddSeparator(categoryId) -DebugMenu:AddButton("alyxlib", "alyxlib_enableextras", "Enable Extras Tab", function() - require "alyxlib.debug.debug_menu_extras" - -- Update the panel immediately - local id = "alyxlib_extras" - DebugMenu:SendCategoryToPanel(DebugMenu:GetCategory(id)) - DebugMenu:SetCategoryIndex(id, 2) +DebugMenu:AddButton(categoryId, "enableextras", "Enable Extras Tab...", function() + if package.loaded["alyxlib.debug.debug_menu_extras"] == nil then + require "alyxlib.debug.debug_menu_extras" + -- Update the panel immediately + local id = "alyxlib_extras" + DebugMenu:SendCategoryToPanel(DebugMenu:GetCategory(id)) + DebugMenu:SetCategoryIndex(id, 2) + ---@TODO Allow disabling extras tab + DebugMenu:SetItemText(categoryId, "enableextras", "Extras Tab Enabled!") + end end) \ No newline at end of file From 4a8fe057417da370981333f674f767144ffc2938 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Fri, 11 Jul 2025 17:27:39 +1200 Subject: [PATCH 092/101] Add version --- scripts/vscripts/alyxlib/debug/debug_menu.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index 85f32b7..5cb1278 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -16,6 +16,7 @@ RegisterAlyxLibConvar("alyxlib_debug_menu_hand", "1", "Hand to attach the debug --- ---@class DebugMenu DebugMenu = {} +DebugMenu.version = "v1.0.0" ---@class DebugMenuCategory ---@field id string @@ -834,4 +835,6 @@ DebugMenu:AddButton(categoryId, "enableextras", "Enable Extras Tab...", function ---@TODO Allow disabling extras tab DebugMenu:SetItemText(categoryId, "enableextras", "Extras Tab Enabled!") end -end) \ No newline at end of file +end) + +return DebugMenu.version \ No newline at end of file From ac160b1a45e58bb5250c6eb56f42f7243110650c Mon Sep 17 00:00:00 2001 From: FrostSource Date: Fri, 11 Jul 2025 17:31:12 +1200 Subject: [PATCH 093/101] Add vis to alyxlib_extras tab --- .../vscripts/alyxlib/debug/debug_menu_extras.lua | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/scripts/vscripts/alyxlib/debug/debug_menu_extras.lua b/scripts/vscripts/alyxlib/debug/debug_menu_extras.lua index da77469..e4b9dac 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu_extras.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu_extras.lua @@ -9,17 +9,21 @@ local version = "v1.0.0" require "alyxlib.debug.debug_menu" -local id = "alyxlib_extras" +local categoryId = "alyxlib_extras" -DebugMenu:AddCategory(id, "AlyxLib Extras") +DebugMenu:AddCategory(categoryId, "AlyxLib Extras") -DebugMenu:AddToggle(id, id.."_showtriggers", "Show Triggers", "showtriggers") +DebugMenu:AddSeparator(categoryId, nil, "Display") -DebugMenu:AddToggle(id, id.."_luxels", "Mat Luxels", "mat_luxels") +DebugMenu:AddToggle(categoryId, "showtriggers", "Show Triggers", "showtriggers") -DebugMenu:AddToggle(id, id.."_fullbright", "Fullbright", "mat_fullbright") +DebugMenu:AddToggle(categoryId, "luxels", "Mat Luxels", "mat_luxels") -DebugMenu:AddToggle(id, id.."_visfreeze", "Freeze Vis", function(on) +DebugMenu:AddToggle(categoryId, "fullbright", "Fullbright", "mat_fullbright") + +DebugMenu:AddToggle(categoryId, "visibility", "Vis", "vis_enable") + +DebugMenu:AddToggle(categoryId, categoryId.."_visfreeze", "Freeze Vis", function(on) if on then SendToConsole("vis_debug_show 1") Player:Delay(function() From 6dea2175004437e64bd6c14996b1fd59a6d281fe Mon Sep 17 00:00:00 2001 From: FrostSource Date: Sun, 13 Jul 2025 21:22:49 +1200 Subject: [PATCH 094/101] Improve comments --- scripts/vscripts/alyxlib/debug/debug_menu.lua | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index 5cb1278..57d4a29 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -18,25 +18,31 @@ RegisterAlyxLibConvar("alyxlib_debug_menu_hand", "1", "Hand to attach the debug DebugMenu = {} DebugMenu.version = "v1.0.0" +--- +---A category of items in the debug menu. +--- ---@class DebugMenuCategory ----@field id string ----@field name string ----@field items DebugMenuItem[] +---@field id string # The unique ID for this category. +---@field name string # The display name for this category. +---@field items DebugMenuItem[] # The items in this category. +--- +---An item in the debug menu. +--- ---@class DebugMenuItem ----@field categoryId string ----@field id string ----@field text string ----@field callback function +---@field categoryId string # The ID of the category this item is in. +---@field id string # The unique ID for this item. +---@field text string # The text to display for this item (if applicable). +---@field callback function # The function to call when this item is clicked. ---@field type "button"|"toggle"|"separator"|"slider"|"cycle" # Type of menu element this item is. ---@field default any|function # The default value sent to the menu. If this is a function the return value will be used. ---@field min number # Minimum value of this slider. ---@field max number # Maxmimum value of this slider. ---@field isPercentage boolean # If true, this slider displays its value as a percentage of min/max. ---@field convar string # The console variable associated with this element. ----@field values any[] # Text/value pairs for this cycler. ----@field truncate number # Used for slider ----@field increment number # Used for slider +---@field values {text:string,value:any}[] # Text/value pairs for this cycler. +---@field truncate number # The number of decimal places to truncate the slider value to (-1 for no truncating). +---@field increment number # The increment value to snap the slider value to (0 for no snapping). ---The panel entity. ---@type CPointClientUIWorldPanel @@ -269,7 +275,7 @@ end --- ---Returns whether the debug menu is currently open. --- ----@return boolean +---@return boolean # True if the debug menu is open function DebugMenu:IsOpen() return self.panel ~= nil and debugMenuOpen end @@ -599,8 +605,8 @@ function DebugMenu:SetCategoryIndex(categoryId, index) end ---Resolves the default value of an element by running any value getter functions. ----@param default any ----@return any +---@param default any|fun():any # The default value to resolve +---@return any # The resolved value local function resolveDefault(default) if type(default) == "function" then return default() @@ -613,7 +619,7 @@ end --- ---This should only be used if modifying the menu in a non-standard way. --- ----@param category DebugMenuCategory +---@param category DebugMenuCategory # The category to send function DebugMenu:SendCategoryToPanel(category) if not self.panel then return @@ -763,7 +769,10 @@ if Convars:GetInt("developer") > 0 then end, nil) end --- AlyxLib defaults +--[[ + Default AlyxLib tab +]] +---@TODO Move to its own file local categoryId = "alyxlib" From 8da51eb92b4964fddd08127311b8b6403448edfc Mon Sep 17 00:00:00 2001 From: FrostSource Date: Sun, 13 Jul 2025 21:58:14 +1200 Subject: [PATCH 095/101] Turn label into a class --- .../scripts/custom_game/alyxlib_debug_menu.js | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/panorama/scripts/custom_game/alyxlib_debug_menu.js b/panorama/scripts/custom_game/alyxlib_debug_menu.js index 87c0073..bce0f99 100644 --- a/panorama/scripts/custom_game/alyxlib_debug_menu.js +++ b/panorama/scripts/custom_game/alyxlib_debug_menu.js @@ -229,9 +229,8 @@ class Category AddLabel(id, text) { - let label = $.CreatePanel("Label", this.content, `${this.id}_${id}`); - label.AddClass("custom_label"); - label.text = text; + const label = new SubMenuLabel(`${this.id}_${id}`, text); + label.AddToPanel(this.content); this.items.push(label); } @@ -761,6 +760,43 @@ class SubMenuSeparator } } +class SubMenuLabel +{ + /** + * Creates a new sub menu label instance. + * @param {string} id Id for this label. + * @param {string} text Text to display with this label. + */ + constructor(id, text) + { + this.id = id; + this.text = text; + + /**@type {Panel} */ + this.panel = null; + } + + /** + * Creates all required elements as children of `panel`. + * @param {Panel} panel Panel to add this label to. + */ + AddToPanel(panel) + { + this.panel = CreatePanel("Label", this.content, this.id, "custom_label"); + this.panel.text = this.text; + } + + /** + * Sets or removes the text displayed with this label. + * @param {string?} text The text to display with this label. + */ + SetText(text = "") + { + this.text = text; + this.panel.text = text; + } +} + /** * Shows a specific category and hides all others. * @param {string} id ID of the category to show. From 4c849303c1ad8bfe061f741604cddda57ccb070f Mon Sep 17 00:00:00 2001 From: FrostSource Date: Sun, 13 Jul 2025 21:58:29 +1200 Subject: [PATCH 096/101] Display demo filename in menu --- scripts/vscripts/alyxlib/debug/debug_menu.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index 57d4a29..46cd72e 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -815,6 +815,8 @@ DebugMenu:AddSeparator(categoryId, nil, "Session") local isRecordingDemo = false local currentDemo = "" +DebugMenu:AddLabel(categoryId, "demo_recording_label", "Last Demo: N/A") + DebugMenu:AddButton(categoryId, "demo_recording", "Start Recording Demo", function() if isRecordingDemo then SendToConsole("stop") @@ -827,6 +829,7 @@ DebugMenu:AddButton(categoryId, "demo_recording", "Start Recording Demo", functi local sanitizedMap = GetMapName():gsub("%s+", ""):gsub("/", "_") currentDemo = "demo_" .. sanitizedMap .. "_" .. localtime.Hours .. "-" .. localtime.Minutes .. "-" .. localtime.Seconds SendToConsole("record " .. currentDemo) + DebugMenu:SetItemText(categoryId, "demo_recording_label", "Last Demo: " .. currentDemo .. ".dem") isRecordingDemo = true DebugMenu:SetItemText(categoryId, "demo_recording", "Stop Recording Demo") end From 8c13f5ba0f8586fe1111018d30271007d86b9d7a Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 17 Jul 2025 16:23:58 +1200 Subject: [PATCH 097/101] Update versions --- scripts/vscripts/alyxlib/debug/vr.lua | 4 ++-- scripts/vscripts/alyxlib/helpers/animation.lua | 4 ++-- scripts/vscripts/alyxlib/helpers/easyconvars.lua | 4 ++-- scripts/vscripts/alyxlib/panorama/core.lua | 4 ++-- scripts/vscripts/alyxlib/player/hands.lua | 4 ++-- scripts/vscripts/alyxlib/precache.lua | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/scripts/vscripts/alyxlib/debug/vr.lua b/scripts/vscripts/alyxlib/debug/vr.lua index 5537df4..9a5be9d 100644 --- a/scripts/vscripts/alyxlib/debug/vr.lua +++ b/scripts/vscripts/alyxlib/debug/vr.lua @@ -1,5 +1,5 @@ --[[ - v1.0.1 + v1.1.0 https://github.com/FrostSource/alyxlib Adds console commands to help debugging VR specific features. @@ -9,7 +9,7 @@ require "alyxlib.debug.vr" ]] -local version = "v1.0.1" +local version = "v1.1.0" ---Get a hand entity from its name. ---@param handName string diff --git a/scripts/vscripts/alyxlib/helpers/animation.lua b/scripts/vscripts/alyxlib/helpers/animation.lua index 4510fee..7795e75 100644 --- a/scripts/vscripts/alyxlib/helpers/animation.lua +++ b/scripts/vscripts/alyxlib/helpers/animation.lua @@ -1,5 +1,5 @@ --[[ - v1.1.0 + v1.2.0 https://github.com/FrostSource/alyxlib Provides functionality for animating getter/setter entity methods using curves. @@ -15,7 +15,7 @@ ---@diagnostic disable: undefined-field ---@class Animation Animation = {} -Animation.version = "v1.1.0" +Animation.version = "v1.2.0" local localBounceOut = function(a, b, t) local normalizedT = t diff --git a/scripts/vscripts/alyxlib/helpers/easyconvars.lua b/scripts/vscripts/alyxlib/helpers/easyconvars.lua index c1e40d1..ae7d0ac 100644 --- a/scripts/vscripts/alyxlib/helpers/easyconvars.lua +++ b/scripts/vscripts/alyxlib/helpers/easyconvars.lua @@ -1,5 +1,5 @@ --[[ - v1.2.0 + v2.0.0 https://github.com/FrostSource/alyxlib Allows for quick creation of convars which support persistence saving, checking GlobalSys for default values, and callbacks on value change. @@ -16,7 +16,7 @@ require "alyxlib.globals" --- ---@class EasyConvars EasyConvars = {} -EasyConvars.version = "v1.2.0" +EasyConvars.version = "v2.0.0" ---Data of a registered convar. ---@class EasyConvarsRegisteredData diff --git a/scripts/vscripts/alyxlib/panorama/core.lua b/scripts/vscripts/alyxlib/panorama/core.lua index e73dc3b..86eb7d7 100644 --- a/scripts/vscripts/alyxlib/panorama/core.lua +++ b/scripts/vscripts/alyxlib/panorama/core.lua @@ -1,12 +1,12 @@ --[[ - v1.1.0 + v1.1.1 https://github.com/FrostSource/alyxlib Panorama core library. ]] Panorama = {} -Panorama.version = "v1.1.0" +Panorama.version = "v1.1.1" ---Filters text string to replace problematic characters. ---@param text string diff --git a/scripts/vscripts/alyxlib/player/hands.lua b/scripts/vscripts/alyxlib/player/hands.lua index dc5f1f0..d4f57cf 100644 --- a/scripts/vscripts/alyxlib/player/hands.lua +++ b/scripts/vscripts/alyxlib/player/hands.lua @@ -1,12 +1,12 @@ --[[ - v1.2.0 + v1.3.0 https://github.com/FrostSource/alyxlib Code for player hands. ]] -local version = "v1.2.0" +local version = "v1.3.0" ---Merge an existing prop with this hand. ---@param prop EntityHandle|string # The prop handle or targetname. diff --git a/scripts/vscripts/alyxlib/precache.lua b/scripts/vscripts/alyxlib/precache.lua index 961378a..d728826 100644 --- a/scripts/vscripts/alyxlib/precache.lua +++ b/scripts/vscripts/alyxlib/precache.lua @@ -1,5 +1,5 @@ --[[ - v1.0.1 + v1.0.2 https://github.com/FrostSource/alyxlib Precaching can only be done with an entity attached script, so this script collects a list of assets to be automatically @@ -31,7 +31,7 @@ if thisEntity then return end -local version = "v1.0.1" +local version = "v1.0.2" require "alyxlib.player.core" From e33f11a42cab6495944a902e2dbcd9673eca7130 Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 17 Jul 2025 17:06:54 +1200 Subject: [PATCH 098/101] Increase major version number The update to EasyConvars is a breaking change, it needs to be reflected --- scripts/vscripts/alyxlib/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vscripts/alyxlib/init.lua b/scripts/vscripts/alyxlib/init.lua index 5171851..4b5439b 100644 --- a/scripts/vscripts/alyxlib/init.lua +++ b/scripts/vscripts/alyxlib/init.lua @@ -16,7 +16,7 @@ local version = "v1.2.2" ---ID of AlyxLib in the Steam workshop ALYXLIB_WORKSHOP_ID = "3329679071" ---The current version of AlyxLib as a whole -ALYXLIB_VERSION = "v1.4.0" +ALYXLIB_VERSION = "v2.0.0" print("Initializing AlyxLib system ".. ALYXLIB_VERSION .." ...") From 72e1cb2ce77d03ffe6423a145066e7b0a318986c Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 17 Jul 2025 17:07:21 +1200 Subject: [PATCH 099/101] Add changes to workshop changelog --- workshop_changelog | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/workshop_changelog b/workshop_changelog index 2d47d8b..bb23126 100644 --- a/workshop_changelog +++ b/workshop_changelog @@ -1,3 +1,15 @@ +Update v2.0.0 (Major update) + +New: + +Added a new in-game menu. Addons can create their own tabs to allow in-game customization. + +The menu can be enabled by adding "-dev" to the Half-Life: Alyx launch parameters in Steam. After loading into a map, press the pause menu button 3 times in a row to open the AlyxLib menu. + +Any AlyxLib addons using EasyConvars will need to be updated to the new standards to be compatible. + +There are also many various improvements and fixes. + Update v1.3.1 (Patch update) General: From a18fa78365572184e1db8ed20d48abfbb476f0ff Mon Sep 17 00:00:00 2001 From: FrostSource Date: Thu, 17 Jul 2025 17:08:14 +1200 Subject: [PATCH 100/101] Change logo Previous logo test was an AI generated image. A simple custom made logo seems more appropiate. --- README.md | 5 +---- assets/256x256.png | Bin 50250 -> 0 bytes assets/alyxlib_logo_256x256.png | Bin 0 -> 9568 bytes 3 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 assets/256x256.png create mode 100644 assets/alyxlib_logo_256x256.png diff --git a/README.md b/README.md index 4c82bba..8801263 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,7 @@

- - AlyxLib Logo + AlyxLib Logo

diff --git a/assets/256x256.png b/assets/256x256.png deleted file mode 100644 index b6264180547c052bc00e7595e0380789cbe12f52..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50250 zcmV)UK(N1wP)4)O00&^-!2tqr01m+Y7!DAC z18@ND$8dlE9DoCGKZXMY-~b$e`!O6K00-az+>hY^0k{uAL?g7X$8Z4tw%`B(xDNpZ z()M^ri6Ib#hM3TnXjC`S@+b-UXGEhqrqKeXA!|_$(;vUlri1~>8B4JpPjm9_(Fk*puid z4|*>B)Vde2?`KAzIr3$9ylwSA%zxyNf1Ul%@(*~QJ^Ul|(}$c-Ke6@#^ojMX9sDWQ zE&$#>e#ob{pLg(Qx1V>=7k0h-urKX?&mrI1^WMY0J^jAJzBm2e!~c8g_uu-#%w0GB zbavS#dlvn`K`*2qJm^`horuprVG^Bx!U%o; zx(fO5x-$LHWFXoHm-+PbPsQCLGD05Zq>%rWpASGkaDV_T0x+Z&@0EQ1kRZC~q2=(x zqh9O4t+@Uxd*&x_WVQ6X?%3mN25<3^!e@YTX)Iq=6io0hH%Yn%fF@;m2Zfe zu%$U)n_H-bwOV6gdSPbAUt2V~wi&f90~}onn0y_)=*t-9FFf>6@1sM*t#^-%2LBAC z{$`+!bJ(%hvh!Zc{=N=y$UYPP`PD0yMrSWy61`&SSmW=OjFab0jz-TJA0y8gAB~hL2^drYEqn|x_ zn7(ISiT=m20ln)to_@as!hJQhe{*nv0CWkQV<*1<@&94+)k90ipHO zS@`(c5AFWYnoH`<@XERAo!3VTQ(NjyGSh6eu3<>})uB@Kaeo-S8I_~6m#suE85yIG z^L_8w(Ghga(2#d19)eMC*rVl9k5q>+t&Vs!2mneYc56f!A`qX57WxqlJ;p&XQx6cA zE(l>96)2``X4x;mI+XoQEyDDtb+8>+OHjt!FsjE$0Gs($Ms6Qksm)oX*7tUjdt zQhySibLa!S|A_qB#SK!sY<}nNdz%|K?4Fyi-CCcSx_s-0p7qt~H!Zo~u5*^X8h_-- zqv!|MROp9R`}<%C{7u0D0?;8)(nJxR_vw{y8H&Do#Ax&4^{35$YUTUquKvuoqelIj zu+`exYK2#gj{4`9$AW*HT#nAH4$%|GhN1_S%4DQEOi^h#!k*9ZE@G%h5L2cxQ@}C9 zTny|lQ{5PjD#!pr*il~ei&0JXDf%_doWieUL`qO4 zM!%>hi46apwiwZ9d7PvG^03FwDVL}>JlZOiMxxTlC_bc8X*^=pTK`33E4|kpyc&O| zK3lu7UL*I;OozX_`!&li-uw@fZ)|+v;3J!#d{~A2_T#=yq(k;Rx-Iq^xi7*20#E?- zTL+cscaB&(^~E)RKlkbNpKtu-TUXX=&8-kcKd4TW-a0Z=ewG)YV|<@hhsxAL5SB3F zDgf7O`NF4;o=OGJwXj1rkVZbn#YN+Q#P6by_#}=el5ZO%&mX+HTq7q(VHzVQ?;ly$ zBXDslJ&@(lYMKaWWo6f?=9OtD`~dJA5m^zyzUXZKC~9UC8cS*02rSFMn$&sP-V&J;N3 z=8Tpj8bgst{^dV&8X#~)Fl>#40-3l-4IIw}uFKy+5{)Eca*i*E0zX$FoaFHQLPZ#C zA`)|A3qg@Ofp8UqeUnW?f)Ma~F&&%h*%6X|^);Zz!bA|(A}|lY$78)q?4X%TXygxa z{1nmnCru7)dY3s_06uAzJSL%F+6owId&6T0#)iGM%U1=bjW2=s)u!kFGQWA_4ZAOR z?ZvfstbZ8b{oW( zNGv4+DjR3u8nLO1$i z6bXXFG$DeyW(3^Q0ijnNCTOS<9zMB>oasaJ+qGS_yZ3zWV_&cR>!Hu`{^Oxl`iBb- z5COO!6M%@;J-X@p9{Jw!YiGZF@Ov6t{&00OYW;fY1btPxL=N`LEngV7nBt1Kp^Ysn z8bwGc%SxP9+|aOcu6uG_ll?{9QY{FCl%x%ZmJLz$>J^d;by!H~5?S>^=hi%|uE)xp zM+8w&^o63BAn2w+&?JrYLWX7^<$VP~K~W|F;bTK;X$X9ZkRc>UBR!S;no<-KC{2{C zhHQcbG3PgQk&nkFMDB3hKJD+~4P)7xkTR6rf}1#*7=n-|`VY#0s#UUVVnuXDP+7RN zzGugVnM>YwA${+m$I~xAJK(ktMnKU0G0^IMNdV~2P7Cm*M?HG>vNwEhVSeV;>Ja^x zau6L;!jaFgP;h!0>&r$%M;uEXu1iyTu%joZ6yNxmvaZG7+nufe+2S@$Pai_o{Vf&S zRtDm}>?=v)2SlZDKiPTqb7c3I4GDUaBSGYL<4PtHfi${limJ+C~3R*fz5UtXK5 zT|52ft9~)_PwQWRKKG206wK~lMBsiy0Jv{HyzIHd3l|^yym0r;KX0|rFUCj9r&R+w z%-9ANoJwn=g6nJJ8@JYTPR4a~DcRe=5sm;&GBAD;kvg-|?b+{9Y~0hxucV1i5MOsL z&$T(ZI>aRj)ogBQIJ5ezZ4oFTxkPF#%D*o48~*IEN`kboL6aO`oI{jN0`jF1GY<|o zSMNtObq%tvPoGc0{G*y^YQ!hb`9f?_$F+YGOJ67^Ff;_-_!2ld7;Sxh&&_}Mee-RH zzY5Mjr9z|denSNA7X%>s*{P-4MTb4RcGq>k^-8TvJ-_)lT#bAxj9Xk|N49GW$%QAy zd!!aIm4;RVQo z#7&bDM^}&|0kl8AixXUlfnsWimxz*(R@#b>@Z?1FV9%fZ?4Db1{?o!gAO33kiAR?@ zAJ}~t{$>dPB@rT9zF&#H{lG^xw%zj6(rD?Mqf7AdL8-(ryhO27=dolsT%VP`X^!Ab z*OxjlQMxXVff1^!)YGLd9CIWsmV2TDOUFg(^>UpW`SLezN!QGmlNIeLR64TVZqlGO;nsh z+$lJ2$|(7~mMz4b53#hQ+@$KgA!)m?39z!m8mT~dVALcMsm@XT8=D_L@X_RQIKm%l zTu|G%>Ds9`t$P9eZ{7n)7LR+Zy(-YU%uQ>O@7NVC`hwx)P9zlmJ-JZ%o z7%q$R|6;M0sD(+S3BZV+C?QGkOjZoh*jd=wx zDq%xF26=KvmFf>eyo6kKn}*b4b5GB(Y2-miDpTe)v9e|entcMBlz^?2%6R?g>O}J! zGdExPi>WjiMA4yK2#drZk>56gBvAziwvdf#MbzW*ChIVx+-=X-f{OTy3-IvmNGeFu9 zi1MM!b8O>Zp-sWEA-LzeI)3(elFtX)_-5XN8K^PI8DnHtoPsl+ZXj#CNgIXxK8T8= zP+k!`4uji3r1*m|MW_@YG`2E&)bMcq>h1q|#z*M+PZ;`JGZOyI5P*m_arA>n9yP!5 z@}EsEDSa>~2NQ^gwsLC^n@%awM@WmhdYvN0wP~buej<^IkXYQ7=Q{{#FLtX5&aZv< zDiB{v_tZ5|D}z=WXnRnpCGAcsf>9MYpqujh6-Ijee{i&PwfHjJe?X3CGR z;|}CO)ek6D9L2}2H~^c7Ca^}B4eTXIQqP+!jaab&$?@jcJZu##j@!@!SL4QpNK20m z59tmuju6J!qr(&QNE+0BHvOqre+s_&#wxG1P`E(%bGXkx0Q$w_hnj!A_~MZ<`k#J) z#^NI)?@e2>M9_{2gO7;P+flqwsKyyi4{e&wI1M7!|f02!e|y9?sEiyUiT)izV*5{R|Ee`fe%Y$-a1F&mQQg@=GAi_FGt3Z#G<#* zxk>DIcLH$S0GXr?%o#hDI(zaoqw~u~Vx3uB42E0Ueslv_I=;IAiRS7=#O~{bsQYqF zSo*fe`rPh*Nq>HF!3!^m8bi`wZ`lM=<4vkQNf)TjB(z)dnnb(-g_KQ0&Y}EEDakkv z?OQvWOA53~MZw60@w*VtoH8cU6_2CmAhlMser`LrsQ%rJIJ3@4oGd#^69xy;et65epB2HE z2P8dS`6b`)p9$t-?eEv349TfC&>jl!Cvp!|K0+5nAU$5N2zIhwLECvS1MHj#xMz9L z3YM*)$4_m%^YYm@9rDl!nj)N16M_A;fa!gL0Fa+NWF(xrI7 z3X#xYKcfWPcRb@ z`JE6KYC%$3WBzpVdG8zS@jO6Y`Eb??k1L#fD8ft|M4T5vhsUGQWlQLH zcD(w~2h*sz-&QZ)ZwUas;@ zZKpYlpxnwPAegD)0`gpGwFad#BcnnjR0M#Ibl`+aKs5)26i1N(Mw%PaG=~SV^x}R~ zR(43JDID@_6mcg|fNZ?Vj-A$Abbb<2#Ih*XzsvsRHQ5)qHk7vGGvh*fVaYypoPp{O zj0;$KH1bqJ^%OFtJb?0WG~t!dSIEbowo?8c?sxdM-wlWZz2Z6Lse5m_DDdF9n6G52 zUn)gf3Xms)q->;>sAvvJvDNHhF0=IVJo( zMKC&9KWg^28!n?Ce$q;W{Qb@*u-_2?`m57%?cP7Xb7Z*me6Q>={NTkpLJv`jz>lcRqoTaD!B10mK4IK;z^nSkesRl{s9K&M9~|A= zwh6eg_BP1!0ou)&vEz+vGe}(Qb|zDL4Xr{XrQ!o^b3HgZGuSl)6TQS;&~!`mPpq1M z88(ZQ4&TnLnguDFO45eXetaY*XkrTDnv5n+LaYrHO)lP-gEC1Q3o-#iLIp*@H#`^{ z@sFPV>&@Swm%d}D_<{B=>{kSU(i)1UZaXz7mCp4&zM3(KlQTeM*2s0l5nly`M9PT7 zr2wjeee~q%#2$SE%krPl_|&4nJ*K2;=+upDi|WpiwqFbHuha;X*Ic7#CelGh%aLA+ z#zz*}{Or-wcM57LI=K&_^=< z{>^}?PeA-olb z>~Nf{^BL}Mn$af4n~L@CMd#1rt`w4lF$WUQn?*!)MIqM*-w8+W z1X9oA)+ew|&6uyQZbh{uDeF2G56#}kL7z{MkBPr@RrD{@H~jA_=tZyIZvuq&8v+17 zK6)Z-w7%~9;RwZ{&oJAkKuvy*$i`BZeld|E(oY(3r07qSE0CCce{swh%P5#Y8YP-1 zj5*p^XhGM3Vcnje(D^g`;P%HX4xGAwfz`fW{444&b^rZb=kFT2kN^ZE<3vhc#01;s zC+4sE$w^`xw*3J1IRS`B3(*_T3mOaLqd^59gn1Ybk)~V}QMm`b@dqpYz9oIH0#v3S zSUy=T18{1LZ5>_TR7i93dQ7n~#PQgIR2pvq7$c)%pQ%`TR|j=;e#yxdgG_h>B%8B? zmQ0$mwrObT#hf1jamSw%mterDPvFMx$2$S(IQ(RP>cz##7bVA21jV@!87(1E*x}OY zdIClFbLYoBF85@>W`BluF$E?jzD#T~n(p6*ubZs~Oj=EvdViGT_=5oS^PS z*<%W5cZ)fZbC99qr@NPa`mBN&W(T6I@pC&04~vedu^G6Ldx>#b&(S{rUQIwRar}@b zrriUDIGnHpsZHAVG?Vd?$B6!(-&EU4Q)jb7_k(3Aee= zq6POU0iai&Rc_X5AHXHQr1p2CTEyd4ez7P*h2W8l?9-_!hFcpE83ad9l+`BM=dX}g z`Tk=_>flJJ5_ypD8njt!xnbx%RRMPF5~A=2FJaDUWrVM8grUqN=H^S!W&b0;8nB>`G0Q(PT||Ah07Uy5+4SXWvk*k=TQ zl6o+E=hbiZhTt(66G3n><~97d+Rr|>fOC2(v$_CeDtM$edxSg{4Z%#F=B{z= zy|}zL=v{e0sTsC4Ime$bcL7p9eIZSdM6>hL6Q#g4aVhfa^NKXOiI8W$+kg-Aa~@22 zN5jm~Z-)9wAB6cQe*tDrzZ7Pl{%x2#^&2pE+E<}|@+V;ar1!zDHP3}T^Q&NChVPBX ztJ(x9Pk|sLDsU=kqsXZbV19#S)B6XI91)>FpD(J9&{@+{AD1>aLv$o6oAV2ag~rdS zb$)j|v3*4D5M|zx6*FS;O(-N{Y*>)e!xUOc}@bx9(LAoH^om_72%PXld)s9qVXxj) zQ#f+{d8O9;{5!A@!!(RMXhk04dMVTQy(R@Ol=e4`Jg#SBwNZVoT2lpF(R)7Gya3Yf6f2E@U#8}g5^gs zf>LFNc?^Lk4V>cPi+yg~) zMcl#%NL$}+Z-oO$*F*%AK>-H^Nxy%}AWP%FvC7yPwDlrrZ-$6!H&^}Di%R(HrDa_Z0b_(C zhVu&#fx6E4_-*lDeNIkc7ujJM-$f5yF|{6EFOdFpxb* zCHBXcfk}J}W^X_m>-hRPoI3u4*6Gu6`^hIxZq|}j-@s|e3`NF<6x=`S4aUzyQXIaB z{K(VxDo*aToAC`h=?_nwV6*&hO!t?ZndkLK5!MJ#TRJYEDv4gY z19c2QZ2fnFW-PzHw(iS)-lQd7m;*Ie&!YUGXi)}+ccSyB{n+jO;%R;zBl_HK3_~Y8 zL)iYgeKa;>rl?B?-!pV{kRAZftH6?%zYkh3{x;0)852g=m;)m15=dR&ZKPx;;J!a9 zR2QtXKvIMuKk|Q2(B=+^#I2X>IXE^1xp5_eY8Aa|>c&4kjO@iQ;k`xxn7HOOrgpt8 z@bIK3OZic0-DlI=A}Xjyg_>H(uEjP1NQ?Y&eSYf(ApGM7V!tpN&_XRF{@rHxC5BIh z8>Zxa-&Nw7GuaY@Jsj4zfNQoi)iZk@H>dN2kJ; zNd&mnfO<%w&Yp$SMvbUcKCJwvZ6k@bsDghIzVYmOcm#Aj0|yT(M?Dh!=YJ3yVM%sn zdr-bpNmSiL*PCc(k zLD$blqc|pj$iLJ&uDoKr$_>90@*?V&5j=I_n#&$kIM4yXULgRKHqf5CHvTK}!nF`a z9v9~^_x(qrQZM2qys?gjr}1t)24VEXX%7I2U$zGAKZoQm3CPposeL@7Bvp!%@1JX!W7&#wH<2BmK9~7E&TFNI}?MU5qSoFu+<9$Bab`-=AZa}Xv`C# z3h{7ZT}G`tqnQm(u7#~)6r8^}GH`)ShT5O0NF=iX)8x6XWGRnlzms?_lE>McBkeii z%4$$)`HkA#zct@>_+G)yMZXEx^?^Qp`=+U57x9G{*b7t!(fM7Qo{uqJ22to!?)r;l?S5Iv zQ~CKtm7g#fZ~_3-cJpZ$o#s9nyX|)M-396rh%T>hb3!KxFTxPYxb6zr3@a~qJCvuk z!F;m`Tk3WA<{h`f#T#ycpKrYzuH3N&uAANi8)j!=CnF50kREDD3mt&5mS7*2pZOlx zv#?$*_99g@EdnQ>$Mw?}NC5jdpFa^QP1Cs{_SM3z*o6cTU+*4IMwW0=P#I;|5|OhS z=PqA#W8mTwfFGY$#f4|;=z!{bzRQeD%mXv0#$x=70Ji-E-VsZlwTXu zBxeWP7atQ6b1cg$ZW|TBwGlXeeB?xeQJGbyn{97M==vn){;5DB#rJ4)b-7hpFU<`WDA_-iNK@ihUDI zJO_Ynn2kO2Nk4nG4&Ql8MM~&>2Sg*OSp7J>7pu4rl3)PH%I_x-sn2l@L3=*sHV3*Q zv1NC#q(ZC`+OU3JkprN{sMkmo1(m9I4k;~LlUlF?hD9d;Wa`ex_=p}&c&aZC`*jcl zJBBfPXC9p3@YLi1m`2=%nE}-KYe8}3UqR$x@!b=B=hu;s!3>u@-$Z2|9&p>Y;DPUY zEQiz|S$_Rge=%puticEU8mTa3)vcF;4&y3;56JTbNA~>?Mn~siNpu$+ zb^CYW*#CJiJouf@hew?K6gcjE=fGj#{v52l@)t02=iN+VFAFZeJ#4~PZodVt+=;B<4zhUK^f!2wu$n1Qu5UxaFd8Ov%%U z7W>81C3So;2pT8%Xvos> z00N_a9mdC6Fh05m)^58Bj{5!Q;l!`L4IcFF7r?__`D8fcBj>@yU;Yf0vIoDL+6)&m zLNF6GMV~H5%*tyJ5yOZHYAcR_+VOwKt%LGCtMK6Deu?n{&mzbIcM2B#V4)xmqcHn1 z!*i7Qsatd@x?hF_s69O3)A9&B@aDVfCoblG0*gifxNbAIYvx?vM?*N|YmXP)^Vm{i z+9}`LQ|L!nW$kJTi;W&F)dHnMs=eD}0gKpAEAK;N|FreKbq>GrDJ!m@>DSkUMC)*7 zfk!C@mh$7fPW4-jHvaK#6IvIJ+-|2*wJC;|NNsXn3TDOk}^lfXmbWv4e& zO!KBJ&=}wO5aK+7-Z;Mv5dt>CDWVOLxL$Gq)juQka0*P!1TCMNB$i+@E_}Jgy>mj3BUX#BZz*t`;nTZGhkPI;id5krXp6uK-fd z&2?3d>?Q02YO(u0{~VZ|Evay8{*FB5K*7pID9tnxE*I(SE{G^kJrwHfJxC#W4NBKXh-XqBmq<=U=)2x)z@7`f{}7>SjT0I9 zQX=>n%HUT#RET2Bru0RDw+9KBRgrO$8 zjCFGH*hVM4kf?Rvq*c44xPmUAxa3~D*DqHz7Qshc8UVrVxybKxqaZsbq<{(%gn_F8 zzEA~_Kz>iz5x5P3U&nV?pJ?0(wL3Nd;oC2uVwIOm0fdVZYaR%VgB}H-HU`oA4BAAy zlSs#Oo&DZH6Qi6X9YQzDAAwsHIQcc5b()q|apG7R!Hpe=|BDTP^E|{3 z5P(Yrk~RsxZn>0D_DE^?TJa`G<+Rrl_#gDb3dM%FMLzT#cgkVbm zDf$tM;Kq%Ijg8SbSQQ|(0);$6w6U>hEM1Sj06KX6?3D>!o`?7PNN{tG^y0IK4?xOI z2uvd2jUzon_!4F60bvqB#C{_%QubiwWvMX}X! z6I^7d2N8MNW-?PA1n9u8pVawMi#oOQPSUTOhyk$ZBX zdV`#pD93*EWUjx|W3@&`lwVzzcbD@U@4KfmJPVq;_g+zYvdi&ZzkVvtBhiSJ5QIeB zqX~sJ7W!4c^R>JJ7=0E9@= z3Qu8+vy7DPuQ%e5i_41Q%UW<5Ml>5-it7$P_7>9k6eEUYaG;E5NTuuA{STP_W~&KF zf}qo-u`ZE?WSjse0KyQ+b^btm0(|w?6Nt|v3Cm1xsQ48aVj_I$kRwFesfJ`Wo2$3T z@BCesxky1n7(VqSFh`fkjm(i0Yn>tIK8rhF#S+6}0BZ9s1KLPRk`dX*yX!l9?$h_f zjMHH@|3gbW;>}H+oakc!_JF}k0E~j|+&=wGT;W@L+g5z31sScA##B~+Hx^T)=gXs# zt0Z^aNmz(2?j>|FtIJ+=d~ECBI31`l2xxC3NNVUv`1EB1Pa6Z}@7hCv*xSZT48Yu0 zfI+AP!fqg92`DsRIjsHrH=7!RVskpXW}z)g854Ir4t{Qh`q1Fk#S@+bP2>aLx*-&L zF|r7{MsgEbQ1~v2EcF8cr;cCYdMJ$jk8L>pMm84pryhWZh2Zjte+vEB^M#veP{zQ) z5`fPg8H|kwC!mH9|0RPF;=QVRqPF=)S|V^?5QX-W-@zldMfwh|*X~N)I=WehkV5(n zBmj}eqPGdd*hmAOvhX7&kL%b0!Lj-8SgHcN_$rqODld8q_+x9-`j6j=o6nveR7^&> zp^@Wr<$14%=3G^{2`R;GUcsPEtAl{#Xp1F~qFj&xYtUK!5nVQqFtQsxP3#djKp5;3 z_`~C*6&};_i88zskX3Ss(d&%?Nnc~@)<|ma$(nrDENRAKHCtyh{k3!Jwe#zfjJ@63 zU7x9Qrgz)5Iet1$!37lsB$|G2ph};|Qh`8zWV`yPhyf6vjii8Kcj-u2cKX?(KjP^( zg_T#uvy3SAV5m8dAc^E2!ompF8XkxGAx{Jx$)FgiF)3@giG8;p&Xxkat~q?a=ezqv z*iR|*_p>bju1;?C@8pW~JN}~nWaOYUR+inV13UN_cT(qtI3z}|>_TB5wRCu+3aESlPZ0!eBfz}WVgO72 z?n8isk&NzXqOaT$TXR+|RUl+yMfG`af%+65OjCFT5S50m#C)p2nUv9UFG6~sh`kNj z_wQ7-5#(C=x+#g!@C1Rzwy6_oYjD=6fxRJ0n;5s~;751!D8-S~Ki z<94-#EJJ{86WH5e*SXI=0XiC-sHMoamsd6AkmVwjY5)%$qJl645JBsRXT!*ePd1We z9BN8ImZ0noA$_)7cuDv0@WY|L{$Xrxd_%akOECFd+G?*Ka_Ao1ZcW1CD|)_HHgD@6y=0ITqLOH^ciM=a0JmK=Fgw6N z2!NnQm2scx{73Q)K;*Vx8VQqzIq_H92dHD+H#P6$RwTk_*LX_|E;gjNHi2wEVB6S# zSI2i-vZkQsM*1^JsW2%|nSCq+c%e;==(@73kpg%J!i2#1|1;hR{_;cQD?ssWA|8`Z z^?Odg0QY$!(3Nv#>|`kjgr|Gx3D1JLV7<&6P{*amM#i}7*bbxyz+Ybk)JO!f|Fj=} z5IVn^O5CZ$kCA(Zr{CAH?*fNO}R-64^zM zfXUNe1$OKj9gq@)*fK18KGgg&geRRLgJU2z-MFJR*YV_+6h|I#Heuh7SFobGQY+H$ z+XR~#NcUHy9*i#ans;8a;dqX7`&Ask%&x}cf~pejK`unGVD0EOi}M>%5nPU_2!|aP zfa50n?*jP?tp0<5++2i?JBXRF%ao}y{85t~l`&?^&t0GMVdSiL0WA-Ub7i)lo{x;| zL#hS*&fX{=M%531v>Gt-yt83;VVIEwEGiMlRdp@Ssl75{uA>~ULx0doB2s%O$d2f^ zCZM?hbasQ>FM@#HcR;Loy8;m*@n5STuIb5PoXBgGsz8qK@&R*UKq^2JjSl(8@j7Nm znU>>>z2pPvg=QwreqO__s4=Rv&DQI{X3*V(ba#4x*?!6UwX+Y}{ka*C_A!pkb4Q#G zLk~FxVp5s7I&~Fy^W8H)3scQT`?1>nu=%Na9yEp-@*j0Nc%ctsoeRv@fj~*fMA88p z>iUl?EFwxp+wxMzF1p4-q2lDa;GXv)z?4c4q3RB`qQBX&O zVd*B~s!&M9c2nYWWF5Er$$9@a$Ipx(up!4!pQ~f`?bmm58?s{kUA;b+@THLigN_AI z0nYa36_(*I!XC`elwk5D?*bkHBy9rPZXsKg@qGM=uGzB-ZkpKzVFzl0PSq%T0kqin z>X~nX*;%CQ7bRFL^?)vDc zCSc6bb+DUkNmr!VH^6ZFu<&_ZX!!v3M(YIFy=fpK)PP7Z^;?bRF%(+Z7Gm96G|z&%%2JfqxPA3IAN}D}vR=R- z@G~PfKo|nyD5#8725^Hxt>C3An=Leqc%3`OM-Z>(8(CH%hqF-=hfdNWz7xsk?yc92 z4xfkM{pyT`e*Gdyg)ioT)IBBYYrQh<=jNGpd+HB>$y49J=z%W~KcZCm`LlFxB+i*) zhpw602~9R%EheAdFuk3f8Wavp4}FwL0X2_xa_U*otg-8&k}4afU9_5GYT+mQr6bzW z%dpOO*OT6%pj$0YbABCS+FP80tTWL`g@ta%m2oe|R3|18Y}>ZyctZQ%G-NOYpg#BL z;ZaWYOVZe^C)1_q$~XjyHsq?a8PLC>XAm6Ug)U!TzxQ`VLLx(k7ntBxJ@frQhezWj zh%(H2WLZ0Wu3* zsd!Gd9BrFjm(JEKg2ZmU@|UizPeXqbgbO)Bm+pHvdsQ*QvX4-6&-@YWeeE9vU}pR5 zi89}xAQ8R}metpd^`~mGLFeqpaoP#}R)4Vf3=s7K9L}GT2(Wb{Vx)%5So{p=->+Yw z^HaBFoNp=`X&Z?Stwl!c7rH&?@x+7Lny10giBA;u0zk$10gWrz#4k0bxc4ly;HLR$ zSRes-JLdr1wVB;6fLmvFO7&cD)%1cjIqc#uz!<7B#P=Y2#2FBV89Kbm3!sWaCTs#3 zdjTeF1QANd@;f>|Rr@T+d5(&5`np?QE>>Z(*F|h|NYn%*#7te3xw8

Jb#% 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 diff --git a/assets/alyxlib_logo_256x256.png b/assets/alyxlib_logo_256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..7d36a5a4e3ffce834771837b1ebab65b1c8cc125 GIT binary patch literal 9568 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 cav>DFhiHq9W_tMzcuNYTt7)iFrREUvKk= Date: Thu, 17 Jul 2025 17:22:07 +1200 Subject: [PATCH 101/101] Update version manifest --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index a8fa482..a06dd3f 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "1.4.0" + "version": "2.0.0" } \ No newline at end of file