Skip to content

Add in-client account registration - #1300

Open
HarleyGilpin wants to merge 20 commits into
GregHib:mainfrom
HarleyGilpin:feat/account-registration
Open

HarleyGilpin wants to merge 20 commits into
GregHib:mainfrom
HarleyGilpin:feat/account-registration

Conversation

@HarleyGilpin

@HarleyGilpin HarleyGilpin commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds account creation from the client's login screen ("New user").

Changes

  • network: RegistrationServer dispatched from GameServer for opcodes 28/22, RegistrationResponse codes, RegistrationValidator, per-IP RegistrationLimiter, AccountCreator interface. Login (LoginServer/PasswordManager) accepts email-shaped account names beyond 12 characters via AccountNames. Invalid names now get response 3 ("Invalid username or password") instead of 11, which the client shows as the "password is an extremely common choice" warning (the message behind Cannot relogin to accounts, getting "Invalid Login or Password" or "Password too common" #1307).
  • engine: PlayerAccountLoader also implements AccountCreator: it reserves the email in AccountDefinitions on the game thread and persists via Storage.create. AccountManager.create gives email accounts a placeholder display name from the local part (numbered if taken) and flags choose_name, so login-created and registered email accounts behave the same. Storage.create persists a brand-new account atomically (FileStorage: createNewFile + temp file + atomic move; DatabaseStorage: single transaction, unique-violation → false). AccountDefinitions.remove for rollback. DisplayNames holds the name rules and suggestion generator.
  • database: accounts.name and abuse_reports.reporter widened to 254 with idempotent migrations in connect.
  • game: at the end of character creation, registered accounts get the authentic Character Name panel (interface 1028 components 154/158-166): the server runs client script 3943 to show it, sends six suggestions as client strings 337-342 and runs 3952; Continue / a suggestion runs 3945 which returns the typed name as a string entry. Taken or invalid names re-prompt with a message and fresh suggestions ("More"/"Previous Suggestions" page through them). When character creation is disabled a name-entry dialogue is used instead. rename/nameTaken extracted from NameChange; the ::rename command now rejects taken names too. New settings:
    accounts.registration=true
    accounts.registration.maxPerIP=3
    accounts.registration.windowMinutes=60
    
    development.accountCreation (auto-create on unknown login) is unchanged.

Testing

  • Unit tests for the server decode paths (hand-built RSA/XTEA packets), validator, limiter, GameServer dispatch, login with email names, registration via PlayerAccountLoader, DisplayNames (rules + suggestions), AccountDefinitions.remove, Storage.create on both backends, and WorldTests for the name panel (confirm, taken/invalid/accepted names, suggestion paging and picking) and the dialogue fallback.
  • End-to-end against a running server with raw sockets: every reply code above, duplicate/rate-limited creates, then login with the wrong and the correct password.

The 634 client's login screen already contains the "New user" form and
sends two single-reply requests: opcode 28 to check an email address and
opcode 22 to create the account. Answer both from a RegistrationServer
wired into GameServer.

- Accounts registered from the client use the email address as the
  account name; login accepts email-shaped names beyond 12 characters.
- Storage.create persists a new account atomically for files and the
  database (name/reporter columns widened to 254 with migrations).
- A display name is derived from the email and the player is prompted to
  choose their own on first login before character creation. The rename
  command now also rejects taken names.
- Password rules (5-20 alphanumeric, not guessable), minimum age and a
  per-IP creation limit are enforced server-side; registration can be
  disabled with accounts.registration.
Accounts registered with an email address now pick their display name on
the "Character Name" panel at the end of character creation instead of
a dialogue prompt. The panel is part of interface 1028: confirming the
appearance runs the client script that shows it, six server generated
suggestions are sent as client strings, and Continue or a suggestion
click runs the submit script which returns the typed name as a string
entry. Taken or invalid names re-prompt with a message and fresh
suggestions; the dialogue prompt remains as a fallback when character
creation is disabled.
The suggestion container in the character creation name panel is hidden
in the cache behind the naming rules text, so the server now toggles the
two when the panel opens and reveals "Previous Suggestions" once a
second page exists. Suggestions are generated from the player's name or
the rejected name with a mix of styles: stem plus numbers, stem plus a
class or creature, an adjective plus the stem, and adjective plus noun.
@skalfate

Copy link
Copy Markdown
Contributor

Amazing stuff!! Some parts I can understand and some not but it doesn't matter to me. Its awesome. This is very appreciated. ❤️

@HarleyGilpin
HarleyGilpin marked this pull request as draft September 17, 2026 18:01
@HarleyGilpin

Copy link
Copy Markdown
Contributor Author

I am going to do some work off this branch to add Ktor restful web API to enable web registration, player counts, and adventure logs. So I'm converting this PR into a draft.

@HarleyGilpin
HarleyGilpin marked this pull request as ready for review September 17, 2026 19:29
Comment on lines +244 to +245
saveHistories(list, playerIds)
true

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This'll need updating with the latest stats sections

@HarleyGilpin HarleyGilpin Sep 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted a shared saveSections used by both save and create so the two can't drift again, a1b4020

* Creates accounts registered from the client's login screen.
* The email address is the account name; a display name is derived from it until the player chooses their own on first login.
*/
class PlayerAccountCreator(

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow why this is all necessary? There are already uniqueness checks for usernames, which are case insensitive and we can just check if the username is an email we assign an incrementing player1/2/3 as the displayName and rename it once they're in-game and have picked a name?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point, simplified in eb403ad:

  • Placeholder name is now the next free Player1/Player2/… via the existing case-insensitive AccountDefinitions.get; DisplayNames.unique and the email local-part derivation are gone.
  • Dropped the in-flight set and the extra storage.exists lookup. The availability check and accountDefinitions.add now run together on the game thread, with Storage.create returning false on a duplicate as the backstop.
  • Removed the unread registered variable.

The rename happens on first login as before: choose_name opens the Character Name panel at the end of character creation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went back to deriving the placeholder from the email in 1320c3f: since the name panel suggestions build on the current name, "Bob smith" gives better suggestions than "Player7". Still the existing case-insensitive checks plus a numeric suffix if taken; the in-flight set and extra storage lookup stay gone.

HarleyGilpin and others added 3 commits September 17, 2026 13:05
Registered accounts get the next free PlayerN display name instead of one derived from the email, and rely on the existing case-insensitive account checks plus the atomic Storage.create instead of a separate in-flight set and storage lookup. Drops the unread registered variable.
Comment on lines +40 to +41
single { PlayerAccountLoader(get(), get(), get(), get(), get(), Contexts.Game, get()) }
single { PlayerAccountCreator(get(), get(), get(), gameContext = Contexts.Game) }

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These do the same thing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, merged them in d93879b. PlayerAccountCreator is gone: PlayerAccountLoader implements AccountCreator (reserve the email in AccountDefinitions on the game thread, Storage.create, roll back on failure) and AccountManager.create sets the placeholder display name for email accounts, so the login path with development.accountCreation and the registration path share it.

The client shows response 11 as "Your password is an extremely common choice" so names over 12 characters or malformed emails looked like a password problem. Drops the unreachable duplicate check in LoginServer.
@HarleyGilpin

Copy link
Copy Markdown
Contributor Author

8447e46 fixes the "Your password is an extremely common choice" message from #1307: PasswordManager returned response 11 for names over 12 characters (and now malformed emails), and 11 is the client's weak-password slot. It sends 3 ("Invalid username or password") now, and the unreachable duplicate name check in LoginServer is gone. Pre-existing on main, so happy to split it out if you'd rather.

Registered players start as the part before the @ (sanitised, numbered if taken) rather than PlayerN so the name panel suggestions are based on something they recognise.
The client queues client string updates until it has read every packet in the frame but runs scripts as soon as they arrive, so running the render script in the same tick showed the strings from the previous call (or the previous player on the same client).
An account created since startup only exists in memory until its first save, so validating it returned account disabled before the already-online check could run. The in-memory hash is checked instead.
@HarleyGilpin

HarleyGilpin commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Two more from testing:

  • 06860da - name panel suggestions were always one call behind (the previous page, or the previous player registered on the same client). The 634 client queues varcstr updates and applies them after it has read all packets in the frame, but runs script packets immediately, so 3952 read the old strings. The render script is now queued a tick after the strings.
  • 8181bbc - logging in while already online on an account created since startup (development.accountCreation, not yet saved) answered 4 "account disabled" because PasswordManager treated a missing file as disabled before the online check ran. The in-memory hash is checked instead, so it falls through to the normal already-online reply. Pre-existing on main.

AccountManager.create gives email accounts their placeholder display name so both the login and registration paths share it, and PlayerAccountLoader implements AccountCreator rather than a second class creating accounts.
@GregHib

GregHib commented Sep 21, 2026

Copy link
Copy Markdown
Owner

It doesn't seem to work? I fill out the details and press enter and the button gets disabled but nothing happens
image

The create account form's Continue script (2967) sends anyone who enters
an age under 13 to a herotopia.ws redirect instead of the create account
packet and never restores the button, so it sits on "Please wait...".
RemoveRegistrationAgeLimit strips that branch from the cache (wired into
CacheBuilder) and the server no longer refuses under 13s.
@HarleyGilpin

HarleyGilpin commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor Author

Cache patch was required to fix the issue. see updated cache files here: https://dl.00002011.xyz/cache.zip

Comment on lines +343 to +351
val temp = directory.resolve("${account.name.lowercase()}.toml.tmp")
try {
account.save(temp)
Files.move(temp.toPath(), file.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING)
} catch (e: Exception) {
temp.delete()
file.delete()
throw e
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the temp be in finally so it's cleared up after the move is complete?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, moved to a finally in 9d7c969 (the atomic move already consumes it on success, so the delete is a no-op there and cleanup on failure).

/**
* Accounts registered with an email address are given a placeholder display name until the player picks one on first login
*/
suspend fun Player.chooseDisplayName() {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be moved inside of Introduction and the tests can access it by createPlayer/playerSpawn

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved into Introduction as a private suspend function in 6bd9f5b; DisplayNameSelection.kt is gone and the test (now IntroductionTest) goes through createPlayer with world.start.creation off so playerSpawn drives the prompt.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants