Super Mango manualPlay

Level Editor


Super Mango includes a standalone visual level editor (out/super-mango-editor) that lets designers place, move, and configure entities on a scrollable canvas, then save the result as a TOML level file that the game engine loads directly.

make editor        # build the editor binary → out/super-mango-editor
make run-editor    # build game + editor, then launch editor

Window Layout

┌─────────────────────────────────────────────────────────────────────────┐
│                         Toolbar (32px)                                   │
├──────────────────────────────────────────────┬──────────────────────────┤
│                                              │                          │
│              Canvas  (896 × 656 px)          │    Panel  (384 × 656 px) │
│                                              │                          │
│   Scrollable level preview — WYSIWYG with    │  ┌────────────────────┐  │
│   the running game. Entities drawn at their  │  │   Entity Palette   │  │
│   exact game sizes and positions.            │  ├────────────────────┤  │
│                                              │  │   Properties       │  │
│                                              │  │   Inspector        │  │
│                                              │  ├────────────────────┤  │
│                                              │  │   Level Config     │  │
│                                              │  └────────────────────┘  │
├──────────────────────────────────────────────┴──────────────────────────┤
│                         Status Bar (32px)                                │
└─────────────────────────────────────────────────────────────────────────┘
  Total: 1280 × 720 px
Area Size Contents
Toolbar 1280 × 32 px Tool selector (Select / Place / Delete), Grid and Debug toggles, zoom dropdown, Campaign (lit while the Campaign View is open; clicking it then closes the view), Contrast (lit while on: every panel, button and field switches to a high-contrast style, black with white outlines and text at 7:1 or more; the level canvas keeps its colours; remembered for the next session), Fog (lit while the fog layers are drawn over the canvas), file buttons (New, Open, Save As, Save), Play (Stop while a playtest runs). The current tool and every toggle that is on (Grid, Debug, Campaign, Contrast) are drawn in the selected colour, with a yellow outline in high contrast
Canvas 896 × 656 px Scrollable level view with zoom. All entity types drawn at game-accurate sizes, over the level’s background layers drawn as the game draws them (each scrolls with the view at its own speed); the toolbar’s Fog toggle adds the fog layers (held still); the space outside the level (left of 0, past its last screen, below its height) is darkened
Panel 384 × 656 px Level config (at most half the column), entity palette and properties inspector, as collapsible sections that follow the tool: with an entity selected and the Select or Delete tool, the palette folds to its title (clicking it takes up Place) and the selection’s properties fill the rest of the column, scrolling when they are taller; with Place, the palette fills the column above a 200 px properties panel
Status bar 1280 × 32 px Cursor world coordinates, snap-to-grid state, active tool, validation summary, entity count (every placement except the player spawn), current filename with * when modified, latest status message (a message too long for the bar ends in ...; rest the pointer on it to read all of it, as with a long validation message in the side panel); under the canvas, in its bottom rows, the minimap

The window opens at 1280 × 720 and can be resized or maximised, but not made smaller than that. The layout does not reflow: the editor always draws this 1280 × 720 layout and scales the whole of it by the largest whole number that fits the window, centred with black margins, so every pixel stays square and sharp. A window from 1280 × 720 up to just under 2560 × 1440 (a maximised window on a 1080p monitor, for example) shows it at 1x; 2560 × 1440 or larger shows it at 2x. On a Retina Mac the window is measured in points, so the default size already draws at full pixel density.

While another window has focus (the playtest game, a file picker, your browser) the editor draws 10 frames a second instead of 60, and goes back to 60 as soon as it has focus again. With focus but no key, click, wheel or pointer movement for 2 seconds it draws 30 frames a second, since nothing in it animates but the caret’s half-second blink; the next input brings back 60 within one frame. A held mouse button never counts as idle. It keeps running rather than waiting for input, so autosave, tooltips and the playtest status check work as before; they time themselves by the clock, not by frames. A hidden window (the --smoke-test render) keeps its own pacing.

The mouse pointer changes with what it is over: an I-beam on a field you can type in, four-way arrows while entities are being dragged, and up-down arrows while a scrollbar’s thumb is held.

Minimap

A 7-pixel strip in the status bar’s bottom rows, under the canvas (editor_minimap.c), shows the whole level squeezed to the canvas’s width: the floor with its gaps cut out, one flat rectangle per entity, and a frame (the UI style’s accent colour) around the part the canvas shows. It sits in rows the status text leaves empty, so it covers nothing of the level or the panels; an overlay on the canvas would hide the level’s floor, where much of the editing happens. Click the strip to centre the canvas there, or hold and drag to slide the view along the level; the zoom and the vertical scroll stay as they were. It is hidden while the Campaign view is open.

Entity colours are by palette category and stay the same in the high-contrast style, since they stand for level content rather than UI (the strip’s background and the frame follow the UI style):

Category Colour
World (rails, checkpoints, the player spawn) grey-blue #96A0B9
Collectibles (coins, stars, the Last Star) gold #F0C85A
Enemies red #EB5555
Hazards orange #FF9628
Surfaces (platforms, bridges, float platforms, bouncepads) cyan #50BEC8
Decorations (vines, ladders, ropes) green #5AAA46
Floor brown #785A3C, with floor gaps in the background colour

Resting the pointer on a toolbar control for half a second shows a tooltip saying what it does and its shortcut, if it has one (Save (Ctrl+S), Show the grid (G)). Clicking the control hides the tip until the pointer moves to another one.

