Skip to content
Suzu
19 min read

【Misc】AIS3 Pre-Exam 2026 Writeup: アッシェンテ!

A misc writeup for アッシェンテ!, analyzing the Minecraft mod and RNG path.

Series
30 posts
View the complete series →
On this page

【AIS3 Pre-Exam 2026 Writeup】Misc - アッシェンテ!

I started from the mod source.

rg -n "banPlayer|finishUsingItem|handleContainerClick|handlePlayerAction|PrimedTnt|recipe|flag" \
  minecraft/mod/src/main/java \
  minecraft/mod/src/main/resources/data/chalmod/recipe/flag.json

ChallengeInstancer.java creates one private dimension for every player. It copies world/template, clears the inventory, and teleports the player to (-4.5, 3.0, -4.5). So the local template world is the real map to study.

ChalMod.java was the first warning. Logout calls banPlayer, and death also calls banPlayer. This matches the challenge text. Reconnecting many times is not a good test method.

The dispenser was protected in ServerGamePacketListenerImplMixin.java. Container clicks are cancelled when the dispenser block position is (0, 4, 3). Destroy block packets for the same position are also cancelled. Opening or breaking the dispenser was my first guess, but the mixin killed both paths.

TNT avoidance also looked useless. PrimedTntMixin.java hooks explode and kills the nearest player with Float.MAX_VALUE explosion damage, then calls player.kill(level) as a failsafe. Running away from TNT does not help. No trigger, no path.

FlagItem.java is simple. Eating the custom flag item on a dedicated server reads /flag.txt and sends it as a title packet. The recipe in flag.json is:

AAA
A A
AAA

So eight diamonds are needed.

A native/debugger angle did not fit this task. There was no useful ELF path, no glibc printf, no GOT, no saved RIP, and no crash trigger. The useful observations came from Java source, local server behavior, and Minecraft packets.

Local server:

cd minecraft/mod
./gradlew runServer

The map was small. Mineflayer was enough to inspect it:

  • chest at (-4, 3, 0), with one axolotl_bucket
  • dispenser at (0, 4, 3)
  • button at (0, 4, 2)
  • tree/log nearby for crafting table
  • dispenser slot 4 had diamonds
  • every other dispenser slot had TNT

Slot 4 had a diamond stack, but one dispenser activation only dispenses one item from the stack. So the bot needs slot 4 eight times. One wrong slot means TNT, death, ban.

Random pressing was dead. The chance is too bad and the remote warning says not to burn accounts.

That left the dispenser RNG.

Minecraft uses LegacyRandomSource, same 48-bit Java LCG:

state = (state * 25214903917 + 11) mod 2^48

Dispenser slot choice works like reservoir sampling over non-empty slots. With nine non-empty slots, I modeled it like this:

function randomSlotState(s) {
  let selected = -1;
  for (let i = 0; i < 9; i++) {
    const [s2, r] = nextIntState(s, i + 1);
    s = s2;
    if (r === 0) selected = i;
  }
  return { state: s, selected };
}

If selected === 4, press is safe. Otherwise it is TNT.

The axolotl bucket looked weird at first, so I ignored it for a while. Later it became the RNG oracle. When an axolotl is placed from a bucket, the server randomizes its yaw. The spawn_entity packet stores yaw as one byte. That byte is basically the top 8 bits after one LCG step:

packet_yaw = floor(nextFloat() * 256)

So one axolotl placement leaks one byte of RNG state.

The slow oracle worked locally, but it was not stable enough:

  1. place axolotl
  2. wait for spawn packet
  3. scoop water
  4. recapture axolotl

The gap between two yaw bytes was not always one step. Axolotl AI could run and consume RNG. Local runs showed different gaps, so remote timing felt unsafe.

The fast oracle fixed this. Predict the new entity id as maxEntityId + 1, place axolotl, scoop water immediately, and send use_entity packets to the predicted id. This usually captures it before AI consumes useful randomness.

const predicted = maxEntityId + 1;
bot.activateItem(false); // place axolotl
bot.activateItem(false); // scoop water
sendPredictedCapture(predicted);

