@open-core/framework 1.0.5 → 1.0.7
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 +25 -28
- package/dist/adapters/contracts/transport/index.d.ts +1 -0
- package/dist/adapters/contracts/transport/index.js +1 -0
- package/dist/adapters/contracts/transport/rpc-error.d.ts +17 -0
- package/dist/adapters/contracts/transport/rpc-error.js +28 -0
- 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 +4 -0
- 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/system/processors/onRpc.processor.js +14 -3
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
[](https://opencorejs.dev)
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
# OpenCore Framework -
|
|
9
|
+
# OpenCore Framework - Stable v1
|
|
10
10
|
|
|
11
|
-
OpenCore is a TypeScript multiplayer runtime framework targeting CitizenFX runtimes (Cfx) via adapters.
|
|
11
|
+
OpenCore is a TypeScript multiplayer runtime framework targeting CitizenFX runtimes (Cfx/RageMP) via adapters.
|
|
12
12
|
|
|
13
13
|
It is not a gamemode or RP framework. It provides:
|
|
14
14
|
|
|
@@ -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
|
|
|
@@ -277,12 +280,6 @@ pnpm lint:fix
|
|
|
277
280
|
pnpm format
|
|
278
281
|
```
|
|
279
282
|
|
|
280
|
-
## Ecosystem
|
|
281
|
-
|
|
282
|
-
OpenCore is designed to be extended via separate packages/resources.
|
|
283
|
-
|
|
284
|
-
- `@open-core/identity`: identity and permission system
|
|
285
|
-
|
|
286
283
|
## License
|
|
287
284
|
|
|
288
285
|
MPL-2.0. See `LICENSE`.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const PUBLIC_RPC_ERROR_MESSAGE = "An internal server error occurred";
|
|
2
|
+
export type RpcErrorInfo = {
|
|
3
|
+
message: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
};
|
|
6
|
+
type ExposedRpcError = {
|
|
7
|
+
message: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
expose: true;
|
|
10
|
+
};
|
|
11
|
+
export declare class RpcPublicError extends Error {
|
|
12
|
+
readonly expose = true;
|
|
13
|
+
constructor(message: string, name?: string);
|
|
14
|
+
}
|
|
15
|
+
export declare function isExposedRpcError(error: unknown): error is ExposedRpcError;
|
|
16
|
+
export declare function serializeRpcError(error: unknown): RpcErrorInfo;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const PUBLIC_RPC_ERROR_MESSAGE = 'An internal server error occurred';
|
|
2
|
+
export class RpcPublicError extends Error {
|
|
3
|
+
expose = true;
|
|
4
|
+
constructor(message, name) {
|
|
5
|
+
super(message);
|
|
6
|
+
if (name) {
|
|
7
|
+
this.name = name;
|
|
8
|
+
}
|
|
9
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function isExposedRpcError(error) {
|
|
13
|
+
return (typeof error === 'object' &&
|
|
14
|
+
error !== null &&
|
|
15
|
+
'message' in error &&
|
|
16
|
+
typeof error.message === 'string' &&
|
|
17
|
+
'expose' in error &&
|
|
18
|
+
error.expose === true);
|
|
19
|
+
}
|
|
20
|
+
export function serializeRpcError(error) {
|
|
21
|
+
if (isExposedRpcError(error)) {
|
|
22
|
+
return {
|
|
23
|
+
message: error.message,
|
|
24
|
+
name: 'name' in error && typeof error.name === 'string' ? error.name : undefined,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return { message: PUBLIC_RPC_ERROR_MESSAGE };
|
|
28
|
+
}
|
|
@@ -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
|
}
|
|
@@ -10,6 +10,7 @@ import { getServerBinaryServiceRegistry } from './decorators/binaryService';
|
|
|
10
10
|
import { getServerControllerRegistry } from './decorators/controller';
|
|
11
11
|
import { getFrameworkModeScope, setRuntimeContext, validateRuntimeOptions, } from './runtime';
|
|
12
12
|
import { BinaryProcessManager } from './system/managers/binary-process.manager';
|
|
13
|
+
import { PlayerPersistenceService } from './services/persistence.service';
|
|
13
14
|
import { SessionRecoveryService } from './services/session-recovery.local';
|
|
14
15
|
import { registerServicesServer } from './services/services.register';
|
|
15
16
|
import { SYSTEM_EVENTS } from '../shared/types/system-types';
|
|
@@ -131,6 +132,9 @@ export async function initServer(options, plugins) {
|
|
|
131
132
|
registerSystemServer(ctx);
|
|
132
133
|
loggers.bootstrap.debug('System processors registered');
|
|
133
134
|
checkProviders(ctx);
|
|
135
|
+
if (ctx.features.sessionLifecycle.enabled && ctx.mode !== 'RESOURCE') {
|
|
136
|
+
GLOBAL_CONTAINER.resolve(PlayerPersistenceService).initialize();
|
|
137
|
+
}
|
|
134
138
|
const scanner = GLOBAL_CONTAINER.resolve(MetadataScanner);
|
|
135
139
|
scanner.scan(getServerControllerRegistry());
|
|
136
140
|
const binaryServices = getServerBinaryServiceRegistry();
|
|
@@ -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
|
}
|
|
@@ -124,10 +124,21 @@ let OnRpcProcessor = class OnRpcProcessor {
|
|
|
124
124
|
});
|
|
125
125
|
throw error;
|
|
126
126
|
}
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
try {
|
|
128
|
+
if (hasNoDeclaredParams) {
|
|
129
|
+
return await handler();
|
|
130
|
+
}
|
|
131
|
+
return await handler(player, ...validatedArgs);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
loggers.netEvent.error(`Handler error in RPC`, {
|
|
135
|
+
event: metadata.eventName,
|
|
136
|
+
handler: handlerName,
|
|
137
|
+
playerId: player.clientID,
|
|
138
|
+
accountId: player.accountID,
|
|
139
|
+
}, error);
|
|
140
|
+
throw error;
|
|
129
141
|
}
|
|
130
|
-
return handler(player, ...validatedArgs);
|
|
131
142
|
});
|
|
132
143
|
loggers.netEvent.debug(`Registered RPC: ${metadata.eventName} -> ${handlerName}`);
|
|
133
144
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-core/framework",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
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",
|