Glk Mapping extension

I was thinking about what it could be like to implement a Glk (GlkOte? GlkOte only?) extension that could support displaying a map provided by the interpreter, e.g. ADRIFT.

Automapping is a pretty weird topic with a lot of edge cases; I’m not surprised nobody’s tried to take it on before.

I started imagining a “Glk Mapping” extension shaped like this:

#define mapcolor_Default  0xFFFFFFFFu

typedef struct glk_mappoint_struct {
    glsi32 x, y;
} glk_mappoint_t;

typedef struct glk_maphyperlink_struct {
    glui32 id;                      /* opaque; nonzero */
    const char *label;              /* null-terminated UTF-8; NULL or "" = unlabeled */
    glui32 npoints;                 /* >= 3 */
    const glk_mappoint_t *points;   /* polygon points */
} glk_maphyperlink_t;

glui32 glk_map_present_svg(
    const unsigned char *data,     /* UTF-8 SVG */
    glui32 len,
    glui32 bgcolor,                /* Glk color code */
    glsi32 focusleft,
    glsi32 focustop,
    glui32 focuswidth,
    glui32 focusheight,
    const glk_maphyperlink_t *hyperlinks,
    glui32 nhyperlinks
);

void glk_map_close(void);

glk_map_present_svg() would create or replace the current map.

  • Returns 1 on success, 0 on failure (invalid SVG).
  • On return (success or failure), the library retains its own copy of the SVG data; the program may free data immediately.
  • The SVG root element must define a viewBox or width/height
  • The runner controls the layout, placement and visibility of the map UI. The runner should provide some UI for panning and zooming the map, as well as hiding/showing the map, but this isn’t required. (Maybe there’d be testable gestalts for those features…? That way the terp/author could say “if this Glk runner doesn’t support panning and zooming, don’t show the map at all”)
    • The map UI is not a Glk “window”; it doesn’t have a Glk window ID, or fit into the Glk window-management system.
  • A successful glk_map_present_svg does not require the map to become visible. If the player previously hid the map, it generally stays hidden, but the latent map updates. Also, if the map is only available as a full-screen experience (e.g. on mobile), then the UI may not display the full-screen map when you present it.
  • The focus variables are ignored if the width or height are 0. In that case, the library may keep the current pan/zoom or fit the whole map — implementation-defined.
  • The bgcolor background color only shows when the user pans the map outside of its bounds, or when the map window’s dimensions are so wide/tall that the map can’t fill the window. mapcolor_Default displays the map with a default background color, chosen by the runner.
  • The map may include clickable hyperlinks, defined as labeled polygons with IDs. (You can pass 0 hyperlinks with a NULL array of hyperlinks.)
    • The mouse cursor should change to a pointing hand when hovering over a hyperlink polygon.
    • The hyperlinks could be navigable via keyboard or other accessibility devices.
    • Each hyperlink must have at least three points in its polygon points array. Hyperlinks with 0, 1, or 2 points will be ignored.
    • Hyperlinks with id 0 are also ignored.
    • If hyperlink polygons overlap, and the user clicks on an overlapping point, the hyperlink that comes last in the array will be selected.
  • Just one map is allowed at a time. (Right??)

glk_map_close() destroys the map data. The library should close/hide map UI. It is legal when no map exists.

Refocus without re-sending the map

void glk_map_set_focus(
    glsi32 focusleft,
    glsi32 focustop,
    glui32 focuswidth,
    glui32 focusheight);

void glk_map_clear_focus(void);

Update hyperlinks without re-sending the map

void glk_map_set_hyperlinks(const glk_maphyperlink_t *hyperlinks,
    glui32 nhyperlinks);

You can clear all hyperlinks by passing 0 hyperlinks.

Present an image resource from a Blorb

glui32 glk_map_present_image(
    glui32 image,       /* Blorb pict id, as in glk_image_draw */
    glui32 bgcolor,
    glsi32 focusleft,
    glsi32 focustop,
    glui32 focuswidth,
    glui32 focusheight,
    const glk_maphyperlink_t *hyperlinks,
    glui32 nhyperlinks
);

Blorbs don’t support SVG (yet, maybe ever). SVG is an enormously complicated standard, with zillions of optional features, some of which can do I/O. :grimacing: But for something like Counterfeit Monkey, where the map is a handrolled PNG, this feels like a nice convenience.

Map Overlays

