===============================================================================
 ZANPRO'S ENHANCED CORE - MAINTAINER GUIDE
===============================================================================

Read this before updating or changing the mod. It explains what each part does
in game, how the code does it, where the files are, and what tends to break
when Minecraft updates. It is written so that a person or an AI assistant with
no earlier context can work on the project safely.

Last written for: mod version 1.1.3, Minecraft Java 26.3.


-------------------------------------------------------------------------------
 0. QUICK FACTS
-------------------------------------------------------------------------------

Author / brand ....... ZanPro (published on CurseForge)
Mod name ............. ZanPro's Enhanced Core
Mod ID ............... zanpros_enhanced_core
Loader ............... Fabric (requires Fabric API)
Minecraft ............ 26.3 (fabric.mod.json: "minecraft": "~26.3")
Java ................. 25 (JDK path is set in gradle.properties)
Fabric Loader ........ 0.19.5+
Fabric API ........... 0.160.5+26.3
Fabric Loom .......... 1.17.21 (build.gradle)
Gradle ............... 9.6.0 (wrapper)
Build ................ gradle clean build   (installed Gradle 9.5+)
                       or .\gradlew.bat clean build   (wrapper)
Output jar ........... build/libs/ZanProsEnhancedCore-<version>.jar
Sides ................ Must be installed on BOTH client and server.

Code uses Mojang's official names (Identifier, Level, BlockBehaviour,
ServerLevel, ...), not Yarn names.

This mod is six older standalone mods merged together:

  Module                           Was standalone mod                 Version
  -------------------------------  ---------------------------------  -------
  A. Vertical Stairs and Slabs     VerticalStairsAndSlabs             1.2.3
  B. Instamine Deepslate           InstamineDeepslate                 1.1.2
  C. Moveable Spawners             MoveableSpawners                   1.1.5
  D. Craftable Survival Debug Stick CraftableSurvivalDebugStick       1.1.2
  E. Remove Nametagged Peaceful    RemoveNametaggedPeacefulDespawn    1.2.0
     Despawn
  F. Remove Too Expensive          RemoveTooExpensive                 1.1.0

!! RULE: NEVER CHANGE THE REGISTRY NAMESPACES !!
Each module still registers its content under its OLD namespace:
  verticalstairsandslabs:   moveablespawners:   survivaldebugstick:
Worlds made with the old separate mods store blocks/items by these IDs. If the
namespaces change, placed vertical stairs/slabs, typed spawner items and debug
sticks will disappear from existing worlds. The namespace constants are the
MOD_ID fields in SurvivalDebugStickMod, MoveableSpawners and
VerticalStairsAndSlabsMod. They are deliberately different from the real
mod ID (zanpros_enhanced_core).

fabric.mod.json lists the six old mod IDs under "provides". This makes Fabric
refuse to start with a clear error if an old standalone jar is still in the
mods folder, instead of crashing on duplicate registrations.

fabric.mod.json also has "breaks": {"mr_disable_tooexpensive": "*"}. That is
the mod ID of an unrelated third-party data-pack mod that did the same job as
Module F. It caused a data pack load error in 26.3, so Fabric is told to refuse
to start (and name it) if that jar is installed alongside this one.


-------------------------------------------------------------------------------
 1. PROJECT LAYOUT
-------------------------------------------------------------------------------

src/main/java/com/zanpro/enhancedcore/
  ZanProsEnhancedCore.java        Main entrypoint. Calls, in order:
                                    SurvivalDebugStickMod.initialize()
                                    MoveableSpawners.initialize()
                                    VerticalStairsAndSlabsMod.initialize()
  ZanProsEnhancedCoreClient.java  Client entrypoint. Registers the debug
                                  stick's client input handler.
  debugstick/                     Module D
  moveablespawners/               Module C
  verticalstairs/                 Module A
  removetooexpensive/             Module F (AnvilCostLimit, the cost rule)
  mixin/                          ALL mixins (one config file for all)
    instaminedeepslate/           Module B
    moveablespawners/             Module C
    peaceful/                     Module E
    removetooexpensive/           Module F
    verticalstairs/               Module A

