## Pressing `>` on a TextDefinition did nothing useful
`#1217` moved `TextDefinition` into the Server project for serialization support and added `[PropertyObject]` along the way. `PropsGump` checks that attribute **before** its parsable fallback, so from that commit on the button either drilled into a read-only `Number`/`String` page (both get-only since `#1221`, so no `>` buttons — a dead end) or, when the value was null, re-sent the same page and looked inert.
Worth being clear that `#765` — which replaced the hand-maintained type list with a generic `IsParsable` branch — was **not** the regression. At that commit `TextDefinition` was `[Parsable]` only, fell through to the new branch, and the editor worked. Only the later attribute hijacked the routing.
`TextDefinition` is now caught ahead of the `[PropertyObject]` branch and routed to `SetGump`, which has had `TextDefinition`-specific code at line 35 all along.
## `0` and `"0"` could not be told apart
`Commands.Split` strips quotes before any parser runs, so this was never fixable in `TextDefinition.Parse` alone — the information is gone two layers earlier. The markers now travel inside the value:
| Input | Result |
|---|---|
| `1060847` | cliloc (unchanged) |
| `#1060847` | cliloc, explicit — the form `ToString()` already writes |
| `@"1060847"` | the literal string |
| `hello` | string (unchanged) |
| `(-null-)` | null (unchanged) |
`GetValue()` quotes a string that would not survive the trip back. That check is a real round trip through the parser rather than a pattern match, so it cannot drift from it. `Types.TryParse` decodes the same escape for plain strings — which is what `[get` has always written for the literal `"null"` without `[set` ever reading it back.
**Saved data is untouched.** Serialization reads a discriminated flag plus int/string (`IGenericReader.cs:175`) and the JSON converter switches on token type; neither calls `Parse`, so a stored string `"1060847"` stays a string and spawner JSON is unchanged. The new syntax stays in the text and command layer, where it reaches `[set`, `[add`, spawner props, the props gump and Advanced Search.
## `where` still parsed its constants its own way
`#2625` carried the hand-rolled constant parser across to `PropertyExpressions.Parse` unchanged. It looks for a static `Parse` overload on the property type and gives up if there is none — and `Type` and `IEntity` have neither:
```
where Subject Kind = Static -> Unable to convert string "Static" into type 'System.Type'.
where Subject Owner = 0x1 -> Unable to convert string "0x1" into type 'Server.Mobile'.
```
Both resolve fine for every other command. Routing through `Types.TryParse` picks up type-name lookup, entity resolution by serial, and the `@"..."` literal convention, so a value that works in one place now works everywhere. It also deletes the duplicate parser (−56 lines in `PropertyExpressions`).
Two things are load-bearing and preserved. `where` has always spelled a null constant as a bare `null`, where `[set` uses `(-null-)`; the shared parser reads a bare `null` as the text, so the null case is handled ahead of it and `= null` keeps meaning null. And nullable targets still unwrap first, so `where <int? prop> = 5` is unaffected.
Bare and hex integers, enums, bools, strings, `Map`, and TextDefinition's `#` / `@"..."` markers all resolve exactly as before — 13 of the 15 tests in `WhereConstantParsingTests` pin that and passed before the change as well as after.
## Documentation
The generic command system had no documentation anywhere in the repo: scopes, `where` and its operators, dot notation, `order by` / `distinct` / `limit`, the value and quoting syntax, `[interface` and `[batch` were all learn-by-reading-the-source. `dev-docs/generic-commands.md` covers them, linked from `CLAUDE.md` and `commands-targeting.md`, and points at <https://muo.gg/commands> for the per-command list rather than duplicating it.
Two behaviours it records that were news to me while writing it:
- `Contained` honours conditions on the normal command path but never sets `SupportsConditionals`, and `[batch` is the only place that reads the flag — so the same condition works typed directly and is refused under batch.
- `Multi`, `Single`, `Self` and `Serial` do not parse modifiers at all, so a `where` clause there is passed to the command as ordinary arguments rather than rejected.
Comments in the changed code were trimmed to what is not evident from the code itself; the rationale they carried is either in the doc now or in the commit that introduced it.
## Behavior changes worth a reviewer's attention
Two things go beyond strict bug-fixing, both deliberate:
- **Hex now parses into a TextDefinition.** Consolidating the three `Parse` overloads onto one span codec means `[set Message 0x102CE7` is cliloc 1060847 rather than the string. Previously only the 1-arg `Parse(string)` did hex and nothing called it. This makes the props gump's own `1060847 (0x102CE7)` display typeable.
- **`@"..."` decodes generally for strings**, not only the exact token `@"null"`. So `[set Name @"hello"` sets `hello`. A half-working escape seemed more surprising than a general one, and the `where` path already decoded it — but narrowing it back is a one-line change if preferred.
## Testing
40 tests, each written first and watched fail for the right reason. They cover the props gump routing (populated and null), cliloc/string/quoted parsing and `GetValue` round trips, `where` constant resolution for Type- and entity-valued properties, and the bare-`null` vs `@"null"` distinction that must not drift.
One sort case is added that #2625 left uncovered: ordering a value-typed chain whose intermediate is null, which reads as `default(int)` rather than throwing. Two other tests from the pre-rebase branch were dropped as duplicates of `ConditionalCompilerEdgeTests`.
Rebased onto `535a09899`. `dotnet build` clean, 0 warnings. **902 UOContent** and **869 Server** tests pass, 0 failures.
## Known gaps
- `Nullable<T>` comparisons work via #2625's lifted operators; nothing here changes that.
- Spawner **Params** are split on plain spaces (`BaseSpawner.cs:1035`) rather than through `Commands.Split`, so a constructor argument still cannot contain one. Untouched here — a tokenizer issue, not a TextDefinition one.
- `Types.ParseStringNumericParamTypes` is now unreferenced. Left in place because it is public and shards may use it.
239 lines
10 KiB
Markdown
239 lines
10 KiB
Markdown
# Generic Commands (finding and manipulating entities)
|
|
|
|
How staff find a set of objects and run a command against all of them: scopes, `where` conditions,
|
|
`order by` / `distinct` / `limit`, dot notation, value syntax, `[interface` and `[batch`.
|
|
|
|
This covers the **generic command system** only. For the full per-command list (`[add`, `[props`,
|
|
`[tele`, …) see the in-game commands page shipped with the distro, published at
|
|
<https://muo.gg/commands>.
|
|
|
|
## The shape of a generic command
|
|
|
|
```
|
|
[<scope> <command> [command args] [where <Type> <conditions>] [distinct <props>] [order by <props>] [limit <n>]
|
|
```
|
|
|
|
- **scope** — which objects to consider (`Global`, `Area`, `Region`, …).
|
|
- **command** — what to do to each (`Delete`, `Props`, `Set`, `Count`, `Interface`, …).
|
|
- **modifiers** — optional filters applied to the found set before the command runs.
|
|
|
|
```
|
|
[global count where Item Movable = false
|
|
[area delete where Item ItemID = 0x1F13
|
|
[region interface where Mobile Hits < 10 order by Hits limit 20
|
|
```
|
|
|
|
Commands opt into which scopes they support and whether they act on Items, Mobiles or both, so not
|
|
every command works under every scope. A command that does not support the scope reports
|
|
*"That is either an invalid command name or one that does not support this modifier."*
|
|
|
|
## Scopes
|
|
|
|
| Scope | Usage | Conditions? | Selects |
|
|
|---|---|---|---|
|
|
| `Global` | `[global <command> [condition]` | yes | every object in the world |
|
|
| `Area` (`Group`) | `[area <command> [condition]` | yes | a bounding box you drag |
|
|
| `Screen` | `[screen <command> [condition]` | yes | everything on your screen |
|
|
| `Range` | `[range <range> <command> [condition]` | yes | within `<range>` tiles of you |
|
|
| `Region` | `[region <command> [condition]` | yes | your current region |
|
|
| `Facet` | `[facet <command> [condition]` | yes | your whole map |
|
|
| `Contained` | `[contained <command> [condition]` | yes¹ | inside a targeted container |
|
|
| `Online` | `[online <command> [condition]` | yes | connected players |
|
|
| `IPAddress` | `[ipaddress <command> [condition]` | yes | accounts sharing a targeted player's IP |
|
|
| `Multi` (`m`) | `[m <command>` | no | several objects you target in turn |
|
|
| `Single` | `[single <command>` | no | one targeted object |
|
|
| `Self` | `[self <command>` | no | you |
|
|
| `Serial` | `[serial <serial> <command>` | no | one object by serial |
|
|
|
|
`Multi`, `Single`, `Self` and `Serial` do not parse modifiers at all — a `where` clause there is
|
|
not rejected, it is passed through to the command as ordinary arguments.
|
|
|
|
¹ `Contained` honours conditions on the normal command path, but it never sets the
|
|
`SupportsConditionals` flag, and `[batch` is the one place that checks it. So a condition works
|
|
under `[contained` typed directly and is refused under `[batch` with that scope.
|
|
|
|
## `where`
|
|
|
|
The **first token after `where` is a type name**, and it is required. It filters the set to objects
|
|
of that type (subclasses included) and fixes the type whose properties the rest of the clause reads.
|
|
|
|
```
|
|
[global count where Item -- every Item
|
|
[global count where BaseCreature Hits < 10
|
|
```
|
|
|
|
Only properties marked `[CommandProperty]` are visible, and your access level must meet the
|
|
attribute's read level.
|
|
|
|
### Operators
|
|
|
|
| Operator | Meaning |
|
|
|---|---|
|
|
| `=`, `==`, `is` | equal |
|
|
| `!=` | not equal |
|
|
| `>`, `<`, `>=`, `<=` | relational (needs a comparable type) |
|
|
| `=~`, `~=`, `==~`, `~==`, `is~`, `~is` | equal, case-insensitive |
|
|
| `!=~`, `~!=` | not equal, case-insensitive |
|
|
| `starts`, `ends`, `contains` | substring tests |
|
|
| `starts~`, `ends~`, `contains~` | substring tests, case-insensitive |
|
|
|
|
The `~` may lead or trail — `~contains` and `contains~` are the same operator.
|
|
|
|
Relational operators on a type with no ordering (no `IComparable`) are rejected rather than
|
|
silently misbehaving. Equality on such a type compares **by value**, not by reference.
|
|
|
|
### Combining conditions
|
|
|
|
Conditions separated by whitespace are ANDed. `or` (or `||`) starts a new alternative group, and
|
|
`not` (or `!`) negates the single condition that follows it.
|
|
|
|
```
|
|
[global count where Item Movable = true Hue = 0
|
|
[global count where Item Hue = 0 or Hue = 1
|
|
[global count where Item not Movable = true
|
|
```
|
|
|
|
## Dot notation
|
|
|
|
A binding may walk a chain of properties:
|
|
|
|
```
|
|
[global count where SkillTeleporter Message.Number = 1060847
|
|
[global interface where BaseCreature ControlMaster.Name =~ bob
|
|
```
|
|
|
|
If a link partway along the chain is null, the object simply does not match — it is not an error,
|
|
and it does not stop the sweep. The same applies under `not`: an unreadable binding never matches.
|
|
|
|
For `order by` and `distinct`, which have no "no match" to give, a null link reads as the property
|
|
type's default (`0`, `null`, …).
|
|
|
|
Chains are **read-only**. `[set Message.Number 5` fails when the intermediate's members are
|
|
get-only, as `TextDefinition`'s are.
|
|
|
|
## Values
|
|
|
|
A comparison constant is resolved by the same parser behind `[set`, `[add`, spawner props and the
|
|
props gump, so a value that works in one place works in all of them.
|
|
|
|
| Form | Means |
|
|
|---|---|
|
|
| `123`, `-4` | a number |
|
|
| `0x1F13` | a number, hex |
|
|
| `true` / `false` | a boolean |
|
|
| `Magery` | an enum member, case-insensitive |
|
|
| `Felucca` | a `Map` |
|
|
| `Static`, `BaseCreature` | a `Type`, by name |
|
|
| `0x40001234` | an entity, resolved by serial |
|
|
| `hello world` | a string — quote it in the command line if it contains spaces |
|
|
| `null` | null (in `where` only — see below) |
|
|
| `(-null-)` | null (in `[set` / `[add` / spawner props) |
|
|
| `@"text"` | the literal text inside, for values that would otherwise be read as something else |
|
|
| `#1234` | a `TextDefinition` cliloc, explicitly |
|
|
|
|
### Quoting, and why `@"..."` exists
|
|
|
|
The command tokenizer strips real quotes before any parser sees them, so `"0"` and `0` arrive
|
|
identical. `@"..."` is the in-band escape that survives:
|
|
|
|
```
|
|
[set Name @"null" -- the four-letter string, not a null
|
|
[set Message @"1060847" -- the string "1060847", not cliloc 1060847
|
|
[set Message #1060847 -- cliloc 1060847, explicitly
|
|
[set Message 1060847 -- cliloc 1060847 (a bare integer is always a cliloc)
|
|
```
|
|
|
|
`[get` writes the same form back for any value that would otherwise be misread, so its output can
|
|
be pasted straight into `[set`.
|
|
|
|
### The one inconsistency: `null`
|
|
|
|
`where` spells a null constant as a bare `null`. `[set` and friends use `(-null-)`, and read a bare
|
|
`null` as the four-letter string. This predates the shared parser and is preserved deliberately —
|
|
every existing `where … = null` clause depends on it.
|
|
|
|
```
|
|
[global count where Item Name = null -- Name is null
|
|
[set Name (-null-) -- set Name to null
|
|
[set Name null -- set Name to the string "null"
|
|
```
|
|
|
|
## `distinct`, `order by`, `limit`
|
|
|
|
```
|
|
[global interface where Item order by Hue desc limit 50
|
|
[global interface where Mobile distinct Name order by Name
|
|
```
|
|
|
|
- `distinct <prop> [<prop> …]` — keeps one object per distinct combination of those properties. It
|
|
sorts internally to do so, so it also reorders the set; add `order by` if the order matters.
|
|
- `order by <prop> [direction] [<prop> …]` — `by` is optional. Direction is `+`/`up`/`asc`/`ascending`
|
|
or `-`/`down`/`desc`/`descending`, defaulting to ascending. Multiple keys break ties left to right.
|
|
- `limit <n>` — keeps the first `n` after the others have run.
|
|
|
|
Keywords are case-insensitive, and **the order you type them does not matter**: they always apply
|
|
as `where` → `distinct` → `order by` → `limit`.
|
|
|
|
## `[interface`
|
|
|
|
```
|
|
[<scope> interface [view <properties …>] [condition]
|
|
```
|
|
|
|
Opens a gump listing every match instead of acting on them. Each row can be inspected, and `view`
|
|
adds columns for the properties you name. This is the safest way to see what a condition selects
|
|
before running something destructive with the same clause.
|
|
|
|
```
|
|
[global interface where Item Movable = false ItemID = 0x1F13
|
|
[global interface view Hue Name where Mobile Hits < 10
|
|
```
|
|
|
|
## `[batch`
|
|
|
|
`[batch` opens a gump that runs **several commands against one found set**. Type `[batch` with no
|
|
arguments; the gump has three parts:
|
|
|
|
- **Scope** — pick one of the scopes above.
|
|
- **Condition** — the whole clause, and it must start with `where`.
|
|
- **Commands** — one or more entries, each with a command and an optional **Object**.
|
|
|
|
Every command runs against the same matched set, in the order listed. It is the tool for "find
|
|
these once, then do three things to them" without re-running an expensive sweep, and for
|
|
multi-step edits that would otherwise race against their own filter — a `where Hue = 0` clause
|
|
re-evaluated after the first command has changed `Hue` would no longer match.
|
|
|
|
The **Object** field is a property chain that redirects that one command onto a sub-object of each
|
|
match. Leave it blank to act on the match itself; set it to `Backpack` to act on each mobile's
|
|
backpack instead. Objects whose chain is null or unreadable are skipped for that command.
|
|
|
|
Logging is suppressed automatically when the set is larger than 20 objects.
|
|
|
|
## Gotchas
|
|
|
|
- The type after `where` is mandatory; `where Movable = true` is a parse error, not a wildcard.
|
|
- Only `[CommandProperty]` members are reachable, and write access is checked separately from read.
|
|
- Relational operators need a comparable type; equality is available on everything.
|
|
- A bare integer targeting a `TextDefinition` is always a cliloc. Use `@"123"` for the string.
|
|
- `where … = null` and `[set … (-null-)` are different spellings of the same idea. See above.
|
|
- Spawner **Params** are split on plain spaces, not the quoting tokenizer, so a constructor
|
|
argument cannot contain a space. Spawner **Props** use the normal tokenizer and value syntax.
|
|
- Test a destructive clause with `[interface` or `count` first.
|
|
|
|
## Key files
|
|
|
|
| Concern | File |
|
|
|---|---|
|
|
| Scopes | `Projects/UOContent/Commands/Generic/Implementors/` |
|
|
| `where` parsing, operators | `Projects/UOContent/Commands/Generic/Implementors/ObjectConditional.cs` |
|
|
| Condition/sort/distinct compilation | `Projects/UOContent/Commands/Generic/Extensions/Compilers/` |
|
|
| Modifier parsing and apply order | `Projects/UOContent/Commands/Generic/Extensions/BaseExtension.cs` |
|
|
| Value parsing | `Projects/UOContent/Utilities/Types.cs` |
|
|
| Command tokenizer | `Projects/Server/Commands.cs` (`Commands.Split`) |
|
|
| `[batch` | `Projects/UOContent/Commands/Batch.cs` |
|
|
| `[interface` | `Projects/UOContent/Commands/Generic/Commands/Interface.cs` |
|
|
|
|
## See also
|
|
|
|
- `dev-docs/commands-targeting.md` — registering commands and the targeting system.
|
|
- <https://muo.gg/commands> — the full command list, regenerated with distro updates.
|