summaryrefslogtreecommitdiff
path: root/proto.ha
diff options
context:
space:
mode:
authorLassi Pulkkinen <lassi@pulk.fi>2024-10-31 03:11:21 +0200
committerLassi Pulkkinen <lassi@pulk.fi>2024-10-31 03:51:35 +0200
commitae44478b30d890fe0fb04022f44d474dcdcc3f9d (patch)
tree5f462459ae4b47d22114eed717d1382d08cf4dfe /proto.ha
Initial commit (import old repo)HEADmain
Diffstat (limited to 'proto.ha')
-rw-r--r--proto.ha715
1 files changed, 715 insertions, 0 deletions
diff --git a/proto.ha b/proto.ha
new file mode 100644
index 0000000..01e0d33
--- /dev/null
+++ b/proto.ha
@@ -0,0 +1,715 @@
+use comparray;
+use endian;
+use mcproto;
+use nbt;
+use fmt;
+use strings;
+use trace;
+
+fn decode_position(ctx: *mcproto::Context) (BlockPos | trace::failed) = {
+ const v = mcproto::decode_long(ctx)?: i64;
+ return BlockPos {
+ x = (v >> 38): i32,
+ y = (v << 52 >> 52): i16,
+ z = (v << 26 >> 38): i32,
+ };
+};
+
+fn decode_nbt(ctx: *mcproto::Context) ((str, nbt::Object) | trace::failed) = {
+ let in = ctx.dec.input[ctx.dec.pos..];
+ match (nbt::load(&in)) {
+ case let res: (str, nbt::Object) =>
+ ctx.dec.pos += len(ctx.dec.input) - ctx.dec.pos - len(in);
+ return res;
+ case nbt::Invalid =>
+ return mcproto::error(ctx, "Invalid NBT");
+ };
+};
+
+type Handler = fn(ctx: *mcproto::Context)
+ (void | trace::failed);
+
+fn game_handle_packet(ctx: *mcproto::Context, packet_id: i32)
+ (void | trace::failed) = {
+ const handler = if (packet_id: size < len(PROTO_HANDLERS))
+ PROTO_HANDLERS[packet_id: size] else null;
+ match (handler) {
+ case let handler: *Handler =>
+ const ctx_ = mcproto::context(ctx, "0x{:02x}", packet_id);
+ return handler(&ctx_);
+ case null =>
+ // TODO: implement all the packets ._.
+ // erroring here would spam too much for now...
+ void;
+ };
+};
+
+const PROTO_HANDLERS: [_]nullable *Handler = [
+ null, // 0x00
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ &handle_block_update,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null, // 0x10
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ &handle_unload_chunk,
+ null,
+ null,
+ null,
+ null, // handle_keep_alive; handled in client_handle_packet.
+ &handle_chunk_data_and_update_light,
+ null,
+ null,
+ null,
+ null, // handle_login; handled in client_handle_packet.
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null, // 0x30
+ null,
+ null,
+ null,
+ &handle_combat_death,
+ null,
+ null,
+ null,
+ &handle_synchronize_player_position,
+ null,
+ null,
+ null,
+ null,
+ &handle_respawn,
+ null,
+ &handle_update_section_blocks,
+ null, // 0x40
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ &handle_set_center_chunk,
+ null,
+ &handle_set_default_spawn_position,
+ null,
+ null,
+ null,
+ null, // 0x50
+ null,
+ null,
+ &handle_set_health,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null, // 0x60
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+];
+
+fn handle_login(ctx: *mcproto::Context) (void | trace::failed) = {
+ const ctx_ = mcproto::context(ctx, "player id");
+ const player_id = mcproto::decode_int(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "is hardcore");
+ const is_hardcore = mcproto::decode_bool(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "gamemode");
+ const gamemode = mcproto::decode_byte(&ctx_)?: Gamemode;
+ if (gamemode >= Gamemode::COUNT) {
+ return mcproto::error(&ctx_, "Invalid gamemode 0x{:02x}",
+ gamemode: u8);
+ };
+ const ctx_ = mcproto::context(ctx, "previous gamemode");
+ const prev_gamemode = mcproto::decode_byte(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "dimension count");
+ const dim_count = mcproto::decode_varint(&ctx_)?;
+ for (let i = 0i32; i < dim_count; i += 1) {
+ const ctx_ = mcproto::context(ctx, "dimension name {}", i);
+ const dim_name = mcproto::decode_string(&ctx_, 0x7fff)?;
+ };
+ const ctx_ = mcproto::context(ctx, "registry");
+ const registry = decode_nbt(&ctx_)?;
+ free(registry.0);
+ const registry = registry.1;
+ defer nbt::finish(registry);
+ const ctx_ = mcproto::context(ctx, "dimension type");
+ const dim_type = mcproto::decode_string(&ctx_, 0x7fff)?;
+ const ctx_ = mcproto::context(ctx, "dimension name");
+ const dim_name = mcproto::decode_string(&ctx_, 0x7fff)?;
+ const ctx_ = mcproto::context(ctx, "hashed seed");
+ const hashed_seed = mcproto::decode_long(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "max players");
+ const max_players = mcproto::decode_varint(&ctx_)?;
+ if (max_players < 0) {
+ return mcproto::error(&ctx_, "Max players must not be negative");
+ };
+ const ctx_ = mcproto::context(ctx, "view distance");
+ const view_distance = mcproto::decode_varint(&ctx_)?;
+ if (view_distance <= 0) {
+ return mcproto::error(&ctx_, "View distance must be positive");
+ };
+ const ctx_ = mcproto::context(ctx, "simulation distance");
+ const sim_distance = mcproto::decode_varint(&ctx_)?;
+ if (sim_distance <= 0) {
+ return mcproto::error(&ctx_,
+ "Simulation distance must be positive");
+ };
+ const ctx_ = mcproto::context(ctx, "reduced debug info");
+ const reduced_debug_info = mcproto::decode_bool(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "enable respawn screen");
+ const enable_respawn_screen = mcproto::decode_bool(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "is debug");
+ const is_debug = mcproto::decode_bool(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "is flat");
+ const is_flat = mcproto::decode_bool(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "has death location");
+ if (mcproto::decode_bool(&ctx_)?) {
+ const ctx_ = mcproto::context(ctx, "death dimension");
+ const death_dim = mcproto::decode_string(&ctx_, 0x7fff)?;
+ const ctx_ = mcproto::context(ctx, "death position");
+ const death_pos = decode_position(&ctx_)?;
+ };
+ mcproto::expect_end(ctx)?;
+
+ GAMEMODE = gamemode;
+ trace::info(&trace::root, "gamemode {}", strgamemode(GAMEMODE));
+
+ assert(len(DIM_TYPES) == 0);
+ match (nbt_get(&registry, "minecraft:dimension_type")) {
+ case let reg: nbt::Object =>
+ match (nbt_get(&reg, "value")) {
+ case let reg: []nbt::Value =>
+ for (let i = 0z; i < len(reg); i += 1) {
+ const entry = match (reg[i]) {
+ case let entry: nbt::Object =>
+ yield entry;
+ case =>
+ return mcproto::error(ctx, "Invalid registry");
+ };
+ const name = match (nbt_get(&entry, "name")) {
+ case let name: str =>
+ yield strings::dup(name);
+ case =>
+ return mcproto::error(ctx, "Invalid registry");
+ };
+ const elem = match (nbt_get(&entry, "element")) {
+ case let elem: nbt::Object =>
+ yield elem;
+ case =>
+ return mcproto::error(ctx, "Invalid registry");
+ };
+ const min_y = match (nbt_get(&elem, "min_y")) {
+ case let min_y: i32 =>
+ if (min_y & 0xf != 0)
+ return mcproto::error(ctx, "Invalid registry");
+ min_y >>= 4;
+ if (min_y: i8 != min_y)
+ return mcproto::error(ctx, "Invalid registry");
+ yield min_y: i8;
+ case =>
+ return mcproto::error(ctx, "Invalid registry");
+ };
+ const height = match (nbt_get(&elem, "height")) {
+ case let height: i32 =>
+ if (height & 0xf != 0)
+ return mcproto::error(ctx, "Invalid registry");
+ height >>= 4;
+ if (height: u8 != height: u32)
+ return mcproto::error(ctx, "Invalid registry");
+ yield height: u8;
+ case =>
+ return mcproto::error(ctx, "Invalid registry");
+ };
+ if (min_y + height: i8 < min_y) {
+ return mcproto::error(ctx, "Invalid registry");
+ };
+ append(DIM_TYPES, DimType {
+ name = name,
+ min_y = min_y,
+ height = height,
+ });
+ };
+ case =>
+ return mcproto::error(ctx, "Invalid registry");
+ };
+ case =>
+ return mcproto::error(ctx, "Invalid registry");
+ };
+
+ let found = false;
+ for (let i = 0z; i < len(DIM_TYPES); i += 1) {
+ if (DIM_TYPES[i].name == dim_type) {
+ found = true;
+ DIM_TYPE = i;
+ };
+ };
+ if (!found) {
+ return mcproto::error(ctx, "Unknown dimension type {}", dim_type);
+ };
+ trace::info(&trace::root, "dimension type {}", DIM_TYPES[DIM_TYPE].name);
+
+ VIEW_DISTANCE = view_distance;
+ trace::info(&trace::root, "view distance {}", VIEW_DISTANCE);
+
+ game_init();
+};
+
+fn handle_respawn(ctx: *mcproto::Context) (void | trace::failed) = {
+ const ctx_ = mcproto::context(ctx, "dimension type");
+ const dim_type = mcproto::decode_string(&ctx_, 0x7fff)?;
+ const ctx_ = mcproto::context(ctx, "dimension name");
+ const dim_name = mcproto::decode_string(&ctx_, 0x7fff)?;
+ const ctx_ = mcproto::context(ctx, "hashed seed");
+ const hashed_seed = mcproto::decode_long(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "gamemode");
+ const gamemode = mcproto::decode_byte(&ctx_)?: Gamemode;
+ if (gamemode >= Gamemode::COUNT) {
+ return mcproto::error(&ctx_, "Invalid gamemode 0x{:02x}",
+ gamemode: u8);
+ };
+ const ctx_ = mcproto::context(ctx, "previous gamemode");
+ const prev_gamemode = mcproto::decode_byte(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "is debug");
+ const is_debug = mcproto::decode_bool(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "is flat");
+ const is_flat = mcproto::decode_bool(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "copy metadata");
+ const copy_metadata = mcproto::decode_bool(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "has death location");
+ if (mcproto::decode_bool(&ctx_)?) {
+ const ctx_ = mcproto::context(ctx, "death dimension");
+ const death_dim = mcproto::decode_string(&ctx_, 0x7fff)?;
+ const ctx_ = mcproto::context(ctx, "death position");
+ const death_pos = decode_position(&ctx_)?;
+ };
+ mcproto::expect_end(ctx)?;
+
+ game_despawn();
+
+ GAMEMODE = gamemode;
+ trace::info(&trace::root, "gamemode {}", strgamemode(GAMEMODE));
+
+ let found = false;
+ for (let i = 0z; i < len(DIM_TYPES); i += 1) {
+ if (DIM_TYPES[i].name == dim_type) {
+ found = true;
+ DIM_TYPE = i;
+ };
+ };
+ if (!found) {
+ return mcproto::error(ctx, "Unknown dimension type {}",
+ dim_type);
+ };
+ trace::info(&trace::root, "dimension type {}",
+ DIM_TYPES[DIM_TYPE].name);
+
+ game_respawn();
+};
+
+fn handle_keep_alive(ctx: *mcproto::Context) (void | trace::failed) = {
+ const ctx_ = mcproto::context(ctx, "id");
+ const id = mcproto::decode_long(&ctx_)?;
+ mcproto::expect_end(ctx)?;
+
+ let out: []u8 = [];
+ defer free(out);
+ mcproto::encode_long(&out, id);
+ network_send(0x11, out);
+};
+
+fn handle_set_center_chunk(ctx: *mcproto::Context)
+ (void | trace::failed) = {
+ const ctx_ = mcproto::context(ctx, "x");
+ const x = mcproto::decode_varint(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "z");
+ const z = mcproto::decode_varint(&ctx_)?;
+ mcproto::expect_end(ctx)?;
+
+ setview(ChunkPos { x = x, z = z });
+};
+
+type SyncPositionFlags = enum u8 {
+ X = 0x1,
+ Y = 0x2,
+ Z = 0x4,
+ // TODO: confirm that these are the correct way around;
+ // update wiki to clarify.
+ YAW = 0x8,
+ PITCH = 0xa,
+};
+
+fn handle_synchronize_player_position(ctx: *mcproto::Context)
+ (void | trace::failed) = {
+ const ctx_ = mcproto::context(ctx, "x");
+ let x = mcproto::decode_double(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "y");
+ let y = mcproto::decode_double(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "z");
+ let z = mcproto::decode_double(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "yaw");
+ let yaw = mcproto::decode_float(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "pitch");
+ let pitch = mcproto::decode_float(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "flags");
+ const flags = mcproto::decode_byte(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "teleport id");
+ const id = mcproto::decode_varint(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "dismount");
+ const dismount = mcproto::decode_bool(&ctx_)?;
+ mcproto::expect_end(ctx)?;
+
+ if (flags & SyncPositionFlags::X != 0) x += PLAYER_POS[0];
+ if (flags & SyncPositionFlags::Y != 0) y += PLAYER_POS[1];
+ if (flags & SyncPositionFlags::Z != 0) z += PLAYER_POS[2];
+ if (flags & SyncPositionFlags::YAW != 0) yaw += PLAYER_YAW;
+ if (flags & SyncPositionFlags::PITCH != 0) pitch += PLAYER_PITCH;
+
+ PLAYER_POS = [x: f32, y: f32, z: f32];
+ PLAYER_YAW = yaw;
+ PLAYER_PITCH = pitch;
+
+ let out: []u8 = [];
+ defer free(out);
+ mcproto::encode_varint(&out, id);
+ network_send(0x00, out);
+
+ // TODO: is this necessary? (probably for anticheat reasons, at
+ // least...)
+ let out: []u8 = [];
+ defer free(out);
+ mcproto::encode_double(&out, PLAYER_POS[0]);
+ mcproto::encode_double(&out, PLAYER_POS[1]);
+ mcproto::encode_double(&out, PLAYER_POS[2]);
+ mcproto::encode_float(&out, PLAYER_YAW);
+ mcproto::encode_float(&out, PLAYER_PITCH);
+ mcproto::encode_bool(&out, true);
+ network_send(0x14, out);
+};
+
+fn handle_set_health(ctx: *mcproto::Context) (void | trace::failed) = {
+ const ctx_ = mcproto::context(ctx, "health");
+ const health = mcproto::decode_float(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "food");
+ const food = mcproto::decode_varint(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "saturation");
+ const saturation = mcproto::decode_float(&ctx_)?;
+ mcproto::expect_end(ctx)?;
+
+ PLAYER_HEALTH = health;
+ PLAYER_FOOD = food;
+ PLAYER_SATURATION = saturation;
+};
+
+fn handle_combat_death(ctx: *mcproto::Context) (void | trace::failed) = {
+ const ctx_ = mcproto::context(ctx, "player id");
+ const player_id = mcproto::decode_varint(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "entity id");
+ const entity_id = mcproto::decode_int(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "message");
+ const message = mcproto::decode_string(&ctx_, 0x7fff)?;
+ mcproto::expect_end(ctx)?;
+
+ trace::info(&trace::root, "death: {}", message);
+
+ death_show(message);
+};
+
+fn handle_set_default_spawn_position(ctx: *mcproto::Context)
+ (void | trace::failed) = {
+ const ctx_ = mcproto::context(ctx, "position");
+ const pos = decode_position(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "yaw");
+ const yaw = mcproto::decode_float(&ctx_)?;
+
+ LOADING_RECEIVED_SPAWN_POSITION = true;
+};
+
+fn handle_block_update(ctx: *mcproto::Context) (void | trace::failed) = {
+ const ctx_ = mcproto::context(ctx, "position");
+ const pos = decode_position(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "blockstate");
+ const bstate = mcproto::decode_varint(&ctx_)?;
+ if (bstate & ~0xffff != 0) {
+ return mcproto::error(&ctx_, "Blockstate id too large");
+ };
+ const bstate = bstate: u16;
+ mcproto::expect_end(&ctx_)?;
+
+ if (getsection(block_section(pos)) is null) {
+ mcproto::log(ctx, trace::level::WARN,
+ "Block update references nonexistent chunk: ( {} {} {} )",
+ pos.x, pos.y, pos.z);
+ return;
+ };
+
+ setblock(pos, bstate);
+};
+
+fn handle_update_section_blocks(ctx: *mcproto::Context)
+ (void | trace::failed) = {
+ const ctx_ = mcproto::context(ctx, "section position");
+ const section_pos = mcproto::decode_long(&ctx_)?: i64;
+ const section_pos = SectionPos {
+ x = (section_pos >> 42): i32,
+ y = (section_pos << 44 >> 44): i8,
+ z = (section_pos << 22 >> 42): i32,
+ };
+ if (getsection(section_pos) is null) {
+ mcproto::log(ctx, trace::level::WARN,
+ "Section update references nonexistent chunk: [ {} {} {} ]",
+ section_pos.x, section_pos.y, section_pos.z);
+ return;
+ };
+ const ctx_ = mcproto::context(ctx, "suppress light updates");
+ const suppress_light_updates = mcproto::decode_bool(&ctx_)?;
+ const ctx_ = mcproto::context(ctx, "block count");
+ const nblocks = mcproto::decode_varint(&ctx_)?;
+ for (let i = 0i32; i < nblocks; i += 1) {
+ const ctx_ = mcproto::context(ctx, "entry {}", i);
+ // TODO: should be varlong
+ const entry = mcproto::decode_varint(&ctx_)?;
+ const pos = BlockPos {
+ x = (section_pos.x << 4) + (entry >> 8 & 0xf),
+ y = (section_pos.y: i16 << 4) + (entry & 0xf): i16,
+ z = (section_pos.z << 4) + (entry >> 4 & 0xf),
+ };
+ const bstate = entry >> 12;
+ if (bstate & ~0xffff != 0) {
+ return mcproto::error(&ctx_, "Blockstate id too large");
+ };
+ const bstate = bstate: u16;
+
+ setblock(pos, bstate);
+ };
+ mcproto::expect_end(ctx)?;
+};
+
+fn handle_chunk_data_and_update_light(ctx: *mcproto::Context)
+ (void | trace::failed) = {
+ const ctx_ = mcproto::context(ctx, "x");
+ const x = mcproto::decode_int(&ctx_)?: i32;
+ const ctx_ = mcproto::context(ctx, "z");
+ const z = mcproto::decode_int(&ctx_)?: i32;
+ const chunk_pos = ChunkPos { x = x, z = z };
+ if (getchunk(chunk_pos) is null) {
+ return mcproto::error(ctx,
+ "Chunk out of range: [ {} {} ]",
+ chunk_pos.x, chunk_pos.z);
+ };
+ const ctx_ = mcproto::context(ctx, "heightmaps");
+ const heightmaps = decode_nbt(&ctx_)?;
+ free(heightmaps.0);
+ const heightmaps = heightmaps.1;
+ defer nbt::finish(heightmaps);
+ const ctx_ = mcproto::context(ctx, "data length");
+ const datalen = mcproto::decode_varint(&ctx_)?;
+ if (datalen < 0) {
+ return mcproto::error(&ctx_, "Data length must not be negative");
+ };
+ const datalen = datalen: u32;
+ const ctx_ = mcproto::context(ctx, "chunk data");
+ mcproto::decode_nbytes(&ctx_, datalen)?;
+ let cdec = *ctx.dec;
+ cdec.input = cdec.input[..cdec.pos];
+ cdec.pos -= datalen;
+ const cctx = mcproto::root(&cdec);
+
+ const chunk = chunk_init(chunk_pos);
+
+ for (let i = 0z; i < CHUNKS_HEIGHT; i += 1) {
+ const cctx_ = mcproto::context(&cctx, "block count");
+ const block_count = mcproto::decode_short(&cctx_)?;
+ const cctx_ = mcproto::context(&cctx, "bits per entry");
+ const bpe = mcproto::decode_byte(&cctx_)?;
+
+ const bstates = if (bpe == 0) { // constant
+ const cctx_ = mcproto::context(&cctx, "constant value");
+ const value = mcproto::decode_varint(&cctx_)?;
+ if (value > 0xffff) {
+ return mcproto::error(&cctx_, "Blockstate id too large");
+ };
+ const cctx_ = mcproto::context(&cctx, "array length");
+ const arraylen = mcproto::decode_varint(&cctx_)?;
+ if (arraylen != 0) {
+ return mcproto::error(&cctx_,
+ "Chunk data array must be empty when bpe = 0");
+ };
+ let array = comparray::new(1, 4096);
+ comparray::clear(&array, value: u16);
+ yield array;
+ } else {
+ let array = if (bpe >= 9) { // direct
+ yield comparray::new(0, 4096);
+ } else { // indirect
+ const cctx_ = mcproto::context(&cctx,
+ "palette length");
+ const palettelen =
+ mcproto::decode_varint(&cctx_)?: u32;
+ if (palettelen >= 256 || palettelen < 2) {
+ // = 256 is actually possible, but it's
+ // going to be a huge pain. oh well.
+ return mcproto::error(&cctx_, "Uh oh.");
+ };
+ let array =
+ comparray::new(palettelen: u8, 4096);
+ let palette = comparray::get_palette(&array);
+ for (let i = 0z; i < palettelen; i += 1) {
+ const cctx_ = mcproto::context(&cctx,
+ "palette entry {}", i);
+ const value =
+ mcproto::decode_varint(&cctx_)?;
+ if (value > 0xffff) {
+ return mcproto::error(&cctx_,
+ "Blockstate id too large");
+ };
+ palette[i] = value: u16;
+ };
+ yield array;
+ };
+
+ const bpe = if (bpe <= 4) 4u8
+ else if (bpe <= 8) bpe: u8
+ else 15u8;
+
+ const cctx_ = mcproto::context(&cctx, "array length");
+ const arraylen = mcproto::decode_varint(&cctx_)?;
+ const epl = 64 / bpe;
+ const arraylen_ = (4096z + epl - 1) / epl;
+ if (arraylen: size != arraylen_) {
+ return mcproto::error(&cctx_,
+ "Chunk data array with bpe = {} must have length {}, not {}",
+ bpe, arraylen_, arraylen);
+ };
+ const cctx_ = mcproto::context(&cctx, "array data");
+ let arraydata =
+ mcproto::decode_nbytes(&cctx_,
+ arraylen: size * 8)?;
+
+ let pos = 0z;
+ let long = 0u64;
+ let shift = 64u8;
+ const mask = (1u64 << bpe) - 1;
+ for (let i = 0z; i < 4096; i += 1) {
+ if (shift + bpe > 64) {
+ long = endian::begetu64(
+ arraydata[pos..pos + 8]);
+ pos += 8;
+ shift = 0;
+ };
+ const value = long >> shift & mask;
+ shift += bpe;
+ if (array.palette_size != 0
+ && value > array.palette_size) {
+ return mcproto::error(&cctx_,
+ "Palette index out of range (entry {})",
+ i);
+ };
+ if (array.palette_size == 0 && value > 0xffff) {
+ return mcproto::error(&cctx_,
+ "Blockstate id too large (entry {})",
+ i);
+ };
+ comparray::set(&array, i, value: u16);
+ };
+
+ yield array;
+ };
+
+ const cctx_ = mcproto::context(&cctx, "bits per entry");
+ const bpe = mcproto::decode_byte(&cctx_)?;
+ if (bpe == 0) { // constant
+ const cctx_ = mcproto::context(&cctx, "constant value");
+ mcproto::decode_varint(&cctx_)?;
+ } else if (bpe < 4) { // indirect
+ const cctx_ = mcproto::context(&cctx, "palette length");
+ const palettelen = mcproto::decode_varint(&cctx)?: u32;
+ for (let i = 0z; i < palettelen; i += 1) {
+ const cctx_ = mcproto::context(&cctx,
+ "palette entry {}", i);
+ mcproto::decode_varint(&cctx_)?;
+ };
+ };
+ const cctx_ = mcproto::context(&cctx, "array length");
+ const arraylen = mcproto::decode_varint(&cctx_)?: u32;
+ const cctx_ = mcproto::context(&cctx, "array data");
+ mcproto::decode_nbytes(&cctx_, arraylen: size * 8)?;
+
+ const section = &(chunk.sections as *[*]Section)[i];
+ section.bstates = bstates;
+ section.dirty = true;
+ };
+
+ // other stuff we don't care about for now.
+};
+
+fn handle_unload_chunk(ctx: *mcproto::Context)
+ (void | trace::failed) = {
+ const ctx_ = mcproto::context(ctx, "x");
+ const x = mcproto::decode_int(&ctx_)?: i32;
+ const ctx_ = mcproto::context(ctx, "z");
+ const z = mcproto::decode_int(&ctx_)?: i32;
+ chunk_destroy(ChunkPos { x = x, z = z });
+};
+
+// this is really awkward, but i hear a "match overhaul" is coming that should
+// improve things...
+fn nbt_get(obj: *nbt::Object, key: str)
+ (...nbt::Value | void) = {
+ match (nbt::get(obj, key)) {
+ case let value: *nbt::Value =>
+ return *value;
+ case null =>
+ return void;
+ };
+};