Case study · Touhou 4: Lotus Land Story

Reconstructing TH04's
bullet patterns.

A ring of bullets starts with a short angle calculation. Use REA to inspect the original DOS function, then follow its instructions into readable C++.

PC-98 · 16-bit DOS · C++ and assembly

View the TH04 reconstruction

From original instructions to checked source

  1. 01 · Original gameMAIN.EXEThe PC-98 executable and its bullet-angle helper.
  2. 02 · Inspect with REAInstructions + referencesVerify the loaded DOS code and read the angle calculation from the executable.
  3. 03 · Read the C++Space → aim → rotateConnect each operation to the maintained reconstruction.
  4. 04 · Check the source2,139 matching bytesRecorded compiler checks cover the bullet-generation code that contains this helper.
REA returns the original code and its analysis evidence. The TH04 project supplies the maintained source and compiler checks.

How a ring aims at the player

Elly's attack requests an aimed ring with an initial count of 16. One helper calculates the direction of each bullet. The same ring can face a fixed direction or turn toward the player.

Your coding agent

Use REA to find how TH04 creates a ring of bullets and aims it at the player. Explain the calculation and show the code.

An example prompt, with the original executable and reconstruction checkout available locally.

TH04 stores a full clockwise turn in 256 angle units. For 16 bullets, each index adds 16 units: 0, 16, 32, …, 240. Adding the player's direction rotates those positions together.

Two diagrams of a 16-bullet ring. The fixed ring starts at angle zero. Adding a player direction of 40 rotates every direction by the same amount; the highlighted first bullet points toward the player. Both keep a spacing of 16 angle units, or 22.5 degrees.
An illustration of the recovered angle math for count 16, player direction 40 and template rotation 0. Open figure

The firing template in the recovered source

Elly's ring attack · source excerpt
bullet_template.angle = 0;
bullet_template.group = BG_RING_AIMED;
bullet_template.count = 16;
bullet_template_tune();

These lines select the pattern before difficulty and performance tuning. Read the attack's source.

What REA returns

We inspect bullet_velocity_and_angle_set(), the function that sets each bullet's velocity and angle. REA returns its instructions, pseudocode and references. Three parts of that result explain the pattern.

  1. Check the DOS image before reading its code

    Agent → REA
    open_binary
    {"path": "MAIN.EXE"}
    
    inspect_native_load_image
    {}
    REA → agent · selected fields
    status: "verified"
    format: "dos-mz"
    load_segment: 4096
    header_bytes: 6144
    module_bytes: 150114

    The check ties the loaded code and relocation records to the supplied executable. The path above is shortened for display.

  2. Read the ring's multiply and divide

    Agent → REA · analyze_function
    {"procedure": "0x2cfc8"}
    REA → agent · instruction excerpt
    0x2d0eb: MOV AX, SI
    0x2d0ed: SHL AX, 0x8
    0x2d0f0: MOV DL, byte ptr [0x53af]
    0x2d0f9: IDIV BX

    SI holds the bullet index. The shift multiplies it by 256, then the division uses the count read from DS:53AF. The maintained source names that field bullet_template.count. The complete sequence appears below.

  3. Find where the pattern turns toward the player

    Same function result · aim excerpt
    0x2d190: SUB AX, word ptr [0x53a6]
    0x2d198: SUB AX, word ptr [0x53a4]
    0x2d19d: CALLF 0x1000:1da8
    0x2d1a2: ADD word ptr [BP + -0x2], AX
    Corresponding maintained C++
    group_angle_offset += iatan2(
      player_pos.cur.y - bullet_template.origin.y,
      player_pos.cur.x - bullet_template.origin.x
    );

    The instruction operands expose two coordinate differences and a direction helper. Its return value is added to the relative ring angle. Source names make those addresses readable; REA lets the agent check the original operations behind them.

From assembly to readable C++

Select a step to match the original instructions with the angle calculation. This summary follows the aimed-ring path; the full source also handles spreads, stacks and random patterns.

