diff --git a/docs/navigation.md b/docs/navigation.md index ff6b3c375..15abd58b7 100644 --- a/docs/navigation.md +++ b/docs/navigation.md @@ -188,7 +188,7 @@ Everything is on `host.Automation.Navigation`. A host without navigation answers | `GoTo(objectId, arrivalMeters)` | Walks to an object and ends beside it, facing it, within `arrivalMeters` (above 0, at most 50) at a spot with a line of sight to it. | | `GoTo(position, arrivalMeters)` | Walks to a place and ends on the floor that place stands on. Faces nothing on arrival. A position with no cell, such as one read from a route file, is placed by its map coordinates alone, and an elevation of NaN stands it on the ground there. | | `PreviewPathAsync(objectId, arrivalMeters)` | Plans a path to an object and returns ordered cell-aware positions with elevation. Does not start or replace a walk. | -| `PreviewPathAsync(position, arrivalMeters)` | Plans to a place on its specified floor, including inside a dungeon. Does not start or replace a walk. | +| `PreviewPathAsync(position, arrivalMeters)` | Plans to a place on its specified floor, including inside a dungeon. A place in a dungeon or building cell is stood on the floor under it (see [Arriving](#arriving)), so a dungeon map cell's `Center` can be given as it is. Does not start or replace a walk. | | `StandOn(objectId, arrivalMeters)` | Walks onto an object and ends on its top, jumping up where the character can. | | `Follow(playerId, bufferMeters)` | Follows a player until stopped: see [Following](#following). Only players can be followed. | | `StopGoTo()` | Ends the walk under way; `Rejected` when there is none. | @@ -320,7 +320,7 @@ Reports are snapshots, safe to read on any plugin tick. Check that `Sequence` is | Goal | Where the walk ends | |---|---| | An object (`go to`) | Beside it, within the arrival distance of its side, at a spot that can see that side, facing it. An object whose collision is in the grid, such as a life stone or a sign, hides its own middle, so its side is what counts. Failing that, the nearest spot that can see it, up to 10 m away, as `Arrived` with a reason. Failing that, the nearest spot a walk reaches, as `ArrivedWithoutSight`. | -| A place (`go to` a cell or map point) | On the floor the place stands on, within the arrival distance. A place on a rock top is arrived at on the rock, or not at all: a route that only reaches the ground beside it ends `NoRoute`. A place joined to the ground by a ramp or stairs is reached up them. | +| A place (`go to` a cell or map point) | On the floor the place stands on, within the arrival distance. A place on a rock top is arrived at on the rock, or not at all: a route that only reaches the ground beside it ends `NoRoute`. A place joined to the ground by a ramp or stairs is reached up them. A place in a dungeon or building cell is first checked against the face-up surfaces the route grid holds there: a floor, or the top of an object such as a bridge, a platform or a crate. If one lies no more than 0.5 m under the place, the place stays where it is; if one lies no more than 0.5 m over the place, the place is lifted onto that surface. So a route waypoint recorded on a platform is arrived at on the platform, while a room's middle, well over a table there, is not stopped on the table. Any other place in such a cell drops to the floor under it: its own cell's floor nearest at or below the place, or where that cell has no floor there, as in the upper half of a tall room authored as a cell on top of a cell, the highest floor under the place of any cell in the landblock. That search takes every cell in the landblock, so a place over a pit or a shaft drops to the pit's bottom. A place in such a cell given with an elevation of NaN first takes the height of the ground there (or the character's height, where no ground is loaded) and then drops the same way, so in a dungeon such a place ends on a floor under that height. Preview and walk do all this the same way. | | A player (`follow`) | Never ends by itself: see [Following](#following). | | An object's top (`stand on`) | On the highest floor over the object's own collision that a body stands on, within the arrival distance of that top's middle. A ramp or stair up to a deck is not the deck. An object with nothing on top to stand on, such as a creature or a thin wall, or no way up, ends `NoRoute`. | diff --git a/docs/plugin-api.md b/docs/plugin-api.md index c3611c21b..2cf8fad03 100644 --- a/docs/plugin-api.md +++ b/docs/plugin-api.md @@ -124,6 +124,58 @@ writes in one of the client's own text classes, so a plugin's own output can use the colour the class carries. `Submit(text)` still runs the full chat pipeline, commands included. +A line a plugin posts may carry a coordinate link. The tag's caption is shown +in the link colour, and clicking it raises `LinkClicked`: + +```csharp +host.Automation.Chat.PostSystemMessage( + @"Next waypoint: the Holtburg bridge<\COORD>"); +``` + +The only tag read is a well-formed ``, closed by +`<\COORD>`. Any other angle-bracket text ("Press ") is shown as +written. + +### Chat links + +`LinkClicked` is raised when the player clicks a coordinate link in the main +chat window. Only a line a plugin posted carries one: its `COORD` tags, and +every coordinate pair `PluginChatCoordinateParser` reads in its plain text +("meet at 42.1N 33.5E"). A pair is a number and a compass letter twice, +neither inside a longer word, so "10n 20east" is not one. + +What players say, tell and post on channels, and the server's own lines +("Your corpse is located at (42.1N, 33.5E).") are shown exactly as they +arrived and hold no coordinate link, even when a player types a `COORD` tag. +A plugin that wants the coordinates in those lines reads them from `Received` +with `PluginChatCoordinateParser`. `PluginChatCoordinateLinkRouter` hands the +coordinates of a clicked link to a callback: + +```csharp +var router = new PluginChatCoordinateLinkRouter( + host.Automation.Chat, + coordinate => SetWaypoint(coordinate.NorthSouth, coordinate.EastWest)); +``` + +Clicking a player's name still starts a tell and raises nothing. The floating +chat windows show no links. + +**Escape text you did not write.** A plugin's line is trusted as the plugin's +own, so a `COORD` tag in it is a working link. A plugin that repeats a +player's words (echoing a tell, relaying a channel) would turn a tag that +player typed into a link showing one place and reporting another. Pass such +text through `PluginChatMarkup.Escape` first; it puts a backslash before every +`<`, the chat drops that backslash, and the text shows exactly as the player +wrote it with no tag read: + +```csharp +host.Automation.Chat.PostSystemMessage( + $"{PluginChatMarkup.Escape(message.Sender)} told you: {PluginChatMarkup.Escape(message.Text)}"); +``` + +A coordinate pair the escaped text shows in plain words is still a link, to +exactly the place it shows. + ### Intercepting what the player types ```csharp diff --git a/docs/plugin-ui-markup.md b/docs/plugin-ui-markup.md index 7d0dd3f16..0c17e932e 100644 --- a/docs/plugin-ui-markup.md +++ b/docs/plugin-ui-markup.md @@ -313,13 +313,14 @@ The two forms cannot be mixed on one list. selected="{SelectedMonster}" onchange="{SelectMonster}"> - + + ``` | Attribute | Column type | Required | Binding | |---|---|---|---| -| `type` | all | yes | `text`, `check`, or `icon` | +| `type` | all | yes | `text`, `check`, `icon`, or `button` | | `width` | all | see below | pixels, or `*` for auto | | `items` | `text` | yes | `IReadOnlyList` | | `colors` | `text` | no | `IReadOnlyList` or ``, `0xRRGGBB` per row | @@ -329,6 +330,9 @@ The two forms cannot be mixed on one list. | `values` | `icon` | yes | `IReadOnlyList` or `` | | `iconkind` | `icon` | no | `did` (default), `spell`, or `item` | | `onclick` | `icon` | yes | `Action` with the row index | +| `items` | `button` | yes | `IReadOnlyList`, each row's caption; an empty caption draws no button | +| `onclick` | `button` | yes | `Action` with the row index; the click does not select the row | +| `buttonw`, `buttonh` | `button` | no | pixels; the button's size, centred in its cell (default: the whole cell) | Widths: `*` shares the remaining width equally with every other `*` column. The last column is always automatic; it takes the whole remainder unless @@ -336,12 +340,34 @@ another column is also `*`. Every other column needs a positive `width` or `*`. Columns that would overflow the list are clamped and later ones draw nothing. Remainder pixels go to the last sharing column. -Rows: the row count is the longest bound column. A `text` or `icon` cell past -its own column's data draws nothing; a `check` cell draws unchecked. A click +Rows: the row count is the longest bound column. A `text`, `icon` or `button` +cell past its own column's data draws nothing; a `check` cell draws unchecked. A click in a `text` column without `onclick` selects the row and fires the list's `onchange`; any other click fires the column's own callback and leaves the selection alone. A click past a column's own data does nothing. +A `button` cell is drawn as a button one pixel inside its cell, or at +`buttonw` by `buttonh` centred in the cell (never larger than the cell; set +either alone to size only that side), with its caption centred in the list's +text colour and cut off at the button's edges. Without a theme its face and +border are those of an unthemed `