Back to Napcraft
SOURCE / PINNED RELEASE

Made of little things.

Napcraft

Release
ee47ac6a25df…
Author-recorded commit
410dea87e109…
License
LICENSE
Author’s source reference
nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-52f9e22f5ce

Archive hash verified: 9e6e0cabb18d0d46…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

docs/examples/minicraft/handler.ts
// Self-contained example backend. Nothing here is built into the provider.
// State values are validated against the adjacent schemas before commit.
type Context = {
  operation: string;
  principal: string;
  account: string | null;
  instance: string;
  release: string;
  owner: string;
  state: {
    get(collection: string, key: string): Promise<any>;
    set(collection: string, key: string, value: unknown): Promise<unknown>;
    access(): Promise<any>;
    setAccess(value: unknown): Promise<unknown>;
    setMember(principal: string, role: string | null): Promise<unknown>;
  };
};
const palette = ['air', 'stone', 'dirt', 'grass', 'wood', 'glass', 'sand', 'water'];
function key(position: { x: number; y: number; z: number }) {
  return `${position.x},${position.y},${position.z}`;
}
async function chunk(ctx: Context, position: { x: number; y: number; z: number }) {
  return (
    (await ctx.state.get('chunks', key(position))) ?? {
      position,
      revision: 0,
      blocks: Array(512).fill(0),
    }
  );
}
async function inventory(ctx: Context) {
  if (!ctx.account) throw new Error('ACCOUNT_REQUIRED: Survival inventory requires an account.');
  return (
    (await ctx.state.get('inventories', ctx.principal)) ?? {
      revision: 0,
      blocks: [64, 64, 64, 64, 64, 64, 64],
    }
  );
}
export async function handle(ctx: Context, input: any): Promise<Record<string, unknown>> {
  if (ctx.operation === 'createWorld') {
    if (
      input.guestsMayBuild &&
      (input.mode !== 'creative' || input.visibility !== 'public' || input.building !== 'everyone')
    )
      throw new Error(
        'BAD_INPUT: Guest building requires a public creative world open to everyone.',
      );
    await ctx.state.setAccess({
      visibility: input.visibility,
      building: input.building,
      guestsMayBuild: input.guestsMayBuild,
      members: {},
    });
    await ctx.state.set('worlds', 'meta', {
      name: input.name,
      seed: input.seed,
      mode: input.mode,
      owner: ctx.principal,
      release: ctx.release,
    });
    if (input.terrain === 'island') {
      // Four bounded chunks form the demo's initial island. Saved once, by the
      // same transaction as creation; clients never synthesize authoritative land.
      for (let cx = 0; cx < 2; cx++)
        for (let cz = 0; cz < 2; cz++) {
          const record = await chunk(ctx, { x: cx, y: 0, z: cz });
          for (let x = 0; x < 8; x++)
            for (let z = 0; z < 8; z++) {
              const wx = cx * 8 + x,
                wz = cz * 8 + z;
              const distance = Math.hypot(wx - 7.5, wz - 7.5);
              const shore = 6.5 + Math.sin(wx * 1.7 + input.seed) * 0.5;
              const height = distance < 3.5 ? 3 : distance < shore ? 2 : 1;
              for (let y = 0; y < height; y++)
                record.blocks[x + 8 * (z + 8 * y)] =
                  y < height - 1 ? 1 : distance < shore - 1 ? 3 : distance < shore ? 6 : 7;
              // A little tree in the corner leaves the central building site clear.
              if (wx === 4 && wz === 4)
                for (let y = height; y < height + 3; y++) record.blocks[x + 8 * (z + 8 * y)] = 4;
              if (Math.abs(wx - 4) <= 1 && Math.abs(wz - 4) <= 1)
                record.blocks[x + 8 * (z + 8 * 5)] = 3;
            }
          record.revision = 1;
          await ctx.state.set('chunks', key(record.position), record);
        }
    }
    return { world: ctx.instance };
  }
  const world = await ctx.state.get('worlds', 'meta');
  if (!world) throw new Error('BAD_INPUT: World metadata is missing.');
  if (ctx.operation === 'readWorld') return world;
  if (ctx.operation === 'readChunks') {
    const chunks = [];
    for (const position of input.positions) chunks.push(await chunk(ctx, position));
    return { chunks };
  }
  if (ctx.operation === 'readMyInventory') return inventory(ctx);
  if (ctx.operation === 'setMember') {
    await ctx.state.setMember(input.principal, input.role === 'remove' ? null : input.role);
    return { changed: true };
  }
  if (!['placeBlock', 'removeBlock'].includes(ctx.operation))
    throw new Error('BAD_INPUT: Unknown game command.');
  const position = {
    x: Math.floor(input.x / 8),
    y: Math.floor(input.y / 8),
    z: Math.floor(input.z / 8),
  };
  const record = await chunk(ctx, position);
  if (record.revision !== input.expectedChunkRevision)
    throw new Error('CONFLICT: Chunk changed. Refresh it before trying again.');
  const offset = (input.x % 8) + 8 * ((input.z % 8) + 8 * (input.y % 8));
  const before = record.blocks[offset];
  const after = ctx.operation === 'placeBlock' ? palette.indexOf(input.block) : 0;
  if (ctx.operation === 'placeBlock' && before !== 0)
    throw new Error('CONFLICT: There is already a block here.');
  if (ctx.operation === 'removeBlock' && before === 0)
    throw new Error('CONFLICT: This cell is already empty.');
  if (world.mode === 'survival') {
    const bag = await inventory(ctx),
      slot = (ctx.operation === 'placeBlock' ? after : before) - 1;
    if (ctx.operation === 'placeBlock' && bag.blocks[slot] <= 0)
      throw new Error('FORBIDDEN: You do not have that block in your inventory.');
    bag.blocks[slot] += ctx.operation === 'placeBlock' ? -1 : 1;
    bag.revision++;
    await ctx.state.set('inventories', ctx.principal, bag);
  }
  record.blocks[offset] = after;
  record.revision++;
  await ctx.state.set('chunks', key(position), record);
  return { chunkRevision: record.revision };
}