Skip to content

Commit 74e8e29

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

File tree

2 files changed

+130
-14
lines changed

2 files changed

+130
-14
lines changed

Sources/CoreBedrock/World/Block/MCSubChunkStorage.swift

Lines changed: 127 additions & 11 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 let blockLayer: BlockStorageLayer
10+
public let 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
}
@@ -26,7 +34,7 @@ struct MCSubChunkStorage {
2634
}
2735

2836
@inline(__always)
29-
func unsafeEnumerateColumnDescendingY(
37+
internal func unsafeEnumerateColumnDescendingY(
3038
atLocalX localX: Int, localZ: Int, _ perform: (Int, CompoundTag) -> Bool
3139
) {
3240
guard let baseIndex = MCSubChunk.linearIndex(localX, 0, localZ) else {
@@ -63,5 +71,113 @@ 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+
rebuildCacheIfNeeded()
79+
80+
let hash = blockHash(block)
81+
if let candidates = paletteIndexCache[hash] {
82+
for index in candidates {
83+
if palette[index] == block {
84+
return index
85+
}
86+
}
87+
}
88+
89+
// Not found, add to palette
90+
let newIndex = palette.count
91+
palette.append(block)
92+
paletteIndexCache[hash, default: []].append(newIndex)
93+
return newIndex
94+
}
95+
96+
public mutating func place(at localX: Int, localY: Int, localZ: Int, paletteIndex: Int) {
97+
guard let linear = MCSubChunk.linearIndex(localX, localY, localZ) else { return }
98+
guard paletteIndex < palette.count else { return }
99+
100+
ensureBitWidthCanHold(paletteIndex)
101+
writePaletteIndex(paletteIndex, atLinearIndex: linear)
102+
}
103+
104+
// MARK: - Private Helpers
105+
106+
private func blockHash(_ block: CompoundTag) -> Int {
107+
block.description.hashValue
108+
}
109+
110+
private mutating func rebuildCacheIfNeeded() {
111+
guard paletteIndexCache.isEmpty else { return }
112+
113+
var cache: [Int: [Int]] = [:]
114+
for (index, block) in palette.enumerated() {
115+
let hash = blockHash(block)
116+
cache[hash, default: []].append(index)
117+
}
118+
paletteIndexCache = cache
119+
}
120+
121+
private mutating func ensureBitWidthCanHold(_ index: Int) {
122+
guard index >= (1 << bitWidth) else { return }
123+
124+
var newBitWidth = bitWidth
125+
while index >= (1 << newBitWidth) {
126+
newBitWidth += 1
127+
}
128+
129+
repackIndices(to: newBitWidth)
130+
}
131+
132+
private mutating func repackIndices(to newBitWidth: Int) {
133+
guard newBitWidth != bitWidth else { return }
134+
135+
let newValuesPerWord = 32 / newBitWidth
136+
let newWordCount = (MCSubChunk.totalBlockCount + newValuesPerWord - 1) / newValuesPerWord
137+
var newIndicesBytes = [UInt8](repeating: 0, count: newWordCount * 4)
138+
139+
for linearIndex in 0..<MCSubChunk.totalBlockCount {
140+
let existingIndex = paletteIndex(at: linearIndex)
141+
writePaletteIndex(existingIndex, atLinearIndex: linearIndex, using: newBitWidth, into: &newIndicesBytes)
142+
}
143+
144+
bitWidth = newBitWidth
145+
indicesBytes = newIndicesBytes
146+
paletteIndexCache = [:] // Invalidate cache after repack
147+
}
148+
149+
private func paletteIndex(at linearIndex: Int) -> Int {
150+
let valuesPerWord = 32 / bitWidth
151+
let wordIndex = linearIndex / valuesPerWord
152+
let indexInWord = linearIndex % valuesPerWord
153+
let offset = indexInWord * bitWidth
154+
let mask = (1 << bitWidth) - 1
155+
156+
return 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+
writePaletteIndex(index, atLinearIndex: linear, using: bitWidth, into: &indicesBytes)
165+
}
166+
167+
private func writePaletteIndex(_ index: Int, atLinearIndex linear: Int, using bw: Int, into bytes: inout [UInt8]) {
168+
let valuesPerWord = 32 / bw
169+
let wordIndex = linear / valuesPerWord
170+
let indexInWord = linear % valuesPerWord
171+
let offset = indexInWord * bw
172+
let mask = UInt32((1 << bw) - 1)
173+
174+
bytes.withUnsafeMutableBytes { rawBuffer in
175+
let words = rawBuffer.baseAddress!.assumingMemoryBound(to: UInt32.self)
176+
var word = UInt32(littleEndian: words[wordIndex])
177+
word &= ~(mask << offset) // Clear bits
178+
word |= UInt32(index) << offset // Set new bits
179+
words[wordIndex] = word.littleEndian
180+
}
181+
}
66182
}
67183
}

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)