F1 shows every keyboard and mouse shortcut in a panel over the editor, drawn in the UI style (so the Contrast toggle applies). While it is up, keys, typing, clicks and the wheel change nothing beneath it; F1 or Esc closes it, and so does a click, which does nothing else. The list and the toolbar tooltips come from one table (src/editor/editor_shortcuts.c), so they cannot disagree; a shortcut added to editor_events.c needs its row there.


Tools

Three interaction modes are available via the toolbar or keyboard shortcuts:

Tool Key Behaviour
Select 1 Click an entity on the canvas to select it. Drag to reposition. Selected entity appears in the Properties panel. Clicking empty space clears the selection; dragging from empty space draws a selection box.
Place 2 Click the canvas to stamp a new entity of the type chosen in the palette; hold the button and drag to paint a row, one entity each time the pointer has moved the entity’s own width or height (on the grid when snapping), up to 64, as one undo step (Esc stops the row early). For the two singletons (Player Spawn, Last Star) the click moves the existing one. A new spike block attaches to the rail nearest the click.
Delete 3 Click an entity to remove it from the level immediately. Clicking a member of a multi-selection removes the whole selection.

Selecting Several Entities

Several entities can be selected at once (up to 64, EDITOR_MAX_SELECTION):

Action Input
Box select Press on empty canvas and drag: every entity the box touches is selected
Add to the selection with a box Shift + drag from empty canvas
Add / remove one entity Shift + click it
Select just one member again Click it without dragging
Clear the selection Esc, or a click on empty canvas

Dragging any selected entity moves the whole group (the grabbed one snaps to the grid; the others keep their distance), and the arrow keys nudge it. Delete or Backspace, Ctrl+C / Ctrl+V and Ctrl+D act on every selected entity. Each of these is a single undo step, however many entities it touched, and redoing a paste or duplicate selects all the copies again. The Delete tool and right-click also act on the whole selection when the entity they hit is part of it (on anything else they delete just that entity). A rail deleted together with the spike blocks and platforms riding it goes after them, so the pair can be deleted (alone, a ridden rail is refused). The Properties panel shows N selected with a count per type instead of fields.

Dragging keeps the point you grabbed under the cursor and starts only after the cursor moves 3 canvas pixels, so a plain click selects without moving anything. Patrolling enemies and saws carry their patrol range with them, and positions are clamped so a drag cannot produce a level that fails validation. While the button is held, keyboard commands (Undo, Delete, Paste…) and right-click quick-delete wait; Esc cancels the move and puts the entity back. Axe traps and saws store y = 0 for “default height”, so dragging one to the very top row stores 1 px instead.

Spike blocks and rail-mode float platforms refer to rails by position, so the editor refuses to delete a rail that one of them rides (the status bar names what still uses it). Deleting an unused rail renumbers the references to later rails, and undo restores the original numbering.

The Delete key, or Backspace (laptops without a Delete key), removes the selection; inside a text field both edit text instead. Backspace deletes the selection only while the canvas has the focus, that is, when your last click was on the canvas: right after Enter commits a field, a Backspace still meant for that field deletes nothing and the status bar says to click the canvas or press Delete, which works wherever the focus is. The arrow keys nudge the selection 1 px, or 16 px with Shift, following the same rules as a drag (floor-bound entities move only sideways, rail riders stay on their rail, and nothing leaves the world or makes the level invalid). A quick run of nudges of the same selection is one undo step; after a one-second pause the next nudge starts a new step. Right-click quick-deletes an entity in any tool (ignored while a drag is in progress). Clicks pick the entity drawn on top: the hit test (src/editor/hit_test.c) walks the exact reverse of the canvas draw order, so enemies and hazards win over collectibles, and those over surfaces and ground pillars. To reach an entity hidden underneath, Alt+click the spot, or click the very same spot again without moving: each such click selects the next entity down and wraps back to the top. The status bar says which one is selected (Selected Coin (2 of 2 here)). (Some Linux window managers take Alt+click for themselves; the second click works everywhere.) Esc cancels a field edit, returns to Select, or clears the selection. Printable shortcuts do not switch tools while a text field is active; Ctrl+C / Ctrl+X / Ctrl+V inside a text field copy, cut and paste the selected text. File/history shortcuts accept Command on macOS as well as Ctrl.

If a hand-edited file or a property field leaves the level invalid, the canvas keeps drawing it with a red banner along the canvas’s bottom edge, The game cannot load this level yet: <error> (click it to go to the field, as the status bar’s summary does), and stays editable; an edit is refused only when it would add an error the level did not have. Only a problem the preview cannot draw past (an array count, the screen count, or the size of a platform, rail, spike strip, bridge, pad, ladder or rope: level_validate_preview) pauses the canvas: clicks then report Canvas paused: <error> in the status bar until you fix the field in the side panel or press Ctrl+Z.


Entity Palette

The right panel lists all 31 placeable entity types (ENT_COUNT) in collapsible categories. Names, categories and order come from one table in src/editor/entity_meta.c:

Category Entities
World Player Spawn, Floor Gap, Checkpoint, Rail
Collectibles Coin, Star Yellow, Star Green, Star Red, Last Star
Enemies Spider, Jumping Spider, Bird, Faster Bird, Fish, Faster Fish
Hazards Axe Trap, Circular Saw, Spike Row, Spike Platform, Spike Block, Blue Flame, Fire Flame
Surfaces Platform, Float Platform, Bridge, Bouncepad Small, Bouncepad Medium, Bouncepad High
Decorations Vine, Ladder, Rope

