diff --git a/auth-authme-package/README.md b/auth-authme-package/README.md index 07483ec..95ade07 100644 --- a/auth-authme-package/README.md +++ b/auth-authme-package/README.md @@ -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 @@ -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 | diff --git a/auth-authme-package/index.ts b/auth-authme-package/index.ts index ce15b8f..c43618e 100644 --- a/auth-authme-package/index.ts +++ b/auth-authme-package/index.ts @@ -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 @@ -43,6 +48,7 @@ const DEFAULTS: Required> = { 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 @@ -80,13 +86,14 @@ export default definePlugin({ 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 @@ -97,23 +104,25 @@ export default definePlugin({ 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 diff --git a/example_plugin/build.gradle.kts b/example_plugin/build.gradle.kts index 0ab41e4..79a1955 100644 --- a/example_plugin/build.gradle.kts +++ b/example_plugin/build.gradle.kts @@ -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: diff --git a/example_plugin/src/test/e2e/tests/auth-resume.spec.ts b/example_plugin/src/test/e2e/tests/auth-resume.spec.ts new file mode 100644 index 0000000..423353e --- /dev/null +++ b/example_plugin/src/test/e2e/tests/auth-resume.spec.ts @@ -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); +});