“Map overlays” are layered images that you can stack on top of a map (and on top of each other). They’re useful for drawing a “you are here” symbol on top of an existing map, or for changing the background color of one room in the map (by redrawing the entire room with a different background color).

typedef glui32 overlayid_t;

overlayid_t glk_map_overlay(
    glui32 image,       /* Blorb pict id, as in glk_image_draw */
    glsi32 left,
    glsi32 top,
    glui32 width,       /* 0 width = natural image width */
    glui32 height,      /* 0 height = natural image height */
    glui32 zindex,
    glui32 link_id,     /* link_id 0 means not hyperlinked */
    const char *linklabel
);

extern overlayid_t glk_map_overlay_svg(
    const unsigned char *data,
    glui32 len,
    glsi32 top,
    glui32 width,       /* 0 width = natural image width */
    glui32 height,      /* 0 height = natural image height */
    glui32 zindex,
    glui32 link_id,     /* link_id 0 means not hyperlinked */
    const char *linklabel
);
  • The map itself would have zindex 0; overlays with higher zindex would render on top of (obscuring) overlays with lower zindex.
  • You can optionally associate an overlay with a hyperlink.
    • In case of overlap:
      • overlay hyperlinks will be selected instead of present hyperlinks
      • overlays with higher zindex will be selected first
      • other than that, the most recently declared (last) hyperlink will be selected

You can also add basic rectangular overlays, like this:

overlayid_t glk_map_fill_rect(glui32 color, glsi32 left, glsi32 top,
    glui32 width, glui32 height, glui32 zindex);

And you can move and clear overlays like this:

glui32 glk_map_overlay_move(
    overlayid_t overlay,
    glsi32 left,
    glsi32 top,
    glui32 width,
    glui32 height,
    glui32 zindex);

glui32 glk_map_overlay_clear(overlayid_t overlay);

glui32 glk_map_overlay_clear_all(void);

Note that a successful call to glk_map_present_svg or glk_map_present_image clears all overlays.

Also note: it’s a good idea to defer/batch (“debounce”) overlay calls, rather than rendering overlay updates synchronously on every call to glk_map_overlay* function. Otherwise, you might end up displaying the UI in an intermediate state.

Map events

void glk_request_map_event(void);
void glk_cancel_map_event(void);

#define evtype_Map (10) /* actual value TBD */

#define mapevent_Hyperlink (1)  /* val2 = link id */
#define mapevent_UserHide  (2)  /* val2 = 0 */
#define mapevent_UserShow  (3)  /* val2 = 0 */

For map events, the win window will always be NULL; val1 would be the event type (Hyperlink, UserHide, or UserShow), and val2 would be the payload or 0.

  • The Hyperlink map event would fire when the user clicks on a hyperlink in the map, e.g. to automatically navigate the user to the selected room.
    • We’re not using the standard Glk hyperlink event type, because the map doesn’t have a Glk window ID.
  • The UserHide event would fire when the user closes the map.
    • The interpreter might decide to print a message when this happens, e.g. “To reopen the map, type MAP.”
  • The UserShow event would fire when the user opens the map using runner chrome (e.g. using the runner’s menu bar or a toolbar button; something outside the Glk-managed UI)
    • This is useful for ADRIFT games, which want to show a “Map” hyperlink in the status bar when the map is closed, and hide the hyperlink when the user opens the map.

Showing the map after the user closes it

void glk_map_show_at_user_request(void);
glui32 glk_map_get_visibility(void);

glk_map_show_at_user_request() shows the map. It’s meant to be used when the user asks to open the map, e.g. after running the MAP command.

glk_map_get_visibility returns true when the map is visible, and false when the map is not visible, either because the user closed it, or because, in the current interpreter, the map is only displayed in a full-screen experience (e.g. mobile).

You can use this to show a hint at/near the start of the game, like, “To show the map, type MAP,” if the map isn’t currently visible.

Out of scope?

  • Multiple maps

What would the Glk runner do, actually?

