diff --git a/cmd/panorama/main.go b/cmd/panorama/main.go index 4d249fa..22b0092 100644 --- a/cmd/panorama/main.go +++ b/cmd/panorama/main.go @@ -9,7 +9,7 @@ import ( "github.com/lord-server/panorama/internal/config" "github.com/lord-server/panorama/internal/game" "github.com/lord-server/panorama/internal/generator" - "github.com/lord-server/panorama/internal/generator/isometric" + "github.com/lord-server/panorama/internal/generator/flat" "github.com/lord-server/panorama/internal/generator/tile" "github.com/lord-server/panorama/internal/server" "github.com/lord-server/panorama/internal/world" @@ -64,20 +64,27 @@ func fullrender(config config.Config) error { return err } - backend, err := world.NewPostgresBackend(config.System.WorldDSN) + wd, err := world.NewWorld(config.System.WorldPath) if err != nil { - slog.Error("unable to connect to world DB", "error", err) - return err - } + slog.Error("unable to open world, falling back to DSN", + "err", err, + "world_path", config.System.WorldPath) - world := world.NewWorldWithBackend(backend) + backend, err := world.NewPostgresBackend(config.System.WorldDSN) + if err != nil { + slog.Error("unable to connect to world DB", "error", err) + return err + } + + wd = world.NewWorldWithBackend(backend) + } tiler := tile.NewTiler(config.Region, config.Renderer.ZoomLevels, config.System.TilesPath) slog.Info("performing a full render", "workers", config.Renderer.Workers, "region", config.Region) - tiler.FullRender(&game, &world, config.Renderer.Workers, config.Region, func() generator.Renderer { - return isometric.NewRenderer(config.Region, &game) + tiler.FullRender(&game, &wd, config.Renderer.Workers, config.Region, func() generator.Renderer { + return flat.NewRenderer(config.Region, &game) }) tiler.DownscaleTiles() diff --git a/config.example.toml b/config.example.toml index 02e9fc6..f9f7cc9 100644 --- a/config.example.toml +++ b/config.example.toml @@ -10,10 +10,11 @@ game_path = "/var/lib/panorama/game" world_path = "/var/lib/panorama/world" # Path to the directory containing the mods +# Default: "/var/lib/panorama/mods" mod_path = "/var/lib/panorama/mods" - -# DSN string used for connecting to PostgreSQL +# DSN string used for connecting to PostgreSQL. Overrides DSN found in +# world_path # Default: "" world_dsn = "" diff --git a/internal/game/media.go b/internal/game/media.go index 88700cb..e3c213c 100644 --- a/internal/game/media.go +++ b/internal/game/media.go @@ -35,6 +35,12 @@ func NewMediaCache() *MediaCache { func (m *MediaCache) fetchMedia(path string) error { return filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error { + if err != nil { + slog.Warn("encountered error while fetching media", "error", err, "dir_entry", d) + + return nil + } + if !d.Type().IsRegular() { return nil } diff --git a/internal/generator/flat/renderer.go b/internal/generator/flat/renderer.go index 67dcffa..f294d7c 100644 --- a/internal/generator/flat/renderer.go +++ b/internal/generator/flat/renderer.go @@ -2,6 +2,7 @@ package flat import ( "image" + "log/slog" "github.com/lord-server/panorama/internal/game" "github.com/lord-server/panorama/internal/generator" @@ -28,15 +29,31 @@ func NewRenderer(region geom.Region, game *game.Game) *FlatRenderer { func (r *FlatRenderer) RenderTile( tilePos generator.TilePosition, - world *world.World, + wd *world.World, game *game.Game, ) *rasterizer.RenderBuffer { rect := image.Rect(0, 0, 256, 256) target := rasterizer.NewRenderBuffer(rect) + err := wd.GetBlocksAlongY(tilePos.X, tilePos.Y, func(pos geom.BlockPosition, block *world.MapBlock) error { + return nil + }) + if err != nil { + slog.Error("unable to get blocks", "error", err) + } + return target } func (r *FlatRenderer) ProjectRegion(region geom.Region) geom.ProjectedRegion { - return geom.ProjectedRegion{} + return geom.ProjectedRegion{ + XBounds: geom.Bounds{ + Min: region.XBounds.Min / 16, + Max: region.XBounds.Max / 16, + }, + YBounds: geom.Bounds{ + Min: region.ZBounds.Min / 16, + Max: region.ZBounds.Max / 16, + }, + } } diff --git a/internal/world/meta.go b/internal/world/meta.go new file mode 100644 index 0000000..45326ea --- /dev/null +++ b/internal/world/meta.go @@ -0,0 +1,41 @@ +package world + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +type Meta map[string]string + +func ParseMeta(path string) (Meta, error) { + meta := make(map[string]string) + + file, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("can't read world metadata: %w", err) + } + + defer file.Close() + + sc := bufio.NewScanner(file) + + for sc.Scan() { + parts := strings.SplitN(sc.Text(), "=", 2) + if len(parts) != 2 { + continue + } + + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + + meta[key] = value + } + + if err := sc.Err(); err != nil { + return nil, err + } + + return meta, nil +} diff --git a/internal/world/world.go b/internal/world/world.go index 8755ec9..09986d8 100644 --- a/internal/world/world.go +++ b/internal/world/world.go @@ -3,6 +3,8 @@ package world import ( "context" "errors" + "fmt" + "path/filepath" lru "github.com/hashicorp/golang-lru/v2" "github.com/jackc/pgx/v5" @@ -12,6 +14,7 @@ import ( type Backend interface { GetBlockData(pos geom.BlockPosition) ([]byte, error) + GetBlocksAlongY(x, z int, callback func(geom.BlockPosition, []byte) error) error Close() } @@ -49,11 +52,80 @@ func (p *PostgresBackend) GetBlockData(pos geom.BlockPosition) ([]byte, error) { return data, nil } +func (p *PostgresBackend) GetBlocksAlongY(x, z int, callback func(geom.BlockPosition, []byte) error) error { + rows, err := p.conn.Query(context.Background(), "SELECT posx, posy, posz, data FROM blocks WHERE posx=$1 and posz=$2 ORDER BY posy", x, z) + if errors.Is(err, pgx.ErrNoRows) { + return nil + } + + if err != nil { + return err + } + + defer rows.Close() + + for rows.Next() { + var ( + pos geom.BlockPosition + data []byte + ) + + err = rows.Scan(&pos.X, &pos.Y, &pos.Z, &data) + if err != nil { + return err + } + + err = callback(pos, data) + if err != nil { + return err + } + } + + rows.Close() + + if err := rows.Err(); err != nil { + return err + } + + return nil +} + type World struct { backend Backend decodedBlockCache *lru.Cache[geom.BlockPosition, *MapBlock] } +func NewWorld(path string) (World, error) { + var world World + + meta, err := ParseMeta(filepath.Join(path, "world.mt")) + if err != nil { + return world, err + } + + backendName, ok := meta["backend"] + if !ok { + return world, errors.New("backend not specified") + } + + var backend Backend + + switch backendName { + case "postgresql": + dsn, ok := meta["pgsql_connection"] + if !ok { + return world, errors.New("postgresql connection not specified") + } + + backend, err = NewPostgresBackend(dsn) + if err != nil { + return world, fmt.Errorf("unable to create PostgreSQL backend: %w", err) + } + } + + return NewWorldWithBackend(backend), nil +} + func NewWorldWithBackend(backend Backend) World { decodedBlockCache, err := lru.New[geom.BlockPosition, *MapBlock](1024 * 16) if err != nil { @@ -96,3 +168,26 @@ func (w *World) GetBlock(pos geom.BlockPosition) (*MapBlock, error) { return block, nil } + +func (w *World) GetBlocksAlongY(x, z int, callback func(geom.BlockPosition, *MapBlock) error) error { + return w.backend.GetBlocksAlongY(x, z, func(pos geom.BlockPosition, data []byte) error { + cachedBlock, ok := w.decodedBlockCache.Get(pos) + + if ok { + if cachedBlock == nil { + return nil + } + + return callback(pos, cachedBlock) + } + + block, err := DecodeMapBlock(data) + if err != nil { + return err + } + + w.decodedBlockCache.Add(pos, block) + + return callback(pos, block) + }) +}