Reference · Engine
Field generation
partialfrom#19#25#33#38#130#145#164
A field is built from fieldSeed: a noise height map, fractal hills, a
dungeon entrance, maybe a lake, objects, ground cover and a start position.
tools/field.py gen reproduces it, checked against the game's own
WORLD::Generate run in tools/eemu.py (which interprets the EE FPU for
this): 150 fields, 0 mismatches - 105 story areas and 45 random keyword
triples, every field type but 4, 15,171 objects, height maps compared bit for
bit.
The port carries the generator in Rust (piney_data::field, worklog 33):
piney-gen (placement::field) reads the tables out of each volume's
executable into the build (field::tables_of), the heights are computed
with the EE's float rules,
and tools/test_field_rs.py compares it with field.py field by field.
Order of events
WORLD_MAN::GO: seed = fieldSeed; randcnt = 0
WORLD::Init consumes a fixed number of draws (snow, smoke):
field types 5, 6: 700, or 1600 when weather >= 4;
type 0: 112; types 2, 3, 5, 6: another 21
WORLD::Generate:
1 heights[80][80] = fieldrand(64) each FIELD::MakeField
2 story area (ccGame.field != 0): mark chips 19-21 x 19-21, flatten
around (20, 20), start there
3 hills: one large (fieldrand(10) + 20 cells) and 3 / 5 / 8 small
(fieldrand(10) + 10) by `ground`; each a FRACTAL2 of a 5x5 key grid
(fieldrand(128) on the rim, fieldrand(768) inside) interpolated with
natural splines, clamped at 0, placed at fieldrand(80 - size) x2,
retried up to 100 times while it overlaps another hill
4 dungeon entrance (SetDungeonEnter); a lake for field types 2, 3, 8, 9, 10
5 InitQuad
6 objects: key (20), sub (30), base (35), tree (35; SetTreeObject_B for
type 1); type 7 halves key, thirds base, halves tree; type 1 divides
trees by 6; all counts x 0.8 / 0.9 / 1.0 by `object`
7 InitQuad; ground cover (2 draws per free chip); start position
Grids: FIELD.map, 80 x 80 floats at FIELD+0xaf02c, 600 units per cell;
four 40 x 40 byte chip grids - check (+0xce430), check2 (+0xcea70),
check3 (+0xcf0b0, hidden) and mnt (+0xcf6f0, hill).
Each object: idx = fieldrand(count) in its table, then position
fieldrand(40 - w), fieldrand(40 - h) until the site passes that object's
tests (free chips, no hill, height span within 64); 300 failures in one call
end it. Objects with the flatten flag level their 3w x 3h cells. The tables
are FOBJECT_INFO_TABLE rows (0x1c bytes: anm, clump, type, w, h, rotflag,
flat) through FOBJECT_TABLE[11].
Of those fields, only the names, the size and flat are read.
- The tables are read in two places. The rows are reached only through
the six
FOBJECT_TABLEs.WORLD::Initreads them for the models' vertex colours, andGenerate's setters read them to place objects. type(0-3) is never read.- 0: key rows.
- 1: sub rows, the lake fields' own lake rows, and three of
BaseObjTBL_I's six (CMP_sfi3tom1-3). - 2: the other base rows and all tree rows.
- 3: entrance rows.
rotflag(0, 1 or 2 in 71 / 12 / 85 rows) is never read.FOBJECTandFOBJECT2keep no pointer to their row, and their constructors set the rotation to zero. So no field object is rotated.flatis always a single bit. 0x2 levels the site and hides it from the cover (118 rows), 0x4 marks the lake pieces (5), and 0x8 asks for the height test (45). The code tests them as bits.
The dungeon entrance in a story area must be at least 8,000 units from the
start - within 8,000 for event 14. The start in a random field is the first
free 2 x 2 chip with fieldrand(100) >= 96; protected story areas also need
it more than 7,000 from the entrance. Field type 4 has no field: its gate
leads straight into the dungeon.
Drawing
tools/field.py and piney_data::field (Field::scene) reproduce what
the game draws. The parts marked read were read from the code; the rest was
checked by running the game's functions in eemu (tools/test_field.py).
Ground.
- Tiles. Every chip is one copy of the type's
BaseMeshNamemodel: 24 vertices spanning ±600, vertexScale 600.RelocateMeshputs it at (600 + 1200·mx, 600 + 1200·my, 0), unrotated. - Heights and colours.
SetMESH2rewrites only z and colour. Vertex k gets the height of cell (2mx + dx, 2my + dy), stored as s16fptosi(4096·h / scale), and that cell's lit RGB, keeping its alpha. x, y and UVs stay the template's. - Hidden chips. A chip under a levelled object or the lake draws no tile.
Lighting. The ground's vertex colours are the map's own:
InitQuadmakes two face normals per cell, from opposite diagonals.CalcQuadVertexNormalaverages six faces per vertex. One of them isquad[x+1][y-1]where the neighbourquad[x][y-1]was meant, a game bug kept in the port.CalcVertexColorgives128·(ambient + max(0, −n·L)·colour), clamped to 0-255.- The light. It comes from
BGTBL[type][weather]: a light and an animation inbg_{x}{weather+1}, whose frame 0 carriesF_AmbientandF_DistantLight.Lambertpasses the light's direction vector toRotMatrixX/Y/Zas if it were angles; kept as the game does it. - Objects.
WORLD::Init'sCalcObjectVertexColorlights the type's table clumps the same way, in the loaded model data. A clump listed twice is lit twice:SubObjTBL_Khas two.
Cover. A cover is the 6-vertex ±300 template (vertexScale 300) at
(600 + 1200·i ± 300, 600 + 1200·j ± 300, 2.5). Quadrants 0 and 2 have x
negative, and 0 and 1 have y negative. SetSmallMESH gives it the
heights and colours of cell (2i + qx, 2j + qy), so it sits 2.5 above the
ground.
Object heights. WORLD::GetHeight casts a segment from z 2500 down to
−1200 into the two triangles of the point's 600-unit cell, using
collisionLP with 0.001 edge slack. It returns −500 on a hidden chip and −1
on a miss. An object's z is that query at the moment it is placed; the
entrance, the lake, type 1's tree rows and levelled objects get 0 instead.
Around the player (read). The field is a torus 48,000 units (80 cells)
across. DrawMesh draws the 12 × 12 chips around the player, −6 to +5 each
way, each at the copy of its position nearest the player. Tiles beyond
8,400 are culled, and tiles fade between 6,700 and 7,200 with alpha
(7200 − d) / 500. Cover draws within 7,200, fading from 6,700.
Lake (read). The three copies of field_eff's ANM_sfwat1_1a sit at
the lake object's centre, (x, y, 0). They differ in render layer: one
scrolls its UVs every frame, and two use a texture Generate builds at run
time.
Background (read).
DrawBGdraws the sky, two cloud layers and the mountains, centred on the player, each in its own background layer. Field type 6 adds an aurora (CMP_sfh7aur{weather+1}). Only the sky material's V scrolls, by 0.003 a frame.- Fog comes from the same per-type, per-weather table: colour, near, far and
a percentage.
ccDrawEnv::SetFog(0x00105820) turns them into VU1's fog terms (fogA, fogB, fMin, fMax), and each vertex's F isclamp(fogB + fogA * w, fMin, fMax)(render). The port'spiney_draw::DepthFog::set_fogis the same arithmetic.
Weather and ambient pictures
Beside the ground and the objects, WORLD makes and draws a field's
weather and its moving scenery. WORLD_MAN::GetWeather and GetTime read
the field type and the background row b (WORLD_MAN +0x0c):
GetWeather 0 fine, 1 cloudy, 2 rain, 3 storm, 4 light snow, 5 heavy snow
types 0-3 b 3: 1, else 0
types 4, 7-10 b 3: 1; b 4, 5: 2; b 6, 7: 3; else 0
types 5, 6 b 0-3: 4, else 5
GetTime 0 day, 1 evening, 2 night
types 4, 7-10 b 1: 1; b 0, 3, 4, 6: 0; else 2
types 5, 6 b 1: 1; b 0, 3, 4: 0; else 2
types 0-3 b 1: 1; b 0, 3: 0; else 2
CheckLensFlare not hacked (WORLD_MAN +0xf0 != 3), weather 0, 1 or 4,
time 0 or 1
What Init and Generate make (every draw fieldrand):
WORLD::Init weather 5: 200 SNOW(1) (8 draws each), weather 4: 100
SNOW(0) (7 each); type 0: 16 embers, SNOW(2) (7 each);
types 2, 3, 5, 6: the 2D smoke asleep fieldrand(300) +
150, its five puffs (SetSmoke2d: x -fieldrand(512), y
fieldrand(384), dx fieldrand(20) + 20, dy fieldrand(20)
- 10); type 0: DrawSteam(0); types 0-3: the heat haze
(sfzair1's ANM_sfzair1a, OBJ_sfzair00 duplicated)
WORLD::Generate after SetStartPos: server 0-4 (ccGame +0x1c, always):
(its tail) TOBJ when fieldrand(100) >= 86; weather 2, 3:
SetRainEffect (50 drops); weather 3: the thunder's four
sprites; night or hacked, not type 9: 5 FIREFLY at
(fieldrand(48000), fieldrand(48000)) on the ground; type
9: 15 FIREFLY2 about the start; type 10, not night, no
rain: BIRD over the dungeon entrance
SetSpecialObj (EntryGimmick) type 7's fires: each chip's FOBJECT
(fobj[x][y], y outer) with an animation, its node
OBJ_roc1fire then OBJ_roc3fire, at the node's place in
the animation plus the object's wp (+0x30), w 1
A SNOW places itself (calcPos) within 1,300 of WORLD_MAN's centre: at
800 above the ground, falling (fieldrand(5) + 1) b a frame and drifting
(fieldrand(a) - a/2) / 10 (light: a 50, b 2; heavy: a 200, b 4, and a fall
of fieldrand(40) + 10). Type 5's flakes are EFF_sfg8sno1-4, type 6's
EFF_sfh8sno1-4 (a quarter each by fieldrand(100)); the embers
(calcPos2) EFF_sfa8fir1 at half scale, rising from the ground and
fading over their last 32 frames. Move keeps each within 1,300 of the
player each way, and a flake below 0 is placed again.
What Draw draws, after the objects and the ground, on the layers
WORLD_MAN::SetActiveLayer names (0 sysLayer, 1 objLayer, 2 obj2Layer, 3
effLayer, 4 floorLayer, 5 charLayer, 6 refLayer):
first call weather 4, 5: ten DrawSnow (the flakes settle)
layer 6 the heat haze (types 0-3): the anm 3,000 ahead of the
player along the camera's heading (6,000 for types 2, 3),
scaled 1.3 across, stepped, transparency 0.7; its model's
texture is the picture drawn so far (waterUVModifi: each
vertex samples the screen where it stands)
layer 3 DrawEffect:
DrawRain weather 2, 3: every drop placed afresh each frame within
1,500 of camera 1's eye, on the ground; its pattern steps
0-19; at 20 its splash (EFF_sfp8rai6) plays 15 patterns
there, 10 above the ground
the storm weather 3: a bolt (EFF_sfp8thu1-4) 12,000 ahead of the
camera, up to 45 degrees either side, 900 up, every 30-59
frames (the first after 60-119), shown four frames at
WORLD's centre offset; a
screen flash (EntryFlash(10, 0x80ffffff, 0, 0, 512, 384))
every 900-1,499 frames (the first after 750), sound 236
30-49 frames later
the fires type 7, even frames: each fire within 6,500 of the eye
puffs effSmoke(fire + m (10, 0, 0), m (1, 0, 0) + (0, 0,
2.5), 1, 8, 208, 512, 32), m three ccRand turns
DrawSnow the flakes moved, then drawn; type 0: the embers
the 2D smoke types 2, 3, 5, 6: asleep 150-449 frames, then shown
150-449 frames: fieldrand(4) + 1 puffs of TEX_sfsmo1 (a
512 x 512 ccMask of the 128 x 128 texture, its colour's
alpha 0x60) drifting right across the screen
the fireflies night, hacked, or type 9: type 9's FIREFLY2s (none in
rain), else the 5 FIREFLYs
layer 5 TOBJ Move, Draw; BIRD Move, Draw
layer 3 the lens flare (CheckLensFlare): LENSFLARE::Draw(bg,
DMY_<x>lig_<b+1>point, 1)
type 0: DrawSteam(1) - every 120-270 frames eight bursts,
3-6 frames apart, of 2-6 effSmoke puffs (t 109) from a
vent within 1,000 of the player, its sound 43 (ccSeOn3D)
at the first
FIREFLY flies a loop of three closed natural splines (NSPLINE, the
FRAC2 spline) about its base with a trail of sixteen sprites and throws
off FIREFLY2 sparks; hacked it is a data bug, EFF_sfzdigi0 with a
trail of EFF_sfzdigi1 glyphs (a new trail sprite every sixth frame, each
pattern fieldrand(15)). Type 9's FIREFLY2s (evarea.md's
class, type 1) wander within 2,000 of the camera and are put back about it
past that. TOBJ is the server's traveller (named here from its files):
Δ's airship (t1), Θ's balloon (t2, its two cloths' V scrolled 0.003
and 0.0015 a frame), Λ's whale (se3_5wh1), Σ's (t4) and Ω's sleigh
(t5, with two runners at its OBJ_dummy_rei0/1). It shows for 630-1,229
frames, fading in and out - Δ's, Λ's and Ω's flying across at 10 a frame,
the others standing - then sleeps. It casts its shadow (ccDrawEnv +0xa0
three times its height, +0xa4 its transparency) into WORLD_MAN's second
packet, 128 x 128 laid twice (the shadow volumes). The airship and the whale
hum: Init starts seData[44] looping silent (tobjSeLoopStart), and
each Draw while its transparency is not 0 sets the loop's pan and volume
for its place, the volume scaled by the transparency (tobjSeLoop;
sound.md). BIRD (CMP_sfp8how1)
flies out from the dungeon entrance at 20 a frame, turning 0.0314 a frame;
after half a turn it is back at the entrance and turns the other way.
The depth shades. WORLD_MAN::WORLD_MAN makes two ccBufferSamplings
(+0x418, +0x41c) with SetShade(0, 896, 0, 8, 7, 3000, 0x48808080) and
SetShade(0, 896, 0, 7, 6, 6000, 0x48808080) (main 0x00106c70): CLAMP
both ways, scale 1, and a texture of their own (texuse 1) at x 896 of
buffer 0, 256 x 128 and 128 x 64. Every WORLD::Draw sends them first on
sysLayer (+0x41c, then +0x418; each prepends, so the 3,000 one draws
first). MakePacket (main 0x00106dd0) sends two strips for each:
- a context-2 strip that copies the whole 512 x 448 picture being drawn into the texture (bilinear, no tests, no blend, Z masked);
- that texture drawn back over sysLayer's view box (UVs its whole size,
bilinear), blended
(Cs - Cd) As + Cdat 0x48, alpha NEVER with FB_ONLY, Z GEQUAL at the depth's Z with no Z write.
So each pixel farther than 3,000 takes 56% of a quarter-size copy, and
beyond 6,000 again of an eighth-size one: the distance goes soft. sysLayer,
made at start-up, draws after GO's objLayer of the same priority 0
(piney-desktop's Layers::older), so the sky, the ground and the objects
soften while the characters (charLayer) and the effects stay sharp. The
port draws them as piney_desktop::noiz::Sampling::shade_own's strips on
TexRef::ScaledFrame, a texture the renderer reads through the smaller
copy; the strips match what the game's MakePacket sends in eemu (places,
Z, UVs, colour).
The lens flare in a field is LENSFLARE::Draw in mode 1:
ccCheckCameraDeg is asked about the camera's own place, so it tests only
that the camera looks within 67.5 degrees of +x; the sun is the background
dummy taken to the player's side of the map, each flare wrapped likewise.
The game's slips, kept.
DrawRainsetsWORLD+0x1c where it means the drop's "placed" flag, so every drop is placed again every frame (50 x 2 draws).- A SNOW's height asks
WORLD_MAN::GetHeight(x, y != -500): the ground at y 0 or 1. WORLD::Init's SNOWs askGetHeightbeforeGeneratehas made the map: heap garbage in the game (zeros in eemu, and in the port). They ask aboutWORLD_MAN's centre as the last area left it (GOdoes not set it); the port takes the start position.- BIRD's far test is
sqrt(d.y + d.y + d.x^2) > 9,600. - The 2D smoke's transparency is an int: it goes from 1 to 0 in a frame.
- The bolt's sideways offset
fieldrand(6000) - 3000is converted as unsigned: a negative one lands near 2^32. FIREFLY2::SetBasePosition2asks the height at the offset, not at the place.
The port. piney_world::field_ambient makes and steps all of it:
Ambient::init and generate at FieldArea::with_world, Ambient::draw
a frame's pictures as a list of Ops in the game's order (sprites,
effSmoke puffs, the 2D smoke's masks, models, sounds, flashes);
field_firefly is FIREFLY, firefly FIREFLY2. FieldArea keeps
fieldrand where Generate left it and the drawing's function statics
(the steam's, the thunder's timers, the balloon's scrolls) across fields
as the game does; ambient_frame runs a frame (the last one's list again
while the tasks sleep) and ambient_models draws the haze (the frame
buffer as its texture), TOBJ and BIRD and the 2D smoke. The sprites go
through piney-effect (piney-game fx/ambient.rs: each ccEff made once as
Init left it, fog and depth test as the game set them), the puffs through
effSmoke, the sounds and the flash through the area's events.
tools/test_field_ambient_rs.py runs the game's WORLD::Init,
Generate and Draw (with DrawEffect, DrawRain, DrawSnow,
DrawSteam, SNOW, FIREFLY, FIREFLY2, TOBJ, BIRD and the lens flare as they
are) in eemu and the ambient_probe example side by side: every field
type and background row, hacked and not, a random server and seed, and a
walk of the player and the camera; compared are fieldrand's count after
Init and after Generate, what they made, and each frame's pictures in
order (names, places, patterns, scales, transparencies, fog bits, layers,
matrices, the masks, the puffs, the sounds with TOBJ's hum, the flash)
with the counts of fieldrand and ccRand. 124 cases of 1,000 frames: 0
mismatches.
piney-game --mode field:14/TYPE,ROW[,HACK[,SEED]] shows another field's
weather over story area 14.
EE floating point
The hills are floating-point, so matching them needs the EE FPU's rules, as
tools/eemu.py now implements: no denormals (exponent 0 is zero), no
infinities or NaNs (exponent 255 is an ordinary number), overflow saturates
to the largest float, division by zero gives it signed, results truncate
toward zero, and madd/msub round the product first. Real hardware
sometimes differs from exact truncation in the last bit; that is not
modelled and has not shown up in any case checked.
The maths library linked into the executable (sinf, cosf, atan2f,
tanf, fmodf and their kernels) is newlib's fdlibm, built for a target
whose largest exponent is normal (_FLT_LARGEST_EXPONENT_IS_NORMAL), so
it has no NaN or infinity cases. piney_data::libm transcribes each
routine operation for operation under these rules.
Other volumes
Each volume's own WORLD::Init and WORLD::Generate in eemu against
tools/field.py, per volume MUT, OUT, QUA:
- all 110 type and weather pairs;
- 33 random fields (every type three times);
- all 112 typeable story areas.
0 mismatches. FIELD, WORLD and the grid offsets are the same on all
four, as are every constant, the six object tables and SmallMeshName.
Differences:
ccGetDistin OUT and QUA (OUT0x001e6e70) uses the FPU'ssqrt.sinstead of newlib'ssqrtf.field.pypicks by scanning the function. No case has shown a difference (grid distances never land within an ulp of 8000 or 7000).IsProtectAreareturns the area-71/47 substitutes and searches 127 records. From MUT on, area 100'sprotect[1]is 0, so 27 areas are protected instead of 28.- From MUT on,
saveData+0x6772 == 1orWORLD+0x2c == 3selects other BG tables inInitandGenerate. This has no effect on the RNG or the layout.
The generated tables of the four volumes are equal but for the square
root (volume.rs's each_volume_has_its_tables). placement::field reads
WORLD::Generate's constants from the code, and Outbreak's and
Quarantine's compiler moved them: this in $s2 (Infection's $s0,
found from the prologue's move), the objects' percentage in $f1
(Infection's $f20), a nop after the SetDungeonEnter call's delay slot,
and the hill loops' counters in $s3/$s4 the other way round (taken
from each loop's decrement).
Unknown
-
The water's run-time texture and UV scroll, the objects' own draw code, and the type-6
FOBJECT2sDrawMeshdraws. -
Whether a tile's alpha is clamped beyond 7,200.
-
WORLD_MAN::EntryGimmick'sSetFood,SetMagicCircleandSetSpecialObj(battle.md) drawfieldrandon the entry task's first slice. That task (priority 64) runs beforeccThFieldDisp(96) in the first walk, so its draws followGenerate's and come before the weather's. The port makes them on the frame ofPlay(0), before the field's first draw. Type 7's fires are still found as the field is made. -
The port's
effSmokepuffs start at the next frame's effect task, a frame after the game's (itsWORLD::Drawruns beforeccThParticle). -
BIRD's clump is drawn as its model at the bird's matrix, and the weather's sprites are drawn unfogged (as all the effects' are).
-
The shots (rain, snow, night with fireflies, the bird, type 7's fires, type 0's steam, hacked) are the port's; no frame of the game's own picture was compared.