erlc-v2 1.1.2 → 1.2.0

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 CHANGED
@@ -185,9 +185,13 @@ Core:
185
185
 
186
186
  Convenience methods:
187
187
 
188
- - `await client.players.list()`
189
- - `await client.map.render(options?)`
190
- - `await client.map.renderUser(userId, options?)`
188
+ - `await client.players.list()`
189
+ - `await client.players.get(userIdOrName)`
190
+ - `await client.players.find(userIdOrName)`
191
+ - `await client.players.findById(userId)`
192
+ - `await client.players.findByName(username)`
193
+ - `await client.map.render(options?)`
194
+ - `await client.map.renderUser(userId, options?)`
191
195
  - `await client.staff.list()`
192
196
  - `await client.logs.kills()`
193
197
  - `await client.logs.joins()`
@@ -217,11 +221,35 @@ Convenience methods:
217
221
 
218
222
  - `bypassCache?: boolean`
219
223
  - `cacheTtlMs?: number`
220
- - `dedupe?: boolean`
221
-
222
- ## Vehicle Search Helpers
223
-
224
- Find one exact plate:
224
+ - `dedupe?: boolean`
225
+
226
+ ## Player Lookup
227
+
228
+ `client.players.list()` returns the ER:LC player objects with the original API fields still on them. The wrapper also adds easier aliases like `userId`, `username`, `team`, `wantedStars`, and `location`.
229
+
230
+ ```js
231
+ const player = await client.players.get(123456789);
232
+
233
+ if (player?.location) {
234
+ console.log(player.username);
235
+ console.log(player.location.postalCode);
236
+ console.log(player.location.streetName);
237
+ console.log(player.location.buildingNumber);
238
+ }
239
+ ```
240
+
241
+ The original response is still there too:
242
+
243
+ ```js
244
+ console.log(player.Player);
245
+ console.log(player.Location?.PostalCode);
246
+ ```
247
+
248
+ Player lookup uses the same cached `Players=true` server request as `players.list()`, so calling `get()`, `findById()`, or `findByName()` right after each other will not keep refetching from `erlc.gg` while the cache entry is still fresh.
249
+
250
+ ## Vehicle Search Helpers
251
+
252
+ Find one exact plate:
225
253
 
