セーブ・ロード

ラベル

まずは、今まで軽く触れておきながら何も説明してなかったラベルについてここで触れておきます。ラベルは、KAGシナリオファイルに記述する通常文字でもタグでもないもので、行頭にアスタリスク記号(*)を配置したものです。その行はまるまるラベルとして扱われます。ラベルは、シナリオの処理する場所を変更(ジャンプ)する際などの目印としても使われますが、セーブ・ロードや文章の既読などの単位としても使われます。

2種類あるラベルですが、途中にパイプライン記号(|)を含めると後者のラベルとなります。ここで、パイプラインまでをラベル名、以後をページ名といいます。

それ以上の細かい説明は省きます。詳しくは(kag3doc)選択肢を使おう(kag3doc)セーブ・ロードに対応させようなどを参照してください。

↓ジャンプなどの目印としてしか利用できない(セーブの単位にならない)[r]
*label_name_1
↓ジャンプにも使えるし、セーブの単位にもなる[r]
*label_name_2|page_name_2

onLabel

KAGシナリオを解析するKAGParserクラスは、ラベルを通過するとonLabelメソッドを呼びます。ページ名がないラベルのときはonLabel("*label_name_1")というように呼んで、ページ名があるときはonLabel("*label_name_2", "page_name_2")というように呼びます。

このKAGParser.onLabelはKAGWindow.onConductorLabelを呼び、ここで、ラベルを通過したときの処理を記述しています。

// kag3/templete/system/Conductor.tjs
0494:   function onLabel()
0495:   {
0496:     return owner.onConductorLabel(...);
0497:   }

usingExtraConductorは、サブルーチン中なら真、そうでないなら偽になります。サブルーチンに関しては別ページにて。とりあえず通常はincRecordLabelを呼んで、次にsetRecordLabelを呼んでいます。前者が直前のラベルを既読カウントに1を加えています。そして、後者でこのラベルを現在のラベルとしてセットしています。そしてsetUserSpeedで、このラベルの文章の未読/既読の状態を調べて文字描画速度を調整しています。そのほか、ページ名がTJS式のときの対応やらも行われています。

そして2478行目からは、ページ名が存在するとき(セーブ可能ラベルのとき)のいろいろな処理を記述しています。根幹になっているのが、storeFlagsメソッドです。

KAGシステムにはユーザが使えるものとしてゲーム変数(f)とシステム変数(sf)と一時変数(tf)の使用が紹介されています。もちこんこれ以外にも使えるのですが、既存の識別子などと競合する可能性もありますから、基本的にはこれらの変数を利用するのがよいです。これらの変数のうち、セーブ・ロードに関係するのはゲーム変数です。storeFlagsメソッドではそのゲーム変数をpflagsにコピーします。(currentLabelなどKAGシステムのコアとしてゲーム変数と同じ扱いで保存させたいものはpcflagsに格納します。)

つまり、pflagsとpcflagsは直前のセーブ可能ラベルにおけるゲーム変数を一時的に保存するための変数(辞書配列)です。

// kag3/templete/system/MainWindow.tjs
2458:   function onConductorLabel(label, page)
2459:   {
2460:     // コンダクタがラベルを通過した
2461:     if(!usingExtraConductor)
2462:     {
2463:       incRecordLabel(true);
2464:       setRecordLabel(conductor.curStorage, label);
2465:     }
2466:     setUserSpeed();
2467:     if(!usingExtraConductor)
2468:     {
2469:       if(!getCurrentRead() && skipMode != 4)
2470:         cancelSkip(); // 未読なのでスキップを停止
2471:       currentLabel = label;
2472:     }
2473:     if(page !== void && page !== '')
2474:     {
2475:       if(page[0] == '&') page = Scripts.eval((page.substring(1)));
2476:       currentPageName = page;
2477:     }
2478:     if(page !== void)
2479:     {
2480:       pushHistoryOfStore();
2481:       stablePosibility = false;
2482:       dm(conductor.curStorage + " : ラベル/ページ : " + label + "/" + currentPageName);
2483:       if(usingExtraConductor) throw new Exception("右クリックサブルーチン内/extraCondutor"
2484:         "サブルーチン内では保存可能なラベルを記述できません");
2485:       storeFlags(), storeLabelPassed = true, setMenuAccessibleAll();
2486:       if(recordHistoryOfStore == 1) // 1 : 保存可能なラベルごと
2487:         setToRecordHistory();
2488:     }
2489:     return true;
2490:   }

