| name | m2ui |
| description | Use when creating, modifying, auditing, or debugging Metin2 client UI code (uiscript dicts, root ui*.py classes, locale entries) โ user says "/m2ui", "create UI", "make a window", "add button to", provides a UI screenshot to replicate, asks to "check UI for bugs" / "diagnose", or reports a visibly broken window ("doesn't open", "click does nothing", "leaks").
|
/m2ui โ Metin2 UI Generator
Dispatched as a subagent with a specific task? Skip mode detection and execute the task directly โ the parent agent already loaded m2ui context and picked the mode.
Mode Detection
Priority order:
- Explicit keyword: args start with
screenshot, talk, script, or diagnose โ that mode
- Image attached โ screenshot mode
- Diagnose request: args say "check", "audit", "review", "diagnose", "find bugs in" โ diagnose mode
- Symptom report: args contain a visible-bug phrase ("doesn't appear", "doesn't open", "doesn't work", "click does nothing", "X is broken", "looks broken", "leak", "crashes after", "stuck", "flickers") โ even when a
.py file is also referenced โ load reference/failure-atlas.md FIRST, diagnose via the matching symptom entry, THEN script mode (if a code fix is needed) or talk mode
- File reference: args name a
.py file in uiscript/ or root/ โ script mode
- Text description: any other text โ talk mode
- No args: ask โ "(a) Create from screenshot, (b) Describe a new UI, (c) Modify an existing file, (d) Diagnose for bugs" โ then dispatch
Read the matching mode file from modes/ adjacent to this SKILL.md (screenshot.md, talk.md, script.md, diagnose.md) and follow it.
Before Generating Any Code
Mandatory floor (always load):
reference/mental-model.md โ ymir engine concepts; deprograms web/React assumptions
reference/event-binding.md โ callback wrapping matrix
Conditional load (only what the task needs):
| Task | Load |
|---|
| New window from scratch | reference/anchors/README.md โ walk its 2-step tree (see Anchor selection) |
| Modifying an existing window | Skip anchors; load the existing files |
| Widget you haven't used recently | reference/widgets.md โ that widget's section |
| Locale-heavy work (many new strings) | reference/locale.md |
C++ Python API (net.X, player.X, ...) not already in context | reference/bindings.md โ grep for the function |
| Patterns reminder (Initialize/Destroy, scrollbar wiring, ListBoxEx, integration template, lazy-load sub-windows, inner helper classes, 2D grids) | reference/patterns.md โ relevant section |
| User reports a visible symptom | reference/failure-atlas.md โ matching symptom entry FIRST, before any anchor |
| Visual style/sizing matters for a new window | reference/visual-conventions.md โ pick archetype + chrome + palette before coding |
| Wiring a window into the main interface | reference/integration.md (always โ after every emission) |
| Window has an OnUpdate body (animation / polling / fade / daily-event timing / movement queues / effect chains) | reference/timer-patterns.md |
Anchor selection (new windows): walk the 2-step decision tree in reference/anchors/README.md โ pick exactly ONE primary archetype (the window's chrome; no exact match โ closest; never skip this step), plus zero or more augmentors (05-feature-gated, 14-drag-and-drop, 15-network-coupled-flow, 16-tabbed-content, 22-compare-tooltip, 23-auto-hide-chrome). Read the primary FIRST; augmentors layer on top and never override the primary's lifecycle/structure. Tie-breaker: match the window's CHROME, not its data โ "tabbed inventory" = primary 08-inventory-equipment + augmentor 16-tabbed-content, NOT 16 alone. For widgets.md/locale.md/bindings.md/patterns.md, load only the section you need, not the whole file.
Output Targets
| Output | Path |
|---|
| uiscript dicts | pack/pack/uiscript/uiscript/ |
| root UI classes | pack/pack/root/ |
| locale strings | auto-detect โ see reference/locale.md |
Critical Rules
All modes, all generated code. Reference files cite these by number โ numbering 1-19 is frozen; new rules append.
@ui.WindowDestroy on every Destroy(self) method
Initialize() or __Initialize() sets all instance vars to None/defaults
Destroy() calls Initialize(); script-backed windows also ClearDictionary()
__del__ calls ui.ScriptWindow.__del__(self)
-
**Callback wrapping** โ every callback that references `self` MUST use `ui.__mem_func__()`, `SAFE_SetEvent` (if fork provides it), or `lambda r=proxy(self): r.X()`. NEVER a bare bound method (`btn.SetEvent(self.OnClick)`) or self-capturing lambda (`lambda: self.OnClick()`) โ both hold `self` alive past `Destroy` and leak. The single most common bug in community Metin2 code. Full matrix: `reference/event-binding.md`.
Open()/Close() โ Open calls Show(), Close calls Hide()
OnPressEscapeKey() returns True (always; not False)
OnMouseWheel() returns True/False based on whether it consumed the event
- No hardcoded strings โ all user text via
localeInfo.* or uiScriptLocale.*
constInfo.intWithCommas() for large numbers
"not_pick" flag on decorative elements (lines, separators, background images)
- Z-order: create widgets back-to-front (SetParent call order = render order)
- Parent bounds clip picking โ size parents to contain all interactive children
- Python 2.7 target โ
// for int division, in not has_key(), keep xrange. Full py2/py3 rules: reference/patterns.md Section 8
- Asset paths must exist โ verify every
d:/ymir work/ui/... path under D:\ymir work\ui\ via Glob before referencing it. New asset needed โ emit # TBD ASSET: <path> โ needs creation; never invent (invented path = red-X/pink-box at runtime, failure-atlas entry 6).
- Verified C++ APIs only โ every call into
net, player, item, chr, app, wndMgr, chat, quest must exist in reference/bindings.md. Absent โ ask the user OR stub with # TODO: verify <module>.<func> exists in your fork; never invent (invented binding = AttributeError crash).
-
**Preserve existing Destroy bodies when adding `@ui.WindowDestroy`** โ add the decorator, NEVER strip the body. Pure assignments (`self.X = None`) are safe. Direct method calls on owned widgets (`self.confirmDialog.Hide()`) MUST be guarded with `if self.X:` โ WOC nulls those attrs before the body runs. Inspect every helper the body calls (`self.__Initialize()`, `self._Reset()`, any name): defaults-only assignments are safe; widget derefs inside the helper need the same guards (or relocate to `Close()`). No guard needed for `self.Hide()`, `self.ClearDictionary()`, `self.SetTop()` โ they touch only WOC-whitelisted attrs. Full whitelist + rationale: `reference/patterns.md` Section 5.11.
- ASCII-only in emitted Python โ new
.py content m2ui writes (code AND comments) is ASCII: no em/en-dash, ellipsis, curly quotes โ use -, --, ..., ', ". Pre-existing non-ASCII and verbatim user-supplied content stay untouched; locale data files exempt (see reference/locale.md). Reason: cp1252/cp949 build encodings.
-
**Verify setter accepts `*args` before Pattern B / Pattern E** โ before emitting `receiver.SetX(ui.__mem_func__(self.M), arg, ...)` or `SAFE_SetEvent(self.M, arg, ...)` with extra args, READ the setter in `pack/pack/root/ui.py`. If it is 1-arg (`def SetX(self, event):`), the call raises `TypeError` at runtime. Fix: (a) augment the setter for `*args` per `reference/framework-augmentations.md` (preferred), or (b) fall back to Pattern C proxy lambda. Common 1-arg setters: `EditLine.Set{Return,Escape,Tab}Event`, `SlotWindow.Set*Event`. Never trust by name โ verify the actual file.
Pre-Emit Self-Review
Mandatory gate BEFORE any output to the user or any file write, on every emission including edits. Every user-reported regression against this skill (leaks, missing decorators, off-screen widgets) traces back to a skipped self-review.
Silently verify each item against the draft; any failure โ revise and re-check. Item numbers are cited by reference files โ order is frozen.
- Rule 1:
@ui.WindowDestroy on every Destroy()
- Rule 2: every
self.X assignment listed in Initialize()/__Initialize()
- Rule 5: every callback wrapped per
reference/event-binding.md matrix โ no bare bound method, no lambda: self.X()
- Rules 7-8:
OnPressEscapeKey() returns True; OnMouseWheel() returns True/False
- Rule 9: all user-visible strings via
localeInfo.*/uiScriptLocale.*
- Rule 11:
"not_pick" on all decorative elements
- Rule 13: parent bounds contain all interactive children
- Rule 12: z-order = back-to-front SetParent order
- Rule 15: image paths verified via Glob (or
# TBD ASSET: ...)
- Rule 16: C++ API calls verified in
reference/bindings.md (or # TODO: verify ...)
- Rule 14: Python 2.7 compatible (
//, in, xrange)
- uiscript dict filename matches the
LoadScriptFile() arg in the root class
- Rule 3:
Destroy() calls Initialize(); script-backed also ClearDictionary()
- Rule 4:
__del__ calls ui.ScriptWindow.__del__(self)
- Alignment resolved โ for every widget with
all_align or centered horizontal_align/vertical_align, mentally resolve the FINAL screen position: all_align re-anchors (x, y) as an offset from PARENT CENTER, not absolute coords. Never all_align on a child whose y is meaningful as absolute. (See reference/widgets.md text section.)
- Rect within parent โ after alignment,
child.x + width <= parent.width and child.y + height <= parent.height; children of board_with_titlebar clear the engine titlebar (y >= 32)
- Rule 17: pre-existing Destroy bodies intact; owned-widget calls guarded with
if self.X: (including inside helper methods the body calls)
- Rule 18: all new
.py content ASCII-only (carve-outs per rule: pre-existing non-ASCII, verbatim user content, locale files)
- Rule 19: every Pattern B / Pattern E site with extra args checked against the actual setter signature in
ui.py (augmented or downgraded to Pattern C)
Optional second pass: for high-stakes generations (screenshot mode, multi-file edits, gated windows) or when the silent review feels like cargo-cult, dispatch the m2ui-pre-emit-reviewer subagent before emission โ independent audit, cites file:line, proposes no fixes. Distinct from diagnose mode (which audits user-supplied files).
After Code Generation
Always emit an interfacemodule.py integration snippet: import (feature-flag-guarded if applicable), instance creation, tooltip binding if applicable, BindInterface(self) if needed, toggle method, Destroy() in cleanup, Hide() in HideAllWindows. Shape + lazy-init/gated-toggle/tooltip variations: reference/integration.md.