2016年10月5日水曜日

ポケモンGOのポッポマラソンに必要なアメ数を計算する。

最近ブームが去りつつあるような気もするポケモンGOですが、私はいまだに楽しく遊んでいます。
しかし、トレーナーレベルが20以降になるとレベルアップに必要な経験値が非常に大きく、なかなか伸び悩んできました。


というわけで、ポケモンGOでトレーナーレベルを上げるには、効率の良い経験値稼ぎが必要です。そこで行われる方法がポッポマラソンなわけですね。
ポッポ、ビートル、キャタピーは比較的入手性がよく、また進化に必要なアメ数がとても少ないです。進化で得られる経験値は500EXPと非常に高く、また、その経験値を入手するタイミングもコントロールできるため、しあわせタマゴで経験値を倍増させている30分間に集中して進化させ、経験値を荒稼ぎするのがいわゆる「ポッポマラソン」なわけです。

しかし、しあわせタマゴは貴重なアイテムであるばかりか30分という制限時間まであり、なおかつ進化にはアニメーションが付きまとうため、最大限手際よく30分間ポケモンを進化させ続ける必要が出てきます。


アニメーションの時間はおおよそ25秒程度、操作をするのに必要な時間を加味して、30分間ではおおよそ60~70匹程度のポケモンを進化させられることができます。すなわち、70匹程度ポケモンが進化させられる状態にしたうえでポッポマラソンを開始させなければなりません。しかし、進化やポケモンを博士に送る(以下、『D進』と呼ぶこととする)ことでさらにアメを得られるため、それに必要なアメの数はそう簡単に暗算できるほどのものでもありません。もちろん進化させるのはポッポだけでないでしょうから、今、自分の手持ちの中で進化できるポケモンの数を管理するのは頭の中だけでできるようなものでもありません。

というわけで、ポッポマラソンに必要なアメ数を計算し、管理する必要が出てきます。

アメは、ポケモンを進化させることで1つ、ポケモンをD進させることで1つ手に入れることができます。すなわち、ポッポマラソン中にもどんどんアメが増えていくわけです。そのことを、Excelを使って管理してみましょう。


まず立式ですが、現在持っているアメの数を$c$、進化に必要なアメの数を$c_e$とします。進化させながらだと進化させた直後に1個アメが手に入りますから、実質進化に必要なアメの数は$c_e-1$個になります。というわけで、進化可能数$e$は
\[e=\left\lfloor\frac{c}{c_e-1}\right\rfloor\]
となります。$\lfloor\rfloor$は床関数ですね。与えた数字以下の最大の整数を返す関数です。


しかし、この式では不十分です。
例えば11個アメを持っていた場合、この式では進化可能数が1になってしまいます。そうです。「実質進化に必要なアメ数」と言いましたが、進化で得るアメは、その進化では使えないですね。 その分の項が入っていませんでした。
これはどう入れるかというと、「最後の進化で得るアメによる進化可能数の上昇」を差し引いてやればいいわけです。最後の進化で得るアメによる進化可能数の上昇というとややこしいかもしれませんが、すなわち$1/c_e$です。すなわち、完成した式は
\[e=\left\lfloor\frac{c}{c_e-1}-\frac{1}{c_e}\right\rfloor\]
と書くことができます。

ただ~し、この式にはまだ1つ問題があります。$c=0$のときに、$e$がマイナスになってしまうんですね。例えば$c_e=12$とすると$e=\lfloor-0.08333...\rfloor=-1$となるわけです。ですので、まあせこいですけど絶対値を取ってから床関数にでも与えれば大丈夫でしょう。床関数の中身が負になる場合はあっても、それが-1以下になることは無いですから。
\[e=\left\lfloor\left|\frac{c}{c_e-1}-\frac{1}{c_e}\right|\right\rfloor\]
ちなみに、Windowsの多くの処理系の小数型から整数型への変換やExcelのROUNDDOWN関数では0方向への丸めとなります。すなわち、正の数に対しては床関数になりますが、負の数に対しては天井関数となるわけです。なので、そのようなもので処理する場合は、このように絶対値をわざわざとる必要はありません。


さて、この式の何がいいかというと、進化直後にD進するときの式にも簡単に転用できるからです。
進化直後にそのポケモンをD進させた場合は、進化とD進で2個アメが手に入りますから、実質進化に必要なアメの数は$c_e-2$個になります。しかし、それでは進化やD進で発生したアメをそのポケモンの進化に使えてしまうことになるので、その分を差し引いておく必要があります。まあ、ついでに負の数になっちゃう問題も込々で式を立てると
\[e_{D進}=\left\lfloor\left|\frac{c}{c_e-2}-\frac{2}{c_e}\right|\right\rfloor\]
となりますね。


「進化させたり、それをD進させたりして、それで生まれるアメをまた再び進化に使う」という考えは手続き的で、このように1つの数式で進化可能数を求めることはできません。どちらかと言えばプログラムでループを回すようなイメージになってしまいます。
ですが、このような「実質的に進化に必要なアメ数」という考え方と、その考えで起こる問題に対する修正項を加えるという考え方で1つの式にすることができ、簡単にExcel等で計算できるようになりました。


あと少しで70匹になるぞ…。

2016年9月22日木曜日

Google Driveのホスティングサービス終了によるSyntaxHighlighter周りの修正

3か月くらい前にSyntax Highlighterが正常に動作しなくなっていたため、その修正をした旨の記事を書きました。

そして、昨日久々に記事を書いて、いくつか自分の過去記事を巡回していたら

ま た 動 か な く な っ て る じ ゃ ね え か

ちきしょーめ、なんかウェブページを開くとダイアログが出てきてソースコードが正常に表示されねえぞ!
こういうことで時間を取られるのは本当に面倒ですが、正常に動作しないブログはいろいろと問題ですので原因の究明と対応が必要になってくるわけです。

いろいろ調べていると、どうもGoogle Drive側に問題があるようです。
Google ドライブでウェブページをホストする
ああ、なんということでしょう。3か月前にドヤ顔で「Bloggerがhttps以外を締め出したからSyntaxHighlighter作った人のホスティングサービスが使えなくなった!だからGoogle Driveに置くことで解決してやったぜ!」って言っていたのに、その2か月後にそのサービスが終了してしまうとは…。どうもホスティングサービスの終了は結構前から予告されていたようで、それに気づかなかった私が情弱だったようですね…。

代替サービスをいろいろ調べましたが、どうもDropboxやOneDriveなどの他のクラウドストレージサービスのホスティングサービスはイマイチのようですね。うーん…。

というわけで行きついたのがFirebaseでした。
ウェブ関係にはめっきり弱い私ですから、明らかに私の手に余りそうなサービスですが、とりあえずホスティングに関してまとまった記事がちらほら見当たりましたので、それに従って挑戦してみることにしました。

というわけで、10分くらいいろいろ戦いながらやってみましたが、比較的簡単にできました。
詳細はググっていただくとして、Node.jsをインストールしてからnpmでFirebaseのクライアントをインストールし、ローカルの適当なフォルダをFirebaseのフォルダに指定した後でpublicフォルダにSyntaxHighlighter周りのファイルを入れてデプロイすれば、そのフォルダがインターネット上に公開されるようになります。そして、FirebaseのサイトからホスティングのURLを確認し、BloggerのテンプレートのURLをGoogle Driveからこちらに変更してやれば完了です。めでたくコードがハイライト表示されるようになりましたとさ。言うまでもなく、Firebaseはhttpsに対応しております。


ふぅ。また3か月後に、今度はFirebaseのサービス終了みたいなお知らせ見たら私は泣くぞ…。

2016年9月21日水曜日

RailwayMap 0.1.0

さて、ついに公開にこぎつけました。

RailwayMap ver.0.1.0

まだ粗削りですが、一応バージョン0代として公開してもいいかなということで、公開します。

