完成版の配布ページはこちら
アーカイブ
Alpha版公開~最終Alpha版の半分くらいまで
ここでは、それを備忘録的に残しています。 ちょっと前の内容と重複する部分(とくにトピックについての説明)もありますが、改めて仕様部分の詳細を解説しています。
4/9以前: AddTopicの力技とその限界
その4の後、Alpha版をリリースしてからは、とにかくトピックまわりとの戦い。
従来のアプローチ
以前はこの問題を AddTopic コマンドの自動注入で対処していた。テキストマッチングが壊れている箇所を検出し、スクリプトで強制的にトピックを追加する。信頼度に応じて4段階のティアに分類し、管理していた。
| ティア | 説明 | 件数 |
|---|---|---|
| AUTO_INJECT | 高信頼。自動注入して安全 | 32件 |
| REVIEW_PROMOTE | 中高信頼。レビュー済みで安全 | 36件 |
| REVIEW | 中信頼。レビュー済み | 42件 |
| TEXT_MARKER_ONLY | 低信頼。テキスト言及のみで検出 | 6,551件 |
「出ない」より「出すぎ」を選んだ
Morrowindの会話システムでは、NPCの発言テキスト中にトピック名と一致する文字列があると、その部分が自動的にハイパーリンクになる。プレイヤーはそれをクリックして新しいトピックを発見し、クエストを進める。これがゲーム進行の根幹だ。
英語では “Have you delivered the coded message?” と書けば coded message がリンクになる。しかし日本語に訳すと「暗号の伝言を届けたか?」になり、トピック名暗号化されたメッセージとは一致しない。
リンクが消える。トピックが発見できない。クエストが進まない。これは翻訳品質の問題ではなく、構造的な不一致だ。
こういう箇所が7,964件ある。
これを放置すれば、日本語版は「翻訳されているが遊べない」状態になる。そして、当時の手持ちの手段はAddTopicだけだった。
当時の判断は「トピックが出すぎるリスク」と「トピックが出ない=ゲーム進行不能のリスク」を天秤にかけて、前者を許容した。
- 「出すぎ」は最悪でもプレイヤーの選択肢が増えるだけで、会話自体が壊れるわけではない。
- 「出ない」はクエストが進行不能になる。
TEXT_MARKER_ONLYはレビュー完了次第、上位ティアへの昇格かリジェクトを判定する想定だったが、レビュー未完のまま全ティアをビルドに取り込んでいた。ゲーム進行不能のリスクに対する安全策として、未レビュー分も含めたリリースを選択した形だ。
ただ、この考え方には盲点があった。
AddTopicを使うのは失敗だった
あとからソースコードを読んで、テキストマッチングが会話スコープに閉じるのに対してAddTopicはグローバルに作用してしまうことが判明した。
テキストマッチングとAddTopicは、外から見ると同じ「トピックを追加する」動作に見えるが、エンジン内部のスコーピングがまったく異なる。
テキストマッチングの実際の挙動
NPCの応答テキストが画面に表示されるとき、エンジンは addTopicsFromText() を呼ぶ。この関数は2段階のフィルタを持っている。
第1段階: テキスト走査(parseTopicIdsFromText)
応答テキストを KeywordSearch で走査し、既知のDIALトピック名に一致する部分文字列を検出する。
"Have you delivered the coded message?"ならcoded messageがヒットする。
第2段階: NPC応答フィルタ(mActorKnownTopics)
ここが重要な点だ。検出されたトピックは、そのまま追加されるわけではない。
エンジンはまず updateActorKnownTopics() を呼んで、今話しているNPCがそのトピックに対して有効な応答(INFO)を持っているかを判定する。この判定はNPCの種族・クラス・所属・好感度・スクリプト条件をすべて評価する。
応答を持つトピックだけが mActorKnownTopics に入る。
void DialogueManager::addTopicsFromText(const std::string& text)
{
updateActorKnownTopics(); // ← 今のNPCの応答可能トピックを構築
for (const auto& topicId : parseTopicIdsFromText(text))
{
if (mActorKnownTopics.count(topicId)) // ← このNPCが応答を持つ場合のみ
mKnownTopics.insert(topicId);
}
}つまりテキストマッチングは二重にスコープされていた。
英語版で coded message がリンクになるのは、そのNPCが coded message トピックへの応答INFOを実際に持っているからだ。応答を持たないNPCのテキストに偶然 coded message が含まれていても、トピックは追加されない。
「テキストに出現する」かつ「今のNPCが応答を持つ」
これがテキストマッチングでハイパーリンクが作られる条件である。
AddTopic(スクリプトコマンド)の実際の挙動
一方、ResultScriptの AddTopic はこうなっている。
void DialogueManager::addTopic(const ESM::RefId& topic)
{
mKnownTopics.insert(topic); // ← 無条件で追加。NPCフィルタなし
}つまり、フィルタが存在しない。
トピックIDを受け取って、プレイヤーの既知トピックリストに直接挿入する。今誰と話しているか、そのNPCがそのトピックに応答を持つか、一切関係ない。
一度追加されたトピックは、以降そのトピックへの応答を持つ全NPCとの会話で選択肢として出現する。
これは便利なようで、非常にバギーな挙動だった。
この差が何を意味するか?
テキストマッチングの場合:
NPC Aの応答に
coded messageが含まれている → NPC Aがcoded messageに応答を持つ
→ トピック追加 → NPC Aとの会話で選択可能
AddTopicの場合:
スクリプトが
AddTopic "coded message"を実行 → 無条件でトピック追加
→ NPC A, B, C, D…coded messageへの応答を持つ全NPCとの会話で即座に選択可能
6,551件のAddTopic注入が意味していたのは、383トピックをこの「NPC応答フィルタなし」のルートで追加することだった。
結果として、プレイヤーがまだ会ったことのないNPCや、ストーリー上まだ到達していないクエストの会話選択肢が、序盤から全面的に露出した。
「トピックが出すぎるリスク」は、想定よりもはるかに深刻だった。「出すぎる」のではなく、クエストの時系列と文脈が完全に崩壊するのだ。
アルマレクシアとの初対面でラストダンジョンの選択肢が出る。ディヴァイス・ファーとの初対面で未受注クエストが進行する…などなど。
Construction Setのドキュメントからはこの非対称性は読み取れず、エンジンのソースコードを読んで初めて判明した。「どうやって安全にAddTopicを注入するか」ではなく、「AddTopicを使わずにリンクを復元できないか」を最初に考えるべきだった。
クエスト境界フィルタの実装
AddTopicで補正すればクエスト境界が崩壊し、補正しなければリンク消失で進行不能になる。どちらに倒しても壊れる以上、まずは応急処置としてクエスト境界フィルタを実装した。
「同じクエスト内の注入だけ許可すればいいのでは?」という発想で、ESMのジャーナル条件(SCVRサブレコード)を解析し、各INFOがどのクエストの文脈で表示されるかを機械的に判定するフィルタを実装した。
判定は4分岐。
- トピックにクエスト情報がない → マージ(汎用トピックなので安全)
- INFOにクエスト情報がない → ブロック(汎用会話にクエスト固有トピックを入れるのは危険)
- クエストIDに交差がある → マージ(同一クエスト内の正当な補完)
- クエストIDに交差がない → ブロック(クロスクエスト注入)
ブロックされた2,528件の「INFOにクエスト情報がない」ケースの中身を調べると、大部分は Hello、Greeting 5、Idle といったいつでも表示される汎用会話だった。
ここにクエスト固有のトピックを注入すれば、初対面から何でも出る状態になる。ブロックして正解だった。
これによって、6,551件中 842件を安全にマージ、5,709件のクロスクエスト注入をブロック。報告された3件のバグはすべて解消した。
しかし、これはAddTopicの被害を限定する応急処置であり、消えたリンク7,964件の根本対応は別途必要だった。
7,964件の仕分け
クエスト境界フィルタと並行して、「消えたハイパーリンク」の全体像を把握する作業も行った。
7,964件とは、31,504行の全INFOレコードを走査して検出した「英語テキストにはDIALトピック名が含まれているが、日本語テキストには対応する日本語DIALトピック名が含まれていない」箇所の数だ。
- つまり、英語版ではリンクになるが日本語版ではリンクにならない箇所が7,964ある。
しかし7,964件すべてが深刻なわけではない。「消えたリンク」が本当にゲームに影響するのは、そのテキストマッチングがトピック発見の唯一の手段だった場合に限られる。3つの観点で分類した。
| 分類 | トピック数 | 行数 | 深刻度 |
|---|---|---|---|
| AddTopicで到達可能 | 324 | 6,817 | 低(見た目のみ) |
| 他JPテキストに出現 | 472 | (重複あり) | 低(見た目のみ) |
| 真に到達不能 | 12 | 20 | 高(ゲーム影響) |
7,964件が12件に収束した。
残りの大半は Imperial(973行)、need(812行)、Talk(567行)のような英語の極めて一般的な単語だ。
NPCが“the Imperial Legion” と言えば
Imperialがリンクになるし、“I need your help” と言えばneedがリンクになる。
しかしこれらは英語版でも意図的なトピック導線ではなく偶然の一致だ。日本語で消えてもゲームに影響はない。
この種の一般語80トピックのストップリストを作成し、7,964件のうち6,211件(78%)を自動ignoreに分類して、レビュー対象を1,753件に削減した。
12件の修正
真に到達不能な12トピックは、2種類の方法で修正した。
DIAL訳語の修正(2件)
トピック名自体を、INFOテキスト中に自然に出現する形に変更した。
my tale(私の伝説 → 私の物語): INFOテキストに「私の物語」が9件出現し、テキストマッチングが回復can be reached(到達可能 → 行き方): INFOテキストに「行き方」が5件出現し、テキストマッチングが回復
これは、いつもどおりの修正。
AddTopic追加(10トピック・15行)
残りはDIAL名を変えても自然にテキスト出現が見込めなかったため、特定のINFOに手動でAddTopicを追加した。例えば:
身分について(Background): ゲーム開始直後、税務官のSellus Graviusの会話に追加ちょっとした提案(little suggestion): Crassius Curioのフラール家ホルテイター推薦場面に追加
などなど。
4/9以降: AddTopicの限界に気づく
クエスト境界フィルタで3件のバグは解消し、7,964件の仕分けで本質的に問題なのは12件だけだと判明した。しかし、構造的な問題は残っていた。
- フィルタの判定が複雑になりすぎる
- 「クエスト情報なし」のINFOの扱いが本質的に曖昧
- AddTopic自体の「無条件追加」という性質は何も変わっていない
先に実装した842件の「安全な注入」も、本当に安全かどうかは個別確認が必要で、そのためには何百時間もかかる。
テスト1: AddTopicの全面カット
まずAddTopicの影響範囲を確認するために、注入していたAddTopicを一括削除してビルドしてみた。極端なケースを先に試して、どのトピックが本当にAddTopicに依存しているかを特定するためだ。
結果は予想通り、テキストマッチングだけでは到達できないトピックが複数あり、ゲーム進行に影響が出た。ただしこのテストで、AddTopicが本当に必要なのはごく一部で、大半はテキストマッチングか他の経路で到達可能だという感触が得られた。
全面カットは当然そのまま採用できないが、「全部必要」ではないことが確認できた。
テスト2: ティア分けによる段階管理
AddTopicの信頼度を4段階に分類して管理する仕組みを、さらに精密にする方向も検討した。
理屈は正しかったのだが、結局6,551件の低信頼候補をどう扱うかという問題は残ったままになった。
- 上位3ティアの計110件は条件が厳格で安心して自動注入できる。
- しかし残りの6,551件は言及はあるが安全かどうかわからない。
AddTopicというツール自体にスコープの概念がない以上、どれだけ分類を精密にしても安全にはならない。
つまり、別のアプローチが必要だった。
転機: マーカーという仕組みに気づく
@テキスト# 構文の発見
AddTopicに頼らず、別の方法でテキストマッチングを復元できないかを探ることにした。 従来のようにスクリプトに頼ると危険なので、ゲーム内仕様ではなくエンジン側の改修でなにかできないかを模索する方針へ転換した。
とりあえず、OpenMWのソースコードを読み、会話トピックの判定がどのように処理されているのかを確認していくことにした。
まず keywordsearch.cpp と dialogue.cpp を追いかけた。テキストマッチングの仕組み自体はシンプルだった。Trieツリーに登録されたDIALトピック名を応答テキスト中から検索し、一致部分をハイパーリンクにする。
そしてソースを読み進めていく中で、@テキスト# というマーカー構文の存在に気づいた。テキスト内に @coded message# と書けば、KeywordSearchのTrieツリーとは独立に、明示的なハイパーリンクを生成できる。
これはまさに欲しかったものだ!!
テキストの翻訳を変えなくても、任意の箇所にリンクを埋め込める。AddTopicのようなグローバルな副作用もないし、 INFOレコードのテキストに書くので、スコープは自動的にそのNPC・その会話で完結されられる。
しかし試してみると、いくつか難点があった。
その1: 表示テキストとトピック名が一致しないと使えない
@暗号化されたメッセージ# と書かないとリンクにならないらしかった。例えば、文章中では「伝言」としか書かれていない場合、@伝言# と書いてもトピック暗号化されたメッセージにはマッチしない。
日本語翻訳では、テキスト中のフレーズとトピック名が一致しないからこそ問題が起きている。同じ制約がマーカーにもある。これでは解決にならない。
その2: グローバル辞書(.top)は同音異義語で破綻する
OpenMWには .top ファイルという仕組みがある。「伝言→暗号化されたメッセージ」のようなマッピングを登録しておけば、@伝言# で正しいトピックに飛べる。
しかしこれはグローバル辞書だ。「依頼」という語が、あるクエストでは「仕事」を指し、別のクエストでは「命令」を指すケースがMorrowindには多い。グローバルに1対1で決めると必ず破綻する。
その3: 暗黙リンクとの排他 ― hasTranslation() の発見
ソースをさらに読み込んでいて、決定的な発見があった。
OpenMWの dialogue.cpp(表示レイヤー)には、翻訳MODが有効かどうかで処理を分岐するコードがあった。
// dialogue.cpp (Response::write) — 2025.12.24本家
if (hyperLinks.size()
&& windowManager->getTranslationDataStorage().hasTranslation())
{
// 明示リンク(@...#)だけを表示。highlightKeywords は呼ばない
}
else
{
// 通常パス: highlightKeywords でテキスト中のキーワードを自動リンク化
keywordSearch.highlightKeywords(text.begin(), text.end(), matches);
}同様の分岐が journalviewmodel.cpp(ジャーナル表示)にもあった。
仕様をまとめると、
hasTranslation()がtrueのとき、KeywordSearchによる自動リンク(暗黙リンク)は表示レイヤーでは一切行われない。.cel(地名翻訳)、.top(フレーズ形変換)、mPhraseForms(語形辞書)のいずれかが読み込まれていれば、翻訳MOD有効と判定される。
日本語MODでは当然 true になる。
つまり、OpenMWの開発者は「翻訳時はキーワードマッチが壊れる」とすでに認識していたのだ。
対応としてロシア語翻訳向けに、翻訳環境では明示リンク(@#)だけを信頼するようDisplay側を設計していた。ソースコメントにも “In translations (at least Russian) the links are marked with @#” と書かれている。
しかし、対応は表示レイヤーだけで、トピック発見ロジックは手つかずだった。
// dialoguemanagerimp.cpp — ゲームロジック (トピック発見)
void DialogueManager::addTopicsFromText(const std::string& text)
{
// HyperTextParser::parseHyperText は明示リンクも暗黙キーワードも
// 両方トークン化する。hasTranslation() のチェックはない。
for (const auto& topicId : parseTopicIdsFromText(text))
{
if (mActorKnownTopics.count(topicId))
mKnownTopics.insert(topicId);
}
}
表示では暗黙リンクを抑制しているのに、トピック発見ロジック (addTopicsFromText) ではKeywordSearchが翻訳環境でもそのまま動いている。
これは2つのことを意味した。
- 表示と発見の不一致
- 画面にはリンクが見えないのに、裏ではキーワードマッチでトピックが発見される(または発見されない)
- プレイヤーからは何が起きているか見えない
@#マーカーの設計意図- OpenMW開発者は、翻訳環境でのリンクは
@#マーカーで明示的に管理するべきだと想定していた - 表示レイヤーがそうなっている以上、ゲームロジック側も本来は同じ方針であるべき
- OpenMW開発者は、翻訳環境でのリンクは
この発見で、マーカーが単なる代替手段ではなく、OpenMWの翻訳アーキテクチャが最初から意図していた正統な仕組みだとわかった。
表示側はすでにマーカー前提で動いている。 ならば、ゲームロジック側をそれに合わせれば、全体として一貫した設計になるはず・・・!
問題は、既存のマーカー構文では表示テキストとトピック名が一致する必要があり、日本語翻訳では使い物にならないこと(難点その1, その2)であった。
そこで @表示テキスト=トピック名# の拡張構文が必要になった。
@表示テキスト=トピック名# 構文の開発
OpenMWソースの改造
3つの問題を一気に解決するために、OpenMWのソースコードを直接改造することにした。
また、このタイミングで先にOpenMWの最新版(v51)を取り込むことにした。本家にもトピックまわりで改修が入っていたことに気づいたからである。
1. @表示=トピック# 拡張構文の追加
@伝言=暗号化されたメッセージ#
= の左が画面に表示されるテキスト、右が実際のトピック名。これで、
- テキストの翻訳を変えなくてよい(伝言のまま)
- 正しいトピックに紐づく(暗号化されたメッセージ)
- INFOレコードごとに個別指定できる(グローバル辞書の問題なし)
同じ「依頼」という単語でも、Aの会話では @依頼=仕事#、Bの会話では @依頼=命令# と書ける。
2. 暗黙リンクとの共存(hasTranslation() 排他の解消)
- 2025.12.24版では
hasTranslation()=trueのとき表示レイヤーが暗黙リンクを完全にスキップしていた - 2026.03.28版のリファクタリングで
KeywordSearch::parseHyperTextに統合された際にこの排他は解消され、明示リンクと暗黙リンクが自然に共存するようになった。
@伝言=暗号化されたメッセージ# でマーカーを入れても、同じテキスト内の他のキーワードマッチは従来通り機能するように実装した。
3. dialoguemanagerimp.cpp の統合
addTopicsFromText が @# マーカーも KeywordSearch も両方処理するように実装した。
なので、マーカーを入れれば AddTopic スクリプトは不要になる。
改造範囲を最小にする工夫
当初は dialogue.cpp、journalviewmodel.cpp、dialoguemanagerimp.cpp、keywordsearch.cpp の4ファイルすべてを大きく変える方針だった。
しかしソースを比較していくうちに、keywordsearch.cpp だけで核心部分が成立することに気づいた。
2026年3月版のOpenMW本家は、すでに parseHyperText() と getDisplayName() を使う構造になっていて、keywordsearch.cpp に = の右辺を取る処理を足すだけで、下流の dialogue.cpp や journalviewmodel.cpp は自動的に機能するようになっていた。
これは「安全圏から着手する」という方針にも合致していた。
マーカーがAddTopicより本質的に安全な理由
ここが最も重要なポイントだ。
AddTopicは「このトピックを知れ」という命令。
NPCが応答を持つかどうかに関係なく、無条件にトピックが追加される。だからクエスト境界を簡単に破壊する。
マーカーは「このテキストはリンクである」という宣言。
ゲームエンジンは、リンク先のトピックについてそのNPCが応答を持っている場合にのみ、トピックを追加する。応答がなければ何も起きない。
これは英語版でのテキストマッチングとまったく同じ動作だ。マーカーは「翻訳で壊れたテキストマッチングを、別の手段で復元する」もの。AddTopicのような「英語版にない挙動を新たに追加する」ものではない。
726行の手動マーカー化
翻訳用の開発ツールを強化
実は、OpenMW日本語版の開発初期段階で、日本語化用のツール(BG3のMOD翻訳ツールを改修したもの)を作っていた。開発用なので私にしか使い方が分からないグチャグチャのものだが、これを大幅に改修して、今後の保守がしやすいように設計変更を進めていた。
マーカー構文がエンジン側で動くことが確認できたので、このマーカー処理を簡易的にできるように、日本語化ツールも同時に改修した。
マーカーにしたいところをドラッグすれば、マーカー構文化できるようにしたり、機械判定でマーカー対応されてない部分を検知して編集候補に出したり…などの機能を実装した。

ひと通り土台ができたら、まずは「このNPCは対象トピックへの応答を持っている(speaker一致あり)」のINFO行を抽出できるようにして、精査したところ、約750行が候補として出てきた。
各行について、
- テキスト中にトピック名の部分一致があるか確認
- あれば、その部分を
@表示テキスト=トピック名#に変換 - 変換結果が文脈として自然か目視確認
この作業を726行分完了した。
AddTopic依存の大幅縮小
マーカー化の結果、従来のAddTopic(約5,000件超)の大部分が不要になった。
- 英語版ESMにもともとあるAddTopicはそのまま残す
- 日本語版が独自に注入していた5,000件超のAddTopicは原則廃止
- ジャーナルINFOへの無意味な注入(738件)も削除
現在は英語版ESMにもともとあるAddTopicのみが残っている。クエスト境界を越えるような危険な注入は全てブロック済み。
.mrk と .top の自動生成
マーカー情報をゲームに渡す .mrk ファイルと、旧形式との互換用の .top ファイルを、ESPビルド時に自動的に生成するよう統合した。手で管理する必要がなくなり、ESPを再ビルドすれば常に最新のマーカーデータが揃う。
.topは日本語化環境では使用しないが、将来用として一緒にビルドされるようにした。
CJK字幕折り返しの一時消失
マーカー実装と並行して、もう一つトラブルがあった。ビルド環境の更新中に、日本語の字幕が折り返されなくなった。
OpenMWが使っているUIライブラリ(MyGUI)は、元々スペース区切りでしか改行しない。日本語のようにスペースなしで文が続く言語では、文末まで突き抜けてしまう。
12月のJPフォークではCJK文字の境界で折り返すパッチを当てていたのだが、2026年3月版の本家ベースに乗り換えた際にパッチ適用済みバイナリが未適用版に戻ってしまった。
原因特定後、ビルドシステムに apply_mygui_patch.cmake というパッチ自動適用の仕組みを組み込み、ソースからMyGUIを毎回ビルドする構成に変更した。これで今後同じ問題は起きない。
AddTopic問題に終止符
従来5,000件超あった独自AddTopicは全廃し、726行のマーカーに置き換えた。マーカーはテキストマッチングと同じスコープで動作するため、英語版と同等のクエスト境界が回復した。
もちろん、抽出フィルタにミスがあってマーカーの入れ忘れが残っている可能性はある。しかし、仮に漏れがあっても影響はその1行に閉じる。AddTopic時代の「1件の注入ミスがゲーム全体のクエスト境界を崩壊させる」リスクとは質が違う。
万が一残っていたら、報告頂きたい。
アーカイブ
コメント