セーブとロードのおはなし

ボブ「HA……HA……HA……」
アリス「あら、ボブが力尽きてるわ。どうしたのかしら?」
ボブ「やぁ、アリス。ちょっと華和梨におけるエントリのセーブとロードについて考えていたら力尽きてしまったのさ。ガクッ」
アリス「あ、ボブが死んだ」

というわけで、たまにはセーブとロードについてちゃんと考えてみよう、という話。
この話は自分でミドルウェアを書こうとするような変態という名の変態にだけ関係する話なので、OpenKEEPSなどを有難く使っている素直な良い子のみんなは気にしないでいいです。気にすると死にます。

注意:以下、ゴースト起動時や他のゴーストからの交代時、ゴースト呼び出され時などに送られてくるOnBootOnFirstBootOnGhostChangedOnGhostCalled(SSP独自イベント)、OnVanished(SSP独自イベント)などのイベントをまとめて起動系イベントと呼ぶことにします。

目次

エントリを分類してみよう

まず最初にエントリを値の使われ方によって分類して、どんなタイミングでセーブやロードをすればいいか考えましょう。

エントリの内容が突発的な値の変化ゴースト起動時の初期化分類どんなエントリ?
変化しない--タイプA値が固定されているエントリ
変化する困らない-タイプBカウンタなどの作業用変数
困るしないタイプCユーザー名や好感度など
するタイプDゴースト起動中のみ有効なフラグなど

タイプA

OpenKEEPSにおけるトーク記述エントリの「sentence」などのようなエントリです。
おおよその場合、ほとんどのエントリがタイプAに相当するはずです。
値が変化しないのですからセーブもロードも不要です。

タイプB

自発トークのタイミング用のカウンタなどの作業用エントリなどが該当します。
自発トークのカウンタの値がいきなりクリアされても自発トークが遅くなったり早くなったりするだけなので基本的に問題ない……よね。
そこを気にする場合、それはタイプDのエントリとなります。
どうせ値が初期化されても大して問題がないのならセーブやロードはしないという方向で。

タイプC

ユーザー名とか好感度とか常に忘れちゃいけない値です。
これはもちろんセーブ対象となります。

タイプD

具体的にはゴーストの起動中にセクハラしたらフラグを立てて終了時に文句を言うとか、ゴースト起動時に起動時刻を覚えておいて終了時にそれを参照してトーク変えたいとか、そういうゴースト起動時に一旦初期化しておいて、その後、ゴースト起動時や起動中に値が設定されるようなエントリです。
一見、何も問題はなさそうですし、セーブ対象とする必要もなさそうですが、ところがどっこい、こいつが恐るべし曲者なのです。
なにがどう曲者なのかはこの後で。

ゴーストの起動時の処理とエントリの状態

次は、セーブやロードの話において押さえておく必要があるゴースト起動時の動作の話です。
通常、ゴーストの起動時には栞のロードが実施されますが、SSPでは設定によってはゴーストキャッシュからの起動という栞のロードを伴わないゴーストの起動があります。
このそれぞれの起動時の動作やエントリの状態については以下の通り。

  1. 通常のゴースト起動(栞ロード)時
    • エントリ状態は辞書ファイルに記述された通りに初期化される
    • スクリプト記述ゾーンに記述されたKISが実行される
  2. ゴーストキャッシュからの起動時
    • エントリ状態は前回終了時の値が復元される
    • スクリプト記述ゾーンに記述されたKISは実行されない

descript.txtに「shiori.cache,0」と書けばゴーストキャッシュを無効にできますが、せっかくぽなさんが作った機能なのでコレも含めて何とかしてみましょう。詳しい話は後ほど。

おまけ:栞ロード時の動作

ちなみに栞ロード時の処理の順番は以下の通り。

  1. kawarirc.kisを読む
  2. ファイルの先頭から読み進めて行きエントリ定義行があったらエントリ定義行の内容を今のエントリの状態に追加、KISがあったらKISを実行。
  3. KISのうちload命令があったら、その時点でload対象ファイルを読んでその中身を読み進め、それが終わったら元のファイルに戻って読み進めます。
    つまり、load命令以降の内容はloadで指定されたファイルの中身を読んでから読まれるということ。

特殊な起動・終了やそれに準ずる動作について

