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
9 changes: 7 additions & 2 deletions runner-package/lib/matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ export class RunnerMatchers<T = unknown> extends Matchers<T> {
const expected = !this.isNot;
const deadline = Date.now() + timeout;

while (Date.now() < deadline) {
// Deadline last, as in `poll`: the condition is checked once more after the final
// wait, so a message that lands during it still counts.
for (;;) {
if (condition() === expected) return;
if (Date.now() >= deadline) break;
await new Promise(resolve => setTimeout(resolve, pollingRate));
}

Expand Down Expand Up @@ -195,7 +198,8 @@ export class PollMatchers<T> {
const deadline = Date.now() + timeout;
let lastError: unknown;

while (Date.now() < deadline) {
// Deadline last, as in `poll`: one more attempt after the final wait.
for (;;) {
const value = await this.fn();
try {
const m = new Matchers(value, this.isNot);
Expand All @@ -204,6 +208,7 @@ export class PollMatchers<T> {
} catch (e) {
lastError = e;
}
if (Date.now() >= deadline) break;
await sleep(Math.min(interval, Math.max(0, deadline - Date.now())));
}

Expand Down
13 changes: 9 additions & 4 deletions runner-package/lib/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,25 +115,30 @@ export class PlayerWrapper {
if (this._listenersBot === this.bot) return;
this._listenersBot = this.bot;

const botUsername = this.username;
// Read on each line, not captured here: `username` is `bot.username`, which stays
// undefined until the client is through the handshake (see `_captureSpawnPromise`,
// which reads it the same way). A snapshot taken here is what put "[Bot undefined]"
// in front of chat lines — including every line a login wall produces, which are the
// ones worth reading when authentication goes wrong.
const botUsername = (): string | undefined => this.bot.username;
const bot = this.bot;

bot.on('message', (jsonMsg: unknown) => {
const message = String(jsonMsg);
console.log(pc.dim(`[Bot ${botUsername}] Received message: "${message}"`));
console.log(pc.dim(`[Bot ${botUsername()}] Received message: "${message}"`));
this.messageBuffer.push(message);
});

bot.on('windowOpen', (window: unknown) => {
if (process.env.PLUGWRIGHT_DEBUG !== '1') return;
const win = window as { title?: string; type?: string | number; slots?: unknown[] };
console.log(pc.gray(`[DEBUG] [Bot ${botUsername}] Global windowOpen event - Title: "${win.title}", Type: ${win.type}, SlotCount: ${win.slots?.length}`));
console.log(pc.gray(`[DEBUG] [Bot ${botUsername()}] Global windowOpen event - Title: "${win.title}", Type: ${win.type}, SlotCount: ${win.slots?.length}`));
});

bot.on('windowClose', (window: unknown) => {
if (process.env.PLUGWRIGHT_DEBUG !== '1') return;
const win = window as { title?: string };
console.log(pc.gray(`[DEBUG] [Bot ${botUsername}] windowClose event - Window: ${win?.title || 'unknown'}`));
console.log(pc.gray(`[DEBUG] [Bot ${botUsername()}] windowClose event - Window: ${win?.title || 'unknown'}`));
});
}

Expand Down
33 changes: 23 additions & 10 deletions runner-package/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@ export const sleep = (ms: number, signal?: AbortSignal) => {

/**
* Polls `fn` until it returns a non-undefined value, or throws on timeout.
* Simple, race-condition-free, works with any state.
*
* The deadline is checked *after* `fn` runs, never instead of it: the last thing this does
* before giving up is look one more time. Checking the clock first discards whatever landed
* during the final sleep, and that window is not the 50ms it looks like — a bot loading
* chunks stalls the event loop for seconds, and when it comes back the value that would have
* passed the poll is already there while the clock is past the deadline. That failure reads
* as "the message arrived and the wait timed out anyway", which is exactly as confusing as
* it sounds.
*/
export async function poll<T>(
fn: () => T | undefined | Promise<T | undefined>,
Expand All @@ -27,10 +34,11 @@ export async function poll<T>(
const { timeout = 5000, interval = 50, message = 'poll() timed out', signal } = options;
const deadline = Date.now() + timeout;

while (Date.now() < deadline) {
for (;;) {
if (signal?.aborted) throw new Error('Aborted');
const result = await fn();
if (result !== undefined) return result;
if (Date.now() >= deadline) break;
await sleep(interval, signal);
}

Expand All @@ -45,18 +53,20 @@ export async function waitForAssertion(
fn: () => Promise<void>,
{ timeout = 5000, interval = 250, signal }: { timeout?: number, interval?: number, signal?: AbortSignal } = {}
): Promise<void> {
const start = Date.now();
const deadline = Date.now() + timeout;
let lastError: unknown;

while (Date.now() - start < timeout) {
// Deadline last, as in `poll`: the final attempt happens after the final sleep.
for (;;) {
if (signal?.aborted) throw new Error('Aborted');
try {
await fn();
return; // passed
} catch (e) {
lastError = e;
await sleep(interval, signal);
}
if (Date.now() >= deadline) break;
await sleep(interval, signal);
}
throw lastError;
}
Expand All @@ -76,13 +86,15 @@ export async function waitUntil(
signal
}: { timeout?: number, interval?: number, message?: string, signal?: AbortSignal } = {}
): Promise<void> {
const start = Date.now();
const deadline = Date.now() + timeout;

while (Date.now() - start < timeout) {
// Deadline last, as in `poll`: the final check happens after the final sleep.
for (;;) {
if (signal?.aborted) throw new Error('Aborted');
if (await predicate()) {
return; // condition met
}
if (Date.now() >= deadline) break;
await sleep(interval, signal);
}

Expand Down Expand Up @@ -115,20 +127,21 @@ export async function waitForStable(
const deadline = Date.now() + timeout;

// First, wait until the condition becomes true
while (Date.now() < deadline) {
for (;;) {
if (signal?.aborted) throw new Error('Aborted');
if (await predicate()) break;
await sleep(interval, signal);
if (Date.now() >= deadline) throw new Error(message);
await sleep(interval, signal);
}

// Then, verify it stays true for the entire `duration`
const stableDeadline = Date.now() + duration;
while (Date.now() < stableDeadline) {
for (;;) {
if (signal?.aborted) throw new Error('Aborted');
if (!(await predicate())) {
throw new Error(message);
}
if (Date.now() >= stableDeadline) break;
await sleep(Math.min(interval, Math.max(0, stableDeadline - Date.now())), signal);
}
}
71 changes: 21 additions & 50 deletions runner-package/lib/wrappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export class GuiItemLocator {
const { timeout = 5000 } = options;
const startTime = Date.now();

while (Date.now() - startTime < timeout) {
for (;;) {
const currentGui = this.gui._getCurrentGuiSnapshot();
if (currentGui) {
const item = currentGui._findItemInternal(this.predicate);
Expand All @@ -73,6 +73,7 @@ export class GuiItemLocator {
return;
}
}
if (Date.now() - startTime >= timeout) break;
await new Promise(resolve => setTimeout(resolve, 100));
}

Expand Down Expand Up @@ -440,21 +441,8 @@ export function createPlayerExtensions(bot: Bot) {
const { timeout = 5000, pollingRate = 100 } = options;
const startTime = Date.now();

return new Promise((resolve, reject) => {
const checkForItem = () => {
const elapsed = Date.now() - startTime;

if (elapsed >= timeout) {
clearInterval(pollInterval);
reject(new Error(`[Player] Timeout waiting for GUI item (${timeout}ms)`));
return;
}

// Check if there's a current window open
if (!bot.currentWindow) {
return; // Continue polling
}

for (;;) {
if (bot.currentWindow) {
const window = bot.currentWindow as Window;
const items = window.slots
.filter((item): item is RawItem => item != null)
Expand All @@ -463,18 +451,17 @@ export function createPlayerExtensions(bot: Bot) {
const matchedItem = items.find(itemMatcher);

if (matchedItem) {
clearInterval(pollInterval);
console.log(`[Player] Found GUI item: ${matchedItem.getDisplayName()} at slot ${matchedItem.slot}`);
resolve(matchedItem);
return matchedItem;
}
};
}

// Start polling
const pollInterval = setInterval(checkForItem, pollingRate);
if (Date.now() - startTime >= timeout) {
throw new Error(`[Player] Timeout waiting for GUI item (${timeout}ms)`);
}

// Initial check
checkForItem();
});
await new Promise(resolve => setTimeout(resolve, pollingRate));
}
},

async clickGuiItem(
Expand All @@ -486,20 +473,8 @@ export function createPlayerExtensions(bot: Bot) {
const { timeout = 5000, pollingRate = 100 } = options;
const startTime = Date.now();

return new Promise((resolve, reject) => {
const checkForItem = async () => {
const elapsed = Date.now() - startTime;

if (elapsed >= timeout) {
clearInterval(pollInterval);
reject(new Error(`[Player] Timeout waiting for GUI item to click (${timeout}ms)`));
return;
}

if (!bot.currentWindow) {
return;
}

for (;;) {
if (bot.currentWindow) {
const window = bot.currentWindow as Window;
const items = window.slots
.filter((item): item is RawItem => item != null)
Expand All @@ -508,8 +483,6 @@ export function createPlayerExtensions(bot: Bot) {
const matchedItem = items.find(itemMatcher);

if (matchedItem) {
clearInterval(pollInterval);

const lore = matchedItem.getLore();
console.log(`[Player] Clicking GUI item: ${matchedItem.getDisplayName()}`);
console.log(` Material: ${matchedItem.name}`);
Expand All @@ -518,19 +491,17 @@ export function createPlayerExtensions(bot: Bot) {
console.log(` Lore: ${lore.join(' | ')}`);
}

try {
await bot.clickWindow(matchedItem.slot, 0, 0);
resolve();
} catch (error) {
reject(error);
}
await bot.clickWindow(matchedItem.slot, 0, 0);
return;
}
};
}

const pollInterval = setInterval(checkForItem, pollingRate);
if (Date.now() - startTime >= timeout) {
throw new Error(`[Player] Timeout waiting for GUI item to click (${timeout}ms)`);
}

checkForItem();
});
await new Promise(resolve => setTimeout(resolve, pollingRate));
}
},

async waitForGui(
Expand Down
Loading