Skip to content

Commit

Permalink
Refactor and finalize rewrite in web part - game player
Browse files Browse the repository at this point in the history
  • Loading branch information
phisn committed Nov 7, 2024
1 parent 329ce24 commit 28da639
Show file tree
Hide file tree
Showing 16 changed files with 308 additions and 488 deletions.
8 changes: 4 additions & 4 deletions packages/game-player/src/modules/module-input/module-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Devices } from "./devices/devices"

export interface InputCaptureResource {
currentInput: GameInput
frames: GameInputCompressed[]
inputs: GameInputCompressed[]
}

export class ModuleInput {
Expand All @@ -15,7 +15,7 @@ export class ModuleInput {
constructor(private store: GamePlayerStore) {
this.store.resources.set("inputCapture", {
currentInput: { rotation: 0, thrust: false },
frames: [],
inputs: [],
})

this.devices = new Devices(store)
Expand All @@ -31,7 +31,7 @@ export class ModuleInput {

const inputCapture = this.store.resources.get("inputCapture")
inputCapture.currentInput = { rotation: 0, thrust: false }
inputCapture.frames = []
inputCapture.inputs = []
}

onFixedUpdate() {
Expand All @@ -43,7 +43,7 @@ export class ModuleInput {
}

const inputCapture = this.store.resources.get("inputCapture")
inputCapture.frames.push(this.compressor.compress(input))
inputCapture.inputs.push(this.compressor.compress(input))
inputCapture.currentInput = input
}
}
2 changes: 1 addition & 1 deletion packages/game/proto/replay.proto
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
syntax = "proto3";

message ReplayModel {
bytes frames = 1;
bytes deltaInputs = 1;
}
8 changes: 4 additions & 4 deletions packages/game/src/model/replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ import { ReplayModel } from "../../proto/replay"
import { GameInput } from "../game"

export function applyReplay(replay: ReplayModel, onUpdate: (input: GameInput) => void) {
const replayInputDiff = decodeInputCompressed(replay.frames)
const replayDeltaInputs = decodeInputCompressed(replay.deltaInputs)

let accumulator = 0

for (const frame of replayInputDiff) {
accumulator += frame.rotationDelta
for (const replayDeltaInput of replayDeltaInputs) {
accumulator += replayDeltaInput.rotationDelta

const input = {
rotation: accumulator,
thrust: frame.thrust,
thrust: replayDeltaInput.thrust,
}

onUpdate(input)
Expand Down
36 changes: 34 additions & 2 deletions packages/server/src/features/replay/replay.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as RAPIER from "@dimforge/rapier2d"
import { zValidator } from "@hono/zod-validator"
import { eq } from "drizzle-orm"
import { and, eq } from "drizzle-orm"
import { ReplayModel } from "game/proto/replay"
import { WorldConfig } from "game/proto/world"
import { Game } from "game/src/game"
Expand Down Expand Up @@ -136,11 +136,40 @@ export const routeReplay = new Hono<Environment>()
throw new HTTPException(400)
}

const [best] = await db
.select(replaySummaryColumns)
.from(replays)
.where(
and(
eq(replays.userId, user.id),
eq(replays.worldname, json.worldname),
eq(replays.gamemode, json.gamemode),
),
)
.orderBy(replays.ticks, replays.deaths)
.limit(1)

if (best) {
const worseTime = best.ticks < result.summary.ticks
const worseDeaths =
best.ticks === result.summary.ticks && best.deaths < result.summary.ticks

if (worseTime || worseDeaths) {
return c.json({
type: "no-improvement" as const,

bestSummary: replaySummaryDTO(best),
})
}
}

const binaryModel = Buffer.from(json.model, "base64")

const [replayInsert] = await db
.insert(replays)
.values({
binaryFrames: encodeReplayFrames(result.replayFrames),
binaryModel: Buffer.from(json.model, "base64"),
binaryModel,
deaths: result.summary.deaths,
gamemode: json.gamemode,
ticks: result.summary.ticks,
Expand All @@ -158,7 +187,10 @@ export const routeReplay = new Hono<Environment>()
.innerJoin(users, eq(users.id, user.id))

return c.json({
type: "improvement" as const,

replaySummary: replaySummaryDTO(replay),
bestSummary: replaySummaryDTO(best),
})
},
)
Expand Down
14 changes: 7 additions & 7 deletions packages/web/src/common/hooks/UseAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ModalCreateAccount } from "../../components/modals/ModalCreateAccount"
import { ModalRenameAccount } from "../../components/modals/ModalRenameAccount"
import { authService } from "../services/auth-service"
import { rpc } from "../services/rpc"
import { useStore } from "../store"
import { useGlobalStore } from "../store"

export interface AuthApi {
login: () => void
Expand All @@ -26,7 +26,7 @@ export function useAuth(): AuthApi {
if (!response.ok) {
console.error("Failed to signin", response)

useStore.getState().newAlert({
useGlobalStore.getState().newAlert({
type: "error",
message: `Failed to signin (${response.status})`,
})
Expand All @@ -37,17 +37,17 @@ export function useAuth(): AuthApi {
const responseJson = await response.json()

if (responseJson.type === "prompt-create") {
useStore.getState().newModal({
useGlobalStore.getState().newModal({
modal: function CreateModal() {
return (
<ModalCreateAccount
creationJwt={responseJson.token}
onCancel={() => {
useStore.getState().removeModal()
useGlobalStore.getState().removeModal()
setLoading(false)
}}
onCreated={() => {
useStore.getState().removeModal()
useGlobalStore.getState().removeModal()
setLoading(false)
}}
/>
Expand Down Expand Up @@ -81,13 +81,13 @@ export function useAuth(): AuthApi {
googleLogin()
},
rename() {
useStore.getState().newModal({
useGlobalStore.getState().newModal({
modal: function RenameModal() {
return (
<ModalRenameAccount
open={true}
onFinished={() => {
useStore.getState().removeModal()
useGlobalStore.getState().removeModal()
setLoading(false)
}}
/>
Expand Down
8 changes: 4 additions & 4 deletions packages/web/src/common/services/auth-service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { UserDTO } from "shared/src/server/user"
import { useStore } from "../store"
import { useGlobalStore } from "../store"
import { replayService } from "./replay-service"
import { rpc } from "./rpc"
import { worldService } from "./world-service"
Expand Down Expand Up @@ -63,7 +63,7 @@ class AuthService {
userDTO: authObject.userDTO,
}

useStore.getState().setCurrentUser(this.state.userDTO)
useGlobalStore.getState().setCurrentUser(this.state.userDTO)
}
} else {
this.state = {
Expand Down Expand Up @@ -93,7 +93,7 @@ class AuthService {
type: "unauthenticated",
}

useStore.getState().setCurrentUser()
useGlobalStore.getState().setCurrentUser()
}

async login(jwt: string) {
Expand Down Expand Up @@ -137,7 +137,7 @@ class AuthService {
} satisfies LocalStorageAuthObject),
)

useStore.getState().setCurrentUser(responseJson.user)
useGlobalStore.getState().setCurrentUser(responseJson.user)

await Promise.all([replayService.sync(), worldService.sync()])
}
Expand Down
100 changes: 84 additions & 16 deletions packages/web/src/common/services/replay-service.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
import Dexie from "dexie"
import { ReplayModel } from "game/proto/replay"
import { bytesToBase64 } from "game/src/model/utils"
import { ReplayDTO, ReplaySummaryDTO } from "shared/src/server/replay"
import { useAuthStore } from "../store/auth-store"
import { authService } from "./auth-service"
import { rpc } from "./rpc"

interface PendingReplay {
hash: string

worldname: string
gamemode: string
model: Uint8Array
}

const db = new Dexie("polyburn-cache-replays") as Dexie & {
pendingReplays: Dexie.Table<PendingReplay>
replays: Dexie.Table<ReplayDTO>
replaySummaries: Dexie.Table<ReplaySummaryDTO>
}

db.version(1).stores({
pendingReplays: "hash",
replays: "id, gamemode, username, worldname",
replaySummaries: "id, gamemode, username, worldname",
})
Expand All @@ -20,7 +33,52 @@ export interface ExReplaySummaryDTO extends ReplaySummaryDTO {
export class ReplayService {
constructor() {}

async sync() {}
async commit(worldname: string, gamemode: string, replayModel: ReplayModel): Promise<string> {
const model = ReplayModel.encode(replayModel).finish()
const hashBuffer = await crypto.subtle.digest("SHA-512", model)
const hash = bytesToBase64(new Uint8Array(hashBuffer))

await db.pendingReplays.put(
{
hash,

worldname,
gamemode,
model,
},
hash,
)

return hash
}

async get(id: string): Promise<ReplayDTO> {
if (useAuthStore.getState().currentUser === undefined) {
const replay = await db.replays.get(id)

if (replay === undefined) {
throw new Error("Replay not found")
}

return replay
}

const response = await rpc.replay.$get({
query: {
replayId: id,
},
})

if (!response.ok) {
throw new Error("Failed to fetch replay")
}

const replay = await response.json()

db.replays.put(replay)

return replay
}

async list(worldname: string): Promise<ExReplaySummaryDTO[]> {
if (useAuthStore.getState().currentUser === undefined) {
Expand Down Expand Up @@ -64,32 +122,42 @@ export class ReplayService {
}))
}

async get(id: string): Promise<ReplayDTO> {
if (useAuthStore.getState().currentUser === undefined) {
const replay = await db.replays.get(id)
async sync() {
if (authService.getState() !== "authenticated") {
return
}

if (replay === undefined) {
throw new Error("Replay not found")
for (const pendingReplay of await db.pendingReplays.toArray()) {
try {
await this.upload(pendingReplay.hash)
} catch (e) {
console.error(e)
}
}
}

return replay
async upload(replayHash: string) {
const pendingReplay = await db.pendingReplays.get(replayHash)

if (pendingReplay === undefined) {
return
}

const response = await rpc.replay.$get({
query: {
replayId: id,
const response = await rpc.replay.$post({
json: {
worldname: pendingReplay.worldname,
gamemode: pendingReplay.gamemode,
model: bytesToBase64(pendingReplay.model),
},
})

if (!response.ok) {
throw new Error("Failed to fetch replay")
if (response.ok === false) {
throw new Error("Failed to upload replay")
}

const replay = await response.json()
db.pendingReplays.delete(replayHash)

db.replays.put(replay)

return replay
return await response.json()
}
}

Expand Down
4 changes: 4 additions & 0 deletions packages/web/src/common/services/world-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export class WorldService {
replayAvailable: true,
}))
}

async get(worldname: string) {
return await db.worlds.where({ worldname }).first()
}
}

export const worldService = new WorldService()
4 changes: 2 additions & 2 deletions packages/web/src/common/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { create } from "zustand"
import { AlertProps } from "../components/layout/alerts/alert-props"
import { ModalProps } from "../components/modals/modal-props"

export interface Store {
export interface GlobalStore {
alerts: AlertProps[]
newAlert: (alert: AlertProps, time?: number) => void

Expand All @@ -15,7 +15,7 @@ export interface Store {
setCurrentUser(user?: UserDTO): void
}

export const useStore = create<Store>(set => ({
export const useGlobalStore = create<GlobalStore>(set => ({
alerts: [],
newAlert: (alert: AlertProps, time?: number) => {
setTimeout(() => {
Expand Down
Loading

0 comments on commit 28da639

Please sign in to comment.