Privacy

API for mods and packs

Make a mod, a resource pack or a modpack work with Punchy!'s first-person hands and animations: rule files, compat entries, moments, conditions, clips and the PunchyApi class.

On this page

Punchy! lets other mods, resource packs and modpacks work with its first-person hands and animations. Mods use the class punchy.api.PunchyApi; packs and mod jars can also write rule files and compat entries, with no code at all. The API is in every Punchy! jar, for every loader and Minecraft version. It names no Minecraft class, so you can compile against any Punchy! jar and the same code runs on all of them.

This is version 3 of the API (PunchyApi.VERSION). Version 1 came with Punchy! 3.1 and version 2 added hands left to Minecraft by state, both-hands items, hidden arms, hidden armor on the arms, flames and rule files. Version 3 adds the parts a mod needs to work with Punchy!‘s animations as well as its hands:

  • Say what happens. A mod fires a moment (examplemod:shoot), publishes a true or false state (examplespells:casting) or tells Punchy! facts about its items. Packs bind clips to those names.
  • Ask what Punchy! shows. Which hand Punchy! draws, which clip plays, which kind Punchy! gives an item, what a clip holds.
  • Hear Punchy!. Clips starting and finishing, moments, ticks, reloads, settings changes, markers inside clips.
  • Play and steer the clips packs ship. Start a clip by name, keep one playing while a state lasts, change its speed, veto it.
  • Change how Punchy! reads items and movement. An item’s kind, its role, its pose, when an equip counts as a change, how a swing is chosen, how the off arm shows.
  • Rule files and compat data for the same things, so a pack or a mod jar can do most of it without code.

Every version 1 and version 2 call keeps its meaning. Later versions may add to the API; they don’t take anything away.

Motion stays the packs’ job. The API never lets code stand in for an animation pack. A mod can fire a moment, and a pack decides which clip answers it; a mod can play a clip by name, and the pack’s clip is what moves; a mod can publish a number, and a pack’s clip variations read it. There is no call that draws an arm motion, shakes the camera or defines a clip from code. Pack creators keep making every animation in their animation software.

This page is for mod makers, pack creators and modpack makers, and it describes Punchy! 3.2. Players need none of it: the mods Punchy! supports by itself are listed at the end of this page and on Settings and controls.

Add it to your mod

Put a Punchy! jar in your project and compile against it without shipping it:

dependencies {
    compileOnly files("libs/punchy-fabric-1.21.1.jar")
}

Only call the API when Punchy! is installed (its mod id is punchy), from a class of its own, so your mod still runs without Punchy!:

// Fabric
if (FabricLoader.getInstance().isModLoaded("punchy")) {
    PunchyCompat.register();
}

// NeoForge and Forge
if (ModList.get().isLoaded("punchy")) {
    PunchyCompat.register();
}
final class PunchyCompat {
    static void register() {
        PunchyApi.useMinecraftHands("examplemod", () -> ExampleModClient.wearsGloves());
        if (PunchyApi.version() >= 3) {
            PunchyApi.onMoment("examplemod", "examplemod:", m -> ExampleModClient.heard(m.name()));
        }
    }
}

PunchyApi.VERSION is a constant, so the compiler copies it into your mod: it tells you which version you compiled against, not which one is installed. PunchyApi.version() tells you the installed one; Punchy! 3.1 has no such method, so a NoSuchMethodError from it means version 1. If you use a call from version 2 or 3, list the Punchy! release that has it in your mod’s dependencies, or guard the call: ask version() first, or wrap it in try { ... } catch (NoSuchMethodError e) { ... }.

Register on the client, from your client setup on. Where an argument is a Minecraft object (ItemStack, the player), it is passed as Object; cast it to the type your mod compiles against and never keep it after the call returns.

The rules every call shares

Ids. Every registration is kept under an id: <modid> (examplemod) or <modid>/<name> (examplemod/hud), so one mod can hold several registrations of one kind. Registering the same kind under the same id again replaces the earlier one (for the kinds that carry a name, such as conditions, the name is part of what is replaced). PunchyApi.remove(id) takes away the id and every <id>/... child, of every kind and every version of the API, and stops what the id is playing. Ids that start with punchy/ are Punchy!‘s own; a registration under one is refused with a line in the log.

Order. Answers are asked best priority first. PunchyApi.priority(id, n) sets the priority of an id for every kind it has or gets later (default 0; higher answers first). Rule files and rules from code sit at PunchyApi.PRIORITY_RULES (-50) and Punchy!‘s own punchy/... support at PunchyApi.PRIORITY_PUNCHY (-100), so a mod’s answer comes before a pack’s, and both before Punchy!‘s own support. Equal priorities go by registration order, with the calls of mods before rules from code and rule files. Where a question takes the first answer that is not null (an item’s kind, a movement state, the off arm), order decides; where any “no” wins (clip filters), it does not.

Errors. If a condition, look, filter, listener or driver of yours throws, Punchy! drops it and writes one line in the log; the game goes on. Registering the same thing again brings it back.

Threads and timing. Callbacks run on the client thread, inside Punchy!‘s tick or frame. Calls that start something (moment, playClip, stopClip, attack, use, inspect, trigger, equipCycle, react, itemRig, particles) may be made from any thread; Punchy! queues them and acts at the start of its next tick, and drops a request that waited more than a second. Nothing runs while Punchy! is off, paused or outside a world.

Cost. Questions Punchy! asks every frame (poses, kinds, conditions) are asked often, sometimes several times a frame, so keep them cheap: read a field, never walk an inventory. Punchy! times your callbacks. The default budget is 50 microseconds per call on frame paths and 200 on tick paths; a callback is slow when its average over two seconds is more than four times that. A slow one is named once in the log and in Copy debug info, and is never switched off. PunchyApi.budget(id, micros) tells Punchy! that your id needs more (up to four times the default). -Dpunchy.api.budget=off turns the timing off.

The player has the last word. On the Compatibility tab, “Mods using Punchy!‘s API” has a switch for each id that has registrations. A mod the player turns off is skipped by every kind, as Built-in Mod Support skips Punchy!‘s own punchy/... ids: its conditions read false, its vetoes and cancels are ignored, its looks give no answer, its modifiers and listeners are not called, and what it plays blends out. Rule files and compat entries come from packs and mod jars, and are turned off where packs are, in Minecraft’s resource packs.

Checking an integration. PunchyApi.registrations(idOrNull) returns one line for each live registration (examplemod onMoment priority 0 on 3.2us, with slow, off or dropped where it applies), then the rule files by resource id and what they hold. The same lines, the conditions your data refers to with their current values, the last moments and the last moment bindings are in Copy debug info (the Help tab of Punchy!‘s menu), in a “Punchy! API” section at its end. PunchyApi.debugInfo(id, supplier) adds lines of your own to it (at most 20 are kept; the supplier runs only when the info is asked for).

PunchyApi.priority("examplemod", 10);
PunchyApi.budget("examplemod/hud", 120);
PunchyApi.debugInfo("examplemod", () -> "examplemod: charge " + ExampleModClient.charge());
for (String line : PunchyApi.registrations("examplemod")) {
    LOGGER.info(line);
}

Which version and which features

int installed = PunchyApi.version();          // 3 for this page
boolean has = PunchyApi.supports("moments");  // false on a Punchy older than 3, or a Minecraft version without it

supports(name) answers whether this Punchy! has a feature and it works on this Minecraft version. Names are the section titles of this page in snake case. A name is never renamed or reused, and an unknown name answers false. Ask it only on a Punchy! whose version() is 3 or more.

NameWhat it covers
version_flagsversion() and supports
prioritypriority
diagnosticsregistrations, debugInfo, the creator inspector in Copy debug info
budgetbudget and the slow report
player_switchthe player’s switch for each mod
hand_ownerhandOwner
statestate()
clip_catalogclipExists, clipNames, clipInfo, sampleClip
item_answerskindOf, armKindOf, itemPoseOf, clipFor, momentBound
settings_readsetting
clip_eventsonClip
momentsonMoment, the cancelable input moments
markersclip markers as moments
tick_phasesonTick start and end
reload_eventsonReload
settings_eventsonSettingsChanged
play_clipplayClip, stopClip
drive_clipdriveClip, followProgress
fire_momentmoment
punchy_momentsattack, use, inspect, trigger, equipCycle, react
cameracameraStrength, camera_kick and camera_kicks
item_rigitemRig, clearItemRig
moment_particlesmoments as on_animation of a particle emitter
third_person_clipplayThirdPersonClip
arm_texturearmTexture, the arm_texture rule
arm_overlaysarmOverlays, the arm_overlays rule
arm_flamesarmFlames, Flames.scale, the arm_flames rule
draw_on_armdrawOnArm
draw_itemdrawItem
hand_pointshandPointInWorld, handPointOnScreen
movement_scalemovementScale
pose_variationsposes in compat entries
item_displayitemDisplay, the display_context rule
keep_use_visualskeepItemUseVisuals, the keep_use_visuals rule
setting_restrictionsrestrictSetting
menu_rowsmenuRows
editor_targetseditorTarget
pack_presetspunchy/presets
item_poseitemPose
movement_statesmovementState
off_armoffArmVisibility
item_kinditemKind and the kind rule
item_rolesitemRole and the item_roles rule
equip_rulesequipRules and the equip rule
attack_rulesattackRules and the attack rule
clip_filtersfilterClips and the filter_clips rule
clip_speedclipSpeed
hide_layershideLayers, the LAYER_* bits and the hide_layers rule
per_arm_bridgeuseMinecraftHandsFor and the arm key of use_minecraft_hands
hide_handshideHands and the hide_hands_while_using rule
arm_statesarmStates
hot_itemshotItems and the hot_items rule
arm_posearmPose
external_arm_poseexternalArmPose
arm_claimsclaimArm
matricesarmMatrix, itemMatrix, cameraAnimation
frame_phasesonFrame
particle_scriptsparticles
compat_from_codecompatData
selector_groupsa match object on an entry of a compat file’s item map
stack_selectorscomponents, nbt, name, custom_model_data, enchantments, damaged and durability_below in selectors
played_momentsthe moments Punchy!‘s machines make as they play a clip (critical_hit, bow_shot …)
spear_stagesthe spear_stage moment; false before Minecraft 1.21.11, which has no spears
equip_dataignore_components, ignore_nbt, changed_by_components and changed_by_nbt in equip rules
item_rig_typesthe on:<moment> and while:<condition> types of an item’s modelPartsAnimation
clip_remapsremapClips and the clip_remaps rule
clip_soundssoundRoute, sound events and options in sound_effects, sound on bindings
clip_file_keysspeed, blend_in, blend_in_easing and aliases in animation files, speed on entries
clip_namespacesanimation files under assets/<any namespace>/punchy/animations/
held_item_lookheldItemLook, NO_STACK and the item_display rule
held_keyshold and a translated title on triggerable entries
mix_labelsmixLabel and mixLabelKey in compat files
conditionscondition, setCondition, conditionValue, the inline forms
item_conditionsitemCondition, itemValue
variablesvariable, setVariable, variableValue and the variables rule kind
queriesq.<name> in expressions and expr conditions
expressionsthe new names and functions in keyframes, expr conditions
rules_from_coderules
pack_optionspackOption, packFlag, packNumber, modOptions
rule_idsa rule’s id and take_away
gated_ruleswhen, held, worn and requires_mods on rule entries
entry_conditionswhen, unless and requires_mods on compat entries
pack_conditionsconditions and moments that packs define
moment_bindingson on triggerable compat entries
pack_noticesrecommendedSettings, requiresPunchyApi, requiresMods, conflictsWithMods and packNotice in compat files

Show your hand on Punchy!‘s arms (versions 1 and 2)

PunchyApi.useMinecraftHands(String modId, BooleanSupplier when)

While when is true, Punchy! draws each first-person arm through Minecraft’s own hand code, placed at the pose of Punchy!‘s animations. Whatever your mod draws for Minecraft’s first-person hand (gloves, a sleeve, a custom model’s hand) then shows on Punchy!‘s animated arm and follows its animations, with no other code for Punchy!. Punchy!‘s held items, its overlays (lava, frost, mud, sweat, flames) and its animations stay as they are.

This works with the usual ways of drawing on the first-person hand: the RenderArmEvent on NeoForge and Forge, and hooks on the player renderer’s hand drawing on Fabric.

Turn Punchy! off for a while

PunchyApi.disable(String modId, BooleanSupplier when)

While when is true, Punchy! steps aside as if its Enable Punchy! setting were off: Minecraft draws and moves the hands, and Punchy! plays no animations.

Leave some items to Minecraft

PunchyApi.leaveItemsToMinecraft(String modId, Predicate<Object> items)

A hand that holds an item for which items is true is drawn and moved by Minecraft, as for an item on Punchy!‘s item blacklist. The argument is the held ItemStack. Since version 2 this reaches every place Punchy! treats an item on its blacklist: the hand and the item drawn, Punchy!‘s animations, the short hand-out when the item changes, and the item’s model.

Leave both hands to Minecraft for an item (version 2)

PunchyApi.leaveBothHandsToMinecraft(String modId, Predicate<Object> items)

While either hand holds a stack for which items is true, both hands are left to Minecraft. Use it for an item that draws both hands itself, such as a two-handed gun whose model has both arms. The argument is the held ItemStack.

Leave hands to Minecraft while something happens (version 2)

PunchyApi.leaveHandsToMinecraft(String modId, PunchyApi.HandCondition when)

@FunctionalInterface
public interface HandCondition {
    boolean leave(Object player, boolean mainHand);
}

While when returns true for a hand, that hand is left to Minecraft whatever it holds, the empty hand included. Use it when your mod draws the first-person hands itself for a while: an animation of its own, a carried block, a camera. player is the player Punchy! asks about; Punchy! asks only about the local player (its first-person hands are that player’s). mainHand says which hand; answer the same for both to leave both.

PunchyApi.leaveHandsToMinecraft("examplemod", (player, mainHand) -> ExampleModClient.isCasting());

Hide Punchy!‘s arms (version 2)

PunchyApi.hideArms(String modId, BooleanSupplier when)

While when is true, Punchy! draws no arms: no skin, no sleeves, no armor and no arm overlays. Held items still show, and Punchy!‘s animations still move them.

Hide the armor on Punchy!‘s arms (version 2)

PunchyApi.hideArmorOnArms(String modId, BooleanSupplier when)

