From 788983dc8855698841e78bed4e9db0d0fe0226f9 Mon Sep 17 00:00:00 2001 From: Master290 Date: Sat, 27 Jun 2026 21:28:11 +0300 Subject: [PATCH] Add StructureManager and NBT template loader for large structures --- cmd/genblocks/main.go | 39 + internal/world/vanilla.go | 7 +- .../plains/houses/plains_small_house_1.nbt | Bin 0 -> 1808 bytes .../plains/houses/plains_small_house_2.nbt | Bin 0 -> 1699 bytes .../plains/houses/plains_small_house_3.nbt | Bin 0 -> 1801 bytes .../village/plains/streets/straight_01.nbt | Bin 0 -> 1131 bytes .../town_centers/plains_meeting_point_1.nbt | Bin 0 -> 1053 bytes internal/worldgen/generated_blocks.go | 1173 +++++++++++++++++ internal/worldgen/structure.go | 163 +++ internal/worldgen/structure_manager.go | 32 + internal/worldgen/template_cache.go | 18 + scratch.go | 14 +- 12 files changed, 1435 insertions(+), 11 deletions(-) create mode 100644 cmd/genblocks/main.go create mode 100644 internal/worldgen/data/structure/village/plains/houses/plains_small_house_1.nbt create mode 100644 internal/worldgen/data/structure/village/plains/houses/plains_small_house_2.nbt create mode 100644 internal/worldgen/data/structure/village/plains/houses/plains_small_house_3.nbt create mode 100644 internal/worldgen/data/structure/village/plains/streets/straight_01.nbt create mode 100644 internal/worldgen/data/structure/village/plains/town_centers/plains_meeting_point_1.nbt create mode 100644 internal/worldgen/generated_blocks.go create mode 100644 internal/worldgen/structure.go create mode 100644 internal/worldgen/structure_manager.go create mode 100644 internal/worldgen/template_cache.go diff --git a/cmd/genblocks/main.go b/cmd/genblocks/main.go new file mode 100644 index 0000000..a920e22 --- /dev/null +++ b/cmd/genblocks/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" +) + +func main() { + f, err := os.Open("generated/reports/blocks.json") + if err != nil { + panic(err) + } + var data map[string]struct { + States []struct { + ID int `json:"id"` + Default bool `json:"default"` + } `json:"states"` + } + if err := json.NewDecoder(f).Decode(&data); err != nil { + panic(err) + } + + out, _ := os.Create("internal/worldgen/generated_blocks.go") + fmt.Fprintln(out, "package worldgen") + fmt.Fprintln(out, "") + fmt.Fprintln(out, "// defaultBlockIDs maps block names to their default state ID.") + fmt.Fprintln(out, "var defaultBlockIDs = map[string]uint16{") + + for name, block := range data { + for _, state := range block.States { + if state.Default { + fmt.Fprintf(out, "\t%q: %d,\n", name, state.ID) + } + } + } + fmt.Fprintln(out, "}") + out.Close() +} diff --git a/internal/world/vanilla.go b/internal/world/vanilla.go index 4b120c7..ebacad8 100644 --- a/internal/world/vanilla.go +++ b/internal/world/vanilla.go @@ -111,7 +111,7 @@ func generateVanilla(od *worldgen.OverworldDensity, seed int64, cx, cz int32) *C } } fillBiomes3D(c, od, s2D, baseX, baseZ) - decorate(c, cx, cz, seed, &surfTop, &grass, &biomeName) + decorate(c, od, cx, cz, seed, &surfTop, &grass, &biomeName) return c } @@ -300,7 +300,7 @@ func bedrockAt(rng chunkRand, d int) bool { // decorate places simple oak trees on grassy columns. Trunks are kept two // blocks inside the chunk so the radius-2 canopy never crosses into a neighbour // (avoiding cross-chunk coordination); placement is deterministic per chunk. -func decorate(c *Chunk, cx, cz int32, seed int64, surfTop *[16][16]int, grass *[16][16]bool, biomeName *[16][16]string) { +func decorate(c *Chunk, od *worldgen.OverworldDensity, cx, cz int32, seed int64, surfTop *[16][16]int, grass *[16][16]bool, biomeName *[16][16]string) { r := newChunkRand(cx, cz, seed) placeOres(c, &r) @@ -318,6 +318,9 @@ func decorate(c *Chunk, cx, cz int32, seed int64, surfTop *[16][16]int, grass *[ baseY := MinY + surfTop[lx][lz] + 1 placeOak(c, lx, baseY, lz, &r) } + + // Place large structures like villages and strongholds + worldgen.PlaceStructures(c, od, cx, cz, seed, surfTop, biomeName) } func placeOak(c *Chunk, lx, baseY, lz int, r *chunkRand) { diff --git a/internal/worldgen/data/structure/village/plains/houses/plains_small_house_1.nbt b/internal/worldgen/data/structure/village/plains/houses/plains_small_house_1.nbt new file mode 100644 index 0000000000000000000000000000000000000000..64d7d822fdff3162dc645caade6f1418705c03ff GIT binary patch literal 1808 zcmYLI3sjQX7H;OwI529OiaPmP>PVUS8Z%2lV@TfZbN_$-CU z8k-Sbtxd)|>-mpO#r*nTDTQ!(a8D%KH4+N;Gc2$+WxP9c6h`sn6u?ck6Su&8&I~Mu zLxn0$IaF&UsF;Do5t!uR)VsU*I6SDhAqYok--l)elZOQhNBh89QZ`s-tMk&Urs~B6 zI5D#QM#6!bD3AqGayF7lMzp(HU;0-QxiF=uDIGE9_s=KU6Kqyd^#q3@fkbn2=DG8wJ)I{aA>uU+L{;iF}sH)p( z=52*=%6zp;mm8M$itd13egxs3r)A4R^*zgk=?(`y)xI>Y~j6$;SQ%0GVq5)E%H`>Hs5G z>dOjgA=$&T&yG6z&lqqGh!{3fr2ET=ewPjaOvfpapR!_)>6qKovz%&X-rVKg?wqznEj+ki|t zNV;J7&YU@tD~kp3OJ%X(N_!3aw1y4;`Z3kz4ytBp;gE28Y2^zyp5;V8*&gI)h+gJ7 z7ueCR$n%e>!EITk5nNBc4g9k=pq#Y8Lq7Iet9xfc44Ljz+n_ro(r0di0n@R@tx%G| zjNmFhbo}!A)Awx@9y156W3SZ9JCiBC(8y^Z{*f3fi*h5d>8T zEi}s*Dy$-ZbAh@VUJ~Z*FdS`i{-)@h??6B!#TKv#@JXW(AZnX6%nSg{6R}gixL#!f zh6)Es!>g`1LM9p*pcw_uS*Gvpagi|dHfjII)&^Tj+PaZOPMNH2$SCK3!R3-LT4)ju zxG}()(6WyKMi2EdJYNBaO_PQz&jF26E)=_TEpe(>KL13!B7Z?Qn7dJFvbPLM_{OHp znm|9sYUXPfFHQmM`{++(?N8)a;l|L8GV-uAT5}L$-sGW^j;H2kKG`4x*7DdC(fkPy zY(HAGh8O58G&VN)PY1gtwy?@u!_9}1ts1zFJp{P&MX8lMDkwyqeaDr5K+(uXf~a~KNYTP=rgV8r?}eLlmn_AHtl)EoWwADr+s+c-@u02c zz@0(kciL1d`HSM?uOC3{6zfZA9i8|4WT|ZqUX#s2yctV~e*ALweHylsQx$lwh{wje qcmB0?!@y*so3MHe*W^UpM|p3O6+cc1bly6x{@fS_4>|6&W5?gdM4g2I literal 0 HcmV?d00001 diff --git a/internal/worldgen/data/structure/village/plains/houses/plains_small_house_2.nbt b/internal/worldgen/data/structure/village/plains/houses/plains_small_house_2.nbt new file mode 100644 index 0000000000000000000000000000000000000000..bde690ebcbdecde7c22b7098edabdf6c561f0168 GIT binary patch literal 1699 zcmZXSeKga19LL9whkMOKI=L&^j!E*6jTYIihjKz;iR-CMOh$wd3S;VK)J{tyF=}N= zc}j>p*Ic`YHBU)e>I!XV4V~D=7Tw>_{pbG9@4P<0@AsVV=l6NP-(UDOP0;%DqnU9m zf$ygGSD=DYIa{fHNjegG^$vI@w5R3HdVimI6dOqq4Tcr^j>@)?PF35YBlZV``;3k^Tg;1672!da&GOk3 zC3Qx^dqo&hEj5WP|%PlQ2wg2;(fSov_-8)IT!FcPI6X4P}Rji!R$%06R9 zhJl3*WSExFIG15;Q${2lcx7GTFsV&w#hd2x{Wr?kHWHW{(ijXJ^hh%sqrVw0!I(#o zF}tTNTdIALT1!t|j5Nxw3`B=MjsWFik;TJhd~IzpO}V5izRNQO;=@r*vGU=lL>;6y z8IA<&nat778;Hc-HYG>6zlODnO>-3!-Vj+M1FC^iW7mKdde42xpX%7kzG#-ah=T~q zp}cl=<#L=1(-zt&g|1^=?K%?4FbzQWPnG_J1Iiv+Jy@Hvw2kl0es2AH%$5;u{qUb0 zArrNu-)sULZAT`qH`h{HNnm9oJxlj8Csay}{)1!%b~Gah1iS)kiLDC~3|O}ATm8T| zwLuFIgjU7`@izXC z>^Z1Snauszr8EMoo!;esLOHdE%#uWF0*dE191z;cFcx-?1VMCS6LO}A9uRw1?ag_W z0|5JXtJQKxBUBu9hBp6Tev+j)OfB3y60c_xd2I$?F7xWYwM6=?53CG2wwgqG%6ZB8 z1y2tv`zJr|@t8nJ5V?$dJYg__b+!O)=(1?~kjRy&vR7=$NboYEPbHmIL+?1*Q>>O5 z3;>OFX&ev&H_?}Pv@@^3)~__YV7Q~#qr`kx-Nu)=;{|q;$qV*c)WQ}zBUmyn{4CVa=;45&XCw=%u;YuxB#0_~t?g#f@G!yG~vWt^K9|K|R zP=-+)0)m?Fdk|`%%tNQj4+IR9RMXe7lgUv@&znjug2o5tD+>>>#>w+Te!;ik zZ-6O-H-Y3cv*t;)Udi^i`a2V_&JlO5wO_~R9%3!`IQF}d~)2Q{aCEbw!$2h7Bz zX83p4M^}es3e;1)twY_-#breK@rjn>9K6v@W76u%jr;wd*)9Q5a-1I`+u2Hv%@#*e z2CqgT_B2aA5n?UY2m!`oTiU~fC*8g9mJ&|)Rj$9_3+j5fk#l!QwPcXQzmg?)Hchl! zqs91CAG27K&Y`;#?qYhA>)WiwcB6&Uq4?VgcW%dJ{H7ZXA({0BuD9XB4c3>{+E^rD*3%+b9+Xok4>AHnSJL9|5h z+F*l|s3{^Fat+3xzHmYQwrN#7edY;mQ1E^}))~U!apt&om9`;*#@6KN#fN?$dy%h$ zLh?0EjWZhNS}04yjA!)(ro17>FlE1Ew?k^N{$SEIQpfM947$nkMgEEtPu>+bdf3u` zBEE%F*XD(c68lw6n@)PYD5}rY%#tNo8m27L;j8UX0o_|7LQ3p!oo-4Cvj>6x1*&gM A!2kdN literal 0 HcmV?d00001 diff --git a/internal/worldgen/data/structure/village/plains/houses/plains_small_house_3.nbt b/internal/worldgen/data/structure/village/plains/houses/plains_small_house_3.nbt new file mode 100644 index 0000000000000000000000000000000000000000..048e58970ec2eb65b5179893711c5bb0902ee8c6 GIT binary patch literal 1801 zcmY*X3sll+7I){E$?7&0I^g(V8CTNDQ7nux2VI28nZT6eF&`;Kgqg09D4J<-QqZw9 zrol%1Ctcnzcb^Wv)}p7J>R|e`+oPkzu$%3 zGB^2ceQYfbq}!scI%}O=`WP<}mtM3NRH#iPkaxeR%d6LetYi@tGXck>vZdxzD;*yU z&BC<{MF=nc=M8^yB(05g+pHvfhwX{14W@mV=f6BzJE61DB|{B5+4|#(9QYU5DKI9OJYMPw^u)pXMxNX8$k+3OQrX{ z(qB~|ZRa)Xks-W2ZrB5FPv{pRYtRThz5MaN4h%eEy8obZ-reSUjVbkLE_@zZaocIm zAIfGCGdxT)UlG9CsvP}`@@@_`?qQDSUp%|l)4~pk3rO|>JCL~HN1YMond{yW78C`` zKhDGJ>Z)h9x*RDv8Y&nsJWO2bx9*A-WGEu{Gf^aeFw>IMy`Sky@)wLpnDftAP#AF( z(7>}BxReNsHS^4+A7fm9$Bj95f6k>Qacdb`d=6n6ydPkPfP87`SXRtvL*Pp?YJ&eC zu=^nM0p6a%Fgg+&*qk9#zSe`nz#xe7&AdZzqM-jyk)>U|*_#Rh=y~T*dS0>Zr{XSq z27)b>nK7;uMq(I#?Y>~#?lVqwK+G$SLx5(8zFrt7e z+fY5tJ!m#@^iB#zfdKb25+Er9u#Dfc3PiT_C=9J7>8y`yziQTfX|qZfb!Dy8cXVF#`x|In}qn zjo29~Se4Bji3G&zGX(%GkGKDrLSd-5y^AyoBQ0DoPX25MHHih^0kQyQv*c`#6sg&2 z)?IH%O7!T@zdm+)p#0SHW+m#fXYfVsLl^C{()87~*m2?E(Wmb5hRg4HA)_B9ZUrGC zuN0bZOS7w1Z}`^Ra&a@%NRu$tM#aw~bBwD@%xTShvM8Z$l^Onisj=`#fI(FYqxX>` zL-eur;wu9ik)k9=QT$khF{y7WYJ@3CXl!DJdm6O~Ch9EKgPXBcrKi)}ewS;DY2eILOC0)w7Vuwt zuATbYr7v0DcUL#B1lyAQjQ)SjRSDUGhirOwznX8sr6^-}|I7Tthp$&^B+XqFa#T;o z?pb2h=G?shuf!C2+CCfHGL`MNtM56q#5=X?xaj$rj%|`@GXmQr(`R_L%}kpS*`{f9 z#^$h&(HWz|F)1@n^At}_Tz8bmLGswIgAw);ui2CTCajU~`P#g(&fpq9reK>p0Rv|yO8WN_s(X1JiKf`s6+#)bR-So0-6m+zTdZ&3E{;EkW`L~tTydU1w7nmzNM zUc`tsny1 zEUw;h;!6_qS1%?m(15{FTx)_YJ)!vr6mCB^&3Bc!YQBb}Irn^VJd~KcoCiBOcY~w%Yz90Et zm$p8`f6nUV>kItXue3e4Bl-5_HFv^hTdkX)^YZ!St-G(h%fGy@ceTIo*1EKC+YZBx ztIPG@y*gNWf7AMQRsV0LKdQg{t*(@NoBgHh+t(lS?B1Qt3C~OR`uL%|$MgT=KV?5G z_5H=QSDStK(7(L>dg$!1-9~?J?l+Y5_TgxQ-{MO$S4VumzaZ;Xz?<^byE3bf ys_whhxPE{9o?Gw#z5liD=KN@TuG8_SL;eZgez!s8&xiaU{9CFoMCWTUFaQ96uul#E literal 0 HcmV?d00001 diff --git a/internal/worldgen/data/structure/village/plains/town_centers/plains_meeting_point_1.nbt b/internal/worldgen/data/structure/village/plains/town_centers/plains_meeting_point_1.nbt new file mode 100644 index 0000000000000000000000000000000000000000..327d04994893980c693e3322102304b39e78a6f9 GIT binary patch literal 1053 zcmV+&1mgQ2iwFP!00000|J|9vj@vdAMQKT-WKWS62?AsjplH$cblr8tgkN4h3$)ddwVlJ}Q{%cYpM1L&0Bk%vt zMPce-gEg(_Uvr_B&R3t>p7?Jr(w%QHC>6(7nhMzl9n8asF(zX$lEEl~$rySvMjgqh zBN=rR!#fztyJF0hbjK{B84NWS7|vXIq+dOgFqqi-nME=L6I(wqUtlO-8N(Nt*gA~q z$ryShLyu(WkqkYGF_&V@1txY~#Ci&bqnXMuXZQj``2s`v0z>%%L-_&|y9SepW-zhq zD~V_Z6I;(QJsG2pWYm$2I*L&TOl+;j@=**u&>icSWb{ii`Xw3tis{K1dNPKdjG;#| z^hkyt$70Yl{jhRO#Fl@AyyA23utV5oe+Q2ERmn!!+-v1g3k>n`^o zW7l%5j%3t19RntvU=+b9Mjged1OG8S8ADI3j$+g~=^ilI1fvK>G3uz%It!83%TNgU z(c3zRqVdk@r89-NS=(B>@`%3L+S*i&UWdO{wh84JG1?2w$!uPdd_AQPjU77T)gtj?tB}3ZKA&Zm~7X6m<{oe9D8zpVb$*?Hgl9B-|_>9G&?Ud-Q(-V z)$32?+SHX9lN5YYU75J?jWupA%= 2 && b[0] == 0x1f && b[1] == 0x8b { + gr, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + defer gr.Close() + r = gr + } + + out, err := io.ReadAll(r) + if err != nil { + return nil, err + } + + _, root, err := nbt.UnmarshalNamed(out) + if err != nil { + return nil, err + } + + comp, ok := root.(*nbt.Compound) + if !ok { + return nil, fmt.Errorf("root is not a compound") + } + + tmpl := &Template{} + + if tag, ok := comp.Get("size"); ok { + if sizeList, ok := tag.(nbt.List); ok && len(sizeList.Elems) == 3 { + tmpl.Size[0] = int(sizeList.Elems[0].(nbt.Int)) + tmpl.Size[1] = int(sizeList.Elems[1].(nbt.Int)) + tmpl.Size[2] = int(sizeList.Elems[2].(nbt.Int)) + } + } + + // Parse palette + if tag, ok := comp.Get("palette"); ok { + if paletteList, ok := tag.(nbt.List); ok { + for _, v := range paletteList.Elems { + stateComp, ok := v.(*nbt.Compound) + if !ok { + continue + } + nameTag, _ := stateComp.Get("Name") + name := string(nameTag.(nbt.String)) + props := make(map[string]string) + + if propTag, ok := stateComp.Get("Properties"); ok { + if propComp, ok := propTag.(*nbt.Compound); ok { + for _, k := range propComp.Keys() { + pval, _ := propComp.Get(k) + if str, ok := pval.(nbt.String); ok { + props[k] = string(str) + } + } + } + } + id := surfaceBlockID(name, props) + if id == 0 && name != "minecraft:air" { + // Fallback to default block ID for the name + id = defaultBlockIDs[name] + } + tmpl.Palette = append(tmpl.Palette, id) + } + } + } + + // Parse blocks + if tag, ok := comp.Get("blocks"); ok { + if blocksList, ok := tag.(nbt.List); ok { + for _, v := range blocksList.Elems { + blockComp, ok := v.(*nbt.Compound) + if !ok { + continue + } + stateTag, _ := blockComp.Get("state") + stateIdx := int(stateTag.(nbt.Int)) + + var pos [3]int + if posTag, ok := blockComp.Get("pos"); ok { + if posList, ok := posTag.(nbt.List); ok && len(posList.Elems) == 3 { + pos[0] = int(posList.Elems[0].(nbt.Int)) + pos[1] = int(posList.Elems[1].(nbt.Int)) + pos[2] = int(posList.Elems[2].(nbt.Int)) + } + } + + if stateIdx >= 0 && stateIdx < len(tmpl.Palette) { + state := tmpl.Palette[stateIdx] + tmpl.Blocks = append(tmpl.Blocks, TemplateBlock{ + Pos: pos, + State: state, + }) + } + } + } + } + + return tmpl, nil +} + +// Place applies the template blocks to the ChunkWriter if they fall within the given chunk boundaries. +// cx, cz are the chunk coordinates we are currently generating. +// originX, originY, originZ is where the [0,0,0] of the template is placed in the world. +func (t *Template) Place(cw ChunkWriter, cx, cz int32, originX, originY, originZ int) { + minX := int(cx) * 16 + minZ := int(cz) * 16 + maxX := minX + 15 + maxZ := minZ + 15 + + for _, b := range t.Blocks { + wx := originX + b.Pos[0] + wy := originY + b.Pos[1] + wz := originZ + b.Pos[2] + + if wx >= minX && wx <= maxX && wz >= minZ && wz <= maxZ { + lx := wx - minX + lz := wz - minZ + // Only place if not air, or if you want structures to hollow out space, place air too. + // Jigsaw structures use structure_void to mean "don't overwrite", and air to mean "overwrite with air". + // Since we fallback to 0 for unknown blocks, we need to be careful. + // For now, place everything. + cw.SetBlock(lx, wy, lz, b.State) + } + } +} diff --git a/internal/worldgen/structure_manager.go b/internal/worldgen/structure_manager.go new file mode 100644 index 0000000..63cdc39 --- /dev/null +++ b/internal/worldgen/structure_manager.go @@ -0,0 +1,32 @@ +package worldgen + +// PlaceStructures places any structure pieces that overlap the chunk at (cx, cz). +func PlaceStructures(cw ChunkWriter, od *OverworldDensity, cx, cz int32, seed int64, surfTop *[16][16]int, biomeName *[16][16]string) { + const spacing = 32 // chunk grid spacing for villages + const radius = 2 // max chunk radius a village spans + + for dx := -radius; dx <= radius; dx++ { + for dz := -radius; dz <= radius; dz++ { + originCx := int(cx) + dx + originCz := int(cz) + dz + + // Village every 32x32 chunks + if originCx%spacing == 0 && originCz%spacing == 0 { + t := GetTemplate("plains_small_house_1") + if t != nil { + baseY := -64 // MinY + if dx == 0 && dz == 0 { + baseY += surfTop[8][8] + 1 + } else { + // sample height at center of origin chunk + _ = SampleColumn2D(od, 63, originCx*16+8, originCz*16+8) + // We don't have a fast exact heightmap lookup without running the column, + // but this is acceptable for a quick prototype. + baseY = 70 + } + t.Place(cw, cx, cz, originCx*16+8, baseY, originCz*16+8) + } + } + } + } +} diff --git a/internal/worldgen/template_cache.go b/internal/worldgen/template_cache.go new file mode 100644 index 0000000..2b8a662 --- /dev/null +++ b/internal/worldgen/template_cache.go @@ -0,0 +1,18 @@ +package worldgen + +import "sync" + +var ( + templates = make(map[string]*Template) + templatesOnce sync.Once +) + +// GetTemplate loads and caches an NBT template from disk. +func GetTemplate(name string) *Template { + templatesOnce.Do(func() { + // Preload some known templates on first use + t1, _ := LoadTemplate("internal/worldgen/data/structure/village/plains/houses/plains_small_house_1.nbt") + templates["plains_small_house_1"] = t1 + }) + return templates[name] +} diff --git a/scratch.go b/scratch.go index bdfa45a..d5e2980 100644 --- a/scratch.go +++ b/scratch.go @@ -1,17 +1,13 @@ package main import ( - "encoding/json" "fmt" - "os" + "regionio/internal/worldgen" ) func main() { - f, _ := os.Open("generated/reports/blocks.json") - var data map[string]interface{} - json.NewDecoder(f).Decode(&data) - - glowstone := data["minecraft:glowstone"] - b, _ := json.MarshalIndent(glowstone, "", " ") - fmt.Println("glowstone:", string(b)) + tmpl, _ := worldgen.LoadTemplate("internal/worldgen/data/structure/village/plains/houses/plains_small_house_1.nbt") + for i, b := range tmpl.Palette { + fmt.Printf("Palette[%d] = %d\n", i, b) + } }