Skip to content
This repository was archived by the owner on Jul 31, 2020. It is now read-only.

エディター部メモ

SAT edited this page May 6, 2020 · 3 revisions

UIの仕様

  • フォントはすべてMeiryoUIにする。
  • 並び替えできないListViewはNonclickableにする。
  • ラベルは左揃え、末尾に半角の:を付ける。
  • ラベルに対応するコントロールは左揃え、すべてのラベルよりも右側になるようにする。

リソースの仕様

  • Squirrel標準命令の書式
    [コード];[末尾追加文字列];[t];[group];[summary];[comment]
    • [t] は末尾にセミコロンを追加する場合に必要だが、この場合は必ず [末尾追加文字列] が必要。
    • [group] はサブノードの分類で、自由に作れる。
    • [summary] はノードの表示名。
    • [comment] はツールチップになるが、句点の後に自動で改行記号が付加される。

コマンドツリーの仕様

  • リソースからデータを読み込み、ツリーを生成する。
    ツリー生成後に、テキストエディターのキーワード色分け設定とオートコンプリートのソース設定が行われる。
    テキストエディターの色分けはSquirrel標準命令とC++の組み込みオブジェクトに限られる。
  • インスタンスは一つだけ。
    各ノードのTagには詳細なオブジェクト情報が格納されており、これをコピーする処理コストを回避するためである。
    ホームポジションはテキストタブとして、イベント編集画面が開くときにはこれを移動させている。
    使い終わったら必ずホームポジションに戻す必要がある。
  • 「Easyエディター」にも対応している。
    これはコマンドツリーで挿入処理を行わずにイベントを経由して各種エディターで挿入処理を行うことで実現している。
    Easyエディター専用のノードもあるが、これはコマンドツリー内のフラグによって有効/無効が管理される。
    このフラグはイベント編集画面が開く/閉じるときに切り替えられる。
  • コマンドツリーの挿入イベントについて
    一つのインスタンスに複数のイベントハンドラーが関連付けられた状態になっている。
    各イベントハンドラーでは、現在のコマンドツリーを保有しているウィンドウのクラス型を比較することによって、該当するエディターだけが処理を行えるようにしている。
  • 挿入ダイアログ
    関数は引数の入力があるため挿入用のダイアログを経由する。
    引数の中でさらに関数を使うときは挿入ダイアログがネストする形になる。
    ダイアログにコマンドツリーのインスタンスを渡すことで、各引数についてもコマンドツリーをコンテキストメニューの形式で再現させている。
    各引数のコントロールはすべて同一のコンテキストメニューを参照し、メニューを開くときに挿入先のコントロールである自身のインスタンスをコンテキストメニューのTagに格納している。
    既存コードから挿入ダイアログへ復元することはできない。
    引数の中に関数がネストしていたり、文字列の中に , が含まれていたりする可能性がある等で処理できないため。
  • コード入力補完窓
    コマンドツリーが保持する全階層のノードをフラットにしたオートコンプリートのソース(static)を用いる。
    入力された文字列と一致する識別子を検索して、そのオブジェクトの詳細情報を表示する。
    サジェスト候補には別途すべてのノードの識別子を格納したソースリスト(static)を用いる。
    全ソースリストは一度生成すると参照の差し替えが行われるようになり、検索の度にリストを再生成することはない。
    現在選択されている単語を基にして検索し、インスタンスである場合はそのクラス型を調べて補完候補を絞り込む。
    このとき、自動で . を挿入する。
    絞り込む場合は全ソースリストを使わずに、絞り込んだリストへの参照に切り替えてサジェスト候補を出す。
  • コード逆探索
    テキストエディターのみ有効。ただし、テキストマージツールでは使用しても効果がない。
    現在選択している部分の単語を切り出し、入力補完で使われるオートコンプリートソースから検索する。
    数値を選択していた場合は、何らかのIDとみなしてデータベースからFixedIDを検索する。16進数にも対応。
    このため、テキストエディターにはデータベースエディターのインスタンスを保持させる。
    検索結果をコマンドツリーで選択する処理はイベントを経由し、コマンドツリーを持つコントロールが操作する。
    関数である場合はダイアログを出して再編集することができる。
    ただし、このとき既に入力してある引数の情報は取り込まれず、新たに引数を挿入する。
  • 関数呼び出し処理の再編集について
    テキストエディターではコード逆探索から、Easyエディターではコマンド置き換えから再編集が行える。
  • 色分けについて、コンテナー系とメンバー系で衝突しているキーワードがある。
    以下は一般的に使われる単語のためどちらの分類にも登録されている。
    AutoTiles Boolean Direction Graphics IDValues Material Money Position SelfValues String UDB

