Jump to content

Classic bug: Galactic perimeter edge errors

From Galactic Empire wiki
Bug ID GE-BUG-009
Affected versions Classic 3.2e and published classic source
Status Verified in source
Fixed in ge-next 0.1; further corrected in 0.2 and 0.3.2
Primary source GEMAIN.C random placement, GEFUNCS.C movement and perimeter handling, GELIB.C rndm(), and GEPLANET.C sector creation

Summary

Classic describes UNIVMAX as the galaxy's radius and says that the galaxy extends that many sectors north, south, east, and west. With the default value of 300, that description implies sector 0 with 300 sectors on either side: sector numbers -300 through 300, or 601 sectors on each axis.

The classic implementation instead provides only 600 complete sectors on each axis. In practical play its coordinate interval is -300 through just under +300, which maps to sectors -300 through 299. The positive sector 300 has no usable width. This produces a 600 by 600 galaxy (360,000 sector positions), not the expected 601 by 601 galaxy (361,201 positions).

The same perimeter code also contains wrapping, corner-impact, and NPC behavior problems.

How classic establishes the galaxy

The galaxy is not created as a complete two-dimensional array when the game is started. Ship and wormhole locations are stored as floating-point galactic coordinates. A sector record is created in the planet database only when code such as getsector() or xgetsector() requests that location, normally because a player enters or scans it. Planets and wormholes for a previously unknown sector are generated at that time.

Classic converts a floating-point coordinate into a sector number with:

return ((int)floor(dcoord));

Consequently, sector 0 occupies coordinates 0.0 through just under 1.0, sector 299 occupies 299.0 through just under 300.0, and sector -300 occupies -300.0 through just under -299.0.

Random full-galaxy positions for Cybertrons, wormhole destinations, and the optional background planet builder use the equivalent of:

rndm(UNIVMAX * 2) - UNIVMAX

At the default setting this produces coordinates from -300 through +300. The random function scales rand() by RAND_MAX, so the exact upper endpoint +300 is technically possible when rand() returns RAND_MAX. Almost every result is below +300, however, and therefore maps to one of the 600 full sectors from -300 through 299. Exact +300 maps to sector 300, but it is only one coordinate at the boundary rather than a usable 10,000-unit sector.

Why sector 300 is not a full sector

After every movement update, classic checks the raw floating-point coordinates:

if (xcoord > UNIVMAX) ...

and performs the corresponding checks for the negative X and both Y edges. With UNIVMAX=300, a ship may occupy exactly 300.0, but any positive movement into the interior of sector 300 immediately crosses the perimeter. The complete positive-edge sector would require coordinates from 300.0 through just under 301.0.

This creates an asymmetric-looking set of sector numbers even though the raw coordinate interval is 600 units wide:

  • Negative edge: sector -300 is a complete sector.
  • Center: sector 0 is a complete sector.
  • Positive edge: sector 299 is the last complete sector.
  • Sector 300 is only the exact boundary coordinate and cannot be traversed as a normal sector.

The classic NAVIGATE command compounds the confusion. It accepts 300 as a destination sector, then aims for coordinate 300.50001, the middle of that nominal sector. Movement reaches the perimeter before the ship can arrive.

Wrapping behavior

Classic's wrapping math confirms that it treats the galaxy as 600 units wide. Crossing the positive edge subtracts UNIVMAX * 2, while crossing the negative edge adds the same amount. For example:

  • Coordinate +300.1 wraps to -299.9, which is sector -300.
  • Coordinate -300.1 wraps to +299.9, which is sector 299.

That is internally consistent for sectors -300 through 299, but not for the documented radius model of sectors -300 through 300. A 601-sector galaxy must retain the complete positive sector 300 and wrap across a span of 601 sectors.

Other perimeter effects

When wrapping is disabled, classic replaces a coordinate that crossed an edge with UNIVMAX - 2, or its negative equivalent, and calls telezip(). At the default size this places the ship at +298 or -298, stops it, and applies perimeter damage. The displacement is always two coordinate units regardless of impact speed.

The X and Y checks independently perform the complete collision response. A ship that crosses both boundaries in one movement update is moved back on both axes, but telezip() is also called twice. The player therefore takes perimeter damage twice for one corner impact and receives duplicate feedback.

NPC ships are stopped by the same routine without being explicitly turned back toward the galaxy. This does not normally produce an immediate series of repeated impacts, because telezip() sets both the NPC's current speed and target speed to zero. The NPC must receive another AI decision before it can accelerate again.

A Cybertron pursuing a player will ordinarily recalculate head2b as the vector toward that player and then assign a new target speed. Because a valid player target is inside the perimeter, this will usually turn the Cybertron back into the galaxy. A cruising Cybertron's periodic update eventually chooses both a random heading and a random speed. Droids depend on their individual class routines; after a perimeter impact, some can simply remain stopped until their class behavior gives them another reason to move.

Classic therefore lacks a deterministic NPC recovery rule, but repeated outward impacts are not its normal behavior. The more realistic outcomes are that other AI logic eventually redirects the NPC, or that the NPC remains stopped near the edge for some time.

Correction

ge-next 0.1 made UNIVMAX match its documented meaning. With the default value of 300, valid full sectors are -300 through 300. The continuous coordinate interval therefore extends from -300.0 through just under +301.0, and wrapping uses a span of (UNIVMAX * 2) + 1, or 601 at the default setting. A ship leaving the positive side of sector 300 wraps into sector -300, and a ship leaving the negative side of sector -300 wraps into sector 300.

ge-next 0.2 refined non-wrapping perimeter collisions. The backward displacement became dependent on impact speed, a corner crossing applies only one damage event while correcting both coordinates, and the ship is turned toward sector 0 0. For NPCs, this replaces classic's reliance on a later class-specific AI decision with an immediate and deterministic inward course.

ge-next 0.3.2 retained the speed-sensitive response but capped its displacement relative to UNIVMAX. This prevents a high-speed impact from throwing a ship across most or all of an unusually small configured galaxy, a case made important by ge-arena's small match galaxies.

Compatibility notes

ge-next preserves the administrator's choice between a wrapping galaxy and a repelling perimeter. Correcting the coordinate span adds the missing positive edge sectors, so existing classic databases should be reset when changing between implementations or changing UNIVMAX, as the classic configuration documentation itself recommends.