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.
| Name | What it covers |
|---|---|
version_ | version() and supports |
priority | priority |
diagnostics | registrations, debugInfo, the creator inspector in Copy debug info |
budget | budget and the slow report |
player_ | the player’s switch for each mod |
hand_owner | handOwner |
state | state() |
clip_ | clipExists, clipNames, clipInfo, sampleClip |
item_ | kindOf, armKindOf, itemPoseOf, clipFor, moment |
settings_ | setting |
clip_ | onClip |
moments | onMoment, the cancelable input moments |
markers | clip markers as moments |
tick_ | onTick start and end |
reload_ | onReload |
settings_ | on |
play_clip | playClip, stopClip |
drive_clip | driveClip, follow |
fire_ | moment |
punchy_ | attack, use, inspect, trigger, equipCycle, react |
camera | camera, camera_ and camera_ |
item_rig | itemRig, clear |
moment_ | moments as on_ of a particle emitter |
third_ | play |
arm_ | armTexture, the arm_ rule |
arm_ | arm, the arm_ rule |
arm_flames | armFlames, Flames.scale, the arm_flames rule |
draw_ | drawOnArm |
draw_item | drawItem |
hand_ | hand, hand |
movement_ | movement |
pose_ | poses in compat entries |
item_ | item, the display_ rule |
keep_ | keep, the keep_ rule |
setting_ | restrict |
menu_rows | menuRows |
editor_ | editor |
pack_ | punchy/ |
item_pose | itemPose |
movement_ | movement |
off_arm | off |
item_kind | itemKind and the kind rule |
item_roles | itemRole and the item_roles rule |
equip_ | equipRules and the equip rule |
attack_ | attack and the attack rule |
clip_ | filter and the filter_ rule |
clip_speed | clipSpeed |
hide_ | hideLayers, the LAYER_* bits and the hide_ rule |
per_ | use and the arm key of use_ |
hide_hands | hideHands and the hide_ rule |
arm_states | armStates |
hot_items | hotItems and the hot_items rule |
arm_pose | armPose |
external_ | external |
arm_claims | claimArm |
matrices | armMatrix, itemMatrix, camera |
frame_ | onFrame |
particle_ | particles |
compat_ | compatData |
selector_ | a match object on an entry of a compat file’s item map |
stack_ | components, nbt, name, custom_, enchantments, damaged and durability_ in selectors |
played_ | the moments Punchy!‘s machines make as they play a clip (critical_, bow_shot …) |
spear_ | the spear_ moment; false before Minecraft 1.21.11, which has no spears |
equip_data | ignore_, ignore_nbt, changed_ and changed_ in equip rules |
item_ | the on: and while: types of an item’s model |
clip_ | remapClips and the clip_ rule |
clip_ | soundRoute, sound events and options in sound_, sound on bindings |
clip_ | speed, blend_in, blend_ and aliases in animation files, speed on entries |
clip_ | animation files under assets/ |
held_ | held, NO_STACK and the item_ rule |
held_keys | hold and a translated title on triggerable entries |
mix_labels | mixLabel and mix in compat files |
conditions | condition, set, condition, the inline forms |
item_ | item, itemValue |
variables | variable, set, variable and the variables rule kind |
queries | q.<name> in expressions and expr conditions |
expressions | the new names and functions in keyframes, expr conditions |
rules_ | rules |
pack_ | packOption, packFlag, packNumber, modOptions |
rule_ids | a rule’s id and take_away |
gated_ | when, held, worn and requires_ on rule entries |
entry_ | when, unless and requires_ on compat entries |
pack_ | conditions and moments that packs define |
moment_ | on on triggerable compat entries |
pack_ | recommended, requires, requires, conflicts 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:
| Bit | Hides | In this version |
|---|---|---|
LAYER_ | the armor on the arm | acts |
LAYER_BURN | the burn overlay of a hand in lava or fire | acts |
LAYER_ | the freeze overlay | acts |
LAYER_MUD | the mud overlay | acts |
LAYER_ | the sweat overlay (Punchy! 3.0 never draws it) | accepted, nothing to hide |
LAYER_ | the fire on the arm | acts |
LAYER_SKIN | the skin; the sleeve, overlays and armor still draw | acts |
LAYER_ | the sleeve; the arm still draws | acts |
LAYER_ITEM | the item that arm holds; Punchy! still animates it, its particles stop | acts |
LAYER_ | the layers of arm | acts |
LAYER_ARM | everything 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 throwsIllegalArgumentException.onReloadhears"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.onSettingsChangedruns 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 anid; an entry with anidand nokeycan 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 throwsIllegalArgumentException.
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):
| Call | Meaning |
|---|---|
named(clip) | a clip name of the packs’ animation files |
hand(main | the hand it is for (default main); picks <clip>_ or <clip>_ of the player’s arm |
bothArms() | drives both arms, as a dual-handed clip |
speed(f) | a multiplier, default 1 |
blendIn(s), blend | seconds; left to Punchy! when not set |
loop(), hold | loop it, or hold the last frame |
startAt(s) | seconds into the clip |
priority(n) | one of CLIP_ (10), CLIP_ (60), CLIP_ (70, the default), CLIP_USE (80), CLIP_ (90); more than 90 counts as 90 |
item | 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 |
follow | 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.
| Key | Where | Meaning |
|---|---|---|
speed | in a clip | a 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. |
blend_in | in a clip | seconds. 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 a clip | smooth, back or strong_ |
aliases | at 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_ | in a clip | see 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’ssounds.jsondefines that name without a file. A sound event that nothing defines is skipped with one line in the log. volumeandpitchare factors (default 1);pitch_rangeis 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.categoryis master by default, as today. Another category (players,ambient,block…) makes that volume slider apply, and the sound plays at the player.positionalplays 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"]}]}
| Key | Meaning |
|---|---|
changed_, changed_ | a 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_, ignore_nbt | a 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):
| Call | What it does |
|---|---|
move | moves the arm by blocks in view space |
turn | turns the arm by degrees about the pivot, with the axes of view space |
move | moves the arm by blocks along its own axes |
turn | 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(), partial, 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", ...)isexamplespells:casting.whenis 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 asv.main_hand.examplegun.ammoandv.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, sovariable("examplemod", "charge", ...)isexamplemod:charge.valueis 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 withsetVariableis 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.
| Group | Names |
|---|---|
| Clip and settings | anim_time (seconds into the clip, as Punchy! has had it; in a keyframe only, it reads 0 in expr and speed), animation_ (the player’s animation speed setting), is_ |
| Health and needs | health, max_health, health_, food, saturation, air_ (1 full, 0 drowning), armor, frozen (0 to 1), experience_, experience_ |
| Motion | ground_ and vertical_ (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_ (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_ (look pitch in degrees), fall_ |
| States, 0 or 1 | is_, is_, is_, is_, is_, is_, is_on_fire, is_riding, is_gliding, is_ |
| Use and attack | use_ (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_ (0 to 1), attack_ (attacks a second) |
| Held items | main_, off_ (the fraction left, 1 when the stack cannot be damaged), main_, off_, selected_ (0 to 8) |
| World | light_ (0 to 15 at the eyes), rain_level, thunder_, time_ (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) |
| Effects | effect.: the effect’s level (its amplifier plus 1), or 0 without it, for example q. |
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 function | Meaning |
|---|---|
v., variable. | a variable, written with a dot where the id has its colon: v. is examplemod: |
v., v. | 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. | a query |
math. | a at t 0, b at t 1 |
math. | 0 below edge0, 1 above edge1, a smooth ramp between |
math. | 0 below edge, otherwise 1 |
math. | -1, 0 or 1 |
math. | the remainder of a / b with the sign of b, so math. is 2 (0 when b is 0) |
math., math. | powers |
math. | 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"}}}}
| Key | Meaning |
|---|---|
expr | evaluated each tick. Besides the keyframe names it takes comparisons and logic: < <= > >= == != && || !, which give 1 or 0 |
smooth | with expr: seconds to ease toward the target, so a 0 or 1 test becomes a weight that eases poses in and out |
count | a 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, max | limit 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 ascreate:potato_cannon, a glob with*for any run of characters and?for any one character such ascgs:*, or a bare namespace such asexposurefor every item of a mod),tags(item tag ids, with or without#; a tag without a namespace is aminecrafttag) andclasses(binary class names such ascom.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. Useclassesfor 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,tagsandclasses; all the keys it has must hold:
| Key | Meaning |
|---|---|
components | data components the stack has (ids, minecraft: by default): ["examplemod:. Minecraft 1.21.1 and newer |
nbt | top-level keys of the stack’s tag: ["Ammo"]. Minecraft 1.20.1 |
name | a glob on the stack’s custom name, in any case: "*excalibur*" |
custom_ | a number, a range as [min, max] or {"min": 1, "max": 9} (from Minecraft 1.21.4 the first number of the component) |
enchantments | enchantment ids the stack has all of: ["minecraft: |
damaged | true or false |
durability_ | the 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) andrequires_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_apiandrequires_modsapply to the whole rule.requires_modslists mod ids that must all be loaded for the rule (or the entry) to count.requires_apiis 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.idnames the rule’s registrations, soPunchyApi.remove(id)takes the rule away andregistrationsshows it. Without anida rule is namedrules/<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
| Kind | What it does |
|---|---|
leave_ | a hand holding a matching item is left to Minecraft (version 2; never gated) |
leave_ | while either hand holds a match, both hands are left to Minecraft (version 2; never gated) |
use_ | entries 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 use |
hide_arms, hide_ | entries with gates only: as the calls |
disable_ | entries with gates only: Punchy! steps aside while one holds; an entry that reads minecraft_ is skipped, since that depends on Punchy! being on |
leave_ | hand (main, off or both, default both) and gates: that hand is left to Minecraft while the gates hold, whatever it holds |
flames | sprites (two block atlas sprite ids), optional tint (#RRGGBB or #AARRGGBB) and gates |
kind | as (a Punchy! kind name) and a selector: matching items get that kind, as itemKind |
item_roles | role (or not, to keep the item out of the role) and a selector, as itemRole |
attack | unthrottled and/or swings_ and a selector, as attack |
equip | skip: true and a selector: a swap to or from a match goes without hand-out and hand-in, as equipRules |
filter_ | clips (names or globs) and gates: those clips do not play while the gates hold |
hide_ | layers, arm and gates, as hideLayers |
hide_ | a selector for the stack in use, and gates, as hideHands |
hot_items | a selector for the held stacks that warm the hand, and gates, as hotItems |
arm_ | texture, tint, full_, arm and gates, as armTexture |
arm_ | texture, tint, full_, arm, fade_in, fade_out, alpha and gates, as arm |
arm_flames | sprites, tint, scale, arm and gates, as armFlames |
display_ | as, a selector and gates, as item |
keep_ | a selector and gates, as keep |
clip_ | clips (a map of clip name to the clip to find instead), priority and gates, as remapClips |
item_ | held (a selector for the stack that would be drawn), show (none or an item id) and gates, as held |
conditions | an object of condition definitions packs make (below) |
moments | an object of moment definitions packs make (below) |
camera_ | a camera kick for a moment (below) |
take_away | ids 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:
onorany: 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.startsorends: 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
onorany,min_valueandmax_value: only values in this range make the moment, andwhen: 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"}
]
}
}
}
| Key | Meaning |
|---|---|
on | a 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 |
priority | an integer, default 0; when several bindings match in one tick, the highest priority plays |
cooldown | seconds, default 0; the binding does not play again for this long (kept per hand) |
min_value, max_value | the binding applies only to moments whose value is in this range |
hand | event (the default: the moment’s hand, or both hands when the moment has none), main, off or either |
camera_ | a camera kick (the keys above) that runs with the binding; a binding with a kick and no clip only kicks |
sound | a 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"]}]}
]
}
}
}
whenandunlesson anycustomAnimationentry: the entry counts whilewhenholds andunlessdoes 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 (anidlewhile 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_modson an item map entry, anitemSpecificentry, acustomAnimationentry 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.statesandnot_statesof 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"}
}
| Key | What the card says |
|---|---|
recommended | The 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. |
requires | The API version the pack needs. When this Punchy! has an older one, the card says so. |
requires | Mod ids the pack needs. The card names the ones that are not installed. |
conflicts | Mod ids the pack does not work with. The card names the ones that are installed. |
packNotice | Your 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 ofrecommendedSettingsand for each field ofpackNoticethe 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.
| Group | Names | Hand | Value or detail |
|---|---|---|---|
| Input (cancelable) | attack | yes | value 1 for a critical candidate |
use | yes | detail use_item, use_, drop_item, interact or interact_ | |
mine_start, mine_stop | main | ||
inspect | main | ||
trigger | yes | detail: the trigger id | |
| State edges | hurt, heal | none | value: health lost or gained |
jump, land | none | land: value, the blocks fallen | |
respawn, level_up | none | level_up: value, the new level | |
item_break | yes | ||
swap_hands, slot_ | none | slot_: value, the new slot | |
enter_, leave_, submerge, surface | none | ||
ignite, extinguish | none | ||
screen_, screen_ | none | ||
sleep_, sleep_end | none | ||
| Played | critical_ | yes | a swing that is a critical hit |
bow_shot | yes | value: the draw time in seconds | |
crossbow_, trident_ | yes | ||
shield_ | yes | the hand of the arm that holds the shield; needs Arm Physics on, as Punchy!‘s own block impact does; no clip | |
consume_ | yes | when the eating or drinking clip of an item that was not being consumed starts | |
equip_out, equip_in | main | as the hand-out and the hand-in begin | |
put_, wake_up, dry_out | none | ||
spear_ | yes | value: 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:
| Form | Meaning |
|---|---|
examplespells: | a condition a mod published or a pack defined; false when nothing registered it |
examplegun:, @off, @any | an item condition of a mod, read for the item in that hand |
!<any form> | not |
held=<sel>, main_, off_ | 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., 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: |
dimension=<id> | the player is in the dimension; several ids joined with |: dimension=minecraft: |
vehicle=<entity id> | the player rides that entity type; several joined with |: vehicle=minecraft: |
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:
| Name | Mod | What shows |
|---|---|---|
punchy/ | Figura | the avatar’s arm |
punchy/cpm | Customizable Player Models | the model’s arm |
punchy/ | Yes Steve Model (and OpenYSM) | the model’s arm |
punchy/ | Better Player Model | the model’s arm |
punchy/ | Artifacts | its gloves |
punchy/ | The Aether | its gloves |
punchy/ | I See My Armored Hand | the armored sleeves; Punchy! leaves the armor on the arms to it |
punchy/ | Accessories | an accessory (or a shown cosmetic) whose renderer draws in first person, such as rings and gloves; only while one is worn |
punchy/ | Curios (NeoForge 1.21.5 and newer) | a shown curio whose renderer draws on the first-person hand; only while one is worn |
punchy/ | Create (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
| Name | Mod | When |
|---|---|---|
punchy/ | KosmX’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.1 | while an animation draws the player’s model in first person |
punchy/ | Player Animation Library, and every mod built on it: Spell Engine (26.x), Better Combat, Dap Ur Homies, Hackers n Slashers 2.0 | while an animation draws the player’s model in first person, once its blend from Minecraft’s hands is done |
punchy/ | Epic Fight | in Epic Fight mode with its animated first-person model, unless a held item asks for Minecraft’s first person |
punchy/ | Mowzie’s Mobs | while one of its abilities plays its own first-person animation |
punchy/ | Carry On | while carrying a block or an entity |
punchy/ | TaCZ, SuperbWarfare and LRTactical (their shared gun library) | while the library draws a gun in first person, its put-away included |
punchy/ | any 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
| Name | What |
|---|---|
punchy/ | an invisible player has no first-person arms, as in Minecraft; held items still show |
punchy/ | Prometheus 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
itemSpecificentry 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". UseonFramefor 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.