Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions src/main/core/worldTiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,78 @@ export function peekChunkTile(
return hit.tiles.get(chunkSlot(localChunk(chunkX), localChunk(chunkZ))) ?? null
}

/**
* Serve what is parsed, queue what is not.
*
* Lives here rather than in the web server so the desktop app and the two web
* surfaces share one queue and one parse budget — three callers each with their
* own would be three times the work and three different maps, which is the
* complaint this consolidates.
*/
const queue: { serverId: string; dim: string; cx: number; cz: number }[] = []
let working = false

export function requestTiles(
serverId: string,
dim: string,
want: { cx: number; cz: number }[]
): { tiles: Record<string, { c: number[]; h: number[] }>; pending: number } {
const tiles: Record<string, { c: number[]; h: number[] }> = {}
const missing: { cx: number; cz: number }[] = []
for (const w of want) {
const t = peekChunkTile(serverId, dim, w.cx, w.cz)
if (t === undefined) missing.push(w)
else if (t) tiles[w.cx + ',' + w.cz] = { c: t.colour, h: t.height }
}
for (const m of missing) {
if (queue.length > 4096) break
if (!queue.some((q) => q.serverId === serverId && q.dim === dim && q.cx === m.cx && q.cz === m.cz)) {
queue.push({ serverId, dim, ...m })
}
}
if (missing.length && !working) void drain()
return { tiles, pending: missing.length }
}

async function drain(): Promise<void> {
working = true
try {
while (queue.length) {
// Yielding between chunks is not enough on its own: the first chunk of an
// unseen region parses the whole file. Wait for the parse budget.
if (!parseBudgetReady()) {
await new Promise((r) => setTimeout(r, 60))
continue
}
const job = queue.shift()
if (!job) break
try {
chunkTile(job.serverId, job.dim, job.cx, job.cz)
} catch {
/* one bad region must not stop the queue */
}
await new Promise((r) => setImmediate(r))
}
} finally {
working = false
}
}

/** `cx,cz;cx,cz…`, capped so one call cannot ask for a whole world. */
export const MAX_TILES_PER_REQUEST = 64

export function parseWantedTiles(raw: string | null | undefined): { cx: number; cz: number }[] {
const out: { cx: number; cz: number }[] = []
for (const pair of (raw ?? '').split(';')) {
const [a, b] = pair.split(',')
const cx = Number(a)
const cz = Number(b)
if (Number.isSafeInteger(cx) && Number.isSafeInteger(cz)) out.push({ cx, cz })
if (out.length >= MAX_TILES_PER_REQUEST) break
}
return out
}

