Greasy Fork is available in English.
A helpful script for Our World of Pixels
// ==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));
}
});
})();
});});