summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/MapSnapshot.fs
blob: 498ef48f7081550c94bcedb570edb4a32bf7007b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
namespace LivingVillage.Desktop

open System
open System.IO
open System.IO.Compression
open System.Text

/// P68:把 `MapGen.Result` 离线渲染为整图 PNG(不依赖图形设备,纯 CPU)。
///
/// 用途:为「512×384 整图」方案验收补一张覆盖全图的真实像素证据。
/// 判据口径由 `scripts/analyze-p68.py` 复用(调色板常量与此处一一对应):
///   * 每瓦片 `scale`(默认 8)像素,输出尺寸 = 宽×scale × 高×scale;
///   * 河流(Water 瓦片)、主路(MainRoadCenter)、农田(PaddyField/VegetablePlot)、
///     民居(Buildings/Farmhouses 屋顶矩形)均有独立可辨色;
///   * 纯函数:同输入逐字节一致;无外部素材依赖。
module MapSnapshot =

    /// 默认每瓦片像素(>= 8,满足证据可辨要求)。
    let defaultScale = 8

    let private grass = (86uy, 140uy, 74uy)
    let private water = (52uy, 110uy, 180uy)
    let private stone = (150uy, 150uy, 150uy)
    let private peat = (110uy, 96uy, 72uy)
    let private paddy = (120uy, 168uy, 84uy)
    let private vegetable = (154uy, 186uy, 96uy)
    let private path = (196uy, 176uy, 120uy)
    let private road = (214uy, 190uy, 128uy)
    let private bridge = (140uy, 96uy, 64uy)
    let private roof = (58uy, 58uy, 70uy)
    let private door = (120uy, 72uy, 40uy)
    let private grove = (34uy, 96uy, 52uy)

    let private tileColor (code: int) : byte * byte * byte =
        match code with
        | 1 -> water
        | 2 -> stone
        | 3 -> peat
        | 4 -> paddy
        | 5 -> vegetable
        | _ -> grass

    /// 把 `MapGen.Result` 渲染为 RGB 像素缓冲(行优先,左上原点),每瓦片 `scale` 像素。
    let renderRgb (map: MapGen.Result) (scale: int) : byte array =
        let s = max 1 scale
        let width = map.Width * s
        let height = map.Height * s
        let pixels = Array.zeroCreate<byte> (width * height * 3)
        let fillTile (tx: int) (ty: int) (r: byte, g: byte, b: byte) =
            if tx >= 0 && tx < map.Width && ty >= 0 && ty < map.Height then
                for py in ty * s .. ty * s + s - 1 do
                    for px in tx * s .. tx * s + s - 1 do
                        let i = (py * width + px) * 3
                        pixels.[i] <- r
                        pixels.[i + 1] <- g
                        pixels.[i + 2] <- b
        let fillRect (left: int) (top: int) (rectWidth: int) (rectHeight: int) (r: byte, g: byte, b: byte) =
            for ty in top .. top + max 0 rectHeight - 1 do
                for tx in left .. left + max 0 rectWidth - 1 do
                    fillTile tx ty (r, g, b)
        for ty in 0 .. map.Height - 1 do
            for tx in 0 .. map.Width - 1 do
                fillTile tx ty (tileColor map.Tiles.[ty * map.Width + tx])
        for (x, y) in map.Paths do
            fillTile x y path
        map.MainRoadCenter
        |> Array.iteri (fun x y -> fillTile x y road)
        for (x, y) in map.Bridges do
            fillTile x y bridge
        let drawBuilding (building: MapGen.Building) =
            fillRect building.Left building.Top building.Width building.Height roof
            fillTile building.DoorX building.DoorY door
        for building in map.Buildings do
            drawBuilding building
        for farmhouse in map.Farmhouses do
            drawBuilding farmhouse
        for g in map.Groves do
            fillTile g.X g.Y grove
        pixels

    let private crcTable =
        [| for n in 0 .. 255 ->
               let mutable c = uint32 n
               for _ in 0 .. 7 do
                   c <- if (c &&& 1u) = 1u then 0xEDB88320u ^^^ (c >>> 1) else c >>> 1
               c |]

    let private crc32 (bytes: byte array) : uint32 =
        let mutable c = 0xFFFFFFFFu
        for b in bytes do
            c <- crcTable.[int ((c ^^^ uint32 b) &&& 0xFFu)] ^^^ (c >>> 8)
        c ^^^ 0xFFFFFFFFu

    let private be32 (value: uint32) : byte array =
        [| byte (value >>> 24); byte ((value >>> 16) &&& 0xFFu); byte ((value >>> 8) &&& 0xFFu); byte (value &&& 0xFFu) |]

    let private chunk (kind: string) (data: byte array) : byte array =
        let kindBytes = Encoding.ASCII.GetBytes kind
        let crc = crc32 (Array.append kindBytes data)
        Array.concat [ be32 (uint32 data.Length); kindBytes; data; be32 crc ]

    /// zlib 容器压缩(BCL `ZLibStream`,无外部依赖),PNG IDAT 直接用。
    let private zlibCompress (data: byte array) : byte array =
        use output = new MemoryStream()
        using (new ZLibStream(output, CompressionLevel.Optimal, true)) (fun zlib ->
            zlib.Write(data, 0, data.Length))
        output.ToArray()

    /// 渲染为 8-bit RGB PNG 字节流(无 alpha)。确定性:同输入逐字节一致。
    let renderPng (map: MapGen.Result) (scale: int) : byte array =
        let s = max 1 scale
        let width = map.Width * s
        let height = map.Height * s
        let rgb = renderRgb map s
        let stride = width * 3
        let raw = Array.zeroCreate<byte> (height * (1 + stride))
        for y in 0 .. height - 1 do
            raw.[y * (1 + stride)] <- 0uy
            Array.blit rgb (y * stride) raw (y * (1 + stride) + 1) stride
        let signature = [| 137uy; 80uy; 78uy; 71uy; 13uy; 10uy; 26uy; 10uy |]
        let ihdr = Array.concat [ be32 (uint32 width); be32 (uint32 height); [| 8uy; 2uy; 0uy; 0uy; 0uy |] ]
        Array.concat [ signature; chunk "IHDR" ihdr; chunk "IDAT" (zlibCompress raw); chunk "IEND" [||] ]

    let save (path: string) (map: MapGen.Result) (scale: int) : unit =
        File.WriteAllBytes(path, renderPng map scale)