@thestatic-tv/dcl-sdk 2.5.17 → 2.5.18
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/dist/index.d.mts +72 -1
- package/dist/index.d.ts +72 -1
- package/dist/index.js +164 -13
- package/dist/index.mjs +152 -4
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -320,6 +320,25 @@ interface SceneTabDefinition {
|
|
|
320
320
|
/** React-ECS component that renders the tab content */
|
|
321
321
|
render: () => JSX.Element | null;
|
|
322
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Scene boundary information calculated from parcels
|
|
325
|
+
*/
|
|
326
|
+
interface SceneBounds$1 {
|
|
327
|
+
/** Minimum X coordinate (always 0 in local coords) */
|
|
328
|
+
minX: number;
|
|
329
|
+
/** Maximum X coordinate (width in meters) */
|
|
330
|
+
maxX: number;
|
|
331
|
+
/** Minimum Z coordinate (always 0 in local coords) */
|
|
332
|
+
minZ: number;
|
|
333
|
+
/** Maximum Z coordinate (depth in meters) */
|
|
334
|
+
maxZ: number;
|
|
335
|
+
/** Scene width in meters */
|
|
336
|
+
width: number;
|
|
337
|
+
/** Scene depth in meters */
|
|
338
|
+
depth: number;
|
|
339
|
+
/** Original parcel strings from scene.json */
|
|
340
|
+
parcels: string[];
|
|
341
|
+
}
|
|
323
342
|
/**
|
|
324
343
|
* Configuration options for AdminPanelModule
|
|
325
344
|
*/
|
|
@@ -428,6 +447,9 @@ declare class GuideModule {
|
|
|
428
447
|
*
|
|
429
448
|
* Works with both channel keys (dclk_) and scene keys (dcls_).
|
|
430
449
|
* Scene keys use /scene-session endpoint, channel keys use /session.
|
|
450
|
+
*
|
|
451
|
+
* Includes automatic boundary detection to ensure metrics only track
|
|
452
|
+
* players who are actually within the scene's parcel boundaries.
|
|
431
453
|
*/
|
|
432
454
|
|
|
433
455
|
declare class SessionModule {
|
|
@@ -437,7 +459,19 @@ declare class SessionModule {
|
|
|
437
459
|
private heartbeatTimerId;
|
|
438
460
|
private isActive;
|
|
439
461
|
private _tier;
|
|
462
|
+
private _bounds;
|
|
463
|
+
private _boundsInitialized;
|
|
464
|
+
private _outOfBoundsCount;
|
|
440
465
|
constructor(client: StaticTVClient);
|
|
466
|
+
/**
|
|
467
|
+
* Get the scene bounds (auto-detected from parcels)
|
|
468
|
+
* Returns null if bounds haven't been initialized yet
|
|
469
|
+
*/
|
|
470
|
+
get bounds(): SceneBounds$1 | null;
|
|
471
|
+
/**
|
|
472
|
+
* Check if player is currently within scene bounds
|
|
473
|
+
*/
|
|
474
|
+
isPlayerInScene(): boolean;
|
|
441
475
|
/**
|
|
442
476
|
* Get the API key ID (used as default sceneId for Pro users)
|
|
443
477
|
*/
|
|
@@ -466,6 +500,11 @@ declare class SessionModule {
|
|
|
466
500
|
* Get player display name - prioritize config, fallback to runtime detection
|
|
467
501
|
*/
|
|
468
502
|
private getDisplayName;
|
|
503
|
+
/**
|
|
504
|
+
* Initialize scene bounds from DCL Runtime
|
|
505
|
+
* Called once on session start
|
|
506
|
+
*/
|
|
507
|
+
private initializeBounds;
|
|
469
508
|
/**
|
|
470
509
|
* Start a new session
|
|
471
510
|
* Called automatically if autoStartSession is true
|
|
@@ -477,6 +516,7 @@ declare class SessionModule {
|
|
|
477
516
|
private startHeartbeat;
|
|
478
517
|
/**
|
|
479
518
|
* Send a session heartbeat
|
|
519
|
+
* Always sends heartbeat with inScene status so dashboard can track location
|
|
480
520
|
*/
|
|
481
521
|
private sendHeartbeat;
|
|
482
522
|
/**
|
|
@@ -500,6 +540,8 @@ declare class SessionModule {
|
|
|
500
540
|
|
|
501
541
|
/**
|
|
502
542
|
* Heartbeat module - track video watching metrics
|
|
543
|
+
*
|
|
544
|
+
* Only tracks when player is within scene bounds.
|
|
503
545
|
*/
|
|
504
546
|
|
|
505
547
|
declare class HeartbeatModule {
|
|
@@ -525,6 +567,7 @@ declare class HeartbeatModule {
|
|
|
525
567
|
private getWallet;
|
|
526
568
|
/**
|
|
527
569
|
* Send a watching heartbeat (1 heartbeat = 1 minute watched)
|
|
570
|
+
* Only sends if player is within scene bounds
|
|
528
571
|
*/
|
|
529
572
|
private sendHeartbeat;
|
|
530
573
|
/**
|
|
@@ -1301,4 +1344,32 @@ declare function getPlayerWallet(): string | null;
|
|
|
1301
1344
|
*/
|
|
1302
1345
|
declare function getPlayerDisplayName(): string | null;
|
|
1303
1346
|
|
|
1304
|
-
|
|
1347
|
+
interface SceneBounds {
|
|
1348
|
+
minX: number;
|
|
1349
|
+
maxX: number;
|
|
1350
|
+
minZ: number;
|
|
1351
|
+
maxZ: number;
|
|
1352
|
+
width: number;
|
|
1353
|
+
depth: number;
|
|
1354
|
+
parcels: string[];
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* Get scene bounds from DCL Runtime
|
|
1358
|
+
* Auto-detects parcels and calculates local coordinate bounds
|
|
1359
|
+
*/
|
|
1360
|
+
declare function getSceneBounds(): Promise<SceneBounds | null>;
|
|
1361
|
+
/**
|
|
1362
|
+
* Check if player is within scene boundaries
|
|
1363
|
+
* Returns true if in bounds, false if out of bounds or can't determine
|
|
1364
|
+
*/
|
|
1365
|
+
declare function isPlayerInBounds(bounds: SceneBounds): boolean;
|
|
1366
|
+
/**
|
|
1367
|
+
* Get player's current position
|
|
1368
|
+
*/
|
|
1369
|
+
declare function getPlayerPosition(): {
|
|
1370
|
+
x: number;
|
|
1371
|
+
y: number;
|
|
1372
|
+
z: number;
|
|
1373
|
+
} | null;
|
|
1374
|
+
|
|
1375
|
+
export { type AdminPanelConfig, AdminPanelUIModule, type Channel, type ChatChannel, type ChatMessage, type ChatUIConfig, ChatUIModule, type CommandHandler, type GuideFeaturedPlaylist, GuideModule, type GuideResponse, type GuideUIConfig, GuideUIModule, type GuideVideo, HeartbeatModule, type HeartbeatResponse, type InteractionResponse, InteractionsModule, KEY_TYPE_CHANNEL, KEY_TYPE_SCENE, NotificationBanner, type PlayerData, type SDKTier, type SceneBounds$1 as SceneBounds, type SceneStats, type SceneStatsResponse, type SceneTabDefinition, SessionModule, type SessionResponse, StaticTVClient, type StaticTVConfig, type StreamData, type VideoSlot, type Vod, fetchUserData, getPlayerDisplayName, getPlayerPosition, getPlayerWallet, getSceneBounds, hideNotification, isPlayerInBounds, setupStaticUI, showNotification };
|
package/dist/index.d.ts
CHANGED
|
@@ -320,6 +320,25 @@ interface SceneTabDefinition {
|
|
|
320
320
|
/** React-ECS component that renders the tab content */
|
|
321
321
|
render: () => JSX.Element | null;
|
|
322
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Scene boundary information calculated from parcels
|
|
325
|
+
*/
|
|
326
|
+
interface SceneBounds$1 {
|
|
327
|
+
/** Minimum X coordinate (always 0 in local coords) */
|
|
328
|
+
minX: number;
|
|
329
|
+
/** Maximum X coordinate (width in meters) */
|
|
330
|
+
maxX: number;
|
|
331
|
+
/** Minimum Z coordinate (always 0 in local coords) */
|
|
332
|
+
minZ: number;
|
|
333
|
+
/** Maximum Z coordinate (depth in meters) */
|
|
334
|
+
maxZ: number;
|
|
335
|
+
/** Scene width in meters */
|
|
336
|
+
width: number;
|
|
337
|
+
/** Scene depth in meters */
|
|
338
|
+
depth: number;
|
|
339
|
+
/** Original parcel strings from scene.json */
|
|
340
|
+
parcels: string[];
|
|
341
|
+
}
|
|
323
342
|
/**
|
|
324
343
|
* Configuration options for AdminPanelModule
|
|
325
344
|
*/
|
|
@@ -428,6 +447,9 @@ declare class GuideModule {
|
|
|
428
447
|
*
|
|
429
448
|
* Works with both channel keys (dclk_) and scene keys (dcls_).
|
|
430
449
|
* Scene keys use /scene-session endpoint, channel keys use /session.
|
|
450
|
+
*
|
|
451
|
+
* Includes automatic boundary detection to ensure metrics only track
|
|
452
|
+
* players who are actually within the scene's parcel boundaries.
|
|
431
453
|
*/
|
|
432
454
|
|
|
433
455
|
declare class SessionModule {
|
|
@@ -437,7 +459,19 @@ declare class SessionModule {
|
|
|
437
459
|
private heartbeatTimerId;
|
|
438
460
|
private isActive;
|
|
439
461
|
private _tier;
|
|
462
|
+
private _bounds;
|
|
463
|
+
private _boundsInitialized;
|
|
464
|
+
private _outOfBoundsCount;
|
|
440
465
|
constructor(client: StaticTVClient);
|
|
466
|
+
/**
|
|
467
|
+
* Get the scene bounds (auto-detected from parcels)
|
|
468
|
+
* Returns null if bounds haven't been initialized yet
|
|
469
|
+
*/
|
|
470
|
+
get bounds(): SceneBounds$1 | null;
|
|
471
|
+
/**
|
|
472
|
+
* Check if player is currently within scene bounds
|
|
473
|
+
*/
|
|
474
|
+
isPlayerInScene(): boolean;
|
|
441
475
|
/**
|
|
442
476
|
* Get the API key ID (used as default sceneId for Pro users)
|
|
443
477
|
*/
|
|
@@ -466,6 +500,11 @@ declare class SessionModule {
|
|
|
466
500
|
* Get player display name - prioritize config, fallback to runtime detection
|
|
467
501
|
*/
|
|
468
502
|
private getDisplayName;
|
|
503
|
+
/**
|
|
504
|
+
* Initialize scene bounds from DCL Runtime
|
|
505
|
+
* Called once on session start
|
|
506
|
+
*/
|
|
507
|
+
private initializeBounds;
|
|
469
508
|
/**
|
|
470
509
|
* Start a new session
|
|
471
510
|
* Called automatically if autoStartSession is true
|
|
@@ -477,6 +516,7 @@ declare class SessionModule {
|
|
|
477
516
|
private startHeartbeat;
|
|
478
517
|
/**
|
|
479
518
|
* Send a session heartbeat
|
|
519
|
+
* Always sends heartbeat with inScene status so dashboard can track location
|
|
480
520
|
*/
|
|
481
521
|
private sendHeartbeat;
|
|
482
522
|
/**
|
|
@@ -500,6 +540,8 @@ declare class SessionModule {
|
|
|
500
540
|
|
|
501
541
|
/**
|
|
502
542
|
* Heartbeat module - track video watching metrics
|
|
543
|
+
*
|
|
544
|
+
* Only tracks when player is within scene bounds.
|
|
503
545
|
*/
|
|
504
546
|
|
|
505
547
|
declare class HeartbeatModule {
|
|
@@ -525,6 +567,7 @@ declare class HeartbeatModule {
|
|
|
525
567
|
private getWallet;
|
|
526
568
|
/**
|
|
527
569
|
* Send a watching heartbeat (1 heartbeat = 1 minute watched)
|
|
570
|
+
* Only sends if player is within scene bounds
|
|
528
571
|
*/
|
|
529
572
|
private sendHeartbeat;
|
|
530
573
|
/**
|
|
@@ -1301,4 +1344,32 @@ declare function getPlayerWallet(): string | null;
|
|
|
1301
1344
|
*/
|
|
1302
1345
|
declare function getPlayerDisplayName(): string | null;
|
|
1303
1346
|
|
|
1304
|
-
|
|
1347
|
+
interface SceneBounds {
|
|
1348
|
+
minX: number;
|
|
1349
|
+
maxX: number;
|
|
1350
|
+
minZ: number;
|
|
1351
|
+
maxZ: number;
|
|
1352
|
+
width: number;
|
|
1353
|
+
depth: number;
|
|
1354
|
+
parcels: string[];
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* Get scene bounds from DCL Runtime
|
|
1358
|
+
* Auto-detects parcels and calculates local coordinate bounds
|
|
1359
|
+
*/
|
|
1360
|
+
declare function getSceneBounds(): Promise<SceneBounds | null>;
|
|
1361
|
+
/**
|
|
1362
|
+
* Check if player is within scene boundaries
|
|
1363
|
+
* Returns true if in bounds, false if out of bounds or can't determine
|
|
1364
|
+
*/
|
|
1365
|
+
declare function isPlayerInBounds(bounds: SceneBounds): boolean;
|
|
1366
|
+
/**
|
|
1367
|
+
* Get player's current position
|
|
1368
|
+
*/
|
|
1369
|
+
declare function getPlayerPosition(): {
|
|
1370
|
+
x: number;
|
|
1371
|
+
y: number;
|
|
1372
|
+
z: number;
|
|
1373
|
+
} | null;
|
|
1374
|
+
|
|
1375
|
+
export { type AdminPanelConfig, AdminPanelUIModule, type Channel, type ChatChannel, type ChatMessage, type ChatUIConfig, ChatUIModule, type CommandHandler, type GuideFeaturedPlaylist, GuideModule, type GuideResponse, type GuideUIConfig, GuideUIModule, type GuideVideo, HeartbeatModule, type HeartbeatResponse, type InteractionResponse, InteractionsModule, KEY_TYPE_CHANNEL, KEY_TYPE_SCENE, NotificationBanner, type PlayerData, type SDKTier, type SceneBounds$1 as SceneBounds, type SceneStats, type SceneStatsResponse, type SceneTabDefinition, SessionModule, type SessionResponse, StaticTVClient, type StaticTVConfig, type StreamData, type VideoSlot, type Vod, fetchUserData, getPlayerDisplayName, getPlayerPosition, getPlayerWallet, getSceneBounds, hideNotification, isPlayerInBounds, setupStaticUI, showNotification };
|
package/dist/index.js
CHANGED
|
@@ -43,8 +43,11 @@ __export(index_exports, {
|
|
|
43
43
|
StaticTVClient: () => StaticTVClient,
|
|
44
44
|
fetchUserData: () => fetchUserData,
|
|
45
45
|
getPlayerDisplayName: () => getPlayerDisplayName,
|
|
46
|
+
getPlayerPosition: () => getPlayerPosition,
|
|
46
47
|
getPlayerWallet: () => getPlayerWallet,
|
|
48
|
+
getSceneBounds: () => getSceneBounds,
|
|
47
49
|
hideNotification: () => hideNotification,
|
|
50
|
+
isPlayerInBounds: () => isPlayerInBounds,
|
|
48
51
|
setupStaticUI: () => setupStaticUI,
|
|
49
52
|
showNotification: () => showNotification
|
|
50
53
|
});
|
|
@@ -248,6 +251,93 @@ function dclClearTimeout(timeoutId) {
|
|
|
248
251
|
}
|
|
249
252
|
}
|
|
250
253
|
|
|
254
|
+
// src/utils/bounds.ts
|
|
255
|
+
var import_ecs2 = require("@dcl/sdk/ecs");
|
|
256
|
+
var import_Runtime = require("~system/Runtime");
|
|
257
|
+
var PARCEL_SIZE = 16;
|
|
258
|
+
var cachedBounds = null;
|
|
259
|
+
var boundsInitialized = false;
|
|
260
|
+
var boundsError = false;
|
|
261
|
+
function parseParcel(parcel) {
|
|
262
|
+
const parts = parcel.split(",");
|
|
263
|
+
if (parts.length !== 2) return null;
|
|
264
|
+
const x = parseInt(parts[0], 10);
|
|
265
|
+
const z = parseInt(parts[1], 10);
|
|
266
|
+
if (isNaN(x) || isNaN(z)) return null;
|
|
267
|
+
return { x, z };
|
|
268
|
+
}
|
|
269
|
+
async function getSceneBounds() {
|
|
270
|
+
if (boundsInitialized && cachedBounds) {
|
|
271
|
+
return cachedBounds;
|
|
272
|
+
}
|
|
273
|
+
if (boundsError) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
const sceneInfo = await (0, import_Runtime.getSceneInformation)({});
|
|
278
|
+
let parcels = [];
|
|
279
|
+
let baseParcel;
|
|
280
|
+
if (sceneInfo.metadataJson) {
|
|
281
|
+
const metadata = JSON.parse(sceneInfo.metadataJson);
|
|
282
|
+
parcels = metadata?.scene?.parcels || [];
|
|
283
|
+
baseParcel = metadata?.scene?.base;
|
|
284
|
+
}
|
|
285
|
+
if (parcels.length === 0) {
|
|
286
|
+
boundsError = true;
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
const coords = parcels.map(parseParcel).filter((c) => c !== null);
|
|
290
|
+
if (coords.length === 0) {
|
|
291
|
+
boundsError = true;
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
const worldMinX = Math.min(...coords.map((c) => c.x));
|
|
295
|
+
const worldMaxX = Math.max(...coords.map((c) => c.x));
|
|
296
|
+
const worldMinZ = Math.min(...coords.map((c) => c.z));
|
|
297
|
+
const worldMaxZ = Math.max(...coords.map((c) => c.z));
|
|
298
|
+
const width = (worldMaxX - worldMinX + 1) * PARCEL_SIZE;
|
|
299
|
+
const depth = (worldMaxZ - worldMinZ + 1) * PARCEL_SIZE;
|
|
300
|
+
cachedBounds = {
|
|
301
|
+
minX: 0,
|
|
302
|
+
maxX: width,
|
|
303
|
+
minZ: 0,
|
|
304
|
+
maxZ: depth,
|
|
305
|
+
width,
|
|
306
|
+
depth,
|
|
307
|
+
parcels
|
|
308
|
+
};
|
|
309
|
+
boundsInitialized = true;
|
|
310
|
+
return cachedBounds;
|
|
311
|
+
} catch (error) {
|
|
312
|
+
boundsError = true;
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function isPlayerInBounds(bounds) {
|
|
317
|
+
try {
|
|
318
|
+
const transform = import_ecs2.Transform.getOrNull(import_ecs2.engine.PlayerEntity);
|
|
319
|
+
if (!transform) return false;
|
|
320
|
+
const pos = transform.position;
|
|
321
|
+
const buffer = 0.5;
|
|
322
|
+
return pos.x >= bounds.minX - buffer && pos.x <= bounds.maxX + buffer && pos.z >= bounds.minZ - buffer && pos.z <= bounds.maxZ + buffer;
|
|
323
|
+
} catch {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function getPlayerPosition() {
|
|
328
|
+
try {
|
|
329
|
+
const transform = import_ecs2.Transform.getOrNull(import_ecs2.engine.PlayerEntity);
|
|
330
|
+
if (!transform) return null;
|
|
331
|
+
return {
|
|
332
|
+
x: transform.position.x,
|
|
333
|
+
y: transform.position.y,
|
|
334
|
+
z: transform.position.z
|
|
335
|
+
};
|
|
336
|
+
} catch {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
251
341
|
// src/modules/session.ts
|
|
252
342
|
function normalizeTier(tier) {
|
|
253
343
|
if (tier === "lite") return "free";
|
|
@@ -262,8 +352,26 @@ var SessionModule = class {
|
|
|
262
352
|
this.heartbeatTimerId = null;
|
|
263
353
|
this.isActive = false;
|
|
264
354
|
this._tier = "free";
|
|
355
|
+
// Boundary checking - auto-detected from scene parcels
|
|
356
|
+
this._bounds = null;
|
|
357
|
+
this._boundsInitialized = false;
|
|
358
|
+
this._outOfBoundsCount = 0;
|
|
265
359
|
this.client = client;
|
|
266
360
|
}
|
|
361
|
+
/**
|
|
362
|
+
* Get the scene bounds (auto-detected from parcels)
|
|
363
|
+
* Returns null if bounds haven't been initialized yet
|
|
364
|
+
*/
|
|
365
|
+
get bounds() {
|
|
366
|
+
return this._bounds;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Check if player is currently within scene bounds
|
|
370
|
+
*/
|
|
371
|
+
isPlayerInScene() {
|
|
372
|
+
if (!this._bounds) return true;
|
|
373
|
+
return isPlayerInBounds(this._bounds);
|
|
374
|
+
}
|
|
267
375
|
/**
|
|
268
376
|
* Get the API key ID (used as default sceneId for Pro users)
|
|
269
377
|
*/
|
|
@@ -306,6 +414,25 @@ var SessionModule = class {
|
|
|
306
414
|
const config = this.client.getConfig();
|
|
307
415
|
return config.player?.name || getPlayerDisplayName();
|
|
308
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* Initialize scene bounds from DCL Runtime
|
|
419
|
+
* Called once on session start
|
|
420
|
+
*/
|
|
421
|
+
async initializeBounds() {
|
|
422
|
+
if (this._boundsInitialized) return;
|
|
423
|
+
try {
|
|
424
|
+
this._bounds = await getSceneBounds();
|
|
425
|
+
this._boundsInitialized = true;
|
|
426
|
+
if (this._bounds) {
|
|
427
|
+
this.client.log(`Scene bounds detected: ${this._bounds.width}x${this._bounds.depth}m (${this._bounds.parcels.length} parcels)`);
|
|
428
|
+
} else {
|
|
429
|
+
this.client.log("Could not detect scene bounds - boundary checking disabled");
|
|
430
|
+
}
|
|
431
|
+
} catch (error) {
|
|
432
|
+
this.client.log(`Failed to detect scene bounds: ${error}`);
|
|
433
|
+
this._boundsInitialized = true;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
309
436
|
/**
|
|
310
437
|
* Start a new session
|
|
311
438
|
* Called automatically if autoStartSession is true
|
|
@@ -315,6 +442,7 @@ var SessionModule = class {
|
|
|
315
442
|
this.client.log("Session already active");
|
|
316
443
|
return this.sessionId;
|
|
317
444
|
}
|
|
445
|
+
await this.initializeBounds();
|
|
318
446
|
try {
|
|
319
447
|
const walletAddress = this.getWallet();
|
|
320
448
|
const dclDisplayName = this.getDisplayName();
|
|
@@ -356,18 +484,33 @@ var SessionModule = class {
|
|
|
356
484
|
}
|
|
357
485
|
/**
|
|
358
486
|
* Send a session heartbeat
|
|
487
|
+
* Always sends heartbeat with inScene status so dashboard can track location
|
|
359
488
|
*/
|
|
360
489
|
async sendHeartbeat() {
|
|
361
490
|
if (!this.sessionId || !this.isActive) return;
|
|
491
|
+
const inScene = this.isPlayerInScene();
|
|
492
|
+
const pos = getPlayerPosition();
|
|
493
|
+
if (!inScene && this._outOfBoundsCount === 0) {
|
|
494
|
+
this.client.log(`Player left scene bounds (${pos?.x.toFixed(1)}, ${pos?.z.toFixed(1)})`);
|
|
495
|
+
} else if (inScene && this._outOfBoundsCount > 0) {
|
|
496
|
+
this.client.log(`Player returned to scene after ${this._outOfBoundsCount} heartbeats outside`);
|
|
497
|
+
}
|
|
498
|
+
if (!inScene) {
|
|
499
|
+
this._outOfBoundsCount++;
|
|
500
|
+
} else {
|
|
501
|
+
this._outOfBoundsCount = 0;
|
|
502
|
+
}
|
|
362
503
|
try {
|
|
363
504
|
await this.client.request(this.getEndpoint(), {
|
|
364
505
|
method: "POST",
|
|
365
506
|
body: JSON.stringify({
|
|
366
507
|
action: "heartbeat",
|
|
367
|
-
sessionId: this.sessionId
|
|
508
|
+
sessionId: this.sessionId,
|
|
509
|
+
inScene,
|
|
510
|
+
position: pos ? { x: Math.round(pos.x), z: Math.round(pos.z) } : null
|
|
368
511
|
})
|
|
369
512
|
});
|
|
370
|
-
this.client.log(
|
|
513
|
+
this.client.log(`Session heartbeat sent (inScene: ${inScene})`);
|
|
371
514
|
} catch (error) {
|
|
372
515
|
this.client.log(`Session heartbeat failed: ${error}`);
|
|
373
516
|
}
|
|
@@ -487,9 +630,14 @@ var HeartbeatModule = class {
|
|
|
487
630
|
}
|
|
488
631
|
/**
|
|
489
632
|
* Send a watching heartbeat (1 heartbeat = 1 minute watched)
|
|
633
|
+
* Only sends if player is within scene bounds
|
|
490
634
|
*/
|
|
491
635
|
async sendHeartbeat() {
|
|
492
636
|
if (!this.currentChannel || !this.isWatching) return;
|
|
637
|
+
if (this.client.session && !this.client.session.isPlayerInScene()) {
|
|
638
|
+
this.client.log(`Watch heartbeat skipped - player out of scene bounds`);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
493
641
|
try {
|
|
494
642
|
const sessionId = this.client.session?.getSessionId();
|
|
495
643
|
await this.client.request("/heartbeat", {
|
|
@@ -2315,7 +2463,7 @@ var import_RestrictedActions3 = require("~system/RestrictedActions");
|
|
|
2315
2463
|
|
|
2316
2464
|
// src/ui/ui-renderer.tsx
|
|
2317
2465
|
var import_react_ecs4 = __toESM(require("@dcl/sdk/react-ecs"));
|
|
2318
|
-
var
|
|
2466
|
+
var import_ecs3 = require("@dcl/sdk/ecs");
|
|
2319
2467
|
var notificationText = "";
|
|
2320
2468
|
var notificationVisible = false;
|
|
2321
2469
|
var notificationEndTime = 0;
|
|
@@ -2336,7 +2484,7 @@ function getNotificationState() {
|
|
|
2336
2484
|
function initNotificationSystem() {
|
|
2337
2485
|
if (notificationInitialized) return;
|
|
2338
2486
|
notificationInitialized = true;
|
|
2339
|
-
|
|
2487
|
+
import_ecs3.engine.addSystem(() => {
|
|
2340
2488
|
if (notificationVisible && Date.now() > notificationEndTime) {
|
|
2341
2489
|
notificationVisible = false;
|
|
2342
2490
|
}
|
|
@@ -3722,7 +3870,7 @@ var AdminPanelUIModule = class {
|
|
|
3722
3870
|
};
|
|
3723
3871
|
|
|
3724
3872
|
// src/StaticTVClient.ts
|
|
3725
|
-
var
|
|
3873
|
+
var import_ecs4 = require("@dcl/sdk/ecs");
|
|
3726
3874
|
var utils = __toESM(require("@dcl-sdk/utils"));
|
|
3727
3875
|
var DEFAULT_BASE_URL = "https://thestatic.tv/api/v1/dcl";
|
|
3728
3876
|
var DEFAULT_FALLBACK_VIDEO = "https://media.thestatic.tv/fallback-loop.mp4";
|
|
@@ -4011,9 +4159,9 @@ var StaticTVClient = class {
|
|
|
4011
4159
|
const fallbackUrl = this.config.fallbackVideoUrl;
|
|
4012
4160
|
const fallbackDisabled = fallbackUrl === "";
|
|
4013
4161
|
if (fallbackDisabled) {
|
|
4014
|
-
if (
|
|
4162
|
+
if (import_ecs4.VideoPlayer.has(screen)) {
|
|
4015
4163
|
this.log("Stopping video (no fallback)");
|
|
4016
|
-
|
|
4164
|
+
import_ecs4.VideoPlayer.getMutable(screen).playing = false;
|
|
4017
4165
|
}
|
|
4018
4166
|
this._currentVideoUrl = "";
|
|
4019
4167
|
} else {
|
|
@@ -4036,14 +4184,14 @@ var StaticTVClient = class {
|
|
|
4036
4184
|
if (screen === void 0) return;
|
|
4037
4185
|
this._clearVerificationTimeout();
|
|
4038
4186
|
this._streamVerified = false;
|
|
4039
|
-
if (
|
|
4040
|
-
const player =
|
|
4187
|
+
if (import_ecs4.VideoPlayer.has(screen)) {
|
|
4188
|
+
const player = import_ecs4.VideoPlayer.getMutable(screen);
|
|
4041
4189
|
player.src = url;
|
|
4042
4190
|
player.playing = true;
|
|
4043
4191
|
player.loop = isFallback;
|
|
4044
4192
|
player.volume = 1;
|
|
4045
4193
|
} else {
|
|
4046
|
-
|
|
4194
|
+
import_ecs4.VideoPlayer.create(screen, {
|
|
4047
4195
|
src: url,
|
|
4048
4196
|
playing: true,
|
|
4049
4197
|
loop: isFallback,
|
|
@@ -4065,8 +4213,8 @@ var StaticTVClient = class {
|
|
|
4065
4213
|
const screen = this.config.videoScreen;
|
|
4066
4214
|
if (screen === void 0) return;
|
|
4067
4215
|
this._videoEventsRegistered = true;
|
|
4068
|
-
|
|
4069
|
-
if (videoEvent.state ===
|
|
4216
|
+
import_ecs4.videoEventsSystem.registerVideoEventsEntity(screen, (videoEvent) => {
|
|
4217
|
+
if (videoEvent.state === import_ecs4.VideoState.VS_PLAYING) {
|
|
4070
4218
|
const timeSinceStart = Date.now() - this._verificationStartTime;
|
|
4071
4219
|
const MIN_VERIFICATION_DELAY = 2e3;
|
|
4072
4220
|
if (this._pendingVideoData && !this._streamVerified && timeSinceStart >= MIN_VERIFICATION_DELAY) {
|
|
@@ -4075,7 +4223,7 @@ var StaticTVClient = class {
|
|
|
4075
4223
|
this.log(`Stream verified: ${this._pendingVideoData.name} (after ${timeSinceStart}ms)`);
|
|
4076
4224
|
}
|
|
4077
4225
|
}
|
|
4078
|
-
if (videoEvent.state ===
|
|
4226
|
+
if (videoEvent.state === import_ecs4.VideoState.VS_ERROR) {
|
|
4079
4227
|
if (this._pendingVideoData) {
|
|
4080
4228
|
this.log(`Stream error for: ${this._pendingVideoData.name}`);
|
|
4081
4229
|
this._handleStreamOffline();
|
|
@@ -4496,8 +4644,11 @@ var StaticTVClient = class {
|
|
|
4496
4644
|
StaticTVClient,
|
|
4497
4645
|
fetchUserData,
|
|
4498
4646
|
getPlayerDisplayName,
|
|
4647
|
+
getPlayerPosition,
|
|
4499
4648
|
getPlayerWallet,
|
|
4649
|
+
getSceneBounds,
|
|
4500
4650
|
hideNotification,
|
|
4651
|
+
isPlayerInBounds,
|
|
4501
4652
|
setupStaticUI,
|
|
4502
4653
|
showNotification
|
|
4503
4654
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -203,6 +203,93 @@ function dclClearTimeout(timeoutId) {
|
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
// src/utils/bounds.ts
|
|
207
|
+
import { engine as engine2, Transform } from "@dcl/sdk/ecs";
|
|
208
|
+
import { getSceneInformation } from "~system/Runtime";
|
|
209
|
+
var PARCEL_SIZE = 16;
|
|
210
|
+
var cachedBounds = null;
|
|
211
|
+
var boundsInitialized = false;
|
|
212
|
+
var boundsError = false;
|
|
213
|
+
function parseParcel(parcel) {
|
|
214
|
+
const parts = parcel.split(",");
|
|
215
|
+
if (parts.length !== 2) return null;
|
|
216
|
+
const x = parseInt(parts[0], 10);
|
|
217
|
+
const z = parseInt(parts[1], 10);
|
|
218
|
+
if (isNaN(x) || isNaN(z)) return null;
|
|
219
|
+
return { x, z };
|
|
220
|
+
}
|
|
221
|
+
async function getSceneBounds() {
|
|
222
|
+
if (boundsInitialized && cachedBounds) {
|
|
223
|
+
return cachedBounds;
|
|
224
|
+
}
|
|
225
|
+
if (boundsError) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const sceneInfo = await getSceneInformation({});
|
|
230
|
+
let parcels = [];
|
|
231
|
+
let baseParcel;
|
|
232
|
+
if (sceneInfo.metadataJson) {
|
|
233
|
+
const metadata = JSON.parse(sceneInfo.metadataJson);
|
|
234
|
+
parcels = metadata?.scene?.parcels || [];
|
|
235
|
+
baseParcel = metadata?.scene?.base;
|
|
236
|
+
}
|
|
237
|
+
if (parcels.length === 0) {
|
|
238
|
+
boundsError = true;
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
const coords = parcels.map(parseParcel).filter((c) => c !== null);
|
|
242
|
+
if (coords.length === 0) {
|
|
243
|
+
boundsError = true;
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const worldMinX = Math.min(...coords.map((c) => c.x));
|
|
247
|
+
const worldMaxX = Math.max(...coords.map((c) => c.x));
|
|
248
|
+
const worldMinZ = Math.min(...coords.map((c) => c.z));
|
|
249
|
+
const worldMaxZ = Math.max(...coords.map((c) => c.z));
|
|
250
|
+
const width = (worldMaxX - worldMinX + 1) * PARCEL_SIZE;
|
|
251
|
+
const depth = (worldMaxZ - worldMinZ + 1) * PARCEL_SIZE;
|
|
252
|
+
cachedBounds = {
|
|
253
|
+
minX: 0,
|
|
254
|
+
maxX: width,
|
|
255
|
+
minZ: 0,
|
|
256
|
+
maxZ: depth,
|
|
257
|
+
width,
|
|
258
|
+
depth,
|
|
259
|
+
parcels
|
|
260
|
+
};
|
|
261
|
+
boundsInitialized = true;
|
|
262
|
+
return cachedBounds;
|
|
263
|
+
} catch (error) {
|
|
264
|
+
boundsError = true;
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function isPlayerInBounds(bounds) {
|
|
269
|
+
try {
|
|
270
|
+
const transform = Transform.getOrNull(engine2.PlayerEntity);
|
|
271
|
+
if (!transform) return false;
|
|
272
|
+
const pos = transform.position;
|
|
273
|
+
const buffer = 0.5;
|
|
274
|
+
return pos.x >= bounds.minX - buffer && pos.x <= bounds.maxX + buffer && pos.z >= bounds.minZ - buffer && pos.z <= bounds.maxZ + buffer;
|
|
275
|
+
} catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function getPlayerPosition() {
|
|
280
|
+
try {
|
|
281
|
+
const transform = Transform.getOrNull(engine2.PlayerEntity);
|
|
282
|
+
if (!transform) return null;
|
|
283
|
+
return {
|
|
284
|
+
x: transform.position.x,
|
|
285
|
+
y: transform.position.y,
|
|
286
|
+
z: transform.position.z
|
|
287
|
+
};
|
|
288
|
+
} catch {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
206
293
|
// src/modules/session.ts
|
|
207
294
|
function normalizeTier(tier) {
|
|
208
295
|
if (tier === "lite") return "free";
|
|
@@ -217,8 +304,26 @@ var SessionModule = class {
|
|
|
217
304
|
this.heartbeatTimerId = null;
|
|
218
305
|
this.isActive = false;
|
|
219
306
|
this._tier = "free";
|
|
307
|
+
// Boundary checking - auto-detected from scene parcels
|
|
308
|
+
this._bounds = null;
|
|
309
|
+
this._boundsInitialized = false;
|
|
310
|
+
this._outOfBoundsCount = 0;
|
|
220
311
|
this.client = client;
|
|
221
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Get the scene bounds (auto-detected from parcels)
|
|
315
|
+
* Returns null if bounds haven't been initialized yet
|
|
316
|
+
*/
|
|
317
|
+
get bounds() {
|
|
318
|
+
return this._bounds;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Check if player is currently within scene bounds
|
|
322
|
+
*/
|
|
323
|
+
isPlayerInScene() {
|
|
324
|
+
if (!this._bounds) return true;
|
|
325
|
+
return isPlayerInBounds(this._bounds);
|
|
326
|
+
}
|
|
222
327
|
/**
|
|
223
328
|
* Get the API key ID (used as default sceneId for Pro users)
|
|
224
329
|
*/
|
|
@@ -261,6 +366,25 @@ var SessionModule = class {
|
|
|
261
366
|
const config = this.client.getConfig();
|
|
262
367
|
return config.player?.name || getPlayerDisplayName();
|
|
263
368
|
}
|
|
369
|
+
/**
|
|
370
|
+
* Initialize scene bounds from DCL Runtime
|
|
371
|
+
* Called once on session start
|
|
372
|
+
*/
|
|
373
|
+
async initializeBounds() {
|
|
374
|
+
if (this._boundsInitialized) return;
|
|
375
|
+
try {
|
|
376
|
+
this._bounds = await getSceneBounds();
|
|
377
|
+
this._boundsInitialized = true;
|
|
378
|
+
if (this._bounds) {
|
|
379
|
+
this.client.log(`Scene bounds detected: ${this._bounds.width}x${this._bounds.depth}m (${this._bounds.parcels.length} parcels)`);
|
|
380
|
+
} else {
|
|
381
|
+
this.client.log("Could not detect scene bounds - boundary checking disabled");
|
|
382
|
+
}
|
|
383
|
+
} catch (error) {
|
|
384
|
+
this.client.log(`Failed to detect scene bounds: ${error}`);
|
|
385
|
+
this._boundsInitialized = true;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
264
388
|
/**
|
|
265
389
|
* Start a new session
|
|
266
390
|
* Called automatically if autoStartSession is true
|
|
@@ -270,6 +394,7 @@ var SessionModule = class {
|
|
|
270
394
|
this.client.log("Session already active");
|
|
271
395
|
return this.sessionId;
|
|
272
396
|
}
|
|
397
|
+
await this.initializeBounds();
|
|
273
398
|
try {
|
|
274
399
|
const walletAddress = this.getWallet();
|
|
275
400
|
const dclDisplayName = this.getDisplayName();
|
|
@@ -311,18 +436,33 @@ var SessionModule = class {
|
|
|
311
436
|
}
|
|
312
437
|
/**
|
|
313
438
|
* Send a session heartbeat
|
|
439
|
+
* Always sends heartbeat with inScene status so dashboard can track location
|
|
314
440
|
*/
|
|
315
441
|
async sendHeartbeat() {
|
|
316
442
|
if (!this.sessionId || !this.isActive) return;
|
|
443
|
+
const inScene = this.isPlayerInScene();
|
|
444
|
+
const pos = getPlayerPosition();
|
|
445
|
+
if (!inScene && this._outOfBoundsCount === 0) {
|
|
446
|
+
this.client.log(`Player left scene bounds (${pos?.x.toFixed(1)}, ${pos?.z.toFixed(1)})`);
|
|
447
|
+
} else if (inScene && this._outOfBoundsCount > 0) {
|
|
448
|
+
this.client.log(`Player returned to scene after ${this._outOfBoundsCount} heartbeats outside`);
|
|
449
|
+
}
|
|
450
|
+
if (!inScene) {
|
|
451
|
+
this._outOfBoundsCount++;
|
|
452
|
+
} else {
|
|
453
|
+
this._outOfBoundsCount = 0;
|
|
454
|
+
}
|
|
317
455
|
try {
|
|
318
456
|
await this.client.request(this.getEndpoint(), {
|
|
319
457
|
method: "POST",
|
|
320
458
|
body: JSON.stringify({
|
|
321
459
|
action: "heartbeat",
|
|
322
|
-
sessionId: this.sessionId
|
|
460
|
+
sessionId: this.sessionId,
|
|
461
|
+
inScene,
|
|
462
|
+
position: pos ? { x: Math.round(pos.x), z: Math.round(pos.z) } : null
|
|
323
463
|
})
|
|
324
464
|
});
|
|
325
|
-
this.client.log(
|
|
465
|
+
this.client.log(`Session heartbeat sent (inScene: ${inScene})`);
|
|
326
466
|
} catch (error) {
|
|
327
467
|
this.client.log(`Session heartbeat failed: ${error}`);
|
|
328
468
|
}
|
|
@@ -442,9 +582,14 @@ var HeartbeatModule = class {
|
|
|
442
582
|
}
|
|
443
583
|
/**
|
|
444
584
|
* Send a watching heartbeat (1 heartbeat = 1 minute watched)
|
|
585
|
+
* Only sends if player is within scene bounds
|
|
445
586
|
*/
|
|
446
587
|
async sendHeartbeat() {
|
|
447
588
|
if (!this.currentChannel || !this.isWatching) return;
|
|
589
|
+
if (this.client.session && !this.client.session.isPlayerInScene()) {
|
|
590
|
+
this.client.log(`Watch heartbeat skipped - player out of scene bounds`);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
448
593
|
try {
|
|
449
594
|
const sessionId = this.client.session?.getSessionId();
|
|
450
595
|
await this.client.request("/heartbeat", {
|
|
@@ -2270,7 +2415,7 @@ import { movePlayerTo, openExternalUrl as openExternalUrl3 } from "~system/Restr
|
|
|
2270
2415
|
|
|
2271
2416
|
// src/ui/ui-renderer.tsx
|
|
2272
2417
|
import ReactEcs4, { ReactEcsRenderer, UiEntity as UiEntity4, Label as Label4 } from "@dcl/sdk/react-ecs";
|
|
2273
|
-
import { engine as
|
|
2418
|
+
import { engine as engine3 } from "@dcl/sdk/ecs";
|
|
2274
2419
|
var notificationText = "";
|
|
2275
2420
|
var notificationVisible = false;
|
|
2276
2421
|
var notificationEndTime = 0;
|
|
@@ -2291,7 +2436,7 @@ function getNotificationState() {
|
|
|
2291
2436
|
function initNotificationSystem() {
|
|
2292
2437
|
if (notificationInitialized) return;
|
|
2293
2438
|
notificationInitialized = true;
|
|
2294
|
-
|
|
2439
|
+
engine3.addSystem(() => {
|
|
2295
2440
|
if (notificationVisible && Date.now() > notificationEndTime) {
|
|
2296
2441
|
notificationVisible = false;
|
|
2297
2442
|
}
|
|
@@ -4450,8 +4595,11 @@ export {
|
|
|
4450
4595
|
StaticTVClient,
|
|
4451
4596
|
fetchUserData,
|
|
4452
4597
|
getPlayerDisplayName,
|
|
4598
|
+
getPlayerPosition,
|
|
4453
4599
|
getPlayerWallet,
|
|
4600
|
+
getSceneBounds,
|
|
4454
4601
|
hideNotification,
|
|
4602
|
+
isPlayerInBounds,
|
|
4455
4603
|
setupStaticUI,
|
|
4456
4604
|
showNotification
|
|
4457
4605
|
};
|
package/package.json
CHANGED