clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

feat: capture Piggyback's marker icons as the archive's PNGs

Dami committed 05f3260
The bundle shipped its categories keyed for icons that were never
fetched, so every pin wore the viewer's fallback glyph. Piggyback draws
its icons only as WebP under an asset prefix of the app's own -- for
cyberpunk-2077 the assets answer under "cyberpunk" -- so the crawler
finds the prefix by asking, decodes each icon, and archives it as the
PNG the pipeline reads. The district name labels draw no icon and want
none; they are text.
commit
05f326058ec8ecd423980fd97390f809b909966e
parent
a2ee4b2179af3da2b6c40953f1bcdc8fd2f3c2a3
author
Dami <dami.etoile@gmail.com>
1 file changed, +113 -0
Mtools/crawl/pb.go113++++++++++++++++++++++++++++++
M tools/crawl/pb.go +113 -0
@@ -6,15 +6,24 @@
66 package main77 88 import (9+ "bytes"910 "context"1011 "encoding/json"1112 "errors"1213 "fmt"1314 "hash/fnv"15+ "image"16+ "image/png"17+ "os"1418 "path/filepath"19+ "sort"1520 "strconv"1621 "strings"1722 23+ // Piggyback draws its icons only as WebP; they decode here and land in24+ // the archive as the PNG the pipeline reads.25+ _ "golang.org/x/image/webp"26+1827 "github.com/FelineStateMachine/atlas/internal/mgdoc"1928 "github.com/FelineStateMachine/atlas/internal/pbmap"2029 )
@@ -86,6 +95,11 @@ func runPB(ctx context.Context, fetcher *fetcher, o options) error {
8695 return nil8796 }8897 98+ if err := fetchPBIcons(ctx, fetcher, gameSlug, pageURL,99+ filepath.Join(o.archive, gameDirectory), capture); err != nil {100+ return err101+ }102+89103 index, err := readTileIndex(mapDir)90104 if err != nil {91105 return err
@@ -353,6 +367,105 @@ func composePBCapture(
353367 return capture354368 }355369 370+// fetchPBIcons stores each type's marker icon under the key the translator371+// gives its category, so the icons attach at the next generation. Piggyback372+// serves them only as WebP under an asset prefix of the app's own -- for373+// cyberpunk-2077 the assets live under "cyberpunk" -- so the prefix is found374+// by asking, and each icon is decoded and archived as the PNG the pipeline375+// reads. A type without an icon, like the district name labels, is simply a376+// type the viewer keeps its fallback glyph for.377+func fetchPBIcons(378+ ctx context.Context,379+ f *fetcher,380+ gameSlug, pageURL, gameDir string,381+ capture *pbmap.Capture,382+) error {383+ keys := make(map[string]bool)384+ for _, category := range capture.Categories {385+ for _, t := range category.Types {386+ keys[t.Key] = true387+ }388+ }389+ for _, pin := range capture.Pins {390+ keys[pin.TypeKey] = true391+ }392+ ordered := make([]string, 0, len(keys))393+ for key := range keys {394+ ordered = append(ordered, key)395+ }396+ sort.Strings(ordered)397+398+ prefix, found := "", false399+ fetched, absent := 0, 0400+ for _, key := range ordered {401+ path := filepath.Join(gameDir, "icons", key+".png")402+ if _, err := os.Stat(path); err == nil {403+ continue404+ }405+ if !found {406+ prefix, found = discoverPBIconPrefix(ctx, f, gameSlug, pageURL, ordered)407+ if !found {408+ fmt.Println(" no icon prefix answers; the viewer will use its fallback glyphs")409+ return nil410+ }411+ }412+ body, _, err := f.get(ctx, pageURL+"/"+prefix+"/icons/"+key+"@2x.webp")413+ if errors.Is(err, errAbsent) {414+ absent++415+ continue416+ }417+ if err != nil {418+ return fmt.Errorf("icon %s: %w", key, err)419+ }420+ decoded, _, err := image.Decode(bytes.NewReader(body))421+ if err != nil {422+ return fmt.Errorf("decode icon %s: %w", key, err)423+ }424+ var out bytes.Buffer425+ if err := png.Encode(&out, decoded); err != nil {426+ return fmt.Errorf("encode icon %s: %w", key, err)427+ }428+ if err := writeFile(path, out.Bytes()); err != nil {429+ return err430+ }431+ fetched++432+ }433+ if fetched > 0 || absent > 0 {434+ fmt.Printf(" %d icons · %d types draw no icon\n", fetched, absent)435+ }436+ return nil437+}438+439+// discoverPBIconPrefix finds the asset prefix by asking: the game's slug, and440+// then the slug with its trailing words dropped, against the first few types.441+func discoverPBIconPrefix(442+ ctx context.Context,443+ f *fetcher,444+ gameSlug, pageURL string,445+ keys []string,446+) (string, bool) {447+ candidates := []string{gameSlug}448+ for at := strings.LastIndex(gameSlug, "-"); at > 0; at = strings.LastIndex(gameSlug[:at], "-") {449+ candidates = append(candidates, gameSlug[:at])450+ }451+ probes := keys452+ if len(probes) > 3 {453+ probes = probes[:3]454+ }455+ for _, candidate := range candidates {456+ for _, key := range probes {457+ _, _, err := f.get(ctx, pageURL+"/"+candidate+"/icons/"+key+"@2x.webp")458+ if err == nil {459+ return candidate, true460+ }461+ if !errors.Is(err, errAbsent) {462+ return "", false463+ }464+ }465+ }466+ return "", false467+}468+356469 func pbDeepest(page *pbPage, o options) int {357470 deepest := page.maxZoom358471 if o.maxZoom > 0 {