162 lines
5.1 KiB
JavaScript
162 lines
5.1 KiB
JavaScript
#!/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();
|