// Package fog 解析与生成世界迷雾(Fog of World)的快照数据。 // // 快照是一个 zip,其中 Model/*/ 下的每个文件是一块「瓦片」:全球 512x512 块瓦片, // 每块瓦片 128x128 个块,每块是 64x64 的位图,一个位对应全球 2^22 x 2^22 网格里 // 约 9.55m*cos(纬度) 宽的一格。网格走网页墨卡托,经纬度即 WGS-84(已实测确认)。 // 文件内容 zlib 压缩:头 32768 字节是 16384 个 uint16(LE),值 = 该块在块体里的 // 序号(1 基,0 表示无数据);块体每块 512 字节位图 + 3 字节附加(地区码与置位校验)。 package fog import ( "archive/zip" "bytes" "compress/zlib" "crypto/md5" "encoding/binary" "fmt" "io" "math" "path" "strconv" "strings" ) const ( // MapTiles 全球瓦片数(每边)。 MapTiles = 512 // TileBlocks 每块瓦片的块数(每边)。 TileBlocks = 128 // BlockCells 每块的格子数(每边)。 BlockCells = 64 // WorldCells 全球格子数(每边),2^22。 WorldCells = MapTiles * TileBlocks * BlockCells bitmapSize = BlockCells * BlockCells / 8 // 512 extraSize = 3 blockSize = bitmapSize + extraSize // 515 headerLen = TileBlocks * TileBlocks headerSize = headerLen * 2 // 32768 mask1 = "olhwjsktri" mask2 = "eizxdwknmo" ) // Block 是一片 64x64 的已探索位图。GX/GY 为全局块坐标(0..65535), // Bitmap 固定 512 字节,行优先、每行 8 字节、字节内高位在前。 type Block struct { GX, GY int Bitmap []byte } // CellCount 返回位图里的置位数(已探索格子数)。 func (b *Block) CellCount() int { n := 0 for _, v := range b.Bitmap { n += int(popcount[v]) } return n } var popcount = func() [256]byte { var t [256]byte for i := range t { t[i] = byte(strings.Count(strconv.FormatUint(uint64(i), 2), "1")) } return t }() // tileIDFromName 从文件名还原瓦片编号,并校验 md5 前缀与 M2 尾缀。 func tileIDFromName(name string) (int, bool) { if len(name) < 7 || len(name[4:]) < 3 { return 0, false } digits := make([]byte, 0, len(name)) for _, c := range name[4 : len(name)-2] { v := strings.IndexRune(mask1, c) if v < 0 { return 0, false } digits = append(digits, byte('0'+v)) } id, err := strconv.Atoi(string(digits)) if err != nil { return 0, false } s := strconv.Itoa(id) if fmt.Sprintf("%x", md5.Sum([]byte(s)))[:4] != name[:4] { return 0, false } tail := name[len(name)-2:] for i := 0; i < 2; i++ { if int(s[len(s)-2+i]-'0') != strings.IndexByte(mask2, tail[i]) { return 0, false } } return id, true } // tileName 把瓦片编号编码回文件名。 func tileName(id int) string { s := strconv.Itoa(id) sum := fmt.Sprintf("%x", md5.Sum([]byte(s))) var b strings.Builder b.WriteString(sum[:4]) for i := 0; i < len(s); i++ { b.WriteByte(mask1[s[i]-'0']) } for i := len(s) - 2; i < len(s); i++ { b.WriteByte(mask2[s[i]-'0']) } return b.String() } // Decode 读取 .fwss(或任何包含迷雾瓦片文件的 zip),返回去重后的块与格子总数。 // 兼容 Model/*/、Sync/、根目录等布局:凡文件名能通过瓦片校验的都算。 func Decode(data []byte) ([]Block, int, error) { zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) if err != nil { return nil, 0, fmt.Errorf("打开迷雾快照: %w", err) } merged := map[[2]int][]byte{} cells := 0 for _, f := range zr.File { if f.FileInfo().IsDir() { continue } id, ok := tileIDFromName(path.Base(f.Name)) if !ok { continue } rc, err := f.Open() if err != nil { return nil, 0, fmt.Errorf("读取 %s: %w", f.Name, err) } raw, err := io.ReadAll(rc) rc.Close() if err != nil { return nil, 0, fmt.Errorf("读取 %s: %w", f.Name, err) } zdata, err := zlibDecompress(raw) if err != nil { return nil, 0, fmt.Errorf("解压 %s: %w", f.Name, err) } if len(zdata) < headerSize { continue } tx, ty := id%MapTiles, id/MapTiles for i := 0; i < headerLen; i++ { idx := int(binary.LittleEndian.Uint16(zdata[2*i:])) if idx == 0 { continue } off := headerSize + (idx-1)*blockSize if off+blockSize > len(zdata) { return nil, 0, fmt.Errorf("%s: 块体越界", f.Name) } bm := make([]byte, bitmapSize) copy(bm, zdata[off:off+bitmapSize]) key := [2]int{tx*TileBlocks + i%TileBlocks, ty*TileBlocks + i/TileBlocks} if old, dup := merged[key]; dup { for j := range bm { old[j] |= bm[j] } continue } merged[key] = bm cells += popcountSum(bm) } } blocks := make([]Block, 0, len(merged)) for k, bm := range merged { blocks = append(blocks, Block{GX: k[0], GY: k[1], Bitmap: bm}) } sortBlocks(blocks) return blocks, cells, nil } func popcountSum(bm []byte) int { n := 0 for _, v := range bm { n += int(popcount[v]) } return n } func sortBlocks(bs []Block) { for i := 1; i < len(bs); i++ { for j := i; j > 0 && (bs[j-1].GY > bs[j].GY || (bs[j-1].GY == bs[j].GY && bs[j-1].GX > bs[j].GX)); j-- { bs[j-1], bs[j] = bs[j], bs[j-1] } } } func zlibDecompress(raw []byte) ([]byte, error) { zr, err := zlib.NewReader(bytes.NewReader(raw)) if err != nil { return nil, err } defer zr.Close() return io.ReadAll(zr) } // Encode 把块集还原成 .fwss 结构(Model/*/ 由块重建),可用于导出与再导入。 // layers 是留存的非已探索层原始文件(Model/~ 与 Model/#),原样写回—— // 它们不参与渲染,但少了它们 fog of world 那边看到的就是残缺快照。 func Encode(blocks []Block, layers map[string][]byte) ([]byte, error) { byTile := map[[2]int][]Block{} for _, b := range blocks { tx, ty := b.GX/TileBlocks, b.GY/TileBlocks byTile[[2]int{tx, ty}] = append(byTile[[2]int{tx, ty}], b) } var buf bytes.Buffer zw := zip.NewWriter(&buf) for t, list := range byTile { sortByLinear(list) header := make([]byte, headerSize) body := make([]byte, 0, len(list)*blockSize) for k, b := range list { i := (b.GY%TileBlocks)*TileBlocks + b.GX%TileBlocks binary.LittleEndian.PutUint16(header[2*i:], uint16(k+1)) body = append(body, b.Bitmap...) body = append(body, extraBytes(b)...) } var raw bytes.Buffer zc := zlib.NewWriter(&raw) if _, err := zc.Write(header); err != nil { return nil, err } if _, err := zc.Write(body); err != nil { return nil, err } if err := zc.Close(); err != nil { return nil, err } id := t[0] + t[1]*MapTiles name := "Model/*/" + tileName(id) w, err := zw.Create(name) if err != nil { return nil, err } if _, err := w.Write(raw.Bytes()); err != nil { return nil, err } } // 其它层直接写原始字节,不做二次压缩,免得多一层格式风险 for p, data := range layers { w, err := zw.Create(p) if err != nil { return nil, err } if _, err := w.Write(data); err != nil { return nil, err } } if err := zw.Close(); err != nil { return nil, err } return buf.Bytes(), nil } func sortByLinear(list []Block) { lin := func(b Block) int { return (b.GY%TileBlocks)*TileBlocks + b.GX%TileBlocks } for i := 1; i < len(list); i++ { for j := i; j > 0 && lin(list[j-1]) > lin(list[j]); j-- { list[j-1], list[j] = list[j], list[j-1] } } } // extraBytes 生成块尾那 3 字节。真机存的是一个**大端 24 位数**,值为 置位数*2+1: // 实测真机包 8701/8701 个块精确满足(如 118 格 -> 0x0000ED = 237 = 118*2+1)。 // 一个 64x64 块最多 4096 格,该值 <= 0x2001,所以最高字节在数学上恒为 0。 // (旧实现写的是 0x10 / 0x80|…,纯属臆造,会让导出包里每个块的元信息都与真机对不上。) func extraBytes(b Block) []byte { sum := b.CellCount()*2 + 1 return []byte{0x00, byte(sum >> 8), byte(sum & 0xFF)} } // CellSideM 是格子在赤道的边长(米)。宽随纬度 cos 收缩,高恒定(网页墨卡托)。 const CellSideM = 9.55462861 // CellOf 把 WGS-84 经纬度投到全球格坐标(网页墨卡托,2^22 网格)。 func CellOf(lat, lon float64) (cx, cy int) { cx = int((lon + 180.0) / 360.0 * WorldCells) rad := lat * math.Pi / 180 cy = int((math.Pi - math.Asinh(math.Tan(rad))) / (2 * math.Pi) * WorldCells) return } // AreaKm2 估算块集覆盖的实地面积:每格面积按块中心纬度做 cos 修正。 func AreaKm2(blocks []Block) float64 { var m2 float64 for _, b := range blocks { m2 += float64(b.CellCount()) * cellAreaM2(b.GX, b.GY) } return m2 / 1e6 } // cellAreaM2 是某块内单格的面积(m²):一格宽 CellSideM、随纬度乘 cos 收缩,高恒定。 func cellAreaM2(gx, gy int) float64 { _, lat := cellLonLat(gx*BlockCells+BlockCells/2, gy*BlockCells+BlockCells/2) return CellSideM * math.Cos(lat*math.Pi/180) * CellSideM }