メモ帳DPA

ぐぐってあまり引っかからないような何かがあったら書いたりする

最近遊んだSteamゲームだけSSD上に自動配置する

背景

ディスクのパフォーマンスはゲームのロード時間に直結するので、ゲームはなるべくSSDに置きたい。ただSSDの容量は限られているので、どんどん積まれ続けていくゲームを全部SSDに置いておくわけにもいかない。
一番簡単なのはSSDを買うことだけど、実際に起動するゲームは大量に積まれているうちのほんの一部だし、ゲームは増え続けるのであまり積極的にこのために買う気もしない。

都度自分で移動するにしても、Steamのインストールフォルダの移動機能は1個ずつしか出来ないのと、ファイル実体を移動する場合はIDで記載されたファイルが何のゲーム用かは中身を見ないと分からないという問題があるので、結構面倒な作業になってしまう。
Steam Moverという外部ツールは手軽に移動できて結構いい線行っていたものの、結局は手動選択なのが惜しいポイントだった。

ということでどうにかした。

方法

そもそもストレージ自体を階層化出来れば綺麗にすべて解決しそうなんだけど、この機能はWindowsServerにしか付いていないらしい。
一応コマンドラインからWin10でも無理やり作れるらしいが、情報も実績も少ないので今は止めておいた。

最近遊んだゲームだけSSDに置くようにすればゲーム限定でそれっぽい動作にはなりそうなので、以下のようにスクリプト化してタスクスケジューラに突っ込むことにした。
2週間以内に遊んだゲームはSSDに配置し、それ以外のゲームはHDD上に配置されるようになった。便利。

情報取得

直近で遊んでいるゲームの抽出は、ローカルのライブラリからプレイ履歴直接取る方法が見つからなかったので、Steam Web APIから拾うことにした。API Keyを発行して、自分のSteamIDを指定すれば使える。

GetRecentlyPlayedGamesというAPIを叩くと、直近2週間でプレイしたゲームのリストが返ってくるので、ここから対象のゲームIDを入手し、Steam Mover相当のコマンドに変換して走らせるだけで済む。

http://api.steampowered.com/IPlayerService/GetRecentlyPlayedGames/v0001/?key=APIキー】&steamid=【SteamユーザID】&format=json

返ってくる内容の例

{"response":{"total_count":6,"games":[{"appid":49520,"name":"Borderlands 2","playtime_2weeks":2246,"playtime_forever":2246,"img_icon_url":"a3f4945226e69b6196074df4c776e342d3e5a3be","img_logo_url":"86b0fa5ddb41b4dfff7df194a017f3418130d668"},{"appid":620980,"name":"Beat Saber","playtime_2weeks":407,"playtime_forever":998,"img_icon_url":"2b54e9f73692cecf2c15c46fbfd2cded0cbded49","img_logo_url":"9863f8790902188c59c44d410f67a3e5ce452bb9"},{"appid":382110,"name":"Virtual Desktop","playtime_2weeks":132,"playtime_forever":7516,"img_icon_url":"c4f78db130aec6ee1188f432b0ceaf102d50e28a","img_logo_url":"4c32cf132501fb26528b3b3316363f89bcf81785"},{"appid":690510,"name":"Black Survival","playtime_2weeks":16,"playtime_forever":16,"img_icon_url":"2a407923a221e54921b7fa6c7bbcf86bb7a6a40e","img_logo_url":"4bdbe1fe41efaa1b5a7508c82d2782a5ab9ae602"},{"appid":206420,"name":"Saints Row IV","playtime_2weeks":7,"playtime_forever":7,"img_icon_url":"b5e8448a3e2ea31ddf3595addae4e1eee2375c0d","img_logo_url":"6f7e659c6b58971ebf9710b7f0048c20c68aadd7"},{"appid":631900,"name":"Airtone","playtime_2weeks":5,"playtime_forever":3021,"img_icon_url":"7f3e856650ac447f80f181aee59ab83d8608f743","img_logo_url":"a391d42fffbe283e16e8639efaf78b872df507d3"}]}}

Steamゲームのファイル

【Steamライブラリパス】\steamapps\appmanifest_【APPID】.acf

ゲームの情報が格納されているマニフェストファイルらしい。ゲームの実体のパスもこの中で指定されている。
このファイルは別にSSDに移動しなくても良いのだけれど、SSDに一緒に置いておいたほうがHDDに戻すときに対象の判別が楽なのでコピーすることにした。

【Steamライブラリパス】\steamapps\common\【ゲーム名】

