2021年9月29日水曜日

User-AgentでiPadとMacの区別は不可能

例えば、あるURLにアクセスした時に、

  • iOS → AppStoreのアプリページ
  • Android → GooglePlayのアプリページ
  • PC → ランディングページ

という形で機種に応じて異なるURLにリダイレクトする場合、PHPであれば、

<?php

// 言語判定

$isJa = false;

$accept_language = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);

foreach ($accept_language as $language) {

    if (preg_match('/^ja/i', $language)) {

        $isJa = true;

        break;

    }

}

// 端末判定

$ua = $_SERVER['HTTP_USER_AGENT'];

if ((strpos($ua, 'iPhone') !== false) || (strpos($ua, 'iPad') !== false)) {

    if ($isJa) {

        print '<meta http-equiv="refresh" content="0;URL=https://apps.apple.com/jp/app/アプリID">';

    } else {

        print '<meta http-equiv="refresh" content="0;URL=https://apps.apple.com/app/アプリID">';

    }

} elseif ((strpos($ua, 'Android') !== false)) {

    print '<meta http-equiv="refresh" content="0;URL=https://play.google.com/store/apps/details?id=パッケージID">';

} else {

    print '<meta http-equiv="refresh" content="0;URL=ランディングページURL">';

}

?>

という感じで、User-Agent毎に出力する meta タグを変えればOK...というミスは、誰しもが踏むトラップかと思います。

iPhone or iPad だと、ブラウザにPCモードとモバイルモードみたいなものがあり、モバイルモードであれば、iPhoneなら「iPhone」、iPadなら「iPad」がUser-Agentに含まれていますが、PCモードだとMacと同じ(Macintosh; xxxx Mac OS xxxxみたいな形)になります。

また、iPadはあるバージョンからデフォルトがPCモードになっています。

つまり、上記コードだと、iPadからアクセスするとAppStoreではなくランディングページに飛ぶことになります。

Macでアクセスした時、ランディングページではなくAppStoreに飛ばしてしまっても問題無ければ、iPhone or iPad の判定式を次のように変更してあげれば良いかと思います。

