package app import ( "fmt" "io" "os" "path/filepath" "strings" "bspviz/internal/bsp" "bspviz/internal/geom" "bspviz/internal/mapfmt" "bspviz/internal/viz" "bspviz/internal/wad" ) type Options struct { WadPath string MapMarker string ListOnly bool Info bool Extract []string OutDir string GeomTest bool BuildBSP bool Alpha float64 Beta float64 Eps float64 LeafMax int MaxDepth int Cands int Seed int64 DotOut string TreePNG string Overlay string Out io.Writer } func Run(opts Options) error { if opts.Out == nil { opts.Out = os.Stdout } if strings.TrimSpace(opts.WadPath) == "" { return fmt.Errorf("wad path required") } w, err := wad.Open(opts.WadPath) if err != nil { return fmt.Errorf("open wad: %w", err) } defer w.Close() if opts.ListOnly { fmt.Fprintf(opts.Out, "WAD: %s\n", opts.WadPath) for i, d := range w.Dir() { fmt.Fprintf(opts.Out, "%3d: %-8s size=%-7d pos=%-8d\n", i, d.Name(), d.Size, d.FilePos) } return nil } start, end, err := w.FindMap(opts.MapMarker) if err != nil { return fmt.Errorf("find map: %w", err) } fmt.Fprintf(opts.Out, "Map %s: Directory [%d, %d)\n", strings.ToUpper(opts.MapMarker), start, end) if opts.Info { lumps, err := w.LoadMapLumps(opts.MapMarker, "VERTEXES", "LINEDEFS") if err != nil { return fmt.Errorf("load map lumps: %w", err) } vb := lumps["VERTEXES"] lb := lumps["LINEDEFS"] m, err := mapfmt.LoadMap(lumps) if err != nil { return err } verts := len(vb) / 4 lines := len(lb) / 14 fmt.Fprintf(opts.Out, "VERTEXES: bytes=%d count=%d\n", len(vb), verts) fmt.Fprintf(opts.Out, "LINEDEFS: bytes=%d count=%d\n", len(lb), lines) fmt.Fprintf(opts.Out, "Map has %d vertices and %d linedefs\n", len(m.Vertices), len(m.Linedefs)) fmt.Fprintf(opts.Out, "First vertex: %+v\n", m.Vertices[0]) fmt.Fprintf(opts.Out, "First linedef: %+v\n", m.Linedefs[0]) if len(vb)%4 != 0 { fmt.Fprintln(opts.Out, "WARN: VERTEXES size ist kein Vielfaches von 4 → Format prüfen") } if len(lb)%14 != 0 { fmt.Fprintln(opts.Out, "WARN: LINEDEFS size ist kein Vielfaches von 14 → Format prüfen") } } if len(opts.Extract) > 0 { lumps, err := w.LoadMapLumps(opts.MapMarker, opts.Extract...) if err != nil { return fmt.Errorf("extract: %w", err) } if err := os.MkdirAll(opts.OutDir, 0o755); err != nil { return fmt.Errorf("mkdir %s: %w", opts.OutDir, err) } for name, data := range lumps { dst := filepath.Join(opts.OutDir, fmt.Sprintf("%s.lmp", name)) if err := os.WriteFile(dst, data, 0o644); err != nil { return fmt.Errorf("write %s: %w", dst, err) } fmt.Fprintf(opts.Out, "wrote %s (%d bytes)\n", dst, len(data)) } } if opts.GeomTest { raw, err := w.LoadMapLumps(opts.MapMarker, "VERTEXES", "LINEDEFS") if err != nil { return fmt.Errorf("load map lumps: %w", err) } m, err := mapfmt.LoadMap(raw) if err != nil { return fmt.Errorf("parse map: %w", err) } segs := mapfmt.LinedefsToSegs(m.Vertices, m.Linedefs) fmt.Fprintf(opts.Out, "GEOM: vertices=%d linedefs=%d segs=%d\n", len(m.Vertices), len(m.Linedefs), len(segs)) if len(segs) == 0 { fmt.Fprintln(opts.Out, "GEOM: keine Segmente gefunden – prüfe LINEDEFS/VERTEXES") return nil } pts := make([]geom.Vec, 0, len(m.Vertices)) for _, v := range m.Vertices { pts = append(pts, geom.V(float64(v.X), float64(v.Y))) } bb := geom.Bounds(pts) w := bb.Max.X - bb.Min.X h := bb.Max.Y - bb.Min.Y fmt.Fprintf(opts.Out, "AABB: min=(%.1f,%.1f) max=(%.1f,%.1f) size=(%.1f×%.1f)\n", bb.Min.X, bb.Min.Y, bb.Max.X, bb.Max.Y, w, h) O := segs[0].A D := geom.Sub(segs[0].B, segs[0].A) if geom.Len(D) < 1e-9 && len(segs) > 1 { O = segs[1].A D = geom.Sub(segs[1].B, segs[1].A) } var left, right, splits, degens int for _, s := range segs { f, b := geom.SplitSeg(s, O, D) if len(f) == 0 && len(b) == 0 { degens++ continue } left += len(f) right += len(b) if len(f) > 0 && len(b) > 0 { splits++ } } fmt.Fprintf(opts.Out, "PROBE-SPLIT: O=(%.1f,%.1f) D=(%.1f,%.1f)\n", O.X, O.Y, D.X, D.Y) fmt.Fprintf(opts.Out, "PROBE-SPLIT: left=%d right=%d splits=%d degens=%d (EPS=%.1e)\n", left, right, splits, degens, geom.EPS) return nil } if opts.BuildBSP { raw, err := w.LoadMapLumps(opts.MapMarker, "VERTEXES", "LINEDEFS") if err != nil { return fmt.Errorf("load map lumps: %w", err) } m, err := mapfmt.LoadMap(raw) if err != nil { return fmt.Errorf("parse map: %w", err) } segs := mapfmt.LinedefsToSegs(m.Vertices, m.Linedefs) p := bsp.Params{ Alpha: opts.Alpha, Beta: opts.Beta, Eps: opts.Eps, MaxDepth: opts.MaxDepth, LeafMax: opts.LeafMax, Cands: opts.Cands, Seed: opts.Seed, } root := bsp.Build(segs, p) st := bsp.Measure(root) fmt.Fprintln(opts.Out, "BSP built.") fmt.Fprintf(opts.Out, " nodes=%d leaves=%d maxDepth=%d totalLeafSegs=%d\n", st.Nodes, st.Leaves, st.MaxDepth, st.TotalSegs) fmt.Fprintf(opts.Out, " params: alpha=%.2f beta=%.2f eps=%.1e leafMax=%d maxDepth=%d cands=%d seed=%d\n", p.Alpha, p.Beta, p.Eps, p.LeafMax, p.MaxDepth, p.Cands, p.Seed) if opts.DotOut != "" { if err := viz.EmitDOT(root, opts.DotOut); err != nil { return fmt.Errorf("write DOT: %w", err) } fmt.Fprintf(opts.Out, "DOT export geschrieben: %s\n", opts.DotOut) if opts.TreePNG != "" { if err := viz.RunGraphviz(opts.DotOut, opts.TreePNG); err != nil { fmt.Fprintf(opts.Out, "Graphviz fehlgeschlagen: %v\n", err) } else { fmt.Fprintf(opts.Out, "Graphviz PNG gebaut: %s\n", opts.TreePNG) } } } if opts.Overlay != "" { if err := viz.RenderPNG(m, root, opts.Overlay); err != nil { return fmt.Errorf("write overlay PNG: %w", err) } fmt.Fprintf(opts.Out, "Overlay PNG geschrieben: %s\n", opts.Overlay) } return nil } return nil }