xabbo-scripts/Scripts/FURNIMATIC.csx
Administrator 7a548130a3 Move all scripts into Scripts/ subfolder
Keeps the repo root clean - only README.md visible on landing page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 09:49:37 +01:00

578 lines
15 KiB
C#

using System.Net;
using System.Text;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Threading.Tasks;
var port = 8230;
var queue = new List<int>();
var progress = 0;
var total = 0;
var delay = 850;
var lastrun = DateTime.Now;
HttpListener server = null;
try {
EnsureInventory();
var items = Inventory
.Where(x => x.IsRecyclable)
.GroupBy(x => x.GetDescriptor())
.Where(g => g.Count() >= 8)
.Select(g => new {
name = g.Key.GetName(),
id = g.Key.GetInfo().Identifier,
rev = g.Key.GetInfo().Revision,
count = g.Count(),
list = g.Select(i => (int)i.Id).ToList()
})
.OrderByDescending(x => x.count)
.ToList();
Log($"Found {items.Count} recyclable types (8+ items)");
items.ForEach(x => Log($" {x.name}: {x.count}x"));
var json = "[" + string.Join(",", items.Select((item, i) =>
$"{{\"i\":{i},\"n\":\"{item.name.Replace("\"", "\\\"")}\",\"id\":\"{item.id}\",\"r\":{item.rev},\"c\":{item.count}}}"
)) + "]";
var html = @"<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>Recycler</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: linear-gradient(135deg, #0f0f0f 0%, #1a1a1a 100%);
color: #e0e0e0;
font: 14px -apple-system, system-ui, sans-serif;
min-height: 100vh;
padding: 20px;
}
.wrap {
max-width: 900px;
margin: 0 auto;
animation: fadein 0.5s;
}
@keyframes fadein {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
h1 {
font-size: 24px;
font-weight: 600;
margin-bottom: 25px;
text-align: center;
background: linear-gradient(90deg, #4ade80, #22d3ee);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.panel {
background: rgba(255,255,255,0.03);
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
.search-box {
width: 100%;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
color: #fff;
padding: 12px;
border-radius: 8px;
font-size: 14px;
margin-bottom: 12px;
transition: all 0.2s;
}
.search-box:focus {
outline: none;
border-color: #4ade80;
background: rgba(255,255,255,0.08);
}
.search-box::placeholder {
color: #666;
}
.controls {
display: flex;
gap: 12px;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.controls label {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #a0a0a0;
}
button {
padding: 10px 20px;
background: linear-gradient(135deg, #4ade80, #22d3ee);
border: none;
color: #000;
font-weight: 600;
cursor: pointer;
border-radius: 8px;
transition: all 0.2s;
font-size: 13px;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(74,222,128,0.3);
}
button:active {
transform: translateY(0);
}
button:disabled {
background: #333;
color: #666;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
button.stop {
background: linear-gradient(135deg, #ef4444, #f97316);
}
button.stop:hover {
box-shadow: 0 5px 15px rgba(239,68,68,0.3);
}
input[type=number] {
width: 70px;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
color: #fff;
padding: 8px;
border-radius: 6px;
font-size: 13px;
transition: all 0.2s;
}
input[type=number]:focus {
outline: none;
border-color: #4ade80;
background: rgba(255,255,255,0.08);
}
.bar {
height: 40px;
background: rgba(0,0,0,0.3);
border-radius: 20px;
position: relative;
overflow: hidden;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.2);
}
.fill {
height: 100%;
background: linear-gradient(90deg, #4ade80, #22d3ee);
border-radius: 20px;
transition: width 0.5s ease;
box-shadow: 0 0 20px rgba(74,222,128,0.5);
}
.bartext {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-weight: 600;
font-size: 14px;
color: #fff;
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
}
.status {
text-align: center;
font-size: 13px;
color: #888;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 12px;
max-height: 450px;
overflow-y: auto;
padding: 4px;
}
.grid::-webkit-scrollbar {
width: 8px;
}
.grid::-webkit-scrollbar-track {
background: rgba(255,255,255,0.02);
border-radius: 4px;
}
.grid::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.1);
border-radius: 4px;
}
.grid::-webkit-scrollbar-thumb:hover {
background: rgba(255,255,255,0.15);
}
.card {
background: rgba(255,255,255,0.04);
border: 2px solid rgba(255,255,255,0.08);
border-radius: 10px;
padding: 12px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.card:hover {
background: rgba(255,255,255,0.07);
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
}
.card.on {
border-color: #4ade80;
background: rgba(74,222,128,0.1);
}
.card.hidden {
display: none;
}
.card img {
width: 60px;
height: 60px;
margin-bottom: 8px;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));
}
.card .name {
font-size: 12px;
margin-bottom: 6px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #ccc;
}
.card .count {
color: #4ade80;
font-weight: 600;
font-size: 14px;
margin-bottom: 8px;
}
.amt {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
margin-top: 8px;
}
.amt button {
width: 24px;
height: 24px;
padding: 0;
background: rgba(255,255,255,0.1);
border-radius: 4px;
font-size: 16px;
line-height: 1;
color: #fff;
}
.amt button:hover {
background: rgba(74,222,128,0.3);
transform: none;
box-shadow: none;
}
.amt span {
min-width: 40px;
font-size: 13px;
font-weight: 600;
color: #fff;
}
.empty {
grid-column: 1 / -1;
text-align: center;
padding: 60px 20px;
color: #666;
}
.result-count {
text-align: center;
font-size: 12px;
color: #666;
margin-bottom: 8px;
}
</style>
</head>
<body>
<div class='wrap'>
<h1>Recycler</h1>
<div class='panel'>
<input type='text' class='search-box' id='search' placeholder='Search items to recycle...' autofocus>
<div class='controls'>
<label>Delay <input type='number' id='delay' value='850' min='100' max='2000' step='50'>ms</label>
<button onclick='reset()'>Clear</button>
<button onclick='go()' id='btn'>Start</button>
<button onclick='stop()' class='stop'>Stop</button>
</div>
</div>
<div class='panel'>
<div class='bar'>
<div class='fill' id='bar' style='width:0%'></div>
<div class='bartext' id='txt'>Ready</div>
</div>
</div>
<div class='panel'>
<div class='status' id='info'>Select items to recycle</div>
<div class='result-count' id='results'></div>
<div class='grid' id='grid'></div>
</div>
</div>
<script>
const d = " + json + @";
let sel = [];
let vals = {};
let searchterm = '';
let wasrunning = false;
d.forEach(x => vals[x.i] = Math.min(80, Math.floor(x.c/8)*8));
function img(x) {
return 'https://images.habbo.com/dcr/hof_furni/' + x.r + '/' + x.id + '_icon.png';
}
function draw() {
const g = document.getElementById('grid');
if (!d.length) {
g.innerHTML = '<div class=""empty"">No recyclable items found<br><small>Need 8+ of the same type</small></div>';
return;
}
let visible = 0;
let html = '';
d.forEach(x => {
const show = !searchterm || x.n.toLowerCase().includes(searchterm.toLowerCase());
if (show) visible++;
html += '<div class=""card' + (show ? '' : ' hidden') + '"" id=""c' + x.i + '"" onclick=""pick(' + x.i + ')"">' +
'<img src=""' + img(x) + '"" onerror=""this.style.display=\'none\'"">' +
'<div class=""name"" title=""' + x.n + '"">' + x.n + '</div>' +
'<div class=""count"">' + x.c + 'x</div>' +
'<div class=""amt"" onclick=""event.stopPropagation()"">' +
'<button onclick=""adj(' + x.i + ',-8)"">-</button>' +
'<span id=""v' + x.i + '"">' + vals[x.i] + '</span>' +
'<button onclick=""adj(' + x.i + ',8)"">+</button>' +
'</div>' +
'</div>';
});
g.innerHTML = html;
// Restore selected state
sel.forEach(i => {
const el = document.getElementById('c' + i);
if (el) el.classList.add('on');
});
// Update result count
const rc = document.getElementById('results');
if (searchterm) {
rc.textContent = visible + ' items found';
} else {
rc.textContent = '';
}
if (visible === 0 && searchterm) {
g.innerHTML = '<div class=""empty"">No items match ""' + searchterm + '""<br><small>Try different search</small></div>';
}
}
function adj(i, n) {
const max = Math.floor(d.find(x => x.i === i).c / 8) * 8;
vals[i] = Math.max(8, Math.min(max, vals[i] + n));
document.getElementById('v' + i).textContent = vals[i];
update();
}
function pick(i) {
const e = document.getElementById('c' + i);
if (sel.includes(i)) {
sel = sel.filter(x => x !== i);
e.classList.remove('on');
} else {
sel.push(i);
e.classList.add('on');
}
update();
}
function reset() {
sel = [];
d.forEach(x => document.getElementById('c' + x.i)?.classList.remove('on'));
update();
}
function update() {
const t = sel.reduce((s, i) => s + vals[i], 0);
document.getElementById('info').textContent = sel.length ?
sel.length + ' types • ' + t + ' items' :
'Select items to recycle';
}
function go() {
if (!sel.length) {
alert('Select items first');
return;
}
const data = sel.map(i => ({i: i, a: vals[i]}));
document.getElementById('btn').disabled = true;
fetch('/recycle', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({items: data, delay: parseInt(document.getElementById('delay').value)})
});
}
function stop() {
fetch('/stop', {method: 'POST'});
document.getElementById('btn').disabled = false;
wasrunning = false;
}
// Search functionality
document.getElementById('search').addEventListener('input', (e) => {
searchterm = e.target.value.trim();
draw();
});
setInterval(() => {
fetch('/status')
.then(r => r.json())
.then(x => {
const p = x.total ? Math.round(x.done / x.total * 100) : 0;
document.getElementById('bar').style.width = p + '%';
document.getElementById('txt').textContent = x.total ? x.done + ' / ' + x.total : 'Ready';
// Track if we were running
if (x.total > 0) {
wasrunning = true;
}
// If we were running and now total is 0, we're done
if (wasrunning && x.total === 0) {
wasrunning = false;
document.getElementById('btn').disabled = false;
document.getElementById('bar').style.width = '100%';
document.getElementById('txt').textContent = 'Complete!';
setTimeout(() => {
document.getElementById('bar').style.width = '0%';
document.getElementById('txt').textContent = 'Ready';
}, 2000);
}
})
.catch(() => {});
}, 500);
draw();
</script>
</body>
</html>";
server = new HttpListener();
server.Prefixes.Add($"http://localhost:{port}/");
server.Start();
Process.Start(new ProcessStartInfo {
FileName = $"http://localhost:{port}/",
UseShellExecute = true
});
Log($"Server on port {port}");
Task.Run(async () => {
while (Run && server.IsListening) {
try {
var ctx = await server.GetContextAsync();
Task.Run(() => handle(ctx));
}
catch { break; }
}
});
while (Run) {
if (queue.Count >= 8 && (DateTime.Now - lastrun).TotalMilliseconds >= delay) {
var batch = queue.Take(8).ToList();
queue.RemoveRange(0, 8);
Send(Out["RecycleItems"], 8, batch[0], batch[1], batch[2], batch[3], batch[4], batch[5], batch[6], batch[7]);
progress += 8;
lastrun = DateTime.Now;
if (queue.Count < 8) {
Log($"Done - recycled {progress} items");
queue.Clear();
progress = 0;
total = 0;
}
}
Delay(10);
}
void handle(HttpListenerContext ctx) {
try {
var req = ctx.Request;
var res = ctx.Response;
var path = req.RawUrl;
if (path == "/") {
var b = Encoding.UTF8.GetBytes(html);
res.ContentType = "text/html";
res.ContentLength64 = b.Length;
res.OutputStream.Write(b, 0, b.Length);
}
else if (path == "/recycle" && req.HttpMethod == "POST") {
using (var r = new StreamReader(req.InputStream)) {
var data = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(r.ReadToEnd());
queue.Clear();
progress = 0;
if (data.ContainsKey("delay"))
delay = System.Text.Json.JsonSerializer.Deserialize<int>(data["delay"].ToString());
if (data.ContainsKey("items")) {
var selected = System.Text.Json.JsonSerializer.Deserialize<List<Dictionary<string, int>>>(data["items"].ToString());
foreach (var s in selected) {
if (s["i"] < items.Count) {
var item = items[s["i"]];
var amt = Math.Min(s["a"], item.count);
for (int i = 0; i < amt && i < item.list.Count; i++)
queue.Add(item.list[i]);
}
}
}
total = queue.Count;
Log($"Queued {total} items @ {delay}ms");
}
res.StatusCode = 200;
}
else if (path == "/stop") {
queue.Clear();
progress = total = 0;
Log("Stopped");
res.StatusCode = 200;
}
else if (path == "/status") {
var json = $"{{\"done\":{progress},\"total\":{total}}}";
var b = Encoding.UTF8.GetBytes(json);
res.ContentType = "application/json";
res.ContentLength64 = b.Length;
res.OutputStream.Write(b, 0, b.Length);
}
else {
res.StatusCode = 404;
}
res.Close();
}
catch { }
}
}
finally {
server?.Stop();
server?.Close();
Log("Shutdown");
}