はじめに
前回の記事はlogging API ver.04の仕様を説明しました。
またその応用例として、タートルが作業をしている間にゲームを終了しても、ゲーム再開時に自動的に初期位置へ戻ってくるという使い方も紹介しました。
loggin APIの応用例(タートル作業中にゲームを中断しても大丈夫!)
サンプルプログラム(ファイル名は、「startup」にすること)
os.loadAPI("logging") if fs.exists(logging.LOG_FILE) then term.write("Return to home position..") logging.makeRevFile() logging.backupFile(logging.LOG_FILE) logging.backupFile(logging.REV_FILE) shell.run(logging.REV_FILE.."-bak") print("ok") else for i=1, 20 do logging.forward() end logging.turnRight() for i=1, 20 do logging.forward() end end
このサンプルコード「startup」の具体的な使い方とその挙動は次のようになります。
- プロンプトで、
> rm my*
のように打ち込んで、ログファイルを消しておく。 - startupプログラムを実行する。
- 以前のログファイルがないので、前に20歩、右向いて、20歩・・・
- ~タートル移動中にゲーム終了~
- ~ゲーム再開~
- 「startup」プログラムが自動実行される
- ログファイルが残っていたらそれは前回作業途中で終了したということ
- loggin APIを使ってログファイルを逆行動プログラムファイルへと変換
- そのプログラムファイルを自動実行することで初期位置へ戻る
この応用プログラムは、ゲームを正常に終了したならば、正常に動きます。
正常終了とは、たとえば、ESCキーを押してゲームメニューを出して「Save and Quit to Title」ボタンで現在のワールドデータをセーブしタイトル画面に戻ったり、またあるいは、マイクラWindowの右上の×ボタンを押すことでも正常に終了できるようです(これはMinecraft1.7.2の場合で、MC1.6.4以前については未検証です)。
しかし!
しかし正常にゲームを終了しなかったとき、たとえば何らかのエラーを起こしてゲームがクラッシュしたり、またあるいは、CTRL+ALT+DELキー押してタスクマネージャーでjavaw関連のプロセスを全てKILL!!したときにはこのサンプルプログラムが動かない可能性があります。
この原因は、異常終了すると直前のワールドセーブデータの状態に巻き戻ってしまうためです。
Minecraftのワールドデータは一定間隔ごとに自動セーブされます。それに対して、タートルのログファイルはタートルが移動するたびリアルタイムに更新されるので、巻き戻ったワールドセーブデータとログファイルに食い違いがでてしまうためなのです。
今回は、この問題を解決しましょう。
解決のアイデア
いきなり結論
タイムスタンプを使って移動履歴を管理しましょう!
ログファイルに移動履歴を追加するときに、同時にタイムスタンプ(タイムスタンプの算出方法は後述)も一緒に追記します。たとえば以下のように。
-- daytime:135.101 turtle.forward() -- daytime:135.109 turtle.forward() -- daytime:135.117 turtle.forward() -- daytime:135.125 turtle.forward()
そして、ゲームを終了して再開したときのマイクラ時刻とログファイル中のタイムスタンプを比較することで、たとえ時間が巻き戻ってしまったとしても、その失われた時間部分のログを無視することができます。
たとえば、ゲームを再開した瞬間のタイムスタンプが135.116だったとしましょう。 上記ログファイルと比較することで、
- 未来のログがある → これは、巻き戻ってますわ
- 後半の135.117以降の記述は無視
- 135.101 ~ 135.109までが正常なログ
と判断できるわけです。
タイムスタンプの算出方法
やはりos.time()
関数によって、マイクラ時間を使うのが妥当でしょう。
ただし注意しなくてはならないのは、os.time()は0.000~23.999までの実数を返すことです。
日付変更するとまた0からはじまるので日付をまたぐと問題になります。
そこで使うのがマイクラ内の経過日数を返すos.day()
です。
1日は24時間なので、以下の計算式でタイムスタンプを作ることができます。
os.day()*24 + os.time()
(os.day()=10で、os.time()=12.500のときは、252.500)
logging API version 0.5
ソースコード: http://pastebin.com/30Gp0qVJ
使用上の注意
このプログラムは以下の環境において、2桁回数の再起動検証を通して正常に動くことを確認しています。
- Minecraft1.6.4+ComputerCraft1.63 + MOD数100近く
- Minecraft1.7.2+ComputerCraft1.64pr3 + MOD数40くらい
しかし、PCにとても負荷がかかっている状況でこのプログラムが正常に動くかどうかは自信がありませんので、重要な施設での使用は万全の注意を払ってご利用ください。
(たとえば、fpsが1桁の非常に重い環境ではどうなるか検討もつきません)
重要な部分の解説
解説のためのコメントを入れたら、全体の行数が実際のコード行数の2倍近くに膨れ上がってしまいました。
例によってコメントの英語のひどさには目をつぶってください ^o^
上記ソースコードから重要な部分を抜粋して、解説します。
また、今回の冒頭に上げたサンプルコードがそのまま使えます。検証用にどうぞ。
47行目
CURRENT_TIME = os.day()*24 + os.time()
タイムスタンプとして使うために、ゲーム起動時のマイクラ時間(datetime)をAPI変数logging.CURRENT_TIMEに代入しておきます。
82-86行目
- logging.writeTimeStamp(filename)の定義
- "-- daytime: 123.456" のようなタイムスタンプをログファイルに書き込むグローバル関数です。
92-99行目
- readTimeStamp(str)の定義
- ファイルから読み取った1行の文字列を引数として与えることで、それがタイムスタンプ行であればそのタイムスタンプ(実数)だけ取り出します。
- string.sub(文字列, 開始位置, 終端位置)は次のような関数です。
- 文字列の開始位置(数値)から終端位置(数値)までを取り出す。なお、終端位置として"-1"を指定すると文字列の末尾を意味する。
- よって、
string.sub(str, 13, -1)
は、文字列strの前から13番目から文字列末尾までの文字列を取り出します。当然ながら、"-- abcdefghijkl123456789"のような14文字以上のコメント行にも反応してしまいますので、ご注意を。心配なら、すぐ上のコメント行と入れ替えてください*1。 - さらにtonumber()関数で文字列を数値化しています。
- この関数は、文字列あるいはfalseを返すよう設計していますが、tonumber(nil)のときに値nilを返すことになるので、
return tonumber() or false
で無理やりfalseを返すようにしています。
103-117行目
- readTrans(filename, trans_tbl)関数の定義
- LOG_FILEから、1行ずつ文字列を読み取り、ローカル変数lineに代入。
- lineがタイムスタンプ行だったら、そのタイムスタンプとゲーム起動時刻「CURRENT_TIME」と比較して、ログファイルから必要な部分だけ読み出す
- lineが関数文字列行だったら、TRANS_TBLを使って、逆関数に変換
- lineがそれ以外ならば処理を飛ばす。
- 最終的に、タイムスタンプを考慮しつつ、巻き戻り部分のログは無視して必要なログだけ読み取って、その逆関数文字列群(テーブル)を返す関数です。
おわりに
かなり長い期間でしたが、これでFS APIの解説とその利用方法についての連載を終了します。
実はもう一つだけこのlogging APIの改善アイデアがあるのですが、それはまた別の機会でも良いかなと。
アイデア「戻ってくるときに行きと同じ経路を逆走するのではなく、その位置からスタート位置まで最短経路を通って戻ってくるような機能」
もしリクエストがあるならば、この連載の補記として続けても・・・(チラッチラッ
*1:コメントアウトしている、string.match()関数を使った正規表現による文字列パターンマッチング(とそのマッチした文字列のキャプチャ)の方がタイムスタンプを正確に取り出せるのですが、解説が大変なためにstring.sub()関数に逃げました ^o^ そのうち、しっかりと解説を・・・するかも? 今の段階では「文字列からパターンにマッチする数値を取り出すおまじない」と思っていただければ。