While when is true, Punchy! draws no armor on its arms. Use it when your mod draws the sleeve itself (for example through useMinecraftHands).

Change the flames on the arms (version 2)

PunchyApi.flames(String modId, PunchyApi.FlameLook look)

@FunctionalInterface
public interface FlameLook {
    PunchyApi.Flames look(Object player);   // null: no opinion
}

PunchyApi.Flames.of(String sprite0, String sprite1)   // two sprites of the block atlas
flames.tint(int argb)                                 // a copy with a color multiplier; 0xFFFFFFFF is none

While the player burns, Punchy! draws fire on its arms. It asks the looks best priority first, and the first one that returns flames sets the two fire sprites it draws, with an optional color. The sprites are ids of the block atlas, the one Minecraft’s fire uses, such as minecraft:block/soul_fire_0 and minecraft:block/soul_fire_1. A sprite the atlas doesn’t have falls back to Punchy!‘s own flames. With no answer, Punchy! draws Minecraft’s fire.

PunchyApi.flames("examplemod", player -> ExampleModClient.burnsBlue(player)
        ? PunchyApi.Flames.of("minecraft:block/soul_fire_0", "minecraft:block/soul_fire_1")
        : null);

Hide single layers of an arm (version 3)

PunchyApi.hideLayers("examplemod", (player, leftArm) ->
        leftArm && ExampleModClient.wearsGauntlet() ? PunchyApi.LAYER_ARMOR | PunchyApi.LAYER_FLAMES : 0);

hideArms and hideArmorOnArms are all or nothing. hideLayers hides single layers, so a mod can take over one thing and leave the rest: a glove over the sleeve, armor of its own, a held item it draws itself. It is asked every frame for each arm Punchy! draws, so keep it cheap; the bits of every registration and rule are ORed. The layers:

BitHidesIn this version
LAYER_ARMORthe armor on the armacts
LAYER_BURNthe burn overlay of a hand in lava or fireacts
LAYER_FROSTthe freeze overlayacts
LAYER_MUDthe mud overlayacts
LAYER_SWEATthe sweat overlay (Punchy! 3.0 never draws it)accepted, nothing to hide
LAYER_FLAMESthe fire on the armacts
LAYER_SKINthe skin; the sleeve, overlays and armor still drawacts
LAYER_SLEEVEthe sleeve; the arm still drawsacts
LAYER_ITEMthe item that arm holds; Punchy! still animates it, its particles stopacts
LAYER_OVERLAYSthe layers of armOverlaysacts
LAYER_ARMeverything hideArms hides (the held item is not part of it)acts on the layers above

While an arm is drawn through Minecraft’s hand code its skin and sleeve are Minecraft’s. A rule file does the same:

{"requires_api": 3, "hide_layers": [{"layers": ["armor"], "arm": "left", "when": "examplemod:wearing_gauntlet"},
                                    {"layers": ["flames", "burn"], "held": {"items": ["examplemod:frost_blade"]}}]}

layers takes the names skin, sleeve, armor, burn, frost, mud, sweat, flames, item, overlays and arm; arm takes left, right, main (the arm of the main hand), off or both (the default); the gates apply (the held gate takes the stack keys of a selector too, such as durability_below).

One arm through Minecraft’s hand code (version 3)

PunchyApi.useMinecraftHandsFor("examplemod", (player, leftArm) -> !leftArm && ExampleModClient.hasRightHandModel());

Like useMinecraftHands, for one arm: while the condition is true for an arm, Punchy! draws that arm through Minecraft’s hand code and the other as before. A rule file says it with the arm key of use_minecraft_hands: {"use_minecraft_hands": [{"arm": "right", "when": "examplemod:right_glove"}]}. An entry without arm, or with "arm": "both", is the version 2 kind and means both arms; the other kinds that hide or switch Punchy! (hide_arms, hide_armor_on_arms, disable_punchy) have no per-arm meaning and skip an entry that names one arm.

No hands for a while (version 3)

PunchyApi.hideHands("examplemod", () -> ExampleModClient.isInCutscene());

While the supplier is true no first-person arms or held items are drawn at all, as while a spyglass is in use; Punchy!‘s animations keep running. A hand left to Minecraft is Minecraft’s and still draws. handOwner answers hidden for both hands. A rule file can hide the hands while an item is in use:

{"requires_api": 3, "hide_hands_while_using": [{"items": ["examplemod:scope"], "when": "mypack:scoped_view"}]}

items, tags and classes name the stack in use (a bow being drawn, a spyglass), and the gates apply.

Burn, frost and mud from your mod’s state (version 3)

PunchyApi.armStates("examplemod", (player, leftArm, out) -> {
    ExampleModClient.Heat heat = ExampleModClient.heatOf(leftArm);        // the mod's own state for this arm
    out.burn(heat.fire())                                                  // 0 to 1: the lava hand overlay of this arm
       .mud(heat.mud() > 0.5F ? 3 : heat.mud() > 0.2F ? 2 : 0)             // stage 0 to 3: mud on this arm's fingertips
       .frost(ExampleModClient.cold());                                    // 0 to 1: frost tips and Freeze Shake
});
PunchyApi.hotItems("examplemod", stack -> ExampleModClient.isHotMetal(stack));

Punchy! draws each of its own arm overlays at the highest of its own value and every answer, so a mod only adds: it never takes away what Punchy! shows for the lava bucket or for a frozen player. The look is asked every frame for each arm Punchy! draws (keep it cheap; the answer is kept for a millisecond, so a burst of questions runs it once) and fills out with what it wants; the highest value of every look counts. frost is for the player, not an arm: the highest answer of either arm is used, for the frost tips on the fingertips (the stage is chosen from the amount, a third at a time, like Punchy!‘s own) and for Freeze Shake. The player’s switches still decide: Burn, Freeze Arm Overlay, Mud and Freeze Shake in Punchy!‘s settings turn the overlay off whatever a mod says. Sweat is not offered, since Punchy! 3.0 never draws it.

hotItems names the held stacks that warm the hand like a lava bucket: the burn overlay of the arm that holds one fades in over 30 ticks and out over 60, as it does for Punchy!‘s own hot items (the lava bucket, magma block and magma cream, blaze rod and powder, fire charge and campfires). A rule file says it with a selector, the same as the other rule kinds that name items:

{"requires_api": 3, "hot_items": [{"tags": ["examplemod:hot_metals"]}, {"items": ["examplemod:slag"], "when": "examplemod:forging"}]}

Stop some of Punchy!‘s animations

PunchyApi.filterAnimations(String modId, PunchyApi.AnimationFilter filter)

Punchy! asks the filter before it starts one of its animation clips, with the clip’s name and whether it plays on the left arm; when the filter returns false, the clip doesn’t play. Clip names are the names in Punchy!‘s animation files, such as attack_1, attack_2 or use_bow_right. filterClips (below) is the version 3 form of the same thing and gives more.

Take your registrations away, ask whether Punchy! is on

PunchyApi.remove(String modId)
boolean on = PunchyApi.isActive();

remove takes away the id, its <id>/... children, everything they registered and what they play. isActive() is true while Punchy!‘s Enable Punchy! setting is on and no mod has turned it off.

What an arm and an item are drawn with

Each of these is asked every frame, so keep it cheap. None of them changes a hand left to Minecraft. The Minecraft objects they hand you come as Object: cast them to your version’s types.

Another texture, tint or light: armTexture

PunchyApi.armTexture("examplemod", (player, leftArm) -> ExampleModClient.wearsSuit(player)
        ? PunchyApi.ArmTexture.of("examplemod:textures/arms/suit.png").fullBright(true)
        : null);

The first answer by priority wins; null is no opinion. The texture is read as a skin of that arm’s model, in the 64x64 skin layout, arm and sleeve: to draw only a sleeve, leave the arm’s region clear. ArmTexture.skin() keeps the player’s own skin, for a tint or a light alone. tint(argb) multiplies color and alpha, so alpha below 255 makes the arm translucent; fullBright(true) draws it at full light whatever the light where the player stands. Punchy!‘s overlays, flames and armor draw over it. hideArms and LAYER_SKIN win. An arm drawn through Minecraft’s hand code keeps Minecraft’s skin. An id that does not parse counts as no look; one that parses but has no texture behind it shows Minecraft’s missing texture, as a missing armor texture does. 3D Skin Layers’ voxel sleeve is not built while a look replaces the skin.

{"requires_api": 3, "arm_texture": {"texture": "examplemod:textures/arms/suit.png", "full_bright": true, "arm": "main", "worn": {"slot": "chest", "items": ["examplemod:suit"]}}}

Extra layers: armOverlays

PunchyApi.armOverlays("examplemod", (player, leftArm, out) -> {
    if (ExampleModClient.isCasting()) {
        out.add("examplemod:textures/arms/runes.png", 0xCC66CCFF, true);
    }
});

Layers go over the skin (or armTexture) and the sleeve, under Punchy!‘s burn, frost, mud and sweat overlays, its flames and its armor: tattoos, war paint, glowing veins, bandages. Every answer adds; a higher priority’s layers are drawn last, on top. At most eight per arm per frame; those of the lowest priorities are left out, with one line in the log. Each is in the 64x64 skin layout, with its own color and alpha, at the arm’s light or at full light. They draw on an arm drawn through Minecraft’s hand code too. LAYER_OVERLAYS and hideArms hide them; LAYER_SKIN does not.

{"requires_api": 3, "arm_overlays": [{"texture": "examplemod:textures/arms/runes.png", "tint": "#CC66CCFF", "full_bright": true, "arm": "main",
                                      "when": "examplemod:casting", "fade_in": 0.3, "fade_out": 1.0}]}

In a rule, fade_in and fade_out ease the layer in and out over that many seconds as its gates change, and alpha (a number or an expression, read each frame, 0 to 1) scales it.

Flames without fire: armFlames

PunchyApi.armFlames("examplemod", (player, leftArm) -> ExampleModClient.isFireStance()
        ? PunchyApi.Flames.of("minecraft:block/soul_fire_0", "minecraft:block/soul_fire_1").scale(1.5F)
        : null);

Flames burn on that arm while a look answers and the player is not on fire. While the player really burns flames decides and this adds nothing, so there are never two sets. The first answer by priority wins. The Enhanced Fire Arm Effects switch and LAYER_FLAMES win. Flames.scale(factor) draws them bigger or smaller, for flames too. The rule is arm_flames, with sprites, tint, scale, arm and gates.

Draw on the arm: drawOnArm

PunchyApi.drawOnArm("examplemod", arm -> {
    PoseStack pose = (PoseStack) arm.poseStack();
    ModelPart part = (ModelPart) arm.armPart();
    // From Minecraft 1.21.11: a SubmitNodeCollector, up to 1.21.5: a MultiBufferSource
    SubmitNodeCollector sink = (SubmitNodeCollector) arm.sink();
    sink.submitModelPart(part, pose, RenderTypes.entityTranslucent(GLOVE), arm.light(), arm.overlay(), null);
});

An arm event for every loader, Fabric included. It runs once per frame for each arm Punchy! draws, after its arm layers and before its flames and armor, in priority order. Drawing the arm part with this pose stack, or calling the arm part’s translateAndRotate on it and drawing in the arm’s own space, lands on Punchy!‘s animated arm, as code written for Minecraft’s arm event does with Minecraft’s model. The object also tells player(), leftArm(), partialTick(), slim(), playerModel(), sleevePart() (the sleeve up to 1.21.1; null from 1.21.5, where it is the arm part’s child) and throughMinecraft(). The object is reused: read it during the call only. From 1.21.11 drawing is deferred, so do not change shared model parts after submitting them. Not called for hands left to Minecraft, where Minecraft’s own arm event runs.

Draw a held item yourself: drawItem

PunchyApi.drawItem("examplemod", item -> {
    if (!(item.stack() instanceof ItemStack stack) || !stack.is(ExampleItems.STAFF)) {
        return false;
    }
    ExampleModClient.drawStaff(stack, (PoseStack) item.poseStack(), item.sink(), item.light(), item.overlay());
    return true;
});

Before Punchy! draws a held item, after its placement, grip and physics, handlers are asked in priority order; the first that answers true takes over that draw. Drawing something extra and answering false decorates the item. displayContext() names the context Punchy! would draw with, such as thirdperson_righthand; matrix(dest) is the item’s root as itemMatrix answers it. Items Punchy! draws with its own boat, minecart and chest models come earlier and are not offered. Answering true skips the rest of Punchy!‘s draw of that item, its particles and a rod’s fishing line with it; hand points on the item still answer. LAYER_ITEM skips the whole item draw, this included.

Points on the hand: handPointInWorld, handPointOnScreen

double[] world = new double[3];
if (PunchyApi.handPointInWorld(false, false, 0F, -0.625F, 0F, world)) {
    ExampleModClient.startBeamAt(world[0], world[1], world[2]);
}
float[] screen = new float[2];
if (PunchyApi.handPointOnScreen(false, true, 0F, 0F, 0F, screen)) {
    ExampleModClient.placeGauge(screen[0], screen[1]);
}

The first argument is the left arm, the second whether the point is on the item the arm holds. The point is in blocks in the space of armMatrix or itemMatrix. The world answer takes the hand pass’s own projection into account, so it is where the point appears in the world in first person, as Punchy! does for its fishing line and its particles. The screen answer is in GUI pixels from the top left. False when that arm or item was not drawn in the last pass, and on the screen also when the point is behind the camera. Shader packs can change the hand’s projection. Call them on the client thread.

Ask Punchy!

Who draws the hand

String owner = PunchyApi.handOwner(true);   // true: main hand, false: off hand

"punchy" (Punchy! draws that hand), "minecraft" (the hand is left to Minecraft: an item on the blacklist, a rule, the leave calls, Vanilla Empty Hand), "hidden" (Punchy! draws no arm for that hand: a mod or a rule hides the arms with hideArms or hide_arms, the off arm is hidden by offArmVisibility, or a spyglass is in use; the held item may still show) or "off" (Punchy! is off, or the camera is not in first person). A mod with a first-person animation of its own switches it off only while the answer is "punchy". The answer can change from frame to frame around equip windows.

What Punchy! showed at the end of its last tick