ゲームの実データが格納されているフォルダ。
このフォルダをSSD上に移動し、元パスからジャンクションを貼れば高速化完了。

出来たスクリプト

# 初期設定 ###########################

$APIKEY = "【APIキー】"
$USERID = "【ユーザID】"

$HDDLIB = "D:\Games\SteamLibrary"
$SSDLIB = "E:\Games\SteamLibrary"

$LOGFILE = "D:\program\steammove\SteamMove.log"

######################################


# IDからインストールパス取得
function GetInstallPath($AppId){
    # AppIdからマニフェストパス指定
    $ManifestPath="${HDDLIB}\SteamApps\appmanifest_${AppId}.acf"
    if(! (test-path ${ManifestPath}) ){ 
        echo "$appid manifestファイルがない"
        exit
    } 

    # マニフェストファイル内から実パス抽出
    $InstallDir=(Get-Content $ManifestPath | grep installdir).trim().split("`t")[2] -replace "`"",""
    $InstallPath="SteamApps\common\${INSTALLDIR}"
    if($InstallPath -eq "SteamApps\common\"){
        echo "$appid InstallDir取得失敗"
        exit
    }

    return $InstallPath
}

# steamapiから2週間以内にプレイしたゲームリスト取得
$RecentlyPlayedList  = (curl "http://api.steampowered.com/IPlayerService/GetRecentlyPlayedGames/v0001/?key=${APIKEY}&steamid=${USERID}&format=json").content
if(! $?){
    echo "SteamAPI取得失敗"
    exit
}


# 最近遊んでないゲームをHDDに移動
echo "■ SSD→HDD"
foreach($Manifests in (Get-ChildItem "${SSDLIB}\steamapps\*acf").Name ){
    $AppId = ($Manifests -replace "appmanifest_","" -replace ".acf","")

    # プレイ済みなら飛ばす
    $RecentlyPlayed=0
    foreach($games in (ConvertFrom-Json $RecentlyPlayedList).response.games ){
        $PlayedAppId=$games.appid
        $Manifestname="appmanifest_${AppId}.acf"

        if($Appid -eq $PlayedAppId){
             $RecentlyPlayed=1
             break
        }
    }
    if($RecentlyPlayed){
        continue
    }

    # パス取得
    $HddInstallPath="${HDDLIB}\$(GetInstallPath $AppId)"
    $SsdInstallPath="${SSDLIB}\$(GetInstallPath $AppId)"

    $ManifestPath="SteamApps\appmanifest_${AppId}.acf"
    $SsdManifestPath="${SSDLIB}\${ManifestPath}"


    # 移動実施
    echo "------------------"
    echo "$HddInstallPath  -> $SsdInstallPath"

    cmd /C rd "${HddInstallPath}"
    robocopy /E "${SsdInstallPath}" "${HddInstallPath}" /LOG+:"${LOGFILE}" /NP 

    cmd /C rd /S /Q "${SsdInstallPath}"
    Remove-Item $SsdManifestPath
}


# 最近遊んだゲームをSSDに移動
echo "■ HDD→SSD"
foreach($games in (ConvertFrom-Json $RecentlyPlayedList).response.games ){
    $Appid=$games.appid

    # パス取得
    $HddInstallPath="${HDDLIB}\$(GetInstallPath $AppId)"
    $SsdInstallPath="${SSDLIB}\$(GetInstallPath $AppId)"

    $ManifestPath="SteamApps\appmanifest_${AppId}.acf"
    $HddManifestPath="${HDDLIB}\${ManifestPath}"
    $SsdManifestPath="${SSDLIB}\${ManifestPath}"


    # 存在してなかったら飛ばす
    if(! (test-path ${HddInstallPath}) ){ continue } 

    # リンク作成済みなら飛ばす
    if( (get-item ${HddInstallPath}).LinkType -eq "Junction" ){ continue  } 


    # 移動実施
    echo "------------------"
    echo "$SsdInstallPath  -> $HddInstallPath"

    robocopy /E "${HddInstallPath}" "${SsdInstallPath}" /LOG+:"${LOGFILE}" /NP
    Copy-Item $HddManifestPath $SsdManifestPath
    cmd /C rd /S /Q "${HddInstallPath}"
    cmd /C mklink /J "${HddInstallPath}" "${SsdInstallPath}"
}

課題とか

久々に起動したゲームはこのスクリプトが走るまでの間は当然HDDに配置されたままになる

→すぐに移動させたかったらスクリプト手動で叩くということで妥協

保持期間を調整できない

APIの対象期間が2週間固定で期間を増やせないのであきらめる