Skip to content

Latest commit

 

History

History
634 lines (475 loc) · 15.4 KB

File metadata and controls

634 lines (475 loc) · 15.4 KB

McFabScript — Scripting Reference

McFabScript is a small domain-specific language built into McFabProxy for automating packet fabrication workflows. It is designed specifically for exploit research: sending sequences of packets, waiting for server responses, inspecting field values, and reacting conditionally — all at millisecond precision.

Scripts run server-side inside the proxy process, with direct access to the live session, the packet capture stream, and the registry. There is no need to round-trip through the browser.


Table of contents

  1. Overview
  2. Running scripts
  3. Language basics
  4. Variables
  5. Operators
  6. Control flow
  7. Functions
  8. Built-in functions
  9. The packet object
  10. The timed_out variable
  11. Registry integration
  12. Examples
  13. Limitations

Overview

McFabScript is:

  • C-style — curly braces, semicolons, familiar operators
  • Dynamically typed — one var keyword, values adapt at runtime
  • Async-capablesleep() and wait_for() yield to the Tokio runtime without blocking the proxy relay
  • Registry-awaresend() looks up packets by name from the live registry; wait_for() matches incoming packets by name and decodes their fields

Scripts run in a dedicated background task. The proxy continues relaying packets normally while a script executes. Sending a packet from a script and the proxy forwarding a client packet can happen concurrently.


Running scripts

Web UI

Open the Script tab. Write or paste your script in the editor pane. Click ▶ Run to execute. The button turns red (■ Stop) while a script is running. Output from log() and errors appear in the output panel below. Click ■ Stop to abort a running script at the next sleep() or wait_for() call.

API

# Run a script
curl -X POST http://127.0.0.1:25568/api/script/run \
  -H 'Content-Type: application/json' \
  -d '{"code": "log(\"hello\"); sleep(500); log(\"world\");"}'

# Stop a running script
curl -X POST http://127.0.0.1:25568/api/script/stop

# Check status
curl http://127.0.0.1:25568/api/script/status

Only one script runs at a time. Submitting a new script while one is running stops the old one first.


Language basics

Every statement ends with a semicolon. Blocks use curly braces. Line comments start with //.

// This is a comment
var x = 42;          // variable declaration
x = x + 1;          // assignment (x must already be declared)
send(swing_arm, hand=0);  // call a built-in

Whitespace and indentation

Whitespace is ignored. Use whatever indentation you prefer — the language does not enforce it.

Reserved words

var, if, else, repeat, times, function, void, raw, true, false

raw is reserved as a sentinel for send(raw, ...) and cannot be used as a packet name in the registry.


Variables

var name = expression;

Variables are dynamically typed. There is one type keyword: var. Values can be numbers, strings, or booleans — the type changes freely.

var x = 5;
var msg = "hello";
var flag = true;
var result = x * 2 + 1;

Assignment (to an already-declared variable):

x = x + 1;
msg = "world";

Assigning to a name that was never declared with var is a runtime error.

Unquoted identifiers as strings

Inside send() and wait_for() calls, unquoted packet names are resolved as string literals if the name does not match a declared variable. This enables the ergonomic syntax send(swing_arm, hand=0) without quoting swing_arm. If you have a variable named the same as a packet, quote the packet name or rename the variable.

send(swing_arm, hand=0);           // "swing_arm" resolved as string
var swing_arm = "off_hand_swing";
send(swing_arm, hand=1);           // variable takes precedence: name = "off_hand_swing"
send("swing_arm", hand=0);         // explicit string — always the packet name

Operators

Arithmetic

Operator Meaning
+ Addition (numbers) or string concatenation (when either side is a string)
- Subtraction
* Multiplication
/ Division (runtime error on divide-by-zero)
% Remainder

Comparison

Operator Meaning
== Equal (null==null, bool==bool, number≈number, string==string)
!= Not equal
< Less than
> Greater than
<= Less than or equal
>= Greater than or equal

Logical

Operator Meaning
&& Logical AND (short-circuits)
|| Logical OR (short-circuits)
! Logical NOT

Bitwise

Operator Meaning
& Bitwise AND
| Bitwise OR
^ Bitwise XOR
~ Bitwise NOT (unary)
<< Left shift
>> Right shift (arithmetic)

Bitwise operators cast both operands to 64-bit integers before operating.

Precedence (high to low)

~  !  -  (unary)
*  /  %
+  -
<<  >>
&
^
|
<  >  <=  >=
==  !=
&&
||