Palette rows are text labels; clicking one selects that type and switches to the Place tool. On the canvas, the placement ghost under the cursor uses the in-game sprite; which sprite, which part of its sheet and what size come from the same table row. Checkpoints use the editor’s primitive marker instead, because they have no sprite asset.


Properties Inspector

When one entity is selected with the Select tool, the Properties panel displays its editable fields (with several selected it shows how many of each type instead). All fields match the TOML schema exactly — what you see in the inspector is what gets written to the file. The labels read as words (Patrol left: for patrol_x0, Start frame: for frame_index); resting the pointer on one shows what the value means, its unit and the TOML key, for example Left end of the patrol, in pixels (patrol_x0).

Example — Spider:

  • x, vx, patrol_x0, patrol_x1, frame_index

Example — Float Platform:

  • mode (dropdown: STATIC / CRUMBLE / RAIL)
  • x, y, tile_count, rail_index, t_offset, speed

Example — Axe Trap:

  • pillar_x, y
  • mode (dropdown: PENDULUM / SPIN)

Example — Checkpoint:

  • x, y
  • screen is derived from x; the inspector labels its runtime purpose: “Respawn when crossed.”

Changes take effect immediately on the canvas (WYSIWYG).

While a number field is being edited, Up and Down add or take away one step (Shift: ten steps), stopping at the field’s limits. A whole-number field steps by its own step (a floor gap’s x moves by one 16 px floor piece), otherwise by 1. A decimal field has no step of its own, so it moves by 1 when its value is 10 or more (positions and speeds, in pixels) and by 0.1 below that (parallax speeds and other factors); a speed that must not be 0 steps over it. The stepped value is staged like typing: Enter, Tab or a click elsewhere applies it as one undo step, and Esc discards it. Text fields ignore Up and Down.

Checkpoint Workflow and Feedback

  1. Open the World palette category, choose Checkpoint, select Place, and click the intended respawn point.
  2. Use Select to drag it or edit its x and y fields. Delete, copy/paste, undo, and redo use the same workflow as other non-singleton placement records.
  3. Save or playtest only after validation succeeds. A checkpoint must be after the effective player start, must have a unique in-world x, and must have an in-world y; invalid records block save and playtest (autosave keeps the last valid version).

The canvas draws a labelled CP n marker. Valid markers are amber, hovered markers brighten, the selected marker is blue, and invalid markers are red. The status bar reports Checkpoint placed, Checkpoint moved, or Checkpoint deleted. In the running game, crossing a valid marker produces a brief CHECKPOINT CP n HUD notice; a death respawn is labelled RESPAWN CP n. The debug inspector exposes the current checkpoint index after those temporary notices expire.


Level Config Panel

The Level Config section in the right panel starts with the validation summary and its messages, then the recent-files list (Ctrl+1 to Ctrl+5), then the level-wide TOML fields. Each validation message is clickable (it highlights under the cursor): a message about an entity, such as checkpoints[0].x is ..., selects that entity (checkpoints, the Player Spawn and the Last Star included), switches to Select, pans the canvas to centre it and focuses the property the message names (spiders[2].vx puts the caret in that spider’s vx field; a dropdown such as an axe’s mode opens its list); a message about a Level Config value focuses that field, unfolding its group and scrolling the panel to it (a layer’s asset dropdown opens, fog_layers[1].speed takes the caret). Clicking the Validation: ... summary in the status bar does the same for the first message. The panel shows eight messages at a time; when there are more, a Rows 1-8 of N line says so and the mouse wheel over the messages scrolls through them (elsewhere in the panel it scrolls the panel). The message clicked last stays highlighted, and Ctrl+C copies its text until you click somewhere else, even when the click put the caret in the field it names. The level-wide fields are:

  • name, description, generated_by. The description may span several lines: Shift+Enter starts a new one. A field shows a line break as and keeps to its own row; text too wide for it ends in ... until you click it. An empty field shows a dim hint, such as (none: the last level) for the next-phase path.
  • screen_count (1–99; world width = screen_count × 400 px)
  • next_phase path (saved under [last_star] in TOML; it must follow the level-reference rule)
  • music_path (dropdown: none, water, lava, winds) and music_volume (0–128)
  • floor_tile_path (dropdown)
  • initial_hearts (0–3), initial_lives (0 in either means the game’s default of 3)
  • score_per_life, coin_score (0 = the defaults, 1000 and 100)
  • A collapsible Movement Physics group for the optional [physics] overrides

The player start and the floor gaps are canvas entities: move the Player Spawn marker, and place or delete Floor Gap entities. A floor gap’s x field rounds to the 16 px floor-piece grid.

