Skip to content

Commit

Permalink
Using ticking areas to load chunks instead
Browse files Browse the repository at this point in the history
  • Loading branch information
SIsilicon committed Apr 6, 2024
1 parent 16b9925 commit 2b94851
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 102 deletions.
32 changes: 27 additions & 5 deletions src/library/utils/tickingarea.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,32 @@
import { Server, Vector } from "@notbeer-api";
import { Vector3, Dimension } from "@minecraft/server";
import { Vector3, Dimension, world } from "@minecraft/server";

export function addTickingArea(start: Vector3, end: Vector3, dimension: Dimension, name: string, preload = false) {
return Server.runCommand(`tickingarea add ${Vector.from(start).print()} ${Vector.from(end).print()} ${name} ${preload}`, dimension).error;
const DIMENSIONS = [world.getDimension("overworld"), world.getDimension("nether"), world.getDimension("the_end")];

/**
* Sets a ticking area in a cuboid region to load chunks. Note that chunks don't get loaded immediately.
* @returns `true` when created successfully; `false` otherwise.
*/
export function setTickingArea(start: Vector3, end: Vector3, dimension: Dimension, name: string) {
const removed = removeTickingArea(name, dimension);
return !!Server.runCommand(`tickingarea add ${Vector.from(start).print()} ${Vector.from(end).print()} ${name}`, dimension).successCount || removed;
}

/**
* Sets a ticking area in a circular region to load chunks. Note that chunks don't get loaded immediately.
* @returns `true` when created successfully; `false` otherwise.
*/
export function setTickingAreaCircle(center: Vector3, radius: 1 | 2 | 3 | 4, dimension: Dimension, name: string) {
const removed = removeTickingArea(name, dimension);
const result = Server.runCommand(`tickingarea add circle ${Vector.from(center).print()} ${radius} ${name}`, dimension);
return !!result.successCount || removed;
}

export function removeTickingArea(name: string, dimension: Dimension) {
return Server.runCommand(`tickingarea remove ${name}`, dimension).error;
/** Removes a ticking area. */
export function removeTickingArea(name: string, dimension?: Dimension) {
if (dimension) {
return !!Server.runCommand(`tickingarea remove ${name}`, dimension).successCount;
} else {
DIMENSIONS.forEach((d) => Server.runCommand(`tickingarea remove ${name}`, d));
}
}
18 changes: 3 additions & 15 deletions src/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Player, system, world } from "@minecraft/server";
import { getTickingAreas, print, printerr, setTickingAreas } from "./util.js";
import { Player, world } from "@minecraft/server";
import { print, printerr } from "./util.js";

// Check if configuration is properly loaded
if (!config.commandPrefix) {
world.getAllPlayers().forEach((player) => printerr("WorldEdit failed to load configuration!", player, false));
throw new Error('Configuration is not properly loaded! If this is a server, "variables.json" is required.');
}

import { contentLog, Server, configuration, removeTickingArea } from "@notbeer-api";
import { contentLog, Server, configuration } from "@notbeer-api";
import { getSession, removeSession } from "./sessions.js";
import { PlayerUtil } from "@modules/player_util.js";
import config from "config.js";
Expand All @@ -20,18 +20,6 @@ Server.setMaxListeners(256);
configuration.multiThreadingTimeBudget = config.asyncTimeBudget;
const activeBuilders: Player[] = [];

Server.on("worldInitialize", () => {
system.run(() => {
for (const tickingArea of getTickingAreas()) {
if (!tickingArea) continue;
for (const dim of ["overworld", "nether", "the_end"]) {
if (!removeTickingArea(tickingArea, world.getDimension(dim))) break;
}
}
setTickingAreas([]);
});
});

let ready = false;
Server.on("ready", (ev) => {
contentLog.debug(`World has been loaded in ${ev.loadTime} ticks!`);
Expand Down
98 changes: 52 additions & 46 deletions src/server/modules/jobs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Server, RawText } from "@notbeer-api";
import { Player, Dimension, Vector3, Block, world, GameMode } from "@minecraft/server";
import { Server, RawText, removeTickingArea, setTickingAreaCircle } from "@notbeer-api";
import { Player, Dimension, Vector3, Block } from "@minecraft/server";
import { PlayerSession } from "server/sessions";
import { UnloadedChunksError } from "./assert";

// eslint-disable-next-line prefer-const
let globalJobIdCounter = 0;
Expand All @@ -14,31 +15,25 @@ interface job {
message: string;
percent: number;
dimension: Dimension;
tickingAreaUsageTime?: number;
tickingAreaRequestTime?: number;
tickingAreaSlot?: number;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type JobFunction = { readonly jobFunc: "nextStep" | "setProgress"; readonly data: any };

function cleanUpPlayer(player: Player) {
if (player.getDynamicProperty("locationBeforeJob")) {
player.teleport(<Vector3>player.getDynamicProperty("locationBeforeJob"), { dimension: world.getDimension(<string>player.getDynamicProperty("dimensionBeforeJob")) });
player.setGameMode(<GameMode>player.getDynamicProperty("gamemodeBeforeJob"));
player.camera.fade({ fadeTime: { fadeInTime: 0, holdTime: 0, fadeOutTime: 0.5 } });
player.setDynamicProperty("locationBeforeJob", undefined);
player.setDynamicProperty("dimensionBeforeJob", undefined);
player.setDynamicProperty("gamemodeBeforeJob", undefined);
player.runCommand("inputpermission set @s movement enabled");
}
}
Server.addListener("playerLoaded", ({ player }) => cleanUpPlayer(player));
world.getAllPlayers().forEach((player) => cleanUpPlayer(player));

class JobHandler {
private jobs = new Map<JobContext, job>();
private current: JobContext;
private occupiedTickingAreaSlots = [false];

constructor() {
Server.on("tick", () => this.printJobs());
Server.on("tick", () => {
this.manageTickingAreaSlots();
this.printJobs();
});
for (let i = 0; i < 9; i++) removeTickingArea("wedit:ticking_area_" + i);
}

public *run<T, TReturn>(session: PlayerSession, steps: number, func: Generator<T | JobFunction, TReturn> | (() => Generator<T | JobFunction, TReturn>)) {
Expand Down Expand Up @@ -95,17 +90,14 @@ class JobHandler {
const job = this.jobs.get(ctx ?? this.current);
const block = job?.dimension.getBlock(loc);
if ((ctx || !block) && job) {
const player = job.player;
if (!player.isValid()) return;
if (!player.getDynamicProperty("locationBeforeJob")) {
player.setDynamicProperty("locationBeforeJob", player.location);
player.setDynamicProperty("dimensionBeforeJob", player.dimension.id);
player.setDynamicProperty("gamemodeBeforeJob", player.getGameMode());
player.runCommand("inputpermission set @s movement disabled");
player.setGameMode(GameMode.spectator);
player.camera.fade({ fadeTime: { fadeInTime: 0, holdTime: 1, fadeOutTime: 0 }, fadeColor: { red: 0, green: 0, blue: 0 } });
if (job.tickingAreaSlot === undefined) {
if (!job.tickingAreaRequestTime) job.tickingAreaRequestTime = Date.now();
return;
}

if (!setTickingAreaCircle(loc, 4, job.dimension, "wedit:ticking_area_" + job.tickingAreaSlot)) {
throw new UnloadedChunksError("worldedit.error.tickArea");
}
player.teleport(loc);
}
return block;
}
Expand All @@ -128,7 +120,10 @@ class JobHandler {
job.percent = 1;
job.step = job.stepCount - 1;
if (job.message?.length) job.message = "Finished!"; // TODO: Localize
if (job.player.isValid) cleanUpPlayer(job.player);
if (job.tickingAreaSlot !== undefined) {
removeTickingArea("wedit:ticking_area_" + job.tickingAreaSlot, job.dimension);
this.occupiedTickingAreaSlots[job.tickingAreaSlot] = false;
}
this.printJobs();
this.jobs.delete(jobId);
}
Expand All @@ -141,38 +136,49 @@ class JobHandler {
if (job.message?.length) {
if (!progresses.has(job.player)) progresses.set(job.player, []);
const percent = (job.percent + job.step) / job.stepCount;
progresses.get(job.player).push([job.message, Math.max(percent, 0)]);
}

if (job.player.isValid() && job.player.getDynamicProperty("locationBeforeJob")) {
job.player.camera.fade({ fadeTime: { fadeInTime: 0.1, holdTime: 1, fadeOutTime: 0.1 } });
progresses.get(job.player).push([job.tickingAreaRequestTime ? "Loading Chunks..." : job.message, Math.max(percent, 0)]);
}
}

for (const [player, progress] of progresses.entries()) {
let text: RawText;
let i = 0;
for (const [message, percent] of progress) {
if (text) {
text.append("text", "\n");
}

if (text) text.append("text", "\n");
let bar = "";
for (let i = 0; i < 20; i++) {
bar += i / 20 <= percent ? "█" : "▒";
}
for (let i = 0; i < 20; i++) bar += i / 20 <= percent ? "█" : "▒";

if (!text) {
text = new RawText();
}
if (progress.length > 1) {
text.append("text", `Job ${++i}: `);
}
if (!text) text = new RawText();
if (progress.length > 1) text.append("text", `Job ${++i}: `);
text.append("translate", message).append("text", `\n${bar} ${(percent * 100).toFixed(2)}%`);
}
Server.queueCommand(`titleraw @s actionbar ${text.toString()}`, player);
}
}

private manageTickingAreaSlots() {
const jobs = Array.from(this.jobs.values());
const jobsRequestingArea = jobs.filter((job) => job.tickingAreaRequestTime).sort((a, b) => a.tickingAreaRequestTime - b.tickingAreaRequestTime);
if (!jobsRequestingArea) return;

const jobsUsingArea = jobs.filter((job) => job.tickingAreaUsageTime && job.tickingAreaUsageTime < Date.now() - 2000).sort((a, b) => a.tickingAreaUsageTime - b.tickingAreaUsageTime);
for (const needy of jobsRequestingArea) {
let slot = this.occupiedTickingAreaSlots.findIndex((slot) => !slot);
if (slot === -1 && jobsUsingArea.length) {
const donor = jobsUsingArea.shift();
slot = donor.tickingAreaSlot;
donor.tickingAreaSlot = undefined;
donor.tickingAreaRequestTime = undefined;
donor.tickingAreaUsageTime = undefined;
}
if (slot !== -1) {
needy.tickingAreaRequestTime = undefined;
needy.tickingAreaUsageTime = Date.now();
needy.tickingAreaSlot = slot;
this.occupiedTickingAreaSlots[slot] = true;
}
}
}
}

export const Jobs = new JobHandler();
37 changes: 2 additions & 35 deletions src/server/util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Block, Vector3, Dimension, Entity, Player, world, BlockComponentTypeMap, RawMessage } from "@minecraft/server";
import { Server, RawText, addTickingArea as addTickArea, removeTickingArea as removeTickArea, Vector } from "@notbeer-api";
import { Block, Vector3, Dimension, Entity, Player, BlockComponentTypeMap, RawMessage } from "@minecraft/server";
import { Server, RawText, Vector } from "@notbeer-api";
import config from "config.js";

/**
Expand Down Expand Up @@ -93,39 +93,6 @@ export function blockHasNBTData(block: Block) {
return components.some((component) => !!block.getComponent(component)) || nbt_blocks.includes(block.typeId);
}

export function getTickingAreas() {
return (world.getDynamicProperty("wedit_ticking_areas") as string)?.split(",") ?? [];
}

export function setTickingAreas(tickingAreas: string[]) {
world.setDynamicProperty("wedit_ticking_areas", tickingAreas.join(","));
}

export function addTickingArea(name: string, dim: Dimension, start: Vector3, end: Vector3) {
const tickingAreas = getTickingAreas();
if (tickingAreas.length >= 10) {
return true;
}
if (!addTickArea(start, end, dim, name, true)) {
tickingAreas.push(name);
setTickingAreas(tickingAreas);
return false;
}
return true;
}

export function removeTickingArea(name: string, dim: Dimension) {
const tickingAreas = getTickingAreas();
if (!tickingAreas.includes(name)) {
return true;
}
if (!removeTickArea(name, dim)) {
setTickingAreas(tickingAreas.filter((tickingArea) => tickingArea !== name));
return false;
}
return true;
}

/**
* Converts a location object to a string.
* @param loc The object to convert
Expand Down
4 changes: 3 additions & 1 deletion texts/en_US.po
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,8 @@ msgid "worldedit.error.loadHistory"
msgstr "Failed to load history point!"
msgid "worldedit.error.stillRecording"
msgstr "History is still being recorded!"
msgid "worldedit.error.tickArea"
msgstr "Failed to create a temporary ticking area!"

# WORLDEDIT COMMANDS

Expand All @@ -338,7 +340,7 @@ msgstr "You don't have the permission to execute this command."
msgid "commands.generic.wedit:outsideWorld"
msgstr "Cannot modify unloaded chunks!"
msgid "commands.generic.wedit:outsideWorld.detail"
msgstr "§eTip: To load more chunks, increase your simulation distance, or add ticking areas where you're operating.§r"
msgstr "§eTip: To load more chunks, increase your simulation distance and stay close to the operation, or add ticking areas where you're operating.§r"
msgid "commands.generic.wedit:tooBig"
msgstr "The number (%1) is too big! It must be at most (%2)."
msgid "commands.generic.wedit:tooSmall"
Expand Down

0 comments on commit 2b94851

Please sign in to comment.