@open-core/framework 0.3.0 → 0.3.1

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.
@@ -162,23 +162,45 @@ async function initServer(options) {
162
162
  if (ctx.mode === 'CORE' &&
163
163
  index_1.GLOBAL_CONTAINER.isRegistered(adapters_1.IEngineEvents) &&
164
164
  index_1.GLOBAL_CONTAINER.isRegistered(adapters_1.INetTransport)) {
165
- const engineEvents = index_1.GLOBAL_CONTAINER.resolve(adapters_1.IEngineEvents); // server -> servers
166
- const net = index_1.GLOBAL_CONTAINER.resolve(adapters_1.INetTransport); // server -> clients
167
- engineEvents.emit('core:ready'); // Broadcast to all Servers resources
168
- net.emitNet('core:ready', 'all'); // Broadcast to all Clients resources
169
- logger_1.loggers.bootstrap.debug(`'core:ready' events emmited to all clients and all servers`);
165
+ const engineEvents = index_1.GLOBAL_CONTAINER.resolve(adapters_1.IEngineEvents);
166
+ const net = index_1.GLOBAL_CONTAINER.resolve(adapters_1.INetTransport);
167
+ // 1. Broadast to resources already running
168
+ engineEvents.emit('core:ready');
169
+ net.emitNet('core:ready', 'all');
170
+ // 2. Listen for 'core:request-ready' for resources starting late (hot-reload)
171
+ engineEvents.on('core:request-ready', () => {
172
+ engineEvents.emit('core:ready');
173
+ });
174
+ logger_1.loggers.bootstrap.info(`'core:ready' logic initialized and broadcasted`);
170
175
  }
176
+ const logLevelLabel = logger_1.LogLevelLabels[(0, logger_1.getLogLevel)()];
177
+ logger_1.loggers.bootstrap.info(`LogLevel Setted: ${logLevelLabel}`);
171
178
  }