鉄道地図を描きたいことって無いですか?ありますよね?
ですが、なかなかいいソフトが無いです。白地図ソフトみたいなものや、Yahoo地図のように、「鉄道路線の線が引かれただけの地図」ならありますが、任意の区間を任意の色で塗って、などといったことはなかなかできません。
例えば、地元の路線地図を描きたい、各社の鉄道ネットワークがどのようになっているのか見てみたい、乗りつぶしマップを描きたい、鉄道路線の研究をしたのでカテゴリごとに塗ってみたいなど、いろいろな鉄道地図を描きたい理由があると思います。

そんな(主に自分の)ニーズに応えるのがこのRailwayMapです。


任意の区間を任意の色、太さ、大きさで描くことができます。
路線データは国土地理院の国土数値情報からユーザー自身でダウンロードしてきてください。

国土数値情報 鉄道データ

現状最新版が2015年版のようですが、2015年版は正常に読み込めません。2014年版とはデータ形式が変わったようです。気が向いたら対応します。2014年版を使ってください。
このデータは、日本の鉄道・軌道全路線が網羅されておりますので、JR在来線、新幹線から路面電車、モノレール、ケーブルカー、トロリーバス、ガイドウェイバスまで全部描画することができます。
メニューのファイルからデータを読み込めば、私の環境ならば15秒くらいで右側にツリー構造が現れます。そして、チェックボックスをいじったり、色や太さなどをいじれば好きなように地図が描けるというわけです。

細かな注意点はZIP内のReadMeを読んでもらうとして、とりあえずQ&Aだけこちらに並べておきます。

Q1.このソフト重いんじゃない?よく固まるぞ!
A1.何十万もある点をプロットするので仕方ありません。諦めてすごいCPUやつよいGPU、でかいメモリを買ってください。それでもだめなら諦めてください。(タスクマネージャー等でこのソフトのCPU使用率が1コア分より低いにも関わらず固まる場合はバグの可能性があります)
Q2.地図の形は変えられないの?
A2.線の太さや縮尺は変えられますが、形はメルカトル図法での出力になります。モルワイデ図法や正距方位図法、ボンヌ図法などでの出力はできません。
Q3.海岸線や緯線経線、縮尺などは描き出せないの?
A3.描き出せません。鉄道路線だけです。現状、実装の優先度はかなり低いです。
Q4.地図は画像ファイルに保存できないの?
A4.できません。できたら実装したいなって思っています。今のうちはPrintScreenしてください。
Q5.一生懸命設定した路線の色や表示/非表示の設定は保存できないの?
A5.できません。できたら実装したいなって思っています。
Q6.Raiload Linesの一番末端のノードの意味がわからない。どのノードがどの区間なのよ。
A6.XMLファイルに何駅と何駅の間のデータか書いてないので、仕方なくデータのIDの順序で並べています。そのうち手は入れたいとは思っていますが、見込みは立っておりません。
Q7.駅名が辞書順で並んでるのはクソ。駅順に並び替えてくれ。
A7.僕もクソだと思いますが、駅順はXMLファイルに含まれていないのでとりあえず今のところこれで我慢してください。
Q8.同じ駅が同じ路線に重複して存在している。例えば東海道新幹線には三島駅と新大阪駅が2つある。バグでは?
A8.バグではありません。元のXMLにも2つあります。理由はよくわかりません。

てなわけで、よろしくお願いします。
現在開発段階の途中で公開したような形なので、そのうちいろいろなところを仕上げていくつもりです。モチベーションが保てるかどうかはわかりませんが。

2016年8月19日金曜日

WPFでPolyPolyline

GDIにはPolyPolylineという関数があります。Polylineは節点を指定して折れ線を描く関数ですが、PolyPolylineはその折れ線を複数同時に描くことができます。Polylineを複数回呼び出すより効率がよく、また、「Polylineを複数回呼び出す」という制御すら必要なくなるので非常に重宝した関数でした。

私みたいな老害は、とりあえずこういうクラシカルな関数が新しい描画システムに無かったら怒るわけです。「なんでWPFにPolyPolylineは無いんだ!」と。(Polylineはあります)

特に、WPFはGDIを使ったクラシカルなアプリとは違い、UIとロジックの分離が基本になります。あまり細かく言うと怖い人達にマサカリを投げられそうなのでやめておきますが、ロジック部が作りだした接点の配列を使って複数の線を動的に描画したいとき、XAMLでは動的にPolylineを複数回呼び出すことができず困ってしまいます。PointCollectionの配列をバインディングしたら複数の線を描画してくれるようなPolyPolylineが欲しい!!!!1!!!


しばらく悩んでいましたが、ふと思いつきました。

「ListViewでItemTemplateを使うと複数のアイテムの描画をかなり強力にカスタマイズできるよな…?ならば、そのシステムで複数の線を描画させればいいのでは?」

ListViewには、その大元となる親クラスとしてItemsControlというものがあります。
これでPointCollectionの配列を受け取って、ItemTemplateで配列の個々のPointCollectionについてPolylineで描画することで、PolyPolylineと同等の機能が実現できるはずです。
<ItemsControl ItemsSource="{Binding PointCollections}">
    <ItemsControl.ItemTemplate>
        <DataTemplate DataType="PointCollection">
            <Polyline Points="{Binding}" Stroke="Black" StrokeThickness="1" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
</ItemsControl>
ItemsControlのItemsSourceでPointCollectionの配列をバインディングし、ItemTemplateでPolylineを使って描画しています。また、ItemsPanelでCanvasを指定してあげることによって、Polylineを描画するエリア(ItemsControlコントロールが貼られている場所)をCanvasにすることができます。
至ってシンプルです。

あとはViewModel側でPointCollectionの配列を与えてあげます。


描画した結果がこちらです。


