├── .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 | * 
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(``);
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 |   
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 | * 
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 | * 
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 | * 
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 | * 
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 | * 
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 |
--------------------------------------------------------------------------------