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>
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
@@ -604,6 +604,8 @@ function unpackLocations(buffer, categories) {
604604 const view = new DataView(buffer);605605 const magic = String.fromCharCode(...new Uint8Array(buffer, 0, 8));606606 if (magic !== "ATLASLOC") throw new Error("location payload is not in the expected form");607+ const version = view.getUint16(8, true);608+ if (version !== 2) throw new Error(`location payload is version ${version}, and this reads 2`);607609 const count = view.getUint32(10, true);608610 609611 let at = 16;@@ -611,6 +613,7 @@ function unpackLocations(buffer, categories) {
611613 const latitudes = new Float32Array(buffer, (at += count * 4), count);612614 const longitudes = new Float32Array(buffer, (at += count * 4), count);613615 const regions = new Int32Array(buffer, (at += count * 4), count);616+ const shards = new Int32Array(buffer, (at += count * 4), count);614617 const offsets = new Uint32Array(buffer, (at += count * 4), count + 1);615618 const owners = new Uint16Array(buffer, (at += (count + 1) * 4), count);616619 const titles = new Uint8Array(buffer, at + count * 2);@@ -624,6 +627,7 @@ function unpackLocations(buffer, categories) {
624627 lat: latitudes[index],625628 lng: longitudes[index],626629 regionId: regions[index] || undefined,630+ shard: shards[index] || undefined,627631 });628632 }629633 }@@ -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 {@@ -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: ®ion, 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+}@@ -26,8 +26,10 @@ import (
2626 // numbers, and numbers written as text cost several times what they measure.2727 2828 const (29- locationMagic = "ATLASLOC"30- locationVersion = 129+ locationMagic = "ATLASLOC"30+ // 2 added the shard column. A map split into layers offers one at a time,31+ // and without it every layer's locations were drawn over every other.32+ locationVersion = 23133 )3234 3335 // mapIndex is a map as the index knows it: enough to list it and to open it,@@ -174,7 +176,7 @@ func packLocations(locations []catalogLocation, owners []uint16) []byte {
174176 }175177 offsets[count] = uint32(len(titles))176178 177- size := 16 + count*16 + (count+1)*4 + count*2 + len(titles)179+ size := 16 + count*20 + (count+1)*4 + count*2 + len(titles)178180 out := make([]byte, size)179181 copy(out, locationMagic)180182 binary.LittleEndian.PutUint16(out[8:], locationVersion)@@ -202,6 +204,9 @@ func packLocations(locations []catalogLocation, owners []uint16) []byte {
202204 }203205 put32(uint32(region))204206 }207+ for _, location := range locations {208+ put32(uint32(int32(location.Shard)))209+ }205210 for _, offset := range offsets {206211 put32(offset)207212 }