For comparison

  • Counterfeit Monkey: Managing its map has been a huge source of bugs over the years.
    • As of v11, Counterfeit Monkey starts by disabling its map, allowing users to reveal the map as a graphics window that occupies the side pane with map on. If you do that on mobile, you’re going to have a bad time, but you’ll probably be able to figure out how to map off to make it go away.
    • Alternately, if you just want to peek at the map, you can type map, which displays in the text buffer as a full-screen image.
    • Each room has its own pre-rendered map, with the “you are here” symbol burned into the image. When showing the map, the UI just shows the current room’s pre-rendered image. (This feels suboptimal. Separate map overlay layers would work much, much better.)
  • Hadean Lands: Custom UI everywhere for Hadean Lands. I don’t think any of the source of that is publicly available…?
    • On desktop OSes, it starts by opening multiple OS windows, one for the text buffer, one for the map, and one for the “journal” that includes rituals, formulas, and facts. The map is pre-rastered as a PNG, with labels as overlays that you can hide/show with a “Labels” button in the map menu, and an animated “you are here” symbol, a star. There’s a “Flash Star” button that enables/disables flashing.
    • On iOS, the game launches in a tabbed view, with these tabs: Game (the text buffer), Journal, Map, Help, Settings.
  • ADRIFT: (This is what originally got me thinking about this problem.) In ADRIFT, authors can declare a map of rooms and exits, each with visibilities. You can make the whole map visible at game start, or reveal the map room-by-room as you explore. From the user’s perspective, it feels like an “automap”, but it’s not trying to automatically parse the transcript; it’s revealing more and more of the author’s hand-coded map model.
    • ADRIFT also lets you click on rooms of the map; the game runs Dijkstra’s algorithm to find the shortest path from your location to the destination, and automatically emits “go north.” “go west.” etc. commands to get you there.
    • The official ADRIFT-5 runner is only available for Windows. ADRIFT’s runner has a fancy dockable UI. You can drag and drop the map window (or other “named windows”) to the top, bottom, right, and left of the screen; you can pop them out and turn them into floating windows that you can reposition wherever you like.


    • FrankenDrift is available on other platforms; it makes a separate OS window available for the map, which the user can position manually. (On macOS, the map window spawns underneath the text buffer, making it difficult to discover, but it is there.) FrankenDrift doesn’t attempt to render a map on Glk.

Terp-rendered SVG maps

Some interpreters support author-provided automaps, like ADRIFT.

In that case, the terp would render the map as an SVG string and present it via glk_map_present_svg().

As the map updates (e.g. moving the “you are here” marker), the terp would call glk_map_present_svg() on each turn, presenting (representing) the entire SVG string each time the map updates. (SVG strings are quite short.)

Color recommendation for terp-rendered SVG maps: Measure the Normal TextColor/BackColor with glk_style_measure, and use those colors in the generated SVG (for now?)

glk_style_measure can report on the foreground text color and background color of the “Normal” style for the transcript window. If the user has opted to override stylehints that the terp has provided, glk_style_measure will report that, giving you a reasonable background color and foreground color for rendering.

If glk_style_measure returns false, use a default color scheme. For example, RemGlk interpreters (including Parchment and Lectrote) don’t implement glk_style_measure at all, returning FALSE in all cases. (glk_style_measure would require an asynchronous remote procedure call, asyncifying the function.)

I note that stylehints have been kinda “deprecated” for years, partially replaced by the garglk_set_zcolors Glk extension, and hopefully soon to be replaced by the CSS Glk extension.

But, as I write this, there is no other API proposed to access the normal text colors, so glk_style_measure is all we have.

My guesses as to what the UI would look like

I’m imagining that Glk runners that support mapping would have different UI for large/small screens (and that the runners would be the best decision makers for how/when to do that).

  • Parchment/webUI runners would probably display the map in a side pane with a close button if the window is “wide enough.” If the window is too narrow, the map would be hidden by default. If there’s an on-screen menu (a hamburger menu, or a toolbar), a “Show Map” button could appear there if the map isn’t visible. (And, I think webUI runners should have a menu, at least a hamburger menu, with buttons to save, restore, restart, adjust settings, and get help.)
    • The terp would call glk_get_visibility() early on, and show a hint, “To open the map, type MAP,” if the map isn’t visible.
  • Desktop runner apps (Gargoyle, Spatterlight, Lectrote) could open the map in an OS window, which the user could reposition. ADRIFT’s runner has a fancy dockable UI, which feels like overkill to me, but it works, and maybe that’s “best” for power users. IMO non-power users struggle to understand it. Grandpa’s Ranch (a beginner-friendly ADRIFT game intended for the Text Adventure Literacy Jam) had to include a bunch of in-game documentation trying to explain to newbies how ADRIFT’s layout system works, which suggests to me that ADRIFT’s layout system is the wrong approach, even for a desktop-only IF interpreter.
    • When closing the map, the terp would get a UserHide event, and print a message, “To reopen the map, type MAP.”
  • Mobile apps have been pretty creative about menus.
    • I mentioned Hadean Lands’s tabs; that looks pretty good.
    • A hamburger menu would work, too.
    • iOS Frotz has a little menu “book” in the prompt; you can tap on it to get a bunch of commands to auto-type. Adding “map” in there would be nice.
    • Check out Fabularium’s custom-keyboard menu. I notice the M key doesn’t have anything on it, which would be perfect for showing the map!
      image

