diff --git a/docs/api/client/system/useStreamSyncedGetter.md b/docs/api/client/system/useStreamSyncedGetter.md
new file mode 100644
index 000000000..09334914e
--- /dev/null
+++ b/docs/api/client/system/useStreamSyncedGetter.md
@@ -0,0 +1,17 @@
+# useStreamSyncedGetter
+
+When using the [Stream Synced Binder](../../server/systems/useStreamSyncedBinder.md), this allows you to get type safe responses on client-side.
+
+```ts
+import * as alt from 'alt-client';
+import { useRebarClient } from '../../../main/client/index.js';
+
+const RebarClient = useRebarClient();
+
+const streamGetter = RebarClient.systems.useStreamSyncedGetter();
+
+const money = streamGetter.player(alt.Player.local).get('money');
+const name = streamGetter.player(alt.Player.local).get('name');
+
+const fuel = streamGetter.vehicle(someVehicle).get('fuel');
+```
diff --git a/docs/api/server/systems/useStreamSyncedBinder.md b/docs/api/server/systems/useStreamSyncedBinder.md
new file mode 100644
index 000000000..aa0f6e0e0
--- /dev/null
+++ b/docs/api/server/systems/useStreamSyncedBinder.md
@@ -0,0 +1,21 @@
+# useStreamSyncedBinder
+
+What this allows you to do is target a specific document field from a `Character` or `Vehicle` document and automatically synchronize the document data to client-side through `streamSyncedMeta`.
+
+Whenever the value changes, it automatically updates for a player or vehicle.
+
+For instance, if specify the propery `money` and `money` is updated for the player, it will be available on client-side, and in the webview.
+
+```ts
+import { useRebar } from '../../../main/server/index.js';
+
+const Rebar = useRebar();
+const SyncedBinder = Rebar.systems.useStreamSyncedBinder();
+
+// Bind character document data
+SyncedBinder.syncCharacterKey('money');
+SyncedBinder.syncCharacterKey('name');
+
+// Bind vehicle document data
+SyncedBinder.syncVehicleKey('fuel');
+```
diff --git a/docs/changelog.md b/docs/changelog.md
index 8c934b583..fa2bc1794 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,5 +1,19 @@
# Changelog
+## Version 41
+
+### Code Changes
+
+- Added `useStreamSyncedBinder` to automatically synchronize document data from server to client for vehicles and characters
+- Added `useSyncedMeta` composable to the webview, to get data synced from `useStreamSyncedBinder`
+- Added `useStreamSyncedGetter` to client-side to get type safe responses for stream synced meta data
+
+### Docs Changes
+
+- Documented `useStreamSyncedBinder`, `useSyncedMeta`, and `useStreamSyncedGetter`
+
+---
+
## Version 40
### Code Changes
diff --git a/docs/webview/composables/use-synced-meta.md b/docs/webview/composables/use-synced-meta.md
new file mode 100644
index 000000000..ea1bdfbc7
--- /dev/null
+++ b/docs/webview/composables/use-synced-meta.md
@@ -0,0 +1,23 @@
+# useSyncedMeta
+
+When using the [Stream Synced Binder](../../api/server/systems/useStreamSyncedBinder.md), this webview component allows you to get that data as vue refs.
+
+```jsx
+
+
+
+
+
Character
+ {{ character }}
+
Vehicle
+ {{ vehicle }}
+
+
+```
diff --git a/package.json b/package.json
index 669edf084..596f944a9 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"author": "stuyk",
"type": "module",
- "version": "40",
+ "version": "41",
"scripts": {
"dev": "nodemon -x pnpm start",
"dev:linux": "nodemon -x pnpm start:linux",
@@ -40,7 +40,7 @@
"mongodb": "^6.7.0",
"sjcl": "^1.0.8",
"typescript": "^5.5.2",
- "vite": "^5.3.1",
+ "vite": "^5.3.2",
"vue": "^3.4.30",
"vue-tsc": "^2.0.22"
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 01ead7cd3..472ca856f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -10,7 +10,7 @@ importers:
dependencies:
'@vitejs/plugin-vue':
specifier: ^5.0.5
- version: 5.0.5(vite@5.3.1(@types/node@20.14.9))(vue@3.4.30(typescript@5.5.2))
+ version: 5.0.5(vite@5.3.2(@types/node@20.14.9))(vue@3.4.30(typescript@5.5.2))
dotenv:
specifier: ^16.4.5
version: 16.4.5
@@ -24,8 +24,8 @@ importers:
specifier: ^5.5.2
version: 5.5.2
vite:
- specifier: ^5.3.1
- version: 5.3.1(@types/node@20.14.9)
+ specifier: ^5.3.2
+ version: 5.3.2(@types/node@20.14.9)
vue:
specifier: ^3.4.30
version: 3.4.30(typescript@5.5.2)
@@ -539,8 +539,8 @@ packages:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'}
- caniuse-lite@1.0.30001637:
- resolution: {integrity: sha512-1x0qRI1mD1o9e+7mBI7XtzFAP4XszbHaVWsMiGbSPLYekKTJF7K+FNk6AsXH4sUpc+qrsI3pVgf1Jdl/uGkuSQ==}
+ caniuse-lite@1.0.30001638:
+ resolution: {integrity: sha512-5SuJUJ7cZnhPpeLHaH0c/HPAnAHZvS6ElWyHK9GSIbVOQABLzowiI2pjmpvZ1WEbkyz46iFd4UXlOHR5SqgfMQ==}
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
@@ -1265,8 +1265,8 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
- vite@5.3.1:
- resolution: {integrity: sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==}
+ vite@5.3.2:
+ resolution: {integrity: sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
@@ -1573,9 +1573,9 @@ snapshots:
dependencies:
'@types/webidl-conversions': 7.0.3
- '@vitejs/plugin-vue@5.0.5(vite@5.3.1(@types/node@20.14.9))(vue@3.4.30(typescript@5.5.2))':
+ '@vitejs/plugin-vue@5.0.5(vite@5.3.2(@types/node@20.14.9))(vue@3.4.30(typescript@5.5.2))':
dependencies:
- vite: 5.3.1(@types/node@20.14.9)
+ vite: 5.3.2(@types/node@20.14.9)
vue: 3.4.30(typescript@5.5.2)
'@volar/language-core@2.3.4':
@@ -1693,7 +1693,7 @@ snapshots:
autoprefixer@10.4.19(postcss@8.4.38):
dependencies:
browserslist: 4.23.1
- caniuse-lite: 1.0.30001637
+ caniuse-lite: 1.0.30001638
fraction.js: 4.3.7
normalize-range: 0.1.2
picocolors: 1.0.1
@@ -1724,7 +1724,7 @@ snapshots:
browserslist@4.23.1:
dependencies:
- caniuse-lite: 1.0.30001637
+ caniuse-lite: 1.0.30001638
electron-to-chromium: 1.4.812
node-releases: 2.0.14
update-browserslist-db: 1.0.16(browserslist@4.23.1)
@@ -1733,7 +1733,7 @@ snapshots:
camelcase-css@2.0.1: {}
- caniuse-lite@1.0.30001637: {}
+ caniuse-lite@1.0.30001638: {}
chalk@4.1.2:
dependencies:
@@ -2384,7 +2384,7 @@ snapshots:
util-deprecate@1.0.2: {}
- vite@5.3.1(@types/node@20.14.9):
+ vite@5.3.2(@types/node@20.14.9):
dependencies:
esbuild: 0.21.5
postcss: 8.4.38
diff --git a/src/main/client/index.ts b/src/main/client/index.ts
index dbbda2b10..8032dafa2 100644
--- a/src/main/client/index.ts
+++ b/src/main/client/index.ts
@@ -23,6 +23,7 @@ import { useRaycast } from './system/raycasts.js';
import { isWorldMenuOpen, useWorldMenu } from './menus/world/index.js';
import { drawText2D, drawText3D } from './screen/textlabel.js';
import { draw, drawSimple } from './screen/marker.js';
+import { useStreamSyncedGetter } from './system/streamSyncedGetter.js';
export function useRebarClient() {
return {
@@ -62,7 +63,10 @@ export function useRebarClient() {
drawSimple,
},
},
- useProxyFetch,
+ systems: {
+ useStreamSyncedGetter,
+ useProxyFetch,
+ },
utility: {
math,
text,
diff --git a/src/main/client/system/streamSyncedGetter.ts b/src/main/client/system/streamSyncedGetter.ts
new file mode 100644
index 000000000..60fe437b2
--- /dev/null
+++ b/src/main/client/system/streamSyncedGetter.ts
@@ -0,0 +1,52 @@
+import * as alt from 'alt-client';
+import { Vehicle } from '../../shared/types/vehicle.js';
+import { Character } from '@Shared/types/character.js';
+
+export function useStreamSyncedGetter() {
+ /**
+ * Get synced data from `streamedSyncedBinder` if it is available for a given vehicle
+ *
+ * @param {alt.Vehicle} vehicle
+ * @return
+ */
+ function vehicle(vehicle: alt.Vehicle) {
+ function has(key: K): boolean {
+ return vehicle.hasStreamSyncedMeta(key);
+ }
+
+ function get(key: K): Vehicle[K] {
+ return vehicle.getStreamSyncedMeta(key) as Vehicle[K];
+ }
+
+ return {
+ has,
+ get,
+ };
+ }
+
+ /**
+ * Get synced data from `streamedSyncedBinder` if it is available for a given player
+ *
+ * @param {alt.Player} player
+ * @return
+ */
+ function player(player: alt.Player) {
+ function has(key: K): boolean {
+ return player.hasStreamSyncedMeta(key);
+ }
+
+ function get(key: K): Character[K] {
+ return player.getStreamSyncedMeta(key) as Character[K];
+ }
+
+ return {
+ has,
+ get,
+ };
+ }
+
+ return {
+ player,
+ vehicle,
+ };
+}
diff --git a/src/main/client/webview/index.ts b/src/main/client/webview/index.ts
index e643e594c..db6f5bbf0 100644
--- a/src/main/client/webview/index.ts
+++ b/src/main/client/webview/index.ts
@@ -324,6 +324,42 @@ export function useWebview(path = 'http://assets/webview/index.html') {
onCloseEvents[pageName].push(callback);
}
+ function onSyncedMetaChange(object: alt.Object, key: string, newValue: any) {
+ if (native.isEntityAPed(object.scriptID)) {
+ webview.emit(Events.view.syncPartialCharacter, key, newValue);
+ return;
+ }
+
+ if (!alt.Player.local.vehicle) {
+ return;
+ }
+
+ if (!native.isEntityAVehicle(object.scriptID)) {
+ return;
+ }
+
+ if (alt.Player.local.vehicle.scriptID !== object.scriptID) {
+ return;
+ }
+
+ webview.emit(Events.view.syncPartialVehicle, key, newValue);
+ }
+
+ function onVehicleEnter(vehicle: alt.Vehicle) {
+ const keys = vehicle.getStreamSyncedMetaKeys();
+ const data = {};
+
+ for (let key of keys) {
+ data[key] = vehicle.getStreamSyncedMeta(key);
+ }
+
+ webview.emit(Events.view.syncVehicle, data);
+ }
+
+ function onVehicleLeave() {
+ webview.emit(Events.view.syncVehicle, {});
+ }
+
if (!isInitialized) {
alt.onServer(Events.view.focus, focus);
alt.onServer(Events.view.unfocus, unfocus);
@@ -337,6 +373,9 @@ export function useWebview(path = 'http://assets/webview/index.html') {
webview.on(Events.view.emitServer, handleServerEvent);
webview.on(Events.view.emitServerRpc, handleServerRpcEvent);
webview.on(Events.view.emitClientRpc, handleClientRpcEvent);
+ alt.on('streamSyncedMetaChange', onSyncedMetaChange);
+ alt.on('enteredVehicle', onVehicleEnter);
+ alt.on('leftVehicle', onVehicleLeave);
}
return {
diff --git a/src/main/server/index.ts b/src/main/server/index.ts
index ded0a3838..3e34a1545 100644
--- a/src/main/server/index.ts
+++ b/src/main/server/index.ts
@@ -75,6 +75,7 @@ import * as ClothingUtility from '@Shared/data/clothing.js';
import { useScreenshot } from './systems/screenshot.js';
import { useKeypress } from './systems/serverKeypress.js';
import { useD2DTextLabel, useD2DTextLabelLocal } from './controllers/d2dTextLabel.js';
+import { useStreamSyncedBinder } from './systems/streamSyncedBinder.js';
export function useRebar() {
return {
@@ -165,6 +166,15 @@ export function useRebar() {
useServerConfig,
useServerTime,
useServerWeather,
+ systems: {
+ useStreamSyncedBinder,
+ useKeybinder,
+ useKeypress,
+ useProxyFetch,
+ useServerConfig,
+ useServerTime,
+ useServerWeather,
+ },
utility: {
clothing: { ...ClothingUtility },
sha256,
@@ -183,6 +193,8 @@ export function useRebar() {
};
}
+useRebar().useKeybinder;
+
declare module 'alt-server' {
// extending interface by interface merging
export interface ICustomGlobalMeta {
diff --git a/src/main/server/systems/streamSyncedBinder.ts b/src/main/server/systems/streamSyncedBinder.ts
new file mode 100644
index 000000000..2718c13bb
--- /dev/null
+++ b/src/main/server/systems/streamSyncedBinder.ts
@@ -0,0 +1,75 @@
+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: [],
+};
+
+function handleKeySet(entity: alt.Entity, key: string, newValue: any) {
+ entity.setStreamSyncedMeta(key, newValue);
+}
+
+export function useStreamSyncedBinder() {
+ /**
+ * Automatically synchronize a character document property to the attached player
+ *
+ * @template K
+ * @param {K} key
+ * @return
+ */
+ function syncCharacterKey(key: K) {
+ const index = keys.Character.findIndex((x) => x === key);
+ if (index >= 0) {
+ return;
+ }
+
+ keys.Character.push(key);
+ Rebar.document.character
+ .useCharacterEvents()
+ .on(key, (entity, newValue) => handleKeySet(entity, key, newValue));
+ }
+
+ /**
+ * Automatically synchronize a vehicle document property to the attached vehicle
+ *
+ * @template K
+ * @param {K} key
+ * @return
+ */
+ function syncVehicleKey(key: K) {
+ const index = keys.Vehicle.findIndex((x) => x === key);
+ if (index >= 0) {
+ return;
+ }
+
+ keys.Vehicle.push(key);
+ Rebar.document.vehicle.useVehicleEvents().on(key, (entity, newValue) => handleKeySet(entity, key, newValue));
+ }
+
+ return {
+ syncCharacterKey,
+ syncVehicleKey,
+ };
+}
+
+RebarEvents.on('character-bound', (player, document) => {
+ for (let key of keys.Character) {
+ handleKeySet(player, key, document[key]);
+ }
+});
+
+RebarEvents.on('vehicle-bound', (vehicle, document) => {
+ for (let key of keys.Vehicle) {
+ handleKeySet(vehicle, key, document[key]);
+ }
+});
diff --git a/src/main/shared/events/index.ts b/src/main/shared/events/index.ts
index 737c33bac..c8bb91ad1 100644
--- a/src/main/shared/events/index.ts
+++ b/src/main/shared/events/index.ts
@@ -163,5 +163,9 @@ export const Events = {
localStorageDelete: 'webview:localstorage:delete',
onPageClose: 'webview:page:close',
onPageOpen: 'webview:page:open',
+ syncCharacter: 'webview:sync:character',
+ syncPartialCharacter: 'webview:sync:partial:character',
+ syncVehicle: 'webview:sync:vehicle',
+ syncPartialVehicle: 'webview:sync:partial:vehicle',
},
};
diff --git a/webview/composables/useSyncedMeta.ts b/webview/composables/useSyncedMeta.ts
new file mode 100644
index 000000000..19918dbbe
--- /dev/null
+++ b/webview/composables/useSyncedMeta.ts
@@ -0,0 +1,51 @@
+import { Events } from '../../src/main/shared/events';
+import { Ref, ref } from 'vue';
+import { Vehicle } from '../../src/main/shared/types/vehicle.js';
+import { Character } from '../../src/main/shared/types/character.js';
+
+let isInit = false;
+
+const character = ref>({});
+const vehicle = ref>({});
+
+function syncCharacterData(data: Object) {
+ for (let key of Object.keys(data)) {
+ character.value[key] = data[key];
+ }
+}
+
+function syncVehicleData(data: Object) {
+ for (let key of Object.keys(data)) {
+ vehicle.value[key] = data[key];
+ }
+}
+
+export function useSyncedMeta() {
+ function init() {
+ if (isInit) {
+ return;
+ }
+
+ isInit = true;
+ if (!('alt' in window)) {
+ return;
+ }
+
+ alt.on(Events.view.syncCharacter, syncCharacterData);
+ alt.on(Events.view.syncVehicle, syncVehicleData);
+ alt.on(Events.view.syncPartialCharacter, (key: string, data: any) => {
+ character.value[key] = data;
+ });
+ alt.on(Events.view.syncPartialVehicle, (key: string, data: any) => (vehicle.value[key] = data));
+ }
+
+ return {
+ init,
+ getCharacter() {
+ return character as Ref;
+ },
+ getVehicle() {
+ return vehicle as Ref;
+ },
+ };
+}
diff --git a/webview/src/App.vue b/webview/src/App.vue
index 419cd3a1a..7950f085b 100644
--- a/webview/src/App.vue
+++ b/webview/src/App.vue
@@ -5,6 +5,7 @@ import { useMessenger } from '../composables/useMessenger';
import { useMinimap } from '../composables/useMinimap';
import { usePageEvents } from '../composables/usePageEvents';
import { usePages } from '../composables/usePages';
+import { useSyncedMeta } from '../composables/useSyncedMeta';
import DevelopmentBar from './components/Development.vue';
const { pagesPersistent, pagesOverlay, page } = usePages();
@@ -20,6 +21,7 @@ function handleMount() {
useAudio();
useMessenger();
useMinimap().init();
+ useSyncedMeta().init();
}
onMounted(handleMount);
diff --git a/webview/tsconfig.node.json b/webview/tsconfig.node.json
index 97ede7ee6..1b1775ddf 100644
--- a/webview/tsconfig.node.json
+++ b/webview/tsconfig.node.json
@@ -1,11 +1,19 @@
{
- "compilerOptions": {
- "composite": true,
- "skipLibCheck": true,
- "module": "ESNext",
- "moduleResolution": "bundler",
- "allowSyntheticDefaultImports": true,
- "strict": true
- },
- "include": ["vite.config.ts"]
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "paths": {
+ "@Server/*": ["main/server/*"],
+ "@Client/*": ["main/client/*"],
+ "@Shared/*": ["../main/shared/*"],
+ "@Plugins/*": ["plugins/*"],
+ "@Composables/*": ["../webview/composables/*"],
+ "@Components/*": ["../webview/components/*"]
+ }
+ },
+ "include": ["vite.config.ts"]
}