UtilityStone is an essentials style toolkit for Endstone servers. It is a spin off of the Minecraft Essentials add on, rebuilt from scratch against the Endstone Python API so that it stays quick on a busy realm rather than on an empty test world. It also ships an optional two way Discord chat relay.
The whole plugin is built around one rule: work that happens once per command is cheap, work that happens once per tick is not. Nothing in UtilityStone runs on a per movement or per tick basis, and nothing writes to disk while the server thread is waiting.
Created by Ozz. Released under the MIT licence, so you are free to copy, change and redistribute it as long as the copyright notice and licence stay with it.
- Endstone 0.11 or newer (built and tested against 0.11.9, Minecraft Bedrock 26.44)
- Python 3.10 or newer
UtilityStone has no third party dependencies. The Discord relay uses aiohttp, which already ships with Endstone, so there is nothing extra to install.
- Download or build
endstone_utilitystone-1.0.1-py3-none-any.whl. - Drop the wheel into your server
pluginsfolder. - Start the server. UtilityStone writes
plugins/utilitystone/config.tomlon first run. - Edit the config if you want, then run
/utilitystone reload.
To build the wheel yourself:
pip install build
python -m build --wheel
Every command is listed with the permission that guards it. Arguments in <angle brackets> are required and
arguments in [square brackets] are optional.
| Command | Permission | Default | What it does |
|---|---|---|---|
/sethome [name] |
utilitystone.command.sethome |
everyone | Saves where you stand. The name defaults to home. |
/home [name] |
utilitystone.command.home |
everyone | Travels to a home. With one home saved the name is optional. |
/delhome <name> |
utilitystone.command.delhome |
everyone | Deletes a home. |
/homes |
utilitystone.command.homes |
everyone | Lists your homes and how many you have left. |
| Command | Permission | Default | What it does |
|---|---|---|---|
/warp [name] |
utilitystone.command.warp |
everyone | Travels to a warp, or lists warps when the name is left off. |
/warps |
utilitystone.command.warps |
everyone | Lists the warps you are allowed to use. |
/setwarp <name> |
utilitystone.command.setwarp |
operator | Creates or moves a warp. |
/delwarp <name> |
utilitystone.command.delwarp |
operator | Deletes a warp. |
/spawn |
utilitystone.command.spawn |
everyone | Travels to the spawn point. |
/setspawn |
utilitystone.command.setspawn |
operator | Sets the spawn point to where you stand. |
| Command | Permission | Default | What it does |
|---|---|---|---|
/tpa <player> |
utilitystone.command.tpa |
everyone | Asks to teleport to somebody. |
/tpahere <player> |
utilitystone.command.tpahere |
everyone | Asks somebody to teleport to you. |
/tpaccept [player] |
utilitystone.command.tpaccept |
everyone | Accepts a request. Alias /tpyes. |
/tpdeny [player] |
utilitystone.command.tpdeny |
everyone | Turns a request down. Alias /tpno. |
/tpcancel |
utilitystone.command.tpcancel |
everyone | Withdraws the request you sent. |
/back |
utilitystone.command.back |
everyone | Returns to where you last teleported from, or where you died. |
| Command | Permission | Default | What it does |
|---|---|---|---|
/heal [player] |
utilitystone.command.heal |
operator | Refills health. |
/feed [player] |
utilitystone.command.feed |
operator | Refills hunger. |
/fly [player] |
utilitystone.command.fly |
operator | Toggles flight. |
/god [player] |
utilitystone.command.god |
operator | Toggles damage immunity. |
/speed <amount> [player] |
utilitystone.command.speed |
operator | Sets walk or fly speed from 0.1 to 10. |
/repair |
utilitystone.command.repair |
operator | Repairs the item in your main hand. |
Running any of these on somebody else needs the matching .others permission, for example
utilitystone.command.heal.others.
| Command | Permission | Default | What it does |
|---|---|---|---|
/pm <player> <message> |
utilitystone.command.pm |
everyone | Sends a private message. Alias /dm. |
/reply <message> |
utilitystone.command.reply |
everyone | Replies to the last message you got. Alias /r. |
/ignore <player> |
utilitystone.command.ignore |
everyone | Hides that player from your chat and blocks their private messages. |
/unignore <player> |
utilitystone.command.unignore |
everyone | Undoes an ignore. |
/ignorelist |
utilitystone.command.ignorelist |
everyone | Lists who you are ignoring. |
/broadcast <message> |
utilitystone.command.broadcast |
operator | Sends a highlighted message to everyone. |
| Command | Permission | Default | What it does |
|---|---|---|---|
/tempban <player> <duration> [reason] |
utilitystone.command.tempban |
operator | Bans for a set length, or perm for good. |
/mute <player> <duration> [reason] |
utilitystone.command.mute |
operator | Blocks a player from chatting. |
/unmute <player> |
utilitystone.command.unmute |
operator | Lets a muted player chat again. |
Bedrock already provides /ban and /unban, so UtilityStone does not replace them. /tempban adds the
timed bans vanilla lacks, and accepts perm when you want a permanent ban with a reason attached. All of
them write to the same server ban list, so vanilla /unban lifts a UtilityStone ban and the entries survive
restarts. Mutes are stored by UtilityStone and expire on their own.
Durations accept 30s, 15m, 2h, 7d, 3w, 1mo, 1y and combinations such as 1d12h. A bare number is
read as minutes. Use perm or forever for something that never expires.
| Command | Permission | Default | What it does |
|---|---|---|---|
/kit [name] |
utilitystone.command.kit |
everyone | Claims a kit, or lists kits when the name is left off. |
/kits |
utilitystone.command.kits |
everyone | Lists the kits you can claim. |
| Command | Permission | Default | What it does |
|---|---|---|---|
/who |
utilitystone.command.who |
everyone | Lists who is online and who is AFK. Alias /online. |
/ping [player] |
utilitystone.command.ping |
everyone | Shows connection latency. |
/playtime [player] |
utilitystone.command.playtime |
everyone | Shows total time played. |
/seen <player> |
utilitystone.command.seen |
everyone | Shows when somebody was last online. Works offline. |
/whois <player> |
utilitystone.command.whois |
everyone | Shows detail about an online player. |
/afk [reason] |
utilitystone.command.afk |
everyone | Marks you as away. |
/utilitystone [info|reload] |
utilitystone.command.utilitystone |
operator | Shows status or reloads the config. Alias /ustone. |
| Permission | Default | What it grants |
|---|---|---|
utilitystone.homes.unlimited |
operator | Saves homes with no limit. |
utilitystone.teleport.instant |
operator | Skips the teleport warmup. |
utilitystone.teleport.nocooldown |
operator | Skips the teleport cooldown. |
utilitystone.chat.color |
operator | Uses & colour codes in chat. |
utilitystone.kit.tools |
operator | Claims the example tools kit. |
Bedrock already ships /kick, /list, /msg, /tell, /w, /tp, /ban and /unban. UtilityStone does
not register any of those names, because taking over a vanilla command is a good way to break the client side
command tree for every player on the server. The replacements are /who for /list and /pm plus /reply
for messaging. Kicking, coordinate teleports and permanent bans stay on the vanilla commands, which already
work, and /tempban covers the timed bans vanilla does not offer.
The config lives at plugins/utilitystone/config.toml. Run /utilitystone reload after editing. A reload
rereads every value and restarts the background tasks, so poll intervals take effect straight away.
| Key | Default | Meaning |
|---|---|---|
saveIntervalSeconds |
30 |
How often the background writer flushes changed data. Clamped to 5 to 900. |
playtimeSyncSeconds |
120 |
How often playtime totals are written for online players, so a crash costs little. |
| Key | Default | Meaning |
|---|---|---|
usePrefix |
true |
Whether plugin replies carry a prefix. |
prefix |
&8[&bUtilityStone&8]&r |
The prefix itself. Supports & colour codes. |
| Key | Default | Meaning |
|---|---|---|
defaultLimit |
3 |
Homes everybody may save. |
[homes.limits] maps a permission node to a larger allowance. A player gets the highest limit they hold. The
shipped example gives utilitystone.homes.vip eight homes and utilitystone.homes.staff twenty. Neither node
is granted by default, so wire them up in whatever permission manager you use.
| Key | Default | Meaning |
|---|---|---|
requirePerWarpPermission |
false |
When true, each warp needs utilitystone.warp.<name>. |
| Key | Default | Meaning |
|---|---|---|
teleportOnFirstJoin |
false |
Sends brand new players to the spawn point one second after they join. |
| Key | Default | Meaning |
|---|---|---|
warmupSeconds |
3 |
Delay before a teleport fires. Set to 0 to teleport instantly. |
cooldownSeconds |
5 |
Wait between teleports. |
requestTimeoutSeconds |
60 |
How long a /tpa request stays open. |
cancelOnMove |
true |
Cancels the warmup if the player walks off. |
moveTolerance |
0.75 |
How far a player may drift during a warmup, in blocks. |
pollTicks |
10 |
How often warmups and request expiry are checked. Ten ticks is twice a second. |
rememberDeathLocation |
true |
Lets /back return you to where you died. |
historySize |
5 |
How many previous positions /back remembers per player. |
| Key | Default | Meaning |
|---|---|---|
manageFormat |
true |
Lets UtilityStone deliver chat itself. |
format |
<{name}> {message} |
Chat layout. {name} and {message} are replaced. |
afkTag |
&7[AFK] &r |
Prefix shown in front of an AFK player's chat. |
The Endstone API hands out the chat recipient list as a copy, so a plugin cannot quietly drop one reader from a
normal chat message. To make /ignore actually work on public chat, UtilityStone cancels the event and sends
the line itself to everybody who is not ignoring the speaker. The default format matches vanilla, so players
will not notice a difference. Set manageFormat to false if another plugin owns your chat, and be aware that
/ignore will then only apply to private messages.
| Key | Default | Meaning |
|---|---|---|
enabled |
true |
Turns automatic AFK detection on. |
timeoutSeconds |
300 |
Idle time before somebody is marked away. |
sampleSeconds |
5 |
How often positions are sampled. |
announce |
true |
Announces AFK changes in chat. |
| Key | Default | Meaning |
|---|---|---|
joinMessage |
empty | Custom join message. {name} is replaced. Empty keeps the server default, none hides it. |
quitMessage |
empty | Same, for leaving. |
welcomeMessage |
empty | Private message sent to a player as they join. |
Each kit is its own table, so [kits.starter] defines a kit called starter.
[kits.starter]
cooldown = "24h"
items = [
{ type = "minecraft:stone_sword", amount = 1 },
{ type = "minecraft:bread", amount = 16 },
]
[kits.tools]
permission = "utilitystone.kit.tools"
cooldown = "7d"
items = [
{ type = "minecraft:diamond_pickaxe", amount = 1, name = "&bStone Cutter", enchants = { efficiency = 3, unbreaking = 2 } },
]| Key | Required | Meaning |
|---|---|---|
items |
yes | List of items. type is required, amount, name, lore and enchants are optional. |
cooldown |
no | Wait between claims, in the usual duration format. Leave it out for no cooldown. |
permission |
no | Node needed to claim the kit. Leave it out and everybody may claim it. |
If a kit sets a permission that UtilityStone does not ship, nobody holds it until you grant it. That is
deliberate. Adding your own nodes to the plugin is not possible at runtime with the current Endstone API, so
point permission at a node your permission manager already knows about, or leave it off entirely.
Anything that does not fit in a player's inventory is dropped at their feet rather than lost.
UtilityStone can mirror your in game chat into a Discord channel and carry Discord messages back into the game. It is an optional module. If you never set it up, nothing changes: the plugin loads, every command works, and the console prints a short note telling you the relay is available if you want it. No token and no channel id are required for the plugin to run.
What gets relayed to Discord:
- Chat messages from players
- Death messages
- Join and leave messages
- Server start and shutdown
What comes back into the game: any normal message posted in the linked channel, shown to every player as
[Discord] Name: message. Messages from bots, webhooks and from UtilityStone's own bot are ignored, so the
relay cannot talk to itself in a loop.
1. Make a Discord application
Go to discord.com/developers/applications and press
New Application. Give it a name such as UtilityStone.
2. Add a bot and copy the token
Open the Bot tab. Press Reset Token, then Copy. This is your DISCORD_BOT_TOKEN. Treat it like a
password: anyone holding it controls the bot. If you ever paste it somewhere public, reset it immediately.
3. Turn on the message content intent
Still on the Bot tab, scroll to Privileged Gateway Intents and switch on MESSAGE CONTENT INTENT. Without it Discord sends your bot empty message bodies, so nothing reaches the game. This is the step people miss most often.
4. Invite the bot to your server
Open OAuth2, then URL Generator. Tick the bot scope, then tick these bot permissions:
- View Channel
- Send Messages
- Read Message History
Copy the generated URL, open it in a browser, and pick the Discord server you want.
5. Get the channel id
In Discord, open User Settings, then Advanced, and turn on Developer Mode. Right click the channel
you want to use and choose Copy Channel ID. It is a long number such as 112233445566778899. That is your
DISCORD_CHANNEL_ID. It is the numeric id, not the channel name.
6. Create the .env file
Make a file called .env inside plugins/utilitystone/ next to config.toml:
DISCORD_BOT_TOKEN=your-bot-token-here
DISCORD_CHANNEL_ID=112233445566778899
There is an .env.example in this repository you can copy. UtilityStone also reads a .env in the server root
folder if there is not one in the plugin folder, and real environment variables of the same names win over
either file, which is handy for hosting panels and Docker.
7. Start the relay
Restart the server, or run /utilitystone reload. You should see this in the console:
[UtilityStone] Discord relay linked as UtilityStone to channel #minecraft.
[UtilityStone] Discord gateway ready as UtilityStone.
Run /utilitystone info in game and the Discord relay line will read connected.
| Console message | What to do |
|---|---|
The bot token was rejected |
The token is wrong or was reset. Copy it again from the Bot tab. |
The bot cannot see channel ... |
Invite the bot to that Discord server and give it View Channel, Send Messages and Read Message History. |
DISCORD_CHANNEL_ID should be the numeric channel id |
You used the channel name. Turn on Developer Mode and copy the id instead. |
| Chat reaches Discord but Discord never reaches the game | The message content intent is off. Go back to step 3. |
Discord relay needs the aiohttp package |
Your Python environment is missing aiohttp, which normally ships with Endstone. Reinstall Endstone. |
These live in the [discord] section of config.toml. Secrets stay in .env and never go in config.toml.
| Key | Default | Meaning |
|---|---|---|
enabled |
true |
Master switch. Set to false to skip the module even when a token is present. |
relayChat |
true |
Send player chat to Discord. |
relayDeaths |
true |
Send death messages to Discord. |
relayJoinLeave |
true |
Send join and leave messages to Discord. |
relayServerState |
true |
Send server start and shutdown notices. |
sendIntervalSeconds |
1.5 |
How often queued lines are posted, batched into one message. |
inboundPollTicks |
10 |
How often Discord messages are handed to the game thread. Ten ticks is twice a second. |
maxInboundLength |
256 |
Longer Discord messages are cut short before being shown in game. |
chatFormat |
**{name}**: {message} |
Layout of a relayed chat line. Discord markdown works here. |
eventFormat |
_{message}_ |
Layout of deaths, joins, leaves and server notices. |
inboundFormat |
&9[Discord] &b{name}&7: &f{message} |
Layout of a Discord message shown in game. Supports & colour codes. |
The relay never runs on the server thread. Endstone ships a shared background event loop, and UtilityStone puts
its Discord work there through endstone.asyncio. Game events append a line to a bounded queue, which is all
the server thread ever does. The loop drains that queue on a timer and posts one batched message rather than
one request per chat line, which keeps a busy server well inside Discord's rate limits. Messages coming the
other way land in a second bounded queue and are read back on the server thread by a scheduled task, because
the Endstone API must only be touched from that thread.
Both queues have a fixed maximum size, so a Discord outage or a rate limit cannot grow memory without bound.
Mentions are disabled on every relayed message, so nobody can ping @everyone from in game, and colour codes
are stripped before a line leaves the server.
These are the decisions that matter when a server is full.
No per tick or per movement event handlers. The usual cause of lag in an essentials plugin is a
PlayerMoveEvent listener, which fires many times per second for every player. With fifty players that is
thousands of Python calls a second. UtilityStone registers no movement listener at all. Teleport warmups and
AFK detection sample positions on a timer instead, twice a second and once every five seconds respectively.
The cost does not change with how fast players are moving.
Cheap guard clauses on hot events. The only genuinely hot event listened to is ActorDamageEvent, needed
for god mode. Its first line checks whether the god mode set is empty and returns immediately if it is, which
is the case on almost every server almost all of the time. No lookups, no allocation.
Disk writes never touch the server thread. All persistent data lives in memory. Writes set a dirty flag, and a single background thread serialises and writes changed files on the save interval. Serialising holds a lock for the microseconds it takes and the file write happens outside it. Files are written to a temporary path and then moved into place, so a crash mid write cannot corrupt them. A file that is damaged anyway gets renamed out of the way at load and rebuilt rather than taking the plugin down.
Constant time lookups. Sessions are held in dictionaries keyed by player UUID. Ignore lists are cached as sets while a player is online instead of scanning stored lists. Teleport requests are indexed by both sender and receiver, so accepting, denying and cleaning up on disconnect are all direct lookups.
Small objects. PlayerSession and the teleport records use __slots__. With fifty sessions plus pending
requests that saves a real amount of memory and makes attribute access faster.
Bounded work. The teleport timer only walks the requests and warmups that actually exist, which is usually zero. Back history is capped per player. Playtime is written on a slow timer, not on every event.
Failures stay contained. Every command runs inside the router's error handler, so a bad argument or an unexpected API result logs a traceback and tells the player something went wrong instead of bubbling up into the server tick.
UtilityStone/
pyproject.toml
LICENSE
README.md
CHANGELOG.md
src/endstone_utilitystone/
__init__.py
plugin.py plugin class, command table, permission table
config.toml default config copied to the data folder on first run
core/
messages.py prefixed and coloured replies
router.py command name to handler dispatch with error trapping
sessions.py per player in memory state
settings.py typed, clamped view over config.toml
storage.py json stores and the background writer
services/
afk.py away detection and tagging
homes.py home storage and limits
kits.py kit definitions, cooldowns, item building
profiles.py persistent player records, playtime, ignore lists
punishments.py mutes, plus bans through the server ban list
spawns.py spawn point
teleports.py requests, warmups, cooldowns, back history
warps.py warp storage and access
commands/
base.py shared helpers for command groups
homes.py warps.py spawn.py teleports.py state.py
messaging.py moderation.py kits.py info.py
listeners/
chat.py chat delivery, mutes, ignore filtering
connection.py join and quit handling
protection.py god mode and death tracking
integrations/
discord/
bridge.py queues, lifecycle and thread handover
env.py minimal .env reader
gateway.py Discord websocket client
rest.py Discord http client
util/
durations.py duration parsing and formatting
locations.py location encoding and distance
text.py colour codes and small string helpers
Data files are written to plugins/utilitystone/: profiles.json, homes.json, warps.json, spawn.json,
punishments.json and kits.json. The optional .env for the Discord relay goes in the same folder. Keep it
out of version control, it holds your bot token.
MIT. See LICENSE. Copy it, change it, ship it in your own project. Just keep the copyright notice and credit Ozz as the original author.