How to add new speech groups to WC2

We know that WC2, SO1 and SO2 have only certain important plots equipped with audio speech, and the rest just come with subtitles. This is the situation for all platform versions of WC2.

So, is there a way to make other parts of the story also equipped with voice?

Note: This is a technical discussion, and I know the biggest issue is actually finding voice actors to dub the plots.

1. The speech files:

So, first of all, let's take a look at the speech files of WC2. They are in a very simple package format. A speech file, named with SPEECH.S** or COMMUNIC.S**, starts with an index area for all the audio sections, and then there are sequential audio sections. The audio sections just store the raw 8bit PCM data. Note that the samplerate might be 10752Hz, a very strange one, as I found in DOSBox Debugger.

If you would like to take a look at the format, please read this:

2. How does the game load the speech files?

I once had an extravagant expecting that the program could automatically find a matching voice based on the text of the line, which is unrealistic. The speech playing is achieved via opcodes in the game script.

Now, let's take a look at the main script file of WC2: SERIES.S00

Thanks to @UnnamedCharacter for Wing Commander Toolbox. I could easily unpack the script file with this tool:

Code:
WCToolsCmd WC2:PC:XmlUnpack SERIES.S00

Then I got the unpacked files: SERIES.S00.xml and a lot of bin files.

In the xml, I found a lot of <ContainerBlock> elements. It seems each one links with a mission in the game. The first one contains such data:

XML:
              <SymbolItem Text="  " />
              <SymbolItem Text="K'Tithrak Mang &#xA; Kilrathi Sector HQ" />
              <SymbolItem Text="I will speak with Prince Thrakhath alone.&#xA;Guards, you are dismissed." />
              <SymbolItem Text="Arise, grandson." />
              <SymbolItem Text=" " />
              <SymbolItem Text="How goes the war against the Terrans?" />
...

See it? These are the lines of the opening scene. Yes, all the lines of the opening are here.

Then, I found another element:
XML:
        <SpeechGroup>
          <FileChunk>
            <FileChunk.Entries>
              <FileEntryItem Block="0" Identifier="0" />
              <FileEntryItem Block="1" Identifier="0" />
...
              <FileEntryItem Block="17" Identifier="0" />
              <FileEntryItem Block="0" Identifier="1" />
              <FileEntryItem Block="1" Identifier="1" />
...
              <FileEntryItem Block="15" Identifier="1" />
              <FileEntryItem Block="0" Identifier="2" />
              <FileEntryItem Block="1" Identifier="2" />
...
              <FileEntryItem Block="9" Identifier="2" />
            </FileChunk.Entries>
            <FileChunk.Names>
              <FileEntryNameItem Name="speech.s00" />
              <FileEntryNameItem Name="speech.s01" />
              <FileEntryNameItem Name="speech.s02" />
            </FileChunk.Names>
          </FileChunk>
        </SpeechGroup>

So, it is pretty clear. Identifier corresponds to speech file, Block corresponds to the audio section.

And then how does the game load them? The answer is in the assembled script:

XML:
            <OffsetChunk file="SERIES.S00-ContainerBlock000-ContainerGroup-SequenceGroup-ScriptGroup-OffsetChunk.bin" />

I am using this tool to assemble/disassemble the scripts:


Note: The ASSEMBLE / DISASSEMBLE COMMANDS of WC Toolbox do not work for me. I just use the standalone one.

Code:
WC2Assembler.exe SERIES.S00-ContainerBlock000-ContainerGroup-SequenceGroup-ScriptGroup-OffsetChunk.bin

And I got a text file: SERIES.S00-ContainerBlock000-ContainerGroup-SequenceGroup-ScriptGroup-OffsetChunk.asm

To make a long story short, after observing, analyzing and experimenting, I found that the minimum unit of voice playback is as follows:

Code:
  _0055:  c036           0                      ; push constant, byte
  _0057:  c162           0                      ; preload speech
  _0059:  c036           0                      ; push constant, byte
  _005b:  c040         761                      ; set global[<uintvar:index>]

Then:

Code:
  _0074:  c111           2                      ; get text

All these above let you hear or see the Kilrah emperor saying "I will speak with Prince Thrakhath alone. Guards, you are dismissed."

Have you discovered the mystery? This line is written in the third element of the text group (3rd is 2, since the beginning is 0), which corresponds to the first element (1st is 0) of the speech group.

So the following line of "Arise, grandson." works by these:

Code:
  _0085:  c036           0                      ; push constant, byte
  _0087:  c162           1                      ; preload speech
  _0089:  c036           0                      ; push constant, byte
  _008b:  c040         761                      ; set global[<uintvar:index>]

Then:

Code:
  _00a4:  c111           3                      ; get text

Next, I have found these:

