diff --git a/packages/game/src/model/replay.ts b/packages/game/src/model/replay.ts index 5600a4d..b365893 100644 --- a/packages/game/src/model/replay.ts +++ b/packages/game/src/model/replay.ts @@ -1,24 +1,22 @@ import { f16round, getFloat16, setFloat16 } from "@petamoriken/float16" import { ReplayModel } from "../../proto/replay" -import { Game, GameInput } from "../game" +import { GameInput } from "../game" -export function replayApplyTo(game: Game, replay: ReplayModel) { - const replayFrames = replayFramesFromBytes(replay.frames) +export function applyReplay(replay: ReplayModel, onUpdate: (input: GameInput) => void) { + const replayInputDiff = decodeInputCompressed(replay.frames) let accumulator = 0 - for (const frame of replayFrames) { - accumulator += frame.diff + for (const frame of replayInputDiff) { + accumulator += frame.rotationDelta const input = { rotation: accumulator, thrust: frame.thrust, } - game.onUpdate(input) + onUpdate(input) } - - return game.store } export class GameInputCompressor { @@ -55,13 +53,17 @@ export interface GameInputCompressed { thrust: boolean } -interface Packet { - write: (view: DataView, offset: number) => number +export interface Packet { + write: (view: DataView, offset: number) => void size: number } -export function replayFramesToBytes(replayFrames: GameInputCompressed[]) { - const packets = [...packRotations(replayFrames), ...packThrusts(replayFrames)] +export function encodeInputCompressed(replayFrames: GameInputCompressed[]) { + const packets = [ + ...packRotations(replayFrames), + ...packThrusts(replayFrames.map(x => x.thrust)), + ] + const packetsSize = packets.reduce((acc, packet) => acc + packet.size, 0) const u8 = new Uint8Array(packetsSize) @@ -70,28 +72,35 @@ export function replayFramesToBytes(replayFrames: GameInputCompressed[]) { let offset = 0 for (const packet of packets) { - offset += packet.write(view, offset) + packet.write(view, offset) + offset += packet.size } return u8 } -export function replayFramesFromBytes(bytes: Uint8Array) { +export function decodeInputCompressed(bytes: Uint8Array): GameInputCompressed[] { const view = new DataView( bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength), ) - const [diffs, offset] = unpackRotations(view, 0) - const thrusts = unpackThrusts(view, offset) + const [rotationDeltas, offset] = unpackRotations(view, 0) + const [thrusts, _] = unpackThrusts(view, offset) - if (diffs.length !== thrusts.length) { + if (rotationDeltas.length !== thrusts.length) { throw new Error("diffs and thrusts length mismatch") } - return diffs.map((diff, i) => ({ - diff, - thrust: thrusts[i], - })) + const gameInputs: GameInputCompressed[] = [] + + for (let i = 0; i < rotationDeltas.length; ++i) { + gameInputs.push({ + rotationDelta: rotationDeltas[i], + thrust: thrusts[i], + }) + } + + return gameInputs } function packRotations(frames: GameInputCompressed[]): Packet[] { @@ -140,23 +149,10 @@ function packRotations(frames: GameInputCompressed[]): Packet[] { } } - const zerocount = packed.filter(ct => ct.type === packDiffType.Zero).length - const nonzerocount = packed.filter(ct => ct.type === packDiffType.NonZero).length - const uniquenonzerocount = new Set( - packed - .filter((ct): ct is packDiffNonZero => ct.type === packDiffType.NonZero) - .map(ct => ct.value), - ).size - - console.log("zerocount", zerocount) - console.log("nonzerocount", nonzerocount) - console.log("uniquenonzerocount", uniquenonzerocount) - return [ { write: (view, offset) => { view.setUint32(offset, packed.length, true) - return 4 }, size: 4, }, @@ -167,11 +163,11 @@ function packRotations(frames: GameInputCompressed[]): Packet[] { case packDiffType.Zero: { view.setUint16(offset, 0) view.setUint8(offset + 2, ct.count) - return 3 + return } case packDiffType.NonZero: { setFloat16(view, offset, ct.value, true) - return 2 + return } } }, @@ -183,32 +179,32 @@ function packRotations(frames: GameInputCompressed[]): Packet[] { function unpackRotations(view: DataView, offset: number) { const length = view.getUint32(offset, true) - const diffs: number[] = [] + const rotationDeltas: number[] = [] offset += 4 for (let i = 0; i < length; i++) { - const diff = getFloat16(view, offset, true) + const rotationDelta = getFloat16(view, offset, true) - if (diff === 0) { + if (rotationDelta === 0) { const count = view.getUint8(offset + 2) for (let j = 0; j < count; j++) { - diffs.push(0) + rotationDeltas.push(0) } offset += 3 } else { - diffs.push(diff) + rotationDeltas.push(rotationDelta) offset += 2 } } - return [diffs, offset] as const + return [rotationDeltas, offset] as const } -function packThrusts([first, ...remaining]: GameInputCompressed[]): Packet[] { +export function packThrusts([first, ...remaining]: boolean[]): Packet[] { interface packThrust { thrust: boolean count: number @@ -216,7 +212,7 @@ function packThrusts([first, ...remaining]: GameInputCompressed[]): Packet[] { const packed: packThrust[] = [ { - thrust: first.thrust, + thrust: first, count: 1, }, ] @@ -224,11 +220,11 @@ function packThrusts([first, ...remaining]: GameInputCompressed[]): Packet[] { for (const item of remaining) { const previous = packed.at(-1)! - if (previous.thrust === item.thrust && previous.count < 255) { + if (previous.thrust === item && previous.count < 255) { previous.count++ } else { packed.push({ - thrust: item.thrust, + thrust: item, count: 1, }) } @@ -238,7 +234,6 @@ function packThrusts([first, ...remaining]: GameInputCompressed[]): Packet[] { { write: (view, offset) => { view.setUint32(offset, packed.length, true) - return 4 }, size: 4, }, @@ -247,7 +242,6 @@ function packThrusts([first, ...remaining]: GameInputCompressed[]): Packet[] { write: (view, offset) => { view.setUint8(offset, ct.thrust ? 1 : 0) view.setUint8(offset + 1, ct.count) - return 2 }, size: 2, }), @@ -255,7 +249,7 @@ function packThrusts([first, ...remaining]: GameInputCompressed[]): Packet[] { ] } -function unpackThrusts(view: DataView, offset: number) { +export function unpackThrusts(view: DataView, offset: number): [boolean[], number] { const length = view.getUint32(offset, true) const thrusts: boolean[] = [] @@ -272,5 +266,5 @@ function unpackThrusts(view: DataView, offset: number) { offset += 2 } - return thrusts + return [thrusts, offset] } diff --git a/packages/server/package.json b/packages/server/package.json index 5dcf661..3ee8b20 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -10,6 +10,7 @@ "@tsndr/cloudflare-worker-jwt": "^2.5.4", "hono": "^4.6.3", "shared": "*", + "game": "*", "zod": "^3.23.8" }, "devDependencies": { diff --git a/packages/server/src/features/replay/replay-model.ts b/packages/server/src/features/replay/replay-model.ts index 258f713..452dc75 100644 --- a/packages/server/src/features/replay/replay-model.ts +++ b/packages/server/src/features/replay/replay-model.ts @@ -1,16 +1,21 @@ +import { sql } from "drizzle-orm" import { blob, index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core" -import { ReplaySummaryDTO } from "shared/src/server/replay" +import { Packet, packThrusts, unpackThrusts } from "game/src/model/replay" +import { randomUUID } from "node:crypto" +import { ReplayFrameDTO, ReplaySummaryDTO } from "shared/src/server/replay" +import { users } from "../user/user-model" export const replays = sqliteTable( "replays", { - id: integer("id").primaryKey({ - autoIncrement: true, - }), + id: text("id") + .primaryKey() + .$defaultFn(() => randomUUID()), + binaryModel: blob("model", { mode: "buffer" }).notNull(), + binaryFrames: blob("model", { mode: "buffer" }).notNull(), deaths: integer("deaths").notNull(), gamemode: text("gamemode").notNull(), - model: blob("model", { mode: "buffer" }).notNull(), ticks: integer("ticks").notNull(), worldname: text("world").notNull(), @@ -22,12 +27,31 @@ export const replays = sqliteTable( }), ) -export type Replay = typeof replays.$inferSelect & { +export interface ReplaySummary { + id: string + deaths: number + gamemode: string rank: number + ticks: number username: string } -export function replaySummaryDTO(replay: Replay): ReplaySummaryDTO { +export const replayRank = sql` + ROW_NUMBER() OVER ( + PARTITION BY ${replays.worldname}, ${replays.gamemode} + ORDER BY ${replays.ticks} ASC + )` + +export const replaySummaryColumns = { + id: replays.id, + deaths: replays.deaths, + gamemode: replays.gamemode, + rank: replayRank, + ticks: replays.ticks, + username: users.username, +} + +export function replaySummaryDTO(replay: ReplaySummary): ReplaySummaryDTO { return { id: replay.id, deaths: replay.deaths, @@ -36,3 +60,83 @@ export function replaySummaryDTO(replay: Replay): ReplaySummaryDTO { username: replay.username, } } + +export function encodeReplayFrames(replayFrames: ReplayFrameDTO[]): Buffer { + const packets = [ + ...packFloats(replayFrames.map(x => x.position.x)), + ...packFloats(replayFrames.map(x => x.position.y)), + ...packFloats(replayFrames.map(x => x.rotation)), + ...packThrusts(replayFrames.map(x => x.thurst)), + ] + + const packetsSize = packets.reduce((acc, x) => acc + x.size, 0) + + const buffer = Buffer.alloc(packetsSize) + const view = new DataView(buffer.buffer) + + let offset = 0 + + for (const packet of packets) { + packet.write(view, offset) + offset += packet.size + } + + return buffer +} + +export function decodeReplayFrames(buffer: Buffer): ReplayFrameDTO[] { + const view = new DataView(buffer.buffer) + + const [x, offset0] = unpackFloats(view, 0) + const [y, offset1] = unpackFloats(view, offset0) + const [rotation, offset2] = unpackFloats(view, offset1) + const [thrust, _] = unpackThrusts(view, offset2) + + const frames: ReplayFrameDTO[] = [] + + for (let i = 0; i < x.length; ++i) { + frames.push({ + position: { + x: x[i], + y: y[i], + }, + rotation: rotation[i], + thurst: thrust[i], + }) + } + + return frames +} + +function packFloats(floats: number[]): Packet[] { + return [ + { + write: (view, offset) => { + view.setUint32(offset, floats.length, true) + }, + size: 4, + }, + { + write: (view, offset) => { + for (let i = 0; i < floats.length; i++) { + view.setFloat32(offset + i * 4, floats[i]) + } + }, + size: floats.length * 4, + }, + ] +} + +function unpackFloats(view: DataView, offset: number): [number[], number] { + const size = view.getUint32(offset) + const result: number[] = [] + + offset += 4 + + for (let i = 0; i < size; ++i) { + result.push(view.getFloat32(offset)) + offset += 4 + } + + return [result, offset] +} diff --git a/packages/server/src/features/replay/replay-service.ts b/packages/server/src/features/replay/replay-service.ts deleted file mode 100644 index 1878c7b..0000000 --- a/packages/server/src/features/replay/replay-service.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { eq, sql } from "drizzle-orm" -import { DrizzleD1Database } from "drizzle-orm/d1" -import { Context } from "hono" -import { Environment } from "../../env" -import { users } from "../user/user-model" -import { replays } from "./replay-model" - -export class ReplayService { - private db: DrizzleD1Database - - constructor(private c: Context) { - this.db = c.get("db") - } - - get replays() { - return this.db - .select({ - ...replays._.columns, - rank: sql` - ROW_NUMBER() OVER ( - PARTITION BY ${replays.worldname}, ${replays.gamemode} - ORDER BY ${replays.ticks} ASC - )`, - username: users.username, - }) - .from(replays) - .innerJoin(users, eq(users.id, replays.userId)) - } -} diff --git a/packages/server/src/features/replay/replay.ts b/packages/server/src/features/replay/replay.ts index 9560e12..21b9f08 100644 --- a/packages/server/src/features/replay/replay.ts +++ b/packages/server/src/features/replay/replay.ts @@ -1,53 +1,392 @@ +import * as RAPIER from "@dimforge/rapier2d" import { zValidator } from "@hono/zod-validator" -import { and, eq } from "drizzle-orm" +import { eq } from "drizzle-orm" +import { ReplayModel } from "game/proto/replay" +import { WorldConfig } from "game/proto/world" +import { Game } from "game/src/game" +import { applyReplay } from "game/src/model/replay" +import { rocketComponents } from "game/src/modules/module-rocket" import { Hono } from "hono" +import { ReplayFrameDTO } from "shared/src/server/replay" import { z } from "zod" import { Environment } from "../../env" import { users } from "../user/user-model" import { UserService } from "../user/user-service" -import { Replay, replaySummaryDTO, replays } from "./replay-model" -import { ReplayService } from "./replay-service" +import { worlds } from "../world/world-model" +import { + ReplaySummary, + encodeReplayFrames, + replaySummaryColumns, + replaySummaryDTO, + replays, +} from "./replay-model" export const routeReplay = new Hono() .get( - "/:worldname/:gamemode", + "/", + zValidator( + "query", + z.object({ + replayId: z.string(), + }), + ), + async c => { + const db = c.get("db") + const query = c.req.valid("query") + + const result = await db + .select({ + frames: replays.id, + }) + .from(replays) + .innerJoin(users, eq(users.id, replays.userId)) + .where(eq(replays.id, query.replayId)) + + const [replay] = result + + if (replay === undefined) { + return c.status(404) + } + + return c.json({ + frames: replay.frames, + }) + }, + ) + .get( + "/world", zValidator( "query", z.object({ worldname: z.string(), - gamemode: z.string(), }), ), async c => { + const db = c.get("db") const query = c.req.valid("query") - const replayService = new ReplayService(c) - const result: Replay[] = await replayService.replays - .where( - and( - eq(replays.worldname, query.worldname), - eq(replays.gamemode, query.gamemode), - ), - ) - .limit(50) + const result: ReplaySummary[] = await db + .select({ + ...replaySummaryColumns, + }) + .from(replays) + .innerJoin(users, eq(users.id, replays.userId)) + .where(eq(replays.worldname, query.worldname)) + .limit(25) return c.json({ replays: result.map(replaySummaryDTO), }) }, ) - .get("/me", async c => { - const userService = new UserService(c) - const user = await userService.getAuthenticated() + .get( + "/user", + zValidator( + "query", + z.object({ + username: z.string(), + }), + ), + async c => { + const db = c.get("db") + const query = c.req.valid("query") - if (user === undefined) { - return c.status(401) - } + const result: ReplaySummary[] = await db + .select({ + ...replaySummaryColumns, + }) + .from(replays) + .innerJoin(users, eq(users.id, replays.userId)) + .where(eq(users.username, query.username)) + + return c.json({ + replays: result.map(replaySummaryDTO), + }) + }, + ) + .post( + "/", + zValidator( + "json", + z.object({ + gamemode: z.string(), + model: z.string(), + worldname: z.string(), + }), + ), + async c => { + const db = c.get("db") + const json = c.req.valid("json") + const userService = new UserService(c) + const user = await userService.getAuthenticated() + + if (user === undefined) { + return c.status(401) + } + + const result = validateReplay(json.gamemode, json.model, json.worldname) + + if (result === undefined) { + return c.status(400) + } + + const [replayInsert] = await db + .insert(replays) + .values({ + binaryFrames: encodeReplayFrames(result.replayFrames), + binaryModel: Buffer.from(json.model, "base64"), + deaths: result.summary.deaths, + gamemode: json.gamemode, + ticks: result.summary.ticks, + userId: user.id, + worldname: json.worldname, + }) + .returning({ + id: replays.id, + }) + + const [replay] = await db + .select(replaySummaryColumns) + .from(replays) + .where(eq(replays.id, replayInsert.id)) + .innerJoin(users, eq(users.id, user.id)) + + return c.json({ + replaySummary: replaySummaryDTO(replay), + }) + }, + ) + +function validateReplay(gamemode: string, replayModelBase64: string, worldname: string) { + const world = worlds.find(x => x.id.name === worldname) + + if (world === undefined) { + return undefined + } + + try { + const worldConfig = WorldConfig.decode(Buffer.from(world.configBase64, "base64")) + + const game = new Game( + { + world: worldConfig, + gamemode: gamemode, + }, + { + rapier: RAPIER, + }, + ) - const replayService = new ReplayService(c) - const result: Replay[] = await replayService.replays.where(eq(users.id, user.id)).limit(50) + const replayModel = ReplayModel.decode(Buffer.from(replayModelBase64, "base64")) + const replayFrames: ReplayFrameDTO[] = [] - return c.json({ - replays: result.map(replaySummaryDTO), + const getRocket = game.store.entities.single(...rocketComponents) + + applyReplay(replayModel, input => { + const rocket = getRocket() + + replayFrames.push({ + position: rocket.get("body").translation(), + rotation: rocket.get("body").rotation(), + thurst: input.thrust, + }) + + game.onUpdate(input) }) + + return { + replayFrames, + summary: game.store.resources.get("summary"), + } + } catch (e) { + console.error(e) + } + + return undefined +} + +/* + +export const caseValidateReplay = publicProcedure + .input( + z + .object({ + worldname: z.string(), + gamemode: z.string(), + replayModelBase64: z.string(), + }) + .required(), + ) + .mutation(async ({ input, ctx: { db, user } }) => { + if (!user) { + console.log("user not found") + + throw new TRPCError({ + message: "User not found", + code: "BAD_REQUEST", + }) + } + + const replayBuffer = Buffer.from(input.replayModelBase64, "base64") + const summary = serverValidateReplay(input.worldname, input.gamemode, replayBuffer) + + const [[bestReplayEntry], [replayRank]] = await db.batch([ + personalBestQuery(db, user.id, input.worldname, input.gamemode), + rankForTicksQuery(db, summary.ticks, input.worldname, input.gamemode), + ]) + + let replaySummaryBest: ReplaySummaryDTO = undefined + + if (bestReplayEntry) { + const rank = await rankForTicks( + db, + bestReplayEntry.ticks, + input.worldname, + input.gamemode, + ) + + replaySummaryBest = { + username: user., + rank, + ticks: bestReplayEntry.ticks, + deaths: bestReplayEntry.deaths, + } + } + + if (replaySummaryBest === undefined || summary.ticks <= replaySummaryBest.ticks) { + const update = { + userId: user.id, + + deaths: summary.deaths, + ticks: summary.ticks, + + model: replayBuffer, + } + + try { + await db + .insert(leaderboard) + .values({ + id: bestReplayEntry?.id, + + world: input.worldname, + gamemode: input.gamemode, + + ...update, + }) + .onConflictDoUpdate({ + target: leaderboard.id, + set: update, + }) + .execute() + } catch (e) { + console.log(e) + + throw new TRPCError({ + message: "Failed to update leaderboard", + code: "INTERNAL_SERVER_ERROR", + }) + } + + console.log( + `replay is better, existing ${replaySummaryBest?.ticks}, new ${summary.ticks}`, + ) + } else { + console.log(`replay is worse, existing ${bestReplayEntry?.ticks}, new ${summary.ticks}`) + } + + return { + replaySummaryBest, + replaySummary, + } }) + +function serverValidateReplay(worldname: string, gamemode: string, replayBuffer: Buffer) { + const worldEntry = worlds.find(world => world.id.name === worldname) + + if (!worldEntry) { + console.log("world not found: " + worldname) + + throw new TRPCError({ + message: "World not found", + code: "BAD_REQUEST", + }) + } + + try { + const replayModel = ReplayModel.decode(replayBuffer) + const worldConfig = WorldConfig.decode(Buffer.from(worldEntry.model, "base64")) + + const gameSummary = replayApplyTo( + new Game( + { + world: worldConfig, + gamemode, + }, + { rapier: RAPIER }, + ), + replayModel, + ).resources.get("summary") + + if (!gameSummary) { + console.log("replay is invalid") + + throw new TRPCError({ + message: "Replay is invalid", + code: "BAD_REQUEST", + }) + } + + return gameSummary + } catch (e) { + console.log(e) + throw new TRPCError({ + message: "Replay is invalid", + code: "BAD_REQUEST", + }) + } +} + +function personalBestQuery(db: DrizzleD1Database, userId: number, world: string, gamemode: string) { + return db + .select({ + id: leaderboard.id, + ticks: leaderboard.ticks, + deaths: leaderboard.deaths, + }) + .from(leaderboard) + .where( + and( + eq(leaderboard.userId, userId), + eq(leaderboard.world, world), + eq(leaderboard.gamemode, gamemode), + ), + ) + .limit(1) +} + +function rankForTicksQuery(db: DrizzleD1Database, ticks: number, world: string, gamemode: string) { + return db + .select({ count: sql`${count()} + 1` }) + .from(leaderboard) + .where( + and( + eq(leaderboard.world, world), + eq(leaderboard.gamemode, gamemode), + lt(leaderboard.ticks, ticks), + ), + ) +} + +async function rankForTicks(db: DrizzleD1Database, ticks: number, world: string, gamemode: string) { + const [result] = await rankForTicksQuery(db, ticks, world, gamemode).execute() + + if (result === undefined) { + console.error("Failed to get rank") + throw new Error("Failed to get rank") + } + + return result.count +} + +*/ diff --git a/packages/server/src/features/user/user-model.ts b/packages/server/src/features/user/user-model.ts index c89c9b6..33f9884 100644 --- a/packages/server/src/features/user/user-model.ts +++ b/packages/server/src/features/user/user-model.ts @@ -1,5 +1,5 @@ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" -import { CurrentUserDTO } from "shared/src/server/user" +import { UserDTO } from "shared/src/server/user" import { z } from "zod" export const users = sqliteTable("users", { @@ -13,7 +13,7 @@ export const users = sqliteTable("users", { export type User = typeof users.$inferSelect -export const currentUserDTO = (user: User): CurrentUserDTO => ({ +export const userDTO = (user: User): UserDTO => ({ username: user.username, }) diff --git a/packages/server/src/features/user/user-service.ts b/packages/server/src/features/user/user-service.ts index 607820d..fd60cf4 100644 --- a/packages/server/src/features/user/user-service.ts +++ b/packages/server/src/features/user/user-service.ts @@ -21,4 +21,6 @@ export class UserService { const [user] = await this.db.select().from(users).where(eq(users.id, userId)) return user } + + async incrementClock(user: User) {} } diff --git a/packages/server/src/features/user/user.ts b/packages/server/src/features/user/user.ts index 67cb81d..ad25144 100644 --- a/packages/server/src/features/user/user.ts +++ b/packages/server/src/features/user/user.ts @@ -6,9 +6,31 @@ import { HTTPException } from "hono/http-exception" import { OAuth2Client } from "oslo/oauth2" import { z } from "zod" import { Environment } from "../../env" -import { currentUserDTO, JwtToken, users } from "./user-model" +import { JwtToken, userDTO, users } from "./user-model" +import { UserService } from "./user-service" export const routeUser = new Hono() + .get( + "/me", + zValidator( + "query", + z.object({ + clock: z.number(), + }), + ), + async c => { + const userService = new UserService(c) + const user = await userService.getAuthenticated() + + if (user === undefined) { + return c.status(401) + } + + return c.json({ + user: userDTO(user), + }) + }, + ) .post( "/signin", zValidator( @@ -130,7 +152,7 @@ export const routeUser = new Hono() } return c.json({ - currentUser: currentUserDTO({ + currentUser: userDTO({ ...response, ...user, }), diff --git a/packages/server/src/features/world/world-model.ts b/packages/server/src/features/world/world-model.ts index 71a851f..20e245c 100644 --- a/packages/server/src/features/world/world-model.ts +++ b/packages/server/src/features/world/world-model.ts @@ -2,7 +2,7 @@ import { WorldIdentifier } from "shared/src/server/world" export interface World { id: WorldIdentifier - model: string + configBase64: string gamemodes: string[] } @@ -12,7 +12,8 @@ export const worlds: World[] = [ name: "Red 1", version: 1, }, - model: "CqAJCgZOb3JtYWwSlQkKDw0fhZ3BFR+FB0Id2w/JQBItDR+FtsEVgZUDQh3bD8lAJQAAEMItpHBhQjWuR9lBPR+Fm0FFAAAAQE0AAABAEi0Nrkc/QRVt5wZCHdsPyUAlAAD4QC2kcBZCNezRjUI94KMwP0UAAABATQAAAEASLQ2k8B5CFX9qWEEd2w/JQCUAAP5BLaRwFkI17NG9Qj3gozA/RQAAAEBNAAAAQBItDeyRm0IVPzWGQR3bD8lAJQCAjUItSOHsQTX26AVDPYTr6cBFAAAAQE0AAABAEi0Nw0XwQhUcd4lAHTMeejwlAIDnQi2kcA5CNfboMkM9EK6nv0UAAABATQAAAEASLQ2PYhxDFT813EEd2w/JQCUAAM9CLaRwbEI1AMAmQz0fhbFBRQAAAEBNAAAAQBItDcM15UIVYxBJQh3bD8lAJQAAeUItUrijQjXs0fpCPZDCM0JFAAAAQE0AAABAEi0N9WiFQhXVeIhCHdsPyUAlw7WBQi3sUY9CNcO1kUI9AACBQkUAAABATQAAAEAaTgpMpHA9wXE9ukHAwP8AAEAAPYCA/wAAtIBDAAD/AIDFAEBAQP8AgMgAAICA/wBAxgC+oKD/AABGAMf///8AV0dxQry8+QBSQPHA////ABpOCkyuR3FBSOHKQf/++ABAxgAA//3wAAA/QMT/++AAQEoAQv/3wAAAPkBF/++AAADHAD//3gAAgMYAAP/vgAAAAIDD////AKxGCq////8AGpcCCpQC9qjBQpqZJEL///8AMNEAOv///wDqy9pH////AOzHNML///8AAMIAx////wAAQkDE////AABFAL3///8AAELAx////wCARgBF////AEBGgMb///8AwEYAv////wAgSQBF////AOBIgMP///8A4EjAR////wAARYDE////AAC+oMj///8AAD8AAP///wAAAODK////AGBJAEf///8AwMTASP///wAgSQAA////AEBEwMb///8AAEOAQ////wBASQC/////AAA+wEj///8AwEqAw////wAAvMBL////AODIAAD///8AQMoAQP///wAAPgBI////ACDIAAD///8AgMCARv///wCAyQAA////AEBFgMb///8AGqcCCqQCpHAZQqRwOcH///8AmFgAwP///wCAxwhU////AGDK4E3///8AwM1gyf///wAAv+DI////AKBLAMP///8AADpgyf///wCARgAA////AAA6YMv///8AQMgAAP///wAAvuDJ////AIBFYMj///8AQMyAwf///wAAtMDG////AGDLAL3///8AOMAMSP///wAkxgCu////AADC4Mj///8AAMNARv///wBgyQAA////AEDHgMP///8AwMeAQf///wAAAEBM////ACDJAAD///8AgMMAx////wAAyoBC////AAC9AMb///8AgMTARf///wCAwIDB////AABFAML///8AAMgANP///wBAxEBG////AADHAAD///8AAMFAyP///wBgyEDE////ABomCiSPQopCcT2DQv/AjQAAxAAA/+R0AAAAAMT/kwAAAEQAAP+bAAASEgoGTm9ybWFsEggKBk5vcm1hbA==", + configBase64: + "CqAJCgZOb3JtYWwSlQkKDw0fhZ3BFR+FB0Id2w/JQBItDR+FtsEVgZUDQh3bD8lAJQAAEMItpHBhQjWuR9lBPR+Fm0FFAAAAQE0AAABAEi0Nrkc/QRVt5wZCHdsPyUAlAAD4QC2kcBZCNezRjUI94KMwP0UAAABATQAAAEASLQ2k8B5CFX9qWEEd2w/JQCUAAP5BLaRwFkI17NG9Qj3gozA/RQAAAEBNAAAAQBItDeyRm0IVPzWGQR3bD8lAJQCAjUItSOHsQTX26AVDPYTr6cBFAAAAQE0AAABAEi0Nw0XwQhUcd4lAHTMeejwlAIDnQi2kcA5CNfboMkM9EK6nv0UAAABATQAAAEASLQ2PYhxDFT813EEd2w/JQCUAAM9CLaRwbEI1AMAmQz0fhbFBRQAAAEBNAAAAQBItDcM15UIVYxBJQh3bD8lAJQAAeUItUrijQjXs0fpCPZDCM0JFAAAAQE0AAABAEi0N9WiFQhXVeIhCHdsPyUAlw7WBQi3sUY9CNcO1kUI9AACBQkUAAABATQAAAEAaTgpMpHA9wXE9ukHAwP8AAEAAPYCA/wAAtIBDAAD/AIDFAEBAQP8AgMgAAICA/wBAxgC+oKD/AABGAMf///8AV0dxQry8+QBSQPHA////ABpOCkyuR3FBSOHKQf/++ABAxgAA//3wAAA/QMT/++AAQEoAQv/3wAAAPkBF/++AAADHAD//3gAAgMYAAP/vgAAAAIDD////AKxGCq////8AGpcCCpQC9qjBQpqZJEL///8AMNEAOv///wDqy9pH////AOzHNML///8AAMIAx////wAAQkDE////AABFAL3///8AAELAx////wCARgBF////AEBGgMb///8AwEYAv////wAgSQBF////AOBIgMP///8A4EjAR////wAARYDE////AAC+oMj///8AAD8AAP///wAAAODK////AGBJAEf///8AwMTASP///wAgSQAA////AEBEwMb///8AAEOAQ////wBASQC/////AAA+wEj///8AwEqAw////wAAvMBL////AODIAAD///8AQMoAQP///wAAPgBI////ACDIAAD///8AgMCARv///wCAyQAA////AEBFgMb///8AGqcCCqQCpHAZQqRwOcH///8AmFgAwP///wCAxwhU////AGDK4E3///8AwM1gyf///wAAv+DI////AKBLAMP///8AADpgyf///wCARgAA////AAA6YMv///8AQMgAAP///wAAvuDJ////AIBFYMj///8AQMyAwf///wAAtMDG////AGDLAL3///8AOMAMSP///wAkxgCu////AADC4Mj///8AAMNARv///wBgyQAA////AEDHgMP///8AwMeAQf///wAAAEBM////ACDJAAD///8AgMMAx////wAAyoBC////AAC9AMb///8AgMTARf///wCAwIDB////AABFAML///8AAMgANP///wBAxEBG////AADHAAD///8AAMFAyP///wBgyEDE////ABomCiSPQopCcT2DQv/AjQAAxAAA/+R0AAAAAMT/kwAAAEQAAP+bAAASEgoGTm9ybWFsEggKBk5vcm1hbA==", gamemodes: ["Normal"], }, { @@ -21,7 +22,8 @@ export const worlds: World[] = [ version: 1, }, // model: "CscCCgZOb3JtYWwSvAIKCg2F65XBFTXTGkISKA2kcLrBFZfjFkIlAAAAwi1SuIlCNa5H+UE9H4X/QUUAAABATQAAAEASKA1SuMFBFZmRGkIlhetRQS3NzFJCNSlcp0I9zcxEQUUAAABATQAAAEASKA0AgEVCFfIboEElAAAoQi0K189BNaRw4UI9rkdZwUUAAABATQAAAEASKA171MBCFcubHcElmpm5Qi0K189BNY/CI0M9rkdZwUUAAABATQAAAEASLQ1syOFCFToytkEdVGuzOiWamblCLSlcZUI1XI8jQz3NzIhBRQAAAEBNAAAAQBItDR/lAUMVk9VNQh2fUDa1JaRw9UItexRsQjWF60FDPQAAlEFFAAAAQE0AAABAEigNw1UzQxVpqkFCJdejJEMtBW94QjXXo0JDPQVvAEJFAAAAQE0AAABACu4KCg1Ob3JtYWwgU2hhcGVzEtwKGt8GCtwGP4UAws3MNEGgEEAAZjYAAP///wB1PAAU////AF5PABT///8AyUtPxP///wAzSg3L////AMBJAcj///8AE0Umzf///wCMVAo5////AJNRpDr///8AVE0WVP///wD0vlZLAAD/AEPI7Bn///8AhcPlOAAA/wAFQZrF////ADS9F8f///8AJMIuwf///wC5xvvF////AOrJ1rf///8Ac8ikQP///wBAxfRF////AGkxi0n///8Aj0LxQgAA/wB1xWY9////AJ/HZAlQUP4AzcUBvQAA/wDwQFzE////ADDGR73///8As8eZPoiI8QBxxWQ3rKz/AFw3LMQAAP8AwkNRtP///wC2RKO4////AEhBe8EAAP8AS0WPPP///wAdSaSx////AMw/Ucj///8A7MBNxv///wDmxnG9////AELCFLr///8Aw8UOof///wAKxCg4AAD/ALg8OMDZ2fsA4j9NwP///wCkxB+/AADwAHGwrr54ePgAVERcwv///wAPwXbA////APW0H0EAAPgASLtnv////wALM67DJSX/AFJApL////8AZj4uwP///wBcu+HATU3/AIU7+8H///8AXMK8Lf///wB7wjM/AAD4AHDCx8D///8AFEH7wP///wAAvnvE////AOTGChL///8A6bncRP///wCAQddAAAD4AB/AxLH///8AIL9RPQAA+ACZwqvG////AOLCLkQAAPgAIcTrwP///wDtwQPH////AOLJbqz///8ALsR6QwAA+AD+x8zA////APtF90kyMv8AH7mZQCcn/wCNxHo8tbX/AIDAiETKyv8AXEAgSgAA+AClyAqS////AH9EG0n///8AS0ypRP///wAxSIK7MDToANjBdUf///8A58yjxP///wCByD1EMDToAIzCYMv///8AnMq3MzA06AC+QenF////ANzGT0T///8AtMFSR////wBzRb85lpj/AFJALEQwNOgAqMIpPjA06AAgyiCF////AAPEE77///8AzT4FSnN1/wAzxWFCMDToAA23PcKXl/8AGcLmQDA06ADMPUnJu77/AFrGxsL///8A1TRGSjA06ACKwik8MDToAE3Apcn///8Ar8SawP///wBsygqP////ABHI8z0wNOgAAABTzv///wAa9wMK9APNzJNCj8JlQP///wBmtly8////ABa2jsg2Nv8AO0SENwAA+ACkvrtEvLz/AG0uOEX///8A4UaHPv///wA+QlXFAAD4AApB2L4AAPgAeDLVRP///wATSHHAAAD4ADhA3EP///8As0MKvAAA8ADOPxM4AAD4AEjBTUD///8Arj5TP3B0+ACyKw9DaGz4ALm6eDz///8AKT4MSP///wDhPy5CAAD/APS/XEL///8A+EV6PwAA/wAdsXtBp6f/AGzEpEEAAP8AisfEuf///wDXwVJI////AJpEaUf///8AhUfxQP///wB7RA3FAAD/ANdBTzUAAP8AC8C9Rv///wBGQoVE////APRMpDz///8A7kS3yAAA/wDLR9HB////AFLHNscAAP8AR0HNwf///wDsvtLGAAD/AABE5kD///8AD0JIRv///wD0RNJA////AEVFqcD///8A3ESpwwAA/wAuwgtJ////AARBqEj///8ALUdbSf///wA01Hks////AHjCAL3///8AF8s5x////wC4vlPP////AME1O8f///8AhsIAPgAA+ABcxZXC7e3/AIrEpUMAAPgAjcbDxcvL/wBdQFzF////AEjI+8EAAOAAQ0GZvf///wAGN77AFRX/APlFXDz///8AikEzwkhI+ADcQmoy////AArNAgoHUmV2ZXJzZRLBAgoPDRydLkMVk5lFQh2z7Zk2EigNpHC6wRWX4xZCJQAAAMItAABMQjUAAEDBPR+F/0FFAAAAQE0AAABAEigNUrjBQRWZkRpCJR+FAMItZuaJQjUAAPpBPQAAAEJFAAAAQE0AAABAEigNAIBFQhXyG6BBJQAAUEEthetRQjWkcKdCPVK4TkFFAAAAQE0AAABAEigNe9TAQhXLmx3BJTQzKEItCtfPQTUeBeJCPa5HWcFFAAAAQE0AAABAEi0NbMjhQhU6MrZBHVRrszolmpm5Qi1SuNRBNVyPI0M9ZmZawUUAAABATQAAAEASLQ0f5QFDFZPVTUIdn1A2tSWk8LlCLXsUZUI1hSskQz0AAIZBRQAAAEBNAAAAQBIoDcNVM0MVaapBQiUAgPVCLQAAbEI1AABCQz0AAJRBRQAAAEBNAAAAQBIhCgZOb3JtYWwSFwoNTm9ybWFsIFNoYXBlcwoGTm9ybWFsEiMKB1JldmVyc2USGAoNTm9ybWFsIFNoYXBlcwoHUmV2ZXJzZQ==", - model: "CscCCgZOb3JtYWwSvAIKCg2F65XBFTXTGkISKA2kcLrBFZfjFkIlAAAAwi1SuIlCNa5H+UE9H4X/QUUAAABATQAAAEASKA1SuMFBFZmRGkIlhetRQS3NzFJCNSlcp0I9zcxEQUUAAABATQAAAEASKA0AgEVCFfIboEElAAAoQi0K189BNaRw4UI9rkdZwUUAAABATQAAAEASKA171MBCFcubHcElmpm5Qi0K189BNY/CI0M9rkdZwUUAAABATQAAAEASLQ1syOFCFToytkEdVGuzOiWamblCLSlcZUI1XI8jQz3NzIhBRQAAAEBNAAAAQBItDR/lAUMVk9VNQh2fUDa1JaRw9UItexRsQjWF60FDPQAAlEFFAAAAQE0AAABAEigNw1UzQxVpqkFCJdejJEMtBW94QjXXo0JDPQVvAEJFAAAAQE0AAABACu4KCg1Ob3JtYWwgU2hhcGVzEtwKGt8GCtwGP4UAws3MNEGgEEAAZjYAAP///wB1PAAU////AF5PABT///8AyUtPxP///wAzSg3L////AMBJAcj///8AE0Umzf///wCMVAo5////AJNRpDr///8AVE0WVP///wD0vlZLAAD/AEPI7Bn///8AhcPlOAAA/wAFQZrF////ADS9F8f///8AJMIuwf///wC5xvvF////AOrJ1rf///8Ac8ikQP///wBAxfRF////AGkxi0n///8Aj0LxQgAA/wB1xWY9////AJ/HZAlQUP4AzcUBvQAA/wDwQFzE////ADDGR73///8As8eZPoiI8QBxxWQ3rKz/AFw3LMQAAP8AwkNRtP///wC2RKO4////AEhBe8EAAP8AS0WPPP///wAdSaSx////AMw/Ucj///8A7MBNxv///wDmxnG9////AELCFLr///8Aw8UOof///wAKxCg4AAD/ALg8OMDZ2fsA4j9NwP///wCkxB+/AADwAHGwrr54ePgAVERcwv///wAPwXbA////APW0H0EAAPgASLtnv////wALM67DJSX/AFJApL////8AZj4uwP///wBcu+HATU3/AIU7+8H///8AXMK8Lf///wB7wjM/AAD4AHDCx8D///8AFEH7wP///wAAvnvE////AOTGChL///8A6bncRP///wCAQddAAAD4AB/AxLH///8AIL9RPQAA+ACZwqvG////AOLCLkQAAPgAIcTrwP///wDtwQPH////AOLJbqz///8ALsR6QwAA+AD+x8zA////APtF90kyMv8AH7mZQCcn/wCNxHo8tbX/AIDAiETKyv8AXEAgSgAA+AClyAqS////AH9EG0n///8AS0ypRP///wAxSIK7MDToANjBdUf///8A58yjxP///wCByD1EMDToAIzCYMv///8AnMq3MzA06AC+QenF////ANzGT0T///8AtMFSR////wBzRb85lpj/AFJALEQwNOgAqMIpPjA06AAgyiCF////AAPEE77///8AzT4FSnN1/wAzxWFCMDToAA23PcKXl/8AGcLmQDA06ADMPUnJu77/AFrGxsL///8A1TRGSjA06ACKwik8MDToAE3Apcn///8Ar8SawP///wBsygqP////ABHI8z0wNOgAAABTzv///wAa9wMK9APNzJNCj8JlQP///wBmtly8////ABa2jsg2Nv8AO0SENwAA+ACkvrtEvLz/AG0uOEX///8A4UaHPv///wA+QlXFAAD4AApB2L4AAPgAeDLVRP///wATSHHAAAD4ADhA3EP///8As0MKvAAA8ADOPxM4AAD4AEjBTUD///8Arj5TP3B0+ACyKw9DaGz4ALm6eDz///8AKT4MSP///wDhPy5CAAD/APS/XEL///8A+EV6PwAA/wAdsXtBp6f/AGzEpEEAAP8AisfEuf///wDXwVJI////AJpEaUf///8AhUfxQP///wB7RA3FAAD/ANdBTzUAAP8AC8C9Rv///wBGQoVE////APRMpDz///8A7kS3yAAA/wDLR9HB////AFLHNscAAP8AR0HNwf///wDsvtLGAAD/AABE5kD///8AD0JIRv///wD0RNJA////AEVFqcD///8A3ESpwwAA/wAuwgtJ////AARBqEj///8ALUdbSf///wA01Hks////AHjCAL3///8AF8s5x////wC4vlPP////AME1O8f///8AhsIAPgAA+ABcxZXC7e3/AIrEpUMAAPgAjcbDxcvL/wBdQFzF////AEjI+8EAAOAAQ0GZvf///wAGN77AFRX/APlFXDz///8AikEzwkhI+ADcQmoy////AArNAgoHUmV2ZXJzZRLBAgoPDRydLkMVk5lFQh2z7Zk2EigNpHC6wRWX4xZCJQAAAMItAABMQjUAAEDBPR+F/0FFAAAAQE0AAABAEigNUrjBQRWZkRpCJR+FAMItZuaJQjUAAPpBPQAAAEJFAAAAQE0AAABAEigNAIBFQhXyG6BBJQAAUEEthetRQjWkcKdCPVK4TkFFAAAAQE0AAABAEigNe9TAQhXLmx3BJTQzKEItCtfPQTUeBeJCPa5HWcFFAAAAQE0AAABAEi0NbMjhQhU6MrZBHVRrszolmpm5Qi1SuNRBNVyPI0M9ZmZawUUAAABATQAAAEASLQ0f5QFDFZPVTUIdn1A2tSWk8LlCLXsUZUI1hSskQz0AAIZBRQAAAEBNAAAAQBIoDcNVM0MVaapBQiUAgPVCLQAAbEI1AABCQz0AAJRBRQAAAEBNAAAAQBIhCgZOb3JtYWwSFwoNTm9ybWFsIFNoYXBlcwoGTm9ybWFsEiMKB1JldmVyc2USGAoNTm9ybWFsIFNoYXBlcwoHUmV2ZXJzZRIfCgRIYXJkEhcKBk5vcm1hbAoNTm9ybWFsIFNoYXBlcw==", + configBase64: + "CscCCgZOb3JtYWwSvAIKCg2F65XBFTXTGkISKA2kcLrBFZfjFkIlAAAAwi1SuIlCNa5H+UE9H4X/QUUAAABATQAAAEASKA1SuMFBFZmRGkIlhetRQS3NzFJCNSlcp0I9zcxEQUUAAABATQAAAEASKA0AgEVCFfIboEElAAAoQi0K189BNaRw4UI9rkdZwUUAAABATQAAAEASKA171MBCFcubHcElmpm5Qi0K189BNY/CI0M9rkdZwUUAAABATQAAAEASLQ1syOFCFToytkEdVGuzOiWamblCLSlcZUI1XI8jQz3NzIhBRQAAAEBNAAAAQBItDR/lAUMVk9VNQh2fUDa1JaRw9UItexRsQjWF60FDPQAAlEFFAAAAQE0AAABAEigNw1UzQxVpqkFCJdejJEMtBW94QjXXo0JDPQVvAEJFAAAAQE0AAABACu4KCg1Ob3JtYWwgU2hhcGVzEtwKGt8GCtwGP4UAws3MNEGgEEAAZjYAAP///wB1PAAU////AF5PABT///8AyUtPxP///wAzSg3L////AMBJAcj///8AE0Umzf///wCMVAo5////AJNRpDr///8AVE0WVP///wD0vlZLAAD/AEPI7Bn///8AhcPlOAAA/wAFQZrF////ADS9F8f///8AJMIuwf///wC5xvvF////AOrJ1rf///8Ac8ikQP///wBAxfRF////AGkxi0n///8Aj0LxQgAA/wB1xWY9////AJ/HZAlQUP4AzcUBvQAA/wDwQFzE////ADDGR73///8As8eZPoiI8QBxxWQ3rKz/AFw3LMQAAP8AwkNRtP///wC2RKO4////AEhBe8EAAP8AS0WPPP///wAdSaSx////AMw/Ucj///8A7MBNxv///wDmxnG9////AELCFLr///8Aw8UOof///wAKxCg4AAD/ALg8OMDZ2fsA4j9NwP///wCkxB+/AADwAHGwrr54ePgAVERcwv///wAPwXbA////APW0H0EAAPgASLtnv////wALM67DJSX/AFJApL////8AZj4uwP///wBcu+HATU3/AIU7+8H///8AXMK8Lf///wB7wjM/AAD4AHDCx8D///8AFEH7wP///wAAvnvE////AOTGChL///8A6bncRP///wCAQddAAAD4AB/AxLH///8AIL9RPQAA+ACZwqvG////AOLCLkQAAPgAIcTrwP///wDtwQPH////AOLJbqz///8ALsR6QwAA+AD+x8zA////APtF90kyMv8AH7mZQCcn/wCNxHo8tbX/AIDAiETKyv8AXEAgSgAA+AClyAqS////AH9EG0n///8AS0ypRP///wAxSIK7MDToANjBdUf///8A58yjxP///wCByD1EMDToAIzCYMv///8AnMq3MzA06AC+QenF////ANzGT0T///8AtMFSR////wBzRb85lpj/AFJALEQwNOgAqMIpPjA06AAgyiCF////AAPEE77///8AzT4FSnN1/wAzxWFCMDToAA23PcKXl/8AGcLmQDA06ADMPUnJu77/AFrGxsL///8A1TRGSjA06ACKwik8MDToAE3Apcn///8Ar8SawP///wBsygqP////ABHI8z0wNOgAAABTzv///wAa9wMK9APNzJNCj8JlQP///wBmtly8////ABa2jsg2Nv8AO0SENwAA+ACkvrtEvLz/AG0uOEX///8A4UaHPv///wA+QlXFAAD4AApB2L4AAPgAeDLVRP///wATSHHAAAD4ADhA3EP///8As0MKvAAA8ADOPxM4AAD4AEjBTUD///8Arj5TP3B0+ACyKw9DaGz4ALm6eDz///8AKT4MSP///wDhPy5CAAD/APS/XEL///8A+EV6PwAA/wAdsXtBp6f/AGzEpEEAAP8AisfEuf///wDXwVJI////AJpEaUf///8AhUfxQP///wB7RA3FAAD/ANdBTzUAAP8AC8C9Rv///wBGQoVE////APRMpDz///8A7kS3yAAA/wDLR9HB////AFLHNscAAP8AR0HNwf///wDsvtLGAAD/AABE5kD///8AD0JIRv///wD0RNJA////AEVFqcD///8A3ESpwwAA/wAuwgtJ////AARBqEj///8ALUdbSf///wA01Hks////AHjCAL3///8AF8s5x////wC4vlPP////AME1O8f///8AhsIAPgAA+ABcxZXC7e3/AIrEpUMAAPgAjcbDxcvL/wBdQFzF////AEjI+8EAAOAAQ0GZvf///wAGN77AFRX/APlFXDz///8AikEzwkhI+ADcQmoy////AArNAgoHUmV2ZXJzZRLBAgoPDRydLkMVk5lFQh2z7Zk2EigNpHC6wRWX4xZCJQAAAMItAABMQjUAAEDBPR+F/0FFAAAAQE0AAABAEigNUrjBQRWZkRpCJR+FAMItZuaJQjUAAPpBPQAAAEJFAAAAQE0AAABAEigNAIBFQhXyG6BBJQAAUEEthetRQjWkcKdCPVK4TkFFAAAAQE0AAABAEigNe9TAQhXLmx3BJTQzKEItCtfPQTUeBeJCPa5HWcFFAAAAQE0AAABAEi0NbMjhQhU6MrZBHVRrszolmpm5Qi1SuNRBNVyPI0M9ZmZawUUAAABATQAAAEASLQ0f5QFDFZPVTUIdn1A2tSWk8LlCLXsUZUI1hSskQz0AAIZBRQAAAEBNAAAAQBIoDcNVM0MVaapBQiUAgPVCLQAAbEI1AABCQz0AAJRBRQAAAEBNAAAAQBIhCgZOb3JtYWwSFwoNTm9ybWFsIFNoYXBlcwoGTm9ybWFsEiMKB1JldmVyc2USGAoNTm9ybWFsIFNoYXBlcwoHUmV2ZXJzZRIfCgRIYXJkEhcKBk5vcm1hbAoNTm9ybWFsIFNoYXBlcw==", gamemodes: ["Normal", "Reverse", "Hard"], }, { @@ -29,7 +31,8 @@ export const worlds: World[] = [ name: "Red 3", version: 1, }, - model: "ClwKBkdsb2JhbBJSEigNzcxUwBXJdsBBJQAA7MEtAADKQTUAAO5BPQAAmMBFAAAAQE0AAABAGiYKJAAANEEAAEA/AAD/AODPAACAgP8AAABAxMDA/wDgTwC0////AAo1CgJGMRIvEi0NMzMbQBWLbFdAHdsPyUAlAADswS0AALhANQAA7kE9AACYwEUAAABATQAAAEAKEgoCRzESDAoKDWZmDsEVZmbEQQoSCgJHMhIMCgoNZmYKwRVmZsJBChIKAkczEgwKCg1mZma/FWZmwkEKEgoCRzQSDAoKDWZmRkAVZmbEQQo1CgJGMhIvEi0NzcwywRWLbFdAHdsPyUAlAACawS0AAMpBNQAAIEE9AACYwEUAAABATQAAAEASHAoITm9ybWFsIDESEAoCRzEKAkYxCgZHbG9iYWwSHAoITm9ybWFsIDISEAoCRzIKAkYxCgZHbG9iYWwSHAoITm9ybWFsIDMSEAoCRzMKAkYxCgZHbG9iYWwSHAoITm9ybWFsIDQSEAoCRzQKAkYxCgZHbG9iYWwSHAoITm9ybWFsIDUSEAoCRjIKAkcxCgZHbG9iYWwSHAoITm9ybWFsIDYSEAoCRjIKAkcyCgZHbG9iYWwSHAoITm9ybWFsIDcSEAoCRjIKAkczCgZHbG9iYWwSHAoITm9ybWFsIDgSEAoCRzQKAkYyCgZHbG9iYWw=", + configBase64: + "ClwKBkdsb2JhbBJSEigNzcxUwBXJdsBBJQAA7MEtAADKQTUAAO5BPQAAmMBFAAAAQE0AAABAGiYKJAAANEEAAEA/AAD/AODPAACAgP8AAABAxMDA/wDgTwC0////AAo1CgJGMRIvEi0NMzMbQBWLbFdAHdsPyUAlAADswS0AALhANQAA7kE9AACYwEUAAABATQAAAEAKEgoCRzESDAoKDWZmDsEVZmbEQQoSCgJHMhIMCgoNZmYKwRVmZsJBChIKAkczEgwKCg1mZma/FWZmwkEKEgoCRzQSDAoKDWZmRkAVZmbEQQo1CgJGMhIvEi0NzcwywRWLbFdAHdsPyUAlAACawS0AAMpBNQAAIEE9AACYwEUAAABATQAAAEASHAoITm9ybWFsIDESEAoCRzEKAkYxCgZHbG9iYWwSHAoITm9ybWFsIDISEAoCRzIKAkYxCgZHbG9iYWwSHAoITm9ybWFsIDMSEAoCRzMKAkYxCgZHbG9iYWwSHAoITm9ybWFsIDQSEAoCRzQKAkYxCgZHbG9iYWwSHAoITm9ybWFsIDUSEAoCRjIKAkcxCgZHbG9iYWwSHAoITm9ybWFsIDYSEAoCRjIKAkcyCgZHbG9iYWwSHAoITm9ybWFsIDcSEAoCRjIKAkczCgZHbG9iYWwSHAoITm9ybWFsIDgSEAoCRzQKAkYyCgZHbG9iYWw=", gamemodes: ["Normal"], }, { @@ -37,7 +40,8 @@ export const worlds: World[] = [ name: "Red 4", version: 1, }, - model: "CvYCCgZOb3JtYWwS6wIKCg0zM9XBFTMzY0ISLQ2fbbnBFQPnFkIdPE0MOCUAAADCLVK4iUI1rkf5QT0fhf9BRQAAAEBNAAAAQBIoDVK4wUEVmZEaQiWF61FBLc3MUkI1KVynQj3NzERBRQAAAEBNAAAAQBIoDQCARUIV8hugQSUAAChCLQrXz0E1pHDhQj2uR1nBRQAAAEBNAAAAQBIoDXvUwEIVy5sdwSWamblCLQrXz0E1j8IjQz2uR1nBRQAAAEBNAAAAQBItDWzI4UIVOjK2QR1Ua7M6JZqZuUItKVxlQjVcjyNDPc3MiEFFAAAAQE0AAABAEi0NH+UBQxWT1U1CHZ9QNrUlpHD1Qi17FGxCNYXrQUM9AACUQUUAAABATQAAAEASKA3DVTNDFWmqQUIl16MkQy0Fb3hCNdejQkM9BW8AQkUAAABATQAAAEASKA2amebBFWQ7YEIlPYoRwi1xvY1CNQAAQMA9AAAAQkUAAABATQAAAEAK7goKDU5vcm1hbCBTaGFwZXMS3Aoa3wYK3AY/hQDCzcw0QaAQQABmNgAA////AHU8ABT///8AXk8AFP///wDJS0/E////ADNKDcv///8AwEkByP///wATRSbN////AIxUCjn///8Ak1GkOv///wBUTRZU////APS+VksAAP8AQ8jsGf///wCFw+U4AAD/AAVBmsX///8ANL0Xx////wAkwi7B////ALnG+8X///8A6snWt////wBzyKRA////AEDF9EX///8AaTGLSf///wCPQvFCAAD/AHXFZj3///8An8dkCVBQ/gDNxQG9AAD/APBAXMT///8AMMZHvf///wCzx5k+iIjxAHHFZDesrP8AXDcsxAAA/wDCQ1G0////ALZEo7j///8ASEF7wQAA/wBLRY88////AB1JpLH///8AzD9RyP///wDswE3G////AObGcb3///8AQsIUuv///wDDxQ6h////AArEKDgAAP8AuDw4wNnZ+wDiP03A////AKTEH78AAPAAcbCuvnh4+ABURFzC////AA/BdsD///8A9bQfQQAA+ABIu2e/////AAszrsMlJf8AUkCkv////wBmPi7A////AFy74cBNTf8AhTv7wf///wBcwrwt////AHvCMz8AAPgAcMLHwP///wAUQfvA////AAC+e8T///8A5MYKEv///wDpudxE////AIBB10AAAPgAH8DEsf///wAgv1E9AAD4AJnCq8b///8A4sIuRAAA+AAhxOvA////AO3BA8f///8A4slurP///wAuxHpDAAD4AP7HzMD///8A+0X3STIy/wAfuZlAJyf/AI3Eejy1tf8AgMCIRMrK/wBcQCBKAAD4AKXICpL///8Af0QbSf///wBLTKlE////ADFIgrswNOgA2MF1R////wDnzKPE////AIHIPUQwNOgAjMJgy////wCcyrczMDToAL5B6cX///8A3MZPRP///wC0wVJH////AHNFvzmWmP8AUkAsRDA06ACowik+MDToACDKIIX///8AA8QTvv///wDNPgVKc3X/ADPFYUIwNOgADbc9wpeX/wAZwuZAMDToAMw9Scm7vv8AWsbGwv///wDVNEZKMDToAIrCKTwwNOgATcClyf///wCvxJrA////AGzKCo////8AEcjzPTA06AAAAFPO////ABr3Awr0A83Mk0KPwmVA////AGa2XLz///8AFraOyDY2/wA7RIQ3AAD4AKS+u0S8vP8AbS44Rf///wDhRoc+////AD5CVcUAAPgACkHYvgAA+AB4MtVE////ABNIccAAAPgAOEDcQ////wCzQwq8AADwAM4/EzgAAPgASMFNQP///wCuPlM/cHT4ALIrD0NobPgAubp4PP///wApPgxI////AOE/LkIAAP8A9L9cQv///wD4RXo/AAD/AB2xe0Gnp/8AbMSkQQAA/wCKx8S5////ANfBUkj///8AmkRpR////wCFR/FA////AHtEDcUAAP8A10FPNQAA/wALwL1G////AEZChUT///8A9EykPP///wDuRLfIAAD/AMtH0cH///8AUsc2xwAA/wBHQc3B////AOy+0sYAAP8AAETmQP///wAPQkhG////APRE0kD///8ARUWpwP///wDcRKnDAAD/AC7CC0n///8ABEGoSP///wAtR1tJ////ADTUeSz///8AeMIAvf///wAXyznH////ALi+U8////8AwTU7x////wCGwgA+AAD4AFzFlcLt7f8AisSlQwAA+ACNxsPFy8v/AF1AXMX///8ASMj7wQAA4ABDQZm9////AAY3vsAVFf8A+UVcPP///wCKQTPCSEj4ANxCajL///8ACs0CCgdSZXZlcnNlEsECCg8NHJ0uQxWTmUVCHbPtmTYSKA2kcLrBFZfjFkIlAAAAwi0AAExCNQAAQME9H4X/QUUAAABATQAAAEASKA1SuMFBFZmRGkIlH4UAwi1m5olCNQAA+kE9AAAAQkUAAABATQAAAEASKA0AgEVCFfIboEElAABQQS2F61FCNaRwp0I9UrhOQUUAAABATQAAAEASKA171MBCFcubHcElNDMoQi0K189BNR4F4kI9rkdZwUUAAABATQAAAEASLQ1syOFCFToytkEdVGuzOiWamblCLVK41EE1XI8jQz1mZlrBRQAAAEBNAAAAQBItDR/lAUMVk9VNQh2fUDa1JaTwuUItexRlQjWFKyRDPQAAhkFFAAAAQE0AAABAEigNw1UzQxVpqkFCJQCA9UItAABsQjUAAEJDPQAAlEFFAAAAQE0AAABAEiEKBk5vcm1hbBIXCg1Ob3JtYWwgU2hhcGVzCgZOb3JtYWwSIwoHUmV2ZXJzZRIYCg1Ob3JtYWwgU2hhcGVzCgdSZXZlcnNl", + configBase64: + "CvYCCgZOb3JtYWwS6wIKCg0zM9XBFTMzY0ISLQ2fbbnBFQPnFkIdPE0MOCUAAADCLVK4iUI1rkf5QT0fhf9BRQAAAEBNAAAAQBIoDVK4wUEVmZEaQiWF61FBLc3MUkI1KVynQj3NzERBRQAAAEBNAAAAQBIoDQCARUIV8hugQSUAAChCLQrXz0E1pHDhQj2uR1nBRQAAAEBNAAAAQBIoDXvUwEIVy5sdwSWamblCLQrXz0E1j8IjQz2uR1nBRQAAAEBNAAAAQBItDWzI4UIVOjK2QR1Ua7M6JZqZuUItKVxlQjVcjyNDPc3MiEFFAAAAQE0AAABAEi0NH+UBQxWT1U1CHZ9QNrUlpHD1Qi17FGxCNYXrQUM9AACUQUUAAABATQAAAEASKA3DVTNDFWmqQUIl16MkQy0Fb3hCNdejQkM9BW8AQkUAAABATQAAAEASKA2amebBFWQ7YEIlPYoRwi1xvY1CNQAAQMA9AAAAQkUAAABATQAAAEAK7goKDU5vcm1hbCBTaGFwZXMS3Aoa3wYK3AY/hQDCzcw0QaAQQABmNgAA////AHU8ABT///8AXk8AFP///wDJS0/E////ADNKDcv///8AwEkByP///wATRSbN////AIxUCjn///8Ak1GkOv///wBUTRZU////APS+VksAAP8AQ8jsGf///wCFw+U4AAD/AAVBmsX///8ANL0Xx////wAkwi7B////ALnG+8X///8A6snWt////wBzyKRA////AEDF9EX///8AaTGLSf///wCPQvFCAAD/AHXFZj3///8An8dkCVBQ/gDNxQG9AAD/APBAXMT///8AMMZHvf///wCzx5k+iIjxAHHFZDesrP8AXDcsxAAA/wDCQ1G0////ALZEo7j///8ASEF7wQAA/wBLRY88////AB1JpLH///8AzD9RyP///wDswE3G////AObGcb3///8AQsIUuv///wDDxQ6h////AArEKDgAAP8AuDw4wNnZ+wDiP03A////AKTEH78AAPAAcbCuvnh4+ABURFzC////AA/BdsD///8A9bQfQQAA+ABIu2e/////AAszrsMlJf8AUkCkv////wBmPi7A////AFy74cBNTf8AhTv7wf///wBcwrwt////AHvCMz8AAPgAcMLHwP///wAUQfvA////AAC+e8T///8A5MYKEv///wDpudxE////AIBB10AAAPgAH8DEsf///wAgv1E9AAD4AJnCq8b///8A4sIuRAAA+AAhxOvA////AO3BA8f///8A4slurP///wAuxHpDAAD4AP7HzMD///8A+0X3STIy/wAfuZlAJyf/AI3Eejy1tf8AgMCIRMrK/wBcQCBKAAD4AKXICpL///8Af0QbSf///wBLTKlE////ADFIgrswNOgA2MF1R////wDnzKPE////AIHIPUQwNOgAjMJgy////wCcyrczMDToAL5B6cX///8A3MZPRP///wC0wVJH////AHNFvzmWmP8AUkAsRDA06ACowik+MDToACDKIIX///8AA8QTvv///wDNPgVKc3X/ADPFYUIwNOgADbc9wpeX/wAZwuZAMDToAMw9Scm7vv8AWsbGwv///wDVNEZKMDToAIrCKTwwNOgATcClyf///wCvxJrA////AGzKCo////8AEcjzPTA06AAAAFPO////ABr3Awr0A83Mk0KPwmVA////AGa2XLz///8AFraOyDY2/wA7RIQ3AAD4AKS+u0S8vP8AbS44Rf///wDhRoc+////AD5CVcUAAPgACkHYvgAA+AB4MtVE////ABNIccAAAPgAOEDcQ////wCzQwq8AADwAM4/EzgAAPgASMFNQP///wCuPlM/cHT4ALIrD0NobPgAubp4PP///wApPgxI////AOE/LkIAAP8A9L9cQv///wD4RXo/AAD/AB2xe0Gnp/8AbMSkQQAA/wCKx8S5////ANfBUkj///8AmkRpR////wCFR/FA////AHtEDcUAAP8A10FPNQAA/wALwL1G////AEZChUT///8A9EykPP///wDuRLfIAAD/AMtH0cH///8AUsc2xwAA/wBHQc3B////AOy+0sYAAP8AAETmQP///wAPQkhG////APRE0kD///8ARUWpwP///wDcRKnDAAD/AC7CC0n///8ABEGoSP///wAtR1tJ////ADTUeSz///8AeMIAvf///wAXyznH////ALi+U8////8AwTU7x////wCGwgA+AAD4AFzFlcLt7f8AisSlQwAA+ACNxsPFy8v/AF1AXMX///8ASMj7wQAA4ABDQZm9////AAY3vsAVFf8A+UVcPP///wCKQTPCSEj4ANxCajL///8ACs0CCgdSZXZlcnNlEsECCg8NHJ0uQxWTmUVCHbPtmTYSKA2kcLrBFZfjFkIlAAAAwi0AAExCNQAAQME9H4X/QUUAAABATQAAAEASKA1SuMFBFZmRGkIlH4UAwi1m5olCNQAA+kE9AAAAQkUAAABATQAAAEASKA0AgEVCFfIboEElAABQQS2F61FCNaRwp0I9UrhOQUUAAABATQAAAEASKA171MBCFcubHcElNDMoQi0K189BNR4F4kI9rkdZwUUAAABATQAAAEASLQ1syOFCFToytkEdVGuzOiWamblCLVK41EE1XI8jQz1mZlrBRQAAAEBNAAAAQBItDR/lAUMVk9VNQh2fUDa1JaTwuUItexRlQjWFKyRDPQAAhkFFAAAAQE0AAABAEigNw1UzQxVpqkFCJQCA9UItAABsQjUAAEJDPQAAlEFFAAAAQE0AAABAEiEKBk5vcm1hbBIXCg1Ob3JtYWwgU2hhcGVzCgZOb3JtYWwSIwoHUmV2ZXJzZRIYCg1Ob3JtYWwgU2hhcGVzCgdSZXZlcnNl", gamemodes: ["Normal", "Reverse"], }, ] diff --git a/packages/server/src/features/world/world-index.ts b/packages/server/src/features/world/world.ts similarity index 71% rename from packages/server/src/features/world/world-index.ts rename to packages/server/src/features/world/world.ts index 69f02ff..3f5dc41 100644 --- a/packages/server/src/features/world/world-index.ts +++ b/packages/server/src/features/world/world.ts @@ -6,23 +6,17 @@ import { Environment } from "../../env" import { worlds } from "./world-model" export const routeWorld = new Hono().get( - "/world", + "/", zValidator( "query", z.object({ - worldnames: z.array(z.string()), + timestamp: z.number().optional(), }), ), async c => { - const db = c.get("db") const query = c.req.valid("query") - const user = c.get("db") - const queryWorlds = worlds.filter(world => query.worldnames.includes(world.id.name)) - - let replays - - const resultWorlds = queryWorlds.map( + const resultWorlds = worlds.map( (world): WorldDTO => ({ gamemodes: world.gamemodes.map( (gamemode): GamemodeDTO => ({ @@ -31,7 +25,7 @@ export const routeWorld = new Hono().get( ), id: world.id, image: "", - model: world.model, + model: world.configBase64, }), ) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index ba79fed..3d967c2 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -7,7 +7,7 @@ import { timing } from "hono/timing" import { Environment } from "./env" import { routeUser } from "./features/user/user" import { middlewareAuth } from "./features/user/user-middleware" -import { routeWorld } from "./features/world/world-index" +import { routeWorld } from "./features/world/world" const app = new Hono() .use(timing()) diff --git a/packages/shared/src/server/replay.ts b/packages/shared/src/server/replay.ts index 625303b..2fdd522 100644 --- a/packages/shared/src/server/replay.ts +++ b/packages/shared/src/server/replay.ts @@ -1,3 +1,15 @@ +import { Point } from "game/src/model/utils" + +export interface ReplayDTO { + frames: ReplayFrameDTO[] +} + +export interface ReplayFrameDTO { + position: Point + rotation: number + thurst: boolean +} + export interface ReplaySummaryDTO { deaths: number id: string diff --git a/packages/shared/src/server/user.ts b/packages/shared/src/server/user.ts index 1ef70d0..e3ea779 100644 --- a/packages/shared/src/server/user.ts +++ b/packages/shared/src/server/user.ts @@ -1,7 +1,7 @@ import { z } from "zod" -export const currentUserDTO = z.object({ +export const userDTO = z.object({ username: z.string(), }) -export type CurrentUserDTO = z.infer +export type UserDTO = z.infer diff --git a/packages/shared/src/server/world.ts b/packages/shared/src/server/world.ts index 6e040d4..9e2b0cd 100644 --- a/packages/shared/src/server/world.ts +++ b/packages/shared/src/server/world.ts @@ -17,12 +17,3 @@ export interface GamemodeDTO { name: string replaySummary?: ReplaySummaryDTO } - -export interface LeaderboardDTO { - entries: LeaderboardEntryDTO[] -} - -export interface LeaderboardEntryDTO { - leaderboardId: number - replaySummary: ReplaySummaryDTO -}