After this change, the yaw observations behaved like gap 1. That made the lattice solver much cleaner.

The LLL helper recovers the 48-bit state from top-byte leaks. The bot sends JSON to /tmp/mcbot/mc_yaw_solver.py:

{
  "observations": [52, 56, 136, 150, 195, 203, 40, 67, 128, 62, 39, 215],
  "gapOptions": [1],
  "prefixObservations": 12,
  "postCallsAfterCollection": 0,
  "diamonds": 8
}

After the state is known, the exploit still does not blindly trust old prediction. Every online cycle takes one new fast yaw observation and re-syncs candidate states. If the candidate set is not unique, skip. If the next dispenser slot is not 4, skip. Only press when the model says slot 4.

Another small bug was the button. Sometimes the button was still powered, so the next click did nothing. The final bot waits for release before every safe press.

After eight diamonds, the bot breaks one log, crafts planks, crafts a crafting table, crafts the flag item manually, eats it, and reads the title packet. Large movement jumps sometimes made the server reject the position. Smooth movement in small steps fixed the tree/crafting part.

Local run:

cd /tmp/mcbot
node solve_fast.js 127.0.0.1 25565 localtest$(date +%s | tail -c 7) 12

Remote run:

cd /tmp/mcbot
node solve_fast.js chals1.ais3.org 25565 rr$(date +%s | tail -c 8) 12 | tee /tmp/mcbot/remote_recheck.log

The local server can reach the flag item but has no real /flag.txt. Remote returned the real title text.

The flag had a Unicode trap:

心理状態᠁そんな

That middle character is U+1801 MONGOLIAN ELLIPSIS, not normal ... and not U+2026. I verified it by logging UTF-8 hex, base64, and codepoints from the title packet.

Final flag:

AIS3{この世に、運なんて存在しない。ルール、前提、心理状態᠁そんな無数の『見えない変数』がもたらす予測できない必然で、ゲームの勝敗は始める前には終わってるんだ。偶然なんてない。_df62cfd62c4655ecd71756cec46e60cb}

LLL helper

The bot calls this helper as /tmp/mcbot/mc_yaw_solver.py.

#!/usr/bin/env python3
import json
import sys
from sympy import Matrix

A = 25214903917
C = 11
M = 1 << 48
MASK = M - 1
OBS_SHIFT = 40


def step(s):
    return (s * A + C) & MASK


def advance(s, n):
    for _ in range(n):
        s = step(s)
    return s


def affine(n):
    mul = 1
    add = 0
    for _ in range(n):
        add = (add * A + C) & MASK
        mul = (mul * A) & MASK
    return mul, add


