@open-core/framework 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -20
- package/dist/adapters/node/transport/node.events.d.ts +2 -0
- package/dist/adapters/node/transport/node.events.js +16 -0
- package/dist/runtime/server/bootstrap.js +13 -0
- package/dist/runtime/server/bus/internal-event.bus.d.ts +10 -1
- package/dist/runtime/server/bus/internal-event.bus.js +95 -0
- package/dist/runtime/server/decorators/onFrameworkEvent.d.ts +18 -2
- package/dist/runtime/server/decorators/onFrameworkEvent.js +18 -2
- package/dist/runtime/server/decorators/onRuntimeEvent.d.ts +15 -2
- package/dist/runtime/server/decorators/onRuntimeEvent.js +15 -2
- package/dist/runtime/server/implementations/local/player.local.d.ts +6 -6
- package/dist/runtime/server/implementations/local/player.local.js +76 -5
- package/dist/runtime/server/types/framework-events.types.d.ts +46 -0
- package/dist/runtime/shared/types/system-types.d.ts +3 -0
- package/dist/runtime/shared/types/system-types.js +3 -0
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -227,34 +227,37 @@ Note: `pnpm test` does not run benchmarks.
|
|
|
227
227
|
|
|
228
228
|
## Benchmarks
|
|
229
229
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
- Core benchmarks (Tinybench)
|
|
233
|
-
- Load benchmarks (Vitest project `benchmark`)
|
|
230
|
+
Benchmarks are split by value, so the default run focuses on framework features that matter for real servers.
|
|
234
231
|
|
|
235
232
|
```bash
|
|
236
|
-
pnpm bench
|
|
233
|
+
pnpm bench
|
|
234
|
+
pnpm bench:value
|
|
235
|
+
pnpm bench:gold
|
|
236
|
+
pnpm bench:startup
|
|
237
|
+
pnpm bench:diagnostic
|
|
238
|
+
pnpm bench:soak
|
|
237
239
|
pnpm bench:load
|
|
238
240
|
pnpm bench:all
|
|
239
241
|
```
|
|
240
242
|
|
|
243
|
+
- `bench` / `bench:value`: value-focused suite. Commands, net events, RPC, lifecycle, ticks, binary path, bootstrap.
|
|
244
|
+
- `bench:gold`: hot-path load scenarios only.
|
|
245
|
+
- `bench:startup`: startup and registration cost.
|
|
246
|
+
- `bench:diagnostic`: internal and low-level synthetic benchmarks.
|
|
247
|
+
- `bench:soak`: long-running stress scenario.
|
|
248
|
+
|
|
241
249
|
### Snapshot (latest local run)
|
|
242
250
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
-
|
|
246
|
-
-
|
|
247
|
-
-
|
|
248
|
-
-
|
|
249
|
-
-
|
|
250
|
-
-
|
|
251
|
-
-
|
|
252
|
-
-
|
|
253
|
-
- Commands - 500 players (validated): `~4.78M ops/sec` (p95 `~0.008ms`)
|
|
254
|
-
- Pipeline - validated (500 players): `~4.79M ops/sec` (p95 `~0.024ms`)
|
|
255
|
-
- Pipeline - full (500 players): `~2.34M ops/sec` (p95 `~0.011ms`)
|
|
256
|
-
- RPC - schema generation complex (500 methods): `~705K ops/sec` (p95 `~0.335ms`)
|
|
257
|
-
- Commands - 500 players (concurrent): `~6.31K ops/sec` (p95 `~76.00ms`)
|
|
251
|
+
Use `benchmark/reports/` as the source of truth. Results vary by machine and should be compared relatively, not treated as product guarantees.
|
|
252
|
+
|
|
253
|
+
- Primary benchmark targets:
|
|
254
|
+
- full command execution
|
|
255
|
+
- full net event handling
|
|
256
|
+
- RPC processing
|
|
257
|
+
- player lifecycle churn
|
|
258
|
+
- tick budget impact
|
|
259
|
+
- bootstrap cost
|
|
260
|
+
- binary transport cost
|
|
258
261
|
|
|
259
262
|
Full reports and methodology are available in benchmark/README.md.
|
|
260
263
|
|
|
@@ -3,12 +3,14 @@ import { RuntimeContext } from '../../contracts/transport/context';
|
|
|
3
3
|
type NodeTarget = number | number[] | 'all';
|
|
4
4
|
export declare class NodeEvents extends EventsAPI<RuntimeContext> {
|
|
5
5
|
private readonly emitter;
|
|
6
|
+
private readonly asyncHandlers;
|
|
6
7
|
on<TArgs extends readonly unknown[]>(event: string, handler: (ctx: {
|
|
7
8
|
clientId?: number;
|
|
8
9
|
raw?: unknown;
|
|
9
10
|
}, ...args: TArgs) => unknown): void;
|
|
10
11
|
emit(event: string, targetOrArg?: NodeTarget | unknown, ...args: unknown[]): void;
|
|
11
12
|
simulateClientEvent(event: string, clientId: number, ...args: unknown[]): void;
|
|
13
|
+
simulateClientEventAsync(event: string, clientId: number, ...args: unknown[]): Promise<void>;
|
|
12
14
|
clearHandlers(): void;
|
|
13
15
|
}
|
|
14
16
|
export {};
|
|
@@ -3,7 +3,14 @@ import { EventsAPI } from '../../contracts/transport/events.api';
|
|
|
3
3
|
import { loggers } from '../../../kernel/logger';
|
|
4
4
|
export class NodeEvents extends EventsAPI {
|
|
5
5
|
emitter = new EventEmitter();
|
|
6
|
+
asyncHandlers = new Map();
|
|
6
7
|
on(event, handler) {
|
|
8
|
+
let handlers = this.asyncHandlers.get(event);
|
|
9
|
+
if (!handlers) {
|
|
10
|
+
handlers = new Set();
|
|
11
|
+
this.asyncHandlers.set(event, handlers);
|
|
12
|
+
}
|
|
13
|
+
handlers.add(handler);
|
|
7
14
|
this.emitter.on(event, (ctx, ...args) => {
|
|
8
15
|
void Promise.resolve(handler(ctx, ...args)).catch((err) => {
|
|
9
16
|
loggers.netEvent.error(`handler error for '${event}'`, {}, err);
|
|
@@ -32,7 +39,16 @@ export class NodeEvents extends EventsAPI {
|
|
|
32
39
|
simulateClientEvent(event, clientId, ...args) {
|
|
33
40
|
this.emitter.emit(event, { clientId, raw: clientId }, ...args);
|
|
34
41
|
}
|
|
42
|
+
async simulateClientEventAsync(event, clientId, ...args) {
|
|
43
|
+
const handlers = this.asyncHandlers.get(event);
|
|
44
|
+
if (!handlers || handlers.size === 0) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const ctx = { clientId, raw: clientId };
|
|
48
|
+
await Promise.allSettled(Array.from(handlers, (handler) => Promise.resolve(handler(ctx, ...args))));
|
|
49
|
+
}
|
|
35
50
|
clearHandlers() {
|
|
36
51
|
this.emitter.removeAllListeners();
|
|
52
|
+
this.asyncHandlers.clear();
|
|
37
53
|
}
|
|
38
54
|
}
|
|
@@ -5,11 +5,14 @@ import { GLOBAL_CONTAINER, MetadataScanner } from '../../kernel/di/index';
|
|
|
5
5
|
import { getLogLevel, LogLevelLabels, loggers } from '../../kernel/logger';
|
|
6
6
|
import { createNodeServerAdapter } from './adapter/node-server-adapter';
|
|
7
7
|
import { installServerAdapter } from './adapter/registry';
|
|
8
|
+
import { configureFrameworkEventBridge } from './bus/internal-event.bus';
|
|
8
9
|
import { PrincipalProviderContract } from './contracts/index';
|
|
9
10
|
import { getServerBinaryServiceRegistry } from './decorators/binaryService';
|
|
10
11
|
import { getServerControllerRegistry } from './decorators/controller';
|
|
12
|
+
import { Players } from './ports/players.api-port';
|
|
11
13
|
import { getFrameworkModeScope, setRuntimeContext, validateRuntimeOptions, } from './runtime';
|
|
12
14
|
import { BinaryProcessManager } from './system/managers/binary-process.manager';
|
|
15
|
+
import { PlayerPersistenceService } from './services/persistence.service';
|
|
13
16
|
import { SessionRecoveryService } from './services/session-recovery.local';
|
|
14
17
|
import { registerServicesServer } from './services/services.register';
|
|
15
18
|
import { SYSTEM_EVENTS } from '../shared/types/system-types';
|
|
@@ -115,6 +118,13 @@ export async function initServer(options, plugins) {
|
|
|
115
118
|
// 1. Register Core Services (WorldContext, PlayerService, etc.)
|
|
116
119
|
registerServicesServer(ctx);
|
|
117
120
|
loggers.bootstrap.debug('Core services registered');
|
|
121
|
+
configureFrameworkEventBridge({
|
|
122
|
+
mode: ctx.mode,
|
|
123
|
+
engineEvents: GLOBAL_CONTAINER.resolve(IEngineEvents),
|
|
124
|
+
players: GLOBAL_CONTAINER.isRegistered(Players)
|
|
125
|
+
? GLOBAL_CONTAINER.resolve(Players)
|
|
126
|
+
: undefined,
|
|
127
|
+
});
|
|
118
128
|
// 2. Load Controllers (Framework & User controllers)
|
|
119
129
|
// This is where user services get registered if they are decorated with @injectable()
|
|
120
130
|
// and imported before init() or discovered here.
|
|
@@ -131,6 +141,9 @@ export async function initServer(options, plugins) {
|
|
|
131
141
|
registerSystemServer(ctx);
|
|
132
142
|
loggers.bootstrap.debug('System processors registered');
|
|
133
143
|
checkProviders(ctx);
|
|
144
|
+
if (ctx.features.sessionLifecycle.enabled && ctx.mode !== 'RESOURCE') {
|
|
145
|
+
GLOBAL_CONTAINER.resolve(PlayerPersistenceService).initialize();
|
|
146
|
+
}
|
|
134
147
|
const scanner = GLOBAL_CONTAINER.resolve(MetadataScanner);
|
|
135
148
|
scanner.scan(getServerControllerRegistry());
|
|
136
149
|
const binaryServices = getServerBinaryServiceRegistry();
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { IEngineEvents } from '../../../adapters/contracts/IEngineEvents';
|
|
2
|
+
import { type FrameworkMode } from '../runtime';
|
|
3
|
+
import { Players } from '../ports/players.api-port';
|
|
4
|
+
import { type FrameworkEventsMap } from '../types/framework-events.types';
|
|
2
5
|
type InternalEventName = keyof FrameworkEventsMap;
|
|
3
6
|
type InternalEventHandler<E extends InternalEventName> = (payload: FrameworkEventsMap[E]) => void;
|
|
7
|
+
interface FrameworkEventBridgeConfig {
|
|
8
|
+
mode: FrameworkMode;
|
|
9
|
+
engineEvents?: IEngineEvents;
|
|
10
|
+
players?: Players;
|
|
11
|
+
}
|
|
4
12
|
export declare function onFrameworkEvent<E extends InternalEventName>(event: E, handler: InternalEventHandler<E>): () => void;
|
|
13
|
+
export declare function configureFrameworkEventBridge(config: FrameworkEventBridgeConfig): void;
|
|
5
14
|
export declare function emitFrameworkEvent<E extends InternalEventName>(event: E, payload: FrameworkEventsMap[E]): void;
|
|
6
15
|
export {};
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { loggers } from '../../../kernel/logger';
|
|
2
|
+
import { SYSTEM_EVENTS } from '../../shared/types/system-types';
|
|
2
3
|
const handlers = {};
|
|
4
|
+
const bridgeListenerRegistrations = new WeakSet();
|
|
5
|
+
let bridgeConfig = {
|
|
6
|
+
mode: 'STANDALONE',
|
|
7
|
+
};
|
|
3
8
|
export function onFrameworkEvent(event, handler) {
|
|
4
9
|
let list = handlers[event];
|
|
5
10
|
if (!list) {
|
|
@@ -13,7 +18,32 @@ export function onFrameworkEvent(event, handler) {
|
|
|
13
18
|
list.splice(index, 1);
|
|
14
19
|
};
|
|
15
20
|
}
|
|
21
|
+
export function configureFrameworkEventBridge(config) {
|
|
22
|
+
bridgeConfig = config;
|
|
23
|
+
if (config.mode !== 'RESOURCE' || !config.engineEvents)
|
|
24
|
+
return;
|
|
25
|
+
if (bridgeListenerRegistrations.has(config.engineEvents))
|
|
26
|
+
return;
|
|
27
|
+
config.engineEvents.on(SYSTEM_EVENTS.framework.dispatch, (envelope) => {
|
|
28
|
+
if (bridgeConfig.mode !== 'RESOURCE')
|
|
29
|
+
return;
|
|
30
|
+
dispatchTransportFrameworkEvent(envelope.event, envelope.payload);
|
|
31
|
+
});
|
|
32
|
+
bridgeListenerRegistrations.add(config.engineEvents);
|
|
33
|
+
}
|
|
16
34
|
export function emitFrameworkEvent(event, payload) {
|
|
35
|
+
dispatchLocalFrameworkEvent(event, payload);
|
|
36
|
+
if (bridgeConfig.mode !== 'CORE' || !bridgeConfig.engineEvents)
|
|
37
|
+
return;
|
|
38
|
+
const transportPayload = serializeFrameworkEvent(event, payload);
|
|
39
|
+
if (!transportPayload)
|
|
40
|
+
return;
|
|
41
|
+
bridgeConfig.engineEvents.emit(SYSTEM_EVENTS.framework.dispatch, {
|
|
42
|
+
event,
|
|
43
|
+
payload: transportPayload,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function dispatchLocalFrameworkEvent(event, payload) {
|
|
17
47
|
const list = handlers[event];
|
|
18
48
|
if (!list)
|
|
19
49
|
return;
|
|
@@ -28,3 +58,68 @@ export function emitFrameworkEvent(event, payload) {
|
|
|
28
58
|
}
|
|
29
59
|
}
|
|
30
60
|
}
|
|
61
|
+
function dispatchTransportFrameworkEvent(event, payload) {
|
|
62
|
+
const hydrated = hydrateFrameworkEvent(event, payload);
|
|
63
|
+
if (!hydrated)
|
|
64
|
+
return;
|
|
65
|
+
dispatchLocalFrameworkEvent(event, hydrated);
|
|
66
|
+
}
|
|
67
|
+
function serializeFrameworkEvent(event, payload) {
|
|
68
|
+
switch (event) {
|
|
69
|
+
case 'internal:playerSessionCreated':
|
|
70
|
+
case 'internal:playerSessionDestroyed':
|
|
71
|
+
return payload;
|
|
72
|
+
case 'internal:playerFullyConnected': {
|
|
73
|
+
const fullyConnectedPayload = payload;
|
|
74
|
+
return {
|
|
75
|
+
clientId: fullyConnectedPayload.player.clientID,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
case 'internal:playerSessionRecovered': {
|
|
79
|
+
const recoveredPayload = payload;
|
|
80
|
+
return {
|
|
81
|
+
clientId: recoveredPayload.clientId,
|
|
82
|
+
license: recoveredPayload.license,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
default:
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function hydrateFrameworkEvent(event, payload) {
|
|
90
|
+
switch (event) {
|
|
91
|
+
case 'internal:playerSessionCreated':
|
|
92
|
+
case 'internal:playerSessionDestroyed':
|
|
93
|
+
return payload;
|
|
94
|
+
case 'internal:playerFullyConnected': {
|
|
95
|
+
const fullyConnectedPayload = payload;
|
|
96
|
+
const player = bridgeConfig.players?.getByClient(fullyConnectedPayload.clientId);
|
|
97
|
+
if (!player) {
|
|
98
|
+
loggers.eventBus.warn('Skipping framework event: player not found during hydration', {
|
|
99
|
+
event,
|
|
100
|
+
clientId: fullyConnectedPayload.clientId,
|
|
101
|
+
});
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return { player };
|
|
105
|
+
}
|
|
106
|
+
case 'internal:playerSessionRecovered': {
|
|
107
|
+
const recoveredPayload = payload;
|
|
108
|
+
const player = bridgeConfig.players?.getByClient(recoveredPayload.clientId);
|
|
109
|
+
if (!player) {
|
|
110
|
+
loggers.eventBus.warn('Skipping framework event: player not found during hydration', {
|
|
111
|
+
event,
|
|
112
|
+
clientId: recoveredPayload.clientId,
|
|
113
|
+
});
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
clientId: recoveredPayload.clientId,
|
|
118
|
+
license: recoveredPayload.license,
|
|
119
|
+
player,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
default:
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -6,7 +6,23 @@ import { FrameworkEventsMap } from '../types/framework-events.types';
|
|
|
6
6
|
* This decorator only stores metadata. The framework binds listeners during bootstrap by scanning
|
|
7
7
|
* controller methods.
|
|
8
8
|
*
|
|
9
|
-
* The handler
|
|
9
|
+
* The handler receives the framework payload associated with the selected event from
|
|
10
|
+
* {@link FrameworkEventsMap}. Unlike {@link OnRuntimeEvent}, this payload is framework-defined
|
|
11
|
+
* and may include hydrated entities such as {@link Player}.
|
|
12
|
+
*
|
|
13
|
+
* Framework events are delivered:
|
|
14
|
+
* - locally in `STANDALONE`
|
|
15
|
+
* - locally inside `CORE`
|
|
16
|
+
* - from `CORE` to `RESOURCE` through the internal framework bridge
|
|
17
|
+
*
|
|
18
|
+
* For bridged events, the transport payload is serialized in `CORE` and then rehydrated in the
|
|
19
|
+
* receiving `RESOURCE`. That means handlers can keep using the same payload shape in every mode.
|
|
20
|
+
*
|
|
21
|
+
* Current built-in payloads:
|
|
22
|
+
* - `internal:playerSessionCreated`: `{ clientId, license }`
|
|
23
|
+
* - `internal:playerSessionDestroyed`: `{ clientId }`
|
|
24
|
+
* - `internal:playerFullyConnected`: `{ player }`
|
|
25
|
+
* - `internal:playerSessionRecovered`: `{ clientId, license, player }`
|
|
10
26
|
*
|
|
11
27
|
* @param event - Internal event name, strongly typed to {@link FrameworkEventsMap}.
|
|
12
28
|
*
|
|
@@ -16,7 +32,7 @@ import { FrameworkEventsMap } from '../types/framework-events.types';
|
|
|
16
32
|
* export class SystemController {
|
|
17
33
|
* @Server.OnFrameworkEvent('internal:playerFullyConnected')
|
|
18
34
|
* onPlayerConnected(payload: PlayerFullyConnectedPayload) {
|
|
19
|
-
* console.log(`Player ${payload.player.
|
|
35
|
+
* console.log(`Player ${payload.player.clientID} connected`)
|
|
20
36
|
* }
|
|
21
37
|
* }
|
|
22
38
|
* ```
|
|
@@ -6,7 +6,23 @@ import { METADATA_KEYS } from '../system/metadata-server.keys';
|
|
|
6
6
|
* This decorator only stores metadata. The framework binds listeners during bootstrap by scanning
|
|
7
7
|
* controller methods.
|
|
8
8
|
*
|
|
9
|
-
* The handler
|
|
9
|
+
* The handler receives the framework payload associated with the selected event from
|
|
10
|
+
* {@link FrameworkEventsMap}. Unlike {@link OnRuntimeEvent}, this payload is framework-defined
|
|
11
|
+
* and may include hydrated entities such as {@link Player}.
|
|
12
|
+
*
|
|
13
|
+
* Framework events are delivered:
|
|
14
|
+
* - locally in `STANDALONE`
|
|
15
|
+
* - locally inside `CORE`
|
|
16
|
+
* - from `CORE` to `RESOURCE` through the internal framework bridge
|
|
17
|
+
*
|
|
18
|
+
* For bridged events, the transport payload is serialized in `CORE` and then rehydrated in the
|
|
19
|
+
* receiving `RESOURCE`. That means handlers can keep using the same payload shape in every mode.
|
|
20
|
+
*
|
|
21
|
+
* Current built-in payloads:
|
|
22
|
+
* - `internal:playerSessionCreated`: `{ clientId, license }`
|
|
23
|
+
* - `internal:playerSessionDestroyed`: `{ clientId }`
|
|
24
|
+
* - `internal:playerFullyConnected`: `{ player }`
|
|
25
|
+
* - `internal:playerSessionRecovered`: `{ clientId, license, player }`
|
|
10
26
|
*
|
|
11
27
|
* @param event - Internal event name, strongly typed to {@link FrameworkEventsMap}.
|
|
12
28
|
*
|
|
@@ -16,7 +32,7 @@ import { METADATA_KEYS } from '../system/metadata-server.keys';
|
|
|
16
32
|
* export class SystemController {
|
|
17
33
|
* @Server.OnFrameworkEvent('internal:playerFullyConnected')
|
|
18
34
|
* onPlayerConnected(payload: PlayerFullyConnectedPayload) {
|
|
19
|
-
* console.log(`Player ${payload.player.
|
|
35
|
+
* console.log(`Player ${payload.player.clientID} connected`)
|
|
20
36
|
* }
|
|
21
37
|
* }
|
|
22
38
|
* ```
|
|
@@ -7,6 +7,19 @@ import type { RuntimeEventName } from '../../../adapters/contracts/runtime';
|
|
|
7
7
|
* This decorator only stores metadata. During bootstrap, the framework scans controller
|
|
8
8
|
* methods and binds handlers to runtime events.
|
|
9
9
|
*
|
|
10
|
+
* The decorated method receives the raw runtime arguments emitted by the adapter for
|
|
11
|
+
* the selected event. OpenCore does not wrap these arguments into an object payload and
|
|
12
|
+
* does not automatically resolve a {@link Player} entity for you.
|
|
13
|
+
*
|
|
14
|
+
* Common server event signatures in the current runtime map:
|
|
15
|
+
* - `playerJoining`: `(clientId: number, identifiers?: Record<string, string>)`
|
|
16
|
+
* - `playerDropped`: `(clientId: number)`
|
|
17
|
+
* - `onServerResourceStop`: `(resourceName: string)`
|
|
18
|
+
* - `playerCommand`: runtime-specific raw arguments from the adapter
|
|
19
|
+
*
|
|
20
|
+
* If you need a framework-managed payload such as `{ player }`, use
|
|
21
|
+
* {@link OnFrameworkEvent} instead.
|
|
22
|
+
*
|
|
10
23
|
* CitizenFX server event reference:
|
|
11
24
|
* https://docs.fivem.net/docs/scripting-reference/events/server-events/
|
|
12
25
|
*
|
|
@@ -17,8 +30,8 @@ import type { RuntimeEventName } from '../../../adapters/contracts/runtime';
|
|
|
17
30
|
* @Server.Controller()
|
|
18
31
|
* export class SessionController {
|
|
19
32
|
* @Server.OnRuntimeEvent('playerJoining')
|
|
20
|
-
* onPlayerJoining() {
|
|
21
|
-
* //
|
|
33
|
+
* onPlayerJoining(clientId: number, identifiers?: Record<string, string>) {
|
|
34
|
+
* // Raw runtime arguments
|
|
22
35
|
* }
|
|
23
36
|
* }
|
|
24
37
|
* ```
|
|
@@ -7,6 +7,19 @@ import { METADATA_KEYS } from '../system/metadata-server.keys';
|
|
|
7
7
|
* This decorator only stores metadata. During bootstrap, the framework scans controller
|
|
8
8
|
* methods and binds handlers to runtime events.
|
|
9
9
|
*
|
|
10
|
+
* The decorated method receives the raw runtime arguments emitted by the adapter for
|
|
11
|
+
* the selected event. OpenCore does not wrap these arguments into an object payload and
|
|
12
|
+
* does not automatically resolve a {@link Player} entity for you.
|
|
13
|
+
*
|
|
14
|
+
* Common server event signatures in the current runtime map:
|
|
15
|
+
* - `playerJoining`: `(clientId: number, identifiers?: Record<string, string>)`
|
|
16
|
+
* - `playerDropped`: `(clientId: number)`
|
|
17
|
+
* - `onServerResourceStop`: `(resourceName: string)`
|
|
18
|
+
* - `playerCommand`: runtime-specific raw arguments from the adapter
|
|
19
|
+
*
|
|
20
|
+
* If you need a framework-managed payload such as `{ player }`, use
|
|
21
|
+
* {@link OnFrameworkEvent} instead.
|
|
22
|
+
*
|
|
10
23
|
* CitizenFX server event reference:
|
|
11
24
|
* https://docs.fivem.net/docs/scripting-reference/events/server-events/
|
|
12
25
|
*
|
|
@@ -17,8 +30,8 @@ import { METADATA_KEYS } from '../system/metadata-server.keys';
|
|
|
17
30
|
* @Server.Controller()
|
|
18
31
|
* export class SessionController {
|
|
19
32
|
* @Server.OnRuntimeEvent('playerJoining')
|
|
20
|
-
* onPlayerJoining() {
|
|
21
|
-
* //
|
|
33
|
+
* onPlayerJoining(clientId: number, identifiers?: Record<string, string>) {
|
|
34
|
+
* // Raw runtime arguments
|
|
22
35
|
* }
|
|
23
36
|
* }
|
|
24
37
|
* ```
|
|
@@ -24,13 +24,13 @@ export declare class LocalPlayerImplementation implements Players, PlayerSession
|
|
|
24
24
|
private readonly world;
|
|
25
25
|
private readonly playerInfo;
|
|
26
26
|
private readonly playerServer;
|
|
27
|
-
private readonly playerLifecycle
|
|
28
|
-
private readonly playerStateSync
|
|
29
|
-
private readonly entityServer
|
|
30
|
-
private readonly events
|
|
31
|
-
private readonly platformContext
|
|
27
|
+
private readonly playerLifecycle?;
|
|
28
|
+
private readonly playerStateSync?;
|
|
29
|
+
private readonly entityServer?;
|
|
30
|
+
private readonly events?;
|
|
31
|
+
private readonly platformContext?;
|
|
32
32
|
private readonly playerAdapters;
|
|
33
|
-
constructor(world: WorldContext, playerInfo: IPlayerInfo, playerServer: IPlayerServer, playerLifecycle
|
|
33
|
+
constructor(world: WorldContext, playerInfo: IPlayerInfo, playerServer: IPlayerServer, playerLifecycle?: IPlayerLifecycleServer | undefined, playerStateSync?: IPlayerStateSyncServer | undefined, entityServer?: IEntityServer | undefined, events?: EventsAPI<"server"> | undefined, platformContext?: IPlatformContext | undefined);
|
|
34
34
|
private isPlayer;
|
|
35
35
|
/**
|
|
36
36
|
* Initializes a new player session for a connecting client.
|
|
@@ -21,6 +21,37 @@ import { IPlayerServer } from '../../../../adapters/contracts/server/IPlayerServ
|
|
|
21
21
|
import { loggers } from '../../../../kernel/logger';
|
|
22
22
|
import { WorldContext } from '../../../core/world';
|
|
23
23
|
import { createLocalServerPlayer } from '../../adapter/registry';
|
|
24
|
+
class NoopPlayerLifecycleServer extends IPlayerLifecycleServer {
|
|
25
|
+
spawn() { }
|
|
26
|
+
teleport() { }
|
|
27
|
+
respawn() { }
|
|
28
|
+
}
|
|
29
|
+
class NoopPlayerStateSyncServer extends IPlayerStateSyncServer {
|
|
30
|
+
getHealth() {
|
|
31
|
+
return 200;
|
|
32
|
+
}
|
|
33
|
+
setHealth() { }
|
|
34
|
+
getArmor() {
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
setArmor() { }
|
|
38
|
+
}
|
|
39
|
+
const DEFAULT_PLATFORM_CONTEXT = {
|
|
40
|
+
platformName: 'node',
|
|
41
|
+
displayName: 'Node.js',
|
|
42
|
+
identifierTypes: ['license'],
|
|
43
|
+
maxPlayers: undefined,
|
|
44
|
+
gameProfile: 'common',
|
|
45
|
+
defaultSpawnModel: 'mp_m_freemode_01',
|
|
46
|
+
defaultVehicleType: 'automobile',
|
|
47
|
+
enableServerVehicleCreation: true,
|
|
48
|
+
};
|
|
49
|
+
function isEntityServer(value) {
|
|
50
|
+
return (!!value && typeof value === 'object' && typeof value.getCoords === 'function');
|
|
51
|
+
}
|
|
52
|
+
function isEventsAPI(value) {
|
|
53
|
+
return (!!value && typeof value === 'object' && typeof value.on === 'function');
|
|
54
|
+
}
|
|
24
55
|
/**
|
|
25
56
|
* Service responsible for managing the lifecycle of player sessions.
|
|
26
57
|
* It acts as the central registry for all connected players, mapping FiveM client IDs
|
|
@@ -49,14 +80,54 @@ let LocalPlayerImplementation = class LocalPlayerImplementation {
|
|
|
49
80
|
this.entityServer = entityServer;
|
|
50
81
|
this.events = events;
|
|
51
82
|
this.platformContext = platformContext;
|
|
52
|
-
|
|
83
|
+
let resolvedPlayerLifecycle = this.playerLifecycle;
|
|
84
|
+
let resolvedPlayerStateSync = this.playerStateSync;
|
|
85
|
+
let resolvedEntityServer = this.entityServer;
|
|
86
|
+
let resolvedEvents = this.events;
|
|
87
|
+
// Backward compatibility for tests/benchmarks still using the old 5-arg constructor.
|
|
88
|
+
if (!resolvedEntityServer &&
|
|
89
|
+
isEntityServer(resolvedPlayerLifecycle) &&
|
|
90
|
+
isEventsAPI(resolvedPlayerStateSync)) {
|
|
91
|
+
resolvedEntityServer = resolvedPlayerLifecycle;
|
|
92
|
+
resolvedEvents = resolvedPlayerStateSync;
|
|
93
|
+
resolvedPlayerLifecycle = new NoopPlayerLifecycleServer();
|
|
94
|
+
resolvedPlayerStateSync = new NoopPlayerStateSyncServer();
|
|
95
|
+
}
|
|
96
|
+
resolvedPlayerLifecycle ??= new NoopPlayerLifecycleServer();
|
|
97
|
+
resolvedPlayerStateSync ??= new NoopPlayerStateSyncServer();
|
|
98
|
+
resolvedEntityServer ??= {
|
|
99
|
+
doesExist: () => true,
|
|
100
|
+
getCoords: () => ({ x: 0, y: 0, z: 0 }),
|
|
101
|
+
setCoords: () => { },
|
|
102
|
+
setPosition: () => { },
|
|
103
|
+
getHeading: () => 0,
|
|
104
|
+
setHeading: () => { },
|
|
105
|
+
getModel: () => 0,
|
|
106
|
+
delete: () => { },
|
|
107
|
+
setOrphanMode: () => { },
|
|
108
|
+
setDimension: () => { },
|
|
109
|
+
getDimension: () => 0,
|
|
110
|
+
getStateBag: () => ({
|
|
111
|
+
set: () => undefined,
|
|
112
|
+
get: () => undefined,
|
|
113
|
+
}),
|
|
114
|
+
getHealth: () => 200,
|
|
115
|
+
setHealth: () => { },
|
|
116
|
+
getArmor: () => 0,
|
|
117
|
+
setArmor: () => { },
|
|
118
|
+
};
|
|
119
|
+
resolvedEvents ??= {
|
|
120
|
+
on: () => { },
|
|
121
|
+
emit: () => { },
|
|
122
|
+
};
|
|
123
|
+
const defaultSpawnModel = (this.platformContext ?? DEFAULT_PLATFORM_CONTEXT).defaultSpawnModel;
|
|
53
124
|
this.playerAdapters = {
|
|
54
125
|
playerInfo: this.playerInfo,
|
|
55
126
|
playerServer: this.playerServer,
|
|
56
|
-
playerLifecycle:
|
|
57
|
-
playerStateSync:
|
|
58
|
-
entityServer:
|
|
59
|
-
events:
|
|
127
|
+
playerLifecycle: resolvedPlayerLifecycle,
|
|
128
|
+
playerStateSync: resolvedPlayerStateSync,
|
|
129
|
+
entityServer: resolvedEntityServer,
|
|
130
|
+
events: resolvedEvents,
|
|
60
131
|
defaultSpawnModel,
|
|
61
132
|
};
|
|
62
133
|
}
|
|
@@ -1,19 +1,65 @@
|
|
|
1
1
|
import { Player } from '../entities';
|
|
2
|
+
/**
|
|
3
|
+
* Emitted when a player session is created in the framework session lifecycle.
|
|
4
|
+
*/
|
|
2
5
|
export interface PlayerSessionCreatedPayload {
|
|
3
6
|
clientId: number;
|
|
4
7
|
license: string | undefined;
|
|
5
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Emitted when a player session is destroyed in the framework session lifecycle.
|
|
11
|
+
*/
|
|
6
12
|
export interface PlayerSessionDestroyedPayload {
|
|
7
13
|
clientId: number;
|
|
8
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Emitted when the framework considers the player fully connected and the runtime-specific
|
|
17
|
+
* {@link Player} entity can be consumed safely by application code.
|
|
18
|
+
*/
|
|
9
19
|
export interface PlayerFullyConnectedPayload {
|
|
10
20
|
player: Player;
|
|
11
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Emitted when the framework recreates a player session after resource restart recovery.
|
|
24
|
+
*/
|
|
12
25
|
export interface PlayerSessionRecoveredPayload {
|
|
13
26
|
clientId: number;
|
|
14
27
|
player: Player;
|
|
15
28
|
license: string | undefined;
|
|
16
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Serialized transport payload for `internal:playerFullyConnected` used across `CORE -> RESOURCE`.
|
|
32
|
+
*/
|
|
33
|
+
export interface PlayerFullyConnectedTransportPayload {
|
|
34
|
+
clientId: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Serialized transport payload for `internal:playerSessionRecovered` used across `CORE -> RESOURCE`.
|
|
38
|
+
*/
|
|
39
|
+
export interface PlayerSessionRecoveredTransportPayload {
|
|
40
|
+
clientId: number;
|
|
41
|
+
license: string | undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Internal transport contract used by the framework bridge when delivering framework events
|
|
45
|
+
* between different server runtimes.
|
|
46
|
+
*/
|
|
47
|
+
export type FrameworkTransportEventsMap = {
|
|
48
|
+
'internal:playerSessionCreated': PlayerSessionCreatedPayload;
|
|
49
|
+
'internal:playerSessionDestroyed': PlayerSessionDestroyedPayload;
|
|
50
|
+
'internal:playerFullyConnected': PlayerFullyConnectedTransportPayload;
|
|
51
|
+
'internal:playerSessionRecovered': PlayerSessionRecoveredTransportPayload;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Envelope emitted through the internal framework bridge.
|
|
55
|
+
*/
|
|
56
|
+
export interface FrameworkEventEnvelope<E extends keyof FrameworkTransportEventsMap> {
|
|
57
|
+
event: E;
|
|
58
|
+
payload: FrameworkTransportEventsMap[E];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Public payload map consumed by {@link OnFrameworkEvent} and `onFrameworkEvent(...)`.
|
|
62
|
+
*/
|
|
17
63
|
export type FrameworkEventsMap = {
|
|
18
64
|
'internal:playerSessionCreated': PlayerSessionCreatedPayload;
|
|
19
65
|
'internal:playerSessionDestroyed': PlayerSessionDestroyedPayload;
|
|
@@ -16,6 +16,9 @@ export declare const SYSTEM_EVENTS: {
|
|
|
16
16
|
readonly command: {
|
|
17
17
|
readonly execute: `opencore:${string}:${string}`;
|
|
18
18
|
};
|
|
19
|
+
readonly framework: {
|
|
20
|
+
readonly dispatch: `opencore:${string}:${string}`;
|
|
21
|
+
};
|
|
19
22
|
readonly spawner: {
|
|
20
23
|
readonly spawn: `opencore:${string}:${string}`;
|
|
21
24
|
readonly teleport: `opencore:${string}:${string}`;
|
|
@@ -17,6 +17,9 @@ export const SYSTEM_EVENTS = {
|
|
|
17
17
|
command: {
|
|
18
18
|
execute: systemEvent('command', 'execute'),
|
|
19
19
|
},
|
|
20
|
+
framework: {
|
|
21
|
+
dispatch: systemEvent('framework', 'dispatch'),
|
|
22
|
+
},
|
|
20
23
|
spawner: {
|
|
21
24
|
spawn: systemEvent('spawner', 'spawn'),
|
|
22
25
|
teleport: systemEvent('spawner', 'teleport'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-core/framework",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "Secure, event-driven TypeScript Framework & Runtime engine for CitizenFX (Cfx).",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -72,9 +72,14 @@
|
|
|
72
72
|
"test:unit": "npx vitest run --project unit",
|
|
73
73
|
"test:integration": "npx vitest run --project integration",
|
|
74
74
|
"test:coverage": "npx vitest run --coverage",
|
|
75
|
-
"bench": "npx tsx benchmark/index.ts",
|
|
76
|
-
"bench:
|
|
77
|
-
"bench:
|
|
75
|
+
"bench": "npx tsx benchmark/index.ts --value",
|
|
76
|
+
"bench:value": "npx tsx benchmark/index.ts --value",
|
|
77
|
+
"bench:gold": "BENCHMARK_SUITE=gold npx vitest run --project benchmark-gold",
|
|
78
|
+
"bench:startup": "npx tsx benchmark/index.ts --startup",
|
|
79
|
+
"bench:diagnostic": "npx tsx benchmark/index.ts --diagnostic",
|
|
80
|
+
"bench:soak": "BENCHMARK_SUITE=soak npx vitest run --project benchmark-soak",
|
|
81
|
+
"bench:core": "npx tsx benchmark/index.ts --diagnostic",
|
|
82
|
+
"bench:load": "npx vitest run --project benchmark-gold --project benchmark-startup",
|
|
78
83
|
"bench:all": "npx tsx benchmark/index.ts --all",
|
|
79
84
|
"validate": "pnpm check && pnpm typecheck && pnpm test",
|
|
80
85
|
"lint-staged": "lint-staged",
|