Skip to content

Commit f3d94ce

Browse files
committed
feat: add block placement and palette indexing APIs to support subchunk editing
1 parent bc0b64b commit f3d94ce

File tree

2 files changed

+131
-13
lines changed

2 files changed

+131
-13
lines changed

Sources/CoreBedrock/World/Block/MCSubChunkStorage.swift

Lines changed: 128 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,27 @@
22
// Created by yechentide on 2025/09/19
33
//
44

5-
struct MCSubChunkStorage {
6-
let version: Int
7-
let chunkY: Int8
5+
public struct MCSubChunkStorage {
6+
public let version: Int
7+
public let chunkY: Int8
88

9-
let blockLayer: BlockStorageLayer
10-
let liquidLayer: BlockStorageLayer
9+
public private(set) var blockLayer: BlockStorageLayer
10+
public private(set) var liquidLayer: BlockStorageLayer
1111

12-
struct BlockStorageLayer: PaletteReadable {
13-
let bitWidth: Int
14-
let palette: [CompoundTag]
15-
let indicesBytes: [UInt8]
12+
public struct BlockStorageLayer: PaletteReadable {
13+
public var bitWidth: Int
14+
public private(set) var palette: [CompoundTag]
15+
public private(set) var indicesBytes: [UInt8]
16+
private var paletteIndexCache: [Int: [Int]]
1617

17-
func paletteValue(at linearIndex: Int) -> CompoundTag? {
18+
public init(bitWidth: Int, palette: [CompoundTag], indicesBytes: [UInt8]) {
19+
self.bitWidth = bitWidth
20+
self.palette = palette
21+
self.indicesBytes = indicesBytes
22+
self.paletteIndexCache = [:]
23+
}
24+
25+
public func paletteValue(at linearIndex: Int) -> CompoundTag? {
1826
guard 0..<MCSubChunk.totalBlockCount ~= linearIndex else {
1927
return nil
2028
}
@@ -63,5 +71,115 @@ struct MCSubChunkStorage {
6371
}
6472
}
6573
}
74+
75+
// MARK: - Public API for Palette Management and Block Placement
76+
77+
public mutating func ensurePaletteIndex(for block: CompoundTag) -> Int {
78+
self.rebuildCacheIfNeeded()
79+
80+
let hash = self.blockHash(block)
81+
if let candidates = paletteIndexCache[hash] {
82+
for index in candidates where self.palette[index] == block {
83+
return index
84+
}
85+
}
86+
87+
// Not found, add to palette
88+
let newIndex = self.palette.count
89+
self.palette.append(block)
90+
self.paletteIndexCache[hash, default: []].append(newIndex)
91+
return newIndex
92+
}
93+
94+
public mutating func place(at localX: Int, localY: Int, localZ: Int, paletteIndex: Int) {
95+
guard let linear = MCSubChunk.linearIndex(localX, localY, localZ) else { return }
96+
guard paletteIndex < self.palette.count else { return }
97+
98+
self.ensureBitWidthCanHold(paletteIndex)
99+
self.writePaletteIndex(paletteIndex, atLinearIndex: linear)
100+
}
101+
102+
// MARK: - Private Helpers
103+
104+
private func blockHash(_ block: CompoundTag) -> Int {
105+
block.description.hashValue
106+
}
107+
108+
private mutating func rebuildCacheIfNeeded() {
109+
guard self.paletteIndexCache.isEmpty else { return }
110+
111+
var cache: [Int: [Int]] = [:]
112+
for (index, block) in self.palette.enumerated() {
113+
let hash = self.blockHash(block)
114+
cache[hash, default: []].append(index)
115+
}
116+
self.paletteIndexCache = cache
117+
}
118+
119+
private mutating func ensureBitWidthCanHold(_ index: Int) {
120+
guard index >= (1 << self.bitWidth) else { return }
121+
122+
var newBitWidth = self.bitWidth
123+
while index >= (1 << newBitWidth) {
124+
newBitWidth += 1
125+
}
126+
127+
self.repackIndices(to: newBitWidth)
128+
}
129+
130+
private mutating func repackIndices(to newBitWidth: Int) {
131+
guard newBitWidth != self.bitWidth else { return }
132+
133+
let newValuesPerWord = 32 / newBitWidth
134+
let newWordCount = (MCSubChunk.totalBlockCount + newValuesPerWord - 1) / newValuesPerWord
135+
var newIndicesBytes = [UInt8](repeating: 0, count: newWordCount * 4)
136+
137+
for linearIndex in 0..<MCSubChunk.totalBlockCount {
138+
let existingIndex = self.paletteIndex(at: linearIndex)
139+
self.writePaletteIndex(
140+
existingIndex, atLinearIndex: linearIndex, using: newBitWidth, into: &newIndicesBytes
141+
)
142+
}
143+
144+
self.bitWidth = newBitWidth
145+
self.indicesBytes = newIndicesBytes
146+
self.paletteIndexCache = [:] // Invalidate cache after repack
147+
}
148+
149+
private func paletteIndex(at linearIndex: Int) -> Int {
150+
let valuesPerWord = 32 / self.bitWidth
151+
let wordIndex = linearIndex / valuesPerWord
152+
let indexInWord = linearIndex % valuesPerWord
153+
let offset = indexInWord * self.bitWidth
154+
let mask = (1 << self.bitWidth) - 1
155+
156+
return self.indicesBytes.withUnsafeBytes { rawBuffer in
157+
let words = rawBuffer.baseAddress!.assumingMemoryBound(to: UInt32.self)
158+
let word = UInt32(littleEndian: words[wordIndex])
159+
return Int((word >> offset) & UInt32(mask))
160+
}
161+
}
162+
163+
private mutating func writePaletteIndex(_ index: Int, atLinearIndex linear: Int) {
164+
self.writePaletteIndex(index, atLinearIndex: linear, using: self.bitWidth, into: &self.indicesBytes)
165+
}
166+
167+
private func writePaletteIndex(
168+
_ index: Int, atLinearIndex linear: Int, using bw: Int, into bytes: inout [UInt8]
169+
) {
170+
let valuesPerWord = 32 / bw
171+
let wordIndex = linear / valuesPerWord
172+
let indexInWord = linear % valuesPerWord
173+
let offset = indexInWord * bw
174+
let mask = UInt32((1 << bw) - 1)
175+
176+
bytes.withUnsafeMutableBytes { rawBuffer in
177+
let words = rawBuffer.baseAddress!.assumingMemoryBound(to: UInt32.self)
178+
var word = UInt32(littleEndian: words[wordIndex])
179+
word &= ~(mask << offset) // Clear bits
180+
word |= UInt32(index) << offset // Set new bits
181+
words[wordIndex] = word.littleEndian
182+
}
183+
}
66184
}
67185
}

Sources/CoreBedrock/World/Block/SubChunkParser.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,16 @@ import Foundation
1919
*/
2020
// swiftlint:enable line_length
2121

22-
struct SubChunkParser {
22+
public struct SubChunkParser {
2323
private let binaryReader: CBBinaryReader
2424
private let chunkY: Int8
2525

26-
init(data: Data, chunkY: Int8) {
26+
public init(data: Data, chunkY: Int8) {
2727
self.binaryReader = CBBinaryReader(data: data)
2828
self.chunkY = chunkY
2929
}
3030

31-
func lightParse() throws -> MCSubChunkStorage? {
31+
public func lightParse() throws -> MCSubChunkStorage? {
3232
let storageVersion = try binaryReader.readUInt8()
3333
return switch storageVersion {
3434
case 9: try self.parseV9Light()

0 commit comments

Comments
 (0)