162 lines
5.1 KiB
JavaScript
Raw Normal View History

#!/usr/bin/node
// Script to prune a world based on usage statistics, by deleting all
// mapblocks that aren't "used" and aren't necessary to protect such a
// mapblock. Usage of mapblocks is determined by meeting a threshold
// for total count of certain actions.
// Configure the script by modifying things in the CONFIGURATION section
// below, then run the script with CWD == world path, and it will output
// a SQL script on stdout. Pipe/paste this script to a SQL client open
// to the world database (while MT is NOT running!) to prune the world.
// ---===### ALWAYS MAKE BACKUPS FIRST ###===---
'use strict';
////////////////////////////////////////////////////////////////////////
// CONFIGURATION
// Relative path to usage survey data; assume user is
// running this with CWD == world path.
const dir = 'szutil_usagesurvey';
// Which actions to include in total for block when determining
// whether a mapblock is "developed" or not. Including "place" will
// ensure player-built structures are protected. Including "dig" will
// prevent mining-only areas from being regenerated (e.g. protect the
// scarcity value of ores).
const includeactions = {
place: true,
dig: true
};
// Total number of actions in block necessary to keep it.
const minactions = 5;
// Size of a mapgen mapchunk in blocks (the default of 5 is probably
// rarely changed).
const mapchunksize = 5;
// Margin around each mapchunk to keep, in mapblocks. Keeping at least
// one extra mapchunk will prevent mapgen in adjacent chunks from
// "bleeding" into developed areas and potentially damaging them.
const mapchunkmargin = mapchunksize;
// Output mode: one of the "OUTPUT MODE" functions below, depending
// on the type of backend used for the world.
const outputmode = sqlite;
////////////////////////////////////////////////////////////////////////
const fs = require('fs');
// For chunking multiple block IDs into SQL statements.
function* chunks(array, size) {
for(let i = 0; i < array.length; i += size)
yield array.slice(i, i + size);
}
// OUTPUT MODE: Sqlite
function sqlite(writeln, keepblocks) {
writeln('begin transaction;');
// Fill temporary table with IDs of blocks to keep.
writeln('create table keep (id primary key);');
for(let batch of chunks(Object.keys(keepblocks)
.sort(), 100))
writeln(`insert into keep(id) values${
batch.map(id => `(${id})`).join(',')};`);
// Delete blocks not in the table.
writeln('delete from blocks where pos not in (select id from keep);');
// Cleanup temp table.
writeln('drop table keep;');
writeln('commit;');
// Space isn't actually reclaimed until vacuum.
writeln('vacuum;');
}
// OUTPUT MODE: PostgreSQL
function pgsql(writeln, keepblocks) {
writeln('begin transaction;');
// Calculate and index on block ID for each block.
writeln('alter table blocks add id bigint;');
writeln('create unique index on blocks(id);');
writeln('update blocks set id = cast(posx as bigint)' +
' + 4096 * cast(posy as bigint) + 16777216 * cast(posz as bigint);');
// Mark all blocks to keep.
writeln('alter table blocks add keep integer null;');
for(let batch of chunks(Object.keys(keepblocks)
.sort(), 1000))
writeln(`update blocks set keep=1 where id in (${
batch.join(',')});`);
// Delete all unmarked blocks.
writeln('delete from blocks where keep is null;');
// Clean up temporary columns.
writeln('alter table blocks drop column keep;');
writeln('alter table blocks drop column id;');
writeln('commit;');
}
// Recreate the math MT uses to convert block ID to/from
// block pos.
const pymod = (n, m) => (n >= 0) ? (n % m) : (m - ((-n) % m));
const wrap = n => (n > 2047) ? (n - 4096) : n;
const idtopos = id => {
const x = wrap(pymod(id, 4096));
id = (id - x) / 4096;
const y = wrap(pymod(id, 4096));
id = (id - y) / 4096;
return { x, y, z: id };
};
const postoid = pos => pos.x + pos.y * 4096 + pos.z * 16777216;
async function main() {
// Scan all mapblock statistics and determine which ones
// to keep based on total actions.
const keepblocks = {};
for(let f of await fs.promises.readdir(dir)) {
const m = f.match(/^blk(.*)\.json$/);
if(!m) console.warn(`UNRECOGNIZED FILE: ${f}`);
const data = JSON.parse((await fs.promises.readFile(`${dir}/${f}`))
.toString());
const id = Number(m[1]);
const acts = Object.values(data)
.reduce((a, b) => a + Object.keys(includeactions)
.reduce(
(c, d) => c + (b[d] || 0), 0), 0);
if(acts > minactions) keepblocks[id] = true;
}
// Expand "keep" regions to entire mapchunks, plus margin,
// to prevent mapgen of adjacent chunks damaging developed areas.
for(let k of Object.keys(keepblocks)) {
const p = idtopos(Number(k));
p.x = Math.floor(p.x / 5) * 5;
p.y = Math.floor(p.y / 5) * 5;
p.z = Math.floor(p.z / 5) * 5;
for(let dx = -mapchunkmargin; dx < mapchunksize + mapchunkmargin; dx++)
for(let dy = -mapchunkmargin; dy < mapchunksize + mapchunkmargin; dy++)
for(let dz = -mapchunkmargin; dz < mapchunksize + mapchunkmargin; dz++)
keepblocks[postoid({
x: p.x + dx,
y: p.y + dy,
z: p.z + dz
})] = true;
}
// Write SQL via chosen output mode.
outputmode(s => process.stdout.write(s + '\n'), keepblocks);
}
main();