Inside an active field, Left / Right move the caret one character, Home / End jump to either end, typing inserts at the caret, and Backspace / Delete remove the character before / after it (whole UTF-8 characters, so é never splits in half). Ctrl+Left / Ctrl+Right jump to the start of the previous word or the end of the next one. Holding Shift with any of these caret keys selects the text the caret passes over; Ctrl+A selects the whole field, and in a field already being edited a click moves the caret and a drag selects (the click that starts editing a field leaves the caret at the end, ready to append). Typing, Backspace, Delete or a paste replace the selection; Ctrl+C and Ctrl+X copy or cut it, and with nothing selected they copy nothing and the status bar says how to select. Tab applies the field and moves to the next field of the side panel, Shift+Tab to the previous one (wrapping at either end); a value that cannot be stored keeps the focus where it is. Dropdowns (a rail’s layout, an axe’s mode, the music track…) have their place in that order too: Tab onto one opens its list, Up / Down (or Home / End) move the highlight, Enter picks the highlighted option as one undo step, Esc closes the list without a change, and Tab / Shift+Tab close it and move on.

Leaving a field applies what you typed: Return, a click on the canvas or another field, or any command (Save, Undo, a shortcut) stores a valid value as its own undo step, with no prompt. Only a value that cannot be stored (12a, a lone -) asks Keep Editing / Discard; Keep Editing leaves the field open and the command waits. Esc always discards the typed text. Number fields with a range clamp the value however the edit ends. A bouncepad’s launch_vy is kept at least as strong as a jump, a rail rider’s speed above 0 and at most the rail speed limit, and an enemy’s patrol range at least as wide as its sprite. An enemy’s vx is limited to MAX_PATROL_SPEED (960 px/s) either way, and a typed 0, which would freeze the enemy, is not stored: the field stays open like any value that cannot be stored. When a field limits or refuses what you typed, the status bar says so (-99999 is below -960, the smallest this field takes; it was limited). Newly placed enemies start at their usual speeds, which are valid. Switching a float platform to Rail gives it a speed of 3 if it had none, and needs a rail in the level. A Static or Crumble platform keeps its old rail_index, which deleting rails does not renumber; if that number names no rail any more, the switch attaches the platform to the rail nearest it. Either way the status bar names the rail it now rides. In Rail mode the rail_index field only accepts existing rails.

A dropdown whose current value is not one of its options (a path typed into the TOML by hand) shows ---, and any option can then be picked. While a dropdown list is open, the next click only picks an option or closes the list; it never reaches the canvas or the fields under the list.


Camera Controls

Action Input
Pan left / right Mouse wheel over the canvas, or a trackpad’s sideways swipe
Pan up / down Shift + Mouse Wheel over the canvas (at 3× and 5× the 300 px world is taller than the canvas); macOS reports Shift+wheel as horizontal scroll, which also pans up/down here
Drag the view Hold the middle mouse button, or Space with the left button, and drag over the canvas (the level follows the pointer; nothing is placed or selected)
Change zoom Toolbar dropdown (zooms around the canvas centre) or Ctrl + Mouse Wheel (zooms around the cursor; ¼×, ½×, 1×, 2×, 3×, 5×, stopping at ¼× and 5×)
Zoom to fit F, or Zoom: Fit in the toolbar list: the largest zoom that shows the whole level, scrolled to its start; a level too long even for ¼× gets the exact zoom that shows it (the status bar gives it in per cent)
Select everything Ctrl+A on the canvas (up to 64 entities, the most a selection holds; the status bar says when there are more)
Cut Ctrl+X: copy the selection, then delete it (Ctrl+V pastes it back)
Snap to grid on / off S (status bar shows Snap: on / Snap: off). Placing and dragging then put the entity’s corner on the 48 px grid
Snap just this once (or not) Shift does the opposite of the S setting, read at each mouse event. Place: hold it as you click; the entity lands the other way. Select: press it only once the drag has started (Shift at the press adds to or takes from the selection instead of dragging); while it is held the move snaps the other way, letting go mid-drag switches back, the entity follows at the next mouse movement, and where it is when the button comes up is where it stays
Toggle grid G (the 48 px cells and the floor line, inside the level; each screen’s boundary and number show either way)

The canvas renders the level in WYSIWYG — entity positions and sizes match the game exactly at zoom 1.0 (logical pixel = 1 canvas pixel). At zoom 2.0 each logical pixel maps to 2 canvas pixels. One wheel notch pans 48 canvas pixels and scrolls a side panel 20 px. A side panel or the Campaign view’s list that holds more than it shows has a scrollbar at its right edge: drag its thumb, or click above or below the thumb to move one view. Trackpads report fractions of a notch; the editor adds them up, so small swipes scroll the side panels, the validation list and the Campaign view’s rows too, and a Ctrl pinch or swipe changes zoom once per whole notch.


Undo / Redo

The editor keeps an undo stack for placement, movement, deletion, property and Level Config changes. It holds the latest 256 steps (UNDO_MAX); older ones are dropped. An action on several entities at once (a nudge, move, delete, paste or duplicate of a multi-selection) records one entry per entity, up to 64 (UNDO_GROUP_MAX), in a single step: it costs one of the 256 slots, undoes and redoes in one go, and the oldest step is always dropped whole. If the editor cannot get the memory to record such an action, it refuses the action and the level is left unchanged. A new edit clears the redo stack; an edit that is refused (a paste into a full array, say) is not an edit and leaves it alone.

Action Shortcut
Undo Ctrl+Z
Redo Ctrl+Y (or Ctrl+Shift+Z)

The undo stack is in-memory only — it is cleared when a new file is opened or created. Undoing back to the saved contents clears the modified marker.