def round_div(n, d):
    if n >= 0:
        return (n + d // 2) // d
    return -((-n + d // 2) // d)


def recover_yaw_state_for_gaps(observations, gaps):
    if len(observations) < 8:
        raise ValueError("need at least 8 yaw bytes")

    ps = [1]
    qs = [0]
    p = 1
    q = 0
    for gap in gaps:
        gp, gq = affine(gap)
        q = (gp * q + gq) & MASK
        p = (gp * p) & MASK
        ps.append(p)
        qs.append(q)

    k = len(observations)
    rows = [[M] + [0] * (k - 1)]
    for i in range(1, k):
        row = [0] * k
        row[0] = ps[i]
        row[i] = -1
        rows.append(row)

    lattice = Matrix(rows).lll()
    shifted = Matrix([((observations[i] << OBS_SHIFT) - qs[i]) % M for i in range(k)])
    projected = lattice * shifted
    nearest = Matrix([round_div(int(x), M) for x in projected])
    correction = lattice.LUsolve(nearest * M - projected)
    candidate = shifted + correction
    states = [int((candidate[i] + qs[i]) % M) for i in range(k)]

    if [s >> OBS_SHIFT for s in states] != observations:
        raise ValueError("LLL recovery failed high-byte verification")
    for i, gap in enumerate(gaps):
        if advance(states[i], gap) != states[i + 1]:
            raise ValueError("LLL recovery failed recurrence verification")
    return states


def recover_yaw_state(observations, first_gap=4, later_gap=3, gap_options=None, prefix_observations=10):
    if not gap_options:
        gaps = [first_gap] + [later_gap] * (len(observations) - 2)
        return recover_yaw_state_for_gaps(observations, gaps), gaps

    from itertools import product

    prefix_n = min(len(observations), max(8, prefix_observations))
    tail = observations[prefix_n:]
    for gaps_prefix in product(gap_options, repeat=prefix_n - 1):
        try:
            states_prefix = recover_yaw_state_for_gaps(observations[:prefix_n], list(gaps_prefix))
        except Exception:
            continue

        states = states_prefix[:]
        gaps = list(gaps_prefix)
        state = states[-1]
        ok = True
        for obs in tail:
            matched = False
            for gap in gap_options:
                candidate = advance(state, gap)
                if (candidate >> OBS_SHIFT) == obs:
                    gaps.append(gap)
                    states.append(candidate)
                    state = candidate
                    matched = True
                    break
            if not matched:
                ok = False
                break
        if ok:
            return states, gaps
    raise ValueError(f"no gap pattern matched observations with options {gap_options}")


def next_int(s, bound):
    while True:
        s = step(s)
        bits = s >> 17
        if bound & (bound - 1) == 0:
            return s, (bound * bits) >> 31
        value = bits % bound
        # Java int overflow semantics for: bits - value + (bound - 1) < 0
        test = (bits - value + (bound - 1)) & 0xffffffff
        if test < 0x80000000:
            return s, value


def random_slot(s, slots=9):
    selected = -1
    for i in range(slots):
        s, r = next_int(s, i + 1)
        if r == 0:
            selected = i
    return s, selected


def consume_default_dispense(s):
    # ItemEntity constructor consumes two nextDouble calls (4 LCG steps), then
    # DefaultDispenseItemBehavior.spawnItem consumes seven doubles (14 steps).
    return advance(s, 18)


def protected_step(s):
    s, selected = random_slot(s, 9)
    if selected == 4:
        s = consume_default_dispense(s)
    else:
        # PrimedTnt constructor consumes one nextDouble.
        s = advance(s, 2)
    return s, selected


def plan_actions(state, diamonds=8, max_actions=500):
    actions = []
    got = 0
    while got < diamonds:
        after_press, slot = protected_step(state)
        if slot == 4:
            got += 1
            actions.append({
                "type": "P",
                "diamond": got,
                "stateBefore": state,
                "stateAfter": after_press
            })
            state = after_press
        else:
            actions.append({
                "type": "A",
                "byte": step(state) >> OBS_SHIFT,
                "nextProtectedSlot": slot,
                "stateBefore": state,
                "stateAfter": advance(state, 3)
            })
            state = advance(state, 3)
        if len(actions) > max_actions:
            raise RuntimeError("too many planned actions")
    return actions, state


def main():
    data = json.load(sys.stdin)
    observations = [int(x) & 0xff for x in data["observations"]]
    first_gap = int(data.get("firstGap", 4))
    later_gap = int(data.get("laterGap", 3))
    post_calls = int(data.get("postCallsAfterCollection", 2))
    diamonds = int(data.get("diamonds", 8))

    gap_options = data.get("gapOptions")
    if gap_options is not None:
        gap_options = [int(x) for x in gap_options]
    states, gaps = recover_yaw_state(
        observations,
        first_gap,
        later_gap,
        gap_options=gap_options,
        prefix_observations=int(data.get("prefixObservations", 10))
    )
    current = advance(states[-1], post_calls)
    actions, final_state = plan_actions(current, diamonds)
    print(json.dumps({
        "ok": True,
        "stateFirstYaw": states[0],
        "stateLastYaw": states[-1],
        "stateAfterCollection": current,
        "gaps": gaps,
        "actions": actions,
        "finalState": final_state,
        "observations": observations
    }))


if __name__ == "__main__":
    try:
        main()
    except Exception as exc:
        print(json.dumps({"ok": False, "error": str(exc)}))
        sys.exit(1)

Full exploit script

This is the mineflayer bot from /tmp/mcbot/solve_fast.js.

const mineflayer = require('mineflayer');
const { Vec3 } = require('vec3');
const conv = require('mineflayer/lib/conversions');
const { execFileSync } = require('child_process');

const host = process.argv[2] || '127.0.0.1';
const port = Number(process.argv[3] || 25565);
const username = process.argv[4] || `ais3${Date.now().toString(36).slice(-8)}`.slice(0, 16);
const observationCount = Number(process.argv[5] || 12);

const bot = mineflayer.createBot({
  host,
  port,
  username,
  auth: 'offline',
  version: '1.21.11',
});

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let seq = 0;
let maxEntityId = 0;
const yawBytes = [];
const titles = [];
const entityByInternalId = new Map();

const LCG_A = 25214903917n;
const LCG_C = 11n;
const LCG_MASK = (1n << 48n) - 1n;

function inv() {
  return bot.inventory
    .items()
    .map((i) => `${i.name}#${i.type}x${i.count}@${i.slot}`)
    .join(', ');
}

function invCount(name) {
  return bot.inventory
    .items()
    .filter((i) => i.name === name)
    .reduce((n, i) => n + i.count, 0);
}

function invItem(name) {
  return bot.inventory.items().find((i) => i.name === name);
}

function stepState(s) {
  return (s * LCG_A + LCG_C) & LCG_MASK;
}

function advanceState(s, n) {
  for (let i = 0; i < n; i++) s = stepState(s);
  return s;
}

function highByte(s) {
  return Number(s >> 40n) & 0xff;
}

function nextIntState(s, bound) {
  while (true) {
    s = stepState(s);
    const bits = Number(s >> 17n);
    if ((bound & (bound - 1)) === 0) return [s, Math.floor((bound * bits) / 0x80000000)];
    const value = bits % bound;
    if ((bits - value + (bound - 1)) >>> 0 < 0x80000000) return [s, value];
  }
}

function randomSlotState(s) {
  let selected = -1;
  for (let i = 0; i < 9; i++) {
    const out = nextIntState(s, i + 1);
    s = out[0];
    if (out[1] === 0) selected = i;
  }
  return { state: s, selected };
}

function protectedStepState(s) {
  const slot = randomSlotState(s);
  s = slot.state;
  if (slot.selected === 4) s = advanceState(s, 18);
  else s = advanceState(s, 2);
  return { state: s, selected: slot.selected };
}

function uniqStates(states) {
  const seen = new Set();
  const out = [];
  for (const state of states) {
    const key = state.toString();
    if (!seen.has(key)) {
      seen.add(key);
      out.push(state);
    }
  }
  return out;
}

function forwardMatches(bases, observedByte, maxGap = 24) {
  const out = [];
  for (const base of bases) {
    let state = base;
    for (let gap = 1; gap <= maxGap; gap++) {
      state = stepState(state);
      if (highByte(state) === observedByte) out.push(state);
    }
  }
  return uniqStates(out);
}

function spoofPos(x, y, z, yaw = bot.entity?.yaw || 0, pitch = bot.entity?.pitch || 0) {
  bot.entity.position.set(x, y, z);
  bot._client.write('position_look', {
    x,
    y,
    z,
    yaw: conv.toNotchianYaw(yaw),
    pitch: conv.toNotchianPitch(pitch),
    flags: { onGround: true, hasHorizontalCollision: false },
  });
}

async function hold(x, y, z, n = 4, delay = 80) {
  for (let i = 0; i < n; i++) {
    spoofPos(x, y, z);
    await sleep(delay);
  }
}

async function moveLine(x, y, z, maxStep = 0.45) {
  const from = bot.entity.position.clone();
  const dx = x - from.x;
  const dy = y - from.y;
  const dz = z - from.z;
  const steps = Math.max(1, Math.ceil(Math.sqrt(dx * dx + dy * dy + dz * dz) / maxStep));
  for (let i = 1; i <= steps; i++) {
    await hold(
      from.x + (dx * i) / steps,
      from.y + (dy * i) / steps,
      from.z + (dz * i) / steps,
      1,
      45
    );
  }
}

async function path(pts, n = 2) {
  for (const p of pts) await hold(...p, n);
}

async function smoothPath(pts) {
  for (const p of pts) await moveLine(...p);
}

async function pathToChest() {
  await path([
    [-4.5, 3, -3.5],
    [-4.2, 3, -2.5],
    [-4.0, 3, -1.5],
    [-3.6, 3, -0.5],
  ]);
}

async function pathToTree() {
  await smoothPath([
    [-4.5, 3, -3.5],
    [-3.8, 3, -3.5],
    [-3.1, 3, -3.5],
    [-2.4, 3, -3.5],
    [-1.7, 3, -3.5],
    [-1.0, 3, -3.2],
    [-0.3, 3, -2.8],
    [0.35, 3, -2.45],
  ]);
}

function rawUseOn(pos, direction, cursorY = 0.5) {
  bot._client.write('block_place', {
    hand: 0,
    location: pos,
    direction,
    cursorX: 0.5,
    cursorY,
    cursorZ: 0.5,
    insideBlock: false,
    worldBorderHit: false,
    sequence: ++seq,
  });
  bot._client.write('arm_animation', { hand: 0 });
}

async function equip(name) {
  const item = invItem(name);
  if (!item) throw new Error(`missing ${name}; inv=${inv()}`);
  await bot.equip(item, 'hand');
  await sleep(80);
}

async function clearHand() {
  if (bot.heldItem) {
    await bot.unequip('hand');
    await sleep(120);
  }
}

async function waitForInventory(name, timeoutMs = 2500) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    if (invItem(name)) return;
    await sleep(60);
  }
  throw new Error(`timeout waiting for ${name}; inv=${inv()}`);
}

async function waitForYaw(before, timeoutMs = 2500) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    if (yawBytes.length > before) return yawBytes[before];
    await sleep(40);
  }
  throw new Error(`timeout waiting for yaw oracle; before=${before}`);
}