PunchyApi.State state = PunchyApi.state();   // an immutable copy, readable from any thread
state.tick();                    // Punchy ticks seen so far
state.active();                  // whether Punchy was on at its last tick. Off, it has no ticks and this copy stays as it was: ask isActive() for now
state.firstPerson();
state.leftHanded();
state.clip("action");            // the clip on a layer, "action" or "overlay"; null when none plays
state.clipOn(false);             // the clip that owns the arm on the right of the screen
state.equipPhase(true);          // "idle", "out" (hand-out) or "in" (hand-in) for the main hand
state.kind(true);                // Punchy's kind of the stack drawn in the main hand, such as "SWORD"
state.is("crouching");           // any condition (see Conditions)

A PunchyApi.Clip has name(), layer(), time(), length(), looping(), holding(), arms() (bits of LEFT_ARM and RIGHT_ARM), playback() (one number per playback, to tell a restart from a loop), ticket() (the playClip ticket for a clip a mod started, 0 for Punchy!‘s own), reason() (who asked, see onClip), token(name) (whether a timeline or whole-clip token is active now; a token stays on from its time to the clip’s end) and tokenTime(name) (the last time at or before now that the token’s frame was passed, or -1).

Only the action layer (attacks, mining, use, equip, variations, your own clips) and the swim overlay report. Layers that only sample clips (walking, riding) and the held item’s own model clips do not.

The clip catalog

boolean has = PunchyApi.clipExists("rifle_shoot");        // names are case sensitive; ask after onReload("loaded")
List<String> all = PunchyApi.clipNames();
PunchyApi.ClipInfo info = PunchyApi.clipInfo("rifle_reload");   // null when missing
float[] out = new float[9];
boolean ok = PunchyApi.sampleClip("rifle_reload", "right_arm", 0.4F, out);

clipInfo returns the clip the player’s Mix Packs choice resolves to: name(), length(), loop() ("once", "loop" or "hold"), bones(), tokens(), timeline() and sounds() (time to tokens or sounds), and options() (the Mix Packs option ids that have the clip). sampleClip samples one bone with Punchy!‘s own sampler, so the numbers match playback: position x, y, z in pixels, rotation x, y, z in degrees, then scale x, y, z. A gun mod can time its reload to the pack’s clip or read a pose from it.

Item answers

String kind = PunchyApi.kindOf(stack);              // "SWORD"; mods' and rules' answers included; null for no stack
String armKind = PunchyApi.armKindOf(stack);        // the kind Punchy poses the arm by
float[] pose = PunchyApi.itemPoseOf(stack, false);  // scale, offset x y z, rotation x y z, pivot x y z; null when none
String clip = PunchyApi.clipFor(stack, true, "attack");   // the clip Punchy would play now; null when none
boolean bound = PunchyApi.momentBound("examplemod:shoot");

itemPoseOf returns the pose Punchy! resolved (packs, the Hand Editor, Mix Packs, then mods’ itemPose modifiers) in the units of compat JSON; leftArm is the arm on the screen’s left. clipFor takes an entry type such as "attack", "use", "idle" or "triggerable", applies variations and Mix Packs, and walks the entry list, so don’t call it every frame. momentBound tells whether a pack binds this moment to a clip or a camera kick for the items held now: a mod that animates the moment itself can skip its own first-person effect when the pack does it.

The player’s settings

Object v = PunchyApi.setting("general.camera_animations");   // Boolean or Float, as the menu shows it; null if unknown

The ids are the rows of Punchy!‘s menu, <tab>.<name>: general.camera_animations, general.show_both_hands, effects.particles, compat.builtin_mod_support and so on. Use onSettingsChanged to hear a change.

Hear Punchy!

Clips

PunchyApi.onClip("examplemod", e -> {
    if (e.phase().equals("finish") && e.clip().startsWith("rifle_reload")) {
        ExampleModClient.reloadAnimationDone();
    }
});

onClip delivers, at the end of Punchy!‘s tick and in order, every clip that starts, loops, holds, finishes or stops on the action layer or the swim overlay. A ClipEvent has phase() ("start", "loop", "hold", "finish" when the clip ended by itself, "stop" when it was replaced or stopped), clip(), layer(), arms(), playback(), ticket(), time(), length(), next() (for a stop: the clip that replaced it) and reason(): who asked for the clip, one of "attack", "mining", "use", "two_handed_use", "equip", "variation", "safety", "api:<id>", "moment:<name>" or "unknown". The event object is reused: copy what you keep. A HUD can show a cast or reload bar; a stamina mod can drain on each attack.

Moments

PunchyApi.onMoment("examplemod", "", m -> {                // "" hears every moment
    if (m.name().equals("attack") && ExampleModClient.exhausted()) {
        m.cancel();                                         // Punchy plays no attack clip for this swing
    }
});
PunchyApi.onMoment("examplemod/guns", "examplegun:", m -> ExampleGun.heard(m.name(), m.value()));   // one namespace

A moment is something that happened: Punchy!‘s own (attack, use, hurt, land), the ones mods fire with PunchyApi.moment, the ones packs define in rule files, and clip markers, all in one stream. The prefix is empty for all of them, a namespace ending in a colon ("examplegun:") for one namespace, or one full name. Punchy!‘s own moments have bare names (see The moments Punchy! has); the others carry a namespace.

A Moment has name(), hand() (NO_HAND, MAIN_HAND or OFF_HAND), value(), detail(), stack() (the hand’s stack, possibly empty, when the moment has a hand), clip() (for markers), source() ("punchy", "mod", "pack" or "marker") and cancelable(), cancel(), canceled().

Input moments (attack, use, mine_start, mine_stop, inspect, trigger) are delivered the moment Punchy! records them, before its machines read them, and can be canceled: Punchy! plays nothing for that input. Minecraft and the server are untouched; the swing still happens, only Punchy!‘s animation is skipped. Everything else is delivered at the start of the next Punchy! tick, in order; markers reach listeners at the end of the tick that found them. At most 32 moments wait for delivery; older ones are dropped, with one log line a minute.

Ticks, reloads, settings

PunchyApi.onTick("examplemod", "start", () -> ExampleModClient.syncWithPunchy());   // "start" or "end"
PunchyApi.onReload("examplemod", phase -> {
    if (phase.equals("loaded")) {
        recoil = (float) PunchyApi.packNumber("examplemod:recoilScale", 1.0);
    }
});
PunchyApi.onSettingsChanged("examplemod", () -> cameraOn = (Boolean) PunchyApi.setting("general.camera_animations"));
  • onTick(id, "start", r) runs at the start of every Punchy! tick, before any of its machines; "end" runs after the last step. A mod keeps its own state in step with Punchy!‘s. Any other phase name throws IllegalArgumentException.
  • onReload hears "loaded" after every successful load of Punchy!‘s data (clips, compat entries with Mix Packs applied, rule files), "settled" once per resource reload twenty ticks later, and "world_reset" once for each level change and when the player leaves a world. Listeners must be idempotent: they run two or three times per reload.
  • onSettingsChanged runs after a change of Punchy!‘s settings is committed (a menu change, a profile load, Reset all).

Tell Punchy!

Fire a moment

PunchyApi.moment("examplegun:shoot");                  // no hand
PunchyApi.moment("examplegun:shoot", true);            // main hand
PunchyApi.moment("examplegun:shoot", true, 0.8);       // main hand, with a number packs can filter on

Tells Punchy! that something happened: a cast, a reload, a shot, a dash. It is queued (any thread) and delivered at the next tick start to moment listeners and to the packs’ data: moment bindings, camera kicks, moments packs define, particle pulses. With nothing bound nothing happens, so firing is always safe, and cheap. The name must carry a namespace (examplegun:shoot); a bare name belongs to Punchy! and throws IllegalArgumentException. The value is a number packs can filter on with min_value and max_value, and use to scale a camera kick.

Make Punchy! record an input

PunchyApi.attack(true, false);             // as if the player swung the main hand (critical or not)
PunchyApi.use(false);                      // as if the use key was pressed with the off hand
PunchyApi.inspect();                       // as if the inspect key was pressed
PunchyApi.trigger("examplemod_special", true);   // fires a pack's triggerable entry by its id
PunchyApi.equipCycle(true, false);         // Punchy's hand-out and hand-in for the main hand
PunchyApi.react(false, "shield_block");    // Punchy's own procedural reaction on an arm

Use these for an input Minecraft never sees: a custom key, a packet, an ability. Punchy!‘s own detection still runs, so calling attack for a swing Minecraft also sees plays it twice. These inputs become moments of Punchy!‘s own, so listeners hear them (source() is "mod") and a cancel works on them as on the real ones.

  • trigger(id, mainHand) fires a triggerable compat entry that has an id; an entry with an id and no key can only be fired this way.
  • equipCycle(mainHand, immediate) plays Punchy!‘s hand-out and hand-in now, for a weapon-mode mod that swaps an item’s mode.
  • react(leftArm, kind) is "action", "shield_block" or "spear_impact"; anything else throws IllegalArgumentException.

Play and steer clips

Clips are the animations the packs ship, found by name (see the clip catalog). The mod chooses when; the pack’s clip is what moves.

Play a clip

long ticket = PunchyApi.playClip("examplemod", PunchyApi.ClipRequest.named("dodge_roll")
        .hand(true).speed(1.2F).priority(PunchyApi.CLIP_ACTION).protect(true));
// ... later
PunchyApi.stopClip("examplemod", ticket);

playClip is queued (any thread). At the next tick start the request joins Punchy!‘s own arbitration: the highest priority of the tick runs and, on a tie, Punchy!‘s own request wins. A use of Punchy!‘s own that is already playing (a bow draw, a crossbow or spear charge, eating, blocking) holds the layer the same way, because Punchy! asks for those clips once and then lets them run: your request is turned away, with a "stop" event for its ticket, unless it asks for more than that use (two-handed uses count as 90, other uses as 80). It returns a ticket, or 0 when it was refused at once: Punchy! is off, the player is outside a world, or the clip does not exist. A request that is turned away at the tick start (the hand is left to Minecraft, or the player turned your mod off) ends with a "stop" clip event for its ticket. The ticket shows up in onClip events and in PunchyApi.Clip.ticket(). stopClip blends the clip out if it still plays.

A ClipRequest is built with chained calls and is not thread safe (build one per request):

CallMeaning
named(clip)a clip name of the packs’ animation files
hand(mainHand)the hand it is for (default main); picks <clip>_right or <clip>_left of the player’s arm
bothArms()drives both arms, as a dual-handed clip
speed(f)a multiplier, default 1
blendIn(s), blendOut(s)seconds; left to Punchy! when not set
loop(), holdLastFrame()loop it, or hold the last frame
startAt(s)seconds into the clip
priority(n)one of CLIP_VARIATION (10), CLIP_MINING (60), CLIP_ACTION (70, the default), CLIP_USE (80), CLIP_TWO_HANDED (90); more than 90 counts as 90
itemRig(type)also plays the held item’s own model clip of that type
kick(on)Punchy!‘s procedural action kick at the start (default on)
protect(on)vetoes the starts of clips of equal or lower priority on its arms while it plays (a tie goes to the protected clip); your own later clips are not held back, end a protected clip with stopClip
followProgress(p)keeps the clip’s time at this fraction (0 to 1) of its length; for driveClip

Use it for dodge, dash and roll mods, heavy swings on a mod’s own timing, and spell casts whose clips a pack ships.

Keep a clip playing while a state lasts

PunchyApi.driveClip("examplemod", player -> ExampleModClient.isAiming()
        ? PunchyApi.ClipRequest.named("scope_aim").loop().priority(PunchyApi.CLIP_USE)
        : null);

While the driver answers a request, Punchy! keeps that clip playing for your id: it starts once and is asked again each tick at its priority. Another clip name restarts it; null blends it out. One driver per id. With followProgress(p) the clip’s time follows a progress you give each tick, so a charge bar or a draw strength can scrub the pack’s clip:

PunchyApi.driveClip("examplemod/charge", player ->
        PunchyApi.ClipRequest.named("charge_up").followProgress(ExampleModClient.chargeFraction()));

The item’s own model animation

PunchyApi.itemRig(true, "reload", true);   // main hand, type "reload", persistent
PunchyApi.clearItemRig(true, "reload");

Plays the held item’s own model animation of a type ("reload", "use" and so on) when the item’s model definition has one. persistent holds it until clearItemRig or until the item changes; otherwise it plays once. Punchy!‘s own persistent types win while they last, and the API never clears one of them.

A model-parts item’s definition can ask for two new kinds of type in its modelPartsAnimation:

"modelPartsAnimation": [{"type": "on:examplegun:shoot", "animation": "rifle_bolt_cycle"},
                        {"type": "while:mypack:aiming", "animation": "rifle_scope_up"},
                        {"type": "while:examplegun:jammed@main", "animation": "rifle_jammed"}]

on:<moment> plays once when that moment fires for the hand that holds the item (a moment with no hand plays it in both hands). while:<condition> holds while the condition is true; @main or @off after the name holds only for that hand, and an item condition of a mod is read for the stack in that hand. The type is set when the condition turns on and cleared when it ends or the item changes; Punchy!‘s own persistent types win while they last. A slide that locks back when the magazine is empty, a scope that flips up while aiming. Older Punchy! never asks for these types, so the entries are harmless there.

A clip on the third-person model

PunchyApi.playThirdPersonClip("examplemod_wave");   // a clip of player_3_p.animation.json

Plays a clip of player_3_p.animation.json on the local player’s model. Punchy!‘s mirror of the first-person clip may replace it on the next tick.

The strength of Punchy!‘s camera shake

PunchyApi.cameraStrength("examplemod", () -> ExampleModClient.reducedMotion() ? 0.0 : 1.0);

Multiplies the camera shake Punchy!‘s own clips make by what the suppliers answer (0 to 1; the product of all of them). It can only lower the shake: motion comfort, replays, cutscenes. It follows the Enable Camera Animations setting. Mods don’t author camera motion; packs do, with camera_kick (below).

Clips in packs and in code: files, remaps, sounds, looks

The clips are the packs’. These are the keys and calls that change how Punchy! finds, times and sounds the clips a pack made, and what a hand is drawn holding. None of them makes a motion.

Clip file keys

Animation files are read from assets/punchy/animations/, assets/minecraft/punchy/animations/ and, since version 3, from assets/<any namespace>/punchy/animations/. A mod jar can ship clips under its own namespace without sharing a path with a pack. A pack’s clip of the same name as a mod’s wins by the usual order of packs, so give your clips a prefix of your own (examplemod_dodge) and a player’s pack can still override them.

These keys belong to a clip or to the file; a Punchy! older than version 3 ignores all of them. They are read for first-person clips only: an item model clip takes sound_effects with effect alone, and a third-person clip takes none of them.