ここでクイズ。以下のうち、間違っているものはどれでしょう。

  1. 栞のロードはゴースト起動時のみ実施され、栞のアンロードはゴースト終了時のみ実施される。
  2. ゴースト終了時はかならずOnClosedやOnGhostChangingなどの終了系のイベントが来る。
  3. OnBoot、OnGhostChangedなどのゴースト起動系イベントはゴースト起動時のみ1つだけ来る。

答え:全部間違い。
ということで、セーブやロードの処理を自分でなんとかしようというときは、こういう特殊な契機も考えておかないといけないのです。頭が痛い。

その1:ゴースト起動中の栞のロードやアンロード

栞のロード(とアンロード)といえばエントリが初期化されスクリプト記述ゾーンのKISがすべて実行されるというデカい処理なのに、ゴースト起動中にそんな困った処理が起きてしまうのです。
その契機は以下の通り。

  1. ネットワーク更新成功時
  2. shiori.dllを含むsupplimentがインストールされたとき
  3. SSPの拡張タグの\![reload,shiori]、\![reload,ghost]が実行されたとき
  4. SSPの開発用パレットで栞やゴーストのリロードを実施したとき

他3つはそうないとはいえ、ネットワーク更新時だなんて……('A`)

その2:終了イベントを伴わないゴースト終了

これはまぁ、言われてみれば「あぁ、そういえば!」というレベルの話。

  1. トーク中で\![change,ghost,~]タグを実行したとき
  2. (OnClosedのイベントのトーク以外での)トーク中で\-でゴースト終了したとき

その3:ゴーストの起動中にゴースト起動イベントが来てしまう

実は、ゴーストの起動時イベントが複数回来る場合があります。
有名なのはOnGhostChangedでトークを返さない(SHIORI/3.0 204 No Contentを返した場合)にOnBootが来るという場合。
これはまぁ、ゴースト起動時に立て続けにくるので良いとして(よくないけど)、本当に困るのがSSP固有の動作として、同時起動しているゴーストが削除された時に送られて来るOnOtherGhostVanishedにトークを返さない場合(SHIORI/3.0 204 No Contentを返した場合)に起動時イベントであるOnVanishedが来るということがあります。

ここまでふまえて。

セーブやロードの方法を考えよう

ようやく本題。
ここからはエントリのタイプ別にセーブとロードの方法を考えていきましょう。

タイプAとタイプBのエントリ

最初に書いたとおり、これらのタイプのエントリはセーブやロードをする必要はありません。以上。

タイプCのエントリのセーブとロード

タイプCのエントリはゴーストの最初の起動時に初期化して、その後、値の変化を保持し続けるエントリです。
というわけで、最低限、栞アンロード時(とゴーストキャッシュ入り)のときのセーブと栞ロード時のロードを実施すれば良いということになります。

まずはゴーストキャッシュのことなど考えず

ひとまずゴーストキャッシュのことは無視して栞アンロード時のエントリのセーブと栞ロード時のエントリのロードについて考えます。
栞ロード時はkawarirc.kisのスクリプト記述ゾーンの最後あたりでロードすればいいとして、栞アンロード時はどうしましょう。
結論からいってしまえば、あまり有名ではありませんが、華和梨では栞のアンロード時に「System.Callback.OnUnload」というコールバックエントリを評価しています。ソースは華和梨公式サイトのドキュメントの「ユーザーズマニュアル」の「8.1. コールバック」。
というわけで、とりあえず、kawarirc.kisの末尾でロード、System.Callback.OnUnloadのコールバックエントリでセーブするという方針でやってみましょう。後のことを考えて、セーブ処理とロード処理はそれぞれ関数化しています(実際にはらくだ屋/華和梨・自作関数集のexsave, exloadなどを使いましょう)。

System.Callback.OnGET,System.Callback.OnNOTIFY: $(entry ev.${System.Request.ID})
ev.OnBoot: \1\s[10]\0\s[0]起動しました。今まで${叩かれた回数}回叩かれました。\e
ev.OnClose: \1\s[10]\0\s[0]さようなら。\-
ev.OnGhostChanging: \1\s[10]\0\s[0]さようなら。\e
ev.OnMouseDoubleClick: $(inc 叩かれた回数)\1\s[10]\0\s[0]これで${叩かれた回数}回目だ。叩くなバカ。\e
System.Callback.OnUnload: $(セーブ)
=kis
function セーブ $(
  save "sav.txt" 叩かれた回数;
);
function ロード $(
  load sav.txt;
);
function 初期化処理 $(
  ロード;
);
初期化処理;
=end

ものすごく単純ですが、とりあえずの動作確認のためのモデルということで。
ゴースト起動してぽこぽこダブルクリックしてからゴーストを終了したり切り替えたり、開発メニューから「\-」やら「\![change,ghost,random]やらを含むスクリプトを送りつけたり、栞リロードしてみたりして叩かれた回数エントリの内容が保持されているのを確認してみてください。

初期値をつけよう

上の内容でゴーストを起動すると、初回起動のときは「起動しました。今まで回叩かれました。」となってしまいます。
それは当然の話で、セーブデータから値を読み込むのが前提なのにそのセーブデータのないのですから叩かれた回数エントリの内容は空っぽなのです。
では、どうすればいいかといえば、セーブデータを読んだ後、値が空っぽだったら初期値を与えてあげればいいだけです(セーブデータ用ファイルの読み込み失敗? そんなことは知らない)。
こんな感じ。

System.Callback.OnGET,System.Callback.OnNOTIFY: $(entry ev.${System.Request.ID})
ev.OnBoot: \1\s[10]\0\s[0]起動しました。今まで${叩かれた回数}回叩かれました。\e
ev.OnClose: \1\s[10]\0\s[0]さようなら。\-
ev.OnGhostChanging: \1\s[10]\0\s[0]さようなら。\e
ev.OnMouseDoubleClick: $(inc 叩かれた回数)\1\s[10]\0\s[0]これで${叩かれた回数}回目だ。叩くなバカ。\e
System.Callback.OnUnload: $(セーブ)
=kis
function セーブ $(
  save "sav.txt" 叩かれた回数;
);
function ロード $(
  load sav.txt;
);
function 初期化処理 $(
  ロード;
  if $[ $(size 叩かれた回数) == 0 ] $(.setstr 叩かれた回数 0);
);
初期化処理;
=end

初期化処理関数の中のif文が新規に追加したところです。
あくまでもモデルなので、実際には初期化の処理はもっとちゃんとした処理を組んであげましょう。
それはさておき、一回、sav.txtを消して起動してみてください。ちゃんと、「起動しました。今まで0回叩かれました。」と言ってくれるはずです。

ゴーストキャッシュ対策をしよう

今度はゴーストキャッシュの対策。
ゴーストキャッシュに入って終了した後でSSPを終了させるともうゴーストキャッシュが使えなくなりますから、ゴーストキャッシュに入る契機でのセーブは必要です。
ゴーストキャッシュから出てきた時は原則的にはロードは不要ですが、その他の処理も実施する可能性を考えて共通関数化しましょう。
じゃあ、ゴーストキャッシュに入ったり、ゴーストキャッシュから出てきたりをどのように検出するかですが、SSPはゴーストキャッシュに入る契機でOnCacheSuspend、ゴーストキャッシュから出てきた契機でOnCacheRestoreというNOTIFYイベントを送信してきますので素直にこれらのイベントの処理として実装すれば問題ありません。
また、栞ロードの場合とゴーストキャッシュから出てきた場合で処理を共通化する際に、気をつけることはらくだ屋/華和梨・自作関数集にも書かれている通りゴーストキャッシュから出てきた時に備えてセーブ対象エントリをクリアすること。
というわけでこんな感じ。

System.Callback.OnGET,System.Callback.OnNOTIFY: $(entry ev.${System.Request.ID})
ev.OnBoot: \1\s[10]\0\s[0]起動しました。今まで${叩かれた回数}回叩かれました。\e
ev.OnClose: \1\s[10]\0\s[0]さようなら。\-
ev.OnGhostChanging: \1\s[10]\0\s[0]さようなら。\e
ev.OnMouseDoubleClick: $(inc 叩かれた回数)\1\s[10]\0\s[0]これで${叩かれた回数}回目だ。叩くなバカ。\e
System.Callback.OnUnload: $(セーブ)
=kis
function セーブ $(
  save "sav.txt" 叩かれた回数;
);
function ロード $(
  clear 叩かれた回数;
  load sav.txt;
);
function 初期化処理 $(
  ロード;
  if $[ $(size 叩かれた回数) == 0 ] $(.setstr 叩かれた回数 0);
);
初期化処理;
=end
ev.OnCacheSuspend: $(セーブ)
ev.OnCacheRestore: $(初期化処理)

ロード関数の中のclearの処理と、OnCacheSuspendとOnCacheRestoreのイベントに対する処理のエントリ(ev.OnCacheSuspend, ev.OnCacheRestore)が新規に追加したところです。
ゴーストキャッシュを有効にしてもちゃんと動くことを確認してみてください。

タイプCのセーブとロードの話はここまで。
タイプDのエントリを使わないということを前提とするなら、ここで話はおしまいです。
実際、タイプDのエントリとは無縁のゴーストも少なくないはず。

タイプDのエントリのセーブとロード

ネットワーク更新時などのゴースト起動中の栞リロード時にエントリの内容を復元する必要があるため、タイプDのエントリも場合によってはセーブとロードをする必要があります。
ここで考えなければならないのは、どうやって起動時のときだけエントリの中身を初期化するかです。
これにはいろいろな方法があるのですが、ここでは、タイプDもタイプC同様にセーブとロードを実施し、起動時イベントの中でタイプDのエントリの値を初期化する、という方法をとります。

起動時イベントの中で初期化しよう

というわけで、まずは最も単純な場合である、起動時イベントのうちOnBootにだけ対応する場合について考えます。
これは、OnBootイベントに対する処理の中でタイプDのエントリの初期化処理を呼んであげるだけです。
モデルはこんな感じ。

System.Callback.OnGET,System.Callback.OnNOTIFY: $(entry ev.${System.Request.ID})
ev.OnBoot: $(初期化処理2)\1\s[10]\0\s[0]起動しました。今まで${叩かれた回数}回叩かれました。\e
ev.OnClose: \1\s[10]\0\s[0]さようなら。\-
ev.OnGhostChanging: \1\s[10]\0\s[0]さようなら。\e
ev.OnMouseDoubleClick: $(inc 叩かれた回数; inc 叩かれた回数.今回;)\1\s[10]\0\s[0]これで${叩かれた回数}回目だ。今回起動時だけで${叩かれた回数.今回}回だ。叩くなバカ。\e
System.Callback.OnUnload: $(セーブ)
=kis
function セーブ $(
  save "sav.txt" 叩かれた回数 叩かれた回数.今回;
);
function ロード $(
  clear 叩かれた回数;
  load sav.txt;
);
function 初期化処理 $(
  ロード;
  if $[ $(size 叩かれた回数) == 0 ] $(.setstr 叩かれた回数 0);
);
function 初期化処理2 $(
  .setstr 叩かれた回数.今回 0;
);
初期化処理;
=end
ev.OnCacheSuspend: $(セーブ)
ev.OnCacheRestore: $(初期化処理)

これでゴースト起動時のみ値を初期化し、ゴースト起動中の栞のリロードでは値を初期化しないことができるはずです。
起動時の栞ロード~起動イベント(OnBootなど)を受信する間にエントリに値が残っていることについては害は無さそうなので気にしないことにします。

でも、これでもちろんめでたしめでたしではないわけで。

起動系イベント受信での初期化処理は1回だけ

次にOnGhostChangedなどのOnBoot以外の起動時イベントにも反応する場合について考えます。
ここで気をつけるのはOnGhostChangedなどの起動時イベントに対して栞が「204 No Content」を返した場合(つまり、OnGhostChangedイベントの応答トークが存在しない場合)、ベースウェアはさらにOnBootなどの別の起動時イベントを送信してくるということ(なお、OnBootイベントに関しては必ず何らかのトークを返さないといけません)。
したがって、タイプDのエントリの初期化処理は、各起動系イベントにおいて、そのイベントに対して返すトークが存在することを確認した上で実施しなければいけません。そうしなければ、エントリの初期化処理が複数回実施されてしまう可能性があります。
その対策を入れたモデルがこちら。

System.Callback.OnGET,System.Callback.OnNOTIFY: $(entry ev.${System.Request.ID})
# OnBoot受信時の処理
ev.OnBoot: $(初期化処理2;entry evTalk.OnBoot;)
# OnBootのイベント応答トーク
evTalk.OnBoot: \1\s[10]\0\s[0]起動しました。今まで${叩かれた回数}回叩かれました。\e
# OnGhostChanged受信時の処理
ev.OnGhostChanged: $(
  if $(size evTalk.OnGhostChanged) $(
    初期化処理2;
    entry evTalk.OnGhostChanged;
  )
)
# OnBootChangedのイベント応答トーク
evTalk.OnGhostChanged: \1\s[10]\0\s[0]切り替えられました。今まで${叩かれた回数}回叩かれました。\e
# 以下、これまでと同じ
ev.OnClose: \1\s[10]\0\s[0]さようなら。\-
ev.OnGhostChanging: \1\s[10]\0\s[0]さようなら。\e
ev.OnMouseDoubleClick: $(inc 叩かれた回数; inc 叩かれた回数.今回;)\1\s[10]\0\s[0]これで${叩かれた回数}回目だ。今回起動時だけで${叩かれた回数.今回}回だ。叩くなバカ。\e
System.Callback.OnUnload: $(セーブ)
=kis
function セーブ $(
  save "sav.txt" 叩かれた回数 叩かれた回数.今回;
);
function ロード $(
  clear 叩かれた回数;
  load sav.txt;
);
function 初期化処理 $(
  ロード;
  if $[ $(size 叩かれた回数) == 0 ] $(.setstr 叩かれた回数 0);
);
function 初期化処理2 $(
  .setstr 叩かれた回数.今回 0;
);
初期化処理;
=end
ev.OnCacheSuspend: $(セーブ)
ev.OnCacheRestore: $(初期化処理)

OnGhostChangedのイベントの受信時の処理の中でイベント応答トークのエントリであるevTalk.OnGhostChangedエントリの単語数を取得し、それが0でなければ(すなわち、一つでも単語があれば)、タイプDのエントリの初期化処理(初期化処理2)を実施し、evTalk.OnGhostChangedを返しています。このとき、本体から更に別の起動系イベントが送られてくることはない(と信頼する)のでタイプDのエントリの初期化処理が重複して実行されることはありません。
なお、OnBootイベントに対して何もトークを返さないというはしたないまねはやめましょう。ゴーストは起動時に少なくとも\0と\1のサーフェスの初期化を実施しなければいけません(materiaがエラー吐きます)。 ここではOnGhostChangedを例としましたが、OnGhostChanged以外の起動系イベントでも、それらのイベントに対して何かトークを返す可能性を考慮するのであれば、同様の処理を実施します。

というわけで、ここで特別付録、SSP起動時に送られてくる起動時イベント一覧。

状況起動時に送られてくるイベントと順番
初回起動時OnFirstBootOnMateriaExistOnEmbryoExistOnNekodorifExistOnBoot
通常起動時OnMateriaExistOnEmbryoExistOnNekodorifExistOnBoot
切り替わった時OnGhostChangedOnMateriaExistOnEmbryoExistOnNekodorifExistOnBoot
ゴースト削除時OnVanishedOnGhostChangedOnMateriaExistOnEmbryoExistOnNekodorifExistOnBoot
呼び出され時OnGhostCalledOnMateriaExistOnEmbryoExistOnNekodorifExistOnBoot

イベントの種類としては、OnBootOnFirstBootOnGhostChangedOnGhostCalledOnVanishedOnMateriaExistOnEmbryoExist、OnNekodorifExistの8種類となります。
なお、上記イベントのうち、OnMateriaExistとOnEmbryoExistはゴースト起動時にmateriaが起動していた場合、OnNekodorifExistはゴースト起動時に猫どりふが同時起動していた場合にのみ通知されるイベントです。

ところが。

OnOtherGhostVanishedとOnVanished

上に述べた各種起動時イベントですが、SSP限定において、起動時でもないのにOnVanishedが飛んできてしまうことがあるのです。そのキーとなるのが、OnOtherGhostVanished
OnOtherGhostVanished自体は起動時イベントではなく、ゴースト起動中に同時起動していたゴーストが削除されたときに送られてくるイベントですが、これに対して栞が「204 No Content」を返した場合(つまり、OnOtherGhostVanishedイベントの応答トークが存在しない場合)、なんとSSPは起動時でもないのにOnVanishedイベントを送信してきてしまうのです。

これが問題になるのはOnVanishedにイベントの応答のトークが存在していて、OnOtherGhostVanishedにはイベントの応答のトークが存在しているとき。
このとき、ゴースト起動中に起動時イベント(OnVanished)を受信することでタイプDのエントリの初期化処理(初期化処理2)が実施されてしまうのです。

じゃあ、どうするか。一番、単純な手としては、OnOtherGhostVanishedのイベントの応答のトークが存在しない場合にOnVanishedのイベント応答のトークを返してあげてOnVanishedが送られて来ないようにすること。どっちにしろ、次にOnVanishedが送られて来るのですから、そんな対策で十分でしょう。
で、モデルは以下の通り。

System.Callback.OnGET,System.Callback.OnNOTIFY: $(entry ev.${System.Request.ID})
# OnBoot受信時の処理
ev.OnBoot: $(初期化処理2;entry evTalk.OnBoot;)
# OnBootのイベント応答トーク
evTalk.OnBoot: \1\s[10]\0\s[0]起動しました。今まで${叩かれた回数}回叩かれました。\e
# OnGhostChanged受信時の処理
ev.OnGhostChanged: $(
  if $(size evTalk.OnGhostChanged) $(
    初期化処理2;
    entry evTalk.OnGhostChanged;
  )
)
# OnBootChangedのイベント応答トーク
evTalk.OnGhostChanged: \1\s[10]\0\s[0]切り替えられました。今まで${叩かれた回数}回叩かれました。\e
# OnVanished受信時の処理
ev.OnVanished: $(
  if $(size evTalk.OnVanished) $(
    初期化処理2;
    entry evTalk.OnVanished;
  )
)
# OnVanishedのイベント応答トーク
evTalk.OnVanished: \1\s[10]\0\s[0]ゴーストが削除されました。今まで${叩かれた回数}回叩かれました。\e
# OnOtherGhostVanished受信時の処理
ev.OnOtherGhostVanished: $(entry evTalk.OnOtherGhostVanished ${evTalk.OnVanished})
# OnOtherGhostVanishedのイベント応答トーク
evTalk.OnOtherGhostVanished: \1\s[10]\0\s[0]次は私の番か……。\e
# 以下、これまでと同じ
ev.OnClose: \1\s[10]\0\s[0]さようなら。\-
ev.OnGhostChanging: \1\s[10]\0\s[0]さようなら。\e
ev.OnMouseDoubleClick: $(inc 叩かれた回数; inc 叩かれた回数.今回;)\1\s[10]\0\s[0]これで${叩かれた回数}回目だ。今回起動時だけで${叩かれた回数.今回}回だ。叩くなバカ。\e
System.Callback.OnUnload: $(セーブ)
=kis
function セーブ $(
  save "sav.txt" 叩かれた回数 叩かれた回数.今回;
);
function ロード $(
  clear 叩かれた回数;
  load sav.txt;
);
function 初期化処理 $(
  ロード;
  if $[ $(size 叩かれた回数) == 0 ] $(.setstr 叩かれた回数 0);
);
function 初期化処理2 $(
  .setstr 叩かれた回数.今回 0;
);
初期化処理;
=end
ev.OnCacheSuspend: $(セーブ)
ev.OnCacheRestore: $(初期化処理)

現在、事実上、起動時イベントが飛んでくるのはゴースト起動時とOnOtherGhostVanishedに何もトークを返さなかった場合のOnVanishedイベントのみです。
ここでは、その事実を信用し、これ以上の対策はもう考えないことにします。

もし信用できない場合は……? パス。信用できない場合の更なる対策処理はとんでもなく複雑な処理になるので。
ただOpenKEEPSがそのへんバッチリ対策しているはずなので気になる方はOpenKEEPSの中をみてください。じゃあね。

参考リンク

コメント

  • 目指したところは現在のSSPで問題おきなきゃいいや、ぐらいのレベルです。 -- そずべねぐ 2009-12-27 (日) 23:14:32
  • あと、このへんおかしいよ、とか、こういうケースが考えられてないよ、とかありましたら教えてください。 -- そずべねぐ 2009-12-27 (日) 23:15:05
  • そういえば、raiseotherで無理やりOnBoot送りつけるなんてあるな……。気になる人は起動時にSender取得してSenderの一致しないOnBootとかは捨ててください。 -- そずべねぐ 2009-12-28 (月) 08:13:31
  • ゴースト終了トーク中にダブルクリックでトークを潰した場合はどうだろう? ここで話してるレベルなら終了時に何かしてるわけじゃないから問題ないかな。 -- そずべねぐ 2009-12-29 (火) 08:25:53
  • あ、そういえば、クラッシュに備えてユーザー名の変更やしゃべり頻度の変更などを実施したときはセーブしておく方が良いというのを書くの忘れてた。 -- そずべねぐ 2010-01-02 (土) 00:32:56


トップ   編集 凍結 差分 履歴 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2010-01-02 (土) 00:33:27