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実装で何本かゲームを作りながら、適宜大幅な仕様変更を交えつつ作っていく予定なので、完成までにはまだまだ時間が掛かりそうです。

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

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