src/main/resources/
  fabric.mod.json                 Mod metadata ("${version}" is filled in by
                                  Gradle from gradle.properties mod_version)
  zanpros_enhanced_core.mixins.json
                                  Mixin config. Every mixin class must be
                                  listed here as "<subpackage>.<ClassName>":
                                  "mixins" = both sides, "client" = client
                                  only (AnvilScreenMixin).
                                  Only mixin classes may live under the
                                  mixin package.
  assets/survivaldebugstick/      Module D client assets
  assets/moveablespawners/        Module C client assets
  assets/verticalstairsandslabs/  Module A client assets
  data/survivaldebugstick/        Module D recipes
  data/verticalstairsandslabs/    Module A recipes and loot tables
  data/minecraft/tags/block, item Module A tags (slabs, stairs, mineable...)

docs/VERTICAL_BLOCK_LIST.md       Every vertical block ID (Module A)
tests/                            Headless checks for Module A (not part of
                                  the normal Gradle build; see tests/README.md)


-------------------------------------------------------------------------------
 A. VERTICAL STAIRS AND SLABS
-------------------------------------------------------------------------------

CurseForge description:
  "Adds craftable vertical slabs and stairs."

What it does in game:
  - Adds 100 vertical slabs and 97 vertical stairs, one for every vanilla slab
    and stair material. That includes wood, stone types, bricks, sandstone,
    quartz, purpur, prismarine, blackstone, deepslate, tuff, cinnabar, sulfur,
    all cut copper/waxed variants, poplar, 16 wool colors and 16 concrete
    colors. smooth_stone, cut_sandstone and cut_red_sandstone have slabs only,
    matching vanilla.
  - Vertical slab: occupies the half of the block toward the player's
    horizontal facing. Placing a second matching slab into it makes a full
    "double" block. Single slabs can be waterlogged. Double slabs cannot and
    reject water.
  - Vertical stairs: have left/right orientation. They join normal vanilla
    stairs automatically (floor/ceiling joins, arch and U-frame corners), in
    either build order. Inner/outer corners BETWEEN TWO VERTICAL STAIRS only
    form for stairs placed as corner pieces (see Placement below).
  - Placement (since 1.2.0):
      * Normal placement: block state corners=false, meaning no corners
        with other vertical stairs (joins with regular stairs still form).
        It continues the line you're building by copying
        facing AND side from (first match wins, getLineNeighbor):
          1. a vertical stair directly below, else directly above (the
             column always wins);
          2. a vertical stair beside the new block in that stair's own row
             (new block to its left/right, not in front or behind;
             getRowNeighbor), checking the clicked side first, then N/E/S/W.
        This applies whatever surface was clicked; a stair clicked from its
        front or back counts as a wall. With nothing to continue: facing from
        the clicked face, side from which half was clicked.
        History: 1.2.0 copied only the side when clicking a wall; 1.2.1
        copied the clicked stair before the column, so clicking a
        back-to-back stair behind a column gap placed it backwards. Fixed in
        1.2.2 by checking the column first.
      * Corner placement: aim at the very corner of the clicked face (within
        3/16 of both edges that meet there, CORNER_AIM_ZONE). The stair uses the
        original free placement (facing from the clicked face, or the player's
        direction on floors/ceilings; side from which half was clicked, so the
        corner you pick decides the side) and gets corners=true: it also
        forms corners with other vertical stairs, recalculated when neighbors
        change.
      * corners defaults to TRUE, so blocks saved before 1.2.0 (which have no
        such property) and blocks set by commands keep the old automatic
        behavior. A debug stick can flip it on any block.
      * A corner-capable stair can still bend against a normally placed
        vertical stair (the normal one just doesn't change shape itself).
      * History: 1.2.0 made corners=false block joins with regular stairs
        too, which removed the automatic arch/U-frame corners. 1.2.3 limited
        corners=false to vertical-to-vertical corners again.
  - Solid faces hide water and use the partial shape for lighting, like vanilla
    stairs/slabs.
  - Crafting: 3 matching source blocks in a vertical column -> 6 vertical
    slabs. 6 source blocks in either of two mirrored stair patterns -> 4
    vertical stairs (files *_stairs.json and *_stairs_alternate.json).
  - All blocks appear in the Building Blocks creative tab.
  - Drops: single slab drops 1, double slab drops 2, stairs drop 1. Block
    properties (hardness, sound, tool) are copied from the source block.

Code (verticalstairs/):
  VerticalStairsAndSlabsMod  Namespace constant + initialize().
  ModBlocks                  List of material families (registerFamily calls
                             in the static block). Registers the block + block
                             item for each and adds them to the creative tab.
                             Copies properties with
                             BlockBehaviour.Properties.ofFullCopy(source).
                             Some sources are looked up by name with
                             vanillaBlock("name"). It throws at startup if
                             Minecraft removed or renamed that block.
  VerticalSlabBlock          States: facing, double, waterlogged.
  VerticalStairsBlock        States: facing, side (left/right), shape
                             (vanilla StairsShape), waterlogged, corners.
                             getStateForPlacement = the placement rules
                             above (isCornerAim, getLineNeighbor);
                             getStairShape: joins with regular stairs
                             first (always); vertical-to-vertical corners
                             only when corners=true.
  VerticalStairSide          The left/right enum.
  StairConnections           Math for mixed normal/vertical stair corners
                             (does not look at corners).
  mixin/verticalstairs/StairBlockMixin
                             Hooks vanilla StairBlock so NORMAL stairs can
                             connect to vertical stairs:
                               - getStairsShape (static) @RETURN: if vanilla
                                 says STRAIGHT, ask StairConnections for a
                                 mixed corner. Vanilla corners keep priority.
                               - updateShape @RETURN: also recalculates when the
                                 block above/below changes (vanilla only checks
                                 horizontal neighbors).

Resources per block (all under the verticalstairsandslabs namespace):
  assets/.../blockstates/<id>.json
  assets/.../items/<id>.json          (item model definition)
  assets/.../models/block/...         (1,380 models, including stair corners)
  assets/.../lang/en_us.json
  data/.../loot_table/blocks/<id>.json (26.3 loot table format)
  data/.../recipe/<id>.json (+ _alternate for stairs)
  data/minecraft/tags/block/*.json and data/minecraft/tags/item/*.json
  Textures are NOT bundled. Models point at vanilla textures.

Texture alignment (uvlock) - IMPORTANT:
  The block models use automatic UVs and the blockstates turn them with "y"
  rotations. Every rotated entry MUST have "uvlock": true (vanilla stairs and
  slabs do the same). Without it the texture turns with the model: the grain
  on top/bottom faces runs crosswise and side-face seams shift, so east/south/
  west-facing blocks don't line up with neighbouring blocks. Only north
  (unrotated) looked right. This was fixed in 1.1.2 by adding uvlock to all
  3,510 rotated entries in the 197 blockstate files. Quick check:
    any rotated entry without uvlock = bug.

To add a new material (e.g. a new vanilla wood type):
  1. Add registerFamily("name", Blocks.SOURCE, hasStairs) in ModBlocks.
  2. Add blockstate (copy an existing one so every rotated entry keeps
     "uvlock": true), item model definition, block models (copy an existing
     material's files and swap the texture/ID), loot table, recipe(s), lang
     entries.
  3. Add the IDs to the right tags (slabs/stairs, wooden_*, wool_*,
     concrete_*, mineable/<tool>, needs_*_tool).
  4. Update docs/VERTICAL_BLOCK_LIST.md and the block count checks in
     tests/BlockBehaviorChecks.java (currently 97 stairs / 100 slabs).

Update risk: HIGH. This is the biggest module. It overrides many Block methods
(getStateForPlacement, getShape, getCollisionShape, updateShape,
getFluidState, canPlaceLiquid, placeLiquid, rotate, mirror,
useShapeForLightOcclusion, isPathfindable, canBeReplaced) and has a
StairBlock mixin. Minecraft often changes those method signatures. Loot table,
recipe, blockstate and model formats also change between versions.


-------------------------------------------------------------------------------
 B. INSTAMINE DEEPSLATE
-------------------------------------------------------------------------------

CurseForge description:
  "Replaces deepslate mining speed with stone. Allows instamining with haste 2
  and efficiency 5."

What it does in game:
  Deepslate-family blocks break as fast as stone, so they can be instamined
  with Haste II + Efficiency V like stone can.

How it works (mixin/instaminedeepslate/DeepslateHardnessMixin):
  Injects at HEAD of BlockBehaviour.BlockStateBase#getDestroySpeed(
  BlockGetter, BlockPos) and returns 1.5F (stone's hardness) when the block's
  registry ID:
    - is in the "minecraft" namespace, AND
    - contains "deepslate", AND
    - does NOT contain "_ore", AND
    - is NOT "reinforced_deepslate".
  This covers deepslate, cobbled/polished/chiseled/cracked deepslate, deepslate
  bricks/tiles and their stairs, slabs and walls. Ores keep vanilla hardness.
  New vanilla deepslate blocks in future versions are included automatically
  because it matches by name.

Known interaction:
  The vertical deepslate blocks from Module A (verticalstairsandslabs
  namespace) are NOT sped up because of the "minecraft" namespace check. They
  copy vanilla deepslate hardness. To include them, also allow the
  "verticalstairsandslabs" namespace in the mixin.

Update risk: LOW. It only breaks if getDestroySpeed is renamed or its
parameters change.


-------------------------------------------------------------------------------
 C. MOVEABLE SPAWNERS
-------------------------------------------------------------------------------

CurseForge description:
  "Simple mod to allow the picking up of spawners with the silk touch
  enhancement using a diamond pickaxe or greater."

What it does in game:
  - Breaking a mob spawner in survival with a DIAMOND or NETHERITE pickaxe
    that has Silk Touch drops a spawner item for that mob, e.g. "Zombie
    Spawner" (light purple name, epic rarity, stacks to 64).
  - Placing that item places a vanilla spawner already set to that mob.
  - A spawner with no mob or an unreadable mob drops a plain vanilla spawner.
  - Silk Touch on any tool stops the spawner's XP drop, so you don't get XP and
    the spawner.
  - Pick-block (middle click) on a spawner gives the matching typed item.
  - Creative mode breaking does not drop anything extra.

Code (moveablespawners/):
  MoveableSpawners  Namespace constant + initialize(). Registers:
                      - PlayerBlockBreakEvents.AFTER: checks survival + tool +
                        silk touch, reads the spawner's mob and pops the item.
                      - PlayerPickItemEvents.BLOCK: typed item on pick-block.
                    Reads the mob from the spawner block entity's saved data:
                    SpawnData.entity.id (falls back to the legacy EntityId).
  ModItems          At startup, registers "<mob>_spawner" items for EVERY
                    vanilla entity type except MobCategory.MISC and the
                    player. New vanilla mobs get items automatically.
  TypedSpawnerItem  The item. Places Blocks.SPAWNER and calls
                    SpawnerBlockEntity.setEntityId(type, random). Display name
                    uses the lang key item.moveablespawners.typed_spawner
                    ("%s Spawner").
  mixin/moveablespawners/SpawnerExperienceMixin
                    HEAD of SpawnerBlock#spawnAfterBreak(BlockState,
                    ServerLevel, BlockPos, ItemStack, boolean). Cancels it (no
                    XP) when the tool has Silk Touch.

Resources:
  assets/moveablespawners/items/<mob>_spawner.json (131 files). All use the
  vanilla spawner block model. Some are for MISC entities (arrow, boat, etc.)
  that never get an item; those files are unused and harmless.

When Minecraft adds a new mob:
  The item registers automatically, but you must add
  assets/moveablespawners/items/<new_mob>_spawner.json (copy any existing file).
  Without it the item shows the missing-texture model. For example,
  sulfur_cube_spawner.json was added for the 26.2 Sulfur Cube.

Update risk: MEDIUM. Watch the spawner NBT layout (SpawnData/entity/id),
spawnAfterBreak's signature, SpawnerBlockEntity.setEntityId,
BlockEntity.saveCustomOnly, CompoundTag getters (currently return Optional),
and the two Fabric API events.


-------------------------------------------------------------------------------
 D. CRAFTABLE SURVIVAL DEBUG STICK
-------------------------------------------------------------------------------

CurseForge description:
  "Allows the crafting of a "Survival Debug Stick" as the regular debug stick
  is unusable when in survival mode."

What it does in game:
  - New item "Survival Debug Stick" (survivaldebugstick:debug_stick) with a
    custom purple texture and enchantment glint. Stacks to 1.
  - Recipe: 3 sticks in a diagonal line, either direction -> 1 debug stick.
  - While looking at a block:
      Left-click  = select the next block-state property (e.g. facing, half)
      Right-click = cycle the selected property's value
      Sneak + either = go backwards
    The result shows on the action bar. The selected property index is saved
    on the item.
  - It works like the vanilla debug stick but in survival. It does not break
    blocks and does not place/use blocks.
  - It is not in any creative tab. Get it by crafting or /give.

Code (debugstick/):
  SurvivalDebugStickMod      Namespace constant + initialize(). Registers the
                             data component and item, the network payload,
                             the server-side packet handler, and an
                             AttackBlockCallback that returns FAIL so the
                             stick never breaks blocks.
  SurvivalDebugStickClient   Client only. Every client tick, if the player is
                             holding the stick and looking at a block, it
                             detects a NEW press of the attack or use key and
                             sends DebugStickActionC2SPayload.
  DebugStickActionC2SPayload Packet (id survivaldebugstick:debug_stick_action):
                             block pos, updateValue (false = select property,
                             true = change value), reverse (sneaking).
  SurvivalDebugStickItem     useOn() returns SUCCESS so right-click does
                             nothing else. handleLeftClick/handleRightClick do
                             the work and message the player.
  ModItems / ModComponents   Item registration and the
                             survivaldebugstick:selected_property_index data
                             component (Integer, persistent).

Server-side safety checks: ignores spectators, ignores blocks more than 8
blocks away (distance squared > 64), and requires the stick in the main hand.

Behavior note: it applies changes with level.setBlockAndUpdate, which triggers
neighbor/shape updates. Some blocks (fences, walls, stairs, redstone) may
recalculate and "snap back". The vanilla debug stick avoids that. If
exact vanilla behavior is wanted, change it to setBlock with flags that skip
neighbor/shape updates.

Update risk: MEDIUM. Watch the Fabric networking API (PayloadTypeRegistry,
ClientPlayNetworking, ServerPlayNetworking), StreamCodec, key mapping names
(options.keyAttack/keyUse), the item model definition format
(assets/.../items/debug_stick.json) and the recipe JSON format.


-------------------------------------------------------------------------------
 E. REMOVE NAMETAGGED PEACEFUL DESPAWN
-------------------------------------------------------------------------------

CurseForge description:
  "Any hostile mob that is nametagged will no longer despawn when game is
  switched to peaceful mode."

What it does in game:
  - Switching to Peaceful no longer deletes hostile mobs that have a custom
    name (name tag). They also stay after saving/relogging in Peaceful.
  - Unnamed hostile mobs are still removed/blocked in Peaceful, like vanilla.

Why it is built this way:
  Minecraft refuses to even CREATE hostile entity types in Peaceful when
  loading them from disk. That happens before the saved name is read, so
  "check the name tag" alone is too late. The mod works in layers:
    1. PeacefulEntityTypeBypassMixin: EntityType#isAllowedInPeaceful() always
       returns true, so saved hostile mobs can load.
    2. PeacefulServerLevelEntityGateMixin: at HEAD of ServerLevel#addFreshEntity
       (required) and the addFreshEntityWithPassengers /
       tryAddFreshEntityWithPassengers variants (optional, require = 0,
       void and boolean versions), it rejects and discards any entity whose
       category is MobCategory.MONSTER and has no custom name while the
       difficulty is Peaceful. This restores the "no hostile spawning in
       Peaceful" rule.
    3. NamedHostileDespawnMixin (on Mob):
         - checkDespawn() HEAD: named Monster -> never despawn. Unnamed
           Monster in Peaceful -> discard.
         - tick() HEAD: unnamed Monster in Peaceful -> discard immediately.

Things to verify when testing / possible gap:
  Layer 2 checks MobCategory.MONSTER, but layer 3 checks
  "instanceof Monster" (the Java class). Some hostile mobs are MONSTER-category
  but don't extend Monster, possibly slimes, magma cubes, ghasts, phantoms,
  hoglins and shulkers depending on the version. For those, the "keep if
  named" rule in checkDespawn may not apply. Test a name-tagged slime and ghast
  in Peaceful. If they vanish, switch layer 3 to check
  getType().getCategory() == MobCategory.MONSTER as well.

Update risk: MEDIUM-HIGH. It depends on vanilla's Peaceful handling, which
Mojang reworks from time to time. Re-test Peaceful after every update: named
zombie survives a difficulty switch + relog; unnamed zombie is removed;
nothing new spawns at night.


-------------------------------------------------------------------------------
 F. REMOVE TOO EXPENSIVE
-------------------------------------------------------------------------------

CurseForge description:
  "Removes the "Too Expensive!" limit from the anvil. Anvil costs max out at
  50 levels, so you can keep repairing and enchanting your gear."

What it does in game:
  - The anvil never refuses a job with "Too Expensive!".
  - Anything that would cost more than 50 levels costs exactly 50. Costs from
    40 to 50, which vanilla refuses, are charged as-is. Costs under 40 are
    unchanged.
  - Rename-only jobs keep vanilla's own cap of 39 levels.
  - If the LEFT slot holds a stack (count > 1), vanilla rules apply unchanged.
    Vanilla uses the 40-level limit to stop you enchanting a whole stack at
    once; this keeps that protection.
  - You still need the levels: the cost shows red and the output can't be
    taken if you don't have enough.
  - Creative mode can always use the anvil, as in vanilla.
  - The item's prior-work penalty (minecraft:repair_cost component) still
    grows as in vanilla; it just can't push the price past 50.
  - Must be installed on the client as well as the server, or the client's
    screen shows "Too Expensive!" for costs 40-50 even though the job works.

Code:
  removetooexpensive/AnvilCostLimit   The rule, used by both mixins so the
                                      server and screen always agree.
                                      MAX_LEVEL_COST = 50 (change the cap
                                      here), VANILLA_LIMIT = 40.
                                      usesVanillaRules(left) = left count > 1.
                                      capCost(left, cost), tooExpensiveLimit(
                                      left) = 51 normally, 40 for stacks.
  mixin/removetooexpensive/AnvilMenuMixin   (both sides)
  mixin/removetooexpensive/AnvilScreenMixin (client only, "client" list)

How vanilla works (AnvilMenu#createResult(), Minecraft 26.3):
  It stores the level cost in the menu's "cost" DataSlot, and contains the
  int constant 40 exactly three times, in this order:
    ordinal 0: if (left.getCount() > 1) i = 40;
               Enchanting a stack: force cost to 40 so the limit blocks it.
    ordinal 1: if (cost >= 40) cost = 39;
               Rename-only jobs are never too expensive.
    ordinal 2: if (cost >= 40 && !creative) result = EMPTY;
               The actual "Too Expensive!" limit.
  The client's AnvilScreen#extractLabels has its own "cost >= 40 && !creative"
  check that draws the red "Too Expensive!" label. In 26.x this method is
  extractLabels(GuiGraphicsExtractor, int, int); before 26.x it was
  renderLabels(GuiGraphics, int, int).

What the mixins change:
  AnvilMenuMixin
    @ModifyArg on every DataSlot#set(int) call in createResult():
      each cost written goes through AnvilCostLimit.capCost -> max 50.
    @ModifyConstant(intValue = 40, ordinal = 2) in createResult():
      the limit becomes AnvilCostLimit.tooExpensiveLimit -> 51, which a
      capped cost can never reach.
    Ordinals 0 and 1 are NOT touched, and for stacks both handlers return the
    vanilla values, so all vanilla protections stay.
  AnvilScreenMixin
    @ModifyConstant(intValue = 40) in extractLabels: same tooExpensiveLimit,
    so costs 40-50 show "Enchantment Cost: N" instead of the red label.

Update risk: MEDIUM. Things to re-check on each Minecraft update:
  - createResult() still exists, the cost is still a DataSlot, and the three
    40s are still in the order above. A different order would NOT crash; the
    wrong 40 would silently be changed. Easy Anvils by Fuzs
    (github.com/Fuzss/easyanvils) keeps a copy of vanilla's createResult in
    VanillaAnvilMenu.java for each version, which is handy for checking.
  - The screen method is still named extractLabels.
  Then run test F in section 2.


-------------------------------------------------------------------------------
 2. CHECKLIST: UPDATING TO A NEW MINECRAFT VERSION
-------------------------------------------------------------------------------

1. Versions
   Gradle              Fabric Loom 1.17.21 needs Gradle 9.5+. With an older
                       installed Gradle, "gradle build" fails with "No matching
                       variant of net.fabricmc:fabric-loom". Use .\gradlew.bat
                       (wrapper, Gradle 9.6.0) or update the installed Gradle.
   gradle.properties   minecraft_version, loader_version, fabric_version
                       (get current values from fabricmc.net/develop)
   build.gradle        Fabric Loom plugin version
   fabric.mod.json     "minecraft": "~<new version>", "fabricloader": ">=..."
   gradle.properties   bump mod_version

2. Read the technical changelog (minecraft.net release article "Technical
   Changes" and misode.github.io/changelog) for changes to recipes, loot
   tables, item modifiers, blockstates, item model definitions, tags, spawner
   NBT, Peaceful rules and anvil cost rules.

3. Build. Compile errors usually mean a renamed/changed Minecraft or Fabric
   API method. Fix them module by module.

4. Launch the game. Mixins use "defaultRequire": 1, so if a target method was
   renamed the game crashes at startup and the log names the failing mixin.
   Mixin targets to re-check:

   instaminedeepslate.DeepslateHardnessMixin
       -> BlockBehaviour.BlockStateBase#getDestroySpeed(BlockGetter, BlockPos)
          returns float, @HEAD
   moveablespawners.SpawnerExperienceMixin
       -> SpawnerBlock#spawnAfterBreak(BlockState, ServerLevel, BlockPos,
          ItemStack, boolean), @HEAD
   peaceful.NamedHostileDespawnMixin
       -> Mob#checkDespawn() and Mob#tick(), @HEAD
   peaceful.PeacefulEntityTypeBypassMixin
       -> EntityType#isAllowedInPeaceful(), @HEAD
   peaceful.PeacefulServerLevelEntityGateMixin
       -> ServerLevel#addFreshEntity(Entity), @HEAD (required)
          + addFreshEntityWithPassengers / tryAddFreshEntityWithPassengers
          (optional, require = 0)
   removetooexpensive.AnvilMenuMixin
       -> AnvilMenu#createResult(): @ModifyArg on every DataSlot#set(int)
          call, and @ModifyConstant on the 3rd int 40 (ordinal = 2)
   removetooexpensive.AnvilScreenMixin (client)
       -> AnvilScreen#extractLabels: @ModifyConstant on int 40
   verticalstairs.StairBlockMixin
       -> StairBlock#getStairsShape (static; also @Shadow-ed) @RETURN
          and StairBlock#updateShape @RETURN

   Fabric API events used: ClientTickEvents.END_CLIENT_TICK,
   ClientPlayNetworking, ServerPlayNetworking, PayloadTypeRegistry,
   AttackBlockCallback, PlayerBlockBreakEvents.AFTER,
   PlayerPickItemEvents.BLOCK, CreativeModeTabEvents.modifyOutputEvent.

5. New vanilla content
   - New mob            -> add assets/moveablespawners/items/<mob>_spawner.json
   - New stair/slab set -> follow "To add a new material" in section A
   - New deepslate block -> nothing to do (matched by name)

6. In-game test pass (survival world):
   A  Place/combine vertical slabs; waterlog them; build a vertical + normal
      stair corner; break one and confirm drops. Place vertical stairs
      against a wall and on top of each other with ordinary aim, and a
      column's next block by clicking the wall beside it, the underside of
      the block above, and a stair behind the gap (back-to-back layer): they
      must stay straight and match the column. Regular stairs placed next to
      normally placed vertical stairs (arch/U-frame corners) must still join,
      in either build order. Two normally placed vertical stairs at right
      angles stay straight; place one aiming at the very corner of the
      clicked face and it forms the corner. Build a window frame
      (vertical stair columns, regular stairs top and bottom) facing EAST or
      WEST: grain and seams must run straight through the corners, same as
      when it faces north.
   B  Mine deepslate with Efficiency V + Haste II -> instant.
   C  Silk Touch diamond pickaxe on a zombie spawner -> Zombie Spawner item,
      no XP; place it -> zombie spawner.
   D  Craft the debug stick; left/right click a stair; sneak reverses.
   E  Name-tag a zombie, switch to Peaceful, relog -> zombie still there;
      unnamed zombies gone.
   F  /give @s minecraft:diamond_sword[minecraft:repair_cost=100] + any
      enchanted book in an anvil -> "Enchantment Cost: 50", no "Too
      Expensive!", takes exactly 50 levels. A repair_cost=40 sword + a
      Sharpness V book
      (/give @s minecraft:enchanted_book[minecraft:stored_enchantments=
      {"minecraft:sharpness":5}]) -> "Enchantment Cost: 45". Renaming the
      repair_cost=100 sword alone -> 39.
