diff --git a/.env.example b/.env.example index cf5f06307..40606efdc 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,6 @@ # Connection string used to connect to a MongoDB database -MONGODB=mongodb://127.0.0.1:27017 \ No newline at end of file +MONGODB=mongodb://127.0.0.1:27017 + +# You can set database name here, so it will populate the config with the value. +# Don't forget to uncomment the line below if you want to change: +# DATABASE_NAME=Rebar diff --git a/docs/changelog.md b/docs/changelog.md index 65eff0c23..63e2b8a96 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,25 @@ order: 95 # Changelog +## Version 52 + +### Code Changes + +- Added ability to change database name through environment variables — @floydya +- Added door system controller — @floydya +- Added server config option for disabling weapon wheel menu — @mnkyarts +- Added onHold callback for `useKeypress` that invokes after `x` time has passed while holding a key — @koron +- Updated `useKeypress` to include `onHold`, which is invoked after `2s` of holding + +### Docs Changes + +- Added door system docs @floydya +- Added `env` options for database name to useConfig page +- Updated server config docs @mnkyarts +- Updated `useKeypress` docs to include `onHold` + +--- + ## Version 51 ### Code Changes diff --git a/docs/data/doors.md b/docs/data/doors.md new file mode 100644 index 000000000..7e791eb89 --- /dev/null +++ b/docs/data/doors.md @@ -0,0 +1,106 @@ +# Doors + +List of doors that could be used with useDoor controller. + +| Description | Model | Position | +| ------------------------------------------------------- | ------------- | ------------------------------------------------ | +| Pacific Standard Bank Main Right Door | `110411286` | `new alt.Vector3(232.6054, 214.1584, 106.4049` | +| Pacific Standard Bank Main Left Door | `110411286` | `new alt.Vector3(231.5123, 216.5177, 106.4049` | +| Pacific Standard Bank Back Right Door | `110411286` | `new alt.Vector3(260.6432, 203.2052, 106.4049` | +| Pacific Standard Bank Back Left Door | `110411286` | `new alt.Vector3(258.2022, 204.1005, 106.4049` | +| Pacific Standard Bank Door To Upstair | `1956494919` | `new alt.Vector3(237.7704, 227.87, 106.426` | +| Pacific Standard Bank Upstair Door | `1956494919` | `new alt.Vector3(236.5488, 228.3147, 110.4328` | +| Pacific Standard Bank Back To Hall Right Door | `110411286` | `new alt.Vector3(259.9831, 215.2468, 106.4049` | +| Pacific Standard Bank Back To Hall Left Door | `110411286` | `new alt.Vector3(259.0879, 212.8062, 106.4049` | +| Pacific Standard Bank Upstair Door To Offices | `1956494919` | `new alt.Vector3(256.6172, 206.1522, 110.4328` | +| Pacific Standard Bank Big Office Door | `964838196` | `new alt.Vector3(260.8579, 210.4453, 110.4328` | +| Pacific Standard Bank Small Office Door | `964838196` | `new alt.Vector3(262.5366, 215.0576, 110.4328` | +| Discount Store South Enter Right Door | `-1148826190` | `new alt.Vector3(82.38156, -1390.476, 29.52609` | +| Discount Store South Enter Left Door | `868499217` | `new alt.Vector3(82.38156, -1390.752, 29.52609` | +| Los Santos Customs Popular Street Door | `270330101` | `new alt.Vector3(723.116, -1088.831, 23.23201` | +| Los Santos Customs Carcer Way Door | `-550347177` | `new alt.Vector3(-356.0905, -134.7714, 40.01295` | +| Los Santos Customs Greenwich Parkway Door | `-550347177` | `new alt.Vector3(-1145.898, -1991.144, 14.18357` | +| Los Santos Customs Route 68 Right Garage Door | `-822900180` | `new alt.Vector3(1174.656, 2644.159, 40.50673` | +| Los Santos Customs Route 68 Left Garage Door | `-822900180` | `new alt.Vector3(1182.307, 2644.166, 40.50784` | +| Los Santos Customs Route 68 Office Door | `1335311341` | `new alt.Vector3(1187.202, 2644.95, 38.55176` | +| Los Santos Customs Route 68 Interior Door | `1544229216` | `new alt.Vector3(1182.646, 2641.182, 39.31031` | +| Beeker's Garage Paleto Bay Right Garage Door | `-822900180` | `new alt.Vector3(114.3135, 6623.233, 32.67305` | +| Beeker's Garage Paleto Bay Left Garage Door | `-822900180` | `new alt.Vector3(108.8502, 6617.877, 32.67305` | +| Beeker's Garage Paleto Bay Office Door | `1335311341` | `new alt.Vector3(105.1518, 6614.655, 32.58521` | +| Beeker's Garage Paleto Bay Interior Door | `1544229216` | `new alt.Vector3(105.7772, 6620.532, 33.34266` | +| Ammu Nation Vespucci Boulevard Right Door | `-8873588` | `new alt.Vector3(842.7685, -1024.539, 28.34478` | +| Ammu Nation Vespucci Boulevard Left Door | `97297972` | `new alt.Vector3(845.3694, -1024.539, 28.34478` | +| Ammu Nation Lindsay Circus Right Door | `-8873588` | `new alt.Vector3(-662.6415, -944.3256, 21.97915` | +| Ammu Nation Lindsay Circus Left Door | `97297972` | `new alt.Vector3(-665.2424, -944.3256, 21.97915` | +| Ammu Nation Popular Street Right Door | `-8873588` | `new alt.Vector3(810.5769, -2148.27, 29.76892` | +| Ammu Nation Popular Street Left Door | `97297972` | `new alt.Vector3(813.1779, -2148.27, 29.76892` | +| Ammu Nation Popular Street Shooting Range Door | `452874391` | `new alt.Vector3(827.5342, -2160.493, 29.76884` | +| Ammu Nation Adam's Apple Boulevard Right Door | `-8873588` | `new alt.Vector3(18.572, -1115.495, 29.94694` | +| Ammu Nation Adam's Apple Boulevard Left Door | `97297972` | `new alt.Vector3(16.12787, -1114.606, 29.94694` | +| Ammu Nation Adam's Apple Boulevard Shooting Range Door | `452874391` | `new alt.Vector3(6.81789, -1098.209, 29.94685` | +| Ammu Nation Vinewood Plaza Right Door | `-8873588` | `new alt.Vector3(243.8379, -46.52324, 70.09098` | +| Ammu Nation Vinewood Plaza Left Door | `97297972` | `new alt.Vector3(244.7275, -44.07911, 70.09098` | +| Ponsonbys Portola Drive Right Door | `-1922281023` | `new alt.Vector3(-715.6154, -157.2561, 37.67493` | +| Ponsonbys Portola Drive Left Door | `-1922281023` | `new alt.Vector3(-716.6755, -155.42, 37.67493` | +| Ponsonbys Cougar Avenue Right Door | `-1922281023` | `new alt.Vector3(-1456.201, -233.3682, 50.05648` | +| Ponsonbys Cougar Avenue Left Door | `-1922281023` | `new alt.Vector3(-1454.782, -231.7927, 50.05649` | +| Ponsonbys Rockford Plaza Right Door | `-1922281023` | `new alt.Vector3(-156.439, -304.4294, 39.99308` | +| Ponsonbys Rockford Plaza Left Door | `-1922281023` | `new alt.Vector3(-157.1293, -306.4341, 39.99308` | +| Sub Urban Prosperity Street Promenade Door | `1780022985` | `new alt.Vector3(-1201.435, -776.8566, 17.99184` | +| Sub Urban Hawick Avenue Door | `1780022985` | `new alt.Vector3(127.8201, -211.8274, 55.22751` | +| Sub Urban Route 68 Door | `1780022985` | `new alt.Vector3(617.2458, 2751.022, 42.75777` | +| Sub Urban Chumash Plaza Door | `1780022985` | `new alt.Vector3(-3167.75, 1055.536, 21.53288` | +| Rob's Liquor Route 1 Main Enter Door | `-1212951353` | `new alt.Vector3(-2973.535, 390.1414, 15.18735` | +| Rob's Liquor Route 1 Personnal Door | `1173348778` | `new alt.Vector3(-2965.648, 386.7928, 15.18735` | +| Rob's Liquor Route 1 Back Door | `1173348778` | `new alt.Vector3(-2961.749, 390.2573, 15.19322` | +| Rob's Liquor Prosperity Street Main Enter Door | `-1212951353` | `new alt.Vector3(-1490.411, -383.8453, 40.30745` | +| Rob's Liquor Prosperity Street Personnal Door | `1173348778` | `new alt.Vector3(-1482.679, -380.153, 40.30745` | +| Rob's Liquor Prosperity Street Back Door | `1173348778` | `new alt.Vector3(-1482.693, -374.9365, 40.31332` | +| Rob's Liquor San Andreas Avenue Main Enter Door | `-1212951353` | `new alt.Vector3(-1226.894, -903.1218, 12.47039` | +| Rob's Liquor San Andreas Avenue Personnal Door | `1173348778` | `new alt.Vector3(-1224.755, -911.4182, 12.47039` | +| Rob's Liquor San Andreas Avenue Back Door | `1173348778` | `new alt.Vector3(-1219.633, -912.406, 12.47626` | +| Rob's Liquor El Rancho Boulevard Main Enter Door | `-1212951353` | `new alt.Vector3(1141.038, -980.3225, 46.55986` | +| Rob's Liquor El Rancho Boulevard Personnal Door | `1173348778` | `new alt.Vector3(1132.645, -978.6059, 46.55986` | +| Rob's Liquor El Rancho Boulevard Back Door | `1173348778` | `new alt.Vector3(1129.51, -982.7756, 46.56573` | +| Bob Mulét Barber Shop Right Door | `145369505` | `new alt.Vector3(-822.4442, -188.3924, 37.81895` | +| Bob Mulét Barber Shop Left Door | `-1663512092` | `new alt.Vector3(-823.2001, -187.0831, 37.81895` | +| Hair on Hawick Barber Shop Door | `-1844444717` | `new alt.Vector3(-29.86917, -148.1571, 57.22648` | +| O'Sheas Barber Shop Door | `-1844444717` | `new alt.Vector3(1932.952, 3725.154, 32.9944` | +| Premium Deluxe Motorsport Parking Right Door | `1417577297` | `new alt.Vector3(-37.33113, -1108.873, 26.7198` | +| Premium Deluxe Motorsport Parking Left Door | `2059227086` | `new alt.Vector3(-39.13366, -1108.218, 26.7198` | +| Premium Deluxe Motorsport Main Right Door | `1417577297` | `new alt.Vector3(-60.54582, -1094.749, 26.88872` | +| Premium Deluxe Motorsport Main Left Door | `2059227086` | `new alt.Vector3(-59.89302, -1092.952, 26.88362` | +| Premium Deluxe Motorsport Right Office Door | `-2051651622` | `new alt.Vector3(-33.80989, -1107.579, 26.57225` | +| Premium Deluxe Motorsport Left Office Door | `-2051651622` | `new alt.Vector3(-31.72353, -1101.847, 26.57225` | +| Franklin House Enter Door | `520341586` | `new alt.Vector3(-14.86892, -1441.182, 31.19323` | +| Franklin House Garage Door | `703855057` | `new alt.Vector3(-25.2784, -1431.061, 30.83955` | +| Vanilla Unicorn Main Enter Door | `-1116041313` | `new alt.Vector3(127.9552, -1298.503, 29.41962` | +| Vanilla Unicorn Back Enter Door | `668467214` | `new alt.Vector3(96.09197, -1284.854, 29.43878` | +| Vanilla Unicorn Office Door | `-626684119` | `new alt.Vector3(99.08321, -1293.701, 29.41868` | +| Vanilla Unicorn Dress Door | `-495720969` | `new alt.Vector3(113.9822, -1297.43, 29.41868` | +| Vanilla Unicorn Private Rooms Door | `-1881825907` | `new alt.Vector3(116.0046, -1294.692, 29.41947` | +| Bolingbroke Penitentiary Main Enter First Door | `741314661` | `new alt.Vector3(1844.998, 2597.482, 44.63626` | +| Bolingbroke Penitentiary Main Enter Second Door | `741314661` | `new alt.Vector3(1818.543, 2597.482, 44.60749` | +| Bolingbroke Penitentiary Main Enter Third Door | `741314661` | `new alt.Vector3(1806.939, 2616.975, 44.60093` | +| Mission Row Police Station Main Enter Right Door | `320433149` | `new alt.Vector3(434.7479, -983.2151, 30.83926` | +| Mission Row Police Station Main Enter Left Door | `-1215222675` | `new alt.Vector3(434.7479, -980.6184, 30.83926` | +| Mission Row Police Station Back Enter Right Door | `-2023754432` | `new alt.Vector3(469.9679, -1014.452, 26.53623` | +| Mission Row Police Station Back Enter Left Door | `-2023754432` | `new alt.Vector3(467.3716, -1014.452, 26.53623` | +| Mission Row Police Station Back To Cells Door | `-1033001619` | `new alt.Vector3(463.4782, -1003.538, 25.00599` | +| Mission Row Police Station Cell Door 1 | `631614199` | `new alt.Vector3(461.8065, -994.4086, 25.06443` | +| Mission Row Police Station Cell Door 2 | `631614199` | `new alt.Vector3(461.8065, -997.6583, 25.06443` | +| Mission Row Police Station Cell Door 3 | `631614199` | `new alt.Vector3(461.8065, -1001.302, 25.06443` | +| Mission Row Police Station Door To Cells Door | `631614199` | `new alt.Vector3(464.5701, -992.6641, 25.06443` | +| Mission Row Police Station Captan's Office Door | `-1320876379` | `new alt.Vector3(446.5728, -980.0106, 30.8393` | +| Mission Row Police Station Armory Double Door Right | `185711165` | `new alt.Vector3(450.1041, -984.0915, 30.8393` | +| Mission Row Police Station Armory Double Door Left | `185711165` | `new alt.Vector3(450.1041, -981.4915, 30.8393` | +| Mission Row Police Station Armory Secure Door | `749848321` | `new alt.Vector3(453.0793, -983.1895, 30.83926` | +| Mission Row Police Station Locker Rooms Door | `1557126584` | `new alt.Vector3(450.1041, -985.7384, 30.8393` | +| Mission Row Police Station Locker Room 1 Door | `-2023754432` | `new alt.Vector3(452.6248, -987.3626, 30.8393` | +| Mission Row Police Station Roof Access Door | `749848321` | `new alt.Vector3(461.2865, -985.3206, 30.83926` | +| Mission Row Police Station Roof Door | `-340230128` | `new alt.Vector3(464.3613, -984.678, 43.83443` | +| Mission Row Police Station Cell And Briefing Right Door | `185711165` | `new alt.Vector3(443.4078, -989.4454, 30.8393` | +| Mission Row Police Station Cell And Briefing Left Door | `185711165` | `new alt.Vector3(446.0079, -989.4454, 30.8393` | +| Mission Row Police Station Briefing Right Door | `-131296141` | `new alt.Vector3(443.0298, -991.941, 30.8393` | +| Mission Row Police Station Briefing Left Door | `-131296141` | `new alt.Vector3(443.0298, -994.5412, 30.8393` | +| Mission Row Police Station Back Gate Door | `-1603817716` | `new alt.Vector3(488.8923, -1011.67, 27.14583` | diff --git a/docs/useRebar/controllers.md b/docs/useRebar/controllers.md index c3c35d9b7..68618ff8c 100644 --- a/docs/useRebar/controllers.md +++ b/docs/useRebar/controllers.md @@ -13,6 +13,7 @@ Controllers pretty much make your gamemode tick by displaying a lot information - Interactable Positions - Menus - Text Labels +- Doors - etc. --- @@ -686,3 +687,59 @@ async function showSomeMenu(player: alt.Player) { console.log(result); } ``` + +## Doors + +Doors are objects that can be opened and closed. When they are locked, no one can bypass them. + +### useDoor + +```ts +import { useRebar } from '@Server/index.js'; +import { DoorState } from '@Shared/types/index.js'; + +const Rebar = useRebar(); +const doorController = Rebar.controllers.useDoor(); + +// Register a door +doorController.register({ + uid: 'pacific-standard-bank-main-right-door', + state: DoorState.LOCKED, + model: 110411286, + pos: { x: 232.6054, y: 214.1584, z: 106.4049 }, + permissions: ['admin', 'bankOperator'], +}); + + +// Then we can add few interactions, that will be applied for all doors in the game: + +const keybinder = Rebar.systems.useKeybinder(); +// Toggle the door state when the player presses 'K'. +// If player has permission to toggle lock state of the door, it will toggle it. +keybinder.on(75, async (player: alt.Player) => { + const nearestDoor = await doorController.getNearestDoor(player); + if (!nearestDoor) return; + doorController.toggleLockState(player, nearestDoor.uid); +}); + + +const { commands, message } = Rebar.systems.useMessenger(); +// Register a command to lock/unlock the door for testing purposes. +// /lockdoor [uid] +commands.register({ + name: 'lockdoor', + desc: '[uid] – Locks the door.', + callback: (player: alt.Player, doorUid: string) => { + doorController.forceSetLockState(doorUid, DoorState.LOCKED); + } +}) + +// /unlockdoor [uid] +commands.register({ + name: 'unlockdoor', + desc: '[uid] – Unlocks the door.', + callback: (player: alt.Player, doorUid: string) => { + doorController.forceSetLockState(doorUid, DoorState.UNLOCKED); + } +}) +``` diff --git a/docs/useRebar/systems/useKeypress.md b/docs/useRebar/systems/useKeypress.md index f51bc951e..841901455 100644 --- a/docs/useRebar/systems/useKeypress.md +++ b/docs/useRebar/systems/useKeypress.md @@ -2,16 +2,59 @@ Keypress lets you handle key up and key down from server-side. It completely ignores if a `page` is open. -```ts -const keypress = Rebar.useKeypress(); +## on + +Each callback is called when the key is pressed down, and when the key is let go after being held down. -keypress.on( - 118, - (player: alt.Player) => { - console.log('up!'); +```ts +// Uses the 'F3' key +Rebar.systems.useKeypress().on( + 114, + // on key up + (player) => { + alt.log('they let go of the key'); }, - (player: alt.Player) => { - console.log('down!'); + // on key down + (player) => { + alt.log('they pressed the key down'); }, ); ``` + +## onHold + +The `onHold` callback is invoked after `2s` on client-side. + +Additionally, the callback time is verified server-side as well to ensure callback times are accurate and can't be invoked manually. + +### Simple Usage + +If you only want it to be invoked if the player holds a key long enough, then here's the short form. + +```ts +// Uses the 'F4' key +Rebar.systems.useKeypress().onHold(115, { + hold: (player) => { + console.log('player held the key long enough'); + }, +}); +``` + +### Advanced Usage + +```ts +function handleHoldUp(player: alt.Player) { + alt.log('just let go of held key'); +} + +function handleHoldDown(player: alt.Player) { + alt.log('just pressed held key down'); +} + +function handleHoldKey(player: alt.Player) { + alt.log('player held the key for a long time'); +} + +// Uses the 'F4' key +Rebar.systems.useKeypress().onHold(115, { hold: handleHoldKey, up: handleHoldUp, down: handleHoldDown }); +``` diff --git a/docs/useRebar/systems/useServerConfig.md b/docs/useRebar/systems/useServerConfig.md index ccdb97b25..86335397a 100644 --- a/docs/useRebar/systems/useServerConfig.md +++ b/docs/useRebar/systems/useServerConfig.md @@ -57,4 +57,7 @@ serverConfig.set('disableDriveBys', true); // Disables ambient noises around the map serverConfig.set('disableAmbientNoise', true); + +// Disables the Weapon Radial Menu +serverConfig.set('disableWeaponRadial', true); ``` diff --git a/docs/useRebar/useConfig.md b/docs/useRebar/useConfig.md index ec94affcb..8b02da4ed 100644 --- a/docs/useRebar/useConfig.md +++ b/docs/useRebar/useConfig.md @@ -12,9 +12,14 @@ This document provides an overview of the configuration management for a Rebar s - Setting a default value if env variable was not defined. - Basic type validation - number, boolean, string (by default). -## Usage +## Defaults + +| Config Variable | Env Variable | Type | Default | +|-----------------|---------------|--------|---------------------------| +| `mongodb` | `MONGODB` | `string` | `mongodb://127.0.0.1:27017` | +| `database_name` | `DATABASE_NAME` | `string` | `Rebar` | -By default, `mongodb` is initialized automatically. It is required and has default value of local mongodb instance. +## Usage Config defined as an interface, all methods covered with type hints, so you will see all variables and correct types on each method call. diff --git a/package.json b/package.json index b403a17e9..17d81ecd0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "author": "stuyk", "type": "module", - "version": "51", + "version": "52", "scripts": { "dev": "nodemon --config ./nodemon-dev.json -x pnpm start", "dev:linux": "nodemon --config ./nodemon-dev.json -x pnpm start:linux", diff --git a/src/main/client/system/serverConfig.ts b/src/main/client/system/serverConfig.ts index 4664f6c7e..56a01306a 100644 --- a/src/main/client/system/serverConfig.ts +++ b/src/main/client/system/serverConfig.ts @@ -50,6 +50,10 @@ function tick() { native.setPlayerCanUseCover(alt.Player.local, false); } + if(config.disableWeaponRadial) { + native.disableControlAction(0, 37, true); + } + let finalRadarState = true; if (config.hideMinimapOnFoot) { diff --git a/src/main/client/system/serverKeypress.ts b/src/main/client/system/serverKeypress.ts index 5ae609633..36879c06e 100644 --- a/src/main/client/system/serverKeypress.ts +++ b/src/main/client/system/serverKeypress.ts @@ -1,11 +1,13 @@ import * as alt from 'alt-client'; -import { Events } from '../../shared/events/index.js'; +import { Events } from '@Shared/events/index.js'; import { useMessenger } from './messenger.js'; const messenger = useMessenger(); const validKeys: Map = new Map(); const nextValidUpPress: { [key: string]: number } = {}; const nextValidDownPress: { [key: string]: number } = {}; +const keyPressTracker: { [key: number]: number } = {}; +const requiredPressDuration = 2000; function handleUpdate(keys: string[]) { validKeys.clear(); @@ -24,6 +26,10 @@ function handleKeyup(key: number) { return; } + if (keyPressTracker[key]) { + delete keyPressTracker[key]; + } + if (messenger.isChatFocused()) { return; } @@ -61,10 +67,44 @@ function handleKeyDown(key: number) { return; } + if (!keyPressTracker[key]) { + keyPressTracker[key] = Date.now() + requiredPressDuration; + } + nextValidDownPress[key] = Date.now() + 500; alt.emitServer(Events.systems.keypress.invokeDown, key); } +function handleKeyHold() { + if (!alt.gameControlsEnabled()) { + return; + } + + if (messenger.isChatFocused()) { + return; + } + + if (alt.isConsoleOpen()) { + return; + } + + const keyPresses = { ...keyPressTracker }; + for (const key in keyPresses) { + const invokeTime = keyPressTracker[key]; + + if (Date.now() < invokeTime) { + continue; + } + + if (keyPressTracker[key]) { + delete keyPressTracker[key]; + } + + alt.emitServer(Events.systems.keypress.invokeHold, parseInt(key)); + } +} + alt.onServer(Events.systems.keypress.update, handleUpdate); +alt.everyTick(handleKeyHold); alt.on('keyup', handleKeyup); alt.on('keydown', handleKeyDown); diff --git a/src/main/client/virtualEntities/doors.ts b/src/main/client/virtualEntities/doors.ts new file mode 100644 index 000000000..e574d14f5 --- /dev/null +++ b/src/main/client/virtualEntities/doors.ts @@ -0,0 +1,94 @@ +import * as alt from 'alt-client'; +import * as native from 'natives'; + +import { Door, DoorState } from '@Shared/types/index.js'; +import { drawText3D } from '@Client/screen/textlabel.js'; + +let doors: Array = []; +let interval: number; + +function draw() { + if (doors.length <= 0) return; + + for (const door of doors) { + if (alt.debug) { + const dist = alt.Player.local.pos.distanceTo(door.pos); + if (dist > 5) continue; + drawText3D(`UID: ${door.uid} - State: ${door.state}`, door.pos, 0.5, new alt.RGBA(255, 255, 255, 255)); + } + + native.setStateOfClosestDoorOfType( + door.model, + door.pos.x, + door.pos.y, + door.pos.z, + door.state === DoorState.LOCKED, + 0, + false, + ); + } +} + +function onStreamEnter(entity: alt.Object) { + if (!isDoor(entity)) return; + + if (!interval) { + interval = alt.setInterval(draw, 0); + } + + const data = getData(entity); + if (!data) return; + + const index = doors.findIndex((x) => x.uid === data.uid); + if (index !== -1) { + doors[index] = { ...data, entity }; + } else { + doors.push({ ...data, entity }); + } +} + +function onStreamExit(entity: alt.Object) { + if (!isDoor(entity)) return; + + const data = getData(entity); + if (!data) return; + + for (let i = doors.length - 1; i >= 0; i--) { + if (doors[i].uid !== data.uid) continue; + doors.splice(i, 1); + } + + if (doors.length <= 0) { + alt.clearInterval(interval); + interval = undefined; + } +} + +function onStreamSyncedMetaChanged(entity: alt.Object, key: string, value: any) { + if (!isDoor(entity)) return; + + const data = getData(entity); + if (!data) return; + + const index = doors.findIndex((x) => x.uid === data.uid); + if (index <= -1) return; + + doors[index] = { ...data, entity }; +} + +function getData(object: alt.Object): Door { + return object.getStreamSyncedMeta('door') as Door; +} + +function isDoor(object: alt.Object) { + if (!(object instanceof alt.VirtualEntity)) { + return false; + } + + return object.getStreamSyncedMeta('type') === 'door'; +} + +alt.log(`Virtual Entities - Loaded Doors Handler`); +alt.on('worldObjectStreamIn', onStreamEnter); +alt.on('worldObjectStreamOut', onStreamExit); +alt.on('streamSyncedMetaChange', onStreamSyncedMetaChanged); diff --git a/src/main/client/virtualEntities/index.ts b/src/main/client/virtualEntities/index.ts index 2c29f4b82..bad951276 100644 --- a/src/main/client/virtualEntities/index.ts +++ b/src/main/client/virtualEntities/index.ts @@ -1,6 +1,7 @@ import * as alt from 'alt-client'; import './d2dTextLabel.js'; +import './doors.js'; import './marker.js'; import './progressbar.js'; import './textlabel.js'; diff --git a/src/main/client/webview/index.ts b/src/main/client/webview/index.ts index 2dc5c2c17..99ef3f64f 100644 --- a/src/main/client/webview/index.ts +++ b/src/main/client/webview/index.ts @@ -325,7 +325,11 @@ export function useWebview(path = 'http://assets/webview/index.html') { } function onSyncedMetaChange(object: alt.Object, key: string, newValue: any) { - if (native.isEntityAPed(object.scriptID) && object.scriptID === alt.Player.local.scriptID) { + if ( + typeof object.scriptID === 'number' && + native.isEntityAPed(object.scriptID) && + object.scriptID === alt.Player.local.scriptID + ) { webview.emit(Events.view.syncPartialCharacter, key, newValue); return; } diff --git a/src/main/server/config/index.ts b/src/main/server/config/index.ts index d7059f95f..6ce1df072 100644 --- a/src/main/server/config/index.ts +++ b/src/main/server/config/index.ts @@ -2,6 +2,7 @@ import dotenv from 'dotenv'; export interface Config { mongodb: string; + database_name: string; } export interface ConfigInit { @@ -116,3 +117,8 @@ useConfig().initFromEnv('mongodb', { env: 'MONGODB', default: 'mongodb://127.0.0.1:27017' }); + +useConfig().initFromEnv('database_name', { + env: 'DATABASE_NAME', + default: 'Rebar', +}); diff --git a/src/main/server/controllers/doors.ts b/src/main/server/controllers/doors.ts new file mode 100644 index 000000000..4eaaf4585 --- /dev/null +++ b/src/main/server/controllers/doors.ts @@ -0,0 +1,196 @@ +import * as alt from 'alt-server'; +import { useGlobal } from '@Server/document/global.js'; +import { objectData } from '@Shared/utility/clone.js'; +import { Door, DoorsConfig, DoorState } from '@Shared/types/index.js'; +import { useAccount, useCharacter } from '@Server/document/index.js'; +import { distance2d } from '@Shared/utility/vector.js'; +import { useEvents } from '@Server/events/index.js'; + +const config = await useGlobal('doors'); +const MAX_DOORS_TO_DRAW = 10; +const streamingDistance = 15; +const doorEntityType = 'door'; +const doorGroup = new alt.VirtualEntityGroup(MAX_DOORS_TO_DRAW); +const doors: (Door & { entity: alt.VirtualEntity })[] = []; +const events = useEvents(); + +/** + * Door configuration that allows you to get and set the lock state of a door. + * For internal use only. + */ +function useDoorConfig() { + /** + * Gets the lock state of a door. + * + * @param {string} uid The uid of the door. + * @returns {DoorState | undefined} The state of the door in the DB. + */ + function getLockState(uid: string): DoorState | undefined { + return config.getField(uid); + } + + /** + * Sets the lock state of a door. + * + * @param {string} uid The uid of the door. + * @param {DoorState} state The state of the door. + * @returns {void} + */ + function setLockState(uid: string, state: DoorState): void { + config.set(uid, state); + const doorIdx = doors.findIndex((door) => door.uid === uid); + if (doorIdx === -1) { + return; + } + + doors[doorIdx].state = state; + doors[doorIdx].entity.setStreamSyncedMeta('door', objectData(doors[doorIdx])); + } + + return { getLockState, setLockState }; +} + +/** + * Door controller that allows you to register doors and toggle their lock state. + */ +export function useDoor() { + const doorConfig = useDoorConfig(); + + /** + * Registers a door to the door controller. This will create a virtual entity for the door. + * The door will be streamed to all players within the streaming distance. + * + * @param {Door} door + * @returns {void} + */ + function register(door: Door): void { + if (doors.find((d) => d.uid === door.uid)) { + throw new Error(`Door with uid ${door.uid} already exists. Please make sure all doors have unique uids.`); + } + + const state = doorConfig.getLockState(door.uid); + if (typeof state === 'undefined') { + doorConfig.setLockState(door.uid, door.state); + } + + door.state = state ?? door.state ?? DoorState.UNLOCKED; + + const entity = new alt.VirtualEntity(doorGroup, new alt.Vector3(door.pos), streamingDistance, { + door, + type: doorEntityType, + }); + doors.push({ ...door, entity }); + } + + /** + * Checks if the player has the required permissions to lock/unlock the door. + * For internal use only. + * + * @param {alt.Player} player + * @param {Door} door + * @returns {boolean} + */ + function checkPermissions(player: alt.Player, door: Door): boolean { + if ( + !door?.permissions?.character && + !door?.permissions?.account && + !door?.groups?.account && + !door?.groups?.character + ) { + return true; + } + const rCharacter = useCharacter(player); + const rAccount = useAccount(player); + if (!rAccount.isValid() || !rCharacter.isValid()) return false; + + let allowed = false; + if (door?.permissions?.character) { + allowed = rCharacter.permissions.hasAnyPermission(door?.permissions?.character ?? []); + } + + if (door?.permissions?.account) { + allowed = rAccount.permissions.hasAnyPermission(door?.permissions?.account ?? []); + } + + if (door?.groups?.character) { + allowed = rCharacter.groupPermissions.hasAtLeastOneGroupWithSpecificPerm(door?.groups?.character ?? {}); + } + + if (door?.groups?.account) { + allowed = rAccount.groupPermissions.hasAtLeastOneGroupWithSpecificPerm(door?.groups?.account ?? {}); + } + + return allowed; + } + + function getNextState(state: DoorState): DoorState { + return state === DoorState.LOCKED ? DoorState.UNLOCKED : DoorState.LOCKED; + } + + /** + * Toggles the lock state of a door. If the player has the required permissions, the lock state will be toggled. + * + * @param {alt.Player} player The player that is toggling the lock state. + * @param {string} uid The uid of the door. + * @returns {boolean} Whether the lock state was toggled or not. + */ + function toggleLockState(player: alt.Player, uid: string): boolean { + const door = doors.find((door) => door.uid === uid); + if (!door) return false; + if (!checkPermissions(player, door)) return false; + + door.state = getNextState(door.state); + doorConfig.setLockState(uid, door.state); + events.invoke(`door-${door.state}`, uid, player); + return true; + } + + /** + * Forces the lock state of a door. This will bypass any permission checks. + * + * @param {string} uid The uid of the door. + * @param {DoorState} state Whether the door is unlocked or not. + * @returns {boolean} Whether the lock state was set or not. + */ + function forceSetLockState(uid: string, state: DoorState): boolean { + const door = doors.find((door) => door.uid === uid); + if (!door) { + return false; + } + + door.state = state; + doorConfig.setLockState(uid, door.state); + events.invoke(`door-${door.state}`, uid, null); + return true; + } + + /** + * Gets the nearest door to the player. + * + * @param {alt.Player} player Player to get the nearest door to. + * @returns {Promise} The nearest door to the player. + */ + async function getNearestDoor(player: alt.Player): Promise { + let lastDistance = streamingDistance; + let closestTarget: Door | undefined = undefined; + for (const door of doors) { + const dist = distance2d(player.pos, door.pos); + if (dist > lastDistance) continue; + lastDistance = dist; + closestTarget = door; + } + return closestTarget; + } + + /** + * Gets a door by its uid. + * + * @param {string} uid Door uid to get. + * @returns {Door | undefined} The door with the specified uid. + */ + function getDoor(uid: string): Door | undefined { + return doors.find((door) => door.uid === uid); + } + + return { register, toggleLockState, forceSetLockState, getNearestDoor, getDoor }; +} diff --git a/src/main/server/database/index.ts b/src/main/server/database/index.ts index f3a156146..14729a77e 100644 --- a/src/main/server/database/index.ts +++ b/src/main/server/database/index.ts @@ -2,8 +2,9 @@ import * as alt from 'alt-server'; import { MongoClient, Db, InsertOneResult, ObjectId, AggregateOptions } from 'mongodb'; import * as Utility from '@Shared/utility/index.js'; import { CollectionNames } from '../document/shared.js'; +import { useConfig } from '@Server/config/index.js'; -const DatabaseName = 'Rebar'; +const config = useConfig(); let isConnected = false; let isInit = false; @@ -37,7 +38,7 @@ export function useDatabase() { return false; } - database = client.db(DatabaseName); + database = client.db(config.getField('database_name')); isInit = false; isConnected = true; alt.log('Connected to MongoDB Successfully'); diff --git a/src/main/server/events/index.ts b/src/main/server/events/index.ts index 6242dcd97..b4dd19325 100644 --- a/src/main/server/events/index.ts +++ b/src/main/server/events/index.ts @@ -19,6 +19,8 @@ declare global { 'vehicle-bound': (vehicle: alt.Vehicle, document: Vehicle) => void; 'page-closed': (player: alt.Player, page: PageNames) => void; 'page-opened': (player: alt.Player, page: PageNames) => void; + 'door-locked': (uid: string, initiator: alt.Player) => void; + 'door-unlocked': (uid: string, initiator: alt.Player | null) => void; 'on-command': (player: alt.Player, commandName: string) => void; message: (player: alt.Player, message: string) => void; } diff --git a/src/main/server/index.ts b/src/main/server/index.ts index 4f4133bbb..b556dca61 100644 --- a/src/main/server/index.ts +++ b/src/main/server/index.ts @@ -7,6 +7,7 @@ import { useApi } from './api/index.js'; import { useConfig } from './config/index.js'; import { useBlipGlobal, useBlipLocal } from './controllers/blip.js'; +import { useDoor } from './controllers/doors.js'; import { useInteraction } from './controllers/interaction.js'; import { useMarkerGlobal, useMarkerLocal } from './controllers/markers.js'; import { useObjectGlobal, useObjectLocal } from './controllers/object.js'; @@ -97,6 +98,7 @@ export function useRebar() { useBlipLocal, useD2DTextLabel, useD2DTextLabelLocal, + useDoor, useInstructionalButtons, useInteraction, useInteractionLocal, diff --git a/src/main/server/systems/serverKeypress.ts b/src/main/server/systems/serverKeypress.ts index 13fd39f7b..9b755098d 100644 --- a/src/main/server/systems/serverKeypress.ts +++ b/src/main/server/systems/serverKeypress.ts @@ -1,16 +1,16 @@ import * as alt from 'alt-server'; import { useRebar } from '../index.js'; -import { Events } from '../../shared/events/index.js'; +import { Events } from '@Shared/events/index.js'; type OnKeybind = (player: alt.Player) => void; type KeybindInfo = { callback: OnKeybind; uid: string }; const Rebar = useRebar(); const RebarEvents = Rebar.events.useEvents(); -const callbacks: { [key: string]: { down: KeybindInfo[]; up: KeybindInfo[] } } = {}; +const keyCallbacks: { [key: string]: { down: KeybindInfo[]; up: KeybindInfo[]; hold: KeybindInfo[] } } = {}; function handleKeyUp(player: alt.Player, key: number) { - if (!callbacks[key]) { + if (!keyCallbacks[key]) { return; } @@ -20,13 +20,13 @@ function handleKeyUp(player: alt.Player, key: number) { } player.setMeta(`keybind-up-${key}`, Date.now() + 500); - for (let kb of callbacks[key].up) { + for (let kb of keyCallbacks[key].up) { kb.callback(player); } } function handleKeyDown(player: alt.Player, key: number) { - if (!callbacks[key]) { + if (!keyCallbacks[key]) { return; } @@ -36,13 +36,29 @@ function handleKeyDown(player: alt.Player, key: number) { } player.setMeta(`keybind-down-${key}`, Date.now() + 500); - for (let kb of callbacks[key].down) { + for (let kb of keyCallbacks[key].down) { + kb.callback(player); + } +} + +function handleKeyHold(player: alt.Player, key: number) { + if (!keyCallbacks[key]) { + return; + } + + const nextCall = (player.getMeta(`keybind-hold-${key}`) as number) ?? 0; + if (Date.now() < nextCall) { + return; + } + + player.setMeta(`keybind-hold-${key}`, Date.now() + 500); + for (let kb of keyCallbacks[key].hold) { kb.callback(player); } } function updateKeypressForPlayer(player: alt.Player) { - player.emit(Events.systems.keypress.update, Object.keys(callbacks)); + player.emit(Events.systems.keypress.update, Object.keys(keyCallbacks)); } function updateKeypresses() { @@ -55,36 +71,101 @@ export function useKeypress() { function on(key: number, callbackUp: OnKeybind, callbackDown: OnKeybind) { const uid = Rebar.utility.uid.generate(); - if (!callbacks[key]) { - callbacks[key] = { + if (!keyCallbacks[key]) { + keyCallbacks[key] = { up: [], down: [], + hold: [], }; } - callbacks[key].up.push({ uid, callback: callbackUp }); - callbacks[key].down.push({ uid, callback: callbackDown }); + keyCallbacks[key].up.push({ uid, callback: callbackUp }); + keyCallbacks[key].down.push({ uid, callback: callbackDown }); updateKeypresses(); return uid; } + function onHold(key: number, callbacks: { hold: OnKeybind; up?: OnKeybind; down?: OnKeybind }) { + const uid = Rebar.utility.uid.generate(); + + if (!keyCallbacks[key]) { + keyCallbacks[key] = { + up: [], + down: [], + hold: [], + }; + } + + keyCallbacks[key].down.push({ + uid, + callback: (player) => { + player.setMeta(`keyhold-${uid}`, Date.now() + 2000); + if (!callbacks.down) { + return; + } + + callbacks.down(player); + }, + }); + + keyCallbacks[key].up.push({ + uid, + callback: (player) => { + player.deleteMeta(`keyhold-${uid}`); + if (!callbacks.up) { + return; + } + + callbacks.up(player); + }, + }); + + keyCallbacks[key].hold.push({ + uid, + callback: (player) => { + const invokeTime = player.getMeta(`keyhold-${uid}`) as number; + if (!invokeTime) { + return; + } + + if (Date.now() < invokeTime) { + return; + } + + player.deleteMeta(`keyhold-${uid}`); + callbacks.hold(player); + }, + }); + + return uid; + } + function off(key: number, uid: string) { - if (!callbacks[key]) { + if (!keyCallbacks[key]) { return; } - const upIndex = callbacks[key].up.findIndex((x) => x.uid === uid); + const upIndex = keyCallbacks[key].up.findIndex((x) => x.uid === uid); if (upIndex >= 0) { - callbacks[key].up.splice(upIndex, 1); + keyCallbacks[key].up.splice(upIndex, 1); } - const downIndex = callbacks[key].down.findIndex((x) => x.uid === uid); + const downIndex = keyCallbacks[key].down.findIndex((x) => x.uid === uid); if (downIndex >= 0) { - callbacks[key].up.splice(downIndex, 1); + keyCallbacks[key].up.splice(downIndex, 1); + } + + const holdIndex = keyCallbacks[key].hold.findIndex((x) => x.uid === uid); + if (holdIndex >= 0) { + keyCallbacks[key].up.splice(holdIndex, 1); } - if (callbacks[key].up.length <= 0 && callbacks[key].down.length <= 0) { - delete callbacks[key]; + if ( + keyCallbacks[key].up.length <= 0 && + keyCallbacks[key].down.length <= 0 && + keyCallbacks[key].hold.length <= 0 + ) { + delete keyCallbacks[key]; } updateKeypresses(); @@ -93,6 +174,7 @@ export function useKeypress() { return { off, on, + onHold, updateKeypressForPlayer, }; } @@ -101,3 +183,4 @@ RebarEvents.on('character-bound', updateKeypressForPlayer); alt.onClient(Events.systems.keypress.invokeUp, handleKeyUp); alt.onClient(Events.systems.keypress.invokeDown, handleKeyDown); +alt.onClient(Events.systems.keypress.invokeHold, handleKeyHold); diff --git a/src/main/shared/events/index.ts b/src/main/shared/events/index.ts index 464a53ea8..69ef82362 100644 --- a/src/main/shared/events/index.ts +++ b/src/main/shared/events/index.ts @@ -122,6 +122,7 @@ export const Events = { update: 'systems:keypress:update', invokeUp: 'systems:keypress:invoke:keyup', invokeDown: 'systems:keypress:invoke:keydown', + invokeHold: 'systems:keypress:invoke:keyhold', }, messenger: { process: 'systems:messenger:process', diff --git a/src/main/shared/types/doors.ts b/src/main/shared/types/doors.ts new file mode 100644 index 000000000..85ec67170 --- /dev/null +++ b/src/main/shared/types/doors.ts @@ -0,0 +1,72 @@ +import * as alt from 'alt-shared'; + + +export enum DoorState { + LOCKED = 'locked', + UNLOCKED = 'unlocked', +} + + +export interface Door { + /** + * Unique identifier for the door. + * + * @type {string} + */ + uid: string; + + /** + * Door locked/unlocked state. + * + * @type {DoorState} + */ + state: DoorState; + + /** + * Position of the door. + * + * @type {alt.Vector3} + */ + pos: alt.Vector3; + + /** + * The hash of the door. + * + * @type {number} + */ + model: number; + + /** + * The permissions that are allowed to lock/unlock the door. + * + * @type {Record<'account' | 'character', string[]>} + */ + permissions: { + account: string[]; + character: string[]; + } + + /** + * The groups that are allowed to lock/unlock the door. + * + * @type {Record} + */ + groups: { + account: { + [key: string]: string[]; + }; + character: { + [key: string]: string[]; + }; + } +} + +export interface DoorsConfig { + + /** + * Door isUnlocked state. + * + * @type {boolean} + */ + [door: string]: DoorState; +} diff --git a/src/main/shared/types/index.ts b/src/main/shared/types/index.ts index f52a46424..2449a3d4b 100644 --- a/src/main/shared/types/index.ts +++ b/src/main/shared/types/index.ts @@ -4,6 +4,7 @@ export * from './blip.js'; export * from './character.js'; export * from './clothingComponent.js'; export * from './credits.js'; +export * from './doors.js'; export * from './instructionalButtons.js'; export * from './marker.js'; export * from './nativeMenu.js'; diff --git a/src/main/shared/types/serverConfig.ts b/src/main/shared/types/serverConfig.ts index 4a20a5fe8..0e1316e2f 100644 --- a/src/main/shared/types/serverConfig.ts +++ b/src/main/shared/types/serverConfig.ts @@ -141,4 +141,12 @@ export interface ServerConfig { * @memberof ServerConfig */ disableAmbientNoise?: boolean; + + /** + * Disable weapon radial menu + * + * @type {boolean} + * @memberof ServerConfig + */ + disableWeaponRadial?: boolean; }