Nimによるコマンドラインツールの開発が一段落したので、
Chromeの開いているタブをまとめる目的で記事化。

なぜコマンドラインツールか

私はWebの業界にあこがれてPHP -> JS -> Node.jsという風に、
Webに特化したエンジニアとしてスキルを磨いてきた。
だからGUIの世界が当然で、CLIの世界は古臭い世界のように勘違いしていた。

実際に触ったCLIはとても素晴らしく創造性に富んだ世界だった。
苦手だったターミナルを少しずつ触り始め、Vimを覚えて、FishShellを覚えてという風に使い方を覚えていくにつれて自分でもCLIツールを作るようになっていった。

Node.jsやPHPではCLIツールを作る事は可能だが、
動作させるには同じバージョン以上の環境が入っている事が要求される。
要するに、一般に広く公開する用途としては向いていない。

これへの解決策としてGolangによる開発を一時期検討した。
ConoHa CLIという形で実装はしたものの、Golangのコードや思想の良さがイマイチ分からず微妙に思っていた。

今はGolangに代わる言語の候補を探しており、今回はNimで一つ作ってみた。

作っているもの

miyabisun/admiral-stats-uploader - GitHub

Admiral Statsという公式のデータを表示・解析するタイプの攻略サイト。
これの支援ツールを作ってみようと思って作成した。

艦これアーケードに関して

艦これアーケードというゲームを最近始めた。
これは元々Webブラウザゲームとして存在していた艦これというゲームのアーケード版。
Web版では編成や装備を整えて出撃した後は見守る事が主の艦これと比べ、舵やアクセルスロットルを模した備え付けのコントローラを操ることで艦隊戦を行うアクション性の強いゲームとなっている。
また、艦娘がカードとして筐体から吐き出され、最大36枚まで束ねてデッキとして利用することも可能。

しかし、あまりにもアーケード版はプレイしづらい。
それは艦娘カードがダブった時の処理だ。

ダブった艦娘は複数枚用意してデッキを組めば能力が多少向上するのだが、その上昇幅はかなり控えめとなっている。
また、狙いの艦娘が中々出ないプレイヤーへの救済措置として改装設計図という仕様が用意されており、複数枚集めると改装済みの強力な艦娘と直接交換することも可能になっている。
よほど好きな娘以外がダブった場合は改装設計図として利用した方が良いのだ。

問題はどの艦娘が取得済みなのか分からない所にある。
私も元々様々なタイトルのカードゲームをプレイしているので、メインで使うカードの50枚程度はさらっと暗記できるが、流石に使わないし興味もないようなカードを何枚も覚えられない。
それでもやっていれば覚えはするが全ての艦娘にはホロ仕様のレアカードや、改装済の強化バージョンがあり、そのレパートリーを含めると全ての状態を網羅することは簡単ではない。

実はこの艦娘カードの取得状況はSEGAの公式サイトの提督情報では裏の通信にAjaxが採用されており、中身のJSONファイルを参照すれば全て閲覧することができる。
しかし、実際に提督情報で表示される情報はその中のほんの一部であり、多角的に分析したり解析したりということは一切出来ない。

これを使って艦娘達のカードのダブリを解決できないかと思ってあれこれ作っていたが、
まさにそれを行うAdmiral Statsというサイトがすでに存在していた。

結局このサイトの支援ツールを作るというところに落ち着いた。
でも海域やミッションでどの艦娘が出るかある程度決まっており、その編を考慮して絞り込む機能がないので最終的には自分で作る必要があるかもなぁと考えている。

ログ

ユニットテスト

まずは単体テストの仕組みを作る。
下記はsrc/modules/hoge.nimをテストするtests/modules/thoge.nim

import unittest, ../../src/modules/hoge

suite "/modules/hoge":
  test "a to b":
    check(hoge(a) == b)
  • nimble testコマンドではtestsディレクトリ直下のnimファイルを全て実行
  • src配下、test配下のファイルを監視して更新の度にテストが自動実行される仕組みを実装
    • これはNode.jsが得意なのでNode.jsで作った
    • ゆくゆくはNimScriptでできるようにしたい
  • Nimではテストファイル名はtから始める

ログイン

SEGA公式の提督情報からJSONファイルをダウンロードしたいのだが、
ログインを行い、セッションを作らなければダウンロード出来ない。

セッション情報はCookieで持っている為、HTTPレスポンスヘッダのSet-Cookie:の文字列を抽出して後から使う事で実現した。

しかし、httpclientモジュールのレスポンスには注意点があり、ハマってしまった。

  • echo $response.headerでheaderを開くと、set-cookieはスライス要素数3を示す
  • echo $respose.header["set-cookie"]で確認すると、最初の要素の文字列になってしまう

Nimフォーラムを読むと、一度テーブルに変換してから取り出せとのことで下記のようにすることで要素の取得に成功

import tables

# 〜〜〜 中略 〜〜〜
response.header.table["set-cookie"]

JSONのダウンロード→アップロード

実際にJSONファイルをダウンロードする。
これはAdmiral Statsの実装を見ながら同じように実装。

import httpclient

let client = newHttpClient()
client.headers = newHttpHeaders({
  "Content-Type": "application/json",
})
let response = client.request(url, HttpGet)
if (response.status == "200 OK"):
  let body = response.body

30分毎の実行

osパッケージにsleepメソッドが用意されている。
下記を実行後、topコマンドで見張っていたが酷いCPU使用率等は特にみられなかったので、
どうやらちゃんと休んでいるようだ。

import os
sleep 30 * 60 * 1000

メンテ時対応

毎週火曜日の2時〜7時は艦これアーケードのメンテ時間。
それに伴い提督情報のサイトも停止してしまう為、ログイン〜情報取得処理が失敗してしまう。

なのでtimesパッケージをもとに現在時刻が火曜日の2〜7時は処理をスキップするよう機能を追加。

import times

proc isundermaintenance*(time: DateTime): bool =
  if time.weekday != dTue: return false
  for it in 2..6:
    if time.hour == it: return true

echo now().isundermaintenance() # true or false

コマンドラインツール対応

0.18現在、parseopt2モジュールは非推奨でparseoptモジュールを使うようにという注意が出る。
使い方は完全に同じ。

今回はシングルコマンドに設計したので関数1個で終わらせた。
ライブラリの都合に振り回されずにサブコマンドの仕組みも簡単に作れそうで、この辺はとても良かった。

import parseopt, opt, sequtils

type
  asuOptions* = object
    id*: string
    pass*: string
    token*: string
    autoupdate*: bool
    help*: bool
    verbose*: bool

proc parseoptions*(str: string): asuOptions =
  var options = asuOptions(id: "", pass: "", token: "", autoupdate: false, help: false, verbose: false)
  var p = initOptParser str
  for kind, key, val in p.getopt():
    case kind
    of cmdLongOption:
      case key
      of "id": options.id = val
      of "pass": options.pass = val
      of "token": options.token = val
      of "autoupdate": options.autoupdate = true
      of "help": options.help = true
      of "verbose": options.verbose = true
    of cmdShortOption:
      case key
      of "i": options.id = val
      of "p": options.pass = val
      of "t": options.token = val
      of "a": options.autoupdate = true
      of "h": options.help = true
    else: continue
  result = options

# コマンドライン引数はシーケンスで来るので、joinでつなげて文字列にしてから渡す
echo os.commandLineParams().join(" ").parseoptions

参考サイト