What do you think?

Is this anything? If I filed PRs on Parchment, Gargoyle, Lectrote, etc. to support this, would you consider merging those PRs, or just close them without merging?

When I see “automap” I think of a system which will automatically draw a map for the player based on a game transcript, adjusting room connections and positions as it goes, based on what the player does.

This seems quite different - it’s really about a pan-and-zoom interface (like Google Maps)? It looks like you’re mostly envisioning a single base image with optionally some additional graphics drawn on top, though they would have to be manually positioned by the game author? If the author is still having to position image elements according to image offsets then I don’t think it really qualifies as an automap.

But a pan-and-zoom mode for a graphics window is an interesting idea in its own right! It would entail basically two things: the idea of a “canvas” size distinct from the actual pixel size of the graphics window (which many graphics extensions would want to use anyway), and a way to specify the original zoom/“focus”/crop of the canvas. If you could obtain the zoom level then you could also use different image sources to draw onto the canvas. More than 2 levels would probably be overkill, but an extension could handle it. Maybe an event to indicate a change in zoom/pan.

Glk libraries would need to support mouse or multi touch pan/zoom operations, and probably add zoom in/out buttons as well.

There’s definitely potential here, though the details will need ruminating over. As would author and player demand. In app maps are great, but simply providing a PDF map would end up better for a lot of both authors and players.

For Parchment something to keep in mind is whether the operations could be transmitted via JSON.

In ADRIFT, authors can declare a map of rooms and exits, each with visibilities. You can make the whole map visible at game start, or reveal the map room-by-room as you explore.

From the user’s perspective, that feels like an “automap,” and I’d call it an automap, but it’s not trying to automatically parse the transcript; it’s revealing more and more of the author’s hand-coded map model. (You can’t do that with PDF!)

Even if the map is hardcoded and pre-rastered, one of the biggest advantages of an in-game map is a “you are here” symbol, which PDFs also can’t provide. But also, having the runner provide a UI for the map potentially solves the problems that Counterfeit Monkey has been wrestling with.

the idea of a “canvas” size distinct from the actual pixel size of the graphics window

This is baked in to SVG.

you could also use different image sources to draw onto the canvas

That’s what I was calling “map overlays,” and I thought (hoped?) it’d be out of scope.

I updated my original post with screenshots of ADRIFT.

A few months ago, I ported the ADRIFT 5 automapper to Javascript for use in my port of SuperPAC (link in AIF forum).

I gave it a try.

  • By default, the map is in “float” mode, where it’s a draggable rectangle, meant to evoke an OS window.
  • There’s no way to pan the map, but the whole map seems to be only a handful of rooms wide/tall, so there’s no need to do so; it all fits on one screen.
  • You can drag the corner to resize it, which is the only way to zoom in (e.g. if the text is too small.)
  • You can click on rooms to autowalk there.
  • There’s an “Options” menu allowing you to switch the map to “dock” mode, which then occupies a fixed pane on the screen. You can set the map to “float”, “dock”, or “none” to hide the map; you can do the same thing with the “graphics” window (float/dock/none).
  • Both windows default to “none” on mobile; neither “float” nor “dock” mode work properly on iPhone Safari 26. You can’t drag the floating rectangle or resize it. In “dock” mode, the map occupies the full screen, and I can’t get access to the transcript.

Overall, the moral I draw from this remains the same:

  • Drawing a map (eliminating the need for users to draw their own map) is useful.
    • “You are here” symbols provide value over static PDF maps.
    • Gradually revealing the map is also helpful to avoid spoilers.
    • Clicking on a map room to walk there (“autowalk”) is also useful, especially in large maps.
  • To make the software work properly, authors should provide their own map model (rather than having the runner try to parse the transcript).
  • If the map is in a fixed pane, users need the ability to turn it on and off.
  • A full-screen “map” mode is needed on mobile.
  • If there’s a full-screen map mode, the UI probably requires an on-screen menu.
    • On-screen menus are pretty great, even without maps. Save, Restore, Restart, Settings all fit well in an on-screen menu.
    • A “map” command can also work, but that would require users to know how to use it. (Maps are especially useful for newbies, who are especially unlikely to know/retain a big list of commands.)