Code:
  _0100:  c036           0                      ; push constant, byte
  _0102:  c162           2                      ; preload speech
  _0104:  c036           1                      ; push constant, byte
  _0106:  c162           3                      ; preload speech
  _0108:  c036           2                      ; push constant, byte
  _010a:  c162           4                      ; preload speech
  _010c:  c036           3                      ; push constant, byte
  _010e:  c162           5                      ; preload speech
  _0110:  c036           0                      ; push constant, byte
  _0112:  c040         761                      ; set global[<uintvar:index>]

So, I guess the usage of this combination is:

Code:
c036           0  ; Indicate the speech slot
c162           2  ; Preload speech
c036           1  ; Indicate the next slot, if you need to do so
c162           3  ; Preload the next speech
...
c036           0  ; Done now!
c040         761  ; Ready to play the speech blocks!
...

So could I just only use the 1st speech slot, solt 0, before each get text?

Well, the answer is YES!

3. How do we add new speeches?

See here:
Code:
  _00cb:  c111          53                      ; get text

It's Shadow's words: "Well, it's another exciting day at Action Central."

I think I could just use the speech playing opcodes before it:

Code:
  _0244:  c036           0                      ; push constant, byte
  _0245:  c162          22                      ; preload speech
  _0246:  c036           0                      ; push constant, byte
  _0247:  c040         761                      ; set global[<uintvar:index>]

  _00cb:  c111          53                      ; get text

Note that the entry at the beginning. Why the new opcodes starts with entries begin with _0244? That is because, the last opcode line in this section is:

Code:
  _0243:  c014       _0000                      ; jmp

Each entry must be unique within the segment. So, we could use the entry numbers larger than the last one in the original script.

Now, Shadow speaks with Blair's voice. Yes! I added a new speech to WC2 cutscene!

But, of course, I would like to add REAL new speech voices, not letting a woman pilot speak in a man's voice.

So, let's back to the SERIES.S00.xml.

XML:
        <SpeechGroup>
          <FileChunk>
            <FileChunk.Entries>
              <FileEntryItem Block="0" Identifier="0" />
              <FileEntryItem Block="1" Identifier="0" />
...
              <FileEntryItem Block="9" Identifier="2" />
              <FileEntryItem Block="0" Identifier="3" />
            </FileChunk.Entries>
            <FileChunk.Names>
              <FileEntryNameItem Name="speech.s00" />
              <FileEntryNameItem Name="speech.s01" />
              <FileEntryNameItem Name="speech.s02" />
              <FileEntryNameItem Name="speech.s30" />
            </FileChunk.Names>
          </FileChunk>
        </SpeechGroup>

Note that I added something new! speech.s30, yes, it is made from Shadow's communication speech file.

And in the asm:

Code:
  _0244:  c036           0                      ; push constant, byte
  _0245:  c162          43                      ; preload speech, THE REAL NEW SPEECH BLOCK!
  _0246:  c036           0                      ; push constant, byte
  _0247:  c040         761                      ; set global[<uintvar:index>]

  _00cb:  c111          53                      ; get text

Finally, let's pack it up!

Code:
WC2Assembler.exe SERIES.S00-ContainerBlock000-ContainerGroup-SequenceGroup-ScriptGroup-OffsetChunk.asm
WCToolsCmd WC2:PC:XmlPack SERIES.S00.xml

Backup the original SERIES.S00 in WC2's GAMEDAT directory, then copy the newly generated SERIES.S00 to that directory.

4. How to make a new speech file?

Use XmlUnpack finction to unpack a current speech file, you will get the xml and a lot of wav files. Open the xml, the sturcture is prety clear. You could add or remove audio block elements. Then XmlPack it, that's all.

5. What is the audio format again?

8bit, 10752Hz, mono. But I think you could use 11025Hz ones, at most they just sounds lower and longer in 10752Hz mode, very slightly.

NOTE: Currently WCToolbox does not accept 10752Hz wav files.

6. Let Hobbes talk in a cutscene!

Now, we could find the line of "Hobbes to å. Inflight comm check.&#xA;Switch to channel 3-2-7." in <ContainerBlock> 008.

So, let's take a look at the disassembled script. There is something new in the following opcodes:

Code:
    _0498:  c135          49,    12

This is a special get text operation, means say in a certain interface. In this case, it is the player sees Hobbes speaking in communication.

So, just treat it as a normal get text.

And, this <ContainerBlock> has no <SpeechGroup>. Do not worry, just add one.

If everything is correct, you could hear hobbes talking in this cutscene.

7. How do I test the cutscenes quickly?

Launch the game like this:

wc2.exe Origin s3 m0

This specific command directly takes you to Ghorah Khar system, Mission 1.

So just change the numbers, you will be taken to any mission.

Under construction...
 
Last edited:
Back
Top