Skip to content

Commit

Permalink
init commit
Browse files Browse the repository at this point in the history
  • Loading branch information
gonpombo8 committed Aug 1, 2024
0 parents commit 54527f0
Show file tree
Hide file tree
Showing 8 changed files with 360 additions and 0 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
on:
push:
branches:
- main
pull_request:
release:
types:
- created

name: CI
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Use Node.js 20.x
uses: actions/setup-node@v2
with:
node-version: 20
- name: install
run: npm install
- name: build
run: npm run build
- name: Publish
uses: menduz/oddish-action@master
with:
registry-url: "https://registry.npmjs.org"
access: public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package-lock.json
*.js
node_modules
bin/
.DS_Store
**/.DS_Store
npm-debug.log*
yarn-debug.log*
yarn-error.log*
3 changes: 3 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"recommendations": ["decentralandfoundation.decentraland-sdk7"]
}
17 changes: 17 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
// Use the Decentraland Editor extension of VSCode to debug the scene
// in chrome from VSCode
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Debug Decentraland in Chrome",
"url": "${command:decentraland-sdk7.commands.getDebugURL}",
"webRoot": "${workspaceFolder}/bin",
"sourceMapPathOverrides": {
"dcl:///*": "${workspaceFolder}/*"
}
}
]
}
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# SDK7 Template Library

## Conecepts

### Engine Param
You need to pass the engine so you reference always the same engine in the lib & in the scene.

### Components
In order to use an ecs component, you need to import it and pass the engine, so both the library and the scene are talking about the same component instance.
```ts
import * as components from '@dcl/ecs/dist/components'
const Transform = components.Transform(engine)
Transform.getOrNull(entity)
```

### Publish
Set your NPM_TOKEN on github secrets and the lib will automatically be deployed to npm registry.
Be sure to set the package.json#name property with your library name.
See .github/workflows/ci.yml file.


### Development
`npm run dev` to start the typescript compiler.
`npm run start` to start the library as a scene.
25 changes: 25 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@dcl-sdk/players-queue",
"version": "1.0.0",
"description": "SDK7 Library to handle Players Queue logic for Multiplayers Game",
"scripts": {
"start": "sdk-commands start",
"dev": "tsc -p tsconfig.json --watch",
"build": "sdk-commands build",
"upgrade-sdk": "npm install --save-dev @dcl/sdk@latest",
"upgrade-sdk:next": "npm install --save-dev @dcl/sdk@next"
},
"devDependencies": {
"@dcl/js-runtime": "latest",
"@dcl/sdk": "latest"
},
"prettier": {
"semi": false,
"singleQuote": true,
"printWidth": 120,
"trailingComma": "none"
},
"dependencies": {
"typescript": "^5.5.4"
}
}
241 changes: 241 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import { Entity, ISchema, MapComponentDefinition, MapResult, PlayerIdentityData, Schemas } from '@dcl/sdk/ecs'
import { type IEngine } from '@dcl/ecs'
import players from '@dcl/sdk/players'
import type { syncEntity as SyncEntityType } from '@dcl/sdk/network'

export type PlayerType = {
address: string;
joinedAt: number;
startPlayingAt: number
active: boolean
}

/**
* SDK methods that the library receives on the initLibrary
*/
let engine: IEngine
export let Player: MapComponentDefinition<MapResult<{
address: ISchema<string>
joinedAt: ISchema<number>
startPlayingAt: ISchema<number>
active: ISchema<boolean>
}>>
let syncEntityApi: typeof SyncEntityType
let playersApi: typeof players

/**
* Internal queue that checks if the user has left the scene for more than $TIMER seconds
*/
const queueLeaveScene: Map<string, number> = new Map()

/**
* Return listeners so they can be override with callbacks
* const listeners = initLibrary()
* listeners.onActivePlayerChange = (player) => player.address
*/
export const listeners: { onActivePlayerChange: (player: PlayerType) => void } = {
onActivePlayerChange: () => {}
}
/**
* We need the engine as a param to avoid references to different engines
* when working on development environments.
*/
export function initLibrary(_engine: IEngine, _syncEntity: typeof SyncEntityType, _playersApi: typeof players) {
engine = _engine
Player = engine.defineComponent('sdk-utils/player:player', {
address: Schemas.String,
joinedAt: Schemas.Int64,
active: Schemas.Boolean,
startPlayingAt: Schemas.Int64
})
syncEntityApi = _syncEntity
playersApi = _playersApi

playersApi.onLeaveScene((userId: string) => {
queueLeaveScene.set(userId, Date.now())
})

engine.addSystem(internalPlayerSystem())
return listeners
}
/**
* Set current player as inactive, and grab the first of the queue
*/
export function setNextPlayer() {
_setNextPlayer(false)
}
/**
* Add current player to the queue
*/
export function addPlayer() {
const userId = getUserId()
if (!userId || isPlayerInQueue(userId)) {
return
}
const timestamp = Date.now()
const entity = engine.addEntity()
Player.create(entity, { address: userId, joinedAt: timestamp })
syncEntityApi(entity, [Player.componentId])
}