/** One chunk's tile, parsing the region if needed. Never call from a request. */
export function chunkTile(
serverId: string,
Expand Down
6 changes: 6 additions & 0 deletions src/main/ipc/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import * as players from '../core/players'
import * as rcon from '../core/rcon'
import * as mods from '../core/mods'
import * as bridgeInstall from '../core/bridgeInstall'
import * as worldTiles from '../core/worldTiles'
import * as backups from '../core/backups'
import * as worlds from '../core/worlds'
import * as scheduler from '../core/scheduler'
Expand Down Expand Up @@ -301,6 +302,11 @@ export function registerIpc(): void {
H(IPC.bridgeInstall, (_e, id: string) =>
bridgeInstall.installBridge(id, { by: 'desktop', source: 'panel' })
)
H(IPC.mapTiles, (_e, id: string, dim: string, chunks: { cx: number; cz: number }[]) =>
// Capped here too: the renderer is trusted, but a bug there should not be
// able to queue a whole world any more than a web caller can.
worldTiles.requestTiles(id, dim, (chunks ?? []).slice(0, worldTiles.MAX_TILES_PER_REQUEST))
)

// --- java installs ---
H(IPC.javaList, (_e, refresh?: boolean) => listJavaInstalls(!!refresh))
Expand Down
10 changes: 10 additions & 0 deletions src/main/smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3932,6 +3932,16 @@ function runPageScript(html: string, seed: Record<string, unknown> = {}): PageRu
clearTimeout: () => {},
requestAnimationFrame: () => 0,
cancelAnimationFrame: () => {},
// Pages load avatars and bake tiles with these. Present but inert: the
// assertions are about the wiring and the text, never the pixels.
Image: class {
crossOrigin = ''
onload: (() => void) | null = null
onerror: (() => void) | null = null
set src(_v: string) {
/* never resolves, so a head stays the dot — which is the fallback path */
}
},
alert: (msg: string) => calls.push(['alert', msg]),
confirm: () => true,
encodeURIComponent,
Expand Down
88 changes: 4 additions & 84 deletions src/main/web/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,90 +78,10 @@ export function _resetRosterCache(): void {
lastFlush.clear()
}

// ---- world tiles (#119) ----
//
// A request NEVER parses a region. It asks for the chunks a viewport covers and
// gets back the ones already parsed; anything missing is queued and appears on a
// later poll. Parsing a region is megabytes of NBT, and letting a caller trigger
// it synchronously is the amplification closed in #107 with a far bigger
// multiplier — one pan across an explored world would be hundreds of regions.
const tileQueue: { serverId: string; dim: string; cx: number; cz: number }[] = []
let tileWorking = false

function queueTiles(serverId: string, dim: string, want: { cx: number; cz: number }[]): void {
for (const w of want) {
if (tileQueue.length > 4096) break
if (!tileQueue.some((q) => q.serverId === serverId && q.dim === dim && q.cx === w.cx && q.cz === w.cz)) {
tileQueue.push({ serverId, dim, ...w })
}
}
if (!tileWorking) void drainTiles()
}

async function drainTiles(): Promise<void> {
tileWorking = true
try {
while (tileQueue.length) {
// Yielding between chunks is not enough on its own: the first chunk of an
// unseen region parses the whole file in one go, so back-to-back regions
// would hold the main thread for seconds. Wait for the parse budget
// instead of spinning through the queue.
if (!worldTiles.parseBudgetReady()) {
await new Promise((r) => setTimeout(r, 60))
continue
}
const job = tileQueue.shift()
if (!job) break
try {
worldTiles.chunkTile(job.serverId, job.dim, job.cx, job.cz)
} catch {
/* one bad region must not stop the queue */
}
// ...and still yield between chunks, for the ones that hit a region the
// previous job already parsed.
await new Promise((r) => setImmediate(r))
}
} finally {
tileWorking = false
}
}

/**
* Serve what is parsed, queue what is not.
*
* `pending` is how the client knows to ask again rather than concluding the
* world is empty there.
*/
function tilesFor(
serverId: string,
dim: string,
want: { cx: number; cz: number }[]
): { tiles: Record<string, { c: number[]; h: number[] }>; pending: number } {
const tiles: Record<string, { c: number[]; h: number[] }> = {}
const missing: { cx: number; cz: number }[] = []
for (const w of want) {
const t = worldTiles.peekChunkTile(serverId, dim, w.cx, w.cz)
if (t === undefined) missing.push(w)
else if (t) tiles[w.cx + ',' + w.cz] = { c: t.colour, h: t.height }
}
if (missing.length) queueTiles(serverId, dim, missing)
return { tiles, pending: missing.length }
}

/** `cx,cz cx,cz …`, capped so one request cannot ask for a whole world. */
const MAX_TILES_PER_REQUEST = 64

function parseWanted(raw: string | null): { cx: number; cz: number }[] {
const out: { cx: number; cz: number }[] = []
for (const pair of (raw ?? '').split(';')) {
const [a, b] = pair.split(',')
const cx = Number(a)
const cz = Number(b)
if (Number.isSafeInteger(cx) && Number.isSafeInteger(cz)) out.push({ cx, cz })
if (out.length >= MAX_TILES_PER_REQUEST) break
}
return out
}
// World tiles (#119) live in `core/worldTiles`, queue and all, so the desktop
// app and the two web surfaces share one parse budget rather than three.
const tilesFor = worldTiles.requestTiles
const parseWanted = worldTiles.parseWantedTiles

// ---- asking the server to write the inventory down (#117) ----
//
Expand Down
1 change: 1 addition & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ const api: MsmsApi = {

bridgeStatus: (id) => ipcRenderer.invoke(IPC.bridgeStatus, id),
installBridge: (id) => ipcRenderer.invoke(IPC.bridgeInstall, id),
mapTiles: (id, dim, chunks) => ipcRenderer.invoke(IPC.mapTiles, id, dim, chunks),

listJava: (refresh) => ipcRenderer.invoke(IPC.javaList, refresh),
resolveJava: (override) => ipcRenderer.invoke(IPC.javaResolve, override),
Expand Down
Loading
Loading