diff --git a/.gitignore b/.gitignore index 39a0086..e66ec9a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,16 @@ +# Source Engine /_bakeresourcecache/ *bakeresourcecache.vpk + +# Tooling __pycache__ /release/ /test_release/ __test* *.bak + +# MkDocs +.cache/ check_lua_versions.sh +serve_mobile.bat \ No newline at end of file diff --git a/.vscode/alyxlib.code-snippets b/.vscode/alyxlib.code-snippets index fc449d3..30c9b08 100644 --- a/.vscode/alyxlib.code-snippets +++ b/.vscode/alyxlib.code-snippets @@ -10,7 +10,7 @@ "if thisEntity then", "\t-- Inherit this script if attached to entity", "\t-- Will also load the script at the same time if needed", - "\tinherit(GetScriptFile())", + "\tinherit(\"${1:EntityName}\")", "\treturn", "end", "", @@ -45,8 +45,8 @@ "scope": "lua", "prefix": ["Game Event"], "body": [ - "---@param params GAME_EVENT_$2", - "base:GameEvent(${1:\"event\"}, function(self, params)", + "---@param event GAME_EVENT_$2", + "base:GameEvent(${1:\"event\"}, function(self, event)", "\t---@cast self ${3:ClassName}", "", "\t$0", @@ -59,8 +59,8 @@ "scope": "lua", "prefix": ["Player Event"], "body": [ - "---@param params PLAYER_EVENT_$2", - "base:PlayerEvent(${1:\"event\"}, function(self, params)", + "---@param event PLAYER_EVENT_$2", + "base:PlayerEvent(${1:\"event\"}, function(self, event)", "\t---@cast self ${3:ClassName}", "", "\t$0", diff --git a/.vscode/alyxlib_tooling.code-snippets b/.vscode/alyxlib_tooling.code-snippets index f85e7eb..8a55ec5 100644 --- a/.vscode/alyxlib_tooling.code-snippets +++ b/.vscode/alyxlib_tooling.code-snippets @@ -47,7 +47,6 @@ "scope": "lua", "prefix": ["snippet", "tool", "footer initialized"], "body": [ - "print(\"${1:name}.lua \".. ${2:version} ..\" initialized...\")", "", "return ${2:version}", ], diff --git a/.vscode/vlua_snippets.code-snippets b/.vscode/vlua_snippets.code-snippets index 602f8b8..0f58b73 100644 --- a/.vscode/vlua_snippets.code-snippets +++ b/.vscode/vlua_snippets.code-snippets @@ -48,7 +48,7 @@ "Hook: OnTakeDamage":{ "scope": "lua", - "prefix": ["Hook", "OnTakeDamage", "Snippet"], + "prefix": ["Hook", "OnTakeDamageTable", "Snippet"], "body": [ "---@param damageTable TypeDamageTable", "function OnTakeDamage(damageTable)", diff --git a/README.md b/README.md index 8801263..f13cf8a 100644 --- a/README.md +++ b/README.md @@ -1,77 +1,81 @@ -

AlyxLib Logo

+> [!TIP] +> **New:** I've launched a dedicated [Documentation Website](https://frostsource.github.io/alyxlib/) and a standalone [Installer App](https://frostsource.github.io/AlyxLibInstaller/) to make setup easier! +
[![License](https://img.shields.io/badge/License-MIT-04663E)](#license) [![issues](https://img.shields.io/github/issues/FrostSource/alyxlib?color=04663E)](https://github.com/FrostSource/alyxlib/issues) ![GitHub commit activity](https://img.shields.io/github/commit-activity/w/FrostSource/alyxlib?color=04663E) -[![Discord](https://img.shields.io/discord/825047476146012261?style=for-the-badge&logo=discord&logoColor=white&label=discord&logoSize=auto&labelColor=5865F2&color=2ea44f)](https://discord.gg/42SC3Wyjv4 "Join the Discord") -[![Steam](https://img.shields.io/steam/downloads/3329679071?style=for-the-badge&logo=steam&label=steam&logoSize=auto&labelColor=black&color=2ea44f)](https://steamcommunity.com/sharedfiles/filedetails/?id=3329679071 "AlyxLib workshop") +
+ +[![Documentation](https://img.shields.io/badge/๐Ÿ“–_Read_The_Docs-blue?style=for-the-badge)](https://frostsource.github.io/alyxlib/) +[![Installer](https://img.shields.io/badge/๐Ÿ“ฅ_Download_Installer-orange?style=for-the-badge)](https://frostsource.github.io/AlyxLibInstaller/) -[![view - Documentation](https://img.shields.io/badge/view-Documentation-blue?style=for-the-badge)](https://github.com/FrostSource/alyxlib/wiki "Go to project documentation") +
+ +[![Steam](https://img.shields.io/steam/downloads/3329679071?style=for-the-badge&logo=steam&label=steam&logoSize=auto&labelColor=black&color=2ea44f)](https://steamcommunity.com/sharedfiles/filedetails/?id=3329679071 "AlyxLib workshop") +[![Discord](https://img.shields.io/discord/825047476146012261?style=for-the-badge&logo=discord&logoColor=white&label=discord&logoSize=auto&labelColor=5865F2&color=2ea44f)](https://discord.gg/42SC3Wyjv4 "Join the Discord")
--- -> [!CAUTION]\ -> This repository has been made public to create the wiki and may not work as intended until this message is removed! - -AlyxLib provides a set of useful Lua libraries for scripting with Half-Life: Alyx, making your development process smoother and more efficient. +AlyxLib provides a set of useful Lua libraries for scripting with **Half-Life: Alyx**, making your development process smoother and more efficient. Your addon is linked to AlyxLib via symbolic links, ensuring that every addon uses the same source and gets updated automatically whenever AlyxLib is updated. Plus, since your workshop item uses AlyxLib as a requirement, it will also receive any fixes without you having to reupload. -## ๐Ÿ“šLibrary overview +## ๐Ÿ“š Library overview -* Full VScript code completion using [Lua Language Server](https://luals.github.io/) +* Full VScript code completion using [Lua Language Server](https://luals.github.io/). * Save/Load most data types easily to any entity. -* Custom class implementation for entities, including inheritence and automatic variable saving. +* Custom class implementation for entities, including inheritance and automatic variable saving. * Player interaction simplification and tracking of items. -* Panorama panel interaction, allowing sending and receiving data with Lua. +* Send data to and from Panorama โ†โ†’ Lua. * Easy controller input tracking with function callbacks. * Lots of useful debugging functions and console commands. +* A fully customizable in-game debug menu. -See the extensive wiki for full function reference and code examples. +See the [Documentation Website](https://frostsource.github.io/alyxlib/) for component breakdowns, function references, and code examples. -## ๐Ÿš€Quick setup guide +## ๐Ÿš€ Quick setup guide -> [!NOTE]\ -> For in-depth setup, see the [Installing AlyxLib](https://github.com/FrostSource/alyxlib/wiki/Installing-AlyxLib) wiki page +> [!NOTE] +> For in-depth or manual setup, see the [Getting Started Guide](https://frostsource.github.io/alyxlib/getting_started/app_installation.html). -1. Download or clone this GitHub repository to your hlvr_addons content folder +1. **[Download AlyxLib Installer](https://frostsource.github.io/AlyxLibInstaller/)** and run the app. -2. Run `alyxlib.py` from the root folder +2. Follow the setup instructions within the app to download AlyxLib. -3. Select your addon from the list of addons shown +3. Open your addon from the file menu and select the AlyxLib modules you wish to use. -4. Select the type of installation you want for your addon (can be changed later) +4. **Before uploading:** Click the `Remove For Upload` button in the app. -5. Before uploading to the workshop, run the `on_upload.bat` that was created in your addon content folder to temporarily remove symlinks, **but do not -exit the program** +5. Upload your addon to the workshop and set [AlyxLib](https://steamcommunity.com/sharedfiles/filedetails/?id=3329679071) as a required item. -6. Upload your addon to the workshop and set [AlyxLib](https://steamcommunity.com/sharedfiles/filedetails/?id=3329679071) as a required item +6. Rename the `0000000000.lua` file in `scripts/vscripts/mods/init/` to match the workshop ID of your new workshop item [(this is the same process outlined in Scalable Init Support)](https://github.com/PeterSHollander/scalable_init_support?tab=readme-ov-file#for-workshop-release) -7. Rename the `0000000000.lua` file in `scripts/vscripts/mods/init/` to have the same number as your new workshop item [(this is the same process outlined in Scalable Init Support)](https://github.com/PeterSHollander/scalable_init_support?tab=readme-ov-file#for-workshop-release) +7. Click `Install` in the app to restore the AlyxLib files in your addon. -8. Finish the instructions shown in the running `on_upload.bat` program to restore the symlinks for further development +## โ“ Need help? -## โ“Need help? +If you run into issues, feel free to [create an issue](https://github.com/FrostSource/alyxlib/issues). -Please feel free to create an issue with your problem, question or suggestion. +You can also join us on [Discord](https://discord.gg/42SC3Wyjv4) for faster responses. -You can also join us on our [Discord server](https://discord.gg/42SC3Wyjv4) to get faster responses. - -## ๐ŸŒŸProjects using AlyxLib +## ๐ŸŒŸ Projects using AlyxLib

- - - - - -

+ + + + + + +

\ No newline at end of file diff --git a/assets/addon-preview-alyx_wears_glasses.jpg b/assets/addon-preview-alyx_wears_glasses.jpg new file mode 100644 index 0000000..f1993e8 Binary files /dev/null and b/assets/addon-preview-alyx_wears_glasses.jpg differ diff --git a/assets/addon-preview-body_holsters.jpg b/assets/addon-preview-body_holsters.jpg new file mode 100644 index 0000000..5687db4 Binary files /dev/null and b/assets/addon-preview-body_holsters.jpg differ diff --git a/assets/addon-preview-combine_shoot_and_scoot.jpg b/assets/addon-preview-combine_shoot_and_scoot.jpg new file mode 100644 index 0000000..07327a3 Binary files /dev/null and b/assets/addon-preview-combine_shoot_and_scoot.jpg differ diff --git a/assets/addon-preview-partial_clip_storage.jpg b/assets/addon-preview-partial_clip_storage.jpg new file mode 100644 index 0000000..d342ee0 Binary files /dev/null and b/assets/addon-preview-partial_clip_storage.jpg differ diff --git a/assets/addon-preview-removable_health_vials.jpg b/assets/addon-preview-removable_health_vials.jpg new file mode 100644 index 0000000..9deec83 Binary files /dev/null and b/assets/addon-preview-removable_health_vials.jpg differ diff --git a/assets/addon-preview-resin_watch.jpg b/assets/addon-preview-resin_watch.jpg new file mode 100644 index 0000000..ee0c5d8 Binary files /dev/null and b/assets/addon-preview-resin_watch.jpg differ diff --git a/deployment_manifest.json b/deployment_manifest.json index fe064bb..d273137 100644 --- a/deployment_manifest.json +++ b/deployment_manifest.json @@ -82,16 +82,62 @@ "panorama": [ { - "type": "symlink", - "description": "Panorama Lua integration symlink", + "type": "hardlink", + "description": "Panorama Lua integration hardlink", "source": "{AlyxLib}/panorama/scripts/custom_game/panorama_lua.js", "destination": "{AddonContent}/panorama/scripts/custom_game/panorama_lua.js" }, { - "type": "symlink", - "description": "Panorama code completion symlink", + "type": "hardlink", + "description": "Panorama code completion hardlink", "source": "{AlyxLib}/panorama/scripts/custom_game/panoramadoc.js", "destination": "{AddonContent}/panorama/scripts/custom_game/panoramadoc.js" + }, + { + "type": "hardlink", + "description": "DebugMenu layout hardlink", + "source": "{AlyxLib}/panorama/layout/custom_game/alyxlib_debug_menu.xml", + "destination": "{AddonContent}/panorama/layout/custom_game/alyxlib_debug_menu.xml" + }, + { + "type": "hardlink", + "description": "DebugMenu script hardlink", + "source": "{AlyxLib}/panorama/scripts/custom_game/alyxlib_debug_menu.js", + "destination": "{AddonContent}/panorama/scripts/custom_game/alyxlib_debug_menu.js" + }, + { + "type": "hardlink", + "description": "DebugMenu styles hardlink", + "source": "{AlyxLib}/panorama/styles/custom_game/alyxlib_debug_menu.css", + "destination": "{AddonContent}/panorama/styles/custom_game/alyxlib_debug_menu.css" + } + ], + + "upload_removal": [ + { + "type": "delete", + "description": "Delete compiled panorama_lua.js", + "source": "{AddonGame}/panorama/scripts/custom_game/panorama_lua.vjs_c" + }, + { + "type": "delete", + "description": "Delete compiled panoramadoc.js", + "source": "{AddonGame}/panorama/scripts/custom_game/panoramadoc.vjs_c" + }, + { + "type": "delete", + "description": "Delete compiled alyxlib_debug_menu.xml", + "source": "{AddonGame}/panorama/layout/custom_game/alyxlib_debug_menu.vxml_c" + }, + { + "type": "delete", + "description": "Delete compiled alyxlib_debug_menu.js", + "source": "{AddonGame}/panorama/scripts/custom_game/alyxlib_debug_menu.vjs_c" + }, + { + "type": "delete", + "description": "Delete compiled alyxlib_debug_menu.css", + "source": "{AddonGame}/panorama/styles/custom_game/alyxlib_debug_menu.vcss_c" } ], diff --git a/panorama/layout/custom_game/alyxlib_debug_menu.xml b/panorama/layout/custom_game/alyxlib_debug_menu.xml index 3551557..3d68935 100644 --- a/panorama/layout/custom_game/alyxlib_debug_menu.xml +++ b/panorama/layout/custom_game/alyxlib_debug_menu.xml @@ -11,8 +11,7 @@ - - + @@ -20,55 +19,64 @@ + - - - - - - - - + - - - + + + + + + + + + + + - - + + + - - - - - - - - - - - + + - + + + + + + + + - - - + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/panorama/scripts/custom_game/alyxlib_debug_menu.js b/panorama/scripts/custom_game/alyxlib_debug_menu.js index bce0f99..9d2c94b 100644 --- a/panorama/scripts/custom_game/alyxlib_debug_menu.js +++ b/panorama/scripts/custom_game/alyxlib_debug_menu.js @@ -5,6 +5,9 @@ let panelReady = false; +/** @type {Category} */ +let settingsCategory = null; + /** * Fires a Panorama output with the given name and arguments. * The output is routed to the panel's input 'RunScriptCode'. @@ -24,12 +27,12 @@ function FireOutput(outputName, ...args) { } /** - * Maps category id to category object. + * List of all categories. * @type {Category[]} */ let categories = []; -const numberOfVisibleCategories = 5; +let numberOfVisibleCategories = 5; let categoryBarCycleIndex = 0; @@ -43,13 +46,17 @@ let currentlySelectedCategory = null; */ let currentlyActiveButton = null; -function TurnButtonIntoDebugMenuButton(button, callback) +function TurnButtonIntoDebugMenuButton(button, callback, onmouseover, onmouseout) { if (button == null) return; - button.SetPanelEvent("onmouseover", () => currentlyActiveButton = button); + button.SetPanelEvent("onmouseover", () => { + currentlyActiveButton = button + if (onmouseover !== undefined) onmouseover(); + }); button.SetPanelEvent("onmouseout", () => { if (currentlyActiveButton == button) currentlyActiveButton = null; + if (onmouseout !== undefined) onmouseout(); }); if (callback !== null && callback !== undefined) @@ -132,7 +139,10 @@ class Category this.panel.AddClass("scroll"); this.content = $.CreatePanel("Panel", this.panel, `${this.id}_content`); this.content.AddClass("content"); + } + AddToCategoryBar() + { // Create category button this.button = CreateDebugMenuButton($("#CategoryBar"), () => SetCategoryVisible(this.id), "CategoryButton", `${this.id}_button`); @@ -170,17 +180,21 @@ class Category if (visible) { this.panel.AddClass("Visible"); - this.button.AddClass("Selected"); + if (this.button != null) + this.button.AddClass("Selected"); } else { this.panel.RemoveClass("Visible"); - this.button.RemoveClass("Selected"); + if (this.button != null) + this.button.RemoveClass("Selected"); } } SetBarButtonVisible(visible) { + if (this.button == null) return; + if (visible) this.button.visible = true; else @@ -285,16 +299,17 @@ class Category * 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. + * @param {string} title Title for this cycle. + * @param {string[]} values Text values for this cycle. + * @param {number?} selectedIndex Starting selected index [0-n]. */ - AddCycle(id, convar, values, selectedValue) + AddCycle(id, convar, title, values, selectedIndex) { - let cycle = new SubMenuCycle(`${this.id}_${id}`, convar, values, (index) => { + let cycle = new SubMenuCycle(`${this.id}_${id}`, convar, title, values, (index) => { FireOutput("_DebugMenuCallbackCycle", id, index + 1); }); cycle.AddToPanel(this.content); - cycle.SetSelectedValueNoFire(selectedValue); + cycle.SetSelectedIndexNoFire(selectedIndex); this.items.push(cycle); } } @@ -445,7 +460,7 @@ class SubMenuSlider this.text = text; this.min = min; this.max = max; - this.isPercentage = false; + this.isPercentage = isPercentage; this.callback = callback; this.truncate = truncate; @@ -474,7 +489,7 @@ class SubMenuSlider // This only works in VR TurnButtonIntoDebugMenuButton(this.panel, () => { - const pos = GetAffordancePosition(); + const pos = GetAffordancePositionInMenuSpace(); 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 @@ -523,8 +538,12 @@ class SubMenuSlider if (this.isPercentage) valueLabel.text = this.GetValueAsPercentage().toFixed(0); - else - valueLabel.text = this.value.toFixed(this.truncate); + else { + if (this.truncate > -1 && this.truncate < 20) + valueLabel.text = this.value.toFixed(this.truncate); + else + valueLabel.text = this.value; + } } /** @@ -542,11 +561,11 @@ class SubMenuSlider } } -/** - * @typedef {Object} SubMenuCycleItem - * @property {string} text - * @property {string?} value - */ +// /** +// * @typedef {Object} SubMenuCycleItem +// * @property {string} text +// * @property {string?} value +// */ class SubMenuCycle { @@ -554,14 +573,16 @@ class SubMenuCycle * * @param {string} id * @param {string} convar - * @param {SubMenuCycleItem[]} values Maximum of 6 items + * @param {string} title Text show next to each value + * @param {string[]} values Maximum of 6 items * @param {function} callback * @param {number} selectedIndex */ - constructor(id, convar, values, callback, selectedIndex) { + constructor(id, convar, title, values, callback, selectedIndex) { this.id = id; this.convar = convar; - this.values = values;//values.slice(0, 6); + this.title = title; + this.values = values; this.callback = callback; /** @type {Panel} */ @@ -581,8 +602,11 @@ class SubMenuCycle 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; + const label = CreatePanel("Label", col, "item"+index, "cycle_label"); + if (this.title && this.title != "") + label.text = `${this.title} : ${item}`; + else + label.text = item; } CreatePanel("Label", col, "custom", "cycle_label").text = "Custom"; const dotRow = CreatePanel("Panel", col, null, "dot_row"); @@ -616,37 +640,18 @@ class SubMenuCycle /** * Sets the selected option without firing the callback. - * @param {number} index The index of the option to select, starting from 0. + * @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; - } - } - } + let foundValue = false; - /** - * 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); - } + if (typeof index === "number" && Number.isFinite(index)) + index = (index + this.values.length) % this.values.length; - 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) { + if (i == index) { item.visible = true; dot.SetHasClass("cycle_dots_selected", true); foundValue = true; @@ -663,11 +668,34 @@ class SubMenuCycle custom.visible = false; } else { this.selectedIndex = -1; - custom.text = `Custom (${value})`; + // custom.text = `Custom (${value})`; custom.visible = true; } } + /** + * 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) { + // Find the matching value index + for (let i = 0; i < this.values.length; i++) { + const _value = this.values[i]; + if (_value === value) { + this.SetSelectedIndexNoFire(i); + return; + } + } + + // Custom value + this.SetSelectedIndexNoFire(undefined); + } + SetSelectedValue(value) { this.SetSelectedValueNoFire(value); this.callback(this.selectedIndex); @@ -782,7 +810,7 @@ class SubMenuLabel */ AddToPanel(panel) { - this.panel = CreatePanel("Label", this.content, this.id, "custom_label"); + this.panel = CreatePanel("Label", panel, this.id, "custom_label"); this.panel.text = this.text; } @@ -803,6 +831,9 @@ class SubMenuLabel */ function SetCategoryVisible(id) { + // Hide settings if available + if (settingsCategory) settingsCategory.SetVisible(false); + for (const category of categories) { if (category.id == id) @@ -810,6 +841,7 @@ function SetCategoryVisible(id) // category.AddClass("Visible"); category.SetVisible(true); currentlySelectedCategory = category; + $.GetContextPanel().currentlySelectedCategory = category.id; } else { @@ -843,6 +875,10 @@ function CycleCategories(direction) */ function UpdateCategoryBarVisibility() { + const bgPanel = $("#background_panel"); + const width = bgPanel.actuallayoutwidth; + numberOfVisibleCategories = Math.max(1, Math.floor(width / 190)); + const selectedIndex = categories.indexOf(currentlySelectedCategory); if (selectedIndex < categoryBarCycleIndex) { categoryBarCycleIndex = selectedIndex; @@ -850,6 +886,12 @@ function UpdateCategoryBarVisibility() categoryBarCycleIndex = selectedIndex - numberOfVisibleCategories + 1; } + const leftCategoryCount = categoryBarCycleIndex; + $("#CycleCategoryLeftButtonLabel").text = leftCategoryCount > 0 ? leftCategoryCount.toString() : ""; + + const rightCategoryCount = Math.max(0, categories.length - categoryBarCycleIndex - numberOfVisibleCategories); + $("#CycleCategoryRightButtonLabel").text = rightCategoryCount > 0 ? rightCategoryCount.toString() : ""; + categories.forEach((category, index) => { const isVisible = index >= categoryBarCycleIndex && index < categoryBarCycleIndex + numberOfVisibleCategories; category.SetBarButtonVisible(isVisible); @@ -868,10 +910,23 @@ function CreateCategory(id, name) { let category = new Category(id, name); - categories.push(category); + if (id === "settings") { + settingsCategory = category; + } else { + category.AddToCategoryBar(); + categories.push(category); + } - if (currentlySelectedCategory == null) - SetCategoryVisible(category.id); + // Restore selected tab on refresh + if ($.GetContextPanel().currentlySelectedCategory) { + if ($.GetContextPanel().currentlySelectedCategory == category.id) { + SetCategoryVisible(category.id); + } + } else if (currentlySelectedCategory == null && category !== settingsCategory) { + SetCategoryVisible(category.id) + } + + UpdateCategoryBarVisibility(); return category; } @@ -883,6 +938,8 @@ function CreateCategory(id, name) */ function GetCategory(id) { + if (id === "settings") return settingsCategory; + for (const category of categories) { if (category.id == id) return category; @@ -900,6 +957,38 @@ function CloseMenu() FireOutput("_CloseMenu"); } +function StartPanelDrag() +{ + FireOutput("_DebugMenuDrag"); +} + +function ShowSettings() +{ + if (settingsCategory) { + SetCategoryVisible(); + settingsCategory.SetVisible(true); + currentlySelectedCategory = settingsCategory; + } +} + +function OpenDialog(text) +{ + const dialog = $('#modal_background'); + const label = $('#dialog_label'); + const contentContainer = $('#ContentContainer'); + label.text = text; + dialog.visible = true; + contentContainer.style.blur = "gaussian( 5 )"; +} + +function CloseDialog() +{ + const dialog = $('#modal_background'); + const contentContainer = $('#ContentContainer'); + dialog.visible = false; + contentContainer.style.blur = "none"; +} + /** * Gets the position of the left or right 'affordance' circle for the VR finger interacting with the menu. * @returns {{x:number,y:number}?} @@ -909,13 +998,30 @@ function GetAffordancePosition() { if (left.visible) return { x: left.actualxoffset, y: left.actualyoffset }; - const right = $('#vr_affordance_left'); + const right = $('#vr_affordance_right'); if (right.visible) return { x: right.actualxoffset, y: right.actualyoffset }; return null; } +/** + * Gets the position of the left or right 'affordance' circle for the VR finger interacting with the menu, + * normalized to the menu container. + * @returns {{x:number,y:number}?} + */ +function GetAffordancePositionInMenuSpace() { + const affordancePosition = GetAffordancePosition(); + if (affordancePosition) { + let bgPanel = $("#background_panel"); + return { + x: affordancePosition.x - bgPanel.actualxoffset, + y: affordancePosition.y - bgPanel.actualyoffset + }; + } + return null; +} + /** * Remaps a number from one range to another. * @param {number} value - The input value to remap. @@ -966,6 +1072,46 @@ function ClickHoveredButton() } } +/** + * Sets the width and height of the menu container. + * @param {number?} width + * @param {number?} height + */ +function SetContainerSize(width, height) { + // Clear container clip so larger sizes don't get clipped before the schedule + const container = $("#affordance_container"); + container.style.clip = "rect(0px, 100%, 100%, 0px)"; + + const bgPanel = $("#background_panel"); + + if (width) { + bgPanel.style.width = `${width}px`; + $.Schedule(0.1, ()=> { UpdateCategoryBarVisibility(); }); + } + if (height) { + bgPanel.style.height = `${height}px`; + $('#CategoriesContainer').style.minHeight = `${height - 111}px`; + } + + // Wait for element update and any animations + // 0.6s is the duration of the opening animation + $.Schedule(0.61, ()=> { + let bgWidth = bgPanel.actuallayoutwidth; + let bgHeight = bgPanel.actuallayoutheight; + let containerWidth = container.actuallayoutwidth; + let containerHeight = container.actuallayoutheight; + + // Calculate centered x position within container + let x = (containerWidth - bgWidth) / 2; + // Calculate the bottom y position + let y = (containerHeight - bgHeight); + + const clipRect = "rect(" + y + "px, " + (x + bgWidth) + "px, " + (y + bgHeight) + "px, " + x + "px)"; + + container.style.clip = clipRect; + }); +} + /** * Parses the incoming Lua command. * @param {string} command @@ -1072,18 +1218,11 @@ function ParseCommand(command, args) 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] - }); - } + const title = args[3]; + const currentIndex = parseInt(args[4])-1; + const rawValues = args.slice(5); - category.AddCycle(id, convar, values, currentValue); + category.AddCycle(id, convar, title, rawValues, currentIndex); break; case "setitemtext": { @@ -1137,6 +1276,20 @@ function ParseCommand(command, args) categories.splice(currentPos, 1); categories.splice(index, 0, category); } + break; + } + + case "setsize": { + const width = parseInt(args[0]); + const height = parseInt(args[1]); + SetContainerSize(width, height); + break; + } + + case "showdialog": { + const text = args[0]; + OpenDialog(text); + break; } } } @@ -1154,6 +1307,7 @@ function ScrollHelperSchedule() { return; } + $.DispatchEvent(scrollHelperScheduleEvent, currentlySelectedCategory.panel); $.DispatchEvent(scrollHelperScheduleEvent, currentlySelectedCategory.panel); $.Schedule(scrollHelperSpeed, ScrollHelperSchedule); } @@ -1196,24 +1350,26 @@ function ScrollHelperClick() { { // Modify preset layout buttons to work with controller trigger TurnButtonIntoDebugMenuButton($("#CloseMenuButton")); + TurnButtonIntoDebugMenuButton($("#SettingsButton")); + TurnButtonIntoDebugMenuButton($("#TitleBar")); TurnButtonIntoDebugMenuButton($("#CycleCategoryLeftButton")); TurnButtonIntoDebugMenuButton($("#CycleCategoryRightButton")); - TurnButtonIntoDebugMenuButton($("#ScrollHelperDown")); - TurnButtonIntoDebugMenuButton($("#ScrollHelperUp")); + // 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 + TurnButtonIntoDebugMenuButton($("#ScrollHelperDown"), ScrollHelperClick, () => StartScrollHelper("ScrollDown"), () => StopScrollHelper()); + TurnButtonIntoDebugMenuButton($("#ScrollHelperUp"), ScrollHelperClick, () => StartScrollHelper("ScrollUp"), () => StopScrollHelper()); // 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")); + if ($.GetContextPanel().initialized) { + $.Msg("Refreshing debug menu due to hot reload"); + $.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", () => StartScrollHelper("ScrollDown")); - $('#ScrollHelperDown').SetPanelEvent("onmouseout", () => StopScrollHelper()); - $('#ScrollHelperDown').SetPanelEvent("onactivate", ScrollHelperClick); - $('#ScrollHelperUp').SetPanelEvent("onmouseover", () => StartScrollHelper("ScrollUp")); - $('#ScrollHelperUp').SetPanelEvent("onmouseout", () => StopScrollHelper()); - $('#ScrollHelperUp').SetPanelEvent("onactivate", ScrollHelperClick); + // Set so the panel doesn't refresh when it's first created + // Only when modified and recompiled + $.GetContextPanel().initialized = true; $.Schedule(1.0, () => panelReady = true); diff --git a/panorama/scripts/custom_game/panorama_lua.js b/panorama/scripts/custom_game/panorama_lua.js index d9925a5..5b842f6 100644 --- a/panorama/scripts/custom_game/panorama_lua.js +++ b/panorama/scripts/custom_game/panorama_lua.js @@ -17,6 +17,13 @@ // @ts-ignore if(false)p=require("./panoramadoc"); +function DecodeString(str) +{ + str = str.replace(/U\+000A/g, "\n"); + str = str.replace(/U\+00027/g, "'"); + return str; +} + /** * * @param {null} _ Unused variable. @@ -25,7 +32,7 @@ if(false)p=require("./panoramadoc"); function LuaCallback(_, encoded) { // $.Msg(encoded) - let decoded = encoded.split("|"); + let decoded = DecodeString(encoded).split("|"); let panel = $.GetContextPanel(); //$.Msg(decoded[0], panel.BHasClass(decoded[0])) diff --git a/panorama/styles/custom_game/alyxlib_debug_menu.css b/panorama/styles/custom_game/alyxlib_debug_menu.css index 57cf28d..2ec5863 100644 --- a/panorama/styles/custom_game/alyxlib_debug_menu.css +++ b/panorama/styles/custom_game/alyxlib_debug_menu.css @@ -1,12 +1,29 @@ #background_panel.bg { + /* background_panel size is updated in JS SetContainerSize() */ /*opacity-mask: url("s2r://panorama/images/game_menu_ui/menu_type_mask_psd.vtex");*/ - flow-children: down; + flow-children: none; 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; + height: 100%; + horizontal-align: center; + vertical-align: bottom; +} +#ContentContainer +{ + blur: none; } +.vr_affordance_container{ + width: 100%; + height: 100%; +} +#layout_root{ + width: 100%; + height: 100%; +} + #background_panel .row .col_body { /*overflow: scroll;*/ @@ -114,25 +131,30 @@ Button.custom_button_with_subtitle.error_msg:not(.disabled) Label.button_label.s } .CycleCategoryLeft { - flow-children: left; - horizontal-align: left; + flow-children: none; overflow: clip clip; - /* background-color: red; */ width: 10%; } +.CycleCategoryLeft Label +{ + font-size: 22px; + text-align: left; + transform: translateX(5px); + width: 50%; +} .CycleCategoryRight { - flow-children: left; - horizontal-align: right; + flow-children: none; overflow: clip clip; - /* background-color: blue; */ width: 10%; } -/* .cycle_image +.CycleCategoryRight Label { - horizontal-align: left; - margin: 0px 40px 10px 10px; -} */ + font-size: 22px; + text-align: right; + transform: translateX(-58px); + width: 50%; +} .CategoryBar { @@ -140,11 +162,11 @@ Button.custom_button_with_subtitle.error_msg:not(.disabled) Label.button_label.s /* background-color: yellow; */ width: fill-parent-flow(1); height: 100%; - opacity: 0.5; + opacity: 1.0; } .CategoryButton { - width: 150px; + width: fill-parent-flow(1); height: 100%; /* background-color: red; */ horizontal-align: left; @@ -386,4 +408,96 @@ Label.custom_label /* Make slider value fit even with many decimal places */ .slider_value { text-overflow: shrink; +} + +.InstantOpen .game_menu_root +{ + transition-property: none; + transition-duration: 0s; + transition-timing-function: ease-in; +} + +.TopBar +{ + flow-children: right; + width: 100%; + margin-bottom: 10px; +} + +#SettingsButton +{ + background-image: url("s2r://panorama/images/game_menu_ui/options_advanced_png.vtex"); + background-repeat: no-repeat; + background-size: cover; + background-position: center; + width: 80px; + /* margin-top: 24px; */ + margin-left: 24px; + margin-right: 24px; + /* padding-top: 8px; */ + /* background-color: #FFBE5533; */ + border: 2px solid #FFBE55; + horizontal-align: left; +} + +#SettingsButton:hover +{ + background-image: url("s2r://panorama/images/game_menu_ui/options_advanced_hover_png.vtex"); + background-color: #FFBE5588; +} + + + +#modal_dialog_container +{ + width: 100%; + height: 100%; + flow-children: none; +} +#modal_dialog +{ + horizontal-align: center; + vertical-align: middle; + /* background-color: red; */ + /* z: 100px; */ + /* perspective: 100; */ + background-color: gradient( radial, 50% 50%, 0% 0%, 100% 100%, from( #553c18ee ), color-stop( 0.97, #000000 ), to( #000000 ) ); + /* box-shadow: inset #995e0011 2px 2px 4px 4px; */ + opacity: 0.99; + border-radius: 1%; +} +#modal_background +{ + width: 100%; + height: 100%; + background-color: #00000055; + visibility: collapse; +} + +.centered +{ + horizontal-align: center; + vertical-align: middle; +} + +.button_modal +{ + width: 220px; + min-height: 78px; + /* margin-top: 24px; + margin-right: 24px; + padding-top: 8px; + background-color: #FFBE5533; + border: 2px solid #FFBE55; + text-align: center; */ +} + +#dialog_label +{ + font-size: 38px; + text-align: center; + margin: 24px; + /* Bottom has a gap for unknown reasons at certain font sizes */ + /* This doesn't fix it but lessens it */ + margin-bottom: 0px; } \ No newline at end of file diff --git a/scripts/vscripts/alyxlib/class.lua b/scripts/vscripts/alyxlib/class.lua index 76bc6e6..0789031 100644 --- a/scripts/vscripts/alyxlib/class.lua +++ b/scripts/vscripts/alyxlib/class.lua @@ -1,80 +1,14 @@ --[[ - v2.3.1 + v2.4.0 https://github.com/FrostSource/alyxlib - If not using `vscripts/alyxlib/core.lua`, load this file at game start using the following line: - - ```lua - require "alyxlib.class" - ``` - - ======================================== Entity Classes ======================================== + Provides object-oriented class and inheritance functionality. - NOTE: Entity classes are still early in development and documentation is sparse. + If not using `alyxlib/init.lua`, load this file at game start using the following line: - The new `entity` function is designed to ease the creation of entity classes and all the trouble - that comes with complex entity scripts. Instead of defining functions and variables in the - private script scope we define them in a shared table class which is inherited by all entities - using this class. - - The simplest example of the entity function is: - - ```lua - ---Define the entity class, which returns both the class (base) and entity handle (self). - ---By "inheriting" EntityClass we get helpful code completion. - ---@class EntityName : EntityClass - local base, self = entity("EntityName") - ---This stops code executing a second time after a game load. - if self.Initiated then return end - - ---A variable is defined for the class. - base.class_variable = 1 - - ---A function is defined for the class. This specific function is called automatically. - ---@param loaded boolean - function base:OnReady(loaded) - ---Reference a class variable, note that we use `self` instead of `base` here inside the function. - self.class_variable = self.class_variable + 1 - end - ``` - - Entity scripts using this feature should *not* use `thisEntity` and instead use `self`. - It's important to remember to define your class functions on the `base` class. Functions - defined on `self` will not be inherited by other classes which inherit your class. - - Thinking is simplified, allowing you to define a base class think which can be paused/resumed - and the state of which will be saved, meaning the think function will automatically resume - after a game load: - - ```lua - function base:OnReady(loaded) - if not loaded then - self:ResumeThink() - end - end - - function base:Think() - if Time() > 50 then - self:PauseThink() - return - end - return 0 - end - ``` - - Like the think state, any fields defined in the `base`/`self` objects will be automatically loaded. - Currently these values still need to be saved manually but this is done easily with the new save function: - - ```lua - base.value = 1 - - function base:ChangeValue() - self.value = self.value + 1 - self:Save() - end - ``` + require "alyxlib.class" ]] -local version = "v2.3.1" +local version = "v2.4.0" require "alyxlib.storage" require "alyxlib.globals" @@ -110,7 +44,7 @@ local function _inherit(base, self, fenv) break end end - Warning("Trying to inherit an already inherited class ( "..class_name.." -> ["..tostring(self)..","..self:GetClassname()..","..self:GetName().."] )\n") + warn("Trying to inherit an already inherited class ( "..class_name.." -> ["..tostring(self)..","..self:GetClassname()..","..self:GetName().."] )") return end @@ -133,7 +67,7 @@ local function _inherit(base, self, fenv) } -- Used to automatically save values meta.__newindex = function(table, key, value) - if not key:startswith("__") and type(value) ~= "function" then + if not key:startswith("_") and type(value) ~= "function" then meta.__values[key] = value self:Save(key, value) else @@ -291,12 +225,17 @@ local function _inherit(base, self, fenv) end end ----Inherit an existing entity class which was defined using `entity` function. +--- +---Inherits an existing entity class which was defined using the [entity](lua://entity) function. +--- +---If no entity is provided, the entity calling the code will inherit the class. +---If no calling entity is found, an error will be thrown. +--- ---@generic T ----@param script `T` # The script to inherit. ----@param entity? EntityHandle # Optional entity which will inherit the script. If not used, the entity running the code will inherit. ----@return T # Base class, the newly created class. ----@return T # Self instance, the entity inheriting `base`. +---@param script `T` # The class/script to inherit +---@param entity? EntityHandle # Entity which will inherit the class +---@return T # Inherited class +---@return T # `self` instance of `entity`, the entity inheriting `script` ---@diagnostic disable-next-line:lowercase-global function inherit(script, entity) local fenv = entity or getfenv(2) @@ -339,13 +278,13 @@ end ---The class is only created once so this can be called in entity attached scripts ---multiple times and all subsequent calls will return the already created class. --- ----@generic T, T2 +---@generic T, T2 ---@param name? `T` # Internal class name ----@param ... string|table # Any inherit classes or scripts. ----@return any # Base class, the newly created class. ----@return T # Self instance, the entity inheriting `base`. ----@return table # Super class, the first inheritance of `base`. ----@return table # Private table +---@param ... string|table # Any classes or scripts inherit +---@return any # The newly created class +---@return T # `self` instance of entity inheriting the class, if called in an attached script +---@return table # The first inherited class, if any +---@return table # Private members table of the class **(unused)** ---@diagnostic disable-next-line: lowercase-global function entity(name, ...) @@ -421,29 +360,34 @@ end --#region EntityClass Definition +--- ---The top-level entity class that provides base functionality. +--- ---@class EntityClass : CBaseEntity,CEntityInstance,CBaseModelEntity,CBasePlayer,CHL2_Player,CBaseAnimating,CBaseFlex,CBaseCombatCharacter,CAI_BaseNPC,CBaseTrigger,CEnvEntityMaker,CInfoWorldLayer,CLogicRelay,CMarkupVolumeTagged,CEnvProjectedTexture,CPhysicsProp,CSceneEntity,CPointClientUIWorldPanel,CPointTemplate,CPointWorldText,CPropHMDAvatar,CPropVRHand ----@field __inherits table # Table of inherited classes. ----@field __name string # Name of the class. ----@field __outputs table # Map of output names to functions that will be connected on spawn. ----@field __game_events table # Map of game events to functions that will be listened to on spawn. ----@field __player_events table # Map of player events to functions that will be listened to on spawn. ----@field __rawget fun(self: EntityClass, key: string): any # Custom rawget function to get a value from meta.__values without checking inherits. ----@field Initiated boolean # If the class entity has been activated. ----@field IsThinking boolean # If the entity is currently thinking with `Think` function. ----@field OnActivate fun(self: EntityClass, activateType: ActivationType) # Called automatically on `Activate` if defined. ----@field OnReady fun(self: EntityClass, readyType: OnReadyType) # Called automatically after `Activate`, if defined, when EasyConvars and Player are initialized. ----@field OnSpawn fun(self: EntityClass, spawnkeys: CScriptKeyValues) # Called automatically on `Spawn` if defined. ----@field UpdateOnRemove fun(self: EntityClass) # Called before the entity is killed. ----@field OnBreak fun(self: EntityClass, inflictor: EntityHandle) # Called when a breakable entity is broken. ----@field OnTakeDamage fun(self: EntityClass, damageTable: OnTakeDamageTable) # Called when entity takes damage. ----@field Precache fun(self: EntityClass, context: CScriptPrecacheContext) # Called before Spawn for precaching. +---@field __inherits table # Table of inherited classes +---@field __name string # Name of the class +---@field __outputs table # Map of output names to functions that will be connected on spawn +---@field __game_events table # Map of game events to functions that will be listened to on spawn +---@field __player_events table # Map of player events to functions that will be listened to on spawn +---@field __rawget fun(self: EntityClass, key: string): any # Custom rawget function to get a value from meta.__values without checking inherits +---@field Initiated boolean # If the class entity has been activated +---@field IsThinking boolean # If the entity is currently thinking with `Think` function +---@field OnActivate fun(self: EntityClass, activateType: ActivationType) # Called automatically on `Activate` if defined +---@field OnReady fun(self: EntityClass, readyType: OnReadyType) # Called automatically after `Activate`, if defined, when EasyConvars and Player are initialized +---@field OnSpawn fun(self: EntityClass, spawnkeys: CScriptKeyValues) # Called automatically on `Spawn` if defined +---@field UpdateOnRemove fun(self: EntityClass) # Called before the entity is killed +---@field OnBreak fun(self: EntityClass, inflictor: EntityHandle) # Called when a breakable entity is broken +---@field OnTakeDamage fun(self: EntityClass, damageTable: OnTakeDamageTable) # Called when entity takes damage +---@field Precache fun(self: EntityClass, context: CScriptPrecacheContext) # Called before Spawn for precaching +---@field Think function # Entity think function. EntityClass = entity("EntityClass") ----Assign a new value to entity's field `name`. +--- +---Assigns a new value to entity's field `name`. ---This also saves the field. ----@param name string ----@param value any +--- +---@param name string # Name of the field +---@param value any # Value to assign ---@deprecated # Values are automatically saved now. function EntityClass:Set(name, value) -- self[name] = value @@ -452,37 +396,55 @@ function EntityClass:Set(name, value) self:Save(name, value) end ----Save a given entity field. Call with no arguments to save all data. ----@param name? string # Name of the field to save. ----@param value? any # Value to save. If not provided the value will be retrieved from the field with the same `name`. +--- +---Manually saves a given entity field. +--- +---If no `value` is provided, the value of the field with the same `name` will be saved. +---If no `name` is provided, all fields will be saved. +--- +---@param name? string # Name of the field to save +---@param value? any # Value to save ---@luadoc-ignore function EntityClass:Save(name, value) + if not IsValidEntity(self) then return end + if name then Storage.Save(self, name, value~=nil and value or self[name]) + return end + for key, val in pairs(self) do - if not key:startswith("__") and type(val) ~= "function" then + if not key:startswith("_") and type(val) ~= "function" then Storage.Save(self, key, val) end end end ----Main entity think function which auto resumes on game load. ----@luadoc-ignore -function EntityClass:Think() - Warning("Trying to think on entity class with no think defined ["..self.__name.."]\n") -end +-- ---Main entity think function which auto resumes on game load. +-- ---@luadoc-ignore +-- function EntityClass:Think() +-- Warning("Trying to think on entity class with no think defined ["..self.__name.."]\n") +-- end ----Resume the entity think function. +--- +---Resumes the entity think function. +--- ---@luadoc-ignore function EntityClass:ResumeThink() - if not self:IsNull() then - self:SetContextThink("__EntityThink", function() return self:Think() end, 0) + if not self:IsNull() and type(self.Think) == "function" then + self:SetContextThink("__EntityThink", function() + -- Handle dead entity and user PauseThink used without returning nil + if not IsValidEntity(self) or not self.IsThinking then return nil end + + return self:Think() + end, 0) self.IsThinking = true end end ----Pause the entity think function. +--- +---Pauses the entity think function. +--- ---@luadoc-ignore function EntityClass:PauseThink() if not self:IsNull() then @@ -491,23 +453,29 @@ function EntityClass:PauseThink() end end ----Define a function to redirected to `output` on spawn. +--- +---Defines a function to redirected to IO `output` on spawn. +--- ---@param output string ---@param func fun(...):any function EntityClass:Output(output, func) self.__outputs[output] = func end ----Define a function for listening to a game event. +--- +---Defines a function for listening to a game event. +--- ---@param gameEvent GameEventsAll ----@param func fun(self: EntityClass, params):any +---@param func fun(self: EntityClass, event):any function EntityClass:GameEvent(gameEvent, func) self.__game_events[gameEvent] = func end ----Define a function for listening to a player event. +--- +---Defines a function for listening to a player event. +--- ---@param playerEvent PLAYER_EVENTS_ALL ----@param func fun(self, params):any +---@param func fun(self, event):any function EntityClass:PlayerEvent(playerEvent, func) self.__player_events[playerEvent] = func end @@ -515,7 +483,9 @@ end --#endregion +--- ---Prints all classes that `ent` inherits. +--- ---@param ent EntityClass ---@param nest? string ---@diagnostic disable-next-line:lowercase-global @@ -542,9 +512,11 @@ function printinherits(ent, nest) end end ----Get the original metatable that Valve assigns to the entity. ----@param ent EntityClass # The entity search. ----@return table? # Metatable originally assigned to `ent`. +--- +---Gets the original metatable that Valve assigns to the entity. +--- +---@param ent EntityClass # The entity search +---@return table? # Metatable originally assigned to `ent` ---@diagnostic disable-next-line:lowercase-global function getvalvemeta(ent) local inherits = rawget(ent, "__inherits") @@ -557,10 +529,14 @@ function getvalvemeta(ent) end end ----Get a list of all classes that `class` inherits. ----Does not include the Valve class; use getvalvemeta() for that. ----@param class EntityClass # The entity or class to search. ----@return EntityClass[] # List of class tables. +--- +---Gets a list of all classes that `class` inherits. +--- +---Does not include the Valve class - use [getvalvemeta](lua://getvalvemeta) for that. +--- +---@see getvalvemeta +---@param class EntityClass # The entity or class to search +---@return EntityClass[] # List of class tables ---@diagnostic disable-next-line:lowercase-global function getinherits(class) local foundinherits = {} @@ -577,12 +553,17 @@ function getinherits(class) return foundinherits end ----Get if an `EntityClass` instance inherits a given `class`. ----@param ent EntityClass|EntityHandle # Entity to check. ----@param class string|table # Name or class table to check. ----@return boolean # True if `ent` inherits `class`, false otherwise. +--- +---Checks if an entity inherits a given `EntityClass`. +---@param ent EntityClass|EntityHandle # Entity to check +---@param class string|table # Name or class table to check +---@return boolean # `true` if `ent` inherits `class`, `false` otherwise +---@see getinherits +---@see IsClassEntity ---@diagnostic disable-next-line:lowercase-global function isinstance(ent, class) + if not IsEntity(ent, true) then return false end + local inherits = rawget(ent, "__inherits") if inherits then for _, inherit in ipairs(inherits) do @@ -604,12 +585,51 @@ function isinstance(ent, class) return false end ----Check if an entity is using the entity class system. ----@param ent EntityHandle # Entity to check. ----@return boolean # True if `ent` is a class entity, false otherwise. +--- +---Checks if an entity is using the AlyxLib class system. +--- +---@param ent EntityHandle # Entity to check +---@return boolean # `true` if `ent` is a class entity, `false` otherwise +---@see isinstance function IsClassEntity(ent) local name = rawget(ent, "__name") return type(name) == "string" and EntityClassNameMap[name] ~= nil end +--- +---Binds a class to an already spawned entity so it remains attached between game loads. +--- +---@param entity EntityHandle # Entity to bind to +---@param script string|table # Script, class, or class name to bind +---@param activateType? ActivationType +local function doClassBind(entity, script, activateType) + local scope = entity:GetOrCreatePrivateScriptScope() + inherit(script, scope) + if activateType and type(scope.Activate) == "function" then + scope.Activate(activateType) + end +end + +--- +---Binds a class to an already spawned entity so it remains attached between game loads. +--- +---@param entity EntityHandle # Entity to bind to +---@param script string # Script, class, or class name to bind +function BindClass(entity, script) + doClassBind(entity, script, 0) + entity:SetContext("BoundEntityClassScript", script, 0) +end + +---@param event PlayerEventPlayerActivate +ListenToPlayerEvent("player_activate", function(event) + local ent = Entities:First() + while ent do + local script = ent:GetContext("BoundEntityClassScript") + if type(script) == "string" then + doClassBind(ent, script) + end + ent = Entities:Next(ent) + end +end) + return version diff --git a/scripts/vscripts/alyxlib/controls/gesture.lua b/scripts/vscripts/alyxlib/controls/gesture.lua index 444c3b6..ca11df0 100644 --- a/scripts/vscripts/alyxlib/controls/gesture.lua +++ b/scripts/vscripts/alyxlib/controls/gesture.lua @@ -1,12 +1,12 @@ --[[ - v1.2.0 + v1.2.1 https://github.com/FrostSource/alyxlib Provides a system for tracking simple hand poses and gestures. - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: - require "alyxlib.input.gesture" + require "alyxlib.controls.gesture" ]] ---@TODO: Refine finger tracking positions, especially thumb. @@ -28,24 +28,37 @@ ---| "Peace" -- Custom gesture names can be appended here to improve writing. +--- +---Gesture table. +--- ---@class GestureTable ----@field name GestureNames # Name of the gesture. ----@field index number|nil # [0-1] range of the index finger position. ----@field middle number|nil # [0-1] range of the middle finger position. ----@field ring number|nil # [0-1] range of the ring finger position. ----@field pinky number|nil # [0-1] range of the pinky finger position. ----@field thumb number|nil # [0-1] range of the thumb finger position. +---@field name GestureNames # Gesture name +---@field index number|nil # [0โ€“1] range of the index finger position +---@field middle number|nil # [0โ€“1] range of the middle finger position +---@field ring number|nil # [0โ€“1] range of the ring finger position +---@field pinky number|nil # [0โ€“1] range of the pinky finger position +---@field thumb number|nil # [0โ€“1] range of the thumb finger position +--- +---Provides a system for tracking simple hand poses and gestures. +--- Gesture = {} Gesture.__index = Gesture -Gesture.version = "v1.2.0" + +--- +---Current version of the gesture system. +--- +Gesture.version = "v1.2.1" --- ---If the gesture system should start automatically on player spawn. ----Set this to false soon after require to stop it. +---This is false by default. --- Gesture.AutoStart = false +--- +---If the gesture system should print debug messages. +--- Gesture.DebugEnabled = false --- @@ -77,19 +90,22 @@ Gesture.PreviousGesture = { [1] = nil, } +--- +---How close the fingers need to be to the gesture values for it to be considered active. +--- Gesture.DiscrepancyTolerance = 0.25 --- ----Create a new gesture table. ----If finger position is nil then the finger isn't taken into consideration. +---Creates a new gesture table. +---If a finger position is `nil` then the finger isn't taken into consideration. --- ----@param name? string ----@param index number|nil ----@param middle number|nil ----@param ring number|nil ----@param pinky number|nil ----@param thumb number|nil ----@return GestureTable +---@param name? string # Gesture name +---@param index number|nil # [0โ€“1] range of the index finger position +---@param middle number|nil # [0โ€“1] range of the middle finger position +---@param ring number|nil # [0โ€“1] range of the ring finger position +---@param pinky number|nil # [0โ€“1] range of the pinky finger position +---@param thumb number|nil # [0โ€“1] range of the thumb finger position +---@return GestureTable # New gesture table local function getGestureTable(name, index, middle, ring, pinky, thumb) return { @@ -120,16 +136,16 @@ local callbacks = { } --- ----Add a new gesture to watch for. +---Adds a new gesture to watch for. --- ----If a finger position is nil then the finger isn't taken into consideration. +---If a finger position is `nil`, that finger isn't considered. --- ----@param name string # Name of the gesture. If not unique it will overwrite the previous gesture with this name. ----@param index number|nil # [0-1] range of the index finger position. ----@param middle number|nil # [0-1] range of the middle finger position. ----@param ring number|nil # [0-1] range of the ring finger position. ----@param pinky number|nil # [0-1] range of the pinky finger position. ----@param thumb number|nil # [0-1] range of the thumb finger position. +---@param name string # Gesture name; overwrites existing one if not unique +---@param index number|nil # [0โ€“1] range of the index finger position. +---@param middle number|nil # [0โ€“1] range of the middle finger position. +---@param ring number|nil # [0โ€“1] range of the ring finger position. +---@param pinky number|nil # [0โ€“1] range of the pinky finger position. +---@param thumb number|nil # [0โ€“1] range of the thumb finger position. function Gesture:AddGesture(name, index, middle, ring, pinky, thumb) Gesture.Gestures[name] = getGestureTable(name, index, middle, ring, pinky, thumb) callbacks[0].start[name] = {} @@ -139,11 +155,11 @@ function Gesture:AddGesture(name, index, middle, ring, pinky, thumb) end --- ----Remove an existing gesture. +---Removes an existing gesture. --- ---Any callbacks registered with the gesture will be unregistered. --- ----@param name string # Name of the gesture. +---@param name string # Gesture name function Gesture:RemoveGesture(name) Gesture.Gestures[name] = nil callbacks[0].start[name] = nil @@ -153,9 +169,9 @@ function Gesture:RemoveGesture(name) end --- ----Remove a list of gestures. +---Removes a list of gestures. --- ----@param names GestureNames[] +---@param names GestureNames[] # List of gesture names function Gesture:RemoveGestures(names) for _,name in ipairs(names) do self:RemoveGesture(name) @@ -186,8 +202,8 @@ Gesture:AddGesture("Peace", 1, 1, 0, 0, nil) --- do_something() --- end --- ----@param hand CPropVRHand|0|1 ----@return GestureNames +---@param hand CPropVRHand|0|1 # The hand to get the gesture of +---@return GestureNames # Gesture name function Gesture:GetGesture(hand) if type(hand) ~= "number" then hand = hand:GetHandID() @@ -198,10 +214,10 @@ function Gesture:GetGesture(hand) end --- ----Gets the current [0-1] finger curl values of a given hand. +---Gets the current [0โ€“1] finger curl values of a given hand. --- ----@param hand CPropVRHand|0|1 ----@return GestureTable +---@param hand CPropVRHand|0|1 # The hand to get the gesture of +---@return GestureTable # Gesture values function Gesture:GetGestureRaw(hand) if type(hand) ~= "number" then hand = hand:GetHandID() @@ -210,14 +226,14 @@ function Gesture:GetGestureRaw(hand) end --- ----Register a callback for a specific gesture start/stop. +---Registers a callback for a specific gesture start/stop. --- ----@param kind "start"|"stop" # If the callback is registered for gesture start or stop. ----@param hand CPropVRHand|-1|0|1 # The ID of the hand to register for (-1 means both). ----@param gesture GestureNames # Name of the gesture. +---@param kind "start"|"stop" # Listen for the start or stop of the gesture +---@param hand CPropVRHand|-1|0|1 # The hand to listen to +---@param gesture GestureNames # The gesture to listen for ---@param duration unknown # Not implemented ----@param callback function # The function that will be called when conditions are met. ----@param context? any +---@param callback function # The function to call when the gesture is triggered +---@param context? any # Context to pass to the callback function Gesture:RegisterCallback(kind, hand, gesture, duration, callback, context) -- Quick way to register both hands. @@ -234,9 +250,9 @@ function Gesture:RegisterCallback(kind, hand, gesture, duration, callback, conte end --- ----Unregister a callback function. +---Unregisters a callback function. --- ----@param callback function +---@param callback function # The function to unregister function Gesture:UnregisterCallback(callback) for _, kinds in pairs(callbacks) do for _, names in pairs(kinds) do @@ -278,10 +294,10 @@ local pinky_min = 1.3178 local gestures = Gesture.Gestures ---@class GESTURE_CALLBACK ----@field kind "start"|"stop" # If the gesture was started or stopped. ----@field name GestureNames # The name of the gesture performed. ----@field hand CPropVRHand # The hand the gesture was performed on. ----@field time number # Server time the gesture occurred. +---@field kind "start"|"stop" # If the gesture was started or stopped +---@field name GestureNames # The name of the gesture performed +---@field hand CPropVRHand # The hand the gesture was performed on +---@field time number # Server time the gesture occurred local function GestureThink() local hmd = Entities:GetLocalPlayer():GetHMDAvatar()--[[@as CPropHMDAvatar]] @@ -411,7 +427,7 @@ local tracking_ent = nil --- ---Starts the gesture system. --- ----@param on EntityHandle? # Optional entity to do the tracking on. This is the player by default. +---@param on EntityHandle? # Optional entity to do the tracking on function Gesture:Start(on) if on == nil then on = Entities:GetLocalPlayer() end tracking_ent = on diff --git a/scripts/vscripts/alyxlib/controls/haptics.lua b/scripts/vscripts/alyxlib/controls/haptics.lua index 31022b5..e0d8e3b 100644 --- a/scripts/vscripts/alyxlib/controls/haptics.lua +++ b/scripts/vscripts/alyxlib/controls/haptics.lua @@ -1,12 +1,12 @@ --[[ - v1.0.2 + v1.0.3 https://github.com/FrostSource/alyxlib Haptic sequences allow for more complex vibrations than the one-shot pulses that the base API provides. - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: - require "alyxlib.input.haptics" + require "alyxlib.controls.haptics" This script was adapted from PeterSHollander's haptics.lua https://github.com/PeterSHollander/glorious_gloves/blob/master/scripts/vscripts/device/haptics.lua @@ -15,6 +15,9 @@ local MIN_PULSE_WIDTH = 1 local MAX_PULSE_WIDTH = 30 +--- +---Haptic sequence. +--- ---@class HapticSequence local HapticSequenceClass = { ---@type string @@ -28,7 +31,11 @@ local HapticSequenceClass = { pulseWidth_us = 0, } HapticSequenceClass.__index = HapticSequenceClass -HapticSequenceClass.version = "v1.0.2" + +--- +---Current version of the haptics system. +--- +HapticSequenceClass.version = "v1.0.3" --- ---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 00432ec..5ddb8ad 100644 --- a/scripts/vscripts/alyxlib/controls/input.lua +++ b/scripts/vscripts/alyxlib/controls/input.lua @@ -1,10 +1,10 @@ --[[ - v4.0.2 + v4.1.0 https://github.com/FrostSource/alyxlib Simplifies the tracking of digital action presses/releases and analog values. - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: require "alyxlib.controls.input" ]] @@ -15,11 +15,11 @@ ---@class Input Input = {} Input.__index = Input -Input.version = "v4.0.2" +Input.version = "v4.1.0" --- ---If the input system should start automatically on player spawn. ----Set this to false soon after require to stop it. +---This is `true` by default. --- Input.AutoStart = true @@ -47,38 +47,47 @@ Input.ReleasedTolerance = 0.5 ---@type number Input.MultiplePressInterval = 0.35 ----@class InputCallbackTable ----@field presses integer ----@field func function ----@field context any # Value passed into first argument of callback. ----@field handkind InputHandKind # Left, right, primary, secondary. ----@field actualhandid 0|1 # This needs to be updated whenver hands change. ----@field button DigitalInputAction # The button for this event. ----@field kind "press"|"release" ----@field press_time number ----@field prev_press_time number ----@field release_time number ----@field multiple_press_count number - ----@class AnalogCallbackTable ----@field analog AnalogInputAction ----@field value { x: number?, y: number? } ----@field checkGreaterThan boolean ----@field func function ----@field context any # Value passed into first argument of callback. ----@field handkind InputHandKind # Left, right, primary, secondary. ----@field actualhandid 0|1 # This needs to be updated whenver hands change. +--- +---Internal data structure for button callbacks. +--- +---@class InputActionDataTable +---@field presses integer # Number of times this button needs to be pressed +---@field func function # The callback function +---@field context any # Value passed into first argument of callback +---@field handkind InputHandKind # The kind of hand that used the button +---@field actualhandid 0|1 # Actual hand ID that used the button +---@field button DigitalInputAction # The button used for this event +---@field kind "press"|"release" # Whether the button was pressed or released +---@field press_time number # Server time the button was pressed +---@field prev_press_time number # Last server time the button was pressed +---@field release_time number # Server time the button was released +---@field multiple_press_count number # Number of times this button was pressed + +--- +---Internal data structure for analog callbacks. +--- +---@class InputAnalogDataTable +---@field analog AnalogInputAction # The analog action for this event +---@field value { x: number?, y: number? } # The required analog value for this event +---@field checkGreaterThan boolean # If checking for greater than `value` or less than +---@field func function # The callback function +---@field context any # Value passed into first argument of callback +---@field handkind InputHandKind # The kind of hand that moved the analog +---@field actualhandid 0|1 # Actual hand ID that moved the analog ---@field literalhandtype 0|1 # The literal value of the hand which is usually the opposite of Id ----@type table +---Table of all button callbacks. +---@type table local buttonCallbacks = {} ----@type table +---Table of all analog callbacks. +---@type table local analogCallbacks = {} +---Counter for button and analog callbacks. +---@type integer local callbackId = 0 - local currentPrimaryHandId = 1 local currentSecondaryHandId = 0 @@ -223,8 +232,8 @@ local ControllerTypeDescriptions = DefaultTable( ---Get the description of a given button. ---Useful for debugging or hint display. --- ----@param button DigitalInputAction ----@return string +---@param button DigitalInputAction # The button to get the description of +---@return string # The description string function Input:GetButtonDescription(button) return DigitalDescriptions[button] end @@ -233,8 +242,8 @@ end ---Get the description of a given analog action. ---Useful for debugging or hint display. --- ----@param analog DigitalInputAction ----@return string +---@param analog DigitalInputAction # The analog action to get the description of +---@return string # The description string function Input:GetAnalogDescription(analog) return AnalogDescriptions[analog] end @@ -242,8 +251,8 @@ end --- ---Get the description of a given controller type. --- ----@param controllerType ControllerType ----@return string +---@param controllerType ControllerType # The controller type +---@return string # The description string function Input:GetControllerTypeDescription(controllerType) return ControllerTypeDescriptions[controllerType] end @@ -251,8 +260,8 @@ end --- ---Get the name of a hand. --- ----@param hand CPropVRHand|0|1 ----@param use_operant boolean? # If true, will return primary/secondary instead of left/right +---@param hand CPropVRHand|0|1 # The hand entity or ID +---@param use_operant? boolean # If true, name will use primary/secondary instead of right/left function Input:GetHandName(hand, use_operant) if type(hand) ~= "number" then hand = hand:GetHandID() @@ -271,18 +280,20 @@ end --#endregion General requests --- ----Register a callback for a specific button press/release. +---Listens to a specific digital input press/release. --- ---@generic T ----@param kind string # The kind of button interaction. ----| '"press"' # Button is pressed. ----| '"release"' # Button is released. ----@param hand CPropVRHand|`InputHandBoth`|InputHandKind # The type of hand to listen on, or the hand itself. +---@param kind string # The kind of button interaction +---| '"press"' # Button is pressed +---| '"release"' # Button is released +---@param hand CPropVRHand|`InputHandBoth`|InputHandKind # The type of hand to listen on, or the hand itself ---| -1 # Both hands ----@param button NamedDigitalInputAction|DigitalInputAction # The button to check. ----@param presses integer|nil # Number of times the button must be pressed in quick succession. E.g. 2 for double click. Only applicable for `kind` press. ----@param callback fun(params:InputPressCallback|InputReleaseCallback)|fun(context:T,params:InputPressCallback|InputReleaseCallback) # The function that will be called when conditions are met. ----@param context? T # Optional context passed into the callback as the first value. Is also used when unregistering. +---@param button NamedDigitalInputAction|DigitalInputAction # The button to listen to +---@param presses integer|nil # Number of times the button must be pressed in quick succession. E.g. 2 for double click. Only applicable for `kind` press +---@param callback fun(params:InputPressCallback|InputReleaseCallback)|fun(context:T,params:InputPressCallback|InputReleaseCallback) # The function that will be called when the button is pressed +---@param context? T # Optional context passed into the callback as the first value +---@return integer # The callback ID +---@return integer|nil # The second callback ID, if `hand` is `InputHandBoth` function Input:ListenToButton(kind, hand, button, presses, callback, context) if type(hand) ~= "number" then @@ -342,29 +353,40 @@ local function getCorrectAnalogValue(analogValue) end --- ----Listen to a specific analog value reaching a certain value. +---Listens to a specific analog action reaching a certain value. --- ----@param kind "up"|"down" # `up` means listen for the value moving above `analogValue`, `down` means listen for it moving below. ----@param hand CPropVRHand|InputHandKind # The hand entity or kind of hand to listen to. ----@param analogAction AnalogInputAction # The specific analog action to listen for. ----@param analogValue AnalogValueType # The value(s) to listen for. ----@param callback fun(params:InputAnalogCallback) # The function that will be called when conditions are met. ----@param context? any # Optional context passed into the callback as the first value. Is also used when unregistering. +---@param kind "up"|"down" # `up` means listen for the value moving above `analogValue`, `down` means listen for it moving below +---@param hand CPropVRHand|InputHandKind # The hand entity or kind of hand to listen on +---@param analogAction AnalogInputAction # The specific analog action to listen for +---@param analogValue AnalogValueType # The value(s) to listen for +---@param callback fun(params:InputAnalogCallback) # The function that will be called when conditions are met +---@param context? any # Optional context passed into the callback as the first value +---@return integer # The callback ID +---@return integer|nil # The second callback ID, if `hand` is `InputHandBoth` function Input:ListenToAnalog(kind, hand, analogAction, analogValue, callback, context) - local handid = convertHandKindToHandId(hand) + if type(hand) ~= "number" then + hand = hand:GetHandID() + -- Quick way to register both hands. + elseif hand == -1 then + local id1 = self:ListenToAnalog(kind, 0, analogAction, analogValue, callback, context) + local id2 = self:ListenToAnalog(kind, 1, analogAction, analogValue, callback, context) + return id1, id2 + end + + local handid = convertHandKindToHandId(hand) local value = getCorrectAnalogValue(analogValue) callbackId = callbackId + 1 analogCallbacks[callbackId] = { - checkGreaterThan = kind == "up", - actualhandid = handid, func = callback, context = context, - literalhandtype = 1 - handid, + handkind = hand, + actualhandid = handid, analog = analogAction, + checkGreaterThan = kind == "up", + literalhandtype = 1 - handid, value = value, - handkind = hand } return callbackId @@ -373,10 +395,10 @@ end --- ---Changes some data which was defined in `ListenToAnalog` for a specific ID. --- ----@param id integer # The ID of the analog event you want to modify. ----@param analogAction? AnalogInputAction # The new action to listen for, or nil to leave unchanged. ----@param analogValue AnalogValueType # The new value to listen for, or nil to leave unchanged. ----@return boolean # True if the ID was found, false otherwise. +---@param id integer # The ID of the analog event you want to modify +---@param analogAction? AnalogInputAction # The new action to listen for, or `nil` to leave unchanged +---@param analogValue AnalogValueType # The new value to listen for, or `nil` to leave unchanged +---@return boolean # True if the ID was found, false otherwise function Input:ModifyAnalogCallback(id, analogAction, analogValue) for _id, analogCallbackData in pairs(analogCallbacks) do if _id == id then @@ -396,11 +418,13 @@ end --- ---Unregisters a listener with a specific ID. --- ----@param id number # The number returned by ListenToButton. +---@see Input.ListenToButton +---@see Input.ListenToAnalog +---@param id number # The ID of the listener function Input:StopListening(id) for _id, tbl in pairs(buttonCallbacks) do if _id == id then - tbl[_id] = nil + buttonCallbacks[_id] = nil return end end @@ -416,8 +440,8 @@ end --- ---Unregisters any listeners with a specific callback/context pair. --- ----@param callback fun(params:InputAnalogCallback) # The callback function that's listening. ----@param context? any # The context that was given. +---@param callback fun(params:InputAnalogCallback) # The callback function +---@param context? any # The context that was given function Input:StopListeningCallbackContext(callback, context) for _id, tbl in pairs(buttonCallbacks) do if tbl.func == callback and tbl.context == context then @@ -437,7 +461,7 @@ end --- ---Unregisters any listeners which have a specific context. --- ----@param context any # The number returned by ListenToButton. +---@param context any # The context that was given function Input:StopListeningByContext(context) for _id, tbl in pairs(buttonCallbacks) do if tbl.context == context then @@ -452,26 +476,35 @@ function Input:StopListeningByContext(context) end end - +--- +---A callback for when a button is pressed. +--- ---@class InputPressCallback ----@field kind "press" # The kind of event. ----@field press_time number # The server time at which the button was pressed. ----@field hand CPropVRHand # EntityHandle for the hand that pressed the button. ----@field button DigitalInputAction # The ID of the button that was pressed. +---@field kind "press" # The kind of action event +---@field press_time number # The server time at which the button was pressed +---@field hand CPropVRHand # EntityHandle for the hand that pressed the button +---@field button DigitalInputAction # The ID of the button that was pressed +--- +---A callback for when a button is released. +--- ---@class InputReleaseCallback ----@field kind "release" # The kind of event. ----@field release_time number # The server time at which the button was released. ----@field hand CPropVRHand # EntityHandle for the hand that released the button. ----@field button DigitalInputAction # The ID of the button that was pressed. ----@field held_time number # Seconds the button was held for prior to being released. +---@field kind "release" # The kind of action event +---@field release_time number # The server time at which the button was released +---@field hand CPropVRHand # EntityHandle for the hand that released the button +---@field button DigitalInputAction # The ID of the button that was pressed +---@field held_time number # Seconds the button was held for prior to being released +--- +---A callback for when an analog action is moved. +--- ---@class InputAnalogCallback ----@field value Vector # The vector value of the analog action at the time of detection. ----@field hand CPropVRHand # EntityHandle for the hand that moved the analog action. ----@field analog AnalogInputAction # The ID of the analog action that was moved. +---@field kind "up"|"down" # The kind of analog event +---@field value Vector # The vector value of the analog action at the time of detection +---@field hand CPropVRHand # EntityHandle for the hand that moved the analog action +---@field analog AnalogInputAction # The ID of the analog action that was moved ----@alias INPUT_CALLBACK InputPressCallback|InputReleaseCallback +---@alias InputGenericCallback InputPressCallback|InputReleaseCallback local function InputThink() ---@TODO remove these variables and use cached literal/hand entity @@ -574,7 +607,8 @@ local function InputThink() local t = { value = value, analog = analogData.analog, - hand = hmd:GetVRHand(analogData.actualhandid) + hand = hmd:GetVRHand(analogData.actualhandid), + kind = analogData.checkGreaterThan and "up" or "down", } if analogData.context then analogData.func(analogData.context, t) @@ -598,12 +632,12 @@ local tracking_ent = nil --- ---Starts the input system. --- ----@param on EntityHandle? # Optional entity to do the tracking on. This is the player by default. +---@param on EntityHandle? # Optional entity to do the tracking on. This is the player by default function Input:Start(on) if on == nil then on = Entities:GetLocalPlayer() end tracking_ent = on tracking_ent:SetContextThink("InputThink", InputThink, 0) - print("Input system starting...") + devprint("Input system starting...") end --- diff --git a/scripts/vscripts/alyxlib/data/color.lua b/scripts/vscripts/alyxlib/data/color.lua index 2943a1e..68fcad8 100644 --- a/scripts/vscripts/alyxlib/data/color.lua +++ b/scripts/vscripts/alyxlib/data/color.lua @@ -1,15 +1,15 @@ --[[ - v1.0.2 + v1.0.3 https://github.com/FrostSource/alyxlib - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: require "alyxlib.data.color" ]] require "alyxlib.globals" require "alyxlib.math.common" -local version = "v1.0.2" +local version = "v1.0.3" local hasfrac = math.has_frac @@ -32,10 +32,10 @@ local function resolveColorRange(value, default) end ---Converts a hue value to an RGB value. ----@param p number # The value used in the RGB calculation. ----@param q number # The value used in the RGB calculation. ----@param t number # The hue value to convert to RGB. ----@return number # The RGB value corresponding to the given hue. +---@param p number # The value used in the RGB calculation +---@param q number # The value used in the RGB calculation +---@param t number # The hue value to convert to RGB +---@return number # The RGB value corresponding to the given hue local function hueToRgb(p, q, t) if t < 0 then t = t + 1 end if t > 1 then t = t - 1 end @@ -46,12 +46,12 @@ local function hueToRgb(p, q, t) end ---Converts HSL color values to RGB. ----@param h number # Hue value range [0-1]. ----@param s number # Saturation value range [0-1]. ----@param l number # Lightness value range [0-1]. ----@return integer r # Red color value in range [0-255]. ----@return integer g # Green color value in range [0-255]. ----@return integer b # Blue color value in range [0-255]. +---@param h number # Hue value range [0-1] +---@param s number # Saturation value range [0-1] +---@param l number # Lightness value range [0-1] +---@return integer r # Red color value in range [0-255] +---@return integer g # Green color value in range [0-255] +---@return integer b # Blue color value in range [0-255] local function hslToRgb(h, s, l) local r, g, b @@ -73,12 +73,12 @@ local function hslToRgb(h, s, l) end ---Converts RGB color values to HSL. ----@param r number # Red value range [0-255]. ----@param g number # Red value range [0-255]. ----@param b number # Red value range [0-255]. ----@return number h # Hue color value in range [0-1]. ----@return number s # Saturation color value in range [0-1]. ----@return number l # Lightness color value in range [0-1]. +---@param r number # Red value range [0-255] +---@param g number # Red value range [0-255] +---@param b number # Red value range [0-255] +---@return number h # Hue color value in range [0-1] +---@return number s # Saturation color value in range [0-1] +---@return number l # Lightness color value in range [0-1] local function rgbToHsl(r, g, b) r = r / 255 g = g / 255 @@ -103,13 +103,13 @@ end ---Represents a color object with RGB and HSL components. ---@class Color ----@field r integer # Red component. ----@field g integer # Green component. ----@field b integer # Blue component. ----@field a integer # Alpha component. ----@field hue integer # Hue component. ----@field saturation integer # Saturation component. ----@field lightness integer # Lightness component. +---@field r integer # Red component +---@field g integer # Green component +---@field b integer # Blue component +---@field a integer # Alpha component +---@field hue integer # Hue component +---@field saturation integer # Saturation component +---@field lightness integer # Lightness component local ColorClass = {} if Storage then @@ -120,10 +120,10 @@ if Storage then --- ---Helper function for saving the `Color`. --- - ---@param handle EntityHandle # The entity to save on. - ---@param name string # The name to save as. - ---@param color Color # The color to save. - ---@return boolean # If the save was successful. + ---@param handle EntityHandle # The entity to save on + ---@param name string # The name to save as + ---@param color Color # The color to save + ---@return boolean # If the save was successful ---@luadoc-ignore function ColorClass.__save(handle, name, color) return Storage.SaveTableCustom(handle, name, color, "Color") @@ -134,8 +134,8 @@ if Storage then --- ---Helper function for loading the `Color`. --- - ---@param handle EntityHandle # Entity to load from. - ---@param name string # Name to load. + ---@param handle EntityHandle # Entity to load from + ---@param name string # Name to load ---@return Color|nil ---@luadoc-ignore function ColorClass.__load(handle, name) @@ -152,10 +152,10 @@ if Storage then ---Load a `Color`. --- ---@generic T - ---@param handle EntityHandle # Entity to load from. - ---@param name string # Name the Color was saved as. - ---@param default? T # Optional default value. - ---@return Color|T + ---@param handle EntityHandle # Entity to load from + ---@param name string # Name the Color was saved as + ---@param default? T # Optional default value + ---@return Color|T # The loaded Color ---@luadoc-ignore Storage.LoadColor = function(handle, name, default) local color = ColorClass.__load(handle, name) @@ -231,9 +231,10 @@ end --- ---Converts this `Color` to a hexadecimal representation. ----The hexadecimal format is in the format #RRGGBB. --- ----@return string # The hexadecimal representation of this `Color`. +---The hexadecimal string is in the format #RRGGBB. +--- +---@return string # The hexadecimal representation of this [Color](lua://Color) function ColorClass:ToHexString() local function toHex(val) return string.format("%02X", val) @@ -242,28 +243,35 @@ function ColorClass:ToHexString() return "#" .. toHex(self.r) .. toHex(self.g) .. toHex(self.b) end ----Get a `Vector` from this `Color` in the form of [x=r, y=g, z=b]. ----@return Vector # The color vector. +--- +---Returns a [Vector](lua://Vector) from this [Color](lua://Color). +---In the form of [x=r, y=g, z=b], with ranges [0-255]. +--- +---@return Vector # The vector representation of this [Color](lua://Color) function ColorClass:ToVector() return Vector(self[1], self[2], self[3]) end ----Get a `Vector` from this `Color` in the form of [x=r, y=g, z=b] but with ranges [0-1]. ----@return Vector # The color vector. +--- +---Returns a [Vector](lua://Vector) from this [Color](lua://Color). +---In the form of [x=r, y=g, z=b], with ranges [0-1]. +--- +---@return Vector # The decimal vector representation of this [Color](lua://Color) function ColorClass:ToDecimalVector() return Vector(self[1] / 255, self[2] / 255, self[3] / 255) end --- ----Sets the color based on the provided RGB (Red, Green, Blue) components and an optional alpha component. ----If any of the provided values have fractional parts, they will all be normalized to the range [0, 255]. +---Sets the RGBA components of this [Color](lua://Color). +--- +---If any of the provided values have fractional parts, they will all be normalized to the range [0, 255]. ---If any of the provided values are nil or omitted, the corresponding component of the color will remain unchanged. --- ----@param r? number # The red component of the color. ----@param g? number # The green component of the color. ----@param b? number # The blue component of the color. ----@param a? number # The alpha component of the color. +---@param r? number # The red component of the color +---@param g? number # The green component of the color +---@param b? number # The blue component of the color +---@param a? number # The alpha component of the color function ColorClass:SetRGB(r, g, b, a) if hasfrac(r) or hasfrac(g) or hasfrac(b) or hasfrac(a) then r = r and r * 255 or self.r @@ -278,7 +286,9 @@ function ColorClass:SetRGB(r, g, b, a) self.a = a end ----Get the HSL color values from this `Color`. +--- +---Gets the HSL color values for this [Color](lua://Color). +--- ---@return number h # Hue color value in range [0-360] ---@return number s # Saturation color value in range [0-100] ---@return number l # Lightness color value in range [0-100] @@ -288,14 +298,14 @@ function ColorClass:GetHSL() end --- ----Sets the color based on the provided HSL (Hue, Saturation, Lightness) components. ----The method accepts values for hue, saturation, and lightness in their respective ranges and updates the color accordingly. +---Sets the HSL (Hue, Saturation, Lightness) components of this [Color](lua://Color). --- ----If any of the provided values have fractional parts, they will be normalized to their appropriate ranges (0 to 360 for hue, 0 to 100 for saturation and lightness). +---If any of the provided values have fractional parts, they will be normalized to their appropriate ranges (0 to 360 for hue, 0 to 100 for saturation and lightness). ---If any of the provided values are nil or omitted, the corresponding component of the color will remain unchanged. ----@param h? number # The hue value of the color (0 to 360), representing the color's position on the color wheel. ----@param s? number # The saturation value of the color (0 to 100), determining the intensity of the color. ----@param l? number # The lightness value of the color (0 to 100), affecting the brightness of the color. +--- +---@param h? number # The hue value of the color (0 to 360), representing the color's position on the color wheel +---@param s? number # The saturation value of the color (0 to 100), determining the intensity of the color +---@param l? number # The lightness value of the color (0 to 100), affecting the brightness of the color function ColorClass:SetHSL(h, s, l) if hasfrac(h) or hasfrac(s) or hasfrac(l) then h = h and h / 360 or nil @@ -315,13 +325,13 @@ function ColorClass:SetHSL(h, s, l) end --- ----Create a new `Color` instance using range [0-1] or [0-255]. +---Creates a new [Color](lua://Color) instance using range [0-255] or [0-1]. --- ----@param r? number # Red color value. ----@param g? number # Green color value. ----@param b? number # Blue color value. ----@param a? number # Alpha value. ----@return Color +---@param r? number # Red color value +---@param g? number # Green color value +---@param b? number # Blue color value +---@param a? number # Alpha value +---@return Color # The new [Color](lua://Color) ---@overload fun(rgb: Vector) ---@overload fun(rgb: string) function Color(r, g, b, a) @@ -354,10 +364,10 @@ function Color(r, g, b, a) end --- ----Get if a value is a `Color`. +---Checks if a value is a `Color`. --- ----@param value any ----@return boolean +---@param value any # The value to check +---@return boolean # `true` if the value is a [Color](lua://Color), `false` otherwise function IsColor(value) return type(value) == "table" and getmetatable(value) == ColorClass end diff --git a/scripts/vscripts/alyxlib/data/inventory.lua b/scripts/vscripts/alyxlib/data/inventory.lua index ebee628..0c5f8d1 100644 --- a/scripts/vscripts/alyxlib/data/inventory.lua +++ b/scripts/vscripts/alyxlib/data/inventory.lua @@ -1,17 +1,20 @@ --[[ - v1.2.3 + v1.2.4 https://github.com/FrostSource/alyxlib An inventory is a table where each key has an integer value assigned to it. When a value hits 0 the key is removed from the table. - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: require "alyxlib.data.inventory" ]] -local version = "v1.2.3" +local version = "v1.2.4" +--- +---Inventory data structure. +--- ---@class Inventory local InventoryClass = { @@ -28,10 +31,10 @@ if pcall(require, "alyxlib.storage") then --- ---Helper function for saving the `inventory`. --- - ---@param handle EntityHandle # The entity to save on. - ---@param name string # The name to save as. - ---@param inventory Inventory # The inventory to save. - ---@return boolean # If the save was successful. + ---@param handle EntityHandle # The entity to save on + ---@param name string # The name to save as + ---@param inventory Inventory # The inventory to save + ---@return boolean # If the save was successful ---@luadoc-ignore function InventoryClass.__save(handle, name, inventory) return Storage.SaveTableCustom(handle, name, inventory, "Inventory") @@ -42,9 +45,9 @@ if pcall(require, "alyxlib.storage") then --- ---Helper function for loading the `inventory`. --- - ---@param handle EntityHandle # Entity to load from. - ---@param name string # Name to load. - ---@return Inventory|nil + ---@param handle EntityHandle # Entity to load from + ---@param name string # Name to load + ---@return Inventory|nil # The loaded inventory ---@luadoc-ignore function InventoryClass.__load(handle, name) local inventory = Storage.LoadTableCustom(handle, name, "Inventory") @@ -56,13 +59,13 @@ if pcall(require, "alyxlib.storage") then CBaseEntity.SaveInventory = Storage.SaveInventory --- - ---Load an Inventory. + ---Loads an Inventory. --- ---@generic T - ---@param handle EntityHandle # Entity to load from. - ---@param name string # Name the Inventory was saved as. - ---@param default? T # Optional default value. - ---@return Inventory|T + ---@param handle EntityHandle # Entity to load from + ---@param name string # Name the Inventory was saved as + ---@param default? T # Optional default value + ---@return Inventory|T # The loaded Inventory ---@luadoc-ignore Storage.LoadInventory = function(handle, name, default) local inventory = InventoryClass.__load(handle, name) @@ -75,11 +78,14 @@ if pcall(require, "alyxlib.storage") then end --- ----Add a number of values to a key. +---Increases value amount to a key. --- ----@param key any ----@param value? integer # Default is 1. ----@return number # The value of the key after adding. +---If the key does not exist, it will be created. +---If no value is provided, it will default to 1. +--- +---@param key any # The key to increment +---@param value? integer # Value to increment by +---@return number # Value of the key after increment function InventoryClass:Add(key, value) value = value or 1 local current = self.items[key] @@ -93,11 +99,11 @@ function InventoryClass:Add(key, value) end --- ----Remove a number of values from a key. +---Decreases the value of a key. --- ----@param key any ----@param value? integer # Default is 1. ----@return number # The value of the key after removal. +---@param key any # The key to decrement +---@param value? integer # Value to decrement by +---@return number # Value of the key after decrement function InventoryClass:Remove(key, value) value = value or 1 local current = self.items[key] @@ -114,10 +120,11 @@ function InventoryClass:Remove(key, value) end --- ----Get the value associated with a key. This is *not* the same as `inv.items[key]`. +---Gets the value associated with a key. +---If the key does not exist, 0 is returned. --- ----@param key any ----@return integer +---@param key any # The key to get +---@return integer # Value of the key function InventoryClass:Get(key) local val = self.items[key] if val then @@ -127,10 +134,10 @@ function InventoryClass:Get(key) end --- ----Get the key with the highest value and its value. +---Gets the key with the highest value and its value. --- ----@return any # The key with the highest value. ----@return integer # The value associated with the key. +---@return any # The key with the highest value +---@return integer # The value associated with the key function InventoryClass:Highest() local best_key, best_value = nil, 0 for key, value in pairs(self.items) do @@ -143,10 +150,10 @@ function InventoryClass:Highest() end --- ----Get the key with the lowest value and its value. +---Gets the key with the lowest value and its value. --- ----@return any # The key with the lowest value. ----@return integer # The value associated with the key. +---@return any # The key with the lowest value +---@return integer # The value associated with the key function InventoryClass:Lowest() local best_key, best_value = nil, nil for key, value in pairs(self.items) do @@ -160,7 +167,7 @@ function InventoryClass:Lowest() end --- ----Get if the inventory contains a key with a value greater than 0. +---Gets if the inventory contains `key` with a value greater than 0. --- ---@param key any ---@return boolean @@ -172,10 +179,10 @@ function InventoryClass:Contains(key) end --- ----Return the number of items in the inventory. +---Returns the number of items in the inventory. --- ----@return integer key_sum # Total number of keys in the inventory. ----@return integer value_sum # Total number of values assigned to all keys. +---@return integer key_sum # Total number of keys in the inventory +---@return integer value_sum # Total number of values assigned to all keys function InventoryClass:Length() local key_sum,value_sum = 0,0 for _,value in pairs(self.items) do @@ -186,9 +193,9 @@ function InventoryClass:Length() end --- ----Get if the inventory is empty. +---Gets if the inventory is empty. --- ----@return boolean +---@return boolean # True if the inventory is empty function InventoryClass:IsEmpty() for _, _ in pairs(self.items) do return false @@ -211,13 +218,13 @@ end --- ----Create a new `Inventory` object. +---Creates a new [Inventory](lua://Inventory) object. --- ----@param starting_inventory? table ----@return Inventory -function Inventory(starting_inventory) +---@param startingInventory? table # Starting inventory +---@return Inventory # The new [Inventory](lua://Inventory) +function Inventory(startingInventory) return setmetatable({ - items = starting_inventory or {} + items = startingInventory or {} }, InventoryClass) end diff --git a/scripts/vscripts/alyxlib/data/queue.lua b/scripts/vscripts/alyxlib/data/queue.lua index 88e2014..f6d4dda 100644 --- a/scripts/vscripts/alyxlib/data/queue.lua +++ b/scripts/vscripts/alyxlib/data/queue.lua @@ -1,16 +1,19 @@ --[[ - v1.5.1 + v1.5.2 https://github.com/FrostSource/alyxlib A queue is a data structure where items are added at one end and removed from the other, so the first item added is the first one taken out. - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: require "alyxlib.data.queue" ]] -local version = "v1.5.1" +local version = "v1.5.2" +--- +---Queue data structure. +--- ---@class Queue local QueueClass = { @@ -27,10 +30,10 @@ if pcall(require, "alyxlib.storage") then --- ---Helper function for saving the `queue`. --- - ---@param handle EntityHandle # The entity to save on. - ---@param name string # The name to save as. - ---@param queue Queue # The stack to save. - ---@return boolean # If the save was successful. + ---@param handle EntityHandle # The entity to save on + ---@param name string # The name to save as + ---@param queue Queue # The stack to save + ---@return boolean # If the save was successful ---@luadoc-ignore function QueueClass.__save(handle, name, queue) return Storage.SaveTableCustom(handle, name, queue, "Queue") @@ -41,9 +44,9 @@ if pcall(require, "alyxlib.storage") then --- ---Helper function for loading the `stack`. --- - ---@param handle EntityHandle # Entity to load from. - ---@param name string # Name to load. - ---@return Queue|nil + ---@param handle EntityHandle # Entity to load from + ---@param name string # Name to load + ---@return Queue|nil # The loaded stack ---@luadoc-ignore function QueueClass.__load(handle, name) local queue = Storage.LoadTableCustom(handle, name, "Queue") @@ -55,13 +58,13 @@ if pcall(require, "alyxlib.storage") then CBaseEntity.SaveQueue = Storage.SaveQueue --- - ---Load a Queue. + ---Loads a Queue. --- ---@generic T - ---@param handle EntityHandle # Entity to load from. - ---@param name string # Name the Queue was saved as. - ---@param default? T # Optional default value. - ---@return Queue|T + ---@param handle EntityHandle # Entity to load from + ---@param name string # Name the Queue was saved as + ---@param default? T # Optional default value + ---@return Queue|T # The loaded Queue ---@luadoc-ignore Storage.LoadQueue = function(handle, name, default) local queue = QueueClass.__load(handle, name) @@ -73,22 +76,23 @@ if pcall(require, "alyxlib.storage") then CBaseEntity.LoadQueue = Storage.LoadQueue end - - --- ----Add values to the queue in the order they appear. +---Adds values to the queue in the order they are provided. --- ----@param ... any +---@param ... any # Any number of values function QueueClass:Enqueue(...) - local args = {...} - for i = #args, 1, -1 do - table.insert(self.items, 1, args[i]) + for i = select("#", ...), 1, -1 do + table.insert(self.items, 1, select(i, ...)) end end ----Get a number of values in reverse order of the queue. ----@param count? number # Default is 1 ----@return ... +--- +---Removes and returns one or more items from the front of the queue. +--- +---If `count` is omitted, a single item will is dequeued. +--- +---@param count? number # Number of items to dequeue +---@return ... # The dequeued items in the original insertion order function QueueClass:Dequeue(count) count = min(count or 1, #self.items) local tbl = {} @@ -99,10 +103,10 @@ function QueueClass:Dequeue(count) end --- ----Peek at a number of items at the front of the queue without removing them. +---Peeks at a number of items at the front of the queue without removing them. --- ----@param count? number # Default is 1 ----@return ... +---@param count? number # Number of items to peek at +---@return ... # The peeked items function QueueClass:Front(count) count = min(count or 1, #self.items) local tbl = {} @@ -113,10 +117,10 @@ function QueueClass:Front(count) end --- ----Peek at a number of items at the back of the queue without removing them. +---Peeks at a number of items at the back of the queue without removing them. --- ----@param count? number # Default is 1 ----@return ... +---@param count? number # Number of items to peek at +---@return ... # The peeked items function QueueClass:Back(count) count = min(count or 1, #self.items) local tbl = {} @@ -127,9 +131,9 @@ function QueueClass:Back(count) end --- ----Remove a value from the queue regardless of its position. +---Removes a value from the queue regardless of its position. --- ----@param value any +---@param value any # The value to remove function QueueClass:Remove(value) for index, val in ipairs(self.items) do if value == val then @@ -140,11 +144,12 @@ function QueueClass:Remove(value) end --- ----Move an existing value to the front of the queue. ----Only the first occurance will be moved. +---Moves an existing value to the back of the queue. +--- +---Only the furthest back occurance will be moved. --- ----@param value any # The value to move. ----@return boolean # True if value was found and moved. +---@param value any # The value to move +---@return boolean # True if value was found and moved function QueueClass:MoveToBack(value) for index, val in ipairs(self.items) do if value == val then @@ -157,11 +162,12 @@ function QueueClass:MoveToBack(value) end --- ----Move an existing value to the bottom of the stack. ----Only the first occurance will be moved. +---Moves an existing value to the front of the queue. --- ----@param value any # The value to move. ----@return boolean # True if value was found and moved. +---Only the furthest back occurance will be moved. +--- +---@param value any # The value to move +---@return boolean # True if value was found and moved function QueueClass:MoveToFront(value) for index, val in ipairs(self.items) do if value == val then @@ -174,25 +180,26 @@ function QueueClass:MoveToFront(value) end --- ----Get if this queue contains a value. +---Gets if this queue contains a value. --- ----@param value any ----@return boolean +---@param value any # The value to search for +---@return boolean # True if the value is in the queue function QueueClass:Contains(value) return vlua.find(self.items, value) ~= nil end --- ----Return the number of items in the queue. +---Returns the number of items in the queue. --- ----@return integer +---@return integer # The number of items function QueueClass:Length() return #self.items end --- ----Get if the stack is empty. +---Gets if the stack is empty. --- +---@return boolean # True if the stack is empty function QueueClass:IsEmpty() return #self.items == 0 end @@ -213,7 +220,8 @@ end --- ----Create a new `Queue` object. +---Create a new [Queue](lua://Queue) instance. +--- ---Last value is at the front of the queue. --- ---E.g. @@ -224,8 +232,8 @@ end --- "Front" --- ) --- ----@param ... any ----@return Queue +---@param ... any # Starting values +---@return Queue # The new queue function Queue(...) return setmetatable({ items = {...} diff --git a/scripts/vscripts/alyxlib/data/stack.lua b/scripts/vscripts/alyxlib/data/stack.lua index 58269b3..b3afb56 100644 --- a/scripts/vscripts/alyxlib/data/stack.lua +++ b/scripts/vscripts/alyxlib/data/stack.lua @@ -1,16 +1,19 @@ --[[ - v1.2.4 + v1.2.5 https://github.com/FrostSource/alyxlib A stack is a data structure where elements are added to the top and removed from the top, with the most recently added item being the first to be removed. - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: require "alyxlib.data.stack" ]] -local version = "v1.2.4" +local version = "v1.2.5" +--- +---Stack data structure. +--- ---@class Stack local StackClass = { @@ -27,10 +30,10 @@ if pcall(require, "alyxlib.storage") then --- ---Helper function for saving the `stack`. --- - ---@param handle EntityHandle # The entity to save on. - ---@param name string # The name to save as. - ---@param stack Stack # The stack to save. - ---@return boolean # If the save was successful. + ---@param handle EntityHandle # The entity to save on + ---@param name string # The name to save as + ---@param stack Stack # The stack to save + ---@return boolean # If the save was successful ---@luadoc-ignore function StackClass.__save(handle, name, stack) return Storage.SaveTableCustom(handle, name, stack, "Stack") @@ -41,9 +44,9 @@ if pcall(require, "alyxlib.storage") then --- ---Helper function for loading the `stack`. --- - ---@param handle EntityHandle # Entity to load from. - ---@param name string # Name to load. - ---@return Stack|nil + ---@param handle EntityHandle # Entity to load from + ---@param name string # Name to load + ---@return Stack|nil # The loaded stack ---@luadoc-ignore function StackClass.__load(handle, name) local stack = Storage.LoadTableCustom(handle, name, "Stack") @@ -55,13 +58,13 @@ if pcall(require, "alyxlib.storage") then CBaseEntity.SaveStack = Storage.SaveStack --- - ---Load a Stack. + ---Loads a Stack. --- ---@generic T - ---@param handle EntityHandle # Entity to load from. - ---@param name string # Name the Stack was saved as. - ---@param default? T # Optional default value. - ---@return Stack|T + ---@param handle EntityHandle # Entity to load from + ---@param name string # Name the Stack was saved as + ---@param default? T # Optional default value + ---@return Stack|T # The loaded stack ---@luadoc-ignore Storage.LoadStack = function(handle, name, default) local stack = StackClass.__load(handle, name) @@ -75,19 +78,19 @@ end --- ----Push values to the stack. +---Pushes values to the stack. --- ----@param ... any +---@param ... any # Any number of values function StackClass:Push(...) - for _, value in ipairs({...}) do - table.insert(self.items, 1, value) + for i = 1, select("#", ...) do + table.insert(self.items, 1, select(i, ...)) end end --- ----Pop a number of items from the stack. +---Pops values from the stack. --- ----@param count? number # Default is 1 +---@param count? number # Number of items to pop ---@return ... function StackClass:Pop(count) count = min(count or 1, #self.items) @@ -99,9 +102,9 @@ function StackClass:Pop(count) end --- ----Peek at a number of items at the top of the stack without removing them. +---Peeks at a number of items on the top of the stack without removing them. --- ----@param count? number # Default is 1 +---@param count? number # Number of items to peek ---@return ... function StackClass:Top(count) count = min(count or 1, #self.items) @@ -113,9 +116,9 @@ function StackClass:Top(count) end --- ----Peek at a number of items at the bottom of the stack without removing them. +---Peeks at a number of items on the bottom of the stack without removing them. --- ----@param count? number # Default is 1 +---@param count? number # Number of items to peek ---@return ... function StackClass:Bottom(count) count = min(count or 1, #self.items) @@ -127,9 +130,9 @@ function StackClass:Bottom(count) end --- ----Remove a value from the stack regardless of its position. +---Removes a value from the stack regardless of its position. --- ----@param value any +---@param value any # The value to remove function StackClass:Remove(value) for index, val in ipairs(self.items) do if value == val then @@ -140,11 +143,12 @@ function StackClass:Remove(value) end --- ----Move an existing value to the top of the stack. +---Moves an existing value to the top of the stack. +--- ---Only the first occurance will be moved. --- ----@param value any # The value to move. ----@return boolean # True if value was found and moved. +---@param value any # The value to move +---@return boolean # True if value was found and moved function StackClass:MoveToTop(value) for index, val in ipairs(self.items) do if value == val then @@ -157,11 +161,12 @@ function StackClass:MoveToTop(value) end --- ----Move an existing value to the bottom of the stack. +---Moves an existing value to the bottom of the stack. +--- ---Only the first occurance will be moved. --- ----@param value any # The value to move. ----@return boolean # True if value was found and moved. +---@param value any # The value to move +---@return boolean # True if value was found and moved function StackClass:MoveToBottom(value) for index, val in ipairs(self.items) do if value == val then @@ -174,25 +179,26 @@ function StackClass:MoveToBottom(value) end --- ----Get if this stack contains a value. +---Gets if this stack contains a value. --- ----@param value any ----@return boolean +---@param value any # The value to search for +---@return boolean # True if the value is in the stack function StackClass:Contains(value) return vlua.find(self.items, value) ~= nil end --- ----Return the number of items in the stack. +---Returns the number of items in the stack. --- ----@return integer +---@return integer # The number of items function StackClass:Length() return #self.items end --- ----Get if the stack is empty. +---Gets if the stack is empty. --- +---@return boolean # True if the stack is empty function StackClass:IsEmpty() return #self.items == 0 end @@ -213,7 +219,8 @@ end --- ----Create a new `Stack` object. +---Creates a new [Stack](lua://Stack) object. +--- ---First value is at the top. --- ---E.g. diff --git a/scripts/vscripts/alyxlib/debug/commands.lua b/scripts/vscripts/alyxlib/debug/commands.lua index 8adad09..d176cad 100644 --- a/scripts/vscripts/alyxlib/debug/commands.lua +++ b/scripts/vscripts/alyxlib/debug/commands.lua @@ -1,23 +1,23 @@ --[[ - v1.1.1 + v1.2.0 https://github.com/FrostSource/alyxlib - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: require "alyxlib.debug.commands" ]] -local version = "v1.1.1" +local version = "v1.2.0" local alyxlibCommands = {} --- ---Registers a command for the AlyxLib library. --- ----@param name string # Name of the command that will be given in the console. ----@param func fun(_:string, ...:string) # Function to run when the command is called. ----@param helpText? string # Description of the command. ----@param flags? number # Flags for the command. +---@param name string # Name of the command that will be given in the console +---@param func fun(_:string, ...:string) # Function to run when the command is called +---@param helpText? string # Description of the command +---@param flags? number # Flags for the command function RegisterAlyxLibCommand(name, func, helpText, flags) helpText = helpText or "[No description]" Convars:RegisterCommand(name, func, helpText, flags or 0) @@ -27,13 +27,47 @@ end --- ---Registers a new AlyxLib console variable. --- ----@param name string # Name of the convar that will be given in the console. ----@param defaultValue string # Default value of the convar. ----@param helpText? string # Description of the convar. ----@param flags? integer # Flags for the convar. -function RegisterAlyxLibConvar(name, defaultValue, helpText, flags) +---If `defaultValue` or `callback` is a function, the convar will be +---created using `EasyConvars`. +--- +---@param name string # Name of the convar that will be given in the console +---@param defaultValue string|function # Default value of the convar or initializer function +---@param helpText? string # Description of the convar +---@param flags? integer # Flags for the convar +---@param callback? fun(newVal:string, oldVal:string): any # Update function called after the value has been changed +function RegisterAlyxLibConvar(name, defaultValue, helpText, flags, callback) helpText = helpText or "[No description]" - Convars:RegisterConvar(name, defaultValue, helpText, flags or 0) + flags = flags or 0 + if callback or type(defaultValue) == "function" then + EasyConvars:RegisterConvar(name, defaultValue, helpText, flags, callback) + else + Convars:RegisterConvar(name, defaultValue, helpText, flags) + end + alyxlibCommands[name] = helpText + + ---@TODO Support initializer values + if type(defaultValue) ~= "function" then + alyxlibCommands[name] = alyxlibCommands[name] .. ", default: "..tostring(defaultValue) + end +end + +--- +---Registers a new AlyxLib console variable. +--- +---@param name string # Name of the convar that will be given in the console +---@param defaultValue string # Default value of the convar +---@param helpText? string # Description of the convar +---@param flags? integer # Flags for the convar +---@param postUpdate? fun(newVal:string, oldVal:string): any # Update function called after the value has been changed +---@param persistent? boolean # Whether the convar should be persistent +function RegisterAlyxLibEasyConvar(name, defaultValue, helpText, flags, postUpdate, persistent) + helpText = helpText or "[No description]" + EasyConvars:RegisterConvar(name, defaultValue, helpText, flags or 0, postUpdate) + + if persistent then + EasyConvars:SetPersistent(name, true) + end + alyxlibCommands[name] = "Default: "..tostring(defaultValue)..", "..helpText end @@ -364,6 +398,31 @@ RegisterAlyxLibCommand("print_ents", function (_, pattern, ...) Debug.PrintEntities(pattern, false, false, properties) end, "Prints all entities with class, name or model matching a pattern", 0) +--- +---Prints an entity by its index. +--- +RegisterAlyxLibCommand("print_ent_by_index", function(_, index) + + if index == nil then + warn("Must supply an index to search for, e.g. prints_ent_by_index 1") + return + end + + local i = tonumber(index) + if i == nil then + warn("Index must be a number, e.g. prints_ent_by_index 1") + return + end + + local ent = EntIndexToHScript(i) + if ent == nil then + warn("Could not find entity with index '"..index.."'") + return + end + + print(Debug.EntStr(ent)) +end, "Prints the entity with the given index", 0) + --- ---Show the position of an entity relative to the player using debug drawing. --- @@ -398,7 +457,8 @@ RegisterAlyxLibCommand("sphere", function (_, x, y, z, r) z = tonumber(z) or 0 r = tonumber(r) or 16 - DebugDrawSphere(Vector(x, y, z), Vector(255, 255, 255), 255, r, false, 10) + DebugDrawSphere(Vector(x, y, z), Vector(255, 255, 255), 255, r, false, 15) + DebugDrawSphere(Vector(x, y, z), Vector(255, 255, 255), 255, r, true, 10) end, "Draws a debug sphere at 3D position with a radius", 0) @@ -433,14 +493,9 @@ end, "Prints context criteria for an entity except for values saved using storag --- RegisterAlyxLibCommand("healme", function (_, amount) - amount = tonumber(amount) + amount = tonumber(amount) or 10 - if not amount then - warn("Must provide a health amount") - return - end - - Player:SetHealth(amount) + Player:SetHealth(Player:GetHealth() + amount) end, "Heals the player by a given amount", 0) --- @@ -461,7 +516,7 @@ RegisterAlyxLibCommand("ent_find_by_address", function (_, tblpart, colon, hash) Msg("\tParent: " .. (tostring(foundEnt:GetMoveParent() or "[none]")) .."\n") Msg("\tModel: " .. foundEnt:GetModelName()) else - Msg("Could not find any entity matching '" .. hash .. "'") + Msg("Could not find any entity matching '" .. tostring(hash) .. "'") end end, "Prints info for an entity by its table address", 0) @@ -484,6 +539,21 @@ RegisterAlyxLibCommand("ent_rename", function(_, pattern, newName) end end, "Renames the first entity found using a pattern to a new name", 0) +--- +---Prints the children of an entity found using `pattern`. +--- +RegisterAlyxLibCommand("ent_print_children", function(_, pattern) + local ent = Debug.FindEntityByPattern(pattern) + if not ent then + warn("Could not find entity with pattern '"..pattern.."'") + return + end + + print(entstr(ent)) + + Debug.PrintEntityList(ent:GetChildrenMemSafe()) +end) + 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 @@ -573,4 +643,7 @@ local symbols = {"and","break","do","else","elseif","end","false","for","functio -- end +---Hidden convar state that can be retrieved by other scripts +Convars:RegisterConvar("noclip_vr_enabled", "0", "True if noclip_vr is enabled (readonly)", FCVAR_HIDDEN) + return version \ No newline at end of file diff --git a/scripts/vscripts/alyxlib/debug/common.lua b/scripts/vscripts/alyxlib/debug/common.lua index 0962164..eece187 100644 --- a/scripts/vscripts/alyxlib/debug/common.lua +++ b/scripts/vscripts/alyxlib/debug/common.lua @@ -1,10 +1,10 @@ --[[ - v2.2.0 + v2.3.0 https://github.com/FrostSource/alyxlib Debug utility functions. - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: require "alyxlib.debug.common" ]] @@ -13,18 +13,22 @@ require "alyxlib.extensions.entity" require "alyxlib.math.common" Debug = {} -Debug.version = "v2.2.0" +Debug.version = "v2.3.0" --- ---Finds the first entity whose name, class or model matches `pattern`. --- ---`pattern` can also be an entity handle string, e.g. `0x0026caf8` --- ----@param pattern string # The search pattern to look for. ----@param exact boolean? # If true the pattern must match exactly, otherwise wildcards will be used. ----@return EntityHandle +---@param pattern string # The search pattern to look for +---@param exact boolean? # If true the pattern must match exactly, otherwise wildcards will be used +---@return EntityHandle? # The found entity, or nil if not found function Debug.FindEntityByPattern(pattern, exact) + if pattern == nil then + return nil + end + if Debug.IsEntityHandleString(pattern) then return Debug.FindEntityByHandleString(pattern) end @@ -51,15 +55,17 @@ function Debug.FindEntityByPattern(pattern, exact) return ent end +---@diagnostic disable-next-line: lowercase-global +entfind = Debug.FindEntityByPattern --- ---Finds all entities whose name, class or model match `pattern`. --- ---`pattern` can also be an entity handle string, e.g. `0x0026caf8` --- ----@param pattern string # The search pattern to look for. ----@param exact boolean? # If true the pattern must match exactly, otherwise wildcards will be used. ----@return EntityHandle[] +---@param pattern string # The search pattern to look for +---@param exact boolean? # If true the pattern must match exactly, otherwise wildcards will be used +---@return EntityHandle[] # The found entities function Debug.FindAllEntitiesByPattern(pattern, exact) local ents = {} @@ -82,17 +88,17 @@ function Debug.FindAllEntitiesByPattern(pattern, exact) end --- ----Prints a formated indexed list of entities with custom property information. +---Prints a formated indexed list of entities with custom property information. ---Also links children with their parents by displaying the index alongside the parent for easy look-up. --- --- Debug.PrintEntityList(ents, {"getclassname", "getname", "getname"}) --- ----If no properties are supplied the default properties are used: GetClassname, GetName, GetModelName ----If an empty property table is supplied only the base values are shown: Index, Handle, Parent +---If no properties are supplied the default properties are used: GetClassname, GetName, GetModelName. +---If an empty property table is supplied only the base values are shown: Index, Handle, Parent. ---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 list EntityHandle[] # List of entities to print +---@param properties? string[] # List of property patterns to search for function Debug.PrintEntityList(list, properties) if #list == 0 then @@ -181,7 +187,7 @@ function Debug.PrintEntityList(list, properties) lenHandle = lenHandle + 1 local formatStr = "%-"..lenIndex.."s %-"..lenHandle.."s" for propertyIndex, propertyTable in ipairs(propertyMetaData) do - formatStr = formatStr .. " | %-"..propertyTable.max.."s" + formatStr = formatStr .. " | %-"..math.min(propertyTable.max, 99).."s" end formatStr = formatStr .. " | %-"..lenParent.."s" @@ -201,10 +207,11 @@ function Debug.PrintEntityList(list, properties) end local cachedEntities = {} + --- ---Prints information about all existing entities. --- ----@param properties? string[] # List of property patterns to search for when displaying entity information. +---@param properties? string[] # List of property patterns to search for function Debug.PrintAllEntities(properties) properties = properties or {"GetClassname", "GetName", "GetModelName"} local list = {} @@ -220,7 +227,7 @@ end --- ---Prints information about any new entities since the last time `Debug.PrintAllEntities` was called. --- ----@param properties? string[] # List of property patterns to search for when displaying entity information. +---@param properties? string[] # List of property patterns to search for function Debug.PrintDiffEntities(properties) properties = properties or {"GetClassname", "GetName", "GetModelName"} local list = {} @@ -235,14 +242,14 @@ function Debug.PrintDiffEntities(properties) end --- ----Print entities matching a search string. +---Prints entities matching a search string. --- ---Searches name, classname and model name. --- ----@param search string # Search string, may include `*`. ----@param exact boolean # If the search should match exactly or part of the name. ----@param dont_include_parents boolean # Parents won't be included in the results. ----@param properties? string[] # List of property patterns to search for when displaying entity information. +---@param search string # Search string, may include `*` +---@param exact boolean # If the search should match exactly or part of the name +---@param dont_include_parents boolean # Parents won't be included in the results +---@param properties? string[] # List of property patterns to search for function Debug.PrintEntities(search, exact, dont_include_parents, properties) -- Get all matching ents local preents = {} @@ -272,9 +279,9 @@ end --- ---Prints information about all entities within a sphere. --- ----@param origin Vector # Position to search for entities at. ----@param radius number # Max radius to find entities within. ----@param properties? string[] # List of property patterns to search for when displaying entity information. +---@param origin Vector # Position to search for entities at +---@param radius number # Max radius to find entities within +---@param properties? string[] # List of property patterns to search for function Debug.PrintAllEntitiesInSphere(origin, radius, properties) Debug.PrintEntityList(Entities:FindAllInSphere(origin, radius), properties) end @@ -384,7 +391,7 @@ end --- ---Prints the keys/values of a table but not any tested tables. --- ----@param tbl table # Table to print. +---@param tbl table # Table to print function Debug.PrintTableShallow(tbl) if type(tbl) ~= "table" then return end print("{") @@ -397,8 +404,8 @@ 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. +---@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 "" @@ -421,7 +428,7 @@ end --- ---Prints all the values in a table, one value per line, without any numbering or padding. --- ----@param tbl table # Table to print. +---@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)) @@ -435,7 +442,7 @@ end --- ---Prints a value and its type in an easy to read format. --- ----@param value any +---@param value any # Value to print function Debug.PrintValue(value) local vs, ts = getValueString(value) print(vs..ts) @@ -444,8 +451,8 @@ end --- ---Draws a debug line to an entity in game. --- ----@param ent EntityHandle|string # Handle or targetname of the entity(s) to find. ----@param duration number? # Number of seconds the debug should display for. +---@param ent EntityHandle|string # Handle or targetname of the entity(s) to find +---@param duration number? # Number of seconds the debug should display for ---@return EntityHandle[]|EntityHandle? # Entities found, or the entity given function Debug.ShowEntity(ent, duration) duration = duration or 20 @@ -497,7 +504,7 @@ CBaseEntity.PrintCriteria = Debug.PrintEntityCriteria --- ---Prints current context criteria for an entity except for values saved using `storage.lua`. --- ----@param ent EntityHandle +---@param ent EntityHandle # Entity to print function Debug.PrintEntityBaseCriteria(ent) ---@type table local c, d = {}, {} @@ -549,8 +556,8 @@ local classes = { --- ---If the entity is an EntityClass entity the original Valve class name will be returned instead of the EntityClass. --- ----@param ent EntityHandle # The entity to get the class name of. ----@return string # The class name of the entity or "none" if not found. +---@param ent EntityHandle # The entity to get the class name of +---@return string # The class name of the entity or "none" if not found function Debug.GetClassname(ent) -- AI_Squad doesn't exist until CAI_BaseNPC:GetSquad() is called @@ -569,6 +576,9 @@ function Debug.GetClassname(ent) end end +--- +---Prints the table values of all built-in Valve classes. +--- function Debug.PrintMetaClasses() for val, name in pairs(classes) do print(name .. ": " .. tostring(val)) @@ -578,6 +588,8 @@ end --- ---Prints a visual ASCII graph showing the distribution of values between a min/max bound. --- +---Higher heights give more accurate results but can overflow the console making it hard to read. +--- ---E.g. --- --- Debug.PrintGraph(6, 0, 1, { @@ -585,7 +597,7 @@ end --- val2 = RandomFloat(0, 1), --- val3 = RandomFloat(0, 1) --- }) ---- -> +--- --- 1^ [] --- | [] [] --- | [] [] [] @@ -600,10 +612,10 @@ end --- val1 = 0.5374761223793 --- val2 = 0.7315416932106 --- ----@param height integer # Height of the actual graph in print rows. Heigher values give more accurate results but can overflow the console making it hard to read. ----@param min_val? number # Minimum expected value for `name_value_pairs`. Default is `0`. ----@param max_val? number # Maxmimum expected value for `name_value_pairs`. Default is `1`. ----@param name_value_pairs table # Values to visualize on the graph. +---@param height integer # Height of the actual graph in print rows +---@param min_val? number # Minimum expected value for `name_value_pairs`; default is `0` +---@param max_val? number # Maxmimum expected value for `name_value_pairs`; default is `1` +---@param name_value_pairs table # Values to visualize on the graph function Debug.PrintGraph(height, min_val, max_val, name_value_pairs) height = height or 10 min_val = min_val or 0 @@ -729,7 +741,7 @@ end --- ---Prints a nested list of entity inheritance. --- ----@param ent EntityHandle +---@param ent EntityHandle # Entity to print the inheritance of function Debug.PrintInheritance(ent) print(ent:GetClassname() .. " -> " .. tostring(ent)) local parent = getmetatable(ent) @@ -753,14 +765,16 @@ end --- ---Returns a simplified vector string with decimal places truncated. --- ----@param vector Vector ----@return string +---@param vector Vector|QAngle|table # Object to print +---@return string # Vector string function Debug.SimpleVector(vector) return "[" .. math.trunc(vector.x, 3) .. ", " .. math.trunc(vector.y, 3) .. ", " .. math.trunc(vector.z, 3) .. "]" end +---@diagnostic disable-next-line: lowercase-global +vecstr = Debug.SimpleVector --- ----Draw a simple sphere without worrying about all the properties. +---Draws a simple sphere without worrying about all the properties. --- ---@param x number # X position ---@param y number # Y position @@ -793,7 +807,7 @@ function Debug.Sphere(x, y, z, radius, time, color) end --- ----Draw a simple line without worrying about all the properties. +---Draws a simple line without worrying about all the properties. --- ---@param startPos Vector # Start position ---@param endPos Vector # End position @@ -855,7 +869,7 @@ end ---@param tblpart string # Entity table string ---@param colon? string # The colon part ---@param hash? string # The hash part ----@return EntityHandle? +---@return EntityHandle? # The found entity, or nil if not found function Debug.FindEntityByHandleString(tblpart, colon, hash) if tblpart == nil and colon == nil and hash == nil then devwarn("Must provide a valid entity table string, e.g. 'table: 0x0012b03'") @@ -896,8 +910,9 @@ end ---Gets whether the string is in the format of an entity handle. --- ---@param handleString string # The handle string ----@return string # The hash part or nil if not an entity handle +---@return string? # The hash part or nil if not an entity handle function Debug.IsEntityHandleString(handleString) + if handleString == nil then return nil end local mtc = handleString:match("^(table:?%s*)") if mtc then handleString = handleString:sub(#mtc+1) @@ -910,7 +925,7 @@ end ---Converts a number to its ordinal string representation (e.g., 1 โ†’ "1st", 2 โ†’ "2nd", 3 โ†’ "3rd"). --- ---@param n integer # Number to convert to ordinal representation ----@return string +---@return string # Ordinal string function Debug.ToOrdinalString(n) local lastTwo = n % 100 if lastTwo >= 11 and lastTwo <= 13 then @@ -927,7 +942,7 @@ end ---Get the script name and line number of a function or traceback level. --- ---@param f integer|function # Level or function ----@return string +---@return string # Source function Debug.GetSourceLine(f) return debug.getinfo(f, "S").short_src..":"..tostring(debug.getinfo(f, "l").currentline) end @@ -947,4 +962,32 @@ function Debug.Try(action, ...) end end +--- +---Converts a table to single line string representation. +--- +---@param tbl table # The table to convert +---@return string # The string representation +function Debug.TableStr(tbl) + local vals = {} + for key, value in pairs(tbl) do + table.insert(vals, key.." = "..tostring(value)) + end + + return "{ "..table.concat(vals, ", ") .. " }" +end + +--- +---Spawns an entity synchronously. +--- +---@param classname string # The classname of the entity +---@param spawnkeys? table|string # The spawnkeys table or targetname +---@return EntityHandle # The spawned entity +---@diagnostic disable-next-line: lowercase-global +function entspawn(classname, spawnkeys) + if type(spawnkeys) == "string" then + spawnkeys = { targetname = spawnkeys } + end + return SpawnEntityFromTableSynchronous(classname, spawnkeys or {}) +end + return Debug.version \ No newline at end of file diff --git a/scripts/vscripts/alyxlib/debug/controller.lua b/scripts/vscripts/alyxlib/debug/controller.lua index 580ef58..c8aa816 100644 --- a/scripts/vscripts/alyxlib/debug/controller.lua +++ b/scripts/vscripts/alyxlib/debug/controller.lua @@ -1,17 +1,18 @@ --[[ - v1.0.2 + v1.0.3 https://github.com/FrostSource/alyxlib Allows quick debugging of VR controllers. - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: require "alyxlib.debug.controller" ]] + -- Used for button descriptions require "alyxlib.controls.input" -local version = "v1.0.2" +local version = "v1.0.3" Convars:RegisterCommand("alyxlib_start_print_controller_button_presses", function (_) diff --git a/scripts/vscripts/alyxlib/debug/debug_menu.lua b/scripts/vscripts/alyxlib/debug/debug_menu.lua index 46cd72e..5a013dc 100644 --- a/scripts/vscripts/alyxlib/debug/debug_menu.lua +++ b/scripts/vscripts/alyxlib/debug/debug_menu.lua @@ -1,59 +1,162 @@ --[[ - v1.0.0 + v1.1.0 https://github.com/FrostSource/alyxlib The debug menu allows for easier VR testing by offering a customizable in-game menu. + + If not using `alyxlib/init.lua`, load this file at game start using the following line: + + require "alyxlib.debug.debug_menu" ]] -RegisterAlyxLibCommand("alyxlib_debug_menu_show", function (name, ...) +local function updateAttachment() + DebugMenu:UpdateMenuAttachment() +end + +RegisterAlyxLibCommand("debug_menu_show", function (name, ...) DebugMenu:ShowMenu() end, "Forces the debug menu to show") -RegisterAlyxLibConvar("alyxlib_debug_menu_hand", "1", "Hand to attach the debug menu to, 0 = Secondary : 1 = Primary") +RegisterAlyxLibConvar("debug_menu_hand", "1", "Hand to attach the debug menu to, 0 = Secondary : 1 = Primary", 0, updateAttachment) + +RegisterAlyxLibConvar("debug_menu_offset_x", "-3", "X offset of the debug menu", 0) +RegisterAlyxLibConvar("debug_menu_offset_y", "-5", "Y offset of the debug menu", 0) +RegisterAlyxLibConvar("debug_menu_offset_z", "-1", "Z offset of the debug menu", 0) +RegisterAlyxLibConvar("debug_menu_offset_pitch", "-14", "Pitch offset of the debug menu", 0) +RegisterAlyxLibConvar("debug_menu_offset_yaw", "-172", "Yaw offset of the debug menu", 0) +RegisterAlyxLibConvar("debug_menu_offset_roll", "-2", "Roll offset of the debug menu", 0) + +RegisterAlyxLibConvar("debug_menu_width", "15", "Width of the debug menu, min=10 : max=30", 0, function(newVal, oldVal) + DebugMenu:SetSize(Convars:GetInt("debug_menu_width"), nil) +end) +RegisterAlyxLibConvar("debug_menu_height", "14", "Height of the debug menu, min=7 : max=30", 0, function(newVal, oldVal) + DebugMenu:SetSize(nil, Convars:GetInt("debug_menu_height")) +end) + +RegisterAlyxLibConvar("debug_menu_floating", function() + -- Float menu for inside-out tracking + if Player:GetVRControllerType() == 3 then + return true + end + + return false +end, "Menu will float in world instead of attached to hand", 0, updateAttachment) + +RegisterAlyxLibConvar("debug_menu_lock", "0", "Prevents the debug menu from being repositioned by the player", 0) + +RegisterAlyxLibCommand("debug_menu_dump_convars", function() + local categories = DebugMenu:GetLinkedConvars() + for _, category in pairs(categories) do + for _, convar in pairs(category.convars) do + Msg(string.format("%s : %s\n", convar.name, convar.value)) + end + end +end, "Dumps all convars tied to the debug menu and their values to the console", 0) + +RegisterAlyxLibCommand("debug_menu_generate_cfg", function () + -- By collecting first we avoid warnings printed throughout the list + + local dumps = DebugMenu:GetLinkedConvars() + + Msg("\n// Copy and paste the following into debug_menu.cfg to save your settings\n\n") + + Msg("// offsets\n") + Msg("debug_menu_offset_x " .. math.trunc(Convars:GetFloat("debug_menu_offset_x"), 2) .. "\n") + Msg("debug_menu_offset_y " .. math.trunc(Convars:GetFloat("debug_menu_offset_y"), 2) .. "\n") + Msg("debug_menu_offset_z " .. math.trunc(Convars:GetFloat("debug_menu_offset_z"), 2) .. "\n") + Msg("debug_menu_offset_pitch " .. math.trunc(Convars:GetFloat("debug_menu_offset_pitch"), 2) .. "\n") + Msg("debug_menu_offset_yaw " .. math.trunc(Convars:GetFloat("debug_menu_offset_yaw"), 2) .. "\n") + Msg("debug_menu_offset_roll " .. math.trunc(Convars:GetFloat("debug_menu_offset_roll"), 2) .. "\n") + Msg("\n") + + for _, dump in pairs(dumps) do + local name = dump.category.name + if name == nil or name == "" then + name = dump.category.id + end + Msg("// " .. name .. "\n") + for _, convar in pairs(dump.convars) do + if convar.value ~= nil then + local defaultStr = "" + if convar.default ~= nil then + defaultStr = (convar.default ~= convar.value) and ("// default: "..convar.default.."") or "" + end + local valStr = convar.value or "" + local str = string.format("%-" .. dump.maxLengthConvar .. "s %-" .. dump.maxLengthValue .. "s %s\n", convar.name, valStr, defaultStr) + Msg(str) + end + end + Msg("\n") + end + + Msg("// End of debug_menu.cfg\n") +end, "Prints the current Debug Menu settings in cfg format which can be pasted into any cfg file", 0) + +---DPI of the debug menu panel. +local DPI = 64 + +---Minimum and maximum sizes of the debug menu panel. +local MIN_WIDTH = 10 +local MIN_HEIGHT = 7 +local MAX_WIDTH = 30 +local MAX_HEIGHT = 30 + +---Attachment name that the panel attaches to on the hand. +local handAttachment = "" --- ---The debug menu allows for easier VR testing by offering a customizable in-game menu. --- ---@class DebugMenu DebugMenu = {} -DebugMenu.version = "v1.0.0" +DebugMenu.version = "v1.1.0" --- ---A category of items in the debug menu. --- ---@class DebugMenuCategory ----@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. +---@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 # 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 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 {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). +---@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 {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) +---@field condition? string|fun():boolean # The condition that must be met for this item to be visible +--- ---The panel entity. +--- ---@type CPointClientUIWorldPanel DebugMenu.panel = nil +--- +---The categories in the debug menu. +--- ---@type DebugMenuCategory[] DebugMenu.categories = {} local debugMenuOpen = false local handChangedListener = nil +---The last hand that clicked a button. +---@type CPropVRHand? +local lastClickHand = nil + ---Command to test trace button presses if not IsVREnabled() then Convars:RegisterCommand("_debug_menu_test_button_press", function() @@ -61,6 +164,129 @@ if not IsVREnabled() then end, "", FCVAR_HIDDEN) end +local function empty(str) + return type(str) ~= "string" or str == "" +end + +-- Hoping these two functions find a way to be generalized later + +-- Convert position and orientation to entity's local coordinate system +local function TransformToLocal(entity, position, angles) + local localPos = entity:TransformPointWorldToEntity(position) + + -- Convert angles to local orientation using reference points + local origin = position + local forwardPoint = origin + angles:Forward() * 10 + local upPoint = origin + angles:Up() * 10 + + local localForward = entity:TransformPointWorldToEntity(forwardPoint) + local localUp = entity:TransformPointWorldToEntity(upPoint) + + return { + position = localPos, + forwardRef = localForward, + upRef = localUp, + } +end + +-- Convert from entity's local coordinate system back to world coordinates +local function TransformToWorld(entity, localTransform) + local worldPos = entity:TransformPointEntityToWorld(localTransform.position) + + -- Reconstruct world orientation from reference points + local worldForwardRef = entity:TransformPointEntityToWorld(localTransform.forwardRef) + local worldUpRef = entity:TransformPointEntityToWorld(localTransform.upRef) + + local worldForward = (worldForwardRef - worldPos):Normalized() + local worldUp = (worldUpRef - worldPos):Normalized() + + -- Convert direction vectors back to angles + local angles = VectorToAngles(worldForward) + + -- Calculate roll from up vector + local expectedRight = worldForward:Cross(Vector(0, 0, 1)):Normalized() + if expectedRight:Length() < 0.1 then + expectedRight = Vector(1, 0, 0) + end + local expectedUp = expectedRight:Cross(worldForward):Normalized() + + local roll = math.atan2(worldUp:Dot(expectedRight), worldUp:Dot(expectedUp)) + angles.z = Rad2Deg(roll) + + return worldPos, angles +end + +---Mirrors the offset and angles for the opposite hand. +---@param offset Vector +---@param angles QAngle +local function mirrorOffsetToOppositeHand(offset, angles, hand) + offset = Vector(offset.x, -offset.y, offset.z) + angles = QAngle(-angles.x, 180-angles.y, angles.z) + return offset, angles +end + +---@param dragHand CPropVRHand +local function startDraggingMenu(dragHand) + + local panel = DebugMenu.panel + + local relativeTransform = TransformToLocal(dragHand, panel:GetOrigin(), panel:GetAngles()) + + panel:SetParent(GetWorld(), nil) + + Player:QuickThink(function() + if not IsValidEntity(dragHand) or + not IsValidEntity(panel) then + return nil + end + + if not Player:IsDigitalActionOnForHand(dragHand.Literal, DIGITAL_INPUT_MENU_INTERACT) then + if Convars:GetBool("debug_menu_floating") then + return nil + end + + local hand = Convars:GetBool("debug_menu_hand") and Player.PrimaryHand or Player.SecondaryHand + panel:SetParent(hand, handAttachment) + + local localOrigin = panel:GetLocalOrigin() + local localAngles = panel:GetLocalAngles() + + if hand == Player.LeftHand then + localOrigin, localAngles = mirrorOffsetToOppositeHand(localOrigin, localAngles) + end + + Convars:SetFloat("debug_menu_offset_x", localOrigin.x) + Convars:SetFloat("debug_menu_offset_y", localOrigin.y) + Convars:SetFloat("debug_menu_offset_z", localOrigin.z) + Convars:SetFloat("debug_menu_offset_pitch", localAngles.x) + Convars:SetFloat("debug_menu_offset_yaw", localAngles.y) + Convars:SetFloat("debug_menu_offset_roll", localAngles.z) + + Msg("\n") + Msg("Debug menu offsets updated:\n") + Msg("\ndebug_menu_offset_x " .. math.trunc(Convars:GetFloat("debug_menu_offset_x"), 2)) + Msg("\ndebug_menu_offset_y " .. math.trunc(Convars:GetFloat("debug_menu_offset_y"), 2)) + Msg("\ndebug_menu_offset_z " .. math.trunc(Convars:GetFloat("debug_menu_offset_z"), 2)) + Msg("\ndebug_menu_offset_pitch " .. math.trunc(Convars:GetFloat("debug_menu_offset_pitch"), 2)) + Msg("\ndebug_menu_offset_yaw " .. math.trunc(Convars:GetFloat("debug_menu_offset_yaw"), 2)) + Msg("\ndebug_menu_offset_roll " .. math.trunc(Convars:GetFloat("debug_menu_offset_roll"), 2)) + Msg("\n\n") + + DebugMenu:UpdateMenuAttachment() + + return nil + end + + local newOrigin, angles = TransformToWorld(dragHand, relativeTransform) + + panel:SetOrigin(newOrigin) + panel:SetQAngle(angles) + + return 0 + end) + +end + --- ---The scope of the debug menu script. --- @@ -80,7 +306,7 @@ local debugPanelScriptScope = { end if item.callback then - item.callback() + item.callback(item) end end, @@ -97,12 +323,12 @@ local debugPanelScriptScope = { end -- Update default if user is tracking manually - if item.default ~= nil and type(item.default) ~= "function" then + if empty(item.convar) or (item.default ~= nil and type(item.default) ~= "function") then item.default = on end if item.callback then - item.callback(on) + item.callback(on, item) end end, @@ -158,24 +384,111 @@ local debugPanelScriptScope = { if DebugMenu:IsOpen() then DebugMenu:Refresh() end + end, + + _DebugMenuDrag = function() + if not Convars:GetBool("debug_menu_lock") and lastClickHand then + startDraggingMenu(lastClickHand) + end end } +---Moves the panel with player's anchor parent so it doesn't get left behind on trains. +---Parenting causes stutters so it must be calculated manually. +---@param panel EntityHandle +local function updatePanelWithAnchorParent(panel) + ---@TODO Unsure best way to follow moving non vr player + if not Player.HMDAnchor then + return + end + + ---@type EntityHandle? + local currentAnchorParent = nil + local anchorRelativeTransform = nil + + panel:SetContextThink("AnchorParentUpdate", function() + local parent = Player.HMDAnchor:GetMoveParent() + if parent ~= currentAnchorParent then + currentAnchorParent = Player.HMDAnchor:GetMoveParent() + if currentAnchorParent then + anchorRelativeTransform = TransformToLocal(currentAnchorParent, panel:GetOrigin(), panel:GetAngles()) + end + end + + if currentAnchorParent and anchorRelativeTransform then + local newPos, newAng = TransformToWorld(currentAnchorParent, anchorRelativeTransform) + panel:SetOrigin(newPos) + panel:SetQAngle(newAng) + return 0 + end + + return 0.5 + end, 0) +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)) + if not IsValidEntity(self.panel) then return end + + if not Player.HMDAvatar or IsFakeVREnabled() then + SendToConsole("bind r _debug_menu_test_button_press") else - self.panel:SetParent(hand, "constraint1") - self.panel:ResetLocal() - self.panel:SetLocalAngles(0, 0, 0) - self.panel:SetLocalOrigin(Vector(4, 9, 0)) + local handType = -1 + if not Convars:GetBool("debug_menu_floating") then + handType = Convars:GetInt("debug_menu_hand") == 1 and InputHandSecondary or InputHandPrimary + + StopListeningToPlayerEvent(handChangedListener) + handChangedListener = ListenToPlayerEvent("primary_hand_changed", function() + self:UpdateMenuAttachment() + end) + end + + -- Cough handpose gets in the way for close menus + Player:SetCoughHandEnabled(false) + + Input:StopListeningByContext(self) + -- Handle distant button presses + Input:ListenToButton("press", + handType, + DIGITAL_INPUT_MENU_INTERACT, 1, + function (context, params) + lastClickHand = params.hand + self:ClickHoveredButton() + end, self) + end + + if Convars:GetBool("debug_menu_floating") or not Player.HMDAvatar or IsFakeVREnabled() then + local player = Entities:GetLocalPlayer() + local eyePos = player:EyePosition() + eyePos = eyePos + player:EyeAngles():Up() * -((Convars:GetFloat("debug_menu_height") or 0) / 2) + local fDir = player:EyeAngles():Forward() + local fAng = VectorToAngles(fDir) + fAng = RotateOrientation(fAng, QAngle(0, -90, 90)) + self.panel:SetQAngle(fAng) + self.panel:SetOrigin(eyePos + fDir * 16) + + -- Panel must be parented for moving during drag to work + self.panel:SetParent(GetWorld(), nil) + + updatePanelWithAnchorParent(self.panel) + else + local hand = Convars:GetBool("debug_menu_hand") and Player.PrimaryHand or Player.SecondaryHand + + local x,y,z = Convars:GetFloat("debug_menu_offset_x"), Convars:GetFloat("debug_menu_offset_y"), Convars:GetFloat("debug_menu_offset_z") + local pitch,yaw,roll = Convars:GetFloat("debug_menu_offset_pitch"), Convars:GetFloat("debug_menu_offset_yaw"), Convars:GetFloat("debug_menu_offset_roll") + + local offset = Vector(x, y, z) + local angles = QAngle(pitch, yaw, roll) + + if hand == Player.LeftHand then + offset, angles = mirrorOffsetToOppositeHand(offset, angles) + end + + self.panel:SetParent(hand, handAttachment) + self.panel:SetLocalOrigin(offset) + self.panel:SetLocalQAngle(angles) end end @@ -187,47 +500,22 @@ function DebugMenu:ShowMenu() 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, - height = 12,--16 - panel_dpi = 64, + width = MAX_WIDTH, + height = MAX_HEIGHT, + panel_dpi = DPI, ignore_input = 0, lit = 0, - interact_distance = 12, + interact_distance = 64, - vertical_align = "1", + vertical_align = "0", -- orientation = "0", horizontal_align = "1", - }) - if not Player.HMDAvatar or IsFakeVREnabled() 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)) - self.panel:SetQAngle(a) - self.panel:SetOrigin(eyePos + dir * 16) - - SendToConsole("bind r _debug_menu_test_button_press") - else - self:UpdateMenuAttachment() - - -- Cough handpose gets in the way for close menus - Player:SetCoughHandEnabled(false) - - -- Handle distant button presses - 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) - - handChangedListener = ListenToPlayerEvent("primary_hand_changed", function() - self:UpdateMenuAttachment() - end) + -- For some reason moving menus slow down css transitions + panel_class_name = Convars:GetBool("debug_menu_floating") and "InstantOpen" or "" + }) - end + self:UpdateMenuAttachment() self.panel:AddCSSClasses("Visible") @@ -238,9 +526,12 @@ function DebugMenu:ShowMenu() Panorama:InitPanel(self.panel, "alyxlib_debug_menu") - self.panel:Delay(function() - debugMenuOpen = true - end, 0.2) + debugMenuOpen = true + + self:SetSize( + Convars:GetInt("debug_menu_width"), + Convars:GetInt("debug_menu_height") + ) self:SendCategoriesToPanel() end @@ -264,9 +555,7 @@ function DebugMenu:CloseMenu() Player:SetCoughHandEnabled(true) - if Player.HMDAvatar then - self:StartListeningForMenuActivation() - else + if not Player.HMDAvatar or IsFakeVREnabled() then SendToConsole("unbind r") end end @@ -292,22 +581,25 @@ function DebugMenu:ClickHoveredButton() end --- ----Get a debug menu item by id. +---Gets a debug menu item by id. --- ---@param id string # The item ID +---@param categoryId? string # Optionally specify a category to look in. If not specified, will look in all categories ---@return DebugMenuItem? # The item if it exists -function DebugMenu:GetItem(id) +function DebugMenu:GetItem(id, categoryId) for _, category in ipairs(self.categories) do - for _, item in ipairs(category.items) do - if item.id == id then - return item + if not categoryId or category.id == categoryId then + for _, item in ipairs(category.items) do + if item.id == id then + return item + end end end end end --- ----Get a debug menu category by id. +---Gets a debug menu category by id. --- ---@param id string # The category ID ---@return DebugMenuCategory? # The category if it exists @@ -321,7 +613,7 @@ function DebugMenu:GetCategory(id) end --- ----Add a category to the debug menu. +---Adds a category to the debug menu. --- ---@param id string # The unique ID for this category ---@param name string # The display name for this category @@ -339,7 +631,24 @@ function DebugMenu:AddCategory(id, name) end --- ----Add a separator line to a category. +---Removes a category from the debug menu. +--- +---This does not update the menu if it's already open; +---use [DebugMenu:Refresh()](lua://DebugMenu.Refresh) to update manually. +--- +---@param id string # The ID of the category to remove +function DebugMenu:RemoveCategory(id) + local category, index = self:GetCategory(id) + if not category then + warn("Cannot remove category '"..id.."': Category does not exist!") + return + end + + table.remove(self.categories, index) +end + +--- +---Adds a separator line to a category. --- ---@param categoryId string # The category ID to add the separator to ---@param separatorId? string # Optional ID for the separator if you want to modify it later @@ -360,12 +669,12 @@ function DebugMenu:AddSeparator(categoryId, separatorId, text) end --- ----Add a button to a category. +---Adds 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 +---@param command string|fun(button:DebugMenuItem) # 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 @@ -392,28 +701,25 @@ function DebugMenu:AddButton(categoryId, buttonId, text, command) end --- ----Add a toggle to a category. +---Adds 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 convar? string # The console variable tied to this toggle +---@param callback? fun(on:boolean,toggle:DebugMenuItem) # Function to run when this toggle is toggled ---@param startsOn? boolean|fun():boolean # Whether the toggle is on by default -function DebugMenu:AddToggle(categoryId, toggleId, text, command, startsOn) +function DebugMenu:AddToggle(categoryId, toggleId, text, convar, callback, 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 - startsOn = startsOn or Convars:GetBool(command) or false + if callback == nil and not empty(convar) then callback = function(on) - SendToConsole(command .. " " .. (on and 1 or 0)) + SendToConsole(convar .. " " .. (on and 1 or 0)) end - elseif type(command) == "function" then - callback = command end table.insert(category.items, { @@ -422,12 +728,13 @@ function DebugMenu:AddToggle(categoryId, toggleId, text, command, startsOn) text = text, callback = callback, type = "toggle", - default = startsOn or false, + default = startsOn, + convar = convar }) end --- ----Add a center aligned label to a category. +---Adds 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 @@ -448,40 +755,32 @@ function DebugMenu:AddLabel(categoryId, labelId, text) end --- ----Add value slider to a category. +---Adds 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 convar string # The console variable to tie this slider to ---@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 callback? fun(value:number,slider:DebugMenuItem) # Callback function ---@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) +function DebugMenu:AddSlider(categoryId, sliderId, text, convar, min, max, isPercentage, truncate, increment, callback, 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 - + if callback == nil and type(convar) == "string" and convar ~= "" then ---@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, { @@ -495,22 +794,24 @@ function DebugMenu:AddSlider(categoryId, sliderId, text, min, max, isPercentage, max = max, convar = convar, isPercentage = isPercentage or false, - truncate = truncate or -1, + truncate = truncate or 2, increment = increment or 0 }) end --- ----Add a value cycler to a category. +---Adds 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 title string|nil # The text to display next to each value +---@param convar? string # The console variable tied to this cycle +---@param values {text:string,value:any}[]|string[] # List of text/value pairs for this cycle, or a list of values +---@param callback? fun(index:number, item:{text:string,value:any?}, cycle:DebugMenuItem) # Function callback ---@param defaultValue? any|fun():any # Value for this cycle to start with -function DebugMenu:AddCycle(categoryId, cycleId, values, command, defaultValue) +function DebugMenu:AddCycle(categoryId, cycleId, title, convar, values, callback, defaultValue) local category = self:GetCategory(categoryId) if not category then warn("Cannot add toggle '"..cycleId.."': Category '"..categoryId.."' does not exist!") @@ -521,41 +822,52 @@ function DebugMenu:AddCycle(categoryId, cycleId, values, command, defaultValue) 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 + ---@type {text:string,value:any} + local parsedValues = {} - local callback - local convar = "" - if type(command) == "string" then - if command == "" then - error("Command must not be a blank string", 2) + if type(values[1]) == "string" then + for k,v in ipairs(values) do + table.insert(parsedValues, {text = v, value = k - 1}) end - convar = command + else + for k,v in ipairs(values) do + table.insert(parsedValues, {text = v.text, value = v.value or (k - 1)}) + end + end + if callback == nil and not empty(convar) then ---@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, + text = title, + convar = convar, + callback = callback, + values = parsedValues, default = defaultValue, - convar = convar }) end --- ----Set the text of an item. +---Shows a dialog box with the specified text and a "CLOSE" button. +--- +---@param text string # The text to display in the dialog +function DebugMenu:ShowDialog(text) + if self.panel then + Panorama:Send(self.panel, "ShowDialog", text) + end +end + +--- +---Sets the text of an item. --- ---Only works on the following types: --- - button @@ -580,13 +892,13 @@ function DebugMenu:SetItemText(categoryId, itemId, text) end --- ----Sets the index of a category in the debug menu. +---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 string # Id of the category to change. ----@param index number # New index for the category. +---@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 @@ -594,23 +906,58 @@ function DebugMenu:SetCategoryIndex(categoryId, index) return end - index = math.max(1, math.min(index, #self.categories)) -- Clamp index to valid range + -- Account for 'settings' as invisible first tab + index = index + 1 + index = Clamp(index, 2, #self.categories) table.remove(self.categories, currentIndex) table.insert(self.categories, index, category) if self.panel then - Panorama:Send(self.panel, "SetCategoryIndex", categoryId, index-1) + Panorama:Send(self.panel, "SetCategoryIndex", categoryId, index - 2) + end +end + +--- +---Sets the size of the debug menu panel if it's open. +--- +---This does not change any convars and is not persisted. +--- +---@param width? number # Width in "panel units" +---@param height? number # Height in "panel units" +function DebugMenu:SetSize(width, height) + if width then + width = Clamp(width, MIN_WIDTH, MAX_WIDTH) * DPI + end + + if height then + height = Clamp(height, MIN_HEIGHT, MAX_HEIGHT) * DPI + end + + if self.panel then + Panorama:Send(self.panel, "SetSize", width, height) end end ---Resolves the default value of an element by running any value getter functions. ----@param default any|fun():any # The default value to resolve +---@param item DebugMenuItem # The item to resolve +---@param tFunc? `Convars.GetStr`|`Convars.GetInt`|`Convars.GetFloat`|`Convars.GetBool` # The value getter function +---@param default? any # The default value ---@return any # The resolved value -local function resolveDefault(default) - if type(default) == "function" then - return default() +local function resolveDefault(item, tFunc, default) + if type(item.default) == "function" then + return item.default() + end + + if item.default ~= nil then + return item.default end + + if item.convar and item.convar ~= "" then + tFunc = tFunc or Convars.GetStr + return tFunc(Convars, item.convar) + end + return default end @@ -630,44 +977,51 @@ function DebugMenu:SendCategoryToPanel(category) 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, resolveDefault(item.default)) - elseif item.type == "button" then - Panorama:Send(panel, "AddButton", item.categoryId, item.id, item.text) + local conditionMet = true + if item.condition then + if type(item.condition) == "string" then + conditionMet = Convars:GetBool(item.condition) + elseif type(item.condition) == "function" then + conditionMet = item.condition() + end + end - elseif item.type == "label" then - Panorama:Send(panel, "AddLabel", item.categoryId, item.id, item.text) + if conditionMet then + if 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, item.id, item.text) + elseif item.type == "toggle" then + Panorama:Send(panel, "AddToggle", item.categoryId, item.id, item.text, resolveDefault(item, Convars.GetBool, false)) - 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 + elseif item.type == "label" then + Panorama:Send(panel, "AddLabel", item.categoryId, item.id, item.text) - 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 + elseif item.type == "separator" then + Panorama:Send(panel, "AddSeparator", item.categoryId, item.id, item.text) - Panorama:Send(panel, "AddCycle", item.categoryId, item.id, item.convar, default, values) - else - warn("Unknown item type '"..item.type.."'") + elseif item.type == "slider" then + local default = resolveDefault(item, Convars.GetFloat, item.min) + Panorama:Send(panel, "AddSlider", item.categoryId, item.id, item.text or item.convar, item.convar, item.min, item.max, default, item.isPercentage, item.truncate, item.increment) + + elseif item.type == "cycle" then + + local default = resolveDefault(item) + + if default ~= nil then + -- find the index of the default value + local index = TableFindKey(item.values, function(v) return tostring(v.value) == tostring(default) end) + if index > 0 then + default = index + else + default = nil + end + end + + Panorama:Send(panel, "AddCycle", item.categoryId, item.id, item.convar, item.text, default, TablePluck(item.values, "text")) + else + warn("Unknown debug menu item type '"..item.type.."'") + end end end end @@ -702,12 +1056,92 @@ end ---Forces the debug menu panel to refresh by removing and re-adding all categories and items. --- function DebugMenu:Refresh() - if self.panel then + if self:IsOpen() then self:ClearMenu() self:SendCategoriesToPanel() end end +--- +---Sets the visibility condition for an item. +--- +---If the condition is not met when the menu opens, the item will not appear in the menu. +--- +---@param categoryId string # The category ID +---@param itemId string # The item ID +---@param condition string|fun():boolean|nil # Convar name, function, or `nil` to remove the condition +---@overload fun(self: DebugMenu, item: DebugMenuItem, condition: string|fun():boolean|nil) +function DebugMenu:SetItemVisibilityCondition(categoryId, itemId, condition) + ---@type DebugMenuItem + local item + if type(categoryId) == "table" then + item = categoryId + condition = itemId + else + item = self:GetItem(itemId, categoryId) + end + + if not item then + warn("Cannot set item visibility condition '"..itemId.."': Item does not exist!") + return + end + + item.condition = condition +end + +--- +---A category of convars dumped by `DebugMenu:GetLinkedConvars`. +--- +---@class DebugMenuDumpedCategory +---@field category DebugMenuCategory # The category that these convars are linked to +---@field maxLengthConvar number # The length of the longest convar name +---@field maxLengthValue number # The length of the longest convar value +---@field convars {name:string, value:string?, default:string?}[] # The convars in this category + +--- +---Gets a list of all convars linked to debug menu items. +--- +---@return DebugMenuDumpedCategory[] # A list of all convars +function DebugMenu:GetLinkedConvars() + ---@type DebugMenuDumpedCategory[] + local categories = {} + + for _,category in pairs(DebugMenu.categories) do + local convars = {} + + ---@type DebugMenuDumpedCategory + local dumpedCategory = {category = category, maxLengthConvar = 0, maxLengthValue = 0, convars = convars} + table.insert(categories, dumpedCategory) + + for _,item in pairs(category.items) do + local convar = item.convar + if convar ~= nil then + dumpedCategory.maxLengthConvar = math.max(dumpedCategory.maxLengthConvar, #convar) + if convar and convar ~= "" then + + local default = nil + local val = nil + + if EasyConvars:Exists(convar) then + default = tostring(EasyConvars:GetConvarData(convar).defaultValue) + val = EasyConvars:GetStr(convar) + else + val = Convars:GetStr(convar) + end + + if val ~= nil then + dumpedCategory.maxLengthValue = math.max(dumpedCategory.maxLengthValue, #val) + end + + table.insert(convars, {name = convar, value = val, default = default}) + end + end + end + end + + return categories +end + --- ---Starts listening for the debug menu activation button. --- @@ -726,19 +1160,20 @@ function DebugMenu:StartListeningForMenuActivation() timeSinceLastButtonPress = math.huge end - local hand = Convars:GetBool("alyxlib_debug_menu_hand") and Player.SecondaryHand or Player.PrimaryHand - - if Player:IsDigitalActionOnForHand(hand.Literal, DIGITAL_INPUT_TOGGLE_MENU) then + -- Menu button is always on secondary hand (seems to be) + 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 + if self:IsOpen() then + self:CloseMenu() + else + self:ShowMenu() + end end end else @@ -754,9 +1189,10 @@ function DebugMenu:StopListeningForMenuActivation() Player:SetContextThink("debug_menu_activate", nil, 0) end -if Convars:GetInt("developer") > 0 then - local listenFunc = ListenToPlayerEvent or ListenToGameEvent - listenFunc("vr_player_ready", function() +-- Developer condition has been "disabled" +-- All players can now open the menu +if Convars:GetInt("developer") > -1 then + ListenToPlayerEvent("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 @@ -770,83 +1206,15 @@ if Convars:GetInt("developer") > 0 then end --[[ - Default AlyxLib tab + Settings tab ]] ----@TODO Move to its own file - -local categoryId = "alyxlib" - -DebugMenu:AddCategory(categoryId, "AlyxLib") - -DebugMenu:AddSeparator(categoryId, nil, "Basic") - -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: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(categoryId, "giveammo", "Give 999 Ammo", function() - SendToConsole("hlvr_setresources 999 999 999 " .. Player:GetResin()) -end) - -DebugMenu:AddButton(categoryId, "giveresin", "Give 999 Resin", function() - SendToConsole("hlvr_addresources 0 0 0 " .. (999 - Player:GetResin())) -end) - -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") - currentDemo = "" - isRecordingDemo = false - DebugMenu:SetItemText(categoryId, "demo_recording", "Start Recording Demo") - else - 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) - DebugMenu:SetItemText(categoryId, "demo_recording_label", "Last Demo: " .. currentDemo .. ".dem") - isRecordingDemo = true - DebugMenu:SetItemText(categoryId, "demo_recording", "Stop Recording Demo") - end -end) - -DebugMenu:AddSeparator(categoryId) - -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) +DebugMenu:AddCategory("settings", "") +DebugMenu:AddToggle("settings", "menu_hand", "Menu on primary hand", "debug_menu_hand") +DebugMenu:AddToggle("settings", "menu_floating", "Menu floating", "debug_menu_floating") +DebugMenu:AddToggle("settings", "menu_lock", "Menu locked", "debug_menu_lock") +DebugMenu:AddSlider("settings", "menu_width", "Menu width", "debug_menu_width", 10, 30, false, 0) +DebugMenu:AddSlider("settings", "menu_height", "Menu height", "debug_menu_height", 7, 30, false, 0) +DebugMenu:AddLabel("settings", "", "Print all current menu settings to the console for saving") +DebugMenu:AddButton("settings", "print_settings", "Print Settings To Console", "debug_menu_generate_cfg") return DebugMenu.version \ No newline at end of file diff --git a/scripts/vscripts/alyxlib/debug/debug_menu_cfg.lua b/scripts/vscripts/alyxlib/debug/debug_menu_cfg.lua new file mode 100644 index 0000000..8f1770f --- /dev/null +++ b/scripts/vscripts/alyxlib/debug/debug_menu_cfg.lua @@ -0,0 +1,247 @@ +--[[ + v1.0.0 + https://github.com/FrostSource/alyxlib + + Allows debug menu additions with a 'debug_menu.cfg' file or through the console. +]] + +local version = "v1.0.0" + +local currentUserCategory = "" + +RegisterAlyxLibCommand("debug_menu_add_category", function (_, name) + currentUserCategory = DoUniqueString("category") + DebugMenu:AddCategory(currentUserCategory, name or "New Category") + DebugMenu:Refresh() +end, "Adds a new category to the debug menu") + +RegisterAlyxLibCommand("debug_menu_add_button", function (_, text, command) + if currentUserCategory == nil or currentUserCategory == "" then + return warn("debug_menu_add_button: No category defined with debug_menu_add_category") + end + DebugMenu:AddButton(currentUserCategory, DoUniqueString("button"), text or "Button", command or "echo Button pressed") + DebugMenu:Refresh() +end, "Adds a new button to the current category") + +RegisterAlyxLibCommand("debug_menu_add_toggle", function (_, text, commandOff, commandOn, default) + if currentUserCategory == nil or currentUserCategory == "" then + return warn("debug_menu_add_toggle: No category defined with debug_menu_add_category") + end + + if default == nil then + if commandOn ~= nil and vlua.find({"off","on","0","1"}, string.lower(commandOn)) then + default = string.lower(commandOn) + commandOn = nil + end + else + default = string.lower(default) + end + + default = (default == "on" or default == "1") + + local callback + local convar + + if string.find(commandOff, "%s") then + -- commandOff is a convar with a value + callback = function(on) + SendToConsole(commandOff) + end + convar = string.sub(commandOff, 1, string.find(commandOff, "%s")-1) + else + -- commandOff is just a convar + convar = commandOff + end + + if commandOn ~= nil then + callback = function(isOn) + if isOn then + SendToConsole(commandOn) + else + SendToConsole(commandOff) + end + end + end + + DebugMenu:AddToggle(currentUserCategory, DoUniqueString("toggle"), text or "Toggle", convar, callback, default) + DebugMenu:Refresh() +end, "Adds a new toggle to the current category") + +RegisterAlyxLibCommand("debug_menu_add_slider", function (_, text, command, min, max, ...) + if currentUserCategory == nil or currentUserCategory == "" then + return warn("debug_menu_add_slider: No category defined with debug_menu_add_category") + end + + local isPercentage = false + local truncate = nil + local increment = nil + local default = nil + + for i = 1, select("#", ...) do + local arg = select(i, ...) + if arg == "%" then isPercentage = true + else + local prefix = arg:sub(1,1) + local value = tonumber(arg:sub(2)) + if prefix == "." then truncate = value + elseif prefix == "+" then increment = value + elseif prefix == "@" then default = value + end + end + end + + min = tonumber(min) assert(type(min) == "number", "debug_menu_add_slider: min must be a number") + max = tonumber(max) assert(type(max) == "number", "debug_menu_add_slider: max must be a number") + assert(type(truncate) == "nil" or type(truncate) == "number", "debug_menu_add_slider: truncate must be a number") + assert(type(truncate) == "nil" or type(increment) == "number", "debug_menu_add_slider: increment must be a number") + + DebugMenu:AddSlider(currentUserCategory, DoUniqueString("slider"), text or "Slider", command, min or 0, max or 1, isPercentage or false, truncate, increment, nil, default) + DebugMenu:Refresh() +end, "Adds a new slider to the current category") + +RegisterAlyxLibCommand("debug_menu_add_cycle", function (_, title, ...) + if currentUserCategory == nil or currentUserCategory == "" then + return warn("debug_menu_add_cycle: No category defined with debug_menu_add_category") + end + + local rawValues = {...} + local parsedValues = {} + + if title == nil then + return warn("debug_menu_add_cycle: No title specified") + end + + -- parse each text:cmd pair + local i = 0 + for _, pair in pairs(rawValues) do + i = i + 1 + local text,value = pair:match("([^:]+):(.+)") + if text == nil or value == nil then + break + end + table.insert(parsedValues, {text = text, value = value}) + end + + if #parsedValues == 0 then + return warn("debug_menu_add_cycle: No values specified") + end + + -- parse any leftover default + local default = tonumber(rawValues[i]) + if default ~= nil and parsedValues[default] ~= nil then + default = parsedValues[default].value + end + + DebugMenu:AddCycle(currentUserCategory, DoUniqueString("cycle"), title or "Cycle", nil, parsedValues, function (index, item, cycle) + SendToConsole(item.value) + end, default) + DebugMenu:Refresh() +end, "Adds a new cycle to the current category") + +---@type string[] +local cycleMapCmds = {} + +RegisterAlyxLibCommand("debug_menu_add_cycle_cmd", function(_, ...) + for _, cmd in ipairs({...}) do + table.insert(cycleMapCmds, cmd) + end +end, "Defines a command for a cycle to be added with debug_menu_add_cycle_map") + +RegisterAlyxLibCommand("debug_menu_add_cycle_map", function(_, title, ...) + if currentUserCategory == nil or currentUserCategory == "" then + return warn("debug_menu_add_cycle_map: No category defined with debug_menu_add_category") + end + + if #cycleMapCmds == 0 then + return warn("debug_menu_add_cycle_map: No commands defined with debug_menu_add_cycle_cmd") + end + + local _cycleMapCmds = cycleMapCmds + cycleMapCmds = {} + + ---@type string[] + local rawValues = {...} + local parsedValues = {} + + if title == nil then + return warn("debug_menu_add_cycle: No title specified") + end + + -- parse each text:cmd pair + local i = 0 + for _, pair in pairs(rawValues) do + i = i + 1 + local text,value = pair:match("([^:]+):(.+)") + if text == nil or value == nil then + break + end + ---@cast text string + ---@cast value string + + -- single cmd cycles use convar + if #_cycleMapCmds == 1 then + table.insert(parsedValues, {text = text, value = value}) + else + local cmdValues = value:split() + if #cmdValues ~= #_cycleMapCmds then + return warn("debug_menu_add_cycle_map: Number of values must match number of commands") + end + + -- build full command string from values + local cmdStr = "" + for j = 1, #_cycleMapCmds do + cmdStr = cmdStr .. _cycleMapCmds[j] .. " " .. cmdValues[j] + if j < #_cycleMapCmds then + cmdStr = cmdStr .. "; " + end + end + + table.insert(parsedValues, {text = text, value = cmdStr}) + end + end + + if #parsedValues == 0 then + return warn("debug_menu_add_cycle: No values specified") + end + + -- parse any leftover default + local default = tonumber(rawValues[i]) + if default ~= nil and parsedValues[default] ~= nil then + default = parsedValues[default].value + end + + if #_cycleMapCmds == 1 then + DebugMenu:AddCycle(currentUserCategory, DoUniqueString("cycle"), title or "Cycle", _cycleMapCmds[1], parsedValues, nil, default) + else + DebugMenu:AddCycle(currentUserCategory, DoUniqueString("cycle"), title or "Cycle", nil, parsedValues, function (index, item, cycle) + SendToConsole(item.value) + end, default) + end + + DebugMenu:Refresh() +end, "Adds a new cycle to the current category using previously defined commands") + +RegisterAlyxLibCommand("debug_menu_add_label", function (_, text) + if currentUserCategory == nil or currentUserCategory == "" then + return warn("debug_menu_add_label: No category defined with debug_menu_add_category") + end + + DebugMenu:AddLabel(currentUserCategory, DoUniqueString("label"), text) + DebugMenu:Refresh() +end, "Adds a new label to the current category") + +RegisterAlyxLibCommand("debug_menu_add_separator", function (_, text) + if currentUserCategory == nil or currentUserCategory == "" then + return warn("debug_menu_add_separator: No category defined with debug_menu_add_category") + end + + DebugMenu:AddSeparator(currentUserCategory, nil, text) + DebugMenu:Refresh() +end, "Adds a new separator to the current category") + +---Execute the debug_menu.cfg file +ListenToPlayerEvent("player_activate", function() + SendToConsole("exec debug_menu") +end) + +return version \ 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 deleted file mode 100644 index e4b9dac..0000000 --- a/scripts/vscripts/alyxlib/debug/debug_menu_extras.lua +++ /dev/null @@ -1,38 +0,0 @@ ---[[ - 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 categoryId = "alyxlib_extras" - -DebugMenu:AddCategory(categoryId, "AlyxLib Extras") - -DebugMenu:AddSeparator(categoryId, nil, "Display") - -DebugMenu:AddToggle(categoryId, "showtriggers", "Show Triggers", "showtriggers") - -DebugMenu:AddToggle(categoryId, "luxels", "Mat Luxels", "mat_luxels") - -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() - 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 diff --git a/scripts/vscripts/alyxlib/debug/menus/debug_menu_alyxlib.lua b/scripts/vscripts/alyxlib/debug/menus/debug_menu_alyxlib.lua new file mode 100644 index 0000000..713d521 --- /dev/null +++ b/scripts/vscripts/alyxlib/debug/menus/debug_menu_alyxlib.lua @@ -0,0 +1,88 @@ +--[[ + v1.0.0 + https://github.com/FrostSource/alyxlib + + Default AlyxLib tab. +]] + +local version = "v1.0.0" + +local cheats = require("alyxlib.debug.menus.debug_menu_cheats") + +RegisterAlyxLibConvar("debug_menu_cheats", "0", "Enables the cheats tab", 0, function() + if Convars:GetBool("debug_menu_cheats") then + cheats.createTab() + else + cheats.removeTab() + end +end) + +local extras = require("alyxlib.debug.menus.debug_menu_extras") + +RegisterAlyxLibConvar("debug_menu_extras", "0", "Enables the extras tab", 0, function() + if Convars:GetBool("debug_menu_extras") then + extras.createTab() + else + extras.removeTab() + end +end) + +-- Add extras and cheats to settings page +if DebugMenu:GetCategory("settings") then + DebugMenu:AddToggle("settings", "menu_cheats", "Cheats tab", "debug_menu_cheats") + DebugMenu:AddToggle("settings", "menu_extras", "Developer tab", "debug_menu_extras") +else + warn("Cannot add extras and cheats to settings tab: Settings tab does not exist!") +end + + +local categoryId = "alyxlib" + +DebugMenu:AddCategory(categoryId, "AlyxLib") + +DebugMenu:AddSeparator(categoryId, nil, "Basic") + +if IsVREnabled() then + DebugMenu:AddToggle(categoryId, "noclip_vr", "NoClip VR", "noclip_vr", nil, 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", "noclip_vr_speed", 0.5, 10, false, 2) + DebugMenu:AddSlider(categoryId, "noclip_vr_boost_speed", "NoClip VR Boost Speed", "noclip_vr_boost_speed", 0.5, 10, false, 2) +end + +DebugMenu:AddToggle(categoryId, "gameinstructor", "Game Instructor Hints", nil, +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, "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") + currentDemo = "" + isRecordingDemo = false + DebugMenu:SetItemText(categoryId, "demo_recording", "Start Recording Demo") + else + 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) + DebugMenu:SetItemText(categoryId, "demo_recording_label", "Last Demo: " .. currentDemo .. ".dem") + isRecordingDemo = true + DebugMenu:SetItemText(categoryId, "demo_recording", "Stop Recording Demo") + end +end) + +return version \ No newline at end of file diff --git a/scripts/vscripts/alyxlib/debug/menus/debug_menu_cheats.lua b/scripts/vscripts/alyxlib/debug/menus/debug_menu_cheats.lua new file mode 100644 index 0000000..95390d2 --- /dev/null +++ b/scripts/vscripts/alyxlib/debug/menus/debug_menu_cheats.lua @@ -0,0 +1,72 @@ +--[[ + v1.0.0 + https://github.com/FrostSource/alyxlib + + Cheats debug menu category for players. +]] + +local version = "v1.0.0" + +-- Simple require can cause loop with debug_menu.lua +if package.loaded["alyxlib.debug.debug_menu"] == nil then + require "alyxlib.debug.debug_menu" +end + +local categoryId = "alyxlib_cheats" + +--- +---Creates the AlyxLib Extras tab in the debug menu. +--- +local function createTab() + if DebugMenu:GetCategory(categoryId) then + return + end + + DebugMenu:AddCategory(categoryId, "Cheats") + + DebugMenu:AddToggle(categoryId, "buddha", "Buddha Mode", "buddha") + + DebugMenu:AddToggle(categoryId, "notarget", "No Target", "notarget") + + DebugMenu:AddButton(categoryId, "heal", "Heal", function() + Entities:GetLocalPlayer():SetHealth(100) + end) + + DebugMenu:AddSeparator(categoryId, nil, "Equipment") + + if IsVREnabled() or IsFakeVREnabled() then + DebugMenu:AddButton(categoryId, "givegrabbity", "Give Grabbity Gloves", "hlvr_give_grabbity_gloves") + end + + DebugMenu:AddToggle(categoryId, "infiniteammo", "Infinite Ammo", "sv_infinite_ammo") + + DebugMenu:AddButton(categoryId, "giveammo", "Give 999 Ammo", function() + SendToConsole("hlvr_setresources 999 999 999 " .. Player:GetResin()) + end) + + DebugMenu:AddButton(categoryId, "giveresin", "Give 999 Resin", function() + SendToConsole("hlvr_addresources 0 0 0 " .. (999 - Player:GetResin())) + end) + + DebugMenu:SendCategoryToPanel(DebugMenu:GetCategory(categoryId)) + DebugMenu:SetCategoryIndex(categoryId, 2) + +end + +--- +---Removes the AlyxLib Extras tab from the debug menu. +--- +local function removeTab() + if not DebugMenu:GetCategory(categoryId) then + return + end + + DebugMenu:RemoveCategory(categoryId) + DebugMenu:Refresh() +end + +return { + version = version, + createTab = createTab, + removeTab = removeTab +} \ No newline at end of file diff --git a/scripts/vscripts/alyxlib/debug/menus/debug_menu_extras.lua b/scripts/vscripts/alyxlib/debug/menus/debug_menu_extras.lua new file mode 100644 index 0000000..5c1870a --- /dev/null +++ b/scripts/vscripts/alyxlib/debug/menus/debug_menu_extras.lua @@ -0,0 +1,75 @@ +--[[ + v1.0.0 + https://github.com/FrostSource/alyxlib + + Extra debug menu category for developers. +]] + +local version = "v1.0.0" + +-- Simple require can cause loop with debug_menu.lua +if package.loaded["alyxlib.debug.debug_menu"] == nil then + require "alyxlib.debug.debug_menu" +end + +local categoryId = "alyxlib_extras" + +--- +---Creates the AlyxLib Extras tab in the debug menu. +--- +local function createTab() + if DebugMenu:GetCategory(categoryId) then + return + end + + DebugMenu:AddCategory(categoryId, "Developer") + + DebugMenu:AddSeparator(categoryId, nil, "Display") + + -- Using a callback avoids convar warning in console + DebugMenu:AddToggle(categoryId, "showtriggers", "Show Triggers", nil, function(on) SendToConsole("showtriggers " .. (on and 1 or 0)) end) + + DebugMenu:AddToggle(categoryId, "luxels", "Mat Luxels", "mat_luxels") + + DebugMenu:AddToggle(categoryId, "fullbright", "Fullbright", "mat_fullbright") + + DebugMenu:AddToggle(categoryId, "visibility", "Vis", "vis_enable") + + DebugMenu:AddToggle(categoryId, "cubemapcolors", "Cubemap Colors", "r_cubemap_debug_colors") + + DebugMenu:AddToggle(categoryId, categoryId.."_visfreeze", "Freeze Vis", nil, 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) + + DebugMenu:AddToggle(categoryId, "lefthanded", "Left Handed", "hlvr_left_hand_primary") + + DebugMenu:SendCategoryToPanel(DebugMenu:GetCategory(categoryId)) + DebugMenu:SetCategoryIndex(categoryId, 3) + +end + +--- +---Removes the AlyxLib Extras tab from the debug menu. +--- +local function removeTab() + if not DebugMenu:GetCategory(categoryId) then + return + end + + DebugMenu:RemoveCategory(categoryId) + DebugMenu:Refresh() +end + +return { + version = version, + createTab = createTab, + removeTab = removeTab +} \ No newline at end of file diff --git a/scripts/vscripts/alyxlib/debug/novr.lua b/scripts/vscripts/alyxlib/debug/novr.lua index 480d82a..6bdcbc4 100644 --- a/scripts/vscripts/alyxlib/debug/novr.lua +++ b/scripts/vscripts/alyxlib/debug/novr.lua @@ -1,16 +1,19 @@ --[[ - v1.1.0 + v1.2.0 https://github.com/FrostSource/alyxlib Adds functions and console commands to help debugging outside VR mode. - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: require "alyxlib.debug.novr" ]] -local version = "v1.1.0" +local version = "v1.2.0" +--- +---Provides functions to help debugging outside VR mode. +--- ---@class NoVR NoVR = {} @@ -19,6 +22,9 @@ NoVR.AutoStartInToolsMode = false --#region Interactions +--- +---NoVR interaction data. +--- ---@class NoVrInteractClass ---@field class string ---@field hold? boolean @@ -77,7 +83,7 @@ local interactClasses = } --- ----Add an interaction class for the NoVR player to interact with. +---Adds an interaction class for the NoVR player to interact with. --- ---@param title string # Text to show in-game on the entity ---@param class string # Class to interact with @@ -100,7 +106,7 @@ local usePressTime = 0 local HOLD_TIME = 0.8 local DISTANCE_WEIGHT = 1 -local LOS_WIGHT = 1.3 +local LOS_WEIGHT = 1.3 ---Get text that should show for an entity. ---@param data NoVrInteractClass @@ -155,32 +161,40 @@ local function activateEntity(entity, data) debugoverlay:Text(getTextPosition(entity, data), 0, getText(data), 0, 50, 255, 50, 255, 2) end +-- index is different for fakevr +-- 5 for novr, 35 for fakevr +local fakeUseButton = IsFakeVREnabled() and 35 or 5 + local function interactThink() - ---@type EntityHandle - local bestEnt = nil ---@type number local bestScore = 0 + ---@type EntityHandle? + local bestEnt = nil + ---@type NoVrInteractClass - local data = nil + local useData = nil + for _, interactData in ipairs(interactClasses) do - -- local nearestEnt = Entities:FindByClassnameNearest(interactData.class, Player:EyePosition(), INTERACT_DISTANCE) local nearestEnts = Entities:FindAllByClassnameWithin(interactData.class, Player:EyePosition(), INTERACT_DISTANCE) for _, nearestEnt in ipairs(nearestEnts) do if nearestEnt then - local dist = VectorDistance(nearestEnt:GetOrigin(), Player:EyePosition()) - local normalizedDist = 1 - (math.min(dist / INTERACT_DISTANCE, 1)) - local dot = Player:EyeAngles():Forward():Dot((nearestEnt:GetAbsOrigin() - Player:EyePosition()):Normalized()) - local score = (normalizedDist * DISTANCE_WEIGHT) + (dot * LOS_WIGHT) + (interactData.weight or 1) - -- debugoverlay:Text(nearestEnt:GetAbsOrigin(), 1, tostring(score) .. " " .. tostring(dot), 0, 255, 0, 0, 255, 0.1) - if score > bestScore then - bestEnt = nearestEnt - bestScore = score - data = interactData + local owner = nearestEnt:GetMoveParent() and nearestEnt:GetMoveParent():GetOwner() + if nearestEnt:GetName() ~= "Conform Helper" and (not owner or (owner:GetClassname() ~= "info_hlvr_equip_player" and owner:GetClassname() ~= "player")) then + local dist = VectorDistance(nearestEnt:GetOrigin(), Player:EyePosition()) + local normalizedDist = 1 - (math.min(dist / INTERACT_DISTANCE, 1)) + local dot = Player:EyeAngles():Forward():Dot((nearestEnt:GetAbsOrigin() - Player:EyePosition()):Normalized()) + local score = (normalizedDist * DISTANCE_WEIGHT) + (dot * LOS_WEIGHT) + (interactData.weight or 1) + -- debugoverlay:Text(nearestEnt:GetAbsOrigin(), 1, tostring(score) .. " " .. tostring(dot), 0, 255, 0, 0, 255, 0.1) + if score > bestScore then + bestEnt = nearestEnt + bestScore = score + useData = interactData + end end end @@ -189,24 +203,23 @@ local function interactThink() end if bestEnt then - local text = getText(data) - local pos = getTextPosition(bestEnt, data) + local text = getText(useData) + local pos = getTextPosition(bestEnt, useData) debugoverlay:Text(pos, 0, text, 0, 255, 255, 255, 255, 0.1) - - if Player:IsVRControllerButtonPressed(5) then + if Player:IsVRControllerButtonPressed(fakeUseButton) then if not isUsePressed then isUsePressed = true usePressTime = Time() - if not data.hold then - activateEntity(bestEnt, data) + if not useData.hold then + activateEntity(bestEnt, useData) end end if (Time() - usePressTime) > HOLD_TIME then - activateEntity(bestEnt, data) + activateEntity(bestEnt, useData) usePressTime = math.huge end else @@ -249,13 +262,15 @@ function NoVR:DisableAllDebugging() Player:SetContextThink("novrInteractThink", nil, 0) end -ListenToPlayerEvent("novr_player", function (params) - cl_forwardspeed = Convars:GetFloat("cl_forwardspeed") - cl_backspeed = Convars:GetFloat("cl_backspeed") - cl_sidespeed = Convars:GetFloat("cl_sidespeed") +ListenToPlayerEvent("player_activate", function (params) + if not IsVREnabled() then + cl_forwardspeed = Convars:GetFloat("cl_forwardspeed") + cl_backspeed = Convars:GetFloat("cl_backspeed") + cl_sidespeed = Convars:GetFloat("cl_sidespeed") - if NoVR.AutoStartInToolsMode and IsInToolsMode() then - NoVR:EnableAllDebugging() + if NoVR.AutoStartInToolsMode and IsInToolsMode() then + NoVR:EnableAllDebugging() + end end end) @@ -408,7 +423,7 @@ end, "", 0) local novrBindings = {} --- ----Unbind all keys bound by [NoVR:BindKey](lua://NoVR.BindKey) +---Unbinds all keys bound by [NoVR:BindKey](lua://NoVR.BindKey) --- function NoVR:UnbindKeys() -- for _, binding in ipairs(novrBindings) do @@ -420,9 +435,9 @@ function NoVR:UnbindKeys() end --- ----Bind a keyboard key to a callback function. +---Binds a keyboard key to a callback function. --- ----@param key KeyboardKey +---@param key KeyboardKey # Key to bind ---@param callback fun()|string # Callback function or command string ---@param name? string # Optional name for the callback command function NoVR:BindKey(key, callback, name) @@ -448,4 +463,53 @@ ListenToGameEvent("server_shutdown", function() end end, nil) +Convars:RegisterCommand("alyxlib_start_print_novr_button_presses", function (_) + local player = Entities:GetLocalPlayer() + if not player then + warn("Cannot print novr buttons while player does not exist!") + return + end + + local buttonsPressed = {} + + player:SetContextThink("DebugPrintNoVRButtonPresses", function() + + local msgPrinted = false + for i = 0, 100 do + if player:IsVRControllerButtonPressed(i) then + if not buttonsPressed[i] then + buttonsPressed[i] = true + Msg("NoVR Button: ".. i .."" .. " Pressed\n") + msgPrinted = true + end + else + if buttonsPressed[i] ~= nil then + buttonsPressed[i] = nil + Msg("NoVR Button: ".. i .."" .. " Released\n") + msgPrinted = true + end + end + end + + if msgPrinted then + Msg("\n") + end + + return 0 + end, 0) + + Msg("Started printing NoVR button presses. Use alyxlib_stop_print_novr_button_presses to stop...\n") +end, "", 0) + +Convars:RegisterCommand("alyxlib_stop_print_novr_button_presses", function (_) + local player = Entities:GetLocalPlayer() + if not player then + warn("Cannot stop printing novr buttons while player does not exist!") + return + end + + player:SetContextThink("DebugPrintNoVRButtonPresses", nil, 0) + Msg("Stopped printing novr button presses...\n") +end, "", 0) + return version \ No newline at end of file diff --git a/scripts/vscripts/alyxlib/debug/profiling.lua b/scripts/vscripts/alyxlib/debug/profiling.lua index 32f1eba..bb10b17 100644 --- a/scripts/vscripts/alyxlib/debug/profiling.lua +++ b/scripts/vscripts/alyxlib/debug/profiling.lua @@ -1,5 +1,5 @@ --[[ - v1.0.0 + v1.0.1 https://github.com/FrostSource/alyxlib Profiler allows for easy profiling of functions. @@ -23,7 +23,7 @@ local freq = ffi.new("LARGE_INTEGER") kernal32.QueryPerformanceFrequency(freq) --- ----Profiler class +---Profiler class. --- ---@class Profiler local profiler = { @@ -108,23 +108,23 @@ function profiler:Profile(func) end --- ----Get the mean (average) profiled time in seconds. +---Gets the mean (average) profiled time in seconds. --- ----@return number +---@return number # The mean profiled time function profiler:GetMean() return self.totalTime / self.numMeasurements end --- ----Get the median profiled time in seconds. +---Gets the median profiled time in seconds. --- ----@return number +---@return number # The median profiled time function profiler:GetMedian() if self.numMeasurements == 0 then return 0 end - local sortedValues = {unpack(self.measurements)} + local sortedValues = vlua.clone(self.measurements) table.sort(sortedValues) local count = #sortedValues if count % 2 == 1 then @@ -136,14 +136,14 @@ function profiler:GetMedian() end --- ----Calculate the standard deviation of the measurements. +---Calculates the standard deviation of the measurements. --- ----The standard deviation is used to determine how spread out the measurements are. ----The higher the value, the more the measurements deviate from the mean, indicating greater variability. ----A lower value means the measurements are closer to the mean, indicating more consistency. +---The standard deviation is used to determine how spread out the measurements are. +---The higher the value, the more the measurements deviate from the mean, indicating greater variability. +---A lower value means the measurements are closer to the mean, indicating more consistency. ---A result of 0 means there is no variability (either because all measurements are the same, or there are no measurements). --- ----@return number +---@return number # The standard deviation function profiler:GetStandardDeviation() local mean = self:GetMean() if self.useRunningTotal then diff --git a/scripts/vscripts/alyxlib/debug/vr.lua b/scripts/vscripts/alyxlib/debug/vr.lua index 9a5be9d..e7a72ec 100644 --- a/scripts/vscripts/alyxlib/debug/vr.lua +++ b/scripts/vscripts/alyxlib/debug/vr.lua @@ -1,15 +1,15 @@ --[[ - v1.1.0 + v1.2.0 https://github.com/FrostSource/alyxlib Adds console commands to help debugging VR specific features. - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: require "alyxlib.debug.vr" ]] -local version = "v1.1.0" +local version = "v1.2.0" ---Get a hand entity from its name. ---@param handName string @@ -102,30 +102,43 @@ Convars:RegisterCommand("add_hand_attachment", function (_, classname, handName) hand:AddHandAttachment(ent) end, "", 0) ----Hidden convar state that can be retrieved by other scripts -RegisterAlyxLibConvar("noclip_vr_enabled", "0", "True if noclip_vr is enabled (readonly)", FCVAR_HIDDEN) +RegisterAlyxLibCommand("hlvr_give_grabbity_gloves", function (_) + if not Player:HasGrabbityGloves() then + local equip = SpawnEntityFromTableSynchronous("info_hlvr_equip_player", { + grabbitygloves = "1", + itemholder = Player:HasItemHolder() and "1" or "0", + backpack_enabled = Player:GetBackpack() ~= nil and "1" or "0", -- this is not an accurate check + }) + equip:EntFire("EquipNow") + equip:EntFire("Kill", nil, 0.1) + end +end, "Gives the player grabbity gloves", FCVAR_NONE) ---Tracks initial button press to prevent repeated logic execution while held local quickTurnFlag = false +---Used to track user's movement type so it can be reset +local movetype = Convars:GetInt('hlvr_movetype_default') + local function noclipVRThink() -- Check offhand first because it's most common, then check primary hand movement local moveVector = Player:GetAnalogActionPositionForHand(Player.SecondaryHand.Literal, ANALOG_INPUT_TELEPORT_TURN) + local hand = Player.SecondaryHand if #moveVector == 0 then moveVector = Player:GetAnalogActionPositionForHand(Player.PrimaryHand.Literal, ANALOG_INPUT_TELEPORT_TURN) + hand = Player.PrimaryHand end if moveVector:Length() > 0 then - local moveType = Player:GetMoveType() local dir - if moveType == PlayerMoveType.ContinuousHand then - dir = (Player.SecondaryHand:GetAngles():Left() * moveVector.x) + (Player.SecondaryHand:GetAngles():Forward() * moveVector.y) + if movetype == PlayerMoveType.ContinuousHand then + dir = (hand:GetAngles():Left() * moveVector.x) + (hand:GetAngles():Forward() * moveVector.y) else dir = (Player:EyeAngles():Left() * moveVector.x) + (Player:EyeAngles():Forward() * moveVector.y) end - local velocity = dir * (Player:IsDigitalActionOnForHand(Player.SecondaryHand.Literal, 3) and Convars:GetFloat("noclip_vr_boost_speed") or Convars:GetFloat("noclip_vr_speed")) + local velocity = dir * (Player:IsDigitalActionOnForHand(hand.Literal, 3) and Convars:GetFloat("noclip_vr_boost_speed") or Convars:GetFloat("noclip_vr_speed")) Player.HMDAnchor:SetOrigin(Player.HMDAnchor:GetOrigin() + velocity) end @@ -163,9 +176,6 @@ end RegisterAlyxLibConvar("noclip_vr_speed", "2", "Speed of the VR noclip movement", 0) RegisterAlyxLibConvar("noclip_vr_boost_speed", "5", "Speed of the VR noclip movement when holding trigger", 0) ----Used to track user's movement type so it can be reset -local movetype = Convars:GetInt('hlvr_movetype_default') - RegisterAlyxLibCommand("noclip_vr", function (_, on) local noclipVREnabled if on == nil then diff --git a/scripts/vscripts/alyxlib/extensions/entities.lua b/scripts/vscripts/alyxlib/extensions/entities.lua index ce9f738..79fbdc0 100644 --- a/scripts/vscripts/alyxlib/extensions/entities.lua +++ b/scripts/vscripts/alyxlib/extensions/entities.lua @@ -1,21 +1,21 @@ --[[ - v1.8.0 + v1.8.1 https://github.com/FrostSource/alyxlib Extensions for the `Entities` class. - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: require "alyxlib.extensions.entities" ]] require "alyxlib.extensions.entity" -local version = "v1.8.0" +local version = "v1.8.1" --- ----Finds the entity in a given list that is closest to a specified position. ----Uses 0`CalcClosestPointOnEntityOBB` to determine the nearest point on each entity's bounding box. +---Finds the entity in a given list that is closest to a specified position. +---Uses `CalcClosestPointOnEntityOBB` to determine the nearest point on each entity's bounding box. --- --- @param ents EntityHandle[] # A list of entities to check --- @param pos Vector # The position to compare distances against @@ -49,7 +49,7 @@ end --- ---If no name is provided, the function falls back to finding the nearest entity of the given classname. --- ----@param name string # The unique name of the entity (if available, "" if not) +---@param name string|"" # The unique name of the entity (if available, "" if not) ---@param class string # The classname of the entity (fallback if name isn't available) ---@param position Vector # The position to search around ---@param radius? number # The max search radius (default: 128) @@ -76,7 +76,7 @@ end --- ---Gets an array of every entity that currently exists. --- ----@return EntityHandle[] +---@return EntityHandle[] # List of all entities function Entities:All() local ents = {} local e = Entities:First() @@ -90,21 +90,21 @@ end --- ---Gets a random entity in the map. --- ----@return EntityHandle +---@return EntityHandle # A random entity function Entities:Random() local all = Entities:All() return all[RandomInt(1, #all)] end --- ----Find an entity within the same prefab as another entity. +---Finds a named entity within the same name-fixed-up prefab as another entity. --- ----Will have issues in nested prefabs. +---Might have issues in nested prefabs. --- ----@param entity EntityHandle ----@param name string ----@return EntityHandle? # The found entity, nil if not found. ----@return string # Prefab part of the name. +---@param entity EntityHandle # A known entity in the prefab +---@param name string # The name to search for +---@return EntityHandle? # The found entity, or `nil` if not found +---@return string # Prefab part of the name function Entities:FindInPrefab(entity, name) local myname = entity:GetName() for _,ent in ipairs(Entities:FindAllByName('*' .. name)) do @@ -117,26 +117,26 @@ function Entities:FindInPrefab(entity, name) end --- ----Find an entity within the same prefab as this entity. +---Finds a named entity within the same name-fixed-up prefab as this entity. --- ----Will have issues in nested prefabs. +---Might have issues in nested prefabs. --- ----@param name string ----@return EntityHandle? ----@return string # Prefab part of the name. +---@param name string # The name to search for +---@return EntityHandle? # The found entity, or `nil` if not found +---@return string # Prefab part of the name function CEntityInstance:FindInPrefab(name) return Entities:FindInPrefab(self, name) end --- ----Find all entities with a cone. +---Finds all entities with a cone. --- ----@param origin Vector # Origin of the cone in worldspace. ----@param direction Vector # Normalized direction vector. ----@param maxDistance number # Max distance the cone will extend towards `direction`. ----@param maxAngle number # Field-of-view in degrees that the cone can see, [0-180]. ----@param checkEntityBounds boolean # If true the entity bounding box will be tested as well as the origin. ----@return EntityHandle[] # List of entities found within the cone. +---@param origin Vector # Origin of the cone in worldspace +---@param direction Vector # Normalized direction vector +---@param maxDistance number # Max distance the cone will extend towards `direction` +---@param maxAngle number # Field-of-view in degrees that the cone can see, [0-180] +---@param checkEntityBounds boolean # If true the entity bounding box will be tested as well as the origin +---@return EntityHandle[] # List of entities found within the cone function Entities:FindAllInCone(origin, direction, maxDistance, maxAngle, checkEntityBounds) local cosMaxAngle = math.cos(math.rad(maxAngle)) local entitiesInSphere = Entities:FindAllInSphere(origin, maxDistance) @@ -168,12 +168,12 @@ function Entities:FindAllInCone(origin, direction, maxDistance, maxAngle, checkE end --- ----Find all entities within `mins` and `maxs` bounding box. +---Finds all entities within `mins` and `maxs` bounding box. --- ----@param mins Vector # Mins vector in world-space. ----@param maxs Vector # Maxs vector in world-space. ----@param checkEntityBounds? boolean # If true the entity bounding boxes will be used for the check instead of the origin. ----@return EntityHandle[] # List of entities found. +---@param mins Vector # Mins vector in world-space +---@param maxs Vector # Maxs vector in world-space +---@param checkEntityBounds? boolean # `true` if the bounding boxes should be used for the check instead of their origin +---@return EntityHandle[] # List of entities found function Entities:FindAllInBounds(mins, maxs, checkEntityBounds) local center = (mins + maxs) / 2 local maxRadius = (maxs - center):Length() @@ -194,12 +194,12 @@ function Entities:FindAllInBounds(mins, maxs, checkEntityBounds) end --- ----Find all entities within an `origin` centered box. +---Finds all entities within an `origin` centered box. --- ----@param width number # Size of the box on the X axis. ----@param length number # Size of the box on the Y axis. ----@param height number # Size of the box on the Z axis. ----@return EntityHandle[] # List of entities found. +---@param width number # Size of the box on the X axis +---@param length number # Size of the box on the Y axis +---@param height number # Size of the box on the Z axis +---@return EntityHandle[] # List of entities found function Entities:FindAllInBox(origin, width, length, height) return Entities:FindAllInBounds( Vector(origin - width, origin - length, origin - height), @@ -208,21 +208,21 @@ function Entities:FindAllInBox(origin, width, length, height) end --- ----Find all entities within an `origin` centered cube of a given `size.` +---Finds all entities within an `origin` centered cube of a given `size.` --- ----@param origin Vector # World space cube position. ----@param size number # Size of the cube in all directions. ----@return EntityHandle[] # List of entities found. +---@param origin Vector # World space cube position +---@param size number # Size of the cube in all directions +---@return EntityHandle[] # List of entities found function Entities:FindAllInCube(origin, size) return Entities:FindAllInBox(origin, size, size, size) end --- ----Find the nearest entity to a world position. +---Finds the nearest entity to a world position. --- ----@param origin Vector # Position to check from. ----@param maxRadius number # Maximum radius to check from `origin`. ----@return EntityHandle? # The nearest entity found, or nil if none found. +---@param origin Vector # Position to check from +---@param maxRadius number # Maximum radius to check from `origin` +---@return EntityHandle? # The nearest entity found, or nil if none found function Entities:FindNearest(origin, maxRadius) local nearestEnt = nil local nearestDistanceSq = math.huge @@ -242,8 +242,8 @@ end --- ---Finds all entities in the map from a list of classnames. --- ----@param classes string[] ----@return EntityHandle[] +---@param classes string[] # List of classnames +---@return EntityHandle[] # List of entities function Entities:FindAllByClassnameList(classes) local ents = {} for _, class in ipairs(classes) do @@ -255,10 +255,10 @@ end --- ---Finds all entities within a radius from a list of classnames. --- ----@param classes string[] ----@param origin Vector ----@param maxRadius number ----@return EntityHandle[] +---@param classes string[] # List of classnames +---@param origin Vector # Position to check from +---@param maxRadius number # Maximum radius to check from `origin` +---@return EntityHandle[] # List of entities function Entities:FindAllByClassnameListWithin(classes, origin, maxRadius) local ents = {} for _, class in ipairs(classes) do @@ -270,10 +270,10 @@ end --- ---Find the entity from a list of possible classnames which is closest to a world position. --- ----@param classes string[] ----@param origin Vector ----@param maxRadius number ----@return EntityHandle? +---@param classes string[] # List of classnames +---@param origin Vector # Position to check from +---@param maxRadius number # Maximum radius to check from `origin` +---@return EntityHandle? # The nearest entity found, or `nil` if none found function Entities:FindByClassnameListNearest(classes, origin, maxRadius) local nearestEnt = nil local nearestDistanceSq = math.huge @@ -295,7 +295,7 @@ end --- ---Finds all NPCs within the map. --- ----@return CAI_BaseNPC[] +---@return CAI_BaseNPC[] # List of NPC entities function Entities:FindAllNPCs() local npcs = {} local ent = Entities:First() @@ -332,12 +332,12 @@ function Entities:IterateAllNPCs() end --- ----Find all entities by model name within a radius. +---Finds all entities by model name within a radius. --- ----@param modelName string ----@param origin Vector ----@param maxRadius number ----@return EntityHandle[] +---@param modelName string # Model name +---@param origin Vector # Position to check from +---@param maxRadius number # Maximum radius to check from `origin` +---@return EntityHandle[] # List of entities function Entities:FindAllByModelWithin(modelName, origin, maxRadius) local ents = {} local currentEnt = Entities:FindByModelWithin(nil, modelName, origin, maxRadius) @@ -349,12 +349,12 @@ function Entities:FindAllByModelWithin(modelName, origin, maxRadius) end --- ----Find the entity by model name nearest to a point. +---Finds the entity by model name nearest to a point. --- ----@param modelName string ----@param origin Vector ----@param maxRadius number ----@return EntityHandle? +---@param modelName string # Model name +---@param origin Vector # Position to check from +---@param maxRadius number # Maximum radius to check from `origin` +---@return EntityHandle? # The nearest entity found, or `nil` if none found function Entities:FindByModelNearest(modelName, origin, maxRadius) local closestEnt = nil local closestDist = math.huge @@ -371,12 +371,12 @@ function Entities:FindByModelNearest(modelName, origin, maxRadius) end --- ----Find the first entity whose model name contains `namePattern`. +---Finds the first entity whose model name contains `namePattern`. --- ---This works by searching every entity in the map and may incur a performance hit in large maps if used often. --- ----@param namePattern string ----@return EntityHandle? +---@param namePattern string # Pattern to search for +---@return EntityHandle? # The found entity, or `nil` if not found function Entities:FindByModelPattern(namePattern) local ent = Entities:First() while ent ~= nil do @@ -388,12 +388,12 @@ function Entities:FindByModelPattern(namePattern) end --- ----Find all entities whose model name contains `namePattern`. +---Finds all entities whose model name contains `namePattern`. --- ---This works by searching every entity in the map and may incur a performance hit in large maps if used often. --- ----@param namePattern string ----@return EntityHandle[] +---@param namePattern string # Pattern to search for +---@return EntityHandle[] # List of entities function Entities:FindAllByModelPattern(namePattern) local ents = {} local ent = Entities:First() diff --git a/scripts/vscripts/alyxlib/extensions/entity.lua b/scripts/vscripts/alyxlib/extensions/entity.lua index 5f97d71..21f587a 100644 --- a/scripts/vscripts/alyxlib/extensions/entity.lua +++ b/scripts/vscripts/alyxlib/extensions/entity.lua @@ -1,20 +1,20 @@ --[[ - v2.7.1 + v2.8.0 https://github.com/FrostSource/alyxlib Provides base entity extension methods. - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: require "alyxlib.extensions.entity" ]] -local version = "v2.7.1" +local version = "v2.8.0" --- ----Get the entities parented to this entity. Including children of children. +---Gets the entities parented to this entity. Including children of children. --- ----This is a memory safe version of GetChildren() which causes a memory leak when called. +---This is a memory safe version of [GetChildren](lua://CBaseEntity.GetChildren) which causes a memory leak when called. ---If you need to get children often you should use this function instead. --- ---@return EntityHandle[] @@ -27,14 +27,14 @@ function CBaseEntity:GetChildrenMemSafe() end --- ----Returns a `function` that iterates over 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. --- ---Useful in `for` loops: --- --- for child in thisEntity:IterateChildren() do ---- print(Debug.EntStr(child)) +--- print(entstr(child)) --- end --- ---This function is memory safe. @@ -60,14 +60,14 @@ function CBaseEntity:IterateChildren() end --- ----Returns a `function` that iterates over all children of this entity in **breadth-first order**. +---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)) +--- print(entstr(child)) --- end --- ---Unlike [IterateChildren](lua://CBaseEntity.IterateChildren), this visits all immediate children first, @@ -104,7 +104,7 @@ end --- ----Get the top level entities parented to this entity. Not children of children. +---Gets the top level entities parented to this entity. *Not* children of children. --- ---This function is memory safe. --- @@ -122,6 +122,8 @@ end --- ---Get the first child in the hierarchy that has targetname or classname. --- +---This function is memory safe. +--- ---@param name string # The name or classname to look for, supports wildcard '*' ---@return EntityHandle? function CBaseEntity:GetChild(name) @@ -166,25 +168,25 @@ end --- ----Send an input to this entity. +---Sends an input to this entity. --- ----@param action string # Input name. ----@param value? any # Parameter override for the input. ----@param delay? number # Delay in seconds. ----@param activator? EntityHandle ----@param caller? EntityHandle +---@param action string # Input name +---@param value? any # Parameter override for the input +---@param delay? number # Delay in seconds +---@param activator? EntityHandle # IO activator +---@param caller? EntityHandle # IO caller function CBaseEntity:EntFire(action, value, delay, activator, caller) DoEntFireByInstanceHandle(self, action, value and tostring(value) or "", delay or 0, activator or nil, caller or nil) end --- ----Get the first child in this entity's hierarchy with a given `classname`. +---Gets 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 search for. ----@return EntityHandle? # The first matching child found, or `nil` if none exists. +---@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 self:IterateChildrenBreadthFirst() do if child:GetClassname() == classname then @@ -196,13 +198,13 @@ function CBaseEntity:GetFirstChildWithClassname(classname) end --- ----Get the first child in this entity's hierarchy with a given `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 search for. ----@return EntityHandle? # The first matching child found, or `nil` if none exists. +---@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 self:IterateChildrenBreadthFirst() do if child:GetName() == name then @@ -214,34 +216,34 @@ function CBaseEntity:GetFirstChildWithName(name) end --- ----Set entity pitch, yaw, roll from a `QAngle`. +---Sets entity pitch, yaw, roll from a `QAngle`. --- ----@param qangle QAngle # The rotation to set (pitch, yaw, roll). +---@param qangle QAngle # The QAngle rotation to set function CBaseEntity:SetQAngle(qangle) self:SetAngles(qangle.x, qangle.y, qangle.z) end --- ----Set entity local pitch, yaw, roll from a `QAngle`. +---Sets entity local pitch, yaw, roll from a `QAngle`. --- ----@param qangle QAngle # The rotation to set (pitch, yaw, roll). +---@param qangle QAngle # The QAngle rotation to set function CBaseEntity:SetLocalQAngle(qangle) self:SetLocalAngles(qangle.x, qangle.y, qangle.z) end --- ----Set entity pitch, yaw or roll. Supply `nil` for any parameter to leave it unchanged. +---Sets entity pitch, yaw or roll. Supply `nil` for any parameter to leave it unchanged. --- ----@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. +---@param pitch? number # Pitch angle +---@param yaw? number # Yaw angle +---@param roll? number # Roll angle 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 angles to (0, 0, 0). --- function CBaseEntity:ResetLocal() self:SetLocalOrigin(Vector()) @@ -249,46 +251,46 @@ function CBaseEntity:ResetLocal() end --- ----Get the bounding size of the entity. +---Gets the bounding size of the entity. --- ----@return Vector # Bounding size of the entity as a Vector. +---@return Vector # Bounding size of the entity as a Vector function CBaseEntity:GetSize() return self:GetBoundingMaxs() - self:GetBoundingMins() end --- ----Get the biggest bounding box axis of the entity. ----This will be `size.x`, `size.y` or `size.z`. +---Gets the biggest bounding box axis of the entity. +---This will be either `size.x`, `size.y` or `size.z`. --- ----@return number # The largest bounding value. +---@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's bounding box. +---Gets the radius of the entity's bounding box. ---This is half the size of the bounding box along its largest axis. --- ----@return number # The bounding radius value. +---@return number # The bounding radius value function CBaseEntity:GetRadius() return self:GetSize():Length() * 0.5 end --- ----Get the volume of the entity bounds in cubic inches. +---Gets the volume of the entity bounds in cubic inches. --- ----@return number # The volume of the entity bounds. +---@return number # The volume of the entity bounds function CBaseEntity:GetVolume() local size = self:GetSize() * self:GetAbsScale() return size.x * size.y * size.z end --- ----Get each corner of the entity's bounding box. +---Gets each corner of the entity's bounding box. --- ----@param rotated? boolean # If true, corners are rotated by the entity's angles. ----@return Vector[] # List of 8 corner positions. +---@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() @@ -314,12 +316,12 @@ function CBaseEntity:GetBoundingCorners(rotated) end --- ----Check if entity is within the given worldspace bounds. +---Checks if entity is within the given worldspace bounds. --- ----@param mins Vector # Worldspace minimum vector for the bounds. ----@param maxs Vector # Worldspace minimum vector for the bounds. ----@param checkEntityBounds? boolean # If true the entity bounding box will be used for the check instead of its origin. ----@return boolean # True if the entity is within the bounds, false otherwise. +---@param mins Vector # Worldspace minimum vector for the bounds +---@param maxs Vector # Worldspace maximum vector for the bounds +---@param checkEntityBounds? boolean # If `true` the entity bounding box will be used for the check instead of its origin +---@return boolean # `true` if the entity is within the bounds, `false` otherwise. function CBaseEntity:IsWithinBounds(mins, maxs, checkEntityBounds) local selfMins, selfMaxs if checkEntityBounds then @@ -339,32 +341,34 @@ function CBaseEntity:IsWithinBounds(mins, maxs, checkEntityBounds) end --- ----Send the `DisablePickup` input to the entity. +---Sends the `DisablePickup` input to the entity. --- function CBaseEntity:DisablePickup() DoEntFireByInstanceHandle(self, "DisablePickup", "", 0, self, self) end --- ----Send the `EnablePickup` input to the entity. +---Sends the `EnablePickup` input to the entity. --- function CBaseEntity:EnablePickup() DoEntFireByInstanceHandle(self, "EnablePickup", "", 0, self, self) end --- ----Delay some code using this entity. +---Delays some code, using this entity as the context. --- ----@param func fun() # The function to delay. ----@param delay? number # Optional delay in seconds (default 0). +---@param func fun() # The function to delay +---@param delay? number # Delay in seconds (default 0) function CBaseEntity:Delay(func, delay) self:SetContextThink(DoUniqueString("delay"), function() func() end, delay or 0) end --- ----Get all parents in the hierarchy upwards. +---Gets all parents in the hierarchy upwards, from immediate parent up to the root. --- ----@return EntityHandle[] # List of parent entities, from immediate parent up to the root. +---@see CBaseEntity.GetMoveParent # The immediate parent +---@see CBaseEntity.GetRootMoveParent # The root parent +---@return EntityHandle[] # List of parent entities function CBaseEntity:GetParents() local parents = {} local parent = self:GetMoveParent() @@ -376,9 +380,9 @@ function CBaseEntity:GetParents() end --- ----Set if the prop is allowed to be dropped. Only works for physics based props. +---Sets if the prop is not allowed to be dropped. Only works for physics based props. --- ----@param enabled boolean # True if the prop can't be dropped, false for can be dropped. +---@param enabled boolean # `true` if the prop is not allowed to be dropped function CBaseEntity:DoNotDrop(enabled) if enabled then self:Attribute_SetIntValue("DoNotDrop", 1) @@ -388,20 +392,21 @@ function CBaseEntity:DoNotDrop(enabled) end --- ----Get all criteria on this entity as a table. +---Gets all criteria for this entity as a table. --- ----@return CriteriaTable # A table of criteria key-value pairs. +---@return CriteriaTable # Criteria key-value pairs function CBaseEntity:GetCriteria() local c = {} self:GatherCriteria(c) return c end + --- ----Get all entities owned by this entity. +---Gets all entities owned by this entity. --- ---**Note:** This searches all entities in the map and should be used sparingly. --- ----@return EntityHandle[] # List of owned entities. +---@return EntityHandle[] # List of owned entities function CBaseEntity:GetOwnedEntities() local ents = {} local ent = Entities:First() @@ -415,9 +420,9 @@ function CBaseEntity:GetOwnedEntities() end --- ----Set the alpha modulation of this entity, plus any children that support [SetRenderAlpha](lua://CBaseModelEntity.SetRenderAlpha). +---Sets the alpha modulation of this entity, plus any children that support [SetRenderAlpha](lua://CBaseModelEntity.SetRenderAlpha). --- ----@param alpha integer # Alpha value (0 = fully transparent, 255 = fully opaque). +---@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 @@ -430,7 +435,7 @@ end --- ---Moves the entity so that its center is at the given position. --- ----@param position Vector # The new center position. +---@param position Vector # The new center position function CBaseEntity:SetCenter(position) local center = self:GetCenter() local origin = self:GetOrigin() @@ -443,10 +448,10 @@ end --- ----Set the entity's origin so that the specified attachment point aligns with the given world position. +---Sets the entity's origin so that the specified attachment point aligns with the given world position. --- ----@param position Vector # The target world position for the attachment point. ----@param attachment string # The name of the attachment point to align. +---@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() @@ -457,7 +462,7 @@ function CBaseAnimating:SetOriginByAttachment(position, attachment) end --- ----Track a property function using a callback when a change is detected. +---Tracks a property function and calls a callback when a change is detected. --- --- -- Make entity fully opaque if alpha is ever detected below 255 --- thisEntity:TrackProperty(thisEntity.GetRenderAlpha, function(prevValue, newValue) @@ -466,37 +471,41 @@ end --- end --- end) --- ----@param propertyFunction fun(handle: EntityHandle): any # Property function to track, e.g. GetRenderAlpha. ----@param onChangeFunction fun(prevValue: any, newValue: any) # Function to call when a change is detected. ----@param interval? number # Think interval, or smallest possible if nil. ----@param context? EntityHandle # Entity to run the thinker on, or this entity if nil. +---@param propertyFunction fun(handle: EntityHandle): any # Property function to track +---@param onChangeFunction fun(prevValue: any, newValue: any) # Function to call when a change is detected +---@param interval? number # Think interval (default: 0) +---@param context? EntityHandle # Entity to run the thinker on, or the calling entity if `nil` +---@return string # The name of the think for stopping later function CBaseEntity:TrackProperty(propertyFunction, onChangeFunction, interval, context) interval = interval or 0 context = context or self local value = propertyFunction(self) - context:SetContextThink(tostring(propertyFunction), function() + local name = tostring(propertyFunction) + context:SetContextThink(name, function() local newValue = propertyFunction(self) if newValue ~= value then onChangeFunction(value, newValue) end return interval end, interval) + + return name end --- ---- Untrack a property function which was set to be tracked using `CBaseEntity:TrackProperty`. +---Untracks a property function which was set to be tracked using [TrackProperty](lua://CBaseEntity.TrackProperty). --- ----@param propertyFunction fun(handle: EntityHandle): any # Property function to untrack, e.g. GetRenderAlpha. +---@param propertyFunction fun(handle: EntityHandle): any # Property function to untrack function CBaseEntity:UntrackProperty(propertyFunction) self:SetContextThink(tostring(propertyFunction), nil, 0) end --- ----Quickly start a think function on the entity with a random name and no delay. +---Quickly starts a think function on the entity with a random name. --- ----@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. +---@param func fun():number? # The think function +---@param delay? number # Delay before starting the think (default: 0) +---@return string # The name of the think for stopping later if desired function CBaseEntity:QuickThink(func, delay) local name = DoUniqueString("QuickThink") self:SetContextThink(name, func, delay or 0) @@ -506,7 +515,7 @@ end --- ---Sets whether the entity is rendered or not. --- ----@param renderingEnabled boolean # If false the entity will become invisible. +---@param renderingEnabled boolean # `true` to enable rendering, `false` to disable function CBaseEntity:SetRenderingEnabled(renderingEnabled) if renderingEnabled then self:RemoveEffects(0x020) @@ -518,7 +527,7 @@ end --- ---Sets whether the entity casts a shadow or not. --- ----@param shadowEnabled boolean # If false the entity will not cast a dynamic shadow. +---@param shadowEnabled boolean # `true` to enable shadow casting, `false` to disable function CBaseEntity:SetCastShadow(shadowEnabled) if shadowEnabled then self:RemoveEffects(0x010) @@ -529,11 +538,13 @@ end --- ---Adds an I/O connection that will call the function on the passed entity when the specified output fires. ----This means the redirection is persistent after game loads. --- ----@param output string # The name of the output to redirect. ----@param func function # The function to redirect to. ----@param entity? EntityHandle # The entity to redirect to, defaults to this entity. +---This is persistent after game loads. +--- +---@see CEntityInstance.DisconnectRedirectedOutput # for disconnecting +---@param output string # The name of the output to redirect +---@param func function # The function to redirect to +---@param entity? EntityHandle # The entity to redirect to, defaults to the calling entity function CEntityInstance:RedirectOutputFunc(output, func, entity) entity = entity or self @@ -547,8 +558,8 @@ end --- ---Gets the position in front of the entityโ€™s eyes at the specified distance. --- ----@param distance number # How far in front of the eyes to get the position. ----@return Vector # The world position in front of the eyes. +---@param distance number # How far in front of the eyes +---@return Vector # The world position in front of the eyes function CBaseEntity:DistanceFromEyes(distance) return self:EyePosition() + self:EyeAngles():Forward() * distance end @@ -556,8 +567,8 @@ end --- ---Gets the world origin position of a named attachment point. --- ----@param name string # The name of the attachment. ----@return Vector # The world position of the attachment. +---@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 @@ -565,8 +576,8 @@ end --- ---Gets the world angles (rotation) of a named attachment point. --- ----@param name string # The name of the attachment. ----@return Vector # The world rotation angles of the attachment. +---@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 @@ -574,8 +585,8 @@ end --- ---Gets the forward direction vector of a named attachment. --- ----@param name string # The name of the attachment. ----@return Vector # The forward unit vector of the attachment in world space. +---@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 @@ -587,4 +598,58 @@ function CBaseEntity:ClearParent() self:SetParent(nil, nil) end +--- +---Sets the absolute world velocity of the entity. +--- +---@param velocity Vector The target velocity in units/second +function CBaseEntity:SetAbsVelocity(velocity) + local currentVelocity = GetPhysVelocity(self) + + local impulse = velocity - currentVelocity + + self:ApplyAbsVelocityImpulse(impulse) +end + +--- +---Tests if the OBB of this entity intersects with the OBB of another entity. +--- +---@param other EntityHandle # The other entity +---@return boolean # `true` if the OBB of this entity intersects with the OBB of the other entity +function CBaseEntity:OBBvsOBB(other) + return OBBvsOBB( + GetEntityOBBData(self), self:GetOrigin(), self:GetAngles(), + GetEntityOBBData(other), other:GetOrigin(), other:GetAngles() + ) +end + +--- +---Tests if the AABB of this entity intersects with the OBB of another entity. +--- +---The AABB is defined by the entity's bounding mins/maxs and its current origin/angles. +--- +---@param other EntityHandle # The other entity +---@return boolean # `true` if the AABB of this entity intersects with the OBB of the other entity +function CBaseEntity:AABBvsOBB(other) + local aMin, aMax = GetEntityAABB(self) + + return AABBvsOBB( + aMin, aMax, + GetEntityOBBData(other), other:GetOrigin(), other:GetAngles() + ) +end + +--- +---Tests if the AABB of this entity intersects with the AABB of another entity. +--- +---The AABB is defined by the entity's bounding mins/maxs and its current origin/angles. +--- +---@param other EntityHandle # The other entity +---@return boolean # `true` if the AABB of this entity intersects with the AABB of the other entity +function CBaseEntity:AABBvsAABB(other) + local aMin, aMax = GetEntityAABB(self) + local bMin, bMax = GetEntityAABB(other) + return AABBvsAABB(aMin, aMax, bMin, bMax) +end + + return version \ No newline at end of file diff --git a/scripts/vscripts/alyxlib/extensions/npc.lua b/scripts/vscripts/alyxlib/extensions/npc.lua index 42ff2b1..d90682f 100644 --- a/scripts/vscripts/alyxlib/extensions/npc.lua +++ b/scripts/vscripts/alyxlib/extensions/npc.lua @@ -1,11 +1,11 @@ --[[ - v1.2.0 + v1.2.1 https://github.com/FrostSource/alyxlib Extension for NPC entities. Some functions are specific to only one entity class such as `npc_combine_s`, check the individual function descriptions. - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: require "alyxlib.extensions.npc" ]] @@ -14,10 +14,10 @@ require "alyxlib.globals" require "alyxlib.extensions.entity" require "alyxlib.extensions.entities" -local version = "v1.2.0" +local version = "v1.2.1" --- ----Schedule enum table for use with CAI_BaseNPC:StartSchedule(). +---Schedule enum table for use with [CAI_BaseNPC:StartSchedule](lua://CAI_BaseNPC.StartSchedule). --- Schedule = { ---@enum ScheduleType @@ -48,14 +48,14 @@ Schedule = { } --- ----Create and start a new schedule for this NPC. +---Creates and starts a new schedule for this NPC. --- ----@param state ScheduleState # The NPC state that should be set. ----@param type ScheduleType # The type of schedule to perform. ----@param interruptability ScheduleInterruptability # What should interrupt the NPC from the schedule. ----@param reacquire boolean # If the NPC should reacquire the schedule after being interrupted. ----@param goal EntityHandle|Vector # Worldspace position or entity goal (entity origin will be used). ----@return EntityHandle # The schedule entity. +---@param state ScheduleState # The NPC state that should be set +---@param type ScheduleType # The type of schedule to perform +---@param interruptability ScheduleInterruptability # What should interrupt the NPC from the schedule +---@param reacquire boolean # If the NPC should reacquire the schedule after being interrupted +---@param goal EntityHandle|Vector # Worldspace position or entity goal (entity origin will be used) +---@return EntityHandle # The new `aiscripted_schedule` function CAI_BaseNPC:StartSchedule(state, type, interruptability, reacquire, goal) local goalName @@ -89,8 +89,8 @@ end --- ---Stops the given schedule for this NPC. --- ----@param schedule EntityHandle # The previously created schedule. ----@param dontKill? boolean # If true the schedule will not be killed at the same time. +---@param schedule EntityHandle # The previously created schedule +---@param dontKill? boolean # If true the schedule will not be killed at the same time function CAI_BaseNPC:StopSchedule(schedule, dontKill) if IsEntity(schedule, true) then schedule:EntFire("StopSchedule") @@ -100,30 +100,32 @@ function CAI_BaseNPC:StopSchedule(schedule, dontKill) end end ----Set state of the NPC. ----@param state ScheduleState +--- +---Sets the state of the NPC by creating a new schedule. +--- +---@param state ScheduleState # The NPC state that should be set function CAI_BaseNPC:SetState(state) local schedule = self:StartSchedule(state, 0, 0, false, self) end --- ----Get if this NPC has an enemy target. +---Gets if this NPC has an enemy target. --- ---This function only works with entities that have `enemy` or `distancetoenemy` criteria. --- ----@return boolean # True if the NPC has an enemy target. +---@return boolean # True if the NPC has an enemy target function CAI_BaseNPC:HasEnemyTarget() local criteria = self:GetCriteria() return criteria.enemy ~= nil or criteria.distancetoenemy ~= 16384 end --- ----Estimate the enemy that this NPC is fighting using its criteria values. +---Estimates the enemy that this NPC is fighting using its criteria values --- ----This function only works with entities that have `enemy` criteria; "npc_combine_s", "npc_zombine", "npc_zombie_blind". +---This function only works with entities that have `enemy` criteria, e.g. "npc_combine_s", "npc_zombine", "npc_zombie_blind". --- ----@param distanceTolerance? number # Discrepancy allowed when comparing distance to enemy. Default 1 ----@return EntityHandle? # Estimated enemy target. +---@param distanceTolerance? number # Discrepancy allowed when comparing distance to enemy; default is `1` +---@return EntityHandle? # Estimated enemy target function CAI_BaseNPC:EstimateEnemyTarget(distanceTolerance) distanceTolerance = distanceTolerance or 1 local criteria = self:GetCriteria() @@ -142,16 +144,21 @@ function CAI_BaseNPC:EstimateEnemyTarget(distanceTolerance) return nil end +--- +---Relationship disposition types. +--- ---@alias RelationshipDisposition ---| "D_HT" # Hate ---| "D_FR" # Fear ---| "D_LI" # Like ---| "D_NU" # Neutral ----Set the relationship of this NPC with a targetname or classname. +--- +---Sets the relationship of this NPC with another entity. +--- ---@param target string|EntityHandle # Targetname, classname or entity. ---@param disposition RelationshipDisposition # Type of relationship with `target`. ----@param priority? number # How much the Subject(s) should Like/Hate/Fear the Target(s). Higher priority = stronger feeling. Default is 0. +---@param priority? number # How much the Subject(s) should Like/Hate/Fear the Target(s). Higher priority = stronger feeling; default is 0. function CAI_BaseNPC:SetRelationship(target, disposition, priority) if IsEntity(target) then target = target--[[@as EntityHandle]]:GetName() @@ -179,11 +186,11 @@ local livingClasses = { } --- ----Get if this NPC is a creature, e.g. combine, headcrab, player +---Checks if this NPC is a creature, e.g. combine, headcrab, player --- ---Will return false for all other class types, such as npc_turret and npc_manhack. --- ----@return boolean +---@return boolean # True if this NPC is a creature function CAI_BaseNPC:IsCreature() return vlua.find(livingClasses, self:GetClassname()) ~= nil end @@ -195,9 +202,9 @@ local combineClasses = { } --- ----Get if this NPC is a combine creature. +---Checks if this NPC is a Combine creature. --- ----@return boolean +---@return boolean # True if this NPC is Combine function CAI_BaseNPC:IsCombine() return vlua.find(combineClasses, self:GetClassname()) ~= nil end @@ -215,17 +222,17 @@ local xenClasses = { } --- ----Get if this NPC is a Xen creature. +---Checks if this NPC is a Xen creature. --- ----@return boolean +---@return boolean # True if this NPC is Xen function CAI_BaseNPC:IsXen() return vlua.find(xenClasses, self:GetClassname()) ~= nil end --- ----Get all members of the same squad as this NPC, including this NPC. +---Gets all members of the same squad as this NPC, including this NPC. --- ----@return CAI_BaseNPC[] +---@return CAI_BaseNPC[] # List of NPC members function CAI_BaseNPC:GetSquadMembers() local squad = self:GetSquad() local members = {} diff --git a/scripts/vscripts/alyxlib/extensions/qangle.lua b/scripts/vscripts/alyxlib/extensions/qangle.lua index f274708..bae7099 100644 --- a/scripts/vscripts/alyxlib/extensions/qangle.lua +++ b/scripts/vscripts/alyxlib/extensions/qangle.lua @@ -1,16 +1,16 @@ --[[ - v1.0.0 + v1.0.1 https://github.com/FrostSource/alyxlib Provides QAngle class extension methods. - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: require "alyxlib.extensions.qangle" ]] -- require "alyxlib.math.common" -local version = "v1.0.0" +local version = "v1.0.1" ---@class QAngle local meta = getmetatable(QAngle()) diff --git a/scripts/vscripts/alyxlib/extensions/string.lua b/scripts/vscripts/alyxlib/extensions/string.lua index 8f172d9..3247551 100644 --- a/scripts/vscripts/alyxlib/extensions/string.lua +++ b/scripts/vscripts/alyxlib/extensions/string.lua @@ -1,41 +1,49 @@ --[[ - v1.3.1 + v2.0.0 https://github.com/FrostSource/alyxlib Provides string class extension methods. - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: require "alyxlib.extensions.string" ]] -local version = "v1.3.1" +local version = "v2.0.0" +---Escapes special characters in a string. +---@param s string # The string to escape +---@return string # The escaped string +local function escape(s) + return (s:gsub("([^%w])", "%%%1")) +end + +--- +---Checks if a string starts with a substring. --- ----Gets if a string starts with a substring. ----@param s string ----@param substr string ----@return boolean +---@param s string # The string to check +---@param substr string # The substring to check +---@return boolean # `true` if the string starts with the substring, `false` otherwise function string.startswith(s, substr) return s:sub(1, #substr) == substr end --- ----Gets if a string ends with a substring. +---Checks if a string ends with a substring. --- ----@param s string ----@param substr string ----@return boolean +---@param s string # The string to check +---@param substr string # The substring to check +---@return boolean # `true` if the string ends with the substring, `false` otherwise function string.endswith(s, substr) return substr == "" or s:sub(-#substr) == substr end --- ----Split an input string using a raw pattern string. No changes are made to the pattern. +---Splits a string using a raw pattern string. No changes are made to the pattern. --- ----@param s string ----@param pattern string # Split pattern. ----@return string[] +---@param s string # String to split. +---@param pattern string # Split pattern +---@return string[] # Array of split strings function string.splitraw(s, pattern) local t = {} for str in s:gmatch(pattern) do @@ -45,28 +53,35 @@ function string.splitraw(s, pattern) end --- ----Split an input string using a separator string. +---Splits a string using a separator string. +--- +---If `sep` is omitted, splits on whitespace (`%s`). +--- +---Uses Lua pattern matching; escape special characters if needed. --- ---@link https://stackoverflow.com/a/7615129 --- ----@param s string ----@param sep string? # String to split by. Default is whitespace. ----@return string[] +---@param s string # String to split +---@param sep string? # String to split by +---@return string[] # Array of split strings function string.split(s, sep) if sep == nil then sep = '%s' + else + sep = escape(sep) end return string.splitraw(s, '([^'..sep..']+)') end --- ---Truncates a string to a maximum length. +--- ---If the string is shorter than `len` the original string is returned. --- ----@param s string ----@param len integer # Maximum length the string can be. ----@param replacement? string # Suffix for long strings. Default is '...' ----@return string +---@param s string # String to truncate +---@param len integer # Maximum length the string can be +---@param replacement? string # Suffix for long strings. Default is `...` +---@return string # The truncated string function string.truncate(s, len, replacement) replacement = replacement or "..." if #s > len then @@ -76,27 +91,68 @@ function string.truncate(s, len, replacement) end --- ----Trims characters from the left side of the string up to the last occurrence of a specified character. +---Slices the string from the left, returning everything after the last occurrence of a specified character. --- ----@param s string ----@param char string # The character to trim the string at the last occurrence. ----@return string # The trimmed string. -function string.trimleft(s, char) - local index = s:match(".*" .. char .. "()") +---@param s string # The string to slice +---@param char string # The character to trim the string at the last occurrence +---@return string # The trimmed string +function string.sliceleft(s, char) + local index = s:match(".*" .. escape(char) .. "()") return index and s:sub(index) or s end + --- ----Trims characters from the right side of the string up to the last occurrence of a specified character. +---Slices the string from the right, returning everything before the first occurrence of a specified character. --- ----@param s string ----@param char string # The character to trim the string at the last occurrence. ----@return string # The trimmed string. -function string.trimright(s, char) - local index = s:find(char) +---@param s string # The string to slice +---@param char string # The character to trim the string at the last occurrence +---@return string # The trimmed string +function string.sliceright(s, char) + local index = s:find(escape(char)) return index and s:sub(1, index - 1) or s end +--- +---Trims characters from the left side of a string. +--- +---@param s string # String to trim +---@param chars? string # Characters to trim (defaults to whitespace) +---@return string # The trimmed string +function string.trimleft(s, chars) + if chars == nil then + chars = "%s" + else + chars = escape(chars) + end + return (s:gsub("^[" .. chars .. "]+", "")) +end + +--- +---Trims characters from the right side of a string. +--- +---@param s string # String to trim +---@param chars? string # Characters to trim (defaults to whitespace) +---@return string # The trimmed string +function string.trimright(s, chars) + if chars == nil then + chars = "%s" + else + chars = escape(chars) + end + return (s:gsub("[" .. chars .. "]+$", "")) +end + +--- +---Trims characters from both sides of a string. +--- +---@param s string # String to trim +---@param chars? string # Characters to trim (defaults to whitespace) +---@return string # The trimmed string +function string.trim(s, chars) + return string.trimleft(string.trimright(s, chars), chars) +end + --- ---Capitalizes letters in the input string. --- @@ -104,9 +160,9 @@ end --- ---If `onlyFirstLetter` is false or not provided, it capitalizes all letters. --- ----@param s string # The input string to be capitalized. ----@param onlyFirstLetter boolean # (optional) If true, only the first letter is capitalized. Default is false. ----@return string # The capitalized string. +---@param s string # The string to be capitalized +---@param onlyFirstLetter? boolean # If true, only the first letter is capitalized +---@return string # The capitalized string function string.capitalize(s, onlyFirstLetter) if onlyFirstLetter then local upper = s:gsub("^%l", string.upper) diff --git a/scripts/vscripts/alyxlib/extensions/template.lua b/scripts/vscripts/alyxlib/extensions/template.lua index 22ab7c7..2846487 100644 --- a/scripts/vscripts/alyxlib/extensions/template.lua +++ b/scripts/vscripts/alyxlib/extensions/template.lua @@ -1,15 +1,15 @@ --[[ - v1.0.0 + v1.0.1 https://github.com/FrostSource/alyxlib Provides point_template extension methods. - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: require "alyxlib.extensions.template" ]] -local version = "v1.0.0" +local version = "v1.0.1" --- ---Spawn the template at the IO caller position, rotated around the caller. diff --git a/scripts/vscripts/alyxlib/extensions/vector.lua b/scripts/vscripts/alyxlib/extensions/vector.lua index ec65aac..dd9e9e0 100644 --- a/scripts/vscripts/alyxlib/extensions/vector.lua +++ b/scripts/vscripts/alyxlib/extensions/vector.lua @@ -1,16 +1,16 @@ --[[ - v1.3.0 + v1.3.1 https://github.com/FrostSource/alyxlib Provides Vector class extension methods. - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: require "alyxlib.extensions.vector" ]] require "alyxlib.math.common" -local version = "v1.3.0" +local version = "v1.3.1" ---@class Vector local meta = getmetatable(Vector()) @@ -22,7 +22,7 @@ local zero = Vector(0, 0, 0) --- ---Calculates the perpendicular vector to the current vector. --- ----@return Vector # The perpendicular vector. +---@return Vector # The perpendicular vector function meta:Perpendicular() if self.y == 0 and self.z == 0 then if self.x == 0 then @@ -37,11 +37,11 @@ function meta:Perpendicular() end --- ----Check if the current vector is perpendicular to another vector. +---Checks if the current vector is perpendicular to another vector. --- ----@param vector Vector # The other vector to check perpendicularity against. ----@param tolerance? number # (optional) The tolerance value for the dot product comparison. Default is 1e-8. ----@return boolean # True if the vectors are perpendicular, false otherwise. +---@param vector Vector # The other vector to check perpendicularity against +---@param tolerance? number # (optional) The tolerance value for the dot product comparison. Default is `1e-8` +---@return boolean # True if the vectors are perpendicular, false otherwise function meta:IsPerpendicularTo(vector, tolerance) tolerance = tolerance or 1e-8 return math.abs(self:Dot(vector)) <= tolerance @@ -50,8 +50,8 @@ end --- ---Checks if the current vector is parallel to the given vector. --- ----@param vector Vector # The vector to compare with. ----@return boolean # True if the vectors are parallel, false otherwise. +---@param vector Vector # The vector to compare with +---@return boolean # True if the vectors are parallel, false otherwise function meta:IsParallelTo(vector) -- Treat zero vectors as parallel to any vector if self == zero or vector == zero then @@ -62,11 +62,11 @@ function meta:IsParallelTo(vector) end --- ----Spherical linear interpolation between the calling vector and the target vector over t = [0, 1]. +---Spherical linear interpolation between the calling vector and the target vector over `t = [0, 1]`. --- ----@param target Vector # The target vector to interpolate towards. ----@param t number # The interpolation factor, ranging from 0 to 1. ----@return Vector # The resulting vector after spherical linear interpolation. +---@param target Vector # The target vector to interpolate towards +---@param t number # The interpolation factor, ranging from 0 to 1 +---@return Vector # The resulting vector after spherical linear interpolation function meta:Slerp(target, t) local dot = self:Dot(target) dot = math.max(-1, math.min(1, dot)) @@ -83,19 +83,18 @@ end --- ---Translates a vector within a local coordinate system. ----This function computes a new vector by applying an offset relative to the local axes defined by the forward, right, and up direction vectors. ---- ----@param offset Vector # The translation offset vector. This defines how much to move along the forward, right, and up directions. ---- - `offset.x`: Translation along the forward vector. ---- - `offset.y`: Translation along the right vector. ---- - `offset.z`: Translation along the up vector. --- ----@param forward Vector # The forward direction of the local coordinate system. ----@param right Vector # The right direction of the local coordinate system. ----@param up Vector # The up direction of the local coordinate system. +---This function computes a new vector by applying an offset relative to the local axes defined by the forward, right, and up direction vectors. --- ----@return Vector # A new vector representing the translated position. +---@param offset Vector # The translation offset vector. This defines how much to move along the forward, right, and up directions +--- - `offset.x`: Translation along the forward vector +--- - `offset.y`: Translation along the right vector +--- - `offset.z`: Translation along the up vector --- +---@param forward Vector # The forward direction of the local coordinate system +---@param right Vector # The right direction of the local coordinate system +---@param up Vector # The up direction of the local coordinate system +---@return Vector # A new vector representing the translated position function meta:LocalTranslate(offset, forward, right, up) local x = self.x + offset.x * forward.x + offset.y * right.x + offset.z * up.x local y = self.y + offset.x * forward.y + offset.y * right.y + offset.z * up.y @@ -106,8 +105,8 @@ end --- ---Calculates the angle difference in degrees between the calling vector and the given vector. This is always the smallest angle. --- ----@param vector Vector # The vector to calculate the angle difference with. ----@return number # Angle difference in degrees. +---@param vector Vector # The vector to calculate the angle difference with +---@return number # Angle difference in degrees function meta:AngleDiff(vector) local denominator = math.sqrt(self:Length() * vector:Length()) if denominator < 1e-15 then @@ -120,9 +119,9 @@ end --- ---Calculates the signed angle difference between the calling vector and the given vector around the specified axis. --- ----@param vector Vector # The vector to calculate the angle difference with. ----@param axis? Vector # The axis of rotation around which the angle difference is calculated. ----@return number # The signed angle difference in degrees. +---@param vector Vector # The vector to calculate the angle difference with +---@param axis? Vector # The axis of rotation around which the angle difference is calculated +---@return number # The signed angle difference in degrees function meta:SignedAngleDiff(vector, axis) axis = axis or Vector(0, 0, 1) local unsignedAngle = self:AngleDiff(vector) @@ -145,6 +144,7 @@ end --- ---Returns the squared length (magnitude) of the vector. +--- ---More efficient than calculating the actual length as it avoids using `sqrt()`. --- ---@return number @@ -155,9 +155,10 @@ end --- ---Checks if this vector is similar to another vector within a given tolerance. --- ----@param vector Vector # The vector to compare against. ----@param tolerance? number # The tolerance within which the vectors are considered similar. Default is 1e-5. See [math.isclose](lua://math.isclose) ----@return boolean # Returns `true` if the vectors are similar within the tolerance, otherwise `false`. +---@see math.isclose +---@param vector Vector # The vector to compare against +---@param tolerance? number # The tolerance within which the vectors are considered similar. Default is `1e-5` +---@return boolean # Returns `true` if the vectors are similar within the tolerance, otherwise `false` function meta:IsSimilarTo(vector, tolerance) tolerance = tolerance or 1e-5 return IsVector(vector) @@ -169,7 +170,7 @@ end --- ---Creates a copy of the vector. --- ----@return Vector # A new vector with the same components as the original. +---@return Vector # A new vector with the same components as the original function meta:Clone() return Vector(self.x, self.y, self.y) end diff --git a/scripts/vscripts/alyxlib/globals.lua b/scripts/vscripts/alyxlib/globals.lua index c7acdaa..645c537 100644 --- a/scripts/vscripts/alyxlib/globals.lua +++ b/scripts/vscripts/alyxlib/globals.lua @@ -1,20 +1,18 @@ --[[ - v2.7.0 + v2.8.0 https://github.com/FrostSource/alyxlib Provides common global functions used throughout extravaganza libraries. - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: - ```lua require "alyxlib.globals" - ``` ]] -- These are expected by globals require 'alyxlib.utils.common' -local _version = "v2.7.0" +local _version = "v2.8.0" --- ---A registered AlyxLib addon. @@ -31,7 +29,7 @@ local _version = "v2.7.0" --- ---List of registered addons using AlyxLib. --- ----Requires the developer to use the `RegisterAlyxLibAddon` function in their addon. +---Requires the developer to use the [RegisterAlyxLibAddon](lua://RegisterAlyxLibAddon) function in their addon. --- ---@type AlyxLibAddon[] AlyxLibAddons = {} @@ -42,7 +40,7 @@ AlyxLibAddons = {} ---@param name string # Full display name of the addon, e.g. "My New Addon" ---@param version string # SemVer version string of the addon, e.g. "v1.2.3" ---@param workshopID? string # The ID of the addon on the Steam workshop ----@param shortName? string # Short unique name of the addon without spaces, e.g. "myaddon". Defaults to `name` without spaces and converted to lowercase\ +---@param shortName? string # Short unique name of the addon without spaces, e.g. "myaddon". Defaults to `name` without spaces and converted to lowercase ---@param minAlyxLibVersion? string # Minimum AlyxLib version that this addon works with, defaults to "v1.0.0" ---@param maxAlyxLibVersion? string # Maximum AlyxLib version that this addon works with, defaults to `ALYXLIB_VERSION` ---@return integer # The index of the addon for use in other AlyxLib functions @@ -88,8 +86,8 @@ end ---It compares the versions based on their `major`, `minor`, and `patch` components. ---If a version is incomplete, the missing components are assumed to be 0. --- ----@param v1 string # The first version string to compare. May include leading "v" and whitespace, and may have missing `minor` or `patch` components. ----@param v2 string # The second version string to compare. Similar format and rules to `v1`. +---@param v1 string # The first version string to compare. May include leading "v" and whitespace, and may have missing `minor` or `patch` components +---@param v2 string # The second version string to compare. Similar format and rules to `v1` --- ---@return -1|0|1 # --- - `-1` if `v1` is older than `v2`. @@ -116,11 +114,11 @@ end --- ----Get the file name of the current script without folders or extension. E.g. `util.util` +---Gets the file name of the current script without folders or extension. E.g. `util.util` --- ----@param sep? string # Separator character, default is '.' ----@param level? (integer|function)? # Function level, [View documents](command:extension.lua.doc?["en-us/51/manual.html/pdf-debug.getinfo"]) ----@return string +---@param sep? string # Separator character (default: `.`) +---@param level? integer|function # Function level, [View documents](command:extension.lua.doc?["en-us/51/manual.html/pdf-debug.getinfo"]) +---@return string # File name function GetScriptFile(sep, level) sep = sep or "." local sys_sep = package.config:sub(1,1) @@ -137,9 +135,9 @@ function GetScriptFile(sep, level) end --- ----Get the list of enabled addons from the `default_enabled_addons_list` Convar. +---Gets the list of enabled addons from the `default_enabled_addons_list` convar. --- ----@return string[] +---@return string[] # List of enabled addons function GetEnabledAddons() local addons = {} ---@type string @@ -151,7 +149,16 @@ function GetEnabledAddons() end --- ----Get if the given `handle` value is an entity, regardless of if it's still alive. +---Checks if the addon with the given `workshopID` is enabled. +--- +---@param workshopID string # The workshop ID of the addon +---@return boolean # `true` if the addon is enabled, `false` otherwise +function IsAddonEnabled(workshopID) + return vlua.find(GetEnabledAddons(), workshopID) ~= nil +end + +--- +---Checks if the given `handle` value is an entity, regardless of if it's still alive. --- ---A common usage is replacing the often used entity check: --- @@ -161,34 +168,34 @@ end --- --- if IsEntity(entity, true) then --- ----@param handle EntityHandle|any ----@param checkValidity? boolean # Optionally check validity with IsValidEntity. +---@param value EntityHandle|any # The value to check +---@param checkValidity? boolean # Optionally check validity with [IsValidEntity](lua://IsValidEntity) ---@return boolean -function IsEntity(handle, checkValidity) - return (type(handle) == "table" and handle.__self and type(handle.__self) == "userdata") and (not checkValidity or IsValidEntity(handle)) +function IsEntity(value, checkValidity) + return (type(value) == "table" and value.__self and type(value.__self) == "userdata") and (not checkValidity or IsValidEntity(value)) end --- ----Add an output to a given entity `handle`. +---Adds an output to a given `entity`. --- ----@param handle EntityHandle|string # The entity to add the `output` to. ----@param output string # The output name to add. ----@param target EntityHandle|string # The entity the output should target, either handle or targetname. ----@param input string # The input name on `target`. ----@param parameter? string # The parameter override for `input`. ----@param delay? number # Delay for the output in seconds. ----@param activator? EntityHandle # Activator for the output. ----@param caller? EntityHandle # Caller for the output. ----@param fireOnce? boolean # If the output should only fire once. -function AddOutput(handle, output, target, input, parameter, delay, activator, caller, fireOnce) +---@param entity EntityHandle|string # The entity to add the `output` to +---@param output string # The output name to add +---@param target EntityHandle|string # The entity the output should target, either handle or targetname +---@param input string # The input name on `target` +---@param parameter? string # The parameter override for `input` (default: `""`) +---@param delay? number # Delay for the output in seconds (default: `0`) +---@param activator? EntityHandle # Activator for the output (default: `nil`) +---@param caller? EntityHandle # Caller for the output (default: `nil`) +---@param fireOnce? boolean # If the output should only fire once (default: `false`) +function AddOutput(entity, output, target, input, parameter, delay, activator, caller, fireOnce) if IsEntity(target) then target = target:GetName() end parameter = parameter or "" delay = delay or 0 local output_str = output..">"..target..">"..input..">"..parameter..">"..delay..">"..(fireOnce and 1 or -1) - if type(handle) == "string" then - DoEntFire(handle, "AddOutput", output_str, 0, activator or nil, caller or nil) + if type(entity) == "string" then + DoEntFire(entity, "AddOutput", output_str, 0, activator or nil, caller or nil) else - DoEntFireByInstanceHandle(handle, "AddOutput", output_str, 0, activator or nil, caller or nil) + DoEntFireByInstanceHandle(entity, "AddOutput", output_str, 0, activator or nil, caller or nil) end end CBaseEntity.AddOutput = AddOutput @@ -196,8 +203,8 @@ CBaseEntity.AddOutput = AddOutput --- ---Checks if the module/script exists. --- ----@param name? string ----@return boolean +---@param name? string # Name of the module +---@return boolean # `true` if the module exists, `false` otherwise ---@diagnostic disable-next-line: lowercase-global function module_exists(name) if name == nil then return false end @@ -221,9 +228,9 @@ end --- ---If the module fails to load then the callback is not executed and no error is thrown, but a warning is displayed in the console. --- ----@param modname string ----@param callback fun(mod_result: unknown)? ----@return unknown +---@param modname string # Name of the module/script +---@param callback fun(mod_result: unknown)? # Callback function +---@return unknown|nil # The value returned by the module, or `nil` if the module fails to load ---@diagnostic disable-next-line: lowercase-global function ifrequire(modname, callback) ---@TODO: Consider using module_exists @@ -238,33 +245,33 @@ function ifrequire(modname, callback) if callback then callback(result) - return result end - return nil + + return result end --- ----Execute a script file. Included in the current scope by default. +---Executes a script file. Included in the current scope by default. --- ----@param scriptFileName string ----@param scope? ScriptScope ----@return boolean +---@param scriptFileName string # Name of the script +---@param scope? ScriptScope # Scope to include the script in +---@return boolean # `true` if the script was successfully included, `false` otherwise function IncludeScript(scriptFileName, scope) return DoIncludeScript(scriptFileName, scope or getfenv(2)) end --- ----Gets if the game was started in VR mode. +---Checks if the game was started in VR mode. --- ----@return boolean +---@return boolean # `true` if the game was started in VR mode, `false` otherwise function IsVREnabled() return GlobalSys:CommandLineCheck('-vr') end --- ----Gets if the game was started with `+vr_enable_fake_vr 1`. +---Checks if the game was started with `+vr_enable_fake_vr 1`. --- ----@return boolean +---@return boolean # `true` if the game was started in fake VR mode, `false` otherwise function IsFakeVREnabled() return GlobalSys:CommandLineInt("+vr_enable_fake_vr", 0) == 1 end @@ -272,7 +279,7 @@ end --- ---Prints all arguments with spaces between instead of tabs. --- ----@param ... any +---@param ... any # Any number of arguments ---@diagnostic disable-next-line: lowercase-global function prints(...) local args = {...} @@ -292,7 +299,7 @@ end --- ---Prints all arguments on a new line instead of tabs. --- ----@param ... any +---@param ... any # Any number of arguments ---@diagnostic disable-next-line: lowercase-global function printn(...) local args = {...} @@ -304,7 +311,7 @@ end --- ---Prints all arguments if convar "developer" is greater than 0. --- ----@param ... any +---@param ... any # Any number of arguments ---@diagnostic disable-next-line: lowercase-global function devprint(...) if Convars:GetInt("developer") > 0 then @@ -315,7 +322,7 @@ end --- ---Prints all arguments on a new line instead of tabs if convar "developer" is greater than 0. --- ----@param ... any +---@param ... any # Any number of arguments ---@diagnostic disable-next-line: lowercase-global function devprints(...) if Convars:GetInt("developer") > 0 then @@ -326,7 +333,7 @@ end --- ---Prints all arguments with spaces between instead of tabs if convar "developer" is greater than 0. --- ----@param ... any +---@param ... any # Any number of arguments ---@diagnostic disable-next-line: lowercase-global function devprintn(...) if Convars:GetInt("developer") > 0 then @@ -337,7 +344,7 @@ end --- ---Prints all arguments if convar "developer" is greater than 1. --- ----@param ... any +---@param ... any # Any number of arguments ---@diagnostic disable-next-line: lowercase-global function devprint2(...) if Convars:GetInt("developer") > 1 then @@ -348,7 +355,7 @@ end --- ---Prints all arguments on a new line instead of tabs if convar "developer" is greater than 1. --- ----@param ... any +---@param ... any # Any number of arguments ---@diagnostic disable-next-line: lowercase-global function devprints2(...) if Convars:GetInt("developer") > 1 then @@ -359,7 +366,7 @@ end --- ---Prints all arguments with spaces between instead of tabs if convar "developer" is greater than 1. --- ----@param ... any +---@param ... any # Any number of arguments ---@diagnostic disable-next-line: lowercase-global function devprintn2(...) if Convars:GetInt("developer") > 1 then @@ -370,7 +377,7 @@ end --- ---Prints a warning in the console, along with a vscript print if inside tools mode. --- ----@param ... any +---@param ... any # Any number of arguments function warn(...) local str = table.concat({...}, " ") Warning(str .. "\n") @@ -383,7 +390,7 @@ end ---Prints a warning in the console, along with a vscript print if inside tools mode. ---But only if convar "developer" is greater than 0. --- ----@param ... any +---@param ... any # Any number of arguments ---@diagnostic disable-next-line: lowercase-global function devwarn(...) if Convars:GetInt("developer") > 0 then @@ -409,9 +416,9 @@ end --- -- Or with alternate name --- Expose(TriggerRelay, "RelayInput") --- ----@param func function # The function to expose. ----@param name? string # Optionally the name of the function for faster processing. ----@param scope? table # Optionally the explicit scope to put the exposed function in. +---@param func function # The function to expose +---@param name? string # Optionally the name of the function for faster processing +---@param scope? table # Optionally the explicit scope to put the exposed function in function Expose(func, name, scope) local fenv = getfenv(func) -- if name is empty then find the name @@ -447,10 +454,10 @@ local base_vector = Vector() local vector_meta = getmetatable(base_vector) --- ----Get if a value is a `Vector` +---Checks if a value is a [Vector](lua://Vector). --- ----@param value any ----@return boolean +---@param value any # Value to check +---@return boolean # `true` if the value is a [Vector](lua://Vector), `false` otherwise function IsVector(value) return getmetatable(value) == vector_meta end @@ -459,10 +466,10 @@ local base_qangle = QAngle() local qangle_meta = getmetatable(base_qangle) --- ----Get if a value is a `QAngle` +---Checks if a value is a [QAngle](lua://QAngle). --- ----@param value any ----@return boolean +---@param value any # Value to check +---@return boolean # `true` if the value is a [QAngle](lua://QAngle), `false` otherwise function IsQAngle(value) return getmetatable(value) == qangle_meta end @@ -475,8 +482,8 @@ end ---`Vector`, ---`QAngle` --- ----@param tbl table ----@return table +---@param tbl table # Table to copy +---@return table # A copy of `tbl` function DeepCopyTable(tbl) -- print("Deep copy inside", tbl) local t = {} @@ -502,11 +509,12 @@ end --- ---Searches for `value` in `tbl` and sets the associated key to `nil`, returning the key if found. --- ----If your table is an array you should use `ArrayRemove` instead. +---If your table is an array you should use [ArrayRemove](lua://ArrayRemove) instead. --- ----@param tbl table ----@param value any ----@return any +---@see ArrayRemove +---@param tbl table # Table to search +---@param value any # Value to search for +---@return any # The key of `value` if found, `nil` otherwise function TableRemove(tbl, value) local k = vlua.find(tbl, value) if k then @@ -519,9 +527,9 @@ end --- ---Returns a random key/value pair from a unordered table. --- ----@param tbl table # Table to get a random pair from. ----@return any key # Random key selected. ----@return any value # Value linked to the random key. +---@param tbl table # Table to get a random pair from +---@return any key # Random key selected +---@return any value # Value linked to the random key function TableRandom(tbl) local count = 0 local selectedKey @@ -539,11 +547,11 @@ function TableRandom(tbl) end --- ----Returns all the keys of a table as a new ordered array. +---Returns all keys of a table as a new ordered array. --- ---@generic K ----@param tbl table ----@return K[] +---@param tbl table # Table to get keys from +---@return K[] # List of keys function TableKeys(tbl) local array = {} for key, _ in pairs(tbl) do @@ -553,11 +561,11 @@ function TableKeys(tbl) end --- ----Returns all the value of a table as a new ordered array. +---Returns all values in a table as a new ordered array. --- ---@generic V ----@param tbl table ----@return V[] +---@param tbl table # Table to get values from +---@return V[] # List of values function TableValues(tbl) local array = {} for _, value in pairs(tbl) do @@ -569,26 +577,57 @@ end --- ---Returns the size of a table by counting all keys. --- ----@param tbl table # The table to count. ----@return number # The size of the table. +---@param tbl table # The table to count +---@return number # The size of the table function TableSize(tbl) if tbl == nil then return 0 end local size = 0 - for _, l in pairs(tbl) do + for _, _ in pairs(tbl) do size = size + 1 end return size end +--- +---Collects all values for a specific key from a list of tables. +--- +---@param tbl table[] # List of tables +---@param key any # Key to get values from +---@return any[] # List of values found for `key` +function TablePluck(tbl, key) + local out = {} + for _, v in ipairs(tbl) do + if type(v) == "table" and v[key] ~= nil then + table.insert(out, v[key]) + end + end + return out +end + +--- +---Returns the key of the first value that matches the predicate. +--- +---@param tbl table # Table to search +---@param predicate fun(value:any):boolean # Predicate function +---@return any # Key of the first value that matches the predicate +function TableFindKey(tbl, predicate) + for i, v in pairs(tbl) do + if predicate(v) then + return i + end + end + return nil +end + --- ---Returns a random value from an array. --- ---@generic T ----@param array T[] # Array to get a value from. ----@param min? integer # Optional minimum bound. ----@param max? integer # Optional maximum bound. ----@return T one # The random value. ----@return integer two # The random index. +---@param array T[] # Array to get a value from +---@param min? integer # Optional minimum bound +---@param max? integer # Optional maximum bound +---@return T one # The random value +---@return integer two # The random index function ArrayRandom(array, min, max) local i = RandomInt(min or 1, max or #array) return array[i], i @@ -599,7 +638,7 @@ end --- ---@source https://stackoverflow.com/a/68486276 --- ----@param array any[] +---@param array any[] # Array to shuffle function ArrayShuffle(array) for i = #array, 2, -1 do local j = RandomInt(1, i) @@ -613,9 +652,9 @@ end ---This is exponentially faster than `table.remove` for large arrays. --- ---@generic T ----@param array T # The array to remove from. ----@param pos integer # Position to remove at. ----@return T # The same array passed in. +---@param array T # The array to remove from +---@param pos integer # Position to remove at +---@return T # The same array passed in function ArrayRemove(array, pos) local j, n = 1, #array @@ -671,8 +710,8 @@ end ---@generic T1 ---@generic T2 ---@param array1 T1[] # Base array ----@param array2 T2[] # Array which will be appended onto the base array. ----@return T1[]|T2[] # The new appended array. +---@param array2 T2[] # Array which will be appended onto the base array +---@return T1[]|T2[] # The new appended array function ArrayAppend(array1, array2) array1 = vlua.clone(array1) for _, v in ipairs(array2) do @@ -688,8 +727,8 @@ end --- ---@generic T ---@param array T[] # Base array ----@param ... T[] # Any arrays to add. ----@return T[] # The new appended array. +---@param ... T[] # Any arrays to add +---@return T[] # The new appended array function ArrayAppends(array, ...) array = vlua.clone(array) for _, tbl in ipairs({...}) do @@ -701,26 +740,24 @@ function ArrayAppends(array, ...) end ---@class TraceTableLineExt : TraceTableLine ----@field ignore (EntityHandle|EntityHandle[])? # Entity or array of entities to ignore. ----@field ignoreclass (string|string[])? # Class or array of classes to ignore. ----@field ignorename (string|string[])? # Name or array of names to ignore. ----@field timeout integer? # Maxmimum number of traces before returning regardless of parameters. ----@field traces integer # Number of traces done. ----@field dontignore EntityHandle # A single entity to always hit, ignoring if it exists in `ignore`. +---@field ignore (EntityHandle|EntityHandle[])? # Entity or array of entities to ignore +---@field ignoreclass (string|string[])? # Class or array of classes to ignore +---@field ignorename (string|string[])? # Name or array of names to ignore +---@field timeout integer? # Maxmimum number of traces before returning regardless of parameters +---@field traces integer # Number of traces done +---@field dontignore EntityHandle # A single entity to always hit, ignoring if it exists in `ignore` --- ---Does a raytrace along a line with extended parameters. ---You ignore multiple entities as well as classes and names. ---Because the trace has to be redone multiple times, a `timeout` parameter can be defined to cap the number of traces. --- ----@param parameters TraceTableLineExt ----@return boolean +---@param parameters TraceTableLineExt # Trace parameters +---@return boolean # `true` if the trace was successful function TraceLineExt(parameters) if IsEntity(parameters.ignore) then - ---@diagnostic disable-next-line: inject-field parameters.ignoreent = {parameters.ignore} else - ---@diagnostic disable-next-line: inject-field parameters.ignoreent = parameters.ignore parameters.ignore = nil end @@ -763,10 +800,10 @@ function TraceLineExt(parameters) end --- ----Does a raytrace along a line until it hits or the world or reaches the end of the line. +---Does a raytrace along a line until it hits the world or reaches the end of the line. --- ----@param parameters TraceTableLine ----@return TraceTableLine +---@param parameters TraceTableLine # Trace parameters +---@return TraceTableLine # Trace parameters with results function TraceLineWorld(parameters) local result = TraceLine(parameters) while parameters.hit and parameters.enthit:GetClassname() ~= "worldent" do @@ -781,8 +818,8 @@ end --- ---Does a raytrace along a line until it hits the specified entity or reaches the end of the line. --- ----@param parameters TraceTableLine ----@return TraceTableLine +---@param parameters TraceTableLine # Trace parameters +---@return TraceTableLine # Trace parameters with results function TraceLineEntity(ent, parameters) local result = TraceLine(parameters) while parameters.hit and parameters.enthit ~= ent do @@ -794,12 +831,14 @@ function TraceLineEntity(ent, parameters) return parameters end +--- ---Performs a simple line trace and returns the trace table. ----@param startpos Vector ----@param endpos Vector ----@param ignore? EntityHandle ----@param mask? integer ----@return TraceTableLine +--- +---@param startpos Vector # Start position +---@param endpos Vector # End position +---@param ignore? EntityHandle # Entity to ignore +---@param mask? integer # Trace mask +---@return TraceTableLine # Trace table with results function TraceLineSimple(startpos, endpos, ignore, mask) ---@type TraceTableLine local traceTable = { @@ -813,18 +852,18 @@ function TraceLineSimple(startpos, endpos, ignore, mask) end --- ----Get if an entity is the world entity. +---Checks if an entity is the world entity. --- ----@param entity EntityHandle ----@return boolean +---@param entity EntityHandle # Entity to check +---@return boolean # `true` if the entity is the world entity, `false` otherwise function IsWorld(entity) return IsEntity(entity) and entity:GetClassname() == "worldent" end --- ----Get the world entity. +---Gets the world entity. --- ----@return EntityHandle +---@return EntityHandle # World entity function GetWorld() return Entities:FindByClassname(nil, "worldent") end @@ -861,9 +900,11 @@ local physicsClasses = { "item_hlvr_weapon_generic_pistol", } ----Get if an entity is a physical entity. ----@param entity EntityHandle ----@return boolean +--- +---Checks if an entity is a physical entity. +--- +---@param entity EntityHandle # Entity to check +---@return boolean # `true` if the entity is a physical entity, `false` otherwise function IsPhysicsObject(entity) if IsEntity(entity) then if entity:GetClassname() == "prop_animinteractable" then @@ -879,11 +920,11 @@ function IsPhysicsObject(entity) end --- ----Get if a table has a key (this essentially the same as tbl[key] ~= nil). +---Checks if a table has a key (this essentially the same as tbl[key] ~= nil). --- ----@param tbl table ----@param key any ----@return boolean +---@param tbl table # Table to check +---@param key any # Key to look for +---@return boolean # `true` if the table has the key, `false` otherwise ---@luadoc-ignore ---@diagnostic disable-next-line:lowercase-global function haskey(tbl, key) @@ -894,26 +935,26 @@ function haskey(tbl, key) end --- ----Check if a value is truthy or falsy. +---Checks if a value is truthy or falsy. --- --- **falsy == `nil`|`false`|`0`|`""`|`{}`** --- ----@param value any # The value to be checked. ----@return boolean # Returns true if the value is truthy, false otherwise. +---@param value any # The value to be checked +---@return boolean # `true` if the value is truthy, `false` otherwise ---@diagnostic disable-next-line:lowercase-global function truthy(value) return not (value == nil or value == false or value == 0 or value == "" or value == "0" or value == "false" or (type(value) == "table" and next(value) == nil)) end --- ----Search an entity for a key using a search pattern. E.g. "getclass" will find "GetClassname" +---Searches an entity for a key using a search pattern. E.g. "getclass" will find "GetClassname" --- ---Works with `class.lua` EntityClass entities. --- ----@param entity EntityHandle|EntityClass ----@param searchPattern string ----@return string? key # The full name of the first key matching `searchPattern`. ----@return any? value # The value of the key found. +---@param entity EntityHandle|EntityClass # The entity to search +---@param searchPattern string # The name pattern to search for +---@return string? key # The full name of the first key matching `searchPattern` +---@return any? value # The value of the key found function SearchEntity(entity, searchPattern) searchPattern = searchPattern:lower() @@ -955,10 +996,10 @@ end --- ---Linearly interpolates between two angles. --- ----@param t number # The interpolation parameter, where 0 returns angle_start and 1 returns angle_end. ----@param angle_start number # The starting angle in degrees. ----@param angle_end number # The ending angle in degrees. ----@return number # The interpolated angle. +---@param t number # The interpolation parameter, where `0` returns `angle_start` and `1` returns `angle_end` +---@param angle_start number # The starting angle in degrees +---@param angle_end number # The ending angle in degrees +---@return number # The interpolated angle function LerpAngle(t, angle_start, angle_end) angle_start = angle_start % 360 angle_end = angle_end % 360 @@ -972,22 +1013,30 @@ function LerpAngle(t, angle_start, angle_end) return interpolated_angle end +--- +---Calculates the closest point on an entity's OBB to a position. +--- +---This is a modified version of the original function that fixes +---off-center models not calculating correctly. +--- +---@param entity EntityHandle # The entity to calculate the closest point on +---@param position Vector # The position to calculate the closest point to +---@return Vector # The closest point on the entity's OBB to the position function CalcClosestPointOnEntityOBBAdjusted(entity, position) local org = entity:GetOrigin() entity:SetOrigin(org + (entity:GetCenter() - entity:GetOrigin())) local calcpos = CalcClosestPointOnEntityOBB(entity, position) entity:SetOrigin(org) return calcpos - -- return calcpos + (entity:GetCenter() - entity:GetOrigin()) end --- ---Assigns a default value to a table which will be returned if an invalid key is accessed. --- ---@generic T ----@param tbl T # The table to which the default value will be assigned. ----@param default any # The default value to be returned for invalid keys. ----@return T # The table with the default value assigned. +---@param tbl T # The table to which the default value will be assigned +---@param default any # The default value to be returned for invalid keys +---@return T # The table with the default value assigned function DefaultTable(tbl, default) return setmetatable(tbl, { __index = function () @@ -999,10 +1048,10 @@ end --- ---Wraps a value within a specified range. --- ----@param value number The value to be wrapped. ----@param min number # The minimum value of the range. ----@param max number # The maximum value of the range. ----@return number # The wrapped value within the specified range. +---@param value number The value to be wrapped +---@param min number # The minimum value of the range +---@param max number # The maximum value of the range +---@return number # The wrapped value within the specified range function Wrap(value, min, max) local range = max - min + 1 return ((value - min) % range) + min @@ -1027,9 +1076,9 @@ end --- return 0 --- end, 0) --- ----@param on? fun(...): any # Function called when the condition is true. ----@param off? fun(...): any # Function called when the condition is false. ----@return fun(condition: boolean, ...): any # The created toggle function. +---@param on? fun(...): any # Function called when the condition is true +---@param off? fun(...): any # Function called when the condition is false +---@return fun(condition: boolean, ...): any # The created toggle function function CreateToggleBehavior(on, off) local flag = false @@ -1053,10 +1102,10 @@ function CreateToggleBehavior(on, off) end --- ----Compute the closest corner relative to a vector on the AABB of an entity. +---Computes the closest corner relative to a vector on the AABB of an entity. --- ----@param entity EntityHandle ----@param position Vector +---@param entity EntityHandle # The entity to calculate the closest corner on +---@param position Vector # The vector to calculate the closest corner to function CalcClosestCornerOnEntityAABB(entity, position) local corners = entity:GetBoundingCorners(true) local closestCorner = corners[1] @@ -1071,4 +1120,32 @@ function CalcClosestCornerOnEntityAABB(entity, position) return closestCorner end +--- +---Sets the absolute world velocity of an entity. +--- +---@param velocity Vector The target velocity in units/second. +function SetPhysVelocity(entity, velocity) + entity:SetAbsVelocity(velocity) +end + +--- +---Returns one of two values based on a percentage chance. +--- +---If the random roll succeeds, returns `onTrue` (default: true). +---If it fails, returns `onFalse` (default: false). +--- +---@param chance number # The percentage chance of success (0-100) +---@param onTrue? any # Value to return if the chance succeeds (default: `true`) +---@param onFalse? any # Value to return if the chance fails (default: `false`) +---@return boolean|any +function RandomChance(chance, onTrue, onFalse) + if onTrue == nil then onTrue = true end + if onFalse == nil then onFalse = false end + if RandomFloat(0, 100) <= chance then + return onTrue + else + return onFalse + end +end + return _version \ No newline at end of file diff --git a/scripts/vscripts/alyxlib/helpers/animation.lua b/scripts/vscripts/alyxlib/helpers/animation.lua index 7795e75..5405f17 100644 --- a/scripts/vscripts/alyxlib/helpers/animation.lua +++ b/scripts/vscripts/alyxlib/helpers/animation.lua @@ -1,10 +1,10 @@ --[[ - v1.2.0 + v1.2.1 https://github.com/FrostSource/alyxlib Provides functionality for animating getter/setter entity methods using curves. - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: require "alyxlib.helpers.animation" ]] @@ -15,7 +15,7 @@ ---@diagnostic disable: undefined-field ---@class Animation Animation = {} -Animation.version = "v1.2.0" +Animation.version = "v1.2.1" local localBounceOut = function(a, b, t) local normalizedT = t @@ -37,6 +37,9 @@ local localBounceIn = function(a, b, t) return a + (b - a) * (1 - localBounceOut(0, 1, 1 - t)) end +--- +---Curves for animating entity properties and values. +--- ---@enum Animation.Curves Animation.Curves = { linear = function(a, b, t) diff --git a/scripts/vscripts/alyxlib/helpers/easyconvars.lua b/scripts/vscripts/alyxlib/helpers/easyconvars.lua index ae7d0ac..5266540 100644 --- a/scripts/vscripts/alyxlib/helpers/easyconvars.lua +++ b/scripts/vscripts/alyxlib/helpers/easyconvars.lua @@ -1,10 +1,10 @@ --[[ - v2.0.0 + v2.1.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. - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: require "alyxlib.helpers.easyconvars" ]] @@ -16,23 +16,27 @@ require "alyxlib.globals" --- ---@class EasyConvars EasyConvars = {} -EasyConvars.version = "v2.0.0" +EasyConvars.version = "v2.1.0" +--- ---Data of a registered convar. +--- ---@class EasyConvarsRegisteredData ----@field type EasyConvarsType # Type of the convar. +---@field type EasyConvarsType # Type of the convar ---@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. ----@field wasChangedByUser boolean # Whether the value was changed by the user. ----@field displayFunc? fun(val:any) # The function called when the convar is called without any parameters. By default it just prints the value. ----@field defaultValue? string # The default value of the convar given by the registration. Not the value set in cfg or launch options. +---@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 +---@field wasChangedByUser boolean # Whether the value was changed by the user +---@field displayFunc? fun(val:any) # The function called when the convar is called without any parameters. By default it just prints the value +---@field defaultValue? string # The default value of the convar given by the registration. Not the value set in cfg or launch options +--- ---Types of convars. +--- ---@alias EasyConvarsType ---| `EASYCONVARS_CONVAR` ---| `EASYCONVARS_COMMAND` @@ -54,6 +58,7 @@ EasyConvars.registered = {} EasyConvars._isLoading = true ---The function called after all convars have been initialized. +---@deprecated local postInitializer = nil ---Converts any value to "1" or "0" depending on whether it represents true or false. @@ -175,10 +180,10 @@ end, nil) --- - [EasyConvars:RegisterToggle](lua://EasyConvars.RegisterToggle) --- - [EasyConvars:RegisterCommand](lua://EasyConvars.RegisterCommand) --- ----@param ctype EasyConvarsType # Type of the convar. +---@param ctype EasyConvarsType # Type of the convar ---@param name string # Name of the convar ----@param defaultValue any|fun():any # Will be converted to a string. If given a function, the value will be determined on player spawn. ----@param onUpdate? (fun(val:string, ...):any?)|(fun(newVal:string, oldVal:string):any) # Optional callback function. +---@param defaultValue any|fun():any # Will be converted to a string. If given a function, the value will be determined on player spawn +---@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 ---@return EasyConvarsRegisteredData # The new registered cvar data @@ -257,7 +262,7 @@ end --- ---This is done automatically when the value is changed if SetPersistent is set to true. --- ----@param name string # The name of the convar to save. +---@param name string # The name of the convar to save function EasyConvars:Save(name) if not self.registered[name] then return end @@ -277,8 +282,8 @@ end --- ---This is done automatically when the player spawns for any previously saved convar. --- ----@param name string # The name of the convar to load. ----@return boolean # Returns true if the convar was loaded successfully. +---@param name string # The name of the convar to load +---@return boolean # `true` if the convar was loaded successfully function EasyConvars:Load(name) local cvar = self.registered[name] if not cvar then return false end @@ -307,8 +312,8 @@ end --- ---It will be saved to the player when changed and load its previous state when the player spawns. --- ----@param name string # The name of the convar. ----@param persistent boolean # Whether the convar should be persistent. +---@param name string # The name of the convar +---@param persistent boolean # `true` if the convar should be persistent function EasyConvars:SetPersistent(name, persistent) if not self.registered[name] then return end self.registered[name].persistent = persistent @@ -372,11 +377,20 @@ function EasyConvars:GetConvarData(name) return self.registered[name] end +--- +---Checks if an easy convar exists. +--- +---@param name string # Name of the convar +---@return boolean # Returns true if the convar exists +function EasyConvars:Exists(name) + return self.registered[name] ~= nil +end + --- ---Returns the convar as a string. --- ---@param name string # Name of the convar ----@return string? # The value of the convar as a string or nil if convar does not exist +---@return string? # The value of the convar as a string or `nil` if convar does not exist function EasyConvars:GetStr(name) local cvar = self.registered[name] if not cvar then return nil end @@ -392,7 +406,7 @@ end ---Returns the convar as a boolean. --- ---@param name string # Name of the convar ----@return boolean? # The value of the convar as a boolean or nil if convar does not exist +---@return boolean? # The value of the convar as a boolean or `nil` if convar does not exist function EasyConvars:GetBool(name) if not self.registered[name] then return nil end return truthy(self:GetStr(name)) @@ -402,7 +416,7 @@ end ---Returns the convar as a float. --- ---@param name string # Name of the convar ----@return number? # The value of the convar as a number or nil if convar does not exist +---@return number? # The value of the convar as a number or `nil` if convar does not exist function EasyConvars:GetFloat(name) if not self.registered[name] then return nil end return tonumber(self:GetStr(name)) or 0 @@ -412,7 +426,7 @@ end ---Returns the convar as an integer. --- ---@param name string # Name of the convar ----@return integer? # The value of the convar as a truncated number or nil if convar does not exist +---@return integer? # The value of the convar as a truncated number or `nil` if convar does not exist function EasyConvars:GetInt(name) if not self.registered[name] then return nil end return math.floor(self:GetFloat(name)) @@ -474,7 +488,7 @@ function EasyConvars:SetRaw(name, value) end --- ----Get if the convar was changed by the user in the console. +---Gets if the convar was changed by the user in the console or via CFG. --- ---@param name string # Name of the convar function EasyConvars:WasChangedByUser(name) @@ -486,11 +500,11 @@ end --- ---Forces the convar to be considered changed by the user. --- ----This can be useful if you want to stop a convar from initializing because you're setting its value early. ----Or if you're setting the value by some unusual means for the user. +---This can be useful if you want to stop a convar from initializing because you're setting its value early +---or if you're setting the value by some unusual means for the user. --- ----@param name string ----@param wasChanged boolean +---@param name string # Name of the convar +---@param wasChanged boolean # `true` to mark the convar as changed by the user function EasyConvars:SetWasChanged(name, wasChanged) local reg = self.registered[name] if not reg then return end @@ -500,8 +514,10 @@ end --- ---Sets the value of the convar if it hasn't been changed by the user. --- +---@see EasyConvars.WasChangedByUser +---@see EasyConvars.SetWasChanged ---@param name string ----@param value any # The value to set. Will be converted to a string representation. +---@param value any # The value to set - will be converted to a string representation function EasyConvars:SetIfUnchanged(name, value) local reg = self.registered[name] if not reg then return end @@ -510,20 +526,34 @@ function EasyConvars:SetIfUnchanged(name, value) end end +---@type function[] +local postInitializers = {} + +--- +---Adds a function to be called after all convars have been initialized. +--- +---Useful for setting convars that are dependent on other convars. +--- +---@param func function # The function to call +function EasyConvars:AddPostInitializer(func) + table.insert(postInitializers, func) +end + +--- +---Adds a function to be called after all convars have been initialized. --- ----Sets the function to be called after all convars have been initialized. +---**Deprecated: Use [AddPostInitializer](lua://EasyConvars.AddPostInitializer) instead** --- ----@param func function +---@deprecated +---@param func function # The function to call function EasyConvars:SetPostInitializer(func) - postInitializer = func + self:AddPostInitializer(func) end --- Check which version of listener is available -local listener = ListenToPlayerEvent -if listener == nil then - listener = ListenToGameEvent -end +-- Use best available listener +local listener = ListenToPlayerEvent or ListenToGameEvent + listener("player_activate", function (params) Player:Delay(function () @@ -544,8 +574,8 @@ listener("player_activate", function (params) end EasyConvars._isLoading = false - if postInitializer then - postInitializer() + for _, func in pairs(postInitializers) do + func() end end) diff --git a/scripts/vscripts/alyxlib/init.lua b/scripts/vscripts/alyxlib/init.lua index 4b5439b..545b4b7 100644 --- a/scripts/vscripts/alyxlib/init.lua +++ b/scripts/vscripts/alyxlib/init.lua @@ -1,5 +1,5 @@ --[[ - v1.2.2 + v1.2.3 https://github.com/FrostSource/alyxlib The main initializer script loads any standard libraries that it can find. @@ -11,12 +11,12 @@ ]] -- Version of this file -local version = "v1.2.2" +local version = "v1.2.3" ---ID of AlyxLib in the Steam workshop ALYXLIB_WORKSHOP_ID = "3329679071" ---The current version of AlyxLib as a whole -ALYXLIB_VERSION = "v2.0.0" +ALYXLIB_VERSION = "v3.0.0" print("Initializing AlyxLib system ".. ALYXLIB_VERSION .." ...") @@ -61,6 +61,7 @@ alyxlib_require "alyxlib.extensions.npc" alyxlib_require "alyxlib.extensions.template" alyxlib_require "alyxlib.math.common" alyxlib_require "alyxlib.math.weighted_random" +alyxlib_require "alyxlib.math.obb" alyxlib_require "alyxlib.data.queue" alyxlib_require "alyxlib.data.stack" alyxlib_require "alyxlib.data.inventory" @@ -88,12 +89,14 @@ alyxlib_require "alyxlib.panorama.core" alyxlib_require "alyxlib.debug.common" alyxlib_require "alyxlib.debug.commands" alyxlib_require "alyxlib.debug.controller" -if IsVREnabled() then +if IsVREnabled() or IsFakeVREnabled() then alyxlib_require "alyxlib.debug.vr" else alyxlib_require "alyxlib.debug.novr" end alyxlib_require "alyxlib.debug.debug_menu" +alyxlib_require "alyxlib.debug.debug_menu_cfg" +alyxlib_require "alyxlib.debug.menus.debug_menu_alyxlib" -- Common third-party libraries diff --git a/scripts/vscripts/alyxlib/math/common.lua b/scripts/vscripts/alyxlib/math/common.lua index 5360961..463a1b0 100644 --- a/scripts/vscripts/alyxlib/math/common.lua +++ b/scripts/vscripts/alyxlib/math/common.lua @@ -1,42 +1,48 @@ --[[ - v1.3.0 + v2.0.0 https://github.com/FrostSource/alyxlib Extends the math library with useful functions. - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: require "alyxlib.math.common" ]] -local version = "v1.3.0" +local version = "v2.0.0" --- ----Get the sign of a number. +---Gets the sign of a number. --- ----@param x number # The input number. ----@return 1|0|-1 # Returns 1 if the number is positive, -1 if the number is negative, or 0 if the number is zero. +---@param x number # The input number +---@return -1|0|1 # Sign of the number: -1 for negative, 1 for positive, 0 if zero function math.sign(x) return x > 0 and 1 or (x < 0 and -1 or 0) end --- ----Truncates a number to the specified number of decimal places. +---Truncates a number to a given number of decimal places. --- ----@param number number # The input number.The input number. ----@param places? integer # The number of decimal places to keep. ----@return number # The input number truncated to the specified decimal places. +---@param number number # The number to truncate +---@param places? integer # Decimal places to truncate to (default: 0) +---@return number # The truncated number function math.trunc(number, places) - local shift = 10 ^ (places or 1) - return math.floor(number * shift) / shift + places = places or 0 + local factor = 10 ^ places + if places >= 0 then + return math.floor(number * factor) / factor + else + factor = 10 ^ (-places) + return math.floor(number / factor) * factor + end end --- ----Rounds a number to the specified number of decimal places. +---Rounds a number to a given number of decimal places. --- ----@param number number # The input number to be rounded. ----@param decimals? integer # The number of decimal places to round to. If not provided, the number will be rounded to the nearest whole number. ----@return number # The input number rounded to the specified decimal places or nearest whole number. +---@param number number # The number to be rounded +---@param decimals? integer # Decimal places to round to (default: 0) +---@return number # The rounded number function math.round(number, decimals) local shift = 10 ^ (decimals or 0) return math.floor(number * shift + 0.5) / shift @@ -45,11 +51,11 @@ end --- ---Checks if two numbers are close to each other within a specified tolerance. --- ----@param a number # The first number to compare. ----@param b number # The second number to compare. ----@param rel_tol? number # The relative tolerance (optional). Defines the maximum allowed relative difference between `a` and `b` as a percentage of the larger of the two values. ----@param abs_tol? number # The absolute tolerance (optional). Defines the maximum allowed fixed difference between `a` and `b`, regardless of their magnitudes. ----@return boolean # Returns `true` if the numbers are considered close based on the specified tolerances; otherwise, returns `false`. +---@param a number # The first number to compare +---@param b number # The second number to compare +---@param rel_tol? number # Defines the maximum allowed relative difference between `a` and `b` as a percentage of the larger of the two values +---@param abs_tol? number # Defines the maximum allowed fixed difference between `a` and `b`, regardless of their magnitudes +---@return boolean # `true` if the numbers are considered close based on the specified tolerances, `false` otherwise --- --- **Examples:** --- @@ -82,8 +88,8 @@ end --- ---Checks if a given number has a fractional part (decimal part). --- ----@param number number # The number to check for fractional part. ----@return boolean # True if the number has a fractional part, false otherwise. +---@param number number # The number to check +---@return boolean # `true` if the number has a fractional part, `false` otherwise. function math.has_frac(number) return type(number) == "number" and number ~= math.floor(number) end @@ -91,8 +97,8 @@ end --- ---Returns the fractional part of a number. --- ----@param number number # The number to get the fractional part of. ----@return number # The fractional part of the number. +---@param number number # The number to get the fractional part of +---@return number # The fractional part of the number function math.get_frac(number) return number - math.floor(number) end diff --git a/scripts/vscripts/alyxlib/math/obb.lua b/scripts/vscripts/alyxlib/math/obb.lua new file mode 100644 index 0000000..aea7484 --- /dev/null +++ b/scripts/vscripts/alyxlib/math/obb.lua @@ -0,0 +1,299 @@ +--[[ + v1.0.0 + https://github.com/FrostSource/alyxlib + + Functions for drawing and testing OBB intersections. + + If not using `alyxlib/init.lua`, load this file at game start using the following line: + + require "alyxlib.math.obb" +]] + +local version = "v1.0.0" + +local _abs = math.abs +local insert = table.insert + +--- +---Projects an OBB onto an axis. +--- +---@param centerLocal Vector # The center of the OBB in local space +---@param half Vector # The half extents of the OBB in local space +---@param origin Vector # The origin of the OBB in world space +---@param angles QAngle # The angles of the OBB in world space +---@param axis Vector # The axis to project on +local function projectOBB(centerLocal, half, origin, angles, axis) + local f, r, u = angles:Forward(), angles:Left(), angles:Up() + local centerWorld = origin + f*centerLocal.x + r*centerLocal.y + u*centerLocal.z + local radius = half.x*_abs(f:Dot(axis)) + + half.y*_abs(r:Dot(axis)) + + half.z*_abs(u:Dot(axis)) + return centerWorld:Dot(axis) - radius, centerWorld:Dot(axis) + radius +end + +--- +---OBB data table for use with obb.lua functions. +--- +---@class OBBData +---@field center Vector # The center of the OBB in local space +---@field half Vector # The half extents of the OBB in local space + +--- +---Returns the center and half extents of an OBB in local space. +--- +---@param mins Vector # The local space minimum corner +---@param maxs Vector # The local space maximum corner +---@return OBBData # The OBB data +function GetBoundingOBBData(mins, maxs) + local center = (mins + maxs) * 0.5 + local half = (maxs - mins) * 0.5 + return { + center = center, + half = half + } +end + +--- +---Returns the center and half extents of an entity's OBB in local space. +--- +---@param entity EntityHandle # The entity to get the OBB data for +---@return OBBData # The OBB data +function GetEntityOBBData(entity) + local mins = entity:GetBoundingMins() + local maxs = entity:GetBoundingMaxs() + return GetBoundingOBBData(mins, maxs) +end + +--- +---Returns the world space minimum and maximum corners of an entity's OBB. +--- +---@param entity EntityHandle # The entity to get the AABB for +---@return Vector # The world space minimum corner +---@return Vector # The world space maximum corner +function GetEntityAABB(entity) + local data = GetEntityOBBData(entity) + local origin = entity:GetOrigin() + local ang = entity:GetAngles() + + local f, r, u = ang:Forward(), ang:Left(), ang:Up() + + -- world center of OBB + local center = origin + + f * data.center.x + + r * data.center.y + + u * data.center.z + + -- half extents projected onto world axes + local hx = _abs(f.x) * data.half.x + + _abs(r.x) * data.half.y + + _abs(u.x) * data.half.z + + local hy = _abs(f.y) * data.half.x + + _abs(r.y) * data.half.y + + _abs(u.y) * data.half.z + + local hz = _abs(f.z) * data.half.x + + _abs(r.z) * data.half.y + + _abs(u.z) * data.half.z + + local halfWorld = Vector(hx, hy, hz) + + return center - halfWorld, center + halfWorld +end + +--- +---Tests if two AABBs intersect. +--- +---@param aMin Vector # The minimum corner of the first AABB +---@param aMax Vector # The maximum corner of the first AABB +---@param bMin Vector # The minimum corner of the second AABB +---@param bMax Vector # The maximum corner of the second AABB +---@return boolean # `true` if the AABBs intersect, `false` otherwise +function AABBvsAABB(aMin, aMax, bMin, bMax) + return aMin.x <= bMax.x and aMax.x >= bMin.x and + aMin.y <= bMax.y and aMax.y >= bMin.y and + aMin.z <= bMax.z and aMax.z >= bMin.z +end + +--- +---Tests if two OBBs intersect. +--- +---@param obbDataA OBBData # The data of the first OBB +---@param originA Vector # The world space origin of the first OBB +---@param anglesA QAngle # The angles of the first OBB +---@param obbDataB OBBData # The data of the second OBB +---@param originB Vector # The world space origin of the second OBB +---@param anglesB QAngle # The angles of the second OBB +---@return boolean # `true` if the OBBs intersect, `false` otherwise +function OBBvsOBB(obbDataA, originA, anglesA, obbDataB, originB, anglesB) + local fA, rA, uA = anglesA:Forward(), anglesA:Left(), anglesA:Up() + local fB, rB, uB = anglesB:Forward(), anglesB:Left(), anglesB:Up() + + local axes = {fA, rA, uA, fB, rB, uB} + + for _,a in ipairs({fA, rA, uA}) do + for _,b in ipairs({fB, rB, uB}) do + local cross = a:Cross(b) + if cross:Length() > 0.0001 then + insert(axes, cross:Normalized()) + end + end + end + + for _,axis in ipairs(axes) do + local minA,maxA = projectOBB(obbDataA.center, obbDataA.half, originA, anglesA, axis) + local minB,maxB = projectOBB(obbDataB.center, obbDataB.half, originB, anglesB, axis) + if minA > maxB or minB > maxA then + return false + end + end + + return true +end + +--- +---Tests if an AABB and an OBB intersect. +--- +---@param aabbMin Vector # The minimum corner of the AABB +---@param aabbMax Vector # The maximum corner of the AABB +---@param obbData OBBData # The data of the OBB +---@param obbOrigin Vector # The world space origin of the OBB +---@param obbAngles QAngle # The angles of the OBB +---@return boolean # `true` if the AABB and OBB intersect, `false` otherwise +function AABBvsOBB(aabbMin, aabbMax, obbData, obbOrigin, obbAngles) + + local aCenter = (aabbMin + aabbMax) * 0.5 + local aOBB = { + center = Vector(0,0,0), + half = (aabbMax - aabbMin) * 0.5 + } + + local aAngles = QAngle(0,0,0) + + return OBBvsOBB( + aOBB, aCenter, aAngles, + obbData, obbOrigin, obbAngles + ) +end + +--- +---Tests if a point is inside an AABB. +--- +---@param aMin Vector # The minimum corner of the AABB +---@param aMax Vector # The maximum corner of the AABB +---@param point Vector # The point to test +---@return boolean # `true` if the point is inside the AABB, `false` otherwise +function PointInAABB(aMin, aMax, point) + return point.x >= aMin.x and point.x <= aMax.x and + point.y >= aMin.y and point.y <= aMax.y and + point.z >= aMin.z and point.z <= aMax.z +end + +--- +---Tests if a point is inside an OBB. +--- +---@param obbData OBBData # The OBB data (center, half extents in local space) +---@param origin Vector # The world space origin of the OBB +---@param angles QAngle # The world space orientation of the OBB +---@param point Vector # The point to test +---@return boolean # `true` if the point is inside the OBB, `false` otherwise +function PointInOBB(obbData, origin, angles, point) + local f, r, u = angles:Forward(), angles:Left(), angles:Up() + + -- world position of the OBBโ€™s local center + local center = origin + + f * obbData.center.x + + r * obbData.center.y + + u * obbData.center.z + + -- vector from OBB center to point + local d = point - center + + -- project onto each local axis and compare to half extents + local projF = d:Dot(f) + local projR = d:Dot(r) + local projU = d:Dot(u) + + return math.abs(projF) <= obbData.half.x and + math.abs(projR) <= obbData.half.y and + math.abs(projU) <= obbData.half.z +end + +--- +---Draws an OBB in the world. +--- +---@param obbData OBBData # The data of the OBB +---@param origin Vector # The world space origin of the OBB +---@param angles QAngle # The angles of the OBB +---@param color Vector # The color of the OBB +---@param noDepthTest boolean # True if the OBB should be drawn above all geometry +---@param seconds number # The number of seconds the OBB should be visible for +function DebugDrawOBB(obbData, origin, angles, color, noDepthTest, seconds) + + local f = angles:Forward() + local r = angles:Left() + local u = angles:Up() + + -- world center of the OBB + local center = origin + + f * obbData.center.x + + r * obbData.center.y + + u * obbData.center.z + + -- build corners + local corners = {} + local i = 1 + for sx = -1, 1, 2 do + for sy = -1, 1, 2 do + for sz = -1, 1, 2 do + corners[i] = center + + f * (sx * obbData.half.x) + + r * (sy * obbData.half.y) + + u * (sz * obbData.half.z) + i = i + 1 + end + end + end + + -- edge indices + local edges = { + {1,2}, {2,4}, {4,3}, {3,1}, -- bottom + {5,6}, {6,8}, {8,7}, {7,5}, -- top + {1,5}, {2,6}, {3,7}, {4,8} -- verticals + } + + -- draw lines + for _,e in ipairs(edges) do + DebugDrawLine(corners[e[1]], corners[e[2]], + color.x, color.y, color.z, + noDepthTest or false, seconds or 0.0) + end +end + +--- +---Draws an entity's OBB in the world. +--- +---@param entity EntityHandle # The entity to draw the OBB for +---@param color Vector # The color of the OBB in RGB +---@param noDepthTest boolean # True if the OBB should be drawn above all geometry +---@param seconds number # The number of seconds the OBB should be visible for +function DebugDrawEntityOBB(entity, color, noDepthTest, seconds) + local obbData = GetEntityOBBData(entity) + DebugDrawOBB(obbData, entity:GetOrigin(), entity:GetAngles(), color, noDepthTest, seconds) +end + +--- +---Draws an entity's AABB in the world. +--- +---The AABB is defined by the entity's bounding mins/maxs and its current origin/angles. +--- +---@param entity EntityHandle # The entity to draw the AABB for +---@param color Vector # The color of the AABB in RGB +---@param noDepthTest boolean # True if the AABB should be drawn above all geometry +---@param seconds number # The number of seconds the AABB should be visible for +function DebugDrawEntityAABB(entity, color, noDepthTest, seconds) + local mins, maxs = GetEntityAABB(entity) + debugoverlay:Box(mins, maxs, color.x, color.y, color.z, 255, noDepthTest or false, seconds or 0) +end + +return version \ No newline at end of file diff --git a/scripts/vscripts/alyxlib/math/weighted_random.lua b/scripts/vscripts/alyxlib/math/weighted_random.lua index 5f7cc32..ebe3459 100644 --- a/scripts/vscripts/alyxlib/math/weighted_random.lua +++ b/scripts/vscripts/alyxlib/math/weighted_random.lua @@ -1,15 +1,15 @@ --[[ - v1.2.4 + v1.2.5 https://github.com/FrostSource/alyxlib Weighted random allows you to assign chances to tables keys. - If not using `vscripts/alyxlib/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: require "alyxlib.math.weighted_random" ]] -local version = "v1.2.4" +local version = "v1.2.5" --- ---A list of tables with associated weights. @@ -68,13 +68,13 @@ if pcall(require, "alyxlib.storage") then CBaseEntity.SaveWeightedRandom = Storage.SaveWeightedRandom --- - ---Load a WeightedRandom. + ---Loads a WeightedRandom. --- ---@generic T - ---@param handle EntityHandle # Entity to load from. - ---@param name string # Name the WeightedRandom was saved as. - ---@param default? T # Optional default value. - ---@return WeightedRandom|T + ---@param handle EntityHandle # Entity to load from + ---@param name string # Name the WeightedRandom was saved as + ---@param default? T # Optional default value + ---@return WeightedRandom|T # The loaded WeightedRandom ---@luadoc-ignore Storage.LoadWeightedRandom = function(handle, name, default) local wr = WR.__load(handle, name) @@ -87,23 +87,23 @@ if pcall(require, "alyxlib.storage") then end --- ----Add a table value with an associated weight. +---Adds a table value with an associated weight. --- ---If `tbl` already has a weight key then `weight` parameter can be omitted. --- ----**Note:** The table `tbl` is not cloned, the given reference is used. +---**Note:** The table `tbl` is not cloned, the given reference is inserted into the list. --- ----@param tbl table # Table of values that will be returned. ----@param weight? number # Weight for this table. +---@param tbl table # Table of values that will be returned +---@param weight? number # Weight for this table function WR:Add(tbl, weight) if weight ~= nil then tbl.weight = weight end self.ItemPool[#self.ItemPool+1] = tbl end --- ----Get the sum of all weights in the WeightedRandom. +---Gets the sum of all weights in the WeightedRandom. --- ----@return number # The sum of all weights. +---@return number # The sum of all weights function WR:TotalWeight() local weight_sum = 0 for _,item in ipairs(self.ItemPool) do @@ -115,9 +115,9 @@ function WR:TotalWeight() end --- ----Pick a random table from the list of weighted tables. +---Picks a random table from the list of weighted tables. --- ----@return WeightedRandomItem|table # The chosen table. +---@return WeightedRandomItem|table # The randomly selected table function WR:Random() local weight_sum = self:TotalWeight() local weight_remaining @@ -139,7 +139,7 @@ function WR:Random() end --- ----Create a new WeightedRandom object with given weights. +---Creates a new WeightedRandom instance with given weights. --- ---E.g. --- @@ -151,10 +151,8 @@ end --- { weight = 0.1, name = "Extremely rare" }, --- }) --- ----Params: ---- ----@param weights WeightedRandomItem[] # List of weighted tables. ----@return WeightedRandom # WeightedRandom object. +---@param weights WeightedRandomItem[] # List of weighted tables +---@return WeightedRandom # The new WeightedRandom instance function WeightedRandom(weights) return setmetatable({ItemPool = weights or {}}, WR) end diff --git a/scripts/vscripts/alyxlib/panorama/core.lua b/scripts/vscripts/alyxlib/panorama/core.lua index 86eb7d7..12becdf 100644 --- a/scripts/vscripts/alyxlib/panorama/core.lua +++ b/scripts/vscripts/alyxlib/panorama/core.lua @@ -1,12 +1,16 @@ --[[ - v1.1.1 + v1.2.0 https://github.com/FrostSource/alyxlib Panorama core library. + + If not using `alyxlib/init.lua`, load this file at game start using the following line: + + require "alyxlib.panorama.core" ]] Panorama = {} -Panorama.version = "v1.1.1" +Panorama.version = "v1.2.0" ---Filters text string to replace problematic characters. ---@param text string @@ -18,13 +22,13 @@ local function FilterText(text) end --- ----Initializes a panorama panel with a unique id. +---Initializes a panorama panel with a unique id. ---This must be done every time the entity loads, even on save game loads. --- ---The entity can be initialized if it accepts the input "AddCSSClass". --- ----@param panelEntity EntityHandle # The entity to initialize. ----@param customId? string # If nil the id will be generated automatically. +---@param panelEntity EntityHandle # The entity to initialize +---@param customId? string # The custom id to use, otherwise a unique id will be generated function Panorama:InitPanel(panelEntity, customId) local id = customId @@ -40,11 +44,27 @@ function Panorama:InitPanel(panelEntity, customId) DoEntFireByInstanceHandle(panelEntity, "AddCSSClass", id, 0, nil, nil) end +---Flattens a nested ordered table into a single list. +---@param tbl table # The table to flatten +---@param out? table # Optional table to flatten into +---@return table # The flattened table or `out` +local function flattenOrderedTable(tbl, out) + out = out or {} + for _, value in ipairs(tbl) do + if type(value) == "table" then + flattenOrderedTable(value, out) + else + table.insert(out, value) + end + end + return out +end + --- ----Send data to a panorama panel. +---Sends data to a panorama panel. --- ----@param panelEntity EntityHandle # The entity to send data to. ----@param ... any # The data to send. Each value will be converted to a string. +---@param panelEntity EntityHandle # The entity to send data to +---@param ... any # The data to send - each value will be converted to a string function Panorama:Send(panelEntity, ...) ---@diagnostic disable-next-line: undefined-field local id = panelEntity.__panoid @@ -54,28 +74,38 @@ function Panorama:Send(panelEntity, ...) end local dataString = id .. "|" - local data = {...} - local i = 1 + local n = select("#", ...) local flattenedData = {} - -- Flatten nested tables into data - for _, value in ipairs(data) do - if type(value) == "table" then - data = vlua.extend(flattenedData, value) + -- Flatten nested tables and convert to strings + for i = 1, n do + local value = select(i, ...) + if value == nil then + table.insert(flattenedData, "") + elseif type(value) == "table" then + flattenOrderedTable(value, flattenedData) else - table.insert(flattenedData, value) + table.insert(flattenedData, tostring(value)) end end local dataLength = #flattenedData -- Put all values into a single pipe separated string - for index, value in ipairs(data) do + for index, value in ipairs(flattenedData) do dataString = dataString .. tostring(value) if index < dataLength then dataString = dataString .. "|" end end dataString = FilterText(dataString) + + -- above 404 it will be clamped + -- above 462 it will be completely ignored + if #dataString > 404 then + warn("Panorama string length", #dataString, "exceeds 404 characters and may be truncated! Consider reducing the amount of data being sent or splitting it into multiple sends.") + ---@TODO split into multiple sends, need a way to buffer SendToConsole calls and ensure they are sent in order + end + -- print("Sending to pano:", dataString) SendToConsole("@panorama_dispatch_event AddStyleToEachChild('"..dataString.."')") end @@ -119,10 +149,10 @@ function Panorama:ToJSON(value) end --- ----Get the panorama id from an entity if it has one. +---Gets the panorama id from an entity if it has one. --- ----@param entityPanel EntityHandle ----@return string? +---@param entityPanel EntityHandle # The entity to get the id from +---@return string? # The panorama id function Panorama:GetId(entityPanel) ---@diagnostic disable-next-line: undefined-field return entityPanel.__panoid diff --git a/scripts/vscripts/alyxlib/player/core.lua b/scripts/vscripts/alyxlib/player/core.lua index 5739697..74f98da 100644 --- a/scripts/vscripts/alyxlib/player/core.lua +++ b/scripts/vscripts/alyxlib/player/core.lua @@ -1,11 +1,11 @@ --[[ - v4.2.2 + v4.3.0 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/init.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: require "alyxlib.player.core" ]] @@ -14,7 +14,7 @@ require "alyxlib.globals" require "alyxlib.extensions.entity" require "alyxlib.storage" -local version = "v4.2.2" +local version = "v4.3.0" ----------------------------- -- Class extension members -- @@ -126,6 +126,7 @@ CBasePlayer.Items = { ---Ammo for the shotgun. This is number of shells. shotgun = 0, ---Ammo for the generic pistol. This is number of magazines, not bullets. + ---@TODO Try to track for individual generic pistols generic_pistol = 0, }, @@ -143,6 +144,9 @@ CBasePlayer.Items = { ---The hlvr_multitool ---@type EntityHandle multitool = nil, + ---The current hlvr_weapon_generic_pistol equipped + ---@type EntityHandle + generic_pistol = nil, ---List of generic pistols the player has ---@type EntityHandle[] genericpistols = {}, @@ -187,7 +191,9 @@ CPropVRHand.Opposite = nil -- Class extension functions -- ------------------------------- ----Force the player to drop an entity if held. +--- +---Forces the player to drop an entity if held. +--- ---@param handle EntityHandle # Handle of the entity to drop function CBasePlayer:DropByHandle(handle) if IsValidEntity(handle) then @@ -197,47 +203,61 @@ function CBasePlayer:DropByHandle(handle) end end ----Force the player to drop any item held in their left hand. +--- +---Forces the player to drop any item held in their left hand. +--- function CBasePlayer:DropLeftHand() self:DropByHandle(self.LeftHand.ItemHeld) end Expose(CBasePlayer.DropLeftHand, "DropLeftHand", CBasePlayer) ----Force the player to drop any item held in their right hand. +--- +---Forces the player to drop any item held in their right hand. +--- function CBasePlayer:DropRightHand() self:DropByHandle(self.RightHand.ItemHeld) end Expose(CBasePlayer.DropRightHand, "DropRightHand", CBasePlayer) ----Force the player to drop any item held in their primary hand. +--- +---Forces the player to drop any item held in their primary hand. +--- function CBasePlayer:DropPrimaryHand() self:DropByHandle(self.PrimaryHand.ItemHeld) end Expose(CBasePlayer.DropPrimaryHand, "DropPrimaryHand", CBasePlayer) ----Force the player to drop any item held in their secondary/off hand. +--- +---Forces the player to drop any item held in their secondary/off hand. +--- function CBasePlayer:DropSecondaryHand() self:DropByHandle(self.SecondaryHand.ItemHeld) end Expose(CBasePlayer.DropSecondaryHand, "DropSecondaryHand", CBasePlayer) ----Force the player to drop the caller entity if held. ----@param data IOParams +--- +---Forces the player to drop the caller entity if held. +--- +---@param data IOParams # The IOParams table function CBasePlayer:DropCaller(data) self:DropByHandle(data.caller) end Expose(CBasePlayer.DropCaller, "DropCaller", CBasePlayer) ----Force the player to drop the activator entity if held. ----@param data IOParams +--- +---Forces the player to drop the activator entity if held. +--- +---@param data IOParams # The IOParams table function CBasePlayer:DropActivator(data) self:DropByHandle(data.activator) end Expose(CBasePlayer.DropActivator, "DropActivator", CBasePlayer) ----Force the player to grab `handle` with `hand`. ----@param handle EntityHandle ----@param hand? CPropVRHand|0|1 +--- +---Forces the player to grab `handle` with `hand`. +--- +---@param handle EntityHandle # Handle of the entity to grab +---@param hand? CPropVRHand|0|1 # Hand to grab with function CBasePlayer:GrabByHandle(handle, hand) if IsEntity(handle, true) then if type(hand) ~= "number" then @@ -257,20 +277,27 @@ function CBasePlayer:GrabByHandle(handle, hand) end end +--- ---Force the player to grab the caller entity. ----@param data IOParams +--- +---@param data IOParams # The IOParams table function CBasePlayer:GrabCaller(data) self:GrabByHandle(data.caller) end Expose(CBasePlayer.GrabCaller, "GrabCaller", CBasePlayer) +--- ---Force the player to grab the activator entity. ----@param data IOParams +--- +---@param data IOParams # The IOParams table function CBasePlayer:GrabActivator(data) self:GrabByHandle(data.activator) end Expose(CBasePlayer.GrabActivator, "GrabActivator", CBasePlayer) +--- +---Movement types for the VR player. +--- ---@enum PlayerMoveType PlayerMoveType = { TeleportBlink = 0, @@ -282,22 +309,40 @@ PlayerMoveType = { --- ---Get VR movement type. --- ----@return PlayerMoveType +---@return PlayerMoveType # The VR movement type function CBasePlayer:GetMoveType() return Convars:GetInt('hlvr_movetype_default') --[[@as PlayerMoveType]] end +--- +---Get VR movement type. +--- +---@return PlayerMoveType # The VR movement type +function GetPlayerMoveType() + return Convars:GetInt('hlvr_movetype_default') --[[@as PlayerMoveType]] +end + --- ---Sets the VR movement type. --- ----@param movetype PlayerMoveType +---@param movetype PlayerMoveType # The VR movement type function CBasePlayer:SetMoveType(movetype) Convars:SetInt("vr_movetype_set", movetype) end +--- +---Sets the VR movement type. +--- +---@param movetype PlayerMoveType # The VR movement type +function SetPlayerMoveType(movetype) + Convars:SetInt("vr_movetype_set", movetype) +end + +--- ---Returns the entity the player is looking at directly. ----@param maxDistance? number # Max distance the trace can search. ----@return EntityHandle? +--- +---@param maxDistance? number # Max distance the trace can search +---@return EntityHandle? # The entity the player is looking at function CBasePlayer:GetLookingAt(maxDistance) maxDistance = maxDistance or 2048 ---@type TraceTableLine @@ -312,9 +357,11 @@ function CBasePlayer:GetLookingAt(maxDistance) return nil end +--- ---Disables fall damage for the player. ----@TODO: Change to entity save. +--- function CBasePlayer:DisableFallDamage() + ---@TODO: Consider changing to SaveEntity. local name = Storage.LoadString(self, "FallDamageFilterName", DoUniqueString("__player_fall_damage_filter")) Storage.SaveString(self, "FallDamageFilterName", name) local filter = Entities:FindByName(nil, name) or SpawnEntityFromTableSynchronous("filter_damage_type",{ @@ -325,7 +372,9 @@ function CBasePlayer:DisableFallDamage() end Expose(CBasePlayer.DisableFallDamage, "DisableFallDamage", CBasePlayer) +--- ---Enables fall damage for the player. +--- function CBasePlayer:EnableFallDamage() --Killing the filter is not necessary but may be helpful. local name = Storage.LoadString(self, "FallDamageFilterName", "") @@ -337,11 +386,13 @@ function CBasePlayer:EnableFallDamage() end Expose(CBasePlayer.EnableFallDamage, "EnableFallDamage", CBasePlayer) +--- ---Adds resources to the player. ----@param pistol_ammo? number ----@param rapidfire_ammo? number ----@param shotgun_ammo? number ----@param resin? number +--- +---@param pistol_ammo? number # Amount of pistol ammo +---@param rapidfire_ammo? number # Amount of rapidfire ammo +---@param shotgun_ammo? number # Amount of shotgun ammo +---@param resin? number # Amount of resin function CBasePlayer:AddResources(pistol_ammo, rapidfire_ammo, shotgun_ammo, resin) pistol_ammo = pistol_ammo or 0 rapidfire_ammo = rapidfire_ammo or 0 @@ -353,16 +404,19 @@ function CBasePlayer:AddResources(pistol_ammo, rapidfire_ammo, shotgun_ammo, res nil, self.Items.ammo.rapidfire + rapidfire_ammo, self.Items.ammo.shotgun + shotgun_ammo, - nil,nil,nil, self.Items.resin + resin ) end +--- ---Sets resources for the player. ----@param pistol_ammo? number ----@param rapidfire_ammo? number ----@param shotgun_ammo? number ----@param resin? number +--- +---**This might give inaccurate amounts for omitted values.** +--- +---@param pistol_ammo? number # Amount of pistol ammo +---@param rapidfire_ammo? number # Amount of rapidfire ammo +---@param shotgun_ammo? number # Amount of shotgun ammo +---@param resin? number # Amount of resin function CBasePlayer:SetResources(pistol_ammo, rapidfire_ammo, shotgun_ammo, resin) -- Number value is different in-game (setresources uses bullets) SendToServerConsole("hlvr_setresources ".. @@ -376,39 +430,33 @@ function CBasePlayer:SetResources(pistol_ammo, rapidfire_ammo, shotgun_ammo, res nil, rapidfire_ammo or self.Items.ammo.rapidfire, shotgun_ammo or self.Items.ammo.shotgun, - nil,nil,nil, resin or self.Items.resin ) end ----Set the items that player has manually. ----This is purely for scripting and does not modify the actual items the player has in game. ----Use `Player:AddResources` to modify in-game items. ----@param energygun_ammo? integer ----@param generic_pistol_ammo? integer ----@param rapidfire_ammo? integer ----@param shotgun_ammo? integer ----@param frag_grenades? integer ----@param xen_grenades? integer ----@param healthpens? integer ----@param resin? integer +--- +---Manually sets the items that player has. +--- +---**This is purely for scripting and does not modify the actual items the player has in-game.** +--- +---Use [Player:AddResources](lua://CBasePlayer.AddResources) to modify in-game items. +--- +---@param energygun_ammo? integer # Amount of pistol ammo +---@param generic_pistol_ammo? integer # Amount of generic pistol ammo +---@param rapidfire_ammo? integer # Amount of rapidfire ammo +---@param shotgun_ammo? integer # Amount of shotgun ammo +---@param resin? integer # Amount of resin function CBasePlayer:SetItems( energygun_ammo, generic_pistol_ammo, rapidfire_ammo, shotgun_ammo, - frag_grenades, - xen_grenades, - healthpens, resin ) self.Items.ammo.energygun = energygun_ammo or self.Items.ammo.energygun self.Items.ammo.generic_pistol = generic_pistol_ammo or self.Items.ammo.generic_pistol self.Items.ammo.rapidfire = rapidfire_ammo or self.Items.ammo.rapidfire self.Items.ammo.shotgun = shotgun_ammo or self.Items.ammo.shotgun - self.Items.grenades.frag = frag_grenades or self.Items.grenades.frag - self.Items.grenades.xen = xen_grenades or self.Items.grenades.xen - self.Items.healthpen = healthpens or self.Items.healthpen self.Items.resin = resin or self.Items.resin ---@TODO: Consider moving save function above this @@ -417,30 +465,30 @@ end --- ----Add pistol ammo to the player. +---Adds pistol ammo to the player. --- ----@param amount number +---@param amount number # Amount of pistol ammo function CBasePlayer:AddPistolAmmo(amount) self:AddResources(amount, nil, nil, nil) end --- ----Add shotgun ammo to the player. +---Adds shotgun ammo to the player. --- ----@param amount number +---@param amount number # Amount of shotgun ammo function CBasePlayer:AddShotgunAmmo(amount) self:AddResources(nil, nil, amount, nil) end --- ----Add rapidfire ammo to the player. +---Adds rapidfire ammo to the player. --- ----@param amount number +---@param amount number # Amount of rapidfire ammo function CBasePlayer:AddRapidfireAmmo(amount) self:AddResources(nil, amount, nil, nil) end --- ----Add resin to the player. +---Adds resin to the player. --- ----@param amount number +---@param amount number # Amount of resin function CBasePlayer:AddResin(amount) self:AddResources(nil, nil, nil, amount) end @@ -448,22 +496,21 @@ end --- ---Gets the items currently held or in wrist pockets. --- ----@return EntityHandle[] +---@return EntityHandle[] # List of items function CBasePlayer:GetImmediateItems() - return { - self.LeftHand.WristItem, - self.RightHand.WristItem, - self.LeftHand.ItemHeld, - self.RightHand.ItemHeld - } + local lh, rh = self.LeftHand, self.RightHand + local items = {} + if lh and lh.WristItem then items[#items+1] = lh.WristItem end + if rh and rh.WristItem then items[#items+1] = rh.WristItem end + if lh and lh.ItemHeld then items[#items+1] = lh.ItemHeld end + if rh and rh.ItemHeld then items[#items+1] = rh.ItemHeld end + return items end --- ----Gets the grenades currently held or in wrist pockets. +---Gets the grenades currently held and in wrist pockets. --- ----Use `#Player:GetGrenades()` to get number of grenades player has access to. ---- ----@return EntityHandle[] +---@return EntityHandle[] # List of grenades function CBasePlayer:GetGrenades() local grenades = {} local immediate_items = self:GetImmediateItems() @@ -476,9 +523,7 @@ function CBasePlayer:GetGrenades() end --- ----Gets the health pens currently held or in wrist pockets. ---- ----Use `#Player:GetHealthPens()` to get number of health pens player has access to. +---Gets the health pens currently held and in wrist pockets. --- ---@return EntityHandle[] function CBasePlayer:GetHealthPens() @@ -495,9 +540,9 @@ end --- ---Marges an existing prop with a given hand. --- ----@param hand CPropVRHand|0|1 # The hand handle or index. ----@param prop EntityHandle|string # The prop handle or targetname. ----@param hide_hand boolean # If the hand should turn invisible after merging. +---@param hand CPropVRHand|0|1 # The hand handle or ID +---@param prop EntityHandle|string # The prop handle or targetname +---@param hide_hand boolean # If the hand should turn invisible after merging function CBasePlayer:MergePropWithHand(hand, prop, hide_hand) if type(hand) == "number" then hand = self.Hands[hand+1] @@ -506,9 +551,10 @@ function CBasePlayer:MergePropWithHand(hand, prop, hide_hand) end --- ----Return if the player has a gun equipped. +---Checks if the player has a gun equipped. +---`hlvr_multitool` is not considered a "gun" --- ----@return boolean +---@return boolean # `true` if the player has a gun equipped function CBasePlayer:HasWeaponEquipped() return self.CurrentlyEquipped == PLAYER_WEAPON_ENERGYGUN or self.CurrentlyEquipped == PLAYER_WEAPON_SHOTGUN @@ -519,9 +565,9 @@ end --- ---Get the amount of ammo stored in the backpack for the currently equipped weapon. --- ----This is not accurate if ammo was given through special means like info_hlvr_equip_player. +---**This is not accurate if ammo was given through special means like `info_hlvr_equip_player`.** --- ----@return number # The amount of ammo, or 0 if no weapon equipped +---@return number # The amount of ammo, or `0` if no weapon equipped function CBasePlayer:GetCurrentWeaponReserves() local currentlyEquipped = self.CurrentlyEquipped if currentlyEquipped == PLAYER_WEAPON_ENERGYGUN then @@ -536,8 +582,10 @@ function CBasePlayer:GetCurrentWeaponReserves() return 0 end ----Player has item holder equipped. ----@return boolean +--- +---Checks if the player has an item (wrist) holder equipped. +--- +---@return boolean # `true` if the player has an item holder function CBasePlayer:HasItemHolder() for _, hand in ipairs(self.Hands) do if hand:GetFirstChildWithClassname("hlvr_hand_item_holder") then @@ -551,8 +599,10 @@ function CBasePlayer:HasItemHolder() return false end ----Player has grabbity gloves equipped. ----@return boolean +--- +---Checks if the player has grabbity gloves equipped. +--- +---@return boolean # `true` if the player has grabbity gloves function CBasePlayer:HasGrabbityGloves() return self.PrimaryHand:GetGrabbityGlove() ~= nil end @@ -561,11 +611,14 @@ function CBasePlayer:GetFlashlight() return self.SecondaryHand:GetFirstChildWithClassname("hlvr_flashlight_attachment") end ----Get the first entity the flashlight is pointed at (if the flashlight exists). +--- +---Gets the first entity the flashlight is pointed at. +--- ---If flashlight does not exist, both returns will be `nil`. ----@param maxDistance number # Max tracing distance, default is 2048. ----@return EntityHandle|nil # The entity that was hit, or nil. ----@return Vector|nil # The position the trace hit, regardless of entity found. +--- +---@param maxDistance? number # Max tracing distance (default: 2048) +---@return EntityHandle|nil # The entity that was hit +---@return Vector|nil # The position the trace hit, regardless of entity found function CBasePlayer:GetFlashlightPointedAt(maxDistance) local flashlight = self:GetFlashlight() if flashlight then @@ -583,10 +636,14 @@ function CBasePlayer:GetFlashlightPointedAt(maxDistance) end end ----Gets the current resin from the player. ----This is can be more accurate than `Player.Items.resin` ----Calling this will update `Player.Items.resin` ----@return number +--- +---Gets the current resin count the player has. +--- +---This is can be more accurate than `Player.Items.resin`. +--- +---Calling this will update `Player.Items.resin`. +--- +---@return number # The current resin count function CBasePlayer:GetResin() local t = ({}) --[[@as CriteriaTable]] self:GatherCriteria(t) @@ -597,38 +654,45 @@ function CBasePlayer:GetResin() return r end ----Gets if player is holding an entity in either hand. ----@param entity EntityHandle ----@return boolean +--- +---Gets if player is holding a given entity in either hand. +--- +---@param entity EntityHandle # The entity to check +---@return boolean # `true` if the player is holding the entity function CBasePlayer:IsHolding(entity) return self.PrimaryHand.ItemHeld == entity or self.SecondaryHand.ItemHeld == entity end ----Get the entity handle of the currently equipped weapon/item. ----If nothing is equipped this will return the primary hand entity. ----@return EntityHandle|nil +--- +---Gets the entity handle of the currently equipped weapon, including `hlvr_multitool`. +--- +---@return EntityHandle|nil # The equipped weapon, or `nil` if no weapon equipped function CBasePlayer:GetWeapon() if self.CurrentlyEquipped == PLAYER_WEAPON_ENERGYGUN then - return Entities:FindByClassnameNearest("hlvr_weapon_energygun", self.PrimaryHand:GetPalmPosition(), 128)--[[@as EntityHandle]] + return self.Items.weapons.energygun + or Entities:FindByClassnameNearest("hlvr_weapon_energygun", self.PrimaryHand:GetPalmPosition(), 128)--[[@as EntityHandle]] elseif self.CurrentlyEquipped == PLAYER_WEAPON_RAPIDFIRE then - return Entities:FindByClassnameNearest("hlvr_weapon_rapidfire", self.PrimaryHand:GetPalmPosition(), 128)--[[@as EntityHandle]] + return self.Items.weapons.rapidfire + or Entities:FindByClassnameNearest("hlvr_weapon_rapidfire", self.PrimaryHand:GetPalmPosition(), 128)--[[@as EntityHandle]] elseif self.CurrentlyEquipped == PLAYER_WEAPON_SHOTGUN then - return Entities:FindByClassnameNearest("hlvr_weapon_shotgun", self.PrimaryHand:GetPalmPosition(), 128)--[[@as EntityHandle]] + return self.Items.weapons.shotgun + or Entities:FindByClassnameNearest("hlvr_weapon_shotgun", self.PrimaryHand:GetPalmPosition(), 128)--[[@as EntityHandle]] elseif self.CurrentlyEquipped == PLAYER_WEAPON_GENERIC_PISTOL then - return Entities:FindByClassnameNearest("hlvr_weapon_generic_pistol", self.PrimaryHand:GetPalmPosition(), 128)--[[@as EntityHandle]] + return self.Items.weapons.generic_pistol + or Entities:FindByClassnameNearest("hlvr_weapon_generic_pistol", self.PrimaryHand:GetPalmPosition(), 128)--[[@as EntityHandle]] elseif self.CurrentlyEquipped == PLAYER_WEAPON_MULTITOOL then - return Entities:FindByClassnameNearest("hlvr_multitool", self.PrimaryHand:GetPalmPosition(), 128)--[[@as EntityHandle]] + return self.Items.weapons.multitool + or Entities:FindByClassnameNearest("hlvr_multitool", self.PrimaryHand:GetPalmPosition(), 128)--[[@as EntityHandle]] else return nil end end --- ----Get the current upgrades for the player's pistol. +---Gets the current upgrades for the player's pistol. --- ----@param weapon? EntityHandle # Optional weapon to check instead of the player's weapon. ----@return PlayerPistolUpgrades[] ----@overload fun(self: CBasePlayer): PlayerPistolUpgrades[] +---@param weapon? EntityHandle # Optional weapon to check instead of the player's weapon +---@return PlayerPistolUpgrades[] # List of upgrades function CBasePlayer:GetPistolUpgrades(weapon) local pistol = weapon or self.Items.weapons.energygun @@ -654,11 +718,10 @@ function CBasePlayer:GetPistolUpgrades(weapon) end --- ----Get the current upgrades for the player's rapidfire. +---Gets the current upgrades for the player's rapidfire. --- ----@param weapon? EntityHandle # Optional weapon to check instead of the player's weapon. ----@return PlayerRapidfireUpgrades[] ----@overload fun(self: CBasePlayer): PlayerRapidfireUpgrades[] +---@param weapon? EntityHandle # Optional weapon to check instead of the player's weapon +---@return PlayerRapidfireUpgrades[] # List of upgrades function CBasePlayer:GetRapidfireUpgrades(weapon) local rapidfire = weapon or self.Items.weapons.rapidfire @@ -672,7 +735,7 @@ function CBasePlayer:GetRapidfireUpgrades(weapon) if child:GetClassname() == "reflex_sights" then table.insert(upgrades, "rapidfire_upgrade_reflex_sight") elseif child:GetName() == "rapidfire_laser_sight" then - table.insert(upgrades, "rapidfire_upgrade_laser_right") + table.insert(upgrades, "rapidfire_upgrade_laser_sight") elseif child:GetName() == "rapidfire_extended_magazine" then table.insert(upgrades, "rapidfire_upgrade_extended_magazine") end @@ -682,13 +745,12 @@ function CBasePlayer:GetRapidfireUpgrades(weapon) end --- ----Get the current upgrades for the player's shotgun. +---Gets the current upgrades for the player's shotgun. --- ---**This will NOT return "shotgun_upgrade_quick_fire" because there is no known way to detect this!** --- ----@param weapon? EntityHandle # Optional weapon to check instead of the player's weapon. ----@return PlayerShotgunUpgrades[] ----@overload fun(self: CBasePlayer): PlayerShotgunUpgrades[] +---@param weapon? EntityHandle # Optional weapon to check instead of the player's weapon +---@return PlayerShotgunUpgrades[] # List of upgrades function CBasePlayer:GetShotgunUpgrades(weapon) local shotgun = weapon or self.Items.weapons.shotgun @@ -712,10 +774,10 @@ function CBasePlayer:GetShotgunUpgrades(weapon) end --- ----Check if the player has a specific weapon upgrade. +---Checks if the player has a specific weapon upgrade. --- ----@param upgrade PlayerPistolUpgrades|PlayerRapidfireUpgrades|PlayerShotgunUpgrades ----@return boolean +---@param upgrade PlayerPistolUpgrades|PlayerRapidfireUpgrades|PlayerShotgunUpgrades # Upgrade to check +---@return boolean # `true` if the player has the upgrade function CBasePlayer:HasWeaponUpgrade(upgrade) if vlua.find(self:GetPistolUpgrades(), upgrade) then return true @@ -728,9 +790,9 @@ function CBasePlayer:HasWeaponUpgrade(upgrade) end --- ----Get the forward vector of the player in world space coordinates (z is zeroed). +---Gets the forward vector of the player in world space coordinates (z is zeroed). --- ----@return Vector +---@return Vector # Forward vector function CBasePlayer:GetWorldForward() local f = self:GetForwardVector() f.z = 0 @@ -745,11 +807,11 @@ local specialAttachmentsOrder = { } --- ----Update player weapon inventory, both removing and setting. +---Updates player weapon inventory, both removing and setting. --- ----@param removes? (string|EntityHandle)[] # List of classnames or handles to remove. ----@param set? string|EntityHandle # Classname or handle to set as active weapon. ----@return EntityHandle? # The handle of the newly set weapon if given and found. +---@param removes? (string|EntityHandle)[] # List of classnames or handles to remove +---@param set? string|EntityHandle # Classname or handle to set as active weapon +---@return EntityHandle? # The handle of the newly set weapon if given and found function CBasePlayer:UpdateWeapons(removes, set) local hand = Player.PrimaryHand @@ -806,9 +868,9 @@ function CBasePlayer:UpdateWeapons(removes, set) end --- ----Remove weapons from the player inventory. +---Removes weapons from the player inventory. --- ----@param weapons (string|EntityHandle)[] # List of classnames or handles to remove. +---@param weapons (string|EntityHandle)[] # List of classnames or handles to remove ---@overload fun(self: CBasePlayer, weapon: string|EntityHandle) function CBasePlayer:RemoveWeapons(weapons) if weapons == nil or (type(weapons) == "table" and #weapons == 0) then @@ -821,10 +883,10 @@ function CBasePlayer:RemoveWeapons(weapons) end --- ----Set the weapon that the player is holding. +---Sets the weapon that the player is holding. --- ----@param weapon string|EntityHandle # Classname or handle to set as active weapon. ----@return EntityHandle? # The handle of the newly set weapon if found. +---@param weapon string|EntityHandle # Classname or handle to set as active weapon +---@return EntityHandle? # The handle of the newly set weapon if found function CBasePlayer:SetWeapon(weapon) return self:UpdateWeapons(nil, weapon) end @@ -891,7 +953,7 @@ end --- ---Returns [Player.Items.weapons](lua://CBasePlayer.Items) flattened into a single array. --- ----@return EntityHandle[] +---@return EntityHandle[] # List of weapon handles function CBasePlayer:GetWeapons() local weapons = Player.Items.weapons @@ -905,19 +967,20 @@ function CBasePlayer:GetWeapons() end --- ----Get the invisible player backpack. +---Gets the invisible player backpack. +--- ---This is will return the backpack even if it has been disabled with a `info_hlvr_equip_player`. --- ----@return EntityHandle? +---@return EntityHandle? # The backpack entity function CBasePlayer:GetBackpack() return Entities:FindByClassname(nil, "player_backpack") end --- ----Enable or disable player movement. Including teleport movement. +---Enables or disables player movement, including teleport movement. --- ----@param enabled boolean # True if movement should be enabled. ----@param delay? number # Optional delay for movement state will be changed +---@param enabled boolean # `true` if movement should be enabled. +---@param delay? number # Delay before movement state will be changed (default: 0) function CBasePlayer:SetMovementEnabled(enabled, delay) Player:EntFire("EnableTeleport", enabled and "1" or "0", delay or 0) end @@ -925,7 +988,7 @@ end --- ---Sets the forward vector of the HMD anchor while keeping the position the same relative to the player. --- ----Normally if the player is off-center when changing the forward vector the player may appear to move too. +---Normally if the player is off-center from their playspace, changing the forward vector can move the player too. --- ---@param forward Vector # Normalized forward vector function CBasePlayer:SetAnchorForwardAroundPlayer(forward) @@ -939,9 +1002,9 @@ end --- ---Sets the angle of the HMD anchor while keeping the position the same relative to the player. --- ----Normally if the player is off-center when changing the angle the player may appear to move too. +---Normally if the player is off-center from their playspace, changing the angle can move the player too. --- ----@param angles QAngle # New angle of the anchor +---@param angles QAngle # New angle for the anchor function CBasePlayer:SetAnchorAnglesAroundPlayer(angles) local oldPos = self:GetAbsOrigin() local relativePos = self.HMDAnchor:TransformPointWorldToEntity(oldPos) @@ -953,9 +1016,10 @@ end --- ---Sets the origin of the HMD anchor while keeping the position the same relative to the player. --- ----This essentially moves the player by moving the anchor and can be used in instances where setting the player origin does not work. +---This essentially moves the player by moving the anchor and can be used in instances where +---setting the player origin does not work. --- ----@param pos Vector +---@param pos Vector # New origin function CBasePlayer:SetAnchorOriginAroundPlayer(pos) self.HMDAnchor:SetAbsOrigin(pos + (self.HMDAnchor:GetAbsOrigin() - self:GetAbsOrigin())) end @@ -963,7 +1027,7 @@ 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 +---@param enabled boolean # `true` if the cough handpose should be enabled function CBasePlayer:SetCoughHandEnabled(enabled) if not self.HMDAvatar then return diff --git a/scripts/vscripts/alyxlib/player/data.lua b/scripts/vscripts/alyxlib/player/data.lua new file mode 100644 index 0000000..9dd18d6 --- /dev/null +++ b/scripts/vscripts/alyxlib/player/data.lua @@ -0,0 +1,281 @@ +--[[ + v1.0.0 + https://github.com/FrostSource/alyxlib + + Advanced player-specific data management and logic handling. + + This module is designed as a library component and is not + automatically loaded into the global scope. + + Usage: + local PlayerData = require("alyxlib.player.data") +]] + +local version = "v1.0.0" + +--- +---Forces all persistent player data to save. +--- +---@see CBasePlayer.Items +---@see CBasePlayer.CurrentlyEquipped +---@see CPropVRHand.WristItem +local function savePlayerData() + Storage.SaveTable(Player, "PlayerItems", Player.Items) + + -- weapon_switch doesn't fire 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) + Storage.SaveEntity(Player, "LeftHandItemHeld", Player.LeftHand.ItemHeld) + end + if Player and Player.RightHand then + Storage.SaveEntity(Player, "RightWristItem", Player.RightHand.WristItem) + Storage.SaveEntity(Player, "RightHandItemHeld", Player.RightHand.ItemHeld) + end +end + +--- +---Loads all persistent player data. +--- +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) + Player.LeftHand.ItemHeld = Storage.LoadEntity(Player, "LeftHandItemHeld", Player.LeftHand.ItemHeld) + end + if Player and Player.RightHand then + Player.RightHand.WristItem = Storage.LoadEntity(Player, "RightWristItem", Player.RightHand.WristItem) + Player.RightHand.ItemHeld = Storage.LoadEntity(Player, "RightHandItemHeld", Player.RightHand.ItemHeld) + end +end + +local previouslyEquipped = nil +local currentlyEquipped = nil +local previousHandle = nil +local currentHandle = nil + +-- These might need to be moved to CPropVRHand.CurrentlyEquipped +-- if general use is desired, but offhand equip is rare +---@type {[0]:string, [1]:string} +local handEquipClass = {} + +local syncPaused = false + +---Standard local weapon offsets from hand when equipped +local weaponOffsets = { + [0] = { -- left + hand_use_controller = Vector(-4.8, 0.576, -0.262), + hlvr_weapon_energygun = Vector(-3.359, 0.667, 1.314), + hlvr_weapon_shotgun = Vector(-3.297, 0.641, 0.753), + hlvr_weapon_rapidfire = Vector(-3.3, 0.643, 0.767), + hlvr_multitool = Vector(-1.695, 0.099, -1.369), + -- unsure if offsets will be different for custom models, + -- using hlvr_weapon_energygun offset + hlvr_weapon_generic_pistol = Vector(-3.359, 0.667, 1.314), + }, + [1] = { -- right + hand_use_controller = Vector(-4.798, -0.674, -0.3), + hlvr_weapon_energygun = Vector(-3.399, -0.769, 1.244), + hlvr_weapon_shotgun = Vector(-3.289, -0.731, 0.711), + hlvr_weapon_rapidfire = Vector(-3.293, -0.731, 0.722), + hlvr_multitool = Vector(-1.692, -0.299, -1.442), + -- unsure if offsets will be different for custom models, + -- using hlvr_weapon_energygun offset + hlvr_weapon_generic_pistol = Vector(-3.399, -0.769, 1.244), + }, +} + +--- +---Returns the default weapon offset for the given weapon classname and hand. +--- +---@param classname string # The classname of the weapon +---@param hand CPropVRHand|number # The hand to get the offset for +---@return Vector # The default weapon offset, or `(0, 0, 0)` if not found +local function GetDefaultWeaponOffset(classname, hand) + hand = hand or Player.PrimaryHand:GetHandID() + if IsEntity(hand) then + hand = hand:GetHandID() + end + + return weaponOffsets[hand][classname] or Vector() +end + +--- +---Syncs the equipped weapon state of the player with the given weapon classname. +--- +---If no classname is given, the weapon classname will be determined from the player's criteria. +--- +---**This is used internally on weapon switch to determine equipped weapons.** +--- +---@param classname? string # The classname of the weapon to sync +---@param handle? EntityHandle # The entity handle of the weapon to sync +---@return EntityHandle? weaponHandle # The entity handle of the weapon that was equipped +---@return CPropVRHand? handHandle # The entity handle of the hand that the weapon was equipped to +local function SyncEquippedWeaponState(classname, handle) + if syncPaused then return end + + if classname == nil then + local criteria = Player:GetCriteria() + classname = criteria.primaryhand_active_attachment + end + + -- print("Swapping from", Player.CurrentlyEquipped, "to", classname) + + -- Cache current state so it can be restored later + -- print("Saving equipped", Player.PreviouslyEquipped, Player.CurrentlyEquipped) + previouslyEquipped = Player.PreviouslyEquipped + currentlyEquipped = Player.CurrentlyEquipped + previousHandle = currentHandle + + Player.PreviouslyEquipped = Player.CurrentlyEquipped + + ---@type CPropVRHand? + local handHandle = nil + + ---@type EntityHandle? + local weaponHandle = nil + + -- Find the weapon and hand + if not handle then + local bestDistance = math.huge + local bestWeapon = nil + for h = 0, 1 do + local hand = h == 0 and Player.LeftHand or Player.RightHand + local offset = weaponOffsets[h][classname] or Vector() + + -- print("Looking for", classname, "in hand", hand:GetName(), "currentlyEquipped", handEquipClass[hand:GetHandID()]) + + -- If switching to hand_use_controller, it must not be the current equipped class + if classname ~= "hand_use_controller" or handEquipClass[hand:GetHandID()] ~= classname then + for _, wpn in ipairs(Entities:FindAllByClassname(classname)) do + -- Weapon must not be held + if wpn ~= hand.ItemHeld then + local dist = VectorDistance(offset, hand:TransformPointWorldToEntity(wpn:GetOrigin())) + -- debugoverlay:Sphere(wpn:GetOrigin(), 1, 0, 255, 0, 255, false, 5) + -- debugoverlay:Text(wpn:GetOrigin()+Vector(0,0,1), 0, tostring(dist), 0, 0, 255, 0, 255, 5) + -- print(dist, ":", hand:GetName(), entstr(wpn), wpn:GetModelName(), "Owner:", entstr(wpn:GetOwner())) + if dist < bestDistance then + bestDistance = dist + bestWeapon = wpn + handHandle = hand + end + end + end + end + end + handle = bestWeapon + end + + weaponHandle = handle + + if not IsValidEntity(weaponHandle) then + warn("Could not find weapon entity for equipped weapon: " .. classname) + return weaponHandle, handHandle + end + + -- if weaponHandle then + -- print("Setting new weapon for hand", entstr(handHandle), classname, entstr(weaponHandle), "Owner:", entstr(weaponHandle:GetOwner())) + -- debugoverlay:Sphere(weaponHandle:GetOrigin(), 2, 255, 255, 255, 255, true, 5) + -- debugoverlay:Text(weaponHandle:GetOrigin(), 0, entstr(weaponHandle).." : "..entstr(handHandle), 0, 255, 255, 255, 255, 5) + -- end + + currentHandle = weaponHandle + + if handHandle == Player.PrimaryHand then + + if classname == "hand_use_controller" then + Player.CurrentlyEquipped = PLAYER_WEAPON_HAND + elseif classname == "hlvr_weapon_energygun" then + Player.CurrentlyEquipped = PLAYER_WEAPON_ENERGYGUN + Player.Items.weapons.energygun = weaponHandle + elseif classname == "hlvr_weapon_rapidfire" then + Player.CurrentlyEquipped = PLAYER_WEAPON_RAPIDFIRE + Player.Items.weapons.rapidfire = weaponHandle + elseif classname == "hlvr_weapon_shotgun" then + Player.CurrentlyEquipped = PLAYER_WEAPON_SHOTGUN + Player.Items.weapons.shotgun = weaponHandle + elseif classname == "hlvr_multitool" then + Player.CurrentlyEquipped = PLAYER_WEAPON_MULTITOOL + Player.Items.weapons.multitool = weaponHandle + elseif classname == "hlvr_weapon_generic_pistol" then + Player.CurrentlyEquipped = PLAYER_WEAPON_GENERIC_PISTOL + Player.Items.weapons.generic_pistol = weaponHandle + + -- Add this custom pistol if this is the first time encountering it + if not vlua.find(Player.Items.weapons.genericpistols, weaponHandle) then + table.insert(Player.Items.weapons.genericpistols, weaponHandle) + end + end + + Player:UpdateWeaponsExistence() + + end + + -- This event can fire before the player has a hand + if handHandle then + if classname == "hand_use_controller" then + handHandle.ItemHeld = nil + else + handHandle.ItemHeld = weaponHandle + end + + handEquipClass[handHandle:GetHandID()] = classname + end + + savePlayerData() + + return weaponHandle, handHandle +end + +--- +---Restores the previously equipped weapon state. +--- +---@param fireEvent? boolean # `true` to force-fire the `weapon_switch` event +local function RestorePreviouslyEquippedWeaponState(fireEvent) + Player.CurrentlyEquipped = previouslyEquipped + if fireEvent then + ---@type GameEventWeaponSwitch + FireGameEvent("weapon_switch", { + item = currentlyEquipped, + }) + else + SyncEquippedWeaponState(currentlyEquipped, previousHandle) + end +end + +--- +---Pauses weapon state synching. This will prevent the `weapon_switch` player event from firing. +--- +local function PauseWeaponStateSync() + syncPaused = true +end + +--- +---Resumes weapon state synching. This will allow the `weapon_switch` player event to fire. +--- +local function ResumeWeaponStateSync() + syncPaused = false +end + +--- +---Checks if weapon state synching is paused. +--- +local function WeaponStateSyncPaused() + return syncPaused +end + +return { + version = version, + SyncEquippedWeaponState = SyncEquippedWeaponState, + RestorePreviouslyEquippedWeaponState = RestorePreviouslyEquippedWeaponState, + PauseWeaponStateSync = PauseWeaponStateSync, + ResumeWeaponStateSync = ResumeWeaponStateSync, + WeaponStateSyncPaused = WeaponStateSyncPaused, + SavePlayerData = savePlayerData, + LoadPlayerData = loadPlayerData, + GetDefaultWeaponOffset = GetDefaultWeaponOffset +} \ No newline at end of file diff --git a/scripts/vscripts/alyxlib/player/events.lua b/scripts/vscripts/alyxlib/player/events.lua index 16fee2b..05d2206 100644 --- a/scripts/vscripts/alyxlib/player/events.lua +++ b/scripts/vscripts/alyxlib/player/events.lua @@ -1,11 +1,17 @@ --[[ - v2.2.0 + v2.3.0 https://github.com/FrostSource/alyxlib + Player event management and callbacks. + + If not using `alyxlib/init.lua`, load this file at game start using the following line: + require "alyxlib.player.events" ]] -local version = "v2.2.0" +local version = "v2.3.0" + +local playerData = require "alyxlib.player.data" ---@class __PlayerRegisteredEventData ---@field callback function @@ -32,24 +38,33 @@ local playerActivated = false ---@alias PLAYER_EVENTS_ALL "novr_player"|"player_activate"|"vr_player_ready"|"item_pickup"|"item_released"|"primary_hand_changed"|"player_drop_ammo_in_backpack"|"player_retrieved_backpack_clip"|"player_stored_item_in_itemholder"|"player_removed_item_from_itemholder"|"player_drop_resin_in_backpack"|"weapon_switch" ----Register a callback function with for a player event. +--- +---Registers a callback function with a player event. +--- ---@param event PLAYER_EVENTS_ALL # Name of the event ---@param callback function # The function that will be called when the event is fired ----@param context? table # Optional: The context to pass to the function as `self`. If omitted the context will not passed to the callback. +---@param context? table # The context to pass to first argument of `callback` - if omitted, the context will not passed to the callback ---@return integer eventID # ID used to unregister function ListenToPlayerEvent(event, callback, context) + assert(registered_event_callbacks[event], "Unknown player event "..event) + if playerActivated and (event == "novr_player" or event == "player_activate" or event == "vr_player_ready") then warn("Player has already spawned so this event won't fire at "..Debug.GetSourceLine(3)) end + devprint2("Listening to player event", event, callback) + registered_event_callbacks[event][registered_event_index] = { callback = callback, context = context} registered_event_index = registered_event_index + 1 return registered_event_index - 1 end ----Unregisters a callback with a name. ----@param eventID integer +--- +---Unregisters a player event callback. +--- +---@param eventID integer|nil # ID returned from [ListenToPlayerEvent](lua://ListenToPlayerEvent) function StopListeningToPlayerEvent(eventID) + if not eventID then return end for _, event in pairs(registered_event_callbacks) do event[eventID] = nil end @@ -62,11 +77,11 @@ local entityPickupEvents = {} local entityPickupIndex = 1 --- ----Listen to the pickup of a specific entity. +---Listens to the pickup of a specific entity. --- ---@param entity EntityHandle # The entity to listen for ---@param callback function # The function that will be called when the entity is picked up ----@param context? any # Optional context passed into the callback as the first value +---@param context? any # Context passed into the callback as the first value ---@return integer # ID used to unregister function ListenToEntityPickup(entity, callback, context) entityPickupEvents[entityPickupIndex] = { @@ -79,7 +94,7 @@ function ListenToEntityPickup(entity, callback, context) end --- ----Stop listening to an entity pickup +---Stops listening to an entity pickup. --- ---@param eventID integer # ID returned from [ListenToEntityPickup](lua://ListenToEntityPickup) function StopListeningToEntityPickup(eventID) @@ -102,35 +117,7 @@ local player_weapon_to_ammotype = [PLAYER_WEAPON_GENERIC_PISTOL] = "AlyxGun", } ---- ----Save the Player.Items table ---- -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 - if Player and Player.RightHand then - Storage.SaveEntity(Player, "RightWristItem", Player.RightHand.WristItem) - end -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. ---@param eventName string # Name of the event, data.game_event_name @@ -146,8 +133,8 @@ local function eventCallback(eventName, newdata) end ---@class PlayerEventPlayerActivate : GameEventBase ----@field player CBasePlayer # The entity handle of the player. ----@field type "spawn"|"load"|"transition" # Type of player activate. +---@field player CBasePlayer # The entity handle of the player +---@field type "spawn"|"load"|"transition" # Type of player activate ---@class PlayerEventVRPlayerReady : PlayerEventPlayerActivate ---@field hmd_avatar CPropHMDAvatar # The hmd avatar entity handle. @@ -158,7 +145,7 @@ end local function listenEventPlayerActivate(data) playerActivated = true Player = GetListenServerHost() - loadPlayerData() + playerData.LoadPlayerData() local previous_map = Storage.LoadString(Player, "PlayerPreviousMap", "") Storage.SaveString(Player, "PlayerPreviousMap", GetMapName()) @@ -208,6 +195,9 @@ local function listenEventPlayerActivate(data) end, 0.5) end + -- Load data again after vr entities have spawned + playerData.LoadPlayerData() + -- Registered callback data.hmd_avatar = Player.HMDAvatar for id, event_data in pairs(registered_event_callbacks["vr_player_ready"]) do @@ -262,10 +252,10 @@ ListenToGameEvent("physgun_pickup", function (params) end, nil) ---@class PlayerEventItemPickup : GameEventItemPickup ----@field item EntityHandle # The entity handle of the item that was picked up. ----@field item_class string # Classname of the entity that was picked up. ----@field hand CPropVRHand # The entity handle of the hand that picked up the item. ----@field otherhand CPropVRHand # The entity handle of the opposite hand. +---@field item EntityHandle # The entity handle of the item that was picked up +---@field item_class string # Classname of the entity that was picked up +---@field hand CPropVRHand # The entity handle of the hand that picked up the item +---@field otherhand CPropVRHand # The entity handle of the opposite hand ---Tracking player held items. ---@param data GameEventItemPickup @@ -338,12 +328,11 @@ end ListenToGameEvent("item_pickup", listenEventItemPickup, nil) ---@class PlayerEventItemReleased : GameEventItemReleased ----@field item EntityHandle # The entity handle of the item that was dropped. ----@field item_class string # Classname of the entity that was dropped. ----@field hand CPropVRHand # The entity handle of the hand that dropped the item. ----@field otherhand CPropVRHand # The entity handle of the opposite hand. +---@field item EntityHandle # The entity handle of the item that was dropped +---@field item_class string # Classname of the entity that was dropped +---@field hand CPropVRHand # The entity handle of the hand that dropped the item +---@field otherhand CPropVRHand # The entity handle of the opposite hand --- ---@type "item_hlvr_crafting_currency_small"|"item_hlvr_crafting_currency_large"|nil ---@type EntityHandle|nil local last_resin_dropped @@ -407,9 +396,10 @@ end ListenToGameEvent("primary_hand_changed", listenEventPrimaryHandChanged, nil) -- Inherit from base instead of event to remove 'ammoType' + ---@class PlayerEventPlayerDropAmmoInBackpack : GameEventBase ----@field ammotype "Pistol"|"SMG1"|"Buckshot"|"AlyxGun" # Type of ammo that was stored. ----@field ammo_amount 0|1|2|3|4 # Amount of ammo stored for the given type (1 clip, 2 shells). +---@field ammotype "Pistol"|"SMG1"|"Buckshot"|"AlyxGun" # Type of ammo that was stored +---@field ammo_amount integer # Amount of ammo stored for the given type, e.g. 1 clip, 2 shells ---Ammo tracking ---@param data GameEventPlayerDropAmmoInBackpack @@ -472,7 +462,7 @@ local function listenEventPlayerDropAmmoInBackpack(data) else warn("Couldn't figure out ammo for "..tostring(ammotype)) end - savePlayerData() + playerData.SavePlayerData() -- Registered callback local newdata = vlua.clone(data)--[[@as PlayerEventPlayerDropAmmoInBackpack]] @@ -485,8 +475,8 @@ ListenToGameEvent("player_drop_ammo_in_backpack", listenEventPlayerDropAmmoInBac -- Inherit from base instead of event to remove 'ammoType' ---@class PlayerEventPlayerRetrievedBackpackClip : GameEventBase ----@field ammotype "Pistol"|"SMG1"|"Buckshot"|"AlyxGun" # Type of ammo that was retrieved. ----@field ammo_amount integer # Amount of ammo retrieved for the given type (1 clip, 2 shells). +---@field ammotype "Pistol"|"SMG1"|"Buckshot"|"AlyxGun" # Type of ammo that was retrieved +---@field ammo_amount integer # Amount of ammo retrieved for the given type, e.g. 1 clip, 2 shells ---Ammo tracking ---@param data GameEventPlayerRetrievedBackpackClip @@ -521,7 +511,7 @@ local function listenEventPlayerRetrievedBackpackClip(data) Player.Items.ammo.shotgun = Player.Items.ammo.shotgun - ammo_amount end - savePlayerData() + playerData.SavePlayerData() -- Registered callback newdata.ammo_amount = ammo_amount @@ -535,7 +525,7 @@ local function listenEventPlayerRetrievedBackpackClip(data) -- Registered callback if do_callback then - savePlayerData() + playerData.SavePlayerData() newdata.ammo_amount = ammo_amount eventCallback(data.game_event_name, newdata) end @@ -543,9 +533,9 @@ end ListenToGameEvent("player_retrieved_backpack_clip", listenEventPlayerRetrievedBackpackClip, nil) ---@class PlayerEventPlayerStoredItemInItemholder : GameEventPlayerStoredItemInItemholder ----@field item EntityHandle # The entity handle of the item that stored. ----@field item_class string # Classname of the entity that was stored. ----@field hand CPropVRHand # Hand that the entity was stored in. +---@field item EntityHandle # The entity handle of the item that stored +---@field item_class string # Classname of the entity that was stored +---@field hand CPropVRHand # Hand that the entity was stored in ---Wrist tracking ---@param data GameEventPlayerStoredItemInItemholder @@ -573,7 +563,7 @@ local function listenEventPlayerStoredItemInItemholder(data) -- elseif data.item == "item_healthvial" then -- Player.Items.healthpen = Player.Items.healthpen + 1 -- end - savePlayerData() + playerData.SavePlayerData() -- Registered callback local newdata = vlua.clone(data)--[[@as PlayerEventPlayerStoredItemInItemholder]] @@ -585,9 +575,9 @@ end ListenToGameEvent("player_stored_item_in_itemholder", listenEventPlayerStoredItemInItemholder, nil) ---@class PlayerEventPlayerRemovedItemFromItemholder : GameEventPlayerRemovedItemFromItemholder ----@field item EntityHandle # The entity handle of the item that removed. ----@field item_class string # Classname of the entity that was removed. ----@field hand CPropVRHand # Hand that the entity was removed form. +---@field item EntityHandle # The entity handle of the item that removed +---@field item_class string # Classname of the entity that was removed +---@field hand CPropVRHand # Hand that the entity was removed from ---Tracking wrist items ---@param data GameEventPlayerRemovedItemFromItemholder @@ -620,7 +610,7 @@ local function listenEventPlayerRemovedItemFromItemholder(data) -- elseif data.item == "item_healthvial" then -- Player.Items.healthpen = Player.Items.healthpen - 1 -- end - savePlayerData() + playerData.SavePlayerData() -- Registered callback local newdata = vlua.clone(data)--[[@as PlayerEventPlayerRemovedItemFromItemholder]] @@ -634,7 +624,7 @@ ListenToGameEvent("player_removed_item_from_itemholder", listenEventPlayerRemove -- No known way to track resin being taken out reliably. ---@class PlayerEventPlayerDropResinInBackpack : GameEventPlayerDropResinInBackpack ----@field resin_ent EntityHandle? # The resin entity being dropped into the backpack. +---@field resin_ent EntityHandle? # The resin entity being dropped into the backpack ---Track resin ---@param data GameEventPlayerDropResinInBackpack @@ -663,8 +653,9 @@ 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. +---@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 +---@field hand CPropVRHand # Hand that the entity was switched to ---Track weapon equipped ---@param data GameEventWeaponSwitch @@ -673,51 +664,26 @@ local function listenEventWeaponSwitch(data) -- Debug.PrintTable(data) -- print("\n") - Player.PreviouslyEquipped = Player.CurrentlyEquipped + if not playerData.WeaponStateSyncPaused() then + Player:Delay(function() + local weaponHandle, handHandle = playerData.SyncEquippedWeaponState(data.item) - ---@type EntityHandle - local weaponHandle = nil + -- Nil return means sync in paused + if not weaponHandle then return end - if data.item == "hand_use_controller" then - Player.CurrentlyEquipped = PLAYER_WEAPON_HAND - else - 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 - elseif data.item == "hlvr_weapon_rapidfire" then - Player.CurrentlyEquipped = PLAYER_WEAPON_RAPIDFIRE - Player.Items.weapons.rapidfire = weaponHandle - elseif data.item == "hlvr_weapon_shotgun" then - Player.CurrentlyEquipped = PLAYER_WEAPON_SHOTGUN - Player.Items.weapons.shotgun = weaponHandle - elseif data.item == "hlvr_multitool" then - Player.CurrentlyEquipped = PLAYER_WEAPON_MULTITOOL - Player.Items.weapons.multitool = weaponHandle - elseif data.item == "hlvr_weapon_generic_pistol" then - Player.CurrentlyEquipped = PLAYER_WEAPON_GENERIC_PISTOL - - -- Add this custom pistol if this is the first time encountering it - if not vlua.find(Player.Items.weapons.genericpistols, weaponHandle) then - table.insert(Player.Items.weapons.genericpistols, weaponHandle) + -- Hand entity isn't returned, so nil it + if weaponHandle:GetClassname() == "hand_use_controller" then + weaponHandle = nil end - end - end - Player:UpdateWeaponsExistence() - - -- This event can fire before the player has a hand - if Player.PrimaryHand then - Player.PrimaryHand.ItemHeld = weaponHandle + -- Registered callback + local newdata = vlua.clone(data)--[[@as PlayerEventWeaponSwitch]] + newdata.item = weaponHandle + newdata.item_class = data.item + newdata.hand = handHandle + eventCallback(data.game_event_name, newdata) + end, 0.01) -- smallest delay for hand_use_controller end - - savePlayerData() - - -- Registered callback - local newdata = vlua.clone(data)--[[@as PlayerEventWeaponSwitch]] - newdata.item = weaponHandle - newdata.item_class = data.item - eventCallback(data.game_event_name, newdata) end ListenToGameEvent("weapon_switch", listenEventWeaponSwitch, nil) diff --git a/scripts/vscripts/alyxlib/player/hands.lua b/scripts/vscripts/alyxlib/player/hands.lua index d4f57cf..42649dc 100644 --- a/scripts/vscripts/alyxlib/player/hands.lua +++ b/scripts/vscripts/alyxlib/player/hands.lua @@ -1,16 +1,21 @@ --[[ - v1.3.0 + v1.3.1 https://github.com/FrostSource/alyxlib Code for player hands. + If not using `alyxlib/init.lua`, load this file at game start using the following line: + + require "alyxlib.player.hands" ]] -local version = "v1.3.0" +local version = "v1.3.1" ----Merge an existing prop with this hand. ----@param prop EntityHandle|string # The prop handle or targetname. ----@param hide_hand boolean # If the hand should turn invisible after merging. +--- +---Merges an existing prop with this hand. +--- +---@param prop EntityHandle|string # The prop handle or targetname +---@param hide_hand boolean # `true` if the hand should turn invisible after merging function CPropVRHand:MergeProp(prop, hide_hand) if type(prop) == "string" then prop = Entities:FindByName(nil, prop) @@ -29,14 +34,18 @@ function CPropVRHand:MergeProp(prop, hide_hand) end end ----Return true if this hand is currently holding a prop. ----@return boolean +--- +---Checks if this hand is currently holding a prop. +--- +---@return boolean # `true` if the hand is holding a prop function CPropVRHand:IsHoldingItem() return IsEntity(self.ItemHeld, true) end ----Drop the item held by this hand. ----@return EntityHandle? +--- +---Drops the item held by this hand. +--- +---@return EntityHandle? # The entity that was dropped function CPropVRHand:Drop() if self.ItemHeld ~= nil then Player:DropByHandle(self.ItemHeld) @@ -45,8 +54,10 @@ function CPropVRHand:Drop() end end ----Grab the entity ----@param ent EntityHandle|string +--- +---Forces this hand to grab an entity. +--- +---@param ent EntityHandle|string # The entity or targetname to grab function CPropVRHand:Grab(ent) if type(ent) == "string" then local name = ent @@ -59,34 +70,44 @@ function CPropVRHand:Grab(ent) ent:Grab(self) end ----Get the rendered glove entity for this hand, i.e. the first `hlvr_prop_renderable_glove` class. ----@return EntityHandle|nil +--- +---Gets the rendered glove entity for this hand, +---i.e. the first `hlvr_prop_renderable_glove` class. +--- +---@return EntityHandle? # The glove entity function CPropVRHand:GetGlove() return self:GetFirstChildWithClassname("hlvr_prop_renderable_glove") end ----Get the entity for this hands grabbity glove (the animated part on the glove). ----@return EntityHandle|nil +--- +---Gets grabbity glove entity for this hand (the animated part on the glove). +--- +---@return EntityHandle|nil # The grabbity glove function CPropVRHand:GetGrabbityGlove() return self:GetFirstChildWithClassname("prop_grabbity_gloves") end ----Returns true if the digital action is on for this. See `ENUM_DIGITAL_INPUT_ACTIONS` for action index values. +--- +---Checks if a digital action is on for this hand. +--- ---Note: Only reports input when headset is awake. Will still transmit input when controller loses tracking. ----@param digitalAction DigitalInputAction ----@return boolean +--- +---@see DigitalInputAction +---@param digitalAction DigitalInputAction # The action to check +---@return boolean # `true` if the action is on function CPropVRHand:IsButtonPressed(digitalAction) return Player:IsDigitalActionOnForHand(self.Literal, digitalAction) end --- ----Get the position of the palm of this hand. +---Gets the position of the palm of this hand. --- ---Returns the palm of the glove if it exists, otherwise the palm of the invisible hand. +--- ---Sometimes the glove becomes desynchronized with the hand, such as interacting with a handpose or holding a weapon, ---so this function will try to return the position of the visible palm whenever possible. --- ----@return Vector +---@return Vector # The palm position function CPropVRHand:GetPalmPosition() local glove = self:GetGlove() if glove then @@ -99,7 +120,7 @@ end --- ---Gets the 'hand_use_controller' entity associated with this hand. --- ----@return EntityHandle +---@return EntityHandle # The hand_use_controller function CPropVRHand:GetHandUseController() for _, controller in ipairs(Entities:FindAllByClassname("hand_use_controller")) do if controller:GetOwner() == self then @@ -109,16 +130,20 @@ function CPropVRHand:GetHandUseController() end end +--- ---Forces the player to drop this entity if held. ----@param self CBaseEntity +--- function CBaseEntity:Drop() Player:DropByHandle(self) end Expose(CBaseEntity.Drop, "Drop", CBaseEntity) ----Force the player to grab this entity with a hand. +--- +---Forces the player to grab this entity with a hand. +--- ---If no hand is supplied then the nearest hand will be used. ----@param hand? CPropVRHand|0|1 +--- +---@param hand? CPropVRHand|0|1 # Hand to grab with function CBaseEntity:Grab(hand) Player:GrabByHandle(self, hand) end diff --git a/scripts/vscripts/alyxlib/player/wrist_attachments.lua b/scripts/vscripts/alyxlib/player/wrist_attachments.lua index b7b84e6..b73b391 100644 --- a/scripts/vscripts/alyxlib/player/wrist_attachments.lua +++ b/scripts/vscripts/alyxlib/player/wrist_attachments.lua @@ -1,11 +1,18 @@ --[[ - v1.2.0 + v1.2.1 https://github.com/FrostSource/alyxlib + + Wrist attachments allow you to attach entities to the player's hands + in an addon shared way. + + If not using `alyxlib/init.lua`, load this file at game start using the following line: + + require "alyxlib.player.wrist_attachments" ]] ---@class WristAttachments WristAttachments = {} -WristAttachments.version = "v1.2.0" +WristAttachments.version = "v1.2.1" ---@alias WristAttachmentHandType "left"|"right"|"primary"|"secondary" @@ -53,14 +60,15 @@ local function getHand(attachment) end end - ----Add a new entity as a wrist attachment. ----@param entity EntityHandle # The entity which will become a wrist attachment. ----@param hand? WristAttachmentHandType # The hand type to attach to initially. ----@param length number # Physical length of the entity to make sure it will not overlap with other wrist attachments. ----@param priority number? # Priority for the entity when there are other wrist attacments. Lower number is higher priority. Cannot specify a value lower than 0. ----@param offset Vector? # Optional offset for the entity (x component is ignored). ----@param angles QAngle? # Optional angles for the entity. +--- +---Adds a new entity as a wrist attachment. +--- +---@param entity EntityHandle # The entity which will become a wrist attachment +---@param hand? WristAttachmentHandType # The hand type to attach to initially +---@param length number # Physical length of the entity to make sure it will not overlap with other wrist attachments +---@param priority number? # Priority for the entity when there are other wrist attacments - lower number is higher priority, cannot specify a value lower than 0 +---@param offset Vector? # Optional origin offset for the entity (x component is ignored) +---@param angles QAngle? # Optional angles offset for the entity function WristAttachments:Add(entity, hand, length, priority, offset, angles) if self:IsEntityAttached(entity) then @@ -81,11 +89,13 @@ function WristAttachments:Add(entity, hand, length, priority, offset, angles) self:Update() end ----Set the hand that the entity should be attached to. ----@param entity EntityHandle # The entity to change data for. ----@param hand WristAttachmentHandType # The type of hand to attach to. ----@param offset Vector # Optional offset for the entity (x component is ignored). ----@param angles QAngle # Optional angles for the entity. +--- +---Sets the hand that the entity should be attached to. +--- +---@param entity EntityHandle # The entity to change data for +---@param hand WristAttachmentHandType # The type of hand to attach to +---@param offset? Vector # Optional origin offset for the entity (x component is ignored) +---@param angles? QAngle # Optional angles offset for the entity function WristAttachments:SetHand(entity, hand, offset, angles) local attachment = self:GetEntityAttachment(entity) if attachment then @@ -98,9 +108,11 @@ function WristAttachments:SetHand(entity, hand, offset, angles) end end ----Get the hand that the entity is attached to. ----@param entity EntityHandle ----@return CPropVRHand? +--- +---Gets the hand that the entity is attached to. +--- +---@param entity EntityHandle # The entity to get the hand for +---@return CPropVRHand? # The hand that the entity is attached to function WristAttachments:GetHand(entity) for _, attachment in ipairs(self.attachments) do if attachment.entity == entity then @@ -109,9 +121,11 @@ function WristAttachments:GetHand(entity) end end ----Get the attachment data related to an attach entity. ----@param entity EntityHandle # The entity to get the data for. ----@return WristAttachmentData? # The attachment data for the entity, if it is attached. +--- +---Gets the attachment data related to an attach entity. +--- +---@param entity EntityHandle # The entity to get the data for +---@return WristAttachmentData? # The attachment data for the entity, or `nil` if not found function WristAttachments:GetEntityAttachment(entity) for _, attachment in ipairs(self.attachments) do if attachment.entity == entity then @@ -121,13 +135,21 @@ function WristAttachments:GetEntityAttachment(entity) return nil end ----Get if an entity is attached to a wrist using this system. ----@param entity EntityHandle # The entity to check. ----@return boolean # True if attached, false otherwise. +--- +---Checks if an entity is attached to a wrist using this system. +--- +---@param entity EntityHandle # The entity to check +---@return boolean # `true` if attached, `false` otherwise. function WristAttachments:IsEntityAttached(entity) return self:GetEntityAttachment(entity) ~= nil end +--- +---Updates the attachments. +--- +---This is called automatically when an attachment is added or removed +---or when the player's primary hand changes. +--- function WristAttachments:Update() local offsets = { [Player.LeftHand] = 1, diff --git a/scripts/vscripts/alyxlib/precache.lua b/scripts/vscripts/alyxlib/precache.lua index d728826..1f7431a 100644 --- a/scripts/vscripts/alyxlib/precache.lua +++ b/scripts/vscripts/alyxlib/precache.lua @@ -1,39 +1,27 @@ --[[ - v1.0.2 + v1.1.0 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 precached when the player spawns, allowing you to precache assets from your global scripts. - If not using `vscripts/alyxlib/core.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: - ```lua require "alyxlib.precache" - ``` - - ======================================== Usage ======================================== - - Add assets to be precached in the following way: - - ```lua - GlobalPrecache("model", "models/weapons/vr_alyxgun/vr_alyxgun_clip.vmdl") - GlobalPrecache("entity", "prop_dynamic", { - model = "models/weapons/vr_alyxgun/vr_alyxgun_clip.vmdl" - }) - ``` ]] if thisEntity then ---@param context CScriptPrecacheContext function Precache(context) - _PrecacheGlobalItems(context) + GlobalPrecache:_PrecacheGlobalItems(context) end return end -local version = "v1.0.2" - -require "alyxlib.player.core" +---@class GlobalPrecache +---@overload fun(type: AlyxLibGlobalPrecacheType, path: string, spawnkeys: table?): nil +GlobalPrecache = {} +GlobalPrecache.version = "v1.1.0" ---@alias AlyxLibGlobalPrecacheType ---| "model_folder" @@ -44,51 +32,141 @@ require "alyxlib.player.core" ---| "model" ---| "entity" +--- +---An asset to be globally precached. +--- ---@class AlyxLibGlobalPrecacheItem ----@field type AlyxLibGlobalPrecacheType # The type of asset to precache. ----@field path string # The asset path to precache (or the classname if type is entity). ----@field spawnkeys table? # The spawnkeys table if type is entity. +---@field type AlyxLibGlobalPrecacheType # The type of asset to precache +---@field path string # The asset path to precache (or the classname if type is entity) +---@field spawnkeys table? # The spawnkeys table if type is entity +--- +---List of assets waiting to be precached. +--- ---@type AlyxLibGlobalPrecacheItem[] -_G.AlyxLibGlobalPrecacheList = {} - ----Add an asset to be precached when the player spawns. ----@param type AlyxLibGlobalPrecacheType # The type of asset to precache. ----@param path string # The asset path to precache (or the classname if type is entity). ----@param spawnkeys table? # The spawnkeys table if type is entity. -function GlobalPrecache(type, path, spawnkeys) - table.insert(AlyxLibGlobalPrecacheList, { +GlobalPrecache._assets = {} + +--- +---List of precache callbacks. +--- +---@type function[] +GlobalPrecache._callbacks = {} + +--- +---Adds an asset to be precached when the player activates. +--- +---If you are precaching *after* the player has activated, +---then you must also call [GlobalPrecache:Flush](lua://GlobalPrecache.Flush) +---after adding assets to be precached. +--- +---@param type AlyxLibGlobalPrecacheType # The type of asset to precache +---@param path string # The asset path to precache (or the classname if `type` is an entity) +---@param spawnkeys table? # The spawnkeys table if type is entity +function GlobalPrecache:Add(type, path, spawnkeys) + table.insert(self._assets, { type = type, path = path, spawnkeys = spawnkeys }) end +-- Backwards compatibility, allow GlobalPrecache() +setmetatable(GlobalPrecache, { + __call=function(_, type, path, spawnkeys) + Warning(("Use GlobalPrecache:Add(...) instead of GlobalPrecache(%q, %q).\n"):format(type, path)) + GlobalPrecache:Add(type, path, spawnkeys) + end +}) + +---Starts the precache process. +local function precacheAsync() + SpawnEntityFromTableAsynchronous("logic_script", { + vscripts = "alyxlib/precache", + }, function (spawnedEnt) + spawnedEnt:Kill() + end, nil) +end + +--- +---Returns whether assets are waiting to be precached. +--- +function GlobalPrecache:IsPending() + return #self._assets > 0 +end + +--- +---Arrange to call the provided functions once all assets are precached. +---If none are currently pending, call immediately. Otherwise, store the callback +---to be called once [GlobalPrecache:Flush()](lua://GlobalPrecache.Flush) is finished. +--- +---@param callback function # The function to call when the precaching is complete +function GlobalPrecache:OnFinished(callback) + if GlobalPrecache:IsPending() then + table.insert(self._callbacks, callback) + else + callback() + end +end + +--- +---Flushes the global precache list and precaches the assets. +--- +---This function must be called following any calls to [GlobalPrecache:Add()](lua://GlobalPrecache.Add) +---if you are precaching *after* the player has activated. +--- +---This is an asynchronous process; the assets will not be immediately available after calling this function. +--- +---@param callback? function # The function to call when the precaching is complete +function GlobalPrecache:Flush(callback) + -- Guard against non-existant player crash + if not Entities:GetLocalPlayer() then + Warning("Cannot flush precache while player does not exist!\n") + return + end + + if type(callback) == "function" then + -- Unconditionally insert, precacheAsync will call this. + table.insert(self._callbacks, callback) + end + precacheAsync() +end + +--- ---Internal function used to start the precache process. --- ---**This should only be called manually if you know what you're doing!** +--- ---@param context CScriptPrecacheContext -function _PrecacheGlobalItems(context) - if #AlyxLibGlobalPrecacheList > 0 then - devprints("Globally precaching", #AlyxLibGlobalPrecacheList, "resources...") - for _, item in ipairs(AlyxLibGlobalPrecacheList) do - devprints("\tPrecaching", item.type, item.path) - if item.type == "model" then - PrecacheModel(item.path, context) - elseif item.type == "entity" then - PrecacheEntityFromTable(item.path, item.spawnkeys, context) - else - PrecacheResource(item.type, item.path, context) - end +function GlobalPrecache:_PrecacheGlobalItems(context) + devprints("Globally precaching", #self._assets, "resources...") + -- Use a while loop and pop here, so any errors don't discard items. + while #self._assets > 0 do + local item = table.remove(self._assets) + devprints2("\tPrecaching", item.type, item.path) + if item.type == "model" then + PrecacheModel(item.path, context) + elseif item.type == "entity" then + PrecacheEntityFromTable(item.path, item.spawnkeys, context) + else + PrecacheResource(item.type, item.path, context) end end + -- Protect against callbacks added by a callback. + while #self._callbacks > 0 do + local cback = table.remove(self._callbacks) + cback() + end end -ListenToPlayerEvent("player_activate", function (params) - SpawnEntityFromTableAsynchronous("logic_script", { - vscripts = "alyxlib/precache" - }, function (spawnedEnt) - end, nil) -end) +require "alyxlib.player.events" + +local listento = ListenToPlayerEvent or ListenToGameEvent + +---player_activate seems to be the soonest time to async spawn +---in all circumstances without crashing. +---@param event GameEventPlayerSpawn +listento("player_activate", function (event) + precacheAsync() +end, nil) -return version \ No newline at end of file +return GlobalPrecache.version diff --git a/scripts/vscripts/alyxlib/storage.lua b/scripts/vscripts/alyxlib/storage.lua index 78f7674..d0d570c 100644 --- a/scripts/vscripts/alyxlib/storage.lua +++ b/scripts/vscripts/alyxlib/storage.lua @@ -1,127 +1,14 @@ --[[ - v3.2.0 + v3.3.0 https://github.com/FrostSource/alyxlib Helps with saving/loading values for persistency between game sessions. Data is saved on a specific entity and if the entity is killed the values cannot be retrieved during that game session. - If not using `vscripts/alyxlib/core.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: - ```lua require "alyxlib.storage" - ``` - - ======================================== Usage ========================================== - - - - Functions are accessed through the global 'Storage' table. - When using dot notation (Storage.SaveNumber) you must provide the entity handle to save on. - When using colon notation (Storage:SaveNumber) the script will find the best handle to save on (usually the player). - - Examples of storing and retrieving the following local values that might be defined - at the top of the file: - - ```lua - -- Getting the values to save - local origin = thisEntity:GetOrigin() - local hp = thisEntity:GetHealth() - local name = thisEntity:GetName() - - -- Saving the values: - - Storage:SaveVector("origin", origin) - Storage:SaveNumber("hp", hp) - Storage:SaveString("name", name) - - -- Loading the values: - - origin = Storage:LoadVector("origin") - hp = Storage:LoadNumber("hp") - name = Storage:LoadString("name") - ``` - - This script also provides functions Storage.Save/Storage.Load for general purpose - type inference storage. It is still recommended that you use the explicit type - functions to keep code easily understandable, but it can help with prototyping: - - ```lua - Storage:Save("origin", origin) - Storage:Save("hp", hp) - Storage:Save("name", name) - - origin = Storage:Load("origin", origin) - hp = Storage:Load("hp", hp) - name = Storage:Load("name", name) - ``` - - Entity versions of the functions exist to make saving to a specific entity easier: - - ```lua - thisEntity:SaveNumber("hp", thisEntity:GetHealth()) - thisEntity:SetHealth(thisEntity:LoadNumber("hp")) - ``` - - ======================================= Complex Tables ======================================== - - Since Lua allows tables to have keys and values to be virtually any value, `Storage.SaveTable` - attempts to save and restore both using their native Storage functions, and does so recursively - meaning nested tables can be saved. - - An example of safely restoring a class instance (assuming the class has a `new` method) - where an instance is created and then the saved values are restored and added to the instance: - - ```lua - vlua.tableadd(MyClass:new(), thisEntity:LoadTable("MyClass", {})) - ``` - - Alternatively if the class uses a metatable and you have access to it: - - ```lua - setmetatable(thisEntity:LoadTable("MyClass", {}), MyClass) - ``` - - Functions for both key and value are currently not supported and will fail to save but will not - block the rest of the table from being saved. This means you can save whole class tables and - restore them with only the relevant saved data. - - =================================== Delegate Save Functions =================================== - - Tables can utilize __save/__load functions to handle their specific saving needs. - The table with these functions needs to be registered with the Storage module and any instances - must have an `__index` key pointing to the base table: - - ```lua - MyClass.__index = MyClass - Storage.RegisterType("MyClass", MyClass) - ``` - - The functions may handle the data saving however they want but should generally use the base - Storage functions to keep things safe and consistent. The below template functions may be - copied to your script as a starting point. - See `data/stack.lua` for an example of these functions in action. - - ```lua - ---@param handle EntityHandle # The entity to save on. - ---@param name string # The name to save as. - ---@param object MyClass # The object to save. - ---@return boolean # If the save was successful. - function MyClass.__save(handle, name, object) - return Storage.SaveTableCustom(handle, name, object, "MyClass") - end - - ---@param handle EntityHandle # Entity to load from. - ---@param name string # Name to load. - ---@return MyClass|nil # The loaded value. nil on fail. - function MyClass.__load(handle, name) - local object = Storage.LoadTableCustom(handle, name, "MyClass") - if object == nil then return nil end - return setmetatable(object, MyClass) - end - ``` - - Storage.Save and Storage.Load will now invoke these functions when appropriate. =========================================== Notes ============================================= @@ -146,10 +33,10 @@ local function Warn(msg) end --- ----Resolve a given handle. +---Resolves a given handle. --- ----@param handle EntityHandle # Handle to resolve. ----@return EntityHandle # Resolved save handle. +---@param handle EntityHandle # Handle to resolve +---@return EntityHandle # Resolved save handle local function resolveHandle(handle) -- Keep given handle if valid entity if handle ~= nil and handle ~= Storage and IsValidEntity(handle) then @@ -171,7 +58,7 @@ end local separator = "::" Storage = {} -Storage.version = "v3.2.0" +Storage.version = "v3.3.0" ---Collection of type names associated with a class table. ---The table should have both __save() and __load() functions. ---@type table @@ -179,41 +66,42 @@ Storage.type_to_class = {} Storage.class_to_type = {} --- ----Register a class table type with a name. +---Registers a class table type with a name. --- ----@param name string # Name that the type will be saved as. ----@param T table # Class table. +---@param name string # Name that the type will be saved as +---@param T table # Class table function Storage.RegisterType(name, T) Storage.type_to_class[name] = T Storage.class_to_type[T] = name end --- ----Unregister a class type. +---Unregisters a class type. --- ----@param name string # Name to unregister. ----@param T table # Class to unregister. +---@param name string # Name to unregister +---@param T table # Class to unregister function Storage.UnregisterType(name, T) Storage.type_to_class[name] = nil Storage.class_to_type[T] = nil end --- ----Join a list of values by the hidden separator. +---Joins a list of values by the hidden separator. --- ----@param ... any # Values to join. ----@return string # Joined string. +---@param ... any # Values to join +---@return string # Joined string function Storage.Join(...) return table.concat({...}, separator) end --- ---Helper function for saving the type correctly. +--- ---No failsafes are provided in this function, you must be sure you are saving correctly. --- ----@param handle EntityHandle # Entity to save on. ----@param name string # Name prefix to save as. ----@param T string # String name of `T`. +---@param handle EntityHandle # Entity to save on +---@param name string # Name prefix to save as +---@param T string # String name of `T` function Storage.SaveType(handle, name, T) handle:SetContext(Storage.Join(name, "type"), T, 0) end @@ -223,12 +111,12 @@ end ------------ --- ----Save a string. +---Saves a string. --- ----@param handle EntityHandle # Entity to save on. ----@param name string # Name to save as. ----@param value string|nil # String to save. ----@return boolean # If the save was successful. +---@param handle EntityHandle # Entity to save on +---@param name string # Name to save as +---@param value string|nil # String to save +---@return boolean # If the save was successful function Storage.SaveString(handle, name, value) handle = resolveHandle(handle) if not handle then @@ -260,54 +148,75 @@ function Storage.SaveString(handle, name, value) end --- ----Save a number. +---Saves a number. --- ----@param handle EntityHandle # Entity to save on. ----@param name string # Name to save as. ----@param value number # Number to save. ----@return boolean # If the save was successful. +---@param handle EntityHandle # Entity to save on +---@param name string # Name to save as +---@param value number|nil # Number to save +---@return boolean # If the save was successful function Storage.SaveNumber(handle, name, value) handle = resolveHandle(handle) if not handle then Warn("Invalid save handle ("..tostring(handle)..")!") return false end + + if value == nil then + handle:SetContext(name, nil, 0) + handle:SetContext(name..separator.."type", nil, 0) + return true + end + handle:SetContextNum(name, value, 0) handle:SetContext(name..separator.."type", "number", 0) return true end --- ----Save a boolean. +---Saves a boolean. --- ----@param handle EntityHandle # Entity to save on. ----@param name string # Name to save as. ----@param bool boolean # Boolean to save. ----@return boolean # If the save was successful. +---@param handle EntityHandle # Entity to save on +---@param name string # Name to save as +---@param bool boolean|nil # Boolean to save +---@return boolean # If the save was successful function Storage.SaveBoolean(handle, name, bool) handle = resolveHandle(handle) if not handle then Warn("Invalid save handle ("..tostring(handle)..")!") return false end + + if bool == nil then + handle:SetContext(name, nil, 0) + handle:SetContext(name..separator.."type", nil, 0) + return true + end + handle:SetContextNum(name, bool and 1 or 0, 0) handle:SetContext(name..separator.."type", "boolean", 0) return true end --- ----Save a Vector. +---Saves a Vector. --- ----@param handle EntityHandle # Entity to save on. ----@param name string # Name to save as. ----@param vector Vector # Vector to save. ----@return boolean # If the save was successful. +---@param handle EntityHandle # Entity to save on +---@param name string # Name to save as +---@param vector Vector|nil # Vector to save +---@return boolean # If the save was successful function Storage.SaveVector(handle, name, vector) handle = resolveHandle(handle) if not handle then Warn("Invalid save handle ("..tostring(handle)..")!") return false end + + if vector == nil then + handle:SetContext(name, nil, 0) + handle:SetContext(name..separator.."type", nil, 0) + return true + end + handle:SetContext(name, "", 0) handle:SetContext(name..separator.."type", "vector", 0) Storage.SaveNumber(handle, name..separator.."x", vector.x) @@ -317,18 +226,25 @@ function Storage.SaveVector(handle, name, vector) end --- ----Save a QAngle. +---Saves a QAngle. --- ----@param handle EntityHandle # Entity to save on. ----@param name string # Name to save as. ----@param qangle QAngle # QAngle to save. ----@return boolean # If the save was successful. +---@param handle EntityHandle # Entity to save on +---@param name string # Name to save as +---@param qangle QAngle|nil # QAngle to save +---@return boolean # If the save was successful function Storage.SaveQAngle(handle, name, qangle) handle = resolveHandle(handle) if not handle then Warn("Invalid save handle ("..tostring(handle)..")!") return false end + + if qangle == nil then + handle:SetContext(name, nil, 0) + handle:SetContext(name..separator.."type", nil, 0) + return true + end + handle:SetContext(name, "", 0) handle:SetContext(name..separator.."type", "qangle", 0) Storage.SaveNumber(handle, name..separator.."x", qangle.x) @@ -338,17 +254,17 @@ function Storage.SaveQAngle(handle, name, qangle) end --- ----Save a table with a custom type. +---Saves a table with a custom type. ---Should be used with custom save functions. --- ---If trying to save a normal table use `Storage.SaveTable`. --- ----@param handle EntityHandle # Entity to save on. ----@param name string # Name to save as. ----@param tbl table # Table to save. ----@param T string # Type to save as. ----@param save_meta? boolean # If keys starting with '__' should be saved. ----@return boolean # If the save was successful. +---@param handle EntityHandle # Entity to save on +---@param name string # Name to save as +---@param tbl table # Table to save +---@param T string # Type to save as +---@param save_meta? boolean # If keys starting with '__' should be saved +---@return boolean # If the save was successful function Storage.SaveTableCustom(handle, name, tbl, T, save_meta) handle = resolveHandle(handle) if not handle then @@ -378,30 +294,30 @@ function Storage.SaveTableCustom(handle, name, tbl, T, save_meta) end --- ----Save a table. +---Saves a table. --- ---May be ordered, unordered or mixed. --- ---May have nested tables. --- ----@param handle EntityHandle # Entity to save on. ----@param name string # Name to save as. ----@param tbl table # Table to save. ----@return boolean # If the save was successful. +---@param handle EntityHandle # Entity to save on +---@param name string # Name to save as +---@param tbl table # Table to save +---@return boolean # If the save was successful function Storage.SaveTable(handle, name, tbl) return Storage.SaveTableCustom(handle, name, tbl, "table") end --- ----Save an entity reference. +---Saves an entity reference. --- ---Entity handles change between game sessions so this function ---modifies the passed entity to make sure it can keep track of it. --- ----@param handle EntityHandle # Entity to save on. ----@param name string # Name to save as. ----@param entity EntityHandle|nil # Entity to save. ----@return boolean # If the save was successful. +---@param handle EntityHandle # Entity to save on +---@param name string # Name to save as +---@param entity EntityHandle|nil # Entity to save +---@return boolean # If the save was successful function Storage.SaveEntity(handle, name, entity, useClassname) handle = resolveHandle(handle) @@ -416,10 +332,10 @@ function Storage.SaveEntity(handle, name, entity, useClassname) end if entity == nil then - Storage.SaveString(handle, name..separator.."targetname", "") - Storage.SaveString(handle, name..separator.."classname", "") - handle:SetContext(name..separator.."unique", "", 0) - handle:SetContext(name..separator.."type", "entity", 0) + Storage.SaveString(handle, name..separator.."targetname", nil) + Storage.SaveString(handle, name..separator.."classname", nil) + handle:SetContext(name..separator.."unique", nil, 0) + handle:SetContext(name..separator.."type", nil, 0) return true elseif not entity or not IsValidEntity(entity) then return false @@ -441,15 +357,15 @@ local _vector = getmetatable(Vector()) local _qangle = getmetatable(QAngle()) --- ----Save a value. +---Saves a value. --- ---Uses type inference to save the value. ---If you are experiencing errors consider saving with one of the explicit type saves. --- ----@param handle EntityHandle # Entity to save on. ----@param name string # Name to save as. ----@param value any # Value to save. ----@return boolean # If the save was successful. +---@param handle EntityHandle # Entity to save on +---@param name string # Name to save as +---@param value any # Value to save +---@return boolean # If the save was successful function Storage.Save(handle, name, value) local t = type(value) if t=="function" then Warn("Functions are not supported for saving yet.") return false @@ -478,16 +394,17 @@ end ------------- --- ----Load a string. +---Loads a string. --- ---@generic T ----@param handle EntityHandle # Entity to load from. ----@param name string # Name the string was saved as. ----@param default? T # Optional default value. ----@return string|T # Saved string or `default`. +---@param handle EntityHandle # Entity to load from +---@param name string # Name the string was saved as +---@param default? T # Optional default value +---@return string|T # Saved string or `default` function Storage.LoadString(handle, name, default) handle = resolveHandle(handle) local t = handle:GetContext(name..separator.."type") + if t == "string" then local value = handle:GetContext(name) if value == nil then return default end @@ -505,13 +422,13 @@ function Storage.LoadString(handle, name, default) end --- ----Load a number. +---Loads a number. --- ---@generic T ----@param handle EntityHandle # Entity to load from. ----@param name string # Name the number was saved as. ----@param default? T # Optional default value. ----@return number|T # Saved number or `default`. +---@param handle EntityHandle # Entity to load from +---@param name string # Name the number was saved as +---@param default? T # Optional default value +---@return number|T # Saved number or `default` function Storage.LoadNumber(handle, name, default) handle = resolveHandle(handle) local t = handle:GetContext(name..separator.."type") @@ -525,13 +442,13 @@ function Storage.LoadNumber(handle, name, default) end --- ----Load a boolean value. +---Loads a boolean value. --- ---@generic T ----@param handle EntityHandle # Entity to load from. ----@param name string # Name the boolean was saved as. ----@param default? T # Optional default value. ----@return boolean|T # Saved boolean or `default`. +---@param handle EntityHandle # Entity to load from +---@param name string # Name the boolean was saved as +---@param default? T # Optional default value +---@return boolean|T # Saved boolean or `default` function Storage.LoadBoolean(handle, name, default) handle = resolveHandle(handle) local t = handle:GetContext(name..separator.."type") @@ -544,13 +461,13 @@ function Storage.LoadBoolean(handle, name, default) end --- ----Load a Vector. +---Loads a Vector. --- ---@generic T ----@param handle EntityHandle # Entity to load from. ----@param name string # Name the Vector was saved as. ----@param default? T # Optional default value. ----@return Vector|T # Saved Vector or `default`. +---@param handle EntityHandle # Entity to load from +---@param name string # Name the Vector was saved as +---@param default? T # Optional default value +---@return Vector|T # Saved Vector or `default` function Storage.LoadVector(handle, name, default) handle = resolveHandle(handle) local t = handle:GetContext(name..separator.."type") @@ -566,13 +483,13 @@ function Storage.LoadVector(handle, name, default) end --- ----Load a QAngle. +---Loads a QAngle. --- ---@generic T ----@param handle EntityHandle # Entity to load from. ----@param name string # Name the QAngle was saved as. ----@param default? T # Optional default value. ----@return QAngle|T # Saved QAngle or `default`. +---@param handle EntityHandle # Entity to load from +---@param name string # Name the QAngle was saved as +---@param default? T # Optional default value +---@return QAngle|T # Saved QAngle or `default` function Storage.LoadQAngle(handle, name, default) handle = resolveHandle(handle) local t = handle:GetContext(name..separator.."type") @@ -588,17 +505,17 @@ function Storage.LoadQAngle(handle, name, default) end --- ----Load a table with a custom type. +---Loads a table with a custom type. ---Should be used with custom load functions. --- ---If trying to load a normal table use `Storage.LoadTable`. --- ---@generic T ----@param handle EntityHandle # Entity to load from. ----@param name string # Name the table was saved as. ----@param T string # Type to save as. ----@param default? T # Optional default value. ----@return table|T # Saved table or `default`. +---@param handle EntityHandle # Entity to load from +---@param name string # Name the table was saved as +---@param T string # Type to save as +---@param default? T # Optional default value +---@return table|T # Saved table or `default` function Storage.LoadTableCustom(handle, name, T, default) handle = resolveHandle(handle) local name_sep = name..separator @@ -621,25 +538,25 @@ function Storage.LoadTableCustom(handle, name, T, default) end --- ----Load a table with a custom type. +---Loads a table with a custom type. --- ---@generic T ----@param handle EntityHandle # Entity to load from. ----@param name string # Name the table was saved as. ----@param default? T # Optional default value. ----@return table|T # Saved table or `default`. +---@param handle EntityHandle # Entity to load from +---@param name string # Name the table was saved as +---@param default? T # Optional default value +---@return table|T # Saved table or `default` function Storage.LoadTable(handle, name, default) return Storage.LoadTableCustom(handle, name, "table", default) end --- ----Load an entity. +---Loads an entity. --- ---@generic T ----@param handle EntityHandle # Entity to load from. ----@param name string # Name to save as. ----@param default? T # Optional default value. ----@return EntityHandle|T # Saved entity or `default`. +---@param handle EntityHandle # Entity to load from +---@param name string # Name to save as +---@param default? T # Optional default value +---@return EntityHandle|T # Saved entity or `default` function Storage.LoadEntity(handle, name, default) handle = resolveHandle(handle) local t = handle:GetContext(name..separator.."type") @@ -677,12 +594,12 @@ function Storage.LoadEntity(handle, name, default) end --- ----Load a value. +---Loads a value. --- ----@param handle EntityHandle # Entity to load from. ----@param name string # Name the value was saved as. ----@param default? any # Optional default value. ----@return any # Saved value or `default`. +---@param handle EntityHandle # Entity to load from +---@param name string # Name the value was saved as +---@param default? any # Optional default value +---@return any # Saved value or `default` function Storage.Load(handle, name, default) handle = resolveHandle(handle) local t = handle:GetContext(name..separator.."type") @@ -709,10 +626,10 @@ function Storage.Load(handle, name, default) end end ----Load all values saved to an entity. ----@param handle EntityHandle # Entity to load from. ----@param direct? boolean # Optionally load values directly into `handle` instead of a new table. ----@return table # Table of loaded values (or `handle` if `direct` is true). +---Loads all values saved to an entity. +---@param handle EntityHandle # Entity to load from +---@param direct? boolean # Optionally load values directly into `handle` instead of a new table +---@return table # Table of loaded values (or `handle` if `direct` is true) function Storage.LoadAll(handle, direct) local tbl = direct and handle or {} handle = resolveHandle(handle) diff --git a/scripts/vscripts/alyxlib/utils/common.lua b/scripts/vscripts/alyxlib/utils/common.lua index 88e9d49..086b098 100644 --- a/scripts/vscripts/alyxlib/utils/common.lua +++ b/scripts/vscripts/alyxlib/utils/common.lua @@ -1,14 +1,13 @@ --[[ - v3.1.0 + v3.1.1 https://github.com/FrostSource/alyxlib - This file contains utility functions to help reduce repetitive code and add general miscellaneous functionality. + This file contains utility functions to help reduce repetitive code + and add general miscellaneous functionality. - If not using `vscripts/alyxlib/core.lua`, load this file at game start using the following line: + If not using `alyxlib/init.lua`, load this file at game start using the following line: - ```lua require "alyxlib.utils.common" - ``` ]] --- @@ -16,13 +15,13 @@ --- ---@class Util Util = {} -Util.version = "v3.1.0" +Util.version = "v3.1.1" --- ----Convert vr_tip_attachment from a game event [1,2] into a hand id [0,1] taking into account left handedness. +---Converts `vr_tip_attachment` from a game event (1,2) into a hand id (0,1) taking primary hand into account. --- ----@param vr_tip_attachment 1|2 ----@return 0|1 +---@param vr_tip_attachment 1|2 # The `vr_tip_attachment` value from a game event +---@return 0|1 # The hand id function Util.GetHandIdFromTip(vr_tip_attachment) local handId = vr_tip_attachment - 1 if not Convars:GetBool("hlvr_left_hand_primary") then @@ -32,12 +31,13 @@ function Util.GetHandIdFromTip(vr_tip_attachment) end --- ----Attempt to find a key in `tbl` pointing to `value`. +---Attempts to find a key in `tbl` pointing to `value`. --- ----@param tbl table # The table to search. ----@param value any # The value to search for. ----@return unknown|nil # The key in `tbl` or nil if no `value` was found. ----@deprecated # Functionally the same as `vlua.find`. +---@param tbl table # The table to search +---@param value any # The value to search for +---@return unknown|nil # The key in `tbl`, or `nil` if no `value` was found +---@deprecated # Functionally equivalent to [vlua.find](lua://vlua.find) +---@see vlua.find # functionally equivalent function Util.FindKeyFromValue(tbl, value) for key, val in pairs(tbl) do if val == value then @@ -48,12 +48,12 @@ function Util.FindKeyFromValue(tbl, value) end --- ----Attempt to find a key in `tbl` pointing to `value` by recursively searching nested tables. +---Attempts to find a key in `tbl` pointing to `value` by recursively searching nested tables. --- ----@param tbl table # The table to search. ----@param value any # The value to search for. ----@param seen? table[] # List of tables that have already been searched. ----@return unknown|nil # The key in `tbl` or nil if no `value` was found. +---@param tbl table # The table to search +---@param value any # The value to search for +---@param seen? table[] # List of tables that have already been searched +---@return unknown|nil # The key in `tbl` or nil if no `value` was found local function _FindKeyFromValueDeep(tbl, value, seen) seen = seen or {} for key, val in pairs(tbl) do @@ -69,18 +69,22 @@ local function _FindKeyFromValueDeep(tbl, value, seen) end --- ----Attempt to find a key in `tbl` pointing to `value` by recursively searching nested tables. +---Attempts to find a key in `tbl` pointing to `value` by recursively searching nested tables. --- ----@param tbl table # The table to search. ----@param value any # The value to search for. ----@return unknown|nil # The key in `tbl` or nil if no `value` was found. +---@param tbl table # The table to search +---@param value any # The value to search for +---@return unknown|nil # The key in `tbl`, or `nil` if no `value` was found. function Util.FindKeyFromValueDeep(tbl, value) return _FindKeyFromValueDeep(tbl, value) end +--- ---Returns the size of any table. ----@param tbl table ----@return integer +--- +---@param tbl table # The table to get the size of +---@return integer # The size of the table +---@deprecated # Functionally equivalent to [TableSize](lua://TableSize) +---@see TableSize # functionally equivalent function Util.TableSize(tbl) local count = 0 for _, _ in pairs(tbl) do @@ -90,30 +94,34 @@ function Util.TableSize(tbl) end --- ----Delay some code. +---Delays some code. --- ----@param func function ----@param delay? number +---@param func function # The function to delay +---@param delay? number # The delay in seconds (default: 0) +---@see CBaseEntity.Delay function Util.Delay(func, delay) GetListenServerHost():SetContextThink(DoUniqueString("delay"), func, delay or 0) end --- ----Get a new `QAngle` from a `Vector`. +---Gets a new `QAngle` from a `Vector`. +--- ---This simply transfers the raw values from one to the other. --- ----@param vec Vector ----@return QAngle +---@param vec Vector # The vector +---@return QAngle # The new QAngle function Util.QAngleFromVector(vec) return QAngle(vec.x, vec.y, vec.z) end ----Create a constraint between two entity handles. ----@param entity1 EntityHandle # First entity to attach. ----@param entity2 EntityHandle|nil # Second entity to attach. Set nil to attach to world. ----@param class? string # Class of constraint, default is `phys_constraint`. ----@param properties? table # Key/value property table. ----@return EntityHandle +--- +---Creates a constraint between two entity handles. +--- +---@param entity1 EntityHandle # First entity to attach +---@param entity2 EntityHandle|nil # Second entity to attach, or `nil` to attach to world +---@param class? string # Class of constraint (default: `phys_constraint`) +---@param properties? table # Key/value property table for the constraint +---@return EntityHandle # The created constraint function Util.CreateConstraint(entity1, entity2, class, properties) -- Cache original names local name1 = entity1:GetName() @@ -156,13 +164,15 @@ end ---|"none" # "None" ---|"custom" # "Custom" ----Create a damaging explosion effect at a position. ----@param origin Vector ----@param explosionType? ExplosionType ----@param magnitude? number ----@param radiusOverride? number ----@param ignoredEntity? EntityHandle|string # If the entity passed does not have a unique name, all entities with that name will be ignored. ----@param ignoredClass? string +--- +---Creates a damaging explosion effect at a position. +--- +---@param origin Vector # Worldspace position +---@param explosionType? ExplosionType # Explosion type (default: "") +---@param magnitude? number # Explosion magnitude (default: 100) +---@param radiusOverride? number # Radius override (default: 0) +---@param ignoredEntity? EntityHandle|string # Targetname to ignore or entity handle with a name +---@param ignoredClass? string # Classname to ignore (default: "") function Util.CreateExplosion(origin, explosionType, magnitude, radiusOverride, ignoredEntity, ignoredClass) local name @@ -185,10 +195,12 @@ function Util.CreateExplosion(origin, explosionType, magnitude, radiusOverride, expl:EntFire("Kill", nil, 0.1) end ----Choose and return a random argument. +--- +---Chooses a random value from the provided arguments. +--- ---@generic T ----@param ... T ----@return T +---@param ... T # Values +---@return T # Chosen value function Util.Choose(...) local args = {...} local numArgs = #args @@ -201,9 +213,13 @@ function Util.Choose(...) end end +--- ---Turns a string of up to three numbers into a vector. ----@param str string # Should have a format of "x y z" ----@return Vector +--- +---Should have a format of "x y z" +--- +---@param str string # String to parse +---@return Vector # Parsed vector function Util.VectorFromString(str) if type(str) ~= "string" then return Vector() diff --git a/scripts/vscripts/alyxlib/utils/enums.lua b/scripts/vscripts/alyxlib/utils/enums.lua index 0488fa4..c676929 100644 --- a/scripts/vscripts/alyxlib/utils/enums.lua +++ b/scripts/vscripts/alyxlib/utils/enums.lua @@ -1,8 +1,12 @@ --[[ - v1.0.1 + v1.0.2 https://github.com/FrostSource/alyxlib Missing enumerations. + + If not using `alyxlib/init.lua`, load this file at game start using the following line: + + require "alyxlib.utils.enums" ]] local version = "v1.0.1" diff --git a/scripts/vscripts/alyxlib/utils/extension_duplicate_fix.lua b/scripts/vscripts/alyxlib/utils/extension_duplicate_fix.lua index c368691..1a73585 100644 --- a/scripts/vscripts/alyxlib/utils/extension_duplicate_fix.lua +++ b/scripts/vscripts/alyxlib/utils/extension_duplicate_fix.lua @@ -1,5 +1,5 @@ --[[ - v1.0.0 + v1.0.1 https://github.com/FrostSource/alyxlib This script searches for, and deletes, any extension fixed-up entities that appear to be duplicates. @@ -9,7 +9,7 @@ Duplicates are detected by checking for increments in the "ex*d_" fix-up name pattern the extensions give. - **This file is not loaded by default, load it with the following line: + **This file is not loaded by default, load it with the following line:** require("alyxlib.utils.extension_duplicate_fix") ]] diff --git a/scripts/vscripts/game/gameinit.lua b/scripts/vscripts/game/gameinit.lua index ab12688..c23b0ee 100644 --- a/scripts/vscripts/game/gameinit.lua +++ b/scripts/vscripts/game/gameinit.lua @@ -50,7 +50,7 @@ if IsServer() then addonTable = _G.SERVER_ADDONS else --- - ---All init scripts running on the server. + ---All init scripts running on the client. --- ---The key is the workshop ID of the addon, and the value is a boolean indicating if there is an init script for the addon. --- diff --git a/templates/gitignore.txt b/templates/gitignore.txt index e736d30..0f391a8 100644 --- a/templates/gitignore.txt +++ b/templates/gitignore.txt @@ -11,4 +11,8 @@ scripts/vscripts/game/gameinit.lua .vscode/alyxlib.code-snippets .vscode/vlua_snippets.code-snippets panorama/scripts/custom_game/panorama_lua.js -panorama/scripts/custom_game/panoramadoc.js \ No newline at end of file +panorama/scripts/custom_game/panoramadoc.js + +panorama/layout/custom_game/alyxlib_debug_menu.xml +panorama/scripts/custom_game/alyxlib_debug_menu.js +panorama/styles/custom_game/alyxlib_debug_menu.css \ No newline at end of file diff --git a/version.json b/version.json index a06dd3f..2bc6d18 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "2.0.0" + "version": "3.0.0" } \ No newline at end of file