<?xml version="1.0" encoding="utf-8"?>
<CheatTable CheatEngineTableVersion="45">
  <CheatEntries>
    <CheatEntry>
      <ID>1</ID>
      <Description>"[ATTACH / ALL OFF] Attach to DQXOffline.exe + disable everything"</Description>
      <Color>00A5FF</Color>
      <VariableType>Auto Assembler Script</VariableType>
      <AssemblerScript>
{ Game    : DQXOffline.exe  (DRAGON QUEST X OFFLINE / Rise of the Five Tribes Offline, Steam app 1358750)
  Author  : Black_wolf
  CheatVer:2.0.1.0-001-013

  Memo: ゲームバージョンは 2.0.1.0（DQXOffline.exe の ProductVersion）
        ゲームエンジンは Unreal Engine 4.26（プロジェクト名 Holiday、ゲームロジックの大半は Blueprint）
        モジュールのビルド日時（PEタイムスタンプ）: 0x645B97AF = 2023-05-10 13:10:07 UTC / SizeOfImage 0x504F000
        ネイティブコードは AOB、ゲームデータは UE4 リフレクション（GNames / GUObjectArray / FProperty）で実行時に解決する。
}

[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.oneShot(memrec, function() DQX.disableAll(memrec) end)
{$asm}
[DISABLE]
</AssemblerScript>
    </CheatEntry>
    <CheatEntry>
      <ID>2</ID>
      <Description>"0. Settings &amp; README"</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 (default)"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.oneShot(memrec, function() DQX.setLang('en') end)
{$asm}
[DISABLE]
</AssemblerScript>
            </CheatEntry>
            <CheatEntry>
              <ID>5</ID>
              <Description>"日本語 / Japanese"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.oneShot(memrec, function() DQX.setLang('ja') end)
{$asm}
[DISABLE]
</AssemblerScript>
            </CheatEntry>
          </CheatEntries>
        </CheatEntry>
        <CheatEntry>
          <ID>6</ID>
          <Description>"README"</Description>
          <Options moManualExpandCollapse="1"/>
          <Color>FFFF00</Color>
          <GroupHeader>1</GroupHeader>
          <CheatEntries>
            <CheatEntry>
              <ID>7</ID>
              <Description>"1) Load your save, then run [ATTACH / ALL OFF] at the top of the table."</Description>
              <GroupHeader>1</GroupHeader>
            </CheatEntry>
            <CheatEntry>
              <ID>8</ID>
              <Description>"2) For game v2.0.1.0 (Steam, 2023-05-10 build). On other builds, features not found are disabled."</Description>
              <GroupHeader>1</GroupHeader>
            </CheatEntry>
            <CheatEntry>
              <ID>9</ID>
              <Description>"3) Orange = script (check to run / toggle), cyan = group, no color = value (double-click to edit)."</Description>
              <GroupHeader>1</GroupHeader>
            </CheatEntry>
            <CheatEntry>
              <ID>10</ID>
              <Description>"4) Messages appear in the Lua Engine window (0. &gt; Diagnostics &gt; Lua Engine)."</Description>
              <GroupHeader>1</GroupHeader>
            </CheatEntry>
            <CheatEntry>
              <ID>11</ID>
              <Description>"5) Save to another slot before using editors and one-shot actions."</Description>
              <GroupHeader>1</GroupHeader>
            </CheatEntry>
            <CheatEntry>
              <ID>12</ID>
              <Description>"6) Data changes (drops, chests, fish, recipes, multipliers) are undone when turned off."</Description>
              <GroupHeader>1</GroupHeader>
            </CheatEntry>
            <CheatEntry>
              <ID>13</ID>
              <Description>"7) Some features call game functions: do not pause or minimize the game while using them."</Description>
              <GroupHeader>1</GroupHeader>
            </CheatEntry>
          </CheatEntries>
        </CheatEntry>
        <CheatEntry>
          <ID>15</ID>
          <Description>"Diagnostics (only when the table does not work)"</Description>
          <Options moManualExpandCollapse="1"/>
          <Color>FFFF00</Color>
          <GroupHeader>1</GroupHeader>
          <CheatEntries>
            <CheatEntry>
              <ID>16</ID>
              <Description>"Lua Engine (check = open / uncheck = close)"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.openLuaEngine()
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.closeLuaEngine() end
{$asm}
</AssemblerScript>
            </CheatEntry>
            <CheatEntry>
              <ID>17</ID>
              <Description>"AOB scan self-test (CE is busy for a while; prints to Lua Engine)"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.oneShot(memrec, function() DQX.scanSelfTest() end)
{$asm}
[DISABLE]
</AssemblerScript>
            </CheatEntry>
            <CheatEntry>
              <ID>18</ID>
              <Description>"Environment / connection info (CE, process, module, build)"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.oneShot(memrec, function() DQX.envReport() end)
{$asm}
[DISABLE]
</AssemblerScript>
            </CheatEntry>
            <CheatEntry>
              <ID>19</ID>
              <Description>"Status: unresolved code / active hooks / active features"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.oneShot(memrec, function() DQX.statusReport() end)
{$asm}
[DISABLE]
</AssemblerScript>
            </CheatEntry>
            <CheatEntry>
              <ID>20</ID>
              <Description>"Add key items... (CAUTION: event flags are not set)"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.oneShot(memrec, function() DQX.addKeyItemPick() end)
{$asm}
[DISABLE]
</AssemblerScript>
            </CheatEntry>
          </CheatEntries>
        </CheatEntry>
      </CheatEntries>
    </CheatEntry>
    <CheatEntry>
      <ID>100</ID>
      <Description>"01. Gameplay"</Description>
      <Options moManualExpandCollapse="1"/>
      <Color>FFFF00</Color>
      <GroupHeader>1</GroupHeader>
      <CheatEntries>
        <CheatEntry>
          <ID>140</ID>
          <Description>"Battle"</Description>
          <Options moManualExpandCollapse="1"/>
          <Color>FFFF00</Color>
          <GroupHeader>1</GroupHeader>
          <CheatEntries>
            <CheatEntry>
              <ID>101</ID>
              <Description>"Invincible in battle (party takes no damage, HP/MP always full)"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('invincible', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('invincible', false) end
{$asm}
</AssemblerScript>
              <Hotkeys>
                <Hotkey>
                  <Action>Toggle Activation</Action>
                  <Keys>
                    <Key>97</Key>
                  </Keys>
                  <ID>0</ID>
                  <ActivateSound TTS="EN">{MRDescription} Activated</ActivateSound>
                  <DeactivateSound TTS="EN">{MRDescription} Deactivated</DeactivateSound>
                </Hotkey>
              </Hotkeys>
            </CheatEntry>
            <CheatEntry>
              <ID>102</ID>
              <Description>"One-hit kill (damage to enemies)"</Description>
              <Options moHideChildren="1"/>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('onehit', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('onehit', false) end
{$asm}
</AssemblerScript>
              <CheatEntries>
                <CheatEntry>
                  <ID>103</ID>
                  <Description>"Damage dealt to enemies"</Description>
                  <ShowAsSigned>0</ShowAsSigned>
                  <VariableType>Float</VariableType>
                  <Address>dqxVars+10</Address>
                </CheatEntry>
              </CheatEntries>
            </CheatEntry>
            <CheatEntry>
              <ID>105</ID>
              <Description>"Always critical (party)"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('crit', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('crit', false) end
{$asm}
</AssemblerScript>
              <Hotkeys>
                <Hotkey>
                  <Action>Toggle Activation</Action>
                  <Keys>
                    <Key>98</Key>
                  </Keys>
                  <ID>0</ID>
                  <ActivateSound TTS="EN">{MRDescription} Activated</ActivateSound>
                  <DeactivateSound TTS="EN">{MRDescription} Deactivated</DeactivateSound>
                </Hotkey>
              </Hotkeys>
            </CheatEntry>
            <CheatEntry>
              <ID>124</ID>
              <Description>"Always catch enemies off guard"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('surprise', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('surprise', false) end
{$asm}
</AssemblerScript>
              <Hotkeys>
                <Hotkey>
                  <Action>Toggle Activation</Action>
                  <Keys>
                    <Key>99</Key>
                  </Keys>
                  <ID>0</ID>
                  <ActivateSound TTS="EN">{MRDescription} Activated</ActivateSound>
                  <DeactivateSound TTS="EN">{MRDescription} Deactivated</DeactivateSound>
                </Hotkey>
              </Hotkeys>
            </CheatEntry>
            <CheatEntry>
              <ID>126</ID>
              <Description>"Always super high tension (party)"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('sht', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('sht', false) end
{$asm}
</AssemblerScript>
            </CheatEntry>
            <CheatEntry>
              <ID>125</ID>
              <Description>"Always steal (every party member, from each monster it defeats)"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('steal', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('steal', false) end
{$asm}
</AssemblerScript>
              <Hotkeys>
                <Hotkey>
                  <Action>Toggle Activation</Action>
                  <Keys>
                    <Key>100</Key>
                  </Keys>
                  <ID>0</ID>
                  <ActivateSound TTS="EN">{MRDescription} Activated</ActivateSound>
                  <DeactivateSound TTS="EN">{MRDescription} Deactivated</DeactivateSound>
                </Hotkey>
              </Hotkeys>
            </CheatEntry>
            <CheatEntry>
              <ID>107</ID>
              <Description>"Enemies always drop items (drop rate 100%)"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('drop', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('drop', false) end
{$asm}
</AssemblerScript>
              <Hotkeys>
                <Hotkey>
                  <Action>Toggle Activation</Action>
                  <Keys>
                    <Key>101</Key>
                  </Keys>
                  <ID>0</ID>
                  <ActivateSound TTS="EN">{MRDescription} Activated</ActivateSound>
                  <DeactivateSound TTS="EN">{MRDescription} Deactivated</DeactivateSound>
                </Hotkey>
              </Hotkeys>
            </CheatEntry>
            <CheatEntry>
              <ID>108</ID>
              <Description>"Always "see through" monsters (Miyaburu)"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('miyaburu', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('miyaburu', false) end
{$asm}
</AssemblerScript>
              <Hotkeys>
                <Hotkey>
                  <Action>Toggle Activation</Action>
                  <Keys>
                    <Key>102</Key>
                  </Keys>
                  <ID>0</ID>
                  <ActivateSound TTS="EN">{MRDescription} Activated</ActivateSound>
                  <DeactivateSound TTS="EN">{MRDescription} Deactivated</DeactivateSound>
                </Hotkey>
              </Hotkeys>
            </CheatEntry>
            <CheatEntry>
              <ID>138</ID>
              <Description>"Invincible in battle 2 (no damage, MP always full, HP kept at 1/4)"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('invincible2', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('invincible2', false) end
{$asm}
</AssemblerScript>
            </CheatEntry>
            <CheatEntry>
              <ID>139</ID>
              <Description>"Maximum number of monsters in battle (symbol encounters)"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('monMax', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('monMax', false) end
{$asm}
</AssemblerScript>
            </CheatEntry>
            <CheatEntry>
              <ID>145</ID>
              <Description>"Reincarnated (tensei) monster lottery always wins (symbol encounters)"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('tenseiMax', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('tenseiMax', false) end
{$asm}
</AssemblerScript>
            </CheatEntry>
          </CheatEntries>
        </CheatEntry>
        <CheatEntry>
          <ID>141</ID>
          <Description>"Field &amp; travel"</Description>
          <Options moManualExpandCollapse="1"/>
          <Color>FFFF00</Color>
          <GroupHeader>1</GroupHeader>
          <CheatEntries>
            <CheatEntry>
              <ID>121</ID>
              <Description>"Movement speed multiplier (player)"</Description>
              <Options moHideChildren="1"/>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('speed', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('speed', false) end
{$asm}
</AssemblerScript>
              <Hotkeys>
                <Hotkey>
                  <Action>Toggle Activation</Action>
                  <Keys>
                    <Key>103</Key>
                  </Keys>
                  <ID>0</ID>
                  <ActivateSound TTS="EN">{MRDescription} Activated</ActivateSound>
                  <DeactivateSound TTS="EN">{MRDescription} Deactivated</DeactivateSound>
                </Hotkey>
              </Hotkeys>
              <CheatEntries>
                <CheatEntry>
                  <ID>122</ID>
                  <Description>"Speed multiplier (0.1 steps, e.g. 1.5)"</Description>
                  <ShowAsSigned>0</ShowAsSigned>
                  <VariableType>Float</VariableType>
                  <Address>dqxVars+24</Address>
                </CheatEntry>
              </CheatEntries>
            </CheatEntry>
            <CheatEntry>
              <ID>129</ID>
              <Description>"Dolboard anywhere (ride in towns too)"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('doruAny', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('doruAny', false) end
{$asm}
</AssemblerScript>
              <Hotkeys>
                <Hotkey>
                  <Action>Toggle Activation</Action>
                  <Keys>
                    <Key>104</Key>
                  </Keys>
                  <ID>0</ID>
                  <ActivateSound TTS="EN">{MRDescription} Activated</ActivateSound>
                  <DeactivateSound TTS="EN">{MRDescription} Deactivated</DeactivateSound>
                </Hotkey>
              </Hotkeys>
            </CheatEntry>
            <CheatEntry>
              <ID>130</ID>
              <Description>"Rura stone usable anywhere"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('ruraAny', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('ruraAny', false) end
{$asm}
</AssemblerScript>
              <Hotkeys>
                <Hotkey>
                  <Action>Toggle Activation</Action>
                  <Keys>
                    <Key>105</Key>
                  </Keys>
                  <ID>0</ID>
                  <ActivateSound TTS="EN">{MRDescription} Activated</ActivateSound>
                  <DeactivateSound TTS="EN">{MRDescription} Deactivated</DeactivateSound>
                </Hotkey>
              </Hotkeys>
            </CheatEntry>
            <CheatEntry>
              <ID>131</ID>
              <Description>"Rura stone: register all destinations"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.oneShot(memrec, function() DQX.ruraUnlockAll() end)
{$asm}
[DISABLE]
</AssemblerScript>
            </CheatEntry>
            <CheatEntry>
              <ID>109</ID>
              <Description>"Open without keys"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('nokey', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('nokey', false) end
{$asm}
</AssemblerScript>
              <Hotkeys>
                <Hotkey>
                  <Action>Toggle Activation</Action>
                  <Keys>
                    <Key>111</Key>
                  </Keys>
                  <ID>0</ID>
                  <ActivateSound TTS="EN">{MRDescription} Activated</ActivateSound>
                  <DeactivateSound TTS="EN">{MRDescription} Deactivated</DeactivateSound>
                </Hotkey>
              </Hotkeys>
            </CheatEntry>
            <CheatEntry>
              <ID>113</ID>
              <Description>"Take all treasure chests in the map"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.oneShot(memrec, function() DQX.openMapChests() end)
{$asm}
[DISABLE]
</AssemblerScript>
              <Hotkeys>
                <Hotkey>
                  <Action>Toggle Activation</Action>
                  <Keys>
                    <Key>96</Key>
                  </Keys>
                  <ID>0</ID>
                  <ActivateSound TTS="EN">{MRDescription} Activated</ActivateSound>
                  <DeactivateSound TTS="EN">{MRDescription} Deactivated</DeactivateSound>
                </Hotkey>
              </Hotkeys>
            </CheatEntry>
            <CheatEntry>
              <ID>123</ID>
              <Description>"Sparkles can be picked again and again"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('kira', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('kira', false) end
{$asm}
</AssemblerScript>
            </CheatEntry>
            <CheatEntry>
              <ID>127</ID>
              <Description>"Switch day / night"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.oneShot(memrec, function() DQX.toggleDayNight() end)
{$asm}
[DISABLE]
</AssemblerScript>
            </CheatEntry>
          </CheatEntries>
        </CheatEntry>
        <CheatEntry>
          <ID>142</ID>
          <Description>"Fishing"</Description>
          <Options moManualExpandCollapse="1"/>
          <Color>FFFF00</Color>
          <GroupHeader>1</GroupHeader>
          <CheatEntries>
            <CheatEntry>
              <ID>110</ID>
              <Description>"Easy fishing"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('fishEasy', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('fishEasy', false) end
{$asm}
</AssemblerScript>
            </CheatEntry>
            <CheatEntry>
              <ID>133</ID>
              <Description>"Catch a chosen fish (pick a fish; any spot)..."</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('fishTarget', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('fishTarget', false) end
{$asm}
</AssemblerScript>
            </CheatEntry>
            <CheatEntry>
              <ID>132</ID>
              <Description>"Rare fish first (more likely, not guaranteed)"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('fishRare', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('fishRare', false) end
{$asm}
</AssemblerScript>
            </CheatEntry>
            <CheatEntry>
              <ID>111</ID>
              <Description>"Fish always maximum size"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('fishMax', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('fishMax', false) end
{$asm}
</AssemblerScript>
            </CheatEntry>
            <CheatEntry>
              <ID>112</ID>
              <Description>"Fish always minimum size"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('fishMin', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('fishMin', false) end
{$asm}
</AssemblerScript>
            </CheatEntry>
            <CheatEntry>
              <ID>134</ID>
              <Description>"Catch every fish (records, fish bag, fishing exp, fish coins)"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.oneShot(memrec, function() DQX.fishCatchAll() end)
{$asm}
[DISABLE]
</AssemblerScript>
            </CheatEntry>
          </CheatEntries>
        </CheatEntry>
        <CheatEntry>
          <ID>143</ID>
          <Description>"Items &amp; gain multipliers"</Description>
          <Options moManualExpandCollapse="1"/>
          <Color>FFFF00</Color>
          <GroupHeader>1</GroupHeader>
          <CheatEntries>
            <CheatEntry>
              <ID>104</ID>
              <Description>"Items never decrease (use items / mix bag)"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('itemKeep', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('itemKeep', false) end
{$asm}
</AssemblerScript>
            </CheatEntry>
            <CheatEntry>
              <ID>115</ID>
              <Description>"EXP multiplier (battle results)"</Description>
              <Options moHideChildren="1"/>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('expMult', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('expMult', false) end
{$asm}
</AssemblerScript>
              <CheatEntries>
                <CheatEntry>
                  <ID>116</ID>
                  <Description>"EXP multiplier"</Description>
                  <ShowAsSigned>0</ShowAsSigned>
                  <VariableType>Float</VariableType>
                  <Address>dqxVars+18</Address>
                </CheatEntry>
              </CheatEntries>
            </CheatEntry>
            <CheatEntry>
              <ID>117</ID>
              <Description>"Gold multiplier (every gold gain)"</Description>
              <Options moHideChildren="1"/>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('goldMult', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('goldMult', false) end
{$asm}
</AssemblerScript>
              <CheatEntries>
                <CheatEntry>
                  <ID>118</ID>
                  <Description>"Gold multiplier (0.1 steps)"</Description>
                  <ShowAsSigned>0</ShowAsSigned>
                  <VariableType>Float</VariableType>
                  <Address>dqxVars+1C</Address>
                </CheatEntry>
              </CheatEntries>
            </CheatEntry>
            <CheatEntry>
              <ID>119</ID>
              <Description>"Item multiplier (every item gain)"</Description>
              <Options moHideChildren="1"/>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('itemMult', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('itemMult', false) end
{$asm}
</AssemblerScript>
              <CheatEntries>
                <CheatEntry>
                  <ID>120</ID>
                  <Description>"Item multiplier (integer 1-100)"</Description>
                  <ShowAsSigned>0</ShowAsSigned>
                  <VariableType>4 Bytes</VariableType>
                  <Address>dqxVars+20</Address>
                </CheatEntry>
              </CheatEntries>
            </CheatEntry>
          </CheatEntries>
        </CheatEntry>
        <CheatEntry>
          <ID>144</ID>
          <Description>"Other"</Description>
          <Options moManualExpandCollapse="1"/>
          <Color>FFFF00</Color>
          <GroupHeader>1</GroupHeader>
          <CheatEntries>
            <CheatEntry>
              <ID>128</ID>
              <Description>"Expedition (subjugation corps) returns now"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.oneShot(memrec, function() DQX.corpReturnNow() end)
{$asm}
[DISABLE]
</AssemblerScript>
            </CheatEntry>
            <CheatEntry>
              <ID>135</ID>
              <Description>"All monsters: 100 defeats each (kill records)"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.oneShot(memrec, function() DQX.monsterAll100() end)
{$asm}
[DISABLE]
</AssemblerScript>
            </CheatEntry>
            <CheatEntry>
              <ID>136</ID>
              <Description>"Get all titles and achievements (Steam)"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.oneShot(memrec, function() DQX.achvAll() end)
{$asm}
[DISABLE]
</AssemblerScript>
            </CheatEntry>
            <CheatEntry>
              <ID>137</ID>
              <Description>"Fill the collection item list (items are not given)"</Description>
              <Color>00A5FF</Color>
              <VariableType>Auto Assembler Script</VariableType>
              <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.oneShot(memrec, function() DQX.archiveAll() end)
{$asm}
[DISABLE]
</AssemblerScript>
            </CheatEntry>
          </CheatEntries>
        </CheatEntry>
      </CheatEntries>
    </CheatEntry>
    <CheatEntry>
      <ID>200</ID>
      <Description>"02. User"</Description>
      <Options moManualExpandCollapse="1"/>
      <Color>FFFF00</Color>
      <GroupHeader>1</GroupHeader>
      <CheatEntries>
        <CheatEntry>
          <ID>201</ID>
          <Description>"Money &amp; coins (enable to list)"</Description>
          <Options moManualExpandCollapse="1"/>
          <Color>FFFF00</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.userRun(function() DQX.refreshMoney(memrec) end)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.clearChildren(memrec) end
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>202</ID>
          <Description>"Party parameters (enable to list)"</Description>
          <Options moManualExpandCollapse="1"/>
          <Color>FFFF00</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.userRun(function() DQX.refreshParty(memrec) end)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.clearChildren(memrec) end
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>203</ID>
          <Description>"Add skill points..."</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.oneShot(memrec, function() DQX.addSkillPoints() end)
{$asm}
[DISABLE]
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>204</ID>
          <Description>"Add fishing EXP (fishing points)..."</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.oneShot(memrec, function() DQX.addFishingExp() end)
{$asm}
[DISABLE]
</AssemblerScript>
        </CheatEntry>
      </CheatEntries>
    </CheatEntry>
    <CheatEntry>
      <ID>300</ID>
      <Description>"03. Items"</Description>
      <Options moManualExpandCollapse="1"/>
      <Color>FFFF00</Color>
      <GroupHeader>1</GroupHeader>
      <CheatEntries>
        <CheatEntry>
          <ID>301</ID>
          <Description>"Item counts (enable to list)"</Description>
          <Options moManualExpandCollapse="1"/>
          <Color>FFFF00</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.userRun(function() DQX.refreshItems(memrec) end)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.clearChildren(memrec) end
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>302</ID>
          <Description>"Add items (window: category -&gt; select several -&gt; quantity)..."</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.oneShot(memrec, function() DQX.addItemPick() end)
{$asm}
[DISABLE]
</AssemblerScript>
        </CheatEntry>
      </CheatEntries>
    </CheatEntry>
    <CheatEntry>
      <ID>400</ID>
      <Description>"04. Blacksmith, Alchemy &amp; Synthesis"</Description>
      <Options moManualExpandCollapse="1"/>
      <Color>FFFF00</Color>
      <GroupHeader>1</GroupHeader>
      <CheatEntries>
        <CheatEntry>
          <ID>401</ID>
          <Description>"Materials are not consumed"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('craftKeep', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('craftKeep', false) end
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>402</ID>
          <Description>"No materials required (recipe materials = 0)"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('noMat', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('noMat', false) end
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>403</ID>
          <Description>"Learn all recipes"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.oneShot(memrec, function() DQX.learnAllRecipes() end)
{$asm}
[DISABLE]
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>404</ID>
          <Description>"Blacksmith: finish in one go (gauges kept at target; choose "Finish")"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('smithOne', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('smithOne', false) end
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>304</ID>
          <Description>"Smithing quality of equipment (window: select -&gt; none / +1 / +2 / +3)..."</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.oneShot(memrec, function() DQX.smithPick() end)
{$asm}
[DISABLE]
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>305</ID>
          <Description>"Accessory synthesis (window: select -&gt; effect per slot -&gt; none / +1 to +4)..."</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.oneShot(memrec, function() DQX.synthPick() end)
{$asm}
[DISABLE]
</AssemblerScript>
        </CheatEntry>
      </CheatEntries>
    </CheatEntry>
    <CheatEntry>
      <ID>500</ID>
      <Description>"05. Casino"</Description>
      <Options moManualExpandCollapse="1"/>
      <Color>FFFF00</Color>
      <GroupHeader>1</GroupHeader>
      <CheatEntries>
        <CheatEntry>
          <ID>501</ID>
          <Description>"Coins never decrease (bets are free; payouts are added as usual)"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('casinoCoin', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('casinoCoin', false) end
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>512</ID>
          <Description>"Always tension up (slot / poker; the casino has no other tension state)"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('casinoTU', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('casinoTU', false) end
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>502</ID>
          <Description>"Slot: always 7-7-7-7-7 (during metal spin: Jackpot)"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('slot777', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('slot777', false) end
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>514</ID>
          <Description>"Slot: always metal spin (metal x5 when not in a metal spin; +30 spins)"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('slotMetalSpin', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('slotMetalSpin', false) end
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>513</ID>
          <Description>"Slot: metal spins never decrease"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('slotMetal', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('slotMetal', false) end
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>503</ID>
          <Description>"Roulette: always 0 (treasure chest pocket)"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('rlt0', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('rlt0', false) end
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>504</ID>
          <Description>"Roulette: always 4"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('rlt1', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('rlt1', false) end
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>505</ID>
          <Description>"Bingo: hammer chance opens every number (NPC cards too)"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('bingoHammer', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('bingoHammer', false) end
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>506</ID>
          <Description>"Bingo: balls never decrease (leave the final round from the menu)"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('bingoBall', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('bingoBall', false) end
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>507</ID>
          <Description>"Bingo: monster MP always full"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('bingoMP', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('bingoMP', false) end
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>508</ID>
          <Description>"Bingo: all lines complete (every square of your card opens)"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('bingoLines', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('bingoLines', false) end
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>509</ID>
          <Description>"Bingo: monster rebirth always (every passed monster panel is reborn)"</Description>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('bingoRebirth', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('bingoRebirth', false) end
{$asm}
</AssemblerScript>
        </CheatEntry>
        <CheatEntry>
          <ID>510</ID>
          <Description>"Bingo: ball bounciness multiplier"</Description>
          <Options moHideChildren="1"/>
          <Color>00A5FF</Color>
          <VariableType>Auto Assembler Script</VariableType>
          <AssemblerScript>[ENABLE]
{$lua}
if syntaxcheck then return end
DQX.feature('bounce', true)
{$asm}
[DISABLE]
{$lua}
if syntaxcheck then return end
if DQX then DQX.feature('bounce', false) end
{$asm}
</AssemblerScript>
          <CheatEntries>
            <CheatEntry>
              <ID>511</ID>
              <Description>"Bounciness multiplier (default 5)"</Description>
              <ShowAsSigned>0</ShowAsSigned>
              <VariableType>Float</VariableType>
              <Address>dqxVars+50</Address>
            </CheatEntry>
          </CheatEntries>
        </CheatEntry>
      </CheatEntries>
    </CheatEntry>
  </CheatEntries>
  <UserdefinedSymbols/>
  <LuaScript>--[[ =====================================================================================
  DQXOffline.CT  -  Cheat Engine table for DRAGON QUEST X OFFLINE (Steam)   (namespace: DQX)
  Based on GameTemplate.CT V02 (common framework). Target : Cheat Engine 7.5+ (64-bit)
  Layout : [ATTACH / ALL OFF]  -&gt;  0. Settings &amp; README  -&gt;  01..05 game categories
  Colors : expandable group = 0xFFFF00 (cyan, BGR) / script = 0x00A5FF (orange, BGR) / value row = none
  IDs    : 1 = ATTACH, 2 = "0." top group, 3..99 = inside "0.", category N uses N*100 .. N*100+99
  Engine : Unreal Engine 4.26 (game logic mostly Blueprint). Game data is located at run time through
           UE4 reflection (GNames / GUObjectArray / FProperty), native code through AOB signatures.
===================================================================================== ]]
DQX = DQX or {}
local DQX = DQX

----------------------------------------------------------------- [GAME] basic settings
DQX.TAG      = 'DQX'
DQX.TITLE    = 'DRAGON QUEST X OFFLINE'
DQX.EXE      = 'DQXOffline.exe'
DQX.MODULE   = 'DQXOffline.exe'
DQX.GAMEVER  = '2.0.1.0'
DQX.BUILD    = {ts=0x645B97AF, size=0x504F000}   -- PE timestamp + SizeOfImage of the analysed build
DQX.TICK_MS  = 300
DQX.lang     = DQX.lang or 'en'

----------------------------------------------------------------- common constants (do not change)
DQX.C_GROUP  = 0xFFFF00
DQX.C_SCRIPT = 0x00A5FF
DQX.ID_ATTACH = 1
DQX.ID_TOP0   = 2
-- shared memory (allocated near DQX.MODULE at attach, registered as CE symbols)
--   dqxFlags +0000..00FF : feature flag bytes        (00 invincible, 01 one-hit kill, 02 gold, 03 EXP, 04 critical, 05 steal,
--                                                    06 slot 77777, 09 bingo bounce, 0A bingo rebirth, 0B casino tension up, 0C slot metal spin, F0 game-thread call request)
--   dqxVars  +0100..01FF : parameters
--   dqxPtrs  +0200..03FF : pointer slots (8 bytes each)
--   +0500 string buffer / +0600 parameter block for native scanners
--   +2000 game-thread call block (count, done, 0x50-byte entries) / +6000 UFunction parameter buffers
--   +8000 output buffer of native scanners (8192 qwords) / +18000 FText* input list / +1C000 FString* output list (2048 each)
-- [COMMON CHANGE] the template allocates 0x1000 bytes; this game needs 0x20000 (call queue + scan / text buffers).
DQX.OFF_VARS, DQX.OFF_PTRS = 0x100, 0x200
DQX.MEMSIZE = 0x20000
DQX.OFF_STR, DQX.OFF_PARAM, DQX.OFF_CALL, DQX.OFF_PARMS, DQX.OFF_OUT = 0x500, 0x600, 0x2000, 0x6000, 0x8000
DQX.OFF_TIN, DQX.OFF_TOUT, DQX.OUT_MAX, DQX.TEXT_MAX = 0x18000, 0x1C000, 8192, 2048

----------------------------------------------------------------- [GAME] AOB signatures
-- code signature : { p=AOB, off=hook site offset, len=bytes overwritten (&gt;=5) }               -&gt; symbol dqxA_&lt;name&gt;
-- [COMMON CHANGE] pointer signature : { p=AOB, rip=offset of the rel32, iend=offset of the end of that instruction,
--                  add=value added to the target, multi=true if several matches are allowed when all of them
--                  resolve to the same target }. Needed for UE4 globals (GNames / GUObjectArray) and call targets.
DQX.SIGS = {
  -- UObject::ProcessEvent (entry). Hooked to run requests on the game thread.
  pe       = { p='40 55 56 57 41 54 41 55 41 56 41 57 48 81 EC ?? ?? ?? ?? 48 8D 6C 24 ?? 48 89 9D ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C5 48 89 85 ?? ?? ?? ?? 8B 41 0C 45 33 F6 3B 05', off=0, len=8 },
  -- UHOLIAttributeSet::PostGameplayEffectExecute : "Damage" meta attribute was just read into xmm0
  dmg      = { p='49 8D 8E ?? ?? ?? ?? E8 ?? ?? ?? ?? 0F 28 F0 89 75 ?? F3 41 0F 5F F2 49 8D 4E ?? F3 0F 11 75', off=0x0C, len=6 },
  -- FMemory::Realloc(void*, SIZE_T, uint32)
  realloc  = { p='48 89 5C 24 08 48 89 74 24 10 57 48 83 EC 20 48 8B F1 41 8B D8 48 8B 0D ?? ?? ?? ?? 48 8B FA 48 85 C9 75 0C E8 ?? ?? ?? ?? 48 8B 0D ?? ?? ?? ?? 48 8B 01 44 8B CB 4C 8B C7 48 8B D6', off=0 },
  -- FNamePool (GNames): lea rax,[NamePoolData]
  gnames   = { p='48 8D 05 ?? ?? ?? ?? EB 13 48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? C6 05 ?? ?? ?? ?? 01 0F 10 03', rip=3, iend=7 },
  -- GUObjectArray: "cmp eax,[GUObjectArray.ObjObjects.NumElements]" inside ProcessEvent (NumElements = base+0x24)
  gobjs    = { p='8B 41 0C 45 33 F6 3B 05 ?? ?? ?? ?? 4D 8B F8 48 8B F2 4C 8B E1', rip=8, iend=12, add=-0x24 },
  -- AHOLICharacterBase -&gt; "GetOwnCharacterType" (BlueprintImplementableEvent caller), via IsOwnerPlayable &amp; co.
  ownType  = { p='48 8B CB E8 ?? ?? ?? ?? 0F B6 C8 83 E9 04 74 ?? 83 E9 01 74 ?? 83 F9 05 74 ?? B0 01', rip=4, iend=8, multi=true },
  -- UHOLIBattleManager::GetBattleExperienceByCharacter (impl entry): xmm3 = DefaultTotalValue (total EXP of the battle)
  expCalc  = { p='4C 8B DC 49 89 73 20 57 48 81 EC 90 00 00 00 F3 0F 10 8C 24 C0 00 00 00 48 8B FA', off=0, len=7 },
  -- UHOLICareerHistory::IncreaseGoldTotalCount (impl): edx = gold gained
  goldStat = { p='44 8B 81 30 05 00 00 B8 7F 96 98 00 44 03 C2', off=0, len=7 },
  -- UHOLIBaseFunction::TryActivateOddFunctionInternal (exec thunk, right after the impl call): al = activated,
  -- byte [rbp+58] = SelfOddFunctionID (1 = ODDFUNCTION_KAISIN), [rbp+48] = this (odd function object)
  kaisin   = { p='E8 ?? ?? ?? ?? 48 8B 4D E8 41 88 45 00 48 85 C9 74 05 E8', off=5, len=8 },
  -- UKismetMathLibrary::execBooleanAND, after both operands were read: rbx = FFrame (+18 Object, +20 Code),
  -- bpl = A, dword [rsp+50] = B. Hook site: "mov [rbx+20],rdi / test bpl,bpl" (Code already past EX_EndFunctionParms)
  critAnd  = { p='48 85 C0 40 0F 95 C7 48 03 F8 48 89 7B 20 40 84 ED 74 17 83 7C 24 50 00 74 10 C6 06 01', rva=0x2A1642C, off=0x0A, len=7 },
  -- casino. HOLICasinoManager::UpdateCasinoSubtractionCoinCount wrapper (the only path that subtracts inventory casino
  -- coins: bingo / poker / roulette / slot all call it). Same prologue as the Add wrapper: the rip displacement of the
  -- FName pointer is part of the pattern.
  coinSub  = { p='48 89 5C 24 08 57 48 83 EC 20 48 8B 19 48 8B F9 89 54 24 38 48 8B 15 5D 83 99 03', rva=0xCFA500, off=0, len=5 },
  -- slot bet (0x140BED490): "cmp byte [rsi+5C0],r14b / jne +C0" (raid check); jumping to the raid target (+CD) skips the
  -- credit subtraction, its display update and the inventory subtraction
  slotBet  = { p='44 38 B6 C0 05 00 00 0F 85 C0 00 00 00 44 8B 86 C0 02 00 00', rva=0xBED553, off=0, len=13 },
  -- roulette result (0x140BCEF60): "mov ebx,eax" = total bet; bet 0 -&gt; credit += gain, subtraction 0, addition gain
  rltBet   = { p='8B D8 8B 96 2C 03 00 00 48 8B 8E A8 02 00 00 E8', rva=0xBCEF99, off=0, len=2 },
  -- bingo entry: "sub [rbx+2E4],edx" (in-game coins -= bet)
  bingoBet = { p='29 93 E4 02 00 00 01 93 48 03 00 00', rva=0xBB11D9, off=0, len=6 },
  -- slot lottery (0x140BF2B10) right after the cumulative-jackpot check: rbx = slot main widget. The game's common
  -- reel-building part starts at +440 (0x140BF3353): role dil, wild bpl, queen r12b, line sil.
  slotRole = { p='44 88 AB 14 05 00 00 8B 8B AC 02 00 00 3B CD', rva=0xBF2F13, off=0, len=13 },
  -- slot metal spin: "mov r8d,[rbx+404] / mov eax,[rbx+510] / dec r8d / mov [rbx+404],r8d" (remaining metal spins)
  slotMetal= { p='44 8B 83 04 04 00 00 8B 83 10 05 00 00 41 FF C8 44 89 83 04 04 00 00', rva=0xBF2CED, off=0x0D, len=3 },
  -- roulette winning number (0x140BCE280): ecx = 0..36 from rand(), stored to RouletteResult +330 (GameResult = n+1)
  -- roulette winning number (0x140BCE280): "mov ecx,24 / ... / cmovle ecx,eax" = min(rand*37/32767, 36) -&gt; RouletteResult +330
  rltMax   = { p='B9 24 00 00 00 66 0F 6E C0 0F 5B C0 F3 0F 59 05 ?? ?? ?? ?? F3 0F 2C C0 3B C1 0F 4E C8', rva=0xBCE2A0, off=0, len=0x1D },
  -- bingo final round ball counter: "inc dword [rsi+2E0]" (CurrentFinalRoundCount), the only increment
  bingoBall= { p='FF 86 E0 02 00 00 48 8B 5C 24 58', rva=0xBBA26C, off=0, len=6 },
  -- HOLIBingoCard GetSquare(card, pos 1-24) -&gt; HOLIBingoCardSquare* (function entry, called on the game thread)
  getSquare= { p='FF CA 83 FA 17 0F 87 FD 00 00 00 48 63 C2 4C 8D 05', rva=0xB822C0, off=0 },
  -- UProjectileMovementComponent bounce: "movss xmm2,[rbx+114]" (Bounciness) / "lea r8,[rsp+30]"; rbx = component
  bounce   = { p='F3 0F 10 93 14 01 00 00 4C 8D 44 24 30 F3 0F 5F D6', rva=0x244D6BF, off=0, len=13 },
  -- bingo main widget OnEndDrawLots (0x140BB0590): copies the board result; rdi = result (+C ExtraOpenType), rbx = widget
  bingoType= { p='0F B6 47 0C 88 83 64 03 00 00 8B 47 10 89 83 68 03 00 00', rva=0xBB05BD, off=0, len=10 },
  -- slot CHECK_TENSIONUP (0x140BEFB59) "IsPlayTensionUp continues?" check: +77 = no tension up (seq 21), +8F = start
  slotTU   = { p='80 BB A8 05 00 00 01 75 6E 44 8B 83 68 03 00 00', rva=0xBEFBC9, off=0, len=9 },
  -- poker: tension-up start test right after the end check: r14b = hand &gt;= four of a kind; rbx = poker game manager
  pokerTU  = { p='48 8B 83 C8 02 00 00 45 84 F6 4C 8B 74 24 50 44 89 B8 98 00 00 00', rva=0xBC0064, off=0, len=10 },
  -- FText::ToString() (rebuilds the display string, e.g. after a string table was loaded) -&gt; const FString&amp;
  ftextToString = { p='40 53 48 83 EC 20 48 8B D9 E8 ?? ?? ?? ?? 48 8B 0B 48 8B 01 48 83 C4 20 5B 48 FF 60 10', off=0 },
  -- FHOLIEquipEnhance constructor: "lea rax,[vtable]" (FHOLIEquipEnhance has a vtable; FHOLIEquipData.Extention.EnhanceData)
  enhVtbl  = { p='C7 41 48 FF FF FF FF 48 89 71 08 48 8D 05 ?? ?? ?? ?? 48 89 01', rip=14, iend=18 },
  -- UCharacterMovementComponent::GetMaxSpeed : walking / falling path "movss xmm0,[rbx+MaxWalkSpeed]"
  maxSpeed = { p='F3 0F 10 83 8C 01 00 00 48 83 C4 20 5B C3 F3 0F 10 83 94 01 00 00', off=0, len=8 },
}

----------------------------------------------------------------- [GAME] hooks
DQX.HOOKS = {
  -- game-thread call service: when dqxFlags+F0 is set, run the queued native calls, then continue ProcessEvent
  peSvc = { sig='pe', aa=[==[
[ENABLE]
alloc(dqx_pe,$1000,$SITE)
label(dqx_pe_orig)
label(dqx_pe_ret)
label(dqx_pe_svc)
label(dqx_pe_loop)
label(dqx_pe_fin)
label(dqx_pe_done)
dqx_pe:
  cmp byte ptr [dqxFlags+F0],0
  jne dqx_pe_svc
dqx_pe_orig:
  $ORIG
  jmp dqx_pe_ret
dqx_pe_svc:
  push rcx
  push rdx
  push r8
  push r9
  push rbx
  push rsi
  sub rsp,38
  xor eax,eax
  xchg al,byte ptr [dqxFlags+F0]
  test al,al
  je dqx_pe_done
  lea rbx,[dqxFlags+2010]
  mov esi,[dqxFlags+2000]
dqx_pe_loop:
  test esi,esi
  je dqx_pe_fin
  mov rax,[rbx+28]
  mov [rsp+20],rax
  mov rax,[rbx+30]
  mov [rsp+28],rax
  mov rcx,[rbx+08]
  mov rdx,[rbx+10]
  mov r8,[rbx+18]
  mov r9,[rbx+20]
  call qword ptr [rbx]
  mov [rbx+38],rax
  movq [rbx+40],xmm0
  add rbx,50
  dec esi
  jmp dqx_pe_loop
dqx_pe_fin:
  mov dword ptr [dqxFlags+2004],1
dqx_pe_done:
  add rsp,38
  pop rsi
  pop rbx
  pop r9
  pop r8
  pop rdx
  pop rcx
  jmp dqx_pe_orig
$SITE:
  jmp dqx_pe
  $NOP
dqx_pe_ret:
[DISABLE]
$SITE:
  $ORIG
dealloc(dqx_pe)
]==] },
  -- damage: allies -&gt; 0 (flag 00), enemies -&gt; dqxVars+10 (flag 01). rdi = target AHOLICharacterBase or 0.
  dmg = { sig='dmg', aa=[==[
[ENABLE]
alloc(dqx_dmg,$1000,$SITE)
label(dqx_dmg_orig)
label(dqx_dmg_ret)
label(dqx_dmg_chk)
label(dqx_dmg_enemy)
label(dqx_dmg_rest)
dqx_dmg:
  test rdi,rdi
  je dqx_dmg_orig
  cmp byte ptr [dqxFlags+00],0
  jne dqx_dmg_chk
  cmp byte ptr [dqxFlags+01],0
  je dqx_dmg_orig
dqx_dmg_chk:
  sub rsp,70
  movups [rsp+20],xmm0
  mov [rsp+30],rcx
  mov [rsp+38],rdx
  mov [rsp+40],r8
  mov [rsp+48],r9
  mov [rsp+50],r10
  mov [rsp+58],r11
  mov [rsp+60],rax
  mov rcx,rdi
  call dqxA_ownType
  movzx eax,al
  movups xmm0,[rsp+20]
  cmp eax,4
  je dqx_dmg_enemy
  cmp eax,5
  je dqx_dmg_enemy
  cmp eax,0A
  je dqx_dmg_enemy
  cmp byte ptr [dqxFlags+00],0
  je dqx_dmg_rest
  xorps xmm0,xmm0
  jmp dqx_dmg_rest
dqx_dmg_enemy:
  cmp byte ptr [dqxFlags+01],0
  je dqx_dmg_rest
  movss xmm0,[dqxVars+10]
dqx_dmg_rest:
  mov rcx,[rsp+30]
  mov rdx,[rsp+38]
  mov r8,[rsp+40]
  mov r9,[rsp+48]
  mov r10,[rsp+50]
  mov r11,[rsp+58]
  mov rax,[rsp+60]
  add rsp,70
dqx_dmg_orig:
  $ORIG
  jmp dqx_dmg_ret
$SITE:
  jmp dqx_dmg
  $NOP
dqx_dmg_ret:
[DISABLE]
$SITE:
  $ORIG
dealloc(dqx_dmg)
]==] },
  -- EXP multiplier: total battle EXP *= dqxVars+18 (flag 03). 
  expMul = { sig='expCalc', aa=[==[
[ENABLE]
alloc(dqx_exp,$1000,$SITE)
label(dqx_exp_orig)
label(dqx_exp_ret)
dqx_exp:
  cmp byte ptr [dqxFlags+03],0
  je dqx_exp_orig
  mulss xmm3,[dqxVars+18]
dqx_exp_orig:
  $ORIG
  jmp dqx_exp_ret
$SITE:
  jmp dqx_exp
  $NOP
dqx_exp_ret:
[DISABLE]
$SITE:
  $ORIG
dealloc(dqx_exp)
]==] },
  -- gold multiplier: extra gold = gain*(x10/10) - gain is added to FHOLIMainInventoryData.Gold (flag 02). 
  --   dqxVars+40 = multiplier x10 (int), dqxVars+44 = offset of Gold, dqxPtrs+0 = live FHOLIMainInventoryData
  goldMul = { sig='goldStat', aa=[==[
[ENABLE]
alloc(dqx_gold,$1000,$SITE)
label(dqx_gold_orig)
label(dqx_gold_ret)
label(dqx_gold_skip)
label(dqx_gold_pos)
label(dqx_gold_max)
dqx_gold:
  cmp byte ptr [dqxFlags+02],0
  je dqx_gold_orig
  test edx,edx
  jle dqx_gold_orig
  push rax
  push rcx
  push r8
  push r9
  movsxd r9,edx
  movsxd rax,dword ptr [dqxVars+40]
  imul rax,r9
  mov r8,0A
  push rdx
  cqo
  idiv r8
  pop rdx
  mov rcx,[dqxPtrs+00]
  test rcx,rcx
  je dqx_gold_skip
  movsxd r8,dword ptr [dqxVars+44]
  add rcx,r8
  mov r8,rax
  sub r8,r9
  movsxd r9,dword ptr [rcx]
  add r9,r8
  cmp r9,0
  jge dqx_gold_pos
  xor r9,r9
dqx_gold_pos:
  cmp r9,98967F
  jle dqx_gold_max
  mov r9,98967F
dqx_gold_max:
  mov [rcx],r9d
dqx_gold_skip:
  mov edx,eax
  pop r9
  pop r8
  pop rcx
  pop rax
dqx_gold_orig:
  $ORIG
  jmp dqx_gold_ret
$SITE:
  jmp dqx_gold
  $NOP
dqx_gold_ret:
[DISABLE]
$SITE:
  $ORIG
dealloc(dqx_gold)
]==] },
  -- movement speed: GetMaxSpeed result *= dqxVars+28 when this == player's CharacterMovementComponent (dqxVars+70)
  spd = { sig='maxSpeed', aa=[==[
[ENABLE]
alloc(dqx_spd,$1000,$SITE)
label(dqx_spd_ret)
label(dqx_spd_skip)
dqx_spd:
  $ORIG
  cmp rbx,[dqxVars+70]
  jne dqx_spd_skip
  mulss xmm0,[dqxVars+28]
dqx_spd_skip:
  jmp dqx_spd_ret
$SITE:
  jmp dqx_spd
  $NOP
dqx_spd_ret:
[DISABLE]
$SITE:
  $ORIG
dealloc(dqx_spd)
]==] },
}

-- always critical (flag 04), decisive path: the activation roll of the critical odd function. When SelfOddFunctionID==1
-- and the owner actor's class (this -&gt; Outer condition component -&gt; +A0 owner -&gt; class) is in the ally class list
-- (dqxVars+80.., zero-terminated, filled by Lua), the result is forced to true.
-- always steal (flag 05): same for SelfOddFunctionID == dqxVars+30 (ODDFUNCTION_AUTO_NUSUMU, read from the UEnum by Lua).
DQX.HOOKS.kaisin = { sig='kaisin', aa=[==[
[ENABLE]
alloc(dqx_ksn,$1000,$SITE)
label(dqx_ksn_orig)
label(dqx_ksn_ret)
label(dqx_ksn_loop)
label(dqx_ksn_yes)
label(dqx_ksn_no)
label(dqx_ksn_steal)
label(dqx_ksn_chk)
dqx_ksn:
  cmp byte ptr [dqxFlags+04],0
  je dqx_ksn_steal
  cmp byte ptr [rbp+58],1
  je dqx_ksn_chk
dqx_ksn_steal:
  cmp byte ptr [dqxFlags+05],0
  je dqx_ksn_orig
  push rcx
  movzx ecx,byte ptr [dqxVars+30]
  cmp byte ptr [rbp+58],cl
  pop rcx
  jne dqx_ksn_orig
dqx_ksn_chk:
  push rcx
  push rdx
  mov rcx,[rbp+48]
  test rcx,rcx
  je dqx_ksn_no
  mov rcx,[rcx+20]
  test rcx,rcx
  je dqx_ksn_no
  mov rcx,[rcx+A0]
  test rcx,rcx
  je dqx_ksn_no
  mov rcx,[rcx+10]
  lea rdx,[dqxVars+80]
dqx_ksn_loop:
  cmp qword ptr [rdx],0
  je dqx_ksn_no
  cmp [rdx],rcx
  je dqx_ksn_yes
  add rdx,8
  jmp dqx_ksn_loop
dqx_ksn_yes:
  mov al,1
dqx_ksn_no:
  pop rdx
  pop rcx
dqx_ksn_orig:
  $ORIG
  jmp dqx_ksn_ret
$SITE:
  jmp dqx_ksn
  $NOP
dqx_ksn_ret:
[DISABLE]
$SITE:
  $ORIG
dealloc(dqx_ksn)
]==] }

-- always critical (flag 04), multi-hit attacks: BP_CriticalFunction_C rolls the critical only when
-- BooleanAND(Is_Currently_Actor, self.bCheckCritical) is true, and the game passes bCheckCritical=false for the 2nd and later
-- hits (NotifySimpleHit / CallNotifySimpleHit, e.g. boomerang: TargetIndex==0 only). When the second operand of a BooleanAND
-- is exactly "EX_InstanceVariable(bCheckCritical) EX_EndFunctionParms" (pointer in dqxVars+38, set by Lua) and the
-- object's owner is an ally (same chain / list as the kaisin hook), B is treated as true. Enemies are unchanged.
DQX.HOOKS.critAnd = { sig='critAnd', aa=[==[
[ENABLE]
alloc(dqx_cand,$1000,$SITE)
label(dqx_cand_ret)
label(dqx_cand_loop)
label(dqx_cand_yes)
label(dqx_cand_no)
label(dqx_cand_skip)
dqx_cand:
  mov [rbx+20],rdi
  cmp byte ptr [dqxFlags+04],0
  je dqx_cand_skip
  test rdi,rdi
  je dqx_cand_skip
  cmp byte ptr [rdi-1],16
  jne dqx_cand_skip
  cmp byte ptr [rdi-0A],1
  jne dqx_cand_skip
  push rax
  push rcx
  push rdx
  mov rax,[dqxVars+38]
  test rax,rax
  je dqx_cand_no
  cmp [rdi-9],rax
  jne dqx_cand_no
  mov rcx,[rbx+18]
  test rcx,rcx
  je dqx_cand_no
  mov rcx,[rcx+20]
  test rcx,rcx
  je dqx_cand_no
  mov rcx,[rcx+A0]
  test rcx,rcx
  je dqx_cand_no
  mov rcx,[rcx+10]
  lea rdx,[dqxVars+80]
dqx_cand_loop:
  cmp qword ptr [rdx],0
  je dqx_cand_no
  cmp [rdx],rcx
  je dqx_cand_yes
  add rdx,8
  jmp dqx_cand_loop
dqx_cand_yes:
  mov dword ptr [rsp+68],1
dqx_cand_no:
  pop rdx
  pop rcx
  pop rax
dqx_cand_skip:
  test bpl,bpl
  jmp dqx_cand_ret
$SITE:
  jmp dqx_cand
  $NOP
dqx_cand_ret:
[DISABLE]
$SITE:
  $ORIG
dealloc(dqx_cand)
]==] }

-- casino patches (no flag: installed while the feature is on)
DQX.HOOKS.coinSub = { sig='coinSub', aa=[==[
[ENABLE]
$SITE:
  ret
[DISABLE]
$SITE:
  $ORIG
]==] }
DQX.HOOKS.slotBet = { sig='slotBet', aa=[==[
[ENABLE]
$SITE:
  jmp dqxA_slotBet+CD
  $NOP
[DISABLE]
$SITE:
  $ORIG
]==] }
DQX.HOOKS.rltBet = { sig='rltBet', aa=[==[
[ENABLE]
$SITE:
  db 31 DB
[DISABLE]
$SITE:
  $ORIG
]==] }
DQX.HOOKS.bingoBet = { sig='bingoBet', aa=[==[
[ENABLE]
$SITE:
  db EB 04
[DISABLE]
$SITE:
  $ORIG
]==] }
DQX.HOOKS.bingoBall = { sig='bingoBall', aa=[==[
[ENABLE]
$SITE:
  db EB 04
[DISABLE]
$SITE:
  $ORIG
]==] }
-- slot always 7-7-7-7-7 (flag 06): role ROLE_SEVEN_FIVE_ALIGN (1) on the center line (2, always bet), no wild / queen,
-- During a metal spin (+403) 7-7-7-7-7 is the JACKPOT: +514 = 1 like the game's own jackpot draw (0x140BF2F04); the payout
-- (0x140BDE604) then adds the cumulative jackpot (x1.5 in tension up) instead of the role rate (MetalRoleRate is 0).
-- slot always metal spin (flag 0C): outside a metal spin the role is METAL_FIVE_ALIGN (0x16): the game's own start
-- (0x140BD9CD0) gives +30 metal spins; during the metal spin the normal lottery (or 77777 with flag 06) is used.
DQX.HOOKS.slotRole = { sig='slotRole', aa=[==[
[ENABLE]
alloc(dqx_slot,$1000,$SITE)
label(dqx_slot_orig)
label(dqx_slot_ret)
label(dqx_slot_777)
label(dqx_slot_set)
label(dqx_slot_go)
dqx_slot:
  cmp byte ptr [dqxFlags+0C],0
  je dqx_slot_777
  cmp byte ptr [rbx+403],0
  jne dqx_slot_777
  mov edi,16
  jmp dqx_slot_set
dqx_slot_777:
  cmp byte ptr [dqxFlags+06],0
  je dqx_slot_orig
  mov edi,1
  mov byte ptr [rbx+514],0
  cmp byte ptr [rbx+403],0
  je dqx_slot_go
  mov byte ptr [rbx+514],1
  jmp dqx_slot_go
dqx_slot_set:
  mov byte ptr [rbx+514],0
dqx_slot_go:
  xor r12d,r12d
  xor ebp,ebp
  mov esi,2
  jmp dqxA_slotRole+440
dqx_slot_orig:
  $ORIG
  jmp dqx_slot_ret
$SITE:
  jmp dqx_slot
  $NOP
dqx_slot_ret:
[DISABLE]
$SITE:
  $ORIG
dealloc(dqx_slot)
]==] }
-- slot metal spins never decrease: "dec r8d" -&gt; 3-byte nop (the count +404 stays, so +401 "metal over" is never set)
DQX.HOOKS.slotMetal = { sig='slotMetal', aa=[==[
[ENABLE]
$SITE:
  db 0F 1F 00
[DISABLE]
$SITE:
  $ORIG
]==] }
-- roulette winning number: byte patches only (no code cave): "mov ecx,24" -&gt; "mov ecx,N" and the "cmovle ecx,eax" that
-- takes the random value -&gt; 3-byte nop, so the result is always N (0 / 4). rlt0 and rlt1 (= 4) are exclusive (same bytes).
DQX.HOOKS.rltFix0 = { sig='rltMax', aa=[==[
[ENABLE]
$SITE:
  db B9 00 00 00 00
dqxA_rltMax+1A:
  db 0F 1F 00
[DISABLE]
$SITE:
  $ORIG
]==] }
DQX.HOOKS.rltFix1 = { sig='rltMax', aa=[==[
[ENABLE]
$SITE:
  db B9 04 00 00 00
dqxA_rltMax+1A:
  db 0F 1F 00
[DISABLE]
$SITE:
  $ORIG
]==] }
-- bingo ball bounciness x N (flag 09): only components whose owner (Outer) class is BP_BingoBall_C (dqxVars+48, set by
-- Lua); multiplier float at dqxVars+50
DQX.HOOKS.bounce = { sig='bounce', aa=[==[
[ENABLE]
alloc(dqx_bnc,$1000,$SITE)
label(dqx_bnc_no)
label(dqx_bnc_ret)
dqx_bnc:
  $ORIG
  cmp byte ptr [dqxFlags+09],0
  je dqx_bnc_ret
  push rax
  mov rax,[rbx+20]
  test rax,rax
  je dqx_bnc_no
  mov rax,[rax+10]
  cmp rax,[dqxVars+48]
  jne dqx_bnc_no
  mulss xmm2,[dqxVars+50]
dqx_bnc_no:
  pop rax
  jmp dqx_bnc_ret
$SITE:
  jmp dqx_bnc
  $NOP
dqx_bnc_ret:
[DISABLE]
$SITE:
  $ORIG
dealloc(dqx_bnc)
]==] }
-- bingo monster rebirth (flag 0A): the extra-open type of a passed monster panel is odd (1/3/5/7, not reborn); reborn =
-- +1 (the game's own glowing-ball rebirth does the same in BP_BingoBoardManager_C.OnOverlapGlowingBall)
DQX.HOOKS.bingoType = { sig='bingoType', aa=[==[
[ENABLE]
alloc(dqx_btp,$1000,$SITE)
label(dqx_btp_st)
label(dqx_btp_ret)
dqx_btp:
  movzx eax,byte ptr [rdi+0C]
  cmp byte ptr [dqxFlags+0A],0
  je dqx_btp_st
  test al,1
  je dqx_btp_st
  cmp al,7
  ja dqx_btp_st
  inc eax
dqx_btp_st:
  mov [rbx+364],al
  jmp dqx_btp_ret
$SITE:
  jmp dqx_btp
  $NOP
dqx_btp_ret:
[DISABLE]
$SITE:
  $ORIG
dealloc(dqx_btp)
]==] }
-- casino tension up (flag 0B), slot: no tension up yet -&gt; the game's own start (+8F: IsPlayTensionUp=1, +413, +41F,
-- start animation); already in tension up -&gt; skip the "ends on a win" check (+77)
DQX.HOOKS.slotTU = { sig='slotTU', aa=[==[
[ENABLE]
alloc(dqx_stu,$1000,$SITE)
label(dqx_stu_start)
label(dqx_stu_orig)
label(dqx_stu_else)
label(dqx_stu_ret)
dqx_stu:
  cmp byte ptr [dqxFlags+0B],0
  je dqx_stu_orig
  cmp byte ptr [rbx+5A8],0
  je dqx_stu_start
  jmp dqxA_slotTU+77
dqx_stu_start:
  jmp dqxA_slotTU+8F
dqx_stu_orig:
  cmp byte ptr [rbx+5A8],1
  jne dqx_stu_else
  jmp dqx_stu_ret
dqx_stu_else:
  jmp dqxA_slotTU+77
$SITE:
  jmp dqx_stu
  $NOP
dqx_stu_ret:
[DISABLE]
$SITE:
  $ORIG
dealloc(dqx_stu)
]==] }
-- casino tension up (flag 0B), poker: every hand takes the game's own start path ("four of a kind or better")
DQX.HOOKS.pokerTU = { sig='pokerTU', aa=[==[
[ENABLE]
alloc(dqx_ptu,$1000,$SITE)
label(dqx_ptu_t)
label(dqx_ptu_ret)
dqx_ptu:
  mov rax,[rbx+2C8]
  cmp byte ptr [dqxFlags+0B],0
  je dqx_ptu_t
  mov r14b,1
dqx_ptu_t:
  test r14b,r14b
  jmp dqx_ptu_ret
$SITE:
  jmp dqx_ptu
  $NOP
dqx_ptu_ret:
[DISABLE]
$SITE:
  $ORIG
dealloc(dqx_ptu)
]==] }

----------------------------------------------------------------- [GAME] native helper routines (called with executeCodeEx)
-- [COMMON CHANGE] assembled once per attach by DQX.initCode(). All routines only READ game memory.
--  dqx_fnfind (pool, str, len)            -&gt; FName ComparisonIndex of an ANSI string (case-insensitive) or -1
--  dqx_itemnames(pool, out, max)         -&gt; comparison indices of ANSI ID_ITEM entries
--  dqx_objfind(objarray, nameIdx, classNameIdx(-1=any), out, max) -&gt; count of UObjects with that name
--  dqx_objisa (objarray, class, out, max)  -&gt; count of live non-CDO instances of class (or subclasses)
--  dqx_holders(paramblock)                 -&gt; classes having a struct (or TArray&lt;struct&gt;) property of a given UScriptStruct
--  dqx_texts  (FText** in, FString** out, count) -&gt; FText::ToString() of each FText (ITextData::GetDisplayString, vtable+10)
DQX.CODE = [==[
alloc(dqx_code,$2000,$MODULE)
label(dqx_fnfind)
label(dqx_itemnames)
label(in_block)
label(in_entry)
label(in_wide)
label(in_next)
label(in_skip)
label(in_fail)
label(in_done)
label(in_return)
label(dqx_objfind)
label(dqx_objisa)
label(dqx_holders)
label(fn_blk)
label(fn_loop)
label(fn_cmp)
label(fn_c1)
label(fn_c2)
label(fn_found)
label(fn_adv_a)
label(fn_wide)
label(fn_next)
label(fn_fail)
label(fn_ret)
label(of_loop)
label(of_take)
label(of_next)
label(of_done)
label(oi_loop)
label(oi_next)
label(oi_done)
label(h_loop)
label(h_prop)
label(h_arr)
label(h_store)
label(h_pnext)
label(h_next)
label(h_done)
label(dqx_texts)
label(tx_loop)
label(tx_store)
label(tx_done)
registersymbol(dqx_fnfind)
registersymbol(dqx_itemnames)
registersymbol(dqx_objfind)
registersymbol(dqx_objisa)
registersymbol(dqx_holders)
registersymbol(dqx_texts)

dqx_code:
dqx_fnfind:
  push rbx
  push rsi
  push rdi
  push r12
  push r13
  push r14
  push r15
  mov r12,rcx
  mov r13,rdx
  mov r14,r8
  xor r15d,r15d
fn_blk:
  mov eax,[r12+08]
  cmp r15d,eax
  ja fn_fail
  mov rsi,[r12+r15*8+10]
  test rsi,rsi
  je fn_next
  mov rbx,rsi
  lea rdi,[rsi+20000]
  cmp r15d,eax
  jne fn_loop
  mov ecx,[r12+0C]
  lea rdi,[rsi+rcx]
fn_loop:
  cmp rbx,rdi
  jae fn_next
  movzx eax,word ptr [rbx]
  test eax,eax
  je fn_next
  mov ecx,eax
  shr ecx,6
  test al,1
  jne fn_wide
  cmp rcx,r14
  jne fn_adv_a
  xor edx,edx
fn_cmp:
  cmp rdx,rcx
  jae fn_found
  movzx r8d,byte ptr [rbx+rdx+2]
  movzx r9d,byte ptr [r13+rdx]
  lea r10d,[r8-41]
  cmp r10d,19
  ja fn_c1
  or r8d,20
fn_c1:
  lea r10d,[r9-41]
  cmp r10d,19
  ja fn_c2
  or r9d,20
fn_c2:
  cmp r8d,r9d
  jne fn_adv_a
  inc rdx
  jmp fn_cmp
fn_found:
  mov rax,rbx
  sub rax,rsi
  shr rax,1
  mov edx,r15d
  shl edx,10
  or eax,edx
  jmp fn_ret
fn_adv_a:
  lea rbx,[rbx+rcx+3]
  and bl,FE
  jmp fn_loop
fn_wide:
  lea rbx,[rbx+rcx*2+2]
  jmp fn_loop
fn_next:
  inc r15d
  jmp fn_blk
fn_fail:
  mov eax,FFFFFFFF
fn_ret:
  pop r15
  pop r14
  pop r13
  pop r12
  pop rdi
  pop rsi
  pop rbx
  ret

dqx_itemnames:
  push rbx
  push rsi
  push rdi
  push r12
  push r13
  push r14
  push r15
  mov r12,rcx
  mov r13,rdx
  mov r14d,r8d
  xor r15d,r15d
  xor r9d,r9d
in_block:
  mov eax,[r12+08]
  cmp eax,1FFF
  ja in_fail
  cmp r15d,eax
  ja in_done
  mov rsi,[r12+r15*8+10]
  test rsi,rsi
  je in_fail
  mov rbx,rsi
  lea rdi,[rsi+20000]
  cmp r15d,eax
  jne in_entry
  mov ecx,[r12+0C]
  cmp ecx,20000
  ja in_fail
  lea rdi,[rsi+rcx]
in_entry:
  lea rax,[rbx+2]
  cmp rax,rdi
  ja in_next
  movzx eax,word ptr [rbx]
  test eax,eax
  je in_next
  mov r10d,eax
  shr r10d,6
  test r10d,r10d
  je in_next
  test al,1
  jne in_wide
  lea rdx,[rbx+r10+2]
  cmp rdx,rdi
  ja in_next
  cmp r10d,7
  jb in_skip
  mov eax,[rbx+2]
  or eax,20202020
  cmp eax,697F6469
  jne in_skip
  movzx eax,word ptr [rbx+6]
  or eax,2020
  cmp eax,6574
  jne in_skip
  movzx eax,byte ptr [rbx+8]
  or eax,20
  cmp eax,6D
  jne in_skip
  cmp r9d,r14d
  jae in_fail
  mov rax,rbx
  sub rax,rsi
  shr eax,1
  mov r11d,r15d
  shl r11d,10
  or eax,r11d
  mov [r13+r9*8],rax
  inc r9d
in_skip:
  lea rbx,[rdx+1]
  and bl,FE
  jmp in_entry
in_wide:
  lea rbx,[rbx+r10*2+2]
  jmp in_entry
in_next:
  inc r15d
  jmp in_block
in_done:
  mov eax,r9d
  jmp in_return
in_fail:
  mov eax,FFFFFFFF
in_return:
  pop r15
  pop r14
  pop r13
  pop r12
  pop rdi
  pop rsi
  pop rbx
  ret

dqx_objfind:
  push rbx
  push rsi
  push rdi
  push r12
  push r13
  push r14
  push r15
  mov r12,[rcx+10]
  mov r13d,[rcx+24]
  mov r14d,edx
  mov r15d,r8d
  mov rsi,r9
  mov edi,[rsp+60]
  xor ebx,ebx
  xor r10d,r10d
of_loop:
  cmp ebx,r13d
  jae of_done
  mov eax,ebx
  shr eax,10
  mov rcx,[r12+rax*8]
  test rcx,rcx
  je of_next
  mov eax,ebx
  and eax,FFFF
  lea rax,[rax+rax*2]
  mov rcx,[rcx+rax*8]
  test rcx,rcx
  je of_next
  cmp [rcx+18],r14d
  jne of_next
  cmp r15d,FFFFFFFF
  je of_take
  mov rdx,[rcx+10]
  test rdx,rdx
  je of_next
  cmp [rdx+18],r15d
  jne of_next
of_take:
  cmp r10d,edi
  jae of_done
  mov [rsi+r10*8],rcx
  inc r10d
of_next:
  inc ebx
  jmp of_loop
of_done:
  mov eax,r10d
  pop r15
  pop r14
  pop r13
  pop r12
  pop rdi
  pop rsi
  pop rbx
  ret

dqx_objisa:
  push rbx
  push rsi
  push rdi
  push r12
  push r13
  push r14
  push r15
  mov r12,[rcx+10]
  mov r13d,[rcx+24]
  mov r14,rdx
  lea r15,[rdx+30]
  mov rsi,r8
  mov edi,r9d
  xor ebx,ebx
  xor r10d,r10d
oi_loop:
  cmp ebx,r13d
  jae oi_done
  mov eax,ebx
  shr eax,10
  mov rcx,[r12+rax*8]
  test rcx,rcx
  je oi_next
  mov eax,ebx
  and eax,FFFF
  lea rax,[rax+rax*2]
  lea rdx,[rcx+rax*8]
  test dword ptr [rdx+08],30000000
  jne oi_next
  mov rcx,[rdx]
  test rcx,rcx
  je oi_next
  test dword ptr [rcx+08],30
  jne oi_next
  mov rdx,[rcx+10]
  test rdx,rdx
  je oi_next
  movsxd rax,dword ptr [r14+38]
  cmp eax,[rdx+38]
  jg oi_next
  mov r8,[rdx+30]
  test r8,r8
  je oi_next
  cmp [r8+rax*8],r15
  jne oi_next
  cmp r10d,edi
  jae oi_done
  mov [rsi+r10*8],rcx
  inc r10d
oi_next:
  inc ebx
  jmp oi_loop
oi_done:
  mov eax,r10d
  pop r15
  pop r14
  pop r13
  pop r12
  pop rdi
  pop rsi
  pop rbx
  ret

dqx_holders:
  push rbx
  push rsi
  push rdi
  push r12
  push r13
  push r14
  push r15
  mov r15,rcx
  mov rax,[r15]
  mov r12,[rax+10]
  mov r13d,[rax+24]
  xor ebx,ebx
  xor esi,esi
h_loop:
  cmp ebx,r13d
  jae h_done
  mov eax,ebx
  shr eax,10
  mov rcx,[r12+rax*8]
  test rcx,rcx
  je h_next
  mov eax,ebx
  and eax,FFFF
  lea rax,[rax+rax*2]
  lea rdx,[rcx+rax*8]
  test dword ptr [rdx+08],30000000
  jne h_next
  mov rdi,[rdx]
  test rdi,rdi
  je h_next
  mov rcx,[rdi+10]
  test rcx,rcx
  je h_next
  mov r8,[r15+08]
  movsxd rax,dword ptr [r8+38]
  cmp eax,[rcx+38]
  jg h_next
  mov r9,[rcx+30]
  test r9,r9
  je h_next
  lea r10,[r8+30]
  cmp [r9+rax*8],r10
  jne h_next
  mov r14,[rdi+50]
  xor r11d,r11d
h_prop:
  test r14,r14
  je h_next
  inc r11d
  cmp r11d,1000
  ja h_next
  mov rax,[r14+08]
  cmp rax,[r15+18]
  jne h_arr
  mov rax,[r14+78]
  cmp rax,[r15+10]
  jne h_pnext
  xor ecx,ecx
  jmp h_store
h_arr:
  cmp rax,[r15+20]
  jne h_pnext
  mov rax,[r14+78]
  test rax,rax
  je h_pnext
  mov rcx,[rax+08]
  cmp rcx,[r15+18]
  jne h_pnext
  mov rcx,[rax+78]
  cmp rcx,[r15+10]
  jne h_pnext
  mov ecx,1
h_store:
  cmp esi,[r15+30]
  jae h_done
  mov rax,[r15+28]
  mov r8d,esi
  shl r8,4
  add rax,r8
  mov [rax],rdi
  mov r8d,[r14+4C]
  mov [rax+08],r8d
  mov [rax+0C],ecx
  inc esi
h_pnext:
  mov r14,[r14+20]
  jmp h_prop
h_next:
  inc ebx
  jmp h_loop
h_done:
  mov eax,esi
  pop r15
  pop r14
  pop r13
  pop r12
  pop rdi
  pop rsi
  pop rbx
  ret

dqx_texts:
  push rbx
  push rsi
  push rdi
  sub rsp,20
  mov rsi,rcx
  mov rdi,rdx
  mov ebx,r8d
tx_loop:
  test ebx,ebx
  je tx_done
  xor eax,eax
  mov rcx,[rsi]
  test rcx,rcx
  je tx_store
  mov rcx,[rcx]
  test rcx,rcx
  je tx_store
  mov rax,[rcx]
  call qword ptr [rax+10]
tx_store:
  mov [rdi],rax
  add rsi,8
  add rdi,8
  dec ebx
  jmp tx_loop
tx_done:
  add rsp,20
  pop rdi
  pop rsi
  pop rbx
  ret
]==]

----------------------------------------------------------------- [GAME] parameter defaults (dqxVars)
DQX.VARDEF = {
  [0x10] = { 'float', 9999999.0 },  -- one-hit kill: damage dealt to enemies
  [0x14] = { 'float', 10000.0 },    -- always critical: value written to CriticalRate / SpellCriticalRatio
  [0x18] = { 'float', 2.0 },        -- EXP multiplier
  [0x1C] = { 'float', 2.0 },        -- gold multiplier
  [0x20] = { 'int',   2 },          -- item multiplier
  [0x24] = { 'float', 1.5 },        -- movement speed multiplier (0.1 steps; value used by the hook is at +28)
  [0x28] = { 'float', 1.0 },
  [0x40] = { 'int',   20 },         -- gold multiplier x10 (written by Lua from +1C)
  [0x44] = { 'int',   0xB0 },       -- offset of FHOLIMainInventoryData.Gold (overwritten from reflection)
  [0x50] = { 'float', 5.0 },        -- bingo ball bounciness multiplier
}

----------------------------------------------------------------- [GAME] pointer slots (dqxPtrs), refreshed by the timer
DQX.PTRS = {
  [0] = function() return DQX.invBase and DQX.invBase() or nil end,     -- live FHOLIMainInventoryData
}

----------------------------------------------------------------- [GAME] called once per attach (after init)
function DQX.onAttach()
  DQX.resetCaches()
end

--=====================================================================================
--  COMMON PART  (from GameTemplate V02; changes are marked [COMMON CHANGE])
--=====================================================================================
DQX.A      = DQX.A or {}
DQX.pend   = DQX.pend or {}
DQX.hookState = DQX.hookState or {}
DQX.featOn = DQX.featOn or {}
DQX.dyn    = DQX.dyn or {}
DQX.suffix = DQX.suffix or {}

----------------------------------------------------------------- helpers
local function rq(a) if not a or a==0 then return nil end local v=readQword(a) if not v or v==0 then return nil end return v end
local function ri(a) if not a or a==0 then return nil end local v=readInteger(a) if v and v&gt;=0x80000000 then v=v-0x100000000 end return v end
local function rf(a) if not a or a==0 then return nil end return readFloat(a) end
DQX.rq, DQX.ri, DQX.rf = rq, ri, rf
function DQX.T(en,ja) if DQX.lang=='ja' then return ja or en end return en end
function DQX.msg(en,ja) print('['..DQX.TAG..'] '..DQX.T(en,ja)) end
function DQX.err(en,ja) error(DQX.T(en,ja),0) end
local function hex(n) return string.format('%X',n) end
DQX.hex = hex

function DQX.mod(name) return getAddressSafe(name or DQX.MODULE) end
function DQX.modSize(name) local ok,s=pcall(getModuleSize,name or DQX.MODULE) return ok and s or nil end
function DQX.base() return getAddressSafe('dqxFlags') end
function DQX.flag(i) return DQX.base()+i end
function DQX.var(o) return DQX.base()+DQX.OFF_VARS+o end

----------------------------------------------------------------- one-shot / user action
function DQX.userRun(fn)
  local ok,e=pcall(fn)
  if not ok then local m=tostring(e) print('['..DQX.TAG..'] '..m) pcall(showMessage,m) end
  return ok
end
function DQX.oneShot(mr, fn)
  DQX.userRun(fn)
  local t=createTimer(nil,false) t.Interval=150
  t.OnTimer=function(tt) tt.destroy() pcall(function() mr.Active=false end) end
  t.Enabled=true
end

----------------------------------------------------------------- attach / init
function DQX.attached() return DQX.pid~=nil and DQX.pid==getOpenedProcessID() and DQX.base()~=nil end
function DQX.require()
  if not DQX.attached() then
    DQX.err('Not attached to '..DQX.EXE..'. Run [ATTACH / ALL OFF] at the top of the table first.',
           DQX.EXE..'にアタッチされていません。先にテーブル先頭の【アタッチ／一括解除】を実行してください。')
  end
end
function DQX.attachGame()
  local pid=getProcessIDFromProcessName(DQX.EXE)
  if not pid or pid==0 then DQX.err('The game is not running: '..DQX.EXE,'ゲームが起動していません: '..DQX.EXE) end
  local changed=false
  if getOpenedProcessID()~=pid then
    openProcess(pid) changed=true
    DQX.msg(('Attached to %s (PID %d).'):format(DQX.EXE,pid),('%s（PID %d）にアタッチしました。'):format(DQX.EXE,pid))
  end
  if not DQX.mod() then pcall(reinitializeSymbolhandler,true) end
  if not DQX.mod() then DQX.err(DQX.MODULE..' is not loaded yet. Wait until the title screen and try again.',
                              DQX.MODULE..' がまだ読み込まれていません。タイトル画面まで待ってから再実行してください。') end
  return changed
end
function DQX.forgetProcess()
  DQX.hookState={} DQX.featOn={} DQX.A={} DQX.pend={}
  DQX.pid=nil
  DQX.patches={}                                   -- [COMMON CHANGE] data patches belong to the old process
  if DQX.resetCaches then DQX.resetCaches() end
end
function DQX.checkBuild()
  if not DQX.BUILD then return true end
  local m=DQX.mod(); local lf=ri(m+0x3C); local ts=lf and readInteger(m+lf+8)
  local sz=DQX.modSize()
  if ts~=DQX.BUILD.ts or (DQX.BUILD.size and sz~=DQX.BUILD.size) then
    DQX.msg(('WARNING: different game build (timestamp %s, size %s). Made for v%s. Features whose code is not found are disabled.'):format(hex(ts or 0),hex(sz or 0),DQX.GAMEVER),
           ('警告: 対応ビルドと異なります（タイムスタンプ %s、サイズ %s）。対応バージョンは v%s です。コードが見つからない機能は無効になります。'):format(hex(ts or 0),hex(sz or 0),DQX.GAMEVER))
    DQX.buildMatches=false
    return false
  end
  DQX.buildMatches=true
  return true
end
-- [COMMON CHANGE] assemble the native helper routines (DQX.CODE)
function DQX.initCode()
  if getAddressSafe('dqx_texts') and getAddressSafe('dqx_itemnames') and DQX.codePid==getOpenedProcessID() then return end
  for _,s in ipairs({'dqx_fnfind','dqx_itemnames','dqx_objfind','dqx_objisa','dqx_holders','dqx_texts'}) do pcall(unregisterSymbol,s) end
  local ok,info=autoAssemble((DQX.CODE:gsub('%$MODULE',DQX.MODULE)))
  if not ok then DQX.err('Helper code assembly failed:\n'..tostring(info),'補助コードの組み立てに失敗しました:\n'..tostring(info)) end
  DQX.codePid=getOpenedProcessID()
end
-- [COMMON CHANGE] allocate the shared page within +-2GB of the module (hook code addresses it RIP-relative).
-- 1) Auto Assembler alloc (CE searches free space near the module)  2) allocateMemory at candidate addresses nearby.
function DQX.allocNear(size)
  local m=DQX.mod(); local ms=DQX.modSize() or 0
  local function near(a) return a and a~=0 and math.abs(a-m)&lt;0x70000000 end
  pcall(unregisterSymbol,'dqxMem')
  local ok=autoAssemble(('alloc(dqxMem,$%X,%s)\nregistersymbol(dqxMem)\n'):format(size, DQX.MODULE))
  local a=ok and getAddressSafe('dqxMem')
  if near(a) then return a end
  for k=0,96 do
    for _,base in ipairs({ m+ms+k*0x1000000, m-(k+1)*0x1000000 }) do
      if base&gt;0x10000 then
        local ok2,b=pcall(allocateMemory, size, base)
        if ok2 and near(b) then return b end
        if ok2 and b and b~=0 then pcall(deAlloc, b) end
      end
    end
  end
  return nil
end
function DQX.init()
  local pid=getOpenedProcessID()
  if DQX.pid==pid and DQX.base() then
    DQX.initCode()
    return true
  end
  DQX.forgetProcess()
  for _,s in ipairs({'dqxFlags','dqxVars','dqxPtrs'}) do pcall(unregisterSymbol,s) end
  local mem=DQX.allocNear(DQX.MEMSIZE)
  if not mem or mem==0 then DQX.err('Memory allocation failed.','メモリ確保に失敗しました。') end
  registerSymbol('dqxFlags', mem, true)
  registerSymbol('dqxVars',  mem+DQX.OFF_VARS, true)
  registerSymbol('dqxPtrs',  mem+DQX.OFF_PTRS, true)
  for o,d in pairs(DQX.VARDEF) do
    if d[1]=='float' then writeFloat(DQX.var(o),d[2]) elseif d[1]=='qword' then writeQword(DQX.var(o),d[2]) else writeInteger(DQX.var(o),d[2]) end
  end
  DQX.pid=pid
  DQX.checkBuild()
  DQX.resolveAll()
  DQX.initCode()
  pcall(DQX.onAttach)
  DQX.startTimer()
  return true
end

----------------------------------------------------------------- AOB resolver (synchronous aobscanregion)
local function aaFind(pat, s, e)
  pcall(unregisterSymbol,'dqx_tmp')
  local ok=autoAssemble(('aobscanregion(dqx_tmp,%s,%s,%s)\nregistersymbol(dqx_tmp)\n'):format(hex(s),hex(e),pat))
  local a=ok and getAddressSafe('dqx_tmp') or nil
  pcall(unregisterSymbol,'dqx_tmp')
  if a then return a end
  -- [COMMON CHANGE] aobscanregion missed patterns that are present in the module (seen with CE 7.7: 8 of 25 code
  -- signatures, bytes verified in memory). Second try with the Lua scanner over all protections, lowest match in range.
  local okS, sl = pcall(AOBScan, pat, '*X*C*W')
  if not okS or not sl then return nil end
  local best=nil
  for i=0,sl.Count-1 do local v=tonumber(sl[i],16) if v and v&gt;=s and v&lt;e and (not best or v&lt;best) then best=v end end
  pcall(function() sl.destroy() end)
  return best
end
local function aobAll(pat, s, e, maxn)
  local r={} local from=s
  while from&lt;e and #r&lt;maxn do local a=aaFind(pat,from,e) if not a then break end r[#r+1]=a from=a+1 end
  return r
end
DQX.aobAll=aobAll
-- [COMMON CHANGE] pointer signatures (rip / iend / add / multi)
local function ripTarget(sg, a)
  local d=readInteger(a+sg.rip); if not d then return nil end
  if d&gt;=0x80000000 then d=d-0x100000000 end
  return a+sg.iend+d+(sg.add or 0)
end
DQX.ripTarget=ripTarget
function DQX.resolveSig(name)
  if DQX.A[name] then return DQX.A[name] end
  local sg=DQX.SIGS[name]; if not sg then DQX.err('Unknown signature: '..name,'未定義のシグネチャ: '..name) end
  local m=DQX.mod(sg.mod); local sz=DQX.modSize(sg.mod)
  local a
  if m and sz then
    if sg.rip and sg.multi then
      local r=aobAll(sg.p, m, m+sz, 8); local t=nil
      for _,x in ipairs(r) do local y=ripTarget(sg,x) if t==nil then t=y elseif t~=y then t=false end end
      a=t or nil
    elseif sg.nth then
      local r=aobAll(sg.p, m, m+sz, (sg.count or sg.nth)+1)
      if #r==(sg.count or #r) and r[sg.nth] then a=r[sg.nth]+(sg.off or 0) end
    else
      -- [COMMON CHANGE] known build: check the pattern at its analysed RVA first (CE 7.7 scans missed patterns that are
      -- present in memory). Accepted only when every byte matches.
      if sg.rva and DQX.buildMatches then
        local toks={} for t in sg.p:gmatch('%S+') do toks[#toks+1]=t end
        local b=readBytes(m+sg.rva, #toks, true)
        if b and #b==#toks then
          local ok=true for i,t in ipairs(toks) do if t~='??' and tonumber(t,16)~=b[i] then ok=false break end end
          if ok then a=m+sg.rva end
        end
      end
      if not a then a=aaFind(sg.p, m, m+sz) end
      if a and sg.rip then a=ripTarget(sg,a) elseif a then a=a+(sg.off or 0) end
    end
  end
  if not a then DQX.pend[name]=true return nil end
  DQX.A[name]=a; DQX.pend[name]=nil
  pcall(unregisterSymbol,'dqxA_'..name) registerSymbol('dqxA_'..name, a, true)
  return a
end
-- [COMMON CHANGE] hooks left in the game code by an earlier CE session (CE closed without ALL OFF) hide their
-- signature. For a hook signature that is not found, the same pattern with the hooked bytes replaced by a jmp is
-- searched; the original bytes are taken from the record written into the old code cave (cave+F00, "DQXORIG1"), or
-- from the pattern itself when it has no wildcard there, and written back.
DQX.ORIG_MAGIC = {0x44,0x51,0x58,0x4F,0x52,0x49,0x47,0x31}
function DQX.restoreStale(sigName)
  local sg=DQX.SIGS[sigName]; if not sg or not sg.len or sg.rip then return 0 end
  local toks={} for t in sg.p:gmatch('%S+') do toks[#toks+1]=t end
  local off, len = sg.off or 0, sg.len
  if off+len&gt;#toks then return 0 end
  local alt={} for i,t in ipairs(toks) do alt[i]=t end
  alt[off+1]='E9' for i=off+2,off+len do alt[i]='??' end
  local m=DQX.mod(sg.mod); local sz=DQX.modSize(sg.mod); if not m or not sz then return 0 end
  local n=0
  for _,x in ipairs(DQX.aobAll(table.concat(alt,' '), m, m+sz, 4)) do
    local site=x+off
    if (readBytes(site,1,false) or 0)==0xE9 then
      local rel=readInteger(site+1) or 0 if rel&gt;=0x80000000 then rel=rel-0x100000000 end
      local cave=site+5+rel
      local mg=readBytes(cave+0xF00,9,true)
      local orig=nil
      if mg then local okm=true for i=1,8 do if mg[i]~=DQX.ORIG_MAGIC[i] then okm=false end end
        if okm and mg[9]==len then orig=readBytes(cave+0xF09,len,true) end end
      -- [COMMON CHANGE] restore only with the cave record: a pattern-only match (E9 + the remaining bytes) is not proof
      -- of an old hook and restoring it overwrote unrelated game code (short patterns matched elsewhere).
      if orig then
        local db={} for i,b in ipairs(orig) do db[i]=('%02X'):format(b) end
        if autoAssemble(('%s:\ndb %s\n'):format(hex(site), table.concat(db,' '))) then n=n+1 end
      end
    end
  end
  return n
end
function DQX.resolveAll()
  local ok,ng=0,0
  for n in pairs(DQX.SIGS) do
    if not DQX.resolveSig(n) and DQX.SIGS[n].len then
      local okr,r=pcall(DQX.restoreStale,n)
      if okr and r&gt;0 then DQX.A[n]=nil DQX.resolveSig(n) end
    end
  end
  for n in pairs(DQX.SIGS) do if DQX.A[n] then ok=ok+1 else ng=ng+1 end end
  DQX.msg(('Signatures: %d found, %d not found.'):format(ok,ng),('シグネチャ: %d件検出、%d件未検出。'):format(ok,ng))
end
function DQX.pendingNames() local r={} for n in pairs(DQX.pend) do r[#r+1]=n end table.sort(r) return r end
function DQX.need(name)
  local a=DQX.resolveSig(name)
  if not a then DQX.err('Game code/data not found: '..name..'\n(the game may have been updated)',
                       'ゲームのコード／データが見つかりません: '..name..'\n（ゲーム更新で変更された可能性があります）') end
  return a
end

----------------------------------------------------------------- hook manager
function DQX.hookOn(name)
  if DQX.hookState[name] then return end
  local h=DQX.HOOKS[name] or DQX.err('Unknown hook: '..name,'未定義のフック: '..name)
  local a=DQX.resolveSig(h.sig)
  if not a then DQX.err('Game code for this feature was not found: '..h.sig..'\n(it may not be loaded yet, or the game was updated)',
                       'この機能に必要なゲームコードが見つかりません: '..h.sig..'\n（未読み込み、またはゲーム更新で変更された可能性があります）') end
  local len=DQX.SIGS[h.sig].len or 5
  local orig=readBytes(a,len,true) or DQX.err('Cannot read hook site.','フック位置を読み取れません。')
  local db={} for i,b in ipairs(orig) do db[i]=string.format('%02X',b) end
  local s=h.aa:gsub('%$SITE','dqxA_'..h.sig):gsub('%$ORIG','db '..table.concat(db,' '))
  s=s:gsub('%$NOP', len&gt;5 and ('nop '..(len-5)) or '')
  local ok,info=autoAssemble(s)
  if not ok then DQX.err('Hook assembly failed: '..name..'\n'..tostring(info),'フックの組み立てに失敗: '..name..'\n'..tostring(info)) end
  DQX.hookState[name]={script=s, info=info, addr=a, orig=orig}
  -- record the original bytes in the cave so that a later CE session can restore them (see DQX.restoreStale)
  pcall(function()
    if (readBytes(a,1,false) or 0)==0xE9 then
      local rel=readInteger(a+1) or 0 if rel&gt;=0x80000000 then rel=rel-0x100000000 end
      local cave=a+5+rel local rec={} for i,b in ipairs(DQX.ORIG_MAGIC) do rec[i]=b end
      rec[#rec+1]=len for _,b in ipairs(orig) do rec[#rec+1]=b end
      writeBytes(cave+0xF00, rec)
    end
  end)
end
function DQX.hookOff(name)
  local st=DQX.hookState[name]; if not st then return end
  if DQX.attached() then
    local ok=autoAssemble(st.script, st.info)
    if not ok then writeBytes(st.addr, st.orig) end
  end
  DQX.hookState[name]=nil
end

----------------------------------------------------------------- features
function DQX.feature(key, on)
  local f=DQX.F[key] or DQX.err('Unknown feature: '..key,'未定義の機能: '..key)
  if on then
    DQX.require()
    if f.pre then f.pre() end
    for _,h in ipairs(f.hooks or {}) do DQX.hookOn(h) end
    if f.flag then writeBytes(DQX.flag(f.flag),1) end
    if f.on then
      local ok,e=pcall(f.on)
      if not ok then                                  -- [COMMON CHANGE] roll back a failed "on"
        if f.flag then pcall(writeBytes,DQX.flag(f.flag),0) end
        if f.off then pcall(f.off) end
        error(e,0)
      end
    end
    DQX.featOn[key]=true
  else
    DQX.featOn[key]=nil
    if not DQX.attached() then return end
    if f.flag then writeBytes(DQX.flag(f.flag),0) end
    if f.off then pcall(f.off) end
    for _,h in ipairs(f.hooks or {}) do
      local used=false
      for k in pairs(DQX.featOn) do for _,x in ipairs(DQX.F[k].hooks or {}) do if x==h then used=true end end end
      if not used then DQX.hookOff(h) end
    end
  end
end
----------------------------------------------------------------- timer (pointer slots, tasks, process watch)
function DQX.tick()
  if not DQX.pid then return end
  if getOpenedProcessID()~=DQX.pid or getProcessIDFromProcessName(DQX.EXE)~=DQX.pid then
    DQX.msg('Game process changed or exited. Run [ATTACH / ALL OFF] again.','ゲームプロセスが変わったか終了しました。【アタッチ／一括解除】を再実行してください。')
    DQX.forgetProcess() return
  end
  local b=DQX.base(); if not b then return end
  DQX.tickNo=(DQX.tickNo or 0)+1
  for slot,fn in pairs(DQX.PTRS) do local ok,a=pcall(fn) writeQword(b+DQX.OFF_PTRS+slot*8, (ok and a) or 0) end
  DQX.taskErr=DQX.taskErr or {}
  for k in pairs(DQX.featOn) do local f=DQX.F[k] if f and f.task then
    local ok,e=pcall(f.task)
    -- a failing task is reported once per feature and message (it used to fail silently)
    if not ok then local m=tostring(e) if DQX.taskErr[k]~=m then DQX.taskErr[k]=m
      DQX.msg(('Feature "%s": periodic processing failed: %s'):format(k,m), ('機能「%s」の定期処理でエラー: %s'):format(k,m)) end
    elseif DQX.taskErr[k] then DQX.taskErr[k]=nil end
  end end
  for n in pairs(DQX.pend) do DQX.resolveSig(n) end
end
function DQX.startTimer()
  if DQX.timer then return end
  DQX.timer=createTimer(nil,false) DQX.timer.Interval=DQX.TICK_MS
  DQX.timer.OnTimer=function() pcall(DQX.tick) end
  DQX.timer.Enabled=true
end

----------------------------------------------------------------- ATTACH / ALL OFF
function DQX.disableAll(self)
  local ok,changed=pcall(DQX.attachGame)
  if not ok then print('['..DQX.TAG..'] '..tostring(changed)) pcall(showMessage,tostring(changed)) return end
  if changed or (DQX.pid and DQX.pid~=getOpenedProcessID()) then DQX.forgetProcess() end
  local okI,e=pcall(DQX.init) if not okI then print('['..DQX.TAG..'] '..tostring(e)) pcall(showMessage,tostring(e)) return end
  local al=getAddressList(); local n=0
  for i=al.Count-1,0,-1 do
    local m=al.getMemoryRecord(i)
    if m.Active and not (self and m.ID==self.ID) then pcall(function() m.Active=false end) n=n+1 end
  end
  for k in pairs(DQX.featOn) do pcall(DQX.feature,k,false) end            -- [COMMON CHANGE] run off() of features (restores data patches)
  if DQX.restoreAllPatches then pcall(DQX.restoreAllPatches) end
  local r=0 for name in pairs(DQX.hookState) do DQX.hookOff(name) r=r+1 end
  for i=0,0xFF do writeBytes(DQX.flag(i),0) end
  DQX.featOn={}
  DQX.msg(('Attach / All OFF: %d records deactivated, %d hooks restored.'):format(n,r),
         ('アタッチ／一括解除: %d件を無効化、%d件のフックを元に戻しました。'):format(n,r))
end

----------------------------------------------------------------- UI helpers
local PICKER_BG, PICKER_FIELD, PICKER_SELECTION = 0x252525, 0x353535, 0x805020

local function pickerForm()
  local f=createForm(false)
  f.Color=PICKER_BG
  f.Font.Name='Yu Gothic UI'
  f.Font.Size=10
  f.Font.Color=0xFFFFFF
  f.Position='poScreenCenter'
  return f
end
local function pickerPanel(panel)
  panel.ParentBackground=false
  panel.Color=PICKER_BG
end
local function pickerField(control)
  control.Color=PICKER_FIELD
  control.Font.Color=0xFFFFFF
end
local function pickerSearch(owner)
  local bar=createPanel(owner)
  bar.Align='alTop' bar.BevelOuter='bvNone' bar.AutoSize=true
  pickerPanel(bar)
  local label=createLabel(bar)
  label.Align='alLeft' label.AutoSize=true label.Layout='tlCenter'
  label.BorderSpacing.Around=8
  label.Caption=DQX.T('Search','検索') label.Font.Color=0xFFFFFF
  local search=createEdit(bar)
  search.Align='alClient' search.BorderSpacing.Around=8
  pickerField(search)
  return search
end
local function pickerSelection(list, shown)
  local selected={}
  for i,entry in ipairs(shown) do
    if (list.MultiSelect and list.Selected[i-1]) or (not list.MultiSelect and list.ItemIndex==i-1) then
      selected[entry.key or entry.value]=true
    end
  end
  return selected
end
local function filterPickerList(list, entries, text, selected)
  local query=text:lower()
  local shown={}
  list.Items.beginUpdate()
  list.Items.clear()
  for _,entry in ipairs(entries) do
    if entry.text:lower():find(query,1,true) then
      shown[#shown+1]=entry
      list.Items.add(entry.text)
    end
  end
  list.Items.endUpdate()
  for i,entry in ipairs(shown) do
    if selected[entry.key or entry.value] then
      if list.MultiSelect then list.Selected[i-1]=true else list.ItemIndex=i-1 end
    end
  end
  return shown
end
local function drawPickerItem(sender, index, rect, selected, disabled)
  local canvas=sender.Canvas
  canvas.Brush.Color=selected and PICKER_SELECTION or PICKER_BG
  canvas.Font.Color=disabled and 0xAAAAAA or 0xFFFFFF
  canvas.fillRect(rect.Left, rect.Top, rect.Right, rect.Bottom)
  if index&gt;=0 and index&lt;sender.Items.Count then
    local text=sender.Items[index]
    local y=rect.Top+math.max(0,(rect.Bottom-rect.Top-canvas.getTextHeight(text))//2)
    canvas.textOut(rect.Left+4,y,text)
  end
end
local function pickerList(owner, multi)
  local list=createListBox(owner)
  list.Style='lbOwnerDrawFixed'
  list.MultiSelect=multi
  list.Color=PICKER_BG
  list.Font.Color=0xFFFFFF
  list.ItemHeight=math.max(list.ItemHeight,list.Canvas.getTextHeight('装備Ag')+6)
  list.Align='alClient'
  list.BorderSpacing.Around=8
  list.OnDrawItem=function(sender,index,rect,state)
    local selected
    if sender.MultiSelect and index&gt;=0 and index&lt;sender.Items.Count then
      selected=sender.Selected[index]
    else
      selected=tostring(state):find('odSelected',1,true)~=nil
    end
    drawPickerItem(sender,index,rect,selected,false)
    if tostring(state):find('odFocused',1,true) then
      sender.Canvas.drawFocusRect(rect.Left,rect.Top,rect.Right,rect.Bottom)
    end
  end
  return list
end
local function pickerCombo(owner)
  local combo=createComboBox(owner)
  combo.Style='csOwnerDrawFixed'
  combo.Color=PICKER_BG
  combo.Font.Color=0xFFFFFF
  local height=combo.Canvas.getTextHeight('装備Ag')+6
  combo.ItemHeight=height
  sendMessage(combo.Handle,0x153,0,height)  -- CB_SETITEMHEIGHT: dropdown row
  sendMessage(combo.Handle,0x153,-1,height) -- CB_SETITEMHEIGHT: closed field
  combo.OnDrawItem=function(sender,index,rect,state)
    local selected=tostring(state):find('odSelected',1,true)~=nil
    drawPickerItem(sender,index,rect,selected and sender.Enabled,not sender.Enabled)
  end
  return combo
end

function DQX.pick(title, names)
  if #names==0 then return nil end
  local f=pickerForm()
  f.Caption=title f.ClientWidth=700 f.ClientHeight=520
  local search=pickerSearch(f)
  local buttons=createPanel(f)
  buttons.Align='alBottom' buttons.BevelOuter='bvNone' buttons.AutoSize=true pickerPanel(buttons)
  local accept=createButton(buttons)
  accept.Align='alRight' accept.AutoSize=true accept.BorderSpacing.Around=8
  accept.Caption=DQX.T('Select','選択') accept.Default=true
  local cancel=createButton(buttons)
  cancel.Align='alRight' cancel.AutoSize=true cancel.BorderSpacing.Around=8
  cancel.Caption=DQX.T('Cancel','キャンセル') cancel.Cancel=true cancel.ModalResult=2
  local list=pickerList(f,false)
  local shown, result = {}, nil
  local function filter()
    local query=search.Text:lower()
    local previous=shown[list.ItemIndex+1]
    shown={}
    list.Items.beginUpdate()
    list.Items.clear()
    for i,name in ipairs(names) do
      if name:lower():find(query,1,true) then
        shown[#shown+1]=i
        list.Items.add(name)
        if i==previous then list.ItemIndex=#shown-1 end
      end
    end
    if list.ItemIndex&lt;0 and #shown&gt;0 then list.ItemIndex=0 end
    list.Items.endUpdate()
    accept.Enabled=#shown&gt;0
  end
  local function choose()
    result=shown[list.ItemIndex+1]
    if result then f.ModalResult=1 end
  end
  accept.OnClick=choose
  list.OnDblClick=choose
  search.OnChange=filter
  filter()
  local ok, modal=pcall(function() return f.showModal() end)
  f.destroy()
  if not ok then error(modal,0) end
  if modal==1 then return result end
end
function DQX.askInt(title, prompt, def, lo, hi)
  local s=inputQuery(title, prompt, tostring(def or 0)); if not s then return nil end
  local v=tonumber(s)
  if not v or v%1~=0 or (lo and v&lt;lo) or (hi and v&gt;hi) then DQX.err('Invalid number: '..s,'不正な数値です: '..s) end
  return math.tointeger(v) or v
end
function DQX.clearChildren(mr) while mr.Count&gt;0 do mr.Child[0].destroy() end end
function DQX.addHdr(parent, en, ja)
  local m=getAddressList().createMemoryRecord()
  m.IsGroupHeader=true m.Description=DQX.T(en,ja) m.Color=DQX.C_GROUP m.appendToEntry(parent) m.DontSave=true
  pcall(function() m.Options='[moManualExpandCollapse]' end)
  DQX.dyn[#DQX.dyn+1]={mr=m,en=en,ja=ja} return m
end
-- [COMMON CHANGE] plain text row (group header without color)
function DQX.addText(parent, en, ja)
  local m=getAddressList().createMemoryRecord()
  m.IsGroupHeader=true m.Description=DQX.T(en,ja) m.appendToEntry(parent) m.DontSave=true
  DQX.dyn[#DQX.dyn+1]={mr=m,en=en,ja=ja} return m
end
function DQX.addRec(parent, en, ja, addr, vt)
  local m=getAddressList().createMemoryRecord()
  m.Description=DQX.T(en,ja) m.Type=vt or vtDword m.Address=addr m.appendToEntry(parent) m.DontSave=true
  DQX.dyn[#DQX.dyn+1]={mr=m,en=en,ja=ja} return m
end

----------------------------------------------------------------- language
function DQX.applyLang()
  local al=getAddressList()
  for i=0,al.Count-1 do
    local m=al.getMemoryRecord(i); local n=DQX.NAMES[m.ID]
    if n then local d=(DQX.lang=='ja') and n[2] or n[1]
      if DQX.suffix[m.ID] then d=d..' : '..DQX.suffix[m.ID] end
      m.Description=d end
  end
  local keep={}
  for _,e in ipairs(DQX.dyn) do if pcall(function() e.mr.Description=DQX.T(e.en,e.ja) end) then keep[#keep+1]=e end end
  DQX.dyn=keep
end
function DQX.setLang(l) DQX.lang=l DQX.applyLang() end

----------------------------------------------------------------- diagnostics
function DQX.luaEngine() local ok,f=pcall(getLuaEngine) if ok then return f end end
function DQX.openLuaEngine(quiet)
  local f=DQX.luaEngine()
  if f then pcall(function() f.show() end) pcall(function() f.bringToFront() end) end
  if not quiet then DQX.msg('Lua Engine opened. Messages of this table are printed here.','Lua Engineを開きました。このテーブルのメッセージはここに表示されます。') end
end
function DQX.closeLuaEngine()
  local f=DQX.luaEngine()
  if f then pcall(function() f.hide() end) end
end
function DQX.envReport()
  DQX.openLuaEngine(true)
  local pid=getOpenedProcessID()
  print(('[%s] CE version      : %s'):format(DQX.TAG, tostring(getCEVersion and getCEVersion())))
  print(('[%s] target process  : %s (running PID %s / opened PID %s / table PID %s)'):format(DQX.TAG, DQX.EXE,
        tostring(getProcessIDFromProcessName(DQX.EXE)), tostring(pid), tostring(DQX.pid)))
  local okx,x64=pcall(targetIs64Bit) print(('[%s] target 64-bit   : %s'):format(DQX.TAG, tostring(okx and x64)))
  local m=DQX.mod()
  if m then
    local lf=ri(m+0x3C); local ts=lf and readInteger(m+lf+8)
    print(('[%s] module          : %s base=%X size=%X PE-timestamp=%X'):format(DQX.TAG, DQX.MODULE, m, DQX.modSize() or 0, ts or 0))
    if DQX.BUILD then print(('[%s] expected build  : timestamp=%X size=%X (v%s)'):format(DQX.TAG, DQX.BUILD.ts, DQX.BUILD.size or 0, DQX.GAMEVER)) end
  else print(('[%s] module          : %s NOT FOUND'):format(DQX.TAG, DQX.MODULE)) end
  print(('[%s] shared memory   : dqxFlags=%s'):format(DQX.TAG, DQX.base() and hex(DQX.base()) or 'not allocated'))
  print(('[%s] attached        : %s'):format(DQX.TAG, tostring(DQX.attached())))
end
function DQX.scanSelfTest()
  DQX.openLuaEngine(true) DQX.require()
  local okn,bad=0,0
  local names={} for n in pairs(DQX.SIGS) do names[#names+1]=n end table.sort(names)
  for _,name in ipairs(names) do
    local sg=DQX.SIGS[name]; local m=DQX.mod(sg.mod); local sz=DQX.modSize(sg.mod)
    local r=aobAll(sg.p, m, m+sz, sg.multi and 8 or (sg.count and sg.count+1) or 3)
    local got={} for i,a in ipairs(r) do got[i]=string.format('%s+%X',sg.mod or DQX.MODULE,a-m) end
    local st
    if sg.multi and sg.rip and #r&gt;=1 then
      local t=nil for _,x in ipairs(r) do local y=ripTarget(sg,x) if t==nil then t=y elseif t~=y then t=false end end
      st=t and 'OK' or 'NOT UNIQUE'
      if t then got[#got+1]=('-&gt; %X'):format(t) end
    elseif sg.nth then
      st=(#r==(sg.count or 1)) and 'OK' or ((#r==0) and 'NOT FOUND' or ('COUNT '..#r))
      if st=='OK' then got[#got+1]=('use #%d'):format(sg.nth) end
    else
      st=(#r==1) and 'OK' or ((#r==0) and 'NOT FOUND' or 'NOT UNIQUE')
      if #r==1 and sg.rip then got[#got+1]=('-&gt; %X'):format(ripTarget(sg,r[1])) end
    end
    if st=='OK' then okn=okn+1 else bad=bad+1 end
    print(('[%s]   %-16s %-10s %s'):format(DQX.TAG, name, st, table.concat(got,', ')))
  end
  DQX.msg(('AOB self-test done: %d OK, %d NG.'):format(okn,bad),('AOBスキャンテスト完了: 正常 %d件、異常 %d件。'):format(okn,bad))
end
function DQX.statusReport()
  DQX.openLuaEngine(true)
  local l=DQX.pendingNames()
  print(('[%s] unresolved signatures: %s'):format(DQX.TAG, #l&gt;0 and table.concat(l,', ') or '-'))
  local h={} for n,st in pairs(DQX.hookState) do h[#h+1]=('%s@%X'):format(n,st.addr) end table.sort(h)
  print(('[%s] active hooks        : %s'):format(DQX.TAG, #h&gt;0 and table.concat(h,', ') or '-'))
  local f={} for k in pairs(DQX.featOn) do f[#f+1]=k end table.sort(f)
  print(('[%s] features on         : %s'):format(DQX.TAG, #f&gt;0 and table.concat(f,', ') or '-'))
  local np=0 for _,l2 in pairs(DQX.patches or {}) do np=np+#l2 end
  print(('[%s] data patches        : %d values'):format(DQX.TAG, np))
  local al=getAddressList(); local a={}
  for i=0,al.Count-1 do local m=al.getMemoryRecord(i) if m.Active then a[#a+1]=('[%d] %s'):format(m.ID,m.Description) end end
  print(('[%s] active records      : %s'):format(DQX.TAG, #a&gt;0 and table.concat(a,' / ') or '-'))
end

----------------------------------------------------------------- initial layout: only "0." expanded
function DQX.initCollapse()
  local al=getAddressList()
  for i=0,al.Count-1 do
    local m=al.getMemoryRecord(i)
    if m.Count&gt;0 then
      local opt='' pcall(function() opt=tostring(m.Options) end)
      if not opt:find('moHideChildren') then
        local inZero=false local p=m
        while p do if p.ID==DQX.ID_TOP0 then inZero=true break end p=p.Parent end
        pcall(function() m.Collapsed = not inZero end)
      end
    end
  end
end

--=====================================================================================
--  [GAME] UE4.26 RUNTIME LAYER (reflection, containers, game-thread calls)
--  Layout constants are those of UE 4.26 and were checked against this executable:
--   UObject: Flags+08 Index+0C Class+10 Name+18 Outer+20 | UField: Next+28 | UStruct: Chain+30 Super+40
--   Children+48 ChildProperties+50 PropertiesSize+58 | FField: Class+08 Next+20 Name+28
--   FProperty: ElementSize+3C Offset+4C | Struct/Array/Map sub-property +78 | MapLayout+88
--   FUObjectArray: Objects+10 NumElements+24, item size 0x18 | FNamePool: CurrentBlock+08 Cursor+0C Blocks+10
--   TMap/TSet: Data+00 Num+08 Max+0C BitInline+10 BitHeap+20 NumBits+28 MaxBits+2C FirstFree+30
--              NumFree+34 HashInline+38 HashHeap+40 HashSize+48 | UDataTable: RowStruct+28 RowMap+30
--=====================================================================================
local UE = {}
DQX.UE = UE
local hex = DQX.hex

function DQX.resetCaches()
  UE.nameCache, UE.idxCache, UE.objCache = {}, {}, {}
  DQX.cache = { tables={}, offs={} }
  DQX._idb = nil
  DQX._snap = {}
  DQX._invInst = nil
end
DQX.resetCaches()

local function rb(a) local v=readBytes(a,1,false) return v end
local function u32(v) return v and (v &amp; 0xFFFFFFFF) or nil end
local function s32(v) if v and v&gt;=0x80000000 then return v-0x100000000 end return v end
UE.rb, UE.s32 = rb, s32

-- UTF-16LE byte table -&gt; UTF-8 string
local function utf16(b)
  local out={} local i=1 local n=#b
  while i+1&lt;=n do
    local c=b[i]|(b[i+1]&lt;&lt;8) i=i+2
    if c&gt;=0xD800 and c&lt;=0xDBFF and i+1&lt;=n then
      local c2=b[i]|(b[i+1]&lt;&lt;8) i=i+2
      c=0x10000+((c-0xD800)&lt;&lt;10)+(c2-0xDC00)
    end
    if c==0 then break end
    if c&lt;0x80 then out[#out+1]=string.char(c)
    elseif c&lt;0x800 then out[#out+1]=string.char(0xC0|(c&gt;&gt;6),0x80|(c&amp;0x3F))
    elseif c&lt;0x10000 then out[#out+1]=string.char(0xE0|(c&gt;&gt;12),0x80|((c&gt;&gt;6)&amp;0x3F),0x80|(c&amp;0x3F))
    else out[#out+1]=string.char(0xF0|(c&gt;&gt;18),0x80|((c&gt;&gt;12)&amp;0x3F),0x80|((c&gt;&gt;6)&amp;0x3F),0x80|(c&amp;0x3F)) end
  end
  return table.concat(out)
end
UE.utf16 = utf16

function UE.pool() return DQX.need('gnames') end
function UE.objs() return DQX.need('gobjs') end

-- FName ComparisonIndex -&gt; string (number suffix appended when num&gt;0)
function UE.nameStr(idx, num)
  if not idx then return nil end
  idx=u32(idx)
  local s=UE.nameCache[idx]
  if not s then
    local base=readQword(UE.pool()+0x10+(idx&gt;&gt;16)*8)
    if not base or base==0 then return nil end
    local e=base+(idx&amp;0xFFFF)*2
    local hb=readBytes(e,2,true); if not hb then return nil end
    local h=hb[1]|(hb[2]&lt;&lt;8)
    local len=h&gt;&gt;6
    if len==0 or len&gt;1023 then return nil end
    if (h&amp;1)==1 then
      local b=readBytes(e+2,len*2,true); if not b then return nil end
      s=utf16(b)
    else
      local b=readBytes(e+2,len,true); if not b then return nil end
      local t={} for i=1,#b do t[i]=string.char(b[i]) end
      s=table.concat(t)
    end
    UE.nameCache[idx]=s
  end
  if num and num&gt;0 then return s..'_'..(num-1) end
  return s
end
function UE.objName(o) if not o or o==0 then return nil end return UE.nameStr(readInteger(o+0x18), readInteger(o+0x1C)) end

local function ex(addr, ...)
  local args={...} local p={}
  for i,v in ipairs(args) do p[i]={type=0,value=v} end
  return executeCodeEx(0, 15000, addr, table.unpack(p))
end
UE.ex = ex

-- string -&gt; FName ComparisonIndex (native search of the name pool), nil if the name does not exist
function UE.nameIdx(str)
  local k=str:lower()
  local v=UE.idxCache[k]
  if v~=nil then return v or nil end
  DQX.require()
  local buf=DQX.base()+DQX.OFF_STR
  if #str&gt;250 then return nil end
  writeString(buf,str) writeBytes(buf+#str,0)
  local r=ex(getAddress('dqx_fnfind'), UE.pool(), buf, #str)
  r=r and (r &amp; 0xFFFFFFFF)
  if not r or r==0xFFFFFFFF then UE.idxCache[k]=false return nil end
  UE.idxCache[k]=r
  return r
end

local function readOut(n)
  local r={} local o=DQX.base()+DQX.OFF_OUT
  for i=0,n-1 do r[#r+1]=readQword(o+i*8) end
  return r
end
-- all UObjects with the given name (and class name)
function UE.find(name, clsName, max)
  local ni=UE.nameIdx(name); if not ni then return {} end
  local ci=-1
  if clsName then ci=UE.nameIdx(clsName); if not ci then return {} end end
  local n=ex(getAddress('dqx_objfind'), UE.objs(), ni, ci &amp; 0xFFFFFFFF, DQX.base()+DQX.OFF_OUT, math.min(max or 64,DQX.OUT_MAX))
  return readOut((n or 0) &amp; 0xFFFFFFFF)
end
function UE.cls(name)
  local k='C:'..name
  local c=UE.objCache[k]
  if c and readInteger(c+0x18)==UE.nameIdx(name) then return c end
  local r=UE.find(name,'Class',4)
  if #r==0 then r=UE.find(name,'BlueprintGeneratedClass',4) end
  if #r==0 then r=UE.find(name,'WidgetBlueprintGeneratedClass',4) end
  UE.objCache[k]=r[1]
  return r[1]
end
function UE.struct(name)
  local k='S:'..name
  local c=UE.objCache[k]
  if c then return c end
  local r=UE.find(name,'ScriptStruct',4)
  UE.objCache[k]=r[1]
  return r[1]
end
function UE.needCls(name) return UE.cls(name) or DQX.err('Class not found: '..name,'クラスが見つかりません: '..name) end
function UE.needStruct(name) return UE.struct(name) or DQX.err('Struct not found: '..name,'構造体が見つかりません: '..name) end
-- live (non-CDO) instances of a class and its subclasses
function UE.instances(cls, max)
  if not cls then return {} end
  local n=ex(getAddress('dqx_objisa'), UE.objs(), cls, DQX.base()+DQX.OFF_OUT, math.min(max or 64,DQX.OUT_MAX))
  return readOut((n or 0) &amp; 0xFFFFFFFF)
end
function UE.isA(obj, cls)
  local c=obj and readQword(obj+0x10); if not c or c==0 or not cls then return false end
  local d=s32(readInteger(cls+0x38)); if not d or d&lt;0 then return false end
  local cd=s32(readInteger(c+0x38)); if not cd or cd&lt;d then return false end
  local ch=readQword(c+0x30); if not ch or ch==0 then return false end
  return readQword(ch+d*8)==cls+0x30
end

-- object still alive: its GUObjectArray slot points back to it and it is neither pending kill / unreachable
-- (EInternalObjectFlags 0x30000000) nor begin/finish-destroyed (RF 0x18000). Used before writing to cached objects.
function UE.alive(obj)
  if not obj or obj==0 then return false end
  local idx=readInteger(obj+0xC); if not idx or idx&lt;0 or idx&gt;=0x4000000 then return false end
  local objs=UE.objs(); local n=readInteger(objs+0x24) or 0
  if idx&gt;=n then return false end
  local chunks=readQword(objs+0x10); if not chunks or chunks==0 then return false end
  local ch=readQword(chunks+(idx&gt;&gt;16)*8); if not ch or ch==0 then return false end
  local it=ch+(idx&amp;0xFFFF)*0x18
  if readQword(it)~=obj then return false end
  local fl=readInteger(it+8) or 0x30000000
  if (fl &amp; 0x30000000)~=0 then return false end
  local rf=readInteger(obj+8) or 0x18000
  return (rf &amp; 0x18000)==0
end
-- object referenced by a TSoftObjectPtr at a (UE4.26: FWeakObjectPtr{ObjectIndex,SerialNumber} +0,
-- FSoftObjectPath.AssetPathName (FName) +0x10). Returns obj or nil, plus the asset path. Never loads anything.
function UE.softObj(a, clsName)
  local idx, ser = readInteger(a), readInteger(a+4)
  if idx and idx&gt;=0 and idx&lt;0x4000000 then
    local objs=UE.objs(); local chunks=readQword(objs+0x10)
    if chunks and chunks~=0 and idx&lt;(readInteger(objs+0x24) or 0) then
      local ch=readQword(chunks+(idx&gt;&gt;16)*8)
      local it=ch and ch~=0 and ch+(idx&amp;0xFFFF)*0x18
      local o=it and readQword(it)
      if o and o~=0 and (ser==0 or readInteger(it+0x10)==ser) and UE.alive(o) then return o end
    end
  end
  local path=UE.nameStr(readInteger(a+0x10), readInteger(a+0x14))
  local an=path and path:match('%.([^%./]+)$')
  if an then for _,o in ipairs(UE.find(an, clsName, 4)) do if UE.alive(o) then return o, path end end end
  return nil, path
end
-- value of an enum entry (UEnum::Names TArray&lt;TPair&lt;FName,int64&gt;&gt; at +0x40), or nil
function UE.enumValue(enumName, entry)
  local e=UE.find(enumName,'Enum',2)[1]; if not e then return nil end
  local d=readQword(e+0x40) local n=readInteger(e+0x48) or 0
  if not d or d==0 then return nil end
  for i=0,math.min(n,2048)-1 do local a=d+i*0x10
    local s=UE.nameStr(readInteger(a),readInteger(a+4))
    if s==entry or s==enumName..'::'..entry then return readQword(a+8) end
  end
end
-- FProperty helpers
function UE.fieldName(f) return UE.nameStr(readInteger(f+0x28), readInteger(f+0x2C)) end
function UE.fieldClass(f) local fc=readQword(f+8) return fc and UE.nameStr(readInteger(fc)) end
function UE.prop(st, name)
  local idx=UE.nameIdx(name); if not idx or not st then return nil end
  local s=st local g=0
  while s and s~=0 and g&lt;64 do
    local f=readQword(s+0x50) local g2=0
    while f and f~=0 and g2&lt;4096 do
      if u32(readInteger(f+0x28))==idx then return f end
      f=readQword(f+0x20) g2=g2+1
    end
    s=readQword(s+0x40) g=g+1
  end
  return nil
end
-- name of the property of st (or a super) at offset off, or nil
function UE.propNameAt(st, off)
  local s=st local g=0
  while s and s~=0 and g&lt;64 do
    local f=readQword(s+0x50) local g2=0
    while f and f~=0 and g2&lt;4096 do
      if readInteger(f+0x4C)==off then return UE.fieldName(f) end
      f=readQword(f+0x20) g2=g2+1
    end
    s=readQword(s+0x40) g=g+1
  end
end
function UE.needProp(st, name)
  return UE.prop(st,name) or DQX.err(('Property not found: %s.%s'):format(UE.objName(st) or '?',name),
                                     ('プロパティが見つかりません: %s.%s'):format(UE.objName(st) or '?',name))
end
function UE.off(st, name)
  local k=hex(st or 0)..'.'..name
  local v=DQX.cache.offs[k]
  if v then return v end
  local f=UE.needProp(st,name)
  v=readInteger(f+0x4C)
  DQX.cache.offs[k]=v
  return v, f
end
function UE.boolBit(f) return readInteger(f+0x4C)+(rb(f+0x79) or 0), rb(f+0x7A) or 1 end
function UE.innerStruct(f) return readQword(f+0x78) end
-- TMap layout (from FMapProperty::MapLayout, with sanity check / fallback)
function UE.mapLayout(f, defVo, defSize)
  local vo=readInteger(f+0x88); local hn=readInteger(f+0x8C); local hi=readInteger(f+0x90); local sz=readInteger(f+0x94)
  if vo and hn and hi and sz and sz&gt;=8 and sz&lt;=0x1000 and vo&gt;0 and vo&lt;sz and hn&lt;sz and hi&lt;sz and hn+4&lt;=sz and hi+4&lt;=sz then
    return {vo=vo, hn=hn, hi=hi, size=sz}
  end
  return {vo=defVo or 8, hn=(defSize or 20)-8, hi=(defSize or 20)-4, size=defSize or 20}
end
-- UFunction lookup (Children chain, incl. super classes)
function UE.func(cls, name)
  local idx=UE.nameIdx(name); if not idx or not cls then return nil end
  local s=cls local g=0
  while s and s~=0 and g&lt;64 do
    local f=readQword(s+0x48) local g2=0
    while f and f~=0 and g2&lt;8192 do
      if u32(readInteger(f+0x18))==idx then return f end
      f=readQword(f+0x28) g2=g2+1
    end
    s=readQword(s+0x40) g=g+1
  end
end

-- FString / FText readers
function UE.readFString(a)
  local d=readQword(a); local n=readInteger(a+8)
  if not d or d==0 or not n or n&lt;=1 or n&gt;4096 then return nil end
  local b=readBytes(d,(n-1)*2,true); if not b then return nil end
  return utf16(b)
end
local function goodText(s) return s and #s&gt;0 and not s:find('[%z\1-\8\11\12\14-\31]') end
-- FText -&gt; string with the game's own FText::ToString (ITextData::GetDisplayString), batched in native code
function UE.readFTexts(addrs)
  local out={}
  local b=DQX.base()
  -- preferred: FText::ToString on the game thread (refreshes string-table texts)
  local ts=DQX.resolveSig('ftextToString')
  if ts then
    local ok=pcall(function()
      local i=1
      while i&lt;=#addrs do
        local calls={} local n=math.min(200, #addrs-i+1)
        for k=0,n-1 do calls[k+1]={ts, addrs[i+k] or 0} end
        local r=DQX.gameCallBatch(calls, 8000)
        for k=0,n-1 do
          local p=r[k+1][1]
          local s=(p and p~=0) and UE.readFString(p) or nil
          out[i+k]=goodText(s) and s or nil
        end
        i=i+n
      end
    end)
    if ok then return out end
    out={}
  end
  local i=1
  while i&lt;=#addrs do
    local n=math.min(DQX.TEXT_MAX, #addrs-i+1)
    for k=0,n-1 do writeQword(b+DQX.OFF_TIN+k*8, addrs[i+k] or 0) end
    ex(getAddress('dqx_texts'), b+DQX.OFF_TIN, b+DQX.OFF_TOUT, n)
    for k=0,n-1 do
      local p=readQword(b+DQX.OFF_TOUT+k*8)
      local s=(p and p~=0) and UE.readFString(p) or nil
      out[i+k]=goodText(s) and s or nil
    end
    i=i+n
  end
  return out
end
function UE.readFText(a)
  if not a or a==0 then return nil end
  local ok,r=pcall(UE.readFTexts,{a})
  return ok and r[1] or nil
end

-- TMap iteration: fn(index, elementAddress)
function UE.mapIter(m, L, fn)
  local data=readQword(m); local num=readInteger(m+8)
  if not data or data==0 or not num or num&lt;=0 or num&gt;500000 then return end
  local sec=readQword(m+0x20); local bits=(sec and sec~=0) and sec or (m+0x10)
  local w, wi = 0, -1
  for i=0,num-1 do
    local k=i&gt;&gt;5
    if k~=wi then w=readInteger(bits+k*4) or 0 wi=k end
    if ((w&gt;&gt;(i&amp;31))&amp;1)==1 then
      if fn(i, data+i*L.size)==false then return end
    end
  end
end
-- TMap&lt;FName,...&gt; lookup through the hash (fast; works on any map with FName keys)
function UE.nameMapState(m)
  local hs=readInteger(m+0x48) or 0
  if hs&lt;=0 or hs&gt;0x1000000 or (hs &amp; (hs-1))~=0 then return nil end
  local data=readQword(m); if not data or data==0 then return nil end
  local hp=readQword(m+0x40); if not hp or hp==0 then hp=m+0x38 end
  return {data=data,hash=hp,mask=hs-1}
end
function UE.mapFindName(m,L,idx,num,state)
  state=state or UE.nameMapState(m)
  if not state then return nil end
  local data=state.data
  local id=s32(readInteger(state.hash+(UE.nameHash(idx,num)&amp;state.mask)*4))
  local g=0
  while id and id&gt;=0 and g&lt;100000 do
    local e=data+id*L.size
    if u32(readInteger(e))==u32(idx) and readInteger(e+4)==num then return e end
    id=s32(readInteger(e+L.hn)) g=g+1
  end
end
-- FName hash as used by TMap&lt;FName,...&gt; in this build (GetTypeHash(FNameEntryId) + Number)
function UE.nameHash(idx, num)
  idx=idx &amp; 0xFFFFFFFF
  local off=idx &amp; 0xFFFF; local blk=(idx&gt;&gt;16) &amp; 0xFFFF
  local r=(blk*0x80001 + off + (off&lt;&lt;16)) &amp; 0xFFFFFFFF
  return ((off&gt;&gt;4) + r + (num or 0)) &amp; 0xFFFFFFFF
end

-- memory allocation with the game's allocator (FMemory::Realloc; thread safe)
function DQX.realloc(p, size)
  local a=DQX.need('realloc')
  local r=ex(a, p or 0, size, 0)
  if not r or r==0 then DQX.err('FMemory::Realloc failed.','FMemory::Reallocに失敗しました。') end
  return r
end
local zeroCache={}
local function zeros(n)
  local z=zeroCache[n]
  if not z then z={} for i=1,n do z[i]=0 end zeroCache[n]=z end
  return z
end
UE.zeros = zeros
function UE.fill0(a, n)
  local p=0
  while p&lt;n do local c=math.min(4096,n-p) writeBytes(a+p, zeros(c)) p=p+c end
end

-- TMap&lt;FName,int32&gt; insert / update. Key = (ComparisonIndex, Number).
function UE.mapSetNameInt(m, L, kIdx, kNum, val)
  local found
  UE.mapIter(m, L, function(i,e)
    if u32(readInteger(e))==u32(kIdx) and readInteger(e+4)==kNum then found=e return false end
  end)
  if found then writeInteger(found+L.vo, val) return found, false end
  local hsize=readInteger(m+0x48) or 0
  -- verify the hash function against existing elements before touching the hash
  if hsize&gt;0 then
    local bad, cnt = false, 0
    UE.mapIter(m, L, function(i,e)
      local h=UE.nameHash(readInteger(e), readInteger(e+4)) &amp; (hsize-1)
      if readInteger(e+L.hi)~=h then bad=true return false end
      cnt=cnt+1 if cnt&gt;=32 then return false end
    end)
    if bad then DQX.err('Container layout check failed (hash). Item was not added.','コンテナ構造の検証に失敗しました（ハッシュ）。追加を中止しました。') end
  end
  local data=readQword(m) or 0; local num=readInteger(m+8) or 0; local max=readInteger(m+12) or 0
  local numFree=readInteger(m+0x34) or 0
  local idx
  local sec=readQword(m+0x20) or 0
  if numFree&gt;0 then
    idx=s32(readInteger(m+0x30))
    if not idx or idx&lt;0 or idx&gt;=num then DQX.err('Container free list is invalid. Item was not added.','コンテナの空きリストが不正です。追加を中止しました。') end
    local e=data+idx*L.size
    local nextFree=s32(readInteger(e+4))
    writeInteger(m+0x30, nextFree &amp; 0xFFFFFFFF)
    writeInteger(m+0x34, numFree-1)
    if numFree-1&gt;0 and nextFree&gt;=0 then writeInteger(data+nextFree*L.size, 0xFFFFFFFF) end
  else
    if num&gt;=max then
      local nmax=num+16
      data=DQX.realloc(data, nmax*L.size)
      writeQword(m, data) writeInteger(m+12, nmax)
    end
    idx=num
    writeInteger(m+8, num+1)
    local nbits=readInteger(m+0x28) or 0; local maxbits=readInteger(m+0x2C) or 0
    if nbits+1&gt;maxbits then
      local words=((nbits+1)+31)&gt;&gt;5
      if sec==0 and words&lt;=4 then
        maxbits=128
      else
        local nw=words+4
        local nsec=DQX.realloc(sec~=0 and sec or 0, nw*4)
        if sec==0 then
          for k=0,3 do writeInteger(nsec+k*4, readInteger(m+0x10+k*4) or 0) end
          UE.fill0(nsec+16, nw*4-16)
        else
          local oldw=maxbits&gt;&gt;5
          if nw&gt;oldw then UE.fill0(nsec+oldw*4, (nw-oldw)*4) end
        end
        writeQword(m+0x20, nsec) sec=nsec
        maxbits=nw*32
      end
      writeInteger(m+0x2C, maxbits)
    end
    writeInteger(m+0x28, nbits+1)
  end
  local bits=(sec~=0) and sec or (m+0x10)
  local wa=bits+(idx&gt;&gt;5)*4
  writeInteger(wa, ((readInteger(wa) or 0) | (1&lt;&lt;(idx&amp;31))) &amp; 0xFFFFFFFF)
  local e=data+idx*L.size
  writeInteger(e, kIdx) writeInteger(e+4, kNum) writeInteger(e+L.vo, val)
  if hsize&lt;=0 then
    writeInteger(m+0x38, 0xFFFFFFFF) writeQword(m+0x40, 0) writeInteger(m+0x48, 1) hsize=1
  end
  local hp=readQword(m+0x40); if not hp or hp==0 then hp=m+0x38 end
  local h=UE.nameHash(kIdx,kNum) &amp; (hsize-1)
  writeInteger(e+L.hi, h)
  writeInteger(e+L.hn, readInteger(hp+h*4))
  writeInteger(hp+h*4, idx)
  return e, true
end
-- construct a UScriptStruct value in place with the game's own C++ constructor (UScriptStruct::CppStructOps-&gt;Construct)
function UE.structConstruct(st, dest)
  local ops=readQword(st+0xB8)
  if not ops or ops==0 then return false end
  local vt=readQword(ops); if not vt or vt==0 then return false end
  local fn=readQword(vt+0x18); local m=DQX.mod(); local ms=DQX.modSize() or 0
  if not fn or fn&lt;m or fn&gt;=m+ms then return false end
  ex(fn, ops, dest)
  return true
end
-- TArray: append count zeroed elements, returns address of the first new element
function UE.arrayAdd(a, esz, count)
  local data=readQword(a) or 0; local num=readInteger(a+8) or 0; local max=readInteger(a+12) or 0
  if num+count&gt;max then
    local nmax=num+count+8
    data=DQX.realloc(data, nmax*esz)
    writeQword(a, data) writeInteger(a+12, nmax)
  end
  local p=data+num*esz
  UE.fill0(p, esz*count)
  writeInteger(a+8, num+count)
  return p
end

-- game-thread calls (ProcessEvent hook). list = { {fn, a1..a6}, ... } -&gt; results { {rax, xmm0q}, ... }
function DQX.gameCallBatch(list, timeout)
  DQX.require()
  if #list==0 then return {} end
  if #list&gt;200 then DQX.err('Too many calls in one batch.','一度に実行する呼び出しが多すぎます。') end
  DQX.hookOn('peSvc')
  local b=DQX.base()
  writeInteger(b+DQX.OFF_CALL+4, 0)
  for i,c in ipairs(list) do
    local e=b+DQX.OFF_CALL+0x10+(i-1)*0x50
    writeQword(e, c[1])
    for k=1,6 do writeQword(e+k*8, c[k+1] or 0) end
  end
  writeInteger(b+DQX.OFF_CALL, #list)
  writeBytes(b+0xF0, 1)
  local t0=getTickCount()
  while readInteger(b+DQX.OFF_CALL+4)~=1 do
    if getTickCount()-t0&gt;(timeout or 4000) then
      writeBytes(b+0xF0, 0)
      DQX.err('The game did not respond (game-thread call timed out). Make sure the game window is running (not paused/minimized).',
              'ゲームが応答しません（ゲームスレッド呼び出しがタイムアウト）。ゲームが動作中（一時停止・最小化していない）か確認してください。')
    end
    sleep(5)
  end
  local r={}
  for i=1,#list do local e=b+DQX.OFF_CALL+0x10+(i-1)*0x50 r[i]={readQword(e+0x38), readQword(e+0x40)} end
  return r
end

-- call UFunctions through UObject::ProcessEvent on the game thread.
-- reqs = { {obj=, func=, set=function(parmsAddr) end, get=function(parmsAddr) return ... end}, ... }
function DQX.callUFuncs(reqs)
  local pe=DQX.need('pe')
  local out={}
  local i=1
  while i&lt;=#reqs do
    local batch, slice = {}, {}
    for j=i, math.min(#reqs, i+63) do
      local rq=reqs[j]
      local parms=DQX.base()+DQX.OFF_PARMS+(j-i)*0x80
      UE.fill0(parms, 0x80)
      if rq.set then rq.set(parms) end
      batch[#batch+1]={pe, rq.obj, rq.func, parms}
      slice[#slice+1]={rq, parms}
    end
    DQX.gameCallBatch(batch, 8000)
    for _,s in ipairs(slice) do local v=true if s[1].get then v=s[1].get(s[2]) end out[#out+1]=v end
    i=i+64
  end
  return out
end

----------------------------------------------------------------- data patches (restorable)
DQX.patches = DQX.patches or {}
local function rd(kind,a) if kind=='f' then return readFloat(a) elseif kind=='b' then return rb(a) else return readInteger(a) end end
local function wr(kind,a,v) if kind=='f' then writeFloat(a,v) elseif kind=='b' then writeBytes(a,v) else writeInteger(a,v) end end
-- owner: the UObject (DataTable) holding addr, defaults to DQX.patchOwner; restore skips owners that were unloaded
function DQX.patch(key, addr, kind, val, owner)
  local l=DQX.patches[key]; if not l then l={idx={}} DQX.patches[key]=l end
  if not l.idx[addr] then
    local o=rd(kind,addr); if o==nil then return false end
    l[#l+1]={addr,kind,o,owner or DQX.patchOwner} l.idx[addr]=#l
  end
  wr(kind,addr,val)
  return true
end
function DQX.restore(key, alive)
  local l=DQX.patches[key]; if not l then return 0 end
  local n=0
  if DQX.attached() then
    for i=#l,1,-1 do local p=l[i]
      if ((not alive) or alive(p[1])) and ((not p[4]) or UE.alive(p[4])) then pcall(wr,p[2],p[1],p[3]) n=n+1 end end
  end
  DQX.patches[key]=nil
  return n
end
function DQX.restoreAllPatches() for k in pairs(DQX.patches) do DQX.restore(k) end end

----------------------------------------------------------------- DataTables
function DQX.tablesOf(structName)
  local c=DQX.cache.tables[structName]
  if c then
    local ok=true for _,dt in ipairs(c.list) do if not UE.alive(dt) then ok=false break end end
    if ok then return c.list, c.st end
  end
  local st=UE.needStruct(structName)
  local dtc=UE.needCls('DataTable')
  local list={}
  for _,dt in ipairs(UE.instances(dtc, 4096)) do
    if readQword(dt+0x28)==st then list[#list+1]=dt end
  end
  DQX.cache.tables[structName]={list=list, st=st}
  return list, st
end
-- rows of a DataTable: { {idx=, num=, row=}, ... }
local ROWL={vo=8, hn=0x10, hi=0x14, size=0x18}
UE.ROWL=ROWL
function DQX.rows(dt)
  local r={}
  UE.mapIter(dt+0x30, ROWL, function(i,e)
    local p=readQword(e+8)
    if p and p~=0 then r[#r+1]={idx=u32(readInteger(e)), num=readInteger(e+4), row=p} end
  end)
  return r
end
function DQX.allRows(structName)
  local list, st = DQX.tablesOf(structName)
  local r={}
  for _,dt in ipairs(list) do for _,x in ipairs(DQX.rows(dt)) do x.dt=dt r[#r+1]=x end end
  return r, st
end
-- offset of the first struct property of type target inside st (depth &lt;= 2), or nil
function DQX.structPath(st, target, depth)
  depth=depth or 2
  local f=readQword(st+0x50) local g=0
  while f and f~=0 and g&lt;2048 do
    if UE.fieldClass(f)=='StructProperty' then
      local s=readQword(f+0x78)
      if s==target then return readInteger(f+0x4C) end
      if depth&gt;1 and s and s~=0 then
        local o=DQX.structPath(s, target, depth-1)
        if o then return readInteger(f+0x4C)+o end
      end
    end
    f=readQword(f+0x20) g=g+1
  end
end

----------------------------------------------------------------- string tables
-- pend: list of {item, FTextAddress}. For texts that come from a string table:
--   KismetTextLibrary::StringTableIdAndKeyFromText -&gt; (TableId, Key)
--   tables not registered yet are loaded with KismetSystemLibrary::LoadAsset_Blocking (TableId = asset path)
--   KismetStringTableLibrary::GetTableEntrySourceString(TableId, Key) -&gt; display string
function UE.cdo(clsName) return UE.find('Default__'..clsName, clsName, 1)[1] end
function UE.stringTableNames(pend)
  local ktl, kst, ksl = UE.needCls('KismetTextLibrary'), UE.needCls('KismetStringTableLibrary'), UE.needCls('KismetSystemLibrary')
  local cTL, cST, cSL = UE.cdo('KismetTextLibrary'), UE.cdo('KismetStringTableLibrary'), UE.cdo('KismetSystemLibrary')
  if not cTL or not cST or not cSL then DQX.err('Kismet library objects not found.','Kismetライブラリのオブジェクトが見つかりません。') end
  local fId=UE.func(ktl,'StringTableIdAndKeyFromText') or DQX.err('StringTableIdAndKeyFromText not found.','StringTableIdAndKeyFromTextが見つかりません。')
  local oT, oTid, oKey, oR = UE.off(fId,'Text'), UE.off(fId,'OutTableId'), UE.off(fId,'OutKey'), UE.off(fId,'ReturnValue')
  local q={}
  for i,p in ipairs(pend) do
    q[i]={obj=cTL, func=fId,
      set=function(b) writeBytes(b+oT, readBytes(p[2],0x18,true)) end,
      get=function(b) if (rb(b+oR) or 0)==0 then return false end
        return {tid={readInteger(b+oTid), readInteger(b+oTid+4)}, key=readBytes(b+oKey,0x10,true), keyS=UE.readFString(b+oKey)} end}
  end
  local ids=DQX.callUFuncs(q)
  local tables, nST, sample = {}, 0, nil
  for i,r in ipairs(ids) do if r then nST=nST+1 local k=(r.tid[1] &amp; 0xFFFFFFFF)..':'..r.tid[2]
    if not tables[k] then tables[k]={tid=r.tid, n=0} end tables[k].n=tables[k].n+1
    if not sample then sample=('%s / %s'):format(tostring(UE.nameStr(r.tid[1],r.tid[2])), tostring(r.keyS)) end end end
  if nST==0 then return 0 end
  -- make sure every table is registered
  local fReg=UE.func(kst,'IsRegisteredTableId'); local fLoad=UE.func(ksl,'LoadAsset_Blocking')
  local oRegId, oRegR = UE.off(fReg,'TableId'), UE.off(fReg,'ReturnValue')
  local list={} for _,t in pairs(tables) do list[#list+1]=t end
  local function regCheck()
    local rq={} for i,t in ipairs(list) do rq[i]={obj=cST, func=fReg,
      set=function(b) writeInteger(b+oRegId,t.tid[1]) writeInteger(b+oRegId+4,t.tid[2]) end,
      get=function(b) return (rb(b+oRegR) or 0)~=0 end} end
    local r=DQX.callUFuncs(rq) for i,t in ipairs(list) do t.reg=r[i] end
  end
  regCheck()
  local need={} for _,t in ipairs(list) do if not t.reg then need[#need+1]=t end end
  if #need&gt;0 and fLoad then
    local oA, fA = UE.off(fLoad,'Asset')
    local asz=fA and readInteger(fA+0x3C) or 0
    if asz==0x28 then
      local rq={} for i,t in ipairs(need) do rq[i]={obj=cSL, func=fLoad,
        set=function(b) writeInteger(b+oA+0x10,t.tid[1]) writeInteger(b+oA+0x14,t.tid[2]) end} end
      DQX.callUFuncs(rq)
      regCheck()
    end
  end
  -- read the entries
  local fSrc=UE.func(kst,'GetTableEntrySourceString') or DQX.err('GetTableEntrySourceString not found.','GetTableEntrySourceStringが見つかりません。')
  local oSId, oSKey, oSR = UE.off(fSrc,'TableId'), UE.off(fSrc,'Key'), UE.off(fSrc,'ReturnValue')
  local rq, who = {}, {}
  for i,r in ipairs(ids) do if r and r.key then
    rq[#rq+1]={obj=cST, func=fSrc,
      set=function(b) writeInteger(b+oSId,r.tid[1]) writeInteger(b+oSId+4,r.tid[2]) writeBytes(b+oSKey, r.key) end,
      get=function(b) return UE.readFString(b+oSR) end}
    who[#rq]=pend[i][1] end end
  local res=DQX.callUFuncs(rq)
  local got, smp = 0, {}
  for i,s in ipairs(res) do if type(s)=='string' and s~='' and not s:find('[%z\1-\8\11\12\14-\31]') then
    who[i].name=s got=got+1 if #smp&lt;3 then smp[#smp+1]=s end end end
  return got
end
-- items without a loaded name text: learn the string-table key format (TableId + Key = prefix..ItemID..suffix)
-- from resolved items of the same ID group, then read the remaining names from those tables.
local function idGroup(id) return (id:gsub('_[^_]*$','')) end
function UE.stringTableByPattern(sample, items, progress)
  local ktl, kst = UE.needCls('KismetTextLibrary'), UE.needCls('KismetStringTableLibrary')
  local cTL, cST = UE.cdo('KismetTextLibrary'), UE.cdo('KismetStringTableLibrary')
  local fId=UE.func(ktl,'StringTableIdAndKeyFromText')
  local oT, oTid, oKey, oR = UE.off(fId,'Text'), UE.off(fId,'OutTableId'), UE.off(fId,'OutKey'), UE.off(fId,'ReturnValue')
  -- one sample per ID group
  local q, qs = {}, {}
  for _,sm in ipairs(sample) do
    local g=idGroup(sm.id)
    if not qs[g] then qs[g]=true
      q[#q+1]={obj=cTL, func=fId, sm=sm,
        set=function(b) writeBytes(b+oT, readBytes(sm.addr,0x18,true)) end,
        get=function(b) if (rb(b+oR) or 0)==0 then return false end return {tid={readInteger(b+oTid),readInteger(b+oTid+4)}, key=UE.readFString(b+oKey) or ''} end}
    end
  end
  local res=DQX.callUFuncs(q)
  local pats, byGroup, seenT = {}, {}, {}
  for i,r in ipairs(res) do
    if r and r.key then local sm=q[i].sm local a,b=r.key:find(sm.id,1,true)
      if a then
        local pt={tid=r.tid, pre=r.key:sub(1,a-1), suf=r.key:sub(b+1), tname=UE.nameStr(r.tid[1],r.tid[2])}
        byGroup[idGroup(sm.id)]=pt
        local k=pt.tname..'|'..pt.pre..'|'..pt.suf
        if not seenT[k] then seenT[k]=true pats[#pats+1]=pt end
      end
    end
  end
  if #pats==0 then return 0 end
  local fSrc=UE.func(kst,'GetTableEntrySourceString')
  local oSId, oSKey, oSR = UE.off(fSrc,'TableId'), UE.off(fSrc,'Key'), UE.off(fSrc,'ReturnValue')
  local function lookup(list, ptOf)
    local out={}
    for c0=1,#list,64 do
      local rq={}
      for k=0,math.min(63,#list-c0) do
        local idx=c0+k local it=list[idx] local pt=ptOf(it)
        local key=pt.pre..it.id..pt.suf
        local kb=DQX.base()+DQX.OFF_TIN+k*0x100
        local bytes={} for ch in key:sub(1,120):gmatch('.') do bytes[#bytes+1]=ch:byte() bytes[#bytes+1]=0 end bytes[#bytes+1]=0 bytes[#bytes+1]=0
        writeBytes(kb, bytes)
        local n=#bytes//2
        rq[#rq+1]={obj=cST, func=fSrc,
          set=function(b) writeInteger(b+oSId,pt.tid[1]) writeInteger(b+oSId+4,pt.tid[2])
            writeQword(b+oSKey,kb) writeInteger(b+oSKey+8,n) writeInteger(b+oSKey+12,n) end,
          get=function(b) local s=UE.readFString(b+oSR) if s and s~='' and not s:find('[%z\1-\8\11\12\14-\31]') then out[idx]=s end return true end}
      end
      DQX.callUFuncs(rq)
      if progress then progress(math.min(#list,c0+63),#list,'names') end
    end
    return out
  end
  local got=0
  -- 1) table of the same ID group
  local first={} for _,it in ipairs(items) do if byGroup[idGroup(it.id)] then first[#first+1]=it end end
  local r1=lookup(first, function(it) return byGroup[idGroup(it.id)] end)
  for i,it in ipairs(first) do if r1[i] then it.name=r1[i] got=got+1 end end
  -- 2) every known table for the rest
  for _,pt in ipairs(pats) do
    local rest={} for _,it in ipairs(items) do if it.name==it.id then rest[#rest+1]=it end end
    if #rest==0 then break end
    local r2=lookup(rest, function() return pt end)
    for i,it in ipairs(rest) do if r2[i] then it.name=r2[i] got=got+1 end end
  end
  return got
end

--=====================================================================================
--  [GAME] GAME DATA ACCESS
--=====================================================================================
local UE = DQX.UE
local hex = DQX.hex
local rb, s32 = UE.rb, UE.s32

-- live FHOLIMainInventoryData (UHOLIInventoryManager::MainInventoryData)
function DQX.invInst()
  local cls=UE.cls('HOLIInventoryManager'); if not cls then return nil end
  local i=DQX._invInst
  if i and readQword(i+0x10)==cls then return i end
  if DQX._invRetry and DQX.tickNo and DQX._invRetry&gt;DQX.tickNo then return nil end
  i=UE.instances(cls, 4)[1]
  DQX._invInst=i
  DQX._invRetry=(not i) and ((DQX.tickNo or 0)+10) or nil
  return i
end
function DQX.invBase()
  if not DQX.attached() then return nil end
  local i=DQX.invInst(); if not i then return nil end
  return i+UE.off(UE.cls('HOLIInventoryManager'),'MainInventoryData')
end
function DQX.needInv()
  local b=DQX.invBase()
  if not b then DQX.err('Inventory not found (load a save first).','所持品データが見つかりません（先にセーブデータをロードしてください）。') end
  return b
end
-- a TMap / TArray member of FHOLIMainInventoryData: returns address, layout, property
function DQX.invMember(name)
  local b=DQX.needInv()
  local st=UE.needStruct('HOLIMainInventoryData')
  local f=UE.needProp(st,name)
  local a=b+readInteger(f+0x4C)
  local L=nil
  if UE.fieldClass(f)=='MapProperty' then L=UE.mapLayout(f, 8, 20) end
  return a, L, f
end
-- CareerHistory UObject
function DQX.career()
  local cls=UE.cls('HOLICareerHistory'); local i=cls and UE.instances(cls,4)[1]
  if not i then DQX.err('Career history not found (load a save first).','冒険の記録データが見つかりません（先にセーブデータをロードしてください）。') end
  return i, cls
end

----------------------------------------------------------------- item master data
local CAT={ {'Weapon','ぶき'},{'Armor','ぼうぐ'},{'Accessory','アクセサリー'},{'Tool','どうぐ'},{'Fishing tool','つりどうぐ'},
            {'Cooking','りょうり'},{'Recipe book','レシピ帳'},{'Scout book','スカウトの書'},{'Material','そざい'},
            {'Fish','さかな'},{'Key item','だいじなもの'},{'Gesture book','しぐさ書'} }
local BAG={ [0]={'None','なし'},{'Use items','つかうもの'},{'Equipment bag','そうびぶくろ'},{'Key items','だいじなもの'},
            {'Mix bag','おまとめぶくろ'},{'Fish','さかな'} }
DQX.CAT, DQX.BAG = CAT, BAG
-- item master data. The inventory keys (e.g. ID_ITEM_USED_YAKUSOU) are searched at run time in every DataTable
-- (hash lookup); matching tables are used as item tables. Loaded UHOLIItem assets whose name is the key are used too.
local function firstTextProp(st)
  local best, any = nil, nil
  local s=st local g=0
  while s and s~=0 and g&lt;8 do
    local f=readQword(s+0x50) local k=0
    while f and f~=0 and k&lt;512 do
      if UE.fieldClass(f)=='TextProperty' then
        local n=(UE.fieldName(f) or ''):lower()
        if not any then any=f end
        if not best and n:find('name') and not n:find('ruby') then best=f end
      end
      f=readQword(f+0x20) k=k+1
    end
    s=readQword(s+0x40) g=g+1
  end
  local f=best or any
  return f and readInteger(f+0x4C) or nil
end
local DEFCAT={[1]=3,[2]=0,[3]=10,[4]=8,[5]=9}
-- Index the ANSI item FNames with the same block/entry rules as dqx_fnfind.
-- A failed block read leaves the original per-name lookup available.
function UE.indexItemNames(progress)
  local pool=UE.pool()
  local native=getAddressSafe('dqx_itemnames')
  local base=DQX.base()
  if native and base then
    if progress then progress(0,1,'itemIds') end
    local out=base+DQX.OFF_OUT
    local ok,count=pcall(UE.ex,native,pool,out,DQX.OUT_MAX)
    count=ok and count and (count &amp; 0xFFFFFFFF)
    if count and count&gt;0 and count&lt;=DQX.OUT_MAX then
      local bytes=readBytes(out,count*8,true)
      if bytes and #bytes==count*8 then
        local complete=true
        for i=1,#bytes,8 do
          local idx=bytes[i]|(bytes[i+1]&lt;&lt;8)|(bytes[i+2]&lt;&lt;16)|(bytes[i+3]&lt;&lt;24)
          local name=UE.nameStr(idx)
          if name then
            local key=name:lower()
            if not UE.idxCache[key] then UE.idxCache[key]=idx end
          else complete=false end
        end
        if progress then progress(1,1,'itemIds') end
        return complete
      end
    end
  end
  local last,cursor=readInteger(pool+8),readInteger(pool+12)
  if not last or last&lt;0 or last&gt;=8192 or not cursor or cursor&lt;0 or cursor&gt;0x20000 then return false end
  local complete=true
  if progress then progress(0,last+1,'itemIds') end
  for block=0,last do
    local base=readQword(pool+0x10+block*8)
    local size=block==last and cursor or 0x20000
    if size&gt;0 then
      local bytes=base and base~=0 and readBytes(base,size,true)
      if not bytes or #bytes~=size then
        complete=false
      else
        local i=1
        while i+1&lt;=size do
          local h=bytes[i]|(bytes[i+1]&lt;&lt;8)
          if h==0 then break end
          local length=h&gt;&gt;6
          local wide=(h&amp;1)~=0
          local payload=wide and length*2 or length
          if length==0 or i+1+payload&gt;size then
            if block==last then complete=false end
            break
          end
          -- Item IDs start with ID_ITEM. Check the bytes before constructing a Lua string.
          if not wide and length&gt;=7 and (bytes[i+2]|32)==105 and (bytes[i+3]|32)==100 and bytes[i+4]==95
             and (bytes[i+5]|32)==105 and (bytes[i+6]|32)==116 and (bytes[i+7]|32)==101 and (bytes[i+8]|32)==109 then
            local name=string.char(table.unpack(bytes,i+2,i+1+length))
            local index=(block&lt;&lt;16)|((i-1)&gt;&gt;1)
            local key=name:lower()
            if not UE.idxCache[key] then UE.idxCache[key]=index end
            UE.nameCache[index]=name
          end
          i=i+2+payload+(payload&amp;1)
        end
      end
    end
    if progress then progress(block+1,last+1,'itemIds') end
  end
  return complete
end

function DQX.itemDB(progress)
  if DQX._idb then return DQX._idb end
  DQX.require()
  local db={list={}, byKey={}, tables={}}
  local bst=UE.needStruct('HOLIItemBasicData')
  local oCat=UE.off(bst,'ItemCategory'); local oBag=UE.off(bst,'BagType')
  local pend={}
  local function report(done,total,phase)
    if progress then progress(done,total,phase) end
  end
  local function add(idx,num,basic,nameAddr,equip,defBag)
    local key=(idx &amp; 0xFFFFFFFF)..':'..num
    if db.byKey[key] then return end
    local cat, bag
    if basic then cat=rb(basic+oCat) or 255 bag=rb(basic+oBag) or 0
      if cat&gt;11 or bag&gt;5 then return end
    end
    local id=UE.nameStr(idx,num) or ('#'..idx)
    if not basic then
      if id:find('^ID_ITEM_EQUIP_') then bag=2 elseif id:find('^ID_ITEM_DAIJINAMONO') then bag=3 else bag=defBag or 0 end
      cat=DEFCAT[bag] or 255
      equip=(bag==2)
    end
    local it={idx=idx &amp; 0xFFFFFFFF, num=num, key=key, id=id, name=id, cat=cat, bag=bag, equip=equip or bag==2}
    db.byKey[key]=it db.list[#db.list+1]=it
    if nameAddr then pend[#pend+1]={it,nameAddr} end
  end
  -- 1) sample keys from the inventory
  local groups={}
  pcall(function()
    for _,g in ipairs({{'ItemList',1},{'SummaryList',4},{'KeyItemList',3}}) do
      local m,L=DQX.invMember(g[1]) local ks={}
      UE.mapIter(m,L,function(i,e) ks[#ks+1]={readInteger(e),readInteger(e+4)} if #ks&gt;=12 then return false end end)
      groups[#groups+1]={bag=g[2], keys=ks}
    end
    local ea=DQX.invMember('EquipList') local est=UE.needStruct('HOLIEquipData')
    local esz=readInteger(est+0x58) or 0x130 local oId=UE.off(est,'EquipID')
    local d=readQword(ea) or 0 local ks={}
    for i=0,math.min(readInteger(ea+8) or 0,12)-1 do local e=d+i*esz+oId ks[#ks+1]={readInteger(e),readInteger(e+4)} end
    groups[#groups+1]={bag=2, keys=ks}
  end)
  -- 2) loaded UHOLIItem (authoritative: name text + basic data) data assets (asset name = key)
  local base=UE.cls('HOLIItem')
  if base then
    local oName=UE.off(base,'ItemName')
    local offB={}
    local assets=UE.instances(base,DQX.OUT_MAX)
    local indexed=UE.indexItemNames(progress)
    report(0,#assets,'assets')
    for i,o in ipairs(assets) do
      local c=readQword(o+0x10)
      if offB[c]==nil then local f=UE.prop(c,'BasicData') offB[c]=f and readInteger(f+0x4C) or false end
      local nameIndex=readInteger(o+0x18)
      local an=UE.nameStr(nameIndex) or ''
      local key=an:match('^PA_(.+)$') or an
      local ki
      if key:sub(1,7)=='ID_ITEM' then
        ki=UE.idxCache[key:lower()] or (not indexed and UE.nameIdx(key)) or nil
      end
      if ki then
        local num=readInteger(o+0x1C)
        add(ki,num,offB[c] and (o+offB[c]) or nil,o+oName,false)
        local it=db.byKey[u32(ki)..':'..num]
        if it and it.equip then it.asset=o end
        db.nAsset=(db.nAsset or 0)+1
      else db.missing=(db.missing or 0)+1 end
      if i%32==0 or i==#assets then report(i,#assets,'assets') end
    end
  end
  -- 2) DataTables that contain those keys (+ tables with the same row struct, + the known item row structs)
  local dtc=UE.needCls('DataTable')
  local all=UE.instances(dtc, DQX.OUT_MAX)
  local rsBag={}
  for _,sn in ipairs({'HOLIItemCommonData','HOLIEquipCommonData'}) do local st=UE.struct(sn) if st then rsBag[st]=false end end
  report(0,#all,'tables')
  local rowTypes={}
  for i,dt in ipairs(all) do
    local rs=readQword(dt+0x28)
    rowTypes[i]=rs
    if rs and rs~=0 and not rsBag[rs] then
      local map=UE.nameMapState(dt+0x30)
      if map then
        for _,g in ipairs(groups) do
          local hit=false
          for _,k in ipairs(g.keys) do
            if UE.mapFindName(dt+0x30,UE.ROWL,k[1],k[2],map) then hit=true break end
          end
          if hit then rsBag[rs]=g.bag break end
        end
      end
    end
    if i%24==0 or i==#all then report(i,#all,'tables') end
  end
  local order={}
  for i,dt in ipairs(all) do
    local rs=rowTypes[i]
    if rs and rsBag[rs]~=nil then
      local oBasic=(rs==bst) and 0 or DQX.structPath(rs,bst,2)
      table.insert(order, oBasic and 1 or #order+1, {dt=dt, rs=rs, oBasic=oBasic})
    end
  end
  local equipCommon=UE.struct('HOLIEquipCommonData')
  report(0,#order,'rows')
  for i,x in ipairs(order) do
    local dt, rs, oBasic = x.dt, x.rs, x.oBasic
    local oName=firstTextProp(rs)
    do
      local rows=DQX.rows(dt)
      db.tables[#db.tables+1]=('%s (%s, %d rows)'):format(UE.objName(dt) or '?', UE.objName(rs) or '?', #rows)
      local isEq=(UE.objName(rs) or ''):find('Equip') and true or false
      for _,r in ipairs(rows) do
        add(r.idx, r.num, oBasic and (r.row+oBasic) or nil, oName and (r.row+oName) or nil, isEq or rsBag[rs]==2, rsBag[rs] or nil)
        if rs==equipCommon then
          local it=db.byKey[u32(r.idx)..':'..r.num]
          if it then it.equipRow=r.row it.equipTable=dt end
        end
      end
    end
    report(i,#order,'rows')
  end
  if #db.list==0 then DQX.err('Item data not found (load a save first).','アイテムのデータが見つかりません（先にセーブデータをロードしてください）。') end
  report(0,#pend,'names')
  for first=1,#pend,200 do
    local last=math.min(#pend,first+199)
    local addrs={}
    for i=first,last do addrs[#addrs+1]=pend[i][2] end
    local ok,names=pcall(UE.readFTexts,addrs)
    if ok then
      for i=first,last do
        local n=names[i-first+1]
        if n and not n:find('MISSING STRING TABLE',1,true) then pend[i][1].name=n end
      end
    end
    report(last,#pend,'names')
  end
  -- pass 2: string-table texts whose table is not loaded -&gt; load the table and read the entry source string
  local rest={} for _,p in ipairs(pend) do if p[1].name==p[1].id then rest[#rest+1]=p end end
  if #rest&gt;0 then
    report(0,#rest,'names')
    for first=1,#rest,64 do
      local last=math.min(#rest,first+63)
      local batch={}
      for i=first,last do batch[#batch+1]=rest[i] end
      pcall(UE.stringTableNames,batch)
      report(last,#rest,'names')
    end
  end
  do
    local miss, smp = {}, {}
    for _,it in ipairs(db.list) do if it.name==it.id and it.id:sub(1,7)=='ID_ITEM' then miss[#miss+1]=it end end
    for _,p in ipairs(pend) do if p[1].name~=p[1].id then smp[#smp+1]={id=p[1].id, addr=p[2]} end end
    if #miss&gt;0 and #smp&gt;0 then
      report(0,#miss,'names')
      pcall(UE.stringTableByPattern,smp,miss,progress)
      report(#miss,#miss,'names')
    end
  end
  -- items that have neither a data asset nor an entry in the game's item name tables do not exist in this game
  -- (leftover IDs only referenced by other tables): they are not offered for adding
  do local keep, drop = {}, {}
    for _,it in ipairs(db.list) do if it.name==it.id and it.id:sub(1,7)=='ID_ITEM' then drop[#drop+1]=it.id else keep[#keep+1]=it end end
    if #drop&gt;0 then db.list=keep end
  end
  for _,it in ipairs(db.list) do
    if it.name==it.id then
      local n=it.id:gsub('^ID_ITEM_EQUIP_[A-Z]+_',''):gsub('^ID_ITEM_[A-Z]+_',''):gsub('^ID_','')
      it.name=n:lower()
    end
  end
  table.sort(db.list, function(a,b) if a.cat~=b.cat then return a.cat&lt;b.cat end return a.id&lt;b.id end)
  DQX._idb=db
  return db
end
function DQX.itemNameByKey(idx,num)
  local db=DQX._idb or (pcall(DQX.itemDB) and DQX._idb)
  local it=db and db.byKey[(idx &amp; 0xFFFFFFFF)..':'..num]
  return it and it.name or UE.nameStr(idx,num) or ('#'..idx)
end

----------------------------------------------------------------- inventory monitor (items never decrease / multiplier)
function DQX.invSnapReset() DQX._snap={} end
function DQX.invMonitor()
  if DQX._monTick==DQX.tickNo then return end
  DQX._monTick=DQX.tickNo
  local keep=DQX.featOn.itemKeep or DQX.featOn.craftKeep
  local mult=1
  if DQX.featOn.itemMult then mult=readInteger(DQX.var(0x20)) or 1 if mult&lt;1 or mult&gt;100 then mult=1 end end
  if not DQX.invBase() then return end
  for _,bag in ipairs({'ItemList','SummaryList'}) do
    local m,L=DQX.invMember(bag)
    local cur={}
    UE.mapIter(m,L,function(i,e)
      local idx=readInteger(e) local num=readInteger(e+4)
      cur[idx..':'..num]={e=e, v=readInteger(e+L.vo) or 0, idx=idx, num=num}
    end)
    local snap=DQX._snap[bag]
    if snap then
      for k,c in pairs(cur) do
        local ov=snap[k] and snap[k].v or 0
        if c.v&gt;ov and mult&gt;1 then
          local nv=math.min(999, ov+(c.v-ov)*mult) writeInteger(c.e+L.vo, nv) c.v=nv
        elseif c.v&lt;ov and keep then
          writeInteger(c.e+L.vo, ov) c.v=ov
        end
      end
      if keep then
        for k,o in pairs(snap) do
          if not cur[k] and o.v&gt;0 then
            if pcall(UE.mapSetNameInt, m, L, o.idx, o.num, o.v) then cur[k]={v=o.v, idx=o.idx, num=o.num} end
          end
        end
      end
    end
    DQX._snap[bag]=cur
  end
end

----------------------------------------------------------------- data-table features
local function needRows(sn)
  local rows, st = DQX.allRows(sn)
  if #rows==0 then DQX.err('Data table not found: '..sn..' (load a save first).','データテーブルが見つかりません: '..sn..'（先にセーブデータをロードしてください）。') end
  return rows, st
end
-- enemies always drop: FHOLIMonsterTresure.NormalDropRate / RareDropRate in every table that contains it
function DQX.dropOn()
  local tst=UE.needStruct('HOLIMonsterTresure')
  local oN=UE.off(tst,'NormalDropRate'); local oR=UE.off(tst,'RareDropRate')
  local dtc=UE.needCls('DataTable')
  local hits={}
  for _,dt in ipairs(UE.instances(dtc,4096)) do
    local rs=readQword(dt+0x28)
    if rs and rs~=0 then
      local o=(rs==tst) and 0 or DQX.structPath(rs,tst,2)
      if o then hits[#hits+1]={dt=dt,o=o} end
    end
  end
  if #hits==0 then DQX.err('Monster drop data not found (load a save first).','モンスターのドロップデータが見つかりません（先にセーブデータをロードしてください）。') end
  -- scale detection: probability (&lt;=1), percent (&lt;=100) or "1/N" denominators (integers, mostly powers of two)
  local mx, n, ints, pow2 = 0, 0, 0, 0
  for _,h in ipairs(hits) do for _,r in ipairs(DQX.rows(h.dt)) do
    for _,o in ipairs({oN,oR}) do local v=readFloat(r.row+h.o+o) or 0
      if v&gt;0 then n=n+1 if v&gt;mx then mx=v end
        if v==math.floor(v) then ints=ints+1 local k=math.floor(v) if k&gt;=2 and (k &amp; (k-1))==0 then pow2=pow2+1 end end end end
  end end
  local target, mode
  if n&gt;0 and ints==n and pow2*2&gt;=n and mx&gt;1 then target, mode = 1.0, '1/N'
  elseif mx&lt;=1.0 then target, mode = 1.0, 'probability'
  else target, mode = 100.0, 'percent' end
  local cnt=0
  for _,h in ipairs(hits) do for _,r in ipairs(DQX.rows(h.dt)) do
    for _,o in ipairs({oN,oR}) do if DQX.patch('drop', r.row+h.o+o, 'f', target, h.dt) then cnt=cnt+1 end end
  end end
  DQX.msg(('Drop rates: %d values set to %g (detected format: %s, max %g).'):format(cnt,target,mode,mx),
          ('ドロップ率: %d件を %g に設定（判定した形式: %s、最大値 %g）。'):format(cnt,target,mode,mx))
end
-- monster list "seen through" (みやぶる) : records + MIYABURU_DISABLE flag of monster settings
function DQX.miyaburuOn()
  local rows, st = DQX.allRows('HOLIMonsterBattleSettingData')
  local cnt=0
  if #rows&gt;0 then
    local o=UE.off(st,'MonsterSettingFlag')
    for _,r in ipairs(rows) do
      local v=readInteger(r.row+o)
      if v and (v &amp; 2)~=0 then DQX.patch('miyaburu', r.row+o, 'i', v &amp; ~2 &amp; 0xFFFFFFFF, r.dt) cnt=cnt+1 end
    end
  end
  DQX.miyaburuTask(true)
  DQX.msg(('Miyaburu: %d monsters unblocked; monster records are marked while ON.'):format(cnt),
          ('みやぶる: %d体のみやぶる不可フラグを解除。ON中はモンスター記録をみやぶり済みにします。'):format(cnt))
end
function DQX.miyaburuTask(force)
  if not force and (DQX.tickNo % 7)~=0 then return end
  local ch, cls = DQX.career()
  local f=UE.needProp(cls,'ToubatuMonsterRecords')
  local L=UE.mapLayout(f, 4, 20)
  local rst=UE.needStruct('HOLIToubatuMonsterRecord')
  local bo, bm = UE.boolBit(UE.needProp(rst,'MiyaburuFlag'))
  local m=ch+readInteger(f+0x4C)
  UE.mapIter(m, L, function(i,e)
    local a=e+L.vo+bo
    local v=rb(a) or 0
    if (v &amp; bm)==0 then writeBytes(a, v | bm) end
  end)
end
function DQX.simplePatch(key, sn, fields, fn)
  local rows, st = needRows(sn)
  local offs={} for i,f in ipairs(fields) do offs[i]=UE.off(st,f) end
  local cnt=0
  for _,r in ipairs(rows) do DQX.patchOwner=r.dt if fn(r.row, offs) then cnt=cnt+1 end end
  DQX.patchOwner=nil
  return cnt
end
-- open locked chests without the key: BP_TresureBox_C.CheckOpenKey (Blueprint bytecode, decoded from the running game):
--   OpenKeyID = TresureBoxItemStruct.KEY_ID; return EventFlags.isHavingDaijinamono(OpenKeyID) (or the upgraded key of UpgradeKeyID).
-- The chest actor keeps its own copy of the row (TresureBoxItemStruct), so a DataTable write does not reach chests already placed,
-- and KEY_ID 0 still asks for key item 0. The function body after its first statement (+0x24) is replaced with
--   ReturnValue = true; return ReturnValue
-- (EX_LetBool 14 / EX_LocalOutVariable 48 / EX_True 27 / EX_Return 04 / EX_EndOfScript 53). isHavingDaijinamono itself is left
-- alone (quests use it). Re-applied when the chest class is reloaded.
local function nokeyPatch()
  local c=UE.cls('BP_TresureBox_C'); if not c then return nil end
  local fn=UE.func(c,'CheckOpenKey'); if not fn or not UE.alive(fn) then return nil end
  if DQX._nokeyFn==fn then return true end
  local d=readQword(fn+0x60) local n=readInteger(fn+0x68) or 0
  if not d or d==0 or n~=521 then return false end
  local b=readBytes(d,n,true) or {}
  -- +00 EX_Let (0F) ... ; +A9 14 48 &lt;ReturnValue&gt;; +1FE 04 48 &lt;ReturnValue&gt; 53
  if b[1]~=0x0F or b[0xA9+1]~=0x14 or b[0xAA+1]~=0x48 or b[0x1FE+1]~=0x04 or b[0x1FF+1]~=0x48 or b[n]~=0x53 then return false end
  local rv=readQword(d+0xAB)
  if not rv or rv==0 or readQword(d+0x200)~=rv then return false end
  local code={0x14,0x48} for i=0,7 do code[#code+1]=(rv&gt;&gt;(i*8))&amp;0xFF end
  code[#code+1]=0x27 code[#code+1]=0x04 code[#code+1]=0x48
  for i=0,7 do code[#code+1]=(rv&gt;&gt;(i*8))&amp;0xFF end
  code[#code+1]=0x53
  for i,v in ipairs(code) do DQX.patch('nokey', d+0x24+i-1, 'b', v, fn) end
  DQX._nokeyFn=fn
  return true
end

-- doors (BP_ManualDoorBase_C: BP_NormalDoor_C / BP_DoubleDoor_C, decoded from their ExecuteUbergraph):
--   if OpenKeyID ~= 0 and not EventFlags.isHavingDaijinamono(OpenKeyID) the door stays shut; OpenKeyID 0 = no key needed.
-- Each placed door keeps OpenKeyID on the actor, so it is set to 0 per instance (restored on OFF).
function DQX.nokeyDoorTask(force)
  if not force and (DQX.tickNo % 10)~=0 then return end
  local c=UE.cls('BP_ManualDoorBase_C')
  if not c then for _,k in ipairs({'bp_manualdoorbase_c','openkeyid'}) do if UE.idxCache[k]==false then UE.idxCache[k]=nil end end return 0 end
  local f=UE.prop(c,'OpenKeyID') if not f then return 0 end
  local off=readInteger(f+0x4C) local n=0
  for _,o in ipairs(UE.instances(c, 256)) do
    if UE.alive(o) then local v=readInteger(o+off) if v and v~=0 then DQX.patch('nokeyDoor', o+off, 'i', 0, o) n=n+1 end end
  end
  return n
end

-- key gates (map exits such as the Windmill Hill gate): BP_TeleportTriggerBase_C runs its Lua event (EventId, e.g.
-- "EvtTouzokunokagi") which checks the key item and shows "this door needs a special key". With an empty EventId the
-- trigger teleports directly (TeleportEvent). EventIds containing "kagi" are emptied (FString length 0, restored on OFF).
function DQX.nokeyGateTask(force)
  if not force and (DQX.tickNo % 10)~=5 then return end
  local c=UE.cls('BP_TeleportTriggerBase_C')
  if not c then for _,k in ipairs({'bp_teleporttriggerbase_c','eventid'}) do if UE.idxCache[k]==false then UE.idxCache[k]=nil end end return 0 end
  local f=UE.prop(c,'EventId') if not f then return 0 end
  local off=readInteger(f+0x4C) local n=0
  for _,o in ipairs(UE.instances(c, 512)) do
    if UE.alive(o) then
      local id=UE.readFString(o+off)
      if id and id:lower():find('kagi',1,true) then DQX.patch('nokeyGate', o+off+8, 'i', 0, o) n=n+1 end
    end
  end
  return n
end
function DQX.nokeyOn()
  DQX._nokeyFn=nil
  local r=nokeyPatch()
  if r==false then DQX.err('Chest key check code not recognized.','宝箱のカギ判定コードを特定できませんでした。') end
  DQX.msg(('Open without keys: chest key check %s; doors and key gates are opened as they load.'):format(r and 'disabled' or 'will be disabled when a map with chests is loaded'),
          ('カギなしで開けられる: 宝箱のカギ判定を%s。扉とカギの門は、読み込まれたものから順に開けられるようにします。'):format(r and '解除しました' or '宝箱のあるマップの読み込み時に解除します'))
end
function DQX.nokeyTask()
  if (DQX.tickNo % 10)~=0 then return end
  if DQX._nokeyFn and not UE.alive(DQX._nokeyFn) then DQX.restore('nokey', function() return false end) DQX._nokeyFn=nil end
  nokeyPatch()
end
function DQX.fishEasyOn()
  local n=DQX.simplePatch('fishEasy','HOLIFishData',{'FishStamina'}, function(row,o)
    local v=readFloat(row+o[1]) if v and v&gt;1 then return DQX.patch('fishEasy',row+o[1],'f',1.0) end end)
  DQX.msg(('Fishing: stamina of %d fish set to 1.'):format(n),('釣り: %d種の魚の体力を1にしました。'):format(n))
end
-- fish size (BP_Fishing_C.DecideFish, decoded): FishData = DT_FishData row (read at every bite).
--   size class = first i with RandomInteger(100) &lt; FishSizeTable[i] (subtracting as it goes); FishSizeTable is set when
--   fishing starts to [100 - big - king, big, king] (FishingParam rarities + skills). Size (x0.1 cm) by class:
--   0 normal: RandomIntegerInRange(SizeNormal, BigSizeMin - 1)   1 big: (BigSizeMin, KingSizeMin - 1)
--   2 king:   RandomIntegerInRange(KingSizeMin, FishSizeMax)       (the class roll is skipped when Big*King*Max &lt;= 0)
-- Maximum: class table [0, 0, 100] and KingSizeMin = FishSizeMax  -&gt; king size, FishSizeMax.
-- Minimum: class table [100, 0, 0] and BigSizeMin = SizeNormal + 1 -&gt; normal size, SizeNormal.
-- The table rows are re-applied every 10 ticks (DT_FishData can be reloaded); SizeMax is not used by DecideFish.
local function fishRows(key, useMax)
  local tl=DQX.tablesOf('HOLIFishData')
  local n=0
  for _,dt in ipairs(tl) do
    if UE.alive(dt) then
      local st=readQword(dt+0x28)
      local oN, oB, oK, oM = UE.off(st,'SizeNormal'), UE.off(st,'BigSizeMin'), UE.off(st,'KingSizeMin'), UE.off(st,'FishSizeMax')
      DQX.patchOwner=dt
      for _,r in ipairs(DQX.rows(dt)) do
        local sn, bg, kg, mx = readFloat(r.row+oN) or 0, readFloat(r.row+oB) or 0, readFloat(r.row+oK) or 0, readFloat(r.row+oM) or 0
        if useMax then
          if mx&gt;0 and kg~=mx then DQX.patch(key, r.row+oK, 'f', mx) end
          if mx&gt;0 then n=n+1 end
        else
          if sn&gt;0 and bg~=sn+1 then DQX.patch(key, r.row+oB, 'f', sn+1) end
          if sn&gt;0 then n=n+1 end
        end
      end
      DQX.patchOwner=nil
    end
  end
  return n
end
local function fishClassTable(useMax)
  local c=UE.cls('BP_Fishing_C')
  if not c then
    if (DQX.tickNo % 10)==0 then for _,k in ipairs({'bp_fishing_c','fishsizetable'}) do if UE.idxCache[k]==false then UE.idxCache[k]=nil end end end
    return 0
  end
  local f=UE.prop(c,'FishSizeTable') if not f then return 0 end
  local off=readInteger(f+0x4C) or 0
  local n=0
  for _,o in ipairs(UE.instances(c, 128)) do
    if UE.alive(o) then
      local d=readQword(o+off) or 0 local cnt=readInteger(o+off+8) or 0
      if d~=0 and cnt==3 then
        local want=useMax and {0,0,100} or {100,0,0}
        for i=0,2 do if readInteger(d+i*4)~=want[i+1] then writeInteger(d+i*4, want[i+1]) end end
        n=n+1
      end
    end
  end
  return n
end
function DQX.fishSizeTask(key, useMax, force)
  fishClassTable(useMax)
  if force or (DQX.tickNo % 10)==0 then return fishRows(key, useMax) end
end
function DQX.fishSizeOn(key, useMax)
  local other=(key=='fishMax') and 'fishMin' or 'fishMax'
  if DQX.featOn[other] then DQX.err('Turn off the other fish size option first.','もう一方の魚サイズ機能を先にOFFにしてください。') end
  local n=DQX.fishSizeTask(key, useMax, true) or 0
  DQX.msg(('Fish size: %d fish set to their %s size (%s size).'):format(n, useMax and 'maximum' or 'minimum', useMax and 'king' or 'normal'),
          ('魚サイズ: %d種を%sサイズにしました（%s）。'):format(n, useMax and '最大' or '最小', useMax and 'キングサイズ' or 'ノーマルサイズ'))
end
function DQX.noMatOn()
  local rows, st = needRows('HOLIRecipeContentData')
  local f=UE.needProp(st,'Materials'); local L=UE.mapLayout(f, 8, 20); local o=readInteger(f+0x4C)
  local cnt=0
  for _,r in ipairs(rows) do
    UE.mapIter(r.row+o, L, function(i,e)
      local v=readInteger(e+L.vo) if v and v~=0 then DQX.patch('noMat', e+L.vo, 'i', 0, r.dt) cnt=cnt+1 end
    end)
  end
  DQX.msg(('Recipes: %d material requirements set to 0.'):format(cnt),('レシピ: %d件の必要素材数を0にしました。'):format(cnt))
end
-- gold multiplier: x10 integer for the hook (0.1 steps)
function DQX.goldPrep()
  local m=readFloat(DQX.var(0x1C)) or 1
  local x10=math.floor(m*10+0.5) if x10&lt;0 then x10=0 end if x10&gt;10000 then x10=10000 end
  writeInteger(DQX.var(0x40), x10)
  return x10/10
end
function DQX.goldOn()
  DQX.needInv()
  writeInteger(DQX.var(0x44), UE.off(UE.needStruct('HOLIMainInventoryData'),'Gold'))
  local m=DQX.goldPrep()
  DQX.msg(('Gold x%.1f: applied to every gold gain reported to the adventure log.'):format(m),('金額 x%.1f: 冒険の記録に計上される全てのゴールド入手に適用します。'):format(m))
end
-- always critical: class pointers for the hook
function DQX.critOn()
  DQX._critCls=nil DQX._critCnt=nil
  local n=DQX.critTask(true)
  DQX.msg(('Always critical: ON (%d ally battle classes).'):format(n), ('いつもクリティカル: ON（味方の戦闘クラス %d 件）。'):format(n))
end
-- movement speed: player's CharacterMovementComponent -&gt; dqxVars+70, multiplier (0.1 steps) -&gt; dqxVars+28
function DQX.playerMovement()
  local pcc=UE.cls('PlayerController'); if not pcc then return nil end
  local oPawn=UE.off(pcc,'Pawn'); local oPl=UE.off(pcc,'Player')
  local chc=UE.cls('Character'); local cmcc=UE.cls('CharacterMovementComponent')
  if not chc or not cmcc then return nil end
  local oCm=UE.off(chc,'CharacterMovement')
  for _,pc in ipairs(UE.instances(pcc,8)) do
    local pl=readQword(pc+oPl); local pawn=readQword(pc+oPawn)
    if pl and pl~=0 and pawn and pawn~=0 and UE.isA(pawn,chc) then
      local cm=readQword(pawn+oCm)
      if cm and cm~=0 and UE.isA(cm,cmcc) then return cm, pawn end
    end
  end
end
function DQX.speedTask(force)
  local v=readFloat(DQX.var(0x24)) or 1
  local m=math.floor(v*10+0.5)/10 if m&lt;0.1 then m=0.1 end if m&gt;10 then m=10 end
  writeFloat(DQX.var(0x28), m)
  if force or (DQX.tickNo % 3)==0 then
    local cm=DQX.playerMovement()
    writeQword(DQX.var(0x70), cm or 0)
  end
  return m
end
function DQX.speedOn()
  local m=DQX.speedTask(true)
  if readQword(DQX.var(0x70))==0 then DQX.err('Player character not found (load a save first).','プレイヤーキャラクターが見つかりません（先にセーブデータをロードしてください）。') end
  DQX.msg(('Movement speed x%.1f.'):format(m),('移動スピード x%.1f。'):format(m))
end

-- sparkles (きらきら) can be picked again: REPOP_TIME = 0 and the pick records (KiraPopDataList) are expired
-- BP_Kirakira_C (Blueprint bytecode, decoded):
--   CheckIsValidItem = NOT bIsValidItem AND KiraItemData.ITEM_ID != None   (bIsValidItem=true means "no item")
--   CheckPop: REPOP_TIME &gt; 0 -&gt; pick records decide; REPOP_TIME == 0 -&gt; Pop = NOT bIsAccess (one-time sparkle)
--   after a pick bIsAccess stays true; the ResetAccess event clears it, re-enables the trigger box and the particle.
-- Repeatable: REPOP_TIME of every sparkle set to a tiny positive value, pick records emptied, and ResetAccess called on
-- every sparkle a few seconds after it was taken (hidden ones are also un-hidden).
DQX.KIRA_REPOP = 0.01
function DQX.kiraOn()
  local ok,n=pcall(DQX.simplePatch,'kira','HOLIKiraItemData',{'REPOP_TIME'}, function(row,o)
    local v=readFloat(row+o[1]) if v and math.abs(v-DQX.KIRA_REPOP)&gt;1e-6 then return DQX.patch('kira',row+o[1],'f',DQX.KIRA_REPOP) end end)
  DQX._kiraT=nil DQX._kiraR=nil
  pcall(DQX.kiraTask, true)
  local r=DQX.kiraRevive(true)
  DQX.msg(('Sparkles: ON. A taken sparkle can be taken again right away, on the spot (%d sparkles in this area).'):format(r),
          ('きらきら: ON。拾ったきらきらはすぐにその場で再び拾えます（このエリアのきらきら %d 個）。'):format(r))
end
function DQX.kiraTask(force, now)
  if not force and not now and (DQX.tickNo % 5)~=0 then return 0 end
  if force or not DQX._kiraT or (DQX.tickNo % 100)==0 then
    local S=UE.needStruct('SaveGameEnvironmentInfo')
    local oL=UE.off(S,'KiraPopDataList')
    local ps=UE.needStruct('HOLIKiraPopData')
    local t={ esz=readInteger(ps+0x58) or 0x28, oT=UE.off(ps,'Timespan') }
    for _,h in ipairs(DQX.findHolders(S,'SaveGameEnvironmentInfo')) do
      if h.kind==0 then for _,inst in ipairs(UE.instances(h.cls,16)) do t[#t+1]={inst=inst, cls=readQword(inst+0x10), a=inst+h.off+oL} end end
    end
    -- classes holding a TArray&lt;FHOLIKiraPopData&gt; directly (live environment data)
    for _,h in ipairs(DQX.findHolders(ps,'HOLIKiraPopData')) do
      if h.kind==1 then for _,inst in ipairs(UE.instances(h.cls,16)) do t[#t+1]={inst=inst, cls=readQword(inst+0x10), a=inst+h.off} end end
    end
    DQX._kiraT=t
  end
  local t=DQX._kiraT local cnt=0
  for _,x in ipairs(t) do
    if UE.alive(x.inst) and readQword(x.inst+0x10)==x.cls then
      local d=readQword(x.a); local n=readInteger(x.a+8) or 0
      if d and d~=0 and n&gt;0 and n&lt;100000 then
        for i=0,n-1 do local e=d+i*t.esz+t.oT if (readQword(e) or 0)~=0 then writeQword(e,0) cnt=cnt+1 end end
      end
    end
  end
  return cnt
end


-- Dolboard anywhere: BP_HOLIGameMode_C."Cannot Ride Doruboard" is copied from the map's PlayerStart on every map load and
-- is read by CanUseDoruboard / CanRideDoruboard / EnablePlayerAction. Keep it false (restored on OFF).
local function doruSetBit(a, bo, mask, on, owner)
  local v=readBytes(a+bo,1) or 0
  local want=on and (v | mask) or (v &amp; (~mask &amp; 0xFF))
  if want~=v then DQX.patchOwner=owner DQX.patch('doruAny', a+bo, 'b', want) DQX.patchOwner=nil return 1 end
  return 0
end
function DQX.doruTask(force)
  if not force and (DQX.tickNo % 3)~=0 then return end
  local c=DQX._doruCls
  if not c or not UE.alive(c) then c=UE.cls('BP_HOLIGameMode_C') DQX._doruCls=c DQX._doruF=nil DQX._doruM=nil end
  if not c then return 0 end
  local f=DQX._doruF or UE.prop(c,'Cannot Ride Doruboard') DQX._doruF=f
  local mi=UE.struct('HOLIMapInfo') local mf=mi and UE.prop(mi,'CanDollboard')
  -- GameMode members holding a HOLIMapInfo copy
  if not DQX._doruM then
    local l={} local s=c local g=0
    while mi and s and s~=0 and g&lt;64 do
      local p=readQword(s+0x50) local k=0
      while p and p~=0 and k&lt;4096 do
        if UE.fieldClass(p)=='StructProperty' and UE.innerStruct(p)==mi then l[#l+1]=readInteger(p+0x4C) end
        p=readQword(p+0x20) k=k+1
      end
      s=readQword(s+0x40) g=g+1
    end
    DQX._doruM=l
  end
  local n=0
  for _,gm in ipairs(UE.instances(c, 4)) do
    if UE.alive(gm) then
      if f then local bo,mask=UE.boolBit(f) doruSetBit(gm, bo, mask, false, gm) end
      if mf then local bo,mask=UE.boolBit(mf) for _,off in ipairs(DQX._doruM) do doruSetBit(gm+off, bo, mask, true, gm) end end
      n=n+1
    end
  end
  -- map info rows (DataTables of HOLIMapInfo)
  if mf and (force or (DQX.tickNo % 30)==0) then
    local bo,mask=UE.boolBit(mf)
    local ok, tl = pcall(DQX.tablesOf, 'HOLIMapInfo')
    for _,dt in ipairs(ok and tl or {}) do
      if UE.alive(dt) then for _,r in ipairs(DQX.rows(dt)) do doruSetBit(r.row, bo, mask, true, dt) end end
    end
  end
  return n
end
function DQX.doruOn()
  local c=UE.cls('BP_HOLIGameMode_C') or DQX.err('Class not found: BP_HOLIGameMode_C','クラスが見つかりません: BP_HOLIGameMode_C')
  if not UE.prop(c,'Cannot Ride Doruboard') then DQX.err('Property not found: Cannot Ride Doruboard','プロパティが見つかりません: Cannot Ride Doruboard') end
  DQX._doruCls=nil DQX._doruM=nil
  DQX.doruTask(true)
  local ok, tl = pcall(DQX.tablesOf, 'HOLIMapInfo')
  local rows=0 for _,dt in ipairs(ok and tl or {}) do rows=rows+#DQX.rows(dt) end
  DQX.msg(('Dolboard anywhere: map info %d rows, %d copies in the game mode.'):format(rows, #(DQX._doruM or {})),
          ('どこでもドルボード: マップ情報 %d 件、ゲームモード内の写し %d 件を乗車可にしました。'):format(rows, #(DQX._doruM or {})))
end


-- Rura stone anywhere: BPFL_Utility_C.DecideRuraResult returns 2 (cannot use) when GameInstance.RuraDisabled or the map's
-- IsRuraProhibition is set, 1 (fails indoors / in a rura space) otherwise unless Mega Rura is unlocked, 0 = can warp.
-- Its first statement is replaced by EX_Jump (06) to its own "ReturnValue = 0; goto return" statement at +0x15C.
local function ruraPatch()
  local c=UE.cls('BPFL_Utility_C'); if not c then return nil end
  local fn=UE.func(c,'DecideRuraResult'); if not fn or not UE.alive(fn) then return nil end
  if DQX._ruraFn==fn then return true end
  local d=readQword(fn+0x60) local n=readInteger(fn+0x68) or 0
  if not d or d==0 or n~=984 then return false end
  local b=readBytes(d,n,true) or {}
  local s=0x15C
  if b[1]~=0x45 or b[s+1]~=0x0F or b[s+9+1]~=0x48 or b[s+18+1]~=0x24 or b[s+19+1]~=0x00 or b[s+20+1]~=0x06 or b[0x3CD+1]~=0x04 or b[n]~=0x53 then return false end
  local rv=readQword(d+s+1)
  if not rv or rv==0 or readQword(d+s+10)~=rv or readQword(d+0x3CF)~=rv then return false end
  local code={0x06, s&amp;0xFF, (s&gt;&gt;8)&amp;0xFF, 0, 0}
  for i,v in ipairs(code) do DQX.patch('ruraAny', d+i-1, 'b', v, fn) end
  DQX._ruraFn=fn
  return true
end
function DQX.ruraAnyOn()
  DQX._ruraFn=nil
  local r=ruraPatch()
  if r==false then DQX.err('Rura check code not recognized.','ルーラの判定コードを特定できませんでした。') end
  if r==nil then DQX.err('Rura check function not found (load your save first).','ルーラの判定関数が見つかりません（セーブデータをロードしてから実行してください）。') end
  DQX.msg('Rura stone: usable anywhere.','ルーラストーン: どこでも使えるようにしました。')
end
function DQX.ruraAnyTask()
  if (DQX.tickNo % 10)~=0 then return end
  if DQX._ruraFn and not UE.alive(DQX._ruraFn) then DQX.restore('ruraAny', function() return false end) DQX._ruraFn=nil end
  ruraPatch()
end


-- fishing fight (BP_Fishing_C, decoded from DecideFish / Execute / ExecuteUbergraph):
--   BitePower (くいつき度) starts at InitialBitePower and is lowered by the fish's actions (Max(BitePower - n, 0 or 1));
--   FishDistance is lowered by each pull (FishDistance - Reduce Fish Distance) and the fish is caught when it reaches 0.
-- Easy fishing keeps BitePower at its highest value of the fight and FishDistance at 0.1 m or less, so the next pull lands it
-- (0.1, not 0: only a pull that brings the distance to 0 calls the game's catch routine), and forces critical pulls.

-- always critical pull: BP_Fishing_C.CalculateReduceFishDistance decides it at +0x203:
--   self.IsCriticalPower = &lt;local Less_IntInt(RandomInteger(1000), Min(CriticalRate, 500))&gt;   (14 01 &lt;prop&gt; 00 &lt;local&gt;)
-- The right-hand side (EX_LocalVariable + pointer, 9 bytes) becomes EX_True (27) followed by 8 EX_Nothing (0B).
local function fishCritPatch(c)
  local fn=UE.func(c,'CalculateReduceFishDistance'); if not fn or not UE.alive(fn) then return nil end
  if DQX._fishCritFn==fn then return true end
  local d=readQword(fn+0x60) local n=readInteger(fn+0x68) or 0
  local f=UE.prop(c,'IsCriticalPower')
  if not d or d==0 or n~=6845 or not f then return false end
  local b=readBytes(d+0x203, 0x13, true) or {}
  if b[1]~=0x14 or b[2]~=0x01 or readQword(d+0x205)~=f or b[11]~=0x00 or (readBytes(d+0x216,1) or 0)~=0x0F then return false end
  DQX.patch('fishEasy', d+0x20D, 'b', 0x27, fn)
  for i=1,8 do DQX.patch('fishEasy', d+0x20D+i, 'b', 0x0B, fn) end
  DQX._fishCritFn=fn
  return true
end

-- automatic fishing (BP_Fishing_C events, decoded from ExecuteUbergraph_BP_Fishing):
--   after casting, HitWaitingHandle holds the bite timer -&gt; HitFish() is that timer's event (clears it, shows "!" and
--   starts MatchingHandle); FishMatchingEnter() is the player's "press now" (clears MatchingHandle, opens the battle);
--   FishCommandEnter() takes the command under the cursor of BattleCommandMenu (WB_UI_System_FishingBattleCommand_C).
-- Each event is called once per timer handle / menu appearance, on the game thread.
DQX._fishLog = DQX._fishLog or {}
function DQX.fishLog(s) local l=os.date('%H:%M:%S')..' '..s table.insert(DQX._fishLog, l) if #DQX._fishLog&gt;200 then table.remove(DQX._fishLog,1) end
  if DQX.FISH_LOG_PRINT then print('[DQX fish] '..l) end end

-- fishing messages ("さかなが かかった！" etc.) wait for the OK button: GameMode.MessageWindowController
-- (BP_MessageWindowController_C) forwards ButtonOK to UI_MessageWindow.MenuEnter() (WB_MessageWindow_C, ubergraph +05B1:
-- finishes the text or, when waiting for a key, goes to the next page). Pressed every 2nd tick while the window plays,
-- never while a choice window (bWaitSelectWindow) is open.
function DQX.fishMsgStep(A)
  if A.msg==false then
    if (DQX.tickNo % 10)~=0 then return end
    for _,k in ipairs({'bp_holigamemode_c','bp_messagewindowcontroller_c','wb_messagewindow_c','messagewindowcontroller','ui_messagewindow','bplaying','bwaitselectwindow','menuenter'}) do
      if UE.idxCache[k]==false then UE.idxCache[k]=nil end end
    A.msg=nil
  end
  if not A.msg then
    local gc=UE.cls('BP_HOLIGameMode_C') local cc=UE.cls('BP_MessageWindowController_C') local wc=UE.cls('WB_MessageWindow_C')
    local M={gc=gc}
    M.oCtl=gc and UE.prop(gc,'MessageWindowController') M.oWin=cc and UE.prop(cc,'UI_MessageWindow')
    M.fPlay=wc and UE.prop(wc,'bPlaying') M.fSel=wc and UE.prop(wc,'bWaitSelectWindow') M.fEnter=wc and UE.func(wc,'MenuEnter')
    if not (M.oCtl and M.oWin and M.fPlay and M.fSel and M.fEnter) then
      A.msg=false
      if not A.msgMiss then A.msgMiss=true DQX.fishLog(('msg: not found gm=%s ctl=%s win=%s ctlProp=%s winProp=%s play=%s sel=%s enter=%s'):format(
        tostring(gc~=nil), tostring(cc~=nil), tostring(wc~=nil), tostring(M.oCtl~=nil), tostring(M.oWin~=nil), tostring(M.fPlay~=nil), tostring(M.fSel~=nil), tostring(M.fEnter~=nil))) end
      return
    end
    M.oCtl=readInteger(M.oCtl+0x4C) M.oWin=readInteger(M.oWin+0x4C) A.msg=M
  end
  local M=A.msg
  if not M.gm or not UE.alive(M.gm) then M.gm=UE.instances(M.gc,2)[1] end
  if not M.gm then return end
  local ctl=readQword(M.gm+M.oCtl) if not ctl or ctl==0 then return end
  local w=readQword(ctl+M.oWin) if not w or w==0 or not UE.alive(w) then return end
  local po,pm=UE.boolBit(M.fPlay) local so,sm=UE.boolBit(M.fSel)
  if ((readBytes(w+po,1) or 0)&amp;pm)==0 or ((readBytes(w+so,1) or 0)&amp;sm)~=0 then M.n=0 return end
  M.n=(M.n or 0)+1
  if M.n%2==0 then DQX.fishLog('MessageWindow.MenuEnter') DQX.callUFuncs({{obj=w, func=M.fEnter}}) end
end
function DQX.fishAutoStep(o, S)
  local c=S.cls
  if not S.auto then
    local A={}
    local function off(n) local f=UE.prop(c,n) return f and readInteger(f+0x4C) end
    A.hw, A.mh, A.menu, A.bs, A.fish = off('HitWaitingHandle'), off('MatchingHandle'), off('BattleCommandMenu'), UE.prop(c,'BattleStart'), UE.prop(c,'IsFishing')
    A.fHit, A.fMatch, A.fCmd = UE.func(c,'HitFish'), UE.func(c,'FishMatchingEnter'), UE.func(c,'FishCommandEnter')
    local uw=UE.cls('UserWidget') A.fVP=uw and UE.func(uw,'IsInViewport') A.vpRet=A.fVP and UE.off(A.fVP,'ReturnValue')
    if not (A.hw and A.mh and A.fHit and A.fMatch) then S.auto=false DQX.fishLog('auto: props/functions not found') return end
    A.done={} S.auto=A
  end
  local A=S.auto if not A then return end
  if A.fish then local fo,fm=UE.boolBit(A.fish) if ((readBytes(o+fo,1) or 0)&amp;fm)==0 then return end end   -- only the spot being fished
  local okm, em = pcall(DQX.fishMsgStep, A) if not okm and not A.msgErr then A.msgErr=true DQX.fishLog('msg error: '..tostring(em)) end
  local hw=readQword(o+A.hw) or 0 local mh=readQword(o+A.mh) or 0
  if hw~=0 and mh==0 then
    if not A.done['h'..hw] then A.done['h'..hw]=true DQX.fishLog('HitFish') DQX.callUFuncs({{obj=o, func=A.fHit}}) end
    return
  end
  if mh~=0 then
    if not A.done['m'..mh] then A.done['m'..mh]=true DQX.fishLog('FishMatchingEnter') DQX.callUFuncs({{obj=o, func=A.fMatch}}) end
    return
  end
  if not (A.menu and A.fCmd and A.fVP and A.bs) then return end
  local bo,bm=UE.boolBit(A.bs)
  if ((readBytes(o+bo,1) or 0)&amp;bm)==0 then A.menuSeen=0 return end
  local m=readQword(o+A.menu)
  if not m or m==0 or not UE.alive(m) then A.menuSeen=0 return end
  local vp=DQX.callUFuncs({{obj=m, func=A.fVP, get=function(p) return (readBytes(p+A.vpRet,1) or 0)~=0 end}})[1]
  if vp then
    A.menuSeen=(A.menuSeen or 0)+1
    if A.menuSeen==2 then DQX.fishLog('FishCommandEnter') DQX.callUFuncs({{obj=o, func=A.fCmd}}) end
  else A.menuSeen=0 end
end
function DQX.fishFightTask()
  local c=UE.cls('BP_Fishing_C')
  if not c then   -- the class name only exists after the fishing blueprint is loaded: retry the name lookup later
    if (DQX.tickNo % 10)==0 then for _,k in ipairs({'bp_fishing_c','bitepower','initialbitepower','fishdistance'}) do if UE.idxCache[k]==false then UE.idxCache[k]=nil end end end
    return
  end
  if DQX._fishCritFn and not UE.alive(DQX._fishCritFn) then DQX._fishCritFn=nil end
  if not DQX._fishCritFn and (DQX.tickNo % 10)==0 then
    local r=fishCritPatch(c)
    if r==false and not DQX._fishCritWarned then DQX._fishCritWarned=true
      DQX.msg('Easy fishing: critical pull code not recognized (critical not forced).','釣り簡単: かいしんの判定コードを特定できなかったため、かいしんは確定にしていません。') end
  end
  local S=DQX._fight
  if not S or S.cls~=c then
    local fB, fI, fD = UE.prop(c,'BitePower'), UE.prop(c,'InitialBitePower'), UE.prop(c,'FishDistance')
    if not (fB and fI and fD) then return end
    S={cls=c, oB=readInteger(fB+0x4C), oI=readInteger(fI+0x4C), oD=readInteger(fD+0x4C), best={}, wait={}} DQX._fight=S
    -- bite wait: RandomIntegerInRange(FishingParam.HitWaitSecondMin, HitWaitSecondMax) seconds after casting
    -- (ExecuteUbergraph +8282, K2_SetTimerDelegate). Kept at 1 s (0 would not start the timer at all).
    local fP=UE.prop(c,'FishingParam') local st=fP and UE.innerStruct(fP)
    if st then
      local f=readQword(st+0x50) local g=0
      while f and f~=0 and g&lt;256 do
        local nm=UE.fieldName(f) or ''
        if nm:find('^HitWaitSecondMin') or nm:find('^HitWaitSecondMax') then S.wait[#S.wait+1]=readInteger(fP+0x4C)+readInteger(f+0x4C) end
        f=readQword(f+0x20) g=g+1
      end
    end
  end
  if not S.list or (DQX.tickNo % 10)==0 then S.list=UE.instances(c, 128) end
  for _,o in ipairs(S.list) do
    if UE.alive(o) and readQword(o+0x10)==c then
      local ini=readInteger(o+S.oI) or 0 local bp=readInteger(o+S.oB) or 0
      local k=('%X:%d'):format(o, ini)
      local best=math.max(S.best[k] or 0, bp, ini)
      S.best[k]=best
      if ini&gt;0 and bp&lt;best then writeInteger(o+S.oB, best) end
      local d=readFloat(o+S.oD)
      if ini&gt;0 and d and d&gt;0.1 then writeFloat(o+S.oD, 0.1) end
      for _,a in ipairs(S.wait) do local v=readInteger(o+a) if v and v&gt;1 and v&lt;1000 then writeInteger(o+a, 1) end end
      local oka, ea = pcall(DQX.fishAutoStep, o, S) if not oka and not S.autoErr then S.autoErr=true DQX.fishLog('auto error: '..tostring(ea)) end
    end
  end
end


-- rare fish first: BP_Fishing_C.DecideFish picks a fish from FishingSpotData.FishIDs (ST_FishingSpotFish) by weighted
-- random over Probability (RandomInteger(sum) and subtracting, ExecuteUbergraph-free part +2716..+2C2C). The stars shown
-- in "ここにいるさかな" are 6 - DT_FishData.FishPondRarity / 100 (WB_UI_System_FishListWindow_C +03B5). Each entry's
-- Probabilities in DT_FishingSpot are set by star tier (fishRareTargets): rarer fish become more likely, nothing is guaranteed.
local function fishStars()
  local t=DQX._fishStars
  if t then return t end
  t={}
  local ok, rows, st = pcall(DQX.allRows, 'HOLIFishData')
  if ok and st and UE.prop(st,'FishPondRarity') then
    local o=UE.off(st,'FishPondRarity')
    for _,r in ipairs(rows) do
      local pr=readInteger(r.row+o) or 0
      local stars=(pr&gt;0) and (6-pr//100) or 1
      if stars&lt;1 then stars=1 elseif stars&gt;5 then stars=5 end
      t[UE.nameStr(r.idx,r.num) or '']=stars
    end
  end
  DQX._fishStars=t
  return t
end
DQX.fishStars=fishStars
-- [118-&gt;119] The element stride of a TArray&lt;UserDefinedStruct&gt; is the aligned size: UStruct.PropertiesSize (+0x58) is the
-- unaligned size (ST_FishingSpotFish: 0x24) and MinAlignment (+0x5C) rounds it up (0x28). Up to 118 the raw 0x24 was used,
-- so entries 1.. were read and written 4*i bytes off (the strings of the next entries).
function UE.structStride(s)
  local sz=readInteger(s+0x58) or 0 local al=readInteger(s+0x5C) or 8
  if not (al==1 or al==2 or al==4 or al==8 or al==16) then al=8 end
  return (sz+al-1)//al*al
end
-- the spot list layout: {ss=ST_FishingSpot, fIds=offset of FishIDs in it, es=ST_FishingSpotFish, esz=, oId=, oPr=}
function DQX.fishSpotLayout()
  local L=DQX._spotL if L then return L end
  local c=UE.cls('BP_Fishing_C') if not c then return nil end
  local fS=UE.prop(c,'FishingSpotData') local ss=fS and UE.innerStruct(fS)
  local fA=ss and readQword(ss+0x50) local fIds
  while fA and fA~=0 do if (UE.fieldName(fA) or ''):find('^FishIDs') then fIds=fA break end fA=readQword(fA+0x20) end
  local inner=fIds and readQword(fIds+0x78) local es=inner and UE.innerStruct(inner)
  if not (fS and fIds and es) then return nil end
  local oId, oPr
  local f=readQword(es+0x50)
  while f and f~=0 do local nm=UE.fieldName(f) or ''
    if nm:find('^FishID_') then oId=readInteger(f+0x4C) elseif nm:find('^Probability_') then oPr=readInteger(f+0x4C) end
    f=readQword(f+0x20) end
  if not (oId and oPr) then return nil end
  L={cls=c, fS=readInteger(fS+0x4C), ss=ss, fIds=readInteger(fIds+0x4C), es=es, esz=UE.structStride(es), oId=oId, oPr=oPr}
  DQX._spotL=L
  return L
end
-- entries of one FishIDs array, or nil when any entry does not look like a fish (then nothing is written)
function DQX.fishSpotEntries(a, L)
  local d=readQword(a) local cnt=readInteger(a+8) or 0
  if not d or d==0 or cnt&lt;=0 or cnt&gt;=64 then return nil end
  local r={}
  for i=0,cnt-1 do local e=d+i*L.esz
    local id=UE.readFString(e+L.oId) local v=readInteger(e+L.oPr)
    if not (id and id:find('^ITEM_') and v and v&gt;=0 and v&lt;10000000) then return nil end
    r[#r+1]={id=id, a=e+L.oPr, v=v}
  end
  return r
end
-- [119] rare first changes the source table DT_FishingSpot (the spot copies its row into FishingSpotData whenever it is
-- found by the line trace, +1468..+1601 in the ubergraph, and empties it again at +07CD/+1835: that copy is reallocated all
-- the time and must not be written). The table rows stay in place; OFF writes the original values back.
-- [120] weights by star tier instead of multiplying the spot's own values: in game (E2000_RIVER_1) the originals were
-- 1/64/30/1/2/2 for stars 1/2/1/3/2/3, so x3 per star still left the common 2-star fish at 75%. Now each tier with fish gets
-- the share RARE_TIER^(stars-1) (split evenly among its fish), so a higher tier is always RARE_TIER times as likely in total
-- as the tier below it; nothing is guaranteed. Entries with Probability 0 (never caught there) stay 0.
local RARE_TIER, RARE_UNIT = 4, 1000
function DQX.fishRareTargets(list, stars)
  local cnt={} for _,x in ipairs(list) do if x.o&gt;0 then local s=stars[x.id] or 1 cnt[s]=(cnt[s] or 0)+1 end end
  local t={}
  for i,x in ipairs(list) do
    if x.o&gt;0 then local s=stars[x.id] or 1 t[i]=math.max(1, (RARE_UNIT*RARE_TIER^(s-1))//cnt[s]) else t[i]=0 end
  end
  return t
end
function DQX.fishRareTask(force)
  if not force and (DQX.tickNo % 10)~=3 then return end
  local L=DQX.fishSpotLayout()
  if not L then for _,k in ipairs({'bp_fishing_c','fishingspotdata'}) do if UE.idxCache[k]==false then UE.idxCache[k]=nil end end return 0 end
  local R=DQX._rare if not R then R={rows={}} DQX._rare=R end
  local dtc=UE.cls('DataTable') if not dtc then return 0 end
  if not R.dts or (DQX.tickNo % 100)==3 then
    R.dts={} for _,dt in ipairs(UE.instances(dtc, 4096)) do if readQword(dt+0x28)==L.ss then R.dts[#R.dts+1]=dt end end
  end
  local stars=fishStars() local n=0
  for _,dt in ipairs(R.dts) do
    if UE.alive(dt) then
      for _,row in ipairs(DQX.rows(dt)) do
        local a=row.row+L.fIds
        local list=DQX.fishSpotEntries(a, L)
        if list then
          local key=readQword(a)
          local st=R.rows[key]
          local same=st and #st.o==#list
          if same then for i,x in ipairs(list) do if x.v~=st.o[i] and x.v~=st.t[i] then same=false break end end end
          if not same then
            for _,x in ipairs(list) do x.o=x.v end
            local o={} for i,x in ipairs(list) do o[i]=x.v end
            st={o=o, t=DQX.fishRareTargets(list, stars)} R.rows[key]=st
          end
          for i,x in ipairs(list) do if x.v~=st.t[i] then DQX.patch('fishRare', x.a, 'i', st.t[i], dt) end end
          n=n+1
        end
      end
    end
  end
  return n
end
function DQX.fishRareOn()
  DQX._rare=nil DQX._fishStars=nil
  local n=DQX.fishRareTask(true) or 0
  DQX.msg(('Rare fish first: %d fishing spots weighted (each star level is x4 as likely in total as the level below).'):format(n),
          ('レア度優先: %d か所の釣り場で、星が1つ多い魚ほど合計で4倍出やすくしました（釣り場の元データに反映し、OFFで元に戻します）。'):format(n))
end


-- catch a chosen fish: BP_Fishing_C.DecideFish has a developer override right after the normal choice (+062D..+07CD):
--   if not IsPackagedForDistribution() and GameMode.DebugFish ~= None then FishId = DebugFish; FishData = DT_FishData[FishId]
-- The packaged-build check (+065E: EX_PopExecutionFlowIfNot 4F, EX_LocalVariable 00 &lt;ptr&gt;) gets EX_True + 8 EX_Nothing,
-- and BP_HOLIGameMode_C.DebugFish (an FName, nothing allocated) is set to the chosen row: the spot's own list is never
-- touched. With DebugFish = None (OFF, or unset) the block does nothing. Both are restored on OFF.
local function fishDebugPatch()
  local c=UE.cls('BP_Fishing_C') if not c then return nil end
  local fn=UE.func(c,'DecideFish') if not fn or not UE.alive(fn) then return nil end
  if DQX._fishDbgFn==fn then return true end
  local d=readQword(fn+0x60) local n=readInteger(fn+0x68) or 0
  if not d or d==0 or n~=11472 then return false end
  local b=readBytes(d+0x641, 0x28, true) or {}
  if b[1]~=0x14 or b[2]~=0x00 or b[0x65E-0x641+1]~=0x4F or b[0x65F-0x641+1]~=0x00 or readQword(d+0x660)~=readQword(d+0x643)
     or (readBytes(d+0x668,1) or 0)~=0x19 then return false end
  DQX.patch('fishTarget', d+0x65F, 'b', 0x27, fn)
  for i=1,8 do DQX.patch('fishTarget', d+0x65F+i, 'b', 0x0B, fn) end
  DQX._fishDbgFn=fn
  return true
end
function DQX.fishTargetTask(force)
  if not force and (DQX.tickNo % 10)~=6 then return end
  local t=DQX._fishTarget if not t then return 0 end
  if DQX._fishDbgFn and not UE.alive(DQX._fishDbgFn) then DQX._fishDbgFn=nil end
  local r=fishDebugPatch()
  if r==nil then for _,k in ipairs({'bp_fishing_c','decidefish'}) do if UE.idxCache[k]==false then UE.idxCache[k]=nil end end end
  if r==false and not DQX._fishDbgWarned then DQX._fishDbgWarned=true
    DQX.msg('Chosen fish: fish decision code not recognized.','指定の魚: 魚を決める処理のコードを特定できませんでした。') end
  local gc=UE.cls('BP_HOLIGameMode_C') local f=gc and UE.prop(gc,'DebugFish')
  if not f then return 0 end
  local off=readInteger(f+0x4C) local n=0
  for _,gm in ipairs(UE.instances(gc, 4)) do
    if UE.alive(gm) then
      if readInteger(gm+off)~=t.idx or readInteger(gm+off+4)~=t.num then
        DQX.patch('fishTarget', gm+off, 'i', t.idx, gm) DQX.patch('fishTarget', gm+off+4, 'i', t.num, gm)
      end
      n=n+1
    end
  end
  return n, r
end
function DQX.fishTargetOn()
  local rows, st = DQX.allRows('HOLIFishData')
  local fT=UE.prop(st,'Text')
  local list, addrs = {}, {}
  for _,r in ipairs(rows) do
    local id=UE.nameStr(r.idx,r.num)
    if id then list[#list+1]={id=id, idx=r.idx, num=r.num} addrs[#addrs+1]=fT and (r.row+readInteger(fT+0x4C)) or 0 end
  end
  local ok, names = pcall(UE.readFTexts, addrs)
  for i,x in ipairs(list) do local nm=ok and names and names[i] x.name=(nm and nm~='') and (nm..'  ('..x.id:gsub('^ITEM_TURI_PRM_LIVE_FISH_','')..')') or x.id end
  table.sort(list, function(a,b) return a.id&lt;b.id end)
  if #list==0 then DQX.err('Fish data not found.','魚のデータが見つかりません。') end
  local names={} for i,x in ipairs(list) do names[i]=x.name end
  local i=DQX.pick(DQX.T('Fish to catch','釣る魚'), names)
  if not i then DQX.err('Cancelled.','キャンセルしました。') end
  DQX._fishTarget={id=list[i].id, idx=list[i].idx, num=list[i].num} DQX._fishDbgFn=nil DQX._fishDbgWarned=nil
  local n, r = DQX.fishTargetTask(true)
  DQX.msg(('Chosen fish: %s (%s). %s'):format(list[i].name, list[i].id, r and 'Set.' or 'It is set when the fishing code is loaded (go near a fishing spot).'),
          ('指定の魚: %s（%s）。%s'):format(list[i].name, list[i].id, r and '設定しました。' or '釣りの処理が読み込まれたときに設定します（釣り場に近づいてください）。'))
end

----------------------------------------------------------------- [GAME] features
DQX.F = {
  -- invincible / invincible2 share the damage hook and flag 00 (allies take no damage): only one of them at a time
  invincible = { pre=function() if DQX.featOn.invincible2 then DQX.err('Turn off "Invincible in battle 2" first.','先に「戦闘無敵2」をOFFにしてください。') end end,
                 hooks={'dmg'}, flag=0x00, on=function() DQX.invincibleOn() end, task=function() DQX.hpmpTask() end },
  invincible2= { pre=function() if DQX.featOn.invincible then DQX.err('Turn off "Invincible in battle" first.','先に「戦闘無敵」をOFFにしてください。') end end,
                 hooks={'dmg'}, flag=0x00, on=function() DQX.invincibleOn(DQX.INV2_FRAC) end, task=function() DQX.hpmpTask(false, DQX.INV2_FRAC) end },
  monMax     = { on=function() DQX.monMaxOn() end, task=function() DQX.monMaxTask() end, off=function() DQX.monMaxOff() end },
  tenseiMax  = { on=function() DQX.tenseiMaxOn() end, task=function() DQX.tenseiMaxTask() end, off=function() DQX.tenseiMaxOff() end },
  onehit     = { hooks={'dmg'}, flag=0x01 },
  itemKeep   = { on=function() DQX.needInv() DQX.invSnapReset() end, task=function() DQX.invMonitor() end },
  craftKeep  = { on=function() DQX.needInv() DQX.invSnapReset() end, task=function() DQX.invMonitor() end },
  itemMult   = { on=function() DQX.needInv() DQX.invSnapReset() end, task=function() DQX.invMonitor() end },
  crit       = { hooks={'kaisin','critAnd'}, flag=0x04, on=function() DQX.critOn() end, task=function() DQX.critTask() end, off=function() DQX.critOff() end },
  drop       = { on=function() DQX.dropOn() end, off=function() DQX.restore('drop') end },
  miyaburu   = { on=function() DQX.miyaburuOn() end, task=function() DQX.miyaburuTask() end, off=function() DQX.restore('miyaburu') end },
  doruAny    = { on=function() DQX.doruOn() end, task=function() DQX.doruTask() end, off=function() DQX.restore('doruAny') DQX._doruCls=nil DQX._doruF=nil DQX._doruM=nil end },
  ruraAny    = { on=function() DQX.ruraAnyOn() end, task=function() DQX.ruraAnyTask() end, off=function() DQX.restore('ruraAny') DQX._ruraFn=nil end },
  smithOne   = { on=function() DQX.smithOneOn() end, task=function() DQX.smithOneTask() end },
  nokey      = { on=function() DQX.nokeyOn() pcall(DQX.nokeyDoorTask, true) pcall(DQX.nokeyGateTask, true) end,
                 task=function() DQX.nokeyTask() DQX.nokeyDoorTask() DQX.nokeyGateTask() end,
                 off=function() DQX.restore('nokey') DQX.restore('nokeyDoor') DQX.restore('nokeyGate') DQX._nokeyFn=nil end },
  fishEasy   = { on=function() DQX.fishEasyOn() DQX._fight=nil DQX._fishCritFn=nil DQX._fishCritWarned=nil end, task=function() DQX.fishFightTask() end,
                 off=function() DQX.restore('fishEasy') DQX._fight=nil DQX._fishCritFn=nil end },
  fishTarget = { on=function() DQX.fishTargetOn() end, task=function() DQX.fishTargetTask() end, off=function() DQX.restore('fishTarget') DQX._fishTarget=nil DQX._fishDbgFn=nil end },
  fishRare   = { on=function() DQX.fishRareOn() end, task=function() DQX.fishRareTask() end, off=function() DQX.restore('fishRare') DQX._rare=nil end },
  fishMax    = { on=function() DQX.fishSizeOn('fishMax',true) end, task=function() DQX.fishSizeTask('fishMax',true) end, off=function() DQX.restore('fishMax') end },
  fishMin    = { on=function() DQX.fishSizeOn('fishMin',false) end, task=function() DQX.fishSizeTask('fishMin',false) end, off=function() DQX.restore('fishMin') end },
  steal      = { hooks={'kaisin'}, flag=0x05, on=function() DQX.stealOn() end, task=function() DQX.stealTask() end, off=function() DQX.restore('steal') DQX._stealFn=nil end },
  sht        = { on=function() DQX.shtOn() end, task=function() DQX.shtTask() end, off=function() DQX._sht=nil end },
  surprise   = { on=function() DQX.surpriseOn() end, task=function() DQX.surpriseTask() end, off=function() DQX.surpriseOff() end },
  kira       = { on=function() DQX.kiraOn() end, task=function() DQX.kiraTask() DQX.kiraRevive() end, off=function() DQX.restore('kira') DQX._kiraT=nil DQX._kiraR=nil end },
  noMat      = { on=function() DQX.noMatOn() end, off=function() DQX.restore('noMat') end },
  expMult    = { hooks={'expMul'}, flag=0x03 },
  goldMult   = { hooks={'goldMul'}, flag=0x02, on=function() DQX.goldOn() end, task=function() DQX.goldPrep() end },
  -- casino
  casinoCoin = { hooks={'coinSub','slotBet','rltBet','bingoBet'} },
  slot777    = { hooks={'slotRole'}, flag=0x06 },
  slotMetal  = { hooks={'slotMetal'} },
  slotMetalSpin = { hooks={'slotRole'}, flag=0x0C },
  rlt0       = { pre=function() if DQX.featOn.rlt1 then DQX.err('Turn off "Roulette: always 4" first.','先に「ルーレットで必ず 4」をOFFにしてください。') end end, hooks={'rltFix0'} },
  rlt1       = { pre=function() if DQX.featOn.rlt0 then DQX.err('Turn off "Roulette: always 0" first.','先に「ルーレットで必ず 0」をOFFにしてください。') end end, hooks={'rltFix1'} },
  bingoHammer= { on=function() DQX.bingoHammerTask() end, task=function() DQX.bingoHammerTask() end, off=function() DQX.restore('bingoHammer') end },
  bingoBall  = { hooks={'bingoBall'} },
  bingoMP    = { on=function() pcall(DQX.bingoMPTask) end, task=function() DQX.bingoMPTask() end },
  bingoLines = { on=function() if not DQX.resolveSig('getSquare') then DQX.err('Game code not found: getSquare','ゲームコードが見つかりません: getSquare') end end,
                 task=function() DQX.bingoLinesTask() end },
  bingoRebirth={ hooks={'bingoType'}, flag=0x0A, on=function() DQX._rebirthDT=nil DQX.bingoRebirthTask() end, task=function() DQX.bingoRebirthTask() end,
                 off=function() DQX.restore('bingoRebirth') DQX._rebirthDT=nil end },
  casinoTU   = { hooks={'slotTU','pokerTU'}, flag=0x0B },
  bounce     = { hooks={'bounce'}, flag=0x09, on=function() DQX.bounceTask(true) end, task=function() DQX.bounceTask() end },
  speed      = { hooks={'spd'}, on=function() DQX.speedOn() end, task=function() DQX.speedTask() end,
                 off=function() if DQX.attached() then writeQword(DQX.var(0x70),0) end end },
}


function DQX.kiraRevive(force)
  local R=DQX._kiraR
  if not R then
    local kc=UE.cls('BP_Kirakira_C') if not kc then return 0 end
    local fV=UE.prop(kc,'bIsValidItem'); local fA=UE.prop(kc,'bIsAccess'); local fH=UE.prop(kc,'bHidden')
    local fK=UE.prop(kc,'KiraItemData'); local fT=UE.prop(kc,'KiraKiraRepopTable')
    if not fV or not fA or not fK then return 0 end
    local ks=UE.needStruct('HOLIKiraItemData')
    local ac=UE.cls('Actor')
    R={kc=kc, oK=readInteger(fK+0x4C), oItem=UE.off(ks,'ITEM_ID'), oRep=UE.off(ks,'REPOP_TIME'), oT=fT and readInteger(fT+0x4C),
       fReset=UE.func(kc,'ResetAccess'), fHide=ac and UE.func(ac,'SetActorHiddenInGame'), since={}}
    R.vo,R.vm=UE.boolBit(fV) R.ao,R.am=UE.boolBit(fA) if fH then R.ho,R.hm=UE.boolBit(fH) end
    if R.fHide and UE.prop(R.fHide,'bNewHidden') then R.oHide=UE.off(R.fHide,'bNewHidden') end
    -- particle component: GetKiraItem deactivates it and the trigger box is re-enabled only while it is active
    local fPS=UE.prop(kc,'ParticleSystem'); local acomp=UE.cls('ActorComponent'); local fIA=acomp and UE.prop(acomp,'bIsActive')
    if fPS and fIA then R.oPS=readInteger(fPS+0x4C) R.iao,R.iam=UE.boolBit(fIA) end
    DQX._kiraR=R
  end
  if force or not R.acts or (DQX.tickNo % 10)==0 then
    local na=UE.instances(R.kc,256)
    -- the set of sparkles changed (area change / streaming): no game calls for a few seconds while actors settle
    local key=table.concat((function() local t={} for i,a in ipairs(na) do t[i]=('%X'):format(a) end table.sort(t) return t end)(),',')
    if R.akey and key~=R.akey then R.settle=((getTickCount and getTickCount()/1000) or os.time())+3 end
    R.akey=key R.acts=na
  end
  local acts={} for _,a in ipairs(R.acts) do if UE.alive(a) and UE.isA(a,R.kc) then acts[#acts+1]=a end end
  -- pick records: emptied (no record = never taken)
  if force or not DQX._kiraT or (DQX.tickNo % 30)==0 then pcall(DQX.kiraTask, force or false, true) end
  local function clearList(L) local c=readInteger(L+8) or 0 if c&gt;0 and c&lt;100000 then writeInteger(L+8,0) end end
  for _,x in ipairs(DQX._kiraT or {}) do if UE.alive(x.inst) and readQword(x.inst+0x10)==x.cls then clearList(x.a) end end
  local now=(getTickCount and getTickCount()/1000) or os.time()
  local q, n = {}, 0
  for _,a in ipairs(acts) do
    if R.oT then clearList(a+R.oT) end
    local k=a+R.oK
    if math.abs((readFloat(k+R.oRep) or 0)-DQX.KIRA_REPOP)&gt;1e-6 then writeFloat(k+R.oRep, DQX.KIRA_REPOP) end
    local item=readInteger(k+R.oItem) or 0
    local vb=rb(a+R.vo) or 0
    if item~=0 and (vb &amp; R.vm)~=0 then writeBytes(a+R.vo, vb &amp; (~R.vm &amp; 0xFF)) end   -- undo older builds (inverted flag)
    local access=((rb(a+R.ao) or 0) &amp; R.am)~=0
    local hidden=R.ho and ((rb(a+R.ho) or 0) &amp; R.hm)~=0
    local pc=R.oPS and readQword(a+R.oPS) or 0
    local inactive=pc~=0 and ((rb(pc+R.iao) or 0) &amp; R.iam)==0
    if (access or hidden or inactive) and item~=0 then
      local t=R.since[a]
      if not t then R.since[a]=now t=now end
      -- particle stopped = the item has already been given (GetKiraItem): reset at once; otherwise wait for the message
      R.cool=R.cool or {}
      if (R.cool[a] or 0)&gt;now then
      elseif inactive or now-t&gt;=1.0 then
        R.cool[a]=now+1.0
        if R.fReset then q[#q+1]={obj=a, func=R.fReset} end
        if hidden and R.fHide then q[#q+1]={obj=a, func=R.fHide, set=function(p) if R.oHide then writeBytes(p+R.oHide,0) end end} end
        R.since[a]=nil n=n+1
      end
    else R.since[a]=nil end
  end
  if #q&gt;0 and (force or not R.settle or now&gt;=R.settle) then
    local q2={} for _,c in ipairs(q) do if UE.alive(c.obj) then q2[#q2+1]=c end end
    if #q2&gt;0 then pcall(DQX.callUFuncs, q2) end
  end
  if force then return #acts end
  return n
end

-- always critical: ally battle classes for the kaisin hook (exact class pointers, zero-terminated list at dqxVars+80).
-- Known names first (cheap lookup); the full class scan runs only when none of them is loaded.
DQX.CRIT_ALLY = {'BP_BattlePlayer_C','BP_BattleFriend_C','BP_BattleGuest_C'}
function DQX.critAllyClasses(scan)
  local list, names = {}, {}
  for _,n in ipairs(DQX.CRIT_ALLY) do local c=UE.cls(n) if c then list[#list+1]=c names[#names+1]=n end end
  if #list==0 and scan then
    local bgc=UE.cls('BlueprintGeneratedClass')
    if bgc then for _,c in ipairs(UE.instances(bgc, DQX.OUT_MAX)) do
      local nm=UE.objName(c) or ''
      if (nm:find('BattlePlayer') or nm:find('BattleFriend') or nm:find('BattleGuest')) and #list&lt;14 then list[#list+1]=c names[#names+1]=nm end
    end end
  end
  for i=0,15 do writeQword(DQX.var(0x80+i*8), list[i+1] or 0) end
  return list, names
end
function DQX.critTask(force)
  if not force and (DQX.tickNo % 30)~=0 then return end
  local l=DQX.critAllyClasses(force)
  -- BP_CriticalFunction_C.bCheckCritical (FProperty*) for the multi-hit hook; the class exists while a battle is loaded
  local cf=UE.cls('BP_CriticalFunction_C')
  local p=cf and UE.prop(cf,'bCheckCritical')
  writeQword(DQX.var(0x38), p or 0)
  return #l
end
function DQX.critOff() end

-- always super high tension: BP_HOLIPlayerCharacter_C.IsSuperHighTension = (GetTension() &gt;= GetMaxTension()), and the damage /
-- healing scale (GEC_BaseExecution_C.ApplySuperHighTensionEffectScale) reads that. The Tension attribute (HOLIAttributeSet,
-- FGameplayAttributeData: BaseValue +8, CurrentValue +0xC) of every ally battle character is kept at MaxTension.
function DQX.shtChars(force)
  local S=DQX._sht
  if S and not force then
    -- stable list: re-scan every 30 ticks (9 s). Empty list or a destroyed character (battle start / end): every 7 ticks
    -- (about 2 s), so that the characters of a new battle are found before the first command input.
    local stale=(#S.list==0) for _,o in ipairs(S.list) do if not UE.alive(o) then stale=true break end end
    if (DQX.tickNo - S.t) &lt; (stale and 7 or 30) then return S end
  end
  local base=UE.needCls('HOLICharacterBase'); local asc=UE.needCls('HOLIAttributeSet')
  S={t=DQX.tickNo, list={}, oAS=UE.off(base,'AttributeSet'), oT=UE.off(asc,'Tension'), oM=UE.off(asc,'MaxTension')}
  for _,c in ipairs(DQX.critAllyClasses(force)) do
    for _,o in ipairs(UE.instances(c, 8)) do S.list[#S.list+1]=o end
  end
  DQX._sht=S
  return S
end
function DQX.shtTask(force)
  local S=DQX.shtChars(force)
  local n=0
  for _,o in ipairs(S.list) do
    if UE.alive(o) then
      local as=readQword(o+S.oAS)
      if as and as~=0 and UE.alive(as) then
        local m=readFloat(as+S.oM+0xC)
        if m and m&gt;0 and m&lt;100000 then
          n=n+1
          if (readFloat(as+S.oT+0xC) or 0)&lt;m then writeFloat(as+S.oT+8, m) writeFloat(as+S.oT+0xC, m) end
        end
      end
    end
  end
  return n
end
-- invincible: HP / MP of ally battle characters kept full (Health = GetMaxHealth(), Mana = GetMaxMana(); dead characters are left alone)
-- HP / MP: through the game's own setters (HOLICharacterBase.SetHealth / SetMana -&gt; AbilitySystemComponent
-- SetNumericAttributeBase), so the attribute change events (OnHealthChanged / OnManaChanged -&gt; EventDispatcher_*Changed,
-- read by the battle UI) fire. The game itself refills with SetHealth(GetMaxHealth()) / SetMana(GetMaxMana()).
-- A direct write of the attribute memory does not fire those events. Only living allies (HP &gt; 0) below maximum are refilled.
-- [invincible2] frac (0..1): HP is kept at floor(maximum HP * frac) (at least 1) instead of full; MP stays full.
-- Raised or lowered through the same SetHealth call, so the battle UI follows. Damage stays 0 through the dmg hook (flag 00).
DQX.INV2_FRAC = 0.25
function DQX.hpmpTask(force, frac)
  local S=DQX.shtChars(force)
  if not S.oH then local asc=UE.needCls('HOLIAttributeSet')
    S.oH, S.oP = UE.off(asc,'Health'), UE.off(asc,'Mana')
    local base=UE.needCls('HOLICharacterBase')
    S.fSetH, S.fSetM = UE.func(base,'SetHealth'), UE.func(base,'SetMana')
    if not (S.fSetH and S.fSetM) then
      DQX.err('HOLICharacterBase.SetHealth / SetMana not found.','HOLICharacterBase.SetHealth / SetMana が見つかりません。') end
    S.max={} S.mf={}
  end
  -- maximum HP / MP: BPI_HOLIPlayerCharacter GetMaxHealthValue(3) / GetMaxManaValue(3), the value the battle status UI divides
  -- by (UI_Status_C.GetHPBarPercent / GetMPBarPercent) and the status screen shows (includes equipment). The MaxHealth /
  -- MaxMana attributes and GetMaxHealth() / GetMaxMana() do not include equipment (log of 082: 494 / 277 against 582 / 335).
  -- Asked every 10 ticks (3 s) and for new characters.
  local function mfOf(cls)
    local m=S.mf[cls] if m~=nil then return m end
    m=false
    local fh, fm = UE.func(cls,'GetMaxHealthValue'), UE.func(cls,'GetMaxManaValue')
    local function lay(fn)
      local r, a
      local c=readQword(fn+0x50) local k=0
      while c and c~=0 and k&lt;16 do
        if UE.fieldName(c)=='ReturnValue' then r=readInteger(c+0x4C) elseif not a then a=readInteger(c+0x4C) end
        c=readQword(c+0x20) k=k+1
      end
      return r, a
    end
    if fh and fm then
      local rh, ah = lay(fh) local rm, am = lay(fm)
      if rh and ah and rm and am then m={fh=fh, rh=rh, ah=ah, fm=fm, rm=rm, am=am} end
    end
    S.mf[cls]=m return m
  end
  local ask={}
  for _,o in ipairs(S.list) do
    if UE.alive(o) and (force or not S.max[o] or (DQX.tickNo % 10)==0) then
      local m=mfOf(readQword(o+0x10))
      if m then
        ask[#ask+1]={obj=o, func=m.fh, set=function(p) writeBytes(p+m.ah, 3) end, get=function(p) return readFloat(p+m.rh) end}
        ask[#ask+1]={obj=o, func=m.fm, set=function(p) writeBytes(p+m.am, 3) end, get=function(p) return readFloat(p+m.rm) end}
      end
    end
  end
  if #ask&gt;0 then
    local r=DQX.callUFuncs(ask)
    for i=1,#ask,2 do S.max[ask[i].obj]={r[i], r[i+1]} end
  end
  local n, q = 0, {}
  for _,o in ipairs(S.list) do
    local m=S.max[o]
    if UE.alive(o) and m then
      local as=readQword(o+S.oAS)
      if as and as~=0 and UE.alive(as) then
        local h, mh = readFloat(as+S.oH+0xC), m[1]
        if h and mh and h&gt;0 and mh&gt;0 and mh&lt;1000000 then
          n=n+1
          local th=mh
          if frac then th=math.floor(mh*frac) if th&lt;1 then th=1 end end
          if (frac and math.abs(h-th)&gt;=0.5) or ((not frac) and h&lt;mh) then q[#q+1]={obj=o, func=S.fSetH, set=function(p) writeFloat(p, th) end} end
          local p, mp = readFloat(as+S.oP+0xC), m[2]
          if p and mp and mp&gt;0 and mp&lt;1000000 and p&lt;mp then q[#q+1]={obj=o, func=S.fSetM, set=function(pp) writeFloat(pp, mp) end} end
        end
      end
    end
  end
  if #q&gt;0 then pcall(DQX.callUFuncs, q) end
  -- battle status UI: the game's own full recovery (BP_HOLIPlayerCharacter_C ubergraph +17C05) calls
  -- SetHealth(GetMaxHealth()) / SetMana(GetMaxMana()) and then GetStatusUI().UpdateHealthAndMana(); SetHealth alone leaves the
  -- HP / MP gauges as they were. Refreshed after a refill, and every 10 ticks so that a gauge left stale is corrected.
  if #q&gt;0 or force or (DQX.tickNo % 10)==0 then pcall(DQX.hpmpUI, S) end
  return n
end
function DQX.invincibleOn(frac)
  DQX._sht=nil
  local n=DQX.hpmpTask(true, frac)
  local S=DQX._sht or {list={}}
  local parts={}
  for _,o in ipairs(S.list) do local m=S.max and S.max[o]
    local as=readQword(o+S.oAS) local h=as and as~=0 and readFloat(as+S.oH+0xC) local p=as and as~=0 and readFloat(as+S.oP+0xC)
    parts[#parts+1]=('HP %s/%s MP %s/%s'):format(tostring(h), tostring(m and m[1]), tostring(p), tostring(m and m[2])) end
  local en, ja = 'Invincible', '戦闘無敵'
  if frac then en, ja = 'Invincible 2 (HP kept at 1/4)', '戦闘無敵2（HPを4分の1で維持）' end
  DQX.msg(('%s: ON (%d ally battle characters now%s%s).'):format(en, n, #parts&gt;0 and ': ' or '', table.concat(parts,', ')),
          ('%s: ON（現在の味方の戦闘キャラクター %d 人%s%s）。'):format(ja, n, #parts&gt;0 and '：' or '', table.concat(parts,'、')))
end
function DQX.hpmpUI(S)
  S.ui=S.ui or {}
  -- the battle UI names may not be in the name pool yet when the table attaches in the field: forget a cached miss
  for _,k in ipairs({'getstatusui','updatehealthandmana','returnvalue'}) do if UE.idxCache[k]==false then UE.idxCache[k]=nil end end
  local need={}
  for _,o in ipairs(S.list) do
    local w=S.ui[o]
    if UE.alive(o) and not (w and UE.alive(w)) then
      local fn=UE.func(readQword(o+0x10),'GetStatusUI')
      local rp=fn and UE.prop(fn,'ReturnValue')
      if rp then local off=readInteger(rp+0x4C) or 0
        need[#need+1]={obj=o, func=fn, get=function(p) return readQword(p+off) end} end
    end
  end
  if #need&gt;0 then
    local r=DQX.callUFuncs(need)
    for i,rq in ipairs(need) do local w=r[i] if w and w~=0 and UE.alive(w) then S.ui[rq.obj]=w end end
  end
  local upd={}
  for _,o in ipairs(S.list) do
    local w=S.ui[o]
    if w and UE.alive(o) and UE.alive(w) then
      local fu=UE.func(readQword(w+0x10),'UpdateHealthAndMana')
      if fu then upd[#upd+1]={obj=w, func=fu} end
    end
  end
  if #upd&gt;0 then DQX.callUFuncs(upd) end
  return #upd
end
-- blacksmith: finish in one go. UHOLIBlacksmithMinigame (native; decoded from GetGaugeValue 140C468D0, GetGaugeTargetValue
-- 140C468B0, GetGaugeMaxValue 140C46850, GetGaugeTargetBounds 140C46870, GetCurrentResultTier 140C45DF0):
--   +0xA0 int32 value[8], +0xC0 count   /   +0xC8 int32 target[8], +0xE8 count   /   +0xF0 int32 max[8], +0x110 count
--   +0x298 tolerance (bounds = target -/+ tolerance)
-- The result tier sums |target - value| over the gauges with target &gt; 0 and gives the best tier (4) when every value is in
-- bounds and the sum is under the threshold. The feature keeps every value equal to its target while the minigame runs,
-- so "しあげる" can be chosen at once (also after a strike, which is undone at the next tick).
local SMG={vo=0xA0, vn=0xC0, to=0xC8, tn=0xE8, mo=0xF0, mn=0x110}
function DQX.smithOneTask()
  local c=UE.cls('HOLIBlacksmithMinigame') if not c then return 0 end
  local size=readInteger(c+0x58) or 0
  if size&lt;0x29C then return 0 end
  local n=0
  for _,o in ipairs(UE.instances(c, 4)) do
    if UE.alive(o) then
      local cnt=readInteger(o+SMG.vn) or -1
      if cnt&gt;=1 and cnt&lt;=8 and readInteger(o+SMG.tn)==cnt and readInteger(o+SMG.mn)==cnt then
        for i=0,cnt-1 do
          local t=readInteger(o+SMG.to+i*4) or 0 local mx=readInteger(o+SMG.mo+i*4) or 0
          if t&gt;0 and t&lt;=mx and mx&lt;1000000 and readInteger(o+SMG.vo+i*4)~=t then writeInteger(o+SMG.vo+i*4, t) end
        end
        n=n+1
      end
    end
  end
  return n
end
function DQX.smithOneOn()
  local n=DQX.smithOneTask()
  DQX.msg(('Blacksmith: finish in one go ON (%d minigames now; every gauge is kept at its target).'):format(n),
          ('鍛冶で一発で仕上げる: ON（現在の鍛冶 %d 件。すべてのゲージを目標値に保ちます）。'):format(n))
end
function DQX.shtOn()
  DQX._sht=nil
  local n=DQX.shtTask(true)
  DQX.msg(('Always super high tension: ON (%d ally battle characters now; applied in every battle).'):format(n),
          ('いつもスーパーハイテンション: ON（現在の味方の戦闘キャラクター %d 人。以後の戦闘でも維持します）。'):format(n))
end

-- always steal: every party member steals from each monster it defeats, whatever its job.
-- Every battle character already owns a BP_AutoStealFunction_C (created for all odd function classes); it only runs when
-- its "Try Activate Odd Function" (TryActivateOddFunctionInternal, SelfOddFunctionID = ODDFUNCTION_AUTO_NUSUMU) succeeds,
-- which normally requires the thief's passive. The kaisin hook forces that result for ally owners (flag 05).
-- BP_AutoStealFunction_C.DoneStealItem then rolls RandomBoolWithWeight(RareDropRate*0.25) / (NormalDropRate*0.25);
-- the two 0.25 float literals of its script (EX_FloatConst 1E 0000803E) are raised so that any rate &gt; 0 succeeds
-- (the game itself takes the normal item when both succeed). BPFL GetTotalAutoStolenInfo collects from all party members.
DQX.STEAL_K = 1000000.0
function DQX.stealPatch()
  local c=UE.cls('BP_AutoStealFunction_C'); if not c then return nil end
  local fn=UE.func(c,'DoneStealItem'); if not fn or not UE.alive(fn) then return nil end
  if DQX._stealFn==fn then return 2 end
  local d=readQword(fn+0x60) local n=readInteger(fn+0x68) or 0
  if not d or d==0 or n&lt;16 or n&gt;0x10000 then return nil end
  local b=readBytes(d,n,true) or {}
  local hits={}
  for i=1,#b-4 do if b[i]==0x1E and b[i+1]==0 and b[i+2]==0 and b[i+3]==0x80 and b[i+4]==0x3E then hits[#hits+1]=d+i end end
  if #hits~=2 then return -#hits end
  for _,a in ipairs(hits) do DQX.patch('steal', a, 'f', DQX.STEAL_K, fn) end
  DQX._stealFn=fn
  return 2
end
function DQX.stealOn()
  local v=UE.enumValue('EHOLIOddFunctionID','ODDFUNCTION_AUTO_NUSUMU')
  if not v or v&lt;=0 or v&gt;255 then DQX.err('ODDFUNCTION_AUTO_NUSUMU not found.','ODDFUNCTION_AUTO_NUSUMU が見つかりません。') end
  writeBytes(DQX.var(0x30), v)
  DQX._stealFn=nil
  local n=#DQX.critAllyClasses(true)
  local r=DQX.stealPatch()
  if r and r&lt;0 then DQX.err(('Steal roll code not recognized (%d literals).'):format(-r),('盗みの判定コードを特定できませんでした（定数 %d 件）。'):format(-r)) end
  DQX.msg(('Always steal: ON (%d ally battle classes, steal roll %s).'):format(n, r and 'patched' or 'patched when battle data is loaded'),
          ('いつも盗む: ON（味方の戦闘クラス %d 件、盗みの判定 %s）。'):format(n, r and '書き換え済み' or 'は戦闘データの読み込み時に書き換え'))
end
function DQX.stealTask()
  if (DQX.tickNo % 30)~=0 then return end
  if DQX._stealFn and not UE.alive(DQX._stealFn) then DQX.restore('steal', function() return false end) DQX._stealFn=nil end
  DQX.critAllyClasses(false)
  DQX.stealPatch()
end


-- enemies always surprised ("まものは おどろき とまどっている" = player first attack, EncountType 2).
-- BP_HOLIBattleManager_C.CheckFirstAttack (Blueprint bytecode, decoded):
--   bEventBattle -&gt; EncountType 1 (no first attack); Battle Encount Data.NoFightPreemptive -&gt; EncountType 0;
--   otherwise PlayerFirstStrikeRate = 100 when IsDebugPlayerFirstAttackFlag, EnemyFirstStrikeRate = 0 when the player flag
--   is set and IsDebugMonsterFirstAttackFlag is not; RandomBoolWithWeight(rate/100) -&gt; EncountType 2 (player first attack).
-- The feature keeps IsDebugPlayerFirstAttackFlag = true and IsDebugMonsterFirstAttackFlag = false on the battle manager.
function DQX.surpriseTask(force)
  local T=DQX._surT
  if not T then
    local c=UE.cls('BP_HOLIBattleManager_C') if not c then return 0 end
    local fP=UE.needProp(c,'IsDebugPlayerFirstAttackFlag'); local fM=UE.needProp(c,'IsDebugMonsterFirstAttackFlag')
    T={cls=c} T.po,T.pm=UE.boolBit(fP) T.mo,T.mm=UE.boolBit(fM)
    local fED=UE.prop(c,'Battle Encount Data'); local est=UE.struct('HOLIEncountBattleData')
    local fNF=est and UE.prop(est,'NoFightPreemptive')
    if fED and fNF then local o,m=UE.boolBit(fNF) T.nfo=readInteger(fED+0x4C)+o T.nfm=m end
    -- symbol monsters: MonsterData (FHOLIMonsterSymbolGeneratorData).NoFightPreemptive
    local sc=UE.cls('BP_HOLI_MonsterSymbol_C'); local gst=UE.struct('HOLIMonsterSymbolGeneratorData')
    local fMD=sc and UE.prop(sc,'MonsterData'); local fGN=gst and UE.prop(gst,'NoFightPreemptive')
    if fMD and fGN then local o,m=UE.boolBit(fGN) T.scls=sc T.sno=readInteger(fMD+0x4C)+o T.snm=m end
    DQX._surT=T
  end
  if force or not T.list or (DQX.tickNo % 20)==0 then
    T.list=UE.instances(T.cls,8)
    T.syms=T.scls and UE.instances(T.scls,512) or {}
  end
  local n, ns = 0, 0
  for _,m in ipairs(T.list) do
    if UE.alive(m) and UE.isA(m,T.cls) then
      local b=rb(m+T.po) or 0 if (b &amp; T.pm)==0 then writeBytes(m+T.po, b | T.pm) n=n+1 end
      local c=rb(m+T.mo) or 0 if (c &amp; T.mm)~=0 then writeBytes(m+T.mo, c &amp; (~T.mm &amp; 0xFF)) end
      if T.nfo then local d=rb(m+T.nfo) or 0 if (d &amp; T.nfm)~=0 then writeBytes(m+T.nfo, d &amp; (~T.nfm &amp; 0xFF)) end end
    end
  end
  for _,sy in ipairs(T.syms or {}) do
    if T.sno and UE.alive(sy) and UE.isA(sy,T.scls) then local d=rb(sy+T.sno) or 0 if (d &amp; T.snm)~=0 then writeBytes(sy+T.sno, d &amp; (~T.snm &amp; 0xFF)) ns=ns+1 end end
  end
  return #T.list
end
function DQX.surpriseOff()
  local T=DQX._surT
  if T and T.list then for _,m in ipairs(T.list) do if UE.alive(m) and UE.isA(m,T.cls) then local b=rb(m+T.po) or 0 writeBytes(m+T.po, b &amp; (~T.pm &amp; 0xFF)) end end end
  DQX.restore('surprise')
  DQX._surT=nil
end
function DQX.surpriseOn()
  DQX._surT=nil
  -- symbol generator data: NoFightPreemptive (first attack not allowed) cleared on every row (restored when switched off)
  local np=0
  pcall(function()
    local gst=UE.needStruct('HOLIMonsterSymbolGeneratorData'); local f=UE.needProp(gst,'NoFightPreemptive'); local o,m=UE.boolBit(f)
    for _,r in ipairs(DQX.allRows('HOLIMonsterSymbolGeneratorData')) do
      local v=rb(r.row+o) or 0 if (v &amp; m)~=0 then DQX.patch('surprise', r.row+o, 'b', v &amp; (~m &amp; 0xFF), r.dt) np=np+1 end
    end
  end)
  local n=DQX.surpriseTask(true)
  if n==0 then DQX.err('Battle manager not found (load a save first).','バトルマネージャーが見つかりません（先にセーブデータをロードしてください）。') end
  DQX.msg(('Always catch enemies off guard: ON (%d battle manager, %d no-first-attack settings removed).'):format(n, np),
          ('いつも不意をつく: ON（バトルマネージャー %d 件、先制攻撃不可の設定 %d 件を解除）。'):format(n, np))
end

-- maximum number of monsters in battle (symbol encounters).
-- BP_HOLI_MonsterSymbol_C.GetBattleAssignMonster (Blueprint bytecode, decoded from the dump of BP_HOLI_MonsterSymbol_C):
--   TempBtlMonsterNumMax = MonsterData.BtlMaxNum (FHOLIMonsterSymbolGeneratorData, the copy held by the symbol actor);
--   for each FHOLIMonsterSymbolGroup of MonsterData.BattleMonsterGroup: group 0, or RandomBoolWithWeight(BattlePopRate)
--   (UE4: Weight &gt;= FRand, so 1.0 = always) -&gt; the group takes part; RandomIntegerInRange(MonsterMin, MonsterMax) monsters
--   of MonsterId are added while TempMonsterTotal &lt; TempBtlMonsterNumMax.
-- The feature, on every spawned symbol (BP_HOLI_MonsterSymbol_C.MonsterData):
--   MonsterMin = MonsterMax, BattlePopRate 0 &lt; rate &lt; 1 -&gt; 1.0 (groups 1.. with rate 0, no MonsterId or MonsterMax 0 are left alone;
--   group 0 always takes part: the bytecode tests EqualEqual(ForEach index, 0) OR RandomBoolWithWeight(BattlePopRate));
--   when the sum of MonsterMax is below BtlMaxNum, the first valid group takes the rest.
--   =&gt; every symbol battle has BtlMaxNum monsters (the game's own limit of that symbol; it is never raised).
-- Not changed: metal / tensei lottery groups, event / boss battles, the DataTable rows. Original values are kept per actor and
-- written back when switched off (actors whose data was reset by the game are re-recorded).
function DQX.monMaxLayout()
  local L=DQX._monMax
  if L then return L end
  local sc=UE.cls('BP_HOLI_MonsterSymbol_C') if not sc then return nil end
  local gst=UE.needStruct('HOLIMonsterSymbolGeneratorData'); local grp=UE.needStruct('HOLIMonsterSymbolGroup')
  local fA=UE.needProp(gst,'BattleMonsterGroup')
  local inner=readQword(fA+0x78)
  if not inner or inner==0 or UE.innerStruct(inner)~=grp then
    DQX.err('Unexpected layout: HOLIMonsterSymbolGeneratorData.BattleMonsterGroup','想定外の構造です: HOLIMonsterSymbolGeneratorData.BattleMonsterGroup') end
  L={sc=sc, oMD=UE.off(sc,'MonsterData'), oArr=readInteger(fA+0x4C), oCap=UE.off(gst,'BtlMaxNum'),
     esz=UE.structStride(grp), oId=UE.off(grp,'MonsterId'), oMin=UE.off(grp,'MonsterMin'), oMx=UE.off(grp,'MonsterMax'),
     oRate=UE.off(grp,'BattlePopRate'), inst={}, list={}}
  if L.esz&lt;=0 or L.esz&gt;0x400 then DQX.err('Unexpected size: HOLIMonsterSymbolGroup','想定外のサイズです: HOLIMonsterSymbolGroup') end
  DQX._monMax=L
  return L
end
local function monRead(L, e) return readInteger(e+L.oMin), readInteger(e+L.oMx), readFloat(e+L.oRate) end
local function monSame(a, b) return a[1]==b[1] and a[2]==b[2] and math.abs((a[3] or 0)-(b[3] or 0))&lt;1e-6 end
-- one symbol actor: returns true when its data is (now) at maximum
function DQX.monMaxApply(L, o)
  local md=o+L.oMD
  local arr=readQword(md+L.oArr) local num=readInteger(md+L.oArr+8) or 0
  if not arr or arr==0 or num&lt;=0 or num&gt;64 then return false end
  local R=L.inst[o]
  -- current values; unchanged since our last write -&gt; nothing to do
  local cur={}
  for i=0,num-1 do local mn,mx,rt=monRead(L, arr+i*L.esz) if not (mn and mx and rt) then return false end cur[i]={mn,mx,rt} end
  if R and R.arr==arr and R.num==num then
    local same=true for i=0,num-1 do if not monSame(cur[i], R.want[i]) then same=false break end end
    if same then return true end
  end
  -- (re)record the original values and compute the maximum from them. Elements still holding our values keep their
  -- recorded originals (only the elements reset by the game are re-recorded).
  local keep = R and R.arr==arr and R.num==num
  local orig, want, sum, first = {}, {}, 0, nil
  for i=0,num-1 do
    local e=arr+i*L.esz
    local g = (keep and monSame(cur[i], R.want[i])) and R.orig[i] or cur[i]
    local mn,mx,rt = g[1], g[2], g[3]
    orig[i]={mn,mx,rt} want[i]={mn,mx,rt}
    local idLen=readInteger(e+L.oId+8) or 0
    -- group 0 always takes part (EqualEqual(ForEach index, 0) OR RandomBoolWithWeight); rate 0 = never for the others
    local valid = idLen&gt;1 and mx&gt;=1 and mx&lt;100 and mn&gt;=0 and mn&lt;=mx and rt&lt;100000 and (i==0 or rt&gt;0)
    if valid then
      want[i]={mx, mx, (rt&gt;0 and rt&lt;1.0) and 1.0 or rt}
      sum=sum+mx
      if not first then first=i end
    end
  end
  if not first then return false end
  local cap=readInteger(md+L.oCap) or 0
  if cap&gt;0 and cap&lt;=32 and sum&lt;cap then local w=want[first] w[1]=w[1]+(cap-sum) w[2]=w[1] end
  for i=0,num-1 do
    local e=arr+i*L.esz local w, c = want[i], cur[i]
    if w[1]~=c[1] then writeInteger(e+L.oMin, w[1]) end
    if w[2]~=c[2] then writeInteger(e+L.oMx, w[2]) end
    if math.abs(w[3]-c[3])&gt;=1e-6 then writeFloat(e+L.oRate, w[3]) end
  end
  L.inst[o]={arr=arr, num=num, orig=orig, want=want, cap=cap, sum=sum}
  return true
end
function DQX.monMaxTask(force)
  local L=DQX.monMaxLayout() if not L then return 0, 0 end
  if force or (DQX.tickNo % 3)==0 then
    L.list=UE.instances(L.sc, 512)
    for o in pairs(L.inst) do if not UE.alive(o) then L.inst[o]=nil end end
  end
  local n, all = 0, 0
  for _,o in ipairs(L.list) do
    if UE.alive(o) and UE.isA(o, L.sc) then
      all=all+1
      local ok,r=pcall(DQX.monMaxApply, L, o)
      if ok and r then n=n+1 end
    end
  end
  return n, all
end
function DQX.monMaxOn()
  DQX._monMax=nil
  local n, all = DQX.monMaxTask(true)
  if not DQX._monMax then
    DQX.msg('Maximum number of monsters: ON (no monster symbols loaded now; applied when they appear on a field map).',
            '戦闘時にモンスター数最大: ON（現在モンスターシンボルがありません。フィールドに出現したときに適用します）。')
    return
  end
  DQX.msg(('Maximum number of monsters: ON (%d of %d monster symbols set to their maximum).'):format(n, all),
          ('戦闘時にモンスター数最大: ON（モンスターシンボル %d / %d 体を最大数に設定）。'):format(n, all))
end
function DQX.monMaxOff()
  local L=DQX._monMax
  local n=0
  if L and DQX.attached() then
    for o,R in pairs(L.inst) do
      if UE.alive(o) and UE.isA(o, L.sc) then
        local md=o+L.oMD
        if readQword(md+L.oArr)==R.arr and readInteger(md+L.oArr+8)==R.num then
          for i=0,R.num-1 do
            local e=R.arr+i*L.esz local w, g = R.want[i], R.orig[i]
            local mn,mx,rt = monRead(L, e)
            -- only values that are still ours are written back
            if mn==w[1] and mx==w[2] and rt and math.abs(rt-w[3])&lt;1e-6 then
              writeInteger(e+L.oMin, g[1]) writeInteger(e+L.oMx, g[2]) writeFloat(e+L.oRate, g[3])
            end
          end
          n=n+1
        end
      end
    end
  end
  DQX._monMax=nil
  return n
end

-- reincarnated (tensei) monster lottery always wins (symbol encounters).
-- BP_HOLI_MonsterSymbol_C.GetTenseiMonster(rate = TenseiMonsterGroup[i].BattlePopRate, IsSecondMonster) (bytecode, decoded):
--   IsSecondMonster -&gt; Num 1; otherwise Num = RandomBoolWithWeight(rate * SkillTenseiRate * DebugTenseiRate / 100) ? 1 : 0.
--   LotteryTensei loops MonsterData.TenseiMonsterGroup and puts the tensei MonsterId into the battle list when Num &gt; 0.
--   SkillTenseiRate = BPI_HOLIPartyDataManager.GetTotalEncountReincarnationRate (system rate * encounter rate), written by
--   GetSkillRate. DebugTenseiRate is a plain float of the symbol actor, read only there.
-- The feature keeps DebugTenseiRate = TENSEI_DEBUG on every spawned symbol, so the weight is &gt;= 1 (always wins) whenever
-- the group rate and SkillTenseiRate are above 0. Groups with rate 0 (no tensei there) are not changed. The original value
-- is kept per actor and written back when switched off.
DQX.TENSEI_DEBUG = 1000000.0
function DQX.tenseiMaxLayout()
  local T=DQX._tensei
  if T then return T end
  local sc=UE.cls('BP_HOLI_MonsterSymbol_C') if not sc then return nil end
  T={sc=sc, oD=UE.off(sc,'DebugTenseiRate'), oS=UE.off(sc,'SkillTenseiRate'), inst={}, list={}}
  local gst=UE.struct('HOLIMonsterSymbolGeneratorData'); local fMD=UE.prop(sc,'MonsterData')
  local fG=gst and UE.prop(gst,'TenseiMonsterGroup')
  if fMD and fG then T.oG=readInteger(fMD+0x4C)+readInteger(fG+0x4C) end
  DQX._tensei=T
  return T
end
function DQX.tenseiMaxTask(force)
  local T=DQX.tenseiMaxLayout() if not T then return 0, 0 end
  if force or (DQX.tickNo % 3)==0 then
    T.list=UE.instances(T.sc, 512)
    for o in pairs(T.inst) do if not UE.alive(o) then T.inst[o]=nil end end
  end
  local n, g = 0, 0
  for _,o in ipairs(T.list) do
    if UE.alive(o) and UE.isA(o, T.sc) then
      local v=readFloat(o+T.oD)
      if v then
        if v~=DQX.TENSEI_DEBUG then
          -- not ours (new actor, or reset by the game): record it as the original
          T.inst[o]=v writeFloat(o+T.oD, DQX.TENSEI_DEBUG)
        elseif T.inst[o]==nil then T.inst[o]=1.0 end
        n=n+1
        if T.oG and (readInteger(o+T.oG+8) or 0)&gt;0 then g=g+1 end
      end
    end
  end
  return n, g
end
function DQX.tenseiMaxOn()
  DQX._tensei=nil
  local n, g = DQX.tenseiMaxTask(true)
  local T=DQX._tensei
  if not T then
    DQX.msg('Tensei lottery always wins: ON (no monster symbols loaded now; applied when they appear on a field map).',
            '転生モンスターの抽選を最大: ON（現在モンスターシンボルがありません。フィールドに出現したときに適用します）。')
    return
  end
  local sk
  for _,o in ipairs(T.list) do if UE.alive(o) then sk=readFloat(o+T.oS) break end end
  DQX.msg(('Tensei lottery always wins: ON (%d monster symbols, %d of them with tensei groups; SkillTenseiRate %s).'):format(n, g, tostring(sk)),
          ('転生モンスターの抽選を最大: ON（モンスターシンボル %d 体、うち転生グループあり %d 体、SkillTenseiRate %s）。'):format(n, g, tostring(sk)))
end
function DQX.tenseiMaxOff()
  local T=DQX._tensei
  local n=0
  if T and DQX.attached() then
    for o,v in pairs(T.inst) do
      if UE.alive(o) and UE.isA(o, T.sc) and readFloat(o+T.oD)==DQX.TENSEI_DEBUG then writeFloat(o+T.oD, v) n=n+1 end
    end
  end
  DQX._tensei=nil
  return n
end


--=====================================================================================
--  [GAME] ACTIONS / LISTS
--=====================================================================================
local UE = DQX.UE
local hex = DQX.hex
local rb, s32 = UE.rb, UE.s32
local function u32key(i,n) return ((i or 0) &amp; 0xFFFFFFFF)..':'..(n or 0) end
local function confirm(en, ja)
  return messageDialog(DQX.T(en,ja), mtConfirmation, mbYes, mbNo)==mrYes
end

----------------------------------------------------------------- 2. User : money &amp; coins
local MONEY={ {'Gold','Gold','所持金（ゴールド）'}, {'BankedGold','Banked gold','銀行の預金'}, {'CasinoCoin','Casino coins','カジノコイン'},
              {'FishCoin','Fish coins','おさかなコイン'}, {'TinyMedals','Mini medals','ちいさなメダル'} }
function DQX.refreshMoney(mr)
  DQX.require()
  DQX.clearChildren(mr)
  DQX.needInv()
  local st=UE.needStruct('HOLIMainInventoryData')
  for _,m in ipairs(MONEY) do
    local f=UE.prop(st,m[1])
    if f then DQX.addRec(mr, m[2], m[3], ('[dqxPtrs+0]+%X'):format(readInteger(f+0x4C)), vtDword) end
  end
end

----------------------------------------------------------------- 2. User : party characters
local function classIsA(c, target)
  if not c or not target then return false end
  local d=s32(readInteger(target+0x38)); local cd=s32(readInteger(c+0x38))
  if not d or not cd or d&lt;0 or cd&lt;d then return false end
  local ch=readQword(c+0x30); return ch and ch~=0 and readQword(ch+d*8)==target+0x30
end
-- classes that have a property of struct type S (kind 0) or TArray&lt;S&gt; (kind 1)  (native scan)
function DQX.findHolders(S, sn)
  local sg=UE.needCls('HOLISaveGame')
  local fMain=UE.needProp(sg,'MainCharacterData'); local fFr=UE.needProp(sg,'FriendCharacterDataArray')
  local structFC=readQword(fMain+8); local arrayFC=readQword(fFr+8)
  local classCls=UE.find('Class','Class',2)[1] or DQX.err('UClass not found.','UClassが見つかりません。')
  local p=DQX.base()+DQX.OFF_PARAM
  writeQword(p,UE.objs()) writeQword(p+8,classCls) writeQword(p+0x10,S) writeQword(p+0x18,structFC)
  writeQword(p+0x20,arrayFC) writeQword(p+0x28,DQX.base()+DQX.OFF_OUT) writeInteger(p+0x30,1024)
  local n=(UE.ex(getAddress('dqx_holders'), p) or 0) &amp; 0xFFFFFFFF
  local res={}
  for i=0,n-1 do
    local o=DQX.base()+DQX.OFF_OUT+i*16
    res[#res+1]={cls=readQword(o), off=readInteger(o+8), kind=readInteger(o+12), sn=sn, S=S}
  end
  return res, sg
end
function DQX.charHolders()
  local res, sg = {}, nil
  for _,sn in ipairs({'HOLIMainCharacterData','HOLIFriendCharacterData'}) do
    local r; r, sg = DQX.findHolders(UE.needStruct(sn), sn)
    for _,x in ipairs(r) do res[#res+1]=x end
  end
  return res, sg
end
-- characters: { {name=, share=, fishing=, main=, src=} }
function DQX.partyChars()
  DQX.require()
  local holders, sg = DQX.charHolders()
  local live, save = {}, {}
  for _,h in ipairs(holders) do
    if classIsA(h.cls, sg) then save[#save+1]=h else live[#live+1]=h end
  end
  for _,h in ipairs(live) do h.pname=UE.propNameAt(h.cls, h.off) or '?' end
  local chosen, insts = live, {}
  local srcNote='live'
  for _,h in ipairs(live) do insts[h]=UE.instances(h.cls, 8) end
  local any=false for _,h in ipairs(live) do if #insts[h]&gt;0 then any=true end end
  -- other managers also hold HOLIMainCharacterData (e.g. BP_HOLIBattleDataManager_C.MainCharacterData, name empty).
  -- When the party data manager is present, only its data is listed.
  DQX._partyIgnored={}
  local party={} for _,h in ipairs(live) do if (UE.objName(h.cls) or ''):find('PartyDataManager',1,true) and #insts[h]&gt;0 then party[#party+1]=h end end
  if #party&gt;0 then
    for _,h in ipairs(live) do if #insts[h]&gt;0 and not (UE.objName(h.cls) or ''):find('PartyDataManager',1,true) then
      DQX._partyIgnored[#DQX._partyIgnored+1]=(UE.objName(h.cls) or '?')..'.'..h.pname end end
    chosen=party
  end
  if not any then
    chosen, srcNote = save, 'save'
    local mc=UE.cls('HOLISaveGameManager'); local mgr=mc and UE.instances(mc,4)[1]
    local cur=mgr and readQword(mgr+UE.off(mc,'CurrentSaveGame'))
    for _,h in ipairs(save) do insts[h]= (cur and cur~=0) and {cur} or UE.instances(h.cls,2) end
  end
  local shareSt=UE.needStruct('HOLICharacterShareData')
  local oNameF=UE.off(shareSt,'CharaName')
  local out, seen = {}, {}
  for _,h in ipairs(chosen) do
    for _,inst in ipairs(insts[h] or {}) do
      local base=inst+h.off
      local oBase=UE.off(h.S,'BaseData')
      local function addChar(elem, main)
        local share=elem+oBase
        if seen[share] then return end
        seen[share]=true
        local c={share=share, main=main, src=(UE.objName(h.cls) or '?')..'.'..(h.pname or UE.propNameAt(h.cls,h.off) or '?')..' ('..srcNote..')'}
        c.name=UE.readFText(share+oNameF) or (main and 'Main character' or 'Friend')
        if main then local fp=UE.prop(h.S,'FishingData') if fp then c.fishing=elem+readInteger(fp+0x4C) end end
        out[#out+1]=c
      end
      if h.kind==1 then
        local data=readQword(base); local num=readInteger(base+8) or 0
        local esz=readInteger(h.S+0x58) or 0
        if data and data~=0 and num&gt;0 and num&lt;64 and esz&gt;0 and esz&lt;0x4000 then
          for i=0,num-1 do addChar(data+i*esz, false) end
        end
      else
        addChar(base, h.sn=='HOLIMainCharacterData')
      end
    end
  end
  if #out==0 then DQX.err('Party data not found (load a save first).','パーティのデータが見つかりません（先にセーブデータをロードしてください）。') end
  table.sort(out, function(a,b) if a.main~=b.main then return a.main end return false end)
  return out
end
local STATUS_JA={MaxHP='最大HP',HP='HP',MaxMP='最大MP',MP='MP',Attack='こうげき力',AttackLeft='こうげき力（左手）',Defense='しゅび力',
  Tension='テンション',Strength='ちから',Vitality='みのまもり',Agility='すばやさ',Dexterity='きようさ',Charm='みりょく',
  Intelligence='かしこさ',AttackMagicPower='こうげき魔力',HealMagicPower='かいふく魔力'}
local STATUS_ORDER={'MaxHP','HP','MaxMP','MP','Attack','Defense','Strength','Vitality','Agility','Dexterity','Charm','Intelligence','AttackMagicPower','HealMagicPower'}
function DQX.refreshParty(mr)
  DQX.require()
  DQX.clearChildren(mr)
  local chars=DQX.partyChars()
  local sh=UE.needStruct('HOLICharacterShareData')
  local stS=UE.needStruct('HOLICharacterStatusData')
  local crS=UE.needStruct('HOLICraftsmanStatusData')
  local fiS=UE.struct('HOLICharacterFishingData')
  local oLv, oEx, oSp = UE.off(sh,'Level'), UE.off(sh,'ExperiencePoint'), UE.off(sh,'SkillPoints')
  local oBs, oCr = UE.off(sh,'BaseStatus'), UE.off(sh,'CraftsmanStatusData')
  for _,c in ipairs(chars) do
    local g=DQX.addHdr(mr, c.name, c.name)
    local s=c.share
    DQX.addRec(g,'Level','レベル',hex(s+oLv),vtDword)
    DQX.addRec(g,'EXP','経験値',hex(s+oEx),vtDword)
    DQX.addRec(g,'Skill points','スキルポイント',hex(s+oSp),vtDword)
    local gs=DQX.addHdr(g,'Base status','基本ステータス')
    for _,n in ipairs(STATUS_ORDER) do
      local f=UE.prop(stS,n)
      if f then DQX.addRec(gs, n, STATUS_JA[n] or n, hex(s+oBs+readInteger(f+0x4C)), vtSingle) end
    end
    local gc=DQX.addHdr(g,'Blacksmith / Alchemy','鍛冶・錬金')
    for _,x in ipairs({{'BlacksmithLevel','Blacksmith level','鍛冶レベル'},{'BlacksmithExperiencePoint','Blacksmith EXP','鍛冶経験値'},
                       {'AlchemyLevel','Alchemy level','錬金レベル'},{'AlchemyExperiencePoint','Alchemy EXP','錬金経験値'}}) do
      local f=UE.prop(crS,x[1]) if f then DQX.addRec(gc,x[2],x[3],hex(s+oCr+readInteger(f+0x4C)),vtDword) end
    end
    if c.fishing and fiS then
      local gf=DQX.addHdr(g,'Fishing','釣り')
      DQX.addRec(gf,'Fishing level','つりレベル',hex(c.fishing+UE.off(fiS,'Level')),vtDword)
      DQX.addRec(gf,'Fishing EXP','つり経験値',hex(c.fishing+UE.off(fiS,'ExperiencePoint')),vtDword)
    end
    pcall(function() g.Collapsed=true end)
  end
  local srcs={} for _,c in ipairs(chars) do srcs[#srcs+1]=c.name..'='..c.src end
  DQX.msg(('Party: %d characters listed (source: %s).'):format(#chars, table.concat(srcs,', ')),
          ('パーティ: %d人を表示しました（参照元: %s）。'):format(#chars, table.concat(srcs,', ')))
end
local function pickChars(title)
  local chars=DQX.partyChars()
  local names={DQX.T('(All characters)','（全員）')}
  for _,c in ipairs(chars) do names[#names+1]=c.name end
  local i=DQX.pick(title, names); if not i then return nil end
  if i==1 then return chars end
  return {chars[i-1]}
end
-- live copies of a character's data held by the player character actors (field / battle):
-- BP_HOLIPlayerCharacter_C.AbilityBaseData.CharacterData (HOLICharacterShareData). The skill panel screen reads the field
-- player's copy (BP_HOLIPlayerCharacter_C.GetSkillPanelData), not the party data. Matched by UniqueID.
function DQX.liveShareCopies()
  local out={}
  local pc=UE.cls('BP_HOLIPlayerCharacter_C'); if not pc then return out end
  local f=UE.prop(pc,'AbilityBaseData'); if not f then return out end
  local ab=UE.innerStruct(f); local oAB=readInteger(f+0x4C)
  local oCD=ab and UE.prop(ab,'CharacterData') and UE.off(ab,'CharacterData'); if not oCD then return out end
  local shs=UE.needStruct('HOLICharacterShareData'); if not UE.prop(shs,'UniqueID') then return out end
  local oU=UE.off(shs,'UniqueID')
  for _,o in ipairs(UE.instances(pc, 32)) do
    if UE.alive(o) then local sh=o+oAB+oCD out[#out+1]={obj=o, share=sh, uid=UE.readFString(sh+oU)} end
  end
  return out
end
function DQX.addSkillPoints()
  DQX.require()
  local list=pickChars(DQX.T('Add skill points','スキルポイント追加')); if not list then return end
  local n=DQX.askInt(DQX.T('Skill points','スキルポイント'), DQX.T('Points to add (1-9999)','追加するポイント（1～9999）'), 100, 1, 9999); if not n then return end
  local sh=UE.needStruct('HOLICharacterShareData')
  local o = UE.off(sh,'SkillPoints')
  local oU = UE.prop(sh,'UniqueID') and UE.off(sh,'UniqueID')
  local copies = oU and DQX.liveShareCopies() or {}
  local nc=0
  for _,c in ipairs(list) do
    local a=c.share+o
    local v=math.min(99999,(readInteger(a) or 0)+n)
    writeInteger(a, v)
    local uid=oU and UE.readFString(c.share+oU)
    for _,cp in ipairs(copies) do
      if uid and uid~='' and cp.uid==uid and cp.share~=c.share then writeInteger(cp.share+o, v) nc=nc+1 end
    end
  end
  DQX.msg(('Added %d skill points to %d character(s) (%d character actor copies updated). Reopen the skill panel screen to see them.'):format(n,#list,nc),
          ('%d人に スキルポイントを%d追加しました（キャラクター側の写し %d件も更新）。スキルパネルの画面は開き直すと反映されます。'):format(#list,n,nc))
end
function DQX.addFishingExp()
  DQX.require()
  local main=nil for _,c in ipairs(DQX.partyChars()) do if c.fishing then main=c break end end
  if not main then DQX.err('Fishing data not found.','釣りのデータが見つかりません。') end
  local n=DQX.askInt(DQX.T('Fishing EXP','つり経験値'), DQX.T('Fishing EXP to add (1-9999999)','追加するつり経験値（1～9999999）'), 1000, 1, 9999999); if not n then return end
  local a=main.fishing+UE.off(UE.needStruct('HOLICharacterFishingData'),'ExperiencePoint')
  writeInteger(a, math.min(99999999,(readInteger(a) or 0)+n))
  DQX.msg(('Added %d fishing EXP (applied by the game at its next level check).'):format(n),('つり経験値を%d追加しました（レベルへの反映はゲーム側の次回判定時）。'):format(n))
end

----------------------------------------------------------------- 3. Items : counts
local BAGS={ {'ItemList','Use items (つかうもの)','つかうもの'}, {'SummaryList','Mix bag (おまとめぶくろ)','おまとめぶくろ'},
             {'KeyItemList','Key items (だいじなもの)','だいじなもの'} }
function DQX.refreshItems(mr)
  DQX.require()
  DQX.clearChildren(mr)
  DQX.needInv()
  pcall(DQX.itemDB)
  for _,b in ipairs(BAGS) do
    local m,L=DQX.invMember(b[1])
    local rows={}
    UE.mapIter(m,L,function(i,e)
      local idx=readInteger(e) local num=readInteger(e+4)
      rows[#rows+1]={name=DQX.itemNameByKey(idx,num), id=UE.nameStr(idx,num) or '?', a=e+L.vo}
    end)
    table.sort(rows,function(x,y) return x.id&lt;y.id end)
    local g=DQX.addHdr(mr, ('%s : %d'):format(b[2],#rows), ('%s : %d種'):format(b[3],#rows))
    for _,r in ipairs(rows) do
      local d=r.name
      DQX.addRec(g, d, d, hex(r.a), vtDword)
    end
    pcall(function() g.Collapsed=true end)
  end
  local a=DQX.invMember('EquipList')
  local n=readInteger(a+8) or 0
  local g=DQX.addHdr(mr, ('Equipment bag (そうびぶくろ) : %d items'):format(n), ('そうびぶくろ : %d個'):format(n))
  local okE, est, esz, oId, oD = pcall(function() local st=UE.needStruct('HOLIEquipData')
    return st, readInteger(st+0x58) or 0x130, UE.off(st,'EquipID'), UE.off(st,'DistinguishID') end)
  local d=readQword(a) or 0
  if okE and d~=0 and n&gt;0 and n&lt;100000 then
    local rows={}
    for i=0,n-1 do local e=d+i*esz
      local idx,num=readInteger(e+oId),readInteger(e+oId+4)
      rows[#rows+1]={name=DQX.itemNameByKey(idx,num), id=UE.nameStr(idx,num) or '?', e=e} end
    table.sort(rows,function(x,y) return x.id&lt;y.id end)
    for _,r in ipairs(rows) do
      DQX.addText(g, r.name, r.name)
    end
  end
  pcall(function() g.Collapsed=true end)
  DQX.msg('Item list created. Re-enable it after the inventory changes (addresses may move).',
          'アイテム一覧を作成しました。所持品が変わったら一度OFF→ONで作り直してください（アドレスが移動する場合があります）。')
end

----------------------------------------------------------------- 3. Items : add
local function equipInfo()
  local est=UE.needStruct('HOLIEquipData')
  local esz=readInteger(est+0x58) or 0
  if esz&lt;0x10 or esz&gt;0x1000 then DQX.err('Equipment data layout check failed.','装備データの構造チェックに失敗しました。') end
  return est, esz, UE.off(est,'EquipID'), UE.off(est,'DistinguishID')
end
-- smithing quality ("できのよさ"): DT_EquipEnhance rows (HOLIEquipEnhance, FTableRowBase) ENHANCE_&lt;equip&gt;_&lt;0..3&gt;,
-- ItemID = equipment ID, SuccessRange = 0 (none) .. 3 (+3). An equipment entry keeps the row name in
-- Extention.EnhanceID and a copy of the row in Extention.EnhanceData (equipment obtained in the game has row _0).
function DQX.enhanceRows()
  local list, st = DQX.tablesOf('HOLIEquipEnhance')
  local c=DQX.cache.enh
  if c and c.n==#list and c.first==list[1] then return c end
  local oItem, oSR = UE.off(st,'ItemID'), UE.off(st,'SuccessRange')
  c={by={}, n=#list, first=list[1], st=st, size=readInteger(st+0x58) or 0}
  for _,dt in ipairs(list) do
    for _,r in ipairs(DQX.rows(dt)) do
      local k=u32key(readInteger(r.row+oItem), readInteger(r.row+oItem+4))
      local t=readInteger(r.row+oSR) or -1
      if t&gt;=0 and t&lt;=3 then c.by[k]=c.by[k] or {} c.by[k][t]={idx=r.idx, num=r.num, row=r.row} end
    end
  end
  if c.size&lt;0x20 or c.size&gt;0x400 then DQX.err('Smithing data layout check failed.','鍛冶データの構造チェックに失敗しました。') end
  DQX.cache.enh=c
  return c
end
local function enhOffsets()
  local hst=UE.needStruct('HOLIEquipData'); local xst=UE.needStruct('HOLIEquipExtention'); local est=UE.needStruct('HOLIEquipEnhance')
  local oX=UE.off(hst,'Extention')
  local soft={} for _,n in ipairs({'OddStatusSlot1','OddStatusSlot2'}) do if UE.prop(est,n) then soft[#soft+1]=UE.off(est,n) end end
  return oX+UE.off(xst,'EnhanceID'), oX+UE.off(xst,'EnhanceData'), UE.off(est,'SuccessRange'), soft
end
-- current tier of an equipment entry, or nil when it has no smithing data
function DQX.enhanceTier(e)
  local oEID, oED, oSR = enhOffsets()
  if (readInteger(e+oEID) or 0)==0 then return nil end
  return readInteger(e+oED+oSR)
end
-- set an equipment entry to tier t using the game's own row; false when the equipment has no such row
function DQX.setEnhance(e, idx, num, t)
  local c=DQX.enhanceRows()
  local rows=c.by[u32key(idx,num)]; local r=rows and rows[t]
  if not r then return false end
  local oEID, oED, _, soft = enhOffsets()
  -- the soft references (special effects) hold an FString (SubPathString, +0x18 in FSoftObjectPtr): only empty ones are copied
  for _,so in ipairs(soft) do
    if (readQword(r.row+so+0x18) or 0)~=0 or (readQword(e+oED+so+0x18) or 0)~=0 then
      DQX.err('Smithing data has an unexpected reference; not changed.','鍛冶データに想定外の参照があるため変更しませんでした。') end
  end
  local b=readBytes(r.row+8, c.size-8, true)
  if not b or #b~=c.size-8 then return false end
  writeBytes(e+oED+8, b)                          -- +0 is the FTableRowBase vtable of the entry: kept
  writeInteger(e+oEID, r.idx) writeInteger(e+oEID+4, r.num)
  return true
end
function DQX.addEquip(it, n)
  local a=DQX.invMember('EquipList')
  local _, esz, oId, oD = equipInfo()
  local data=readQword(a) or 0; local num=readInteger(a+8) or 0
  if num&lt;0 or num&gt;100000 then DQX.err('Equipment bag check failed.','そうびぶくろのチェックに失敗しました。') end
  local maxd=0
  for i=0,num-1 do local d=s32(readInteger(data+i*esz+oD)) or 0 if d&gt;maxd then maxd=d end end
  local p=UE.arrayAdd(a, esz, n)
  for k=0,n-1 do
    local e=p+k*esz
    DQX.initEquip(e)
    writeInteger(e+oId, it.idx) writeInteger(e+oId+4, it.num) writeInteger(e+oD, maxd+1+k)
    pcall(DQX.setEnhance, e, it.idx, it.num, 0)   -- like equipment obtained in the game (row _0: price, special effects)
  end
end
-- FHOLIEquipData must be constructed (FHOLIEquipEnhance inside it has a vtable; a zero-filled entry cannot be equipped)
function DQX.initEquip(e)
  local est=UE.needStruct('HOLIEquipData')
  if UE.structConstruct(est, e) and (readQword(e+DQX.enhOff())or 0)~=0 then return true end
  local vt=DQX.need('enhVtbl')
  writeQword(e+DQX.enhOff(), vt)
  return true
end
function DQX.enhOff()
  local est=UE.needStruct('HOLIEquipData'); local xst=UE.needStruct('HOLIEquipExtention')
  return UE.off(est,'Extention')+UE.off(xst,'EnhanceData')
end
local BAGMAP={[1]='ItemList',[3]='KeyItemList',[4]='SummaryList'}
-- skill-up panels: the skill panel screen does not count the items; it reads HOLICharacterShareData.SkillUpPanels
-- (TArray&lt;uint8&gt;, count per EHOLISkillLevelUpType; index 0..3 = bronze / silver / gold / rainbow: in a save with
-- 26 / 25 / 17 / 115 items the array was 26, 25, 17, 115). The main character's party data and its live character
-- copies (UniqueID) are increased together with the items.
local SKILLUP={ID_ITEM_OTHER_SKILLUPPANEL_BRONZE=0, ID_ITEM_OTHER_SKILLUPPANEL_SILVER=1, ID_ITEM_OTHER_SKILLUPPANEL_GOLD=2, ID_ITEM_OTHER_SKILLUPPANEL_RAINBOW=3}
function DQX.addSkillUpPanels(index, n)
  local sh=UE.needStruct('HOLICharacterShareData')
  local oP=UE.off(sh,'SkillUpPanels')
  local oU=UE.prop(sh,'UniqueID') and UE.off(sh,'UniqueID')
  local main for _,c in ipairs(DQX.partyChars()) do if c.main then main=c break end end
  if not main then return 0 end
  local targets={main.share}
  local uid=oU and UE.readFString(main.share+oU)
  if uid and uid~='' then for _,cp in ipairs(DQX.liveShareCopies()) do if cp.uid==uid and cp.share~=main.share then targets[#targets+1]=cp.share end end end
  for _,shp in ipairs(targets) do
    local a=shp+oP
    local num=readInteger(a+8) or 0
    if num&lt;0 or num&gt;4096 then DQX.err('Skill-up panel data check failed.','スキルアップパネルのデータのチェックに失敗しました。') end
    if num&lt;=index then UE.arrayAdd(a, 1, index+1-num) end
    local e=readQword(a)+index
    writeBytes(e, math.min(255, (readBytes(e,1) or 0)+n))
  end
  return #targets
end
function DQX.addItem(it, n)
  if it.equip or it.bag==2 then DQX.addEquip(it, n) return true end
  if SKILLUP[it.id] then pcall(DQX.addSkillUpPanels, SKILLUP[it.id], n) end
  local mn=BAGMAP[it.bag]
  if not mn then return false end
  local m,L=DQX.invMember(mn)
  local cur=0
  UE.mapIter(m,L,function(i,e) if readInteger(e)==it.idx and readInteger(e+4)==it.num then cur=readInteger(e+L.vo) or 0 return false end end)
  local v=(mn=='KeyItemList') and math.max(1,cur) or math.min(999, cur+n)
  UE.mapSetNameInt(m, L, it.idx, it.num, v)
  return true
end
-- register in the item archive ("obtained" lists); best effort
function DQX.archiveItems(items)
  local inst=DQX.invInst(); local cls=UE.cls('HOLIInventoryManager')
  if not inst or not cls then return end
  local fT=UE.func(cls,'AddToolItemToArchives'); local fE=UE.func(cls,'AddEquipmentToArchives')
  local reqs={}
  for _,it in ipairs(items) do
    local f=(it.equip or it.bag==2) and fE or fT
    if f then
      local o=UE.off(f,'ItemID')
      reqs[#reqs+1]={obj=inst, func=f, set=function(p) writeInteger(p+o,it.idx) writeInteger(p+o+4,it.num) end}
    end
  end
  if #reqs&gt;0 then pcall(DQX.callUFuncs, reqs) end
end
local function lockMax(name)
  local c=UE.cls('HOLILockElements'); local i=c and UE.instances(c,4)[1]
  if not i then return nil end
  local f=UE.prop(c,name); if not f then return nil end
  local v=readInteger(i+readInteger(f+0x4C)); if v and v&gt;0 and v&lt;100000 then return v end
end
local function mapCount(m) return (readInteger(m+8) or 0)-(readInteger(m+0x34) or 0) end
-- addable items grouped by category: { {cat=, label=, items={...}}, ... } (sorted by category, items by name)
-- keyOnly=true: only key items (だいじなもの); otherwise key items are excluded
function DQX.itemCategories(keyOnly,progress)
  local db=DQX.itemDB(progress)
  local cats, order = {}, {}
  if progress then progress(0,#db.list,'items') end
  for i,it in ipairs(db.list) do
    if (it.equip or BAGMAP[it.bag] or it.bag==2) and ((it.bag==3)==(keyOnly==true)) then
      local c=it.cat
      if not cats[c] then cats[c]={} order[#order+1]=c end
      table.insert(cats[c], it)
    end
    if progress and (i%64==0 or i==#db.list) then progress(i,#db.list,'items') end
  end
  table.sort(order)
  local out={}
  for _,c in ipairs(order) do
    local l=DQX.CAT[c+1] and DQX.T(DQX.CAT[c+1][1],DQX.CAT[c+1][2]) or DQX.T('Other','その他')
    table.sort(cats[c], function(x,y) return x.name&lt;y.name end)
    out[#out+1]={cat=c, label=l, items=cats[c]}
  end
  return out
end
-- add several items n each, respecting the bag limits (equipment bag: total entries; use items: kinds).
-- returns added (list of {it,count}), skipped (list of items not added because the bag is full)
function DQX.addItemsBatch(items, n)
  local capItem=lockMax('ItemMax'); local capEq=lockMax('EquipMax')
  local mItem,Li=DQX.invMember('ItemList')
  local ea=DQX.invMember('EquipList')
  local nItem=mapCount(mItem); local nEq=readInteger(ea+8) or 0
  local haveKind={}
  UE.mapIter(mItem,Li,function(i,e) haveKind[readInteger(e)..':'..readInteger(e+4)]=true end)
  local added, skipped, got = {}, {}, {}
  for _,it in ipairs(items) do
    local isEq=it.equip or it.bag==2
    local cnt=n
    if isEq then
      cnt=math.min(n,20)
      if capEq then cnt=math.min(cnt, capEq-nEq) end
    elseif it.bag==1 and capItem and not haveKind[it.key] and nItem&gt;=capItem then
      cnt=0
    end
    if cnt&lt;=0 then skipped[#skipped+1]=it
    elseif DQX.addItem(it, cnt) then
      added[#added+1]={it=it, count=cnt} got[#got+1]=it
      if isEq then nEq=nEq+cnt elseif it.bag==1 and not haveKind[it.key] then haveKind[it.key]=true nItem=nItem+1 end
      if isEq and cnt&lt;math.min(n,20) then skipped[#skipped+1]=it end
    else skipped[#skipped+1]=it end
  end
  DQX.archiveItems(got)
  DQX.invSnapReset()
  return added, skipped, capItem, capEq
end
local loadEffNames, effName, softId
-- Display the equipment asset and the tier-0 row used by addEquip.
function DQX.equipmentPickerRows(category, progress)
  local T=DQX.T
  local unknown=T('Unavailable','未取得')
  local common=UE.struct('HOLIEquipCommonData')
  local basic=UE.needStruct('HOLIItemBasicData')
  local itemStatus=UE.needStruct('HOLIItemStatusData')
  local charStatus=UE.needStruct('HOLICharacterStatusData')
  local check=UE.needStruct('HOLIEquipItemCheckCondition')
  local statBase=UE.off(itemStatus,'StatusData')
  local oIndex,oJob,oLevel=UE.off(basic,'Index'),UE.off(check,'ActionJobData'),UE.off(check,'EquipLevel')
  local oKind=UE.off(check,'WeaponType')
  -- EHOLIWeaponType includes armor and both shield variants in this build.
  local kinds={
    [0]={'One-handed sword','片手剣'}, {'Two-handed sword','両手剣'}, {'Dagger','短剣'},
    {'Wand','スティック'}, {'Heavy wand','両手杖'}, {'Spear','ヤリ'}, {'Axe','オノ'},
    {'Staff','棍'}, {'Claw','ツメ'}, {'Whip','ムチ'}, {'Fan','扇'}, {'Hammer','ハンマー'},
    {'Boomerang','ブーメラン'}, {'Bow','弓'}, {'Shield','盾'}, {'Greatshield','大盾'},
    {'Upper body','からだ上'}, {'Lower body','からだ下'}, {'Head','頭'},
    [30]={'Greatshield','大盾'}, [31]={'Head','頭'},
    [32]={'Upper body','からだ上'}, [33]={'Lower body','からだ下'}, [34]={'Robe','ローブ'},
  }
  local jobs={'一般','戦','僧','魔','武','盗','旅','バ','パ','魔戦','レ','賢','ス','ま','道','踊','占','Smn','遊',
              'マイユ','ヒューザ','フウラ','ダストン','ラグアス','アンルシア'}
  local jobNames={'Nml','War','Cle','Mag','Mnk','Thf','Bst','Bms','Pld','Frc','Rng','Sag','Sst','Mom','Itm','Dan',
                  'Ftl','Smn','Asb','Maiyu','Hyuza','Fuura','Dasuton','Raguasu','Anrusia'}
  local jobLong={'一般','戦士','僧侶','魔法使い','武闘家','盗賊','旅芸人','バトルマスター','パラディン','魔法戦士',
                 'レンジャー','賢者','スーパースター','まもの使い','どうぐ使い','踊り子','占い師','Smn','遊び人',
                 'マイユ','ヒューザ','フウラ','ダストン','ラグアス','アンルシア'}
  local jobBits={}
  for i,name in ipairs(jobNames) do jobBits[i]=UE.enumValue('EHOLIAvaliableJob','AGENT_JOB_TYPE_'..name:upper()) end
  local enh=DQX.enhanceRows()
  local columns={
    {'Attack','Kougeki','ATK','攻撃力'}, {'Defense','Shubi','DEF','守備力'},
    {'MaxHP','HP','Max HP','最大HP'}, {'MaxMP','MP','Max MP','最大MP'},
    {'AttackMagicPower','Kouma','Mag. attack','攻撃魔力'}, {'HealMagicPower','Kaima','Healing magic','回復魔力'},
    {'Agility','Subayasa','Agility','すばやさ'}, {'Dexterity','Kiyousa','Dexterity','きようさ'},
    {'Charm','Miryoku','Charm','おしゃれさ'},
  }
  for _,stat in ipairs(columns) do
    stat.base=statBase+UE.off(charStatus,stat[1])
    stat.up=UE.off(enh.st,stat[2])
  end
  local extraStats={
    {UE.off(itemStatus,'AbsorbMP'),UE.off(enh.st,'AbsorbMP'),T('MP absorption','MP吸収')},
    {UE.off(itemStatus,'Guard'),UE.off(enh.st,'Guard'),T('Guard','ガード')},
  }
  local softSlots={}
  for _,name in ipairs({'OddStatusSlot1','OddStatusSlot2'}) do
    if UE.prop(enh.st,name) then softSlots[#softSlots+1]=UE.off(enh.st,name) end
  end
  local layouts={}
  local function layout(st)
    if not st or st==0 then return nil end
    if layouts[st]==nil then
      local status=UE.prop(st,'StatusData')
      local cond=UE.prop(st,'CheckCondition')
      local b=UE.prop(st,'BasicData')
      if status and cond and b and readQword(status+0x78)==itemStatus and readQword(cond+0x78)==check then
        local slots=UE.prop(st,'OddStatusSlots')
        local inner=slots and readQword(slots+0x78)
        layouts[st]={status=readInteger(status+0x4C),condition=readInteger(cond+0x4C),basic=readInteger(b+0x4C),
          slots=inner and UE.fieldClass(inner)=='SoftObjectProperty' and readInteger(inner+0x3C)==0x28 and readInteger(slots+0x4C)}
      else layouts[st]=false end
    end
    return layouts[st]
  end
  local function jobText(mask, level)
    if not mask then return unknown,'' end
    local labels, terms = {}, {}
    for i=1,#jobNames do
      local bit=jobBits[i]
      if bit and (mask &amp; (1&lt;&lt;bit))~=0 then
        labels[#labels+1]=T(jobNames[i],jobs[i])
        terms[#terms+1]=jobLong[i]..' '..jobNames[i]
      end
    end
    local text=#labels==#jobs and T('All jobs / companions','全職・仲間')
      or (#labels&gt;0 and table.concat(labels,' / ') or (mask==0 and T('None','なし') or unknown))
    if level and level&gt;0 then text=text..('  Lv.%d+'):format(level) end
    return text,table.concat(terms,' ')
  end
  local primary=({[0]=1,[1]=2})[category.cat]
  local function read(it, asset)
    local src, spec
    if asset and asset~=0 and UE.alive(asset) and UE.objName(asset)=='PA_'..it.id then
      src=asset
      spec=layout(readQword(asset+0x10))
    end
    if not spec and common and it.equipTable and UE.alive(it.equipTable) then
      src=it.equipRow
      spec=layout(common)
    end
    local ids, seen, other = {}, {}, {}
    local function effect(ptr)
      local id=softId(ptr)
      if id and not seen[id] then seen[id]=true ids[#ids+1]=id end
    end
    local tier=enh.by[it.key]
    local row=tier and tier[0] and tier[0].row
    local id, kind, main, wear, terms = unknown, primary and unknown or '', primary and unknown or '', unknown, ''
    if spec then
      local index=readInteger(src+spec.basic+oIndex)
      if index and index&gt;=0 then id=tostring(index) end
      local status=src+spec.status
      for i,stat in ipairs(columns) do
        local v=readFloat(status+stat.base)
        if v then
          v=v+(row and readFloat(row+stat.up) or 0)
          if i==primary then main=('%g'):format(v)
          elseif v~=0 then other[#other+1]=T(stat[3],stat[4])..(' %+g'):format(v) end
        end
      end
      for _,stat in ipairs(extraStats) do
        local value=readFloat(status+stat[1])
        if value then
          value=value+(row and readFloat(row+stat[2]) or 0)
          if value~=0 then other[#other+1]=stat[3]..(' %+g'):format(value) end
        end
      end
      local condition=src+spec.condition
      if primary then
        local names=kinds[rb(condition+oKind)]
        if names then kind=T(names[1],names[2]) end
      end
      wear,terms=jobText(readInteger(condition+oJob),readInteger(condition+oLevel))
      if spec.slots then
        local arr=src+spec.slots
        local data,count=readQword(arr),readInteger(arr+8)
        if data and data~=0 and count and count&gt;=0 and count&lt;=64 then
          for i=0,count-1 do effect(data+i*0x28) end
        end
      end
    end
    if row then for _,off in ipairs(softSlots) do effect(row+off) end end
    return {value=it,id=id,kind=kind,main=main,wear=wear,terms=terms,ids=ids,other=other,known=not not spec}
  end
  local function loadMissing(items)
    if #items==0 then return {} end
    local cls=UE.needCls('KismetSystemLibrary')
    local obj=UE.cdo('KismetSystemLibrary')
    local fn=UE.func(cls,'LoadAsset_Blocking')
    if not obj or not fn then return {} end
    local oAsset, oResult=UE.off(fn,'Asset'),UE.off(fn,'ReturnValue')
    local dir=({[0]='WeaponItems',[1]='EquipItems',[2]='AccessoryItems'})[category.cat]
    local requests={}
    for i,it in ipairs(items) do
      local name='PA_'..it.id
      local folder=it.id:find('^ID_ITEM_EQUIP_SHIELD_') and 'WeaponItems' or dir
      local path=('/Game/Items/%s/%s.%s'):format(folder,name,name)
      local pi,pn=DQX.makeName(path)
      requests[i]={obj=obj,func=fn,set=function(b)
        writeInteger(b+oAsset,0xFFFFFFFF)
        writeInteger(b+oAsset+0x10,pi)
        writeInteger(b+oAsset+0x14,pn)
      end,get=function(b) return readQword(b+oResult) or 0 end}
    end
    return DQX.callUFuncs(requests)
  end
  local result={}
  local total=#category.items
  for first=1,total,24 do
    local last=math.min(total,first+23)
    local missing,indices = {}, {}
    for i=first,last do
      local it=category.items[i]
      if not (it.asset and UE.alive(it.asset)) and not (it.equipTable and UE.alive(it.equipTable)) then
        missing[#missing+1]=it indices[i]=#missing
      end
    end
    local ok,assets=pcall(loadMissing,missing)
    local effects={}
    for i=first,last do
      local it=category.items[i]
      local r=read(it,(ok and indices[i] and assets[indices[i]]) or it.asset)
      result[i]=r
      for _,id in ipairs(r.ids) do effects[#effects+1]=id end
    end
    loadEffNames(effects)
    for i=first,last do
      local r=result[i]
      local details={}
      local unresolved=false
      for _,id in ipairs(r.ids) do
        local name=effName(id)
        if name==id then unresolved=true else details[#details+1]=name end
      end
      if unresolved then details[#details+1]=T('Effect name unavailable','効果名未取得') end
      for _,stat in ipairs(r.other) do details[#details+1]=stat end
      r.effect=#details&gt;0 and table.concat(details,' / ') or (r.known and '—' or unknown)
      r.text=table.concat({r.id,r.value.name,r.kind,r.main,r.effect,r.wear,r.terms},' ')
    end
    progress(last,total,'equipment')
  end
  return result
end

-- item adder window: category list -&gt; item list (multi-select) -&gt; add; "Back" returns to the category list
-- keyOnly: the key item adder (Diagnostics). Key items added this way do not set the event flags the game sets when
-- the item is obtained normally, so quests can stop progressing.
local KEY_WARN_E='Key items added here do not set the event flags that the game sets when the item is obtained normally. Quests can stop progressing and the game can break. Save to another slot first.'
local KEY_WARN_J='ここで追加しただいじなものは、ゲーム内で正しく入手したときに立つイベントフラグが立ちません。クエストが進行しなくなるなど、ゲームがおかしくなる可能性があります。先に別のスロットにセーブしてください。'
function DQX.addKeyItemPick()
  DQX.require()
  if not confirm(KEY_WARN_E..' Continue?', KEY_WARN_J..'続けますか？') then return end
  DQX.addItemPick(true)
end
function DQX.addItemPick(keyOnly)
  DQX.require()
  local wkey=keyOnly and 'addKey' or 'addItem'
  if DQX.focusWindow(wkey) then return end
  local cats
  local T=DQX.T
  local f=pickerForm()
  f.Caption=keyOnly and T('Add key item - category','だいじなもの追加 - 分類') or T('Add item - category','アイテム追加 - 分類')
  f.BorderStyle='bsSizeable'
  f.BorderIcons='[biSystemMenu,biMinimize,biMaximize]'
  f.Width=keyOnly and 620 or math.min(1100,getScreenWidth())
  f.Height=680
  -- layout by alignment only (no fixed pixel positions), so that it follows the Windows display scaling
  local function sp(c, n) pcall(function() c.BorderSpacing.Around=n end) end
  local bottom=createPanel(f) bottom.Align='alBottom' bottom.BevelOuter='bvNone' pcall(function() bottom.AutoSize=true end) pickerPanel(bottom)
  local search=pickerSearch(f)
  local lb=pickerList(f,true)
  local view=createListView(f)
  view.Align='alClient' view.BorderSpacing.Around=8 view.Visible=false
  view.ViewStyle='vsReport' view.RowSelect=true view.HideSelection=false
  view.ReadOnly=true view.MultiSelect=true view.Color=PICKER_BG view.Font.Color=0xFFFFFF
  local headers,sortColumn,ascending={},nil,true
  local function updateHeaders()
    for i,title in ipairs(headers) do
      view.Columns[i-1].Caption=title..(i==sortColumn and (ascending and ' ↑' or ' ↓') or '')
    end
  end
  local function configureColumns(category)
    view.Items.clear()
    view.Columns.clear()
    headers,sortColumn,ascending={},nil,true
    local function column(caption,width)
      headers[#headers+1]=caption
      local col=view.Columns.add() col.Width=width
    end
    column('ID',85)
    column(T('Name','名前'),category==2 and 205 or 190)
    if category~=2 then
      column(T('Type','種類'),110)
      column(category==0 and T('ATK','攻撃力') or T('DEF','守備力'),90)
    end
    column(T('Effects','効果'),category==2 and 455 or 340)
    column(T('Equip','装備'),category==2 and 275 or 230)
    updateHeaders()
  end
  local row=createPanel(bottom) row.Align='alBottom' row.BevelOuter='bvNone' pcall(function() row.AutoSize=true end) pickerPanel(row)
  local info=createLabel(bottom) info.Align='alClient' info.WordWrap=true info.AutoSize=true sp(info,6)
  local lq=createLabel(row) lq.Align='alLeft' lq.Left=1 lq.Caption=T('Quantity','個数') pcall(function() lq.Layout='tlCenter' end) sp(lq,6)
  local eq=createEdit(row) eq.Align='alLeft' eq.Left=2 eq.Width=60 eq.Text='1' sp(eq,4) pickerField(eq)
  local bOpen=createButton(row) bOpen.Align='alLeft' bOpen.Left=3 bOpen.Caption=T('Open','開く') pcall(function() bOpen.AutoSize=true end) sp(bOpen,4)
  local bAdd=createButton(row) bAdd.Align='alLeft' bAdd.Left=4 bAdd.Caption=T('Add selected','選択を追加') pcall(function() bAdd.AutoSize=true end) sp(bAdd,4)
  local bBack=createButton(row) bBack.Align='alLeft' bBack.Left=5 bBack.Caption=T('Back','戻る') pcall(function() bBack.AutoSize=true end) sp(bBack,4)
  local bClose=createButton(row) bClose.Align='alRight' bClose.Caption=T('Close','閉じる') pcall(function() bClose.AutoSize=true end) sp(bClose,4)
  local function fitInfo() pcall(function() bottom.Height=row.Height+info.Height+16 end) end
  local cur, loading, refreshing
  local entries, shown = {}, {}
  local equipRows, equipShown = nil, {}
  local function equipmentCells(r)
    if cur.cat==2 then return {r.id,r.value.name,r.effect,r.wear} end
    return {r.id,r.value.name,r.kind,r.main,r.effect,r.wear}
  end
  local function filterEquipment()
    local selected={}
    for i,r in ipairs(equipShown) do if view.Items[i-1].Selected then selected[r.value.key]=true end end
    local query=search.Text:lower()
    local filtered={}
    local numeric=sortColumn==1 or (sortColumn==4 and cur.cat~=2)
    local unavailable=T('Unavailable','未取得')
    for i,r in ipairs(equipRows) do
      if r.text:lower():find(query,1,true) then
        local cells=equipmentCells(r)
        local value=cells[sortColumn]
        if numeric then value=tonumber(value)
        elseif value=='' or value=='—' or value==unavailable then value=nil
        elseif value then value=value:lower() end
        filtered[#filtered+1]={row=r,cells=cells,value=value,position=i}
      end
    end
    if sortColumn then
      table.sort(filtered,function(a,b)
        if a.value==b.value then return a.position&lt;b.position end
        if a.value==nil then return false end
        if b.value==nil then return true end
        if ascending then return a.value&lt;b.value end
        return a.value&gt;b.value
      end)
    end
    refreshing=true
    equipShown={}
    view.Items.beginUpdate()
    view.Items.clear()
    for _,entry in ipairs(filtered) do
      local r,cells=entry.row,entry.cells
      equipShown[#equipShown+1]=r
      local item=view.Items.add()
      item.Caption=cells[1]
      for i=2,#cells do item.SubItems.add(cells[i]) end
      if selected[r.value.key] then item.Selected=true end
    end
    view.Items.endUpdate()
    refreshing=false
    info.Caption=T('Showing ','表示 ')..#equipShown..' / '..#equipRows..T('  |  Ctrl/Shift to select several.','  |  Ctrl／Shiftで複数選択')
    fitInfo()
  end
  view.OnColumnClick=function(sender,column)
    if loading or not equipRows then return end
    for i=1,#headers do
      if sender.Columns[i-1].Caption==column.Caption then
        if sortColumn==i then ascending=not ascending else sortColumn=i ascending=true end
        updateHeaders()
        filterEquipment()
        break
      end
    end
  end
  view.OnCustomDrawItem=function(sender,item)
    local r=equipShown[item.Index+1]
    if not r then return false end
    local rect,canvas=item.displayRect('drBounds'),sender.Canvas
    local x=rect.Left
    canvas.Font.Color=0xFFFFFF
    canvas.Brush.Color=item.Selected and PICKER_SELECTION or PICKER_BG
    canvas.fillRect(0,rect.Top,sender.ClientWidth,rect.Bottom)
    for i,text in ipairs(equipmentCells(r)) do
      local width=sender.Columns[i-1].Width
      if x+width&gt;0 and x&lt;sender.ClientWidth then
        local y=rect.Top+math.max(0,(rect.Bottom-rect.Top-canvas.getTextHeight(text))//2)
        canvas.textRect({Left=x+6,Top=rect.Top,Right=x+width-5,Bottom=rect.Bottom},x+6,y,text)
      end
      x=x+width
    end
    return false
  end
  view.OnSelectItem=function(_,item,selected)
    local r=not refreshing and selected and equipShown[item.Index+1]
    if r then
      info.Caption=('%s  |  %s  |  %s'):format(r.value.name,r.effect,r.wear)
      fitInfo()
    end
  end
  local function filter()
    if loading then return end
    if equipRows then filterEquipment()
    else shown=filterPickerList(lb,entries,search.Text,pickerSelection(lb,shown)) end
  end
  local function resetFilter()
    shown={} equipShown={}
    view.Items.clear()
    if search.Text=='' then filter() else search.Text='' end
  end
  local timer=createTimer(f,false)
  timer.Interval=10
  local function startLoader(work, finished, failed)
    local pid=DQX.pid
    local phases={
      itemIds={'Item IDs','アイテムID'}, assets={'Item assets','アイテムデータ'},
      tables={'Data tables','データテーブル'},
      rows={'Item tables','アイテム表'}, names={'Item names','アイテム名'},
      items={'Item list','アイテム一覧'}, equipment={'Equipment details','装備の詳細'},
    }
    local lastPhase,lastPaint
    local function progress(done,total,phase)
      local now=getTickCount()
      if phase==lastPhase and done&lt;total and now-lastPaint&lt;25 then return end
      lastPhase,lastPaint=phase,now
      coroutine.yield(T(phases[phase][1],phases[phase][2]),done,total)
    end
    local job=coroutine.create(function() return work(progress) end)
    loading=true
    search.Enabled=false bOpen.Enabled=false bAdd.Enabled=false bBack.Enabled=false
    info.Caption=T('Loading 0 / …','読み込み中 0 / …')
    fitInfo()
    timer.OnTimer=function(sender)
      sender.Enabled=false
      local attached,err=pcall(function()
        DQX.require()
        if DQX.pid~=pid then DQX.err('The game process changed.','ゲームプロセスが変わりました。') end
      end)
      local ok,value,done,total=attached,err
      if attached then ok,value,done,total=coroutine.resume(job) end
      if ok and coroutine.status(job)~='dead' then
        info.Caption=T('Loading ','読み込み中 ')..value..('  %d / %d'):format(done,total)
        fitInfo()
        sender.Enabled=true
        return
      end
      loading=false
      search.Enabled=true bOpen.Enabled=true bAdd.Enabled=true bBack.Enabled=true
      if ok then
        local rendered,e=pcall(finished,value)
        if rendered then return end
        value=e
      end
      if failed then failed() end
      info.Caption=T('Loading failed: ','読み込み失敗：')..tostring(value)
      fitInfo()
    end
    timer.Enabled=true
  end
  search.OnChange=filter
  local function showCats()
    cur=nil equipRows=nil
    view.Visible=false lb.Visible=true
    f.Caption=(keyOnly and T('Add key item - category','だいじなもの追加 - 分類') or T('Add item - category','アイテム追加 - 分類'))
    lb.MultiSelect=false
    entries={}
    for _,c in ipairs(cats) do
      entries[#entries+1]={text=('%s (%d)'):format(c.label,#c.items),value=c}
    end
    resetFilter()
    info.Caption=T('Double-click a category (or select it and press Open).','分類をダブルクリック（または選択して「開く」）してください。')
    if keyOnly then info.Caption=T(KEY_WARN_E,KEY_WARN_J)..'\n'..info.Caption end
    bOpen.Visible=true bAdd.Visible=false bBack.Visible=false eq.Visible=false lq.Visible=false
    fitInfo()
  end
  local equipmentCache={}
  local function showItems(c)
    cur=c
    f.Caption=(keyOnly and T('Add key item - ','だいじなもの追加 - ') or T('Add item - ','アイテム追加 - '))..c.label
    bOpen.Visible=false bAdd.Visible=true bBack.Visible=true eq.Visible=true lq.Visible=true
    local function display(rows)
      equipRows=rows
      if rows then
        equipShown={}
        configureColumns(c.cat)
        lb.Visible=false view.Visible=true
      else
        lb.MultiSelect=true
        entries={}
        for _,it in ipairs(c.items) do entries[#entries+1]={text=it.name,value=it} end
        info.Caption=T('Select items (Ctrl/Shift for several), set the quantity and press Add.','アイテムを選択（Ctrl／Shiftで複数）し、個数を指定して「選択を追加」を押してください。')
        if keyOnly then info.Caption=T(KEY_WARN_E,KEY_WARN_J)..'\n'..info.Caption end
      end
      resetFilter()
      fitInfo()
    end
    if c.cat==0 or c.cat==1 or c.cat==2 then
      if equipmentCache[c.cat] then display(equipmentCache[c.cat])
      else
        startLoader(function(progress) return DQX.equipmentPickerRows(c,progress) end,function(rows)
          equipmentCache[c.cat]=rows
          display(rows)
        end,showCats)
      end
    else display(nil) end
  end
  local function openSel()
    if loading then return end
    local entry=shown[lb.ItemIndex+1]
    if entry and not cur then showItems(entry.value) end
  end
  lb.OnDblClick=openSel
  bOpen.OnClick=openSel
  bBack.OnClick=function() showCats() end
  bClose.OnClick=function() f.close() end
  bAdd.OnClick=function() DQX.winRun(info, fitInfo, function()
    DQX.require()
    if not cur or loading then return end
    local sel={}
    if equipRows then
      for i,r in ipairs(equipShown) do if view.Items[i-1].Selected then sel[#sel+1]=r.value end end
    else
      for i,entry in ipairs(shown) do if lb.Selected[i-1] then sel[#sel+1]=entry.value end end
    end
    if #sel==0 then info.Caption=T('Nothing selected.','何も選択されていません。') return end
    local n=tonumber(eq.Text)
    if not n or n%1~=0 or n&lt;1 or n&gt;999 then info.Caption=T('Quantity must be 1-999 (equipment: up to 20 copies).','個数は1～999で指定してください（そうびは20個まで）。') return end
    local key, eqSel = false, false
    for _,it in ipairs(sel) do if it.bag==3 then key=true end if it.equip or it.bag==2 then eqSel=true end end
    if eqSel and n&gt;20 then info.Caption=T('Equipment: up to 20 copies at a time.','そうびは一度に20個までです。') return end
    if key and not confirm('Adding key items may break quest progress. Continue?','だいじなものの追加はクエスト進行に影響する場合があります。続けますか？') then return end
    local ok, added, skipped, capI, capE = pcall(DQX.addItemsBatch, sel, math.tointeger(n) or n)
    if not ok then info.Caption=tostring(added) return end
    local total=0 for _,a in ipairs(added) do total=total+a.count end
    local msgE=('%s: %d kinds added (%d in total).'):format(cur.label,#added,total)
    local msgJ=('%s: %d種類（合計%d個）を追加しました。'):format(cur.label,#added,total)
    if #skipped&gt;0 then
      msgE=msgE..(' %d kinds were not added (or only partly) because the bag is full (use items max %s kinds / equipment bag max %s).'):format(#skipped,tostring(capI),tostring(capE))
      msgJ=msgJ..('袋がいっぱいのため%d種類は追加できない（または一部のみ追加）でした（つかうもの上限 %s種類／そうびぶくろ上限 %s個）。'):format(#skipped,tostring(capI),tostring(capE))
    end
    info.Caption=T(msgE,msgJ) fitInfo()
    DQX.msg(msgE,msgJ)
  end) end
  bOpen.Visible=false bAdd.Visible=false bBack.Visible=false eq.Visible=false lq.Visible=false
  DQX.showWindow(wkey,f)
  startLoader(function(progress) return DQX.itemCategories(keyOnly,progress) end,function(result)
    cats=result
    showCats()
  end)
end

-- non-modal windows: one per kind; opening it again brings the open one to the front. A window closes itself (caFree).
DQX._wins = DQX._wins or {}
function DQX.focusWindow(key)
  local w=DQX._wins[key]
  if w then
    if pcall(function() w.show() w.bringToFront() end) then return true end
    DQX._wins[key]=nil
  end
  return false
end
function DQX.showWindow(key, f)
  f.OnClose=function() DQX._wins[key]=nil return caFree end
  DQX._wins[key]=f
  f.show()
end
-- run a button action of a window: errors are shown in the window and as a dialog (not lost in the Lua Engine)
function DQX.winRun(info, fit, fn, ...)
  local ok,e=pcall(fn, ...)
  if not ok then
    local m=tostring(e)
    pcall(function() info.Caption=m if fit then fit() end end)
    DQX.msg(m, m)
    pcall(function() messageDialog(m, mtError, mbOK) end)
  end
end
-- equipment entries of the bag and the copies equipped by the party (same EquipID + DistinguishID)
local function equipEntries()
  local hst=UE.needStruct('HOLIEquipData'); local esz=readInteger(hst+0x58) or 0
  local oId, oD = UE.off(hst,'EquipID'), UE.off(hst,'DistinguishID')
  local ea=DQX.invMember('EquipList')
  local d=readQword(ea) or 0; local n=readInteger(ea+8) or 0
  if n&lt;0 or n&gt;100000 or esz&lt;0x10 then DQX.err('Equipment bag check failed.','そうびぶくろのチェックに失敗しました。') end
  local worn={}
  pcall(function()
    local sh=UE.needStruct('HOLICharacterShareData'); local ed=UE.needStruct('HOLICharacterEquipDetails')
    local oL=UE.off(sh,'EquipDetails')+UE.off(ed,'EquipDataList')
    -- party data and the live character actors (BP_HOLICharacterFieldFriend_C / FieldPlayer / CaptureCharacter ...
    -- AbilityBaseData.CharacterData): after equipping a friend in the menu only the actor copies hold the new equipment
    -- (DQX_worn.txt: Hyuza's AKUSERUGIA #81 / #82 were in BP_HOLICharacterFieldFriend_C, not in PartyDataManager.FriendsData)
    local shares, seen = {}, {}
    for _,c in ipairs(DQX.partyChars()) do if not seen[c.share] then seen[c.share]=true shares[#shares+1]=c.share end end
    pcall(function() for _,cp in ipairs(DQX.liveShareCopies()) do if not seen[cp.share] then seen[cp.share]=true shares[#shares+1]=cp.share end end end)
    for _,sh0 in ipairs(shares) do
      local a=sh0+oL; local cd=readQword(a) or 0; local cn=readInteger(a+8) or 0
      if cd~=0 and cn&gt;0 and cn&lt;=32 then
        for i=0,cn-1 do local e=cd+i*esz
          local k=u32key(readInteger(e+oId),readInteger(e+oId+4))..'#'..tostring(readInteger(e+oD))
          worn[k]=worn[k] or {} table.insert(worn[k], e) end
      end
    end
  end)
  local out={}
  for i=0,n-1 do
    local e=d+i*esz
    local idx, num = readInteger(e+oId), readInteger(e+oId+4)
    local k=u32key(idx,num)..'#'..tostring(readInteger(e+oD))
    out[#out+1]={e=e, idx=idx &amp; 0xFFFFFFFF, num=num, worn=worn[k], k=k}
  end
  return out
end
-- the window stays open while the game runs: the selected entries are looked up again (EquipID + DistinguishID) right
-- before writing, because the bag array can move or change in the meantime
local function freshSelected(lb, list)
  local want={} local order={}
  for i,entry in ipairs(list) do
    if lb.Selected[i-1] then
      want[entry.key]=true
      order[#order+1]=entry.key
    end
  end
  local by={} for _,x in ipairs(equipEntries()) do if want[x.k] and not by[x.k] then by[x.k]=x end end
  local out, missing = {}, 0
  for _,k in ipairs(order) do if by[k] then out[#out+1]=by[k] else missing=missing+1 end end
  return out, missing
end
-- smithing window: select equipment (several), then set none / +1 / +2 / +3
function DQX.smithPick()
  DQX.require()
  if DQX.focusWindow('smith') then return end
  DQX.needInv()
  local enh=DQX.enhanceRows()
  local T=DQX.T
  local f=pickerForm()
  f.Caption=T('Smithing quality','鍛冶のできのよさ') f.Width=640 f.Height=680 f.Position='poScreenCenter'
  local function sp(c, n) pcall(function() c.BorderSpacing.Around=n end) end
  local bottom=createPanel(f) bottom.Align='alBottom' bottom.BevelOuter='bvNone' pcall(function() bottom.AutoSize=true end) pickerPanel(bottom)
  local search=pickerSearch(f)
  local lb=pickerList(f,true)
  local row=createPanel(bottom) row.Align='alBottom' row.BevelOuter='bvNone' pcall(function() row.AutoSize=true end) pickerPanel(row)
  local info=createLabel(bottom) info.Align='alClient' info.WordWrap=true info.AutoSize=true sp(info,6)
  local btn={}
  for t=0,3 do
    local b=createButton(row) b.Align='alLeft' b.Left=t+1 pcall(function() b.AutoSize=true end) sp(b,4)
    b.Caption=(t==0) and T('None','なし') or ('+'..t) btn[t]=b
  end
  local bClose=createButton(row) bClose.Align='alRight' bClose.Caption=T('Close','閉じる') pcall(function() bClose.AutoSize=true end) sp(bClose,4)
  local function fitInfo() pcall(function() bottom.Height=row.Height+info.Height+16 end) end
  local entries, shown = {}, {}
  local function label(x)
    local tier=DQX.enhanceTier(x.e)
    local rows=enh.by[u32key(x.idx,x.num)]
    local mx=-1 if rows then for t=0,3 do if rows[t] then mx=t end end end
    local st
    if mx&lt;=0 then st=T('cannot be smithed','鍛冶できない')
    elseif tier==nil then st=T('no smithing data','鍛冶データなし')
    else st=(tier==0) and T('none','なし') or ('+'..tier) end
    return ('%s  [%s]%s'):format(DQX.itemNameByKey(x.idx,x.num), st, x.worn and T(' (equipped)','（装備中）') or '')
  end
  local function filter(selected)
    shown=filterPickerList(lb,entries,search.Text,selected)
  end
  search.OnChange=function() filter(pickerSelection(lb,shown)) end
  local function refresh(keepSel)
    local selected=keepSel and pickerSelection(lb,shown) or {}
    entries={}
    for _,x in ipairs(equipEntries()) do
      entries[#entries+1]={text=label(x),key=x.k}
    end
    filter(selected)
  end
  local function apply(t)
    DQX.require()
    local n, skip = 0, 0
    local sel, missing = freshSelected(lb, shown)
    for _,x in ipairs(sel) do
      if DQX.setEnhance(x.e, x.idx, x.num, t) then
        n=n+1
        for _,w in ipairs(x.worn or {}) do pcall(DQX.setEnhance, w, x.idx, x.num, t) end
      else skip=skip+1 end
    end
    local tn=(t==0) and T('none','なし') or ('+'..t)
    local mE=('Smithing quality set to %s: %d items (%d items have no %s data and were not changed).'):format(tn,n,skip,tn)
    local mJ=('できのよさを %s にしました：%d個（%s のデータが無い %d個は変更していません）。'):format(tn,n,tn,skip)
    if missing&gt;0 then mE=mE..(' %d selected items are no longer in the bag.'):format(missing) mJ=mJ..('選択したもののうち %d個はそうびぶくろにありません。'):format(missing) end
    info.Caption=T(mE,mJ) fitInfo() DQX.msg(mE,mJ)
    DQX.invSnapReset()
    refresh(true)
  end
  for t=0,3 do btn[t].OnClick=function() DQX.winRun(info, fitInfo, apply, t) end end
  bClose.OnClick=function() f.close() end
  info.Caption=T('Select equipment (Ctrl/Shift for several) and press the quality to set. Equipped copies are changed too.',
                 '装備を選択（Ctrl／Shiftで複数）し、付けたいできのよさのボタンを押してください。装備中のものも同時に変わります。')
  refresh(false) fitInfo()
  DQX.showWindow('smith', f)
end

-- accessory synthesis ("合成"): decoded from WB_UI_Synthesis_C (ExecuteUbergraph +2333..+32EA, CreateEnhanceId) and
-- WB_UI_Synthesis_MixAddition_C.GetSynthesisList. An accessory keeps
--   Extention.SyntheticOddStatusItems  TMap&lt;int32, TSoftObjectPtr&lt;HOLIOddStatusItem&gt;&gt;, keys 0..2 (Min(Map_Length, 2): 3 slots)
--   Extention.TraditionOddStatusItems  TSoftObjectPtr (tradition / 伝承: only an accessory whose DT_Facility_SynthesisAccessoryData row
--                                      has InheritAccessoryID; its candidates are the effects of that inherited accessory)
--   Extention.EnhanceID                FName "ENHANCE_&lt;id without ID_ITEM_EQUIP_&gt;_&lt;slots + (tradition ? 1 : 0)&gt;"  (+1 .. +4)
-- Effect candidates: DT_Facility_SynthesisAccessoryData.SynthesisID (ODDSTATUS_...), stored as the soft path
--   /Game/Items/OddStatusItems/PA_&lt;ID&gt;.PA_&lt;ID&gt;   (as in a synthesised accessory of the running game)
local function synTable()
  local rows, st = DQX.allRows('HOLISynthesisAccessoryData')
  if #rows==0 then DQX.err('Synthesis data not found (load a save first).','合成データが見つかりません（先にセーブデータをロードしてください）。') end
  local oS, oI = UE.off(st,'SynthesisID'), UE.off(st,'InheritAccessoryID')
  local by={}
  for _,r in ipairs(rows) do
    local a=r.row+oS local d=readQword(a) or 0 local n=readInteger(a+8) or 0
    local c={}
    if d~=0 and n&gt;0 and n&lt;64 then
      for i=0,n-1 do local ni,nn=readInteger(d+i*8),readInteger(d+i*8+4)
        if ni and ni~=0 then c[#c+1]={idx=ni &amp; 0xFFFFFFFF, num=nn, id=UE.nameStr(ni,nn) or '?'} end end
    end
    local ii,inn=readInteger(r.row+oI),readInteger(r.row+oI+4)
    by[u32key(r.idx,r.num)]={cands=c, inherit=(ii and ii~=0) and u32key(ii,inn) or nil}
  end
  for _,x in pairs(by) do x.trad=x.inherit and by[x.inherit] and by[x.inherit].cands or nil if x.trad and #x.trad==0 then x.trad=nil end end
  return by
end
-- FName from a string with the game's own FName constructor (KismetStringLibrary.Conv_StringToName): number suffixes
-- ("..._2") are split exactly as the game does when it builds these names.
DQX._fnameCache = DQX._fnameCache or {}
function DQX.makeName(str)
  local k=DQX.pid..':'..str
  local c=DQX._fnameCache[k] if c then return c[1], c[2] end
  if #str&gt;1000 then DQX.err('Name too long.','名前が長すぎます。') end
  local cls=UE.needCls('KismetStringLibrary') local cdo=UE.cdo('KismetStringLibrary') or DQX.err('KismetStringLibrary not found.','KismetStringLibrary が見つかりません。')
  local fn=UE.func(cls,'Conv_StringToName') or DQX.err('Conv_StringToName not found.','Conv_StringToName が見つかりません。')
  local oIn, oR = UE.off(fn,'InString'), UE.off(fn,'ReturnValue')
  local buf=DQX.base()+DQX.OFF_PARAM+0x800
  local b={} for i=1,#str do b[#b+1]=str:byte(i) b[#b+1]=0 end b[#b+1]=0 b[#b+1]=0
  writeBytes(buf, b)
  local r=DQX.callUFuncs({{obj=cdo, func=fn, set=function(p) writeQword(p+oIn, buf) writeInteger(p+oIn+8, #str+1) writeInteger(p+oIn+12, #str+1) end,
    get=function(p) return {readInteger(p+oR), readInteger(p+oR+4)} end}})[1]
  if not r or not r[1] or r[1]==0 then DQX.err('Name could not be made: '..str, '名前を作成できませんでした: '..str) end
  DQX._fnameCache[k]=r
  return r[1], r[2]
end
local function effPath(id) return ('/Game/Items/OddStatusItems/PA_%s.PA_%s'):format(id, id) end
local function synOffsets()
  local hst=UE.needStruct('HOLIEquipData'); local xst=UE.needStruct('HOLIEquipExtention')
  local oX=UE.off(hst,'Extention')
  local fS=UE.needProp(xst,'SyntheticOddStatusItems')
  local L=UE.mapLayout(fS, 8, 0x38)
  if L.vo~=8 or L.size&lt;0x38 then DQX.err('Synthesis data layout check failed.','合成データの構造チェックに失敗しました。') end
  return oX+UE.off(xst,'SyntheticOddStatusItems'), oX+UE.off(xst,'TraditionOddStatusItems'), oX+UE.off(xst,'EnhanceID'), L
end
-- current slots of an accessory entry: {ids...}, tradition id or nil (ids are the ODDSTATUS_... names)
softId=function(a)
  local s=UE.nameStr(readInteger(a+0x10), readInteger(a+0x14))
  if not s or s=='None' then return nil end
  return s:match('%.PA_([%w_%-]+)$') or s
end
function DQX.synSlots(e)
  local oS, oT, _, L = synOffsets()
  local t={}
  UE.mapIter(e+oS, L, function(i,el) t[#t+1]={k=readInteger(el) or 0, id=softId(el+L.vo)} end)
  table.sort(t, function(a,b) return a.k&lt;b.k end)
  local ids={} for _,x in ipairs(t) do ids[#ids+1]=x.id or '?' end
  return ids, softId(e+oT)
end
-- write a TSoftObjectPtr (FWeakObjectPtr +0, TagAtLastTest +8, FSoftObjectPath: AssetPathName +0x10, SubPathString +0x18).
-- live: the value is in use. Its SubPathString is kept when it is an empty string (an allocated buffer with Num &lt;= 1) and
-- refused when it holds text. Not live (a free map element being filled): the old bytes are not data and are overwritten.
local function writeSoft(a, pIdx, pNum, live)
  local keepSub=false
  if live and (readQword(a+0x18) or 0)~=0 then
    if (readInteger(a+0x20) or 0)&gt;1 then
      DQX.err('Synthesis data has an unexpected reference (sub path text); not changed.','合成データに想定外の参照（サブパスの文字列）があるため変更しませんでした。') end
    keepSub=true
  end
  writeInteger(a, 0xFFFFFFFF) writeInteger(a+4, 0) writeInteger(a+8, 0) writeInteger(a+0xC, 0)
  writeInteger(a+0x10, pIdx or 0) writeInteger(a+0x14, pNum or 0)
  if not keepSub then writeQword(a+0x18, 0) writeQword(a+0x20, 0) end
end
-- set the synthesis of one accessory entry: slots = { {idx,num} path FNames } (0..3), trad = path FName or nil, enh = EnhanceID FName
local function synApply(e, slots, trad, enh)
  local oS, oT, oE, L = synOffsets()
  local m=e+oS
  local n=#slots
  -- checks on the current map before writing
  local hs=readInteger(m+0x48) or 0
  if hs&lt;0 or hs&gt;0x10000 or (hs&gt;0 and (hs &amp; (hs-1))~=0) then DQX.err('Synthesis data check failed (hash).','合成データのチェックに失敗しました（ハッシュ）。') end
  local bad=false
  UE.mapIter(m, L, function(i,el)
    if (readQword(el+L.vo+0x18) or 0)~=0 and (readInteger(el+L.vo+0x20) or 0)&gt;1 then bad=true end
    if hs&gt;0 and (readInteger(el+L.hi) or -1)~=((readInteger(el) or 0) &amp; (hs-1)) then bad=true end
  end)
  if bad then DQX.err('Synthesis data check failed; not changed.','合成データのチェックに失敗したため変更しませんでした。') end
  -- elements 0..n-1 are rewritten; a live element there keeps an empty sub path buffer, a free one is overwritten
  local liveIdx={} UE.mapIter(m, L, function(i,el) liveIdx[i]=true end)
  local data=readQword(m) or 0 local max=readInteger(m+12) or 0
  if n&gt;max then data=DQX.realloc(data~=0 and data or 0, n*L.size) writeQword(m, data) writeInteger(m+12, n) end
  local sec=readQword(m+0x20) or 0
  local bits=(sec~=0) and sec or (m+0x10)
  if sec==0 then for k=0,3 do writeInteger(m+0x10+k*4, 0) end else writeInteger(sec, 0) end
  if hs==0 and n&gt;0 then writeQword(m+0x40, 0) writeInteger(m+0x48, 1) hs=1 end
  local hp=readQword(m+0x40); if not hp or hp==0 then hp=m+0x38 end
  for h=0,hs-1 do writeInteger(hp+h*4, 0xFFFFFFFF) end
  for i=0,n-1 do
    local el=data+i*L.size
    writeInteger(el, i) writeInteger(el+4, 0)
    writeSoft(el+L.vo, slots[i+1][1], slots[i+1][2], liveIdx[i])
    local h=i &amp; (hs-1)
    writeInteger(el+L.hi, h) writeInteger(el+L.hn, readInteger(hp+h*4)) writeInteger(hp+h*4, i)
  end
  writeInteger(bits, (1&lt;&lt;n)-1)
  writeInteger(m+8, n)
  writeInteger(m+0x28, n)
  if sec==0 and (readInteger(m+0x2C) or 0)&lt;128 then writeInteger(m+0x2C, 128) end
  writeInteger(m+0x30, 0xFFFFFFFF) writeInteger(m+0x34, 0)
  if trad then writeSoft(e+oT, trad[1], trad[2], true) else writeSoft(e+oT, 0, 0, true) end
  writeInteger(e+oE, enh[1]) writeInteger(e+oE+4, enh[2])
end
-- display name of an effect: the HOLIOddStatusItem asset (PA_&lt;ID&gt;) is loaded with the game's own
-- KismetSystemLibrary.LoadAsset_Blocking (the synthesis menu itself loads it with LoadPrimaryAssetItemBlocking before use)
-- and its Name text is read; the ID is shown only when that fails
local effNames, effectNamePid = {}, nil
loadEffNames=function(ids)
  if effectNamePid~=DQX.pid then effNames={} effectNamePid=DQX.pid end
  local need={}
  for _,id in ipairs(ids) do if effNames[id]==nil then effNames[id]=false need[#need+1]=id end end
  if #need==0 then return end
  local ok,e=pcall(function()
    local ksl=UE.needCls('KismetSystemLibrary') local cSL=UE.cdo('KismetSystemLibrary')
    local fL=UE.func(ksl,'LoadAsset_Blocking')
    local c=UE.needCls('HOLIOddStatusItem') local oN, oE = UE.off(c,'Name'), UE.off(c,'Explanation')
    if not cSL or not fL then return end
    local oA, oR = UE.off(fL,'Asset'), UE.off(fL,'ReturnValue')
    local rq={}
    for i,id in ipairs(need) do
      local pi,pn=DQX.makeName(effPath(id))
      rq[i]={obj=cSL, func=fL, set=function(b) writeInteger(b+oA, 0xFFFFFFFF) writeInteger(b+oA+0x10, pi) writeInteger(b+oA+0x14, pn) end,
             get=function(b) return readQword(b+oR) end}
    end
    local r=DQX.callUFuncs(rq)
    local pending={}
    local function valid(t) return t and t~='' and not t:find('MISSING STRING TABLE',1,true) end
    for i,id in ipairs(need) do
      local o=r[i]
      if o and o~=0 and UE.alive(o) then
        pending[#pending+1]={{id=id},o+oN,o+oE}
      end
    end
    if #pending&gt;0 then
      local addrs={}
      for i,p in ipairs(pending) do addrs[i]=p[2] end
      local names=UE.readFTexts(addrs)
      local missing, explanations = {}, {}
      for i,p in ipairs(pending) do
        local t=names[i]
        if valid(t) then effNames[p[1].id]=t:gsub('[\r\n]+',' ')
        else missing[#missing+1]=p explanations[#explanations+1]=p[3] end
      end
      if #missing&gt;0 then
        local descriptions=UE.readFTexts(explanations)
        local fallback={}
        for i,p in ipairs(missing) do
          local t=descriptions[i]
          if valid(t) then effNames[p[1].id]=t:gsub('[\r\n]+',' ')
          else fallback[#fallback+1]=p end
        end
        if #fallback&gt;0 then
          pcall(UE.stringTableNames,fallback)
          for _,p in ipairs(fallback) do
            if valid(p[1].name) then effNames[p[1].id]=p[1].name:gsub('[\r\n]+',' ') end
          end
        end
      end
    end
  end)
  if not ok then DQX.msg('Effect names could not be read: '..tostring(e), '効果名を読み込めませんでした: '..tostring(e)) end
end
effName=function(id)
  if effNames[id]==nil then loadEffNames({id}) end
  return effNames[id] or id
end
-- synthesis window: select accessories, choose the effect of each slot (and the tradition slot), press none / +1 .. +4
function DQX.synthPick()
  DQX.require()
  if DQX.focusWindow('synth') then return end
  DQX.needInv()
  local by=synTable()
  local T=DQX.T
  effNames={}
  local f=pickerForm()
  f.Caption=T('Accessory synthesis','アクセサリの合成') f.Width=700 f.Height=720 f.Position='poScreenCenter'
  local function sp(c, n) pcall(function() c.BorderSpacing.Around=n end) end
  local bottom=createPanel(f) bottom.Align='alBottom' bottom.BevelOuter='bvNone' pcall(function() bottom.AutoSize=true end) pickerPanel(bottom)
  local search=pickerSearch(f)
  local lb=pickerList(f,true)
  local row=createPanel(bottom) row.Align='alBottom' row.BevelOuter='bvNone' pcall(function() row.AutoSize=true end) pickerPanel(row)
  local cb, cap, findEffect = {}, {}, {}
  local slotPanel=createPanel(bottom) slotPanel.Align='alBottom' slotPanel.BevelOuter='bvNone' pcall(function() slotPanel.AutoSize=true end) pickerPanel(slotPanel)
  for k=4,1,-1 do
    -- rows sized by their contents (fixed pixel heights clip the text on scaled displays)
    local p=createPanel(slotPanel) p.Align='alTop' p.BevelOuter='bvNone' pcall(function() p.AutoSize=true end) pickerPanel(p)
    local l=createLabel(p) l.Align='alLeft' pcall(function() l.AutoSize=true l.Layout='tlCenter' end) sp(l,6)
    l.Caption=(k&lt;=3) and T(('Slot %d'):format(k), ('枠%d'):format(k)) or T('Tradition (+4)','伝承（+4）')
    local find=createButton(p)
    find.Align='alRight' find.AutoSize=true find.Caption=T('Search','検索') sp(find,4)
    find.Visible=false
    local c=pickerCombo(p) c.Align='alClient' sp(c,4)
    find.OnClick=function()
      local names={}
      for i=0,c.Items.Count-1 do names[#names+1]=c.Items[i] end
      local index=DQX.pick(l.Caption,names)
      if index then c.ItemIndex=index-1 end
    end
    pcall(function() l.Constraints.MinWidth=c.Canvas.getTextWidth('伝承（+4）　')+12 end)
    cb[k]=c cap[k]=l findEffect[k]=find
  end
  local info=createLabel(bottom) info.Align='alClient' info.WordWrap=true info.AutoSize=true sp(info,6)
  local btn={}
  for t=0,4 do
    local b=createButton(row) b.Align='alLeft' b.Left=t+1 pcall(function() b.AutoSize=true end) sp(b,4)
    b.Caption=(t==0) and T('None','なし') or ('+'..t) btn[t]=b
  end
  local bClose=createButton(row) bClose.Align='alRight' bClose.Caption=T('Close','閉じる') pcall(function() bClose.AutoSize=true end) sp(bClose,4)
  local function fitInfo() pcall(function() bottom.Height=row.Height+slotPanel.Height+info.Height+16 end) end
  local entries, shown = {}, {}
  local curKey, filtering
  local function label(x)
    local ids, tr = DQX.synSlots(x.e)
    loadEffNames(ids) if tr then loadEffNames({tr}) end
    local n=#ids+(tr and 1 or 0)
    local t={} for _,id in ipairs(ids) do t[#t+1]=effName(id) end
    if tr then t[#t+1]=T('tradition: ','伝承：')..effName(tr) end
    return ('%s  [%s]%s%s'):format(DQX.itemNameByKey(x.idx,x.num), n==0 and T('none','なし') or ('+'..n),
      #t&gt;0 and ('  '..table.concat(t,' / ')) or '', x.worn and T(' (equipped)','（装備中）') or '')
  end
  local function fillCombos(key)
    curKey=key
    local d=key and by[key]
    for k=1,4 do
      local c=cb[k] c.Items.clear()
      local cands=d and ((k&lt;=3) and d.cands or d.trad) or nil
      if cands then for _,x in ipairs(cands) do c.Items.add(effName(x.id)) end c.ItemIndex=0 c.Enabled=true else c.Enabled=false end
      findEffect[k].Visible=c.Items.Count&gt;c.DropDownCount
    end
    btn[4].Enabled=(d and d.trad)~=nil
    -- +4 needs a tradition source (InheritAccessoryID) in DT_Facility_SynthesisAccessoryData
    cap[4].Caption=(d and not d.trad) and T('Tradition (+4): not possible for this accessory (no inheritance in the game data)','伝承（+4）：このアクセサリは不可（ゲームのデータに伝承元がありません）')
      or T('Tradition (+4)','伝承（+4）')
  end
  local function syncSelection()
    if filtering then return end
    local key
    for i,entry in ipairs(shown) do
      if lb.Selected[i-1] then
        local x=entry.value
        key=u32key(x.idx,x.num)
        break
      end
    end
    if key~=curKey then fillCombos(key) end
  end
  local function filter(selected)
    filtering=true
    shown=filterPickerList(lb,entries,search.Text,selected)
    filtering=false
    syncSelection()
  end
  search.OnChange=function() filter(pickerSelection(lb,shown)) end
  lb.OnSelectionChange=syncSelection
  local function refresh(keepSel)
    local selected=keepSel and pickerSelection(lb,shown) or {}
    local list={}
    for _,x in ipairs(equipEntries()) do
      local d=by[u32key(x.idx,x.num)]
      if d and #d.cands&gt;0 then list[#list+1]=x end
    end
    local pre, seenK = {}, {}
    for _,x in ipairs(list) do local k=u32key(x.idx,x.num) if not seenK[k] then seenK[k]=true local d=by[k]
      for _,c in ipairs(d.cands) do pre[#pre+1]=c.id end for _,c in ipairs(d.trad or {}) do pre[#pre+1]=c.id end end end
    loadEffNames(pre)
    entries={}
    local width=0
    for _,x in ipairs(list) do
      local text=label(x)
      entries[#entries+1]={text=text,key=x.k,value=x}
      width=math.max(width,lb.Canvas.getTextWidth(text)+8)
    end
    lb.ScrollWidth=width
    filter(selected)
  end
  -- the soft paths made here must equal the ones the game stored in already synthesised accessories (checked once)
  local function checkPaths()
    if DQX._synPathOK==DQX.pid then return end
    local oS, _, _, L = synOffsets()
    local checked=0
    for _,entry in ipairs(entries) do
      local x=entry.value
      UE.mapIter(x.e+oS, L, function(i,el)
        local id=softId(el+L.vo)
        if id and checked&lt;4 then
          local pi,pn=DQX.makeName(effPath(id))
          if u32key(pi,pn)~=u32key(readInteger(el+L.vo+0x10),readInteger(el+L.vo+0x14)) then
            DQX.err('Effect reference check failed (the stored path differs); not changed.','効果の参照の検証に失敗しました（保存されているパスと一致しません）。変更していません。') end
          checked=checked+1
        end
      end)
    end
    if checked&gt;0 then DQX._synPathOK=DQX.pid end
  end
  local function apply(t)
    DQX.require()
    checkPaths()
    if not curKey then info.Caption=T('Select an accessory first.','先にアクセサリを選択してください。') fitInfo() return end
    local d=by[curKey]
    if t==4 and not d.trad then return end
    -- effects chosen in the slots (candidates of the accessory shown in the slot lists)
    local nS=math.min(t,3)
    local slots={}
    for k=1,nS do local x=d.cands[(cb[k].ItemIndex or 0)+1] if not x then return end
      local pi,pn=DQX.makeName(effPath(x.id)) slots[k]={pi,pn} end
    local trad
    if t==4 then local x=d.trad[(cb[4].ItemIndex or 0)+1] if not x then return end local pi,pn=DQX.makeName(effPath(x.id)) trad={pi,pn} end
    local n, skip = 0, 0
    local sel, missing = freshSelected(lb, shown)
    skip=missing
    for _,x in ipairs(sel) do
      do
        if u32key(x.idx,x.num)==curKey then
          local sid=(UE.nameStr(x.idx,x.num) or ''):gsub('^ID_ITEM_EQUIP_','')
          local ei,en=DQX.makeName(('ENHANCE_%s_%d'):format(sid, t))
          synApply(x.e, slots, trad, {ei,en})
          for _,w in ipairs(x.worn or {}) do pcall(synApply, w, slots, trad, {ei,en}) end
          n=n+1
        else skip=skip+1 end
      end
    end
    local tn=(t==0) and T('none','なし') or ('+'..t)
    local mE=('Synthesis set to %s: %d accessories.'):format(tn,n)..(skip&gt;0 and (' %d were not changed (another kind, or no longer in the bag).'):format(skip) or '')
    local mJ=('合成を %s にしました：%d個。'):format(tn,n)..(skip&gt;0 and ('%d個は変更していません（別の種類、またはそうびぶくろから無くなったもの）。'):format(skip) or '')
    info.Caption=T(mE,mJ) fitInfo() DQX.msg(mE,mJ)
    DQX.invSnapReset()
    refresh(true)
  end
  for t=0,4 do btn[t].OnClick=function() DQX.winRun(info, fitInfo, apply, t) end end
  bClose.OnClick=function() f.close() end
  info.Caption=T('Select accessories of one kind (Ctrl/Shift for several), choose the effect of each slot, and press the level. +N uses slots 1..N (+4 = 3 slots and the tradition effect; only accessories that can inherit). Equipped copies are changed too.',
                 '同じ種類のアクセサリを選択（Ctrl／Shiftで複数）し、各枠の効果を選んでから段階のボタンを押してください。+N は枠1〜N を使います（+4 は3枠＋伝承効果。伝承できるアクセサリだけ）。装備中のものも同時に変わります。')
  refresh(false) fillCombos(nil) fitInfo()
  DQX.showWindow('synth', f)
end

-- expedition (討伐隊) returns now. HOLIMainInventoryData.CorpIraiInfo (HOLICorpIraiInfo): AcceptedTime is the return time
-- (WB_Facility_Subjugation_UIMenu_C sets AcceptedTime = UtcNow + duration; DQX_corp.txt: 2026-10-03 12:23 UTC for a 10 h
-- expedition sent at 02:23 UTC). BP_HOLIInventoryManager_C.CheckOshiraseCorpIrai reports the return when
-- isAccepted and not isOshiraseFinished and (AcceptedTime &lt; UtcNow, or ForceFinished in a non-packaged build only).
-- The return time is set to 10 seconds ago (FDateTime ticks = 100 ns since 0001-01-01).
function DQX.corpReturnNow()
  DQX.require() DQX.needInv()
  local st=UE.needStruct('HOLIMainInventoryData'); local cs=UE.needStruct('HOLICorpIraiInfo')
  local a=DQX.needInv()+UE.off(st,'CorpIraiInfo')
  local oA, oT = UE.prop(cs,'isAccepted'), UE.off(cs,'AcceptedTime')
  local bo, bm = UE.boolBit(oA)
  local acc=((rb(a+bo) or 0) &amp; bm)~=0
  if not acc then DQX.err('No expedition is out.','派遣中の討伐隊がありません。') end
  local nowTicks=(os.time()+62135596800)*10000000
  local cur=readQword(a+oT) or 0
  if cur&lt;=0 or cur&gt;nowTicks+30*86400*10000000 then DQX.err('Expedition data check failed (return time).','討伐隊のデータのチェックに失敗しました（帰還時刻）。') end
  if cur&lt;nowTicks then DQX.msg('The expedition has already returned (talk to the expedition clerk).','討伐隊はすでに帰還しています（討伐隊員に話しかけてください）。') return end
  writeQword(a+oT, nowTicks-10*10000000)
  DQX.msg('Expedition: return time set to now. Talk to the expedition clerk (or reopen the menu).','討伐隊: 帰還時刻を現在にしました。討伐隊員に話しかけてください（メニューを開き直してください）。')
end

-- day / night: the game's own BP_TimeManager_C.SetCurrentTimeZone (EHOLITimeZone Morning=0 / Night=1). It sets the clock to
-- the start of that half of the day ("Set Current Time") and runs ExecuteWeatherAndTimeEvent, which broadcasts
-- ShiftTimeToMorning / ShiftTimeToNight (the lighting change). Switching to morning starts the next day, as when sleeping.
function DQX.toggleDayNight()
  DQX.require()
  local c=UE.needCls('BP_TimeManager_C')
  local tm for _,o in ipairs(UE.instances(c, 4)) do if UE.alive(o) then tm=o break end end
  if not tm then DQX.err('Time manager not found (load a save first).','時間の管理データが見つかりません（先にセーブデータをロードしてください）。') end
  local fG=UE.func(c,'GetCurrentTimeZone') or DQX.err('GetCurrentTimeZone not found.','GetCurrentTimeZone が見つかりません。')
  local fS=UE.func(c,'SetCurrentTimeZone') or DQX.err('SetCurrentTimeZone not found.','SetCurrentTimeZone が見つかりません。')
  local oG, oS = UE.off(fG,'CurrentTimeZone'), UE.off(fS,'CurrentTimeZone')
  local z=DQX.callUFuncs({{obj=tm, func=fG, get=function(p) return rb(p+oG) end}})[1]
  if z~=0 and z~=1 then DQX.err('Unexpected time zone value: '..tostring(z),'想定外の時間帯の値です: '..tostring(z)) end
  local nz=1-z
  DQX.callUFuncs({{obj=tm, func=fS, set=function(p) writeBytes(p+oS, nz) end}})
  if nz==1 then DQX.msg('Day/night: switched to night.','昼夜: 夜にしました。')
  else DQX.msg('Day/night: switched to day (the next day starts, as when sleeping).','昼夜: 昼にしました（眠ったときと同じく次の日になります）。') end
end

----------------------------------------------------------------- 4. Blacksmith / alchemy : recipes
function DQX.learnAllRecipes()
  DQX.require()
  local rows=DQX.allRows('HOLIRecipeBookData')
  if #rows==0 then DQX.err('Recipe book data not found (load a save first).','レシピ帳のデータが見つかりません（先にセーブデータをロードしてください）。') end
  local a=DQX.invMember('MasterRecipeList')
  local data=readQword(a) or 0; local num=readInteger(a+8) or 0
  local have={}
  for i=0,num-1 do have[readInteger(data+i*8)..':'..readInteger(data+i*8+4)]=true end
  local add={}
  for _,r in ipairs(rows) do local k=r.idx..':'..r.num if not have[k] then have[k]=true add[#add+1]=r end end
  if #add==0 then DQX.msg('All recipes are already learned.','すべてのレシピを習得済みです。') return end
  local p=UE.arrayAdd(a, 8, #add)
  for i,r in ipairs(add) do writeInteger(p+(i-1)*8, r.idx) writeInteger(p+(i-1)*8+4, r.num) end
  DQX.msg(('Learned %d recipe books (total %d).'):format(#add, num+#add),('レシピ帳を%d冊習得しました（合計 %d）。'):format(#add, num+#add))
end

----------------------------------------------------------------- 1. Tools : open all chests of a map
-- names of the currently loaded levels: world names, their package paths, and loaded streaming levels
-- (PackageNameToLoad / WorldAsset path). Returned lower-case.
function DQX.loadedWorldNames()
  local r, seen = {}, {}
  local function put(s) if s and s~='' then s=s:lower() if not seen[s] then seen[s]=true r[#r+1]=s end end end
  local wc=UE.cls('World')
  if wc then for _,w in ipairs(UE.instances(wc,512)) do
    put(UE.objName(w))
    local pk=readQword(w+0x20) local g=0
    while pk and pk~=0 and g&lt;4 do put(UE.objName(pk)) pk=readQword(pk+0x20) g=g+1 end
  end end
  local lc=UE.cls('LevelStreaming')
  if lc then
    local oLoaded=UE.off(lc,'LoadedLevel'); local oPkg=UE.off(lc,'PackageNameToLoad'); local oAsset=UE.off(lc,'WorldAsset')
    for _,ls in ipairs(UE.instances(lc,1024)) do
      local lv=readQword(ls+oLoaded)
      if lv and lv~=0 then
        put(UE.nameStr(readInteger(ls+oPkg), readInteger(ls+oPkg+4)))
        put(UE.nameStr(readInteger(ls+oAsset+0x10), readInteger(ls+oAsset+0x14)))
        local lw=readQword(lv+0x20) if lw and lw~=0 then put(UE.objName(lw)) local pk=readQword(lw+0x20) if pk and pk~=0 then put(UE.objName(pk)) end end
      end
    end
  end
  return r
end
-- current map IDs: names of the loaded UWorld objects / streaming levels that look like a map ID (e.g. "r1240")
function DQX.currentMapIds()
  local ids, seen = {}, {}
  local function put(s) if s and not seen[s] then seen[s]=true ids[#ids+1]=s end end
  local wc=UE.cls('World')
  if wc then for _,w in ipairs(UE.instances(wc,64)) do
    local n=(UE.objName(w) or ''):lower() if n:match('^%a%d%d%d%d$') then put(n) end end end
  if #ids==0 then
    for _,w in ipairs(DQX.loadedWorldNames()) do
      local m=w:match('^(%a%d%d%d%d)$') or w:match('/(%a%d%d%d%d)$') if m then put(m) end end
  end
  return ids
end
-- field item ("たからもの") tables of the current map. The game's GenerateTreasureDataList walks
-- HOLICareerHistory.MapDataTable (HOLILDMapData rows) and reads each row's FieldItem soft reference
-- (HOLIFieldItemData rows; row name = ID used by IsTreasureTaken/TakeTreasure, same as chests).
-- The map row is the one whose name is the map ID or whose TreasureBox reference is DT_Holi_&lt;map&gt;_TreasureBox.
function DQX.mapFieldItemTables(ids)
  local ch, ccls = DQX.career()
  local mdt=UE.softObj(ch+UE.off(ccls,'MapDataTable'),'DataTable')
  if not mdt then return nil end
  local mst=UE.needStruct('HOLILDMapData')
  local oFI, oTB = UE.off(mst,'FieldItem'), UE.off(mst,'TreasureBox')
  local want={} for _,id in ipairs(ids) do want[id:lower()]=true want[('dt_holi_'..id..'_treasurebox'):lower()]=true end
  local out, seen = {}, {}
  for _,r in ipairs(DQX.rows(mdt)) do
    local key=(UE.nameStr(r.idx,r.num) or ''):lower()
    local tbp=UE.nameStr(readInteger(r.row+oTB+0x10), readInteger(r.row+oTB+0x14)) or ''
    local tbn=(tbp:match('%.([^%./]+)$') or ''):lower()
    if want[key] or want[tbn] then
      local dt, path = UE.softObj(r.row+oFI,'DataTable')
      if dt and not seen[dt] then seen[dt]=true out[#out+1]={dt=dt, map=key}
      elseif not dt and path and path:find('%.') then out[#out+1]={missing=path, map=key} end
    end
  end
  return out
end
-- chests of the current map = rows of DT_Holi_&lt;map&gt;_TreasureBox (row name = chest ID used by IsTreasureTaken/TakeTreasure)
-- plus the map's field items (たからもの)
function DQX.openMapChests()
  DQX.require()
  DQX.needInv()
  local ids=DQX.currentMapIds()
  if #ids==0 then
    DQX.err('The current map could not be determined.','現在のマップを特定できませんでした。')
  end
  local tst=UE.needStruct('HOLITreasureBoxItemData')
  local f={} for _,n in ipairs({'ITEM0_ID','ITEM0_NUM','ITEM1_ID','ITEM1_NUM','GOLD'}) do f[n]=UE.prop(tst,n) and UE.off(tst,n) or nil end
  local fst=UE.struct('HOLIFieldItemData')
  local fi={} if fst then for _,n in ipairs({'ITEM_ID','ITEM_NUM','GOLD'}) do fi[n]=UE.prop(fst,n) and UE.off(fst,n) or nil end end
  local boxes = {}
  for _,id in ipairs(ids) do
    local tn='DT_Holi_'..id..'_TreasureBox'
    for _,dt in ipairs(UE.find(tn,'DataTable',4)) do
      for _,r in ipairs(DQX.rows(dt)) do boxes[#boxes+1]={bid={r.idx,r.num}, row=r.row, kind='box'} end
    end
  end
  local nBox=#boxes
  local okF, fts = false, nil
  if fst then okF, fts = pcall(DQX.mapFieldItemTables, ids) if not okF then fts=nil end end
  local missing=0
  for _,t in ipairs(fts or {}) do
    if t.dt then for _,r in ipairs(DQX.rows(t.dt)) do boxes[#boxes+1]={bid={r.idx,r.num}, row=r.row, kind='field'} end
    else missing=missing+1 end
  end
  local nField=#boxes-nBox
  local mname=table.concat(ids,', ')
  if #boxes==0 then
    DQX.msg(('%s: this map has no treasure chest or treasure (field item) data.'):format(mname), ('%s: このマップには宝箱・たからもののデータがありません。'):format(mname))
    return
  end
  local ch, ccls = DQX.career()
  local fIs=UE.func(ccls,'IsTreasureTaken') or DQX.err('IsTreasureTaken not found.','IsTreasureTakenが見つかりません。')
  local fTk=UE.func(ccls,'TakeTreasure') or DQX.err('TakeTreasure not found.','TakeTreasureが見つかりません。')
  local oIsId, oIsRet = UE.off(fIs,'ObjectID'), UE.off(fIs,'ReturnValue')
  local oTkId = UE.off(fTk,'ObjectID')
  local q={}
  for _,b in ipairs(boxes) do q[#q+1]={obj=ch, func=fIs,
    set=function(p) writeInteger(p+oIsId,b.bid[1]) writeInteger(p+oIsId+4,b.bid[2]) end,
    get=function(p) return (rb(p+oIsRet) or 0)~=0 end} end
  local taken=DQX.callUFuncs(q)
  local db=DQX.itemDB()
  local gold, items, unknown, takeReq, got = 0, 0, 0, {}, {}
  local opened={box=0, field=0}
  local function give(idx,num,cnt)
    if not idx or idx==0 or not cnt or cnt&lt;=0 then return end
    local it=db.byKey[(idx &amp; 0xFFFFFFFF)..':'..num]
    if it and pcall(DQX.addItem, it, cnt) then items=items+cnt got[#got+1]=it
    else unknown=unknown+1 end
  end
  for i,b in ipairs(boxes) do
    local r=b.row
    if not taken[i] then
      local g=0
      if b.kind=='box' then
        if f.ITEM0_ID then give(readInteger(r+f.ITEM0_ID), readInteger(r+f.ITEM0_ID+4), f.ITEM0_NUM and s32(readInteger(r+f.ITEM0_NUM)) or 1) end
        if f.ITEM1_ID then give(readInteger(r+f.ITEM1_ID), readInteger(r+f.ITEM1_ID+4), f.ITEM1_NUM and s32(readInteger(r+f.ITEM1_NUM)) or 1) end
        g=f.GOLD and s32(readInteger(r+f.GOLD)) or 0
      else
        if fi.ITEM_ID then give(readInteger(r+fi.ITEM_ID), readInteger(r+fi.ITEM_ID+4), fi.ITEM_NUM and s32(readInteger(r+fi.ITEM_NUM)) or 1) end
        g=fi.GOLD and s32(readInteger(r+fi.GOLD)) or 0
      end
      if g and g&gt;0 then gold=gold+g end
      opened[b.kind]=opened[b.kind]+1
      takeReq[#takeReq+1]={obj=ch, func=fTk, set=function(p) writeInteger(p+oTkId,b.bid[1]) writeInteger(p+oTkId+4,b.bid[2]) end}
    end
  end
  if gold&gt;0 then
    local ga=DQX.invBase()+UE.off(UE.needStruct('HOLIMainInventoryData'),'Gold')
    writeInteger(ga, math.min(9999999,(readInteger(ga) or 0)+gold))
  end
  if #takeReq&gt;0 then DQX.callUFuncs(takeReq) end
  DQX.archiveItems(got)
  DQX.invSnapReset()
  local noteE, noteJ = '', ''
  if not fts then noteE, noteJ = ' Treasure (field item) data could not be read.', ' たからもののデータを読めませんでした。'
  elseif missing&gt;0 then noteE, noteJ = (' %d treasure (field item) table(s) not loaded.'):format(missing), ('（たからもののデータ%d件が未読み込み）'):format(missing) end
  DQX.msg(('%s: %d chests opened (%d already open), %d treasures taken (%d already taken), %d items, %d gold, %d unknown contents.%s Objects on screen update after the map is reloaded.'):format(
            mname,opened.box,nBox-opened.box,opened.field,nField-opened.field,items,gold,unknown,noteE),
          ('%s: 宝箱%d個を開けました（開封済み%d個）。たからもの%d個を取得しました（取得済み%d個）。アイテム%d個、%dゴールド、不明な中身%d件。%s画面上の表示はマップを読み直すと反映されます。'):format(
            mname,opened.box,nBox-opened.box,opened.field,nField-opened.field,items,gold,unknown,noteJ))
end


--=====================================================================================
--  [GAME] CASINO
--=====================================================================================
-- live bingo main widget (HOLICasinoBingoMainWidget, created per bingo game)
function DQX.bingoWidget()
  local c=UE.cls('HOLICasinoBingoMainWidget'); if not c then return nil end
  for _,o in ipairs(UE.instances(c, 8)) do if UE.alive(o) then return o, c end end
end
local function objAt(o, cls, name)
  local f=UE.prop(cls, name); if not f then return nil end
  local p=readQword(o+readInteger(f+0x4C)); if p and p~=0 and UE.alive(p) then return p end
end

-- hammer chance opens every square: the number of squares the hammer opens is the native field +3B4 (normal, 7) /
-- +3B8 (raid, 10) of the main widget (set in its constructor, used by ChooseHammerChanceNumber 0x140B9D850; the pool
-- holds the 24 numbers 1-24). The opened list is also passed to the NPC cards.
DQX.BINGO_HAMMER_OFFS = {0x3B4, 0x3B8}
function DQX.bingoHammerTask()
  local w=DQX.bingoWidget(); if not w then return end
  for _,o in ipairs(DQX.BINGO_HAMMER_OFFS) do
    local v=readInteger(w+o)
    if v and v&gt;0 and v&lt;=24 then DQX.patch('bingoHammer', w+o, 'i', 24, w) end
  end
end

-- monster MP always full: WB_BingoMonsterMPPanel_C.SetMP(3) (the game's own setter: CurrentMP + gauge). MP 3 makes the
-- extra open always succeed (0x140BBB500); the game resets it to 0 after a success and this puts it back.
function DQX.bingoMPTask()
  local w,c=DQX.bingoWidget(); if not w then return end
  local p=objAt(w, c, 'MonsterMPPanel'); if not p then return end
  local pc=readQword(p+0x10)
  local fm=UE.prop(pc,'CurrentMP'); local fs=UE.func(pc,'SetMP'); if not fm or not fs then return end
  local mp=readInteger(p+readInteger(fm+0x4C))
  if mp and mp&lt;3 then
    local pv=readQword(fs+0x50) local po=pv and pv~=0 and readInteger(pv+0x4C) or 0
    pcall(DQX.callUFuncs, {{obj=p, func=fs, set=function(a) writeInteger(a+po, 3) end}})
  end
end

-- all lines: every square of the player's card is opened with the game's own HOLIBingoCard.OpenPosNumber(pos), so the
-- game counts the lines itself (UpdateBingoCountAndEffect after each draw). Squares are read with the native
-- GetSquare(card, pos 1-24) (the panels can rotate, so position -&gt; square is resolved each time).
function DQX.bingoLinesTask()
  if (DQX.tickNo % 3)~=0 then return end
  local w,c=DQX.bingoWidget(); if not w then return end
  local card=objAt(w, c, 'BingoCard'); if not card then return end
  local gs=DQX.resolveSig('getSquare'); if not gs then return end
  local sq=UE.cls('HOLIBingoCardSquare'); local cc=UE.cls('HOLIBingoCard'); if not sq or not cc then return end
  local fo=UE.prop(sq,'Opened'); local fn=UE.prop(sq,'Number'); local fop=UE.func(cc,'OpenPosNumber')
  if not fo or not fn or not fop then return end
  local oo,om=UE.boolBit(fo); local on=readInteger(fn+0x4C)
  local calls={} for pos=1,24 do calls[#calls+1]={gs, card, pos} end
  local ok,res=pcall(DQX.gameCallBatch, calls, 4000); if not ok then return end
  local q={}
  local pv=readQword(fop+0x50) local po=pv and pv~=0 and readInteger(pv+0x4C) or 0
  for pos=1,24 do
    local s=res[pos] and res[pos][1]
    if s and s~=0 and UE.alive(s) then
      local num=readInteger(s+on) or 0
      local opened=((readBytes(s+oo,1,false) or 0) &amp; om)~=0
      if num&gt;0 and not opened then q[#q+1]={obj=card, func=fop, set=function(a) writeInteger(a+po, pos) end} end
    end
  end
  if #q&gt;0 then pcall(DQX.callUFuncs, q) end
end

-- monster rebirth (転生) always: glowing ball item 3 = rebirth. After a panel pass the ball draws from
-- BP_BingoBoardManager_C.GlowingBallItemRateExtraOpen (copied from DT_BingoGlowingBallItemRateExtraOpen at BeginPlay
-- and to each glowing ball at every draw): every Item becomes 3 (rates unchanged). The DataTable is patched too so that a
-- new board manager copies the patched values.
function DQX.bingoRebirthTask()
  local st=UE.struct('HOLIBingoGlowingBallItemRate'); if not st then return end
  local oI=UE.off(st,'Item'); local sz=readInteger(st+0x58) or 0x10
  local done=DQX._rebirthDT
  if not done then
    done={}
    for _,r in ipairs(DQX.allRows('HOLIBingoGlowingBallItemRate')) do
      if (UE.objName(r.dt) or ''):find('ExtraOpen') then DQX.patch('bingoRebirth', r.row+oI, 'b', 3, r.dt) end
    end
    DQX._rebirthDT=done
  end
  local bc=UE.cls('BP_BingoBoardManager_C'); if not bc then return end
  local f=UE.prop(bc,'GlowingBallItemRateExtraOpen'); if not f then return end
  for _,bm in ipairs(UE.instances(bc, 4)) do
    if UE.alive(bm) then
      local a=bm+readInteger(f+0x4C) local d=readQword(a) or 0 local n=readInteger(a+8) or 0
      if d~=0 and n&gt;0 and n&lt;32 then for i=0,n-1 do DQX.patch('bingoRebirth', d+i*sz+oI, 'b', 3, bm) end end
    end
  end
end

-- ball bounciness: class pointer of BP_BingoBall_C for the bounce hook
function DQX.bounceTask(force)
  if not force and (DQX.tickNo % 10)~=0 then return end
  local c=UE.cls('BP_BingoBall_C')
  writeQword(DQX.var(0x48), c or 0)
end


-- Rura stone: register every destination of the rura stone registry (GameInstance.RuraPointRegistry,
-- BP_WarpPointRegistryForRuraStone_C) with the native HOLIWarpPointRegistry::RegisterPoint for each row of its PointListTable
-- (DT_RuraPointList). The registration notify (messages) is not fired. The registry is written to the save when you save.
function DQX.ruraUnlockAll()
  DQX.require()
  local cls=UE.needCls('BP_WarpPointRegistryForRuraStone_C')
  local reg=nil for _,o in ipairs(UE.instances(cls, 4)) do if UE.alive(o) then reg=o break end end
  if not reg then DQX.err('Rura stone registry not found (load your save first).','ルーラストーンの登録データが見つかりません（セーブデータをロードしてから実行してください）。') end
  local dt=readQword(reg+UE.off(cls,'PointListTable'))
  if not dt or dt==0 or not UE.alive(dt) then DQX.err('Rura point table not found.','ルーラ先の一覧が見つかりません。') end
  local fR=UE.func(cls,'RegisterPoint') or DQX.err('Function not found: RegisterPoint','関数が見つかりません: RegisterPoint')
  local fQ=UE.func(cls,'ReferPoint') or DQX.err('Function not found: ReferPoint','関数が見つかりません: ReferPoint')
  local rP, rR = UE.off(fR,'PointId'), UE.off(fR,'ReturnValue')
  local qP, qR = UE.off(fQ,'PointId'), UE.off(fQ,'ReturnValue')
  local rows=DQX.rows(dt)
  local ask={}
  for _,r in ipairs(rows) do ask[#ask+1]={obj=reg, func=fQ, set=function(p) writeInteger(p+qP, r.idx) writeInteger(p+qP+4, r.num) end, get=function(p) return (readBytes(p+qR,1) or 0)~=0 end} end
  local had=DQX.callUFuncs(ask)
  local reqs, before = {}, 0
  for i,r in ipairs(rows) do
    if had[i] then before=before+1
    else reqs[#reqs+1]={obj=reg, func=fR, set=function(p) writeInteger(p+rP, r.idx) writeInteger(p+rP+4, r.num) end, get=function(p) return (readBytes(p+rR,1) or 0)~=0 end} end
  end
  local added=0
  if #reqs&gt;0 then for _,v in ipairs(DQX.callUFuncs(reqs)) do if v then added=added+1 end end end
  local after=0 for _,v in ipairs(DQX.callUFuncs(ask)) do if v then after=after+1 end end
  DQX.msg(('Rura stone: %d destinations, %d were registered, %d added, now %d registered.'):format(#rows, before, added, after),
          ('ルーラストーン: 行き先 %d 件のうち登録済み %d 件、新たに %d 件を登録し、登録済みは %d 件になりました。'):format(#rows, before, added, after))
end

-- Catch every fish (BP_Fishing_C catch flow, decoded): for each fish of DT_FishData (ZukanNo &gt; 0) at its maximum size
--   BP_CareerHistory_C.UpdateFishHistory(ZukanNo, Size)                     (fish records / zukan, SetFishHistory +002E)
--   BP_HOLIInventoryManager_C.AddFishItem({FishId, Size, ""})               (fish bag, ubergraph +A056..+A0BB; bag max 100)
--   fishing exp FTrunc(GetTuriExp x 1.0) (SetFishHistory +00A5, without the party exp rate)
-- then the exp is added like "Level Up Sequence": next = DT_FishingLv[Level+1].Exp; at Level &gt;= LockElements.FishingLevelMax
-- the exp becomes 0, else while exp &gt;= next: exp -= next, Level += 1 (BP_HOLIEvtManager_C.CheckReleaseQuestFishing(Level)
-- for each new level); saved with SetCharacterFishingData. Fish coins: AddFishCoin(sum of GetOsakanacoin).
local function oneInst(cn)
  local c=UE.needCls(cn)
  for _,o in ipairs(UE.instances(c, 8)) do if UE.alive(o) and readQword(o+0x10)==c then return o, c end end
  for _,o in ipairs(UE.instances(c, 8)) do if UE.alive(o) then return o, readQword(o+0x10) end end
  DQX.err(cn..' not found (load your save first).', cn..' が見つかりません（セーブデータをロードしてから実行してください）。')
end
local function needFn(c, n) return UE.func(c, n) or DQX.err('Function not found: '..n, '関数が見つかりません: '..n) end
function DQX.fishLevelUp(level, exp, cap, needOf)
  local ups={}
  while true do
    if level&gt;=cap then exp=0 break end
    local nx=needOf(level+1)
    if not nx then exp=0 break end
    if exp&lt;nx then break end
    exp=exp-nx level=level+1 ups[#ups+1]=level
  end
  return level, exp, ups
end
function DQX.fishCatchAll()
  DQX.require()
  local rows, st = DQX.allRows('HOLIFishData')
  local oZ, oS, oE, oC = UE.off(st,'ZukanNo'), UE.off(st,'FishSizeMax'), UE.off(st,'GetTuriExp'), UE.off(st,'GetOsakanacoin')
  local list, seen = {}, {}
  for _,r in ipairs(rows) do
    local z=readInteger(r.row+oZ) or 0 local k=r.idx..':'..r.num
    if z&gt;0 and not seen[k] then seen[k]=true
      list[#list+1]={idx=r.idx, num=r.num, z=z, size=readFloat(r.row+oS) or 0, exp=math.floor(readFloat(r.row+oE) or 0), coin=readInteger(r.row+oC) or 0} end
  end
  if #list==0 then DQX.err('Fish data not found.','魚のデータが見つかりません。') end
  local ch, chc = oneInst('BP_CareerHistory_C')
  local inv, invc = oneInst('BP_HOLIInventoryManager_C')
  local pm, pmc = oneInst('BP_HOLIPartyDataManager_C')
  local fH, fA, fCoin = needFn(chc,'UpdateFishHistory'), needFn(invc,'AddFishItem'), needFn(invc,'AddFishCoin')
  local fG, fS = needFn(pmc,'GetCharacterFishingData'), needFn(pmc,'SetCharacterFishingData')
  local hZ, hS = UE.off(fH,'ZukanNo'), UE.off(fH,'Size')
  local aI, aR = UE.off(fA,'FishItem'), UE.off(fA,'Success')
  local cA, cR = UE.off(fCoin,'AddCoin'), UE.off(fCoin,'bSuccess')
  local gD, sD = UE.off(fG,'FishingData'), UE.off(fS,'FishingData')
  -- level table and cap
  local lvRow={} local oLx
  for _,dt in ipairs(UE.instances(UE.needCls('DataTable'), 4096)) do
    if UE.objName(dt)=='DT_FishingLv' and UE.alive(dt) then
      local rs=readQword(dt+0x28) local f=readQword(rs+0x50)
      while f and f~=0 do if (UE.fieldName(f) or ''):find('^Exp_') then oLx=readInteger(f+0x4C) end f=readQword(f+0x20) end
      if oLx then for _,r in ipairs(DQX.rows(dt)) do local n=tonumber(UE.nameStr(r.idx,r.num) or '') if n then lvRow[n]=readInteger(r.row+oLx) end end end
      break
    end
  end
  if not oLx then DQX.err('DT_FishingLv not found.','釣りレベル表（DT_FishingLv）が見つかりません。') end
  local le=oneInst('HOLILockElements') local cap=readInteger(le+UE.off(UE.needCls('HOLILockElements'),'FishingLevelMax')) or 0
  -- current fishing data (copied whole: Level, ExperiencePoint, rod, lure)
  local cur=DQX.callUFuncs({{obj=pm, func=fG, get=function(p) return readBytes(p+gD, 0x18, true) end}})[1]
  if type(cur)~='table' or #cur&lt;0x18 then DQX.err('Fishing data could not be read.','釣りデータを読み取れませんでした。') end
  local function i32(b,o) local v=b[o+1]|(b[o+2]&lt;&lt;8)|(b[o+3]&lt;&lt;16)|(b[o+4]&lt;&lt;24) if v&gt;=0x80000000 then v=v-0x100000000 end return v end
  local lv0, ex0 = i32(cur,0), i32(cur,4)
  -- records and fish
  local rq={}
  for _,x in ipairs(list) do
    rq[#rq+1]={obj=ch, func=fH, set=function(p) writeInteger(p+hZ, x.z) writeFloat(p+hS, x.size) end}
    rq[#rq+1]={obj=inv, func=fA, set=function(p) writeInteger(p+aI, x.idx) writeInteger(p+aI+4, x.num) writeFloat(p+aI+8, x.size) end,
               get=function(p) return (readBytes(p+aR,1) or 0)~=0 end}
  end
  local res=DQX.callUFuncs(rq)
  local got=0 for i=2,#res,2 do if res[i]==true then got=got+1 end end
  -- exp and level
  local add, coins = 0, 0 for _,x in ipairs(list) do add=add+x.exp coins=coins+x.coin end
  local lv, ex, ups = DQX.fishLevelUp(lv0, ex0+add, cap, function(n) return lvRow[n] end)
  DQX.callUFuncs({{obj=pm, func=fS, set=function(p) writeBytes(p+sD, cur) writeInteger(p+sD, lv) writeInteger(p+sD+4, ex) end}})
  local em
  local gc=UE.cls('BP_HOLIGameMode_C') local pe=gc and UE.prop(gc,'EvtManager')
  for _,gm in ipairs(gc and UE.instances(gc,4) or {}) do local o=pe and readQword(gm+readInteger(pe+0x4C)) if o and o~=0 and UE.alive(o) then em=o break end end
  local fQ=em and UE.func(readQword(em+0x10),'CheckReleaseQuestFishing')
  if fQ and #ups&gt;0 then local qL=UE.off(fQ,'Level') local q={}
    for _,l in ipairs(ups) do q[#q+1]={obj=em, func=fQ, set=function(p) writeInteger(p+qL, l) end} end
    DQX.callUFuncs(q) end
  local okC=DQX.callUFuncs({{obj=inv, func=fCoin, set=function(p) writeInteger(p+cA, coins) end, get=function(p) return (readBytes(p+cR,1) or 0)~=0 end}})[1]
  DQX.msg(('Catch every fish: %d fish recorded at max size, %d added to the fish bag (%d did not fit), fishing exp +%d (Lv %d -&gt; %d, cap %d), fish coins +%d%s.')
            :format(#list, got, #list-got, add, lv0, lv, cap, coins, okC and '' or ' (NOT added: over the limit)'),
          ('全種類の魚を釣る: %d種を最大サイズで記録、魚の袋に %d匹追加（入りきらず %d匹）、釣り経験値 +%d（Lv %d → %d、上限 %d）、おさかなコイン +%d%s。')
            :format(#list, got, #list-got, add, lv0, lv, cap, coins, okC and '' or '（上限を超えるため追加されませんでした）'))
end

-- All monsters defeated 100 times (monster list / kill records): every ZukanNo of DT_MonsterData
-- (HOLIBattleMonsterData.MonsterDetails.ZukanNo &gt; 0, ToubetuExclude off) gets
-- HOLICareerHistory::RecordToubatuMonster(ZukanNo, 100 - current, Miyaburu = true), the call the game makes after a battle
-- (BP_HOLIBattleManager_C.updateZukan +1021; it adds the count). EditToubatuMonsterRecord changed nothing in game (127
-- analysis: 85 records read, none changed). Monsters already at 100+ are left as they are. The current counts are read
-- from CareerHistory.ToubatuMonsterRecords (TMap&lt;int, HOLIToubatuMonsterRecord{ToubatuCount, MiyaburuFlag}&gt;).
function DQX.monsterAll100()
  DQX.require()
  local rows, st = DQX.allRows('HOLIBattleMonsterData')
  local oD=UE.off(st,'MonsterDetails') local ds=UE.innerStruct(UE.needProp(st,'MonsterDetails'))
  local oZ = UE.off(ds,'ZukanNo')
  local fX=UE.needProp(ds,'ToubetuExclude') local xb, xm = UE.boolBit(fX)
  local set, list = {}, {}
  for _,r in ipairs(rows) do
    local d=r.row+oD local z=readInteger(d+oZ) or 0
    local ex=((readBytes(d+xb,1) or 0)&amp;xm)~=0
    if z&gt;0 and z&lt;100000 and not ex and not set[z] then set[z]=true list[#list+1]=z end
  end
  table.sort(list)
  if #list==0 then DQX.err('Monster data not found.','モンスターのデータが見つかりません。') end
  local c=UE.needCls('BP_CareerHistory_C') local ch
  for _,o in ipairs(UE.instances(c, 8)) do if UE.alive(o) then ch=o break end end
  if not ch then DQX.err('Career history not found (load your save first).','冒険の記録が見つかりません（セーブデータをロードしてから実行してください）。') end
  local fE=UE.func(c,'RecordToubatuMonster') or DQX.err('Function not found: RecordToubatuMonster','関数が見つかりません: RecordToubatuMonster')
  local pZ, pC, pM = UE.off(fE,'ZukanNo'), UE.off(fE,'Count'), UE.off(fE,'Miyaburu')
  local mp=UE.needProp(c,'ToubatuMonsterRecords') local L=UE.mapLayout(mp, 4, 20)
  local function counts()
    local t={} UE.mapIter(ch+readInteger(mp+0x4C), L, function(i,e) t[readInteger(e) or -1]=readInteger(e+L.vo) or 0 end) return t
  end
  local cur=counts()
  local rq, before = {}, 0
  for _,z in ipairs(list) do
    local v=cur[z] or 0 if v&gt;=100 then before=before+1 end
    local add=100-v
    if add&gt;0 then rq[#rq+1]={obj=ch, func=fE, set=function(p) writeInteger(p+pZ, z) writeInteger(p+pC, add) writeBytes(p+pM, 1) end} end
  end
  if #rq&gt;0 then DQX.callUFuncs(rq) end
  local aft=counts() local after=0 for _,z in ipairs(list) do if (aft[z] or 0)&gt;=100 then after=after+1 end end
  DQX.msg(('Monsters: %d monster types, %d had 100+ defeats, now %d have 100+ (marked as seen through).'):format(#list, before, after),
          ('モンスター: %d種のうち討伐100匹以上は %d種でしたが、%d種になりました（みやぶった扱いにもしました）。'):format(#list, before, after))
end

-- All titles and achievements: HOLICareerHistory::TakeSyougou(SyougouId) for every row of DT_SyougouData; the queue of
-- "title obtained" notices (DisplaySyougouQueue) is emptied afterwards so 600+ notices are not shown one by one.
-- Achievements (Steam): BP_HOLIGameInstance_C.TakeTrophy(TrophyID) for every ACH_nnn name of the game, the same call the
-- game makes in UpdateStillLockedTrophy for a taken title whose trophy is still locked (it queues the unlock and sends it
-- to Steam on its timer; already unlocked ones are skipped by the game).
function DQX.achvAll()
  DQX.require()
  local c=UE.needCls('BP_CareerHistory_C') local ch
  for _,o in ipairs(UE.instances(c, 8)) do if UE.alive(o) then ch=o break end end
  if not ch then DQX.err('Career history not found (load your save first).','冒険の記録が見つかりません（セーブデータをロードしてから実行してください）。') end
  local fT=UE.func(c,'TakeSyougou') or DQX.err('Function not found: TakeSyougou','関数が見つかりません: TakeSyougou')
  local fN=UE.func(c,'CalcTakenSyougouCount')
  local tI=UE.off(fT,'SyougouId')
  local dt
  for _,x in ipairs(UE.instances(UE.needCls('DataTable'), 4096)) do if UE.objName(x)=='DT_SyougouData' and UE.alive(x) then dt=x break end end
  if not dt then DQX.err('Title table (DT_SyougouData) not found.','称号の一覧（DT_SyougouData）が見つかりません。') end
  local rs=readQword(dt+0x28) local oTr=UE.off(rs,'TrophyID')
  local rows=DQX.rows(dt)
  local function taken() if not fN then return -1 end local r=DQX.callUFuncs({{obj=ch, func=fN, get=function(p) return readInteger(p+UE.off(fN,'ReturnValue')) end}})[1] return r end
  local n0=taken()
  local rq, ach, seen = {}, {}, {}
  for _,r in ipairs(rows) do
    rq[#rq+1]={obj=ch, func=fT, set=function(p) writeInteger(p+tI, r.idx) writeInteger(p+tI+4, r.num) end}
    local ti, tn = readInteger(r.row+oTr), readInteger(r.row+oTr+4)
    local nm=UE.nameStr(ti, tn)
    if nm and nm:find('^ACH_') and not seen[nm] then seen[nm]=true ach[#ach+1]={idx=ti, num=tn, name=nm} end
  end
  DQX.callUFuncs(rq)
  local q=UE.prop(c,'DisplaySyougouQueue') if q then writeInteger(ch+readInteger(q+0x4C)+8, 0) end
  local n1=taken()
  for i=1,200 do local nm=('ACH_%03d'):format(i)
    if not seen[nm] then local idx=UE.nameIdx(nm) if idx then seen[nm]=true ach[#ach+1]={idx=idx, num=0, name=nm} end end end
  local gc=UE.needCls('BP_HOLIGameInstance_C') local gi
  for _,o in ipairs(UE.instances(gc, 4)) do if UE.alive(o) then gi=o break end end
  local fK=gi and UE.func(gc,'TakeTrophy')
  local sent=0
  if fK then local kI=UE.off(fK,'TrophyID') local tq={}
    for _,a in ipairs(ach) do tq[#tq+1]={obj=gi, func=fK, set=function(p) writeInteger(p+kI, a.idx) writeInteger(p+kI+4, a.num) end} end
    DQX.callUFuncs(tq) sent=#tq end
  DQX.msg(('Titles: %d in the table, taken %d -&gt; %d. Achievements: %d requested (sent to Steam by the game in order).'):format(#rows, n0, n1, sent),
          ('称号: 一覧 %d件、取得済み %d件 → %d件。実績: %d件の解除を依頼しました（ゲームが順番にSteamへ送ります）。'):format(#rows, n0, n1, sent))
end

-- Collection item list (収集アイテムリスト) without getting the items: the list is HOLIInventoryManager.ItemArchives, filled
-- by the native AddEquipmentToArchives / AddToolItemToArchives / AddKeyItemToArchives(ItemId). The item IDs are the keys of
-- the inventory manager's own catalogues EquipPrimaryAssetNameAll / CommonItemPrimaryAssetNameAll /
-- KeyItemPrimaryAssetNameAll. [130] Their keys are primary asset names "PA_ID_ITEM_..." while the archive lists hold item IDs
-- "ID_ITEM_..." (130 analysis); the item ID is the key without "PA_" and is used only when that name exists in the game.
-- Version 129 added the "PA_..." keys themselves: they are removed from the three lists first (TArray&lt;FName&gt;, compacted).
function DQX.archiveAll()
  DQX.require()
  local c=UE.needCls('BP_HOLIInventoryManager_C') local inv
  for _,o in ipairs(UE.instances(c, 8)) do if UE.alive(o) then inv=o break end end
  if not inv then DQX.err('Inventory manager not found (load your save first).','インベントリが見つかりません（セーブデータをロードしてから実行してください）。') end
  local sets={{'EquipPrimaryAssetNameAll','AddEquipmentToArchives','HasEquipmentInArchives','equipment','装備品'},
              {'CommonItemPrimaryAssetNameAll','AddToolItemToArchives','HasToolItemInArchives','items','どうぐ・素材など'},
              {'KeyItemPrimaryAssetNameAll','AddKeyItemToArchives','HasKeyItemInArchives','key items','だいじなもの'}}
  local removed=DQX.archiveCleanPA(c, inv)
  local en, ja = {}, {}
  for _,s in ipairs(sets) do
    local mp=UE.prop(c, s[1]) local fA=UE.func(c, s[2]) local fH=UE.func(c, s[3])
    if mp and fA then
      local L=UE.mapLayout(mp, 8, 24)
      local ids={}
      if L.vo&gt;=8 then
        UE.mapIter(inv+readInteger(mp+0x4C), L, function(i,e)
          local idx, num = readInteger(e), readInteger(e+4)
          local nm=UE.nameStr(idx, num)
          local id=nm and nm:match('^PA_(ID_[%w_%-]+)$')
          local ii=id and UE.nameIdx(id)
          if ii then ids[#ids+1]={idx=ii, num=0} end
        end)
      end
      local aI=UE.off(fA,'ItemId')
      local function has()
        if not fH then return -1 end local hI, hR = UE.off(fH,'ItemId'), UE.off(fH,'ReturnValue') local q={}
        for _,x in ipairs(ids) do q[#q+1]={obj=inv, func=fH, set=function(p) writeInteger(p+hI, x.idx) writeInteger(p+hI+4, x.num) end, get=function(p) return (readBytes(p+hR,1) or 0)~=0 end} end
        local n=0 for _,v in ipairs(#q&gt;0 and DQX.callUFuncs(q) or {}) do if v==true then n=n+1 end end return n
      end
      local b=has()
      local q={} for _,x in ipairs(ids) do q[#q+1]={obj=inv, func=fA, set=function(p) writeInteger(p+aI, x.idx) writeInteger(p+aI+4, x.num) end} end
      if #q&gt;0 then DQX.callUFuncs(q) end
      local a=has()
      en[#en+1]=('%s %d -&gt; %d of %d'):format(s[4], b, a, #ids) ja[#ja+1]=('%s %d → %d件（全%d件）'):format(s[5], b, a, #ids)
    else
      en[#en+1]=s[4]..' not found' ja[#ja+1]=s[5]..' 見つかりません'
    end
  end
  if removed&gt;0 then en[#en+1]=('%d wrong entries of version 129 removed'):format(removed) ja[#ja+1]=('129版で誤って登録した %d件を削除'):format(removed) end
  DQX.msg('Collection list (items are not given): '..table.concat(en,', ')..'.', '収集アイテムリスト（アイテムは増えません）: '..table.concat(ja,'、')..'。')
end

-- remove "PA_..." names (added by version 129) from ItemArchives.ToolItemList / KeyItemList / EquipmentList (TArray&lt;FName&gt;)
function DQX.archiveCleanPA(c, inv)
  local ap=UE.prop(c,'ItemArchives') if not ap then return 0 end
  local as=UE.innerStruct(ap) local base=inv+readInteger(ap+0x4C) local n=0
  for _,ln in ipairs({'ToolItemList','KeyItemList','EquipmentList'}) do
    local lp=UE.prop(as, ln)
    if lp and UE.fieldClass(lp)=='ArrayProperty' then
      local a=base+readInteger(lp+0x4C) local d=readQword(a) local cnt=readInteger(a+8) or 0
      if d and d~=0 and cnt&gt;0 and cnt&lt;100000 then
        local w=0
        for i=0,cnt-1 do
          local idx, num = readInteger(d+i*8), readInteger(d+i*8+4)
          local nm=UE.nameStr(idx, num) or ''
          if nm:find('^PA_') then n=n+1 else
            if w~=i then writeInteger(d+w*8, idx) writeInteger(d+w*8+4, num) end
            w=w+1 end
        end
        if w~=cnt then writeInteger(a+8, w) end
      end
    end
  end
  return n
end

----------------------------------------------------------------- [GAME] record names (ID -&gt; {en, ja})
DQX.NAMES = {
  [1] = {'[ATTACH / ALL OFF] Attach to DQXOffline.exe + disable everything', '【アタッチ／一括解除】DQXOffline.exeにアタッチ＋全機能オフ'},
  [2] = {'0. Settings &amp; README', '0. 設定・説明'},
  [3] = {'Language / 言語設定', '言語設定 / Language'},
  [4] = {'English (default)', 'English（既定）'},
  [5] = {'日本語 / Japanese', '日本語 / Japanese'},
  [6] = {'README', 'はじめに'},
  [7] = {'1) Load your save, then run [ATTACH / ALL OFF] at the top of the table.', '1) セーブデータをロードしてから、先頭の【アタッチ／一括解除】を実行してください。'},
  [8] = {'2) For game v2.0.1.0 (Steam, 2023-05-10 build). On other builds, features not found are disabled.', '2) 対応：2.0.1.0（Steam版 2023-05-10ビルド）。別ビルドでは一部の機能が無効になります。'},
  [9] = {'3) Orange = script (check to run / toggle), cyan = group, no color = value (double-click to edit).', '3) オレンジ＝スクリプト、水色＝グループ、色なし＝値（ダブルクリックで入力）。'},
  [10] = {'4) Messages appear in the Lua Engine window (0. &gt; Diagnostics &gt; Lua Engine).', '4) メッセージは Lua Engine に表示されます（0. ＞ 診断 ＞ Lua Engine）。'},
  [11] = {'5) Save to another slot before using editors and one-shot actions.', '5) 書き換え系の操作の前に、別スロットにセーブしてください。'},
  [12] = {'6) Data changes (drops, chests, fish, recipes, multipliers) are undone when turned off.', '6) データ書き換え系（ドロップ・魚・レシピ・倍率など）は OFF にすると元に戻ります。'},
  [13] = {'7) Some features call game functions: do not pause or minimize the game while using them.', '7) 一部の機能はゲームの関数を呼びます。使用中は一時停止・最小化しないでください。'},
  [15] = {'Diagnostics (only when the table does not work)', '診断（動作しないときのみ）'},
  [16] = {'Lua Engine (check = open / uncheck = close)', 'Lua Engine（チェックで開く／外すと閉じる）'},
  [17] = {'AOB scan self-test (CE is busy for a while; prints to Lua Engine)', 'AOBスキャンテスト（しばらくCEが応答しません。結果はLua Engineに表示）'},
  [18] = {'Environment / connection info (CE, process, module, build)', '環境・接続情報（CE・プロセス・モジュール・ビルド）'},
  [19] = {'Status: unresolved code / active hooks / active features', '状態：未検出コード／有効フック／有効機能'},
  [20] = {'Add key items... (CAUTION: event flags are not set)', 'だいじなもの追加...（注意：イベントフラグは立ちません）'},
  [100] = {'01. Gameplay', '01. Gameplay'},
  [101] = {'Invincible in battle (party takes no damage, HP/MP always full)', '戦闘無敵（味方はダメージを受けない・HP/MPまんたんで減らない）'},
  [102] = {'One-hit kill (damage to enemies)', 'ワンヒットキル（敵へのダメージ）'},
  [103] = {'Damage dealt to enemies', '敵に与えるダメージ'},
  [104] = {'Items never decrease (use items / mix bag)', 'アイテム減らない（つかうもの・おまとめぶくろ）'},
  [105] = {'Always critical (party)', 'いつもクリティカル攻撃（味方）'},
  [107] = {'Enemies always drop items (drop rate 100%)', 'いつも敵は何かを落とす（ドロップ率100%）'},
  [108] = {'Always "see through" monsters (Miyaburu)', 'いつもモンスターみやぶる'},
  [109] = {'Open without keys', 'カギなしで開けられる'},
  [110] = {'Easy fishing', '釣り簡単'},
  [111] = {'Fish always maximum size', 'いつもおさかな最大'},
  [112] = {'Fish always minimum size', 'いつもおさかな最小'},
  [113] = {'Take all treasure chests in the map', 'マップ内の全宝箱を取得'},
  [115] = {'EXP multiplier (battle results)', '経験値の倍率（戦闘結果）'},
  [116] = {'EXP multiplier', '経験値倍率'},
  [117] = {'Gold multiplier (every gold gain)', '金額の倍率（すべてのゴールド入手）'},
  [118] = {'Gold multiplier (0.1 steps)', '金額倍率（0.1刻み）'},
  [119] = {'Item multiplier (every item gain)', 'アイテムの倍率（すべての入手）'},
  [120] = {'Item multiplier (integer 1-100)', 'アイテム倍率（整数 1～100）'},
  [121] = {'Movement speed multiplier (player)', '移動スピードの倍率（プレイヤー）'},
  [122] = {'Speed multiplier (0.1 steps, e.g. 1.5)', 'スピード倍率（0.1刻み 例: 1.5）'},
  [123] = {'Sparkles can be picked again and again', 'きらきらを何回でも拾える'},
  [124] = {'Always catch enemies off guard', 'いつも不意をつく'},
  [125] = {'Always steal (every party member, from each monster it defeats)', 'いつも盗む（盗賊以外も・倒した敵から）'},
  [126] = {'Always super high tension (party)', 'いつもスーパーハイテンション（味方）'},
  [127] = {'Switch day / night', '昼夜を入れ替える'},
  [128] = {'Expedition (subjugation corps) returns now', '討伐隊をすぐ帰還させる'},
  [129] = {'Dolboard anywhere (ride in towns too)', 'どこでもドルボード（街の中でも乗れる）'},
  [130] = {'Rura stone usable anywhere', 'どこでもルーラストーン'},
  [131] = {'Rura stone: register all destinations', 'ルーラストーン全解除（行き先をすべて登録）'},
  [132] = {'Rare fish first (more likely, not guaranteed)', 'レア度優先（レアな魚が出やすい・必ずではない）'},
  [133] = {'Catch a chosen fish (pick a fish; any spot)...', '指定の魚を釣る（魚を選択・釣り場に関係なく）...'},
  [134] = {'Catch every fish (records, fish bag, fishing exp, fish coins)', '全種類の魚を釣る（図鑑・魚・釣り経験値・おさかなコイン）'},
  [135] = {'All monsters: 100 defeats each (kill records)', '全モンスターを100匹討伐済みにする（討伐記録）'},
  [136] = {'Get all titles and achievements (Steam)', '全称号・全実績（Steam）を取得'},
  [137] = {'Fill the collection item list (items are not given)', '収集アイテムリストを埋める（アイテムは入手しない）'},
  [138] = {'Invincible in battle 2 (no damage, MP always full, HP kept at 1/4)', '戦闘無敵2（味方はダメージを受けない・MPまんたん・HPは4分の1で維持）'},
  [139] = {'Maximum number of monsters in battle (symbol encounters)', '戦闘時にモンスター数最大（シンボルエンカウント）'},
  [145] = {'Reincarnated (tensei) monster lottery always wins (symbol encounters)', '転生モンスターの抽選を最大にする（必ず当たる・シンボルエンカウント）'},
  [140] = {'Battle', '戦闘'},
  [141] = {'Field &amp; travel', 'フィールド・移動'},
  [142] = {'Fishing', '釣り'},
  [143] = {'Items &amp; gain multipliers', 'アイテム・取得倍率'},
  [144] = {'Other', 'その他'},
  [200] = {'02. User', '02. ユーザ'},
  [201] = {'Money &amp; coins (enable to list)', '所持金・コイン（有効化で一覧表示）'},
  [202] = {'Party parameters (enable to list)', 'パーティの各種パラメータ（有効化で一覧表示）'},
  [203] = {'Add skill points...', 'スキルポイント追加...'},
  [204] = {'Add fishing EXP (fishing points)...', '釣りポイント（つり経験値）追加...'},
  [300] = {'03. Items', '03. アイテム'},
  [301] = {'Item counts (enable to list)', '保有アイテム個数（有効化で一覧表示）'},
  [302] = {'Add items (window: category -&gt; select several -&gt; quantity)...', 'アイテム追加（画面：分類→複数選択→個数）...'},
  [304] = {'Smithing quality of equipment (window: select -&gt; none / +1 / +2 / +3)...', 'そうびのできのよさ（画面：選択→なし／+1／+2／+3）...'},
  [305] = {'Accessory synthesis (window: select -&gt; effect per slot -&gt; none / +1 to +4)...', 'アクセサリの合成（画面：選択→枠ごとの効果→なし／+1〜+4）...'},
  [400] = {'04. Blacksmith, Alchemy &amp; Synthesis', '04. 鍛冶・錬金・合成'},
  [401] = {'Materials are not consumed', 'アイテム消費しない'},
  [402] = {'No materials required (recipe materials = 0)', '必要アイテムいらない（レシピの必要数を0にする）'},
  [403] = {'Learn all recipes', 'レシピ全取得'},
  [404] = {'Blacksmith: finish in one go (gauges kept at target; choose "Finish")', '鍛冶で一発で仕上げる（ゲージを目標値に保つ・「しあげる」を選ぶ）'},
  [500] = {'05. Casino', '05. カジノ'},
  [501] = {'Coins never decrease (bets are free; payouts are added as usual)', 'コイン減らない（賭けたコインが減らない・配当は通常どおり）'},
  [502] = {'Slot: always 7-7-7-7-7 (during metal spin: Jackpot)', 'スロットで必ず 77777（メタルスピン中はジャックポット）'},
  [503] = {'Roulette: always 0 (treasure chest pocket)', 'ルーレットで必ず 0（宝箱のポケット）'},
  [504] = {'Roulette: always 4', 'ルーレットで必ず 4'},
  [505] = {'Bingo: hammer chance opens every number (NPC cards too)', 'ビンゴでハンマーチャンスですべて開く（NPCのカードも）'},
  [506] = {'Bingo: balls never decrease (leave the final round from the menu)', 'ビンゴでボール減らない（決勝はメニューから抜けてください）'},
  [507] = {'Bingo: monster MP always full', 'ビンゴでモンスターMPまんたん'},
  [508] = {'Bingo: all lines complete (every square of your card opens)', 'ビンゴで全ラインそろう（自分のカードの全マスを開く）'},
  [509] = {'Bingo: monster rebirth always (every passed monster panel is reborn)', 'ビンゴで必ずモンスター転生（通過したモンスターパネルは必ず転生）'},
  [510] = {'Bingo: ball bounciness multiplier', 'ビンゴでボールの跳ね返り係数操作'},
  [511] = {'Bounciness multiplier (default 5)', '跳ね返り係数の倍率（既定 5）'},
  [512] = {'Always tension up (slot / poker; the casino has no other tension state)', 'いつもテンションアップ（スロット・ポーカー）'},
  [513] = {'Slot: metal spins never decrease', 'スロットでメタルスピンが減らない'},
  [514] = {'Slot: always metal spin (metal x5 when not in a metal spin; +30 spins)', 'スロットで必ずメタルスピン（メタル5揃い・+30回）'},
}

pcall(DQX.initCollapse)
DQX.applyLang()
</LuaScript>
</CheatTable>
