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 0000000..d7ac9c4 Binary files /dev/null and b/level/litematic/testdata/place.litematic differ 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