minewebgen - internal/generator/generator.go
1 package main
2
3 import (
4 "image"
5 "image/color"
6 "image/draw"
7 "runtime"
8 "strconv"
9 "strings"
10
11 "vimagination.zapto.org/minecraft"
12 "vimagination.zapto.org/minecraft/nbt"
13 "vimagination.zapto.org/minewebgen/internal/data"
14 "vimagination.zapto.org/ora"
15 )
16
17 func toGray(o *ora.ORA, name string) (*image.Gray, error) {
18 var p *image.Gray
19 if l := o.Layer(name); l != nil {
20 p = image.NewGray(o.Bounds())
21 i, err := l.Image()
22 if err != nil {
23 return nil, err
24 }
25 draw.Draw(p, image.Rect(0, 0, p.Bounds().Max.X, p.Bounds().Max.Y), i, image.Point{}, draw.Src)
26 }
27 return p, nil
28 }
29
30 func toPaletted(o *ora.ORA, name string, palette color.Palette) (*image.Paletted, error) {
31 var p *image.Paletted
32 if l := o.Layer(name); l != nil {
33 p = image.NewPaletted(o.Bounds(), palette)
34 i, err := l.Image()
35 if err != nil {
36 return nil, err
37 }
38 draw.Draw(p, image.Rect(0, 0, p.Bounds().Max.X, p.Bounds().Max.Y), i, image.Point{}, draw.Src)
39 }
40 return p, nil
41 }
42
43 type level struct {
44 *minecraft.Level
45 MemoryLimit uint64
46 memStats runtime.MemStats
47 toCheck uint16
48 }
49
50 const CHECKLEVEL = 1024
51
52 func (l *level) checkMem() {
53 if l.MemoryLimit == 0 {
54 return
55 }
56 if l.toCheck == CHECKLEVEL {
57 runtime.ReadMemStats(&l.memStats)
58 if l.memStats.HeapAlloc > uint64(l.MemoryLimit) {
59 l.Save()
60 l.Close()
61 }
62 l.toCheck = 0
63 } else {
64 l.toCheck++
65 }
66 }
67
68 func (l *level) GetBiome(x, z int32) (minecraft.Biome, error) {
69 l.checkMem()
70 return l.Level.GetBiome(x, z)
71 }
72
73 func (l *level) GetBlock(x, y, z int32) (minecraft.Block, error) {
74 l.checkMem()
75 return l.Level.GetBlock(x, y, z)
76 }
77
78 func (l *level) GetHeight(x, z int32) (int32, error) {
79 l.checkMem()
80 return l.Level.GetHeight(x, z)
81 }
82
83 func (l *level) SetBiome(x, z int32, biome minecraft.Biome) error {
84 l.checkMem()
85 return l.Level.SetBiome(x, z, biome)
86 }
87
88 func (l *level) SetBlock(x, y, z int32, block minecraft.Block) error {
89 l.checkMem()
90 return l.Level.SetBlock(x, y, z, block)
91 }
92
93 type generator struct {
94 generator data.GeneratorData
95 Terrain struct {
96 Blocks []data.Blocks
97 Palette color.Palette
98 }
99 Biomes struct {
100 Values []minecraft.Biome
101 Palette color.Palette
102 }
103 Plants struct {
104 Blocks []data.Blocks
105 Palette color.Palette
106 }
107 }
108
109 func (g *generator) Generate(name, mapPath string, o *ora.ORA, c chan paint, m chan string, memoryLimit uint64) error {
110 sTerrain, err := toPaletted(o, "terrain", g.Terrain.Palette)
111 if err != nil {
112 return err
113 }
114 if sTerrain == nil {
115 return layerError{"terrain"}
116 }
117
118 sHeight, err := toGray(o, "height")
119 if err != nil {
120 return err
121 }
122 if sHeight == nil {
123 return layerError{"height"}
124 }
125
126 sBiomes, err := toPaletted(o, "biomes", g.Biomes.Palette)
127 if err != nil {
128 return err
129 }
130 sWater, err := toGray(o, "water")
131 if err != nil {
132 return err
133 }
134 sPlants, err := toPaletted(o, "plants", g.Plants.Palette)
135 if err != nil {
136 return err
137 }
138
139 p, err := minecraft.NewFilePath(mapPath)
140 if err != nil {
141 return err
142 }
143
144 l, err := minecraft.NewLevel(p)
145 if err != nil {
146 return err
147 }
148 level := &level{Level: l, MemoryLimit: memoryLimit}
149
150 level.LevelName(name)
151
152 m <- "Building Terrain"
153 if err = g.buildTerrain(p, level, sTerrain, sBiomes, sPlants, sHeight, sWater, c); err != nil {
154 return err
155 }
156
157 level.LevelName(name)
158 level.Generator(minecraft.FlatGenerator)
159 level.GeneratorOptions("0")
160 level.GameMode(minecraft.Creative)
161
162 for k, v := range g.generator.Options {
163 v = strings.ToLower(v)
164 switch strings.ToLower(k) {
165 case "generate-structures":
166 level.MapFeatures(v != "false")
167 case "hardcore":
168 level.Hardcore(v != "false")
169 case "gamemode":
170 gm, _ := strconv.Atoi(v)
171 if gm >= 0 && gm <= 3 {
172 level.GameMode(int32(gm))
173 }
174 case "difficulty":
175 d, _ := strconv.Atoi(v)
176 if d >= 0 && d <= 3 {
177 level.Difficulty(int8(d))
178 }
179 case "daylight-cycle":
180 level.DayLightCycle(v != "false")
181 case "fire-tick":
182 level.FireTick(v != "false")
183 case "keep-inventory":
184 level.KeepInventory(v != "false")
185 }
186 }
187
188 level.AllowCommands(true)
189 level.MobSpawning(false)
190 level.MobGriefing(false)
191 level.Spawn(10, 250, 10)
192
193 m <- "Exporting"
194 level.Save()
195 level.Close()
196 return nil
197 }
198
199 type layerError struct {
200 name string
201 }
202
203 func (l layerError) Error() string {
204 return "missing layer: " + l.name
205 }
206
207 type blocks struct {
208 Base, Top minecraft.Block
209 TopLevel uint8
210 }
211
212 func modeTerrain(p *image.Paletted, l int) uint8 {
213 b := p.Bounds()
214 modeMap := make([]uint8, l)
215 var most, mode uint8
216 for i := b.Min.X; i < b.Max.X; i++ {
217 for j := b.Min.Y; j < b.Max.Y; j++ {
218 pos := p.ColorIndexAt(i, j)
219 modeMap[pos]++
220 if m := modeMap[pos]; m > most {
221 most = m
222 mode = pos
223 }
224 }
225 }
226 return mode
227 }
228
229 func meanHeight(g *image.Gray) uint8 {
230 b := g.Bounds()
231 var total uint64
232 for i := b.Min.X; i < b.Max.X; i++ {
233 for j := b.Min.Y; j < b.Max.Y; j++ {
234 total += uint64(g.GrayAt(i, j).Y)
235 }
236 }
237 return uint8(total / uint64((b.Dx() * b.Dy())))
238 }
239
240 type chunkCache struct {
241 mem *minecraft.MemPath
242 level *minecraft.Level
243 clear nbt.Tag
244 cache map[uint16]nbt.Tag
245 blocks []data.Blocks
246 }
247
248 func newCache(blocks []data.Blocks) *chunkCache {
249 mem := minecraft.NewMemPath()
250 l, _ := minecraft.NewLevel(mem)
251
252 bedrock := minecraft.Block{ID: 7}
253
254 l.SetBlock(0, 0, 0, minecraft.Block{})
255 l.Save()
256 l.Close()
257 clearChunk, _ := mem.GetChunk(0, 0)
258
259 for j := int32(0); j < 255; j++ {
260 l.SetBlock(-1, j, -1, bedrock)
261 l.SetBlock(-1, j, 16, bedrock)
262 l.SetBlock(16, j, -1, bedrock)
263 l.SetBlock(16, j, 16, bedrock)
264 for i := int32(0); i < 16; i++ {
265 l.SetBlock(i, j, -1, bedrock)
266 l.SetBlock(i, j, 16, bedrock)
267 l.SetBlock(-1, j, i, bedrock)
268 l.SetBlock(16, j, i, bedrock)
269 }
270 }
271 l.Save()
272 l.Close()
273 mem.SetChunk(clearChunk)
274 return &chunkCache{
275 mem: mem,
276 level: l,
277 clear: clearChunk,
278 cache: make(map[uint16]nbt.Tag),
279 blocks: blocks,
280 }
281 }
282
283 func (c *chunkCache) getFromCache(x, z int32, terrain uint8, height int32) nbt.Tag {
284 cacheID := uint16(terrain)<<8 | uint16(height)
285 chunk, ok := c.cache[cacheID]
286 if !ok {
287 b := c.blocks[terrain].Base
288 closest := c.clear
289 var (
290 closestLevel int32
291 cl int32
292 h int32
293 )
294 for {
295 cl++
296 h = height - cl
297 if h == 0 {
298 break
299 }
300 if chunk, ok := c.cache[uint16(terrain)<<8|uint16(h)]; ok {
301 closestLevel = h
302 closest = chunk
303 break
304 }
305 h = height + cl
306 if h > 255 {
307 continue
308 }
309 if chunk, ok := c.cache[uint16(terrain)<<8|uint16(h)]; ok {
310 closestLevel = h
311 closest = chunk
312 break
313 }
314 }
315 ld := closest.Data().(nbt.Compound).Get("Level").Data().(nbt.Compound)
316 ld.Set(nbt.NewTag("xPos", nbt.Int(0)))
317 ld.Set(nbt.NewTag("zPos", nbt.Int(0)))
318 c.mem.SetChunk(closest)
319 if closestLevel < height {
320 for j := height - 1; j >= closestLevel; j-- {
321 for i := int32(0); i < 16; i++ {
322 for k := int32(0); k < 16; k++ {
323 c.level.SetBlock(i, j, k, b)
324 }
325 }
326 }
327 } else {
328 for j := closestLevel; j > height; j-- {
329 for i := int32(0); i < 16; i++ {
330 for k := int32(0); k < 16; k++ {
331 c.level.SetBlock(i, j, k, minecraft.Block{})
332 }
333 }
334 }
335 }
336 c.level.Save()
337 c.level.Close()
338 chunk, _ = c.mem.GetChunk(0, 0)
339 c.cache[cacheID] = chunk
340 }
341 ld := chunk.Data().(nbt.Compound).Get("Level").Data().(nbt.Compound)
342 ld.Set(nbt.NewTag("xPos", nbt.Int(x)))
343 ld.Set(nbt.NewTag("zPos", nbt.Int(z)))
344 return chunk
345 }
346
347 func (g *generator) buildTerrain(mpath minecraft.Path, level *level, terrain, biomes, plants *image.Paletted, height, water *image.Gray, c chan paint) error {
348 b := terrain.Bounds()
349 proceed := make(chan uint8, 10)
350 errChan := make(chan error, 1)
351 go func() {
352 defer close(proceed)
353 cc := newCache(g.Terrain.Blocks)
354 for j := 0; j < b.Max.Y; j += 16 {
355 chunkZ := int32(j >> 4)
356 for i := 0; i < b.Max.X; i += 16 {
357 chunkX := int32(i >> 4)
358 h := int32(meanHeight(height.SubImage(image.Rect(i, j, i+16, j+16)).(*image.Gray)))
359 wh := int32(meanHeight(water.SubImage(image.Rect(i, j, i+16, j+16)).(*image.Gray)))
360 var t uint8
361 if wh >= h<<1 { // more water than land...
362 c <- paint{
363 color.RGBA{0, 0, 255, 255},
364 chunkX, chunkZ,
365 }
366 t = uint8(len(g.Terrain.Blocks) - 1)
367 h = wh
368 } else {
369 t = modeTerrain(terrain.SubImage(image.Rect(i, j, i+16, j+16)).(*image.Paletted), len(g.Terrain.Palette))
370 c <- paint{
371 g.Terrain.Palette[t],
372 chunkX, chunkZ,
373 }
374 }
375 if err := mpath.SetChunk(cc.getFromCache(chunkX, chunkZ, t, h)); err != nil {
376 errChan <- err
377 return
378 }
379 proceed <- t
380 }
381 }
382 }()
383 ts := make([]uint8, 0, 1024)
384 for i := 0; i < (b.Max.X>>4)+2; i++ {
385 ts = append(ts, <-proceed) // get far enough ahead so all chunks are surrounded before shaping, to get correct lighting
386 }
387 select {
388 case err := <-errChan:
389 return err
390 default:
391 }
392 for j := int32(0); j < int32(b.Max.Y); j += 16 {
393 chunkZ := j >> 4
394 for i := int32(0); i < int32(b.Max.X); i += 16 {
395 chunkX := i >> 4
396 var totalHeight int32
397 ot := ts[0]
398 ts = ts[1:]
399 oy, _ := level.GetHeight(i, j)
400 for x := i; x < i+16; x++ {
401 for z := j; z < j+16; z++ {
402 if biomes != nil {
403 level.SetBiome(x, z, g.Biomes.Values[biomes.ColorIndexAt(int(x), int(z))])
404 }
405 h := int32(height.GrayAt(int(x), int(z)).Y)
406 totalHeight += h
407 wl := int32(water.GrayAt(int(x), int(z)).Y)
408 y := oy
409 if h > y {
410 y = h
411 }
412 if wl > y {
413 y = wl
414 }
415 for ; y > h && y > wl; y-- {
416 level.SetBlock(x, y, z, minecraft.Block{})
417 }
418 if plants != nil {
419 p := g.Plants.Blocks[plants.ColorIndexAt(int(x), int(z))]
420 py := int32(1)
421 for ; py <= int32(p.Level); py++ {
422 level.SetBlock(x, y+py, z, p.Base)
423 }
424 level.SetBlock(x, y+py, z, p.Top)
425 }
426 for ; y > h; y-- {
427 level.SetBlock(x, y, z, minecraft.Block{ID: 9})
428 }
429 t := terrain.ColorIndexAt(int(x), int(z))
430 tb := g.Terrain.Blocks[t]
431 for ; y > h-int32(tb.Level); y-- {
432 level.SetBlock(x, y, z, tb.Top)
433 }
434 if t != ot {
435 h = 0
436 } else {
437 h = oy
438 }
439 for ; y >= h; y-- {
440 level.SetBlock(x, y, z, tb.Base)
441 }
442 }
443 }
444 c <- paint{
445 color.Alpha{uint8(totalHeight >> 8)},
446 chunkX, chunkZ,
447 }
448 select {
449 case p, ok := <-proceed:
450 if ok {
451 ts = append(ts, p)
452 }
453 case err := <-errChan:
454 return err
455 }
456 }
457 }
458 return nil
459 }