diff --git a/.gitignore b/.gitignore index ce51d67e0..8933257d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .server-crashes-cache/ cache/ -./data/ +data/**/*.bin modules/ node_modules/ resources/main/ diff --git a/data/clothes.bin b/data/clothes.bin deleted file mode 100644 index 343e4fb77..000000000 Binary files a/data/clothes.bin and /dev/null differ diff --git a/data/pedmodels.bin b/data/pedmodels.bin deleted file mode 100644 index ad5df29e2..000000000 Binary files a/data/pedmodels.bin and /dev/null differ diff --git a/data/rpfdata.bin b/data/rpfdata.bin deleted file mode 100644 index 7e5ccc842..000000000 Binary files a/data/rpfdata.bin and /dev/null differ diff --git a/data/vehmodels.bin b/data/vehmodels.bin deleted file mode 100644 index 17a52f202..000000000 Binary files a/data/vehmodels.bin and /dev/null differ diff --git a/data/vehmods.bin b/data/vehmods.bin deleted file mode 100644 index 23aae61ea..000000000 Binary files a/data/vehmods.bin and /dev/null differ diff --git a/data/weaponmodels.bin b/data/weaponmodels.bin deleted file mode 100644 index 4a5d5c3c5..000000000 Binary files a/data/weaponmodels.bin and /dev/null differ diff --git a/docs/changelog.md b/docs/changelog.md index 63e2b8a96..da7a9d2d9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,138 @@ order: 95 # Changelog +## Version 53 + +### Breaking Changes + +- Removed `useVehicleEvents`, `useCharacterEvents`, and `useAccountEvents` + - Replaced with `alt.on('rebar:vehicleUpdated')`, `alt.on('rebar:playerCharacterUpdated')`, and `alt.on('rebar:playerAccountUpdated')`. +- Removed alt.getMeta for Rebar Imports + - This likely doesn't effect anyone +- Removed `useServerTime` + - Replaced with `useWorldService` +- Removed `useServerWeather` + - Replaced with `useWorldService` +- Removed `useMessenger.message.on`, + - Same functionality can be achieved through `alt.on` with `playerSendMessage` +- Removed `useRebarEvents` and moved all events to `alt.on` and `alt.emit` + - Custom delcarations are still possible. + - Replaced all `rebar-event-names` with `camelCased` events + - Event Name Changes: + - Renamed `message` event to `rebar:playerSendMessage` + - Renamed `on-command` event to `rebar:playerCommand` + - Renamed `on-rpc-restart` event to `rebar:rpcRestart` + - Renamed `account-bound` event to `rebar:playerAccountBound` + - Renamed `character-bound` event to `rebar:playerCharacterBound` + - Renamed `vehicle-bound` event to `rebar:vehicleBound` + - Renamed `page-opened` event to `rebar:playerPageOpened` + - Renamed `page-closed` event to `rebar:playerPageClosed` + - Renamed `time-changed` event to `rebar:timeChanged` + - Renamed `time-hour-changed` event to `rebar:timeHourChanged` + - Renamed `time-minute-changed` event to `rebar:timeMinuteChanged` + - Renamed `time-second-changed` event to `rebar:timeSecondChanged` + - Renamed `weather-changed` event to `rebar:weatherChanged` + - Renamed `weather-forecast-changed` event to `rebar:weatherForecastChanged` + - Renamed `doorLocked` event to `rebar:doorLocked` + - Renamed `doorUnlocked` event to `rebar:doorUnlocked` +- Reworked permission system: + - Removed `permission` and `groupPermissions` properties from `useAccount` and `useCharacter`. + - When you check for players' permissions, it will automatically check both groups and plain permissions. + +### Code Changes + +- Added more verbose error printing for plugin imports, should function like before again +- Added ability to use string union for blip sprite types + - Automatically converted to numerical +- Added ability to use string union for blip colors + - Automatically converted to numerical +- Made blips shortRange parameter optional, and default to true +- Updated `useWeapon` for `player` to properly save weapon data and ammo count +- **Removed** `ammo` from the database, and ammo is now stored on the individual weapon instead +- Fix vehicle handler so setting `{}` will actually clear the vehicle handling +- Fix issue where removing attachements was not working correctly +- Replaced in-house get closest entity function with `alt.getClosestEntities` +- Changed `Account` from `type` to `interface` +- Moved `Page Events` to `pageSystem` to keep functionality working +- Made it so emitting notifications from server-side defaults to a GTA:V Notification, until a library is added +- Added `useServiceRegister` + - Provides common APIs for common features to integrate custom functionality. + - Services do nothing until a library registers itself under a service. +- Added `useCurrencyService` + - All these functions do nothing until a library is registered\ + - add (invoke adding currency) + - sub (invoke removing currency) + - has (check if has enough currency) + - emits events when currency added or subtracted +- Added `useDeathService` + - All these functions do nothing until a library is registered + - respawn (invoke a respawn) + - revive (invoke a revive, in place) + - emits events when respawned, or revived +- Added `useItemService` + - All these functions do nothing until a library is registered + - add (invoke an item add) + - sub (invoke an item subtraction) + - remove (invoke an item remove) + - has (invoke if player has an item) + - itemCreate (create an item to add to the database) + - itemRemove (remove an item from the database) + - emits events when items added, subtracted, or removed +- Added `useNotificationService` + - All these functions do nothing until a library is registered + - emit (invoke a notification send) + - broadcast (invoke a notification send, to all players) + - emits events when notification emitted, or broadcasted +- Added `useTimeService` + - All these functions do nothing until a library is registered + - setTime (set the time for the whole server) + - getTime (get the current time for the server) + - emits events when time updated by hour, minute, or second +- Added `useWeatherService` + - setWeather (set the weather for the server) + - setWeatherForecast (set weather forecast for the server) + - getWeather (get the current weather for the server) + - getWeatherForecast (get weather forecast for the server) + - emits events when weather updated, or forecast updated +- Added Custom alt.getMeta Keys for... + - serverTime + - serverWeather + - serverWeatherForecast +- Added support for interactions to + - addBlip + - addMarker + - addTextLabel + - getBlip + - getMarker + - getTextLabel + - getPos + - Destroy all of the above when interaction is destroyed +- Updated Document Typings for ... to better handle module extension + - Account + - Character + - Vehicle +- Added `rebar:onTick` which just emits a tick every 1 second for general usage +- Added `isOverlayOpen` and `isPersistentPageOpen` to client-side for checking if a page is open +- Made dev menu from the `webview:dev` command scrollable +- Groups are now created globally, and you can assign players' documents to the group. +- You can inherit a new group from another one; it will inherit all permissions from the parent. +- You can now access permissions/groups of character/account via `useVirtual`. +- Two new player-getters: + - `withPermission(documentType: 'account' | 'character' | 'any', permission: string)` + - `memberOfGroup(documentType: 'account' | 'character' | 'any', groupName: string)` +- Updated weapon helpers to allow for `hash` or `string` models +- Added `invokeWithResult` for player natives, to invoke a native and get a result +- Added vehicle door sync for open / shut states + +### Docs Changes + +- Document `getWeapons` and update documentation for `useWeapon` for the player +- Document all services under `useService` +- Document all event changes, and update events page +- Document the new permission system. + +--- + ## Version 52 ### Code Changes diff --git a/docs/index.md b/docs/index.md index 3b07e8297..54be32c65 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,13 +4,9 @@ order: 99 # What is Rebar? -Rebar is a plug-and-play base framework for the alternative GTA:V multiplayer client alt:V. Rebar is meant to act as a light framework that provides utility, and a basic standard for building plugins that can be compatible with other plugins. +Rebar is a plugin-based framework for GTA:V based on alt:V that focuses on server-side functionality to control almost all player aspects. Rebar provides a robust starting point for any game mode as you'll be able to choose from a wide variety of community plugins to install. Rebar thrives with its welcoming Discord community, community contributions, and plugins. -It has a heavy focus on simplifying building game modes and lowering the friction of building a server. - -## Why Rebar? - -Rebar is the foundational piece necessary to construct large concrete structures. Think of this framework as achieving the same from a game-mode standpoint. +Rebar stops server developers from spending years building boilerplate code, and instead gets them straight to writing their game mode. ## Tech Stack @@ -19,6 +15,122 @@ Rebar is the foundational piece necessary to construct large concrete structures - MongoDB - Vue -## How do I get started? +## Why the name Rebar? + +Rebar is the foundational piece necessary to construct large concrete structures. Think of this framework as achieving the same from a game mode standpoint. + +## Rebar vs Fivem (ESX, QBCore) + +While ESX, and QBCore have been around forever, Rebar handles all the heavy lifting for you. + ++++ Keybind Example +||| FiveM + +```lua +Citizen.CreateThread(function() + while true do + Citizen.Wait(0) + + if IsControlJustPressed(1, 51) then + print("E key was pressed!") + end + end +end) +``` + +||| Rebar + +```ts +Keybinder.on(75, (player) => { + console.log('Pressed k'); +}); +``` + +||| ++++ Ped Example +||| FiveM + +```lua +local pedModel = "a_m_y_hipster_01" +local spawnCoords = vector3(427.13, -806.46, 29.49) +local targetCoords = vector3(435.0, -800.0, 29.49) + +function loadModel(model) + RequestModel(model) + while not HasModelLoaded(model) do + Citizen.Wait(0) + end +end + +Citizen.CreateThread(function() + loadModel(GetHashKey(pedModel)) + local ped = CreatePed(4, GetHashKey(pedModel), spawnCoords.x, spawnCoords.y, spawnCoords.z, 0.0, true, false) + TaskGoStraightToCoord(ped, targetCoords.x, targetCoords.y, targetCoords.z, 1.0, -1, 0.0, 0.0) + SetEntityInvincible(ped, true) +end) +``` + +||| Rebar + +```ts +const pedModel = 'a_m_y_hipster_01'; // The pedestrian model +const spawnCoords = { x: 427.13, y: -806.46, z: 29.49 }; // Spawn coordinates +const targetCoords = { x: 435.0, y: -800.0, z: 29.49 }; // Target coordinates + +const ped = Rebar.controllers.usePed(new alt.Ped(pedModel, spawnCoords, alt.Vector3.zero)); + +ped.setOption('makeStupid', true); +ped.setOption('invincible', true); + +ped.invoke('taskGoStraightToCoord', targetCoords.x, targetCoords.y, targetCoords.z, 1.0, -1, 0, 0); +``` + +||| + ++++ Text Example +||| FiveM + +```lua +Citizen.CreateThread(function() + while true do + Citizen.Wait(0) + local x, y, z = 427.13, -806.46, 29.49 + local text = "Hello, World!" + local scale = 0.5 + local color = {255, 255, 255, 255} + + SetTextFont(0) + SetTextProportional(1) + SetTextScale(scale, scale) + SetTextColour(color[1], color[2], color[3], color[4]) + SetTextEntry("STRING") + AddTextComponentString(text) + DrawText(x, y) + end +end) + +``` + +||| Rebar + +```ts +const label = Rebar.controllers.useD2DTextLabel({ + text: 'Hello World', + pos: new alt.Vector3(427.13, -806.46, 29.49), + fontColor: new alt.RGBA(255, 255, 255, 255), + fontSize: 0.5, +}); +``` + +||| ++++ + +## Getting Started + +### Experienced Developers Head on over to [Install & Upgrade](./install.md) to learn about the installation process. + +### New Developers + +Head on over to the [book on Rebar](./tutorials/rebar/chapter-01-preface.md) and how to utilize it. There's even a few small tutorials on programming. diff --git a/docs/info/webview/create.md b/docs/info/webview/create.md index b4a8de5ac..313f9f85b 100644 --- a/docs/info/webview/create.md +++ b/docs/info/webview/create.md @@ -54,7 +54,7 @@ import { useWebview } from '@Server/player/webview.js'; // Show the page function someShowFunction(somePlayer: alt.Player) { - useWebview(somePlayer).show('MyExampleView'); + useWebview(somePlayer).show('MyExampleView', 'page'); } // Hide the page diff --git a/docs/install.md b/docs/install.md index 5f79f374b..a4d2a5c3b 100644 --- a/docs/install.md +++ b/docs/install.md @@ -108,6 +108,13 @@ pnpm install pnpm binaries ``` +### Grant execute permissions + +```bash +sudo chmod +x altv-server +sudo chmod +x altv-crash-handler +``` + ### Start ``` diff --git a/docs/tutorials/general/code-examples.md b/docs/tutorials/general/code-examples.md index bc5d36a7c..c90da257f 100644 --- a/docs/tutorials/general/code-examples.md +++ b/docs/tutorials/general/code-examples.md @@ -28,35 +28,6 @@ import * as alt from 'alt-server'; const Rebar = alt.getMeta('Rebar'); ``` -## Rebar Events - -Rebar has a handful of events that can be used in tandem with alt:V events. - -These become more useful when a character select, or auth plugins are installed. - -```ts -import { useRebar } from '@Server/index.js'; - -const Rebar = useRebar(); -const RebarEvents = Rebar.events.useEvents(); - -RebarEvents.on('character-bound', (player) => { - // do something when they've selected a character -}); -``` - -```ts -RebarEvents.on('account-bound', (player) => { - // do something when they've logged into an account -}); -``` - -```ts -RebarEvents.on('time-changed', (hour, minute, second) => { - // Do something when the time changes in-game -}); -``` - ## Notifying a Player Send a default GTA:V notification to the player. @@ -65,9 +36,8 @@ Send a default GTA:V notification to the player. import { useRebar } from '@Server/index.js'; const Rebar = useRebar(); -const RebarEvents = Rebar.events.useEvents(); -RebarEvents.on('character-bound', (player) => { +alt.on('playerConnect', (player) => { const rPlayer = Rebar.usePlayer(player); rPlayer.notify.showNotification('Welcome to the server!'); }); @@ -82,9 +52,8 @@ import * as alt from 'alt-server'; import { useRebar } from '@Server/index.js'; const Rebar = useRebar(); -const RebarEvents = Rebar.events.useEvents(); -RebarEvents.on('character-bound', (player) => { +alt.on('playerConnect', (player) => { const rPlayer = Rebar.usePlayer(player); rPlayer.notify.showShard({ title: 'Welcome to the Server', @@ -102,9 +71,8 @@ import * as alt from 'alt-server'; import { useRebar } from '@Server/index.js'; const Rebar = useRebar(); -const RebarEvents = Rebar.events.useEvents(); -RebarEvents.on('character-bound', (player) => { +alt.on('playerConnect', (player) => { const rPlayer = Rebar.usePlayer(player); rPlayer.notify.showMissionText('Visit our website at https://rebarv.com'); }); diff --git a/docs/tutorials/rebar/chapter-10-authentication.md b/docs/tutorials/rebar/chapter-10-authentication.md index 2deaa57e3..bcec2bea3 100644 --- a/docs/tutorials/rebar/chapter-10-authentication.md +++ b/docs/tutorials/rebar/chapter-10-authentication.md @@ -153,6 +153,16 @@ We're also going to blur the screen so it's a little more pleasing to look at. rPlayer.world.setScreenBlur(200); ``` +### Disable the player controls + +Since we don't want the player to accidentally opening the pausemenu if their Username or password contains the letter 'P' we're going to disable the controls. Just remember to enable them again, when releasing the player into the world. + +```ts +// server/index.ts + +rPlayer.world.disableControls(); +``` + ### Initial Result You should see a `hello world` in the top-left of your screen. @@ -363,7 +373,7 @@ We can build validation by using the `watch` function from `vue`. What this will watch(username, (value) => { usernameValid.value = false; - // If the length of the username is less than 2, return + // If the length of the username is less than 3, return if (value.length <= 2) { return; } @@ -374,7 +384,7 @@ watch(username, (value) => { watch(password, (value) => { passwordValid.value = false; - // If the length of the password is less than 2, return + // If the length of the password is less than 3, return if (value.length <= 2) { return; } @@ -407,7 +417,7 @@ Now we're going to sanitize the username input further, and only allow `A-Z` and watch(username, (value) => { usernameValid.value = false; - // If the length of the username is less than 2, return + // If the length of the username is less than 3, return if (value.length <= 2) { return; } @@ -749,6 +759,7 @@ Let's take a moment to remember that we: - Changed their Dimension - Made them invisible - Blurred the screen +- Disabled the controls ### Finish Loading @@ -771,6 +782,7 @@ function finish(player: alt.Player) { const rPlayer = Rebar.usePlayer(player); rPlayer.world.freezeCamera(false); rPlayer.world.clearScreenBlur(200); + rPlayer.world.enableControls(); rPlayer.webview.hide('Authentication'); } ``` diff --git a/docs/useRebar/controllers.md b/docs/useRebar/controllers.md index 68618ff8c..bbddc0d47 100644 --- a/docs/useRebar/controllers.md +++ b/docs/useRebar/controllers.md @@ -692,6 +692,9 @@ async function showSomeMenu(player: alt.Player) { Doors are objects that can be opened and closed. When they are locked, no one can bypass them. +Permissions can be set on doors to allow certain players to lock/unlock them. +More details about how to use permissions can be found [here](/userebar/systems/permissions/useEntityPermissions.md). + ### useDoor ```ts diff --git a/docs/useRebar/document/document-account.md b/docs/useRebar/document/document-account.md index 102616cee..203e8609c 100644 --- a/docs/useRebar/document/document-account.md +++ b/docs/useRebar/document/document-account.md @@ -154,151 +154,13 @@ function someFunction(player: alt.Player) { } ``` -### permissions +### [permissions](/userebar/systems/permissions/playerPermissions.md#useaccount) -You can grant/revoke permissions to the account and check if account has them. +Click on the link above to see how to use account permissions. -#### addPermission +### [groups](/userebar/systems/permissions/playerGroups.md#useaccount) -Grants a permission to the account. - -```ts -async function someFunction(player: alt.Player) { - const document = Rebar.document.account.useAccount(player); - await document.permissions.addPermission('admin'); -} -``` - -#### removePermission - -Revokes permission from the account. - -```ts -async function someFunction(player: alt.Player) { - const document = Rebar.document.account.useAccount(player); - await document.permissions.removePermission('admin'); -} -``` - -#### hasPermission - -Checks if account has a permission. - -```ts -function someFunction(player: alt.Player) { - const document = Rebar.document.account.useAccount(player); - const hasPerm: boolean = document.permissions.hasPermission('admin'); -} -``` - -#### hasAllPermissions - -Checks if account has all of the provided permissions: - -```ts -function someFunction(player: alt.Player) { - const document = Rebar.document.account.useAccount(player); - const hasAllPerm: boolean = document.permissions.hasAllPermissions(['admin', 'support']); -} -``` - -#### hasAnyPermission - -Checks if account has at least one of the provided permissions: - -```ts -function someFunction(player: alt.Player) { - const document = Rebar.document.account.useAccount(player); - const hasAnyPerm: boolean = document.permissions.hasAnyPermission(['admin', 'support']); -} -``` - -### groupPermissions - -Permission groups allow you to assign permissions under a specific group name for an account. - -#### addPermissions - -Adds permission to specified group. If an account had no group before - it will create it. - -```ts -async function someFunction(player: alt.Player) { - const document = Rebar.document.account.useAccount(player); - await document.groupPermissions.addPermissions('admin', 'noclip'); -} -``` - -#### removePermissions - -Removes permission from group, it will also remove a group, if there are no permissions left. - -```ts -async function someFunction(player: alt.Player) { - const document = Rebar.document.account.useAccount(player); - await document.groupPermissions.removePermissions('admin', 'noclip'); -} -``` - -#### removeGroup - -Completely removes group from an account with all its permissions. - -```ts -async function someFunction(player: alt.Player) { - const document = Rebar.document.account.useAccount(player); - await document.groupPermissions.removeGroup('admin'); -} -``` - -#### hasGroup - -Checks if account belongs to group. - -```ts -function someFunction(player: alt.Player) { - const document = Rebar.document.account.useAccount(player); - const isAdmin: boolean = document.groupPermissions.hasGroup('admin'); -} -``` - -#### hasGroupPerm - -Checks if account belongs to group and has a specific permission. - -```ts -function someFunction(player: alt.Player) { - const document = Rebar.document.account.useAccount(player); - const canNoclip: boolean = document.groupPermissions.hasGroupPerm('admin', 'noclip'); -} -``` - -#### hasAtLeastOneGroupPerm - -Checks if account belongs to group and has any of provided permissions. - -```ts -function someFunction(player: alt.Player) { - const document = Rebar.document.account.useAccount(player); - const canBanOrNoclip: boolean = document.groupPermissions.hasAtLeastOneGroupPerm('admin', ['noclip', 'ban']); -} -``` - -#### hasAtLeastOneGroupWithSpecificPerm - -Checks if account belongs to any of the group and has corresponding rights. - -```ts -function someFunction(player: alt.Player) { - const document = Rebar.document.account.useAccount(player); - const isStaff = document.groupPermissions.hasAtLeastOneGroupWithSpecificPerm({ - admin: ['noclip', 'ban'], - support: ['answerReport'], - }); - // This will be true if: - // 1. Account has `admin` group with `noclip` OR/AND `ban` permission. - // 2. Account has `support` group with `answerReport` permission. -} -``` +Click on the link above to see how to use account groups. ## useAccountEvents diff --git a/docs/useRebar/document/document-character.md b/docs/useRebar/document/document-character.md index 067a19e0d..605e76b42 100644 --- a/docs/useRebar/document/document-character.md +++ b/docs/useRebar/document/document-character.md @@ -107,151 +107,13 @@ function someFunction(player: alt.Player) { } ``` -### permissions +### [permissions](/userebar/systems/permissions/playerPermissions.md#usecharacter) -You can grant/revoke permissions to the character and check if character has them. +Click on the link above to see how to use character permissions. -#### addPermission +### [groups](/userebar/systems/permissions/playerGroups.md#usecharacter) -Grants a permission to the character. - -```ts -async function someFunction(player: alt.Player) { - const document = Rebar.document.character.useCharacter(player); - await document.permissions.addPermission('admin'); -} -``` - -#### removePermission - -Revokes permission from the character. - -```ts -async function someFunction(player: alt.Player) { - const document = Rebar.document.character.useCharacter(player); - await document.permissions.removePermission('admin'); -} -``` - -#### hasPermission - -Checks if character has a permission. - -```ts -function someFunction(player: alt.Player) { - const document = Rebar.document.character.useCharacter(player); - const hasPerm: boolean = document.permissions.hasPermission('admin'); -} -``` - -#### hasAllPermissions - -Checks if character has all of the provided permissions: - -```ts -function someFunction(player: alt.Player) { - const document = Rebar.document.character.useCharacter(player); - const hasAllPerm: boolean = document.permissions.hasAllPermissions(['admin', 'support']); -} -``` - -#### hasAnyPermission - -Checks if character has at least one of the provided permissions: - -```ts -function someFunction(player: alt.Player) { - const document = Rebar.document.character.useCharacter(player); - const hasAnyPerm: boolean = document.permissions.hasAnyPermission(['admin', 'support']); -} -``` - -### groupPermissions - -Permission groups allow you to assign permissions under a specific group name for an character. - -#### addPermissions - -Adds permission to specified group. If an character had no group before - it will create it. - -```ts -async function someFunction(player: alt.Player) { - const document = Rebar.document.character.useCharacter(player); - await document.groupPermissions.addPermissions('admin', 'noclip'); -} -``` - -#### removePermissions - -Removes permission from group, it will also remove a group, if there are no permissions left. - -```ts -async function someFunction(player: alt.Player) { - const document = Rebar.document.character.useCharacter(player); - await document.groupPermissions.removePermissions('admin', 'noclip'); -} -``` - -#### removeGroup - -Completely removes group from an character with all its permissions. - -```ts -async function someFunction(player: alt.Player) { - const document = Rebar.document.character.useCharacter(player); - await document.groupPermissions.removeGroup('admin'); -} -``` - -#### hasGroup - -Checks if character belongs to group. - -```ts -function someFunction(player: alt.Player) { - const document = Rebar.document.character.useCharacter(player); - const isAdmin: boolean = document.groupPermissions.hasGroup('admin'); -} -``` - -#### hasGroupPerm - -Checks if character belongs to group and has a specific permission. - -```ts -function someFunction(player: alt.Player) { - const document = Rebar.document.character.useCharacter(player); - const canNoclip: boolean = document.groupPermissions.hasGroupPerm('admin', 'noclip'); -} -``` - -#### hasAtLeastOneGroupPerm - -Checks if character belongs to group and has any of provided permissions. - -```ts -function someFunction(player: alt.Player) { - const document = Rebar.document.character.useCharacter(player); - const canBanOrNoclip: boolean = document.groupPermissions.hasAtLeastOneGroupPerm('admin', ['noclip', 'ban']); -} -``` - -#### hasAtLeastOneGroupWithSpecificPerm - -Checks if character belongs to any of the group and has corresponding rights. - -```ts -function someFunction(player: alt.Player) { - const document = Rebar.document.character.useCharacter(player); - const isStaff = document.groupPermissions.hasAtLeastOneGroupWithSpecificPerm({ - admin: ['noclip', 'ban'], - support: ['answerReport'], - }); - // This will be true if: - // 1. Character has `admin` group with `noclip` OR/AND `ban` permission. - // 2. Character has `support` group with `answerReport` permission. -} -``` +Click on the link above to see how to use character groups. ## useCharacterEvents diff --git a/docs/useRebar/document/document-virtual.md b/docs/useRebar/document/document-virtual.md index 0ccefee0c..ed1d06b6c 100644 --- a/docs/useRebar/document/document-virtual.md +++ b/docs/useRebar/document/document-virtual.md @@ -4,6 +4,8 @@ See [Documents Section](./index.md) for further information on documents. ## useVirtual +If document is not found in the database, it will return `undefined`. + ```ts import { useRebar } from '@Server/index.js'; @@ -15,7 +17,7 @@ interface CustomDocument { test2: string; } -const virtualDocument = Rebar.document.virtual.useVirtual('663ce39eb270106cf02fb7e3', 'SomeCollection'); +const virtualDocument = await Rebar.document.virtual.useVirtual('663ce39eb270106cf02fb7e3', 'SomeCollection'); ``` ### get @@ -24,11 +26,14 @@ Get all data for a document ```ts async function someFunction() { - const virtualDocument = Rebar.document.virtual.useVirtual( + const virtualDocument = await Rebar.document.virtual.useVirtual( '663ce39eb270106cf02fb7e3', 'SomeCollection', ); - const data = await virtualDocument.get(); + if (!virtualDocument) { + return; + } + const data = virtualDocument.get(); console.log(data); } ``` @@ -39,11 +44,14 @@ Get a specific field from a document ```ts async function someFunction() { - const virtualDocument = Rebar.document.virtual.useVirtual( + const virtualDocument = await Rebar.document.virtual.useVirtual( '663ce39eb270106cf02fb7e3', 'SomeCollection', ); - const test1 = await virtualDocument.getField('test1'); + if (!virtualDocument) { + return; + } + const test1 = virtualDocument.getField('test1'); console.log(test1); } ``` @@ -54,11 +62,13 @@ Set a specific field for the document, and save to the database ```ts async function someFunction() { - const virtualDocument = Rebar.document.virtual.useVirtual( + const virtualDocument = await Rebar.document.virtual.useVirtual( '663ce39eb270106cf02fb7e3', 'SomeCollection', ); - + if (!virtualDocument) { + return; + } await virtualDocument.set('test1', 'hi'); } ``` @@ -69,11 +79,46 @@ Set multiple fields for the document, and save to the database ```ts async function someFunction() { - const virtualDocument = Rebar.document.virtual.useVirtual( + const virtualDocument = await Rebar.document.virtual.useVirtual( '663ce39eb270106cf02fb7e3', 'SomeCollection', ); - + if (!virtualDocument) { + return; + } await virtualDocument.setBulk({ test1: 'hi', test2: 'hi' }); } ``` + +### refresh + +Refresh the document from the database + +```ts +async function someFunction() { + const virtualDocument = await Rebar.document.virtual.useVirtual( + '663ce39eb270106cf02fb7e3', + 'SomeCollection', + ); + if (!virtualDocument) { + return; + } + await virtualDocument.refresh(); +} +``` + +### [permissions](/userebar/systems/permissions/playerPermissions.md#usevirtual) + +If you want to modify permissions for a document, you can use the `permissions` property. + +It is available only for `Accounts` and `Characters` collections. + +Click the link above(heading) to see more details on how to use this property. + +### [groups](/userebar/systems/permissions/playerGroups.md#usevirtual) + +If you want to modify groups for a document, you can use the `groups` property. + +It is available only for `Accounts` and `Characters` collections. + +Click the link above(heading) to see more details on how to use this property. diff --git a/docs/useRebar/events.md b/docs/useRebar/events.md new file mode 100644 index 000000000..4040cf608 --- /dev/null +++ b/docs/useRebar/events.md @@ -0,0 +1,195 @@ +--- +order: 700 +--- + +# events + +These events are unique to the Rebar framework, and help provide information about when something happens. + +A lot of these events can only be invoked by using the [Services](./useServices.md) functionality. + +## Event Formatting + +The syntax for any custom events should be as follows: + +```ts +declare module 'alt-server' { + export interface ICustomEmitEvent { + 'rebar:pluginName:eventName': (something: string) => void; + } +} +``` + +## Usage + +```ts +// Called when an account is bound to a player +alt.on('rebar:playerAccountBound', (player, document) => { + console.log(document); +}); + +// Called when a character is bound to a player +alt.on('rebar:playerCharacterBound', (player, document) => { + console.log(document); +}); + +// Called when a vehicle document is bound to a vehicle +alt.on('rebar:vehicleBound', (vehicle, document) => { + console.log(document); +}); + +// Called when a player sends a message +alt.on('rebar:playerSendMessage', (player, msg) => { + console.log(msg); +}); + +// Called whenever the time changes +alt.on('rebar:timeChanged', (hour, minute, second) => { + console.log(hour, minute, second); +}); + +// Called whenever the hour increments by 1 +alt.on('rebar:timeHourChanged', (hour) => { + console.log(hour); +}); + +// Called whenever the minute increments by 1 +alt.on('rebar:timeMinuteChanged', (minute) => { + console.log(minute); +}); + +// Called whenever the second increments by 1 +alt.on('rebar:timeSecondChanged', (second) => { + console.log(second); +}); + +// Called when a page is opened +alt.on('rebar:playerPageOpened', (player, pageName) => { + console.log('page opened'); + console.log(pageName); +}); + +// Called when a page is closed +alt.on('rebar:playerPageClosed', (player, pageName) => { + console.log('page closed'); + console.log(pageName); +}); + +// Called when currency is added to a player +alt.on('rebar:playerCurrencyAdd', (player, type, quantity) => { + console.log(`Added ${type} of ${quantity}`); +}); + +// Called when currency is added to a player +alt.on('rebar:playerCurrencySub', (player, type, quantity) => { + console.log(`Subtracted ${type} of ${quantity}`); +}); + +// Called when server weather is changed +alt.on('rebar:weatherChanged', (weather) => { + console.log(`Weather is now ${weather}`); +}); + +// Called when a door is locked +alt.on('rebar:doorLocked', (uid, initiator: alt.Player) => { + console.log(`Door ${uid} was locked...`); +}); + +// Called when a door is locked +alt.on('rebar:doorUnlocked', (uid, initiator: alt.Player | null) => { + console.log(`Door ${uid} was unlocked...`); +}); + +// Called when a notification is emitted to a player +alt.on('rebar:playerEmitNotification', (player, msg, type) => { + console.log(msg); +}); + +// Called when a notification is broadcast to all players +alt.on('rebar:broadcastNotification', (msg, type) => { + console.log(msg); +}); + +// Called when the server invokes a respawn for a given player +alt.on('rebar:playerRespawn', (player, pos) => { + console.log(`Respawning player...`); +}); + +// Called when the server invokes a revive for a given player in the same position +alt.on('rebar:playerRevive', (player) => { + console.log(`Reviving player in position...`); +}); + +// Called when the server invokes the hot reload functionality +// 99% of devs will not be using this +alt.on('rebar:rpcRestart', () => { + console.log(`Invoked when hot reload is invoked...`); +}); + +// Called when any field in the character document is updated for a player +alt.on('rebar:playerCharacterUpdated', (player, fieldName, value) => { + if (fieldName !== 'armour') { + return; + } +}); +// Called when any field in the account document is updated for a player +alt.on('rebar:playerAccountUpdated', (player, fieldName, value) => { + if (fieldName !== 'email') { + return; + } +}); + +// Called when any field in the vehicle document is updated for a vehicle +alt.on('rebar:vehicleUpdated', (vehicle, fieldName, value) => { + if (fieldName !== 'fuel') { + return; + } +}); + +// Called when an item is added to an entity's document, such as a player. +alt.on('rebar:entityItemAdd', (entity, id, quantity, data) => { + if (!(entity instanceof alt.Player)) { + return; + } + + // Listening for player only item add events +}); + +// Called when an item quantity is subtracted from an entity's document, such as a player. +alt.on('rebar:entityItemSub', (entity, id, quantity) => { + if (!(entity instanceof alt.Player)) { + return; + } + + // Listening for player only item sub events +}); + +// Called when an item is removed from entity's document, such as a player +alt.on('rebar:entityItemRemove', (entity, uid) => { + if (!(entity instanceof alt.Player)) { + return; + } + + // Listening for player only item remove events +}); + +// Called every 1s based on server time, there at 60 ticks in a minute. +alt.on('rebar:onTick', (tick: number) => { + console.log(tick); +}); +``` + +## Custom Events + +You can declare custom `alt.on` events in your plugin like this. + +You can invoke them with `alt.emit`. + +```ts +declare module 'alt-server' { + export interface ICustomEmitEvent { + weatherForecastChanged: (weather: Weathers[]) => void; + weatherChanged: (weather: Weathers) => void; + } +} +``` diff --git a/docs/useRebar/systems/permissions/about.md b/docs/useRebar/systems/permissions/about.md new file mode 100644 index 000000000..51992fb01 --- /dev/null +++ b/docs/useRebar/systems/permissions/about.md @@ -0,0 +1,43 @@ +--- +title: About permissions +order: a +--- + +# About Player Permissions + +There is a documentation for permission system that allows you to control what players can and cannot do. + +Rebar exposes the same interface for both account and character permissions. +This means that you can use almost the same code to check permissions regardless of the source. + +Also, Rebar allows you to modify permissions when player is offline, with use of [`useVirtualDocument`](/userebar/document/document-virtual/). + +## Permission resolution order + +When checking permissions, Rebar will check permissions in the following order: + +1. Account +2. Account's groups +3. Character +4. Character's groups + +This means that if you want to grant a permission to all player's characters, you should set it on the account level. + +## Useful links + +### Player permissions +[!ref Account permissions](/userebar/systems/permissions/playerPermissions.md#useaccount) +[!ref Character permissions](/userebar/systems/permissions/playerPermissions.md#usecharacter) +[!ref Virtual document permissions](/userebar/systems/permissions/playerPermissions.md#usevirtual) + +### Player groups +[!ref Account groups](/userebar/systems/permissions/playerGroups.md#useaccount) +[!ref Character groups](/userebar/systems/permissions/playerGroups.md#usecharacter) +[!ref Virtual document groups](/userebar/systems/permissions/playerGroups.md#usevirtual) + +### Using groups +[!ref usePermissionGroup](/userebar/systems/permissions/usePermissionGroup.md) + +### Checking permissions +[!ref usePermissions](/userebar/systems/permissions/usePermissions.md) +[!ref useEntityPermissions](/userebar/systems/permissions/useEntityPermissions.md) diff --git a/docs/useRebar/systems/permissions/index.yml b/docs/useRebar/systems/permissions/index.yml new file mode 100644 index 000000000..a7dce3847 --- /dev/null +++ b/docs/useRebar/systems/permissions/index.yml @@ -0,0 +1,3 @@ +expanded: false +order: 550 +label: 'permissions' diff --git a/docs/useRebar/systems/permissions/playerGroups.md b/docs/useRebar/systems/permissions/playerGroups.md new file mode 100644 index 000000000..112df9310 --- /dev/null +++ b/docs/useRebar/systems/permissions/playerGroups.md @@ -0,0 +1,305 @@ +--- +order: c +title: Player groups +--- + +# Managing player groups + +Player groups are a way to organize players into groups, and then assign permissions to those groups. This way, you can easily manage permissions for multiple players at once. + +## Attaching player to a group [!badge Async] + +To attach a player to a group, you can use the `useAccount`, `useCharacter` and `useVirtual` functions. All of them have a `groups` property that you can use to attach a player to a group. + ++++ useCharacter +```typescript #7 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +async function makeAdmin(player: alt.Player) { + const character = Rebar.document.character.useCharacter(player); + await character.groups.add('admin'); +} + +``` +!!!info Event will be triggered +This will trigger a `rebar:permissions:groups:add` event with following arguments: +- `player: alt.Player` — player that group was added to. +- `group: string` — group that was added to character. +- `target: 'character' | 'account'` — target of the operation. +!!! + ++++ useAccount +```typescript #7 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +async function makeAdmin(player: alt.Player) { + const account = Rebar.document.account.useAccount('account-id'); + await account.groups.add('admin'); +} +``` +!!!info Event will be triggered +This will trigger a `rebar:permissions:groups:add` event with following arguments: +- `player: alt.Player` — player that group was added to. +- `group: string` — group that was added to account. +- `target: 'character' | 'account'` — target of the operation. +!!! ++++ useVirtual + +!!!warning For offline players only +This method is intended to be used for offline players only. If you want to grant a permission to a player that is currently online, use `useCharacter` or `useAccount` permissions proxies. +!!! + +```typescript #9 +import {useRebar} from '@Server/index.js'; + +const Rebar = useRebar(); +const {CollectionNames} = Rebar.database; + +async function makeAdmin(_id: string) { + const document = await Rebar.document.virtual.useVirtual(_id, CollectionNames.Accounts); + if (document) { + await virtual.groups.add('admin'); + } +} +``` +!!!warning Event won't be triggered +Unlike the `useCharacter` and `useAccount` permissions proxies, the `useVirtual` permissions proxy won't trigger any events. +!!! ++++ + +## Removing player from a group [!badge Async] + +To remove a player from a group, you can use the `useAccount`, `useCharacter` and `useVirtual` functions. All of them have a `groups` property that you can use to remove a player from a group. + ++++ useCharacter +```typescript #7 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +async function removeAdmin(player: alt.Player) { + const character = Rebar.document.character.useCharacter(player); + await character.groups.remove('admin'); +} +``` + +!!!info Event will be triggered +This will trigger a `rebar:permissions:groups:remove` event with following arguments: +- `player: alt.Player` — player that group was removed from. +- `group: string` — group that was removed from character. +- `target: 'character' | 'account'` — target of the operation. +!!! + ++++ useAccount +```typescript #7 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +async function removeAdmin(player: alt.Player) { + const account = Rebar.document.account.useAccount('account-id'); + await account.groups.remove('admin'); +} +``` + +!!!info Event will be triggered +This will trigger a `rebar:permissions:groups:remove` event with following arguments: +- `player: alt.Player` — player that group was removed from. +- `group: string` — group that was removed from account. +- `target: 'character' | 'account'` — target of the operation. +!!! + ++++ useVirtual + +!!!warning For offline players only +This method is intended to be used for offline players only. If you want to grant a permission to a player that is currently online, use `useCharacter` or `useAccount` permissions proxies. +!!! + +```typescript #9 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); +const { CollectionNames } = Rebar.database; + +async function removeAdmin(_id: string) { + const document = await Rebar.document.useVirtual(_id, CollectionNames.Accounts); + if (document) { + await virtual.groups.remove('admin'); + } +} +``` + +!!!warning Event won't be triggered +Unlike the `useCharacter` and `useAccount` permissions proxies, the `useVirtual` permissions proxy won't trigger any events. +!!! + ++++ + +## Clearing all groups [!badge Async] + +To clear all groups from a player, you can use the `useAccount`, `useCharacter` and `useVirtual` functions. All of them have a `groups` property that you can use to clear all groups from a player. + ++++ useCharacter +```typescript #7 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +async function clearGroups(player: alt.Player) { + const character = Rebar.document.character.useCharacter(player); + await character.groups.clear(); +} +``` + +!!!info Event will be triggered + +This will trigger a `rebar:permissions:groups:clear` event with following arguments: +- `player: alt.Player` — player that groups were cleared from. +- `groups: string[]` — groups that were cleared from character. +- `target: 'character' | 'account'` — target of the operation. +!!! + ++++ useAccount +```typescript #7 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +async function clearGroups(player: alt.Player) { + const account = Rebar.document.account.useAccount('account-id'); + await account.groups.clear(); +} +``` + +!!!info Event will be triggered + +This will trigger a `rebar:permissions:groups:clear` event with following arguments: +- `player: alt.Player` — player that groups were cleared from. +- `groups: string[]` — groups that were cleared from account. +- `target: 'character' | 'account'` — target of the operation. +!!! + ++++ useVirtual + +!!!warning For offline players only + +This method is intended to be used for offline players only. If you want to grant a permission to a player that is currently online, use `useCharacter` or `useAccount` permissions proxies. + +!!! + +```typescript #9 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +async function clearGroups(_id: string) { + const document = await Rebar.document.useVirtual(_id, CollectionNames.Accounts); + if (document) { + await virtual.groups.clear(); + } +} +``` + +!!!warning Event won't be triggered +Unlike the `useCharacter` and `useAccount` permissions proxies, the `useVirtual` permissions proxy won't trigger any events. +!!! + ++++ + +## Getting a list of groups + +To get a list of groups that a player is in, you can use the `useAccount`, `useCharacter` and `useVirtual` functions. All of them have a `groups` property that you can use to get a list of groups. + ++++ useCharacter +```typescript #7 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +function getGroups(player: alt.Player) { + const character = Rebar.document.character.useCharacter(player); + return character.groups.get(); +} +``` + ++++ useAccount +```typescript #7 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +function getGroups(player: alt.Player) { + const account = Rebar.document.account.useAccount('account-id'); + return account.groups.get(); +} +``` + ++++ useVirtual + +```typescript #9 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); +const { CollectionNames } = Rebar.database; + +async function getGroups(_id: string) { + const document = await Rebar.document.useVirtual(_id, CollectionNames.Accounts); + if (document) { + return virtual.groups.get(); + } + return undefined; +} +``` + ++++ + +## Check group membership + +To check if a player is a member of a group, you can use the `useAccount`, `useCharacter` and `useVirtual` functions. All of them have a `groups` property that you can use to check if a player is a member of a group. + ++++ useCharacter +```typescript #7 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +function isMemberOfAdminGroup(player: alt.Player) { + const character = Rebar.document.character.useCharacter(player); + return character.groups.memberOf('admin'); +} +``` + ++++ useAccount +```typescript #7 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +function isMemberOfAdminGroup(player: alt.Player) { + const account = Rebar.document.account.useAccount('account-id'); + return account.groups.memberOf('admin'); +} +``` + ++++ useVirtual + +```typescript #9 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); +const { CollectionNames } = Rebar.database; + +async function isMemberOfAdminGroup(_id: string) { + const document = await Rebar.document.useVirtual(_id, CollectionNames.Accounts); + if (document) { + return virtual.groups.memberOf('admin'); + } + return false; +} +``` + ++++ diff --git a/docs/useRebar/systems/permissions/playerPermissions.md b/docs/useRebar/systems/permissions/playerPermissions.md new file mode 100644 index 000000000..707c45c7d --- /dev/null +++ b/docs/useRebar/systems/permissions/playerPermissions.md @@ -0,0 +1,449 @@ +--- +title: Player permissions +order: b +--- + +# Managing player permissions + +To manage player permissions, Rebar exposes the same interface for both account and character permissions. + +### Grant permission [!badge Async] + +To grant a permission to a player, you can use one of the `grant` methods. + ++++ useCharacter + +```typescript #6 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +async function allowBan(player: alt.Player) { + const granted = await Rebar.document.character.useCharacter(player).permissions.grant('ban'); + if (!granted) { + console.log('Permission was already granted.'); + } else { + console.log('Permission was granted.'); + } +} + +``` +!!!info Event will be triggered +This will trigger a `rebar:permissions:grant` event with following arguments: +- `player: alt.Player` — player that permissions was granted to. +- `permission: string` — permission that was granted. +- `target: 'character' | 'account'` — target of the operation. +!!! + ++++ useAccount + +```typescript #6 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +async function allowBan(player: alt.Player) { + const granted = await Rebar.document.account.useAccount(player).permissions.grant('ban'); + if (!granted) { + console.log('Permission was already granted.'); + } else { + console.log('Permission was granted.'); + } +} + +``` +!!!info Event will be triggered +This will trigger a `rebar:permissions:grant` event with following arguments: +- `player: alt.Player` — player that permissions was granted to. +- `permission: string` — permission that was granted. +- `target: 'character' | 'account'` — target of the operation. +!!! + ++++ useVirtual + +!!!warning For offline players only +This method is intended to be used for offline players only. If you want to grant a permission to a player that is currently online, use `useCharacter` or `useAccount` permissions proxies. +!!! + +```typescript #7,9 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); +const { CollectionNames } = Rebar.database; + +async function allowBan(_id: string) { + const document = await Rebar.document.virtual.useVirtual(_id, CollectionNames.Accounts); + if (document) { + const granted = await document.permissions.grant('ban'); + if (!granted) { + console.log('Permission was already granted.'); + } else { + console.log('Permission was granted.'); + } + } else { + console.log('Account not found.'); + } +} + +``` +!!!warning Event won't be triggered +Unlike the `useCharacter` and `useAccount` permissions proxies, the `useVirtual` permissions proxy won't trigger any events. +!!! ++++ + +### Revoke permission [!badge Async] + +To revoke a permission from a player, you can use one of the `revoke` methods. + ++++ useCharacter + +```typescript #6 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +async function disallowBan(player: alt.Player) { + const revoked = await Rebar.document.character.useCharacter(player).permissions.revoke('ban'); + if (!revoked) { + console.log('Permission was already revoked.'); + } else { + console.log('Permission was revoked.'); + } +} + +``` +!!!info Event will be triggered +This will trigger a `rebar:permissions:revoke` event with following arguments: +- `player: alt.Player` — player that permissions was revoked from. +- `permission: string` — permission that was revoked. +- `target: 'character' | 'account'` — target of the operation. +!!! + ++++ useAccount + +```typescript #6 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +async function disallowBan(player: alt.Player) { + const revoked = await Rebar.document.account.useAccount(player).permissions.revoke('ban'); + if (!revoked) { + console.log('Permission was already revoked.'); + } else { + console.log('Permission was revoked.'); + } +} + +``` + +!!!info Event will be triggered +This will trigger a `rebar:permissions:revoke` event with following arguments: +- `player: alt.Player` — player that permissions was revoked from. +- `permission: string` — permission that was revoked. +- `target: 'character' | 'account'` — target of the operation. +!!! + ++++ useVirtual + +!!!warning For offline players only +This method is intended to be used for offline players only. If you want to revoke a permission from a player that is currently online, use `useCharacter` or `useAccount` permissions proxies. +!!! + +```typescript #7,9 +import {useRebar} from '@Server/index.js'; + +const Rebar = useRebar(); +const {CollectionNames} = Rebar.database; + +async function disallowBan(_id: string) { + const document = await Rebar.document.virtual.useVirtual(_id, CollectionNames.Accounts); + if (document) { + const revoked = await document.permissions.revoke('ban'); + if (!revoked) { + console.log('Permission was already revoked.'); + } else { + console.log('Permission was revoked.'); + } + } else { + console.log('Account not found.'); + } +} + +``` +!!!warning Event won't be triggered +Unlike the `useCharacter` and `useAccount` permissions proxies, the `useVirtual` permissions proxy won't trigger any events. +!!! ++++ + +### Clear permissions [!badge Async] + +To clear all permissions from a player, you can use one of the `clear` methods. + ++++ useCharacter + +```typescript #6 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +async function clearPermissions(player: alt.Player) { + await Rebar.document.character.useCharacter(player).permissions.clear(); + console.log('Permissions were cleared.'); +} +``` +!!!info Event will be triggered +This will trigger a `rebar:permissions:clear` event with following arguments: +- `player: alt.Player` — player that permissions were cleared from. +- `permissions: string[]` — permissions that were cleared. +- `target: 'character' | 'account'` — target of the operation. +!!! + ++++ useAccount + +```typescript #6 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +async function clearPermissions(player: alt.Player) { + await Rebar.document.account.useAccount(player).permissions.clear(); + console.log('Permissions were cleared.'); +} +``` + +!!!info Event will be triggered +This will trigger a `rebar:permissions:clear` event with following arguments: +- `player: alt.Player` — player that permissions were cleared from. +- `permissions: string[]` — permissions that were cleared. +- `target: 'character' | 'account'` — target of the operation. +!!! + ++++ useVirtual + +!!!warning For offline players only +This method is intended to be used for offline players only. If you want to clear permissions from a player that is currently online, use `useCharacter` or `useAccount` permissions proxies. +!!! + +```typescript #7,9 +import {useRebar} from '@Server/index.js'; + +const Rebar = useRebar(); +const { CollectionNames } = Rebar.database; + +async function clearPermissions(_id: string) { + const document = await Rebar.document.virtual.useVirtual(_id, CollectionNames.Accounts); + if (document) { + await document.permissions.clear(); + console.log('Account permissions were cleared.'); + } else { + console.log('Account not found.'); + } +} +``` +!!!warning Event won't be triggered +Unlike the `useCharacter` and `useAccount` permissions proxies, the `useVirtual` permissions proxy won't trigger any events. +!!! ++++ + +### List permissions + +To list all permissions granted to a player, you can use one of the `list` methods. + ++++ useCharacter + +```typescript #6 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +function listPermissions(player: alt.Player) { + const permissions = Rebar.document.character.useCharacter(player).permissions.list(); + console.log('Permissions:', permissions); +} +``` + ++++ useAccount + +```typescript #6 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +function listPermissions(player: alt.Player) { + const permissions = Rebar.document.account.useAccount(player).permissions.list(); + console.log('Permissions:', permissions); +} +``` + ++++ useVirtual + +```typescript #7,9 +import {useRebar} from '@Server/index.js'; + +const Rebar = useRebar(); +const { CollectionNames } = Rebar.database; + +async function listPermissions(_id: string) { + const document = await Rebar.document.virtual.useVirtual(_id, CollectionNames.Accounts); + if (document) { + const permissions = document.permissions.list(); + console.log('Permissions:', permissions); + } else { + console.log('Account not found.'); + } +} +``` ++++ + +### Check specific permission + +To check if a player has a specific permission, you can use one of the `has` methods. + ++++ useCharacter + +```typescript #6 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +async function hasBanPermission(player: alt.Player) { + const hasPermission = await Rebar.document.character.useCharacter(player).permissions.has('ban'); + console.log('Has permission:', hasPermission); +} +``` + ++++ useAccount + +```typescript #6 + +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +async function hasBanPermission(player: alt.Player) { + const hasPermission = await Rebar.document.account.useAccount(player).permissions.has('ban'); + console.log('Has permission:', hasPermission); +} +``` + ++++ useVirtual + +```typescript #7,9 +import {useRebar} from '@Server/index.js'; + +const Rebar = useRebar(); +const { CollectionNames } = Rebar.database; + +async function hasBanPermission(_id: string) { + const document = await Rebar.document.virtual.useVirtual(_id, CollectionNames.Accounts); + if (document) { + const hasPermission = await document.permissions.has('ban'); + console.log('Has permission:', hasPermission); + } else { + console.log('Account not found.'); + } +} +``` ++++ + +### Check all permissions + +To check if player has all of the specified permissions, you can use one of the `hasAll` methods. + ++++ useCharacter + +```typescript #6 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +async function hasAllPermissions(player: alt.Player) { + const hasPermissions: boolean = await Rebar.document.character.useCharacter(player).permissions.hasAll(['ban', 'kick']); + console.log('Has all permissions:', hasPermissions); +} +``` + ++++ useAccount + +```typescript #6 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +async function hasAllPermissions(player: alt.Player) { + const hasPermissions: boolean = await Rebar.document.account.useAccount(player).permissions.hasAll(['ban', 'kick']); + console.log('Has all permissions:', hasPermissions); +} +``` + ++++ useVirtual + +```typescript #7,9 +import {useRebar} from '@Server/index.js'; + +const Rebar = useRebar(); +const { CollectionNames } = Rebar.database; + +async function hasAllPermissions(_id: string) { + const document = await Rebar.document.virtual.useVirtual(_id, CollectionNames.Accounts); + if (document) { + const hasPermissions: boolean = await document.permissions.hasAll(['ban', 'kick']); + console.log('Has all permissions:', hasPermissions); + } else { + console.log('Account not found.'); + } +} +``` ++++ + +### Check any permission + +To check if player has at least one of the specified permissions, you can use one of the `hasAny` methods. + ++++ useCharacter + +```typescript #6 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +async function hasAnyPermission(player: alt.Player) { + const hasPermissions: boolean = await Rebar.document.character.useCharacter(player).permissions.hasAnyOf(['ban', 'kick']); + console.log('Has any permission:', hasPermissions); +} +``` + ++++ useAccount + +```typescript #6 +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +async function hasAnyPermission(player: alt.Player) { + const hasPermissions: boolean = await Rebar.document.account.useAccount(player).permissions.hasAnyOf(['ban', 'kick']); + console.log('Has any permission:', hasPermissions); +} +``` + ++++ useVirtual + +```typescript #7,9 +import {useRebar} from '@Server/index.js'; + +const Rebar = useRebar(); +const { CollectionNames } = Rebar.database; + +async function hasAnyPermission(_id: string) { + const document = await Rebar.document.virtual.useVirtual(_id, CollectionNames.Accounts); + if (document) { + const hasPermissions: boolean = await document.permissions.hasAnyOf(['ban', 'kick']); + console.log('Has any permission:', hasPermissions); + } else { + console.log('Account not found.'); + } +} +``` ++++ + diff --git a/docs/useRebar/systems/permissions/useEntityPermissions.md b/docs/useRebar/systems/permissions/useEntityPermissions.md new file mode 100644 index 000000000..b1ddca20c --- /dev/null +++ b/docs/useRebar/systems/permissions/useEntityPermissions.md @@ -0,0 +1,56 @@ +--- +order: f +title: useEntityPermissions +--- + +# useEntityPermissions + +This function allows you to easily integrate permission system into your feature. + +It checks if player has a permission to perform an action on an entity, which type was extended from `PermissionOptions`. + +```typescript +import { PermissionOptions } from '@Shared/types/index.js'; + +export interface MyEntity extends PermissionOptions { + // your entity properties +} +``` + +This extends the entity with `permissions` property, this allows you to define complex permission structures, like: + +`permissions` property accepts a string, an array of strings, or an object with `and` and `or` properties. + +```typescript + +const firstEntity: MyEntity = { + permissions: 'myPermission', +} + +const secondEntity: MyEntity = { + permissions: ['myPermission', 'anotherPermission'], +} + +const thirdEntity: MyEntity = { + permissions: { + and: ['myPermission', { or: ['anotherPermission', 'yetAnotherPermission'] }], + }, +} + +``` + +## Checking permissions + +This function allows you to check if player has a permission to perform an action on an entity. + +It will search for a permission for both player's account and character. + +```typescript +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +function hasPermission(player: alt.Player, entity: MyEntity): boolean { + return Rebar.permissions.useEntityPermissions(entity).hasPermission(player); +} +``` diff --git a/docs/useRebar/systems/permissions/usePermissionGroup.md b/docs/useRebar/systems/permissions/usePermissionGroup.md new file mode 100644 index 000000000..b2db96e32 --- /dev/null +++ b/docs/useRebar/systems/permissions/usePermissionGroup.md @@ -0,0 +1,133 @@ +--- +order: e +title: usePermissionGroup +--- + +# usePermissionGroup + +Permission groups are a way to organize permissions into groups, and then assign those groups to players. This way, you can easily manage permissions for multiple players at once. + +## Creating a permission group + +You can create a permission group with these properties: +- `permissions: string[]` - an array of permission names that this group should have. +- `inherits?: string` - a name of a group that this group should inherit permissions from. This way, you can create a hierarchy of groups. +- `version?: number` - a version of the group. + +`inherits` is a way to create a hierarchy of groups. If a group inherits from another group, it will have all the permissions from the inherited group. + +Version is used to track changes in the group. On server start, the server is loading all groups from the database and comparing them with the groups in the code. + +Conditions when the group will be updated in the database: +- The same group in the code and group in the database have **no** version property. +- Group in the code has version that is **greater than** the group in the database. + +```typescript +import { useRebar } from '@Server/index.js'; +const groups = Rebar.permissions.usePermissionGroup(); + +await groups.add('support', { + permissions: ['kick', 'adminChat', 'mute'], + version: 1, +}); + +await groups.add('admin', { + permissions: ['ban', 'unban'], + inherits: 'support', +}); + +await groups.add('chief', { + permissions: ['grant', 'revoke'], + inherits: 'admin', +}); +``` + +In this example, we created three groups: `support`, `admin`, and `chief`. +- The `support` group has three permissions: `kick`, `adminChat`, and `mute`. +- The `admin` group has two permissions: `ban` and `unban`, and inherits all permissions from the `support` group. +- The `chief` group has two permissions: `grant` and `revoke`, and inherits all permissions from the `admin` group, which in turn inherits all permissions from the `support` group. + +## Removing a permission group + +You can remove a permission group by calling the `remove` method with the name of the group you want to remove. + +```typescript +import { useRebar } from '@Server/index.js'; + +const groups = Rebar.permissions.usePermissionGroup(); + +await groups.remove('support'); +``` + +If groups exists, it will remove the `support` group from the database. + +!!!info +On success, this will remove this group from all players(online/offline) that have this group attached. +!!! + +## Add permissions to a group + +You can add permissions to a group by calling the `addPermission` method with the name of the group and the permission you want to add. + +```typescript +import { useRebar } from '@Server/index.js'; + +const groups = Rebar.permissions.usePermissionGroup(); + +await groups.addPermission('support', 'teleport'); +``` + +This will add the `teleport` permission to the `support` group. + +!!!info +On success, this will increment the version of the group and update this group in the database. +So, on next server start, if you want to make changes to the group, you need to increment the version of the group in the code. +!!! + +## Remove permissions from a group + +You can remove permissions from a group by calling the `removePermission` method with the name of the group and the permission you want to remove. + +```typescript +import { useRebar } from '@Server/index.js'; + +const groups = Rebar.permissions.usePermissionGroup(); + +await groups.removePermission('support', 'teleport'); +``` + +This will remove the `teleport` permission from the `support` group. + +!!!info +On success, this will increment the version of the group and update this group in the database. +So, on next server start, if you want to make changes to the group, you need to increment the version of the group in the code. +!!! + +## Check permission + +You can check if group has a permission by calling the `groupHasPermission` method with the name of the group and the permission you want to check. + +```typescript +import { useRebar } from '@Server/index.js'; + +const groups = Rebar.permissions.usePermissionGroup(); + +const hasPermission = await groups.groupHasPermission('support', 'teleport'); +``` + +This will return `true` if the `support` group has the `teleport` permission, otherwise `false`. + +## List Permissions + +You can list all permissions recursively for a group by calling the `groupsToPlainPermissions` method with the name of the group. + +```typescript +import { useRebar } from '@Server/index.js'; + +const groups = Rebar.permissions.usePermissionGroup(); + +const permissions = await groups.groupsToPlainPermissions('chief'); +``` + +This will return an array of all permissions for the `chief` group, including all inherited permissions. +So, according to the `add` example, this will return `['kick', 'adminChat', 'mute', 'ban', 'unban', 'grant', 'revoke']`. diff --git a/docs/useRebar/systems/permissions/usePermissions.md b/docs/useRebar/systems/permissions/usePermissions.md new file mode 100644 index 000000000..8f5bc8981 --- /dev/null +++ b/docs/useRebar/systems/permissions/usePermissions.md @@ -0,0 +1,64 @@ +--- +order: d +title: usePermissions +--- + +# usePermissions + +This function allows you to easily check if a player has a permission. It is a wrapper around the `useAccount` and `useCharacter` functions, and it will automatically check the permissions for the player's character or account. + +## Checking permissions + +```typescript +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +function hasPermission(player: alt.Player, permission: string): boolean { + return Rebar.permissions.usePermissions(player).hasPermission(permission); +} +``` + +This will start from account permissions, then will check account groups' permissions, then character permissions, and finally character groups' permissions. If the permission is found at any level, it will return `true` immediately. If the permission is not found at any level, it will return `false`. + +## Getting a flat permission list + +```typescript +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +function getPermissions(player: alt.Player): string[] { + return Rebar.permissions.usePermissions(player).listAllPermissions(); +} +``` + +This will return a flat list of all permissions that the player has. +Includes: +- Account permissions +- Account group permissions, including inherited groups +- Character permissions +- Character group permissions, including inherited groups + +## Proxy functions + +The `usePermissions` function also has proxy functions for `useAccount` and `useCharacter` permissions/groups functions. +This allows you to easily modify permissions and groups for the player's account or character. + +```typescript +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +function grantCharacterPermission(player: alt.Player, permission: string): void { + Rebar.permissions.usePermissions(player).character.permissions.grant(permission); +} + +function grantAccountPermission(player: alt.Player, permission: string): void { + Rebar.permissions.usePermissions(player).account.permissions.grant(permission); +} +``` + +Follow these links to see more details on how to use these functions for groups and permissions: +- [permissions](/userebar/systems/permissions/playerPermissions.md) +- [groups](/userebar/systems/permissions/playerGroups.md) diff --git a/docs/useRebar/systems/useMessenger.md b/docs/useRebar/systems/useMessenger.md index 82ddde95a..96d80fac9 100644 --- a/docs/useRebar/systems/useMessenger.md +++ b/docs/useRebar/systems/useMessenger.md @@ -4,6 +4,9 @@ The messenger system allows for developers to easily send up messages from playe However, the messages are not automatically sent to other players. You as a developer get to decide who sees what messages or if they see text messages at all. +Commands have permission system built-in, so you can easily allow players to use certain commands based on their permissions. +For more details on how to use permissions, check [useEntityPermissions](/userebar/systems/permissions/useEntityPermissions.md). + ## Usage ```ts @@ -26,7 +29,7 @@ messenger.commands.register({ messenger.commands.register({ name: '/goto', desc: '/goto [x][y][z]', - options: { accountPermissions: ['moderator'] }, + options: { permissions: ['moderator'] }, callback: (player: alt.Player, x: string, y: string, z: string) => { try { const pos = new alt.Vector3(parseFloat(x), parseFloat(y), parseFloat(z)); diff --git a/docs/useRebar/systems/useServerTime.md b/docs/useRebar/systems/useServerTime.md deleted file mode 100644 index d0e3be93f..000000000 --- a/docs/useRebar/systems/useServerTime.md +++ /dev/null @@ -1,24 +0,0 @@ -# useServerTime - -This stores server time during runtime, and allows for time to be shared to other plugins. - -Additionally, when these functions are used the internal RebarEvents for time changes are invoked. - -```ts -import * as alt from 'alt-server'; -import { useRebar } from '@Server/index.js'; - -const Rebar = useRebar(); -const ServerTime = Rebar.useServerTime(); - -function whatever() { - // Returns { hour, minute, second } - const { hour, minute, second } = ServerTime.getTime(); - - // Get current time - const currentTime = new Date(Date.now()); - ServerTime.setHour(currentTime.getHours()); - ServerTime.setMinute(currentTime.getMinutes()); - ServerTime.setSecond(currentTime.getSeconds()); -} -``` diff --git a/docs/useRebar/systems/useServerWeather.md b/docs/useRebar/systems/useServerWeather.md deleted file mode 100644 index eb6f3d846..000000000 --- a/docs/useRebar/systems/useServerWeather.md +++ /dev/null @@ -1,25 +0,0 @@ -# useServerWeather - -This stores server weather during runtime, and allows for weather to be shared to other plugins. - -Additionally, when these functions are used the internal RebarEvents for weather changes are invoked. - -```ts -import * as alt from 'alt-server'; -import { useRebar } from '@Server/index.js'; - -const Rebar = useRebar(); -const serverWeather = Rebar.useServerWeather(); - -function whatever() { - // Used as a way to store the current weather on server-side for other plugins - serverWeather.set('CLEARING'); - - // Used as a way to show a weather forecast for the upcoming weather events - serverWeather.setForecast(['CLEAR', 'CLOUDS', 'RAIN', 'THUNDER', 'FOGGY', 'CLOUDS', 'CLEARING', 'EXTRASUNNY']) - - const currentWeather = serverWeather.get(); - - const currentForecast = serverWeather.getForecast(); -} -``` diff --git a/docs/useRebar/useApi.md b/docs/useRebar/useApi.md index 0ea6638a2..2f80c85e7 100644 --- a/docs/useRebar/useApi.md +++ b/docs/useRebar/useApi.md @@ -79,3 +79,13 @@ async function init() { init(); ``` + +4. Using a sperate api.ts +## How to put the API into a seperate file + +If you want to write your API in a separate file like an 'api.ts' then you have to import it in your index.ts because only the 'index.ts' is loaded by default. You can then use the API in any other plugin + +```ts +import ‘path_to_file/api.js’; +``` + diff --git a/docs/useRebar/useCronJob.md b/docs/useRebar/useCronJob.md new file mode 100644 index 000000000..b56bcb72d --- /dev/null +++ b/docs/useRebar/useCronJob.md @@ -0,0 +1,65 @@ +--- +order: 930 +--- + +# useCronJob + +This API allows you to create timed events that happen at different intervals based on server time. Such as every 5th minute of the hour, every hour, etc. + +Details on how to create the cron expression can be found in the node-cron documentation: + +[Cron Syntax](https://www.npmjs.com/package/node-cron#cron-syntax) + +## How to create new Cronjob + +To create a new cron job, you need a unique name, a cron expression that defines the time of execution and one or more functions that are executed. + +```ts +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); +const cronJob = Rebar.useCronJob(); + +declare global { + export interface CronJobs { + 'my-cronjob': { + cronExpression: string; + tasks: any[]; + }; + } +} + +export function task1() { + alt.log('Task 1'); +} + +cronJob.create('my-cronjob', '*/5 * * * * *', [task1]); +``` + +## How to remove a Cronjob + +```ts +cronJob.remove('my-cronjob'); +``` + +## How to add a function to an existing cronjob + + +```ts +export function task2() { + alt.log('Task 2'); +} + +cronJob.addTaskToCron('my-cronjob', task2); +``` + +## How to remove a function to an existing cronjob + + +```ts +export function task2() { + alt.log('Task 2'); +} + +cronJob.removeTaskFromCron('my-cronjob', task2); +``` diff --git a/docs/useRebar/useEvents.md b/docs/useRebar/useEvents.md deleted file mode 100644 index 67490e776..000000000 --- a/docs/useRebar/useEvents.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -order: 700 ---- - -# useEvents - -These events are unique to the Rebar framework, and help provide information about when something happens. - -## Usage - -```ts -import { useRebar } from '@Server/index.js'; - -const RebarEvents = useRebar().events.useEvents(); - -// Called when an account is bound to a player -RebarEvents.on('account-bound', (player, document) => { - console.log(document); -}); - -// Called when a character is bound to a player -RebarEvents.on('character-bound', (player, document) => { - console.log(document); -}); - -// Called when a vehicle document is bound to a vehicle -RebarEvents.on('vehicle-bound', (vehicle, document) => { - console.log(document); -}); - -// Called when a player sends a message -RebarEvents.on('message', (player, msg) => { - console.log(msg); -}); - -// Called whenever the time changes -RebarEvents.on('time-changed', (hour, minute, second) => { - console.log(hour, minute, second); -}); - -// Called whenever the hour increments by 1 -RebarEvents.on('time-hour-changed', (hour) => { - console.log(hour); -}); - -// Called whenever the minute increments by 1 -RebarEvents.on('time-minute-changed', (minute) => { - console.log(minute); -}); - -// Called whenever the second increments by 1 -RebarEvents.on('time-second-changed', (second) => { - console.log(second); -}); - -// Called when a page is opened -RebarEvents.on('page-opened', (player, pageName) => { - console.log('page opened'); - console.log(pageName); -}); - -// Called when a page is closed -RebarEvents.on('page-closed', (player, pageName) => { - console.log('page closed'); - console.log(pageName); -}); -``` - -## Custom Events - -You can declare global custom events in your plugin like this. - -```ts -declare global { - export interface RebarEvents { - 'character-select-done': (player: alt.Player) => void; - } -} -``` diff --git a/docs/useRebar/usePlayer.md b/docs/useRebar/usePlayer.md index c4afd2a41..bf23b85a2 100644 --- a/docs/useRebar/usePlayer.md +++ b/docs/useRebar/usePlayer.md @@ -393,9 +393,6 @@ rebarPlayer.weapon.sync(); // Save weapons & ammo await rebarPlayer.weapon.save(); -// Save just ammo -await rebarPlayer.weapon.saveAmmo(); - // Add a weapon, and save to the database, and re-apply weapons await rebarPlayer.weapon.add('WEAPON_MINIGUN', 100); @@ -408,17 +405,16 @@ await rebarPlayer.weapon.clear(); // Remove a weapon and all ammo for the weapon await rebarPlayer.weapon.clearWeapon('WEAPON_MINIGUN'); +// Returns all weapons the database has stored for the player +const weapons = rebarPlayer.weapon.getWeapons(); + // Override and apply weapons to a player const weapons = [ - { hash: alt.hash('WEAPON_MINIGUN'), components: [], tintIndex: 0 }, - { hash: alt.hash('WEAPON_RPG'), components: [], tintIndex: 0 }, + { hash: alt.hash('WEAPON_MINIGUN'), components: [], tintIndex: 0, ammo: 255 }, + { hash: alt.hash('WEAPON_RPG'), components: [], tintIndex: 0, ammo: 255 }, ]; -const ammo = { - [alt.hash('WEAPON_MINIGUN')]: 999, - [alt.hash('WEAPON_RPG')]: 5, -}; -rebarPlayer.weapon.apply(weapons, ammo); +rebarPlayer.weapon.apply(weapons); ``` ## Webview diff --git a/docs/useRebar/useRebar.md b/docs/useRebar/useRebar.md index 87da7e2e3..0c36b071a 100644 --- a/docs/useRebar/useRebar.md +++ b/docs/useRebar/useRebar.md @@ -19,11 +19,3 @@ const Rebar = useRebar(); const document = Rebar.document.account.useAccount(); const audioInstance = Rebar.player.useAudio(somePlayer); ``` - -## alt:V Based Import - -```ts -import * as alt from 'alt-server'; - -const Rebar = alt.getMeta('Rebar'); -``` diff --git a/docs/useRebar/useServices.md b/docs/useRebar/useServices.md new file mode 100644 index 000000000..d56c4e58d --- /dev/null +++ b/docs/useRebar/useServices.md @@ -0,0 +1,315 @@ +--- +order: 550 +--- + +# useServices + +Services provide a way for plugin developers to call a shared interface handled by Rebar to handle common place functionality. + +Examples being... + +- Adding Currency +- Subtracting Currency +- Adding an Item +- Removing an Item +- Checking if an Item Exists +- Changing Weather +- Getting Weather +- Changing Time +- Getting Time +- etc. + +## Example + +### Service Register Example + +```ts +Rebar.services.useServiceRegister().register('items', { + add: async (player, id, quantity) => { + // Do standard add items to inventory, and whatever else is necessary + // such as inventory checks, weight checks, etc. + // Return false if added incorrectly + + // Return true if added correctly + return true; + }, + sub: async (player, id quantity) => { + + + // Return true if subtracted enough + return true; + }, + has: async (player, id, quantity) => { + // Return true if has enough of an item + }, + remove: async (player, uid) => { + // return true if removed an item entirely + } +}); +``` + +### Service Invoke Example + +```ts +async function addFishingRod(player: alt.Player) { + const didAdd = await useItemService().add(player, 'fishingrod', 1); + if (!didAdd) { + // fishing rod was not added + return; + } + + // fishing rod was added, do something here... +} +``` + +## useCurrencyService + +### Register + +```ts +Rebar.services.useServiceRegister().register('currencyService', { + async add(player, type, quantity) { + // Handle add logic... + // Return true if added successfully + return true; + }, + async sub(player, type, quantity) { + // Handle sub logic... + // Return true if added successfully + return true; + }, + async has(player, type, quantity) { + // Handle has logic... + // Return true if has enough of a currency type + return true; + }, +}); +``` + +### add + +Adds a specified currency type to a given player with a specific quantity. + +```ts +const didAdd = await Rebar.services.useCurrencyService().add(somePlayer, 'cash', 5); +``` + +### sub + +Subtracts a specified currency type to a given player with a specific quantity. + +```ts +const didSub = await Rebar.services.useCurrencyService().sub(somePlayer, 'cash', 5); +``` + +### has + +Checks if the player has enough of a specified currency type. + +```ts +const has = await Rebar.services.useCurrencyService().has(somePlayer, 'cash'); +``` + +## useDeathService + +### Register + +```ts +Rebar.services.useServiceRegister().register('deathService', { + respawn(player, pos) { + // handle respawning a player somewhere + }, + revive(player) { + // handle reviving a player in the same position + }, +}); +``` + +### respawn + +Respawn is meant to be used as a way to respawn the player through some other means in your game mode. + +```ts +Rebar.services.useDeathService().respawn(somePlayer, new alt.Vector3(0, 0, 0)); +``` + +### revive + +Revive is meant to be used to respawn the player in their same position + +```ts +Rebar.services.useDeathService().revive(somePlayer); +``` + +## useItemService + +### Register + +```ts +Rebar.services.useServiceRegister().register('itemService', { + async add(player, id, quantity, data) { + // this should try and add an item quantity to the given player, with optional specified data + // Return false if they run out of room, or exceed weight. Up to library creator. + + return true; + }, + async sub(player, id, quantity) { + // This should try and subtract item quantity from the given player, with optional specified data + // Return false if they don't have enough + + return true; + }, + async has(player, id, quantity) { + // This should try and see if the player has a specified item, and a given quantity + // Return false if they do not + + return true; + }, + async remove(player, uid) { + // This should return true if the item can successfully be removed entirely by a unique identifier + // Every item should have some form of uid for easy removal + // Return false if the uid item does not exist + + return true; + }, +}); +``` + +### add + +This should try and add an item quantity to the given player. + +This will return `false` if the item cannot be added. + +```ts +const didAdd = Rebar.services.useItemService().add(somePlayer, 'fishing-rod', 1, { customData: 'here' }); +``` + +### sub + +This should try and subtract an item quantity from the given player. + +This will return `false` if the item quantity cannot be subtracted. + +```ts +const didSub = Rebar.services.useItemService().sub(somePlayer, 'fishing-rod', 1); +``` + +### has + +This should try and return `true/false` if the player has a specific item and a given quantity. + +```ts +const hasEnough = Rebar.services.useItemService().has(somePlayer, 'cabbage-seeds', 100); +``` + +### remove + +This should try and remove any item with a given `uid` and remove it entirely from the inventory. + +_UID stands for unique identifier._ + +```ts +const isRemoved = await Rebar.services.useItemService().remove(somePlayer, 'c03b9f08-dd85-46fe-ba07-429b0f79deaf'); +``` + +## useNotificationService + +### Register + +```ts +Rebar.services.useServiceRegister().register('notificationService', { + broadcast(msg, type) { + // Should send a message to all logged in players in the form of a notification + // Type can literally be anything you want + }, + emit(player, msg, type) { + // Should send a message to the given player in the form of a notification + // Type can literally be anything you want + }, +}); +``` + +### broadcast + +```ts +Rebar.services.useNotificationService().broadcast('hello everyone!', 'announcement'); +``` + +### emit + +```ts +Rebar.services.useNotificationService().emit(somePlayer, 'hello person!', 'info'); +``` + +## useTimeService + +### Register + +```ts +Rebar.services.useServiceRegister().register('timeService', { + setTime(hour, minute, second) { + // what to do when time is set + }, +}); +``` + +### setTime + +Sets the in-game time for the whole server. + +`hour`,`minute`,`second` + +```ts +Rebar.services.useTimeService().setTime(5, 0, 0); +``` + +### getTime + +Returns the current server time. + +```ts +const currentTime = Rebar.services.useTimeService().getTime(); +console.log(currentTime.hour); +``` + +## useWeatherService + +### Register + +```ts +Rebar.services.useServiceRegister().register('weatherService', { + setWeather(type) { + // Passes a single weather type compatible for the game + }, + setWeatherForecast(types) { + // Passes an array of weather types to loop through + }, +}); +``` + +### setWeather + +```ts +Rebar.services.useWeatherService().setWeather('EXTRASUNNY'); +``` + +### setWeatherForecast + +```ts +Rebar.services + .useWeatherService() + .setWeather(['EXTRASUNNY', 'CLEARING', 'OVERCAST', 'THUNDER', 'OVERCAST', 'CLEARING']); +``` + +### getWeather + +```ts +const currentWeather = Rebar.services.useWeatherService().getWeather(); +``` + +### getWeatherForecast + +```ts +const weatherForecast = Rebar.services.useWeatherService().getWeatherForecast(); +``` diff --git a/docs/useRebar/utility.md b/docs/useRebar/utility.md index b7fb34666..30d12f503 100644 --- a/docs/useRebar/utility.md +++ b/docs/useRebar/utility.md @@ -189,7 +189,7 @@ Generates a random short string as an identifier const uid = Rebar.utility.uid.generate(); ``` -# useProtectCallback +## useProtectCallback When you're using events, or you need callbacks to be fully protected and permissioned this utility functions provides a simple wrapper to check for permissions before the callback is executed. @@ -221,7 +221,7 @@ alt.onClient( ); ``` -# useRateLimitCallback +## useRateLimitCallback When you're using events, or you need callbacks to be protected by a rate limiter, this is a wrapper you can use. diff --git a/docs/useRebarClient/api/api.md b/docs/useRebarClient/api/api.md index 8fafca55c..7206e6f76 100644 --- a/docs/useRebarClient/api/api.md +++ b/docs/useRebarClient/api/api.md @@ -23,8 +23,8 @@ export function useMyCoolAPI() { ``` 2. Create a global declaration for your API. - +++ Without arguments ++++ Without arguments ```ts import { useClientApi } from '@Client/api/index.js'; @@ -119,6 +119,7 @@ function someFunction(somePlayer: alt.Player) { useMyCoolAPI(somePlayer).logPlayerName(); } ``` ++++ If you do not want to worry about load order. Consider the following pattern: diff --git a/docs/useRebarClient/client-api-usage.md b/docs/useRebarClient/client-api-usage.md index 1c3517a36..84046c96a 100644 --- a/docs/useRebarClient/client-api-usage.md +++ b/docs/useRebarClient/client-api-usage.md @@ -4,14 +4,8 @@ The client API can be accessed through a single import. If you want more direct imports, those are also available through your intellisense. -## alt:V Based Import -```ts -import * as alt from 'alt-client'; - -const Rebar = alt.getMeta('RebarClient'); -``` - ## Direct Import + ```ts import { useRebar } from '@Client/index.js'; diff --git a/package.json b/package.json index 17d81ecd0..252f260ff 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "author": "stuyk", "type": "module", - "version": "52", + "version": "53", "scripts": { "dev": "nodemon --config ./nodemon-dev.json -x pnpm start", "dev:linux": "nodemon --config ./nodemon-dev.json -x pnpm start:linux", @@ -15,38 +15,40 @@ "postinstall": "pnpm binaries && pnpm build:docker" }, "devDependencies": { - "@altv/types-client": "^16.2.3", - "@altv/types-natives": "^16.2.0", - "@altv/types-server": "^16.2.2", - "@altv/types-shared": "^16.2.1", + "@altv/types-client": "^16.2.6", + "@altv/types-natives": "^16.2.1", + "@altv/types-server": "^16.2.4", + "@altv/types-shared": "^16.2.3", "@altv/types-webview": "^16.2.1", "@altv/types-worker": "^16.2.0", - "@types/node": "^20.14.11", + "@types/node": "^20.16.4", "altv-pkg": "^2.7.5", - "autoprefixer": "^10.4.19", + "autoprefixer": "^10.4.20", "fast-glob": "^3.3.2", "fkill": "^9.0.0", "nodemon": "^3.1.4", - "postcss": "^8.4.39", + "postcss": "^8.4.45", "prettier": "^3.3.3", "prettier-plugin-tailwindcss": "^0.5.14", "retypeapp": "^3.5.0", "shx": "^0.3.4", "sucrase": "^3.35.0", - "tailwindcss": "^3.4.6" + "tailwindcss": "^3.4.10" }, "dependencies": { - "@hono/node-server": "^1.12.0", - "@vitejs/plugin-vue": "^5.0.5", + "@hono/node-server": "^1.12.2", + "@types/sjcl": "^1.0.34", + "@vitejs/plugin-vue": "^5.1.3", "concurrently": "^8.2.2", "dotenv": "^16.4.5", - "hono": "^4.5.0", + "hono": "^4.5.11", "mongodb": "^6.8.0", + "node-cron": "^3.0.3", "sjcl": "^1.0.8", - "typescript": "^5.5.3", - "vite": "^5.3.4", - "vue": "^3.4.32", - "vue-tsc": "^2.0.26" + "typescript": "^5.5.4", + "vite": "^5.4.3", + "vue": "^3.5.1", + "vue-tsc": "^2.1.4" }, "prettier": { "tabWidth": 4, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb64f1529..96a166254 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,11 +9,14 @@ importers: .: dependencies: '@hono/node-server': - specifier: ^1.12.0 - version: 1.12.0 + specifier: ^1.12.2 + version: 1.12.2(hono@4.5.11) + '@types/sjcl': + specifier: ^1.0.34 + version: 1.0.34 '@vitejs/plugin-vue': - specifier: ^5.0.5 - version: 5.0.5(vite@5.3.4(@types/node@20.14.11))(vue@3.4.32(typescript@5.5.3)) + specifier: ^5.1.3 + version: 5.1.3(vite@5.4.3(@types/node@20.16.4))(vue@3.5.1(typescript@5.5.4)) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -21,54 +24,57 @@ importers: specifier: ^16.4.5 version: 16.4.5 hono: - specifier: ^4.5.0 - version: 4.5.0 + specifier: ^4.5.11 + version: 4.5.11 mongodb: specifier: ^6.8.0 version: 6.8.0 + node-cron: + specifier: ^3.0.3 + version: 3.0.3 sjcl: specifier: ^1.0.8 version: 1.0.8 typescript: - specifier: ^5.5.3 - version: 5.5.3 + specifier: ^5.5.4 + version: 5.5.4 vite: - specifier: ^5.3.4 - version: 5.3.4(@types/node@20.14.11) + specifier: ^5.4.3 + version: 5.4.3(@types/node@20.16.4) vue: - specifier: ^3.4.32 - version: 3.4.32(typescript@5.5.3) + specifier: ^3.5.1 + version: 3.5.1(typescript@5.5.4) vue-tsc: - specifier: ^2.0.26 - version: 2.0.26(typescript@5.5.3) + specifier: ^2.1.4 + version: 2.1.4(typescript@5.5.4) devDependencies: '@altv/types-client': - specifier: ^16.2.3 - version: 16.2.3(@altv/types-shared@16.2.1)(@altv/types-worker@16.2.0) + specifier: ^16.2.6 + version: 16.2.6(@altv/types-shared@16.2.3)(@altv/types-worker@16.2.0) '@altv/types-natives': - specifier: ^16.2.0 - version: 16.2.0(@altv/types-client@16.2.3(@altv/types-shared@16.2.1)(@altv/types-worker@16.2.0)) + specifier: ^16.2.1 + version: 16.2.1(@altv/types-client@16.2.6(@altv/types-shared@16.2.3)(@altv/types-worker@16.2.0)) '@altv/types-server': - specifier: ^16.2.2 - version: 16.2.2(@altv/types-shared@16.2.1) + specifier: ^16.2.4 + version: 16.2.4(@altv/types-shared@16.2.3) '@altv/types-shared': - specifier: ^16.2.1 - version: 16.2.1 + specifier: ^16.2.3 + version: 16.2.3 '@altv/types-webview': specifier: ^16.2.1 version: 16.2.1 '@altv/types-worker': specifier: ^16.2.0 - version: 16.2.0(@altv/types-client@16.2.3) + version: 16.2.0(@altv/types-client@16.2.6) '@types/node': - specifier: ^20.14.11 - version: 20.14.11 + specifier: ^20.16.4 + version: 20.16.4 altv-pkg: specifier: ^2.7.5 version: 2.7.5 autoprefixer: - specifier: ^10.4.19 - version: 10.4.19(postcss@8.4.39) + specifier: ^10.4.20 + version: 10.4.20(postcss@8.4.45) fast-glob: specifier: ^3.3.2 version: 3.3.2 @@ -79,8 +85,8 @@ importers: specifier: ^3.1.4 version: 3.1.4 postcss: - specifier: ^8.4.39 - version: 8.4.39 + specifier: ^8.4.45 + version: 8.4.45 prettier: specifier: ^3.3.3 version: 3.3.3 @@ -97,8 +103,8 @@ importers: specifier: ^3.35.0 version: 3.35.0 tailwindcss: - specifier: ^3.4.6 - version: 3.4.6 + specifier: ^3.4.10 + version: 3.4.10 packages: @@ -106,24 +112,24 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@altv/types-client@16.2.3': - resolution: {integrity: sha512-qpJBm80+T313VhmyMy2AloqgYQSrAHxeIAfzFv1zuMhiQFo1OUIQoGcr5uxaon4WgTd+6khwvA35G5tuWKqpZQ==} + '@altv/types-client@16.2.6': + resolution: {integrity: sha512-iOO7BuKSYo0vAr3lhtscgikN7Tc20kw6wyplTQispEQuCqbWtAU0BjHmdD81bUX1BEjkSL9DqEAYp3OnjJZA9A==} peerDependencies: '@altv/types-shared': ^16.2.0 '@altv/types-worker': ^16.2.0 - '@altv/types-natives@16.2.0': - resolution: {integrity: sha512-kZ6Or14I9DLPPbSAEEDxS+Pt+Efv1lsJSCFeYemnmoQEJtVT/XbCjkwYUd9HHzbcxkyAB87DVn1toQrAjYCSxg==} + '@altv/types-natives@16.2.1': + resolution: {integrity: sha512-V3VrJrB7HP+rG/SDPZHaR1enASGuWqXtcDQixa+CxwFeFH7GxeqH0qhdD1F8SH3NsiKg5CGvVsUulHEO5KkzBA==} peerDependencies: '@altv/types-client': ^16.2.0 - '@altv/types-server@16.2.2': - resolution: {integrity: sha512-DvVPSdfRjkcfPW+Ug2DOfTuab6soUqP18LV9yY9du5ufu/9CGBHx20GZXhGLhDhe7OQ7Ua8TKj8Qt0JFUVYVgQ==} + '@altv/types-server@16.2.4': + resolution: {integrity: sha512-mfqO0UWMF6j1egQoKvqAdQkvipZCudTCqcnyd9UuJX+lkwlBfWZalni2UxVMdHSGv5eax53s2LVnvXw4cOMAhQ==} peerDependencies: '@altv/types-shared': ^16.2.0 - '@altv/types-shared@16.2.1': - resolution: {integrity: sha512-qQ6z58dqwmAV5UcDh5nWBaieOuhDaXNgzj63XJ3MJiR6z7ShI4onZszzEX9ALaLGdakPupWs+0pm/MJV7diZQQ==} + '@altv/types-shared@16.2.3': + resolution: {integrity: sha512-RcAc58S12SfqBV4WDD4kx2wupQYGbKlOOsnQikhq/A5x4yoUbrF8v3zvoKwaAdQVbZNAQynpcTxanv3y+v0nKg==} '@altv/types-webview@16.2.1': resolution: {integrity: sha512-JEk1+XR1ya+mYkKoF3/tLYfaHUKkL6A8gtckuuGuf4IPrKO80UVGYI+2LxvqMBKcF6aDGI3MOoD9DYV0DznWSQ==} @@ -141,17 +147,17 @@ packages: resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} engines: {node: '>=6.9.0'} - '@babel/parser@7.24.8': - resolution: {integrity: sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==} + '@babel/parser@7.25.6': + resolution: {integrity: sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/runtime@7.24.8': - resolution: {integrity: sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==} + '@babel/runtime@7.25.6': + resolution: {integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==} engines: {node: '>=6.9.0'} - '@babel/types@7.24.9': - resolution: {integrity: sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==} + '@babel/types@7.25.6': + resolution: {integrity: sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==} engines: {node: '>=6.9.0'} '@esbuild/aix-ppc64@0.21.5': @@ -292,9 +298,11 @@ packages: cpu: [x64] os: [win32] - '@hono/node-server@1.12.0': - resolution: {integrity: sha512-e6oHjNiErRxsZRZBmc2KucuvY3btlO/XPncIpP2X75bRdTilF9GLjm3NHvKKunpJbbJJj31/FoPTksTf8djAVw==} + '@hono/node-server@1.12.2': + resolution: {integrity: sha512-xjzhqhSWUE/OhN0g3KCNVzNsQMlFUAL+/8GgPUr3TKcU7cvgZVBGswFofJ8WwGEHTqobzze1lDpGJl9ZNckDhA==} engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} @@ -318,8 +326,8 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@mongodb-js/saslprep@1.1.8': - resolution: {integrity: sha512-qKwC/M/nNNaKUBMQ0nuzm47b7ZYWQHN3pcXq4IIcoSBc2hOIrflAxJduIvvqmhoz3gR2TacTAs8vlsCVPkiEdQ==} + '@mongodb-js/saslprep@1.1.9': + resolution: {integrity: sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==} '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -337,91 +345,94 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@rollup/rollup-android-arm-eabi@4.18.1': - resolution: {integrity: sha512-lncuC4aHicncmbORnx+dUaAgzee9cm/PbIqgWz1PpXuwc+sa1Ct83tnqUDy/GFKleLiN7ZIeytM6KJ4cAn1SxA==} + '@rollup/rollup-android-arm-eabi@4.21.2': + resolution: {integrity: sha512-fSuPrt0ZO8uXeS+xP3b+yYTCBUd05MoSp2N/MFOgjhhUhMmchXlpTQrTpI8T+YAwAQuK7MafsCOxW7VrPMrJcg==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.18.1': - resolution: {integrity: sha512-F/tkdw0WSs4ojqz5Ovrw5r9odqzFjb5LIgHdHZG65dFI1lWTWRVy32KDJLKRISHgJvqUeUhdIvy43fX41znyDg==} + '@rollup/rollup-android-arm64@4.21.2': + resolution: {integrity: sha512-xGU5ZQmPlsjQS6tzTTGwMsnKUtu0WVbl0hYpTPauvbRAnmIvpInhJtgjj3mcuJpEiuUw4v1s4BimkdfDWlh7gA==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.18.1': - resolution: {integrity: sha512-vk+ma8iC1ebje/ahpxpnrfVQJibTMyHdWpOGZ3JpQ7Mgn/3QNHmPq7YwjZbIE7km73dH5M1e6MRRsnEBW7v5CQ==} + '@rollup/rollup-darwin-arm64@4.21.2': + resolution: {integrity: sha512-99AhQ3/ZMxU7jw34Sq8brzXqWH/bMnf7ZVhvLk9QU2cOepbQSVTns6qoErJmSiAvU3InRqC2RRZ5ovh1KN0d0Q==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.18.1': - resolution: {integrity: sha512-IgpzXKauRe1Tafcej9STjSSuG0Ghu/xGYH+qG6JwsAUxXrnkvNHcq/NL6nz1+jzvWAnQkuAJ4uIwGB48K9OCGA==} + '@rollup/rollup-darwin-x64@4.21.2': + resolution: {integrity: sha512-ZbRaUvw2iN/y37x6dY50D8m2BnDbBjlnMPotDi/qITMJ4sIxNY33HArjikDyakhSv0+ybdUxhWxE6kTI4oX26w==} cpu: [x64] os: [darwin] - '@rollup/rollup-linux-arm-gnueabihf@4.18.1': - resolution: {integrity: sha512-P9bSiAUnSSM7EmyRK+e5wgpqai86QOSv8BwvkGjLwYuOpaeomiZWifEos517CwbG+aZl1T4clSE1YqqH2JRs+g==} + '@rollup/rollup-linux-arm-gnueabihf@4.21.2': + resolution: {integrity: sha512-ztRJJMiE8nnU1YFcdbd9BcH6bGWG1z+jP+IPW2oDUAPxPjo9dverIOyXz76m6IPA6udEL12reYeLojzW2cYL7w==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.18.1': - resolution: {integrity: sha512-5RnjpACoxtS+aWOI1dURKno11d7krfpGDEn19jI8BuWmSBbUC4ytIADfROM1FZrFhQPSoP+KEa3NlEScznBTyQ==} + '@rollup/rollup-linux-arm-musleabihf@4.21.2': + resolution: {integrity: sha512-flOcGHDZajGKYpLV0JNc0VFH361M7rnV1ee+NTeC/BQQ1/0pllYcFmxpagltANYt8FYf9+kL6RSk80Ziwyhr7w==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.18.1': - resolution: {integrity: sha512-8mwmGD668m8WaGbthrEYZ9CBmPug2QPGWxhJxh/vCgBjro5o96gL04WLlg5BA233OCWLqERy4YUzX3bJGXaJgQ==} + '@rollup/rollup-linux-arm64-gnu@4.21.2': + resolution: {integrity: sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.18.1': - resolution: {integrity: sha512-dJX9u4r4bqInMGOAQoGYdwDP8lQiisWb9et+T84l2WXk41yEej8v2iGKodmdKimT8cTAYt0jFb+UEBxnPkbXEQ==} + '@rollup/rollup-linux-arm64-musl@4.21.2': + resolution: {integrity: sha512-48pD/fJkTiHAZTnZwR0VzHrao70/4MlzJrq0ZsILjLW/Ab/1XlVUStYyGt7tdyIiVSlGZbnliqmult/QGA2O2w==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.18.1': - resolution: {integrity: sha512-V72cXdTl4EI0x6FNmho4D502sy7ed+LuVW6Ym8aI6DRQ9hQZdp5sj0a2usYOlqvFBNKQnLQGwmYnujo2HvjCxQ==} + '@rollup/rollup-linux-powerpc64le-gnu@4.21.2': + resolution: {integrity: sha512-cZdyuInj0ofc7mAQpKcPR2a2iu4YM4FQfuUzCVA2u4HI95lCwzjoPtdWjdpDKyHxI0UO82bLDoOaLfpZ/wviyQ==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.18.1': - resolution: {integrity: sha512-f+pJih7sxoKmbjghrM2RkWo2WHUW8UbfxIQiWo5yeCaCM0TveMEuAzKJte4QskBp1TIinpnRcxkquY+4WuY/tg==} + '@rollup/rollup-linux-riscv64-gnu@4.21.2': + resolution: {integrity: sha512-RL56JMT6NwQ0lXIQmMIWr1SW28z4E4pOhRRNqwWZeXpRlykRIlEpSWdsgNWJbYBEWD84eocjSGDu/XxbYeCmwg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.18.1': - resolution: {integrity: sha512-qb1hMMT3Fr/Qz1OKovCuUM11MUNLUuHeBC2DPPAWUYYUAOFWaxInaTwTQmc7Fl5La7DShTEpmYwgdt2hG+4TEg==} + '@rollup/rollup-linux-s390x-gnu@4.21.2': + resolution: {integrity: sha512-PMxkrWS9z38bCr3rWvDFVGD6sFeZJw4iQlhrup7ReGmfn7Oukrr/zweLhYX6v2/8J6Cep9IEA/SmjXjCmSbrMQ==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.18.1': - resolution: {integrity: sha512-7O5u/p6oKUFYjRbZkL2FLbwsyoJAjyeXHCU3O4ndvzg2OFO2GinFPSJFGbiwFDaCFc+k7gs9CF243PwdPQFh5g==} + '@rollup/rollup-linux-x64-gnu@4.21.2': + resolution: {integrity: sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.18.1': - resolution: {integrity: sha512-pDLkYITdYrH/9Cv/Vlj8HppDuLMDUBmgsM0+N+xLtFd18aXgM9Nyqupb/Uw+HeidhfYg2lD6CXvz6CjoVOaKjQ==} + '@rollup/rollup-linux-x64-musl@4.21.2': + resolution: {integrity: sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.18.1': - resolution: {integrity: sha512-W2ZNI323O/8pJdBGil1oCauuCzmVd9lDmWBBqxYZcOqWD6aWqJtVBQ1dFrF4dYpZPks6F+xCZHfzG5hYlSHZ6g==} + '@rollup/rollup-win32-arm64-msvc@4.21.2': + resolution: {integrity: sha512-9rRero0E7qTeYf6+rFh3AErTNU1VCQg2mn7CQcI44vNUWM9Ze7MSRS/9RFuSsox+vstRt97+x3sOhEey024FRQ==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.18.1': - resolution: {integrity: sha512-ELfEX1/+eGZYMaCIbK4jqLxO1gyTSOIlZr6pbC4SRYFaSIDVKOnZNMdoZ+ON0mrFDp4+H5MhwNC1H/AhE3zQLg==} + '@rollup/rollup-win32-ia32-msvc@4.21.2': + resolution: {integrity: sha512-5rA4vjlqgrpbFVVHX3qkrCo/fZTj1q0Xxpg+Z7yIo3J2AilW7t2+n6Q8Jrx+4MrYpAnjttTYF8rr7bP46BPzRw==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.18.1': - resolution: {integrity: sha512-yjk2MAkQmoaPYCSu35RLJ62+dz358nE83VfTePJRp8CG7aMg25mEJYpXFiD+NcevhX8LxD5OP5tktPXnXN7GDw==} + '@rollup/rollup-win32-x64-msvc@4.21.2': + resolution: {integrity: sha512-6UUxd0+SKomjdzuAcp+HAmxw1FlGBnl1v2yEPSabtx4lBfdXHDVsW7+lQkgz9cNFJGY3AWR7+V8P5BqkD9L9nA==} cpu: [x64] os: [win32] '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - '@types/node@20.14.11': - resolution: {integrity: sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==} + '@types/node@20.16.4': + resolution: {integrity: sha512-ioyQ1zK9aGEomJ45zz8S8IdzElyxhvP1RVWnPrXDf6wFaUb+kk1tEcVVJkF7RPGM0VWI7cp5U57oCPIn5iN1qg==} + + '@types/sjcl@1.0.34': + resolution: {integrity: sha512-bQHEeK5DTQRunIfQeUMgtpPsNNCcZyQ9MJuAfW1I7iN0LDunTc78Fu17STbLMd7KiEY/g2zHVApippa70h6HoQ==} '@types/webidl-conversions@7.0.3': resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} @@ -429,58 +440,61 @@ packages: '@types/whatwg-url@11.0.5': resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==} - '@vitejs/plugin-vue@5.0.5': - resolution: {integrity: sha512-LOjm7XeIimLBZyzinBQ6OSm3UBCNVCpLkxGC0oWmm2YPzVZoxMsdvNVimLTBzpAnR9hl/yn1SHGuRfe6/Td9rQ==} + '@vitejs/plugin-vue@5.1.3': + resolution: {integrity: sha512-3xbWsKEKXYlmX82aOHufFQVnkbMC/v8fLpWwh6hWOUrK5fbbtBh9Q/WWse27BFgSy2/e2c0fz5Scgya9h2GLhw==} engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: vite: ^5.0.0 vue: ^3.2.25 - '@volar/language-core@2.4.0-alpha.16': - resolution: {integrity: sha512-oOTnIZlx0P/idFwVw+W0NbzKDtZAQMzXSdIFfTePCKcXlb4Ys12GaGkx8NF9dsvPYV3nbv3ZsSxnkZWBmNKd7A==} + '@volar/language-core@2.4.2': + resolution: {integrity: sha512-sONt5RLvLL1SlBdhyUSthZzuKePbJ7DwFFB9zT0eyWpDl+v7GXGh/RkPxxWaR22bIhYtTzp4Ka1MWatl/53Riw==} + + '@volar/source-map@2.4.2': + resolution: {integrity: sha512-qiGfGgeZ5DEarPX3S+HcFktFCjfDrFPCXKeXNbrlB7v8cvtPRm8YVwoXOdGG1NhaL5rMlv5BZPVQyu4EdWWIvA==} - '@volar/source-map@2.4.0-alpha.16': - resolution: {integrity: sha512-sL9vNG7iR2hiKZor7UkD5Sufu3QCia4cbp2gX/nGRNSdaPbhOpdAoavwlBm0PrVkpiA19NZuavZoobD8krviFg==} + '@volar/typescript@2.4.2': + resolution: {integrity: sha512-m2uZduhaHO1SZuagi30OsjI/X1gwkaEAC+9wT/nCNAtJ5FqXEkKvUncHmffG7ESDZPlFFUBK4vJ0D9Hfr+f2EA==} - '@volar/typescript@2.4.0-alpha.16': - resolution: {integrity: sha512-WCx7z5O81McCQp2cC0c8081y+MgTiAR2WAiJjVL4tr4Qh4GgqK0lgn3CqAjcKizaK1R5y3wfrUqgIYr+QeFYcw==} + '@vue/compiler-core@3.5.1': + resolution: {integrity: sha512-WdjF+NSgFYdWttHevHw5uaJFtKPalhmxhlu2uREj8cLP0uyKKIR60/JvSZNTp0x+NSd63iTiORQTx3+tt55NWQ==} - '@vue/compiler-core@3.4.32': - resolution: {integrity: sha512-8tCVWkkLe/QCWIsrIvExUGnhYCAOroUs5dzhSoKL5w4MJS8uIYiou+pOPSVIOALOQ80B0jBs+Ri+kd5+MBnCDw==} + '@vue/compiler-dom@3.5.1': + resolution: {integrity: sha512-Ao23fB1lINo18HLCbJVApvzd9OQe8MgmQSgyY5+umbWj2w92w9KykVmJ4Iv2US5nak3ixc2B+7Km7JTNhQ8kSQ==} - '@vue/compiler-dom@3.4.32': - resolution: {integrity: sha512-PbSgt9KuYo4fyb90dynuPc0XFTfFPs3sCTbPLOLlo+PrUESW1gn/NjSsUvhR+mI2AmmEzexwYMxbHDldxSOr2A==} + '@vue/compiler-sfc@3.5.1': + resolution: {integrity: sha512-DFizMNH8eDglLhlfwJ0+ciBsztaYe3fY/zcZjrqL1ljXvUw/UpC84M1d7HpBTCW68SNqZyIxrs1XWmf+73Y65w==} - '@vue/compiler-sfc@3.4.32': - resolution: {integrity: sha512-STy9im/WHfaguJnfKjjVpMHukxHUrOKjm2vVCxiojQJyo3Sb6Os8SMXBr/MI+ekpstEGkDONfqAQoSbZhspLYw==} + '@vue/compiler-ssr@3.5.1': + resolution: {integrity: sha512-C1hpSHQgRM8bg+5XWWD7CkFaVpSn9wZHCLRd10AmxqrH17d4EMP6+XcZpwBOM7H1jeStU5naEapZZWX0kso1tQ==} - '@vue/compiler-ssr@3.4.32': - resolution: {integrity: sha512-nyu/txTecF6DrxLrpLcI34xutrvZPtHPBj9yRoPxstIquxeeyywXpYZrQMsIeDfBhlw1abJb9CbbyZvDw2kjdg==} + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} - '@vue/language-core@2.0.26': - resolution: {integrity: sha512-/lt6SfQ3O1yDAhPsnLv9iSUgXd1dMHqUm/t3RctfqjuwQf1LnftZ414X3UBn6aXT4MiwXWtbNJ4Z0NZWwDWgJQ==} + '@vue/language-core@2.1.4': + resolution: {integrity: sha512-i8pfAgNjTNjabBX1xRsuV6aRw2E8bdQXwd5H8m3cUkTVJju3QN5nfdoXET0uK+yXsuloNJPzo6PXFujRRPNmMA==} peerDependencies: typescript: '*' peerDependenciesMeta: typescript: optional: true - '@vue/reactivity@3.4.32': - resolution: {integrity: sha512-1P7QvghAzhSIWmiNmh4MNkLVjr2QTNDcFv2sKmytEWhR6t7BZzNicgm5ENER4uU++wbWxgRh/pSEYgdI3MDcvg==} + '@vue/reactivity@3.5.1': + resolution: {integrity: sha512-aFE1nMDfbG7V+U5vdOk/NXxH/WX78XuAfX59vWmCM7Ao4lieoc83RkzOAWun61sQXlzNZ4IgROovFBHg+Iz1+Q==} - '@vue/runtime-core@3.4.32': - resolution: {integrity: sha512-FxT2dTHUs1Hki8Ui/B1Hu339mx4H5kRJooqrNM32tGUHBPStJxwMzLIRbeGO/B1NMplU4Pg9fwOqrJtrOzkdfA==} + '@vue/runtime-core@3.5.1': + resolution: {integrity: sha512-Ce92CCholNRHR3ZtzpRp/7CDGIPFxQ7ElXt9iH91ilK5eOrUv3Z582NWJesuM3aYX71BujVG5/4ypUxigGNxjA==} - '@vue/runtime-dom@3.4.32': - resolution: {integrity: sha512-Xz9G+ZViRyPFQtRBCPFkhMzKn454ihCPMKUiacNaUhuTIXvyfkAq8l89IZ/kegFVyw/7KkJGRGqYdEZrf27Xsg==} + '@vue/runtime-dom@3.5.1': + resolution: {integrity: sha512-B/fUJfBLp5PwE0EWNfBYnA4JUea8Yufb3wN8fN0/HzaqBdkiRHh4sFHOjWqIY8GS75gj//8VqeEqhcU6yUjIkA==} - '@vue/server-renderer@3.4.32': - resolution: {integrity: sha512-3c4rd0522Ao8hKjzgmUAbcjv2mBnvnw0Ld2f8HOMCuWJZjYie/p8cpIoYJbeP0VV2JYmrJJMwGQDO5RH4iQ30A==} + '@vue/server-renderer@3.5.1': + resolution: {integrity: sha512-C5V/fjQTitgVaRNH5wCoHynaWysjZ+VH68drNsAvQYg4ArHsZUQNz0nHoEWRj41nzqkVn2RUlnWaEOTl2o1Ppg==} peerDependencies: - vue: 3.4.32 + vue: 3.5.1 - '@vue/shared@3.4.32': - resolution: {integrity: sha512-ep4mF1IVnX/pYaNwxwOpJHyBtOMKWoKZMbnUyd+z0udqIxLUh7YCCd/JfDna8aUrmnG9SFORyIq2HzEATRrQsg==} + '@vue/shared@3.5.1': + resolution: {integrity: sha512-NdcTRoO4KuW2RSFgpE2c+E/R/ZHaRzWPxAGxhmxZaaqLh6nYCXx7lc9a88ioqOCxCaV2SFJmujkxbUScW7dNsQ==} aggregate-error@5.0.0: resolution: {integrity: sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==} @@ -516,8 +530,8 @@ packages: arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - autoprefixer@10.4.19: - resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==} + autoprefixer@10.4.20: + resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: @@ -543,8 +557,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.23.2: - resolution: {integrity: sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==} + browserslist@4.23.3: + resolution: {integrity: sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -556,8 +570,8 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - caniuse-lite@1.0.30001642: - resolution: {integrity: sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==} + caniuse-lite@1.0.30001655: + resolution: {integrity: sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -616,8 +630,8 @@ packages: de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} - debug@4.3.5: - resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} + debug@4.3.6: + resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -641,8 +655,8 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.4.829: - resolution: {integrity: sha512-5qp1N2POAfW0u1qGAxXEtz6P7bO1m6gpZr5hdf5ve6lxpLM7MpiM4jIPz7xcrNlClQMafbyUDDWjlIQZ1Mw0Rw==} + electron-to-chromium@1.5.13: + resolution: {integrity: sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -659,8 +673,8 @@ packages: engines: {node: '>=12'} hasBin: true - escalade@3.1.2: - resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} escape-string-regexp@5.0.0: @@ -696,8 +710,8 @@ packages: resolution: {integrity: sha512-MdYSsbdCaIRjzo5edthZtWmEZVMfr1qrtYZUHIdO3swCE+CoZA8S5l0s4jDsYlTa9ZiXv0pTgpzE7s4N8NeUOA==} engines: {node: '>=18'} - foreground-child@3.2.1: - resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==} + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} fraction.js@4.3.7: @@ -758,8 +772,8 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true - hono@4.5.0: - resolution: {integrity: sha512-ZbezypZfn4odyApjCCv+Fw5OgweBqRLA/EsMyc4FUknFvBJcBIKhHy4sqmD1rWpBc/3wUlaQ6tqOPjk36R1ckg==} + hono@4.5.11: + resolution: {integrity: sha512-62FcjLPtjAFwISVBUshryl+vbHOjg8rE4uIK/dxyR8GpLztunZpwFmfEvmJCUI7xoGh/Sr3CGCDPCmYxVw7wUQ==} engines: {node: '>=16.0.0'} human-signals@3.0.1: @@ -792,8 +806,8 @@ packages: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} - is-core-module@2.15.0: - resolution: {integrity: sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==} + is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} engines: {node: '>= 0.4'} is-extglob@2.1.1: @@ -843,8 +857,8 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - magic-string@0.30.10: - resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} + magic-string@0.30.11: + resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} memory-pager@1.5.0: resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} @@ -856,8 +870,8 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - micromatch@4.0.7: - resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} mimic-fn@4.0.0: @@ -925,6 +939,10 @@ packages: node-addon-api@1.7.2: resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} + node-cron@3.0.3: + resolution: {integrity: sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==} + engines: {node: '>=6.0.0'} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -934,8 +952,8 @@ packages: encoding: optional: true - node-releases@2.0.17: - resolution: {integrity: sha512-Ww6ZlOiEQfPfXM45v17oabk77Z7mg5bOt7AjDyzy7RjK9OrLrLC8dyZQoAPEOtFX9SaNf1Tdvr5gRJWdTJj7GA==} + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} nodemon@3.1.4: resolution: {integrity: sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==} @@ -994,8 +1012,8 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - picocolors@1.0.1: - resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + picocolors@1.1.0: + resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} @@ -1037,21 +1055,21 @@ packages: ts-node: optional: true - postcss-nested@6.0.1: - resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} engines: {node: '>=12.0'} peerDependencies: postcss: ^8.2.14 - postcss-selector-parser@6.1.1: - resolution: {integrity: sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==} + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.4.39: - resolution: {integrity: sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==} + postcss@8.4.45: + resolution: {integrity: sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==} engines: {node: ^10 || ^12 || >=14} prettier-plugin-tailwindcss@0.5.14: @@ -1163,8 +1181,8 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.18.1: - resolution: {integrity: sha512-Elx2UT8lzxxOXMpy5HWQGZqkrQOtrVDDa/bm9l10+U4rQnVzbL/LgZ4NOM1MPIDyHk69W4InuYDF5dzRh4Kw1A==} + rollup@4.21.2: + resolution: {integrity: sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -1265,8 +1283,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - tailwindcss@3.4.6: - resolution: {integrity: sha512-1uRHzPB+Vzu57ocybfZ4jh5Q3SdlH7XW23J5sQoM9LhE9eIOlzxer/3XPSsycvih3rboRsvt0QCmzSrqyOYUIA==} + tailwindcss@3.4.10: + resolution: {integrity: sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==} engines: {node: '>=14.0.0'} hasBin: true @@ -1307,19 +1325,19 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - tslib@2.6.3: - resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} - typescript@5.5.3: - resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==} + typescript@5.5.4: + resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} engines: {node: '>=14.17'} hasBin: true undefsafe@2.0.5: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} update-browserslist-db@1.1.0: resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} @@ -1330,8 +1348,12 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - vite@5.3.4: - resolution: {integrity: sha512-Cw+7zL3ZG9/NZBB8C+8QbQZmR54GwqIz+WMI4b3JgdYJvX+ny9AjJXqkGQlDXSXRP9rP0B4tbciRMOVEKulVOA==} + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + vite@5.4.3: + resolution: {integrity: sha512-IH+nl64eq9lJjFqU+/yrRnrHPVTlgy42/+IzbOdaFDVlyLgI/wDlf+FCobXLX1cT0X5+7LMyH1mIy2xJdLfo8Q==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -1339,6 +1361,7 @@ packages: less: '*' lightningcss: ^1.21.0 sass: '*' + sass-embedded: '*' stylus: '*' sugarss: '*' terser: ^5.4.0 @@ -1351,6 +1374,8 @@ packages: optional: true sass: optional: true + sass-embedded: + optional: true stylus: optional: true sugarss: @@ -1361,17 +1386,14 @@ packages: vscode-uri@3.0.8: resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} - vue-template-compiler@2.7.16: - resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} - - vue-tsc@2.0.26: - resolution: {integrity: sha512-tOhuwy2bIXbMhz82ef37qeiaQHMXKQkD6mOF6CCPl3/uYtST3l6fdNyfMxipudrQTxTfXVPlgJdMENBFfC1CfQ==} + vue-tsc@2.1.4: + resolution: {integrity: sha512-XTzMXQcsixAvNbpou/9qngEsZawaiJRZH3Ja+lfgRfv2A1TJv9vnZ/Kyv7XxPqv/TaZVFSnjGpM87VbWIg6yQg==} hasBin: true peerDependencies: typescript: '>=5.0.0' - vue@3.4.32: - resolution: {integrity: sha512-9mCGIAi/CAq7GtaLLLp2J92pEic+HArstG+pq6F+H7+/jB9a0Z7576n4Bh4k79/50L1cKMIhZC3MC0iGpl+1IA==} + vue@3.5.1: + resolution: {integrity: sha512-k4UNnbPOEskodSxMtv+B9GljdB0C9ubZDOmW6vnXVGIfMqmEsY2+ohasjGguhGkMkrcP/oOrbH0dSD41x5JQFw==} peerDependencies: typescript: '*' peerDependenciesMeta: @@ -1424,8 +1446,8 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} - yaml@2.4.5: - resolution: {integrity: sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==} + yaml@2.5.1: + resolution: {integrity: sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==} engines: {node: '>= 14'} hasBin: true @@ -1441,40 +1463,40 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@altv/types-client@16.2.3(@altv/types-shared@16.2.1)(@altv/types-worker@16.2.0)': + '@altv/types-client@16.2.6(@altv/types-shared@16.2.3)(@altv/types-worker@16.2.0)': dependencies: - '@altv/types-shared': 16.2.1 - '@altv/types-worker': 16.2.0(@altv/types-client@16.2.3) + '@altv/types-shared': 16.2.3 + '@altv/types-worker': 16.2.0(@altv/types-client@16.2.6) - '@altv/types-natives@16.2.0(@altv/types-client@16.2.3(@altv/types-shared@16.2.1)(@altv/types-worker@16.2.0))': + '@altv/types-natives@16.2.1(@altv/types-client@16.2.6(@altv/types-shared@16.2.3)(@altv/types-worker@16.2.0))': dependencies: - '@altv/types-client': 16.2.3(@altv/types-shared@16.2.1)(@altv/types-worker@16.2.0) + '@altv/types-client': 16.2.6(@altv/types-shared@16.2.3)(@altv/types-worker@16.2.0) - '@altv/types-server@16.2.2(@altv/types-shared@16.2.1)': + '@altv/types-server@16.2.4(@altv/types-shared@16.2.3)': dependencies: - '@altv/types-shared': 16.2.1 + '@altv/types-shared': 16.2.3 - '@altv/types-shared@16.2.1': {} + '@altv/types-shared@16.2.3': {} '@altv/types-webview@16.2.1': {} - '@altv/types-worker@16.2.0(@altv/types-client@16.2.3)': + '@altv/types-worker@16.2.0(@altv/types-client@16.2.6)': dependencies: - '@altv/types-client': 16.2.3(@altv/types-shared@16.2.1)(@altv/types-worker@16.2.0) + '@altv/types-client': 16.2.6(@altv/types-shared@16.2.3)(@altv/types-worker@16.2.0) '@babel/helper-string-parser@7.24.8': {} '@babel/helper-validator-identifier@7.24.7': {} - '@babel/parser@7.24.8': + '@babel/parser@7.25.6': dependencies: - '@babel/types': 7.24.9 + '@babel/types': 7.25.6 - '@babel/runtime@7.24.8': + '@babel/runtime@7.25.6': dependencies: regenerator-runtime: 0.14.1 - '@babel/types@7.24.9': + '@babel/types@7.25.6': dependencies: '@babel/helper-string-parser': 7.24.8 '@babel/helper-validator-identifier': 7.24.7 @@ -1549,7 +1571,9 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true - '@hono/node-server@1.12.0': {} + '@hono/node-server@1.12.2(hono@4.5.11)': + dependencies: + hono: 4.5.11 '@isaacs/cliui@8.0.2': dependencies: @@ -1577,7 +1601,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@mongodb-js/saslprep@1.1.8': + '@mongodb-js/saslprep@1.1.9': dependencies: sparse-bitfield: 3.0.3 @@ -1596,59 +1620,61 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@rollup/rollup-android-arm-eabi@4.18.1': + '@rollup/rollup-android-arm-eabi@4.21.2': optional: true - '@rollup/rollup-android-arm64@4.18.1': + '@rollup/rollup-android-arm64@4.21.2': optional: true - '@rollup/rollup-darwin-arm64@4.18.1': + '@rollup/rollup-darwin-arm64@4.21.2': optional: true - '@rollup/rollup-darwin-x64@4.18.1': + '@rollup/rollup-darwin-x64@4.21.2': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.18.1': + '@rollup/rollup-linux-arm-gnueabihf@4.21.2': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.18.1': + '@rollup/rollup-linux-arm-musleabihf@4.21.2': optional: true - '@rollup/rollup-linux-arm64-gnu@4.18.1': + '@rollup/rollup-linux-arm64-gnu@4.21.2': optional: true - '@rollup/rollup-linux-arm64-musl@4.18.1': + '@rollup/rollup-linux-arm64-musl@4.21.2': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.18.1': + '@rollup/rollup-linux-powerpc64le-gnu@4.21.2': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.18.1': + '@rollup/rollup-linux-riscv64-gnu@4.21.2': optional: true - '@rollup/rollup-linux-s390x-gnu@4.18.1': + '@rollup/rollup-linux-s390x-gnu@4.21.2': optional: true - '@rollup/rollup-linux-x64-gnu@4.18.1': + '@rollup/rollup-linux-x64-gnu@4.21.2': optional: true - '@rollup/rollup-linux-x64-musl@4.18.1': + '@rollup/rollup-linux-x64-musl@4.21.2': optional: true - '@rollup/rollup-win32-arm64-msvc@4.18.1': + '@rollup/rollup-win32-arm64-msvc@4.21.2': optional: true - '@rollup/rollup-win32-ia32-msvc@4.18.1': + '@rollup/rollup-win32-ia32-msvc@4.21.2': optional: true - '@rollup/rollup-win32-x64-msvc@4.18.1': + '@rollup/rollup-win32-x64-msvc@4.21.2': optional: true '@types/estree@1.0.5': {} - '@types/node@20.14.11': + '@types/node@20.16.4': dependencies: - undici-types: 5.26.5 + undici-types: 6.19.8 + + '@types/sjcl@1.0.34': {} '@types/webidl-conversions@7.0.3': {} @@ -1656,89 +1682,94 @@ snapshots: dependencies: '@types/webidl-conversions': 7.0.3 - '@vitejs/plugin-vue@5.0.5(vite@5.3.4(@types/node@20.14.11))(vue@3.4.32(typescript@5.5.3))': + '@vitejs/plugin-vue@5.1.3(vite@5.4.3(@types/node@20.16.4))(vue@3.5.1(typescript@5.5.4))': dependencies: - vite: 5.3.4(@types/node@20.14.11) - vue: 3.4.32(typescript@5.5.3) + vite: 5.4.3(@types/node@20.16.4) + vue: 3.5.1(typescript@5.5.4) - '@volar/language-core@2.4.0-alpha.16': + '@volar/language-core@2.4.2': dependencies: - '@volar/source-map': 2.4.0-alpha.16 + '@volar/source-map': 2.4.2 - '@volar/source-map@2.4.0-alpha.16': {} + '@volar/source-map@2.4.2': {} - '@volar/typescript@2.4.0-alpha.16': + '@volar/typescript@2.4.2': dependencies: - '@volar/language-core': 2.4.0-alpha.16 + '@volar/language-core': 2.4.2 path-browserify: 1.0.1 vscode-uri: 3.0.8 - '@vue/compiler-core@3.4.32': + '@vue/compiler-core@3.5.1': dependencies: - '@babel/parser': 7.24.8 - '@vue/shared': 3.4.32 + '@babel/parser': 7.25.6 + '@vue/shared': 3.5.1 entities: 4.5.0 estree-walker: 2.0.2 source-map-js: 1.2.0 - '@vue/compiler-dom@3.4.32': + '@vue/compiler-dom@3.5.1': dependencies: - '@vue/compiler-core': 3.4.32 - '@vue/shared': 3.4.32 + '@vue/compiler-core': 3.5.1 + '@vue/shared': 3.5.1 - '@vue/compiler-sfc@3.4.32': + '@vue/compiler-sfc@3.5.1': dependencies: - '@babel/parser': 7.24.8 - '@vue/compiler-core': 3.4.32 - '@vue/compiler-dom': 3.4.32 - '@vue/compiler-ssr': 3.4.32 - '@vue/shared': 3.4.32 + '@babel/parser': 7.25.6 + '@vue/compiler-core': 3.5.1 + '@vue/compiler-dom': 3.5.1 + '@vue/compiler-ssr': 3.5.1 + '@vue/shared': 3.5.1 estree-walker: 2.0.2 - magic-string: 0.30.10 - postcss: 8.4.39 + magic-string: 0.30.11 + postcss: 8.4.45 source-map-js: 1.2.0 - '@vue/compiler-ssr@3.4.32': + '@vue/compiler-ssr@3.5.1': dependencies: - '@vue/compiler-dom': 3.4.32 - '@vue/shared': 3.4.32 + '@vue/compiler-dom': 3.5.1 + '@vue/shared': 3.5.1 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 - '@vue/language-core@2.0.26(typescript@5.5.3)': + '@vue/language-core@2.1.4(typescript@5.5.4)': dependencies: - '@volar/language-core': 2.4.0-alpha.16 - '@vue/compiler-dom': 3.4.32 - '@vue/shared': 3.4.32 + '@volar/language-core': 2.4.2 + '@vue/compiler-dom': 3.5.1 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.1 computeds: 0.0.1 minimatch: 9.0.5 muggle-string: 0.4.1 path-browserify: 1.0.1 - vue-template-compiler: 2.7.16 optionalDependencies: - typescript: 5.5.3 + typescript: 5.5.4 - '@vue/reactivity@3.4.32': + '@vue/reactivity@3.5.1': dependencies: - '@vue/shared': 3.4.32 + '@vue/shared': 3.5.1 - '@vue/runtime-core@3.4.32': + '@vue/runtime-core@3.5.1': dependencies: - '@vue/reactivity': 3.4.32 - '@vue/shared': 3.4.32 + '@vue/reactivity': 3.5.1 + '@vue/shared': 3.5.1 - '@vue/runtime-dom@3.4.32': + '@vue/runtime-dom@3.5.1': dependencies: - '@vue/reactivity': 3.4.32 - '@vue/runtime-core': 3.4.32 - '@vue/shared': 3.4.32 + '@vue/reactivity': 3.5.1 + '@vue/runtime-core': 3.5.1 + '@vue/shared': 3.5.1 csstype: 3.1.3 - '@vue/server-renderer@3.4.32(vue@3.4.32(typescript@5.5.3))': + '@vue/server-renderer@3.5.1(vue@3.5.1(typescript@5.5.4))': dependencies: - '@vue/compiler-ssr': 3.4.32 - '@vue/shared': 3.4.32 - vue: 3.4.32(typescript@5.5.3) + '@vue/compiler-ssr': 3.5.1 + '@vue/shared': 3.5.1 + vue: 3.5.1(typescript@5.5.4) - '@vue/shared@3.4.32': {} + '@vue/shared@3.5.1': {} aggregate-error@5.0.0: dependencies: @@ -1773,14 +1804,14 @@ snapshots: arg@5.0.2: {} - autoprefixer@10.4.19(postcss@8.4.39): + autoprefixer@10.4.20(postcss@8.4.45): dependencies: - browserslist: 4.23.2 - caniuse-lite: 1.0.30001642 + browserslist: 4.23.3 + caniuse-lite: 1.0.30001655 fraction.js: 4.3.7 normalize-range: 0.1.2 - picocolors: 1.0.1 - postcss: 8.4.39 + picocolors: 1.1.0 + postcss: 8.4.45 postcss-value-parser: 4.2.0 balanced-match@1.0.2: {} @@ -1805,18 +1836,18 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.23.2: + browserslist@4.23.3: dependencies: - caniuse-lite: 1.0.30001642 - electron-to-chromium: 1.4.829 - node-releases: 2.0.17 - update-browserslist-db: 1.1.0(browserslist@4.23.2) + caniuse-lite: 1.0.30001655 + electron-to-chromium: 1.5.13 + node-releases: 2.0.18 + update-browserslist-db: 1.1.0(browserslist@4.23.3) bson@6.8.0: {} camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001642: {} + caniuse-lite@1.0.30001655: {} chalk@4.1.2: dependencies: @@ -1881,11 +1912,11 @@ snapshots: date-fns@2.30.0: dependencies: - '@babel/runtime': 7.24.8 + '@babel/runtime': 7.25.6 de-indent@1.0.2: {} - debug@4.3.5(supports-color@5.5.0): + debug@4.3.6(supports-color@5.5.0): dependencies: ms: 2.1.2 optionalDependencies: @@ -1910,7 +1941,7 @@ snapshots: eastasianwidth@0.2.0: {} - electron-to-chromium@1.4.829: {} + electron-to-chromium@1.5.13: {} emoji-regex@8.0.0: {} @@ -1944,7 +1975,7 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 - escalade@3.1.2: {} + escalade@3.2.0: {} escape-string-regexp@5.0.0: {} @@ -1980,7 +2011,7 @@ snapshots: '@nodelib/fs.walk': 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 - micromatch: 4.0.7 + micromatch: 4.0.8 fastq@1.17.1: dependencies: @@ -2002,7 +2033,7 @@ snapshots: ps-list: 8.1.1 taskkill: 5.0.0 - foreground-child@3.2.1: + foreground-child@3.3.0: dependencies: cross-spawn: 7.0.3 signal-exit: 4.1.0 @@ -2032,7 +2063,7 @@ snapshots: glob@10.4.5: dependencies: - foreground-child: 3.2.1 + foreground-child: 3.3.0 jackspeak: 3.4.3 minimatch: 9.0.5 minipass: 7.1.2 @@ -2058,7 +2089,7 @@ snapshots: he@1.2.0: {} - hono@4.5.0: {} + hono@4.5.11: {} human-signals@3.0.1: {} @@ -2081,7 +2112,7 @@ snapshots: dependencies: binary-extensions: 2.3.0 - is-core-module@2.15.0: + is-core-module@2.15.1: dependencies: hasown: 2.0.2 @@ -2117,7 +2148,7 @@ snapshots: lru-cache@10.4.3: {} - magic-string@0.30.10: + magic-string@0.30.11: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -2127,7 +2158,7 @@ snapshots: merge2@1.4.1: {} - micromatch@4.0.7: + micromatch@4.0.8: dependencies: braces: 3.0.3 picomatch: 2.3.1 @@ -2153,7 +2184,7 @@ snapshots: mongodb@6.8.0: dependencies: - '@mongodb-js/saslprep': 1.1.8 + '@mongodb-js/saslprep': 1.1.9 bson: 6.8.0 mongodb-connection-string-url: 3.0.1 @@ -2172,16 +2203,20 @@ snapshots: node-addon-api@1.7.2: optional: true + node-cron@3.0.3: + dependencies: + uuid: 8.3.2 + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 - node-releases@2.0.17: {} + node-releases@2.0.18: {} nodemon@3.1.4: dependencies: chokidar: 3.6.0 - debug: 4.3.5(supports-color@5.5.0) + debug: 4.3.6(supports-color@5.5.0) ignore-by-default: 1.0.1 minimatch: 3.1.2 pstree.remy: 1.1.8 @@ -2228,7 +2263,7 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - picocolors@1.0.1: {} + picocolors@1.1.0: {} picomatch@2.3.1: {} @@ -2240,41 +2275,41 @@ snapshots: pirates@4.0.6: {} - postcss-import@15.1.0(postcss@8.4.39): + postcss-import@15.1.0(postcss@8.4.45): dependencies: - postcss: 8.4.39 + postcss: 8.4.45 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.8 - postcss-js@4.0.1(postcss@8.4.39): + postcss-js@4.0.1(postcss@8.4.45): dependencies: camelcase-css: 2.0.1 - postcss: 8.4.39 + postcss: 8.4.45 - postcss-load-config@4.0.2(postcss@8.4.39): + postcss-load-config@4.0.2(postcss@8.4.45): dependencies: lilconfig: 3.1.2 - yaml: 2.4.5 + yaml: 2.5.1 optionalDependencies: - postcss: 8.4.39 + postcss: 8.4.45 - postcss-nested@6.0.1(postcss@8.4.39): + postcss-nested@6.2.0(postcss@8.4.45): dependencies: - postcss: 8.4.39 - postcss-selector-parser: 6.1.1 + postcss: 8.4.45 + postcss-selector-parser: 6.1.2 - postcss-selector-parser@6.1.1: + postcss-selector-parser@6.1.2: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 postcss-value-parser@4.2.0: {} - postcss@8.4.39: + postcss@8.4.45: dependencies: nanoid: 3.3.7 - picocolors: 1.0.1 + picocolors: 1.1.0 source-map-js: 1.2.0 prettier-plugin-tailwindcss@0.5.14(prettier@3.3.3): @@ -2319,7 +2354,7 @@ snapshots: resolve@1.22.8: dependencies: - is-core-module: 2.15.0 + is-core-module: 2.15.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -2327,26 +2362,26 @@ snapshots: reusify@1.0.4: {} - rollup@4.18.1: + rollup@4.21.2: dependencies: '@types/estree': 1.0.5 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.18.1 - '@rollup/rollup-android-arm64': 4.18.1 - '@rollup/rollup-darwin-arm64': 4.18.1 - '@rollup/rollup-darwin-x64': 4.18.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.18.1 - '@rollup/rollup-linux-arm-musleabihf': 4.18.1 - '@rollup/rollup-linux-arm64-gnu': 4.18.1 - '@rollup/rollup-linux-arm64-musl': 4.18.1 - '@rollup/rollup-linux-powerpc64le-gnu': 4.18.1 - '@rollup/rollup-linux-riscv64-gnu': 4.18.1 - '@rollup/rollup-linux-s390x-gnu': 4.18.1 - '@rollup/rollup-linux-x64-gnu': 4.18.1 - '@rollup/rollup-linux-x64-musl': 4.18.1 - '@rollup/rollup-win32-arm64-msvc': 4.18.1 - '@rollup/rollup-win32-ia32-msvc': 4.18.1 - '@rollup/rollup-win32-x64-msvc': 4.18.1 + '@rollup/rollup-android-arm-eabi': 4.21.2 + '@rollup/rollup-android-arm64': 4.21.2 + '@rollup/rollup-darwin-arm64': 4.21.2 + '@rollup/rollup-darwin-x64': 4.21.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.21.2 + '@rollup/rollup-linux-arm-musleabihf': 4.21.2 + '@rollup/rollup-linux-arm64-gnu': 4.21.2 + '@rollup/rollup-linux-arm64-musl': 4.21.2 + '@rollup/rollup-linux-powerpc64le-gnu': 4.21.2 + '@rollup/rollup-linux-riscv64-gnu': 4.21.2 + '@rollup/rollup-linux-s390x-gnu': 4.21.2 + '@rollup/rollup-linux-x64-gnu': 4.21.2 + '@rollup/rollup-linux-x64-musl': 4.21.2 + '@rollup/rollup-win32-arm64-msvc': 4.21.2 + '@rollup/rollup-win32-ia32-msvc': 4.21.2 + '@rollup/rollup-win32-x64-msvc': 4.21.2 fsevents: 2.3.3 run-parallel@1.2.0: @@ -2355,7 +2390,7 @@ snapshots: rxjs@7.8.1: dependencies: - tslib: 2.6.3 + tslib: 2.7.0 semver@7.6.3: {} @@ -2442,7 +2477,7 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - tailwindcss@3.4.6: + tailwindcss@3.4.10: dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -2454,16 +2489,16 @@ snapshots: is-glob: 4.0.3 jiti: 1.21.6 lilconfig: 2.1.0 - micromatch: 4.0.7 + micromatch: 4.0.8 normalize-path: 3.0.0 object-hash: 3.0.0 - picocolors: 1.0.1 - postcss: 8.4.39 - postcss-import: 15.1.0(postcss@8.4.39) - postcss-js: 4.0.1(postcss@8.4.39) - postcss-load-config: 4.0.2(postcss@8.4.39) - postcss-nested: 6.0.1(postcss@8.4.39) - postcss-selector-parser: 6.1.1 + picocolors: 1.1.0 + postcss: 8.4.45 + postcss-import: 15.1.0(postcss@8.4.45) + postcss-js: 4.0.1(postcss@8.4.45) + postcss-load-config: 4.0.2(postcss@8.4.45) + postcss-nested: 6.2.0(postcss@8.4.45) + postcss-selector-parser: 6.1.2 resolve: 1.22.8 sucrase: 3.35.0 transitivePeerDependencies: @@ -2499,54 +2534,51 @@ snapshots: ts-interface-checker@0.1.13: {} - tslib@2.6.3: {} + tslib@2.7.0: {} - typescript@5.5.3: {} + typescript@5.5.4: {} undefsafe@2.0.5: {} - undici-types@5.26.5: {} + undici-types@6.19.8: {} - update-browserslist-db@1.1.0(browserslist@4.23.2): + update-browserslist-db@1.1.0(browserslist@4.23.3): dependencies: - browserslist: 4.23.2 - escalade: 3.1.2 - picocolors: 1.0.1 + browserslist: 4.23.3 + escalade: 3.2.0 + picocolors: 1.1.0 util-deprecate@1.0.2: {} - vite@5.3.4(@types/node@20.14.11): + uuid@8.3.2: {} + + vite@5.4.3(@types/node@20.16.4): dependencies: esbuild: 0.21.5 - postcss: 8.4.39 - rollup: 4.18.1 + postcss: 8.4.45 + rollup: 4.21.2 optionalDependencies: - '@types/node': 20.14.11 + '@types/node': 20.16.4 fsevents: 2.3.3 vscode-uri@3.0.8: {} - vue-template-compiler@2.7.16: - dependencies: - de-indent: 1.0.2 - he: 1.2.0 - - vue-tsc@2.0.26(typescript@5.5.3): + vue-tsc@2.1.4(typescript@5.5.4): dependencies: - '@volar/typescript': 2.4.0-alpha.16 - '@vue/language-core': 2.0.26(typescript@5.5.3) + '@volar/typescript': 2.4.2 + '@vue/language-core': 2.1.4(typescript@5.5.4) semver: 7.6.3 - typescript: 5.5.3 + typescript: 5.5.4 - vue@3.4.32(typescript@5.5.3): + vue@3.5.1(typescript@5.5.4): dependencies: - '@vue/compiler-dom': 3.4.32 - '@vue/compiler-sfc': 3.4.32 - '@vue/runtime-dom': 3.4.32 - '@vue/server-renderer': 3.4.32(vue@3.4.32(typescript@5.5.3)) - '@vue/shared': 3.4.32 + '@vue/compiler-dom': 3.5.1 + '@vue/compiler-sfc': 3.5.1 + '@vue/runtime-dom': 3.5.1 + '@vue/server-renderer': 3.5.1(vue@3.5.1(typescript@5.5.4)) + '@vue/shared': 3.5.1 optionalDependencies: - typescript: 5.5.3 + typescript: 5.5.4 webidl-conversions@3.0.1: {} @@ -2584,14 +2616,14 @@ snapshots: y18n@5.0.8: {} - yaml@2.4.5: {} + yaml@2.5.1: {} yargs-parser@21.1.1: {} yargs@17.7.2: dependencies: cliui: 8.0.1 - escalade: 3.1.2 + escalade: 3.2.0 get-caller-file: 2.0.5 require-directory: 2.1.1 string-width: 4.2.3 diff --git a/scripts/buildPluginImports.js b/scripts/buildPluginImports.js index ee0cc6ff0..af59a2ef0 100644 --- a/scripts/buildPluginImports.js +++ b/scripts/buildPluginImports.js @@ -1,5 +1,6 @@ import * as fs from 'fs'; import glob from 'fast-glob'; +import path from 'path'; const serverImportsPath = './resources/core/main/server/plugins.js'; const clientImportsPath = './resources/core/main/client/plugins.js'; @@ -17,7 +18,6 @@ function getIndexPath(folder) { async function start() { const pluginFolders = await glob('./src/plugins/*', options); - const serverFolders = []; const clientFolders = []; @@ -26,39 +26,53 @@ async function start() { continue; } - if (fs.existsSync(pluginFolder + '/client')) { - clientFolders.push(pluginFolder + '/client'); - } - - if (fs.existsSync(pluginFolder + '/server')) { - serverFolders.push(pluginFolder + '/server'); + if (path.basename(pluginFolder).startsWith('[') && path.basename(pluginFolder).endsWith(']')) { + const category = path.basename(pluginFolder).slice(1, -1); + const categoryPlugins = await glob(pluginFolder + '/*', options); + for (let categoryPlugin of categoryPlugins) { + if (fs.existsSync(categoryPlugin + '/client')) { + clientFolders.push({ folder: categoryPlugin + '/client', category }); + } + if (fs.existsSync(categoryPlugin + '/server')) { + serverFolders.push({ folder: categoryPlugin + '/server', category }); + } + } + } else { + if (fs.existsSync(pluginFolder + '/client')) { + clientFolders.push({ folder: pluginFolder + '/client', category: 'Uncategorized' }); + } + if (fs.existsSync(pluginFolder + '/server')) { + serverFolders.push({ folder: pluginFolder + '/server', category: 'Uncategorized' }); + } } } + - // Propogate server import paths - for (let serverFolder of serverFolders) { - const path = getIndexPath(serverFolder); - if (!path) { - continue; - } + function getSortedEntries(folders) { + return folders + .map(({ folder, category }) => { + const path = getIndexPath(folder); + if (!path) { + return null; + } + const folderSlice = folder.split('/'); + const folderName = folderSlice[folderSlice.length - 2]; + return { folderName, category, path }; + }) + .filter((entry) => entry !== null) + .sort((a, b) => a.folderName.localeCompare(b.folderName)); + } - const folderSlice = serverFolder.split('/'); - const folderName = folderSlice[folderSlice.length - 2]; - fs.appendFileSync(serverImportsPath, `alt.log('::: Plugin: ${folderName}');` + '\r\n'); + const sortedServerEntries = getSortedEntries(serverFolders); + for (const { folderName, category, path } of sortedServerEntries) { + fs.appendFileSync(serverImportsPath, `alt.log('::: Plugin: ${folderName} | Category: ${category}');` + '\r\n'); const importLine = `import '${path}';`; fs.appendFileSync(serverImportsPath, importLine + '\r\n'); } - // Propogate client import paths - for (let clientFolder of clientFolders) { - const path = getIndexPath(clientFolder); - if (!path) { - continue; - } - - const folderSlice = clientFolder.split('/'); - const folderName = folderSlice[folderSlice.length - 2]; - fs.appendFileSync(clientImportsPath, `alt.log('::: Plugin: ${folderName}');` + '\r\n'); + const sortedClientEntries = getSortedEntries(clientFolders); + for (const { folderName, category, path } of sortedClientEntries) { + fs.appendFileSync(clientImportsPath, `alt.log('::: Plugin: ${folderName} | Category: ${category}');` + '\r\n'); const importLine = `import '${path}';`; fs.appendFileSync(clientImportsPath, importLine + '\r\n'); } diff --git a/scripts/copyFiles.js b/scripts/copyFiles.js index cddd7715b..babffaf91 100644 --- a/scripts/copyFiles.js +++ b/scripts/copyFiles.js @@ -1,16 +1,17 @@ import * as fs from 'fs'; import glob from 'fast-glob'; +import path from 'path'; const filesToCopy = { - 'src/plugins/**/sounds/**/*.ogg': { + 'src/plugins/**/**/sounds/**/*.ogg': { destination: ['resources', 'webview/public'], keyword: 'sounds', }, - 'src/plugins/**/images/**/*.+(jpg|jpeg|png|bmp|svg|webp|gif)': { + 'src/plugins/**/**/images/**/*.+(jpg|jpeg|png|bmp|svg|webp|gif)': { destination: ['resources', 'webview/public'], keyword: 'images', }, - 'src/plugins/**/fonts/**/*.+(ttf|otf)': { + 'src/plugins/**/**/fonts/**/*.+(ttf|otf)': { destination: ['resources', 'webview/public'], keyword: 'fonts', }, @@ -46,7 +47,7 @@ function moveRmluiFiles() { } catch (err) {} } - const rmluiFiles = glob.sync(`src/plugins/**/rmlui/**/*.+(html|ttf)`); + const rmluiFiles = glob.sync(`src/plugins/**/**/rmlui/**/*.+(html|ttf)`); for (let file of rmluiFiles) { const splitPath = file.split('/'); const pluginName = getPluginName(splitPath); @@ -73,6 +74,10 @@ function moveRmluiFiles() { */ function getPluginName(splitPath) { let index = splitPath.findIndex((x) => x.includes('plugins')); + if (splitPath[index + 1].startsWith('[') && splitPath[index + 1].endsWith(']')) { + // This is a categorized plugin + return splitPath[index + 2]; + } return splitPath[index + 1]; } @@ -99,11 +104,9 @@ function copyFiles() { if (Array.isArray(destination)) { for (let dest of destination) { - const finalPath = dest + '/' + splitPath.join('/'); - const splitFinalPath = finalPath.split('/'); - splitFinalPath.pop(); - - const finalFolderPath = splitFinalPath.join('/'); + const finalPath = path.join(dest, ...splitPath); + const finalFolderPath = path.dirname(finalPath); + if (!fs.existsSync(finalFolderPath)) { fs.mkdirSync(finalFolderPath, { recursive: true }); } @@ -111,11 +114,9 @@ function copyFiles() { fs.copyFileSync(file, finalPath); } } else { - const finalPath = destination + '/' + splitPath.join('/'); - const splitFinalPath = finalPath.split('/'); - splitFinalPath.pop(); - - const finalFolderPath = splitFinalPath.join('/'); + const finalPath = path.join(destination, ...splitPath); + const finalFolderPath = path.dirname(finalPath); + if (!fs.existsSync(finalFolderPath)) { fs.mkdirSync(finalFolderPath, { recursive: true }); } diff --git a/scripts/webview.js b/scripts/webview.js index dea7bfe6f..e24a25637 100644 --- a/scripts/webview.js +++ b/scripts/webview.js @@ -1,5 +1,6 @@ import glob from 'fast-glob'; import path from 'path'; +import fs from 'fs'; import { getEnabledPlugins, sanitizePath } from './shared.js'; import { writeFile } from './fileHelpers.js'; @@ -13,29 +14,33 @@ function start() { continue; } - const files = glob.sync(sanitizePath(path.join(pluginPath, 'webview/*.vue'))); - - for (const file of files) { - const componentName = path.basename(file, '.vue'); - vueFiles[componentName] = sanitizePath(`../../src/plugins/${pluginName}/webview/${componentName}.vue`); + if (fs.statSync(pluginPath).isDirectory() && pluginName.startsWith('[') && pluginName.endsWith(']')) { + const categoryPlugins = fs.readdirSync(pluginPath); + for (const categoryPlugin of categoryPlugins) { + const categoryPluginPath = path.join(pluginPath, categoryPlugin); + if (fs.statSync(categoryPluginPath).isDirectory()) { + processPlugin(categoryPluginPath, `${pluginName}/${categoryPlugin}`, vueFiles); + } + } + } else { + processPlugin(pluginPath, pluginName, vueFiles); } } const importsHeader = `// THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY CONTENTS\n\n`; let content = '//@ts-nocheck\n' + importsHeader; - content = - content + - Object.keys(vueFiles) - .map((componentName) => { - return `import ${componentName} from '${vueFiles[componentName]}';`; - }) - .join('\n'); + const sortedComponentNames = Object.keys(vueFiles).sort((a, b) => a.localeCompare(b)); + + content += sortedComponentNames + .map((componentName) => { + return `import ${componentName} from '${vueFiles[componentName]}';`; + }) + .join('\n'); - content = - content + + content += '\n\nexport const PLUGIN_IMPORTS = {\n' + - Object.keys(vueFiles) + sortedComponentNames .map((componentName) => { return ` ${componentName}: ${componentName},`; }) @@ -45,7 +50,16 @@ function start() { const importPath = sanitizePath(path.join(process.cwd(), 'webview/pages/plugins.ts')); writeFile(importPath, content); - return Object.keys(vueFiles).length; + return sortedComponentNames.length; +} + +function processPlugin(pluginPath, pluginName, vueFiles) { + const files = glob.sync(sanitizePath(path.join(pluginPath, 'webview/*.vue'))); + + for (const file of files) { + const componentName = path.basename(file, '.vue'); + vueFiles[componentName] = sanitizePath(`../../src/plugins/${pluginName}/webview/${componentName}.vue`); + } } start(); diff --git a/src/main/client/controllers/blip.ts b/src/main/client/controllers/blip.ts index 99faaf1d9..38294cc1d 100644 --- a/src/main/client/controllers/blip.ts +++ b/src/main/client/controllers/blip.ts @@ -1,6 +1,6 @@ import * as alt from 'alt-client'; import { Events } from '@Shared/events/index.js'; -import { Blip } from '@Shared/types/blip.js'; +import { Blip } from '@Shared/types/index.js'; const blips: { [key: string]: alt.PointBlip } = {}; @@ -16,13 +16,13 @@ async function handleCreate(blipData: Blip) { blips[blipData.uid].category = blipData.category; } - if (typeof blipData.routeColor !== "undefined") { + if (typeof blipData.routeColor !== 'undefined') { blips[blipData.uid].route = true; blips[blipData.uid].routeColor = blipData.routeColor; } - blips[blipData.uid].sprite = blipData.sprite; - blips[blipData.uid].color = blipData.color; + blips[blipData.uid].sprite = blipData.sprite as number; + blips[blipData.uid].color = blipData.color as number; blips[blipData.uid].shortRange = blipData.shortRange; blips[blipData.uid].name = blipData.text; blips[blipData.uid].dimension = blipData.dimension ?? 0; diff --git a/src/main/client/controllers/marker.ts b/src/main/client/controllers/marker.ts index e4aba0ae6..a897d60ca 100644 --- a/src/main/client/controllers/marker.ts +++ b/src/main/client/controllers/marker.ts @@ -1,7 +1,7 @@ import * as alt from 'alt-client'; import * as ScreenMarker from '../screen/marker.js'; import { Events } from '@Shared/events/index.js'; -import { Marker } from '@Shared/types/marker.js'; +import { Marker } from '@Shared/types/index.js'; import { distance2d } from '@Shared/utility/vector.js'; const MAX_DISTANCE = 50; @@ -22,7 +22,7 @@ function draw() { continue; } - ScreenMarker.draw(marker.type, marker.pos, marker.scale, marker.color, false, false, false); + ScreenMarker.draw(marker.type as number, marker.pos, marker.scale, marker.color, false, false, false); } } diff --git a/src/main/client/controllers/object.ts b/src/main/client/controllers/object.ts index e3da13678..3a5327524 100644 --- a/src/main/client/controllers/object.ts +++ b/src/main/client/controllers/object.ts @@ -1,6 +1,6 @@ import * as alt from 'alt-client'; import { Events } from '@Shared/events/index.js'; -import { iObject } from '@Shared/types/object.js'; +import {iObject} from '@Shared/types/index.js'; import { distance2d } from '@Shared/utility/vector.js'; const MAX_DISTANCE = 50; diff --git a/src/main/client/controllers/textlabel.ts b/src/main/client/controllers/textlabel.ts index 4beb7b035..076b9f476 100644 --- a/src/main/client/controllers/textlabel.ts +++ b/src/main/client/controllers/textlabel.ts @@ -1,7 +1,7 @@ import * as alt from 'alt-client'; import * as ScreenText from '../screen/textlabel.js'; import { Events } from '@Shared/events/index.js'; -import { TextLabel } from '@Shared/types/textLabel.js'; +import {TextLabel} from '@Shared/types/index.js'; import { distance2d } from '@Shared/utility/vector.js'; const MAX_DISTANCE = 25; diff --git a/src/main/client/index.ts b/src/main/client/index.ts index 116e649af..493881c45 100644 --- a/src/main/client/index.ts +++ b/src/main/client/index.ts @@ -79,31 +79,3 @@ export function useRebarClient() { }, }; } - -declare module 'alt-shared' { - // extending interface by interface merging - export interface ICustomGlobalMeta { - /** - * Only available client-side, client folder - * - * @type {ReturnType} - * @memberof ICustomGlobalMeta - */ - RebarClient: ReturnType; - - /** - * Used for getting plugin APIs - * - * Only available client-side, client folder - * - * @type {ReturnType} - * @memberof ICustomGlobalMeta - */ - RebarClientPluginAPI: ReturnType; - } -} - -alt.setMeta('RebarClient', useRebarClient()); -alt.setMeta('RebarClientPluginAPI', useRebarClient().useClientApi()); - -alt.getMeta(''); diff --git a/src/main/client/screen/credits.ts b/src/main/client/screen/credits.ts index 99dfc0179..a5de89a7a 100644 --- a/src/main/client/screen/credits.ts +++ b/src/main/client/screen/credits.ts @@ -1,9 +1,9 @@ import * as alt from 'alt-client'; import * as native from 'natives'; -import { requestScaleForm, Scaleform } from './scaleform.js'; -import { Credit, CreditAlignment } from '@Shared/types/credits.js'; -import { Events } from '@Shared/events/index.js'; +import {requestScaleForm, Scaleform} from './scaleform.js'; +import {Credit, CreditAlignment} from '@Shared/types/index.js'; +import {Events} from '@Shared/events/index.js'; let scaleform: Scaleform; let interval: number; diff --git a/src/main/client/screen/shard.ts b/src/main/client/screen/shard.ts index a844d601a..e2caeb8c6 100644 --- a/src/main/client/screen/shard.ts +++ b/src/main/client/screen/shard.ts @@ -1,6 +1,6 @@ import * as alt from 'alt-client'; import { Scaleform, requestScaleForm } from './scaleform.js'; -import { Shard } from '@Shared/types/shard.js'; +import {Shard} from '@Shared/types/index.js'; import { Events } from '@Shared/events/index.js'; let scaleform: Scaleform; diff --git a/src/main/client/screen/spinner.ts b/src/main/client/screen/spinner.ts index 22abbac5f..f900467ae 100644 --- a/src/main/client/screen/spinner.ts +++ b/src/main/client/screen/spinner.ts @@ -1,6 +1,6 @@ import * as alt from 'alt-client'; import * as native from 'natives'; -import { Spinner } from '@Shared/types/spinner.js'; +import {Spinner} from '@Shared/types/index.js'; import { Events } from '@Shared/events/index.js'; let timeout: number; diff --git a/src/main/client/system/attachments.ts b/src/main/client/system/attachments.ts index 612c4dda0..831995c36 100644 --- a/src/main/client/system/attachments.ts +++ b/src/main/client/system/attachments.ts @@ -18,6 +18,10 @@ function cleanupAttachments(id: number) { } async function createAttachments(player: alt.Player, attachments: Attachment[]) { + if (entityAttachments[player.id] && entityAttachments[player.id].length >= 1) { + cleanupAttachments(player.id); + } + entityAttachments[player.id] = []; for (let attachment of attachments) { @@ -46,6 +50,8 @@ async function createAttachments(player: alt.Player, attachments: Attachment[]) false, true, ); + + entityAttachments[player.id].push(object); } } diff --git a/src/main/client/system/native.ts b/src/main/client/system/native.ts index a8a199cb0..8b5a624ad 100644 --- a/src/main/client/system/native.ts +++ b/src/main/client/system/native.ts @@ -6,4 +6,9 @@ function invoke(nativeName: string, ...args: any[]) { native[nativeName](...args); } +function invokeAsRpc(nativeName: string, ...args: any[]) { + return native[nativeName](...args); +} + alt.onServer(Events.player.native.invoke, invoke); +alt.onRpc(Events.player.native.invokeWithResult, invokeAsRpc); diff --git a/src/main/client/system/raycasts.ts b/src/main/client/system/raycasts.ts index 93557f4f6..468283da1 100644 --- a/src/main/client/system/raycasts.ts +++ b/src/main/client/system/raycasts.ts @@ -79,6 +79,8 @@ export function useRaycast() { return undefined; } + + /** * Get the object the player is looking at * @@ -118,10 +120,27 @@ export function useRaycast() { return results.coords; } + function getFocusedCustom(flag: number, debug = false) { + const results = performRaycast(flag, debug); + if (!results.result || !results.didHit) { + return undefined; + } + const type = native.getEntityType(results.entity); + const entityPos = native.getEntityCoords(results.entity, false); + + return { + scriptId: results.entity, + type, + pos: results.coords, + entityPos, + }; + } + return { getFocusedEntity, getFocusedObject, getFocusedPosition, + getFocusedCustom, }; } @@ -130,3 +149,4 @@ const raycast = useRaycast(); alt.onRpc(Events.systems.raycast.getFocusedEntity, raycast.getFocusedEntity); alt.onRpc(Events.systems.raycast.getFocusedObject, raycast.getFocusedObject); alt.onRpc(Events.systems.raycast.getFocusedPosition, raycast.getFocusedPosition); +alt.onRpc(Events.systems.raycast.getFocusedCustom, raycast.getFocusedCustom); diff --git a/src/main/client/system/streamSyncedGetter.ts b/src/main/client/system/streamSyncedGetter.ts index 60fe437b2..d75c08a28 100644 --- a/src/main/client/system/streamSyncedGetter.ts +++ b/src/main/client/system/streamSyncedGetter.ts @@ -1,6 +1,5 @@ import * as alt from 'alt-client'; -import { Vehicle } from '../../shared/types/vehicle.js'; -import { Character } from '@Shared/types/character.js'; +import {Character, Vehicle} from '@Shared/types/index.js'; export function useStreamSyncedGetter() { /** diff --git a/src/main/client/system/vehicle.ts b/src/main/client/system/vehicle.ts index 4c2a98fbd..7844a8710 100644 --- a/src/main/client/system/vehicle.ts +++ b/src/main/client/system/vehicle.ts @@ -1,6 +1,39 @@ import * as alt from 'alt-client'; +import * as native from 'natives'; import { Events } from '../../shared/events/index.js'; +function updateVehicleDoorState(vehicle: alt.Vehicle) { + for (let i = 0; i <= 5; i++) { + const state = vehicle.getStreamSyncedMeta(`door-${i}`) ? true : false; + + if (state) { + native.setVehicleDoorOpen(vehicle, i, false, true); + } else { + native.setVehicleDoorShut(vehicle, i, true); + } + } +} + +alt.on('streamSyncedMetaChange', (object: alt.BaseObject, key, value) => { + if (!(object instanceof alt.Vehicle)) { + return; + } + + if (!key.includes('door')) { + return; + } + + updateVehicleDoorState(object); +}); + +alt.on('gameEntityCreate', (entity) => { + if (!(entity instanceof alt.Vehicle)) { + return; + } + + updateVehicleDoorState(entity); +}); + alt.onServer(Events.vehicle.set.rpm, (value: number) => { if (!alt.Player.local.vehicle) { return; diff --git a/src/main/client/system/vehicleHandling.ts b/src/main/client/system/vehicleHandling.ts index 73c2d7e14..aa1627295 100644 --- a/src/main/client/system/vehicleHandling.ts +++ b/src/main/client/system/vehicleHandling.ts @@ -10,6 +10,8 @@ alt.on('streamSyncedMetaChange', (vehicle: alt.BaseObject, key: string) => { } const handlingData: Partial = vehicle.getStreamSyncedMeta('handling'); + vehicle.handling.reset(); + for (const [key, value] of Object.entries(handlingData)) { vehicle.handling[key] = value; } diff --git a/src/main/client/virtualEntities/marker.ts b/src/main/client/virtualEntities/marker.ts index 5d0e09c17..f94110354 100644 --- a/src/main/client/virtualEntities/marker.ts +++ b/src/main/client/virtualEntities/marker.ts @@ -1,5 +1,5 @@ import * as alt from 'alt-client'; -import { Marker } from '@Shared/types/marker.js'; +import {Marker} from '@Shared/types/index.js'; import * as ScreenMarker from '../screen/marker.js'; const GroupType = 'marker'; diff --git a/src/main/client/virtualEntities/textlabel.ts b/src/main/client/virtualEntities/textlabel.ts index 95a102674..29555031b 100644 --- a/src/main/client/virtualEntities/textlabel.ts +++ b/src/main/client/virtualEntities/textlabel.ts @@ -1,5 +1,5 @@ import * as alt from 'alt-client'; -import { TextLabel } from '@Shared/types/textLabel.js'; +import {TextLabel} from '@Shared/types/index.js'; import * as ScreenText from '../screen/textlabel.js'; const GroupType = 'textlabel'; diff --git a/src/main/client/webview/index.ts b/src/main/client/webview/index.ts index 99ef3f64f..7ff80d3eb 100644 --- a/src/main/client/webview/index.ts +++ b/src/main/client/webview/index.ts @@ -15,6 +15,8 @@ let webview: alt.WebView; let cursorCount: number = 0; let isPageOpen = false; let openPages: PageNames[] = []; +let openOverlays: PageNames[] = []; +let openPeristentPages: PageNames[] = []; let escapeToClosePage: PageNames; function handleServerEvent(event: string, ...args: any[]) { @@ -156,6 +158,27 @@ export function useWebview(path = 'http://assets/webview/index.html') { } alt.emitServer(Events.view.onPageOpen, vueName); + return; + } + + if (type === 'overlay') { + const idx = openOverlays.findIndex((x) => x === vueName); + if (idx >= 0) { + return; + } + + openOverlays.push(vueName); + return; + } + + if (type === 'persistent') { + const idx = openPeristentPages.findIndex((x) => x === vueName); + if (idx >= 0) { + return; + } + + openPeristentPages.push(vueName); + return; } } @@ -232,6 +255,14 @@ export function useWebview(path = 'http://assets/webview/index.html') { return openPages.findIndex((page) => page === vueName) > -1; } + function isOverlayOpen(vueName: PageNames): boolean { + return openOverlays.findIndex((page) => page === vueName) > -1; + } + + function isPersistentPageOpen(vueName: PageNames) { + return openPeristentPages.findIndex((page) => page === vueName) > -1; + } + /** * This will only be called once, and the callback will be delayed by roughly 5s * @@ -397,6 +428,8 @@ export function useWebview(path = 'http://assets/webview/index.html') { showCursor, show, isSpecificPageOpen, + isOverlayOpen, + isPersistentPageOpen, isAnyPageOpen() { return isPageOpen; }, diff --git a/src/main/server/controllers/blip.ts b/src/main/server/controllers/blip.ts index e6b534c55..52a78c28e 100644 --- a/src/main/server/controllers/blip.ts +++ b/src/main/server/controllers/blip.ts @@ -1,7 +1,8 @@ import * as alt from 'alt-server'; import * as Utility from '@Shared/utility/index.js'; -import { Blip } from '@Shared/types/blip.js'; +import {BlipNames} from '@Shared/data/blipNames.js'; import { Events } from '@Shared/events/index.js'; +import {Blip, BlipColor} from '@Shared/types/index.js'; let interval: number; @@ -37,6 +38,18 @@ export function useBlipGlobal(blipData: Blip) { blipData.dimension = 0; } + if (typeof blipData.sprite === 'string') { + blipData.sprite = BlipNames[blipData.sprite]; + } + + if (typeof blipData.color === 'string') { + blipData.color = BlipColor[blipData.color]; + } + + if (typeof blipData.shortRange === 'undefined') { + blipData.shortRange = true; + } + let blip: alt.PointBlip; let entity: alt.Entity; @@ -55,8 +68,8 @@ export function useBlipGlobal(blipData: Blip) { blip.category = blipData.category; } - blip.sprite = blipData.sprite; - blip.color = blipData.color; + blip.sprite = blipData.sprite as number; + blip.color = blipData.color as number; blip.shortRange = blipData.shortRange; blip.name = blipData.text; blip.dimension = blipData.dimension ?? 0; @@ -182,6 +195,18 @@ export function useBlipLocal(player: alt.Player, blipData: Blip) { blipData.dimension = player.dimension; } + if (typeof blipData.sprite === 'string') { + blipData.sprite = BlipNames[blipData.sprite]; + } + + if (typeof blipData.color === 'string') { + blipData.color = BlipColor[blipData.color]; + } + + if (typeof blipData.shortRange === 'undefined') { + blipData.shortRange = true; + } + function destroy() { player.emit(Events.controllers.blip.destroy, blipData.uid); } diff --git a/src/main/server/controllers/doors.ts b/src/main/server/controllers/doors.ts index 4eaaf4585..d19d4f5a0 100644 --- a/src/main/server/controllers/doors.ts +++ b/src/main/server/controllers/doors.ts @@ -2,9 +2,15 @@ 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'; +import {useEntityPermissions} from "@Server/systems/permissions/entityPermissions.js"; + +declare module 'alt-server' { + export interface ICustomEmitEvent { + 'rebar:doorLocked': (uid: string, initiator: alt.Player) => void; + 'rebar:doorUnlocked': (uid: string, initiator: alt.Player | null) => void; + } +} const config = await useGlobal('doors'); const MAX_DOORS_TO_DRAW = 10; @@ -12,7 +18,6 @@ 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. @@ -82,47 +87,6 @@ export function useDoor() { 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; } @@ -137,11 +101,11 @@ export function useDoor() { 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; + if (!useEntityPermissions(door).check(player)) return false; door.state = getNextState(door.state); doorConfig.setLockState(uid, door.state); - events.invoke(`door-${door.state}`, uid, player); + alt.emit(door.state === DoorState.LOCKED ? 'rebar:doorLocked' : 'rebar:doorUnlocked', uid, player); return true; } @@ -160,7 +124,7 @@ export function useDoor() { door.state = state; doorConfig.setLockState(uid, door.state); - events.invoke(`door-${door.state}`, uid, null); + alt.emit(door.state === DoorState.LOCKED ? 'rebar:doorLocked' : 'rebar:doorUnlocked', uid, null); return true; } diff --git a/src/main/server/controllers/interaction.ts b/src/main/server/controllers/interaction.ts index f63a12569..18d2c2bfd 100644 --- a/src/main/server/controllers/interaction.ts +++ b/src/main/server/controllers/interaction.ts @@ -1,6 +1,9 @@ import * as alt from 'alt-server'; import { Events } from '@Shared/events/index.js'; import * as Utility from '@Shared/utility/index.js'; +import { useBlipGlobal } from './blip.js'; +import { useMarkerGlobal } from './markers.js'; +import { useD2DTextLabel } from './d2dTextLabel.js'; export type InteractionCallback = (entity: alt.Player, colshape: alt.Colshape, uid: string) => void; @@ -104,6 +107,10 @@ export function useInteraction(colshape: alt.Colshape, type: 'vehicle' | 'player let msgEnter = undefined; let msgLeave = undefined; + let blip: ReturnType; + let marker: ReturnType; + let label: ReturnType; + function handleEnter(player: alt.Player) { player.emit(Events.controllers.interaction.set, uid, msgEnter, colshape.pos); @@ -158,7 +165,7 @@ export function useInteraction(colshape: alt.Colshape, type: 'vehicle' | 'player } /** - * Destroy the interaction point + * Destroy the interaction point, and any attached label, blip, or marker * */ function destroy() { @@ -170,6 +177,18 @@ export function useInteraction(colshape: alt.Colshape, type: 'vehicle' | 'player try { shape.destroy(); } catch (err) {} + + if (label) { + label.destroy(); + } + + if (blip) { + blip.destroy(); + } + + if (marker) { + marker.destroy(); + } } /** @@ -194,6 +213,42 @@ export function useInteraction(colshape: alt.Colshape, type: 'vehicle' | 'player interactions.push({ handleEnter, handleLeave, handleInteract, uid, type }); return { + addBlip: (...args: Parameters) => { + if (blip) { + blip.destroy(); + } + + blip = useBlipGlobal(...args); + return blip; + }, + addMarker: (...args: Parameters) => { + if (marker) { + marker.destroy(); + } + + marker = useMarkerGlobal(...args); + return marker; + }, + addTextLabel: (...args: Parameters) => { + if (label) { + label.destroy(); + } + + label = useD2DTextLabel(...args); + return label; + }, + getPos() { + return colshape.pos; + }, + getBlip() { + return blip; + }, + getMarker() { + return marker; + }, + getTextLabel() { + return label; + }, on, onEnter, onLeave, diff --git a/src/main/server/controllers/markers.ts b/src/main/server/controllers/markers.ts index b13ff898b..164208470 100644 --- a/src/main/server/controllers/markers.ts +++ b/src/main/server/controllers/markers.ts @@ -1,7 +1,7 @@ +import * as alt from 'alt-server'; import { Events } from '@Shared/events/index.js'; -import { Marker, MarkerType } from '@Shared/types/marker.js'; +import {Marker, MarkerType} from '@Shared/types/index.js'; import * as Utility from '@Shared/utility/index.js'; -import * as alt from 'alt-server'; const GroupType = 'marker'; const MAX_MARKERS = 10; diff --git a/src/main/server/controllers/object.ts b/src/main/server/controllers/object.ts index 26ab70405..7cf35a0f1 100644 --- a/src/main/server/controllers/object.ts +++ b/src/main/server/controllers/object.ts @@ -1,8 +1,7 @@ import * as alt from 'alt-server'; import * as Utility from '@Shared/utility/index.js'; -import { TextLabel } from '@Shared/types/textLabel.js'; import { Events } from '@Shared/events/index.js'; -import { iObject } from '@Shared/types/object.js'; +import {iObject} from '@Shared/types/index.js'; /** * Create an object globally diff --git a/src/main/server/controllers/ped.ts b/src/main/server/controllers/ped.ts index 5e742e71a..22ed63ac9 100644 --- a/src/main/server/controllers/ped.ts +++ b/src/main/server/controllers/ped.ts @@ -1,9 +1,8 @@ import * as alt from 'alt-server'; import * as native from 'natives'; -import { PedOptions } from '@Shared/types/pedOptions.js'; +import {PedOptions, OmitFirstArg} from '@Shared/types/index.js'; import { Events } from '../../shared/events/index.js'; import * as Utility from '@Shared/utility/index.js'; -import { useRebar } from '../index.js'; const sessionKey = 'ped:uid'; @@ -48,7 +47,6 @@ type PedNatives = Pick< | 'taskWanderStandard' >; -type OmitFirstArg = F extends (x: any, ...args: infer P) => infer R ? (...args: P) => R : never; type PedDeathCallback = (uid: string, killer: alt.Entity, weaponHash: number) => void; const peds: Map> = new Map(); diff --git a/src/main/server/controllers/pickups.ts b/src/main/server/controllers/pickups.ts index 5c3010d58..8305dfc61 100644 --- a/src/main/server/controllers/pickups.ts +++ b/src/main/server/controllers/pickups.ts @@ -1,7 +1,7 @@ import * as alt from 'alt-server'; import * as Utility from '@Shared/utility/index.js'; -import { WeaponPickup } from '@Shared/types/pickup.js'; -import { WeaponPickups } from '../../shared/data/pickups.js'; +import {WeaponPickup} from '@Shared/types/index.js'; +import {WeaponPickups} from '@Shared/data/pickups.js'; type PlayerCallback = (player: alt.Player, pickup: WeaponPickup, destroy: Function) => void; type InternalPickup = { colshape: alt.Colshape; entity: alt.Object; invoke: (player: alt.Player) => void }; diff --git a/src/main/server/controllers/textlabel.ts b/src/main/server/controllers/textlabel.ts index 06b0dda2f..84b796e53 100644 --- a/src/main/server/controllers/textlabel.ts +++ b/src/main/server/controllers/textlabel.ts @@ -1,7 +1,7 @@ +import * as alt from 'alt-server'; import { Events } from '@Shared/events/index.js'; -import { TextLabel } from '@Shared/types/textLabel.js'; +import {TextLabel} from '@Shared/types/index.js'; import * as Utility from '@Shared/utility/index.js'; -import * as alt from 'alt-server'; const GroupType = 'textlabel'; const MAX_LABELS = 10; diff --git a/src/main/server/cronjob/index.ts b/src/main/server/cronjob/index.ts new file mode 100644 index 000000000..9bc0d8a0f --- /dev/null +++ b/src/main/server/cronjob/index.ts @@ -0,0 +1,77 @@ +import cron from 'node-cron'; + +declare global { + export interface CronJobs {} +} + +const registeredJobs = new Map void)[] }>(); + +export function useCronJob() { + function create(jobName: string, cronExpression: string, tasks: (() => void)[], overwrite = false): boolean { + if (!cron.validate(cronExpression)) { + return false; + } + + if (registeredJobs.has(jobName)) { + if (!overwrite) { + return false; + } + const { job } = registeredJobs.get(jobName); + job.stop(); + registeredJobs.delete(jobName); + } + + const job = cron.schedule(cronExpression, () => { + tasks.forEach(task => task()); + }); + + registeredJobs.set(jobName, { job, cronExpression, tasks }); + + return true; + } + + function remove(jobName: string) { + if (!registeredJobs.has(jobName)) { + return; + } + + const { job } = registeredJobs.get(jobName); + job.stop(); + registeredJobs.delete(jobName); + } + + function addTaskToCron(jobName: K, task: () => void) { + if (!registeredJobs.has(jobName)) { + return; + } + + const { job, cronExpression, tasks } = registeredJobs.get(jobName); + job.stop(); + + tasks.push(task); + create(jobName, cronExpression, tasks, true); + } + + function removeTaskFromCron(jobName: K, task: () => void) { + if (!registeredJobs.has(jobName)) { + return; + } + + const { job, cronExpression, tasks } = registeredJobs.get(jobName); + job.stop(); + + const index = tasks.indexOf(task); + if (index > -1) { + tasks.splice(index, 1); + } + + create(jobName, cronExpression, tasks, true); + } + + return { + create, + addTaskToCron, + removeTaskFromCron, + remove + }; +} diff --git a/src/main/server/database/index.ts b/src/main/server/database/index.ts index 14729a77e..143fffaaa 100644 --- a/src/main/server/database/index.ts +++ b/src/main/server/database/index.ts @@ -1,8 +1,8 @@ import * as alt from 'alt-server'; -import { MongoClient, Db, InsertOneResult, ObjectId, AggregateOptions } from 'mongodb'; +import {MongoClient, Db, InsertOneResult, ObjectId, AggregateOptions, UpdateFilter} from 'mongodb'; import * as Utility from '@Shared/utility/index.js'; -import { CollectionNames } from '../document/shared.js'; -import { useConfig } from '@Server/config/index.js'; +import {CollectionNames} from '../document/shared.js'; +import {useConfig} from '@Server/config/index.js'; const config = useConfig(); @@ -11,7 +11,7 @@ let isInit = false; let database: Db; let client: MongoClient; -alt.on('on-rpc-restart', () => { +alt.on('rebar:rpcRestart', () => { if (!client) { return; } @@ -95,7 +95,8 @@ export function useDatabase() { try { await client.createCollection(name); - } catch (err) {} + } catch (err) { + } } /** @@ -121,8 +122,47 @@ export function useDatabase() { try { const result = await client .collection(collection) - .findOneAndUpdate({ _id: ObjectId.createFromHexString(data._id) }, { $set: dataClone }); - return result.ok ? true : false; + .findOneAndUpdate({_id: ObjectId.createFromHexString(data._id)}, {$set: dataClone}); + return true; + } catch (err) { + return false; + } + } + + /** + * Update many documents by a filter. + * + * @export + * @template T + * @param {T} filter + * @param {UpdateFilter} update + * @param {string} collection + */ + async function updateMany(filter: T, update: UpdateFilter, collection: string): Promise { + const client = await getClient(); + try { + const result = await client.collection(collection).updateMany(filter, update); + return result.acknowledged; + } catch (err) { + return false; + } + } + + /** + * Unset fields in a document by `_id` and `collection`. + * + * Returns `true` if successful. + * + * @param {string} _id Document _id + * @param {string[]} fields Fields to unset + * @param {string} collection Collection name + */ + async function unset(_id: string, fields: string[], collection: string) { + const client = await getClient(); + + try { + const result = await client.collection(collection).updateOne({_id: ObjectId.createFromHexString(_id)}, {"$unset": Object.assign({}, ...fields.map((x) => ({[x]: ''})))}); + return result.acknowledged } catch (err) { return false; } @@ -142,7 +182,7 @@ export function useDatabase() { const client = await getClient(); try { - const result = await client.collection(collection).deleteOne({ _id: ObjectId.createFromHexString(_id) }); + const result = await client.collection(collection).deleteOne({_id: ObjectId.createFromHexString(_id)}); return result.deletedCount >= 1; } catch (err) { return false; @@ -169,7 +209,7 @@ export function useDatabase() { const client = await getClient(); try { - const dataLookup: any = { ...dataToMatch }; + const dataLookup: any = {...dataToMatch}; if (dataToMatch._id) { dataLookup._id = ObjectId.createFromHexString(dataToMatch._id); @@ -180,7 +220,7 @@ export function useDatabase() { return undefined; } - return { ...document, _id: String(document._id) }; + return {...document, _id: String(document._id)}; } catch (err) { console.log(err); return undefined; @@ -203,7 +243,7 @@ export function useDatabase() { const client = await getClient(); try { - const dataLookup: any = { ...dataToMatch }; + const dataLookup: any = {...dataToMatch}; if (dataToMatch._id) { dataLookup._id = ObjectId.createFromHexString(dataToMatch._id); @@ -212,7 +252,7 @@ export function useDatabase() { const cursor = await client.collection(collection).find(dataLookup); const documents = await cursor.toArray(); return documents.map((x) => { - return { ...x, _id: String(x._id) }; + return {...x, _id: String(x._id)}; }); } catch (err) { return []; @@ -236,7 +276,7 @@ export function useDatabase() { const cursor = await client.collection(collection).find(); const documents = await cursor.toArray(); return documents.map((x) => { - return { ...x, _id: String(x._id) }; + return {...x, _id: String(x._id)}; }) as (T & { _id: string })[]; } catch (err) { return undefined; @@ -257,7 +297,7 @@ export function useDatabase() { const client = await getClient(); try { - const result = await client.collection(collection).deleteOne({ _id: ObjectId.createFromHexString(_id) }); + const result = await client.collection(collection).deleteOne({_id: ObjectId.createFromHexString(_id)}); return result.acknowledged; } catch (err) { return false; @@ -286,7 +326,7 @@ export function useDatabase() { const cursor = await client.collection(collection).aggregate(pipeline, options); const documents = await cursor.toArray(); return documents.map((x) => { - return { ...x, _id: String(x._id) }; + return {...x, _id: String(x._id)}; }) as (T & { _id: string })[]; } catch (err) { return undefined; @@ -306,7 +346,9 @@ export function useDatabase() { isConnected() { return isConnected; }, + updateMany, update, aggregate, + unset, }; } diff --git a/src/main/server/document/account.ts b/src/main/server/document/account.ts index 47e0ecc91..b69171c81 100644 --- a/src/main/server/document/account.ts +++ b/src/main/server/document/account.ts @@ -1,14 +1,22 @@ import * as alt from 'alt-server'; import * as Utility from '../utility/index.js'; -import { Account } from '@Shared/types/account.js'; -import { KnownKeys } from '@Shared/utilityTypes/index.js'; +import { Character, Account } from '@Shared/types/index.js'; import { useDatabase } from '@Server/database/index.js'; import { CollectionNames, KeyChangeCallback } from './shared.js'; -import { Character } from '@Shared/types/character.js'; -import { useRebar } from '../index.js'; -import { usePermissionProxy } from '@Server/systems/permissionProxy.js'; +import { usePermissionProxy } from '@Server/systems/permissions/permissionProxy.js'; +import { useIncrementalId } from './increment.js'; + +declare module 'alt-server' { + export interface ICustomEmitEvent { + 'rebar:playerAccountBound': (player: alt.Player, document: Account) => void; + 'rebar:playerAccountUpdated': ( + player: alt.Player, + fieldName: K, + value: Account[K], + ) => void; + } +} -const Rebar = useRebar(); const sessionKey = 'document:account'; const callbacks: { [key: string]: Array } = {}; const db = useDatabase(); @@ -32,26 +40,25 @@ export function useAccount(player: alt.Player) { /** * Return current player data and their associated account object. * - * @template T - * @return {(T & Account) | undefined} + * @return {(Account | undefined)} */ - function get(): (T & Account) | undefined { + function get(): Account | undefined { if (!player.hasMeta(sessionKey)) { return undefined; } - return player.getMeta(sessionKey); + return player.getMeta(sessionKey); } /** * Get the current value of a specific field inside of the player data object. * Can be extended to obtain any value easily. * - * @template T - * @param {(keyof KnownKeys)} fieldName - * @return {ReturnType | undefined} + * @template K + * @param {K} fieldName + * @return {(Account[K] | undefined)} */ - function getField(fieldName: keyof KnownKeys): ReturnType | undefined { + function getField(fieldName: K): Account[K] | undefined { if (!player.hasMeta(sessionKey)) { return undefined; } @@ -63,18 +70,18 @@ export function useAccount(player: alt.Player) { * Sets a player document value, and saves it automatically to the selected account database. * Automatically calls all callbacks associated with the field name. * - * @template T - * @param {(keyof KnownKeys)} fieldName - * @param {*} value - * @return {void} + * @template K + * @param {K} fieldName + * @param {Account[K]} value + * @return */ - async function set>(fieldName: Keys, value: any) { + async function set(fieldName: K, value: Account[K]) { if (!player.hasMeta(sessionKey)) { return undefined; } const typeSafeFieldName = String(fieldName); - let data = player.getMeta(sessionKey) as T & Account; + let data = player.getMeta(sessionKey) as Account; let oldValue = undefined; if (data[typeSafeFieldName]) { @@ -87,6 +94,8 @@ export function useAccount(player: alt.Player) { player.setMeta(sessionKey, data); await db.update({ _id: data._id, [typeSafeFieldName]: value }, CollectionNames.Accounts); + alt.emit('rebar:playerAccountUpdated', player, fieldName, value); + if (typeof callbacks[typeSafeFieldName] === 'undefined') { return; } @@ -100,16 +109,15 @@ export function useAccount(player: alt.Player) { * Sets player document values, and saves it automatically to the selected Account's database. * Automatically calls all callbacks associated with the field name. * - * @template T - * @param {(Partial)} fields - * @returns {void} + * @param {Partial} fields + * @return */ - async function setBulk>(fields: Keys) { + async function setBulk(fields: Partial) { if (!player.hasMeta(sessionKey)) { return undefined; } - let data = player.getMeta(sessionKey) as Account & T; + let data = player.getMeta(sessionKey) as Account; const oldValues = {}; @@ -127,6 +135,8 @@ export function useAccount(player: alt.Player) { await db.update({ _id: data._id, ...fields }, CollectionNames.Accounts); Object.keys(fields).forEach((key) => { + alt.emit('rebar:playerAccountUpdated', player, key as keyof Account, data[key]); + if (typeof callbacks[key] === 'undefined') { return; } @@ -140,13 +150,12 @@ export function useAccount(player: alt.Player) { /** * Return all characters that belong to this account * - * @template T - * @return {(Promise<(Character & T)[]>)} + * @return {Promise} */ - async function getCharacters(): Promise<(Character & T)[]> { - const data = player.getMeta(sessionKey) as Account & T; + async function getCharacters(): Promise { + const data = player.getMeta(sessionKey) as Character; const results = await db.getMany({ account_id: data._id }, CollectionNames.Characters); - return results as (Character & T)[]; + return results as Character[]; } /** @@ -190,50 +199,17 @@ export function useAccount(player: alt.Player) { return getField('id'); } - const identifier = await Rebar.database.useIncrementalId(Rebar.database.CollectionNames.Accounts); + const identifier = await useIncrementalId(CollectionNames.Accounts); const id = await identifier.getNext(); await setBulk({ id }); return id; } - const { permissions, groupPermissions } = usePermissionProxy(player, 'account', get, set); - /** - * Old permission system. Will be deprecated. - * @deprecated - */ - const permission = { - /** - * @deprecated - */ - addPermission: async (permissionName: string) => { - alt.logWarning('Consider using useAccount(...).permissions.addPermission instead. This will be deprecated.'); - return permissions.addPermission(permissionName); - }, - /** - * @deprecated - */ - removePermission: async (permissionName: string) => { - alt.logWarning('Consider using useAccount(...).permissions.removePermission instead. This will be deprecated.'); - return permissions.removePermission(permissionName); - }, - /** - * @deprecated - */ - hasPermission: (permissionName: string) => { - alt.logWarning('Consider using useAccount(...).permissions.hasPermission instead. This will be deprecated.'); - return permissions.hasPermission(permissionName); - }, - /** - * @deprecated - */ - setBanned: async (reason: string) => { - alt.logWarning('Consider using useAccount(...).setBanned instead. This will be deprecated.'); - return setBanned(reason); - }, - } + const { permissions, groups } = usePermissionProxy(get, setBulk, player, 'account'); return { - permission, + permissions, + groups, addIdentifier, get, getCharacters, @@ -244,8 +220,6 @@ export function useAccount(player: alt.Player) { setPassword, checkPassword, setBanned, - permissions, - groupPermissions, }; } @@ -263,7 +237,7 @@ export function useAccountBinder(player: alt.Player) { } player.setMeta(sessionKey, document); - Rebar.events.useEvents().invoke('account-bound', player, document); + alt.emit('rebar:playerAccountBound', player, document); const accountUse = useAccount(player); try { @@ -289,26 +263,3 @@ export function useAccountBinder(player: alt.Player) { unbind, }; } - -export function useAccountEvents() { - /** - * Listen for individual player document changes. - * - * @param {string} fieldName - * @param {KeyChangeCallback} callback - * @return {void} - */ - function on(fieldName: keyof KnownKeys, callback: KeyChangeCallback) { - const actualFieldName = String(fieldName); - - if (typeof callbacks[actualFieldName] === 'undefined') { - callbacks[actualFieldName] = [callback]; - } else { - callbacks[actualFieldName].push(callback); - } - } - - return { - on, - }; -} diff --git a/src/main/server/document/character.ts b/src/main/server/document/character.ts index 073f09954..95186dc45 100644 --- a/src/main/server/document/character.ts +++ b/src/main/server/document/character.ts @@ -1,14 +1,25 @@ import * as alt from 'alt-server'; -import { Character } from '@Shared/types/character.js'; -import { KnownKeys } from '@Shared/utilityTypes/index.js'; +import { Character, Vehicle } from '@Shared/types/index.js'; import { useDatabase } from '@Server/database/index.js'; import { CollectionNames, KeyChangeCallback } from './shared.js'; -import { Vehicle } from 'main/shared/types/vehicle.js'; -import { useRebar } from '../index.js'; -import { usePermissionProxy } from '@Server/systems/permissionProxy.js'; -import { removeGroup } from 'natives'; +import { usePermissionProxy } from '@Server/systems/permissions/permissionProxy.js'; +import { useIncrementalId } from './increment.js'; +import { usePlayerAppearance } from '../player/appearance.js'; +import { useClothing } from '../player/clothing.js'; +import { useWeapon } from '../player/weapon.js'; +import { useState } from '../player/state.js'; + +declare module 'alt-server' { + export interface ICustomEmitEvent { + 'rebar:playerCharacterBound': (player: alt.Player, document: Character) => void; + 'rebar:playerCharacterUpdated': ( + player: alt.Player, + fieldName: K, + value: Character[K], + ) => void; + } +} -const Rebar = useRebar(); const sessionKey = 'document:character'; const callbacks: { [key: string]: Array } = {}; const db = useDatabase(); @@ -32,28 +43,25 @@ export function useCharacter(player: alt.Player) { /** * Return current player data and their associated character object. * - * @template T - * @return {(T & Character) | undefined} + * @return {(Character | undefined)} */ - function get(): (T & Character) | undefined { + function get(): Character | undefined { if (!player.hasMeta(sessionKey)) { return undefined; } - return player.getMeta(sessionKey); + return player.getMeta(sessionKey); } /** * Get the current value of a specific field inside of the player data object. * Can be extended to obtain any value easily. * - * @template T - * @param {(keyof KnownKeys)} fieldName - * @return {ReturnType | undefined} + * @template K + * @param {K} fieldName + * @return {(Character[K] | undefined)} */ - function getField = keyof KnownKeys>( - fieldName: K, - ): (Character & T)[K] | undefined { + function getField(fieldName: K): Character[K] | undefined { if (!player.hasMeta(sessionKey)) { return undefined; } @@ -65,18 +73,18 @@ export function useCharacter(player: alt.Player) { * Sets a player document value, and saves it automatically to the selected Character database. * Automatically calls all callbacks associated with the field name. * - * @template T - * @param {(keyof KnownKeys)} fieldName - * @param {*} value - * @return {void} + * @template K + * @param {K} fieldName + * @param {Character[K]} value + * @return */ - async function set>(fieldName: Keys, value: any) { + async function set(fieldName: K, value: Character[K]) { if (!player.hasMeta(sessionKey)) { return undefined; } const typeSafeFieldName = String(fieldName); - let data = player.getMeta(sessionKey) as T & Character; + let data = player.getMeta(sessionKey) as Character; let oldValue = undefined; if (data[typeSafeFieldName]) { @@ -89,6 +97,8 @@ export function useCharacter(player: alt.Player) { player.setMeta(sessionKey, data); await db.update({ _id: data._id, [typeSafeFieldName]: value }, CollectionNames.Characters); + alt.emit('rebar:playerCharacterUpdated', player, fieldName, value); + if (typeof callbacks[typeSafeFieldName] === 'undefined') { return; } @@ -102,16 +112,15 @@ export function useCharacter(player: alt.Player) { * Sets player document values, and saves it automatically to the selected Character's database. * Automatically calls all callbacks associated with the field name. * - * @template T - * @param {(Partial)} fields - * @returns {void} + * @param {Partial} fields + * @return */ - async function setBulk>(fields: Keys) { + async function setBulk(fields: Partial) { if (!player.hasMeta(sessionKey)) { return undefined; } - let data = player.getMeta(sessionKey) as Character & T; + let data = player.getMeta(sessionKey) as Character; const oldValues = {}; @@ -129,6 +138,8 @@ export function useCharacter(player: alt.Player) { await db.update({ _id: data._id, ...fields }, CollectionNames.Characters); Object.keys(fields).forEach((key) => { + alt.emit('rebar:playerCharacterUpdated', player, key as keyof Character, data[key]); + if (typeof callbacks[key] === 'undefined') { return; } @@ -142,13 +153,12 @@ export function useCharacter(player: alt.Player) { /** * Return all vehicles that belong to this account * - * @template T - * @return {(Promise<(Vehicle & T)[]>)} + * @return {Promise} */ - async function getVehicles(): Promise<(Vehicle & T)[]> { - const data = player.getMeta(sessionKey) as Vehicle & T; + async function getVehicles(): Promise { + const data = player.getMeta(sessionKey) as Vehicle; const results = await db.getMany({ owner: data._id }, CollectionNames.Vehicles); - return results as (Vehicle & T)[]; + return results as Vehicle[]; } async function addIdentifier() { @@ -156,72 +166,25 @@ export function useCharacter(player: alt.Player) { return getField('id'); } - const identifier = Rebar.database.useIncrementalId(Rebar.database.CollectionNames.Characters); + const identifier = useIncrementalId(CollectionNames.Characters); const id = await identifier.getNext(); await setBulk({ id }); return id; } - const { permissions, groupPermissions } = usePermissionProxy(player, 'character', get, set); + const { permissions, groups } = usePermissionProxy(get, setBulk, player, 'character'); - /** - * Old permission system. Will be deprecated in the future. - * Use the new permission system instead. - * @deprecated - */ - const permission = { - /** - * @deprecated - */ - addPermission: async (permissionName: string) => { - alt.logWarning('Consider using useCharacter(...).permissions.addPermission. This will be deprecated.'); - return await permissions.addPermission(permissionName); - }, - /** - * @deprecated - */ - removePermission: async (permissionName: string) => { - alt.logWarning('Consider using useCharacter(...).permissions.removePermission. This will be deprecated.'); - return await permissions.removePermission(permissionName); - }, - /** - * @deprecated - */ - hasPermission: (permissionName: string) => { - alt.logWarning('Consider using useCharacter(...).permissions.hasPermission. This will be deprecated.'); - return permissions.hasPermission(permissionName); - }, - /** - * @deprecated - */ - hasGroupPermission: (groupName: string, permissionName: string) => { - alt.logWarning('Consider using useCharacter(...).permissions.hasGroupPermission. This will be deprecated.'); - return groupPermissions.hasGroupPerm(groupName, permissionName); - }, - /** - * @deprecated - */ - hasAnyGroupPermission: (groupName: string, permissionNames: string[]) => { - alt.logWarning('Consider using useCharacter(...).permissions.hasAnyGroupPermission. This will be deprecated.'); - return groupPermissions.hasAtLeastOneGroupPerm(groupName, permissionNames); - }, - /** - * @deprecated - */ - addGroupPerm: async (groupName: string, permissionName: string) => { - alt.logWarning('Consider using useCharacter(...).permissions.addGroupPerm. This will be deprecated.'); - return await groupPermissions.addPermissions(groupName, [permissionName]); - }, - /** - * @deprecated - */ - removeGroupPerm: async (groupName: string, permissionName: string) => { - alt.logWarning('Consider using useCharacter(...).permissions.removeGroupPerm. This will be deprecated.'); - return await groupPermissions.removePermissions(groupName, [permissionName]); - } - } - - return { addIdentifier, get, getField, isValid, getVehicles, permission, permissions, groupPermissions, set, setBulk }; + return { + permissions, + groups, + addIdentifier, + get, + getField, + isValid, + getVehicles, + set, + setBulk, + }; } export function useCharacterBinder(player: alt.Player, syncPlayer = true) { @@ -241,13 +204,13 @@ export function useCharacterBinder(player: alt.Player, syncPlayer = true) { } player.setMeta(sessionKey, document); - Rebar.events.useEvents().invoke('character-bound', player, document); + alt.emit('rebar:playerCharacterBound', player, document); if (syncPlayer) { - Rebar.player.usePlayerAppearance(player).sync(); - Rebar.player.useClothing(player).sync(); - Rebar.player.useWeapon(player).sync(); - Rebar.player.useState(player).sync(); + usePlayerAppearance(player).sync(); + useClothing(player).sync(); + useWeapon(player).sync(); + useState(player).sync(); } const characterUse = useCharacter(player); @@ -274,26 +237,3 @@ export function useCharacterBinder(player: alt.Player, syncPlayer = true) { unbind, }; } - -export function useCharacterEvents() { - /** - * Listen for individual player document changes. - * - * @param {string} fieldName - * @param {KeyChangeCallback} callback - * @return {void} - */ - function on(fieldName: keyof KnownKeys, callback: KeyChangeCallback) { - const actualFieldName = String(fieldName); - - if (typeof callbacks[actualFieldName] === 'undefined') { - callbacks[actualFieldName] = [callback]; - } else { - callbacks[actualFieldName].push(callback); - } - } - - return { - on, - }; -} diff --git a/src/main/server/document/global.ts b/src/main/server/document/global.ts index 2573d8e85..af968ccd9 100644 --- a/src/main/server/document/global.ts +++ b/src/main/server/document/global.ts @@ -61,10 +61,16 @@ export async function useGlobal(identifier: string) { return await db.update({ _id: data[identifier]._id, ...newData }, CollectionNames.Global); } + async function unset(fieldName: string): Promise { + delete data[identifier][fieldName]; + return await db.unset(identifier, [fieldName], CollectionNames.Global); + } + return { get, getField, set, setBulk, + unset, }; } diff --git a/src/main/server/document/increment.ts b/src/main/server/document/increment.ts index d0f576906..cb25b7db6 100644 --- a/src/main/server/document/increment.ts +++ b/src/main/server/document/increment.ts @@ -1,12 +1,10 @@ -import { useRebar } from '../index.js'; +import { useGlobal } from './global.js'; -const Rebar = useRebar(); - -let incremental: Awaited>>; +let incremental: Awaited>>; async function init() { try { - incremental = await Rebar.document.global.useGlobal('incrementals'); + incremental = await useGlobal('incrementals'); } catch (err) {} } diff --git a/src/main/server/document/vehicle.ts b/src/main/server/document/vehicle.ts index 66a57124a..d53dabab6 100644 --- a/src/main/server/document/vehicle.ts +++ b/src/main/server/document/vehicle.ts @@ -1,11 +1,21 @@ import * as alt from 'alt-server'; -import { Vehicle } from '@Shared/types/vehicle.js'; -import { KnownKeys } from '@Shared/utilityTypes/index.js'; +import {Vehicle as VehicleDocument} from '@Shared/types/index.js'; import { useDatabase } from '@Server/database/index.js'; import { CollectionNames, KeyChangeCallback } from './shared.js'; -import { useRebar } from '../index.js'; +import { useIncrementalId } from './increment.js'; +import { useVehicle as useVehicleRebar } from '../vehicle/index.js'; + +declare module 'alt-server' { + export interface ICustomEmitEvent { + 'rebar:vehicleBound': (vehicle: alt.Vehicle, document: VehicleDocument) => void; + 'rebar:vehicleUpdated': ( + vehicle: alt.Vehicle, + fieldName: K, + value: VehicleDocument[K], + ) => void; + } +} -const Rebar = useRebar(); const sessionKey = 'document:vehicle'; const callbacks: { [key: string]: Array> } = {}; const db = useDatabase(); @@ -30,14 +40,14 @@ export function useVehicle(vehicle: alt.Vehicle) { * Return current vehicle data and their associated Vehicle object. * * @template T - * @return {(T & Vehicle) | undefined} + * @return {VehicleDocument | undefined} */ - function get(): (T & Vehicle) | undefined { + function get(): VehicleDocument | undefined { if (!vehicle.hasMeta(sessionKey)) { return undefined; } - return vehicle.getMeta(sessionKey); + return vehicle.getMeta(sessionKey); } /** @@ -48,7 +58,7 @@ export function useVehicle(vehicle: alt.Vehicle) { * @param {(keyof KnownKeys)} fieldName * @return {ReturnType | undefined} */ - function getField(fieldName: keyof KnownKeys): ReturnType | undefined { + function getField(fieldName: K): VehicleDocument[K] | undefined { if (!vehicle.hasMeta(sessionKey)) { return undefined; } @@ -65,13 +75,13 @@ export function useVehicle(vehicle: alt.Vehicle) { * @param {*} value * @return {void} */ - async function set>(fieldName: Keys, value: any) { + async function set(fieldName: K, value: VehicleDocument[K]) { if (!vehicle.hasMeta(sessionKey)) { return undefined; } const typeSafeFieldName = String(fieldName); - let data = vehicle.getMeta(sessionKey) as T & Vehicle; + let data = vehicle.getMeta(sessionKey) as VehicleDocument; let oldValue = undefined; if (data[typeSafeFieldName]) { @@ -84,6 +94,8 @@ export function useVehicle(vehicle: alt.Vehicle) { vehicle.setMeta(sessionKey, data); await db.update({ _id: data._id, [typeSafeFieldName]: value }, CollectionNames.Vehicles); + alt.emit('rebar:vehicleUpdated', vehicle, fieldName, value); + if (typeof callbacks[typeSafeFieldName] === 'undefined') { return; } @@ -101,12 +113,12 @@ export function useVehicle(vehicle: alt.Vehicle) { * @param {(Partial)} fields * @returns {void} */ - async function setBulk>(fields: Keys) { + async function setBulk(fields: Partial) { if (!vehicle.hasMeta(sessionKey)) { return undefined; } - let data = vehicle.getMeta(sessionKey) as Vehicle & T; + let data = vehicle.getMeta(sessionKey) as VehicleDocument; const oldValues = {}; @@ -124,6 +136,8 @@ export function useVehicle(vehicle: alt.Vehicle) { await db.update({ _id: data._id, ...fields }, CollectionNames.Vehicles); Object.keys(fields).forEach((key) => { + alt.emit('rebar:vehicleUpdated', vehicle, key as keyof VehicleDocument, data[key]); + if (typeof callbacks[key] === 'undefined') { return; } @@ -139,7 +153,7 @@ export function useVehicle(vehicle: alt.Vehicle) { return getField('id'); } - const identifier = await Rebar.database.useIncrementalId(Rebar.database.CollectionNames.Vehicles); + const identifier = await useIncrementalId(CollectionNames.Vehicles); const id = await identifier.getNext(); await setBulk({ id }); return id; @@ -156,16 +170,16 @@ export function useVehicleBinder(vehicle: alt.Vehicle) { * * @param {Vehicle & T} document */ - function bind(document: Vehicle & T, syncVehicle = true): ReturnType | undefined { + function bind(document: VehicleDocument, syncVehicle = true): ReturnType | undefined { if (!vehicle.valid) { return undefined; } vehicle.setMeta(sessionKey, document); - Rebar.events.useEvents().invoke('vehicle-bound', vehicle, document); + alt.emit('rebar:vehicleBound', vehicle, document); if (syncVehicle) { - Rebar.vehicle.useVehicle(vehicle).sync(); + useVehicleRebar(vehicle).sync(); } const vehicleUse = useVehicle(vehicle); @@ -193,26 +207,3 @@ export function useVehicleBinder(vehicle: alt.Vehicle) { unbind, }; } - -export function useVehicleEvents() { - /** - * Listen for individual vehicle document changes. - * - * @param {string} fieldName - * @param {KeyChangeCallback} callback - * @return {void} - */ - function on(fieldName: keyof KnownKeys, callback: KeyChangeCallback) { - const actualFieldName = String(fieldName); - - if (typeof callbacks[actualFieldName] === 'undefined') { - callbacks[actualFieldName] = [callback]; - } else { - callbacks[actualFieldName].push(callback); - } - } - - return { - on, - }; -} diff --git a/src/main/server/document/virtual.ts b/src/main/server/document/virtual.ts index 2fb334ecc..de473ab41 100644 --- a/src/main/server/document/virtual.ts +++ b/src/main/server/document/virtual.ts @@ -1,29 +1,43 @@ -import { useDatabase } from '@Server/database/index.js'; +import * as alt from 'alt-server'; +import {useDatabase} from '@Server/database/index.js'; +import {usePermissionProxy} from "@Server/systems/permissions/index.js"; +import { CollectionNames } from '@Server/document/shared.js'; type BaseDocument = { _id: string }; const db = useDatabase(); -export function useVirtual(_id: string, collectionName: string) { +export async function useVirtual(_id: string, collectionName: string) { + let data = (await db.get({ _id }, collectionName)) as T; + + if (typeof data === 'undefined') { + return undefined; + } + /** - * Return the entire document from the database + * Get the current document data. * - * @return {(Promise)} + * @return {T} + */ + function get(): T { + return data; + } + + /** + * Refresh the document from the database. */ - async function get(): Promise { - const data = await db.get({ _id }, collectionName); - return data as BaseDocument & T; + async function refresh(): Promise { + data = (await db.get({ _id }, collectionName)) as T; } /** * Returns a specific key from the document * * @template K - * @param {K} fieldName - * @return {(Promise)} + * @param {K} fieldName The field to get + * @return {(Promise)} The value of the field */ - async function getField(fieldName: K): Promise { - const data = await get(); + function getField(fieldName: K): Promise { return data[String(fieldName)]; } @@ -31,21 +45,45 @@ export function useVirtual(_id: string, c * Set a specific value for the given document and save to database * * @template K - * @param {K} fieldName - * @param {T[K]} value + * @param {K} fieldName The field to set + * @param {T[K]} value The value to set + * @return {Promise} Returns true if the operation was successful */ - async function set(fieldName: K, value: T[K]) { - await db.update({ _id, [String(fieldName)]: value }, collectionName); + async function set(fieldName: K, value: T[K]): Promise { + if (await db.update({ _id, [String(fieldName)]: value }, collectionName)) { + data[String(fieldName)] = value; + return true; + } + return false; } /** * Set multiple fields for the given document and save to database * - * @param {Partial} fields + * @param {Partial} fields The fields to update + * @return {Promise} Returns true if the operation was successful */ - async function setBulk(fields: Partial) { - await db.update({ _id, ...fields }, collectionName); + async function setBulk(fields: Partial): Promise { + if (await db.update({ _id, ...fields }, collectionName)) { + data = { ...data, ...fields }; + return true; + } + return false; } - return { get, getField, set, setBulk }; + const { permissions, groups } = usePermissionProxy(get, setBulk, undefined, undefined); + + const proxyHandler: ProxyHandler = { + get(target, prop) { + if (collectionName !== CollectionNames.Accounts && collectionName !== CollectionNames.Characters) { + throw new Error(`Access to ${String(prop)} is not allowed for collection ${collectionName}`); + } + return target[prop]; + }, + }; + + const permissionsProxy: ReturnType['permissions'] = new Proxy(permissions, proxyHandler); + const groupsProxy: ReturnType['groups'] = new Proxy(groups, proxyHandler); + + return { refresh, get, getField, set, setBulk, permissions: permissionsProxy, groups: groupsProxy }; } diff --git a/src/main/server/events/index.ts b/src/main/server/events/index.ts deleted file mode 100644 index b4dd19325..000000000 --- a/src/main/server/events/index.ts +++ /dev/null @@ -1,68 +0,0 @@ -import * as alt from 'alt-server'; -import { Account } from '@Shared/types/account.js'; -import { Character } from '@Shared/types/character.js'; -import { Vehicle } from '@Shared/types/vehicle.js'; -import { Weathers } from '@Shared/data/weathers.js'; -import { PageNames } from '../../shared/webview/index.js'; -import { Events } from '../../shared/events/index.js'; - -declare global { - export interface RebarEvents { - 'weather-forecast-changed': (weather: Weathers[]) => void; - 'weather-changed': (weather: Weathers) => void; - 'time-changed': (hour: number, minute: number, second: number) => void; - 'time-second-changed': (minute: number) => void; - 'time-minute-changed': (minute: number) => void; - 'time-hour-changed': (hour: number) => void; - 'account-bound': (player: alt.Player, document: Account) => void; - 'character-bound': (player: alt.Player, document: Character) => void; - '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; - } -} - -type EventCallbacks = { [key in K]: RebarEvents[K][] }; - -const eventCallbacks: Partial> = {}; - -export function useEvents() { - function on(event: K, callback: RebarEvents[K]) { - if (!eventCallbacks[event]) { - eventCallbacks[event] = []; - } - - eventCallbacks[event].push(callback); - } - - function invoke(event: K, ...args: Parameters) { - if (!eventCallbacks[event]) { - return; - } - - for (let cb of eventCallbacks[event]) { - // Normally I would not do this but I know this works, and TypeScript is being a jerk. - // @ts-ignore - cb(...args); - } - } - - return { - invoke, - on, - }; -} - -// Listens for when a page is opened -alt.onClient(Events.view.onPageOpen, (player: alt.Player, pageName: PageNames) => - useEvents().invoke('page-opened', player, pageName), -); - -// Listens for when a page is closed -alt.onClient(Events.view.onPageClose, (player: alt.Player, pageName: PageNames) => - useEvents().invoke('page-closed', player, pageName), -); diff --git a/src/main/server/getters/player.ts b/src/main/server/getters/player.ts index 41c263071..e8b63e132 100644 --- a/src/main/server/getters/player.ts +++ b/src/main/server/getters/player.ts @@ -1,7 +1,6 @@ import * as alt from 'alt-server'; import * as Utility from '@Shared/utility/index.js'; import { useAccount, useCharacter } from '@Server/document/index.js'; -import { getClosestEntity } from './shared.js'; export function usePlayerGetter() { /** @@ -172,41 +171,19 @@ export function usePlayerGetter() { * @return {(alt.Player | undefined)} */ function closestToPlayer(player: alt.Player, range = 10): alt.Player | undefined { - return getClosestEntity(player, [...alt.Player.all], range); + const results = alt.getClosestEntities(player.pos, range, player.dimension, -1, 1) as alt.Player[]; + return results.length >= 1 ? results[0] : undefined; } - // /** - // * Get the current waypoint marked on a player's map. - // * Will return undefined it is not currently set. - // * - // * @param {alt.Player} player An alt:V Player Entity - // * @return {(alt.IVector3 | undefined)} - // */ - // export function waypoint(player: alt.Player): alt.IVector3 | undefined { - // return player.currentWaypoint; - // } - /** * The player closest to a vehicle. * * @param {alt.Vehicle} vehicle An alt:V Vehicle Entity * @return {(alt.Player | undefined)} */ - function closestToVehicle(vehicle: alt.Vehicle): alt.Player | undefined { - const players = [...alt.Player.all].filter((target) => { - if (!target || !target.valid) { - return false; - } - - const document = useCharacter(target); - if (typeof document === 'undefined') { - return false; - } - - return true; - }); - - return Utility.vector.getClosestOfType(vehicle.pos, players); + function closestToVehicle(vehicle: alt.Vehicle, range = 25): alt.Player | undefined { + const results = alt.getClosestEntities(vehicle.pos, range, vehicle.dimension, -1, 1) as alt.Player[]; + return results.length >= 1 ? results[0] : undefined; } /** diff --git a/src/main/server/getters/players.ts b/src/main/server/getters/players.ts index 176f72ec3..6760ccb71 100644 --- a/src/main/server/getters/players.ts +++ b/src/main/server/getters/players.ts @@ -1,6 +1,9 @@ import * as alt from 'alt-server'; import * as Utility from '@Shared/utility/index.js'; -import { useCharacter } from '@Server/document/character.js'; +import {useCharacter} from '@Server/document/character.js'; +import {useAccount} from "@Server/document/index.js"; + +export type PlayersGetterDocumentType = 'account' | 'character' | 'any'; export function usePlayersGetter() { /** @@ -73,7 +76,7 @@ export function usePlayersGetter() { continue; } - playersInRange.push({ player, dist }); + playersInRange.push({player, dist}); } return playersInRange.sort((a, b) => { @@ -172,5 +175,95 @@ export function usePlayersGetter() { return alt.Player.all.filter((x) => x.vehicle && x.vehicle.id === vehicle.id); } - return { online, onlineWithWeapons, inRangeWithDistance, inRange, withName, driving, walking, inVehicle }; + /** + * Returns all players who have a specific permission. + * + * @param {PlayersGetterDocumentType} documentType The document type to check. + * @param {string} permission The permission to check for. + * @return {alt.Player[]} + */ + function withPermission(documentType: PlayersGetterDocumentType, permission: string): alt.Player[] { + + return alt.Player.all.filter((player) => { + if (!player || !player.valid) { + return false; + } + + let [accountMatch, characterMatch] = [false, false]; + + if (documentType === 'account' || documentType === 'any') { + const account = useAccount(player); + if (account.isValid() && account.permissions.has(permission)) { + accountMatch = true; + } + if (documentType === 'account') { + return accountMatch; + } + } + + if (documentType === 'character' || documentType === 'any') { + const character = useCharacter(player); + if (character.isValid() && character.permissions.has(permission)) { + characterMatch = true; + } + if (documentType === 'character') { + return characterMatch; + } + } + + return accountMatch || characterMatch; + }); + } + + /** + * Returns all players who are a member of a specific group. + * + * @param {PlayersGetterDocumentType} documentType The document type to check. + * @param {string} groupName The group to check for. + * @return {alt.Player[]} + */ + function memberOfGroup(documentType: PlayersGetterDocumentType, groupName: string): alt.Player[] { + return alt.Player.all.filter((player) => { + if (!player || !player.valid) { + return false; + } + + let [accountMatch, characterMatch] = [false, false]; + + if (documentType === 'account' || documentType === 'any') { + const account = useAccount(player); + if (account.isValid() && account.groups.memberOf(groupName)) { + accountMatch = true; + } + if (documentType === 'account') { + return accountMatch; + } + } + + if (documentType === 'character' || documentType === 'any') { + const character = useCharacter(player); + if (character.isValid() && character.groups.memberOf(groupName)) { + characterMatch = true; + } + if (documentType === 'character') { + return characterMatch; + } + } + + return accountMatch || characterMatch; + }); + } + + return { + online, + onlineWithWeapons, + inRangeWithDistance, + inRange, + withName, + driving, + walking, + inVehicle, + withPermission, + memberOfGroup + }; } diff --git a/src/main/server/getters/vehicle.ts b/src/main/server/getters/vehicle.ts index 569868b0e..ae99078e1 100644 --- a/src/main/server/getters/vehicle.ts +++ b/src/main/server/getters/vehicle.ts @@ -2,7 +2,6 @@ import * as alt from 'alt-server'; import * as players from './players.js'; import * as Utility from '@Shared/utility/index.js'; import { useVehicle } from '@Server/document/vehicle.js'; -import { getClosestEntity } from './shared.js'; export function useVehicleGetter() { /** @@ -125,7 +124,8 @@ export function useVehicleGetter() { * @return {(alt.Vehicle | undefined)} */ function closestVehicle(player: alt.Player, range = 25): alt.Vehicle | undefined { - return getClosestEntity(player, [...alt.Vehicle.all], range); + const results = alt.getClosestEntities(player.pos, range, player.dimension, -1, 2) as alt.Vehicle[]; + return results.length >= 1 ? results[0] : undefined; } return { diff --git a/src/main/server/index.ts b/src/main/server/index.ts index b556dca61..82e8213f6 100644 --- a/src/main/server/index.ts +++ b/src/main/server/index.ts @@ -1,11 +1,14 @@ import * as alt from 'alt-server'; import * as Utility from '@Shared/utility/index.js'; import './startup.js'; +import './systems/pageSystem.js'; import { useApi } from './api/index.js'; import { useConfig } from './config/index.js'; +import { useCronJob } from './cronjob/index.js'; + import { useBlipGlobal, useBlipLocal } from './controllers/blip.js'; import { useDoor } from './controllers/doors.js'; import { useInteraction } from './controllers/interaction.js'; @@ -19,26 +22,20 @@ import { useDatabase } from './database/index.js'; import { useAccount, useAccountBinder, - useAccountEvents, useCharacter, useCharacterBinder, - useCharacterEvents, useGlobal, useIncrementalId, useVehicleBinder, useVehicle as useVehicleDocument, - useVehicleEvents, useVirtual, } from './document/index.js'; - import { CollectionNames } from './document/shared.js'; import { useProtectCallback } from './utility/protectCallback.js'; import { useRateLimitCallback } from './utility/rateLimitCallback.js'; -import { useEvents } from './events/index.js'; - import { usePlayerGetter } from './getters/player.js'; import { usePlayersGetter } from './getters/players.js'; import { useVehicleGetter } from './getters/vehicle.js'; @@ -57,8 +54,7 @@ import { useWebview } from './player/webview.js'; import { useWorld } from './player/world.js'; import { useMessenger } from './systems/messenger.js'; -import { usePermission } from './systems/permission.js'; -import { usePermissionGroup } from './systems/permissionGroup.js'; +import { usePermissionGroup, useEntityPermissions, usePermissions } from './systems/permissions/index.js'; import { usePlayer } from './player/index.js'; import { useState } from './player/state.js'; @@ -67,8 +63,6 @@ import { sha256, sha256Random } from './utility/hash.js'; import { check, hash } from './utility/password.js'; import { useVehicle } from './vehicle/index.js'; import { useVehicleHandling } from './vehicle/vehicleHandling.js'; -import { useServerTime } from './systems/serverTime.js'; -import { useServerWeather } from './systems/serverWeather.js'; import { useProxyFetch } from './systems/proxyFetch.js'; import { usePed } from './controllers/ped.js'; import { useServerConfig } from './systems/serverConfig.js'; @@ -87,12 +81,20 @@ import { useAttachment } from './player/attachment.js'; import { useInteractionLocal } from './controllers/interactionLocal.js'; import { useInstructionalButtons } from './controllers/instructionalButtons.js'; import { useHono } from './rpc/index.js'; +import { useDeathService } from './services/death.js'; +import { useCurrencyService } from './services/currency.js'; +import { useItemService } from './services/items.js'; +import { useNotificationService } from './services/notifications.js'; +import { useTimeService } from './services/time.js'; +import { useWeatherService } from './services/weather.js'; +import { useServiceRegister } from './services/index.js'; export function useRebar() { return { useApi, useConfig, useHono, + useCronJob, controllers: { useBlipGlobal, useBlipLocal, @@ -123,12 +125,10 @@ export function useRebar() { account: { useAccount, useAccountBinder, - useAccountEvents, }, character: { useCharacter, useCharacterBinder, - useCharacterEvents, }, global: { useGlobal, @@ -136,15 +136,11 @@ export function useRebar() { vehicle: { useVehicle: useVehicleDocument, useVehicleBinder, - useVehicleEvents, }, virtual: { useVirtual, }, }, - events: { - useEvents, - }, get: { usePlayerGetter, usePlayersGetter, @@ -172,25 +168,31 @@ export function useRebar() { messenger: { useMessenger, }, - permission: { - usePermission, + permissions: { usePermissionGroup, + useEntityPermissions, + usePermissions, }, useKeybinder, useKeypress, usePlayer, useProxyFetch, useServerConfig, - useServerTime, - useServerWeather, + services: { + useCurrencyService, + useDeathService, + useItemService, + useNotificationService, + useServiceRegister, + useTimeService, + useWeatherService, + }, systems: { useMessenger, useKeybinder, useKeypress, useProxyFetch, useServerConfig, - useServerTime, - useServerWeather, useStreamSyncedBinder, }, utility: { @@ -211,29 +213,3 @@ export function useRebar() { }, }; } - -declare module 'alt-server' { - // extending interface by interface merging - export interface ICustomGlobalMeta { - /** - * Used for getting plugin APIs - * - * Only available on server-side, server folder - * - * @type {ReturnType} - * @memberof ICustomGlobalMeta - */ - Rebar: ReturnType; - - /** - * Only available on server-side, server folder - * - * @type {ReturnType} - * @memberof ICustomGlobalMeta - */ - RebarAPI: ReturnType; - } -} - -alt.setMeta('Rebar', useRebar()); -alt.setMeta('RebarPluginAPI', useRebar().useApi()); diff --git a/src/main/server/player/attachment.ts b/src/main/server/player/attachment.ts index 522cb9918..b9170ba50 100644 --- a/src/main/server/player/attachment.ts +++ b/src/main/server/player/attachment.ts @@ -1,5 +1,5 @@ import * as alt from 'alt-server'; -import { Attachment } from '@Shared/types/attachment.js'; +import {Attachment} from '@Shared/types/index.js'; declare module 'alt-shared' { export interface ICustomEntityStreamSyncedMeta { diff --git a/src/main/server/player/audio.ts b/src/main/server/player/audio.ts index 1152c7c96..8c412f99d 100644 --- a/src/main/server/player/audio.ts +++ b/src/main/server/player/audio.ts @@ -11,12 +11,17 @@ export function useAudio(player: alt.Player) { native.invoke('playSoundFrontend', -1, audioName, audioRef, true); } - function playSound(soundPath: string) { - webview.emit(Events.player.audio.play.local, soundPath); + function playSound(soundPath: string, volume: number = 1) { + webview.emit(Events.player.audio.play.local, soundPath, volume); } + function stopAudio() { + webview.emit(Events.player.audio.stop.local); + } + return { playFrontendSound, playSound, + stopAudio, }; } diff --git a/src/main/server/player/clothing.ts b/src/main/server/player/clothing.ts index d8ff4f44e..ef28ff04a 100644 --- a/src/main/server/player/clothing.ts +++ b/src/main/server/player/clothing.ts @@ -1,7 +1,6 @@ import * as alt from 'alt-server'; import { useCharacter } from '@Server/document/character.js'; -import { Character } from '@Shared/types/character.js'; -import { ClothingComponent } from '@Shared/types/clothingComponent.js'; +import {Character, ClothingComponent} from '@Shared/types/index.js'; const fModel = alt.hash('mp_f_freemode_01'); const mModel = alt.hash(`mp_m_freemode_01`); diff --git a/src/main/server/player/index.ts b/src/main/server/player/index.ts index d604f4128..2cc895fb5 100644 --- a/src/main/server/player/index.ts +++ b/src/main/server/player/index.ts @@ -18,6 +18,7 @@ import { useRaycast } from './raycast.js'; import { useAccount } from '../document/account.js'; import { useScreenshot } from '../systems/screenshot.js'; import { useAttachment } from './attachment.js'; +import { usePermissions } from '../systems/permissions/index.js'; const playerGetter = usePlayerGetter(); const vehicleGetter = useVehicleGetter(); @@ -62,5 +63,6 @@ export function usePlayer(player: alt.Player) { weapon: useWeapon(player), webview: useWebview(player), world: useWorld(player), + permissions: usePermissions(player), }; } diff --git a/src/main/server/player/native.ts b/src/main/server/player/native.ts index 8090cdf9c..371037709 100644 --- a/src/main/server/player/native.ts +++ b/src/main/server/player/native.ts @@ -31,8 +31,16 @@ export function useNative(player: alt.Player) { } } + async function invokeWithResult( + nativeName: T, + ...args: Parameters<(typeof native)[T]> + ) { + return player.emitRpc(Events.player.native.invokeWithResult, nativeName, ...args); + } + return { invoke, invokeMany, + invokeWithResult, }; } diff --git a/src/main/server/player/notify.ts b/src/main/server/player/notify.ts index 4ec0e9fd4..72192e52e 100644 --- a/src/main/server/player/notify.ts +++ b/src/main/server/player/notify.ts @@ -1,20 +1,18 @@ import * as alt from 'alt-server'; import { Events } from '@Shared/events/index.js'; -import { Spinner } from '@Shared/types/spinner.js'; -import { Shard } from '@Shared/types/shard.js'; -import { Credit } from '@Shared/types/credits.js'; -import { Message } from '@Shared/types/message.js'; +import {Spinner, Shard, Credit, Message} from '@Shared/types/index.js'; import { useMessenger } from '../systems/messenger.js'; +import { useNotificationService } from '../services/notifications.js'; const messenger = useMessenger(); export function useNotify(player: alt.Player) { - function showNotification(message: string) { + function showNotification(message: string, type?: string) { if (!player || !player.valid) { return; } - player.emit(Events.player.notify.notification.create, message); + useNotificationService().emit(player, message, type); } function showMissionText(message: string, duration?: number) { diff --git a/src/main/server/player/raycast.ts b/src/main/server/player/raycast.ts index f9d59f6d5..53551402c 100644 --- a/src/main/server/player/raycast.ts +++ b/src/main/server/player/raycast.ts @@ -39,9 +39,29 @@ export function useRaycast(player: alt.Player) { return await player.emitRpc(Events.systems.raycast.getFocusedPosition, debug); } + async function getFocusedCustom(flag: number, debug = false) { + const validValues = [0, 1, 2, 4, 8, 16, 32, 128, 256, 4294967295]; + + if (flag !== -1) { + let flagcalc = flag; + for (const value of validValues) { + if ((flagcalc & value) === value) { + flagcalc -= value; + } + } + + if (flagcalc !== 0) { + return undefined; + } + } + + return await player.emitRpc(Events.systems.raycast.getFocusedCustom, flag, debug); + } + return { getFocusedEntity, getFocusedObject, getFocusedPosition, + getFocusedCustom, }; } diff --git a/src/main/server/player/state.ts b/src/main/server/player/state.ts index af655f6d6..466cd3aa1 100644 --- a/src/main/server/player/state.ts +++ b/src/main/server/player/state.ts @@ -1,8 +1,6 @@ import * as alt from 'alt-server'; -import { BaseCharacter, Character } from '../../shared/types/character.js'; -import { useRebar } from '../index.js'; - -const Rebar = useRebar(); +import { BaseCharacter } from '../../shared/types/character.js'; +import { useCharacter } from '../document/character.js'; export function useState(player: alt.Player) { /** @@ -40,7 +38,7 @@ export function useState(player: alt.Player) { * Save current player position, rot, health, armor, etc. */ function save() { - const document = Rebar.document.character.useCharacter(player); + const document = useCharacter(player); if (!document.get()) { return; } @@ -60,7 +58,7 @@ export function useState(player: alt.Player) { * @return */ function sync() { - const document = Rebar.document.character.useCharacter(player); + const document = useCharacter(player); const data = document.get(); if (!data) { return; diff --git a/src/main/server/player/status.ts b/src/main/server/player/status.ts index 00a14641e..e248f2885 100644 --- a/src/main/server/player/status.ts +++ b/src/main/server/player/status.ts @@ -1,15 +1,13 @@ import * as alt from 'alt-server'; -import { useRebar } from '../index.js'; - -const Rebar = useRebar(); +import { useAccount, useCharacter } from '../document/index.js'; export function useStatus(player: alt.Player) { function hasAccount() { - return Rebar.document.account.useAccount(player).isValid(); + return useAccount(player).isValid(); } function hasCharacter() { - return Rebar.document.character.useCharacter(player).isValid(); + return useCharacter(player).isValid(); } return { diff --git a/src/main/server/player/weapon.ts b/src/main/server/player/weapon.ts index cd560dc77..a0995a29f 100644 --- a/src/main/server/player/weapon.ts +++ b/src/main/server/player/weapon.ts @@ -1,24 +1,20 @@ import * as alt from 'alt-server'; import { useCharacter } from '../document/character.js'; -import { useRebar } from '../index.js'; - -const Rebar = useRebar(); export function useWeapon(player: alt.Player) { /** * Give and appy weapons to a player * - * @param {alt.IWeapon[]} weapons + * @param {(alt.IWeapon & { ammo: number })[])} weapons * @param {{ [hash: string]: number }} ammoData */ - function apply(weapons: alt.IWeapon[], ammoData: { [hash: string]: number }) { + function apply(weapons: (alt.IWeapon & { ammo: number })[]) { const lastWeapon = player.currentWeapon; player.removeAllWeapons(); for (let weapon of weapons) { - const ammo = ammoData[weapon.hash] ?? 0; - player.giveWeapon(weapon.hash, ammo, lastWeapon === weapon.hash); + player.giveWeapon(weapon.hash, weapon.ammo, lastWeapon === weapon.hash); player.setWeaponTintIndex(weapon.hash, weapon.tintIndex); for (let component of weapon.components) { player.addWeaponComponent(weapon.hash, component); @@ -34,14 +30,13 @@ export function useWeapon(player: alt.Player) { async function clear() { player.removeAllWeapons(); - const document = Rebar.document.character.useCharacter(player); + const document = useCharacter(player); if (!document.get()) { return; } await document.setBulk({ weapons: [], - ammo: {}, }); sync(); @@ -53,120 +48,211 @@ export function useWeapon(player: alt.Player) { * @param {string} model * @return */ - async function clearWeapon(model: string) { - const weaponHash = alt.hash(model); - const document = Rebar.document.character.useCharacter(player); - player.removeWeapon(weaponHash); + async function clearWeapon(model: string | number) { + if (typeof model === 'string') { + model = alt.hash(model); + } + + const document = useCharacter(player); + player.removeWeapon(model); if (!document.get()) { return; } const weapons = document.getField('weapons') ?? []; - const ammo = document.getField('ammo') ?? {}; - - const index = weapons.findIndex((x) => x.hash === weaponHash); + const index = weapons.findIndex((x) => x.hash === model); if (index >= 0) { weapons.splice(index, 1); } - if (ammo[weaponHash]) { - delete ammo[weaponHash]; - } + await document.set('weapons', weapons); + sync(); + } - await document.setBulk({ - weapons, - ammo, - }); + /** + * Get all weapons that the character has currently + * + * @return + */ + function getWeapons() { + const document = useCharacter(player); + if (!document.get()) { + return []; + } - sync(); + const weapons = document.getField('weapons') ?? []; + return weapons; } /** * Add a weapon to the player * - * @param {string} model + * @param {string | number} model * @param {number} ammo * @return */ - async function add(model: string, ammoCount: number) { - const document = Rebar.document.character.useCharacter(player); + async function add(model: string | number, ammoCount: number) { + const document = useCharacter(player); player.giveWeapon(model, ammoCount, true); if (!document.get()) { return; } const weapons = document.getField('weapons') ?? []; - const ammo = document.getField('ammo') ?? {}; - - weapons.push({ components: [], hash: alt.hash(model), tintIndex: 0 }); - ammo[alt.hash(model)] = ammoCount; - await document.setBulk({ - weapons, - ammo, + weapons.push({ + components: [], + hash: typeof model === 'string' ? alt.hash(model) : model, + tintIndex: 0, + ammo: ammoCount, }); + await document.set('weapons', weapons); sync(); } /** * Add ammo for the current given weapon, and save to the database * - * @param {string} model + * @param {string | number} model * @param {number} ammoCount * @return */ - async function addAmmo(model: string, ammoCount: number) { - const document = Rebar.document.character.useCharacter(player); + async function addAmmo(model: string | number, ammoCount: number) { + const document = useCharacter(player); if (!document.get()) { + return false; + } + + const modelHash = typeof model === 'string' ? alt.hash(model) : model; + const weapons = document.getField('weapons') ?? []; + + const index = weapons.findIndex((x) => x.hash === modelHash); + if (index <= -1) { + return false; + } + + weapons[index].ammo += ammoCount; + player.setWeaponAmmo(model, weapons[index].ammo); + document.set('weapons', weapons); + return true; + } + + /** + * Add a component to the specified weapon and save to the database + * + * @param {string | number} model + * @param {number} component + * @return + */ + async function addWeaponComponent(model: string | number, component: number) { + const document = useCharacter(player); + if (!document.get()) { + return; + } + + const weaponHash = typeof model === 'string' ? alt.hash(model) : model; + const weapons = document.getField('weapons') ?? []; + + const weaponIndex = weapons.findIndex((w) => w.hash === weaponHash); + if (weaponIndex === -1) { return; } - const ammo = document.getField('ammo') ?? {}; - if (ammo[alt.hash(model)]) { - ammo[alt.hash(model)] += ammoCount; - } else { - ammo[alt.hash(model)] = ammoCount; + const updatedWeapons = [...weapons]; + const weapon = { ...updatedWeapons[weaponIndex] }; + + if (!weapon.components.includes(component)) { + weapon.components = [...weapon.components, component]; } - player.setWeaponAmmo(model, ammo[alt.hash(model)]); - document.set('ammo', ammo); + updatedWeapons[weaponIndex] = weapon; + + player.addWeaponComponent(weaponHash, component); + + await document.set('weapons', updatedWeapons); } /** - * Save current player weapons + * Remove a component from the specified weapon and save to the database + * + * @param {number | string} model + * @param {number} component + * @return */ - function save() { - const document = Rebar.document.character.useCharacter(player); + async function removeWeaponComponent(model: number | string, component: number) { + const document = useCharacter(player); if (!document.get()) { return; } - const ammo: { [key: string]: number } = {}; - for (let weapon of player.weapons) { - ammo[weapon.hash] = player.getAmmo(weapon.hash); + const weaponHash = typeof model === 'string' ? alt.hash(model) : model; + const weapons = document.getField('weapons') ?? []; + + const weaponIndex = weapons.findIndex((w) => w.hash === weaponHash); + if (weaponIndex === -1) { + return; } - document.setBulk({ - weapons: player.weapons, - ammo: ammo, - }); + const updatedWeapons = [...weapons]; + const weapon = { ...updatedWeapons[weaponIndex] }; + + const componentIndex = weapon.components.indexOf(component); + if (componentIndex !== -1) { + weapon.components = [ + ...weapon.components.slice(0, componentIndex), + ...weapon.components.slice(componentIndex + 1), + ]; + } + + updatedWeapons[weaponIndex] = weapon; + + player.removeWeaponComponent(weaponHash, component); + + await document.set('weapons', updatedWeapons); } - function saveAmmo() { - const document = Rebar.document.character.useCharacter(player); + /** + * Save current player weapons + */ + function save() { + const document = useCharacter(player); if (!document.get()) { return; } - const ammo: { [key: string]: number } = {}; + const weapons = document.getField('weapons'); + if (!weapons || weapons.length <= 0) { + return; + } + for (let weapon of player.weapons) { - ammo[weapon.hash] = player.getAmmo(weapon.hash); + const index = weapons.findIndex((x) => x.hash === weapon.hash); + if (index <= -1) { + continue; + } + + const { ammoTypeHash } = alt.getWeaponModelInfoByHash(weapon.hash); + weapons[index].ammo = player.getAmmo(ammoTypeHash); } - document.set('ammo', ammo); + document.set('weapons', weapons); } + /** + * Calls the `save` function + * + * @deprecated + */ + function saveAmmo() { + save(); + } + + /** + * Synchronize the weapons the player currently has + * + * @return + */ function sync() { const document = useCharacter(player); const data = document.get(); @@ -174,15 +260,18 @@ export function useWeapon(player: alt.Player) { return; } - apply(data.weapons, data.ammo); + apply(data.weapons); } return { add, addAmmo, + addWeaponComponent, + removeWeaponComponent, apply, clear, clearWeapon, + getWeapons, save, saveAmmo, sync, diff --git a/src/main/server/player/webview.ts b/src/main/server/player/webview.ts index 419487730..123e2778f 100644 --- a/src/main/server/player/webview.ts +++ b/src/main/server/player/webview.ts @@ -1,7 +1,7 @@ import * as alt from 'alt-server'; import { Events } from '@Shared/events/index.js'; import { PageNames, PageType } from '@Shared/webview/index.js'; -import { PageInfo } from '@Shared/types/webview.js'; +import {PageInfo} from '@Shared/types/index.js'; const SessionKeys = { WebviewPage: 'webview:page', diff --git a/src/main/server/player/world.ts b/src/main/server/player/world.ts index 5843c4ceb..bf1727e9e 100644 --- a/src/main/server/player/world.ts +++ b/src/main/server/player/world.ts @@ -191,12 +191,18 @@ export function useWorld(player: alt.Player) { zone: string; crossingRoad: string; }> { - if (!player || !player.valid) return; + if (!player || !player.valid) { + return undefined; + } + return await player.emitRpc(Events.systems.world.pointDetails, point); } async function getTravelDistance(point1: alt.Vector3, point2: alt.Vector3 = undefined): Promise { - if (!player || !player.valid) return; + if (!player || !player.valid) { + return 0; + } + point2 = point2 ?? player.pos; const result = await player.emitRpc(Events.systems.world.travelDistance, point1, point2); return result; diff --git a/src/main/server/rpc/index.ts b/src/main/server/rpc/index.ts index d44b30fd3..f8b2de7d6 100644 --- a/src/main/server/rpc/index.ts +++ b/src/main/server/rpc/index.ts @@ -21,7 +21,7 @@ if (alt.debug) { app.route('/transmitter', Transmitter.get()); } -alt.on('on-rpc-restart', () => { +alt.on('rebar:rpcRestart', () => { if (!server) { return; } @@ -30,7 +30,6 @@ alt.on('on-rpc-restart', () => { server.close(); }); - function init() { if (server) return; server = serve({ fetch: app.fetch, port: 8787 }); @@ -49,10 +48,10 @@ export function useHono() { if (c.env.incoming.socket.remoteAddress !== '127.0.0.1') { c.status(403); return c.json({ data: 'Unauthorized' }); - } - return await next(); - } - } + } + return await next(); + }, + }; return { addRouter, middlewares }; } diff --git a/src/main/server/rpc/server/index.ts b/src/main/server/rpc/server/index.ts index dcbde1f03..a6b3617c3 100644 --- a/src/main/server/rpc/server/index.ts +++ b/src/main/server/rpc/server/index.ts @@ -1,15 +1,12 @@ import * as alt from 'alt-server'; import { Hono } from 'hono'; import { type HttpBindings } from '@hono/node-server'; -import { useRebar } from '../../index.js'; const app = new Hono<{ Bindings: HttpBindings }>(); -const Rebar = useRebar(); -const RebarEvents = Rebar.events.useEvents(); declare module 'alt-server' { export interface ICustomEmitEvent { - 'on-rpc-restart': () => void; + 'rebar:rpcRestart': () => void; } } @@ -25,7 +22,7 @@ app.get('/restart', (c) => { player.kick(); } - alt.emit('on-rpc-restart'); + alt.emit('rebar:rpcRestart'); c.status(200); return c.json({ data: 'ok' }); @@ -40,7 +37,7 @@ app.get('/reload', async (c) => { } alt.log(`RPC - Restarting Resource '${resource}'`); - alt.emit('on-rpc-restart'); + alt.emit('rebar:rpcRestart'); await alt.Utils.wait(500); diff --git a/src/main/server/services/currency.ts b/src/main/server/services/currency.ts new file mode 100644 index 000000000..882760617 --- /dev/null +++ b/src/main/server/services/currency.ts @@ -0,0 +1,77 @@ +import * as alt from 'alt-server'; +import { useServiceRegister } from './index.js'; + +export interface CurrencyService { + /** + * Called when you want to add a currency to a player + * + * @memberof CurrencyService + */ + add: (player: alt.Player, type: string, quantity: number) => Promise; + + /** + * Called when you want to remove a currency from a player + * + * @memberof CurrencyService + */ + sub: (player: alt.Player, type: string, quantity: number) => Promise; + + /** + * Called when you want to check if the player has enough currency + * + * @memberof CurrencyService + */ + has: (player: alt.Player, type: string, quantity: number) => Promise; +} + +declare global { + interface RebarServices { + currencyService: CurrencyService; + } +} + +declare module 'alt-server' { + export interface ICustomEmitEvent { + 'rebar:playerCurrencyAdd': (...args: Parameters) => void; + 'rebar:playerCurrencySub': (...args: Parameters) => void; + } +} + +export function useCurrencyService() { + return { + add(...args: Parameters) { + const service = useServiceRegister().get('currencyService'); + if (!service || !service.add) { + return false; + } + + const result = service.add(...args); + if (result) { + alt.emit('rebar:playerCurrencyAdd', ...args); + } + + return result; + }, + sub(...args: Parameters) { + const service = useServiceRegister().get('currencyService'); + if (!service || !service.sub) { + return false; + } + + const result = service.sub(...args); + if (result) { + alt.emit('rebar:playerCurrencySub', ...args); + } + + return result; + }, + has(...args: Parameters) { + const service = useServiceRegister().get('currencyService'); + if (!service || !service.has) { + return false; + } + + return service.has(...args); + }, + }; +} diff --git a/src/main/server/services/death.ts b/src/main/server/services/death.ts new file mode 100644 index 000000000..03c66f5cf --- /dev/null +++ b/src/main/server/services/death.ts @@ -0,0 +1,52 @@ +import * as alt from 'alt-server'; +import { useServiceRegister } from './index.js'; + +export interface DeathService { + /** + * Called when a player is respawned in a new location + * + * @memberof DeathService + */ + respawn: (player: alt.Player, pos: alt.Vector3) => void; + + /** + * Called when a player is revived in the same location + * + * @memberof DeathService + */ + revive: (player: alt.Player) => void; +} + +declare global { + interface RebarServices { + deathService: DeathService; + } +} + +declare module 'alt-server' { + export interface ICustomEmitEvent { + 'rebar:playerRespawn': (...args: Parameters) => void; + 'rebar:playerRevive': (...args: Parameters) => void; + } +} + +export function useDeathService() { + return { + respawn(...args: Parameters) { + const service = useServiceRegister().get('deathService'); + if (service && service.respawn) { + service.respawn(...args); + } + + alt.emit('rebar:playerRespawn', ...args); + }, + revive(...args: Parameters) { + const service = useServiceRegister().get('deathService'); + if (service && service.respawn) { + service.revive(...args); + } + + alt.emit('rebar:playerRevive', ...args); + }, + }; +} diff --git a/src/main/server/services/index.ts b/src/main/server/services/index.ts new file mode 100644 index 000000000..99e11e272 --- /dev/null +++ b/src/main/server/services/index.ts @@ -0,0 +1,68 @@ +declare global { + interface RebarServices {} +} + +const Services: Map = new Map(); + +export function useServiceRegister() { + /** + * Add a service handler with a service name, and service handlers + * + * @template K + * @param {K} serviceName + * @param {RebarServices[K]} serviceHandler + * @return + */ + function register(serviceName: K, serviceHandler: RebarServices[K]) { + if (Services.has(serviceName)) { + throw new Error( + `Service ${serviceName} is already registered. Services should only have one library handler.`, + ); + } + + Services.set(serviceName, serviceHandler); + + return { + remove: () => { + remove(serviceName); + }, + }; + } + + /** + * Remove a service handler by service name + * + * @template K + * @param {K} serviceName + * @param {string} uid + * @return + */ + function remove(serviceName: K) { + if (!Services.has(serviceName)) { + return; + } + + Services.delete(serviceName); + } + + /** + * Get all services by service name + * + * @template K + * @param {K} serviceName + * @return + */ + function get(serviceName: K) { + if (!Services.has(serviceName)) { + return undefined as Partial; + } + + return Services.get(serviceName) as Partial; + } + + return { + register, + get, + remove, + }; +} diff --git a/src/main/server/services/items.ts b/src/main/server/services/items.ts new file mode 100644 index 000000000..708ea1c9c --- /dev/null +++ b/src/main/server/services/items.ts @@ -0,0 +1,185 @@ +import * as alt from 'alt-server'; +import { useServiceRegister } from './index.js'; +import { Item, RebarBaseItem } from '@Shared/types/items.js'; + +export interface ItemService { + /** + * Add an item to the given entity with a given quantity based on a common id + * + * Additionally, a sobject data may be passed if necessary. + * + * @memberof ItemService + */ + add: (entity: alt.Entity, id: string, quantity: number, data?: any) => Promise; + + /** + * Subtract an item quanatiy from the given entity with a given quantity based on a common id + * + * @memberof ItemService + */ + sub: (entity: alt.Entity, id: string, quantity: number) => Promise; + + /** + * Check if the entity has enough of an item based on a common id + * + * @memberof ItemService + */ + has: (entity: alt.Entity, id: string, quantity: number) => Promise; + + /** + * Remove an item from the entity based on a unique identifier + * + * @memberof ItemService + */ + remove: (entity: alt.Entity, uid: string) => Promise; + + /** + * Use an item if it can be used, based on unique identifier + * + * @param entity + * @param uid + * @returns + */ + use: (entity: alt.Entity, uid: string) => Promise; + + /** + * Verify if an entity has enough space to store the item + * + * @memberof ItemService + */ + hasSpace: (entity: alt.Entity, item: Item) => Promise; + + /** + * Create an item in the database if it does not exist + * + * @memberof ItemService + */ + itemCreate: (data: RebarBaseItem) => Promise; + + /** + * Remove an item from the database if it exists + * + * @memberof ItemService + */ + itemRemove: (id: keyof RebarItems) => Promise; +} + +declare global { + interface RebarServices { + itemService: ItemService; + } +} + +declare module 'alt-server' { + export interface ICustomEmitEvent { + 'rebar:entityItemAdd': (entity: alt.Entity, id: string, quantity: number, data?: any) => void; + 'rebar:entityItemSub': (entity: alt.Entity, id: string, quantity: number) => void; + 'rebar:entityItemRemove': (entity: alt.Entity, uid: string) => void; + 'rebar:entityItemUse': (entity: alt.Entity, uid: string) => void; + } +} + +export function useItemService() { + async function add(...args: Parameters) { + const service = useServiceRegister().get('itemService'); + if (!service || !service.add) { + return false; + } + + const result = await service.add(...args); + if (result) { + alt.emit('rebar:entityItemAdd', ...args); + } + + return result; + } + + async function sub(...args: Parameters) { + const service = useServiceRegister().get('itemService'); + if (!service || !service.sub) { + return false; + } + + const result = await service.sub(...args); + if (result) { + alt.emit('rebar:entityItemSub', ...args); + } + + return result; + } + + async function has(...args: Parameters) { + const service = useServiceRegister().get('itemService'); + if (!service || !service.has) { + return false; + } + + return await service.has(...args); + } + + async function remove(...args: Parameters) { + const service = useServiceRegister().get('itemService'); + if (!service || !service.remove) { + return false; + } + + const result = await service.remove(...args); + if (result) { + alt.emit('rebar:entityItemRemove', ...args); + } + + return result; + } + + async function use(...args: Parameters) { + const service = useServiceRegister().get('itemService'); + if (!service || !service.use) { + return false; + } + + const result = await service.use(...args); + if (result) { + alt.emit('rebar:entityItemUse', ...args); + } + + return result; + } + + async function hasSpace(...args: Parameters) { + const service = useServiceRegister().get('itemService'); + if (!service || !service.hasSpace) { + return false; + } + + return await service.hasSpace(...args); + } + + async function itemCreate(...args: Parameters) { + const service = useServiceRegister().get('itemService'); + if (!service || !service.itemCreate) { + return; + } + + await service.itemCreate(...args); + } + + async function itemRemove(...args: Parameters) { + const service = useServiceRegister().get('itemService'); + if (!service || !service.itemRemove) { + return; + } + + await service.itemRemove(...args); + } + + return { + add, + sub, + has, + hasSpace, + itemCreate, + itemRemove, + remove, + use, + }; +} diff --git a/src/main/server/services/notifications.ts b/src/main/server/services/notifications.ts new file mode 100644 index 000000000..315b89956 --- /dev/null +++ b/src/main/server/services/notifications.ts @@ -0,0 +1,66 @@ +import * as alt from 'alt-server'; +import { useServiceRegister } from './index.js'; +import { Events } from '@Shared/events/index.js'; + +export interface NotificationService { + /** + * emit a notification to player + * + * @memberof NotificationService + */ + emit: (player: alt.Player, msg: string, type?: string) => void; + + /** + * Send a notification to all players + * + * @memberof NotificationService + */ + broadcast: (msg: string, type?: string) => void; +} + +declare global { + interface RebarServices { + notificationService: NotificationService; + } +} + +declare module 'alt-server' { + export interface ICustomEmitEvent { + 'rebar:playerEmitNotification': (...args: Parameters) => void; + 'rebar:broadcastNotification': (...args: Parameters) => void; + } +} + +export function useNotificationService() { + return { + emit(...args: Parameters) { + const service = useServiceRegister().get('notificationService'); + const player = args[0]; + if (!player || !player.valid) { + return; + } + + if (service && service.emit) { + service.emit(...args); + } else { + args.shift(); + player.emit(Events.player.notify.notification.create, ...args); + } + + alt.emit('rebar:playerEmitNotification', ...args); + }, + broadcast(...args: Parameters) { + const service = useServiceRegister().get('notificationService'); + + if (service && service.broadcast) { + service.broadcast(...args); + } else { + for (let player of alt.Player.all) { + player.emit(Events.player.notify.notification.create, ...args); + } + } + + alt.emit('rebar:broadcastNotification', ...args); + }, + }; +} diff --git a/src/main/server/services/time.ts b/src/main/server/services/time.ts new file mode 100644 index 000000000..c01517cb4 --- /dev/null +++ b/src/main/server/services/time.ts @@ -0,0 +1,74 @@ +import * as alt from 'alt-server'; + +const DEFAULT_TIME = { hour: 8, minute: 0, second: 0 }; + +export interface TimeService { + /** + * Set the current time in-game for the server + * + * @memberof TimeService + */ + setTime: (hour: number, minute: number, second: number) => void; +} + +declare global { + interface RebarServices { + timeService: TimeService; + } +} + +declare module 'alt-server' { + export interface ICustomGlobalMeta { + serverTime: { hour: number; minute: number; second: number }; + } + + export interface ICustomEmitEvent { + 'rebar:timeChanged': (hour: number, minute: number, second: number) => void; + 'rebar:timeSecondChanged': (minute: number) => void; + 'rebar:timeMinuteChanged': (minute: number) => void; + 'rebar:timeHourChanged': (hour: number) => void; + } +} + +export function useTimeService() { + return { + setTime(hour: number, minute: number, second: number) { + if (hour >= 24 || hour < 0) { + hour = 0; + } + + if (minute >= 60 || minute < 0) { + minute = 0; + } + + if (second >= 60 || second < 0) { + second = 0; + } + + const time = alt.hasMeta('serverTime') ? alt.getMeta('serverTime') : DEFAULT_TIME; + + if (time.hour !== hour) { + alt.emit('rebar:timeHourChanged', hour); + } + + if (time.minute !== minute) { + alt.emit('rebar:timeMinuteChanged', minute); + } + + if (time.second !== second) { + alt.emit('rebar:timeSecondChanged', second); + } + + time.hour = hour; + time.minute = minute; + time.second = second; + alt.setMeta('serverTime', time); + alt.emit('rebar:timeChanged', time.hour, time.minute, time.second); + }, + getTime() { + return alt.getMeta('serverTime'); + }, + }; +} + +alt.setMeta('serverTime', DEFAULT_TIME); diff --git a/src/main/server/services/weather.ts b/src/main/server/services/weather.ts new file mode 100644 index 000000000..16877e24e --- /dev/null +++ b/src/main/server/services/weather.ts @@ -0,0 +1,74 @@ +import * as alt from 'alt-server'; +import { useServiceRegister } from './index.js'; +import { Weathers } from '@Shared/data/weathers.js'; + +const DEFAULT_WEATHER: Weathers = 'EXTRASUNNY'; +const DEFAULT_WEATHER_FORECAST: Weathers[] = ['EXTRASUNNY', 'CLOUDS', 'RAIN', 'THUNDER', 'CLOUDS', 'CLEARING']; + +export interface WeatherService { + /** + * Set the current weather for a player + * + * @memberof WorldService + */ + setWeather: (type: Weathers) => void; + + /** + * Set the current weather forecast + * + * @memberof WorldService + */ + setWeatherForecast: (types: Weathers[]) => void; +} + +declare global { + interface RebarServices { + weatherService: WeatherService; + } +} + +declare module 'alt-server' { + export interface ICustomGlobalMeta { + serverWeather: Weathers; + serverWeatherForecast: Weathers[]; + } + + export interface ICustomEmitEvent { + 'rebar:weatherForecastChanged': (weather: Weathers[]) => void; + 'rebar:weatherChanged': (weather: Weathers) => void; + } +} + +export function useWeatherService() { + return { + setWeatherForecast(weathers: Weathers[]) { + const service = useServiceRegister().get('weatherService'); + + alt.setMeta('serverWeatherForecast', weathers); + alt.emit('rebar:weatherForecastChanged', weathers); + + if (service && service.setWeatherForecast) { + service.setWeatherForecast(weathers); + } + }, + setWeather(weatherType: Weathers) { + const service = useServiceRegister().get('weatherService'); + + alt.setMeta('serverWeather', weatherType); + alt.emit('rebarweatherChanged', weatherType); + + if (service && service.setWeather) { + service.setWeather(weatherType); + } + }, + getWeather() { + return alt.getMeta('serverWeather'); + }, + getWeatherForecast() { + return alt.getMeta('serverWeatherForecast'); + }, + }; +} + +alt.setMeta('serverWeather', DEFAULT_WEATHER); +alt.setMeta('serverWeatherForecast', DEFAULT_WEATHER_FORECAST); diff --git a/src/main/server/startup.ts b/src/main/server/startup.ts index 4bb6705b7..2ff71b8f6 100644 --- a/src/main/server/startup.ts +++ b/src/main/server/startup.ts @@ -5,10 +5,10 @@ import { useTranslate } from '@Shared/translate.js'; import { useConfig } from './config/index.js'; import { useDatabase } from './database/index.js'; import './rpc/index.js'; +import './systems/tick.js'; import { useRebar } from './index.js'; const Rebar = useRebar(); -const RebarEvents = Rebar.events.useEvents(); const config = useConfig(); const database = useDatabase(); const { t } = useTranslate('en'); @@ -24,6 +24,7 @@ async function handleStart() { await import('./plugins.js'); alt.log(':: Plugins Loaded'); } catch (err) { + alt.logError(err); alt.logWarning(`Failed to load any plugins, a plugin has errors in it.`); } @@ -47,7 +48,7 @@ async function handleStart() { player.frozen = false; const character = rPlayer.character.get(); - RebarEvents.invoke('character-bound', player, character); + alt.emit('rebar:playerCharacterBound', player, character); } } diff --git a/src/main/server/systems/messenger.ts b/src/main/server/systems/messenger.ts index 0e0e3ad83..19f0811dc 100644 --- a/src/main/server/systems/messenger.ts +++ b/src/main/server/systems/messenger.ts @@ -1,16 +1,16 @@ import * as alt from 'alt-server'; -import { useRebar } from '../index.js'; -import { Events } from '../../shared/events/index.js'; -import { Message } from '../../shared/types/message.js'; - -export type PlayerMessageCallback = (player: alt.Player, msg: string) => void; - -export type PermissionOptions = { - permissions?: string[]; - accountPermissions?: string[]; - groups?: Record; - accountGroups?: Record; -}; +import { useWebview } from '../player/webview.js'; +import { useStatus } from '../player/status.js'; +import {Events} from '@Shared/events/index.js'; +import {Message, PermissionOptions} from '@Shared/types/index.js'; +import {useEntityPermissions} from "@Server/systems/permissions/entityPermissions.js"; + +declare module 'alt-server' { + export interface ICustomEmitEvent { + 'rebar:playerSendMessage': (player: alt.Player, msg: string) => void; + 'rebar:playerCommand': (player: alt.Player, commandName: string) => void; + } +} export type Command = { name: string; @@ -37,52 +37,9 @@ const tagOrComment = new RegExp( 'gi', ); -const Rebar = useRebar(); -const RebarEvents = Rebar.events.useEvents(); - const commands: Command[] = []; -const callbacks: PlayerMessageCallback[] = []; let endCommandRegistrationTime = Date.now(); -/** - * - * @param {alt.Player} player Player to check permissions for. - * @param {PermissionOptions} options Permission options to check against. - * @returns {boolean} True if the player has access to the functionality. - */ -function isAvailableForPlayer(player: alt.Player, options?: PermissionOptions): boolean { - if (!player.valid) return false; - if (!options) return true; - if ( - !options.accountPermissions && - !options.permissions && - !options.groups && - !options.accountGroups - ) { - return true; - } - - const rPlayer = Rebar.usePlayer(player); - - if (rPlayer.account.groupPermissions.hasAtLeastOneGroupWithSpecificPerm(options.accountGroups)) { - return true; - } - - if (rPlayer.character.groupPermissions.hasAtLeastOneGroupWithSpecificPerm(options.groups)) { - return true; - } - - if (rPlayer.account.permissions.hasAnyPermission(options.accountPermissions)) { - return true; - } - - if (rPlayer.character.permissions.hasAnyPermission(options.permissions)) { - return true; - } - - return false; -} - export function useMessenger() { function registerCommand(command: Command) { command.name = command.name.replaceAll('/', ''); @@ -97,12 +54,8 @@ export function useMessenger() { endCommandRegistrationTime = Date.now() + 5000; } - function onMessage(cb: PlayerMessageCallback) { - callbacks.push(cb); - } - function hasCommandPermission(player: alt.Player, command: Command) { - return isAvailableForPlayer(player, command.options); + return useEntityPermissions(command.options).check(player); } async function invokeCommand(player: alt.Player, cmdName: string, ...args: any[]): Promise { @@ -121,7 +74,7 @@ export function useMessenger() { try { await command.callback(player, ...args); - RebarEvents.invoke('on-command', player, cmdName); + alt.emit('rebar:playerCommand', player, cmdName); return true; } catch (err) { return false; @@ -129,13 +82,13 @@ export function useMessenger() { } function sendMessage(player: alt.Player, message: Message) { - const webview = Rebar.player.useWebview(player); + const webview = useWebview(player); webview.emit(Events.systems.messenger.send, message); } function broadcastMessage(message: Message, options?: PermissionOptions) { for (const player of alt.Player.all) { - if (isAvailableForPlayer(player, options)) { + if (!options || useEntityPermissions(options).check(player)) { sendMessage(player, message); } } @@ -174,7 +127,6 @@ export function useMessenger() { hasCommandPermission, }, message: { - on: onMessage, send: sendMessage, broadcast: broadcastMessage, }, @@ -207,18 +159,14 @@ function processMessage(player: alt.Player, msg: string) { return; } - if (!Rebar.player.useStatus(player).hasCharacter()) { + if (!useStatus(player).hasCharacter()) { return; } const messageSystem = useMessenger(); if (msg.charAt(0) !== '/') { msg = cleanMessage(msg); - for (let cb of callbacks) { - cb(player, msg); - } - - Rebar.events.useEvents().invoke('message', player, msg); + alt.emit('rebar:playerSendMessage', player, msg); return; } diff --git a/src/main/server/systems/pageSystem.ts b/src/main/server/systems/pageSystem.ts new file mode 100644 index 000000000..dba2884f7 --- /dev/null +++ b/src/main/server/systems/pageSystem.ts @@ -0,0 +1,20 @@ +import * as alt from 'alt-server'; +import { Events } from '@Shared/events/index.js'; +import { PageNames } from '@Shared/webview/index.js'; + +declare module 'alt-server' { + export interface ICustomEmitEvent { + 'rebar:playerPageClosed': (player: alt.Player, page: PageNames) => void; + 'rebar:playerPageOpened': (player: alt.Player, page: PageNames) => void; + } +} + +// Listens for when a page is opened +alt.onClient(Events.view.onPageOpen, (player: alt.Player, pageName: PageNames) => + alt.emit('rebar:playerPageOpened', player, pageName), +); + +// Listens for when a page is closed +alt.onClient(Events.view.onPageClose, (player: alt.Player, pageName: PageNames) => + alt.emit('rebar:playerPageClosed', player, pageName), +); diff --git a/src/main/server/systems/permission.ts b/src/main/server/systems/permission.ts deleted file mode 100644 index 96eafeaeb..000000000 --- a/src/main/server/systems/permission.ts +++ /dev/null @@ -1,385 +0,0 @@ -import * as alt from 'alt-server'; -import * as document from '../document/index.js'; -import { Account } from 'main/shared/types/account.js'; -import { Character } from 'main/shared/types/character.js'; -import { useDatabase } from '@Server/database/index.js'; -import { CollectionNames } from '@Server/document/shared.js'; - -const { getMany } = useDatabase(); -const documentType = { - account: document.useAccount, - character: document.useCharacter, - vehicle: document.useVehicle, -}; - -export type DefaultPerms = 'admin' | 'moderator'; -export type SupportedDocuments = 'account' | 'character'; - -const InternalFunctions = { - /** - * Add a permission to a player based on default permissions, or a custom permission. - * - * @template CustomPerms - * @param {alt.Player} player An alt:V Player Entity - * @param {(DefaultPerms | CustomPerms)} perm - */ - async add( - player: alt.Player, - perm: DefaultPerms | CustomPerms, - dataName: SupportedDocuments, - ): Promise { - if (typeof documentType[dataName] === 'undefined') { - alt.logWarning(`Athena.document.${dataName} is not a supported document type.`); - return false; - } - - const document = documentType[dataName](player); - const data = document.get(); - if (typeof data === 'undefined') { - return false; - } - - if (!data.permissions) { - data.permissions = []; - } - - const formattedPerm = String(perm).toLowerCase(); - const index = data.permissions.findIndex((x) => x === formattedPerm); - if (index >= 0) { - return false; - } - - data.permissions.push(formattedPerm); - await document.set('permissions', data.permissions); - return true; - }, - /** - * Remove a permission from a player based on default permissions, or a custom permission. - * - * @template CustomPerms - * @param {alt.Player} player An alt:V Player Entity - * @param {(DefaultPerms | CustomPerms)} perm - */ - async remove( - player: alt.Player, - perm: DefaultPerms | CustomPerms, - dataName: SupportedDocuments, - ): Promise { - if (typeof documentType[dataName] === 'undefined') { - alt.logWarning(`Athena.document.${dataName} is not a supported document type.`); - return false; - } - - const document = documentType[dataName](player); - const data = document.get(); - if (typeof data === 'undefined') { - return false; - } - - if (!data.permissions) { - data.permissions = []; - } - - const formattedPerm = String(perm).toLowerCase(); - const index = data.permissions.findIndex((x) => x === formattedPerm); - if (index <= -1) { - return false; - } - - data.permissions.splice(index, 1); - await document.set('permissions', data.permissions); - return true; - }, - /** - * Clear all permissions from a player's account. - * - * @param {alt.Player} player An alt:V Player Entity - * @return {void} - */ - async clear(player: alt.Player, dataName: SupportedDocuments) { - if (typeof documentType[dataName] === 'undefined') { - alt.logWarning(`Athena.document.${dataName} is not a supported document type.`); - return; - } - - const document = documentType[dataName](player); - const data = document.get(); - if (typeof data === 'undefined') { - return; - } - - data.permissions = []; - await document.set('permissions', data.permissions); - }, - /** - * Check if a player has a permission. - * - * @template CustomPerms - * @param {alt.Player} player An alt:V Player Entity - * @param {(DefaultPerms | CustomPerms)} perm - * @return {boolean} - */ - has(player: alt.Player, perm: DefaultPerms | CustomPerms, dataName: SupportedDocuments): boolean { - if (typeof documentType[dataName] === 'undefined') { - alt.logWarning(`Athena.document.${dataName} is not a supported document type.`); - return false; - } - - const document = documentType[dataName](player); - const data = document.get(); - if (typeof data === 'undefined' || typeof data.permissions === 'undefined') { - return false; - } - - if (data.permissions.length <= 0) { - return false; - } - - const formattedPerm = String(perm).toLowerCase(); - return data.permissions.findIndex((x) => x === formattedPerm) !== -1; - }, - /** - * Check if a player has at least one permission given an Array of permissions. - * - * @template CustomPerms - * @param {alt.Player} player An alt:V Player Entity - * @param {(Array)} perm - * @return {boolean} - */ - hasOne( - player: alt.Player, - perms: Array, - dataName: SupportedDocuments, - ): boolean { - if (typeof documentType[dataName] === 'undefined') { - alt.logWarning(`Athena.document.${dataName} is not a supported document type.`); - return false; - } - - if (!perms || perms.length <= 0) { - return false; - } - - const document = documentType[dataName](player); - const data = document.get(); - if (typeof data === 'undefined' || typeof data.permissions === 'undefined') { - return false; - } - - // Should return true if a permission is just an empty array. - // Should also check this before checking player perms if they haven't had any perms added yet. - if (perms.length <= 0) { - return true; - } - - if (data.permissions.length <= 0) { - return false; - } - - for (let perm of perms) { - const index = data.permissions.findIndex((x) => x === perm); - if (index <= -1) { - continue; - } - - return true; - } - - return false; - }, - /** - * Check if a player has all permissions in an array.. - * - * @template CustomPerms - * @param {alt.Player} player An alt:V Player Entity - * @param {(Array)} perms - * @return {boolean} - */ - hasAll( - player: alt.Player, - perms: Array, - dataName: SupportedDocuments, - ): boolean { - if (typeof documentType[dataName] === 'undefined') { - alt.logWarning(`Athena.document.${dataName} is not a supported document type.`); - return false; - } - - if (!perms || perms.length <= 0) { - return false; - } - - const document = documentType[dataName](player); - const data = document.get(); - if (typeof data === 'undefined' || typeof data.permissions === 'undefined') { - return false; - } - - // Should return true if a permission is just an empty array. - // Should also check this before checking player perms if they haven't had any perms added yet. - if (perms.length <= 0) { - return true; - } - - if (data.permissions.length <= 0) { - return false; - } - - for (let perm of perms) { - const index = data.permissions.findIndex((x) => x === perm); - if (index <= -1) { - return false; - } - - continue; - } - - return true; - }, -}; - -export function usePermission(player: alt.Player) { - /** - * Add a permission to an account or character. - * - * - * @template CustomPerms - * @param {('character' | 'account')} type - * @param {alt.Player} player An alt:V Player Entity - * @param {(DefaultPerms | CustomPerms)} perm - * @return {Promise} - */ - async function add( - type: 'character' | 'account', - perm: DefaultPerms | CustomPerms, - ): Promise { - return await InternalFunctions.add(player, perm, type); - } - - /** - * Remove a permission from an account or character. - * - * - * @template CustomPerms - * @param {('character' | 'account')} type - * @param {alt.Player} player An alt:V Player Entity - * @param {(DefaultPerms | CustomPerms)} perm - * @return {Promise} - */ - async function remove( - type: 'character' | 'account', - perm: DefaultPerms | CustomPerms, - ): Promise { - return await InternalFunctions.remove(player, perm, type); - } - - /** - * Clear all permissions for an account or character. - * - * - * @param {('character' | 'account')} type - * @return {Promise} - */ - async function clear(type: 'character' | 'account') { - return await InternalFunctions.clear(player, type); - } - - /** - * Check if a character or account has a single permission. - * - * - * @template CustomPerms - * @param {('character' | 'account')} type - * @param {(DefaultPerms | CustomPerms)} perm - * @return {boolean} - */ - function has(type: 'character' | 'account', perm: DefaultPerms | CustomPerms): boolean { - return InternalFunctions.has(player, perm, type); - } - - /** - * Check if a character or account has a atleast one permission. - * - * - * @template CustomPerms - * @param {('character' | 'account')} type - * @param {(Array)} perms - * @return {boolean} - */ - function hasOne( - type: 'character' | 'account', - perms: Array, - ): boolean { - return InternalFunctions.hasOne(player, perms, type); - } - - /** - * Check if a character or account has all the permissions. - * - * @template CustomPerms - * @param {('character' | 'account')} type - * @param {(Array)} perms - * @return {boolean} - */ - function hasAll( - type: 'character' | 'account', - perms: Array, - ): boolean { - return InternalFunctions.hasAll(player, perms, type); - } - - return { - add, - clear, - has, - hasOne, - hasAll, - remove, - }; -} - -/** - * Get all documents that have a specified permission in their permissions array. - * Will return an empty array if no permissions are found. - * - * @template CustomPerms - * @param {('character' | 'account')} type - * @param {(Array)} perms - */ -export async function getAll( - type: 'character' | 'account', - perm: DefaultPerms | CustomPerms, -): Promise | Array> { - const collectionName = type === 'character' ? CollectionNames.Characters : CollectionNames.Accounts; - const results = await getMany({ permissions: [String(perm)] }, collectionName); - return type === 'character' ? (results as Character[]) : (results as Account[]); -} - -export function getPermissions(entity: alt.Player, type: 'character' | 'account'); -export function getPermissions(entity: alt.Vehicle, type: 'vehicle'); -/** - * Get permissions for a given entity and type - * - * @param {alt.Entity} entity - * @param {('account' | 'character' | 'vehicle')} type - * @return {Array} - */ -export function getPermissions(entity: alt.Entity, type: 'account' | 'character' | 'vehicle'): Array { - let data; - switch (type) { - case 'account': - const accountDocument = documentType.account(entity as alt.Player); - data = accountDocument.get(); - return data.permissions ? data.permissions : []; - case 'character': - const characterDocument = documentType.character(entity as alt.Player); - data = characterDocument.get(); - return data.permissions ? data.permissions : []; - case 'vehicle': - const vehicleDocument = documentType.vehicle(entity as alt.Vehicle); - data = vehicleDocument.get(); - return data.permissions ? data.permissions : []; - default: - return []; - } -} diff --git a/src/main/server/systems/permissionGroup.ts b/src/main/server/systems/permissionGroup.ts deleted file mode 100644 index 5341c1cf5..000000000 --- a/src/main/server/systems/permissionGroup.ts +++ /dev/null @@ -1,232 +0,0 @@ -export interface PermissionGroup { - groups?: { [key: string]: Array }; -} - -export function usePermissionGroup(document: T & PermissionGroup) { - /** - * Add a group key to a document, and return the document. - * - * Document can be anything. - * - * #### Example - * ```ts - * const data = Athena.document.character.get(somePlayer); - * if (!data) { - * return; - * } - * - * const modifiedDocument = Athena.systems.permissionGroup.addGroupKey(data, 'police', 'police-chief'); - * await Athena.document.character.set(somePlayer, 'groups', modifiedDocument.groups); - * ``` - * - * @template T - * @param {string} groupName - * @param {(Array | string)} value - * @return {(T & PermissionGroup)} - */ - function addGroupPerm(groupName: string, value: Array | string): T & PermissionGroup { - if (!document.groups) { - document.groups = {}; - } - - if (!document.groups[groupName]) { - document.groups[groupName] = []; - } - - if (typeof value === 'string') { - value = [value]; - } - - for (let permission of value) { - const index = document.groups[groupName].findIndex((x) => x === permission); - if (index >= 0) { - continue; - } - - document.groups[groupName].push(permission); - } - - return document; - } - - /** - * Remove a permission from a group key. - * - * @export - * @param {string} groupName - * @param {string} value - * @return {(T & PermissionGroup)} - */ - function removeGroupPerm(groupName: string, value: Array | string): T & PermissionGroup { - if (!document.groups) { - return document; - } - - if (!document.groups[groupName]) { - return document; - } - - if (typeof value === 'string') { - value = [value]; - } - - for (let i = value.length - 1; i >= 0; i--) { - const permission = value[i]; - const index = document.groups[groupName].findIndex((x) => x === permission); - if (index <= -1) { - continue; - } - - document.groups[groupName].splice(i, 1); - } - - if (document.groups[groupName].length <= 0) { - delete document.groups[groupName]; - } - - return document; - } - - /** - * Removes a group entirely from a document. - * - * @export - * @param {string} groupName - * @return {*} - */ - function removeGroup(groupName: string) { - if (!document.groups) { - return document; - } - - delete document.groups[groupName]; - return document; - } - - /** - * Checks if the documet is part of a group. - * - * @param {string} groupName - * @return {boolean} - */ - function hasGroup(groupName: string): boolean { - if (!document.groups) { - return false; - } - - if (!document.groups[groupName]) { - return false; - } - - return true; - } - - /** - * Check if a document has a specific group permission. - * - * @param {string} groupName - * @param {string} permission - * @returns {boolean} - */ - function hasGroupPerm(groupName: string, permission: string): boolean { - if (!document.groups) { - return false; - } - - if (!document.groups[groupName]) { - return false; - } - - return document.groups[groupName].findIndex((x) => x === permission) >= 0; - } - - /** - * Check if a specific document has any of the listed permissions. - * - * @param {string} groupName - * @param {Array} permissions - */ - function hasAtLeastOneGroupPerm(groupName: string, permissions: Array): boolean { - if (!document.groups) { - return false; - } - - if (!document.groups[groupName]) { - return false; - } - - for (let perm of permissions) { - const result = hasGroupPerm(groupName, perm); - if (!result) { - continue; - } - - return true; - } - - return false; - } - - function hasAtLeastOneGroupWithSpecificPerm(groups: Record): boolean { - if (!document.groups) { - return false; - } - - for (const groupName in groups) { - if (!groups.hasOwnProperty(groupName)) { - continue; - } - - if (!document.groups[groupName]) { - continue; - } - - for (const permission of groups[groupName]) { - if (hasGroupPerm(groupName, permission)) { - return true; - } - } - } - - return false; - } - - return { - addGroupPerm, - hasAtLeastOneGroupPerm, - hasAtLeastOneGroupWithSpecificPerm, - hasGroup, - hasGroupPerm, - removeGroup, - removeGroupPerm, - usePermissionGroup, - }; -} - -/** - * Checks if the given documents have a common permission. - * - * @name hasCommonPermission - * @param {Array} documents - * @param {string} groupName - * @param {string} permission - * @returns {boolean} - */ -export function hasCommonPermission(documents: Array, groupName: string, permission: string) { - for (let document of documents) { - if (!document.groups) { - return false; - } - - if (!document.groups[groupName]) { - return false; - } - - const index = document.groups[groupName].findIndex((x) => x === permission); - if (index <= -1) { - return false; - } - } - - return true; -} diff --git a/src/main/server/systems/permissionProxy.ts b/src/main/server/systems/permissionProxy.ts deleted file mode 100644 index 528f601e0..000000000 --- a/src/main/server/systems/permissionProxy.ts +++ /dev/null @@ -1,200 +0,0 @@ -import * as alt from 'alt-server'; -import { usePermission } from './permission.js'; -import { usePermissionGroup } from './permissionGroup.js'; - -type DocumentType = 'character' | 'account'; -type DocumentGetter = () => Object | undefined; -type DocumentSetter = (fieldName: string, value: any) => Promise; - -/** - * Permission proxy for useCharacter/useAccount/useVehicle. - * - * @export - * @param {alt.Player} player - * @param {DocumentType} documentType Document type to handle permissions for. - * @param {DocumentGetter} getter Getter function to get the document. - * @param {DocumentSetter} setter Setter function to set the document. - */ -export function usePermissionProxy( - player: alt.Player, - documentType: DocumentType, - getter: DocumentGetter, - setter: DocumentSetter, -) { - const permissions = { - /** - * Proxies the add permission function. - * - * @param {string} permission - * @returns {Promise} - */ - addPermission: async (permission: string): Promise => { - if (!player.valid) return false; - const perm = usePermission(player); - return await perm.add(documentType, permission); - }, - - /** - * Proxies the remove permission function. - * - * @param {string} permission - * @returns {Promise} - */ - removePermission: async (permission: string): Promise => { - if (!player.valid) return false; - const perm = usePermission(player); - return await perm.remove(documentType, permission); - }, - - /** - * Proxies the has permission function. - * - * @param {string} permission - * @returns {boolean} - */ - hasPermission: (permission: string): boolean => { - if (!player.valid) return false; - const perm = usePermission(player); - return perm.has(documentType, permission); - }, - - /** - * Proxies the has all permission function. - * - * @param {string[]} permissions - * @returns {boolean} - */ - hasAllPermissions: (permissions: string[]): boolean => { - if (!player.valid) return false; - const perm = usePermission(player); - return perm.hasAll(documentType, permissions); - }, - - /** - * Proxies the has any permission function. - * - * @param {string[]} permissions - * @returns {boolean} - */ - hasAnyPermission: (permissions: string[]): boolean => { - if (!player.valid) return false; - const perm = usePermission(player); - return perm.hasOne(documentType, permissions); - }, - - /** - * Proxies the clear permissions function. - * - * @returns {void} - */ - clear: (): void => { - if (!player.valid) return; - const perm = usePermission(player); - perm.clear(documentType); - }, - }; - - const groupPermissions = { - /** - * Proxies the add group function. - * - * @param {string} groupName Group name to add. - * @param {string[]} permissions Permissions to add to group. - * @returns {Promise} Returns true if successful. - */ - addPermissions: async (groupName: string, permissions: string[] | string): Promise => { - const data = getter(); - if (typeof data === 'undefined') return false; - - if (typeof permissions === 'string') { - permissions = [permissions]; - } - - const perm = usePermissionGroup(data); - const updatedDocument = perm.addGroupPerm(groupName, permissions); - await setter('groups', updatedDocument.groups); - return true; - }, - /** - * Proxies the remove permission from group function. - * - * @param {string} groupName Group name to remove permission from. - * @param {string[]} permissions Permissions to remove from group. - * @returns {Promise} Returns true if successful. - */ - removePermissions: async (groupName: string, permissions: string[]): Promise => { - const data = getter(); - if (typeof data === 'undefined') return false; - const perm = usePermissionGroup(data); - const updatedDocument = perm.removeGroupPerm(groupName, permissions); - await setter('groups', updatedDocument.groups); - return true; - }, - /** - * Proxies the remove group function. - * - * @param {string} groupName Group name to remove. - * @returns {Promise} Returns true if successful. - */ - removeGroup: async (groupName: string): Promise => { - const data = getter(); - if (typeof data === 'undefined') return false; - const perm = usePermissionGroup(data); - const updatedDocument = perm.removeGroup(groupName); - await setter('groups', updatedDocument.groups); - return true; - }, - /** - * Proxies the has group function. - * - * @param {string} groupName Group name to check. - * @returns {boolean} Returns true if the group exists. - */ - hasGroup: (groupName: string): boolean => { - const data = getter(); - if (typeof data === 'undefined') return false; - const perm = usePermissionGroup(data); - return perm.hasGroup(groupName); - }, - /** - * Proxies the has group permission function. - * - * @param {string} groupName Group name to check. - * @param {string} permission Permission to check. - * @returns {boolean} Returns true if the group has the permission. - */ - hasGroupPerm: (groupName: string, permission: string): boolean => { - const data = getter(); - if (typeof data === 'undefined') return false; - const perm = usePermissionGroup(data); - return perm.hasGroupPerm(groupName, permission); - }, - /** - * Proxies the has at least one group permission function. - * - * @param {string} groupName Group name to check. - * @param {string[]} permissions Permissions to check. - * @returns {boolean} Returns true if the group has at least one permission. - */ - hasAtLeastOneGroupPerm: (groupName: string, permissions: string[]): boolean => { - const data = getter(); - if (typeof data === 'undefined') return false; - const perm = usePermissionGroup(data); - return perm.hasAtLeastOneGroupPerm(groupName, permissions); - }, - /** - * Proxies the has at least one group with specific permission function. - * - * @param {Record} groups Groups to check. - * @returns {boolean} Returns true if at least one group has specific permission. - */ - hasAtLeastOneGroupWithSpecificPerm: (groups: Record): boolean => { - const data = getter(); - if (typeof data === 'undefined') return false; - const perm = usePermissionGroup(data); - return perm.hasAtLeastOneGroupWithSpecificPerm(groups); - }, - } - - return { permissions, groupPermissions }; -} diff --git a/src/main/server/systems/permissions/entityPermissions.ts b/src/main/server/systems/permissions/entityPermissions.ts new file mode 100644 index 000000000..31b6cc22a --- /dev/null +++ b/src/main/server/systems/permissions/entityPermissions.ts @@ -0,0 +1,29 @@ +import * as alt from 'alt-server'; +import { Permission, PermissionOptions } from '@Shared/types/index.js'; +import { useAccount, useCharacter } from '@Server/document/index.js'; +import { usePermissions } from '@Server/systems/permissions/usePermissions.js'; + +function evaluatePermission(permission: Permission, hasPermission: (perm: string) => boolean): boolean { + if (typeof permission === 'string') { + return hasPermission(permission); + } else if (Array.isArray(permission)) { + return permission.every((perm) => hasPermission(perm)); + } else if ('and' in permission) { + return permission.and.every((perm) => evaluatePermission(perm, hasPermission)); + } else if ('or' in permission) { + return permission.or.some((perm) => evaluatePermission(perm, hasPermission)); + } + return false; +} + +export function useEntityPermissions(entity: T) { + function check(player: alt.Player): boolean { + const rCharacter = useCharacter(player); + const rAccount = useAccount(player); + if (!rAccount.isValid() && !rCharacter.isValid()) return false; + const permissions = usePermissions(player); + return evaluatePermission(entity.permissions, (perm) => permissions.hasPermission(perm)); + } + + return { check }; +} diff --git a/src/main/server/systems/permissions/groupPermissions.ts b/src/main/server/systems/permissions/groupPermissions.ts new file mode 100644 index 000000000..373de3a09 --- /dev/null +++ b/src/main/server/systems/permissions/groupPermissions.ts @@ -0,0 +1,175 @@ +import * as alt from 'alt-server'; +import { useGlobal } from '@Server/document/global.js'; +import { usePlayersGetter } from '@Server/getters/players.js'; +import { useAccount, useCharacter } from '@Server/document/index.js'; +import { useDatabase } from '@Server/database/index.js'; +import { CollectionNames } from '@Server/document/shared.js'; + +interface PermissionGroup { + permissions: Array; + inherits?: string; + version?: number; +} + +interface PermissionGroupConfig { + [groupName: string]: PermissionGroup; +} + +let initialized = false; +let permissionGroups: Awaited>>; +const permissionIndex: Map> = new Map(); +const database = useDatabase(); + +class InternalFunctions { + static async init() { + if (initialized) return; + initialized = true; + permissionGroups = await useGlobal('permissionGroups'); + this.buildIndex(); + } + + static addPermissionsToSet(groupName: string, permissions: Set, visited: Set = new Set()) { + if (visited.has(groupName)) { + throw new Error(`Cyclic dependency detected: ${[...visited, groupName].join(' -> ')}`); + } + visited.add(groupName); + + const group = permissionGroups.getField(groupName); + if (!group) return; + + for (const permission of group.permissions) { + permissions.add(permission); + } + if (group.inherits) { + this.addPermissionsToSet(group.inherits, permissions, new Set(visited)); + } + } + + static buildIndex() { + permissionIndex.clear(); + for (const [groupName, group] of Object.entries(permissionGroups.get())) { + if (['_id', 'identifier'].includes(groupName)) continue; + + const permissions = new Set(); + this.addPermissionsToSet(groupName, permissions); + permissionIndex.set(groupName, permissions); + } + } +} + +export function usePermissionGroup() { + async function add(groupName: string, options: PermissionGroup): Promise { + const existingGroup = permissionGroups.getField(groupName); + + if (existingGroup) { + if (!options.version && existingGroup.version) { + // We have no version in the new group, but we have a version in the existing group. + alt.logWarning( + `[Group: ${groupName}] Group was updated in runtime previously. Prefer to update the code to include all the changes in the group.`, + ); + return false; + } else if (options.version && existingGroup.version && options.version <= existingGroup.version) { + // The version is lower than the existing version. + if (options.version < existingGroup.version) { + alt.logWarning( + `[Group: ${groupName}] Group version is lower than the version in database. Prefer to update the code to include all the changes in the group.`, + ); + } + return false; + } + } + await permissionGroups.set(groupName, options); + InternalFunctions.buildIndex(); + return true; + } + + async function remove(groupName: string): Promise { + const existingGroup = permissionGroups.getField(groupName); + if (!existingGroup) { + return false; + } + await permissionGroups.unset(groupName); + + InternalFunctions.buildIndex(); + + // Remove the group from all accounts that are online. + const accountPromises = usePlayersGetter() + .memberOfGroup('account', groupName) + .map(async (player: alt.Player) => { + const account = useAccount(player); + await account.groups.remove(groupName); + }); + + // Remove the group from all characters that are online. + const characterPromises = usePlayersGetter() + .memberOfGroup('character', groupName) + .map(async (player: alt.Player) => { + const character = useCharacter(player); + await character.groups.remove(groupName); + }); + + await Promise.all([ + ...accountPromises, + ...characterPromises, + // Remove the group from all accounts that are offline. + database.updateMany({ groups: groupName }, { $pull: { groups: groupName } }, CollectionNames.Accounts), + // Remove the group from all characters that are offline. + database.updateMany({ groups: groupName }, { $pull: { groups: groupName } }, CollectionNames.Characters), + ]); + return true; + } + + async function addPermissions(groupName: string, permission: string[]): Promise { + const existingGroup = permissionGroups.getField(groupName); + if (!existingGroup) { + return false; + } + const newPermissions = existingGroup.permissions.concat( + permission.filter((x) => !existingGroup.permissions.includes(x)), + ); + + let version = existingGroup.version || 0; + version++; + + await permissionGroups.set(groupName, { ...existingGroup, permissions: newPermissions, version }); + InternalFunctions.buildIndex(); + return true; + } + + async function removePermissions(groupName: string, permissions: string[]) { + const existingGroup = permissionGroups.getField(groupName); + if (!existingGroup) { + return false; + } + const newPermissions = existingGroup.permissions.filter((x) => !permissions.includes(x)); + + let version = existingGroup.version || 0; + version++; + + await permissionGroups.set(groupName, { ...existingGroup, permissions: newPermissions, version }); + InternalFunctions.buildIndex(); + return true; + } + + function groupHasPermission(groupName: string, permission: string) { + const permissions = permissionIndex.get(groupName); + return permissions ? permissions.has(permission) : false; + } + + function groupsToPlainPermissions(groupNames: string[]): string[] { + const permissions = new Set(); + for (const groupName of groupNames) { + const groupPermissions = permissionIndex.get(groupName); + if (groupPermissions) { + for (const permission of groupPermissions) { + permissions.add(permission); + } + } + } + return [...permissions]; + } + + return { add, remove, addPermissions, removePermissions, groupHasPermission, groupsToPlainPermissions }; +} + +InternalFunctions.init(); diff --git a/src/main/server/systems/permissions/index.ts b/src/main/server/systems/permissions/index.ts new file mode 100644 index 000000000..46b11c182 --- /dev/null +++ b/src/main/server/systems/permissions/index.ts @@ -0,0 +1,4 @@ +export { usePermissionProxy } from './permissionProxy.js'; +export { useEntityPermissions } from './entityPermissions.js'; +export { usePermissions } from './usePermissions.js'; +export { usePermissionGroup } from './groupPermissions.js'; diff --git a/src/main/server/systems/permissions/permissionProxy.ts b/src/main/server/systems/permissions/permissionProxy.ts new file mode 100644 index 000000000..5fe629795 --- /dev/null +++ b/src/main/server/systems/permissions/permissionProxy.ts @@ -0,0 +1,129 @@ +import * as alt from 'alt-server'; +import { usePlainPermission } from '@Server/systems/permissions/plainPermissions.js'; +import { usePermissionGroup } from '@Server/systems/permissions/groupPermissions.js'; +import { PermissionsDocumentMixin, GroupsDocumentMixin } from '@Shared/types/index.js'; + +type DocumentGetterResult = T | undefined; +type Document = PermissionsDocumentMixin & GroupsDocumentMixin; +type DocumentGetter = () => DocumentGetterResult; +type DocumentBulkSetter = (fields: Partial) => Promise; + +type Target = 'character' | 'account'; + +declare module 'alt-server' { + export interface ICustomEmitEvent { + ['rebar:permissions:grant']: (entity: alt.Player, permission: string, target: Target) => void; + ['rebar:permissions:revoke']: (entity: alt.Player, permission: string, target: Target) => void; + ['rebar:permissions:clear']: (entity: alt.Player, removedPermissions: string[], target: Target) => void; + ['rebar:permissions:group:add']: (entity: alt.Player, groupName: string, target: Target) => void; + ['rebar:permissions:group:remove']: (entity: alt.Player, groupName: string, target: Target) => void; + ['rebar:permissions:group:clear']: (entity: alt.Player, removedGroups: string[], target: Target) => void; + } +} + +export function usePermissionProxy( + getter: DocumentGetter, + bulkSetter: DocumentBulkSetter, + entity: alt.Player = undefined, + target: Target = undefined, +) { + const permissionsList = (): string[] => { + const document = getter(); + + const documentPermissions = document.permissions || []; + if (typeof document.groups === 'object' && !Array.isArray(document.groups)) { + alt.logWarning( + `${target || 'virtual'} document ${document._id} has a groups object instead of an array. Prefer to migrate.`, + ); + return documentPermissions; + } + const groupPermissions = usePermissionGroup().groupsToPlainPermissions(document.groups || []); + return [...new Set([...documentPermissions, ...groupPermissions])]; + }; + + const permissions = { + grant: async (permission: string): Promise => { + const document = getter(); + const _permissions = usePlainPermission(document).grant(permission); + if (!_permissions) return false; + await bulkSetter(_permissions as Partial); + if (entity) { + alt.emit('permissions:grant', entity, permission, target); + } + return true; + }, + revoke: async (permission: string): Promise => { + const document = getter(); + const _permissions = usePlainPermission(document).revoke(permission); + if (!_permissions) return false; + await bulkSetter(_permissions as Partial); + if (entity) { + alt.emit('permissions:revoke', entity, permission, target); + } + return true; + }, + clear: async (): Promise => { + const document = getter(); + const permissions = [...(document.permissions || [])]; + await bulkSetter({ permissions: [] } as Partial); + alt.emit('permissions:clear', entity, permissions, target); + }, + list: permissionsList, + has: (permission: string): boolean => { + const permissions = permissionsList(); + return permissions.includes(permission); + }, + hasAll: (permissions: string[]): boolean => { + const permissionsToCheck = permissionsList(); + + for (const permission of permissions) { + if (permissionsToCheck.indexOf(permission) === -1) return false; + } + return true; + }, + hasAnyOf: (permissions: string[]): boolean => { + const permissionsToCheck = permissionsList(); + + for (const permission in permissions) { + if (permissionsToCheck.indexOf(permission) !== -1) return true; + } + return false; + }, + }; + + const groups = { + add: async (groupName: string): Promise => { + const document = getter(); + if (!document.groups) document.groups = []; + else if (document.groups.includes(groupName)) return false; + document.groups.push(groupName); + await bulkSetter({ groups: document.groups } as Partial); + alt.emit('permissions:group:add', entity, groupName, target); + return true; + }, + remove: async (groupName: string): Promise => { + const document = getter(); + if (!document.groups || !document.groups.includes(groupName)) return false; + document.groups = document.groups.filter((group) => group !== groupName); + await bulkSetter({ groups: document.groups } as Partial); + alt.emit('permissions:group:remove', entity, groupName, target); + return true; + }, + clear: async (): Promise => { + const document = getter(); + const groups = document.groups || []; + await bulkSetter({ groups: [] } as Partial); + alt.emit('permissions:group:clear', entity, groups, target); + }, + list: (): string[] => { + const document = getter(); + return document.groups || []; + }, + memberOf: (groupName: string): boolean => { + const document = getter(); + return document.groups?.includes(groupName) || false; + }, + }; + + return { permissions, groups }; +} diff --git a/src/main/server/systems/permissions/plainPermissions.ts b/src/main/server/systems/permissions/plainPermissions.ts new file mode 100644 index 000000000..ca5221994 --- /dev/null +++ b/src/main/server/systems/permissions/plainPermissions.ts @@ -0,0 +1,52 @@ +import { PermissionsDocumentMixin } from '@Shared/types/index.js'; + +export function usePlainPermission(document: T) { + /** + * Grants a permission to a player. + * + * @param {string} permission The permission to grant. + * @return The part of document with the permission granted. + */ + function grant(permission: string): PermissionsDocumentMixin | null { + if (!document.permissions) { + document.permissions = []; + } + if (document.permissions.includes(permission)) { + return null; + } + + document.permissions.push(permission); + return { permissions: document.permissions }; + } + + /** + * Checks if a player has a permission. + * + * @param {string} permission The permission to check. + * @return {boolean} Whether the player has the permission. + */ + function check(permission: string): boolean { + return document.permissions.includes(permission); + } + + /** + * Revokes a permission from a player. + * + * @param {string} permission The permission to revoke. + * @return The document with the permission revoked. + */ + function revoke(permission: string): PermissionsDocumentMixin | null { + const index = document.permissions.indexOf(permission); + if (index === -1) { + return null; + } + document.permissions.splice(index, 1); + return { permissions: document.permissions }; + } + + return { + grant, + check, + revoke, + }; +} diff --git a/src/main/server/systems/permissions/usePermissions.ts b/src/main/server/systems/permissions/usePermissions.ts new file mode 100644 index 000000000..594d0c70d --- /dev/null +++ b/src/main/server/systems/permissions/usePermissions.ts @@ -0,0 +1,42 @@ +import * as alt from 'alt-server'; +import { useAccount, useCharacter } from '@Server/document/index.js'; + +export function usePermissions(player: alt.Player) { + const account = useAccount(player); + const character = useCharacter(player); + + const hasPermission = (permission: string): boolean => { + if (account.isValid()) { + if (account.permissions.has(permission)) { + return true; + } + } + + if (character.isValid()) { + if (character.permissions.has(permission)) { + return true; + } + } + + return false; + }; + + const listAllPermissions = (): string[] => { + const characterPermissions = character.permissions.list(); + const accountPermissions = account.permissions.list(); + return [...new Set([...characterPermissions, ...accountPermissions])]; + }; + + return { + hasPermission, + listAllPermissions, + account: { + permissions: account.permissions, + groups: account.groups, + }, + character: { + permissions: character.permissions, + groups: character.groups, + }, + }; +} diff --git a/src/main/server/systems/screenshot.ts b/src/main/server/systems/screenshot.ts index 2e3b70de7..74f9ed687 100644 --- a/src/main/server/systems/screenshot.ts +++ b/src/main/server/systems/screenshot.ts @@ -2,10 +2,9 @@ import * as alt from 'alt-server'; import fs from 'fs'; import { Events } from '@Shared/events/index.js'; import { useBuffer } from '@Shared/utility/buffer.js'; -import { useRebar } from '../index.js'; import * as Clothing from '@Shared/data/clothing.js'; +import { usePlayer } from '../player/index.js'; -const Rebar = useRebar(); const BufferHelper = useBuffer(); const cache: { [id: string]: { data: Array; isComplete: boolean } } = {}; @@ -85,7 +84,7 @@ export function useScreenshot(player: alt.Player) { * @return */ async function takeVehicleScreenshot(pos: alt.Vector3, name: string, model: number) { - const rPlayer = Rebar.usePlayer(player); + const rPlayer = usePlayer(player); rPlayer.world.setWeather('EXTRASUNNY', 0); rPlayer.world.setTime(12, 0, 0); @@ -124,7 +123,7 @@ export function useScreenshot(player: alt.Player) { * @return */ async function takeWeaponScreenshot(name: string) { - const rPlayer = Rebar.usePlayer(player); + const rPlayer = usePlayer(player); rPlayer.world.setWeather('EXTRASUNNY', 0); rPlayer.world.setTime(12, 0, 0); @@ -159,7 +158,7 @@ export function useScreenshot(player: alt.Player) { ) { let isMale = dlcName.toLowerCase().includes('mp_m') || dlcName.includes('Male_'); - const rPlayer = Rebar.usePlayer(player); + const rPlayer = usePlayer(player); rPlayer.world.setWeather('EXTRASUNNY', 0); rPlayer.world.setTime(12, 0, 0); diff --git a/src/main/server/systems/serverKeybinds.ts b/src/main/server/systems/serverKeybinds.ts index 05717f5db..a7775c00e 100644 --- a/src/main/server/systems/serverKeybinds.ts +++ b/src/main/server/systems/serverKeybinds.ts @@ -1,12 +1,10 @@ import * as alt from 'alt-server'; -import { useRebar } from '../index.js'; import { Events } from '../../shared/events/index.js'; +import * as Utility from '@Shared/utility/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]: KeybindInfo[] } = {}; function handleKeybind(player: alt.Player, key: number) { @@ -37,7 +35,7 @@ function updateKeybinds() { export function useKeybinder() { function on(key: number, callback: OnKeybind) { - const uid = Rebar.utility.uid.generate(); + const uid = Utility.uid.generate(); if (!callbacks[key]) { callbacks[key] = []; @@ -74,5 +72,5 @@ export function useKeybinder() { }; } -RebarEvents.on('character-bound', updateKeybindForPlayer); +alt.on('rebar:playerCharacterBound', updateKeybindForPlayer); alt.onClient(Events.systems.keybinds.invoke, handleKeybind); diff --git a/src/main/server/systems/serverKeypress.ts b/src/main/server/systems/serverKeypress.ts index 9b755098d..f91e2b246 100644 --- a/src/main/server/systems/serverKeypress.ts +++ b/src/main/server/systems/serverKeypress.ts @@ -1,12 +1,10 @@ import * as alt from 'alt-server'; -import { useRebar } from '../index.js'; import { Events } from '@Shared/events/index.js'; +import * as Utility from '@Shared/utility/index.js'; type OnKeybind = (player: alt.Player) => void; type KeybindInfo = { callback: OnKeybind; uid: string }; -const Rebar = useRebar(); -const RebarEvents = Rebar.events.useEvents(); const keyCallbacks: { [key: string]: { down: KeybindInfo[]; up: KeybindInfo[]; hold: KeybindInfo[] } } = {}; function handleKeyUp(player: alt.Player, key: number) { @@ -69,7 +67,7 @@ function updateKeypresses() { export function useKeypress() { function on(key: number, callbackUp: OnKeybind, callbackDown: OnKeybind) { - const uid = Rebar.utility.uid.generate(); + const uid = Utility.uid.generate(); if (!keyCallbacks[key]) { keyCallbacks[key] = { @@ -86,7 +84,7 @@ export function useKeypress() { } function onHold(key: number, callbacks: { hold: OnKeybind; up?: OnKeybind; down?: OnKeybind }) { - const uid = Rebar.utility.uid.generate(); + const uid = Utility.uid.generate(); if (!keyCallbacks[key]) { keyCallbacks[key] = { @@ -179,8 +177,7 @@ export function useKeypress() { }; } -RebarEvents.on('character-bound', updateKeypressForPlayer); - +alt.on('rebar:playerCharacterBound', 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/server/systems/serverTime.ts b/src/main/server/systems/serverTime.ts deleted file mode 100644 index f5e27d4ba..000000000 --- a/src/main/server/systems/serverTime.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { useRebar } from '@Server/index.js'; - -const Rebar = useRebar(); -const RebarEvents = Rebar.events.useEvents(); - -const time = { - hour: 8, - minute: 0, - second: 0, -}; - -/** - * Server time is the in-world time for the server and not real world time. - * - * @export - * @return - */ -export function useServerTime() { - /** - * Set the in-game world time - * - * @param {number} value - */ - function setHour(value: number) { - if (value >= 24) { - value = 0; - } - - if (time.hour !== value) { - RebarEvents.invoke('time-hour-changed', value); - RebarEvents.invoke('time-changed', time.hour, time.minute, time.second); - } - - time.hour = value; - } - - /** - * Set the in-game world minute - * - * @param {number} value - */ - function setMinute(value: number) { - if (value >= 60) { - value = 0; - } - - if (time.minute !== value) { - RebarEvents.invoke('time-minute-changed', value); - RebarEvents.invoke('time-changed', time.hour, time.minute, time.second); - } - - time.minute = value; - } - - /** - * Set the in-game world second - * - * @param {number} value - */ - function setSecond(value: number) { - if (value >= 60) { - value = 0; - } - - if (time.minute !== value) { - RebarEvents.invoke('time-second-changed', value); - RebarEvents.invoke('time-changed', time.hour, time.minute, time.second); - } - - time.second = value; - } - - /** - * Get the current in-game world time - * - * @return - */ - function getTime() { - return time; - } - - return { - getTime, - setHour, - setMinute, - setSecond, - }; -} diff --git a/src/main/server/systems/serverWeather.ts b/src/main/server/systems/serverWeather.ts deleted file mode 100644 index 33cf46216..000000000 --- a/src/main/server/systems/serverWeather.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Weathers } from "@Shared/data/weathers.js"; -import { useRebar } from "../index.js"; - -const Rebar = useRebar(); -const RebarEvents = Rebar.events.useEvents(); - -let currentWeather: Weathers = 'EXTRASUNNY'; -let currentForecast: Weathers[] = []; - -export function useServerWeather() { - function set(weather: Weathers) { - currentWeather = weather; - RebarEvents.invoke('weather-changed', currentWeather); - } - - function setForecast(weathers: Weathers[]) { - currentForecast = weathers; - RebarEvents.invoke('weather-forecast-changed', currentForecast); - } - - function get() { - return currentWeather; - } - - function getForecast() { - return currentForecast; - } - - return { - get, - set, - setForecast, - getForecast - } -} \ No newline at end of file diff --git a/src/main/server/systems/streamSyncedBinder.ts b/src/main/server/systems/streamSyncedBinder.ts index 2718c13bb..a6fdcffcd 100644 --- a/src/main/server/systems/streamSyncedBinder.ts +++ b/src/main/server/systems/streamSyncedBinder.ts @@ -1,15 +1,11 @@ import * as alt from 'alt-server'; import { Character, Vehicle } from '@Shared/types/index.js'; -import { useRebar } from '../index.js'; type DataTypes = { Character: keyof Character; Vehicle: keyof Vehicle; }; -const Rebar = useRebar(); -const RebarEvents = Rebar.events.useEvents(); - const keys: { [K in keyof DataTypes]: DataTypes[K][] } = { Character: [], Vehicle: [], @@ -34,9 +30,6 @@ export function useStreamSyncedBinder() { } keys.Character.push(key); - Rebar.document.character - .useCharacterEvents() - .on(key, (entity, newValue) => handleKeySet(entity, key, newValue)); } /** @@ -53,7 +46,6 @@ export function useStreamSyncedBinder() { } keys.Vehicle.push(key); - Rebar.document.vehicle.useVehicleEvents().on(key, (entity, newValue) => handleKeySet(entity, key, newValue)); } return { @@ -62,14 +54,30 @@ export function useStreamSyncedBinder() { }; } -RebarEvents.on('character-bound', (player, document) => { +alt.on('rebar:playerCharacterBound', (player, document) => { for (let key of keys.Character) { handleKeySet(player, key, document[key]); } }); -RebarEvents.on('vehicle-bound', (vehicle, document) => { +alt.on('rebar:vehicleBound', (vehicle, document) => { for (let key of keys.Vehicle) { handleKeySet(vehicle, key, document[key]); } }); + +alt.on('rebar:vehicleUpdated', (vehicle, key, value) => { + if (keys.Vehicle.findIndex((x) => key == x) <= -1) { + return; + } + + handleKeySet(vehicle, key, value); +}); + +alt.on('rebar:playerCharacterUpdated', (player, key, value) => { + if (keys.Character.findIndex((x) => key == x) <= -1) { + return; + } + + handleKeySet(player, key, value); +}); diff --git a/src/main/server/systems/tick.ts b/src/main/server/systems/tick.ts new file mode 100644 index 000000000..ec64a07a2 --- /dev/null +++ b/src/main/server/systems/tick.ts @@ -0,0 +1,19 @@ +import * as alt from 'alt-server'; + +declare module 'alt-server' { + export interface ICustomEmitEvent { + 'rebar:onTick': (tick: number) => void; + } +} + +let lastSecond = new Date(Date.now()).getSeconds(); + +alt.everyTick(() => { + const newSecond = new Date(Date.now()).getSeconds(); + if (newSecond === lastSecond) { + return; + } + + lastSecond = newSecond; + alt.emit('rebar:onTick', newSecond); +}); diff --git a/src/main/server/utility/protectCallback.ts b/src/main/server/utility/protectCallback.ts index e3114aaca..f5134c39a 100644 --- a/src/main/server/utility/protectCallback.ts +++ b/src/main/server/utility/protectCallback.ts @@ -1,62 +1,20 @@ import * as alt from 'alt-server'; import { usePlayer } from '../player/index.js'; +import { PermissionOptions } from '@Shared/types/index.js'; +import { useEntityPermissions } from '@Server/systems/permissions/entityPermissions.js'; -function hasPermission( - player: alt.Player, - accountPermissions: string[], - characterPermissions: string[], - groupPermissions: { [key: string]: string[] }, -) { +function hasPermission(player: alt.Player, permissionOptions: PermissionOptions) { const rPlayer = usePlayer(player); if (!rPlayer.isValid()) { return false; } - const accPerms = rPlayer.character.getField('permissions') ?? []; - const charPerms = rPlayer.character.getField('permissions') ?? []; - const groupPerms = rPlayer.character.getField('groups') ?? {}; - - for (let perm of accountPermissions) { - if (!accPerms.includes(perm)) { - continue; - } - - return true; - } - - for (let perm of characterPermissions) { - if (!charPerms.includes(perm)) { - continue; - } - - return true; - } - - for (let key of Object.keys(groupPermissions)) { - // Does not belong to group - if (!groupPerms[key]) { - continue; - } - - // Belongs to group - for (let perm of groupPermissions[key]) { - if (!groupPerms[key].includes(perm)) { - continue; - } - - return true; - } - } - - return false; + return useEntityPermissions(permissionOptions).check(player); } -export function useProtectCallback( - callback: Function, - permission: { character?: string[]; account?: string[]; group?: { [key: string]: string[] } }, -) { +export function useProtectCallback(callback: Function, permissionOptions: PermissionOptions) { return (player: alt.Player, ...args: any[]) => { - if (!hasPermission(player, permission.account ?? [], permission.character ?? [], permission.group ?? {})) { + if (!hasPermission(player, permissionOptions)) { return; } diff --git a/src/main/server/utility/rateLimitCallback.ts b/src/main/server/utility/rateLimitCallback.ts index c127068df..143f152d8 100644 --- a/src/main/server/utility/rateLimitCallback.ts +++ b/src/main/server/utility/rateLimitCallback.ts @@ -1,10 +1,8 @@ import * as alt from 'alt-server'; -import { useRebar } from '../index.js'; +import { useAccount } from '../document/account.js'; type RateLimitTracker = { nextCall: number; violations: 0 }; -const Rebar = useRebar(); - /** * Protect an event, or callback with rate limits. * @@ -40,7 +38,7 @@ export function useRateLimitCallback(callback: Function, uid: string, timeBetwee return; } - const document = Rebar.document.account.useAccount(player); + const document = useAccount(player); if (!document.get()) { alt.logWarning(`Player ID | ${player.id} called ${uid} too fast, they were kicked`); diff --git a/src/main/server/vehicle/index.ts b/src/main/server/vehicle/index.ts index 4a1e29f58..2e942d8bc 100644 --- a/src/main/server/vehicle/index.ts +++ b/src/main/server/vehicle/index.ts @@ -1,12 +1,14 @@ import * as alt from 'alt-server'; -import { useRebar } from '../index.js'; import { Vehicle, WheelState } from '../../shared/types/vehicle.js'; import * as Utility from '@Shared/utility/index.js'; import { Events } from '../../shared/events/index.js'; import { useVehicleHandling } from './vehicleHandling.js'; +import { useDatabase } from '../database/index.js'; +import { useVehicleBinder, useVehicle as useVehicleDocument } from '../document/vehicle.js'; +import { CollectionNames } from '../document/shared.js'; +import { usePlayer } from '../player/index.js'; -const Rebar = useRebar(); -const db = Rebar.database.useDatabase(); +const db = useDatabase(); export function useVehicle(vehicle: alt.Vehicle) { /** @@ -58,39 +60,44 @@ export function useVehicle(vehicle: alt.Vehicle) { } // Synchronize neon - if (document.neonPlacement && document.neonColor) { - vehicle.neon = document.neonPlacement; - vehicle.neonColor = document.neonColor; + if (document.neon && document.neon.color) { + vehicle.neon = document.neon.placement; + vehicle.neonColor = document.neon.color; } // Synchronize primary custom paint job - if (typeof document.customPrimaryColor !== 'undefined') { - vehicle.customPrimaryColor = document.customPrimaryColor; + if (typeof document.color.primaryCustom !== 'undefined') { + vehicle.customPrimaryColor = document.color.primaryCustom; } // Synchronize secondary custom paint job - if (typeof document.customSecondaryColor !== 'undefined') { - vehicle.customSecondaryColor = document.customSecondaryColor; + if (typeof document.color.secondaryCustom !== 'undefined') { + vehicle.customSecondaryColor = document.color.secondaryCustom; } // Synchronize primary paint job - if (typeof document.primaryColor !== 'undefined') { - vehicle.primaryColor = document.primaryColor; + if (typeof document.color.primary !== 'undefined') { + vehicle.primaryColor = document.color.primary; } // Synchronize secondary paint job - if (typeof document.secondaryColor !== 'undefined') { - vehicle.secondaryColor = document.secondaryColor; + if (typeof document.color.secondary !== 'undefined') { + vehicle.secondaryColor = document.color.secondary; } // Synchronize wheelColor - if (typeof document.wheelColor !== 'undefined') { - vehicle.wheelColor = document.wheelColor; + if (typeof document.color.wheel !== 'undefined') { + vehicle.wheelColor = document.color.wheel; } // Synchronize pearlColor - if (typeof document.pearlColor !== 'undefined') { - vehicle.pearlColor = document.pearlColor; + if (typeof document.color.pearl !== 'undefined') { + vehicle.pearlColor = document.color.pearl; + } + + // Synchronize xenonColor + if (typeof document.color.xenon !== 'undefined') { + vehicle.headlightColor = document.color.xenon; } // Synchronize vehicle extras @@ -139,7 +146,7 @@ export function useVehicle(vehicle: alt.Vehicle) { return undefined; } - const document = Rebar.document.vehicle.useVehicle(vehicle); + const document = useVehicleDocument(vehicle); if (document.get()) { return undefined; } @@ -153,11 +160,11 @@ export function useVehicle(vehicle: alt.Vehicle) { pos: vehicle.pos, rot: vehicle.rot, }, - Rebar.database.CollectionNames.Vehicles, + CollectionNames.Vehicles, ); - const vehicleDocument = await db.get({ _id: id }, Rebar.database.CollectionNames.Vehicles); - Rebar.document.vehicle.useVehicleBinder(vehicle).bind(vehicleDocument); + const vehicleDocument = await db.get({ _id: id }, CollectionNames.Vehicles); + useVehicleBinder(vehicle).bind(vehicleDocument); return vehicleDocument; } @@ -173,11 +180,11 @@ export function useVehicle(vehicle: alt.Vehicle) { return false; } - if (Rebar.document.vehicle.useVehicle(vehicle).get()) { + if (useVehicleDocument(vehicle).get()) { return false; } - Rebar.document.vehicle.useVehicleBinder(vehicle).bind(document); + useVehicleBinder(vehicle).bind(document); return true; } @@ -187,7 +194,7 @@ export function useVehicle(vehicle: alt.Vehicle) { * @return */ function isBound() { - return Rebar.document.vehicle.useVehicle(vehicle).get() ? true : false; + return useVehicleDocument(vehicle).get() ? true : false; } /** @@ -197,7 +204,7 @@ export function useVehicle(vehicle: alt.Vehicle) { * @return */ async function repair(): Promise { - const document = Rebar.document.vehicle.useVehicle(vehicle); + const document = useVehicleDocument(vehicle); const model = vehicle.model; const pos = vehicle.pos; const rot = vehicle.rot; @@ -212,7 +219,7 @@ export function useVehicle(vehicle: alt.Vehicle) { return vehicle; } - Rebar.document.vehicle.useVehicleBinder(vehicle).bind(document.get(), false); + useVehicleBinder(vehicle).bind(document.get(), false); await save(); sync(); @@ -225,7 +232,7 @@ export function useVehicle(vehicle: alt.Vehicle) { * @return */ function save() { - const document = Rebar.document.vehicle.useVehicle(vehicle); + const document = useVehicleDocument(vehicle); if (!document.get()) { return; } @@ -285,7 +292,7 @@ export function useVehicle(vehicle: alt.Vehicle) { * @return */ function hasOwner() { - const document = Rebar.document.vehicle.useVehicle(vehicle); + const document = useVehicleDocument(vehicle); return document.get() ? true : false; } @@ -302,7 +309,12 @@ export function useVehicle(vehicle: alt.Vehicle) { * @param {number} door */ function toggleDoor(door: number) { - vehicle.setDoorState(door, vehicle.getDoorState(door) === 0 ? 7 : 0); + if (door < 0 || door > 5) { + throw new Error('Toggle door values must be 0 - 5'); + } + + const state = vehicle.getStreamSyncedMeta(`door-${door}`) ? false : true; + vehicle.setStreamSyncedMeta(`door-${door}`, state); } /** @@ -371,7 +383,7 @@ export function useVehicle(vehicle: alt.Vehicle) { * @return */ async function addKey(owner_document_id: string) { - const document = Rebar.document.vehicle.useVehicle(vehicle); + const document = useVehicleDocument(vehicle); if (!document.get()) { return false; } @@ -394,7 +406,7 @@ export function useVehicle(vehicle: alt.Vehicle) { * @return */ async function removeKey(owner_document_id: string) { - const document = Rebar.document.vehicle.useVehicle(vehicle); + const document = useVehicleDocument(vehicle); if (!document.get()) { return false; } @@ -416,7 +428,7 @@ export function useVehicle(vehicle: alt.Vehicle) { * @return */ async function clearKeys() { - const document = Rebar.document.vehicle.useVehicle(vehicle); + const document = useVehicleDocument(vehicle); if (!document.get()) { return false; } @@ -435,12 +447,12 @@ export function useVehicle(vehicle: alt.Vehicle) { * @param {alt.Player} player */ function verifyOwner(player: alt.Player, requireOwnership: boolean, ownerOnly = false) { - const document = Rebar.document.vehicle.useVehicle(vehicle); + const document = useVehicleDocument(vehicle); if (!document.get()) { return requireOwnership ? false : true; } - const rPlayer = Rebar.usePlayer(player); + const rPlayer = usePlayer(player); const _id = rPlayer.character.getField('_id'); if (!_id) { return false; @@ -479,7 +491,7 @@ export function useVehicle(vehicle: alt.Vehicle) { * If the vehicle has a `document` bound to it, it will apply the appearance. */ function sync() { - const document = Rebar.document.vehicle.useVehicle(vehicle); + const document = useVehicleDocument(vehicle); if (!document.get()) { return; } diff --git a/src/main/shared/data/blipNames.ts b/src/main/shared/data/blipNames.ts new file mode 100644 index 000000000..0da091c40 --- /dev/null +++ b/src/main/shared/data/blipNames.ts @@ -0,0 +1,886 @@ +export const BlipNames = { + radar_higher: 0, + radar_level: 1, + radar_lower: 2, + radar_police_ped: 3, + radar_wanted_radius: 4, + radar_area_blip: 5, + radar_centre: 6, + radar_north: 7, + radar_waypoint: 8, + radar_radius_blip: 9, + radar_radius_outline_blip: 10, + radar_weapon_higher: 11, + radar_weapon_lower: 12, + radar_higher_ai: 13, + radar_lower_ai: 14, + radar_police_heli_spin: 15, + radar_police_plane_move: 16, + radar_numbered_1: 17, + radar_numbered_2: 18, + radar_numbered_3: 19, + radar_numbered_4: 20, + radar_numbered_5: 21, + radar_numbered_6: 22, + radar_numbered_7: 23, + radar_numbered_8: 24, + radar_numbered_9: 25, + radar_numbered_10: 26, + radar_mp_crew: 27, + radar_mp_friendlies: 28, + radar_empty: 29, + radar_empty1: 30, + radar_empty2: 31, + radar_script_objective: 32, + radar_empty3: 33, + radar_empty4: 34, + radar_station: 35, + radar_cable_car: 36, + radar_activities: 37, + radar_raceflag: 38, + radar_fire: 39, + radar_safehouse: 40, + radar_police: 41, + radar_police_chase: 42, + radar_police_heli: 43, + radar_bomb_a: 44, + radar_bomb_b: 45, + radar_bomb_c: 46, + radar_snitch: 47, + radar_planning_locations: 48, + radar_crim_arrest: 49, + radar_crim_carsteal: 50, + radar_crim_drugs: 51, + radar_crim_holdups: 52, + radar_crim_pimping: 53, + radar_crim_player: 54, + radar_fence: 55, + radar_cop_patrol: 56, + radar_cop_player: 57, + radar_crim_wanted: 58, + radar_heist: 59, + radar_police_station: 60, + radar_hospital: 61, + radar_assassins_mark: 62, + radar_elevator: 63, + radar_helicopter: 64, + radar_joyriders: 65, + radar_random_character: 66, + radar_security_van: 67, + radar_tow_truck: 68, + radar_drive_thru: 69, + radar_illegal_parking: 70, + radar_barber: 71, + radar_car_mod_shop: 72, + radar_clothes_store: 73, + radar_gym: 74, + radar_tattoo: 75, + radar_armenian_family: 76, + radar_lester_family: 77, + radar_michael_family: 78, + radar_trevor_family: 79, + radar_jewelry_heist: 80, + radar_drag_race: 81, + radar_drag_race_finish: 82, + radar_car_carrier: 83, + radar_rampage: 84, + radar_vinewood_tours: 85, + radar_lamar_family: 86, + radar_taco_van: 87, + radar_franklin_family: 88, + radar_chinese_strand: 89, + radar_flight_school: 90, + radar_eye_sky: 91, + radar_air_hockey: 92, + radar_bar: 93, + radar_base_jump: 94, + radar_basketball: 95, + radar_biolab_heist: 96, + radar_bowling: 97, + radar_burger_shot: 98, + radar_cabaret_club: 99, + radar_car_wash: 100, + radar_cluckin_bell: 101, + radar_comedy_club: 102, + radar_darts: 103, + radar_docks_heist: 104, + radar_fbi_heist: 105, + radar_fbi_officers_strand: 106, + radar_finale_bank_heist: 107, + radar_financier_strand: 108, + radar_golf: 109, + radar_gun_shop: 110, + radar_internet_cafe: 111, + radar_michael_family_exile: 112, + radar_nice_house_heist: 113, + radar_random_female: 114, + radar_random_male: 115, + radar_repo: 116, + radar_restaurant: 117, + radar_rural_bank_heist: 118, + radar_shooting_range: 119, + radar_solomon_strand: 120, + radar_strip_club: 121, + radar_tennis: 122, + radar_trevor_family_exile: 123, + radar_michael_trevor_family: 124, + radar_vehicle_spawn: 125, + radar_triathlon: 126, + radar_off_road_racing: 127, + radar_gang_cops: 128, + radar_gang_mexicans: 129, + radar_gang_bikers: 130, + radar_gang_families: 131, + radar_gang_professionals: 132, + radar_snitch_red: 133, + radar_crim_cuff_keys: 134, + radar_cinema: 135, + radar_music_venue: 136, + radar_police_station_blue: 137, + radar_airport: 138, + radar_crim_saved_vehicle: 139, + radar_weed_stash: 140, + radar_hunting: 141, + radar_pool: 142, + radar_objective_blue: 143, + radar_objective_green: 144, + radar_objective_red: 145, + radar_objective_yellow: 146, + radar_arms_dealing: 147, + radar_mp_friend: 148, + radar_celebrity_theft: 149, + radar_weapon_assault_rifle: 150, + radar_weapon_bat: 151, + radar_weapon_grenade: 152, + radar_weapon_health: 153, + radar_weapon_knife: 154, + radar_weapon_molotov: 155, + radar_weapon_pistol: 156, + radar_weapon_rocket: 157, + radar_weapon_shotgun: 158, + radar_weapon_smg: 159, + radar_weapon_sniper: 160, + radar_mp_noise: 161, + radar_poi: 162, + radar_passive: 163, + radar_usingmenu: 164, + radar_friend_franklin_p: 165, + radar_friend_franklin_x: 166, + radar_friend_michael_p: 167, + radar_friend_michael_x: 168, + radar_friend_trevor_p: 169, + radar_friend_trevor_x: 170, + radar_gang_cops_partner: 171, + radar_friend_lamar: 172, + radar_weapon_minigun: 173, + radar_weapon_grenadeLauncher: 174, + radar_weapon_armour: 175, + radar_property_takeover: 176, + radar_gang_mexicans_highlight: 177, + radar_gang_bikers_highlight: 178, + radar_triathlon_cycling: 179, + radar_triathlon_swimming: 180, + radar_property_takeover_bikers: 181, + radar_property_takeover_cops: 182, + radar_property_takeover_vagos: 183, + radar_camera: 184, + radar_centre_red: 185, + radar_handcuff_keys_bikers: 186, + radar_handcuff_keys_vagos: 187, + radar_handcuffs_closed_bikers: 188, + radar_handcuffs_closed_vagos: 189, + radar_handcuffs_open_bikers: 190, + radar_handcuffs_open_vagos: 191, + radar_camera_badger: 192, + radar_camera_facade: 193, + radar_camera_ifruit: 194, + radar_crim_arrest_bikers: 195, + radar_crim_arrest_vagos: 196, + radar_yoga: 197, + radar_taxi: 198, + radar_numbered_11: 199, + radar_numbered_12: 200, + radar_numbered_13: 201, + radar_numbered_14: 202, + radar_numbered_15: 203, + radar_numbered_16: 204, + radar_shrink: 205, + radar_epsilon: 206, + radar_financier_strand_grey: 207, + radar_trevor_family_grey: 208, + radar_trevor_family_red: 209, + radar_franklin_family_grey: 210, + radar_franklin_family_blue: 211, + radar_franklin_a: 212, + radar_franklin_b: 213, + radar_franklin_c: 214, + radar_numbered_red_1: 215, + radar_numbered_red_2: 216, + radar_numbered_red_3: 217, + radar_numbered_red_4: 218, + radar_numbered_red_5: 219, + radar_numbered_red_6: 220, + radar_numbered_red_7: 221, + radar_numbered_red_8: 222, + radar_numbered_red_9: 223, + radar_numbered_red_10: 224, + radar_gang_vehicle: 225, + radar_gang_vehicle_bikers: 226, + radar_gang_vehicle_cops: 227, + radar_gang_vehicle_vagos: 228, + radar_guncar: 229, + radar_driving_bikers: 230, + radar_driving_cops: 231, + radar_driving_vagos: 232, + radar_gang_cops_highlight: 233, + radar_shield_bikers: 234, + radar_shield_cops: 235, + radar_shield_vagos: 236, + radar_custody_bikers: 237, + radar_custody_vagos: 238, + radar_gang_wanted_bikers: 239, + radar_gang_wanted_bikers_1: 240, + radar_gang_wanted_bikers_2: 241, + radar_gang_wanted_bikers_3: 242, + radar_gang_wanted_bikers_4: 243, + radar_gang_wanted_bikers_5: 244, + radar_gang_wanted_vagos: 245, + radar_gang_wanted_vagos_1: 246, + radar_gang_wanted_vagos_2: 247, + radar_gang_wanted_vagos_3: 248, + radar_gang_wanted_vagos_4: 249, + radar_gang_wanted_vagos_5: 250, + radar_arms_dealing_air: 251, + radar_playerstate_arrested: 252, + radar_playerstate_custody: 253, + radar_playerstate_driving: 254, + radar_playerstate_keyholder: 255, + radar_playerstate_partner: 256, + radar_gang_wanted_1: 257, + radar_gang_wanted_2: 258, + radar_gang_wanted_3: 259, + radar_gang_wanted_4: 260, + radar_gang_wanted_5: 261, + radar_ztype: 262, + radar_stinger: 263, + radar_packer: 264, + radar_monroe: 265, + radar_fairground: 266, + radar_property: 267, + radar_gang_highlight: 268, + radar_altruist: 269, + radar_ai: 270, + radar_on_mission: 271, + radar_cash_pickup: 272, + radar_chop: 273, + radar_dead: 274, + radar_territory_locked: 275, + radar_cash_lost: 276, + radar_cash_vagos: 277, + radar_cash_cops: 278, + radar_hooker: 279, + radar_friend: 280, + radar_mission_2to4: 281, + radar_mission_2to8: 282, + radar_mission_2to12: 283, + radar_mission_2to16: 284, + radar_custody_dropoff: 285, + radar_onmission_cops: 286, + radar_onmission_lost: 287, + radar_onmission_vagos: 288, + radar_crim_carsteal_cops: 289, + radar_crim_carsteal_bikers: 290, + radar_crim_carsteal_vagos: 291, + radar_band_strand: 292, + radar_simeon_family: 293, + radar_mission_1: 294, + radar_mission_2: 295, + radar_friend_darts: 296, + radar_friend_comedyclub: 297, + radar_friend_cinema: 298, + radar_friend_tennis: 299, + radar_friend_stripclub: 300, + radar_friend_livemusic: 301, + radar_friend_golf: 302, + radar_bounty_hit: 303, + radar_ugc_mission: 304, + radar_horde: 305, + radar_cratedrop: 306, + radar_plane_drop: 307, + radar_sub: 308, + radar_race: 309, + radar_deathmatch: 310, + radar_arm_wrestling: 311, + radar_mission_1to2: 312, + radar_shootingrange_gunshop: 313, + radar_race_air: 314, + radar_race_land: 315, + radar_race_sea: 316, + radar_tow: 317, + radar_garbage: 318, + radar_drill: 319, + radar_spikes: 320, + radar_firetruck: 321, + radar_minigun2: 322, + radar_bugstar: 323, + radar_submarine: 324, + radar_chinook: 325, + radar_getaway_car: 326, + radar_mission_bikers_1: 327, + radar_mission_bikers_1to2: 328, + radar_mission_bikers_2: 329, + radar_mission_bikers_2to4: 330, + radar_mission_bikers_2to8: 331, + radar_mission_bikers_2to12: 332, + radar_mission_bikers_2to16: 333, + radar_mission_cops_1: 334, + radar_mission_cops_1to2: 335, + radar_mission_cops_2: 336, + radar_mission_cops_2to4: 337, + radar_mission_cops_2to8: 338, + radar_mission_cops_2to12: 339, + radar_mission_cops_2to16: 340, + radar_mission_vagos_1: 341, + radar_mission_vagos_1to2: 342, + radar_mission_vagos_2: 343, + radar_mission_vagos_2to4: 344, + radar_mission_vagos_2to8: 345, + radar_mission_vagos_2to12: 346, + radar_mission_vagos_2to16: 347, + radar_gang_bike: 348, + radar_gas_grenade: 349, + radar_property_for_sale: 350, + radar_gang_attack_package: 351, + radar_martin_madrazzo: 352, + radar_enemy_heli_spin: 353, + radar_boost: 354, + radar_devin: 355, + radar_dock: 356, + radar_garage: 357, + radar_golf_flag: 358, + radar_hangar: 359, + radar_helipad: 360, + radar_jerry_can: 361, + radar_mask: 362, + radar_heist_prep: 363, + radar_incapacitated: 364, + radar_spawn_point_pickup: 365, + radar_boilersuit: 366, + radar_completed: 367, + radar_rockets: 368, + radar_garage_for_sale: 369, + radar_helipad_for_sale: 370, + radar_dock_for_sale: 371, + radar_hangar_for_sale: 372, + radar_placeholder_6: 373, + radar_business: 374, + radar_business_for_sale: 375, + radar_race_bike: 376, + radar_parachute: 377, + radar_team_deathmatch: 378, + radar_race_foot: 379, + radar_vehicle_deathmatch: 380, + radar_barry: 381, + radar_dom: 382, + radar_maryann: 383, + radar_cletus: 384, + radar_josh: 385, + radar_minute: 386, + radar_omega: 387, + radar_tonya: 388, + radar_paparazzo: 389, + radar_aim: 390, + radar_cratedrop_background: 391, + radar_green_and_net_player1: 392, + radar_green_and_net_player2: 393, + radar_green_and_net_player3: 394, + radar_green_and_friendly: 395, + radar_net_player1_and_net_player2: 396, + radar_net_player1_and_net_player3: 397, + radar_creator: 398, + radar_creator_direction: 399, + radar_abigail: 400, + radar_blimp: 401, + radar_repair: 402, + radar_testosterone: 403, + radar_dinghy: 404, + radar_fanatic: 405, + radar_invisible: 406, + radar_info_icon: 407, + radar_capture_the_flag: 408, + radar_last_team_standing: 409, + radar_boat: 410, + radar_capture_the_flag_base: 411, + radar_mp_crew2: 412, + radar_capture_the_flag_outline: 413, + radar_capture_the_flag_base_nobag: 414, + radar_weapon_jerrycan: 415, + radar_rp: 416, + radar_level_inside: 417, + radar_bounty_hit_inside: 418, + radar_capture_the_usaflag: 419, + radar_capture_the_usaflag_outline: 420, + radar_tank: 421, + radar_player_heli: 422, + radar_player_plane: 423, + radar_player_jet: 424, + radar_centre_stroke: 425, + radar_player_guncar: 426, + radar_player_boat: 427, + radar_mp_heist: 428, + radar_temp_1: 429, + radar_temp_2: 430, + radar_temp_3: 431, + radar_temp_4: 432, + radar_temp_5: 433, + radar_temp_6: 434, + radar_race_stunt: 435, + radar_hot_property: 436, + radar_urbanwarfare_versus: 437, + radar_king_of_the_castle: 438, + radar_player_king: 439, + radar_dead_drop: 440, + radar_penned_in: 441, + radar_beast: 442, + radar_edge_pointer: 443, + radar_edge_crosstheline: 444, + radar_mp_lamar: 445, + radar_bennys: 446, + radar_corner_number_1: 447, + radar_corner_number_2: 448, + radar_corner_number_3: 449, + radar_corner_number_4: 450, + radar_corner_number_5: 451, + radar_corner_number_6: 452, + radar_corner_number_7: 453, + radar_corner_number_8: 454, + radar_yacht: 455, + radar_finders_keepers: 456, + radar_assault_package: 457, + radar_hunt_the_boss: 458, + radar_sightseer: 459, + radar_turreted_limo: 460, + radar_belly_of_the_beast: 461, + radar_yacht_location: 462, + radar_pickup_beast: 463, + radar_pickup_zoned: 464, + radar_pickup_random: 465, + radar_pickup_slow_time: 466, + radar_pickup_swap: 467, + radar_pickup_thermal: 468, + radar_pickup_weed: 469, + radar_weapon_railgun: 470, + radar_seashark: 471, + radar_pickup_hidden: 472, + radar_warehouse: 473, + radar_warehouse_for_sale: 474, + radar_office: 475, + radar_office_for_sale: 476, + radar_truck: 477, + radar_contraband: 478, + radar_trailer: 479, + radar_vip: 480, + radar_cargobob: 481, + radar_area_outline_blip: 482, + radar_pickup_accelerator: 483, + radar_pickup_ghost: 484, + radar_pickup_detonator: 485, + radar_pickup_bomb: 486, + radar_pickup_armoured: 487, + radar_stunt: 488, + radar_weapon_lives: 489, + radar_stunt_premium: 490, + radar_adversary: 491, + radar_biker_clubhouse: 492, + radar_biker_caged_in: 493, + radar_biker_turf_war: 494, + radar_biker_joust: 495, + radar_production_weed: 496, + radar_production_crack: 497, + radar_production_fake_id: 498, + radar_production_meth: 499, + radar_production_money: 500, + radar_package: 501, + radar_capture_1: 502, + radar_capture_2: 503, + radar_capture_3: 504, + radar_capture_4: 505, + radar_capture_5: 506, + radar_capture_6: 507, + radar_capture_7: 508, + radar_capture_8: 509, + radar_capture_9: 510, + radar_capture_10: 511, + radar_quad: 512, + radar_bus: 513, + radar_drugs_package: 514, + radar_pickup_jump: 515, + radar_adversary_4: 516, + radar_adversary_8: 517, + radar_adversary_10: 518, + radar_adversary_12: 519, + radar_adversary_16: 520, + radar_laptop: 521, + radar_pickup_deadline: 522, + radar_sports_car: 523, + radar_warehouse_vehicle: 524, + radar_reg_papers: 525, + radar_police_station_dropoff: 526, + radar_junkyard: 527, + radar_ex_vech_1: 528, + radar_ex_vech_2: 529, + radar_ex_vech_3: 530, + radar_ex_vech_4: 531, + radar_ex_vech_5: 532, + radar_ex_vech_6: 533, + radar_ex_vech_7: 534, + radar_target_a: 535, + radar_target_b: 536, + radar_target_c: 537, + radar_target_d: 538, + radar_target_e: 539, + radar_target_f: 540, + radar_target_g: 541, + radar_target_h: 542, + radar_jugg: 543, + radar_pickup_repair: 544, + radar_steeringwheel: 545, + radar_trophy: 546, + radar_pickup_rocket_boost: 547, + radar_pickup_homing_rocket: 548, + radar_pickup_machinegun: 549, + radar_pickup_parachute: 550, + radar_pickup_time_5: 551, + radar_pickup_time_10: 552, + radar_pickup_time_15: 553, + radar_pickup_time_20: 554, + radar_pickup_time_30: 555, + radar_supplies: 556, + radar_property_bunker: 557, + radar_gr_wvm_1: 558, + radar_gr_wvm_2: 559, + radar_gr_wvm_3: 560, + radar_gr_wvm_4: 561, + radar_gr_wvm_5: 562, + radar_gr_wvm_6: 563, + radar_gr_covert_ops: 564, + radar_adversary_bunker: 565, + radar_gr_moc_upgrade: 566, + radar_gr_w_upgrade: 567, + radar_sm_cargo: 568, + radar_sm_hangar: 569, + radar_tf_checkpoint: 570, + radar_race_tf: 571, + radar_sm_wp1: 572, + radar_sm_wp2: 573, + radar_sm_wp3: 574, + radar_sm_wp4: 575, + radar_sm_wp5: 576, + radar_sm_wp6: 577, + radar_sm_wp7: 578, + radar_sm_wp8: 579, + radar_sm_wp9: 580, + radar_sm_wp10: 581, + radar_sm_wp11: 582, + radar_sm_wp12: 583, + radar_sm_wp13: 584, + radar_sm_wp14: 585, + radar_nhp_bag: 586, + radar_nhp_chest: 587, + radar_nhp_orbit: 588, + radar_nhp_veh1: 589, + radar_nhp_base: 590, + radar_nhp_overlay: 591, + radar_nhp_turret: 592, + radar_nhp_mg_firewall: 593, + radar_nhp_mg_node: 594, + radar_nhp_wp1: 595, + radar_nhp_wp2: 596, + radar_nhp_wp3: 597, + radar_nhp_wp4: 598, + radar_nhp_wp5: 599, + radar_nhp_wp6: 600, + radar_nhp_wp7: 601, + radar_nhp_wp8: 602, + radar_nhp_wp9: 603, + radar_nhp_cctv: 604, + radar_nhp_starterpack: 605, + radar_nhp_turret_console: 606, + radar_nhp_mg_mir_rotate: 607, + radar_nhp_mg_mir_static: 608, + radar_nhp_mg_proxy: 609, + radar_acsr_race_target: 610, + radar_acsr_race_hotring: 611, + radar_acsr_wp1: 612, + radar_acsr_wp2: 613, + radar_bat_club_property: 614, + radar_bat_cargo: 615, + radar_bat_truck: 616, + radar_bat_hack_jewel: 617, + radar_bat_hack_gold: 618, + radar_bat_keypad: 619, + radar_bat_hack_target: 620, + radar_pickup_dtb_health: 621, + radar_pickup_dtb_blast_increase: 622, + radar_pickup_dtb_blast_decrease: 623, + radar_pickup_dtb_bomb_increase: 624, + radar_pickup_dtb_bomb_decrease: 625, + radar_bat_rival_club: 626, + radar_bat_drone: 627, + radar_bat_cash_reg: 628, + radar_cctv: 629, + radar_bat_assassinate: 630, + radar_bat_pbus: 631, + radar_bat_wp1: 632, + radar_bat_wp2: 633, + radar_bat_wp3: 634, + radar_bat_wp4: 635, + radar_bat_wp5: 636, + radar_bat_wp6: 637, + radar_blimp_2: 638, + radar_oppressor_2: 639, + radar_bat_wp7: 640, + radar_arena_series: 641, + radar_arena_premium: 642, + radar_arena_workshop: 643, + radar_race_wars: 644, + radar_arena_turret: 645, + radar_arena_rc_car: 646, + radar_arena_rc_workshop: 647, + radar_arena_trap_fire: 648, + radar_arena_trap_flip: 649, + radar_arena_trap_sea: 650, + radar_arena_trap_turn: 651, + radar_arena_trap_pit: 652, + radar_arena_trap_mine: 653, + radar_arena_trap_bomb: 654, + radar_arena_trap_wall: 655, + radar_arena_trap_brd: 656, + radar_arena_trap_sbrd: 657, + radar_arena_bruiser: 658, + radar_arena_brutus: 659, + radar_arena_cerberus: 660, + radar_arena_deathbike: 661, + radar_arena_dominator: 662, + radar_arena_impaler: 663, + radar_arena_imperator: 664, + radar_arena_issi: 665, + radar_arena_sasquatch: 666, + radar_arena_scarab: 667, + radar_arena_slamvan: 668, + radar_arena_zr380: 669, + radar_ap: 670, + radar_comic_store: 671, + radar_cop_car: 672, + radar_rc_time_trials: 673, + radar_king_of_the_hill: 674, + radar_king_of_the_hill_teams: 675, + radar_rucksack: 676, + radar_shipping_container: 677, + radar_agatha: 678, + radar_casino: 679, + radar_casino_table_games: 680, + radar_casino_wheel: 681, + radar_casino_concierge: 682, + radar_casino_chips: 683, + radar_casino_horse_racing: 684, + radar_adversary_featured: 685, + radar_roulette_1: 686, + radar_roulette_2: 687, + radar_roulette_3: 688, + radar_roulette_4: 689, + radar_roulette_5: 690, + radar_roulette_6: 691, + radar_roulette_7: 692, + radar_roulette_8: 693, + radar_roulette_9: 694, + radar_roulette_10: 695, + radar_roulette_11: 696, + radar_roulette_12: 697, + radar_roulette_13: 698, + radar_roulette_14: 699, + radar_roulette_15: 700, + radar_roulette_16: 701, + radar_roulette_17: 702, + radar_roulette_18: 703, + radar_roulette_19: 704, + radar_roulette_20: 705, + radar_roulette_21: 706, + radar_roulette_22: 707, + radar_roulette_23: 708, + radar_roulette_24: 709, + radar_roulette_25: 710, + radar_roulette_26: 711, + radar_roulette_27: 712, + radar_roulette_28: 713, + radar_roulette_29: 714, + radar_roulette_30: 715, + radar_roulette_31: 716, + radar_roulette_32: 717, + radar_roulette_33: 718, + radar_roulette_34: 719, + radar_roulette_35: 720, + radar_roulette_36: 721, + radar_roulette_0: 722, + radar_roulette_00: 723, + radar_limo: 724, + radar_weapon_alien: 725, + radar_race_open_wheel: 726, + radar_rappel: 727, + radar_swap_car: 728, + radar_scuba_gear: 729, + radar_cpanel_1: 730, + radar_cpanel_2: 731, + radar_cpanel_3: 732, + radar_cpanel_4: 733, + radar_snow_truck: 734, + radar_buggy_1: 735, + radar_buggy_2: 736, + radar_zhaba: 737, + radar_gerald: 738, + radar_ron: 739, + radar_arcade: 740, + radar_drone_controls: 741, + radar_rc_tank: 742, + radar_stairs: 743, + radar_camera_2: 744, + radar_winky: 745, + radar_mini_sub: 746, + radar_kart_retro: 747, + radar_kart_modern: 748, + radar_military_quad: 749, + radar_military_truck: 750, + radar_ship_wheel: 751, + radar_ufo: 752, + radar_seasparrow2: 753, + radar_dinghy2: 754, + radar_patrol_boat: 755, + radar_retro_sports_car: 756, + radar_squadee: 757, + radar_folding_wing_jet: 758, + radar_valkyrie2: 759, + radar_sub2: 760, + radar_bolt_cutters: 761, + radar_rappel_gear: 762, + radar_keycard: 763, + radar_password: 764, + radar_island_heist_prep: 765, + radar_island_party: 766, + radar_control_tower: 767, + radar_underwater_gate: 768, + radar_power_switch: 769, + radar_compound_gate: 770, + radar_rappel_point: 771, + radar_keypad: 772, + radar_sub_controls: 773, + radar_sub_periscope: 774, + radar_sub_missile: 775, + radar_painting: 776, + radar_car_meet: 777, + radar_car_test_area: 778, + radar_auto_shop_property: 779, + radar_docks_export: 780, + radar_prize_car: 781, + radar_test_car: 782, + radar_car_robbery_board: 783, + radar_car_robbery_prep: 784, + radar_street_race_series: 785, + radar_pursuit_series: 786, + radar_car_meet_organiser: 787, + radar_securoserv: 788, + radar_bounty_collectibles: 789, + radar_movie_collectibles: 790, + radar_trailer_ramp: 791, + radar_race_organiser: 792, + radar_chalkboard_list: 793, + radar_export_vehicle: 794, + radar_train: 795, + radar_heist_diamond: 796, + radar_heist_doomsday: 797, + radar_heist_island: 798, + radar_slamvan2: 799, + radar_crusader: 800, + radar_construction_outfit: 801, + radar_overlay_jammed: 802, + radar_heist_island_unavailable: 803, + radar_heist_diamond_unavailable: 804, + radar_heist_doomsday_unavailable: 805, + radar_placeholder_7: 806, + radar_placeholder_8: 807, + radar_placeholder_9: 808, + radar_featured_series: 809, + radar_vehicle_for_sale: 810, + radar_van_keys: 811, + radar_suv_service: 812, + radar_security_contract: 813, + radar_safe: 814, + radar_ped_r: 815, + radar_ped_e: 816, + radar_payphone: 817, + radar_patriot3: 818, + radar_music_studio: 819, + radar_jubilee: 820, + radar_granger2: 821, + radar_explosive_charge: 822, + radar_deity: 823, + radar_d_champion: 824, + radar_buffalo4: 825, + radar_agency: 826, + radar_biker_bar: 827, + radar_simeon_overlay: 828, + radar_junk_skydive: 829, + radar_luxury_car_showroom: 830, + radar_car_showroom: 831, + radar_car_showroom_simeon: 832, + radar_flaming_skull: 833, + radar_weapon_ammo: 834, + radar_community_series: 835, + radar_cayo_series: 836, + radar_clubhouse_contract: 837, + radar_agent_ulp: 838, + radar_acid: 839, + radar_acid_lab: 840, + radar_dax_overlay: 841, + radar_dead_drop_package: 842, + radar_downtown_cab: 843, + radar_gun_van: 844, + radar_stash_house: 845, + radar_tractor: 846, + radar_warehouse_juggalo: 847, + radar_warehouse_juggalo_dax: 848, + radar_weapon_crowbar: 849, + radar_duffel_bag: 850, + radar_oil_tanker: 851, + radar_acid_lab_tent: 852, + radar_van_burrito: 853, + radar_acid_boost: 854, + radar_ped_gang_leader: 855, + radar_multistorey_garage: 856, + radar_seized_asset_sales: 857, + radar_cayo_attrition: 858, + radar_bicycle: 859, + radar_bicycle_trial: 860, + radar_raiju: 861, + radar_conada2: 862, + radar_overlay_ready_for_sell: 863, + radar_overlay_missing_supplies: 864, + radar_streamer216: 865, + radar_signal_jammer: 866, + radar_salvage_yard: 867, + radar_robbery_prep_equipment: 868, + radar_robbery_prep_overlay: 869, + radar_yusuf: 870, + radar_vincent: 871, + radar_vinewood_garage: 872, + radar_lstb: 873, + radar_cctv_workstation: 874, + radar_hacking_device: 875, + radar_race_drag: 876, + radar_race_drift: 877, + radar_casino_prep: 878, + radar_planning_wall: 879, + radar_weapon_crate: 880, + radar_weapon_snowball: 881, + radar_train_signals_green: 882, + radar_train_signals_red: 883, +}; diff --git a/src/main/shared/events/index.ts b/src/main/shared/events/index.ts index 69ef82362..26a162ec2 100644 --- a/src/main/shared/events/index.ts +++ b/src/main/shared/events/index.ts @@ -62,6 +62,9 @@ export const Events = { play: { local: 'audio:player:sound:2d', }, + stop: { + local: 'udio:player:sound:2d:stop', + }, }, controls: { set: 'player:controls:set', @@ -71,6 +74,7 @@ export const Events = { }, native: { invoke: 'player:native:invoke', + invokeWithResult: 'player:native:invoke:with:result', }, notify: { notification: { @@ -138,6 +142,7 @@ export const Events = { getFocusedEntity: 'systems:raycast:get:focused:entity', getFocusedPosition: 'systems:raycast:get:focused:position', getFocusedObject: 'systems:raycast:get:focused:object', + getFocusedCustom: 'systems:raycast:get:focused:custom', }, screenshot: { get: 'systems:screenshot:get', diff --git a/src/main/shared/types/account.ts b/src/main/shared/types/account.ts index dfb5b1db0..a8c5e5b1b 100644 --- a/src/main/shared/types/account.ts +++ b/src/main/shared/types/account.ts @@ -1,7 +1,9 @@ +import {GroupsDocumentMixin, PermissionsDocumentMixin} from "@Shared/types/permissions.js"; + /** * Used to store Discord Information, IPs, and User Data */ -export type Account = { +export interface Account extends PermissionsDocumentMixin, GroupsDocumentMixin { /** * A unique MongoDB identifier to identify the accoutn in the database. * @type {*} @@ -79,4 +81,4 @@ export type Account = { * @type {string} */ password?: string; -}; +} diff --git a/src/main/shared/types/blip.ts b/src/main/shared/types/blip.ts index 783dd5272..e7fa57e35 100644 --- a/src/main/shared/types/blip.ts +++ b/src/main/shared/types/blip.ts @@ -1,93 +1,94 @@ import * as alt from 'alt-shared'; +import { BlipNames } from '../data/blipNames.js'; -export enum BlipColor { - WHITE_1 = 0, - RED = 1, - GREEN = 2, - BLUE = 3, - WHITE_2 = 4, - YELLOW = 5, - LIGHT_RED = 6, - VIOLET = 7, - PINK = 8, - LIGHT_ORANGE = 9, - LIGHT_BROWN = 10, - LIGHT_GREEN = 11, - LIGHT_BLUE = 12, - LIGHT_PURPLE = 13, - DARK_PURPLE = 14, - CYAN = 15, - LIGHT_YELLOW = 16, - ORANGE = 17, - LIGHT_BLUE_2 = 18, - DARK_PINK = 19, - DARK_YELLOW = 20, - DARK_ORANGE = 21, - LIGHT_GRAY = 22, - LIGHT_PINK = 23, - LEMON_GREEN = 24, - FOREST_GREEN = 25, - ELECTRIC_BLUE = 26, - BRIGHT_PURPLE = 27, - DARK_YELLOW_2 = 28, - DARK_BLUE = 29, - DARK_CYAN = 30, - LIGHT_BROWN_2 = 31, - LIGHT_BLUE_3 = 32, - LIGHT_YELLOW_2 = 33, - LIGHT_PINK_2 = 34, - LIGHT_RED_2 = 35, - BEIGE = 36, - WHITE_3 = 37, - BLUE_2 = 38, - LIGHT_GRAY_2 = 39, - DARK_GRAY = 40, - PINK_RED = 41, - BLUE_3 = 42, - LIGHT_GREEN_2 = 43, - LIGHT_ORANGE_2 = 44, - WHITE_4 = 45, - GOLD = 46, - ORANGE_2 = 47, - BRILLIANT_ROSE = 48, - RED_2 = 49, - MEDIUM_PURPLE = 50, - SALMON = 51, - DARK_GREEN = 52, - BLIZZARD_BLUE = 53, - ORACLE_BLUE = 54, - SILVER = 55, - BROWN = 56, - BLUE_4 = 57, - EAST_BAY = 58, - RED_3 = 59, - YELLOW_ORANGE = 60, - MULBERRY_PINK = 61, - ALTO_GRAY = 62, - JELLY_BEAN_BLUE = 63, - DARK_ORANGE_2 = 64, - MAMBA = 65, - YELLOW_ORANGE_2 = 66, - BLUE_5 = 67, - BLUE_6 = 68, - GREEN_2 = 69, - YELLOW_ORANGE_3 = 70, - YELLOW_ORANGE_4 = 71, - TRANSPARENT_BLACK = 72, - YELLOW_ORANGE_5 = 73, - BLUE_7 = 74, - RED_4 = 75, - DEEP_RED = 76, - BLUE_8 = 77, - ORACLE_BLUE_2 = 78, - TRANSPARENT_RED = 79, - TRANSPARENT_BLUE = 80, - ORANGE_3 = 81, - LIGHT_GREEN_3 = 82, - PURPLE = 83, - BLUE_9 = 84, - TRANSPARENT_BLACK_2 = 85, -} +export const BlipColor = { + WHITE_1: 0, + RED: 1, + GREEN: 2, + BLUE: 3, + WHITE_2: 4, + YELLOW: 5, + LIGHT_RED: 6, + VIOLET: 7, + PINK: 8, + LIGHT_ORANGE: 9, + LIGHT_BROWN: 10, + LIGHT_GREEN: 11, + LIGHT_BLUE: 12, + LIGHT_PURPLE: 13, + DARK_PURPLE: 14, + CYAN: 15, + LIGHT_YELLOW: 16, + ORANGE: 17, + LIGHT_BLUE_2: 18, + DARK_PINK: 19, + DARK_YELLOW: 20, + DARK_ORANGE: 21, + LIGHT_GRAY: 22, + LIGHT_PINK: 23, + LEMON_GREEN: 24, + FOREST_GREEN: 25, + ELECTRIC_BLUE: 26, + BRIGHT_PURPLE: 27, + DARK_YELLOW_2: 28, + DARK_BLUE: 29, + DARK_CYAN: 30, + LIGHT_BROWN_2: 31, + LIGHT_BLUE_3: 32, + LIGHT_YELLOW_2: 33, + LIGHT_PINK_2: 34, + LIGHT_RED_2: 35, + BEIGE: 36, + WHITE_3: 37, + BLUE_2: 38, + LIGHT_GRAY_2: 39, + DARK_GRAY: 40, + PINK_RED: 41, + BLUE_3: 42, + LIGHT_GREEN_2: 43, + LIGHT_ORANGE_2: 44, + WHITE_4: 45, + GOLD: 46, + ORANGE_2: 47, + BRILLIANT_ROSE: 48, + RED_2: 49, + MEDIUM_PURPLE: 50, + SALMON: 51, + DARK_GREEN: 52, + BLIZZARD_BLUE: 53, + ORACLE_BLUE: 54, + SILVER: 55, + BROWN: 56, + BLUE_4: 57, + EAST_BAY: 58, + RED_3: 59, + YELLOW_ORANGE: 60, + MULBERRY_PINK: 61, + ALTO_GRAY: 62, + JELLY_BEAN_BLUE: 63, + DARK_ORANGE_2: 64, + MAMBA: 65, + YELLOW_ORANGE_2: 66, + BLUE_5: 67, + BLUE_6: 68, + GREEN_2: 69, + YELLOW_ORANGE_3: 70, + YELLOW_ORANGE_4: 71, + TRANSPARENT_BLACK: 72, + YELLOW_ORANGE_5: 73, + BLUE_7: 74, + RED_4: 75, + DEEP_RED: 76, + BLUE_8: 77, + ORACLE_BLUE_2: 78, + TRANSPARENT_RED: 79, + TRANSPARENT_BLUE: 80, + ORANGE_3: 81, + LIGHT_GREEN_3: 82, + PURPLE: 83, + BLUE_9: 84, + TRANSPARENT_BLACK_2: 85, +}; export type Blip = { /** @@ -109,7 +110,7 @@ export type Blip = { * @type {boolean} * */ - shortRange: boolean; + shortRange?: boolean; /** * The blip appearance which is known as a 'sprite'. @@ -118,14 +119,14 @@ export type Blip = { * @type {number} * */ - sprite: number; + sprite: number | keyof typeof BlipNames; /** * The color of this * @type {number} * */ - color: BlipColor | number; + color: number | keyof typeof BlipColor; /** * The text / name of this blip. Can be whatever. diff --git a/src/main/shared/types/character.ts b/src/main/shared/types/character.ts index 7e31719fd..5a9a28a43 100644 --- a/src/main/shared/types/character.ts +++ b/src/main/shared/types/character.ts @@ -1,6 +1,7 @@ import * as alt from 'alt-shared'; import { Appearance } from './appearance.js'; import { ClothingComponent } from './clothingComponent.js'; +import {GroupsDocumentMixin, PermissionsDocumentMixin} from "@Shared/types/index.js"; export interface BaseCharacter { /** @@ -56,7 +57,7 @@ export interface BaseCharacter { * * @interface Character */ -export interface Character extends BaseCharacter { +export interface Character extends BaseCharacter, PermissionsDocumentMixin, GroupsDocumentMixin { /** * The character identifier in the database. * @type {*} @@ -136,14 +137,6 @@ export interface Character extends BaseCharacter { */ interior?: number | undefined; - /** - * Permissions for a given user. - * - * @type {Array} - * - */ - permissions?: Array; - /** * Clothes that will be applied to the player last. * Uniforms should be used in tandem with typical inventory clothing. @@ -172,25 +165,10 @@ export interface Character extends BaseCharacter { */ skin?: string | number; - /** - * A key value pair that contains a list of permissions a character has for a group. - * - * @type {{ [key: string]: Array }} - * @memberof Character - */ - groups?: { [key: string]: Array }; - /** * Player weapons that the player currently has equipped * - * @type {alt.IWeapon[]} - */ - weapons?: alt.IWeapon[]; - - /** - * Ammo the player has based on weapon hash - * - * @type {{ [weaponHash: string]: number }} + * @type {(alt.IWeapon & { ammo: number })[]} */ - ammo?: { [weaponHash: string]: number }; + weapons?: (alt.IWeapon & { ammo: number })[]; } diff --git a/src/main/shared/types/doors.ts b/src/main/shared/types/doors.ts index 85ec67170..439ddb83c 100644 --- a/src/main/shared/types/doors.ts +++ b/src/main/shared/types/doors.ts @@ -1,4 +1,5 @@ import * as alt from 'alt-shared'; +import {PermissionOptions} from "@Shared/types/index.js"; export enum DoorState { @@ -7,7 +8,7 @@ export enum DoorState { } -export interface Door { +export interface Door extends PermissionOptions { /** * Unique identifier for the door. * @@ -35,30 +36,6 @@ export interface 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 { diff --git a/src/main/shared/types/index.ts b/src/main/shared/types/index.ts index 2449a3d4b..6d6a4dc36 100644 --- a/src/main/shared/types/index.ts +++ b/src/main/shared/types/index.ts @@ -1,5 +1,6 @@ export * from './account.js'; export * from './appearance.js'; +export * from './attachment.js'; export * from './blip.js'; export * from './character.js'; export * from './clothingComponent.js'; @@ -7,9 +8,19 @@ export * from './credits.js'; export * from './doors.js'; export * from './instructionalButtons.js'; export * from './marker.js'; +export * from './message.js'; export * from './nativeMenu.js'; export * from './object.js'; +export * from './omitFirstArg.js'; +export * from './pedOptions.js'; +export * from './permissions.js'; +export * from './pickup.js'; +export * from './playerStats.js'; +export * from './progressBar.js'; +export * from './serverConfig.js'; export * from './shard.js'; export * from './spinner.js'; export * from './textLabel.js'; export * from './vehicle.js'; +export * from './webview.js'; +export * from './worldMenu.js'; diff --git a/src/main/shared/types/items.ts b/src/main/shared/types/items.ts new file mode 100644 index 000000000..01ef40c64 --- /dev/null +++ b/src/main/shared/types/items.ts @@ -0,0 +1,72 @@ +export interface RebarBaseItem { + /** + * A general purpose item identifier. + * + * Used for things like `food-burger` + * + * @type {keyof RebarItems} + */ + id: keyof RebarItems; + + /** + * The unique name of the item + * + * @type {string} + */ + name: string; + + /** + * The description of the item + * + * @type {string} + */ + desc: string; + + /** + * The maximum amount of items that can exist in this stack of items + * + * @type {number} + */ + maxStack: number; + + /** + * Weight per item, this is not the total weight. + * + * You'll want to do `quantity * weight` to see the total weight of the stack. + * + * @type {number} + */ + weight: number; + + /** + * Icon for the item with extension + * + * ie. `icon-burger.png` + * + * @type {string} + */ + icon: string; +} + +export type Item = { + /** + * A unique string that is attached to the item. + * + * @type {string} + */ + uid: string; + + /** + * The number of items in the stack of items + * + * @type {number} + */ + quantity: number; + + /** + * Any custom data that belongs to the item + * + * @type {{ [key: string]: any }} + */ + data?: { [key: string]: any }; +} & RebarBaseItem; diff --git a/src/main/shared/types/omitFirstArg.ts b/src/main/shared/types/omitFirstArg.ts new file mode 100644 index 000000000..4ee3a8d06 --- /dev/null +++ b/src/main/shared/types/omitFirstArg.ts @@ -0,0 +1 @@ +export type OmitFirstArg = F extends (x: any, ...args: infer P) => infer R ? (...args: P) => R : never; diff --git a/src/main/shared/types/permissions.ts b/src/main/shared/types/permissions.ts new file mode 100644 index 000000000..2160b3bc9 --- /dev/null +++ b/src/main/shared/types/permissions.ts @@ -0,0 +1,30 @@ +export interface PermissionsDocumentMixin { + /** + * The permissions associated with this document. + * @type {string[]} + */ + permissions?: string[]; +} + + +export interface GroupsDocumentMixin { + /** + * The groups associated with this document. + * @type {string[]} + */ + groups?: string[]; +} + +export type Permission = string | string[] | PermissionLogic; +type PermissionLogic = And | Or; +type And = { and: Permission[] }; +type Or = { or: Permission[] }; + +export interface PermissionOptions { + /** + * Permissions required to access this entity. + * + * @type {Permission} + */ + permissions?: Permission; +} diff --git a/src/main/shared/types/vehicle.ts b/src/main/shared/types/vehicle.ts index 0779326ee..554856bcd 100644 --- a/src/main/shared/types/vehicle.ts +++ b/src/main/shared/types/vehicle.ts @@ -110,32 +110,6 @@ export interface Vehicle { */ windows?: { [key: string]: number }; - /** - * Neon color for the vehicle - * - * @type {alt.RGBA} - * @memberof Vehicle - */ - neonColor?: alt.RGBA; - - /** - * Where the placement of the neon is - * - * @type {{ - * front: boolean, - * back: boolean, - * left: boolean, - * right: boolean - * }} - * @memberof Vehicle - */ - neonPlacement?: { - front: boolean; - back: boolean; - left: boolean; - right: boolean; - }; - stateProps?: { /** * Dirt level of the vehicle @@ -195,53 +169,91 @@ export interface Vehicle { daylightOn?: boolean; }; - /** - * Set a primary color based on GTAV colors - * - * @type {number} - * @memberof Vehicle - */ - primaryColor?: number; + neon?: { + /** + * Neon color for the vehicle + * + * @type {alt.RGBA} + * @memberof Vehicle + */ + color: alt.RGBA; - /** - * Set a secondary color based on GTAV colors - * - * @type {number} - * @memberof Vehicle - */ - secondaryColor?: number; + /** + * Where the placement of the neon is + * + * @type {{ + * front: boolean, + * back: boolean, + * left: boolean, + * right: boolean + * }} + * @memberof Vehicle + */ + placement: { + front: boolean; + back: boolean; + left: boolean; + right: boolean; + }; + }; - /** - * The custom primary color to set on the vehicle - * - * @type {alt.RGBA} - * @memberof Vehicle - */ - customPrimaryColor?: alt.RGBA; + color: { + /** + * Set a primary color based on GTAV colors + * + * @type {number} + * @memberof Vehicle + */ + primary: number; - /** - * The custom secondary color to set on the vehicle - * - * @type {alt.RGBA} - * @memberof Vehicle - */ - customSecondaryColor?: alt.RGBA; + /** + * The custom primary color to set on the vehicle + * + * @type {alt.RGBA} + * @memberof Vehicle + */ + primaryCustom: alt.RGBA; - /** - * The wheel color to set on the vehicle (0-159) - * - * @type {alt.RGBA} - * @memberof Vehicle - */ - wheelColor?: number; + /** + * Set a secondary color based on GTAV colors + * + * @type {number} + * @memberof Vehicle + */ + secondary: number; - /** - * The pearl color to set on the vehicle (0-159) - * - * @type {number} - * @memberof Vehicle - */ - pearlColor?: number; + /** + * The custom secondary color to set on the vehicle + * + * @type {alt.RGBA} + * @memberof Vehicle + */ + secondaryCustom: alt.RGBA; + + /** + * The wheel color to set on the vehicle (0-159) + * + * @type {alt.RGBA} + * @memberof Vehicle + */ + wheel: number; + + /** + * The pearl color to set on the vehicle (0-159) + * + * @type {number} + * @memberof Vehicle + */ + pearl: number; + + /** + * The xenon (headlight) color to set on the vehicle (0-13) + * + * @type {number} + * @memberof Vehicle + */ + xenon: number; + }; /** * The number plate text for the vehicle diff --git a/webview/composables/useAudio.ts b/webview/composables/useAudio.ts index eda03a3c3..b8bc006a1 100644 --- a/webview/composables/useAudio.ts +++ b/webview/composables/useAudio.ts @@ -4,30 +4,36 @@ import { useEvents } from './useEvents.js'; const events = useEvents(); let isInitialized = false; +let currentAudio: HTMLAudioElement | null = null; export function useAudio() { if (!isInitialized) { isInitialized = true; events.on(Events.player.audio.play.local, play); + events.on(Events.player.audio.stop.local, stop); } async function play(path: string, volume: number = 1) { + stop(); + const audio = new Audio(path); audio.volume = volume; audio.loop = false; await audio.play(); - // this._audio[soundID] = new Audio(path); - // this._audio[soundID].soundID = soundID; - // this._audio[soundID].addEventListener('ended', this.audioStopped); - // this._audio[soundID].crossOrigin = 'anonymous'; - // const ambientContext = new AudioContext(); - // const source = ambientContext.createMediaElementSource(this._audio[soundID]); - // this._ambientPan[soundID] = ambientContext.createStereoPanner(); - // source.connect(this._ambientPan[soundID]); - // this._ambientPan[soundID].connect(ambientContext.destination); - // this._audio[soundID].setAttribute('src', path); - // this._ambientPan[soundID].pan.value = pan; + currentAudio = audio; + + audio.addEventListener('ended', () => { + currentAudio = null; + }); + } + + function stop() { + if (currentAudio) { + currentAudio.pause(); + currentAudio.currentTime = 0; + currentAudio = null; + } } /** diff --git a/webview/composables/useLocalStorage.ts b/webview/composables/useLocalStorage.ts index 720d9b13b..60f71c3ae 100644 --- a/webview/composables/useLocalStorage.ts +++ b/webview/composables/useLocalStorage.ts @@ -1,6 +1,7 @@ import { Events } from '../../src/main/shared/events/index.js'; const requests: { [key: string]: Function } = {}; +const mock: { [key: string]: any } = {}; let isInit = false; @@ -17,12 +18,13 @@ export function useLocalStorage() { if (!isInit) { isInit = true; if ('alt' in window) { - alt.on(Events.view.localStorageGet, handleLocalStorage) + alt.on(Events.view.localStorageGet, handleLocalStorage); } } function set(key: string, value: any) { if (!('alt' in window)) { + mock[key] = value; return; } @@ -30,24 +32,25 @@ export function useLocalStorage() { } async function get(key: string): Promise { + if (!('alt' in window)) { + return mock[key]; + } + const result = await new Promise((resolve: Function) => { const callback = (result: any) => { - return resolve(result) - } + return resolve(result); + }; requests[key] = callback; - if (!('alt' in window)) { - return; - } - alt.emit(Events.view.localStorageGet, key); - }) + }); return result as T; } function remove(key: string) { if (!('alt' in window)) { + delete mock[key]; return; } @@ -58,5 +61,5 @@ export function useLocalStorage() { get, remove, set, - } + }; } diff --git a/webview/public/fonts/.gitkeep b/webview/public/fonts/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/webview/public/images/.gitkeep b/webview/public/images/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/webview/public/sounds/.gitkeep b/webview/public/sounds/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/webview/src/components/Development.vue b/webview/src/components/Development.vue index ed8d205e3..75c372f03 100644 --- a/webview/src/components/Development.vue +++ b/webview/src/components/Development.vue @@ -30,18 +30,22 @@ const pages = computed(() => {