package world import ( "github.com/go-gl/mathgl/mgl64" "maps" "math" "sync" ) // Loader implements the loading of the world. A loader can typically be moved around the world to load // different parts of the world. An example usage is the player, which uses a loader to load chunks around it // so that it can view them. type Loader struct { r int w *World viewer Viewer mu sync.RWMutex pos ChunkPos loadQueue []ChunkPos loaded map[ChunkPos]*Column closed bool } // NewLoader creates a new loader using the chunk radius passed. Chunks beyond this radius from the position // of the loader will never be loaded. // The Viewer passed will handle the loading of chunks, including the viewing of entities that were loaded in // those chunks. func NewLoader(chunkRadius int, world *World, v Viewer) *Loader { l := &Loader{r: chunkRadius, loaded: make(map[ChunkPos]*Column), viewer: v} l.world(world) return l } // World returns the World that the Loader is in. func (l *Loader) World() *World { l.mu.RLock() defer l.mu.RUnlock() return l.w } // ChangeWorld changes the World of the Loader. The currently loaded chunks are reset and any future loading // is done from the new World. func (l *Loader) ChangeWorld(tx *Tx, new *World) { l.mu.Lock() defer l.mu.Unlock() loaded := maps.Clone(l.loaded) l.w.Exec(func(tx *Tx) { for pos := range loaded { tx.World().removeViewer(tx, pos, l) } }) clear(l.loaded) l.w.viewerMu.Lock() delete(l.w.viewers, l) l.w.viewerMu.Unlock() l.world(new) } // ChangeRadius changes the maximum chunk radius of the Loader. func (l *Loader) ChangeRadius(tx *Tx, new int) { l.mu.Lock() defer l.mu.Unlock() l.r = new l.evictUnused(tx) l.populateLoadQueue() } // Move moves the loader to the position passed. The position is translated to a chunk position to load func (l *Loader) Move(tx *Tx, pos mgl64.Vec3) { l.mu.Lock() defer l.mu.Unlock() chunkPos := chunkPosFromVec3(pos) if chunkPos == l.pos { return } l.pos = chunkPos l.evictUnused(tx) l.populateLoadQueue() } // Load loads n chunks around the centre of the chunk, starting with the middle and working outwards. For // every chunk loaded, the Viewer passed through construction in New has its ViewChunk method called. // Load does nothing for n <= 0. func (l *Loader) Load(tx *Tx, n int) { l.mu.Lock() defer l.mu.Unlock() if l.closed || l.w == nil { return } for i := 0; i < n; i++ { if len(l.loadQueue) == 0 { break } pos := l.loadQueue[0] c := tx.w.chunk(pos) l.viewer.ViewChunk(pos, l.w.Dimension(), c.BlockEntities, c.Chunk) l.w.addViewer(tx, c, l) l.loaded[pos] = c // Shift the first element from the load queue off so that we can take a new one during the next // iteration. l.loadQueue = l.loadQueue[1:] } } // Chunk attempts to return a chunk at the given ChunkPos. If the chunk is not loaded, the second return value will // be false. func (l *Loader) Chunk(pos ChunkPos) (*Column, bool) { l.mu.RLock() c, ok := l.loaded[pos] l.mu.RUnlock() return c, ok } // Close closes the loader. It unloads all chunks currently loaded for the viewer, and hides all entities that // are currently shown to it. func (l *Loader) Close(tx *Tx) { l.mu.Lock() defer l.mu.Unlock() for pos := range l.loaded { tx.World().removeViewer(tx, pos, l) } l.loaded = map[ChunkPos]*Column{} l.w.viewerMu.Lock() delete(l.w.viewers, l) l.w.viewerMu.Unlock() l.closed = true l.viewer = nil } // world sets the loader's world, adds them to the world's viewer list, then starts populating the load queue. // This is only here to get rid of duplicated code, ChangeWorld should be used instead of this. func (l *Loader) world(new *World) { l.w = new l.w.addWorldViewer(l) l.populateLoadQueue() } // evictUnused gets rid of chunks in the loaded map which are no longer within the chunk radius of the loader, // and should therefore be removed. func (l *Loader) evictUnused(tx *Tx) { for pos := range l.loaded { diffX, diffZ := pos[0]-l.pos[0], pos[1]-l.pos[1] dist := math.Sqrt(float64(diffX*diffX) + float64(diffZ*diffZ)) if int(dist) > l.r { delete(l.loaded, pos) l.w.removeViewer(tx, pos, l) } } } // populateLoadQueue populates the load queue of the loader. This method is called once to create the order in // which chunks around the position the loader is now in should be loaded. Chunks are ordered to be loaded // from the middle outwards. func (l *Loader) populateLoadQueue() { // We'll first load the chunk positions to load in a map indexed by the distance to the centre (basically, // what precedence it should have), and put them in the loadQueue in that order. queue := map[int32][]ChunkPos{} r := int32(l.r) for x := -r; x <= r; x++ { for z := -r; z <= r; z++ { distance := math.Sqrt(float64(x*x) + float64(z*z)) chunkDistance := int32(math.Round(distance)) if chunkDistance > r { // The chunk was outside the chunk radius. continue } pos := ChunkPos{x + l.pos[0], z + l.pos[1]} if _, ok := l.loaded[pos]; ok { // The chunk was already loaded, so we don't need to do anything. continue } if m, ok := queue[chunkDistance]; ok { queue[chunkDistance] = append(m, pos) continue } queue[chunkDistance] = []ChunkPos{pos} } } l.loadQueue = l.loadQueue[:0] for i := int32(0); i < r; i++ { l.loadQueue = append(l.loadQueue, queue[i]...) } }