async function digAt(pos, stand = null, collect = null) {
  if (stand) await hold(...stand, 3);
  const block = bot.blockAt(pos);
  console.log('[dig]', pos.toString(), block?.name);
  if (block && block.name !== 'air') {
    await bot.dig(block);
    if (collect) await hold(...collect, 12, 90);
    await sleep(250);
  }
}

async function collectDroppedNear(center, wantedName, beforeCount, timeoutMs = 5000) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    if (invCount(wantedName) > beforeCount) return;
    const nearby = Object.values(bot.entities)
      .filter((e) => e.name === 'item' && e.position && e.position.distanceTo(center) < 4.5)
      .sort((a, b) => a.position.distanceSquared(center) - b.position.distanceSquared(center));
    if (nearby.length) {
      const item = nearby[0];
      await hold(item.position.x, 3, item.position.z, 2, 45);
    } else {
      await hold(center.x, 3, center.z, 1, 70);
    }
  }
  throw new Error(`failed to collect ${wantedName}; inv=${inv()}`);
}

async function waitButtonReleased(timeoutMs = 2500) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const button = bot.blockAt(new Vec3(0, 4, 2));
    if (!button?.getProperties?.().powered) return;
    await sleep(50);
  }
  throw new Error('button stayed powered too long');
}

async function takeAxolotlBucket() {
  await pathToChest();
  const chest = await bot.openChest(bot.blockAt(new Vec3(-4, 3, 0)));
  const axolotl = chest.containerItems().find((i) => i.name === 'axolotl_bucket');
  if (!axolotl) throw new Error('chest has no axolotl_bucket');
  await chest.withdraw(axolotl.type, null, 1);
  await sleep(250);
  await chest.close();
  console.log('[after chest]', inv());
}