ファイルツリーの仕様

  • 新規作成
    別ダイアログで名前を決めて、即座にファイル生成を行う。
    ファイル生成を行った後でリロードを行い、自動でそのノードを選択する。
  • リネーム
    ノードのラベル編集機能を利用している。
    確定前に検証を行い、リネームを実行する。
  • 削除
    ファイルを削除し、ノードを削除する。
    このとき、リロードは行わない。
  • コピー/貼り付け
    テキストエディター:ファイル・フォルダー両方に対応する。
    マップエディター:ファイルのみ対応する。これはマップのGUID更新とスクリプトの生成が関わっているためである。
  • リロード
    リロード後に選択するノードまで一つ一つ辿っていくため、その過程で選択されたノードの選択イベントがすべて発生してしまう。
    通常はフォルダーしか通らないので、経由している中でファイルを開くなどの処理は起こらない。

マップエディターの仕様

  • カーボン機能について
    親マップを指定することにより、子マップで (0, 0) 空白タイルの部分が同一タイル座標の親マップのタイルで補完される機能。
    「生きていた町が滅亡した」など、既存のマップを拡張的に編集したいときに使われることを想定している。
    • サイズが親と子で異なっていても問題なく動作する。
    • 影については、同一座標に対して親か子のどちらか片方で付けられている場合のみ表示される。
       すなわち、親マップで付けられた影を、子マップで再配置することで消すことができる。
    • イベントは関連付けられないため、マップをコピーすることとは異なる。
  • ランダム生成機能について
    • 小部屋と通路から成る。
    • 通路は常に幅1、小部屋は必ず四角形となる。
    • あくまでも原型の生成補助という位置付けのため、機械的な生成に留める。
    • 生成後にユーザーによって手直しが加わることを前提とする。
    • 正方形に近いマップだと縦長or横長の小部屋が一つ目にできてしまう。
  • カーソルを高速に移動させてタイルを配置しようとすると、間引かれてしまう。
    MouseMoveイベント自体が一定の間隔でもって間引く仕様であるため。
    移動したら1ピクセル単位で必ずイベントが発生する、というわけではないらしい。

マップイベントの仕様

  • タイル単位の座標は「タイル座標」、スクリーン上の座標は「ピクセル座標」として区別する。
  • マップに対してグローバル一意識別子(GUID)を与え、この識別子をイベントスクリプトのファイル名にする。
    イベントスクリプトはTexts\Scripts\MapEvents直下にすべてフラットに配置する。
    こうすることにより、マップがリネームされても、移動されても対応できる。
    削除されたときだけ、それに合わせて消せばよいが、最悪削除に失敗しても問題がない。
  • イベントスクリプトの仕様
    イベントが配置される順序は、イベントリストのコレクションの順序に合わせる。
    さらに、イベントスクリプトの関数は、必ずイベントが配置される順に定義する。
    これはエディターがコレクションの順にスクリプトが定義されているものと想定しており、イベントIDをチェックせずに順次読み込んでいくため。
  • イベント編集ダイアログの仕様 ESCキーで閉じるようにしているが、変更が無条件に破棄されるのは危険なので、確認を取るようにしている。
    ただし、別のタブに移動する等して編集が確定された部分については変更されたかどうかの確認ができない。
    よって、[あるタブで編集する->別のタブに移動する->ESCキー押す] の操作では最新のタブで変更されていないため、確認なしでダイアログが閉じる。

マップイベント編集「Easyエディター」の仕様

  • 各行には、生コード・インデント深さ・表示用テキストを格納する。
  • 原則としてコマンドツリーに登録されている単語を逐語訳する。
    ブロックの始端と終端は記号に対して反応するため、制御文の末尾に始端記号を書いても、改行してから始端記号を書いても問題はない。
    生コードに対してシステムが変更を加えることはなく、あくまでもユーザーによってのみ編集される。
  • 選択行がない場合は先頭行にカーソルがあるものとする。
  • 末尾の空行は、末尾に挿入するための便宜的な行であるため、削除も変更もできない。
  • 関数を挿入する場合は引数を入力する欄を含めた挿入ダイアログを経由する。
    挿入ダイアログの引数部分は、従来通りのコーディングスタイルになる。
  • 既に入力されている行を編集する場合は、従来通りのコーディングスタイルになる。
  • 切り取り/コピー/貼り付けは、対象行の生コード扱うことで実現している。
    static変数で管理しているため、プロセス内では常にデータが保持される。

データベースの仕様

GUIタイプのデータベース

