Florrkit - Hitboxes, petal particles & more for florr.io

Hitboxes, petal particles, inventory rarity counter & more for florr.io

// ==UserScript==
// @name         Florrkit - Hitboxes, petal particles & more for florr.io
// @namespace    http://tampermonkey.net/
// @version      0.5.1
// @description  Hitboxes, petal particles, inventory rarity counter & more for florr.io
// @author       zertalious
// @match        https://florr.io/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=florr.io
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_info
// @run-at       document-start
// ==/UserScript==

const PROD = true;
const WASM_URL = PROD ? 'https://florrkit.zertalious.com/out.wasm?v=' + GM_info.script.version : 'http://localhost:7700/wasm';

HTMLElement.prototype.insertBefore = new Proxy(HTMLElement.prototype.insertBefore, {
	apply(target, thisArgs, args) {
		if (args[0].src && args[0].src.indexOf('client.js') > -1) {
			Module.locateFile = () => WASM_URL;

			return new Promise(resolve => {
				const script = document.createElement('script');
				script.src = 'https://cdn.jsdelivr.net/gh/Qwokka/WAIL@9ed21abc43045e19f9b3756de109a6e361fb9292/wail.js';
				script.onload = resolve
				document.body.appendChild(script);
			}).then(() => Reflect.apply(target, thisArgs, args));
		}

		return Reflect.apply(...arguments);
	}
});

function ProxyFunction(object, key, callback) {
	const original = object[key];

	object[key] = function () {
		callback(this, arguments);
		return original.apply(this, arguments);
	}

	return object[key];
}

const CTX = CanvasRenderingContext2D.prototype;

ProxyFunction(CTX, 'fillRect', (ctx, args) => {
	if (settings.showRarityCount && ctx.fillStyle === '#5a9fdb') {
		drawRarityCount(ctx);
	}
});

function drawRarityCount(ctx) {
	const rarityCount = getRarityCount();

	const list = [];
	for (const rarity in rarityCount) {
		list.push([
			rarity, rarityCount[rarity]
		]);
	}

	if (list.length === 0) return;

	const margin = 10;
	const rowHeight = 24;

	const itemCount = list.length;
	const width = 280;
	const height = Math.ceil(itemCount / 2) * rowHeight + margin * 2;
	const r = 8;

	const matrix = ctx.getTransform();

	const {
		a: W,
		e: x,
		f: y
	} = matrix;

	ctx.save();
	ctx.setTransform(1, 0, 0, 1, 0, 0);
	ctx.translate(x + W / 2, y);

	const f = W / 500;
	ctx.scale(f, f);
	ctx.translate(-width / 2, -height - 3);

	ctx.beginPath();
	ctx.roundRect(0, 0, width, height + r, r);
	ctx.clip();

	ctx.fillStyle = '#4981b1';
	ctx.fill();

	ctx.translate(0, margin);

	for (let i = 0; i < itemCount; i++) {
		ctx.save();

		if (itemCount % 2 === 1 && i === 0) {
			ctx.translate(width / 2, rowHeight / 2);
		} else {
			ctx.translate(
				((i - (itemCount % 2)) % 2) * width / 2 + width / 4,
				Math.floor(((itemCount % 2) + i) / 2) * rowHeight + rowHeight / 2
			);
		}

		const [rarity, count] = list[i];
		const [name, color] = rarities[rarity];
		const text = count.toLocaleString('en-US') + ' ' + name;

		ctx.textAlign = 'center';
		ctx.textBaseline = 'middle';
		ctx.font = 'bolder 19px Ubuntu';
		ctx.fillStyle = color;
		ctx.strokeStyle = '#000';
		ctx.lineWidth = 1.9;

		ctx.strokeText(text, 0, 0);
		ctx.fillText(text, 0, 0);
		ctx.restore();
	}

	ctx.restore();
}

const rarities = [
	['Common', '#7EEF6D'],
	['Unusual', '#FFE65D'],
	['Rare', '#4D52E3'],
	['Epic', '#861FDE'],
	['Legendary', '#DE1F1F'],
	['Mythic', '#1FDBDE'],
	['Ultra', '#FF2B75'],
	['Super', '#2BFFA3'],
	['Unique', '#555555']
];

let INVENTORY_ADDRESS = -1;
const PETAL_COUNT = 106;
const RARITY_COUNT = 9;

