<?xml version='1.0' encoding='utf-8'?>
<CheatTable CheatEngineTableVersion="45">
  <CheatEntries>
    <CheatEntry>
      <ID>1</ID>
      <Description>"【アタッチ／一括解除】ゲームを選択・確認"</Description>
      <Color>00A5FF</Color>
      <VariableType>Auto Assembler Script</VariableType>
      <AssemblerScript>

{ 	Game    : Survivalist Invisible Strain.exe
 	Author  : Black_wolf
 	CheatVer:286-001-004
 	Memo:   Game Version 286
	　　　  FileVersion 6000.0.35.10107846 / Steam Build 24880722
	　　　  Game Engine: Unity 6000.0.35f1 (Unity Mono)
}

[ENABLE]
{$lua}
if syntaxcheck then return end
GT.oneShot(memrec, function() GT.attachGame() end)
{$asm}
[DISABLE]
</AssemblerScript>
    </CheatEntry>
    <CheatEntry>
      <ID>2</ID>
      <Description>"0. 設定"</Description>
      <Options moManualExpandCollapse="1" />
      <Color>FFFF00</Color>
      <GroupHeader>1</GroupHeader>
      <CheatEntries>
        <CheatEntry>
          <ID>3</ID>
          <Description>"言語設定 / Language"</Description>
          <Options moManualExpandCollapse="1" />
          <Color>FFFF00</Color>
          <GroupHeader>1</GroupHeader>
          <CheatEntries>
            <CheatEntry>
              <ID>4</ID>
              <Description>"English（初期表示・既定）"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.oneShot(memrec,function() GT.setLang('en') end)
{$asm}
[DISABLE]
</AssemblerScript>
            </CheatEntry>
            <CheatEntry>
              <ID>5</ID>
              <Description>"日本語（切替）"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.oneShot(memrec,function() GT.setLang('ja') end)
{$asm}
[DISABLE]
</AssemblerScript>
            </CheatEntry>
          </CheatEntries>
        </CheatEntry>
        <CheatEntry>
          <ID>6</ID>
          <Description>"はじめに"</Description>
          <Options moManualExpandCollapse="1" />
          <Color>FFFF00</Color>
          <GroupHeader>1</GroupHeader>
          <CheatEntries>
            <CheatEntry>
              <ID>7</ID>
              <Description>"初期表示は英語です。機能を有効にする前にシングルプレイのセーブをロードしてください。"</Description>
              <GroupHeader>1</GroupHeader>
            </CheatEntry>
            <CheatEntry>
              <ID>8</ID>
              <Description>"エディタで対象を選び、値をダブルクリックして編集します。"</Description>
              <GroupHeader>1</GroupHeader>
            </CheatEntry>
            <CheatEntry>
              <ID>9</ID>
              <Description>"ゲーム・プレイ状態・対象の変化時は変更を停止します。"</Description>
              <GroupHeader>1</GroupHeader>
            </CheatEntry>
            <CheatEntry>
              <ID>10</ID>
              <Description>"別版・別ストアは意味的検証が必要です。互換性を保証するものではありません。"</Description>
              <GroupHeader>1</GroupHeader>
            </CheatEntry>
            <CheatEntry>
              <ID>11</ID>
              <Description>"値の再取得は手動です。自動再取得は必要時に明示設定します。"</Description>
              <GroupHeader>1</GroupHeader>
            </CheatEntry>
          </CheatEntries>
        </CheatEntry>
        <CheatEntry>
          <ID>12</ID>
          <Description>"診断"</Description>
          <Options moManualExpandCollapse="1" />
          <Color>FFFF00</Color>
          <GroupHeader>1</GroupHeader>
          <CheatEntries>
            <CheatEntry>
              <ID>13</ID>
              <Description>"Lua Engine（ONで開く／OFFで閉じる）"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.userRun(function() GT.luaEngine(true) end)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if GT then GT.userRun(function() GT.luaEngine(false) end) end
{$asm}
</AssemblerScript>
            </CheatEntry>
            <CheatEntry>
              <ID>14</ID>
              <Description>"解決器を確認（読み取り）"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.oneShot(memrec,function() GT.scanSelfTest() end)
{$asm}
[DISABLE]
</AssemblerScript>
            </CheatEntry>
            <CheatEntry>
              <ID>15</ID>
              <Description>"環境・接続情報"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.oneShot(memrec,function() GT.envReport() end)
{$asm}
[DISABLE]
</AssemblerScript>
            </CheatEntry>
            <CheatEntry>
              <ID>16</ID>
              <Description>"機能・解決状態"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.oneShot(memrec,function() GT.statusReport() end)
{$asm}
[DISABLE]
</AssemblerScript>
            </CheatEntry>
          </CheatEntries>
        </CheatEntry>
      </CheatEntries>
    </CheatEntry>
    <CheatEntry>
      <ID>100</ID>
      <Description>"01. 便利機能"</Description>
      <Options moManualExpandCollapse="1" />
      <Color>FFFF00</Color>
      <GroupHeader>1</GroupHeader>
      <CheatEntries>
        <CheatEntry>
          <ID>110</ID>
          <Description>"無敵"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'God',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'God',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>111</ID>
          <Description>"体力無限"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'BloodLoss',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'BloodLoss',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>124</ID>
          <Description>"一撃必殺"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'oneHitKill',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'oneHitKill',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>112</ID>
          <Description>"怪我・病気しない"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'InjuriesAndDisease',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'InjuriesAndDisease',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>113</ID>
          <Description>"疲労なし"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'Fatigue',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'Fatigue',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>114</ID>
          <Description>"空腹なし"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'Hunger',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'Hunger',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>115</ID>
          <Description>"喉乾かない"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'Thirst',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'Thirst',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>116</ID>
          <Description>"睡眠不足なし"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'SleepDeprivation',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'SleepDeprivation',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>117</ID>
          <Description>"感染しない"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'Infection',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'Infection',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>118</ID>
          <Description>"体温正常"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'BodyTemperatureInCelsius',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'BodyTemperatureInCelsius',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>119</ID>
          <Description>"最大重量無視"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'ignoreWeight',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'ignoreWeight',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>120</ID>
          <Description>"弾・弓減らない"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'keepAmmo',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'keepAmmo',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>121</ID>
          <Description>"ステルスモード"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'stealthMode',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'stealthMode',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>122</ID>
          <Description>"XP取得倍率操作"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'xpMultiplier',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'xpMultiplier',false)
{$asm}
</AssemblerScript>
          <Options moHideChildren="1" />
        </CheatEntry>
        <CheatEntry>
          <ID>123</ID>
          <Description>"有効にした便利機能をメンバーに"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'memberScope',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'memberScope',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
      </CheatEntries>
    </CheatEntry>
    <CheatEntry>
      <ID>200</ID>
      <Description>"02. プレイヤー"</Description>
      <Options moManualExpandCollapse="1" />
      <Color>FFFF00</Color>
      <GroupHeader>1</GroupHeader>
      <CheatEntries>
        <CheatEntry>
          <Options moHideChildren="1" />
          <ID>211</ID>
          <Description>"プレイヤスピード倍率操作"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'moveMultiplier',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'moveMultiplier',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>210</ID>
          <Description>"プレイヤーエディタ"</Description>
          <Color>00A5FF</Color>
          <Options moHideChildren="1" />
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.openEditor(memrec,'player')
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if GT then GT.userRun(function() GT.closeEditor(memrec) end) end
{$asm}
</AssemblerScript>
        </CheatEntry>
      </CheatEntries>
    </CheatEntry>
    <CheatEntry>
      <ID>300</ID>
      <Description>"03. コミュニティ"</Description>
      <Options moManualExpandCollapse="1" />
      <Color>FFFF00</Color>
      <GroupHeader>1</GroupHeader>
      <CheatEntries>
        <CheatEntry>
          <ID>316</ID>
          <Description>"全メンバー一覧"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.oneShot(memrec,function() GT.action('listMembers') end)
{$asm}
[DISABLE]
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>317</ID>
          <Description>"全NPC一覧"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.oneShot(memrec,function() GT.action('listNPCs') end)
{$asm}
[DISABLE]
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>311</ID>
          <Description>"メンバーエディタ"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.openEditor(memrec,'members')
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if GT then GT.userRun(function() GT.closeEditor(memrec) end) end
{$asm}
</AssemblerScript>
          <Options moHideChildren="1" />
        </CheatEntry>
        <CheatEntry>
          <ID>315</ID>
          <Description>"NPCエディタ"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.openEditor(memrec,'npcs')
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if GT then GT.userRun(function() GT.closeEditor(memrec) end) end
{$asm}
</AssemblerScript>
          <Options moHideChildren="1" />
        </CheatEntry>
        <CheatEntry>
          <ID>310</ID>
          <Description>"コミュニティエディタ"</Description>
          <Color>00A5FF</Color>
          <Options moHideChildren="1" />
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.openEditor(memrec,'units')
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if GT then GT.userRun(function() GT.closeEditor(memrec) end) end
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>314</ID>
          <Description>"メンバー全回復"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.oneShot(memrec,function() GT.action('recoverMembers') end)
{$asm}
[DISABLE]
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>312</ID>
          <Description>"メンバー好感度最大"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'memberApproval',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'memberApproval',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>313</ID>
          <Description>"全NPCの好感度最大"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'npcApproval',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'npcApproval',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
      </CheatEntries>
    </CheatEntry>
    <CheatEntry>
      <ID>400</ID>
      <Description>"04. アイテム"</Description>
      <Options moManualExpandCollapse="1" />
      <Color>FFFF00</Color>
      <GroupHeader>1</GroupHeader>
      <CheatEntries>
        <CheatEntry>
          <ID>412</ID>
          <Description>"アイテム減らない"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'keepItems',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'keepItems',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>410</ID>
          <Description>"アイテム個数変更"</Description>
          <Color>00A5FF</Color>
          <Options moHideChildren="1" />
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.openItemQuantities(memrec)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if GT then GT.userRun(function() GT.closeEditor(memrec) end) end
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>411</ID>
          <Description>"アイテム追加(カテゴリ別　個数)"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.oneShot(memrec,function() GT.addItem() end)
{$asm}
[DISABLE]
</AssemblerScript>
        </CheatEntry>
      </CheatEntries>
    </CheatEntry>
    <CheatEntry>
      <ID>450</ID>
      <Description>"05. クラフト・ビルド"</Description>
      <Color>FFFF00</Color>
      <GroupHeader>1</GroupHeader>
      <Options moManualExpandCollapse="1" />
      <CheatEntries>
        <CheatEntry>
          <ID>452</ID>
          <Description>"作成にアイテム必要なし"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'noMaterials',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'noMaterials',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>451</ID>
          <Description>"即クラフト・ビルド"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'accelerateWork',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'accelerateWork',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
      </CheatEntries>
    </CheatEntry>
    <CheatEntry>
      <ID>500</ID>
      <Description>"06. ワールド"</Description>
      <Options moManualExpandCollapse="1" />
      <Color>FFFF00</Color>
      <GroupHeader>1</GroupHeader>
      <CheatEntries>
        <CheatEntry>
          <ID>512</ID>
          <Description>"常に昼"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'alwaysDay',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'alwaysDay',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>510</ID>
          <Description>"天候指定"</Description>
          <Color>00A5FF</Color>
          <Options moHideChildren="1" />
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.oneShot(memrec,function() GT.action('setWeather') end)
{$asm}
[DISABLE]
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>511</ID>
          <Description>"天候固定"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'weatherLock',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'weatherLock',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>513</ID>
          <Description>"時刻を固定"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'lockTime',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'lockTime',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>514</ID>
          <Description>"霧無効"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'noFog',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'noFog',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>515</ID>
          <Description>"MAP全開放"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'revealMap',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'revealMap',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
      </CheatEntries>
    </CheatEntry>
    <CheatEntry>
      <ID>600</ID>
      <Description>"07. その他"</Description>
      <Options moManualExpandCollapse="1" />
      <Color>FFFF00</Color>
      <GroupHeader>1</GroupHeader>
      <CheatEntries>
        <CheatEntry>
          <ID>604</ID>
          <Description>"Steam実績全解除..."</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.oneShot(memrec,function() GT.action('achievements') end)
{$asm}
[DISABLE]
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>605</ID>
          <Description>"ボーダレスウィンドウにする"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'borderlessWindow',true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
GT.toggleRecord(memrec,'borderlessWindow',false)
{$asm}
</AssemblerScript>
        </CheatEntry>
      </CheatEntries>
    </CheatEntry>
  </CheatEntries>
  <LuaScript>-- GameTemplate V3 / Cheat Engine 7.5+ / Lua 5.3+
-- Standalone runtime only. No installation paths, disk dumps, or analysis log writers.
-- [GAME] sections are contracts to implement after LOCAL binary analysis.
-- Empty contracts deliberately fail closed: this template does not contain game cheats.
if GT then
  if type(GT.shutdown)~='function' then
    print('[GT] An older GT table is still loaded. Restore and close it before loading V3. / 旧GTテーブルを解除して閉じてからV3を開いてください。')
    return
  end
  local restored,err=pcall(GT.shutdown,true)
  if not restored then
    print('[GT] '..tostring(err))
    return -- retain the old state if restoration fails
  end
end
GT = {}
local G = GT
G.VERSION, G.CHEATVER, G.TAG = 'V3', '286-001-004', 'SIS'
G.lang = 'en' -- Initial display is ALWAYS English; Japanese is an explicit user switch.
G.C_GROUP, G.C_SCRIPT = 0xFFFF00, 0x00A5FF
G.serial, G.epoch = 0, 0
G.A, G.status, G.hooks, G.active = {}, {}, {}, {}
G.sessions, G.dynamic, G.queue, G.forms, G.deferred = {}, {}, {}, {}, {}
G.history = {}

-- [GAME] Identification must validate the selected binary, not just its filename/store.
-- candidates(name,pid): cheap name/window predicate; nil means show process picker.
-- identify(modules,pid,name): return true only for this game, with binary evidence.
-- boundary(): stable world/save-root identity token; changes invalidate editors/features.
-- Modules use exact basenames/aliases or a predicate, NEVER a directory or fixed base/RVA.
G.CONFIG = {title='Game Template V3', candidates=nil, identify=nil,
  boundary=nil, tick=250, retry=5000, maxCandidates=64, editorAutoRefresh=false, editorUndo=false,
  scanChunkBytes=262144, resolveBudgetMs=1500, retryBatch=1,
  modules={code={main=true}}}
-- [GAME] Prefer reflected/exported semantic names; otherwise bounded AOB + validation.
-- Each resolver MUST validate the target's class/owner/type/call contract, not only bytes.
-- kind='site'|'rip'|'call'|'multi'; off relative to match; disp/length describe instruction.
-- ranges={executable=true} is default. Data patterns may use {executable=false}.
-- patterns={ {p='...',off=...,disp=...,length=...}, ... } are alternative anchors.
-- resolve=function(modules) return {candidate1,...} end allows Mono/UE/export resolution.
-- validate=function(target,site,module) return true end is mandatory.
-- multi requires count=N; fixed-RVA adapters must verify binary, instructions and structure.
G.SIGS = {}
-- [GAME] Hooks: {sig=...,len=...,aa='[ENABLE] ... [DISABLE] ...',
--   validate=function(site,original) ... end, quiesce=function(state) ... end}
-- $SITE/$ORIG/$NOP are expanded. AA must assert original bytes, use a near jump,
-- preserve registers/ABI, RELOCATE displaced RIP-relative/call instructions correctly,
-- restore original bytes before freeing code caves; quiesce must drain in-flight jobs.
G.HOOKS = {}
-- [GAME] Features: {hooks={},prepare=fn,on=fn,off=fn,task=fn,available=fn,
--   boundary=nil|false|function() return stableScalar end}. nil uses CONFIG.boundary;
-- false is for verified process-only features, NOT a bypass for world-dependent work.
-- off must be idempotent and restore partial effects if on fails.
-- Whole-feature cleanup stays pending if a restore fails; do not pretend it succeeded.
G.F = {}
-- [GAME] Editors: rows() -&gt; {{id=stableID,en=...,ja=...},...};
-- context(id) -&gt; {identity=stableToken,...}; fields={ {en,ja,kind='integer'|'number'|'string',
-- min,max,maxLength,choices={{value=...,en=...,ja=...}},read(ctx),write(ctx,value),
-- available(ctx)}, ... }. Re-resolve field layout INSIDE read/write, never store raw offsets
-- from a different build. write must return true and verify game-side invariants.
-- resolve by Mono metadata / UE FProperty / local-binary-derived validated operands.
-- Required lifecycle: verified context -&gt; validate input -&gt; fresh read -&gt; write -&gt; readback.
-- Bindings below are intentionally absent until a game adapter is implemented.
G.EDITORS = {}
G.ACTIONS = {}
-- No executable fake AOB, example player offset, fixed folder or original-game address.

local function need(v,en,ja) if not v then error(G.T(en,ja),0) end return v end
local function hex(v) return string.format('%X',v) end
local function keyOK(v) return type(v)=='string' and v:match('^[%a_][%w_]*$') end
local function equal(a,b)
  if type(a)~='table' or type(b)~='table' or #a~=#b then return false end
  for i=1,#a do if a[i]~=b[i] then return false end end return true
end
local function bytes(a,n)
  local ok,b=pcall(readBytes,a,n,true)
  if ok and type(b)=='table' and #b==n then return b end
end
local function db(b) local t={} for i,v in ipairs(b) do t[i]=('%02X'):format(v) end return table.concat(t,' ') end
local function checkedCall(fn,...)
  local r=fn(...); need(r~=false,'Operation rejected.','処理が拒否されました。'); return r
end
G.sameBytes, G.readExact = equal, bytes
function G.T(en,ja) return G.lang=='ja' and (ja or en) or en end
function G.msg(en,ja) local message=tostring(G.T(en,ja))
  message=message:gsub('%[string "[^\n]-"%]:%d+:%s*','')
  print('['..G.TAG..'] '..message) end
function G.err(en,ja) error(G.T(en,ja),0) end
function G.userRun(fn)
  local ok,e=pcall(fn)
  if not ok then G.msg(tostring(e),tostring(e)) end
  return ok,e
end
function G.defer(fn)
  local epoch=G.epoch
  local t=createTimer(nil,false); G.deferred[t]=true; t.Interval=1
  t.OnTimer=function()
    if not G.deferred[t] then return end
    G.deferred[t]=nil
    pcall(function()
      t.Enabled=false
      t.OnTimer=nil
      t.destroy()
    end)
    if G.epoch==epoch then G.userRun(fn) end
  end
  t.Enabled=true
end
function G.oneShot(mr,fn)
  local id=mr and mr.ID
  local tracked=id and G.dynamic[id]
  local epoch=G.epoch
  G.defer(function()
    G.userRun(fn)
    if id and GT==G and (id==1 or G.epoch==epoch) and G.dynamic[id]==tracked then
      local current=getAddressList().getMemoryRecordByID(id)
      if current and current.Active then current.Active=false end
    end
  end)
end
function G.cancelDeferred()
  local pending=G.deferred
  G.deferred={}
  for timer in pairs(pending) do
    -- CE may already have destroyed its Lua wrapper during teardown.
    -- Removing the registration also prevents a queued callback from running.
    pcall(function()
      timer.Enabled=false
      timer.OnTimer=nil
      timer.destroy()
    end)
  end
end