function sendPredictedCapture(predicted) {
  bot._client.write('use_entity', {
    target: predicted,
    mouse: 2,
    sneaking: false,
    hand: 0,
    x: 0,
    y: 0.25,
    z: 0,
  });
  bot._client.write('use_entity', { target: predicted, mouse: 0, sneaking: false, hand: 0 });
}

async function aimAtOracleFloor(cell) {
  await bot.lookAt(new Vec3(cell.x + 0.5, cell.y - 0.02, cell.z + 0.5), true);
  spoofPos(cell.x + 0.5, 3, cell.z + 0.5);
}

async function aimAtOracleWater(cell) {
  await bot.lookAt(new Vec3(cell.x + 0.5, cell.y + 0.35, cell.z + 0.5), true);
  spoofPos(cell.x + 0.5, 3, cell.z + 0.5);
}

async function fastCycle(cell, label = 'oracle') {
  await equip('axolotl_bucket');
  const before = yawBytes.length;
  const predicted = maxEntityId + 1;
  await hold(cell.x + 0.5, 3, cell.z + 0.5, 2, 60);
  await aimAtOracleFloor(cell);
  bot.activateItem(false);
  await aimAtOracleWater(cell);
  bot.activateItem(false);
  sendPredictedCapture(predicted);
  const observed = await waitForYaw(before);
  await waitForInventory('axolotl_bucket');
  console.log(`[fast ${label}]`, `pred=${predicted}`, `yaw=${observed}`, `max=${maxEntityId}`);
  return observed;
}

