Register event handlers, commands, and repeating tasks
Decorators
PyJavaBridge uses Python decorators to register event handlers, commands, and repeating tasks. Your decorated functions are discovered automatically when the script loads.
@event
@event
async def player_join(e):
await e.player.send_message("Welcome!")Register a function as an event handler. The function name determines the event type — it is converted from snake_case to PascalCase and Event is appended. For example, player_join becomes PlayerJoinEvent.
Signature
@event(*, once_per_tick=False, priority="NORMAL", throttle_ms=0)Parameters
once_per_tick
- Type:
bool - Default:
False
When True, the handler is called at most once per server tick, even if the event fires multiple times. Useful for high-frequency events like player_move.
@event(once_per_tick=True)
async def player_move(e):
# Called at most once per tick per event, not every micro-movement
pass
priority
- Type:
str - Default:
"NORMAL"
The Bukkit event priority. Determines the order in which handlers run. Options (lowest runs first):
| Priority | Description |
|---|---|
LOWEST | Runs first. Use for early cancellation. |
LOW | Runs before normal handlers. |
NORMAL | Default priority. |
HIGH | Runs after normal handlers. |
HIGHEST | Runs last before monitor. |
MONITOR | Read-only observation. Do not modify the event. |
@event(priority="HIGH")
async def block_break(e):
# This runs after NORMAL-priority handlers
pass
throttle_ms
- Type:
int - Default:
0
Minimum milliseconds between handler invocations. If the event fires more frequently, extra invocations are silently dropped.
@event(throttle_ms=500)
async def player_interact(e):
# At most once every 500ms, prevents spam-clicking abuse
pass
Bare decorator
You can use @event without parentheses for the default settings:
@event
async def player_quit(e):
await server.broadcast(f"{{e.player.name}} left!")@command
@command("Greet a player", permission="myplugin.greet")
async def greet(e):
await e.player.send_message("Hello!")Register a function as a script command. The command is automatically available as /functionname (or as specified via the name parameter). Command arguments are parsed from the function's additional parameters after event.
Signature
@command(description=None, *, name=None, permission=None, tab_complete=None)Parameters
description
- Type:
str | None - Default:
None
A human-readable description of the command. This is the first positional argument.
@command("Teleport to spawn")
async def spawn(e):
...name
- Type:
str | None - Default:
None(uses function name)
Override the command name. By default the function name is used. If the function name starts with cmd_, the prefix is automatically stripped.
@command("Teleport home", name="home")
async def teleport_home(e):
# Registered as /home, not /teleport_home
...@command("Greet a player")
async def cmd_greet(e):
# Registered as /greet, not /cmd_greet
await e.player.send_message("Hello!")permission
- Type:
str | None - Default:
None
Permission node required to use the command. Players without this permission cannot execute it.
@command("Ban a player", permission="admin.ban")
async def ban(e, target: str):
...tab_complete
- Type:
dict[int, list[str]] | None - Default:
None
Static tab completion suggestions per argument position (0-indexed). Use -1 as a wildcard for all positions without their own entry. For dynamic completions, see the @my_command.tab_complete decorator below.
@command("Set game mode", tab_complete={{
0: ["survival", "creative", "adventure", "spectator"]
}})
async def gamemode(e, mode: str):
...@command("Teleport to a place", tab_complete={{
0: ["spawn", "home", "shop", "arena"],
}})
async def tp(e, place: str):
...Wildcard example — suggest the same options for any argument position:
@command("Tag players", tab_complete={{
-1: ["vip", "admin", "builder", "mod"]
}})
async def tag(e, *tags):
...Dynamic tab completion
For dynamic completions that depend on server state (online players, entities, etc.), use the @my_command.tab_complete decorator:
@command("Teleport to a player")
async def tp(e, target: str):
...
@tp.tab_complete
async def tp_complete(sender, args):
# args is the list of current arguments being typed
players = await server.get_online_players()
names = [p.name for p in players]
# Filter by what the user has typed so far
partial = args[-1].lower() if args else ""
return [n for n in names if n.lower().startswith(partial)]The tab completion handler receives:
- sender — the player or command sender requesting completions
- args — list of current argument strings (the last one is being typed)
It should return a list of suggestion strings. The handler can be async or synchronous.
@command("Give item to player")
async def give(e, player: str, item: str, amount: str = "1"):
...
@give.tab_complete
async def give_complete(sender, args):
if len(args) <= 1:
# First arg: player names
players = await server.get_online_players()
partial = args[0].lower() if args else ""
return [p.name for p in players if p.name.lower().startswith(partial)]
elif len(args) == 2:
# Second arg: item types
items = ["diamond", "iron_ingot", "gold_ingot", "emerald"]
partial = args[1].lower()
return [i for i in items if i.startswith(partial)]
elif len(args) == 3:
# Third arg: amounts
return ["1", "16", "32", "64"]
return []Command arguments
Extra function parameters after the event become command arguments. They are parsed positionally from the player's input:
@command("Give items")
async def give(e, material: str, amount: str = "1"):
# /give diamond 64
# material = "diamond", amount = "64"
await Item.give(e.player, material, int(amount))All arguments are passed as strings. You handle type conversion yourself.
@task
@task(interval=20, delay=0)
async def tick_loop():
...Register a repeating async task. The function is called repeatedly at the specified interval after an optional initial delay.
Signature
@task(*, interval=20, delay=0)Parameters
interval
- Type:
int - Default:
20
Ticks between invocations. Minecraft runs at 20 ticks per second, so interval=20 means once per second, interval=1 means every tick.
delay
- Type:
int - Default:
0
Initial delay in ticks before the first invocation. Use this to wait for the server to fully start.
@task(interval=20*60, delay=20*5)
async def auto_save():
# Runs every 60 seconds, starting 5 seconds after script load
await server.execute("save-all")Notes
- Task functions take no arguments (unlike event/command handlers).
- The task is cancelled automatically when the script is unloaded.
- If the function is still running when the next invocation is due, the next invocation is skipped.
@async_task
from bridge import async_task
@async_task
async def do_work():
await something()Make an async def fire-and-forget safe. The decorated function returns a BridgeCall when called — the coroutine is immediately scheduled as a background task.
Callers can await the result to wait for completion, or ignore it to let it run in the background. This matches the behaviour of built-in bridge API calls.
Signature
@async_task
Usage
@async_task
async def build_arena():
await world.fill(x1, y1, z1, x2, y2, z2, "stone")
# Fire-and-forget:
build_arena()
# Or await:
await build_arena()Notes
- Works on both module-level functions and methods.
- The return value is a
BridgeCall, which is anAwaitable. - Unawaited errors are silently logged (same as unawaited bridge API calls).
@preserve
@preserve
def player_scores():
return {{}}Persist a variable across hot reloads. Decorate a no-arg factory function. On first load, the factory runs and its return value is cached to disk as JSON. On subsequent reloads, the cached value is returned instead.
Signature
@preserve
Usage
@preserve
def kill_counts():
return {{}}
@event
async def entity_death(e):
if e.entity.type.name == "ZOMBIE":
killer = e.entity.source
if killer:
kill_counts[killer.name] = kill_counts.get(killer.name, 0) + 1
Notes
- Only works with JSON-serializable return values.
- Data is stored in
plugins/PyJavaBridge/preserve/<function_qualname>.json. - The value is loaded once at script start and saved on changes.
fire_event
fire_event(event_name, data=None)Fire a custom event that all scripts (including the calling script) can listen to. This enables cross-script communication.
Signature
fire_event(event_name: str, data: dict | None = None) -> None
Parameters
event_name
- Type:
str
The custom event name. Listeners subscribe using the @event decorator with a matching function name (snake_case).
data
- Type:
dict | None - Default:
None
Optional data payload passed to listeners via the event object.
Usage
# Script A: fire a custom event
fire_event("quest_complete", {{"player": player.name, "quest": "dragon_slayer"}})
# Script B: listen for it
@event
async def quest_complete(e):
await server.broadcast(f"{{e.player}} completed {{e.quest}}!")