From e253f4da012314816dfad2bd5474f13ccb8f4b67 Mon Sep 17 00:00:00 2001 From: Alexander <123891354+FDUTCH@users.noreply.github.com> Date: Wed, 29 Jul 2026 00:00:35 +0200 Subject: [PATCH 1/3] run PlayerProvider.Save() asyncroniusly --- server/server.go | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/server/server.go b/server/server.go index 6865291ba..c63a2a75c 100644 --- a/server/server.go +++ b/server/server.go @@ -541,25 +541,32 @@ func (srv *Server) dimension(dimension world.Dimension) *world.World { // handleSessionClose handles the closing of a session. It removes the player // of the session from the server. func (srv *Server) handleSessionClose(tx *world.Tx, c session.Controllable) { - srv.pmu.Lock() - _, ok := srv.p[c.UUID()] - delete(srv.p, c.UUID()) - srv.pmu.Unlock() - if !ok { + id := c.UUID() + if _, online := srv.Player(id); !online { // When a player disconnects immediately after a session is started, it // might not be added to the players map yet. This is expected, but we // need to be careful not to crash when this happens. return } - if tx != nil { - if err := srv.conf.PlayerProvider.Save(c.UUID(), c.(*player.Player).Data(), tx.World()); err != nil { - srv.conf.Log.Error("Save player data: " + err.Error()) + data := c.(*player.Player).Data() + w := tx.World() + go func() { + if tx != nil { + if err := srv.conf.PlayerProvider.Save(id, data, w); err != nil { + srv.conf.Log.Error("Save player data: " + err.Error()) + } + } else { + srv.conf.Log.Error("Save player data: player's worlds closed before teardown; data not saved", "uuid", c.UUID()) } - } else { - srv.conf.Log.Error("Save player data: player's worlds closed before teardown; data not saved", "uuid", c.UUID()) - } - srv.pwg.Done() + + // removing player from the list after saving their data, + // to prevent possible race condition exploits. + srv.pmu.Lock() + delete(srv.p, c.UUID()) + srv.pmu.Unlock() + srv.pwg.Done() + }() } // createPlayer creates a new player instance using the UUID and connection From 4a204f5024a36b7caec1a99819e0a62fead0b905 Mon Sep 17 00:00:00 2001 From: Alexander <123891354+FDUTCH@users.noreply.github.com> Date: Wed, 29 Jul 2026 18:40:07 +0200 Subject: [PATCH 2/3] requested --- server/server.go | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/server/server.go b/server/server.go index c63a2a75c..162cf542b 100644 --- a/server/server.go +++ b/server/server.go @@ -542,33 +542,36 @@ func (srv *Server) dimension(dimension world.Dimension) *world.World { // of the session from the server. func (srv *Server) handleSessionClose(tx *world.Tx, c session.Controllable) { id := c.UUID() + if tx == nil { + srv.conf.Log.Error("Save player data: player's worlds closed before teardown; data not saved", "uuid", c.UUID()) + srv.removePlayerFromList(id) + return + } + if _, online := srv.Player(id); !online { // When a player disconnects immediately after a session is started, it // might not be added to the players map yet. This is expected, but we // need to be careful not to crash when this happens. return } - data := c.(*player.Player).Data() w := tx.World() go func() { - if tx != nil { - if err := srv.conf.PlayerProvider.Save(id, data, w); err != nil { - srv.conf.Log.Error("Save player data: " + err.Error()) - } - } else { - srv.conf.Log.Error("Save player data: player's worlds closed before teardown; data not saved", "uuid", c.UUID()) + if err := srv.conf.PlayerProvider.Save(id, data, w); err != nil { + srv.conf.Log.Error("Save player data: " + err.Error()) } - - // removing player from the list after saving their data, - // to prevent possible race condition exploits. - srv.pmu.Lock() - delete(srv.p, c.UUID()) - srv.pmu.Unlock() - srv.pwg.Done() + srv.removePlayerFromList(id) }() } +// removePlayerFromList removes player from the server's internal player list. +func (srv *Server) removePlayerFromList(id uuid.UUID) { + srv.pmu.Lock() + delete(srv.p, id) + srv.pmu.Unlock() + srv.pwg.Done() +} + // createPlayer creates a new player instance using the UUID and connection // passed. func (srv *Server) createPlayer(id uuid.UUID, conn session.Conn, conf player.Config, w *world.World) incoming { From efdfc6fd0e69434aa56f9a5ae258466e1f194ceb Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Thu, 30 Jul 2026 20:40:11 -0500 Subject: [PATCH 3/3] Fix player state during asynchronous saves --- server/conf.go | 1 + server/server.go | 32 +++++++++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/server/conf.go b/server/conf.go index 839559a13..ddd22943e 100644 --- a/server/conf.go +++ b/server/conf.go @@ -189,6 +189,7 @@ func (conf Config) New() *Server { conf: conf, incoming: make(chan incoming), p: make(map[uuid.UUID]*onlinePlayer), + saving: make(map[uuid.UUID]struct{}), world: &world.World{}, nether: &world.World{}, end: &world.World{}, } for _, lf := range conf.Listeners { diff --git a/server/server.go b/server/server.go index 162cf542b..280a93424 100644 --- a/server/server.go +++ b/server/server.go @@ -58,6 +58,9 @@ type Server struct { // p holds a map of all players currently connected to the server. When they // leave, they are removed from the map. p map[uuid.UUID]*onlinePlayer + // saving holds the UUIDs of players whose data is still being saved. New + // sessions are rejected until their previous data has been persisted. + saving map[uuid.UUID]struct{} // pwg is a sync.WaitGroup used to wait for all players to be disconnected // before server shutdown, so that their data is saved properly. pwg sync.WaitGroup @@ -460,6 +463,11 @@ func (srv *Server) wait() { // channel. func (srv *Server) finaliseConn(ctx context.Context, conn session.Conn, l Listener) { id := uuid.MustParse(conn.IdentityData().Identity) + if srv.playerConnectedOrSaving(id) { + _ = l.Disconnect(conn, "Already logged in.") + srv.conf.Log.Debug("spawn failed: already logged in", "raddr", conn.RemoteAddr()) + return + } data := srv.defaultGameData() d, w, err := srv.conf.PlayerProvider.Load(id, srv.dimension) @@ -482,7 +490,7 @@ func (srv *Server) finaliseConn(ctx context.Context, conn session.Conn, l Listen srv.conf.Log.Debug("spawn failed: "+err.Error(), "raddr", conn.RemoteAddr()) return } - if _, ok := srv.Player(id); ok { + if srv.playerConnectedOrSaving(id) { _ = l.Disconnect(conn, "Already logged in.") srv.conf.Log.Debug("spawn failed: already logged in", "raddr", conn.RemoteAddr()) return @@ -548,22 +556,40 @@ func (srv *Server) handleSessionClose(tx *world.Tx, c session.Controllable) { return } - if _, online := srv.Player(id); !online { + srv.pmu.Lock() + if _, ok := srv.p[id]; !ok { + srv.pmu.Unlock() // When a player disconnects immediately after a session is started, it // might not be added to the players map yet. This is expected, but we // need to be careful not to crash when this happens. return } + delete(srv.p, id) + srv.saving[id] = struct{}{} + srv.pmu.Unlock() + data := c.(*player.Player).Data() w := tx.World() go func() { if err := srv.conf.PlayerProvider.Save(id, data, w); err != nil { srv.conf.Log.Error("Save player data: " + err.Error()) } - srv.removePlayerFromList(id) + srv.pmu.Lock() + delete(srv.saving, id) + srv.pmu.Unlock() + srv.pwg.Done() }() } +// playerConnectedOrSaving checks if a player is connected or still saving. +func (srv *Server) playerConnectedOrSaving(id uuid.UUID) bool { + srv.pmu.RLock() + defer srv.pmu.RUnlock() + _, connected := srv.p[id] + _, saving := srv.saving[id] + return connected || saving +} + // removePlayerFromList removes player from the server's internal player list. func (srv *Server) removePlayerFromList(id uuid.UUID) { srv.pmu.Lock()