async function placePressFastCapture(cell) {
  await equip('axolotl_bucket');
  const before = yawBytes.length;
  const predicted = maxEntityId + 1;
  await hold(cell.x + 0.5, 3, cell.z + 0.5, 2, 60);
  await aimAtOracleFloor(cell);
  bot.activateItem(false);
  rawUseOn(new Vec3(0, 4, 2), 2);
  await aimAtOracleWater(cell);
  bot.activateItem(false);
  sendPredictedCapture(predicted);
  const observed = await waitForYaw(before);
  await waitForInventory('axolotl_bucket');
  console.log('[fast press]', `pred=${predicted}`, `yaw=${observed}`, `max=${maxEntityId}`);
  return observed;
}

function solveFastState() {
  const input = JSON.stringify({
    observations: yawBytes.slice(0, observationCount),
    diamonds: 8,
    postCallsAfterCollection: 0,
    gapOptions: [1],
    prefixObservations: observationCount,
  });
  const stdout = execFileSync('python3', ['/tmp/mcbot/mc_yaw_solver.py'], {
    input,
    encoding: 'utf8',
    maxBuffer: 1024 * 1024,
  });
  const solved = JSON.parse(stdout);
  if (!solved.ok) throw new Error(`solver failed: ${solved.error}`);
  return solved;
}

async function collectAfterImmediatePress(expectedCount) {
  for (let i = 0; i < 90; i++) {
    await hold(0.5, 5.0, 3.5, 1, 60);
    const count = invCount('diamond');
    if (count >= expectedCount) {
      console.log(`[diamond] ${count}/8`);
      return;
    }
    const item = Object.values(bot.entities).find(
      (e) => e.name === 'item' && e.position && e.position.distanceTo(new Vec3(0.5, 5, 3.5)) < 2.8
    );
    if (item) await hold(item.position.x, 5.0, item.position.z, 1, 35);
  }
  throw new Error(
    `diamond was not collected after press; want=${expectedCount} now=${invCount('diamond')} inv=${inv()}`
  );
}

async function craftItem(name, count, tablePos = null) {
  const item = bot.registry.itemsByName[name];
  if (!item) throw new Error(`unknown item ${name}`);
  const table = tablePos ? bot.blockAt(tablePos) : null;
  const recipes = bot.recipesFor(item.id, null, count, table);
  if (!recipes.length) throw new Error(`no recipe for ${name}; inv=${inv()}`);
  await bot.craft(recipes[0], count, table);
  await sleep(500);
  console.log('[crafted]', name, inv());
}

