├── .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 | 
6 | 
7 | 
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 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
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 |
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 |
51 |
57 |
58 |
--------------------------------------------------------------------------------
/interactionDUI/dui_source/src/components/AudioPlayer.vue:
--------------------------------------------------------------------------------
1 |
74 |
75 |
76 |
87 |
88 |
89 |
119 |
--------------------------------------------------------------------------------
/interactionDUI/dui_source/src/components/DevTools.vue:
--------------------------------------------------------------------------------
1 |
76 |
77 |
78 |
79 |
80 |
81 | {{ item.name }}
82 |
83 |
84 |
85 |
86 | {{ count }}
87 |
88 |
Down
89 |
Up
90 |
91 |
Dark Mode: {{ darkMode }}
92 |
Theme: {{ currentTheme }}
93 |
94 | Show current
95 | Hide
96 |
97 |
98 |
99 |
100 |
204 |
--------------------------------------------------------------------------------
/interactionDUI/dui_source/src/components/ImageRenderer.vue:
--------------------------------------------------------------------------------
1 |
94 |
95 |
96 |
97 |
98 |
104 |
105 |
106 |
114 |
115 |
116 |
180 |
--------------------------------------------------------------------------------
/interactionDUI/dui_source/src/components/MenuOption.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
44 |
--------------------------------------------------------------------------------
/interactionDUI/dui_source/src/components/MenuProgressbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
19 |
--------------------------------------------------------------------------------
/interactionDUI/dui_source/src/components/ProgressBar.vue:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 |
28 |
29 | {{ label }}
30 |
31 |
32 |
33 |
36 |
{{ progressValue(value).toFixed(2) }}%
37 |
38 |
39 |
40 |
41 |
93 |
--------------------------------------------------------------------------------
/interactionDUI/dui_source/src/components/VideoRenderer.vue:
--------------------------------------------------------------------------------
1 |
2 |
27 |
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 |
75 |
76 |
85 |
86 |
87 | {{ state.content }}
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
149 |
--------------------------------------------------------------------------------
/interactionDUI/dui_source/src/views/MenuContentRenderer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
30 |
31 |
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
--------------------------------------------------------------------------------