セーブ・ロードメニュー

デフォルトで備わっているセーブ・ロードの機能はメニューによって実装されています。話をややこしくしないために、フリーセーブモードはデフォルトの通り使用しないことにします。その場合、「栞をはさむ」メニューをクリックするとそれぞれの栞にセーブできるようなサブメニューが存在します。

このサブメニューの項目はKAGMenuItemクラスのインスタンスです。そして、KAGMenuItemクラスのコンストラクタに渡される引数のうち、第4引数にはクリックされたときに実行されるメソッドのオブジェクト(括弧がない状態のメソッド)を記述します。1630行目を見ると、「栞をはさむ」メニューのサブメニューを選択するとonBookMarkStoreメソッドが呼ばれることがわかります。ただし、このときonBookMarkStoreメソッドには、メソッドを呼んだオブジェクト――すなわちKAGMenuItemクラスのインスタンスが引数として渡されます。

// kag3/templete/system/MainWindow.tjs
1621:   function createBookMarkSubMenus()
1622:   {
1623:     // 「栞をたどる」「栞をはさむ」以下にサブメニュー項目を追加
1624:     if(freeSaveDataMode) return; // フリーセーブモードではなにもしない
1625:     if(typeof this.storeMenu !== "undefined" && storeMenu.visible)
1626:     {
1627:       for(var i = 0; i<numBookMarks; i++)
1628:       {
1629:         var item;
1630:         storeMenu.add(item = new KAGMenuItem(this, string i, 0, onBookMarkStore,
1631:           false));
1632:         item.bmNum = i;
1633:         item.orgEnabled = false;
1634:       }
1635:     }

onBookMarkStoreは、何番目の栞かをあらわすsender.bmNumを使用して、saveBookMarkWithAskメソッドを呼び出しています。これは、セーブを行うかダイアログでユーザに問う形式のセーブです。リードオンリーモードや、栞に保護がかかっていれば何もしません。セーブするかどうか訊ねるダイアログで、yesが選択されると、saveBookMarkを呼び出します。

// kag3/templete/system/MainWindow.tjs
1719:   function onBookMarkStore(sender)
1720:   {
1721:     // 栞をはさむメニューが選択された
1722: //    if(!sender.parent.accessEnabled) return;
1723:     saveBookMarkWithAsk(sender.bmNum);
1724:   }
 
1988:   function saveBookMarkWithAsk(num)
1989:   {
1990:     // 栞番号 num に栞を設定する
1991:     // そのとき、設定するかどうかをたずねる
1992:     if(readOnlyMode) return false;
1993:     if(bookMarkProtectedStates[num]) return false;
1994:     var prompt = "栞 ";
1995:     if(num < numBookMarks) prompt += (num + 1);
1996:     if(bookMarkDates[num] != "") // bookMarkDates が空文字の場合は栞は存在しない
1997:       prompt += "「" + bookMarkNames[num] + "」";
1998:     prompt += "に「"+ pcflags.currentPageName + "」をはさみますか?";
1999:     var result = askYesNo(prompt);
2000:     if(result) return saveBookMark(num);
2001:     return false;
2002:   }

saveBookMarkメソッドではsaveBookMarkToFileを呼び出しています。saveBookMarkToFileメソッドが実際にデータをセーブしているメソッドです。pcflagsとpflagsをまとめたdataという辞書配列を作り、辞書配列の構造を保ったまま保存((tjs2doc)Dictionary.saveStruct)しています。サムネイルで保存するように設定していれば、ウィンドウの画面の画像のファイルの中にdataを保存(画像としても有効で、かつ内部に任意のデータを孕んだファイルとして保存)します。

ファイルへの書き出しが成功すると、getBookMarkInfoFromDataを呼んで今度はKAGシステムとしてnum番目の栞にセーブされたという情報を格納します。bookMarkNames[num]にセーブされたときのページ名を、bookMarkDates[num]にセーブされたときの時刻を格納しています。

// kag3/templete/system/MainWindow.tjs
1897:   function saveBookMark(num, savehist = true)
1898:   {
1899:     // 栞番号 num に栞を保存する
1900:     if(readOnlyMode) return false;
1901:     if(bookMarkProtectedStates[num]) return false;
1902: 
1903:     var ret = saveBookMarkToFile(getBookMarkFileNameAtNum(num), savehist);
1904:     if(ret)
1905:     {
1906:       // メニュー / bookMarkNames / bookMarkDates を更新
1907:       getBookMarkInfoFromData(pcflags, num);
1908:     }
1909:     return ret;
1910:   }
 
1912:   function getBookMarkInfoFromData(dic, num)
1913:   {
1914:     // 辞書配列 dic から栞のページ名と日付を読み出し、
1915:     // bookMarkDates[num] や bookMarkNames[num] に設定する
1916:     if(num < numBookMarks)
1917:     {
1918:       bookMarkNames[num] = dic.currentPageName;
1919:       var date = new Date();
1920:       date.setTime(dic.storeTime);
1921:       date = "%04d/%02d/%02d %02d:%02d".sprintf(
1922:         date.getYear(), date.getMonth() + 1, date.getDate(),
1923:         date.getHours(), date.getMinutes() );
1924:       bookMarkDates[num] = date;
1925:       setBookMarkMenuCaptions();
1926:       saveSystemVariables();
1927:     }
1928:   }
 
1810:   function saveBookMarkToFile(fn, savehist = true)
1811:   {
1812:     // ファイル fn に栞を保存する
1813:     if(readOnlyMode) return false;
1814:     pcflags.storeTime = (new Date()).getTime(); // 日付を保存
1815: 
1816:     // セーブデータをまとめる
1817:     var data = %[];
1818:     data.id = saveDataID;
1819:     data.core = pcflags;
1820:     data.user = pflags;
1821:     if(savehist) data.history = historyOfStore;
1822: 
1823:     if(saveThumbnail)
1824:     {
1825:       // サムネイルを保存
1826:       lockSnapshot();
1827:       try
1828:       {
1829:         // サムネイルのサイズまで縮小
1830:         var size = calcThumbnailSize();
1831:         var tmp = new Layer(this, primaryLayer);
1832:         try
1833:         {
1834:           tmp.setImageSize(size.width, size.height);
1835:           tmp.face = dfAlpha;
1836:           tmp.stretchCopy(0, 0, size.width, size.height, snapshotLayer,
1837:             0, 0, snapshotLayer.imageWidth, snapshotLayer.imageHeight, stLinear);
1838:           /*
1839:           // サムネイル画像をセピア調にして保存する場合はコメントアウトを解除
1840:           tmp.doGrayScale();
1841:           tmp.adjustGamma(
1842:                   1.3, 0, 255,  // R gamma, floor, ceil
1843:                   1.0, 0, 255,  // G gamma, floor, ceil
1844:                   0.8, 0, 255); // B gamma, floor, ceil
1845:           */
1846:           try
1847:           {
1848:             // サムネイルを保存
1849:             tmp.saveLayerImage(fn, "bmp" + thumbnailDepth);
1850: 
1851:             // データを保存
1852:             var mode = saveDataMode;
1853:             mode += "o" + size.size; // モード文字列に 書き込みオフセットを指定
1854:             (Dictionary.saveStruct incontextof data)(fn, mode);
1855:           }
1856:           catch(e)
1857:           {
1858:             invalidate tmp;
1859:             unlockSnapshot();
1860:             System.inform("ファイルに保存できません (ファイルを開けないか、"
1861:               "書き込み禁止です)");
1862:             return false;
1863:           }
1864:         }
1865:         catch(e)
1866:         {
1867:           invalidate tmp;
1868:           throw e;
1869:         }
1870:         invalidate tmp;
1871:       }
1872:       catch(e)
1873:       {
1874:         unlockSnapshot();
1875:         throw e;
1876:       }
1877:       unlockSnapshot();
1878:     }
1879:     else
1880:     {
1881:       // 通常のファイルに保存
1882:       try
1883:       {
1884:         (Dictionary.saveStruct incontextof data)(fn, saveDataMode);
1885:       }
1886:       catch(e)
1887:       {
1888:         System.inform("ファイルに保存できません (ファイルを開けないか、"
1889:           "書き込み禁止です)");
1890:         return false;
1891:       }
1892:     }
1893: 
1894:     return true;
1895:   }

システム変数の保存

前セクションのgetBookMarkInfoFromDataメソッドの一番最後でsaveSystemVariablesメソッドが呼ばれています。これはシステム変数を保存するメソッドです。

// kag3/templete/system/MainWindow.tjs
1912:   function getBookMarkInfoFromData(dic, num)
1913:   {
 
1926:       saveSystemVariables();
1927:     }
1928:   }

