├── .github └── FUNDING.yml ├── .gitignore ├── .prettierrc ├── README.md ├── package.json ├── scripts ├── compiler.js ├── copy.js ├── shared.js ├── transform.js └── watch.js ├── server.toml ├── src-webviews ├── index.html ├── public │ └── vite.svg ├── resource.toml ├── src │ ├── App.css │ ├── App.tsx │ ├── assets │ │ └── react.svg │ ├── index.css │ ├── main.tsx │ ├── shims-react.d.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── src └── core │ ├── client │ ├── startup.ts │ └── webview │ │ └── index.ts │ ├── resource.toml │ ├── server │ ├── startup.ts │ └── utility │ │ ├── ipc.ts │ │ └── reconnect.ts │ └── shared │ └── webviewEvents.ts └── tsconfig.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [stuyk] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | altv-server 3 | resources 4 | .vscode 5 | *.dll 6 | *.exe 7 | update.json 8 | modules/ 9 | data/ 10 | *.log 11 | cache/ 12 | crashdumps/ 13 | package-lock.json 14 | ipc.txt -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "printWidth": 120, 4 | "singleQuote": true, 5 | "semi": true, 6 | "arrowParens": "always" 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Typescript Boilerplate for alt:V with React - v5.0.1 3 |

4 |

5 | 6 |

7 | 8 |

9 | Super Fast Compilation 10 |

