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
5 changes: 4 additions & 1 deletion auth-authme-package/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ The same block works on a `LocalMode` environment. A local server running AuthMe

## Which command it sends

The server decides, not the account. `account.justCreated` is a hint from the account pool, and it is wrong every time a pool account outlives the run that created it — that is the second run against any stand. So the plugin waits for either prompt and answers whichever arrived. The register pattern is tested first, since AuthMe's register prompt mentions the password too and would otherwise look like a login prompt.
The server decides, not the account. `account.justCreated` is a hint from the account pool, and it is wrong every time a pool account outlives the run that created it — that is the second run against any stand. So the plugin waits for whichever of a register prompt, a login prompt, or a session-resume message arrives, and acts on that. The register pattern is tested first, since AuthMe's register prompt mentions the password too and would otherwise look like a login prompt.

A reconnect within AuthMe's own session timeout gets no prompt at all — AuthMe already considers the account logged in and says so via `sessionResumedPattern` instead. The plugin stops there without sending a command. Nothing matching within `timeoutMs` is treated as a genuine failure, not a stale session.

## Options

Expand All @@ -43,6 +45,7 @@ The server decides, not the account. `account.justCreated` is a hint from the ac
| `registerPromptPattern` | `regist` | Regex identifying the register prompt |
| `successPattern` | `success\|welcome\|logged in\|authenticat` | Regex confirming the command was accepted |
| `authenticatedPattern` | `logged in\|authenticat` | Narrower regex confirming the player is actually authenticated |
| `sessionResumedPattern` | `Session Reconnection` | Regex confirming AuthMe resumed the session on its own, no prompt needed |
| `timeoutMs` | `15000` | How long to wait for each prompt or confirmation |
| `password` | — | Fallback password for accounts that carry none |

Expand Down
37 changes: 23 additions & 14 deletions auth-authme-package/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export interface AuthAuthmeOptions {
authenticatedPattern?: string;
/** How long to wait for each prompt/confirmation before giving up. */
timeoutMs?: number;
/** Regex matched against server messages confirming AuthMe resumed an existing session on
* its own — no login/register needed. AuthMe sends this instead of a prompt when it
* considers the connecting player already authenticated (a reconnect within its session
* timeout). */
sessionResumedPattern?: string;
/** Password used for accounts that carry none of their own — the throwaway identities an
* environment without an account pool generates per bot. Plugin options travel as plain
* values, so only use this where the password is worth nothing: a local, disposable
Expand All @@ -43,6 +48,7 @@ const DEFAULTS: Required<Omit<AuthAuthmeOptions, 'password'>> = {
successPattern: 'success|welcome|logged in|authenticat',
authenticatedPattern: 'success(ful)? login|logged in|authenticat',
timeoutMs: 15000,
sessionResumedPattern: 'Session Reconnection',
};

// `onPlayerCreate` doesn't receive the plugin's options — only `setup()` does — so the
Expand Down Expand Up @@ -80,13 +86,14 @@ export default definePlugin<AuthAuthmeOptions>({

const registerPrompt = new RegExp(resolved.registerPromptPattern, 'i');
const loginPrompt = new RegExp(resolved.loginPromptPattern, 'i');
const sessionResumed = new RegExp(resolved.sessionResumedPattern, 'i');
const successPattern = new RegExp(resolved.successPattern, 'i');

// Which of the two the server asks for is the server's decision, not ours:
// `account.justCreated` is a hint from the account pool, and it is wrong whenever a
// pool account outlives the run that created it. So wait for either prompt and answer
// the one that actually arrived. Register is tested first because AuthMe's register
// prompt names the password too, and would otherwise match the login pattern.
// pool account outlives the run that created it. So wait for whichever of the three
// arrives and act on that. Register is tested first because AuthMe's register prompt
// names the password too, and would otherwise match the login pattern.
//
// Hardcoded to 0 rather than `player.getMessageBufferIndex()`: the login prompt can
// arrive during the handshake, before this handler even runs, so reading the buffer
Expand All @@ -97,23 +104,25 @@ export default definePlugin<AuthAuthmeOptions>({
const since = (index: number, pattern: RegExp): string | undefined =>
player.messageBuffer.slice(index).find((m: string) => pattern.test(m));

// The server occasionally sends no prompt at all on a reconnect (#57) — AuthMe still
// considers the account logged in from a connection that never fully closed. Rather
// than fail the whole test over one missing prompt, assume login (never registration:
// a silent reconnect can only happen to an account that already exists) and let the
// command below run into either a real prompt AuthMe queued up in the meantime, or an
// "already logged in" reply — both success/authenticated patterns already match it.
const isRegistration = await poll(
// The server occasionally reconnects a player without prompting at all — AuthMe still
// considers the account logged in from a connection that never fully closed, and says
// so with `sessionResumedPattern` instead of a prompt. Trust that message and stop here:
// no command to send, nothing left to confirm. Anything else within `timeoutMs` is a
// genuine miss, not a stale session, and throws.
const promptResult = await poll<'register' | 'login' | 'resumed'>(
() => {
if (since(joinIndex, registerPrompt)) return true;
if (since(joinIndex, loginPrompt)) return false;
if (since(joinIndex, registerPrompt)) return 'register';
if (since(joinIndex, loginPrompt)) return 'login';
if (since(joinIndex, sessionResumed)) return 'resumed';
return undefined;
},
{
timeout: resolved.timeoutMs,
message: `authme: never saw a login or register prompt for "${account.username}"`,
message: `authme: never saw a login/register prompt or session-resume message for "${account.username}"`,
},
).catch(() => false);
);
if (promptResult === 'resumed') return;
const isRegistration = promptResult === 'register';

// Everything below only looks at messages newer than the command. A server's greeting
// often carries a word like "welcome", which would otherwise pass for confirmation
Expand Down
5 changes: 4 additions & 1 deletion example_plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ plugwright {
file("plugins/AuthMe/config.yml", """
settings:
sessions:
enabled: false
# On, so a bot that reconnects within the session timeout gets
# resumed silently instead of prompted — the case the authme
# plugin's sessionResumedPattern is built to detect.
enabled: true
registration:
dialog:
preJoin:
Expand Down
15 changes: 15 additions & 0 deletions example_plugin/src/test/e2e/tests/auth-resume.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Reproduces a bot reconnect while AuthMe still considers it logged in — no login/register
* prompt arrives at all. Covers the fix: the authme plugin recognizes AuthMe's own
* session-resume message and stops there, instead of guessing and sending a command blind.
*/

import { expect, test } from '@plugwright/runner';

test('rejoin resumes the session without a prompt', async ({ player }) => {
// onPlayerCreate has already run and resolved by the time rejoin() returns, so the
// resume message is in the buffer already — no extra wait needed.
await player.rejoin();

await expect(player).toHaveReceivedMessage(/session reconnection/i);
});
Loading