diff --git a/storage/simple_server.go b/storage/simple_server.go index 738b1fa..699c428 100644 --- a/storage/simple_server.go +++ b/storage/simple_server.go @@ -12,12 +12,16 @@ import ( const fileCacheSize = 8 +var ( + ChunkNotFoundError = errors.New("chunk was not found in storage") +) + type SimpleServer struct { } // Filesystem operations -func (s *SimpleServer) FetchChunk(pos world.ChunkPos) (world.ChunkData, error) { +func (s *SimpleServer) FetchOrCreateChunk(pos world.ChunkPos) (world.ChunkData, error) { chunkFileName := filepath.Join(ChunkFileDirectory, pos.ToFileName()) var chunkData world.ChunkData @@ -43,6 +47,25 @@ func (s *SimpleServer) FetchChunk(pos world.ChunkPos) (world.ChunkData, error) { return chunkData, err } } + defer chunkFile.Close() + + return ReadChunkFromFile(chunkFile) +} + +func (s *SimpleServer) FetchChunk(pos world.ChunkPos) (world.ChunkData, error) { + chunkFileName := filepath.Join(ChunkFileDirectory, pos.ToFileName()) + + var chunkData world.ChunkData + + chunkFile, err := os.Open(chunkFileName) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return chunkData, ChunkNotFoundError + } else { + return chunkData, err + } + } + defer chunkFile.Close() return ReadChunkFromFile(chunkFile) } @@ -53,7 +76,7 @@ func (s *SimpleServer) ChangeBlock( worldPosition world.BlockPos, targetState world.BlockID, ) error { - chunk, err := s.FetchChunk(worldPosition.ToChunkPos()) + chunk, err := s.FetchOrCreateChunk(worldPosition.ToChunkPos()) if err != nil { return err } @@ -71,7 +94,7 @@ func (s *SimpleServer) ChangeBlockRange( } func (s *SimpleServer) ReadBlockAt(pos world.BlockPos) (world.BlockID, error) { - chunk, err := s.FetchChunk(pos.ToChunkPos()) + chunk, err := s.FetchOrCreateChunk(pos.ToChunkPos()) if err != nil { return world.Empty, err } @@ -80,5 +103,5 @@ func (s *SimpleServer) ReadBlockAt(pos world.BlockPos) (world.BlockID, error) { } func (s *SimpleServer) ReadChunkAt(pos world.ChunkPos) (world.ChunkData, error) { - return s.FetchChunk(pos) + return s.FetchOrCreateChunk(pos) } diff --git a/visualization/visualize_chunk.go b/visualization/visualize_chunk.go index 983f2d6..9c24efc 100644 --- a/visualization/visualize_chunk.go +++ b/visualization/visualize_chunk.go @@ -1,66 +1,138 @@ package visualization import ( - "fmt" + "errors" "strings" + "git.nicholasnovak.io/nnovak/spatial-db/storage" + "git.nicholasnovak.io/nnovak/spatial-db/world" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/spf13/cobra" ) +const ( + chunkWidth = 15 + chunkHeight = 5 + + borderPadding = 2 +) + var ( - modelStyle = lipgloss.NewStyle(). - Width(15). - Height(5). - Align(lipgloss.Center, lipgloss.Center). - BorderStyle(lipgloss.HiddenBorder()) - focusedModelStyle = lipgloss.NewStyle(). - Width(15). - Height(5). + missingChunkStyle = lipgloss.NewStyle(). + Width(chunkWidth). + Height(chunkHeight). + Align(lipgloss.Center, lipgloss.Center). + BorderStyle(lipgloss.HiddenBorder()). + BorderForeground(lipgloss.Color("69")) + presentChunkStyle = lipgloss.NewStyle(). + Width(chunkWidth). + Height(chunkHeight). Align(lipgloss.Center, lipgloss.Center). BorderStyle(lipgloss.NormalBorder()). BorderForeground(lipgloss.Color("69")) - spinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("69")) - helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")) + selectedChunkStyle = lipgloss.NewStyle(). + Width(chunkWidth). + Height(chunkHeight). + Align(lipgloss.Center, lipgloss.Center). + BorderStyle(lipgloss.DoubleBorder()). + BorderForeground(lipgloss.Color("202")) + + loadedChunkCache = make(map[world.ChunkPos]bool) ) type chunkViewerModel struct { - index int + chunkServer *storage.SimpleServer + + visibleChunkRows int + visibleChunkCols int + + currentPos world.ChunkPos +} + +func (m *chunkViewerModel) updateShownChunks(newWidth, newHeight int) { + m.visibleChunkRows = newHeight / (chunkHeight + borderPadding) + m.visibleChunkCols = newWidth / (chunkWidth + borderPadding) } func (m chunkViewerModel) Init() tea.Cmd { - return nil + return tea.EnterAltScreen } func (m chunkViewerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { - case "ctrl+c", "q": + case "ctrl+c", "q", "esc": return m, tea.Quit } switch msg.Type { case tea.KeyRight: - // pass + m.currentPos.X += 1 + case tea.KeyLeft: + m.currentPos.X -= 1 + case tea.KeyUp: + m.currentPos.Z -= 1 + case tea.KeyDown: + m.currentPos.Z += 1 } + case tea.WindowSizeMsg: + m.updateShownChunks(msg.Width, msg.Height) } return m, nil } func (m chunkViewerModel) View() string { var s strings.Builder - if m.index == 0 { - s.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, focusedModelStyle.Render(fmt.Sprintf("%4s", "No")), modelStyle.Render("NO"))) - } else { - s.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, modelStyle.Render(fmt.Sprintf("%4s", "Yes")), focusedModelStyle.Render("YES"))) + + midRow := m.visibleChunkRows / 2 + midCol := m.visibleChunkCols / 2 + + for rowIndex := 0; rowIndex < m.visibleChunkRows; rowIndex++ { + renderedRow := make([]string, m.visibleChunkCols) + for colIndex := 0; colIndex < m.visibleChunkCols; colIndex++ { + currentChunkPos := world.ChunkPos{ + X: midCol - colIndex - m.currentPos.X, + Z: midRow - rowIndex - m.currentPos.Z, + } + + var fetchChunkErr error + if isPresent, cached := loadedChunkCache[currentChunkPos]; cached { + if isPresent { + fetchChunkErr = nil + } else { + fetchChunkErr = storage.ChunkNotFoundError + } + } else { + _, fetchChunkErr = m.chunkServer.FetchChunk(currentChunkPos) + loadedChunkCache[currentChunkPos] = fetchChunkErr == nil + } + + chunkDisplay := currentChunkPos.StringCoords() + + if rowIndex == midRow && colIndex == midCol { + renderedRow[colIndex] = selectedChunkStyle.Render(chunkDisplay) + } else { + if fetchChunkErr == nil { + renderedRow[colIndex] = presentChunkStyle.Render(chunkDisplay) + } else if errors.Is(fetchChunkErr, storage.ChunkNotFoundError) { + renderedRow[colIndex] = missingChunkStyle.Render(chunkDisplay) + } else { + panic(fetchChunkErr) + } + } + } + s.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, renderedRow...) + "\n") } + return s.String() } -func initChunkViewer() chunkViewerModel { +func initChunkViewer(chunkServer *storage.SimpleServer) chunkViewerModel { var model chunkViewerModel + model.chunkServer = chunkServer + return model } @@ -70,7 +142,13 @@ var VisualizeChunkCommand = &cobra.Command{ Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - prog := tea.NewProgram(initChunkViewer()) + // Initialize the server in the specified directory + storage.ChunkFileDirectory = args[0] + + // Create a new server to read from those files + var chunkServer storage.SimpleServer + + prog := tea.NewProgram(initChunkViewer(&chunkServer), tea.WithAltScreen()) if _, err := prog.Run(); err != nil { return err diff --git a/world/data_format.go b/world/data_format.go index 393acf6..c508438 100644 --- a/world/data_format.go +++ b/world/data_format.go @@ -36,10 +36,14 @@ type ChunkPos struct { Z int `json:"z"` } -func (cp *ChunkPos) ToFileName() string { +func (cp ChunkPos) ToFileName() string { return fmt.Sprintf("p.%d.%d.chunk", cp.X, cp.Z) } +func (cp ChunkPos) StringCoords() string { + return fmt.Sprintf("%d, %d", cp.X, cp.Z) +} + type ChunkSection struct { // The count of full blocks in the chunk BlockCount uint `json:"block_count"`