So if I’m understanding right, this proposal lets a Glk application request a window (which will be positioned wherever the Glk library thinks best) that can be zoomed and panned by the user, and the Glk application can then draw its map into that (but needs to handle the zooming and panning itself)?

GUI highly resembles the ADRIFT setup because it’s a port of an ADRIFT 5 game. Making the map non-scrollable and non-zoomable was deliberate on my part; the map is small enough to fit all at once. (Except there’s a second floor to the map, and it shows the current floor.)

Also, it shows indicators for the various NPCs on the map (the little circles).

(It’s weird that Dan just put more effort into commenting on the automap than the various testers put into commenting on the game.)

That’s not the way I’d phrase it, if I understand my Glk terms correctly. (I might not understand my Glk terms correctly! I’m pretty new to Glk.)

In my terms:

  • Gargoyle.exe/Gargoyle.app is a “Glk application”. (Parchment is also a “Glk application.”)
  • A Glk application can embed one or more “embedded Glk interpreters”. A Glk application can itself be considered an “interpreter” (if it allows users to choose a game to play), but an “application” is the top-level interpreter.
  • When an embedded Glk interpreter uses the Glk API, it can be linked with any compatible Glk “library” as part of building the Glk application.
    • C-based Glk libraries (cheapglk, RemGlk) call the embedded interpreter’s glk_main() function. The embedded interpreter can then open one or more Glk “windows” (panes) with glk_window_open(), call glk_select(), and request input with glk_request_line_event(). (It decides what Glk APIs to call by interpreting the game file.)
    • GlkOte-based Glk libraries (including AsyncGlk) call an accept function on the window.Game object, which expects JSON responses (usually from RemGlk)
  • The embedded Glk interpreter interprets the game file and calls Glk API functions.

In my terms, I would say that the “embedded Glk interpreter” can provide a map image (which will be displayed/positioned wherever the “Glk application” thinks best) that can be zoomed and panned by the user. The “Glk application” would decide whether to open an OS window, or to use a window pane in the current OS window, or to just display the map full-screen, allowing the user to dismiss it.

To clear up other possible misunderstandings:

  • My proposal does not call for displaying the map in a “Glk window.” (Glk windows are normally not OS windows; in hindsight, I wish we’d called them “panes” instead.)
  • glk_map_present would return a success code, but not a window; glk_map_present would not have a “rock” (a reference/handle) that refers to the map.
  • If the Glk application decides to present the map in a docked side pane, it would resize the top-level Glk window (firing a Glk resize event), reserving space for the map in its own area outside the Glk window-arrangement system.
  • Thus, it would not be possible to render the map in a docked side pane underneath the status line, because the status line is a pane in the Glk window-arrangement system.
  • Neither would it be possible to sandwich the map between two Glk windows (panes).
Could a Glk map API use Glk windows (panes)? Mayyyybe, but I think it's a bad idea.
  • Glk currently doesn’t have a model of “floating windows,” windows that the user can drag around and reposition. All Glk windows are panes inside the game’s rectangle.
  • Neither does Glk have a mechanism for users to control which Glk windows are visible/closable. (If it did, there would presumably have to be some generic UI at the Glk application layer to re-show hidden/closed windows… I think it would be difficult to design an API like that.)
  • Glk window layout is under the control of the embedded interpreter, who usually leaves a lot of that under the control of the author, which means that the Glk application can’t implement “responsive design” all on its own, where the window layout changes when the layout gets narrow/wide.
    • Early versions of Counterfeit Monkey showed the map in a docked side pane (a Glk window) at all times, even on mobile. Glk applications couldn’t jump in and fix that; the fix had to be in Counterfeit Monkey. (And in all N applications that try to do this.)
    • City of Secrets still doesn’t play correctly on mobile, for reasons like this.

Giving the Glk application control over the map position seems like the right approach.

It seems like revealing part of the map bit by bit is an important aspect of the Adrift implementation, but it’s not part of this proposal?