KeyWhereMeaning
speedin a clipa number or an expression (see Numbers), evaluated each tick. It multiplies the clip’s step after Punchy!‘s own rules, limited to 0.05 to 10: "speed": "0.6 + q.attack_cooldown * 0.4"
blend_inin a clipseconds. Used when the machine that starts the clip asked for no blend-in of its own; a start that skips the blend-in still skips it
blend_in_easingin a clipsmooth, back or strong_back
aliasesat the top of the file{"swiming": "swimming"}: the name on the left finds the clip on the right, a clip of the same file. A clip the file really has under the name on the left wins, and an alias of an alias is not followed. Punchy! builds the alias again under its own name, so its rules by name see it (a pack that spells the clip swimming serves Punchy!‘s swiming), and the clip belongs to the pack that declared it in Mix Packs
sound_effectsin a clipsee Sounds below

An entry of a compat file takes speed too: {"type": "useItem", "name": "drink_potion", "speed": 1.2} multiplies the speed the machine asks for, or the speed of an anim_speed_ token in the clip, read once when the entry’s clip starts. It reaches the machines that play their clip through Punchy!‘s shared configured-clip path: attack, mining, falling, trident, triggerable and use item (and inspect and shield on 1.21.11). Other entry types ignore it: inspect and shield on other versions, idle, hand in and out, bow, crossbow, spear, consume, swimming, climbing, crawling, fire, walking on water, drying out, waking up and the boat paddle.

{"format_version": "1.8.0",
 "aliases": {"swiming": "swimming", "hand_in": "mypack_draw"},
 "animations": {
   "heavy_swing": {"animation_length": 0.9, "speed": "0.6 + q.attack_cooldown * 0.4",
                   "blend_in": 0.08, "blend_in_easing": "back", "bones": {}}}}

Held keys and key titles

A triggerable entry with a key plays once each time the key is pressed. With "hold": true it plays while the key is held:

{"type": "triggerable", "key": "G", "id": "check_watch", "name": "watch_look", "hold": true, "title": "key.mypack.check_watch"}

The clip loops while the key is down (or holds its last frame, when the clip is hold_on_last_frame) and ends when the key is let go: after the cycle, or at once for an entry with stop_immediate, with the entry’s blend-out in both cases. Another machine that takes the arm ends the hold. A title that is a language key the active packs define is shown translated in Controls; any other title is shown as it is written. A Punchy! older than version 3 plays a hold entry once per press and shows the title as written.

Mix Packs labels

A compat file may name its pack’s options for the Mix Packs screen:

{"mixLabel": "Heavy Weapons Pack"}

mixLabelKey takes a language key instead. Mix Packs shows that name where Punchy! would make one from the pack’s id or the zip’s file name, which changes when a player renames the zip. Saved choices keep using pack ids.

Remap clips

PunchyApi.remapClips("examplecombat", (clip, mainHand) -> ExampleCombat.inStance() ? ExampleCombat.clipFor(clip) : null);

Whenever Punchy! looks a clip up by name (every machine, every entry, every type fallback) the remaps are asked first. The first non-null answer by priority replaces the name, provided that clip exists; if it does not, the original is kept. A remap of attack also covers attack_right and attack_left, which then look for the target’s handed names (when the target has none, the base clip answers, not the original’s handed clips). Punchy!‘s rules by name then go by the clip that plays. A remap that asks Punchy! for clips itself is not asked again. Clips Punchy! keeps in fixed places (swimming, put out fire, wake up, hand-in) are not looked up by name: aliases cover those.

It is asked many times a tick: answer from a map, not from a calculation. Class and stance mods, injuries, morphs. It only picks which of the packs’ clips answers.

A pack or a mod jar can do the same with data, in a rule file:

{"requires_api": 3, "clip_remaps": [{"when": "examplecombat:in_combat", "priority": 5, "clips": {"idle": "combat_idle", "attack": "combat_attack"}},
                                    {"when": "mypack:tired", "clips": {"mining_foward": "mining_tired"}}]}

The entries of a rule file are tried by priority (higher first, default 0), and an entry counts while its gates hold.

Sounds

A clip’s sound_effects entry is a name or an object:

"sound_effects": {"0.10": {"effect": "minecraft:item.armor.equip_iron", "volume": 0.6, "pitch": 1.2, "pitch_range": 0.1},
                  "0.35": {"effect": "swish_heavy", "category": "players", "positional": true},
                  "0.50": "examplemod:gun.reload_click"}
  • A name with a namespace is any registered sound event, with no placeholder .ogg. A name without one is one of Punchy!‘s animation sounds, as before (punchy:animationssounds/<name>), and now also plays when the pack’s sounds.json defines that name without a file. A sound event that nothing defines is skipped with one line in the log.
  • volume and pitch are factors (default 1); pitch_range is a random offset either way around the pitch. Punchy!‘s own sounds keep their base volume (a quarter, as an interface sound); a sound event of another mod plays at its own.
  • category is master by default, as today. Another category (players, ambient, block …) makes that volume slider apply, and the sound plays at the player. positional plays it at the player instead of as an interface sound.
  • Inspect Volume still applies to the inspect clip, and sounds still stop when their clip changes.

A moment binding takes sound the same way (a name or an object), and plays it when the binding plays, also when the binding has no clip.

PunchyApi.soundRoute("examplemod", (sound, clip, leftArm) ->
        ExampleMod.isUnderwater() ? PunchyApi.ClipSound.of(sound).volume(0.4F).pitch(0.8F) : null);

Before Punchy! plays a sound a clip names, the first answer by priority may play another sound event (ClipSound.of("examplemod:gun.reload_click")), change the volume and pitch factors, or skip it (ClipSound.none()); null is no opinion. sound is the name as the data wrote it and clip the clip that plays it. Answering the same plain name keeps the sound and lets the factors change how it plays. A muffled room, a volume slider of your own, your own sound events instead of a pack’s.

What a hand shows

PunchyApi.heldItemLook("examplemod", (player, leftArm, drawn) ->
        ExampleMod.isCasting() && leftArm ? ExampleMod.ORB_STACK : null);   // PunchyApi.NO_STACK draws nothing

After Punchy! has decided what to draw for a hand (clip switch tokens, the consumed stack, equip lag) the first non-null answer by priority replaces it, for drawing only: the pose, kind, arm tuning and particles follow the drawn stack, while actions use the real one. Answer the same object every frame (keep it), or the hand redraws a new item each time. Hiding a rod also hides where its line starts. Weapon skins, an orb in the casting hand, a magazine in the off hand during a reload.

A rule file does the same for data:

{"requires_api": 3, "item_display": [{"held": {"items": ["examplemod:spell_focus"]}, "show": "none"},
                                     {"held": {"items": ["examplemod:quiver"]}, "show": "minecraft:arrow", "when": "examplemod:armed"}]}

held is a selector for the stack that would be drawn, show is none or an item id, and the gates apply.

Items, arms and movement

These change how Punchy! reads things. They are asked often, so keep them cheap (O(1)): itemPose about 33 times a frame, itemKind about 29 times a frame.

An item’s pose

PunchyApi.itemPose("examplemod", (stack, leftArm, pose) -> {
    if (ExampleModClient.isScoped(stack)) {
        pose[2] += 0.5F;   // offset y
    }
});

Changes the item pose Punchy! resolved for a stack and an arm, for that call only. It is never saved and the Hand Editor keeps showing the base pose. pose is scale, offset x, y, z, rotation x, y, z, then pivot x, y, z, in the units of compat JSON; edit it in place. Modifiers run lowest priority first and all apply.

Movement states

PunchyApi.movementState("examplemod", (player, state) -> state.equals("crawling") && ExampleModClient.isProne() ? Boolean.TRUE : null);

Changes how Punchy! reads a movement state for the local player: the first non-null answer wins. It always applies to pack clip variations that name the state (crouching, sprinting, swimming …). Two names reach further: crawling drives Punchy!‘s crawl clip, and pack_locomotion answered false turns the pack’s walk, sprint and jump layer off. Other machines read Minecraft directly and do not change.

The off arm

PunchyApi.offArmVisibility("examplemod", player -> ExampleModClient.twoWeaponMode() ? Boolean.TRUE : null);

TRUE draws the off arm even when empty (as Show Both Hands does), FALSE hides the arm and its item except while a two-handed clip needs it, null has no opinion; the first answer wins. A hand left to Minecraft stays Minecraft’s.

An item’s kind

PunchyApi.itemKind("examplemod", stack -> ExampleModClient.isBlade(stack) ? "SWORD" : null);

Gives an item a Punchy! kind (the first non-null answer wins). It is asked after a pack’s own kind for the item and before Punchy!‘s rules, and changes poses, default clips, particles and kind fallbacks, not which machine handles the item. The names are Punchy!‘s kinds: SWORD, AXE, PICKAXE, SHOVEL, HOE, BOW, CROSSBOW, SPEAR, TRIDENT, SHIELD, FOOD, TORCH, LANTERN, BLOCK, FISHING_ROD and the rest of the kinds the Hand Editor lists. An unknown name is ignored with one line in the log.

An item’s role

PunchyApi.itemRole("examplemod", (stack, role) -> role.equals("throwable") && ExampleModClient.isKnife(stack) ? Boolean.TRUE : null);

Puts a stack into one of Punchy!‘s machines ("consume", "spear", "throwable") or keeps it out (FALSE); null has no opinion. Mods are asked first, then rules, then Punchy!. Bows, crossbows, shields, tridents and fishing rods are decided inside Punchy!‘s machines; use driveClip for modded ones.

Equips and swings

PunchyApi.equipRules("examplemod", new PunchyApi.EquipRules() {
    @Override
    public Boolean changed(Object before, Object after) {
        return ExampleModClient.sameGun(before, after) ? Boolean.FALSE : null;   // FALSE: the same item
    }

    @Override
    public Boolean skip(Object before, Object after, boolean mainHand) {
        return ExampleModClient.modeSwap(before, after) ? Boolean.TRUE : null;   // TRUE: swap without hand-out and hand-in
    }
});

PunchyApi.attackRules("examplemod", new PunchyApi.AttackRules() {
    @Override
    public Integer comboIndex(Object stack, boolean mainHand, int clipCount) {
        return ExampleModClient.comboStep(stack);   // 0-based, clamped to the clips the stack has
    }

    @Override
    public Boolean unthrottled(Object stack) {
        return ExampleModClient.isDagger(stack) ? Boolean.TRUE : null;   // every swing plays, no cap of about 3.5 a second
    }

    @Override
    public Boolean swingsAtAir(Object stack) {
        return null;   // TRUE: a tool swing at air plays the attack clip, not the mining fallback
    }
});

equipRules replaces Punchy!‘s two tests for equips: whether a new stack of the same item counts as a change (a gun whose ammo count is in its components should not play hand-out and hand-in on every shot), and whether a swap goes without the hand-out and hand-in. attackRules chooses which combo clip the next swing plays, whether swings are throttled, and whether a tool swings at air. Each question takes the first answer that is not null.

A rule file can compare the data of two stacks of one item without code, in the equip kind:

{"requires_api": 3, "equip": [{"items": ["examplemod:rifle"], "changed_by_components": ["examplemod:ammo"], "changed_by_nbt": ["Ammo"],
                               "ignore_components": ["minecraft:enchantments"], "ignore_nbt": ["Glint"]}]}
KeyMeaning
changed_by_components, changed_by_nbta difference in this data counts as a change (a new hand-out and hand-in), though Punchy!‘s own test would not see it
ignore_components, ignore_nbta difference only in this data does not count as a change

Components exist from Minecraft 1.21.1 (their ids, minecraft: by default) and NBT before it (the top-level keys of the stack’s tag); a key of the kind a version does not have is ignored, so one rule file serves every version. Punchy!‘s own test stays the default for everything the entry does not name: it already leaves out the damage and the components of other mods (and, before 1.21.1, Damage and Forge’s capabilities), and ignore_* is about the difference from there. The entry takes a selector and the usual gates, and skip may stay in the same entry.

Veto clips

PunchyApi.filterClips("examplemod", q -> !(q.layer().equals("idle") && ExampleModClient.isCutscene()));

Vetoes a clip before it starts, on every layer Punchy! can intercept (q.layer() is "action", "overlay", "idle", "item" or "third_person"), with q.clip(), q.arms(), q.reason() (as in onClip), q.stack() and q.mainHand(). Any false wins. The query object is reused: copy what you keep. filterAnimations is still asked, with the clip’s final name.

Clip speed

PunchyApi.clipSpeed("examplemod", (clip, layer, reason) -> reason.equals("attack") ? ExampleModClient.attackSpeedFactor() : 1.0F);

Multiplies the speed of clips after Punchy!‘s own rules (the Animation Speed slider, name rules, anim_speed tokens), from 0 (frozen) to 10, asked once per tick per playback. Sounds, markers and particle pulses stay in step with the clip. Use it for attack speed, Haste, stamina and time stops.

Sway: movementScale

PunchyApi.movementScale("examplemod", channel -> channel.equals("walk") && ExampleModClient.heavyArmor() ? 0.6F : 1F);

Scales Punchy!‘s procedural sway by channel: walk, sprint, jump, idle, look, yaw_lag, fall, flight. The answers of every registration multiply, kept within 0 to 2, and apply from the next tick. Asked once per channel per tick.

The display context of a held item: itemDisplay

PunchyApi.itemDisplay("examplemod", (stack, leftArm) -> stack instanceof ItemStack s && s.is(ExampleItems.RIFLE) ? "firstperson" : null);

The first answer by priority wins over Punchy!‘s own choice (the third-person hand, switched to first person when the model’s up direction differs): thirdperson, firstperson, fixed, ground, gui, head or none, and the side follows the arm. The rule is display_context, with as, a selector and gates.

An item’s own use visuals: keepItemUseVisuals

PunchyApi.keepItemUseVisuals("examplemod", stack -> stack instanceof ItemStack s && s.is(ExampleItems.CHARGE_STAFF));

For matching stacks Punchy! stops idling the item model’s use properties (is_using_item and use_duration), so the item’s own use look shows while Punchy! animates the arm. The rule is keep_use_visuals, with a selector and gates.

The arm’s pose, where it is, and frame phases

