Gate entities on client chunk residency
This commit is contained in:
parent
2b07d6be20
commit
4453bdeb55
3 changed files with 79 additions and 2 deletions
|
|
@ -270,6 +270,47 @@ func TestIntegrationFourClientsVisibilityMovementLeaveAndLight(t *testing.T) {
|
|||
srv.Entities().Remove(mobID)
|
||||
}
|
||||
|
||||
func TestEntityVisibilityWaitsForResidentChunk(t *testing.T) {
|
||||
cfg := server.DefaultConfig()
|
||||
cfg.WorldDir = ""
|
||||
srv, err := server.NewWithCache(cfg, world.NewCache(-1, world.GenerateFlat))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
recorder := &recordingConn{}
|
||||
h := &handler{conn: NewConn(recorder), srv: srv, log: slog.Default(), viewDistance: 2}
|
||||
h.conn.Profile = server.Profile{Name: "Alice", UUID: server.OfflineUUID("Alice")}
|
||||
h.session, err = srv.RegisterPlayer(h.conn.Profile, h.conn.Send)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer srv.UnregisterPlayer(h.session)
|
||||
srv.SetPlayerTransform(h.session, 8, 80, 8, 0, 0, true)
|
||||
h.streamer = &streamer{resident: make(map[[2]int32]bool)}
|
||||
srv.Entities().Add(&world.Entity{
|
||||
TypeID: 100, TypeName: "minecraft:pig", X: 24, Y: 80, Z: 8,
|
||||
})
|
||||
|
||||
if err := h.syncVisibleEntities(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := countPackets(recorder.take(t), protocol.PlayAddEntity); got != 0 {
|
||||
t.Fatalf("add-entity packets before chunk = %d, want 0", got)
|
||||
}
|
||||
|
||||
h.streamer.setResident([2]int32{1, 0}, true)
|
||||
if err := h.syncVisibleEntities(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertPacketIDs(t, recorder.take(t), protocol.PlayAddEntity)
|
||||
|
||||
h.streamer.setResident([2]int32{1, 0}, false)
|
||||
if err := h.syncVisibleEntities(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertPacketIDs(t, recorder.take(t), protocol.PlayRemoveEntities)
|
||||
}
|
||||
|
||||
func countPackets(packets []protocol.Packet, id int32) int {
|
||||
count := 0
|
||||
for _, packet := range packets {
|
||||
|
|
|
|||
|
|
@ -261,7 +261,8 @@ func (h *handler) syncVisibleEntities() error {
|
|||
|
||||
currentEntities := make(map[int32]visibleEntity)
|
||||
for _, player := range players {
|
||||
if player.EntityID == viewer.EntityID || !playerVisible(viewer, player, h.visibilityRadius()) {
|
||||
if player.EntityID == viewer.EntityID || !playerVisible(viewer, player, h.visibilityRadius()) ||
|
||||
!h.entityChunkResident(player.X, player.Z) {
|
||||
continue
|
||||
}
|
||||
currentEntities[player.EntityID] = visibleEntity{
|
||||
|
|
@ -275,7 +276,7 @@ func (h *handler) syncVisibleEntities() error {
|
|||
}
|
||||
}
|
||||
for _, entity := range h.srv.Entities().All() {
|
||||
if entityVisible(viewer, entity, h.visibilityRadius()) {
|
||||
if entityVisible(viewer, entity, h.visibilityRadius()) && h.entityChunkResident(entity.X, entity.Z) {
|
||||
currentEntities[entity.ID] = visibleEntity{entity: entity, onGround: true}
|
||||
}
|
||||
}
|
||||
|
|
@ -314,6 +315,15 @@ func (h *handler) syncVisibleEntities() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (h *handler) entityChunkResident(x, z float64) bool {
|
||||
if h.streamer == nil {
|
||||
return true
|
||||
}
|
||||
cx := int32(int64(math.Floor(x)) >> 4)
|
||||
cz := int32(int64(math.Floor(z)) >> 4)
|
||||
return h.streamer.isResident(cx, cz)
|
||||
}
|
||||
|
||||
func playerVisible(viewer, target server.PlayerSnapshot, radius int) bool {
|
||||
return chunksWithin(viewer.X, viewer.Z, target.X, target.Z, radius)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,12 @@ type streamer struct {
|
|||
centerZ int32
|
||||
hasCenter bool
|
||||
|
||||
// resident is the cross-goroutine view of chunks whose level_chunk packet
|
||||
// has reached the connection. Entity sync uses it to avoid exposing mobs or
|
||||
// players before the terrain under them exists client-side.
|
||||
residentMu sync.RWMutex
|
||||
resident map[[2]int32]bool
|
||||
|
||||
viewRadius int // chunks within this Chebyshev radius are sent to the client
|
||||
genRadius int // viewRadius + 1: pre-generated but not sent (predictive ring)
|
||||
poolSize int // parallel generation workers
|
||||
|
|
@ -75,6 +81,7 @@ func newStreamer(cache *world.Cache, conn *Conn, log *slog.Logger, viewDistance
|
|||
log: log,
|
||||
recenter: make(chan recenterReq, 4),
|
||||
loaded: make(map[[2]int32]bool),
|
||||
resident: make(map[[2]int32]bool),
|
||||
viewRadius: viewDistance,
|
||||
genRadius: viewDistance + 1,
|
||||
poolSize: pool,
|
||||
|
|
@ -173,6 +180,7 @@ func (s *streamer) processRecenter(ctx context.Context, cx, cz int32) (recenterR
|
|||
// only server-side by tickets and never left loaded on the client.
|
||||
for key := range s.loaded {
|
||||
if !view[key] {
|
||||
s.setResident(key, false)
|
||||
s.sendForgetLevelChunk(key[0], key[1])
|
||||
delete(s.loaded, key)
|
||||
}
|
||||
|
|
@ -344,9 +352,27 @@ func (s *streamer) parallelSend(ctx context.Context, keys [][2]int32) {
|
|||
}
|
||||
}
|
||||
s.loaded[[2]int32{j.cx, j.cz}] = true
|
||||
s.setResident([2]int32{j.cx, j.cz}, true)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *streamer) setResident(key [2]int32, present bool) {
|
||||
s.residentMu.Lock()
|
||||
if present {
|
||||
s.resident[key] = true
|
||||
} else {
|
||||
delete(s.resident, key)
|
||||
}
|
||||
s.residentMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *streamer) isResident(cx, cz int32) bool {
|
||||
s.residentMu.RLock()
|
||||
present := s.resident[[2]int32{cx, cz}]
|
||||
s.residentMu.RUnlock()
|
||||
return present
|
||||
}
|
||||
|
||||
// parallelPreload warms terrain for the given chunks without calculating light,
|
||||
// encoding frames, or sending packets. Errors are ignored.
|
||||
func (s *streamer) parallelPreload(ctx context.Context, keys [][2]int32) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue