How to create a plugin for your MU Online server's WebEngine
Learn how to structure, code, and publish your own plugin for your MU Online server's WebEngine (web panel), integrating rankings, voting, the shop, and commands with the game database.
The WebEngine is the backbone of your MU Online server's online presence: it displays the reset ranking, processes account registration, integrates toplist voting, shows the cash shop, and in many cases allows remote commands like resetting a character or changing class. When the default panel doesn
The WebEngine is the backbone of your MU Online server's online presence: it displays the reset ranking, processes account registration, integrates toplist voting, shows the cash shop, and in many cases allows remote commands like resetting a character or changing class. When the default panel doesn't cover a specific need — a seasonal event, a Discord integration, a coupon system — the right move isn't to patch the core, it's to build a plugin. This tutorial shows how to structure, code, test, and publish a plugin from scratch, covering the typical architecture, secure database access, and production considerations.
What a WebEngine plugin is and when to build one
A plugin is an isolated module that integrates with the main panel without touching its core files. It exposes its own routes, its own views, and, if needed, its own database tables — all kept separate so a base WebEngine update doesn't overwrite your work. Build a plugin when the feature is optional, specific to your server (not generic enough to belong in the core), or needs its own lifecycle (enable/disable, versioning, distribution to other admins).
Typical architecture of a PHP-based WebEngine
Most MU panels use an MVC (Model-View-Controller) structure. Understanding this structure is the first step before writing a single line of plugin code.
| Layer | Responsibility | Typical location |
|---|---|---|
| Controller | Receives the HTTP request, invokes the logic | application/controllers/ |
| Model | Database queries (MSSQL/MySQL) | application/models/ |
| View | HTML/Twig/Blade rendered to the user | application/views/ |
| Config | Connection strings, keys, routes | application/config/ |
| Plugins | Isolated third-party modules | application/plugins/<name>/ |
Prerequisites before you start
- WebEngine already installed and working, connected to the server database (see the server setup tutorial).
- Write access to the panel source code (FTP/SSH) and a staging environment separate from production.
- Read credentials for the game database (ideally a user with restricted permissions, not
sa). - A code editor with PHP and SQL support, plus a database client (SSMS or HeidiSQL) to inspect tables.
Step 1 — Define the plugin's scope and contract
Before coding, write down in one sentence what the plugin does: "Displays a promo code redemption panel that credits Cash Points to the account." That contract defines which tables you'll touch (MEMB_STAT, CashShop_Log, or equivalents), which routes you'll expose (/plugins/promocode/redeem), and the permissions required (logged-in user, not banned).
Step 2 — Create the plugin's folder structure
An isolated plugin typically follows this skeleton:
application/plugins/promocode/
├── PromocodeController.php
├── PromocodeModel.php
├── views/
│ └── redeem.php
├── config.php
└── install.sql
The install.sql file creates the plugin's own tables (e.g., plugin_promocode_codes), so it doesn't need to alter the game's core tables.
Step 3 — Write the Model (database access)
<?php
class PromocodeModel extends CI_Model {
public function validateCode($code, $accountId) {
$this->db->where('code', $code);
$this->db->where('used', 0);
$query = $this->db->get('plugin_promocode_codes');
return $query->row();
}
public function creditCash($accountId, $amount) {
// Always through a transaction — never a direct UPDATE without concurrency control
$this->db->trans_start();
$this->db->query(
"UPDATE MEMB_STAT SET CashPoint = CashPoint + ? WHERE memb___id = ?",
[$amount, $accountId]
);
$this->db->trans_complete();
return $this->db->trans_status();
}
}
Note the use of transactions and parameterized queries — never concatenate user input directly into SQL; that's the most common entry point for SQL Injection in poorly built MU panels.
Step 4 — Write the Controller and the route
<?php
class PromocodeController extends CI_Controller {
public function redeem() {
if (!$this->session->userdata('logged_in')) {
redirect('login');
}
$code = $this->input->post('code');
$account = $this->session->userdata('account_id');
$this->load->model('plugins/promocode/PromocodeModel');
$item = $this->PromocodeModel->validateCode($code, $account);
if (!$item) {
$this->session->set_flashdata('error', 'Invalid or already used code.');
redirect('plugins/promocode');
}
$this->PromocodeModel->creditCash($account, $item->value);
$this->session->set_flashdata('success', 'Cash credited successfully!');
redirect('plugins/promocode');
}
}
Register the route in the panel's routes file (application/config/routes.php), pointing plugins/promocode to this controller.
Step 5 — Integrate with the MuServer database schema
Tables vary by emulator, but the most common integration points are:
| Need | Typical table (MSSQL) | Caution |
|---|---|---|
| Account data | MEMB_INFO / MEMB_STAT | Never modify a plain-text password |
| Shop Cash/Points | MEMB_STAT.CashPoint or a dedicated table | Always use a transaction |
| In-game mail | PostMain / PostList (varies) | Follow the exact format the GameServer expects |
| Action log | Plugin's own table | Always log any credited value |
If your emulator exposes stored procedures for sensitive operations (creating an item, sending mail), prefer calling them instead of doing manual INSERT/UPDATE — they already handle concurrency and MU's specific binary formats.
Step 6 — Authentication, session, and permissions
Reuse the WebEngine's existing session system (don't build a parallel one). Always check that the user is logged in before any action that writes data, and add a second layer of verification if the action involves monetary values (e.g., re-confirming the password for high-value redemptions).
Step 7 — Views and user experience
Keep the layout consistent with the rest of the panel (same CSS, same header/footer), reusing existing templates instead of building isolated HTML. This keeps the plugin from "feeling" like a different site and breaking player trust.
Step 8 — Testing in a staging environment
- Restore a copy of the production database into a test database.
- Point the staging WebEngine to that database.
- Test the full flow: valid code, invalid code, already-used code, banned account, simulated connection failure.
- Check the generated logs and confirm no value gets credited twice under concurrent requests (test concurrency with two tabs at the same time).
Step 9 — Package and version the plugin
Package the plugin as a distributable bundle (a zip with install.sql, the code, and an installation README), and keep a version number (1.0.0) in config.php. This makes future updates easier and lets you roll back if a deploy breaks something in production.
Step 10 — Publish to production safely
Deploy outside peak hours, with a database backup right before the deploy. Run install.sql first, validate the created tables, then push the plugin code. Monitor the WebEngine logs and the database's CPU/query load during the first few hours after launch.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| 500 error when accessing the plugin route | Route not registered or model not loaded | Check routes.php and the load->model call in the controller |
| Cash credited twice | Missing transaction/lock on the request | Wrap the operation in a transaction and mark the code as used before crediting |
| SQL Injection found in an audit | Direct concatenation of input into a query | Switch to parameterized queries (? bind) |
| Plugin breaks after a core update | Plugin modified core files instead of staying isolated | Restructure so it never touches files outside plugins/ |
| Player session not recognized in the plugin | Parallel/incompatible session system | Reuse the WebEngine's native session |
Plugin release checklist
- Plugin scope and contract defined in one clear sentence.
- Structure isolated in
plugins/<name>/, without touching core files. - Parameterized queries and transactions on any monetary write.
- Authentication reusing the WebEngine's native session.
- Tested in staging with error cases and concurrency.
- Database backup taken before the production deploy.
- Logging and monitoring active during the first hours after launch.
With the plugin published and stable, the natural next step is to think about how it connects to the rest of the player experience — for example, tying code redemption to in-game events described in the MU Online server setup tutorial, closing the loop between web and game.
Frequently asked questions
What exactly is a MU server's WebEngine?
It's the web panel (usually built in PHP or .NET) that shows rankings and handles account registration, voting, the cash shop, and remote commands. It connects to the same database as MuOnline/GameServer to read and write player data.
Do I need to know PHP to build a plugin?
It depends on the WebEngine you're using. Most community panels (based on CodeIgniter or Laravel) use PHP; some newer projects use .NET/C#. You need to know the specific panel's language and the database schema (MSSQL in most emulators).
Can a plugin execute commands directly on the player's character?
Yes, as long as the plugin writes to tables or stored procedures the GameServer already reads, such as command queues, in-game mail, or item tables. Never edit sensitive columns directly (like Money, Level) without going through the official procedures, or you risk item duplication or wiped characters.
How do I test a plugin without affecting the production server?
Always use a staging environment with a copy of the database and, if possible, a test GameServer. Only publish to the live environment after validating queries, response times, and error cases (duplicate account, nonexistent item, insufficient balance).
Is it safe to expose an API inside the plugin for the game client to consume?
Yes, but it requires authentication (per-session token), rate limiting, and strict input validation. A poorly protected API becomes an entry point for voting bots, cash duplication, or brute-force attacks against accounts.