It doesn’t need to be, because ADRIFT could call glk_map_present_svg each time the player moves, and/or reveals a room. The interpreter would generate a new SVG each time; the runner application wouldn’t need to know anything about that.

I think the API is too simplistic, requiring sending a potentially large image whenever the map changes (which could be often). Plus the requirement for SVG would be too burdensome for some interpreters.

A better API, imho, would be to send multiple images to populate the map window with. These images would come from a blorb and use blorb ID numbers. The map would need a way to be cleared, so it can be rebuilt from scratch after restoring a savegame (etc).

The map window could be layered (each image having a Z component), which makes things like a “you are here” image easy to implement for the gamecode or interpreter.

Yeah, that’s what I called “map overlays.”

For rasterized (PNG/JPG) maps, I guess it makes sense that you’d want rasterized map overlays, as well. (It does suck that Counterfeit Monkey is embedding N images for N rooms, each with the “you are here” symbol burned in.)

I’ve added a “If the map can be a rasterized image resource, you’ll probably want to rasterize map overlays, too” section to my top post:

overlayid_t glk_map_overlay(
    glui32 image,       /* Blorb pict id, as in glk_image_draw */
    glsi32 left,
    glsi32 top,
    glui32 width,
    glui32 height,
    glui32 zindex);

glui32 glk_map_overlay_move(
    overlayid_t overlay,
    glsi32 left,
    glsi32 top,
    glui32 width,
    glui32 height,
    glui32 zindex);

glui32 glk_map_overlay_clear(overlayid_t overlay);

glui32 glk_map_overlay_clear_all(void);

But I think SVGs (or some vector-art standard like SVGs, e.g. PDFs) will be necessary for cases like ADRIFT, where the blorb doesn’t include any rasterized map images. Plus, vector art looks way better than rasterized images when zooming in, especially rasterized text.

SVGs are surprisingly easy to generate from scratch; you don’t even need an “SVG library” to generate SVGs. They’re just XML text, and they’re almost never “potentially large images.” (But the Glk runner applications would need to embed an SVG library to render SVG.)

SVGs are a potential security risk in browsers however. I haven’t looked into whether they can be safely sandboxed. If they can be then they’d be good to support generally, not just for this.

Dannii, in these days one must be very cautious on “potential security issues”, for many OT reasons.

Said that, I prefer an “advent calendar” approach, with the “visited” status being also the open/closed status of the calendar’s door. This allow an agnostic approach to the picture format of the complete map and its overlay.

Best regards from Italy,
dott. Piergiorgio.

Dannii, what exactly is the security risk? (Yes, the person who created the SVG can run Javascript in the context of the site, but the site creator can already send arbitrary Javascript in the context of that site.)

Andrewj: What do you mean by potentially large? The given images were under 8k, and size doesn’t matter because they’re being generated locally.

Right, but the person who created the SVG is not the site creator, right? They’re any rando on the internet who wrote an IF piece.

An interpreter like Parchment lets you play storyfiles from anywhere on the internet. So I need to be careful what is allowed.

Here’s a possible API for presenting a map model via Glk, instead of accepting user-provided SVGs.

Presenting the model

You’d call glk_map_present_model with an array of “nodes” and “links”.

/* mapflags for glk_map_present_model: */
#define mapflag_SuggestShow         0x00000002  /* soft hint: prefer showing the map */
#define mapflag_UserRequestedShow   0x00000004  /* user asked to show map; re-show if hidden */


/* Replace the current semantic map model.
   Returns 1 on success, 0 on failure (invalid parameters, OOM, unsupported).
   Library copies what it needs; pointers need only live for the call. */

glui32 glk_map_present_model(
    glui32 mapflags,
    const glk_mapnode_t *nodes,
    glui32 nnodes,
    const glk_maplink_t *links,
    glui32 nlinks
);

void glk_map_clear(void);

Nodes

typedef struct glk_mappoint_struct {
    glsi32 x, y, z;            /* abstract map units, not pixels */
} glk_mappoint_t;

/* nodeflags: */
#define mapnode_None    0
#define mapnode_Here    0x1    /* "you are here"; used on at most one node */
#define mapnode_Focus   0x2    /* include this room’s box in the camera focus region;
                                  runner unions all Focus boxes and keeps them viewable.
                                  Independent of Here; often Here|Focus on current room. */