システム変数とは、ゲームをプレイする際に栞をロードしたとしても一貫して保たれるべきデータが含まれています。システム変数にもコア(KAGシステム)用のscflagsとユーザ用のsflagsの2つの変数が存在します。このうちユーザ用のsflagsはglobal.sfという名前の別名がつけられているため、KAGシナリオからsf.testとアクセスすることができます。

ゲーム変数と違ってラベル通過時点ではなく、常に最新の情報で保存します。saveSystemVariablesメソッドは、(フルスクリーンなど)変更があったときや終了するときにちょくちょく呼ばれて、その時点のシステム変数をファイルに書き出します。終了時点に保存された内容を、再び起動したときに読み込んでいるため、情報の引き継ぎが可能となっています。

// kag3/templete/system/MainWindow.tjs
1137:   function saveSystemVariables()
1138:   {
1139:     // システム変数の保存
1140:     if(!isMain) return;
1141: 
1142:     // プラグインを呼ぶ
1143:     forEachEventHook('onSaveSystemVariables',
1144:       function(handler, f) { handler(); } incontextof this);
1145: 
1146:     // フルスクリーン
1147:     scflags.fullScreen = fullScreened;
1148: 
1149:     // 文字表示速度
1150:     scflags.autoModePageWait = autoModePageWait;
1151:     scflags.autoModeLineWait = autoModeLineWait;
1152:     scflags.userChSpeed = userChSpeed;
1153:     scflags.userCh2ndSpeed = userCh2ndSpeed;
1154:     scflags.chDefaultAntialiased = chDefaultAntialiased;
1155:     scflags.chDefaultFace = chDefaultFace;
1156:     scflags.chNonStopToPageBreak = chNonStopToPageBreak;
1157:     scflags.ch2ndNonStopToPageBreak = ch2ndNonStopToPageBreak;
1158: 
1159:     // ブックマーク名
1160:     scflags.bookMarkNames = bookMarkNames; // (コピーではなくて)参照で十分
1161:     scflags.bookMarkDates = bookMarkDates;
1162:     scflags.bookMarkProtectedStates = bookMarkProtectedStates;
1163: 
1164:     scflags.lastSaveDataNameGlobal = lastSaveDataNameGlobal;
1165: 
1166:     // ファイルに書き込む
1167:     if(!readOnlyMode)
1168:     {
1169:       var fn = saveDataLocation + "/" + dataName +
1170:         "sc.ksd";
1171:       (Dictionary.saveStruct incontextof scflags)(fn, saveDataMode);
1172: 
1173:       var fn = saveDataLocation + "/" + dataName +
1174:         "su.ksd";
1175:       (Dictionary.saveStruct incontextof sflags)(fn, saveDataMode);
1176:     }
1177:   }

