├── .prettierignore ├── .eslintignore ├── .gitattributes ├── .gitmodules ├── src ├── ui │ ├── loading_screen │ │ ├── assets │ │ │ ├── logo.png │ │ │ ├── renner-bold.otf │ │ │ └── renner-book.otf │ │ ├── index.html │ │ ├── script.js │ │ └── stylesheet.css │ └── nui │ │ ├── assets │ │ └── fonts │ │ │ ├── Circular-Bold.ttf │ │ │ ├── Circular-Book.ttf │ │ │ ├── renner-bold.otf │ │ │ └── renner-book.otf │ │ ├── nui.html │ │ ├── stylesheet.css │ │ ├── modules │ │ ├── WINDOW_EVENT_EMITTER.js │ │ └── NUI_LOADER.js │ │ └── types.d.ts ├── server │ ├── events │ │ ├── index.ts │ │ └── shared.ts │ ├── utils │ │ ├── index.ts │ │ └── crypter.ts │ ├── manager │ │ ├── index.ts │ │ ├── command │ │ │ ├── manager.ts │ │ │ └── handler.ts │ │ └── deferralsManager.ts │ ├── database │ │ ├── schema.ts │ │ ├── index.ts │ │ └── mysql │ │ │ └── index.ts │ ├── index.ts │ └── players.ts ├── client │ ├── utils │ │ ├── index.ts │ │ └── game.ts │ ├── events │ │ ├── index.ts │ │ ├── nui.ts │ │ └── shared.ts │ ├── manager │ │ ├── index.ts │ │ ├── keybind.ts │ │ └── command.ts │ ├── players.ts │ ├── index.ts │ └── game.ts ├── types.d.ts └── shared │ ├── events │ ├── type.ts │ ├── base.ts │ └── local.ts │ ├── manager │ └── tick.ts │ └── utils │ └── base.ts ├── .vscode ├── settings.json └── launch.json ├── .gitignore ├── .github ├── workflows │ └── eslint.yml └── FUNDING.yml ├── fxmanifest.lua ├── .natuna ├── scripts │ └── db │ │ ├── migrate.js │ │ ├── create-schema.js │ │ └── mysql │ │ ├── migration.sql │ │ └── index.js ├── esbuild.js └── package.schema.json ├── .env.example ├── LICENSE ├── docs ├── CONTRIBUTING.md ├── BUILDING.md ├── GETTING_STARTED.md └── CODE_OF_CONDUCT.md ├── tsconfig.json ├── package.json └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | /dist/**/*.js 2 | dist -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /docs 4 | *.js -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "public"] 2 | path = public 3 | url = git@github.com:natunaorg/natunaorg.github.io.git 4 | -------------------------------------------------------------------------------- /src/ui/loading_screen/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natunaorg/fivem/HEAD/src/ui/loading_screen/assets/logo.png -------------------------------------------------------------------------------- /src/ui/nui/assets/fonts/Circular-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natunaorg/fivem/HEAD/src/ui/nui/assets/fonts/Circular-Bold.ttf -------------------------------------------------------------------------------- /src/ui/nui/assets/fonts/Circular-Book.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natunaorg/fivem/HEAD/src/ui/nui/assets/fonts/Circular-Book.ttf -------------------------------------------------------------------------------- /src/ui/nui/assets/fonts/renner-bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natunaorg/fivem/HEAD/src/ui/nui/assets/fonts/renner-bold.otf -------------------------------------------------------------------------------- /src/ui/nui/assets/fonts/renner-book.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natunaorg/fivem/HEAD/src/ui/nui/assets/fonts/renner-book.otf -------------------------------------------------------------------------------- /src/ui/loading_screen/assets/renner-bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natunaorg/fivem/HEAD/src/ui/loading_screen/assets/renner-bold.otf -------------------------------------------------------------------------------- /src/ui/loading_screen/assets/renner-book.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natunaorg/fivem/HEAD/src/ui/loading_screen/assets/renner-book.otf -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "json.schemas": [ 3 | { 4 | "fileMatch": ["package.json"], 5 | "url": ".natuna/package.schema.json" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/server/events/index.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/server"; 3 | 4 | import SharedEvent from "@server/events/shared"; 5 | import LocalEvent from "@shared/events/local"; 6 | 7 | export default class Events { 8 | server = new LocalEvent(); 9 | shared = new SharedEvent(); 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | **/node_modules/* 3 | /.pnp 4 | .pnp.js 5 | 6 | # production 7 | /dist 8 | 9 | # typedoc 10 | /public/docs 11 | 12 | # misc 13 | .env 14 | .DS_Store 15 | .yarn.installed 16 | *.pem 17 | /.natuna/logs 18 | 19 | # debug 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /src/client/utils/index.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/client"; 3 | 4 | import GameUtils from "@client/utils/game"; 5 | import UtilsBase from "@shared/utils/base"; 6 | 7 | export default class Utils extends UtilsBase { 8 | constructor() { 9 | super(); 10 | } 11 | 12 | game = new GameUtils(); 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/eslint.yml: -------------------------------------------------------------------------------- 1 | name: ESLint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | 11 | - name: Install modules 12 | run: yarn 13 | 14 | - name: Run ESLint 15 | run: yarn lint 16 | -------------------------------------------------------------------------------- /src/client/events/index.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/client"; 3 | 4 | import NUIEvent from "@client/events/nui"; 5 | import SharedEvent from "@client/events/shared"; 6 | import LocalEvent from "@shared/events/local"; 7 | 8 | export default class Events { 9 | nui = new NUIEvent(); 10 | client = new LocalEvent(); 11 | shared = new SharedEvent(); 12 | } 13 | -------------------------------------------------------------------------------- /src/ui/nui/nui.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Natuna Framework NUI 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/server/utils/index.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/server"; 3 | 4 | import UtilsBase from "@shared/utils/base"; 5 | import Crypter from "@server/utils/crypter"; 6 | 7 | export default class Utils extends UtilsBase { 8 | constructor() { 9 | super(); 10 | } 11 | 12 | /** 13 | * @description 14 | * Crypter to Encrypt or Decrypt your secret data 15 | */ 16 | crypter = new Crypter(); 17 | } 18 | -------------------------------------------------------------------------------- /fxmanifest.lua: -------------------------------------------------------------------------------- 1 | fx_version 'cerulean' 2 | game {'gta5'} 3 | 4 | author 'Rafly Maulana' 5 | description 'FiveM Typescript/Javascript Bundled Framework with single module engine that runs on Javascript runtime. Powered with NodeJS.' 6 | version '1.5.0' 7 | 8 | ui_page 'src/ui/nui/nui.html' 9 | loadscreen 'src/ui/loading_screen/index.html' 10 | 11 | files "**/ui/**/*" 12 | client_script 'dist/client/**/*.js' 13 | server_script 'dist/server/**/*.js' 14 | -------------------------------------------------------------------------------- /.natuna/scripts/db/migrate.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const dotenv = require("dotenv"); 3 | 4 | dotenv.config({ 5 | path: path.join(process.cwd(), ".env"), 6 | }); 7 | 8 | const mysql = require("./mysql"); 9 | 10 | switch (process.env.DATABASE_DRIVER) { 11 | case "mysql": 12 | mysql.migrate(process); 13 | break; 14 | 15 | default: 16 | throw new Error(`Unknown database driver: ${process.env.DATABASE_DRIVER}`); 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "command": "yarn dev", 9 | "name": "Start Development Mode", 10 | "request": "launch", 11 | "type": "node-terminal" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-var 2 | declare var __natunaDirname: string; 3 | 4 | declare namespace NodeJS { 5 | export interface ProcessEnv { 6 | DATABASE_HOST: string | undefined; 7 | DATABASE_PORT: string | undefined; 8 | DATABASE_NAME: string | undefined; 9 | DATABASE_USER: string | undefined; 10 | DATABASE_DRIVER: string | undefined; 11 | DATABASE_PASSWORD: string | undefined; 12 | CRYPTER_SECRET_KEY: string | undefined; 13 | CRYPTER_ALGORITHM: string | undefined; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/client/manager/index.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/client"; 3 | 4 | import type Events from "@client/events"; 5 | import type Utils from "@client/utils"; 6 | 7 | import CommandManager from "@client/manager/command"; 8 | import KeyBindManager from "@client/manager/keybind"; 9 | import TickManager from "@shared/manager/tick"; 10 | 11 | export default class Manager { 12 | constructor( 13 | private events: Events, // 14 | private utils: Utils 15 | ) {} 16 | 17 | tick = new TickManager(); 18 | command = new CommandManager(this.events); 19 | keybind = new KeyBindManager(this.utils); 20 | } 21 | -------------------------------------------------------------------------------- /src/server/manager/index.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/server"; 3 | 4 | import type Events from "@server/events"; 5 | import type Players from "@server/players"; 6 | 7 | import CommandManager from "@server/manager/command/manager"; 8 | import TickManager from "@shared/manager/tick"; 9 | import Utils from "@server/utils"; 10 | 11 | export default class Manager { 12 | constructor( 13 | private events: Events, // 14 | private players: Players, 15 | private utils: Utils 16 | ) {} 17 | 18 | tick = new TickManager(); 19 | command = new CommandManager(this.events, this.players, this.utils); 20 | } 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # github: [raflymln] 4 | # patreon: # Replace with a single Patreon username 5 | # open_collective: # Replace with a single Open Collective username 6 | ko_fi: raflymln 7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # liberapay: # Replace with a single Liberapay username 10 | # issuehunt: # Replace with a single IssueHunt username 11 | # otechie: # Replace with a single Otechie username 12 | custom: ['https://www.paypal.com/paypalme/raflymln'] 13 | -------------------------------------------------------------------------------- /src/client/events/nui.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/client"; 3 | 4 | export default class NUIEvent { 5 | /** 6 | * @description 7 | * Add a listener for NUI callback events. 8 | */ 9 | listen = (name: string, handler: (data: any, callback: (data: Record) => void) => any) => { 10 | name = encodeURIComponent(name); 11 | 12 | RegisterNuiCallbackType(name); 13 | on(`__cfx_nui:${name}`, handler); 14 | }; 15 | 16 | /** 17 | * @description 18 | * Trigger NUI event 19 | */ 20 | emit = (name: string, data: Record = {}) => { 21 | SendNuiMessage( 22 | JSON.stringify({ 23 | name, 24 | data, 25 | }) 26 | ); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/server/database/schema.ts: -------------------------------------------------------------------------------- 1 | // This file is autogenerated, you should not edit it. 2 | // Run `yarn db:create-schema` in the project root to regenerate this file. 3 | 4 | export type DatabaseDriver = "mysql"; 5 | export type DatabaseSchema = { 6 | ban_lists: { 7 | license: string; 8 | reason: string; 9 | }; 10 | characters: { 11 | id: number; 12 | user_id: number; 13 | first_name: string; 14 | last_name: string; 15 | last_position: string; 16 | skin: string; 17 | health: number; 18 | armour: number; 19 | }; 20 | users: { 21 | id: number; 22 | license: string; 23 | active_character_id: number; 24 | last_ip: string; 25 | last_login: string; 26 | }; 27 | whitelist_lists: { 28 | license: string; 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # [INTRODUCTION] 2 | # Environment (.env) file is a file that stores configuration in 3 | # the environment separate from where the code is based. 4 | # - To use this file, remove the .example from the file name 5 | # ---------------------------------------------------------------- 6 | 7 | # [DATABASE] 8 | # Driver 9 | DATABASE_DRIVER="mysql" # Currently only mysql is supported 10 | 11 | # Host 12 | DATABASE_HOST="localhost" 13 | DATABASE_PORT="3306" 14 | DATABASE_NAME="natuna" 15 | DATABASE_USER="root" 16 | DATABASE_PASSWORD="" 17 | 18 | # [CRYPTER] 19 | # Natuna provide Crypter as a library for encrypting and decrypting data. 20 | # - You should change the secret key to something more secure. 21 | # - Only change the algorithm if you know what you are doing. 22 | CRYPTER_ALGORITHM="aes-256-ctr" 23 | CRYPTER_SECRET_KEY="akusayangsemuanyamuach1324656769420" -------------------------------------------------------------------------------- /src/shared/events/type.ts: -------------------------------------------------------------------------------- 1 | export enum SharedEventType { 2 | // Config 3 | GET_CLIENT_CONFIG = "natuna:client:config", 4 | 5 | // Players 6 | CURRENT_PLAYER_UPDATE = "natuna:server:players:currentPlayerUpdate", 7 | UPDATED_DATA_BROADCAST = "natuna:client:players:updatedDataBroadcast", 8 | 9 | // Commands 10 | CLIENT_EXECUTE_COMMAND = "natuna:client:command:execute", 11 | SET_COMMAND_DESCRIPTION = "natuna:client:command:setDescription", 12 | REGISTER_COMMAND = "natuna:server:command:register", 13 | 14 | // Shared Event (on server) 15 | SERVER_EVENT_HANDLER = "natuna:shared:server:eventHandler", 16 | SERVER_CALLBACK_RECEIVER = "natuna:shared:server:sendCallbackValues", 17 | 18 | // Shared Event (on client) 19 | CLIENT_EVENT_HANDLER = "natuna:shared:client:eventHandler", 20 | CLIENT_CALLBACK_RECEIVER = "natuna:shared:client:sendCallbackValues", 21 | } 22 | -------------------------------------------------------------------------------- /src/ui/nui/stylesheet.css: -------------------------------------------------------------------------------- 1 | /* CUSTOM CSS STYLING HERE 2 | ====================*/ 3 | 4 | @font-face { 5 | font-family: "Circular"; 6 | src: url("./assets/fonts/Circular-Bold.ttf"); 7 | font-weight: bold; 8 | font-style: normal; 9 | } 10 | 11 | @font-face { 12 | font-family: "Circular"; 13 | src: url("./assets/fonts/Circular-Book.ttf"); 14 | font-weight: normal; 15 | font-style: normal; 16 | } 17 | 18 | @font-face { 19 | font-family: Renner; 20 | src: url(./assets/fonts/renner-book.otf); 21 | font-weight: normal; 22 | } 23 | 24 | @font-face { 25 | font-family: Renner; 26 | src: url(./assets/fonts/renner-bold.otf); 27 | font-weight: bold; 28 | } 29 | 30 | /* NUI GENERAL 31 | ====================*/ 32 | 33 | html, 34 | body { 35 | height: 100%; 36 | width: 100%; 37 | margin: 0; 38 | } 39 | 40 | .nui-wrapper { 41 | height: 100%; 42 | width: 100%; 43 | position: absolute; 44 | top: 0; 45 | left: 0; 46 | } 47 | -------------------------------------------------------------------------------- /src/shared/manager/tick.ts: -------------------------------------------------------------------------------- 1 | export type TickHandler = () => void; 2 | 3 | export default class TickManager { 4 | constructor() { 5 | setTick(() => { 6 | for (const tick of this.#list) { 7 | tick.handler(); 8 | } 9 | }); 10 | } 11 | 12 | #count = 0; 13 | #list: { id: number; handler: TickHandler }[] = []; 14 | 15 | set = (handler: TickHandler) => { 16 | const id = this.#count; 17 | 18 | this.#list.push({ id, handler }); 19 | this.#count++; 20 | 21 | return id; 22 | }; 23 | 24 | remove = (id: number) => { 25 | this.#list = this.#list.filter((t) => t.id !== id); 26 | return true; 27 | }; 28 | 29 | update = (id: number, handler: TickHandler) => { 30 | const index = this.#list.findIndex((t) => t.id === id); 31 | 32 | if (index === -1) { 33 | return false; 34 | } 35 | 36 | this.#list[index].handler = handler; 37 | return true; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Rafly Maulana 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/client/manager/keybind.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/client"; 3 | 4 | import type Utils from "@client/utils"; 5 | 6 | export default class KeyBindManager { 7 | constructor( 8 | private utils: Utils // 9 | ) {} 10 | 11 | #keyControlCount = 1; 12 | 13 | /** 14 | * @description 15 | * Set a keyboard key mapper to a function. This function also allows user to change their keybind on pause menu settings. 16 | * 17 | * ![](https://i.cfx.re/rage/fwuiComplexObjectDirectImpl/Contains/1836.png) 18 | */ 19 | registerKeyControl = (key: string, description: string, onClick: () => any, onReleased: () => any = () => true) => { 20 | const controlID = `control-${this.#keyControlCount}-${this.utils.generateUniqueId()}`; 21 | const controlIDHash = "~INPUT_" + this.utils.getHashString(`+${controlID}`) + "~"; 22 | 23 | RegisterCommand(`+${controlID}`, () => onClick(), false); 24 | RegisterCommand(`-${controlID}`, () => onReleased(), false); 25 | RegisterKeyMapping(`+${controlID}`, description, "keyboard", key); 26 | 27 | this.#keyControlCount++; 28 | return controlIDHash; 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /.natuna/scripts/db/create-schema.js: -------------------------------------------------------------------------------- 1 | const dotenv = require("dotenv"); 2 | const path = require("path"); 3 | const fs = require("fs"); 4 | const prettier = require("prettier"); 5 | 6 | dotenv.config({ 7 | path: path.join(process.cwd(), ".env"), 8 | }); 9 | 10 | const pkg = require("../../../package.json"); 11 | const mysql = require("./mysql"); 12 | 13 | let types = ""; 14 | 15 | switch (process.env.DATABASE_DRIVER) { 16 | case "mysql": 17 | types = mysql.createSchema(process); 18 | break; 19 | 20 | default: 21 | throw new Error(`Unsupported database driver: ${process.env.DATABASE_DRIVER}`); 22 | } 23 | 24 | const result = ` 25 | // This file is autogenerated, you should not edit it. 26 | // Run \`yarn db:create-schema\` in the project root to regenerate this file. 27 | 28 | export type DatabaseDriver = "${process.env.DATABASE_DRIVER}"; 29 | export type DatabaseSchema = {${types}}; 30 | `; 31 | 32 | const formatted = prettier.format(result, { 33 | ...pkg.prettier, 34 | parser: "typescript", 35 | }); 36 | 37 | const schemaPath = path.join(process.cwd(), "src", "server", "database", "schema.ts"); 38 | 39 | fs.writeFileSync(schemaPath, formatted); 40 | -------------------------------------------------------------------------------- /src/shared/events/base.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/client"; 3 | 4 | type Listeners = { 5 | id: number; 6 | name: string; 7 | handler: (...args: any) => any; 8 | }; 9 | 10 | export type CallbackValueData = { 11 | uniqueId: string; 12 | values: any; 13 | }; 14 | 15 | export type EmitData = { 16 | name: string; 17 | uniqueId: string; 18 | args: any[]; 19 | }; 20 | 21 | export default class EventBase { 22 | protected $listenerCounter = 1; 23 | protected $listeners: Listeners[] = []; 24 | protected $callbackValues: CallbackValueData[] = []; 25 | 26 | protected $validateEventName = (name: string | string[]) => { 27 | // Check if the name is an array or string 28 | if ((typeof name == "object" && Array.isArray(name)) || typeof name !== "string") { 29 | throw new Error(`Invalid Event Name Properties for ${name}`); 30 | } 31 | 32 | // If the name was a string then convert it to an array 33 | if (typeof name === "string") { 34 | name = [name]; 35 | } 36 | 37 | // Check if the name is an array of strings 38 | if (name.find((alias) => typeof alias !== "string")) { 39 | throw new Error(`Invalid Event Name Properties for ${name}`); 40 | } 41 | 42 | return name; 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /src/server/utils/crypter.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/server"; 3 | 4 | import { randomBytes, createCipheriv, createDecipheriv } from "crypto"; 5 | 6 | export default class Crypter { 7 | #iv: Buffer = randomBytes(16); 8 | #algorithm = process.env.CRYPTER_ALGORITHM; 9 | #secretKey = process.env.CRYPTER_SECRET_KEY; 10 | 11 | /** 12 | * @description 13 | * Encrypt a data 14 | * 15 | * @example 16 | * ```ts 17 | * encrypt('bacon'); // Result: "e7b75a472b65bc4a42e7b3f788..." 18 | * ``` 19 | */ 20 | encrypt = (text: string) => { 21 | const cipher = createCipheriv(this.#algorithm, this.#secretKey, this.#iv); 22 | const encrypted = Buffer.concat([cipher.update(text), cipher.final()]); 23 | 24 | return encrypted.toString("hex"); 25 | }; 26 | 27 | /** 28 | * @description 29 | * Decrypt a hash/encrypted data 30 | * 31 | * @example 32 | * ```ts 33 | * decrypt('e7b75a472b65bc4a42e7b3f788...') // Result: "bacon" 34 | * ``` 35 | */ 36 | decrypt = (hash: { iv: any; content: any }) => { 37 | const decipher = createDecipheriv(this.#algorithm, this.#secretKey, Buffer.from(hash.iv, "hex")); 38 | const decrpyted = Buffer.concat([decipher.update(Buffer.from(hash.content, "hex")), decipher.final()]); 39 | 40 | return decrpyted.toString(); 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/ui/nui/modules/WINDOW_EVENT_EMITTER.js: -------------------------------------------------------------------------------- 1 | /* global $, GetParentResourceName */ 2 | 3 | /** 4 | * @description 5 | * List of events registered 6 | */ 7 | window.events = []; 8 | 9 | /** 10 | * @description 11 | * Trigger an event to another NUI 12 | */ 13 | window.emit = (name, data = {}) => { 14 | const event = new CustomEvent(name, { detail: data }); 15 | window.dispatchEvent(event); 16 | }; 17 | 18 | /** 19 | * @description 20 | * Listen for event 21 | */ 22 | window.on = (name, handler) => { 23 | const index = window.events.length; 24 | const wrapper = (event) => handler(event.detail); 25 | 26 | window.addEventListener(name, wrapper); 27 | window.events.push({ index, wrapper, name }); 28 | 29 | return index; 30 | }; 31 | 32 | /** 33 | * @description 34 | * Remove an event listener 35 | */ 36 | window.removeListener = (name, index) => { 37 | for (const listener of window.events) { 38 | if (listener.name === name && (!listener.index || listener.index === index)) { 39 | window.removeEventListener(name, listener.wrapper); 40 | } 41 | } 42 | }; 43 | 44 | /** 45 | * @description 46 | * Send data back to client (use addNUIEventhandler on the client script) 47 | */ 48 | window.sendData = (name, data = {}) => { 49 | name = encodeURIComponent(name); 50 | $.post(`https://${GetParentResourceName()}/${name}`, JSON.stringify(data)); 51 | }; 52 | -------------------------------------------------------------------------------- /src/ui/nui/types.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module NUI 3 | * @category NUI 4 | */ 5 | 6 | export const window: { 7 | /** 8 | * @description 9 | * Trigger an event to another NUI 10 | * 11 | * @example 12 | * ```js 13 | * window.emit('someNUIEvent', { 14 | * text: "someText" 15 | * }); 16 | * ``` 17 | */ 18 | emit: (name: string, data?: { [key: string]: any }) => void; 19 | 20 | /** 21 | * @description 22 | * Listen events from other NUI `window.emit` or from client `triggerNUIEvent`. 23 | * 24 | * It also return the event index at the initialization, used to specify this event on removeListener function. 25 | * 26 | * @example 27 | * ```js 28 | * window.on('someNUIEvent', (data)=> { 29 | * copyText(data.text) 30 | * }); 31 | * ``` 32 | */ 33 | on: (name: string, handler: (data: { [key: string]: any }) => any) => number; 34 | 35 | /** 36 | * @description 37 | * Remove an event listener. If eventIndex is not specified, then it would remove all event listener with the same name. 38 | * 39 | * @example 40 | * ```js 41 | * window.removeListener('someNUIEvent', 1); 42 | * ``` 43 | */ 44 | removeListener: (name: string, eventIndex?: number) => void; 45 | 46 | /** 47 | * @description 48 | * Emit an event to client. Listen the event on client with `addNUIEventHandler` 49 | * 50 | * @example 51 | * ```js 52 | * window.sendData('client:getMenuIndex', { 53 | * menuIndex: 1 54 | * }) 55 | * ``` 56 | */ 57 | sendData: (name: string, data?: { [key: string]: any }) => void; 58 | }; 59 | -------------------------------------------------------------------------------- /.natuna/scripts/db/mysql/migration.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE IF NOT EXISTS `natuna` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; 2 | 3 | CREATE TABLE IF NOT EXISTS `ban_lists` ( 4 | `license` varchar(255) NOT NULL, 5 | `reason` longtext NOT NULL, 6 | PRIMARY KEY (`license`) 7 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 8 | 9 | CREATE TABLE IF NOT EXISTS `characters` ( 10 | `id` int(11) NOT NULL AUTO_INCREMENT, 11 | `user_id` int(11) NOT NULL, 12 | `first_name` varchar(255) NOT NULL, 13 | `last_name` varchar(255) NOT NULL, 14 | `last_position` longtext DEFAULT NULL, 15 | `skin` longtext DEFAULT NULL, 16 | `health` int(11) DEFAULT NULL, 17 | `armour` int(11) DEFAULT NULL, 18 | PRIMARY KEY (`id`), 19 | KEY `user_id` (`user_id`) 20 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 21 | 22 | CREATE TABLE IF NOT EXISTS `users` ( 23 | `id` int(11) NOT NULL AUTO_INCREMENT, 24 | `license` varchar(255) NOT NULL, 25 | `active_character_id` int(11) DEFAULT NULL, 26 | `last_ip` varchar(255) DEFAULT NULL, 27 | `last_login` varchar(255) DEFAULT NULL, 28 | PRIMARY KEY (`id`), 29 | KEY `active_character_id` (`active_character_id`) 30 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 31 | 32 | CREATE TABLE IF NOT EXISTS `whitelist_lists` ( 33 | `license` varchar(255) NOT NULL, 34 | PRIMARY KEY (`license`) 35 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 36 | 37 | ALTER TABLE `characters` 38 | DROP CONSTRAINT IF EXISTS `characters_ibfk_1`; 39 | ALTER TABLE `characters` 40 | ADD CONSTRAINT `characters_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; 41 | 42 | ALTER TABLE `users` 43 | DROP CONSTRAINT IF EXISTS `users_ibfk_1`; 44 | ALTER TABLE `users` 45 | ADD CONSTRAINT `users_ibfk_1` FOREIGN KEY (`active_character_id`) REFERENCES `characters` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; 46 | 47 | COMMIT; 48 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | We're happy if you wanted to help us developing this framework to it's peak! You could help us by creating new feature or maintain current code and fix current bug or etc. 3 | 4 | Please keep in mind that we need to keep this framework always tidy and clean, so, there'd be a rules and instruction how to contribute to this framework. 5 | 6 | 1. ### *Clean and OOP Code* 7 | Please always keep the code clean and optimized, and try to always use Object Oriented Programming (Classes). 8 | 9 | 2. ### *Discuss with others* 10 | Please maintain a good communication between developers before working on new feature and make sure if someone hasn't working on a same thing already. 11 | 12 | You could discuss this with others on our [Discord Server](https://discord.gg/kGPHBvXzGM). 13 | 14 | 3. ### *Make Sure Your Code Is Pass The Testing* 15 | We like to have the consistency of the code, so, please make sure your code is pass the testing. Use `npm run lint` or `yarn lint` command to run the test. 16 | 17 | 4. ### *Pull Request* 18 | Always maintain to have your fork with the latest version on the upstream branch, and always make sure to have a pull request to different branch than the master branch. 19 | 20 | Also, you don't have to update the documentation, the repository maintainers would handle that. 21 | 22 | ## Steps 23 | To do a pull request you just only need to do this: 24 | 25 | 1. [Fork this repository](https://github.com/natuna-framework/fivem/fork) to your account (Must be forked!). 26 | 2. Edit the forked repository. 27 | 3. Go to [pull request](https://github.com/natuna-framework/fivem/pulls) tab and click on New Pull Request button. 28 | 29 | Learn more about pull request on [here](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request). -------------------------------------------------------------------------------- /src/ui/loading_screen/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Natuna Indonesia 5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 |

Temukan berbagai cerita menarik dan ciptakan perjalananmu disini!

14 |
15 | 16 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/server/database/index.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/server"; 3 | 4 | import type Logger from "@ptkdev/logger"; 5 | import type { DatabaseDriver, DatabaseSchema } from "@server/database/schema"; 6 | 7 | import mysql from "mysql2"; 8 | import MySQL, { executeQuery as MySQLExecuteQuery } from "@server/database/mysql"; 9 | 10 | // prettier-ignore 11 | export type DatabaseDriverUsed = 12 | DatabaseDriver extends "mysql" 13 | ? MySQL 14 | : DatabaseDriver extends "sqlite" 15 | ? never 16 | : DatabaseDriver extends "mongodb" 17 | ? never 18 | : DatabaseDriver extends "postgresql" 19 | ? never 20 | : never; 21 | 22 | export default function Database(logger: Logger) { 23 | switch (process.env.DATABASE_DRIVER) { 24 | case "mysql": 25 | const connection = mysql.createPool({ 26 | isServer: false, 27 | host: process.env.DATABASE_HOST, 28 | user: process.env.DATABASE_USER, 29 | database: process.env.DATABASE_NAME, 30 | password: process.env.DATABASE_PASSWORD, 31 | port: Number(process.env.DATABASE_PORT), 32 | waitForConnections: true, 33 | connectionLimit: 10, 34 | queueLimit: 0, 35 | }); 36 | 37 | MySQLExecuteQuery(connection, `CREATE DATABASE IF NOT EXISTS \`${process.env.DATABASE_NAME}\``) 38 | .then(() => { 39 | logger.info("Successfully established database connection"); 40 | }) 41 | .catch((err) => { 42 | logger.error("Failed to establish database connection"); 43 | throw new Error(err); 44 | }); 45 | 46 | return (tableName: keyof DatabaseSchema) => new MySQL(connection, tableName); 47 | 48 | default: 49 | return null; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /docs/BUILDING.md: -------------------------------------------------------------------------------- 1 | # Building 2 | Natuna Framework came in source code form, so you can build it with your own tools. We used EsBuild as our default compiler and it's so fast. Learn more about EsBuild [here](https://esbuild.github.io/) 3 | 4 | ## Requirements 5 | 1. Basic knowledge of Command Prompt 6 | 2. Basic knowledge of NodeJS commands 7 | 8 | ## Package Requirements 9 | We would used a latest version of these packages, so please keep the latest update for these packages. 10 | 11 | 1. NodeJS - https://nodejs.org/en/download/ 12 | 2. NPM - https://docs.npmjs.com/downloading-and-installing-node-js-and-npm 13 | 3. Yarn - https://classic.yarnpkg.com/en/docs/install/#windows-stable 14 | 15 | ## Types of Builds 16 | 17 | ### 1. **Files**
18 | Builds all files in the `src` folder.
19 | 20 | - **✔️ You need to build the files when:** 21 | 1. After cloning this repository on your local machine (Installation). 22 | 2. After editing any script under `src` folder. 23 | 3. After generating a new Database schema. 24 | 25 | - **❌ You DON'T need to build the files when:** 26 | 1. After editing configuration on `package.json`. 27 | 28 | To build files, you need to run: 29 | - `npm run build` or `yarn build` to perform a single production build. 30 | - `npm run build:dev` or `yarn build:dev` to perform a development build. 31 | - `npm run dev` or `yarn dev` to perform a watch build, which will automatically rebuild the files when you change them. 32 | 33 | ### 2. **Database**
34 | Builds the database schema. This is required after you edit the database migration file so that the new schema will be applied to the database module. 35 | 36 | Current database is only support MySQL, and its schema is generated from SQL migration file under `.natuna/database/mysql/migration.sql`. 37 | 38 | To build database, you need to run: 39 | - `npm run db:migrate` or `yarn db:migrate` to perform a a database migration from the migration file. 40 | - `npm run db:create-schema` or `yarn db:create-schema` to generate the new database schema from the migration file. 41 | - `npm run db:setup` or `yarn db:setup` to perform 2 steps above. 42 | 43 | ## Steps 44 | 1. Go to the Natuna Framework folder. 45 | 2. Open command prompt and locate to the project folder using `cd` command. 46 | 3. Run a build command based on your preference, check the build options above. 47 | 4. Wait until it's finished. 48 | 5. If there are no error presented at the end, then you good to go. 49 | 50 | > If you're ever wonder, yes, EsBuild is generating your whole file mostly under 1 second. -------------------------------------------------------------------------------- /src/server/manager/command/manager.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/server"; 3 | 4 | import type { Config, Handler } from "@server/manager/command/handler"; 5 | import type Events from "@server/events"; 6 | import type Players from "@server/players"; 7 | import type Utils from "@server/utils"; 8 | 9 | import Command from "@server/manager/command/handler"; 10 | import { SharedEventType } from "@shared/events/type"; 11 | 12 | export default class CommandManager { 13 | constructor( 14 | private events: Events, // 15 | private players: Players, 16 | private utils: Utils 17 | ) { 18 | this.events.shared.listen(SharedEventType.REGISTER_COMMAND, this.register); 19 | } 20 | 21 | #commands: Record = {}; 22 | 23 | #addCommand = (source: number, name: string, handler: Handler, config: Config = {}, isClientCommand = false) => { 24 | if (this.#commands[name]) { 25 | if (!isClientCommand) { 26 | throw new Error(`Command "${name}" had already been registered before!`); 27 | } else { 28 | this.events.shared.emit(SharedEventType.SET_COMMAND_DESCRIPTION, source, [name, config]); 29 | return true; 30 | } 31 | } 32 | 33 | this.#commands[name] = new Command(this.events, this.players, this.utils, name, handler, config, isClientCommand); 34 | this.events.shared.emit(SharedEventType.SET_COMMAND_DESCRIPTION, source, [name, config]); 35 | return true; 36 | }; 37 | 38 | /** 39 | * @description 40 | * Registrating a command. If isClientCommand was set true, the handler would just triggering a client registered command 41 | * 42 | * @param name Name of the command 43 | * @param handler Function to executed 44 | * @param config Configuration of the command 45 | * @param isClientCommand Whether if the command was sent from client or not 46 | * 47 | * @example 48 | * ```ts 49 | * registerCommand( 50 | * 'hello', 51 | * (src, args) => console.log('Hello!'), 52 | * { 53 | * description: "Say Hello" 54 | * } 55 | * }); 56 | * ``` 57 | */ 58 | register = (source: number, name: string | string[], handler: Handler, config: Config = {}, isClientCommand = false) => { 59 | if (typeof name === "string") { 60 | name = [name]; 61 | } 62 | 63 | for (const alias of name) { 64 | this.#addCommand(source, alias, handler, config, isClientCommand); 65 | } 66 | 67 | return true; 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /src/client/manager/command.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/client"; 3 | 4 | import type Events from "@client/events"; 5 | import type { Handler, Config } from "@server/manager/command/handler"; 6 | 7 | import { SharedEventType } from "@shared/events/type"; 8 | 9 | export default class CommandManager { 10 | constructor( 11 | private events: Events // 12 | ) { 13 | this.events.shared.listen(SharedEventType.SET_COMMAND_DESCRIPTION, this.setDescription); 14 | this.events.shared.listen(SharedEventType.CLIENT_EXECUTE_COMMAND, (name: string, args: Array, raw: string) => { 15 | const src = GetPlayerServerId(PlayerId()); 16 | return this.#list[name](src, args, raw || name); 17 | }); 18 | } 19 | 20 | /** @hidden */ 21 | #list: Record = {}; 22 | 23 | /** 24 | * @description 25 | * Registrating a command. The actual command handler is registered on server to unlock the feature like permission based command or something, so it'd write a event on client and registered a handler on server to trigger the command from registered command in client. 26 | * 27 | * @example 28 | * ```ts 29 | * registerCommand( 30 | * 'hello', 31 | * (src, args) => console.log('Hello!'), 32 | * { 33 | * description: "Say Hello" 34 | * } 35 | * }); 36 | * ``` 37 | */ 38 | register = (name: string | Array, handler: Handler, config: Config = {}): void => { 39 | const addCommand = (name: string) => { 40 | this.#list[name] = handler; 41 | this.events.shared.emit(SharedEventType.REGISTER_COMMAND, [name, {}, config, true]); 42 | }; 43 | 44 | if (Array.isArray(name)) { 45 | for (const alias of name) addCommand(alias); 46 | } else { 47 | addCommand(name); 48 | } 49 | }; 50 | 51 | /** 52 | * @description 53 | * Set a description on command, this function is executed automatically after you registering a command with configuration that contain description. 54 | * 55 | * https://docs.fivem.net/docs/resources/chat/events/chat-addSuggestion/ 56 | */ 57 | setDescription = (name: string, config: Pick): boolean => { 58 | setImmediate(() => { 59 | emit("chat:addSuggestion", [ 60 | `/${name}`, // 61 | config.description || "No Description is Set", 62 | config.argsDescription || [], 63 | ]); 64 | }); 65 | 66 | return true; 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /src/ui/loading_screen/script.js: -------------------------------------------------------------------------------- 1 | new (class Background { 2 | constructor() { 3 | let currentBg = 0; 4 | const backgrounds = [ 5 | "https://img3.goodfon.com/wallpaper/nbig/7/f0/grand-theft-auto-5-gta-5-v-4k.jpg", 6 | "https://i.pinimg.com/originals/86/b2/54/86b2543d67abf7d1945a16ea2694c242.jpg", 7 | "https://libertycity.net/uploads/download/gta5_newobjects/fulls/ngai78eu4dq8pct3tp7an53v91/15210337351268_8bb164-gta5_2018_03_11_21_48_41_798.jpg", 8 | "https://wallup.net/wp-content/uploads/2016/05/27/226763-Grand_Theft_Auto_V.jpg", 9 | "https://steamuserimages-a.akamaihd.net/ugc/1666853488464316494/A3BE3989F12E1038E3CDAD7A1157F164B4492B6D/?imw=5000&imh=5000&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=false", 10 | ]; 11 | 12 | setInterval(() => { 13 | document.querySelector("body").style.backgroundImage = `url('${backgrounds[currentBg]}')`; 14 | currentBg += 1; 15 | 16 | if (currentBg === backgrounds.length) { 17 | currentBg = 0; 18 | } 19 | }, 10 * 1000); 20 | } 21 | })(); 22 | 23 | new (class ProgressLoader { 24 | constructor() { 25 | let count = 0; 26 | let thisCount = 0; 27 | const message = { 28 | INIT_BEFORE_MAP_LOADED: "Setting Up Game", 29 | INIT_AFTER_MAP_LOADED: "Initializing Game", 30 | INIT_SESSION: "Initializing Session", 31 | }; 32 | 33 | const handlers = { 34 | startInitFunctionOrder: (data) => { 35 | count = data.count; 36 | document.querySelector(".progress-text").innerHTML = message[data.type] || data.type; 37 | }, 38 | 39 | startDataFileEntries: (data) => { 40 | count = data.count; 41 | }, 42 | 43 | initFunctionInvoking: (data) => { 44 | document.querySelector(".progress-bar-filled").style.width = (data.idx / count) * 100 + "%"; 45 | }, 46 | 47 | performMapLoadFunction: (data) => { 48 | ++thisCount; 49 | document.querySelector(".progress-bar-filled").style.width = (thisCount / count) * 100 + "%"; 50 | }, 51 | 52 | onLogLine: (data) => { 53 | document.querySelector(".progress-text").innerHTML = data.message + "..!"; 54 | }, 55 | }; 56 | 57 | window.addEventListener("message", (e) => { 58 | const handler = handlers[e.data.eventName]; 59 | 60 | if (handler) { 61 | handler(e.data); 62 | } 63 | }); 64 | } 65 | })(); 66 | -------------------------------------------------------------------------------- /src/shared/events/local.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/client"; 3 | import "@citizenfx/server"; 4 | 5 | import type { NoFunction } from "@shared/utils/base"; 6 | import type { EmitData } from "@shared/events/base"; 7 | 8 | import EventBase from "@shared/events/base"; 9 | 10 | enum EventType { 11 | EVENT_HANDLER = "natuna:local:eventHandler", 12 | } 13 | 14 | export default class LocalEvent extends EventBase { 15 | constructor() { 16 | super(); 17 | 18 | on(EventType.EVENT_HANDLER, (props: EmitData) => { 19 | const listeners = this.$listeners.filter((listener) => listener.name === props.name); 20 | 21 | for (const listener of listeners) { 22 | const value = listener.handler(props.args); 23 | 24 | this.$callbackValues.push({ 25 | uniqueId: props.uniqueId, 26 | values: value ?? null, 27 | }); 28 | } 29 | }); 30 | } 31 | 32 | /** 33 | * @description 34 | * Emit an event locally (Client to Client or Server to Server) 35 | */ 36 | emit = async (name: string | string[], args?: NoFunction[]) => { 37 | name = this.$validateEventName(name); 38 | const uniqueId = Math.random().toString(36).substring(2); 39 | 40 | for (const alias of name) { 41 | const emitData: EmitData = { 42 | name: alias, 43 | uniqueId, 44 | args, 45 | }; 46 | 47 | emit(EventType.EVENT_HANDLER, emitData); 48 | } 49 | 50 | let callbackValues = this.$callbackValues.findIndex((data) => data.uniqueId === uniqueId); 51 | 52 | while (callbackValues === -1) { 53 | await new Promise((resolve) => setTimeout(resolve, 1000)); 54 | callbackValues = this.$callbackValues.findIndex((data) => data.uniqueId === uniqueId); 55 | } 56 | 57 | const returnValue = this.$callbackValues[callbackValues].values; 58 | 59 | // Remove the callback values from the array 60 | this.$callbackValues.splice(callbackValues, 1); 61 | 62 | return returnValue; 63 | }; 64 | 65 | /** 66 | * @description 67 | * Listen from a local event 68 | */ 69 | listen = (name: string | string[], handler: (...args: any) => any) => { 70 | name = this.$validateEventName(name); 71 | let ids: number[] = []; 72 | 73 | for (const alias of name) { 74 | this.$listeners.push({ 75 | id: this.$listenerCounter, 76 | name: alias, 77 | handler, 78 | }); 79 | 80 | ids.push(this.$listenerCounter); 81 | 82 | this.$listenerCounter++; 83 | } 84 | 85 | return ids; 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /src/shared/utils/base.ts: -------------------------------------------------------------------------------- 1 | export type NoFunction = T extends (...args: any) => any ? never : T; 2 | 3 | export default class UtilsBase { 4 | /** 5 | * @description 6 | * Cool Natuna Ascii Art 7 | */ 8 | asciiArt = [ 9 | "░░░ ░░ ░░░░░ ░░░░░░░░ ░░ ░░ ░░░ ░░ ░░░░░ ", 10 | "▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ ▒▒ ▒▒ ▒▒", 11 | "▒▒ ▒▒ ▒▒ ▒▒▒▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒▒▒▒", 12 | "▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓", 13 | "██ ████ ██ ██ ██ ██████ ██ ████ ██ ██", 14 | ].join("\n"); 15 | 16 | /** 17 | * @description 18 | * Uppercase first letter of each words 19 | * 20 | * @example 21 | * ```ts 22 | * ucwords('hello world'); // Hello World 23 | * ``` 24 | */ 25 | ucwords = (str: string) => { 26 | return str.toLowerCase().replace(/\b[a-z]/g, (letter) => letter.toUpperCase()); 27 | }; 28 | 29 | /** 30 | * @description 31 | * Use this function to hold next script below this from executing before it finish the timeout itself 32 | * 33 | * @example 34 | * ```ts 35 | * const isActive = false; 36 | * setTimeout(() => (isActive = true), 3000) 37 | * 38 | * console.log(isActive); // false 39 | * 40 | * while(!isActive) { 41 | * await sleep(5000) 42 | * }; 43 | * 44 | * console.log(isActive); // true 45 | * ``` 46 | */ 47 | sleep = (ms: number): Promise => new Promise((res) => setTimeout(res, ms)); 48 | 49 | generateUniqueId = () => { 50 | return Date.now().toString(36) + Math.random().toString(36).substring(2); 51 | }; 52 | 53 | getHashString = (val: string) => { 54 | let hash = 0; 55 | let string = val.toLowerCase(); 56 | 57 | for (let i = 0; i < string.length; i++) { 58 | let letter = string[i].charCodeAt(0); 59 | hash = hash + letter; 60 | hash += (hash << 10) >>> 0; 61 | hash ^= hash >>> 6; 62 | hash = hash >>> 0; 63 | } 64 | 65 | hash += hash << 3; 66 | 67 | if (hash < 0) hash = hash >>> 0; 68 | 69 | hash ^= hash >>> 11; 70 | hash += hash << 15; 71 | 72 | if (hash < 0) hash = hash >>> 0; 73 | 74 | return hash.toString(16).toUpperCase(); 75 | }; 76 | 77 | validator = { 78 | isObject: (obj: Record) => { 79 | if (!obj || typeof obj == "undefined" || typeof obj !== "object" || Array.isArray(obj)) { 80 | return false; 81 | } 82 | return true; 83 | }, 84 | isArray: (arr: any[]) => { 85 | if (!arr || typeof arr == "undefined" || !Array.isArray(arr)) { 86 | return false; 87 | } 88 | return true; 89 | }, 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /src/client/players.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/client"; 3 | 4 | import type Events from "@client/events"; 5 | import type Game from "@client/game"; 6 | import type { Query, Data } from "@server/players"; 7 | 8 | import { SharedEventType } from "@shared/events/type"; 9 | 10 | export default class Players { 11 | constructor( 12 | private events: Events, // 13 | private game: Game 14 | ) { 15 | this.events.shared.listen(SharedEventType.UPDATED_DATA_BROADCAST, (players: Data[]) => { 16 | this.#list = players; 17 | }); 18 | } 19 | 20 | #list: Data[] = []; 21 | 22 | /** 23 | * @description 24 | * List all players 25 | * 26 | * **[IMPORTANT]** Don't use this function on Tick/Interval! 27 | * 28 | * @example 29 | * ```ts 30 | * await list(); 31 | * ``` 32 | */ 33 | listAll = () => { 34 | // reduce is fast rather than for loop or map 35 | return this.#list.reduce((data, player) => { 36 | const ped = GetPlayerPed(GetPlayerFromServerId(player.server_id)); 37 | 38 | player.last_position = this.game.entity.getCoords(ped); 39 | data.push(player); 40 | 41 | return data; 42 | }, []); 43 | }; 44 | 45 | /** 46 | * @description 47 | * Received current data of a player 48 | * 49 | * **[IMPORTANT]** Don't use this function on Tick/Interval! 50 | * 51 | * @param obj Data object to input 52 | * 53 | * @example 54 | * ```ts 55 | * await get({ 56 | * where: { 57 | * steam_id: "76561198290395137" 58 | * } 59 | * }); 60 | * ``` 61 | */ 62 | get = (query: Query) => { 63 | const data = this.listAll().find((player: Data) => { 64 | if (query.license) { 65 | return player.license === query.license; 66 | } else if (query.server_id) { 67 | return player.server_id === query.server_id; 68 | } else if (query.user_id) { 69 | return player.user_id === query.user_id; 70 | } 71 | }); 72 | 73 | return data; 74 | }; 75 | 76 | /** 77 | * @description 78 | * Update current data of a player 79 | * 80 | * **[IMPORTANT]** Don't use this function on Tick/Interval! 81 | * 82 | * @param data Data object to input 83 | * 84 | * @example 85 | * ```ts 86 | * await update({ 87 | * data: { 88 | * someNestedThings: true 89 | * }, 90 | * where: { 91 | * steam_id: "76561198290395137" 92 | * } 93 | * }); 94 | * ``` 95 | */ 96 | update = async (data: Data) => { 97 | return await this.events.shared.emit(SharedEventType.CURRENT_PLAYER_UPDATE, [data]); 98 | }; 99 | } 100 | -------------------------------------------------------------------------------- /src/client/events/shared.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/client"; 3 | 4 | import type { NoFunction } from "@shared/utils/base"; 5 | import type { CallbackValueData, EmitData } from "@shared/events/base"; 6 | 7 | import EventBase from "@shared/events/base"; 8 | import { SharedEventType } from "@shared/events/type"; 9 | 10 | export default class SharedEvent extends EventBase { 11 | constructor() { 12 | super(); 13 | 14 | onNet(SharedEventType.CLIENT_EVENT_HANDLER, (props: EmitData) => { 15 | const listeners = this.$listeners.filter((listener) => listener.name === props.name); 16 | 17 | for (const listener of listeners) { 18 | const value = listener.handler(props.args); 19 | 20 | emitNet(SharedEventType.SERVER_CALLBACK_RECEIVER, { 21 | uniqueId: props.uniqueId, 22 | values: value ?? null, 23 | }); 24 | } 25 | }); 26 | 27 | onNet(SharedEventType.CLIENT_CALLBACK_RECEIVER, (data: CallbackValueData) => { 28 | this.$callbackValues.push(data); 29 | }); 30 | } 31 | 32 | /** 33 | * @description 34 | * Emit an event from Client to Server 35 | */ 36 | emit = async (name: string | string[], args?: NoFunction[]): Promise => { 37 | name = this.$validateEventName(name); 38 | const uniqueId = Math.random().toString(36).substring(2); 39 | 40 | for (const alias of name) { 41 | const emitData: EmitData = { 42 | name: alias, 43 | uniqueId, 44 | args, 45 | }; 46 | 47 | emitNet(SharedEventType.SERVER_EVENT_HANDLER, emitData); 48 | } 49 | 50 | let callbackValues = this.$callbackValues.findIndex((data) => data.uniqueId === uniqueId); 51 | 52 | while (callbackValues === -1) { 53 | await new Promise((resolve) => setTimeout(resolve, 1000)); 54 | callbackValues = this.$callbackValues.findIndex((data) => data.uniqueId === uniqueId); 55 | } 56 | 57 | const returnValue = this.$callbackValues[callbackValues].values; 58 | 59 | // Remove the callback values from the array 60 | this.$callbackValues.splice(callbackValues, 1); 61 | 62 | return returnValue; 63 | }; 64 | 65 | /** 66 | * @description 67 | * Listen from a Server event 68 | */ 69 | listen = (name: string | string[], handler: (...args: any) => any) => { 70 | name = this.$validateEventName(name); 71 | let ids: number[] = []; 72 | 73 | for (const alias of name) { 74 | this.$listeners.push({ 75 | id: this.$listenerCounter, 76 | name: alias, 77 | handler, 78 | }); 79 | 80 | ids.push(this.$listenerCounter); 81 | 82 | this.$listenerCounter++; 83 | } 84 | 85 | return ids; 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /src/server/events/shared.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/server"; 3 | 4 | import type { NoFunction } from "@shared/utils/base"; 5 | import type { CallbackValueData, EmitData } from "@shared/events/base"; 6 | 7 | import EventBase from "@shared/events/base"; 8 | import { SharedEventType } from "@shared/events/type"; 9 | 10 | export default class SharedEvent extends EventBase { 11 | constructor() { 12 | super(); 13 | 14 | onNet(SharedEventType.SERVER_EVENT_HANDLER, (props: EmitData) => { 15 | const listeners = this.$listeners.filter((listener) => listener.name === props.name); 16 | 17 | for (const listener of listeners) { 18 | const value = listener.handler(globalThis.source, props.args); 19 | 20 | emitNet(SharedEventType.CLIENT_CALLBACK_RECEIVER, globalThis.source, { 21 | uniqueId: props.uniqueId, 22 | values: value ?? null, 23 | }); 24 | } 25 | }); 26 | 27 | onNet(SharedEventType.SERVER_CALLBACK_RECEIVER, (data: CallbackValueData) => { 28 | this.$callbackValues.push(data); 29 | }); 30 | } 31 | 32 | /** 33 | * @description 34 | * Emit an event from Server to Client 35 | */ 36 | emit = async (name: string | string[], target: number | "global", args?: NoFunction[]) => { 37 | name = this.$validateEventName(name); 38 | const uniqueId = Math.random().toString(36).substring(2); 39 | 40 | if (target === "global") { 41 | target = -1; 42 | } 43 | 44 | for (const alias of name) { 45 | const emitData: EmitData = { 46 | name: alias, 47 | uniqueId, 48 | args, 49 | }; 50 | 51 | emitNet(SharedEventType.CLIENT_EVENT_HANDLER, target, emitData); 52 | } 53 | 54 | let callbackValues = this.$callbackValues.findIndex((data) => data.uniqueId === uniqueId); 55 | 56 | while (callbackValues === -1) { 57 | await new Promise((resolve) => setTimeout(resolve, 1000)); 58 | callbackValues = this.$callbackValues.findIndex((data) => data.uniqueId === uniqueId); 59 | } 60 | 61 | const returnValue = this.$callbackValues[callbackValues].values; 62 | 63 | // Remove the callback values from the array 64 | this.$callbackValues.splice(callbackValues, 1); 65 | 66 | return returnValue; 67 | }; 68 | 69 | /** 70 | * @description 71 | * Listen from a Client event 72 | */ 73 | listen = (name: string | string[], handler: (source: number, ...args: any) => any) => { 74 | name = this.$validateEventName(name); 75 | let ids: number[] = []; 76 | 77 | for (const alias of name) { 78 | this.$listeners.push({ 79 | id: this.$listenerCounter, 80 | name: alias, 81 | handler, 82 | }); 83 | 84 | ids.push(this.$listenerCounter); 85 | 86 | this.$listenerCounter++; 87 | } 88 | 89 | return ids; 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /docs/GETTING_STARTED.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | Interested using and developing this framework? Let's begin... 3 | 1. [Installing](#installing-it) 4 | 2. [Updating](#updating) 5 | 3. [Frequently Asked Questions](#frequently-asked-questions) 6 | 7 | --- 8 | 9 | ## Installing It 10 | ### - Requirements 11 | 1. Basic knowledge of Command Prompt 12 | 2. Basic knowledge of Git commands 13 | 2. Basic knowledge of MYSQL/SQL database 14 | 15 | ### - Steps 16 | 1. **Clone this repository to your local machine**
17 | Make sure you put it on FiveM resources folder and rename it to `natuna` or something else, also don't forget to add this resource to `server.cfg` file. 18 | 19 | > We suggest you to clone this repository using `git` instead of downloading this repository as a .zip or .tar since it'd help you update this framework more easily using `git pull` command later. 20 | 21 | 2. **Open command prompt and locate to this resource folder**
22 | Use `cd` command to locate to the cloned folder on your local machine. 23 | 24 | 4. **Edit the configuration file at `package.json`**
25 | You can change almost everything, but Natuna Framework configuration is located under `natuna` variable. 26 | 27 | 5. **Copy the `.env.example` file to `.env`**
28 | Copy or just rename it, you choose. 29 | 30 | 6. **Edit the `.env` file**
31 | Env file is used to store your secret keys, database credentials, etc. 32 | 33 | 7. **Install all dependencies**
34 | Use `npm install` or `yarn` command to install all dependencies. 35 | 36 | 8. **Migrate and generate the database schema**
37 | Run the database migration first before generating the schema. 38 | To read and learn about this, please go to [BUILDING.md](BUILDING.md). 39 | 40 | 9. **Build the files**
41 | **Run the steps 8 first before running this step.** 42 | To read and learn about building this files, please go to [BUILDING.md](BUILDING.md). 43 | 44 | 10. **Start your FiveM server**
45 | When you starting the server, you will found out a message saying 46 | 47 | - `Couldn't start resource natuna.` and 48 | - `Running build tasks on natuna - it'll restart once completed` 49 | 50 | It happens because FiveM is installing the project dependencies into FiveM local node_modules folder. Just wait until it's finished. 51 | 52 | ## Updating 53 | Everytime this resource is starting, you would find a log on console saying whether your version of this resource are outdated or not. If your version was outdated, please update your version using `git pull` command on this resource folder. 54 | 55 | ## Frequently Asked Questions 56 | 57 | 1. **Why is my character not spawning after i join my server?**
58 | 59 | That because this framework currently has no built-in character maker/selector. Since this framework still on development, you should start making your own and we hope you can contribute to this current project. 60 | 61 | 2. **Why is there isn't anything?**
62 | 63 | This project is completely a base system, known as framework. It's not a game. It's a base system that you can use to build your own game. So you need to add a plugin to this framework to make your game. 64 | 65 | Currently this framework is still in development, so the plugin is still in development. 66 | 67 | 3. **How do i create a plugin or ui?**
68 | 69 | Coming soon~ 70 | -------------------------------------------------------------------------------- /src/server/manager/deferralsManager.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/server"; 3 | 4 | import type Logger from "@ptkdev/logger"; 5 | 6 | import Database from "@server/database"; 7 | import Players from "@server/players"; 8 | 9 | export default class DeferralsManager { 10 | constructor( 11 | source: number, // 12 | private db: ReturnType, 13 | private isWhitelisted: boolean, 14 | private players: Players, 15 | private name: string, 16 | private deferrals: Record, 17 | private logger: Logger 18 | ) { 19 | this.#playerIds = this.players.utils.getIdentifiers(source); 20 | 21 | (async () => { 22 | this.deferrals.defer(); 23 | deferrals.update(`[🏝 Natuna] Hello ${this.name}! Please wait until we verify your account.`); 24 | this.#checkLicense(); 25 | await this.#checkWhitelist(); 26 | 27 | this.logger.debug(`Player ${this.name} Joining the server. (License ID: ${this.#playerIds.license})`); 28 | deferrals.update(`[🏝 Natuna] Finding your account in our database.`); 29 | await this.#checkAccount(); 30 | 31 | setImmediate(() => deferrals.done()); 32 | })(); 33 | } 34 | 35 | #playerIds: ReturnType; 36 | 37 | #checkLicense = () => { 38 | if (!this.#playerIds.license || typeof this.#playerIds.license == "undefined") { 39 | return this.deferrals.done("[🏝 Natuna] Your game license is invalid!"); 40 | } 41 | }; 42 | 43 | #checkWhitelist = async () => { 44 | if (this.isWhitelisted) { 45 | const checkWhitelistStatus = await this.db("whitelist_lists").findFirst({ 46 | where: { 47 | license: this.#playerIds.license, 48 | }, 49 | }); 50 | 51 | if (!checkWhitelistStatus) { 52 | return this.deferrals.done("[🏝 Natuna] You are not whitelisted!"); 53 | } 54 | } 55 | }; 56 | 57 | #checkAccount = async () => { 58 | const user = await this.db("users").findFirst({ 59 | where: { 60 | license: this.#playerIds.license, 61 | }, 62 | }); 63 | 64 | const newCheckpointData = { 65 | last_ip: this.#playerIds.ip.toString(), 66 | last_login: new Date().toLocaleString("en-US", { timeZone: process.env.TZ }).toString(), 67 | }; 68 | 69 | switch (!user) { 70 | case true: 71 | await this.db("users").create({ 72 | data: { 73 | license: this.#playerIds.license, 74 | ...newCheckpointData, 75 | }, 76 | }); 77 | 78 | break; 79 | 80 | case false: 81 | // Check if user was banned 82 | const checkBanStatus = await this.db("ban_lists").findFirst({ 83 | where: { 84 | license: this.#playerIds.license, 85 | }, 86 | }); 87 | 88 | if (checkBanStatus) { 89 | return this.deferrals.done(checkBanStatus.reason); 90 | } 91 | 92 | // If not 93 | await this.db("users").update({ 94 | data: { 95 | ...newCheckpointData, 96 | }, 97 | where: { 98 | license: this.#playerIds.license, 99 | }, 100 | }); 101 | } 102 | }; 103 | } 104 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/client"; 3 | 4 | import type { Config } from "@server"; 5 | 6 | import Utils from "@client/utils"; 7 | import Game from "@client/game"; 8 | import Players from "@client/players"; 9 | import Events from "@client/events"; 10 | import Manager from "@client/manager"; 11 | 12 | import { SharedEventType } from "@shared/events/type"; 13 | 14 | export default class Client { 15 | constructor() { 16 | on("onClientResourceStart", this.#onClientResourceStart); 17 | } 18 | 19 | config: Config = {}; 20 | utils: Utils = new Utils(); 21 | events: Events = new Events(); 22 | game: Game = new Game(this.utils); 23 | manager: Manager = new Manager(this.events, this.utils); 24 | players: Players = new Players(this.events, this.game); 25 | 26 | #onClientResourceStart = async (resourceName: string) => { 27 | if (resourceName == GetCurrentResourceName()) { 28 | this.events.shared.emit(SharedEventType.GET_CLIENT_CONFIG).then((config) => { 29 | console.log("Starting Client..."); 30 | console.log(this.utils.asciiArt); 31 | 32 | this.config = config; 33 | this.#setGameSettings(); 34 | this.#setDiscordRPC(); 35 | 36 | console.log("Client Ready!"); 37 | }); 38 | } 39 | }; 40 | 41 | #setGameSettings = async () => { 42 | const GAME = this.config.game; 43 | 44 | this.manager.tick.set(() => { 45 | if (GAME.noDispatchService) { 46 | this.game.disableDispatchService(); 47 | } 48 | 49 | if (GAME.noWantedLevel) { 50 | this.game.resetWantedLevel(); 51 | } 52 | }); 53 | 54 | if (GAME.pauseMenuTitle) { 55 | AddTextEntry("FE_THDR_GTAO", GAME.pauseMenuTitle); 56 | } 57 | 58 | if (!GAME.autoRespawnDisabled) { 59 | globalThis.exports.spawnmanager.setAutoSpawn(true); 60 | globalThis.exports.spawnmanager.spawnPlayer({ 61 | x: 466.8401, 62 | y: 197.7201, 63 | z: 111.5291, 64 | heading: 291.71, 65 | model: "a_m_m_farmer_01", 66 | skipFade: false, 67 | }); 68 | } else { 69 | globalThis.exports.spawnmanager.setAutoSpawn(false); 70 | } 71 | }; 72 | 73 | #setDiscordRPC = () => { 74 | const RPC = this.config.discordRPC; 75 | SetDiscordAppId(RPC.appId); 76 | 77 | const parseRPCString = (string: string) => { 78 | return string 79 | .replace(/{{PLAYER_NAME}}/g, GetPlayerName(PlayerId())) // Player Name 80 | .replace(/{{TOTAL_ACTIVE_PLAYERS}}/g, String(this.players.listAll().length)); // Total Active Player 81 | }; 82 | 83 | const setRPC = () => { 84 | SetRichPresence(parseRPCString(RPC.text)); 85 | 86 | SetDiscordRichPresenceAsset(RPC.largeImage.assetName); 87 | SetDiscordRichPresenceAssetText(parseRPCString(RPC.largeImage.hoverText)); 88 | 89 | SetDiscordRichPresenceAssetSmall(RPC.smallImage.assetName); 90 | SetDiscordRichPresenceAssetSmallText(parseRPCString(RPC.smallImage.hoverText)); 91 | 92 | if (RPC.buttons[0]) { 93 | SetDiscordRichPresenceAction(0, RPC.buttons[0].label, RPC.buttons[0].url); 94 | } 95 | 96 | if (RPC.buttons[1]) { 97 | SetDiscordRichPresenceAction(1, RPC.buttons[1].label, RPC.buttons[1].url); 98 | } 99 | }; 100 | 101 | setRPC(); 102 | setInterval(setRPC, RPC.refreshInterval * 1000); 103 | }; 104 | } 105 | 106 | const client = new Client(); 107 | globalThis.exports("client", client); 108 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/server"; 3 | 4 | import path from "path"; 5 | import * as dotenv from "dotenv"; 6 | 7 | globalThis.__natunaDirname = GetResourcePath(GetCurrentResourceName()); 8 | dotenv.config({ 9 | path: path.join(globalThis.__natunaDirname, ".env"), 10 | }); 11 | 12 | import pkg from "@/package.json"; 13 | import fetch from "node-fetch"; 14 | import Logger from "@ptkdev/logger"; 15 | 16 | import Players from "@server/players"; 17 | import Events from "@server/events"; 18 | import Utils from "@server/utils"; 19 | import Manager from "@server/manager"; 20 | import Database from "@server/database"; 21 | 22 | import DeferralsManager from "@server/manager/deferralsManager"; 23 | import { SharedEventType } from "@shared/events/type"; 24 | 25 | export type Config = Partial; 26 | 27 | export default class Server { 28 | constructor() { 29 | this.events.shared.listen(SharedEventType.GET_CLIENT_CONFIG, () => this.config); 30 | 31 | on("onServerResourceStart", this.#onServerResourceStart); 32 | on("playerConnecting", (...args: any) => { 33 | return new DeferralsManager(globalThis.source, this.db, this.config.whitelisted, this.players, args[0], args[2], this.logger); 34 | }); 35 | } 36 | 37 | config: Config = pkg.natuna; 38 | logger = new Logger({ 39 | language: "en", 40 | colors: true, 41 | debug: true, 42 | info: true, 43 | warning: true, 44 | error: true, 45 | sponsor: true, 46 | write: true, 47 | type: "json", 48 | rotate: { 49 | size: "10M", 50 | encoding: "utf8", 51 | }, 52 | path: { 53 | debug_log: path.posix.join(GetResourcePath(GetCurrentResourceName()), ".natuna", "logs", "debug.log"), 54 | error_log: path.posix.join(GetResourcePath(GetCurrentResourceName()), ".natuna", "logs", "errors.log"), 55 | }, 56 | }); 57 | 58 | events: Events = new Events(); 59 | utils: Utils = new Utils(); 60 | db: ReturnType = Database(this.logger); 61 | players: Players = new Players(this.db, this.events); 62 | manager: Manager = new Manager(this.events, this.players, this.utils); 63 | 64 | #checkPackageVersion = () => { 65 | return new Promise((resolve) => { 66 | this.logger.debug(`Welcome! Checking your version...`); 67 | this.logger.debug(`You are currently using version ${pkg.version}.`); 68 | 69 | // Can't use async await, idk why :( 70 | fetch("https://raw.githack.com/natuna-framework/fivem/master/package.json") 71 | .then((res) => res.text()) 72 | .then((data) => { 73 | const rpkg: typeof pkg = JSON.parse(data); 74 | 75 | const currentVersion = parseInt(pkg.version.replace(/\./g, "")); 76 | const remoteVersion = parseInt(rpkg.version.replace(/\./g, "")); 77 | 78 | switch (true) { 79 | case currentVersion < remoteVersion: 80 | this.logger.warning(`You are not using the latest version of Natuna Framework (${rpkg.version}), please update it!`); 81 | break; 82 | case currentVersion > remoteVersion: 83 | this.logger.error(`You are not using a valid version version of Natuna Framework!`); 84 | break; 85 | default: 86 | this.logger.info("You are using a latest version of Natuna Framework!"); 87 | } 88 | 89 | return resolve(true); 90 | }); 91 | }); 92 | }; 93 | 94 | #onServerResourceStart = async (resourceName: string) => { 95 | if (GetCurrentResourceName() == resourceName) { 96 | console.log(this.utils.asciiArt); 97 | this.logger.debug("Starting Server..."); 98 | 99 | await this.#checkPackageVersion(); 100 | 101 | this.logger.info("Server Ready!"); 102 | } 103 | }; 104 | } 105 | 106 | const server = new Server(); 107 | globalThis.exports("server", server); 108 | -------------------------------------------------------------------------------- /.natuna/scripts/db/mysql/index.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const mysql = require("mysql2"); 4 | 5 | module.exports.migrate = async (process) => { 6 | const connection = mysql.createPool({ 7 | isServer: false, 8 | host: process.env.DATABASE_HOST, 9 | user: process.env.DATABASE_USER, 10 | database: process.env.DATABASE_NAME, 11 | port: Number(process.env.DATABASE_PORT), 12 | waitForConnections: true, 13 | connectionLimit: 10, 14 | queueLimit: 0, 15 | }); 16 | 17 | const migrationFilePath = path.join(__dirname, "..", "mysql", "migration.sql"); 18 | const sql = fs 19 | .readFileSync(migrationFilePath, "utf8") 20 | .toString() 21 | .replace(/(\r\n|\n|\r)/gm, " ") // remove newlines 22 | .replace(/\s+/g, " ") // excess white space 23 | .split(";") // split into all statements 24 | .map(Function.prototype.call, String.prototype.trim) 25 | .filter(function (el) { 26 | return el.length != 0; 27 | }); // remove any empty ones 28 | 29 | for (const command of sql) { 30 | console.log(`Executing: ${command}`); 31 | 32 | const result = await new Promise((res, rej) => { 33 | connection.query(command, (err, results, fields) => { 34 | if (err) { 35 | rej(err); 36 | } 37 | 38 | res(results); 39 | }); 40 | }); 41 | 42 | console.log(result); 43 | } 44 | 45 | console.log("Database migration completed."); 46 | process.exit(0); 47 | }; 48 | 49 | module.exports.createSchema = () => { 50 | let types = ""; 51 | const typesMap = {}; 52 | 53 | const migrationFilePath = path.join(__dirname, "..", "mysql", "migration.sql"); 54 | 55 | const sqlArray = fs 56 | .readFileSync(migrationFilePath, "utf8") 57 | .toString() 58 | .replace(/(\r\n|\n|\r)/gm, " ") // remove newlines 59 | .replace(/\s+/g, " ") // excess white space 60 | .split(";") // split into all statements 61 | .map(Function.prototype.call, String.prototype.trim) 62 | .filter(function (el) { 63 | return el.length != 0; 64 | }); // remove any empty ones 65 | 66 | sqlArray.map((sql) => { 67 | const match = sql.match(/^CREATE TABLE .*? `(.*?)`/); 68 | 69 | if (match) { 70 | const tableName = match[1]; 71 | typesMap[tableName] = {}; 72 | 73 | const rows = sql.match(/`([a-zA-Z_]+)`\s([a-zA-Z_]+)/g); 74 | if (rows) { 75 | rows.map((row) => { 76 | let [name, type] = row.split(" "); 77 | 78 | switch (type) { 79 | case "int": 80 | case "tinyint": 81 | case "smallint": 82 | case "mediumint": 83 | case "bigint": 84 | case "float": 85 | case "double": 86 | case "decimal": 87 | case "bit": 88 | case "bool": 89 | case "boolean": 90 | type = "number"; 91 | break; 92 | 93 | case "varchar": 94 | case "text": 95 | case "longtext": 96 | case "char": 97 | case "tinytext": 98 | case "mediumtext": 99 | case "enum": 100 | case "set": 101 | type = "string"; 102 | break; 103 | } 104 | 105 | typesMap[tableName][name.replace(/`/g, "")] = type; 106 | }); 107 | } 108 | } 109 | }); 110 | 111 | for (const [tableName, rows] of Object.entries(typesMap)) { 112 | types += `${tableName}: {\n`; 113 | 114 | for (const [rowName, rowType] of Object.entries(rows)) { 115 | types += `${rowName}: ${rowType};`; 116 | } 117 | 118 | types += `};`; 119 | } 120 | 121 | return types; 122 | }; 123 | -------------------------------------------------------------------------------- /src/ui/loading_screen/stylesheet.css: -------------------------------------------------------------------------------- 1 | /* FONTS 2 | ================== */ 3 | 4 | @font-face { 5 | font-family: Renner; 6 | src: url(./assets/renner-book.otf); 7 | font-weight: normal; 8 | } 9 | 10 | @font-face { 11 | font-family: Renner; 12 | src: url(./assets/renner-bold.otf); 13 | font-weight: bold; 14 | } 15 | 16 | /* BASE 17 | ================== */ 18 | 19 | html, 20 | body { 21 | height: 100%; 22 | width: 100%; 23 | margin: 0 auto; 24 | padding: 0; 25 | animation: BackgroundShrink 10s infinite alternate; 26 | position: relative; 27 | overflow: hidden; 28 | background-size: 110% 110%; 29 | background-position: center center; 30 | background: url(https://steamuserimages-a.akamaihd.net/ugc/1666853488464316494/A3BE3989F12E1038E3CDAD7A1157F164B4492B6D/?imw=5000&imh=5000&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=false); 31 | } 32 | 33 | .blocker { 34 | position: absolute; 35 | width: 100%; 36 | left: 0; 37 | background-color: black; 38 | z-index: 2; 39 | height: 6rem; 40 | animation: CinemaStarting 5s; 41 | } 42 | 43 | /* HEADER (BLOCKER BLACK TOP) 44 | ================== */ 45 | 46 | header { 47 | top: 0; 48 | } 49 | 50 | /* MAIN (MIDDLE) 51 | ================== */ 52 | 53 | main { 54 | position: relative; 55 | display: flex; 56 | align-items: center; 57 | justify-content: center; 58 | flex-direction: column; 59 | height: 100%; 60 | width: 100%; 61 | z-index: 1; 62 | background: linear-gradient(0deg, rgba(254, 187, 98, 0.42), rgba(254, 187, 98, 0.42)); 63 | } 64 | 65 | main > .logo { 66 | max-height: 24rem; 67 | filter: drop-shadow(0 0.25rem 0.25rem rgba(0, 0, 0, 0.6)); 68 | object-fit: contain; 69 | } 70 | 71 | main > .subtitle { 72 | padding-top: 3.5rem; 73 | font-family: Renner; 74 | font-weight: bold; 75 | font-style: normal; 76 | color: white; 77 | letter-spacing: 0.05em; 78 | font-size: 1rem; 79 | text-shadow: 0 0.25rem 0.25rem rgba(0, 0, 0, 0.4); 80 | } 81 | 82 | /* FOOTER (BLACK BLOCKER BOTTOM) 83 | ================== */ 84 | 85 | footer { 86 | bottom: 0; 87 | display: flex; 88 | align-items: center; 89 | justify-content: space-between; 90 | flex-direction: row; 91 | padding: 0 2rem; 92 | box-sizing: border-box; 93 | } 94 | 95 | footer > section { 96 | animation: FadeIn 7s; 97 | } 98 | 99 | /* -- Loader */ 100 | footer > .loader { 101 | display: flex; 102 | flex-direction: row; 103 | justify-content: center; 104 | align-items: center; 105 | background: #c47f57; 106 | border-radius: 0.75rem; 107 | padding: 0 1.25rem; 108 | } 109 | 110 | footer > .loader > svg { 111 | height: 1.125rem; 112 | fill: #333333; 113 | } 114 | 115 | footer > .loader > p { 116 | font-family: Renner; 117 | font-weight: bold; 118 | padding: 0 0.875rem 0 0.5rem; 119 | color: #333333; 120 | } 121 | 122 | footer > .loader > .progress-bar { 123 | width: 7rem; 124 | height: 0.5rem; 125 | background: rgba(0, 0, 0, 0.5); 126 | border-radius: 0.25rem; 127 | } 128 | 129 | footer > .loader > .progress-bar > .progress-bar-filled { 130 | background: #333333; 131 | border-radius: 0.25rem; 132 | width: 0%; 133 | height: 100%; 134 | transition: width 1s; 135 | } 136 | 137 | /* -- Credits */ 138 | footer > .credits { 139 | text-align: right; 140 | display: flex; 141 | justify-content: center; 142 | align-items: flex-end; 143 | flex-direction: column; 144 | } 145 | 146 | footer > .credits > p { 147 | font-family: Renner; 148 | font-size: 0.875rem; 149 | line-height: 1.25rem; 150 | color: white; 151 | font-weight: bold; 152 | padding: 0; 153 | margin: 0; 154 | padding-bottom: 0.25rem; 155 | letter-spacing: 0.02rem; 156 | } 157 | 158 | footer > .credits > a { 159 | font-family: Renner; 160 | font-size: 0.75rem; 161 | line-height: 1rem; 162 | color: white; 163 | opacity: 0.6; 164 | letter-spacing: 0.02rem; 165 | } 166 | 167 | /* KEYFRAMES 168 | ================== */ 169 | 170 | @keyframes CinemaStarting { 171 | 0% { 172 | height: 0; 173 | } 174 | 100% { 175 | height: 6rem; 176 | } 177 | } 178 | 179 | @keyframes FadeIn { 180 | from { 181 | opacity: 0; 182 | } 183 | to { 184 | opacity: 1; 185 | } 186 | } 187 | 188 | @keyframes BackgroundShrink { 189 | 0% { 190 | background-size: 110% 110%; 191 | } 192 | 100% { 193 | background-size: 100% 100%; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*"], 3 | "exclude": ["**/node_modules", "**/__tests__/*"], 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "paths": { 7 | "*": ["types/*"], 8 | "@server": ["src/server/index.ts"], 9 | "@server/*": ["src/server/*"], 10 | "@client": ["src/client/index.ts"], 11 | "@client/*": ["src/client/*"], 12 | "@shared": ["src/shared/index.ts"], 13 | "@shared/*": ["src/shared/*"], 14 | "@/*": ["./*"] 15 | }, 16 | "outDir": "dist", 17 | "noImplicitAny": true, 18 | "module": "commonjs", 19 | "target": "es6", 20 | "allowJs": true, 21 | "lib": ["es2017"], 22 | "types": [], 23 | "moduleResolution": "node", 24 | "resolveJsonModule": true, 25 | "esModuleInterop": true, 26 | "alwaysStrict": true, 27 | "skipLibCheck": true, 28 | "removeComments": true, 29 | "pretty": true 30 | }, 31 | "typedocOptions": { 32 | "name": "Natuna Framework", 33 | "theme": "default", 34 | "out": "public/fivem", 35 | "entryPoints": ["src/client", "src/server", "src/shared"], 36 | "entryPointStrategy": "expand", 37 | "includeVersion": true, 38 | "customCss": "./public/assets/typedoc-custom.css", 39 | "mergeModulesRenameDefaults": true, 40 | "mergeModulesMergeMode": "module", 41 | "excludeConstructors": true, 42 | "excludePrivate": true, 43 | "excludeProtected": true, 44 | "removePrimaryNavigation": true, 45 | "removeSecondaryNavigation": false, 46 | "staticMarkdownDocs": [ 47 | { 48 | "pageUrl": "/docs/getting-started", 49 | "filePath": "./docs/GETTING_STARTED.md" 50 | }, 51 | { 52 | "pageUrl": "/docs/building-files", 53 | "filePath": "./docs/BUILDING.md" 54 | }, 55 | { 56 | "pageUrl": "/docs/code-of-conduct", 57 | "filePath": "./docs/CODE_OF_CONDUCT.md" 58 | }, 59 | { 60 | "pageUrl": "/docs/contributing", 61 | "filePath": "./docs/CONTRIBUTING.md" 62 | } 63 | ], 64 | "customNavigations": [ 65 | { 66 | "title": "🔗 Navigations", 67 | "links": [ 68 | { 69 | "label": "🏠 Home", 70 | "href": "/index.html" 71 | }, 72 | { 73 | "label": "🤝 Discord Server", 74 | "href": "https://discord.gg/kGPHBvXzGM" 75 | }, 76 | { 77 | "label": "⛺ Client Functions", 78 | "href": "/classes/client.Client.html" 79 | }, 80 | { 81 | "label": "💻 Server Functions", 82 | "href": "/classes/server.Server.html" 83 | }, 84 | { 85 | "label": "📦 All Module List", 86 | "href": "/modules.html" 87 | } 88 | ] 89 | }, 90 | { 91 | "title": "📚 Documentations", 92 | "links": [ 93 | { 94 | "label": "💪 Getting Started", 95 | "href": "/docs/getting-started.html" 96 | }, 97 | { 98 | "label": "🔧 Building Files", 99 | "href": "/docs/building-files.html" 100 | }, 101 | { 102 | "label": "🤵 Code of Conduct", 103 | "href": "/docs/code-of-conduct.html" 104 | }, 105 | { 106 | "label": "🎁 Contributing to The Project", 107 | "href": "/docs/contributing.html" 108 | } 109 | ] 110 | } 111 | ], 112 | "markdownFilesContentReplacement": [ 113 | { 114 | "content": "BUILDING.md", 115 | "replacement": "building-files.html" 116 | }, 117 | { 118 | "content": "CODE_OF_CONDUCT.md", 119 | "replacement": "code-of-conduct.html" 120 | }, 121 | { 122 | "content": "CONTRIBUTING.md", 123 | "replacement": "contributing.html" 124 | }, 125 | { 126 | "content": "GETTING_STARTED.md", 127 | "replacement": "getting-started.html" 128 | } 129 | ] 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@natunaorg/fivem", 3 | "version": "1.5.0", 4 | "description": "FiveM Typescript/Javascript Bundled Framework with single module engine that runs on Javascript runtime. Powered with NodeJS.", 5 | "homepage": "https://fivem.natuna.runes.asia", 6 | "license": "MIT", 7 | "private": false, 8 | "main": "src/", 9 | "packageManager": "yarn@1.22.17", 10 | "repository": { 11 | "url": "https://github.com/natuna-framework/natuna", 12 | "type": "git" 13 | }, 14 | "author": { 15 | "name": "Runes", 16 | "email": "team@runes.asia", 17 | "url": "https://runes.asia" 18 | }, 19 | "maintainers": [ 20 | { 21 | "name": "Rafly Maulana", 22 | "email": "me@raflymaulana.me", 23 | "url": "https://raflymaulana.me" 24 | } 25 | ], 26 | "scripts": { 27 | "build": "node .natuna/esbuild.js", 28 | "build:prod": "yarn build --mode=production", 29 | "dev": "yarn build --watch", 30 | "lint": "eslint --ext .ts,.js .", 31 | "lint:fix": "yarn lint --fix", 32 | "db:create-schema": "node .natuna/scripts/db/create-schema.js", 33 | "db:migrate": "node .natuna/scripts/db/migrate.js", 34 | "db:setup": "yarn db:create-schema && yarn db:migrate" 35 | }, 36 | "dependencies": { 37 | "@citizenfx/client": "^2.0.4358-1", 38 | "@citizenfx/server": "^2.0.4249-1", 39 | "@ptkdev/logger": "^1.8.0", 40 | "dotenv": "^16.0.0", 41 | "esbuild": "^0.14.12", 42 | "mysql2": "^2.3.3", 43 | "node-fetch": "^2.6.1" 44 | }, 45 | "devDependencies": { 46 | "@types/node": "^17.0.10", 47 | "@types/node-fetch": "^2.5.12", 48 | "@typescript-eslint/eslint-plugin": "^5.10.1", 49 | "@typescript-eslint/parser": "^5.10.1", 50 | "eslint": "^8.7.0", 51 | "eslint-config-prettier": "^8.3.0", 52 | "eslint-plugin-prettier": "^4.0.0", 53 | "prettier": "^2.5.1", 54 | "typescript": "^4.5.5" 55 | }, 56 | "optionalDependencies": { 57 | "@wearerunes/typedoc-plugin": "^1.0.4", 58 | "typedoc": "^0.22.12", 59 | "typedoc-plugin-mdn-links": "^1.0.5", 60 | "typedoc-plugin-merge-modules": "^3.1.0", 61 | "typedoc-plugin-reference-excluder": "^1.0.0" 62 | }, 63 | "peerDependencies": { 64 | "typedoc": "^0.22.12" 65 | }, 66 | "prettier": { 67 | "trailingComma": "es5", 68 | "bracketSpacing": true, 69 | "arrowParens": "always", 70 | "endOfLine": "auto", 71 | "htmlWhitespaceSensitivity": "css", 72 | "bracketSameLine": true, 73 | "jsxSingleQuote": false, 74 | "printWidth": 200, 75 | "semi": true, 76 | "tabWidth": 4 77 | }, 78 | "eslintConfig": { 79 | "parser": "@typescript-eslint/parser", 80 | "plugins": [ 81 | "@typescript-eslint", 82 | "prettier" 83 | ], 84 | "extends": [ 85 | "eslint:recommended", 86 | "prettier", 87 | "plugin:@typescript-eslint/recommended" 88 | ], 89 | "parserOptions": { 90 | "sourceType": "module", 91 | "ecmaVersion": 2020 92 | }, 93 | "env": { 94 | "browser": true, 95 | "node": true 96 | }, 97 | "rules": { 98 | "no-case-declarations": "off", 99 | "no-empty-function": "off", 100 | "prefer-const": "off", 101 | "prettier/prettier": "error", 102 | "react/no-unescaped-entities": "off", 103 | "@typescript-eslint/no-explicit-any": "off" 104 | } 105 | }, 106 | "natuna": { 107 | "whitelisted": true, 108 | "game": { 109 | "autoRespawnDisabled": false, 110 | "noDispatchService": false, 111 | "noWantedLevel": true, 112 | "pauseMenuTitle": "Natuna Indonesia | discord.gg/kGPHBvXzGM" 113 | }, 114 | "discordRPC": { 115 | "text": "Playing on Natuna Indonesia", 116 | "appId": "866690121485910017", 117 | "refreshInterval": 30, 118 | "largeImage": { 119 | "assetName": "logo", 120 | "hoverText": "Natuna Framework" 121 | }, 122 | "smallImage": { 123 | "assetName": "indonesia", 124 | "hoverText": "Made in Indonesia" 125 | }, 126 | "buttons": [ 127 | { 128 | "label": "Discord", 129 | "url": "https://discord.gg/kGPHBvXzGM" 130 | }, 131 | { 132 | "label": "GitHub", 133 | "url": "fivem://connect/aasd.com/asdasd" 134 | } 135 | ] 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /.natuna/esbuild.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const esbuild = require("esbuild"); 3 | const path = require("path"); 4 | 5 | const isProduction = process.argv.findIndex((argItem) => argItem === "--mode=production") >= 0; 6 | const isWatch = process.argv.findIndex((argItem) => argItem === "--watch") >= 0; 7 | 8 | (async () => { 9 | /** 10 | * @type {(import("esbuild").BuildOptions & { label: string })[]} 11 | */ 12 | const contexts = [ 13 | { 14 | label: "client", 15 | platform: "browser", 16 | entryPoints: ["./src/client/index.ts"], 17 | target: ["chrome93"], 18 | format: "iife", 19 | }, 20 | { 21 | label: "server", 22 | platform: "node", 23 | entryPoints: ["./src/server/index.ts"], 24 | target: ["node16"], 25 | external: [], 26 | inject: [], 27 | format: "cjs", 28 | sourcemap: true, 29 | define: { 30 | require: "requireTo", 31 | }, 32 | plugins: [ 33 | { 34 | name: "ts-paths", 35 | setup: (build) => { 36 | build.onResolve({ filter: /@citizenfx/ }, (args) => { 37 | return { namespace: "ignore", path: "." }; 38 | }); 39 | 40 | build.onResolve({ filter: /.*/ }, (args) => { 41 | if (!args.path.match(/^@(server|client|shared)/) && args.kind === "import-statement") { 42 | let modulePath; 43 | 44 | // @/ means the root of the project 45 | if (args.path.startsWith("@/")) { 46 | modulePath = path.join(...process.cwd().split(path.sep), args.path.replace(/^@\//, "")); 47 | } else { 48 | modulePath = require.resolve(args.path); 49 | 50 | // require.resolve return the index.js file, while i'm here 51 | // just trying to add the root path to the package path in the node_modules folder 52 | 53 | // [require.resolve] => D:\Servers\NatunaIndonesia\txData\CFX\resources\[local]\natuna\node_modules\mysql2\index.js 54 | // [code below] => D:\Servers\NatunaIndonesia\txData\CFX\resources\[local]\natuna\node_modules\mysql2 55 | if (path.isAbsolute(modulePath)) { 56 | modulePath = path.join(...process.cwd().split(path.sep), "node_modules", args.path); 57 | } 58 | } 59 | 60 | return { 61 | path: modulePath, 62 | external: true, 63 | }; 64 | } 65 | }); 66 | 67 | build.onLoad({ filter: /.*/, namespace: "ignore" }, (args) => { 68 | return { 69 | contents: "", 70 | }; 71 | }); 72 | }, 73 | }, 74 | ], 75 | }, 76 | ]; 77 | 78 | for (const context of contexts) { 79 | // Remove Additional Option Conflicts 80 | const label = context.label; 81 | delete context.label; 82 | 83 | try { 84 | const result = await esbuild.build({ 85 | bundle: true, 86 | assetNames: `[name].[ext]`, 87 | outdir: "dist/" + label, 88 | minify: isProduction, 89 | sourcemap: true, 90 | metafile: true, 91 | watch: isWatch 92 | ? { 93 | onRebuild: (err, res) => { 94 | if (err) { 95 | return console.error(`[${label}]: Rebuild failed`, err); 96 | } 97 | 98 | console.log(`[${label}]: Rebuild succeeded, warnings:`, res.warnings); 99 | }, 100 | } 101 | : false, 102 | ...context, 103 | }); 104 | 105 | if (isProduction) { 106 | const analize = await esbuild.analyzeMetafile(result.metafile, { 107 | color: true, 108 | verbose: true, 109 | }); 110 | 111 | console.log(analize); 112 | } 113 | } catch (error) { 114 | console.error(`[${label}]: Build failed`, error); 115 | } 116 | } 117 | })(); 118 | -------------------------------------------------------------------------------- /docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | mraflymaulana@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /.natuna/package.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "required": ["natuna"], 4 | "definitions": { 5 | "image": { 6 | "type": "object", 7 | "required": ["hoverText", "assetName"], 8 | "properties": { 9 | "hoverText": { 10 | "type": "string", 11 | "description": "Text to display while hovering over the image", 12 | "default": "Hi There!", 13 | "minLength": 5 14 | }, 15 | "assetName": { 16 | "type": "string", 17 | "description": "Discord Rich Presence Art Asset Name", 18 | "default": "logo", 19 | "minLength": 1 20 | } 21 | } 22 | } 23 | }, 24 | "properties": { 25 | "natuna": { 26 | "title": "Natuna Framework", 27 | "description": "Settings for Natuna Framework", 28 | "type": "object", 29 | "required": ["whitelisted", "game", "discordRPC"], 30 | "properties": { 31 | "whitelisted": { 32 | "description": "Is the server whitelisted? (Configure who can bypass the whitelist in the Database)", 33 | "type": "boolean", 34 | "default": false 35 | }, 36 | "game": { 37 | "description": "Game Settings", 38 | "type": "object", 39 | "required": ["noDispatchService", "noWantedLevel", "autoRespawnDisabled", "pauseMenuTitle"], 40 | "properties": { 41 | "noDispatchService": { 42 | "type": "boolean", 43 | "description": "Disable Emergency Service like Police, Ambulance, Firefighters, etc?", 44 | "default": false 45 | }, 46 | "noWantedLevel": { 47 | "type": "boolean", 48 | "description": "Disable Wanted Level?", 49 | "default": false 50 | }, 51 | "autoRespawnDisabled": { 52 | "type": "boolean", 53 | "description": "Set whether auto respawn is disabled if player dies, requires \"spawnmanager\" script to be activated", 54 | "default": false 55 | }, 56 | "pauseMenuTitle": { 57 | "type": "string", 58 | "description": "Set the title of the pause menu", 59 | "default": "Natuna Indonesia | discord.gg/kGPHBvXzGM", 60 | "minLength": 1 61 | } 62 | } 63 | }, 64 | "discordRPC": { 65 | "description": "Discord Rich Presence Settings", 66 | "type": "object", 67 | "required": ["appId", "refreshInterval", "text", "largeImage", "smallImage", "buttons"], 68 | "properties": { 69 | "appId": { 70 | "type": "string", 71 | "description": "Discord App ID", 72 | "default": "866690121485910017", 73 | "minLength": 12 74 | }, 75 | "refreshInterval": { 76 | "type": "number", 77 | "description": "Discord Rich Presence refresh interval (in seconds).", 78 | "default": 30, 79 | "minimum": 5 80 | }, 81 | "text": { 82 | "type": "string", 83 | "description": "Text to display", 84 | "default": "Playing on Natuna Indonesia", 85 | "minLength": 5 86 | }, 87 | "largeImage": { 88 | "description": "Large Image", 89 | "$ref": "#/definitions/image" 90 | }, 91 | "smallImage": { 92 | "description": "Small Image", 93 | "$ref": "#/definitions/image" 94 | }, 95 | "buttons": { 96 | "type": "array", 97 | "description": "Buttons displayed on the Discord Rich Presence", 98 | "minItems": 1, 99 | "maxItems": 2, 100 | "items": { 101 | "type": "object", 102 | "description": "Button", 103 | "required": ["label", "url"], 104 | "properties": { 105 | "label": { 106 | "type": "string", 107 | "description": "Text to display", 108 | "default": "Some Link", 109 | "minLength": 5 110 | }, 111 | "url": { 112 | "type": "string", 113 | "description": "The URL to open when clicking the button. This has to start with fivem://connect/ or https://.", 114 | "default": "https://example.com", 115 | "minLength": 10, 116 | "pattern": "^(fivem://connect/|https://)" 117 | } 118 | } 119 | } 120 | } 121 | } 122 | } 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/server/manager/command/handler.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/server"; 3 | 4 | import type Events from "@server/events"; 5 | import type Players from "@server/players"; 6 | import type Utils from "@server/utils"; 7 | 8 | import { SharedEventType } from "@shared/events/type"; 9 | 10 | /** 11 | * @description 12 | * A Handler to execute function when called. 13 | * 14 | * @param src (Source) return the player id whose triggering it. 15 | * @param args (Arguments) return text after command in Array, example, if you're triggering the command like this "/hello all people", the arguments returns was ["all", "people"]. 16 | * @param raw (Raw) return raw version of the command triggered. 17 | */ 18 | export type Handler = (src: number, args: any[], raw: string) => any; 19 | 20 | /** 21 | * @description 22 | * Set a configuration on a command 23 | * 24 | * @arg description Description of a command 25 | * @arg argsDescription Description of every arguments required 26 | * @arg restricted If you want to limit your command with an ace permission automatically 27 | * @arg cooldown Cooldown of the command usage 28 | * @arg consoleOnly Whether if the command can only be used on the console 29 | * @arg requirements User requirements before using the command 30 | * @arg caseInsensitive Whether the command is case sensitive or not 31 | * @arg cooldownExclusions Set the cooldown bypass for some users or groups 32 | * 33 | * @example 34 | * ```ts 35 | * { 36 | * description: "Set Weather Status", 37 | * argsDescription: [ 38 | * { name: "Weather Condition", help: "clear | rain | thunder" } // Argument 1 39 | * // Argument 2, 3, ... 40 | * ] 41 | * } 42 | * ``` 43 | */ 44 | export type Config = { 45 | argsRequired?: boolean | number; 46 | description?: string; 47 | argsDescription?: { 48 | name: string; 49 | help: string; 50 | }[]; 51 | cooldown?: number; 52 | consoleOnly?: boolean; 53 | requirements?: { 54 | userIDs?: number[]; 55 | custom?: () => boolean; 56 | }; 57 | caseInsensitive?: boolean; 58 | cooldownExclusions?: { 59 | userIDs?: number[]; 60 | }; 61 | restricted?: boolean; 62 | }; 63 | 64 | /** 65 | * This class is included when registering a command on server with `registerCommand` function. 66 | * 67 | * Use it if you know what you're doing. 68 | */ 69 | export default class Command { 70 | constructor( 71 | private events: Events, // 72 | private players: Players, 73 | private utils: Utils, 74 | private name: string, 75 | private rawHandler: Handler, 76 | private rawConfig: Config, 77 | private isClientCommand: boolean 78 | ) { 79 | this.#config = this.#parseConfig(this.rawConfig); 80 | 81 | RegisterCommand( 82 | name, 83 | (src: number, args: string[], raw: string) => { 84 | const validation = this.#validateExecution(src, args, raw); 85 | 86 | if (validation) { 87 | return this.#handler(src, args, raw); 88 | } else if (typeof validation === "string") { 89 | return console.log(validation); 90 | } 91 | }, 92 | this.#config.restricted 93 | ); 94 | } 95 | 96 | #config: Config = {}; 97 | #cooldownList: Record = {}; 98 | 99 | #handler = (src: number, args: string[], raw: string) => { 100 | if (this.isClientCommand) { 101 | return this.events.shared.emit(SharedEventType.CLIENT_EXECUTE_COMMAND, src, [this.name, args, raw]); 102 | } 103 | 104 | return this.rawHandler; 105 | }; 106 | 107 | /** 108 | * @description 109 | * Parse the command configuration and validate it. 110 | * 111 | * @param config Command Configuration 112 | */ 113 | #parseConfig = (config: Config) => { 114 | config = this.utils.validator.isObject(config) ? config : {}; 115 | config.requirements = this.utils.validator.isObject(config.requirements) ? config.requirements : {}; 116 | config.cooldownExclusions = this.utils.validator.isObject(config.cooldownExclusions) ? config.cooldownExclusions : {}; 117 | config.argsDescription = this.utils.validator.isArray(config.argsDescription) ? config.argsDescription : []; 118 | 119 | config = { 120 | argsRequired: false, 121 | description: "No Description Available", 122 | cooldown: 0, 123 | consoleOnly: false, 124 | caseInsensitive: false, 125 | restricted: false, 126 | ...config, 127 | argsDescription: [...config.argsDescription], 128 | requirements: { 129 | ...config.requirements, 130 | }, 131 | cooldownExclusions: { 132 | ...config.cooldownExclusions, 133 | }, 134 | }; 135 | 136 | return config; 137 | }; 138 | 139 | /** 140 | * @description 141 | * Validate an execution of the command. 142 | * 143 | * @param src Server ID of the player that executed the command 144 | * @param args Arguments given in the command 145 | * @param raw Raw command output 146 | */ 147 | #validateExecution = (src: number, args: string[], raw: string) => { 148 | if (this.#config.argsRequired && args.length <= this.#config.argsRequired) { 149 | return "Not enough arguments passed."; 150 | } 151 | 152 | if (this.#config.consoleOnly && src !== 0) { 153 | return "This command can only be executed from console!"; 154 | } 155 | 156 | const user = this.players.get({ 157 | server_id: src, 158 | }); 159 | 160 | if (user) { 161 | const cooldown = this.#config.cooldown; 162 | const cooldownList = this.#cooldownList; 163 | const cooldownExclusions = this.#config.cooldownExclusions; 164 | 165 | if (cooldown && cooldownList[src] && Date.now() - cooldownList[src] <= cooldown) { 166 | if ( 167 | !cooldownExclusions || // Option A: If there are no cooldown exclusion 168 | (cooldownExclusions.userIDs && !cooldownExclusions.userIDs.includes(user.user_id)) // Option B: If there are steamIDs configured, but player wasn't on the list 169 | ) { 170 | return "Still under cooldown."; 171 | } 172 | } 173 | 174 | const requirements = this.#config.requirements; 175 | 176 | if (requirements) { 177 | switch (true) { 178 | case requirements.userIDs && !requirements.userIDs.includes(user.user_id): 179 | case requirements.custom && !requirements.custom(): 180 | return "You don't have the required permissions to execute this command."; 181 | } 182 | } 183 | 184 | if (this.#config.caseInsensitive && this.name !== raw) { 185 | return false; 186 | } 187 | 188 | this.#cooldownList[src] = Date.now(); 189 | return true; 190 | } 191 | 192 | return false; 193 | }; 194 | } 195 | -------------------------------------------------------------------------------- /src/server/players.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/server"; 3 | 4 | import Database from "@server/database"; 5 | import Events from "@server/events"; 6 | import { SharedEventType } from "@shared/events/type"; 7 | 8 | export type Query = { 9 | user_id?: number; 10 | server_id?: number; 11 | license?: string; 12 | }; 13 | 14 | export type Data = Query & { 15 | $index?: number; 16 | updated_at?: number; 17 | last_position?: { 18 | x: number; 19 | y: number; 20 | z: number; 21 | heading: number; 22 | }; 23 | }; 24 | 25 | export type Identifiers = { 26 | steam?: string; 27 | license?: string; 28 | fivem?: string; 29 | discord?: string; 30 | ip?: string; 31 | [key: string]: any; 32 | }; 33 | 34 | export default class Players { 35 | constructor( 36 | private db: ReturnType, // 37 | private events: Events 38 | ) { 39 | on("playerJoining", async () => { 40 | return await this.#add({ 41 | server_id: globalThis.source, 42 | }); 43 | }); 44 | 45 | on("playerDropped", () => { 46 | return this.#delete({ 47 | server_id: globalThis.source, 48 | }); 49 | }); 50 | 51 | this.events.shared.listen(SharedEventType.CURRENT_PLAYER_UPDATE, async (source, data: Data) => { 52 | return await this.update(data); 53 | }); 54 | } 55 | 56 | #list: Data[] = []; 57 | 58 | /** 59 | * @description 60 | * List all players 61 | * 62 | * @param callbackHandler Handler for the data retrieved 63 | * 64 | * @example 65 | * ```ts 66 | * list(); 67 | * ``` 68 | */ 69 | listAll = () => { 70 | return this.#list; 71 | }; 72 | 73 | /** 74 | * @description 75 | * Received current data of a player 76 | * 77 | * @param query Data object to input 78 | * 79 | * @example 80 | * ```ts 81 | * get({ 82 | * where: { 83 | * license: "abc12345" 84 | * } 85 | * }); 86 | * ``` 87 | */ 88 | get = (query: Query) => { 89 | const license = this.#getLicenseId(query); 90 | const dataIndex = this.#list.findIndex((player) => player.license === license); 91 | 92 | if (dataIndex !== -1) { 93 | return { 94 | ...this.#list[dataIndex], 95 | $index: dataIndex, 96 | }; 97 | } 98 | 99 | return undefined; 100 | }; 101 | 102 | /** 103 | * @description 104 | * Update current data of a player 105 | * 106 | * @param data Data object to input 107 | * 108 | * @example 109 | * ```ts 110 | * update({ 111 | * data: { 112 | * someNestedThings: true 113 | * }, 114 | * where: { 115 | * license: "76asdasd56119829asdads0395137" 116 | * } 117 | * }); 118 | * ``` 119 | */ 120 | update = async (data: Data) => { 121 | const currentData = this.get(data); 122 | 123 | if (!currentData) { 124 | if (typeof data.server_id === "number" || typeof data.license === "string") { 125 | const newData = await this.#add(data); 126 | 127 | if (!newData) { 128 | throw new Error("No player data available, tried to add new player but failed."); 129 | } 130 | } else { 131 | throw new Error("No player data available, data not found and can't create a new one since no server_id or license was provided."); 132 | } 133 | } 134 | 135 | this.#list[currentData.$index] = { 136 | ...currentData, 137 | ...data, 138 | updated_at: Date.now(), 139 | }; 140 | 141 | this.#updateToClient(); 142 | 143 | return true; 144 | }; 145 | 146 | #add = async (data: Data) => { 147 | if (!this.get(data)) { 148 | const license = this.#getLicenseId(data); 149 | 150 | if (license) { 151 | const user = await this.db("users").findFirst({ 152 | where: { 153 | license, 154 | }, 155 | }); 156 | 157 | this.#list.push({ 158 | license, 159 | user_id: user.id, 160 | server_id: data.server_id, 161 | updated_at: Date.now(), 162 | }); 163 | 164 | this.#updateToClient(); 165 | 166 | return true; 167 | } 168 | 169 | return false; 170 | } 171 | 172 | return false; 173 | }; 174 | 175 | #delete = (data: Data) => { 176 | const currentData = this.get(data); 177 | 178 | if (currentData) { 179 | this.#list.splice(currentData.$index, 1); 180 | return true; 181 | } 182 | 183 | return false; 184 | }; 185 | 186 | #getLicenseId = (query: Query) => { 187 | const keysLength = Object.keys(query).length; 188 | 189 | if (keysLength === 0) { 190 | throw new Error("No 'where' option available"); 191 | } 192 | 193 | if (keysLength > 1) { 194 | throw new Error("'where' option on the configuration can only contains 1 key"); 195 | } 196 | 197 | switch (true) { 198 | case typeof query.server_id !== "undefined": 199 | const identifiers = this.utils.getIdentifiers(query.server_id); 200 | return String(identifiers.license); 201 | 202 | case typeof query.license !== "undefined": 203 | return query.license; 204 | 205 | case typeof query.user_id !== "undefined": 206 | const player = this.#list.find((player) => player.user_id === query.user_id); 207 | return player ? player.license : null; 208 | } 209 | 210 | return false; 211 | }; 212 | 213 | #updateToClient = () => { 214 | this.events.shared.emit(SharedEventType.UPDATED_DATA_BROADCAST, -1, [this.#list]); 215 | }; 216 | 217 | utils = { 218 | /** 219 | * @description 220 | * Get all set of player ID and return it on JSON format 221 | * 222 | * @param src Server ID of the Player 223 | * 224 | * @example 225 | * ```ts 226 | * const license = getIdentifiers(1).license; 227 | * ``` 228 | */ 229 | getIdentifiers: (playerServerId: number) => { 230 | const fxdkMode = GetConvarInt("sv_fxdkMode", 0); 231 | const identifiers: Identifiers = { 232 | license: fxdkMode ? "fxdk_license" : undefined, 233 | }; 234 | 235 | for (let i = 0; i < GetNumPlayerIdentifiers(String(playerServerId)); i++) { 236 | const id = GetPlayerIdentifier(String(playerServerId), i).split(":"); 237 | identifiers[id[0]] = id[1]; 238 | } 239 | 240 | if (identifiers.steam && typeof identifiers.steam !== "undefined") { 241 | identifiers.steam = BigInt(`0x${identifiers.steam}`).toString(); 242 | } 243 | 244 | return identifiers; 245 | }, 246 | }; 247 | } 248 | -------------------------------------------------------------------------------- /src/ui/nui/modules/NUI_LOADER.js: -------------------------------------------------------------------------------- 1 | /* global $ */ 2 | 3 | let JQueryReady = false; 4 | $(() => (JQueryReady = true)); 5 | 6 | class NUILoader { 7 | constructor(pluginName) { 8 | this.pluginName = pluginName; 9 | this.nuiWrapperId = `natuna-nui\\:${this.pluginName}`; 10 | 11 | const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); 12 | 13 | (async () => { 14 | while (!JQueryReady) await wait(100); 15 | 16 | const html = await this.utils.getContent(`../../../plugins/${this.pluginName}/ui/index.html`); 17 | const parsedHTML = await this.HTMLParser(html); 18 | 19 | // Append the Parsed HTML 20 | $("body").prepend(`
${parsedHTML}
`); 21 | 22 | // Set the Z-Index of the NUI 23 | const zIndex = $(`#${this.nuiWrapperId} meta[name="nuiIndex"]`).attr("content") || 1; 24 | $(`#${this.nuiWrapperId}`).css("z-index", parseInt(zIndex)); 25 | })(); 26 | } 27 | 28 | CSSParser = (content) => { 29 | const regex = /url\(("|'|`|)(.*?)("|'|`|)(\))/gm; 30 | const regexMatched = this.utils.getMatchedRegex(regex, content); 31 | 32 | for (const match of regexMatched) { 33 | if (match.startsWith(`./`)) { 34 | const newAttribute = match.replace(".", `../../../plugins/${this.pluginName}/ui`); 35 | content = content.replace(match, newAttribute); 36 | } 37 | } 38 | 39 | content = this.utils.renderCSSWithSelector(content, `#${this.nuiWrapperId}`); 40 | 41 | return content; 42 | }; 43 | 44 | JSParser = (content) => { 45 | // $("") && document.querySelector() 46 | const regex1 = /(\$|querySelector)\(("|'|`)(.*?)("|'|`)\)/gm; 47 | const regexMatched1 = this.utils.getMatchedRegex(regex1, content); 48 | 49 | for (const match of regexMatched1) { 50 | if (match.startsWith(`#`)) { 51 | const newAttribute = match.replace("#", `#${this.pluginName}-`); 52 | content = content.replace(match, newAttribute); 53 | } 54 | } 55 | 56 | // document.getElementById() 57 | const regex2 = /(getElementById)\(("|'|`)(.*?)("|'|`)\)/gm; 58 | const regexMatched2 = this.utils.getMatchedRegex(regex2, content); 59 | 60 | for (const match of regexMatched2) { 61 | if (!match.startsWith(`getElementById`) && !match.match("(\"|'|`)")) { 62 | // eslint-disable-next-line no-useless-escape 63 | const string = match.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); 64 | const regex = new RegExp(`getElementById\\(("|'|\`)${string}("|'|\`)\\)`, "gm"); 65 | const newId = `${this.pluginName}-${match}`; 66 | 67 | content = content.replace(regex, `getElementById(\`${newId}\`)`); 68 | } 69 | } 70 | 71 | return content; 72 | }; 73 | 74 | HTMLParser = async (content) => { 75 | // Find 'src' and 'href' attributes 76 | const regex1 = /(src|href)=("|'|`)(.*?)("|'|`)/gm; 77 | const regexMatched1 = this.utils.getMatchedRegex(regex1, content); 78 | 79 | for (const match of regexMatched1) { 80 | if (match.startsWith(`./`)) { 81 | let file; 82 | const newUrl = match.replace(".", `../../../plugins/${this.pluginName}/ui`); 83 | 84 | if (match.endsWith(".js") || match.endsWith(".css")) { 85 | file = await this.utils.getContent(newUrl); 86 | content = content.replace(match, "//:0"); 87 | } 88 | 89 | if (match.endsWith(".js")) { 90 | content += ""; 91 | } else if (match.endsWith(".css")) { 92 | content += ""; 93 | } else { 94 | content = content.replace(match, newUrl); 95 | } 96 | } 97 | } 98 | 99 | // Find 'id' attributes 100 | const regex2 = /id=("|'|`)(.*?)("|'|`)/gm; 101 | const regexMatched2 = this.utils.getMatchedRegex(regex2, content); 102 | 103 | for (const match of regexMatched2) { 104 | if (!match.startsWith(`id=`) && !match.match("(\"|'|`)")) { 105 | // eslint-disable-next-line no-useless-escape 106 | const string = match.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); 107 | const regex = new RegExp(`id=("|'|\`)${string}("|'|\`)`, "gm"); 108 | const newId = `${this.pluginName}-${match}`; 109 | 110 | content = content.replace(regex, `id="${newId}"`); 111 | } 112 | } 113 | 114 | return content; 115 | }; 116 | 117 | utils = { 118 | getContent: (url) => { 119 | return new Promise((resolve, reject) => { 120 | $.ajax(url, { 121 | dataType: "text", 122 | success: function (response) { 123 | // Do something with the response 124 | resolve(response); 125 | }, 126 | }); 127 | }); 128 | }, 129 | getMatchedRegex: (regex, content) => { 130 | let regexMatched; 131 | let returnMatch = []; 132 | 133 | while ((regexMatched = regex.exec(content)) !== null) { 134 | // This is necessary to avoid infinite loops with zero-width matches 135 | if (regexMatched.index === regex.lastIndex) { 136 | regex.lastIndex++; 137 | } 138 | 139 | for (const match of regexMatched) { 140 | returnMatch.push(match); 141 | } 142 | } 143 | 144 | return returnMatch; 145 | }, 146 | renderCSSWithSelector: (css, selector) => { 147 | return (css + "" || "") 148 | .replace(/\n|\t/g, " ") 149 | .replace(/\s+/g, " ") 150 | .replace(/\s*\/\*.*?\*\/\s*/g, " ") 151 | .replace(/(^|\})(.*?)(\{)/g, function ($0, $1, $2, $3) { 152 | var collector = [], 153 | parts = $2.split(","); 154 | for (var i in parts) { 155 | collector.push(selector + " " + parts[i].replace(/^\s*|\s*$/, "")); 156 | } 157 | return $1 + " " + collector.join(", ") + " " + $3; 158 | }) 159 | .replace(/(body|html)/, ""); 160 | }, 161 | }; 162 | } 163 | 164 | window.addEventListener("message", (event) => { 165 | if (typeof event.data.name !== "undefined") { 166 | return window.emit(event.data.name, event.data.data); 167 | } 168 | }); 169 | 170 | window.on("natuna:nui:retrievePluginList", (data) => { 171 | return new NUILoader(data.name); 172 | }); 173 | 174 | window.on("natuna:nui:debugHTML", () => { 175 | const text = $("html").html(); 176 | const node = document.createElement("textarea"); 177 | const selection = document.getSelection(); 178 | 179 | node.textContent = text; 180 | document.body.appendChild(node); 181 | 182 | selection.removeAllRanges(); 183 | node.select(); 184 | document.execCommand("copy"); 185 | 186 | selection.removeAllRanges(); 187 | document.body.removeChild(node); 188 | 189 | window.sendData("natuna:client:nuiDebugSuccess"); 190 | return true; 191 | }); 192 | -------------------------------------------------------------------------------- /src/server/database/mysql/index.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/server"; 3 | 4 | import type mysql from "mysql2"; 5 | import type { DatabaseSchema } from "@server/database/schema"; 6 | 7 | type Query = Partial; 8 | 9 | /** 10 | * @description 11 | * Executing database query in promise 12 | * 13 | * @example 14 | * ```ts 15 | * executeQuery("SELECT * FROM `users`") 16 | * ``` 17 | */ 18 | export const executeQuery = async (connection: mysql.Pool, query: string): Promise => { 19 | return new Promise((resolve, reject) => { 20 | connection.execute(query, (err: string, result: any) => { 21 | if (err) { 22 | reject(err); 23 | } 24 | 25 | resolve(result); 26 | }); 27 | }); 28 | }; 29 | 30 | export default class MySQLHandler { 31 | constructor( 32 | private connection: mysql.Pool, // 33 | private table: K 34 | ) {} 35 | 36 | /** 37 | * @description 38 | * Write a data into database table 39 | * 40 | * @example 41 | * ```ts 42 | * db.users.write({ 43 | * data: { 44 | * name: "Don Chad" 45 | * } 46 | * }) 47 | * ``` 48 | */ 49 | create = (obj: { data: Query }) => { 50 | if (this.utils.validateQueryObject(obj, ["data"])) { 51 | const keys = Object.keys(obj.data) 52 | .map((x) => this.parser.key(x)) 53 | .join(","); 54 | const values = Object.values(obj.data) 55 | .map((x) => (typeof x === "string" ? `'${x}'` : x)) 56 | .join(","); 57 | 58 | const query = `INSERT INTO \`${this.table}\`(${keys}) VALUES (${values})`; 59 | 60 | return this.utils.executeQuery(query); 61 | } 62 | }; 63 | 64 | /** 65 | * @description 66 | * Find a data from database table 67 | * 68 | * @example 69 | * ```ts 70 | * db.users.find({ 71 | * where: { 72 | * name: "Don Chad" 73 | * } 74 | * }) 75 | * ``` 76 | */ 77 | find = (obj: { where: Query }) => { 78 | if (this.utils.validateQueryObject(obj, ["where"])) { 79 | let query; 80 | 81 | if (obj.where) { 82 | const whereOptions = this.parser.keyVal(obj.where); 83 | query = `SELECT * FROM \`${this.table}\` WHERE ${whereOptions}`; 84 | } else { 85 | query = `SELECT * FROM \`${this.table}\``; 86 | } 87 | 88 | return this.utils.executeQuery(query); 89 | } 90 | }; 91 | 92 | /** 93 | * @description 94 | * Find first data from database table 95 | * 96 | * @example 97 | * ```ts 98 | * db.users.findFirst({ 99 | * where: { 100 | * name: "Don Chad" 101 | * } 102 | * }) 103 | * ``` 104 | */ 105 | findFirst = async (obj: { where: Query }) => { 106 | if (this.utils.validateQueryObject(obj, ["where"])) { 107 | const result: any = await this.find(obj); 108 | return result && result.length > 0 ? result[0] : false; 109 | } 110 | }; 111 | 112 | /** 113 | * @description 114 | * Update a data from database table 115 | * 116 | * @example 117 | * ```ts 118 | * // Normal Version. 119 | * db.users.update({ 120 | * where: { 121 | * name: "Don Chad" 122 | * }, 123 | * data: { 124 | * name: "John Doe" 125 | * } 126 | * }) 127 | * 128 | * // Function Delivered Version. 129 | * db.users.update({ 130 | * where: { 131 | * name: "Don Chad" 132 | * }, 133 | * data: (data) => { 134 | * // MUST RETURNS as an OBJECT 135 | * return { 136 | * count: data.count += 1 137 | * } 138 | * } 139 | * }) 140 | * ``` 141 | */ 142 | update = (obj: { data: Query; where: Query }) => { 143 | if (this.utils.validateQueryObject(obj, ["data", "where"])) { 144 | const whereOptions = this.parser.keyVal(obj.where); 145 | let updateOptions; 146 | 147 | if (typeof obj.data == "function") { 148 | const data: any = this.find({ where: obj.where }); 149 | const newInputData = obj.data[data]; 150 | 151 | if (typeof newInputData !== "object" || Array.isArray(newInputData)) { 152 | throw new Error("Returned Data Must be an Object!"); 153 | } 154 | 155 | updateOptions = this.parser.keyVal(newInputData); 156 | } else { 157 | updateOptions = this.parser.keyVal(obj.data); 158 | } 159 | 160 | const query = `UPDATE \`${this.table}\` SET ${updateOptions} WHERE ${whereOptions}`; 161 | 162 | return this.utils.executeQuery(query); 163 | } 164 | }; 165 | 166 | /** 167 | * @description 168 | * Delete a data from database table 169 | * 170 | * @example 171 | * ```ts 172 | * db.users.delete({ 173 | * where: { 174 | * name: "Don Chad" 175 | * } 176 | * }) 177 | * ``` 178 | */ 179 | delete = (obj: { where: Query }) => { 180 | if (this.utils.validateQueryObject(obj, ["where"])) { 181 | const whereOptions = this.parser.keyVal(obj.where); 182 | 183 | const query = `DELETE FROM \`${this.table}\` WHERE ${whereOptions}`; 184 | 185 | return this.utils.executeQuery(query); 186 | } 187 | }; 188 | 189 | parser = { 190 | /** 191 | * @description 192 | * Put a templated string around the key for SQL to identified it as a structure name, not a value 193 | */ 194 | key: (key: string) => `\`${key}\``, 195 | 196 | /** 197 | * @description 198 | * Parsing object keys and it values to SQL key and value format and mapping it into a string format 199 | */ 200 | keyVal: (dataObj: Record) => 201 | Object.keys(dataObj).map((x) => { 202 | const value = typeof dataObj[x] == "string" ? `"${dataObj[x].replace(/"/g, '\\"')}"` : dataObj[x]; 203 | return `${this.parser.key(x)}=${value}`; 204 | }), 205 | }; 206 | 207 | utils = { 208 | /** 209 | * @description 210 | * Validating query object before starting to execute it 211 | */ 212 | validateQueryObject: (obj: any, requiredKey: Array = []) => { 213 | if (typeof obj !== "object" || Array.isArray(obj)) { 214 | throw new Error("Database Query Object MUST be in object (JSON) type!"); 215 | } 216 | 217 | for (const key of requiredKey) { 218 | this.utils.validateQueryObjectData(key, obj[key]); 219 | } 220 | 221 | return true; 222 | }, 223 | 224 | /** 225 | * @description 226 | * Validating object data whether if it's object or not 227 | */ 228 | validateQueryObjectData: (key: string, data: any) => { 229 | if (typeof data !== "undefined" && (typeof data !== "object" || Array.isArray(data))) { 230 | throw new Error("Data of query object: `" + key + "` MUST be in object (JSON) type!"); 231 | } 232 | 233 | return true; 234 | }, 235 | 236 | /** 237 | * @description 238 | * Executing database query in promise 239 | * 240 | * @example 241 | * ```ts 242 | * executeQuery("SELECT * FROM `users`") 243 | * ``` 244 | */ 245 | executeQuery: async (query: string) => { 246 | return await executeQuery(this.connection, query); 247 | }, 248 | }; 249 | } 250 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > Hi everyone, it's been a long time since I was here last, but a lot of things happened and I can only do those things at a time. First of all, I would like to thank each and every one of you for following Natuna and also joining as a member here. 2 | > 3 | > In short, I don't have any plans to grow this project much in the future, Natuna was originally created to create a new ecosystem within FiveM that emphasizes the use of Javascript/Typescript, but over time, I didn't see the growth of that ecosystem within FiveM, which led to few people being interested in this project and caused (for now) no one to be interested in contributing massively to this project. 4 | > 5 | > This has led to me having to work full-time on the project, but as we all know, we need money to live, including internet, electricity, and other expenses. Therefore, with no/lack of financial or direct support for this project, I am unable to continue this any further. 6 | > 7 | > I would like to apologize to all members who had high hopes for this project, but it is possible that it will be continued in the future if there are people who are interested in helping with this project. 8 | > 9 | > For now, I have my own agency for website development services, design, marketing, and so on which you can check out at https://runes.asia/. 10 | > 11 | > And I am also developing a Discord Music Bot called Hupa which you can check out at https://discordbotlist.com/bots/hupa. 12 | > 13 | > And as for this server, it will remain for an indefinite period of time, thank you for everything! 14 | 15 | --- 16 | 17 | # Natuna FiveM Framework 18 | 19 | 20 | 21 | ### FiveM® TypeScript based framework with single resources handler system that runs on the JavaScript & NodeJS runtime. 22 | 23 | ![Github Repository Project Funding](https://img.shields.io/badge/Project%20Funding-%240.0-orange) ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) ![Github Repository Issues Tracking](https://img.shields.io/bitbucket/issues-raw/natuna-framework/natuna) 24 | 25 | ###### © 2022 Natuna Framework by [Runes](https://runes.asia). Runes and its associates are not affiliated with or endorsed by Rockstar North, Take-Two Interactive or other rightsholders. Any trademarks used belong to their respective owners. 26 | 27 | ## Links: 28 | 29 | 1. [Getting Started](docs/GETTING_STARTED.md) (Installing, Updating, FAQ, etc.) 30 | 2. [Building Documentation](docs/BUILDING.md) (See how to build to framework) 31 | 3. [Code Documentation](https://developer.natuna.asia/fivem) (Functions, Variable, Classes, Interface, Type, etc.) 32 | 4. [Contributing Guide](docs/CONTRIBUTING.md) 33 | 5. [Discord Server](https://discord.gg/kGPHBvXzGM) (Need Help?) 34 | 6. [Donation, Additional Informations](#interested-with-this-framework) (Feeling Generous?) 35 | 36 | ## Summary 37 | 38 | This project was created with the intention of enlivening the FiveM Javascript community. Most people use other frameworks like ESX or QB-Framework but most of them are written in LUA language, so because of that, I was interested in making something completely different from the community. 39 | 40 |
41 | Before You Start, Read This 42 | 43 | This framework is not created for people who do not understand programming languages. There are a few skill requirements including: 44 | 45 | 1. Command line usage 46 | 2. JavaScript/TypeScript 47 | 3. Understanding module bundlers (Webpack) 48 | 4. Node.JS Package Managers (e.g. [npm](https://www.npmjs.com/) / [yarn](https://yarnpkg.com)) 49 | 5. Being confident in your learning / reach in order to understand how this framework is working 50 | 6. Know to not ask the developer(s) for support if the instructions were clearly stated in the documentation. 51 | 7. **KNOW HOW TO RESPECT OTHER PEOPLE'S HARD WORK & TO NOT STEAL!** 52 | 53 | This framework is still far from perfect, so if you you could help me developing this framework, that would be great! 54 | 55 |
56 | 57 | ## Features 58 | 59 | Instead of the same thing all over and over again, we aim to give you something different by providing this framework, such as: 60 | 61 |
62 | 1. Single Handler System Based 63 | 64 | With Natuna, all ticks, event, variables are handled with this framework itself. 65 | 66 | 67 | 68 | 69 |
70 | 71 |
72 | 2. Typescript is Better 73 | 74 | 1. **IntelliSense Ready**
75 | Learn more about [VSCode IntelliSense on Here](https://code.visualstudio.com/docs/editor/intellisense). 76 | 77 | 78 | 2. **JSDoc Ready**
79 | Learn more about [JSDoc](https://jsdoc.app/). 80 | 81 | 82 | 3. **Safety Checks**
83 | Learn more about [why should we use TypeScript](https://dev.to/mistval/type-safe-typescript-4a6f). 84 | 85 | 86 |
87 | 88 |
89 | 3. Wrapper 90 | 91 | 1. **Database Wrapper**
92 | You don't need to write old traditional query language to do get or update things on your database. 93 | ```ts 94 | db.tableName.create({ 95 | data: { 96 | column_1: value_1, 97 | column_2: value_2, 98 | column_3: value_3 99 | } 100 | }) 101 | ``` 102 | 103 | 2. **Command Wrapper**
104 | You may registering command using `RegisterCommand` FiveM native, but for us, that is boring. 105 | ```ts 106 | registerCommand( 107 | // Name 108 | 'hello', 109 | // Handler 110 | (src, args) => { 111 | return console.log('Hello!'); 112 | }, 113 | // Configuration 114 | { 115 | description: "Say Hello" 116 | } 117 | }); 118 | ``` 119 | 120 | 3. **Multi NUI Wrapper**
121 | Wanted to use multi NUI system? NOT A PROBLEM! This framework also contain it own built in NUI system so you don't need to create like 100+ resources for 100 NUI, instead, using 1 resources, this framework, would be enough to handle all of that. 122 | 123 | 4. **Event Wrapper**
124 | Natuna wrap all events to be handled on single event loop. 125 | 126 | 127 |
128 | 129 |
130 | 4. And Many Other Features In the Future! 131 | 132 | Expect more from us in the future! We would give you something different and something more great above your expectation. 133 | 134 |
135 | 136 | ## Interested with this framework? 137 | 138 | Buckle up! Let's go to [Installation Documentation](docs/GETTING_STARTED.md) to setup this framework on your server. 139 | 140 | 141 | 1. **We're Fully Open Sourced!** 142 | 143 | You can be a part of our big journey in the future, please create and develop this framework by doing Fork and Pull Requests on this repository. 144 | 145 | You can also contribute even if only by providing suggestions or reporting problems with this framework in the [issues](https://github.com/natuna-framework/fivem/issues) section of this repository, or in the support link listed in the help section. 146 | 147 | See Our [Contributing Guide](CONTRIBUTING.md) 148 | 149 | 2. **Need Help?** 150 | 151 | If you have tried your best in overcoming every problem but to no avail, don't hesitate to discuss with us at our [Discord Server](https://discord.gg/kGPHBvXzGM). 152 | 153 | 3. **Feeling Generous?** 154 | 155 | This project is 100% free to use. Either you just wanted to use it or modified it, it's up to yourself. You don't even need license key, subscription or whatsoever that related to money. 156 | 157 | If you want to help us, the core developers, in finance, or help the infrastructure of this project to cover monthly expenses and so on, you can become our donor via the link below: 158 | 159 | - [Ko-fi](https://ko-fi.com/raflymln) 160 | - [Paypal](https://www.paypal.com/paypalme/mraflymaulana) 161 | 162 | Please understand that sponsors will receive special content from us as a token of our gratitude, however, we will continue to run this project as an open source project without any mandatory fees. 163 | -------------------------------------------------------------------------------- /src/client/utils/game.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/client"; 3 | 4 | export type MapBlip = { 5 | title?: string; 6 | colour?: number; 7 | iconId?: number; 8 | location?: { 9 | x: number; 10 | y: number; 11 | z: number; 12 | }; 13 | id?: number; 14 | }; 15 | 16 | export default class GameUtils { 17 | /** 18 | * @description 19 | * Draw 3D floating text 20 | * 21 | * ![](https://forum.cfx.re/uploads/default/original/3X/8/a/8ad0a2e8b139cb11d240fdc4bd115c42c91b9a0b.jpeg) 22 | * 23 | * @variation 24 | * - Font list: https://gtaforums.com/topic/794014-fonts-list/ 25 | * - Font Size: 0.0 - 1.0 26 | * - Colour: [r,g,b,a] 27 | * 28 | * @example 29 | * ```ts 30 | * drawText3D("Press E to Pickup", { 31 | * x: 0.8, 32 | * y: 0.85, 33 | * font: 4, 34 | * size: 0.6, 35 | * colour: [255,255,255,255] 36 | * }) 37 | * ``` 38 | */ 39 | drawText3D = (text: string, config: { x: number; y: number; z: number; font?: number; size?: number; colour: [number, number, number, number] }) => { 40 | SetDrawOrigin(config.x, config.y, config.z, 0); 41 | SetTextFont(config.font || 4); 42 | SetTextProportional(false); 43 | SetTextScale(0.0, config.size || 0.6); 44 | SetTextColour(...config.colour); 45 | SetTextDropshadow(0, 0, 0, 0, 255); 46 | SetTextEdge(2, 0, 0, 0, 150); 47 | SetTextDropShadow(); 48 | SetTextOutline(); 49 | BeginTextCommandDisplayText("STRING"); 50 | SetTextCentre(true); 51 | AddTextComponentSubstringPlayerName(text); 52 | EndTextCommandDisplayText(0.0, 0.0); 53 | ClearDrawOrigin(); 54 | }; 55 | 56 | /** 57 | * @description 58 | * Draw text on screen 59 | * 60 | * ![](https://forum.cfx.re/uploads/default/original/3X/2/6/26dd0f34710d145af0f6b49d3bc4b12b80e6df8e.png) 61 | * 62 | * @variation 63 | * - Font list: https://gtaforums.com/topic/794014-fonts-list/ 64 | * - Percentage of the axis: 0.0f < x, y < 1.0f 65 | * 66 | * @example 67 | * ```ts 68 | * drawText("Your Server ID: 1", { 69 | * x: 0.8, 70 | * y: 0.85, 71 | * font: 4, 72 | * size: 0.6 73 | * }); 74 | * ``` 75 | */ 76 | drawText = (text: string, config: { x: number; y: number; font?: number; size?: number }) => { 77 | SetTextFont(config.font || 4); 78 | SetTextProportional(true); 79 | SetTextScale(0.0, config.size || 0.6); 80 | SetTextDropshadow(1, 0, 0, 0, 255); 81 | SetTextEdge(1, 0, 0, 0, 255); 82 | SetTextDropShadow(); 83 | SetTextOutline(); 84 | BeginTextCommandDisplayText("STRING"); 85 | AddTextComponentSubstringPlayerName(text); 86 | EndTextCommandDisplayText(config.x, config.y); 87 | }; 88 | 89 | /** 90 | * @description 91 | * Draw a list of instructional buttons. 92 | * 93 | * ![](https://forum.cfx.re/uploads/default/original/3X/7/6/76ed876c492fbe1b155b1131221c8911d7d3a50b.png) 94 | * 95 | * @variation 96 | * - Controls: https://docs.fivem.net/docs/game-references/controls/#controls 97 | * @param buttonList List of buttons 98 | * 99 | * @example 100 | * ```ts 101 | * drawInstructionalButtons([ 102 | * { controlType: 0, controlId: 32, message: "Move Forward" }, 103 | * { controlType: 0, controlId: 33, message: "Move Backward" } 104 | * ]) 105 | * ``` 106 | */ 107 | drawInstructionalButtons = async (buttonList: Array<{ controlType: number; controlId: number; message: string }>) => { 108 | const SetButton = (padIndex: number, control: number) => { 109 | ScaleformMovieMethodAddParamPlayerNameString(GetControlInstructionalButton(padIndex, control, true)); 110 | }; 111 | 112 | const SetButtonMessage = (text: string) => { 113 | BeginTextCommandScaleformString("STRING"); 114 | AddTextComponentScaleform(text); 115 | EndTextCommandScaleformString(); 116 | }; 117 | 118 | const scaleform = RequestScaleformMovie("instructional_buttons"); 119 | while (!HasScaleformMovieLoaded(scaleform)) { 120 | await new Promise((res) => setTimeout(res, 5)); 121 | } 122 | 123 | PushScaleformMovieFunction(scaleform, "CLEAR_ALL"); 124 | PopScaleformMovieFunctionVoid(); 125 | 126 | PushScaleformMovieFunction(scaleform, "SET_CLEAR_SPACE"); 127 | PushScaleformMovieFunctionParameterInt(200); 128 | PopScaleformMovieFunctionVoid(); 129 | 130 | for (const keyIndex in buttonList.reverse()) { 131 | const data = buttonList[keyIndex]; 132 | 133 | PushScaleformMovieFunction(scaleform, "SET_DATA_SLOT"); 134 | PushScaleformMovieFunctionParameterInt(+keyIndex); 135 | SetButton(data.controlType, data.controlId); 136 | SetButtonMessage(data.message); 137 | PopScaleformMovieFunctionVoid(); 138 | } 139 | 140 | PushScaleformMovieFunction(scaleform, "DRAW_INSTRUCTIONAL_BUTTONS"); 141 | PopScaleformMovieFunctionVoid(); 142 | 143 | PushScaleformMovieFunction(scaleform, "SET_BACKGROUND_COLOUR"); 144 | PushScaleformMovieFunctionParameterInt(0); 145 | PushScaleformMovieFunctionParameterInt(0); 146 | PushScaleformMovieFunctionParameterInt(0); 147 | PushScaleformMovieFunctionParameterInt(80); 148 | PopScaleformMovieFunctionVoid(); 149 | 150 | DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 255, 0); 151 | return scaleform; 152 | }; 153 | 154 | /** 155 | * @description 156 | * Show a GTA V feed notification 157 | * 158 | * ![](https://i.imgur.com/TJvqkYq.png) 159 | * 160 | * @variation 161 | * - Background Color List: https://gyazo.com/68bd384455fceb0a85a8729e48216e15 162 | * 163 | * @example 164 | * ```ts 165 | * createFeedNotification("You Got a New Message!", { 166 | * backgroundColor: 140 167 | * }); 168 | * ``` 169 | */ 170 | createFeedNotification = (text: string, config: { backgroundColor?: number } = {}) => { 171 | BeginTextCommandThefeedPost("STRING"); 172 | ThefeedSetNextPostBackgroundColor(config.backgroundColor || 140); 173 | AddTextComponentSubstringPlayerName(text); 174 | EndTextCommandThefeedPostTicker(false, false); 175 | }; 176 | 177 | /** 178 | * @description 179 | * Create help notification 180 | * 181 | * ![](https://forum.cfx.re/uploads/default/original/4X/b/a/9/ba9186c16bd7a6c43d4f778f00ce3ce9a76cfe2d.png) 182 | * 183 | * @example 184 | * ```ts 185 | * createHelpNotification("Press ~INPUT_MOVE_UP_ONLY~ to Walk"); 186 | * ``` 187 | */ 188 | createHelpNotification = (text: string, config: { beep?: boolean } = {}) => { 189 | BeginTextCommandDisplayHelp("STRING"); 190 | AddTextComponentSubstringPlayerName(text); 191 | EndTextCommandDisplayHelp(0, false, config.beep || false, -1); 192 | }; 193 | 194 | /** 195 | * @description 196 | * Get hash value from the string 197 | * 198 | * @example 199 | * ```ts 200 | * getHashString('Some Value'); 201 | * ``` 202 | */ 203 | getHashString = (val: string) => { 204 | let hash = 0; 205 | let string = val.toLowerCase(); 206 | 207 | for (let i = 0; i < string.length; i++) { 208 | let letter = string[i].charCodeAt(0); 209 | hash = hash + letter; 210 | hash += (hash << 10) >>> 0; 211 | hash ^= hash >>> 6; 212 | hash = hash >>> 0; 213 | } 214 | 215 | hash += hash << 3; 216 | 217 | if (hash < 0) hash = hash >>> 0; 218 | 219 | hash ^= hash >>> 11; 220 | hash += hash << 15; 221 | 222 | if (hash < 0) hash = hash >>> 0; 223 | 224 | return hash.toString(16).toUpperCase(); 225 | }; 226 | 227 | /** 228 | * @description 229 | * Get the coordinates of the reflected entity/object the camera is aiming at 230 | * 231 | * @example 232 | * ```ts 233 | * getCamTargetedCoords(6.0); 234 | * ``` 235 | */ 236 | getCamTargetedCoords = (distance: number) => { 237 | const [camRotX, , camRotZ] = GetGameplayCamRot(2); 238 | const [camX, camY, camZ] = GetGameplayCamCoord(); 239 | 240 | const tZ = camRotZ * 0.0174532924; 241 | const tX = camRotX * 0.0174532924; 242 | const num = Math.abs(Math.cos(tX)); 243 | 244 | const x = camX + -Math.sin(tZ) * (num + distance); 245 | const y = camY + Math.cos(tZ) * (num + distance); 246 | const z = camZ + Math.sin(tX) * 8.0; 247 | 248 | return { x, y, z }; 249 | }; 250 | 251 | /** 252 | * @description 253 | * Get the entity camera direction offset from axis X. 254 | * 255 | * @example 256 | * ```ts 257 | * getCamDirection(); 258 | * ``` 259 | */ 260 | getCamDirection = () => { 261 | const heading = GetGameplayCamRelativeHeading() + GetEntityHeading(GetPlayerPed(-1)); 262 | const pitch = GetGameplayCamRelativePitch(); 263 | 264 | let x = -Math.sin((heading * Math.PI) / 180.0); 265 | let y = Math.cos((heading * Math.PI) / 180.0); 266 | let z = Math.sin((pitch * Math.PI) / 180.0); 267 | 268 | const len = Math.sqrt(x * x + y * y + z * z); 269 | if (len !== 0) { 270 | x = x / len; 271 | y = y / len; 272 | z = z / len; 273 | } 274 | 275 | return { x, y, z }; 276 | }; 277 | 278 | /** 279 | * @description 280 | * Get entity the ped aimed at 281 | * 282 | * @example 283 | * ```ts 284 | * getTargetedEntity(6.0); 285 | * ``` 286 | */ 287 | getTargetedEntity = (distance: number) => { 288 | const [camX, camY, camZ] = GetGameplayCamCoord(); 289 | const targetCoords = this.getCamTargetedCoords(distance); 290 | const RayHandle = StartShapeTestRay(camX, camY, camZ, targetCoords.x, targetCoords.y, targetCoords.z, -1, PlayerPedId(), 0); 291 | const [, , , , entityHit] = GetRaycastResult(RayHandle); 292 | 293 | return { entityHit, ...targetCoords }; 294 | }; 295 | 296 | /** 297 | * @description 298 | * Map Blip Functions 299 | */ 300 | mapBlip = { 301 | /** 302 | * @hidden 303 | */ 304 | lists: {}, 305 | 306 | /** 307 | * @description 308 | * Add a map blip (Must include all configuration below) 309 | * 310 | * @variation 311 | * - Blip List: https://docs.fivem.net/docs/game-references/blips/ 312 | * - Colour List: https://runtime.fivem.net/doc/natives/?_0x03D7FB09E75D6B7E 313 | * 314 | * @example 315 | * ```ts 316 | * create({ 317 | * "title": "Example", 318 | * "colour": 30, 319 | * "id": 108, 320 | * "location": { 321 | * "x": 260.130, 322 | * "y": 204.308, 323 | * "z": 109.287 324 | * } 325 | * }); 326 | * ``` 327 | */ 328 | create: (config: MapBlip) => { 329 | config.id = AddBlipForCoord(config.location.x, config.location.y, config.location.z); 330 | SetBlipSprite(config.id, config.iconId); 331 | SetBlipDisplay(config.id, 4); 332 | SetBlipScale(config.id, 1.0); 333 | SetBlipColour(config.id, config.colour); 334 | SetBlipAsShortRange(config.id, true); 335 | BeginTextCommandSetBlipName("STRING"); 336 | AddTextComponentString(config.title); 337 | EndTextCommandSetBlipName(config.id); 338 | 339 | return ((this.mapBlip as any).lists[config.id] = config); 340 | }, 341 | 342 | /** 343 | * @description 344 | * Edit a map blip 345 | * 346 | * @variation 347 | * - Blip List: https://docs.fivem.net/docs/game-references/blips/ 348 | * - Colour List: https://runtime.fivem.net/doc/natives/?_0x03D7FB09E75D6B7E 349 | * 350 | * @example 351 | * ```ts 352 | * edit({ 353 | * "title": "Clothing Store" 354 | * }); 355 | * ``` 356 | */ 357 | edit: (blipId: number, newConfig: MapBlip) => { 358 | const oldConfig: MapBlip = (this.mapBlip as any).lists[blipId]; 359 | newConfig = { 360 | ...oldConfig, 361 | ...newConfig, 362 | location: { 363 | ...oldConfig.location, 364 | ...newConfig.location, 365 | }, 366 | }; 367 | 368 | this.mapBlip.remove(blipId); 369 | this.mapBlip.create(newConfig); 370 | 371 | return newConfig; 372 | }, 373 | 374 | /** 375 | * @description 376 | * Remove a map blip 377 | * 378 | * @example 379 | * ```ts 380 | * remove(1); 381 | * ``` 382 | */ 383 | remove: (blipId: number) => { 384 | RemoveBlip(blipId); 385 | }, 386 | }; 387 | } 388 | -------------------------------------------------------------------------------- /src/client/game.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import "@citizenfx/client"; 3 | 4 | import type Utils from "@client/utils"; 5 | 6 | export default class Game { 7 | constructor( 8 | private utils: Utils // 9 | ) {} 10 | 11 | /** 12 | * @description 13 | * Basically optimizing texture and things in GTA, such as clearing blood, wetness, dirt, etc. in ped 14 | * 15 | * @param ped Ped 16 | * 17 | * @example 18 | * ```ts 19 | * optimizeFPS(PlayerPedId()); 20 | * ``` 21 | */ 22 | optimizeFPS = (ped: number = PlayerPedId()) => { 23 | ClearAllBrokenGlass(); 24 | ClearAllHelpMessages(); 25 | LeaderboardsReadClearAll(); 26 | ClearBrief(); 27 | ClearGpsFlags(); 28 | ClearPrints(); 29 | ClearSmallPrints(); 30 | ClearReplayStats(); 31 | LeaderboardsClearCacheData(); 32 | ClearFocus(); 33 | ClearHdArea(); 34 | ClearPedBloodDamage(ped); 35 | ClearPedWetness(ped); 36 | ClearPedEnvDirt(ped); 37 | ResetPedVisibleDamage(ped); 38 | }; 39 | 40 | /** 41 | * @description 42 | * Disable default game dispatch radio 43 | * 44 | * @example 45 | * ```ts 46 | * disableDispatchService(); 47 | * ``` 48 | */ 49 | disableDispatchService = () => { 50 | for (let i = 0; i <= 20; i++) { 51 | EnableDispatchService(i, false); 52 | Citizen.invokeNative("0xDC0F817884CDD856", i, false); // EnableDispatchService in Native 53 | } 54 | }; 55 | 56 | /** 57 | * @description 58 | * Reset wanted level for a player 59 | * 60 | * @param playerId Player Ped 61 | * 62 | * @example 63 | * ```ts 64 | * resetWantedLevel(PlayerId()); 65 | * ``` 66 | */ 67 | resetWantedLevel = (playerId: number = PlayerId()) => { 68 | if (GetPlayerWantedLevel(playerId) !== 0) { 69 | SetPlayerWantedLevel(playerId, 0, false); 70 | SetPlayerWantedLevelNow(playerId, false); 71 | } 72 | 73 | return true; 74 | }; 75 | 76 | /** 77 | * @description 78 | * Only work on an entity (Ped, Player, Vehicle, Object, etc) 79 | */ 80 | entity = { 81 | /** 82 | * @description 83 | * Get coordinates of an entity 84 | * 85 | * @param entity Entity 86 | * 87 | * @example 88 | * ```ts 89 | * getCoords(PlayerPedId()); 90 | * ``` 91 | */ 92 | getCoords: (entity: number = PlayerPedId()) => { 93 | const [x, y, z] = GetEntityCoords(entity, true); 94 | const heading = GetEntityHeading(entity); 95 | 96 | return { x, y, z, heading }; 97 | }, 98 | }; 99 | 100 | /** 101 | * @description 102 | * Only work for a ped (Pedestrian, Player) 103 | */ 104 | ped = { 105 | /** 106 | * @description 107 | * Teleport to a waypoint marker set on the map 108 | * 109 | * @param ped Ped 110 | * 111 | * @example 112 | * ```ts 113 | * teleportToMarker(PlayerPedId()); 114 | * ``` 115 | */ 116 | teleportToMarker: async (ped: number = PlayerPedId()) => { 117 | if (!DoesEntityExist(ped) || !IsEntityAPed(ped)) { 118 | throw new Error("Entity given was not a ped!"); 119 | } 120 | 121 | const waypoint = GetFirstBlipInfoId(8); 122 | 123 | if (!DoesBlipExist(waypoint)) { 124 | throw new Error("Blip Doesn't Exist"); 125 | } 126 | 127 | const coords = GetBlipInfoIdCoord(waypoint); 128 | 129 | for (let height = 1; height <= 1000; height++) { 130 | const [, groundZ] = GetGroundZFor_3dCoord(coords[0], coords[1], height, false); 131 | 132 | this.ped.teleportToCoordinates(ped, { x: coords[0], y: coords[1], z: height }); 133 | 134 | await this.utils.sleep(5); 135 | if (groundZ) return true; 136 | } 137 | 138 | return false; 139 | }, 140 | 141 | /** 142 | * @description 143 | * Teleport to a coordinates 144 | * 145 | * @param ped Ped 146 | * @param coords Coordinate of the location 147 | * 148 | * @example 149 | * ```ts 150 | * teleportToCoordinates(PlayerPedId(), { 1000, 2000, 500 }); 151 | * ``` 152 | */ 153 | teleportToCoordinates: (ped: number = PlayerPedId(), coords: { x: string | number; y: string | number; z: string | number }) => { 154 | if (!DoesEntityExist(ped) || !IsEntityAPed(ped)) { 155 | throw new Error("Entity given was not a ped!"); 156 | } 157 | 158 | const parseCoords = (axis: string | number) => { 159 | if (typeof axis === "string") { 160 | const parsedString = axis.replace(/,/g, ""); 161 | axis = parseInt(parsedString); 162 | } 163 | 164 | return axis; 165 | }; 166 | 167 | SetPedCoordsKeepVehicle(ped, parseCoords(coords.x), parseCoords(coords.y), parseCoords(coords.z)); 168 | 169 | return true; 170 | }, 171 | 172 | /** 173 | * @description 174 | * Get current vehicle of a ped 175 | * 176 | * @param ped Ped 177 | * @param isDriver If set to true, it will return false if the player is in the vehicle not as a driver. 178 | * 179 | * @example 180 | * ```ts 181 | * getCurrentVehicle(PlayerPedId(), false); 182 | * ``` 183 | */ 184 | getCurrentVehicle: (ped: number = PlayerPedId(), isDriver = false) => { 185 | if (!DoesEntityExist(ped) || !IsEntityAPed(ped)) { 186 | throw new Error("Entity given was not a ped!"); 187 | } 188 | 189 | if (IsPedSittingInAnyVehicle(ped)) { 190 | const vehicle = GetVehiclePedIsIn(ped, false); 191 | 192 | if (!isDriver || (isDriver && GetPedInVehicleSeat(vehicle, -1) == ped)) { 193 | return vehicle; 194 | } 195 | } 196 | 197 | return false; 198 | }, 199 | }; 200 | 201 | /** 202 | * @description 203 | * Only work for an active player 204 | */ 205 | player = { 206 | /** 207 | * @description 208 | * Set a local player to disable collision. 209 | * 210 | * @param playerPed Player Ped 211 | * 212 | * @example 213 | * ```ts 214 | * setNoClip(PlayerPedId()); 215 | * ``` 216 | */ 217 | setNoClip: async (speed = 2) => { 218 | const ped = PlayerPedId(); 219 | const entity = IsPedInAnyVehicle(ped, false) ? GetVehiclePedIsUsing(ped) : ped; 220 | 221 | let camHeading = this.utils.game.getCamDirection(); 222 | let { x, y, z, heading } = this.entity.getCoords(entity); 223 | 224 | // INPUT_MOVE_UP_ONLY (W) 225 | DisableControlAction(0, 32, true); 226 | if (IsDisabledControlPressed(0, 32)) { 227 | x = x + speed * camHeading.x; 228 | y = y + speed * camHeading.y; 229 | z = z + speed * camHeading.z; 230 | } 231 | 232 | // INPUT_MOVE_DOWN_ONLY (S) 233 | DisableControlAction(0, 33, true); 234 | if (IsDisabledControlPressed(0, 33)) { 235 | x = x - speed * camHeading.x; 236 | y = y - speed * camHeading.y; 237 | z = z - speed * camHeading.z; 238 | } 239 | 240 | // INPUT_MOVE_LEFT_ONLY (A) 241 | DisableControlAction(0, 34, true); 242 | if (IsDisabledControlPressed(0, 34)) heading += 3.0; 243 | 244 | // INPUT_MOVE_RIGHT_ONLY (D) 245 | DisableControlAction(0, 35, true); 246 | if (IsDisabledControlPressed(0, 35)) heading -= 3.0; 247 | 248 | // INPUT_COVER (Q) 249 | DisableControlAction(0, 44, true); 250 | if (IsDisabledControlPressed(0, 44)) z += 0.21 * (speed + 0.3); 251 | 252 | // INPUT_MULTIPLAYER_INFO (Z) 253 | DisableControlAction(0, 20, true); 254 | if (IsDisabledControlPressed(0, 20)) z -= 0.21 * (speed + 0.3); 255 | 256 | DisableControlAction(0, 268, true); 257 | DisableControlAction(0, 31, true); 258 | DisableControlAction(0, 269, true); 259 | DisableControlAction(0, 266, true); 260 | DisableControlAction(0, 30, true); 261 | DisableControlAction(0, 267, true); 262 | 263 | SetEntityVelocity(entity, 0.0, 0.0, 0.0); 264 | SetEntityRotation(entity, 0.0, 0.0, 0.0, 0, false); 265 | SetEntityCollision(entity, false, false); 266 | 267 | SetEntityHeading(entity, heading); 268 | SetEntityCoordsNoOffset(entity, x, y, z, true, true, true); 269 | 270 | await this.utils.game.drawInstructionalButtons([ 271 | { controlType: 0, controlId: 32, message: "Move Forward" }, 272 | { controlType: 0, controlId: 33, message: "Move Backward" }, 273 | { controlType: 0, controlId: 34, message: "Rotate Left" }, 274 | { controlType: 0, controlId: 35, message: "Rotate Right" }, 275 | { controlType: 0, controlId: 44, message: "Fly Up" }, 276 | { controlType: 0, controlId: 20, message: "Fly Down" }, 277 | ]); 278 | 279 | return true; 280 | }, 281 | 282 | /** 283 | * @description 284 | * Get Nearest player from a coords. Returning list of player ids if found. 285 | * 286 | * @param _coords Coordinates of the location to find the player 287 | * 288 | * @example 289 | * ```ts 290 | * getNearestOneIn({ 100, 300, 400, 5.0 }); 291 | * ``` 292 | */ 293 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 294 | getNearestOneIn: async (coords: { x: number; y: number; z: number; radius?: number }) => { 295 | // const players = await this.client.players.list(); 296 | let playerList: any[] = []; 297 | 298 | // for (const player of players) { 299 | // const playerId = GetPlayerFromServerId(player.server_id); 300 | // const ped = GetPlayerPed(playerId); 301 | // const playerCoords = this.entity.getCoords(ped); 302 | // const distance = GetDistanceBetweenCoords(coords.x, coords.y, coords.z, playerCoords.x, playerCoords.y, playerCoords.z, true); 303 | 304 | // if (distance <= (coords.radius || 5.0)) { 305 | // playerList.push(player.server_id); 306 | // } 307 | // } 308 | 309 | return playerList; 310 | }, 311 | 312 | /** 313 | * @description 314 | * Get a closest player near an entity (Ped, Object, etc). 315 | * 316 | * @param entity Entity (Ped, Object, etc). 317 | * 318 | * @example 319 | * ```ts 320 | * getNearestOneFrom(PlayerPedId()); 321 | * ``` 322 | */ 323 | getNearestOneFrom: (entity: number, radius: number) => { 324 | if (!DoesEntityExist(entity)) { 325 | throw new Error("Entity does not exist"); 326 | } 327 | 328 | const { x, y, z } = this.entity.getCoords(entity); 329 | return this.player.getNearestOneIn({ x, y, z, radius }); 330 | }, 331 | }; 332 | 333 | /** 334 | * @description 335 | * Only work with vehicles 336 | */ 337 | vehicle = { 338 | /** 339 | * @description 340 | * Spawn a vehicle based on its model name 341 | * 342 | * @param model Vehicle Model Name 343 | * @param coords Coordinate of the location to spawn 344 | * 345 | * @example 346 | * ```ts 347 | * spawn('zentorno', { 1000, 5000, 500, 50 }) 348 | * ``` 349 | */ 350 | spawn: async (model: string, coords: { x: number; y: number; z: number; heading?: number } = this.entity.getCoords()) => { 351 | const hash = GetHashKey(model); 352 | 353 | if (!IsModelInCdimage(hash) || !IsModelAVehicle(hash)) { 354 | throw new Error("Requested Model is Not Found!"); 355 | } 356 | 357 | RequestModel(hash); 358 | while (!HasModelLoaded(hash)) { 359 | await this.utils.sleep(500); 360 | } 361 | 362 | const vehicle = CreateVehicle(hash, coords.x, coords.y, coords.z, coords.heading || 0, true, false); 363 | 364 | SetEntityAsNoLongerNeeded(vehicle); 365 | SetModelAsNoLongerNeeded(model); 366 | 367 | return true; 368 | }, 369 | 370 | /** 371 | * @description 372 | * Delete vehicle 373 | * 374 | * @param vehicleEntityOrCoords Entity of the vehicle or the coordinate 375 | * 376 | * @example 377 | * ```ts 378 | * delete(1024); 379 | * ``` 380 | */ 381 | delete: (vehicle: number) => { 382 | if (!DoesEntityExist(vehicle) || !IsEntityAVehicle(vehicle)) { 383 | throw new Error("Vehicle does not exist!"); 384 | } 385 | 386 | SetEntityAsMissionEntity(vehicle, true, true); 387 | DeleteVehicle(vehicle); 388 | 389 | return true; 390 | }, 391 | 392 | /** 393 | * @description 394 | * Get a closest vehicle near a coordinate 395 | * 396 | * @param coords Coordinate of the location to find vehicle 397 | * 398 | * @example 399 | * ```ts 400 | * getNearestOneIn({ 1000, 5000, 300, 5.0 }); 401 | * ``` 402 | */ 403 | getNearestOneIn: (coords: { x: number; y: number; z: number; radius?: number }) => { 404 | const vehicleFlags = [0, 2, 4, 6, 7, 23, 127, 260, 2146, 2175, 12294, 16384, 16386, 20503, 32768, 67590, 67711, 98309, 100359]; 405 | 406 | for (const flag of vehicleFlags) { 407 | const vehicle = GetClosestVehicle(coords.x, coords.y, coords.z, coords.radius || 5.0, 0, flag); 408 | 409 | if (vehicle) return vehicle; 410 | } 411 | 412 | return false; 413 | }, 414 | 415 | /** 416 | * @description 417 | * Get a closest vehicle near an entity (Ped, Object, etc). 418 | * 419 | * @param entity Entity (Ped, Object, etc). 420 | * 421 | * @example 422 | * ```ts 423 | * getNearestOneFrom(PlayerPedId()); 424 | * ``` 425 | */ 426 | getNearestOneFrom: (entity: number, radius?: number) => { 427 | if (!DoesEntityExist(entity)) { 428 | throw new Error("Entity does not exist"); 429 | } 430 | 431 | const { x, y, z } = this.entity.getCoords(entity); 432 | return this.vehicle.getNearestOneIn({ x, y, z, radius }); 433 | }, 434 | }; 435 | } 436 | --------------------------------------------------------------------------------