function getRarityCount() {
	if (INVENTORY_ADDRESS <= -1) return {};

	const map = {};

	for (let petal = 1; petal <= PETAL_COUNT; petal++) {
		for (let rarity = 0; rarity < RARITY_COUNT; rarity++) {
			const offset = (petal * RARITY_COUNT + rarity) << 2;
			const stock = Module.HEAPU32[(INVENTORY_ADDRESS + offset) >> 2];

			if (stock > 0) {
				map[rarity] = (map[rarity] || 0) + stock;
			}
		}
	}

	return map;
}

function unlockAllPetals() {
	if (INVENTORY_ADDRESS <= -1) return;

	for (let petal = 1; petal <= PETAL_COUNT; petal++) {
		for (let rarity = 0; rarity < RARITY_COUNT; rarity++) {
			const offset = (petal * RARITY_COUNT + rarity) << 2;
			Module.HEAPU32[(INVENTORY_ADDRESS + offset) >> 2] = 1;
		}
	}
}

let canvas;
let ctx;

const FlorrkitImports = {
	print(n) {
		console.log('float: ' + n);
	},
	printInt(n) {
		console.log('int: ' + n);
	},
	drawCircle(world, entity, layer) {
		if (!settings.showHitbox) return;
		if (layer === 8) return; // petal drops

		const playerRarity = u32(u32(u32(entity, 72)), 68);
		if (playerRarity > 0) {
			const n = u8(playerRarity, 18);
			if (!settings.showPlayerHitbox) return;
		}

		const petalRarity = u32(u32(u32(entity, 72)), 84);
		if (petalRarity > 0) {
			const n = u8(petalRarity, 10);
			if (!settings.showPetalHitbox) return;
		}

		if (!playerRarity && !petalRarity) {
			if (!settings.showMobHitbox) return;
		}

		const isDead = u8(u32(entity, 76), 8) === 1;
		if (isDead) return;

		ctx.save();

		ctx.setTransform(
			f32(world, 0),
			f32(world, 4),
			f32(world, 8),
			f32(world, 12),
			f32(world, 16),
			f32(world, 20)
		);

		const pos = u32(u32(u32(entity, 72)), 64);
		const x = f64(pos, 352);
		const y = f64(pos, 360);

		const size = f32(u32(entity, 72), 8);

		ctx.beginPath();
		ctx.arc(x, y, size, 0, Math.PI * 2);
		ctx.strokeStyle = hitboxColorEl.value;
		ctx.lineWidth = 2;
		ctx.stroke();

		ctx.restore();
	},
	getParticleMinRarity: () => settings.showParticles ? 0 : 7,
	showUniqueParticles: () => settings.showUniqueParticles ? 1 : 0, 
	shouldShowHealthBar: () => settings.showHealthBar ? 1 : 0, 
	alwaysShowPetalRarity: () => settings.alwaysShowPetalRarity ? 1 : 0
};

function u8(address, offset = 0) {
	return Module.HEAPU8[address + offset];
}
 
function u32(address, offset = 0) {
	return Module.HEAPU32[(address + offset) >> 2];
}
 
function f32(address, offset = 0) {
	return Module.HEAPF32[(address + offset) >> 2];
}
 
function f64(address, offset = 0) {
	return Module.HEAPF64[(address + offset) >> 3];
}

const _instantiateStreaming = WebAssembly.instantiateStreaming;
WebAssembly.instantiateStreaming = function () {
	return _instantiateStreaming(new Response());
}

const _instantiate = WebAssembly.instantiate;
WebAssembly.instantiate = function (buffer, imports) {
	console.log('wasm overloaded!');
	settingsBtnEl.style.background = '';

	imports.florrkit = FlorrkitImports;

	const bytes = new Uint8Array(buffer);

	find(bytes, [
		OP_I32_AND,
		OP_I32_CONST, ...VarSint32ToArray(8),
		OP_I32_SHR_U,
		OP_I32_ADD,
		OP_I32_CONST, ...VarSint32ToArray(2),
		OP_I32_SHL,
		OP_I32_CONST
	], (i, j) => {
	 	INVENTORY_ADDRESS = readInt32(bytes, j)[0];
		console.log('Inventory address: ' + INVENTORY_ADDRESS);
	});

	return _instantiate(buffer, imports);
}

function find(bytes, match, callback) {
	console.log(`>>> SEARCHING >>>\n${match}`);

	for (let i = 0; i < bytes.length; i++) {
		let skip = false;

		for (let j = 0; j < match.length; j++) {
			if (match[j] !== -1 && bytes[i + j] !== match[j]) {
				skip = true;
				break;
			}
		}

		if (skip) continue;

		callback(i, i + match.length);
	}
}