async function makeCraftingTable() {
  await pathToTree();
  await clearHand();
  const logsBefore = invCount('oak_log');
  await digAt(new Vec3(0, 3, -3), [0.35, 3, -2.45]);
  await collectDroppedNear(new Vec3(0.5, 3, -2.5), 'oak_log', logsBefore);
  await craftItem('oak_planks', 1);
  await craftItem('crafting_table', 1);
  await bot.equip(invItem('crafting_table'), 'hand');
  await path([
    [0.1, 3, -2.0],
    [-0.4, 3, -1.6],
    [-0.9, 3, -1.3],
  ]);
  rawUseOn(new Vec3(-1, 2, -1), 1);
  for (let i = 0; i < 30 && bot.blockAt(new Vec3(-1, 3, -1))?.name !== 'crafting_table'; i++) {
    await sleep(120);
    spoofPos(-0.9, 3, -1.3);
  }
  const table = bot.blockAt(new Vec3(-1, 3, -1));
  if (table?.name !== 'crafting_table') throw new Error('failed to place crafting table');
  console.log('[table]', table.position.toString());
  return table.position;
}

async function manualCraftFlag(tablePos) {
  const table = bot.blockAt(tablePos);
  await hold(-0.9, 3, -1.3, 4);
  bot.activateBlock(table);
  for (let i = 0; i < 30 && !bot.currentWindow; i++) await sleep(100);
  const window = bot.currentWindow;
  if (!window || !window.type.startsWith('minecraft:crafting'))
    throw new Error(`crafting table did not open: ${window?.type}`);

  const diamondId = bot.registry.itemsByName.diamond.id;
  const destSlots = [1, 2, 3, 4, 6, 7, 8, 9];
  for (const dest of destSlots) {
    if (!window.selectedItem || window.selectedItem.type !== diamondId) {
      const source = window.findInventoryItem(diamondId, null);
      if (!source) throw new Error(`missing diamonds while crafting flag; inv=${inv()}`);
      await bot.clickWindow(source.slot, 0, 0);
    }
    await bot.clickWindow(dest, 1, 0);
    await sleep(80);
  }

  for (let i = 0; i < 40 && !window.slots[0]; i++) await sleep(100);
  if (!window.slots[0]) throw new Error('flag recipe produced no result slot');
  console.log(
    '[flag result]',
    `type=${window.slots[0].type}`,
    `name=${window.slots[0].name}`,
    `count=${window.slots[0].count}`
  );
  await bot.clickWindow(0, 0, 0);
  await sleep(200);
  const empty = window.firstEmptyInventorySlot();
  if (empty === null) throw new Error('no empty slot for flag');
  await bot.clickWindow(empty, 0, 0);
  await sleep(300);
  bot.closeWindow(window);
  await sleep(300);
  console.log('[after flag craft]', inv());
}

async function eatFlag() {
  const diamondId = bot.registry.itemsByName.diamond.id;
  const flag = bot.inventory.items().find((i) => i.name === 'unknown' && i.type !== diamondId);
  if (!flag) throw new Error(`flag item not found after craft; inv=${inv()}`);
  await bot.equip(flag, 'hand');
  await sleep(200);
  console.log('[eat]', `type=${flag.type}`, `slot=${flag.slot}`);
  bot.activateItem(false);
  await sleep(2600);
  bot.deactivateItem();
  await sleep(1200);
}

function entityNameForType(type) {
  if (!entityByInternalId.size) {
    for (const entity of bot.registry.entitiesArray)
      entityByInternalId.set(entity.internalId, entity);
  }
  return entityByInternalId.get(type)?.name || `type_${type}`;
}

bot._client.on('spawn_entity', (packet) => {
  maxEntityId = Math.max(maxEntityId, packet.entityId);
  const name = entityNameForType(packet.type);
  if (name === 'axolotl') {
    const byte = packet.yaw & 0xff;
    yawBytes.push(byte);
    console.log('[oracle]', yawBytes.length, `id=${packet.entityId}`, byte);
  }
});

