clone

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

fix: carry the shard through the packed locations

Dami committed 474415c
A map split into layers offers one at a time, and the viewer has always known
to leave the others out of it. What it could not do was tell which layer a
location belonged to: when locations moved from the catalog JSON into the
packed payload, the shard column did not go with them. Every location arrived
naming no layer at all, so every layer's pins were drawn on every other, over
the empty space beside the ground they belong to. Tears of the Kingdom showed
its whole 6,848 on each of Sky, Surface and Depths.

The column is written now, and the payload version goes to 2 with it. The
reader checks that version rather than reading past it, so a payload it does
not understand fails where the mistake is instead of quietly misreading every
field after the one that moved.

Two tests hold the column in place, since a field left out of the payload is a
field the map does not have: one over the writer's layout, and one over the
shipped Hyrule, where every location must name a layer its variants know.
commit
474415c9c6081fe6171c91f213e7742585dfccf1
parent
382875166d51dd58049924d11cfbb8b7eac881d6
author
Dami <dami.etoile@gmail.com>
38 files changed, +107 -3
Massets/catalog/18.binbin
Massets/catalog/203.binbin
Massets/catalog/230.binbin
Massets/catalog/3.binbin
Massets/catalog/3758.binbin
Massets/catalog/3759.binbin
Massets/catalog/3760.binbin
Massets/catalog/3761.binbin
Massets/catalog/3762.binbin
Massets/catalog/3763.binbin
Massets/catalog/3764.binbin
Massets/catalog/3765.binbin
Massets/catalog/427.binbin
Massets/catalog/446.binbin
Massets/catalog/477.binbin
Massets/catalog/478.binbin
Massets/catalog/479.binbin
Massets/catalog/481.binbin
Massets/catalog/536.binbin
Massets/catalog/580.binbin
Massets/catalog/602.binbin
Massets/catalog/603.binbin
Massets/catalog/604.binbin
Massets/catalog/605.binbin
Massets/catalog/606.binbin
Massets/catalog/62.binbin
Massets/catalog/760.binbin
Massets/catalog/877.binbin
Massets/catalog/883.binbin
Massets/catalog/884.binbin
Massets/catalog/885.binbin
Massets/catalog/886.binbin
Massets/catalog/923.binbin
Massets/catalog/972.binbin
Mfrontend/app.js4++
Mroutes_test.go46++++++++++++++++++++++++++++
Mtools/generate/main_test.go49++++++++++++++++++++++++++++++
Mtools/generate/split.go11+++++--
M assets/catalog/18.bin +0 -0

Binary file.

M assets/catalog/203.bin +0 -0

Binary file.

M assets/catalog/230.bin +0 -0

Binary file.

M assets/catalog/3.bin +0 -0

Binary file.

M assets/catalog/3758.bin +0 -0

Binary file.

M assets/catalog/3759.bin +0 -0

Binary file.

M assets/catalog/3760.bin +0 -0

Binary file.

M assets/catalog/3761.bin +0 -0

Binary file.

M assets/catalog/3762.bin +0 -0

Binary file.

M assets/catalog/3763.bin +0 -0

Binary file.

M assets/catalog/3764.bin +0 -0

Binary file.

M assets/catalog/3765.bin +0 -0

Binary file.

M assets/catalog/427.bin +0 -0

Binary file.

M assets/catalog/446.bin +0 -0

Binary file.

M assets/catalog/477.bin +0 -0

Binary file.

M assets/catalog/478.bin +0 -0

Binary file.

M assets/catalog/479.bin +0 -0

Binary file.

M assets/catalog/481.bin +0 -0

Binary file.

M assets/catalog/536.bin +0 -0

Binary file.

M assets/catalog/580.bin +0 -0

Binary file.

M assets/catalog/602.bin +0 -0

Binary file.

M assets/catalog/603.bin +0 -0

Binary file.

M assets/catalog/604.bin +0 -0

Binary file.

M assets/catalog/605.bin +0 -0

Binary file.

M assets/catalog/606.bin +0 -0

Binary file.

M assets/catalog/62.bin +0 -0

Binary file.

M assets/catalog/760.bin +0 -0

Binary file.

M assets/catalog/877.bin +0 -0

Binary file.

M assets/catalog/883.bin +0 -0

Binary file.

M assets/catalog/884.bin +0 -0

Binary file.

M assets/catalog/885.bin +0 -0

Binary file.

M assets/catalog/886.bin +0 -0

Binary file.

M assets/catalog/923.bin +0 -0

Binary file.

M assets/catalog/972.bin +0 -0

Binary file.

