Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: multiple scenes per world #281

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "template-server",
"name": "worlds-content-server",
"license": "Apache-2.0",
"scripts": {
"build": "tsc -p tsconfig.json",
Expand Down
24 changes: 18 additions & 6 deletions src/adapters/update-owner-job.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { AppComponents, BlockedRecord, IRunnable, Notification, TWO_DAYS_IN_MS, Whitelist, WorldRecord } from '../types'
import {
AppComponents,
BlockedRecord,
IRunnable,
Notification,
SceneRecord,
TWO_DAYS_IN_MS,
Whitelist,
WorldRecord
} from '../types'
import SQL from 'sql-template-strings'
import { CronJob } from 'cron'

type WorldData = Pick<WorldRecord, 'name' | 'owner' | 'size' | 'entity'>
type WorldData = Pick<WorldRecord, 'name' | 'owner'> & Pick<SceneRecord, 'size'>

export async function createUpdateOwnerJob(
components: Pick<
Expand Down Expand Up @@ -118,15 +127,18 @@ export async function createUpdateOwnerJob(
async function run() {
const startDate = new Date()

const records = await components.database.query<WorldData>(
'SELECT name, owner, size, entity FROM worlds WHERE entity_id IS NOT NULL'
)
const records = await components.database.query<WorldData>(SQL`
SELECT owner, name, SUM(size)
FROM worlds
INNER JOIN scenes ON worlds.name = scenes.world_name
GROUP BY name
`)

const onlyDclNameRecords = records.rows
.filter((row) => !!row.name && row.name.endsWith('.dcl.eth'))
.map((row) => {
return {
name: row.name,
entity: row.entity,
owner: row.owner,
size: BigInt(row.size)
}
Expand Down
14 changes: 10 additions & 4 deletions src/adapters/wallet-stats.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AppComponents, IWalletStats, MB_BigInt, WalletStats, WorldRecord } from '../types'
import { AppComponents, IWalletStats, MB_BigInt, SceneRecord, WalletStats, WorldRecord } from '../types'
import { EthAddress } from '@dcl/schemas'
import SQL from 'sql-template-strings'

Expand Down Expand Up @@ -28,13 +28,19 @@ export async function createWalletStatsComponent(
return json['data']
}

type Record = Pick<WorldRecord, 'name'> & Pick<SceneRecord, 'size'>

async function fetchStoredData(wallet: string) {
const rows = await components.database.query<Pick<WorldRecord, 'name' | 'entity_id' | 'size'>>(SQL`
SELECT name, entity_id, size FROM worlds WHERE owner = ${wallet.toLowerCase()}`)
const rows = await components.database.query<Record>(SQL`
SELECT name, SUM(size) as size
FROM worlds
INNER JOIN scenes ON worlds.name = scenes.world_name
WHERE owner = ${wallet.toLowerCase()}
GROUP BY name
`)
return rows.rows.map((row) => {
return {
name: row.name,
entityId: row.entity_id,
size: BigInt(row.size)
}
})
Expand Down
51 changes: 31 additions & 20 deletions src/adapters/worlds-indexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,38 @@ export async function createWorldsIndexerComponent({
}: Pick<AppComponents, 'worldsManager'>): Promise<IWorldsIndexer> {
async function getIndex(): Promise<WorldsIndex> {
const deployedEntities = await worldsManager.getDeployedWorldEntities()
const index: WorldData[] = deployedEntities.map((entity) => {
const worldName = entity.metadata.worldConfiguration.name
const thumbnailFile = entity.content.find(
(content: ContentMapping) => content.file === entity.metadata.display?.navmapThumbnail
)
return {
name: worldName,
scenes: [
{
id: entity.id,
title: entity.metadata?.display?.title,
description: entity.metadata?.display?.description,
thumbnail: thumbnailFile?.hash,
pointers: entity.pointers,
runtimeVersion: entity.metadata?.runtimeVersion,
timestamp: entity.timestamp
}
]
}
})
const byName: Map<string, WorldData> = deployedEntities
.map((entity): WorldData => {
const worldName = entity.metadata.worldConfiguration.name
const thumbnailFile = entity.content.find(
(content: ContentMapping) => content.file === entity.metadata.display?.navmapThumbnail
)
return {
name: worldName,
scenes: [
{
id: entity.id,
title: entity.metadata?.display?.title,
description: entity.metadata?.display?.description,
thumbnail: thumbnailFile?.hash,
pointers: entity.pointers,
runtimeVersion: entity.metadata?.runtimeVersion,
timestamp: entity.timestamp
}
]
}
})
.reduce((acc: Map<string, WorldData>, data) => {
const worldData = acc.get(data.name)
if (worldData) {
worldData.scenes.push(data.scenes[0])
} else {
acc.set(data.name, data)
}
return acc
}, new Map())

const index = Array.from(byName.values())
return { index, timestamp: Date.now() }
}

Expand Down
162 changes: 102 additions & 60 deletions src/adapters/worlds-manager.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {
AppComponents,
ContributorDomain,
IPermissionChecker,
IWorldsManager,
Permissions,
SceneRecord,
WorldMetadata,
WorldRecord,
ContributorDomain
WorldRecord
} from '../types'
import { streamToBuffer } from '@dcl/catalyst-storage'
import { Entity, EthAddress } from '@dcl/schemas'
Expand All @@ -21,16 +22,12 @@ export async function createWorldsManagerComponent({
}: Pick<AppComponents, 'logs' | 'database' | 'nameDenyListChecker' | 'storage'>): Promise<IWorldsManager> {
const logger = logs.getLogger('worlds-manager')

async function getRawWorldRecords(): Promise<WorldRecord[]> {
const result = await database.query<WorldRecord>(
SQL`SELECT worlds.*, blocked.created_at AS blocked_since
FROM worlds
LEFT JOIN blocked ON worlds.owner = blocked.wallet`
)
async function getRawSceneRecords(): Promise<SceneRecord[]> {
const result = await database.query<SceneRecord>(SQL`SELECT * FROM scenes`)

const filtered: WorldRecord[] = []
const filtered: SceneRecord[] = []
for (const row of result.rows) {
if (await nameDenyListChecker.checkNameDenyList(row.name)) {
if (await nameDenyListChecker.checkNameDenyList(row.world_name)) {
filtered.push(row)
}
}
Expand All @@ -44,12 +41,15 @@ export async function createWorldsManagerComponent({
return undefined
}

const result = await database.query<WorldRecord>(
SQL`SELECT worlds.*, blocked.created_at AS blocked_since
FROM worlds
LEFT JOIN blocked ON worlds.owner = blocked.wallet
WHERE worlds.name = ${worldName.toLowerCase()}`
)
type Record = WorldRecord & Pick<SceneRecord, 'entity_id' | 'entity'>
const result = await database.query<Record>(SQL`
SELECT worlds.*, scenes.entity_id, scenes.entity, blocked.created_at AS blocked_since
FROM worlds
JOIN scenes ON worlds.name = scenes.world_name
LEFT JOIN blocked ON worlds.owner = blocked.wallet
WHERE worlds.name = ${worldName.toLowerCase()}
ORDER BY scenes.created_at
`)

if (result.rowCount === 0) {
return undefined
Expand All @@ -58,8 +58,10 @@ export async function createWorldsManagerComponent({
const row = result.rows[0]
const tempWorldMetadata: Partial<WorldMetadata> = {}
if (row.entity) {
tempWorldMetadata.entityId = row.entity_id
tempWorldMetadata.runtimeMetadata = extractWorldRuntimeMetadata(worldName, { ...row.entity, id: row.entity_id })
tempWorldMetadata.runtimeMetadata = extractWorldRuntimeMetadata(
worldName,
result.rows.map((row) => ({ ...row.entity, id: row.entity_id }))
)
}
if (row.permissions) {
tempWorldMetadata.permissions = row.permissions
Expand All @@ -85,24 +87,56 @@ export async function createWorldsManagerComponent({
const fileInfos = await storage.fileInfoMultiple(scene.content?.map((c) => c.hash) || [])
const size = scene.content?.reduce((acc, c) => acc + (fileInfos.get(c.hash)?.size || 0), 0) || 0

const sql = SQL`
INSERT INTO worlds (name, entity_id, owner, deployer, deployment_auth_chain, entity, permissions, size, created_at, updated_at)
VALUES (${worldName.toLowerCase()}, ${scene.id},
${owner?.toLowerCase()}, ${deployer}, ${deploymentAuthChainString}::json,
${scene}::json,
${JSON.stringify(defaultPermissions())}::json,
${size},
${new Date()}, ${new Date()})
ON CONFLICT (name)
DO UPDATE SET entity_id = ${scene.id},
owner = ${owner?.toLowerCase()},
deployer = ${deployer},
entity = ${scene}::json,
size = ${size},
deployment_auth_chain = ${deploymentAuthChainString}::json,
updated_at = ${new Date()}
const existingScenes = await getDeployedScenesForWorld(worldName)
const collidingScenes = await findCollisions(scene, existingScenes)

try {
await database.query(SQL`BEGIN`)
await database.query(
SQL`DELETE
FROM scenes
WHERE world_name = ${worldName.toLowerCase()}
AND entity_id = ANY (${collidingScenes.map((s) => s.entity_id)})`
)

const sqlWorld = SQL`
INSERT INTO worlds (name, owner, permissions, created_at, updated_at)
VALUES (${worldName.toLowerCase()},
${owner?.toLowerCase()},
${JSON.stringify(defaultPermissions())}::json,
${new Date()},
${new Date()})
ON CONFLICT (name)
DO UPDATE SET owner = ${owner?.toLowerCase()},
updated_at = ${new Date()}
`
await database.query(sql)
await database.query(sqlWorld)

const sqlScene = SQL`
INSERT INTO scenes (world_name, entity_id, deployer, deployment_auth_chain, entity, size, created_at, updated_at)
VALUES (${worldName.toLowerCase()},
${scene.id},
${deployer},
${deploymentAuthChainString}::json,
${scene}::json,
${size},
${new Date()},
${new Date()})
ON CONFLICT (world_name, entity_id)
DO UPDATE SET entity_id = ${scene.id},
deployer = ${deployer},
deployment_auth_chain = ${deploymentAuthChainString}::json,
entity = ${scene}::json,
size = ${size},
updated_at = ${new Date()}
`
await database.query(sqlScene)
await database.query(SQL`COMMIT`)
} catch (error: any) {
logger.warn(`Error deploying scene: ${error.message}`)
await database.query(SQL`ROLLBACK`)
throw error
}
}

async function storePermissions(worldName: string, permissions: Permissions): Promise<void> {
Expand All @@ -114,14 +148,16 @@ export async function createWorldsManagerComponent({
DO UPDATE SET permissions = ${JSON.stringify(permissions)}::json,
updated_at = ${new Date()}
`
await database.query(sql)
const result = await database.query(sql)
console.log(`done storing permissions for ${worldName}`, result.rows)
console.log('all', (await database.query(SQL`SELECT * FROM worlds`)).rows)
}

async function getDeployedWorldCount(): Promise<{ ens: number; dcl: number }> {
const result = await database.query<{ name: string }>('SELECT name FROM worlds WHERE entity_id IS NOT NULL')
const result = await database.query<Pick<SceneRecord, 'world_name'>>('SELECT DISTINCT world_name FROM scenes')
return result.rows.reduce(
(acc, row) => {
if (row.name.endsWith('.dcl.eth')) {
if (row.world_name.endsWith('.dcl.eth')) {
acc.dcl++
} else {
acc.ens++
Expand All @@ -132,19 +168,19 @@ export async function createWorldsManagerComponent({
)
}

const mapEntity = (row: Pick<WorldRecord, 'entity_id' | 'entity'>) => ({
const mapEntity = (row: Pick<SceneRecord, 'entity_id' | 'entity'>) => ({
...row.entity,
id: row.entity_id
})

async function getDeployedWorldEntities(): Promise<Entity[]> {
const result = await database.query<Pick<WorldRecord, 'name' | 'entity_id' | 'entity'>>(
'SELECT name, entity_id, entity FROM worlds WHERE entity_id IS NOT NULL ORDER BY name'
const result = await database.query<Pick<SceneRecord, 'world_name' | 'entity_id' | 'entity'>>(
'SELECT world_name, entity_id, entity FROM scenes ORDER BY world_name'
)

const filtered: Pick<WorldRecord, 'name' | 'entity_id' | 'entity'>[] = []
const filtered: Pick<SceneRecord, 'world_name' | 'entity_id' | 'entity'>[] = []
for (const row of result.rows) {
if (await nameDenyListChecker.checkNameDenyList(row.name)) {
if (await nameDenyListChecker.checkNameDenyList(row.world_name)) {
filtered.push(row)
}
}
Expand All @@ -158,8 +194,8 @@ export async function createWorldsManagerComponent({
return undefined
}

const result = await database.query<Pick<WorldRecord, 'entity_id' | 'entity'>>(
SQL`SELECT entity_id, entity FROM worlds WHERE name = ${worldName.toLowerCase()} AND entity_id IS NOT NULL ORDER BY name`
const result = await database.query<Pick<SceneRecord, 'entity_id' | 'entity'>>(
SQL`SELECT entity_id, entity FROM scenes WHERE world_name = ${worldName.toLowerCase()} ORDER BY world_name`
)

if (result.rowCount === 0) {
Expand All @@ -175,18 +211,7 @@ export async function createWorldsManagerComponent({
}

async function undeploy(worldName: string): Promise<void> {
const sql = SQL`
UPDATE worlds
SET entity_id = null,
owner = null,
deployer = null,
entity = null,
size = null,
deployment_auth_chain = null,
updated_at = ${new Date()}
WHERE name = ${worldName.toLowerCase()}
`
await database.query(sql)
await database.query(SQL`DELETE FROM scenes WHERE world_name = ${worldName.toLowerCase()}`)
}

async function getContributableDomains(address: string): Promise<{ domains: ContributorDomain[]; count: number }> {
Expand All @@ -209,16 +234,33 @@ export async function createWorldsManagerComponent({
}
}

async function getDeployedScenesForWorld(worldName: string): Promise<SceneRecord[]> {
const result = await database.query<SceneRecord>(
SQL`SELECT * FROM scenes WHERE world_name = ${worldName.toLowerCase()} ORDER BY created_at DESC`
)

return result.rows
}

async function findCollisions(entity: Entity, previousScenes: SceneRecord[]): Promise<SceneRecord[]> {
const newParcels = new Set(entity.pointers)
return previousScenes.filter(
(row) =>
newParcels.has(row.entity.metadata.scene.base) ||
row.entity.metadata.scene.parcels.some((parcel: string) => newParcels.has(parcel))
)
}

return {
getRawWorldRecords,
getRawSceneRecords,
getContributableDomains,
getDeployedWorldCount,
getDeployedWorldEntities,
getMetadataForWorld,
getEntityForWorld,
deployScene,
storePermissions,
permissionCheckerForWorld,
undeploy,
getContributableDomains
undeploy
}
}
Loading
Loading