cocos-owop

A helpful script for Our World of Pixels

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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));
    }
  });
})();
});});