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)
|