Use parentheses to override: (a + b) * c.


Control flow

If / else if / else

if (condition) {
    // ...
} else if (other_condition) {
    // ...
} else {
    // ...
}
var hp = 3;
if (hp <= 0) {
    log("dead");
    exit();
} else if (hp < 10) {
    log("low health: " + hp);
} else {
    log("healthy");
}

Repeat loop

repeat <count> times {
    // body
}

count can be a variable or expression. The loop runs exactly that many times (truncated to a non-negative integer).

repeat 5 times {
    send(swing_arm, hand=0);
    sleep(100);
}

var n = 10;
repeat n times {
    send(use_item, hand=0);
    sleep(50);
}

There is no break, continue, or while. Use exit() to stop execution entirely, or structure your logic with repeat + if.


Functions

function name(param1, param2) {
    // body
}

function no_args(void) {
    // body
}

Functions have no return value. They execute for their side effects (sending packets, logging, sleeping). Functions can call other functions and built-ins. Recursion is supported but there is no stack depth guard — keep it shallow.

function spam(count, delay_ms) {
    repeat count times {
        send(swing_arm, hand=0);
        sleep(delay_ms);
    }
}

function greet(name) {
    log("hello " + name);
}

greet("world");
spam(5, 100);

Function definitions can appear anywhere in the script. They are collected before execution begins, so you can call a function before its definition.

Parameters: positional only. Extra arguments are ignored; missing arguments default to null.


Built-in functions

send(packet_name, field=value, ...)

Send a named packet from the registry using the current protocol state.

send(swing_arm, hand=0);
send(chat_message, message="hello server");
send(player_action, action=1, location=0, face=0);

Field values are passed as named arguments matching the registry field names. Missing fields produce a runtime error. Extra fields are ignored.

The current protocol state (play, login, etc.) determines which registry section is searched. If the packet name is not found in the current state, a runtime error is raised.

send(raw, id, hex)

Send a raw packet by numeric ID and hex payload, bypassing the registry.

send(raw, 0x1b, "01");            // swing_arm with hand=1 (off hand)
send(raw, 0x2c, "00000000");      // raw keep-alive response

id is a number (decimal or 0x hex). hex is a string of hex bytes without spaces. An empty string sends a packet with no payload.

sleep(ms)

Pause execution for the given number of milliseconds. The proxy continues relaying packets during the pause.

sleep(500);     // 500 ms
sleep(50);      // 50 ms

All time values in McFabScript are in milliseconds.

wait_for(packet_name, timeout=ms)

Wait for a packet with the given name to pass through the proxy (either direction). Blocks until a matching packet is captured or the timeout elapses.

wait_for(entity_status, timeout=3000);

if (timed_out) {
    log("no entity_status received within 3 seconds");
    exit();
}

log("entity id: " + packet.entity_id);

On success:

  • timed_out is set to false
  • packet.field_name gives access to the decoded fields of the captured packet

On timeout:

  • timed_out is set to true
  • packet.* fields from the previous wait_for remain unchanged

The packet name must exist in the registry for the relevant protocol state. See Registry integration for how to add S2C packet definitions.

timeout defaults to 5000 ms if omitted.

log(value)

Write a message to the Script output panel. Any value (number, string, bool) is accepted and converted to a string.

log("hello");
log(42);
log(x + " items processed");
log(packet.player_id);

Log entries appear in the output panel in real time and are tagged with a timestamp and level ([info], [warn], [error]).

exit()

Stop the script immediately. No further statements execute.

if (timed_out) {
    log("aborting: server did not respond");
    exit();
}

exit() always succeeds — it is not an error condition.


The packet object

After a successful wait_for() call, the captured packet's fields are accessible via packet.field_name:

wait_for(entity_status, timeout=2000);
if (!timed_out) {
    log("entity id: " + packet.entity_id);
    log("status: " + packet.entity_status);
}

Fields are decoded from the raw wire bytes using the registry definition. The decoded value type depends on the field type:

