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(®istry, "minecraft:dimension_type")) { case let reg: nbt::Object => match (nbt_get(®, "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; }; };