Client files and world queries
The game server reads the Ultima Online client files it needs for gameplay: the tile
properties (tiledata.mul), the maps with their statics, and the multi layouts of
houses and boats. On top of them it answers two questions every game system asks:
can a mover take this step, and can this point see that one; and a third built on the
first: which way does a mover walk from here to there. C# code reaches all of
this through six services of Moongate.Server.Ultima, registered by the Ultima
plugin in game and standalone modes.
| Service | Answers | Call from |
|---|---|---|
ITileDataService |
Name, flags, weight, height of a land or item graphic | Any thread |
IMapService |
Terrain and statics of a map cell | The game loop |
IMultiService |
The components of a house, boat or other multi | Any thread |
IMovementService |
Whether a step is allowed and the Z it lands at | The game loop |
ILineOfSightService |
Whether one point sees another | The game loop |
IPathfindingService |
The steps a mover walks between two places | The game loop |
The map readers share their buffers and one file handle per map, so the map, movement and line of sight services belong on the game loop. Tile data and multis are copied into memory and only read afterwards.
What is read at startup
Section titled “What is read at startup”| Priority | Service | Reads | Stops the server when |
|---|---|---|---|
| -10 | IUltimaDataService |
ultima.ultima_path, the client version, tiledata.mul |
the directory or tiledata.mul is missing |
| -5 | IDataLoaderService |
the shard data files, including data/maps.toml |
a data file is missing or invalid |
| -4 | IMapService |
map{n}.mul or map{n}LegacyMUL.uop, staidx{n}.mul and statics{n}.mul for every map of maps.toml, n being its file_index |
a map lacks one of its files |
| -4 | IMultiService |
every multi from MultiCollection.uop, or from multi.idx with multi.mul when the client has no UOP file |
neither format is there, or no multi can be read |
The error messages and their fixes are in
Common startup problems. Set the client
directory with ultima.ultima_path.
A plugin service that reads maps or multis starts after -4; see the
startup priorities.
Tile data
Section titled “Tile data”var backpack = tileDataService.GetItem(0x0E75); // "backpack", weight 3, Container | Wearablevar water = tileDataService.GetLand(0x00A8); // "water", Impassable | Wet| Member | Meaning |
|---|---|
LandCount, ItemCount |
The number of land and item graphics; the item count depends on the client version |
GetLand(id), GetItem(id) |
The tile; ArgumentOutOfRangeException for an id out of range |
TryGetLand(id, out tile), TryGetItem(id, out tile) |
The tile, or false for an id out of range |
LandTile has Id, Name, Flags and TextureId. ItemTile has Id, Name,
Flags, Weight (255 means it cannot be picked up), Height, StandHeight (half the
height for a Bridge such as a stair), Layer, Quantity and Animation. The first
call copies the tables into read-only arrays, so every later lookup is an array index.
var land = mapService.GetLand(MapType.Felucca, 1602, 1591); // cobblestones, Z 20var statics = mapService.GetStatics(MapType.Felucca, 1400, 1500); // willow tree and leaves, Z 10var name = tileDataService.GetItem(statics[0].Id).Name;| Member | Meaning |
|---|---|
Maps |
The maps loaded, in the order of maps.toml |
Contains(map, x, y) |
Whether the map is loaded and the cell lies inside it |
GetLand(map, x, y) |
The land graphic id and Z of the cell |
GetStatics(map, x, y) |
Each static object’s item graphic id, Z and hue, in file order; empty when there are none |
A map that is not loaded throws KeyNotFoundException; a cell outside the map throws
ArgumentOutOfRangeException. Blocks of 8x8 cells are read the first time a cell in
them is asked for and kept in a bounded cache.
Multis
Section titled “Multis”var house = multiService.GetMulti(0x64); // 148 components, from (-3, -3) to (4, 4), height 36| Member | Meaning |
|---|---|
Count |
The number of multis loaded |
GetMulti(id) |
The multi; KeyNotFoundException when the client has none with this id |
TryGetMulti(id, out multi) |
The multi, or false |
A MultiDefinition has Id (its item graphic is 0x4000 plus the id), Min and
Max, the smallest and largest X and Y offsets, Height, the highest Z offset, and
Components. Each MultiComponent has an ItemId, an Offset from the multi’s
centre as a Point3D, and Visible: hidden components, such as the centre marker,
only take up space.
Movement
Section titled “Movement”IMovementService uses the rules of ModernUO, which match the client’s own
prediction: a mover is 16 units tall, climbs at most 2 units per step, stands on half
the height of a bridge such as a stair, and a diagonal step needs both cells beside
it free.
if (movementService.CheckMovement(MapType.Felucca, from, DirectionType.East, MovementAbilityType.Walk, out var newZ)){ // the step is allowed; the mover lands at newZ}TryGetSwimZ(map, x, y, out z)finds where a swimmer can be placed at a cell: the highest water with room for a person above it, so not under a dock or a bridge; it is false outside the map, on a map not loaded, or with no such water.MovementAbilityType.Walkmoves over land, statics and surfaces that are not water;Swimenters water;Walk | Swimdoes both.PassDoors, added to either, walks through doors: game masters and administrators have it.- The items lying on the ground of a cell count as its statics do, by the tile data
of their graphic: an impassable one in the mover’s way blocks the step, such as a
closed door, a crate or a wall placed by
.decorate; a surface one that cannot be picked up, such as a floor or a stair placed there, can be stood on. An open door has swung onto the next cell, so its doorway is free and that cell is not. An item above the mover’s head or under its feet does not block. - Only the low three bits of the direction count, so
DirectionType.Runningis ignored. - A step that leaves the map, or starts outside it, returns false with
newZequal to the starting Z; a blocked step returns false with the Z the mover stands at. GetAverageZ(map, x, y)gives the terrain height at the centre of a cell, from its four corners.
Line of sight
Section titled “Line of sight”ILineOfSightService walks POL’s integer 3D line between two points and tests each
point with ModernUO’s rules. Pass both points at eye or target height; a mobile’s eye
is its Z plus 14:
var visible = lineOfSightService.HasLineOfSight(MapType.Felucca, new Point3D(1602, 1591, 20 + 14), new Point3D(1610, 1591, 20 + 14));- Statics flagged
WindoworNoShootand terrain block the line; a blocker at the target’s cell and height does not. - Points farther than
ultima.line_of_sight.max_distance(default 25) along X or Y are never in sight, and neither is a point outside the map. - A map that is not loaded throws
KeyNotFoundException. - The service allocates nothing: each cell’s statics are read once, even when the line crosses it several times.
Pathfinding
Section titled “Pathfinding”IPathfindingService.FindPath(map, from, to, ability, allowPartial) finds the shortest way a
mover can walk between two places with A*, and gives a PathResult:
| Field | Holds |
|---|---|
Kind |
Found, Partial, NotFound or TooFar |
Steps |
The directions to walk from the start, one per step; empty when there is no path |
End |
Where the steps lead: the goal, the closest place for a partial path, or the start |
var path = pathfinding.FindPath(npc.Map, npc.Location, target.Location, MovementAbilityType.Walk, true);
// A path to where the NPC already stands is found with no steps.if (path.Kind is PathResultType.Found or PathResultType.Partial && path.Steps.Count > 0){ mobiles.TryMove(npc, path.Steps[0]);}Every step of a path is one IMovementService.CheckMovement allows, from the height the step
before it landed at, so a path is made of steps the mover can really take: a diagonal needs
both tiles beside it, and a swimmer (MovementAbilityType.Swim) gets a path through water. A
straight step costs 10 and a diagonal 14, as ModernUO, UOX3 and Sphere, and the octile
distance to the goal guides the search, so the path found is the shortest inside what it looked
at. The goal is reached when a step lands on its tile within a mover’s height (16) of it.
Two settings bound a search:
| Key | Default | Bounds |
|---|---|---|
ultima.world.pathfinding_range |
38, as ModernUO | How far apart start and goal may be along X or Y; farther is TooFar with nothing searched. The search looks at a square of this size plus one, placed so that both lie in it |
ultima.world.pathfinding_max_nodes |
1000, as ModernUO | How many places a search expands before it gives up with NotFound |
With allowPartial, a goal that cannot be reached gives the path to the place closest to it
among those the search expanded (Partial), unless none is closer than the start.
A search runs to its end inside the call, on the game loop. Measured on the Trammel of the
7.0 client around Britain: a path of 4 steps took 0.1 ms, one of 36 steps 1.3 ms, one of 48
steps 6.6 ms, and a search that found nothing took 11 to 12 ms, the 1,000 places of the cap.
So a caller must not search again on every tick for a goal that was not reached: wait before
trying again, as ModernUO’s two seconds. INpcPathService does that for NPCs: it keeps the
path each one walks and gives its next step (Next(npc, goal, range, ability), then Stepped once the step was tried;
Forget(npc) drops the path of an NPC that was deleted).
It searches only with no steps left or a changed goal, two seconds after the NPC’s last
search at the soonest, ten when that search did not reach the same goal, and for ten NPCs a
second in the whole server; an NPC that may not search steps straight towards its goal. The Lua function
npc.walk_to is built on it.
A path sees what the movement sees: a closed door, a crate or any impassable item on the
ground blocks it, and it goes around. A search with MovementAbilityType.OpenDoors plans through the closed doors that are not locked, which is how npc.walk_to searches for an NPC that opens doors; the step itself is still stopped by the door, and the NPC opens it first. Other mobiles do not block
a path, since the movement does not consider them. A tile on the way has one height in a
search, the one of the shortest way to it, so a path cannot pass both over and under the same
tile, such as across a bridge and then beneath it. The goal’s own tile is different: it is
only entered at the goal’s height, so a goal on a balcony is reached by its stairs and not
stopped at on the ground under it.
Follow a partial path to its end before searching again: a search from each new tile can choose another closest place, and the mover would walk back and forth. A goal nobody can stand on, such as a wall, always costs a whole search.
Not included yet
Section titled “Not included yet”Mobiles and placed multis are not part of the movement and line of sight checks, and
items on the ground are part of the movement only: a line of sight still passes through
a closed door. Where an NPC or a dropped item is placed (TryGetSpawnZ, TryGetDropZ)
looks at the land and the statics, not at the items on the ground. See
Implementation status.
Made with ❤️ by Tom