Everything on this page about motion is a pack’s job: a pack’s clip moves the arm. This section is for what a clip cannot know, because it depends on a mod’s own mechanics: a rifle’s recoil, the sway of a heavy tool, the way a carried weight pulls the shoulder down, an animation library that poses the arm itself. The calls are small on purpose. armPose moves and turns the arm by a little (a quarter of a block and thirty degrees at most), externalArmPose lets another system pose it, claimArm keeps Punchy!‘s clips off an arm, and armMatrix and itemMatrix say where the arm and the item are, so a mod can draw something that belongs to them. To make an arm do a motion, ship a clip and play it on a moment (moment, playClip, a binding), or drive it from a variable.

Two spaces, and a picture of them

view space (the camera's), in blocks        arm space, for an arm that hangs straight down

          y (up)                                       y (up)
          |                                            |
          |                                    shoulder pivot = the origin (0, 0, 0)
          +----- x (to the right)                      |   the arm hangs along minus y
         /                                             |
        z (toward you)                          hand at (0, -0.625, 0)

View space is the camera’s: the origin at the camera, x to the right of the screen, y up and z toward you, in blocks. It is where Minecraft’s first-person pass ends up after the game’s model view matrix, with Punchy!‘s camera compensation applied, so a point at z = -0.6 is in front of you wherever the player looks. It is not the space of the pose you draw with: see “Drawing at the arm or the item”. Arm space belongs to the arm and turns with it: the origin is the shoulder pivot, and for an arm that hangs straight down x is to the right, y is up and z is toward you, with the hand about 0.625 below the origin. Turns are in degrees with right-handed axes: a positive pitch swings the hand away from you, a positive roll swings it to the right, and a positive yaw twists the arm counterclockwise seen from above. The left arm uses the same directions (right is the screen’s right for both).

Where the arm and the item are

Matrix4f arm = new Matrix4f();
if (PunchyApi.armMatrix(false, arm)) {              // false for the right arm, true for the left; arm space to view space
    Vector3f palm = arm.transformPosition(new Vector3f(0F, -0.55F, 0F));   // a point on the hand, in view space
    ExampleModClient.drawOrbAt(palm);
}
Matrix4f item = new Matrix4f();
if (PunchyApi.itemMatrix(false, item)) {            // the item's root to view space
    ExampleModClient.startLaserAt(item.getTranslation(new Vector3f()));
}
Quaternionf turn = new Quaternionf();
Vector3f move = new Vector3f();
if (PunchyApi.cameraAnimation(turn, move)) {        // Punchy's camera shake and kicks right now
    ExampleModClient.followTheView(turn, move);
}

armMatrix(leftArm, dest) fills dest with the arm Punchy! drew in its last first-person pass, edits of armPose and externalArmPose included, and answers true. It answers false when that arm was not drawn (a hand left to Minecraft, an empty off hand Punchy! does not show, hideArms, hideHands, a spyglass in use, offArmVisibility answering false), when no pass has run for about a second (third person, F1, a sleeping player), and from a change of world until the next pass. An arm drawn through Minecraft’s hand code has its matrix. itemMatrix does the same for the held item’s root: where Punchy! placed the item after grip, tuning and physics, before the item model’s own display transform; its origin is where the item sits in the hand and its axes are those the display transforms of item models expect. Items Punchy! draws with its own boat, minecart and chest models are not seen. The values are those of the last pass that was drawn, so inside an onFrame("end") listener they are the pass that just ended. Read them on the client thread, best from onFrame, since outside it they can be up to a second old. cameraAnimation(rotation, offset) fills the camera turn (a quaternion) and the offset (blocks) Punchy! applies this frame, from a clip’s camera bone and from camera kicks, and answers false when there is none; Enable Camera Animations decides whether there is any.

Frame phases

PunchyApi.onFrame("examplemod", "start", (partialTick, pass) -> ExampleModClient.beginHandFrame());
PunchyApi.onFrame("examplemod", "end", (partialTick, pass) -> {
    Matrix4f arm = ExampleModClient.scratch();
    if (PunchyApi.armMatrix(false, arm)) {
        ExampleModClient.placeMuzzleFlash(arm);
    }
});

The listeners of a phase run around Punchy!‘s first-person pass: "start" before Punchy! draws the arms and the items, "end" after. pass counts the passes since the game started. A shader pack with a translucent block in a hand runs the pass twice in a frame and each counts, so a listener that draws something draws it twice in that frame. The listeners run whenever Minecraft draws the first-person hands, whether or not Punchy! is drawing (Punchy! turned off or the hands left to Minecraft leave the pass empty), and not in third person, under F1, in spectator or while sleeping. They are asked on the frame path, so the budget is 50 microseconds a call.

Drawing at the arm or the item

armMatrix and itemMatrix are in view space, but the pose you draw with is not. Minecraft puts its model view matrix in front of every pose, and in the first-person pass that matrix changes with the version: from 1.21.1 on it holds the camera’s turn and the pass’s own pose stack starts with the opposite turn, with an Iris shader pack on 1.21.1 and 1.21.11 it holds the view bob, and on 1.20.1 it is the identity. A pose set straight to armMatrix is right on 1.20.1, but from 1.21.1 on only while the player looks due north and level. Inside an onFrame listener, take the model view matrix off first:

Matrix4f modelView = RenderSystem.getModelViewStack();         // on 1.20.1, RenderSystem.getModelViewMatrix()
Matrix4f toDraw = new Matrix4f(modelView).invert().mul(arm);   // a copy: the stack is the game's own
PoseStack pose = new PoseStack();
pose.mulPose(toDraw);                                          // on 1.20.1, mulPoseMatrix

This holds inside a listener only: elsewhere in the frame the model view matrix and the field of view are no longer the pass’s.

To put something in the world at a view space point, start from the camera’s position and add the point turned by the camera’s rotation. From 1.21.1 on camera.rotation() has x to the right and z back, as view space does. On 1.20.1 it has x to the left and z forward, so use new Quaternionf(camera.rotation()).rotateY((float) Math.PI) there. The first-person pass always draws at a field of view of 70 degrees (less in a fluid or while dying), whatever the player’s setting, so a world object only meets the hand on screen at FOV 70. To meet it at another FOV, first multiply the point’s x and y by tan(worldFov / 2) / tan(handFov / 2).

With Iris on 26.2 the bob and the hurt tilt go into the projection matrix instead, so the matrices lack them while a shader pack is on.

Fit the arm to your mechanics: armPose

PunchyApi.armPose("examplemod/recoil", "final", pose -> {
    if (pose.mainHand() && ExampleModClient.isRifle(pose.stack())) {
        float kick = ExampleModClient.recoil(pose.partialTick());     // the mod's own spring, 0 to 1
        pose.moveView(0F, 0F, 0.05F * kick)                           // back toward you
            .pivot("hand").turnLocal(4F * kick, 0F, 0F);              // and the muzzle up, about the hand
    }
});

Every frame, for each arm Punchy! poses, the modifiers of all mods run, lowest priority first, so the highest priority has the last word. A modifier says what to do through the ArmPose it gets (valid during the call only):

CallWhat it does
moveView(x, y, z)moves the arm by blocks in view space
turnView(pitch, yaw, roll)turns the arm by degrees about the pivot, with the axes of view space
moveLocal(x, y, z)moves the arm by blocks along its own axes
turnLocal(pitch, yaw, roll)turns the arm by degrees about the pivot, about its own axes
scale(factor)grows or shrinks the arm about the pivot
pivot(where)"shoulder" (every modifier starts there), "hand" or "item" (where Punchy! held the item in the pass before; the hand when there was none): the point later turns and scales are about
leftArm(), mainHand(), partialTick(), player(), stack()which arm, the stack it holds, the frame’s partial tick
rig()a copy of Punchy!‘s own rig matrix of the arm, in pixels with the rig’s axes, for experts; changing it changes nothing

Stage "final" runs after all of Punchy!‘s pose (the clip, its procedural motion, the tuning, the camera lag): recoil, sway, aim, droop. Stage "root" runs before Punchy! composes the arm, so its own motion, riding, the clip and the tuning come on top of the change (a lean of the body, a carried weight); in the root stage the arm’s own axes are those of the body the arm hangs from. The arm, its armor, its sleeve, its overlays, its flames, the held item, the item’s particles and the fishing line all follow, also for an arm Punchy! draws through Minecraft’s hand code (useMinecraftHands); a hand left to Minecraft is not touched. A clip’s disable_arm_tuning does not stop a modifier, since a modifier is not tuning.

A modifier is stateless: it is called every frame and says where the arm should be now, so a mod keeps its own spring or timer. It cannot animate an arm. Everything all modifiers add together for one arm in one frame is limited, in each space and on each axis: moves to PunchyApi.ARM_POSE_MAX_MOVE (0.25 block), turns to PunchyApi.ARM_POSE_MAX_TURN (30 degrees), and the scale to 0.75 to 1.25. When a sum passes its limit the calls on that axis are all made smaller by the same factor. Calls in view space are made before calls along the arm’s own axes; at most 24 turns and scales count for one arm in one frame. A pivot name that is not one of the three throws, which drops the modifier. Registering again under the same id and stage replaces the modifier.

Pose the arm from your own system: externalArmPose

PunchyApi.externalArmPose("examplemod/emotes", (player, leftArm, partialTick, out) -> {
    EmoteFrame frame = ExampleModClient.currentEmote(leftArm);
    if (frame == null) {
        return 0F;                                        // no opinion: Punchy poses the arm
    }
    frame.fillMatrix(out, partialTick);                   // out is in the space of armMatrix
    return ExampleModClient.emoteBlend();                 // 0 to 1: how far Punchy's arm goes toward it
});
PunchyApi.claimArm("examplemod/emotes", (player, leftArm) -> ExampleModClient.currentEmote(leftArm) != null);

For an emote library, a climbing or cutscene mod, or a spell mod that already animates the first-person arm, so that the arm keeps everything else Punchy! gives it: skin, sleeve, armor, overlays, flames and the held item. Each frame the first source by priority that answers a weight above 0 wins, and Punchy! blends its arm toward the matrix the source filled in by that weight: position and scale mixed, rotation along the shortest turn. out already holds Punchy!‘s own arm when the source is called, in the space of armMatrix, so to blend in from Punchy!‘s pose a source can change out and ramp its weight from 0 to 1. The final-stage modifiers run after the external pose. A matrix with values that are not numbers drops the source.

claimArm(id, when) says that, while when is true for an arm, Punchy! starts no clip on it (a clip for both hands counts for each arm) and keeps drawing the arm; the arm is the mod’s to pose. A clip that already plays on the arm keeps playing, and a clip Punchy! lays over another one as a secondary clip cannot be kept from starting, so a source with a weight of 1 is what hides it. The condition is asked whenever Punchy! would start a clip, so keep it cheap; Punchy! asks again at most once a millisecond.

Particle bursts: particles

PunchyApi.particles(false, "particle(type=minecraft:flame, count=6, spread=0.1:0.1:0.1)");   // false: the right arm

Queues one burst of a Punchy! particle script (the particle(...) text of a compat particle_emitters entry or a model-parts definition) at the root of the item the arm holds, the next time Punchy! draws that arm’s item. It is safe from any thread. Use count (or burst); a script with a rate or a duration is given only the one frame it is asked for and makes next to nothing. The particles are Punchy!‘s own and follow the player’s Enable Particle System setting. An item Punchy! draws with its own boat, minecart or chest models, or whose pack model makes its own particles, does not get the burst, and a burst that waits more than a second for an item to be drawn is dropped.

Conditions

A condition is a true or false state with a name. Mods publish them, packs define them, and packs use them wherever Punchy! takes a condition: the states of a clip variation, the when of a compat entry, moment bindings, rule files. The names and forms are listed under The conditions packs can use.

PunchyApi.condition("examplespells", "casting", () -> ExampleSpells.isCasting());   // pulled: asked at most once per 50 ms
PunchyApi.setCondition("examplespells", "channeling", true);                        // pushed: keeps its value until you change it
boolean now = PunchyApi.conditionValue("examplespells:casting");
boolean inline = PunchyApi.conditionValue("held=minecraft:bow");
  • condition(id, name, when) publishes a name. A name without a namespace gets your mod id as one: condition("examplespells", "casting", ...) is examplespells:casting. when is asked at most once per 50 ms, and only while something refers to the name. Several registrations of one name combine with OR.
  • setCondition(id, name, on) is the same for a state you push: it keeps its value until you change it, and costs nothing between changes.
  • conditionValue(name) evaluates any condition now: one of Punchy!‘s own state names, a mod’s or a pack’s condition, or an inline form.
  • A name nothing registered reads false on every Punchy!, so a pack can name your condition whether or not your mod is installed.
PunchyApi.itemCondition("examplegun", "loaded", stack -> ExampleGun.ammo(stack) > 0);
PunchyApi.itemValue("examplegun", "ammo", stack -> ExampleGun.isGun(stack) ? ExampleGun.ammo(stack) : Double.NaN);
  • itemCondition(id, name, test) is a named test on the stacks your mod describes (loaded, jammed, charged). Packs read it with a hand: examplegun:loaded@main, examplegun:loaded@off, examplegun:loaded@any.
  • itemValue(id, name, value) is a named number on your stacks (ammo left, charge, heat); NaN means no value for that stack. It must depend only on the stack. Packs read it in expressions as v.main_hand.examplegun.ammo and v.off_hand.examplegun.ammo (see Numbers: variables, queries and expressions).

Numbers: variables, queries and expressions

A pack’s clip can read numbers while it plays. Mods publish them (mana, recoil, heat, charge, the ammo in a gun), Punchy! publishes some of its own (health, whether the player sneaks, how long an item has been in use), and a pack can define more from those. They are read in the keyframes of the pack’s clips, in expr conditions and in pack variables. A number changes how strongly the pack’s own motion shows; it never makes motion. A variable no clip reads moves nothing, and the keyframes stay the pack creator’s work.

Variables

PunchyApi.variable("examplemod", "charge", () -> ExampleMod.chargeFraction());   // pulled: asked once per tick
PunchyApi.setVariable("examplemod", "recoil", 0.8);                              // pushed: keeps its value
double now = PunchyApi.variableValue("examplemod:charge");                       // NaN when nobody publishes it
  • variable(id, name, value) publishes a number. A name without a namespace gets your mod id as one, so variable("examplemod", "charge", ...) is examplemod:charge. value is asked once per Punchy! tick, at the start of the tick, and only while a loaded clip, condition or pack variable refers to the name. Keep it cheap: it runs on the client thread.
  • setVariable(id, name, value) is the same for a number you push: it keeps its value until you change it, and Punchy! asks no one between changes.
  • Punchy! keeps the last two samples and blends them by the time since the tick, so a value that changes once per tick moves smoothly on the screen. NaN and infinity read as 0.
  • A variable has one value per name. A later registration of the same name takes it from an earlier one (one line in the log). A mod’s variable beats a pack’s variable of the same name. While Punchy! is off it has no ticks, so pulled variables, pack variables and queries stay at their last sample.
  • variableValue(name) reads the value of any variable by its full name, a mod’s or a pack’s. It answers NaN when nobody publishes the name. A value pushed with setVariable is returned as pushed; a pulled or pack-defined one is its last sample. Reading a name keeps it sampled for a while.
  • A name nothing publishes reads 0 inside an expression, so a pack can name your variable whether or not your mod is installed.

Queries

Queries are Punchy!‘s own numbers about the local player, written q.<name> or query.<name>. They are sampled once per tick, only when something refers to them, and blended like variables. Outside a world they read 0. They are not support for another mod, so Built-in Mod Support does not turn them off. Names follow Bedrock’s Molang where the meaning is the same, so a creator can preview a clip with Blockbench’s variable placeholders. A name this Punchy! does not have reads 0 (one line in the log), so a pack written for a later list still loads.

GroupNames
Clip and settingsanim_time (seconds into the clip, as Punchy! has had it; in a keyframe only, it reads 0 in expr and speed), animation_speed (the player’s animation speed setting), is_first_person
Health and needshealth, max_health, health_fraction, food, saturation, air_fraction (1 full, 0 drowning), armor, frozen (0 to 1), experience_level, experience_progress
Motionground_speed and vertical_speed (blocks a second, from the real move of the last tick, so a rider’s is the vehicle’s and a stair step is a short spike), walk_distance (blocks this player has moved on the ground plane; a respawn or a change of dimension starts it over, and it counts only while something reads queries), yaw_speed (degrees a second, mouse turns included), head_x_rotation (look pitch in degrees), fall_distance
States, 0 or 1is_on_ground, is_sneaking, is_sprinting, is_swimming, is_in_water, is_in_water_or_rain, is_on_fire, is_riding, is_gliding, is_using_item
Use and attackuse_progress (0 to 1 of the used item’s duration; a bow or a trident lasts an hour by the game’s count, so use use_time for those), use_time (seconds), attack_cooldown (0 to 1), attack_speed (attacks a second)
Held itemsmain_hand_durability, off_hand_durability (the fraction left, 1 when the stack cannot be damaged), main_hand_count, off_hand_count, selected_slot (0 to 8)
Worldlight_level (0 to 15 at the eyes), rain_level, thunder_level, time_of_day (0 to 1 over a day: 0 at sunrise, 0.25 at noon, 0.75 at midnight), moon_phase (0 to 7; from 1.21.11 on it reads 0 outside the Overworld)
Effectseffect.<namespace>.<path>: the effect’s level (its amplifier plus 1), or 0 without it, for example q.effect.minecraft.speed

Expressions in keyframes

A keyframe component in a pack’s clip file can be a number or an expression text. Punchy! has always read expressions with + - * /, parentheses, the numbers and the functions sin, cos, tan (in degrees), abs, sqrt, floor, ceil, round, min, max and clamp, and the constants pi and e, and that does not change: a division by zero is 0, an unknown name is 0, and math. before a function is optional. Version 3 adds names and functions, never operators:

Name or functionMeaning
v.<ns>.<path>, variable.<ns>.<path>a variable, written with a dot where the id has its colon: v.examplemod.charge is examplemod:charge
v.main_hand.<ns>.<path>, v.off_hand.<ns>.<path>an item value (itemValue) of the stack in that hand; an empty hand or a stack the mod gives no value for reads 0
q.<name>, query.<name>a query
math.lerp(a, b, t)a at t 0, b at t 1
math.smoothstep(edge0, edge1, x)0 below edge0, 1 above edge1, a smooth ramp between
math.step(edge, x)0 below edge, otherwise 1
math.sign(x)-1, 0 or 1
math.mod(a, b)the remainder of a / b with the sign of b, so math.mod(-1, 3) is 2 (0 when b is 0)
math.pow(a, b), math.exp(x)powers
math.select(c, a, b)a when c is not 0, otherwise b
"rotation": {"0.0": ["-v.examplegun.recoil * 25", 0, "math.sin(q.anim_time * 90) * 2 * (1 - q.health_fraction)"],
             "0.5": ["math.lerp(-10, 10, v.mypack.aim)", 0, "v.main_hand.examplegun.ammo * 0.5"]}

The clip, its bones and its timing are the pack’s; the expressions only let a pack’s own motion react to the number. They work in first-person clips and item model clips. Do not write them in third-person clips: that file’s reader takes numbers only, and a text in a keyframe loses that clip and every clip after it in player_3_p.animation.json, on every Punchy!. An expression that uses only Punchy!‘s old names and functions is read exactly as before. Comparisons and logic have no operators in keyframes: use math.select, or put the test in an expr condition or a pack variable.

A Punchy! older than version 3 does not know the new names. It reads an unknown name as 0 and keeps loading the clip, so a pack that uses them still loads there and only loses the motion that depends on them. A pack that wants a fallback can write the keyframe so that 0 is the plain pose.

Pack variables

A rule file can define variables from the moments and queries Punchy! already has. They are the same kind of name as a mod’s variable (a bare name gets the rule file’s namespace), so a clip, a condition or another variable reads them the same way.

{"requires_api": 3, "variables": {
  "mypack:aim":   {"expr": "q.is_sneaking * q.is_using_item", "smooth": 0.15},
  "mypack:tired": {"expr": "1 - q.food / 20", "min": 0, "max": 1},
  "mypack:combo": {"count": "attack", "reset": 1.2, "max": 4, "wrap": true},
  "mypack:heat":  {"add": {"on": "examplegun:shoot", "amount": "value"}, "decay": 0.5, "max": 1},
  "mypack:fall":  {"set": {"on": "land", "to": "value"}}}}
KeyMeaning
exprevaluated each tick. Besides the keyframe names it takes comparisons and logic: < <= > >= == != && || !, which give 1 or 0
smoothwith expr: seconds to ease toward the target, so a 0 or 1 test becomes a weight that eases poses in and out
counta moment name: adds one each time it fires. reset seconds without it sends the count back to 0, max stops it, wrap starts again at 1 after max instead
add{"on": moment, "amount": number or "value"}: adds the number, or the moment’s value, each time it fires. decay takes that many away each second
set{"on": moment, "to": number or "value"}: keeps the number, or the moment’s value, from the last time it fired
min, maxlimit any of them

Variables may read each other; Punchy! works them out in the order they depend on each other, and a cycle reads 0. Counters go back to 0 on a world change and when the player respawns. A moment name without a namespace is one of Punchy!‘s own (see The moments Punchy! has).

expr conditions

A condition object with an expr key tests an expression, true when it is not 0: {"expr": "v.examplemod.charge >= 0.95"}. It takes the same names and functions as a keyframe and the comparisons and logic above, except that q.anim_time reads 0. It works wherever a condition does.

Rules from code and pack options

PunchyApi.rules("examplemod", """
        {"rules": [
          {"kind": [{"as": "SWORD", "items": ["examplemod:*_blade"]}]},
          {"use_minecraft_hands": [{"when": "examplemod:wearing_gloves"}]}
        ]}
        """);

rules(id, json) gives Punchy! rules your mod builds at run time, with whatever a rule file can say (see Rule files). The text is one rule or {"rules": [ ... ]}. It is parsed at once, kept under your id and applied again after every reload; a text that cannot be read is logged and ignored. Calling again replaces it; remove takes it away. Rules from code rank at PRIORITY_RULES.

String raw = PunchyApi.packOption("examplegun:recoilScale");        // raw JSON text, or null
boolean bar = PunchyApi.packFlag("examplegun:showReloadBar", true);   // a yes or no option, or the fallback
double scale = PunchyApi.packNumber("examplegun:recoilScale", 1.0);   // a number, or the fallback
PunchyApi.compatData("examplemod", """
        {"itemSpecific": {"examplemod:rifle": {"customAnimation": [
          {"type": "triggerable", "on": "examplemod:shoot", "animation": "rifle_shoot"}]}}}
        """);

compatData(id, json) hands Punchy! a compat file’s JSON object (see Compat entries) built at run time, for data that exists only then: entries, groups and bindings in the format packs use in assets/minecraft/punchy/compat. Punchy! reads its tuning from the packs’ files at each load and puts yours underneath them, in id order, so a pack’s entry for an item always wins over a mod’s. It is not a Mix Packs option and the player’s profiles never record it. Calling again with the same id replaces it, remove takes it away, and a change after a load makes Punchy! read its tuning again. A text that is not a JSON object is logged and ignored. A mod that knows its items when it is built does better to ship a compat file in its jar (with a selector group, below); this is for data that is not known until the game runs.

Reads the options a pack set for your mod in a compat file’s modOptions (see below). The highest pack wins for each key. Read them in onReload("loaded"), since they are read at every load of the data. A value of another type answers the fallback.

Rule files, for packs and mod jars

A resource pack or a mod jar can leave items to Minecraft, and since version 3 say much more, without code. Put JSON files in assets/<any namespace>/punchy_rules/, for example assets/examplemod/punchy_rules/guns.json. Punchy! reads them at every resource reload (on start, when packs change, and with F3+T). Every file of every pack and every mod jar applies; a file doesn’t replace another of the same name.

A file is one rule, or {"rules": [ ... ]} for several. A rule:

{
  "id": "examplemod/rules",
  "requires_api": 3,
  "requires_mods": ["examplemod"],

  "leave_to_minecraft": {"items": ["examplemod:camera"]},

  "use_minecraft_hands": [{"when": "examplemod:wearing_gloves"}],
  "kind": [{"as": "SWORD", "tags": ["examplemod:blades"]}],
  "item_roles": [{"role": "throwable", "items": ["examplemod:throwing_knife"]}],
  "attack": [{"unthrottled": true, "tags": ["examplemod:daggers"]}],
  "equip": [{"skip": true, "items": ["examplemod:*_rifle"]}],
  "filter_clips": [{"clips": ["wake_up"], "when": "examplemod:in_cutscene"}],
  "conditions": {"aiming": {"all": ["crouching", "using_item"]}},
  "moments": {"aim_start": {"starts": "examplemod:aiming"}},
  "camera_kicks": [{"on": "examplemod:shoot", "turn": [-2, 0.5, 0], "seconds": 0.15}],
  "take_away": ["punchy/accessories"]
}

Selectors and gates

  • A selector names items: items (an exact id such as create:potato_cannon, a glob with * for any run of characters and ? for any one character such as cgs:*, or a bare namespace such as exposure for every item of a mod), tags (item tag ids, with or without #; a tag without a namespace is a minecraft tag) and classes (binary class names such as com.example.Outer$Inner; a stack matches when its item’s class, one of its superclasses or one of its interfaces has that name). A selector matches when any part does. A class of a mod that isn’t installed never matches, so naming it is safe. Use classes for mods’ classes only: Minecraft’s own classes have other names while the game runs on Fabric up to 1.21.11, so name Minecraft’s items by id or tag.
  • A selector in a version 3 kind or a gate (not in a version 2 kind, and not in a group) can also be narrowed by the stack itself, with these keys beside items, tags and classes; all the keys it has must hold:
KeyMeaning
componentsdata components the stack has (ids, minecraft: by default): ["examplemod:loaded"]. Minecraft 1.21.1 and newer
nbttop-level keys of the stack’s tag: ["Ammo"]. Minecraft 1.20.1
namea glob on the stack’s custom name, in any case: "*excalibur*"
custom_model_dataa number, a range as [min, max] or {"min": 1, "max": 9} (from Minecraft 1.21.4 the first number of the component)
enchantmentsenchantment ids the stack has all of: ["minecraft:sharpness"]
damagedtrue or false
durability_belowthe fraction of the durability left is below this: 0.2 for a nearly broken tool (a stack that cannot be damaged has 1)

A key of the kind of data a version does not have narrows nothing there (components before 1.21.1, nbt after), so write both for a rule that should hold on both. Keys with no item part narrow every item. Answers about a stack object are kept for one tick.

{"requires_api": 3, "kind": [{"as": "SWORD", "items": ["examplemod:*_blade"], "durability_below": 0.2}]}
  • A gate limits when an entry counts. Every version 3 entry takes when (a condition: a name, a list of names that must all hold, or a condition object), held (a selector, with "hand": "main" | "off" | "either", default either), worn (a selector, with "slot": "head" | "chest" | "legs" | "feet", default any) and requires_mods. An entry holds while all its gates hold; an entry without gates always holds. Gates are read for the local player and kept for 50 ms.
  • At the rule level, id, requires_api and requires_mods apply to the whole rule. requires_mods lists mod ids that must all be loaded for the rule (or the entry) to count. requires_api is the API version the rule needs: a rule that asks for a newer version than the installed Punchy!‘s is skipped whole, with one line in the log per file, rather than applied in the part an older Punchy! reads. id names the rule’s registrations, so PunchyApi.remove(id) takes the rule away and registrations shows it. Without an id a rule is named rules/<namespace> after its file’s namespace.
  • Keys Punchy! doesn’t know are ignored. A file Punchy! can’t read is named in the log and skipped; the other files still count. A kind or an entry that cannot be read is named in the log and skipped.

The kinds

KindWhat it does
leave_to_minecrafta hand holding a matching item is left to Minecraft (version 2; never gated)
leave_both_hands_to_minecraftwhile either hand holds a match, both hands are left to Minecraft (version 2; never gated)
use_minecraft_handsentries with gates: while one holds, Punchy! draws its arms through Minecraft’s hand code, as the call does; with arm (left, right, main, off) only that arm, as useMinecraftHandsFor
hide_arms, hide_armor_on_armsentries with gates only: as the calls
disable_punchyentries with gates only: Punchy! steps aside while one holds; an entry that reads minecraft_hands is skipped, since that depends on Punchy! being on
leave_hands_to_minecrafthand (main, off or both, default both) and gates: that hand is left to Minecraft while the gates hold, whatever it holds
flamessprites (two block atlas sprite ids), optional tint (#RRGGBB or #AARRGGBB) and gates
kindas (a Punchy! kind name) and a selector: matching items get that kind, as itemKind
item_rolesrole (or not, to keep the item out of the role) and a selector, as itemRole
attackunthrottled and/or swings_at_air and a selector, as attackRules
equipskip: true and a selector: a swap to or from a match goes without hand-out and hand-in, as equipRules
filter_clipsclips (names or globs) and gates: those clips do not play while the gates hold
hide_layerslayers, arm and gates, as hideLayers
hide_hands_while_usinga selector for the stack in use, and gates, as hideHands
hot_itemsa selector for the held stacks that warm the hand, and gates, as hotItems
arm_texturetexture, tint, full_bright, arm and gates, as armTexture
arm_overlaystexture, tint, full_bright, arm, fade_in, fade_out, alpha and gates, as armOverlays
arm_flamessprites, tint, scale, arm and gates, as armFlames
display_contextas, a selector and gates, as itemDisplay
keep_use_visualsa selector and gates, as keepItemUseVisuals
clip_remapsclips (a map of clip name to the clip to find instead), priority and gates, as remapClips
item_displayheld (a selector for the stack that would be drawn), show (none or an item id) and gates, as heldItemLook
conditionsan object of condition definitions packs make (below)
momentsan object of moment definitions packs make (below)
camera_kicksa camera kick for a moment (below)
take_awayids of Punchy!‘s own punchy/... support to turn off (see Mods Punchy! supports by itself); other ids are skipped with a line in the log

Every kind takes one entry or a list of entries. Examples:

{"requires_mods": ["tacz"], "leave_both_hands_to_minecraft": {"classes": ["com.tacz.guns.api.item.IGun"]}}
{
  "hide_arms": [{"when": "examplemod:invisible_in_cutscene"}],
  "leave_hands_to_minecraft": [{"hand": "off", "held": {"items": ["examplemod:map_book"], "hand": "off"}}],
  "flames": [{"sprites": ["minecraft:block/soul_fire_0", "minecraft:block/soul_fire_1"], "tint": "#99CCFF", "worn": {"items": ["examplemod:frost_ring"]}}]
}

Conditions and moments packs define

{
  "conditions": {
    "aiming": {"all": ["crouching", "using_item"]},
    "ready": ["examplegun:loaded@main", "!sprinting"]
  },
  "moments": {
    "aim_start": {"starts": "examplemod:aiming"},
    "aim_stop": {"ends": "examplemod:aiming"},
    "big_hit": {"on": "hurt", "min_value": 6},
    "any_shot": {"any": ["examplegun:shoot", "otherguns:shoot"]}
  }
}

A name without a namespace gets the namespace of the folder the rule file is in (for rules from code, your mod id): aiming in assets/examplemod/punchy_rules/... is examplemod:aiming. A pack-defined condition is used like any other, by the states of clip variations, by when on entries and by other definitions; one that refers back to itself is false.

A pack-defined moment is made from other moments or from a condition turning on or off, and then it reaches every moment listener and every binding like any other moment:

  • on or any: a moment name or a list of them (Punchy!‘s own bare names, or namespaced ones). The new moment fires when one of them fires, with its hand and value.
  • starts or ends: a condition. The moment fires when the condition turns on (starts) or off (ends); never in the first second in a world or after a respawn, while what the server says of the new player arrives.
  • With on or any, min_value and max_value: only values in this range make the moment, and when: a condition that must hold.

Camera kicks

{"camera_kicks": [{
  "on": "examplegun:shoot",
  "turn": [-2, 0.5, 0],
  "move": [0, 0, 0.02],
  "random": [0.4, 0.4, 0],
  "seconds": 0.15,
  "easing": "easeOutCubic",
  "scale": 1.0,
  "scale_by_value": false,
  "min_value": 0,
  "when": "first_person"
}]}

A kick is one impulse of the first-person camera that eases back to zero. turn is pitch, yaw and roll in degrees, in Minecraft’s own sign (a negative pitch looks up, a positive yaw turns right, a positive roll tilts the view clockwise); move is x, y, z in blocks, in view space (x to the right, y up, z toward you); random adds up to that much random turn on each axis; seconds is the length (0.02 to 5, default 0.2); easing is one of Punchy!‘s easing names (default easeOutCubic); scale multiplies the kick, and scale_by_value also multiplies it by the moment’s value. on is the moment; min_value, max_value and the gates apply. Kicks add to Punchy!‘s own camera animation, only while Enable Camera Animations is on; at most 32 run at once, and together they are capped at 30 degrees and half a block on each axis. camera_kick on a moment binding (below) takes the same keys, without on, min_value, max_value and the gates.

Compat entries: moment bindings, gates, camera kicks, mod options

Compat files are the JSON files a pack keeps in assets/minecraft/punchy/compat/ (or assets/punchy/compat/) to tell Punchy! which clips to play for which items. Version 3 adds these keys to them. A Punchy! older than version 3 ignores every one of them (an entry without key that binds a moment simply never plays), so a pack stays valid on older versions.

Moment bindings

A triggerable entry that has on plays when that moment happens, for the items held:

{
  "itemSpecific": {
    "examplegun:rifle": {
      "customAnimation": [
        {"type": "triggerable", "on": "examplegun:shoot", "animation": "rifle_shoot", "priority": 5, "cooldown": 0.15,
         "camera_kick": {"turn": [-1.5, 0.3, 0], "seconds": 0.12}},
        {"type": "triggerable", "on": ["examplegun:reload_start", "examplegun:reload_restart"], "animation": "rifle_reload",
         "hand": "main"},
        {"type": "triggerable", "on": "attack", "min_value": 1, "animation": "rifle_bash"}
      ]
    }
  }
}
KeyMeaning
ona moment name, or a list of them: the entry plays when one fires. Punchy!‘s own moments have bare names (attack, hurt)
animation (or name)the clip, as for any triggerable entry
priorityan integer, default 0; when several bindings match in one tick, the highest priority plays
cooldownseconds, default 0; the binding does not play again for this long (kept per hand)
min_value, max_valuethe binding applies only to moments whose value is in this range
handevent (the default: the moment’s hand, or both hands when the moment has none), main, off or either
camera_kicka camera kick (the keys above) that runs with the binding; a binding with a kick and no clip only kicks
sounda sound (a name, or an object with effect and the options of a clip’s sound, see Sounds) that plays with the binding, also when the binding has no clip

Punchy! looks at the items held in each hand: the entries it would use for those items (their items, when and unless, world filters and the player’s Mix Packs choice) are the candidates. One binding plays per tick: the highest priority over every moment of the tick, the first fired on a tie. The clip plays through Punchy!‘s triggerable machine, so its priority, blending and sounds are those of any triggerable clip, and onClip reports it with the reason moment:<name>. Input moments that Punchy! records for the binding’s own clip do not bind again. An entry with no id gets one made for it. A mod that animates a moment itself can ask momentBound and leave the effect to the pack.

Selector groups

An entry of the compat file’s item map (the one that holds groups such as sword) may say which items it stands for with a match object, instead of a name or a pattern:

"item": {"mypack:guns": {"match": {"tags": ["c:tools/guns"], "classes": ["com.example.guns.api.GunItem"],
                                   "items": ["examplemod:*_rifle"], "requires_mods": ["examplemod"]},
                         "kind": "CROSSBOW", "transforms": {"right": {"offY": -0.05, "rotX": -10}},
                         "customAnimation": [{"type": "triggerable", "on": "guns:reload_start", "name": "rifle_reload"}]}}

At each load of Punchy!‘s tuning every registered item that matches gets the group’s data as its own item entry, as Punchy!‘s pattern entries do, unless an entry for it exists already or an earlier group took it. Earlier means groups that name exact ids first, then classes, then tags, then namespaces and globs, and by key within one kind. kind defaults to INGREDIENT. The expanded entries are ordinary item entries for Mix Packs, profiles and the Hand Editor. The match object takes items, tags and classes as a selector does, and requires_mods (the group is left out while a mod is missing); the keys that narrow by the stack do not apply to a group, which expands per item. On Minecraft 26.x the item tags are bound when a world loads; Punchy! reads its tuning again then, and tag groups fill in. Items another mod registers after a load wait for the next reload. One group poses every gun of a mod by its interface, and a pack can still pose any one of them by name.

Gates on entries

{
  "itemSpecific": {
    "examplegun:rifle": {
      "requires_mods": ["examplegun"],
      "customAnimation": [
        {"type": "idle", "animation": "rifle_idle_loaded", "when": "examplegun:loaded@main"},
        {"type": "idle", "animation": "rifle_idle_empty", "unless": "examplegun:loaded@main"},
        {"type": "attack", "animation": "rifle_fire", "requires_mods": ["examplegun"],
         "variations": [{"animation": "rifle_fire_scoped", "states": ["examplegun:scoped", "!sprinting"]}]}
      ]
    }
  }
}
  • when and unless on any customAnimation entry: the entry counts while when holds and unless does not. Each takes a condition (a name, a list that must all hold, or a condition object). An item can have several entries of one type when their gates differ, as in the example (an idle while loaded and another unless loaded); Punchy!‘s merge with the default animations keeps them apart. An entry without gates still takes the place of the default entry of its type, and an entry that needs a mod no one has does not hide the plain one.
  • requires_mods on an item map entry, an itemSpecific entry, a customAnimation entry or a variation: it counts only when all those mods are loaded. A Punchy! older than version 3 ignores the key and keeps the entry, so write such an entry so that it is harmless without the mod.
  • states and not_states of a clip variation take the same condition forms: a namespaced name, an item condition with a hand, a ! before any form, or one of the inline forms (held=..., worn=..., mod=..., after=...). On a Punchy! older than version 3 those read as false, so the variation does not apply.

Pose variations

On an item, itemSpecific or emptyHand entry of a compat file:

"poses": [{"states": ["crouching", "using_item"], "transforms": {"right": {"offY": -0.05, "rotX": -12}}, "armTransforms": {"right": {"rotX": -8}}, "blend": 0.25, "priority": 2},
          {"states": ["mypack:aiming"], "mode": "replace", "transforms": {"right": {"offX": -0.12, "rotY": 4}}, "blend": 0.15}]

While every state of a pose holds, its values add to the item’s and the arm’s resolved pose (scale multiplies), or set them with "mode": "replace". states are any conditions. The highest priority that holds is the target, eased over blend seconds (0.2 by default) for each hand. Keys are scale, offX, offY, offZ, rotX, rotY, rotZ and, for the item, pivotX, pivotY, pivotZ, per left and right. An item key is a Punchy! kind; an itemSpecific key is a plain item id. Clips that drop tuning drop the variation with it. The Hand Editor edits the base pose only, and profiles do not save these.

Options for mods

{
  "modOptions": {
    "examplegun:recoilScale": 0.5,
    "examplegun:showReloadBar": false,
    "examplegun:tint": "#FFAA00"
  }
}

A top-level modOptions object in a compat file sets options a mod reads with packOption, packFlag and packNumber. Keys are namespaced by the reading mod. Per key the highest pack wins. A pack uses it to tell a mod how to behave with the pack: less recoil because the pack’s clip already kicks, no reload bar because the pack animates it.

Pack notices

A compat file may ask Punchy! to show the player one card about the pack:

{
  "recommendedSettings": {"general.camera_animations": true, "general.speed": 4.0},
  "requiresPunchyApi": 3,
  "requiresMods": ["examplemod"],
  "conflictsWithMods": ["examplefirstperson"],
  "packNotice": {"title": "mypack.notice.title", "text": "mypack.notice.text"}
}
KeyWhat the card says
recommendedSettingsThe settings the pack plays best with, as menu row ids and values (see “The player’s settings”). Values are plain: true or false, or a number. The card names every setting it offers, with its value, and offers Turn on and Not now. A card offers up to eight settings; Turn on writes exactly the ones the card names.
requiresPunchyApiThe API version the pack needs. When this Punchy! has an older one, the card says so.
requiresModsMod ids the pack needs. The card names the ones that are not installed.
conflictsWithModsMod ids the pack does not work with. The card names the ones that are installed.
packNoticeYour own title and text. Each is a language key a pack defines, or plain text. Without a title the card is named after the pack.

How it behaves:

  • One card for each change. Punchy! shows a pack’s card at the first quiet moment in a world (nothing on the screen, no loading) after the pack is first used, and again only when one of these keys changes, or when the mods the pack needs or clashes with change (a conflicting mod installed later brings the card; so does a required mod that goes missing). It remembers the cards the player answered in config/punchy/pack_notices.json, so a card never comes back at every start. A card counts as answered when the player presses either button or Esc. If something else takes the screen first (a server opening a container, a death screen), the card is not counted and comes back at the next quiet moment.
  • Nothing changes by itself. Turn on writes the values on the card the way the menu does when the player changes the rows. Not now, closing the card or just playing leaves every setting as it is. A Space or Enter that was held down when the card opened (flying up, swimming up), or is still held after an answer, does not press a button, so the key repeat cannot choose for the player. Punchy! has no undo for Turn on: the player puts a row back in the menu. A setting that the game reads only when resources load shows after the player reloads (F3+T), and the card says so.
  • Nothing to say, no card. A pack whose recommended settings the player already has, whose mods are all in place and whose API is old enough shows nothing, and Punchy! remembers that too, so turning a setting off later does not bring the card up.
  • Several packs. Each pack with something to say gets its own card, the highest pack in Minecraft’s list first. When two packs recommend the same row, the higher pack gets it and the lower pack’s card leaves it out.
  • Values that do not fit are left out. A row id Punchy! does not have, a number for a switch, a switch for a slider, a number outside the slider’s range, or a list or object in place of a value is skipped (and logged); the rest of the card is shown. A slider value is rounded to the slider’s step.
  • Several files. A pack may spread these keys over more than one compat file. Files are read lowest first: for requiresPunchyApi, for each row of recommendedSettings and for each field of packNotice the later file says over the earlier one, and the mod lists add up.
  • Text. The pack’s own text is shown as you wrote it or translated by its language key, and a percent sign in it is shown as it is. The lines Punchy! adds (the settings, the missing mods, the conflicts, the API) are in English and come first; your text comes last, because a long text is cut from its end and the settings must stay in view.
  • Packs inside mods and servers. Forge on 1.20.1 hands all mod resources to the game as one pack, so each compat file in a mod jar gets a card of its own, named by the file unless it has a title. On 1.20.1 every server’s pack has the same id; a card is remembered by what it says, so two servers’ cards are each shown once.
  • Older Punchy!. A Punchy! older than version 3 ignores all five keys, so a pack stays valid there. A mod can ask supports("pack_notices").

Pack presets

A pack can offer named setups: assets/<namespace>/punchy/presets/<name>.json.

{"title": "Cinematic",
 "settings": {"general.camera_animations": true, "effects.particles": false},
 "tuning": {"globalItem": {"right": {"offY": -0.02}}},
 "mix": {"<a Mix Packs slot>": "<one of its options>"}}

They appear at the end of F8’s Profiles as From <pack>: <title>. Load applies the setting rows, hand poses and Mix Packs choices as a saved profile does. Since a preset never becomes the profile in use, its hand poses last until the next resource reload or profile load; save the profile under a name to keep a copy of your own. A preset cannot be deleted and goes away with the pack. settings takes the F8 row ids only; anything else is left out with a line in the log. tuning is in the format of a hand pose profile, with whole poses.

Punchy!‘s menus

A setting off for a while: restrictSetting

PunchyApi.restrictSetting("examplemod", "general.camera_animations", ExampleModClient::inCutscene);

While the condition is true the feature reads as off, for that while only and never saved. It can only turn a feature off, never on. F8 locks the row and says a mod needs it off. Only these rows: general.camera_animations, effects.particles, effects.parts_glow, effects.armor_sleeves, effects.fire_arms, effects.freeze_shake, effects.freeze_snow, effects.mud, effects.burn, world.arm_physics, world.item_physics and world.rp_model_parts. Any other is ignored, with a line in the log. One row per id, so a second row goes under a child id such as examplemod/mud.

Rows on Punchy!‘s tabs: menuRows

PunchyApi.menuRows("examplemod", "compat", null, tab -> tab.toggle("spell_casting", Text.literal("Use Punchy for spell casting"), Text.EMPTY,
        ExampleConfig::punchyCasting, ExampleConfig::setPunchyCasting, true));

Adds a mod’s own rows to one of Punchy!‘s tabs (general, movement, effects, world or compat), right after a row such as camera_animations, or at the end of the tab when the row is null or is not on that tab. They come under a line with the id’s name. The builder is the one of Punchy!‘s menu API, and it is called each time the menu is opened. The values are the mod’s own to keep. A mod’s own tabs still go through PunchyMenus.register.

Targets in the Hand Editor: editorTarget

PunchyApi.editorTarget("examplemod/glove", "Glove", new PunchyApi.PoseTarget() {
    @Override public float[] read(boolean leftArm) { return GloveConfig.pose(leftArm); }
    @Override public void write(boolean leftArm, float[] pose) { GloveConfig.setPose(leftArm, pose); }
});

The Hand Editor lists the target beside Item, Arm and Line. Its ten sliders read and write through it, in the order scale, offset x, y, z, rotation x, y, z, pivot x, y, z. Mirror copies to the other side. Save stores the values with the profile in use, in a file of the profile’s name in the folder extras beside the hand pose profiles, and loading that profile writes them back. Leaving without saving restores them. packValues is what Reset puts back, and appliesNow says whether there is anything to edit for that hand now.

Clip markers

A timeline token with a namespace in a first-person clip is a marker:

"rifle_reload": {
  "animation_length": 2.4,
  "timeline": {
    "0.0": "use_keyframes_v2;anim_speed_1;",
    "0.9": "examplegun:magazine_out;",
    "1.7": "examplegun:magazine_in;"
  },
  "bones": { "...": {} }
}

When the clip’s time passes the token, a moment named by the token (examplegun:magazine_in) is delivered: source() is "marker", clip() is the clip’s name, value() is the marker’s time in the clip, and hand() is the arm the clip is on (NO_HAND for a clip on both arms). Mods hear markers with onMoment, and packs use them like any moment: as bindings, camera kicks, moment definitions or particle pulses. A mod times its sounds and effects to the animation by naming a marker; the animator moves the marker in the animation software. Tokens of Punchy!‘s own item-id families (hide_left_switch_item_ and its relatives) are not markers. An older Punchy! ignores the token.

Markers reach listeners at the end of the tick that found them, and the data at the start of the next, so a marker never starts a clip in the tick it was found.

A particle emitter’s on_animation can name a moment, written with a time as a clip’s name is, "on_animation": "examplegun:shoot@0", and the emitter pulses when that moment is delivered (a name ending in _left or _right pulses only for that arm’s hand moments). The time is what makes it a trigger: without one, Punchy! reads the emitter as having no trigger and it emits all the time. An older Punchy! waits for a clip of that name, which never plays.

The moments Punchy! has

Punchy!‘s own moments have bare names. Input moments can be canceled; state edges are found at the start of each tick by comparing the local player’s state with the last tick’s. No edge fires on the first tick in a world, after a respawn or after a dimension change.

GroupNamesHandValue or detail
Input (cancelable)attackyesvalue 1 for a critical candidate
useyesdetail use_item, use_item_on, drop_item, interact or interact_at
mine_start, mine_stopmain
inspectmain
triggeryesdetail: the trigger id
State edgeshurt, healnonevalue: health lost or gained
jump, landnoneland: value, the blocks fallen
respawn, level_upnonelevel_up: value, the new level
item_breakyes
swap_hands, slot_changenoneslot_change: value, the new slot
enter_water, leave_water, submerge, surfacenone
ignite, extinguishnone
screen_open, screen_closenone
sleep_start, sleep_endnone
Playedcritical_hityesa swing that is a critical hit
bow_shotyesvalue: the draw time in seconds
crossbow_shot, trident_throwyes
shield_blockyesthe hand of the arm that holds the shield; needs Arm Physics on, as Punchy!‘s own block impact does; no clip
consume_startyeswhen the eating or drinking clip of an item that was not being consumed starts
equip_out, equip_inmainas the hand-out and the hand-in begin
put_out_fire, wake_up, dry_outnone
spear_stageyesvalue: the charge stage (1 to 3); Minecraft 1.21.11 and newer

The played moments are made when Punchy!‘s own machine starts the clip for them, so they tell a mod or a pack what Punchy! is showing; they cannot be canceled. A machine that finds no clip in the player’s packs plays none and makes none: the bow’s release clip, for one, is the use_bow_shot entry of a bow in a pack’s compat file, and Punchy!‘s own pack has none, so bow_shot is made only while a pack gives the bow one. Equally, a hand-out is made only while the hand-out plays. A listener hears them at the next tick start with clip() set to the clip that plays then (shield_block has none). A pack binds them as it binds any moment: a camera kick on bow_shot, a sound on equip_in, a particle burst on critical_hit. Nothing is made while no listener, rule or binding wants moments.

Moments mods fire carry a namespace. Moments packs define carry the pack’s namespace. Markers carry the namespace of the token.

The conditions packs can use

A condition is written as text where Punchy! takes a condition, and as JSON in the places that take an object.

Punchy!‘s own state names (unchanged): crouching or sneaking, walking or moving, sprinting, idle or stationary, jumping or rising, falling, on_ground or grounded, in_air or airborne, swimming, underwater, climbing, riding, flying, gliding, burning, hungry, low_health or low_life, block_above or under_block, block_below or on_block, raining, thundering, day, night.

New built-in names: minecraft_hands (Punchy! draws through Minecraft’s hand code now), first_person, using_item (any hand uses an item), using_main_hand, using_off_hand, in_lava, in_rain (rain falls on the player), wet (in water or in rain), frozen (freezing in powder snow), invisible, left_handed and screen_open (a screen is open).

Forms:

FormMeaning
examplespells:castinga condition a mod published or a pack defined; false when nothing registered it
examplegun:loaded@main, @off, @anyan item condition of a mod, read for the item in that hand
!<any form>not
held=<sel>, main_hand=<sel>, off_hand=<sel>an item is held (either hand, main hand, off hand): an id, a glob, a namespace or a #tag, several joined with |
worn=<sel>, worn.head=, worn.chest=, worn.legs=, worn.feet=an item is worn (in any slot, or that slot)
mod=<mod id>the mod is loaded
effect=<id>, effect=<id>@<level>the player has the effect, at that level or higher (level 1 is the plain effect, @2 is level II): effect=minecraft:speed@2
dimension=<id>the player is in the dimension; several ids joined with |: dimension=minecraft:the_nether|minecraft:the_end
vehicle=<entity id>the player rides that entity type; several joined with |: vehicle=minecraft:boat|minecraft:horse
after=<moment>@<seconds>true for that many seconds after the moment last fired

The = forms contain a character no id contains, so they cannot clash with a mod’s namespace. Text conditions are parsed once per distinct string and kept.

JSON. A condition is a name (a string), a list (all must hold) or an object whose keys must all hold:

{"all": ["crouching", "!sprinting"], "any": [{"held": {"tags": ["c:tools/guns"]}}, "examplemod:armed"],
 "not": "burning", "states": ["examplespells:casting"], "not_states": ["swimming"],
 "after": "examplegun:shoot", "for": 0.5,
 "held": {"items": ["minecraft:bow"], "hand": "main"}, "main_hand": {"tags": ["minecraft:swords"]},
 "off_hand": {"items": ["minecraft:shield"]}, "worn": {"items": ["examplemod:gloves"], "slot": "chest"},
 "mods": ["examplemod"]}

all, any and not take conditions; states and not_states take names; after with for is the moment form; held, main_hand, off_hand and worn take selectors (worn takes slot, held takes hand); mods lists mod ids that must be loaded; expr takes an expression text and holds while it is not 0 (see Numbers: variables, queries and expressions): {"expr": "v.examplemod.charge >= 0.95 && q.is_sneaking"}. Punchy!‘s own variation keys (health_below, health_above, health_percent_below, food_below, food_above, min_fall_distance, in_water, inside_water, weather_detection, time_detection, block_detection, biome_detection) keep their meaning inside an object, with a block or biome scan limited to a radius of 4. A key Punchy! does not know makes its condition false with one line in the log, so a typo never matches by accident.

A mod’s pulled condition is asked at most once per 50 ms and only while something refers to it. Answers a rule or a gate used are kept for 50 ms.

Mods Punchy! supports by itself

Punchy!‘s own support for other mods goes through the same API and the same rule format. Its registrations are named punchy/<name>, so a mod can take one away with PunchyApi.remove("punchy/<name>") (also before Punchy! has drawn its first frame), and a rule file can take one away with take_away, for as long as its pack is on. The player can turn all of them off, with Punchy!‘s built-in item rules, with the Built-in Mod Support setting on the Compat tab (on by default); mods’ own registrations and rule files stay.

Hands through Minecraft

While one of these mods is installed, Punchy! draws its arms through Minecraft’s hand code, so the mod’s hand shows on Punchy!‘s arms and follows its animations:

NameModWhat shows
punchy/figuraFigurathe avatar’s arm
punchy/cpmCustomizable Player Modelsthe model’s arm
punchy/yes_steve_modelYes Steve Model (and OpenYSM)the model’s arm
punchy/better_player_modelBetter Player Modelthe model’s arm
punchy/artifactsArtifactsits gloves
punchy/aetherThe Aetherits gloves
punchy/ismahI See My Armored Handthe armored sleeves; Punchy! leaves the armor on the arms to it
punchy/accessoriesAccessoriesan accessory (or a shown cosmetic) whose renderer draws in first person, such as rings and gloves; only while one is worn
punchy/curiosCurios (NeoForge 1.21.5 and newer)a shown curio whose renderer draws on the first-person hand; only while one is worn
punchy/create_backtankCreate (Forge 1.20.1, NeoForge 1.21.1, Create Fabric 1.20.1)the netherite backtank’s dark diving suit sleeve, which Create draws in place of the skin; only while it is worn in the chest slot

Hands left to Minecraft while another mod draws them

NameModWhen
punchy/playeranimatorKosmX’s playerAnimator, and every mod built on it: Iron’s Spells ‘n Spellbooks and its addons, Spell Engine and its addons (1.20.1, 1.21.1), Better Combat, Hackers n Slashers 1.1while an animation draws the player’s model in first person
punchy/player_animation_libraryPlayer Animation Library, and every mod built on it: Spell Engine (26.x), Better Combat, Dap Ur Homies, Hackers n Slashers 2.0while an animation draws the player’s model in first person, once its blend from Minecraft’s hands is done
punchy/epicfightEpic Fightin Epic Fight mode with its animated first-person model, unless a held item asks for Minecraft’s first person
punchy/mowziesmobsMowzie’s Mobswhile one of its abilities plays its own first-person animation
punchy/carryonCarry Onwhile carrying a block or an entity
punchy/simplebedrockmodelTaCZ, SuperbWarfare and LRTactical (their shared gun library)while the library draws a gun in first person, its put-away included
punchy/cameraany mod that views through another entity (Ars Nouveau’s scrying, security cameras)while the first-person camera follows an entity other than the player

Arms and flames

NameWhat
punchy/invisibilityan invisible player has no first-person arms, as in Minecraft; held items still show
punchy/prometheusPrometheus API and Soul Fire’d: a burning player’s arms show the flames of their fire type (blue soul fire and every fire a mod adds through them)

Built-in item rules

Written in the rule-file format and kept inside Punchy!:

  • Both hands left to Minecraft: TaCZ guns and its addons’ items (com.tacz.guns.api.item.IGun, com.tacz.guns.api.item.IAnimationItem), SuperbWarfare guns, Vic’s Point Blank guns and throwables, Iron’s Artifice guns.
  • The holding hand left to Minecraft: Create’s shootable gadgets and arm-pose items (potato cannon, handheld worldshaper, extendo grip, and every item of an addon built on them), Power Grid’s portable saw, drill and electrozapper, Simulated’s plunger launcher, Create Gunsmithing’s guns, hammer and grenade, Create Diesel Generators’ chemical sprayers, and Exposure’s camera and photographs.
  • Minecraft’s brush, whose sweep is drawn in Minecraft’s hand code, is left to Minecraft too, unless a pack or the hand pose profile in use has an itemSpecific entry for that brush: then Punchy! keeps it and the pack’s clips and pose play.

Nothing is needed for Ars Nouveau beyond punchy/camera (it draws no hands of its own), or for Cobblemon Mega Showdown and Cosmetica, which draw nothing on the first-person hand.

What this version leaves out

  • Per-frame phases for onTick: only "start" and "end". Use onFrame for what happens each frame.

Left out on purpose, since they would let code stand in for animation packs: clips handed over from code, easing curves from code, canned arm motion from code (armKick), and camera kicks or shakes from code (cameraKick, cameraShake). Packs make the motion; mods say when.

Last checked against the mod on 7 November 2026.