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