Each undo or redo names the step in the status bar, in the Campaign view’s words: Undid: moved Coin, Redid: placed Coin (3) for a group of one type, Undid: deleted 4 entities for a mixed group, Undid: changed level settings for Level Config. With nothing left it says Nothing to undo (or Nothing to redo). When the entity the step changed last is off the canvas, the view scrolls to centre it, as clicking a validation message does (keeping the zoom); an entity the step removed has nothing to show, so the view stays put, and so it does when the entity is already in view.

The Campaign View keeps a separate, smaller history for its own edits. While it is open, Ctrl+Z and Ctrl+Y act on that history and leave this one alone.

The editor keeps recent files (the last 5), its settings (editor_settings.txt: the Contrast toggle) and recovery snapshots for modified levels in its OS preference directory, retaining the Super Mango/Editor/ organization/application suffix. Autosave runs every 30 seconds while the level is modified. Recovery snapshots must load through the same validation as any level, so while the current level has validation errors autosave writes the most recent valid version of the document instead (“Autosaved last valid version”); if nothing newer than the saved file is valid, it skips that round. Every attempt, successful or not, restarts the 30-second timer, so a failing autosave reports Autosave failed; retrying in 30 s at most once per interval.

The folder holds at most 32 recovery copies. When leftover copies fill it, autosave pauses with Autosave paused: 32 old recovery copies fill the folder; Ctrl+R to recover or discard them instead of failing silently. Ctrl+R shows one copy at a time (source file and time) with Cancel / Recover / Discard; with several copies the third button is More…, which offers Back / Discard… / Next, and Discard… asks Back / This copy / All N copies. Discard deletes that copy’s files; All N copies deletes every copy offered (never another running editor’s live copy) and closes the picker. Each copy records the process id of the editor that wrote it and when that editor started: a copy whose editor is still running (a second editor window, say) is that editor’s live work and is never offered or deleted by another one. The start time tells a crashed editor from an unrelated program that later got the same process id, so such a crash copy is still offered, on Windows, Linux and macOS alike. Only when the system will not tell the editor that start time (a program run by another user, on macOS and Windows) is the process id compared alone, and the copy reappears once that program ends. A snapshot file that lost its .meta description can never be offered, so the editor deletes such orphans when it starts, once they are more than five minutes old (a younger one may be another editor’s first snapshot, whose .meta is written right after it).


Copy / Paste

Action Shortcut
Copy the selection Ctrl+C
Paste (offset from original) Ctrl+V
Duplicate the selection (offset, clipboard untouched) Ctrl+D

Ctrl+C copies the whole selection. Each pasted entity is moved a little so it does not hide the original: exactly where dragging it 24 px right and 24 px down would put it (things whose height comes from the floor or water move only right, floor gaps one whole gap width), except rail riders, which step along their rail, and the pasted copies become the selection. A rail copied together with its riders pastes as a new rail carrying the copied riders. A paste is all or nothing: if one copy cannot be added, none is. Each further Ctrl+V steps one more offset from the previous copy, so repeated pastes lay out a row instead of stacking on one spot; a new Ctrl+C starts again from the copied entity.

Ctrl+D duplicates the selection without touching the clipboard: the copy moves by the same offset a paste uses and becomes the selection, so pressing Ctrl+D again keeps stepping. A rail rider stays on its own rail. The Player Spawn and the Last Star exist once per level and are not duplicated.

Place, drag and Paste clamp entities into the world (patrol ranges included, and a pasted spike block’s t_offset wraps onto its rail). Paste is refused while the level has validation errors. A copied rail rider remembers the rail it rode, so a paste re-attaches it to that rail even after the rail was moved or other rails were deleted, and never to an identical rail next to it. Pasted into another level, it attaches to a rail with the same shape and position. When an entity cannot be added — its array is full, its rail is not in this level (or was deleted), or the result would fail validation, such as a checkpoint behind the player start — the status bar explains why and the level is left unchanged.


Play-Test Integration

The Play button or F5 validates the active LevelDef, serializes it to a private TOML snapshot in the editor preference directory, then launches the sibling game executable with --no-save --level <snapshot>. It does not overwrite the open source file or use your personal game profile. Validation errors block playtest and appear in the validation summary/status feedback. While the game runs, the editor draws only a “Playing” overlay with a Stop button: the canvas, panels and keyboard shortcuts are inactive, so the document cannot change under the running snapshot. Clicking Stop (or quitting the editor) asks the game to exit, waits up to one second, then force-stops it and reaps the process; closing the game window also returns to the editor. Either way the temporary playtest file is removed.

# Run an already-saved level with the same profile isolation:
./out/super-mango --no-save --level levels/your_level.toml

Enabling Debug Mode in the toolbar adds --debug to the game launch, showing collision boxes, FPS counter, and the event log.

Playtest From Here

Shift+F5 playtests from a point instead of the level’s start. With exactly one checkpoint selected, the game starts on that checkpoint (--start-checkpoint <n>); otherwise it starts with the player centred on the world x under the mouse (--start-x <px>), standing on the highest surface there: the ground floor, a pillar, a bridge, or a fixed or crumbling float platform. Surfaces are found with the player’s physics box, the same way collision finds them, so an x just beside a pillar starts on its top. The selected checkpoint’s properties also have a Playtest from here button. The point is checked with the game’s own rules first, so pointing over a floor gap with nothing above it, or away from the canvas with no checkpoint selected, is refused in the status bar (Playtest from here: nothing to stand on at x 416 (a floor gap)) and nothing is launched. In the game the start point is also where a lost life comes back to, until a later checkpoint is crossed; Retry after Game Over starts the level from its own start (see the runtime flags). A run that starts part-way through records no best time, ghost or Continue point (editor playtests also pass --no-save).


