Version 0.2.0 · Paper 26.3 · Java 25
Never run in a game yet. Everything below is built and covered by 954 tests, and none of it has been verified on a live server. Treat the troubleshooting section as predictions, not field reports.
Version notes
| Version | Minecraft / Paper | Notes |
|---|---|---|
| 0.2.0 | 26.3 only (api-version: '26.3'; built on Paper 26.3 build 194) | It will not load on 26.2. The served pack.mcmeta is written for format 97.1 (26.3), never [97, 0], which would mark the pack "made for an older version" on every 26.3 client. Old 26.2 helper configs still load: swing_animation becomes attack_animation + interact_animation, map_color is dropped (one warning per helper config in the load's report, and one Nixie notice per file per server start, code item.components.upgraded), and every pack item is rebuilt once on its first 26.3 join so it holds what 0.2.0 writes (saved spears keep their stab anyway: the 26.3 world upgrade moves swing_animation the same way). Models a pack still marks "shade": false are served with "shade_direction_override": "up". Recipe changes go in at most two batches per /pf zip (two resyncs). The pack 2 x-ray check uses 26.3's 80 shader files. See CHANGELOG.md. |
| 0.1.0 | 26.2 (api-version: '26.2') | The last 26.2 build, kept in releases/0.1.0/ and the 26.2 archive. Use it only on a 26.2 server. |
Switching a server to 26.3: Paper 26.3, Packforge 0.2.0 and the 26.3 content packs go in on the same day. Rebuild the Vanilla Verify layer from client-26.3.jar (§6, second-pack) before turning the second pack back on; until then set second-pack.enabled: false. A 26.2 pack.mcmeta (max_format [88, 0]) still loads on 26.3 but marks the pack as made for an older version, and the zip report says so: install the packs' 26.3 releases.
Watchdog codes
Packforge writes these as Nixie's EGW-WATCH records ([EGW-WATCH][plugin=Packforge][rank=..]...), at WARNING. The full contract is docs/WATCHDOG.md.
| Code | Rank | When | What to do |
|---|---|---|---|
item.components.upgraded | 1 (notice) | A helper config written for 26.2 names swing_animation or map_color, which 26.3 removed. The item loads, rewritten as the 26.3 world upgrade rewrites saved items. One notice per helper config per server start, naming its items, never at every /pf zip; the load's report (and so /pf zip) carries a warning for that file every time. | Install the pack's 26.3 release, or make the same edit in its helper config. |
1. What it is, and what it refuses to be
Packforge is the EGW ecosystem's own content host. It serves your Java resource pack, puts your Bedrock and Geyser artifacts where Geyser reads them, and — the part that matters — checks all three agree before anything ships.
Packforge checks the Java pack, Bedrock pack and Geyser mappings together. It reports mismatched states, models and sounds with the files involved. Warnings do not block otherwise usable content; conflicts that would damage the served pack or installed content are refused.
Packforge is EGW’s crossplay content host. It supplies items and their components, custom blocks, recipes, music discs, pack delivery, shared menus, player heads and chat glyphs. Java and Bedrock support are built into the same workflow.
What it will never do:
- Generate Bedrock assets from Java ones. You supply the
.mcpack. Auto-porting works for most assets and
then costs you a day on the rest, inside a code path that is harder to debug than doing it by hand.
- Merge two contents' files. Two contents shipping the same vanilla file with different bytes are refused by name,
not silently resolved. (The second pack's two layers are the one exception, and only for the files the game itself combines across packs — atlases, sounds, language files, fonts — combined exactly the way the game would; §6, second-pack.)
- Rebuild on boot. Both build commands are operator-run. A bad edit never reaches players on its own.
2. Install
- Drop
Packforge-0.2.0.jarintoplugins/(removePackforge-0.1.0.jar) and start the server. 0.2.0 needs Paper
26.3; on a 26.2 server keep 0.1.0.
- Set
pack-host.public-addressinconfig.yml. Blank falls back toBukkit.getIp(), which is empty
on most setups, and players then get a URL they cannot reach. This is the single most likely thing to waste your first hour. Boot says so in capitals: NO PACK URL: when no address resolves, and PACK URL ONLY WORKS ON THIS MACHINE: when it resolves to 127.0.0.1, localhost or 0.0.0.0.
- Make sure
pack-host.port(default8123) is reachable from outside. /pf status— confirm the host is up and the URL looks right.
The pack is required by default and sent before join,: a Java player who declines it cannot join (their own client disconnects them), and one whose download fails is kicked with a message saying what to do. Bedrock players are never sent it and never kicked. Check the URL works from outside before players join, or set pack-host.kick.on-fail: false while you do.
Optional but recommended: Geyser and Floodgate (for Bedrock), GrimsClientChecker (so players can choose chest or touch menus). All soft dependencies — absent, the relevant feature degrades and nothing breaks.
Using an existing pack-host address
If you already have a reachable pack-host address and port, you can configure Packforge to use them. Stop the previous host before assigning its port to Packforge; only one process can listen on a port.
| Existing hosting setting | Packforge setting | Purpose |
|---|---|---|
resource-pack.hosting.self-host.pack-port | pack-host.port | 27077 |
resource-pack.hosting.self-host.server-ip | pack-host.public-address | 216.245.202.162 |
auto_apply.before_join: true | pack-host.before-join.enabled: true (default) | |
| Required resource pack | pack-host.kick.on-decline / on-fail | Control how required Java-pack declines and failures are handled. |
self-host.protection.rate_limit (3 per 2 s, 30 min after 5) | pack-host.rate-limit (the same numbers, default) | |
items.auto_update.enabled: true | items.auto-update: true (default) | |
recipes.custom-recipes.unlock.automatically: true | always on: recipes unlock at join, after a reload and on touch (§7) | |
| Recipes with empty permissions | items.open-recipes-without-permission | Choose whether ordinary players can craft recipes whose permission field is empty. |
| Give a pack item | /pf give <player> <ns:id> [amount] [silent] |
Stop the server and the previous pack host, configure Packforge’s hosting address and port, then start the server and run /pf zip. Check the URL shown by /pf status and resolve any hosting warning before requiring the pack.
3. The mental model
drop/ you put all three artifacts here. move EMPTIES it.
contents/ Java assets only. THE INDEX of what the pack contains.
output/ pack.zip — what players download. Generated, never edited.
move sends each artifact to its real home:
| Artifact | Goes to |
|---|---|
Geyser mapping .json | plugins/Geyser-Spigot/custom_mappings/ |
.mcpack | plugins/Geyser-Spigot/packs/ |
| Java pack | contents/ |
| Optional helper-config YAML | items/<content>/, or items/<namespace>/ when imported from a namespace’s configs/ folder |
Mappings and .mcpack files do not live in this plugin. They pass through. Their home is Geyser's folders, and after move this plugin holds only Java assets — which is all it hosts.
zip packages contents/ into output/pack.zip, hashes it, and re-pushes to online players — only to those who do not already have exactly that file. It is Java-only: a .mcpack is already packaged and Geyser takes it as supplied.
A zip that changed nothing re-sends nothing. The zip is built byte for byte the same from the same files (same sha1 every time), and a Java client reloads every pack it is sent — even one it already holds: the black loading screen, then every chunk drawn again. So a player whose client already has the pack (loaded, still downloading it, its prompt still open, or declined) is not sent it again, and a zip that built the same file says output/pack.zip is unchanged … so it was not re-sent. When the pack did change, each player is sent it once: their client keeps the old pack on screen while the new one downloads, then swaps them in one reload — Packforge never removes the pack before sending the new one, which could cost a second reload with no pack at all in between.
How the pack reaches a Java player:
- Before join. While the player is still connecting (Paper's configuration phase), they are sent the pack and
held on the loading screen until their client says it loaded, failed, or before-join.timeout-seconds (120) runs out. They never stand in the world on vanilla textures behind a reload screen.
- Decline. With
kick.on-declineon (the default) the pack is sent as required, as a required pack sends
it when its kick-player-on-decline is on. The client then always asks — even if the player set this server's Server Resource Packs to Disabled — and on a decline disconnects itself, with its own "requires a custom resource pack" message, before the server hears anything. So kick.decline-message is not what a vanilla player sees; the log shows <name> left at the resource pack prompt without accepting the pack. With kick.on-decline off, the pack is sent as optional and a player who declines plays without it.
- Kick. A Java player whose required pack fails to download or load is disconnected with
kick.fail-message — before they enter the world when it happens during connecting, or as soon as the client says so once they are in. A failure because /pf zip replaced the pack mid-download (the client fetched the new zip against the old hash) is not a kick: the log says so and they are sent the new pack. Bedrock players are never sent the pack and never kicked over it.
- On join, as the fallback. Anyone the first step did not settle — before-join off, a pack that failed while
kicks are off, a pack /pf zip replaced during the wait, or a prompt the player never answered — is sent it on join. A player whose pack loaded before join is not sent it again (the client would reload it), and one who accepted it and is still downloading when the wait ran out is left to finish. A player who sat at the prompt (AFK, alt-tabbed) until the wait ran out is let in and asked again in the world: their client throws an open prompt away when the world loads, and the in-world prompt cannot be closed without answering (Escape declines).
- The second pack is decided before join, when LuckPerms can say. LuckPerms loads a player's permissions while
they log in, so a player flagged with second-pack.permission is sent the second pack straight away: one pack screen. Without LuckPerms — or for a permission granted only in one world, which reads as "not held" before the player is in a world — they get the default pack before join and are swapped onto the second at join: one extra pack screen, for them only.
Paper does not send a PlayerResourcePackStatusEvent for a pack loaded before join. Plugins watching that event see the join-phase offer when the player is allowed in; a player disconnected during connecting never reaches gameplay.
The embedded host answers 429 Too Many Requests to an address making more than 3 requests in 2 seconds, and blocks an address for 30 minutes after it goes over 5 times (pack-host.rate-limit, the default limits).
A content is checked on what it ships, not on what it lands beside. Validation reads the rest of the drop, contents/ and Geyser's installed mappings only to answer "does this model exist?" and "is this block state covered on Bedrock?", because Geyser loads every mapping file together. A pack that places no block is never asked about anybody's tripwire or note-block states, and Crops' merged note_block.json is covered by JukeBoxes' own mapping exactly as it is in game. Drop them together and each counts the other as present.
Packforge reports disagreements between a content pack’s Java and Bedrock sides. Warnings identify each affected state and file; compatible content can still be placed.
- a Java block state no installed Geyser mapping overrides (Crops 0.15.4 beside the 0.15.0 mapping: the Sushi
Station's note=15,powered=true and three crop states) — Bedrock shows the bare carrier there until the matching mapping is installed;
- a model, item model or
.oggthe pack names and does not contain. When nothing installed or in the drop ships
that namespace yet, the warning says so: Crops' note_block.json names JukeBoxes' models, so moving Crops first is an order, not a fault;
- a Java sound with no Bedrock counterpart, and a Java file the client could not read either.
It refuses only what would damage the served pack or content already installed, and says why: a folder it cannot read, a Geyser mapping or .mcpack it cannot open, or a .mcpack with nothing but a manifest (each would replace the installed file of its name), a path that works out to outside contents/, one content shipping one path twice with different bytes, and a vanilla file another content already placed with different bytes.
Credits and licence files at a pack root land in contents/credits/<content>/, so every pack's attribution ships and none of them overwrite each other. A whole pack copied straight into contents/ is read the same way: contents/GrimsBackpacks-Java-0.1.1/credits.txt ships as credits/grimsbackpacks-java/credits.txt. Every pack's own pack.mcmeta and pack.png after the first are set aside quietly. At zip time the served pack.mcmeta is built from the one that stayed, with any pack-host identity keys you set merged on (see "Every zip carries a readable pack.mcmeta" below).
To import existing content, copy its namespace folders into drop/. Item, model, texture, equipment and sound folders become Java assets; configs/ becomes helper configuration. A resourcepack/ folder or complete Java pack is read as pack content. Geyser packs and mappings already installed in Geyser’s folders can remain there.
Or skip drop/: copy them straight into contents/ and run /pf zip. zip reads a folder in contents/ by the same rules — a namespace folder lands under assets/<namespace>/, a whole Java pack folder merges in, its configs/ are never shipped, _iainternal is skipped. move is still the way to get the validation and ownership checks; it is no longer the only way for a pack to be loaded properly.
Helper configs in contents/ load at each content refresh: on every boot, /pf reload, move and zip. Every .yml in a configs/ folder that defines items:, recipes:, sounds:, categories: or chat glyphs (glyphs:, font_images:) is read into items/<folder>/, so a pack copied in carries its components from the first boot, not the first zip. items/.harvested.yml records where each copy came from, and that record is what keeps items/ in step:
- Delete a content folder, or rename one of its configs to
.yml.off(the documented way to switch off the
Dreadplate re-trim lock or a recipe file), and its copy is removed at the next boot, reload, move or zip — named in the report — so its items, recipes, custom blocks and sounds stop loading, so removed content stops applying.
- A changed config replaces its copy.
- A file in
items/with no record is yours and is never touched. If a config would land on it, the report says
so and leaves both alone. A copy you edit after it was read in becomes yours too: it is kept, and the report tells you when its source changes underneath it.
- A copy left by an older Packforge, from before the record, is adopted if it still matches its config byte for
byte. One that does not is treated as yours; delete it to have the config read in again.
- A file
/pf moveplaced belongs to that content's release and the harvest never touches it — so a folder copied
into contents/ first and later moved through drop/ keeps its configs. If a different copy of the same config is still in contents/, the report names both; remove the one you replaced.
If two folders in contents/ would both read a config into the same items/ file — two releases of one content side by side — the first in sorted order is read in and both are named. A whole pack folder's own pack.mcmeta is not packaged on this path, but the first one in sorted order is what the served pack.mcmeta is built from — the same one move would have kept.
Every zip carries a readable pack.mcmeta. A client that cannot read the served pack's pack.mcmeta — no file, no pack section, or a description it cannot decode — drops every server pack it was sent, so every custom texture goes at once and nothing is logged. zip therefore always writes one:
- It starts from the content's own:
contents/pack.mcmeta(wheremoveleaves the first content's; for the
second pack uses the same one — never contents2/'s — so both packs look the same on the pack screen), else the first whole Java pack folder's in contents/. If no content ships one — only namespace folders, say — it writes one for this server: your description (or Server content pack), and min_format/max_format/pack_format set to the server's own resource pack format, read from the server jar's version.json, minor included: [97, 1] and 97 on Minecraft 26.3. A max_format of [97, 0] would leave a 26.3 client (97.1) out; a bare 97 means every minor of 97.
- It merges the
pack-hostkeys you set onto it, and only those. Setting just a description or just an icon keeps
the content's format range and everything else it wrote.
- It checks the result against the client's own rules, which were run against the 26.2 client itself and checked
again in the 26.3 server's PackFormat (the same rules, now with minors):
- The description must be text or a text component the client can decode. This is the one that loses
everything, and the easy slips are all fatal: "color": "grey" (the client only knows gray), "Gold" in capitals, an empty list [], an extra: [], a part with no text. zip checks components conservatively — text, translate and keybind with colours, bold/italic/underlined/strikethrough/obfuscated, font, insertion and shadow_color — and refuses anything else (click_event, hover_event, score, selector, NBT and sprite parts), which a pack screen has no use for anyway.
- The format fields. A client that cannot read them still loads the pack, but only as one made for another
version, so these are refused too.
A content's unreadable copy is repaired (a description the client cannot decode replaced, then its format fields replaced) and the report says so; the content's file is never changed. A setting of yours that makes it unreadable — a description component the client cannot decode, a pack-format outside the range, a min-format above max-format — refuses the zip by name, and players keep the pack they have.
The zip report names where the pack.mcmeta came from, and warns when its range leaves out the server's format (on 26.3 that includes a range ending at [97, 0], and every 26.2 pack's [88, 0]), or when the server's format could not be read from its jar and it runs a Minecraft version other than 26.3.
Two folders in contents/ shipping one path with different bytes are named at zip. That is what two releases of one pack copied in side by side look like. The zip carries the copy from the later folder in sorted order — the same on every machine, so the pack's sha1 no longer depends on the disk — and the report names both folders, a few of the paths, and the winner. Delete the folder you replaced and zip again. Byte-identical copies are not named.
/pf zip combines the supplied packs and generates the additional resources they need:
- Atlas entries, the way 26.2 draws them. The client bakes placed blocks and items with one lookup that tries
Items can use textures from the item or block atlas; placed blocks use the block atlas. Packforge adds the aliases needed when a placed model uses an item texture, while keeping inventory textures on the correct atlas. Pack blockstates and overridden vanilla block models are included in this check.
- Vanilla for every carrier state a pack does not draw. When a pack ships
note_block.jsonortripwire.json
as a variants file (Crops does both), the served copy gains a variant pointing at vanilla's model for every state the pack leaves out — all 27 note-block instruments, notes 0-24, powered or not; every tripwire state — under keys that never overlap the pack's own.
- Blockstates for custom blocks a helper config defines (
behaviours: block), at the state the pack's Geyser
mapping names. Chorus and mushroom blocks keep their vanilla look for every other state.
- Front-lit inventory icons. An element model an item draws that has no parent and no
gui_lightis served with
gui_light: front gives chest icons, rods and machine items front-lit inventory rendering. The client obtains generated-item particles from the item’s base texture.
sounds.jsonfor sound events a helper config declares undersounds:, such as music discs.- 26.3 model shading. 26.3 removed a model element's
shadeflag, and its client ignores a leftover one, so a
face a pack meant to be unshaded comes out darker on the sides. On a 26.3 server every model the pack serves with an element marked "shade": false is served with "shade_direction_override": "up" instead (full brightness, as before). Crops 0.15.4, Backpacks 0.1.1 and JukeBoxes 0.0.8 had 43 such elements; their 26.3 releases already write the override, and are left alone. The zip report's model shading: line counts them.
- Chat glyphs and the EGW chat font (§8): a bitmap in
font/default.jsonfor every glyph — kept after a content's
own default.json providers when it ships one — the pictures Packforge draws or carries (badges, tags, the built-in icons, under assets/packforge/textures/glyph/), and the font egw:chat.
These go into the output pack only; contents/ is never changed. The zip report has one line per kind, so you can see each was considered and how many files it touched.
Helper configs give items everything the mapping cannot. A Geyser mapping only says what Bedrock needs — base item, model, name — so an item built from it alone has no food, no stack size, no equippable (armour renders as its base netherite or iron), no attributes, no trim, and none of the tags a plugin uses to recognise its own items. A helper config says those things. Drop it beside the pack's Java pack, loose or in a configs/ folder; move files it under items/<content>/. One in an Content namespace folder's configs/ (drop/grims_chests/configs/rings.yml) lands at items/<namespace>/<path under configs/> — items/grims_chests/rings.yml — the same file the harvest of that folder copied into contents/ writes, so the two routes never load an item twice.
Two helper-config formats are recognised by their top-level keys:
- Helper format —
namespace:plusitems:. Each item uses the typed keys (material,name,lore,
stack-size, max-damage, food, equippable, tool, attributes, …) and, for anything else, components: — any vanilla data component written as YAML. An item with no item-model draws <namespace>:<id>. demo/GrimsCrops-HelperConfig-Demo/gcrops.yml is a worked example with the format documented at its top.
- Namespace format —
info.namespaceanditems:, withitem_modeland nativenbtcomponents.
Supported item properties include durability and fire resistance. Unsupported runtime behaviours are listed in the load report. The recipes: block is read as described in the recipe section.
Either way, components are applied by the server's own /give parser, and the typed keys are set over them. A pack with no helper config still gets its items, derived from its mapping — except in the namespaces whose items their own plugins make (items.derive-from-geyser.exclude-namespaces: gcrops, grims_nilla_fishin, grimsbp, grimsjukeboxes, grimsredstonewifi). Derived, those were vanilla items dressed as the real thing: a bundle named "Basic Leather Backpack + Leather Chestplate" with no backpack behind it, Crops dishes that kept a poisonous potato's poison and never stacked with Crops' own. They are not listed in /pf browse, and neither are those namespaces' block states. Items a helper config defines in them (JukeBoxes' four) still are. The load report counts what was left to each plugin. An armour or wing item that comes only from a mapping — so it would be its plain base item on the player — is warned about by name, because its helper config is missing.
Helper configs supply item names and lore. Names are written as both item_name and custom_name. Plain helper-config names and lore default to white, non-italic text; explicit colours and italics are preserved.
With items.auto-update enabled, Packforge checks the config hash of pack items when players join or open a container. Outdated items are rebuilt from the current helper config. Plugin menus are excluded. The updater preserves:
- the amount, and the wear as the same share of the new durability (a quarter worn stays a quarter worn);
- its enchantments and stored enchantments, over any the config gives (the stack's level wins);
- a name a player gave it at an anvil (the item's own name is replaced by the config's);
- a trim, unless the config ships its own; the anvil's repair cost;
- anything inside it (a container's or bundle's contents, loaded projectiles, a book's pages), and other plugins' data.
Config fixes can update existing gear. Undefined items and functional stacks made by other plugins are left alone. Gear temporarily presented in Bedrock Enchant Bridge’s Bedrock form waits until that bridge restores it, then updates at the next applicable join or container opening.
Smithing recipes are read, and decide the table, using the configured entries. A helper config's recipes: smithing: entries (template, base, addition, result, permission/permission_suffix, result.move_attributes) are keyed by namespace and recipe ID. An entry is registered as a server recipe only when the table's slots would not take what it names otherwise; an entry whose template, base and addition vanilla's own smithing recipes already take (a trim template, any netherite piece, a trim material) needs none and is decided at the table alone — all 792 Dreadplate locks are such entries, and the boot and zip reports say how many. When a smithing table's three slots match an entry — a slot naming a pack item takes only that pack item, a slot naming a material never takes a pack item made of it — a player allowed by the entry's permission gets its result, and anyone else an empty slot. That is how Dreadplate's 792 re-trim locks work: one entry for every trim pattern, trim material and piece, the helmet included, all with the permission egw_retrim_lock that nobody is granted. A player gets nothing; an op, or a group holding *, gets the piece back unchanged. Nobody ever gets a registry trim on Dreadplate. Gem armour, vanilla armour and everything no entry names trims normally.
move_attributes carries from the base to the result what it names, with the configured defaults (enchants and other_nbt on, the rest off): name (a name a player gave it), lore (the base's lore, then any of the result's lines it does not already have), enchants (higher level wins), durability (the same share of wear), custom_model_data, material, other_nbt (other plugins' data).
Under the recipes, a safety net. A pack item whose helper config sets minecraft:trim — Dreadplate's chestplate, leggings and boots carry their soul runes inline — gets an empty result at a smithing table that no smithing entry decides: with the lock file switched off, or a trim template or material a future Minecraft adds before the lock file lists it. A smithing-table trim would replace the inline one and blank the piece for Bedrock viewers. pf.smithing.retrim lets one player past the net; it is default: false, no admin node carries it, and it only counts when granted by name: a * wildcard does not grant it.
Pack items do not mix with vanilla items at the vanilla stations. Vanilla knows nothing about pack items, and at four stations it hands back the vanilla half of a mix: two damaged emerald pickaxes repair in a crafting grid into one plain iron pickaxe, two Dreadplate chestplates into plain netherite (eight netherite blocks gone), and a vanilla netherite chestplate on an anvil or a grindstone eats a Dreadplate one. Packforge guards all four, each under its own guards. switch (§6):
| Station | Allowed | Refused (no result) |
|---|---|---|
| Crafting grid | A helper-config recipe, with pack items only where it asks for them. Two of the same pack item repair into a fresh copy of that item: vanilla's durability arithmetic, curses carried over, other enchantments dropped as vanilla's grid repair drops them | Any other recipe — vanilla (special and transmute recipes such as dyeing and fireworks included), or another plugin's — with a pack item in the grid. A pack item where a recipe asks for the vanilla material it is made of. Grid repair of a pack item with a vanilla one or a different pack item |
| Crafter | Everything without pack items | A pack item going in by hopper, dropper, click, shift-click, number key, F or drag. Any helper-config recipe — a Crafter has no player to check the recipe's permission against. Any craft while a pack item is already inside |
| Anvil | Renames, enchanted books, two of the same pack item, and repair with the material the item's minecraft:repairable names | A pack item with a vanilla one of the same type, in either slot. Two different pack items. A pack item offered as another item's repair material |
| Grindstone | One item alone (disenchanting), two of the same pack item | A pack item with a vanilla one, or with a different pack item |
Gem gear repairs with its own gem: emeralds for emerald gear and amethyst shards for amethyst gear. Dreadplate and its scythe use their netherite base’s repair material; wings use phantom membranes. Station guards prevent mixing a pack item with a different pack item or a vanilla look-alike.
Station guards recognise tagged pack items, not every stack that happens to draw a pack model. Gameplay plugins such as Chests, Crops, Nilla Fishin’, Backpacks, Redstone Wifi and JukeBoxes make their own functional stacks. Use those plugins’ recipes or give commands: an artwork item from /pf browse is not a substitute.
Boot logs one Station guards: line saying all four are on, or warns Station guards OFF: naming each one switched off and what that lets through.
The item updater and smithing recipes recognise tagged pack items, never a model alone.
The item list is rebuilt at boot, on /pf reload, and after every move and zip. move, zip and reload read the files, try each item's components and resolve the recipes off the main thread, then swap the finished list in on it in one short step whose lines arrive in the same report. Recipes are then changed only where they differ from what is registered — an unchanged recipe set touches nothing — in at most two calls: one batch of removes and one of adds (Paper 26.3's removeRecipes and addRecipes). Every change to the server's recipes resends every online player their advancements, tags and recipe book; on 26.2 that happened once per recipe, which is what froze the server for 10-15 s after /pf zip before 0.1.0 and was still a short burst of hitches in 0.1.0. On 26.3 a zip costs at most two resends, each in a tick of its own. What those steps did goes to the server log when they finish, and /pf status says while some are still waiting. An item whose model the pack does not contain is never listed, so Crops' growth-stage block art does not appear in the browser as missing textures.
Content ids compare without their version: GrimsCrops-Bedrock-0.15.0 and GrimsCrops-Bedrock-0.16.0 are the same content, and so are GrimsRedstoneWifi-0.4.1-Java and GrimsRedstoneWifi-0.4.4-Java (a version just before a -Java, -Bedrock or -Geyser ending is stripped too; the platform word is kept, so the Java and Bedrock halves stay two contents).
A content is known by its pack, not by how you laid out the drop. Ownership and the list of placed files are kept under each Java pack's own name: the id in its pack.mcmeta when it has one (Grims Crops gcrops, Grims Backpacks grimsbp), else its folder without the version (grimsnillafishin-java), and for a content namespace folder its namespace (gdd). So Crops placed as the bare GrimsCrops-Java-0.15.4/ folder and updated as drop/GrimsCrops-0.15.5/ beside its .mcpack, or loose beside GrimsCrops-Bedrock-0.15.6.mcpack, is one content updating its own tripwire.json, not a collision; and a patch to drop/contents/gdd/ touches gdd and no other namespace in that tree.
A complete new release replaces the old one; a patch replaces only the files it carries. Packforge records ownership so a later complete release removes files that pack no longer ships. A release is recognised as:
- A pack folder with its
pack.mcmeta. Without one, the drop is a patch. - An Content namespace folder that looks whole: it carries every asset folder (
items/,models/,
textures/…) and the configs/ its previous move placed, and more of the files it placed than it leaves out. drop/gdd/textures/item/prospector_pickaxe_consolidation.png on its own is a patch; so is a folder of every texture re-exported with no models/.
- Helper configs are taken back only with a release that carries helper configs. A drop of configs alone —
drop/grims_chests/configs/rings.yml — is a patch: it replaces the configs it carries and names the ones it left.
- Nothing a loose drop with no name places is ever taken back:
drop/itself as the pack root, with no.mcpack
beside it and no id in its pack.mcmeta. Every such drop shares one fallback name, so the move says so and records nothing for it. Put a release in drop/<name>/ to have its leftovers cleaned up.
A patch's report says how many files of the previous move it left in place, and why it was read as a patch. A file another content owns, or also ships, is never taken. Nothing taken back is deleted: each file is moved to taken-back/<time>/ under the plugin folder, at the path it had (contents/…, items/…), and the move warns, naming them. Copy back anything that should have stayed; delete the folder once the pack looks right. Contents placed before this record existed start being tracked at their next move, so their first update after it leaves the older leftovers in place.
If every file a content placed is deleted from contents/ by hand, the next move warns that its helper configs in items/ still load, naming each: delete them, or rename them to .yml.off, if the content is gone for good.
A .mcpack replaces the installed pack with the same manifest UUID, whatever its file name. GrimsBackpacks-Bedrock-0.1.10.mcpack replaces GrimsBackpacks-Bedrock-0.1.3.mcpack, and the report names the old file: Bedrock identifies packs by that UUID, and two with one UUID is two releases of one pack loaded at once.
4. Commands
/pf
| Subcommand | Permission | Does |
|---|---|---|
move | pf.build | Empties drop/ and drop2/, placing each artifact. Validates first: disagreements are warnings, and a content refused (see §3) is skipped whole, never half-placed. A new release takes back what its previous release placed and no longer ships |
zip | pf.build | Packages contents/ → output/pack.zip, hashes, repushes. With second-pack.enabled it also packages contents2/ under contents/ → output2/pack.zip and checks pack 2 against vanilla x-ray (§6, second-pack). With Geyser installed it also builds Packforge-Glyphs.mcpack (glyphs and the Bedrock EGW alphabets, §8): a changed one waits in Geyser's packs/ as Packforge-Glyphs.mcpack.next and replaces the served one when the server stops; the report says when a restart is needed |
push [player|all] | pf.push | Resend the already-built Java pack without rebuilding it. No target sends to yourself; the console must name a player or all. Bedrock players are skipped. |
give <player> <ns:id> [amount] [silent] | pf.give | Give a pack item to an online player. Amount is 1–2304, default 1; full stacks are split and overflow drops at the player. silent suppresses the player message. Item IDs and player names support completion. |
glyphs [name] | pf.glyphs | Lists the glyphs grouped — badges, tags, icons and emoji (six to a row; hover one for its aliases and typed Unicode; the console gets each name with its aliases), other images — with a preview; click one to copy it; each badge, tag and image line names its world form's first character, and the last line is the TAB tagprefix line for nametags (click to copy). With a name (or an alias: glyphs lol is joy): its characters, its world form (nametags, signs), its placeholder, the exact LuckPerms prefix command (click to put it in your chat box), its aliases and typed Unicode, and its TAB forms. Works from the console, which is shown names and escapes instead of the characters (§8) |
glyphs calibrate | pf.glyphs | Run it as a Bedrock player. Shows plain text (Ag Tricks) on a line of its own, then one line per variant — plain Ag, the EGW Ag, the owner badge and a coin as Bedrock glyphs, each after a plain space so nothing can overlap — at pixels-per-unit: 1 with lift: 0, 1 and 2, and at pixels-per-unit: 2, each labelled with its exact settings and the one in use marked; then the config.yml keys to set (§8, "Bedrock glyph size"). From Java or the console it says it is for Bedrock and how the pack in use draws |
glyphs calibrate world | pf.glyphs | Run it as a player, best on Bedrock. Puts five invisible armor stands in front of you for 60 seconds, their nametags reading Ag <owner badge> <coin> Tricks world-offset: N with the badge and coin drawn at world offsets -2 (top) to 2 (bottom), the one in use marked; says which config.yml key to set (§8, "Bedrock world text"). Running it again replaces your stands; they are never saved with the world. On Java there is nothing to measure (Java draws world forms exactly as the glyphs, and has no calibration characters: boxes). From the console it says it needs a player |
validate | pf.use | Dry run. Writes nothing, places nothing, not even a folder |
browse [words] | pf.use | Opens the content browser: one category per pack with its items and blocks together, or search results for the words |
status | pf.use | Host, URL (with a warning when only this machine can reach it), how the pack is delivered, hash, file counts, second pack, Geyser, tripwire setting, items and custom blocks, glyphs and the EGW font (served to Java, live in Bedrock chat), menus, bosses, carriers |
reload | pf.build | Re-reads config.yml, the helper configs in contents/ (into items/), items/, bosses/ and menus/ |
move, zip, reload and validate run off the main thread (only short steps hop onto it) and refuse to run while one of them is still running. push and give run on the main thread, since one sends packets and the other changes an inventory.
/gmenu
list, open, reload, debug, bind, unbind, give, heads. A menu is a file in menus/, drawn as a chest for Java players and a native form for touch players.
/gmenu heads harvest is the one worth knowing: fill an inventory with the player heads your server uses, run it once, and Bedrock players see real faces instead of Steve. Needs a full Geyser restart — see §9.
/gboss
list, info, spawn, kill. Permission pf.boss.admin. Definitions live in bosses/.
5. Permissions
Split by consequence, not by command count.
| Node | Default | Covers |
|---|---|---|
pf.use | op | status, validate — reads only |
pf.build | op | move, zip, reload — everything that changes what players download |
pf.push | op | push — re-sends the built pack; changes nothing on disk, but puts a pack screen in front of the players it names |
pf.give | op | Give pack items |
pf.glyphs | op | glyphs — lists chat glyphs and hands out their characters and LuckPerms prefix commands; glyphs calibrate (Bedrock glyph size and lift); glyphs calibrate world (Bedrock nametag offset; spawns temporary armor stands) |
pf.admin | op | all five of the above |
pf.recipe.<p> | Operators, unless granted | Craft a helper-config recipe whose permission is <p>. For empty permissions, use items.open-recipes-without-permission to control access. |
pf.glyph.<p> | Operators, unless granted | Use a glyph whose helper config sets permission <p>. Glyphs without a permission are open to everyone. Tags and badges default to shortcode: false. |
pf.glyphs.signs | everyone | write :name: glyph shortcodes (aliases and typed emoji too) on signs, front and back, standing and hanging (each glyph's own pf.glyph.<p> on top). Without it, glyph characters pasted onto a sign are taken out (§8, "Signs") |
pf.boss.admin | op | /gboss |
pf.menu.use | everyone | open menus that name no permission of their own |
pf.menu.admin | op | menu reload, debug, bind, give, open-for-others |
pf.pack.verify | nobody | flags a player for the second pack (vanilla rock, ore, cave, fluid and shader files under the custom content) instead of the default; with LuckPerms a grant or removal, on the player or on a group, applies at once (§6, second-pack) |
reload sits under build on purpose: anyone who can run move or zip is already changing what every player downloads, so gating a config re-read separately would be theatre.
default: op does not keep non-ops out. It only means a fresh install is not locked out. Grant pf.build to an Admin group in LuckPerms and they can move, zip and reload without op.
pf.smithing.retrim (default false, not under pf.admin) — trim an item that ships its own trim at a smithing table no helper-config smithing recipe decides. Counted only when granted by name: a * wildcard does not grant it (LuckPerms reports a wildcard-covered node as set, so Packforge checks the player's effective permissions for the node itself). It never opens Dreadplate's re-trim locks, which use their own permission. See §3.
6. Config reference
pack-host
| Key | Default | Notes |
|---|---|---|
enabled | true | Turns the embedded HTTP server off entirely |
port | 8123 | Its own port, never the game port |
public-address | '' | Set this. Blank falls back to Bukkit.getIp(), usually empty |
external-url | '' | Serve the zip yourself instead. The hash still comes from the local build |
prompt | 'Evergrims Werks content pack' | The pack screen text |
required | true | Whether a Java player must hold the pack. Only a required pack ever kicks; false makes it optional and nobody is kicked |
before-join.enabled | true | Offer the pack during connection; the join-phase push is a fallback. |
before-join.timeout-seconds | 120 | How long a connecting player waits (1–600). A download still running then carries on in the world, and its result is acted on when it arrives. A player who never answered the prompt is asked again in the world |
kick.on-decline | true | Keep out a Java player who declines a required pack. This decides what the client is told: on, the pack is sent as required and the client disconnects itself on a decline (its own message); off, it is sent as optional and a decliner plays without it |
kick.on-fail | true | Kick a Java player whose required pack fails (FAILED_DOWNLOAD, INVALID_URL, FAILED_RELOAD, DISCARDED) — never over a pack /pf zip replaced mid-download. Bedrock players are never kicked |
kick.decline-message / kick.fail-message | see config.yml | MiniMessage. The decline one is a backstop a vanilla client never shows (it leaves by itself, see kick.on-decline); the fail one is what a failed download sees |
rate-limit.enabled | true | Per-address limit on the embedded host; over it is 429 Too Many Requests. Turn off behind a reverse proxy — every player then has the proxy's address |
rate-limit.max-requests / period-seconds | 3 / 2 | the default limits. A player downloads once per join, so this only stops looping clients and port scanners |
rate-limit.block-after / block-minutes | 5 / 30 | An address that goes over the limit this many times within block-minutes is refused for block-minutes, and the block is logged once with the address. 0 never blocks |
description | '' | The line under the pack title. Plain text, or a JSON component for colour and a second line. Blank keeps the content's. zip refuses a component the client cannot decode (grey for gray is the usual one), because that loses every texture |
icon | '' | A 64×64 PNG in the plugin folder |
pack-format / min-format / max-format | -1 | Merged onto the content's pack.mcmeta; -1 keeps the content's value, or the server's own format (97.1 on 26.3) when no content ships one. Setting one end of the range writes both ends; min-format is written [min, 0] and max-format as the bare number, every minor of it (97 takes a 26.3 client, [97, 0] would not). A min-format of 64 or below is written the old way too, with supported_formats (84 / 32 / 9999 works). zip refuses a combination the client cannot read |
skip-bedrock-players | true | Leave on. See §9 |
reassert-priority-ticks | 0 | Re-push N ticks after join so a later plugin does not sit on top of us. Off by default: it costs every player a second pack screen |
The title line the client shows above your description — World Specific Resources — is a hardcoded client string for server-sent packs. It cannot be changed. Only the description and icon are yours.
second-pack
A Java-only feature, off by default, for players you suspect of x-ray. In plain words:
- Pack 1 is
contents/— the server's own content: custom items, blocks, armour, tools, weapons, discs. Every
player gets it (output/pack.zip), and a player's own texture packs keep working underneath it for everything pack 1 does not change.
- Pack 2 is
contents2/— plain vanilla rock, ore, cave, water, lava and shader files, or your own "server
vanilla" look for those blocks. Only flagged players get it: output2/pack.zip is pack 2 under pack 1, in one download, served instead of output/pack.zip.
A server pack always sits above the packs a player picked, so a flagged player's game draws, top to bottom: pack 1, then pack 2, then their own packs, then vanilla. Anything pack 2 contains covers their packs — an x-ray pack that made stone see-through has nothing left to change — and anything it leaves out still comes from their packs.
Pack 1 wins over pack 2. Where both ship the same file, contents/'s copy is served, so a whole vanilla pack in contents2/ can never undo a custom block, the EGW jukebox or an item. Four kinds of file are not "one copy wins" in the game itself — atlases, sounds.json, language files and fonts are combined across packs — and zip combines those the same way (pack 2's part first, pack 1's on top). Block states are the exception: pack 1's file is served whole, which looks the same against a vanilla pack 2. A "server vanilla" pack 2 whose models draw pictures from outside textures/block/ gets the atlas entries those pictures need, as pack 1's content does. zip prints one info line with how many files the two layers share; that is information, not a problem.
Flag a player with LuckPerms:
lp user <name> permission set pf.pack.verify true lp user <name> permission unset pf.pack.verify (unflag)
or flag a group, and put players in it:
lp group <group> permission set pf.pack.verify true lp user <name> parent add <group> (unflag: parent remove)
With LuckPerms installed every one of these applies at once — a player's own flag, a group's flag, joining or leaving a flagged group, a temporary flag (settemp) running out: the player is sent the other pack — one reload, no reconnect — and only if it really is a different pack. A flagged player who joins is sent the second pack before join (one pack screen). Grant it globally, not per world: a per-world grant cannot be read before the player is in a world, so they get the default pack first and the second at join. Without LuckPerms there is no "permission changed" signal, so a change applies at the player's next join, /pf push <player>, or the next /pf zip. With second-pack.required and pack-host.kick.on-decline on (the defaults), a flagged player who declines the second pack is disconnected by their own game — that refusal is the enforcement. Bedrock players are never sent either Java pack.
Build pack 2 with the tool in the Packforge project, from the client jar of the version your server runs:
python3 tools/vanilla_verify.py <path to client-26.3.jar>
(On a 26.2 server, with 0.1.0, it was client-26.2.jar.) client-26.3.jar is Mojang's 26.3 client, sha1 e877b6a07acd633fb3bb475002175cec036e7b87 as Mojang's 26.3 version file lists it; a 26.3 layer has 80 shader files and a pack.mcmeta for [97, 1].
It writes assets/content-packs/VanillaVerify/egw_vanilla_verify/: vanilla's blockstates, models (with their parent models) and pictures for the 86 rock, soil, ore and cave blocks — stone and its variants, deepslate, dirt, gravel, sand, terracotta, the nether and end rock, every ore, and what lines or covers caves at ore depth (sculk, moss, grass, mud, cobblestone, bedrock, obsidian...) — the water and lava pictures, the block and item atlas files, and every core shader. Copy that folder into drop2/, set second-pack.enabled: true, /pf reload, /pf move, /pf zip.
Pack 2 must match the server's Minecraft version. Its shader files only work on the game version they came from, and pack 1 and pack 2 travel in one zip. A flagged player whose game cannot load those shaders — an older or newer version, for example through a version-bridging proxy — loses the whole second pack, custom items and blocks included, and with pack-host.kick.on-fail on (the default) is kicked on every join until you unflag them. Rebuild pack 2 whenever the server updates.
Every zip checks pack 2 and warns, grouped by kind and naming a few examples, about anything a vanilla x-ray pack could still change: the rock, ore and cave block states, their models and parent models (which must stay plain cubes: no emptied, unculled, oversized or glowing ones), their pictures (including ones that are see-through themselves), the water and lava pictures, the two atlas files, and every core shader file. It never stops a zip — a partial pack 2 still covers what it ships. A clean pack 2 gets one line: pack 2 x-ray check: it covers all 86 rock, soil, ore and cave blocks ... all 80 core shader files of Minecraft 26.3. The shader list is the one of the version the server runs (26.3: 15 terrain files and 65 others). A pack 2 that still carries 26.2's shader files (rendertype_clouds, text_background...) is named as built for another version: rebuild it from client-26.3.jar.
What it stops: x-ray done with a texture pack on a normal (vanilla) game — see-through pictures (stone, ores, dirt, sculk, lava...), emptied or reshaped models, atlas tricks, edited shaders, including mobs and chests drawn through walls and full brightness. What it does not stop: x-ray mods, which ignore packs entirely, and ore the server itself sends. Keep Paper's own anti-xray chunk obfuscation on (paper-world-defaults.yml, engine-mode 1 or 2) regardless. What it does not cover: every block outside those 86 — wood, leaves, glass, ice, plants, pointed dripstone and the like — still looks however the player's own pack says; nothing is mined through them. It is not covert either: the flagged player sees a pack prompt.
discs
verify-ogg-exists and require-bedrock-counterpart both default on and should stay on. A sounds.json naming an .ogg that is not in the pack produces a disc that plays nothing, logs nothing, and looks fine — the worst silent failure in the system. Both warn by name and never stop a content being placed; turning one off only stops the warning.
carriers
| Key | Default | Notes |
|---|---|---|
require-tripwire-updates-disabled | true | Paper's block-updates.disable-tripwire-updates must be true or tripwire carriers get reset by neighbours |
pool | [minecraft:tripwire] | Which blocks states may be allocated from — see §10 |
validation
refuse-vanilla-collisions defaults on: two contents shipping one vanilla-namespace file with different bytes are refused, naming both. Off, the later content overwrites the file with a warning. Byte-identical copies are never a collision.
strict is no longer read: a disagreement between a content's Java and Bedrock sides is always a warning (see §3). Boot and /pf reload say so once if your config.yml still sets it; delete the line.
items
| Key | Default | Notes |
|---|---|---|
derive-from-geyser.exclude-namespaces | gcrops, grims_nilla_fishin, grimsbp, grimsjukeboxes, grimsredstonewifi | No item is derived from a Geyser mapping in these namespaces, and their block states are not listed in /pf browse: their plugins make them. That includes a plugin's extension mappings named after it — Crops' hops ship as gcropsext_hops_blocks.json — so a mapping whose name starts with an excluded namespace and _, - or ext is hidden too. Helper-config items in them still load. Used as the default when an older config.yml has no such key; [] derives everything |
auto-update | true | Refresh outdated pack items on join and container opening while preserving player changes. Apply configuration changes with /pf reload. |
open-recipes-without-permission | false | Whether everyone can craft helper-config recipes whose permission is empty. Explicit pack permissions still apply. Run /pf reload after changing it. |
guards
All on by default, with the default settings; an older config.yml without the block gets them on. /pf reload applies a change at once. What each allows and refuses is the table in §3.
| Key | Default | Notes |
|---|---|---|
crafting-grid | true | Pack items go only into helper-config recipes that ask for them; grid repair needs two of the same pack item and keeps it that item. Off, vanilla grid repair turns pack gear into its vanilla base |
crafter | true | No pack item goes into a Crafter, and no Crafter makes a helper-config recipe. Off, a Crafter makes permission-gated recipes (Dreadplate, the scythe) for anyone |
anvil | true | No combining a pack item with a vanilla or different item. Material repair (minecraft:repairable) stays allowed on or off |
grindstone | true | No merging a pack item with a vanilla or different item. Disenchanting stays allowed on or off |
glyphs
Chat glyphs, rank badges, icons, emoji and the EGW font — §8. All on by default (but the text-tag example); an older config.yml without the block gets them on.
| Key | Default | Notes |
|---|---|---|
enabled | true | Everything below. Off: nothing is read, zipped or drawn, chat is untouched, and the next zip has Packforge-Glyphs.mcpack removed when the server stops |
font.style | egw-werks | The EGW style chat uses and tags are drawn in: egw-werks (condensed and machined, the EGW default), egw-hearth (rounded and soft) or egw-grimoire (an elegant serif), all three bundled in the jar. Blank or missing: egw-werks, else the first installed; a style named explicitly (an older install's egw-hearth) is kept; one that is not installed falls back the same way with a warning. /pf reload reads it, the next /pf zip draws it (Bedrock: after the restart that follows) |
font.replace-default | true | Make that style the default font, everywhere: chat, player names and nametags, the tab list, scoreboards, signs, item names, menus, books. Java: the pack's default.json draws every character the style has, vanilla's font the rest. Bedrock: the zip draws Bedrock's own font sheets (font/default8.png, font/glyph_00.png) from the style into Packforge-Glyphs.mcpack. With it on, Java chat is not set in egw:chat (so no chat-line marker) and Bedrock chat uses the alphabets only for hex colours Bedrock lacks. A config.yml without the key gets it on. /pf zip applies it; Bedrock after the restart that follows (§8, "The EGW font as the default font") |
chat-font.java.enabled | true | Write egw:chat into the pack and show chat in it to Java players whose pack loaded. While font.replace-default is on and a player's pack draws its default font in EGW, their lines are not set in egw:chat (they are EGW already); the font is still written, for formats that name it |
chat-font.java.parts.prefix / .name / .message | true | Which parts of a line get the font. Under LPC, mixed parts collapse to "the whole line, when prefix is on" (§8) |
chat-font.java.delivery | signed | How a line in the EGW font reaches a Java player: signed (Paper's chat; the client tags every such line "modified") or unsigned (an unsigned chat message; tagged "server message", cannot be reported). §8, "The chat-line marker" |
chat-shortcodes.enabled | true | :name: in chat becomes the glyph, for a sender the glyph allows — and so does each of its aliases (:lol:, §8 "Emoji"). Only players' screens change: the message other plugins and Discord relays read keeps :joy: as typed |
unicode-emoji | true | Typed Unicode emoji become the glyph that lists them (unicode: in its helper config): 😂 the EGW joy emoji, ❤️ the heart icon, 🌙 the moon, skin tones and U+FE0F forms included, the longest listed sequence first. In chat for each player whose pack draws the glyph (Java and Bedrock; anyone else sees the emoji as typed), for a sender the glyph allows; on signs as they are written. The console, logs and bridges read the message as typed; characters no glyph lists are never touched. Acts from the next message and sign (§8, "Emoji") |
placeholders.img | true | Enable legacy %img_<name>% aliases where available. Prefer Packforge’s own %packforge_…% placeholders. Restart after changing this compatibility option. |
bedrock-pack.enabled | true | Build Packforge-Glyphs.mcpack for Geyser's packs/ at zip (only with Geyser installed); a changed pack, or its removal when off, takes effect when the server stops |
bedrock.chat-font.enabled | true | Draw the EGW font into the Bedrock pack (one alphabet per colour) and write Bedrock players' chat in it. Off acts at /pf reload; on needs a zip and a restart. Needs bedrock-pack.enabled. While the pack Geyser loaded draws Bedrock's own font in EGW (font.replace-default), only text in a hex colour Bedrock lacks is written in an alphabet |
bedrock.chat-font.parts.prefix / .name / .message | true | Which parts of a Bedrock player's line are converted. Mixed parts under LPC are found by their text (§8) |
bedrock.tinted | true | Bedrock tints glyphs by the § colour, so the white alphabet shows Bedrock's own 28 colours. Unproven in game — the first in-game check (§8) settles it; false gives every colour the nearest drawn alphabet |
bedrock.colors | ember, coolant, brass, bone | Colours drawn as alphabets besides white, '#rrggbb' in quotes, most important first. A value that is not a colour is reported and left out |
bedrock.colors-from-luckperms | true | Also every hex colour in LuckPerms group prefixes, suffixes and colour meta, and in LPC's formats (only with LuckPerms installed). Colours Bedrock already has are skipped while tinted |
bedrock.nearest-distance | 48 | How far (RGB distance, 0 to 441) a colour may be from an alphabet's and still use it |
bedrock.reserved-pages | ['E8'] | Bedrock glyph pages other packs draw on: no glyph, badge, icon or alphabet is given a character there, on either edition. E8 is the EGW Sidebar pack's (TAB's egw-bedrock board). A glyph already on a listed page moves off it at the next /pf reload, reported with its new characters. An empty list reserves nothing; an entry that is not a page is reported and left out (§8, "Why those characters") |
bedrock.pixels-per-unit | 1 | How many pixels per Java unit every Bedrock glyph is drawn with — the EGW letters, tags, and (unless set apart below) icons, pictures and badges. Bedrock draws one glyph pixel per text pixel, so 1 is exactly the size of Bedrock's own text (the EGW art is halved to it, keeping every stroke) and 2 is twice that, every pixel of the art — how the packs seen at twice the text's size were drawn. 1 or 2. A glyph's characters never change with it. Needs /pf reload, /pf zip and a restart (§8, "Bedrock glyph size") |
bedrock.icon-pixels-per-unit | as pixels-per-unit | The same, for the built-in icons, the emoji and every other picture glyph: 2 keeps them twice the text's size with all their detail, beside letters at 1. At 1 a picture with a hand-drawn image-1x (every built-in emoji) shows that instead of its picture halved; at 2 it shows its full picture. Missing or blank: follows pixels-per-unit. Reload, zip and restart |
bedrock.badge-pixels-per-unit | as pixels-per-unit | The same, for the rank badges: 2 keeps the plates big and their labels crisp. Missing or blank: follows pixels-per-unit. Mini badges stay at 1 whatever it says (one character each, §8 "Mini badges"). Reload, zip and restart |
bedrock.lift | 1 | How many pixels above the text's baseline every Bedrock glyph stands, 0 to 8. Bedrock puts a glyph cell's bottom on the baseline and draws nothing below it, so a letter's tail (g y p q j _) shows only in the lift: 1 keeps one pixel of tail, as deep as Bedrock's own, with the letters a pixel up; 0 stands them exactly on the baseline with the tails cut off. At pixels-per-unit: 2 a tail is 2 pixels: use 2 (glyphs drawn at 2 rise at most 2). A picture is never cut, only raised to fit. /pf glyphs calibrate shows 0, 1 and 2 beside plain text. Reload, zip and restart. The keys this replaced (display-scale, anchor, baseline-offset) are no longer read; every load says so while a config.yml still has them |
bedrock.world-offset | 0 | Where Bedrock draws each glyph's world form — the characters nametags and signs are written with — in its 16-pixel cell: 0 is the middle (Bedrock centres a glyph cell on the line in world text, where chat puts its bottom on the baseline), positive lower, negative higher, -8 to 8 pixels. Chat and the tab list never move with it. A picture as tall as the cell (at 2 pixels a unit) cannot move. /pf glyphs calibrate world shows -2 to 2 on armor stand nametags. Reload, zip and restart (§8, "Bedrock world text") |
builtin.icons | true | The 69 EGW icons shipped in the jar (:heart:, %img_heart%), with Discord's names for the emoji they draw as aliases (:gem:) and the Unicode emoji that become them (❤️ 🔥 🌙 …). A helper-config glyph of the same name takes one's place. Off: none drawn; their characters stay reserved |
builtin.emoji | true | The 79 EGW emoji shipped in the jar, named as Discord names its default emoji (:joy:, :thumbsup:, :100: …), each with its aliases (the earlier EGW names among them: :laughing:), the Unicode emoji typed for it, and its hand-drawn 8×8 Bedrock art. Everyone may use them, as the icons. A helper-config glyph of the same name takes one's place (so the egw_emoji content folder from the assets is not needed: delete it). Off: none drawn; their characters stay reserved. The next /pf zip applies it (§8, "Emoji") |
builtin.badges | true | The EGW badges shipped in the jar. Their editable copy is items/packforge/badges.yml (marked packforge-builtin: badges), written whenever no marked copy exists. A copy an earlier release wrote keeps working, and the badges a later release adds are drawn beside it (§8, "A copy written by an earlier release"). Off: no badges, nothing written, and a marked copy is not read |
signs.enabled | true | :name: (or an alias, or with unicode-emoji a typed emoji the glyph lists) on a sign a player writes becomes the glyph — its world form, which Bedrock draws level with sign text — in white (needs pf.glyphs.signs and the glyph's own permission). Off acts from the next sign; pasted glyph characters are taken out either way |
example-config | false | Write the text-tag example (items/packforge/group-tags.yml: ADMIN, MOD, VIP, MEMBER as tag: glyphs) once. Was on before the badges; an install that has the file keeps it |
debug
| Key | Default | Meaning |
|---|---|---|
timings | false | Log, prefixed [timings], how long each phase of /pf zip, move, reload and the startup item load took (async and main thread apart), and one line per server tick in which Packforge ran anything on the main thread it times — item installs, recipe steps, recipe-book unlocks, the join push, pack status answers — with the milliseconds of each. For chasing a stall; /pf reload applies it at once |
Menus, heads, bosses
heads. controls the shared player-head pool. bosses. sets the default bossbar colour, overlay and 48-block view radius. sounds., bound-items. and player-list.* come from the absorbed menu engine.
7. Recipes
Add a pack
1. Put the Java pack folder, the .mcpack and the Geyser mapping(s) in drop/<name>/
2. /pf validate ← dry run, writes nothing
3. /pf move ← drop/ empties
4. /pf zip ← players get it
5. Restart for the Bedrock side (see §9)
Update a pack — same, with the whole new release in drop/. move reports "updating, which it already owns", takes back the files the old release placed that the new one no longer ships (each named, and moved to taken-back/<time>/ rather than deleted), and replaces the old .mcpack even when its file name changed. No need to remove anything, and the drop does not have to be laid out the way the last release was.
Patch one file — drop just that file, laid out the way the pack lays it out:
- a content namespace folder: the folder with only the fixed file in it, e.g.
drop/gdd/textures/item/prospector_pickaxe_consolidation.png;
- one helper config:
drop/<namespace>/configs/<file>.yml, e.g.drop/egw_showcase/configs/dreadplate_recipes.yml; - a pack folder: the folder without its
pack.mcmeta, e.g.drop/GrimsCrops-Java-0.15.4/assets/gcrops/….
Each replaces only what it carries; the report says it was read as a patch. Or edit contents/ directly and /pf zip.
Remove one asset — delete it from contents/ and run /pf zip. contents/ is a plain directory.
Brand the pack — set pack-host.description, drop icon.png in the plugin folder, /pf zip, rejoin.
Register heads for Bedrock — hold them, /gmenu heads harvest, then restart Geyser.
Find an item: use /pf browse, its compass search, or /pf browse <words>. Search matches IDs, names, packs and block states. Items and blocks appear together in their pack category. A helper config’s categories: can define names, icons and order; unlisted items use their derived category.
Switch off one helper config — for a folder copied straight into contents/, rename it to .yml.off there and /pf reload (or zip); its copy in items/ is removed and named in the report. For content placed by /pf move there is no copy in contents/: rename items/<content>/<file>.yml to .yml.off and /pf reload. A later move of a release that ships the file puts it back.
Recipes — a helper config's recipes: crafting_table: and recipes: smithing: blocks are registered as they are written: shaped with a pattern, shapeless without, smithing as §3 describes.
- Recipe keys use
<info.namespace>:<recipe id>, such asegw_gems:emerald_helmet.
For example, egw_showcase:dreadplate_helmet. Recipe unlocks are stored by key. Duplicate keys are renamed with the source filename and reported. A key already registered by another plugin is reported rather than overwritten.
- Only changes are registered. Each recipe is fingerprinted — pattern, ingredients, result, amount, and every pack
item by its definition hash — and a move, zip or reload removes and adds only the recipes whose fingerprint changed, in one batch of removes and one of adds (Paper 26.3's removeRecipes and addRecipes), each of which resends recipe books once. An unchanged set registers, removes and resends nothing, and checks only the crafting recipes Packforge registered are still there; the 792 smithing locks' keys are looked up at startup and when the set gains one, not at every zip. A permission edit alone needs no re-registration and applies at once. A data pack reload (/minecraft:reload) drops every recipe a plugin registered; Packforge adds its own back straight after it, in one batch, and says so in the server log. A recipe the server turns down part-way through a batch is named; the ones before it are in and the ones after it go in a second batch.
- Permission: a recipe's
permission(for a shaped recipe,permission_suffixfirst) is granted by
Use pf.recipe.<permission> for permission-gated recipes. For a recipe with an empty permission, items.open-recipes-without-permission: true opens it to ordinary players. Explicit permission suffixes still require a grant.
- Recipe book: each recipe is filed under Equipment (armour, tools, weapons), Building (blocks) or Misc by what
it makes, grouped with the other recipes for the same item. Every player allowed to make a recipe has it unlocked: at join, after every /pf reload, move and zip, and when they pick up or click a pack item the recipe makes or takes (so permission changes during a session can appear then). This is what lets Bedrock players craft them at all: Geyser builds the Bedrock crafting screen only from the recipe-book entries the server sends. A player who may not make a recipe is never shown it.
- Crafting recipes are made at a crafting table or in the player's own grid, never by a Crafter
(guards.crafter), and a pack item goes into one only where the recipe names that item (guards.crafting-grid): see §3.
Hand out an item — /pf give <player> <ns:id> [amount] [silent], from the game or the console. For the Dreadplate wings, which have no recipe on purpose: /pf give Grimsby egw_showcase:dreadplate_wings.
Music discs — an item with jukebox_disc: song: gets its song. zip writes every song into the packforge_songs datapack in the world's datapacks/ folder, using the sound event the config's sounds: declares and the length of the .ogg itself. Songs load at server start, so the zip report says when a restart is needed; until then the disc exists but plays nothing.
For other plugins — two services on Bukkit's ServicesManager, from Packforge-api.jar (com.evergrims.packforge.api):
PackforgeItems—item("namespace:id")hands out a fresh stack,idOf(stack)says which item a stack is
itemIds() lists known namespace IDs. The item updater, smithing recipes and station guards recognise tagged pack items, not a model alone; functional plugin stacks retain their own plugin’s behaviour.
PackforgeBlocks—place("namespace:id", location),idAt(block),remove(block),blockData(id)for
custom blocks a helper config defines with behaviours: block, on the carrier state the pack's Geyser mapping names. No physics is applied, so neighbours cannot rewrite the state as it is set. Plus declare(id, placedModelType, model) — see below.
PackforgeHost—isServing(): a pack is built, hashed and offered at a URL that resolved.servesNamespace(ns):
that, and the served zip contains assets/<ns>/. Both read what was recorded when /pf zip (or boot) published the pack, so they are cheap enough to ask at the moment something is drawn. This is the question a plugin asks before switching on textured visuals, not "is Packforge installed": before the first zip, with no address, or with that content not placed, the answer is no and the plugin should draw its vanilla look (Grims Chests 0.4.8 drew barrier-masked chests with models no client had).
EGW plugins use Packforge’s interfaces to obtain their content when Packforge is running.
Custom blocks a plugin declares. A pack that ships no helper config — the live Grims Nilla Fishin Java pack is one — has nothing defining its blocks, so its plugin declares them instead, on enable:
boolean cage = blocks.declare("grims_nilla_fishin:fish_trap", "REAL_TRANSPARENT",
"grims_nilla_fishin:block/basic_fish_trap");
- The type is a helper config's
placed_model.type:REAL_TRANSPARENT(chorus),REAL(mushroom blocks),
REAL_NOTE (note block) or REAL_WIRE (tripwire). A model without a namespace takes the id's.
- The block gets a state the way a helper-config block does: the one its Geyser mapping names (by the id or the
Custom blocks use the supplied Geyser mapping where available. Automatic state assignment is limited to chorus and mushroom blocks; note-block and tripwire content needs an explicit mapping.
- From then on
blockData,placeandidAtanswer for it, and the next/pf zipdraws it on Java. - It is saved in
plugins/Packforge/declared-blocks.yml(with the plugin that declared it), so a zip run before
that plugin has loaded still draws it. Declaring the same thing again — every boot — writes nothing.
- A helper config defining the same id wins; the declaration waits behind it, and the load report says so.
- It returns whether the block is drawable now: it has a state and
PackforgeHost.servesNamespaceis true for
its model's namespace. The carrier blockstate pointing that state at the model is written by /pf zip, so the very first declaration shows on Java from the zip after it.
- To remove one (the plugin is gone for good), delete its entry from
declared-blocks.yml, then/pf reloadand
/pf zip. The reload reads the file again and names what it no longer declares, so the zip stops drawing it and the next declaration from another plugin cannot write it back. A plugin that is still installed simply declares it again at its next start. A file edited into something that no longer reads is reported and left alone: the declarations already known are kept, and nothing is written to the file until it reads again.
/pf status counts custom blocks on the Items line, and how many came from declarations. The boot log has one Custom block <id> -> <carrier>[<state>] (declared by <plugin>; ...) line per declared block.
8. Chat glyphs, rank badges, icons and the EGW font
Seen in game on Bedrock: twice the size, then overlapping calibration glyphs. Bedrock draws a glyph at one glyph pixel per text pixel, and the EGW art has two pixels per Java unit. Bedrock glyphs are now drawn at one pixel a unit by default — the size of Bedrock's own text — on standard 256-pixel sheets: look at
assets/branding/egw-font/preview-bedrock-1x.pngbefore deploying, then run/pf glyphs calibrateon Bedrock once to settle the lift ("Bedrock glyph size", below). The rest is built from the 26.2 client's own font code, the 26.2 server's chat code, LPC 4.4.0's own classes, TAB 6.1.0's placeholder code and the live Geyser jar, and covered by tests, but not yet seen; the checks to make are at the end of this section.
Seen in game on Bedrock: chat lines up, but nametag badges sat low. On the owner's server the ADMIN badge in a player's nametag (TAB's
tagprefixfrom the LuckPerms prefix) showed its top at the middle of the name, about half a text height too low, while the same badge in chat sat right. Bedrock centres a glyph cell on the line in world text and puts its bottom on the baseline in chat, so every glyph now has a world form for nametags and signs: set TAB'stagprefix: "%packforge_world_prefix%",/pf zip, restart, and run/pf glyphs calibrate worldonce ("Bedrock world text (nametags, signs)", below).
Seen in game on Bedrock: the sidebar places glyphs like a nametag, and breaks wide ones apart. On the owner's board (TAB's Bedrock scoreboard) the chat-form badge and the coin, globe and clock icons sat low, as in a nametag, and the full badge showed a visible gap between each of its characters: Bedrock leaves one between glyphs there, and the only cure is a glyph that fits one character. So sidebar lines take world forms, and a badge there takes its mini badge — the emblem on its plate, one character, 8 pixels tall beside 7-pixel capitals:
%packforge_world_white_badge_<rank>_mini%("Mini badges (the Bedrock sidebar)", below).
What it does. A glyph is a picture that sits in text like a letter: a rank badge in front of a name, an icon in a message, on a sign or in the tab list. Packforge draws them for both platforms from one definition:
- The EGW font everywhere (
glyphs.font.replace-default, on by default) — the EGW style becomes the default font
on both editions: chat, player names and nametags, the tab list, scoreboards, signs, item names, menus and books are all in it, and any character it does not draw keeps vanilla's on Java ("The EGW font as the default font", below).
- Java —
/pf zipputs each glyph into the pack's default font, and the EGW style with them. With the default font
left alone, Java players get chat in the EGW font instead (egw:chat): any character the EGW font does not draw falls back to the default font, so every message still reads, and one white font gives every colour, hex included.
- Bedrock — the same zip builds
Packforge-Glyphs.mcpackfor Geyser'spacks/(only when Geyser is installed).
Restart the server after a zip that changed a glyph: Geyser reads packs only when it starts, so a changed pack waits beside the one Geyser serves (Packforge-Glyphs.mcpack.next) and takes its place when the server stops. The zip report says when a restart is needed. Bedrock cannot be sent a Java font, so the zip draws Bedrock's own font sheets from the EGW style into that pack (with the default font replaced), and the EGW font's letters too — once in white and once per chat colour — so chat text in a hex colour is written in them, in exact custom colours that Bedrock's own 28 colours cannot show ("EGW font on Bedrock", below).
Built in, like the font. The jar carries the eleven EGW badges — the eight ranks (badge_player … badge_owner) and three for who a player is rather than their rank (badge_beta, badge_crowsmp, badge_egw) — the 69 EGW icons (heart, crown, coin, beta …) and the 79 EGW emoji, named as Discord names its default emoji (joy, thumbsup, 100, full_moon …, with aliases such as :lol: and the Unicode emoji typed for them), so a fresh install has them with nothing in contents/. Badges are drawn by Packforge itself from colours in a helper config, so recolouring one is a config edit and a /pf zip, and a server adds its own rank by copying a block. Tags — words drawn in the EGW font — and hand-drawn pictures (any PNG the pack ships, or a compatible pack's font_images) work beside them.
Try it
1. /pf zip builds both packs (Java and Packforge-Glyphs.mcpack) and sends the Java pack to whoever
does not have it yet
2. restart only for Bedrock, and only when the zip report says the glyph pack changed: the new
one goes into Geyser's packs/ as the server stops; this is also what brings Bedrock
players their chat in the EGW font
/pf glyphs calibrate on Bedrock, once: do glyphs stand on Bedrock's own baseline? ("Bedrock glyph size")
/pf glyphs calibrate world on Bedrock, once: are badges level with the name on a nametag? ("Bedrock world
text"); TAB's tagprefix: "%packforge_world_prefix%" puts them on players' nametags
3. /pf glyphs badges, tags, icons, emoji and images, grouped, with a preview — click one to copy it
4. /pf glyphs badge_admin the exact LuckPerms command for the admin group's prefix — click it, check it, send it
5. type :coin: :crown: icons, for everyone (chat shortcodes). With LPC, :heart: :star: :check: :cross: are
LPC's own emoji (❤ ★ ✔ ✖) first — and those characters become the EGW icons anyway
(glyphs.unicode-emoji), so nothing needs removing from LPC's emoji.replacements
6. type :joy: 😂 👍🏽 emoji, for everyone: Discord's name (:joy: :thumbsup: :100:), an alias (:lol: :+1:)
or the emoji itself from a phone keyboard; a Discord relay forwards :joy: as typed and
Discord shows its own 😂 ("Emoji", below)
The EGW font's three styles are bundled in the jar (fonts/egw/, with the font's own README): egw-werks (condensed and machined, the default), egw-hearth (rounded and soft) and egw-grimoire (an elegant serif); glyphs.font.style picks one. A config.yml without the key, or with it blank, gets egw-werks; one that names a style keeps it — an install whose config.yml was written by an earlier release says egw-hearth, so change it there to follow the new default. To switch, set it, /pf reload, /pf zip, and restart if the zip report says the Bedrock glyph pack changed (a new style redraws every Bedrock alphabet). A style dropped into plugins/Packforge/fonts/egw/ (its .json beside its .png, in the font's egw-bitmap-font/1 format) replaces a bundled one of the same id. /pf status names the style in use on its Items line, and says whether the served pack carries the chat font yet. With no style installed, tags are not drawn (the load report says so) and chat keeps the default font; badges and icons do not need it.
The EGW font as the default font
What players see. With glyphs.font.replace-default: true — the default, shipped on for EGW — every piece of text is in the EGW style (glyphs.font.style), player names included: chat, nametags, the tab list, the sidebar, signs, item names and lore, menus and their titles, books, titles and the action bar. Java players get it with the pack the next /pf zip sends; Bedrock players after the restart that follows it (the zip report says EGW font replaces the default font (style egw-werks) on Java and Bedrock, and /pf status says default font EGW on Java (served) and Bedrock (live) once both are in place). To go back, set it to false, /pf zip, restart.
Java: the pack's default.json, stacked above vanilla's. The 26.2 client reads every pack's copy of assets/minecraft/font/default.json and puts the higher pack's providers first (FontManager.loadResourceStack), and vanilla's own copy is only three references — minecraft:include/space (the space and U+200C), minecraft:include/default (its bitmap sheets) and minecraft:include/unifont (the Unicode font). So Packforge's default.json holds the glyphs, then the EGW style's space and bitmap providers, and nothing of vanilla's: a character the style draws is EGW's, and any other — an accent it lacks, Greek, CJK, emoji — falls through to exactly what vanilla draws. The EGW providers use the same sheet, height and ascent as egw:chat (11 units, baseline 9 down: capitals on vanilla's 7 units, descenders to the bottom of the 9-unit line), so line height, baseline and text shadow are vanilla's. They carry vanilla's own "filter": {"uniform": false}, so a player who turns on Force Unicode Font still gets the Unicode font. A content that ships its own default.json drawing letters the style draws gives way to the style (the zip report names the clash; turn the option off to keep the content's).
Java chat needs nothing more. A player whose pack draws the default font in EGW sees every line in it already, so chat no longer sets egw:chat on their lines — which means no chat-line marker: lines stay plain signed chat, reportable, with no bar ("The chat-line marker", below). glyphs.chat-font.java.parts and .delivery only matter again with the option off. Players without the pack see vanilla's font, as before.
What the client measures. The 26.2 client measures every text through the font it draws it in — sign lines against the sign's width, book pages wrapped at 114 pixels for 14 lines, chat at its own width, menus and the tab list as they lay out — and no screen holds a table of vanilla's widths, so nothing in the client breaks. The EGW styles are narrower than vanilla's font, though (Werks about 25 to 30% on ordinary text, Hearth and Grimoire about 15%), so text laid out for one font and read in the other can look off: a sign line or a full book page written by a player with the pack runs past the board, or onto a 15th line, for a player without it; and anything a plugin pads or centres with vanilla's widths — centred chat lines, columns of spaces on a scoreboard, menu titles that place a background picture with negative-space characters after their text — sits a little off. Text that only follows a picture (the usual menu-title layout) is unaffected.
Bedrock: its own font sheets, drawn from the style. A Bedrock pack cannot add a font, but it can replace Bedrock's own bitmap font, and /pf zip does, at Bedrock's own text size (one pixel a unit, capitals 7 pixels, the EGW art halved the same way the alphabets are). Bedrock reads two sheets for it (checked against the vanilla Bedrock 1.26 resource pack, whose font/font_metadata.json names only the title and smooth fonts):
| Sheet | What it is | What Packforge puts there |
|---|---|---|
font/default8.png | 128 pixels, 16 by 16 cells of 8: printable ASCII at its own code, accented letters and code page 437's symbols (box drawing, blocks, Greek, maths) in a fixed order; a glyph is as wide as its last visible column, and Bedrock adds a pixel after it | every ASCII character from the style; the accented letters it draws; accented capitals (and å, İ, Ş, Ŵ, ž, ğ) composed from the style's own letter and accent, squeezed to the cell as vanilla's sheet does (accent, a row, a 5-row capital); box drawing, blocks and shades ruled as one-pixel lines and fills; · ∙ ± ÷ ¬ ≤ ≥ ≡ ≈ ■ built from the style's own dot, bars and signs |
font/glyph_00.png | the Unicode page for U+0000 to U+00FF, 16-pixel cells. Bedrock draws a whole line from its Unicode pages when the line holds any character default8.png lacks — so every chat line with a badge, an icon, an EGW alphabet letter or a — | every character of the page the style draws, accented capitals included, placed exactly as the EGW alphabet letters and the glyphs are (glyphs.bedrock.lift above the cell's bottom) — a line drawn from these pages is all one kind of character, so its plain text, its badges and its icons line up by construction — each keeping its gap to the next with a faint last column |
The space is never drawn on either sheet: Bedrock keeps its own width for it. What nothing draws shows as nothing on Bedrock while the option is on, because the pack's sheet replaces vanilla's whole and Mojang's font art is not Packforge's to ship: Greek letters, ∞ ∅ ∈ ∩ ⌠ ⌡ √ ⁿ ², ½ ¼ ª º ƒ © ™ ® Œ œ, and on glyph_00.png the rarer Latin-1 signs (¢ ¤ ¥ ¦ § ¨ ¯ ³ ´ µ ¶ ¸ ¹ ¾ Ð Þ ð þ). The zip report lists them. Characters on Bedrock's other pages — emoji, —, ★, ✔, other scripts — are untouched and stay Bedrock's own. Look at assets/branding/egw-font/preview-default8.png for the sheet and lines of Bedrock text laid out from it.
Bedrock chat. With Bedrock's own font in EGW, plain chat is EGW already, in Bedrock's 28 colours, so the alphabets are used only where they still add something: text in a hex colour Bedrock lacks, written in that colour's alphabet (or one within glyphs.bedrock.nearest-distance of it). White text, Bedrock's own colours and hex colours with no alphabet near them are left as text (Geyser rounds the last to its nearest colour). This follows the pack Geyser loaded at startup, so until the restart after the zip, chat converts as before.
Bedrock players who see their own font anyway. Bedrock's chat settings let a player switch the chat font to the smooth one (Noto Sans); that player sees Noto Sans, not these sheets — nothing a pack can change. Screens built with Bedrock's newer Ore UI draw their own TrueType font too, and the Minecraft Ten titles are not the bitmap font. Everything else Bedrock draws with its bitmap font — chat, nametags, signs, the sidebar, forms, item names — is EGW.
Not yet seen in game. The Bedrock half is built from the vanilla sheets' layout and the Bedrock community's documentation, not from Bedrock's code: that a line holding a glyph is drawn from the Unicode pages, and that Bedrock adds a pixel after a default8.png glyph. The checks at the end of this section settle it. /pf glyphs calibrate keeps its meaning: it compares glyphs with the text beside them, and glyph_00.png's letters follow the lift you settle on, so whatever lift lines a badge up with Bedrock's text lines up the letters of a badge line too.
World text is where the two rules can part. A nametag or sidebar line places a glyph cell in the middle of the line, which is why badges and icons there are written in their world forms (centred, "Bedrock world text" below), while glyph_00.png's letters stand where chat wants them, at the lift. What was seen in game so far (a chat badge about 4 pixels low on a nametag, beside a level one in chat) says such a line takes its plain letters from default8.png: then nothing changes there, and only a letter default8.png lacks (Ã, Ý, ð) sits a few pixels low in a name. If a nametag or the sidebar draws the whole line from the Unicode pages after all, the name's letters come out about 3 pixels below its world-form badge, icon or mini; glyphs.bedrock.world-offset: 3 (4 less the lift, for text-sized pictures) puts them level again. Set it by hand: the stands of /pf glyphs calibrate world only try -2 to 2.
The rank badges
Small machined plates — a 45° chamfer on two corners, a one-pixel rim and bevel, an emblem at the left and the label in the Werks badge capitals — designed in assets/branding/egw-badges/ and drawn by Packforge exactly as the designer's make_badges.py draws them (a test compares every badge with the designer's PNG pixel for pixel).
| Glyph | Label | Emblem | LuckPerms group | Priority |
|---|---|---|---|---|
badge_player | PLAYER | person | default | 100 |
badge_egw | EGW | diamond (the EGW mark) | egw | 101 |
badge_crowsmp | CROWSMP | crow | crowsmp | 102 |
badge_beta | BETA | flask | beta | 105 |
badge_vip | VIP | gem | vip | 110 |
badge_booster | BOOSTER | bolt | booster | 120 |
badge_mod | MOD | shield | mods (live's name for it) | 130 |
badge_admin | ADMIN | gear | admin | 140 |
badge_sysadmin | SYS-ADMIN | terminal | sysadmin | 150 |
badge_dev | DEV | code | dev | 160 |
badge_owner | OWNER | crown | owner | 170 |
The priorities climb with the rank, so a player in several groups shows the highest badge, and all sit above live's old text prefixes ([Admin] at 80, [Mod] at 60). Colours are the designer's EGW-palette values — placeholders until the owner's final colours arrive.
BETA, CROWSMP and EGW are for people, not ranks: beta testers (a parchment plate, a bubbling test flask), the owner's friends from CrowSMP (a night-slate plate, a crow's head with a brass eye — original art, not CrowSMP's own logo) and the owner's other friends (the EGW diamond mark on a soul-fire plate in a brass frame). Their priorities sit between PLAYER (100) and VIP (110): one of them replaces PLAYER for a member of its group, and any staff or supporter rank above it still wins (LuckPerms shows one prefix, the highest). Their LuckPerms groups — beta, crowsmp, egw — are not made by Packforge, and nothing here needs a new TAB group: give each existing group its prefix and the rest follows.
| Where | BETA | CROWSMP | EGW |
|---|---|---|---|
LuckPerms prefix (chat, the tab list via %luckperms-prefix%) | /lp group beta meta setprefix 105 "%img_badge_beta% " | /lp group crowsmp meta setprefix 102 "%img_badge_crowsmp% " | /lp group egw meta setprefix 101 "%img_badge_egw% " |
TAB tabprefix | %packforge_white_badge_beta% | %packforge_white_badge_crowsmp% | %packforge_white_badge_egw% |
TAB tagprefix (above the head) | %packforge_world_white_badge_beta% | %packforge_world_white_badge_crowsmp% | %packforge_world_white_badge_egw% |
| The Bedrock sidebar (mini) | %packforge_world_white_badge_beta_mini% | %packforge_world_white_badge_crowsmp_mini% | %packforge_world_white_badge_egw_mini% |
/pf glyphs badge_beta (and the other two) prints the same prefix command to click; _DEFAULT_'s tagprefix: "%packforge_world_prefix%" already gives the nametag form of whatever prefix a player has. The icon beta (a lab flask, :beta: in chat, %img_beta% in a prefix or message) goes with the BETA badge. They reach players with the next /pf zip, and Bedrock players after the restart that follows; on a server whose items/packforge/badges.yml an earlier release wrote, nothing needs editing ("A copy written by an earlier release", below). assets/branding/egw-badges/preview-new-badges.png shows the three beside the others, in chat and at Bedrock's size.
Where they live. The set ships in the jar and is written to items/packforge/badges.yml — its editable copy — whenever no helper config under items/ is marked packforge-builtin: badges (that line is at the top of the copy). While a marked file exists, it is the badge set: the jar's copy is not read, a badge deleted from the file is gone, and the file may be moved anywhere under items/. Delete it to get the release's own badges back (it is written again, fresh, at the next start or /pf reload). glyphs.builtin.badges: false drops the set: nothing is written and a marked file is not read. A file already at items/packforge/badges.yml without the marker is someone's own and is never written over (the report says so, and the jar's badges are drawn). A copy that stops parsing — one tab-indented line is enough — is not written over either: the load report names it with the parser's error, and the release's badges stand in until it is fixed, so fix it before the next /pf zip (§9).
A copy written by an earlier release. items/packforge/badges.yml is written once, so a server that ran an earlier release keeps the copy that release wrote — and that copy never names a badge added since. So the badge set is versioned: the jar's file says badge-set: 2, each badge it added says since: 2, and a copy without the line (every copy written before it existed) counts as set 1. At every load, the jar's badges newer than the copy's set — today badge_egw, badge_crowsmp and badge_beta for a set-1 copy — are drawn from the jar beside it, with their minis, LuckPerms groups and priorities, and the load report says so in one info line. Everything else is still the copy's: its recolours, its own badges, and a badge it was written with and its owner deleted stays deleted. To make one of the new badges yours, paste its block (below) under glyphs: in the copy — from then on the copy's block is drawn, on the same characters, and the line stops naming it. To go without one, write badge_beta: false (or badge_crowsmp: false, badge_egw: false) under glyphs:; badge_beta_mini: false drops only the mini. Leave badge-set: and since: as they are. A copy written by this release already holds the three blocks:
badge_egw:
badge:
label: 'EGW'
emblem: diamond
colors: {plate: '#8000ff', rim: '#a8772f', highlight: '#9a4dff', shade: '#4a1a8c', label: '#eee8dd',
emblem: '#e6aa55', emblem-accent: '#eee8dd'}
shortcode: false
luckperms: {group: egw, priority: 101}
badge_crowsmp:
badge:
label: 'CROWSMP'
emblem: crow
colors: {plate: '#466480', rim: '#14111c', highlight: '#6cb0c8', shade: '#2c2539', label: '#eee8dd',
emblem: '#14111c', emblem-accent: '#e6aa55'}
shortcode: false
luckperms: {group: crowsmp, priority: 102}
badge_beta:
badge:
label: 'BETA'
emblem: flask
colors: {plate: '#bdb1a4', rim: '#14111c', highlight: '#eee8dd', shade: '#8a8078', label: '#170828',
emblem: '#170828', emblem-accent: '#77a65a'}
shortcode: false
luckperms: {group: beta, priority: 105}
Recolour a badge. Edit its colours in items/packforge/badges.yml, /pf zip, and restart when the zip report says the Bedrock glyph pack changed. Its characters do not change, so every LuckPerms prefix holding it shows the new colours with nothing to re-copy.
Add a badge. Copy a block, give it a new name (badge_builder), a label, an emblem, a plate colour and its LuckPerms group; /pf reload, /pf zip, then /pf glyphs badge_builder for its prefix command:
badge_builder:
badge:
label: BUILDER
emblem: icon:pickaxe # any built-in icon, in its own colours
colors:
plate: '#e6aa55' # the only colour you must give
shortcode: false
luckperms: {group: builder, priority: 125}
Badge reference (a badge: section in any helper config):
| Key | Default | Notes |
|---|---|---|
label | — | The badge capitals: A–Z, 0–9, - + ! . and the space; lower case is drawn as capitals. A character they lack is left out and the load report names it. May be '' when there is an emblem (an emblem-only badge) |
emblem | none | A badge emblem — person, gem, bolt, shield, gear, terminal, crown, code, hammer, flask, crow, diamond (the EGW mark), star — or icon:<name> for any built-in icon (drawn in its own colours, centred on the label as the icons are drawn to sit on a line), or none. A plain icon name that is no emblem works too. An unknown one is reported and the badge drawn without |
colors.plate | required | The face. A badge without a readable plate colour is refused, by name |
colors.rim | #14111c | The 1 px outline (EGW's outline colour) |
colors.highlight / colors.shade | the plate, lightened / darkened | The bevel: lit along the top and left, shaded along the bottom and right. shadow is read as shade |
colors.label | #170828 or #eee8dd | EGW ground or bone, whichever reads better on the plate |
colors.emblem / colors.emblem-accent | the label's / bone or the highlight | The emblem's two colours (# and * in the designer's emblem art). emblem_accent is read too |
corners | chamfered | chamfered (the Werks cut) or square |
bevel | true | false: a flat face, no highlight or shade |
mini | on | The badge's mini badge, <name>_mini ("Mini badges", below): false for none, or a section of what the mini changes — emblem, colors (any of the colours above; a plate works out the rest from itself), corners, bevel. Left out, everything follows the badge. A badge without an emblem gets none unless mini.emblem names one |
shortcode defaults to false for badges, as for tags: with it on, anyone allowed could type :badge_admin: and wear it. luckperms.group and luckperms.priority are what /pf glyphs writes the prefix command for.
Sizes. (Mini badges are one character everywhere: "Mini badges", below.) On Java a badge is one character: 18 pixels tall at two to a unit, height 9 and ascent 8, so the plate centres on the Werks capitals beside it and fills one chat line. On Bedrock it is the plate rows alone (8 units, the height of a glyph's box) cut every 8 units — 3 to 7 characters, the designer's own split — drawn at glyphs.bedrock.badge-pixels-per-unit: at 1 (the default) the plate is 8 pixels tall, a line of Bedrock text, with its label halved colour by colour so it still reads; at 2 it is the designer's full 16 pixels. The pieces join exactly: each is measured from its first column to its last, so no gap or overlap is left between them in chat — but Bedrock's sidebar leaves a gap between them all the same, which is what the mini badges below are for. The badge capitals' W is 10 pixels (5 units) wide, the only one wider than 8: drawn as an M turned over, 8 wide, its middle stroke fell between Bedrock's pixels and halved into an H (OWNER read DHNER, EGW EGH). So OWNER — and any label of your own with a W — is one unit wider than in the release before. OWNER keeps its characters (still five cells); a label of your own that the extra unit takes past a cell's edge is handled like any glyph that grew ("Characters are permanent", below).
Mini badges (the Bedrock sidebar)
Every badge with an emblem also comes as a mini badge, <name>_mini — badge_player_mini … badge_owner_mini for the eleven built-in ones, badge_beta_mini, badge_crowsmp_mini and badge_egw_mini among them: the badge's emblem on its plate, in its colours, with no label, written with one character.
Why. Bedrock leaves a gap between two glyph characters on its sidebar — seen on the owner's board, where a full badge (3 to 7 characters) showed a break between every piece — and wiki.bedrock.dev's only cure is a glyph that fits one character. A mini has no break to show.
| Where | Use | Write |
|---|---|---|
| The Bedrock sidebar (TAB's Bedrock scoreboard) | the mini | %packforge_world_white_badge_admin_mini% |
| Anywhere else a badge must not break | the mini | %packforge_world_white_<name>_mini% (world text), %packforge_white_<name>_mini% (the tab list) |
| Chat, the tab list, nametags, the Java sidebar | the full badge | as in the rest of this section |
Sidebar lines take the world form (%packforge_world_white_…%), because Bedrock's sidebar places glyphs as its nametags do ("Bedrock world text", below); the coin, globe and clock on the board want theirs too (%packforge_world_white_coin%). Java draws a world form exactly as the glyph, so this is safe on a board both editions see. The EGW TAB kit (EGW-TAB 1.2.0) does exactly this: its Bedrock board's first line is %condition:egw-mini%, a condition chain of its own whose answers are %packforge_world_white_badge_<rank>_mini%, while the Java board keeps the full badges (%condition:egw-badge%).
Size. On Bedrock a mini is drawn at one pixel a unit, whatever badge-pixels-per-unit says: its plate is 8 pixels tall — Bedrock's capitals are 7, so it stands one pixel (about 14%) taller than the letters beside it, the nearest whole pixel to a fifth taller — and 9 to 12 pixels wide, with the one-pixel gap after it inside its 16-pixel cell. At badge-pixels-per-unit: 2 none of the eleven would fit one cell (18 to 24 pixels and a 2-pixel gap) and each would stand twice the text's height, so minis stay at 1 there and the zip report says so. On Java a mini is one character too, drawn from the same art at the badges' usual height (9 units, ascent 8).
Colours follow the badge. A mini is made from its badge at every load: the emblem, every colour (given or worked out), the corners and the bevel are the badge's, so a recolour of badge_vip in items/packforge/badges.yml recolours badge_vip_mini with it — and an existing copy of that file needs nothing added to get the minis. Its LuckPerms group, priority and shortcode setting are its badge's too, but a mini is never a group's prefix: at the badge's priority it would replace the badge in chat and the tab list, so /pf glyphs badge_vip_mini gives no prefix command and points to the badge instead. To make a mini differ, give its badge a mini: section; whatever the section leaves out still follows the badge:
badge_admin:
badge:
label: ADMIN
emblem: gear
colors:
plate: '#cf5a3c'
mini:
emblem: terminal # any badge emblem, or icon:<name>
colors:
emblem: '#eee8dd' # this colour only: every other one stays the badge's
corners: square # corners: and bevel: as for a badge
A colors.plate under mini: gives the mini a plate of its own, and then the colours it does not name are worked out from that plate, as a badge's are, rather than taken from the badge (whose highlight and shade belong to its own plate). mini: false gives a badge no mini. A badge without an emblem has none unless its mini: names one (mini: {emblem: star}), and neither does a badge whose name already ends in _mini. A glyph defined in the same file under a mini's name takes the mini's place (the load report says so); a built-in mini goes with its badge when a helper config replaces that badge. A mini too wide for one cell — only possible with an emblem far wider than the EGW ones — is cut like a full badge, and the load report says so.
/pf glyphs lists the minis in the Badges group, each after its badge (mini badge of badge_admin); /pf glyphs badge_admin_mini gives its character, its world form and its placeholders, and /pf glyphs badge_admin points to it. Each mini has its own permanent characters, two (its own and its world form's), handed out at the first load with this release; nothing else moves. Like any new glyph they reach players with /pf zip, and Bedrock players after the restart that follows.
The EGW icons
Sixty-nine full-colour icons from assets/branding/egw-glyphs/ (all original art, every colour from the EGW palette), one Bedrock cell each, sized to sit on an EGW Werks line:
| Row | Icons |
|---|---|
| things | heart half_heart star sparkle crown skull egw trophy medal gift coin money_bag diamond emerald key lock |
| tools | sword pvp shield bow pickaxe axe hammer gear wrench fishing_rod fish wheat apple potion book scroll |
| world | map compass globe home flag chest bell mail chat clock hourglass fire lightning snowflake sun moon |
| marks | music check cross warning info question plus minus arrow_up arrow_down arrow_left arrow_right bullet person eye pin |
| status | online away busy offline — dots for the tab list |
| beta | beta — a lab flask with a bubble, for beta testers and features in testing (the BETA badge's icon) |
| Where | Write |
|---|---|
| chat | :coin: (everyone; LPC's own emoji list replaces :heart: :star: :check: :cross: with ❤ ★ ✔ ✖ first, and those become the EGW icons all the same, below), or Discord's name for the emoji an icon draws — :gem: :crossed_swords: :green_circle: (its aliases, "Emoji" below) |
| chat, typed | ❤️ 🔥 🌙 ⭐ 👑 💎 … — the Unicode emoji an icon draws become it (glyphs.unicode-emoji, "Emoji" below) |
| a sign | :coin:, an alias too (below) |
| a LuckPerms prefix | %img_crown% |
| TAB, scoreboards, nametags | %packforge_white_crown% |
| a badge emblem | emblem: icon:crown |
| find or copy one | /pf glyphs (icons six to a row; the console gets each name with its aliases) or /pf glyphs crown |
They are not written anywhere: the pictures stay in the jar and /pf zip carries them into both packs. A glyph of the same name in a helper config (any namespace — the egw_glyphs content folder from the assets, or a heart of your own) takes the built-in one's place, and the load report says so, so :heart: always names one glyph. glyphs.builtin.icons: false switches all of them off; their characters stay reserved for when they come back.
Emoji
Seventy-nine chat emoji from assets/branding/egw-emoji/ — original pixel art, every colour from the EGW palette (the faces and hands in brass: the palette has no emoji yellow) — built into the jar like the icons, for everyone, on both editions, and named as Discord names its default emoji, so :joy: in Minecraft is :joy: on Discord. Look at assets/branding/egw-emoji/preview.png for all of them at both sizes and in mock Java and Bedrock chat.
| Row | Emoji |
|---|---|
| faces | slight_smile grinning smile joy rofl wink blush heart_eyes star_struck sunglasses nerd thinking person_shrugging neutral_face unamused rolling_eyes |
| faces | slight_frown cry sob angry rage open_mouth scream sleeping nauseated_face sweat_smile upside_down partying_face stuck_out_tongue clown ghost alien |
| hands and marks | wave thumbsup thumbsdown clap ok_hand pray muscle point_up raised_hands punch handshake eyes 100 zzz gg exclamation |
| moon and food | new_moon waxing_crescent_moon first_quarter_moon waxing_gibbous_moon full_moon waning_gibbous_moon last_quarter_moon waning_crescent_moon birthday cookie bread meat_on_bone sushi jack_o_lantern camping torch |
| animals, nature, things | chicken pig cow sheep bee cat wolf evergreen_tree blossom mushroom cloud_rain rainbow ocean video_game rocket |
| Where | Write |
|---|---|
| chat | :joy:, any of its aliases — :laughing: :lol: :tears_of_joy: :haha: — or the emoji itself, 😂, as a phone or PC keyboard types it |
| a sign | the same three ways (written as the emoji's world form, in white, "Signs" below) |
| a LuckPerms prefix, a format | %img_birthday% (names only: an alias is a shortcode, not a placeholder) |
| TAB, scoreboards, nametags | %packforge_white_birthday%, %packforge_world_white_birthday% |
| find or copy one | /pf glyphs (the Emoji group, six to a row; hover one for its aliases and Unicode), /pf glyphs lol |
For players: assets/branding/egw-emoji/EGW-Emoji-Sheet.png shows every emoji and icon with its :name: and aliases, a blurple dot on each one Discord shows too, sized to read on a phone — post it on Discord. EGW-Emoji-Copy-Paste.txt beside it holds every name in six chat messages (Minecraft's 256-character limit) and a short alias list. Both are drawn by Packforge from its own built-ins (EmojiSheet in the test sources, checked by EmojiSheetTest on every build); run it again after the built-in emoji or icons change.
Discord's names, so a relay just works. Each emoji's name is Discord's own default shortcode for the emoji its picture draws — :joy: 😂, :thumbsup: 👍, :100: 💯, :slight_smile: 🙂, :smile: 😄, :birthday: 🎂 — and its aliases start with Discord's other names for it (:waving_hand:, :thumbup:, :+1:). The icons answer to Discord's names for the emoji they draw as aliases (:gem: is diamond, :crossed_swords: is pvp, :x: is cross). A player who types :joy: sees the EGW face; the message itself is never changed — Packforge rewrites only each player's copy of the line (the console's copy, and what every plugin reads from the chat event, keep the text as typed) — so a Discord relay forwards :joy: and Discord shows its own default 😂. Only Discord's default emoji can work this way: a Discord server's custom emoji have no name Minecraft could send. The list is the one Discord's own client ships (stable client, version 1791709437947, 2026-10-11; the source and how to refresh it are in assets/branding/egw-emoji/README.md).
- The earlier EGW names still work, as aliases:
:laughing:isjoy,:thumbs_up:thumbsup,:hundred:
100, :cake: birthday, :happy: smile — all 33 renamed emoji (the README lists them). Two shortcodes moved: :smile: was the slight smile and is now Discord's 😄 (EGW's happy); the slight smile is :slight_smile:. And :smiley: went with it: Discord shows it as 😃, which smile lists, so it is smile here too. Typed in Minecraft an earlier name is still the EGW emoji, but Discord shows it as text unless it is a Discord name too — so :laughing: reaches Discord as 😆, not 😂. %img_<name>% takes the new names only (%img_joy%).
ggandtorchare EGW's own (no Unicode emoji, so no Discord name): Discord shows:gg:as text.- Nine handles mean another emoji on Discord, kept so typing them still works here: the emoji aliases
cool
(Discord 🆒), glasses (👓), frowning (😦), tongue (👅), ok (🆗) and ship (🚢), and the icon names bow (🙇), cross (✝️) and person (🧑) — Discord emoji no EGW glyph draws, so they reach Minecraft as sent. Most icon names (pvp, diamond, online …) are no Discord shortcode at all; their Discord alias (:crossed_swords:, :gem:, :green_circle:) is the one that reads the same on both sides. /pf glyphs <name> lists every alias.
- Not yet seen with a bot. Discord's own message box turns
:joy:into 😂 before it sends; whether Discord does
the same for :joy: posted by a bot or webhook has not been tested. If the relay's messages show :joy: as text, the relay should swap each :name: and alias for the emoji's first Unicode form before posting (unicode[0] in emoji.json, and in icons.json for the icons) — exactly what Discord itself sends.
- Discord to Minecraft. Discord sends emoji as Unicode (😂), which the emoji's
unicode:lists turn into the EGW
emoji — in player chat and on signs, where Packforge converts. A line a relay posts into Minecraft is a plugin's message rather than a player's chat (unless the relay sends it as one), so it keeps the emoji as sent: Bedrock and most phones draw it, vanilla Java's font draws few.
Who may use them. Everyone, exactly as the icons: each is shortcode: true with no permission:, so :name:, its aliases and its Unicode work for every player in chat, and on signs for everyone holding pf.glyphs.signs (everyone by default). An emoji says nothing about who a player is — unlike a badge or a tag, which stay shortcode: false — so there is nothing to guard. To restrict one, define a glyph of the same name in a helper config with a permission: (and its picture, assets/branding/egw-emoji/packforge/egw_emoji/textures/glyph/<name>.png, in a content folder): it takes the built-in one's place, aliases and Unicode included if you copy them, and then needs pf.glyph.<permission>.
Aliases. Each emoji answers to more shortcodes than its name — Discord's other names, then the earlier EGW names and their aliases: :lol: and :laughing: are joy, :+1: and :like: are thumbsup, :hi: :hello: and :bye: are wave, :dog: is wolf, :wave_water: is ocean (the sea; wave is the hand) — 207 for the emoji and 54 for the icons, listed in /pf glyphs <name> and in the assets' READMEs. An alias is used exactly like the name, by the same rule. A name always wins: an alias that is also any glyph's name — a heart of your own called love would take :love: from heart_eyes — is left out of the alias list, and every load warns, naming both; an alias two glyphs declare goes to the first (the server's own helper configs come before the built-in sets) and is warned about the same way. Aliases are shortcodes only: %img_lol% is no placeholder (%img_joy% is).
Typed Unicode emoji (glyphs.unicode-emoji, on by default). Every emoji lists the Unicode it stands for — 😂 😆 😹 for joy, 👋 and its five skin tones for wave, 🤷 🤷♂️ 🤷♀️ and each of those toned for person_shrugging — and a player who types one, from a phone keyboard or the PC's emoji picker, gets the EGW emoji in its place. The first form listed is the one Discord sends for the name. The icons list theirs too, so Unicode whose picture an EGW icon already draws becomes that icon: ❤️ ♥ and every coloured heart are heart, 🔥 fire, 🌙 moon (the crescent; the other moon phases are emoji), ⭐ ★ star, 👑 crown, ✅ ✔ check, ❌ ✖ cross, 🟢 🟡 🔴 the status dots, and so on (/pf glyphs heart shows each icon's list). How it matches:
- Longest first. At each place the longest listed sequence wins, so 🤷🏽♀️ (five code points) is one shrug, not a
shrug, a stray skin tone and a sign. Every skin tone and every U+FE0F form is listed on its own, as the designer's emoji.json lists them; a U+FE0F right after a match is taken with it (it only asks for the emoji style the glyph already is), and one a listed sequence holds may be missing (🤷♂ without it, as some keyboards send it, is still the shrug).
- A joined emoji is one emoji. A zero-width joiner (
U+200D) makes one emoji of several: 👩🌾 is a farmer, not a
woman and a sheaf of wheat. So a listed sequence converts only where it stands whole: a joined emoji a glyph lists in full (🤷♀️) becomes it, and one nobody lists — 👩🌾, 🐈⬛, ❤️🔥, 🏳️🌈, a kiss or a family — is left exactly as typed, every part of it, never an EGW wheat, cat, heart or flag pulled out of the middle.
- Per viewer, like shortcodes. Each player's copy of a chat line is converted for them: a Java player whose pack
draws the glyph and a Bedrock player whose glyph pack does (after the restart) see the EGW emoji, white like every glyph; a Java player without the pack, and a Bedrock player before the restart that brings it, see the emoji as typed, as their own client draws it — never spelled out. The console, logs and Discord bridges always get the message as typed. The sender needs what the glyph's shortcode needs (nothing, for the built-in sets).
- LPC's own emoji turn into the icons. LPC replaces
:heart::star::check::cross:with ❤ ★ ✔ ✖ before
Packforge sees the line; those characters are listed by the icons, so they show as the EGW heart, star, check and cross — LPC's emoji.replacements can stay as they are.
- Untouched otherwise. A character no glyph lists — é, ✓, 🦖,
<3,:)— is never changed; no ASCII text can be
listed at all. Text in other places — private messages and other plugins' messages, books, item names, the tab list — is not chat, and keeps its emoji (as it keeps :name:).
- Signs convert as they are written, into the world form ("Signs", below); a sign keeps what it was written with.
glyphs.unicode-emoji: false and /pf reload leave every typed emoji as typed, from the next message and sign.
Bedrock: the hand-drawn 8×8. Bedrock draws a glyph one pixel per text pixel, so at glyphs.bedrock.icon-pixels-per-unit: 1 (the default) Packforge would halve each 16×16 picture — and a 16-pixel face halved to 8 loses its eyes. So every emoji carries its second drawing, emoji-1x.png's 8×8 art, drawn pixel by pixel for that size (its image-1x, below): at 1 Bedrock shows exactly that, in chat and in the world form (centred, for nametags, signs and the sidebar); at 2 it shows the full 16×16 Java art. An emoji is 8 pixels tall beside Bedrock's 7-pixel capitals, one pixel taller than the letters, like the mini badges, and sits on the line the same way icons do (glyphs.bedrock.lift). On Bedrock each emoji takes its picture's whole 8 pixels and the one-pixel gap, as the icons do, so the narrow ones (exclamation, point_up, blossom, torch) keep a full emoji's room there; Java spaces every glyph to its ink.
The egw_emoji content folder that assets/branding/egw-emoji/tools/egw_emoji.py export writes is not needed: the jar carries the same emoji. Installed, its glyphs take the built-in ones' place by name. A copy exported before the emoji took Discord's names is warned about at every load — it draws them under the earlier names (laughing, thumbs_up), which then take those shortcodes from the built-in emoji, without the hand-drawn Bedrock art, the aliases and the Unicode; a current export is only noted. Unless you keep it on purpose, delete contents/egw_emoji/ (and its egw-emoji.yml from items/ if you copied it there), /pf reload, /pf zip.
Characters. Each emoji is one character, and one more for its world form, handed out the first time its name loads and kept for good like any glyph's; nothing else moves. They reach Java players with the next /pf zip, and Bedrock players after the restart that follows (until then Bedrock players see the emoji as typed). A glyph's characters belong to its name, so the 33 emoji renamed to Discord's names get new ones; on a server that ran the build with the earlier names, those names' characters stay reserved in glyph-assignments.yml and draw nothing — a sign written with one of those emoji under that build shows nothing there until it is written again, and a %img_laughing% in a format must become %img_joy% (:laughing: typed in chat keeps working). glyphs.builtin.emoji: false switches all 79 off; their characters stay reserved for when they come back. A glyph of the same name in a helper config takes a built-in emoji's place, and the load report says so.
LuckPerms + LPC, step by step
Live chat is LPC with LuckPerms. LPC builds each line from a format, replaces {prefix} with the player's LuckPerms prefix, then runs PlaceholderAPI over the whole line, then reads it as MiniMessage. So a prefix may hold a placeholder, and the glyph follows it if it ever changes:
- Give each group its badge. From
/pf glyphs badge_admin(the group comes from the badge'sluckperms.group):
For example: /lp group admin meta setprefix 140 "%packforge_glyph_badge_admin% ". Keep the trailing space between the badge and player name. /pf glyphs supplies commands and characters for the installed badges. Packforge does not create permission groups; assign the badges to your existing groups.
- Let LPC show the prefix. LPC's
chat-formatalready starts with{prefix}, but live LPC'sgroup-formats
override it with hard-coded text (admin: "<dark_purple>[Admin]<light_purple> {name}…"). Swap the hard-coded tag for {prefix} in each group format you give a badge: ``yaml group-formats: admin: "{prefix}<light_purple>{name}<gray> » <light_purple>{message}" ` (or write %img_badge_admin% straight into the format), then /lpc reload`.
- Leave the colour alone — and never
%packforge_white_…%here. Chat shows glyph characters in white for every
player by itself, so the purple around a badge does not tint it. And LPC reads prefixes and formats as MiniMessage (its config says so), which refuses a § code outright ("Legacy formatting codes have been detected"): a prefix holding %packforge_white_badge_admin% would make that group's chat fail to format. If you want white spelled out, write MiniMessage: "<white>%img_badge_admin%</white> ".
- Check it as a Java player, a Bedrock player (after the restart) and in the console. The console shows the
formatter's line untouched — it cannot draw glyphs anyway.
- Group colours on Bedrock come by themselves. Every hex colour in a group's prefix, suffix or colour meta and in
LPC's formats gets an EGW alphabet at the next /pf zip (glyphs.bedrock.colors-from-luckperms), so after the restart a Bedrock player sees <#cf5a3c> as exactly that colour ("EGW font on Bedrock", below).
TAB, scoreboards and other plugins
Any plugin that runs PlaceholderAPI can show a glyph. Outside chat nothing whitens it, and the Java client multiplies a glyph by the colour of the text it is in — a badge after &3 text turns teal, an icon on black turns black — so use %packforge_white_<name>% there: the same characters after §f. TAB reads & and § codes, so it fits every TAB text. Above heads (tagprefix, tagsuffix) and on scoreboards use the world form, %packforge_world_white_<name>%, or above heads for everyone at once %packforge_world_prefix%: Bedrock places glyphs in nametags and on its sidebar differently from the tab list ("Bedrock world text", below), and a badge on the Bedrock sidebar takes its mini badge ("Mini badges", above):
# TAB groups.yml — a badge in front of each group's names in the tab list and above heads
admin:
tabprefix: "%packforge_white_badge_admin% &c"
tagprefix: "%packforge_world_white_badge_admin% &c"
# TAB config.yml — header/footer lines and scoreboard lines
header-footer:
designs:
default:
header:
- "%packforge_white_egw% &3&lEvergrims Werks %packforge_white_egw%"
scoreboard:
scoreboards:
scoreboard:
lines:
- "%packforge_world_white_badge_admin_mini% &f%player%"
- "%packforge_world_white_coin% &eBalance: &f%vault_eco_balance%"
- "%packforge_world_white_online% &aOnline: &f%online%"
Live TAB's _DEFAULT_ group uses tabprefix: "%luckperms-prefix%". That works with a badge too: TAB 6.1.0 parses placeholders inside a placeholder's output (checked in its TabPlaceholder), so the LuckPerms prefix's %img_badge_admin% is filled in, and at the start of a name it is already white. Keep it for the tab list, and give the nametag the same prefix in world forms — once, in _DEFAULT_, for every group:
_DEFAULT_:
tabprefix: "%luckperms-prefix%" # the player list: the glyphs' own characters, as chat
tagprefix: "%packforge_world_prefix%" # above the head: the same prefix, its glyphs in their world form
tagsuffix: "%packforge_world_suffix%"
A group that sets its own tagprefix overrides _DEFAULT_ — the EGW TAB kit (assets/server-configs/TAB) sets one for every rank, already the world form from EGW-TAB 1.2.0 on (1.1.0 had %packforge_white_badge_<rank>%) — so give each of yours the world form too: tagprefix: "%packforge_world_white_badge_admin% &c". Put a badge after coloured text only through %packforge_white_…%, and keep that out of the LuckPerms prefix itself while LPC reads it (step 3 above): give TAB its own per-group tabprefix instead. Glyphs in the tab list, nametags and scoreboards reach Bedrock players too (Geyser sends the characters; Packforge-Glyphs.mcpack draws them) — but no page of the EGW Sidebar board's: E8 is theirs (glyphs.bedrock.reserved-pages). Bedrock's sidebar — every scoreboard, the EGW Sidebar board included — places glyphs as a nametag does (seen on the owner's board: chat-form icons sat low) and leaves a gap between glyph characters, so its lines take the world form, %packforge_world_white_<name>%, and its badges their minis. A Java player without the pack sees boxes there: outside chat there is no per-player spelling out.
Signs
:name: on a sign becomes the glyph when a player writes it — front or back, standing, wall or hanging — and so does an alias (:lol:) and, with glyphs.unicode-emoji, a typed emoji the glyph lists (😂, ❤️), for the player chat would allow it (a glyph's permission: as pf.glyph.<p>) who also holds pf.glyphs.signs (everyone by default). Packforge puts white in front of each glyph on the sign: sign text is black by default, and black times any colour is black, so without it every icon would be a black silhouette; the words around it keep the sign's dye colour. Bedrock players get the same characters (Geyser sends them with §f in front) and see the picture from the glyph pack. A sign is text in the world, which Bedrock places like a nametag, so Packforge writes each glyph's world form on it — a shortcode and pasted characters alike — drawn level with the sign's words ("Bedrock world text", below); Java draws it exactly as the glyph.
line 1: :coin: SHOP
line 2: Diamonds
line 3: :diamond: 64 = 5 :coin:
- Each side is written on its own (the client edits one side at a time), so the back works exactly like the front.
Hanging signs are signs: the same event, the same rule. Glowing ink changes how sign text is drawn: check it in game.
- Badges and tags are
shortcode: false, so no player writes a rank onto a sign; glyph characters pasted onto a sign
are held to the same rule as in chat — those the player could not write as a shortcode are taken out (before other plugins read the lines), and those they could are set in white too.
- Other plugins see the lines as typed: a shop plugin reading
[Buy]reads it before Packforge turns shortcodes into
glyphs (at the last priority), and its own styling stays.
glyphs.signs.enabled: falsestops shortcodes on signs at once; pasted characters are still taken out.- Signs already written keep what they hold — a sign written before world forms holds the chat characters, which
Bedrock draws about half a line low: edit the line (write it again) to rewrite it in the world form. A Java player whose client lacks the pack sees boxes on a sign — chat spells glyphs out per player, a sign cannot. Another plugin writing signs itself (a shop, a hologram) takes %packforge_world_white_<name>%.
Defining glyphs
In any helper config under items/ (or a pack's configs/ folder, read in like the others):
namespace: egw
glyphs:
admin:
tag:
text: ADMIN
color: '#cf5a3c' # quote it: an unquoted # starts a YAML comment
style: background # plain | brackets (default) | background | outline
text-color: '#eee8dd' # background only; default white or near-black by contrast
# background: '#…' # background only; default: color
# outline: '#…' # outline only; default: color at a quarter brightness
# font-style: egw-werks # egw-werks | egw-hearth | egw-grimoire; default glyphs.font.style
shortcode: false # :admin: in chat and on signs; default false for tags and badges, true for images
# permission: staff # who may type the shortcode: pf.glyph.staff
luckperms: {group: admin, priority: 100}
badge_builder:
badge: # a rank badge: the reference table above
label: BUILDER
emblem: hammer
colors: {plate: '#e6aa55'}
luckperms: {group: builder, priority: 125}
heart:
image: egw:glyph/heart # assets/egw/textures/glyph/heart.png, shipped by a pack
height: 9 # Java units (default: the image's own height in pixels)
ascent: 8 # Java units (default: 8, or the height when it is shorter)
doge:
image: egw:glyph/doge # 16x16, height 8: two pixels a unit, like the EGW icons
image-1x: egw:glyph_1x/doge # the same picture drawn by hand at 8x8, for Bedrock at one pixel a unit
height: 8
ascent: 7
aliases: [shibe, much_wow] # more shortcodes: :shibe: :much_wow: (quote one YAML reads as a number: '+1')
unicode: ['🐕🦺', 'U+1F9AE'] # typed emoji that become it: the characters, or their code points
Each glyph is exactly one of tag:, badge: or image:. glyphs.example-config: true writes items/packforge/group-tags.yml once — ADMIN, MOD, VIP and MEMBER as text tags, an example of tag: — which every install got before the badges became the shipped rank set; an install that has it keeps it.
Aliases, Unicode and a hand-drawn Bedrock picture — the three keys the built-in emoji use, open to any glyph:
| Key | For | Notes |
|---|---|---|
aliases | any glyph | More shortcodes, a list or one name: lower-case letters, digits, _ - . +, colons optional (':lol:' is lol). Used exactly as the name, by the same rule (shortcode:, permission:), in chat and on signs; /pf glyphs <alias> shows the glyph. A name always wins: an alias that is any glyph's name is left out, and one two glyphs declare goes to the first — each warned at every load. Not a placeholder: %img_<name>% takes the name only |
unicode | any glyph | The Unicode emoji typed in chat and on signs that become the glyph (glyphs.unicode-emoji): a list or one entry, each the characters themselves ('😂', '❤️') or their code points ('U+1F44B U+1F3FD', as emoji.json writes them). List every form you want caught — each skin tone, with and without U+FE0F — the longest listed sequence wins. An entry of plain ASCII (':)'), with a space, a private-use character or §, or starting with a skin tone, U+FE0F or the joiner is refused and named; a sequence two glyphs list goes to the first, warned. Same sender rule as the shortcode |
image-1x | an image: glyph | The picture drawn by hand at one pixel a Java unit — as tall as height and as wide as the picture at that size (16×16 at height: 8 takes 8×8) — which Bedrock shows at glyphs.bedrock.icon-pixels-per-unit: 1 (chat and world form) instead of the picture halved; at 2 the picture itself. One missing from the pack, or of another size, is named in the load report and the picture is halved as before. Ignored, with a warning, on a tag or badge (Packforge draws those for Bedrock itself) |
The helper-config font_images: format supports path, scale_ratio (height), y_position (ascent), permission and command suggestions. Packforge allocates glyph characters itself; it reports ignored fixed-symbol assignments.
Characters are permanent. Each glyph gets characters from U+E200 up, recorded in glyph-assignments.yml, and keeps them for good; characters once given out are never given to another glyph, even after the glyph is deleted — a LuckPerms prefix holding them must never start drawing something else. Built-in badges and icons are given theirs by the same rule, the first time they load. A glyph wider than one Bedrock cell (8 units) is several consecutive characters, and gets a little room to grow. If a tag's words get much longer it moves to new characters; the zip report says so, and a prefix written with %img_<name>% follows on its own while one written with the characters must be copied again from /pf glyphs.
Why those characters: Bedrock draws U+E000 to U+E1FF itself (controller buttons, the Minecoin), so a pack that defines them repaints those for everyone; U+E200 to U+F8FF is free on Bedrock (wiki.bedrock.dev, custom emojis) and Java (vanilla maps no private-use character). Packforge uses E2 to E9 for glyphs, EA to F7 for the Bedrock EGW alphabets (below), and leaves F8 alone (reserved for compatibility).
Pages other packs own. Bedrock reads glyphs in pages of 256 (font/glyph_E8.png is U+E800 to U+E8FF), and two packs that ship the same page do not merge — the higher one replaces the whole page. So a page another pack uses is never given to a Packforge glyph or alphabet: glyphs.bedrock.reserved-pages lists them, ['E8'] by default — the EGW Sidebar pack's, which TAB's egw-bedrock board draws from. Java keeps the same rule (a glyph is one string sent to both editions, so a character must mean one thing on both), which leaves glyphs seven pages (E2–E7, E9: 1,792 characters). Claim a page in the WerkShop's docs/BEDROCK-GLYPH-REGISTRY.md before a pack uses it, and add it here. A glyph already on a page you add moves off it at the next /pf reload — the report names its new characters, its old ones stay retired — and an alphabet there is simply redrawn elsewhere.
Chat in the EGW font, per player
Each chat line is finished separately for each player who sees it, after the chat formatter (LPC) has done its part:
| Viewer | Gets |
|---|---|
| Java player whose client loaded the pack offered now | :name: shortcodes (aliases too) and typed Unicode emoji a glyph lists, glyphs in white; the line in the EGW font already when that pack draws its default font in EGW (glyphs.font.replace-default), else the EGW font on the parts glyphs.chat-font.java.parts names (when that pack carries it) |
| Bedrock player | shortcodes, typed Unicode emoji and glyphs (drawn by the Packforge-Glyphs.mcpack Geyser loaded at startup), and every letter the EGW alphabets draw written in them, in the alphabet of its colour, on the parts glyphs.bedrock.chat-font.parts names (next section) — only text in a hex colour Bedrock lacks, when that pack draws Bedrock's own font in EGW |
Java player without the pack (declined, failed, still loading, or still holding a pack a /pf zip just replaced) | each glyph spelled out, [ADMIN] in the tag's colour; shortcodes and typed emoji as typed; no font — a client asked for a font it lacks draws every letter as a box |
| the console, logs, Discord bridges | the formatter's line, untouched. The event's message is changed only to take out glyph characters the sender pasted and may not use (below): plugins reading it see what was typed, less those |
The chat-line marker: the one thing egw:chat costs on Java — and why the default font is the better way. With glyphs.font.replace-default on (the default), a player whose pack draws the default font in EGW gets their lines without any font of their own, so they carry no marker at all; what follows is about the option off. The 26.2 client marks every chat line it cannot call secure with a bar at its left edge, and a hover text. Signed chat counts as modified as soon as the line the server sends with it uses any font but the default — the client checks exactly that (ChatTrustLevel.isModifiedStyle) — so with the EGW font on, every Java chat line from every player gets a bar for players whose pack carries the font. Under LPC without the font, lines are secure and carry no bar. There is no way to have both the font and no bar, so glyphs.chat-font.java.delivery lets you choose the bar:
delivery | The line is | The bar, and its hover text | Also |
|---|---|---|---|
signed (default) | Paper's own signed chat, in the EGW font | dark grey; "Message modified by the server. Original:" and the typed text | reportable; the client's own Hide of a player works; a player with Only Show Secure Chat on sees just the typed text, no name (as under LPC today) |
unsigned | sent by Packforge as an unsigned chat message (Paper's paper:raw type), and Paper's signed copy is not sent to that player | light grey, like join messages; "Server message. Cannot be reported." | cannot be reported; the client's Hide does not apply; Only Show Secure Chat players see the whole line. Chat visibility and account chat restrictions still apply |
Only Java players whose pack carries the font are affected; Bedrock players, Java players without the pack and the console are unchanged either way. To have no bar at all, turn glyphs.chat-font.java.enabled off: glyphs still work, in the default font. On a server with no chat formatter (Paper's own <Name> message), Packforge leaves Paper's chat alone for a message unless some online player's line changes — the font for a Java player, the EGW alphabets for a Bedrock player, or a glyph in the line — because Paper then sends the formatted line instead of plain vanilla chat, and a player with Only Show Secure Chat on sees the message without the name.
Glyph by glyph, against the pack each player holds. A glyph added (or moved) by /pf reload is in /pf glyphs and %img_<name>% at once, but in a Java player's pack only after the next /pf zip, and in a Bedrock player's only after the restart that follows it. Until then that glyph — and only that one — is spelled out for them and its shortcode left as typed. Java players are checked against the very pack their client loaded (by its hash), so a player still holding the previous pack while a zip's re-push reaches them is treated as having none for that moment; one whose re-push timed out stays so until they rejoin. Bedrock players are checked against the glyph pack Geyser loaded at startup: with glyphs.bedrock-pack.enabled off (after the zip that removes the pack, and a restart), they see every glyph spelled out.
Parts. All on (the default), the whole line is in the EGW font under any formatter. Mixed, Paper's own chat format keeps them apart; LPC does not — it builds the whole line itself from its own copies of the name and message — so under LPC the whole line follows parts.prefix, and the log says so once. To choose by hand, use LPC's MiniMessage: <font:egw:chat>{message}</font> in a format. That goes to every viewer, though, Java players without the pack included.
Shortcodes. :heart: becomes the glyph for a sender the glyph allows (shortcode: true, and pf.glyph.<p> when it names a permission), and so does each of its aliases (:lol: for joy). LPC replaces its own emoji (:heart: → ❤ in LPC's config) first, so on a name both use, LPC's wins — but its ❤ ★ ✔ ✖ are characters the EGW icons list, so they become the icons anyway (next paragraph).
Typed emoji. With glyphs.unicode-emoji on, a Unicode emoji a glyph lists (unicode:) becomes that glyph by the same rule — 😂 the joy emoji, ❤️ the heart icon — for each viewer whose pack draws it; others see it as typed ("Emoji", above).
Pasted glyph characters. A glyph is only characters, and every glyph's characters are listed in the public pack, so a player could paste the admin tag's characters into a message. Before anything else reads the message (the earliest chat priority, before LPC), Packforge takes out every glyph character the sender may not use as a shortcode — so a tag (shortcode: false) can never be typed, only given through a prefix — along with characters no glyph holds and the Bedrock alphabets' characters. A message without any is left exactly as it was. Logs and Discord bridges see the message without them too; only a plugin reading Paper's old AsyncPlayerChatEvent sees them as pasted.
EGW font on Bedrock
With the default font replaced (glyphs.font.replace-default, the default), Bedrock's own font is already EGW ("The EGW font as the default font", above), and everything below matters only for text in a hex colour Bedrock lacks: that text is still written in the alphabet drawn in its colour, so the colour shows exactly. White text and Bedrock's own 28 colours stay as text. With the option off, all of it applies to every chat line.
Why it takes a pack of letters. A Java client can be sent a font; a Bedrock client cannot. Geyser hands Bedrock plain text with section colours, and the only pictures a Bedrock pack can add to text are its private-use glyph sheets. So /pf zip draws every letter of the EGW font into Packforge-Glyphs.mcpack as a private-use character of its own — an alphabet — and each Bedrock player's chat is rewritten into those characters as it is sent to them.
Why one alphabet per colour. Bedrock has no hex colours. Geyser rounds every colour to the nearest of the 28 Bedrock knows (Java's sixteen plus twelve Bedrock-only ones, §g to §v; checked in the live Geyser-Spigot.jar), so #cf5a3c never reaches a Bedrock player as itself. The one way to show it exactly is a copy of the alphabet already drawn in #cf5a3c, sent in white so nothing tints it again. One white alphabet covers every other colour, as long as Bedrock tints glyphs by the section colour the way Java does (glyphs.bedrock.tinted, below).
Which alphabet a run of text gets, by its colour:
| The text's colour | Written in | Sent in |
|---|---|---|
| exactly a colour that has an alphabet | that alphabet | white, so Bedrock does not tint it twice |
within nearest-distance (default 48) of one | the nearest alphabet — a gradient's in-between shades land on the colours they run between | white |
further from every alphabet, tinted: true | the white alphabet | the text's own colour, which Geyser rounds to the nearest of Bedrock's 28 |
further from every alphabet, tinted: false | the nearest alphabet, however far | white |
Uncoloured chat is white, so it uses the white alphabet. The distance is the straight line between two colours in red, green and blue: 0 is exact only, 441 is anything.
Which colours get an alphabet. White always, then glyphs.bedrock.colors (the EGW palette's ember, coolant, brass and bone by default — the example tags' colours), then, with colors-from-luckperms: true and LuckPerms installed, every hex colour written in a LuckPerms group's prefixes, suffixes and colour meta (LPC's username-color, message-color, username-gradient; heaviest group first, every context) and in LPC's config.yml formats (chat-format, group and track formats, message styles, name gradients). They are read again at every /pf reload and /pf zip, so a new group prefix gets its alphabet at the next zip. While tinted is true a hex colour Bedrock already has (one of its 28, such as #ff5555, which is <red>) is skipped: the white alphabet shows it.
How many fit. The alphabets share U+EA00 to U+F7FF: 3,584 characters. Each alphabet takes 160 (the 148 or 149 characters a style draws, rounded up to a whole sheet row), so 22 alphabets fit: white and 21 colours. Past that, the last colours asked for — LPC's format colours, then LuckPerms' lightest groups — get none: every load and zip warns, naming them, and on Bedrock they show in the nearest alphabet or the nearest § colour. Trim glyphs.bedrock.colors, or list the important ones there (yours come before LuckPerms'), to choose.
What is drawn. A Bedrock letter is drawn in a glyph's 8-unit box (baseline 7 down, as Java's ASCII font), at glyphs.bedrock.pixels-per-unit — at 1 its capitals are 7 pixels, Bedrock's own — with its baseline glyphs.bedrock.lift pixels above Bedrock's ("Bedrock glyph size", below), and its one-unit gap to the next letter kept; the EGW font is drawn for Java's 9-unit line. Of each style's 175 characters, 148 fit (149 in egw-grimoire): every letter, digit and ASCII symbol, and most accented lower-case letters. g j p q y _ ç ý ÿ ¡ ¿ Ç lose the lowest half-unit of their tail, and show of the unit left only what the lift has room for. The accented capitals (À to Ý) and, except in egw-grimoire, å reach above the box and are left out rather than drawn without their accents — É drawn as E would change the word — so they stay Bedrock's own letters, in the right colour. The zip report lists both.
Characters stay put. Each letter's place in an alphabet and each colour's block of characters are recorded in glyph-assignments.yml (alphabets:), only ever added to. A colour you remove keeps its block — add it back and it gets the same characters — until the range runs out of room, and only then is its block given to another colour (the report says so). So adding or removing one colour changes one block of the Bedrock sheets, and the pack's version (and the restart it asks for) changes only when an alphabet really did. Never delete or regenerate this file, or bedrock-glyph-pack.yml beside it.
When it starts. Chat writes only the alphabets the pack Geyser loaded at startup holds (recorded in bedrock-glyph-pack.yml under that pack's version), never the next zip's — a Bedrock client asked for a character its pack lacks would draw nothing. And Packforge never changes the pack Geyser serves while the server runs: Geyser reads a pack's name and version once, at its start, but its bytes at every download, so a pack replaced mid-session would hand a Bedrock player who joins before the restart — a first join, a cleared cache, a new device — sheets whose alphabets have moved or gone (a dropped colour, a font that grew), and every letter of that colour would be blank or wrong. So a zip that changed the pack leaves it as Packforge-Glyphs.mcpack.next beside the served one (Geyser loads only .zip and .mcpack files, so it never reads that), and it takes the served one's place as the server stops — or, after a crash, as it starts, before Geyser loads its packs. Until that restart every Bedrock player, cached or downloading fresh, holds the old pack, and chat keeps the old set; from it, everyone holds the new one and chat writes it. A zip that switches the pack off waits the same way: the pack goes when the server stops. Undo a change and zip again before the restart, and the waiting pack is withdrawn (no restart needed). /geyser reload does not bring a waiting pack — it reloads the one in place. The zip report says which set chat writes; /pf status says it on its Items line (Bedrock chat font: white + 4 colours live, or what it waits for: a restart, or /pf zip and a restart).
Parts. glyphs.bedrock.chat-font.parts picks the format's own text (prefix), the name and the message. All on, the whole line is converted under any formatter. Mixed, under LPC the parts are found by their text (the message is the last place the typed text appears, the name the last place before it); when a filter, a mention or an emoji replacement changed the message so it cannot be found, that line follows prefix as a whole, and the log says so once.
Off. glyphs.bedrock.chat-font.enabled: false and /pf reload stop the rewriting at once; the next zip drops the alphabets from the pack (one more restart for Bedrock players to lose them). Java players, the console and the event's message are never touched either way.
What it cannot do — honest limits.
- Chat only. Player chat lines that go through Paper's chat event (what LPC formats) are rewritten. Nothing else
is: other plugins' Bedrock forms and menus, titles, the action bar, scoreboards, the tab list, signs, books, item names, private messages and Geyser's own messages stay in Bedrock's colours — and, with the default font left alone, in Bedrock's font (with glyphs.font.replace-default on they are in the EGW font, through Bedrock's own sheets).
- It needs the Bedrock pack. Geyser must be installed, and the server restarted after a
/pf zipthat changed the
pack (a /geyser reload is not enough). Keep Geyser's force-resource-packs: true (its default, and live's): a Bedrock player who could refuse the pack would see the converted letters as nothing. Geyser running on this server is assumed: with geyser.data-folder pointing at a Geyser elsewhere (a proxy), restart that Geyser together with the server, since the waiting pack is put in place by this server's stop and start.
- Copying chat gives private-use characters. A Bedrock player who copies a line gets characters that are not
letters; pasted elsewhere they show as boxes or nothing. Bedrock's narrator (text to speech) cannot read them either. The console, the server log and Discord bridges get the plain text.
- Some characters fall back. Anything the alphabets do not draw — accented capitals, emoji, other scripts — stays
in Bedrock's own font, in the right colour, so a word mixing them looks mixed. Obfuscated (<obf>, §k) text stays Bedrock's too, since Bedrock would scramble a converted letter into its own font's letters.
- Colours outside the alphabets are rounded. With 22 alphabets at most, a colour without one shows in the nearest
alphabet (within nearest-distance) or the nearest of Bedrock's 28. A gradient across many colours shows in steps.
- Click and hover are kept on the line, but Bedrock shows neither — as without Packforge.
Bedrock glyph size
What was seen, and what it means. Bedrock draws a custom glyph at one glyph pixel per text pixel: its size on screen is the number of pixels drawn, whatever the sheet's resolution or the cell's size, and Bedrock's own capitals are 7 pixels tall in an 8-pixel line. The EGW art is drawn at two pixels per Java unit, so the first pack showed chat exactly twice the normal size, and so did the next, which kept two pixels a unit in bigger cells. That release's calibration glyphs, on a 1024-pixel sheet, came out at the size their pixel count gave — and overlapped each other, as if Bedrock measured their width against a 16-pixel cell while drawing them at their own. So every Bedrock sheet is now the standard 256 pixels with 16-pixel cells, where Bedrock's measuring and drawing agree, and the size is a density you choose. (From tests on the live server, nogard.dev's Minecraft glyph generator and wiki.bedrock.dev's custom emojis.)
Density. glyphs.bedrock.pixels-per-unit: 1 (the default) draws every glyph at one pixel per Java unit — exactly the size of Bedrock's own text: an EGW capital is 7 pixels, like Bedrock's. Packforge halves the art to get there, pixel-art safe: every 2 by 2 block becomes one pixel, a stroke one unit thin or thinner never vanishes, a stroke that straddles two blocks is drawn once (not doubled into a blob), a dot keeps its pixel, the hook of a g or y gets the last row to itself (the bowl gives up a straight row, as in Bedrock's own font, so g is not q), and a badge label or an icon's detail is halved colour by colour, so a dark label on a light plate survives — a colour the label shares with the emblem is halved part by part, so the owner badge's crown keeps its jewels on its points. At this size the EGW letters are three to five pixels wide and read like Bedrock's own small text. 2 draws every pixel of the art, twice the text's size, as before. icon-pixels-per-unit (the built-in icons and every picture glyph) and badge-pixels-per-unit (the rank badges) follow pixels-per-unit unless set: 2 keeps icons or badges big and detailed beside text-sized letters. See it before deploying: assets/branding/egw-font/preview-bedrock-1x.png shows a mock chat line beside a 7-pixel bar (Bedrock's capital height), the whole alphabet, the 11 badges and their 11 minis, a mock Bedrock sidebar, the 69 icons and the example tags at 1, the same at 2, and the calibration lines — enlarged four times, no smoothing.
Lift. Bedrock puts a glyph cell's bottom on the text's baseline and draws nothing below a cell, so a letter's tail (g y p q j _) can only show in transparent rows under the letter: glyphs.bedrock.lift, in pixels. 1 (the default) keeps one pixel of tail — as deep as Bedrock's own descenders — and stands EGW letters one pixel above Bedrock's; 0 stands them exactly on the baseline with the tails' last row cut off (q reads as o, g loses its hook); 2 leaves a second row under the one-pixel tail, two pixels up. At pixels-per-unit: 2 a tail is two pixels: use 2 (glyphs drawn at 2 rise at most 2 — the capital fills the rest of the cell; the zip report says when a higher lift is held). A picture — an icon, a badge plate — is never cut: one that would reach below its cell is raised to fit. One lift serves every glyph in chat; nametags and signs place glyphs another way, set by world-offset ("Bedrock world text", below).
Widths and joins. Bedrock ends a glyph at its last visible pixel and puts the next character straight after it, with no gap of its own. So each EGW letter carries its own one-unit gap (a pixel at density 1), every glyph ends with the one-unit gap Java leaves after it (all but a glyph whose last piece fills its whole cell — the MOD and DEV badges at badge-pixels-per-unit: 2, which end at their edge, so put a space after them in a prefix), and a first or last column that must count but is empty — the gap at the cut of a tag, a letter's spacing — holds one black pixel at 8% opacity: counted by Bedrock (5 to 10% counts), unseen on chat. A wide glyph is still cut into 8-unit pieces, one character each (16 pixels at density 2, 8 at density 1, each in a 16-pixel cell); the whole glyph is drawn first and then cut, so the pieces join exactly and none is wider than the space it takes. The pieces never depend on the density, so a glyph's characters — in LuckPerms prefixes, on signs, behind %img_<name>% — never change when you switch it.
Every zip report has a Bedrock glyph density: line — the settings in use and the sheets — and names any glyph whose picture does not halve or double cleanly (an odd size, or taller than a cell: drawn pixel for pixel to the nearest size). A changed density or lift redraws every sheet, so like any change to the pack it waits for the restart.
Calibrate — once, on a Bedrock device:
1. /pf zip builds Packforge-Glyphs.mcpack, calibration glyphs included
2. restart the server Geyser reads the pack only when it starts (the zip report asks for this)
3. join on Bedrock
4. /pf glyphs calibrate "Ag Tricks" in Bedrock's own font on a line of its own, then one line per variant:
plain "Ag", the EGW "Ag", the owner badge and a coin, each after a plain space,
then the exact settings the line is drawn with — pixels-per-unit: 1 with lift: 0,
1 and 2, then pixels-per-unit: 2. The one in use says "(in use)"
5. pick the lift whose EGW "Ag" stands on the same line as the plain "Ag" before it (the g
tails level), and the size you like (1 is the text's own)
6. set them in config.yml glyphs.bedrock.pixels-per-unit and glyphs.bedrock.lift — under glyphs: → bedrock:,
beside reserved-pages:. A config.yml from an earlier release lacks them and uses the
defaults until they are added (the jar's own config.yml shows them, with comments).
One that still has display-scale, anchor or baseline-offset is told at every load
that those do nothing now: delete them
7. /pf reload names the keys that changed (a typo is named as a warning instead)
8. /pf zip, then restart the zip report's "Bedrock glyph density:" line shows the new settings
9. /pf glyphs calibrate again, to check: the line that matched now says "(in use)"
The calibration glyphs are 36 characters on one of Packforge's glyph pages (4 variants of the letters A and g, room for the owner badge's cells, and the coin), handed out once like any glyph's and recorded in glyph-assignments.yml as (bedrock-calibration); never on a reserved page. An install that ran the release before keeps the 64 characters that release gave the set — the new set fits inside them, and nothing moves. Only /pf glyphs calibrate sends them: they are not glyphs, so no shortcode, placeholder, listing or sign produces them, and chat takes them out of a message like any Packforge character a player may not use. The command always shows the set of the pack Bedrock players hold — the one Geyser loaded at startup — so after a zip it shows the old set until the restart (it says when a newer pack waits). Before the first zip and restart with this release, the pack in use has no set this release can show: the command says to zip and restart.
Other devices. Bedrock draws chat text and glyphs with the same text renderer, one glyph pixel per text pixel, so density and lift are expected to look the same on PC and consoles; that is unseen — run the command once on a second kind of device. If two devices disagree, pick the lift for the device most of your players use.
Bedrock world text (nametags, signs)
What was seen. With chat lined up (a cell's bottom on the baseline, "Bedrock glyph size" above), the owner's Bedrock client showed the ADMIN badge in a player's nametag — TAB's tagprefix from the LuckPerms prefix, at badge-pixels-per-unit: 1 — with its top at about the middle of the name: some 4 pixels of an 8-pixel line too low.
Two rules. Bedrock places a glyph's 16-pixel cell according to where the text is drawn:
- Chat, and UI text like it — the chat window and, as far as anything shows, the player list, TAB's header and
footer, titles and the action bar: the cell's bottom sits on the text's baseline. pixels-per-unit, lift and /pf glyphs calibrate set this.
- Text in the world — nametags above players, mobs and armor stands, very likely signs, and (seen on the owner's
board) the sidebar, TAB's scoreboard: Bedrock centres the whole cell on the text line. A text-sized glyph drawn at the bottom of its cell for chat drops about half a text height. (A glyph that filled all 16 rows would look the same under both rules; at the text's size none can.) The sidebar also leaves a gap between glyph characters, so a badge there is its one-character mini ("Mini badges (the Bedrock sidebar)", above).
World forms. So every glyph — badges, icons, tags and pictures; not the EGW chat alphabets, which only chat writes — has a second set of characters, its world form: the same picture, cut into the same pieces with the same widths (so it joins and spaces exactly as the chat form), drawn in the middle of each 16-pixel cell and moved by glyphs.bedrock.world-offset pixels (positive lower, negative higher, -8 to 8, default 0). Java maps the world characters to the very same bitmap, so a Java player sees no difference anywhere. A world form's characters are handed out once and kept for good like any glyph's, recorded in glyph-assignments.yml as (world) egw:badge_admin, never on a reserved page; no existing character moves. /pf glyphs <name> shows a glyph's world form beside its own, and the zip report has a Bedrock world text: line.
| Where the text is drawn | Rule | Write |
|---|---|---|
| Chat (LPC formats, LuckPerms prefixes LPC reads) | chat | %img_<name>%, :name: — the glyph's own characters, as before |
TAB's tab list (tabprefix, tabsuffix), header and footer | chat (UI text) | %luckperms-prefix%, %packforge_white_<name>% |
| TAB's scoreboard (the sidebar), the EGW Sidebar board | world | %packforge_world_white_<name>%; a badge: %packforge_world_white_<name>_mini% |
Nametags: TAB's tagprefix, tagsuffix | world | %packforge_world_prefix%, %packforge_world_suffix%, or %packforge_world_white_<name>% |
| Armor stand and hologram names another plugin sets | world | %packforge_world_white_<name>% (%packforge_world_<name>% without the white) |
| Signs | world | :name: — Packforge writes the world form by itself |
TAB, once for every group. In TAB's groups.yml:
_DEFAULT_:
tabprefix: "%luckperms-prefix%" # the player list: chat rule, the glyphs' own characters
tagprefix: "%packforge_world_prefix%" # above the head: the same prefix, its glyphs in their world form
tabsuffix: "%luckperms-suffix%"
tagsuffix: "%packforge_world_suffix%"
Live TAB's _DEFAULT_ has tagprefix: "%luckperms-prefix%" and tagsuffix: "%luckperms-suffix%": those two lines are the ones to change. Its Player group and the per-world section set their own tagprefix (plain text, no glyph), which still wins for those players. The EGW TAB kit (assets/server-configs/TAB) replaces that groups.yml with one tagprefix per rank and in _DEFAULT_: from EGW-TAB-1.2.0.zip on they are already world forms, tagprefix: "%packforge_world_white_badge_<rank>% &<colour>" (the tabprefix lines keep %packforge_white_…%). Where the older EGW-TAB-1.1.0 is installed, its tagprefix lines still use %packforge_white_badge_<rank>%: install 1.2.0, or change each to %packforge_world_white_badge_<rank>%, then /tab reload.
%packforge_world_prefix% is the player's own LuckPerms prefix — the text %luckperms_prefix% gives, the same for everyone looking — with every Packforge glyph in it in its world form: raw glyph characters swapped for their world twins, and Packforge's own glyph placeholders in it (%img_badge_admin%, %packforge_glyph_…%, %packforge_white_…%) filled in with the world form, because TAB would otherwise fill them in with the chat characters. Everything else — & and § codes, MiniMessage tags, other plugins' placeholders — is left exactly as it is, for TAB. %packforge_world_suffix% does the same for the suffix (tagsuffix). Both are empty without LuckPerms (Vault is not read) and for a player LuckPerms has not loaded. A group that sets its own tagprefix in TAB overrides _DEFAULT_: change it too (tagprefix: "%packforge_world_white_badge_admin% &c"), then /tab reload. Nobody sees their own nametag: look at another player's, or have someone look at yours.
Before the zip and the restart, the world placeholders give each glyph's own characters — low on a Bedrock nametag, as before, rather than nothing — and switch to the world form by themselves once the served Java pack and the Bedrock pack Geyser loaded both draw it. So the order is: update, /pf zip, restart, then TAB's tagprefix.
Signs write the world form by themselves, for shortcodes and pasted characters alike ("Signs", above). A sign written before this release keeps the chat characters until its line is written again; one written between the update and the zip shows boxes on Java (and nothing on Bedrock until the restart) until then.
Calibrate — once, on a Bedrock device:
1. /pf zip builds Packforge-Glyphs.mcpack with the world forms and the world calibration set
2. restart the server Geyser reads the pack only when it starts (the zip report asks for this)
3. join on Bedrock, stand in the open, look straight ahead
4. /pf glyphs calibrate world five armor stands 3 blocks in front of you, for 60 seconds, each nametag reading
Ag <owner badge> <coin> Tricks world-offset: N
the badge and coin drawn at -2 at the top (2 pixels higher) down to 2 at the
bottom; the one in use says "(in use)"
5. pick the nametag whose badge and coin sit level with the plain Ag and Tricks: their
tops with the capitals' tops, nothing hanging below the line
6. set it in config.yml glyphs: → bedrock: → world-offset: <that number>, beside lift:. A config.yml
from an earlier release lacks the key and uses 0 until it is added
7. /pf reload names the change (glyphs.bedrock.world-offset: 0 -> N)
8. /pf zip, then restart the zip report's "Bedrock world text:" line shows the offset
9. /pf glyphs calibrate world again: the one that matched says "(in use)" — and a player's nametag shows the
badge level with the name
The stands are invisible markers with nothing but a nametag (Geyser shows an armor stand's name as a Bedrock nametag, the same renderer TAB's tagprefix reaches). Running the command again moves them; they go by themselves after 60 seconds and with the plugin, and are never saved with the world. Everyone nearby sees them; a Java player sees boxes where the badge and coin are (the Java pack has no calibration characters: Java draws every world form exactly as the glyph, so there is nothing to measure on Java). From the console the command says it needs a player. Its 35 characters — five offsets of the owner badge's cells and the coin, drawn at the pack's own badge and icon density — are handed out once like the chat set's, recorded as (bedrock-world-calibration) (a 36-character span), never on a reserved page, and only this command shows them; before the first zip and restart with this release it says to do those first.
Limits. World text was seen on nametags; signs are expected to follow the same rule (Bedrock lays sign text out in the world) — check one ("Checks", below). A picture as tall as the whole cell (a badge or an icon at 2 pixels a unit) is centred by itself, and the world offset cannot move it (the zip report says so when the offset is not 0). One offset serves every Bedrock player, as the lift does.
Checks for the first in-game test
- The default font on Java (
glyphs.font.replace-default, after/pf zip): chat, your name above your head and in
the tab list, the sidebar, an item's name, a chest's title, a sign and a book are all in the EGW font; a chat line has no bar at its left edge; an accented letter the style lacks (ą, ł) and an emoji still draw, in vanilla's font. Turn on Force Unicode Font in Language settings: the Unicode font, as vanilla. Type a sign line to its end, then look at it from a client without the pack: it runs a little past the board (the EGW font is narrower) — expected.
- The default font on Bedrock, after the restart: chat, nametags, the sidebar, signs and a form are in the EGW font.
Compare a plain chat line with one that holds a badge: the letters look the same and stand on the same line (a badge line is drawn from glyph_00.png, at glyphs.bedrock.lift). Letters neither touch nor gape (if every plain line's letters touch, Bedrock adds no pixel after a default8.png glyph — say so). If a badge line's letters sit a pixel higher or lower than a plain line's, /pf glyphs calibrate and the lift it shows put them level. é É Ç Ñ ─ █ · ± draw; spaces look normal. A hex-coloured prefix shows its exact colour. A player who sets Bedrock's chat font to the smooth one sees Noto Sans: expected. Then another player's nametag and a sidebar line with a mini: the badge (and the coin) level with the name, as before the switch. If it now sits about 3 pixels high, the name's letters having dropped, set glyphs.bedrock.world-offset: 3 by hand (the calibration stands only reach 2), /pf reload, /pf zip, restart — and say so ("The EGW font as the default font", "World text is where the two rules can part").
- A tag in a prefix, under LPC, on Java: drawn whole, white (not tinted purple), level with the name.
- The same on Bedrock after the restart. Bedrock draws wide tags as several cells: look for gaps, seams or overlaps
between the cells, most likely on the background style (ADMIN), whose box is cut every 8 units. Baseline: run /pf glyphs calibrate first ("Bedrock glyph size").
- Chat in the EGW font on Java; an accented letter or an emoji in a message still drawn (by the default font).
- The chat-line marker — decides a setting. With the font, every Java chat line has a bar at its left edge (dark
grey, "Message modified by the server" on hover). Try glyphs.chat-font.java.delivery: unsigned and /pf reload: a light grey bar instead ("Server message. Cannot be reported."). Keep the one you prefer — or turn glyphs.chat-font.java.enabled off for no bar and no font. A line from a player you hid in Social Interactions still shows with unsigned; that is expected.
- A Java player who declined the pack sees
[ADMIN], not boxes, and no box-font chat. - A default-group player pastes the admin tag (copy it from
/pf glyphs admin) into a message: nobody sees the tag,
and the console line has no box characters where it was.
- Add a tag to a helper config,
/pf reload, and chat with it in a prefix: Java players see it spelled out until
/pf zip and their pack reload, then the picture; Bedrock players see it spelled out until the restart. During the zip's re-push, a Java player briefly sees every tag spelled out and no EGW font (their old pack no longer counts).
- The console shows plain lines; EssentialsDiscord forwards the message as typed.
/pf glyphs adminfrom the game: clicking the command fills the chat box with the right characters.- Badges. With
%img_badge_admin%as the admin prefix under LPC: the badge sits level with the name (plate
centred on the Werks capitals, half a unit of line free above and below), not tinted by the purple format, no line above or below overlapping it. Two badge lines in a row: their plates do not touch. Compare one with assets/branding/egw-badges/preview.png. Change badge_admin's plate colour in items/packforge/badges.yml, /pf zip: the Java badge changes, its characters (in /pf glyphs badge_admin) do not.
- Badges on Bedrock after the restart: at
badge-pixels-per-unit: 1the plate one text line high, its label
readable; at 2 the designer's full plate. 3 to 7 cells wide — look for gaps, seams or overlaps every 8 units (the pieces are cut to join exactly, with no spacing of Bedrock's own between them), and where the plate sits against the name's baseline.
- Mini badges on the Bedrock sidebar. Put
%packforge_world_white_badge_admin_mini% &f%player%on a Bedrock board's
first line and %packforge_world_white_badge_admin% &f%player% on the next, /tab reload: the mini is one plate with the gear, no break in it, a pixel taller than the capitals beside it and level with them; the full badge below shows the breaks the mini avoids. Then the coin line in its world form: level with its words, not low.
- BETA, CROWSMP and EGW on a server that already had
items/packforge/badges.yml. After updating,/pf reload:
the load report's info line names badge_egw, badge_crowsmp, badge_beta as drawn from the jar beside the copy, and the copy itself is unchanged. /pf glyphs badge_crowsmp gives /lp group crowsmp meta setprefix 102 "%img_badge_crowsmp% "; set the three prefixes on the groups you already have, /pf zip, restart for Bedrock. A member of beta and nothing higher shows BETA in chat, in the tab list and above the head; one also in vip shows VIP. On a Bedrock board, %packforge_world_white_badge_crowsmp_mini% is one plate with the crow, no break. :beta: in chat is the flask icon.
- Icons.
:heart: :coin: :crown:in chat on Java and Bedrock: full colour, sitting on the line like capitals.
With LPC's own :heart: replacement left in place, :heart: still shows the EGW heart (LPC writes ❤, which becomes the icon); with glyphs.unicode-emoji: false it shows LPC's ❤ instead.
- Emoji. After
/pf zip(and the restart, for Bedrock)::joy: :thumbsup: :100: :smile: :lol: :full_moon:in
chat on Java and Bedrock — the EGW faces in brass, not tinted by the message colour; :laughing: (an earlier name) is the same face as :joy:. From a phone (Bedrock or a Java client with an emoji picker): 😂 👍🏽 🤷🏾♀️ ❤️ 🔥 🌙 — the tears-of-joy face, one brass thumb (no stray tone after it), one shrug, the EGW heart, fire and moon. Then 👩🌾 and ❤️🔥: each stays the one emoji it was typed as, drawn by the player's own client, with no EGW wheat or heart pulled out of it. A Java player without the pack sees the same line with the emoji as typed; the console shows them as typed. On Bedrock at icon-pixels-per-unit: 1, compare the faces with assets/branding/egw-emoji/preview.png's 1x row: eyes and mouths readable, 8 pixels tall beside 7-pixel capitals, not overlapping the next letter; look closely at exclamation and point_up — narrow art, a full emoji's room. Write :lol: and 😂 on a sign: the face, in colour, level with the words on Bedrock. /pf glyphs lol opens joy with its aliases and Unicode. Note how the 1x faces read on the owner's Bedrock device for the designer.
- Discord relay. Type
gg :joy: :thumbsup: :100: 😂in game and look at the relayed line in Discord: it must
arrive as typed, and Discord should show its own 😂 👍 💯 for the shortcodes. If Discord shows :joy: as text, the relay posts shortcodes as they are and needs to swap them for Unicode (§8 "Emoji", "Not yet seen with a bot"). Then send 😂 from Discord: in game it shows as the relay posts it — the EGW face only if the relay sends it as player chat.
- Signs. As a default player, write
:coin: SHOPon the front of an oak sign and:diamond:on its back: both
icons in full colour on the black text, on Java and Bedrock; the words stay black. Try a hanging sign, a dark oak sign, and one with glow ink. Write :badge_admin: and paste the admin badge's characters: neither appears. /lp user <name> permission set pf.glyphs.signs false: the next sign keeps :coin: as typed.
- TAB. With TAB installed,
tabprefix: "%packforge_white_badge_admin% &c"for the admin group in TAB's
groups.yml and /tab reload: the badge in the tab list, in its own colours, on Java and Bedrock. A header line %packforge_white_egw% &3Evergrims Werks shows the EGW mark. Live's tabprefix: "%luckperms-prefix%" shows the LuckPerms prefix's badge too. Above the head: tagprefix: "%packforge_world_prefix%" in _DEFAULT_ (next list).
On Bedrock, after /pf zip and a restart (/pf status should say Bedrock chat font: white + 4 colours live):
- Lift first — decides a setting.
/pf glyphs calibrateas a Bedrock player, then the steps in "Bedrock glyph
size" above until a line of EGW chat stands on Bedrock's own line. Check that no glyph overlaps the next and that the EGW capitals at pixels-per-unit: 1 are exactly as tall as the plain ones. Note the lift that won (and the device) for the next release's default. Also compare the badge and the coin at 1 and at 2, to choose badge-pixels-per-unit and icon-pixels-per-unit.
- World text — decides a setting.
/pf glyphs calibrate worldas a Bedrock player, then the steps in "Bedrock
world text" above until the badge and coin sit level with Ag and Tricks. Then TAB's tagprefix: "%packforge_world_prefix%" and /tab reload: another player's nametag (nobody sees their own) shows their badge level with their name, on Bedrock and Java; the tab list (%luckperms-prefix%) and chat unchanged. Note the offset that won (and the device) for the next release's default.
- Signs on Bedrock. Write
:coin: SHOPon a new sign: the coin level withSHOP, not half a line low. If it sits
high instead, signs follow chat's rule after all: report it (signs would go back to the glyphs' own characters).
- Letter spacing. EGW letters carry their own one-pixel gap, since Bedrock is documented to add none between
glyphs. If converted chat looks spaced out next to Bedrock's own words (two pixels between letters), Bedrock adds a gap after all: report it, it is a one-line change.
- Tinting — the one that decides a setting. Chat as a player whose LPC group format colours the message with a
named colour (live's admin format uses <light_purple>). Light-purple EGW letters: Bedrock tints glyphs, and glyphs.bedrock.tinted: true is right. White letters: set it to false, /pf reload, add the named colours your formats use to glyphs.bedrock.colors as hex (<light_purple> is '#ff55ff'), then zip and restart. Nothing in Bedrock's documentation says which; it is reasoned from Java.
- Exact colour. Put
<#cf5a3c>(ember) before{name}in a test group format: the name on Bedrock is the same
colour as on a Java screen, not Bedrock's nearest red. Then <#ff0000>, which has no alphabet: Bedrock's nearest red (with tinted: true).
- Letters. After calibrating, EGW letters the size of Bedrock's own, on the same baseline (type
Éa—Éis
Bedrock's own, a is EGW), spaced like words rather than like a grid, with no clipped tops; g, y and p keep enough tail to read (at lift 1, one pixel: g and y end in a hook to the left, q and p go straight down). A line of only EGW letters is the same height as a line of Bedrock's own (system messages in the same chat are the comparison).
- Bold and italic (
<b>,<i>) on converted letters: whatever Bedrock does to a glyph (possibly nothing). - Others untouched. The same line on Java and in the console has its ordinary letters; copying a Bedrock line gives
private-use characters (expected).
- The restart rule. Add a colour,
/pf zip(the report says the new pack waits asPackforge-Glyphs.mcpack.next
and asks for a restart), chat once: Bedrock still writes the old set. Join with a second Bedrock device that never had the pack (or one whose pack cache was cleared): it downloads the old pack and shows the same letters, none blank, and Geyser's log has no "Detected a change in the resource pack" line. Stop the server: Packforge's last lines say the pack was put in place. After the start, the new set on both, and .next is gone from packs/. Once, kill the server instead of stopping it after such a zip: the next start puts the pack in place before Geyser loads it (the boot log says so), and Bedrock chat writes the new set.
9. Troubleshooting
Players see vanilla textures and the pack screen times out. pack-host.public-address is blank or the port is closed. /pf status shows the URL being handed out — try fetching it from outside the server.
Boot says PACK URL ONLY WORKS ON THIS MACHINE: (or NO PACK URL:). The download URL's host is 127.0.0.1, localhost or 0.0.0.0 — which on a player's computer means their own computer — or there is no address at all. Fine on a test server you only join from the same machine; anywhere else, set pack-host.public-address to the IP or host name players connect to. With the default kicks, an unreachable URL is not "no textures", it is every Java player kicked.
Every Java player is kicked with "The server's resource pack did not load". Their downloads fail (FAILED_DOWNLOAD in the log, one Kicked <name> line each): the URL in /pf status is not reachable from outside — wrong address, closed port, or another process holding the port. Set pack-host.kick.on-fail: false and /pf reload to let players in on vanilla textures while you fix it, then turn it back on.
A player is disconnected with the client's own "requires a custom resource pack" text, and the log says <name> left at the resource pack prompt without accepting the pack. They declined the pack, or closed the game at the prompt. That is the client enforcing a required pack — it leaves by itself before the server hears a decline, which is why your kick.decline-message is not shown. They rejoin and accept. To let decliners play without the pack instead, set pack-host.kick.on-decline: false and /pf reload.
One player cannot download the pack and the log says "Pack host blocked <address>". That address made more than 3 requests in 2 seconds, 5 times, so it is refused for 30 minutes — a client stuck in a download loop, usually. If several players share that address because a reverse proxy sits in front of the pack host, set pack-host.rate-limit.enabled: false. Otherwise the block ends on its own; a restart also clears it.
The server freezes for seconds after /pf zip, move or reload (Java and Bedrock players alike). It should not any more: the heavy work runs off the main thread, and recipes are only re-registered where they changed, in at most two batch calls (an unchanged set touches nothing). If a stall comes back, set debug.timings: true, /pf reload, and repeat the command: the [timings] run line says which phase took how long on which thread, and the [timings] main thread tick N lines name every tick Packforge spent time in and on what. A recipe add/recipe remove step (one batch each on 26.3) costs a few ms plus ~10 ms per online player (measured on Paper 26.2) — that is Paper resending everyone's advancements, tags and recipe book; more than one of each per zip means a recipe was turned down part-way through a batch (the server log names it). Send the lines with the report.
Java players see a black flash, then small stutters, after /pf zip. That is their client reloading the pack: the loading screen, then every chunk drawn again. It happens only when the zip changed the pack (the report says Repushed output/pack.zip to N players); a zip that built the same file says it was not re-sent, and nobody's client reloads. If the flash comes with an unchanged zip, turn on debug.timings: the /pf zip run line ends pack unchanged, not re-sent: sent to 0 — anything else there, send it with the report.
A player lost the pack, or you fixed a texture and want to see it without a rebuild. /pf push <player> re-sends the built pack to them; /pf push all to everyone online. It rebuilds nothing — run /pf zip for that.
zip refused because of pack.mcmeta. One of your pack-host keys would make the served pack.mcmeta one the client cannot read, and the refusal names which part. If it names pack-host.description, the component has a slip the client cannot decode — a colour spelled grey, an empty list, a part with no text — and a client that cannot decode the description drops every custom texture at once: fix it, or write plain text. If it names a format, the client would load the pack only as one made for another version: set pack-format, min-format and max-format back to -1 to keep the content's own, or set them consistently (97 each on 26.3; an old pack-format: 88 left from 26.2 is outside 26.3's range and is refused). Then /pf reload and /pf zip. The pack players hold is untouched until a zip succeeds.
Bedrock players are kicked for declining a resource pack. Check skip-bedrock-players: Bedrock players should not receive Packforge’s Java pack prompt. If that setting is enabled and Packforge still sends the Java pack to Bedrock, report the behaviour.
New Geyser mappings or skulls have not appeared on Bedrock. /geyser reload is not enough — it reloads packs but not mappings. A full server restart is required. Any command here that writes to Geyser's folders says so.
move warned about a content. It was placed; the warning names what its two sides disagree on. The common one is a state in the content's Java blockstates file that no installed Geyser mapping overrides — usually an installed mapping older than the pack, so install the mapping that matches the release and restart. A model "in a namespace nothing ships yet" means the content that owns that namespace has not been moved: move it too. A Geyser override for a state no Java blockstates file draws is fine when Java draws that block as a display entity, or the mapping is kept for blocks placed by an older version.
move refused a content. Read the refusal; it names the content, the file and why placing it would damage something: a folder it cannot read, a mapping or .mcpack it cannot open, a .mcpack holding only its manifest, one content shipping one path twice with different bytes, or the collision below. Nothing of that content was placed; fix the input and move again.
move refused a vanilla-namespace collision. Two contents both ship assets/minecraft/blockstates/tripwire.json, with different bytes. The client loads exactly one file at that path and stacked packs do not merge JSON, so the plugin refuses and names both rather than picking a winner. Identical copies are not refused, and neither is a new release of the same pack under another drop layout: a pack is known by its pack.mcmeta id or its folder name (§3). Neither pack needs editing: to serve the refused content's copy instead, delete the named file from contents/ and move again; to keep the installed one, leave the refused content in drop/.
move warned that it "took back" files. A whole new release of a pack no longer ships them, so they were moved out of contents/ (or items/) into taken-back/<time>/, at the path they had. If one should have stayed — the drop was meant as a patch — copy it back and /pf zip. A one-file fix dropped the way §7 "Patch one file" lays it out is never read as a release.
zip warned that two folders ship the same path. Two folders in contents/ — usually an old and a new release of one pack copied in side by side — ship files at one pack path with different bytes. The zip took the later folder's in sorted order. Delete the folder you replaced and zip again.
A config in contents/ was "not read in" because an items/ file is "treated as your own". That items/ file has no record of being read in: you wrote it, or an older Packforge copied it before the record existed and it has changed since. Delete it and /pf reload; the config is read in and tracked from then on.
A recipe key is already registered. Another plugin owns the same key. Check the load report and remove the duplicate recipe provider before restarting; two plugins cannot register the same recipe key.
A Dreadplate piece gives an empty result at a smithing table. That is the re-trim lock (dreadplate_retrim_lock.yml), as on live: nobody holds egw_retrim_lock, so nobody can put a registry trim on a piece, the helmet included. An op, or a group with *, gets the piece back unchanged instead — still not the trim. To let trims through, rename the lock file to .yml.off — in contents/egw_showcase/configs/ for a folder copied in, or items/egw_showcase/ for one placed by /pf move — and /pf reload; the chestplate, leggings and boots, which ship their own trim, are still held by the safety net unless the player is granted pf.smithing.retrim by name.
Players cannot see or craft a pack recipe. Check the pack’s pf.recipe.<permission> grant. Empty permissions use items.open-recipes-without-permission. Run /pf reload or have the player rejoin after correcting access. /pf status reports registered recipes.
An item a player holds still looks like before a helper-config edit. It is rebuilt when they rejoin or open a container holding it (items.auto-update). A stack in a plugin's menu is never rebuilt, by design. If it still does not change, check the edit reached items/ (the reload report names harvested configs) and that items.auto-update is true.
/pf give says "No item … is defined". The id is <namespace>:<id> as the helper config writes it (egw_showcase:dreadplate_wings); tab completion lists every id, and finds one by the part after the colon too.
Two pack items in a crafting grid, or a pack item in a recipe, give no result. That is the crafting-grid guard. A pack item goes only into a helper-config recipe that asks for it, and grid repair needs two of the same pack item — then the result is that item, not its vanilla base. To repair a pack item with its material, use an anvil. If two copies of one item still give nothing, one of them may be from a helper config that no longer defines it (a pack item is never handed back as its vanilla base); check it is still in /pf browse.
A hopper will not feed a Crafter, or a Crafter will not craft. The hopper or Crafter holds a pack item: pack items never go into a Crafter, and a Crafter never makes a helper-config recipe or crafts while a pack item is inside (guards.crafter). The hopper keeps the item, and everything queued behind it waits; take the pack item out of the hopper or the Crafter. The Crafter stays in place.
A Grims Chests token, Crops item, trap, backpack, Wifi part or Walkman is refused at a station. It should not be: the guards only refuse stacks carrying a recognised pack-item tag, and those plugins' stacks carry neither. A copy taken from /pf browse is Packforge's own pack item, not the plugin's; use the plugin's own give command. If a stack the plugin itself made is refused, report it with the plugin's name and the station.
Crops render as the wrong thing, or not at all. Check Paper's block-updates.disable-tripwire-updates is true. /pf status reports it. Without it, neighbours and entities reset tripwire states and your crops become string.
A tag or emoji shows as [ADMIN] instead of the picture. That is the stand-in for a player whose pack does not draw that glyph (yet). Chat checks each glyph against the pack the player actually holds. On Java: the player has no pack (declined, failed, still loading), or the glyph was added or moved by /pf reload after the last /pf zip — run /pf zip (online players are sent the new pack, and see the picture once it loads). On Bedrock: the server has not been restarted since the zip that drew the glyph into Packforge-Glyphs.mcpack (until then that pack waits as Packforge-Glyphs.mcpack.next) — or Geyser was not installed then, or glyphs.bedrock-pack.enabled is off.
A typed emoji (😂, ❤️) stays the phone's emoji instead of becoming the EGW one. That is what a player sees whose pack does not draw the glyph yet: the emoji is left as typed, never spelled out. Java: no pack, or the emoji was added after the last /pf zip. Bedrock: no restart yet since the zip that drew it. For every player: glyphs.unicode-emoji is off, the glyph is shortcode: false or needs a pf.glyph.<p> the sender lacks, or no glyph lists that exact sequence — /pf glyphs <name> shows what one lists (a rare skin-tone or gendered form may be missing; add it to unicode: of a glyph of your own). Private messages and other plugins' messages are not chat, and keep their emoji. A Java client without the pack may draw the emoji as an outline or a box: vanilla's font has few.
The load report warns that the egw_emoji content folder still defines the emoji. The emoji are built in; that folder (from assets/branding/egw-emoji), exported before the emoji took Discord's names, takes their place under the earlier names (laughing, thumbs_up), without their hand-drawn Bedrock art, aliases and Unicode — and its names take those shortcodes from the built-in emoji, so the load also warns about them as aliases. Delete contents/egw_emoji/ (and its egw-emoji.yml under items/, if you copied it), /pf reload, /pf zip. (A current export is only noted, not warned about.)
An emoji's placeholder (%img_laughing%) stopped working, or an emoji on a sign shows nothing. 33 emoji took Discord's names (laughing is joy now, §8 "Emoji"); a placeholder takes a name only, so write %img_joy%. Their characters belong to the new names: a sign written with one under the build that used the earlier names holds characters that draw nothing now — write it again. Typed :laughing: keeps working (an alias).
The load report warns about an alias or a Unicode sequence. Two glyphs claim one shortcode or one emoji. A name always wins over an alias (the alias is left out); an alias or a sequence two glyphs declare goes to the first (your helper configs come before the built-in sets). The warning names both: rename one, or take the alias or sequence out of one of them.
A tag shows as a box in chat. Chat never sends a glyph to a player whose pack lacks it, so a box comes from text that holds characters nothing draws: a prefix set with the characters of a glyph that has since moved (the zip report said so — copy it again from /pf glyphs, or use %img_<name>%, which follows), or characters of a glyph deleted from its config.
/pf glyphs does not list a glyph, or a tag has a ? in it. /pf reload and read the report: it names every glyph it left out and why — a colour written without quotes (YAML reads #cf5a3c as a comment), an image the pack does not ship, no EGW font installed, a name two configs share. A ? is a letter the EGW style does not draw.
Chat is not in the EGW font. On Java, only players whose pack loaded get it. /pf status says on its Items line whether the served pack draws the default font in EGW (default font EGW on Java (served)) or carries the chat font; after changing glyphs.font.replace-default or glyphs.chat-font.java.enabled, run /pf zip. Under LPC, mixed parts settings put the font on the whole line or nowhere (§8) — they apply only with glyphs.font.replace-default off.
On Bedrock, /pf status says why on the same line: not loaded yet — run /pf zip, then restart (Geyser reads packs only at its startup, and chat writes only what the loaded pack holds); no Geyser — the pack was never written; nothing to draw — no EGW font is installed (the load report says so); off — glyphs.bedrock.chat-font.enabled, glyphs.bedrock-pack.enabled or every glyphs.bedrock.chat-font.parts entry is false. An accented capital, an emoji or obfuscated text staying in Bedrock's font is expected (§8).
Bedrock players still see Bedrock's own font everywhere, or only in chat. The default font reaches Bedrock only through Packforge-Glyphs.mcpack, so after /pf zip the server must restart (/pf status: Bedrock (after /pf zip and a restart); once loaded, Bedrock (live)). Geyser must be installed and glyphs.bedrock-pack.enabled on (the zip report says not on Bedrock: and why). A player whose chat font is set to the smooth one in Bedrock's settings sees that font — ask them to switch it back to the default. Screens of Bedrock's newer Ore UI keep their own font whatever a pack does.
On Bedrock a character shows as nothing (Greek, ½, ©, Œ, ²). Expected while glyphs.font.replace-default is on: Bedrock's own font sheet is replaced whole, and the characters the EGW style cannot draw are left empty rather than filled with Mojang's art (§8, "The EGW font as the default font"; the zip report lists them). Avoid them in names, prefixes and boards, or turn the option off for Bedrock's own font back (/pf zip, restart).
After the font change, a centred chat line, a column of spaces or a menu title looks shifted. The EGW font is narrower than vanilla's (Werks by about a quarter), and the plugin laid that text out with vanilla's widths. Re-centre or re-pad it by eye in the new font, or turn glyphs.font.replace-default off. A menu whose background picture comes first and its title after it is unaffected.
Bedrock chat in the EGW font is all white, whatever its colour. Bedrock does not tint glyphs the way Java does. Set glyphs.bedrock.tinted: false and /pf reload: every colour then uses the nearest drawn alphabet. Add the colours your formats use to glyphs.bedrock.colors (named ones as hex: <gray> is '#aaaaaa'), /pf zip, and restart.
A colour is close but not exact on Bedrock. It has no alphabet of its own. Put it in glyphs.bedrock.colors (or in a LuckPerms prefix with colors-from-luckperms on), /pf zip and restart. If the zip warns that only 22 alphabets fit, the colours it names are left out: trim the list, or move the ones that matter to the front.
Bedrock players see blanks or boxes where chat letters should be. Their client does not hold the pack chat is writing for: a player who refused the pack (keep Geyser's force-resource-packs: true), or Packforge-Glyphs.mcpack was deleted, replaced or renamed by hand since the startup (Geyser's log then says "Detected a change in the resource pack"). Packforge itself never changes that file while the server runs: /pf zip leaves its new pack as Packforge-Glyphs.mcpack.next until the server stops, so do not copy that file into place by hand. Restart after any change to Geyser's packs/. If the log at startup says the pack "is not a pack bedrock-glyph-pack.yml describes", chat is in Bedrock's font on purpose: /pf zip and restart.
On Bedrock, glyphs and EGW chat look twice the size of Bedrock's own text, sit too high or too low, or overlap. Twice the size is a pack built before this release (the art at two pixels a unit) or pixels-per-unit: 2; the first /pf zip and restart with this release draw at 1, the text's own size. Too high or too low: run /pf glyphs calibrate as a Bedrock player and follow §8, "Bedrock glyph size" — set glyphs.bedrock.lift, then /pf reload, /pf zip and restart. Glyphs running into each other: every sheet is now 256 pixels with 16-pixel cells, which is what fixed the overlapping calibration glyphs; if it still happens, note which glyph and the device. Still the same after a change? The zip report's Bedrock glyph density: line says what the new pack draws at, and the startup log says when the pack Geyser loaded draws at something other than config.yml asks — a missed reload, zip or restart.
On Bedrock, a badge in a nametag sits low (its top at the middle of the name) or high, while chat is right. The nametag is written with the glyphs' chat characters, or the world offset is off. TAB's tagprefix should be %packforge_world_prefix% (or %packforge_world_white_<name>%), not %luckperms-prefix% — check a group that sets its own tagprefix too. Right after the update the world placeholders still give the chat characters until /pf zip and the restart after it (the zip report's Bedrock world text: line says the pack Geyser serves has none yet). Then run /pf glyphs calibrate world and set glyphs.bedrock.world-offset (§8, "Bedrock world text"). An empty %packforge_world_prefix%: LuckPerms is not installed, or the player has no prefix.
On the Bedrock sidebar a badge shows gaps between its pieces, or a badge or an icon sits low. The sidebar places glyphs as nametags do and leaves a gap between glyph characters. Write the world form there (%packforge_world_white_<name>%), and for a badge its mini (%packforge_world_white_badge_<rank>_mini%): one character, so nothing to break. /pf glyphs lists the minis once loaded; Bedrock players see them after the /pf zip and restart that bring them. A mini that is still cut (the load report says so) has an emblem too wide for one cell.
/pf glyphs calibrate world says there is nothing to measure, or spawns nothing. The pack Geyser loaded at startup has no world calibration set: /pf zip, restart, rejoin, run it again. From the console it says it needs a player. The stands go after 60 seconds — run it again to put them back where you stand.
/pf glyphs calibrate says there is nothing to measure, or no calibration glyphs. The pack Geyser loaded at startup is not one this release built: there has been no /pf zip and restart since the update (or bedrock-glyph-pack.yml lost its record). /pf zip, restart, rejoin, run it again. From Java or the console it only says it is for Bedrock — the lines are Bedrock glyphs, which a Java client draws as boxes.
A badge, tag or icon looks tinted (purple, grey) in the tab list or on a scoreboard. Outside chat nothing whitens it: use %packforge_white_<name>% there instead of %img_<name>% — on a scoreboard, %packforge_world_white_<name>% (in TAB, not in a prefix LPC reads — §8, "TAB, scoreboards and other plugins").
Chat stopped being formatted after a prefix change; the console shows "Legacy formatting codes have been detected". A LuckPerms prefix or an LPC format holds a § code — most likely %packforge_white_…%. LPC reads both as MiniMessage, which refuses §. Use %img_<name>% in prefixes LPC reads (chat whitens glyphs by itself), or <white>…</white>.
An icon on a sign is a black shape, sits low on Bedrock, or :coin: stays as typed. Black: the sign was written before Packforge's sign support, or by a plugin that set its text directly (re-write the line). Low on Bedrock: the sign was written before world forms (re-write the line), or by another plugin with the chat characters (%packforge_world_white_<name>% there). As typed: the player lacks pf.glyphs.signs or the glyph's own pf.glyph.<p>, the glyph is shortcode: false (badges and tags always are), glyphs.signs.enabled is off, or no glyph has that name (/pf glyphs).
A badge colour change does not show. Edit items/packforge/badges.yml (or whichever file is marked packforge-builtin: badges), then /pf zip — /pf reload alone only re-reads it. Bedrock shows it after the restart the zip report asks for. If /pf glyphs badge_admin names "(built in) badges.yml" as its file, no marked copy was read: the copy is missing (it is written at the next start or /pf reload), it does not parse, or glyphs.builtin.badges is off.
Every badge is back to its release colours, and a badge you added is gone. The badge set's copy stopped reading as YAML — usually one line indented with a tab. The load report (start, /pf reload, /pf zip) says "items/packforge/badges.yml is the rank badges' editable copy, but it does not read as YAML" and ends with the parser's own words, which name the line. Until it is fixed the release's default badges stand in, so a /pf zip in the meantime ships the defaults. Fix that line, /pf reload, /pf zip: the badges come back with the same characters, so no prefix needs touching. The file is never written over while it is broken (deleting it also brings the defaults back, and throws your edits away).
A bossbar is stuck on screen. It should clear on death, despawn, chunk unload and plugin disable. If one persists, relog — and report it.
10. Known limits
The carrier pool is tripwire only. Six carriers are implemented — tripwire, note_block, three mushroom blocks and chorus plant — but only tripwire is in the default pool.
note_block is the interesting one: Grims Crops already draws its Stove, sprinklers and still head on eighteen note_block states, so the host adopts and protects them. It stays out of the pool because a note block a player places is instrument=harp, note=0, powered=false — which is Crops' Stove — so every vanilla note block renders as custom content. Crops accepts that on a server that places none; name it in carriers.pool only if you accept it too.
Mushroom blocks and chorus plants are implemented but excluded: huge-mushroom generation sets exactly the six face booleans a mushroom carrier would use, and chorus plants recompute faces from neighbours and self-destruct when unsupported. No state of either is unreachable in survival, so a generated block anywhere in the world would render as your content.
The practical lever is headroom, not a new carrier: Crops reserves 64 states and draws 32. Every crop reserves a block of 8 and uses 4. Reclaiming that doubles capacity — but do it before those states are in worlds, because adopted claims are marked immutable on purpose.
A chorus custom block borrows a state natural chorus can reach. Once the fish trap is declared (or defined) and zipped, chorus plants in its exact state — west connection only — draw as a trap on Java, as Fishin's mapping already makes them on Bedrock. Natural chorus can still reach a state claimed by custom artwork. Paper's block-updates.disable-chorus-plant-updates keeps placed chorus from re-shaping, but does not stop world generation from producing that shape in the End.
Movement while downloading: Packforge normally holds the player at the loading screen before joining. If the configured timeout lets them into the world before the download finishes, they can move while it completes. An unanswered pack prompt can be offered again after join.
Declining a required pack: the client normally disconnects itself using its own message. kick.decline-message applies only when the client reports a decline and remains connected.
Station guard behaviour:
- Material repair on an anvil is allowed.
- A hopper cannot feed a pack item into a Crafter. The item stays in the hopper.
- Anvil costs follow vanilla rules.
- Grid repair carries curses over, following vanilla behaviour.
be repaired into an uncursed one.
- The crafting grid gives nothing for any recipe that is not a helper-config recipe while a pack item is in it,
The crafting guard also covers computed vanilla recipes, including dyeing, fireworks, banners, decorated shields, maps, books and pots, plus other plugins’ dynamically calculated results.
- On an anvil, a pack item offered as a vanilla item's repair material is refused (an iron pickaxe on the left, a
The guard checks both inputs, so a vanilla item cannot consume a pack item as its ordinary base material.
- A Crafter with a pack item already inside crafts nothing at all, whatever the recipe — a backstop for one put there
while guards.crafter was off.
The guards cover the crafting grid, the Crafter, the anvil and the grindstone only: stonecutters, looms and smithing upgrades are not covered (the smithing re-trim lock is separate, see §3).
Smithing and recipe behaviour:
- A smithing result does not duplicate lore lines already present on its base item.
Only a player-assigned name is carried to a transformed item; its old configured display name is not copied over the new item’s name.
- Without the required permission, the recipe result is unavailable.
empty.
- With
items.open-recipes-without-permission: true, a recipe whosepermissionis empty is open to everyone.
items.open-recipes-without-permission is off by default. Enable it to open helper-config recipes with an empty permission to ordinary players. Explicit pack permissions remain separate.
- Every allowed recipe is unlocked at join, not only when its item is first touched — so a Bedrock player allowed to
Eligible recipes unlock without requiring the player to own the output item first.
Item-update limits: updates run on join and container opening, not on pickup, drop or inside plugin menus. items.auto-update is a global switch. Bukkit persistent data is preserved; unsupported raw custom data outside that container is not. Configured names refresh, repair cost is preserved, and gear temporarily converted by Bedrock Enchant Bridge waits until it is restored.
Registries are bootstrap-only. A new mob variant needs a restart, not /pf reload.
Bedrock gets one pack. Geyser's packs/ is global — every Bedrock player receives everything in it, and there is no per-player selection. The second pack is Java-only for that reason, among others.
Console Bedrock cannot sideload packs at all. Marketplace is the only route, so server-sent packs are the only way content reaches an Xbox, PlayStation or Switch player.
Menu overlays do not exist. Custom GUI background art is a font glyph drawn over a chest's title on Java. Bedrock's glyph sheets — which chat glyphs do use — cannot be placed over a container that way, and a feature Bedrock cannot respect does not ship here.
Java chat in egw:chat always carries a marker. The 26.2 client tags signed chat that uses a non-default font as "modified", and unsigned chat as a "server message"; both draw a bar beside the line. glyphs.chat-font.java.delivery picks which (§8, "The chat-line marker"). With glyphs.font.replace-default on (the default) chat needs no font of its own and carries no marker; with it off, only turning the Java chat font off removes it.
Bedrock's own font has no fallback. With glyphs.font.replace-default on, font/default8.png and font/glyph_00.png are Packforge's whole: a character the EGW style cannot draw, compose or rule there shows as nothing on Bedrock (Greek, a few maths signs, ½ © ™ ® Œ), because Mojang's font art is never shipped. Players who pick Bedrock's smooth chat font, and Ore UI screens, keep their own font.
Exact hex colours on Bedrock are chat only. Bedrock has no Java font providers and no hex colours, so chat text in a hex colour Bedrock lacks is rewritten into EGW alphabets drawn in the glyph pack, one per colour: at most 22 (white and 21 colours), only chat lines (not forms, titles, scoreboards, signs or private messages), only after a restart that loads the pack, and copied text is private-use characters. Everything else shows in the nearest of Bedrock's 28 colours — in the EGW font through Bedrock's own sheets while glyphs.font.replace-default is on, in Bedrock's font with it off (then every chat line is rewritten, and accented capitals and anything else the alphabets lack stay Bedrock's). The full list is in §8, "EGW font on Bedrock".
A wide glyph is several characters on Bedrock. Bedrock draws one cell per character, so a tag or a badge is cut into 8-unit pieces, between letters where it can be. Each piece is measured from its first column to its last (a faint pixel keeps an empty edge column), so they join without a gap of Bedrock's own; Java draws the whole glyph on its first character. At one pixel a unit a piece fills half its cell's width, but the cutting stays the same at every density: changing how many characters a glyph takes would change the characters in every prefix holding it. Bedrock's sidebar leaves a gap between the pieces anyway; there, use a mini badge, which is one character.
A mini badge is the emblem alone, at the text's size. No label, 8 pixels tall at one pixel a unit whatever badge-pixels-per-unit says, so ranks tell apart by emblem and colour only (ADMIN's gear and SYS-ADMIN's terminal share the ember plate), and a halved emblem is a few pixels of itself. A mini drawn at two pixels a unit would not fit one character, which is the whole point of it.
At the text's own size, the EGW art loses detail. One pixel a unit is half the art's resolution: letters are three to five pixels wide (g and y give a row of their bowl to their hook, as Bedrock's own font does), badge emblems become a few pixels, and an icon is an 8-pixel sketch of itself. icon-pixels-per-unit: 2 and badge-pixels-per-unit: 2 keep those whole, at twice the text's size. Nothing can hang below a Bedrock glyph cell, so descenders are as deep as the lift.
The Bedrock baseline is measured, not known. Bedrock's documentation and the glyph tools put a cell's bottom on the text's baseline; the default lift of 1 keeps a pixel of tail and stands letters a pixel up. /pf glyphs calibrate shows lift 0, 1 and 2 on your device. One setting serves every Bedrock player, so if devices disagree, one of them is a little off.
Bedrock's world text rule is inferred from one nametag. The world forms are centred because a nametag badge sat half a line low while chat was right; signs are assumed to follow nametags. /pf glyphs calibrate world corrects the height on your device; if signs turn out to follow chat instead, they need the glyphs' own characters. Signs written before world forms keep the chat characters until edited.
Typed emoji become glyphs in player chat and on signs only. Like shortcodes, the conversion happens where Packforge finishes a line per player (chat) or as a sign is written. Private messages, other plugins' messages, books, anvils, item names and command arguments keep the emoji as typed. A Java player without the pack sees the typed emoji as their client draws it (vanilla's font draws few emoji, as outlines), and a Bedrock player before the restart as Bedrock does. Copying converted chat gives private-use characters, as for any glyph. So a Discord relay's own lines in Minecraft — plugin messages, unless the relay sends them as a player's chat — keep Discord's emoji as Unicode.
Only Discord's default emoji relay. The emoji carry Discord's default shortcodes, so :joy: means the same on both sides; a Discord server's custom emoji have no shortcode Minecraft could send, and EGW's own gg and torch reach Discord as text. Whether Discord draws a shortcode a bot or webhook posts is untested (§8 "Emoji").
On Bedrock, every picture glyph takes its whole picture's width. Bedrock ends a glyph where its last visible pixel is, so each piece is measured to its picture's last column with a faint pixel, plus the one-pixel gap: an emoji or an icon is 9 pixels wide at one pixel a unit even when its art is narrower (exclamation is 2 pixels of ink). Java spaces a glyph to its ink, so a narrow emoji sits a little looser on Bedrock.
Outside chat a glyph is the same for everyone. Chat spells a glyph out ([ADMIN]) for a player whose pack cannot draw it; signs, the tab list, nametags and scoreboards cannot be finished per player, so a Java player who declined the pack sees boxes there, and a glyph added since the last zip (or, on Bedrock, the last restart) shows as a box or nothing until then.
11. Related documents
CHANGELOG.md— what changed in each version (0.2.0: the 26.3 port)The-WerkShop/docs/26.3/BLADEFX-PACKFORGE-26.3.md— the 26.3 port plan, and the in-game checks for the 26.3 test
server (its section 3c)
dist/0.1.0-checkpoint/TEST-PLAN.md— the in-game verification sequenceThe-WerkShop/docs/Crossplay-Helper-Plugin-Scope.md— scope, and every design decision with its reasoningGrimsCrops/docs/CROSSPLAY-HOST-MIGRATION.md— what Crops needs to do to move over (almost nothing)