After a little fiddling, I came up with what I think is a better DTD for the conversation XML. It's mostly the same, just a bit more well-defined, and instead of having the text of a choice be an attribute, I turned it back into a text element (the way it was in the first one) that must be followed by either another conversation or a redirect element.
<?xml version="1.0"?>
<!DOCTYPE conversations [
<!ELEMENT conversations (conversation+)>
<!ELEMENT conversation (text*,(choice|redirect)?)>
<!ELEMENT choice (option+)>
<!ELEMENT option (text,(conversation|redirect))>
<!ELEMENT text (#PCDATA)>
<!ELEMENT redirect EMPTY>
<!ATTLIST conversation id ID #IMPLIED>
<!ATTLIST redirect toId IDREF #REQUIRED>
]>
I also tried to think of a way to use DTD Entities to allow for dynamic strings, but I couldn't. In the C# code that parses text, though, you could easily add a string formatter. So if the XML was:
<text>Oh, {0}! I didn't see you there</text>
Then you could use
text = String.Format(text,playerName);
And it would replace the {0} with a name the user had previously input. You could do this with any number of things. For example:
text = String.Format(text,party[0].Name,party[1].Name,party[2].Name,money);
This would replace all {0} with the 1st person's name, {1} with the 2nd, {2} with the 3rd persons name, {3} with the amount of money currently carried, and so on. However, if you have a lot of dynamic strings you want to work with, it might be easier to keep track of using String.Replace. For example:
<text>Oh! %NAME%! I didn't see you there!</text>
This could be handled simply:
text = text.Replace("%NAME%",party[0].Name);
To do what I previously outlined, with several names:
text = text.replace("%PARTY1%",party[0].Name)
.replace("%PARTY2%",party[1].Name)
.replace("%PARTY3%",party[2].Name)
.replace("%MONEY%",money);
This lets you keep the text in the XML file somewhat readable.