Campaign View

Ctrl+M or the toolbar’s Campaign button opens levels/campaigns/main.toml, the list of levels the game’s start menu offers, in place of the canvas. Each row shows the level file, its name (the menu’s label, editable in place) and the game’s verdict on it: ok, or why the start menu would grey it out (next_phase is out of campaign order, final level has a next_phase, level file is invalid…). A problem with the campaign as a whole (a repeated level, nothing playable) shows in red above the list. The verdicts come from the same C rules the game applies when it loads the manifest (campaign_catalog_check in src/levels/campaign_catalog.c), rerun after every edit.

Button Action
Up / Down Move the selected row (click a row to select it)
Remove Take the selected level out of the campaign; its file is not touched
Add level… Pick a level file with the native picker; it must sit directly in levels/ (the level-reference rule) and not be listed already
Link in order Set every listed level’s [last_star].next_phase to the row below it, and clear the last one’s, so the chain matches the order shown
Undo (Ctrl+Z) / Redo (Ctrl+Y or Ctrl+Shift+Z) Take back the last edit made in the view, or make it again (see below). Each is drawn dim, and takes no click, while there is nothing for it to do
Save (Ctrl+S) Write each level whose name or next_phase changed, and the manifest, to temporary files first; replace the real files only once all of them are written
Revert Throw away every unsaved change in the view (a name being typed included) by reading the manifest and its levels again; with unsaved changes the first Revert only warns, the second reverts
Close (Esc, Ctrl+M, or the toolbar’s lit Campaign button) Back to the level; with unsaved campaign changes the first Close (or quit) only warns, the second discards them. A first Esc while a name is being typed only cancels the typing

Save is refused while the rules find something this view can fix: a campaign-wide problem, or a level that is out of order or has no name (Link in order fixes the order). A listed file that does not load is the file’s own problem and may stay listed; the menu shows it disabled. Before writing anything, and again right before replacing the files, Save checks that every file it would rewrite (each changed level and the manifest itself) still holds what the view read when it opened, so a file changed by another program is never overwritten; it also refuses to rewrite the level open in the editor while that level has unsaved changes, and reloads it afterwards when it was clean. A failure while writing the temporary files changes nothing on disk. The replacing itself is one quick rename per file, so it can fail half-way only if the system refuses a rename (a full disk, a file locked by another program on Windows); the status bar then says how many files were written and which one failed (Campaign partly saved: 2 of 4 files written; writing levels/b.toml failed), and the next Save writes the rest. If Windows moved a file aside and then could not move the new one in, the temporary file is the only complete copy: it is kept, and the status bar names it (Campaign partly saved (1 of 4 files written). Save incomplete: the new levels/b.toml is safe in <path>). A name typed but not yet applied counts as an unsaved change, so the title shows * and quitting asks first. When the list is longer than the view, the mouse wheel over it scrolls the rows. While the view is open, clicks on it never reach the canvas and level shortcuts are ignored.

The buttons sit in one row along the bottom, in the table’s order.

Undo and redo. The view has an undo history of its own, apart from the level’s. Each edit made in it is one step: moving a row, removing one, adding a level, renaming one (one step for each name applied, however many keys it took) and Link in order. The Undo button or Ctrl+Z takes back the newest step and the Redo button or Ctrl+Y (or Ctrl+Shift+Z) makes it again; the status bar names the step (Undid: moved levels/c.toml). A button and its key do the same thing; the one difference is that with nothing to undo or redo the button is dim and ignores the click, while the key answers Campaign: nothing to undo. The history holds the latest 64 steps (CAMPAIGN_HISTORY_MAX); older ones are dropped. A new edit clears what could be redone. A name being typed is applied first, as its own step, so Undo while typing takes that name back and Redo returns it (Undo is therefore not dim while a changed name is being typed). While the view is open these keys never touch the level’s undo stack; once it is closed they undo the level again.

A step records what was done, not a copy of the level, so a row that left the list (a removed level, or an added one that was undone) comes back by reading its level file again. A removed row also gets back the name and next_phase it showed if those had not been saved yet. If the file no longer loads, the step is refused, the list stays as it is and the status bar says why (Cannot undo: levels/b.toml no longer loads. Put the file back, or Undo again to skip this step); the same Undo works once the file is back. Link in order needs memory to remember the old links: if the editor cannot get it, no link is changed (Not linked: out of memory).

Skipping a refused Undo. A refused Undo stands in the way of every older step, so it can be skipped instead of waiting for the file or using Revert. While Undo is stopped there, the view says so in red above the buttons (Undo is stopped: levels/b.toml was removed and no longer loads. Put the file back, or press Undo again to skip that step.) and the button reads Undo... with a red line under it. The first refusal never skips anything. From the second Undo on (button or key, the file still not loading) the editor asks, with three buttons: Cancel, Keep the step and Skip this step. Keep the step is the default, so pressing Enter changes nothing; only the answer Skip this step drops the step. Keep the step, Cancel, closing the dialog, or a dialog that cannot be shown all leave the history as it was. The status bar then reads Undo step kept: levels/b.toml no longer loads. Put the file back, then Undo, or Undo step kept: confirmation failed when the dialog could not be shown. Skipping changes nothing in the list: the removed level stays out of it for good, and a name or link changed in its row without saving is forgotten. The status bar names what was skipped (Skipped: removed levels/b.toml (it stays out of the campaign)).

