diff options
| author | historia <[not public]> | 2026-09-16 02:56:56 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-16 02:56:56 -0400 |
| commit | f2d44f10f52d3fe68fdb19b518a330f7af9e1ed2 (patch) | |
| tree | 9db54e8d4fd2f9bd30c651700f7cf246eef1b017 /internal/game/account_txn.go | |
| parent | ad60843fbf577eda80105e3916822d663aaefcb3 (diff) | |
| download | thehouseoficarus-f2d44f10f52d3fe68fdb19b518a330f7af9e1ed2.tar.gz | |
fix: disconnect race, rename ownership, reserved pickup, economy recovery retry/refund semantics
Diffstat (limited to 'internal/game/account_txn.go')
| -rw-r--r-- | internal/game/account_txn.go | 50 |
1 files changed, 44 insertions, 6 deletions
diff --git a/internal/game/account_txn.go b/internal/game/account_txn.go index 488a5f6..2ed62bb 100644 --- a/internal/game/account_txn.go +++ b/internal/game/account_txn.go @@ -162,6 +162,11 @@ func (g *Game) renameCharacterTxn(accountName, oldName, newName string) error { return err } p.Name = newName + if p.ID == "" { + // Assign the stable identity at rename time so journal records + // written after this point resolve the renamed character by ID. + p.ID = newCharacterID() + } data, err := g.AccountStore.MarshalCharacter(p) if err != nil { return err @@ -235,8 +240,11 @@ func (g *Game) purgeAccountTxn(accountName string, characters []string) error { return txn.Commit() } -// renameAccountTxn moves the account file and rewrites its embedded name in -// one transaction. +// renameAccountTxn moves the account file, rewrites its embedded name, +// and rewrites every owned character file's owner field in one +// transaction. Without the character rewrite the renamed account loses +// access to its characters: membership authorization compares the +// character file's owner against the account name. func (g *Game) renameAccountTxn(oldName, newName string) error { txn := g.newTxn() if err := txn.Move(g.AccountStore.AccountPath(oldName), g.AccountStore.AccountPath(newName)); err != nil { @@ -254,6 +262,24 @@ func (g *Game) renameAccountTxn(oldName, newName string) error { if err := txn.Replace(g.AccountStore.AccountPath(newName), accData, 0600); err != nil { return err } + // Re-point each owned character at the new account name inside the + // same transaction. Legacy or unreadable character files are skipped: + // membership falls back to the account list, which is already + // rewritten above. + for _, charName := range acc.Characters { + p, err := g.AccountStore.LoadCharacter(charName) + if err != nil || p.Account == "" || !equalsFold(p.Account, oldName) { + continue + } + p.Account = newName + charData, err := g.AccountStore.MarshalCharacter(p) + if err != nil { + return err + } + if err := txn.Replace(g.AccountStore.CharPath(charName), charData, 0600); err != nil { + return err + } + } return txn.Commit() } @@ -284,9 +310,14 @@ func (g *Game) isMemberOf(accountName, characterName string) bool { // state to every session of that account, resetting stale pending menu // selections (pending character names, menu choices) after lifecycle // mutations. Called from the owner goroutine only. -func (g *Game) publishAccountUpdate(accountName string) { - for _, sess := range g.allSessionsOfAccount(accountName) { - acc, err := g.AccountStore.LoadAccount(accountName) +// +// oldName and newName differ only for an account rename: sessions +// authenticated before the rename still carry the OLD cached account +// name, so both must match or stale sessions would be left bound to a +// name that no longer exists on disk. +func (g *Game) publishAccountUpdate(oldName, newName string) { + for _, sess := range g.allSessionsOfEitherAccount(oldName, newName) { + acc, err := g.AccountStore.LoadAccount(newName) if err != nil { // The account file vanished (purge from another session); the // session is stale and must leave the menu. @@ -338,12 +369,19 @@ func (g *Game) publishAccountUpdate(accountName string) { // allSessionsOfAccount returns every session authenticated to accountName, // including sessions sitting in menus (not just in-game characters). func (g *Game) allSessionsOfAccount(accountName string) []*net.Session { + return g.allSessionsOfEitherAccount(accountName, accountName) +} + +// allSessionsOfEitherAccount matches sessions authenticated to either +// account name (an account rename leaves stale sessions under the old +// name until they are republished). +func (g *Game) allSessionsOfEitherAccount(oldName, newName string) []*net.Session { if g.Hub == nil { return nil } var out []*net.Session for _, sess := range g.Hub.AllSessions() { - if sess.Account != nil && equalsFold(sess.Account.Name, accountName) { + if sess.Account != nil && (equalsFold(sess.Account.Name, oldName) || equalsFold(sess.Account.Name, newName)) { out = append(out, sess) } } |
