The ePADD — one tab that holds the apps
The console tab strip was a junk drawer. A bridge console carries help, library,
upgrade, quest and, in a dev build, debug before Engineering adds fabricate and
cargo — seven of the eight slots the bar allows, after which the rest roll into
a More (n) dropdown. It could not be ordered either: position was .mast load order,
and the button label was the raw lowercase route path, so the bar read
fabricate upgrade cargo.
The ePADD replaces that with one button. Opening it gives a home screen of app tiles — named, described, grouped, and scoped to the station that should see them.
[ ePADD ][ back ] Artemis T+00:14:22
Ship
[ Away Team ] [ Status 2 ] [ Messages 3 ] [ Cargo ]
[ Fabricate 1 ] [ Upgrades ]
Mission
[ Quests 2 ] [ Library ] [ Help ]
A tile carries an icon, a title, a second line of description, and a badge — a
short live value (3 unread, 2 building, 42/60) that the crew read without opening
anything. That badge is the reason the apps carrying live state do not each need a
panel on the bridge.
An app is a route
An app is a //gui/app/<name> route plus one registration. The route is the screen; the
registration is the tile.
//gui/app/cargo
jump cargo_screen
gui_app_register("cargo", title="Cargo", icon="epadd.cargo",
consoles="engineering", group="Ship", sort=10,
description="Hold manifest")
The two answer different questions, which is why they are separate:
- The route's own
ifsays whether the app is available — it is still the authority, exactly as a tab's condition always was.//gui/app/away_team if away_relevant()keeps the tile off a mission with no landing parties. - The registration says how it is presented, which the route grammar has no room
for (
[\w]+and an optionalifis the whole of it).
An app route with no registration is perfectly legal: it is reachable from another
screen and never appears on the home grid, which is all a "page" ever needed to be. A
registration with no route is reported by name in mast.runtime.log and left off
the PADD — almost always a typo, or a //gui/tab that was never migrated.
If it used to be a //gui/tab, rebuild both
The app table lives in the library and the routes live in a mastlib. A library that
reads the new table beside a mastlib still declaring //gui/tab/cargo gives you a
tile that does nothing when pressed. Rebuild the .sbslib and the .mastlib.
Scoping a tile
consoles takes a comma list, or "*" for every ship console. Names are matched
after normalization, so "engineering" matches the engine's normal_engi.
gui_app_register("airwing", title="Airwing", icon="epadd.airwing",
consoles="hangar", group="Ship", sort=40,
description="Craft, pilots, sortie board")
"*" deliberately does not include the away console. An away team is not everywhere
on the ship, it is somewhere else entirely, and a landing party has no use for the cargo
hold. An app opts in with away=True, or names consoles="away" to go there and
nowhere else.
Groups and order
group is the heading a tile files under. Ship, Mission and Systems render in
that order and anything else lands after them alphabetically; sort orders within a
group, low first, ties breaking on title.
The convention LegendaryMissions follows: Ship is what this hull is telling you (status, messages, cargo, upgrades), Mission is what outlives the hull (quests, lore, help), Systems is tooling.
A live badge
status is a string or a callable, called at build time:
gui_app_register("messages", title="Messages", icon="epadd.messages",
group="Ship", sort=3, description="From the crew, and from home",
status=lm_epadd_unread, away=True)
Anything the callable raises is swallowed and costs only its own badge — a badge must never be able to take the home screen down with it.
Icons
icon is a name for gui_icon_name, never a sheet index, so a mod can re-point a name
at its own art and every tile follows. An unknown name draws nothing and says so once,
so an app can be registered before anyone has drawn its glyph.
Namespace them. The icon registry is one flat namespace and a registration is a
claim on it: a bare brain takes over the built-in glyph for everything else that draws
it. LegendaryMissions claims epadd.* from its own sheet:
gui_icon_add_atlas_grid(media_shared("epadd/icons"), 4, 5, cell=128, names=[
"epadd.cargo", "epadd.fabricate", "epadd.upgrades", "epadd.quests", ...])
Writing an app's screen
An app screen is an ordinary GUI screen. Two helpers give it the PADD's chrome, and both are optional — a list/detail screen that wants the whole sheet reads better without them.
=== cargo_screen
gui_tab_back(CONSOLE_SELECT)
gui_app_chrome("Cargo", "what the hold is carrying")
gui_section(style="area: 0, 109px, 100, 100;")
...
await gui()
gui_app_chrome(title, subtitle=None)draws the title bar in its own band and returns the subtitle widget, so a live screen can update that line in place. Pass""for a line that is empty now but will have text later — the widget is created either way.gui_app_subnav(["brain", "mast"]), called straight after the chrome, puts this app's other screens on the right-hand end of the same bar.
The PADD keeps no history. The one Back in the game is the console's, on the tab
bar, which every PADD screen declares with gui_tab_back(CONSOLE_SELECT) like any other
screen. There is no HOME button on the chrome either: the strip's status region already
opens the home screen.
gui_app_open("cargo") opens an app from code — the same two lines a tab click
runs, so an app opened from the PADD arrives exactly as a tab would have.
The home screen
A mission reaches the PADD by declaring the shell route. sbs_utils ships no .mast of
its own, so this stub is what turns it on; LegendaryMissions carries it in
consoles/epadd.mast:
//gui/app/epadd
jump epadd_home_screen
=== epadd_home_screen
gui_tab_back(CONSOLE_SELECT)
gui_app_home(ship_name=ship.name)
on change gui_app_revision():
jump epadd_home_screen
on change mission_elapsed_text():
gui_app_home_tick()
await gui()
->END
Two details in there are load-bearing:
- A signal does not wake
await gui(). Without theon change gui_app_revision()the home screen is frozen at whatever it said when it was opened: mail arriving never moves the Messages badge, and an app whose route condition turns on never appears. - The mission clock moves the widget, not the page. A repaint a second is every tile
on the sheet over the wire, per console, forever, so
gui_app_home_tick()updates that one widget and touches nothing else.
The grid falls back to a scrolling list when the tiles do not fit — a grid does not scroll and the engine does not clip. The decision reads the client's real height, so it is about the screen and not a count: thirty apps is a list at 1024x768 and a grid at 1920x1080. Columns follow the client's aspect ratio, and a console carrying more than a dozen apps goes dense (six columns, descriptions dropped).
The PADD is always on
It used to be opt-in, behind a setting, and that went when apps became their own
route kind. Switching it off does not fall back to the classic strip — apps are
not on the tab bar at all, so it leaves them with no way in. Declaring the
//gui/app/epadd route is the opt-in.
The identity badge
Above the ship data panel, on every console, the PADD draws who is sitting there and how
much is waiting: Lt Marek (2). The count is apps with something to say, not
messages — "how many of these should I open" is the number a crew member cannot get
any other way. gui_app_identity_text() is what it draws, and it answers None when
there is nothing worth a line, so a mission using none of this gets no empty box.
There is no badge on the main screen. It names the person at a console, and the main screen is the room's.
Messages
The Messages app is the one most missions will touch, because two different things arrive in the same inbox on purpose:
- Crew to crew. Helm texts Engineering. It is a bridge simulator with people sat at separate screens who cannot see each other, and passing a note is half of what they would do if they could.
- From content. A mission, a quest beat or a map sends a letter from a character.
message_send("All hands: contact in ten minutes.", sender="The Captain")
message_mail("Made it to the outer colonies. Mum sends her love.",
to="helm", sender="Your brother")
Addressed to a console, not to a person. A console is what the PADD knows, what a
client is sitting at, and what survives a player disconnecting and coming back; a crew
name does none of those. to="*" is everyone and is the default. Read state is per
console too, kept beside the messages rather than on the client, because consoles get
reassigned and an inbox that forgot what it had read every time somebody swapped seats
would be worse than not tracking it at all.
A message can ask a question. choices are amd_choice dicts, capped at four, and
answering settles the choice, applies its outcomes and posts the reply back into the
thread — which is what makes it read as a conversation rather than a form that was
filled in. Two consoles pressing in the same frame cannot both apply the outcome: the
arbitration token moves before the outcomes run.
Mail written in AMD
A mission's letters belong in an .amd file a writer can be handed without reading any
code. One heading per message:
## [Nan says hello](mail_nan)
---
From: Mum
To: helm
After: 90
---
Your nan has learned what a subspace relay is and now believes she can be heard by
everyone on the ship at once, so she has started saying hello to all of them.
- [Tell her hello back](reply) ; signal nan_answered
- [Pretend you were on shift](dodge)
From:is who it is from; a message without one is ignored.To:is a console, a comma list, or*(the default).After:is how many seconds into the mission it arrives. Spread them out — mail that all lands at once is a document, not a message.- A
- [label](target) ; outcomesline is a reply the crew can send, up to four.
Load it once, from the top level, and run a pump to deliver what is due:
default shared MAIL_LOADED = False
if not MAIL_LOADED:
shared MAIL_LOADED = True
message_load_amd(document_get_amd_file(None, "Messages",
content=media_read_relative_file("messages.amd")))
task_schedule(mail_pump)
== mail_pump ==
--- pump
await delay_sim(15)
message_deliver_due()
jump pump
Keep message bodies ASCII and under about 600 characters: the engine draws no curly
quotes, no em-dashes and no emoji, and a ^ is a line break to it.
The away team
The PADD is the away team's console. away=True on a registration means the landing
party carries that app down with them, and the away scene mirrors each beat into the
inbox, so a party reads its story in the same place it reads its mail.
That is also what retired the separate away console for PADD missions: the crew carry the screen with them, and whatever job apps a mission adds sit beside it. See Away missions.
A PADD mission's main screen shows nothing about the away mission
The shared view is driven by the older away_begin path, which PADD missions do not
emit. The PADD model has no answer for a shared surface — the same fact as
"no badge on the main screen".
See also
- Story & NPC messages — the ship's log, and the other place text reaches a crew.
- Player ships & consoles — the tab strip the PADD replaces.
- Icons by name — how
icon=resolves. - API: gui, messages.