M frontend/app.js +4 -0
M routes_test.go +46 -0
@@ -68,6 +68,9 @@ func readPackedLocations(t *testing.T, mapID int64) []packedLocation {
6868 if string(raw[:8]) != "ATLASLOC" {6969 t.Fatalf("map %d location payload is not in the expected form", mapID)7070 }71+ if version := binary.LittleEndian.Uint16(raw[8:]); version != 2 {72+ t.Fatalf("map %d location payload is version %d, and this reads 2", mapID, version)73+ }7174 count := int(binary.LittleEndian.Uint32(raw[10:]))7275 at := 167376 u32 := func(index int) uint32 { return binary.LittleEndian.Uint32(raw[at+index*4:]) }
@@ -87,6 +90,11 @@ func readPackedLocations(t *testing.T, mapID int64) []packedLocation {
8790 lng[index] = math.Float32frombits(u32(index))8891 }8992 at += count * 8 // longitudes, then region ids, which this reader ignores93+ shards := make([]int32, count)94+ for index := range shards {95+ shards[index] = int32(u32(index))96+ }97+ at += count * 49098 offsets := make([]uint32, count+1)9199 for index := range offsets {92100 offsets[index] = u32(index)
@@ -106,6 +114,7 @@ func readPackedLocations(t *testing.T, mapID int64) []packedLocation {
106114 Title: string(titles[offsets[index]:offsets[index+1]]),107115 Latitude: float64(lat[index]),108116 Category: int(owners[index]),117+ Shard: int64(shards[index]),109118 }110119 }111120 return out
@@ -116,6 +125,7 @@ type packedLocation struct {
116125 Title string117126 Latitude float64118127 Category int128+ Shard int64119129 }120130 121131 type mapPayload struct {
@@ -123,11 +133,15 @@ type mapPayload struct {
123133 Name string `json:"name"`124134 Tiles string `json:"tiles"`125135 Formats []string `json:"formats"`136+ Shard int64 `json:"shard"`126137 MinZoom int `json:"minZoom"`127138 MaxZoom int `json:"maxZoom"`128139 Bounds *struct {129140 X, Y, Width, Height int130141 } `json:"bounds"`142+ Surface *struct {143+ X, Y, Width, Height int144+ } `json:"surface"`131145 } `json:"variants"`132146 Groups []struct {133147 Categories []struct {
@@ -222,6 +236,38 @@ func TestEveryListedMapHasItsPayload(t *testing.T) {
222236 }223237 }224238 239+// A map split into layers offers one at a time, and a location that does not240+// name its layer is drawn on all of them -- over the empty space beside a241+// layer it does not belong to. The packed shard is what prevents that, so242+// every location of a layered map must carry one its variants know.243+func TestLayeredMapLocationsNameTheirLayer(t *testing.T) {244+ const hyrule = 536245+ payload := readMapPayload(t, hyrule)246+ layers := make(map[int64]string, len(payload.Variants))247+ for _, variant := range payload.Variants {248+ if variant.Shard == 0 {249+ t.Fatalf("Tears of the Kingdom layer %q names no shard", variant.Name)250+ }251+ layers[variant.Shard] = variant.Name252+ }253+ if len(layers) < 2 {254+ t.Fatalf("Tears of the Kingdom has %d layers, want the Sky, Surface and Depths", len(layers))255+ }256+257+ counts := make(map[int64]int, len(layers))258+ for _, location := range readPackedLocations(t, hyrule) {259+ if _, ok := layers[location.Shard]; !ok {260+ t.Fatalf("%q is on shard %d, which is no layer of this map", location.Title, location.Shard)261+ }262+ counts[location.Shard]++263+ }264+ for shard, name := range layers {265+ if counts[shard] == 0 {266+ t.Errorf("the %s layer holds no locations", name)267+ }268+ }269+}270+225271 func TestEmbeddedCatalogCarriesTextLabelsAndZones(t *testing.T) {226272 index := readIndex(t)227273 find := func(game, name string) int64 {
M tools/generate/main_test.go +49 -0
@@ -1,6 +1,7 @@
11 package main22 33 import (4+ "encoding/binary"45 "os"56 "path/filepath"67 "testing"
@@ -127,3 +128,51 @@ func TestSortGameMapsPrefersPrimaryMap(t *testing.T) {
127128 }128129 }129130 }131+132+// The packed payload is the only thing the frontend reads a location from, so133+// a field left out of it is a field the map does not have. Shard went missing134+// this way once, and every layer's pins drew over every other.135+func TestPackLocationsCarriesEveryColumn(t *testing.T) {136+ region := int64(77)137+ locations := []catalogLocation{138+ {ID: 11, Title: "Sky Mine", Latitude: 1.5, Longitude: -2.5, Shard: 1783},139+ {ID: 12, Title: "Deep Well", Latitude: -3, Longitude: 4, RegionID: &region, Shard: 1785},140+ }141+142+ packed := packLocations(locations, []uint16{0, 1})143+144+ if magic := string(packed[:8]); magic != locationMagic {145+ t.Fatalf("magic = %q, want %q", magic, locationMagic)146+ }147+ if version := binary.LittleEndian.Uint16(packed[8:]); version != locationVersion {148+ t.Fatalf("version = %d, want %d", version, locationVersion)149+ }150+ count := int(binary.LittleEndian.Uint32(packed[10:]))151+ if count != len(locations) {152+ t.Fatalf("count = %d, want %d", count, len(locations))153+ }154+155+ column := func(index int) []int32 {156+ at := 16 + index*count*4157+ values := make([]int32, count)158+ for i := range values {159+ values[i] = int32(binary.LittleEndian.Uint32(packed[at+i*4:]))160+ }161+ return values162+ }163+164+ if ids := column(0); ids[0] != 11 || ids[1] != 12 {165+ t.Fatalf("ids = %v, want [11 12]", ids)166+ }167+ if regions := column(3); regions[0] != 0 || regions[1] != 77 {168+ t.Fatalf("regions = %v, want [0 77]", regions)169+ }170+ if shards := column(4); shards[0] != 1783 || shards[1] != 1785 {171+ t.Fatalf("shards = %v, want [1783 1785]", shards)172+ }173+174+ titles := string(packed[len(packed)-len("Sky MineDeep Well"):])175+ if titles != "Sky MineDeep Well" {176+ t.Fatalf("titles = %q, want %q", titles, "Sky MineDeep Well")177+ }178+}
M tools/generate/split.go +8 -3