The older steps were recorded while that level was still listed, so their row numbers counted it. Skipping rewrites them for a list without that row: each one still undoes and redoes exactly what it did to the other levels, one row higher where it acted below the lost one. A step that acted on the lost level alone (its rename, a move of it or past it, a relink that changed only its link) has nothing left to do and goes with it, and the status bar counts those (..., and 2 older steps that changed nothing else). If the level was added in a step the history still holds, that Add goes too. Undoing everything after a skip therefore arrives at the campaign as it was, without the lost level. A refused Redo (an undone Add whose file is gone) is not skipped: any new edit clears the steps to redo.

The * in the title compares the view with the files, so it follows the history: undoing back to what the files hold clears it, and undoing past a Save sets it again. Save keeps the history. Revert and closing the view clear it; Revert is still the way back to the files on disk in one go.


File Operations

Operation Shortcut / Button Notes
New Ctrl+N / New button Prompts to save if modified
Open Ctrl+O / Open button Opens a native file picker
Save Ctrl+S / Save button Overwrites the current file
Save As Ctrl+Shift+S / Save As button Native file picker for new path
Recover autosave Ctrl+R Recover or discard a leftover recovery snapshot
Recent file Ctrl+1 through Ctrl+5 Open a recent file
Open by drag-and-drop Drop one .toml file on the window Opens it like a recent file; closes the Campaign view first
Campaign view Ctrl+M / Campaign button Edit the campaign manifest (see Campaign View)

The title bar and status bar show an asterisk (*) after the filename when there are unsaved changes. New, Open, a recent file, a dropped level, recovery and quit first ask Save / Discard / Cancel when the level has been modified; Save continues only if the save succeeds.

These questions, and the editor’s other two- and three-button ones (an invalid field value, replacing a file, a file changed on disk, the Recover picker, skipping an Undo step), are asked inside the editor window: the editor dims behind a box with the question and its buttons. The highlighted button starts on the safe default and Enter (or Space) presses it; Tab, Shift+Tab and the arrow keys move the highlight; Esc, or closing the window, cancels; a click (press and release on the same button) chooses that button. The editor waits until one is chosen. Only the file pickers (Open, Save As) are still the system’s own dialogs.

A drop opens a level only when it is exactly one file ending in .toml (in any letter case); two files, or a picture, leave the document alone and the status bar says Drop one .toml level to open it. Drops are ignored while a playtest runs. If the Campaign view has unsaved changes, the first drop only warns (Save them, or drop the level again to discard them) and a second drop closes the view and opens the level. raylib copies a dropped path into a fixed buffer (256 bytes on Windows, 4096 elsewhere) and cuts a longer one short, so a path that fills it is refused; open such a file with Ctrl+O.

Saved files are plain TOML — they can be edited in any text editor and immediately reloaded in the editor or game.

Saves write a sibling temporary file, flush it, then atomically rename it over the destination and (on macOS/Linux) sync the containing directory so the new entry survives a crash. On macOS/Linux a newly created level gets ordinary document permissions (0666 minus your umask, usually 0644), while recovery snapshots and playtest copies stay private to you (0600); replacing an existing file keeps its permissions, and its owner and group where the system allows it. On Windows the replace can stop halfway (the old file already moved aside, the new one not yet in place, for example while a virus scanner holds the file); the editor then finishes with a plain move, trying up to five times 100 ms apart, and if every try fails it keeps the temporary file, never deletes it, and names it in the status bar: Save incomplete: your level is safe in <path>. Before replacing a file, Save checks that it still holds what the editor last read or wrote; if another program changed it, Save asks Cancel / Replace / Save As. If the file changes in the moment between that question and the write, nothing is written and the status bar says Save stopped: <path> changed on disk; nothing was written. Save again to replace it or Save As. Save on a symlinked level updates the file the link points to and keeps the link, but only while the link still points at the file the editor opened; otherwise it asks you to use Save As. Save As refuses a destination that is a symbolic link (Save failed: <path> is a symbolic link; choose another name) and refuses the editor’s private recovery and playtest files.

Save, autosave, and Play run editor_validate_level() first. Errors include everything level_validate_runtime() rejects (bad counts, out-of-world placements, invalid checkpoints, unsafe paths, a next_phase that breaks the level-reference rule, a negative screen_count) and, for paths those rules accept, asset or next_phase files that do not exist; no field is reported twice. Like the game, the editor reads screen_count = 0 as the default of four screens. They block persistence and playtest, and the status bar reports Save blocked: <first error>. An empty level name or a Last Star left at the origin are warnings only. The status bar shows Validation: OK or counts such as Validation: 2 errors, Validation: 1 warning or Validation: 1 error, 3 warnings (a zero count is left out); the Level Config panel lists the messages. Every broken rule gets its own message (the game itself stops at the first), so a level with a bad coin and a frozen spider shows both at once; the report keeps the first 256 messages (EDITOR_VALIDATION_MAX_MESSAGES), more than a hand-written level produces, and ends with ... and N more when there are others.

