Skip to content
Canvas.ts 5.33 KiB
Newer Older
Grant's avatar
Grant committed
import { CanvasConfig } from "@sc07-canvas/lib/src/net";
Grant's avatar
Grant committed
import { prisma } from "./prisma";
import { Redis } from "./redis";
Grant's avatar
Grant committed
import { SocketServer } from "./SocketServer";
import { Logger } from "./Logger";
Grant's avatar
Grant committed

class Canvas {
Grant's avatar
Grant committed
  /**
   * Size of the canvas
   */
  private canvasSize: [width: number, height: number];
Grant's avatar
Grant committed

  constructor() {
Grant's avatar
Grant committed
    this.canvasSize = [100, 100];
Grant's avatar
Grant committed
  }

Grant's avatar
Grant committed
  getCanvasConfig(): CanvasConfig {
Grant's avatar
Grant committed
    return {
Grant's avatar
Grant committed
      size: this.canvasSize,
Grant's avatar
Grant committed
      zoom: 7,
Grant's avatar
Grant committed
      pixel: {
Grant's avatar
Grant committed
        cooldown: 10,
Grant's avatar
Grant committed
        multiplier: 3,
        maxStack: 6,
      },
Grant's avatar
Grant committed
      undo: {
        grace_period: 5000,
      },
Grant's avatar
Grant committed
    };
  }

Grant's avatar
Grant committed
  /**
   * Change size of the canvas
   *
   * Expensive task, will take a bit
   *
   * @param width
   * @param height
   */
  async setSize(width: number, height: number) {
    Logger.info("Canvas#setSize has started", {
      old: this.canvasSize,
      new: [width, height],
    });

Grant's avatar
Grant committed
    this.canvasSize = [width, height];
    await prisma.setting.upsert({
      where: { key: "canvas.size" },
      create: {
        key: "canvas.size",
        value: JSON.stringify({ width, height }),
      },
      update: {
        key: "canvas.size",
        value: JSON.stringify({ width, height }),
      },
    });
Grant's avatar
Grant committed

    // we're about to use the redis keys, make sure they are all updated
    await this.pixelsToRedis();
    // the redis key is 1D, since the dimentions changed we need to update it
    await this.canvasToRedis();

    // announce the new config, which contains the canvas size
    SocketServer.instance.broadcastConfig();

    // announce new pixel array that was generated previously
    await this.getPixelsArray().then((pixels) => {
      SocketServer.instance.io.emit("canvas", pixels);
    });

    Logger.info("Canvas#setSize has finished");
Grant's avatar
Grant committed
  /**
   * Latest database pixels -> Redis
   */
  async pixelsToRedis() {
    const redis = await Redis.getClient();
Grant's avatar
Grant committed

Grant's avatar
Grant committed
    const key = Redis.keyRef("pixelColor");
Grant's avatar
Grant committed

Grant's avatar
Grant committed
    for (let x = 0; x < this.canvasSize[0]; x++) {
      for (let y = 0; y < this.canvasSize[1]; y++) {
Grant's avatar
Grant committed
        const pixel = await prisma.pixel.findFirst({
          where: {
            x,
            y,
          },
          orderBy: [
            {
              createdAt: "asc",
            },
          ],
        });

        await redis.set(key(x, y), pixel?.color || "transparent");
      }
    }
  }

  /**
   * Redis pixels -> single Redis comma separated list of hex
   * @returns 1D array of pixel values
   */
  async canvasToRedis() {
    const redis = await Redis.getClient();
Grant's avatar
Grant committed

    const pixels: string[] = [];
Grant's avatar
Grant committed

    // (y -> x) because of how the conversion needs to be done later
    // if this is inverted, the map will flip when rebuilding the cache (5 minute expiry)
    // fixes #24
Grant's avatar
Grant committed
    for (let y = 0; y < this.canvasSize[1]; y++) {
      for (let x = 0; x < this.canvasSize[0]; x++) {
Grant's avatar
Grant committed
        pixels.push(
Grant's avatar
Grant committed
          (await redis.get(Redis.key("pixelColor", x, y))) || "transparent"
Grant's avatar
Grant committed
    await redis.set(Redis.key("canvas"), pixels.join(","), { EX: 60 * 5 });
Grant's avatar
Grant committed

    return pixels;
  }

  /**
   * force an update at a specific position
   */
  async updateCanvasRedisAtPos(x: number, y: number) {
    const redis = await Redis.getClient();
Grant's avatar
Grant committed

    const pixels: string[] = (
Grant's avatar
Grant committed
      (await redis.get(Redis.key("canvas"))) || ""
    ).split(",");
Grant's avatar
Grant committed

Grant's avatar
Grant committed
    pixels[this.canvasSize[0] * y + x] =
Grant's avatar
Grant committed
      (await redis.get(Redis.key("pixelColor", x, y))) || "transparent";
Grant's avatar
Grant committed

Grant's avatar
Grant committed
    await redis.set(Redis.key("canvas"), pixels.join(","), { EX: 60 * 5 });
Grant's avatar
Grant committed
  }

  async getPixelsArray() {
    const redis = await Redis.getClient();
Grant's avatar
Grant committed

Grant's avatar
Grant committed
    if (await redis.exists(Redis.key("canvas"))) {
      const cached = await redis.get(Redis.key("canvas"));
Grant's avatar
Grant committed
      return cached!.split(",");
    }

    return await this.canvasToRedis();
  }

  async getPixel(x: number, y: number) {
    return await prisma.pixel.findFirst({
      where: {
        x,
        y,
      },
    });
  }

Grant's avatar
Grant committed
  async setPixel(user: { sub: string }, x: number, y: number, hex: string) {
    const redis = await Redis.getClient();
Grant's avatar
Grant committed

    await prisma.pixel.create({
      data: {
        userId: user.sub,
        color: hex,
        x,
        y,
      },
    });

    await prisma.user.update({
      where: { sub: user.sub },
      data: { lastPixelTime: new Date() },
    });

    await redis.set(Redis.key("pixelColor", x, y), hex);
Grant's avatar
Grant committed

    // maybe only update specific element?
    // i don't think it needs to be awaited
    await this.updateCanvasRedisAtPos(x, y);
  }
Grant's avatar
Grant committed

  /**
   * Force a pixel to be updated in redis
   * @param x
   * @param y
   */
  async refreshPixel(x: number, y: number) {
    const redis = await Redis.getClient();
    const key = Redis.key("pixelColor", x, y);

    // find if any pixels exist at this spot, and pick the most recent one
    const pixel = await prisma.pixel.findFirst({
      where: { x, y },
      orderBy: { createdAt: "desc" },
    });
    let paletteColorID = -1;

    // if pixel exists in redis
    if (pixel) {
      redis.set(key, pixel.color);
      paletteColorID = (await prisma.paletteColor.findFirst({
        where: { hex: pixel.color },
      }))!.id;
    } else {
      redis.del(key);
    }

    await this.updateCanvasRedisAtPos(x, y);

    // announce to everyone the pixel's color
    // using -1 if no pixel is there anymore
    SocketServer.instance.io.emit("pixel", {
      x,
      y,
      color: paletteColorID,
    });
  }
Grant's avatar
Grant committed
}

export default new Canvas();