11 | 12 | > [!IMPORTANT] 13 | > This boilerplate will no longer be updated. 14 | > Consider using [Rebar Framework](https://rebarv.com/) for more up-to-date practices, and a plugin system. 15 | 16 | # Features 17 | 18 | A simple Typescript Boilerplate that builds incredibly fast using the [SWC Library](https://github.com/swc-project/swc). 19 | 20 | - Auto Refresh Server 21 | - Auto Compile TypeScript Files 22 | - Auto Download Resources 23 | - Single Resource Code Support 24 | - Fastest Auto Reconnect Time on Recompile 25 | - Built-in React for WebViews 26 | 27 | # Installation 28 | 29 | * [Install NodeJS 18+](https://nodejs.org/en/download/current/) 30 | * [Install GIT](https://git-scm.com/downloads) 31 | 32 | ## Clone the Repository 33 | 34 | Use the command below in any terminal, command prompt, etc. 35 | 36 | ```sh 37 | git clone https://github.com/Stuyk/altv-typescript-react 38 | ``` 39 | 40 | ## Install the Repository 41 | 42 | Use the command below in any terminal, command prompt, etc. 43 | 44 | ```sh 45 | cd altv-typescript-react 46 | npm install 47 | ``` 48 | 49 | ## Download Server Files 50 | 51 | Use the command below in any terminal, command prompt, etc. This will download all necessary server files from an additional package used by this project. 52 | 53 | ```sh 54 | npm run update 55 | ``` 56 | 57 | ## Start Production Server (Windows) 58 | 59 | Run this command to run the server in production mode. 60 | 61 | ``` 62 | npm run windows 63 | ``` 64 | 65 | ## Start Production Server (Linux) 66 | 67 | Run this command to run the server in production mode. 68 | 69 | ``` 70 | npm run linux 71 | ``` 72 | 73 | ## Start Developer Server 74 | 75 | Run this command to run the server in development mode. 76 | 77 | This will automatically reconnect your alt:V Client if you have `debug` mode turned on. 78 | 79 | ``` 80 | npm run dev 81 | ``` 82 | 83 | ## WebView Previews 84 | If you need to modify the WebView and want to work out of the browser, use the following command. 85 | 86 | ``` 87 | npm run react-dev 88 | ``` 89 | 90 | ## End Server Runtime 91 | 92 | Use the key combination `ctrl + c` to kill your server in your terminal, command prompt, etc. 93 | 94 | ## How to Add Mods, and New Resources 95 | 96 | Always add your already compiled resources & mods into the `resources` folder. 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "altv-quickstart-ts", 3 | "version": "5.0.1", 4 | "description": "TypeScript with alt:V Made Easy", 5 | "scripts": { 6 | "[-] Server Deployment Commands (They All Do Different Things)": "", 7 | "build": "node ./scripts/compiler.js && npx vite build ./src-webviews && node ./scripts/copy.js && node ./scripts/transform.js", 8 | "windows": "npm run build && altv-server.exe", 9 | "linux": "npm run build && ./altv-server", 10 | "dev": "node ./scripts/watch.js", 11 | "[-] Utility": "", 12 | "react-dev": "npx vite ./src-webviews --clearScreen=false --host=localhost --port=3000", 13 | "update": "altv-pkg d release" 14 | }, 15 | "author": "stuyk", 16 | "license": "ISC", 17 | "devDependencies": { 18 | "@altv/types-client": "^2.5.3", 19 | "@altv/types-natives": "^1.5.3", 20 | "@altv/types-server": "^2.6.6", 21 | "@altv/types-shared": "^1.4.2", 22 | "@altv/types-webview": "^1.0.5", 23 | "@swc/cli": "^0.1.62", 24 | "@swc/core": "^1.3.52", 25 | "@types/node": "^18.15.11", 26 | "@types/react": "^18.0.28", 27 | "@types/react-dom": "^18.0.11", 28 | "@vitejs/plugin-react-swc": "^3.0.0", 29 | "altv-pkg": "^2.0.11", 30 | "fs-extra": "^11.1.1", 31 | "glob": "^10.2.1", 32 | "react": "^18.2.0", 33 | "react-dom": "^18.2.0", 34 | "typescript": "latest", 35 | "watcher": "^2.2.2" 36 | }, 37 | "prettier": { 38 | "tabWidth": 4, 39 | "semi": true, 40 | "printWidth": 120, 41 | "arrowParens": "always", 42 | "singleQuote": true 43 | }, 44 | "type": "module", 45 | "dependencies": { 46 | "vite": "^4.3.1" 47 | }, 48 | "engines": { 49 | "node": ">=18" 50 | } 51 | } -------------------------------------------------------------------------------- /scripts/compiler.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import * as glob from 'glob'; 3 | import swc from '@swc/core' 4 | import { normalizeFilePath } from './shared.js'; 5 | 6 | const SWC_CONFIG = { 7 | jsc: { 8 | parser: { 9 | syntax: 'typescript', 10 | dynamicImport: true, 11 | decorators: true, 12 | }, 13 | transform: { 14 | legacyDecorator: true, 15 | decoratorMetadata: true, 16 | }, 17 | target: 'es2020', 18 | }, 19 | sourceMaps: false, 20 | }; 21 | 22 | const startTime = Date.now(); 23 | const filesToCompile = glob.sync('./src/core/**/*.ts'); 24 | 25 | if (fs.existsSync('resources/core')) { 26 | fs.rmSync('resources/core', { force: true, recursive: true }); 27 | } 28 | 29 | 30 | let compileCount = 0; 31 | for (let i = 0; i < filesToCompile.length; i++) { 32 | const filePath = normalizeFilePath(filesToCompile[i]); 33 | const finalPath = filePath.replace('src/', 'resources/').replace('.ts', '.js'); 34 | const compiled = swc.transformFileSync(filePath, SWC_CONFIG); 35 | fs.outputFileSync(finalPath, compiled.code, { encoding: 'utf-8' }); 36 | compileCount += 1; 37 | } 38 | 39 | console.log(`${compileCount} Files Built | ${Date.now() - startTime}ms`);; 40 | -------------------------------------------------------------------------------- /scripts/copy.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import * as glob from 'glob'; 3 | import { normalizeFilePath } from './shared.js'; 4 | 5 | const startTime = Date.now(); 6 | const files = glob.sync(['src/core/**/*.!(ts)', 'src-webviews/**/*.toml']) 7 | 8 | let filesCopied = 0; 9 | for (let file of files) { 10 | const filePath = normalizeFilePath(file); 11 | 12 | if (filePath.includes('src/')) { 13 | const finalPath = filePath.replace('src/', 'resources/'); 14 | fs.copySync(filePath, finalPath, { overwrite: true }); 15 | } 16 | 17 | if (filePath.includes('src-webviews')) { 18 | const finalPath = filePath.replace('src-webviews/', 'resources/webviews/'); 19 | fs.copySync(filePath, finalPath, { overwrite: true }); 20 | } 21 | 22 | filesCopied += 1; 23 | } 24 | 25 | console.log(`${filesCopied} Files Moved | ${Date.now() - startTime}ms`); -------------------------------------------------------------------------------- /scripts/shared.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra'; 2 | 3 | export function normalizeFilePath(filePath) { 4 | return filePath.replace(/\\/gm, '/'); 5 | } 6 | 7 | export function writeToIpc(command) { 8 | fs.appendFileSync('ipc.txt', `\r\n${command}`); 9 | } 10 | 11 | export async function sleep(ms) { 12 | return new Promise((resolve) => { 13 | setTimeout(resolve, ms); 14 | }) 15 | } -------------------------------------------------------------------------------- /scripts/transform.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import * as glob from 'glob'; 3 | import fs from 'fs'; 4 | 5 | /** 6 | * What does this do? 7 | * Appends `.js` to the end of all imports that are not 'nodejs' imports. 8 | * This is required for alt:V 9 | */ 10 | 11 | const resourcePath = path.join(process.cwd(), 'resources/core/**/*.js').replace(/\\/gm, '/'); 12 | const filePaths = glob.sync(resourcePath); 13 | 14 | const funcsToIgnore = [ 15 | // 16 | 'export function', 17 | 'export const', 18 | 'export let', 19 | 'export async function', 20 | '=>', 21 | ]; 22 | 23 | for (let filePath of filePaths) { 24 | const fileContents = fs.readFileSync(filePath, { encoding: 'utf-8' }); 25 | const splitContents = fileContents.split(/\r?\n/); 26 | 27 | const filePathing = filePath.split('/'); 28 | filePathing.pop(); 29 | const directoryPath = filePathing.join('/'); 30 | 31 | let wasModified = false; 32 | for (let i = 0; i < splitContents.length; i++) { 33 | if (!splitContents[i].includes('import') && !splitContents[i].includes('export')) { 34 | continue; 35 | } 36 | 37 | let shouldSkip = false; 38 | for (let funcToIgnore of funcsToIgnore) { 39 | if (splitContents[i].includes(funcToIgnore)) { 40 | shouldSkip = true; 41 | break; 42 | } 43 | } 44 | 45 | if (shouldSkip) { 46 | continue; 47 | } 48 | 49 | const filePathReg = new RegExp(/('|").*.('|")/g); 50 | const extractions = splitContents[i].match(filePathReg); 51 | if (extractions === null || !extractions) { 52 | continue; 53 | } 54 | 55 | const relativeFilePath = extractions[0].replace(/'/gm, '').replace(/"/gm, ''); 56 | if (relativeFilePath.charAt(0) !== '.' && relativeFilePath.charAt(0) !== '/') { 57 | continue; 58 | } 59 | 60 | const actualFilePath = path.join(directoryPath, relativeFilePath).replace(/\\/gm, '/'); 61 | if (fs.existsSync(actualFilePath)) { 62 | const barrelFileTest = fs.statSync(actualFilePath); 63 | if (barrelFileTest.isDirectory()) { 64 | splitContents[i] = splitContents[i].replace(relativeFilePath, `${relativeFilePath}/index.js`); 65 | wasModified = true; 66 | continue; 67 | } 68 | } 69 | 70 | if (!splitContents[i].includes('.js')) { 71 | splitContents[i] = splitContents[i].replace(relativeFilePath, `${relativeFilePath}.js`); 72 | wasModified = true; 73 | } 74 | } 75 | 76 | if (!wasModified) { 77 | continue; 78 | } 79 | 80 | const finalFile = splitContents.join('\r\n'); 81 | fs.writeFileSync(filePath, finalFile, { encoding: 'utf-8' }); 82 | } -------------------------------------------------------------------------------- /scripts/watch.js: -------------------------------------------------------------------------------- 1 | 2 | import { spawnSync, spawn, ChildProcess } from 'node:child_process' 3 | import Watcher from 'watcher'; 4 | import { writeToIpc, sleep } from './shared.js'; 5 | 6 | const fileWatcher = new Watcher(['./src', './src-webviews'], { recursive: true, renameDetection: true }); 7 | const isWindows = process.platform === "win32"; 8 | const altvProcessName = isWindows ? './altv-server.exe' : './altv-server' 9 | 10 | /** @type {ChildProcess} */ 11 | let childProcess = undefined 12 | let rebootDebounce = Date.now() + 0; 13 | 14 | async function compiler() { 15 | console.log(`Starting Compile`) 16 | const webviewProcess = spawn(isWindows ? 'npx.cmd' : 'npx', ['vite', 'build', './src-webviews'], { stdio: 'inherit' }) 17 | 18 | spawnSync('node', ['./scripts/compiler.js'], { stdio: 'inherit' }) 19 | spawnSync('node', ['./scripts/transform.js'], { stdio: 'inherit' }) 20 | 21 | await new Promise((resolve) => { 22 | webviewProcess.on('exit', resolve); 23 | }) 24 | 25 | spawnSync('node', ['./scripts/copy.js'], { stdio: 'inherit' }) 26 | console.log(`Compile Complete`) 27 | } 28 | 29 | async function reboot() { 30 | if (rebootDebounce > Date.now()) { 31 | return; 32 | } 33 | 34 | rebootDebounce = Date.now() + 1000; 35 | writeToIpc('kick-all'); 36 | await sleep(250); 37 | if (childProcess) { 38 | try { 39 | childProcess.kill(); 40 | } catch (err) { } 41 | 42 | await new Promise((resolve) => { 43 | const interval = setInterval(() => { 44 | if (!childProcess.killed) { 45 | childProcess.kill(); 46 | return; 47 | } 48 | 49 | childProcess = undefined 50 | clearInterval(interval); 51 | resolve(); 52 | }, 100); 53 | }) 54 | } 55 | 56 | await compiler(); 57 | childProcess = spawn(altvProcessName, { stdio: 'inherit' }) 58 | } 59 | 60 | function start() { 61 | fileWatcher.on('change', reboot); 62 | reboot(); 63 | } 64 | 65 | start(); -------------------------------------------------------------------------------- /server.toml: -------------------------------------------------------------------------------- 1 | announce = false 2 | 3 | description = 'test' 4 | website = 'test.com' 5 | gamemode = 'Freeroam' 6 | 7 | language = 'en' 8 | modules = [ 'js-module' ] 9 | name = 'TestServer' 10 | players = 1024 11 | port = 7788 12 | tags = [ 'customTag1', 'customTag2', 'customTag3', 'customTag4' ] 13 | 14 | resources = [ 15 | # Add mods here 16 | 'webviews', 17 | 'core' 18 | ] 19 | 20 | # Turn this off in production mode 21 | debug = true 22 | 23 | # Leave host as 0.0.0.0, don't change it. 24 | host = '0.0.0.0' 25 | 26 | [js-module] 27 | global-webcrypto = true 28 | global-fetch = true 29 | 30 | [voice] 31 | bitrate = 64000 -------------------------------------------------------------------------------- /src-webviews/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src-webviews/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src-webviews/resource.toml: -------------------------------------------------------------------------------- 1 | type = 'asset-pack' 2 | client-files = [ '*' ] -------------------------------------------------------------------------------- /src-webviews/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /src-webviews/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import reactLogo from './assets/react.svg'; 3 | import viteLogo from '/vite.svg'; 4 | import './App.css'; 5 | import { WebViewEvents } from '../../src/core/shared/webviewEvents'; 6 | 7 | function App() { 8 | const [count, setCount] = useState(0); 9 | const [isVisible, setVisibility] = useState(false); 10 | 11 | function invokeEvent() { 12 | console.log('Test!'); 13 | 14 | if ('alt' in window) { 15 | alt.emit('emitToClient'); 16 | } 17 | } 18 | 19 | useEffect(() => { 20 | if (!alt) { 21 | return; 22 | } 23 | 24 | alt.on(WebViewEvents.toggleVisibility, (shouldBeVisible: boolean) => { 25 | setVisibility(shouldBeVisible); 26 | }); 27 | }, []); 28 | 29 | return ( 30 |
31 |
32 | 33 | Vite logo 34 | 35 | 36 | React logo 37 | 38 |
39 |

Vite + React

40 |
41 | 42 | 43 |

44 | Edit src/App.tsx and save to test HMR 45 |

46 |
47 |

Click on the Vite and React logos to learn more

48 |
49 | ); 50 | } 51 | 52 | export default App; 53 | -------------------------------------------------------------------------------- /src-webviews/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src-webviews/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: transparent; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | .hide { 18 | display: none; 19 | opacity: 0; 20 | } 21 | 22 | a { 23 | font-weight: 500; 24 | color: #646cff; 25 | text-decoration: inherit; 26 | } 27 | a:hover { 28 | color: #535bf2; 29 | } 30 | 31 | body { 32 | margin: 0; 33 | display: flex; 34 | place-items: center; 35 | min-width: 320px; 36 | min-height: 100vh; 37 | } 38 | 39 | h1 { 40 | font-size: 3.2em; 41 | line-height: 1.1; 42 | } 43 | 44 | button { 45 | border-radius: 8px; 46 | border: 1px solid transparent; 47 | padding: 0.6em 1.2em; 48 | font-size: 1em; 49 | font-weight: 500; 50 | font-family: inherit; 51 | background-color: #1a1a1a; 52 | cursor: pointer; 53 | transition: border-color 0.25s; 54 | } 55 | button:hover { 56 | border-color: #646cff; 57 | } 58 | button:focus, 59 | button:focus-visible { 60 | outline: 4px auto -webkit-focus-ring-color; 61 | } 62 | 63 | @media (prefers-color-scheme: light) { 64 | :root { 65 | color: #213547; 66 | background-color: #ffffff; 67 | } 68 | a:hover { 69 | color: #747bff; 70 | } 71 | button { 72 | background-color: #f9f9f9; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src-webviews/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.tsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /src-webviews/src/shims-react.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'alt' { 2 | import '@altv/types-webview'; 3 | import '@altv/types-shared'; 4 | export default alt; 5 | } 6 | -------------------------------------------------------------------------------- /src-webviews/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src-webviews/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | "moduleResolution": "bundler", 8 | "allowImportingTsExtensions": true, 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "noEmit": true, 12 | "jsx": "react-jsx" 13 | }, 14 | "include": ["src"], 15 | "references": [{ "path": "./tsconfig.node.json" }] 16 | } 17 | -------------------------------------------------------------------------------- /src-webviews/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src-webviews/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react-swc'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | base: './', 8 | build: { 9 | outDir: '../resources/webviews', 10 | emptyOutDir: true, 11 | minify: 'esbuild', 12 | reportCompressedSize: false, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /src/core/client/startup.ts: -------------------------------------------------------------------------------- 1 | import './webview/index'; -------------------------------------------------------------------------------- /src/core/client/webview/index.ts: -------------------------------------------------------------------------------- 1 | import * as alt from 'alt-client'; 2 | import * as native from 'natives'; 3 | import { WebViewEvents } from '../../shared/webviewEvents'; 4 | 5 | const F2_KEY = 113; 6 | let view: alt.WebView; 7 | let isFocused = false; 8 | 9 | export function focusWebView() { 10 | if (isFocused) { 11 | view.unfocus(); 12 | view.emit(WebViewEvents.toggleVisibility, false); 13 | alt.showCursor(false); 14 | alt.toggleGameControls(true); 15 | native.triggerScreenblurFadeOut(100); 16 | isFocused = false; 17 | } else { 18 | view.focus(); 19 | view.emit(WebViewEvents.toggleVisibility, true); 20 | alt.showCursor(true); 21 | alt.toggleGameControls(false); 22 | native.triggerScreenblurFadeIn(100); 23 | isFocused = true; 24 | } 25 | } 26 | 27 | alt.on('keydown', async (keyCode: number) => { 28 | if (keyCode !== F2_KEY) { 29 | return; 30 | } 31 | 32 | if (view) { 33 | focusWebView(); 34 | return; 35 | } 36 | 37 | view = new alt.WebView('http://assets/webviews/index.html'); 38 | await new Promise((resolve: (...args: any[]) => void) => { 39 | view.once('load', resolve); 40 | }); 41 | 42 | focusWebView(); 43 | }); 44 | -------------------------------------------------------------------------------- /src/core/resource.toml: -------------------------------------------------------------------------------- 1 | type = 'js' 2 | main = 'server/startup.js' 3 | client-files = [ 'client/*', 'shared/*' ] 4 | client-main = 'client/startup.js' 5 | deps = [] -------------------------------------------------------------------------------- /src/core/server/startup.ts: -------------------------------------------------------------------------------- 1 | import './utility/ipc'; // Used to reconnect, do not remove. 2 | import * as alt from 'alt-server'; 3 | 4 | import { connectLocalClient } from './utility/reconnect'; 5 | 6 | alt.log(`alt:V Server - Boilerplate Started`); 7 | alt.on('playerConnect', handlePlayerConnect); 8 | 9 | function handlePlayerConnect(player: alt.Player) { 10 | alt.log(`[${player.id}] ${player.name} has connected to the server.`); 11 | 12 | player.model = 'mp_m_freemode_01'; 13 | player.spawn(36.19486618041992, 859.3850708007812, 197.71343994140625, 0); 14 | alt.emitClient(player, 'log:Console', 'alt:V Server - Boilerplate Started'); 15 | } 16 | 17 | connectLocalClient(); 18 | -------------------------------------------------------------------------------- /src/core/server/utility/ipc.ts: -------------------------------------------------------------------------------- 1 | import * as alt from 'alt-server'; 2 | import fs from 'fs'; 3 | 4 | const FILE_NAME = 'ipc.txt'; 5 | 6 | function onChange() { 7 | const content = fs.readFileSync(FILE_NAME, { encoding: 'utf-8' }); 8 | const contents = content.split('\n'); 9 | const lastLine = contents[contents.length - 1].replace(/\n/g, ''); 10 | 11 | switch (lastLine) { 12 | case 'kick-all': 13 | alt.log(`Invoking IPC Event 'kick-all'`); 14 | alt.Player.all.forEach((player) => { 15 | player.kick('Restarting Server'); 16 | }); 17 | break; 18 | } 19 | } 20 | 21 | if (alt.debug) { 22 | if (fs.existsSync(FILE_NAME)) { 23 | fs.rmSync(FILE_NAME); 24 | } 25 | 26 | fs.writeFileSync(FILE_NAME, ''); 27 | fs.watch(FILE_NAME, onChange); 28 | alt.log(`Listening for IPC Events in Debug Mode`); 29 | } 30 | -------------------------------------------------------------------------------- /src/core/server/utility/reconnect.ts: -------------------------------------------------------------------------------- 1 | const enum Status { 2 | Loading = 'LOADING', 3 | MainMenu = 'MAIN_MENU', 4 | DownloadingFiles = 'DOWNLOADING_FILES', 5 | Connecting = 'CONNECTING', 6 | InGame = 'IN_GAME', 7 | Disconnecting = 'DISCONNECTING', 8 | Error = 'ERROR', 9 | } 10 | 11 | const RETRY_DELAY = 2500; 12 | const DEBUG_PORT = 9223; 13 | 14 | async function getLocalClientStatus(): Promise { 15 | try { 16 | const response = await fetch(`http://127.0.0.1:${DEBUG_PORT}/status`); 17 | return response.text() as unknown as Status; 18 | } catch (error) { 19 | return null; 20 | } 21 | } 22 | 23 | export async function connectLocalClient(): Promise { 24 | const status = await getLocalClientStatus(); 25 | if (status === null) { 26 | return; 27 | } 28 | 29 | if (status !== Status.MainMenu && status !== Status.InGame) { 30 | setTimeout(() => connectLocalClient(), RETRY_DELAY); 31 | } 32 | 33 | try { 34 | await fetch(`http://127.0.0.1:${DEBUG_PORT}/reconnect`, { 35 | method: 'POST', 36 | body: 'serverPassword', // only needed when a password is set in the server.toml 37 | }); 38 | } catch (error) { 39 | console.log(error); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/core/shared/webviewEvents.ts: -------------------------------------------------------------------------------- 1 | export const WebViewEvents = { 2 | toggleVisibility: 'WebView:Toggle:Visibility', 3 | }; 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "typeRoots": ["./node_modules/@types", "./node_modules/@altv"], 4 | "removeComments": true, 5 | "noUnusedLocals": false, 6 | "noUnusedParameters": false, 7 | "noImplicitReturns": true, 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "allowJs": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "module": "esNext", 14 | "target": "esNext", 15 | "outDir": "./resources", 16 | "rootDir": "./src", 17 | "allowSyntheticDefaultImports": true, 18 | "declaration": false 19 | }, 20 | "include": ["./src/**/*.ts"], 21 | "exclude": ["node_modules", "**/*.spec.ts", "resources", "**/*.json"] 22 | } 23 | --------------------------------------------------------------------------------