Registry field type McFabScript value
bool Boolean
i8, u8, i16, u16, i32, u32, i64 Number
varint Number
f32, f64 Number
string String
uuid String (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
hexbytes String (hex-encoded bytes)

packet is populated by the last wait_for() call in the current script execution. Accessing a field that does not exist in the packet's registry definition raises a runtime error.


The timed_out variable

timed_out is a built-in boolean that is automatically set by wait_for(). You do not declare it — it is always available.

wait_for(chat, timeout=5000);
if (timed_out) {
    log("no chat message for 5 seconds");
} else {
    log("got chat: " + packet.message);
}

Always check timed_out after wait_for(). If you call wait_for() and the timeout fires, accessing packet.* fields returns values from the previous successful capture (or raises an error if there was none).


Registry integration

McFabScript's send() and wait_for() both look up packets by name from the live registry (registry/774.json). The registry is shared with the rest of the proxy — changes made through the Registry Wizard or /api/registry/add are immediately visible to running scripts.

C2S packets (client → server)

These are the packets you send to the server. The registry ships with handshake, login, and configuration entries. You add play entries by watching the packet stream and using the Registry Wizard.

S2C packets (server → client)

wait_for() captures packets in any direction. To wait for a server response by name, you need a registry entry for the S2C packet. Add it like any other entry — the registry does not distinguish direction at the storage level.

Workflow:

  1. Open the Monitor tab while performing a game action.
  2. Find the S2C packet you want to react to (direction: s2c).
  3. Click Create Registry Entry, name it, define its fields.
  4. Use wait_for(your_packet_name, timeout=2000) in your script.

Examples

Spam swing arm

// Swing arm 20 times with 50 ms gaps
repeat 20 times {
    send(swing_arm, hand=0);
    sleep(50);
}
log("done");

Send and wait for response

// Send a use-item packet, wait for the server acknowledgement
send(use_item, hand=0);
wait_for(block_update, timeout=1000);

if (timed_out) {
    log("no block_update after 1 second");
    exit();
}

log("block pos: " + packet.location);
log("block state: " + packet.block_state_id);

Conditional loop based on server response

var attempts = 0;
var success = false;

repeat 5 times {
    send(interact_entity, entity_id=1, type=0, hand=0, sneaking=0);
    sleep(100);
    wait_for(entity_status, timeout=500);
    if (!timed_out) {
        log("got response on attempt " + attempts);
        success = true;
    }
    attempts = attempts + 1;
}

if (!success) {
    log("entity never responded after " + attempts + " attempts");
}

Timing measurement

// Measure round-trip time for a packet exchange
var start = 0;
// Note: there is no clock function yet — use sleep as a timing anchor

send(player_position, x=0.0, y=64.0, z=0.0, on_ground=1);
wait_for(chunk_data, timeout=5000);

if (timed_out) {
    log("server did not send chunk data");
} else {
    log("chunk received — position: " + packet.chunk_x + "," + packet.chunk_z);
}

Functions for repetitive tasks

function try_interact(entity_id, delay_ms) {
    send(interact_entity, entity_id=entity_id, type=0, hand=0, sneaking=0);
    sleep(delay_ms);
    wait_for(entity_status, timeout=500);
    if (timed_out) {
        log("entity " + entity_id + " did not respond");
    } else {
        log("entity " + entity_id + " status: " + packet.entity_status);
    }
}

try_interact(1, 100);
try_interact(2, 100);
try_interact(3, 100);

Bitwise field manipulation

// Check a bitmask field from a server packet
wait_for(player_abilities, timeout=2000);
if (!timed_out) {
    var flags = packet.flags;
    var is_invulnerable = (flags & 0x01) != 0;
    var is_flying       = (flags & 0x02) != 0;
    var can_fly         = (flags & 0x04) != 0;
    var instant_build   = (flags & 0x08) != 0;
    log("invulnerable: " + is_invulnerable);
    log("flying: " + is_flying);
    log("can fly: " + can_fly);
    log("instant build: " + instant_build);
}

Limitations

Offline-mode only. Scripts send packets through the same session as the regular proxy relay. They share the same restriction: offline-mode servers only (no encryption).

No return values. Functions do not return values. Use a global variable to communicate results out of a function.

No while or break. Use repeat N times with exit() for early termination.

Abort only at await points. Clicking ■ Stop cancels the script at the next sleep() or wait_for() call. A tight loop with no sleep() or wait_for() cannot be interrupted until it yields. Always include sleep() inside hot loops.

No compound assignment. x += 1 is not supported. Write x = x + 1.

No nested functions. Functions are defined at the top level only. Inner function definitions inside blocks are not supported.

Field decoding requires registry entries. wait_for() can only match and decode packets that have a registry definition. Packets without a definition pass through unmatched.

Packet matching is name+state only. wait_for() matches the first packet whose name appears in the registry for the packet's protocol state, regardless of direction. If you have both C2S and S2C packets with the same name in the same state, the first one captured wins.

Single script at a time. Submitting a new script stops any running script first.