clone

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

feat: let a source contribute a whole map to a composed game

Dami committed a7b442d
Sources do not divide the world the same way: MapGenie draws Tsushima,
IGN's wiki draws Iki Island, and both belong to one game. Composition
now takes each contribution on its own terms -- pins of a shared map
resolve into it as before, and a map the game has never drawn joins
whole, layers and pins and icons as captured, its icon names kept where
they are free and source-prefixed where the composed game already
spells something else with them. One bundle is written per game slug:
the game as every source together knows it, while the captures stay
untouched in the archive for any better policy to recompose.
commit
a7b442d4a610d9883aa244a875a477c2689af715
parent
eb5deccda5ed3bfcbdf29e51bb7175ba19cd41e9
author
Dami <dami.etoile@gmail.com>
1 file changed, +69 -7
Mtools/generate/merge.go76+++++++++++++++++++++++++++---
M tools/generate/merge.go +69 -7
@@ -15,6 +15,7 @@ package main
1515 // captures, not an edit.1616 1717 import (18+ "bytes"1819 "fmt"1920 "math"2021 "sort"
@@ -90,10 +91,14 @@ type heldPin struct {
9091 Reason string `json:"why"`9192 }9293 93-// mergeAcrossSources folds every game slug's catalog entries into one served94-// account: the newest capture absorbs the others' pins, and every source95-// still emits its own bundle untouched, so nothing is lost however the merge96-// policy evolves.94+// mergeAcrossSources composes every game slug's catalog entries into one95+// bundle. Each source contributed what it had -- a map the game already96+// draws, a map the game has never seen, icons, pins -- and the composition97+// takes each contribution on its own terms: pins of a shared map resolve98+// into it, a map without a counterpart joins the game whole, and the one99+// bundle written per slug is the game as every source together knows it.100+// The captures stay untouched in the archive, so recomposing under a better101+// policy never needs anything recrawled.97102 func mergeAcrossSources(games []catalogGame, shared tileGrid) ([]catalogGame, error) {98103 bySlug := make(map[string][]int)99104 var order []string
@@ -105,6 +110,7 @@ func mergeAcrossSources(games []catalogGame, shared tileGrid) ([]catalogGame, er
105110 }106111 sort.Strings(order)107112 113+ absorbed := make(map[int]bool)108114 for _, slug := range order {109115 group := bySlug[slug]110116 if len(group) < 2 {
@@ -123,9 +129,18 @@ func mergeAcrossSources(games []catalogGame, shared tileGrid) ([]catalogGame, er
123129 if err := mergeGame(&games[winner], &games[donor], shared); err != nil {124130 return nil, fmt.Errorf("merge %s: %w", slug, err)125131 }132+ absorbed[donor] = true126133 }134+ sortGameMaps(games[winner].Slug, games[winner].Maps)127135 }128- return games, nil136+137+ composed := make([]catalogGame, 0, len(games))138+ for index, game := range games {139+ if !absorbed[index] {140+ composed = append(composed, game)141+ }142+ }143+ return composed, nil129144 }130145 131146 func newestCapture(game catalogGame) string {
@@ -147,8 +162,13 @@ func mergeGame(winner, donor *catalogGame, shared tileGrid) error {
147162 donorMap := &donor.Maps[index]148163 target := winnerMaps[donorMap.Slug]149164 if target == nil {150- fmt.Printf("merge %s: %s has no counterpart in the serving capture; its pins stay in their own bundle\n",151- winner.Slug, donorMap.Slug)165+ // A map the game has never drawn is a whole contribution: it166+ // joins the game as this source captured it, icons and all.167+ if err := contributeMap(winner, donor, donorMap); err != nil {168+ return err169+ }170+ fmt.Printf("merge %s: %s joins whole from %s\n",171+ winner.Slug, donorMap.Slug, sourceLabelOf(donorMap, donor))152172 continue153173 }154174 if target.Parent != "" || donorMap.Parent != "" {
@@ -163,6 +183,48 @@ func mergeGame(winner, donor *catalogGame, shared tileGrid) error {
163183 return nil164184 }165185 186+// contributeMap carries one source's map into the composed game untouched:187+// its layers, pins, and zones as captured, and its icons brought across --188+// under their own names where those are free or already hold the same bytes,189+// under source-prefixed names where the composed game spells something else190+// with them.191+func contributeMap(winner, donor *catalogGame, donorMap *catalogMap) error {192+ contributed := *donorMap193+ sourceTag := slugifyLabel(sourceLabelOf(donorMap, donor))194+ renamed := make(map[string]string)195+ for groupIndex := range contributed.Groups {196+ categories := contributed.Groups[groupIndex].Categories197+ for categoryIndex := range categories {198+ category := &categories[categoryIndex]199+ if category.IconAsset == "" {200+ continue201+ }202+ data, held := donor.Icons[category.IconAsset]203+ if !held {204+ category.IconAsset = ""205+ category.IconPicture = false206+ continue207+ }208+ name := category.IconAsset209+ if replacement, seen := renamed[name]; seen {210+ category.IconAsset = replacement211+ continue212+ }213+ if winner.Icons == nil {214+ winner.Icons = make(map[string][]byte)215+ }216+ if existing, taken := winner.Icons[name]; taken && !bytes.Equal(existing, data) {217+ name = sourceTag + "--" + name218+ }219+ winner.Icons[name] = data220+ renamed[category.IconAsset] = name221+ category.IconAsset = name222+ }223+ }224+ winner.Maps = append(winner.Maps, contributed)225+ return nil226+}227+166228 func mergeMap(167229 winnerGame *catalogGame,168230 winner *catalogMap,