A file that will not open is not loaded (the current document stays), and the status bar says Load failed: <file>: <first problem>. The Level Config panel then starts with Could not open <file>, listing why: a TOML syntax error with its line (line 3: TOML syntax: ...), a schema error with the line of the bad value (line 9: root.coins[1].x has type string, expected finite number), or every runtime rule the file breaks, each with its line. Clicking a row selects it and Ctrl+C copies it, line number included. Clicking that heading hides the list; opening a level or starting a new one clears it.

CI can initialize the editor, render five bounded frames, and exit with:

./out/super-mango-editor --smoke-test

Architecture

The editor uses focused modules in src/editor/ and shared persistence/UI code in src/shared/:

File Responsibility
editor_main.c Entry point — raylib-backed EditorState lifecycle, --smoke-test
editor.c / editor.h Core state struct, init/loop/cleanup, EntityType enum (31 types), EditorTool, EditorCamera, Selection
editor_frame.c, editor_events.c One frame (validation, autosave, drawing) and keyboard/mouse/wheel event routing, including every shortcut
editor_chrome.c, editor_panels.c, editor_layout.c Toolbar, status bar and side-panel layout
editor_minimap.c The minimap strip in the status bar: drawing and click/drag scrolling
editor_shortcuts.c The shortcut table behind the toolbar tooltips and the F1 shortcut list
editor_files.c, editor_session.c Open/save/Save As, playtest copies and recent files; dirty tracking, document hash and confirmation prompts; the focus-dependent frame cap (editor_pace_frames, called by the editor’s frame and a prompt’s)
editor_recovery.c Autosave every 30 s while dirty, and crash recovery: finding leftover snapshots, the Recover picker, loading and retiring them
editor_playtest.c Launching, stopping and reaping the playtest game process
editor_campaign.c The Campaign view: editing levels/campaigns/main.toml with the game’s campaign rules (src/levels/campaign_catalog.c), and the view’s own undo history
editor_clipboard.c, editor_undo_apply.c Copy/paste, and applying undo/redo commands to the level
editor_validation.c / editor_validation.h In-memory level validation report used by status, save, autosave, and playtest
canvas.c / canvas.h Level preview rendering, canvas_screen_to_world, grid overlay
palette.c / palette.h Entity palette panel — category rows, type selection, a thumbnail of each entity’s sprite (its first frame, trimmed to the art); a new type to place opens its category and scrolls into view
properties.c / properties.h Property inspector panel — one draw_<type>_properties() function per entity type, chosen from the s_property_panels table, plus Level Config drawn by one config_* helper per settings group
asset_choices.c / asset_choices.h The files each asset dropdown offers. editor_ui_test fails when a file in assets/sprites/backgrounds, foregrounds, levels or assets/sounds/levels is missing from its list; a level path no list names shows its file name in the dropdown
tools.c / tools.h Mouse interaction for Select / Place / Delete tools
hit_test.c / hit_test.h Entity rectangles shared by click hit-testing and the selection outline; reverse-draw-order hit test
entity_meta.c / entity_meta.h The per-type table (names, category, capacity, where the placements live in LevelDef, the placement-ghost sprite) and the shared read/insert/remove helpers used by tools, undo and paste
src/shared/ui.c / ui.h Immediate-mode UI widget library shared with game settings
undo.c / undo.h Undo stack and PlacementData clipboard union
src/shared/serializer.h, serializer_load.c, serializer_load_*.c Public TOML API and staged parsing into LevelDef, including strict checkpoints
src/shared/serializer_save.c, serializer_emit.c, serializer_io.c TOML emission and atomic file persistence
file_dialog.c Native OS file pickers for Open and Save As (macOS osascript, Linux zenity, Windows PowerShell)
dialog_choice.c / dialog_choice.h The two- and three-button prompts, drawn in the editor window by a small frame loop of their own (dialog_choice)

The EditorState struct mirrors the game’s GameState design: one container passed by pointer to every function, owning its raylib render target, textures, level data, camera, tools, undo history and UI state. Text uses raylib’s built-in font, which the UI borrows and never unloads. The standalone editor owns its process window; native file pickers use the existing OS-dialog boundary, and confirmations are drawn in that window.

dialog_choice keeps the old blocking call, so its callers did not change: it runs its own loop of frames (input, draw, present) until a button is chosen. A prompt can open while events are handled or in the middle of drawing (a toolbar button or a field acts when it is drawn). For the second case the frame records what it has bound: EditorState.drawing_target names the render target texture mode is drawing into, and every clip goes through ui_clip_begin / ui_scroll_begin, which remember it in UIState. The prompt ends both, draws into a target of its own over a dimmed copy of the last complete frame (prompt_backdrop, copied by dialog_choice_keep_frame after every frame), and on return binds the target and sets the clip again, so the interrupted frame carries on. A mouse release it reads for a press made before it opened is put back in the input queue for the editor. Test builds answer prompts with queued events; once those run out, the prompt returns -1 (no answer), as it does at once in a hidden window.


Relationship to the Game Engine

The editor and game share LevelDef, the serializer and level validation. Runtime object loading in src/levels/level_loader.c belongs to the game. This means:

  • Both applications use the same file schema. Runtime texture availability and playability still need a playtest.
  • A new entity needs coordinated changes to shared schema/load/save modules, runtime and editor integrations; see Entity Walkthrough.
  • The editor’s canvas draws entities using the same sprite paths as the game — adding a new entity type requires adding its texture to EntityTextures and a render call in canvas_render.

See Level Design — TOML Reference for the full schema of every entity type.