typedef struct glk_mapnode_struct {
    glui32 id;                 /* opaque room rock (terp-chosen; session-stable is enough) */
    const char *label;         /* UTF-8 display name; required (empty OK); copied on present */
    glui32 nodeflags;          /* mapnode_* */
    glk_mappoint_t origin;     /* top-left of room box */
    glui32 width, height;      /* box size in abstract map units */
} glk_mapnode_t;

Links

/* Directions */
#define mapdir_North       1
#define mapdir_NorthEast   2
#define mapdir_East        3
#define mapdir_SouthEast   4
#define mapdir_South       5
#define mapdir_SouthWest   6
#define mapdir_West        7
#define mapdir_NorthWest   8
#define mapdir_Up          9
#define mapdir_Down       10
#define mapdir_In         11
#define mapdir_Out        12

/* Ports: A link starts at one port and optionally ends at another */
typedef struct glk_mapport_struct {
    glui32 nodeid;             /* matches glk_mapnode_t.id */
    glui32 dir;                /* mapdir_* */
} glk_mapport_t;

/* null port: arrow pointing into empty space */
#define glk_mapport_null ((glk_mapport_t){ 0, 0 }) 

/* line style */
#define maplink_Solid    0
#define maplink_Dotted   1   /* for restricted connections */

typedef struct glk_maplink_struct {
    glk_mapport_t from;        /* leave this room in this direction; from.nodeid must be in nodes[] */
    glk_mapport_t to;          /* arrive in this room from this direction (or glk_mapport_null) */
    glui32 style;              /* maplink_Solid or maplink_Dotted */
    glui32 via_count;          /* bend points */
    const glk_mappoint_t *via; /* via_count points, or NULL if via_count == 0 */
} glk_maplink_t;

Example

  • The player is in the Laboratory, west of the Hall, with a dotted-line link between them
  • There’s a Laboratory exit to the south to a room that the player hasn’t seen yet, so it doesn’t appear in the node list; its to is null. It would be rendered as a line pointing south from the Laboratory into empty space.

glk_map_present_model(
    mapflag_SuggestShow,
    (glk_mapnode_t[]){
        {
            .id = 1001, .label = "Laboratory",
            .nodeflags = mapnode_Here | mapnode_Focus,
            .origin = { 0, 0, 0 }, .width = 6, .height = 4
        },
        {
            .id = 1002, .label = "Hall",
            .nodeflags = mapnode_None,
            .origin = { 8, 0, 0 }, .width = 6, .height = 4
        },
    }, 2,
    (glk_maplink_t[]){
        {
            .from = { 1001, mapdir_West },
            .to = { 1002, mapdir_East },
            .style = maplink_Dotted,
            .via_count = 1, .via = (glk_mappoint_t[]){ { -2, 2, 0 } }
        },
        {
            .from = { 1001, mapdir_South },
            .to = glk_mapport_null,
            .style = maplink_Solid,
            .via_count = 0, .via = NULL
        },
    }, 2
);

Responsibilities of the Glk runner application

The Glk runner would have to convert the model into an image. (I recommend generating an SVG!)

  • As in my earlier proposal, the map would appear outside the top-level Glk window (i.e. not in a “Glk window” pane at all). If the Glk application decides to show the map in a window pane, the top-level Glk window would be resized (firing resize events).
  • The Glk application controls how to draw the rooms (room label fonts, room shapes [rectangle? rounded rectangle?], colors) and what the “you are here” indicator is (a symbol? a modified background color?)
  • Allow the user to pan/zoom the map
  • When the Glk application presents the map, zoom/pan to fit the selected “focus” nodes in the map

But what if the game wants to control more stuff?

  • Room shapes: octagons, triangles, ovals, rounding corners, etc.
  • Room border styles (dotted, wavy, colors, no border)
  • Line thickness
  • Fonts
  • Putting more text in the room (subtitles, object lists)
  • “You are here” symbol appearance (icon vs. color)
  • Link port labels (“trapdoor”)
  • Other overlays:
    • @ShardWorkX wanted to put little dots representing various NPCs on the map
    • Having several room colors, to depict different types of rooms (does the game decide? What about if the user prefers a dark mode UI vs. light mode?)
    • Putting rooms into containers (with background colors, titles)
    • Arbitrary non-room text on the map

Every individual thing on this list would require more API. :grimacing:

WDYT? Is this better?

Personally I think that proposed API is too specific to be feasible.