package commands import ( "bytes" "encoding/binary" "fmt" "path/filepath" "sort" "strings" "git.kirsle.net/SketchyMaze/doodle/pkg/doodads" "git.kirsle.net/SketchyMaze/doodle/pkg/enum" "git.kirsle.net/SketchyMaze/doodle/pkg/level" "git.kirsle.net/SketchyMaze/doodle/pkg/level/rle" "git.kirsle.net/SketchyMaze/doodle/pkg/log" "github.com/urfave/cli/v2" ) // Show information about a Level or Doodad file. var Show *cli.Command func init() { Show = &cli.Command{ Name: "show", Usage: "show information about a level or doodad file", ArgsUsage: "<.level or .doodad>", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "actors", Usage: "print verbose actor data in Level files", }, &cli.BoolFlag{ Name: "chunks", Usage: "print verbose data about all the pixel chunks in a file", }, &cli.BoolFlag{ Name: "script", Usage: "print the script from a doodad file and exit", }, &cli.StringFlag{ Name: "attachment", Aliases: []string{"a"}, Usage: "print the contents of the attached filename to terminal", }, &cli.BoolFlag{ Name: "verbose", Aliases: []string{"v"}, Usage: "print verbose output (all verbose flags enabled)", }, &cli.BoolFlag{ Name: "visualize-rle", Usage: "visually dump RLE encoded chunks to the terminal (VERY noisy for large drawings!)", }, &cli.StringFlag{ Name: "chunk", Usage: "specific chunk coordinate; when debugging chunks, only show this chunk (example: 2,-1)", }, }, Action: func(c *cli.Context) error { if c.NArg() < 1 { return cli.Exit( "Usage: doodad show <.level .doodad ...>", 1, ) } filenames := c.Args().Slice() for _, filename := range filenames { switch strings.ToLower(filepath.Ext(filename)) { case enum.LevelExt: if err := showLevel(c, filename); err != nil { log.Error(err.Error()) return cli.Exit("Error", 1) } case enum.DoodadExt: if err := showDoodad(c, filename); err != nil { log.Error(err.Error()) return cli.Exit("Error", 1) } default: log.Error("File %s: not a level or doodad", filename) } } return nil }, } } // showLevel shows data about a level file. func showLevel(c *cli.Context, filename string) error { lvl, err := level.LoadJSON(filename) if err != nil { return err } // Are we printing an attached file? if filename := c.String("attachment"); filename != "" { if data, err := lvl.GetFile(filename); err == nil { fmt.Print(string(data)) return nil } else { fmt.Printf("Couldn't get attached file '%s': %s\n", filename, err) return err } } // Is it a new zipfile format? var fileType = "json or gzip" if lvl.Zipfile != nil { fileType = "zipfile" } fmt.Printf("===== Level: %s =====\n", filename) fmt.Println("Headers:") fmt.Printf(" File format: %s\n", fileType) fmt.Printf(" File version: %d\n", lvl.Version) fmt.Printf(" Game version: %s\n", lvl.GameVersion) fmt.Printf(" Level UUID: %s\n", lvl.UUID) fmt.Printf(" Level title: %s\n", lvl.Title) fmt.Printf(" Author: %s\n", lvl.Author) fmt.Printf(" Password: %s\n", lvl.Password) fmt.Printf(" Locked: %+v\n", lvl.Locked) fmt.Println("") fmt.Println("Game Rules:") fmt.Printf(" Difficulty: %s (%d)\n", lvl.GameRule.Difficulty, lvl.GameRule.Difficulty) fmt.Printf(" Survival: %+v\n", lvl.GameRule.Survival) fmt.Println("") showPalette(lvl.Palette) fmt.Println("Level Settings:") fmt.Printf(" Page type: %s\n", lvl.PageType.String()) fmt.Printf(" Max size: %dx%d\n", lvl.MaxWidth, lvl.MaxHeight) fmt.Printf(" Wallpaper: %s\n", lvl.Wallpaper) fmt.Println("") fmt.Println("Attached Files:") if files := lvl.ListFiles(); len(files) > 0 { for _, v := range files { data, _ := lvl.GetFile(v) fmt.Printf(" %s: %d bytes\n", v, len(data)) } fmt.Println("") } else { fmt.Printf(" None\n\n") } // Print the actor information. fmt.Println("Actors:") fmt.Printf(" Level contains %d actors\n", len(lvl.Actors)) if c.Bool("actors") || c.Bool("verbose") { fmt.Println(" List of Actors:") for id, actor := range lvl.Actors { fmt.Printf(" - Name: %s\n", actor.Filename) fmt.Printf(" UUID: %s\n", id) fmt.Printf(" At: %s\n", actor.Point) if len(actor.Options) > 0 { var ordered = []string{} for name := range actor.Options { ordered = append(ordered, name) } sort.Strings(ordered) fmt.Println(" Options:") for _, name := range ordered { val := actor.Options[name] fmt.Printf(" %s %s = %v\n", val.Type, val.Name, val.Value) } } if c.Bool("links") { for _, link := range actor.Links { if other, ok := lvl.Actors[link]; ok { fmt.Printf(" Link: %s (%s)\n", link, other.Filename) } else { fmt.Printf(" Link: %s (**UNRESOLVED**)", link) } } } } fmt.Println("") } else { fmt.Print(" Use -actors or -verbose to serialize Actors\n\n") } // Serialize chunk information. showChunker(c, lvl.Chunker, 0) fmt.Println("") return nil } func showDoodad(c *cli.Context, filename string) error { dd, err := doodads.LoadJSON(filename) if err != nil { return err } if c.Bool("script") { fmt.Printf("// %s.js\n", filename) fmt.Println(strings.TrimSpace(dd.Script)) return nil } // Is it a new zipfile format? var fileType = "json or gzip" if dd.Zipfile != nil { fileType = "zipfile" } fmt.Printf("===== Doodad: %s =====\n", filename) fmt.Println("Headers:") fmt.Printf(" File format: %s\n", fileType) fmt.Printf(" File version: %d\n", dd.Version) fmt.Printf(" Game version: %s\n", dd.GameVersion) fmt.Printf(" Doodad title: %s\n", dd.Title) fmt.Printf(" Author: %s\n", dd.Author) fmt.Printf(" Dimensions: %s\n", dd.Size) fmt.Printf(" Hitbox: %s\n", dd.Hitbox) fmt.Printf(" Locked: %+v\n", dd.Locked) fmt.Printf(" Hidden: %+v\n", dd.Hidden) fmt.Printf(" Script size: %d bytes\n", len(dd.Script)) fmt.Println("") if len(dd.Tags) > 0 { fmt.Println("Tags:") for k, v := range dd.Tags { fmt.Printf(" %s: %s\n", k, v) } fmt.Println("") } if len(dd.Options) > 0 { var ordered = []string{} for name := range dd.Options { ordered = append(ordered, name) } sort.Strings(ordered) fmt.Println("Options:") for _, name := range ordered { opt := dd.Options[name] fmt.Printf(" %s %s = %v\n", opt.Type, opt.Name, opt.Default) } fmt.Println("") } showPalette(dd.Palette) for i, layer := range dd.Layers { fmt.Printf("Layer %d: %s\n", i, layer.Name) showChunker(c, layer.Chunker, i) } fmt.Println("") return nil } func showPalette(pal *level.Palette) { fmt.Println("Palette:") for _, sw := range pal.Swatches { fmt.Printf(" - Swatch name: %s\n", sw.Name) fmt.Printf(" Attributes: %s\n", sw.Attributes()) fmt.Printf(" Color: %s\n", sw.Color.ToHex()) } fmt.Println("") } func showChunker(c *cli.Context, ch *level.Chunker, layer int) { var ( worldSize = ch.WorldSize() chunkSize = int(ch.Size) width = worldSize.W - worldSize.X height = worldSize.H - worldSize.Y // Chunk debugging CLI options. visualize = c.Bool("visualize-rle") specificChunk = c.String("chunk") ) // If it's a Zipfile, count its chunks. var chunkCount = len(ch.Chunks) if ch.Zipfile != nil { chunkCount = len(level.ChunksInZipfile(ch.Zipfile, layer)) } fmt.Println("Chunks:") fmt.Printf(" Pixels Per Chunk: %d^2\n", ch.Size) fmt.Printf(" Number Generated: %d\n", chunkCount) fmt.Printf(" Coordinate Range: (%d,%d) ... (%d,%d)\n", worldSize.X, worldSize.Y, worldSize.W, worldSize.H, ) fmt.Printf(" World Dimensions: %dx%d\n", width, height) // Verbose chunk information. if c.Bool("chunks") || c.Bool("verbose") { fmt.Println(" Chunk Details:") for point := range ch.IterChunks() { // Debugging specific chunk coordinate? if specificChunk != "" && point.String() != specificChunk { log.Warn("Skip chunk %s: not the specific chunk you're looking for", point) continue } chunk, ok := ch.GetChunk(point) if !ok { continue } fmt.Printf(" - Coord: %s\n", point) fmt.Printf(" Type: %s\n", chunkTypeToName(chunk.Type)) fmt.Printf(" Range: (%d,%d) ... (%d,%d)\n", int(point.X)*chunkSize, int(point.Y)*chunkSize, (int(point.X)*chunkSize)+chunkSize, (int(point.Y)*chunkSize)+chunkSize, ) fmt.Printf(" Usage: %f (%d len of %d)\n", chunk.Usage(), chunk.Len(), chunkSize*chunkSize) // Visualize the RLE encoded chunks? if visualize && chunk.Type == level.RLEType { ext, bin, err := ch.RawChunkFromZipfile(point) if err != nil { log.Error(err.Error()) continue } else if ext != ".bin" { log.Error("Unexpected filetype for RLE compressed chunk (expected .bin, got %s)", ext) continue } // Read off the first byte (chunk type) var reader = bytes.NewBuffer(bin) binary.ReadUvarint(reader) bin = reader.Bytes() grid, err := rle.NewGrid(chunkSize) if err != nil { log.Error(err.Error()) continue } grid.Decompress(bin) fmt.Println(grid.Visualize()) } } } else { fmt.Println(" Use -chunks or -verbose to serialize Chunks") } fmt.Println("") } func chunkTypeToName(v uint64) string { switch v { case level.MapType: return "map" case level.RLEType: return "rle map" default: return fmt.Sprintf("type %d", v) } }