├── .github ├── FUNDING.yml ├── actions │ └── version.cjs └── workflows │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── interactionDUI ├── core.client.lua ├── dui_source │ ├── .prettierrc.json │ ├── eslint.config.mjs │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── settings.json │ ├── shims-vue.d.ts │ ├── src │ │ ├── App.vue │ │ ├── components │ │ │ ├── AudioPlayer.vue │ │ │ ├── DevTools.vue │ │ │ ├── ImageRenderer.vue │ │ │ ├── MenuOption.vue │ │ │ ├── MenuProgressbar.vue │ │ │ ├── ProgressBar.vue │ │ │ └── VideoRenderer.vue │ │ ├── main.js │ │ ├── reset.css │ │ ├── style.scss │ │ ├── themes.scss │ │ ├── types │ │ │ ├── mockData.ts │ │ │ └── types.d.ts │ │ ├── util │ │ │ └── index.ts │ │ └── views │ │ │ ├── ActionPromptIndicator.vue │ │ │ └── MenuContentRenderer.vue │ ├── tsconfig.json │ ├── vite.config.dev.js │ └── vite.config.prod.js └── fxmanifest.lua ├── interactionMenu ├── config.shared.lua ├── fxmanifest.lua └── lua │ ├── bridge │ ├── main.lua │ └── qb.lua │ ├── client │ ├── 3dDuiMaker.lua │ ├── drawIndicator.lua │ ├── extends │ │ ├── nested.lua │ │ └── pagination.lua │ ├── garbageCollector.lua │ ├── icons │ │ ├── box.png │ │ ├── glowingball.png │ │ ├── indicator.png │ │ ├── stove.png │ │ ├── stove2.png │ │ ├── vending.png │ │ └── wrench.png │ ├── interact.lua │ ├── menuContainer.lua │ ├── userInputManager.lua │ └── util.lua │ ├── examples │ ├── chairs.lua │ ├── empty.lua │ ├── entityZone.lua │ ├── garage.lua │ ├── globals.lua │ ├── holdIndicator.lua │ ├── instance.lua │ ├── nestedMenu.lua │ ├── onBones.lua │ ├── onEntities.lua │ ├── onModel.lua │ ├── onPosition.lua │ ├── onZone.lua │ ├── paginatedMenu.lua │ ├── particle.lua │ └── showroom.lua │ ├── providers │ ├── qb-target.lua │ └── qb-target_test.lua │ └── server │ └── server.lua └── interactionRenderer ├── README.md ├── fxmanifest.lua └── stream └── interaction_renderer.gfx /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | ko_fi: swkeep 4 | -------------------------------------------------------------------------------- /.github/actions/version.cjs: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const version = process.env.RELEASE_VERSION; 4 | const newVersion = version.replace('v', ''); 5 | 6 | try { 7 | let manifestFile = fs.readFileSync('./interactionMenu/fxmanifest.lua', 'utf8'); 8 | const newFileContent = manifestFile.replace(/\bversion\s+(.*)$/gm, `version '${newVersion}'`); 9 | fs.writeFileSync('fxmanifest.lua', newFileContent); 10 | } catch (err) { 11 | console.error(err); 12 | } -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | create-release: 10 | name: Build Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Setup node 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: 16.x 17 | 18 | - name: Checkout source code 19 | uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 0 22 | ref: ${{ github.event.repository.default_branch }} 23 | 24 | - name: Set env 25 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 26 | 27 | - name: Install dependencies for InteractionDUI 28 | run: | 29 | cd interactionDUI/dui_source 30 | npm install 31 | 32 | - name: Build Vue app for InteractionDUI 33 | run: | 34 | cd interactionDUI/dui_source 35 | npm run build 36 | 37 | - name: Bundle files 38 | run: | 39 | mkdir -p ./temp/interactionMenu 40 | mkdir -p ./temp/interactionDUI 41 | mkdir -p ./temp/interactionRenderer 42 | cp -r ./interactionDUI/dui ./temp/interactionDUI/ 43 | cp ./interactionDUI/fxmanifest.lua ./temp/interactionDUI 44 | cp ./interactionDUI/core.client.lua ./temp/interactionDUI 45 | cp -r ./interactionMenu ./temp 46 | cp -r ./interactionRenderer ./temp 47 | 48 | - name: Zip files 49 | run: | 50 | cd ./temp && zip -r ../interactionMenu.zip ./ 51 | 52 | - name: Create Release 53 | uses: "marvinpinto/action-automatic-releases@v1.2.1" 54 | id: auto_release 55 | with: 56 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 57 | title: ${{ env.RELEASE_VERSION }} 58 | prerelease: false 59 | files: interactionMenu.zip 60 | 61 | env: 62 | CI: false 63 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | interactionDUI/dui 2 | interactionDUI/dui_source/node_modules 3 | interactionDUI/watch.cjs 4 | interactionMenu/.vscode 5 | interactionMenu/watch.cjs 6 | interactionMenu/.editorconfig -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Interaction Menu 2 | 3 |
4 | 5 | ![](https://img.shields.io/github/v/release/swkeep/interaction-menu?logo=github) 6 | ![](https://img.shields.io/github/downloads/swkeep/interaction-menu/total?logo=github) 7 | ![](https://img.shields.io/github/downloads/swkeep/interaction-menu/latest/total?logo=github) 8 | 9 |
10 | 11 | A standalone DUI-based interaction menu for FiveM, designed to enhance player interactions with the environment on your server. 12 | 13 | This menu isn't designed to replace target scripts (though you could, but it would take quite a bit of effort on your end). Instead, it's meant to work alongside them, adding an extra layer of interactions to players. That said, keep in mind that the script uses sprites and DUI, which can be more resource-intensive compared to NUI-based scripts (nothing to worry about if you have some experience with scripting). 14 | 15 | ## Preview 16 | 17 |
18 | 19 | 20 | 23 | 26 | 29 | 30 |
21 | 22 | 24 | 25 | 27 | 28 |
31 |
32 | 33 |
34 | 35 | 36 | 39 | 42 | 43 |
37 | 38 | 40 | 41 |
44 |
45 | 46 | **[Watch on YouTube](https://www.youtube.com/watch?v=7ylxnj4HC5A)** 47 | 48 | ## Download 49 | 50 | **[Get the latest release](https://github.com/swkeep/interaction-menu/releases/latest)** 51 | 52 | ## Documentation 53 | 54 | **[Click Here](https://swkeep.com)** 55 | 56 | ## Developer Tools and Examples 57 | 58 | The script includes numerous examples in the `/lua/examples` directory to help you get started. 59 | 60 | To explore these examples live within your server: 61 | 1. Set `Config.devMode` and `Config.debugPoly` to `true` in `config.shared.lua`. 62 | 2. This enables developer mode, allowing you to see and interact with the examples in real time. 63 | 64 | **Important:** Remember to disable developer mode when you're done testing for optimal performance. 65 | 66 |
67 | Developer Mode Example 68 |
69 | 70 | ## My Store 71 | 72 | **[Visit my store](https://swkeep.tebex.io/)** 73 | 74 | 75 | ## Contributing 76 | 77 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 78 | 79 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". 80 | 81 | Don't forget to give the project a star! Thanks again! 82 | 83 | 84 | ## License 85 | 86 | See `LICENSE` for more information. 87 | 88 | 89 | ## Contact 90 | 91 | Swkeep - [@Discord](https://discord.gg/ccMArCwrPV) 92 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "tabWidth": 4, 5 | "printWidth": 120, 6 | "trailingComma": "all" 7 | } 8 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import prettier from 'eslint-plugin-prettier'; 2 | import js from '@eslint/js'; 3 | import eslintPluginVue from 'eslint-plugin-vue'; 4 | import ts from 'typescript-eslint'; 5 | 6 | export default [ 7 | js.configs.recommended, 8 | ...ts.configs.recommended, 9 | ...eslintPluginVue.configs['flat/recommended'], 10 | { 11 | files: ['*.vue', '**/*.vue'], 12 | languageOptions: { 13 | parserOptions: { 14 | parser: '@typescript-eslint/parser', 15 | }, 16 | }, 17 | 18 | plugins: { 19 | prettier, 20 | }, 21 | 22 | ignores: ['vite.config.dev.js', 'vite.config.prod.js'], 23 | }, 24 | ]; 25 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | swkeep's interaction menu 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keep_interact_dui", 3 | "private": true, 4 | "version": "1.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --host --config vite.config.prod.js", 8 | "build": "vite build --emptyOutDir --config vite.config.prod.js", 9 | "watch": "vite build --watch --emptyOutDir --config vite.config.dev.js", 10 | "preview": "vite preview", 11 | "lint": "eslint --quiet", 12 | "fix": "eslint --fix", 13 | "format": "prettier . --write" 14 | }, 15 | "dependencies": { 16 | "dompurify": "^3.1.6", 17 | "vue": "^3.3.12" 18 | }, 19 | "devDependencies": { 20 | "@types/dompurify": "^3.0.5", 21 | "@types/node": "^20.15.0", 22 | "@vitejs/plugin-vue": "^4.5.2", 23 | "eslint": "^9.9.0", 24 | "eslint-config-prettier": "^9.1.0", 25 | "eslint-plugin-prettier": "^5.2.1", 26 | "eslint-plugin-vue": "^9.27.0", 27 | "prettier": "^3.3.3", 28 | "sass": "^1.69.5", 29 | "typescript-eslint": "^8.1.0", 30 | "vite": "^5.0.10" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | // shims-vue.d.ts 2 | declare module '*.vue' { 3 | import { DefineComponent } from 'vue'; 4 | 5 | const component: DefineComponent; 6 | export default component; 7 | } 8 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/src/App.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 58 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/src/components/AudioPlayer.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | 88 | 89 | 119 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/src/components/DevTools.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 99 | 100 | 204 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/src/components/ImageRenderer.vue: -------------------------------------------------------------------------------- 1 | 94 | 116 | 180 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/src/components/MenuOption.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 44 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/src/components/MenuProgressbar.vue: -------------------------------------------------------------------------------- 1 | 12 | 19 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/src/components/ProgressBar.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 40 | 41 | 93 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/src/components/VideoRenderer.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 79 | 80 | 119 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue'; 3 | 4 | import './reset.css'; 5 | import './style.scss'; 6 | 7 | // themes 8 | import './themes.scss'; 9 | 10 | createApp(App).mount('#app'); 11 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/src/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, 7 | body, 8 | div, 9 | span, 10 | applet, 11 | object, 12 | iframe, 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6, 19 | p, 20 | blockquote, 21 | pre, 22 | a, 23 | abbr, 24 | acronym, 25 | address, 26 | big, 27 | cite, 28 | code, 29 | del, 30 | dfn, 31 | em, 32 | img, 33 | ins, 34 | kbd, 35 | q, 36 | s, 37 | samp, 38 | small, 39 | strike, 40 | strong, 41 | sub, 42 | sup, 43 | tt, 44 | var, 45 | b, 46 | u, 47 | i, 48 | center, 49 | dl, 50 | dt, 51 | dd, 52 | ol, 53 | ul, 54 | li, 55 | fieldset, 56 | form, 57 | label, 58 | legend, 59 | table, 60 | caption, 61 | tbody, 62 | tfoot, 63 | thead, 64 | tr, 65 | th, 66 | td, 67 | article, 68 | aside, 69 | canvas, 70 | details, 71 | embed, 72 | figure, 73 | figcaption, 74 | footer, 75 | header, 76 | hgroup, 77 | menu, 78 | nav, 79 | output, 80 | ruby, 81 | section, 82 | summary, 83 | time, 84 | mark, 85 | audio, 86 | video { 87 | margin: 0; 88 | padding: 0; 89 | border: 0; 90 | font-size: 100%; 91 | font: inherit; 92 | vertical-align: baseline; 93 | } 94 | 95 | /* HTML5 display-role reset for older browsers */ 96 | article, 97 | aside, 98 | details, 99 | figcaption, 100 | figure, 101 | footer, 102 | header, 103 | hgroup, 104 | menu, 105 | nav, 106 | section { 107 | display: block; 108 | } 109 | 110 | body { 111 | line-height: 1; 112 | } 113 | 114 | ol, 115 | ul { 116 | list-style: none; 117 | } 118 | 119 | blockquote, 120 | q { 121 | quotes: none; 122 | } 123 | 124 | blockquote:before, 125 | blockquote:after, 126 | q:before, 127 | q:after { 128 | content: ''; 129 | content: none; 130 | } 131 | 132 | table { 133 | border-collapse: collapse; 134 | border-spacing: 0; 135 | } 136 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/src/style.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Jura:400,700'); 2 | 3 | $max-width: 20em; 4 | 5 | :root { 6 | --primary-color: rgba(226, 145, 38, 1); 7 | --primary-color-glow: rgba(226, 145, 38, 0.5); 8 | --primary-color-background: rgba(143, 92, 24, 0.3); 9 | --primary-color-border: rgba(226, 145, 38, 0.7); 10 | --text-color: white; 11 | --max-width: 20em; 12 | } 13 | 14 | body { 15 | width: 100vw; 16 | height: 100vh; 17 | } 18 | 19 | #app { 20 | width: 100%; 21 | height: 100%; 22 | font-weight: bold; 23 | font-family: 'Jura', Arial, sans-serif; 24 | } 25 | 26 | .interact-container { 27 | width: 100%; 28 | height: 100%; 29 | display: flex; 30 | justify-content: center; 31 | align-content: center; 32 | flex-wrap: wrap; 33 | overflow: hidden; 34 | gap: 1rem; 35 | 36 | // yeah that's dark mode xd 37 | &[data-dark='true'] { 38 | .menus-container { 39 | background-color: rgba(0, 0, 0, 0.6); 40 | } 41 | 42 | .indicator { 43 | background-color: rgba(0, 0, 0, 0.6); 44 | } 45 | } 46 | } 47 | 48 | .menus-container { 49 | width: fit-content; 50 | max-width: $max-width; 51 | display: flex; 52 | justify-content: center; 53 | flex-direction: column; 54 | font-size: 2rem; 55 | background-color: rgba(0, 0, 0, 0.1); 56 | border-radius: 1em; 57 | transition: background-color 2.5s ease; 58 | 59 | &--glow { 60 | box-shadow: 0 0px 20px 4px var(--primary-color-glow); 61 | } 62 | } 63 | 64 | .menu { 65 | // radio style from https://codepen.io/havardob/pen/dyYXBBr by Håvard Brynjulfsen 66 | &--hidden { 67 | display: none; 68 | } 69 | 70 | &__option { 71 | display: flex; 72 | position: relative; 73 | overflow: hidden; 74 | margin: 0.2em; 75 | color: wheat; 76 | 77 | &--no-margin { 78 | margin: 0 !important; 79 | } 80 | 81 | &__radio { 82 | position: absolute; 83 | appearance: none; 84 | 85 | &:checked + .label { 86 | background-color: var(--primary-color-background); 87 | 88 | &:before { 89 | box-shadow: inset 0 0 0 0.5em var(--primary-color); 90 | } 91 | } 92 | } 93 | 94 | .label { 95 | width: 100%; 96 | display: flex; 97 | align-items: center; 98 | padding: 0.3em 0.7em 0.3em 0.3em; 99 | border-radius: 99em; 100 | transition: 0.25s ease; 101 | 102 | &__text { 103 | margin-left: 0.375em; 104 | } 105 | 106 | &--sub-menu::after { 107 | content: '🠊'; 108 | position: absolute; 109 | right: 24px; 110 | animation: my-animation 1s infinite ease-in-out; 111 | } 112 | 113 | @keyframes my-animation { 114 | 0% { 115 | transform: translateX(0); 116 | } 117 | 50% { 118 | transform: translateX(-6px); 119 | } 120 | 100% { 121 | transform: translateX(0); 122 | } 123 | } 124 | 125 | &__icon { 126 | min-width: 1.2em; 127 | text-align: center; 128 | } 129 | 130 | &--radio:before { 131 | display: flex; 132 | flex-shrink: 0; 133 | content: ''; 134 | background-color: transparent; 135 | width: 1.5em; 136 | height: 1.5em; 137 | border-radius: 50%; 138 | margin-right: 0.375em; 139 | transition: 0.25s ease; 140 | box-shadow: inset 0 0 0 0.1em var(--primary-color); 141 | } 142 | 143 | &--center { 144 | width: 100%; 145 | display: flex; 146 | justify-content: center; 147 | align-items: center; 148 | padding: 0.3em 0.7em 0.3em 0.3em; 149 | flex-direction: column; 150 | gap: 1rem; 151 | } 152 | } 153 | } 154 | } 155 | 156 | .video-container { 157 | // need it for list animation 158 | width: $max-width; 159 | display: flex; 160 | justify-content: space-evenly; 161 | align-items: center; 162 | user-select: none; 163 | pointer-events: none; 164 | gap: 0.5rem; 165 | padding: 0.2rem; 166 | 167 | .video-container__video, 168 | img { 169 | width: 100%; 170 | border-radius: 1rem; 171 | } 172 | } 173 | 174 | .fade-enter-from, 175 | .fade-leave-to { 176 | opacity: 0; 177 | transform: translateX(-30px); 178 | } 179 | 180 | .fade-enter-active, 181 | .fade-leave-active { 182 | transition: 183 | opacity 0.4s cubic-bezier(0.23, 1, 0.32, 1), 184 | transform 0.25s ease-in; 185 | } 186 | 187 | .fade-enter-to { 188 | opacity: 1; 189 | transform: translateX(0); 190 | } 191 | 192 | .slide-enter-from { 193 | opacity: 0; 194 | transform: translateY(160px); 195 | } 196 | 197 | .slide-enter-active { 198 | transition: 199 | transform 1.4s ease, 200 | opacity 1.8s ease; 201 | } 202 | 203 | .slide-move { 204 | transition: transform 0.4s; 205 | } 206 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/src/themes.scss: -------------------------------------------------------------------------------- 1 | // [data-theme="default"] {} 2 | [data-invoking-resource='keep-test'] { 3 | .menus-container { 4 | border-radius: 0px; 5 | background-color: rgba(255, 121, 121, 0.6); 6 | } 7 | } 8 | 9 | [data-theme='box'] { 10 | .menus-container { 11 | border-radius: 0px; 12 | } 13 | 14 | .menu__option__radio-option { 15 | &:checked + .label { 16 | &:before { 17 | box-shadow: inset 0 0 0 0.4em var(--primary-color); 18 | } 19 | } 20 | } 21 | 22 | .menu__option { 23 | .label { 24 | border-radius: 0px; 25 | 26 | &--radio:before { 27 | border-radius: 0px; 28 | } 29 | } 30 | } 31 | 32 | .indicator { 33 | border-radius: 0px; 34 | } 35 | } 36 | 37 | [data-theme='red'] { 38 | .menus-container { 39 | border-radius: 0px; 40 | } 41 | 42 | .menu { 43 | &__option { 44 | color: #f5f5f5; 45 | 46 | &__radio { 47 | &:checked + .label { 48 | background-color: #1e1e1e; 49 | 50 | &:before { 51 | box-shadow: inset 0 0 0 0.5em #ff5f5f; 52 | } 53 | } 54 | } 55 | 56 | .label { 57 | background-color: #2d2d2d; 58 | 59 | &--radio:before { 60 | box-shadow: inset 0 0 0 0.1em #ff5f5f; 61 | } 62 | } 63 | } 64 | } 65 | 66 | .indicator { 67 | border-radius: 0px; 68 | } 69 | } 70 | 71 | [data-theme='orange'] { 72 | .menus-container { 73 | border-radius: 0px; 74 | } 75 | 76 | .menu { 77 | &__option { 78 | color: #eaeaea; 79 | 80 | &__radio { 81 | &:checked + .label { 82 | background-color: #2a2a2a; 83 | 84 | &:before { 85 | box-shadow: inset 0 0 0 0.5em #ff4500; 86 | } 87 | } 88 | } 89 | 90 | .label { 91 | &--radio:before { 92 | box-shadow: inset 0 0 0 0.1em #ff4500; 93 | } 94 | } 95 | } 96 | } 97 | 98 | .indicator { 99 | border-radius: 0px; 100 | } 101 | } 102 | 103 | [data-theme='cyan'] { 104 | &[data-dark='true'] { 105 | .menus-container { 106 | background-color: transparent; 107 | } 108 | 109 | .indicator { 110 | background-color: transparent; 111 | } 112 | } 113 | 114 | .menus-container { 115 | background-color: transparent; 116 | border-radius: 0px; 117 | } 118 | 119 | .menu { 120 | &__option { 121 | color: #d1d1d1; 122 | overflow: visible; 123 | &__radio { 124 | position: absolute; 125 | appearance: none; 126 | 127 | &:checked + .label { 128 | background-color: rgba(30, 30, 30, 0.8); 129 | 130 | &:before { 131 | content: ''; 132 | box-shadow: 133 | inset 0 0.5em 0.5em rgba(0, 255, 191, 0.6), 134 | inset 0 0 0 0.1em rgba(0, 255, 191, 0.6); 135 | } 136 | } 137 | } 138 | .label { 139 | background-color: rgba(30, 30, 30, 0.4); 140 | 141 | &--radio:before { 142 | box-shadow: 143 | inset 0 0 0 0.1em rgba(0, 249, 187, 0.7), 144 | inset 0 0 0 0.1em rgba(0, 255, 191, 0.9); 145 | } 146 | } 147 | } 148 | } 149 | 150 | .indicator { 151 | border: 6px solid rgb(0, 255, 191); 152 | background-color: rgba(0, 255, 191, 0.2); 153 | 154 | &__fill { 155 | background-color: rgba(0, 255, 191, 0.4); 156 | } 157 | 158 | &--success { 159 | border-color: rgb(81, 255, 0) !important; 160 | } 161 | 162 | &--fail { 163 | border-color: rgb(199, 37, 61) !important; 164 | } 165 | 166 | &--glow { 167 | box-shadow: 168 | 0 0px 10px rgb(0, 255, 191), 169 | 0 0 30px rgb(0, 255, 191); 170 | } 171 | } 172 | } 173 | 174 | // do not look at this :( 175 | [data-theme='nopixel'] { 176 | &[data-dark='true'] { 177 | .menus-container { 178 | background-color: transparent; 179 | } 180 | 181 | .indicator { 182 | background-color: transparent; 183 | } 184 | } 185 | 186 | .menus-container { 187 | background-color: transparent; 188 | border-radius: 0px; 189 | } 190 | 191 | .menu { 192 | &__option { 193 | color: #ffffff; 194 | overflow: visible; 195 | 196 | &__radio { 197 | position: absolute; 198 | appearance: none; 199 | 200 | &::before { 201 | content: ''; 202 | position: absolute; 203 | top: 0.5em; 204 | left: -0.26em; 205 | width: 4.2em; 206 | height: 4.2em; 207 | border: 0.3em solid rgba(255, 255, 255, 0.5); 208 | border-radius: 50%; 209 | border-top-color: transparent; 210 | border-bottom-color: transparent; 211 | box-shadow: 0 0 0 0em rgba(255, 255, 255, 0.5); 212 | } 213 | 214 | &:checked + .label { 215 | background-color: transparent; 216 | 217 | &__radio { 218 | &::before { 219 | border: 0.3em solid rgba(255, 255, 255, 0.5); 220 | } 221 | } 222 | 223 | &:before { 224 | content: ''; 225 | box-shadow: inset 0 0 0.4em 0.5em rgba(0, 255, 191, 0.5); 226 | } 227 | 228 | &:after { 229 | position: absolute; 230 | width: 0.5em; 231 | height: 0.5em; 232 | top: 1.05em; 233 | left: 0.81em; 234 | content: ''; 235 | background-color: rgba(0, 255, 191, 0.5); 236 | border-radius: 50%; 237 | box-shadow: inset 0 0 0.4em 0.5em rgba(0, 255, 191, 0.5); 238 | } 239 | } 240 | 241 | &:checked + .label div { 242 | background-color: rgba(2, 147, 111, 0.9); 243 | } 244 | } 245 | 246 | .label { 247 | border-radius: 0px; 248 | background-color: transparent; 249 | 250 | &--radio:before { 251 | box-shadow: inset 0 0 0.4em 0.5em rgba(255, 255, 255, 0.5); 252 | } 253 | 254 | .label__container { 255 | position: relative; 256 | width: 80%; 257 | background-color: rgba(255, 255, 255, 0.3); 258 | padding: 0.5rem; 259 | margin-left: 1rem; 260 | } 261 | } 262 | 263 | .label .label__container { 264 | padding: 1rem; 265 | } 266 | 267 | .label .label__container { 268 | &::after { 269 | position: absolute; 270 | left: 0; 271 | top: 0; 272 | width: 20px; 273 | height: 20px; 274 | content: ''; 275 | border-left: 3px solid rgb(255, 255, 255); 276 | border-top: 3px solid rgb(255, 255, 255); 277 | } 278 | 279 | &::before { 280 | position: absolute; 281 | right: 0; 282 | bottom: 0; 283 | width: 20px; 284 | height: 20px; 285 | content: ''; 286 | border-right: 3px solid rgb(255, 255, 255); 287 | border-bottom: 3px solid rgb(255, 255, 255); 288 | } 289 | } 290 | } 291 | } 292 | 293 | .indicator { 294 | border: none; 295 | background: radial-gradient(circle, rgba(0, 82, 62, 1) 0%, rgba(3, 134, 103, 1) 55%); 296 | border-radius: 3px; 297 | 298 | &__inner { 299 | display: flex; 300 | justify-content: center; 301 | align-items: center; 302 | width: 80%; 303 | height: 80%; 304 | } 305 | 306 | &__fill { 307 | background-color: rgba(0, 255, 191, 0.4); 308 | height: 50%; 309 | } 310 | 311 | &--success { 312 | border-color: rgb(81, 255, 0) !important; 313 | } 314 | 315 | &--fail { 316 | border-color: rgb(199, 37, 61) !important; 317 | } 318 | 319 | &--glow { 320 | box-shadow: none; 321 | } 322 | 323 | &::after { 324 | position: absolute; 325 | left: 0; 326 | top: 0; 327 | width: 20px; 328 | height: 20px; 329 | content: ''; 330 | border-left: 3px solid rgb(255, 255, 255); 331 | border-top: 3px solid rgb(255, 255, 255); 332 | } 333 | 334 | &::before { 335 | position: absolute; 336 | right: 0; 337 | bottom: 0; 338 | width: 20px; 339 | height: 20px; 340 | content: ''; 341 | border-right: 3px solid rgb(255, 255, 255); 342 | border-bottom: 3px solid rgb(255, 255, 255); 343 | } 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/src/types/mockData.ts: -------------------------------------------------------------------------------- 1 | export const menuMockData = [ 2 | { 3 | selected: 1, 4 | theme: 'default', 5 | indicator: { 6 | prompt: 'Enter', 7 | glow: true, 8 | active: true, 9 | }, 10 | menus: [ 11 | { 12 | id: 'test', 13 | flags: { 14 | hide: false, 15 | }, 16 | options: [ 17 | { 18 | vid: 1, 19 | label: 'Sand', 20 | flags: { 21 | action: true, 22 | hide: false, 23 | }, 24 | }, 25 | { 26 | vid: 2, 27 | label: 'State: Locked', 28 | flags: { 29 | update: true, 30 | hide: false, 31 | }, 32 | }, 33 | { 34 | vid: 3, 35 | label: 'Sub Menu', 36 | flags: { 37 | update: true, 38 | hide: false, 39 | subMenu: true 40 | }, 41 | }, 42 | ], 43 | }, 44 | ], 45 | }, 46 | { 47 | selected: 1, 48 | theme: 'default', 49 | menus: [ 50 | { 51 | id: 'test', 52 | flags: { 53 | hide: false, 54 | }, 55 | options: [ 56 | { 57 | vid: 1, 58 | label: 'Center No Action', 59 | flags: { 60 | hide: false, 61 | }, 62 | }, 63 | { 64 | vid: 2, 65 | label: 'Sand', 66 | flags: { 67 | action: true, 68 | hide: false, 69 | }, 70 | }, 71 | { 72 | vid: 3, 73 | label: 'State: Locked', 74 | flags: { 75 | update: true, 76 | hide: false, 77 | }, 78 | }, 79 | { 80 | vid: 4, 81 | audio: { 82 | url: 'http://127.0.0.1:8080/Seven-Pounds-Energy-Complextro.mp3', 83 | volume: 1.0, 84 | progress: true, 85 | // percent: true, 86 | // loop: true, 87 | timecycle: true, 88 | }, 89 | flags: { 90 | update: true, 91 | hide: false, 92 | }, 93 | }, 94 | ], 95 | }, 96 | ], 97 | }, 98 | 99 | { 100 | selected: 1, 101 | theme: 'default', 102 | menus: [ 103 | { 104 | id: 'test2', 105 | flags: { 106 | hide: false, 107 | }, 108 | options: [ 109 | { 110 | vid: 1, 111 | picture: { 112 | // transition: 'slide-up', 113 | height: '20em', 114 | url: ['http://127.0.0.1:8080/warframe1.jpg', 'http://127.0.0.1:8080/warframe2.jpg'], 115 | }, 116 | flags: { 117 | hide: false, 118 | }, 119 | }, 120 | { 121 | vid: 2, 122 | picture: { 123 | filters: { 124 | brightness: 100, 125 | }, 126 | url: 'http://127.0.0.1:8080/00235-990749447.png', 127 | }, 128 | flags: { 129 | hide: false, 130 | }, 131 | }, 132 | { 133 | vid: 3, 134 | label: 'Test Title', 135 | description: 'Test Subtitle', 136 | video: { 137 | url: 'http://127.0.0.1:8080/Nevermore.mp4', 138 | volume: 0.0, 139 | progress: true, 140 | // percent: true, 141 | // loop: true, 142 | timecycle: true, 143 | }, 144 | flags: { 145 | hide: false, 146 | }, 147 | }, 148 | { 149 | vid: 4, 150 | label: 'Progress', 151 | progress: { 152 | type: 'info', 153 | percent: true, 154 | value: 69, 155 | }, 156 | flags: { 157 | hide: false, 158 | }, 159 | }, 160 | ], 161 | }, 162 | ], 163 | }, 164 | 165 | { 166 | selected: 1, 167 | theme: 'default', 168 | indicator: { 169 | prompt: 'Sound', 170 | glow: true, 171 | active: true, 172 | }, 173 | menus: [ 174 | { 175 | id: 'test', 176 | flags: { 177 | hide: false, 178 | }, 179 | options: [ 180 | { 181 | vid: 1, 182 | label: 'Center No Action', 183 | flags: { 184 | hide: false, 185 | }, 186 | icon: 'fas fa-align-center', 187 | }, 188 | { 189 | vid: 2, 190 | label: 'Sand', 191 | flags: { 192 | action: true, 193 | hide: false, 194 | }, 195 | icon: 'fas fa-umbrella-beach', 196 | }, 197 | { 198 | vid: 3, 199 | label: 'State: Locked', 200 | flags: { 201 | update: true, 202 | hide: false, 203 | }, 204 | icon: 'fas fa-lock', 205 | }, 206 | ], 207 | }, 208 | ], 209 | }, 210 | ]; 211 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/src/types/types.d.ts: -------------------------------------------------------------------------------- 1 | export type FocusTrackerT = 'indicator' | 'menu'; 2 | export interface FocusTracker { 3 | indicator: boolean; 4 | menu: boolean; 5 | } 6 | 7 | export interface Indicator { 8 | glow?: boolean; 9 | underline?: boolean; 10 | prompt?: string; 11 | hold?: number; 12 | } 13 | 14 | export interface OptionsStyle { 15 | color: { 16 | background: string; 17 | label: string; 18 | labelSelected: string; 19 | backgroundSelected: string; 20 | }; 21 | text: { 22 | labelFontSize: string; 23 | }; 24 | } 25 | 26 | interface VideoData { 27 | url: string; 28 | currentTime?: number; 29 | autoplay?: boolean; 30 | loop?: boolean; 31 | progress?: boolean; 32 | percent?: boolean; 33 | timecycle?: boolean; 34 | volume?: number; 35 | opacity?: number; 36 | } 37 | 38 | export interface AudioData { 39 | url: string; 40 | currentTime?: number; 41 | autoplay?: boolean; 42 | loop?: boolean; 43 | volume?: number; 44 | progress?: boolean; 45 | percent?: boolean; 46 | timecycle?: boolean; 47 | } 48 | 49 | type BorderType = 'dash' | 'solid' | 'double' | 'none' | null | undefined; 50 | 51 | declare enum TransitionType { 52 | LEFT = 'slide-left', 53 | UP = 'slide-up', 54 | RIGHT = 'slide-right', 55 | DOWN = 'slide-down', 56 | } 57 | 58 | export interface Filters { 59 | brightness?: number; // percentage 0-100 60 | contrast?: number; // percentage 0-100 61 | saturation?: number; // percentage 0-100 62 | hue?: number; // degrees 63 | blur?: number; // pixels 64 | grayscale?: number; // percentage 0-100 65 | sepia?: number; // percentage 0-100 66 | invert?: number; // percentage 0-100 67 | } 68 | 69 | interface Picture { 70 | url: string; 71 | interval?: number; 72 | filters?: Filters; 73 | transition?: TransitionType; 74 | opacity?: number; 75 | width?: number; 76 | height?: number; 77 | border?: BorderType; 78 | } 79 | 80 | interface Progress { 81 | type: string; 82 | value?: number; 83 | percent?: boolean; 84 | } 85 | 86 | interface OptionFlags { 87 | action?: boolean; 88 | event?: boolean; 89 | update?: boolean; 90 | disable: boolean; 91 | dynamic?: boolean; 92 | hide: boolean; 93 | deleted?: boolean; 94 | canInteract: boolean; 95 | subMenu: boolean; 96 | } 97 | 98 | export interface Option { 99 | id: string | number; 100 | vid: string | number; 101 | label: string; 102 | description: string; 103 | icon: string; 104 | video?: VideoData; 105 | audio?: AudioData; 106 | picture?: Picture; 107 | style?: OptionsStyle; 108 | progress?: Progress; 109 | flags: OptionFlags; 110 | checked?: boolean; 111 | } 112 | 113 | export interface Menu { 114 | id: string | number; 115 | metadata: { [key: string]: string }; 116 | options: { [key: string]: Option }; 117 | selected: Array; 118 | flags: OptionFlags; 119 | } 120 | 121 | export interface InteractionMenu { 122 | id: string | number; 123 | indicator?: Indicator; 124 | loading?: boolean; 125 | menus: Menu[]; 126 | selected: Array; 127 | theme: string; 128 | glow: boolean; 129 | width: number | string; 130 | } 131 | 132 | export interface MenuOption { 133 | id: number; 134 | content: string; 135 | } 136 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/src/util/index.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onBeforeUnmount } from 'vue'; 2 | import { Option } from '../types/types'; 3 | 4 | interface EventData { 5 | action: string; 6 | data: any; 7 | } 8 | 9 | type EventHandler = (data: any) => void; 10 | 11 | const subscribe = (action: string, handler: EventHandler): void => { 12 | const eventListener = (event: MessageEvent): void => { 13 | const { action: eventAction, data } = event.data as EventData; 14 | 15 | if (handler && eventAction === action) handler(data); 16 | }; 17 | 18 | onMounted(() => window.addEventListener('message', eventListener)); 19 | onBeforeUnmount(() => window.removeEventListener('message', eventListener)); 20 | }; 21 | 22 | interface DebugEvent { 23 | action: string; 24 | data: any; 25 | } 26 | 27 | const debug = (events: DebugEvent[], timer = 1000): void => { 28 | if (process.env.NODE_ENV !== 'development') return; 29 | 30 | events.forEach((event, index) => { 31 | setTimeout( 32 | () => { 33 | const eventData: EventData = { 34 | action: event.action, 35 | data: event.data, 36 | }; 37 | 38 | const customEvent = new MessageEvent('message', { 39 | data: eventData, 40 | }); 41 | 42 | window.dispatchEvent(customEvent); 43 | }, 44 | timer * (index + 1), 45 | ); 46 | }); 47 | }; 48 | 49 | const dev_run = (handler: () => void): void => { 50 | if (process.env.NODE_ENV !== 'development') return; 51 | handler(); 52 | }; 53 | 54 | const itemStyle = (item: Option) => { 55 | const { checked, style } = item; 56 | const background = checked 57 | ? style?.color?.backgroundSelected || style?.color?.background 58 | : style?.color?.background; 59 | const labelColor = style?.color?.label; 60 | const labelFontSize = style?.text?.labelFontSize; 61 | 62 | return { 63 | backgroundColor: background, 64 | color: labelColor, 65 | fontSize: labelFontSize, 66 | }; 67 | }; 68 | 69 | const pad = (num: number) => String(Math.floor(num)).padStart(2, '0'); 70 | const formatTime = (seconds: number) => { 71 | return `${pad(seconds / 3600)}:${pad((seconds / 60) % 60)}:${pad(seconds % 60)}`; 72 | }; 73 | 74 | export { itemStyle, subscribe, debug, dev_run, formatTime }; 75 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/src/views/ActionPromptIndicator.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 94 | 95 | 149 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/src/views/MenuContentRenderer.vue: -------------------------------------------------------------------------------- 1 | 32 | 158 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "skipLibCheck": true, 4 | "target": "esnext", 5 | "useDefineForClassFields": true, 6 | "module": "esnext", 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "jsx": "preserve", 10 | "sourceMap": true, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "lib": ["esnext", "dom"] 14 | }, 15 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] 16 | } 17 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/vite.config.dev.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vite'; 3 | import vue from '@vitejs/plugin-vue'; 4 | 5 | function myPlugin() { 6 | return { 7 | name: 'watch', 8 | 9 | closeBundle(_src, _id) { 10 | const options = { 11 | tcp: false, 12 | challenge: false, 13 | }; 14 | let conn = new Rcon('localhost', 30120, 'swkeep_localhost', options); 15 | conn.connect(); 16 | 17 | conn.on('auth', function () { 18 | conn.send('ensure ' + 'interactionDUI'); 19 | console.warn('Restarting interactionDUI'); 20 | setTimeout(() => { 21 | conn.send('ensure ' + 'interactionMenu'); 22 | console.warn('Restarting interactionMenu'); 23 | }, 1000); 24 | }); 25 | }, 26 | }; 27 | } 28 | 29 | export default defineConfig({ 30 | plugins: [vue(), myPlugin()], 31 | base: './', 32 | build: { 33 | rollupOptions: { 34 | output: { 35 | manualChunks: false, 36 | inlineDynamicImports: true, 37 | entryFileNames: '[name].js', 38 | assetFileNames: '[name].[ext]', 39 | }, 40 | }, 41 | outDir: path.join(__dirname, '../dui'), 42 | minify: true, 43 | }, 44 | }); 45 | 46 | // #region Rcon 47 | 48 | import util from 'util'; 49 | import events from 'events'; 50 | import net from 'net'; 51 | import dgram from 'dgram'; 52 | import { Buffer } from 'buffer'; 53 | 54 | var PacketType = { 55 | COMMAND: 0x02, 56 | AUTH: 0x03, 57 | RESPONSE_VALUE: 0x00, 58 | RESPONSE_AUTH: 0x02, 59 | }; 60 | 61 | /** 62 | * options: 63 | * tcp - true for TCP, false for UDP (optional, default true) 64 | * challenge - if using UDP, whether to use the challenge protocol (optional, default true) 65 | * id - RCON id to use (optional) 66 | */ 67 | function Rcon(host, port, password, options) { 68 | if (!(this instanceof Rcon)) return new Rcon(host, port, password, options); 69 | options = options || {}; 70 | 71 | this.host = host; 72 | this.port = port; 73 | this.password = password; 74 | this.rconId = options.id || 0x0012d4a6; // This is arbitrary in most cases 75 | this.hasAuthed = false; 76 | this.outstandingData = null; 77 | this.tcp = options.tcp == null ? true : options.tcp; 78 | this.challenge = options.challenge == null ? true : options.challenge; 79 | 80 | events.EventEmitter.call(this); 81 | } 82 | 83 | util.inherits(Rcon, events.EventEmitter); 84 | 85 | Rcon.prototype.send = function (data, cmd, id) { 86 | var sendBuf; 87 | if (this.tcp) { 88 | cmd = cmd || PacketType.COMMAND; 89 | id = id || this.rconId; 90 | 91 | var length = Buffer.byteLength(data); 92 | sendBuf = Buffer.alloc(length + 14); 93 | sendBuf.writeInt32LE(length + 10, 0); 94 | sendBuf.writeInt32LE(id, 4); 95 | sendBuf.writeInt32LE(cmd, 8); 96 | sendBuf.write(data, 12); 97 | sendBuf.writeInt16LE(0, length + 12); 98 | } else { 99 | if (this.challenge && !this._challengeToken) { 100 | this.emit('error', new Error('Not authenticated')); 101 | return; 102 | } 103 | var str = 'rcon '; 104 | if (this._challengeToken) str += this._challengeToken + ' '; 105 | if (this.password) str += this.password + ' '; 106 | str += data + '\n'; 107 | sendBuf = Buffer.alloc(4 + Buffer.byteLength(str)); 108 | sendBuf.writeInt32LE(-1, 0); 109 | sendBuf.write(str, 4); 110 | } 111 | this._sendSocket(sendBuf); 112 | }; 113 | 114 | Rcon.prototype._sendSocket = function (buf) { 115 | if (this._tcpSocket) { 116 | this._tcpSocket.write(buf.toString('binary'), 'binary'); 117 | } else if (this._udpSocket) { 118 | this._udpSocket.send(buf, 0, buf.length, this.port, this.host); 119 | } 120 | }; 121 | 122 | Rcon.prototype.connect = function () { 123 | var self = this; 124 | 125 | if (this.tcp) { 126 | this._tcpSocket = net.createConnection(this.port, this.host); 127 | this._tcpSocket 128 | .on('data', function (data) { 129 | self._tcpSocketOnData(data); 130 | }) 131 | .on('connect', function () { 132 | self.socketOnConnect(); 133 | }) 134 | .on('error', function (err) { 135 | self.emit('error', err); 136 | }) 137 | .on('end', function () { 138 | self.socketOnEnd(); 139 | }); 140 | } else { 141 | this._udpSocket = dgram.createSocket('udp4'); 142 | this._udpSocket 143 | .on('message', function (data) { 144 | self._udpSocketOnData(data); 145 | }) 146 | .on('listening', function () { 147 | self.socketOnConnect(); 148 | }) 149 | .on('error', function (err) { 150 | self.emit('error', err); 151 | }) 152 | .on('close', function () { 153 | self.socketOnEnd(); 154 | }); 155 | this._udpSocket.bind(0); 156 | } 157 | }; 158 | 159 | Rcon.prototype.disconnect = function () { 160 | if (this._tcpSocket) this._tcpSocket.end(); 161 | if (this._udpSocket) this._udpSocket.close(); 162 | }; 163 | 164 | Rcon.prototype.setTimeout = function (timeout, callback) { 165 | if (!this._tcpSocket) return; 166 | 167 | var self = this; 168 | this._tcpSocket.setTimeout(timeout, function () { 169 | self._tcpSocket.end(); 170 | if (callback) callback(); 171 | }); 172 | }; 173 | 174 | Rcon.prototype._udpSocketOnData = function (data) { 175 | var a = data.readUInt32LE(0); 176 | if (a == 0xffffffff) { 177 | var str = data.toString('utf-8', 4); 178 | var tokens = str.split(' '); 179 | if (tokens.length == 3 && tokens[0] == 'challenge' && tokens[1] == 'rcon') { 180 | this._challengeToken = tokens[2].substr(0, tokens[2].length - 1).trim(); 181 | this.hasAuthed = true; 182 | this.emit('auth'); 183 | } else { 184 | this.emit('response', str.substr(1, str.length - 2)); 185 | } 186 | } else { 187 | this.emit('error', new Error('Received malformed packet')); 188 | } 189 | }; 190 | 191 | Rcon.prototype._tcpSocketOnData = function (data) { 192 | if (this.outstandingData != null) { 193 | data = Buffer.concat([this.outstandingData, data], this.outstandingData.length + data.length); 194 | this.outstandingData = null; 195 | } 196 | 197 | while (data.length >= 12) { 198 | var len = data.readInt32LE(0); // Size of entire packet, not including the 4 byte length field 199 | if (!len) return; // No valid packet header, discard entire buffer 200 | 201 | var packetLen = len + 4; 202 | if (data.length < packetLen) break; // Wait for full packet, TCP may have segmented it 203 | 204 | var bodyLen = len - 10; // Subtract size of ID, type, and two mandatory trailing null bytes 205 | if (bodyLen < 0) { 206 | data = data.slice(packetLen); // Length is too short, discard malformed packet 207 | break; 208 | } 209 | 210 | var id = data.readInt32LE(4); 211 | var type = data.readInt32LE(8); 212 | 213 | if (id == this.rconId) { 214 | if (!this.hasAuthed && type == PacketType.RESPONSE_AUTH) { 215 | this.hasAuthed = true; 216 | this.emit('auth'); 217 | } else if (type == PacketType.RESPONSE_VALUE) { 218 | // Read just the body of the packet (truncate the last null byte) 219 | // See https://developer.valvesoftware.com/wiki/Source_RCON_Protocol for details 220 | var str = data.toString('utf8', 12, 12 + bodyLen); 221 | 222 | if (str.charAt(str.length - 1) === '\n') { 223 | // Emit the response without the newline. 224 | str = str.substring(0, str.length - 1); 225 | } 226 | 227 | this.emit('response', str); 228 | } 229 | } else if (id == -1) { 230 | this.emit('error', new Error('Authentication failed')); 231 | } else { 232 | // ping/pong likely 233 | var str = data.toString('utf8', 12, 12 + bodyLen); 234 | 235 | if (str.charAt(str.length - 1) === '\n') { 236 | // Emit the response without the newline. 237 | str = str.substring(0, str.length - 1); 238 | } 239 | 240 | this.emit('server', str); 241 | } 242 | 243 | data = data.slice(packetLen); 244 | } 245 | 246 | // Keep a reference to remaining data, since the buffer might be split within a packet 247 | this.outstandingData = data; 248 | }; 249 | 250 | Rcon.prototype.socketOnConnect = function () { 251 | this.emit('connect'); 252 | 253 | if (this.tcp) { 254 | this.send(this.password, PacketType.AUTH); 255 | } else if (this.challenge) { 256 | var str = 'challenge rcon\n'; 257 | var sendBuf = Buffer.alloc(str.length + 4); 258 | sendBuf.writeInt32LE(-1, 0); 259 | sendBuf.write(str, 4); 260 | this._sendSocket(sendBuf); 261 | } else { 262 | var sendBuf = Buffer.alloc(5); 263 | sendBuf.writeInt32LE(-1, 0); 264 | sendBuf.writeUInt8(0, 4); 265 | this._sendSocket(sendBuf); 266 | 267 | this.hasAuthed = true; 268 | this.emit('auth'); 269 | } 270 | }; 271 | 272 | Rcon.prototype.socketOnEnd = function () { 273 | this.emit('end'); 274 | this.hasAuthed = false; 275 | }; 276 | 277 | // #endregion 278 | -------------------------------------------------------------------------------- /interactionDUI/dui_source/vite.config.prod.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vite'; 3 | import vue from '@vitejs/plugin-vue'; 4 | 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | base: './', 8 | build: { 9 | rollupOptions: { 10 | output: { 11 | manualChunks: false, 12 | inlineDynamicImports: true, 13 | entryFileNames: '[name].js', 14 | assetFileNames: '[name].[ext]', 15 | }, 16 | }, 17 | outDir: path.join(__dirname, '../dui'), 18 | minify: true, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /interactionDUI/fxmanifest.lua: -------------------------------------------------------------------------------- 1 | -- _ 2 | -- | | 3 | -- _____ _| | _____ ___ _ __ 4 | -- / __\ \ /\ / / |/ / _ \/ _ \ '_ \ 5 | -- \__ \\ V V /| < __/ __/ |_) | 6 | -- |___/ \_/\_/ |_|\_\___|\___| .__/ 7 | -- | | 8 | -- |_| 9 | -- https://github.com/swkeep 10 | 11 | fx_version 'cerulean' 12 | games { 'gta5' } 13 | 14 | name 'interactionDUI' 15 | description 'Dui helper of interacion menu' 16 | version '1.0.0' 17 | author "swkeep" 18 | repository 'https://github.com/swkeep/interaction-menu' 19 | 20 | shared_scripts {} 21 | 22 | client_script { 23 | 'core.client.lua' 24 | } 25 | 26 | files { 27 | "dui/*.*", 28 | } 29 | 30 | lua54 'yes' 31 | -------------------------------------------------------------------------------- /interactionMenu/config.shared.lua: -------------------------------------------------------------------------------- 1 | -- _ 2 | -- | | 3 | -- _____ _| | _____ ___ _ __ 4 | -- / __\ \ /\ / / |/ / _ \/ _ \ '_ \ 5 | -- \__ \\ V V /| < __/ __/ |_) | 6 | -- |___/ \_/\_/ |_|\_\___|\___| .__/ 7 | -- | | 8 | -- |_| 9 | -- https://github.com/swkeep 10 | 11 | Config = {} 12 | 13 | Config.devMode = false 14 | Config.debugPoly = false 15 | 16 | Config.interactionAudio = { 17 | mouseWheel = { 18 | audioName = 'NAV_UP_DOWN', 19 | audioRef = 'HUD_FRONTEND_DEFAULT_SOUNDSET' 20 | }, 21 | onSelect = { 22 | audioName = 'SELECT', 23 | audioRef = 'HUD_FRONTEND_DEFAULT_SOUNDSET' 24 | } 25 | } 26 | 27 | Config.intervals = { 28 | detection = 500 29 | } 30 | 31 | Config.features = { 32 | positionCorrection = true, 33 | timeBasedTheme = true, 34 | drawIndicator = { 35 | active = true 36 | } 37 | } 38 | 39 | Config.icons = { 40 | 'stove', 41 | 'stove2', 42 | 'glowingball', 43 | 'box', 44 | 'wrench', 45 | 'vending' 46 | } 47 | 48 | Config.triggerZoneScript = 'PolyZone' -- ox_lib/PolyZone 49 | Config.screenBoundaryShape = 'none' -- circle/rectangle/none 50 | Config.controls = { 51 | -- Note: Player have to to reset their key bindings to default for changes to take effect. 52 | 53 | -- What is this? 54 | -- This setting allows us to define controls for all menus. 55 | -- Enabling this feature, significantly improves performance (0.04ms to 0.01ms). 56 | enforce = true, 57 | interact = { 58 | -- https://docs.fivem.net/docs/game-references/input-mapper-parameter-ids/keyboard/ 59 | defaultMapper = 'KEYBOARD', 60 | defaultParameter = 'E' 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /interactionMenu/fxmanifest.lua: -------------------------------------------------------------------------------- 1 | -- _ 2 | -- | | 3 | -- _____ _| | _____ ___ _ __ 4 | -- / __\ \ /\ / / |/ / _ \/ _ \ '_ \ 5 | -- \__ \\ V V /| < __/ __/ |_) | 6 | -- |___/ \_/\_/ |_|\_\___|\___| .__/ 7 | -- | | 8 | -- |_| 9 | -- https://github.com/swkeep 10 | 11 | fx_version 'cerulean' 12 | games { 'gta5' } 13 | 14 | name 'interactionMenu' 15 | description 'A standalone raycast and world based interaction menu for FiveM' 16 | version '1.0.0' 17 | author "swkeep" 18 | repository 'https://github.com/swkeep/interaction-menu' 19 | 20 | shared_scripts { 21 | 'config.shared.lua', 22 | } 23 | 24 | client_script { 25 | -- ox_lib 26 | -- '@ox_lib/init.lua', 27 | -- PolyZone 28 | '@PolyZone/client.lua', 29 | '@PolyZone/BoxZone.lua', 30 | '@PolyZone/CircleZone.lua', 31 | '@PolyZone/ComboZone.lua', 32 | 33 | -- bridges 34 | 'lua/bridge/main.lua', 35 | 36 | -- core 37 | 'lua/client/util.lua', 38 | 'lua/client/3dDuiMaker.lua', 39 | 'lua/client/menuContainer.lua', 40 | 'lua/client/userInputManager.lua', 41 | 'lua/client/interact.lua', 42 | 'lua/client/drawIndicator.lua', 43 | 'lua/client/garbageCollector.lua', 44 | 'lua/client/extends/*.lua', 45 | 46 | -- providers 47 | 'lua/providers/qb-target.lua', 48 | 'lua/providers/qb-target_test.lua', 49 | 50 | -- examples / tests 51 | 'lua/examples/*.lua', 52 | } 53 | 54 | server_script { 55 | 'lua/server/server.lua' 56 | } 57 | 58 | files { 59 | 'lua/client/icons/*.*', 60 | 'lua/bridge/qb.lua', 61 | } 62 | 63 | provide 'qb-target' 64 | 65 | lua54 'yes' 66 | -------------------------------------------------------------------------------- /interactionMenu/lua/bridge/main.lua: -------------------------------------------------------------------------------- 1 | local resource_name = GetCurrentResourceName() 2 | 3 | local LoadResourceFile = LoadResourceFile 4 | local context = IsDuplicityVersion() and 'server' or 'client' 5 | 6 | string.split = function(str, pattern) 7 | pattern = pattern or "[^%s]+" 8 | if pattern:len() == 0 then 9 | pattern = "[^%s]+" 10 | end 11 | local parts = { __index = table.insert } 12 | setmetatable(parts, parts) 13 | str:gsub(pattern, parts) 14 | setmetatable(parts, nil) 15 | parts.__index = nil 16 | return parts 17 | end 18 | 19 | local function loadModule(module_name, dir) 20 | local chunk = LoadResourceFile(resource_name, ('%s.lua'):format(dir)) 21 | 22 | if chunk then 23 | local fn, err = load(chunk) 24 | if not fn or err then 25 | return error(('\n^1Error (%s): %s^0'):format(dir, err), 3) 26 | end 27 | 28 | local result = fn() 29 | 30 | return result[context]() 31 | end 32 | end 33 | 34 | ---comment 35 | ---@param module string 36 | local function link(module) 37 | local sub = module:split('[^.]+') 38 | local dir = ('lua/bridge/%s'):format(sub[1]) 39 | return loadModule(sub[1], dir) 40 | end 41 | 42 | Bridge = { 43 | active = false 44 | } 45 | 46 | if GetResourceState('qb-core') == 'started' then 47 | local bridge_link = link('qb') 48 | Bridge.hasItem = bridge_link.hasItem 49 | Bridge.getJob = bridge_link.getJob 50 | Bridge.getGang = bridge_link.getGang 51 | Bridge.active = true 52 | end 53 | -------------------------------------------------------------------------------- /interactionMenu/lua/bridge/qb.lua: -------------------------------------------------------------------------------- 1 | local function updatePlayerItems(playerData, qb_items) 2 | for _, itemData in pairs(playerData.items or {}) do 3 | if qb_items[itemData.name] then 4 | qb_items[itemData.name] = qb_items[itemData.name] + itemData.amount 5 | else 6 | qb_items[itemData.name] = itemData.amount 7 | end 8 | end 9 | end 10 | 11 | local function reset(t) table.wipe(t) end 12 | 13 | return { 14 | client = function() 15 | local QBCore = exports['qb-core']:GetCoreObject() 16 | local playerData = QBCore.Functions.GetPlayerData() or {} 17 | local qb_items = {} 18 | 19 | AddEventHandler('QBCore:Client:OnPlayerLoaded', function() 20 | playerData = QBCore.Functions.GetPlayerData() 21 | reset(qb_items) 22 | updatePlayerItems(playerData, qb_items) 23 | end) 24 | 25 | RegisterNetEvent('QBCore:Client:OnPlayerUnload', function() 26 | playerData = {} 27 | reset(qb_items) 28 | end) 29 | 30 | RegisterNetEvent('QBCore:Player:SetPlayerData', function(val) 31 | playerData = QBCore.Functions.GetPlayerData() 32 | reset(qb_items) 33 | updatePlayerItems(playerData, qb_items) 34 | end) 35 | 36 | RegisterNetEvent('QBCore:Client:OnJobUpdate', function(JobInfo) 37 | playerData = QBCore.Functions.GetPlayerData() 38 | reset(qb_items) 39 | updatePlayerItems(playerData, qb_items) 40 | end) 41 | 42 | RegisterNetEvent('QBCore:Client:OnGangUpdate', function(GangInfo) 43 | playerData = QBCore.Functions.GetPlayerData() 44 | reset(qb_items) 45 | updatePlayerItems(playerData, qb_items) 46 | end) 47 | 48 | local function init() 49 | playerData = QBCore.Functions.GetPlayerData() 50 | reset(qb_items) 51 | updatePlayerItems(playerData, qb_items) 52 | end 53 | 54 | init() 55 | 56 | return { 57 | ['getJob'] = function() 58 | local job = playerData['job'] 59 | return job.name, job.grade.level 60 | end, 61 | ['getGang'] = function() 62 | local gang = playerData['gang'] 63 | return gang.name, gang.grade.level 64 | end, 65 | ['hasItem'] = function(itemName, requiredAmount) 66 | if not requiredAmount then requiredAmount = 1 end 67 | local hasItem = qb_items[itemName] ~= nil 68 | local hasEnough = hasItem and qb_items[itemName] >= requiredAmount or false 69 | return hasItem, hasEnough 70 | end, 71 | ['hasItems'] = function(itemNames, requiredAmount) 72 | if not requiredAmount then requiredAmount = 1 end 73 | 74 | for _, itemName in pairs(itemNames) do 75 | local hasItem = qb_items[itemName] ~= nil 76 | local hasEnough = hasItem and qb_items[itemName] >= requiredAmount or false 77 | 78 | if not hasEnough then 79 | return false, itemName 80 | end 81 | end 82 | 83 | return true 84 | end 85 | } 86 | end, 87 | server = function() 88 | 89 | end 90 | } 91 | -------------------------------------------------------------------------------- /interactionMenu/lua/client/3dDuiMaker.lua: -------------------------------------------------------------------------------- 1 | local scaleform 2 | local text_color = { 255, 255, 255 } 3 | 4 | local function Draw2DText(content, font, colour, scale, x, y) 5 | SetTextFont(font) 6 | SetTextScale(scale, scale) 7 | SetTextColour(colour[1], colour[2], colour[3], 255) 8 | SetTextEntry("STRING") 9 | SetTextDropShadow(0, 0, 0, 0, 255) 10 | SetTextDropShadow() 11 | SetTextEdge(4, 0, 0, 0, 255) 12 | SetTextOutline() 13 | AddTextComponentString(content) 14 | DrawText(x, y) 15 | end 16 | 17 | local function handleArrowInput(center) 18 | local rot = GetGameplayCamRot(2) 19 | local heading = rot.z 20 | local delta = 0.05 21 | DisableControlAction(0, 36, true) 22 | if IsDisabledControlPressed(0, 36) then -- ctrl held down 23 | delta = 0.01 24 | end 25 | 26 | DisableControlAction(0, 27, true) 27 | if IsDisabledControlPressed(0, 27) then -- arrow up 28 | local newCenter = PolyZone.rotate(center.xy, vector2(center.x, center.y + delta), heading) 29 | return vector3(newCenter.x, newCenter.y, center.z) 30 | end 31 | if IsControlPressed(0, 173) then -- arrow down 32 | local newCenter = PolyZone.rotate(center.xy, vector2(center.x, center.y - delta), heading) 33 | return vector3(newCenter.x, newCenter.y, center.z) 34 | end 35 | if IsControlPressed(0, 174) then -- arrow left 36 | local newCenter = PolyZone.rotate(center.xy, vector2(center.x - delta, center.y), heading) 37 | return vector3(newCenter.x, newCenter.y, center.z) 38 | end 39 | if IsControlPressed(0, 175) then -- arrow right 40 | local newCenter = PolyZone.rotate(center.xy, vector2(center.x + delta, center.y), heading) 41 | return vector3(newCenter.x, newCenter.y, center.z) 42 | end 43 | 44 | return center 45 | end 46 | 47 | local function handle_rotation(r, _type) 48 | local x, y, z = r.x, r.y, r.z 49 | local delta = 1.0 50 | 51 | DisableControlAction(0, 36, true) 52 | if IsDisabledControlPressed(0, 36) then -- ctrl held down 53 | delta = 0.1 54 | end 55 | 56 | if _type == 'x' then 57 | -- Rotation around X-axis 58 | DisableControlAction(0, 174, true) 59 | if IsDisabledControlPressed(0, 174) then 60 | x = x + delta 61 | end 62 | DisableControlAction(0, 175, true) 63 | if IsDisabledControlPressed(0, 175) then 64 | x = x - delta 65 | end 66 | elseif _type == 'y' then 67 | -- Rotation around Y-axis 68 | DisableControlAction(0, 174, true) 69 | if IsDisabledControlPressed(0, 174) then 70 | y = y + delta 71 | end 72 | DisableControlAction(0, 175, true) 73 | if IsDisabledControlPressed(0, 175) then 74 | y = y - delta 75 | end 76 | elseif _type == 'z' then 77 | -- Rotation around Z-axis 78 | DisableControlAction(0, 174, true) 79 | if IsDisabledControlPressed(0, 174) then 80 | z = z + delta 81 | end 82 | DisableControlAction(0, 175, true) 83 | if IsDisabledControlPressed(0, 175) then 84 | z = z - delta 85 | end 86 | end 87 | 88 | 89 | return vec3(x, y, z) 90 | end 91 | 92 | local function handle_height(p) 93 | local delta = 0.05 94 | DisableControlAction(0, 36, true) 95 | if IsDisabledControlPressed(0, 36) then -- ctrl held down 96 | delta = 0.01 97 | end 98 | 99 | DisableControlAction(0, 27, true) 100 | if IsDisabledControlPressed(0, 27) then 101 | p = vector3(p.x, p.y, p.z + delta) 102 | end 103 | 104 | DisableControlAction(0, 173, true) 105 | if IsDisabledControlPressed(0, 173) then 106 | p = vector3(p.x, p.y, p.z - delta) 107 | end 108 | 109 | return p 110 | end 111 | 112 | CreateThread(function() 113 | Util.preloadSharedTextureDict() 114 | 115 | local timeout = 5000 116 | local startTime = GetGameTimer() 117 | 118 | repeat 119 | Wait(1000) 120 | until GetResourceState('interactionDUI') == 'started' or (GetGameTimer() - startTime >= timeout) 121 | 122 | if GetResourceState('interactionDUI') == 'started' then 123 | scaleform = exports['interactionDUI']:Get() 124 | scaleform.setPosition(vector3(0, 0, 0)) 125 | scaleform.dettach() 126 | scaleform.setStatus(false) 127 | else 128 | print('ResourceState:', GetResourceState('interactionDUI')) 129 | error("interactionDUI resource did not start within the timeout period") 130 | end 131 | end) 132 | 133 | local function drawText(state) 134 | -- Display instructions 135 | Draw2DText("Press ~g~ENTER~w~ to Save | Press ~g~ESC~w~ to Exit | ~g~X~w~ to Reset", 4, 136 | text_color, 0.4, 0.43, 0.863) 137 | 138 | Draw2DText("Change Rotation Axis: ~g~E~w~ | Change Arrow: ~g~Q~w~ | Precision Mode: Hold ~g~Ctrl~w~", 4, 139 | text_color, 0.4, 0.43, 0.883) 140 | 141 | -- Display current settings 142 | Draw2DText(("Rotation Axis: ~g~%s~w~ | Movement Type: ~g~%s~w~"):format( 143 | string.upper(state.mousewheel), 144 | string.upper(state.arrow)), 4, 145 | text_color, 0.4, 0.43, 0.903) 146 | end 147 | 148 | local menuData = { 149 | id = '1231', 150 | loading = false, 151 | menus = { 152 | { 153 | id = 1, 154 | flags = { 155 | hide = false 156 | }, 157 | options = { 158 | { 159 | label = 'https', 160 | picture = { 161 | url = 162 | 'https://cdn.discordapp.com/attachments/1059914360887193711/1128867024827863121/photo-1610824224972-db9878a2fe2c.jpg', 163 | }, 164 | flags = { 165 | hide = false 166 | }, 167 | } 168 | } 169 | } 170 | }, 171 | selected = { false } 172 | } 173 | 174 | local function showDemoInteraction(position, rotation) 175 | scaleform.setPosition(position) 176 | scaleform.setRotation(rotation) 177 | scaleform.set3d(true) 178 | scaleform.setScale(1) 179 | scaleform.setStatus(true) 180 | 181 | scaleform.send("interactionMenu:loading:hide") 182 | scaleform.send("interactionMenu:menu:show", { 183 | menus = menuData.menus, 184 | selected = 1 185 | }) 186 | end 187 | 188 | local controls = { 189 | mousewheel = { 'x', 'y', 'z' }, 190 | arrow = { 'position', 'height', 'rotation' } 191 | } 192 | 193 | local function hideScaleform() 194 | if not scaleform then return end 195 | scaleform.send("interactionMenu:hideMenu") 196 | scaleform.send("interactionMenu:loading:hide") 197 | exports['interactionMenu']:pause(false) 198 | scaleform.setStatus(false) 199 | end 200 | 201 | function Util.start(p, r) 202 | exports['interactionMenu']:pause(true) 203 | local ped = PlayerPedId() 204 | local state = { 205 | mousewheel = 'x', 206 | imousewheel = 1, 207 | arrow = 'position', 208 | iarrow = 1, 209 | } 210 | 211 | local position = p or GetEntityCoords(ped) 212 | local rotation = r or vec3(0, 0, 0) 213 | showDemoInteraction(position, rotation) 214 | 215 | while true do 216 | drawText(state) 217 | 218 | if state.arrow == 'position' then 219 | position = handleArrowInput(position) 220 | scaleform.setPosition(position) 221 | elseif state.arrow == 'height' then 222 | position = handle_height(position) 223 | scaleform.setPosition(position) 224 | elseif state.arrow == 'rotation' then 225 | rotation = handle_rotation(rotation, state.mousewheel) 226 | scaleform.setRotation(rotation) 227 | end 228 | 229 | if IsControlJustReleased(0, 38) then 230 | state.iarrow = state.iarrow % #controls.arrow + 1 231 | state.arrow = controls.arrow[state.iarrow] 232 | end 233 | 234 | DisableControlAction(0, 44, true) 235 | if IsDisabledControlJustPressed(0, 44) then 236 | state.imousewheel = state.imousewheel % #controls.mousewheel + 1 237 | state.mousewheel = controls.mousewheel[state.imousewheel] 238 | end 239 | 240 | if IsControlJustReleased(0, 154) then 241 | position = GetEntityCoords(ped) 242 | rotation = vec3(0, 0, 0) 243 | end 244 | 245 | if IsControlJustReleased(0, 191) then 246 | hideScaleform() 247 | return { 248 | ['position'] = position, 249 | ['rotation'] = rotation 250 | } 251 | end 252 | 253 | if IsDisabledControlPressed(0, 200) then 254 | hideScaleform() 255 | return 'exit' 256 | end 257 | 258 | Wait(0) 259 | end 260 | end 261 | 262 | RegisterNetEvent('interaction-menu:client:helper', function() 263 | Util.print_table(Util.start()) 264 | end) 265 | 266 | AddEventHandler('onResourceStop', function(resource) 267 | if resource == GetCurrentResourceName() then return end 268 | 269 | hideScaleform() 270 | end) 271 | -------------------------------------------------------------------------------- /interactionMenu/lua/client/drawIndicator.lua: -------------------------------------------------------------------------------- 1 | if not Config.features.drawIndicator.active then 2 | return 3 | end 4 | 5 | -- #region Show sprite while holding alt 6 | 7 | local SpatialHashGrid = Util.SpatialHashGrid 8 | local currentSpriteThreadHash = nil 9 | local isSpriteThreadRunning = false 10 | local isTargetSpritesActive = false 11 | local StateManager = Util.StateManager() 12 | local grid_position = SpatialHashGrid:new('position', 100) 13 | local visiblePoints = {} 14 | local visiblePointCount = 0 15 | 16 | CreateThread(function() 17 | local txd = CreateRuntimeTxd('interaction_txd_indicator') 18 | CreateRuntimeTextureFromImage(txd, 'indicator', "lua/client/icons/indicator.png") 19 | for index, value in ipairs(Config.icons) do 20 | CreateRuntimeTextureFromImage(txd, value, ("lua/client/icons/%s.png"):format(value)) 21 | end 22 | end) 23 | 24 | -- even tho we can set it using the menu's data i use a const white color 25 | -- sprite colors 26 | local red = 255 27 | local green = 255 28 | local blue = 255 29 | local alpha = 255 30 | 31 | -- minimum and maximum scale factors for x and y 32 | local minScaleX = 0.02 / 4 33 | local maxScaleX = minScaleX * 5 34 | local minScaleY = 0.035 / 4 35 | local maxScaleY = minScaleY * 5 36 | 37 | -- Distance thresholds 38 | local minDistance = 2.0 39 | local maxDistance = 20.0 40 | local maxEntities = 10 41 | 42 | -- draw the sprite with scaling based on distance 43 | local function drawSprite(p, player_position, icon) 44 | if not p then return end 45 | -- Calculate the distance between the player and the point 46 | local distance = #(vec3(p.x, p.y, p.z) - player_position) 47 | local clampedDistance = math.max(minDistance, math.min(maxDistance, distance)) 48 | 49 | -- Pre-calculate the scale factor range 50 | local scaleRangeX = maxScaleX - minScaleX 51 | local scaleRangeY = maxScaleY - minScaleY 52 | local distanceRange = maxDistance - minDistance 53 | local normalizedDistance = (clampedDistance - minDistance) / distanceRange 54 | 55 | -- Calculate the scale factors based on the clamped distance 56 | local scaleX = minScaleX + scaleRangeX * (1 - normalizedDistance) 57 | local scaleY = minScaleY + scaleRangeY * (1 - normalizedDistance) 58 | 59 | -- Set the draw origin to the point's coordinates 60 | SetDrawOrigin(p.x, p.y, p.z, 0) 61 | 62 | -- Draw the sprite with the calculated scales 63 | DrawSprite('interaction_txd_indicator', icon or 'indicator', 0, 0, scaleX, scaleY, 0, red, green, blue, alpha) 64 | ClearDrawOrigin() 65 | end 66 | 67 | local nearby_objects = {} 68 | local nearby_objects_limited = {} 69 | 70 | local function getNearbyObjects(isActive, currentMenu, coords) 71 | local objects = GetGamePool('CObject') 72 | local entityHandle = StateManager.get('entityHandle') 73 | nearby_objects_limited = {} 74 | 75 | for i = 1, #objects do 76 | local object = objects[i] 77 | local objectCoords = GetEntityCoords(object) 78 | local distance = #(coords - objectCoords) 79 | 80 | if distance < maxDistance then 81 | local existingData = nearby_objects[object] 82 | local entity_type = GetEntityType(object) 83 | local model = GetEntityModel(object) 84 | 85 | local menuType = Container.getMenuType { 86 | model = model, 87 | entity = object, 88 | entityType = entity_type 89 | } 90 | 91 | if menuType > 1 and entityHandle ~= object then 92 | if not existingData then 93 | local menu = Container.getMenu(model, object, nil) 94 | nearby_objects[object] = { 95 | object = object, 96 | coords = objectCoords, 97 | type = entity_type, 98 | icon = menu and menu.icon, 99 | distance = distance, 100 | menu = menu 101 | } 102 | else 103 | existingData.coords = objectCoords 104 | existingData.distance = distance 105 | end 106 | end 107 | else 108 | nearby_objects[object] = nil 109 | end 110 | end 111 | 112 | for object, data in pairs(nearby_objects) do 113 | if isActive == false or data and entityHandle ~= data.object then 114 | nearby_objects_limited[#nearby_objects_limited + 1] = data 115 | end 116 | end 117 | 118 | table.sort(nearby_objects_limited, function(a, b) 119 | return a.distance < b.distance 120 | end) 121 | end 122 | 123 | function UpdateNearbyObjects() 124 | local playerPosition = StateManager.get('playerPosition') 125 | local currentMenu = StateManager.get('id') 126 | local isActive = StateManager.get('active') 127 | getNearbyObjects(isActive, currentMenu, playerPosition) 128 | end 129 | 130 | function CleanNearbyObjects() 131 | nearby_objects = {} 132 | nearby_objects_limited = {} 133 | end 134 | 135 | local function StartSpriteThread() 136 | if isSpriteThreadRunning then return end 137 | isSpriteThreadRunning = true 138 | local player = PlayerPedId() 139 | local playerPosition = StateManager.get('playerPosition') 140 | local currentMenu = StateManager.get('id') 141 | local isActive = StateManager.get('active') 142 | 143 | -- This is kinda overkill, but in my testing, sometimes one of these threads would stay alive, 144 | -- causing flickering and performance drops. 145 | local threadHash = math.random(1000000) 146 | currentSpriteThreadHash = threadHash 147 | 148 | CreateThread(function() 149 | while isSpriteThreadRunning and currentSpriteThreadHash == threadHash do 150 | isActive = StateManager.get('active') 151 | currentMenu = StateManager.get('id') 152 | getNearbyObjects(isActive, currentMenu, playerPosition) 153 | local nearPoints, totalNearPoints = grid_position:queryRange(playerPosition, 20) 154 | visiblePoints, visiblePointCount = Util.filterVisiblePointsWithinRange(playerPosition, nearPoints) 155 | 156 | Wait(1000) 157 | end 158 | end) 159 | 160 | CreateThread(function() 161 | while isTargetSpritesActive and currentSpriteThreadHash == threadHash do 162 | playerPosition = GetEntityCoords(player) 163 | 164 | if visiblePointCount > 0 then 165 | for _, value in ipairs(visiblePoints.inView) do 166 | if value.id ~= currentMenu then 167 | drawSprite(value.point, playerPosition, visiblePoints.closest.icon) 168 | end 169 | end 170 | 171 | if visiblePoints.closest.id ~= currentMenu and not isActive then 172 | drawSprite(visiblePoints.closest.point, playerPosition, visiblePoints.closest.icon) 173 | end 174 | end 175 | 176 | for index, value in pairs(nearby_objects_limited) do 177 | if index > maxEntities then break end 178 | drawSprite(value.coords, playerPosition, value.icon) 179 | end 180 | 181 | Wait(0) 182 | end 183 | isSpriteThreadRunning = false 184 | end) 185 | end 186 | 187 | RegisterCommand('+toggleTargetSprites', function() 188 | isTargetSpritesActive = true 189 | StartSpriteThread() 190 | end, false) 191 | 192 | RegisterCommand('-toggleTargetSprites', function() 193 | isTargetSpritesActive = false 194 | isSpriteThreadRunning = false 195 | end, false) 196 | 197 | RegisterKeyMapping('+toggleTargetSprites', 'Toggle Target Sprites', 'keyboard', 'LMENU') 198 | RegisterKeyMapping('~!+toggleTargetSprites', 'Toggle Target Sprites - Alternate Key', 'keyboard', 'RMENU') 199 | 200 | -- #endregion 201 | -------------------------------------------------------------------------------- /interactionMenu/lua/client/extends/nested.lua: -------------------------------------------------------------------------------- 1 | -- _ 2 | -- | | 3 | -- _____ _| | _____ ___ _ __ 4 | -- / __\ \ /\ / / |/ / _ \/ _ \ '_ \ 5 | -- \__ \\ V V /| < __/ __/ |_) | 6 | -- |___/ \_/\_/ |_|\_\___|\___| .__/ 7 | -- | | 8 | -- |_| 9 | -- https://github.com/swkeep 10 | local __export = exports['interactionMenu'] 11 | local refresh = __export.refresh 12 | local NestedMenuBuilder = {} 13 | NestedMenuBuilder.__index = NestedMenuBuilder 14 | 15 | function string:split(delimiter) 16 | local result = {} 17 | local pattern = string.format("([^%s]+)", delimiter) 18 | for part in self:gmatch(pattern) do 19 | table.insert(result, part) 20 | end 21 | 22 | return result 23 | end 24 | 25 | function NestedMenuBuilder:addHeader() 26 | table.insert(self.options, { 27 | label = 'Back', 28 | icon = 'fa fa-arrow-left', 29 | action = function() 30 | local t = self.current_menu:split('/') 31 | table.remove(t) 32 | self.current_menu = (#t > 0 and table.concat(t, '/') .. '/') or "/" 33 | 34 | self:updateMenuVisibility(#self.current_menu:split("/") + 1) 35 | refresh() 36 | end, 37 | canInteract = function() 38 | return self.current_menu ~= "/" 39 | end 40 | }) 41 | end 42 | 43 | function NestedMenuBuilder:updateMenuVisibility(depth) 44 | for i = 2, #self.options do 45 | local isVisible = #self.options[i].path:split('/') == depth 46 | __export:set { 47 | menuId = self.menu_id, 48 | type = 'hide', 49 | option = i, 50 | value = not isVisible 51 | } 52 | end 53 | end 54 | 55 | -- this is kind of recursive! 56 | function NestedMenuBuilder:generateOptions(options, full_list, parentPath) 57 | for index, option in ipairs(options) do 58 | local path = parentPath and (parentPath .. '/' .. index) or '/' .. tostring(index) 59 | local isMainMenu = not path:find('/', 2) 60 | local hasSubMenu = option.subMenu ~= nil 61 | 62 | local action = hasSubMenu and function() 63 | self.current_menu = path .. "/" 64 | self:updateMenuVisibility(#self.current_menu:split("/") + 1) 65 | if option.action then option.action() end 66 | refresh() 67 | end or option.action 68 | 69 | table.insert(full_list, { 70 | path = path, 71 | label = option.label, 72 | icon = option.icon, 73 | action = action, 74 | bind = option.bind, 75 | event = option.event, 76 | command = option.command, 77 | canInteract = option.canInteract, 78 | progress = option.progress, 79 | dynamic = option.dynamic, 80 | style = option.style, 81 | video = option.video, 82 | picture = option.picture, 83 | hide = not isMainMenu, 84 | subMenu = hasSubMenu 85 | }) 86 | 87 | if hasSubMenu then 88 | self:generateOptions(option.subMenu, full_list, path) 89 | end 90 | end 91 | end 92 | 93 | function NestedMenuBuilder:create(t) 94 | self = setmetatable({}, NestedMenuBuilder) 95 | self.itemsPerPage = t.itemsPerPage or 6 96 | self.current_menu = "/" 97 | self.menu_id = nil 98 | self.options = {} 99 | self.user_options = t.options or {} 100 | 101 | self:addHeader() 102 | self:generateOptions(self.user_options, self.options, nil) 103 | 104 | t.options = self.options 105 | self.menu_id = __export:Create(t) 106 | 107 | return self.menu_id 108 | end 109 | 110 | local function interface(t) 111 | return NestedMenuBuilder:create(t) 112 | end 113 | 114 | exports("nestedMenu", interface) 115 | -------------------------------------------------------------------------------- /interactionMenu/lua/client/extends/pagination.lua: -------------------------------------------------------------------------------- 1 | -- _ 2 | -- | | 3 | -- _____ _| | _____ ___ _ __ 4 | -- / __\ \ /\ / / |/ / _ \/ _ \ '_ \ 5 | -- \__ \\ V V /| < __/ __/ |_) | 6 | -- |___/ \_/\_/ |_|\_\___|\___| .__/ 7 | -- | | 8 | -- |_| 9 | -- https://github.com/swkeep 10 | local __export = exports['interactionMenu'] 11 | local refresh = __export.refresh 12 | local PaginationBuilder = {} 13 | PaginationBuilder.__index = PaginationBuilder 14 | 15 | function PaginationBuilder:calculatePages() 16 | self.pages = math.ceil(#self.user_options / self.itemsPerPage) 17 | end 18 | 19 | function PaginationBuilder:addHeader() 20 | self.options = { 21 | { 22 | label = "Page: #1 | Total: " .. tostring(self.pages), 23 | bind = function() 24 | return ("Page: #%d | Total: %d"):format(self.currentPage, self.pages) 25 | end, 26 | canInteract = function() 27 | return self.pages > 1 28 | end 29 | }, 30 | } 31 | end 32 | 33 | function PaginationBuilder:addFooter() 34 | local function updatePage(direction) 35 | self.currentPage = (self.currentPage - 1 + direction + self.pages) % self.pages + 1 36 | 37 | for i = 2, #self.options - 2 do 38 | local option = self.options[i] 39 | __export:set { 40 | menuId = self.menu_id, 41 | type = 'hide', 42 | option = i, 43 | value = option.page ~= self.currentPage 44 | } 45 | end 46 | 47 | refresh() 48 | end 49 | 50 | -- Pagination controls 51 | if self.pages > 1 then 52 | table.insert(self.options, { 53 | label = 'Prev Page', 54 | icon = 'fa fa-arrow-left', 55 | action = function() updatePage(-1) end 56 | }) 57 | 58 | table.insert(self.options, { 59 | label = 'Next Page', 60 | icon = 'fa fa-arrow-right', 61 | action = function() updatePage(1) end 62 | }) 63 | end 64 | end 65 | 66 | function PaginationBuilder:generateOptions() 67 | for index, option in ipairs(self.user_options) do 68 | local page = math.ceil(index / self.itemsPerPage) 69 | table.insert(self.options, { 70 | label = option.label, 71 | icon = option.icon, 72 | action = option.action, 73 | bind = option.bind, 74 | event = option.event, 75 | command = option.command, 76 | canInteract = option.canInteract, 77 | progress = option.progress, 78 | dynamic = option.dynamic, 79 | style = option.style, 80 | video = option.video, 81 | picture = option.picture, 82 | page = page, 83 | hide = page ~= 1, 84 | }) 85 | end 86 | end 87 | 88 | function PaginationBuilder:create(t) 89 | self = setmetatable({}, PaginationBuilder) 90 | self.itemsPerPage = t.itemsPerPage or 6 91 | self.currentPage = 1 92 | self.menu_id = nil 93 | self.options = {} 94 | self.user_options = t.options or {} 95 | 96 | self:addHeader() 97 | self:calculatePages() 98 | self:generateOptions() 99 | self:addFooter() 100 | 101 | t.options = self.options 102 | self.menu_id = __export:Create(t) 103 | 104 | return self.menu_id 105 | end 106 | 107 | local function interface(t) 108 | return PaginationBuilder:create(t) 109 | end 110 | 111 | exports("paginatedMenu", interface) 112 | exports("paginateMenu", interface) 113 | -------------------------------------------------------------------------------- /interactionMenu/lua/client/garbageCollector.lua: -------------------------------------------------------------------------------- 1 | local COLLECTOR_CONFIG = { 2 | INTERVAL = 1000 * 60 3 | } 4 | 5 | local GarbageCollector = {} 6 | local grid = Util.SpatialHashGrid:get('position') 7 | 8 | -- Helper to remove menu data 9 | local function removeMenuData(key, value, indexTable, idField) 10 | local menus = indexTable[value[idField]] 11 | if not menus then return end 12 | 13 | for i, menuId in ipairs(menus) do 14 | if menuId == value.id then 15 | table.remove(menus, i) 16 | if #menus == 0 then 17 | indexTable[value[idField]] = nil 18 | end 19 | Container.data[key] = nil 20 | break 21 | end 22 | end 23 | end 24 | 25 | local function removeMenuSimple(key) 26 | if Container.data[key] then 27 | Container.data[key] = nil 28 | end 29 | end 30 | 31 | function GarbageCollector.position(menuId, value) 32 | removeMenuSimple(menuId) 33 | end 34 | 35 | function GarbageCollector.zone(menuId, value) 36 | removeMenuSimple(menuId) 37 | end 38 | 39 | function GarbageCollector.entity(key, value) 40 | removeMenuData(key, value, Container.indexes.entities, 'handle') 41 | end 42 | 43 | function GarbageCollector.player(key, value) 44 | removeMenuData(key, value, Container.indexes.players, 'playerId') 45 | end 46 | 47 | function GarbageCollector.model(key, value) 48 | removeMenuData(key, value, Container.indexes.models, 'model') 49 | end 50 | 51 | function GarbageCollector.bone(key, value) 52 | local vehicleBones = Container.indexes.bones[value.vehicle.handle] 53 | if vehicleBones then 54 | removeMenuData(key, value, vehicleBones, 'bone') 55 | end 56 | end 57 | 58 | function Container.remove(id) 59 | local menuRef = Container.get(id) 60 | if not menuRef then 61 | Util.print_debug('Could not find this menu') 62 | return 63 | end 64 | 65 | -- remove zone and position triggers early 66 | if menuRef.type == 'zone' then 67 | local zone = Container.zones[id] 68 | Container.zones[id] = nil 69 | zone:destroy() 70 | elseif menuRef.type == 'position' then 71 | grid:remove(menuRef.position) 72 | elseif menuRef.type == 'entity' and menuRef.tracker == 'boundingBox' then 73 | EntityDetector.unwatch(menuRef.entity.handle) 74 | end 75 | 76 | if menuRef.type == "bones" or menuRef.type == "entities" or menuRef.type == "zones" or menuRef.type == 'peds' then 77 | if menuRef.type == 'bones' then 78 | for index, value in ipairs(Container.indexes.globals['bones'][menuRef.bone]) do 79 | if value == id then 80 | table.remove(Container.indexes.globals['bones'][menuRef.bone], index) 81 | CleanNearbyObjects() 82 | break 83 | end 84 | end 85 | else 86 | for index, value in pairs(Container.indexes.globals[menuRef.type]) do 87 | if value == id then 88 | table.remove(Container.indexes.globals[menuRef.type], index) 89 | CleanNearbyObjects() 90 | break 91 | end 92 | end 93 | end 94 | end 95 | 96 | menuRef.flags.deleted = true 97 | menuRef.deletedAt = GetGameTimer() / 1000 98 | Container.total = Container.total - 1 99 | end 100 | 101 | exports('remove', Container.remove) 102 | 103 | -- Garbage collection thread 104 | CreateThread(function() 105 | while true do 106 | Wait(COLLECTOR_CONFIG.INTERVAL) 107 | 108 | local currentTime = GetGameTimer() / 1000 109 | local chunkSize = 500 110 | local processed = 0 111 | 112 | for key, value in pairs(Container.data) do 113 | processed = processed + 1 114 | 115 | -- Check and remove deleted items if they have passed the delay period 116 | if value.flags.deleted and (currentTime - value.deletedAt) > 6 then 117 | local collector = GarbageCollector[value.type] 118 | if collector then 119 | collector(key, value) 120 | end 121 | end 122 | 123 | -- Pause between chunks to avoid overwhelming the game 124 | if processed >= chunkSize then 125 | processed = 0 126 | Wait(250) 127 | end 128 | end 129 | end 130 | end) 131 | -------------------------------------------------------------------------------- /interactionMenu/lua/client/icons/box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swkeep/interaction-menu/cb68e9ac318b22b39784fcec3b3fe2d1fe1c7dfe/interactionMenu/lua/client/icons/box.png -------------------------------------------------------------------------------- /interactionMenu/lua/client/icons/glowingball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swkeep/interaction-menu/cb68e9ac318b22b39784fcec3b3fe2d1fe1c7dfe/interactionMenu/lua/client/icons/glowingball.png -------------------------------------------------------------------------------- /interactionMenu/lua/client/icons/indicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swkeep/interaction-menu/cb68e9ac318b22b39784fcec3b3fe2d1fe1c7dfe/interactionMenu/lua/client/icons/indicator.png -------------------------------------------------------------------------------- /interactionMenu/lua/client/icons/stove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swkeep/interaction-menu/cb68e9ac318b22b39784fcec3b3fe2d1fe1c7dfe/interactionMenu/lua/client/icons/stove.png -------------------------------------------------------------------------------- /interactionMenu/lua/client/icons/stove2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swkeep/interaction-menu/cb68e9ac318b22b39784fcec3b3fe2d1fe1c7dfe/interactionMenu/lua/client/icons/stove2.png -------------------------------------------------------------------------------- /interactionMenu/lua/client/icons/vending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swkeep/interaction-menu/cb68e9ac318b22b39784fcec3b3fe2d1fe1c7dfe/interactionMenu/lua/client/icons/vending.png -------------------------------------------------------------------------------- /interactionMenu/lua/client/icons/wrench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swkeep/interaction-menu/cb68e9ac318b22b39784fcec3b3fe2d1fe1c7dfe/interactionMenu/lua/client/icons/wrench.png -------------------------------------------------------------------------------- /interactionMenu/lua/client/userInputManager.lua: -------------------------------------------------------------------------------- 1 | local holdStart = nil 2 | local lastHoldTrigger = nil 3 | local DisableControlAction = DisableControlAction 4 | local IsControlJustReleased = IsControlJustReleased 5 | local IsDisabledControlJustReleased = IsDisabledControlJustReleased 6 | 7 | -- UserInputManager Class 8 | UserInputManager = {} 9 | UserInputManager.__index = UserInputManager 10 | 11 | function UserInputManager:new() 12 | local instance = setmetatable({}, self) 13 | instance.currentMenuData = nil 14 | instance.holdStart = nil 15 | instance.lastHoldTrigger = nil 16 | instance.holding = false 17 | return instance 18 | end 19 | 20 | function UserInputManager:setMenuData(menuData) 21 | self.currentMenuData = menuData 22 | end 23 | 24 | function UserInputManager:clearMenuData() 25 | self.currentMenuData = nil 26 | end 27 | 28 | function UserInputManager:handleMouseWheel(direction) 29 | if not self.currentMenuData then return end 30 | Interact:scroll(self.currentMenuData, direction) 31 | end 32 | 33 | function UserInputManager:holdKey() 34 | if self.currentMenuData and self.currentMenuData.indicator and self.currentMenuData.indicator.hold then 35 | local currentTime = GetGameTimer() 36 | 37 | if self.lastHoldTrigger then 38 | if (currentTime - self.lastHoldTrigger) >= 1000 then 39 | self.lastHoldTrigger = nil 40 | else 41 | return 42 | end 43 | end 44 | 45 | if not self.holdStart then 46 | self.holdStart = currentTime 47 | end 48 | 49 | local holdDuration = self.currentMenuData.indicator.hold 50 | local elapsedTime = currentTime - self.holdStart 51 | local percentage = (elapsedTime / holdDuration) * 100 52 | Interact:fillIndicator(self.currentMenuData, percentage) 53 | 54 | if elapsedTime >= holdDuration then 55 | self.holdStart = nil 56 | self.lastHoldTrigger = currentTime 57 | Container.keyPress(self.currentMenuData) 58 | Interact:indicatorStatus(self.currentMenuData, 'success') 59 | Interact:fillIndicator(self.currentMenuData, 0) 60 | end 61 | end 62 | end 63 | 64 | function UserInputManager:pressKey() 65 | if not self.currentMenuData then return end 66 | Container.keyPress(self.currentMenuData) 67 | end 68 | 69 | function UserInputManager:leftEarly() 70 | if self.currentMenuData then 71 | Util.print_debug("Player stopped holding the key early") 72 | Interact:indicatorStatus(self.currentMenuData, 'fail') 73 | Interact:fillIndicator(self.currentMenuData, 0) 74 | self.holdStart = nil 75 | end 76 | end 77 | 78 | function UserInputManager:startHoldDetection() 79 | if not self.currentMenuData then return end 80 | if self.holding then return end 81 | self.holding = true 82 | 83 | -- what we should use: hold or press 84 | if self.currentMenuData.indicator and self.currentMenuData.indicator.hold then 85 | CreateThread(function() 86 | while self.holding do 87 | if self.currentMenuData then 88 | self:holdKey() 89 | end 90 | Wait(0) 91 | end 92 | 93 | self:leftEarly() 94 | self.holding = false 95 | end) 96 | else 97 | self:pressKey() 98 | end 99 | end 100 | 101 | function UserInputManager:stopHoldDetection() 102 | self.holding = false 103 | end 104 | 105 | function UserInputManager.defaultMouseWheel(menuData) 106 | -- not the best way to do it but it works if we add new options on runtime 107 | -- HideHudComponentThisFrame(19) 108 | 109 | -- Mouse Wheel Down / Arrow Down 110 | DisableControlAction(0, 85, true) -- INPUT_VEH_RADIO_WHEEL (Mouse scroll wheel) 111 | DisableControlAction(0, 86, true) -- INPUT_VEH_NEXT_RADIO (Mouse wheel up) 112 | DisableControlAction(0, 81, true) -- INPUT_VEH_PREV_RADIO (Mouse wheel down) 113 | -- DisableControlAction(0, 82, true) -- INPUT_VEH_SELECT_NEXT_WEAPON (Keyboard R) 114 | -- DisableControlAction(0, 83, true) -- INPUT_VEH_SELECT_PREV_WEAPON (Keyboard E) 115 | 116 | DisableControlAction(0, 14, true) 117 | DisableControlAction(0, 15, true) 118 | if IsDisabledControlJustReleased(0, 14) or IsControlJustReleased(0, 173) then 119 | Interact:scroll(menuData, true) 120 | -- Mouse Wheel Up / Arrow Up 121 | elseif IsDisabledControlJustReleased(0, 15) or IsControlJustReleased(0, 172) then 122 | Interact:scroll(menuData, false) 123 | end 124 | end 125 | 126 | function UserInputManager.defaultKeyHandler(menuData) 127 | -- E 128 | local padIndex = (menuData.indicator and menuData.indicator.keyPress) and menuData.indicator.keyPress.padIndex or 0 129 | local control = (menuData.indicator and menuData.indicator.keyPress) and menuData.indicator.keyPress.control or 38 130 | 131 | if menuData.indicator and menuData.indicator.hold then 132 | if IsControlPressed(padIndex, control) then 133 | local currentTime = GetGameTimer() 134 | if lastHoldTrigger then 135 | if (currentTime - lastHoldTrigger) >= 1000 then 136 | lastHoldTrigger = nil 137 | else 138 | return 139 | end 140 | end 141 | if not holdStart then 142 | holdStart = currentTime 143 | end 144 | local holdDuration = menuData.indicator.hold 145 | local elapsedTime = currentTime - holdStart 146 | local percentage = (elapsedTime / holdDuration) * 100 147 | Interact:fillIndicator(menuData, percentage) 148 | 149 | if elapsedTime >= holdDuration then 150 | holdStart = nil 151 | lastHoldTrigger = currentTime 152 | Container.keyPress(menuData) 153 | Interact:indicatorStatus(menuData, 'success') 154 | Interact:fillIndicator(menuData, 0) 155 | end 156 | else 157 | if holdStart then 158 | Util.print_debug("Player stopped holding the key early") 159 | Interact:indicatorStatus(menuData, 'fail') 160 | Interact:fillIndicator(menuData, 0) 161 | holdStart = nil 162 | end 163 | end 164 | else 165 | if not IsControlJustReleased(padIndex, control) then return end 166 | Container.keyPress(menuData) 167 | end 168 | end 169 | 170 | -- Instantiate UserInputManager 171 | local UserInputManager = UserInputManager:new() 172 | 173 | -- Register Commands for Mouse Wheel 174 | RegisterCommand('+interaction:wheel_up', function() 175 | UserInputManager:handleMouseWheel(false) 176 | end, false) 177 | 178 | RegisterCommand('+interaction:wheel_down', function() 179 | UserInputManager:handleMouseWheel(true) 180 | end, false) 181 | 182 | RegisterKeyMapping('+interaction:wheel_up', 'Interaction MouseWheel (up)', 'MOUSE_WHEEL', "IOM_WHEEL_UP") 183 | RegisterKeyMapping('+interaction:wheel_down', 'Interaction MouseWheel (down)', 'MOUSE_WHEEL', "IOM_WHEEL_DOWN") 184 | TriggerEvent('chat:removeSuggestion', '/+interaction:wheel_up') 185 | TriggerEvent('chat:removeSuggestion', '/+interaction:wheel_down') 186 | 187 | -- Register Commands for Key Hold 188 | if Config.controls.enforce then 189 | local controls = Config.controls.interact 190 | 191 | RegisterCommand('+interaction_interact', function() 192 | UserInputManager:startHoldDetection() 193 | end, false) 194 | 195 | RegisterCommand('-interaction_interact', function() 196 | UserInputManager:stopHoldDetection() 197 | end, false) 198 | 199 | RegisterKeyMapping('+interaction_interact', 'Trigger selected interaction option', controls.defaultMapper, 200 | controls.defaultParameter) 201 | TriggerEvent('chat:removeSuggestion', '/+interaction:interact') 202 | TriggerEvent('chat:removeSuggestion', '/-interaction:interact') 203 | end 204 | -------------------------------------------------------------------------------- /interactionMenu/lua/examples/chairs.lua: -------------------------------------------------------------------------------- 1 | -- _ 2 | -- | | 3 | -- _____ _| | _____ ___ _ __ 4 | -- / __\ \ /\ / / |/ / _ \/ _ \ '_ \ 5 | -- \__ \\ V V /| < __/ __/ |_) | 6 | -- |___/ \_/\_/ |_|\_\___|\___| .__/ 7 | -- | | 8 | -- |_| 9 | -- https://github.com/swkeep 10 | if not DEVMODE then return end 11 | local positions = { 12 | vector4(794.00, -2991.52, -70.0, 0), 13 | vector4(796.50, -2991.52, -70.0, 0), 14 | vector4(799.00, -2991.52, -70.0, 0), 15 | vector4(801.50, -2991.52, -70.0, 0), 16 | vector4(804.00, -2991.52, -70.0, 0), 17 | vector4(806.50, -2991.52, -70.0, 0), 18 | vector4(809.00, -2991.52, -70.0, 0), 19 | 20 | vector4(794.00, -2997.10, -70.00, 180), 21 | vector4(796.50, -2997.10, -70.00, 180), 22 | vector4(799.00, -2997.10, -70.00, 180), 23 | vector4(801.50, -2997.10, -70.00, 180), 24 | vector4(804.00, -2997.10, -70.00, 180), 25 | vector4(806.50, -2997.10, -70.00, 180), 26 | vector4(809.00, -2997.10, -70.00, 180), 27 | 28 | vector4(794.54, -3002.94, -70.00, 0), 29 | vector4(798.54, -3002.94, -70.00, 0), 30 | vector4(801.04, -3002.94, -70.00, 0), 31 | vector4(803.54, -3002.94, -70.00, 0), 32 | vector4(806.04, -3002.94, -70.00, 0), 33 | vector4(808.54, -3002.94, -70.00, 0), 34 | 35 | vector4(794.00, -3008.70, -70.00, 180), 36 | vector4(796.50, -3008.70, -70.00, 180), 37 | vector4(799.00, -3008.70, -70.00, 180), 38 | vector4(801.50, -3008.70, -70.00, 180), 39 | vector4(804.00, -3008.70, -70.00, 180), 40 | vector4(806.50, -3008.70, -70.00, 180), 41 | vector4(809.00, -3008.70, -70.00, 180), 42 | } 43 | 44 | local chairs = { 45 | "prop_table_02_chr", 46 | "prop_table_03_chr", 47 | "prop_table_03b_chr", 48 | "prop_table_04_chr", 49 | "apa_mp_h_stn_chairarm_01", 50 | "apa_mp_h_stn_chairarm_02", 51 | "apa_mp_h_stn_chairarm_03", 52 | "apa_mp_h_stn_chairarm_11", 53 | "apa_mp_h_stn_chairarm_12", 54 | "apa_mp_h_stn_chairarm_13", 55 | "apa_mp_h_stn_chairarm_23", 56 | "apa_mp_h_stn_chairarm_24", 57 | "apa_mp_h_stn_chairarm_25", 58 | "apa_mp_h_yacht_strip_chair_01", 59 | "bkr_prop_biker_boardchair01", 60 | "bkr_prop_biker_chair_01", 61 | "bkr_prop_clubhouse_armchair_01a", 62 | "bkr_prop_clubhouse_chair_01", 63 | "bkr_prop_clubhouse_offchair_01a", 64 | "bkr_prop_weed_chair_01a", 65 | "ex_mp_h_din_chair_04", 66 | "ex_mp_h_din_chair_08", 67 | "ex_mp_h_din_chair_09", 68 | "ex_mp_h_din_chair_12", 69 | "ex_prop_offchair_exec_01", 70 | "ex_prop_offchair_exec_02", 71 | "ex_prop_offchair_exec_03", 72 | "ex_prop_offchair_exec_04", 73 | "gr_prop_gr_chair02_ped", 74 | "gr_prop_gr_offchair_01a", 75 | "gr_prop_highendchair_gr_01a", 76 | "hei_heist_din_chair_01", 77 | "hei_heist_stn_chairarm_06", 78 | "hei_prop_hei_skid_chair", 79 | "imp_prop_impexp_offchair_01a", 80 | "p_armchair_01_s", 81 | "p_clb_officechair_s", 82 | "p_ilev_p_easychair_s", 83 | "p_soloffchair_s", 84 | "p_yacht_chair_01_s", 85 | "prop_armchair_01", 86 | "prop_chateau_chair_01", 87 | "prop_clown_chair", 88 | "prop_cs_office_chair", 89 | "prop_gc_chair02", 90 | "prop_old_deck_chair", 91 | "prop_old_wood_chair", 92 | "prop_old_wood_chair_lod", 93 | "prop_rock_chair_01", 94 | "prop_skid_chair_01", 95 | "prop_skid_chair_02", 96 | "prop_skid_chair_03", 97 | "prop_sol_chair", 98 | "prop_wheelchair_01", 99 | } 100 | local menus = {} 101 | local spawned_models = {} 102 | local entities = {} 103 | local sitting = false 104 | local current_chair = nil 105 | 106 | local function stand_up() 107 | if not sitting then return end 108 | 109 | local playerPed = PlayerPedId() 110 | ClearPedTasks(playerPed) 111 | TaskStartScenarioInPlace(playerPed, "WORLD_HUMAN_STAND_IDLE", 0, true) 112 | FreezeEntityPosition(current_chair, true) 113 | Wait(500) 114 | sitting = false 115 | current_chair = nil 116 | end 117 | 118 | local function sit(entity) 119 | if sitting then return end 120 | sitting = true 121 | current_chair = entity 122 | local playerPed = PlayerPedId() 123 | local playerCoords = GetEntityCoords(playerPed) 124 | local entityCoords = GetEntityCoords(entity) 125 | local heading = GetEntityHeading(entity) + 180.0 126 | 127 | FreezeEntityPosition(entity, true) 128 | TaskStartScenarioAtPosition(playerPed, "PROP_HUMAN_SEAT_BENCH", entityCoords.x, entityCoords.y, playerCoords.z - 0.5, heading, 0, true, true) 129 | end 130 | 131 | local function DrawText3D(x, y, z, text) 132 | local onScreen, _x, _y = World3dToScreen2d(x, y, z) 133 | if not onScreen then return end 134 | local camCoords = GetGameplayCamCoord() 135 | local scale = 150 / (GetGameplayCamFov() * #(camCoords - vec3(x, y, z))) 136 | SetTextScale(scale or 0.35, scale or 0.35) 137 | SetTextFont(4) 138 | SetTextEntry("STRING") 139 | SetTextCentre(true) 140 | 141 | AddTextComponentString(text) 142 | DrawText(_x, _y) 143 | end 144 | 145 | local function init() 146 | for _, value in ipairs(positions) do 147 | local index = #entities + 1 148 | local model = chairs[math.random(1, #chairs)] 149 | entities[index] = Util.spawnObject(joaat(model), value) 150 | spawned_models[index] = { model, value } 151 | 152 | menus[#menus + 1] = exports['interactionMenu']:Create { 153 | tracker = 'boundingBox', 154 | entity = entities[index], 155 | dimensions = { 156 | vec3(-1, -1.5, -0.3), 157 | vec3(1, 1.5, 2) 158 | }, 159 | options = { 160 | { 161 | label = 'Sit', 162 | action = function() 163 | sit(entities[index]) 164 | end, 165 | canInteract = function() 166 | return not sitting 167 | end 168 | }, 169 | { 170 | label = 'Stand Up', 171 | action = stand_up, 172 | canInteract = function() 173 | return sitting 174 | end 175 | }, 176 | } 177 | } 178 | Wait(30) 179 | end 180 | 181 | CreateThread(function() 182 | while #spawned_models > 0 do 183 | for _, value in ipairs(spawned_models) do 184 | DrawText3D(value[2].x, value[2].y, value[2].z + 0.5, value[1]) 185 | end 186 | Wait(0) 187 | end 188 | end) 189 | end 190 | 191 | local function cleanup() 192 | stand_up() 193 | 194 | for _, menu_id in ipairs(menus) do 195 | exports['interactionMenu']:remove(menu_id) 196 | end 197 | for _, entity in ipairs(entities) do 198 | if DoesEntityExist(entity) then 199 | DeleteEntity(entity) 200 | end 201 | end 202 | 203 | entities = {} 204 | menus = {} 205 | spawned_models = {} 206 | end 207 | 208 | CreateThread(function() 209 | InternalRegisterTest(init, cleanup, "chair_test", "Lots of Chairs", "fa-solid fa-chair") 210 | end) 211 | -------------------------------------------------------------------------------- /interactionMenu/lua/examples/empty.lua: -------------------------------------------------------------------------------- 1 | -- _ 2 | -- | | 3 | -- _____ _| | _____ ___ _ __ 4 | -- / __\ \ /\ / / |/ / _ \/ _ \ '_ \ 5 | -- \__ \\ V V /| < __/ __/ |_) | 6 | -- |___/ \_/\_/ |_|\_\___|\___| .__/ 7 | -- | | 8 | -- |_| 9 | -- https://github.com/swkeep 10 | 11 | local function init() 12 | 13 | end 14 | 15 | local function cleanup() 16 | 17 | end 18 | 19 | CreateThread(function() 20 | InternalRegisterTest(init, cleanup, "1", "! Empty", "fa-solid fa-warehouse") 21 | end) 22 | -------------------------------------------------------------------------------- /interactionMenu/lua/examples/entityZone.lua: -------------------------------------------------------------------------------- 1 | -- _ 2 | -- | | 3 | -- _____ _| | _____ ___ _ __ 4 | -- / __\ \ /\ / / |/ / _ \/ _ \ '_ \ 5 | -- \__ \\ V V /| < __/ __/ |_) | 6 | -- |___/ \_/\_/ |_|\_\___|\___| .__/ 7 | -- | | 8 | -- |_| 9 | -- https://github.com/swkeep 10 | local entity = nil 11 | local sit = false 12 | local menu_id = nil 13 | local entities = {} 14 | 15 | local function loadAnim(animation) 16 | RequestAnimDict(animation) 17 | while not HasAnimDictLoaded(animation) do 18 | Citizen.Wait(100) 19 | end 20 | return true 21 | end 22 | 23 | local function goThere(ped) 24 | local pos = vector3(815.74, -2991.26, -69.0) 25 | local intial_pos = GetEntityCoords(ped) 26 | 27 | TaskGoToCoordAnyMeans(ped, pos.x, pos.y, pos.z, 10.0, 0, 0, 0, 0) 28 | while true do 29 | local current_position = GetEntityCoords(ped) 30 | if #(current_position - pos) < 3 then 31 | break 32 | end 33 | 34 | Wait(100) 35 | end 36 | TaskGoToCoordAnyMeans(ped, intial_pos.x, intial_pos.y, intial_pos.z, 10.0, 0, 0, 0, 0) 37 | while true do 38 | local current_position = GetEntityCoords(ped) 39 | if #(current_position - intial_pos) < 3 then 40 | break 41 | end 42 | 43 | Wait(100) 44 | end 45 | end 46 | 47 | local function init() 48 | local max_health = 1000 49 | local start_pos = InternalGetTestSlot('front', 2) 50 | start_pos = vec4(start_pos.x, start_pos.y, start_pos.z - 0.5, start_pos.w) 51 | entity = Util.spawnPed(GetHashKey('u_m_y_juggernaut_01'), start_pos) 52 | SetEntityMaxHealth(entity, max_health) 53 | SetEntityHealth(entity, max_health) 54 | FreezeEntityPosition(entity, false) 55 | 56 | menu_id = exports['interactionMenu']:Create { 57 | tracker = 'boundingBox', 58 | dimensions = { 59 | vec3(-2, -2, -0.3), 60 | vec3(2, 2, 2) 61 | }, 62 | entity = entity, 63 | offset = vector3(0, 0, 0.4), 64 | options = { 65 | { 66 | icon = 'fas fa-heart', 67 | label = "Health", 68 | progress = { 69 | type = "info", 70 | value = 0, 71 | percent = true 72 | }, 73 | bind = function(_entity, distance, coords, name, bone) 74 | local max_hp = 100 - GetPedMaxHealth(entity) 75 | local current_hp = 100 - GetEntityHealth(entity) 76 | 77 | return math.floor((current_hp * 100) / max_hp) 78 | end 79 | }, 80 | { 81 | icon = 'fas fa-map-marker-alt', 82 | label = 'Sit/Stand', 83 | action = function(entity) 84 | -- local dict = 'creatures@retriever@amb@world_dog_sitting@idle_a' 85 | -- local anim = 'idle_c' 86 | local dict = 'anim@amb@business@bgen@bgen_no_work@' 87 | local anim = 'sit_phone_phoneputdown_idle_nowork' 88 | 89 | if not sit then 90 | loadAnim(dict) 91 | local flag = 0 92 | TaskPlayAnim(entity, dict, anim, 8.0, 0, -1, flag or 1, 0, 0, 0, 0) 93 | sit = true 94 | else 95 | StopAnimTask(entity, dict, anim, 1.0) 96 | sit = false 97 | end 98 | end 99 | }, 100 | { 101 | icon = 'fas fa-map-marker-alt', 102 | label = 'Move', 103 | action = function(entity) 104 | goThere(entity) 105 | end 106 | }, 107 | { 108 | label = "Building Search (Police)", 109 | icon = 'fas fa-search', 110 | job = { 111 | 'police', 112 | 'k9' 113 | }, 114 | action = function(entity) 115 | 116 | end 117 | }, 118 | { 119 | label = "Apprehension (Police)", 120 | icon = 'fas fa-handcuffs', 121 | job = { 122 | ['police'] = {}, 123 | 'k9' 124 | }, 125 | action = function(entity) 126 | 127 | end 128 | } 129 | } 130 | } 131 | end 132 | 133 | local function cleanup() 134 | DeleteEntity(entity) 135 | if not menu_id then return end 136 | exports['interactionMenu']:remove(menu_id) 137 | 138 | for index, entry in ipairs(entities) do 139 | DeleteEntity(entry.entity) 140 | end 141 | 142 | entities = {} 143 | menu_id = nil 144 | end 145 | 146 | CreateThread(function() 147 | InternalRegisterTest(init, cleanup, "entity_zone_test", "x Entity Zone", "fa-solid fa-cube") 148 | end) 149 | -------------------------------------------------------------------------------- /interactionMenu/lua/examples/garage.lua: -------------------------------------------------------------------------------- 1 | -- _ 2 | -- | | 3 | -- _____ _| | _____ ___ _ __ 4 | -- / __\ \ /\ / / |/ / _ \/ _ \ '_ \ 5 | -- \__ \\ V V /| < __/ __/ |_) | 6 | -- |___/ \_/\_/ |_|\_\___|\___| .__/ 7 | -- | | 8 | -- |_| 9 | -- https://github.com/swkeep 10 | local spawn = vector4(795.25, -3002.85, -69.41, 269.61) 11 | local out = vector4(781.83, -2973.24, 5.39, 248.68) 12 | local gate = vector3(816.53, -3001.47, -69.0) 13 | 14 | local positions = { 15 | vector4(794.00, -2997.10, -70.00, -140), 16 | -- vector4(796.50, -2997.10, -70.00, 0), 17 | vector4(799.00, -2997.10, -70.00, -140), 18 | -- vector4(801.50, -2997.10, -70.00, 0), 19 | vector4(804.00, -2997.10, -70.00, -140), 20 | -- vector4(807.00, -2997.10, -70.00, 0), 21 | vector4(809.00, -2997.10, -70.00, -140), 22 | 23 | vector4(794.00, -3008.70, -70.00, -40), 24 | -- vector4(796.50, -3008.70, -70.00, 140), 25 | vector4(799.00, -3008.70, -70.00, -40), 26 | -- vector4(801.50, -3008.70, -70.00, 140), 27 | vector4(804.00, -3008.70, -70.00, -40), 28 | -- vector4(806.50, -3008.70, -70.00, 140), 29 | vector4(809.00, -3008.70, -70.00, -40), 30 | } 31 | 32 | local vehicle_models = { "adder", "zentorno", "t20", "infernus", "banshee", "turismor", "entityxf", "cheetah", "osiris" } 33 | local entities = {} 34 | local menus = {} 35 | 36 | local function handleVehicleAction(index, position, target) 37 | DoScreenFadeOut(500) 38 | Wait(500) 39 | local playerped = PlayerPedId() 40 | TaskWarpPedIntoVehicle(playerped, entities[index], -1) 41 | SetEntityCoordsNoOffset(entities[index], target.x, target.y, target.z, false, false, false) 42 | SetEntityHeading(entities[index], target.w) 43 | FreezeEntityPosition(entities[index], false) 44 | DoScreenFadeIn(500) 45 | end 46 | 47 | local function returnVehicle(index, position) 48 | DoScreenFadeOut(500) 49 | Wait(500) 50 | local playerped = PlayerPedId() 51 | local pos = GetEntityCoords(playerped) 52 | SetEntityCoordsNoOffset(playerped, pos.x, pos.y, pos.z, false, false, false) 53 | SetEntityHeading(playerped, 0) 54 | SetEntityCoordsNoOffset(entities[index], position.x, position.y, position.z, false, false, false) 55 | SetEntityHeading(entities[index], position.w) 56 | DoScreenFadeIn(500) 57 | end 58 | 59 | local function canInteractAtDistance(index, position, distanceCheck) 60 | local pos = GetEntityCoords(entities[index]) 61 | local distance = #(pos - vec3(position.x, position.y, position.z)) 62 | return distanceCheck == "less" and distance < 2 or distanceCheck == "greater" and distance > 2 63 | end 64 | 65 | local function createMenu(index, position) 66 | return exports['interactionMenu']:Create { 67 | entity = entities[index], 68 | options = { 69 | { 70 | icon = "fa-solid fa-car", 71 | label = "Enter", 72 | canInteract = function() 73 | return canInteractAtDistance(index, position, "less") 74 | end, 75 | action = function() 76 | handleVehicleAction(index, position, spawn) 77 | end 78 | }, 79 | { 80 | icon = "fa-solid fa-sign-out-alt", 81 | label = "Take Out", 82 | canInteract = function() 83 | return canInteractAtDistance(index, position, "less") 84 | end, 85 | action = function() 86 | handleVehicleAction(index, position, out) 87 | end 88 | }, 89 | { 90 | icon = "fa-solid fa-arrow-alt-circle-left", 91 | label = "Return", 92 | canInteract = function() 93 | return canInteractAtDistance(index, position, "greater") 94 | end, 95 | action = function() 96 | returnVehicle(index, position) 97 | end 98 | }, 99 | } 100 | } 101 | end 102 | 103 | local function init() 104 | for _, position in ipairs(positions) do 105 | local index = #entities + 1 106 | local randomVehicle = vehicle_models[math.random(#vehicle_models)] 107 | entities[index] = Util.spawnVehicle(randomVehicle, position) 108 | FreezeEntityPosition(entities[index], true) 109 | 110 | menus[#menus + 1] = createMenu(index, position) 111 | Wait(100) 112 | end 113 | end 114 | 115 | local function cleanup() 116 | for _, menu_id in ipairs(menus) do 117 | exports['interactionMenu']:remove(menu_id) 118 | end 119 | for _, entity in ipairs(entities) do 120 | if DoesEntityExist(entity) then 121 | DeleteEntity(entity) 122 | end 123 | end 124 | menus = {} 125 | entities = {} 126 | end 127 | 128 | CreateThread(function() 129 | InternalRegisterTest(init, cleanup, "garage_example", "Garage", "fa-solid fa-warehouse") 130 | end) 131 | -------------------------------------------------------------------------------- /interactionMenu/lua/examples/globals.lua: -------------------------------------------------------------------------------- 1 | -- _ 2 | -- | | 3 | -- _____ _| | _____ ___ _ __ 4 | -- / __\ \ /\ / / |/ / _ \/ _ \ '_ \ 5 | -- \__ \\ V V /| < __/ __/ |_) | 6 | -- |___/ \_/\_/ |_|\_\___|\___| .__/ 7 | -- | | 8 | -- |_| 9 | -- https://github.com/swkeep 10 | if not DEVMODE then return end 11 | local menus = {} 12 | 13 | local function create_debug_global(type, label, icon, action) 14 | menus[#menus + 1] = exports['interactionMenu']:createGlobal { 15 | type = type, 16 | offset = vec3(0, 0, 0), 17 | maxDistance = 1.0, 18 | options = { 19 | { 20 | label = label, 21 | icon = icon, 22 | action = action 23 | } 24 | } 25 | } 26 | end 27 | 28 | local function init() 29 | create_debug_global('entities', '[Debug] On All Entities', 'fa fa-bug', function(entity) 30 | print(entity) 31 | end) 32 | create_debug_global('peds', '[Debug] On All Peds', 'fa fa-person', function(entity) 33 | print(entity) 34 | end) 35 | create_debug_global('vehicles', '[Debug] On All Vehicles', 'fa fa-car', function(entity) 36 | print(entity) 37 | end) 38 | create_debug_global('players', '[Debug] On All Players', 'fa fa-person', function(entity) 39 | print(entity) 40 | end) 41 | 42 | menus[#menus + 1] = exports['interactionMenu']:createGlobal { 43 | type = 'bones', 44 | bone = 'platelight', 45 | options = { 46 | { 47 | label = '[Debug] On All plates', 48 | icon = 'fa fa-rectangle-ad', 49 | action = function(entity) 50 | print('Plate:', GetVehicleNumberPlateText(entity)) 51 | end 52 | } 53 | } 54 | } 55 | 56 | menus[#menus + 1] = exports['interactionMenu']:createGlobal { 57 | type = 'zones', 58 | options = { 59 | { 60 | label = '[Debug] On All Zones', 61 | icon = 'fa fa-person', 62 | action = function(data) 63 | Util.print_table(data) 64 | end 65 | } 66 | } 67 | } 68 | end 69 | 70 | local function cleanup() 71 | for index, menu_id in ipairs(menus) do 72 | exports['interactionMenu']:remove(menu_id) 73 | end 74 | menus = {} 75 | end 76 | 77 | CreateThread(function() 78 | InternalRegisterGlobalTest(init, cleanup, "global_menus", "Toggle Global Menus") 79 | end) 80 | -------------------------------------------------------------------------------- /interactionMenu/lua/examples/holdIndicator.lua: -------------------------------------------------------------------------------- 1 | -- _ 2 | -- | | 3 | -- _____ _| | _____ ___ _ __ 4 | -- / __\ \ /\ / / |/ / _ \/ _ \ '_ \ 5 | -- \__ \\ V V /| < __/ __/ |_) | 6 | -- |___/ \_/\_/ |_|\_\___|\___| .__/ 7 | -- | | 8 | -- |_| 9 | -- https://github.com/swkeep 10 | local menu_id = nil 11 | local position = vector4(794.48, -3002.87, -69.41, 90.68) 12 | 13 | local function init() 14 | menu_id = exports['interactionMenu']:Create { 15 | rotation = vector3(-40, 0, 270), 16 | position = vector4(795.0, -3002.15, -69.41, 90.68), 17 | scale = 1, 18 | -- theme = 'box', 19 | indicator = { 20 | prompt = 'Hold "E"', 21 | hold = 1000, 22 | }, 23 | zone = { 24 | type = 'boxZone', 25 | position = position, 26 | heading = position.w, 27 | width = 2.0, 28 | length = 2.0, 29 | debugPoly = Config.debugPoly, 30 | minZ = position.z - 1, 31 | maxZ = position.z + 2, 32 | }, 33 | options = { 34 | { 35 | label = 'Back', 36 | icon = 'fa fa-arrow-left', 37 | action = function() 38 | end 39 | }, 40 | { 41 | label = 'Stop', 42 | icon = 'fa fa-stop', 43 | action = function() 44 | end 45 | } 46 | } 47 | } 48 | end 49 | 50 | local function cleanup() 51 | if not menu_id then return end 52 | exports['interactionMenu']:remove(menu_id) 53 | end 54 | 55 | CreateThread(function() 56 | InternalRegisterTest(init, cleanup, "hold_indicator", "Hold Indicator", "fa-solid fa-stopwatch-20") 57 | end) 58 | -------------------------------------------------------------------------------- /interactionMenu/lua/examples/instance.lua: -------------------------------------------------------------------------------- 1 | -- _ 2 | -- | | 3 | -- _____ _| | _____ ___ _ __ 4 | -- / __\ \ /\ / / |/ / _ \/ _ \ '_ \ 5 | -- \__ \\ V V /| < __/ __/ |_) | 6 | -- |___/ \_/\_/ |_|\_\___|\___| .__/ 7 | -- | | 8 | -- |_| 9 | -- https://github.com/swkeep 10 | local selected = 1 11 | local refs = {} -- rooms 12 | local refs2 = {} -- these are globals and effect all test rooms 13 | local status = "" 14 | local active_emoji = '|%s' 15 | local disable_emoji = '|%s' 16 | local emoji_numbers = { 17 | "1️⃣", 18 | "2️⃣", 19 | "3️⃣", 20 | "4️⃣", 21 | "5️⃣", 22 | "6️⃣", 23 | "7️⃣", 24 | "8️⃣", 25 | "9️⃣" 26 | } 27 | local test_slots = { 28 | front = { 29 | vector4(795.23, -3008.43, -69.41, 89.31), 30 | vector4(795.2, -3002.94, -69.41, 89.9), 31 | vector4(795.17, -2996.87, -69.41, 89.18) 32 | }, 33 | middle = { 34 | vector4(800.21, -3008.53, -69.41, 90.93), 35 | vector4(800.93, -3003.08, -69.41, 89.2), 36 | vector4(800.55, -2996.81, -69.41, 90.42) 37 | }, 38 | back = { 39 | vector4(806.36, -3008.67, -69.41, 90.76), 40 | vector4(806.75, -3002.84, -69.41, 90.15), 41 | vector4(806.96, -2997.04, -69.41, 89.18), 42 | } 43 | } 44 | 45 | local function execute_callback(fn) 46 | if type(fn) ~= "function" then return false end 47 | CreateThread(fn) 48 | end 49 | 50 | local function load() 51 | if not selected then return false end 52 | execute_callback(refs[selected].init) 53 | end 54 | 55 | local function unload() 56 | if not selected then return false end 57 | execute_callback(refs[selected].cleanup) 58 | end 59 | 60 | local function setStatus() 61 | status = '' 62 | for index, value in pairs(refs2) do 63 | if value.active then 64 | status = status .. active_emoji:format(emoji_numbers[index]) 65 | else 66 | status = status .. disable_emoji:format(emoji_numbers[index]) 67 | end 68 | end 69 | status = status .. "|" 70 | end 71 | 72 | function InternalGetTestSlot(name, index) 73 | return test_slots[name][index] 74 | end 75 | 76 | function InternalRegisterTest(init_func, cleanup_func, name, desb, icon) 77 | refs[#refs + 1] = { 78 | name = name, 79 | desb = desb, 80 | init = init_func, 81 | cleanup = cleanup_func, 82 | icon = icon 83 | } 84 | end 85 | 86 | function InternalRegisterGlobalTest(init_func, cleanup_func, name, desb, icon) 87 | refs2[#refs2 + 1] = { 88 | name = name, 89 | desb = desb, 90 | init = init_func, 91 | cleanup = cleanup_func, 92 | icon = icon 93 | } 94 | end 95 | 96 | CreateThread(function() 97 | Wait(250) 98 | 99 | local options = { 100 | { 101 | label = 'Active Test: [####] ', 102 | dynamic = true, 103 | bind = function() 104 | return ('Active Test [%s]'):format(refs[selected].desb) 105 | end 106 | } 107 | } 108 | 109 | table.sort(refs, function(a, b) 110 | return a.desb < b.desb 111 | end) 112 | 113 | for index, menu in pairs(refs) do 114 | options[#options + 1] = { 115 | label = ("[%02d] %s"):format(index, menu.desb), 116 | icon = menu.icon, 117 | action = function(data) 118 | if selected == index then 119 | warn('already selected') 120 | return 121 | end 122 | unload() 123 | selected = index 124 | load() 125 | end 126 | } 127 | end 128 | 129 | if selected then 130 | load() 131 | end 132 | 133 | local options2 = { 134 | { 135 | label = "", 136 | bind = function() 137 | return status 138 | end 139 | }, 140 | } 141 | 142 | for index, ref in pairs(refs2) do 143 | options2[#options2 + 1] = { 144 | label = ref.desb, 145 | icon = ref.icon, 146 | action = function(data) 147 | if not ref.active then 148 | ref.active = true 149 | execute_callback(ref.init) 150 | else 151 | ref.active = false 152 | execute_callback(ref.cleanup) 153 | end 154 | setStatus() 155 | exports['interactionMenu']:refresh() 156 | end 157 | } 158 | end 159 | 160 | setStatus() 161 | 162 | exports['interactionMenu']:paginatedMenu { 163 | itemsPerPage = 10, 164 | offset = vector3(0, 0, 0), 165 | rotation = vector3(-20, 0, -90), 166 | position = vector4(785.6, -2999.2, -68.5, 271.65), 167 | scale = 1, 168 | width = "100%", 169 | zone = { 170 | type = 'sphere', 171 | position = vector3(784.54, -2999.8, -69.0), 172 | radius = 1.25, 173 | useZ = true, 174 | debugPoly = Config.debugPoly 175 | }, 176 | suppressGlobals = true, 177 | options = options 178 | } 179 | 180 | exports['interactionMenu']:paginatedMenu { 181 | itemsPerPage = 10, 182 | offset = vector3(0, 0, 0), 183 | rotation = vector3(-20, 0, -90), 184 | position = vector3(785.5, -2996.2, -69.0), 185 | scale = 1, 186 | width = "100%", 187 | zone = { 188 | type = 'sphere', 189 | position = vector3(784.54, -2996.85, -69.0), 190 | radius = 1.25, 191 | useZ = true, 192 | debugPoly = Config.debugPoly 193 | }, 194 | suppressGlobals = true, 195 | options = options2 196 | } 197 | 198 | AddEventHandler('onResourceStop', function(resource) 199 | if resource ~= GetCurrentResourceName() then return end 200 | if not selected then return false end 201 | local ref = refs[selected] 202 | ref.cleanup() 203 | end) 204 | end) 205 | -------------------------------------------------------------------------------- /interactionMenu/lua/examples/nestedMenu.lua: -------------------------------------------------------------------------------- 1 | -- _ 2 | -- | | 3 | -- _____ _| | _____ ___ _ __ 4 | -- / __\ \ /\ / / |/ / _ \/ _ \ '_ \ 5 | -- \__ \\ V V /| < __/ __/ |_) | 6 | -- |___/ \_/\_/ |_|\_\___|\___| .__/ 7 | -- | | 8 | -- |_| 9 | -- https://github.com/swkeep 10 | local positions = { 11 | vector4(794.00, -2997.10, -69.00, 0), 12 | vector4(799.00, -2997.10, -69.00, 0), 13 | vector4(806.50, -2997.10, -69.00, 0), 14 | 15 | vector4(794.54, -3002.94, -69.00, 180), 16 | vector4(801.04, -3002.94, -69.00, 180), 17 | vector4(806.04, -3002.94, -69.00, 180), 18 | vector4(808.54, -3002.94, -69.00, 180), 19 | 20 | vector4(794.00, -3008.70, -69.00, 180), 21 | vector4(799.00, -3008.70, -69.00, 180), 22 | vector4(804.00, -3008.70, -69.00, 180), 23 | vector4(809.00, -3008.70, -69.00, 180), 24 | } 25 | local menus = {} 26 | 27 | local function init() 28 | local options = { 29 | { 30 | label = "Title", 31 | }, 32 | { 33 | label = "Bind (Root/1)", 34 | bind = function() 35 | return "Random Number: " .. math.random(0, 100) 36 | end 37 | }, 38 | { 39 | label = "Option 1 (Root/1)", 40 | icon = "fa fa-cogs", 41 | action = function() 42 | print("Option 1 action (Root/1)") 43 | end 44 | }, 45 | { 46 | label = "Option 2 (Root/2)", 47 | icon = "fa fa-play", 48 | action = function() 49 | print("Option 2 action (Root/2)") 50 | end, 51 | subMenu = { 52 | { 53 | video = { 54 | url = 'https://cdn.swkeep.com/interaction_menu_internal_tests/test_video.mp4', 55 | volume = 0, 56 | currentTime = 100, 57 | progress = true, 58 | autoplay = true, 59 | loop = true, 60 | -- percent = true, 61 | timecycle = true, 62 | } 63 | }, 64 | { 65 | label = "SubOption 1 (Root/2/1)", 66 | action = function() 67 | print("SubOption 1 action (Root/2/1)") 68 | end 69 | }, 70 | { 71 | label = "SubOption 2 (Root/2/2)", 72 | action = function() 73 | print("SubOption 2 action (Root/2/2)") 74 | end, 75 | subMenu = { 76 | { 77 | picture = { 78 | url = 'https://cdn.swkeep.com/interaction_menu/preview_1.jpg' 79 | } 80 | }, 81 | { 82 | label = "SubSubOption 1 (Root/2/2/1)", 83 | action = function() 84 | print("SubSubOption 1 action (Root/2/2/1)") 85 | end 86 | }, 87 | { 88 | label = "SubSubOption 2 (Root/2/2/2)", 89 | action = function() 90 | print("SubSubOption 2 action (Root/2/2/2)") 91 | end 92 | }, 93 | { 94 | label = "SubSubOption 3 (Root/2/2/3)", 95 | action = function() 96 | print("SubSubOption 3 action (Root/2/2/3)") 97 | end, 98 | subMenu = { 99 | { 100 | label = "Health", 101 | icon = "fas fa-heartbeat", 102 | progress = { 103 | type = "error", 104 | value = 50, 105 | percent = true 106 | } 107 | }, 108 | { 109 | label = "DeepOption 1 (Root/2/2/3/1)", 110 | action = function() 111 | print("DeepOption 1 action (Root/2/2/3/1)") 112 | end 113 | }, 114 | { 115 | label = "DeepOption 2 (Root/2/2/3/2)", 116 | action = function() 117 | print("DeepOption 2 action (Root/2/2/3/2)") 118 | end 119 | } 120 | } 121 | } 122 | } 123 | }, 124 | { 125 | label = "SubOption 3 (Root/2/3)", 126 | action = function() 127 | print("SubOption 3 action (Root/2/3)") 128 | end 129 | } 130 | } 131 | }, 132 | { 133 | label = "Option 3 (Root/3)", 134 | icon = "fa fa-info-circle", 135 | action = function() 136 | print("Option 3 action (Root/3)") 137 | end 138 | } 139 | } 140 | 141 | for menu_index = 1, #positions, 1 do 142 | local pos = positions[menu_index] 143 | local rot = vector3(-20, 0, -90) 144 | 145 | options[1].label = "Menu: " .. menu_index 146 | 147 | menus[#menus + 1] = exports["interactionMenu"]:nestedMenu({ 148 | position = pos, 149 | rotation = rot, 150 | scale = 1, 151 | width = '80%', 152 | zone = { 153 | type = "sphere", 154 | position = pos, 155 | radius = 1.5, 156 | useZ = true, 157 | debugPoly = Config.debugPoly 158 | }, 159 | options = options 160 | }) 161 | end 162 | end 163 | 164 | local function cleanup() 165 | for _, menu_id in ipairs(menus) do 166 | exports['interactionMenu']:remove(menu_id) 167 | end 168 | 169 | menus = {} 170 | end 171 | 172 | CreateThread(function() 173 | InternalRegisterTest(init, cleanup, "nested_menu", "Nested Menu", "fa-solid fa-network-wired") 174 | end) 175 | -------------------------------------------------------------------------------- /interactionMenu/lua/examples/onBones.lua: -------------------------------------------------------------------------------- 1 | -- _ 2 | -- | | 3 | -- _____ _| | _____ ___ _ __ 4 | -- / __\ \ /\ / / |/ / _ \/ _ \ '_ \ 5 | -- \__ \\ V V /| < __/ __/ |_) | 6 | -- |___/ \_/\_/ |_|\_\___|\___| .__/ 7 | -- | | 8 | -- |_| 9 | -- https://github.com/swkeep 10 | -- https://docs.fivem.net/natives/?_0xFB71170B7E76ACBA 11 | -- Bone indices -> pastebin.com/D7JMnX1g 12 | 13 | if not DEVMODE then return end 14 | local positions = { 15 | vector4(794.00, -2997.10, -70.00, 0), 16 | vector4(796.50, -2997.10, -70.00, 0), 17 | vector4(799.00, -2997.10, -70.00, 0), 18 | vector4(801.50, -2997.10, -70.00, 0), 19 | vector4(804.00, -2997.10, -70.00, 0), 20 | vector4(807.00, -2997.10, -70.00, 0), 21 | 22 | vector4(794.54, -3002.94, -70.00, 180), 23 | vector4(798.54, -3002.94, -70.00, 180), 24 | vector4(801.04, -3002.94, -70.00, 180), 25 | vector4(803.54, -3002.94, -70.00, 180), 26 | vector4(806.04, -3002.94, -70.00, 180), 27 | vector4(808.54, -3002.94, -70.00, 180), 28 | } 29 | 30 | local function toggle_door(vehicle, doorId) 31 | if GetVehicleDoorAngleRatio(vehicle, doorId) > 0.0 then 32 | SetVehicleDoorShut(vehicle, doorId, false) 33 | else 34 | SetVehicleDoorOpen(vehicle, doorId, false, false) 35 | end 36 | Wait(650) 37 | end 38 | 39 | local function createWheelAction(wheelIndex) 40 | return function(entity) 41 | SetVehicleTyreBurst(entity, wheelIndex, true, 1000.0) 42 | end 43 | end 44 | 45 | local bones = { 46 | platelight = { 47 | { 48 | label = "Switch Plate", 49 | icon = "fas fa-toggle-on", 50 | action = function(entity) 51 | Wait(1000) 52 | SetVehicleNumberPlateText(entity, 'swkeep' .. math.random(0, 9)) 53 | Wait(500) 54 | end 55 | }, 56 | }, 57 | 58 | wheel_lf = { 59 | { 60 | label = "Front Left Wheel (Remove)", 61 | icon = "fas fa-truck-monster", 62 | action = createWheelAction(0), 63 | }, 64 | }, 65 | wheel_rf = { 66 | { 67 | label = "Front Right Wheel (Remove)", 68 | icon = "fas fa-truck-monster", 69 | action = createWheelAction(1), 70 | }, 71 | }, 72 | wheel_lr = { 73 | { 74 | label = "Rear Left Wheel (Remove)", 75 | icon = "fas fa-truck-monster", 76 | action = createWheelAction(4), 77 | } 78 | }, 79 | wheel_rr = { 80 | { 81 | label = "Rear Right Wheel (Remove)", 82 | icon = "fas fa-truck-monster", 83 | action = createWheelAction(5), 84 | } 85 | }, 86 | 87 | seat_dside_f = { 88 | { 89 | label = "Driver Seat", 90 | icon = "fas fa-user", 91 | action = function(e) 92 | end 93 | }, 94 | { 95 | label = "Passenger Seat", 96 | icon = "fas fa-user", 97 | action = function(entity) 98 | TaskEnterVehicle(PlayerPedId(), entity, 2000, 0, 2.0, 1, 0) 99 | end 100 | }, 101 | }, 102 | 103 | seat_pside_f = { 104 | { 105 | label = "Passenger Seat", 106 | icon = "fas fa-user", 107 | action = function(entity) 108 | TaskEnterVehicle(PlayerPedId(), entity, 2000, 0, 2.0, 1, 0) 109 | end 110 | }, 111 | { 112 | label = "Driver Seat", 113 | icon = "fas fa-user", 114 | action = function(entity) 115 | TaskEnterVehicle(PlayerPedId(), entity, 2000, -1, 1.0, 16, 0) 116 | end 117 | }, 118 | }, 119 | 120 | door_dside_f = { 121 | { 122 | label = "Front Left Door", 123 | icon = "fas fa-door-open", 124 | action = function(entity) 125 | toggle_door(entity, 0) 126 | end 127 | }, 128 | }, 129 | 130 | window_lf = { 131 | { 132 | label = "Front Left Window", 133 | icon = "fas fa-window-close", 134 | action = function(entity) 135 | toggle_door(entity, 0) 136 | end 137 | } 138 | }, 139 | 140 | 141 | engine = { 142 | { 143 | label = "Engine Health", 144 | icon = "fas fa-heartbeat", 145 | progress = { 146 | type = "info", 147 | value = 30, 148 | percent = true 149 | }, 150 | bind = function(entity) 151 | return GetVehicleEngineHealth(entity) / 10 152 | end 153 | }, 154 | { 155 | label = "Body Health", 156 | icon = "fas fa-hard-hat", 157 | progress = { 158 | type = "info", 159 | value = 70 160 | }, 161 | bind = function(entity) 162 | return GetVehicleBodyHealth(entity) / 10 163 | end 164 | }, 165 | { 166 | label = "Engine Status", 167 | icon = "fas fa-cogs", 168 | action = function(entity) 169 | SetVehicleEngineOn(entity, true, false, false) 170 | end 171 | } 172 | }, 173 | 174 | bonnet = { 175 | { 176 | label = "Hood", 177 | icon = "fas fa-car", 178 | action = function(entity) 179 | toggle_door(entity, 4) 180 | end 181 | } 182 | }, 183 | 184 | exhaust = { 185 | { 186 | label = "Exhaust", 187 | icon = "fas fa-smog", 188 | action = function() 189 | end 190 | } 191 | }, 192 | 193 | boot = { 194 | { 195 | label = "Trunk", 196 | action = function(entity) 197 | toggle_door(entity, 5) 198 | end 199 | } 200 | }, 201 | } 202 | 203 | local menus = {} 204 | local entities = {} 205 | 206 | local function init() 207 | entities[#entities + 1] = Util.spawnVehicle('adder', positions[1]) 208 | entities[#entities + 1] = Util.spawnVehicle('adder', positions[2]) 209 | entities[#entities + 1] = Util.spawnVehicle('adder', positions[7]) 210 | entities[#entities + 1] = Util.spawnVehicle('adder', positions[8]) 211 | entities[#entities + 1] = Util.spawnVehicle('adder', positions[9]) 212 | 213 | for index, vehicle in ipairs(entities) do 214 | SetVehicleNumberPlateText(vehicle, 'swkeep-' .. index) 215 | 216 | for boneName, bone in pairs(bones) do 217 | menus[#menus + 1] = exports['interactionMenu']:Create { 218 | bone = boneName, 219 | vehicle = vehicle, 220 | offset = vec3(0, 0, 0), 221 | maxDistance = 2.0, 222 | indicator = { 223 | prompt = 'E', 224 | }, 225 | options = bone or {} 226 | } 227 | end 228 | end 229 | 230 | local index = #entities + 1 231 | entities[index] = Util.spawnVehicle('adder', positions[3]) 232 | SetVehicleNumberPlateText(entities[index], 'no menu') 233 | end 234 | 235 | local function cleanup() 236 | for index, menu_id in ipairs(menus) do 237 | exports['interactionMenu']:remove(menu_id) 238 | end 239 | for index, entity in ipairs(entities) do 240 | if DoesEntityExist(entity) then 241 | DeleteEntity(entity) 242 | end 243 | end 244 | menus = {} 245 | entities = {} 246 | end 247 | 248 | CreateThread(function() 249 | InternalRegisterTest(init, cleanup, "on_bones", "On Vehicle Boens", "fa-solid fa-border-none") 250 | end) 251 | -------------------------------------------------------------------------------- /interactionMenu/lua/examples/onModel.lua: -------------------------------------------------------------------------------- 1 | -- _ 2 | -- | | 3 | -- _____ _| | _____ ___ _ __ 4 | -- / __\ \ /\ / / |/ / _ \/ _ \ '_ \ 5 | -- \__ \\ V V /| < __/ __/ |_) | 6 | -- |___/ \_/\_/ |_|\_\___|\___| .__/ 7 | -- | | 8 | -- |_| 9 | -- https://github.com/swkeep 10 | if not DEVMODE then return end 11 | local positions = { 12 | vector4(794.00, -2997.10, -70.00, 0), 13 | vector4(796.50, -2997.10, -70.00, 0), 14 | vector4(799.00, -2997.10, -70.00, 0), 15 | vector4(801.50, -2997.10, -70.00, 0), 16 | vector4(804.00, -2997.10, -70.00, 0), 17 | vector4(806.50, -2997.10, -70.00, 0), 18 | vector4(809.00, -2997.10, -70.00, 0), 19 | 20 | vector4(794.54, -3002.94, -70.00, 180), 21 | vector4(796.54, -3002.94, -70.00, 180), 22 | vector4(798.54, -3002.94, -70.00, 180), 23 | vector4(801.04, -3002.94, -70.00, 180), 24 | vector4(803.54, -3002.94, -70.00, 180), 25 | vector4(806.04, -3002.94, -70.00, 180), 26 | vector4(808.54, -3002.94, -70.00, 180), 27 | 28 | vector4(794.00, -3008.70, -70.00, 180), 29 | vector4(796.50, -3008.70, -70.00, 180), 30 | vector4(799.00, -3008.70, -70.00, 180), 31 | vector4(801.50, -3008.70, -70.00, 180), 32 | vector4(804.00, -3008.70, -70.00, 180), 33 | vector4(806.50, -3008.70, -70.00, 180), 34 | vector4(809.00, -3008.70, -70.00, 180), 35 | } 36 | 37 | local menus = {} 38 | local entities = {} 39 | 40 | local function init() 41 | local objects = { 42 | { model = `xm_prop_crates_sam_01a`, start = 1, finish = 7 }, 43 | { model = `prop_vend_snak_01`, start = 8, finish = 14 }, 44 | { model = `prop_paper_bag_01`, start = 15, finish = 19 } 45 | } 46 | 47 | for _, object in ipairs(objects) do 48 | for i = object.start, object.finish do 49 | entities[#entities + 1] = Util.spawnObject(object.model, positions[i]) 50 | end 51 | end 52 | 53 | menus[#menus + 1] = exports['interactionMenu']:Create { 54 | type = 'model', 55 | model = `xm_prop_crates_sam_01a`, 56 | offset = vec3(0, 0, 0.5), 57 | maxDistance = 2.0, 58 | options = { 59 | { 60 | label = "Open", 61 | action = function(e) 62 | 63 | end 64 | }, 65 | { 66 | icon = "fas fa-spinner", 67 | label = "Spinner", 68 | action = function(e) 69 | 70 | end 71 | }, 72 | { 73 | label = "rgb(50,100,50)", 74 | style = { 75 | color = { 76 | label = 'rgb(50,255,50)', 77 | } 78 | } 79 | }, 80 | { 81 | label = "rgb(250,0,50)", 82 | style = { 83 | color = { 84 | label = 'rgb(250,0,50)', 85 | } 86 | } 87 | }, 88 | { 89 | icon = "fas fa-spinner", 90 | label = "rgb(0,0,250)", 91 | style = { 92 | color = { 93 | background = 'wheat', 94 | label = 'rgb(0,0,250)', 95 | } 96 | }, 97 | action = function(e) 98 | 99 | end 100 | } 101 | } 102 | } 103 | 104 | menus[#menus + 1] = exports['interactionMenu']:Create { 105 | type = 'model', 106 | model = `prop_vend_snak_01`, 107 | offset = vec3(0, 0, 0), 108 | maxDistance = 2.0, 109 | extra = { 110 | job = { 111 | ['police'] = { 1, 3, 2 } 112 | }, 113 | }, 114 | options = { 115 | { 116 | label = 'Job Access: Police', 117 | icon = 'fa fa-handcuffs', 118 | action = function() 119 | 120 | end 121 | } 122 | } 123 | } 124 | 125 | menus[#menus + 1] = exports['interactionMenu']:Create { 126 | type = 'model', 127 | model = `prop_vend_snak_01`, 128 | offset = vec3(0, 0, 0), 129 | maxDistance = 2.0, 130 | options = { 131 | { 132 | label = 'First On Model', 133 | icon = 'fa fa-book', 134 | action = function(e) 135 | end 136 | } 137 | } 138 | } 139 | 140 | menus[#menus + 1] = exports['interactionMenu']:Create { 141 | type = 'model', 142 | model = `prop_vend_snak_01`, 143 | offset = vec3(0, 0, 0), 144 | maxDistance = 2.0, 145 | options = { 146 | { 147 | label = 'Second On Model', 148 | icon = 'fa fa-book', 149 | action = function(e) 150 | end 151 | } 152 | } 153 | } 154 | 155 | menus[#menus + 1] = exports['interactionMenu']:Create { 156 | type = 'model', 157 | model = `prop_paper_bag_01`, 158 | offset = vec3(0, 0, 0), 159 | maxDistance = 3.0, 160 | options = { 161 | { 162 | label = 'Second On Model', 163 | icon = 'fa fa-book', 164 | action = function(e) 165 | end 166 | } 167 | } 168 | } 169 | 170 | menus[#menus + 1] = exports['interactionMenu']:Create { 171 | type = 'model', 172 | model = `prop_paper_bag_01`, 173 | offset = vec3(0, 0, 0), 174 | maxDistance = 3.0, 175 | options = { 176 | { 177 | label = 'Pick', 178 | icon = 'fa fa-book', 179 | action = function(e) 180 | DeleteEntity(e) 181 | end 182 | } 183 | } 184 | } 185 | 186 | menus[#menus + 1] = exports['interactionMenu']:Create { 187 | type = 'model', 188 | model = `adder`, 189 | offset = vec3(0, 0, 0), 190 | maxDistance = 2.0, 191 | indicator = { 192 | prompt = 'E', 193 | }, 194 | options = { 195 | { 196 | label = "[Debug] On adder", 197 | icon = 'fa fa-car', 198 | action = function(e) 199 | print(e) 200 | end 201 | } 202 | } 203 | } 204 | end 205 | 206 | local function cleanup() 207 | for index, menu_id in ipairs(menus) do 208 | exports['interactionMenu']:remove(menu_id) 209 | end 210 | for index, entity in ipairs(entities) do 211 | if DoesEntityExist(entity) then 212 | DeleteEntity(entity) 213 | end 214 | end 215 | menus = {} 216 | entities = {} 217 | end 218 | 219 | CreateThread(function() 220 | InternalRegisterTest(init, cleanup, "on_model", "On Models", "fa-solid fa-tent-arrows-down") 221 | end) 222 | -------------------------------------------------------------------------------- /interactionMenu/lua/examples/onPosition.lua: -------------------------------------------------------------------------------- 1 | -- _ 2 | -- | | 3 | -- _____ _| | _____ ___ _ __ 4 | -- / __\ \ /\ / / |/ / _ \/ _ \ '_ \ 5 | -- \__ \\ V V /| < __/ __/ |_) | 6 | -- |___/ \_/\_/ |_|\_\___|\___| .__/ 7 | -- | | 8 | -- |_| 9 | -- https://github.com/swkeep 10 | if not DEVMODE then return end 11 | local positions = { 12 | vector4(794.00, -2997.10, -70.00, 0), 13 | vector4(796.50, -2997.10, -70.00, 0), 14 | vector4(799.00, -2997.10, -70.00, 0), 15 | vector4(801.50, -2997.10, -70.00, 0), 16 | vector4(804.00, -2997.10, -70.00, 0), 17 | vector4(806.50, -2997.10, -70.00, 0), 18 | vector4(809.00, -2997.10, -70.00, 0), 19 | 20 | vector4(794.54, -3002.94, -70.00, 180), 21 | vector4(798.54, -3002.94, -70.00, 180), 22 | vector4(801.04, -3002.94, -70.00, 180), 23 | vector4(803.54, -3002.94, -70.00, 180), 24 | vector4(806.04, -3002.94, -70.00, 180), 25 | vector4(808.54, -3002.94, -70.00, 180), 26 | 27 | vector4(794.00, -3008.70, -70.00, 180), 28 | vector4(796.50, -3008.70, -70.00, 180), 29 | vector4(799.00, -3008.70, -70.00, 180), 30 | vector4(801.50, -3008.70, -70.00, 180), 31 | vector4(804.00, -3008.70, -70.00, 180), 32 | vector4(806.50, -3008.70, -70.00, 180), 33 | vector4(809.00, -3008.70, -70.00, 180), 34 | } 35 | 36 | local menus = {} 37 | local function init() 38 | local names = { "Alice", "Bob", "Charlie", "David", "Eva", "Frank" } 39 | 40 | for i = 1, 10, 1 do 41 | local options = {} 42 | 43 | for index = 1, math.random(2, 15), 1 do 44 | -- Select a random name from the list 45 | local randomName = names[math.random(1, #names)] 46 | options[#options + 1] = { 47 | icon = "fas fa-sign-in-alt", 48 | label = randomName, 49 | action = function() 50 | Wait(5000) 51 | print(randomName) 52 | end 53 | } 54 | end 55 | 56 | menus[#menus + 1] = exports['interactionMenu']:Create { 57 | type = 'position', 58 | position = positions[i], 59 | options = options, 60 | maxDistance = 2.0, 61 | extra = { 62 | onSeen = function() 63 | print('seen') 64 | end 65 | } 66 | } 67 | end 68 | 69 | -- SetTimeout(4000, function() 70 | -- exports['interactionMenu']:set { 71 | -- menuId = menus[1], 72 | -- type = 'position', 73 | -- value = positions[11] 74 | -- } 75 | -- end) 76 | end 77 | 78 | local function cleanup() 79 | for index, value in ipairs(menus) do 80 | exports['interactionMenu']:remove(value) 81 | end 82 | 83 | menus = {} 84 | end 85 | 86 | CreateThread(function() 87 | InternalRegisterTest(init, cleanup, "on_position", "On Locations", "fa-solid fa-location-dot") 88 | end) 89 | -------------------------------------------------------------------------------- /interactionMenu/lua/examples/onZone.lua: -------------------------------------------------------------------------------- 1 | -- _ 2 | -- | | 3 | -- _____ _| | _____ ___ _ __ 4 | -- / __\ \ /\ / / |/ / _ \/ _ \ '_ \ 5 | -- \__ \\ V V /| < __/ __/ |_) | 6 | -- |___/ \_/\_/ |_|\_\___|\___| .__/ 7 | -- | | 8 | -- |_| 9 | -- https://github.com/swkeep 10 | if not DEVMODE then return end 11 | local menus = {} 12 | local position = vector4(795.27, -2996.99, -69.41, 89.63) 13 | 14 | local function init() 15 | position = vector4(795.27, -2996.99, -69.41, 89.63) 16 | menus[#menus + 1] = exports['interactionMenu']:Create { 17 | rotation = vector3(-40, 0, 240), 18 | position = position, 19 | scale = 1, 20 | zone = { 21 | type = 'boxZone', 22 | position = position, 23 | heading = position.w, 24 | width = 4.0, 25 | length = 4.0, 26 | debugPoly = Config.debugPoly, 27 | minZ = position.z - 1, 28 | maxZ = position.z + 2, 29 | }, 30 | options = { 31 | { 32 | video = { 33 | url = 'https://cdn.swkeep.com//interaction_menu_internal_tests/test_video.mp4', 34 | loop = true, 35 | autoplay = true, 36 | volume = 0.1 37 | } 38 | }, 39 | { 40 | label = 'Launch Trojan Horse', 41 | icon = 'fa fa-code', 42 | action = function(data) 43 | print("Action 'Launch Trojan Horse'") 44 | end 45 | }, 46 | { 47 | label = 'Disable Security Cameras', 48 | icon = 'fa fa-video-slash', 49 | action = function(data) 50 | print("Action 'Disable Security Cameras'") 51 | end 52 | }, 53 | { 54 | label = 'Override Access Control', 55 | icon = 'fa fa-key', 56 | action = function(data) 57 | print("Action 'Override Access Control'") 58 | end 59 | }, 60 | { 61 | label = 'Download Classified Files', 62 | icon = 'fa fa-download', 63 | action = function(data) 64 | print("Action 'Download Classified Files'") 65 | end 66 | } 67 | } 68 | } 69 | 70 | position = vector4(794.48, -3002.87, -69.41, 90.68) 71 | menus[#menus + 1] = exports['interactionMenu']:Create { 72 | rotation = vector3(-40, 0, 270), 73 | position = position, 74 | scale = 1, 75 | zone = { 76 | type = 'circleZone', 77 | position = position, 78 | radius = 2.0, 79 | useZ = true, 80 | debugPoly = Config.debugPoly 81 | }, 82 | options = { 83 | { 84 | label = 'Menu on sphere zone', 85 | icon = 'fa fa-code', 86 | action = function(data) 87 | 88 | end 89 | }, 90 | { 91 | video = { 92 | url = 'https://cdn.swkeep.com//interaction_menu_internal_tests/test_video.mp4', 93 | loop = true, 94 | autoplay = true, 95 | volume = 0.1 96 | } 97 | }, 98 | } 99 | } 100 | 101 | menus[#menus + 1] = exports['interactionMenu']:Create { 102 | rotation = vector3(-40, 0, 240), 103 | position = vector3(796.56, -3008.69, -69.0), 104 | scale = 1, 105 | zone = { 106 | type = 'polyZone', 107 | points = { 108 | vector3(792.42, -3009.89, -69.0), 109 | vector3(792.26, -3007.18, -69.0), 110 | vector3(796.56, -3008.69, -69.0) 111 | }, 112 | minZ = position.z - 1, 113 | maxZ = position.z + 1, 114 | debugPoly = Config.debugPoly, 115 | }, 116 | options = { 117 | { 118 | label = 'Menu on poly zone', 119 | icon = 'fa fa-code', 120 | action = function(data) 121 | 122 | end 123 | }, 124 | { 125 | video = { 126 | url = 'https://cdn.swkeep.com//interaction_menu_internal_tests/test_video.mp4', 127 | loop = true, 128 | autoplay = true, 129 | volume = 0.1 130 | } 131 | }, 132 | } 133 | } 134 | 135 | -- menus[#menus + 1] = exports['interactionMenu']:Create { 136 | -- rotation = vector3(0, 0, 180), 137 | -- position = vector4(808.0, -3011.99, -68.0, 2.82), 138 | -- scale = 2, 139 | -- zone = { 140 | -- type = 'comboZone', 141 | -- zones = { 142 | -- { 143 | -- type = 'circleZone', 144 | -- position = vector4(806.9, -3008.61, -69.41, 90.29), 145 | -- radius = 1.0, 146 | -- useZ = true, 147 | -- debugPoly = Config.debugPoly 148 | -- }, 149 | -- { 150 | -- type = 'circleZone', 151 | -- position = vector4(806.89, -3003.05, -69.41, 89.71), 152 | -- radius = 1.0, 153 | -- useZ = true, 154 | -- debugPoly = Config.debugPoly 155 | -- }, 156 | -- }, 157 | -- }, 158 | -- options = { 159 | -- { 160 | -- video = { 161 | -- url = 'https://cdn.swkeep.com//interaction_menu_internal_tests/test_video.mp4', 162 | -- loop = true, 163 | -- autoplay = true, 164 | -- volume = 0.1 165 | -- } 166 | -- }, 167 | -- } 168 | -- } 169 | end 170 | 171 | local function cleanup() 172 | for _, menu_id in ipairs(menus) do 173 | exports['interactionMenu']:remove(menu_id) 174 | end 175 | 176 | menus = {} 177 | end 178 | 179 | CreateThread(function() 180 | InternalRegisterTest(init, cleanup, "on_zone", "On Zones Test", "fa-solid fa-table-cells") 181 | end) 182 | -------------------------------------------------------------------------------- /interactionMenu/lua/examples/paginatedMenu.lua: -------------------------------------------------------------------------------- 1 | -- _ 2 | -- | | 3 | -- _____ _| | _____ ___ _ __ 4 | -- / __\ \ /\ / / |/ / _ \/ _ \ '_ \ 5 | -- \__ \\ V V /| < __/ __/ |_) | 6 | -- |___/ \_/\_/ |_|\_\___|\___| .__/ 7 | -- | | 8 | -- |_| 9 | -- https://github.com/swkeep 10 | local positions = { 11 | vector4(794.00, -2997.10, -69.00, 0), 12 | vector4(799.00, -2997.10, -69.00, 0), 13 | vector4(806.50, -2997.10, -69.00, 0), 14 | 15 | vector4(794.54, -3002.94, -69.00, 180), 16 | vector4(801.04, -3002.94, -69.00, 180), 17 | vector4(806.04, -3002.94, -69.00, 180), 18 | vector4(808.54, -3002.94, -69.00, 180), 19 | 20 | vector4(794.00, -3008.70, -69.00, 180), 21 | vector4(799.00, -3008.70, -69.00, 180), 22 | vector4(804.00, -3008.70, -69.00, 180), 23 | vector4(809.00, -3008.70, -69.00, 180), 24 | } 25 | 26 | local menus = {} 27 | local icons = { 28 | 'fa fa-rectangle-ad', 'fa fa-circle', 'fa fa-square', 'fa fa-star', 'fa fa-heart', 29 | 'fa fa-bell', 'fa fa-flag', 'fa fa-check', 'fa fa-times', 'fa fa-thumbs-up' 30 | } 31 | 32 | local gtaLabels = { 33 | "Start Heist", "Enter Vehicle", "Visit Ammu-Nation", "Call Lester", "Switch Character", 34 | "Start Mission", "Join Crew", "Enter Safehouse", "Buy Weapon", "Vehicle Mod", 35 | "Check Wanted Level", "Change Outfit", "Check Map", "Enter Casino", "Toggle Radio", 36 | "Buy Property", "Get Bounty", "Call Mechanic", "Request Helicopter", "Start Chase" 37 | } 38 | 39 | local function generateOptions() 40 | local options = {} 41 | for i = 1, 100 do 42 | local random_label = gtaLabels[math.random(#gtaLabels)] 43 | local randomColor = string.format("rgb(%d, %d, %d)", math.random(0, 255), math.random(0, 255), math.random(0, 255)) 44 | local label = string.format('
%s
', randomColor, random_label) 45 | 46 | options[#options + 1] = { 47 | label = label, 48 | icon = icons[math.random(#icons)], 49 | action = function(entity) 50 | print("Selected action: " .. random_label) 51 | end 52 | } 53 | end 54 | return options 55 | end 56 | 57 | local function init() 58 | for menuIndex = 1, #positions do 59 | local pos = positions[menuIndex] 60 | local rot = vector3(-20, 0, -90) 61 | 62 | menus[#menus + 1] = exports["interactionMenu"]:paginatedMenu({ 63 | itemsPerPage = 10, 64 | position = pos, 65 | rotation = rot, 66 | scale = 1, 67 | width = '80%', 68 | zone = { 69 | type = "sphere", 70 | position = pos, 71 | radius = 1.5, 72 | useZ = true, 73 | debugPoly = Config.debugPoly 74 | }, 75 | options = generateOptions() 76 | }) 77 | end 78 | end 79 | 80 | local function cleanup() 81 | for _, menu_id in ipairs(menus) do 82 | exports['interactionMenu']:remove(menu_id) 83 | end 84 | 85 | menus = {} 86 | end 87 | 88 | CreateThread(function() 89 | InternalRegisterTest(init, cleanup, "paginated_menu", "Paginated Menu", "fa-solid fa-list-ol") 90 | end) 91 | -------------------------------------------------------------------------------- /interactionMenu/lua/examples/particle.lua: -------------------------------------------------------------------------------- 1 | if not DEVMODE then return end 2 | local active = false 3 | local particleEffect = "scr_agencyheistb" 4 | local particleName = "scr_agency3b_blding_smoke" 5 | local particles = {} 6 | local particlePositions = { 7 | -- vector4(794.00, -2991.52, -70.0, 0), 8 | -- vector4(796.50, -2991.52, -70.0, 0), 9 | -- vector4(799.00, -2991.52, -70.0, 0), 10 | -- vector4(801.50, -2991.52, -70.0, 0), 11 | -- vector4(804.00, -2991.52, -70.0, 0), 12 | -- vector4(806.50, -2991.52, -70.0, 0), 13 | -- vector4(809.00, -2991.52, -70.0, 0), 14 | 15 | vector4(794.00, -2997.10, -70.00, 180), 16 | -- vector4(796.50, -2997.10, -70.00, 180), 17 | vector4(799.00, -2997.10, -70.00, 180), 18 | -- vector4(801.50, -2997.10, -70.00, 180), 19 | vector4(804.00, -2997.10, -70.00, 180), 20 | -- vector4(806.50, -2997.10, -70.00, 180), 21 | vector4(809.00, -2997.10, -70.00, 180), 22 | 23 | -- vector4(794.54, -3002.94, -70.00, 0), 24 | -- vector4(798.54, -3002.94, -70.00, 0), 25 | -- vector4(801.04, -3002.94, -70.00, 0), 26 | -- vector4(803.54, -3002.94, -70.00, 0), 27 | -- vector4(806.04, -3002.94, -70.00, 0), 28 | -- vector4(808.54, -3002.94, -70.00, 0), 29 | 30 | -- vector4(794.00, -3008.70, -70.00, 180), 31 | -- vector4(796.50, -3008.70, -70.00, 180), 32 | -- vector4(799.00, -3008.70, -70.00, 180), 33 | -- vector4(801.50, -3008.70, -70.00, 180), 34 | -- vector4(804.00, -3008.70, -70.00, 180), 35 | -- vector4(806.50, -3008.70, -70.00, 180), 36 | -- vector4(809.00, -3008.70, -70.00, 180), 37 | } 38 | 39 | local function requestParticleAsset(asset) 40 | RequestNamedPtfxAsset(asset) 41 | while not HasNamedPtfxAssetLoaded(asset) do 42 | Citizen.Wait(0) 43 | end 44 | end 45 | 46 | local function init() 47 | if active then return end 48 | active = true 49 | 50 | requestParticleAsset(particleEffect) 51 | 52 | for _, position in pairs(particlePositions) do 53 | UseParticleFxAsset(particleEffect) 54 | particles[#particles + 1] = StartParticleFxLoopedAtCoord( 55 | particleName, 56 | position.x, 57 | position.y, 58 | position.z, 59 | 0.0, -2.0, 0.8, 0.5, true, false, true, false 60 | ) 61 | Wait(100) 62 | end 63 | end 64 | 65 | local function cleanup() 66 | for index, value in ipairs(particles) do 67 | StopParticleFxLooped(value, true) 68 | RemoveParticleFx(value, true) 69 | end 70 | 71 | particles = {} 72 | active = false 73 | end 74 | 75 | CreateThread(function() 76 | InternalRegisterGlobalTest(init, cleanup, "particle", "Toggle Particle") 77 | end) 78 | -------------------------------------------------------------------------------- /interactionMenu/lua/providers/qb-target.lua: -------------------------------------------------------------------------------- 1 | if GetResourceState('qb-core') ~= 'started' then return end 2 | 3 | local function replaceExport(exportName, func) 4 | Util.replaceExport('qb-target', exportName, func) 5 | end 6 | 7 | local function convertTargetOptions(targetOptions) 8 | targetOptions = targetOptions.options 9 | local menuOptions = {} 10 | 11 | for id, value in pairs(targetOptions) do 12 | local payload 13 | local option = { 14 | icon = value.icon, 15 | label = value.label, 16 | canInteract = value.canInteract, 17 | order = value.num, 18 | job = value.job, 19 | gang = value.gang 20 | } 21 | 22 | -- Move other properties to `payload` 23 | for key, val in pairs(value) do 24 | if not option[key] and key ~= "event" and key ~= "action" and key ~= 'type' then 25 | if not payload then payload = {} end 26 | payload[key] = val 27 | end 28 | end 29 | 30 | if value.action then 31 | option.action = value.action 32 | elseif value.event then 33 | option.event = { 34 | type = value.type, 35 | name = value.event, 36 | payload = payload 37 | } 38 | end 39 | 40 | menuOptions[#menuOptions + 1] = option 41 | end 42 | 43 | -- #TODO: sort by order/num 44 | 45 | return menuOptions 46 | end 47 | 48 | replaceExport('AddCircleZone', function(name, center, radius, options, targetoptions) 49 | local distance = targetoptions.distance or 3 50 | local menu = { 51 | id = name, 52 | position = center, 53 | options = convertTargetOptions(targetoptions), 54 | maxDistance = distance, 55 | schemaType = 'qbtarget' 56 | } 57 | 58 | return exports['interactionMenu']:Create(menu) 59 | end) 60 | 61 | replaceExport('AddBoxZone', function(name, center, length, width, options, targetoptions) 62 | local distance = targetoptions.distance or 3 63 | local menu = { 64 | id = name, 65 | position = center, 66 | options = convertTargetOptions(targetoptions), 67 | maxDistance = distance, 68 | schemaType = 'qbtarget' 69 | } 70 | 71 | return exports['interactionMenu']:Create(menu) 72 | end) 73 | 74 | local function getCentroid(polygon) 75 | local centroid = vec3(0, 0, 0) 76 | local numPoints = #polygon 77 | 78 | for i = 1, numPoints do 79 | centroid = centroid + polygon[i] 80 | end 81 | 82 | return (centroid / numPoints) 83 | end 84 | 85 | replaceExport('AddPolyZone', function(name, points, options, targetoptions) 86 | -- #TODO: fix: or just set it to not supported export 87 | local newPoints = table.create(#points, 0) 88 | local thickness = math.abs(options.maxZ - options.minZ) 89 | local distance = targetoptions.distance or 3 90 | 91 | local menu = { 92 | id = name, 93 | position = vec3(0, 0, 0), 94 | options = convertTargetOptions(targetoptions), 95 | maxDistance = distance, 96 | schemaType = 'qbtarget' 97 | } 98 | 99 | for i = 1, #points do 100 | local point = points[i] 101 | newPoints[i] = vec3(point.x, point.y, options.maxZ - (thickness / 2)) 102 | end 103 | 104 | menu.center = getCentroid(newPoints) 105 | return exports['interactionMenu']:Create(menu) 106 | end) 107 | 108 | replaceExport('RemoveZone', function(id) 109 | exports['interactionMenu']:remove(id) 110 | end) 111 | 112 | replaceExport('AddTargetBone', function(bones, options) 113 | bones = Util.ensureTable(bones) 114 | local invokingResource = GetInvokingResource() 115 | 116 | for _, bone in ipairs(bones) do 117 | local id = invokingResource .. '_' .. bone 118 | 119 | exports['interactionMenu']:createGlobal { 120 | id = id, 121 | type = 'bones', 122 | bone = bone, 123 | offset = vec3(0, 0, 0), 124 | maxDistance = 1.0, 125 | options = convertTargetOptions(options), 126 | schemaType = 'qbtarget' 127 | } 128 | end 129 | end) 130 | 131 | replaceExport('RemoveTargetBone', function(bones, labels) 132 | bones = Util.ensureTable(bones) 133 | local invokingResource = GetInvokingResource() 134 | 135 | for i = 1, #bones do 136 | local bone = bones[i] 137 | local id = invokingResource .. '_' .. bone 138 | 139 | exports['interactionMenu']:remove(id) 140 | end 141 | end) 142 | 143 | replaceExport('AddTargetEntity', function(entities, options) 144 | entities = Util.ensureTable(entities) 145 | 146 | for i = 1, #entities do 147 | local entity = entities[i] 148 | 149 | if NetworkGetEntityIsNetworked(entity) then 150 | local netId = NetworkGetNetworkIdFromEntity(entity) 151 | 152 | exports['interactionMenu']:Create { 153 | id = entity, 154 | netId = netId, 155 | options = convertTargetOptions(options), 156 | schemaType = 'qbtarget' 157 | } 158 | else 159 | exports['interactionMenu']:Create { 160 | id = entity, 161 | entity = entity, 162 | options = convertTargetOptions(options), 163 | schemaType = 'qbtarget' 164 | } 165 | end 166 | end 167 | end) 168 | 169 | replaceExport('RemoveTargetEntity', function(entities, labels) 170 | entities = Util.ensureTable(entities) 171 | 172 | for i = 1, #entities do 173 | local entity = entities[i] 174 | 175 | if NetworkGetEntityIsNetworked(entity) then 176 | local netId = NetworkGetNetworkIdFromEntity(entity) 177 | exports['interactionMenu']:remove(netId) 178 | else 179 | exports['interactionMenu']:remove(entity) 180 | end 181 | end 182 | end) 183 | 184 | replaceExport('AddTargetModel', function(models, options) 185 | models = Util.ensureTable(models) 186 | local distance = options.distance or 3 187 | local invokingResource = GetInvokingResource() 188 | 189 | for i = 1, #models do 190 | local model = models[i] 191 | local id = invokingResource .. '|' .. model 192 | local modelHash = type(model) == "number" and model or joaat(model) 193 | 194 | exports['interactionMenu']:Create { 195 | id = id, 196 | model = modelHash, 197 | maxDistance = distance, 198 | options = convertTargetOptions(options), 199 | schemaType = 'qbtarget' 200 | } 201 | end 202 | end) 203 | 204 | replaceExport('RemoveTargetModel', function(models, labels) 205 | models = Util.ensureTable(models) 206 | local invokingResource = GetInvokingResource() 207 | 208 | for i = 1, #models do 209 | local model = models[i] 210 | local id = invokingResource .. '|' .. model 211 | exports['interactionMenu']:remove(id) 212 | end 213 | end) 214 | 215 | local idTemplate = '%s_%s%s_%s' 216 | local function handleGlobalEntity(action, entityType, options) 217 | local distance = options.distance or 3 218 | local invokingResource = GetInvokingResource() 219 | options = convertTargetOptions(options) 220 | 221 | for _, value in ipairs(options) do 222 | local label = value.label or value 223 | local id = idTemplate:format(invokingResource, action, entityType, Util.cleanString(label)) 224 | 225 | if action == 'add' then 226 | exports['interactionMenu']:createGlobal { 227 | id = id, 228 | type = entityType, 229 | offset = vec3(0, 0, 0), 230 | maxDistance = distance, 231 | options = { value }, 232 | schemaType = 'qbtarget' 233 | } 234 | elseif action == 'remove' then 235 | exports['interactionMenu']:remove(id) 236 | end 237 | end 238 | end 239 | 240 | replaceExport('AddGlobalPed', function(options) 241 | handleGlobalEntity('add', 'peds', options) 242 | end) 243 | 244 | replaceExport('RemoveGlobalPed', function(labels) 245 | handleGlobalEntity('remove', 'peds', Util.ensureTable(labels)) 246 | end) 247 | 248 | replaceExport('AddGlobalVehicle', function(options) 249 | handleGlobalEntity('add', 'vehicles', options) 250 | end) 251 | 252 | replaceExport('RemoveGlobalVehicle', function(labels) 253 | handleGlobalEntity('remove', 'vehicles', Util.ensureTable(labels)) 254 | end) 255 | 256 | replaceExport('AddGlobalObject', function(options) 257 | handleGlobalEntity('add', 'entities', options) 258 | end) 259 | 260 | replaceExport('RemoveGlobalObject', function(labels) 261 | handleGlobalEntity('remove', 'entities', Util.ensureTable(labels)) 262 | end) 263 | 264 | replaceExport('AddGlobalPlayer', function(options) 265 | handleGlobalEntity('add', 'players', options) 266 | end) 267 | 268 | replaceExport('RemoveGlobalPlayer', function(labels) 269 | handleGlobalEntity('remove', 'players', Util.ensureTable(labels)) 270 | end) 271 | 272 | -- might be possible! 273 | replaceExport('AddEntityZone') 274 | replaceExport('DisableTarget') 275 | replaceExport('CheckEntity') 276 | replaceExport('CheckBones') 277 | -- mine is always active!? 278 | replaceExport('IsTargetActive') 279 | replaceExport('IsTargetSuccess') 280 | 281 | -- bruh 282 | replaceExport('RaycastCamera') 283 | replaceExport('DisableNUI') 284 | replaceExport('EnableNUI') 285 | replaceExport('LeftTarget') 286 | replaceExport('DrawOutlineEntity') 287 | replaceExport('AddGlobalType') 288 | replaceExport('RemoveGlobalType') 289 | replaceExport('DeletePeds') 290 | replaceExport('RemoveSpawnedPed') 291 | replaceExport('SpawnPed') 292 | replaceExport('GetGlobalTypeData') 293 | replaceExport('GetZoneData') 294 | replaceExport('GetTargetBoneData') 295 | replaceExport('GetTargetEntityData') 296 | replaceExport('GetTargetModelData') 297 | replaceExport('GetGlobalPedData') 298 | replaceExport('GetGlobalVehicleData') 299 | replaceExport('GetGlobalObjectData') 300 | replaceExport('GetGlobalPlayerData') 301 | replaceExport('UpdateGlobalTypeData') 302 | replaceExport('UpdateZoneData') 303 | replaceExport('UpdateTargetBoneData') 304 | replaceExport('UpdateTargetEntityData') 305 | replaceExport('UpdateTargetModelData') 306 | replaceExport('UpdateGlobalPedData') 307 | replaceExport('UpdateGlobalVehicleData') 308 | replaceExport('UpdateGlobalObjectData') 309 | replaceExport('UpdateGlobalPlayerData') 310 | replaceExport('GetPeds') 311 | replaceExport('UpdatePedsData') 312 | replaceExport('AllowTargeting') 313 | -------------------------------------------------------------------------------- /interactionMenu/lua/providers/qb-target_test.lua: -------------------------------------------------------------------------------- 1 | if not Config.devMode then return end 2 | if GetResourceState('qb-core') ~= 'started' then return end 3 | 4 | local function AddBoxZone() 5 | local k = 1 6 | local v = { 7 | position = vector4(-2037.4, 3191.8, 32.81, 47.71), 8 | length = 1, 9 | width = 2 10 | } 11 | exports["qb-target"]:AddBoxZone("Bank_" .. k, v.position, v.length, v.width, { 12 | name = "Bank_" .. k, 13 | heading = v.heading, 14 | minZ = v.minZ, 15 | maxZ = v.maxZ, 16 | debugPoly = Config.debugPoly 17 | }, { 18 | options = { 19 | { 20 | type = "client", 21 | event = "qb-banking:openBankScreen", 22 | icon = "fas fa-university", 23 | label = "Access Bank", 24 | } 25 | }, 26 | distance = 1.5 27 | }) 28 | end 29 | 30 | local function AddCircleZone() 31 | local i = 1 32 | local v = vector3(-2036.28, 3191.27, 32.81) 33 | exports['qb-target']:AddCircleZone('PoliceDuty_' .. i, vector3(v.x, v.y, v.z), 0.5, { 34 | name = 'PoliceDuty_' .. i, 35 | useZ = true, 36 | debugPoly = Config.debugPoly, 37 | }, { 38 | options = { 39 | { 40 | type = 'client', 41 | event = 'qb-policejob:ToggleDuty', 42 | icon = 'fas fa-sign-in-alt', 43 | label = 'Sign In', 44 | jobType = 'leo', 45 | }, 46 | }, 47 | distance = 1.5 48 | }) 49 | v = vector4(-2035.55, 3192.14, 32.81, 239.09) 50 | exports['qb-target']:AddCircleZone('PoliceTrash_' .. i, vector3(v.x, v.y, v.z), 0.5, { 51 | name = 'PoliceTrash_' .. i, 52 | useZ = true, 53 | debugPoly = Config.debugPoly, 54 | }, { 55 | options = { 56 | { 57 | type = 'server', 58 | event = 'qb-policejob:server:trash', 59 | icon = 'fas fa-trash', 60 | label = "Open Bin", 61 | jobType = 'leo', 62 | }, 63 | }, 64 | distance = 1.5 65 | }) 66 | end 67 | 68 | local function AddTargetBone() 69 | local bones = { 70 | 'platelight', 71 | 'exhaust' 72 | } 73 | exports['qb-target']:AddTargetBone(bones, { 74 | options = { 75 | { 76 | num = 1, 77 | icon = 'fa-solid fa-car', 78 | label = 'Scan Plate', 79 | action = function(entity) 80 | print("Plate Scan:", entity) 81 | end, 82 | job = 'police', 83 | } 84 | }, 85 | distance = 4.0, 86 | }) 87 | 88 | local wheels = { 89 | "wheel_lf", 90 | "wheel_rf", 91 | "wheel_lm1", 92 | "wheel_rm1", 93 | "wheel_lm2", 94 | "wheel_rm2", 95 | "wheel_lm3", 96 | "wheel_rm3", 97 | "wheel_lr", 98 | "wheel_rr", 99 | } 100 | exports['qb-target']:AddTargetBone(wheels, { 101 | options = { 102 | { 103 | event = "qb-target_test:client:wheel", 104 | icon = "fas fa-wrench", 105 | label = "Adjust", 106 | item = 'iron', 107 | }, 108 | }, 109 | distance = 1.5 110 | }) 111 | 112 | Wait(5000) 113 | exports['qb-target']:RemoveTargetBone(wheels) 114 | AddEventHandler("qb-target_test:client:wheel", function(data) 115 | print('qb-target_test:client:wheel') 116 | end) 117 | end 118 | 119 | local function AddTargetEntity() 120 | local p1 = vector4(-2036.35, 3195.72, 31.81, 241.25) 121 | local p2 = vector4(-2035.67, 3196.93, 31.81, 274.3) 122 | local ped1 = Util.spawnPed(`g_m_y_famdnf_01`, p1) 123 | local ped2 = Util.spawnPed(`g_m_y_famdnf_01`, p2) 124 | exports['qb-target']:AddTargetEntity({ 125 | ped1, ped2 126 | }, { 127 | options = { 128 | { 129 | label = "Ped", 130 | icon = "fas fa-eye", 131 | action = function(data) 132 | Util.print_table(data) 133 | end, 134 | }, 135 | } 136 | }) 137 | 138 | -- Wait(5000) 139 | -- exports['qb-target']:RemoveTargetEntity(ped1) 140 | -- exports['qb-target']:RemoveTargetEntity(ped2) 141 | end 142 | 143 | local function AddTargetModel() 144 | Config.Dumpsters = { 218085040, 666561306, -58485588, -206690185, 1511880420, 682791951 } 145 | exports['qb-target']:AddTargetModel(Config.Dumpsters, { 146 | options = { 147 | { 148 | event = "qb-target_test:client:dumpster", 149 | icon = "fas fa-dumpster", 150 | label = "Search Dumpster", 151 | }, 152 | }, 153 | distance = 2 154 | }) 155 | AddEventHandler('qb-target_test:client:dumpster', function(data) 156 | print("qb-target_test:client:dumpster") 157 | end) 158 | 159 | Wait(5000) 160 | 161 | exports['qb-target']:RemoveTargetModel(Config.Dumpsters) 162 | end 163 | 164 | local function AddGlobalPed() 165 | local options = { 166 | options = { 167 | { 168 | icon = 'fas fa-dumpster', 169 | label = 'Global ped test', 170 | canInteract = function(entity) 171 | return true 172 | end, 173 | action = function() 174 | 175 | end 176 | }, 177 | { 178 | icon = 'fas fa-dumpster', 179 | label = 'Global ped test 2', 180 | canInteract = function(entity) 181 | return true 182 | end, 183 | action = function() 184 | 185 | end 186 | }, 187 | }, 188 | distance = 5, 189 | } 190 | 191 | exports['qb-target']:AddGlobalPed(options) 192 | 193 | Wait(5000) 194 | 195 | exports['qb-target']:RemoveGlobalPed('Global ped test 2') 196 | exports['qb-target']:RemoveGlobalPed({ 'Global ped test' }) 197 | end 198 | 199 | local function Debug() 200 | CreateThread(function() 201 | Wait(1000) 202 | 203 | local currentResourceName = GetCurrentResourceName() 204 | local targeting = exports['qb-target'] 205 | 206 | AddEventHandler(currentResourceName .. ':debug', function(data) 207 | local entity = data.entity 208 | local model = GetEntityModel(entity) 209 | local type = GetEntityType(entity) 210 | 211 | print('Entity: ' .. entity, 'Model: ' .. model, 'Type: ' .. type) 212 | if data.remove then 213 | targeting:RemoveTargetEntity(data.entity, 'Hello World') 214 | else 215 | targeting:AddTargetEntity(data.entity, { 216 | options = { 217 | { 218 | type = "client", 219 | event = currentResourceName .. ':debug', 220 | icon = "fas fa-circle-check", 221 | label = "Hello World", 222 | remove = true 223 | }, 224 | }, 225 | distance = 3.0 226 | }) 227 | end 228 | end) 229 | 230 | targeting:AddGlobalPed({ 231 | options = { 232 | { 233 | type = "client", 234 | event = currentResourceName .. ':debug', 235 | icon = "fas fa-male", 236 | label = "(Debug) Ped", 237 | }, 238 | }, 239 | distance = Config.MaxDistance 240 | }) 241 | 242 | targeting:AddGlobalVehicle({ 243 | options = { 244 | { 245 | type = "client", 246 | event = currentResourceName .. ':debug', 247 | icon = "fas fa-car", 248 | label = "(Debug) Vehicle", 249 | }, 250 | }, 251 | distance = Config.MaxDistance 252 | }) 253 | 254 | targeting:AddGlobalObject({ 255 | options = { 256 | { 257 | type = "client", 258 | event = currentResourceName .. ':debug', 259 | icon = "fas fa-cube", 260 | label = "(Debug) Object", 261 | }, 262 | }, 263 | distance = Config.MaxDistance 264 | }) 265 | 266 | targeting:AddGlobalPlayer({ 267 | options = { 268 | { 269 | type = "client", 270 | event = currentResourceName .. ':debug', 271 | icon = "fas fa-cube", 272 | label = "(Debug) Player", 273 | }, 274 | }, 275 | distance = Config.MaxDistance 276 | }) 277 | end) 278 | end 279 | 280 | local function OptionsInnerProperties() 281 | local k = 'normal' 282 | local v = { 283 | coords = vector4(-1996.94, 3195.34, 32.81, 278.35), 284 | label = '24/7 Supermarket', 285 | targetIcon = 'fas fa-shopping-basket', 286 | targetLabel = 'Open Shop', 287 | requiredJob = 'police' 288 | } 289 | exports['qb-target']:AddCircleZone(k, vector3(v.coords.x, v.coords.y, v.coords.z), 0.5, { 290 | name = k, 291 | debugPoly = false, 292 | useZ = true 293 | }, { 294 | options = { 295 | { 296 | label = v.targetLabel, 297 | icon = v.targetIcon, 298 | item = v.requiredItem, 299 | shop = k, 300 | job = v.requiredJob, 301 | gang = v.requiredGang, 302 | action = function() 303 | print('test') 304 | end 305 | } 306 | }, 307 | distance = 2.0 308 | }) 309 | end 310 | 311 | -- CreateThread(function() 312 | -- Wait(1000) 313 | -- print('Starting qb-target tests') 314 | 315 | -- AddCircleZone() 316 | -- AddBoxZone() 317 | -- AddTargetBone() 318 | -- AddTargetEntity() 319 | -- AddTargetModel() 320 | -- AddGlobalPed() 321 | -- Debug() 322 | -- OptionsInnerProperties() 323 | -- exports['qb-target']:RaycastCamera() 324 | -- end) 325 | -------------------------------------------------------------------------------- /interactionMenu/lua/server/server.lua: -------------------------------------------------------------------------------- 1 | if not Config.devMode then return end 2 | local function print_table(t) 3 | print(json.encode(t, { indent = true, sort_keys = true })) 4 | end 5 | 6 | RegisterNetEvent('interaction-menu:server:syncAnimation', function(target) 7 | local src = source 8 | 9 | TriggerClientEvent('interaction-menu:client:syncAnimation', target) 10 | end) 11 | 12 | RegisterCommand("interactionMenu", function(source, args, rawCommand) 13 | TriggerClientEvent('interaction-menu:client:helper', source) 14 | end, false) 15 | 16 | RegisterNetEvent('testEvent:server', function(payload, information) 17 | print_table(payload) 18 | print_table(information) 19 | end) 20 | -------------------------------------------------------------------------------- /interactionRenderer/README.md: -------------------------------------------------------------------------------- 1 | This is `generic_texture_renderer_gfx` by [thers](https://forum.cfx.re/u/thers). 2 | 3 | I renamed it to lower the chance of conflicts. 4 | 5 | [Link to the forum post](https://forum.cfx.re/t/release-generic-dui-2d-3d-renderer/131208) 6 | -------------------------------------------------------------------------------- /interactionRenderer/fxmanifest.lua: -------------------------------------------------------------------------------- 1 | fx_version 'cerulean' 2 | games { 'gta5' } 3 | 4 | name 'interactionDUI' 5 | description 'Dui Renderer of interacion menu' 6 | version '1.0.0' 7 | author "swkeep" 8 | repository 'https://github.com/swkeep/interaction-menu' 9 | -------------------------------------------------------------------------------- /interactionRenderer/stream/interaction_renderer.gfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swkeep/interaction-menu/cb68e9ac318b22b39784fcec3b3fe2d1fe1c7dfe/interactionRenderer/stream/interaction_renderer.gfx --------------------------------------------------------------------------------