// https://leb128.com/script.js

function readInt32(bytes, i) {
	let result = 0;
	let shift = 0;
	for (; i < bytes.length; i++) {
		if (i === bytes.length - 1 && (bytes[i] & 0x80) !== 0) {
			throw new Error("Invalid LEB128 encoding: last byte must not have MSB set");
		}
		result |= (bytes[i] & 0x7F) << shift;
		if ((bytes[i] & 0x80) === 0) break;
		shift += 7;
	}
	return [result, i];
}

// ui

const settings = {
	showHitbox: true,
	showPlayerHitbox: true,
	showPetalHitbox: true,
	showMobHitbox: true, 
	showParticles: true,
	showUniqueParticles: true,
	showRarityCount: true, 
	showHealthBar: true, 
	alwaysShowPetalRarity: false
};

const Icons = {
	settings: `<svg height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 46.937 46.937" xml:space="preserve"> <g> <path style="fill:#fff;" d="M35.639,20.94c0,0,3.321-2.927,5.753-6.389c2.748-3.863,5.187-8.821,5.187-8.821 c0.516-1.614,0.607-2.964-0.63-3.852l-1.656-1.19c-1.237-0.891-2.602-0.336-3.852,0.627c0,0-4.085,3.948-6.771,7.684 c-2.686,3.734-4.168,7.529-4.168,7.529c-0.417,1.227-0.542,2.388,0.059,3.259l-0.592,0.839c-1.071-0.826-2.159-1.554-3.206-2.152 c-3.308-1.892-5.93-3.682-5.309-5.5l1.125-3.29c0.369-1.083-1.329-4.975-2.669-6.314C18.464,2.925,18,2.566,17.579,2.288 c-0.792-0.522-2.892-1.202-4.653-1.058c-1.763,0.144-3.437,0.473-3.688,0.675C9.071,2.041,8.962,2.202,8.92,2.382 c-0.077,0.323,0.068,0.69,0.415,1.034l3.03,3.02c1.066,1.072,1.063,2.815-0.005,3.886l-3.246,3.246 c-0.516,0.515-1.205,0.799-1.937,0.799c-0.741,0-1.432-0.285-1.951-0.806l-3.017-3.018c-0.347-0.345-0.713-0.492-1.036-0.416 c-0.179,0.043-0.339,0.152-0.473,0.32c-0.202,0.25-0.532,1.923-0.677,3.685c-0.145,1.763,0.433,3.854,0.874,4.636 c0.442,0.78,2.219,2.381,4.069,3.181c1.448,0.624,2.929,1.035,3.503,0.84c0,0,1.475-0.503,3.293-1.123 c1.819-0.621,3.843,2.156,5.951,5.614c0.958,1.573,2.126,3.157,3.419,4.448l-8.054,11.409c-0.637,0.902-0.422,2.15,0.48,2.787 c0.351,0.247,0.753,0.366,1.151,0.366c0.628,0,1.246-0.295,1.636-0.847l7.885-11.169c0.737,0.561,1.435,1.084,2.071,1.553 c2.175,1.604,3.98,2.932,3.974,3.048c-0.004,0.07-0.008,0.141-0.008,0.212c0,4.202,3.408,7.611,7.61,7.611 c4.203,0,7.613-3.409,7.613-7.611c0-4.204-3.408-7.611-7.61-7.611c-0.072,0-0.142,0.003-0.212,0.009 c-0.117,0.007-1.126-2.277-2.867-4.737c-0.75-1.06-1.696-2.229-2.839-3.425l0.935-1.324C33.796,22.092,34.72,21.634,35.639,20.94z M35.06,36.266c0.752-0.755,1.752-1.168,2.82-1.168c2.197,0,3.986,1.788,3.986,3.987c0,2.2-1.79,3.989-3.986,3.989 c-2.203,0-3.989-1.789-3.989-3.989C33.891,38.02,34.303,37.017,35.06,36.266z"/> </g> </svg>`
};