if ((strpos($ua, 'iPhone') !== false) || (strpos($ua, 'iPad') !== false) || (strpos($ua, 'Mac') !== false)) {

User-AgentでiPadとMacの区別は不可能です。(そもそも、ブラウザやOSバージョンによってコロコロ変わるので確実性は保証できません)

参考までに、手元にあるMacとiPadのUser-Agentは以下のように(Mac OS Xのバージョン部分以外は完全に一致する形に)なっていました。

MacBook Air 2020 (Intel) + macOS Big Sir 11.6 + Safari の User-Agent

Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15

iPad Pro 3rd + iPadOS 15.0 + Safari (default = Desktop) の User-Agent

Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15


2021年8月21日土曜日

Activity遷移でTransactionTooLargeExceptionが発生する問題

Activityから別のActivityへ遷移した時、Bundleに保存されている情報がスタックされるのですが、Bundleにスタックできる情報量は1MB以下に制限されていて、1MBを超えるとTransactionTooLargeExceptionでアプリがクラッシュします。

ActivityのBundleだけでなく、FragmentのBundleの情報もスタックされる点に注意が必要です。(これだけが言いたかった)

正直なところ、「Activityの再生成メカニズムはまともに機能できていないのではないか?」と思う今日この頃です。再生成メカニズムというのは具体的には、Activityのライフサイクルの解説図面で言うところの「App process killed」のシーケンスです。

「App process killed」のシーケンスが実行されると、例えばシングルトンで保持しているインスタンス類は全てnullになってロストします。つまり、onCreateはその状態で再実行される可能性があるので、onCreateではシングルトンのデータ復旧処理などを記述しておく必要があります。

その設計思想通りに設計されたアプリが果たしてどの程度あるのか。

少なくとも、私は見たことがありません。

例えば、Google謹製のYouTubeアプリですら、初期状態からのリラン(プロセスリスタートと同等)をしています。

iOSにはこのような中途半端な復旧シーケンスは無くて、カーネルにアプリのプロセスが停止させられた場合、プロセスリスタートになります。

要は、以下が本来あるべき姿だと思います。

Activity Life cycleの本来あるべき姿(私見)

カーネルによりプロセスが停止された場合、アプリプロセスはリスタートで良いと思います。

むしろ、カーネルによる停止時にonDestroy実行保証してくれる方が重要だと思います。それが実現されれば、onDestroyでアプリプロセスがリスタートされた時に必要な情報をアプリ自身で記憶しておき、リスタート時にアプリ自身で復旧シーケンスを実行することが可能なので。

もう少し言ってしまうと、下図ぐらいシンプルにしてしまった方が良かったのではないかなと思います。

  • Start/Resume → onForegroundでまとめる
  • Pause/Stop → onBackgroundでまとめる
  • onRestart相当のものは無くても良い(必要ならonForegroundの引数でOK)
Androidのライフサイクルがこれぐらいシンプルだったら、そもそもTransactionTooLargeExceptionや前の記事で書いたNoSuchMethodExceptionなどというトラブルを生み出さなくても良かった筈。

FragmentActivityがonCreateでNoSuchMethodExceptionで落ちる(アプリが起動時に落ちるetc)

Androidの一部機種で、ActivityがonCreateでNoSuchMethodExceptionが発生してクラッシュする謎の不具合に悩まされ続けていました。せめて、再現できる環境が作れれば良いのですが、クラッシュレポートから確認できる機種とOSバージョンを合わせても再現しないから厄介でした。

再現さえしてくれれば確実に修正できるのですが、再現しない以上、クラッシュレポートから想定される原因を推測して、対策バージョンをリリース、その後のレポートの変化を見て推測の精度を上げるというアプローチで時間をかけて対策するしかありません。

最初に結論を書くと、Fragmentの取り扱いに関する問題で、以下2点の対策をしておけば大丈夫でした。

  1. Fragmentの追加(commit)は Activity.onResume 〜 Activity.onPause の間に行う
  2. Fragmentのコンストラクタは必ず引数無しにする

以下、少し長くなりますが、順を追って説明します。

リリース当初は、NoSuchMethodExceptionは発生していなくて、代わりにIllegalStateExceptionが発生していました。これは単純な私の実装ミスです。

onCreateでFragmentを生成してcommitするとIllegalStateExceptionが発生します。

これは、生成したFragmentをcommitしても良いのは、onResume 〜 onPause の間だけという制約があるためです。そして、Activityが起動すると onCreate → onStart → onResume という順序でコールバックされます。つまり、onCreate呼び出しはonResume前だから、onCreateでFragmentをcommitすると必ずIllegalStatusExceptionになる「はず」です。

ですが、50機種近い機種依存テストを実施しても何故か問題が顕在化しなかったので、リリース時にこのバグを積み残してしまった結果、これに起因するクラッシュが結構な数報告されました。

この問題は、原因がハッキリしているので対策は簡単です。

私は自前のBaseActivityに「onResume中でなければ実行できない処理を滞留しておく仕組み」を作り、Fragmentのcommitはその仕組で滞留させておきonResumeで実行される形にしました。

具体的には、BaseActivityに次のようなexecuteWhileResumeというメソッドを追加します:

open class MyBaseActivity : FragmentActivity() {
private val pendingProcedures = MutableList(0) { {} }
private var pausing = true

fun executeWhileResume(procedure: () -> Unit) {
if (pausing) {
pendingProcedures.add(procedure)
} else {
procedure.invoke()
}
}

override fun onResume() {
super.onResume()
pausing = false
while (pendingProcedures.isNotEmpty()) {
pendingProcedures.removeAt(0).invoke()
}
}

override fun onPause() {
pausing = true
super.onPause()
}
}

そして、Fragmentをcommitする処理は、executeWhileResume { 処理 } とすれば、onResume後に処理が実行されるようになり、IllegalStateExceptionが発生することは無くなりました。

しかし、今度はonCreateの延長で NoSuchMethodException が発生する問題がクラッシュレポートに報告されるようになります。(ここからが本題)

NoSuchMethodException は、とあるFragmentの引数無しコンストラクタをgetConstructorしようとして発生しています。どのFragmentのコンストラクタを実行しようとしたのかは、難読化されていて分かりませんが、クラッシュレポートの報告では「p4.c0.<init>」となっていました。

まず、解せないのは、onCreateの延長でFragmentのコンストラクタが呼び出されている点です。

というのもFragmentの生成→commitは全てexecuteWhileResumeで実行しているので、onCreateからFragmentを生成する筈がありません。もちろん、xml内に埋め込んでいるFragmentも無いので、何故onCreateでこのバグが報告されたのか意味不明でした。

Stack Overflowに似たような報告があります。

https://stackoverflow.com/questions/56668934/java-lang-nosuchmethodexception-for-oncreate

最も支持されている回答は、

The activity is being restored from an instance state bundle. Part of the restore operation is recreating its fragments. (訳: アクティビティは、インスタンス状態のバンドルからリストアされています。リストア操作の一部として、そのフラグメントを再作成しています。)

Your activity has a fragment and the fragment class does not have a 0-arg constructor required by the framework. (訳: アクティビティにフラグメントがあり、フラグメントクラスにフレームワークで必要とされる0argのコンストラクタがありません。)

とのこと。

ただ、先述の通りonCreateではFragmentの生成をしていないので、当初この回答を理解することはできませんでした。(※結果的にはこの回答が正しかったですが...)

別のクラッシュレポートを確認したところ、何故か、onCreateでPreferenceに値をセットしているところでもNoSuchMethodExceptionが発生するレポートも挙がっていました。

これには混乱しました。

完全に意味不明です。

何かしらのI/Oが発生すると、外的にFragmentが生成されるのではないか」という仮設が浮上してしまう始末の悪さ。

そこで、実装としてはかなりキモチワルイのですが、Activityの初期化シーケンスを次のように修正してみることにしました:

  • onCreate では setContentView、findViewById 以外を呼び出さない
  • 従来 onCreate で実行していた初期化処理全般は onResume で自前の初期化処理を once call (一度だけ呼び出す)

これで治ったら逆に意味不明です。

案の定、この対策ではレポート報告は収束しませんでした。

新たなクラッシュレポートは更に意味不明な内容で、Activity の onCreate で自前のログ記録ユーティリティでデバッグログ出力メソッド(リリースビルド時は無出力)をコールしていたのですが、その延長でNoSuchMethodExceptionが発生していました。なにゆえ???

そのデバッグログ出力メソッドは、デバッグビルドであるかを判定してLog.dでログを出力するだけの単純なものです。そして、当然リリースビルドなのでLog.dによるログ出力すらしていない筈です。つまり、何も処理を実行していないメソッド呼び出しの延長で、arg0が無い謎のFragmentのコンストラクタ(p4.c0.<init>)が実行されようとしたのです。

当初、p4.c0.<init> は難読化されたクラス名かと思いましたが、実はAndroidのソースコードでは内部的に https://p4.org と呼ばれる通信制御ライブラリが使われているので、もしかしてそのP4なのだろうか?????

参考までに以下、p4公式サイトの説明を邦訳:

P4(Programming Protocol-Independent Packet Processors)とは、ネットワーク機器のドメイン固有の言語であり、データプレーン機器(スイッチ、NIC、ルーター、フィルターなど)がどのようにパケットを処理するかを規定したものです。

P4以前は、ベンダーがネットワークでサポートされる機能を完全にコントロールしていました。また、ネットワークシリコンが可能な動作の多くを決定するため、シリコンベンダーが新機能(VXLANなど)のロールアウトをコントロールし、ロールアウトには何年もかかっていました。

P4はこのような従来のモデルを覆します。アプリケーション開発者やネットワークエンジニアは、P4を使ってネットワークに特定の動作を実装することができ、変更も数年ではなく数分で行うことができます。

出力ログを収集する怪しいウィルスでもあるのだろうか?と陰謀論のようなことを考えてしまいました。

とりあえずonCreateでのログ出力メソッドの呼び出しは(Activityのライフサイクル検証のため)デバッグ用に入れた冗長なものなので消しておくことにしました。

もう1点、実装当初、Fragmentの再生成を抑止するために、コチラの情報を参考にして、onCreateで super.onCreate(savedInstanceState?.apply { remove("android:support:fragments") }) を実行するようにしていたのですが、コレが問題を引き起こす原因になっている可能性も考えられるかも...とも思うようになりました。というのも、再生成を抑止するために「super.onCreate(null)」を呼び出すと「深刻な問題」を引き起こす可能性があるとコチラに書かれていたので、このロジック的に理解できない呼び出しが影響して、今の意味不明な状態が起きているのではないかと推測しました。

実のところ、この処置(Fragmentの再生性抑止)は、当初のIllegalStateExceptionで血迷って入れてしまったコードなので、executeWhilteResumeの仕組みを導入した時点で削除すべきでした。

という訳で、

  • BaseActivityのログ出力処理を削除
  • super.onCreate(savedInstanceState?.apply { remove("android:support:fragments") }) → super.onCreate(savedInstanceState) に変更 

という修正を入れて、「これで治ってくれ!」と願いながらリリース。

翌日、願うようにCrashlyticsコンソール(この対策の少し前に導入)を覗いてみたのですが、残念ながらまだNoSuchMethodExceptionの報告は収束していません。

ただ、NoSuchMethodException が発生する状況は変わらないのですが、p4.c0.<init> ではなく r4.c0.<init> が呼び出される形になったので、(当然ですが)やはりP4は関係無く、単に難読化で変更されたクラス名だったようです。

難読化、正直居るん? と思いつつあります。

Crashlyticsコンソールで問題発生時のコンディションを詳しく確認した結果、何故onCreateで何らかのFragmentのarg0コンストラクタが実行されたのかを理解することができました。

自分のActivityのonCreateから、NoSuchMethodExceptionが発生するまでのスタックトレースを見てみると、androidx.fragment.app.FragmentController.dispatchCreateの延長でr4.c0.<init>がコールされていることが分かりスッキリ。

つまり、FragmentControllerの仕組みでActivityが再生成されれば現象が発生するようです。そして、起動すると即落ちする現象が発生するユーザ環境では、何故かActivity生成時にこの再生成シーケンスが呼び出されたのであろうことが理解できました。(この情報は、Developer Consoleのクラッシュレポートでは分からず、Crashlyticsを導入して初めて把握できるようになります)

Fragmentサブクラスに引数無しコンストラクタを定義してもAndroid Studioが一切警告してくれなかった(昔はちゃんと警告してくれていた)ので、最新のTarget SDKならその問題が治ったのかと勘違いしてしまい、引数付きのFragmentコンストラクタを大量に作ってしまっていたので、これがFragmentControllerの仕組みとミスマッチだったようです。

という訳で、

  • 自前Fragmentはすべて引数無しに変更
  • 自前Fragmentは例外なく companion object { fun newInstance(... なりでインスタンス化する仕様に変更

という対処をしてリリース。

なお、どういった条件でActivityの再生性が発生するのかは依然として分かりませんが、一度OSホーム画面に戻り、メモリ所要量の多いアプリを複数個起動してからアプリに復帰すれば、Activityのライフサイクル(App process killed)に従ってActivityの再生成が行われるので、その方式でチェック。

そして、「今度こそは治ってくれ...」と切実に願いながらリリース。

無事、NoSuchMethodExceptionのクラッシュレポートは収束したようです!

ただし、今度はまた別の問題(TransactionTooLargeException)がレポートされてきたので、まだしばらくは休めそうにないですが。


2021年8月3日火曜日

Android: EditTextのバックスペース(削除)キーが効かない問題

かなり久々にAndroidのプログラミングネタです。

EditText のバックスペースキー(KeyEvent.KEYCODE_DEL)が一部端末で効かない問題があり、原因を調べてみました。

調査のため、EditText::setOnKeyListener を仕掛けてみたのですが、そもそもバックスペースキーのイベントが飛んできていないらしい。

そこで、試しに Android 9(P) 以降で追加された addOnUnhandledKeyEventListener という、その名の通りハンドルされなかったキーイベントを検出するリスナを仕掛けてみたところ、反応がありました。

という訳で、EditText::addOnUnhandledKeyEventListener がファイアしたらdispatchKeyEventをするという雑な対処を入れてみたところ、正常に動作するようになりました。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
edit.addOnUnhandledKeyEventListener { v, event ->
v.dispatchKeyEvent(event)
true
}
}

ただし、1枚のFragment / Activityに複数のEditTextがある場合、必ずしも編集中のEditTextのOnUnhandledKeyEventListener がファイアされる訳ではないようです。どうも固定のプライオリティ順序でイベントがファイアする(まるでバグのような)仕様のようです。

という訳で、複数のEditTextがある場合、hasFocusでフォーカスを持っているEditTextに対してdispatchKeyEventをしてあげるというクソのような対処が必要になるようです。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val listener = View.OnUnhandledKeyEventListener { v, event ->
when {
edit1.hasFocus() -> {
edit1.dispatchKeyEvent(event)
true
}
edit2.hasFocus() -> {
edit2.dispatchKeyEvent(event)
true
}
else -> {
false
}
}
}
edit1.addOnUnhandledKeyEventListener(listener)
edit2.addOnUnhandledKeyEventListener(listener)
}

そもそも、何故バックスペースキーのイベント(カーソルキーのイベントなども同様)がunhandledになってしまったのかが少し気になるところなので、View::addOnUnhandledKeyEventListenerのドキュメンを確認してみましたが、有益な情報は記述されていないようです。たぶん、Pieで実装された何かしらのOSのエンハンスに引っ張られてunhandledなイベントをアプリ側で捕捉しなければならない何らかのアレが起こったのであろう...と想像。


2021年6月6日日曜日

覇邪の封印(MSX版)の攻略手順

MSX版「覇邪の封印」の攻略情報を書きます。

MSX版には、パッケージに布製の地図とフィギュアが同梱されていますが、これらは単なるオマケではなく、ゲームをプレイするために必要なツールでして、説明書でもフィギュアの左足部分を現在位置に置いてプレイする旨が指示されています。実際に地図の上にフィギュアを置いてプレイしなければ開始直後(トウメガネやセンリノタマ入手まで)で詰みます。

という訳でマップ情報を掲載
スタート地点は左下の「アルカス城」です。

以下、チャートを記しますが、初見プレイなら見ないことを推奨。
(1) 初期地点の城(アルカス城)へ行く
→ コサーマへ会えと言われる
(2) 東2→北7→東1 で「ガリアの街」へ(入らなくてOK)
(3) 東3→北3→東2→北1→東5→北4→東3→北3 で「オルコの街」へ
※この間に「悪の商人」(ハゲてる方の商人)を1体以上狩れることを祈る
※「悪の商人」は1体につき2,000G & 知名度up
(4) オルコの街で「トウメガネ」購入(10,000G)
※以下、地図を見ながら移動
(5) 知名度+150以上になったら「エラトス城」へ行きライセンスをもらう
→ 魔獣を倒した時にキバ(換金アイテム)を獲得できるようになる
(6) エラトス城の東の島に居るコサーマに合う
→ フルキカブトを入手
→ 長老に500G払って使い方を確認
※魔具は基本的に長老に聞いて初めて使えるようになる
(7) 50,000G 溜めて鍛冶屋を雇う
※ここまでで恐らく初期装備は壊れているハズ
※全裸&素手で殴れば大丈夫なので初期装備は修理不要
→ 鍛冶屋を雇ったら タテ と ヨロイ を買う
※雀の涙程度だが防御力が上がり狩り効率がよくなる
(8) キバ300本貯まったらアレオイア城へ城へ行って武器を入手
→ イリスノセンプ(主人公用)を入手
→ 他の仲間の武器は今貰っても捨てるしかないのでもらわない
※主な狩場 = エラテアから一歩西に出たところのローバル(1体キバ4本)
※固定エンカなので効率よく稼げる
(9) 50,000G 溜まったらアルゴノフネを買う
※メノス or エラテアの市場で売っている
(10) 30,000G 溜まったらレッチノツエを買う
※まれにランダムエンカウントの魔道士からタダで貰える
(11) 30,000G 溜まったらセンリノタマを買う
※メノス or エラテアの市場で売っている
※マップの見える範囲が広くなる(無くても問題無い)
(12) 知名度が+6000を超えたら「シロノマキュウ」を入手
※500G払って使い方も聞いておくこと
(13) ウンムタクを狩って「イカリノカガミ」を入手
※500G払って使い方も聞いておくこと
※倒すと知名度が-1,000なので注意
※オルコの街の北(北東)側の平原でエンカウント
(14) 96,000G 貯まったら鍛冶屋の村でイリスノセンプを強化
(15) ローバル狩りである程度レベルを上げる
※この時点ではレベルMAXまで上げなくても良い
※魔獣の洞くつで防具を入手した後の知名度回復作業(24)でレベルMAXにする
※普通にプレイしても、この段階でまだ全然レベルMAXになっていない筈
※攻撃力のバーの上の青いバーが動かなくなればレベルMAX
(16) ブローニュの石碑へ行き呪文(ガリョウイザタタン)を覚える
(17) ガディアの街の市場で仲間(ガイ)を加入
(18) エラトス城へキバ300本を持っていきガイの武器入手
(19) アベイデア城付近の魔獣の洞くつ攻略して(クロノスノヨロイ→主人公)
(20) ドーリスの街の酒場で仲間(メディア)を加入
(21) アベイデア城へキバ300本を持っていきメディアの武器入手 
(22) 残りの魔獣の洞くつをすべて攻略(全員のヨロイとタテが揃う)
※盾の装備順を間違えるとトレモスが仲間に加わわらないので注意
※間違った盾を装備してしまった場合は持ち替えればOK
・オデッセ → 主人公
・ヘクトール → ガイ
・セイレーン → メディア
(23) 聖騎士狩りをして「コウモリノカメン」を入手しておく
※魔獣の洞くつ攻略で知名度が大幅にダウンして-10,000でカンストしている
※聖騎士は1体倒すと-9,999知名度が下がるがカンストしていれば問題無い
(24) 知名度回復作業
※ローバルを400体狩って知名度を+1以上にしておく
※この時点でレベルMAXになるのが理想
(25) ケーペウス神殿で仲間(トレモス)を加入
(26) 石碑で「トレモスキタレリ」を唱えてトレモスの武器と防具を揃える
(27) 鍛冶屋でトレモスの武器を強化
※資金に余裕があればガイ(更に余裕があればメディア)の武器も強化
(28) コサーマに再開
→ 呪文「キタレイアソン」を覚える
(29) イアソンの記念碑で呪文を唱えて3つの鍵を入手
(30) 地下神殿へ行きテラリン撃破
※地下神殿の鍵を挿す順序:
・天の鍵 = ウエ
・地の鍵 = ミギ
・冥の鍵 = ヒダリ
→ テラリン以外のエンカウントはすべて「逃げる」こと
※鍛冶屋無効(武器の耐久度が下がったら回復できない)
※呪い師無効(ココで使えないとなると加える意味がほぼ無い)
呪い師を雇う最適なタイミングは (28) の後
→ 最初にアイテム(イカリノカガミ、シロノマキュウ、レッチノツエ)を使う
→ 次に肉壁たち(トレモス、メディア、ガイ)に死ぬまで殴らせる
→ 最後に主人公が殴ればギリギリ倒せる
※復活の玉があれば余裕をもって倒せる(復活の玉込みならレベリング作業時間をかなり短縮可能な見込みだが、どの程度短縮するかは要調査)

2021年5月25日火曜日

S3+CloudFrontで配信している静的ページをFTPで更新できるようにした時のメモ

AWSのS3+CloudFrontで配信している静的Webサイトの更新をFTP(SFTP)でできるようにした時のメモです。

(手順)
  1. サーバ(EC2)にサイト更新用のアカウント(ex: webadmin)を作成してSFTPで繋がるようにする
  2. S3の内容をサーバのローカル(ex: /home/webadmin/web-root)にコピー
  3. lsyncdで/home/webadmin/web-root以下を監視(設定内容を後述)
    1. ファイル追加時:
      1. aws s3 cp でローカル→S3へ転送
    2. ファイル変更時:
      1. aws s3 cp でローカル→S3へ転送
      2. aws cloudfront create-invalidation で invalidation を実行
    3. ファイル削除時:
      1. aws s3 rm で S3からファイルを削除
      2. aws cloudfront create-invalidation で invalidation を実行

(/etc/lsyncd.conf)

settings {

    logfile = "/var/log/lsyncd.log",

    statusFile = "/var/log/lsyncd.status",

    nodaemon = false,

    statusInterval = 1,

    delay = 15,

}


s3sync = {

    maxProcesses = 1,

    onCreate = "[ -f ^source^pathname ] && aws s3 cp ^source^pathname ^target^pathname || true",

    onModify = "[ -f ^source^pathname ] && aws s3 cp ^source^pathname ^target^pathname && aws cloudfront create-invalidation --distribution-id ディストリビューションID --paths ^pathname || true",

    onDelete = "[ -f ^source^pathname ] && aws s3 rm ^target^pathname && aws cloudfront create-invalidation --distribution-id ディストリビューションID --paths ^pathname || true",

}


sync {

    s3sync,

    source = "/home/webadmin/web-root",

    target = "s3://S3バケット名",

}

※上記の網掛け部分を適宜書き換える

(補足)

  • 最初、AWS Transfer for SFTPを使おうとしたけど、CloudFrontのinvalidation込みで動かそうとすると結局サーバが必要になりそうだったし、Lambdaを使うのも面倒だったので、個人的に一番楽な方法で対応
  • この方式に切り替えたサイトはAWSコンソール上で直接S3にファイルアップロードするのは避けるように運用変更が必要(S3とローカルで不整合が発生してしまうので)
  • 本当は git でwebサイト全体を管理して、master更新の都度S3+CloudFrontに反映する感じの方が個人的には良いと思います...ただ、非エンジニアにgitを使わせるのは難儀なので、そうなると安定のFTP(ただし、これから構築するならFTPやFTPSではなくSFTPですが)

2021年3月7日日曜日

Fairy Computer(新しいレトロゲーム機)

Fairy Computer System 80 (FCS80) という新しいレトロ(?)ゲーム機を開発中。

VGSとは別枠です。

VGSが目指した所は、純粋なエミュレーションではなく、アプリケーション・フレームワークによるプラットフォーム共通化でした。規模が全然違いますがUnityみたいなものです。FCS80はVGSと違う純粋なハードウェアエミュレーションによる「仮想ゲーム機」です。

https://github.com/suzukiplan/fcs80

CPUはZ80A互換。

8bit CPUの中で最も拡張性が高いZ80A互換CPUを採用しました。

動作速度はドーピングできなくもないですが、実機Z80Aと同じ(3579545Hz)にしておきましたので、仮に実機を開発する場合でもCPU調達については容易に可能です。

また、音源チップもAY-3-8910互換APUです。

こちらも(Z80Aよりは少し難しいかもしれませんが)一応簡単に調達できると思います。

ただし、PPU(VDP)に関しては既製品ではなく、オリジナルチップ(FCS80-VIDEO)なので、実機を作る場合はFPGA等でエミュレーション?(そっち方面は詳しくないです...)する必要があるかもしれません。

PPUもお手軽に調達できるように既製品を使うことを考えて、最有力候補はTMS9918Aだったのですが、それだとMSXやSG-1000と大差無くなってしまうのでツマラナイ。

ただし、TMS9918Aというと8bit時代のゲーム機のVDP代表格ですが、実のところ8bitでのプログラミングがしやすいかというと微妙です。(そもそもTMS9918Aを最初に搭載したTI-99/4Aは16bit機ですし)

ファミコンのPPUは6502でのプログラミングに特化したものですが、Z80Aのプログラミングに特化したVDPというのは実は存在しないような気がしたので、それならばZ80Aととことん相性の良いVDPを設計してみよう...というのがFCS80を作り始めたキッカケです。

TMS9918A系譜のVDPの何処がダメなのかというと、主にVRAMアドレスの管理が面倒です。TMS9918AのVRAMは16KB($0000〜$3FFF = 14bit)ですが、アドレスアクセスポートは1系統のみなので「2回連続してOUT」という野暮ったい実装が必要な点がイケてない。(実はこの点はファミコンのPPUでも同じです)

そこで、CPUメモリからVRAMをmemory mapする方式を採用しました。つまり、LD系の命令だけで簡単にVRAMを更新したり、読み取ったりすることが可能です。どちらかというとパソコン的なアプローチですね。

(CPU memory map)

AddressMap
$0000 ~ $1FFFPRG0: ROM Bank #0 (Program)
$2000 ~ $3FFFPRG1: ROM Bank #1 (Program or Data)
$4000 ~ $5FFFPRG2: ROM Bank #2 (Program or Data)
$6000 ~ $7FFFPRG3: ROM Bank #3 (Program or Data)
$8000 ~ $BFFFVRAM (16KB)
$C000 ~ $FFFFRAM (16KB)

VRAMがたったの16KBしかありませんが、そのたった16KBで2画面(BG + FG)、スムーススクロール、IRQによるラスタースクロール可能、256スプライト、256色同時発色というファミコンを遥かに凌駕するスペックを実現しています。(実のところまだ2KBぐらい空き領域 = Reserved Area がある)

(VRAM memory map)

CPU addressVRAM addressMap
$8000 ~ $83FF$0000 ~ $03FFBG Name Table (32 x 32)
$8400 ~ $87FF$0400 ~ $07FFBG Attribute Table (32 x 32)
$8800 ~ $8BFF$0800 ~ $0BFFFG Name Table (32 x 32)
$8C00 ~ $8FFF$0C00 ~ $0FFFFG Attribute Table (32 x 32)
$9000 ~ $93FF$1000 ~ $13FFOAM; Object Attribute Memory (4 x 256)
$9400 ~ $95FF$1400 ~ $15FFPalette Table (2 x 16 x 16)
$9600$1600Register #0: Scanline vertical counter (read only)
$9601$1601Register #1: Scanline horizontal counter (read only)
$9602$1602Register #2: BG Scroll X
$9603$1603Register #3: BG Scroll Y
$9604$1604Register #4: FG Scroll X
$9605$1605Register #5: FG Scroll Y
$9606$1606Register #6: IRQ scanline position (NOTE: 0 is disable)
$9607 ~ $9FFF$1607 ~ $1FFFReserved area
$A000 ~ $BFFF$2000 ~ $3FFFCharacter Pattern Table (32 x 256)

グラフィック以外の特徴的な点としては、バンクコントローラがカセット搭載ではなく本体搭載という点です。MSXのメガロムでいうところのASC8方式なので、1セグメント8KBで最大256バンク(つまり、最大2MB = 16メガビット)のROM空間を持つことができます。

あとは、RAMサイズは16KBです。

パソコンだったら多少物足りないサイズ感ですが、ゲーム専用機としてはかなりゴージャスなので、恐らくC言語でも割と普通にプログラミングできるかもしれません。(ちなみにSG-1000やファミコンは標準で2KB)

ただ、飽くまでも「Z80アセンブリ言語でのプログラミングがしやすい」という所を目指していきます。

基礎設計はだいたい出来上がっているのですが、example実装で何本かゲームを作りながら、適宜大幅な仕様変更を交えつつ作っていく予定なので、完成までにはまだまだ時間が掛かりそうです。

2021年1月11日月曜日

湾岸エリア攻略

今回の冬休みは、地元の県(静岡)から帰省禁止令(正確には不要不急の帰省を控えるようにするお達し)が出ていたので、東京で過ごしました。特にやることもないので、自転車で東京をぐるぐる回って過ごしました。

荒川サイクリングロードを使えば、東京の上下の端までの移動が簡単 & 安全にできます。

まずは、江東区若洲にある東京ゲートブリッジへ行ってきました。

というのも、とある位置情報ゲーム(テクテクライフ)で中央防波堤付近へ行く必要があったのですが、中央防波堤には徒歩や自転車では行くことができないので。東京ゲートブリッジを使えば中央防波堤に上陸こそできませんが、結構近づくことができます。(ゲームの方は上陸しなくても近づけば目的を達成できる)

ただし、休暇の期間中、東京ゲートブリッジの歩道は閉鎖されていたので、目的は未達...

※ちなみに、12/31の大晦日に行きました

若洲にはキャンプ場があり、初日の出が良い感じに見えそうな気がするので、キャンパーが沢山居るかと思いきや、目視で確認した範囲ではテントは3つしか無かったです。若洲〜新木場(夢の島)近辺には倉庫とかしかないので、大晦日は完全にゴーストタウンでした。普段は大型トラックが沢山通っているであろう大きな道路がほぼ貸し切り状態だったので、自転車でも車道を安全に走れました。

別の日に、中央区の晴海にも行きました。

コチラもテクテクライフで現地に行く必要があったので。

江東区の湾岸エリアほどではないですが、コチラも中々のゴーストタウン。晴海はそもそも、観光に行くところではないですが、やはり帰省している人が多いのかもしれません。

有明〜お台場方面にも行きました。

晴海と違って観光地ということもあり、まばらに人が居ましたが、「ここは東京なのか?」と錯覚する程度にはゴーストタウンでした。



お台場へ行った目的はレインボーブリッジを渡らないと塗れない部分を塗るため。

レインボーブリッジは車、ゆりかもめ、徒歩の何れかの手段で渡ることができます。なお、自転車の場合、後輪に滑車みたいなものを付けて押していく必要があります。

最後に葛西臨海公園。

コチラも離れ小島(下図)のようなところがあり、現地塗りが必要だったので。離れ小島には普通に橋で歩いて渡れます。

このような感じで東京の湾岸エリア(下図の赤枠で囲っているあたり)を回ってきました。

(主な攻略エリア)

  • 港区(港南・お台場)
  • 中央区(晴海・勝どき)
  • 江東区(有明・豊洲・東雲・辰巳・塩浜・新木場・若洲)
  • 江戸川区(臨海町)

トータルの走行距離はだいたい300kmぐらい。

東京の自宅から実家までの徒歩ルートだとだいたい240kmぐらいなので、実家までの片道分以上の距離を動き回りました。もちろん、1日で全部回った訳ではなく、何日かに分けて自宅〜東京湾岸エリアを何度も往復しています。1日あたりの平均移動距離は50kmぐらい。ミニベロ(小径車)だとこの辺が限界で、もっと行動範囲を増やすにはロードレーサーとかが必要になります。(持ってない)

東京湾岸エリアは、住むにはものすごく不便な場所(そもそも新木場と臨海町には今のところ住宅は無い)ですが、道路が広く、新木場や若洲などのロジスティクス倉庫ぐらいしか無いエリアは休日に車も少ないので、自転車乗りには住心地が良いかもしれません。

ただし、投機的な事情で高級住宅街に担ぎ上げられてしまったので、平民が住めるエリアではありませんが。暴落して安くなったらあの辺への引っ越しもアリかな...と思考を巡らせましたが、どんなに暴落しても足立区や千葉とかより安くなることは無いだろうから、やはりナシかなぁ。でも、豊洲とか有明あたりなら頑張れば都心にも自転車で通えるので、毎日出勤する必要がある日々に戻ったらナシではないかも。

2021年1月7日木曜日

MSXを技術的に振り返る

MSX開発の関係者を匂わせるタイトルかもしれませんが、私は全然関係ない(外野)です。

私はマイコン世代(死語)の人間ですが、実はリアルタイムでMSXは触ったことすら無いです。私が最初に触ったパソコンはPC-9801(16bit機)で、8bit機のMSXは触る機会すらありませんでした。

なので、MSXには全然詳しくないし、思い入れとかも全然ありません。PC-9801だと使えないスプライトが使えたり、PSG音源が標準実装されている点が羨ましいと思った記憶がある程度です。

最近、TinyMSXというMSXエミュレータを開発した関係で、MSXのハード仕様的な部分であれば現役でMSXを使っていた世代の諸先輩方よりも詳しくなりました。そんな客観的視点から「MSXとは何だったのか」を分析してみようと思います。

なお、MSXにはMSX、MSX2、MSX2+、MSX turboRという4シリーズあります。

以降、初代MSXのことは便宜上「MSX1」と表記します。

まず、MSX1のハード構成は以下の通りです。

  • CPU: Z80A互換
  • MMU: 独自システム(スロット)
  • Sound: AY-3-8910
  • Video: TMS9918A

TMS9918Aはテキサス・インスツルメンツ社が開発した画像処理装置(Video Display Processor)です。かなりクセが強いですが、16KBという比較的少ないビデオメモリ(VRAM)でそこそこカラフル(16色中16色を同時発色可能)な映像表現ができます。

TMS9918は元々、1979年にリリースされたテキサス・インスツルメンツのTI-99/4というパソコン(ホームコンピュータ)に搭載するために開発されたVDPです。そして、TMS9918Aは1981年にリリースされた後継機のTI-99/4Aに搭載されました。TMS9918との違いは画面モードにMode 2が追加された点です。(Mode 2は256x3の豊富 & カラフルなキャラクタパターンを表示できる画面モードで、MSX1の市販ゲームソフトの大半がMode 2を使っています)

なお、TI-99/4は最初に16bit CPU(TMS9900)を搭載した先進的なホームコンピュータとして知られています。ただし、変態キーボードを搭載していたり、競合のAppleIIと比べて値段が高かったりといった難があり、1981年までの2年間で2万台以下しか売れませんでした。一方、後継機のTI-99/4Aはかなりヒットして、北米でトータル280万台出荷されました。

MSX1が販売された1983年の時点では、TMS9918Aは既に2年落ちの古いVDPでしたが、TI-99/4Aのヒットにより安価に調達できたものと考えられます。同年にSEGAから発売されたSG-1000にもTMS9918Aが採用されました。

ちなみに、SG-1000と同じ年(というか同じ発売日)に任天堂からファミリーコンピュータも販売されましたが、ファミコンのVDP(PPU)は、TMS9918Aと違い、3色のカラフルなスプライトを同時に64枚(TMS9918Aは単色で同時に32枚)表示でき、1ドット単位の滑らかなBGスクロールができる(TMS9918Aだと8ドット単位のカクカクなBGスクロールしかできない)という、当時としてはとても優れたものでした。

MSX1のCPUは1976年にリリースされたZ80の互換CPU(8bit)です。

そして、PSG(音源)のAY-3-8910は、1978年にリリースされたサウンドチップで、当時のゲームサウンドではほぼデファクトスタンダードの地位だったので、ゲーム機やホビー系パソコンなどに広く搭載されていました。なお、一部のMSXではYAMAHA製のYM2149(AY-3-8910の互換チップ)が使われています。

つまり、1983年に発売されたMSX1は、1976〜1981年ごろに普及した(2〜7年落ちの)古いチップセットが使われています。MSXの基本コンセプトは「ホームコンピュータの標準化」なので、ハード部分は枯れた技術の寄せ集めの方が参入障壁が下がり都合が良かったものと想像できます。

ただし、MSX2(1985年)でだいぶ状況が変わります。

MSX2では、ASCII, Microsoft, YAMAHAがMSX用に共同開発したV9938(通称MSX-VIDEO)というTMS9918A上位互換のVDPが採用されました。

V9938は、カラフルなビットマップ表示(※これはASCIIからのリクエスト)や1行に80列表示できるテキスト表示(※これはMicrosoftからのリクエスト)などが出来る反面、ゲーム向けの機能強化については申し訳ない程度にしか強化されませんでした。

当時のASCIIとMicrosoftはあまりゲームを重視していなかったようです。

グラフィックはかなりキレイになりました。

ただし、ビットマップベースの画像処理には膨大な量のデータ処理が必要です。TMS9918Aは8x8のキャラクタベースの画像処理でしたが、ビットマップベースだとデータ量が単純計算で64倍に増えることになります。しかし、CPUは依然としてZ80(9年落ち)なので、ビットマップベースの膨大な量のデータ処理には向きません。その問題を解決するため、V9938にはコマンドと呼ばれるDMA装置が実装されています。しかし、そもそもデータ処理量が多いのでコマンド実行速度は期待したほど速くありませんでした。そのため、V9938はファミコンよりもキレイなグラフ表示などができる反面、キャラクタベースの画像処理主体のゲーム・グラフィックとしての性能はファミコン(MSX2発売時点で2年落ち)に劣るものでした。実際、ザナックEXと呼ばれるファミコン版ザナックをMSX2へ移植したゲームが存在しますが、処理落ちがかなり酷いものになってしまいました。

MSX2+(1988年)でVDPがV9938からV9958に強化され、横方向のハードウェアスクロールが可能になるなどのマイナーチェンジがされましたが、CPUは依然としてZ80(12年落ち)でした。CPUについてはMSX turboRでR800(16bit)になり劇的に速くなったものの、VDPはV9958に据え置かれており、今度はコマンドのウェイト発生が動作速度upする上でのネックになったものと想像できます。

MSXは世界向けの標準規格を目指していましたが、MSX2の時点で海外のメーカーはほぼ撤退し、MSX2+では日本メーカー3社のみが残り、最後のMSX turboRに至っては日本メーカー1社だけの日本ローカル規格になってしまいました。

MSXの標準化路線自体は正しかったものの、MSX2以降でミスディレクションしているように見えます。MSX1はCPUとVDPのバランスが良かったのですが、MSX2以降の全てのシリーズではCPUとVDPのバランスが常に悪かったという点が目立ちます。MSXはどちらかというと、「ゲーム開発できるゲーム機」という側面が強かったので、ゲーム(ホビー)向けに振り切っていれば、標準規格としてもっと生き長らえることができたかもしれません。

YAMAHAはV9938の開発後、SEGAマークIIIに搭載される315-5124というTMS9918A上位互換(V9938とは別の系譜)のVDPを開発しています。315-5124はMode 4という機能が追加実装され、Mode 4ではカラフル(512色中32色同時発色)なスプライトとBGを表示でき、1ドット単位の滑らかなスクロールも実現できます。更に、Mode 4のBGはV9938のようなビットマップベースではなくキャラクタベースなので、V9938と違いバランスが良い(Z80の処理性能でも制御しやすい)VDPだといえます。Mode 4はファミコンのPPU以上の性能があります。ファミコンだとBGの属性テーブルのビットレイアウトが独特(1バイトで2x2ブロック=4キャラクタ分の属性を設定する複雑な仕様)なのに対して、315-5124はネームテーブルと属性テーブルが各1バイトのシンプルなレイアウトになっているので、315-5124の方がゲームプログラムの開発には適していると思います。

要するに、V9938ではなく315-5124を搭載したMSX (MSX1+?) が欲しかった。

合理的ではないものを作りたい

ここ最近、実機版の東方VGSの開発が忙しくて、東方VGSの曲追加が滞っています。 東方VGS(実機版)のデザインを作りながら検討中。基本レトロUIベースですがシークバーはモダンに倣おうかな…とか pic.twitter.com/YOYprlDsYD — SUZUKI PLAN (...