その他の方法でセーブ・ロード

メニュー以外からのセーブ・ロードはどのように行われているのでしょうか。

saveタグは、ask属性に値によってsaveBookMarkWithAskメソッドとsaveBookMarkメソッドを呼び分けています。また、(kag3doc)TJSをもっと使うためににあるkag.storeBookMarkも同様です。

// kag3/templete/system/MainWindow.tjs
5092:   save : function(elm)
5093:   {
5094:     // 栞の読み込み
5095:     if(elm.ask !== void && +elm.ask)
5096:       saveBookMarkWithAsk(+elm.place);
5097:     else
5098:       saveBookMark(+elm.place);
5099:     return -4;
5100:   } incontextof this,
// kag3/templete/system/MainWindow.tjs
2210:   function storeBookMark(num, ask = true)
2211:   {
2212:     // KAG 2.x 互換用
2213:     if(ask)
2214:       return saveBookMarkWithAsk(num);
2215:     else
2216:       return saveBookMark(num);
2217:   }

栞の消去

次に栞のデータについて見てみましょう。

KAG3にはerasebookmarkタグというものがあります。タグハンドラではeraseBookMarkメソッドを呼び出しています。eraseBookMarkメソッドは栞が保護されていなければ、bookMarkDates[num]に空文字列を代入しているのみです。そして、setBookMarkMenuCaptionsメソッドでメニューの栞のキャプション(メニューに表示される文字列)を再設定しています。そして、bookMarkDates[num]が空のときは栞は存在しないとみなします。