-- PE header offsets below are FORMAT constants, never game addresses/layout assumptions.
local function u32(a) local v=readInteger(a); if v and v&lt;0 then v=v+0x100000000 end return v end
local function s32(a) local v=u32(a); if v and v&gt;=0x80000000 then v=v-0x100000000 end return v end
local function u16(a) local b=bytes(a,2); return b and b[1]+b[2]*256 end
function G.pe(m)
  local b=m.Address
  need(bytes(b,2) and u16(b)==0x5A4D,'Invalid MZ header.','MZヘッダが不正です。')
  local off=u32(b+0x3C)
  need(off and off&gt;=64 and off&lt;0x100000,'Invalid PE header.','PEヘッダが不正です。')
  local p=b+off
  need(u32(p)==0x4550,'Invalid PE signature.','PEシグネチャが不正です。')
  local n,optional=u16(p+6),u16(p+20); local magic=u16(p+24)
  need(n and n&gt;0 and n&lt;=96 and optional and optional&gt;=96 and
    (magic==0x10B or magic==0x20B),'Unsupported PE layout.','未対応のPE形式です。')
  local size=u32(p+24+56)
  need(size and size&gt;0 and (not m.Size or size&lt;=m.Size),
    'Invalid image bounds.','イメージの範囲が不正です。')
  local info={base=b,size=size,ts=u32(p+8),machine=u16(p+4),sections={}}
  for i=0,n-1 do
    local a=p+24+optional+i*40
    local va,vs,raw,ch=u32(a+12),u32(a+8),u32(a+16),u32(a+36)
    need(va and vs and raw and ch,'Unreadable section table.','セクション表を読み取れません。')
    local len=vs&gt;0 and vs or raw
    if len&gt;0 then
      need(va&lt;size and va+len&lt;=size,'Section outside image.','セクションがイメージ範囲外です。')
      info.sections[#info.sections+1]={first=b+va,last=b+va+len,execute=(ch &amp; 0x20000000)~=0}
    end
  end
  return info
end
function G.module(alias)
  alias=alias or 'code'
  local spec=need(G.CONFIG.modules[alias],'Unknown module alias.','未定義のモジュールです。')
  local result
  for i,m in ipairs(G.modules or {}) do
    local match=spec.main and i==1 or false
    for _,name in ipairs(spec.names or {}) do if m.Name:lower()==name:lower() then match=true end end
    if spec.match and spec.match(m)==true then match=true end
    if match then need(not result,'Ambiguous module.','モジュール候補が複数あります。'); result=m end
  end
  return need(result,'Required module is not loaded.','必要なモジュールがロードされていません。')
end
function G.alive()
  if not G.pid or getOpenedProcessID()~=G.pid then return false end
  local ps=getProcesslist()
  if ps[G.pid]~=G.processName then return false end
  if G.lifeHandle then
    local ok,result=pcall(executeCodeLocalEx,'kernel32.WaitForSingleObject',G.lifeHandle,0)
    if not ok or result~=0x102 then return false end
  end
  local ok,ms=pcall(enumModules,G.pid)
  if not ok or not ms or not ms[1] or ms[1].Address~=G.mainBase or ms[1].Name~=G.mainName then return false end
  local good,pe=pcall(G.pe,ms[1])
  return good and pe.ts==G.mainPE.ts and pe.size==G.mainPE.size
end
function G.require()
  need(G.alive(),'Connection changed. Run ATTACH / ALL OFF.','接続が変わりました。アタッチ／一括解除を実行してください。')
end
function G.captureBoundary()
  G.require()
  need(type(G.CONFIG.boundary)=='function','Game adapter is not configured.','ゲーム用の解析結果が設定されていません。')
  local token=G.CONFIG.boundary()
  need(token~=nil and token~=false,'Game data is not ready.','ゲームデータの準備ができていません。')
  need(type(token)=='string' or (type(token)=='number' and token==token and math.abs(token)&lt;math.huge),'Boundary must be a stable scalar token.','境界識別子は安定した文字列か数値にしてください。')
  return token
end
-- Scope dependencies to the feature that needs them. Process identity is always required.
function G.featureBoundary(f)
  G.require()
  if f.boundary==false then return G.epoch end
  if f.boundary==nil then return G.captureBoundary() end
  need(type(f.boundary)=='function','Invalid feature boundary contract.','機能の境界定義が不正です。')
  local token=f.boundary()
  need(type(token)=='string' or (type(token)=='number' and token==token and math.abs(token)&lt;math.huge),
    'Feature data is not ready.','この機能に必要なデータの準備ができていません。')
  return token
end
function G.attachGame()
  need(type(G.CONFIG.identify)=='function','Configure the locally analysed game adapter first.','先にローカル解析済みのゲーム設定を実装してください。')
  if G.pid then
    if G.alive() then G.disableAll(); return end
    -- A signalled lifetime handle proves the old process exited; never infer from PID alone.
    local exited=false
    if G.lifeHandle then
      local ok,result=pcall(executeCodeLocalEx,'kernel32.WaitForSingleObject',G.lifeHandle,0)
      exited=ok and result==0
    end
    need(exited or (not G.lifeHandle and not getProcesslist()[G.pid]),
      'Select the original process and restore it before changing targets.',
      '対象を変更する前に元のプロセスを選択し、一括解除してください。')
    G.forgetProcess()
  end
  local rows={}
  for pid,name in pairs(getProcesslist()) do
    if not G.CONFIG.candidates or G.CONFIG.candidates(name,pid)==true then
      rows[#rows+1]={id=pid,en=name..' (PID '..pid..')',ja=name..' (PID '..pid..')',process=name}
    end
  end
  table.sort(rows,function(a,b) return a.id&lt;b.id end)
  need(#rows&gt;0,'No candidate processes.','対象候補のプロセスがありません。')
  local row=#rows==1 and rows[1] or G.choose(G.T('Select game','ゲームを選択'),rows,false)
  if not row then return end
  openProcess(row.id)
  need(getOpenedProcessID()==row.id,'Cannot open process.','プロセスを開けません。')
  local modules=enumModules(row.id)
  need(modules and modules[1],'No modules found.','モジュールが見つかりません。')
  need(G.CONFIG.identify(modules,row.id,row.process)==true,
    'The selected binary is not the supported game.','選択されたバイナリは対象ゲームと確認できません。')
  local main=G.pe(modules[1])
  -- A Windows SYNCHRONIZE handle tracks a process instance even when its PID is reused.
  local handle=executeCodeLocalEx('kernel32.OpenProcess',0x100000,0,row.id)
  need(handle and handle~=0,'Cannot track process lifetime.','プロセスの生存期間を確認できません。')
  G.pid,G.processName,G.modules=row.id,row.process,modules
  G.mainBase,G.mainName,G.mainPE=modules[1].Address,modules[1].Name,main
  G.lifeHandle=handle; G.epoch=G.epoch+1
  G.A,G.status={},{}
  G.startTimer()
  G.msg('Attached. All features are OFF.','アタッチしました。全機能は無効です。')
end

-- Runtime lookup only: no offline disassembler/dumper/probe or output path is embedded.
function G.scanPattern(pat,first,last,cap,deadline)
  need(type(pat)=='string' and pat~='','Empty pattern.','パターンが空です。')
  local fixed,patternBytes=0,0
  for token in pat:gmatch('%S+') do
    patternBytes=patternBytes+1
    need(token:match('^%x%x$') or token=='??','Use byte tokens or ??.','バイト値または??を指定してください。')
    if token~='??' then fixed=fixed+1 end
  end
  need(fixed&gt;=4,'Pattern is too broad.','パターンの識別力が不足しています。')
  need(type(first)=='number' and type(last)=='number' and first&lt;last and type(cap)=='number' and cap&gt;=1,
    'Invalid scan range or candidate limit.','探索範囲または候補上限が不正です。')
  local chunk=G.CONFIG.scanChunkBytes
  need(type(chunk)=='number' and chunk%1==0 and chunk&gt;=patternBytes and chunk&lt;=1048576,
    'Invalid scan chunk size.','探索チャンクサイズが不正です。')
  local result,pos={},first
  G.serial=G.serial+1; local symbol='gtV3_scan_'..G.serial
  local function cleanup() pcall(unregisterSymbol,symbol) end
  local ok,err=pcall(function()
    while pos&lt;last do
      need(not deadline or getTickCount()&lt;deadline,'Resolver search budget reached.','解決器の探索時間上限に達しました。')
      cleanup()
      local finish=math.min(last,pos+chunk)
      if finish-pos&lt;patternBytes then break end
      local assembled=autoAssemble(('aobscanregion(%s,%X,%X,%s)\nregistersymbol(%s)'):format(symbol,pos,finish-1,pat,symbol))
      if not assembled then
        if finish==last then break end
        pos=finish-patternBytes+1 -- overlap catches matches crossing chunk boundaries
      else
        local a=getAddressSafe(symbol); cleanup()
        need(a and a&gt;=pos and a+patternBytes&lt;=finish,'Invalid scanner result.','スキャン結果が範囲外です。')
        result[#result+1]=a
        need(#result&lt;=cap,'Too many pattern candidates.','パターン候補が多すぎます。')
        pos=a+1
      end
    end
  end)
  cleanup(); if not ok then error(err,0) end
  return result
end
function G.resolveSig(name,fresh)
  G.require()
  local sg=need(G.SIGS[name],'Unknown resolver: '..tostring(name),'未定義の解決器です: '..tostring(name))
  need(keyOK(name),'Invalid resolver name.','解決器名が不正です。')
  need(type(sg.validate)=='function','Semantic validation is required.','対象の意味的な検証が必要です。')
  local module,pe
  local available,why=pcall(function()
    G.modules=enumModules(G.pid) -- DLLs may load after attachment
    module=G.module(sg.module); pe=G.pe(module)
  end)
  if not available then G.A[name]=nil; G.status[name]=tostring(why); return nil,tostring(why) end
  local fingerprint=table.concat({G.pid,module.Address,pe.ts,pe.size,pe.machine},':')
  local cached=G.A[name]
  if cached and not fresh and cached.fingerprint==fingerprint then
    local okay,valid=pcall(function()
      for _,r in ipairs(cached.entries) do
        if sg.validate(r.target,r.site,module)~=true or (r.anchor and not equal(bytes(r.anchorAt,#r.anchor),r.anchor)) then return false end
      end
      return true
    end)
    if okay and valid then return cached.value end
  end
  G.A[name]=nil
  local budget=sg.budgetMs or G.CONFIG.resolveBudgetMs
  need(type(budget)=='number' and budget&gt;0 and budget&lt;math.huge,'Invalid resolver time budget.','探索時間上限が不正です。')
  local deadline=getTickCount()+budget
  local entries,seen={},{}
  local function accept(target,site,anchor)
    if type(target)=='number' and target&gt;0 and sg.validate(target,site,module)==true and not seen[target] then
      entries[#entries+1]={target=target,site=site,anchor=anchor}; seen[target]=true
    end
  end
  local ok,e=pcall(function()
    if sg.resolve then
      -- Semantic adapters must cooperate with this deadline; Lua cannot interrupt native calls.
      local list=sg.resolve(G.modules,deadline) or {}
      need(getTickCount()&lt;deadline,'Resolver search budget reached.','解決器の探索時間上限に達しました。')
      need(type(list)=='table','Resolver must return a candidate list.','解決器は候補一覧を返してください。')
      need(#list&lt;=G.CONFIG.maxCandidates,'Too many candidates.','候補が多すぎます。')
      for _,a in ipairs(list) do accept(a,a,nil) end
    else
      local patterns=sg.patterns or {sg}
      for _,p in ipairs(patterns) do
        for _,section in ipairs(pe.sections) do
          if section.execute~=(sg.executable==false) then
            for _,a in ipairs(G.scanPattern(p.p,section.first,section.last,G.CONFIG.maxCandidates,deadline)) do
              local site=a+(p.off or sg.off or 0); local target=site
              local kind=sg.kind or 'site'
              if kind=='call' then
                need(bytes(site,1) and readBytes(site,1)==0xE8,'Expected direct call.','直接call命令ではありません。')
                local d=need(s32(site+1),'Unreadable displacement.','変位を読み取れません。'); target=site+5+d
              elseif kind=='rip' then
                local disp,len=p.disp or sg.disp,p.length or sg.length
                need(disp and len and disp&gt;=0 and disp+4&lt;=len,'Invalid RIP contract.','RIP相対命令の定義が不正です。')
                target=site+len+need(s32(site+disp),'Unreadable displacement.','変位を読み取れません。')
              else need(kind=='site' or kind=='multi','Unknown resolver kind.','未対応の解決方式です。') end
              -- Anchor length is the full source pattern, starting at the actual match.
              local count=0 for _ in p.p:gmatch('%S+') do count=count+1 end
              local anchor=need(bytes(a,count),'Cannot read anchor.','照合元を読み取れません。')
              accept(target,site,nil)
              -- Retain full anchor separately (site may be offset from pattern start).
              local r=entries[#entries]
              if r and r.target==target and not r.anchor then r.anchor=anchor; r.anchorAt=a end
            end
          end
        end
      end
    end
    local expected=(sg.kind=='multi') and sg.count or 1
    need(type(expected)=='number' and expected&gt;=1 and #entries==expected,
      'Resolver missing or ambiguous: '..name,'未検出または複数候補です: '..name)
  end)
  if not ok then G.status[name]=tostring(e); return nil,tostring(e) end
  local values={} for i,r in ipairs(entries) do values[i]=r.target end
  local value=sg.kind=='multi' and values or values[1]
  G.A[name]={fingerprint=fingerprint,entries=entries,value=value}; G.status[name]='OK'
  return value
end
function G.needSig(name)
  local value,e=G.resolveSig(name); return need(value,e or 'Resolver unavailable.',e or '対象を解決できません。')
end

function G.hookOn(name)
  G.require()
  if G.hooks[name] then return false end
  local h=need(G.HOOKS[name],'Hook is not configured.','フックが設定されていません。')
  need(h.len and h.len&gt;=5 and h.len&lt;=64 and type(h.validate)=='function' and type(h.quiesce)=='function',
    'Hook requires length, validation and quiescence contracts.','フックには長さ・検証・実行待機の定義が必要です。')
  local site=G.needSig(h.sig)
  need(type(site)=='number','A hook requires one code site.','フック位置は1か所に限定してください。')
  for _,st in pairs(G.hooks) do need(site+h.len&lt;=st.addr or site&gt;=st.addr+#st.orig,
    'Hook sites overlap.','フック範囲が重複しています。') end
  local original=need(bytes(site,h.len),'Cannot read original code.','元コードを読み取れません。')
  need(h.validate(site,original)==true,'Hook instruction contract failed.','フック命令の検証に失敗しました。')
  need(type(h.aa)=='string' and h.aa:find('[ENABLE]',1,true) and h.aa:find('[DISABLE]',1,true),
    'Hook AA needs ENABLE and DISABLE.','AAの有効化・解除処理が必要です。')
  local script=h.aa:gsub('%$SITE',hex(site)):gsub('%$ORIG','db '..db(original)):gsub('%$NOP',h.len&gt;5 and 'nop '..(h.len-5) or '')
  -- Guard preimage in AA too. Developer must not override this assertion.
  script=script:gsub('%[ENABLE%]','[ENABLE]\nassert('..hex(site)..','..db(original)..')',1)
  local ok,info=autoAssemble(script)
  need(ok,'Hook assembly failed.','フックの組み立てに失敗しました。')
  local patched=bytes(site,h.len)
  G.hooks[name]={addr=site,orig=original,patch=patched,script=script,info=info,pid=G.pid}
  need(patched and not equal(original,patched),'Hook bytes were not changed.','フックの書き込みを確認できません。')
  G.A[h.sig]=nil
  return true
end
function G.hookOff(name)
  local st=G.hooks[name]; if not st then return end
  G.require()
  need(st.pid==G.pid,'Hook belongs to another process.','別プロセスのフックです。')
  need(G.HOOKS[name].quiesce(st)==true,'Hook execution has not drained.','フックの実行完了を確認できません。')
  local now=bytes(st.addr,#st.orig)
  need(equal(now,st.patch) or equal(now,st.orig),
    'Code ownership changed; restore was stopped.','コードの所有状態が変化したため復元を停止しました。')
  need(autoAssemble(st.script,st.info),'Hook restoration failed; state retained.','フック復元に失敗しました。状態を保持します。')
  need(equal(bytes(st.addr,#st.orig),st.orig),'Restored bytes do not match.','復元したバイトが一致しません。')
  G.hooks[name]=nil
end
local function usedHook(name)
  for key in pairs(G.active) do for _,h in ipairs(G.F[key].hooks or {}) do if h==name then return true end end end
  return false
end
function G.feature(key,on)
  G.require()
  local f=need(G.F[key],'Feature is not configured.','機能が設定されていません。')
  if on then
    if G.active[key] then return end
    if f.available then need(f.available()==true,'Feature unavailable for this binary.','このバイナリでは利用できません。') end
    local boundary=G.featureBoundary(f)
    if f.prepare then checkedCall(f.prepare) end -- resolve/validate before writing
    local added={}; G.active[key]={boundary=boundary,starting=true}
    local ok,e=pcall(function()
      for _,h in ipairs(f.hooks or {}) do
        if not G.hooks[h] then added[#added+1]=h; G.hookOn(h) end
      end
      need(G.featureBoundary(f)==boundary,'Game data changed.','ゲームデータが変化しました。')
      if f.on then
        local result=f.on()
        if result=='cancelled' then G.active[key]=nil; return end
        need(result~=false,'Operation failed.','操作に失敗しました。')
      end
      G.active[key].starting=nil
    end)
    if not ok then
      local clean,ce=pcall(function() if f.off then checkedCall(f.off) end end)
      if clean then
        G.active[key]=nil
        for i=#added,1,-1 do if not usedHook(added[i]) then local restored,re=pcall(G.hookOff,added[i]); if not restored then clean=false; ce=re end end end
      end
      if not clean then G.active[key]={boundary=boundary,cleanup=true}; error(tostring(e)..' / '..tostring(ce),0) end
      error(e,0)
    end
  else
    if not G.active[key] then return end
    G.active[key].cleanup=true -- stop scheduled tasks even if off/restore fails
    if f.off then checkedCall(f.off) end
    local previous=G.active[key]; G.active[key]=nil
    local ok,e=pcall(function() for _,h in ipairs(f.hooks or {}) do if not usedHook(h) then G.hookOff(h) end end end)
    if not ok then previous.cleanup=true; G.active[key]=previous; error(e,0) end
  end
end
function G.toggleRecord(mr,key,on)
  if G.disarming then return end
  if on then
    local id=mr and mr.ID
    local epoch=G.epoch
    if not id then return end
    G.defer(function()
      if GT~=G or G.epoch~=epoch or G.disarming then return end
      local list=getAddressList()
      local current=list.getMemoryRecordByID(id)
      if not current or not current.Active then return end
      local ok=G.userRun(function() G.feature(key,true) end)
      if GT==G and G.epoch==epoch and (not ok or not G.active[key]) then
        current=list.getMemoryRecordByID(id)
        if current and current.Active then current.Active=false end
      end
    end)
  elseif G.active[key] then
    G.userRun(function() G.feature(key,false) end)
  end
end
function G.action(key)
  G.require(); local fn=need(G.ACTIONS[key],'Action is not configured.','操作が設定されていません。')
  return fn()
end

-- WP10-style native CT editors. Values use isolated mirrors, NEVER direct game addresses.
-- A user edit touches only scratch memory; the queued commit verifies process, world,
-- object identity and unchanged old value before invoking the adapter's write contract.
function G.mirrorBase()
  G.require()
  if not G.uiBase then
    G.uiBase=need(allocateMemory(0x80000),'Cannot allocate editor mirrors.','編集用メモリを確保できません。')
    need(G.uiBase~=0,'Cannot allocate editor mirrors.','編集用メモリを確保できません。')
    G.slots={}
  end
  return G.uiBase
end
function G.allocSlot()
  local b=G.mirrorBase()
  for i=0,1023 do if not G.slots[i] then G.slots[i]=true; return i,b+i*512 end end
  G.err('Too many editor rows; close another editor.','編集行が多すぎます。ほかのエディタを閉じてください。')
end
function G.checkContext(session)
  need(session.live,'Editor was closed.','エディタは閉じられています。')
  G.require()
  need(session.epoch==G.epoch and session.pid==G.pid and G.captureBoundary()==session.boundary,
    'Game context changed. Reopen the editor.','ゲームの状態が変わりました。エディタを開き直してください。')
  local definition=G.EDITORS[session.key]
  local ctx=definition.context(session.id)
  need(ctx and ctx.identity~=nil and ctx.identity==session.identity,
    'Target identity changed. Reopen the editor.','対象の実体が変わりました。エディタを開き直してください。')
  return ctx
end
function G.normalized(field,value,reading)
  if field.kind=='string' then
    value=tostring(value)
    need(not value:find('\0',1,true) and #value&lt;=(field.maxLength or 120),
      'Text is too long or contains NUL.','文字列が長すぎるか、NULを含んでいます。')
  else
    value=tonumber(value)
    need(value and value==value and math.abs(value)&lt;math.huge,
      'Enter a finite number.','有限の数値を入力してください。')
    need(math.abs(value)&lt;=9007199254740991,'Value exceeds exact numeric range.','数値の正確な表現範囲を超えています。')
    local strict=reading or field.strict or field.choices
    if field.kind=='integer' then
      if strict then need(value%1==0,'Enter an integer.','整数を入力してください。')
      else value=math.floor(value) end
    end
    if strict then
      need((not field.min or value&gt;=field.min) and (not field.max or value&lt;=field.max),
        'Value is out of range.','入力値が範囲外です。')
    else
      if field.min then value=math.max(field.min,value) end
      if field.max then value=math.min(field.max,value) end
    end
  end
  if field.choices then
    local found=false for _,r in ipairs(field.choices) do if r.value==value then found=true end end
    need(found,'Select a listed value.','一覧の値を選択してください。')
  end
  return value
end
function G.paintMirror(info,value)
  local session=info.session
  need(session.live and session.epoch==G.epoch and G.sessions[session.parentId]==session
    and session.rows[info.id]==info,'Editor was closed.','エディタは閉じられています。')
  G.require()
  info.painting=true
  local ok,e=pcall(function()
    if info.field.kind=='string' then need(writeString(info.address,tostring(value)..'\0',true), 'Mirror write failed.','表示値を更新できません。')
    else need(writeDouble(info.address,value),'Mirror write failed.','表示値を更新できません。') end
    info.last=value
  end)
  info.painting=false; if not ok then error(e,0) end
end
function G.commit(info,value,before)
  local s=info.session
  need(s.live and G.sessions[s.parentId]==s and s.rows[info.id]==info,
    'Editor was closed.','エディタは閉じられています。')
  local ctx=G.checkContext(s); value=G.normalized(info.field,value)
  local current=info.field.read(ctx)
  need(current==before or (s.key=='members' and type(current)=='number') or (info.field.liveFloat and type(current)=='number'),'Value changed in the game; refresh first.','ゲーム内で値が変化しました。再取得してください。')
  need(type(info.field.write)=='function','This field is read-only.','この項目は読み取り専用です。')
  need(info.field.write(ctx,value)==true,'Game rejected the edit.','ゲーム側の変更処理に失敗しました。')
  local fresh=G.checkContext(s)
  if info.field.liveFloat then value=string.unpack('&lt;f',string.pack('&lt;f',value)) end
  need(info.field.read(fresh)==value,'Readback differs; refresh before another edit.','変更結果が一致しません。再取得してください。')
  G.paintMirror(info,value)
end
function G.queueEdit(info,newvalue)
  if info.painting or G.disarming or not info.session.live then return end
  local s=info.session
  local ok,e=pcall(function()
    need(not info.field.readOnly,'This value is read-only.','この項目は表示専用です。')
    local before=info.last; local value=G.normalized(info.field,newvalue)
    G.checkContext(s)
    G.queue[info.id]={info=info,before=before,value=value}
  end)
  if not ok then G.msg(tostring(e),tostring(e)); pcall(G.paintMirror,info,info.last) end
end
function G.track(m,en,ja)
  m.DontSave=true; G.dynamic[m.ID]={mr=m,en=en,ja=ja}; return m
end
function G.addHdr(parent,en,ja)
  local m=getAddressList().createMemoryRecord(); m.IsGroupHeader=true
  m.Description=G.T(en,ja); m.Color=G.C_GROUP; m.Options='[moManualExpandCollapse]'
  m.appendToEntry(parent); m.Collapsed=true; return G.track(m,en,ja)
end
function G.addAction(parent,en,ja,body)
  local m=getAddressList().createMemoryRecord()
  m.Description=G.T(en,ja); m.Color=G.C_SCRIPT; m.Type=vtAutoAssembler
  m.Script='[ENABLE]\n{$lua}\nif syntaxcheck then return end\nGT.oneShot(memrec,function() '..body..' end)\n{$asm}\n[DISABLE]'
  m.appendToEntry(parent); return G.track(m,en,ja)
end
function G.clearChildren(parent)
  while parent.Count&gt;0 do
    local m=parent.Child[0]; G.clearChildren(m)
    local d=G.dynamic[m.ID]
    if d and d.info then
      G.queue[m.ID]=nil
      if G.slots then G.slots[d.info.slot]=nil end
      d.info.mr.OnValueChangedByUser=nil
    end
    local id=m.ID
    G.dynamic[id]=nil
    m.OnActivate=nil
    m.Active=false
    local current=getAddressList().getMemoryRecordByID(id)
    if current then current.destroy() end
  end
end
function G.closeEditor(parent)
  local s=G.sessions[parent.ID]
  if s then
    s.live=false
    for id in pairs(s.rows) do G.queue[id]=nil end
    G.sessions[parent.ID]=nil
  end
  G.clearChildren(parent)
  local names=G.NAMES[parent.ID]; if names then parent.Description=G.T(names[1],names[2]) end
end
function G.selectEditor(parent,key,selected)
  G.require(); local d=need(G.EDITORS[key],'Editor is not configured.','エディタが設定されていません。')
  local boundary=G.captureBoundary()
  local rows=selected and {selected} or d.rows(); need(type(rows)=='table' and #rows&gt;0,'No editable targets.','編集対象がありません。')
  local chosen=selected
  if key=='items' then need(chosen,'Item list context is missing.','所持品一覧を取得できません。')
  elseif not chosen then chosen=G.choose(G.T(d.en,d.ja),rows,false) end
  if not chosen then return false end
  need(G.captureBoundary()==boundary,'Game data changed while choosing.','選択中にゲームデータが変化しました。')
  local ctx=need(d.context(chosen.id),'Target is unavailable.','対象を取得できません。')
  need(ctx.identity~=nil,'Editor identity contract is missing.','対象の同一性確認が未実装です。')
  G.closeEditor(parent)
  local session={key=key,id=chosen.id,identity=ctx.identity,boundary=boundary,
    epoch=G.epoch,pid=G.pid,parent=parent,parentId=parent.ID,rows={},live=true}
  G.sessions[parent.ID]=session
  local ok,e=pcall(function()
    session.label={chosen.en or chosen.name,chosen.ja or chosen.name}
    local suffix=G.T(session.label[1],session.label[2]); parent.Description=G.T(d.en,d.ja)..(suffix~='' and (' : '..suffix) or '')
    if key~='addItems' and key~='items' then G.addAction(parent,'Change target...','対象を変更...',('GT.selectEditor(getAddressList().getMemoryRecordByID(%d),%q)'):format(parent.ID,key)) end
    G.addAction(parent,'Refresh values','値を再取得','GT.refreshEditors()')
    local groups={}
    local fields=d.fieldsFor and d.fieldsFor(d.fields) or d.fields
    for _,field in ipairs(fields) do
      if not field.available or field.available(ctx)==true then
        local value=G.normalized(field,field.read(ctx),true)
        local slot,address=G.allocSlot()
        local m=getAddressList().createMemoryRecord()
        local info={mr=m,id=m.ID,session=session,field=field,slot=slot,address=address}
        m.Description=G.T(field.en,field.ja); m.Address=hex(address)
        if field.kind=='string' then
          need((field.maxLength or 120)&lt;=120,'Mirror text limit is 120 UTF-8 bytes.','表示文字列の上限はUTF-8で120バイトです。')
          m.Type=vtString; m.String.Size=field.maxLength or 120; m.String.Unicode=true
        else m.Type=vtDouble end
        local owner=parent
        if field.category then
          owner=groups[field.category]
          if not owner then
            owner=G.addHdr(parent,field.groupEn,field.groupJa)
            if key=="items" then owner.Collapsed=false end
            groups[field.category]=owner
          end
        end
        m.appendToEntry(owner); G.track(m,field.en,field.ja).DontSave=true
        G.dynamic[m.ID].info=info; session.rows[m.ID]=info
        G.paintMirror(info,value)
        G.applyChoices(info)
        m.OnActivate=function(_,before) return not before end -- no native freeze of mirrors
        m.OnValueChangedByUser=function(_,old,new)
          -- Read the numeric mirror, avoiding locale-dependent decimal strings.
          G.queueEdit(info,field.kind=='string' and new or tonumber(new) or readDouble(address))
        end
      end
    end
    G.checkContext(session)
    parent.Collapsed=false
  end)
  if not ok then G.closeEditor(parent); error(e,0) end
  return true
end
function G.openEditorTarget(parent,key,target)
  G.editorTargets=G.editorTargets or {}
  if parent.Active then return G.selectEditor(parent,key,target) end
  G.editorTargets[parent.ID]=target
  parent.Active=true
  G.editorTargets[parent.ID]=nil
end
function G.openEditor(parent,key)
  local target=G.editorTargets and G.editorTargets[parent.ID]
  -- The list can replace a parent before or during the modal selection.
  local id,epoch=parent.ID,G.epoch
  G.defer(function()
    if GT~=G or G.epoch~=epoch or G.disarming then return end
    local list=getAddressList()
    local current=list.getMemoryRecordByID(id)
    if not current or not current.Active then return end
    local ok,result=G.userRun(function() return G.selectEditor(current,key,target) end)
    if GT==G and G.epoch==epoch and (not ok or result==false) then
      current=list.getMemoryRecordByID(id)
      if current and current.Active then current.Active=false end
    end
  end)
end
function G.refreshEditors(identityOnly)
  local stale={}
  for _,s in pairs(G.sessions) do
    local ok,e=pcall(function()
      local ctx=G.checkContext(s)
      for id,info in pairs(s.rows) do
        if not identityOnly and not G.queue[id] then G.paintMirror(info,G.normalized(info.field,info.field.read(ctx),true)) end
      end
    end)
    if not ok then stale[#stale+1]={session=s,error=e} end
  end
  for _,v in ipairs(stale) do
    local session=v.session
    if session.live and G.sessions[session.parentId]==session then
      local list=getAddressList()
      local parent=list.getMemoryRecordByID(session.parentId)
      if parent then
        G.closeEditor(parent)
        parent=list.getMemoryRecordByID(session.parentId)
        if parent and parent.Active and not G.sessions[session.parentId] then parent.Active=false end
      end
      G.msg(tostring(v.error),tostring(v.error))
    end
  end
end
function G.applyChoices(info)
  if not info.field.choices then return end
  local lines={}
  for _,v in ipairs(info.field.choices) do lines[#lines+1]=tostring(v.value)..':'..G.T(v.en,v.ja) end
  local m=info.mr; m.DropDownList.Text=table.concat(lines,'\n')
  m.DropDownReadOnly=true; m.DropDownDescriptionOnly=true; m.DisplayAsDropDownListItem=true
end

-- Standard selection UI: Ctrl/Shift, shown/all selection controls,
-- stable-ID state, count, quantity, and separate Back/Cancel results. No game writes.
-- Checkboxes are an opt-in exception for a specifically suitable use case; never choose them by default.
-- options={checks,back,state={selected={},query=...,quantity=...},quantity={min,max,default}}
-- Returns row(s), {reason='ok'|'back'|'cancel',quantity=...,state=state}.
-- Back preserves draft; Cancel restores the input state. Single-choice API stays compatible.
function G.applyLang()
  local al=getAddressList()
  for i=0,al.Count-1 do
    local m=al.getMemoryRecord(i); local names=G.NAMES[m.ID]
    if names and not G.sessions[m.ID] then m.Description=G.T(names[1],names[2]) end
  end
  for _,d in pairs(G.dynamic) do d.mr.Description=G.T(d.en,d.ja); if d.info then G.applyChoices(d.info) end end
  for _,s in pairs(G.sessions) do
    local d=G.EDITORS[s.key]; local suffix=G.T(s.label[1],s.label[2]); s.parent.Description=G.T(d.en,d.ja)..(suffix~='' and (' : '..suffix) or '')
  end
end
function G.setLang(lang) need(lang=='en' or lang=='ja','Invalid language.','言語設定が不正です。'); G.lang=lang; G.applyLang() end
function G.initCollapse()
  local al=getAddressList()
  for i=0,al.Count-1 do
    local m=al.getMemoryRecord(i)
    if m.Count&gt;0 then
      local p=m; local settings=false
      while p do if p.ID==2 then settings=true break end p=p.Parent end
      m.Collapsed=not settings
    end
  end
end
local function dismissPickers()
  G.pickerGeneration=(G.pickerGeneration or 0)+1
  local forms={}
  for form in pairs(G.forms) do forms[#forms+1]=form end
  for _,form in ipairs(forms) do
    if G.forms[form] then form.ModalResult=2 end
  end
end
local function disableAll(reloading)
  G.require(); G.cancelDeferred(); G.queue={}; G.resumeFeatures=nil
  dismissPickers()
  local errors={}; local keys={}
  for key in pairs(G.active) do keys[#keys+1]=key end
  for _,key in ipairs(keys) do local ok,e=pcall(G.feature,key,false); if not ok then errors[#errors+1]=tostring(e) end end
  local orphan={} for h in pairs(G.hooks) do if not usedHook(h) then orphan[#orphan+1]=h end end
  for _,h in ipairs(orphan) do local ok,e=pcall(G.hookOff,h); if not ok then errors[#errors+1]=tostring(e) end end
  need(#errors==0,table.concat(errors,'\n'),table.concat(errors,'\n'))
  if reloading then return end
  local parents={} for _,s in pairs(G.sessions) do parents[#parents+1]=s.parent end
  for _,p in ipairs(parents) do G.closeEditor(p) end
  do
    local al=getAddressList()
    local ids={}
    for i=al.Count-1,0,-1 do
      local row=al.getMemoryRecord(i)
      if row and row.ID&gt;=100 and row.Active then ids[#ids+1]=row.ID end
    end
    for _,id in ipairs(ids) do
      -- Disable callbacks can delete other rows and invalidate list indices.
      local row=al.getMemoryRecordByID(id)
      if row and row.Active then row.Active=false end
    end
  end
  G.history={}; G.msg('All features are OFF.','全機能を解除しました。')
end
function G.disableAll(reloading)
  if G.disarming then return end
  G.disarming=true
  local ok,err=pcall(disableAll,reloading)
  G.disarming=false
  if not ok then error(err,0) end
end
function G.stopTimer()
  local timer=G.timer
  G.timer=nil
  if timer then
    pcall(function()
      timer.Enabled=false
      timer.OnTimer=nil
      timer.destroy()
    end)
  end
end
local featureRecordIds={God=110, BloodLoss=111, oneHitKill=124, InjuriesAndDisease=112, Fatigue=113, Hunger=114, Thirst=115, SleepDeprivation=116, Infection=117, BodyTemperatureInCelsius=118, ignoreWeight=119, keepAmmo=120, stealthMode=121, xpMultiplier=122, memberScope=123, moveMultiplier=211, memberApproval=312, npcApproval=313, keepItems=412, noMaterials=452, accelerateWork=451, alwaysDay=512, weatherLock=511, lockTime=513, noFog=514, revealMap=515, borderlessWindow=605}
function G.tick()
  if not G.pid then return end
  if not next(G.active) and not next(G.queue) and not next(G.resumeFeatures or {}) then return end
  if not G.alive() then
    G.queue={}; G.resumeFeatures=nil; G.stopTimer()
    -- Do not destroy mirror rows against a different target or restore old bytes there.
    -- Clear callbacks and editor records only; allocated pages remain owned by old process.
    local parents={} for _,s in pairs(G.sessions) do parents[#parents+1]=s.parent end
    for _,p in ipairs(parents) do G.closeEditor(p) end
    G.msg('Process changed/exited. Automatic work stopped. Select the original process to restore.',
      'プロセスが変化／終了しました。自動処理を停止しました。復元時は元のプロセスを選択してください。')
    return
  end
  local pending=G.queue; G.queue={}
  for _,job in pairs(pending) do
    local info=job.info
    local session=info.session
    if session.live and G.epoch==session.epoch then
      local ok,e=pcall(G.commit,info,job.value,job.before)
      if not ok then
        G.msg(tostring(e),tostring(e))
        if session.live and G.epoch==session.epoch then pcall(G.paintMirror,info,info.last) end
      end
    end
  end
  local worldChecked,worldOK,world
  local worldChanged={}
  local stopped,resumed=0,0
  local pendingResume=G.resumeFeatures or {}
  G.resumeFeatures=pendingResume
  for key,epoch in pairs(pendingResume) do
    local row=getAddressList().getMemoryRecordByID(featureRecordIds[key])
    if epoch~=G.epoch or not row or not row.Active then
      pendingResume[key]=nil
    else
      local ready=pcall(G.featureBoundary,G.F[key])
      if ready then
        pendingResume[key]=nil
        local ok,e=pcall(G.feature,key,true)
        if ok then
          resumed=resumed+1
        else
          if not G.active[key] then row.Active=false end
          G.msg(('Could not reapply %s: %s'):format(row.Description,tostring(e)),
                ('%s を再適用できませんでした：%s'):format(row.Description,tostring(e)))
        end
      end
    end
  end
  if resumed&gt;0 then
    G.msg(('Reapplied %d features.'):format(resumed),('%d件の機能を再適用しました。'):format(resumed))
  end
  local keys={} for k in pairs(G.active) do keys[#keys+1]=k end
  for _,key in ipairs(keys) do
    local st,f=G.active[key],G.F[key]
    if st and not st.cleanup and not st.starting then
      local ok,e=pcall(function()
        local token
        if f.boundary==false then token=G.epoch
        elseif f.boundary then token=f.boundary()
        else
          if not worldChecked then
            worldOK,world=pcall(G.CONFIG.boundary)
            worldChecked=true
          end
          if worldOK then token=world end
        end
        if token~=st.boundary then error(worldChanged,0) end
        if f.task then checkedCall(f.task) end
      end)
      if not ok then
        local clean,ce=pcall(G.feature,key,false)
        if not clean and G.active[key] then G.active[key].cleanup=true end
        if clean and key=='borderlessWindow' then
          local row=getAddressList().getMemoryRecordByID(605)
          if row and row.Active then row.Active=false end
        end
        if e==worldChanged then
          if clean then
            local id=featureRecordIds[key]
            local row=id and getAddressList().getMemoryRecordByID(id)
            if row and row.Active then pendingResume[key]=G.epoch end
            stopped=stopped+1
          else
            G.msg(('Could not disable %s after the world changed: %s'):format(key,tostring(ce)),
                  ('ワールド変更後に %s を解除できませんでした：%s'):format(key,tostring(ce)))
          end
        else
          G.msg(tostring(e)..(clean and '' or ' / '..tostring(ce)),tostring(e)..(clean and '' or ' / '..tostring(ce)))
        end
      end
    end
  end
  if stopped&gt;0 then
    G.msg(('World/save changed. Suspended %d features.'):format(stopped),
          ('ロードのため%d件の機能を一時停止しました。'):format(stopped))
  end
  local now=getTickCount()
  if not G.lastRetry or now-G.lastRetry&gt;=G.CONFIG.retry then
    G.lastRetry=now
    -- Only previously requested/missing resolvers; never a blind whole-game probe.
    local names={}
    for name,state in pairs(G.status) do if state~='OK' then names[#names+1]=name end end
    table.sort(names)
    local count=math.min(#names,G.CONFIG.retryBatch or 1)
    for _=1,count do
      G.retryCursor=(G.retryCursor or 0)%#names+1
      pcall(G.resolveSig,names[G.retryCursor],true)
    end
  end
end
function G.startTimer()
  G.stopTimer(); local t=createTimer(nil,false); G.timer=t; t.Interval=G.CONFIG.tick
  t.OnTimer=function()
    if G.timer~=t or G.inTick or G.disarming then return end
    G.inTick=true; local ok,e=pcall(G.tick); G.inTick=false
    if not ok then G.stopTimer(); G.queue={}; G.msg(tostring(e),tostring(e)) end
  end
  t.Enabled=true
end
function G.forgetProcess(reloading)
  G.stopTimer(); G.cancelDeferred(); G.resumeFeatures=nil
  dismissPickers()
  if not reloading then
    local parents={} for _,s in pairs(G.sessions) do parents[#parents+1]=s.parent end
    for _,p in ipairs(parents) do G.closeEditor(p) end
  end
  G.sessions,G.dynamic={},{}
  if G.lifeHandle then executeCodeLocalEx('kernel32.CloseHandle',G.lifeHandle) end
  G.lifeHandle=nil; G.pid=nil; G.modules=nil; G.uiBase=nil; G.slots=nil
  G.active,G.hooks,G.A,G.status,G.queue,G.history={},{},{},{},{},{}
  G.retryCursor,G.lastRetry=nil,nil
  G.epoch=G.epoch+1
end
function G.shutdown(reloading,closing)
  if G.pid then
    if G.alive() then
      G.disableAll(reloading or closing)
      -- During CE close the address list can still read these mirrors.
      -- Retain the buffer until the target exits if the list is being closed.
      if G.uiBase and not closing then
        need(deAlloc(G.uiBase),'Cannot free editor mirrors.','編集用メモリを解放できません。')
        G.uiBase=nil
        G.slots=nil
      end
    else
      local exited=false
      if G.lifeHandle then
        local ok,r=pcall(executeCodeLocalEx,'kernel32.WaitForSingleObject',G.lifeHandle,0); exited=ok and r==0
      end
      need(exited,'Select the original process and restore before reloading this table.',
        'テーブル再読込前に元のプロセスを選択して解除してください。')
    end
  end
  G.forgetProcess(reloading or closing)
  if not closing and G.removeCloseGuard then G.removeCloseGuard() end
end
function G.luaEngine(on)
  local form=getLuaEngine(); if on then form.show(); form.bringToFront() else form.hide() end
end
function G.envReport()
  G.msg('Template '..G.VERSION..' / '..G.CHEATVER,'テンプレート '..G.VERSION..' / '..G.CHEATVER)
  G.msg('PID: '..tostring(G.pid)..' / opened: '..tostring(getOpenedProcessID()),
    'PID: '..tostring(G.pid)..' / 接続先: '..tostring(getOpenedProcessID()))
  if G.pid and G.alive() then
    for _,m in ipairs(enumModules(G.pid)) do
      local ok,p=pcall(G.pe,m)
      if ok then print(('%s base=%X size=%X machine=%X PE-id=%X'):format(m.Name,m.Address,p.size,p.machine,p.ts)) end
    end
  end
end
function G.statusReport()
  local n=0 for _ in pairs(G.active) do n=n+1 end
  local h=0 for _ in pairs(G.hooks) do h=h+1 end
  G.msg(('Features: %d / Hooks: %d'):format(n,h),('有効機能: %d / フック: %d'):format(n,h))
  for name,value in pairs(G.status) do print(name..': '..value) end
end
-- [GAME] Names are inserted by the build tool together with the XML tree.

G.NAMES = {
  [412] = {"Keep items","アイテム減らない"},
  [211] = {"Player movement speed multiplier","プレイヤスピード倍率操作"},
  [452] = {"Craft without materials","作成にアイテム必要なし"},
  [450] = {"05. Craft and build","05. クラフト・ビルド"},
  [451] = {"Instant craft and build","即クラフト・ビルド"},
  [605] = {"Borderless window","ボーダレスウィンドウにする"},
  [604] = {"Unlock all Steam achievements...","Steam実績全解除..."},
  [515] = {"Reveal entire map","MAP全開放"},
  [514] = {"Disable fog","霧無効"},
  [513] = {"Lock time of day","時刻を固定"},
  [512] = {"Always daytime","常に昼"},
  [511] = {"Lock weather front","天候固定"},
  [124] = {"One-hit kill","一撃必殺"},
  [123] = {"Apply active convenience features to members","有効にした便利機能をメンバーに"},
  [122] = {"XP gain multiplier","XP取得倍率操作"},
  [121] = {"Stealth mode","ステルスモード"},
  [120] = {"Keep ammunition and arrows","弾・弓減らない"},
  [119] = {"Ignore carry limit","最大重量無視"},
  [118] = {"Normal body temperature","体温正常"},
  [117] = {"Prevent infection","感染しない"},
  [116] = {"No sleep deprivation","睡眠不足なし"},
  [115] = {"No thirst","喉乾かない"},
  [114] = {"No hunger","空腹なし"},
  [113] = {"No fatigue","疲労なし"},
  [112] = {"No injuries or disease","怪我・病気しない"},
  [111] = {"Unlimited health","体力無限"},
  [110] = {"Invulnerability","無敵"},
  [317] = {"All NPCs","全NPC一覧"},
  [316] = {"All members","全メンバー一覧"},
  [315] = {"NPC editor","NPCエディタ"},
  [314] = {"Fully heal members","メンバー全回復"},
  [313] = {"Max all NPC approval","全NPCの好感度最大"},
  [312] = {"Max member approval","メンバー好感度最大"},
  [311] = {"Member editor","メンバーエディタ"},
  [1] = {"[ATTACH / ALL OFF] Select and verify game","【アタッチ／一括解除】ゲームを選択・確認"},
  [2] = {"0. Settings","0. 設定"},
  [3] = {"Language / 言語設定","言語設定 / Language"},
  [4] = {"English (initial / default)","English（初期表示・既定）"},
  [5] = {"Japanese / 日本語","日本語（切替）"},
  [6] = {"README","はじめに"},
  [7] = {"Initial language: English. Load a local single-player save before enabling features.","初期表示は英語です。機能を有効にする前にシングルプレイのセーブをロードしてください。"},
  [8] = {"Open an editor, select a target, then double-click its value.","エディタで対象を選び、値をダブルクリックして編集します。"},
  [9] = {"Game / world / target changes cancel pending edits.","ゲーム・プレイ状態・対象の変化時は変更を停止します。"},
  [10] = {"Other builds/stores need semantic validation; compatibility is not guaranteed.","別版・別ストアは意味的検証が必要です。互換性を保証するものではありません。"},
  [11] = {"Editor values refresh on request; automatic refresh is opt-in.","値の再取得は手動です。自動再取得は必要時に明示設定します。"},
  [12] = {"Diagnostics","診断"},
  [13] = {"Lua Engine (ON = show / OFF = hide)","Lua Engine（ONで開く／OFFで閉じる）"},
  [14] = {"Validate requested resolvers (read only)","解決器を確認（読み取り）"},
  [15] = {"Environment / connection info","環境・接続情報"},
  [16] = {"Feature / resolver status","機能・解決状態"},
  [100] = {"01. Gameplay","01. 便利機能"},
  [200] = {"02. Player","02. プレイヤー"},
  [210] = {"Player editor","プレイヤーエディタ"},
  [300] = {"03. Community","03. コミュニティ"},
  [310] = {"Community editor","コミュニティエディタ"},
  [400] = {"04. Items","04. アイテム"},
  [410] = {"Item quantity editor","アイテム個数変更"},
  [411] = {"Add items (category and quantity)","アイテム追加(カテゴリ別　個数)"},
  [500] = {"06. World","06. ワールド"},
  [510] = {"Set weather","天候指定"},
  [600] = {"07. Other","07. その他"},
}
local fields, statics, methods = {}, {}, {}
local function field(class,name)
  local key=class..'.'..name
  if not fields[key] then
    local c=mono_findClass('',class)
    assert(c and c~=0,class)
    for _,f in ipairs(assert(mono_class_enumFields(c,true))) do
      if f.name==name then
        fields[key]=f.offset
        if f.isStatic then statics[key]=assert(mono_class_getStaticFieldAddress(c))+f.offset end
        break
      end
    end
  end
  return assert(fields[key],key)
end
local function static(class,name)
  field(class,name)
  return assert(statics[class..'.'..name])
end
local function ref(address)
  local value=readPointer(address)
  if value and value~=0 then return value end
end
local exe='Survivalist Invisible Strain.exe'
G.CONFIG.title='Survivalist: Invisible Strain'
G.CONFIG.candidates=function(name) return name:lower()==exe:lower() end
G.CONFIG.identify=function(modules)
  if modules[1].Name:lower()~=exe:lower() then return false end
  local path=modules[1].PathToFile
  if not path then return false end
  local dir=path:match('^(.*)[/\\]')
  local dll=dir and dir..'\\Survivalist Invisible Strain_Data\\Managed\\Assembly-CSharp.dll'
  if not dll or (md5file(dll) or ''):lower()~='6c6b95167c02f08fdb9f622381c52664' then return false end
  fields,statics,methods={},{},{}
  G.listCountOffset,G.listClass=nil,nil
  if mono_AttachedProcess==getOpenedProcessID() and libmono and not libmono.monopipe then
    mono_AttachedProcess=nil
  end
  assert(not isPaused(),G.T('Resume the game before connecting.','ゲームの一時停止を解除してください。'))
  local mono=LaunchMonoDataCollector()
  if libmono and libmono.monopipe then
    -- Let CE's timeout handler return an error to the caller without a modal dialog.
    libmono.monopipe.OnAboutToTimeout=function() return true end
  end
  return mono and mono~=0 and readInteger(static('GameImpl','ReleaseVersion'))==286
end
local attachLocalGame=G.attachGame
local manualLanguage=false
local setLanguage=G.setLang
G.setLang=function(language)
  manualLanguage=true
  return setLanguage(language)
end
G.attachGame=function()
  local before=G.epoch
  attachLocalGame()
  if G.epoch~=before and G.pid and not manualLanguage then
    local app=ref(static('GameImpl','Instance'))
    local settings=app and ref(app+field('GameImpl','Settings'))
    local language=settings and readInteger(settings+field('SettingsData','Language'))
    if language==13 then setLanguage('ja')
    elseif language==0 then setLanguage('en') end
  end
end
local function session()
  local s=ref(static('Session','Instance'))
  if not s or readInteger(s+field('Session','State'))~=3 then return end
  local l=ref(s+field('Session','NetworkPlayerIDs'))
  if not l then return end
  if not G.listCountOffset then
    local c=mono_object_getClass(l)
    for _,f in ipairs(mono_class_enumFields(c,false) or {}) do
      if f.name=='_size' then G.listCountOffset,G.listClass=f.offset,c; break end
    end
  end
  if not G.listCountOffset or readInteger(l+G.listCountOffset)~=0 then return end
  return s
end
G.CONFIG.boundary=function()
  local s=session()
  if s then return ('%X/%d'):format(s,assert(readInteger(s+field('Session','GameUniqueId')))) end
end
local function player()
  local h=ref(static('Hud','Instance'))
  local p=h and ref(h+field('Hud','LocalControlledCharacter'))
  if p and (readInteger(p+field('Character','Consciousness')) or 3)&lt;3
       and readInteger(p+field('Character','Infection'))==0 then return p end
end
local function managedText(address)
  local s=ref(address)
  return s and mono_string_readString(s) or ''
end
local function method(class,name,count,firstType,firstName)
  local key=('%s.%s/%d/%s/%s'):format(class,name,count,tostring(firstType),tostring(firstName))
  if methods[key] then return methods[key] end
  local c=mono_findClass('',class)
  assert(c and c~=0,class)
  local result
  local label=('%s.%s/%d'):format(class,name,count)
  for _,m in ipairs(assert(mono_class_enumMethods(c,false),'Cannot enumerate '..class)) do
    if m.name==name then
      local sig=assert(mono_method_get_parameters(m.method),'Cannot read signature: '..label)
      if #sig.parameters==count
        and (not firstType or sig.parameters[1].type==firstType)
        and (not firstName or sig.parameters[1].name==firstName) then
        assert(not result,'Ambiguous method: '..label)
        result=m.method
      end
    end
  end
  methods[key]=assert(result,'No matching method: '..label..(' (first type=%s, first name=%s)'):format(tostring(firstType),tostring(firstName)))
  return result
end
local function invoke(m,object,args)
  local result,exception=mono_invoke_method(nil,m,object,args)
  if exception and exception~=0 and exception~='' then error(tostring(exception),0) end
  return result
end
-- UnityEngine.CoreModule.dll: FullScreenMode.Windowed = 3.
local windowState,windowBuffer
local function windowAPI(name,...)
  local api=getAddressSafe('user32.'..name,true)
  assert(type(api)=='number' and api~=0,'Windows API unavailable: '..name)
  local ok,result=pcall(executeCodeLocalEx,api,...)
  if not ok then error('user32.'..name..': '..tostring(result),0) end
  assert(type(result)=='number','Windows API returned no result: '..name)
  return result
end
local function screenModeAccessors()
  local class=assert(mono_findClass('UnityEngine','Screen'))
  local getter,setter
  for _,entry in ipairs(assert(mono_class_enumMethods(class))) do
    if entry.name=='get_fullScreenMode' then getter=entry.method end
    if entry.name=='set_fullScreenMode' then setter=entry.method end
  end
  return assert(getter),assert(setter)
end
local function screenMode(getter)
  local mode=invoke(getter,0,{})
  mode=type(mode)=='string' and tonumber(mode:match('%((%d+)%)$')) or mode
  assert(type(mode)=='number' and mode%1==0 and mode&gt;=0 and mode&lt;=3,'Invalid screen mode')
  return mode
end
local function gameWindow()
  local hwnd=findWindow(nil,G.CONFIG.title)
  if hwnd and hwnd~=0 and getWindowProcessID(hwnd)==G.pid then return hwnd end
  error(G.T('Game window unavailable.','ゲームウィンドウを取得できません。'),0)
end
local function windowRect(api,hwnd)
  assert(windowAPI(api,hwnd,windowBuffer)~=0,api)
  local r={}
  for i=0,3 do
    local v=assert(readIntegerLocal(windowBuffer+i*4))
    r[i+1]=v&gt;=0x80000000 and v-0x100000000 or v
  end
  return r
end
local function placeWindow(hwnd,style,r)
  windowAPI('SetWindowLongPtrW',hwnd,-16,style)
  assert((windowAPI('GetWindowLongPtrW',hwnd,-16)&amp;0xffffffff)==style,'Window style did not change')
  assert(windowAPI('SetWindowPos',hwnd,0,0,0,0,0,0x37)~=0,'SetWindowPos frame update failed')
  assert(windowAPI('SetWindowPos',hwnd,0,r[1],r[2],r[3]-r[1],r[4]-r[2],0x14)~=0,
    'SetWindowPos resize failed')
  local actual=windowRect('GetWindowRect',hwnd)
  for i=1,4 do
    assert(actual[i]==r[i],('Window bounds mismatch: expected %d,%d,%d,%d; got %d,%d,%d,%d'):format(
      r[1],r[2],r[3],r[4],actual[1],actual[2],actual[3],actual[4]))
  end
end
G.F.borderlessWindow={
  boundary=false,
  on=function()
    local getter,setter=screenModeAccessors()
    local hwnd=gameWindow()
    local wasMinimized=windowAPI('IsIconic',hwnd)~=0
    if not windowBuffer then
      local buffer=allocateSharedMemoryLocal('SISWindowRect_'..getTickCount(),64)
      assert(type(buffer)=='number' and buffer~=0,'Window buffer unavailable')
      windowBuffer=buffer
    end
    local monitor=assert(windowAPI('MonitorFromWindow',hwnd,2))
    assert(monitor~=0 and writeIntegerLocal(windowBuffer,40) and windowAPI('GetMonitorInfoW',monitor,windowBuffer)~=0,'Monitor bounds unavailable')
    local bounds={}
    for i=1,4 do
      local v=assert(readIntegerLocal(windowBuffer+i*4))
      bounds[i]=v&gt;=0x80000000 and v-0x100000000 or v
    end
    windowState={epoch=G.epoch,hwnd=hwnd,mode=screenMode(getter),getter=getter,setter=setter,bounds=bounds,wasMinimized=wasMinimized}
    return true
  end,
  task=function()
    local s=assert(windowState)
    if not s.style then
      local hwnd=gameWindow()
      s.hwnd=hwnd
      if windowAPI('IsIconic',hwnd)~=0 then
        s.restoreAttempts=(s.restoreAttempts or 0)+1
        assert(s.restoreAttempts&lt;=12,G.T('The game did not restore in the background.',
          'ゲームウィンドウを背面で復元できませんでした。'))
        if s.restoreHandle~=hwnd then
          assert(windowAPI('ShowWindowAsync',hwnd,4)~=0,'ShowWindowAsync failed')
          s.restoreHandle=hwnd
        end
        return true
      end
      s.restoreHandle=nil
      if screenMode(s.getter)~=3 then
        s.attempts=(s.attempts or 0)+1
        assert(s.attempts&lt;=12,G.T('Unity did not switch to windowed mode.',
          'ウィンドウ表示への切り替えを確認できません。'))
        if not s.modeRequested then
          s.modeRequested=true
          invoke(s.setter,0,{{type=vtDword,value=3}})
        end
        return true
      end
      s.rect=windowRect('GetWindowRect',s.hwnd)
      local style=windowAPI('GetWindowLongPtrW',s.hwnd,-16)&amp;0xffffffff
      assert(style~=0,'Window style unavailable')
      s.style=style
      placeWindow(s.hwnd,style&amp;(~0x00cf0000)&amp;0xffffffff,s.bounds)
    end
    return true
  end,
  off=function()
    local s=windowState
    if not s then return true end
    if s.epoch==G.epoch then
      if s.style and getWindowProcessID(s.hwnd)==G.pid then placeWindow(s.hwnd,s.style,s.rect) end
      if s.modeRequested and s.mode~=3 then invoke(s.setter,0,{{type=vtDword,value=s.mode}}) end
      if s.wasMinimized and getWindowProcessID(s.hwnd)==G.pid then
        assert(windowAPI('ShowWindowAsync',s.hwnd,7)~=0,'Could not restore minimized state')
      end
    end
    windowState=nil
    return true
  end,
}
-- Preserve which stage failed even if CE raises a generic access violation.
for _,operation in ipairs({'on','task','off'}) do
  local run=G.F.borderlessWindow[operation]
  G.F.borderlessWindow[operation]=function()
    local ok,result=pcall(run)
    if not ok then
      local stage=({on='prepare',task='apply',off='restore'})[operation]
      error(('Borderless (%s): %s'):format(stage,tostring(result)),0)
    end
    return result
  end
end
local function displayName(object)
  local name=invoke(method('BaseObject','GetDisplayNameString',0),object)
  assert(type(name)=='string',G.T('Could not read the game display name.','ゲーム内の表示名を取得できません。'))
  return name
end
local function itemDisplayNames(name)
  if type(name)=='string' and name:match('%S') then
    return name,name
  end
  return 'No in-game display name','ゲーム内表示名なし'
end
local function playerLabel(p)
  local name=displayName(p)
  return name~='' and name or G.T('Unnamed member','名前のないメンバー')
end
local function floatField(class,name,en,ja,minimum,maximum)
  return {kind='number',liveFloat=true,en=en,ja=ja,min=minimum,max=maximum,
    read=function(ctx) return readFloat(ctx.object+field(class,name)) end,
    write=function(ctx,v)
      local a=ctx.object+field(class,name)
      return writeFloat(a,v) and math.abs((readFloat(a) or math.huge)-v)&lt;0.001
    end}
end
local stats={
  {'BloodLoss','Blood loss','出血量',0,0,0.99},
  {'Fatigue','Fatigue','疲労',0,0,1},
  {'Hunger','Hunger','空腹',0,0,12600},
  {'Thirst','Thirst','渇き',0,0,3600},
  {'SleepDeprivation','Sleep deprivation','睡眠不足',0,0,12600},
  {'BodyTemperatureInCelsius','Body temperature','体温',37,20,40},
}
G.EDITORS.player={en='Player editor',ja='プレイヤーエディタ',fields={},
  rows=function()
    local p=player()
    if not p then return {} end
    local label=playerLabel(p)
    return {{id=readInteger(p+field('BaseObject','Id')),en=label,ja=label}}
  end,
  context=function(id)
    local p=player()
    if p and readInteger(p+field('BaseObject','Id'))==id then return {object=p,identity=id} end
  end}
for _,stat in ipairs(stats) do
  local key,en,ja,target,lo,hi=table.unpack(stat)
  G.EDITORS.player.fields[#G.EDITORS.player.fields+1]=floatField('Character',key,en,ja,lo,hi)
  local function maintain()
    local p=assert(player(),G.T('No active character.','操作中のキャラクターがいません。'))
    local a=p+field('Character',key)
    if readFloat(a)~=target then return writeFloat(a,target) end
    return true
  end
  G.F[key]={on=maintain}
end
function G.scanSelfTest()
  local token=G.captureBoundary()
  local p=assert(player(),G.T('No active character.','操作中のキャラクターがいません。'))
  for _,f in ipairs(G.EDITORS.player.fields) do
    print(f.en..': '..tostring(f.read({object=p})))
  end
  return token
end
local listLayouts={}
local function objects(list)
  local c=assert(mono_object_getClass(list))
  local layout=listLayouts[c]
  if not layout then
    layout={}
    for _,f in ipairs(assert(mono_class_enumFields(c,false))) do layout[f.name]=f.offset end
    listLayouts[c]=layout
  end
  local count=assert(readInteger(list+assert(layout._size)))
  assert(count&gt;=0 and count&lt;=16384,'Invalid list size')
  local result={}
  if count==0 then return result end
  local array=assert(ref(list+assert(layout._items)))
  assert(mono_arrayinstance_getCount(array)&gt;=count,'Invalid list capacity')
  local start=assert(mono_arrayinstance_getItemAddress(array,0))
  assert(mono_arrayinstance_getItemAddress(array,1)-start==8,'Invalid reference stride')
  for i=0,count-1 do result[#result+1]=assert(ref(start+8*i)) end
  return result
end
local function communities()
  local s=assert(session())
  local manager=assert(ref(s+field('Session','CommunityManager')))
  return objects(assert(ref(manager+field('CommunityManager','Communities'))))
end
local function identity(object)
  return assert(readInteger(object+field('BaseObject','Id')))
end
local function intField(class,name,en,ja,maximum,isByte)
  local read=isByte and function(a) return readBytes(a,1) end or readInteger
  local write=isByte and function(a,v) return writeBytes(a,v) end or writeInteger
  return {kind='integer',en=en,ja=ja,min=0,max=maximum,
    read=function(ctx) return read(ctx.object+field(class,name)) end,
    write=function(ctx,v)
      local a=ctx.object+field(class,name)
      return write(a,v) and read(a)==v
    end}
end
G.EDITORS.units={en='Community editor',ja='コミュニティエディタ',
  rows=function()
    local rows={}
    for _,p in ipairs(communities()) do
      local id=identity(p)
      local label=displayName(p)
      if label=='' then
        local leader=ref(p+field('Community','Leader'))
        local name=leader and displayName(leader) or ''
        label=name~='' and G.T(name..' — leader',name..'（リーダー）') or G.T('Unnamed community','名前のないコミュニティ')
      end
      rows[#rows+1]={id=id,en=label,ja=label}
    end
    return rows
  end,
  context=function(id)
    for _,p in ipairs(communities()) do
      if identity(p)==id then return {object=p,identity=id} end
    end
  end,
  fields={
    intField('Community','ExtortionPresenceThreshold','Extortion presence threshold','恐喝の人数しきい値',32),
    intField('Community','PlayerSurrenderDisabled','Disable player surrender','プレイヤーの降伏を無効化',1,true),
    intField('Community','BuildingsCantBeCaptured','Prevent building capture','建物の占領を禁止',1,true),
  }}

local god
local function restoreGod()
  if not god then return true end
  local s=ref(static('Session','Instance'))
  if s==god.session and readInteger(s+field('Session','GameUniqueId'))==god.world then
    local manager=assert(ref(s+field('Session','CharacterManager')))
    local list=assert(ref(manager+field('CharacterManager','Characters')))
    for _,p in ipairs(objects(list)) do
      if identity(p)==god.id then
        local address=p+field('Character','God')
        if not writeBytes(address,god.previous) or readBytes(address,1)~=god.previous then return false end
        break
      end
    end
  end
  god=nil
  return true
end
G.F.God={
  boundary=function()
    local s,p=G.CONFIG.boundary(),player()
    if s and p then return s..'/'..identity(p) end
  end,
  on=function()
    local s,p=assert(session()),assert(player())
    local address=p+field('Character','God')
    god={session=s,world=assert(readInteger(s+field('Session','GameUniqueId'))),
      id=identity(p),previous=assert(readBytes(address,1))}
    return writeBytes(address,1) and readBytes(address,1)==1
  end,
  off=restoreGod,
}
local function inventory()
  local p=player()
  if not p then return end
  return ref(p+field('Character','Inventory')),p
end
local function inventoryItems()
  local inv=inventory()
  if not inv then return {} end
  return objects(assert(ref(inv+field('EquipmentContainer','Contents'))))
end
G.EDITORS.items={en='Item quantity editor',ja='アイテム個数変更',
  rows=function()
    local rows={}
    for _,item in ipairs(inventoryItems()) do
      local proto=ref(item+field('Equipment','Prototype'))
      if proto then
        local id=identity(item)
        local en,ja=itemDisplayNames(displayName(item))
        rows[#rows+1]={id=id,en=en,ja=ja,category=managedText(proto+field('EquipmentPrototype','Category'))}
      end
    end
    return rows
  end,
  context=function(id)
    local inv,p=inventory()
    if not inv then return end
    for _,item in ipairs(inventoryItems()) do
      if identity(item)==id and ref(item+field('Equipment','InventoryOwner'))==p then
        return {object=item,owner=p,inventory=inv,identity=('%d/%d'):format(identity(p),id)}
      end
    end
  end,
  fields={{kind='integer',en='Quantity',ja='個数',min=1,max=9999,
    read=function(ctx) return readInteger(ctx.object+field('Equipment','Amount')) end,
    write=function(ctx,value)
      local set=method('Equipment','SetNewAmount',1)
      local cache=method('EquipmentContainer','CacheEncumbered',1)
      invoke(set,ctx.object,{value})
      invoke(cache,ctx.inventory,{ctx.owner})
      return readInteger(ctx.object+field('Equipment','Amount'))==value
    end}}}
local function steamExports(requested)
  for _,module in ipairs(enumModules(G.pid) or {}) do
    if module.Name:lower()=='steam_api64.dll' then
      local base=module.Address
      local pe=G.pe(module)
      local header=base+assert(readInteger(base+0x3C))
      local magic=readBytes(header+24,2,true)
      assert(magic and magic[1]==0x0B and magic[2]==2,'Expected Steam PE32+ module')
      local function within(rva,size)
        assert(rva and rva&gt;0 and size&gt;=0 and rva&lt;=pe.size-size,'Invalid Steam export RVA')
        return base+rva
      end
      local exportRva=assert(readInteger(header+24+112))
      local exportSize=assert(readInteger(header+24+116))
      local directory=within(exportRva,exportSize)
      assert(exportSize&gt;=40,'Invalid Steam export directory')
      local functions=assert(readInteger(directory+20))
      local count=assert(readInteger(directory+24))
      assert(functions&gt;0 and functions&lt;=65536 and count&gt;0 and count&lt;=65536,'Invalid Steam export count')
      local functionTable=within(readInteger(directory+28),functions*4)
      local names=within(readInteger(directory+32),count*4)
      local ordinals=within(readInteger(directory+36),count*2)
      local found,remaining={},0
      for _ in pairs(requested) do remaining=remaining+1 end
      for i=0,count-1 do
        local rva=assert(readInteger(names+i*4))
        local text=readString(within(rva,1),math.min(160,pe.size-rva))
        if requested[text] and not found[text] then
          local lo,hi=readBytes(ordinals+i*2,2)
          local index=assert(lo)+assert(hi)*256
          assert(index&lt;functions,'Invalid Steam export ordinal')
          local target=assert(readInteger(functionTable+index*4))
          assert(target&lt;exportRva or target&gt;=exportRva+exportSize,'Unexpected forwarded Steam export')
          found[text]=within(target,1)
          remaining=remaining-1
          if remaining==0 then return found end
        end
      end
      break
    end
  end
  G.err('Required Steam exports are unavailable.','Steam DLLの実績エクスポートを解決できません。')
end
local function steamCall(address,...)
  return assert(executeCodeEx(0,5000,address,...),'Steam API call failed')
end
G.ACTIONS.achievements=function()
  local app=assert(ref(static('GameImpl','Instance')))
  local mgr=assert(ref(static('AchievementsManager','Instance')))
  assert(readBytes(app+field('GameImpl','SteamInitialized'),1)==1 and
    readBytes(mgr+field('AchievementsManager','StatsInitialized'),1)==1,
    G.T('Steam stats are not ready.','Steam実績情報の準備ができていません。'))
  local interfaceName='SteamAPI_SteamUserStats_v012'
  local prefix='SteamAPI_ISteamUserStats_'
  local requested={[interfaceName]=true}
  for _,name in ipairs({'GetNumAchievements','GetAchievementName','GetAchievement','SetAchievement','StoreStats'}) do
    requested[prefix..name]=true
  end
  local exports=steamExports(requested)
  local interface=exports[interfaceName]
  local count,name,get,set,store=exports[prefix..'GetNumAchievements'],
    exports[prefix..'GetAchievementName'],exports[prefix..'GetAchievement'],
    exports[prefix..'SetAchievement'],exports[prefix..'StoreStats']
  local stats=steamCall(interface)
  assert(stats~=0,'Steam stats interface is unavailable')
  local n=steamCall(count,stats)
  assert(n&gt;=1 and n&lt;=256,'Unexpected Steam achievement count')
  local scratch=assert(allocateMemory(8))
  assert(scratch~=0,'Cannot allocate Steam status buffer')
  local ok,err=pcall(function()
    local pending,keys,seen={},{},{}
    for i=0,n-1 do
      local pointer=steamCall(name,stats,{type=0,value=i})
      local key=pointer~=0 and readString(pointer,256) or nil
      assert(key and key~='' and not seen[key],
        G.T(('Invalid achievement name at index %d.'):format(i),
          ('Steam実績名を取得できません。インデックス %d'):format(i)))
      seen[key]=true
      local arg={type=3,value=key}
      assert((steamCall(get,stats,arg,scratch)&amp;0xFF)==1,'Cannot read Steam achievement status: '..key)
      local achieved=readBytes(scratch,1)
      assert(achieved==0 or achieved==1,'Invalid Steam achievement status: '..key)
      keys[#keys+1]=key
      if achieved==0 then pending[#pending+1]=key end
    end
    if #pending==0 then
      G.msg('All Steam achievements are already unlocked.','Steam実績は全て解除済みです。')
      return
    end
    G.require()
    local changed,failed=0,0
    for _,key in ipairs(pending) do
      local label=key
      local accepted,result=pcall(steamCall,set,stats,{type=3,value=key})
      if accepted and (result&amp;0xFF)==1 then
        changed=changed+1
      else
        failed=failed+1
        G.msg('Steam unlock failed: '..label,'Steam実績の解除に失敗: '..label)
      end
    end
    if changed&gt;0 then
      local ok,result=pcall(steamCall,store,stats)
      if not ok or (result&amp;0xFF)~=1 then
        G.msg('Steam did not accept the store request.','Steamが実績の保存要求を受理しませんでした。')
      end
    end
    local unlocked=0
    for _,key in ipairs(keys) do
      local verified,value=pcall(steamCall,get,stats,{type=3,value=key},scratch)
      if verified and (value&amp;0xFF)==1 and readBytes(scratch,1)==1 then
        unlocked=unlocked+1
      else
        local label=key
        G.msg('Steam unlock not confirmed: '..label,'Steam実績の解除を確認できません: '..label)
      end
    end
    G.msg(('Steam client: %d/%d unlocked, %d newly accepted, %d failed.'):format(unlocked,n,changed,failed),
      ('Steamクライアント: %d/%d件解除済み、今回の受理%d件、失敗%d件。'):format(unlocked,n,changed,failed))
  end)
  deAlloc(scratch)
  if not ok then error(err,0) end
end
local function weather()
  local s=session()
  return s and ref(s+field('Session','Weather'))
end
local weatherLock
local function worldDisplaySwitch(resolve)
  local address,previous,epoch
  return {
    on=function()
      local target=assert(resolve(),G.T('World display is not ready.','ワールド表示の準備ができていません。'))
      local value=assert(readBytes(target,1))
      assert(value==0 or value==1,'Unexpected world display flag')
      address,previous,epoch=target,value,G.epoch
      return writeBytes(address,0) and readBytes(address,1)==0
    end,
    off=function()
      if not address then return true end
      if G.epoch==epoch and resolve()==address then
        if not writeBytes(address,previous) or readBytes(address,1)~=previous then return false end
      end
      address,previous,epoch=nil,nil,nil
      return true
    end,
  }
end
local function mainFogFlag()
  local hud=ref(static('HudBehaviour','Instance'))
  local camera=hud and ref(hud+field('HudBehaviour','UnityGameCameraBehaviour'))
  local effect=camera and ref(camera+field('CameraBehaviour','FogOfWarBehaviour'))
  return effect and effect+field('FogOfWarBehaviour','FogEnabled')
end
G.F.noFog=worldDisplaySwitch(mainFogFlag)
G.F.revealMap=worldDisplaySwitch(function()
  return static('FogOfWar','DebugFogOfWarEnabled')
end)

G.ACTIONS.setWeather=function()
  local boundary=G.captureBoundary()
  local w=assert(weather())
  local row=G.choose(G.T('Set weather','天候指定'),{
    {id=0,en='Clear',ja='晴れ'},
    {id=1,en='Warm front',ja='温暖前線'},
    {id=2,en='Cold front',ja='寒冷前線'},
    {id=3,en='Cloudy',ja='曇り'},
    {id=4,en='Light rain',ja='小雨'},
    {id=5,en='Heavy rain',ja='大雨'},
    {id=6,en='Sleet',ja='みぞれ'},
    {id=7,en='Snow',ja='雪'},
    {id=8,en='Heavy snow',ja='大雪'},
  },false)
  if not row then return end
  assert(G.captureBoundary()==boundary and weather()==w,'Weather context changed')
  local rand=assert(ref(static('MathUtil','NonDeterministicRand')))
  local start=method('Weather','StartFront',2)
  local relock=weatherLock and weatherLock.epoch==G.epoch
  if relock then G.feature('weatherLock',false) end
  local ok,err=pcall(function()
    local profiles={
      [0]={0,0},[3]={0.85,0},[4]={0.8,0.25,12},[5]={1,0.95,12},
      [6]={0.9,0.55,1},[7]={0.85,0.35,-4},[8]={1,0.95,-6},
    }
    local profile=profiles[row.id]
    local front=profile and 0 or row.id
    if profile then
      assert(writeFloat(w+field('Weather','Cloudiness'),profile[1]))
      assert(writeFloat(w+field('Weather','PrecipitationAmount'),profile[2]))
      if profile[3] then
        local base=profile[3]
        if row.id==6 then
          local offset=field('Weather','TemperatureInCelsiusNotIncludingDiurnalVariation')
          local delta=assert(readFloat(w+offset))-assert(readFloat(w+field('Weather','TemperatureInCelsius')))
          base=base+delta
        end
        assert(writeFloat(w+field('Weather','TemperatureInCelsiusNotIncludingDiurnalVariation'),base))
      end
    end
    invoke(start,w,{{type=vtDword,value=front},{type=vtPointer,value=rand}})
    assert(readInteger(w+field('Weather','CurrentWeatherType'))==front,
      G.T('The game did not retain the selected front.','ゲーム側で指定した前線を確認できません。'))
  end)
  if relock then
    local restored,reason=pcall(G.feature,'weatherLock',true)
    if not restored then
      local row=getAddressList().getMemoryRecordByID(511)
      if row then row.Active=false end
      if ok then error(reason,0) end
      G.msg(tostring(reason),tostring(reason))
    end
  end
  if not ok then error(err,0) end
  G.msg('Weather set: '..row.en,'天候を指定しました。'..row.ja)
end

local objectMethods={}
local objectMethodEpoch
local function objectMethod(object,name,count)
  if objectMethodEpoch~=G.epoch then
    objectMethods={}
    objectMethodEpoch=G.epoch
  end
  local c=assert(mono_object_getClass(object))
  local key=('%X/%s/%d'):format(c,name,count)
  if objectMethods[key] then return objectMethods[key] end
  while c and c~=0 do
    local result
    for _,m in ipairs(assert(mono_class_enumMethods(c,false))) do
      if m.name==name and #assert(mono_method_get_parameters(m.method)).parameters==count then
        assert(not result,('Ambiguous object method: %s/%d'):format(name,count))
        result=m.method
      end
    end
    if result then
      objectMethods[key]=result
      return result
    end
    c=mono_class_getParent(c)
  end
  error('Missing method '..name,0)
end
local categoryTerms={
  Valuables='貴重品',Weapons='武器',Ammo='弾薬',Melee='近接武器',Ranged='遠距離武器',
  Throwable='投擲武器',Medical='医療品',Antigen='抗原',Clothing='衣類',Armor='防具',
  Helmets='ヘルメット',Legs='脚部',Vest='ベスト',Backpacks='バックパック',Bottom='ボトムス',
  Glasses='眼鏡',Hats='帽子',Shoes='靴',Top='上着',Food='食料',Crops='作物',
  Dishes='料理',Drink='飲料',Alcohol='酒類',Meat='肉',Pills='錠剤',
  Processed='加工食品',Seeds='種',Crafting='クラフト素材',Containers='容器',
  Books='本',Novel='小説',Skill='スキル本',Special='特殊',
}
local function categoryNames(key)
  if key=='' then return 'Uncategorized','カテゴリなし' end
  local parts,localized={},{}
  for part in key:gsub('^%d+:',''):gmatch('[^/]+') do
    parts[#parts+1]=part
    localized[#localized+1]=categoryTerms[part] or part
  end
  return table.concat(parts,' / '),table.concat(localized,' / ')
end
local singleItem=G.EDITORS.items
local quantityField=singleItem.fields[1]
G.EDITORS.items={
  en=singleItem.en,ja=singleItem.ja,fields={},
  rows=function()
    return {{id=0,en='',ja=''}}
  end,
  context=function()
    local inv,owner=inventory()
    if inv and owner then return {identity=('%X/%d'):format(inv,identity(owner))} end
  end,
}
function G.openItemQuantities(parent)
  local id,epoch=parent.ID,G.epoch
  G.defer(function()
    if GT~=G or G.epoch~=epoch or G.disarming then return end
    local list=getAddressList()
    local current=list.getMemoryRecordByID(id)
    if not current or not current.Active then return end
    local ok=G.userRun(function()
      local boundary=G.captureBoundary()
      local context=assert(G.EDITORS.items.context())
      local rows=singleItem.rows()
      local fields={}
      local building=true
      local amounts={}
      for _,item in ipairs(inventoryItems()) do
        amounts[identity(item)]=assert(readInteger(item+field('Equipment','Amount')))
      end
      for _,row in ipairs(rows) do
        local id,category=row.id,row.category
        local en,ja=categoryNames(category)
        local initial=amounts[id] or 0
        fields[#fields+1]={en=row.en,ja=row.ja,kind='integer',min=0,max=2147483647,strict=true,
          category=category,groupEn=en,groupJa=ja,
          read=function()
            if building then return initial end
            local current=singleItem.context(id)
            return current and quantityField.read(current) or 0
          end,
          write=function(_,value)
            local current=singleItem.context(id)
            return value&gt;=1 and current~=nil and quantityField.write(current,value)
          end}
      end
      table.sort(fields,function(a,b)
        return a.category&lt;b.category or (a.category==b.category and a.en&lt;b.en)
      end)
      assert(G.captureBoundary()==boundary and G.EDITORS.items.context().identity==context.identity,'Inventory changed')
      if GT~=G or G.epoch~=epoch or G.disarming then return end
      current=list.getMemoryRecordByID(id)
      if not current or not current.Active then return end
      G.EDITORS.items.fields=fields
      assert(G.selectEditor(current,'items',{id=0,en='',ja=''}))
      building=false
    end)
    if not ok and GT==G and G.epoch==epoch then
      current=list.getMemoryRecordByID(id)
      if current and current.Active then current.Active=false end
    end
  end)
end
local function itemCategories(catalogOnly)
  local game=assert(ref(static('GameImpl','Instance')))
  local dict=assert(ref(game+field('GameImpl','CurrentEquipmentPrototypes')))
  local n=assert(invoke(objectMethod(dict,'get_Count',0),dict))
  assert(n&gt;=1 and n&lt;=16384,'Invalid equipment catalog size')
  local values=assert(invoke(objectMethod(dict,'get_Values',0),dict))
  local class=assert(mono_findClass('','EquipmentPrototype'))
  local array=assert(mono_array_new(class,n))
  assert(array~=0,'Cannot allocate equipment catalog')
  invoke(objectMethod(values,'CopyTo',2),values,{{type=vtPointer,value=array},0})
  local base=assert(mono_arrayinstance_getItemAddress(array,0))
  local categories,byName,byId={},{},{}
  local translate=not catalogOnly and method('GameImpl','Translate',1,MONO_TYPE_I4)
  for i=0,n-1 do
    local proto=assert(ref(base+i*8))
    local id=managedText(proto+field('EquipmentPrototype','Name'))
    if id~='' and not byId[id] then
      byId[id]=proto
      if not catalogOnly then
        local category=managedText(proto+field('EquipmentPrototype','Category'))
        local hash=assert(readInteger(proto+field('EquipmentPrototype','NameHash')))
        local label=hash~=0 and invoke(translate,0,{{type=vtDword,value=hash}}) or nil
        local en,ja=itemDisplayNames(label)
        if not byName[category] then
          local en,ja=categoryNames(category)
          local group={id=category,displayId=#categories+1,en=en,ja=ja,rows={}}
          byName[category]=group
          categories[#categories+1]=group
        end
        local rows=byName[category].rows
        rows[#rows+1]={id=id,en=en,ja=ja}
      end
    end
  end
  table.sort(categories,function(a,b) return a.en&lt;b.en end)
  for _,category in ipairs(categories) do
    table.sort(category.rows,function(a,b) return a.en&lt;b.en or (a.en==b.en and a.id&lt;b.id) end)
  end
  return categories,byId
end
local function initializeSpawnedEquipment(item,proto)
  local seed=identity(item)
  local liquid=ref(proto+field('EquipmentPrototype','DefaultLiquidPrototype'))
  local capacity=assert(readFloat(proto+field('EquipmentPrototype','LiquidCapacity')))
  if liquid and capacity&gt;0 then
    local low=assert(readFloat(proto+field('EquipmentPrototype','DefaultLiquidFilledMin')))
    local high=assert(readFloat(proto+field('EquipmentPrototype','DefaultLiquidFilledMax')))
    local fraction=invoke(method('MathUtil','RandomFloat',1),0,{{type=vtSingle,value=seed}})
    assert(type(fraction)=='number' and fraction&gt;=0 and fraction&lt;=1)
    local volume=capacity*(low+(high-low)*fraction)/100
    invoke(method('Equipment','SetLiquid',2),item,{liquid,{type=vtSingle,value=volume}})
  end
  local minimum=assert(readInteger(proto+field('EquipmentPrototype','DefaultInfectionTypeMin')))
  if minimum&gt;0 then
    local maximum=assert(readInteger(proto+field('EquipmentPrototype','DefaultInfectionTypeMax')))
    local strain=invoke(method('MathUtil','RandomInt',3),0,{seed,minimum,maximum+1})
    assert(type(strain)=='number' and strain&gt;=minimum and strain&lt;=maximum)
    assert(writeInteger(item+field('Equipment','InfectedWith'),strain))
  end
end
local function addSelectedItem(row,qty,boundary,inv,owner,catalog)
  local proto=assert(catalog[row.id],'Equipment no longer exists')
  local stackable=assert(readBytes(proto+field('EquipmentPrototype','CanBeCombined'),1))~=0
    and assert(readInteger(proto+field('EquipmentPrototype','DefaultInfectionTypeMin')))==0
  local spawn=method('Equipment','Spawn',2)
  local add=method('EquipmentContainer','Add',2)
  local function verifyInventory()
    local freshInv,freshOwner=inventory()
    assert(G.captureBoundary()==boundary and freshInv==inv and freshOwner==owner,'Inventory changed')
  end
  local count=stackable and 1 or qty
  local confirmed=0
  local ok,failure=pcall(function()
    for index=1,count do
      verifyInventory()
      local item=assert(invoke(spawn,0,{proto,stackable and qty or 1}))
      assert(item~=0,'Could not spawn '..row.id)
      local prepared,reason=pcall(function()
        initializeSpawnedEquipment(item,proto)
        verifyInventory()
      end)
      if not prepared then
        if G.captureBoundary()==boundary then invoke(objectMethod(item,'Delete',0),item) end
        error(reason,0)
      end
      local added=invoke(add,inv,{owner,item})
      assert(added and added~=0 and ref(added+field('Equipment','InventoryOwner'))==owner,
        G.T(('Inventory addition could not be confirmed (%d/%d).'):format(index,count),
            ('所持品への追加結果を確認できません（%d/%d）。'):format(index,count)))
      confirmed=confirmed+(stackable and qty or 1)
    end
  end)
  if not ok then
    G.msg(('Confirmed additions: %d/%d.'):format(confirmed,qty),
      ('追加を確認できた個数は%d/%dです。'):format(confirmed,qty))
    error(failure,0)
  end
  return confirmed
end

function G.addItem()
  local boundary=G.captureBoundary()
  local inv,owner=inventory()
  assert(inv and owner,G.T('No active inventory.','操作中の所持品がありません。'))
  local categories=itemCategories()
  while true do
    local category=G.choose(G.T('Item category','アイテムカテゴリ'),categories,false)
    if not category then return end
    local item,back,quantity=G.choose(G.T('Add items','アイテム追加'),category.rows,false,{back=true,nameOnly=true,quantity=true})
    if item then
      local currentInv,currentOwner=inventory()
      assert(G.captureBoundary()==boundary and currentInv==inv and currentOwner==owner,'Inventory changed')
      local _,catalog=itemCategories(true)
      local total=addSelectedItem(item,quantity,boundary,inv,owner,catalog)
      G.refreshEditors()
      G.msg(('Added %s × %d.'):format(item.en,total),('%sを%d個追加しました。'):format(item.ja,total))
      return
    end
    if not back then return end
  end
end

local injuryListLayout,injuryListEpoch
local function maintainInjuriesAndDisease(p)
  p=assert(p or player(),G.T('No active character.','操作中のキャラクターがいません。'))
  local injuries=assert(ref(p+field('Character','Injuries')))
  local layout=injuryListEpoch==G.epoch and injuryListLayout
  if not layout then
    layout={}
    local c=assert(mono_object_getClass(injuries))
    for _,f in ipairs(assert(mono_class_enumFields(c,false))) do layout[f.name]=f.offset end
    assert(layout._size)
    injuryListLayout,injuryListEpoch=layout,G.epoch
  end
  local countAddress=injuries+assert(layout._size)
  local count=assert(readInteger(countAddress))
  assert(count&gt;=0 and count&lt;=1024,'Invalid injury count')
  if count&gt;0 then
    local remove=method('Character','RemoveInjuryAt',1)
    for i=count-1,0,-1 do
      invoke(remove,p,{i})
      assert(readInteger(countAddress)==i,'Could not remove injury')
    end
    assert(writeBytes(p+field('Character','WantUnityUpdateInjuries'),1))
    assert(writeBytes(p+field('Character','WantUnityUpdateDecals'),1))
  end
  local cancer=p+field('Character','LungCancer')
  if readFloat(cancer)~=0 then return writeFloat(cancer,0) end
  return true
end
local function playerMembers()
  local s=assert(session())
  local manager=assert(ref(s+field('Session','CommunityManager')))
  local community=ref(manager+field('CommunityManager','PlayerCommunity'))
  if not community then return {},nil end
  return objects(assert(ref(community+field('Community','Members')))),community
end
local recoverCharacter
local recoveredMorale={}
local function hasRecoveredMorale(p)
  local entry=recoveredMorale[identity(p)]
  return entry and entry.epoch==G.epoch and entry.object==p
end
G.ACTIONS.recoverMembers=function()
  local members,community=playerMembers()
  assert(community,G.T('No player community.','プレイヤーのコミュニティがありません。'))
  local recovered,skipped=0,0
  local owner=field('Character','Community')
  for _,p in ipairs(members) do
    if ref(p+owner)==community then
      if recoverCharacter(p) then recovered=recovered+1 else skipped=skipped+1 end
    end
  end
  G.msg(('Recovered: %d. Skipped: %d.'):format(recovered,skipped),
    ('回復: %d人。対象外: %d人。'):format(recovered,skipped))
  return recovered
end

local function editableMember(p,community)
  return ref(p+field('Character','Community'))==community and
    (readInteger(p+field('Character','Consciousness')) or 3)&lt;3 and
    readInteger(p+field('Character','Infection'))==0
end
G.EDITORS.members={en='Member editor',ja='メンバーエディタ',
  fields=G.EDITORS.player.fields,
  rows=function()
    local rows={}
    local members,community=playerMembers()
    for _,p in ipairs(members) do
      if editableMember(p,community) then
        local id=identity(p)
        local label=playerLabel(p)
        if label=='' then label='#'..id end
        rows[#rows+1]={id=id,en=label,ja=label}
      end
    end
    return rows
  end,
  context=function(id)
    local members,community=playerMembers()
    for _,p in ipairs(members) do
      if identity(p)==id and editableMember(p,community) then
        return {object=p,identity=('%d/%d'):format(identity(community),id)}
      end
    end
  end,
}
local characterFields=G.EDITORS.player.fields
for _,spec in ipairs({
  {'Toilet','Bladder','尿意',0,nil,'WetPantsTime'},
  {'InfectionProgression','Infection progression','感染進行度',0,2},
  {'Excitement','Excitement','興奮度',0,1},
  {'LimitBloodLoss','Blood loss limit','出血上限',0,1},
  {'BloodAlcoholConcentration','Blood alcohol','血中アルコール濃度',0,1},
  {'LungCancer','Lung cancer','肺がん',0,1},
}) do
  local key,en,ja,lo,hi,maxField=table.unpack(spec,1,6)
  local f=floatField('Character',key,en,ja,lo,hi)
  if maxField then
    local write=f.write
    f.write=function(ctx,value)
      assert(value&lt;=assert(readFloat(static('Character',maxField))),G.T('Value exceeds game limit.','ゲーム内の上限を超えています。'))
      return write(ctx,value)
    end
  end
  characterFields[#characterFields+1]=f
end
for _,spec in ipairs({
  {'NameKnown','Name known','名前が判明'},
  {'Investigated','Investigated','調査済み'},
  {'DontLeaveCommunity','Stay in community','コミュニティから離脱しない'},
  {'DontAbandonPlayer','Do not abandon player','プレイヤーを見捨てない'},
}) do
  characterFields[#characterFields+1]=intField('Character',spec[1],spec[2],spec[3],1,true)
end
local gold={kind='integer',en='Reserved gold',ja='予約ゴールド',min=0,max=100000}
gold.read=function(ctx) return invoke(method('Character','GetReservedGoldAmount',0),ctx.object) end
gold.write=function(ctx,value)
  invoke(method('Character','SetReservedGoldAmount',1),ctx.object,{value})
  return gold.read(ctx)==value
end
characterFields[#characterFields+1]=gold

local communityFields=G.EDITORS.units.fields
for _,spec in ipairs({
  {'CommunityNameKnown','Name known','名前が判明'},
  {'DiscoverNameIfAtWar','Discover name at war','交戦時に名前が判明'},
  {'CommunityStatusShown','Show community status','コミュニティ状態を表示'},
  {'ExtortAISettlements','Extort AI settlements','NPC集落を恐喝する'},
  {'IsFEMA','FEMA','FEMA所属'},
  {'HiddenCommunity','Hidden community','非表示コミュニティ'},
}) do
  communityFields[#communityFields+1]=intField('Community',spec[1],spec[2],spec[3],1,true)
end
local function inlineSkillField(name)
  local class=assert(mono_findClass('','Skillset'))
  local members=assert(mono_class_enumFields(class,false))
  local first,offset
  for _,f in ipairs(members) do
    if not f.isStatic and not f.isConst then
      first=math.min(first or f.offset,f.offset)
      if f.name==name then offset=f.offset end
    end
  end
  -- Strength is the first instance field in the verified managed layout.
  assert((first==0 or first==16) and offset and offset&gt;=first and offset-first&lt;512,
    'Invalid Skillset layout: '..name)
  return field('Character','Skillset')+offset-first
end
local memberProgress={}
for index,spec in ipairs({
  {'Strength','Strength','筋力'},{'HandToHand','Hand to hand','格闘'},
  {'Archery','Archery','弓術'},{'Firearms','Firearms','銃器'},
  {'Stealth','Stealth','ステルス'},{'Construction','Construction','建設'},
  {'Farming','Farming','農業'},{'Medicine','Medicine','医療'},
  {'Cooking','Cooking','料理'},{'Constitution','Constitution','体質'},
}) do
  local key,en,ja=table.unpack(spec)
  local offsets
  local function layout()
    if not offsets or offsets.epoch~=G.epoch then
      offsets={epoch=G.epoch,level=inlineSkillField(key),cap=inlineSkillField(key..'Cap')}
    end
    return offsets
  end
  characterFields[#characterFields+1]={
    kind='integer',en=en,ja=ja,min=0,max=5,strict=true,
    available=function(ctx)
      local ok,f=pcall(layout)
      if not ok then return false end
      local level=readInteger(ctx.object+f.level)
      local cap=readInteger(ctx.object+f.cap)
      return level~=nil and cap~=nil and level&gt;=0 and level&lt;=cap and cap&lt;=5
    end,
    read=function(ctx) return readInteger(ctx.object+layout().level) end,
    write=function(ctx,v)
      local f=layout()
      assert(v&lt;=assert(readInteger(ctx.object+f.cap)),G.T('Skill cap exceeded.','スキル上限を超えています。'))
      local address=assert(mono_compile_method(method('Skillset','SetLevel',3)))
      assert(address~=0,'Cannot resolve Skillset.SetLevel')
      local inline=ctx.object+field('Character','Skillset')
      assert(inline&lt;=ctx.object+f.level and ctx.object+f.cap-inline&lt;512,'Invalid inline skillset layout')
      executeCodeEx(0,5000,address,inline,ctx.object,index-1,v)
      return readInteger(ctx.object+f.level)==v
    end,
  }
  local progress=floatField('Character',key,en..' progression points',ja..'の技能経験値')
  progress.readOnly=true
  local progressOffset,progressEpoch
  progress.available=function()
    local ok,offset=pcall(inlineSkillField,key..'Progression')
    if ok then progressOffset,progressEpoch=offset,G.epoch end
    return ok
  end
  progress.read=function(ctx)
    assert(progressEpoch==G.epoch,'Skill progression layout is unavailable')
    return readFloat(ctx.object+progressOffset)
  end
  memberProgress[#memberProgress+1]=progress
  memberProgress[#memberProgress+1]={
    kind='integer',en=en..' cap',ja=ja..'の上限',min=0,max=5,strict=true,
    available=function(ctx)
      local ok,f=pcall(layout)
      if not ok then return false end
      local cap=readInteger(ctx.object+f.cap)
      return cap~=nil and cap&gt;=0 and cap&lt;=5
    end,
    read=function(ctx) return readInteger(ctx.object+layout().cap) end,
    write=function(ctx,value)
      local fn=assert(mono_compile_method(method('Skillset','SetCap',3)))
      assert(fn~=0)
      executeCodeEx(0,5000,fn,ctx.object+field('Character','Skillset'),ctx.object,index-1,value)
      return readInteger(ctx.object+layout().cap)==value
    end,
  }
  local function skillKnownAddress(ctx)
    return ctx.object+inlineSkillField('SkillKnown')
  end
  local mask=1&lt;&lt;(index-1)
  memberProgress[#memberProgress+1]={
    kind='integer',en=en..' known',ja=ja..'の判明状態',min=0,max=1,strict=true,
    read=function(ctx) return (assert(readSmallInteger(skillKnownAddress(ctx))) &amp; mask)~=0 and 1 or 0 end,
    write=function(ctx,value)
      local address=skillKnownAddress(ctx)
      local before=assert(readSmallInteger(address))
      return writeSmallInteger(address,value==1 and (before | mask) or (before &amp; ~mask))
    end,
  }
end
-- Read the value-type lists without calling the game's mutable opinion cache.
local function valueList(object,name,class)
  local list=assert(ref(object+field('Character',name)))
  local c=assert(mono_object_getClass(list))
  local layout=listLayouts[c]
  if not layout then
    layout={}
    for _,entry in ipairs(assert(mono_class_enumFields(c,false))) do layout[entry.name]=entry.offset end
    listLayouts[c]=layout
  end
  local items,size=assert(layout._items),assert(layout._size)
  local count=assert(readInteger(list+size))
  assert(count&gt;=0 and count&lt;=16384,'Invalid '..name..' count')
  local array=assert(ref(list+items))
  assert(count&lt;=mono_arrayinstance_getCount(array),'Invalid '..name..' capacity')
  local first=assert(mono_arrayinstance_getItemAddress(array,0))
  local stride=assert(mono_arrayinstance_getItemAddress(array,1))-first
  local header
  for _,entry in ipairs(assert(mono_class_enumFields(assert(mono_findClass('System','Int32')),false))) do
    if entry.name=='m_value' then header=entry.offset break end
  end
  assert(header==16,'Unexpected Mono object header')
  return count,first,stride,function(name,width)
    local offset=field(class,name)-header
    assert(offset&gt;=0 and offset+width&lt;=stride,'Invalid '..class..'.'..name..' layout')
    return offset
  end
end
-- BrainScanPage displays RoundToInt(Character.CalcMorale()).
-- A cache edit expires when the game invalidates morale after a memory change.
characterFields[#characterFields+1]={
  kind='number',en='Morale (until recalculated)',ja='士気（再計算まで）',
  min=-100,max=100,strict=true,liveFloat=true,
  read=function(ctx)
    local p=ref(ctx.object+field('Character','Authoritative')) or ctx.object
    if readBytes(p+field('Character','CachedMoraleDirty'),1)==0 then
      return readFloat(p+field('Character','CachedMorale'))
    end
    local count,base,stride,member=valueList(p,'Memories','Memory')
    local offset=member('MoraleContribution',4)
    local total=0
    for i=0,count-1 do total=total+assert(readFloat(base+i*stride+offset)) end
    return math.max(-100,math.min(100,total))
  end,
  write=function(ctx,value)
    local p=ctx.object
    local authoritative=method('Character','GetAuthoritativeOrElseThisCharacter',0)
    p=assert(invoke(authoritative,p))
    assert(p~=0,'Character is unavailable')
    local cached=p+field('Character','CachedMorale')
    local dirty=p+field('Character','CachedMoraleDirty')
    local resume=not isPaused()
    if resume then pause() end
    local ok,result=pcall(function()
      if not writeFloat(cached,value) then return false end
      return writeBytes(dirty,0)
    end)
    if resume then unpause() end
    if not ok then error(result,0) end
    return result
  end,
}
local selectedOpinion
local function memberOpinion(ctx,offset,unclamped)
  local source=ctx.object
  local target=assert(player(),G.T('No active character.','操作中のキャラクターがいません。'))
  local authoritative=field('Character','Authoritative')
  source=ref(source+authoritative) or source
  target=ref(target+authoritative) or target
  if source==target then return offset==0 and 100 or 0 end
  if selectedOpinion(source) then return 100 end
  local contribution=offset==0 and 'ApprovalContribution' or 'RespectContribution'
  local total=0
  local count,base,stride,member=valueList(source,'Relationships','Relationship')
  local relationshipTarget=member('RelationshipTarget',8)
  local relationshipValue=member(contribution,4)
  for i=0,count-1 do
    local address=base+i*stride
    if ref(address+relationshipTarget)==target then
      total=total+assert(readFloat(address+relationshipValue))
    end
  end
  count,base,stride,member=valueList(source,'Memories','Memory')
  local actor,object,prototype,time=member('Actor',8),member('Object',8),member('Prototype',8),member('Time',8)
  local value=member(contribution,4)
  local targetCommunity=ref(target+field('Character','Community'))
  local rule=field('MemoryPrototype','RuleSet')
  local never=assert(readQword(static('Target','Never')))
  for i=0,count-1 do
    local address=base+i*stride
    local kind=assert(readInteger(assert(ref(address+prototype))+rule))
    local timestamp=assert(readQword(address+time))
    if timestamp&gt;never and timestamp&lt;math.maxinteger then
      local rememberedActor,rememberedObject=ref(address+actor),ref(address+object)
      local amount=assert(readFloat(address+value))
      if kind==12 then -- OpinionOfGroup
        if rememberedObject==targetCommunity and rememberedActor==source then total=total+amount end
      elseif rememberedActor==target or (offset==0 and kind==4 and rememberedObject==target and amount&lt;0) then
        total=total+amount
      end
    end
  end
  return unclamped and total or math.max(-100,math.min(100,total))
end
for _,progress in ipairs(memberProgress) do
  characterFields[#characterFields+1]=progress
end
local memberFields=characterFields
for _,spec in ipairs({
  {'Adrenaline','Adrenaline','アドレナリン',0,1},
  {'SedativeEffect','Sedative effect','鎮静効果',0},
  {'Toilet','Toilet need','排泄欲求',0},
  {'AimingAccuracy','Aiming accuracy','照準精度',0},
  {'TemperatureInsulationDelta','Insulation temperature adjustment','断熱による体温補正'},
}) do
  memberFields[#memberFields+1]=floatField('Character',table.unpack(spec))
end
for _,spec in ipairs({
  {'GetWeightInKg','Body weight (kg)','体重（kg）'},
  {'GetMaxInventoryWeight','Carry limit','最大所持重量'},
  {'GetBodyTemperatureIncludingFeverInCelsius','Temperature including fever','発熱込みの体温'},
}) do
  local name,en,ja=table.unpack(spec)
  memberFields[#memberFields+1]={kind='number',en=en,ja=ja,readOnly=true,
    read=function(ctx) return invoke(method('Character',name,0),ctx.object) end}
end
local carried=floatField('Character','CachedInventoryWeight','Carried weight','所持重量')
carried.readOnly=true
memberFields[#memberFields+1]=carried
for _,spec in ipairs({
  {'Injuries','Injury count','負傷件数'},
  {'Memories','Memory count','記憶件数'},
  {'Relationships','Relationship count','人物関係の件数'},
  {'Personality','Personality trait count','性格特性の件数'},
  {'Roles','Assigned role count','担当役割の件数'},
}) do
  local name,en,ja=table.unpack(spec)
  memberFields[#memberFields+1]={kind='integer',en=en,ja=ja,readOnly=true,min=0,
    read=function(ctx)
      local list=assert(ref(ctx.object+field('Character',name)))
      return invoke(objectMethod(list,'get_Count',0),list)
    end}
end
local age={kind='number',en='Age',ja='年齢',liveFloat=true,strict=true}
age.read=function(ctx)
  return readFloat(assert(ref(ctx.object+field('Character','Appearance')))+field('CharacterAppearance','Age'))
end
age.write=function(ctx,value)
  local min=assert(readFloat(static('HumanAppearance','MinAge')))
  local max=assert(readFloat(static('HumanAppearance','MaxAge')))
  assert(value&gt;=min and value&lt;=max,G.T('Age exceeds the game range.','年齢がゲームの範囲外です。'))
  invoke(method('Character','SetAge',1),ctx.object,{{type=vtSingle,value=value}})
  return true
end
characterFields[#characterFields+1]=age
characterFields[#characterFields+1]={
  kind='number',en='Standing height',ja='立位の身長',readOnly=true,
  read=function(ctx) return invoke(method('Character','GetHeightIgnoringCrouching',0),ctx.object) end,
}
for _,spec in ipairs({
  {'FirstName','First name','名'},
  {'Surname','Surname','姓'},
}) do
  local key,en,ja=table.unpack(spec)
  characterFields[#characterFields+1]={kind='string',en=en,ja=ja,readOnly=true,maxLength=120,
    read=function(ctx)
      local object=ctx.object
      local raw=managedText(object+field('Character',key))
      if raw=='' then return '' end
      local verified=assert(readInteger(object+field('Character',key..'Verified')))
      local name={type=vtString,value=raw}
      local englishOnly={type=vtByte,value=0}
      local status={type=vtDword,value=verified}
      local result
      if key=='Surname' then
        local appearance=assert(ref(object+field('Character','Appearance')))
        local gender=assert(readInteger(appearance+field('CharacterAppearance','Gender')))
        result=invoke(method('GameImpl','TranslateSurname',4),0,
          {name,{type=vtDword,value=gender},englishOnly,status})
      else
        result=invoke(method('GameImpl','TranslateName',3),0,{name,englishOnly,status})
      end
      assert(type(result)=='string',G.T('Could not translate the name.','名前を翻訳できません。'))
      return result
    end}
end
for _,spec in ipairs({
  {'BackgroundKnown','Background known','経歴が判明'},
  {'BrainScanned','Brain scanned','脳スキャン済み'},
  {'Boxer','Boxer','ボクサー'},
  {'God','God mode','無敵状態'},
  {'CanFollowPlayer','Can follow player','プレイヤーに追従可能'},
  {'PlayDead','Play dead','死んだふり'},
  {'HadInvisibleStrainFromStart','Had invisible strain from start','最初から透明株に感染'},
  {'AlwaysActivateInvisibleStrain','Always activate invisible strain','透明株を常に発症させる'},
  -- DontInfectWithInvisibleStrain is a Character reference, not a byte flag.
  -- {'DontInfectWithInvisibleStrain','Invisible strain protection','透明株への感染防止'},
}) do
  characterFields[#characterFields+1]=intField('Character',spec[1],spec[2],spec[3],1,true)
end
for _,spec in ipairs({
  {'Consciousness','Consciousness','意識状態',3,false},
  {'Infection','Infection type','感染種別',5,false},
  {'InvisibleStrain','Invisible strain type','透明株の種別',2,false},
  {'Encumbered','Encumbered','重量超過',1,true},
}) do
  local row=intField('Character',table.unpack(spec))
  row.readOnly=true
  characterFields[#characterFields+1]=row
end
G.EDITORS.members.fields={table.unpack(characterFields)}
for _,spec in ipairs({
  {0,'Approval toward active character (total)','操作中の人物への承認（合計）'},
  {4,'Respect toward active character (total)','操作中の人物への尊敬（合計）'},
}) do
  local offset,en,ja=table.unpack(spec)
  G.EDITORS.members.fields[#G.EDITORS.members.fields+1]={
    kind='number',en=en,ja=ja,min=-100,max=100,readOnly=true,
    read=function(ctx) return memberOpinion(ctx,offset) end,
  }
end
-- Match the game's CharacterPersonalityEditor and use its setter to refresh the cache.
local function personalityFields(base)
  local result={table.unpack(base)}
  local game=assert(ref(static('GameImpl','Instance')))
  local list=assert(invoke(method('GameImpl','GetAllPersonalities',0),game))
  local translate=method('GameImpl','Translate',1,MONO_TYPE_STRING)
  local has=method('Character','HasPersonality',1,MONO_TYPE_STRING)
  local set=method('Character','SetPersonality',2,MONO_TYPE_STRING)
  local choices={{value=0,en='Off',ja='なし'},{value=1,en='On',ja='あり'}}
  for _,address in ipairs(objects(list)) do
    local name=mono_string_readString(address)
    if name and name~='' then
      local label=invoke(translate,0,{{type=vtString,value='PERSONALITY_'..name:gsub(' ','')}})
      if not label or label=='' then label=name end
      local function argument() return {type=vtString,value=name} end
      local function read(ctx)
        local traits=objects(assert(ref(ctx.object+field('Character','Personality'))))
        for _,trait in ipairs(traits) do
          if mono_string_readString(trait)==name then return 1 end
        end
        return 0
      end
      result[#result+1]={kind='integer',en='Personality: '..label,ja='性格：'..label,choices=choices,min=0,max=1,
        read=read,write=function(ctx,value)
          local function currentTraits()
            return objects(assert(ref(ctx.object+field('Character','Personality'))))
          end
          local knownAddress=ctx.object+field('Character','PersonalityKnown')
          local before=assert(readInteger(knownAddress))
          local known={}
          for i,trait in ipairs(currentTraits()) do
            known[mono_string_readString(trait)]=(before &amp; (1&lt;&lt;((i-1)%32)))~=0
          end
          invoke(set,ctx.object,{argument(),{type=vtByte,value=value}})
          local updated=0
          for i,trait in ipairs(currentTraits()) do
            if known[mono_string_readString(trait)] then updated=updated | (1&lt;&lt;((i-1)%32)) end
          end
          return writeInteger(knownAddress,updated) and read(ctx)==value
        end}
      local function personalityMask(ctx)
        local traits=objects(assert(ref(ctx.object+field('Character','Personality'))))
        for index,trait in ipairs(traits) do
          if mono_string_readString(trait)==name then return 1&lt;&lt;((index-1)%32) end
        end
      end
      result[#result+1]={kind='integer',en='Known: '..label,ja='判明：'..label,choices=choices,min=0,max=1,
        available=function(ctx) return personalityMask(ctx)~=nil end,
        read=function(ctx)
          local mask=personalityMask(ctx)
          return mask and (assert(readInteger(ctx.object+field('Character','PersonalityKnown'))) &amp; mask)~=0 and 1 or 0
        end,
        write=function(ctx,value)
          local mask=assert(personalityMask(ctx),G.T('Trait is absent.','この性格特性はありません。'))
          local address=ctx.object+field('Character','PersonalityKnown')
          local before=assert(readInteger(address))
          local updated=value==1 and (before | mask) or (before &amp; ~mask)
          return writeInteger(address,updated &amp; 0xffffffff)
        end,
      }
    end
  end
  return result
end
G.EDITORS.player.fieldsFor=personalityFields
G.EDITORS.members.fieldsFor=personalityFields
local function npcRows(membersOnly)
  local members,community=playerMembers()
  local candidates=members
  if not membersOnly then
    local manager=assert(ref(assert(session())+field('Session','CharacterManager')))
    candidates=objects(assert(ref(manager+field('CharacterManager','Characters'))))
  end
  local rows={}
  local active=player()
  for _,p in ipairs(candidates) do
    local class=mono_object_getClass(p)
    while class and class~=0 and mono_class_getName(class)~='Human' do class=mono_class_getParent(class) end
    if class and class~=0 and editableMember(p,ref(p+field('Character','Community'))) and (membersOnly or p~=active) then
      local id=identity(p)
      local name=playerLabel(p)
      rows[#rows+1]={id=id,en=name,ja=name,member=community~=nil and ref(p+field('Character','Community'))==community,object=p}
    end
  end
  return rows
end
local function npcContext(id)
  local manager=assert(ref(assert(session())+field('Session','CharacterManager')))
  for _,p in ipairs(objects(assert(ref(manager+field('CharacterManager','Characters'))))) do
    if identity(p)==id and editableMember(p,ref(p+field('Character','Community'))) then
      return {object=p,identity=tostring(id)}
    end
  end
end
G.EDITORS.npcs={en='NPC editor',ja='NPCエディタ',fields=G.EDITORS.members.fields,
  fieldsFor=personalityFields,rows=function() return npcRows(false) end,context=npcContext}
local revealCharacter
local maximizeOpinion
selectedOpinion=function(p)
  local state=G.selectedOpinion
  return state and state.epoch==G.epoch and state.ids[identity(p)]
end
local recruitOnGameThread
local function showNpcList(membersOnly)
  local boundary=G.captureBoundary()
  local key=membersOnly and 'members' or 'npcs'
  local editorID=membersOnly and 311 or 315
  local function checked(row)
    assert(G.captureBoundary()==boundary,G.T('The world changed.','ワールドが変化しました。'))
    return assert(G.EDITORS[key].context(row.id),G.T('Character unavailable.','対象を取得できません。'))
  end
  local function open(row)
    checked(row)
    local parent=assert(getAddressList().getMemoryRecordByID(editorID))
    G.openEditorTarget(parent,key,row)
  end
  local actions={
    {'Edit in editor','エディタで編集',open},
    {'Change value','値の変更',function(row,selectedRows)
      local d=G.EDITORS[key]
      local ctx=checked(row)
      local choices={}
      for index,f in ipairs(d.fieldsFor(d.fields)) do
        if f.write and not f.readOnly and (not f.available or f.available(ctx)) then
          choices[#choices+1]={id=index,en=f.en,ja=f.ja,field=f}
        end
      end
      local selected=G.choose(G.T('Change value','値の変更'),choices,false,{nameOnly=true})
      if not selected then return end
      local f=selected.field
      local before={}
      for _,r in ipairs(selectedRows) do
        local ctx=checked(r)
        assert(not f.available or f.available(ctx),r.name)
        before[r]=f.read(ctx)
      end
      local value=inputQuery(G.T('Change value','値の変更'),G.T(f.en,f.ja),(#selectedRows==1 and tostring(before[row]) or ''))
      if value==nil then return end
      value=G.normalized(f,f.kind=='string' and value or tonumber(value))
      for _,r in ipairs(selectedRows) do
        assert(f.read(checked(r))==before[r],r.name)
      end
      local failed={}
      for _,r in ipairs(selectedRows) do
        local ok,err=pcall(function() assert(f.write(checked(r),value)==true) end)
        if not ok then failed[#failed+1]=r.name..': '..tostring(err) end
      end
      if #failed&gt;0 then error(table.concat(failed,'\n'),0) end
    end},
    {'Max approval','好感度最大',function(row) maximizeOpinion(checked(row).object) end},
    {'Fully heal','全回復',function(row) assert(recoverCharacter(checked(row).object),G.T('Cannot heal this character.','この人物は回復対象外です。')) end},
    {'Reveal all information','全情報判明',function(row) revealCharacter(checked(row).object) end},
    {'Recruit by force','強奪',function(row)
      local p=checked(row).object
      local _,community=playerMembers()
      assert(community and ref(p+field('Character','Community'))~=community)
      recruitOnGameThread(p,community)
      assert(ref(p+field('Character','Community'))==community)
      row.member=true
    end,function(row) return not row.member end},
  }
  local columns={
    {key='id',title='ID',numeric=true,width=85},
    {key='name',title=G.T('Name','名前'),width=200},
    {key='community',title=G.T('Community','所属'),width=160},
    {key='status',title=G.T('Status','状態'),width=95},
  }
  local readers={}
  local function number(en,ja,width,read)
    local id=#readers+1
    readers[id]=read
    columns[#columns+1]={key=id,title=G.T(en,ja),width=width,numeric=true}
  end
  local function round(v) return v and math.floor(v*10+0.5)/10 end
  local function float(name)
    local offset=field('Character',name)
    return function(p) return round(readFloat(p+offset)) end
  end
  number('Age','年齢',68,function(p) return round(age.read({object=p})) end)
  number('Morale','士気',72,function(p)
    local offset=field('Character','CachedMorale')
    if readBytes(p+field('Character','CachedMoraleDirty'),1)==0 then return round(readFloat(p+offset)) end
    local count,base,stride,member=valueList(p,'Memories','Memory')
    local delta=member('MoraleContribution',4)
    local sum=0
    for i=0,count-1 do sum=sum+assert(readFloat(base+i*stride+delta)) end
    return round(math.max(-100,math.min(100,sum)))
  end)
  number('Approval','承認',72,function(p) return round(memberOpinion({object=p},0)) end)
  number('Respect','尊敬',72,function(p) return round(memberOpinion({object=p},4)) end)
  for _,stat in ipairs(stats) do number(stat[2],stat[3],100,float(stat[1])) end
  number('Infection progression','感染進行度',110,float('InfectionProgression'))
  number('Carried weight','所持重量',100,float('CachedInventoryWeight'))
  for _,skill in ipairs({
    {'Strength','Strength','筋力'},{'HandToHand','Hand to hand','近接格闘'},
    {'Archery','Archery','弓'},{'Firearms','Firearms','銃火器'},
    {'Stealth','Stealth','隠密'},{'Construction','Construction','建築'},
    {'Farming','Farming','農業'},{'Medicine','Medicine','医療'},
    {'Cooking','Cooking','料理'},{'Constitution','Constitution','体質'},
  }) do
    local ok,offset=pcall(inlineSkillField,skill[1])
    if ok then number(skill[2],skill[3],95,function(p) return readInteger(p+offset) end) end
  end
  local rows=npcRows(membersOnly)
  local names={}
  local function refresh(row,initial)
    local p=initial and row.object or checked(row).object
    local community=ref(p+field('Character','Community'))
    if community and names[community]==nil then
      local ok,name=pcall(displayName,community)
      names[community]=ok and name or ''
    end
    row.community=community and names[community] or G.T('Unaffiliated','無所属')
    local state=readInteger(p+field('Character','Consciousness'))
    row.status=state==0 and G.T('Awake','覚醒') or state==1 and G.T('Sleeping','睡眠') or state==2 and G.T('Unconscious','気絶') or ''
    for i,read in ipairs(readers) do
      local ok,value=pcall(read,p)
      row[i]=ok and value or nil
    end
  end
  -- Initial population uses the same validated object, without rebuilding the NPC list per row.
  for _,row in ipairs(rows) do
    row.name=G.T(row.en,row.ja)
    refresh(row,true)
  end
  G.showCharacterList(G.T(membersOnly and 'All members' or 'All NPCs',
    membersOnly and '全メンバー一覧' or '全NPC一覧'),rows,columns,actions,open,refresh)
end
G.ACTIONS.listMembers=function() showNpcList(true) end
G.ACTIONS.listNPCs=function() showNpcList(false) end
local memberScope={epoch=nil,address=nil,signature=nil}
local memberActivationIds=setmetatable({}, {__mode='k'})
local memberActivationSerial=0
local memberFeatures={
  God=true,BloodLoss=true,InjuriesAndDisease=true,Fatigue=true,Hunger=true,Thirst=true,
  SleepDeprivation=true,Infection=true,BodyTemperatureInCelsius=true,ignoreWeight=true,
  keepAmmo=true,stealthMode=true,oneHitKill=true,xpMultiplier=true,injuryFilter=true,preventDisease=true,
  ignoreCombatImpact=true,unlimitedHealth=true,infection=true,
}
local function memberScopeAddress()
  if memberScope.epoch~=G.epoch then
    memberScope={epoch=G.epoch,address=assert(allocateMemory(16384))}
    assert(memberScope.address~=0 and writeInteger(memberScope.address,0))
  end
  return memberScope.address
end
local function syncMemberGod(members,selected)
  local previous=memberScope.god or {}
  memberScope.god=previous
  local manager=ref(assert(session())+field('Session','CharacterManager'))
  local list=manager and ref(manager+field('CharacterManager','Characters'))
  local live={}
  for _,p in ipairs(list and objects(list) or members) do live[p]=identity(p) end
  local offset=field('Character','God')
  local originalId=god and god.id
  for p,entry in pairs(previous) do
    if not selected[p] or live[p]~=entry.id then
      if live[p]==entry.id then
        assert(writeBytes(p+offset,entry.value) and readBytes(p+offset,1)==entry.value,
          'Could not restore member invulnerability')
      end
      previous[p]=nil
    end
  end
  for p in pairs(selected) do
    if live[p] and not previous[p] and live[p]~=originalId then
      local entry={id=live[p],value=assert(readBytes(p+offset,1))}
      previous[p]=entry
      assert(writeBytes(p+offset,1) and readBytes(p+offset,1)==1)
    end
  end
end
local initializeMemberFeatures
local function refreshMemberScope()
  local address=memberScopeAddress()
  local members,community=playerMembers()
  memberScope.world=G.CONFIG.boundary()
  local selected,identities={},{}
  for _,p in ipairs(members) do
    if editableMember(p,community) then
      selected[#selected+1]=p
      identities[p]=identity(p)
    end
  end
  table.sort(selected)
  local entries={}
  for index,p in ipairs(selected) do entries[index]=('%X/%d'):format(p,identities[p]) end
  local signature=tostring(community)..':'..table.concat(entries,',')
  local activeKeys={}
  local keys={}
  for _,key in ipairs({'BloodLoss','Fatigue','Hunger','Thirst','SleepDeprivation','BodyTemperatureInCelsius','InjuriesAndDisease'}) do
    local activation=G.active[key]
    if activation and not activation.cleanup and not activation.starting then
      local serial=memberActivationIds[activation]
      if not serial then
        memberActivationSerial=memberActivationSerial+1
        serial=memberActivationSerial
        memberActivationIds[activation]=serial
      end
      keys[key]=serial
      activeKeys[#activeKeys+1]=key..'/'..serial
    end
  end
  local godActive=G.active.God and not G.active.God.cleanup
  if godActive then activeKeys[#activeKeys+1]='God' end
  signature=signature..':'..table.concat(activeKeys,',')
  if signature==memberScope.signature then return true end
  memberScope.initialized=memberScope.initialized or {}
  local initialized={}
  local controlled=player()
  local godTargets={}
  if godActive then
    for _,p in ipairs(selected) do godTargets[p]=true end
  end
  if godActive or next(memberScope.god or {}) then syncMemberGod(members,godTargets) end
  for _,p in ipairs(selected) do
    local id=identities[p]
    local old=memberScope.initialized[p] or {}
    if old.id~=id then old={} end
    initialized[p]={id=id}
    for key,serial in pairs(keys) do
      if old[key]~=serial and p~=controlled then
        assert(initializeMemberFeatures(p,key),'Could not initialize member feature '..key)
      end
      initialized[p][key]=serial
    end
  end
  memberScope.initialized=initialized
  assert(#selected&lt;=2046,'Community exceeds member scope capacity')
  local data=string.pack('&lt;I8I8',0,community or 0)
  local items={data}
  for _,p in ipairs(selected) do items[#items+1]=string.pack('&lt;I8',p) end
  data=table.concat(items)
  local buffer={}
  for index=1,#data do buffer[index]=data:byte(index) end
  local resume=not isPaused()
  if resume then pause() end
  local ok,written=pcall(function()
    assert(writeInteger(address,0))
    assert(writeBytes(address,buffer))
    return writeInteger(address,#selected)
  end)
  if resume then unpause() end
  assert(ok and written,'Could not update member scope')
  memberScope.signature=signature
  return true
end
G.F.memberScope={
  on=refreshMemberScope,
  task=refreshMemberScope,
  off=function()
    if memberScope.epoch~=G.epoch then return true end
    assert(writeInteger(memberScope.address,0))
    memberScope.signature=nil
    memberScope.initialized=nil
    if memberScope.god and session() and memberScope.world==G.CONFIG.boundary() then
      syncMemberGod({}, {})
    end
    memberScope.god=nil
    return true
  end,
}
initializeMemberFeatures=function(p,key)
  if key=='InjuriesAndDisease' then return maintainInjuriesAndDisease(p) end
  for _,spec in ipairs(stats) do
    if spec[1]==key then
      return writeFloat(p+field('Character',key),spec[4])
    end
  end
  return true
end
local function extendMemberComparison(body,labels,feature)
  if not memberFeatures[feature] then return body,labels end
  labels=labels or {}
  local index=0
  body=body:gsub('cmp ([^\n]+),r11\njne @normal',function(operand)
    index=index+1
    local loop='sis_member_loop_'..index
    local match='sis_member_match_'..index
    local miss='sis_member_miss_'..index
    local done='sis_member_done_'..index
    local lookup='sis_member_lookup_'..index
    for _,label in ipairs({loop,match,miss,done,lookup}) do labels[#labels+1]=label end
    return ([[
cmp %s,r11
jne %s
test r11,r11
jne %s
%s:
mov r11,%s
push rax
push r10
test r11,r11
je %s
mov r10,%X
mov eax,[r10]
test eax,eax
je %s
mov rax,[r10+8]
cmp [r11+%X],rax
jne %s
mov eax,[r10]
%s:
cmp [r10+rax*8+8],r11
je %s
dec eax
jne %s
%s:
pop r10
pop rax
jmp @normal
%s:
pop r10
pop rax
%s:]]):format(operand,lookup,done,lookup,operand,miss,memberScopeAddress(),miss,field('Character','Community'),miss,loop,match,loop,miss,match,done)
  end)
  return body,labels
end

local hookPages,hookPageEpoch={},nil
local function reserveHookSlot(site)
  if hookPageEpoch~=G.epoch then
    hookPages,hookPageEpoch={},G.epoch
  end
  for _,page in ipairs(hookPages) do
    local address=page.base+page.used
    local displacement=address-site-5
    if page.used&lt;page.size and displacement&gt;=-0x80000000 and displacement&lt;=0x7FFFFFFF then
      page.used=page.used+1024
      return address
    end
  end
  for _,size in ipairs({65536,4096}) do
    local address=allocateMemory(size,site)
    if address and address~=0 then
      local displacement=address-site-5
      if displacement&gt;=-0x80000000 and displacement&lt;=0x7FFFFFFF then
        hookPages[#hookPages+1]={base=address,used=1024,size=size}
        return address
      end
      deAlloc(address)
    end
  end
  return nil
end

-- The WP10 entry jump also works for the return to the original code.
-- Encode it explicitly so assembler branch sizing cannot change the layout.
local function absoluteJump(address)
  return string.char(0xFF,0x25,0,0,0,0)..string.pack('&lt;I8',address)
end

local function scopedPatch(feature,specs)
  local function stores(entry,jit,store)
    local offset=field(store.class,store.field)
    local code=assert(bytes(entry,jit.code_size))
    local sites, address={},entry
    while address&lt;entry+jit.code_size do
      local step=assert(getInstructionSize(address))
      assert(step&gt;0 and step&lt;=15,'Invalid JIT instruction length')
      if address+step&gt;entry+jit.code_size then break end
      local start=address-entry+1
      local raw={table.unpack(code,start,start+step-1)}
      local index, rex=1,0
      if raw[index]==0xF3 and not store.byte then index=index+1 end
      if raw[index] and raw[index]&gt;=0x40 and raw[index]&lt;=0x4F then
        rex=raw[index]
        index=index+1
      end
      local modrm
      if store.byte and (raw[index]==0x88 or raw[index]==0xC6) then
        modrm=index+1
      elseif not store.byte and raw[1]==0xF3 and raw[index]==0x0F and raw[index+1]==0x11 then
        modrm=index+2
      end
      if modrm then
        local mode, base=raw[modrm]&gt;&gt;6,raw[modrm]&amp;7
        if raw[index]==0xC6 and (raw[modrm]&amp;0x38)~=0 then mode=3 end
        local displacement=modrm+1
        if base==4 and (mode==1 or mode==2) then
          assert(raw[displacement],'Invalid SIB byte')
          if (raw[displacement]&amp;0x38)==0x20 and (rex&amp;2)==0 then
            base=raw[displacement]&amp;7
          else
            base=nil
          end
          displacement=displacement+1
        end
        if base and (mode==1 or mode==2) then
          local format=mode==1 and '&lt;i1' or '&lt;i4'
          if string.unpack(format,string.char(table.unpack(raw,displacement)))==offset then
            local registers={'rax','rcx','rdx','rbx','rsp','rbp','rsi','rdi',
              'r8','r9','r10','r11','r12','r13','r14','r15'}
            local register=registers[base+1+(rex&amp;1)*8]
            assert(step&gt;=5 and register~='rsp' and register~='r11','Unsupported field store')
            sites[#sites+1]={address,step,register,step}
          end
        end
      end
      address=address+step
    end
    assert(#sites==store.count,('Unexpected JIT store count for %s: %d/%d'):format(store.field,#sites,store.count))
    return sites
  end
  local state
  local stateEpoch
  local function restoreSites()
    for i=#state.sites,1,-1 do
      local patch=state.sites[i]
      local current=bytes(patch.address,#patch.original)
      assert(equal(current,patch.installed) or equal(current,patch.original),'Method bytes changed externally')
      if not equal(current,patch.original) then
        assert(writeBytes(patch.address,patch.original),'Could not restore method')
        assert(equal(bytes(patch.address,#patch.original),patch.original),'Method restore verification failed')
      end
      table.remove(state.sites,i)
    end
  end
  return {
    on=function()
      assert(not state or stateEpoch~=G.epoch,'Previous patch still needs restoration')
      state={sites={}}
      stateEpoch=G.epoch
      local plans={}
      for index,spec in ipairs(specs) do
        local target=method(spec[1],spec[2],spec[3],nil,spec.firstName)
        local site=assert(mono_compile_method(target))
        assert(site~=0,'Could not compile '..spec[2])
        local jit=assert(mono_getJitInfo(site),'Method entry is not in a Mono JIT region')
        assert(jit.code_start==site and jit.method==target,
          'Unexpected Mono JIT method entry')
        local locations
        if spec.store then
          locations=stores(site,jit,spec.store)
          if spec[1]=='Character' and spec[2]=='ConsumeLiquid' then
            local targets={}
            local cursor=site
            while cursor&lt;site+jit.code_size do
              local op=disassemble(cursor):match('^.- %- .- %- (.+)$') or ''
              local dest=op:match('^j%w+%s+([0-9A-Fa-f]+)$')
              if dest then targets[tonumber(dest,16)]=true end
              cursor=cursor+assert(getInstructionSize(cursor))
            end
            for _,location in ipairs(locations) do
              local start,span=location[1],location[2]
              while span&lt;14 do
                local address=start+span
                local op=(disassemble(address):match('^.- %- .- %- (.+)$') or ''):lower()
                assert(not targets[address] and not op:find('rip',1,true) and
                  (op:match('^mov r%w+,r%w+$') or op:match('^xorpd xmm%d+,xmm%d+$') or
                   op:match('^movss xmm%d+,%[r%w+[%+%-][0-9a-f]+%]$')),
                  'Unsupported ConsumeLiquid continuation')
                span=span+assert(getInstructionSize(address))
                assert(start+span&lt;=site+jit.code_size,'Invalid ConsumeLiquid continuation')
              end
              location[2]=span
            end
          end
        end
        local length=0
        while not locations and length&lt;14 and length&lt;jit.code_size do
          local address=site+length
          local instruction=assert(disassemble(address))
          local op=(instruction:match('^.- %- .- %- (.+)$') or ''):lower()
          op=op:gsub('%s*:%s.*$',''):gsub('%s*,%s*',','):gsub('%s+$','')
          if length==0 and op:match('^jmp%s') then
            error(G.T(
              ('%s.%s already starts with a jump. Restore the active table or use a fresh game process.'):format(spec[1],spec[2]),
              ('%s.%s の先頭に既存のジャンプ命令があります。使用中のCTで解除するか、ゲームを再起動してください。'):format(spec[1],spec[2])),0)
          end
          local safe=not op:find('rip',1,true) and
            (op:match('^push %w+$') or op:match('^sub rsp,') or
             op:match('^mov %[rsp[%+%-]') or op:match('^mov %[rsp%]') or
             op:match('^mov %[rbp[%+%-]') or op:match('^mov %[rbp%]') or
             op:match('^movss %[rsp[%+%-]') or op:match('^movaps %[rsp[%+%-]') or
             op:match('^lea r[%w]+,%[rsp[%+%-]') or
             op:match('^mov r[%w]+,r[%w]+$') or op:match('^nop'))
          if not safe and length&gt;=5 then break end
          assert(safe,'Unsupported method entry: '..spec[2]..' '..op)
          local step=assert(getInstructionSize(address))
          assert(step&gt;0 and step&lt;=15 and length+step&lt;=24)
          length=length+step
        end
        assert(locations or (length&gt;=5 and jit.code_size&gt;=length),'Unexpected Mono JIT method length')
        for _,location in ipairs(locations or {{site,length}}) do
          local site,length,base,storeLength=table.unpack(location)
          local original=assert(bytes(site,length))
          G.patchSerial=(G.patchSerial or 0)+1
          local cave=('sis_%s_%d_%d'):format(feature,index,G.patchSerial)
          local normal=cave..'_normal'
          local hud=static('Hud','Instance')
          local controlled=field('Hud','LocalControlledCharacter')
          local body,labels
          local guard, restore = '', ''
          if base then
            restore='pop r11\npopfq\n'
            local replacement=spec.store.byte and
              ('mov byte ptr [%s+%X],0\n'):format(base,field(spec.store.class,spec.store.field)) or ''
            local tail=length&gt;storeLength and ('db '..db({table.unpack(original,storeLength+1)})..'\n') or ''
            body=('cmp %s,r11\njne @normal\n%s%s%sdb %s'):format(
              base,replacement,restore,tail,db({absoluteJump(site+length):byte(1,14)}))
          else
            body,labels=spec[4](controlled)
          end
          body,labels=extendMemberComparison(body,labels,feature)
          if spec.worldScoped then
            guard=('db 49 BB\ndq %X\n'):format(spec.worldScoped)
          else
            guard=([[db 49 BB
dq %X
mov r11,[r11]
test r11,r11
je %s
mov r11,[r11+%X]
]]):format(hud,normal,controlled)
            if not memberFeatures[feature] then
              guard=guard..('test r11,r11\nje %s\n'):format(normal)
            end
          end
          if base then guard='pushfq\npush r11\n'..guard end
          local script=([[
[ENABLE]
define(%s,%X)
label(%s)
%s:
%s%s
%s:
%sdb %s
db %s
[DISABLE]
]]):format(cave,0,normal,cave,guard,body:gsub('@normal',normal),
            normal,restore,db(original),db({absoluteJump(site+length):byte(1,14)}))
          for _,label in ipairs(labels or {}) do
            local unique=cave..'_'..label
            script=script:gsub('%f[%w_]'..label..'%f[^%w_]',unique)
            script=script:gsub('%[ENABLE%]', '[ENABLE]\nlabel('..unique..')',1)
          end
          plans[#plans+1]={site=site,length=length,original=original,script=script,
            name=spec[2],cave=cave,prefixOffset=base and 3 or 0}
        end
      end
      local pageUsage={}
      if hookPageEpoch==G.epoch then
        for i,page in ipairs(hookPages) do pageUsage[i]=page.used end
      end
      local prepared,prepareError=pcall(function()
        for _,plan in ipairs(plans) do
          local absolute=plan.length&gt;=14
          local landing
          if absolute then
            landing=allocateMemory(1024)
          else
            landing=reserveHookSlot(plan.site)
          end
          if not landing or landing==0 then
            local message=absolute and G.T('Cannot allocate hook memory.','差し込み用メモリを確保できません。')
              or G.T('No nearby memory available for this instruction.','この命令の近くに差し込み用メモリを確保できません。')
            error(('%s (%s, %X)'):format(message,plan.name,plan.site),0)
          end
          if absolute then plan.allocation=landing end
          local displacement=landing-plan.site-5
          if not absolute and (displacement &lt; -0x80000000 or displacement &gt; 0x7FFFFFFF) then
            error('Cannot allocate nearby hook: '..plan.name,0)
          end
          local script=plan.script:gsub('define%('..plan.cave..',0%)',
            ('define(%s,%X)'):format(plan.cave,landing),1)
          local valid,detail=autoAssembleCheck(script,true,false)
          if not valid then
            error('Could not assemble '..plan.name..': '..tostring(detail),0)
          end
          local assembled,reason=autoAssemble(script)
          if not assembled then
            error('Could not install trampoline '..plan.name..': '..tostring(reason),0)
          end
          local prefix=bytes(landing+plan.prefixOffset,2)
          assert(prefix and prefix[1]==0x49 and prefix[2]==0xBB,'Unexpected hook destination: '..plan.name)
          local jump
          if absolute then
            jump=absoluteJump(landing)
          else
            jump=string.char(0xE9)..string.pack('&lt;i4',displacement)
          end
          jump=jump..string.char(0x90):rep(plan.length-#jump)
          plan.installed={jump:byte(1,#jump)}
        end
      end)
      if not prepared then
        for _,plan in ipairs(plans) do
          if plan.allocation then deAlloc(plan.allocation) end
        end
        for i=#hookPages,1,-1 do
          if pageUsage[i] then
            hookPages[i].used=pageUsage[i]
          else
            deAlloc(hookPages[i].base)
            table.remove(hookPages,i)
          end
        end
        error(prepareError,0)
      end
      local resume=not isPaused()
      if resume then pause() end
      local verified,reason=pcall(function()
        for _,plan in ipairs(plans) do
          assert(equal(bytes(plan.site,plan.length),plan.original),'Method changed before installation: '..plan.name)
          local patch={address=plan.site,original=plan.original,installed=plan.installed}
          state.sites[#state.sites+1]=patch
          local ok,written=pcall(writeBytes,plan.site,plan.installed)
          local current=bytes(plan.site,plan.length)
          if current then patch.installed=current end
          assert(ok and written and equal(current,plan.installed),'Could not install '..plan.name)
        end
      end)
      local restored,restoreError=true,nil
      if not verified then restored,restoreError=pcall(restoreSites) end
      if resume then unpause() end
      if not restored then error(tostring(reason)..' / '..tostring(restoreError),0) end
      if not verified then error(reason,0) end
      return true
    end,
    off=function()
      if not state then return true end
      assert(stateEpoch==G.epoch,'Patch belongs to a previous process attachment')
      local resume=not isPaused()
      if resume then pause() end
      local restored,reason=pcall(restoreSites)
      if resume then unpause() end
      if not restored then error(reason,0) end
      state=nil
      return true
    end,
  }
end

G.F.weatherLock={
  on=function()
    local w=assert(weather())
    assert(readFloat(w+field('Weather','FrontTransition'))&lt;1,'Weather front is changing')
    local patch=scopedPatch('weatherLock',{
      {'Weather','DeterministicUpdate',1,worldScoped=w,
        store={class='Weather',field='FrontTransition',count=1}},
    })
    weatherLock={patch=patch,epoch=G.epoch}
    return patch.on()
  end,
  off=function()
    if not weatherLock then return true end
    if weatherLock.epoch==G.epoch then assert(weatherLock.patch.off()) end
    weatherLock=nil
    return true
  end,
}

local survivalStores={
  Fatigue={{'ApplyFatiguePenalty',1,1},{'UpdateAlive',2,2}},
  Hunger={{'ApplyFatiguePenalty',1,1},{'SimulateSurvivalFactors',2,2},
    {'Eat',4,1},{'ConsumeLiquid',5,1},{'PowerNap',0,1}},
  Thirst={{'ApplyFatiguePenalty',1,1},{'SimulateSurvivalFactors',2,2},
    {'Eat',4,1},{'ConsumeLiquid',5,1},{'PowerNap',0,1}},
  SleepDeprivation={{'SimulateSurvivalFactors',2,3},{'Eat',4,1},{'ConsumeLiquid',5,1}},
  BodyTemperatureInCelsius={{'ApplyFatiguePenalty',1,1},{'SimulateSurvivalFactors',2,1}},
}
for key,writers in pairs(survivalStores) do
  local specs={{'Character','Set'..key,1,
    store={class='Character',field=key,count=1}}}
  for _,writer in ipairs(writers) do
    specs[#specs+1]={'Character',writer[1],writer[2],
      store={class='Character',field=key,count=writer[3]}}
  end
  if key=='BodyTemperatureInCelsius' then
    specs[#specs+1]={'Character','GetBodyTemperatureIncludingFeverInCelsius',0,function()
      local normal=static('Character','BodyTemperatureInCelsiusNormal')
      return ('cmp rcx,r11\njne @normal\nmov r10,%X\nmovss xmm0,[r10]\nret'):format(normal)
    end}
  end
  local initial=G.F[key].on
  local patch=scopedPatch(key,specs)
  G.F[key]={
    on=function()
      patch.on()
      return initial()
    end,
    off=patch.off,
  }
end

local clock
local function clockMode(mode)
  return {
    prepare=function()
      if clock and clock.epoch~=G.epoch then clock=nil end
      if clock and clock.mode~=mode then
        local other=clock.mode=='day' and 'alwaysDay' or 'lockTime'
        local id=other=='alwaysDay' and 512 or 513
        G.feature(other,false)
        assert(not clock,'Could not restore previous world clock')
        local row=getAddressList().getMemoryRecordByID(id)
        if row then row.Active=false end
      end
      return true
    end,
    on=function()
      local w=assert(weather())
      local startHour=field('Weather','StartHourOfDay')
      local dayLength=static('Sun','DayLength')
      local dayTicks=assert(readQword(dayLength))
      assert(dayTicks&gt;0,'Invalid day length')
      local playTicks=assert(readQword(assert(session())+field('Session','PlayTime')))
      local natural=playTicks/dayTicks+assert(readFloat(w+startHour))/24
      local fraction=mode=='day' and 0.5 or natural-math.floor(natural)
      local bits=string.unpack('&lt;I4',string.pack('&lt;f',fraction))
      local patch=scopedPatch(mode,{
        {'Weather','CalcDaysSinceStartFromTime',1,function()
          return ([[
cmp rcx,r11
jne @normal
mov r10,%X
cvtsi2sd xmm0,rdx
cvtsi2sd xmm1,qword ptr [r10]
divsd xmm0,xmm1
cvtsd2ss xmm0,xmm0
movss xmm1,[rcx+%X]
mov r10d,41C00000
movd xmm2,r10d
divss xmm1,xmm2
addss xmm0,xmm1
cvttss2si r10,xmm0
cvtsi2ss xmm1,r10
ucomiss xmm0,xmm1
jae sis_clock_floor
dec r10
cvtsi2ss xmm1,r10
sis_clock_floor:
mov r10d,%X
movd xmm0,r10d
addss xmm0,xmm1
ret
]]):format(dayLength,startHour,bits),{'sis_clock_floor'}
        end,worldScoped=w},
      })
      clock={mode=mode,patch=patch,epoch=G.epoch}
      return patch.on()
    end,
    off=function()
      if not clock or clock.mode~=mode then return true end
      if not clock.patch.off() then return false end
      clock=nil
      return true
    end,
  }
end
G.F.alwaysDay=clockMode('day')
G.F.lockTime=clockMode('time')

local function calledFrom(class,name,count)
  local target=method(class,name,count)
  local address=assert(mono_compile_method(target))
  local info=assert(mono_getJitInfo(address),'Cannot verify caller '..class..'.'..name)
  assert(info.method==target and info.code_start==address and info.code_size&gt;0 and info.code_size&lt;0x100000)
  return info.code_start,info.code_start+info.code_size
end
local function characterGameCall(character,argument,body,labels)
  assert(not isPaused(),G.T('Resume the game first.','ゲームの一時停止を解除してください。'))
  local mailbox=assert(allocateMemory(32))
  assert(mailbox~=0)
  local patch=scopedPatch('characterAction',{
    {'GameImpl','Update',0,worldScoped=mailbox,function()
      return ([[
xor eax,eax
mov r10d,2
lock cmpxchg dword ptr [r11],r10d
jne @normal
sub rsp,38
mov [rsp+20],rcx
mov [rsp+28],r11
mov rcx,[r11+8]
mov rdx,[r11+10]
%s
mov rcx,[rsp+20]
mov r11,[rsp+28]
mov dword ptr [r11],3
add rsp,38
]]):format(body),labels
    end},
  })
  local armed=false
  local ok,err=pcall(function()
    assert(writeInteger(mailbox,1) and writeQword(mailbox+8,character) and writeQword(mailbox+16,argument))
    patch.on()
    -- A failed write can still have changed the remote mailbox.
    armed=true
    assert(writeInteger(mailbox,0),'Could not submit character action')
    local deadline=getTickCount()+3000
    local status=readInteger(mailbox)
    while status and status~=3 and getTickCount()&lt;deadline do
      sleep(10)
      status=readInteger(mailbox)
    end
  end)
  if not G.alive() then
    error(G.T('Connection lost during character action.','人物操作中に接続が切れました。'),0)
  end
  local resume=not isPaused()
  if resume then pause() end
  local cleaned,status=pcall(function()
    local state=readInteger(mailbox)
    if armed and state==0 then
      assert(writeInteger(mailbox,1),'Could not cancel character action')
    end
    patch.off()
    if not armed or state==0 or state==1 or state==3 then
      assert(deAlloc(mailbox)~=false,'Could not release character action memory')
    end
    return state
  end)
  if resume then unpause() end
  if not cleaned then error(status,0) end
  if not ok then error(err,0) end
  if status==3 then return true end
  error(G.T('Character action did not complete on the game thread.','ゲーム側の人物操作の完了を確認できませんでした。'),0)
end
revealCharacter=function(p)
  local authoritative=field('Character','Authoritative')
  p=ref(p+authoritative) or p
  local known=field('Character','NameKnown')
  local scanned=field('Character','BrainScanned')
  local background=field('Character','BackgroundKnown')
  local body=([[
mov byte ptr [rcx+%X],1
mov byte ptr [rcx+%X],1
mov byte ptr [rcx+%X],1
mov rax,%X
call rax
mov r11,[rsp+28]
mov rcx,[r11+8]
mov rax,%X
call rax
mov r11,[rsp+28]
mov rcx,[r11+8]
mov rax,%X
call rax
]]):format(known,scanned,background,
    calledFrom('Character','SetAllPersonalitiesKnown',0),
    calledFrom('Character','SetAllSkillsKnown',0),
    calledFrom('Relationship','SetAllRelationshipsKnown',1))
  characterGameCall(p,0,body)
  assert(readBytes(p+known,1)==1 and readBytes(p+scanned,1)==1 and readBytes(p+background,1)==1,
    G.T('Character information did not update.','人物情報の公開を確認できませんでした。'))
end
recruitOnGameThread=function(p,community)
  return characterGameCall(p,community,('mov rax,%X\ncall rax'):format(calledFrom('Character','SetCommunity',1)))
end
G.F.recoveryMorale={
  on=function()
    recoveredMorale={}
    return true
  end,
  task=function()
    local manager=assert(ref(assert(session())+field('Session','CharacterManager')))
    local roster=objects(assert(ref(manager+field('CharacterManager','Characters'))))
    local cached=field('Character','CachedMorale')
    local dirty=field('Character','CachedMoraleDirty')
    for _,p in ipairs(roster) do
      if hasRecoveredMorale(p) and (readBytes(p+dirty,1)~=0 or readFloat(p+cached)~=100) then
        assert(writeFloat(p+cached,100) and writeBytes(p+dirty,0))
      end
    end
    return true
  end,
  off=function() recoveredMorale={} return true end,
}
recoverCharacter=function(p)
  local conscious=field('Character','Consciousness')
  if assert(readInteger(p+conscious))&gt;=3 or readInteger(p+field('Character','Infection'))~=0 then return false end
  local injuries=field('Character','Injuries')
  local list=assert(ref(p+injuries))
  local size
  for _,entry in ipairs(assert(mono_class_enumFields(assert(mono_object_getClass(list)),false))) do
    if entry.name=='_size' then size=entry.offset break end
  end
  assert(size and assert(readInteger(list+size))&lt;=1024)
  local reset={}
  local function resetFloat(name,value)
    reset[#reset+1]=('mov dword ptr [rcx+%X],%X'):format(field('Character',name),string.unpack('&lt;I4',string.pack('&lt;f',value)))
  end
  for _,stat in ipairs(stats) do resetFloat(stat[1],stat[4]) end
  for _,name in ipairs({'LungCancer','InfectionProgression','BloodAlcoholConcentration','SedativeEffect'}) do resetFloat(name,0) end
  local body=([[
%s
mov dword ptr [rcx+%X],0
mov qword ptr [rcx+%X],0
mov byte ptr [rcx+%X],0
mov r10,[rcx+%X]
mov edx,[r10+%X]
test edx,edx
jle recovery_wake
dec edx
mov rax,%X
call rax
jmp recovery_reload
]]):format(table.concat(reset,'\n'),field('Character','InvisibleStrain'),
    field('Character','InvisibleStrainJustActivated'),field('Character','InfectionRemarkedOn'),
    injuries,size,calledFrom('Character','RemoveInjuryAt',1))
  local loop=([[
recovery_reload:
mov r11,[rsp+28]
mov rcx,[r11+8]
mov r10,[rcx+%X]
mov edx,[r10+%X]
test edx,edx
jle recovery_wake
dec edx
mov rax,%X
call rax
jmp recovery_reload
recovery_wake:
mov byte ptr [rcx+%X],1
mov byte ptr [rcx+%X],1
cmp dword ptr [rcx+%X],0
je recovery_done
xor edx,edx
xor r8d,r8d
mov rax,%X
call rax
recovery_done:
]]):format(injuries,size,calledFrom('Character','RemoveInjuryAt',1),
    field('Character','WantUnityUpdateInjuries'),field('Character','WantUnityUpdateDecals'),
    conscious,calledFrom('Character','SetConsciousness',2))
  characterGameCall(p,0,body..loop,{'recovery_reload','recovery_wake','recovery_done'})
  assert(readInteger(p+conscious)==0 and readInteger(list+size)==0,'Recovery readback failed')
  if not G.active.recoveryMorale then G.feature('recoveryMorale',true) end
  recoveredMorale[identity(p)]={epoch=G.epoch,object=p}
  assert(writeFloat(p+field('Character','CachedMorale'),100) and writeBytes(p+field('Character','CachedMoraleDirty'),0))
  return true
end
local instantCraft=scopedPatch('instantCraft',{
  -- Use the registered clip events, then let Character.Update finish the goal.
  {'Character','TriggerAnimEvents',1,function()
    local actions={57,58,68,69,76,93,95,96,97,98,99,100,107,137,138,139,140,141,142}
    local anim=field('Character','CurrentActionAnim')
    local selected={}
    for _,value in ipairs(actions) do
      selected[#selected+1]=('cmp eax,%X\nje complete'):format(value)
    end
    local function compile(class,name,count)
      return calledFrom(class,name,count)
    end
    return ([[
cmp byte ptr [rcx+%X],0
je @normal
cmp dword ptr [rcx+%X],-1
jne @normal
mov eax,[rcx+%X]
%s
jmp @normal
complete:
sub rsp,48
mov [rsp+20],rcx
mov [rsp+28],rdx
mov rax,%X
call rax
test al,al
je resume
mov rcx,[rsp+20]
mov r10,[rcx+%X]
test r10,r10
je resume
cmp byte ptr [r10+%X],0
jne resume
mov [rsp+30],r10
mov rax,%X
call rax
mov [rsp+38],rax
mov rcx,[rsp+30]
mov rax,%X
call rax
test rax,rax
jle resume
mov rcx,[rsp+20]
movss xmm0,[rcx+%X]
xorps xmm1,xmm1
ucomiss xmm0,xmm1
jbe resume
mov rdx,[rsp+38]
sub rdx,[rsp+28]
xorps xmm1,xmm1
cvtsi2ss xmm1,rdx
mulss xmm1,xmm0
cvttss2si rdx,xmm1
test rdx,rdx
jns start_ready
xor edx,edx
start_ready:
cmp rdx,rax
jg skip
lea r9,[rax+1]
mov r8,rdx
mov rdx,[rsp+20]
mov [rsp+40],rax
mov rcx,[rsp+30]
mov rax,%X
call rax
mov rcx,[rsp+20]
mov r10,[rsp+30]
cmp [rcx+%X],r10
jne skip
mov rax,%X
call rax
mov rcx,[rsp+20]
mov r10,[rsp+38]
mov rdx,[rsp+40]
sub rdx,rax
sub r10,rdx
sub [rcx+%X],r10
skip:
add rsp,48
ret
resume:
mov rcx,[rsp+20]
mov rdx,[rsp+28]
add rsp,48
]]):format(field('Character','IsCurrentActionAnimFromGoal'),field('Character','PredictedFreshFrame'),
      anim,table.concat(selected,'\n'),compile('Character','IsControllableByPlayer',0),
      field('Character','AnimWrapper'),field('AnimWrapper','Looped'),
      compile('Character','GetActionAnimTime',0),compile('AnimWrapper','GetDuration',0),
      field('Character','AnimSpeed'),compile('AnimWrapper','TriggerEventsBetween',3),
      field('Character','AnimWrapper'),compile('Character','GetActionAnimDuration',0),
      field('Character','AnimStartTime')),{'complete','start_ready','skip','resume'}
  end},
  {'MoveToAndMine','GetNextSubGoal',2,function()
    local rich=static('MoveToAndMine','ProgressNeededToExtractRichDeposits')
    local isMine=assert(mono_findClass('','Mine'))
    local isRock=assert(mono_findClass('','Rock'))
    local isBoulder=assert(mono_findClass('','Boulder'))
    local instanceOf=assert(getAddressSafe('mono-2.0-bdwgc.dll.mono_object_isinst') or getAddressSafe('mono_object_isinst'))
    local sample=assert(mono_array_new(assert(mono_findClass('System','Int32')),4))
    local arrayData=assert(mono_arrayinstance_getItemAddress(sample,0))-sample
    assert(arrayData&gt;0 and mono_arrayinstance_getItemAddress(sample,1)-sample-arrayData==4)
    return ([[
cmp dword ptr [rdx+%X],-1
jne @normal
mov eax,[rdx+%X]
cmp eax,89
je qualify
cmp eax,8A
je qualify
cmp eax,8C
je qualify
cmp eax,8D
jne @normal
qualify:
sub rsp,58
mov [rsp+20],rcx
mov [rsp+28],rdx
mov [rsp+30],r8
mov rcx,rdx
mov rax,%X
call rax
test al,al
je restore_args
mov rcx,[rsp+20]
mov rax,%X
call rax
test rax,rax
je restore_args
mov [rsp+38],rax
mov rcx,rax
mov rdx,%X
mov rax,%X
call rax
test rax,rax
je surface
mov rcx,[rsp+20]
mov edx,[rcx+%X]
cmp edx,3
ja restore_args
mov [rsp+40],edx
mov rcx,[rsp+38]
mov rax,%X
call rax
test eax,eax
jle restore_args
mov rcx,[rsp+38]
mov rcx,[rcx+%X]
test rcx,rcx
je restore_args
mov edx,[rsp+40]
lea rcx,[rcx+rdx*4+%X]
jmp set_progress
surface:
mov rcx,[rsp+38]
mov rdx,%X
mov rax,%X
call rax
test rax,rax
je boulder
lea rcx,[rax+%X]
jmp rich_progress
boulder:
mov rcx,[rsp+38]
mov rdx,%X
mov rax,%X
call rax
test rax,rax
je restore_args
lea rcx,[rax+%X]
rich_progress:
mov rax,%X
mov eax,[rax]
set_progress:
cmp [rcx],eax
jge restore_args
mov [rcx],eax
restore_args:
mov rcx,[rsp+20]
mov rdx,[rsp+28]
mov r8,[rsp+30]
add rsp,58
]]):format(field('Character','PredictedFreshFrame'),field('Character','CurrentActionAnim'),
      calledFrom('Character','IsControllableByPlayer',0),calledFrom('Goal','GetTargetObject',0),
      isMine,instanceOf,field('MoveToAndMine','MineralType'),calledFrom('Mine','GetMiningProgressNeededToExtract',1),
      field('Mine','MiningProgress'),arrayData,isRock,instanceOf,field('Rock','MiningProgress'),
      isBoulder,instanceOf,field('Boulder','MiningProgress'),rich),
      {'qualify','surface','boulder','rich_progress','set_progress','restore_args'}
  end},
  {'MoveToAndChop','GetNextSubGoal',2,function()
    return ([[
cmp dword ptr [rdx+%X],-1
jne @normal
mov eax,[rdx+%X]
sub eax,5F
cmp eax,4
ja @normal
cmp eax,2
je @normal
sub rsp,48
mov [rsp+20],rcx
mov [rsp+28],rdx
mov [rsp+30],r8
mov [rsp+38],eax
mov rcx,rdx
mov rax,%X
call rax
test al,al
je restore_args
mov rcx,[rsp+20]
cmp dword ptr [rsp+38],2
ja log_target
mov rax,%X
call rax
test rax,rax
je restore_args
lea rcx,[rax+%X]
jmp set_progress
log_target:
mov rax,%X
call rax
test rax,rax
je restore_args
lea rcx,[rax+%X]
set_progress:
cmp dword ptr [rcx],1
jge restore_args
mov dword ptr [rcx],1
restore_args:
mov rcx,[rsp+20]
mov rdx,[rsp+28]
mov r8,[rsp+30]
add rsp,48
]]):format(field('Character','PredictedFreshFrame'),field('Character','CurrentActionAnim'),
      calledFrom('Character','IsControllableByPlayer',0),calledFrom('Goal','GetTargetTree',0),
      field('TreeProp','ChoppedDownProgress'),calledFrom('Goal','GetTargetFallenTree',0),
      field('FallenTreeProp','ChoppedLogProgress')),{'log_target','set_progress','restore_args'}
  end},
  -- ActionAnim values from the supported Assembly-CSharp.dll.
  {'BuildAnim','Update',2,function()
    local building=field('BuildAnim','Building')
    local propInfo=field('Prop','UnderConstructionInfo')
    local singleInfo=field('SingleTileProp','UnderConstructionInfo')
    local propDeleted=field('Prop','_deleted')
    local singleDeleted=field('SingleTileProp','_deleted')
    local isProp=assert(mono_findClass('','Prop'))
    local isSingle=assert(mono_findClass('','SingleTileProp'))
    local instanceOf=assert(getAddressSafe('mono-2.0-bdwgc.dll.mono_object_isinst') or getAddressSafe('mono_object_isinst'))
    local recipe=field('UnderConstructionInfo','Recipe')
    local time=field('UnderConstructionInfo','TimeTakenOnResource')
    local duration=field('Recipe','CraftingTime')
    local count=calledFrom('Recipe','CountIngredients',0)
    local completed=calledFrom('UnderConstructionInfo','IsCompleted',0)
    local consume=calledFrom('BuildAnim','UseResource',1)
    local progress=calledFrom('UnderConstructionInfo','GetProgress',2)
    return ([[
cmp rdx,r11
jne @normal
mov r10,[rcx+%X]
test r10,r10
je @normal
sub rsp,68
mov [rsp+20],rcx
mov [rsp+28],rdx
mov [rsp+30],r8
mov [rsp+38],r10
mov rcx,r10
mov rdx,%X
mov rax,%X
call rax
test rax,rax
jne sis_build_prop
mov rcx,[rsp+38]
mov rdx,%X
mov rax,%X
call rax
test rax,rax
je sis_build_resume
cmp dword ptr [rax+%X],2
je sis_build_resume
mov r10,[rax+%X]
jmp sis_build_set_time
sis_build_prop:
cmp dword ptr [rax+%X],2
je sis_build_resume
mov r10,[rax+%X]
sis_build_set_time:
test r10,r10
je sis_build_resume
mov rax,[r10+%X]
test rax,rax
je sis_build_resume
mov eax,[rax+%X]
mov [r10+%X],eax
mov [rsp+40],r10
mov rcx,[r10+%X]
mov rax,%X
call rax
test eax,eax
jle sis_build_resume
cmp eax,1000
ja sis_build_resume
mov [rsp+48],eax
sis_build_consume:
mov rcx,[rsp+40]
mov rax,%X
call rax
test al,al
jne sis_build_resume
mov rcx,[rsp+40]
lea rdx,[rsp+50]
lea r8,[rsp+54]
mov rax,%X
call rax
mov eax,[rsp+50]
mov [rsp+58],eax
mov rcx,[rsp+20]
mov rdx,[rsp+28]
mov rax,%X
call rax
mov rcx,[rsp+40]
lea rdx,[rsp+50]
lea r8,[rsp+54]
mov rax,%X
call rax
mov eax,[rsp+50]
cmp eax,[rsp+58]
je sis_build_resume
dec dword ptr [rsp+48]
jne sis_build_consume
sis_build_resume:
mov rcx,[rsp+20]
mov rdx,[rsp+28]
mov r8,[rsp+30]
add rsp,68
]]):format(building,isProp,instanceOf,isSingle,instanceOf,singleDeleted,singleInfo,propDeleted,propInfo,recipe,duration,time,recipe,count,completed,progress,consume,progress),
      {'sis_build_prop','sis_build_set_time','sis_build_consume','sis_build_resume'}
  end},
  {'CraftAnim','Update',2,function()
    local recipe=field('CraftAnim','FollowingRecipe')
    local time=field('CraftAnim','TimeTaken')
    local duration=field('Recipe','CraftingTime')
    return ([[
cmp rdx,r11
jne @normal
mov r10,[rcx+%X]
test r10,r10
je @normal
mov r10d,[r10+%X]
mov [rcx+%X],r10d
]]):format(recipe,duration,time)
  end},
  {'CraftingProp','PropUpdate',2,function()
    local owner=field('CraftingProp','CurrentCrafter')
    local recipe=field('CraftingProp','CraftingRecipe')
    local time=field('CraftingProp','CraftingTimeSpent')
    local duration=field('Recipe','CraftingTime')
    return ([[
cmp [rcx+%X],r11
jne @normal
mov r10,[rcx+%X]
test r10,r10
je @normal
mov r10d,[r10+%X]
mov [rcx+%X],r10d
]]):format(owner,recipe,duration,time)
  end},
})
G.F.accelerateWork=instantCraft

G.F.stealthMode=scopedPatch('stealthMode',{
  {'Character','GetVisibility',6,function()
    return [[
cmp rcx,r11
jne @normal
mov r10,[rsp+30]
mov byte ptr [r10],0
mov r10,[rsp+38]
mov byte ptr [r10],0
xorps xmm0,xmm0
ret
]]
  end},
  {'Character','IsInSmellRange',2,function()
    return [[
cmp r8,r11
jne @normal
xor eax,eax
ret
]]
  end},
  {'Target','MarkVisible',2,function()
    return ('cmp [rcx+%X],r11\njne @normal\nret'):format(field('Target','Object'))
  end},
  {'Target','OnHeardFootsteps',1,function()
    return ('cmp [rcx+%X],r11\njne @normal\nret'):format(field('Target','Object'))
  end},
  {'Target','UpdateLastKnownInfo',1,function()
    return ('cmp [rcx+%X],r11\njne @normal\nret'):format(field('Target','Object'))
  end},
  {'Target','OnSmelled',1,function()
    return ('cmp [rcx+%X],r11\njne @normal\nret'):format(field('Target','Object'))
  end},
  {'Target','OnAttackedMe',3,function()
    return ('cmp [rcx+%X],r11\njne @normal\nret'):format(field('Target','Object'))
  end},
  {'Target','OnGrabbedMe',1,function()
    return ('cmp [rcx+%X],r11\njne @normal\nret'):format(field('Target','Object'))
  end},
  {'Target','get_FullyTracked',0,function()
    return ('cmp [rcx+%X],r11\njne @normal\nxor eax,eax\nret'):format(field('Target','Object'))
  end},
  {'Target','get_Visible',0,function()
    return ('cmp [rcx+%X],r11\njne @normal\nxor eax,eax\nret'):format(field('Target','Object'))
  end},
  {'Target','get_HasLineOfSightIgnoringPlantCover',0,function()
    return ('cmp [rcx+%X],r11\njne @normal\nxor eax,eax\nret'):format(field('Target','Object'))
  end},
  {'Target','get_CanSmellTarget',0,function()
    return ('cmp [rcx+%X],r11\njne @normal\nxor eax,eax\nret'):format(field('Target','Object'))
  end},
})
local function injuryFieldOffset(name)
  local first,found
  for _,f in ipairs(assert(mono_class_enumFields(mono_findClass('','Injury'),false))) do
    if not f.isStatic and not f.isConst then
      first=math.min(first or f.offset,f.offset)
      if f.name==name then found=f.offset end
    end
  end
  assert((first==0 or first==16) and found and found&gt;=first,'Invalid Injury layout')
  return found-first
end
G.F.Infection=scopedPatch('infection',{
  {'Injury','GetInfectionProgressionRate',2,function()
    return 'cmp rdx,r11\njne @normal\nxorps xmm0,xmm0\nret'
  end},
  {'Character','WillActivateInvisibleStrain',0,function()
    return 'cmp rcx,r11\njne @normal\nxor eax,eax\nret'
  end},
  {'Character','WillActivateDoomStrain',1,function()
    return 'cmp rcx,r11\njne @normal\nxor eax,eax\nret'
  end},
  {'Character','Zombify',1,function()
    return 'cmp rcx,r11\njne @normal\nret'
  end},
  {'Character','ActivateInvisibleStrain',0,function()
    return 'cmp rcx,r11\njne @normal\nret'
  end},
  {'Character','SwallowInfectedFood',1,function()
    return [[
cmp rcx,r11
jne @normal
ret
]]
  end},
  {'Character','OnDamaged',19,function()
    local strain=field('Character','InvisibleStrain')
    return ([[
cmp rcx,r11
jne @normal
test rdx,rdx
je sis_infection_strip
cmp dword ptr [r9],2
jne sis_infection_strip
cmp dword ptr [rsp+30],0
jne sis_infection_strip
cmp dword ptr [rdx+%X],0
je sis_infection_strip
mov r10,[rsp+48]
mov dword ptr [r10],0
mov r10,[rsp+A0]
mov byte ptr [r10],0
ret
sis_infection_strip:
mov dword ptr [rsp+30],0
]]):format(strain),{'sis_infection_strip'}
  end},
  {'Character','ApplyBandage',5,function()
    return 'cmp rcx,r11\njne @normal\nmov dword ptr [rsp+28],0'
  end},
})
local ignoreWeight=scopedPatch('ignoreWeight',{
  {'EquipmentContainer','CacheEncumbered',1,
    store={class='Character',field='Encumbered',count=1,byte=true}},
  {'TileObject','GetAvailableInventorySpace',0,function()
    local first,last=calledFrom('PlantableCrop','Harvest',4)
    return ([[
cmp rcx,r11
jne @normal
mov r10,%X
cmp [rsp],r10
jb @normal
mov r10,%X
cmp [rsp],r10
jae @normal
mov r10d,49742400
movd xmm0,r10d
ret
]]):format(first,last)
  end},
  {'EquipmentContainer','GetWeight',1,function()
    local takeStart,takeEnd=calledFrom('TakeAnim','Take',8)
    local tradeStart,tradeEnd=calledFrom('TradePage','ConfirmTrade',2)
    return ([[
cmp rdx,r11
jne @normal
mov r10,%X
cmp [rsp],r10
jb sis_weight_trade
mov r10,%X
cmp [rsp],r10
jae sis_weight_trade
mov r10d,FF800000
movd xmm0,r10d
ret
sis_weight_trade:
mov r10,%X
cmp [rsp],r10
jb @normal
mov r10,%X
cmp [rsp],r10
jae @normal
mov r10d,7F800000
movd xmm0,r10d
ret
]]):format(takeStart,takeEnd,tradeStart,tradeEnd),{'sis_weight_trade'}
  end},
  {'TileObject','HasInventorySpaceFor',1,function()
    return 'cmp rcx,r11\njne @normal\nmov eax,1\nret'
  end},
  {'TileObject','GetAmountOfEquipmentThatCanBeStored',1,function()
    local amount=field('Equipment','Amount')
    return ('cmp rcx,r11\njne @normal\ntest rdx,rdx\nje @normal\nmov eax,[rdx+%X]\nret'):format(amount)
  end},
  {'Equipment','GetMaxTransferrableTo',1,function()
    local amount=field('Equipment','Amount')
    return ('cmp rdx,r11\njne @normal\nmov eax,[rcx+%X]\nret'):format(amount)
  end},
})
G.F.ignoreWeight={
  on=function()
    local p=assert(player())
    local address=p+field('Character','Encumbered')
    ignoreWeight.on()
    return assert(writeBytes(address,0))
  end,
  off=function()
    ignoreWeight.off()
    if not session() then return true end
    local p=player()
    local inv=p and ref(p+field('Character','Inventory'))
    if inv then invoke(method('EquipmentContainer','CacheEncumbered',1),inv,{p}) end
    return true
  end,
}
local multiplierValues={}
local function configureMultiplier(key,id,makePatch)
  local installed,row,address,editAddress,epoch
  G.F[key]={
    on=function()
      if epoch~=G.epoch or not address then
        local buffer=assert(allocateMemory(8))
        assert(buffer~=0)
        address,epoch=buffer,G.epoch
      end
      local value=multiplierValues[key] or 1
      assert(writeFloat(address,value))
      editAddress=address+4
      assert(writeFloat(editAddress,value))
      installed=makePatch(address)
      installed.on()
      local parent=assert(getAddressList().getMemoryRecordByID(id))
      local record=getAddressList().createMemoryRecord()
      row=record.ID
      G.track(record,'Multiplier','倍率')
      record.Type=vtSingle
      record.Address=hex(editAddress)
      record.Description=G.T('Multiplier','倍率')
      record.appendToEntry(parent)
      record.OnActivate=function(_,before) return not before end
      local id=record.ID
      record.OnValueChangedByUser=function()
        if row~=id or not G.dynamic[id] or epoch~=G.epoch then return end
        local v=readFloat(editAddress)
        if not v or v~=v or math.abs(v)==math.huge then
          writeFloat(editAddress,multiplierValues[key] or 1)
          G.msg('Enter a finite multiplier.','有限の倍率を入力してください。')
          return
        end
        if writeFloat(address,v) then
          multiplierValues[key]=v
        else
          writeFloat(editAddress,multiplierValues[key] or 1)
          G.msg("Could not apply multiplier.","倍率を反映できませんでした。")
        end
      end
      parent.Collapsed=false
      return true
    end,
    off=function()
      if installed then assert(installed.off()); installed=nil end
      if row then
        local id=row
        row=nil
        local current=getAddressList().getMemoryRecordByID(id)
        if G.dynamic[id] and current then
          current.OnValueChangedByUser=nil
          current.OnActivate=nil
          current.destroy()
        end
        G.dynamic[id]=nil
      end
      -- Keep the buffer valid for any thread returning through the trampoline.
      return true
    end,
  }
end
configureMultiplier('xpMultiplier',122,function(address)
  return scopedPatch('xpMultiplier',{{'Skillset','AddProgress',3,function()
    return ('cmp rdx,r11\njne @normal\nmov r10,%X\nmulss xmm3,[r10]'):format(address)
  end}})
end)
configureMultiplier('moveMultiplier',211,function(address)
  return scopedPatch('moveMultiplier',{{'Character','AddVelocityXZ',2,function()
    local first,last=calledFrom('Character','OnProcessMoveAction',2)
    return ([[
cmp rcx,r11
jne @normal
mov r10,%X
cmp [rsp],r10
jb @normal
mov r10,%X
cmp [rsp],r10
jae @normal
mov r10,%X
movss xmm5,[r10]
shufps xmm5,xmm5,0
movq xmm4,rdx
mulps xmm4,xmm5
movq rdx,xmm4
]]):format(first,last,address)
  end}})
end)
G.F.keepAmmo=scopedPatch('keepAmmo',{
  {'Character','HasInfiniteAmmo',1,function()
    -- Compare the equipped item with the requested weapon. The reload path
    -- supplies an AmmoWeapon and still chooses the game's normal ammo type.
    local equipped=field('Character','EquippedItem')
    local first,last=calledFrom('ReloadAnim','OnAnimationEvent',3)
    return ([[
cmp rcx,r11
jne @normal
test rdx,rdx
je @normal
cmp [rcx+%X],rdx
jne @normal
mov r10,%X
cmp [rsp],r10
jb @normal
mov r10,%X
cmp [rsp],r10
jae @normal
mov eax,1
ret
]]):format(equipped,first,last)
  end},
  {'AmmoWeapon','ConsumeAmmo',1,function()
    local owner=field('Equipment','InventoryOwner')
    local equipped=field('Character','EquippedItem')
    local first,last=calledFrom('AmmoWeapon','OnFired',9)
    return ([[
cmp [rcx+%X],r11
jne @normal
cmp [r11+%X],rcx
jne @normal
mov r10,%X
cmp [rsp],r10
jb @normal
mov r10,%X
cmp [rsp],r10
jae @normal
ret
]]):format(owner,equipped,first,last)
  end},
})
local function consumptionGuard(calls,body)
  local lines,labels={},{}
  for index,call in ipairs(calls) do
    local first,last=calledFrom(table.unpack(call))
    local skip='sis_consume_next_'..index
    labels[#labels+1]=skip
    lines[#lines+1]=('mov r10,%X\ncmp [rsp],r10\njb %s\nmov r10,%X\ncmp [rsp],r10\njae %s\n%s\n%s:'):format(first,skip,last,skip,body,skip)
  end
  lines[#lines+1]='jmp @normal'
  return table.concat(lines,'\n'),labels
end
G.F.keepItems=scopedPatch('keepItems',{
  {'EquipmentContainer','UseItem',3,function()
    local owner=field('Equipment','InventoryOwner')
    local amount=field('Equipment','Amount')
    local inventory=field('Character','Inventory')
    return ([[
cmp rdx,r11
jne @normal
cmp [rdx+%X],rcx
jne @normal
test r8,r8
je @normal
cmp [r8+%X],r11
jne @normal
mov eax,[r8+%X]
cmp eax,r9d
cmovg eax,r9d
ret
]]):format(inventory,owner,amount)
  end},

  {'Equipment','IncrementAmount',1,function()
    local owner=field('Equipment','InventoryOwner')
    local code,labels=consumptionGuard({
      {'PlantCropsAnim','OnAnimationEvent',3},
      {'Throwable','OnFired',9},
      {'BuildAnim','UseResource',1},
    },'ret')
    return ('cmp [rcx+%X],r11\njne @normal\ntest edx,edx\njns @normal\n'):format(owner)..code,labels
  end},
  {'Equipment','DrainLiquid',1,function()
    local owner=field('Equipment','InventoryOwner')
    local volume=field('Equipment','LiquidContentsAmount')
    local code,labels=consumptionGuard({
      {'Character','ConsumeLiquid',4},
      {'Recipe','UseIngredients',7},
      {'EquipmentContainer','DrainLiquidOfType',6},
      {'WaterPlantAnim','PourWater',2},
    },('xorps xmm0,xmm0\nmaxss xmm0,xmm1\nminss xmm0,[rcx+%X]\nret'):format(volume))
    return ('cmp [rcx+%X],r11\njne @normal\n'):format(owner)..code,labels
  end},
})
G.F.noMaterials=scopedPatch('noMaterials',{
  {'BuildAnim','UseResource',1,function()
    local building=field('BuildAnim','Building')
    local propInfo=field('Prop','UnderConstructionInfo')
    local singleInfo=field('SingleTileProp','UnderConstructionInfo')
    local propDeleted=field('Prop','_deleted')
    local singleDeleted=field('SingleTileProp','_deleted')
    local prop=assert(mono_findClass('','Prop'))
    local single=assert(mono_findClass('','SingleTileProp'))
    local instanceOf=assert(getAddressSafe('mono-2.0-bdwgc.dll.mono_object_isinst') or getAddressSafe('mono_object_isinst'))
    local recipe=field('UnderConstructionInfo','Recipe')
    local function listField(owner,name,member)
      local class=assert(mono_findClass('',owner))
      for _,entry in ipairs(assert(mono_class_enumFields(class,false))) do
        if entry.name==name then
          local list=assert(mono_field_getClass(entry.field))
          for _,f in ipairs(assert(mono_class_enumFields(list,false))) do
            if f.name==member then return f.offset end
          end
        end
      end
      error('Missing list field '..owner..'.'..name..'.'..member,0)
    end
    local ingredients=field('Recipe','Ingredients')
    local size=listField('Recipe','Ingredients','_size')
    local items=listField('Recipe','Ingredients','_items')
    for _,name in ipairs({'Prototypes','LiquidTypes'}) do
      assert(listField('Ingredient',name,'_size')==size and listField('Ingredient',name,'_items')==items)
    end
    local ingredientClass=assert(mono_findClass('','Ingredient'))
    local sample=assert(mono_array_new(ingredientClass,2))
    assert(sample~=0)
    local first=assert(mono_arrayinstance_getItemAddress(sample,0))
    assert(mono_arrayinstance_getItemAddress(sample,1)-first==8)
    local data=first-sample
    assert(data&gt;=16 and data&lt;=64)
    local amount=field('Ingredient','Amount')
    local volume=field('Ingredient','LiquidAmount')
    local prototypes=field('Ingredient','Prototypes')
    local liquids=field('Ingredient','LiquidTypes')
    local used=calledFrom('UnderConstructionInfo','GetAmountUsedOfIngredient',1)
    local useItem=calledFrom('UnderConstructionInfo','UseIngredient',2)
    local useLiquid=calledFrom('UnderConstructionInfo','UseLiquidIngredient',2)
    return ([[
cmp rdx,r11
jne @normal
mov r10,[rcx+%X]
test r10,r10
je @normal
sub rsp,58
mov [rsp+20],r10
mov rcx,r10
mov rdx,%X
mov rax,%X
call rax
test rax,rax
jne sis_free_prop
mov rcx,[rsp+20]
mov rdx,%X
mov rax,%X
call rax
test rax,rax
je sis_free_done
cmp dword ptr [rax+%X],2
je sis_free_done
mov r10,[rax+%X]
jmp sis_free_info
sis_free_prop:
cmp dword ptr [rax+%X],2
je sis_free_done
mov r10,[rax+%X]
sis_free_info:
test r10,r10
je sis_free_done
mov [rsp+28],r10
mov rax,[r10+%X]
test rax,rax
je sis_free_done
mov rax,[rax+%X]
test rax,rax
je sis_free_done
mov [rsp+30],rax
mov dword ptr [rsp+38],0
sis_free_loop:
mov rax,[rsp+30]
mov r10d,[rsp+38]
cmp r10d,[rax+%X]
jge sis_free_done
cmp r10d,1000
jae sis_free_done
mov rax,[rax+%X]
test rax,rax
je sis_free_done
mov rdx,[rax+r10*8+%X]
test rdx,rdx
je sis_free_next
mov [rsp+40],rdx
mov rcx,[rsp+28]
mov rax,%X
call rax
mov r10,[rsp+40]
mov rax,[r10+%X]
test rax,rax
je sis_free_liquid
cmp dword ptr [rax+%X],0
jle sis_free_next
mov rax,[rax+%X]
test rax,rax
je sis_free_next
mov rdx,[rax+%X]
test rdx,rdx
je sis_free_next
cvttss2si eax,xmm0
mov r8d,[r10+%X]
sub r8d,eax
jle sis_free_next
mov rcx,[rsp+28]
mov rax,%X
call rax
jmp sis_free_next
sis_free_liquid:
mov rax,[r10+%X]
test rax,rax
je sis_free_next
cmp dword ptr [rax+%X],0
jle sis_free_next
movss xmm2,[r10+%X]
subss xmm2,xmm0
xorps xmm1,xmm1
ucomiss xmm2,xmm1
jbe sis_free_next
mov rax,[rax+%X]
test rax,rax
je sis_free_next
mov rdx,[rax+%X]
test rdx,rdx
je sis_free_next
mov rcx,[rsp+28]
mov rax,%X
call rax
sis_free_next:
inc dword ptr [rsp+38]
jmp sis_free_loop
sis_free_done:
add rsp,58
ret
]]):format(building,prop,instanceOf,single,instanceOf,singleDeleted,singleInfo,propDeleted,propInfo,recipe,ingredients,
      size,items,data,used,prototypes,size,items,data,amount,useItem,liquids,size,volume,items,data,useLiquid),
      {'sis_free_prop','sis_free_info','sis_free_loop','sis_free_liquid','sis_free_next','sis_free_done'}
  end},
  {'BuildGoal','DoesBuilderHaveNeededResources',1,function()
    local building=field('BuildGoal','CurrentBuilding')
    return ('cmp rdx,r11\njne @normal\ncmp qword ptr [rcx+%X],0\nje @normal\nmov eax,1\nret'):format(building)
  end},
  {'Recipe','HasAllIngredients',4,function()
    return 'cmp rdx,r11\njne @normal\nmov eax,1\nret'
  end},
  {'Recipe','GetMaxCraftableUsingItemsInInventory',2,function()
    return 'cmp rdx,r11\njne @normal\nmov eax,3E7\nret'
  end},
  {'Recipe','UseIngredients',7,function()
    local owner=assert(mono_findClass('','CraftingProp'))
    local listClass
    for _,f in ipairs(assert(mono_class_enumFields(owner,false))) do
      if f.name=='UsedIngredients' then listClass=mono_field_getClass(f.field); break end
    end
    local clear
    for _,m in ipairs(assert(mono_class_enumMethods(assert(listClass),false))) do
      if m.name=='Clear' and #assert(mono_method_get_parameters(m.method)).parameters==0 then
        clear=mono_compile_method(m.method)
        break
      end
    end
    assert(clear and clear~=0,'Cannot resolve ingredient list Clear')
    local progress=calledFrom('Skillset','AddProgress',3)
    local skillset=field('Character','Skillset')
    local skill=field('Recipe','SkillType')
    local experience=field('Recipe','SkillProgression')
    return ([[
cmp rdx,r11
jne @normal
sub rsp,38
mov [rsp+20],rcx
mov [rsp+28],rdx
test r9,r9
je sis_craft_empty
mov rcx,r9
mov rax,%X
call rax
sis_craft_empty:
mov rcx,[rsp+20]
mov rdx,[rsp+28]
mov r8d,[rcx+%X]
cvtsi2ss xmm3,dword ptr [rcx+%X]
lea rcx,[rdx+%X]
mov rax,%X
call rax
add rsp,38
mov r10,[rsp+28]
mov dword ptr [r10],0
mov r10,[rsp+30]
mov dword ptr [r10],0
mov r10,[rsp+38]
mov byte ptr [r10],0
ret
]]):format(clear,skill,experience,skillset,progress),{'sis_craft_empty'}
  end},
})

local restoreBloodLoss=G.F.BloodLoss
local injuryUsers={}
local injuryEpoch
local injuryFlags
local injuryPatch=scopedPatch('injuryFilter',{
  {'Character','AddInjury',1,function()
    local infection=injuryFieldOffset('InfectionType')
    local original=injuryFieldOffset('OriginalInfectionType')
    return ([[
cmp rcx,r11
jne @normal
mov r10,%X
test byte ptr [r10],1
jne sis_injury_no_wound
test byte ptr [r10],2
je @normal
mov dword ptr [rdx+%X],0
mov dword ptr [rdx+%X],0
jmp @normal
sis_injury_no_wound:
ret
]]):format(injuryFlags,infection,original),{'sis_injury_no_wound'}
  end},
})
local function useInjuryFilter(bit,enabled)
  if injuryEpoch~=G.epoch then injuryUsers={}; injuryFlags=nil; injuryEpoch=G.epoch end
  if enabled and not next(injuryUsers) then
    if not injuryFlags or injuryFlags==0 then injuryFlags=assert(allocateMemory(8)) end
    assert(injuryFlags~=0 and writeBytes(injuryFlags,0),'Cannot initialize injury filter')
    injuryUsers[bit]=true
    injuryPatch.on()
  elseif enabled then
    injuryUsers[bit]=true
  elseif injuryUsers[bit] then
    if not injuryUsers[3-bit] then
      assert(writeBytes(injuryFlags,0))
      injuryPatch.off()
      -- A thread may still be returning through the old hook. Keep its flag
      -- storage valid until the target process exits.
      injuryUsers[bit]=nil
      return true
    end
    injuryUsers[bit]=nil
  else
    return true
  end
  local flags=(injuryUsers[1] and 1 or 0)+(injuryUsers[2] and 2 or 0)
  return writeBytes(injuryFlags,flags)
end
local function addInjuryFilter(key,bit)
  local feature=G.F[key]
  local start,stop=feature.on,feature.off
  feature.on=function()
    assert(useInjuryFilter(bit,true))
    return start()
  end
  feature.off=function()
    if stop then assert(stop()) end
    return useInjuryFilter(bit,false)
  end
end
local preventDisease=scopedPatch('preventDisease',{
  {'Character','SetLungCancer',1,
    store={class='Character',field='LungCancer',count=1}},
})
G.F.InjuriesAndDisease={
  on=function()
    preventDisease.on()
    return maintainInjuriesAndDisease()
  end,
  off=preventDisease.off,
}
addInjuryFilter('InjuriesAndDisease',1)
addInjuryFilter('Infection',2)
local originalGod=G.F.God
local ignoreCombatImpact=scopedPatch('ignoreCombatImpact',{
  {'Character','OnExplosionImpact',10,function()
    return 'cmp rcx,r11\njne @normal\ntest rdx,rdx\nje @normal\nxorps xmm0,xmm0\nret'
  end},
  {'Character','OnBurned',10,function()
    return 'cmp rcx,r11\njne @normal\ntest rdx,rdx\nje @normal\nxorps xmm0,xmm0\nret'
  end},
})
G.F.God={
  boundary=originalGod.boundary,
  on=function()
    ignoreCombatImpact.on()
    return originalGod.on()
  end,
  off=function()
    ignoreCombatImpact.off()
    return originalGod.off()
  end,
}
local preventBloodLoss=scopedPatch('unlimitedHealth',{
  {'Character','OnDie',8,function()
    return 'cmp rcx,r11\njne @normal\nret'
  end},
  {'Character','ApplyBloodLoss2',10,function()
    return 'cmp rcx,r11\njne @normal\nxorps xmm0,xmm0\nret'
  end},
})
G.F.BloodLoss={
  on=function()
    preventBloodLoss.on()
    return restoreBloodLoss.on()
  end,
  off=preventBloodLoss.off,
}


-- Use the ordinary hit pipeline and leave the defender's health limits intact.
G.F.oneHitKill=scopedPatch('oneHitKill',{
  {'Character','GetModifiedDamageToMe2',10,function()
    local enemy=assert(mono_compile_method(method('Character','IsEnemy',1)))
    assert(enemy~=0,'Cannot resolve Character.IsEnemy')
    local sparring=field('Character','SparringPartner')
    return ([[cmp rdx,r11
jne @normal
cmp rcx,rdx
je @normal
cmp [rcx+%X],rdx
je @normal
sub rsp,58
mov [rsp+20],rcx
mov [rsp+28],rdx
mov [rsp+30],r9
movss [rsp+38],xmm2
mov rax,%X
mov rcx,rdx
mov rdx,[rsp+20]
call rax
mov rcx,[rsp+20]
mov rdx,[rsp+28]
mov r9,[rsp+30]
movss xmm2,[rsp+38]
add rsp,58
test al,al
je @normal
mov r10,[rsp+58]
test r10,r10
je @normal
mov byte ptr [r10],0
mov eax,461C4000
movd xmm0,eax
ret]]):format(sparring,enemy)
  end},
})


local approvalMode,approvalEpoch
local approvalPatch=scopedPatch('maxApproval',{
  {'Character','CalcApprovalRating',6,function()
    return ([[cmp rdx,r11
jne @normal
cmp rcx,rdx
je @normal
cmp dword ptr [rcx+%X],3
jae @normal
cmp dword ptr [rcx+%X],0
jne @normal
mov r10,%X
cmp byte ptr [r10],2
je approval_apply
mov eax,[r10+8]
test eax,eax
jz approval_community
push r9
mov r9d,[rcx+%X]
approval_scan:
cmp [r10+rax*4+8],r9d
je approval_selected
dec eax
jnz approval_scan
pop r9
jmp approval_community
approval_selected:
pop r9
jmp approval_apply
approval_community:
cmp byte ptr [r10],1
jne @normal
mov r10,[rdx+%X]
test r10,r10
je @normal
cmp [rcx+%X],r10
jne @normal
approval_apply:
sub rsp,58
mov [rsp+40],r8
mov [rsp+48],r9
mov r10,[rsp+80]
mov [rsp+20],r10
mov r10,[rsp+88]
mov [rsp+28],r10
mov r10,[rsp+90]
mov [rsp+30],r10
call @normal
mov r8,[rsp+40]
mov r9,[rsp+48]
mov dword ptr [r8],42C80000
mov dword ptr [r9],42C80000
add rsp,58
ret]]):format(field('Character','Consciousness'),field('Character','Infection'),
      approvalMode,field('BaseObject','Id'),field('Character','Community'),field('Character','Community')),{'approval_apply','approval_scan','approval_selected','approval_community'}
  end,firstName='character'},
})
local function prepareApproval()
  if approvalEpoch~=G.epoch then
    approvalMode=assert(allocateMemory(65552))
    assert(approvalMode~=0 and writeBytes(approvalMode,0) and writeInteger(approvalMode+8,0))
    approvalEpoch=G.epoch
    G.selectedOpinion=nil
  end
end
G.F.selectedOpinion={
  on=function()
    prepareApproval()
    if not G.active.memberApproval and not G.active.npcApproval then approvalPatch.on() end
    G.selectedOpinion={epoch=G.epoch,ids={},count=0}
    return true
  end,
  off=function()
    if approvalEpoch~=G.epoch then return true end
    assert(writeInteger(approvalMode+8,0))
    G.selectedOpinion=nil
    if not G.active.memberApproval and not G.active.npcApproval then return approvalPatch.off() end
    return true
  end,
}
maximizeOpinion=function(p)
  if not G.active.selectedOpinion then G.feature('selectedOpinion',true) end
  local state=assert(G.selectedOpinion)
  local id=identity(p)
  if state.ids[id] then return end
  local count=state.count+1
  assert(count&lt;=16384 and writeInteger(approvalMode+8+count*4,id))
  assert(writeInteger(approvalMode+8,count))
  state.ids[id]=true
  state.count=count
end
local function approvalFeature(other,mode)
  return {
    on=function()
      assert(player(),G.T('No active character.','操作中のキャラクターがいません。'))
      prepareApproval()
      if not G.active[other] and not G.active.selectedOpinion then approvalPatch.on() end
      return assert(writeBytes(approvalMode,G.active[other] and 2 or mode))
    end,
    off=function()
      if approvalEpoch~=G.epoch then return true end
      if G.active[other] and not G.active[other].cleanup then
        return assert(writeBytes(approvalMode,3-mode))
      end
      assert(writeBytes(approvalMode,0))
      if G.active.selectedOpinion then return true end
      return approvalPatch.off()
    end,
  }
end
G.F.memberApproval=approvalFeature('npcApproval',1)
G.F.npcApproval=approvalFeature('memberApproval',2)

do
    local Picker={}
    local function text(v)
      return v == nil and '' or tostring(v)
    end
    function Picker.matches(value, query, numeric)
      query = text(query):match('^%s*(.-)%s*$')
      if query == '' then
        return true
      end
      if numeric then
        local n = tonumber(value)
        if not n then
          return false
        end
        local number = '[+-]?%d*%.?%d+'
        local lo, hi = query:match('^(' .. number .. ')%s*%-%s*(' .. number .. ')$')
        if lo then
          return n &gt;= tonumber(lo) and n &lt;= tonumber(hi)
        end
        local op, rhs = query:match('^([&lt;&gt;!=]+)%s*(' .. number .. ')$')
        rhs = tonumber(rhs)
        if rhs then
          if op == '&gt;=' then
            return n &gt;= rhs
          elseif op == '&lt;=' then
            return n &lt;= rhs
          elseif op == '&gt;' then
            return n &gt; rhs
          elseif op == '&lt;' then
            return n &lt; rhs
          elseif op == '=' or op == '==' then
            return n == rhs
          elseif op == '!=' then
            return n ~= rhs
          end
        end
        local exact = tonumber(query)
        if exact then
          return n == exact
        end
      end
      return text(value):lower():find(query:lower(), 1, true) ~= nil
    end
    function Picker.view(rows, columns, queries, sortIndex, ascending)
      local shown = {}
      for _, row in ipairs(rows) do
        local include = true
        for i, col in ipairs(columns) do
          if not Picker.matches(row[col.key], queries[i], col.numeric) then
            include = false
            break
          end
        end
        if include then
          shown[#shown + 1] = row
        end
      end
      local col = columns[sortIndex]
      table.sort(shown, function(a, b)
        local x, y = a[col.key], b[col.key]
        if x == nil or y == nil then
          if x ~= y then
            return y == nil
          end
        else
          if not col.numeric then
            x = text(x):lower()
            y = text(y):lower()
          end
          if x ~= y then
            if ascending then
              return x &lt; y
            else
              return x &gt; y
            end
          end
        end
        return a.id &lt; b.id
      end)
      return shown
    end
    function Picker.selectedRows(state,list)
      local rows={}
      local index=-1
      while true do
        local nextIndex=tonumber(sendMessage(list.Handle,0x100c,index,2)) or -1
        if nextIndex&lt;=index or nextIndex&gt;=#state.shown then break end
        rows[#rows+1]=state.shown[nextIndex+1]
        index=nextIndex
      end
      return rows
    end
    function Picker.create(options)
      local state = {
        rows = options.rows,
        columns = options.columns,
        shown = {},
        filters = {},
        sortIndex = options.sortIndex or #options.columns,
        ascending = true,
        lastId = options.current,
        result = nil,
        updating = false,
      }
      local form = createForm(false)
      state.form = form
      G.forms[form]=true
      form.Name = 'WP26_EntityPicker101'
      form.Caption = options.title
      form.Position = 'poScreenCenter'
      form.BorderStyle = 'bsSizeable'
      form.ClientWidth = options.multi and 1200 or 840
      form.ClientHeight = math.max(330, math.min(620, 180 + #options.rows * 32))
      form.Font.Name = 'Yu Gothic UI'
      form.Font.Size = 10
      form.Color = 0x252525
      form.Font.Color = 0xf0f0f0
      pcall(function()
        form.Constraints.MinWidth = 700
        form.Constraints.MinHeight = 380
      end)
      local list = createListView(form)
      state.list = list
      list.Name = 'WP26_EntityTable101'
      list.ViewStyle = 'vsReport'
      list.RowSelect = true
      list.HideSelection = false
      list.ReadOnly = true
      list.MultiSelect = options.multi == true
      list.OwnerData = true
      list.Color = 0x252525
      list.Font.Color = 0xf0f0f0
      local headers = {}
      local headerButtons = {}
      state.headerButtons = headerButtons
      list.ShowColumnHeaders = options.multi == true
      for i, col in ipairs(state.columns) do
        local h = list.Columns.add()
        h.Caption = col.title
        headers[i] = h
        local button = createButton(form)
        button.Name = 'WP26_EntitySort101_' .. i
        button.Caption = col.title
        headerButtons[i] = button
        button.OnClick = function()
          state.sort(i)
        end
        local edit = createEdit(form)
        edit.Name = 'WP26_EntityFilter101_' .. i
        edit.Text = ''
        edit.Color = 0x353535
        edit.Font.Color = 0xffffff
        edit.ShowHint = true
        pcall(function()
          edit.TextHint = G.T('Search ' .. col.title, col.title .. '検索')
        end)
        edit.Hint = col.numeric and G.T('Exact value, range10-30, or &gt;=10', '数値、範囲10-30、&gt;=10で検索')
          or G.T('Search this column', 'この列を検索')
        state.filters[i] = edit
      end
      local count = createLabel(form)
      state.count = count
      count.Font.Color = 0xcfcfcf
      local clear = createButton(form)
      state.clear = clear
      clear.Caption = G.T('Clear filters', '検索解除')
      local accept = createButton(form)
      state.accept = accept
      accept.Name = 'WP26_EntityAccept101'
      accept.Caption = G.T('Select', '選択')
      accept.Default = true
      local cancel = createButton(form)
      state.cancel = cancel
      cancel.Name = 'WP26_EntityCancel101'
      cancel.Caption = G.T('Cancel', 'キャンセル')
      cancel.Cancel = true
      local quantityLabel, quantityEdit, quantityError
      if options.quantity then
        quantityLabel = createLabel(form)
        quantityLabel.Caption = G.T('Quantity','個数')
        quantityEdit = createEdit(form)
        quantityEdit.Text = '1'
        quantityEdit.Hint = '1–999'
        quantityEdit.ShowHint = true
        pcall(function() quantityLabel.FocusControl = quantityEdit end)
        quantityError = createLabel(form)
        quantityError.Caption = G.T('Enter 1–999','1～999を入力')
        quantityError.Visible = false
        local tabOrder = 0
        for _, filter in ipairs(state.filters) do
          filter.TabOrder = tabOrder
          tabOrder = tabOrder + 1
        end
        for _, control in ipairs({list,quantityEdit,clear,cancel,accept}) do
          control.TabOrder = tabOrder
          tabOrder = tabOrder + 1
        end
      end
      local function readQuantity()
        if not quantityEdit then return end
        local text = quantityEdit.Text
        local value = text:match('^%s*%d+%s*$') and tonumber(text)
        if value and value &gt;= 1 and value &lt;= 999 then return value end
      end
      function state.selectedId()
        local item = list.Selected
        local row = item and state.shown[item.Index + 1]
        return row and row.id or nil
      end
      local function updateAccept()
        local selectedCount=options.multi and #Picker.selectedRows(state,list) or 0
        if options.multi then
          count.Caption=G.T('Shown ','表示 ')..#state.shown..' / '..#state.rows
            ..G.T('   Selected ','   選択 ')..selectedCount
        end
        local valid = not quantityEdit or readQuantity() ~= nil
        if quantityError then quantityError.Visible = not valid end
        accept.Enabled = valid and ((options.multi and selectedCount==1)
          or (not options.multi and state.selectedId()~=nil))
      end
      if quantityEdit then quantityEdit.OnChange = updateAccept end
      list.OnData = function(sender, item)
        local row = state.shown[item.Index + 1]
        if not row then
          return
        end
        item.Caption = text(row[state.columns[1].key])
        item.SubItems.clear()
        for i = 2, #state.columns do
          item.SubItems.add(text(row[state.columns[i].key]))
        end
      end
      function state.render()
        if state.updating then
          return
        end
        local selected = state.selectedId()
        if selected ~= nil then
          state.lastId = selected
        end
        local queries = {}
        for i, edit in ipairs(state.filters) do
          queries[i] = edit.Text
        end
        local selectedIds={}
        if options.multi then
          for _,row in ipairs(Picker.selectedRows(state,list)) do selectedIds[row.id]=true end
        end
        state.shown = Picker.view(
          state.rows,
          state.columns,
          queries,
          state.sortIndex,
          state.ascending
        )
        list.beginUpdate()
        list.Items.clear()
        list.Items.Count = #state.shown
        local selectedIndex = -1
        for i, row in ipairs(state.shown) do
          if row.id == state.lastId then
            selectedIndex = i - 1
            break
          end
        end
        list.ItemIndex = selectedIndex
        if options.multi and next(selectedIds) then
          for i,row in ipairs(state.shown) do
            list.Items[i-1].Selected=selectedIds[row.id]==true
          end
        end
        list.endUpdate()
        if state.lastId == nil and #state.shown &gt; 0 then
          list.ItemIndex = 0
          state.lastId = state.shown[1].id
        end
        for i, h in ipairs(headers) do
          h.Caption = state.columns[i].title .. (i == state.sortIndex and (state.ascending and ' ↑' or ' ↓') or '')
          headerButtons[i].Caption = h.Caption
        end
        count.Caption = G.T('Shown ', '表示 ') .. #state.shown .. ' / ' .. #state.rows
        updateAccept()
      end
      function state.sort(index)
        if state.sortIndex == index then
          state.ascending = not state.ascending
        else
          state.sortIndex = index
          state.ascending = true
        end
        state.render()
      end
      list.OnColumnClick = function(sender, column)
        for i, h in ipairs(headers) do
          if h == column or h.Caption == column.Caption then
            state.sort(i)
            return
          end
        end
      end
      list.OnSelectItem = function()
        updateAccept()
      end
      local function choose()
        local id = state.selectedId()
        if options.multi then
          local selected=Picker.selectedRows(state,list)
          id=#selected==1 and selected[1].id or nil
        end
        local quantity = readQuantity()
        if id ~= nil and (not quantityEdit or quantity) then
          state.quantity = quantity
          state.result = id
          form.ModalResult = 1
        end
      end
      accept.OnClick = choose
      list.OnDblClick = choose
      if options.back then
        cancel.Caption = G.T('Back','戻る')
        cancel.Cancel = false
      end
      cancel.OnClick = function()
        state.result = nil
        state.back = options.back
        form.ModalResult = 2
      end
      for _, edit in ipairs(state.filters) do
        edit.OnKeyDown = function(sender, key)
          if key == 40 and #state.shown &gt; 0 then
            if list.ItemIndex &lt; 0 then
              list.ItemIndex = 0
            end
            list.setFocus()
            return 0
          end
          return key
        end
      end
      for _, edit in ipairs(state.filters) do
        edit.OnChange = state.render
      end
      clear.OnClick = function()
        state.updating = true
        for _, edit in ipairs(state.filters) do
          edit.Text = ''
        end
        state.updating = false
        state.render()
      end
      list.OnCustomDrawItem = function(sender, item, drawState)
        local row = state.shown[item.Index + 1]
        if not row then
          return false
        end
        local rect = item.displayRect('drBounds')
        local canvas = sender.Canvas
        canvas.Font.Color = 0xffffff
        canvas.Brush.Color = item.Selected and 0x805020 or 0x252525
        canvas.fillRect(rect.Left, rect.Top, sender.Width, rect.Bottom)
        local x = rect.Left
        for i, col in ipairs(state.columns) do
          local width = headers[i].Width
          canvas.textRect(
            { Left = x + 6, Top = rect.Top, Right = x + width - 6, Bottom = rect.Bottom },
            x + 6,
            rect.Top + 2,
            text(row[col.key])
          )
          x = x + width
        end
        return false
      end
      local function layout()
        local w, h = form.ClientWidth, form.ClientHeight
        local x = 14
        local available = w - 48
        local fixed, flex = 0, 0
        for _, col in ipairs(state.columns) do
          if col.flex then
            flex = flex + 1
          else
            fixed = fixed + (col.width or 100)
          end
        end
        for i, col in ipairs(state.columns) do
          local width = col.flex and math.max(180, math.floor((available - fixed) / math.max(1, flex)))
            or (col.width or 100)
          headers[i].Width = width
          headers[i].MinWidth = width
          headers[i].MaxWidth = width
          local edit = state.filters[i]
          edit.Left = x
          edit.Top = 54
          edit.Width = width - 6
          edit.Visible = not options.multi or x+width &lt;= w-14
          edit.Height = 28
          x = x + width
          local button = headerButtons[i]
          button.Left = edit.Left
          button.Top = 91
          button.Width = width
          button.Visible = not options.multi
          button.Height = 30
        end
        list.Left = 14
        list.Top = options.multi and 88 or 122
        list.Width = w - 28
        list.Height = h - list.Top - 64
        count.Left = 16
        count.Top = h - 43
        clear.Left = 155
        clear.Top = h - 49
        clear.Width = 110
        clear.Height = 33
        if quantityEdit then
          quantityLabel.Left = 16
          quantityLabel.Top = h - 43
          quantityEdit.Left = 80
          quantityEdit.Top = h - 49
          quantityEdit.Width = 64
          quantityError.Left = 16
          quantityError.Top = h - 72
          list.Height = list.Height - 12
          count.Left = 278
        end
        accept.Left = w - (options.back and 136 or 266)
        accept.Top = h - 49
        accept.Width = 118
        accept.Height = 33
        cancel.Left = w - (options.back and 266 or 136)
        cancel.Top = h - 49
        cancel.Width = 118
        cancel.Height = 33
      end
      form.OnResize = layout
      form.OnShow = state.render
      layout()
      state.render()
      function state.destroy()
        G.forms[form]=nil
        form.destroy()
      end
      return state
    end
    function Picker.show(options)
      local generation=G.pickerGeneration
      local state = Picker.create(options)
      local ok, result = pcall(function()
        state.form.showModal()
        return state.result
      end)
      state.destroy()
      if not ok then
        error(result, 0)
      end
      if GT~=G or G.pickerGeneration~=generation then return nil,false end
      return result,state.back,state.quantity
    end

function G.showCharacterList(title,rows,columns,actions,open,refresh)
  local state=Picker.create({title=title,rows=rows,columns=columns,multi=true,sortIndex=2})
  local popup=createPopupMenu(state.form)
  local targets={}
  state.list.PopupMenu=popup
  state.list.OnMouseDown=function(sender,button,x,y)
    if button~='mbRight' and button~=1 then return end
    local item=sender.getItemAt(x,y)
    local clicked=item and state.shown[item.Index+1]
    targets={}
    if clicked then
      local selected=Picker.selectedRows(state,sender)
      local includes=false
      for _,row in ipairs(selected) do if row==clicked then includes=true break end end
      targets=includes and selected or {clicked}
      if not includes then
        for _,row in ipairs(selected) do
          for i,shown in ipairs(state.shown) do
            if row==shown then sender.Items[i-1].Selected=false break end
          end
        end
        item.Selected=true
      end
    end
    for i,action in ipairs(actions) do
      local eligible=0
      for _,row in ipairs(targets) do
        if not action[4] or action[4](row) then eligible=eligible+1 end
      end
      popup.Items[i-1].Visible=not action[4] or eligible&gt;0
      popup.Items[i-1].Enabled=eligible&gt;0 and (action[3]~=open or #targets==1)
    end
  end
  for index,action in ipairs(actions) do
    local item=createMenuItem(popup)
    item.Caption=G.T(action[1],action[2])
    item.Enabled=false
    item.OnClick=function()
      if #targets==0 then return end
      if action[3]==open then
        if #targets==1 then
          state.result=targets[1].id
          state.form.ModalResult=1
        end
        return
      end
      local selected=targets
      G.userRun(function()
        local failures={}
        if index==2 then
          local ok,err=pcall(action[3],selected[1],selected)
          if not ok then failures[#failures+1]=tostring(err) end
        else
          for _,row in ipairs(selected) do
            if not action[4] or action[4](row) then
              local connected=pcall(G.require)
              if not connected then
                failures[#failures+1]=G.T('Connection lost; batch stopped.','接続が切れたため一括処理を中断しました。')
                break
              end
              local ok,err=pcall(action[3],row)
              if not ok then
                failures[#failures+1]=row.name..': '..tostring(err)
              end
            end
          end
        end
        for _,row in ipairs(selected) do pcall(refresh,row) end
        if G.forms[state.form] then state.render() end
        if #failures&gt;0 then error(table.concat(failures,'\n'),0) end
      end)
    end
    popup.Items.add(item)
  end
  local ok,err=pcall(function() state.form.showModal() end)
  local selected=state.result
  state.destroy()
  if not ok then error(err,0) end
  if selected then
    for _,row in ipairs(rows) do if row.id==selected then return open(row) end end
  end
end

function G.choose(title,rows,multiple,options)
  assert(not multiple,'WP10 picker expects a single target')
  local mapped, originals={},{}
  for _,row in ipairs(rows) do
    mapped[#mapped+1]={id=row.id,displayId=row.displayId or row.id,name=G.T(row.en or row.name,row.ja or row.name)}
    originals[row.id]=row
  end
  local columns={
    {key='displayId',title='ID',numeric=type(mapped[1] and mapped[1].displayId)=='number',width=100},
    {key='name',title=G.T('Name','名前'),flex=true},
  }
  if options and options.nameOnly then table.remove(columns,1) end
  local id,back,quantity=Picker.show({title=title,rows=mapped,columns=columns,back=options and options.back,sortIndex=options and options.sortIndex,quantity=options and options.quantity})
  return id~=nil and originals[id] or nil,back,quantity
end
end

G.initCollapse()
G.applyLang()
local function installCloseGuard()
local mainForm=getMainForm()
local previousCloseQuery=mainForm.OnCloseQuery
local function restoreBeforeClose(sender)
  if previousCloseQuery and previousCloseQuery(sender)==false then return false end
  if not G.pid then return true end
  if G.closing or G.disarming or G.inTick then return false end
  G.closing=true
  local ok,err=pcall(G.shutdown,false,true)
  G.closing=false
  if not ok then
    G.msg(tostring(err),tostring(err))
    return false
  end
  return true
end
mainForm.OnCloseQuery=restoreBeforeClose
G.removeCloseGuard=function()
  local ok,err=pcall(function()
    if mainForm.OnCloseQuery==restoreBeforeClose then
      mainForm.OnCloseQuery=previousCloseQuery
    end
  end)
  if not ok then G.msg(tostring(err),tostring(err)) end
end
end
local closeGuardOK=pcall(installCloseGuard)
if not closeGuardOK then
  G.msg('Automatic restoration on CE close is unavailable. Use Attach / All Off before closing.','CE終了時の自動復元を登録できません。終了前にアタッチ／一括解除を実行してください。')
end
G.initialized=true
</LuaScript>
</CheatTable>