What reviewers check in a Rust plugin
On this page
A plugin that follows the framework's own documentation is easy to review and kind to a server. This is the list we read a plugin against, in plain English: the rule, why it matters, and the page it comes from. There are 43 rules in 6 groups.
Each rule is tagged. In the framework docs means a framework or Microsoft page states it. Our house rule means it is our own rule, learned on real servers, and the linked page is the background to it. Where the Oxide and Carbon documents disagree with each other we leave the point out rather than pick a side.
Part of this is covered by the directory's automated checks (how the checks work): they list the hooks a file registers, flag repeating timers under one second, scans of every entity and Harmony patches, and compile each file on the current Rust build. The rest takes a person reading the code. A clean check means a program looked, not that anyone endorses the plugin.
Performance
A Rust server has one main thread and every plugin shares it. These rules keep a plugin from costing frames.
-
Typed hooks, cheapest test first. Declare a hook with the exact parameter type you care about, and leave early on the cheapest test. Our house rule
Why: On Carbon only the matching overload is called, and hooks such as entity damage run for almost every entity, so each extra step is paid thousands of times.
-
One timer per system. Run one repeating timer for each feature and loop over its players or entities inside it, never faster than once a second. Our house rule
Why: Every timer is a callback the server runs whether or not anything changed, and a timer per player multiplies both the cost and the clean-up.
Source: Oxide: timers
-
No lookup before the real call. Call Remove, TryGetValue or TryAdd directly instead of asking ContainsKey first. In the framework docs
Why: Checking and then acting hashes the key twice, and the direct call already does the check. A List is the exception: its Add does not check for duplicates, so a Contains guard around it is real logic.
Source: Microsoft CA1853, Microsoft CA1854
-
Pooled objects go back in a finally block. Return every pooled list or object in a finally block, using the free call that matches how you got it. In the framework docs
Why: An exception that skips the return loses the object from the pool for good.
Source: Oxide: pooling
-
Nothing heavy on the every-tick path. A hook that fires constantly must not allocate, send chat or write a file. Our house rule
Why: Decay and damage hooks run for every wall and entity, so one allocation or one message per call turns into a stutter.
Source: Oxide: plugin guidelines
-
Save after a change, not every tick. Write data to disk after something changed, or on a timer, never on every tick. Our house rule
Why: A file write costs the server far more than the few bytes it saves.
Source: Oxide: data storage
-
No scanning every entity from a timer. Keep your own list of the things you care about instead of searching all entities on a timer. Our house rule
Why: A scan visits everything the server holds, every time it runs, however little you wanted.
Source: Oxide: plugin guidelines
Correctness
Things that work on the author's test server and quietly go wrong on someone else's.
-
No reflection on game types. Use the game's members directly instead of reflection with GetField, GetMethod or Invoke. Our house rule
Why: The game's assemblies are publicized when the framework loads, so private members are reachable as written and reflection only adds cost. Unity's own assemblies are not publicized, so reflection there can still be right.
Source: Oxide: the publicizer, Carbon: creating your project
-
Config lists replace, not append. Mark a config list or dictionary that has default entries with ObjectCreationHandling.Replace. In the framework docs
Why: Without it the saved entries are added onto the defaults every time the file loads, so they duplicate on each restart.
Source: Oxide: data storage, Json.NET: ObjectCreationHandling
-
A broken data file starts empty. Wrap the read of a data file in try and catch, log once, and start from an empty default. Our house rule
Why: A hand-edited or half-written file would otherwise stop the plugin from loading, and the same error repeats every boot.
Source: Oxide: data storage
-
Web requests: plugin reference and a real check. Pass the plugin to webrequest.Enqueue, and in the callback treat a wrong status code or a null response as a failure. In the framework docs
Why: The request is registered to the plugin that sent it, and the callback runs whether or not the service answered properly.
Source: Oxide: web requests
-
Parameters in SQL, never pasted text. Pass values to a query as parameters instead of joining them into the query string. In the framework docs
Why: Pasted-in text lets a player's input change what the query does.
Source: Oxide: databases
-
Heal with Heal. Restore health by calling the game's Heal method instead of assigning to the health value. In the framework docs
Why: The game's own method is the supported way to change health, and assigning the value directly bypasses it.
Source: Oxide: best practices
-
Public API methods carry the hook attribute. Mark a public method that other plugins call with the HookMethod attribute. Our house rule
Why: On Carbon a call to a public method without it can quietly return nothing, so the other plugin never learns it failed.
Source: Oxide: attributes
-
Universal commands check for no player. A universal command handler must handle a null player. In the framework docs
Why: The same handler also runs from the server console and RCON, where there is no player at all.
Source: Carbon: commands
Lifecycle
A plugin loads, starts, reloads and unloads while the server keeps running. Each step has a job and a trap.
-
Register permissions in Init. Register every permission in Init, with a lowercase name of the form pluginname.permission. In the framework docs
Why: A permission that was never registered cannot be granted to anyone.
Source: Oxide: permissions, Carbon: permissions
-
Config and language in Loaded. Read configuration and language strings in Loaded, not Init. In the framework docs
Why: That is where the lifecycle page says to read them, because Init runs earlier, before a plugin's dependencies are available.
Source: Oxide: plugin lifecycle
-
Harmony patches use AutoPatch. Put a Harmony patch in a nested class marked AutoPatch instead of creating a Harmony instance yourself. In the framework docs
Why: The framework installs the patch when the plugin starts and removes it when it stops, so a reload cannot stack a second copy or strip another plugin's patch.
-
Do not call other plugins from Init. Leave calls to another plugin until after Init has finished. In the framework docs
Why: The other plugin may not be loaded yet, and a call to one that is not loaded returns nothing without an error.
Source: Oxide: plugin lifecycle
-
Guard optional work in start-up. Wrap optional start-up work, such as looking up a soft dependency or sending a first web call, so a failure there is caught. In the framework docs
Why: An exception thrown in Init or Loaded rolls back the whole load, so one optional step could take the plugin down.
Source: Oxide: plugin lifecycle
-
Stop what you started. In Unload, remove your panels, stop your timers and coroutines, and let your patches go. In the framework docs
Why: A reload that leaves the old copy's timers and panels running puts them beside the new copy's.
Source: Oxide: plugin lifecycle, Oxide: coroutines
-
Rebuild state from the world. In OnServerInitialized, walk the real list of players or entities and keep only the records that still have something behind them. Our house rule
Why: That hook also runs when a plugin is reloaded, and it is what clears out records left over from before a restart or wipe.
Source: Oxide: plugin lifecycle
UI
A panel is a set of named elements the server sends to the player. Sending less, and sending it less often, is the whole game.
-
No full redraw per click. When a value changes, update only the named element that changed, and never destroy and rebuild the whole panel. Our house rule
Why: A rebuild re-sends every element and makes the client request every image again, all for a change on one line.
Source: Oxide: CUI class reference, Oxide: countdown and update example
-
Long lists scroll. Show a long list in a scroll view instead of Previous and Next buttons. Our house rule
Why: One send serves the whole list and the player scrolls on their own screen, with no round trip for each page.
Source: Oxide: scroll view example, Oxide: scroll view with layout
-
Remove panels only where they are open. Send a panel's removal only to players who have that panel open. Our house rule
Why: Building and sending a removal for a player with nothing on screen is wasted work, and across every death and disconnect on a busy server it adds up.
Source: Oxide: CUI class reference
-
Unique, stable element names. Give every element its own unique name that does not change, and add a parent before its children. In the framework docs
Why: Duplicate names cause trouble, and stable names are what make in-place updates and targeted removal possible.
Source: Oxide: CUI class reference
-
Anchors for position. Place panels with anchors from 0 to 1, and use pixel offsets only for small insets. In the framework docs
Why: Offsets are measured against a 1280 by 720 screen and do not scale, while anchors follow the player's resolution.
Source: Oxide: position versus resolution, Unity: Rect Transform
-
Let the client count. Use the countdown component for a number that just counts time, instead of redrawing it from a server timer. In the framework docs
Why: A server timer sends an update every second to every viewer, while the component ticks on the player's own screen.
-
Turn the cursor on for clickable panels. Enable the cursor for any panel the player has to click, drag or scroll. In the framework docs
Why: Without it the player cannot reach the controls at all.
Source: Oxide: draggable example
What the game client refuses
Faults that compile, pass a code review and only show on a real screen. Each one below was found on a live client and has a name we use for it. The linked pages are the background, not the source: none of this is written down in the framework guides.
-
The two-graphic fault. Give one UI element one graphic: a button, an image, a raw image, a text or an input field, never two of them together. Our house rule
Why: The client cannot put a second graphic on the same object, fails the whole message with an object-reference error and disconnects the player; an icon button is a single button with its sprite set.
Source: Oxide: CUI class reference
-
The linear-light rule. Make a full-screen page behind a menu fully solid, with an alpha of exactly 1. Our house rule
Why: The client blends in linear light, so a backing at 0.93 still lets a bright screen behind it read at about a third of its brightness, and an open inventory stays legible through it.
Source: Oxide: position versus resolution, Unity: Rect Transform
-
The first-open trap. Never build a scroll view on the inventory screen's own layer; place plain tiles there and open anything longer as a full-screen page. Our house rule
Why: The client finishes setting that screen up only when the player first opens it, and a scroll view created before then stays empty for the rest of the session while its header and buttons look normal.
Source: Oxide: scroll view example
-
The short-box blank. Give a text element a box about one and a half times as tall as its font size. Our house rule
Why: Text that does not fit its box is not clipped or shrunk, it is simply not drawn, so a 34-point title in a 34-unit box is invisible.
Source: Oxide: position versus resolution, Unity: Rect Transform
-
A wheel needs a surface. Put a visible backing image on every scroll view, with an alpha of at least 0.02, and set its inertia. Our house rule
Why: A fully transparent image is skipped by the mouse, so the list draws and its scrollbar drags but the wheel does nothing.
Source: Oxide: scroll view example
-
Plain numbers in anchors. Write anchors and offsets as plain numbers separated by a space, with no language suffix such as the f of 0.095f. Our house rule
Why: The value is text the client parses, not code the compiler checks, so a suffix that compiles cleanly produces an element the client cannot place.
Source: Oxide: position versus resolution, Unity: Rect Transform
-
Drawn once per connection. Send a persistent element once when the player connects and again only when its content changes, not on every respawn or wake. Our house rule
Why: Server-drawn elements stay on the client through death and respawn, and re-sending them was three quarters of the UI traffic in a fifty-player test.
Source: Oxide: CUI class reference
Documentation
Another person has to read, run and update the plugin after you. These rules are for them.
-
Comments say why. Write comments in English above the code they describe, and use them for the reason, not a retelling of the line. In the framework docs
Why: The code already says what it does; a comment earns its place by naming the game fact, the trap or the reason a guard is there. Change history belongs in the changelog, not in comments.
Source: Oxide: plugin guidelines
-
XML docs on the public surface. Put a three-slash summary on the plugin class and on every public method that other plugins can call, with param and returns tags for API methods. In the framework docs
Why: It is how someone calling your plugin learns what it takes and what it gives back.
Source: Oxide: plugin guidelines, Microsoft: XML documentation comments
-
Info, Description and a real version. Give the plugin an Info attribute with a three-part version that you raise on every release, and a Description. In the framework docs
Why: Owners read the version to know what they are running, and the numbers only help if you keep them honest.
Source: Oxide: attributes, Semantic Versioning
-
A changelog and a README. Ship a CHANGELOG and a README beside the plugin. In the framework docs
Why: Owners decide whether to update from the changelog, and the README tells them the install steps, commands and permissions.
Source: Oxide: plugin guidelines, Keep a Changelog
-
Regions for long files. Group related code in region blocks, such as hooks, commands, config, UI and data. In the framework docs
Why: A long file is quicker to navigate and to review when it has landmarks.
Source: Oxide: plugin guidelines
-
Names that match the convention. Use PascalCase for files and classes, an underscore and camelCase for private fields, lowercase command names, and permissions of the form pluginname.permission. In the framework docs
Why: Owners type the commands and permission names, and developers read the rest, so the usual shapes save everyone guessing.
Source: Oxide: plugin guidelines, Microsoft: identifier names
-
Named constants, not magic numbers. Give a number that means something a named constant or a config setting. In the framework docs
Why: A bare 47 tells the next reader nothing, and the same number in three places gets changed in only two.
Source: Oxide: best practices
Think a rule is wrong, or know a better source for one? Say so on the Help board. More reading is on Rust modding resources, and Outside Unsafe Base: what changed and why is a worked example of a review.
