cocos-owop

A helpful script for Our World of Pixels

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name cocos-owop
// @namespace https://meowing.net
// @version 0.8
// @description A helpful script for Our World of Pixels
// @author catcake43
// @match https://ourworldofpixels.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=ourworldofpixels.com
// @grant none
// ==/UserScript==
window.addEventListener("load", () => {const OWOP = window.OWOP;OWOP.once(OWOP.events.misc.toolsInitialized, () => {
(() => {
  var __defProp = Object.defineProperty;
  var __getOwnPropNames = Object.getOwnPropertyNames;
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  var __hasOwnProp = Object.prototype.hasOwnProperty;
  function __accessProp(key) {
    return this[key];
  }
  var __toCommonJS = (from) => {
    var entry = (__moduleCache ??= new WeakMap).get(from), desc;
    if (entry)
      return entry;
    entry = __defProp({}, "__esModule", { value: true });
    if (from && typeof from === "object" || typeof from === "function") {
      for (var key of __getOwnPropNames(from))
        if (!__hasOwnProp.call(entry, key))
          __defProp(entry, key, {
            get: __accessProp.bind(from, key),
            enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
          });
    }
    __moduleCache.set(from, entry);
    return entry;
  };
  var __moduleCache;
  var __returnValue = (v) => v;
  function __exportSetter(name, newValue) {
    this[name] = __returnValue.bind(null, newValue);
  }
  var __export = (target, all) => {
    for (var name in all)
      __defProp(target, name, {
        get: all[name],
        enumerable: true,
        configurable: true,
        set: __exportSetter.bind(all, name)
      });
  };

  // src/index.ts
  var exports_src = {};
  __export(exports_src, {
    pool: () => pool,
    desync: () => desync,
    config: () => config
  });

  // src/utils.ts
  var CHUNK_SIZE = 16;
  var WORLD_POS_MULT = 16;
  function left(left2, ..._) {
    return left2;
  }

  class Pos {
    x;
    y;
    constructor(x, y) {
      this.x = x;
      this.y = y;
    }
    static fromWorldPos(worldX, worldY) {
      return new this(worldX / WORLD_POS_MULT, worldY / WORLD_POS_MULT);
    }
    static fromChunkPos(chunkX, chunkY) {
      return new this(chunkX * CHUNK_SIZE, chunkY * CHUNK_SIZE);
    }
    static chunkAligned(x, y) {
      return this.fromChunkPos(Math.floor(x / CHUNK_SIZE), Math.floor(y / CHUNK_SIZE));
    }
    get worldX() {
      return this.x * WORLD_POS_MULT;
    }
    get worldY() {
      return this.y * WORLD_POS_MULT;
    }
    set worldX(worldX) {
      this.x = worldX / WORLD_POS_MULT;
    }
    set worldY(worldY) {
      this.y = worldY / WORLD_POS_MULT;
    }
    get chunkX() {
      return this.x / CHUNK_SIZE;
    }
    get chunkY() {
      return this.y / CHUNK_SIZE;
    }
    set chunkX(chunkX) {
      this.x = chunkX * CHUNK_SIZE;
    }
    set chunkY(chunkY) {
      this.y = chunkY * CHUNK_SIZE;
    }
    get chunkXFloor() {
      return Math.floor(this.chunkX);
    }
    get chunkYFloor() {
      return Math.floor(this.chunkY);
    }
    add(pos) {
      return new Pos(this.x + pos.x, this.y + pos.y);
    }
    equals(pos) {
      return this.x === pos.x && this.y === pos.y;
    }
  }

  class Col {
    r;
    g;
    b;
    constructor(r, g, b) {
      this.r = r;
      this.g = g;
      this.b = b;
    }
    static fromArray(rgb) {
      return new this(rgb[0], rgb[1], rgb[2]);
    }
    static fromInt(rgb) {
      return new this(rgb & 255, rgb >> 8 & 255, rgb >> 16 & 255);
    }
    equals(col) {
      return this.r === col.r && this.g === col.g && this.b === col.b;
    }
    toInt() {
      return this.b << 16 | this.g << 8 | this.r;
    }
  }

  class ColAlpha extends Col {
    a;
    constructor(r, g, b, a) {
      super(r, g, b);
      this.a = a;
    }
    blendOn(bg) {
      const a = this.a / 255;
      const z = 1 - a;
      return new Col(Math.round(this.r * a + bg.r * z), Math.round(this.g * a + bg.g * z), Math.round(this.b * a + bg.b * z));
    }
    equals(col) {
      return this.r === col.r && this.g === col.g && this.b === col.b && this.a === col.a;
    }
  }

  class Bucket {
    rate;
    per;
    lastValue;
    lastDate;
    constructor(rate, per) {
      this.rate = rate;
      this.per = per;
      this.lastValue = 0;
      this.lastDate = Date.now();
    }
    get value() {
      return Math.min(this.lastValue + this.rate / this.per / 1000 * (Date.now() - this.lastDate), this.rate);
    }
    set value(value) {
      this.lastValue = value;
      this.lastDate = Date.now();
    }
  }

  // src/proto.ts
  class PacketWriter {
    dv;
    index = 0;
    constructor(buffer) {
      this.dv = new DataView(buffer);
    }
    writeArray(data) {
      new Uint8Array(this.dv.buffer).set(data, this.index);
      this.index += data.byteLength;
    }
    writeUint8(...data) {
      for (const d of data) {
        this.dv.setUint8(this.index++, d);
      }
    }
    writeUint16LE(...data) {
      for (const d of data) {
        this.dv.setUint16(this.index, d, true);
        this.index += 2;
      }
    }
    writeInt32LE(...data) {
      for (const d of data) {
        this.dv.setInt32(this.index, d, true);
        this.index += 4;
      }
    }
    done() {
      return this.index >= this.dv.byteLength;
    }
  }

  class PacketReader {
    dv;
    index = 0;
    constructor(buffer) {
      this.dv = new DataView(buffer);
    }
    readArray(length) {
      return left(new Uint8Array(this.dv.buffer.slice(this.index, this.index + length)), this.index += length);
    }
    readUint8(length) {
      return Array.from({ length }, () => this.dv.getUint8(this.index++));
    }
    readUint16LE(length) {
      return Array.from({ length }, () => left(this.dv.getUint16(this.index, true), this.index += 2));
    }
    readUint32LE(length) {
      return Array.from({ length }, () => left(this.dv.getUint32(this.index, true), this.index += 4));
    }
    readInt32LE(length) {
      return Array.from({ length }, () => left(this.dv.getInt32(this.index, true), this.index += 4));
    }
    readUint64LE(length) {
      return Array.from({ length }, () => left(this.dv.getBigUint64(this.index, true), this.index += 8));
    }
    done() {
      return this.index >= this.dv.byteLength;
    }
  }

  class PacketC2S {
  }

  class PacketC2SBinary extends PacketC2S {
  }
  class PacketS2C {
  }

  class PacketC2SJoinWorld extends PacketC2SBinary {
    data;
    constructor(name) {
      super();
      const data = new TextEncoder().encode(name);
      this.data = new ArrayBuffer(data.length + 2);
      const writer = new PacketWriter(this.data);
      writer.writeArray(data);
      writer.writeUint16LE(25565);
    }
  }

  class PacketC2SUpdatePixel extends PacketC2SBinary {
    data;
    constructor(x, y, r, g, b) {
      super();
      this.data = new ArrayBuffer(11);
      const writer = new PacketWriter(this.data);
      writer.writeInt32LE(x, y);
      writer.writeUint8(r, g, b);
    }
  }
  class PacketC2SSendUpdates extends PacketC2SBinary {
    data;
    constructor(x, y, r, g, b, tool) {
      super();
      this.data = new ArrayBuffer(12);
      const writer = new PacketWriter(this.data);
      writer.writeInt32LE(x, y);
      writer.writeUint8(r, g, b, tool);
    }
  }
  class PacketS2CSetId extends PacketS2C {
    id;
    constructor(reader) {
      super();
      [this.id] = reader.readUint32LE(1);
    }
  }

  class PacketS2CTeleport extends PacketS2C {
    x;
    y;
    constructor(reader) {
      super();
      [this.x, this.y] = reader.readInt32LE(2);
    }
  }

  class PacketS2CSetRank extends PacketS2C {
    rank;
    constructor(reader) {
      super();
      [this.rank] = reader.readUint8(1);
    }
  }

  class PacketS2CSetPQuota extends PacketS2C {
    rate;
    per;
    pmult;
    constructor(reader) {
      super();
      [this.rate, this.per] = reader.readUint16LE(2);
      this.pmult = reader.done() ? 1 : reader.readUint8(1)[0] / 10;
    }
  }

  class PacketS2CChunkProtected extends PacketS2C {
    cx;
    cy;
    newState;
    constructor(reader) {
      super();
      [this.cx, this.cy] = reader.readInt32LE(2);
      [this.newState] = reader.readUint8(1);
    }
  }

  class PacketS2CMaxCount extends PacketS2C {
    maxCount;
    constructor(reader) {
      super();
      [this.maxCount] = reader.readUint16LE(1);
    }
  }

  class PacketS2CDonUntil extends PacketS2C {
    donUntilTs;
    constructor(reader) {
      super();
      this.donUntilTs = Number(reader.readUint64LE(1)[0]);
    }
  }
  var parseS2C = (data) => {
    const reader = new PacketReader(data);
    return {
      c: [PacketS2CSetId, , , PacketS2CTeleport, PacketS2CSetRank, , PacketS2CSetPQuota, PacketS2CChunkProtected, PacketS2CMaxCount, PacketS2CDonUntil][reader.readUint8(1)[0]],
      reader
    };
  };

  // src/client.ts
  var WebSocket = OWOP.global.AnnoyingAPI.ws;
  class Client {
    ws;
    pos = new Pos(0, 0);
    col = new Col(0, 0, 0);
    tool = 0;
    bucket = new Bucket(0, 1);
    state = 0 /* Connecting */;
    id;
    constructor(url, world) {
      this.ws = new WebSocket(url);
      this.ws.binaryType = "arraybuffer";
      this.ws.addEventListener("open", () => {
        this.send(new PacketC2SJoinWorld(world));
        this.state = 1 /* Joining */;
      });
      this.ws.addEventListener("message", (event) => {
        if (event.data instanceof ArrayBuffer) {
          const { c, reader } = parseS2C(event.data);
          if (c === undefined)
            return;
          if (c === PacketS2CSetId) {
            const packet = new c(reader);
            this.id = packet.id;
            this.state = 2 /* Ready */;
          } else if (c === PacketS2CSetPQuota) {
            const packet = new c(reader);
            this.bucket.per = packet.per;
            this.bucket.rate = packet.rate;
            this.bucket.value = 0;
          }
        }
      });
      ((fn) => {
        this.ws.addEventListener("close", fn);
        this.ws.addEventListener("error", fn);
      })(() => {
        this.state = 3 /* Disconnected */;
      });
    }
    update(pos, col, tool) {
      pos ??= this.pos;
      col ??= this.col;
      tool ??= this.tool;
      this.send(new PacketC2SSendUpdates(pos.worldX, pos.worldY, col.r, col.g, col.b, tool));
      this.pos = pos;
      this.col = col;
      this.tool = tool;
    }
    setPixel(pos, col) {
      const oldPos = this.pos;
      const newPos = pos;
      const chunkSqDist = (newPos.chunkXFloor - oldPos.chunkXFloor) ** 2 + (newPos.chunkYFloor - oldPos.chunkYFloor) ** 2;
      const shouldMove = chunkSqDist >= 4 ** 2;
      if (shouldMove)
        this.update(newPos);
      this.send(new PacketC2SUpdatePixel(pos.x, pos.y, col.r, col.g, col.b));
      if (shouldMove && config.sneaky)
        this.update(oldPos);
      --this.bucket.value;
    }
    send(packet) {
      this.ws.send(packet.data);
    }
    destroy() {
      this.ws.close();
    }
  }

  // src/clientpool.ts
  class ClientPool {
    clients = new Set;
    chunkedQueue = [];
    constructor() {
      OWOP.on(OWOP.events.tick, () => {
        const task = this.chunkedQueue[0];
        if (task === undefined)
          return;
        const pos = Pos.fromChunkPos(task.x, task.y);
        const chunk = OWOP.misc.world.getChunkAt(task.x, task.y);
        for (;task.index < 256; ++task.index) {
          const dx = task.index % 16;
          const dy = Math.floor(task.index / 16);
          const newPos = new Pos(pos.x + dx, pos.y + dy);
          const newCol = new ColAlpha(task.data[task.index * 4], task.data[task.index * 4 + 1], task.data[task.index * 4 + 2], task.data[task.index * 4 + 3]);
          const pixel = OWOP.misc.world.getPixel(newPos.x, newPos.y);
          if (pixel === null)
            break;
          const bgCol = new Col(pixel[0], pixel[1], pixel[2]);
          const blendedCol = newCol.blendOn(bgCol);
          if (bgCol.equals(blendedCol))
            continue;
          const client = this.client;
          if (client === undefined)
            break;
          client.setPixel(newPos, blendedCol);
          chunk.update(newPos.x, newPos.y, blendedCol.toInt());
          desync.addPixel(newPos, bgCol);
        }
        if (task.index >= 256)
          this.chunkedQueue.shift();
        OWOP.emit(OWOP.events.renderer.updateChunk, OWOP.misc.world.getChunkAt(task.x, task.y));
      });
    }
    add(client) {
      this.clients.add(client);
    }
    get client() {
      for (const client of this.clients.values()) {
        if (client.state === 3 /* Disconnected */)
          client.destroy(), this.clients.delete(client);
        if (client.state === 2 /* Ready */ && client.bucket.value / client.bucket.rate > config.bucketThreshold)
          return client;
      }
    }
    queueImage(canvas, pos) {
      const context = canvas.getContext("2d");
      if (context === null)
        return;
      const [chunkX, chunkY] = [pos.chunkXFloor, pos.chunkYFloor];
      const chunkAligned = Pos.fromChunkPos(chunkX, chunkY);
      const offset = new Pos(pos.x - chunkAligned.x, pos.y - chunkAligned.y);
      const chunkWidth = Math.ceil((canvas.width + offset.x) / CHUNK_SIZE);
      const chunkHeight = Math.ceil((canvas.height + offset.y) / CHUNK_SIZE);
      for (let i = 0;i < chunkWidth; ++i) {
        for (let j = 0;j < chunkHeight; ++j) {
          const cx = chunkX + i;
          const cy = chunkY + j;
          const vPos = Pos.fromChunkPos(i, j);
          this.chunkedQueue.push({
            x: cx,
            y: cy,
            index: 0,
            data: new Uint8Array(context.getImageData(vPos.x - offset.x, vPos.y - offset.y, 16, 16).data)
          });
        }
      }
    }
  }

  // src/commands.ts
  var registerCommands = () => {
    const prefix = ".";
    const commands = {
      help() {
        OWOP.chat.local("Available commands: help, say, tp, tpto");
        return "";
      },
      say(args) {
        return args.join(" ");
      },
      tp(args) {
        const x = Number(args[0]);
        const y = Number(args[1]);
        if (Number.isNaN(x) || Number.isNaN(y))
          return OWOP.chat.local("Please provide valid x and y coordinates."), "";
        OWOP.camera.centerCameraTo(x, y);
        return "";
      },
      tpto(args) {
        const id = args[0];
        const player = OWOP.misc.world.players[id];
        if (player === undefined)
          return OWOP.chat.local("Player not found."), "";
        const pos = Pos.fromWorldPos(player._x.val, player._y.val);
        return commands.tp([String(pos.x), String(pos.y)]);
      }
    };
    const exec = (cmdObj, args) => {
      const cmd = cmdObj[args.shift() ?? ""];
      if (typeof cmd === "function")
        return cmd(args);
      if (typeof cmd === "object")
        return exec(cmd, args);
      OWOP.chat.local(`Unknown command. Try ${prefix}help or ${prefix}say [your message].`);
      return "";
    };
    const originalSendModifier = OWOP.chat.sendModifier;
    OWOP.chat.sendModifier = (msg, ...args) => {
      if (msg.startsWith(prefix)) {
        const full = msg.slice(prefix.length);
        const args2 = full.match(/[^\s"']+|"([^"]*)"/g) ?? [];
        return exec(commands, args2);
      }
      return originalSendModifier?.(msg, ...args) ?? msg;
    };
  };

  // src/config.ts
  class Config {
    sneaky;
    bucketThreshold;
    desyncTimeout;
    follow;
    followColor;
    followTool;
    followSteps;
    followRadius;
    constructor() {
      const item = localStorage.getItem("cocosconfig");
      const conf = item && JSON.parse(item);
      this.sneaky = conf?.sneaky ?? false;
      this.bucketThreshold = conf?.bucketThreshold ?? 0.5;
      this.desyncTimeout = conf?.desyncTimeout ?? 2000;
      this.follow = conf?.follow ?? "";
      this.followColor = conf?.followColor ?? false;
      this.followTool = conf?.followTool ?? false;
      this.followSteps = conf?.followSteps ?? 40;
      this.followRadius = conf?.followRadius ?? 10;
    }
    save() {
      localStorage.setItem("cocosconfig", JSON.stringify(this));
    }
  }

  // src/desync.ts
  class Desync {
    map = new Map;
    addPixel(pos, prevCol) {
      if (config.desyncTimeout < 1)
        return;
      this.removePixel(pos);
      const [cx, cy] = [pos.chunkXFloor, pos.chunkYFloor];
      this.map.set(`${pos.x},${pos.y}`, setTimeout(() => {
        OWOP.misc.world.getChunkAt(cx, cy).update(pos.x, pos.y, prevCol.toInt());
        OWOP.emit(OWOP.events.renderer.updateChunk, OWOP.misc.world.getChunkAt(cx, cy));
      }, config.desyncTimeout));
    }
    removePixel(pos) {
      const k = `${pos.x},${pos.y}`;
      const t = this.map.get(k);
      if (t === undefined)
        return;
      clearTimeout(t);
      this.map.delete(k);
    }
  }

  // src/follow.ts
  class Follow {
    _radius;
    _steps;
    step = 0;
    constructor(radius, steps) {
      this._radius = radius;
      this._steps = steps;
    }
    follow() {
      ++this.step;
      this.step %= this._steps;
      const pos = Pos.fromWorldPos(OWOP.mouse.worldX, OWOP.mouse.worldY);
      for (const [client, ps] of this.clients.entries()) {
        client.update(pos.add(ps[this.step]), config.followColor ? new Col(...OWOP.player.selectedColor) : undefined, config.followTool ? OWOP.player.toolId : undefined);
      }
    }
    isApplicable(pool) {
      const clients = [...pool.clients].filter((client) => client.state === 2 /* Ready */);
      if (clients.length !== this.clients.size)
        return false;
      if (!clients.every((client) => this.clients.has(client)))
        return false;
      return true;
    }
    get radius() {
      return this._radius;
    }
    get steps() {
      return this._steps;
    }
  }

  class FollowCircle extends Follow {
    clients;
    constructor(radius, steps, pool) {
      super(radius, steps);
      this.clients = new Map;
      const clients = [...pool.clients].filter((client) => client.state === 2 /* Ready */);
      const r = this.radius;
      const tau = Math.PI * 2;
      const diffClient = tau / clients.length;
      const diffStep = tau / this.steps;
      for (let i = 0;i < clients.length; ++i) {
        const ps = [];
        this.clients.set(clients[i], ps);
        for (let j = 0;j < this.steps; ++j) {
          const rad = diffClient * i + diffStep * j;
          const x = r * Math.cos(rad);
          const y = r * Math.sin(rad);
          ps.push(new Pos(x, y));
        }
      }
    }
  }

  class FollowAtom extends Follow {
    clients;
    constructor(radius, steps, pool) {
      super(radius, steps);
      this.clients = new Map;
      const clients = [...pool.clients].filter((client) => client.state === 2 /* Ready */);
      const tau = Math.PI * 2;
      const middle = clients.length / 2;
      const clients1 = clients.slice(middle);
      const clients2 = clients.slice(0, middle);
      const rx = this.radius * 0.6;
      const ry = this.radius * 1.4;
      const diffClient1 = tau / clients1.length;
      const diffClient2 = tau / clients2.length;
      const theta1 = Math.PI / 4;
      const theta2 = -Math.PI / 4;
      const sinTheta1 = Math.sin(theta1);
      const cosTheta1 = Math.cos(theta1);
      const sinTheta2 = Math.sin(theta2);
      const cosTheta2 = Math.cos(theta2);
      const diffStep = tau / this.steps;
      for (let i = 0;i < clients1.length; ++i) {
        const ps = [];
        this.clients.set(clients1[i], ps);
        for (let j = 0;j < this.steps; ++j) {
          const rad = diffClient1 * i + diffStep * j;
          const x = rx * Math.cos(rad) * cosTheta1 - ry * Math.sin(rad) * sinTheta1;
          const y = rx * Math.cos(rad) * sinTheta1 + ry * Math.sin(rad) * cosTheta1;
          ps.push(new Pos(x, y));
        }
      }
      for (let i = 0;i < clients2.length; ++i) {
        const ps = [];
        this.clients.set(clients2[i], ps);
        for (let j = 0;j < this.steps; ++j) {
          const rad = diffClient2 * i + diffStep * j;
          const x = rx * Math.cos(rad) * cosTheta2 - ry * Math.sin(rad) * sinTheta2;
          const y = rx * Math.cos(rad) * sinTheta2 + ry * Math.sin(rad) * cosTheta2;
          ps.push(new Pos(x, y));
        }
      }
    }
  }

  // src/implfollow.ts
  var follow = null;
  var tickFollow = () => {
    if (config.follow === "circle") {
      if (!(follow instanceof FollowCircle) || !follow.isApplicable(pool) || follow.steps !== config.followSteps || follow.radius !== config.followRadius)
        follow = new FollowCircle(config.followRadius, config.followSteps, pool);
      follow.follow();
    } else if (config.follow === "atom") {
      if (!(follow instanceof FollowAtom) || !follow.isApplicable(pool) || follow.steps !== config.followSteps || follow.radius !== config.followRadius)
        follow = new FollowAtom(config.followRadius, config.followSteps, pool);
      follow.follow();
    } else {
      follow = null;
    }
  };

  // src/gui.ts
  class OWOPWindow {
    window;
    constructor(title, closeable = false) {
      this.window = new OWOP.windowSys.class.window(title, { closeable }, () => {});
    }
    getContainer() {
      return this.window.container;
    }
    open() {
      OWOP.windowSys.addWindow(this.window);
    }
    close() {
      OWOP.windowSys.delWindow(this.window);
    }
  }

  class TabbedWindow extends OWOPWindow {
    currentTab = "";
    tabs = new Map;
    tabsContainer = document.createElement("div");
    contentContainer = document.createElement("div");
    constructor(title, closeable) {
      super(title, closeable);
      this.window.container.appendChild(this.tabsContainer);
      this.window.container.appendChild(this.contentContainer);
    }
    get tab() {
      return this.currentTab;
    }
    set tab(id) {
      const tab = this.tabs.get(id);
      if (tab === undefined)
        return;
      for (const element of this.contentContainer.children) {
        if (!(element instanceof HTMLElement))
          return;
        element.style.display = "none";
      }
      tab.container.style.display = "block";
      this.currentTab = tab.id;
    }
    addTab(id, name) {
      const container = document.createElement("div");
      this.contentContainer.appendChild(container);
      const button = document.createElement("button");
      button.textContent = name;
      button.addEventListener("click", () => this.tab = id);
      this.tabsContainer.appendChild(button);
      this.tabs.set(id, { id, name, button, container });
    }
    getTabContainer(id) {
      return this.tabs.get(id)?.container;
    }
  }

  // src/implgui.ts
  var clipboardCanvas = document.createElement("canvas");
  var tickGui = () => {};
  var buildWindowConnTab = (container, win) => {
    const headerDiv = document.createElement("div");
    const urlInput = document.createElement("input");
    urlInput.type = "url";
    urlInput.value = "wss://ourworldofpixels.com";
    const connsInput = document.createElement("input");
    connsInput.style.width = "50px";
    connsInput.type = "number";
    connsInput.min = "1";
    connsInput.value = "1";
    const connBtn = document.createElement("button");
    connBtn.textContent = "+";
    headerDiv.append(urlInput, connsInput, connBtn);
    const connsTableDiv = document.createElement("div");
    connsTableDiv.style.height = "250px";
    connsTableDiv.style.overflowY = "scroll";
    const connsTable = document.createElement("table");
    const thead = document.createElement("thead");
    thead.style.top = "0";
    const tr = document.createElement("tr");
    const columns = Array.from({ length: 6 }, () => document.createElement("th"));
    const selectAllInput = document.createElement("input");
    selectAllInput.type = "checkbox";
    columns[0].append(selectAllInput);
    columns[0].style.width = "15px";
    columns[1].textContent = "?";
    columns[1].style.width = "15px";
    columns[2].textContent = "id";
    columns[2].style.width = "50px";
    columns[3].textContent = "x";
    columns[3].style.width = "50px";
    columns[4].textContent = "y";
    columns[4].style.width = "50px";
    columns[5].textContent = "bucket";
    columns[5].style.width = "50px";
    tr.append(...columns);
    thead.append(tr);
    const tbody = document.createElement("tbody");
    connsTable.append(thead, tbody);
    connsTableDiv.append(connsTable);
    const footerDiv = document.createElement("div");
    const dcBtn = document.createElement("button");
    dcBtn.textContent = "Disconnect";
    footerDiv.append(dcBtn);
    const statusDiv = document.createElement("div");
    container.append(headerDiv, connsTableDiv, footerDiv, statusDiv);
    connBtn.addEventListener("click", () => {
      const url = urlInput.value;
      const conns = Number(connsInput.value);
      for (let i = 0;i < conns; ++i) {
        pool.add(new Client(url, OWOP.world.name));
      }
    });
    const rowMap = new Map;
    selectAllInput.addEventListener("change", () => {
      for (const row of rowMap.values()) {
        row.checkbox.checked = selectAllInput.checked;
      }
    });
    dcBtn.addEventListener("click", () => {
      for (const [client, row] of rowMap.entries()) {
        if (!row.checkbox.checked)
          continue;
        client.destroy();
        pool.clients.delete(client);
      }
    });
    tickGui = () => {
      if (win.tab !== "conn")
        return;
      const clients = [...pool.clients];
      const readyClients = clients.filter((c) => c.state === 2 /* Ready */);
      statusDiv.textContent = `${readyClients.length} | ${clients.filter((c) => c.state !== 3 /* Disconnected */).length} | ${readyClients.reduce((a, b) => a + b.bucket.value, 0).toFixed(0)}`;
      for (const [client, { row, columns: columns2 }] of rowMap.entries()) {
        if (!pool.clients.has(client)) {
          row.remove();
          rowMap.delete(client);
          continue;
        }
        columns2[1].textContent = (() => {
          switch (client.state) {
            case 0 /* Connecting */:
              return "\uD83C\uDF10";
            case 1 /* Joining */:
              return "⏳";
            case 2 /* Ready */:
              return "✅️";
            case 3 /* Disconnected */:
              return "❌";
          }
        })();
        columns2[2].textContent = client.id ? String(client.id) : "-";
        columns2[3].textContent = client.pos.x.toFixed(2);
        columns2[4].textContent = client.pos.y.toFixed(2);
        columns2[5].textContent = client.bucket.value.toFixed(0);
      }
      if (pool.clients.size > rowMap.size) {
        for (const client of pool.clients.values()) {
          if (rowMap.has(client))
            continue;
          const tr2 = document.createElement("tr");
          const columns2 = Array.from({ length: 6 }, () => document.createElement("td"));
          const selectInput = document.createElement("input");
          selectInput.type = "checkbox";
          selectInput.checked = selectAllInput.checked;
          columns2[0].append(selectInput);
          tr2.append(...columns2);
          tbody.append(tr2);
          const row = { row: tr2, columns: columns2, checkbox: selectInput };
          rowMap.set(client, row);
          selectInput.addEventListener("change", () => {
            selectAllInput.checked = [...rowMap.values()].every((row2) => row2.checkbox.checked);
          });
        }
      }
    };
  };
  var buildWindowClipTab = (container) => {
    const clipboardInput = document.createElement("input");
    clipboardInput.type = "file";
    clipboardInput.accept = "image/*";
    clipboardInput.addEventListener("change", () => {
      if (clipboardInput.files === null)
        return;
      const context = clipboardCanvas.getContext("2d");
      if (context === null)
        return;
      const clipboard = clipboardInput.files[0];
      const url = URL.createObjectURL(clipboard);
      const image = new Image;
      image.src = url;
      image.addEventListener("load", () => {
        clipboardCanvas.width = image.width;
        clipboardCanvas.height = image.height;
        context.drawImage(image, 0, 0);
        URL.revokeObjectURL(url);
        clipboardInput.value = "";
      });
    });
    clipboardCanvas.width = 1;
    clipboardCanvas.height = 1;
    clipboardCanvas.style.display = "block";
    clipboardCanvas.style.maxWidth = "200px";
    clipboardCanvas.style.maxHeight = "200px";
    container.append(clipboardInput, clipboardCanvas);
  };
  var buildWindowConfTab = (container) => {
    const sneakyLabel = document.createElement("label");
    sneakyLabel.style.display = "block";
    const sneakyText = document.createTextNode("Sneaky ");
    const sneakyInput = document.createElement("input");
    sneakyInput.type = "checkbox";
    sneakyInput.checked = config.sneaky;
    sneakyLabel.append(sneakyText, sneakyInput);
    const bucketThresholdLabel = document.createElement("label");
    bucketThresholdLabel.style.display = "block";
    const bucketThresholdText = document.createTextNode("Bucket threshold ");
    const bucketThresholdInput = document.createElement("input");
    bucketThresholdInput.type = "number";
    bucketThresholdInput.min = "0";
    bucketThresholdInput.max = "1";
    bucketThresholdInput.step = "0.01";
    bucketThresholdInput.value = String(config.bucketThreshold);
    bucketThresholdLabel.append(bucketThresholdText, bucketThresholdInput);
    const desyncTimeoutLabel = document.createElement("label");
    desyncTimeoutLabel.style.display = "block";
    const desyncTimeoutText = document.createTextNode("Desync timeout ");
    const desyncTimeoutInput = document.createElement("input");
    desyncTimeoutInput.type = "number";
    desyncTimeoutInput.min = "0";
    desyncTimeoutInput.max = "1";
    desyncTimeoutInput.step = "0.01";
    desyncTimeoutInput.value = String(config.desyncTimeout);
    desyncTimeoutLabel.append(desyncTimeoutText, desyncTimeoutInput);
    const followLabel = document.createElement("label");
    followLabel.style.display = "block";
    const followText = document.createTextNode("Follow ");
    const followSelect = document.createElement("select");
    const followSelectOptions = Array.from({ length: 3 }, () => document.createElement("option"));
    followSelectOptions[0].textContent = "None";
    followSelectOptions[0].value = "";
    followSelectOptions[1].textContent = "Circle";
    followSelectOptions[1].value = "circle";
    followSelectOptions[2].textContent = "Atom";
    followSelectOptions[2].value = "atom";
    followSelect.append(...followSelectOptions);
    followSelect.value = config.follow;
    followLabel.append(followText, followSelect);
    const followColorLabel = document.createElement("label");
    followColorLabel.style.display = "block";
    const followColorText = document.createTextNode("Follow color ");
    const followColorInput = document.createElement("input");
    followColorInput.type = "checkbox";
    followColorInput.checked = config.followColor;
    followColorLabel.append(followColorText, followColorInput);
    const followToolLabel = document.createElement("label");
    followToolLabel.style.display = "block";
    const followToolText = document.createTextNode("Follow tool ");
    const followToolInput = document.createElement("input");
    followToolInput.type = "checkbox";
    followToolInput.checked = config.followTool;
    followToolLabel.append(followToolText, followToolInput);
    const followStepsLabel = document.createElement("label");
    followStepsLabel.style.display = "block";
    const followStepsText = document.createTextNode("Follow steps ");
    const followStepsInput = document.createElement("input");
    followStepsInput.type = "number";
    followStepsInput.min = "0";
    followStepsInput.value = String(config.followSteps);
    followStepsLabel.append(followStepsText, followStepsInput);
    const followRadiusLabel = document.createElement("label");
    followRadiusLabel.style.display = "block";
    const followRadiusText = document.createTextNode("Follow radius ");
    const followRadiusInput = document.createElement("input");
    followRadiusInput.type = "number";
    followRadiusInput.min = "0";
    followRadiusInput.value = String(config.followRadius);
    followRadiusLabel.append(followRadiusText, followRadiusInput);
    const btnsDiv = document.createElement("div");
    const saveBtn = document.createElement("button");
    saveBtn.textContent = "Save";
    btnsDiv.append(saveBtn);
    container.append(sneakyLabel, bucketThresholdLabel, desyncTimeoutLabel, followLabel, followColorLabel, followToolLabel, followStepsLabel, followRadiusLabel, btnsDiv);
    sneakyInput.addEventListener("change", () => {
      config.sneaky = sneakyInput.checked;
    });
    bucketThresholdInput.addEventListener("change", () => {
      config.bucketThreshold = Number(bucketThresholdInput.value);
    });
    desyncTimeoutInput.addEventListener("change", () => {
      config.desyncTimeout = Number(desyncTimeoutInput.value);
    });
    followSelect.addEventListener("change", () => {
      config.follow = followSelect.value;
    });
    followColorInput.addEventListener("change", () => {
      config.followColor = followColorInput.checked;
    });
    followToolInput.addEventListener("change", () => {
      config.followTool = followToolInput.checked;
    });
    followStepsInput.addEventListener("change", () => {
      config.followSteps = Number(followStepsInput.value);
    });
    followRadiusInput.addEventListener("change", () => {
      config.followRadius = Number(followRadiusInput.value);
    });
    saveBtn.addEventListener("click", () => {
      config.save();
    });
  };
  var buildWindow = () => {
    const win = new TabbedWindow("cocos");
    win.getContainer().style.width = "300px";
    win.addTab("conn", "Connections");
    win.addTab("clip", "Clipboard");
    win.addTab("conf", "Options");
    win.tab = "conn";
    buildWindowConnTab(win.getTabContainer("conn"), win);
    buildWindowClipTab(win.getTabContainer("clip"));
    buildWindowConfTab(win.getTabContainer("conf"));
    win.open();
  };

  // src/impltool.ts
  var buildTools = () => {
    OWOP.tools.addToolObject(new OWOP.tools.class("(o) Chunker", OWOP.cursors.erase, OWOP.fx.player.RECT_SELECT_ALIGNED(16), OWOP.RANK.NONE, (tool) => {
      let pos = new Pos(0, 0);
      let color = new Col(0, 0, 0);
      const tick = () => {
        const rgb = color.toInt();
        const chunk = OWOP.misc.world.getChunkAt(pos.chunkXFloor, pos.chunkYFloor);
        outer:
          for (let i = 0;i < 16; ++i) {
            for (let j = 0;j < 16; ++j) {
              const npos = new Pos(pos.x + i, pos.y + j);
              const pixel = OWOP.misc.world.getPixel(npos.x, npos.y);
              if (pixel === null)
                break outer;
              const pixelColor = new Col(pixel[0], pixel[1], pixel[2]);
              if (color.equals(pixelColor))
                continue;
              const client = pool.client;
              if (client === undefined)
                break outer;
              client.setPixel(npos, color);
              chunk.update(npos.x, npos.y, rgb);
              desync.addPixel(npos, pixelColor);
            }
          }
        OWOP.emit(OWOP.events.renderer.updateChunk, OWOP.misc.world.getChunkAt(pos.chunkXFloor, pos.chunkYFloor));
      };
      tool.setEvent("mousedown mousemove", (mouse) => {
        if (!(mouse.buttons & 3))
          return;
        pos = Pos.chunkAligned(OWOP.mouse.tileX, OWOP.mouse.tileY);
        color = Col.fromArray(mouse.buttons === 2 ? [255, 255, 255] : OWOP.player.selectedColor);
        tool.setEvent("tick", tick);
      });
      tool.setEvent("mouseup deselect", (mouse) => {
        tool.setEvent("tick", null);
      });
    }));
    OWOP.tools.addToolObject(new OWOP.tools.class("(o) Copy", OWOP.cursors.copy, OWOP.fx.player.NONE, OWOP.RANK.NONE, (tool) => {
      const drawText = (ctx, str, x, y, centered) => {
        ctx.strokeStyle = "#000000", ctx.fillStyle = "#FFFFFF", ctx.lineWidth = 2.5, ctx.globalAlpha = 0.5;
        if (centered) {
          x -= ctx.measureText(str).width >> 1;
        }
        ctx.strokeText(str, x, y);
        ctx.globalAlpha = 1;
        ctx.fillText(str, x, y);
      };
      tool.setFxRenderer((fx, ctx, time) => {
        if (!fx.extra.isLocalPlayer)
          return 1;
        const x = fx.extra.player.x;
        const y = fx.extra.player.y;
        const fxx = (Math.floor(x / 16) - OWOP.camera.x) * OWOP.camera.zoom;
        const fxy = (Math.floor(y / 16) - OWOP.camera.y) * OWOP.camera.zoom;
        const oldlinew = ctx.lineWidth;
        ctx.lineWidth = 1;
        if (tool.extra.end) {
          const s = tool.extra.start;
          const e = tool.extra.end;
          const x2 = (s[0] - OWOP.camera.x) * OWOP.camera.zoom + 0.5;
          const y2 = (s[1] - OWOP.camera.y) * OWOP.camera.zoom + 0.5;
          const w = e[0] - s[0];
          const h = e[1] - s[1];
          ctx.beginPath();
          ctx.rect(x2, y2, w * OWOP.camera.zoom, h * OWOP.camera.zoom);
          ctx.globalAlpha = 1;
          ctx.strokeStyle = "#FFFFFF";
          ctx.stroke();
          ctx.setLineDash([3, 4]);
          ctx.strokeStyle = "#000000";
          ctx.stroke();
          ctx.globalAlpha = 0.25 + Math.sin(time / 500) / 4;
          ctx.fillStyle = OWOP.renderer.patterns.unloaded;
          ctx.fill();
          ctx.setLineDash([]);
          const oldfont = ctx.font;
          ctx.font = "16px sans-serif";
          const txt = (!tool.extra.clicking ? "Right click to copy " : "") + "(" + Math.abs(w) + "x" + Math.abs(h) + ")";
          let txtx = window.innerWidth >> 1;
          let txty = window.innerHeight >> 1;
          txtx = Math.max(x2, Math.min(txtx, x2 + w * OWOP.camera.zoom));
          txty = Math.max(y2, Math.min(txty, y2 + h * OWOP.camera.zoom));
          drawText(ctx, txt, txtx, txty, true);
          ctx.font = oldfont;
          ctx.lineWidth = oldlinew;
          return 0;
        } else {
          ctx.beginPath();
          ctx.moveTo(0, fxy + 0.5);
          ctx.lineTo(window.innerWidth, fxy + 0.5);
          ctx.moveTo(fxx + 0.5, 0);
          ctx.lineTo(fxx + 0.5, window.innerHeight);
          ctx.globalAlpha = 1;
          ctx.strokeStyle = "#FFFFFF";
          ctx.stroke();
          ctx.setLineDash([3]);
          ctx.strokeStyle = "#000000";
          ctx.stroke();
          ctx.setLineDash([]);
          ctx.lineWidth = oldlinew;
          return 1;
        }
      });
      tool.extra.start = null;
      tool.extra.end = null;
      tool.extra.clicking = false;
      tool.setEvent("mousedown", (mouse, event) => {
        const s = tool.extra.start;
        const e = tool.extra.end;
        const isInside = () => {
          return mouse.tileX >= s[0] && mouse.tileX < e[0] && mouse.tileY >= s[1] && mouse.tileY < e[1];
        };
        if (mouse.buttons === 1 && !tool.extra.end) {
          tool.extra.start = [mouse.tileX, mouse.tileY];
          tool.extra.clicking = true;
          tool.setEvent("mousemove", (mouse2, event2) => {
            if (tool.extra.start && mouse2.buttons === 1) {
              tool.extra.end = [mouse2.tileX, mouse2.tileY];
              return 1;
            }
          });
          const finish = () => {
            tool.setEvent("mousemove mouseup deselect", null);
            tool.extra.clicking = false;
            const s2 = tool.extra.start;
            const e2 = tool.extra.end;
            if (e2) {
              if (s2[0] === e2[0] || s2[1] === e2[1]) {
                tool.extra.start = null;
                tool.extra.end = null;
              }
              if (s2[0] > e2[0]) {
                const tmp = e2[0];
                e2[0] = s2[0];
                s2[0] = tmp;
              }
              if (s2[1] > e2[1]) {
                const tmp = e2[1];
                e2[1] = s2[1];
                s2[1] = tmp;
              }
            }
            OWOP.renderer.render(OWOP.renderer.rendertype.FX);
          };
          tool.setEvent("deselect", finish);
          tool.setEvent("mouseup", (mouse2, event2) => {
            if (!(mouse2.buttons & 1)) {
              finish();
            }
          });
        } else if (mouse.buttons === 1 && tool.extra.end) {
          if (isInside()) {
            const offx = mouse.tileX;
            const offy = mouse.tileY;
            tool.setEvent("mousemove", (mouse2, event2) => {
              const dx = mouse2.tileX - offx;
              const dy = mouse2.tileY - offy;
              tool.extra.start = [s[0] + dx, s[1] + dy];
              tool.extra.end = [e[0] + dx, e[1] + dy];
            });
            const end = () => {
              tool.setEvent("mouseup deselect mousemove", null);
            };
            tool.setEvent("deselect", end);
            tool.setEvent("mouseup", (mouse2, event2) => {
              if (!(mouse2.buttons & 1)) {
                end();
              }
            });
          } else {
            tool.extra.start = null;
            tool.extra.end = null;
          }
        } else if (mouse.buttons === 2 && tool.extra.end && isInside()) {
          tool.extra.start = null;
          tool.extra.end = null;
          const x = s[0];
          const y = s[1];
          const w = e[0] - s[0];
          const h = e[1] - s[1];
          const c = clipboardCanvas;
          c.width = w;
          c.height = h;
          const ctx = c.getContext("2d");
          if (ctx === null)
            return;
          const d = ctx.createImageData(w, h);
          for (let i = y;i < y + h; i++) {
            for (let j = x;j < x + w; j++) {
              const pix = OWOP.misc.world.getPixel(j, i);
              if (!pix)
                continue;
              d.data[4 * ((i - y) * w + (j - x))] = pix[0];
              d.data[4 * ((i - y) * w + (j - x)) + 1] = pix[1];
              d.data[4 * ((i - y) * w + (j - x)) + 2] = pix[2];
              d.data[4 * ((i - y) * w + (j - x)) + 3] = 255;
            }
          }
          ctx.putImageData(d, 0, 0);
          OWOP.player.tool = "(o) paste";
        }
      });
    }));
    OWOP.tools.addToolObject(new OWOP.tools.class("(o) Paste", OWOP.cursors.paste, OWOP.fx.player.NONE, OWOP.RANK.NONE, (tool) => {
      tool.setFxRenderer((fx, ctx, time) => {
        const z = OWOP.camera.zoom;
        const x = fx.extra.player.x;
        const y = fx.extra.player.y;
        const fxx = Math.floor(x / 16) - OWOP.camera.x;
        const fxy = Math.floor(y / 16) - OWOP.camera.y;
        const q = pool.chunkedQueue;
        if (q.length) {
          const cs = 16;
          ctx.strokeStyle = "#000000";
          ctx.globalAlpha = 0.8;
          ctx.beginPath();
          for (let i = 0;i < q.length; i++) {
            ctx.rect((q[i].x * cs - OWOP.camera.x) * z, (q[i].y * cs - OWOP.camera.y) * z, z * cs, z * cs);
          }
          ctx.stroke();
          return 0;
        }
        if (clipboardCanvas && fx.extra.isLocalPlayer) {
          ctx.globalAlpha = 0.5 + Math.sin(time / 500) / 4;
          ctx.strokeStyle = "#000000";
          ctx.scale(z, z);
          ctx.drawImage(clipboardCanvas, fxx, fxy);
          ctx.scale(1 / z, 1 / z);
          ctx.globalAlpha = 0.8;
          ctx.strokeRect(fxx * z, fxy * z, clipboardCanvas.width * z, clipboardCanvas.height * z);
          return 0;
        }
      });
      tool.setEvent("mousedown", (mouse) => {
        if (mouse.buttons & 1) {
          pool.queueImage(clipboardCanvas, new Pos(OWOP.mouse.tileX, OWOP.mouse.tileY));
        } else if (mouse.buttons & 2) {
          pool.chunkedQueue = [];
        }
      });
    }));
  };

  // src/index.ts
  var config = new Config;
  var desync = new Desync;
  var pool = new ClientPool;
  buildWindow();
  buildTools();
  registerCommands();
  OWOP.on(OWOP.events.tick, () => {
    tickGui();
    tickFollow();
  });
  OWOP.on(OWOP.events.net.sec.rank, () => {
    OWOP.showPlayerList(true);
  });
  OWOP.on(OWOP.events.net.world.tilesUpdated, (updates) => {
    for (const update of updates) {
      desync.removePixel(new Pos(update.x, update.y));
    }
  });
})();
});});