bot.on('entitySpawn', (entity) => {
  maxEntityId = Math.max(maxEntityId, entity.id);
});

for (const packetName of ['set_title_text', 'set_title_subtitle', 'action_bar', 'system_chat']) {
  bot._client.on(packetName, (packet) => {
    titles.push({ packetName, packet });
    console.log(`[${packetName}]`, JSON.stringify(packet));
    const value = packet?.text?.value;
    if (packetName === 'set_title_text' && typeof value === 'string') {
      const bytes = Buffer.from(value, 'utf8');
      console.log('[title_text_utf8_hex]', bytes.toString('hex'));
      console.log('[title_text_base64]', bytes.toString('base64'));
      console.log(
        '[title_text_codepoints]',
        Array.from(value)
          .map((ch) => `U+${ch.codePointAt(0).toString(16).toUpperCase().padStart(4, '0')}`)
          .join(' ')
      );
    }
  });
}

bot.on('message', (msg) => console.log('[chat]', msg.toString()));
bot.on('death', () => console.log('[death]'));
bot.on('kicked', (reason) => console.log('[kicked]', JSON.stringify(reason)));
bot.on('error', (err) => console.log('[error]', err.stack || err.message));
bot.on('end', (reason) => console.log('[end]', reason, 'titles=' + JSON.stringify(titles)));

bot.once('spawn', async () => {
  try {
    maxEntityId = Math.max(maxEntityId, bot.entity.id);
    await sleep(2500);
    console.log('[spawn]', username, bot.entity.position.toString(), `entity=${bot.entity.id}`);
    await takeAxolotlBucket();

    const cell = new Vec3(-2, 2, 0);
    await digAt(cell, [cell.x + 0.5, 3, cell.z + 0.5]);

    for (let i = 0; i < observationCount; i++) {
      console.log(`[collect fast oracle] ${i + 1}/${observationCount}`);
      await fastCycle(cell, String(i + 1));
    }
    console.log('[oracle bytes]', yawBytes.slice(0, observationCount).join(','));

    const solved = solveFastState();
    let current = BigInt(solved.stateAfterCollection);
    let candidates = [current];
    console.log('[state first yaw]', '0x' + BigInt(solved.stateFirstYaw).toString(16));
    console.log('[state current]', '0x' + current.toString(16));
    console.log('[gaps]', solved.gaps.join(','));

    let diamonds = invCount('diamond');
    let onlineCycles = 0;
    while (diamonds < 8) {
      if (++onlineCycles > 1000) throw new Error('too many online oracle cycles');
      const observed = await fastCycle(cell, `online-${onlineCycles}`);
      candidates = forwardMatches(candidates, observed);
      if (!candidates.length) {
        throw new Error(`could not resync observed yaw ${observed}`);
      }
      if (candidates.length !== 1) {
        console.log(
          '[online]',
          `cycle=${onlineCycles}`,
          `yaw=${observed}`,
          `states=${candidates.length}`,
          'ambiguous skip'
        );
        continue;
      }

      const yawState = candidates[0];
      const planned = protectedStepState(yawState);
      console.log(
        '[online]',
        `cycle=${onlineCycles}`,
        `yaw=${observed}`,
        `state=0x${yawState.toString(16)}`,
        `slot=${planned.selected}`,
        planned.selected === 4 ? `press D${diamonds + 1}` : 'skip'
      );
      if (planned.selected === 4) {
        await waitButtonReleased();
        rawUseOn(new Vec3(0, 4, 2), 2);
        current = planned.state;
        candidates = [current];
        await collectAfterImmediatePress(diamonds + 1);
        diamonds = invCount('diamond');
      } else {
        current = yawState;
        candidates = [current];
      }
    }

    const tablePos = await makeCraftingTable();
    await manualCraftFlag(tablePos);
    await eatFlag();
    console.log('[done]', inv());
  } catch (err) {
    console.log('[fatal]', err.stack || err.message);
  } finally {
    await sleep(1000);
    bot.end();
  }
});

Share this page