const div = fromHtml(`<div>
<style>

	[stroke] {
		--stroke-size: 0.15em;
		position: relative;
	}

	[stroke]:before {
		content: attr(stroke);
		-webkit-text-stroke: var(--stroke-size) #000;
	}

	[stroke]:after {
		content: attr(stroke);
		color: inherit;
		position: absolute;
		left: 0;
		top: 0;
	}

	body {
		margin: 0;
		padding: 0;
		overflow: hidden;
		font-family: Ubuntu;
		font-size: 11px;
		user-select: none;
		font-weight: bolder;
		-webkit-user-select: none;
		-moz-user-select: none;
		-ms-user-select: none;
		-webkit-touch-callout: none;
		touch-action: none;
		color: #fff;
	}

	.dialog {
		position: absolute;
		right: 56px;
		bottom: 8px;
		background: #bbb;
		padding: 5px;
		border: 4px solid rgba(0, 0, 0, 0.1);
		border-radius: 4px;
		width: 220px;
		display: flex;
		flex-direction: column;
		z-index: 2;
		max-height: 80%;
	}

	.dialog-header {
		--stroke-size: 0.12em;
		font-size: 19px;
		margin: 0 auto;
		margin-bottom: 8px;
	}

	.dialog-content {
		flex: 1;
		height: 100%;
		overflow-y: auto;
		padding: 0 5px;
		display: flex;
		flex-direction: column;
		grid-gap: 6px;
	}

	label {
		display: flex;
		grid-gap: 4px;
		align-items: center;
		cursor: pointer;
	}

	.checkbox {
		width: 20px;
		height: 20px;
		background: #777;
		margin: 0;
		border: 3px solid rgba(0, 0, 0, 0.2);
		border-radius: 2px;
		appearance: none;
		-webkit-appearance: none;
		position: relative;
		cursor: pointer;
		outline: 0;
	}

	.checkbox:after {
		content: ' ';
		position: absolute;
		left: 50%;
		top: 50%;
		width: 0;
		height: 0;
		background: #ccc;
		transition: all 0.2s;
		transform: translate(-50%, -50%);
	}

	.checkbox:checked:after {
		width: 100%;
		height: 100%;
	}

	.btn {
		cursor: pointer;
		display: grid;
		place-items: center;
		position: relative;
		overflow: hidden;
		border: 2px solid rgba(0, 0, 0, 0.2);
		border-radius: 5px;
		background: #bbb;
		padding: 3px 6px;
		white-space: nowrap;
		flex: 1;
	}

	.icon-btn {
		font-size: 26px;
		width: 1em;
		height: 1em;
		padding: 3px;
	}

	.icon-btn svg {
		width: 0.8em;
		height: 0.8em;
	}

	.close-btn {
		position: absolute;
		right: 5px;
		top: 5px;
		font-size: 12px;
		border-width: 3px;
		background: #d75658;
	}

	.cross {
		width: 100%;
		height: 100%;
		opacity: 0.6;
	}

	.cross:before, .cross:after {
		content: ' ';
		position: absolute;
		left: 50%;
		top: 50%;
		width: 80%;
		height: 15%;
		background: #fff;
		transform: translate(-50%, -50%) rotate(45deg);
		border-radius: 3px;
	}

	.cross:after {
		transform: translate(-50%, -50%) rotate(-45deg);
	}

	.overlay {
		position: absolute;
		left: 0;
		top: 0;
		width: 100%;
		height: 100%;
		pointer-events: none;
	}

	.overlay > * {
		pointer-events: all;
	}

	.settings-btn {
		position: absolute;
		right: 8px;
		bottom: 8px;
		font-size: 30px;
	}

	.colorpicker {
		appearance: none;
		-moz-appearance: none;
		-webkit-appearance: none;
		background: none;
		width: 1em;
		height: 1em;
		font-size: 17px;
		width: 2.45em;
		margin: 0;
		outline: 0;
		padding: 0;
		border: 2px solid #000;
		cursor: pointer;
	}

	::-webkit-color-swatch-wrapper {
		padding: 0;
	}

	::-webkit-color-swatch {
		border: 0;
		padding: 0;
		border-radius: inherit;
	}

</style>

<div class="overlay">
	<div class="dialog settings">
		<div class="btn icon-btn close-btn">
			<div class="cross"></div>
		</div>
		<div class="dialog-header" stroke="Florrkit v${GM_info.script.version}"></div>
		<div class="dialog-content">
			<settings></settings>
			<div style="display: flex; grid-gap: 5px; align-items: center;">
				<div stroke="Hitbox Color:"></div>
				<input type="color" class="colorpicker hitbox-color" value="${GM_getValue('hitboxColor', '#ff0000')}">
			</div>
			<div class="btn unlock-petals">
				<div stroke="Unlock All Petals"></div>
			</div>
			<div style="display: flex; grid-gap: 4px; justify-content: stretch">
				<div class="btn" data-link="https://discord.gg/JJFh7qzHDR">
					<div stroke="Discord"></div>
				</div>
				<div class="btn" data-link="https://zertalious.xyz">
					<div stroke="Website"></div>
				</div>
			</div>
			<div style="display: flex; flex-direction: column; align-items: center; text-align: center;">
				<div stroke="Created by Zertalious"></div>
			</div>
		</div>
	</div>

	<div class="btn icon-btn settings-btn" style="background: #fd6a6a">${Icons.settings}</div>
</div>

</div>`);