1万個を超えるアイテム数ですが、これでも難なく表示してくれちゃうWPFさんイケメン!(GDIだと(時代も時代ですが)リソース不足で落ちるのが関の山(ry

もちろん、ItemsControlを使っていますので、PointCollectionへの配列ではなく座標情報以外に別の情報も含んだクラスへの配列にすることで、例えばある一部分だけ色を変えたり線の太さを変えたり、はたまた点線にしたりすることもできるでしょう。そこまでやると、さすがにGDIのPolyPolylineにはできないことになりますね。強力なWPF+XAMLだからこそできることになるでしょう。

2016年7月10日日曜日

VLC media playerをLAN経由で操作する


VLC media playerというメディア再生ソフトがあります。クロスプラットフォームでオープンソース、そして数多くのコーデックを内蔵しているため、ほとんどのメディアファイルを再生できます。私も、動画ファイルの再生にはとても重宝しています。

さて、このVLC media playerですが、LAN経由で操作する機能を持っています。
例えば動画を再生中、パソコンから離れて見ていても、手元のスマホなどがLANにつながっていれば、再生を止めたり、シークしたり、プレイリストの次のファイルへ行ったりと、様々な操作ができるとても便利な機能です。

有効にするのも簡単です。詳細は他のページに譲りますが、VLCの設定画面でWebインターフェースを有効にし、
C:\Program Files\VideoLAN\VLC\lua\http\.hosts
でアクセスを受け入れるIPアドレスのマスクを設定してやれば良いだけです。



そして適当なウェブブラウザでhttp://localhost:8080を開くとウェブからVLCを操作する画面が出てきます。もちろんLAN他のPCからIPアドレスを指定してアクセスすればリモートで操作できるわけですし、これを使いやすいようにしたAndroid/iPhoneアプリなども出回っています。

というわけは、当然C#でVLCリモートコントロールライブラリが作れるのでは?という発想になるわけです。
まあ、パソコンの場合ウェブブラウザからアクセスできるのでそんなに必要性は無いわけですが…。いやいや、自分でアプリ作りたいでしょ…作りたくない?

いろいろ調べてみましたが、意外と操作するのは簡単みたいです。
VLC HTTP requests
ここに詳しく書かれていました。HTTPのGETリクエストでコマンドを送る仕様になっているようで、非常にシンプルです。ブラウザで試してみることすらできてしましますが、例えば
http://localhost:8080/requests/status.xml?command=pl_play
にアクセスするだけでVLCで一時停止/停止していたファイルを再生することができてしまいます。
また、一方でそのstatus.xmlの本文にXMLで現在再生中のファイルのファイル名やメタ情報や再生長、再生位置やボリュームから現在フルスクリーンで表示しているかどうかまで非常に多種多様な情報が入っています。これをパースすることでシークバーなどを作れそうですね。


というわけで、ライブラリを作ってみました。

VLC Controller - nuget

やっていることはstatus.xmlとplaylist.xmlのパースと、コマンドの送信機能です。

VLCを操作する

まずは、上記のNugetからライブラリをインストールし、インスタンスを作ります。
VlcController vlc = new VlcController() {
    HostUrl = @"http://localhost:8080/",
    Password = "****",
};
VLCが動いているマシンのURLとBasic認証用のパスワードを指定し、インスタンスを形成します。
public async void PlayButtonClicked()
{
    await vlc.Play();
}
あとは超簡単で、このように必要に応じてメソッドを呼び出せば良いです。どのようなメソッドがあるかはVisualStudio等で確認してください。引数も特に困ることは無いかと思います。

あ、再生メソッドやプレイリスト編集系メソッド等に「id」っていう引数が出てくるものがあると思いますが、それに関しては、下記のプレイリストについてのところでidが説明されますので、それを入れてください。

VLCの状態を取得する

再生中のファイルの情報や、再生位置、ボリューム、ランダム再生のON/OFFなどといった、再生状態にかかわる情報はStatusプロパティ、プレイリストについてはPlaylistプロパティに保持されています。このデータは一定間隔でポーリングして取得しており、デフォルトではポーリングはON、 ポーリング間隔は1秒にしてあります。これもVlcControllerクラスのプロパティから変更可能です。
もしもVLCが起動されていないなどで接続できていない場合はIsConnectedプロパティがfalseになります。また、それと同時にStatusプロパティとPlaylistプロパティがnullになります。注意してください。

Status

Statusクラスには、上記の通り再生状態にかかわる情報が入っています。プロパティ一覧を見れば、どのような情報が返ってくるのかがわかるでしょう。

Playlist

Playlistクラスはツリー構造が取られています。VLCのプレイリストはツリー構造になっており、通常のプレイリストの他、マイビデオなどにあるメディアもプレイリストから参照できるようになっています。


左上に「マイビデオ」等が書かれていますね。 これを1度クリックすると読み込まれ、Webインターフェースを介して触ることができるようになります。

PlaylistクラスにはRootNodeプロパティがあり、ここにはNodeクラスのインスタンスが入っています。このルートノードの下に、プレイリスト、メディアライブラリ、マイビデオなどといった各項目のノードが入っております。
各階層の中に入っている再生項目はLeafクラスのインスタンスになっており、これには長さ、URI、現在再生中の項目かどうかを示すフラグなどといった情報が入っています。LeafとNodeはともにElementBase抽象クラスを親クラスとして持っており、NodeクラスのChildrenプロパティはElementBaseの配列になっていることに注意してください。

ElementBaseクラスにはIdプロパティがあり、各ノード、LeafにユニークなIdが振られています。これを一部のコントロール系メソッドの引数に渡してやることによって、任意の項目を再生したり、プレイリストから削除したりすることができるようになります。


このライブラリを使って、ガジェットのようなものを作ったり、ツールバーに常駐するようなツールを作ることで、単なるWebインターフェースよりも使い勝手のいいリモコンツールを作ることができるかもしれませんね。
もしくは、このライブラリが使えるかどうかはわかりませんが、Wi-Fi経由でVLCを操作するリモコンなどを最近流行りのラズベリーパイみたいなコンピューターで作ってみるのも手かもしれません。

夢は広がりますね!

2016年6月28日火曜日

Bloggerのhttps強化によるSyntaxHighlighter周りの修正

(2016/9/22追記)Google Driveのホスティングサービス終了により、この記事の方法では正常に動作しなくなりました。対策として、Firebaseのホスティングサービスを使用する方法があります。詳しく?はこちらへ。


どうも久しぶりです。

最近、自分のブログにアクセスしようとするとやたら重たいなということに気づき、いろいろ探ってみるとSyntaxHighlighterが怪しいということになりました。

SyntaxHighlighterはソースコードの予約語などをハイライトするツールで、多くの言語にも対応していたためこのブログでも使用していたのですが、どうやら最近そのソースコードのハイライトがされなくなっているようでした。

なぜだかわからないまま試行錯誤していたのですが、ようやく原因がわかりました。
Bringing HTTPS to all blogspot domain blogs 
どうも今年の5月の頭にblogspotドメインからありとあらゆるドメインへのhttpsリダイレクトがデフォルトでONになったようで、当ブログも例外ではありませんでした。
このブログではSyntaxHighlighter公式のホスティングサービスを用いて必要なjsファイルなどを読み込んでいましたが、そのホスティング サービスはhttpsに対応しておらず、そのリダイレクトで読み込みに時間がかかった挙句失敗するという非常に残念な状態になっていたようです。

上記のブログにはこのようなセンテンスもありました。
Please be aware that mixed content may cause some of your blog's functionality not to work in the HTTPS version. Mixed content is often caused by incompatible templates, gadgets, or post content. While we're proactively fixing most of these errors, some of them can only be fixed by you, the blog authors.
(筆者拙訳)混在コンテンツ(HTTPSとHTTPが混在したコンテンツ)はブログの一部機能が正常に動作しなくなる原因となりうる点に注意して下さい。テンプレートやガジェット、投稿コンテンツがHTTPSに対応していない場合、混在コンテンツになりえます。我々はそれらの問題の多くを積極的に修正しているところですが、一部はブログ著者のあなたでなければ修正できないものもあります。
ということで、今回の問題はその「ブログ著者じゃないと修正できない問題」に該当するようです。


問題がわかれば対処はどうにかなります。

まずは、SyntaxHighlighterをDLし、自分のGoogle Driveに保存して公開します。
そして、下記のコードをテンプレートのHTMLのヘッダー要素の一番最後にコピペします。

<!-- Begin SyntaxHighlighter-->
<link href='https://googledrive.com/host/[ID]/styles/shCore.css' rel='stylesheet' type='text/css'/> 
<link href='https://googledrive.com/host/[ID]/styles/shThemeDefault.css' rel='stylesheet' type='text/css'/> 
<script src='https://googledrive.com/host/[ID]/scripts/shCore.js' type='text/javascript'/> 
<script src='https://googledrive.com/host/[ID]/scripts/shBrushCpp.js' type='text/javascript'/> 
<script src='https://googledrive.com/host/[ID]/scripts/shBrushCSharp.js' type='text/javascript'/> 
<script src='https://googledrive.com/host/[ID]/scripts/shBrushCss.js' type='text/javascript'/> 
<script src='https://googledrive.com/host/[ID]/scripts/shBrushJava.js' type='text/javascript'/> 
<script src='https://googledrive.com/host/[ID]/scripts/shBrushJScript.js' type='text/javascript'/> 
<script src='https://googledrive.com/host/[ID]/scripts/shBrushPowerShell.js' type='text/javascript'/> 
<script src='https://googledrive.com/host/[ID]/scripts/shBrushPython.js' type='text/javascript'/> 
<script src='https://googledrive.com/host/[ID]/scripts/shBrushXml.js' type='text/javascript'/> 
<script type='text/javascript'>
window.setTimeout(function() {
    SyntaxHighlighter.config.bloggerMode = true;
    SyntaxHighlighter.defaults['toolbar'] = false;
    SyntaxHighlighter.all();
}, 20);
</script>

[ID]の部分にはGoogle Driveの共有フォルダのアクセスIDを入力します。
IDとはGoogle Driveで共有設定したときに出てくる画面のid=の文字列です。上の画像のモザイクを掛けた部分です。
もちろん、テーマや読み込むスクリプトなどは必要に応じて変更すると良いでしょう。

また、最後に
SyntaxHighlighter.defaults['toolbar'] = false;
の一文を設けていますが、これを置くことでソースコード画面の右上の「?」マークが消えます。最初は消さなくてもいいかな~って思っていましたが、1行目に横長のコードを書くと鬱陶しくて仕方ないんですよね、これ。


というわけで、無事SyntaxHighlighterの問題を解決し、ブログの読み込みも早くなりましたとさ。めでたしめでたし。

P.S. ついでにブログのデザインも変えてみました。自分で目がチカチカするな~とか思ったらまた別のデザインにするかもしれません。

2016年2月9日火曜日

expressionとstatement

たまにはプログラミング言語についての話をしてみようかと思います。

最近、Twitterで中括弧戦争みたいなのが起きているらしいです。ぶっちゃけ、プログラミング言語なんてコンパイルできれば正義ですし、なおかつ保守性が高いコードを書けるのならば二重丸です。中括弧を付ける場合も付けない場合も、どこかに改行を入れる場合も、それが見やすいと思えるのならばそれで良いでしょう。実際、その辺はコーディング規約とかコーディングスタイルと言った言葉で、各宗派がある程度出来上がっていますし、たいてい、この手の宗派は入門に使った書籍に準じてしまうでしょうから、一緒のプロジェクトをやっているのでもなければ過干渉する必要はないと私は思っています。

しかし、これを機にググってみると中括弧に関する理解の甘い記述が見受けられます。プログラミング言語は自然言語ではないので、「言語ありきで、それについて文法などの説明を後付けする言語」ではなく、「文法ありきでそれをもとに記述する言語」です。
と言うわけで、私のC言語のこのあたりに関する理解について、ここにメモしておこうと思います。

中括弧の省略記法ではない

まず、if文などの制御文についてです。
if(hoge) {
    foo();
}

C言語ではおよそこのような書き方をします。ちなみに、中身が一文のみの場合、中括弧は省略できるとの記述があるものがしばしば見られます。
if(hoge)
    foo();

結果論としては正しいというか、そう見えます。しかし、中括弧が存在するのが原則で、省略記法としてその中身が一文だけなら中括弧が省略できるという理解なのならば、それは不十分です。


この手のプログラミング言語には「式(expression)」と「文(statement)」の概念を持っています。

まず、式とは
  • 値の計算を指定する
  • オブジェクトもしくは関数を指し示す
  • 副作用を引き起こす
の3種類の動作、またはその組み合わせを行うもののことを言います。副作用(side effect)という言葉は関数型言語マン以外には馴染みの薄い表現で、かつマイナスのイメージを持つ言葉かもしれませんが、ここでは「実行環境の状態を変化させる」程度の意味しか持ちません。例えば、変数の値を変更する(ことによって実行環境のメモリの値を一部書き換える)などといったことです。
式は評価した値を持ちます。

一方で、文とは実行すべき動作を規定したものです。JIS規格では
  • ラベル付き文
  • 複合文
  • 式文
  • 選択文
  • 繰返し文
  • 分岐文
の6種類が文として定義されています。詳しくは割愛しますが、ラベル付き文、式文、分岐文はセミコロンで終わる文法になっていて、選択文や繰返し文はいわゆるif文やwhile文など(分岐や繰り返しの終了地点まで含んだもの)で、複合文は中括弧で囲まれたブロックとなっています。式文はその名の通り式が入る文で、ここに空文も含まれています。分岐文はbreakとかcontinueとかreturnとかですね。


これらを踏まえた上で、if文の構文がどう定義されているかと言うと、
if ( 式 ) 文
です。 いたってシンプルです。

ここから導き出されるのは、中括弧無しは一文の場合の省略記法ではなく、式文で書くか複合文で書くかの違いに過ぎないということです。並列していくつもある「文」のうち1つを書けるという話なので、「原則が中括弧」みたいな発想はその人の固定観念ということになってしまいますね。


カンマ演算子

複合文は0個以上のをまとめて1つのとして扱うものですが、C言語には複数のを1つのとしてまとめる「カンマ演算子」と言うものがあったりします。ほぼほぼ出番が無い演算子ですので、知らない人も多いかもしれません。

カンマ演算子は次のような構文を持ちます。
式, 式, 式, ... , 式
2個以上の式をカンマでつなげるだけです。なお、この式の評価した値は最後の式(一番右側の式)となります。
int i, j, k;

k = (i = 0, j = 1); 
printf("i = %d, j = %d, k = %d", i, j, k);

このコードはコンパイルでき、実行すると
i = 0, j = 1, k = 1
という出力が得られます。 これを上手く使うことで、複合文のような記述を式文を使って作ることができます。
if(hoge)
    puts("hoge"),
    puts("foo");
else
    puts("bar");

ifの1つ下の行のputsは、末尾がセミコロンではなくカンマになっています。すなわち、2行目と3行目の2つのputsを合わせて1つの式にしているわけです。そのため、ifの直下の文は1つの式文になるため、これはコンパイルが通ります。もしもifに与えた条件式hogeが真ならば、hogeとfooが両方とも画面に出力されます。

この記法は中括弧で複数文をくくるより果てしなく見にくいです。と言うより、カンマかセミコロンかなんてパッと見で区別できる人なんてなかなかいません。そのような観点から、使われることはまずないと思われますが、たまに関数マクロなんかで上手く使えば問題の起きにくいマクロが作れたりしてそのような場に出番があると言えるでしょう。

else if文は存在しない

さて、C言語の選択文の項目には、実際には3種類の構文が規定されています。
  • if ( 式 ) 文
  • if ( 式 ) 文 else 文
  • switch ( 式 ) 文
見出しでネタバレしちゃっていますが、そうです、C言語に「else if文」と言うのは存在しません。しかしよく「else if」と記述する人はいますし、実際それでもコンパイルは通ります。
実はこれ、if文全体が1つの文と見なされることを利用し、else以下の文としてif文を置いただけということになります。すなわち、else ifはこのように書くのと同値です。
if(hoge) {
    foo();
} else {
    if(hogehoge) {
        foobar();
    } else {
        fooooo();
    }
}

一般的にこのような記法は無駄にインデントが増えるだけで好まれません。「if文の条件が成り立たなかったときに次の条件が成り立つかを判定する」という概念を1つ頭の中に入れるだけでelse ifという記述は読みやすくなりますから、その記法に反対する人はいないでしょう。しかし、言語仕様ではあくまでも「else以下の文をif文とした記法」となっているわけです。

それを踏まえると、else whileやelse switchなどの記法も同様に文法上書けることになってしまいますし、
if(hoge)
    while(hogehoge) {
        foobar();
        fooooo();
    }

といった記法も許されます。
読みやすいかどうかは別として、どこまでが文で、どこまでが式かと言うことをしっかりと理解しておけば、プログラムを見たときの脳内コンパイルは捗るでしょう。

C#のusing文

C#におけるusing文は、上記のif文同様に
using ( リソース取得式 ) 文
という構文になっています。もちろんusing文自体も文の1つにカウントされるため、using以下にusingを書くことができます。
using(StreamWriter sw = new StreamWriter("dst.txt"))
using(StreamReader sr = new StreamReader("src.txt"))
{
    while((line = sr.ReadLine()) != null)
        sw.WriteLine(line);
}

このように書くことによって、複数のIDisposableインスタンスを作って最後にすべて破棄するという動作を見た目上やっているようになります。本来ならば2つ目のStreamReaderのところはインデントを1段下げるべきなのでしょうが、特に並列的に書いて問題ないところですし、逆に下げると見にくくなるので敢えて下げていません。
下手したら、「usingは並列にいくつも書ける」みたいな説明をしだすところもあるかもしれませんが、特段文法上そういった定めをしているわけではなく、これはC言語系統の文法の体裁として、むしろ一般的な記述であるわけです。

まとめと私見

ここまで読んで、C言語系の言語のソースコードを見る目が変わった方もいるかもしれません。今まではなんとなく書いていたけど、今となればどれが文でどれが式かを意識しながら書けるようになっているかもしれません。

そもそも、プログラミング言語は、0と1のみで表されるコンピューターへの命令を人間にわかりやすい方法で書けるようにしようとして発展してきたものです。今は高度に抽象化され、さらに様々な記法やパラダイムで多種多様な形態を持っていますが、最終的に動作するコンピューターの仕組みは原則として同じです。そして、モダンなプログラミング言語はそのコンピューターの仕組みを知らなくてもできるだけ簡単に書けるように敷居を下げた一方で、やはりハイレベルなプログラミングにはどうしてもそういった知識は付いて回ります。
おそらくプログラミング言語に関してもそれは同じで、一般的な書き方、一般に可読性が良く保守性が高いと言われる記法はありますが、それより一歩進んでコンパイラーがどのように構文を解析し理解するかを頭に置いてプログラムを書くのはとても大事なことだと言えるでしょう。

私は、たまたまC言語を始めるにあたって巡り合った入門書に比較的そういった記述が多く、このような考え方を非常に強く植えつけられながらC言語を理解していきました。そのため、if以下の文などに中括弧を付けるかどうかの議論で「省略したほうがスマート」とか「省略したら可読性が下がる」みたいな意見を見るたびに「そもそも省略じゃないよねそれ」という気持ちになってしまいます。


私は、ifの下を必ず複合文にするかと言うと、そうではありません。むしろ、1つしか式を書かない場合は積極的に式文を使います。
「if文の中身を複数文にしたくなった時に中括弧を付け忘れたらバグになるじゃないか」という意見をたまに見ますが、私はそう思いません。インデントはIDEがちゃんと整理してくれますから、インデントだけ付けて中括弧を付け忘れる状態に気づかないということもまず無いですし、そもそも変更箇所の前後の中括弧が目に留まらないということもまずありません。むしろ、中括弧で1行を使ってしまうとプログラムの縦方向の密度が無駄に下がり、可読性が下がると考えています。
ただし、ifの直下にwhile文だけが入る場合など、一文でも複数行にまたがる場合は基本的に中括弧を入れています。すなわち、ほぼほぼ行数で決めているといったところでしょうか。

一方で「if(式)」とその「中身の文」の間に改行を入れない記法はまずしません。
if(input < 0)  return ERROR;

このような記法は、まあこれくらいなら良いのかもしれませんが、条件式が少しでも長くなると可読性が一気に下がるので私は好みません。たとえ空文でも次の行を1行使います。


結局、この辺りは好みだと思います。自分がC言語を習得したときの入門書、もしくはこれまでに使っていたプログラミング言語の風習、自分の感性、そういったものが合わさって「自分はこう書く」みたいな話が出てくるものでしょう。

大事なのは、言語の仕様を理解し、それに即し、また可読性と保守性が良くなるように自分で工夫を重ねることだと思います。そういったところから、技術力の高さをアピールしていきたいですね。

2016年1月22日金曜日

ポケモン「サファイア」の時計を復活させる

さて、巷ではもっぱらリメイク版である「アルファサファイア」「オメガルビー」が話題ですが(とは言っても発売は1年以上前ですが)、ここではリメイクされる前の「サファイア」を対象にしています。
検証はしていませんが、「ルビー」「エメラルド」も同様に復活させられる可能性もありますし、また、サファイアでもロットが違う製品だと違うチップが載っていて同様にいかない可能性もあります。

流石にゲームボーイアドバンスはもうレトロゲーム認定してもいいのではないかと思います。とても懐かしいですね。
久しぶりにサファイアをプレイしたくなったので、起動してみました。


おお、時計が電池切れだと。
電池切れで時計が動かなくなったから時間絡みのイベントは起きなくなるけど、それ以外のイベントは正常に動くからプレイ可能だよとのメッセージが出てきました。
ゲームボーイカラーまではセーブデータがRAM上に保存されていて、バックアップ用電池が切れるとセーブデータも吹き飛んでいましたが、ゲームボーイアドバンスからはセーブデータ自体は不揮発性メモリに書き込まれるようになったようです。しかし、時計管理はあくまでもカートリッジの責務なんですね。 DSからは時計もゲーム機本体のほうの責務になったみたいですが。

まあとりあえず、ふむふむと思いながらプレイを開始してみましたが、案外時間関係のイベントが多いことに気が付きます。
例えば、
  • きのみが育たなくなる
  • 毎日きのみをくれる人がくれなくなる
  • レコードを混ぜると出現する友達の秘密基地で、1日1回のバトルができなくなる
  • ミナモデパートで1日1回のくじが引けなくなる
  • 1週間に1回わざマシンをくれる人がくれなくなる
  • あさせのほらあなであさせのかいがらやあさせのしおが採れなくなる
  • カイナシティのがんばりやさんが二度と頑張らなくなる
  • マボロシじまが出現しなくなる(時計が動いていても出現したの見たことありませんが)
などなど、ゲームの楽しみが一気に消え去ります。こんなポケモンで一体何をしろと言うのだ!ひたすら殿堂入りか?殿堂入りをすればいいのか!?


電池交換は、実はまだゲームボーイアドバンスのソフトならば任天堂がサポートしてくれています(※注)

バックアップ電池の交換について

ちゃんと正常にゲームをプレイしたい人はまずこっちに頼んだほうが手っ取り早く確実で安いです。ですが、こういうのを分解して自分で修理するのを楽しみたい私は、自分で修理に挑戦してみることにしました。
※注:記事投稿時点。現在はバックアップ電池の交換サービスは終了しているようです。

なお、当たり前ですが、自分で分解、修理をするとその製品について二度と任天堂のサポートを受けられなくなる可能性があります。また、ゲームバランスを大幅に崩すようなゲームの改造も民事訴訟の対象になる可能性が無いことはないようです。もちろん、この記事を見て改造をして失敗した人とかがいたとしても私は責任を負えませんので、自己責任でお願いいたします。
今回の私の記事は、純粋に修理をするためというのと、あとは、まあゲームボーイアドバンスはもう枯れ果てたゲーム市場ですから、メーカーに目くじらを立てられるようなことも無いでしょうということで書くことにしました。


分解、そして電池交換

まずはカートリッジを分解します。分解はY字の特殊ドライバーを使いますが、ゲームボーイカラーまでのカートリッジに比べたら格段と入手性の高い汎用特殊ドライバーで何とかなります。
ねじを外したら、筐体を少しスライドするとロックが外れてパカッと開きます。


こんな感じになります。
ボタン電池が付いていますね。これが切れた電池で、CR1616のタブ付き電池になっています。

まずはタブの根元をはんだごてで温めて古い電池を外します。


CR1616はリチウム電池なので公称3Vですが、もうすでに0.15Vまで下がっていました。完全放電状態です。

ネットをいろいろ探すと、ここまではたどり着いている方も多く、多くの紹介記事では、このタブと電池の接合部を剥がして、新しい電池をテープ等で固定している方が多いように見受けられました。

し かし、それではタブの金属疲労の問題は元より、電池との接触も悪くなることが多いのではないかと思い、私は電池に直接はんだ付けすることにしました。電池は精巧に作られた化学製品で、電池をはんだごてで熱すると中の電解液が気化するなどの化学的変化を起こし、最悪、電池が爆発することがあるので非常に危険です。なので、良い子は真似しないようにしましょう((((((

新しい電池を百円ショップで買ってきて取り付けた画像がこちらです。


マイナス電極側が上になるように取り付けていますが、プラス電極が横に回り込んできているので、接触してショートさせることがないように配線を一部ビニールで覆っています。

これで起動すると、冒頭に出てきていた電池切れのメッセージが出なくなりました。
よしこれで時間関係のイベントが復活だ!よっしゃああああああ!!!!!

まだ時間関係のイベントが発生しない!

起動時の電池切れメッセージこそ出なくなったものの、きのみを植えて1日程度待っても芽が生えてきません。きのみをくれる人もきのみをくれません。おかしいです。電池が復活しても時間関係のイベントが発生しないのです。なぜだ、なぜなのだ!

ヒントは、キナギタウンにいる、1週間に1回わざマシンをくれる人のところにありました。


あと2105日!?およそ5年と9か月です。
正常ならば、ここで「あと6にち」など、1週間以内の日数を案内してくれます。

ここで僕は気が付きました。
ゲームの内部では、次にわざマシンをあげられるようになる日時を記録していたとしたらどうなるでしょう。現在の日時との差分を取って、その日数を「あと○にちたてば~」と答えることになるはずです。
現在の日時はというと、先ほど交換した電池で駆動しているRTCが管理しています。電池を交換したのでRTCのカウントはリセットされ、0日目になったはずです。

そ れ だ

すなわち、ゲームプレイ開始から2098日目に、次にわざマシンがあげられるようになる日を2105日目と記録していたため、電池を交換後でもお構いなしにRTCの日数カウンタがその日になるのを待つ必要が生まれた、ということになります。電池切れなんてそうそう起こることでもなければ、ユーザー自身での修理を想定していたわけでもないので、このような適当な実装になっているのでしょうね。
おそらく、1日に1回起きる系のイベントも同様に記録されていて正常に発生しないということなのでしょう。

このことからも、GBAソフトをメーカーに送って電池交換をしてもらった場合は、電池交換後にRTCのカウンタをずらすか、専用のツールでセーブデータを書き換えてイベントが正常に発生するようにしているものと思われます。
今回は自分で交換したためそのような書き換えができませんので、2105日待つ必要があります。

えっ

ネットで調べたところ、同じような症状になっている人もちらほら見かけました。
そして、ルビー・サファイアではRTCが1年を刻むときのみが育たなくなるバグがあったのですが、その修正プログラムを使えば1度に限ってこの状態を修正できるとの情報がありました。しかし、すでにこのパッチは当てており、リーフグリーン・ファイアレッドからのファームウェア更新を利用して修正することはできませんでした。

つまり、いよいよもって本当に2105日待つ必要があるということになります。

待てるかい!

そして、RTCのハードウェアハックへ

さて、ここまで自分でいろいろやってしまって引くに引けなくなった僕が次に考えたことは、「RTCの値を上書きして2105日分進めればいいんじゃね?」ということでした。
ネットでは電池交換まではしたけど同様に時間関係のイベントが発生しない状態になっている人までは見かけましたが、さすがにRTCをいじろうとしている人までは見かけませんでした。
ですが、カートリッジの基板を見ると、ICチップの脇に丁寧に「1M/512K FLASH U2」とか「U3 RTC」などとICの説明が書いてあります。そうです、どれがRTCかも明確です。もっと言えば、脇に水晶発振子があることからも明白でした。そこで、このRTCの仕様さえわかればRTCの書き換えもできるのではないか、という発想で型番をググってみました。

S-3511A
2018年2月28日追記:リンク切れになっております。通販サイトのタイロテックのこちらからダウンロード可能です。

見事にヒットしました。当時はセイコーインスツルメンツでしたが、今は半導体部門は子会社化されてエスアイアイ・セミコンダクタになっているみたいですね。まあ何にせよSEIKOです。SEIKOが供給していたRTCモジュールが僕のサファイアには搭載されているみたいです。すでにディスコンになっているようですが、データシートは問題なく読めます。よっしゃああああああ!!!!!

まずはプロトコルの確認です。まあRTCだとI2Cかなと思って眺めましたが、ACKとかスレーブアドレスとか無さそうだな。うんうん、SPIかな…いやデータ線が1本しか無いぞこれは…

独自規格だー┗(^o^)┛WwwwWWww┏(^o^)┓ドコドコドコドコwwwwwwwww

なんてこった。独自規格で通信プロトコルを定義しています。
なので、書き換えをする場合はI2CやSPIなどマイコンに載っているハードウェアを使わずに、このプロトコルに則ったIOの制御を自前でしてや らなければなりません。


また、当たり前ですが、データを書き換えるためにはICに何かしら電気的に接続する必要があります。そして、バックアップ用電池で動作している状態(=通電した状態)で書き換えをしたうえで接続を外し、元のカートリッジの形に戻してあげなければなりません。はんだ付けとかでやるのはちょっとリスクが高すぎますよね(実は自分もやってみたりしたのですが、結構スリルがありました)

そこで登場するのがICクリップです。
ICテストクリップ(SOP-8) [TCLIP-SOP8]
これは、基板に実装されたICを挟むことで電気的に接続し、外部に配線を回してくれるツールです。ちょっと値段は高めですが、通電中に書き換えるのならば通電中はんだ付けよりもこういうツールを使ったほうが安心でしょう。


実際に買ってきてRTCを挟んだものがこれです。こんな感じで簡単に接続できます。これはいいです。

マイコンは何でもいいのですが、まあ、安さと小ささ、そしてプログラムメモリの容量からPIC16F1705を選びました。
PIC16F1705
PIC16F1705は特にアナログ機能が強化されたマイコンですが、残念ながら今回はその機能は使いません。

適当に表示用のI2C液晶と操作ボタンを付けた回路を構成し、完成です。


めんどくさいので電源はCR2032を使いました。液晶バックライトを使ったらそんなに持たないでしょうが、まあ長時間運用するものでもないですし大丈夫でしょう。

I2C液晶は適当に制御プログラムを移植してきて動作を確認します。今回aitendoで買ってきた液晶は、液晶の表示向きを反転させるピンが出ていて、それに気づかずにだいぶ苦労しました。適当にそのピンをGNDかVDDに接続することで向きを固定できます。

さて、本題のRTC通信プログラムに入っていきましょう。
タイミングチャートとにらめっこしながらプログラムを書きます。各パルス幅は最低0.1us程度なので、まあゆとりをもって1usくらい取ってあげることにします。

#define sequenceDelay() __delay_us(1);

void rtc_Init()
{
    RTC_CS = 0;
    RTC_SCK = 1;
    RTC_SIO = 0;
    TRIS_RTC_CS = 0;
    TRIS_RTC_SCK = 0;
    TRIS_RTC_SIO = 1;
}

void rtc_WriteBit(bool_t data)
{
    RTC_SCK = 0;
    sequenceDelay();
    RTC_SIO = data ? 1 : 0;  
    sequenceDelay();
    RTC_SCK = 1;
    sequenceDelay();
}

void rtc_WriteByte(uint8_t data)
{
    for(uint8_t i = 0; i < 8; i++) {
        rtc_WriteBit(data & 0x01);  //Send from LSB to MSB
        data >>= 1;
    }
}

void rtc_WriteSequence(uint8_t command, const uint8_t *pData, uint8_t length)
{
    RTC_SCK = 1;
    RTC_SIO = 0;
    
    sequenceDelay();
    RTC_CS = 1;
    sequenceDelay();
    TRIS_RTC_SIO = 0;
    rtc_WriteByte(command);
    for(uint8_t i = 0; i < length; i++)
        rtc_WriteByte(pData[i]);
    RTC_CS = 0;
    sequenceDelay();    
    TRIS_RTC_SIO = 1;
}

bool_t rtc_ReadBit()
{
    bool_t ret;
    
    RTC_SCK = 0;
    sequenceDelay();
    RTC_SCK = 1;
    sequenceDelay();
    ret = PORT_RTC_SIO;
    
    return ret;
}

uint8_t rtc_ReadByte()
{
    uint8_t data = 0;
    for(uint8_t i = 0; i < 8; i++) {
        data >>= 1;
        if(rtc_ReadBit())  //Read from LSB to MSB
            data |= 0x80;
    }
    return data;
}

void rtc_ReadSequence(uint8_t command, uint8_t *pData, uint8_t length)
{    
    RTC_SCK = 1;
    RTC_SIO = 0;
    
    sequenceDelay();
    RTC_CS = 1;
    sequenceDelay();    
    TRIS_RTC_SIO = 0;
    rtc_WriteByte(command);    
    TRIS_RTC_SIO = 1;
    sequenceDelay();
    for(uint8_t i = 0; i < length; i++)
        pData[i] = rtc_ReadByte();
    RTC_CS = 0;
}

ビットを読み書きする関数し、それをバイトデータを読み書きする関数から呼び出します。

書き込みシーケンスと読み込みシーケンスは、まずはそのデータ内容を示すコマンドを送ってからデータの読み書きをしますので、そういったことをやりやすくした関数を用意します。ここでとても重要なのが、データはLSBからMSBに向かって送りますが、コマンドはMSBからLSBに向かって送る点です。なんでこんなクソ仕様にしたのかわかりませんが、コマンドとデータで送る順序が違うので、ここでは、「コマンドの値をデータシートに書いてある順序と逆順にしておく」という方法で対応し、プログラム自体はすべてLSBからMSBに向かって送るようにしました。

実際のコマンドは全部で7種類あり、リセット以外の6種類は読み書き両方ができるため、実質13種類あります。
ですが、RTCを制御するうえでは、年データからの時刻データの読み書きと、ステータスレジスタの読み書きと、リセットコマンドの送信さえできれば問題ないので、その分だけ実装しました。

#define RTC_COMMAND_FULL_READ       0b10100110
#define RTC_COMMAND_FULL_WRITE      0b00100110
#define RTC_COMMAND_STATUS_READ     0b11000110
#define RTC_COMMAND_STATUS_WRITE    0b01000110
#define RTC_COMMAND_RESET           0b10000110

#define RTC_DATETIME_LENGTH 7


typedef union _tagRTC_STATUS {
    uint8_t value;
    struct _tagBits {
        unsigned bit0  : 1;
        unsigned INTFE : 1;
        unsigned bit2  : 1;
        unsigned INTME : 1;
        unsigned bit4  : 1;
        unsigned INTAE : 1;
        unsigned Is24  : 1;
        unsigned POWER : 1;
    } bits;
} RTC_STATUS;



void rtc_WriteDateTime(const DATE_TIME *pdt, bool_t Is24)
{
    uint8_t data[RTC_DATETIME_LENGTH];
    bool_t IsPM = pdt->hour >= 12;
    uint8_t hour = pdt->hour;
    
    if(IsPM && !Is24)
        hour -= 12;    

    data[0] = ByteToBCD(pdt->year);
    data[1] = ByteToBCD(pdt->month);
    data[2] = ByteToBCD(pdt->day);
    data[3] = pdt->weekday;
    data[4] = ByteToBCD(hour) | (IsPM ? 0x80 : 0x00);
    data[5] = ByteToBCD(pdt->min);
    data[6] = ByteToBCD(pdt->sec);
        
    rtc_WriteSequence(RTC_COMMAND_FULL_WRITE, data, RTC_DATETIME_LENGTH);
}

void rtc_ReadDateTime(DATE_TIME *pdt)
{
    uint8_t data[RTC_DATETIME_LENGTH];

    rtc_ReadSequence(RTC_COMMAND_FULL_READ, data, RTC_DATETIME_LENGTH);

    bool_t isPM = data[4] & 0x80;
    
    pdt->year    = BCDToByte(data[0]);
    pdt->month   = BCDToByte(data[1]);
    pdt->day     = BCDToByte(data[2]);
    pdt->weekday = data[3];
    pdt->hour    = BCDToByte(data[4] & 0x3F);
    pdt->min     = BCDToByte(data[5]);
    pdt->sec     = BCDToByte(data[6] & 0x7F);
    pdt->msec    = 0;

    if(isPM && pdt->hour < 12)
        pdt->hour += 12;
}

void rtc_WriteStatus(RTC_STATUS status)
{
    rtc_WriteSequence(RTC_COMMAND_STATUS_WRITE, &status.value, 1);
}

RTC_STATUS rtc_ReadStatus()
{
    RTC_STATUS status;
    rtc_ReadSequence(RTC_COMMAND_STATUS_READ, &status.value, 1);
    return status;
}

void rtc_Reset()
{
    rtc_WriteSequence(RTC_COMMAND_RESET, NULL, 0);
}

こんな感じになりました。

このRTCモジュールは12時間表示モードと24時間表示モードがあり、時刻データの読み書きをするときは現在どっちのモードなのかを意識する必要があります。時刻データには現在が午後か午前かを表すフラグがあるので、データ読み込み後にはそのフラグを見てデータを処理してからBCDをbyteに変換しなければいけませんし、書き込み前にはSTATUSレジスタを読み込んで現在のモードに合わせて12時間表示化してデータを送信する必要があります。

あとは、これをいい感じに制御するインターフェースを作れば完成です。
PICのタイマー2はコンパレーターとセットで動いていて、コンパレーターで指定した値に達したら割り込みを掛けるとともに自動でタイマーをゼロにリセットしてくれます。なので、時計用途でとても使いやすいです(割り込みルーチン内でタイマー値を再設定する必要が無いため)。
時計と時計合わせ機能、そして上記の5つのコマンドの送受信機能を実装すれば、あとはRTCの時刻データを読み込み、修正し、書き込むことができるようになります。これで、ゲームの時間を今回は2105日分+α早送りしてやれば見事ゲームが正常な状態になります。


ちなみにですが、このRTCは電池を交換した後にSTATUSレジスタの読み込みと、適宜リセットコマンドの送信をしてやらなければなりません。ですが、その機能はGBAカートリッジ側でゲーム起動時に行ってくれるようなので、時刻データを読み書きする分には特に気を使う必要はなさそうです。

あと、RTCに設定する時刻は、リアルの時刻ではありません。まず、年月日データが大きな意味を持っていないことは誰にでも想像がつくでしょう(製造時に時計合わせしているだなんて考えにくいですからね)。時刻データは、ゲーム初期化時に聞かれる時刻設定がそのままオフセットになっているのか、私の環境ではRTCが0時00分00秒を示しているときにゲーム内では夕方5時過ぎになっているようでした。なので、その分を意識して時計合わせをしなければなりません。

まあ、なんにせよ、何回か「ゲームを起動して時刻を確認してから終了し、値を更新する」という作業を繰り返さなければならないことは間違いありません。少し面倒ですが、これで完全に時計を合わせられるようになります。


書き換え風景です。
このICクリップですが、やはり足元での接触が悪いことがあり、その場合はRTCのデータが上手く読み書きできなかったり、最悪RTCの中身が吹き飛びます。ですがまあ、根気よくやっていれば大丈夫でしょう…。

時刻データを書き換えるということ

ここまで読んで気づいた方もいるかもしれませんが、今回の記事は「ゲームボーイアドバンス用ソフトの時計をずらす」ということに帰着します。今回やったのは修理のための行為ですが、チートに転用しうることも容易に想像がつくでしょう。
今回はRTCにアクセスし時刻を動かすツールですから、ゲームプログラムのROMの読み書きはできません。ROMの読み書きは特に海賊版のデータが出回る可能性を大いに含みますから、著作権の観点からも、利益の観点からも、メーカーも総力を挙げてそういったツールの撲滅にかかるようです。
ですが、今回はせいぜい時計をずらす程度のチートツールとしてしか使えません。著作権の侵害にあたる可能性はまず無く、せいぜい著作人格権で問題になることがあるかもしれないという程度でしょう(チートツールは作者の意図したゲーム性を破壊することで著作人格権の侵害とみなされることがあるらしいです)。しかし、ポケモンは時刻をずらしたところでそんなにゲーム性を著しく欠くほどのチートにはならないでしょうし、上にも書いた通り、ゲームボーイアドバンスはもう何世代も前のゲーム機で市場は枯れ果てていますから、メーカーも目くじらを立てて怒ってくることも無いと思っています。
というわけで、現在私はこの記事に重大な問題があるとは認識しておりません。関係者の方で、万が一何か問題があるというのならばぜひご一報ください。

改造、修理も男のロマンですからね。怒られない程度の範囲内ならばやって楽しいでしょう。というわけで、私は自分のやったことを公開することにしました。

この記事を参考に改造する方がいるのならば、くれぐれも分をわきまえて行うようにしてくださいね。


さて、いかがだったでしょうか。

ゲームソフトにハード的な寿命がある最後の世代だと言えるゲームボーイアドバンス、その過渡期だからこそ発生した「セーブデータと時計データの錯誤」が生み出した問題ですが、何とか自力で解決することができました。
組み込み制御の知識がある程度あって、かつこの時代のソフトを復活させたいのならば、ぜひやってみてはいかがでしょうか。

あ、あくまでも自己責任でお願いしますよ。

2016年1月14日木曜日

PICマイコンでBCD変換

さて、今回は車輪の再発明というか、別に世の中的に新しいことはありませんが、BCD変換アルゴリズムについて知ったことがあったので備忘録的にまとめておこうと思います。

二進化十進表現(BCD)は、何かと組み込みプログラミングに付いて回ります。値を液晶に表示するときなんかは、内部的にBCD変換をすることになるはずです(C言語ならばprintfとかを使えばプログラマーが意識することはなくなるかもしれませんが)。
他にも、RTCなんかはカレンダー機能を内蔵していて、データのやり取りにはBCD形式を用いているものも結構ありますし、PIC24Fシリーズが持っているRTCCもBCDです。

さて、このBCDですが、このように組み込みプログラミング上で使おうとすると、マイコン内部の値、すなわちただの2進数との変換が必要になってきます。私の中ではこれは案外コストがかかるものであるという認識がありました。
というのも、BCD変換は要するに10進数の桁の抽出ですから、変数を10で除したり、その剰余を計算する必要があります。マイコンのような貧弱なCPUでは除算命令を持っていないことが多く、内部的にはループ等で除算が実装されていると思われます。また、C言語では除算演算子と剰余演算子が別個なので、同じ除数、被除数の場合はループならば本来は同時に商と剰余が産出されますが、C言語で記述する場合は、コンパイラが最適化してくれない限り、商と剰余で2回除算が必要になります。
もちろん自分でループを実装すればその手間は省けますが、それでもあまりスマートな気はしませんよね。

というわけで、こういうのって絶対すごいアルゴリズムあるよね~ってググっていたら見つけました。
BCD変換ルーチンについて
2020/5/4追記: 上記サイトリンク切れにつき、当ブログにてアルゴリズムを詳しく紹介する記事を書きました。アルゴリズムについてはこちらを参照ください。)
1度読んだだけではどういう意味かよく分かりませんでしたが(ついでに<SUB>タグなどが上手く仕事していなくてとても読みにくいですが)、何度か読むと意味がわかってきました。シフト演算で値をコピーするときにBCD変換しようという発想で、基本的にはコピー先の各桁が10以上になったら6を足して繰り上げしてあげるってことみたいです。ただ、シフト演算は要するに2倍するということですので、2倍後の各桁は最大で18になりえます。ですが、それは4ビット値の最大値(15)を超えて不都合なので、シフト演算で2倍する前に5以上かを判別し、繰り上げ処理(2倍する前なので6の半分の3を足す)をやってあげるということのようです。これによって、BCD変換前の値のビット数だけのループ回数と、あとはシフト演算や簡単な比較、足し算だけでBCD変換ができてしまいます。なんと低コストなのでしょう。PICにはディジットキャリービットがありますから、下位4bitが桁あふれをしたかを判別することができるので、4bitごとの演算はとてもやりやすく、さらに計算の低コスト化ができるでしょう。コンパイラの頭の良さを試しどころです。

というわけで、PICを意識したC言語でこのアルゴリズムを実装してみました。
typedef union {
    uint24_t value;
    struct _tagField {
        unsigned binary  : 8;
        unsigned BCD_1   : 4;
        unsigned BCD_10  : 4;
        unsigned BCD_100 : 4;
    } field;
} BYTE_BCD_CONVERTER_UNION;

uint16_t ByteToBCD(uint8_t val)
{
    BYTE_BCD_CONVERTER_UNION conv;
    conv.value = 0;
    
    conv.field.binary = val;
    
    for(uint8_t i = 0; i < 8; i++) {
        if(conv.field.BCD_1 >= 5)
            conv.field.BCD_1 += 3;
        if(conv.field.BCD_10 >= 5)
            conv.field.BCD_10 += 3;
        if(conv.field.BCD_100 >= 5)
            conv.field.BCD_100 += 3;
        conv.value <<= 1;
    }
    
    return (uint16_t)(conv.value >> 8);
}
8bitの値(byte)から3桁のBCD値を作り出す関数です。変にシフト演算とかを使いまくって「桁の値が5以上だったら」などという処理をするよりか、うまいことビットフィールド構造体を使うことで、コンパイラができる限り低コストなコードを生成してくれます。と同時にリトルエンディアンの処理系依存になってしまいますが、まあ、大体身の回りはリトルエンディアンなので良いでしょう(ヘラヘラ
ためしに、BCDの1の位と10の位が5以上かを見て処理する部分の逆アセンブル結果を見てみましょう。ちなみに、コンパイラはXC8 v1.35の無料版で、ターゲットはPIC16F1823です。
22:                    if(conv.field.BCD_1 >= 5)
0279  087A     MOVF i, W
027A  390F     ANDLW 0xF
027B  00F4     MOVWF multiplicand
027C  3005     MOVLW 0x5
027D  0274     SUBWF multiplicand, W
027E  1C03     BTFSS STATUS, 0x0
027F  2A8A     GOTO 0x28A
23:                        conv.field.BCD_1 += 3;
0280  087A     MOVF i, W
0281  390F     ANDLW 0xF
0282  00F4     MOVWF multiplicand
0283  3003     MOVLW 0x3
0284  07F4     ADDWF multiplicand, F
0285  087A     MOVF i, W
0286  0674     XORWF multiplicand, W
0287  39F0     ANDLW 0xF0
0288  0674     XORWF multiplicand, W
0289  00FA     MOVWF i
24:                    if(conv.field.BCD_10 >= 5)
028A  0E7A     SWAPF i, W
028B  390F     ANDLW 0xF
028C  00F4     MOVWF multiplicand
028D  3005     MOVLW 0x5
028E  0274     SUBWF multiplicand, W
028F  1C03     BTFSS STATUS, 0x0
0290  2A9C     GOTO 0x29C
25:                        conv.field.BCD_10 += 3;
0291  0E7A     SWAPF i, W
0292  390F     ANDLW 0xF
0293  00F4     MOVWF multiplicand
0294  3003     MOVLW 0x3
0295  07F4     ADDWF multiplicand, F
0296  0EF4     SWAPF multiplicand, F
0297  087A     MOVF i, W
0298  0674     XORWF multiplicand, W
0299  390F     ANDLW 0xF
029A  0674     XORWF multiplicand, W
029B  00FA     MOVWF i
こんな感じになっています。1の位側はそのままANDで0x0Fでマスクしてから比較処理をし、10の位側はSWAPFで上4bitを下と入れ替えて、から同様にマスクして処理をしています。シフト演算ではなくSWAPFを使って4bit一気に動かしたという処理は、まあ及第点でしょう。
実際、自分でアセンブリで書くのならば、1の位はそのままマスクせずに0x05を引いてからSTATUSレジスタのディジットボロービットを見ることになりそうですし、0x03を足してもその4bit値としては桁あふれは絶対起こさないということはわかっているので、XOR→AND→XORなんていう回りくどい処理は省くでしょう。また、10の位はSWAPFなんてせずに、0x50を引いて上位4bitが5以上かを見ればよいですし、ここで足し算をするときもSWAP→マスク→3を足す→SWAP→XOR→AND→XORなんていう回りくどいことをせず、0x30を足せば良いでしょう。
…と考えると、このバイナリもまだまだ相当ステップ省略できそうですね。「桁あふれしないことはわかっている」っていうのはかなりハードな最適化なのでコンパイラができなくても仕方ないかなと思いますが、SWAPをして3を足す代わりにそのまま0x30を足す、くらいの芸当はやってほしかったですね。まあ、無料版だから仕方ないか。C言語でそう書けばいいんでしょうけどね…。