付属ファイルを持てる機構を用意している。
原則として、すべての情報をマネージャークラスが保持し、DB保存時に一括でファイル出力する。
ただし、UserDBに限っては、DataGridViewを複数生成するのはリソース浪費につながるおそれがあるため、選択項目が変更されるたびにファイル出力を行うことにした。
この場合、ユーザーに "この違い" を感じさせることは使いづらさに繋がってしまう可能性も考えられるので、付属ファイルの保存はしていてもUserDB全体としては保存すべきであるとして、### 印をあえて残すようにしている。

システムDBの読み書き

  • 状況把握
    PropertyGridで編集するため、他のDBのような読み書きが困難。

  • 対策
    このDBは例外的にXMLのシリアライザーを利用して読み書きを行う。

  • 注意
    シリアライザーはpublicなメンバーのみ対象とする。
    シリアライザーを利用するにあたって、シリアライズできない構造体や列挙体のプロパティは、低レベルのプロパティを経由する必要がある。
    シリアライザーでの読み書き中にエラーが起きた場合、InnerExceptionを参照するとその内容がわかる。

ID変更問題への対処

  • 状況把握
    従来のIDはデータに対して一つしか付けられず、可変のものであった。
    IDがデータを識別するために必要なものであるため、変更されるとスクリプトなどでエラーが起きてしまう。
    並び替えの手段としてIDは必要だが、スクリプトを書く立場からすれば変更されて欲しくない。

  • 対策
    不可視のFixedID、可視の可変IDを用意する。
    可変IDは並び替えの用途に限定し、データの識別には固定のID列を使う。
    ただし、FixedIDは抜け番や順番の前後が発生しうるので連番IDを前提とした繰り返し処理はできない。
    データベースでも別のデータベース項目を参照するものについては、編集時には「可変ID:名前」として表示し、実際に保存する値は「FixedID」とする。

  • 注意
    FixedIDは 0~2147483647 までのランダム値が割り当てられる。
    リスト上では一意だが、過去も未来も含めて一意であるわけではないため、消した項目を指していたはずのスクリプトコードが、新たに追加された全く無関係な項目を指すようになったためにエラーにならない、という予期せぬ問題が稀に起きうる。

データベース初期化の流れ

  1. MainWindow上に配置されたDBエディターコントロールを初期化
    1-1. DBタブを生成し、対応するDataGridViewを動的に追加
    タブ内には必ず唯一のユーザーコントロールを追加し、これは必ずctlDBBaseクラスを継承する。
    フォーム上にDataGridViewを表示しないDBも、内部DBとして生成する。
    DataGridViewのTagにはそのデータベースの名前を格納する。
    1-2. 生成したDataGridViewの列を設定
    1-3. DataGridViewの統一設定を適用
    1-4. FixedID列を左端に追加:非表示のDataGridViewにも適用
  2. プロジェクトオープン
  3. データベース単体読み込み
    依存項目は仮読み込み状態にする
  4. 依存項目を正式にセット

列情報の管理について

  • 概要
    デザイナー上で設定すると仕様変更に耐えられないため、すべてコード化する。
    すべてDataGridViewを用いて統一的に管理する。

  • 実装
    DataGridViewの各列のTagにDBColumnの派生クラスをセットする。
    このクラスがデータベース列に関するすべての情報を保持する。

  • 注意
    列挙体とswitch文の組み合わせが多用された経緯から、ポリモーフィズムへの移行を行った。
    大原則としてアップキャスト(親クラスに変換)しても、子クラスにある情報が失われることはないため、同一のシグネチャーを利用して異なる処理を実装(オーバーライド)することができる。
    従来のswitch文での分岐は、オーバーライドによるメソッド呼び出しか、対象の派生クラスに変換(ダウンキャスト)できるかどうかをチェックすることで実装する。
    すべての列のTagに列情報が格納されるため、消費メモリがここで大きく増加する。
    列情報を変更または増減した場合、それまでに出力されたデータベースのファイルは読み込めなくなる。

ユーザーデータベースの列情報の読み書きについて

  • 概要
    ポリモーフィズムを利用しているため、単純に共通のフィールドの値を書き出す、ということにはならない。
    各列のクラスがシリアライズ/デシリアライズを実装し、型名に応じてインスタンス化を行って復元する。

  • 実装
    Type.GetTypeメソッドから始まるメタプログラミングを活用する。
    指定した型名を持つインスタンスを生成することができるため、型名を読み込んで対応するクラスをインスタンス化する。

  • 注意
    シリアライズ/デシリアライズの部分はフィールド値の順序を必ず一致させなければならない。
    特に復元時は添え字をダイレクト指定しているため、不具合が起きやすい状態になっている。