/**
* Check's if the current user is active.
*/
export function isActive(): boolean {
const [entity, player] = getActivePlayer()
if (player) {
return player.address === getUserId()
}
return false
}

/**
* Get queue of players ordered
*/
export function getQueue() {
const queue = new Map<string, { player: PlayerType, entity: Entity }>()

for (const [entity, player] of engine.getEntitiesWith(Player)) {
if (!queue.has(player.address)) {
queue.set(player.address, { player, entity })
continue
}
queue.set(player.address, { player, entity })
}

return [...queue.values()].sort((a, b) => a.player.joinedAt < b.player.joinedAt ? -1 : 1)
}


/**
* ======== INTERNAL HELPERS ========
*/

/**
* Cache the client userId
*/
let userId: string | undefined
function getUserId() {
if (userId) return userId
return userId = playersApi.getPlayer()?.userId
}

function _setNextPlayer(force?: boolean) {
const [_, activePlayer] = getActivePlayer()

if (!force && activePlayer?.address !== getUserId()) {
return
}

for (const [_, player] of engine.getEntitiesWith(Player)) {
if (player.active) {
removePlayer(player.address)
}
}
const nextPlayer = getQueue()[0]
if (nextPlayer && nextPlayer.player.address === getUserId()) {
lastActivePlayer = nextPlayer.player.address
Player.getMutable(nextPlayer.entity).active = true
Player.getMutable(nextPlayer.entity).startPlayingAt = Date.now()
if (listeners.onActivePlayerChange) {
listeners.onActivePlayerChange(Player.get(nextPlayer.entity))
}
}
}


/**
* Run a system every 4s that checks if a user has been disconnected
* from the scene and removes it from the Queue.
*/
let lastActivePlayer: string
function internalPlayerSystem() {
let timer = 0
return function(dt: number) {
timer += dt
if (timer < 1) {
return
}
timer = 0
// Listen to disconnected players
const TIMER = 2000
for (const [userId, leaveSceneAt] of queueLeaveScene) {
if (Date.now() - leaveSceneAt >= TIMER) {
if (!isPlayerConnected(userId)) {
removePlayer(userId)
}
queueLeaveScene.delete(userId)
}
}

const [_, activePlayer] = getActivePlayer()

// Emit onActivePlayerChange if the last player has changed
if (activePlayer && activePlayer.address !== lastActivePlayer) {
lastActivePlayer = activePlayer.address
listeners.onActivePlayerChange(activePlayer)
}

// Listen to changes in the queue and if there is no active player set it.
if (!activePlayer) {
const nextPlayer = getQueue()[0]
if (nextPlayer && nextPlayer.player.address === getUserId()) {
_setNextPlayer(true)
}
}
}
}


/**
* Check if player is still connected to the scene
*/
function isPlayerConnected(userId: string) {
for (const [_, player] of engine.getEntitiesWith(PlayerIdentityData)) {
if (player.address === userId) {
return true
}
}
return false
}

/**
* Check if the player is already in the Queue
*/
function isPlayerInQueue(userId: string) {
for (const [_, player] of engine.getEntitiesWith(Player)) {
if (player.address === userId) {
return true
}
}
return false
}

/**
* Remove Player from queue
*/
function removePlayer(_userId?: string) {
const userId = _userId ?? getUserId()

if (!userId) {
return
}

for (const [entity, player] of engine.getEntitiesWith(Player)) {
if (player.address === userId) {
engine.removeEntity(entity)
}
}
}

/**
* Get active player
*/
function getActivePlayer(): [Entity, PlayerType] | [] {
for (const [entity, player] of engine.getEntitiesWith(Player)) {
if (player.active) {
return [entity, player]
}
}
return []
}
11 changes: 11 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"allowJs": true,
"strict": true
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"extends": "@dcl/sdk/types/tsconfig.ecs7.json"
}

0 comments on commit 54527f0

Please sign in to comment.