226
254
  ```js
227
255
  const car = await client.vehicles.findByPlate("LINCOLN7");
@@ -333,9 +361,10 @@ Built-in routes:
333
361
 
334
362
  - `GET /erlc`
335
363
  - `GET /erlc/health`
336
- - `GET /erlc/server`
337
- - `GET /erlc/players`
338
- - `GET /erlc/vehicles`
364
+ - `GET /erlc/server`
365
+ - `GET /erlc/players`
366
+ - `GET /erlc/players/:userIdOrName`
367
+ - `GET /erlc/vehicles`
339
368
  - `GET /erlc/vehicles/:plate`
340
369
  - `GET /erlc/emergency-calls`
341
370
  - `POST /erlc/command`
package/index.d.ts CHANGED
@@ -107,20 +107,54 @@ export interface CommandExecuteResult {
107
107
  };
108
108
  }
109
109
 
110
- export interface VehicleData {
111
- Name?: string;
112
- Owner?: string;
113
- Plate?: string;
110
+ export interface VehicleData {
111
+ Name?: string;
112
+ Owner?: string;
113
+ Plate?: string;
114
114
  Texture?: string | null;
115
115
  ColorHex?: string;
116
116
  ColorName?: string;
117
- [key: string]: any;
118
- }
119
-
120
- export interface EmergencyCallData {
121
- Team?: string;
122
- Caller?: number;
123
- Players?: number[];
117
+ [key: string]: any;
118
+ }
119
+
120
+ export interface PlayerLocationData {
121
+ LocationX?: number;
122
+ LocationZ?: number;
123
+ PostalCode?: string;
124
+ StreetName?: string;
125
+ BuildingNumber?: string;
126
+ locationX: number | null;
127
+ locationZ: number | null;
128
+ postalCode: string | null;
129
+ streetName: string | null;
130
+ buildingNumber: string | null;
131
+ raw: Record<string, any>;
132
+ [key: string]: any;
133
+ }
134
+
135
+ export interface PlayerData {
136
+ Team?: string;
137
+ Player?: string;
138
+ Callsign?: string | null;
139
+ Location?: Record<string, any>;
140
+ Permission?: string;
141
+ WantedStars?: number;
142
+ name: string | null;
143
+ username: string | null;
144
+ userId: number | null;
145
+ team: string | null;
146
+ callsign: string | null;
147
+ permission: string | null;
148
+ wantedStars: number | null;
149
+ location: PlayerLocationData | null;
150
+ raw: Record<string, any>;
151
+ [key: string]: any;
152
+ }
153
+
154
+ export interface EmergencyCallData {
155
+ Team?: string;
156
+ Caller?: number;
157
+ Players?: number[];
124
158
  Position?: number[];
125
159
  StartedAt?: number;
126
160
  CallNumber?: number;
@@ -220,10 +254,10 @@ export interface MapMarkerOptions {
220
254
  shadow?: boolean;
221
255
  }
222
256
 
223
- export interface MapRenderOptions {
224
- userId?: number | string;
225
- userIds?: Array<number | string>;
226
- players?: any[];
257
+ export interface MapRenderOptions {
258
+ userId?: number | string;
259
+ userIds?: Array<number | string>;
260
+ players?: PlayerData[] | any[];
227
261
  mapUrl?: string;
228
262
  season?: string;
229
263
  type?: string;
@@ -277,7 +311,7 @@ export interface ServerResponse {
277
311
  joinKey: string | null;
278
312
  accVerifiedReq: string | null;
279
313
  teamBalance: boolean | null;
280
- players: any[];
314
+ players: PlayerData[];
281
315
  staff: any;
282
316
  joinLogs: any[];
283
317
  queue: any[];
@@ -339,9 +373,25 @@ export class Client extends EventEmitter {
339
373
  requestOptions?: RequestOptions,
340
374
  ) => Promise<ServerResponse>;
341
375
  };
342
- players: {
343
- list: (requestOptions?: RequestOptions) => Promise<any[]>;
344
- };
376
+ players: {
377
+ list: (requestOptions?: RequestOptions) => Promise<PlayerData[]>;
378
+ get: (
379
+ user: number | string,
380
+ requestOptions?: RequestOptions,
381
+ ) => Promise<PlayerData | null>;
382
+ find: (
383
+ user: number | string,
384
+ requestOptions?: RequestOptions,
385
+ ) => Promise<PlayerData | null>;
386
+ findById: (
387
+ userId: number | string,
388
+ requestOptions?: RequestOptions,
389
+ ) => Promise<PlayerData | null>;
390
+ findByName: (
391
+ name: string,
392
+ requestOptions?: RequestOptions,
393
+ ) => Promise<PlayerData | null>;
394
+ };
345
395
  map: {
346
396
  render: (
347
397
  options?: MapRenderOptions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "erlc-v2",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Premium, lightweight JavaScript wrapper for the ER:LC API v2.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
package/src/Client.js CHANGED
@@ -5,11 +5,11 @@ const Poller = require("./events/Poller");
5
5
  const LocalApiServer = require("./api/LocalApiServer");
6
6
  const { DEFAULT_OPTIONS, QUERY_FLAG_MAP } = require("./util/constants");
7
7
  const { mergeOptions } = require("./util/options");
8
- const { createLogger } = require("./util/logger");
9
- const { normalizeServerResponse } = require("./util/normalize");
10
- const { renderPlayerMap } = require("./map/renderPlayerMap");
11
- const { searchVehicles, findVehicleByPlate } = require("./util/vehicleSearch");
12
- const { ERLCError } = require("./errors");
8
+ const { createLogger } = require("./util/logger");
9
+ const { normalizeServerResponse } = require("./util/normalize");
10
+ const { renderPlayerMap } = require("./map/renderPlayerMap");
11
+ const { searchVehicles, findVehicleByPlate } = require("./util/vehicleSearch");
12
+ const { ERLCError } = require("./errors");
13
13
 
14
14
  const BASE_URL = "https://api.erlc.gg";
15
15
  const BLOCKED_COMMANDS = new Set([
@@ -61,15 +61,42 @@ function getCommandKeyword(command) {
61
61
  return raw.split(/\s+/)[0].toLowerCase();
62
62
  }
63
63
 
64
- function isPromiseLike(value) {
65
- return (
66
- value !== null &&
67
- typeof value === "object" &&
68
- typeof value.then === "function"
69
- );
70
- }
71
-
72
- class Client extends EventEmitter {
64
+ function isPromiseLike(value) {
65
+ return (
66
+ value !== null &&
67
+ typeof value === "object" &&
68
+ typeof value.then === "function"
69
+ );
70
+ }
71
+
72
+ function low(value) {
73
+ return String(value ?? "").trim().toLowerCase();
74
+ }
75
+
76
+ function findPlayer(players, user, byName = false) {
77
+ if (!Array.isArray(players)) return null;
78
+
79
+ const text = low(user);
80
+ if (!text) return null;
81
+
82
+ const asId = Number(text);
83
+ const hasId = Number.isFinite(asId);
84
+
85
+ return (
86
+ players.find((p) => {
87
+ if (!p || typeof p !== "object") return false;
88
+ if (!byName && hasId && Number(p.userId) === asId) return true;
89
+
90
+ const name = low(p.username ?? p.name);
91
+ const raw = low(p.Player);
92
+ if (byName) return name === text || raw === text;
93
+
94
+ return name === text || raw === text;
95
+ }) ?? null
96
+ );
97
+ }
98
+
99
+ class Client extends EventEmitter {
73
100
  constructor(options = {}) {
74
101
  super();
75
102
 
@@ -104,12 +131,15 @@ class Client extends EventEmitter {
104
131
  this._fetchServer(flags, requestOptions),
105
132
  };
106
133
 
107
- this.players = {
108
- list: (requestOptions = {}) =>
109
- this.server
110
- .fetch({ players: true }, requestOptions)
111
- .then((d) => d.players),
112
- };
134
+ this.players = {
135
+ list: (requestOptions = {}) => this._listPlayers(requestOptions),
136
+ get: (user, requestOptions = {}) => this._getPlayer(user, requestOptions),
137
+ find: (user, requestOptions = {}) => this._getPlayer(user, requestOptions),
138
+ findById: (userId, requestOptions = {}) =>
139
+ this._getPlayer(userId, requestOptions),
140
+ findByName: (name, requestOptions = {}) =>
141
+ this._getPlayerByName(name, requestOptions),
142
+ };
113
143
 
114
144
  this.map = {
115
145
  render: (options = {}, requestOptions = {}) =>
@@ -340,7 +370,7 @@ class Client extends EventEmitter {
340
370
  return query;
341
371
  }
342
372
 
343
- async _fetchServer(flags = {}, requestOptions = {}) {
373
+ async _fetchServer(flags = {}, requestOptions = {}) {
344
374
  if (this.state.disconnected && this.state.disconnectError) {
345
375
  throw this.state.disconnectError;
346
376
  }
@@ -365,10 +395,26 @@ class Client extends EventEmitter {
365
395
  bucket: response.bucket,
366
396
  rateLimit: response.rateLimit,
367
397
  };
368
- return normalized;
369
- }
370
-
371
- async _executeCommand(command, requestOptions = {}) {
398
+ return normalized;
399
+ }
400
+
401
+ async _listPlayers(requestOptions = {}) {
402
+ return this.server
403
+ .fetch({ players: true }, requestOptions)
404
+ .then((d) => d.players);
405
+ }
406
+
407
+ async _getPlayer(user, requestOptions = {}) {
408
+ const players = await this._listPlayers(requestOptions);
409
+ return findPlayer(players, user);
410
+ }
411
+
412
+ async _getPlayerByName(name, requestOptions = {}) {
413
+ const players = await this._listPlayers(requestOptions);
414
+ return findPlayer(players, name, true);
415
+ }
416
+
417
+ async _executeCommand(command, requestOptions = {}) {
372
418
  if (this.state.disconnected && this.state.disconnectError) {
373
419
  throw this.state.disconnectError;
374
420
  }
@@ -249,10 +249,10 @@ class LocalApiServer {
249
249
  return;
250
250
  }
251
251
 
252
- if (req.method === "GET" && routePath === "/players") {
253
- const players = await this.client.players.list({
254
- bypassCache: parseBool(url.searchParams.get("bypassCache")),
255
- });
252
+ if (req.method === "GET" && routePath === "/players") {
253
+ const players = await this.client.players.list({
254
+ bypassCache: parseBool(url.searchParams.get("bypassCache")),
255
+ });
256
256
  this._recordRequest({
257
257
  req,
258
258
  url,
@@ -264,14 +264,43 @@ class LocalApiServer {
264
264
  sendJson(res, 200, {
265
265
  count: players.length,
266
266
  players,
267
- });
268
- return;
269
- }
270
-
271
- if (req.method === "GET" && routePath === "/vehicles") {
272
- const vehicles = await this.client.vehicles.list({
273
- bypassCache: parseBool(url.searchParams.get("bypassCache")),
274
- });
267
+ });
268
+ return;
269
+ }
270
+
271
+ if (req.method === "GET" && routePath.startsWith("/players/")) {
272
+ const user = decodeURIComponent(routePath.slice("/players/".length));
273
+ const player = await this.client.players.get(user, {
274
+ bypassCache: parseBool(url.searchParams.get("bypassCache")),
275
+ });
276
+ if (!player) {
277
+ this._recordRequest({
278
+ req,
279
+ url,
280
+ startedAt,
281
+ kind: "route",
282
+ status: 404,
283
+ note: `player=${user}`,
284
+ });
285
+ sendJson(res, 404, { message: "Player not found" });
286
+ return;
287
+ }
288
+ this._recordRequest({
289
+ req,
290
+ url,
291
+ startedAt,
292
+ kind: "route",
293
+ status: 200,
294
+ note: `player=${user}`,
295
+ });
296
+ sendJson(res, 200, player);
297
+ return;
298
+ }
299
+
300
+ if (req.method === "GET" && routePath === "/vehicles") {
301
+ const vehicles = await this.client.vehicles.list({
302
+ bypassCache: parseBool(url.searchParams.get("bypassCache")),
303
+ });
275
304
  const filtered = searchVehicles(vehicles, {
276
305
  query: url.searchParams.get("query") || url.searchParams.get("search"),
277
306
  plate: url.searchParams.get("plate"),
@@ -1,3 +1,61 @@
1
+ function toUserId(value) {
2
+ if (typeof value === "number" && Number.isFinite(value)) return value;
3
+ if (typeof value !== "string") return null;
4
+
5
+ const text = value.trim();
6
+ if (!text) return null;
7
+
8
+ const idPart = text.includes(":") ? text.split(":").pop() : text;
9
+ const parsed = Number(idPart);
10
+ return Number.isFinite(parsed) ? parsed : null;
11
+ }
12
+
13
+ function playerName(value) {
14
+ if (typeof value !== "string") return null;
15
+ const text = value.trim();
16
+ if (!text) return null;
17
+ return text.includes(":") ? text.slice(0, text.lastIndexOf(":")) : text;
18
+ }
19
+
20
+ function normalizeLocation(raw) {
21
+ if (!raw || typeof raw !== "object") return null;
22
+
23
+ return {
24
+ ...raw,
25
+ locationX: raw.LocationX ?? null,
26
+ locationZ: raw.LocationZ ?? null,
27
+ postalCode: raw.PostalCode ?? null,
28
+ streetName: raw.StreetName ?? null,
29
+ buildingNumber: raw.BuildingNumber ?? null,
30
+ raw,
31
+ };
32
+ }
33
+
34
+ function normalizePlayer(p = {}) {
35
+ const rawPlayer = p.Player ?? p.Name ?? null;
36
+ const location = normalizeLocation(p.Location);
37
+
38
+ return {
39
+ ...p,
40
+ name: playerName(rawPlayer),
41
+ username: playerName(rawPlayer),
42
+ userId: toUserId(rawPlayer),
43
+ team: p.Team ?? null,
44
+ callsign: p.Callsign ?? null,
45
+ permission: p.Permission ?? null,
46
+ wantedStars: p.WantedStars ?? null,
47
+ location,
48
+ raw: p,
49
+ };
50
+ }
51
+
52
+ function normalizePlayers(rawPlayers) {
53
+ if (!Array.isArray(rawPlayers)) return [];
54
+ return rawPlayers.map((p) =>
55
+ p && typeof p === "object" ? normalizePlayer(p) : p,
56
+ );
57
+ }
58
+
1
59
  function normalizeServerResponse(raw = {}) {
2
60
  return {
3
61
  name: raw.Name ?? null,
@@ -8,7 +66,7 @@ function normalizeServerResponse(raw = {}) {
8
66
  joinKey: raw.JoinKey ?? null,
9
67
  accVerifiedReq: raw.AccVerifiedReq ?? null,
10
68
  teamBalance: raw.TeamBalance ?? null,
11
- players: Array.isArray(raw.Players) ? raw.Players : [],
69
+ players: normalizePlayers(raw.Players),
12
70
  staff: raw.Staff ?? { Admins: {}, Mods: {}, Helpers: {} },
13
71
  joinLogs: Array.isArray(raw.JoinLogs) ? raw.JoinLogs : [],
14
72
  queue: Array.isArray(raw.Queue) ? raw.Queue : [],
@@ -23,4 +81,7 @@ function normalizeServerResponse(raw = {}) {
23
81
 
24
82
  module.exports = {
25
83
  normalizeServerResponse,
84
+ normalizePlayer,
85
+ normalizePlayers,
86
+ toUserId,
26
87
  };