特定のセルに代入を行うタイプのボタン処理について

代入先はインデックスで指定する。
代入先が所定のデータを受け入れられるかどうかをボタン処理の中でチェックを行う。
そうすることで、代入先が存在しないなどのエラーを防ぐ。

仮読み込みの仕組みについて

  • 概要
    他のデータベースに依存するような項目について、読み込み順序を考慮すると非常に複雑になるため、単独で読み込む際には「仮読み込み」を行う。

  • 実装
    仮読み込みを行うときは、FixedIDをセルのTagに格納する。
    すべてのデータベースの単独読み込みが完了してから、仮読み込みを行っている列に対して正式なセットを行う。
    FixedIDから [可視ID:名前] の探索を行い、リスト型であればその項目を選択した状態にする。

  • 注意
    単独編集を行った後に別のデータベースを編集する場合、依存先が変更されている可能性があるため、このタイミングにも再度セットを行う必要がある。SrcResetメソッドにて実装する。

各種エディターの仕様

アクセス制御の仕様

  • 概要
    ロード時はCreate/Readモードで編集権獲得、失敗したらReadモードで読み取り専用へ。
    セーブ時は一時的にCreateモードに変更して処理し、すぐに編集権を獲得し直す。

  • 実装
    アクセス制御するファイルを読み書きするときは以下の関数を通して処理を行う。

    • ファイル名が変更されるプロパティ内で
      設定時 Module.Common.StartFileAccessLock / null時 Module.Common.EndFileAccessLock
      さらにStart時は戻り値でReadOnlyを設定
    • ロード処理内で Module.Common.FileReadAll
    • セーブ処理内で Module.Common.FileWriteAll
  • 注意
    決して独自にStreamReaderなどで読み書きしてはならない。(どうしても必要な場合はFileStreamオブジェクトを使うこと)

編集履歴

  • 概要
    ProjectManager.CEditLogクラス内に自分の編集履歴を記録する。
    操作にはIDと名前を割り当てて抽象化しており、登録されていない操作は記録できない。
    他ユーザーの編集履歴は検知したときにその都度読み込み、リストに表示するだけの処理を行う。

  • 実装
    各エディター内の処理中、ファイルに変更を与えるタイミングで、ProjectManager.CEditLog.AddEditLogイベントを発生させ、それをMainWnd側が捕捉してProjectインスタンス内にある編集履歴を記録する処理へ誘導する。
    なお、ProjectManager内部で編集履歴に追加する場合はイベントを経由せず直接操作する。

  • 注意
    操作はIDと名前を対応して予め定義しているため、操作名を文字列で指定するような条件を作ってはならない。

データ読み書きのロジック

読み込み、書き込みの対象となるオブジェクト(ListViewItemなど)に対するメソッドを作り、列挙体で対応する添え字を定義し、読み込み時は string.Split[...] で列挙体指定。
書き込み時は for(int i = 0; i < Module.Common.GetEnumCount<列挙体名>(); i++) -> switch で書き込み。

妥協仕様

.NET Framework

  • List.Sortは不安定なソートであるため、複数連続でソートを実行すると内容が同一であっても順番が変更される可能性がある。

共通機能

  • アプリケーション環境設定はプロパティグリッドで変更する都合で二度手間が生じているが、単純コードであるため保守容易性が低いわけではない。
  • ツクール素材の変換について
    XPのオートタイル素材は既に古く、マイナーであるため、対応しない。
    歩行グラフィックの変換について、補間方法を最近傍以外にすると、境界線が生じて透過できなくなるため、ユーザーによる指定を禁止している。

データベース

  • データベースの列順序について、各々バージョンを用意するのは非常に手間がかかるため、バージョンの相違はチェックしないことにした。
    もしリリース後に列順序を変更するようなことがあった場合、その都度移行するためのツールを用意することにする。
    ただし、重大な問題が発生しない限りはむやみに順序変更しないこと。
  • DBGUI派生クラスについて、GUIの操作に対するイベントハンドラーは基本クラス側で定義できなかった。
    管理オブジェクトであるmgrは各々型が異なり、その型もジェネリック型であるため、これ以上複雑にできない。
    newを使ってmgrを宣言すると、同名のオブジェクトが複数共存することになり、意味がない。
    元々GUIのイベントハンドラー部分は少ないコード量で済む。
  • タイルセットDBについて、オートタイルの数は127セットが限界数
    左上の空白タイルが必要だったため、このような中途半端な数になっている。
    ツクールのオートタイルは128セットなので、1つだけ登録できないものが出てくる。