Commit 3dcaa813 authored by Grant's avatar Grant
Browse files

chore: fix pixel stack issues (initial load, cooldown timer, stack max+1)

parent fbf0b1ca
Loading
Loading
Loading
Loading
Loading
+39 −15
Original line number Diff line number Diff line
@@ -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<true>();
  const [timeLeft, setTimeLeft] = useState(getTimeLeft(pixels, config));
const PlaceCountdown = ({ available }: { available: number }) => {
  const { config } = useAppContext<true>();
  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<true>();
  const [available, setAvailable] = useState(-1);
  const { canvasPosition, cursor, config } = useAppContext<true>();
  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 (
    <>
      <div id="canvas-meta" className="toolbar-box">
@@ -94,9 +118,9 @@ export const CanvasMeta = () => {
        <span>
          Pixels:{" "}
          <span>
            {pixels.available}/{config.canvas.pixel.maxStack}
            {available}/{config.canvas.pixel.maxStack}
          </span>{" "}
          <PlaceCountdown />
          <PlaceCountdown available={available} />
        </span>
        <span>
          Users Online:{" "}
+1 −11
Original line number Diff line number Diff line
@@ -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 && (
        <div className="fixed top-0 left-0 w-full h-full z-[49] backdrop-blur-sm bg-black/30 text-white flex items-center justify-center">
        <div className="fixed top-0 left-0 w-full h-full z-49 backdrop-blur-sm bg-black/30 text-white flex items-center justify-center">
          <Spinner label="Loading..." />
        </div>
      )}
+4 −0
Original line number Diff line number Diff line
@@ -57,6 +57,9 @@ export class CanvasCore extends EventEmitter<CanvasEvents> {
    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<CanvasEvents> {
      .then((ack) => {
        if (ack.success) {
          this.handlePixel(ack.data);
          this.lastPlace = Date.now();
          Network.emit("placeSuccessful");
        } else {
          console.warn(
+17 −1
Original line number Diff line number Diff line
@@ -30,6 +30,10 @@ const Logger = getLogger("SOCKET");
export type Socket = RawSocket<ClientToServerEvents, ServerToClientEvents>;
export type RemoteSocket = RawRemoteSocket<ServerToClientEvents, any>;

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<ClientToServerEvents, ServerToClientEvents>;
  io: Server<ClientToServerEvents, ServerToClientEvents, ServerToServerEvents>;
  MOD_NS: Namespace<ModClientToServerEvents, ModServerToClientEvents>;

  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,
+2 −2
Original line number Diff line number Diff line
@@ -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 &&
Loading