吉里吉里2/TJS2では標準ではファイルを削除することはできません。プラグインなどを利用することで可能ですが、栞の削除では使われていません。実際のセーブファイルは存在したままですが、KAGシステムの管理から外すことで、存在しないものとしているのです。

// kag3/templete/system/MainWindow.tjs
5061:   erasebookmark : function(elm)
5062:   {
5063:     // 栞を削除
5064:     eraseBookMark(+elm.place);
5065:     return 0;
5066:   } incontextof this,
 
2129:   function eraseBookMark(num)
2130:   {
2131:     // 栞を消す
2132:     // num < numBookMarks の時にしか動作しないようになったので注意
2133:     if(num < numBookMarks)
2134:     {
2135:       if(!bookMarkProtectedStates[num])
2136:       {
2137:         bookMarkDates[num] = "";
2138:         setBookMarkMenuCaptions();
2139:       }
2140:     }
2141:   }

特別なセーブ

「最初に戻る」という機能があります。通常はタイトル画面などに設定しておき、プレイ中どこからでも戻ることができます。startanchorタグで設定できます。タグハンドラから追っていくと、999番目の栞にセーブしていることがわかります。また、gostartタグも同様に999番目の栞のロードです。

このように、「最初に戻る」機能は特別な番号の栞に行っているセーブです。ですので、セーブ可能なラベルを通過しなければ使えないのです。また、セーブ・ロードと同じものですので、(startanchorタグの位置のゲーム変数ではなく)セーブ可能ラベル通過時におけるゲーム変数しか保存されません。

// kag3/templete/system/MainWindow.tjs
5102:  startanchor : function(elm)
5103:  {
5104:    // 「最初に戻る」の使用不可・使用可を設定する
5105:    setStartAnchorEnabled(elm.enabled === void || +elm.enabled);
5106:    return 0;
5107:  } incontextof this,
 
2166:   function setStartAnchorEnabled(enabled)
2167:   {
2168:     // 「最初に戻る」の有効/無効の設定
2169:     startAnchorEnabled = enabled;
2170:     if(enabled) saveBookMark(999, false); // 999 番に保存
2171:     setMenuAccessibleAll();
2172:   }
 
inside/save.txt · 最終更新: 2008/01/28 12:02 by tohka