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)
|
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 {
|
func countPackets(packets []protocol.Packet, id int32) int {
|
||||||
count := 0
|
count := 0
|
||||||
for _, packet := range packets {
|
for _, packet := range packets {
|
||||||
|
|
|
||||||
|
|
@ -261,7 +261,8 @@ func (h *handler) syncVisibleEntities() error {
|
||||||
|
|
||||||
currentEntities := make(map[int32]visibleEntity)
|
currentEntities := make(map[int32]visibleEntity)
|
||||||
for _, player := range players {
|
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
|
continue
|
||||||
}
|
}
|
||||||
currentEntities[player.EntityID] = visibleEntity{
|
currentEntities[player.EntityID] = visibleEntity{
|
||||||
|
|
@ -275,7 +276,7 @@ func (h *handler) syncVisibleEntities() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, entity := range h.srv.Entities().All() {
|
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}
|
currentEntities[entity.ID] = visibleEntity{entity: entity, onGround: true}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -314,6 +315,15 @@ func (h *handler) syncVisibleEntities() error {
|
||||||
return nil
|
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 {
|
func playerVisible(viewer, target server.PlayerSnapshot, radius int) bool {
|
||||||
return chunksWithin(viewer.X, viewer.Z, target.X, target.Z, radius)
|
return chunksWithin(viewer.X, viewer.Z, target.X, target.Z, radius)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,12 @@ type streamer struct {
|
||||||
centerZ int32
|
centerZ int32
|
||||||
hasCenter bool
|
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
|
viewRadius int // chunks within this Chebyshev radius are sent to the client
|
||||||
genRadius int // viewRadius + 1: pre-generated but not sent (predictive ring)
|
genRadius int // viewRadius + 1: pre-generated but not sent (predictive ring)
|
||||||
poolSize int // parallel generation workers
|
poolSize int // parallel generation workers
|
||||||
|
|
@ -75,6 +81,7 @@ func newStreamer(cache *world.Cache, conn *Conn, log *slog.Logger, viewDistance
|
||||||
log: log,
|
log: log,
|
||||||
recenter: make(chan recenterReq, 4),
|
recenter: make(chan recenterReq, 4),
|
||||||
loaded: make(map[[2]int32]bool),
|
loaded: make(map[[2]int32]bool),
|
||||||
|
resident: make(map[[2]int32]bool),
|
||||||
viewRadius: viewDistance,
|
viewRadius: viewDistance,
|
||||||
genRadius: viewDistance + 1,
|
genRadius: viewDistance + 1,
|
||||||
poolSize: pool,
|
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.
|
// only server-side by tickets and never left loaded on the client.
|
||||||
for key := range s.loaded {
|
for key := range s.loaded {
|
||||||
if !view[key] {
|
if !view[key] {
|
||||||
|
s.setResident(key, false)
|
||||||
s.sendForgetLevelChunk(key[0], key[1])
|
s.sendForgetLevelChunk(key[0], key[1])
|
||||||
delete(s.loaded, key)
|
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.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,
|
// parallelPreload warms terrain for the given chunks without calculating light,
|
||||||
// encoding frames, or sending packets. Errors are ignored.
|
// encoding frames, or sending packets. Errors are ignored.
|
||||||
func (s *streamer) parallelPreload(ctx context.Context, keys [][2]int32) {
|
func (s *streamer) parallelPreload(ctx context.Context, keys [][2]int32) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue