From 2af5bd222ecb5be2d6686c86ba3aa7cf8680cf68 Mon Sep 17 00:00:00 2001 From: Doc Date: Mon, 18 Aug 2025 12:29:17 +0200 Subject: [PATCH] implemented wap parsing and info extraction --- internal/wad/wad.go | 195 +++++++++++++++++++++++++++++++++++++++++++- main.go | 90 +++++++++++++++++++- 2 files changed, 282 insertions(+), 3 deletions(-) diff --git a/internal/wad/wad.go b/internal/wad/wad.go index a0e40dd..eb1de7d 100644 --- a/internal/wad/wad.go +++ b/internal/wad/wad.go @@ -1,3 +1,196 @@ package wad -//init +import ( + "encoding/binary" + "fmt" + "io" + "os" + "strings" +) + +type header struct { + Ident [4]byte // "IWAD" or "PWAD" + NumLumps int32 + DirOffset int32 +} + +type DirEntry struct { + FilePos int32 + Size int32 + Name8 [8]byte +} + +func (d DirEntry) Name() string { return trimName(d.Name8) } + +type Wad struct { + f *os.File + dir []DirEntry + fsize int64 + isIWAD bool + isPWAD bool + path string +} + +func Open(path string) (*Wad, error) { + f, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("open wad: %w", err) + } + st, err := f.Stat() + if err != nil { + _ = f.Close() + return nil, fmt.Errorf("stat wad: %w", err) + } + var hdr header + if err := binary.Read(f, binary.LittleEndian, &hdr); err != nil { + _ = f.Close() + return nil, fmt.Errorf("read header: %w", err) + } + id := string(hdr.Ident[:]) + if id != "IWAD" && id != "PWAD" { + _ = f.Close() + return nil, fmt.Errorf("not a WAD file (ident=%q)", id) + } + if hdr.DirOffset < 0 || int64(hdr.DirOffset) > st.Size() { + _ = f.Close() + return nil, fmt.Errorf("bad directory offset: %d", hdr.DirOffset) + } + if _, err := f.Seek(int64(hdr.DirOffset), io.SeekStart); err != nil { + _ = f.Close() + return nil, fmt.Errorf("seek directory: %w", err) + } + if hdr.NumLumps < 0 || hdr.NumLumps > 1<<20 { + _ = f.Close() + return nil, fmt.Errorf("unreasonable lump count: %d", hdr.NumLumps) + } + dir := make([]DirEntry, hdr.NumLumps) + if err := binary.Read(f, binary.LittleEndian, &dir); err != nil { + _ = f.Close() + return nil, fmt.Errorf("read directory: %w", err) + } + for i, d := range dir { + if d.FilePos < 0 || d.Size < 0 { + _ = f.Close() + return nil, fmt.Errorf("dir[%d]: negative pos/size", i) + } + end := int64(d.FilePos) + int64(d.Size) + if end < 0 || end > st.Size() { + _ = f.Close() + return nil, fmt.Errorf("dir[%d:%s]: out of file bounds", i, trimName(d.Name8)) + } + } + w := &Wad{ + f: f, + dir: dir, + fsize: st.Size(), + isIWAD: id == "IWAD", + isPWAD: id == "PWAD", + path: path, + } + return w, nil +} + +func (w *Wad) Close() error { + if w == nil || w.f == nil { + return nil + } + err := w.f.Close() + w.f = nil + return err +} + +func (w *Wad) Dir() []DirEntry { return w.dir } + +func (w *Wad) ReadLump(i int) (name string, data []byte, err error) { + if i < 0 || i >= len(w.dir) { + return "", nil, fmt.Errorf("lump index out of range: %d", i) + } + de := w.dir[i] + name = trimName(de.Name8) + if _, err := w.f.Seek(int64(de.FilePos), io.SeekStart); err != nil { + return name, nil, fmt.Errorf("seek %s: %w", name, err) + } + buf := make([]byte, de.Size) + _, err = io.ReadFull(w.f, buf) + if err != nil { + return name, nil, fmt.Errorf("read %s: %w", name, err) + } + return name, buf, nil +} + +func (w *Wad) ReadLumpByName(name string) ([]byte, int, error) { + want := strings.ToUpper(name) + for i, d := range w.dir { + if trimName(d.Name8) == want { + _, b, err := w.ReadLump(i) + return b, i, err + } + } + return nil, -1, fmt.Errorf("lump %q not found", want) +} + +func (w *Wad) FindMap(marker string) (start, end int, err error) { + m := strings.ToUpper(marker) + start = -1 + for i, d := range w.dir { + if d.Size == 0 && trimName(d.Name8) == m { + start = i + break + } + } + if start < 0 { + return -1, -1, fmt.Errorf("map marker %q not found", m) + } + end = len(w.dir) + for i := start + 1; i < len(w.dir); i++ { + if w.dir[i].Size == 0 { + end = i + break + } + } + return start, end, nil +} + +func (w *Wad) LoadMapLumps(marker string, names ...string) (map[string][]byte, error) { + start, end, err := w.FindMap(marker) + if err != nil { + return nil, err + } + want := make(map[string]struct{}, len(names)) + for _, n := range names { + want[strings.ToUpper(n)] = struct{}{} + } + out := make(map[string][]byte, len(names)) + for i := start + 1; i < end; i++ { + n := trimName(w.dir[i].Name8) + if _, ok := want[n]; !ok { + continue + } + _, b, err := w.ReadLump(i) + if err != nil { + return nil, err + } + out[n] = b + if len(out) == len(want) { + break + } + } + missing := make([]string, 0) + for n := range want { + if _, ok := out[n]; !ok { + missing = append(missing, n) + } + } + if len(missing) > 0 { + return nil, fmt.Errorf("map %s: missing lumps: %v", strings.ToUpper(marker), missing) + } + return out, nil +} + +func trimName(n8 [8]byte) string { + n := string(n8[:]) + if i := strings.IndexByte(n, 0); i >= 0 { + n = n[:i] + } + return strings.ToUpper(strings.TrimSpace(n)) +} diff --git a/main.go b/main.go index 5405651..5e496d2 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,92 @@ package main -// init -func main() { +import ( + "bspviz/internal/wad" + "flag" + "fmt" + "log" + "os" + "path/filepath" + "strings" +) +func main() { + // Flags + wadPath := flag.String("wad", "", "Pfad zur WAD (required)") + mapMarker := flag.String("map", "MAP01", "Map-Marker (z.B. MAP01, E1M1, MYMAP)") + listOnly := flag.Bool("list", false, "Nur Directory auflisten und beenden") + info := flag.Bool("info", false, "Roh-Infos zur Map (Counts von VERTEXES/LINEDEFS)") + extract := flag.String("extract", "", "Kommagetrennte Lump-Namen aus der Map extrahieren (z.B. VERTEXES,LINEDEFS)") + outdir := flag.String("out", ".", "Zielordner für -extract") + flag.Parse() + + if *wadPath == "" { + fmt.Fprintf(os.Stderr, "Usage:\n") + fmt.Fprintf(os.Stderr, " go run ./cmd/bspviz -wad MYMAP.wad -list\n") + fmt.Fprintf(os.Stderr, " go run ./cmd/bspviz -wad MYMAP.wad -map MYMAP -info\n") + fmt.Fprintf(os.Stderr, " go run ./cmd/bspviz -wad MYMAP.wad -map MYMAP -extract VERTEXES,LINEDEFS -out dumps/\n") + os.Exit(2) + } + + w, err := wad.Open(*wadPath) + if err != nil { + log.Fatalf("open wad: %v", err) + } + defer w.Close() + + if *listOnly { + fmt.Printf("WAD: %s\n", *wadPath) + for i, d := range w.Dir() { + fmt.Printf("%3d: %-8s size=%-7d pos=%-8d\n", i, d.Name(), d.Size, d.FilePos) + } + return + } + + start, end, err := w.FindMap(*mapMarker) + if err != nil { + log.Fatalf("find map: %v", err) + } + fmt.Printf("Map %s: Directory [%d, %d)\n", strings.ToUpper(*mapMarker), start, end) + + if *info { + lumps, err := w.LoadMapLumps(*mapMarker, "VERTEXES", "LINEDEFS") + if err != nil { + log.Fatalf("load map lumps: %v", err) + } + vb := lumps["VERTEXES"] + lb := lumps["LINEDEFS"] + + verts := len(vb) / 4 + lines := len(lb) / 14 + fmt.Printf("VERTEXES: bytes=%d count=%d\n", len(vb), verts) + fmt.Printf("LINEDEFS: bytes=%d count=%d\n", len(lb), lines) + + if len(vb)%4 != 0 { + fmt.Println("WARN: VERTEXES size ist kein Vielfaches von 4 → Format prüfen") + } + if len(lb)%14 != 0 { + fmt.Println("WARN: LINEDEFS size ist kein Vielfaches von 14 → Format prüfen") + } + } + + if *extract != "" { + want := strings.Split(*extract, ",") + for i := range want { + want[i] = strings.ToUpper(strings.TrimSpace(want[i])) + } + lumps, err := w.LoadMapLumps(*mapMarker, want...) + if err != nil { + log.Fatalf("extract: %v", err) + } + if err := os.MkdirAll(*outdir, 0o755); err != nil { + log.Fatalf("mkdir %s: %v", *outdir, err) + } + for name, data := range lumps { + dst := filepath.Join(*outdir, fmt.Sprintf("%s.lmp", name)) + if err := os.WriteFile(dst, data, 0o644); err != nil { + log.Fatalf("write %s: %v", dst, err) + } + fmt.Printf("wrote %s (%d bytes)\n", dst, len(data)) + } + } }