Skip to content

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.

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.

var backpack = tileDataService.GetItem(0x0E75); // "backpack", weight 3, Container | Wearable
var 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 20
var statics = mapService.GetStatics(MapType.Felucca, 1400, 1500); // willow tree and leaves, Z 10
var 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.

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.

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.Walk moves over land, statics and surfaces that are not water; Swim enters water; Walk | Swim does 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.Running is ignored.
  • A step that leaves the map, or starts outside it, returns false with newZ equal 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.

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 Window or NoShoot and 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.

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.

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

Buy me a coffee