From 48a9be08a30873eebc44181eed04008764b308ea Mon Sep 17 00:00:00 2001 From: stuyk Date: Thu, 23 May 2024 11:54:38 -0600 Subject: [PATCH] feat: useWeapon, useState, some sync stuff, saving stuff, etc. --- docs/api/server/controllers/blip.md | 2 +- .../api/server/document/document-character.md | 14 ++ docs/api/server/document/document-vehicle.md | 12 + docs/api/server/player/player-appearance.md | 2 +- docs/api/server/player/player-clothing.md | 2 +- docs/api/server/player/player-state.md | 17 ++ docs/api/server/player/player-weapon.md | 44 ++++ docs/api/server/vehicle/vehicle-use.md | 27 +++ docs/changelog.md | 38 +++ docs/webview/composables/use-audio.md | 6 + src/main/client/webview/index.ts | 22 +- src/main/server/document/character.ts | 17 +- src/main/server/document/vehicle.ts | 7 +- src/main/server/index.ts | 12 +- src/main/server/player/appearance.ts | 166 +++++++------ src/main/server/player/clothing.ts | 148 ++++++------ src/main/server/player/state.ts | 77 +++++++ src/main/server/player/weapon.ts | 190 +++++++++++++++ src/main/server/systems/messenger.ts | 6 +- src/main/server/vehicle/index.ts | 218 ++++++++++++++++++ src/main/shared/events/index.ts | 2 + src/main/shared/types/character.ts | 103 +++++---- src/main/shared/types/vehicle.ts | 96 +++++++- webview/composables/useAudio.ts | 18 +- webview/composables/useEvents.ts | 29 +++ 25 files changed, 1079 insertions(+), 196 deletions(-) create mode 100644 docs/api/server/player/player-state.md create mode 100644 docs/api/server/player/player-weapon.md create mode 100644 docs/api/server/vehicle/vehicle-use.md create mode 100644 src/main/server/player/state.ts create mode 100644 src/main/server/player/weapon.ts create mode 100644 src/main/server/vehicle/index.ts diff --git a/docs/api/server/controllers/blip.md b/docs/api/server/controllers/blip.md index 049c0a144..4efc26079 100644 --- a/docs/api/server/controllers/blip.md +++ b/docs/api/server/controllers/blip.md @@ -17,7 +17,7 @@ import { BlipColor } from '@Shared/types/blip.js'; const Rebar = useRebar(); // Create a global blip -const blip = Rebar.controllers.useBlipGlobal(player, { +const blip = Rebar.controllers.useBlipGlobal({ pos: SpawnPos, color: BlipColor.BLUE, sprite: 57, diff --git a/docs/api/server/document/document-character.md b/docs/api/server/document/document-character.md index 364d04992..926def2d0 100644 --- a/docs/api/server/document/document-character.md +++ b/docs/api/server/document/document-character.md @@ -8,6 +8,20 @@ It automatically saves data to the MongoDB database when any `set` function is u You should bind character data after fetching call characters owned by an account. +When you bind character data to a player the following is synchronized: + +- Position +- Rotation +- Clothing +- Appearance +- Model +- Skin +- Weapons +- Weapon Ammo +- Health +- Armor +- Dimension + ```ts import { useRebar } from '@Server/index.js'; diff --git a/docs/api/server/document/document-vehicle.md b/docs/api/server/document/document-vehicle.md index de25f02d2..2fee3aa49 100644 --- a/docs/api/server/document/document-vehicle.md +++ b/docs/api/server/document/document-vehicle.md @@ -8,6 +8,18 @@ It automatically saves data to the MongoDB database when any `set` function is u You should bind character data after fetching call characters owned by an account. +When you bind vehicle data to a vehicle the following is synchronized: + +- Position +- Rotation +- Model +- Mods +- Health +- Windows +- Wheels +- Extras +- Dimension + ```ts import { useRebar } from '@Server/index.js'; diff --git a/docs/api/server/player/player-appearance.md b/docs/api/server/player/player-appearance.md index b989b9ad5..63c9a04eb 100644 --- a/docs/api/server/player/player-appearance.md +++ b/docs/api/server/player/player-appearance.md @@ -45,5 +45,5 @@ appearance.setModel(true); appearance.setTattoos([{ collection: 'mpairraces_overlays', overlay: 'MP_Airraces_Tattoo_000_M' }]); // Called automatically, but resynchronizes freeroam player appearance -appearance.update(); +appearance.sync(); ``` diff --git a/docs/api/server/player/player-clothing.md b/docs/api/server/player/player-clothing.md index d99e4aaef..63daa4064 100644 --- a/docs/api/server/player/player-clothing.md +++ b/docs/api/server/player/player-clothing.md @@ -56,5 +56,5 @@ clothing.setClothingComponent('mask', alt.hash('some_dlc'), 5, 0, 0); clothing.setPropComponent('glasses', alt.hash('some_dlc'), 5, 0); // Forces character clothing to update, and rerenders everything -clothing.update(); +clothing.sync(); ``` diff --git a/docs/api/server/player/player-state.md b/docs/api/server/player/player-state.md new file mode 100644 index 000000000..9cf91354a --- /dev/null +++ b/docs/api/server/player/player-state.md @@ -0,0 +1,17 @@ +# Weapon + +Used to synchronize or apply weapons to a player. + +```ts +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +const playerState = Rebar.player.useState(somePlayer); + +// Use character document for weapons +playerState.sync(); + +// Override and apply some state to a player +playerWeapons.apply({ pos: alt.Vector3.zero }); +``` diff --git a/docs/api/server/player/player-weapon.md b/docs/api/server/player/player-weapon.md new file mode 100644 index 000000000..732d8810e --- /dev/null +++ b/docs/api/server/player/player-weapon.md @@ -0,0 +1,44 @@ +# Weapon + +Used to synchronize or apply weapons to a player. + +```ts +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +const playerWeapons = Rebar.player.useWeapon(somePlayer); + +// Use character document for weapons +playerWeapons.sync(); + +// Save weapons & ammo +await playerWeapons.save(); + +// Save just ammo +await playerWeapons.saveAmmo(); + +// Add a weapon, and save to the database, and re-apply weapons +await playerWeapons.add('WEAPON_MINIGUN', 100); + +// Add ammo for specific gun +await playerWeapons.addAmmo('WEAPON_MINIGUN', 100); + +// Clear all weapons & ammo +await playerWeapons.clear(); + +// Remove a weapon and all ammo for the weapon +await playerWeapons.clearWeapon('WEAPON_MINIGUN'); + +// 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 }, +]; +const ammo = { + [alt.hash('WEAPON_MINIGUN')]: 999, + [alt.hash('WEAPON_RPG')]: 5, +}; + +playerWeapons.apply(weapons, ammo); +``` diff --git a/docs/api/server/vehicle/vehicle-use.md b/docs/api/server/vehicle/vehicle-use.md new file mode 100644 index 000000000..5a6dca228 --- /dev/null +++ b/docs/api/server/vehicle/vehicle-use.md @@ -0,0 +1,27 @@ +# useVehicle + +Used to create a new vehicle document, repair vehicles, apply vehicle documents, etc. + +```ts +import { useRebar } from '@Server/index.js'; + +const Rebar = useRebar(); + +// Used to apply mods, health, etc. to a vehicle if it has a document bound +Rebar.vehicle.useVehicle(vehicle).sync(); + +// Use a document template to any given vehicle, does not save data +Rebar.vehicle.useVehicle(vehicle).apply({ pos: new alt.Vector3(0, 0, 0) }); + +// Save all current damage, position, rotation etc. +// This does not create a new document for the given vehicle +Rebar.vehicle.useVehicle(vehicle).save(); + +// Correctly repairs the vehicle and returns the new vehicle instance +// Players will be removed from the vehicle on repair +await Rebar.vehicle.useVehicle(vehicle).repair(); + +// Creating a vehicle and assigning it to a character id, or some other identifier +const newVehicle = new alt.Vehicle('infernus', alt.Vector3.zero, alt.Vector3.zero); +const document = await Rebar.vehicle.useVehicle(newVehicle).create(someCharacterIdOrSomethingElse); +``` diff --git a/docs/changelog.md b/docs/changelog.md index d40ea1144..74a057def 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,44 @@ order: -1000 # Changelog +## Version 6 + +### Code Changes + +- Added `vehicle` synchronization when a vehicle document is bound to the vehicle +- Added `useVehicle` function for synchronizing vehicle data, applying data, repairing, and creating new vehicle documents + - Synchronizes damage (not appearance) + - Synchronizes position, and rotation + - Synchronizes window damage + - Synchronizes tire damage + - Synchronizes dirt levels + - Synchronizes mods +- Added `character` synchronization when a character document is bound to the player + - Synchronizes appearance, and clothing + - Synchronizes weapons, and ammo + - Synchronizes position, and rotation + - Synchronizes health, and armor + - Synchronizes death state +- Added ways to disable auto-sync for `vehicle` and `character` documents in the `binding` functions +- Added `onKeyUp` to the `Webview Events` functionality, allowing an easy way to listen for keybinds +- Added `playFrontendSound` to `useAudio` composable in the webview +- Added `useWeapon` to player pathway. Allows for synchronizing weapons, and ammo for database +- Added ability for commands to be `async` +- Separated logic for appyling data on `appearance` and `clothing` so overrides are possible +- Changed all `update()` functions to `sync` and added backwards compatible `update` function +- Split `Character` into `BaseCharacter` and `Character`, nothing changed externally + +### Docs Changes + +- Updated `blip` controller docs for typo +- Added `useVehicle` documentation +- Updated documentation for `useCharacterBinder` that will allow ignoring auto-sync on binding +- Updated documentation for `useVehicleBinder` that will allow ignoring auto-sync on binding +- Added `useWeapon` documentation +- Added `useState` documentation +- Changed `update()` references to `sync()` +- Updated documentation for `useAudio` composable + ## Version 5 ### Code Changes diff --git a/docs/webview/composables/use-audio.md b/docs/webview/composables/use-audio.md index d3938cb1a..03a56d966 100644 --- a/docs/webview/composables/use-audio.md +++ b/docs/webview/composables/use-audio.md @@ -2,6 +2,8 @@ Gives you the ability to play custom sounds from the Webview. +If you wish to play frontend sounds check out the [Frontend Sound List](../../data/frontend-sounds.md). + ```html ``` diff --git a/src/main/client/webview/index.ts b/src/main/client/webview/index.ts index ef7ebe0eb..7c8ece56a 100644 --- a/src/main/client/webview/index.ts +++ b/src/main/client/webview/index.ts @@ -1,4 +1,5 @@ import * as alt from 'alt-client'; +import * as native from 'natives'; import { Events } from '@Shared/events/index.js'; import { PageNames, PageType } from '@Shared/webview/index.js'; @@ -24,13 +25,32 @@ function handleClientEvent(event: string, ...args: any[]) { ClientEvents[event](...args); } +async function handleFrontendSound(audioName: string, audioRef: string, audioBank = '') { + if (audioBank !== '') { + native.requestScriptAudioBank(audioBank, false, -1); + } + + native.playSoundFrontend(-1, audioName, audioRef, true); +} + export function useWebview(path = 'http://assets/webview/index.html') { let isInitialized = true; if (!webview) { + isInitialized = false; webview = new alt.WebView(path); webview.unfocus(); - isInitialized = false; + webview.on(Events.view.playFrontendSound, handleFrontendSound); + alt.on('keyup', emitKeypress); + } + + /** + * Emits key presses to the webview + * + * @param {number} key + */ + function emitKeypress(key: number) { + webview.emit(Events.view.onKeypress, key); } /** diff --git a/src/main/server/document/character.ts b/src/main/server/document/character.ts index 1d732a38d..33d36294a 100644 --- a/src/main/server/document/character.ts +++ b/src/main/server/document/character.ts @@ -51,7 +51,9 @@ export function useCharacter(player: alt.Player) { * @param {(keyof KnownKeys)} fieldName * @return {ReturnType | undefined} */ - function getField(fieldName: keyof KnownKeys): ReturnType | undefined { + function getField = keyof KnownKeys>( + fieldName: K, + ): (Character & T)[K] | undefined { if (!player.hasMeta(sessionKey)) { return undefined; } @@ -290,13 +292,16 @@ export function useCharacter(player: alt.Player) { return { get, getField, isValid, getVehicles, permission, set, setBulk }; } -export function useCharacterBinder(player: alt.Player) { +export function useCharacterBinder(player: alt.Player, syncPlayer = true) { /** * Binds a player identifier to a Character document. * This document is cleared on disconnected automatically. * This should be the first thing you do after having a user authenticate. * + * Pass `syncPlayer` as false to prevent synchronization of appearance and clothes on binding. + * * @param {Character & T} document + * @param {boolean} syncPlayer */ function bind(document: Character & T): ReturnType | undefined { if (!player.valid) { @@ -305,6 +310,14 @@ export function useCharacterBinder(player: alt.Player) { player.setMeta(sessionKey, document); Rebar.events.useEvents().invoke('character-bound', player, document); + + if (syncPlayer) { + Rebar.player.usePlayerAppearance(player).sync(); + Rebar.player.useClothing(player).sync(); + Rebar.player.useWeapon(player).sync(); + Rebar.player.useState(player).sync(); + } + return useCharacter(player); } diff --git a/src/main/server/document/vehicle.ts b/src/main/server/document/vehicle.ts index 8fca9c63e..a3af2ea30 100644 --- a/src/main/server/document/vehicle.ts +++ b/src/main/server/document/vehicle.ts @@ -145,13 +145,18 @@ export function useVehicleBinder(vehicle: alt.Vehicle) { * * @param {Vehicle & T} document */ - function bind(document: Vehicle & T): ReturnType | undefined { + function bind(document: Vehicle & T, syncVehicle = true): ReturnType | undefined { if (!vehicle.valid) { return undefined; } vehicle.setMeta(sessionKey, document); Rebar.events.useEvents().invoke('vehicle-bound', vehicle, document); + + if (syncVehicle) { + Rebar.vehicle.useVehicle(vehicle).sync(); + } + return useVehicle(vehicle); } diff --git a/src/main/server/index.ts b/src/main/server/index.ts index 5a93197da..299246e02 100644 --- a/src/main/server/index.ts +++ b/src/main/server/index.ts @@ -22,7 +22,7 @@ import { useCharacter, useCharacterBinder, useGlobal, - useVehicle, + useVehicle as useVehicleDocument, useVehicleBinder, useVehicleEvents, useVirtual, @@ -45,6 +45,7 @@ import { useWebview } from './player/webview.js'; import { useClothing } from './player/clothing.js'; import { useAnimation } from './player/animation.js'; import { usePlayerAppearance } from './player/appearance.js'; +import { useWeapon } from './player/weapon.js'; import { useMessenger } from './systems/messenger.js'; import { usePermission } from './systems/permission.js'; @@ -52,6 +53,8 @@ import { usePermissionGroup } from './systems/permissionGroup.js'; import { check, hash } from './utility/password.js'; import { sha256, sha256Random } from './utility/hash.js'; +import { useVehicle } from './vehicle/index.js'; +import { useState } from './player/state.js'; export function useRebar() { return { @@ -88,7 +91,7 @@ export function useRebar() { useGlobal, }, vehicle: { - useVehicle, + useVehicle: useVehicleDocument, useVehicleBinder, useVehicleEvents, }, @@ -114,6 +117,8 @@ export function useRebar() { useClothing, useNative, useNotify, + useState, + useWeapon, useWebview, useWorld, }, @@ -132,5 +137,8 @@ export function useRebar() { hash, }, }, + vehicle: { + useVehicle, + }, }; } diff --git a/src/main/server/player/appearance.ts b/src/main/server/player/appearance.ts index 8b2e6fb9b..e64d940a6 100644 --- a/src/main/server/player/appearance.ts +++ b/src/main/server/player/appearance.ts @@ -1,12 +1,92 @@ import * as alt from 'alt-server'; import { useCharacter } from '@Server/document/character.js'; import { getHairOverlay } from '@Shared/data/hairOverlay.js'; +import { Appearance } from '../../shared/types/appearance.js'; export type Decorator = { overlay: string; collection: string }; export type HairStyle = { hair: number; dlc?: string | number; color1: number; color2: number; decorator: Decorator }; export type BaseStyle = { style: number; opacity: number; color: number }; export function usePlayerAppearance(player: alt.Player) { + /** + * Apply appearance data to the given player. + * + * @param {Partial} data + */ + function apply(data: Partial) { + if (typeof data.sex !== undefined) { + player.model = data.sex === 0 ? 'mp_f_freemode_01' : 'mp_m_freemode_01'; + } + + clear(); + + // Set Face + player.setHeadBlendData( + data.faceMother ?? 0, + data.faceFather ?? 0, + 0, + data.skinMother ?? 0, + data.skinFather ?? 0, + 0, + parseFloat(data.faceMix.toString()) ?? 0.5, + parseFloat(data.skinMix.toString()) ?? 0.5, + 0, + ); + + // Facial Features + if (Array.isArray(data.structure)) { + for (let i = 0; i < data.structure.length; i++) { + const value = data.structure[i]; + player.setFaceFeature(i, value); + } + } + + // Hair - Tattoo + updateTattoos(); + + // Hair - Supports DLC + if (typeof data.hairDlc === 'undefined' || data.hairDlc === 0) { + player.setClothes(2, data.hair ?? 0, 0, 0); + } else { + player.setDlcClothes(data.hairDlc, 2, data.hair ?? 0, 0, 0); + } + + player.setHairColor(data.hairColor1 ?? 0); + player.setHairHighlightColor(data.hairColor2 ?? 0); + + // Facial Hair + if (typeof data.facialHair !== 'undefined') { + player.setHeadOverlay(1, data.facialHair, data.facialHairOpacity); + player.setHeadOverlayColor(1, 1, data.facialHairColor1, data.facialHairColor1); + } + + // Chest Hair + if (data.chestHair !== null && data.chestHair !== undefined) { + player.setHeadOverlay(10, data.chestHair, data.chestHairOpacity); + player.setHeadOverlayColor(10, 1, data.chestHairColor1, data.chestHairColor1); + } + + // Eyebrows + if (typeof data.eyebrows !== 'undefined') { + player.setHeadOverlay(2, data.eyebrows, data.eyebrowsOpacity); + player.setHeadOverlayColor(2, 1, data.eyebrowsColor1, data.eyebrowsColor1); + } + + // Decor + if (Array.isArray(data.headOverlays)) { + for (let i = 0; i < data.headOverlays.length; i++) { + const overlay = data.headOverlays[i]; + const color2 = overlay.color2 ? overlay.color2 : overlay.color1; + + player.setHeadOverlay(overlay.id, overlay.value, parseFloat(overlay.opacity.toString())); + player.setHeadOverlayColor(overlay.id, 1, overlay.color1, color2); + } + } + + // Eyes + player.setEyeColor(data.eyes ?? 0); + } + /** * Set a player's hairstyle. * @@ -217,86 +297,14 @@ export function usePlayerAppearance(player: alt.Player) { * * @return */ - function update() { + function sync() { const document = useCharacter(player); const dataDocument = document.get(); if (!dataDocument || !dataDocument.appearance) { return; } - const data = dataDocument.appearance; - - if (typeof data.sex !== undefined) { - player.model = data.sex === 0 ? 'mp_f_freemode_01' : 'mp_m_freemode_01'; - } - - clear(); - - // Set Face - player.setHeadBlendData( - data.faceMother ?? 0, - data.faceFather ?? 0, - 0, - data.skinMother ?? 0, - data.skinFather ?? 0, - 0, - parseFloat(data.faceMix.toString()) ?? 0.5, - parseFloat(data.skinMix.toString()) ?? 0.5, - 0, - ); - - // Facial Features - if (Array.isArray(data.structure)) { - for (let i = 0; i < data.structure.length; i++) { - const value = data.structure[i]; - player.setFaceFeature(i, value); - } - } - - // Hair - Tattoo - updateTattoos(); - - // Hair - Supports DLC - if (typeof data.hairDlc === 'undefined' || data.hairDlc === 0) { - player.setClothes(2, data.hair ?? 0, 0, 0); - } else { - player.setDlcClothes(data.hairDlc, 2, data.hair ?? 0, 0, 0); - } - - player.setHairColor(data.hairColor1 ?? 0); - player.setHairHighlightColor(data.hairColor2 ?? 0); - - // Facial Hair - if (typeof data.facialHair !== 'undefined') { - player.setHeadOverlay(1, data.facialHair, data.facialHairOpacity); - player.setHeadOverlayColor(1, 1, data.facialHairColor1, data.facialHairColor1); - } - - // Chest Hair - if (data.chestHair !== null && data.chestHair !== undefined) { - player.setHeadOverlay(10, data.chestHair, data.chestHairOpacity); - player.setHeadOverlayColor(10, 1, data.chestHairColor1, data.chestHairColor1); - } - - // Eyebrows - if (typeof data.eyebrows !== 'undefined') { - player.setHeadOverlay(2, data.eyebrows, data.eyebrowsOpacity); - player.setHeadOverlayColor(2, 1, data.eyebrowsColor1, data.eyebrowsColor1); - } - - // Decor - if (Array.isArray(data.headOverlays)) { - for (let i = 0; i < data.headOverlays.length; i++) { - const overlay = data.headOverlays[i]; - const color2 = overlay.color2 ? overlay.color2 : overlay.color1; - - player.setHeadOverlay(overlay.id, overlay.value, parseFloat(overlay.opacity.toString())); - player.setHeadOverlayColor(overlay.id, 1, overlay.color1, color2); - } - } - - // Eyes - player.setEyeColor(data.eyes ?? 0); + apply(dataDocument.appearance); } /** @@ -328,7 +336,16 @@ export function usePlayerAppearance(player: alt.Player) { } } + /** + * @deprecated use sync + * + */ + function update() { + sync(); + } + return { + apply, clear, getHairOverlay, setEyeColor, @@ -338,6 +355,7 @@ export function usePlayerAppearance(player: alt.Player) { setHeadBlendData, setModel, setTattoos, + sync, update, updateTattoos, }; diff --git a/src/main/server/player/clothing.ts b/src/main/server/player/clothing.ts index 623b02499..d8ff4f44e 100644 --- a/src/main/server/player/clothing.ts +++ b/src/main/server/player/clothing.ts @@ -79,6 +79,70 @@ async function updateClothing(document: ReturnType, compone } export function useClothing(player: alt.Player) { + function apply(data: Character) { + const propComponents = [0, 1, 2, 6, 7]; + for (let i = 0; i < propComponents.length; i++) { + player.clearProp(propComponents[i]); + } + + let sex = 1; + if (data.appearance && typeof data.appearance.sex !== 'undefined') { + sex = data.appearance.sex; + } + + if (data.skin === null || typeof data.skin === 'undefined') { + const useModel = sex ? mModel : fModel; + if (player.model !== useModel) { + player.model = useModel; + } + } else { + const customModel = typeof data.skin !== 'number' ? alt.hash(data.skin) : data.skin; + if (player.model === customModel) { + return; + } + + player.model = customModel; + return; + } + + const dataSet = sex === 0 ? femaleClothes : maleClothes; + Object.keys(dataSet).forEach((key) => { + player.setDlcClothes(0, parseInt(key), parseInt(dataSet[key]), 0, 0); + }); + + // Apply Clothing + if (Array.isArray(data.clothing)) { + for (let i = 0; i < data.clothing.length; i++) { + const component = data.clothing[i]; + + // We look at the equipped item data sets; and find compatible clothing information in the 'data' field. + // Check if the data property is the correct format for the item. + if (component.isProp) { + player.setDlcProp(component.dlc, component.id, component.drawable, component.texture); + } else { + const palette = typeof component.palette === 'number' ? component.palette : 0; + player.setDlcClothes(component.dlc, component.id, component.drawable, component.texture, palette); + } + } + } + + // Apply Uniform if available + if (Array.isArray(data.uniform)) { + for (let i = 0; i < data.uniform.length; i++) { + const component = data.uniform[i]; + + // We look at the equipped item data sets; and find compatible clothing information in the 'data' field. + // Check if the data property is the correct format for the item. + if (component.isProp) { + player.setDlcProp(component.dlc, component.id, component.drawable, component.texture); + } else { + const palette = typeof component.palette === 'number' ? component.palette : 0; + player.setDlcClothes(component.dlc, component.id, component.drawable, component.texture, palette); + } + } + } + } + /** * This function sets a uniform for a player in game. * @@ -98,7 +162,7 @@ export function useClothing(player: alt.Player) { } await document.set('uniform', components); - update(); + sync(); return true; } @@ -115,7 +179,7 @@ export function useClothing(player: alt.Player) { } await document.set('uniform', undefined); - update(); + sync(); } /** @@ -134,7 +198,7 @@ export function useClothing(player: alt.Player) { } await document.set('clothing', components); - update(); + sync(); return true; } @@ -258,7 +322,7 @@ export function useClothing(player: alt.Player) { } await document.set('clothing', []); - update(); + sync(); } /** @@ -275,7 +339,7 @@ export function useClothing(player: alt.Player) { } await document.set('skin', typeof model === 'string' ? alt.hash(model) : model); - update(); + sync(); return true; } @@ -290,7 +354,7 @@ export function useClothing(player: alt.Player) { } await document.set('skin', undefined); - update(); + sync(); return true; } @@ -305,7 +369,7 @@ export function useClothing(player: alt.Player) { * @returns The function does not always return a value. It may return the result of the * `Overrides.update` function if it exists and is called, but otherwise it may not return anything. */ - function update(document: Character = undefined) { + function sync(document: Character = undefined) { if (!player || !player.valid) { return; } @@ -322,70 +386,19 @@ export function useClothing(player: alt.Player) { return; } - const propComponents = [0, 1, 2, 6, 7]; - for (let i = 0; i < propComponents.length; i++) { - player.clearProp(propComponents[i]); - } - - let sex = 1; - if (data.appearance && typeof data.appearance.sex !== 'undefined') { - sex = data.appearance.sex; - } - - if (data.skin === null || typeof data.skin === 'undefined') { - const useModel = sex ? mModel : fModel; - if (player.model !== useModel) { - player.model = useModel; - } - } else { - const customModel = typeof data.skin !== 'number' ? alt.hash(data.skin) : data.skin; - if (player.model === customModel) { - return; - } - - player.model = customModel; - return; - } - - const dataSet = sex === 0 ? femaleClothes : maleClothes; - Object.keys(dataSet).forEach((key) => { - player.setDlcClothes(0, parseInt(key), parseInt(dataSet[key]), 0, 0); - }); - - // Apply Clothing - if (Array.isArray(data.clothing)) { - for (let i = 0; i < data.clothing.length; i++) { - const component = data.clothing[i]; - - // We look at the equipped item data sets; and find compatible clothing information in the 'data' field. - // Check if the data property is the correct format for the item. - if (component.isProp) { - player.setDlcProp(component.dlc, component.id, component.drawable, component.texture); - } else { - const palette = typeof component.palette === 'number' ? component.palette : 0; - player.setDlcClothes(component.dlc, component.id, component.drawable, component.texture, palette); - } - } - } - - // Apply Uniform if available - if (Array.isArray(data.uniform)) { - for (let i = 0; i < data.uniform.length; i++) { - const component = data.uniform[i]; + apply(data); + } - // We look at the equipped item data sets; and find compatible clothing information in the 'data' field. - // Check if the data property is the correct format for the item. - if (component.isProp) { - player.setDlcProp(component.dlc, component.id, component.drawable, component.texture); - } else { - const palette = typeof component.palette === 'number' ? component.palette : 0; - player.setDlcClothes(component.dlc, component.id, component.drawable, component.texture, palette); - } - } - } + /** + * @deprecated use sync + * + */ + function update() { + sync(); } return { + apply, clearClothing, clearSkin, clearUniform, @@ -396,6 +409,7 @@ export function useClothing(player: alt.Player) { setClothingComponent, setPropComponent, setUniform, + sync, update, }; } diff --git a/src/main/server/player/state.ts b/src/main/server/player/state.ts new file mode 100644 index 000000000..41f9cf4c7 --- /dev/null +++ b/src/main/server/player/state.ts @@ -0,0 +1,77 @@ +import * as alt from 'alt-server'; +import { BaseCharacter, Character } from '../../shared/types/character.js'; +import { useRebar } from '../index.js'; + +const Rebar = useRebar(); + +export function useState(player: alt.Player) { + /** + * Apply state changes based on partial character document + * + * @param {Partial} document + */ + function apply(document: Partial) { + if (document.pos) { + player.pos = new alt.Vector3(document.pos.x, document.pos.y, document.pos.z); + } + + if (document.rot) { + player.rot = new alt.Vector3(document.rot.x, document.rot.y, document.rot.z); + } + + if (document.health) { + player.health = document.health ?? 200; + } + + if (document.armour) { + player.armour = document.armour ?? 0; + } + + if (document.dimension) { + player.dimension = document.dimension ?? 0; + } + + if (document.isDead) { + player.health = 99; + } + } + + /** + * Save current player position, rot, health, armor, etc. + */ + function save() { + const document = Rebar.document.character.useCharacter(player); + if (!document.get()) { + return; + } + + document.setBulk({ + pos: player.pos, + rot: player.rot, + health: player.health, + armour: player.armour, + isDead: player.isDead, + }); + } + + /** + * Synchronize state based on document + * + * @return + */ + function sync() { + const document = Rebar.document.character.useCharacter(player); + const data = document.get(); + if (!data || !data.weapons) { + return; + } + + apply(data); + } + + return { + apply, + save, + sync, + }; +} diff --git a/src/main/server/player/weapon.ts b/src/main/server/player/weapon.ts new file mode 100644 index 000000000..cd560dc77 --- /dev/null +++ b/src/main/server/player/weapon.ts @@ -0,0 +1,190 @@ +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 {{ [hash: string]: number }} ammoData + */ + function apply(weapons: alt.IWeapon[], ammoData: { [hash: string]: 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.setWeaponTintIndex(weapon.hash, weapon.tintIndex); + for (let component of weapon.components) { + player.addWeaponComponent(weapon.hash, component); + } + } + } + + /** + * Clear all weapons and ammo + * + * @return + */ + async function clear() { + player.removeAllWeapons(); + + const document = Rebar.document.character.useCharacter(player); + if (!document.get()) { + return; + } + + await document.setBulk({ + weapons: [], + ammo: {}, + }); + + sync(); + } + + /** + * Clear and remove all ammo for a specific weapon + * + * @param {string} model + * @return + */ + async function clearWeapon(model: string) { + const weaponHash = alt.hash(model); + const document = Rebar.document.character.useCharacter(player); + player.removeWeapon(weaponHash); + if (!document.get()) { + return; + } + + const weapons = document.getField('weapons') ?? []; + const ammo = document.getField('ammo') ?? {}; + + const index = weapons.findIndex((x) => x.hash === weaponHash); + if (index >= 0) { + weapons.splice(index, 1); + } + + if (ammo[weaponHash]) { + delete ammo[weaponHash]; + } + + await document.setBulk({ + weapons, + ammo, + }); + + sync(); + } + + /** + * Add a weapon to the player + * + * @param {string} model + * @param {number} ammo + * @return + */ + async function add(model: string, ammoCount: number) { + const document = Rebar.document.character.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, + }); + + sync(); + } + + /** + * Add ammo for the current given weapon, and save to the database + * + * @param {string} model + * @param {number} ammoCount + * @return + */ + async function addAmmo(model: string, ammoCount: number) { + const document = Rebar.document.character.useCharacter(player); + if (!document.get()) { + return; + } + + const ammo = document.getField('ammo') ?? {}; + if (ammo[alt.hash(model)]) { + ammo[alt.hash(model)] += ammoCount; + } else { + ammo[alt.hash(model)] = ammoCount; + } + + player.setWeaponAmmo(model, ammo[alt.hash(model)]); + document.set('ammo', ammo); + } + + /** + * Save current player weapons + */ + function save() { + const document = Rebar.document.character.useCharacter(player); + if (!document.get()) { + return; + } + + const ammo: { [key: string]: number } = {}; + for (let weapon of player.weapons) { + ammo[weapon.hash] = player.getAmmo(weapon.hash); + } + + document.setBulk({ + weapons: player.weapons, + ammo: ammo, + }); + } + + function saveAmmo() { + const document = Rebar.document.character.useCharacter(player); + if (!document.get()) { + return; + } + + const ammo: { [key: string]: number } = {}; + for (let weapon of player.weapons) { + ammo[weapon.hash] = player.getAmmo(weapon.hash); + } + + document.set('ammo', ammo); + } + + function sync() { + const document = useCharacter(player); + const data = document.get(); + if (!data || !data.weapons) { + return; + } + + apply(data.weapons, data.ammo); + } + + return { + add, + addAmmo, + apply, + clear, + clearWeapon, + save, + saveAmmo, + sync, + }; +} diff --git a/src/main/server/systems/messenger.ts b/src/main/server/systems/messenger.ts index 801bae2df..8021307d2 100644 --- a/src/main/server/systems/messenger.ts +++ b/src/main/server/systems/messenger.ts @@ -14,7 +14,7 @@ export type Command = { name: string; desc: string; options?: CommandOptions; - callback: (player: alt.Player, ...args: any[]) => void | boolean; + callback: (player: alt.Player, ...args: any[]) => void | boolean | Promise | Promise; }; const tagOrComment = new RegExp( @@ -76,7 +76,7 @@ export function useMessenger() { return false; } - function invokeCommand(player: alt.Player, cmdName: string, ...args: any[]): boolean { + async function invokeCommand(player: alt.Player, cmdName: string, ...args: any[]): Promise { cmdName = cmdName.replace('/', ''); cmdName = cmdName.toLowerCase(); @@ -91,7 +91,7 @@ export function useMessenger() { } try { - command.callback(player, ...args); + await command.callback(player, ...args); return true; } catch (err) { return false; diff --git a/src/main/server/vehicle/index.ts b/src/main/server/vehicle/index.ts new file mode 100644 index 000000000..19283f854 --- /dev/null +++ b/src/main/server/vehicle/index.ts @@ -0,0 +1,218 @@ +import * as alt from 'alt-server'; +import { useRebar } from '../index.js'; +import { Vehicle, WheelState } from '../../shared/types/vehicle.js'; + +const Rebar = useRebar(); +const db = Rebar.database.useDatabase(); + +export function useVehicle(vehicle: alt.Vehicle) { + /** + * Apply a document to the vehicle + * + * @param {Partial} document + */ + function apply(document: Partial) { + if (document.stateProps) { + for (let prop of Object.keys(document.stateProps)) { + vehicle[prop] = document.stateProps[prop]; + } + } + + // Synchronize wheel state + if (document.wheelState) { + for (let i = 0; i < document.wheelState.length; i++) { + const state = document.wheelState[i]; + if (state === 'burst') { + vehicle.setWheelBurst(i, true); + continue; + } + + if (state === 'detached') { + vehicle.setWheelDetached(i, true); + continue; + } + } + } + + // Synchronize mods + if (document.mods) { + for (let key of Object.keys(document.mods)) { + const id = parseInt(key); + try { + vehicle.setMod(id, document.mods[key]); + } catch (err) {} + } + } + + // Synchronize vehicle extras + if (document.extras) { + for (let key of Object.keys(document.extras)) { + const id = parseInt(key); + try { + vehicle.setExtra(id, document.extras[key]); + } catch (err) {} + } + } + + // Synchronize windows + if (document.windows) { + const isArmored = vehicle.hasArmoredWindows; + + for (let key of Object.keys(document.windows)) { + const id = parseInt(key); + if (!isArmored) { + vehicle.setWindowDamaged(id, document.windows[key] ? true : false); + continue; + } + + vehicle.setArmoredWindowHealth(id, document.windows[key]); + } + } + + // Synchronize dimension + vehicle.dimension = document.dimension ?? 0; + } + + /** + * Create a document based on the assigned vehicle, and return document. + * + * If the vehicle is already bound, `undefined` will be returned. + * + * @param {string} owner + * @param {string} model + * @param {alt.Vector3} pos + * @param {alt.Vector3} rot + * @return + */ + async function create(ownerIdOrIdentifier: string) { + if (!vehicle.valid) { + return undefined; + } + + const document = Rebar.document.vehicle.useVehicle(vehicle); + if (document.get()) { + return undefined; + } + + const id = await db.create>( + { + model: vehicle.model, + dimension: vehicle.dimension, + fuel: 0, + owner: ownerIdOrIdentifier, + pos: vehicle.pos, + rot: vehicle.rot, + }, + Rebar.database.CollectionNames.Vehicles, + ); + + const vehicleDocument = await db.get({ _id: id }, Rebar.database.CollectionNames.Vehicles); + Rebar.document.vehicle.useVehicleBinder(vehicle).bind(vehicleDocument); + return vehicleDocument; + } + + /** + * Despawns current vehicle, grabs existing document on the vehicle. + * Recreates the vehicle, saves health, and then re-applies the document. + * + * @return + */ + async function repair(): Promise { + const document = Rebar.document.vehicle.useVehicle(vehicle); + const model = vehicle.model; + const pos = vehicle.pos; + const rot = vehicle.rot; + + try { + vehicle.destroy(); + } catch (err) {} + + vehicle = new alt.Vehicle(model, pos, rot); + + if (!document.get()) { + return vehicle; + } + + Rebar.document.vehicle.useVehicleBinder(vehicle).bind(document.get(), false); + await save(); + sync(); + + return vehicle; + } + + /** + * Only saves vehicles that already have a pre-existing document + * + * @return + */ + function save() { + const document = Rebar.document.vehicle.useVehicle(vehicle); + if (!document.get()) { + return; + } + + const windows: { [key: string]: number } = {}; + const isArmored = vehicle.hasArmoredWindows; + for (let i = 0; i < 6; i++) { + if (!isArmored) { + windows[i] = vehicle.isWindowDamaged(i) ? 1 : 0; + continue; + } + + windows[i] = vehicle.getArmoredWindowHealth(i); + } + + const data: Partial = { + pos: vehicle.pos, + rot: vehicle.rot, + windows, + stateProps: { + bodyHealth: vehicle.bodyHealth, + dirtLevel: vehicle.dirtLevel, + engineHealth: vehicle.engineHealth, + engineOn: vehicle.engineOn, + lightState: vehicle.lightState, + lockState: vehicle.lockState, + modKit: vehicle.modKit, + }, + }; + + // Save wheel state + if (vehicle.wheelsCount >= 1) { + const wheelState: WheelState[] = []; + for (let i = 0; i < vehicle.wheelsCount; i++) { + if (vehicle.isWheelBurst(i)) { + wheelState.push('burst'); + continue; + } + + if (vehicle.isWheelDetached(i)) { + wheelState.push('detached'); + continue; + } + + wheelState.push('attached'); + } + + data.wheelState = wheelState; + } + + document.setBulk(data); + } + + /** + * If the vehicle has a `document` bound to it, it will apply the appearance. + */ + function sync() { + const document = Rebar.document.vehicle.useVehicle(vehicle); + if (!document.get()) { + return; + } + + // Synchronize health, engine health, dirt level, modKit etc. + const data = document.get(); + apply(data); + } + + return { apply, create, repair, save, sync }; +} diff --git a/src/main/shared/events/index.ts b/src/main/shared/events/index.ts index 1ba5f4b04..6e4797fd0 100644 --- a/src/main/shared/events/index.ts +++ b/src/main/shared/events/index.ts @@ -91,6 +91,7 @@ export const Events = { view: { onServer: 'webview:on:server', onEmit: 'webview:emit:on', + onKeypress: 'webview:emit:keypress', emitServer: 'webview:emit:server', emitClient: 'webview:emit:client', emitReady: 'webview:emit:page:ready', @@ -100,5 +101,6 @@ export const Events = { hideAllByType: 'webview:emit:page:hide:all:type', focus: 'webview:emit:focus', unfocus: 'webview:emit:unfocus', + playFrontendSound: 'webview:play:frontend:sound', }, }; diff --git a/src/main/shared/types/character.ts b/src/main/shared/types/character.ts index 12e7d6c1e..35f495a8b 100644 --- a/src/main/shared/types/character.ts +++ b/src/main/shared/types/character.ts @@ -2,6 +2,54 @@ import * as alt from 'alt-shared'; import { Appearance } from './appearance.js'; import { ClothingComponent } from './clothingComponent.js'; +export type BaseCharacter = { + /** + * The current dimension of the player. When they spawn + * they are automatically moved into this dimension. + * + * @type {number} + * + */ + dimension?: number; + + /** + * The position that this character last logged out at. + * This also updates every 5s or so. + * @type {alt.IVector3} + * + */ + pos?: alt.IVector3; + + /** + * The rotation that this character last logged out at. + * + * @type {alt.IVector3} + */ + rot?: alt.IVector3; + + /** + * The amount of health the player last had. + * @type {number} 99 - 199 + * + */ + health?: number; + + /** + * The amount of armour the player last had. + * @type {number} 0 - 100 + * + */ + armour?: number; + + /** + * Is this player dead or not. + * Health does not dictate whether a player is alive or dead. + * @type {boolean} + * + */ + isDead?: boolean; +}; + /** * Used as the main interface for storing character data. * @@ -23,23 +71,6 @@ export type Character = { */ account_id: string; - /** - * The current dimension of the player. When they spawn - * they are automatically moved into this dimension. - * - * @type {number} - * - */ - dimension?: number; - - /** - * The position that this character last logged out at. - * This also updates every 5s or so. - * @type {Partial} - * - */ - pos?: Partial; - /** * The name of this character to display to other users. * @type {string} @@ -61,20 +92,6 @@ export type Character = { */ bank?: number; - /** - * The amount of health the player last had. - * @type {number} 99 - 199 - * - */ - health?: number; - - /** - * The amount of armour the player last had. - * @type {number} 0 - 100 - * - */ - armour?: number; - /** * The amount of food the player has. * @type {number} 0 - 100 @@ -89,14 +106,6 @@ export type Character = { */ water?: number; - /** - * Is this player dead or not. - * Health does not dictate whether a player is alive or dead. - * @type {boolean} - * - */ - isDead?: boolean; - /** * Amount of hours the player has played. * @type {number} @@ -162,4 +171,18 @@ export type Character = { * @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 }} + */ + ammo?: { [weaponHash: string]: number }; +} & BaseCharacter; diff --git a/src/main/shared/types/vehicle.ts b/src/main/shared/types/vehicle.ts index 9266524d8..35dd1e825 100644 --- a/src/main/shared/types/vehicle.ts +++ b/src/main/shared/types/vehicle.ts @@ -1,5 +1,7 @@ import * as alt from 'alt-shared'; +export type WheelState = 'attached' | 'detached' | 'burst'; + /** * Used as the main describer of a stored vehicle. * @@ -27,10 +29,10 @@ export interface Vehicle { /** * The model of this vehicle. - * @type {string} + * @type {string | number} * */ - model: string; + model: string | number; /** * The last position where this vehicle was last left. @@ -68,4 +70,94 @@ export interface Vehicle { * */ fuel: number; + + /** + * Store current wheel state of the vehicle + * + * @type { WheelState[]} + * @memberof Vehicle + */ + wheelState?: WheelState[]; + + /** + * A list of mods based on value of number `numerically` and what value as a `number`. + * + * @type {{ [key: string]: number }} + */ + mods?: { [key: string]: number }; + + /** + * A list of vehicle extras to apply + * + * @type {{ [key: string]: boolean }} + * @memberof Vehicle + */ + extras?: { [key: string]: boolean }; + + /** + * Window state for armored and regular windows + * + * @type {{ [key: string]: number }} + * @memberof Vehicle + */ + windows?: { [key: string]: number }; + + stateProps?: { + /** + * Dirt level of the vehicle + * + * @type {number} + * @memberof Vehicle + */ + dirtLevel?: number; + + /** + * Lock state of the vehicle + * + * @type {number} + */ + lockState?: number; + + /** + * Engine health of the vehicle + * + * @type {number} + */ + engineHealth?: number; + + /** + * Is the vehicle engine turned on + * + * @type {boolean} + */ + engineOn?: boolean; + + /** + * The body health of the vehicle + * + * @type {number} + */ + bodyHealth?: number; + + /** + * The light state of the vehicle, whether they're on or off + * + * @type {number} + */ + lightState?: number; + + /** + * Usually set to `1` when mods are available for the given vehicle + * + * @type {number} + */ + modKit?: number; + + /** + * Are the daylights for the vehicle turned on + * + * @type {boolean} + */ + daylightOn?: boolean; + }; } diff --git a/webview/composables/useAudio.ts b/webview/composables/useAudio.ts index 2fe7818f1..18bfcab54 100644 --- a/webview/composables/useAudio.ts +++ b/webview/composables/useAudio.ts @@ -1,5 +1,5 @@ import { Events } from '../../src/main/shared/events'; -import { useEvents } from './useEvents'; +import { useEvents } from './useEvents.js'; const events = useEvents(); @@ -29,7 +29,23 @@ export function useAudio() { // this._ambientPan[soundID].pan.value = pan; } + /** + * Play a native frontend sound from the Webview + * + * @param {string} audioName + * @param {string} audioRef + * @return + */ + async function playFrontend(audioName: string, audioRef: string, audioBank = '') { + if (!('alt' in window)) { + return; + } + + alt.emit(Events.view.playFrontendSound, audioName, audioRef, audioBank); + } + return { play, + playFrontend, }; } diff --git a/webview/composables/useEvents.ts b/webview/composables/useEvents.ts index 29d80b919..358964e9c 100644 --- a/webview/composables/useEvents.ts +++ b/webview/composables/useEvents.ts @@ -1,6 +1,7 @@ import { Events } from '../../src/main/shared/events/index.js'; const OnEvents: { [key: string]: (...args: any[]) => void } = {}; +const OnKeybind: { [identifier: string]: { key: number; callback: (...args: any[]) => void } } = {}; let isInitialized = false; @@ -17,10 +18,21 @@ function handleEmits(event: string, ...args: any[]) { OnEvents[event](...args); } +function handleKeypress(keycode: number) { + for (let value of Object.values(OnKeybind)) { + if (value.key !== keycode) { + continue; + } + + value.callback(keycode); + } +} + export function useEvents() { if (!isInitialized && 'alt' in window) { isInitialized = true; alt.on(Events.view.onEmit, handleEmits); + alt.on(Events.view.onKeypress, handleKeypress); } /** @@ -73,9 +85,26 @@ export function useEvents() { OnEvents[eventName] = callback; } + /** + * Register a callback that sends when a key bind is pressed. + * + * Used for advanced interface functionality. Great for overlays. + * + * @param {string} identifier + * @param {number} key + * @param {() => void} callback + */ + function onKeyUp(identifier: string, key: number, callback: () => void) { + OnKeybind[identifier] = { + key, + callback, + }; + } + return { emitClient, emitServer, on, + onKeyUp, }; }