From 2802cc36ce8cbd2061b5988f434b226af2c704c1 Mon Sep 17 00:00:00 2001 From: adman Date: Sun, 22 Mar 2026 19:45:05 +0100 Subject: [PATCH] litematic format --- data/protocol_version.go | 1 + level/litematic/main.go | 103 ++++++++++++++++++++++- level/litematic/main_test.go | 42 +++++++++ level/litematic/testdata/place.litematic | Bin 0 -> 1764 bytes level/packet.go | 7 ++ 5 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 level/litematic/main_test.go create mode 100644 level/litematic/testdata/place.litematic diff --git a/data/protocol_version.go b/data/protocol_version.go index e3dceeb..732bbcc 100644 --- a/data/protocol_version.go +++ b/data/protocol_version.go @@ -46,6 +46,7 @@ func LookUpVersionByDataVersion(version int32) (ret VersionInfo, err error) { for _, v := range ProtocolVersions { if v.DataVersion == version { ret = v + return } } err = UnknownMinecraftVersionError diff --git a/level/litematic/main.go b/level/litematic/main.go index 350bff7..3862a91 100644 --- a/level/litematic/main.go +++ b/level/litematic/main.go @@ -1,7 +1,15 @@ -package main +package litematic import ( + "errors" + "io" + "math" + "os" + "strings" + "github.com/admin-else/strom/data" + "github.com/admin-else/strom/level" + "github.com/admin-else/strom/nbt" "github.com/go-viper/mapstructure/v2" ) @@ -33,6 +41,38 @@ type Region struct { BlockStates []int64 } +func (r *Region) GetBlockAt(x, y, z int32) (name string, properties map[string]string, err error) { + uintSlice := make([]uint64, len(r.BlockStates)) + for i, v := range r.BlockStates { + uintSlice[i] = uint64(v) + } + if r.Size.X < 0 { + r.Size.X *= -1 + } + if r.Size.Y < 0 { + r.Size.Y *= -1 + } + if r.Size.Z < 0 { + r.Size.Z *= -1 + } + + if x >= r.Size.X || y >= r.Size.Y || z >= r.Size.Z { + err = OutOfBoundsErr + return + } + i := x + z*r.Size.X + y*r.Size.X*r.Size.Z + bpe := int32(math.Ceil(math.Log2(float64(len(r.BlockStatePalette))))) + i = level.GetEntryFromLongs(uintSlice, i, bpe) + if i >= int32(len(r.BlockStatePalette)) { + err = BlockStateNotInPalletErr + return + } + block := r.BlockStatePalette[i] + name = block.Name + properties = block.Properties + return +} + type File struct { Version int32 SubVersion int32 @@ -46,6 +86,24 @@ type Structure struct { BlockStatePalletRealized map[string][]int32 } +func LoadFromPath(path string) (s *Structure, err error) { + f, err := os.Open(path) + if err != nil { + return + } + defer f.Close() + s, err = LoadFromFile(f) + return +} + +func LoadFromFile(r io.Reader) (s *Structure, err error) { + n, err := nbt.ReadFile(r) + if err != nil { + return + } + return Load(n.Value) +} + func Load(v any) (s *Structure, err error) { s = &Structure{} err = mapstructure.Decode(v, &s.File) @@ -56,11 +114,12 @@ func Load(v any) (s *Structure, err error) { if err != nil { return } + s.BlockStatePalletRealized = make(map[string][]int32) for k, r := range s.Regions { s.BlockStatePalletRealized[k] = make([]int32, len(r.BlockStatePalette)) for i, b := range r.BlockStatePalette { var stateId int32 - stateId, err = data.StateIdFromBlocKAndStateMap(ret.MinecraftVersion, b.Name, b.Properties) + stateId, err = data.StateIdFromBlocKAndStateMap(ret.MinecraftVersion, strings.TrimPrefix(b.Name, "minecraft:"), b.Properties) if err != nil { return } @@ -69,3 +128,43 @@ func Load(v any) (s *Structure, err error) { } return } + +var BlockStateNotInPalletErr = errors.New("block state not in pallet") +var OutOfBoundsErr = errors.New("out of bounds") +var RegionNotInFileErr = errors.New("region not in file") + +func (s *Structure) GetBlockAt(name string, x, y, z int32) (blockState int32, err error) { + r, ok := s.Regions[name] + if !ok { + err = RegionNotInFileErr + return + } + uintSlice := make([]uint64, len(r.BlockStates)) + for i, v := range r.BlockStates { + uintSlice[i] = uint64(v) + } + if r.Size.X < 0 { + r.Size.X *= -1 + } + if r.Size.Y < 0 { + r.Size.Y *= -1 + } + if r.Size.Z < 0 { + r.Size.Z *= -1 + } + + if x >= r.Size.X || y >= r.Size.Y || z >= r.Size.Z { + err = OutOfBoundsErr + return + } + i := x + z*r.Size.X + y*r.Size.X*r.Size.Z + bpe := int32(math.Ceil(math.Log2(float64(len(r.BlockStatePalette))))) + i = level.GetEntryFromLongs(uintSlice, i, bpe) + mapSlice := s.BlockStatePalletRealized[name] + if i >= int32(len(mapSlice)) { + err = BlockStateNotInPalletErr + return + } + blockState = mapSlice[i] + return +} diff --git a/level/litematic/main_test.go b/level/litematic/main_test.go new file mode 100644 index 0000000..601b251 --- /dev/null +++ b/level/litematic/main_test.go @@ -0,0 +1,42 @@ +package litematic_test + +import ( + "embed" + "testing" + + "github.com/admin-else/strom/data" + "github.com/admin-else/strom/level/litematic" +) + +//go:embed testdata +var testData embed.FS + +func TestLitematic(t *testing.T) { + f, err := testData.Open("testdata/place.litematic") + if err != nil { + t.Fatal(err) + return + } + defer f.Close() + s, err := litematic.LoadFromFile(f) + if err != nil { + t.Fatal(err) + return + } + stateId, err := s.GetBlockAt("Cactus", 4, 0, 4) + if err != nil { + t.Fatal(err) + return + } + version, err := data.LookUpVersionByDataVersion(s.MinecraftDataVersion) + if err != nil { + t.Fatal(err) + return + } + b, state, err := data.FromBlockState(version.MinecraftVersion, stateId) + if err != nil { + t.Fatal(err) + return + } + t.Logf("Block: %v, state: %v", b.Name, state) +} diff --git a/level/litematic/testdata/place.litematic b/level/litematic/testdata/place.litematic new file mode 100644 index 0000000000000000000000000000000000000000..d7ac9c4551ad52ff78f44b37941859df8b07733f GIT binary patch literal 1764 zcmV-qcRwC<#y%0@_m#eg&hKqCn9m=`DrP1VL{G zBT)`(iZnPSq|J_@kQ^<)nfbo` zvjgmeu?f5VlYbh-Q95C_9(eO?EpGOo1h4!(cH>o9mfwi$%j$Z;7`wslu!BcY7G!~+ zGO#;r`h6JB&z@vn<{x{ZpJo0;_&+^h`#*XspWR;tkv~toW%jifB#hi&D);jw&ZCpn zI(?DE@#^F`z)jfXIEl~w1OgqfPuAybKTFoWcs=*ijNM#%VG2*ar#;UUfZa0!rrq|BpwlqHcjwPRV#Ki}ahJDX4XAuS~zwX{A=W)L9ldfUV zXjeD*LMtcq=4YzKJ$6@3+hQiapGTuPr6o(gFWXLfTmsmwnePp!i32j6j zf&|GFpbMTP`#I`R0(zn8kn{J_^8Ee#ceSAh?A^N!d_g~<6HD8Sum~B^m2Wfu$~Z{E z>jyJFWlG3kfQu2^?Fk9`k+72P`|4<-E2d2C3t2#2RWJSs9VDjsw?suTn!JOtQ4KpC zACc^I;5Ad#@Ir4X`zVev933@2Wjag;7|Fbv4+I{U13(ULIqa$wR2@Tg;G9Mi424pH z7Z{7CkU}eCunq-R<3XR}aM%@62N-rbKJjFu12I*?Xs!EcS#Gu?e9Cm-co5W!0bS+E zjre4P#{=heC#rSr%)gtTzBn}J!~9fDQ3ia} zi7E8ASuw8zCVz9DMRetF^Q`!2=10-{)(1o<1D@JFvdMlskG8*EPZOO~*<3H24o(NB zgPy;Rxyi#=zrt5_bn!6Ozp3}tu;|QP6wxh(_vEwvgML0 zZ2nzpI6qAtn)tvQ&G6&+p**|r^*YA#)5KSClt=M|Zw=F1$9Fn79h?r^bs&TH;A)H4 zHWRMG$E&Tb-T3`+hU#j|kIs|L|37Tv+w$aea5^|04iEAAhp@xi;C@=0@=vGBCHidh zCTpT7WRu^v|EM~^;M$#c+mGm2hb=zVu|03{BRb*t*Fie;rE59r{*xBKgu&Qu)8@h_O5-iVp$jfw+4@-LTE@h}}K9%k@S$iGhE)8!wk1ENDc zI?8m4=puT{kLc73t?*~W6YAQXH~A5r@Q3SQ%A5R72d6_1PuHx&YnXPvf{#LeeMJ7D zI%w;Imgx@GhX(r2*N54z)`uzSy1RAS>Cl5;@#J*q;psYcxa$3qDNZMEGs@ro_fLh7 zSA*X5`k?q3rURKNZ}L+=)$15y9eOt+R{eC=#CJM49oqPK{r~82)UjVWI>Olq_e*1R zAfvaJ1{=$dj<4dFZ9KkSM<4#>>gdD2EKg1cyE>c>+wk=|{<}IjbM`UWgdH5mX^;hR z1cx9p#{Psu5UcA29eQ}|M~fgj{UKZji>JZ-j7~$m_cRFo_6dnqV|n71YRuRkn><-R zf95Bthz7?iir|m^%v*SwcfjsG^wW6~oM-aOeSlv1kK;wK4E%*?{QKYk6hH5?yWoD1 z_+F;Gd%_!MUid5y*DIf~-|FKSt79Gy*sbr@*^4+4QF1Ry!x)Y=qt|4jDZDme_a8;` zP!JNG(z%c#(YF;PZ|id*#jALfaRZgmi-HXPaZ{! GQUCzZ@|H#b literal 0 HcmV?d00001 diff --git a/level/packet.go b/level/packet.go index 8d7aa04..3870358 100644 --- a/level/packet.go +++ b/level/packet.go @@ -75,6 +75,13 @@ func LongsToData(data []uint64, n, bpe int32) (ret []int32) { return } +func GetEntryFromLongs(data []uint64, i, bpe int32) (ret int32) { + mask := (uint64(1) << uint32(bpe)) - 1 + elementsPerLong := 64 / bpe + ret = int32((data[i/elementsPerLong] >> ((i % elementsPerLong) * bpe)) & mask) + return +} + // UnpackBlockData minecraft does this at net.minecraft.world.chunk.PaletteProvider func UnpackBlockData(r io.Reader) (blocks []int32, err error) { var bitsPerEntry uint8