Skip to main content

The Local Player

session.entityState.localPlayer is an EntityLocalPlayer: you. It extends EntityPlayer (and so AbstractEntity), so it has all the usual entity and player state (position, health, rotation, uuid, gamemode, …). On top of that it adds the actions and controllers below.

const player = ctx.session.entityState.localPlayer;
ctx.on("movement_tick", (state) => state.jump());
Moving the player happens in movement_tick

setPosition, setMotion, addMotion, strafe and jump are no longer on the local player. They live on the MovementState passed to the movement_tick event, which runs before the client moves. See Movement & the Movement Tick and its migration table.

Identity & session state​

MemberTypeDescription
xuidstringYour XUID.
serverAddressstringAddress of the connected server.
gameVersionstringClient game version string.
isInitializedbooleanWhether the local player has finished initializing.
tickbigintThe player's current tick counter.

Movement state​

MemberTypeDescription
moveVectorVector2Raw strafe input vector from the last auth-input packet.
isMovingbooleanWhether movement keys are pressed. false when only coasting.
sneaking / sprinting / swimming / gliding / crawlingbooleanPose and movement flags.
verticalCollisionbooleanCollided vertically last tick.
horizontalCollisionbooleanCollided horizontally last tick.
prevServerSideYawnumberServer-side yaw on the previous tick.
get predictedMotionVector3Predicted motion ignoring collision, i.e. what motion would be with no blocks in the way.
get serverSideRotationVector2The rotation being sent to the server: the committed override, or your real rotation if nothing overrides it. During movement_tick this is still the previous tick's commit, because this tick's isn't decided until every listener has run.

position, motion, rotation and the on-ground state are refreshed from game memory at the start of each movement_tick, before any module runs. They don't come from the auth-input packet. They are a snapshot, though: writing through MovementState doesn't update them, so read live values from the state object instead.

Auth-input data​

The auth-input packet stores its input flags as a list of string tags (InputData), not as a flags object. The local player gives you a live view of the packet currently being handled:

MemberTypeDescription
get inputDatareadonly InputData[]Input data of the auth-input packet being handled. This is a live view, so copy it if you want to keep it.
hasInputData(data)booleanWhether the given tag is present.
addInputData(data)voidAdd a tag. Does nothing if it's already there.
removeInputData(data)voidRemove a tag.
ctx.on("tick", () => {
const player = ctx.session.entityState.localPlayer;
if (player.hasInputData("start_jumping")) {
player.removeInputData("start_jumping");
}
});

InputData is a string union of every input tag ("start_jumping", "start_sprinting", "missed_swing", "vertical_collision", …), and your editor autocompletes them. The values only mean something during the tick event, while the packet is being handled.

Replaces inputFlags

The old inputFlags: InputFlag object is gone. player.inputFlags.start_jumping becomes player.hasInputData("start_jumping"), and player.inputFlags.missed_swing = true becomes player.addInputData("missed_swing").

Action verbs​

These queue actions for the current or next tick. Rotations are Vector2s: build one with new utils.Vector2(...) (see Utilities) or reuse the player's own serverSideRotation.

MethodDescription
swingItem(source?)Swing the arm once this tick. You can tag it with a source string.
sendJumpFlagNextPacket()Make the next auth-input packet read as a jump, the way the client sends one. MovementState.jump() already does this for you. See Jumping.
releaseJumpNextPacket()Make the next auth-input packet show the jump key released for one tick. See Jumping.
click(rotation)Queue an interact/attack click at the given rotation.
missClick(rotation)Queue a deliberate miss-click.
interactEntity(entity, swing, rotation, action, positionOverride?)Attack or interact with an entity. action is "attack" or "interact".
interactBlock(position, face, swingSource?, consumeItem?, clickOffset?, hand?)Use or place against a block face. clickOffset is the point you clicked on the face, relative to the block; it's random if you leave it out. hand is the hand reported to the server: "main_hand" (the default) or "off_hand".
sendChatMessage(message)Send a chat message to the server as the player.
import { EntityNetworkPlayer } from "@protohax/userscript";

ctx.on("tick", () => {
const player = ctx.session.entityState.localPlayer;

// a minimal killaura: hit the first attackable player without changing
// the rotation already being sent to the server
for (const e of ctx.session.entityState.entities.values()) {
if (e instanceof EntityNetworkPlayer && e.canBeAttacked) {
player.interactEntity(e, true, player.serverSideRotation, "attack");
break;
}
}
});

Jumping​

A jump has two sides. Your client moves you up, and the server decides whether it accepts the jump from the flags in your auth-input packet. The local player keeps the two in step.

MemberTypeDescription
get serverJumpDelaynumberThe server's jump delay as of the last packet sent.
get canServerJumpbooleanWhether the server would let the next packet jump. true while serverJumpDelay is 1 or less.
sendJumpFlagNextPacket()voidMake the next packet read as a jump.
releaseJumpNextPacket()voidMake the next packet show the jump key released for one tick.

The server's jump delay​