REA · original 16-bit instructions
0x2cfcf: MOV SI, word ptr [BP + 0x4]
0x2d0eb: MOV AX, SI
0x2d0ed: SHL AX, 0x8
0x2d0f0: MOV DL, byte ptr [0x53af]
0x2d0f4: MOV DH, 0x0
0x2d0f6: PUSH DX
0x2d0f7: CWD
0x2d0f8: POP BX
0x2d0f9: IDIV BX
0x2d0fb: MOV word ptr [BP + -0x2], AX
0x2d18d: MOV AX, [0x4650]
0x2d190: SUB AX, word ptr [0x53a6]
0x2d194: PUSH AX
0x2d195: MOV AX, [0x464e]
0x2d198: SUB AX, word ptr [0x53a4]
0x2d19c: PUSH AX
0x2d19d: CALLF 0x1000:1da8
0x2d1a2: ADD word ptr [BP + -0x2], AX
0x2d1b9: MOV AL, byte ptr [BP + -0x2]
0x2d1bc: ADD AL, byte ptr [0x53ad]
0x2d1c0: MOV [0xbcc8], AL
Readable C++ · aimed-ring summary
unsigned char aimed_ring_angle(
    int index, int count,
    int player_direction,
    unsigned char rotation)
{
    int angle = index;
    angle = (angle * 256) / count;
    angle += player_direction;
    angle += rotation;
    return (unsigned char)angle;
}

01 · Read the bullet index. The stack argument is loaded into SI. In a 16-bullet ring, this member index runs from 0 through 15.

The assembly selects the aimed-ring branch, aim calculation and final store. The C++ above is a readable summary. Compiler checks below apply to the maintained source in the linked project.

Read the maintained ring and aim source
Ring macro · TH04 branch, excerpt
angle_offset = (i * BULLET_ANGLE_FULL_TURN) / count;
if (i >= (count - 1)) {
    group_complete = true;
}
goto aim_or_no_aim;
Angle and velocity · source excerpt
aim:
    group_angle_offset += iatan2(
        (player_pos.cur.y - bullet_template.origin.y),
        (player_pos.cur.x - bullet_template.origin.x)
    );

no_aim:
    vector2_near(
        bullet_template.velocity,
        (group_angle_offset + bullet_template.angle),
        speed
    );
    bullet_spawn_angle = (group_angle_offset + bullet_template.angle);

Ring macro · Aim and velocity source · Angle representation

Checking the maintained source

The TH04 project's recorded checks compile the bullet-generation code, including this helper, with its pinned Turbo C++ 4.0J toolchain.

2,139 matching bytes

The compiled bullet-generation code matches a 2,139-byte section of the original executable.

Two clean builds

Two separate builds produce the same matching bytes. The recorded input hashes match the maintained source shown here.

Recorded on 3 October 2026. Read the compiler checks.

Target identity and analysis scope
Original target MAIN.EXE, 156,258 bytes, DOS MZ
Helper entry 0x2cfc8 in REA's loaded image
Original file offset 0x1e7c8, returned by address_to_file_offset
Recorded code inspection REA 4.1.0 · 7 October 2026

Compiler checks cover the complete bullet-add code section, including this helper. They also compare its MAP contribution and overlapping relocation.

REA verified the static DOS load image at load segment 0x1000. Its addresses describe that analysis image. The function result reports 503 body bytes in two ranges across a 517-byte span; the selected instructions above lie in those reported ranges.

The source names and compiler checks come from TH04 checkpoint 0d72e980. Earlier reconstruction observations used the project's own analysis scripts; this page adds a fresh REA inspection of the same original target. The supplied game's identity is pinned by its repository manifest, with independent pristine-dump provenance still open.

The figure illustrates the angle calculation. Full-game controls, timing and PC-98 hardware behavior have separate runtime checks in the reconstruction project.

The full TH04 reconstruction

The project builds the game's four DOS products from maintained C++ and assembly. Beyond bullet generation, the source covers stages, bosses, rendering, input, menus, endings and score handling.

Explore the TH04 repository

Top