Website: Inscription Tooltips
Inscribed items show extra lines in the game tooltip: Potential, slots, inscriptions and the bound owner. Item data on a website (character viewer, rankings, market, warehouse) only shows the 16-byte item, so these lines are missing.
This guide shows how to read the inscription data and print the same lines as the game client.
For what inscriptions are and how to configure them, see Inscription (overview).
What the tooltip shows#
The game client adds this block at the end of the item tooltip, after the normal option lines:
Potential 30 / 80 <- yellow, bold
Might II <- violet, bold (filled slot)
Damage +4% <- blue (its description)
Inscription Slot (empty) <- gray (open slot)
Inscription Slot (corrupted) <- dark red (corrupted slot)
Bound to Hero <- red (only if bound)
Potential 30 / 80= Potential left / Potential total.- Closed slots print nothing.
- An item with no inscription data prints nothing at all.
Where the data lives#
You need three things:
| Data | Where | What you get |
|---|---|---|
| Item serial | The 16-byte item, bytes 3-6 | Key into ItemExt |
| Inscription record | SQL table ItemExt, column Data | Potential, slots, bound name |
| Inscription names and texts | Data/Plugins/Inscription/Pools.xml | Name, description, magnitude per tier |
Also read the Enabled attribute of Data/Plugins/Inscription/Inscription.xml. When it is 0, the game shows no inscription lines, so the website must not either.
Step 1 - Get the serial from the item#
Every 16-byte item has its serial in bytes 3-6, big-endian (most significant byte first). This is the same in every column that stores items: Character.Inventory, warehouse.Items, ExtWarehouse.Items, market rows.
| Byte | Content |
|---|---|
| 0-2 | Item index, level, options (you already decode these) |
| 3-6 | Serial, big-endian unsigned 32-bit |
| 7-15 | More options, sockets |
Skip:
- Empty slots: byte 0 =
0xFF, byte 7 has bit0x80set, and the high 4 bits of byte 9 are all set (0xF0). - Serial 0: shop and system items without a serial can never carry inscriptions.
- Stackable items (jewels, potions): never carry inscriptions.
Example: bytes 3-6 = 00 01 E2 40 → serial 0x0001E240 = 123456.
Step 2 - Load the ItemExt rows#
CREATE TABLE ItemExt (
ItemSerial bigint PRIMARY KEY,
Data varbinary(128) NOT NULL,
Updated datetime NOT NULL
)
One row per item that has extra data. No row = nothing to show.
Collect the serials of all items on the page, then load them in one query:
SELECT ItemSerial, Data FROM ItemExt WHERE ItemSerial IN (@s0, @s1, @s2, ...)
Step 3 - Find the inscription record#
Data is a list of records, one per end-game system. Inscription is system 1. Other systems may be added later, so always walk the list; never assume the inscription record is first.
Each record:
| Offset | Size | Field |
|---|---|---|
| 0 | 1 | SystemId (0 = end of list) |
| 1 | 1 | Version |
| 2 | 1 | Len = size of the body |
| 3 | Len | Body |
Walk: start at offset 0. Stop when SystemId is 0 or the record would run past byte 128. Otherwise, if SystemId is 1 that is the record; if not, jump 3 + Len bytes to the next one.
Use the record only when Version = 1 and Len ≥ 21. Anything else: show nothing (same as the game).
Step 4 - Read the record body#
| Offset | Size | Field | Values |
|---|---|---|---|
| 0 | 1 | Potential | 0-100, total budget |
| 1 | 1 | Spent | Potential used so far |
| 2 | 3 | Slot 1 | Id, Tier, State (one byte each) |
| 5 | 3 | Slot 2 | Id, Tier, State |
| 8 | 3 | Slot 3 | Id, Tier, State |
| 11 | 10 | Bound | Character name, ASCII, zero-padded. All zero = not bound |
| 21 | 2 | (server only) | Reroll range. Never show it |
Slot fields:
| Field | Meaning |
|---|---|
Id | Entry Id in Pools.xml. 0 = none |
Tier | 0 = inscribed, 1 = empowered once (II), 2 = twice (III) ... up to 7 (VIII) |
State | 0 = closed, 1 = open (empty), 2 = filled, 3 = corrupted |
Step 5 - Load the inscription texts from Pools.xml#
<Pool Name="Weapon" ItemMin="0" ItemMax="3071">
<Entry Id="1" Name="Might" Desc="Damage +%d%%" Archetype="Amplify" OptionIndex="125"
Magnitude="3,4,5" Cost="15,8,8" Weight="100" Catalyst="14,16"/>
</Pool>
For every <Entry> in every <Pool>, keep:
| Attribute | Use |
|---|---|
Id | Key (1-255, unique across all pools) |
Name | First 31 characters |
Desc | First 95 characters. Can be empty |
Magnitude | Comma list, one number per tier (max 8) |
Magnitude rule: a short list repeats its last number. Magnitude="3,4,5" means tiers 0-7 = 3, 4, 5, 5, 5, 5, 5, 5. An empty list = all 0.
Parse the file once and cache it. Reload it when the file changes.
Step 6 - Print the lines#
In this order:
-
Potential line (yellow, bold):
Potential {left} / {Potential}whereleft = Potential - Spent, never below 0. -
One block per slot, slot 1 to 3:
State Lines 0 closed nothing 1 open Inscription Slot (empty)- gray3 corrupted Inscription Slot (corrupted)- dark red2 filled, Idfound in Pools.xmlName + tier suffix - violet, bold. Then the description - blue (skip if Descis empty)2 filled, Idnot in Pools.xmlUnknown Inscription- gray -
Bound line (red), only if the name is not empty:
Bound to {name}.
Tier suffix: tier 0 = nothing, 1 = II, 2 = III, 3 = IV, 4 = V, 5 = VI, 6 = VII, 7+ = VIII. Example: Might II.
Description: take Desc, replace the first %d with that tier's magnitude, and replace every %% with %. Damage +%d%% at tier 1 → Damage +4%.
Colors used by the game client, for a dark tooltip background:
| Line | Color |
|---|---|
| Potential | #FFCC1A (yellow) |
| Inscription name | #B366FF (violet) |
| Description | #80B3FF (blue) |
| Empty slot, unknown | #666666 (gray) |
| Corrupted slot | dark red, for example #A01818 |
| Bound | #FF331A (red) |
Full code#
Uses mssql and fast-xml-parser (npm i fast-xml-parser).
// inscription.ts
import fs from "fs";
import sql from "mssql";
import { XMLParser } from "fast-xml-parser";
const DATA_DIR = "C:/MuServer/Data/Plugins/Inscription"; // your server's path
const SYS_INSCRIPTION = 1;
const TIER_TEXT = ["", " II", " III", " IV", " V", " VI", " VII", " VIII"];
// ---------- Step 1: serial ----------
export function itemSerial(item: Buffer): number | null {
if (item.length < 16) return null;
const empty = item[0] === 0xff && (item[7] & 0x80) !== 0 && (item[9] & 0xf0) === 0xf0;
if (empty) return null;
const serial = item.readUInt32BE(3);
return serial === 0 ? null : serial;
}
// ---------- Step 2: ItemExt rows ----------
export async function loadItemExt(pool: sql.ConnectionPool, serials: number[]): Promise<Map<number, Buffer>> {
const out = new Map<number, Buffer>();
const unique = [...new Set(serials)];
for (let i = 0; i < unique.length; i += 500) {
const chunk = unique.slice(i, i + 500);
const req = pool.request();
chunk.forEach((s, n) => req.input(`s${n}`, sql.BigInt, s));
const names = chunk.map((_, n) => `@s${n}`).join(",");
const r = await req.query(`SELECT ItemSerial, Data FROM ItemExt WHERE ItemSerial IN (${names})`);
for (const row of r.recordset) out.set(Number(row.ItemSerial), row.Data as Buffer);
}
return out;
}
// ---------- Step 3 + 4: record ----------
export interface InscSlot { id: number; tier: number; state: number }
export interface InscRecord { potential: number; spent: number; slots: InscSlot[]; bound: string }
export function readInscription(data: Buffer): InscRecord | null {
let p = 0;
while (p + 3 <= data.length && data[p] !== 0) {
const len = data[p + 2];
if (p + 3 + len > data.length) return null; // broken tail
if (data[p] === SYS_INSCRIPTION) {
if (data[p + 1] !== 1 || len < 21) return null; // unknown version
const b = data.subarray(p + 3, p + 3 + len);
const slots = [0, 1, 2].map((s) => ({ id: b[2 + s * 3], tier: b[3 + s * 3], state: b[4 + s * 3] }));
const name = b.subarray(11, 21);
const end = name.indexOf(0);
return {
potential: b[0],
spent: b[1],
slots,
bound: name.subarray(0, end < 0 ? 10 : end).toString("latin1"),
};
}
p += 3 + len;
}
return null;
}
// ---------- Step 5: Pools.xml ----------
interface InscEntry { name: string; desc: string; magnitude: number[] }
let cache: { mtime: number; enabled: boolean; entries: Map<number, InscEntry> } | null = null;
const asArray = <T>(v: T | T[] | undefined): T[] => (v === undefined ? [] : Array.isArray(v) ? v : [v]);
function loadConfig() {
const poolsFile = `${DATA_DIR}/Pools.xml`;
const mainFile = `${DATA_DIR}/Inscription.xml`;
const mtime = Math.max(fs.statSync(poolsFile).mtimeMs, fs.statSync(mainFile).mtimeMs);
if (cache && cache.mtime === mtime) return cache;
const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "", parseAttributeValue: false });
const main = parser.parse(fs.readFileSync(mainFile, "utf8"));
const pools = parser.parse(fs.readFileSync(poolsFile, "utf8"));
const entries = new Map<number, InscEntry>();
for (const pool of asArray<any>(pools.Pools?.Pool)) {
for (const e of asArray<any>(pool.Entry)) {
const id = parseInt(e.Id, 10);
if (!(id >= 1 && id <= 255) || entries.has(id)) continue; // the server skips these too
const mag = String(e.Magnitude ?? "").split(",").map((x) => parseInt(x.trim(), 10)).filter((x) => !isNaN(x)).slice(0, 8);
if (mag.length === 0) mag.push(0);
while (mag.length < 8) mag.push(mag[mag.length - 1]); // short list repeats its last value
entries.set(id, {
name: String(e.Name ?? "").slice(0, 31),
desc: String(e.Desc ?? "").slice(0, 95),
magnitude: mag,
});
}
}
cache = { mtime, enabled: String(main.Inscription?.Enabled ?? "0") === "1", entries };
return cache;
}
// ---------- Step 6: lines ----------
export type LineColor = "yellow" | "violet" | "blue" | "gray" | "darkred" | "red";
export interface TooltipLine { text: string; color: LineColor; bold: boolean }
function formatDesc(desc: string, value: number): string {
let out = "";
let replaced = false;
for (let i = 0; i < desc.length; i++) {
if (desc[i] === "%" && desc[i + 1] === "d" && !replaced) { out += String(value); replaced = true; i++; continue; }
if (desc[i] === "%" && desc[i + 1] === "%") i++;
out += desc[i];
}
return out;
}
export function inscriptionLines(data: Buffer | undefined): TooltipLine[] {
if (!data) return [];
const cfg = loadConfig();
if (!cfg.enabled) return [];
const rec = readInscription(data);
if (!rec) return [];
const lines: TooltipLine[] = [];
const left = Math.max(0, rec.potential - rec.spent);
lines.push({ text: `Potential ${left} / ${rec.potential}`, color: "yellow", bold: true });
for (const slot of rec.slots) {
if (slot.state === 1) lines.push({ text: "Inscription Slot (empty)", color: "gray", bold: false });
else if (slot.state === 3) lines.push({ text: "Inscription Slot (corrupted)", color: "darkred", bold: false });
else if (slot.state === 2) {
const e = cfg.entries.get(slot.id);
if (!e) { lines.push({ text: "Unknown Inscription", color: "gray", bold: false }); continue; }
const tier = Math.min(slot.tier, 7);
lines.push({ text: e.name + TIER_TEXT[tier], color: "violet", bold: true });
if (e.desc) lines.push({ text: formatDesc(e.desc, e.magnitude[tier]), color: "blue", bold: false });
}
}
if (rec.bound) lines.push({ text: `Bound to ${rec.bound}`, color: "red", bold: false });
return lines;
}
Using it on a character page:
const inv: Buffer = character.Inventory; // varbinary(3792)
const items = [];
for (let slot = 0; slot * 16 + 16 <= inv.length; slot++) items.push(inv.subarray(slot * 16, slot * 16 + 16));
const ext = await loadItemExt(pool, items.map(itemSerial).filter((s): s is number => s !== null));
for (const item of items) {
const serial = itemSerial(item);
const extra = serial === null ? [] : inscriptionLines(ext.get(serial));
// render your normal tooltip, then `extra` at the end (after a blank line)
}
Uses PDO (sqlsrv) and SimpleXML.
<?php
// inscription.php
const INSC_DATA_DIR = 'C:/MuServer/Data/Plugins/Inscription'; // your server's path
const INSC_TIER_TEXT = ['', ' II', ' III', ' IV', ' V', ' VI', ' VII', ' VIII'];
// ---------- Step 1: serial ----------
function itemSerial(string $item): ?int {
if (strlen($item) < 16) return null;
$b = array_values(unpack('C16', $item));
if ($b[0] === 0xFF && ($b[7] & 0x80) && ($b[9] & 0xF0) === 0xF0) return null; // empty slot
$serial = unpack('N', substr($item, 3, 4))[1]; // big-endian
return $serial === 0 ? null : $serial;
}
// ---------- Step 2: ItemExt rows ----------
function loadItemExt(PDO $db, array $serials): array {
$out = [];
foreach (array_chunk(array_values(array_unique($serials)), 500) as $chunk) {
$marks = implode(',', array_fill(0, count($chunk), '?'));
$st = $db->prepare("SELECT ItemSerial, Data FROM ItemExt WHERE ItemSerial IN ($marks)");
$st->execute($chunk);
$st->bindColumn(1, $serial);
$st->bindColumn(2, $data, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY);
while ($st->fetch(PDO::FETCH_BOUND)) $out[(int)$serial] = is_resource($data) ? stream_get_contents($data) : $data;
}
return $out;
}
// ---------- Step 3 + 4: record ----------
function readInscription(string $data): ?array {
$n = strlen($data);
$p = 0;
while ($p + 3 <= $n && ord($data[$p]) !== 0) {
$len = ord($data[$p + 2]);
if ($p + 3 + $len > $n) return null; // broken tail
if (ord($data[$p]) === 1) {
if (ord($data[$p + 1]) !== 1 || $len < 21) return null; // unknown version
$b = substr($data, $p + 3, $len);
$slots = [];
for ($s = 0; $s < 3; $s++) {
$slots[] = ['id' => ord($b[2 + $s * 3]), 'tier' => ord($b[3 + $s * 3]), 'state' => ord($b[4 + $s * 3])];
}
$name = substr($b, 11, 10);
$zero = strpos($name, "\0");
return [
'potential' => ord($b[0]),
'spent' => ord($b[1]),
'slots' => $slots,
'bound' => $zero === false ? $name : substr($name, 0, $zero),
];
}
$p += 3 + $len;
}
return null;
}
// ---------- Step 5: Pools.xml ----------
function inscriptionConfig(): array {
static $cache = null;
$pools = INSC_DATA_DIR . '/Pools.xml';
$main = INSC_DATA_DIR . '/Inscription.xml';
clearstatcache();
$mtime = max(filemtime($pools), filemtime($main));
if ($cache !== null && $cache['mtime'] === $mtime) return $cache;
$enabled = (string)simplexml_load_file($main)['Enabled'] === '1';
$entries = [];
foreach (simplexml_load_file($pools)->Pool as $pool) {
foreach ($pool->Entry as $e) {
$id = (int)$e['Id'];
if ($id < 1 || $id > 255 || isset($entries[$id])) continue; // the server skips these too
$mag = [];
foreach (explode(',', (string)$e['Magnitude']) as $x) {
if (trim($x) !== '' && count($mag) < 8) $mag[] = (int)trim($x);
}
if (!$mag) $mag[] = 0;
while (count($mag) < 8) $mag[] = end($mag); // short list repeats its last value
$entries[$id] = [
'name' => substr((string)$e['Name'], 0, 31),
'desc' => substr((string)$e['Desc'], 0, 95),
'magnitude' => $mag,
];
}
}
return $cache = ['mtime' => $mtime, 'enabled' => $enabled, 'entries' => $entries];
}
// ---------- Step 6: lines ----------
function formatInscDesc(string $desc, int $value): string {
$out = '';
$replaced = false;
$n = strlen($desc);
for ($i = 0; $i < $n; $i++) {
$next = $i + 1 < $n ? $desc[$i + 1] : '';
if ($desc[$i] === '%' && $next === 'd' && !$replaced) { $out .= $value; $replaced = true; $i++; continue; }
if ($desc[$i] === '%' && $next === '%') $i++;
$out .= $desc[$i];
}
return $out;
}
/** @return array<array{text: string, color: string, bold: bool}> */
function inscriptionLines(?string $data): array {
if ($data === null) return [];
$cfg = inscriptionConfig();
if (!$cfg['enabled']) return [];
$rec = readInscription($data);
if ($rec === null) return [];
$left = max(0, $rec['potential'] - $rec['spent']);
$lines = [['text' => "Potential $left / {$rec['potential']}", 'color' => 'yellow', 'bold' => true]];
foreach ($rec['slots'] as $slot) {
if ($slot['state'] === 1) $lines[] = ['text' => 'Inscription Slot (empty)', 'color' => 'gray', 'bold' => false];
elseif ($slot['state'] === 3) $lines[] = ['text' => 'Inscription Slot (corrupted)', 'color' => 'darkred', 'bold' => false];
elseif ($slot['state'] === 2) {
$e = $cfg['entries'][$slot['id']] ?? null;
if ($e === null) { $lines[] = ['text' => 'Unknown Inscription', 'color' => 'gray', 'bold' => false]; continue; }
$tier = min($slot['tier'], 7);
$lines[] = ['text' => $e['name'] . INSC_TIER_TEXT[$tier], 'color' => 'violet', 'bold' => true];
if ($e['desc'] !== '') $lines[] = ['text' => formatInscDesc($e['desc'], $e['magnitude'][$tier]), 'color' => 'blue', 'bold' => false];
}
}
if ($rec['bound'] !== '') $lines[] = ['text' => 'Bound to ' . $rec['bound'], 'color' => 'red', 'bold' => false];
return $lines;
}
Using it on a character page:
$items = str_split($character['Inventory'], 16); // varbinary(3792)
$ext = loadItemExt($db, array_filter(array_map('itemSerial', $items), fn($s) => $s !== null));
foreach ($items as $item) {
$serial = itemSerial($item);
$extra = $serial === null ? [] : inscriptionLines($ext[$serial] ?? null);
// render your normal tooltip, then $extra at the end (after a blank line):
foreach ($extra as $l) {
printf('<div class="insc-%s%s">%s</div>', $l['color'], $l['bold'] ? ' bold' : '', htmlspecialchars($l['text']));
}
}
Worked example#
A sword with serial 123456. Item bytes 3-6: 00 01 E2 40.
ItemExt.Data for serial 123456 (first 26 bytes, the rest is zeros):
01 01 17 50 19 01 01 02 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 32 64
| Bytes | Meaning |
|---|---|
01 01 17 | System 1 (Inscription), version 1, body 23 bytes |
50 | Potential 80 |
19 | Spent 25 |
01 01 02 | Slot 1: entry 1, tier 1, filled |
00 00 01 | Slot 2: open |
00 00 00 | Slot 3: closed |
00 × 10 | Not bound |
32 64 | Server only - ignore |
Entry 1 in the shipped Pools.xml: Name="Might" Desc="Damage +%d%%" Magnitude="3,4,5". Tier 1 → magnitude 4.
Result:
Potential 55 / 80
Might II
Damage +4%
Inscription Slot (empty)
Important Behavior#
- Data is live. The game server writes
ItemExtas soon as an inscription changes. The item columns (Inventory, ...) are saved on the normal save cycle, so for a few seconds a just-moved item can show in its old place. That is normal. - Rows follow the serial. Trade, warehouse, drop and pick-up keep the record. Selling to an NPC, using the item in a mix, or the item vanishing from the ground deletes the row.
- No record yet. Eligible items get their Potential the first time a player gets them (pick-up, mix, shop, or the first login after the plugin was turned on). Until then the item has no row and shows nothing. The game shows nothing too.
- Plugin off or unlicensed. With
Enabled="0"the rows stay in the database but the game shows nothing. Your website checksEnabledfor this. The website cannot see the license; if the paid module is not licensed, the server owner should setEnabled="0". - Entry removed from Pools.xml. Items keep the
Id. The game showsUnknown Inscription; do the same. Never renumber entry Ids. - Same serial twice. Only stackable items can share a serial, and they never have records. One serial = one record.
Optional - catalyst lines on jewels#
The game also explains catalyst jewels. Hovering a jewel that is the Catalyst of an entry shows:
Inscription Catalyst <- yellow, bold
Weapon: Might <- violet (pool name: entry name)
Damage +3% <- blue (tier 0 magnitude)
One pair of lines per entry whose Catalyst="Group,Index[,Level]" matches the jewel (and its level, if given). The game only lists entries whose Class includes the viewer's class; on a website, either filter by the character being viewed or show all.
Common Issues#
Related pages#
- Inscription (overview)
- Inscription.xml -
Enabled, eligible items, Potential bands, slots - Pools.xml - entries, names, descriptions, magnitudes
- Recipes.xml - inscribe / empower mixes