aboutsummaryrefslogtreecommitdiff
path: root/internal/game/account_txn.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-16 02:56:56 -0400
committerhistoria <[not public]>2026-09-16 02:56:56 -0400
commitf2d44f10f52d3fe68fdb19b518a330f7af9e1ed2 (patch)
tree9db54e8d4fd2f9bd30c651700f7cf246eef1b017 /internal/game/account_txn.go
parentad60843fbf577eda80105e3916822d663aaefcb3 (diff)
downloadthehouseoficarus-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.go50
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)
}
}