The Legend of Zelda: The Wind Waker
Nintendo's 2002 GameCube Zelda, sailed across an ocean by a wind the player turns, read through the zeldaret decompilation.
On this page
The atlas reads The Wind Waker through the Zelda Reverse Engineering Team’s decompilation, C++ that rebuilds each GameCube release; the claims here cite the code the USA release compiles. Its boat actor doesn’t yet compile to the game’s bytes as a whole, so the boat claims read the decompilers’ reconstruction. Names and comments quoted here are theirs. Nothing has been measured in the game yet.
The boat’s speed comes from the wind. The code compares the wind’s direction with the boat’s facing: with the wind along the heading the sail gives its full top speed of 55, across it three quarters, at 135° under a third, and past about 140° nothing1. With the sail down, the stick moves it at up to 10 instead1. The tiller turns it fastest at rest and under a third as fast at full sail2. With the sail up and enough speed, R hops the boat3.
The wind has a strength from the room and a direction from the player. Each room’s wind level gives 0.3, 0.6, or 0.9, and the boat reaches its top speed only at 0.5 or more4. The Wind’s Requiem points the wind one of eight ways, 45° apart, and the save keeps the choice5.
The clock counts a day as 360 units and adds 0.02 an update in rooms where time passes; in other rooms it runs only until 11:00 by day or 23:00 by night6.
Mechanics
| Mechanic | Kind | How |
|---|---|---|
| Sailing | Wind angle | the angle between the wind and the boat's facing scales a top speed of 55: full with the wind along the heading, three quarters across it, none past 140° |
| Wind | Player-set | the Wind's Requiem picks one of eight directions, 45° apart, kept in the save; each room's wind level sets the strength |
| Time of day | Game clock | a day is 360 units, 15 an hour, advancing 0.02 an update where time passes; elsewhere it creeps only to 11:00 or 23:00 |
Every claim about this game, by concept (6)
References
- ^ abIn the decompilation's C++ for the boat (d_a_ship, which doesn't yet match the game's bytes as a whole; the USA GZLE01 build), procSteerMove, while the boat isn't airborne (daSFLG_FLY_e off), takes the gap between the wind vector's yaw (cM_atan2s of its x and z) and the boat's facing shape_angle.y, as a 16-bit angle from 0 to 0x8000, and turns it into a wind factor: 1 − gap/0x10000 below 0x4000 (90°), 0.75 − (gap − 0x4000) × 5.493164e-05 up to 0x6000 (135°), then 0.3 − (gap − 0x6000) × 0.00032967035, floored at 0. So the factor is 1 with the wind along the boat's facing, 0.875 at 45°, 0.75 at 90°, 0.3 at 135°, and 0 from 140° to 180° (computed). The target speed is the wind power divided by 0.5, capped at 1, times the factor times 55. The branches then run in this order, one per update. First, on the body animation's frame 7, if speedF is below twice the target (at most 80) it is raised to that and mTargetSpeed is set to the target (10000 if the boat is being moved by force), and nothing else changes speedF that update. Otherwise, while mTargetSpeed is below speedF, firstDecrementShipSpeed lowers speedF toward mTargetSpeed (a tenth of the gap, at most 5, at least 1) and sets mTargetSpeed to 10000 once the gap is under 3; this comes before the sail check, so it applies with the sail flag off too. Otherwise, with the sail-on flag off or a target of 0, decrementShipSpeed eases speedF toward 10 × the stick's value × the cosine of its angle (a twentieth of the gap, at most 0.1, at least 0.015). Otherwise decrementShipSpeed eases it down to a lower target, and cLib_addCalc up to a higher one (a tenth of the gap, at most 0.5, at least 0.1).Codezeldaret/twwrevised
How we know
1594 if (!checkStateFlg(daSFLG_FLY_e)) { 1595 cXyz* windVec = dKyw_get_wind_vec(); 1596 f32 targetSpeed = *dKyw_get_wind_power() / 0.5f; 1597 int angleDiff = abs((s16)(cM_atan2s(windVec->x, windVec->z) - shape_angle.y)); ⋮ 1600 if (angleDiff < 0x4000) { 1601 windFactor = 1.0f - angleDiff * (1.0f / 0x10000); 1602 } else if (angleDiff <= 0x6000) { 1603 windFactor = 0.75f - (angleDiff - 0x4000) * 5.493164e-05f; 1604 } else { 1605 windFactor = 0.3f - (angleDiff - 0x6000) * 0.00032967035f; 1606 if (windFactor < 0.0f) { 1607 windFactor = 0.0f; ⋮
src/d/actor/d_a_ship.cpp lines 1594–1637 at cb24f55 · upstream ↗ 1090void daShip_c::decrementShipSpeed(f32 i_targetSpeed) { 1091 cLib_addCalc(&speedF, i_targetSpeed, 0.05f, 0.1f, 0.015f); ⋮ 1095void daShip_c::firstDecrementShipSpeed(f32 i_targetSpeed) { 1096 if (cLib_addCalc(&speedF, i_targetSpeed, 0.1f, 5.0f, 1.0f) < 3.0f) { 1097 mTargetSpeed = 10000.0f;
src/d/actor/d_a_ship.cpp lines 1090–1098 at cb24f55 · upstream ↗ 22f32 cLib_addCalc(f32* pValue, f32 target, f32 scale, f32 maxStep, f32 minStep) { 23 if (*pValue != target) { 24 f32 step = scale * (target - *pValue); 25 if (step >= minStep || step <= -minStep) { 26 if (step > maxStep) { 27 step = maxStep; ⋮ 29 if (step < -maxStep) { 30 step = -maxStep; ⋮ 32 *pValue += step;
src/SSystem/SComponent/c_lib.cpp lines 22–32 at cb24f55 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
- ^In the decompilation's C++ for the boat (d_a_ship, not yet matching as a whole; the USA build), procSteerMove moves the unnamed angle m0366, which the atlas reads as the tiller, toward mStickMVal × 0x2000 × −sin(mStickMAng) with cLib_addCalcAngleS(…, 4, l_HIO.tiller_speed, 0x100), tiller_speed being 700, and, unless the boat is airborne, passes it to setMoveAngle. Unless the boat is being moved by force (checkForceMove), setMoveAngle sets a rate of 4 − |speedF| / 55 × 3, held between 0.1 and 3.6, blends it toward 3.6 by the unnamed counter m0388 over 15 (3.6 once m0388 reaches 15), and turns the facing by −rate × (m0366 >> 6). With m0388 at 0 and a tiller of 0x2000, that is about 461 units (2.5°) an update at rest and 128 units (0.7°) at speed 55 (computed): the boat turns slower the faster it sails.Codezeldaret/tww
How we know
1586 cLib_addCalcAngleS(&m0366, mStickMVal * 0x2000 * -cM_ssin(mStickMAng), 4, l_HIO.tiller_speed, 0x100); 1587 if (!checkStateFlg(daSFLG_FLY_e)) { 1588 setMoveAngle(m0366);
src/d/actor/d_a_ship.cpp lines 1586–1589 at cb24f55 · upstream ↗ 71const s16 daShip_HIO_c0::tiller_speed = 700;src/d/actor/d_a_ship.cpp line 71 at cb24f55 · upstream ↗ 1113void daShip_c::setMoveAngle(s16 moveAngle) { 1114 if (!checkForceMove()) { 1115 s16 initialAngle = shape_angle.y; 1116 f32 turnRate = 4.0f - std::abs(speedF) / 55.0f * 3.0f; 1117 if (turnRate > 3.6f) { 1118 turnRate = 3.6f; ⋮ 1120 else if (turnRate < 0.1f) { 1121 turnRate = 0.1f; ⋮ 1123 if (m0388 >= 0xF) { 1124 turnRate = 3.6f; ⋮ 1126 else { 1127 turnRate += ((3.6f - turnRate) * m0388) / 15.0f; ⋮
src/d/actor/d_a_ship.cpp lines 1113–1130 at cb24f55 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^In the decompilation's C++ for the boat (d_a_ship, not yet matching as a whole; the USA build), procSteerMove allows a hop when no event runs, Link isn't barred from control (checkNoControll), the body animation is FN_MAST_ON2 at frame 7 or later, the boat isn't airborne, its forward speed mFwdVel is over 16.5, it isn't being moved by force, and the sailing target speed is at least 11. Then the R trigger's lock (mDoCPd_R_LOCK_TRIGGER) sets the airborne and jump flags and an upward speed equal to the target speed, held between 15 and 40, and sets mJumpRate to (speed.y − 11) × 0.014545455.Codezeldaret/tww
How we know
1638 if (!dComIfGp_event_runCheck() && 1639 !daPy_getPlayerLinkActorClass()->checkNoControll() && 1640 m0392 == dRes_INDEX_SHIP_BCK_FN_MAST_ON2_e && 1641 mpBodyAnm->getFrame() >= 7.0f && 1642 !checkStateFlg(daSFLG_FLY_e) && 1643 mFwdVel > 16.5f) { 1644 if (!checkForceMove() && targetSpeed >= 11.0f) { 1645 onStateFlg(daSFLG_JUMP_OK_e); 1646 if (mDoCPd_R_LOCK_TRIGGER(0)) { 1647 onStateFlg((daSHIP_SFLG)(daSFLG_FLY_e | daSFLG_JUMP_e)); 1648 speed.y = targetSpeed; 1649 if (speed.y < 15.0f) { ⋮
src/d/actor/d_a_ship.cpp lines 1638–1658 at cb24f55 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abIn the USA build, dKyw_wind_set, when no actor has set a wind vector override (mpWindVecOverride), takes the wind's strength from the room's file list (dStage_FileList_dt_GlobalWindLevel): level 0 gives 0.3, level 1 gives 0.6, level 2 gives 0.9, any other level 0. A custom wind power above 0 replaces it, an event wind setting of 2 makes it 0, and it is capped at 1. While mInitAnimTimer is nonzero the wind vector and power jump to the new values; otherwise each call moves each vector component a tenth of the way (at most 2, at least 0.001) and the power a tenth of the way (at most 1, at least 0.005). Since the boat, in the decompilers' not-yet-matching C++ for d_a_ship (procSteerMove), divides the power by 0.5 and caps it at 1, a level-0 room tops out at 0.6 of the boat's 55, 33 units an update, and levels 1 and 2 at the full 55 (computed, once the power has settled).Codezeldaret/twwrevised
How we know
990 if (g_env_light.mWind.mpWindVecOverride != NULL) { 991 wind_vec = *g_env_light.mWind.mpWindVecOverride; 992 strength = g_env_light.mWind.mWindStrengthOverride; ⋮ 1022 if (fili_p != NULL) { 1023 wind_level = dStage_FileList_dt_GlobalWindLevel(fili_p); ⋮ 1026 switch (wind_level) { 1027 case 0: 1028 strength = 0.3f; 1029 break; 1030 case 1: 1031 strength = 0.6f; 1032 break; ⋮
src/d/d_kankyo_wether.cpp lines 990–1040 at cb24f55 · upstream ↗ 1062 if (g_env_light.mWind.mCustomWindPower > 0.0f) { 1063 strength = g_env_light.mWind.mCustomWindPower; ⋮ 1067 if (g_env_light.mWind.mEvtWindSet == 2) { 1068 strength = 0.0f; ⋮ 1071 if (strength > 1.0f) { 1072 strength = 1.0f; ⋮ 1075 if (g_env_light.mInitAnimTimer != 0) { 1076 g_env_light.mWind.mWindVec = wind_vec; 1077 g_env_light.mWind.mWindPower = strength; 1078 } else { 1079 f32 scale = 0.1f; 1080 cLib_addCalc(&g_env_light.mWind.mWindVec.x, wind_vec.x, scale, 2.0f, 0.001f); ⋮
src/d/d_kankyo_wether.cpp lines 1062–1084 at cb24f55 · upstream ↗ 1596 f32 targetSpeed = *dKyw_get_wind_power() / 0.5f;src/d/actor/d_a_ship.cpp line 1596 at cb24f55 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
- ^In the USA build, the wind-direction picker dOperate_wind_c (which the atlas reads as the Wind's Requiem's, from its TAKT_WIND sounds; "takt" is the baton) sets mDeg on opening from the current wind vector's yaw and then, with the stick pushed to at least 0.9, sets mDeg to one of eight compass values, 0 to 315 in 45-degree steps. A confirms it: windSet passes dKyw_tact_wind_set a pitch of 0 and a yaw of 180° − mDeg (0 gives −0x8000, 90 gives 0x4000, 270 gives −0x4000). dKyw_tact_wind_set stores both angles for the environment and in the save (dComIfGs_setWindX, setWindY) and sets the tact-wind flag; dKyw_wind_set then builds the wind vector as (cos pitch × cos yaw, sin pitch, cos pitch × sin yaw). dKyw_wind_init reloads the saved angles unless both are −1. B cancels the picker.Codezeldaret/tww
How we know
372 if (stick->getValueStick() >= 0.89999998f && fabs(stick_value) - fabs(field_0x8b8) >= 0.0) { 373 int stick_control = dOw_stickControl(field_0x8d0, stick->getAngleStick()); ⋮ 377 switch (stick_control) { 378 case 8: 379 if (mDeg != 315) { 380 field_0x8ca = 7; 381 mDeg = 315; 382 mDoAud_seStart(JA_SE_TAKT_WIND_CURSOR);
src/d/d_operate_wind.cpp lines 370–384 at cb24f55 · upstream ↗ 462void dOperate_wind_c::windSet() { 463 switch (mDeg) { 464 case 315: 465 dKyw_tact_wind_set(0, -0x6000); 466 break; 467 case 0: 468 dKyw_tact_wind_set(0, -0x8000); 469 break; 470 case 45: 471 dKyw_tact_wind_set(0, 0x6000); 472 break; 473 case 90: ⋮
src/d/d_operate_wind.cpp lines 462–488 at cb24f55 · upstream ↗ 613 cXyz* wind_vec = dKyw_get_wind_vec(); 614 s16 temp_r3 = cM_atan2s(wind_vec->x, wind_vec->z) + 0x8000; 615 mDeg = dOw_angleRegular(((180.0f * temp_r3) / (f32)0x8000) - 90.0f); 616 arrowRotate(&field_0xec, mDeg);
src/d/d_operate_wind.cpp lines 613–616 at cb24f55 · upstream ↗ 661 if (CPad_CHECK_TRIG_A(0)) { 662 windSet(); 663 dComIfGp_setOperateWindChangeOff(); 664 } else if (CPad_CHECK_TRIG_B(0)) { 665 dComIfGp_setOperateWindCancelOff();
src/d/d_operate_wind.cpp lines 661–666 at cb24f55 · upstream ↗ 1328void dKyw_tact_wind_set(s16 i_windX, s16 i_windY) { ⋮ 1332 env_light.mWind.mTactWindAngleX = i_windX; 1333 env_light.mWind.mTactWindAngleY = i_windY; 1334 dComIfGs_setWindX(i_windX); 1335 dComIfGs_setWindY(i_windY); ⋮ 1338 env_light.mWind.mTactWindAngleFlags = 1;
src/d/d_kankyo_wether.cpp lines 1328–1338 at cb24f55 · upstream ↗ 1010 } else if (g_env_light.mWind.mTactWindAngleFlags != 0) { 1011 tact_wind_x = g_env_light.mWind.mTactWindAngleX; 1012 tact_wind_y = g_env_light.mWind.mTactWindAngleY; ⋮ 1018 wind_vec.x = cM_scos(tact_wind_x) * cM_scos(tact_wind_y); 1019 wind_vec.y = cM_ssin(tact_wind_x); 1020 wind_vec.z = cM_scos(tact_wind_x) * cM_ssin(tact_wind_y);
src/d/d_kankyo_wether.cpp lines 1010–1020 at cb24f55 · upstream ↗ 967 if (dComIfGs_getWindX() == -1 && dComIfGs_getWindY() == -1) { 968 g_env_light.mWind.mTactWindAngleFlags = 0; ⋮ 972 g_env_light.mWind.mTactWindAngleFlags = 1; 973 g_env_light.mWind.mTactWindAngleX = dComIfGs_getWindX(); 974 g_env_light.mWind.mTactWindAngleY = dComIfGs_getWindY();
src/d/d_kankyo_wether.cpp lines 967–974 at cb24f55 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^In the USA build, the time of day is a float from 0 to 360, 15 to an hour (dKy_getdaytime_hour divides by 15); dKy_daynight_check calls 6:00 to 17:59 day. The environment's setup sets the advance mTimeAdv to 0.02, which dKy_custom_timeset can change. Each setDaytime call adds it only when no event-night stop is set, the save's get-item flag (2, 0) is set, no event runs, and (USA and PAL only) Link isn't on stage "sea" room 14 with stage layer 2 or 3. In rooms that pass time (getTimePass) the clock wraps from 360 to 0 and the day of the week goes up by one; in other rooms it advances only by day before 165 (11:00) and by night from 270 (18:00) to 345 (23:00). At 0.02 a day is 18,000 setDaytime calls (computed).Codezeldaret/tww
How we know
476 g_env_light.mTimeAdv = 0.02f;src/d/d_kankyo.cpp line 476 at cb24f55 · upstream ↗ 502void dScnKy_env_light_c::setDaytime() { ⋮ 504#if VERSION > VERSION_DEMO ⋮ 506#endif ⋮ 510#if VERSION > VERSION_JPN 511 if (strcmp(dComIfGp_getStartStageName(), "sea") == 0 && dComIfGp_roomControl_getStayNo() == 14) ⋮ 513 if (dComIfGp_getStartStageLayer() == 2 || dComIfGp_getStartStageLayer() == 3) { 514 var_r30 = true; ⋮ 517#endif ⋮ 519 if (!dKy_checkEventNightStop() && dComIfGs_isGetItem(2, 0) && dComIfGp_event_runCheck() == FALSE 520#if VERSION > VERSION_DEMO 521 && !var_r30 522#endif ⋮
src/d/d_kankyo.cpp lines 502–538 at cb24f55 · upstream ↗ 614int dKy_getdaytime_hour() { 615 return dComIfGs_getTime() / 15.0f; ⋮ 625int dKy_daynight_check() { 626 s32 hour = dKy_getdaytime_hour(); ⋮ 628 if (hour >= 6 && hour < 18) { 629 return dKy_TIME_DAY_e;
src/d/d_kankyo.cpp lines 614–631 at cb24f55 · upstream ↗ 3361void dKy_custom_timeset(f32 i_speed) { 3362 g_env_light.mTimeAdv = i_speed;
src/d/d_kankyo.cpp lines 3361–3362 at cb24f55 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
Sources
Code
- zeldaret. 2026. “tww: a decompilation of The Legend of Zelda: The Wind Waker (zeldaret/tww).” Commit cb24f55. zeldaret/tww @ cb24f55 · licence, all uses