Skip to content

Commit

Permalink
Replay coding refactor & server simplify
Browse files Browse the repository at this point in the history
  • Loading branch information
phisn committed Oct 7, 2024
1 parent 235a62d commit 5adfa11
Show file tree
Hide file tree
Showing 14 changed files with 575 additions and 141 deletions.
94 changes: 44 additions & 50 deletions packages/game/src/model/replay.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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[] {
Expand Down Expand Up @@ -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,
},
Expand All @@ -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
}
}
},
Expand All @@ -183,52 +179,52 @@ 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
}

const packed: packThrust[] = [
{
thrust: first.thrust,
thrust: first,
count: 1,
},
]

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,
})
}
Expand All @@ -238,7 +234,6 @@ function packThrusts([first, ...remaining]: GameInputCompressed[]): Packet[] {
{
write: (view, offset) => {
view.setUint32(offset, packed.length, true)
return 4
},
size: 4,
},
Expand All @@ -247,15 +242,14 @@ 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,
}),
),
]
}

function unpackThrusts(view: DataView, offset: number) {
export function unpackThrusts(view: DataView, offset: number): [boolean[], number] {
const length = view.getUint32(offset, true)
const thrusts: boolean[] = []

Expand All @@ -272,5 +266,5 @@ function unpackThrusts(view: DataView, offset: number) {
offset += 2
}

return thrusts
return [thrusts, offset]
}
1 change: 1 addition & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@tsndr/cloudflare-worker-jwt": "^2.5.4",
"hono": "^4.6.3",
"shared": "*",
"game": "*",
"zod": "^3.23.8"
},
"devDependencies": {
Expand Down
118 changes: 111 additions & 7 deletions packages/server/src/features/replay/replay-model.ts
Original file line number Diff line number Diff line change
@@ -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(),

Expand All @@ -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<number>`
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,
Expand All @@ -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]
}
Loading

0 comments on commit 5adfa11

Please sign in to comment.