diff options
Diffstat (limited to 'internal/game/core_login_char.go')
| -rw-r--r-- | internal/game/core_login_char.go | 132 |
1 files changed, 71 insertions, 61 deletions
diff --git a/internal/game/core_login_char.go b/internal/game/core_login_char.go index 9d7c211..68a0d58 100644 --- a/internal/game/core_login_char.go +++ b/internal/game/core_login_char.go @@ -2,7 +2,6 @@ package game import ( "fmt" - "os" "strings" "thehouseoficarus/internal/net" @@ -34,33 +33,26 @@ func (g *Game) handleNewCharName(sess *net.Session, input string) { return } + // Everything below runs on the owner goroutine (session input): the + // existence check, file creation, and account publication are one + // transaction with no interleaved session input, so a concurrent + // creation of the same name cannot slip through the check. if g.AccountStore.CharacterExists(name) { sess.WriteLine("A character with that name already exists.") sess.Write("Character name: ") return } - p := player.New(name) - p.RoomID = g.StartingRoom - p.Stats.RecordRoomVisit(g.StartingRoom) - - if err := g.AccountStore.SaveCharacter(p); err != nil { - sess.WriteLine(fmt.Sprintf("Error creating character: %v", err)) - sess.State = net.StateMenu - g.showMenu(sess) - return - } - - acc, err := g.AccountStore.LoadAccount(sess.Account.Name) + p, err := g.createCharacterTxn(sess.Account.Name, name, g.StartingRoom) if err != nil { sess.WriteLine(fmt.Sprintf("Error creating character: %v", err)) sess.State = net.StateMenu g.showMenu(sess) return } - acc.Characters = append(acc.Characters, name) - g.AccountStore.SaveAccount(acc) - sess.Account.Characters = acc.Characters + _ = p + sess.Account.Characters = append(sess.Account.Characters, name) + g.publishAccountUpdate(sess.Account.Name) g.connectCharacter(sess, name) @@ -70,6 +62,29 @@ func (g *Game) handleNewCharName(sess *net.Session, input string) { } func (g *Game) connectCharacter(sess *net.Session, name string) { + // This whole function runs as one owner step (invoked from session + // input): membership validation, the online check, registry + // publication, and room entry are serialized against every other + // lifecycle operation, closing the check-then-register gap. + + // Authorize by ownership, not just existence: the character file's + // owner must match this account, so a stale cached name cannot connect + // a character reassigned to another account. + actualName, err := g.canonicalChar(name) + if err != nil { + sess.WriteLine(fmt.Sprintf("Error loading character: %v", err)) + sess.State = net.StateMenu + g.showMenu(sess) + return + } + if !g.isMemberOf(sess.Account.Name, actualName) { + sess.WriteLine("That character does not belong to this account.") + sess.State = net.StateMenu + g.showMenu(sess) + return + } + name = actualName + g.charsMu.Lock() if existing := g.loggedInChars[name]; existing != nil { g.charsMu.Unlock() @@ -87,6 +102,7 @@ func (g *Game) connectCharacter(sess *net.Session, name string) { g.showMenu(sess) return } + g.adoptCharacterIdentity(p, sess.Account.Name) sess.Player = p sess.State = net.StateGame @@ -171,49 +187,45 @@ func (g *Game) handleRenameCharName(sess *net.Session, input string) { g.showMenu(sess) return } - if g.AccountStore.CharacterExists(newName) { - sess.WriteLine("A character with that name already exists.") - sess.Write("New name: ") + + // Checks and mutation run as one owner step: no session input can + // interleave between the membership/online checks and the file + // rename, and the rename is a single journal transaction. + actualOld, err := g.canonicalChar(oldName) + if err != nil { + sess.WriteLine(fmt.Sprintf("Error renaming: %v", err)) + g.showMenu(sess) return } - if g.characterOnline(oldName) { - sess.WriteLine("That character is logged in elsewhere. Log them out first.") + if !g.isMemberOf(sess.Account.Name, actualOld) { + sess.WriteLine(fmt.Sprintf("No character named %s on this account.", oldName)) sess.PendingChar = "" g.showMenu(sess) return } - - oldPath := g.AccountStore.CharPath(oldName) - newPath := g.AccountStore.CharPath(newName) - if err := os.Rename(oldPath, newPath); err != nil { - sess.WriteLine(fmt.Sprintf("Error renaming: %v", err)) - g.showMenu(sess) + if newName != actualOld && g.AccountStore.CharacterExists(newName) { + sess.WriteLine("A character with that name already exists.") + sess.Write("New name: ") return } - - p, _ := g.AccountStore.LoadCharacter(newName) - if p != nil { - p.Name = newName - g.AccountStore.SaveCharacter(p) + if g.characterOnline(actualOld) { + sess.WriteLine("That character is logged in elsewhere. Log them out first.") + sess.PendingChar = "" + g.showMenu(sess) + return } - acc, err := g.AccountStore.LoadAccount(sess.Account.Name) - if err != nil { + if err := g.renameCharacterTxn(sess.Account.Name, actualOld, newName); err != nil { sess.WriteLine(fmt.Sprintf("Error renaming: %v", err)) g.showMenu(sess) return } - for i, c := range acc.Characters { - if c == oldName { - acc.Characters[i] = newName - break - } - } - g.AccountStore.SaveAccount(acc) - sess.Account.Characters = acc.Characters sess.PendingChar = "" - sess.WriteLine(fmt.Sprintf("%s renamed to %s.", oldName, newName)) + sess.WriteLine(fmt.Sprintf("%s renamed to %s.", actualOld, newName)) + // Refresh every session of this account so no stale character list or + // pending selection survives the rename. + g.publishAccountUpdate(sess.Account.Name) g.showMenu(sess) } @@ -256,39 +268,37 @@ func (g *Game) handleDeleteChar(sess *net.Session, input string) { return } - if g.characterOnline(sess.PendingChar) { - sess.WriteLine("That character is logged in elsewhere. Log them out first.") + // Checks and mutation run as one owner step; the delete is a single + // journal transaction (character file + account list). + actualName, err := g.canonicalChar(sess.PendingChar) + if err != nil { + sess.WriteLine(fmt.Sprintf("Error deleting: %v", err)) sess.PendingChar = "" g.showMenu(sess) return } - - charPath := g.AccountStore.CharPath(sess.PendingChar) - if err := os.Remove(charPath); err != nil { - sess.WriteLine(fmt.Sprintf("Error deleting: %v", err)) + if !g.isMemberOf(sess.Account.Name, actualName) { + sess.WriteLine(fmt.Sprintf("No character named %s on this account.", sess.PendingChar)) + sess.PendingChar = "" + g.showMenu(sess) + return + } + if g.characterOnline(actualName) { + sess.WriteLine("That character is logged in elsewhere. Log them out first.") sess.PendingChar = "" g.showMenu(sess) return } - acc, err := g.AccountStore.LoadAccount(sess.Account.Name) - if err != nil { + if err := g.deleteCharacterTxn(sess.Account.Name, actualName); err != nil { sess.WriteLine(fmt.Sprintf("Error deleting: %v", err)) sess.PendingChar = "" g.showMenu(sess) return } - var newChars []string - for _, c := range acc.Characters { - if c != sess.PendingChar { - newChars = append(newChars, c) - } - } - acc.Characters = newChars - g.AccountStore.SaveAccount(acc) - sess.Account.Characters = newChars - sess.WriteLine(fmt.Sprintf("%s has been deleted.", sess.PendingChar)) + sess.WriteLine(fmt.Sprintf("%s has been deleted.", actualName)) sess.PendingChar = "" + g.publishAccountUpdate(sess.Account.Name) g.showMenu(sess) } |