172
179
  function createCoreDependency(coreName) {
180
+ logger_1.loggers.bootstrap.debug(`Setting up detection mechanisms for Core '${coreName}'...`);
173
181
  return new Promise((resolve, reject) => {
174
182
  let resolved = false;
183
+ let pollingInterval;
184
+ let timeout;
175
185
  const engineEvents = index_1.GLOBAL_CONTAINER.resolve(adapters_1.IEngineEvents);
176
186
  const cleanup = () => {
177
187
  resolved = true;
178
- clearTimeout(timeout);
179
- clearInterval(pollingInterval);
188
+ if (timeout)
189
+ clearTimeout(timeout);
190
+ if (pollingInterval)
191
+ clearInterval(pollingInterval);
180
192
  };
181
- // 1. Check if already ready via export (Polling)
193
+ // 1. Register listener FIRST (before any requests)
194
+ const onReady = () => {
195
+ if (!resolved) {
196
+ logger_1.loggers.bootstrap.debug(`Core '${coreName}' detected via 'core:ready' event!`);
197
+ cleanup();
198
+ resolve();
199
+ }
200
+ };
201
+ engineEvents.on('core:ready', onReady);
202
+ logger_1.loggers.bootstrap.debug(`Listening for 'core:ready' event from Core`);
203
+ // 2. Check if already ready via export (Polling)
182
204
  const checkReady = () => {
183
205
  var _a, _b;
184
206
  if (resolved)
@@ -186,34 +208,33 @@ function createCoreDependency(coreName) {
186
208
  try {
187
209
  const globalExports = globalThis.exports;
188
210
  const isReady = (_b = (_a = globalExports === null || globalExports === void 0 ? void 0 : globalExports[coreName]) === null || _a === void 0 ? void 0 : _a.isCoreReady) === null || _b === void 0 ? void 0 : _b.call(_a);
211
+ logger_1.loggers.bootstrap.debug(`Polling isCoreReady export: ${isReady}`);
189
212
  if (isReady === true) {
190
- logger_1.loggers.bootstrap.debug(`Core '${coreName}' detected as already ready via export.`);
213
+ logger_1.loggers.bootstrap.debug(`Core '${coreName}' detected via isCoreReady export!`);
191
214
  cleanup();
192
215
  resolve();
193
216
  }
194
217
  }
195
- catch (_c) {
196
- // Core might not be started yet, ignore
218
+ catch (e) {
219
+ logger_1.loggers.bootstrap.debug(`Export check failed: ${e}`);
197
220
  }
198
221
  };
199
- const pollingInterval = setInterval(checkReady, 500);
222
+ pollingInterval = setInterval(checkReady, 500);
200
223
  checkReady(); // Initial check
201
- // 2. Timeout protection
202
- const timeout = setTimeout(() => {
224
+ // 3. Request status (for hot-reload cases where Core is already up)
225
+ // This is sent AFTER registering the listener so we can receive the response
226
+ if (!resolved) {
227
+ logger_1.loggers.bootstrap.debug(`Requesting Core status via 'core:request-ready' event`);
228
+ engineEvents.emit('core:request-ready');
229
+ }
230
+ // 4. Timeout protection
231
+ timeout = setTimeout(() => {
203
232
  if (!resolved) {
233
+ logger_1.loggers.bootstrap.warn(`Timeout waiting for Core '${coreName}' after ${CORE_WAIT_TIMEOUT}ms`);
204
234
  cleanup();
205
235
  reject(new Error(`[OpenCore] Timeout waiting for CORE '${coreName}'. The Core did not emit 'core:ready' or expose 'isCoreReady' within ${CORE_WAIT_TIMEOUT}ms.`));
206
236
  }
207
237
  }, CORE_WAIT_TIMEOUT);
208
- // 3. Listen for the event (for resources starting after/during CORE init)
209
- const onReady = () => {
210
- if (!resolved) {
211
- logger_1.loggers.bootstrap.debug(`Core '${coreName}' detected via 'core:ready' event.`);
212
- cleanup();
213
- resolve();
214
- }
215
- };
216
- engineEvents.on('core:ready', onReady);
217
238
  });
218
239
  }
219
240
  async function dependencyResolver(waitFor, onReady) {
@@ -159,12 +159,22 @@ let CommandExportController = class CommandExportController {
159
159
  * Exported as: `exports[coreResourceName].registerCommand`
160
160
  */
161
161
  registerCommand(metadata) {
162
- var _a;
163
162
  const commandKey = metadata.command.toLowerCase();
164
- if (this.remoteCommands.has(commandKey)) {
163
+ const existing = this.remoteCommands.get(commandKey);
164
+ if (existing) {
165
+ // Allow re-registration from the same resource (hot-reload scenario)
166
+ if (existing.resourceName === metadata.resourceName) {
167
+ logger_1.loggers.command.debug(`Re-registering command '${metadata.command}' from same resource (hot-reload)`, { command: metadata.command, resource: metadata.resourceName });
168
+ // Update the entry with new metadata
169
+ this.remoteCommands.set(commandKey, {
170
+ metadata,
171
+ resourceName: metadata.resourceName,
172
+ });
173
+ return;
174
+ }
165
175
  logger_1.loggers.command.warn(`Remote command '${metadata.command}' already registered`, {
166
176
  command: metadata.command,
167
- existingResource: (_a = this.remoteCommands.get(commandKey)) === null || _a === void 0 ? void 0 : _a.resourceName,
177
+ existingResource: existing.resourceName,
168
178
  newResource: metadata.resourceName,
169
179
  });
170
180
  return;
@@ -1,5 +1,4 @@
1
1
  export declare class ReadyController {
2
2
  private isReady;
3
- constructor();
4
3
  isCoreReady(): boolean;
5
4
  }
@@ -14,11 +14,7 @@ const controller_1 = require("../decorators/controller");
14
14
  const export_1 = require("../decorators/export");
15
15
  let ReadyController = class ReadyController {
16
16
  constructor() {
17
- this.isReady = false;
18
- // Set ready after a small tick to ensure bootstrap finishes
19
- setTimeout(() => {
20
- this.isReady = true;
21
- }, 0);
17
+ this.isReady = true;
22
18
  }
23
19
  isCoreReady() {
24
20
  return this.isReady;
@@ -32,6 +28,5 @@ __decorate([
32
28
  __metadata("design:returntype", Boolean)
33
29
  ], ReadyController.prototype, "isCoreReady", null);
34
30
  exports.ReadyController = ReadyController = __decorate([
35
- (0, controller_1.Controller)(),
36
- __metadata("design:paramtypes", [])
31
+ (0, controller_1.Controller)()
37
32
  ], ReadyController);
@@ -85,6 +85,11 @@ let RemoteCommandExecutionController = class RemoteCommandExecutionController {
85
85
  * @param args - Command arguments
86
86
  */
87
87
  async handleCommandExecution(clientID, commandName, args) {
88
+ logger_1.loggers.command.debug(`Received command execution request`, {
89
+ command: commandName,
90
+ clientID,
91
+ args,
92
+ });
88
93
  const player = this.playerDirectory.getByClient(clientID);
89
94
  if (!player) {
90
95
  logger_1.loggers.command.warn(`Command execution failed: player not found`, {
@@ -93,8 +98,17 @@ let RemoteCommandExecutionController = class RemoteCommandExecutionController {
93
98
  });
94
99
  return;
95
100
  }
101
+ logger_1.loggers.command.debug(`Executing command for player`, {
102
+ command: commandName,
103
+ playerName: player.name,
104
+ clientID,
105
+ });
96
106
  try {
97
107
  await this.commandService.execute(player, commandName, args);
108
+ logger_1.loggers.command.debug(`Command executed successfully`, {
109
+ command: commandName,
110
+ clientID,
111
+ });
98
112
  }
99
113
  catch (error) {
100
114
  // Do not notify the player here. Report through the global observer.
@@ -1,6 +1,3 @@
1
1
  import { CommandMetadata } from '../decorators/command';
2
2
  import { Player } from '../entities';
3
- /**
4
- * Centraliza validación de argumentos de comandos.
5
- */
6
3
  export declare function validateAndExecuteCommand(meta: CommandMetadata, player: Player, args: string[], handler: (...args: any[]) => any): Promise<any>;
@@ -7,13 +7,10 @@ exports.validateAndExecuteCommand = validateAndExecuteCommand;
7
7
  const zod_1 = __importDefault(require("zod"));
8
8
  const kernel_1 = require("../../../kernel");
9
9
  const schema_generator_1 = require("../system/schema-generator");
10
- /**
11
- * Centraliza validación de argumentos de comandos.
12
- */
10
+ const process_tuple_schema_1 = require("./process-tuple-schema");
13
11
  async function validateAndExecuteCommand(meta, player, args, handler) {
14
12
  const paramNames = meta.expectsPlayer ? meta.paramNames.slice(1) : meta.paramNames;
15
13
  let schema = meta.schema;
16
- // Caso: comando sin player → no espera argumentos
17
14
  if (!meta.expectsPlayer) {
18
15
  if (args.length > 0) {
19
16
  throw new kernel_1.AppError('GAME:BAD_REQUEST', `Incorrect usage, use: ${meta.usage}`, 'client', {
@@ -22,7 +19,6 @@ async function validateAndExecuteCommand(meta, player, args, handler) {
22
19
  }
23
20
  return await handler();
24
21
  }
25
- // Autogenerar esquema si no lo definieron
26
22
  if (!schema) {
27
23
  schema = (0, schema_generator_1.generateSchemaFromTypes)(meta.paramTypes);
28
24
  if (!schema) {
@@ -32,7 +28,6 @@ async function validateAndExecuteCommand(meta, player, args, handler) {
32
28
  return await handler(player);
33
29
  }
34
30
  }
35
- // OBJETO schema
36
31
  if (schema instanceof zod_1.default.ZodObject) {
37
32
  const keys = Object.keys(schema.shape);
38
33
  for (const p of paramNames) {
@@ -60,7 +55,8 @@ async function validateAndExecuteCommand(meta, player, args, handler) {
60
55
  }
61
56
  // TUPLA schema
62
57
  if (schema instanceof zod_1.default.ZodTuple) {
63
- const validated = await schema.parseAsync(args).catch(() => {
58
+ const processedArgs = (0, process_tuple_schema_1.processTupleSchema)(schema, args);
59
+ const validated = await schema.parseAsync(processedArgs).catch(() => {
64
60
  throw new kernel_1.AppError('GAME:BAD_REQUEST', `Incorrect usage, use: ${meta.usage}`, 'client', {
65
61
  usage: meta.usage,
66
62
  });
@@ -0,0 +1,20 @@
1
+ import z from 'zod';
2
+ /**
3
+ * Processes tuple schema validation with greedy handling for rest parameters.
4
+ *
5
+ * This function handles two cases:
6
+ * 1. If last parameter is ZodArray and there are MORE args than schema items,
7
+ * collect the extra args into the array position.
8
+ * 2. If last parameter is ZodString and there are MORE args than schema items,
9
+ * join the extra args into a single string.
10
+ *
11
+ * Examples:
12
+ * - handler(player, action: string, ...rest: string[]) with args ["hello", "world", "!"]
13
+ * → schema is [z.string(), z.array(z.string())] (2 items)
14
+ * → args has 3 items, so we group extra: ["hello", ["world", "!"]]
15
+ *
16
+ * - handler(player, command: string, args: string[]) with args ["vida", ["arg1"]]
17
+ * → schema is [z.string(), z.array(z.string())] (2 items)
18
+ * → args has 2 items, matches schema, no processing needed
19
+ */
20
+ export declare function processTupleSchema(schema: z.ZodTuple, args: any[]): any[];
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.processTupleSchema = processTupleSchema;
7
+ const zod_1 = __importDefault(require("zod"));
8
+ /**
9
+ * Processes tuple schema validation with greedy handling for rest parameters.
10
+ *
11
+ * This function handles two cases:
12
+ * 1. If last parameter is ZodArray and there are MORE args than schema items,
13
+ * collect the extra args into the array position.
14
+ * 2. If last parameter is ZodString and there are MORE args than schema items,
15
+ * join the extra args into a single string.
16
+ *
17
+ * Examples:
18
+ * - handler(player, action: string, ...rest: string[]) with args ["hello", "world", "!"]
19
+ * → schema is [z.string(), z.array(z.string())] (2 items)
20
+ * → args has 3 items, so we group extra: ["hello", ["world", "!"]]
21
+ *
22
+ * - handler(player, command: string, args: string[]) with args ["vida", ["arg1"]]
23
+ * → schema is [z.string(), z.array(z.string())] (2 items)
24
+ * → args has 2 items, matches schema, no processing needed
25
+ */
26
+ function processTupleSchema(schema, args) {
27
+ const items = schema.description ? [] : schema._def.items;
28
+ if (items.length === 0) {
29
+ return args;
30
+ }
31
+ // Only process if we have MORE args than schema expects
32
+ // This means we need to group extra args into the last position
33
+ if (args.length <= items.length) {
34
+ return args;
35
+ }
36
+ const lastItem = items[items.length - 1];
37
+ const positionalCount = items.length - 1;
38
+ // If last parameter is an array type, collect extra args into it
39
+ if (lastItem instanceof zod_1.default.ZodArray) {
40
+ const positional = args.slice(0, positionalCount);
41
+ const restArray = args.slice(positionalCount);
42
+ return [...positional, restArray];
43
+ }
44
+ // If last parameter is a string, join extra args with space
45
+ if (lastItem instanceof zod_1.default.ZodString) {
46
+ const positional = args.slice(0, positionalCount);
47
+ const restString = args.slice(positionalCount).join(' ');
48
+ return [...positional, restString];
49
+ }
50
+ return args;
51
+ }
@@ -72,20 +72,35 @@ let RemoteCommandService = class RemoteCommandService extends command_execution_
72
72
  register(metadata, handler) {
73
73
  var _a, _b, _c, _d;
74
74
  const commandKey = metadata.command.toLowerCase();
75
+ const resourceName = GetCurrentResourceName();
76
+ logger_1.loggers.command.debug(`Registering command locally`, {
77
+ command: metadata.command,
78
+ resource: resourceName,
79
+ });
75
80
  // Store handler with full metadata locally (for schema validation)
76
81
  this.commands.set(commandKey, {
77
82
  meta: metadata,
78
83
  handler,
79
84
  });
80
85
  // Register metadata with CORE (security only, schema is not serializable)
81
- this.core.registerCommand({
82
- command: metadata.command,
83
- description: metadata.description,
84
- usage: metadata.usage,
85
- isPublic: (_a = metadata.isPublic) !== null && _a !== void 0 ? _a : false,
86
- resourceName: GetCurrentResourceName(),
87
- security: metadata.security,
88
- });
86
+ try {
87
+ this.core.registerCommand({
88
+ command: metadata.command,
89
+ description: metadata.description,
90
+ usage: metadata.usage,
91
+ isPublic: (_a = metadata.isPublic) !== null && _a !== void 0 ? _a : false,
92
+ resourceName,
93
+ security: metadata.security,
94
+ });
95
+ }
96
+ catch (e) {
97
+ logger_1.loggers.command.error(`Failed to register command with CORE`, {
98
+ command: metadata.command,
99
+ resource: resourceName,
100
+ error: e,
101
+ });
102
+ return;
103
+ }
89
104
  const publicFlag = metadata.isPublic ? ' [Public]' : '';
90
105
  const schemaFlag = metadata.schema ? ' [Validated]' : '';
91
106
  const securityFlags = [];
@@ -23,6 +23,7 @@ const INetTransport_1 = require("../../../../adapters/contracts/INetTransport");
23
23
  const logger_1 = require("../../../../kernel/logger");
24
24
  const net_event_security_observer_contract_1 = require("../../contracts/security/net-event-security-observer.contract");
25
25
  const security_handler_contract_1 = require("../../contracts/security/security-handler.contract");
26
+ const process_tuple_schema_1 = require("../../helpers/process-tuple-schema");
26
27
  const resolve_method_1 = require("../../helpers/resolve-method");
27
28
  const player_directory_port_1 = require("../../services/ports/player-directory.port");
28
29
  const metadata_server_keys_1 = require("../metadata-server.keys");
@@ -81,7 +82,8 @@ let NetEventProcessor = class NetEventProcessor {
81
82
  }
82
83
  try {
83
84
  if (schema instanceof zod_1.default.ZodTuple) {
84
- validatedArgs = schema.parse(args);
85
+ const processedArgs = (0, process_tuple_schema_1.processTupleSchema)(schema, args);
86
+ validatedArgs = schema.parse(processedArgs);
85
87
  }
86
88
  else {
87
89
  if (args.length !== 1) {
@@ -18,7 +18,9 @@ function typeToZodSchema(type) {
18
18
  case Boolean:
19
19
  return zod_1.default.coerce.boolean();
20
20
  case Array:
21
- return zod_1.default.array(zod_1.default.any());
21
+ // Command/event arguments are always strings, so Array means string[]
22
+ // This enables spread operator support: handler(player: Player, ...args: string[])
23
+ return zod_1.default.array(zod_1.default.string());
22
24
  case Object:
23
25
  return undefined;
24
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-core/framework",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Secure, Event-Driven, OOP Engine for FiveM. Stop scripting, start engineering.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",