After a jump off the ground, the server won't take another from a held jump key for 10 ticks. It can't be read from the server, so the local player models it from the packets actually sent. For each packet, in the server's order:

  1. The delay counts down by one, stopping at 0.
  2. If jump is held, the delay is 0, you're on the ground and the packet fires a jump (start_jumping), the delay resets to 10.
  3. If jump isn't held at all, the delay drops to 0.

Because the countdown happens before the check, a delay of 1 still jumps on the next packet. That's why canServerJump is serverJumpDelay <= 1.

MovementState.jump() checks canServerJump before jumping off the ground, so a forced jump never lands inside the server's delay.

sendJumpFlagNextPacket()​

Makes the next packet look like a jump the client sent itself:

  • The jump key is held: jump_down, jumping and jump_current_raw.
  • The jump fires: start_jumping. This is only added when canServerJump is true. Inside the server's jump delay, a real client can't fire a jump either, so only the held key is sent.
  • The raw press edge (jump_pressed_raw) is added if the key wasn't held on the last packet sent.

jumping is what the server itself treats as "jump held", so a server that simulates your movement jumps along with you.

releaseJumpNextPacket()​

Makes the next packet show the jump key released for one tick. It removes jump_down, jumping, jump_current_raw and jump_pressed_raw, and adds the raw release edge (jump_released_raw) if the key was held before. The server zeroes its jump delay, just as if you let go of the key. Your client's own delay isn't touched.

It's skipped when a jump is queued for the same packet, and in water or on scaffolding (auto_jumping_in_water, ascend_block), where the server treats jump as held anyway.

Together with MovementState.setJumpDelay, this lets you shorten the delay on both sides: lower your client's delay after a jump, and send one release so the server's delay runs out on the same tick.

When the flags are applied

Queued jump flags are applied to the packet right after the tick listeners run. So a call from movement_tick or from tick both reach this tick's packet.

Inventories​

The local player owns three containers. The Inventory page covers them in full.

MemberType
inventoryPlayerInventory
inventoryArmorPlayerInventoryArmor
inventoryOffhandPlayerInventoryOffhand

breakingController​

Controls block breaking. Type: AbstractBreakingController.

MemberTypeDescription
breakingVector3 | nullPosition of the block being broken, or null.
canInjectBreak()booleanWhether you can inject a break right now.
startBreak(position, face, range?, callback?)voidStart breaking a block. callback(progress, tickProgress) reports every tick. Pass range: 0 to turn off auto-abort.
abortBreak()voidAbort the current injected break.
forceStopBreak()voidFinish the current break early, as if it completed normally.
const bc = ctx.session.entityState.localPlayer.breakingController;
if (bc.canInjectBreak()) {
// position is any IVector3 ({ x, y, z }); face is a block face index
bc.startBreak({ x: 10, y: 64, z: 20 }, 1, 5, (progress) => {
if (progress === null) console.log("break finished/aborted");
});
}

rotationScheduler​

Schedules server-side rotation overrides (aim). Type: Scheduler<Vector2>.

The winning override is committed once, at the end of each movement_tick. The auth-input packet is then built from that commit, not from a fresh read. Because entries expire on wall-clock timers, this commit is what ties one rotation to one tick. An aim you request in a movement_tick ends up on that same tick's packet.

A SprintIntent works differently. The game's sprint systems run before movement_tick, so a sprint request from there applies on the next tick. Request it from input_tick to have it land on the same packet.

MemberTypeDescription
request(value, priority, provider, expiryMillis)voidRequest a rotation override. The highest priority wins. provider identifies you; expiryMillis is how long the request lasts.
cancel(provider)voidCancel this provider's request.
get currentT | undefinedThe value currently winning.
subscribe(listener)() => voidWatch the current value. Returns an unsubscribe function.

SchedulerPriority is a string enum. You reach it through the utils namespace; it isn't a top-level export:

import { utils } from "@protohax/userscript";
// utils.SchedulerPriority.Lowest | Low | Medium | High | Highest
import { utils } from "@protohax/userscript";

const player = ctx.session.entityState.localPlayer;

// request from `movement_tick` so the aim lands on this tick's packet
ctx.on("movement_tick", () => {
player.rotationScheduler.request(
new utils.Vector2(0, 90), // (pitch, yaw)
utils.SchedulerPriority.High,
ctx, // the requester identity — see below
50,
);
});
ctx.onDisable(() => player.rotationScheduler.cancel(ctx));

provider is compared by identity only, so any stable value works. ctx is the usual choice. It is unique per module instance per session, like the this the built-in modules pass, and it goes away with the session. Only use a separate token (const OTHER = {}) when one module needs two independent requests at the same time. Declare it inside setup so it isn't shared across sessions.

Request the aim from movement_tick, not tick

The commit happens before the auth-input packet is built. tick fires while that packet is being handled, which is after the commit. So a rotation requested from a tick handler only shows up on the next tick's packet. This is why the built-in combat modules aim from movement_tick.