diff --git a/packages/client/src/components/Toolbar/CanvasMeta.tsx b/packages/client/src/components/Toolbar/CanvasMeta.tsx index 78554a6fc0746871ded81bafffa8f239048978e2..8215393a9b25c9de39961ad392a07458b0780d40 100644 --- a/packages/client/src/components/Toolbar/CanvasMeta.tsx +++ b/packages/client/src/components/Toolbar/CanvasMeta.tsx @@ -12,14 +12,23 @@ import { useEffect, useState } from "react"; import { type ClientConfig } from "@sc07-canvas/lib/net"; import network from "../../lib/network"; -const getTimeLeft = (pixels: { available: number }, config: ClientConfig) => { - // this implementation matches the server's implementation +const getTimeLeft = (available: number, config: ClientConfig) => { + /** + * when the user last placed a pixel (unix ts) + */ + const lastPlace = CanvasCore.get().lastPlace; - const cooldown = CanvasLib.getPixelCooldown(pixels.available + 1, config); - const pixelExpiresAt = - CanvasCore.get().lastPlace && CanvasCore.get().lastPlace! + cooldown * 1000; + if (!lastPlace) return undefined; - const pixelCooldown = pixelExpiresAt && (Date.now() - pixelExpiresAt) / 1000; + const cooldown = CanvasLib.getPixelCooldown(available + 1, config); + const pixelAvailableAt = + lastPlace + + // include already collected pixels + cooldown * 1000 * available + + // pixel we're currently waiting for + cooldown * 1000; + + const pixelCooldown = (Date.now() - pixelAvailableAt) / 1000; if (!pixelCooldown) return undefined; if (pixelCooldown > 0) return 0; @@ -27,24 +36,24 @@ const getTimeLeft = (pixels: { available: number }, config: ClientConfig) => { return Math.abs(pixelCooldown).toFixed(1); }; -const PlaceCountdown = () => { - const { pixels, config } = useAppContext(); - const [timeLeft, setTimeLeft] = useState(getTimeLeft(pixels, config)); +const PlaceCountdown = ({ available }: { available: number }) => { + const { config } = useAppContext(); + const [timeLeft, setTimeLeft] = useState(getTimeLeft(available, config)); useEffect(() => { const timer = setInterval(() => { - setTimeLeft(getTimeLeft(pixels, config)); + setTimeLeft(getTimeLeft(available, config)); }, 100); return () => { clearInterval(timer); }; - }, [pixels]); + }, [available]); return ( <> {timeLeft - ? pixels.available < config.canvas.pixel.maxStack && timeLeft + "s" + ? available < config.canvas.pixel.maxStack && timeLeft + "s" : ""} ); @@ -70,9 +79,24 @@ const OnlineCount = () => { }; export const CanvasMeta = () => { - const { canvasPosition, cursor, pixels, config } = useAppContext(); + const [available, setAvailable] = useState(-1); + const { canvasPosition, cursor, config } = useAppContext(); const { isOpen, onOpen, onOpenChange } = useDisclosure(); + useEffect(() => { + const handle = ({ available }: { available: number }) => { + console.log("canvasmeta", available); + setAvailable(available); + }; + + network.on("pixels", handle); + network.waitForState("pixels").then(([data]) => handle(data)); + + return () => { + network.off("pixels", handle); + }; + }, []); + return ( <>
@@ -94,9 +118,9 @@ export const CanvasMeta = () => { Pixels:{" "} - {pixels.available}/{config.canvas.pixel.maxStack} + {available}/{config.canvas.pixel.maxStack} {" "} - + Users Online:{" "} diff --git a/packages/client/src/contexts/AppContext.tsx b/packages/client/src/contexts/AppContext.tsx index 63a34265f68baa41a5ab79323799abc2440af4bf..a54a169c3300c286c4a0f6d7450c5db25cb0d1bc 100644 --- a/packages/client/src/contexts/AppContext.tsx +++ b/packages/client/src/contexts/AppContext.tsx @@ -52,7 +52,6 @@ interface IAppContext { setCanvasPosition: (v: ICanvasPosition) => void; cursor: ICursor; setCursorPos: (x: number | undefined, y: number | undefined) => void; - pixels: { available: number }; undo?: { available: true; expireAt: number }; loadChat: boolean; @@ -125,7 +124,6 @@ export const AppContext = ({ children }: PropsWithChildren) => { // --- settings --- const [loadChat, _setLoadChat] = useState(false); - const [pixels, setPixels] = useState({ available: 0 }); const [undo, setUndo] = useState<{ available: true; expireAt: number }>(); // overlays visible @@ -221,10 +219,6 @@ export const AppContext = ({ children }: PropsWithChildren) => { setAuth(user); } - function handlePixels(pixels: { available: number }) { - setPixels(pixels); - } - function handleUndo( data: { available: false } | { available: true; expireAt: number }, ) { @@ -245,8 +239,6 @@ export const AppContext = ({ children }: PropsWithChildren) => { Network.on("user", handleUser); Network.on("config", handleConfig); - Network.waitForState("pixels").then(([data]) => handlePixels(data)); - Network.on("pixels", handlePixels); Network.on("undo", handleUndo); Network.on("connected", handleConnect); @@ -267,7 +259,6 @@ export const AppContext = ({ children }: PropsWithChildren) => { Network.off("user", handleUser); Network.off("config", handleConfig); - Network.off("pixels", handlePixels); Network.off("undo", handleUndo); Network.off("connected", handleConnect); Network.off("disconnected", handleDisconnect); @@ -297,7 +288,6 @@ export const AppContext = ({ children }: PropsWithChildren) => { setCanvasPosition, cursor, setCursorPos, - pixels, undo, loadChat, setLoadChat, @@ -315,7 +305,7 @@ export const AppContext = ({ children }: PropsWithChildren) => { }} > {!config && ( -
+
)} diff --git a/packages/client/src/lib/canvas.ts b/packages/client/src/lib/canvas.ts index 8ad4e3ff3ab34559219e64bc3fd8d5c0daaee8cd..cc8d148dcab05fe758a35dbc534b1bc58612a002 100644 --- a/packages/client/src/lib/canvas.ts +++ b/packages/client/src/lib/canvas.ts @@ -57,6 +57,9 @@ export class CanvasCore extends EventEmitter { Network.on("pixel", this.handlePixel); Network.on("square", this.handleSquare); Network.on("pixelLastPlaced", this.handlePixelLastPlaced); + Network.waitForState("pixelLastPlaced").then(([time]) => + this.handlePixelLastPlaced(time), + ); KeybindManager.on("SNAPSHOT", () => this.snapshot()); } @@ -412,6 +415,7 @@ export class CanvasCore extends EventEmitter { .then((ack) => { if (ack.success) { this.handlePixel(ack.data); + this.lastPlace = Date.now(); Network.emit("placeSuccessful"); } else { console.warn( diff --git a/packages/server/src/controllers/SocketController.ts b/packages/server/src/controllers/SocketController.ts index dd7b253e240032e9033d8c4950f35640d535f7eb..8b7e213fcc130d120525d6211d4eaff01c53fb67 100644 --- a/packages/server/src/controllers/SocketController.ts +++ b/packages/server/src/controllers/SocketController.ts @@ -30,6 +30,10 @@ const Logger = getLogger("SOCKET"); export type Socket = RawSocket; export type RemoteSocket = RawRemoteSocket; +interface ServerToServerEvents { + userStackUpdated: (sub: string, stack: number) => void; +} + interface Hooks { socketConnection: (socket: Socket) => void; } @@ -42,7 +46,7 @@ export class SocketController { socketConnection: [], }; private static debugHooks: { [k: string]: (...rest: any[]) => void } = {}; - io: Server; + io: Server; MOD_NS: Namespace; private constructor(server?: http.Server, session?: ISessionProvider) { @@ -59,6 +63,15 @@ export class SocketController { this.io.engine.use(session); this.io.on("connection", this.handleConnection.bind(this)); + this.io.on("userStackUpdated", async (sub, stack) => { + // the worker process handles stack updates + // this will get broadcasted and we should assume it's what the database reflects + // we're updating this property directly instead of using #modifyStack as the worker already let the client know + // this is mainly used for correctly syncing this number to the client on initial sync + const user = await User.fromSub(sub); + user.pixelStack = stack; + }); + this.MOD_NS = this.io.of("/mod"); this.MOD_NS.use(async (socket, next) => { const user = await this.getUserFromSocket(socket, true); @@ -329,6 +342,9 @@ export class SocketController { new Date(Date.now() + CONFIG.canvas.undo.grace_period), ); + // flag the user placed (used for next available pixel countdown) + await user.markPlaced(); + const newPixel: Pixel = { x: pixel.x, y: pixel.y, diff --git a/packages/server/src/jobs/networking.ts b/packages/server/src/jobs/networking.ts index b07a73383c776632bd0fa7558ccbc60d49564922..f97399b02f885273262766258548038ef7fb846a 100644 --- a/packages/server/src/jobs/networking.ts +++ b/packages/server/src/jobs/networking.ts @@ -139,6 +139,8 @@ const runPixelStacking = async () => { continue; } + await user.update(true); + // time in seconds since last stack gain (including a potential bonus depending on previously remaining time) const timeSinceLastPlace = (Date.now() - user.lastTimeGainStarted.getTime()) / 1000; @@ -147,8 +149,6 @@ const runPixelStacking = async () => { clientConfig, ); - await user.update(true); - // this impl has the side affect of giving previously offline users all the stack upon reconnecting if ( timeSinceLastPlace >= cooldown && diff --git a/packages/server/src/models/User.ts b/packages/server/src/models/User.ts index 7159905ef5705b48160ccac8f01ef85ce4a97dcb..fd708f7ff2d74c8fedd18594fdcae11d7f7483f1 100644 --- a/packages/server/src/models/User.ts +++ b/packages/server/src/models/User.ts @@ -193,6 +193,14 @@ export class User { updatedUser.lastTimeGainStarted.getTime(), ); + // ensure all processes know about this update + // this event won't be handled by the current process, only others + SocketController.get().io.serverSideEmit( + "userStackUpdated", + this.sub, + updatedUser.pixelStack, + ); + // we just modified the user data, so we should force an update await this.update(true); } @@ -204,6 +212,19 @@ export class User { .io.to("user:" + this.sub) .emit(...args); + async markPlaced() { + await prisma.user.update({ + where: { + sub: this.sub, + }, + data: { + lastTimeGainStarted: new Date(), + }, + }); + + await this.update(); + } + /** * Set undoExpires in database and notify all user's sockets of undo ttl */