const overlayEl = div.querySelector('.overlay');
const settingsEl = div.querySelector('.settings');

const hitboxColorEl = div.querySelector('.hitbox-color');
hitboxColorEl.onchange = function () {
	GM_setValue('hitboxColor', this.value);
}

function updateScale() {
	const scale = Math.max(window.innerWidth / 1500, window.innerHeight / 700) * 1.10;

	Object.assign(overlayEl.style, {
		transformOrigin: '0 0',
		transform: `scale(${scale})`,
		width: window.innerWidth / scale + 'px',
		height: window.innerHeight / scale + 'px'
	});
}

div.querySelector('.unlock-petals').onclick = unlockAllPetals;

function initLinks() {
	const els = div.querySelectorAll('[data-link]');

	for (let i = 0; i < els.length; i++) {
		els[i].onclick = onClick;
	}

	function onClick() {
		window.open(this.getAttribute('data-link'), '_open');
	}
}

function initSettings() {
	const placeholder = div.querySelector('settings');

	for (const key in settings) {
		const el = fromHtml(`<label>
			<input type="checkbox" class="checkbox">
			<span stroke="${fromCamel(key)}"></span>
		</label>`);

		const checkboxEl = el.querySelector('.checkbox');
		settings[key] = GM_getValue(key, settings[key]);

		checkboxEl.checked = settings[key];
		checkboxEl.onchange = function () {
			settings[key] = this.checked;
			GM_setValue(key, settings[key]);
		}

		placeholder.parentNode.insertBefore(el, placeholder);
	}

	placeholder.remove();
}

const settingsBtnEl = div.querySelector('.settings-btn');

const settingsDialog = new Dialog(
	div.querySelector('.settings'),
	settingsBtnEl
);

function Dialog(el, btnEl) {
	let t = 0;
	let visible = false;

	this.update = function () {
		t += ((visible ? 1 : 0) - t) * 0.2;
		el.style.transform = `translateY(${(1 - t) * 150}%)`;
	}

	this.setVisible = function (v) {
		visible = v;
	}

	el.querySelector('.close-btn').onclick = function () {
		visible = false;
	}

	btnEl.onclick = function () {
		visible = !visible;
	}
}

function animate() {
	settingsDialog.update();
	window.requestAnimationFrame(animate);
}

initLinks();
initSettings();

updateScale();
window.addEventListener('resize', updateScale);

animate();

const interval = setInterval(() => {
	if (document.body) {
		clearInterval(interval);

		canvas = document.getElementById('canvas');
		ctx = canvas.getContext('2d');

		canvas.addEventListener('click', () => settingsDialog.setVisible(false));

		const host = fromHtml(`<div style="
			position: absolute;
			left: 0;
			top: 0;
			width: 100%;
			height: 100%;
			pointer-events: none;
			font-family: Ubuntu;
			color: white;
			font-weight: bolder;
			font-size: 11px;
		"></div>`);

		const shadow = host.attachShadow({ mode: 'closed' });

		while (div.children.length > 0) {
			shadow.appendChild(div.children[0]);
		}

		document.body.appendChild(host);
	}
}, 0);

function fromHtml(html) {
	const div = document.createElement('div');
	div.innerHTML = html;
	return div.children[0];
}

function fromCamel(text){
	const result = text.replace(/([A-Z])/g,' $1');
	return result.charAt(0).toUpperCase() + result.slice(1);
}

// Event.prototype.preventDefault = function () {}
// localStorage.florrio_tutorial = 'complete';

// maybe use in future
function Pointer(address) {
	this.u8 = (offset = 0) => new Pointer(Module.HEAPU8[address + offset]);
	this.u32 = (offset = 0) => new Pointer(Module.HEAPU32[(address + offset) >> 2]);
	this.f32 = (offset = 0) => new Pointer(Module.HEAPF32[(address + offset) >> 2]);
	this.f64 = (offset = 0) => new Pointer(Module.HEAPF64[(address + offset) >> 3]);
}
长期地址
遇到问题?请前往 GitHub 提 Issues。