# Myriad documentation (every page) Source: https://myriadclient.dev/docs --- Page: https://myriadclient.dev/docs # Myriad docs > How to use Myriad, the addon-first Minecraft utility client, and how to build addons for it. Myriad is a free, open-source utility client for Minecraft 26.2 on Fabric. Its menu is a tiling window manager, every module comes from an addon, and the stock modules pass Grim on 2b2t. These docs have two halves. - [Using Myriad](https://myriadclient.dev/docs/use): Installing, the menu and its keys, modules and settings, control panels, macros, themes, sharing your setup. No programming needed. - [Developers](https://myriadclient.dev/docs/develop): Writing addons: modules, settings, events, the menu, rendering, the services, hooks, shared configs, publishing. With complete, working code. ## For AI agents Everything here is also available as plain Markdown for language models: - [`/llms.txt`](/llms.txt): an index of every page, with a one-line summary each. - [`/llms-full.txt`](/llms-full.txt): the whole documentation in one file. - Any page as Markdown: add `.md` to its address, for example [`/docs/develop/hooks.md`](https://myriadclient.dev/docs/develop/hooks.md). Start an agent on [Developers → For AI agents](https://myriadclient.dev/docs/develop/agents), which says how to read these docs and the rules an addon has to follow. --- Page: https://myriadclient.dev/docs/use # Getting started > Install Myriad, open the menu, and turn on your first module. Myriad is a Fabric mod. It adds a menu (a tiling window manager, like a desktop inside Minecraft) and a system of addons that provide the modules. The stock modules come in **Myriad Essentials**, which most people install alongside Myriad itself. ## Install ### Install Fabric Install the [Fabric loader](https://fabricmc.net/use/installer/) for Minecraft 26.2, and put the [Fabric API](https://modrinth.com/mod/fabric-api) in your `mods` folder. ### Add Myriad and Essentials Download `myriad-.jar` from [Myriad's releases](https://github.com/MyriadClient/myriad-core/releases/latest) and `myriad-essentials-.jar` from [Essentials' releases](https://github.com/MyriadClient/myriad-essentials/releases/latest), and put both in `mods`. ### Start the game Myriad's title screen appears, in Myriad's colours, with the usual buttons plus one for the Myriad menu. > **Note:** Myriad keeps itself and your addons up to date: when there's a new version, a notification says so, and the **Addons** window installs it. Nothing installs without you asking. ## Open the menu The menu opens with the **mod key** and a number. The mod key is `Alt` unless you change it. | Keys | What it does | |---|---| | `Alt`+`1` … `9` | Opens the menu on that workspace. The same keys again close it. | | `Alt`+`Space` | Opens the **launcher** on its own, over the game: type a module's name and press `Enter`. | | `Esc` | Closes the menu. | The keys are also in Minecraft's **Options → Controls → Key Binds**, under *Myriad*, where you can change them. If you'd rather open the menu with a single key (like `Right Shift`), bind **Open Menu** there. ## Turn on a module Each window in the menu holds one addon's modules in one category, like **Combat · Essentials**. Click a module to turn it on or off; right-click it (or click the arrow) to see its settings. The quickest way is the launcher: press `Alt`+`Space`, type part of a name (`free look`), and press `Enter`. ## Where next - [The menu](https://myriadclient.dev/docs/use/menu): workspaces, windows and the keys that move them. - [Modules and settings](https://myriadclient.dev/docs/use/modules): binds, hold mode and every kind of setting. - [Control panels](https://myriadclient.dev/docs/use/control-panels): turn a whole set of modules on with one key, each set up for the job. - [Sharing your config](https://myriadclient.dev/docs/use/sharing): share your setup as a short code. --- Page: https://myriadclient.dev/docs/use/menu # The menu > Workspaces, windows, layouts and the keys that drive Myriad's tiling menu. Myriad's menu works like a tiling window manager (it's modelled on [Hyprland](https://hyprland.org) and [Omarchy](https://omarchy.org)). Instead of one big screen of buttons, you get **windows** arranged on **workspaces**, and you decide what goes where. ## Workspaces There are nine workspaces, plus one for the HUD. Switch with `Alt`+`1` … `9`, or click the numbers in the bar at the top. Use them however suits you: a combat workspace, a building workspace, one for an addon you use on one server. The bar shows workspaces 1–4 always, and 5–9 while they have windows. The icon after the numbers is the [HUD workspace](https://myriadclient.dev/docs/use/hud). ## Windows Most windows are **category windows**: one addon's modules in one category, titled like *Render · Essentials*. Addons share the standard categories (Combat, Movement, Render, Player, World, Misc), so their windows sit together. Other windows are tools: Theme, Keybinds, Profiles, Control Panels, Share, the Console, the Profiler and so on. Open any of them from the [launcher](https://myriadclient.dev/docs/use/launcher). A window can be: - **Tiled**: it takes a share of the workspace, and the others make room. Drag the gaps between windows to resize them. - **Floating**: it sits on top where you put it. Drag it by its title bar. - **Full screen**: it fills the workspace. The buttons at the right of a title bar float or close the window. Some windows add their own controls there, like a control panel's on/off switch. **Merging**: drag a window by its title bar onto another window's title bar and they become one window, with each addon as a section. The icon on a section's header moves it back out. When you install an addon, its windows open beside the windows of the same categories if there's room, or on a workspace of their own. A notification says where. Windows you've arranged are never moved, and a window you close stays closed (the launcher reopens it). ## Layouts Each workspace has a layout: - **Dwindle** (the default): each new window splits the focused one, so a workspace grows into a grid. - **Columns**: one column per window, side by side, with each module's settings unfolding inline. When they don't all fit, the row scrolls sideways. `Alt`+`L` switches the workspace's layout. ## Keys The mod key stands in for Super (Hyprland keeps Super for itself). It's `Alt` by default; change it in **Theme → Preferences**. | Keys | Action | |---|---| | `mod`+`Space` | launcher | | `mod`+`1`…`9` / `mod`+`Shift`+`1`…`9` | switch workspace / move the window there | | `mod`+`W` / `mod`+`T` / `mod`+`F` | close / float or tile / full screen | | `mod`+`J` / `mod`+`L` | change the split / change the layout | | `mod`+`Tab` / `mod`+`Shift`+`Tab` / `mod`+`Ctrl`+`Tab` | next / previous / last workspace | | `mod`+`S` / `mod`+`Shift`+`S` | the HUD workspace / move the window to the HUD | | `mod`+arrows / `mod`+`Shift`+arrows | focus / swap with the window that way | | `mod`+`-` / `mod`+`=` | shrink / grow the window | | `mod`+`Return` / `mod`+`K` | console / keybinds | | `mod`+`Shift`+`Space` / `mod`+`Shift`+`Backspace` | hide the bar / remove the gaps | | `mod`+drag with the left / right mouse button | move / resize | Every one of these can be changed in the **Keybinds** window (`mod`+`K`). ## Without a mouse Inside a window, `j`/`k` (or the arrow keys) move the selection, `h`/`l` change a value or fold a section, `Enter` or `Space` activate, `o` unfolds a module's settings, `/` jumps to the filter, and `Esc` leaves a text field or closes the menu. > **Note:** Using Omarchy? Hyprland takes `Alt`+`Tab` for itself: use `mod`+scroll to change workspace, or pick another mod key. --- Page: https://myriadclient.dev/docs/use/launcher # The launcher > Find and run anything in Myriad from the keyboard, with @addon and #kind filters. The launcher is the fastest way to do almost anything. Press `Alt`+`Space`, in game or in the menu, and start typing. It finds modules, control panels, macros, windows, categories, themes, profiles and workspaces. The search is fuzzy: `frlk` finds *Free Look*. | Key | In the launcher | |---|---| | `Enter` | run the top result: toggle a module, open a window, apply a theme | | `Shift`+`Enter` or right-click | the other action: a module's settings instead of toggling it | | `Ctrl`+`j` / `Ctrl`+`k`, or the arrows | move through the results | | `Esc` | close it | Opened over the game (not in the menu), the launcher closes once it's done what you asked, and you're back playing. Something that needs the menu (a window, settings) opens the menu for you. ## Filters Two symbols narrow the results, on their own or with text: - **`@addon`** keeps one addon's things. Any part of its name or id works: `@ess` is Essentials, `@stash` is Stash Hunter. - **`#kind`** keeps one kind, or one category: `#panel`, `#control`, `#macro`, `#theme`, `#profile`, `#render`, `#combat`, `#movement`… | Type | You get | |---|---| | `#control` | every control panel | | `@ess #render` | Essentials' render modules | | `#panel` | every window you can open | | `@stash` | everything from Stash Hunter | | `#macro kit` | macros with "kit" in the name | ## Making things Whatever you type can become something new: the last two results are **New macro: …** and **New control panel: …**, named after what you typed. See [Macros](https://myriadclient.dev/docs/use/macros) and [Control panels](https://myriadclient.dev/docs/use/control-panels). --- Page: https://myriadclient.dev/docs/use/modules # Modules and settings > Turning modules on and off, binding keys, hold mode, and the kinds of settings modules have. A **module** is one feature: Free Look, Kill Aura, Auto Eat. Addons provide them. Each sits in a category window in the menu. ## On and off Click a module to turn it on or off. A tinted bar on the left of its row means it's on. From chat, `.toggle ` does the same (`.toggle free_look on`). When a module turns on or off, a line in chat says so. Turn that off per module with its **Chat Feedback** setting. ## Settings Right-click a module, or click the arrow at the end of its row, to unfold its settings. Settings come in groups; the first is open, later ones start folded. | Kind | Looks like | |---|---| | On/off | a switch | | A number | a slider (click the value to type one) | | A choice | a dropdown | | A colour | a swatch: a fixed colour, a rainbow, or one of the theme's colours (so it changes with the theme) | | A key | a bind button: click, then press a key or mouse button | | A list | blocks, items, entities, modules or text, with a picker | | A position | `x y z`, with a **Here** button | | A file | a path, with the system file picker | Hover a setting to see what it does. Some settings appear only when they matter: a module's *Range* might only show while its *Mode* uses a range. `.set ` changes a setting from chat, and `.set ` lists them. `.reset ` puts every setting back to its default. ## Keybinds Every module has a **Keybind** (in its *Module* group, at the bottom of its settings): press it in game to turn the module on or off. Hold `Ctrl`, `Alt`, `Shift` or `Super` while you press the key to bind a combination like `Ctrl`+`T`. Mouse buttons work too. `.bind ` does it from chat: `.bind tunnel t`, `.bind hole_esp ctrl+t`, `.bind zoom mouse5`, `.bind tunnel none` to unbind. A key press runs only the closest match. With one module on `T` and another on `Ctrl`+`T`, `Ctrl`+`T` runs only the second. A combination nothing is bound to falls back to the plain key: `Shift`+`T` still runs the `T` bind, so you can sneak and toggle at once. Two things bound to the very same keys both run. This goes for every Myriad bind: modules, control panels, macros and global actions like Open Menu. - **Hold**: on while the key is held, off when you let go. Good for Zoom or Free Look. - **Show In List**: whether the Module List HUD element shows it. The **Keybinds** window (`Alt`+`K`) lists every bind in one place: modules, the menu's own keys, and global actions. > **Note:** A setting with a coloured tag beside its value is being set by something else for now, like a control panel. See [Setting layers](https://myriadclient.dev/docs/use/setting-layers). --- Page: https://myriadclient.dev/docs/use/control-panels # Control panels > Turn a set of modules on together with one key, each with settings just for that job. A **control panel** is a set of modules you turn on and off together, each with the settings it should have for one job. One key (or the switch in its title bar) turns them all on, set up the way you saved them. Turning it off turns them back off and gives you your own settings back. Some ideas: - **Highway**: Elytra Fly flying Recast, Elytra Swap keeping the elytra, Auto Eat. - **Base building**: Scaffold, Air Place, Auto Tool, with a slower Scaffold than you normally use. - **Mining trip**: Packet Mine, Auto Tool, Storage at a short range. ## Make one ### Create it In the launcher, type a name and pick **New control panel: …**. Or open the **Control Panels** window and use the field at the bottom. ### Add modules Its window opens. Open its **Settings** section and pick the modules under **Modules**. ### Save settings for it (optional) Each module is listed as a card. Unfold one and change its settings the way this panel should have them, then press **Save**. A badge on the card shows how many settings it saves. **Clear** forgets them. ### Give it a key In **Settings**, set a **Keybind**. Turn on **Hold** to have it on only while you hold the key. ## What happens when it's on - Each of its modules turns on. - The settings it saved **lie over** the module's own. Your own settings aren't changed: they're still there underneath. In the module's settings, the panel's values show with the panel's name in a tag. - If you change one of those settings while the panel is on, you're changing **the panel's** value, not yours. - Turning the panel off turns off only the modules it turned on (one that was already on stays on), and the panel's settings come off, so your own show again. So you can try a setup without losing yours. See [Setting layers](https://myriadclient.dev/docs/use/setting-layers) for how this works. ## Ready-made panels Addons can come with control panels. Essentials has **Highway** and **Cruise**. You can change them like your own; **Reset** in a panel's settings brings back the addon's version, and a deleted one comes back with the button in the Control Panels window's title bar. ## From chat | Command | | |---|---| | `.control` | list them | | `.control on ` / `off` / `toggle` | turn one on or off | > **Note:** Control panels belong to your [profile](https://myriadclient.dev/docs/use/profiles), like modules do. To give one to someone else, [share it](https://myriadclient.dev/docs/use/sharing). --- Page: https://myriadclient.dev/docs/use/setting-layers # Setting layers > How control panels and modules can set a setting for a while without changing yours. Every setting has **your value**: the one you set, and the only one Myriad saves. On top of it, other things can lay a value **for a while**, and take it off again when they're done. Your value is never touched. Think of it as layers, the top one being the one in effect: | Layer | Who sets it | Example | |---|---|---| | A module | another module that needs a setting a certain way while it works | a flight addon that needs Velocity off while it flies | | A control panel | a [control panel](https://myriadclient.dev/docs/use/control-panels) that's on | *Highway* sets Elytra Fly's Type to Recast | | A server | (coming) settings for one server | | | **You** | you | the value you set | When a layer comes off, whatever is under it shows again. That's why turning a control panel off puts your settings back: nothing was overwritten, so there's nothing to put back. It's also why a crash or a disconnect can never leave your settings changed. ## Seeing it A setting with a layer on it shows a small tag with **who set it** beside its value. Hover it to see that your own value comes back afterwards. ## Changing a layered setting If a control panel is on and you change one of its settings, you're tuning **the panel**: it saves the new value for next time, and yours stays as it was. The same goes for `.set`, which says so ("Width set to 4 (for Digging)"). A setting set by a module (not a panel) can't be changed while that module is using it. > **Note:** Only your own values are saved, and only your own values go into [shared configs](https://myriadclient.dev/docs/use/sharing). --- Page: https://myriadclient.dev/docs/use/macros # Macros > Modules you make that run commands, chat or server commands on a key. A **macro** is a module you make that runs commands. Being a module, it has a keybind, works in the launcher, and can go in control panels. ## Make one In the launcher, type a name and pick **New macro: …**, or run `.macro new `. It appears in the **Macros** category. Open its settings and fill in **Commands**, one per line: | Line | Does | |---|---| | `.toggle esp` | a Myriad command | | `/home` | a server command | | `wait 20` | waits 20 ticks (one second) | | `hello!` | sends it as chat | ## Modes - **Run**: each press runs Commands once, top to bottom. - **Toggle**: turning it on runs Commands; turning it off runs **Off Commands**. ## From chat `.macro` lists them, `.macro run ` runs one, `.macro new ` makes one, `.macro delete ` deletes one (also the **Delete Macro** button in its settings). A macro's commands are never put in a [shared config](https://myriadclient.dev/docs/use/sharing): they could hold a password or where you are. --- Page: https://myriadclient.dev/docs/use/sharing # Sharing your config > Share modules, control panels and your theme as a short code, view shares on the website, and take one in. Share your setup as a short code like `myriad_JS3k4d`. Anyone can look at it on the website, or take it into their own Myriad in a click: as their settings, or as a control panel to try without losing theirs. A share can hold any mix of: - single modules, whole windows (an addon's category), or every module of an addon; - control panels; - your theme (its colours and style; not your personal preferences); - keybinds, if you choose to include them (they're left out unless you do). ## Share ### Open the Share window From the launcher: type `share`. ### Pick what goes in Click a window's card to include all of it, or right-click to unfold it and pick single modules. Switch on control panels, the theme, and keybinds if you want them. ### Share it Give it a title and press **Share**. The code is copied for you. Send it, or its link: `https://myriadclient.dev/config/myriad_JS3k4d`. From chat: `.share module `, `.share addon `, `.share panel `, `.share theme`. ## What's never shared Some settings hold things that are yours alone: a Discord webhook link, coordinates, a file on your computer, the commands of a macro (which could include a password). Addons mark these as **private**, and a share never contains their values. Others see that the setting exists, never what it's set to. Taking a share in leaves your own private settings as they are. ## Look at one on the website Open a share's link: the website shows its modules with their settings, drawn like the menu, in the sharer's colours. Private settings show as hidden. Nothing to install to look. ## Take one in In the Share window, paste a code (or a link) under **Take one in** and press **Look**. It says what's in it, and whether it needs an addon you don't have. Then: - **Try as control panel**: makes a control panel of its modules and settings and turns it on. Your own settings are untouched underneath: turn the panel off and you're back to yours. The safest way to try someone's setup. - **Use these settings**: its settings become yours, for the modules in it (private settings excepted), and its modules are turned on or off as they were shared. Its control panels and theme are added too. From chat: `.share try ` and `.share import `. Modules from addons you don't have are skipped, and Myriad lists them. ## Your shares The bottom of the Share window lists what you've shared from this computer: copy a code again, or **Delete** a share (it's gone from the website too). `.share` lists them in chat and `.share delete ` deletes one. Only the computer that shared something can delete it. > **Note:** Shares are public to anyone with the code. Don't put anything in a title you wouldn't post. --- Page: https://myriadclient.dev/docs/use/themes # Themes > Change how the menu looks, make your own themes, and Myriad's preferences. A **theme** is the whole look of the menu: colours, borders (with gradients, even rotating ones), rounding, gaps, blur, shadows, how inactive windows dim, the animations and their speeds, the bar and the fonts. Open the **Theme** window from the launcher. ## Presets Myriad comes with **Myriad** (the logo's mint on near-black), **Tokyo Night**, **Gruvbox** and **Catppuccin Mocha**, and addons can add more. Pick one and it applies at once. Change anything in a preset and your changes are saved on top of it (it says *Edited*). **Reset** brings back the original. ## Your own **New** or **Duplicate** makes a theme you can name and give an author. Each is a file in `.minecraft/myriad/themes/`; send the file to share it, or [share it as a code](https://myriadclient.dev/docs/use/sharing). ## Theme colours The theme has a palette (accent, secondary, red, green, yellow, blue, magenta, cyan) that modules use for their colours by default. So when you change the theme, ESP, tracers, the module list and notifications follow. A colour you set on a module yourself stays yours. ## Preferences Some options are yours, not the theme's, and stay when you switch themes. They're under **Preferences** in the Theme window: - **Mod key**: `Alt`, Super or `Ctrl`. - **UI scale**, and whether the menu pauses singleplayer. - **Title Screen**: Myriad's own title screen, or Minecraft's. - **Myriad Cape**: whether you wear Myriad's cape (other Myriad players nearby see it; `.cape off` takes it off). - Vim keys in windows, and more. --- Page: https://myriadclient.dev/docs/use/hud # The HUD > Arrange what's drawn over the game: coordinates, the module list, armour and more. The HUD is what Myriad draws over the game: the watermark, the module list, coordinates, armour, totems and so on. You arrange it on the **HUD workspace**: `Alt`+`S`, or the icon after the workspace numbers in the bar. - **Move** an element by dragging it. It snaps to the edges and the centre, and sticks to the nearest third of the screen, so it stays put at any window size. - **Right-click** one for its settings: scale, colours, alignment. - The **HUD** window there lists every element (from every addon). Click one to add or remove it; unfold it for its options. `Delete` removes the element under the mouse. Essentials adds Watermark, Module List, Looking At, Coordinates, Armor, Binds, Chest Count, Direction, Effects, FPS, HP, Ping, Player Count, Speed, Totems and TPS. --- Page: https://myriadclient.dev/docs/use/profiles # Profiles > Keep separate setups per server or play style, and where Myriad keeps its files. A **profile** is a whole setup: which modules are on, their settings and binds, your control panels, and how the menu is laid out. Keep one per server, or per play style, and switch between them. Open the **Profiles** window from the launcher, or use `.profile`: | Command | | |---|---| | `.profile` | list profiles | | `.profile load ` | switch (made from your current setup if it doesn't exist) | | `.profile save ` | copy the current setup to a new profile | | `.profile delete ` | delete one | Friends, global keys and your theme preferences aren't part of a profile: they're the same everywhere. ## Your files Everything lives in `.minecraft/myriad/`: - `profiles//`: a profile (modules, the menu layout, control panels). - `themes/`: your themes. - `myriad.json`: global options and key actions. `friends.json`: friends. - `addons//`: what each addon saves for itself. Myriad saves a moment after anything changes, writes every file safely (a crash mid-save can't break it), and keeps a `.bak` of each from the start of the session. Settings of an addon you've removed are kept, so adding it back loses nothing. `.reload` reads the profile from disk again, if you've edited it by hand. --- Page: https://myriadclient.dev/docs/use/anti-cheat # Anti-cheat profile > How Myriad's modules adapt to servers running Grim (like 2b2t) or only vanilla checks. Servers check different things. On one running **Grim** (2b2t, for one), Myriad's modules rotate before acting, click only faces you can see, move along the direction they send, mine when Grim allows, and click in your inventory only while you stand still. On a server with only Minecraft's own checks, they act as fast as it lets them. **Auto** (the default) works out which a server is: Grim once the server sends the steady pings anti-cheats like it use, or at once for servers known to run it (2b2t). You can pick yourself in the **Profiles** window, or with: | Command | | |---|---| | `.anticheat` | what's in effect, and what Auto detected | | `.anticheat auto` / `grim` / `vanilla` | choose | | `.anticheat known add ` / `remove` | servers to treat as Grim from the moment you join | Essentials is made for 2b2t and stays safe on Grim whichever you pick. --- Page: https://myriadclient.dev/docs/use/hooks # What addons changed > See which addons change how Myriad itself works, and turn any of it off. Besides adding modules, an addon can change how parts of Myriad work: how rotations turn, where blocks are placed, when inventory clicks go out, how highlights look. Some replace a whole part (an addon might bring its own rotations), some only for their own modules, some only on one server. The **Hooks** window (from the launcher) shows all of it: - **Services**: the parts an addon took over, and which is in use for everyone right now. - **Hooks**: single decisions addons make instead of Myriad, and when (for a module, a server). Each is a card. **Click one to turn it off**; click again to turn it back on. If one of them ever breaks (throws an error), Myriad turns it off by itself until you restart, and the card says *failed*. From chat: `.hooks` lists them, `.hooks off ` / `.hooks on ` switch one, and `.hooks why ` says which of them affect a module. --- Page: https://myriadclient.dev/docs/use/addons # Addons and updates > Install addons, keep them up to date, and what happens when one fails. Modules come from **addons**: ordinary Fabric mods made for Myriad. Install one by putting its jar in `mods` next to Myriad's. | Addon | | |---|---| | [Myriad Essentials](https://github.com/MyriadClient/myriad-essentials) | the stock modules and HUD elements | | [Myriad Boze](https://github.com/MyriadClient/myriad-boze) | Boze's modules in Myriad's menu | | [Myriad Omarchy](https://github.com/MyriadClient/myriad-omarchy) | themes that follow your Omarchy system theme | ## The Addons window Open **Addons** from the launcher. It lists every addon with its version and links, and each addon's own settings. **Updates**: Myriad checks each addon's releases. When there's a new version, a badge shows it, and **Update** installs it (it takes effect the next time you start the game). The switch in the title bar turns checking off; the buttons beside it check now and update everything. From chat: `.update check`, `.update all`. If an addon fails to start, Myriad takes out everything it added and carries on without it. The Addons window shows why it failed. --- Page: https://myriadclient.dev/docs/use/commands # Commands > Every chat command Myriad has, from toggling modules to sharing configs. Myriad's commands start with `.` (change it with `.prefix`). They autocomplete in chat and in the Console window (`Alt`+`Return`). | Command | | |---|---| | `.toggle [on\|off]` | turn a module on or off | | `.bind ` | bind a module: `t`, `ctrl+t`, `shift+alt+f6`, `mouse5`, `rmb` | | `.set [setting] [value]` | change a setting (`.set esp colors.players #FF00FFAA` when two groups share a name) | | `.reset ` | a module's settings back to their defaults | | `.modules`, `.binds` | list modules (click to toggle) and binds | | `.control [on\|off\|toggle ]` | [control panels](https://myriadclient.dev/docs/use/control-panels) | | `.macro [run\|new\|delete ]` | [macros](https://myriadclient.dev/docs/use/macros) | | `.share …` | [shared configs](https://myriadclient.dev/docs/use/sharing): `module`, `addon`, `panel`, `theme`, `import`, `try`, `delete` | | `.profile [load\|save\|delete ]` | [profiles](https://myriadclient.dev/docs/use/profiles) | | `.theme ` | apply a theme | | `.friend add\|remove\|list` | friends (modules leave them alone) | | `.anticheat …` | [anti-cheat profile](https://myriadclient.dev/docs/use/anti-cheat) | | `.hooks [on\|off \| why ]` | [what addons changed](https://myriadclient.dev/docs/use/hooks) | | `.addons`, `.update [check\|all\|]` | addons and updates | | `.cape [on\|off]` | Myriad's cape | | `.profiler [on\|off]` | where the time goes, by module | | `.panic` | turn every module off | | `.menu` | open the menu | | `.reload` | read the profile from disk again | | `.diagnostics` | copy versions, addons and settings for a bug report | | `.say ` | send a message that starts with the prefix, as chat | | `.fakeplayer add\|remove\|clear\|list` | client-side dummy players, for testing | | `.disconnect` | leave the server | | `.help` | all of this | --- Page: https://myriadclient.dev/docs/use/essentials # Essentials > The stock modules that come with Myriad Essentials. [Myriad Essentials](https://github.com/MyriadClient/myriad-essentials) is the stock set of modules. It's an addon like any other, made for 2b2t: everything passes Grim by default. Each module's settings explain its options. | Category | Modules | |---|---| | Combat | **Offhand** (a totem, crystal, golden apple or shield, falling back to a totem when it matters), **Auto Armor**, **Auto Disconnect**, **Kill Aura** | | Movement | **Elytra Fly** (Recast for highways, Altitude for open country, Control where elytras are checked loosely), **Elytra Swap** (keeps your elytra from wearing out), **Lock Rotation** (keep flying the way you faced), **Rocket Boost**, **Auto Walk**, **Inventory Move**, **Velocity** | | Player | **Auto Eat**, **Auto Tool**, **Inventory Tweaks**, **Middle Click**, **Packet Mine**, **Reach**, **Stack Replenish**, **Wall Interact**, **X Carry** | | Render | **ESP**, **Blocks**, **Block Highlight**, **Storage**, **Tracers**, **Nametags**, **Tooltips**, **Full Bright**, **Time of Day**, **Free Look**, **Freecam**, **View Model**, **Zoom** | | World | **Air Place**, **Scaffold** | It also has two ready-made [control panels](https://myriadclient.dev/docs/use/control-panels): **Highway** (Recast, Elytra Swap, Auto Eat) and **Cruise** (Altitude, Elytra Swap, Lock Rotation, Auto Eat). Elytra Fly, Elytra Swap and Lock Rotation are separate so you can combine them: Elytra Swap works for any flight (and for flying yourself, with *When You Fly*), and Lock Rotation holds the heading for Altitude and Control. --- Page: https://myriadclient.dev/docs/use/faq # Questions > Common questions and fixes. ### The menu doesn't open Check the mod key in Minecraft's **Controls → Key Binds → Myriad**: another mod (or your system) might use `Alt`+number. Pick another mod key under **Theme → Preferences**, or bind **Open Menu** to one key. ### A module does nothing on 2b2t Check the [anti-cheat profile](https://myriadclient.dev/docs/use/anti-cheat): `.anticheat` says what's in effect. Some features (Air Place on current Grim, Elytra Fly's Control) don't work where Grim runs, and say so. ### My settings changed by themselves Something may be [layered](https://myriadclient.dev/docs/use/setting-layers) over them: a control panel that's on, or a module using it. The setting shows a tag with who. Turn that off and yours are back. ### Where are my files? In `.minecraft/myriad/`. See [Profiles](https://myriadclient.dev/docs/use/profiles#your-files). ### Reporting a bug Run `.diagnostics` (it copies what's useful to the clipboard) and open an issue on [GitHub](https://github.com/MyriadClient/myriad-core/issues). --- Page: https://myriadclient.dev/docs/develop # Developers > Build addons for Myriad. What an addon is, what the API gives you, and a complete first addon. A Myriad **addon** is an ordinary Fabric mod with a `myriad` entrypoint. Everything players see in Myriad, including the stock modules, comes from addons built on the same public API (`dev.myriadclient.api`). Myriad core itself is the menu, the settings system, the services and the glue. What the API gives you: - **Modules** with typed **settings**, binds, hold mode and HUD info, saved per profile. - An **event bus** for ticks, packets, input, rendering, the world and the UI. - **Services** so addons share one implementation of hard things: rotations (with move fix), inventory and silent swaps, placement, breaking (vanilla, packet and Grim-timed), building from blueprints, containers, tasks over ticks, the packet budget, elytra flight. - **Windows and HUD elements** drawn with an SDF canvas, a widget toolkit, and title-bar controls. - **World rendering**: immediate shapes, GPU meshes, per-chunk caches, shader highlights. - **Hooks**: replace a service with your own, or change one decision in Myriad or another addon. - **Control panel presets** and **shared configs**. ## A complete addon ```java src/main/java/com/example/addon/MyAddon.java package com.example.addon; import dev.myriadclient.api.addon.AddonContext; import dev.myriadclient.api.addon.MyriadAddon; public final class MyAddon implements MyriadAddon { @Override public void initialize(AddonContext ctx) { ctx.registerModules(new Greeter()); } } ``` ```java src/main/java/com/example/addon/Greeter.java package com.example.addon; import dev.myriadclient.api.event.Subscribe; import dev.myriadclient.api.event.events.WorldEvent; import dev.myriadclient.api.module.Categories; import dev.myriadclient.api.module.Module; import dev.myriadclient.api.setting.StringSetting; public final class Greeter extends Module { private final StringSetting message = sgGeneral.string("Message") .description("Shown when you join a world.") .defaultValue("Welcome back!") .build(); public Greeter() { super(Categories.MISC, "Greeter", "Greets you when you join a world."); } @Subscribe private void onJoin(WorldEvent.Join e) { info(message.get()); // a chat line tagged [Myriad] } } ``` ```json src/main/resources/fabric.mod.json { "schemaVersion": 1, "id": "my-addon", "version": "${version}", "name": "My Addon", "environment": "client", "entrypoints": { "myriad": ["com.example.addon.MyAddon"] }, "depends": { "myriad": ">=0.3.0", "minecraft": "~26.2" } } ``` That's an addon: the module shows in the menu under *Misc · My Addon*, has a Message setting, a keybind, hold mode, and its on/off state and settings save with the player's profile. > **Note:** Start from the [addon template](https://github.com/MyriadClient/myriad-addon-template), and read the [example addon](https://github.com/MyriadClient/myriad-core/tree/master/example-addon) for every pattern in use. ## Where to go - [Setting up](https://myriadclient.dev/docs/develop/setup) - [Modules](https://myriadclient.dev/docs/develop/modules) - [Settings](https://myriadclient.dev/docs/develop/settings) - [Services](https://myriadclient.dev/docs/develop/services) - [Hooks](https://myriadclient.dev/docs/develop/hooks) - [For AI agents](https://myriadclient.dev/docs/develop/agents) --- Page: https://myriadclient.dev/docs/develop/setup # Setting up > Create an addon project from the template, run the dev client, and build your jar. You need JDK 25 and Git. Gradle comes with the project (`./gradlew`). ### Create the project Use the [addon template](https://github.com/MyriadClient/myriad-addon-template) (GitHub's **Use this template**), or clone it. ### Rename it - the mod id `my-addon` in `src/main/resources/fabric.mod.json`, in `settings.gradle`, and in the mixin config's file name (`my-addon.mixins.json`); - the package `com.example.addon` and the class `MyAddon`; - `maven_group` and `archives_base_name` in `gradle.properties`. ### Run it ```bash ./gradlew runClient ``` Minecraft starts with Myriad, Essentials and your addon, the menu open. ### Build it ```bash ./gradlew build ``` The jar is in `build/libs/`. ## The build ```groovy build.gradle (the parts that matter) repositories { exclusiveContent { forRepositories(maven { name = "Myriad" url = "https://maven.myriadclient.dev/releases" }) filter { includeGroup "dev.myriadclient" } } } dependencies { minecraft "com.mojang:minecraft:${project.minecraft_version}" implementation "net.fabricmc:fabric-loader:${project.loader_version}" implementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" implementation "dev.myriadclient:myriad:${project.myriad_version}" // Only in the dev client, to test next to the stock modules. Don't use its classes: players can remove it. localRuntime "dev.myriadclient:myriad-essentials:${project.essentials_version}" } ``` Myriad's artifacts (`dev.myriadclient:myriad`, with sources and javadoc, and `dev.myriadclient:myriad-essentials`) are on `https://maven.myriadclient.dev/releases`. Your IDE shows the javadoc of every API class: it's written to be read. To build against Myriad changes that aren't released yet: run `./gradlew publishToMavenLocal` in a Myriad checkout and add `mavenLocal()` to `forRepositories(...)`. ## `fabric.mod.json` ```json { "schemaVersion": 1, "id": "my-addon", "version": "${version}", "name": "My Addon", "description": "What it does, in one line.", "authors": ["You"], "icon": "assets/my-addon/icon.png", "contact": { "sources": "https://github.com/you/my-addon", "issues": "https://github.com/you/my-addon/issues" }, "environment": "client", "entrypoints": { "myriad": ["com.example.addon.MyAddon"] }, "mixins": ["my-addon.mixins.json"], "depends": { "myriad": ">=0.3.0", "minecraft": "~26.2" } } ``` - `entrypoints.myriad` is your [`MyriadAddon`](https://myriadclient.dev/docs/develop/addon). - `contact.sources` pointing at your GitHub repository lets Myriad [update your addon](https://myriadclient.dev/docs/develop/publishing). - `depends.myriad` is the oldest Myriad you support (see [Compatibility](https://myriadclient.dev/docs/develop/compatibility)). ## The dev client The template's `loom.runs.client` sets Myriad's dev options as system properties. Add the ones you want: ```groovy build.gradle loom { runs { client { property("myriad.openDesktop", "true") // the menu open on start property("myriad.devConsole", "run/myriad-dev-inbox") // scripted steps (below) property("myriad.auditMixins", "true") // apply every mixin at start, so a bad target fails at once property("myriad.screenshotEvery", "10") // a screenshot every 10 s into run/screenshots programArgs("--quickPlaySingleplayer", "New World") // straight into a world } } } ``` The **dev console** reads commands from the inbox file (one per line) and logs what happens: Myriad commands, `hold forward,sprint 20`, `look `, `wait `, `screenshot `, `menu open`, `panel `, and `selftest` (turns every module on and off and reports any that throw). See [Testing](https://myriadclient.dev/docs/develop/testing). > **Warning:** Only use `dev.myriadclient.api`. Anything in `dev.myriadclient.impl` is internal and changes without notice; an addon that touches it breaks on the next release. --- Page: https://myriadclient.dev/docs/develop/addon # The addon and its context > MyriadAddon's phases and everything AddonContext registers. Your entrypoint implements `MyriadAddon`. Myriad calls its phases for every addon in turn: | Phase | When | Use it for | |---|---|---| | `registerCategories(ctx)` | first, for every addon | your own categories, so other addons can use them too | | `initialize(ctx)` | then, for every addon | everything else: modules, commands, windows, hooks | | (config load) | | Myriad loads the player's profile: your modules' settings and states come in | | `postInitialize(ctx)` | last | looking for optional integrations (other mods, other addons) | If any phase throws, your addon is marked **failed**, everything it registered is removed, and the game carries on. The Addons window shows the error. ```java public final class MyAddon implements MyriadAddon { private Category automation; @Override public void registerCategories(AddonContext ctx) { automation = ctx.registerCategory("Automation", "", SettingColor.role(SettingColor.Mode.CYAN)); } @Override public void initialize(AddonContext ctx) { // Shared state: made here, passed to whatever needs it. Not in static fields. WaypointStore waypoints = new WaypointStore(ctx.storage()); waypoints.load(); ctx.registerModules(new Waypoints(waypoints), new AutoFarm(automation)); // menu order ctx.registerCommand(new WaypointCommand(waypoints)); PanelType window = ctx.registerPanel("Waypoints", "", () -> new WaypointsPanel(waypoints)); ctx.registerHud("Nearest Waypoint", "", () -> new NearestWaypointHud(waypoints)); ctx.registerKeyAction("Open Waypoints", Keybind.NONE, () -> Myriad.ui().openPanel(window)); ctx.events().subscribe(new SessionStats()); // always-on listener } @Override public void postInitialize(AddonContext ctx) { if (FabricLoader.getInstance().isModLoaded("baritone")) ctx.logger().info("Baritone found"); } } ``` ## What the context registers | Method | Registers | |---|---| | `registerModules(Module...)` | [modules](https://myriadclient.dev/docs/develop/modules), in menu order | | `registerCategory(name, icon, color)` | a category (in `registerCategories`) | | `registerCommand(Command)` | a [chat command](https://myriadclient.dev/docs/develop/commands) | | `registerPanel(name, icon, factory)` / `registerPanel(PanelType)` | a [window](https://myriadclient.dev/docs/develop/ui) | | `registerHud(name, icon, factory[, anchor, x, y])` | a [HUD element](https://myriadclient.dev/docs/develop/ui#hud-elements) | | `registerBarWidget(BarWidget)` | something in the menu's top bar | | `registerLayout(Layout)` | a workspace layout | | `registerTheme(Theme)` | a theme preset | | `registerKeyAction(name, defaultBind, action)` | a global key, rebindable in the Keybinds window | | `registerControlPanel(ControlPreset)` | a [ready-made control panel](https://myriadclient.dev/docs/develop/control-panels) | | `hook(Hook)` / `replace(Service)` | a [hook or service replacement](https://myriadclient.dev/docs/develop/hooks) | | `settings(name)` | settings of the addon itself, shown in the Addons window, saved per profile | | `settingWidgets()` | the editor for a [custom setting type](https://myriadclient.dev/docs/develop/settings#your-own-setting-types) | | `events()` | the event bus (listeners you subscribe through it are attributed to your addon) | | `storage()` | your [own folder](https://myriadclient.dev/docs/develop/storage) for saved data | | `logger()`, `namespace()`, `id(path)` | your logger, your mod id, and an id in your namespace | Everything registered through the context is owned by your addon: shown with its name in the menu, and removed if it fails. ## Registering later An addon that bridges another mod may only be able to register its modules once that mod is up. That's allowed: the menu picks them up the next time it opens. Those modules missed the config load, so they should mirror state kept elsewhere: return `true` from `Module.isMirror()` and Myriad neither saves them nor toggles them on their keybind, and `setEnabledSilently(...)` follows the other side without chat feedback. > **Note:** Reach Myriad from anywhere with the static `Myriad` class: `Myriad.modules()`, `Myriad.rotations()`, `Myriad.ui()`, `Myriad.notifications()`… `Myriad.isReady()` says whether it has started. --- Page: https://myriadclient.dev/docs/develop/modules # Modules > Writing modules: lifecycle, events while enabled, HUD info, conflicts, renaming and migrating settings. A module extends `Module` and passes its category, name and description to the constructor. ```java public final class Sprint extends Module { private final BoolSetting omni = sgGeneral.bool("Omnidirectional") .description("Sprint in every direction, not only forward.") .build(); public Sprint() { super(Categories.MOVEMENT, "Sprint", "Sprints for you."); } @Subscribe(inGame = true) private void onTick(TickEvent.Pre e) { if (omni.get() || mc.player.input.keyPresses.forward()) mc.player.setSprinting(true); } @Override protected void onDisable() { if (inGame()) mc.player.setSprinting(false); } } ``` - The module's **id** is its namespace (your mod id) and its name in snake case: `my-addon:sprint`. - `mc` is the game client. `inGame()` is true with a world and a player. - `sgGeneral` is the first settings group; make more with `settings.group("Name")`. See [Settings](https://myriadclient.dev/docs/develop/settings). - Every module also has a **Module** group with `keybind`, `holdMode`, `visibleInList` and `chatFeedback`. ## Lifecycle | | | |---|---| | `onEnable()` / `onDisable()` | turned on / off (also when a profile loads with it on) | | `@Subscribe` methods | the module is subscribed to the event bus **only while on**, so its handlers run only then | | `whileEnabled(start, stop)` | run `start` now if on and on every enable, `stop` on every disable: for helpers you start and stop | | `isEnabled()`, `toggle()`, `enable()`, `disable()`, `setEnabled(b)` | state | Do the work in event handlers. Don't keep timers or threads of your own: use [Tasks](https://myriadclient.dev/docs/develop/services#tasks), which are cancelled when the module turns off. ## Talking to the player | | | |---|---| | `info(msg)`, `warn(msg)`, `error(msg)` | a chat line tagged with Myriad's prefix | | `hudInfo()` | text beside the module in the Module List (its mode, a count); null for none | | `Myriad.notifications().info(title, msg)` | a toast | ## Reaching a module From a mixin, another module or another addon: ```java Sprint sprint = Modules.active(Sprint.class); // the module while it's on, else null if (sprint != null) { ... } boolean on = Modules.isActive(Sprint.class); Sprint any = Modules.get(Sprint.class); // on or off (null if not registered) // A module from an addon you don't compile against: Myriad.modules().get(MyriadId.of("other-addon", "their_module")).ifPresent(Module::toggle); ``` Don't keep a static `INSTANCE`: `Modules.active` is the way, and it's null before Myriad starts. ## Mixins Keep mixins thin: the logic goes in a method on the module, which starts with `Modules.active(...)`, and the mixin calls it. ```java @Mixin(Minecraft.class) public abstract class MinecraftMixin { @Inject(method = "createTitle", at = @At("RETURN"), cancellable = true) private void myAddon$windowTitle(CallbackInfoReturnable cir) { // prefix handlers with your mod id WindowTitle m = Modules.active(WindowTitle.class); if (m != null) cir.setReturnValue(m.title.get()); } } ``` Prefer an [event](https://myriadclient.dev/docs/develop/events) to a mixin whenever one exists. For mixins into other mods, see [Compatibility](https://myriadclient.dev/docs/develop/compatibility#mixins-into-other-mods). ## Two modules that can't both be on ```java public Freecam() { super(Categories.RENDER, "Freecam", "Fly the camera around."); conflictsWith(FreeLook.class); // turning this on turns Free Look off } ``` ## Renaming a module or its settings Players' saved settings follow a rename if you say how: ```java public Tunnel() { super(Categories.WORLD, "Tunnel", "Digs a tunnel the way you face."); formerNames("Tunnel Miner"); // saved under these before: settings, bind and state carry over } @Override public int settingsVersion() { return 2; // bump when you rename, move or change the meaning of settings } @Override protected void migrateSettings(int fromVersion, SavedSettings saved) { if (fromVersion == 1) { saved.rename("General", "Size", "Width"); saved.map("General", "Mode", v -> v.getAsString().equals("Fast") ? new JsonPrimitive("Quick") : v); } } ``` `migrateSettings` runs once per version step, before old settings load. `SavedSettings` has `get`, `set`, `remove`, `rename`, `move` (to another group or setting) and `map`. Saved settings only hold values that differ from the defaults, so a missing value means "default". Settings a module doesn't know are kept on save, so going back to an older version loses nothing. > **Note:** Make a setting when the player will want to change it, or when it changes what the module does. Something that would be on almost all the time should simply be on. --- Page: https://myriadclient.dev/docs/develop/settings # Settings > Every setting type, the builder, groups, visibility, private settings, layered overrides and custom types. Settings are typed fields on a module, built with a fluent builder from a **group**. They're saved with the player's profile (only values that differ from the default), edited in the menu and with `.set`, and shared in [shared configs](https://myriadclient.dev/docs/develop/sharing). ```java public final class Nuker extends Module { private final DoubleSetting range = sgGeneral.doubleSetting("Range") .description("How far to break blocks.") .defaultValue(4.5) .range(0, 6) // the hard limits .decimals(1) .build(); private final EnumSetting mode = sgGeneral.enumSetting("Mode", Mode.ALL).build(); private final RegistryListSetting blocks = sgGeneral.blocks("Blocks") .defaultValue(Blocks.STONE, Blocks.DEEPSLATE) .visible(() -> mode.get() == Mode.LIST) // shown only when it matters .build(); private final SettingGroup sgRender = settings.group("Render"); // later groups start folded private final ColorSetting color = sgRender.color("Color") .defaultValue(SettingColor.role(SettingColor.Mode.RED)) // follows the theme .build(); public enum Mode { ALL, LIST } ... } ``` Read a value with `get()`. Settings are fields like any other: keep them `private final`, or `public final` when another class (a mixin, a [control panel preset](https://myriadclient.dev/docs/develop/control-panels)) needs them. ## Every type | Builder (on a group) | Type | Value | |---|---|---| | `bool(name)` | `BoolSetting` | `boolean` | | `intSetting(name)` | `IntSetting` | `int`; `.range(min, max)`, `.sliderRange(min, max)` | | `doubleSetting(name)` | `DoubleSetting` | `double`; `.range`, `.sliderRange`, `.decimals(n)`; `getFloat()` | | `enumSetting(name, default)` | `EnumSetting` | an enum constant (shown title-cased) | | `choice(name, () -> options)` | `ChoiceSetting` | one of a list worked out at runtime | | `string(name)` | `StringSetting` | text | | `stringList(name)` | `StringListSetting` | lines of text | | `color(name)` | `ColorSetting` | a `SettingColor`: fixed, rainbow, or a theme role; `.argb()` | | `keybind(name)` | `KeybindSetting` | a key or mouse button; `wasPressed(event)` | | `action(name, runnable)` | `ActionSetting` | a button | | `blocks`, `items`, `entityTypes`, `statusEffects`, `registryList(name, registry)` | `RegistryListSetting` | a set of registry entries, with a picker | | `item(name, default)`, `block(name, default)`, `sound(name, default)`, `registry(name, registry, default)` | `RegistrySetting` | one registry entry | | `enumSet(name, Enum.class)` | `EnumSetSetting` | any number of an enum's constants | | `intList(name)` | `IntListSetting` | whole numbers | | `modules(name)` | `ModuleListSetting` | other modules ("pause while these are on") | | `blockPos(name)` | `BlockPosSetting` | a position, with a **Here** button | | `file(name)` | `FileSetting` | a path, with the system file picker | Every builder has `.description(text)`, `.defaultValue(v)`, `.visible(() -> bool)`, `.onChanged(v -> ...)` and `.protect()`, then `.build()`, which adds the setting to the group. ## Reacting to changes ```java private final IntSetting radius = sgGeneral.intSetting("Radius").defaultValue(32).range(1, 128) .onChanged(r -> cache.invalidateAll()) // one setting .build(); public MyModule() { super(...); settings.onAnyChanged(s -> rebuildPlan()); // any of this module's settings } ``` ## Private settings: `protect()` A setting that holds something private to the player (a webhook URL, a server address, a password, coordinates) must be marked **protected**. Its value then never goes into a shared config: shares say the setting is there, never what it's set to, and taking a share in leaves the player's own value alone. ```java private final StringSetting webhook = sgGeneral.string("Webhook") .description("Your Discord webhook URL.") .protect() .build(); ``` `BlockPosSetting` and `FileSetting` are protected already (`.protect(false)` for a position that's a public place). When in doubt, protect it. See [Shared configs](https://myriadclient.dev/docs/develop/sharing). ## A bind inside a module A `KeybindSetting` that does something while the module is on (a "sub bind"): ```java private final KeybindSetting pearl = sgGeneral.keybind("Pearl Key").build(); @Subscribe private void onKey(KeyEvent e) { if (pearl.wasPressed(e)) throwPearl(); } ``` `wasPressed` ignores presses while a screen is open. A bind with modifiers needs them held; a bind without any matches whatever is held, so the player can sneak and press it. Sub binds don't take part in the closest-match rule that module and control panel binds follow (a press runs only the binds with the most modifiers that match it). `Keybind.parse` reads a bind the way the menu writes it, which is handy for commands of your own: `Keybind.parse("ctrl+t")`, `"mouse5"`, `"rmb"`, `"none"` (`Keybind.NONE`); null if it isn't a bind. `Keybind.key(GLFW.GLFW_KEY_T, GLFW.GLFW_MOD_CONTROL)` builds one in code. ## Layered values A setting has the player's value (the only one saved) and, on top, **overrides** that code lays for as long as it needs them. `get()` is the value in effect; `playerValue()` the player's own. This is how control panels work, and how your code should borrow another module's setting instead of changing it and putting it back. ```java BoolSetting knockback = (BoolSetting) velocity.settings.get("knockback").orElseThrow(); // by key, as .set takes it // While flying: lay "off" over whatever the player set. The UI shows it tagged "My Flight". knockback.override(this, Setting.Layer.MODULE, "My Flight", false); // Landed: take it off. The player's value shows again; nothing to restore. knockback.release(this); // or every override this owner laid on a module's settings: velocity.settings.releaseAll(this); ``` Layers, lowest first: `SERVER`, `CONTROL_PANEL`, `MODULE`. The highest wins; on ties, the latest. Pass an `editor` to `override(owner, layer, label, value, editor)` to receive edits the player makes while your override is on top (control panels save them); without one, edits go to the player's value. Never write another module's setting with `set(...)` to make it behave: lay an override. Writing changes what the player saved; an override can't leak. ## Settings of the addon itself Options that belong to the addon rather than a module (units, a prefix) come from `ctx.settings("Name")`: a `Settings` shown under the addon in the Addons window and saved with the profile. ```java AddonSettings units = ctx.settings("Units"); EnumSetting distance = units.group("General").enumSetting("Distance", Unit.BLOCKS).build(); ``` ## Your own setting types Extend `Setting` and register an editor widget for it: ```java public class RangeSetting extends Setting { public record Range(double min, double max) {} private final double lower, upper; public RangeSetting(String name, String description, Range defaultValue, double lower, double upper) { super(name, description, defaultValue, (Supplier) null); this.lower = lower; this.upper = upper; } @Override protected Range validate(Range v) { ... } // clamp and order @Override public JsonElement toJson() { ... } // the player's value @Override public void fromJson(JsonElement json) { ... } @Override public boolean parse(String input) { ... } // what .set takes @Override public String valueString() { ... } // how it's shown } ``` ```java // in initialize: ctx.settingWidgets().register(RangeSetting.class, s -> { HBox row = new HBox(3); row.add(new Slider(() -> s.get().min(), v -> s.set(new RangeSetting.Range(v, s.get().max())), s.lower(), s.upper(), 1)); row.add(new Slider(() -> s.get().max(), v -> s.set(new RangeSetting.Range(s.get().min(), v)), s.lower(), s.upper(), 1)); return row; }, false); // in a module: private final RangeSetting delay = sgGeneral.add(new RangeSetting("Delay", "A min-max pair.", new RangeSetting.Range(1, 3), 0, 10)); ``` > **Note:** In a custom type, read `get()` for what it does and `value` (the player's own) in `toJson`, `parse` and `valueString`, so overrides work. --- Page: https://myriadclient.dev/docs/develop/events # Events > The event bus, @Subscribe options, priorities, cancelling, and every core event. Myriad posts events for everything that happens: ticks, packets, input, rendering, the world, the UI. Handle one by annotating a method with `@Subscribe`. A module's handlers run only while it's on. ```java @Subscribe(inGame = true) // skipped unless there's a world and a player private void onTick(TickEvent.Pre e) { ... } @Subscribe(priority = Priority.HIGH) // higher runs first; any int works private void onChat(ChatReceiveEvent e) { if (e.message().getString().contains("spam")) e.cancel(); } @Subscribe(packets = ClientboundSetTimePacket.class) // only for these packets: far cheaper than instanceof private void onTime(PacketEvent.Receive e) { ... } ``` | `@Subscribe` option | | |---|---| | `priority` | order among handlers (`Priority.HIGHEST` 200 … `LOWEST` -200; `BEFORE_ACTIONS` runs before services act) | | `inGame` | skip the handler unless a world and player exist | | `packets` | on a `PacketEvent` handler: only these packet classes | | `receiveCancelled` | still receive the event after another handler cancelled it | A handler for a supertype receives its subtypes too. A handler that keeps throwing is logged against its addon and then turned off, so one broken addon can't break the game. ## Outside modules ```java // Any object's @Subscribe methods, until you unsubscribe it: ctx.events().subscribe(sessionStats); // One handler, as a lambda; keep the Subscription to drop it: Subscription sub = Myriad.events().listen(WorldEvent.Join.class, e -> joinedAt = System.currentTimeMillis()); sub.unsubscribe(); ``` ## Timing Act (click, place, break, attack, switch slots) in `TickEvent.Pre`, before the player moves, as vanilla does. Grim flags actions sent after the tick's movement packet; Myriad holds any that come later until the start of the next tick, so they're safe but a tick late. The [services](https://myriadclient.dev/docs/develop/services) act at the right time on their own. ## Core events | Event | | |---|---| | `TickEvent.Pre`, `TickEvent.Post` | every client tick (20 a second), before and after the player | | `Render2DEvent` | the HUD frame; `canvas()` draws in GUI pixels | | `Render3DEvent` | the world frame; `shapes()` draws boxes and lines (see [Drawing](https://myriadclient.dev/docs/develop/rendering)) | | `PacketEvent.Send`, `PacketEvent.Receive` | every packet, bundles split; cancel or `setPacket` to replace | | `MovementPacketsEvent` | the movement packet about to go out: rewrite position, rotation, on-ground | | `PlayerMoveEvent` | this tick's movement: change speed, fly | | `InputEvent` | the movement keys for this tick: press or release them | | `MouseLookEvent` | turning the camera: scale or cancel it | | `CollisionShapeEvent` | what a block collides as: walk on water, avoid cactus | | `TeleportEvent` | the server set your position (a teleport, a rubberband) | | `HealthEvent` | your health and food from the server, with the damage taken | | `AttackEvent` | before an attack goes out (cancellable) | | `InteractEvent.Block`, `.Item`, `.EntityTarget` | right-clicks (cancellable) | | `ItemUseEvent.Finished`, `.Stopped` | eating, drinking, drawing a bow | | `BlockBreakEvent.Start`, `.Progress`, `BlockBrokenEvent` | mining | | `BlockUpdateEvent` | the server changed a block (old and new state) | | `ChunkEvent.Loaded`, `.Unloaded` | chunks | | `EntityEvent.Added`, `.Removed`, `.TotemPopped`, `.Died` | entities | | `ContainerEvent.Opened`, `.Loaded`, `.SlotUpdated`, `.Closed` | containers (chests, your inventory) | | `ContainerScreenEvent.SlotDrawn`, `.Tooltip`, `.Click` | icons on slots, previews, taking over clicks | | `ItemTooltipEvent` | add tooltip lines | | `ChatReceiveEvent`, `ChatSendEvent` | chat in and out (cancel, change); `parsed()` says who and whether it's a whisper | | `BlockRenderEvent` | leave a block out of the chunk mesh (X-ray) | | `EntityRenderEvent.Visible`, `.Nametag`, `.Model` | hide an entity, replace its name tag, draw its model again | | `HighlightEvent.Entity`, `.Shapes` | shader outlines and glows (see [Highlights](https://myriadclient.dev/docs/develop/rendering#highlights)) | | `GlideEvent` | an elytra glide's movement this tick | | `CameraEvent`, `PlaySoundEvent` | the camera; cancel any sound | | `KeyEvent`, `MouseButtonEvent`, `MouseScrollEvent`, `CharEvent` | input | | `ScreenOpenEvent` | a screen about to open: replace or cancel it | | `WorldEvent.Join`, `.Leave`, `DisconnectEvent` | joining and leaving | | `ModuleToggleEvent` | any module turned on or off | | `WindowResizeEvent`, `GameReadyEvent`, `ShutdownEvent` | the game window and lifecycle | > **Note:** Event objects are made by Myriad (their constructors are `@ApiStatus.Internal`). Some are reused for every call (`HighlightEvent.Entity` is): don't keep one past your handler. --- Page: https://myriadclient.dev/docs/develop/commands # Commands > Chat commands with Brigadier, Myriad's argument types, and replying. A command extends `Command` and builds its syntax with [Brigadier](https://github.com/Mojang/brigadier), the library Minecraft's own commands use. It autocompletes in chat and in the Console window, and runs with Myriad's prefix (`.`). ```java public final class WaypointCommand extends Command { private final WaypointStore waypoints; public WaypointCommand(WaypointStore waypoints) { super("waypoint", "Adds, removes and lists waypoints.", "wp"); // name, description, aliases this.waypoints = waypoints; } @Override public void build(LiteralArgumentBuilder b) { b.executes(c -> { // .waypoint waypoints.all().forEach((name, pos) -> info(Component.literal(name + " ").append(Texts.coords(pos)))); return SINGLE_SUCCESS; }); b.then(literal("add").then(argument("name", StringArgumentType.word()) .then(argument("pos", Arguments.blockPos()).executes(c -> { // .waypoint add home ~ ~ ~ BlockPos pos = c.getArgument("pos", BlockPos.class); waypoints.put(StringArgumentType.getString(c, "name"), pos); return SINGLE_SUCCESS; })))); b.then(literal("remove").then(argument("name", Arguments.suggesting(waypoints::names)).executes(c -> { if (!waypoints.remove(StringArgumentType.getString(c, "name"))) { error("No waypoint by that name"); return 0; } return SINGLE_SUCCESS; }))); } } ``` Register it with `ctx.registerCommand(new WaypointCommand(waypoints))`. ## Arguments Brigadier's own (`StringArgumentType`, `IntegerArgumentType`, ...) plus Myriad's in `Arguments`: | | | |---|---| | `Arguments.blockPos()` | `x y z`, with `~` for your position | | `Arguments.item()`, `block()`, `entityType()`, `registry(registry, what)` | registry entries, autocompleted | | `Arguments.enumValue(MyEnum.class)` | an enum constant | | `Arguments.duration()` | `500ms`, `20t`, `10s`, `1m30s`, `2h`, as milliseconds (a bare number is seconds) | | `Arguments.choice(() -> list)` | one of a list (refuses anything else) | | `Arguments.suggesting(() -> list)` | anything, suggesting from a list | | `Arguments.module()` | a module by name or id | | `Arguments.player()` | a player in the tab list | ## Replying `info(text)` and `error(text)` (strings or `Component`s) send a chat line tagged with Myriad's prefix. `Texts` makes clickable chat: `Texts.command(label, ".toggle esp")`, `Texts.copy(label, value)`, `Texts.coords(pos)`, `Texts.hover(text, tooltip)`. `Myriad.chat(text, id)` sends a line that replaces the previous one with the same id (progress, status). Run another command from code with `Myriad.runCommand(".toggle esp on")`. --- Page: https://myriadclient.dev/docs/develop/ui # Windows, HUD and widgets > Panels for the menu, HUD elements, the widget toolkit, title-bar controls, bar widgets and themes. Everything in the menu is a **panel** inside a window. Panels draw on a `Canvas`: SDF rounded rectangles, gradient borders, shadows, frosted backdrops and TrueType text, in the player's theme. Icons are glyphs from the bundled Nerd Font (`""`). ## A window built from widgets Most windows extend `WidgetPanel` and build their content from widgets. Call `rebuild()` when the structure changes. ```java public final class WaypointsPanel extends WidgetPanel { private final WaypointStore waypoints; private String name = ""; public WaypointsPanel(WaypointStore waypoints) { this.waypoints = waypoints; } @Override public String title() { return "Waypoints"; } @Override protected void build(VBox content) { waypoints.all().forEach((n, pos) -> { HBox row = new HBox(4); row.addWeighted(new Label(n + " " + pos.toShortString()), 1); row.addFixed(new Button("Remove", () -> { waypoints.remove(n); rebuild(); }), 50); content.add(row); }); HBox add = new HBox(4); add.addWeighted(new TextField(() -> name).placeholder("Name").onChange(s -> name = s), 1); add.addFixed(new Button("Add here", () -> { if (mc.player != null) waypoints.put(name, mc.player.blockPosition()); rebuild(); }).accent(), 60); content.add(add); } } ``` ```java // in initialize: PanelType window = ctx.registerPanel("Waypoints", "", () -> new WaypointsPanel(waypoints)); Myriad.ui().openPanel(window); // from a key action, a command, a module ``` Widgets: `Label` (`.dim()`, `.bold()`), `Button` (`.accent()`), `Toggle`, `Slider`, `Dropdown`, `TextField` (`.placeholder`, `.onChange`, `.onSubmit`), `ColorPicker`, `KeybindButton`, `RegistryPicker`, `StringListEditor`, `Collapsible`, `HBox`/`VBox` (`addWeighted`, `addFixed`), `ScrollView`, `Spacer`, and `SettingsView.build(settings)`, which lays out a whole `Settings` like a module's. For full control, extend `Panel` and implement `render(canvas, width, height, mouseX, mouseY)` and the input methods (`mouseClicked`, `keyPressed`, ...). | Panel method | | |---|---| | `title()`, `subtitle()`, `icon()`, `tooltip()` | what the title bar and hover show | | `titleControls()` | controls in the title bar (below) | | `preferredSize(canvas)` | the size a floating window opens at | | `tick()`, `onOpen()`, `onClose()` | lifecycle | | `save(json)`, `load(json)` | state kept with the window between sessions | | `settings` | a panel's own settings, saved with the window, shown on right-click | ## Title-bar controls A window can put a switch or icon buttons in its title bar, beside float and close. Make them once: ```java private final List controls = List.of( TitleControl.toggle(() -> store.syncing(), store::setSyncing, () -> "Sync with the server"), TitleControl.button(() -> "", store::refresh, () -> "Refresh").visible(() -> store.syncing()) ); @Override public List titleControls() { return controls; } ``` ## HUD elements A HUD element is a panel drawn over the game on the HUD workspace. For lines of text, extend `TextHudPanel`: sizing, alignment, scale and the standard colour options come with it. ```java public final class PlayerCountHud extends TextHudPanel { public PlayerCountHud() { super("Player Count", "", HudStyle.Mode.TEXT); } @Override protected void lines(Lines out) { int players = preview() ? 1 : mc.getConnection() == null ? 0 : mc.getConnection().getOnlinePlayers().size(); out.add("Players", String.valueOf(players)); } } ``` ```java ctx.registerHud("Player Count", "", PlayerCountHud::new); // added from the HUD workspace ctx.registerHud("Watermark", "", Watermark::new, PanelType.Anchor.TOP_LEFT, 0, 0); // placed on a fresh install ``` `preview()` is true while there's no world (arranging the HUD from the title screen): show sample values. For drawing of your own, extend `HudPanel` and use `HudStyle` and `ItemHud`. ## The bar and themes - **Bar widgets** extend `BarWidget` (a side, an order, `width(canvas, height)`, `render(...)`, `mouseClicked(...)`). Register with `ctx.registerBarWidget(...)`. - **Themes**: `ctx.registerTheme(new Theme(ctx.id("sunset"), "Sunset", t -> { t.accent.set(SettingColor.of(0xFFF5A97F)); }))`. Unset values come from Myriad's default. `ThemePalette` builds one from a terminal palette. - **Colours** in your panels and modules should come from the theme: `Myriad.ui().theme().accent.argb()`, and for settings, `SettingColor.role(SettingColor.Mode.ACCENT)`. ## Drawing in vanilla screens `Myriad.ui().draw(guiGraphics, canvas -> ...)` draws with the canvas inside any vanilla screen or tooltip. In a `Render2DEvent` handler, use `event.canvas()`. > **Note:** `Myriad.ui()` is the desktop: `openPanel`, `find`, `windows()`, `confirm(title, message, label, onConfirm, onCancel)` for a yes/no dialog, `theme()`, `applyTheme(...)`. --- Page: https://myriadclient.dev/docs/develop/rendering # Drawing in the world > Immediate shapes, GPU meshes, per-chunk caches, shader highlights with custom fills, and labels. Three ways to draw shapes in the world, one vocabulary (`ShapeBuilder`: `box`, `blockShape`, `side`, `line`, `quad`, `circle`, in world coordinates). Pick by how often the shapes change: | Changes | Use | Cost per frame | |---|---|---| | every frame (entities, the crosshair target) | `event.shapes()` in a `Render3DEvent` handler | rebuilds and uploads every shape | | now and then (a path, a plan, a selection) | a `WorldMesh`, rebuilt when it changes | one draw call | | per chunk (ores, storage, holes) | a `ChunkCache` with a mesher | one draw per visible chunk | ## Every frame ```java @Subscribe(inGame = true) private void onRender(Render3DEvent e) { if (target == null) return; AABB box = Entities.lerpedBox(target, e.tickDelta()); Renderer3D.box(box, ColorUtil.withAlpha(color.get().argb(), 40), color.get().argb(), Renderer3D.ShapeMode.BOTH, false); } ``` ## Per chunk ```java private final ChunkCache found = ChunkCache.of(this, chunk -> { // runs when a chunk loads or changes LongArrayList out = new LongArrayList(); BlockScan.forEach(chunk, s -> s.is(Blocks.ANCIENT_DEBRIS), (pos, s) -> out.add(pos.asLong())); return out.isEmpty() ? null : out.toLongArray(); }) .range(range::get) // chunks around the player .mesh((positions, mesh) -> { // kept on the GPU until the chunk changes for (long p : positions) mesh.blockShape(BlockPos.of(p), fill.argb(), line.argb(), ShapeMode.BOTH, true); }) .build(); ``` The cache runs while its module is on and frees everything when it turns off. Chunks are worked through within a time budget each tick, nearest first (`.async()` moves long scans to the worker pool), and a chunk is recomputed only when a block in it changes (`.neighbours()` also when the chunk beside it changes). Keep the compute function to facts about the world and turn settings into looks in the mesher: a colour change then only needs `remeshAll()`; call `invalidateAll()` when a setting changes what is found. `BlockScan` skips every 16×16×16 section whose palette can't hold a match, so a search for a rare block reads almost nothing. ## Highlights Highlights are outlines traced by a shader around the exact silhouette of what's highlighted (an entity with its armour and held items, or a set of boxes), with an optional glow, fill and gradient, through walls or not. ```java private final HighlightSettings look = new HighlightSettings(settings.group("Outline")); // the standard options @Subscribe(inGame = true) private void onHighlight(HighlightEvent.Entity e) { // each entity as it's prepared for drawing if (e.entity() instanceof Player) e.highlight(look.style(), 0xFFFF5555); } @Subscribe(inGame = true) private void onHighlightShapes(HighlightEvent.Shapes e) { // once a frame: boxes and block shapes for (BlockPos pos : chests) e.block(pos, look.style(), 0xFFFFCC00); } ``` `HighlightStyle` is immutable: `HighlightStyle.OUTLINE.withGlow(6).withFill(HighlightStyle.Fill.DOTS)`. Lengths are in GUI pixels. The first listener to highlight an entity wins; touching silhouettes of the same look share one outline. Highlighting costs nothing while nothing listens. **Your own fill** is a fragment shader in your assets, importing Myriad's composite and defining `customFill`: ```java public static final HighlightStyle.Fill STRIPES = HighlightStyle.Fill.custom(Identifier.fromNamespaceAndPath("my-addon", "fill/stripes")); ``` ```glsl assets/my-addon/shaders/fill/stripes.fsh #version 330 #moj_import vec4 customFill(HighlightFill f) { float stripe = step(0.5, fract((f.local.x + f.local.y) / f.spacing)); return vec4(f.color.rgb, f.color.a * f.opacity * stripe); } ``` Create custom fills as `static final` fields of your addon class, so they exist before the game compiles shaders. ## Labels `WorldLabel` puts text and item icons on world positions. For your own, project with `Projection.toScreen(pos)` in a `Render2DEvent` handler and draw on `event.canvas()`. ## The standard looks `BoxStyle` (outline, fill, opacity, width, through walls) and `EntityGroups` (players, friends, monsters, crystals, pearls, … each with a theme colour) give ESP-like modules the same options as the stock ones. `FadeMap` fades highlights in and out. --- Page: https://myriadclient.dev/docs/develop/services # Services > Rotations, inventory, placement, breaking, building, containers, tasks, the packet budget, anti-cheat and elytra flight. Services are one shared implementation of the hard things, so addons don't fight over the same state (two modules both rotating, both switching slots) and don't each get anti-cheat details wrong. Use them instead of sending packets yourself. Their internals keep up with servers and anti-cheats from release to release; the calls stay the same. Every service is on `Myriad` (`Myriad.rotations()`, …). Calls that act on the player take an **owner** (your module): what it asked for is dropped when it turns off, and [hooks](https://myriadclient.dev/docs/develop/hooks) can tell callers apart. ## Options and the anti-cheat profile Services that act take an immutable `Options`. Start from a preset and change what you need: ```java Placement.Options opts = Placement.Options.forServer().withRange(5).withSwing(false); ``` `forServer()` follows the player's [anti-cheat profile](https://myriadclient.dev/docs/use/anti-cheat): careful where the server simulates the player (Grim), quick where it doesn't. Don't add a "Strict" setting to your module: use `forServer()`, or ask `Myriad.antiCheat().isStrict()`. ## Rotations Per-tick rotation requests, arbitrated per axis (pass `Float.NaN` for an axis you don't need), the highest priority winning. The camera isn't moved; the rotation goes to the server. ```java @Subscribe(inGame = true) private void onTick(TickEvent.Pre e) { // request in Pre so move fix covers this tick's movement Myriad.rotations().lookAt(this, target.getEyePosition(), Rotations.PRIORITY_HIGH, Rotations.Options.forServer().withTurnSpeed(turnSpeed.get())); } ``` - `Options.withTurnSpeed(degreesPerTick)` turns gradually and eases back afterwards; `withMoveFix(true)` walks along the sent yaw (what Grim expects). - `request(owner, yaw, pitch, priority, options, afterSent)`: `afterSent` runs once the rotation has gone out, the moment to act with it. - `serverYaw()`, `serverPitch()`, `isRotating()`, `rotationForAction()` (the rotation this tick's packet will carry). ## Inventory | | | |---|---| | `select(slot)`, `silentSwap(slot, action)` | switch the held slot; or do something with another slot for one action, invisibly | | `hold(owner, slot, ticks)` / `holdWeakly` / `release(owner)` | hold a slot on the server for a while (mining runs at its speed) | | `findInHotbar(pred)`, `findInInventory(pred)`, `bestInHotbar(score)`, `count(pred)` | find items | | `moveToHotbar`, `ensureInHotbar(pred, preferred)`, `pullToHotbar`, `borrow(owner, index, replaceable)` / `giveBack` | bring items to hand | | `move`, `merge`, `swapWithOffhand`, `quickMove`, `drop` | inventory clicks | | `safeToClick()`, `prepareClick()` | Grim cancels inventory clicks while you move: check, or release your keys for a tick | | `spare(owner, pred)` | ask other modules to leave some items alone (worn-out tools) | | `attackCharge()`, `serverItem()`, `serverSlot()` | what the server thinks you hold | ```java int sword = Myriad.inventory().findInHotbar(ItemInfo::isWeapon); if (sword >= 0) Myriad.inventory().silentSwap(sword, () -> Interactions.attack(target, true)); ``` ## Placement ```java Placement.Attempt a = Myriad.placement().place(this, pos, slot, Placement.Options.forServer()); if (!a.sent()) warn("Can't place there: " + a.check()); // OUT_OF_RANGE, ENTITY_IN_WAY, NOT_VISIBLE, ... a.result().thenAccept(placed -> { if (!placed) retryLater(pos); }); // once the server kept or refused it ``` Neighbour clicks, silent swap, visible faces first, rotation through `rotations()` when the options rotate. `check(pos, options)` says why not without trying. `clickTargets(pos)` and `place(owner, hit, slot, options)` for when the clicked face matters (stairs, logs, slabs). ## Breaking ```java Breaking.Attempt a = Myriad.breaking().breakBlock(this, pos, priority, Breaking.Options.forServer()); ``` Queues by priority across modules, picks and holds the fastest tool on the server, batches instant breaks, shows the crack, rotates if asked. Modes: `VANILLA` (like the attack button), `PACKET` (finishes the moment the server allows), `FAST` and `FAST_GRIM` (2b2t-style decoy starts), with `withDoubleBreak` to mine a second block meanwhile. `progress(pos)`, `isPending(pos)`, `cancel(owner)`. ## Building Describe a shape as a `Blueprint` of `Target`s; `building()` breaks and places what's in reach each tick in a working order. ```java Blueprint wall = Blueprint.box(from, to, Target.block(Blocks.OBSIDIAN)); Build build = Myriad.building().start(this, wall, Building.Options.forServer().withKeepUp(true)); build.finished().thenAccept(done -> info(done ? "Built" : "Stopped")); ``` `Target.air()`, `solid()`, `block(b)`, `anyOf(...)`, `state(blockState)` (orientation included). `Blueprint.of(map)`, `box`, `dynamic(supplier)`. `Build.steps()` says what's happening at each position or why it's stuck (`NO_ITEM`, `FLUID`, `WRONG_ANGLE`, ...); `keepUp` maintains the shape (surround). ## Containers ```java Myriad.containers().open(chestPos, 40).thenAccept(view -> { int slot = view.find(s -> s.is(Items.GOLDEN_APPLE)); if (slot >= 0) view.quickMove(slot); Myriad.containers().close(); }); ``` ## Tasks Work over ticks, cancelled when the owner (your module) turns off: ```java Myriad.tasks().later(this, 20, () -> info("A second later")); Myriad.tasks().every(this, 5, this::scan); Myriad.tasks().sequence(this) .run(this::openChest) .waitUntil(() -> Myriad.containers().current().isPresent(), 40) .await(() -> Myriad.inventory().refresh(), 40) // a CompletableFuture to wait for .require(this::hasRoom, "Inventory full") .retry(2) .onFail(t -> warn(t.getMessage())) .start(); ``` ## The rest | | | |---|---| | `Myriad.limits()` | the shared packet budget: `canSend(kind, n)`, and `urgent(action)` for must-happen actions (totems) | | `Myriad.antiCheat()` | `profile()`, `isStrict()`, what Auto detected | | `Myriad.tickSpeed()` | client tick speed per owner (`set(owner, multiplier)`), multiplied together, cleared on disable | | `Myriad.server()` | TPS, ping, address, `isLagging(ms)`, `rubberbandedWithin(ms)`, `reconnect()` | | `Myriad.elytra()` | the elytra engine: `noDurability(owner)`, `chestSwap()`, `glideHold()`, `addPilot(...)`; `GlideEvent` | | `Myriad.friends()` | `isFriend(player)` | | `Myriad.controls()` | control panels: list, switch, create | | `Myriad.shares()` | [shared configs](https://myriadclient.dev/docs/develop/sharing) | | `Myriad.hooks()` | [hooks](https://myriadclient.dev/docs/develop/hooks) in effect | ## Helpers Static helpers so addons don't each re-derive the same things (all under `dev.myriadclient.api`): | | | |---|---| | `combat.Targets`, `combat.TargetSettings` | finding entities with shared rules (hostile, friends, invisible, walls), sorted | | `combat.Damage`, `combat.Threats`, `combat.Crystals`, `combat.Trajectory` | explosion and melee damage, what could kill you, crystal spots, projectile paths | | `world.Holes`, `world.ChunkCache`, `world.BlockScan` | holes and surrounds; per-chunk work; fast block search | | `util.Entities`, `util.BlockInfo`, `util.ItemInfo` | facts about entities, blocks and items | | `util.Reach`, `util.Interactions`, `util.Packets`, `util.Mining`, `util.Movement` | reach and line of sight; attacking, using, swinging; sending packets; mining speed; movement keys and speed | | `util.Positions`, `util.MathUtil`, `util.Slots` | spheres and boxes of positions; angles and vectors; slot indexes | | `util.Timer`, `util.RateCounter`, `util.Format`, `util.Texts`, `util.Ticks`, `util.TickCached` | timing and formatting; a value worked out once per tick | | `util.Async`, `util.Http` | the worker pool (and back to the render thread); GET/POST/DELETE with JSON | | `util.Baritone` | drive Baritone when it's installed, without depending on it | | `util.FakePlayers` | client-side dummy players for testing | > **Note:** Service interfaces are `@ApiStatus.NonExtendable`: don't implement them. To bring your own rotations, inventory, placement or breaking, [replace the service](https://myriadclient.dev/docs/develop/hooks#replacing-a-service) through its `Forwarding` class. --- Page: https://myriadclient.dev/docs/develop/hooks # Hooks > Change how Myriad (or another addon) works: replace a whole service, or handle one decision, for everyone, your own modules, or one server. Modules add features. **Hooks** change how existing ones work, without changing their code. They're how an addon brings its own rotations, makes Kill Aura pick targets differently, or makes Elytra Swap behave differently on one server, and how Myriad stays open to ideas (and bypasses) its authors don't have. There are two levels: | | What it is | You write | |---|---|---| | **Service replacement** | Take over a whole core service: rotations, inventory, placement or breaking. | a class extending the service's `Forwarding` class | | **Hook handler** | Give the answer to one decision inside a service or module: how a rotation turns, where a placement clicks, which entity Kill Aura attacks. | a lambda | Both are registered with a builder on your `AddonContext`, both say **when** they apply with a `Match` (a module, an addon, a server, an anti-cheat profile), and both show in the player's **Hooks** window, where any of them can be turned off. ## Which tool Before reaching for a hook, check it's the right tool: | You want to… | Use | |---|---| | react to something, or adjust it alongside everyone else (cancel a packet, add a tooltip line) | an [event](https://myriadclient.dev/docs/develop/events) | | make another module's **setting** take a value while your code needs it | a [layered override](https://myriadclient.dev/docs/develop/settings#layered-values) | | give **the** answer to a decision Myriad or an addon makes, where the most specific answer should win | a **hook handler** | | bring your **own implementation** of rotations, inventory, placement or breaking | a **service replacement** | | let **other addons** change a decision in **your** module | **declare a hook** | Events are broadcast: everyone sees them, in priority order, and there's no single answer. Hooks are asked: there's one chain of contributions, most specific first, and one answer comes back. > **Warning:** Don't use a hook to do what a setting or an override already does, and don't declare hooks for every method of your module. A hook is for a decision someone else has a real reason to make differently, per module or per server. ## Match: when it applies ```java // Every call (the default). Match.always() // Calls for one module, by id; or for instances of a class. Match.caller("myriad-essentials:recast") Match.caller(MyAura.class) // Calls for any module of one addon. Match.addon("my-addon") // Only while that anti-cheat profile is in effect; only on that server (and its subdomains). Match.always().on(AntiCheat.Profile.GRIM) Match.always().server("6b6t.org") // Combined. Match.caller("myriad-essentials:recast").server("2b2t.org").on(AntiCheat.Profile.GRIM) ``` The **caller** is the object a call is for: usually the module asking (the `owner` passed to a service, or the `caller` passed to `Hook.call`). It's matched by its id when it has one (`Module`s do, and so do Elytra Fly's types: `myriad-essentials:recast`), by its class, or by the addon its id or class belongs to. When several contributions match, the **most specific runs first**: | Part | Weight | |---|---| | a caller (id or class) | 8 | | an addon | 4 | | a server | 2 | | a profile | 1 | Equally specific ones run in the order their addons loaded. ## Replacing a service A replacement takes over `Myriad.rotations()`, `inventory()`, `placement()` or `breaking()`. Extend the service's `Forwarding` class: it hands every call you don't override to the service it wraps (core's), so you write only what you do differently, and methods later versions add keep working. ```java public final class HumanisedRotations extends Rotations.Forwarding { private final Random random = new Random(); public HumanisedRotations(Rotations core) { super(core); } @Override public void request(Object owner, float yaw, float pitch, int priority, Options options, Runnable afterSent) { // Change what the caller asked for, then hand it on to core, which still arbitrates between everyone. float jitter = (float) random.nextGaussian() * 0.3f; super.request(owner, yaw + jitter, Float.isNaN(pitch) ? pitch : pitch + jitter / 2, priority, options.withTurnSpeed(Math.max(options.turnSpeed(), 25)), afterSent); } } ``` ```java // in initialize: ctx.replace(Service.ROTATIONS) .named("Humanised rotations") // what the player sees in the Hooks window .when(Match.addon(ctx.namespace()).on(AntiCheat.Profile.GRIM)) .with(HumanisedRotations::new); // gets core's, returns yours ``` ### For whom: everyone, or your own modules - **Without a caller in the `Match`** (`Match.always()`, or only a server/profile), the replacement is **for everyone**: every module of every addon, and core's own services, use it. - **With a caller or an addon** (`Match.addon("my-addon")`), it's **only for those callers**. Your modules use yours, while Essentials and everyone else keep core's (or the one for everyone). Nothing conflicts. `Myriad.rotations()` and the others route each call to the right implementation. Calls that say who they're for (the methods taking an `owner`: `request`, `place`, `breakBlock`, `hold`, `borrow`, ...) go to the implementation for that owner. Calls that don't (`serverYaw()`, `serverSlot()`, `safeToClick()`, ...) go to the one for everyone. > **Warning:** There's still one player, one movement packet and one selected slot. A replacement **scoped to some callers** should change what they ask for and hand it on (`super.request(...)`) to core's, which arbitrates between everyone. Only a replacement **for everyone** should take over sending things itself; two that both send rotations would fight over the packet. ### Taking over completely A replacement for everyone can do everything itself. It's subscribed to the event bus while it's in use (its `@Subscribe` methods run only then) and hears `activated()` and `deactivated()`: ```java public final class MyRotations extends Rotations.Forwarding { private float yaw = Float.NaN, pitch = Float.NaN; public MyRotations(Rotations core) { super(core); } @Override public void request(Object owner, float yaw, float pitch, int priority, Options options, Runnable afterSent) { this.yaw = yaw; // your own arbitration, smoothing, timing... this.pitch = pitch; } @Subscribe(priority = Priority.LOWEST) // after core's own (LOW), so what you write goes out private void onMovement(MovementPacketsEvent e) { if (!Float.isNaN(yaw)) e.yaw = yaw; if (!Float.isNaN(pitch)) e.pitch = pitch; yaw = pitch = Float.NaN; } @Override public float serverYaw() { ... } // answer what you now own @Override public void deactivated() { // the player turned it off, or left the server it matched yaw = pitch = Float.NaN; } } ``` The factory runs once, the first time the replacement is needed; the instance is kept. A replacement whose `Match` names a server starts when the player joins it and stops when they leave. ## Handling a hook A hook is one decision. You get a `HookCall` with the input, who it's for, and where, and return the answer, or ask the next handler with `proceed()`. ```java ctx.hook(Rotations.TURN) .named("Eased turns") .when(Match.addon(ctx.namespace())) .handle(call -> { Rotations.Turn t = call.input(); // from, to, speed (0 = snap), yaw or pitch if (t.speed() <= 0) return call.proceed(); // snapping: leave it to the next handler / core float delta = t.yaw() ? Mth.wrapDegrees(t.to() - t.from()) : t.to() - t.from(); return t.from() + delta * 0.35f; // ease a third of the way each tick }); ``` | `HookCall` | | |---|---| | `input()` | what the hook was called with (an immutable record) | | `caller()` / `callerAddon()` / `owner()` | who the call is for: its id, its addon, the object | | `profile()`, `server()` | the anti-cheat profile in effect, the server's host | | `proceed()` | the next handler's answer (a less specific one, or in the end the default) | | `proceed(input)` | the same, with a different input | Handlers stack: a general one and a caller-specific one both run, the specific one first, each able to `proceed()` to the other. Return without calling `proceed()` to take over the decision. ### Core's hooks | Hook | Input → answer | Caller | |---|---|---| | `Rotations.TURN` | `Turn(from, to, speed, yaw)` → the angle to send this tick | the request's owner | | `Placement.AIM` | `Target(pos, options)` → the `BlockHitResult` to click, or null for nowhere | the placement's owner | | `Inventory.CLICK_READY` | `ClickMoment(sprinting, keys, sneaking)` → whether an inventory click may go out now | none | | `HighlightEvent.ENTITY_HIGHLIGHT` | `EntityHighlight(entity, style, color)` → how it's highlighted (null style: not at all) | none | ```java // Never click the top face on 6b6t (say its anti-cheat dislikes it): fall back to core's choice otherwise. ctx.hook(Placement.AIM) .named("No top faces") .when(Match.always().server("6b6t.org")) .handle(call -> { BlockHitResult hit = call.proceed(); return hit != null && hit.getDirection() == Direction.UP ? null : hit; }); ``` ```java // A server that allows clicks while sprinting (but not with keys held). ctx.hook(Inventory.CLICK_READY) .named("Sprint clicks") .when(Match.always().server("example.net")) .handle(call -> !call.input().keys()); ``` ```java // Friends in green, whatever module highlighted them; keep the alpha (it fades highlights in and out). ctx.hook(HighlightEvent.ENTITY_HIGHLIGHT).named("Friends in green").handle(call -> { HighlightEvent.EntityHighlight h = call.proceed(); boolean friend = h.style() != null && h.entity() instanceof Player p && Myriad.friends().isFriend(p); int green = SettingColor.role(SettingColor.Mode.GREEN).argb() & 0xFFFFFF; return friend ? new HighlightEvent.EntityHighlight(h.entity(), h.style(), h.color() & 0xFF000000 | green) : h; }); ``` ## Declaring your own hook When your module makes a decision other addons have a reason to make differently, declare a hook for it. Other addons then [compile against your addon](#hooking-another-addon) and handle it. ```java public class AutoFarm extends Module { /** What a harvest decision knows. Immutable, everything a handler needs. */ public record Crop(BlockPos pos, BlockState state, int age, int maxAge) {} /** Whether to harvest a crop now. Default: when it's fully grown. Caller: Auto Farm. */ public static final Hook HARVEST = Hook.of("my-addon:harvest", "Harvest", "Whether Auto Farm harvests a crop now."); private boolean shouldHarvest(BlockPos pos, BlockState state, int age, int maxAge) { return HARVEST.call(this, new Crop(pos, state, age, maxAge), c -> c.age() >= c.maxAge()); } } ``` - `Hook.of(id, name, description)`: the id is in your namespace; name and description show in the Hooks window. - `call(caller, input, fallback)`: `caller` is who it's for (pass `this`); `fallback` is your default, and what the last handler's `proceed()` gets. - Declare hooks as `public static final` constants in a public class, so other addons can compile against them. - Make the input a **record** with everything a handler needs, so you can add fields without breaking handlers. - Give the callers that matter **ids**: a module has one; a helper object can implement `Identified`, the way Elytra Fly's types do, so `Match.caller("my-addon:fast_mode")` can target it. ### Performance An unhooked hook costs a map lookup: `call` runs your fallback straight away. When something handles it, the chain for each caller and place is worked out once and cached. To skip building an expensive input when nothing listens: ```java if (!HARVEST.hooked()) return age >= maxAge; // no record, no list, nothing ``` Hooks called on the render thread every frame (like the highlight hook) must be quick, in your handlers too. ## Hooking another addon To handle a hook declared by another addon (or Essentials), compile against it and register only when it's installed. Keep the code that touches its classes in a class of its own, so it never loads without the addon: ```groovy build.gradle dependencies { compileOnly "dev.myriadclient:myriad-essentials:${project.essentials_version}" } ``` ```java // MyAddon.initialize: if (FabricLoader.getInstance().isModLoaded("myriad-essentials")) EssentialsHooks.register(ctx); // EssentialsHooks.java: the only class that touches Essentials. final class EssentialsHooks { static void register(AddonContext ctx) { ctx.hook(ElytraSwap.BEHAVIOUR) .named("Recast keeps its elytra on 6b6t") .when(Match.caller("myriad-essentials:recast").server("6b6t.org")) .handle(call -> call.input().withSwaps(false)); } } ``` Essentials' hooks are listed in [Building on Essentials](https://myriadclient.dev/docs/develop/essentials). ## When things go wrong - A handler or replacement that **throws** is turned off for the session, the error is logged against your addon, the player gets a notification, and the call goes on to the next handler (or core's own). One broken hook never breaks the game. - The player can **turn off** any contribution in the Hooks window, or with `.hooks off `. - `.hooks` lists what's in effect, and `.hooks why ` shows which hooks and replacements a module's calls go through, in order. Use them while you test. ## Checklist - Is it a decision (one answer), not a reaction? Otherwise use an event. - Is the `Match` as narrow as it can be? Prefer your own addon or caller to everyone. - Does a scoped replacement hand on to core rather than sending things itself? - Does your handler `proceed()` when it has nothing to say? - Is it cheap on hot paths, and does your declared hook check `hooked()` before building costly input? - Is the `named(...)` label something a player understands in the Hooks window? --- Page: https://myriadclient.dev/docs/develop/control-panels # Control panel presets > Ship ready-made control panels with your addon, with typed settings for your own modules. A [control panel](https://myriadclient.dev/docs/use/control-panels) switches a set of modules together, each with settings it lays over the module's own while it's on. Your addon can ship ready-made ones; players find them in their Control Panels, can edit them (their edits are kept on top), delete them, and reset them back to yours. ```java // in initialize, after registering the modules it names: Tunnel tunnel = new Tunnel(); AutoTool autoTool = new AutoTool(); ctx.registerModules(tunnel, autoTool, holeEsp); ctx.registerControlPanel(ControlPreset.builder("Digging") .module(tunnel, m -> m.set(tunnel.width, 3).set(tunnel.height, 3)) // typed: checked by the compiler .module(autoTool) .module("myriad-essentials:packet_mine", m -> m.set("mode", "Fast")) // another addon's module: by id, as .set takes it .keybind(Keybind.key(GLFW.GLFW_KEY_G)) .build()); ``` - `module(Module)` and `module(Module, settings -> ...)` take your own modules, registered already. Settings are typed: `m.set(tunnel.width, 3)` takes an `IntSetting` and an `int`. For that the setting has to be reachable: make it `public final` on the module. - `module(String id)` and `module(String id, settings -> ...)` take any module by id, installed or not, with settings written as `.set` takes them (`m.set("key", "value")`). - `keybind(...)` is its default bind (none unless given). The panel's id is `:`. `Myriad.controls()` lists, switches and creates control panels at runtime: `all()`, `byName(name)`, `get(id)`, `create(name)`, `panel.setActive(true)`. --- Page: https://myriadclient.dev/docs/develop/sharing # Shared configs > Keep private settings out of shares with protect(), and build, publish and apply shares from code. Players [share their setup](https://myriadclient.dev/docs/use/sharing) as a code like `myriad_JS3k4d`: modules with their settings, control panels, the theme. As an addon author, you have one job: **mark private settings**. The API also lets you build and take in shares from code. ## Private settings: `protect()` Any setting whose value is private to the player has to be protected. Its value then never leaves their computer in a share: the share (and the website) only says the setting exists. Taking a share in never changes it either. ```java private final StringSetting webhook = sgGeneral.string("Webhook") .description("Discord webhook for finds.") .protect() .build(); private final IntSetting centerX = sgGeneral.intSetting("Center X").range(-30_000_000, 30_000_000) .protect() // coordinates in plain numbers: protect them too .build(); ``` Protect anything that is, or could be: - a URL with a token in it (webhooks, API keys), a password, an account name; - coordinates, a server address, a player's name the player chose to track; - a file path (it can name the player's home folder); - free text the player types that could hold any of these. `BlockPosSetting` and `FileSetting` are protected unless you call `.protect(false)`. Myriad's macro commands are protected. **When in doubt, protect it**: a player can always share a value by hand, but a leaked one can't be taken back. > **Warning:** Protection is decided by your code, not the player. Shares are public to anyone with the code. ## Building a share `Myriad.shares().builder()` puts one together, from any mix of things: ```java ConfigShare share = Myriad.shares().builder() .title("Highway setup") .module(elytraFly) // one module .addonCategory("myriad-essentials", Categories.MOVEMENT) // one of the menu's windows .addon("my-addon") // every module of an addon .controlPanel(highway) // a control panel, with what it saves .theme() // the active theme's look .binds(false) // keybinds stay out unless true .build(); ``` Then: ```java Myriad.shares().publish(share).thenAccept(p -> Async.onRenderThread(() -> Myriad.notifications().info("Shared", p.code() + " (" + p.url() + ")"))); Myriad.shares().fetch("myriad_JS3k4d").thenAccept(s -> Async.onRenderThread(() -> { ConfigShares.Applied result = Myriad.shares().apply(s, ConfigShares.Apply.AS_CONTROL_PANEL); if (!result.missing().isEmpty()) Myriad.notifications().warn("Missing", String.join(", ", result.missing())); })); ``` | | | |---|---| | `publish(share)` | uploads it; the code arrives with the future. Kept, with the key that deletes it, in `published()` | | `fetch(codeOrLink)` | downloads a share | | `apply(share, Apply.REPLACE)` | its settings become the player's (protected ones excepted), modules on or off as shared | | `apply(share, Apply.AS_CONTROL_PANEL)` | a new control panel of its modules and settings, layered over the player's | | `unpublish(code)` | deletes a share published from this computer | | `ConfigShares.isCode(text)` | whether text is a share code | Futures complete off the render thread: hop back with `Async.onRenderThread` before touching the game. ## The format A share is JSON (`ConfigShare.FORMAT` 1). Each module carries its settings (only values that differ from the defaults, no protected ones) and a `view`: how it looked in the sharer's menu (name, category with icon and colour, each setting's kind and shown value), so the website can draw it without knowing your addon. ```json { "format": 1, "title": "Highway setup", "made": { "core": "0.3.4", "minecraft": "26.2" }, "addons": [{ "id": "myriad-essentials", "name": "Myriad Essentials", "version": "0.3.2" }], "modules": [{ "id": "myriad-essentials:elytra_fly", "enabled": true, "settings": { "general": { "type": "Recast" } }, "view": { "name": "Elytra Fly", "category": { "name": "Movement", "icon": "", "color": "#89B4FAFF" }, "groups": [{ "name": "General", "settings": [ { "name": "Type", "kind": "choice", "shown": "Recast", "options": ["Recast", "Altitude"] }, { "name": "Webhook", "kind": "text", "protected": true } ] }] } }], "controlPanels": [{ "name": "Highway", "members": ["myriad-essentials:elytra_fly"], "settings": { } }], "theme": { "name": "Myriad", "settings": { } } } ``` Shares live on Myriad's API (`https://api.myriadclient.dev/v1/configs`), which also refuses to store a value for any setting marked protected, and the website shows them at `https://myriadclient.dev/config/`. --- Page: https://myriadclient.dev/docs/develop/storage # Storage and config > Your addon's own files, versioned data that upgrades itself, and how Myriad saves profiles. Settings save themselves with the player's profile. For anything else your addon keeps (waypoints, a list of finds, a cache), use **your own folder**: `ctx.storage()`, at `.minecraft/myriad/addons//`. ```java public final class WaypointStore { private static final String FILE = "waypoints.json"; private static final int VERSION = 2; private final AddonStorage storage; private final Map waypoints = new LinkedHashMap<>(); public WaypointStore(AddonStorage storage) { this.storage = storage; } public void load() { storage.readJson(FILE, VERSION, WaypointStore::upgrade).ifPresent(json -> { for (var e : json.getAsJsonObject().entrySet()) waypoints.put(e.getKey(), BlockPos.of(e.getValue().getAsLong())); }); } public void save() { JsonObject o = new JsonObject(); waypoints.forEach((name, pos) -> o.addProperty(name, pos.asLong())); storage.writeJson(FILE, o, VERSION); } /** Older data, one version step at a time. */ private static JsonElement upgrade(int from, JsonElement data) { if (from == 1) { /* version 1 kept a list: turn it into a map */ } return data; } } ``` | `AddonStorage` | | |---|---| | `readJson(name)`, `writeJson(name, json)` | plain JSON files | | `writeJson(name, json, version)`, `readJson(name, version, upgrade)` | versioned: older data is brought up to date step by step on read | | `directory()` | the folder, for files of your own format | Writes are atomic (a crash mid-write can't corrupt the file), and the first write each session keeps a `.bak`. ## How Myriad saves - Module settings and states, control panels and the menu layout belong to the active **profile** (`profiles//`). Your module's settings switch with it, for free. - Only values that differ from the defaults are saved, so a better default in an update reaches everyone who never changed it. - Saved settings a module doesn't know (from a newer or older version) are kept on save; entries of addons that aren't installed are kept too. - `Myriad.config()` is the config manager: `activeProfile()`, `profiles()`, `switchProfile(name)`, `markDirty()` (save soon), `saveNow()`. --- Page: https://myriadclient.dev/docs/develop/compatibility # Compatibility and versions > The API contract, versioning, supporting several Myriad versions, optional integrations and mixins into other mods. An addon built against one version of Myriad keeps working on the next. What that takes: ## The contract - **`dev.myriadclient.api` is the API.** Everything in `dev.myriadclient.impl` is internal and changes without notice. Myriad's own addons fail their build if they touch it; yours should never import it. - **Services change underneath, not at the surface.** How `placement()` clicks or `breaking()` times a break changes to keep up with servers and anti-cheats; your calls stay the same. - **Options come from presets.** `Placement.Options`, `Breaking.Options`, `Rotations.Options`, `Building.Options` are immutable classes: start from `forServer()`, `DEFAULT`, `STRICT`… and change what you need with `with...`. New options then start from the preset's value without breaking your code. - **`@ApiStatus.NonExtendable` interfaces** (every service, `AddonContext`, `Canvas`, `Window`, `Hooks`, ...) are implemented by Myriad only and gain methods in minor releases: don't implement them. To bring your own service, use its [`Forwarding` class](https://myriadclient.dev/docs/develop/hooks#replacing-a-service). Interfaces you implement (`MyriadAddon`, `Layout`, `Blueprint`, `Replacement`, `HookHandler`) only gain `default` methods. - **`@ApiStatus.Internal`** marks what's public for Myriad's own use (event constructors, lifecycle calls). Don't call it. - **Removals are announced**: `@Deprecated` for at least one minor release first, with the replacement in the javadoc. ## Versions Myriad follows semantic versioning for the API: while the major version is 0, a **minor** release may change the API (with deprecations first), a **patch** release never does. Each repository's `CHANGELOG.md` says what changed. Declare the oldest Myriad you support in `fabric.mod.json`: ```json "depends": { "myriad": ">=0.3.0" } ``` To use something newer while still supporting older versions: ```java if (Myriad.isAtLeast("0.4.0")) { useTheNewThing(); } ``` Keep code that names a newer API class in a class of its own, called only after the check, so older versions never load it. ## Optional integrations For another mod (Baritone, Litematica) or another addon you don't depend on: ```java @Override public void postInitialize(AddonContext ctx) { if (FabricLoader.getInstance().isModLoaded("litematica")) LitematicaBridge.register(ctx); } ``` `compileOnly` the other mod, and keep every class that touches it behind the `isModLoaded` check. For Baritone, `util.Baritone` already does this: every call is a no-op without it. Don't depend on Essentials: players can remove it. Everything general it used to keep to itself is in the core API. ## Mixins into other mods Set the plugin in your mixin config and put mixins into another mod under `compat.`; they apply only when that mod is installed (`compat.no_` only when it isn't): ```json my-addon.mixins.json { "required": true, "package": "com.example.addon.mixin", "plugin": "dev.myriadclient.api.mixin.CompatMixinPlugin", "compatibilityLevel": "JAVA_25", "client": ["MinecraftMixin", "compat.sodium.SodiumWorldRendererMixin"] } ``` > **Note:** Run the dev client with `myriad.auditMixins` on: every mixin applies at start, so a wrong target fails at once instead of the first time its class loads. --- Page: https://myriadclient.dev/docs/develop/performance # Performance > Keep your addon cheap: work per tick, draw per frame, cache per chunk, and measure with the Profiler. Myriad is built so a module costs nothing until it does something, and the **Profiler** window (or `.profiler on`) shows what each module, service and listener costs, in milliseconds per second. To keep an addon cheap: - **Work per tick, draw per frame.** A render handler runs a few hundred times a second; the world changes twenty. Put what you find (targets, colours, paths) in a `TickCached` and read it from the render handler. ```java private final TickCached> targets = TickCached.of(() -> targetSettings.query().range(range.get()).list()); @Subscribe(inGame = true) private void onRender(Render3DEvent e) { for (Entity t : targets.get()) draw(e, t); // worked out once per tick, however many frames read it } ``` - **Per chunk, not per tick.** Anything about blocks goes in a `ChunkCache` (redone only when a block in the chunk changes) with a mesher, so a frame is one draw per chunk. See [Drawing](https://myriadclient.dev/docs/develop/rendering#per-chunk). - **Say what you listen for.** `@Subscribe(inGame = true)`, `packets = {...}` on packet handlers, and no handler at all for something you need now and then (`Myriad.events().listen` returns a `Subscription` to drop). - **Cache what the game recomputes.** `Threats.worst()`, `Inventory.attackCharge()` and friend lookups are cached per tick already; `Targets.query().list()` isn't. - **Hooks**: check `hooked()` before building an expensive input for a hook you declare; keep render-thread handlers quick. - **Measure.** Anything over a millisecond per second in a tick handler, or a few in a render handler, is worth a look. --- Page: https://myriadclient.dev/docs/develop/testing # Testing > The dev client, the scripted dev console, selftest, fake players, and checking against Grim. ## The dev client `./gradlew runClient` starts Minecraft with Myriad, Essentials and your addon. [Setting up](https://myriadclient.dev/docs/develop/setup#the-dev-client) lists the system properties that open the menu, go straight into a world, take screenshots and audit mixins. ## The dev console With `myriad.devConsole` set, the client reads commands from that file, one per line, and logs each as it runs. A script (or an agent) can drive the game without a keyboard: | Line | | |---|---| | `.toggle my_module on`, `/gamemode creative` | Myriad and server commands | | `hold forward,sprint 40`, `release` | hold movement keys for ticks | | `look `, `select <1-9>`, `attack`, `use` | aim and act | | `wait `, `status` | wait; log position, motion, rotation, health, held item, FPS | | `screenshot ` | a screenshot into `run/screenshots/.png` | | `menu open`, `workspace `, `panel `, `launcher [query]`, `settings ` | drive the menu | | `mouse