<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>siao.ai blog</title>
    <link>https://blog.siao.ai/</link>
    <atom:link href="https://blog.siao.ai/feed.xml" rel="self" type="application/rss+xml"/>
    <description>siao.ai blog</description>
    <language>zh-Hant</language>
    <lastBuildDate>Mon, 24 Aug 2026 00:00:00 GMT</lastBuildDate>
    <item>
      <title>重言 Re-Sinitic：形聲為什麼沒有湧現 - 研究日誌（一）</title>
      <link>https://blog.siao.ai/re-sinitic-01/</link>
      <guid isPermaLink="true">https://blog.siao.ai/re-sinitic-01/</guid>
      <pubDate>Mon, 24 Aug 2026 00:00:00 GMT</pubDate>
      <description>固定漢語的語意結構，從零重新演化語音與書寫層，問形聲是不是雙通道文字系統的必然收斂解。三個階段測試下來，形聲都沒有出現——這是探索階段的第一集記錄，S3 才剛開始，還沒有 confirmatory run。</description>
      <content:encoded><![CDATA[<p>這是「重言 Re-Sinitic」的第一篇研究日誌。專案還在探索階段，S3 才剛跑起來、confirmatory run 還沒做——這篇記的是目前為止量到的東西，不是最終結論。後面應該還會有第二集、第三集。</p>
<h2 id="這個專案在問什麼">這個專案在問什麼</h2>
<p>固定漢語的語意結構（meaning space）當控制變數，從零重新演化語言的表達層——音韻與書寫。問一件具體的事：</p>
<blockquote>
<p>形聲——一個字由表義部件加表音部件組成——是不是雙通道語言系統的必然收斂解？如果不是，它需要什麼前提？</p>
</blockquote>
<p>文獻上的空白很具體：現有的符號湧現研究全部停在「圖畫 → 抽象符號」，沒有人碰過形音義三元結構。</p>
<p>「做完」長什麼樣：S1 離散語音層跑出可信的音系 → S2 書寫層檢定形聲是否湧現 → S3 檢定方言壓力的作用，用 2×2 設計分辨「同音壓力」與「方言壓力」哪個才是必要條件。</p>
<h2 id="主結論-先講重點">主結論（先講重點）</h2>
<blockquote>
<p>在本研究測試的所有條件下，形聲都沒有湧現。</p>
</blockquote>
<p>兩種理論上應該產生它的壓力各自產生了「一半」，而且是相反的一半：同音壓力讓語音資訊<strong>進入</strong>字形，方言壓力讓語音資訊<strong>退出</strong>字形。兩者都不產生「分工」。</p>
<p>形聲的定義性特徵不是「字形帶語音資訊」，是<strong>字形可分解為兩個功能不同的部分</strong>。這裡的操作化是雙重分離——遮蔽語意路只傷語意 Reader、遮蔽語音路只傷語音 Reader。那個交互作用在任何條件下都沒有變正。</p>
<h2 id="s1-語音層可以從零演化出來">S1：語音層可以從零演化出來</h2>
<p>226 個詞、10 子音 8 母音、訊息長度 12、CV 音節模板、一個 D3 混淆通道。最佳設定下溝通成功率 72%、topsim 0.088–0.158、有同音詞的詞只有 1.3%。</p>
<p>一個值得記的量測錯誤：齊夫簡短律（M2，越常用的詞越短）的效果量一開始被低估了大約五倍。原因是訊息長度有硬上界，而 65–76% 的詞卡在那個上界——這是設限依變數（censored dependent variable），該用 Tobit 迴歸而不是 OLS。用 OLS 會把斜率往零壓，看起來效應很弱，其實是量測方式本身在說謊。</p>
<h2 id="s2-同音壓力有門檻-但門檻之後走向拼音化">S2：同音壓力有門檻，但門檻之後走向拼音化</h2>
<p>6 個劑量 × 3 種子，自變數是<strong>湧現的</strong>同音率（0.013 → 0.496），結果變數是「主力 − 同格位純表意基線」的聲符式共用效果量。</p>
<pre tabindex="0"><code>同音率 &#x3C; 0.2（n=12）   平均 −0.028 ± 0.096   t = −1.00   ← 沒有效應
同音率 > 0.2（n=6）    平均 +0.494 ± 0.207   t = +5.84   ← 強效應
</code></pre>
<p>是門檻，不是斜坡：純門檻模型 R² = 0.775 勝過純線性的 0.730，兩者一起放進模型時只有門檻項活下來。</p>
<p>但門檻之後長出來的不是形聲。隨劑量升高：</p>
<table tabindex="0">
<thead>
<tr>
<th></th>
<th>趨勢</th>
</tr>
</thead>
<tbody>
<tr>
<td>聲符純度</td>
<td>0.054 → 0.178（上升）</td>
</tr>
<tr>
<td>形符純度</td>
<td>0.120 → 0.011（崩塌）</td>
</tr>
<tr>
<td>雙重分離</td>
<td>+0.002 → −0.038（更負）</td>
</tr>
</tbody>
</table>
<p>字形越來越像在編碼發音，而不是分化成「一半表義、一半表音」。那是拼音化的方向，不是形聲化的方向。</p>
<h2 id="s3-方言壓力讓-writer-放棄語音路-而不是把它隔離出來">S3：方言壓力讓 Writer 放棄語音路，而不是把它隔離出來</h2>
<p>3 種子 × 6 劑量，自變數是<strong>湧現的</strong>跨子群語音距離 D（0 → 0.556）。</p>
<table tabindex="0">
<thead>
<tr>
<th></th>
<th align="right">係數</th>
<th align="right">t 值</th>
<th>架構書 §5.1 的預測</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>形符純度 ~ D</td>
<td align="right">+0.011</td>
<td align="right">+0.13</td>
<td>為正（形符強化）</td>
<td>✗</td>
</tr>
<tr>
<td>聲符純度 ~ D</td>
<td align="right">−0.124</td>
<td align="right">−2.62</td>
<td>為負（聲符退化）</td>
<td>✓</td>
</tr>
<tr>
<td>消融損失 ~ D</td>
<td align="right">−0.176</td>
<td align="right">−5.41</td>
<td>—</td>
<td>—</td>
</tr>
</tbody>
</table>
<p>最強的訊號是消融：遮掉語音路造成的損失隨 D 單調下降，在最高劑量甚至變負——那條路已經是純粹的噪音，遮不遮都無所謂。</p>
<p>跨方言壓力不會讓書寫系統重組成形符加聲符，它讓 Writer 逐步、完全地放棄語音通道。形符純度沒有上升，是因為語意路本來就在做全部的工作，沒有東西可以「強化」。</p>
<h2 id="為什麼沒有湧現-一個可檢定的假設">為什麼沒有湧現：一個可檢定的假設</h2>
<p>三個階段的結果指向同一件事：系統從來沒有理由「同時」使用兩條通道。它總是選一條——同音壓力下選語音，方言壓力下選語意。</p>
<p>分工要划算，必須有某種力量讓「兩條各做一半」勝過「一條做全部」。本研究的設定裡沒有那種力量，而最可能的缺項是：</p>
<p><strong>詞表太小。</strong> 226 個詞、每字 6 筆畫。在這個規模上「背下來」永遠比「發展出系統性」便宜。真實漢字系統面對的是數千字，而可分解性的收益隨詞表規模超線性成長——形符讓你猜出沒學過的字的意思，聲符讓你猜出它的音。那個收益在 226 個詞上幾乎為零。</p>
<p>這個假設可檢定：把詞表放大一個數量級，看雙重分離會不會轉正。成本是標註，不是算力——而那正是目前最貴的資源。</p>
<p>其他候選（依可能性排序）：</p>
<ul>
<li><strong>缺少習得成本。</strong> 模型每一代都從零學，學習成本沒有進入損失函數。真實文字系統的壓力有一大塊來自「下一代要學得會」。</li>
<li><strong>書寫成本被證實只驅動崩潰，不驅動符號化。</strong> 在這個規模上，Reader 只有 15–26% 準確率時，任何筆畫成本都會贏過溝通收益。</li>
</ul>
<h2 id="這些結論有多硬">這些結論有多硬</h2>
<p>老實講：</p>
<table tabindex="0">
<thead>
<tr>
<th></th>
<th>狀態</th>
</tr>
</thead>
<tbody>
<tr>
<td>探索 / 確認</td>
<td>全部是探索階段（train 226 詞）。heldout 102 詞從未載入</td>
</tr>
<tr>
<td>pre-registration</td>
<td>2026-08-23 凍結，含 S1/S2/S3 的完整分析計畫</td>
</tr>
<tr>
<td>種子</td>
<td>每個劑量 3 個（prereg 要求確認階段要 5 個）</td>
</tr>
<tr>
<td>詞表</td>
<td>單一詞表，單人標註。字形盲測顯示語意場 97.2% 可從語意單獨重建</td>
</tr>
<tr>
<td>最大的統計弱點</td>
<td>同音率與溝通成功率共線，r = −0.954。「形聲跟著同音走」這個判定靠「係數消不消失」成立，係數大小本身不可解讀</td>
</tr>
<tr>
<td>最大的設計弱點</td>
<td>詞表規模（見上）</td>
</tr>
</tbody>
</table>
<h2 id="附-量到看起來正常-其實在說謊的數字">（附）量到看起來正常，其實在說謊的數字</h2>
<p>過程中修掉的量測錯誤，每一個都會給出「看起來合理」的錯誤數字：</p>
<ul>
<li>同音率一開始用<strong>詞對</strong>當分母算——25425 對裡只有 107 對同音，算出 0.005，但實際上 30% 的詞都有同音詞。分母選錯，整個數字的意思就變了。</li>
<li>形聲效應用<strong>置換檢定</strong>當參照——字形分佈本身不均勻，讓虛無分佈被壓低，結果連純表意的基線都顯著。</li>
<li>齊夫斜率用 <strong>OLS</strong>——76% 的詞撞上界，斜率被壓縮了五倍（見上面 S1 那段）。</li>
<li>雙重分離用<strong>加總</strong>當量表——單通道系統可以拿到 0.324 分，是真正雙通道系統的 15 倍。</li>
</ul>
<p>紀律後來訂下來：任何分母可能是零、任何自變數可能沒有變異的地方，一律回傳 <code>nan</code>，不回傳 0 也不回傳巨大值——因為「沒有效應」跟「這個 run 根本沒問到問題」是兩件事，後續的動作完全相反：前者該寫進論文，後者該調參數重跑。</p>
<h2 id="下一步">下一步</h2>
<p>S3 的 confirmatory run 還沒開始，heldout 詞表也還沒碰。最有機會回答「為什麼沒有湧現」的下一步，是把詞表放大一個數量級——如果雙重分離在更大的詞表上轉正，「詞表太小」這個假設就立住了；如果沒有，就要回頭檢查習得成本或書寫成本這兩個候選。</p>
<p>等 S3 confirmatory 跑完、或詞表擴大實驗有結果，會有第二集。</p>]]></content:encoded>
    </item>
    <item>
      <title>Hololive Dreams 逆向 - 筆記歸檔</title>
      <link>https://blog.siao.ai/hololive-dreams-archive-20260823/</link>
      <guid isPermaLink="true">https://blog.siao.ai/hololive-dreams-archive-20260823/</guid>
      <pubDate>Sun, 23 Aug 2026 00:00:00 GMT</pubDate>
      <description>一款 Unity 6（metadata v39）手遊資源系統的逆向連載歸檔。IL2CPP dump、資源目錄、免越獄離線鏈、有骨架綁定的 glTF、CRC32 反查動作綁定、Live2D 原生層、以及還沒解完的彈簧骨物理。</description>
      <content:encoded><![CDATA[<h2 id="preface">Preface</h2>
<p>Cutoff: 26/08/23　Revision: 26/08/23</p>
<p>一款 Unity 6 手遊資源系統的逆向紀錄。目前狀態：資產、動畫、臉部、頭髮、相機、Live2D 全部可交付，彈簧骨物理是唯一還沒解完的東西。</p>
<p>工具與完整逐章日誌在 <a href="https://git.siao.ai/siao/hohohololive">https://git.siao.ai/siao/hohohololive</a>（<strong>不含解密</strong>，理由見（8））。本文的原始日誌在「<strong>原始日誌</strong>」面板裡，展開可以直接在這一頁讀完，不用跳出去。寬螢幕上它跟著文章捲動在右側；窄螢幕上它排在文末，從上面的目次最後一項可以直接跳過去。每一節結尾都有一行「本節來源」，點下去會跳到面板並直接展開那一份——不用先捲到文末找。</p>
<p>本文的程式碼片段是<strong>建置時從 SiaoHub 直接嵌入</strong>的，不是手貼，所以不會跟原始碼脫節；SiaoHub 那邊的檔案頁也會反向標示「被這篇文章引用」。</p>
<p>命名致敬 <a href="https://github.com/mos9527/sssekai">sssekai</a> —— 那個專案與它的<a href="https://mos9527.com/posts/pjsk/archive-20240105/">筆記歸檔</a>是這次逆向路上最有用的參考。</p>
<p>分析目標：<code>game.qualiarts.hololive.dreams.com</code> 1.0.0（iOS，已解密 IPA），Unity <code>6000.3.0b1</code>，IL2CPP metadata v39。</p>
<hr>
<h2 id="1-裝置連線與-il2cpp-metadata-v39">（1）裝置連線與 IL2CPP metadata v39</h2>
<h3 id="1-傳輸-先看天花板">1. 傳輸：先看天花板</h3>
<p>app 容器下 <code>Library/octo/</code> 是資源下載快取，3.4GB。子目錄 <code>v1/</code> 有 178 個 <code>.awb</code>、1240 個 <code>.acb</code>、176 個 <code>.usm</code>，全是 CRIWARE 音效／影片格式。</p>
<p>WiFi SSH 下 <code>scp</code> 傳 12MB 花 11 分鐘。第一反應是換 cipher（預設那組沒有硬體加速），換完看起來瞬間變快——那是本地磁碟緩衝的假象，大檔就原形畢露。</p>
<p>但更重要的是即使 cipher 真的有效也沒意義：3.4GB 在 WiFi 上再怎麼調都是幾十分鐘等級。改走 USB：</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0" data-language="bash" data-theme="github-light-high-contrast github-dark-high-contrast"><code data-language="bash" data-theme="github-light-high-contrast github-dark-high-contrast" style="display: grid;"><span data-line=""><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">iproxy</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> 2222:22</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> &#x26;         </span><span style="--shiki-light:#66707B;--shiki-dark:#BDC4CC"># SSH</span></span>
<span data-line=""><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">iproxy</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> 27042:27042</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> &#x26;     </span><span style="--shiki-light:#66707B;--shiki-dark:#BDC4CC"># Frida</span></span></code></pre></figure>
<table tabindex="0">
<thead>
<tr>
<th>路徑</th>
<th>實測</th>
<th>3.4GB 估時</th>
</tr>
</thead>
<tbody>
<tr>
<td>WiFi SSH（預設 cipher）</td>
<td>~18 KB/s</td>
<td>約 2 天</td>
</tr>
<tr>
<td>WiFi SSH（gcm）</td>
<td>小檔看似極快，大檔仍慢</td>
<td>—</td>
</tr>
<tr>
<td>USB（<code>iproxy</code>）</td>
<td>40–70 MB/s</td>
<td>約 1 分鐘</td>
</tr>
</tbody>
</table>
<p>整包 <code>tar</code> 在動任何東西之前先拉回本機。後面有好幾次需要清掉裝置端快取觀察下載時機，沒有備份的話每次清除都不可逆。</p>
<h3 id="2-metadata-是加密的-binary-不是">2. Metadata 是加密的，binary 不是</h3>
<pre tabindex="0"><code>$ xxd -l 8 global-metadata.dat
00000000: 8f2b 0d1c ...       # 預期 AF 1B B1 FA
</code></pre>
<p>magic 對不上。但<strong>磁碟上的 <code>UnityFramework</code> 並沒有加密</strong>——這兩者要分清楚，我一開始沒分（見（附）踩坑）。</p>
<p>metadata 在執行時必然是解開的，掃記憶體找 magic：</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0" data-language="python" data-theme="github-light-high-contrast github-dark-high-contrast"><code data-language="python" data-theme="github-light-high-contrast github-dark-high-contrast" style="display: grid;"><span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">import</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> frida</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">MAGIC</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> =</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> b</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">\xaf\x1b\xb1\xfa</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">def</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF"> on_message</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(msg, data):</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">    if</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> msg[</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"payload"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">].get(</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"event"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">) </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">==</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> "metadata"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">:</span></span>
<span data-line=""><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">        open</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"global-metadata-decrypted.dat"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"wb"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">).write(data)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">dev </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> frida.get_usb_device()</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">pid </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> dev.spawn([</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"game.qualiarts.hololive.dreams.com"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">])</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">ses </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> dev.attach(pid)</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">scr </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> ses.create_script(</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">open</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"dump.js"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">).read())</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">scr.on(</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"message"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, on_message)</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">scr.load()</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">dev.resume(pid)</span></span></code></pre></figure>
<p><code>Process.enumerateRanges("r--")</code> 逐段掃，單一位址命中：</p>
<pre tabindex="0"><code>magic   = AF 1B B1 FA
version = 39
</code></pre>
<p><code>version = 39</code> 是合法的 IL2CPP 版本號，代表這是解密後的真身而不是巧合命中。</p>
<h3 id="3-版本號偽裝的失敗模式才是答案">3. 版本號偽裝的失敗模式才是答案</h3>
<p><code>Il2CppDumper</code> 最新版只支援到 31。標準做法是改版本號騙過去，因為格式常常沒變、只是跳號。</p>
<table tabindex="0">
<thead>
<tr>
<th>偽裝成</th>
<th>結果</th>
</tr>
</thead>
<tbody>
<tr>
<td>27</td>
<td>失敗 —— key 衝突</td>
</tr>
<tr>
<td>29</td>
<td>失敗 —— 同一個 key 衝突</td>
</tr>
<tr>
<td>31</td>
<td>失敗 —— 同一個 key 衝突</td>
</tr>
</tbody>
</table>
<p>三次掛在<strong>同一個位置</strong>。如果只是跳號，改成不同的舊版本應該在不同地方壞掉；三次一模一樣，代表解析器讀到的結構在那個點就跟預期分岔了——這是真正更新過的格式。</p>
<p>這個判斷值錢的地方在於它一次關掉整條分支。「再試一個版本號」每次只花三十秒，所以很容易一直試下去。</p>
<p>改用 <code>Il2CppInspectorRedux</code>（LukeFZ fork），產出 56 萬個方法名稱對虛擬位址的完整 map。</p>
<h3 id="4-反編譯環境-繞開-jvm">4. 反編譯環境：繞開 JVM</h3>
<p>Ghidra 需要 JVM，而這台機器的 <code>AppleSystemPolicy</code> 核心模組擋掉未簽章的 java——不是工具沙盒的限制，是系統本身，在自己的終端機直接跑也一樣 <code>Kill: 9</code>。</p>
<p>不跟系統打。<code>rz-ghidra</code> 把 Ghidra 反編譯引擎的 C++ 核心（SLEIGH + decompiler）抽出來編成 rizin 外掛，執行時不需要 JVM：</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0" data-language="bash" data-theme="github-light-high-contrast github-dark-high-contrast"><code data-language="bash" data-theme="github-light-high-contrast github-dark-high-contrast" style="display: grid;"><span data-line=""><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">git</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> clone</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> --recurse-submodules</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> https://github.com/rizinorg/rz-ghidra.git</span></span>
<span data-line=""><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">cd</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> rz-ghidra</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> &#x26;&#x26; </span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">mkdir</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> build</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> &#x26;&#x26; </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">cd</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> build</span></span>
<span data-line=""><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">cmake</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> -DCMAKE_BUILD_TYPE=Release</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> ..</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> &#x26;&#x26; </span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">make</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> -j$(</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">sysctl</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> -n </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">hw.ncpu</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">)</span></span>
<span data-line=""><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">cp</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> *</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">.dylib</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> /opt/homebrew/lib/rizin/plugins/</span></span></code></pre></figure>
<p><strong>坑一</strong>：光複製 <code>.dylib</code> 不夠，還要把 build 出的 <code>.sla</code>（編譯後的 sleigh 語言規格）放回原始碼樹裡跟 <code>.slaspec</code> 同一層，並設 <code>SLEIGHHOME</code>。錯誤訊息不會提到少了資料檔。</p>
<p><strong>坑二</strong>：<code>rizin -q -c "s &#x3C;addr>; af; pdg"</code> 的 <code>af</code> 若在某位址落進另一個更早、更大的函式範圍內，會沿用既有邊界，反編譯出<strong>不相關的函式內容</strong>。我曾因此誤判某位址是 IL2CPP 共用的泛型 Dictionary 查表邏輯，一度以為某個關鍵值是查表得來而非算出來的。</p>
<p><strong>狀態：完成。</strong></p>
<p>本節來源：<a href="#doc-ClaudeDocs-OVERVIEW"><code>OVERVIEW.md</code></a></p>
<p><strong>References</strong></p>
<ul>
<li><a href="https://github.com/LukeFZ/Il2CppInspectorRedux">https://github.com/LukeFZ/Il2CppInspectorRedux</a></li>
<li><a href="https://github.com/rizinorg/rz-ghidra">https://github.com/rizinorg/rz-ghidra</a></li>
</ul>
<hr>
<h2 id="2-保護層定位-官方加密根本沒開">（2）保護層定位：官方加密根本沒開</h2>
<p>這款遊戲用 CRIWARE 音效中介軟體，而 CRIWARE 有官方加密。method map 裡確實有：</p>
<pre tabindex="0"><code>Vision.Sound.CriWareDecrypter.Initialize(string key, bool enableAtom, bool enableMana)
</code></pre>
<p>看起來就是它。hook 上去看實際呼叫參數：</p>
<pre tabindex="0"><code>[+] CriWareDecrypter.Initialize
    key         = "..." (非空)
    enableAtom  = false
    enableMana  = false
</code></pre>
<p>兩個開關都是 false，官方加密根本沒啟用——key 傳進去了但沒人用。真正在保護資源的是遊戲自製的另一層（<code>Vision.Octo.ResourceDecrypter</code>）。</p>
<p>我在這之前對著 CRIWARE 的文件研究了幾個小時。hook 一下看實際值花不到十分鐘。</p>
<blockquote>
<p><strong>更正（分析當時）</strong>：一開始看表面（bundle 沒有音效那組的明文前綴、開頭高熵）就斷定音效與 bundle 是<strong>兩套不同機制</strong>，繞了很大一圈（試過 LZ4、AES、GPU hook、Metal capture）。真正的突破是回到基本功做<strong>已知明文攻擊</strong>——兩者其實共用同一套，只差前綴與起點。教訓：表面差異不等於機制差異，而「看起來不一樣」很容易變成停止驗證的理由。</p>
</blockquote>
<p><strong>狀態：完成。</strong></p>
<p>本節來源：<a href="#doc-ClaudeDocs-OVERVIEW"><code>OVERVIEW.md</code></a></p>
<hr>
<h2 id="3-資源目錄與免越獄離線鏈">（3）資源目錄與免越獄離線鏈</h2>
<h3 id="1-規模論證-為什麼不用被動-hook">1. 規模論證：為什麼不用被動 hook</h3>
<p>解密需要每個檔案的原始檔名（<code>address</code>）。檔名不在加密檔裡，在遊戲的資源目錄中。</p>
<p>被動做法：hook 住解密函式，玩遊戲，載入什麼記錄什麼。一定會成功，技術上毫無風險。</p>
<p>問題是規模：</p>
<pre tabindex="0"><code>加密檔案總數        1467
被動 hook 覆蓋率    取決於玩到哪
估計時數            上百小時
覆蓋率保證          無（限時活動資源可能永遠觸發不了）
</code></pre>
<p>當一條路的成本是「時間 × 運氣」而且沒有覆蓋率保證時，它不是一條路，是一個斜坡。</p>
<h3 id="2-先量-再決定值不值得">2. 先量，再決定值不值得</h3>
<p><code>octo/pdb/5/100001/octocacheevai</code>，4.4MB。先算熵：</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0" data-language="python" data-theme="github-light-high-contrast github-dark-high-contrast"><code data-language="python" data-theme="github-light-high-contrast github-dark-high-contrast" style="display: grid;"><span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">from</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> collections </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">import</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> Counter</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">import</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> math</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">d </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> open</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(path, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"rb"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">).read()</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">c </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> Counter(d)</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">H </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> -</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">sum</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">((n </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">/</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> len</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(d)) </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">*</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> math.log2(n </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">/</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> len</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(d)) </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">for</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> n </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">in</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> c.values())</span></span>
<span data-line=""><span style="--shiki-light:#66707B;--shiki-dark:#BDC4CC"># -> 8.0 bits/byte</span></span></code></pre></figure>
<p>滿分 8.0，確認是真加密而不只是壓縮或序列化格式——值得花力氣，也代表沒有靜態解析的可能。</p>
<h3 id="3-定位解開後的形式">3. 定位解開後的形式</h3>
<p>索引在執行時一定會被解開來用，所以解開後的形式必然在 process 記憶體裡。不用解它的加密，只要找到它解開後的樣子。</p>
<p>第一版想抓「原地解密」：hook <code>read()</code>，記下 buffer 位址，延遲幾秒後回頭讀同一塊。全錯——native buffer 在函式返回後很快被回收挪作他用（見（附）踩坑）。</p>
<p>改成：等 60 秒讓索引完全載入，然後對整個 process 記憶體掃一個已知會出現在裡面的檔名字串。</p>
<pre tabindex="0"><code>命中區塊  16 MB
命中次數  3563
</code></pre>
<p>那塊就是解開後的完整資源目錄，protobuf 格式。</p>
<h3 id="4-條目結構">4. 條目結構</h3>
<pre tabindex="0"><code>1a &#x3C;len>              # 條目 (length-delimited submessage)
  08 &#x3C;varint>         # 1  id          -> 快取目錄名 = ("A"|"R") + id，再 hex 編碼
  12 &#x3C;len> &#x3C;bytes>    # 2  name        -> address（原始檔名）
  18 &#x3C;varint>         # 3  size        -> 明文位元組數
  2a 20 &#x3C;32 bytes>    # 5  md5         -> 快取檔名
  3a &#x3C;len> &#x3C;bytes>    # 7  objectName  -> CDN 物件鍵，6 字元隨機字串
</code></pre>
<p>實例（<code>VisionProject.acf</code>，整筆長度 <code>0x42</code> = 66 bytes，逐欄相加剛好吻合）：</p>
<pre tabindex="0"><code>1a 42  08 01  12 11 "VisionProject.acf"  18 89 77  2a 20 "bd19...8afb"  3a 06 "VQHQAP"
       id=1        name(17)              size=15241   md5(32)            objectName(6)
</code></pre>
<h3 id="5-驗證-對帳-不是-看起來對">5. 驗證：對帳，不是「看起來對」</h3>
<p>octo 快取的目錄名是 hex 編碼的 ASCII——<code>413138363439</code> 解出來是 <code>A18649</code>。所以 <code>("A"|"R") + id</code> 可以拿本地 4942 個快取檔逐一對帳：</p>
<pre tabindex="0"><code>id 相符      4929
id 不符        13
不在 catalog    0
                     -> 99.74%
</code></pre>
<p>13 筆不符全部是<strong>記憶體區塊邊界截斷</strong>：address 開頭混進 <code>*</code>、<code>#</code> 等雜訊字元（例：<code>*vo_live_cmn_chr_0</code>）。那是 dump 的邊界問題不是解析邏輯問題，用「address 首字元須為英數」濾掉即可。</p>
<p>這一步是整段最重要的。一個解析器輸出「看起來很像檔名的字串」時，它可能只是在讀雜訊。要有一個<strong>獨立來源</strong>能對帳——這裡是本地快取的目錄名。</p>
<p>最終 <strong>16823 筆</strong> hash 對 address 的完整對照表，批次覆蓋率 99.93%，1595 個檔案、1.5GB。</p>
<h3 id="6-objectname-→-cdn">6. <code>objectName</code> → CDN</h3>
<p>先前逆 <code>OctoAPI.DecryptAes</code> 時解出過一個 CDN 樣板：</p>
<pre tabindex="0"><code>https://asset.game-hololive-dreams.com/{o}
</code></pre>
<p>當時不知道 <code>{o}</code> 是什麼，只當紅鯡魚。解完條目結構才知道：<code>{o}</code> 就是 field 7 的 <code>objectName</code>。</p>
<pre tabindex="0"><code>GET https://asset.game-hololive-dreams.com/UFfHjj
    User-Agent: UnityPlayer/6000.3.0b1
-> 1462529 bytes, md5 = 94bb0bfa4f2a82415c87fff62763ef6a
</code></pre>
<p>catalog 裡的 md5 算的是<strong>密文</strong>，所以下載後可以直接校驗完整性，不需要先解密。</p>
<p><strong>代表越獄只剩「取得一次 catalog」這一個用途。</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0" data-language="python" data-theme="github-light-high-contrast github-dark-high-contrast"><code data-language="python" data-theme="github-light-high-contrast github-dark-high-contrast" style="display: grid;"><span data-line=""><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">DEFAULT_URL_FORMAT</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> =</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> "https://asset.game-hololive-dreams.com/</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">{o}</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"</span></span>
<span data-line=""><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">USER_AGENT</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> =</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> "UnityPlayer/6000.3.0b1"</span></span>
<span data-line=""> </span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">def</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF"> url_for</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(entry: </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">dict</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, url_format: </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">str</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> =</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> DEFAULT_URL_FORMAT</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">) -> </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">str</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">:</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">    return</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> url_format.replace(</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">{o}</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, entry[</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"object_name"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">])</span></span></code></pre></figure>
<p><a href="https://git.siao.ai/siao/hohohololive/blob/main/hohohololive/cdn.py#L21-L26">在 SiaoHub 檢視 siao/hohohololive/hohohololive/cdn.py L21-26</a></p>
<h3 id="7-欄位前進解析-一個假設造成-94-漏抓">7. 欄位前進解析：一個假設造成 94% 漏抓</h3>
<p>第一版解析器假設 <code>objectName</code>（field 7）緊接在 <code>md5</code>（field 5）之後。結果 608 筆 <code>mdl_chr</code> 只有 2 筆抓到。</p>
<p>中間隔著<strong>重複的 field 6</strong>：</p>
<pre tabindex="0"><code>2a 20 &#x3C;md5>  30 ca8402  30 e19402  30 809502 ...  3a 06 "SswxO0"  42 23 &#x3C;address>
             \____ 6 = 依賴資產 id (repeated varint) ____/  \_ 7 = objectName
</code></pre>
<p>3D 模型每個都依賴貼圖與材質，所以<strong>全部</strong>有 field 6，全被跳過。改成正規的欄位前進解析——依 wire type 逐欄推進 tag → 長度 → 值：</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0" data-language="python" data-theme="github-light-high-contrast github-dark-high-contrast"><code data-language="python" data-theme="github-light-high-contrast github-dark-high-contrast" style="display: grid;"><span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">        while</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> p </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">&#x3C;</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> n:</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">            tag </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> blob[p]</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">            field, wire </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> tag </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">>></span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> 3</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, tag </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">&#x26;</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> 7</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">            p </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">+=</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> 1</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">            if</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> wire </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">==</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> 0</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">:                       </span><span style="--shiki-light:#66707B;--shiki-dark:#BDC4CC"># varint</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                v </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> shift </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> 0</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">                while</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> p </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">&#x3C;</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> n:</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                    b </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> blob[p]</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                    v </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">|=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> (b </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">&#x26;</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> 0x</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">7F</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">) </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">&#x3C;&#x3C;</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> shift</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                    p </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">+=</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> 1</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">                    if</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> not</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> (b </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">&#x26;</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> 0x</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">80</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">):</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">                        break</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                    shift </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">+=</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> 7</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">                if</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> field </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">==</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> 6</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">:</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                    deps.append(v)</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">                else</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">:</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">                    break</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">            elif</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> wire </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">==</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> 2</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">:                     </span><span style="--shiki-light:#66707B;--shiki-dark:#BDC4CC"># length-delimited</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">                if</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> p </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">>=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> n:</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">                    break</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                ln2 </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> blob[p]</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                p </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">+=</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> 1</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">                if</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> ln2 </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">&#x26;</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> 0x</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">80</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">:                  </span><span style="--shiki-light:#66707B;--shiki-dark:#BDC4CC"># 兩位元組長度, 已超出本筆範圍</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">                    break</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                val </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> blob[p:p </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">+</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> ln2]</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                p </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">+=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> ln2</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">                if</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> field </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">==</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> 7</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> and</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> len</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(val) </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">==</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> ln2 </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">and</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> all</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">0x</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">20</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> &#x3C;=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> c </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">&#x3C;</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> 0x</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">7F</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> for</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> c </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">in</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> val):</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                    obj </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> val.decode(</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"ascii"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">)</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">                break</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">            else</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">:</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">                break</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">        out.append({</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"id"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: oid, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"address"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: address, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"size"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: size,</span></span>
<span data-line=""><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">                    "md5"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: md5.decode(), </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"object_name"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: obj,</span></span>
<span data-line=""><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">                    "dependencies"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: deps})</span></span></code></pre></figure>
<p><a href="https://git.siao.ai/siao/hohohololive/blob/main/hohohololive/catalog.py#L112-L145">在 SiaoHub 檢視 siao/hohohololive/hohohololive/catalog.py L112-145</a></p>
<p><code>field == 7</code> 那條還多要求「長度吻合且全為可列印 ASCII」，因為記憶體 dump 的邊界截斷會讓最後一筆條目讀出半個欄位。修正後：</p>
<pre tabindex="0"><code>修正前  33799 / 36119 筆有 objectName
修正後  36119 / 36119
</code></pre>
<p>順帶把依賴清單（field 6）也解了出來，之後做「連依賴一起抓」很方便。</p>
<p>protobuf 的欄位順序沒有保證，<code>repeated</code> 欄位更是任意長。用「位移量」定位欄位是在賭序列化器的實作細節，賭輸的時候它不會報錯，只會少抓——而 2/608 這種比例很明顯，33799/36119 就不一定會被發現。</p>
<h3 id="8-實測">8. 實測</h3>
<pre tabindex="0"><code>待下載   944 筆 (3D + 缺的 Live2D + mot_define), 1.01 GB
成功     944
跳過       0
失敗       0
</code></pre>
<p>完整離線鏈：<strong>CDN 下載 → 校驗 → 解密 → 抽取。</strong></p>
<p><strong>狀態：完成。</strong></p>
<p>本節來源：<a href="#doc-ClaudeDocs-RE_NOTES_3D"><code>RE_NOTES_3D.md</code></a> §2、§5</p>
<p><strong>References</strong></p>
<ul>
<li><a href="https://protobuf.dev/programming-guides/encoding/">https://protobuf.dev/programming-guides/encoding/</a></li>
</ul>
<hr>
<h2 id="4-3d-模型-有骨架綁定的-gltf">（4）3D 模型：有骨架綁定的 glTF</h2>
<h3 id="1-頂點串流佈局">1. 頂點串流佈局</h3>
<pre tabindex="0"><code>stride(s) = Σ dimension × sizeof(format)
offset(0) = 0
offset(s) = align16(offset(s-1) + vertexCount × stride(s-1))
</code></pre>
<p>角色 body 實測：</p>
<pre tabindex="0"><code>stream0 stride 40  ch0 Position(3f)  ch1 Normal(3f)  ch2 Tangent(4f)
stream1 stride 16  ch3 Color(4×UNorm8) ch4 UV0(2×half) ch7 UV3(2f)
stream2 stride 32  ch12 BlendWeight(4f) ch13 BlendIndices(4×uint32)
</code></pre>
<p>驗證方式：對每個網格算「計算總長 vs 實際 <code>m_DataSize</code>」，全部逐位元組吻合。</p>
<h3 id="2-兩個真實的-bug">2. 兩個真實的 bug</h3>
<p><strong>(1) 骨影響數不是固定 4。</strong> 我原本假設 ch12/ch13 恆為 dim4，直接 <code>[:, :4]</code>：</p>
<table tabindex="0">
<thead>
<tr>
<th>網格</th>
<th>ch12</th>
<th>ch13</th>
<th>實際</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Geo_Body_LOD0</code></td>
<td>dim4</td>
<td>dim4</td>
<td>4 骨混合</td>
</tr>
<tr>
<td><code>Geo_Eye_LOD0</code></td>
<td>dim2</td>
<td>dim2</td>
<td><strong>2 骨混合</strong></td>
</tr>
<tr>
<td><code>Geo_Brow_LOD0</code> / <code>Geo_Iris_LOD0</code></td>
<td>無</td>
<td><strong>dim1</strong></td>
<td><strong>剛性單骨，權重恆 1</strong></td>
</tr>
</tbody>
</table>
<p>對 dim2 的網格，<code>[:, :4]</code> 只拿到 2 寬陣列卻宣告成 VEC4 → 權重和變成 −2.37 ~ 3.02、joint 索引 63471。眉毛與虹膜則因為「沒有 ch12」被整個當成無綁定跳過。修正：一律補零到 4 寬；缺 ch12 時視為剛性綁定，第 0 槽權重設 1.0。</p>
<p><strong>(2) 頂點資料有兩種存法。</strong> 角色模型內嵌在 <code>m_VertexData.m_DataSize</code>，但<strong>場景零件多半放在外部串流檔（<code>.resS</code>）</strong>，由 <code>mesh.m_StreamData</code> 指出 path/offset/size。沒處理時 <code>fbx_mdl_env_*</code> 整批解出 0 個網格。改用 <code>UnityPy.helpers.ResourceReader.get_resource_data()</code> 取回。</p>
<h3 id="3-座標系轉換">3. 座標系轉換</h3>
<p>Unity 左手系 → glTF 右手系，以鏡射 X 軸 <code>M = diag(-1,1,1)</code>：</p>
<table tabindex="0">
<thead>
<tr>
<th>對象</th>
<th>轉換</th>
</tr>
</thead>
<tbody>
<tr>
<td>位置 / 法線 / 切線</td>
<td><code>(x,y,z) → (-x,y,z)</code></td>
</tr>
<tr>
<td>旋轉四元數</td>
<td><code>(x,y,z,w) → (x,-y,-z,w)</code></td>
</tr>
<tr>
<td>逆綁定矩陣</td>
<td><code>M' = S·M·S</code>，<code>S = diag(-1,1,1,1)</code></td>
</tr>
<tr>
<td>UV</td>
<td><code>v → 1-v</code>（Unity 原點左下，glTF 左上）</td>
</tr>
<tr>
<td>三角形環繞</td>
<td>反轉（行列式變號）</td>
</tr>
</tbody>
</table>
<p>四元數那條的推導：<code>R' = M R M</code>，<code>M</code> 為非正常變換（det = −1），把「繞軸 <code>a</code> 轉 θ」變成「繞 <code>(aₓ, -a_y, -a_z)</code> 轉 θ」，代入 <code>q = (sin(θ/2)·a, cos(θ/2))</code> 即得。</p>
<p>這幾條寫在實作的 docstring 裡而不是只寫在日誌裡，因為它們是<strong>每次改動這個檔案都要重新確認的前提</strong>：</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0" data-language="python" data-theme="github-light-high-contrast github-dark-high-contrast"><code data-language="python" data-theme="github-light-high-contrast github-dark-high-contrast" style="display: grid;"><span data-line=""><span style="--shiki-light:#66707B;--shiki-dark:#BDC4CC">## 座標系轉換</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">Unity 是左手系 (Y</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">-</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">up, Z</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">-</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">forward)，glTF 是右手系 (Y</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">-</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">up, Z</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">-</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">back)。</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">以鏡射 X 軸 M </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> diag(</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">-</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">1</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">1</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">1</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">) 轉換:</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    位置</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">/</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">法線</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">/</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">切線   (x,y,z) </span><span style="--shiki-light:#6E011A;--shiki-light-font-style:italic;--shiki-dark:#FFB1AF;--shiki-dark-font-style:italic">-></span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> (</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">-</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">x, y, z)</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    旋轉四元數       (x,y,z,w) </span><span style="--shiki-light:#6E011A;--shiki-light-font-style:italic;--shiki-dark:#FFB1AF;--shiki-dark-font-style:italic">-></span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> (x, </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">-</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">y, </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">-</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">z, w)</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">        推導: </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">R</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">' = M R M, M 為非正常變換 (det=-1),</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">        使繞軸 a 轉 θ 變成繞 (ax,</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">-</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">ay,</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">-</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">az) 轉 θ</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    逆綁定矩陣       M</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">' = S · M · S     (S = diag(-1,1,1,1))</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">    三角形環繞順序   必須反轉 (行列式變號)</span></span></code></pre></figure>
<p><a href="https://git.siao.ai/siao/hohohololive/blob/main/hohohololive/gltf.py#L27-L37">在 SiaoHub 檢視 siao/hohohololive/hohohololive/gltf.py L27-37</a></p>
<h3 id="4-一個推論錯了兩次的地方-描邊殼">4. 一個推論錯了兩次的地方：描邊殼</h3>
<p><code>Geo_Body_LOD0</code> 有 3 個子網格，面數 13932 / 240 / <strong>13932</strong>——第 0 與第 2 完全一樣。索引緩衝 41796 + 720 + 41796 = 84312 剛好把 168624 bytes（uint16）填滿，所以那不是解析錯誤，是<strong>真實資料</strong>。</p>
<p><strong>第一次推論（錯）</strong>：這些重複子網格在 <code>renderer.m_Materials</code> 裡沒有對應材質，所以判定「Unity 因此不渲染它」，用「無材質」當剔除條件。</p>
<p><strong>真相</strong>：材質一直都在，只是<strong>放在依賴 bundle 裡的共用材質</strong>，沒載入依賴時 PPtr 解不開而已。加上 <code>load_bundle_with_deps()</code> 之後名字直接浮出來：</p>
<pre tabindex="0"><code>材質: ['m_eye', 'm_bdy', 'm_bdyco', 'SubMeshOutlineMaterial', 'm_fef']
  Geo_Body_LOD0 sub0 面=13932 材質=m_bdy
  Geo_Body_LOD0 sub1 面=  240 材質=m_bdyco
  Geo_Body_LOD0 sub2 面=13932 材質=SubMeshOutlineMaterial   &#x3C;- 描邊殼
</code></pre>
<p>卡通渲染的 inverted-hull 描邊：同一份幾何，法線外推後翻面只畫背面。</p>
<p>「這個東西沒有 X 所以引擎不用它」——<strong>當 X 是跨 bundle 解析出來的東西時，這句話的前提可能只是你沒載入依賴。</strong></p>
<h3 id="5-blendshape-→-gltf-morph-target">5. BlendShape → glTF morph target</h3>
<p>臉部表情不靠骨架，走 blendshape，而且<strong>只在臉部網格上</strong>：</p>
<table tabindex="0">
<thead>
<tr>
<th>網格</th>
<th align="right">通道數</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Geo_Eye_LOD0</code></td>
<td align="right">16</td>
</tr>
<tr>
<td><code>Geo_Brow_LOD0</code></td>
<td align="right">14</td>
</tr>
<tr>
<td><code>Geo_Iris_LOD0</code></td>
<td align="right">2</td>
</tr>
<tr>
<td><code>Geo_Body_LOD0/1</code></td>
<td align="right">0（身體變形全靠骨架）</td>
</tr>
</tbody>
</table>
<p>合計 <strong>32</strong> —— 與動作 clip 內 typeID 137 的曲線數完全相同，互為佐證。</p>
<p>Unity 以稀疏方式儲存：<code>channels[]</code> 指向 <code>shapes[]</code> 的一段，<code>shapes[i]</code> 再指向 <code>vertices[]</code> 的一段，每個頂點自帶原網格索引。glTF 的 morph target 需要與網格等長的稠密位移陣列，所以逐一展開回去（位移同樣要套 X 鏡射）。</p>
<p><code>channel.frameCount > 1</code> 表示漸進形變，glTF 一個 target 只能表示一個形狀，取最後一幀。實測本作全部 <code>frameCount = 1</code>。</p>
<p><strong>狀態：完成。</strong></p>
<p>本節來源：<a href="#doc-ClaudeDocs-RE_NOTES_3D"><code>RE_NOTES_3D.md</code></a> §6、§7</p>
<hr>
<h2 id="5-3d-動作-crc32-反查與-mot-define">（5）3D 動作：CRC32 反查與 <code>mot_define</code></h2>
<h3 id="1-突破口是-monobehaviour-不是-animationclip">1. 突破口是 MonoBehaviour，不是 AnimationClip</h3>
<p><code>AnimationClip</code> 的 <code>genericBindings</code> 只存雜湊：</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0" data-language="python" data-theme="github-light-high-contrast github-dark-high-contrast"><code data-language="python" data-theme="github-light-high-contrast github-dark-high-contrast" style="display: grid;"><span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">{</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'path'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">1182008026</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'attribute'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">1661978518</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">'typeID'</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">137</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">}</span></span></code></pre></figure>
<p>一開始拿角色 <code>skeleton.json</code> 的骨骼路徑算 CRC32 去比對，<strong>608 個骨架 × 所有路徑形式，0 命中</strong>。</p>
<p>真正的答案在同一個 bundle 的 MonoBehaviour（<code>VisionActorMotionDefine</code>）：它的 <code>baseAnimation.bindings</code> 以<strong>明文</strong>列出每條綁定：</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0" data-language="json" data-theme="github-light-high-contrast github-dark-high-contrast"><code data-language="json" data-theme="github-light-high-contrast github-dark-high-contrast" style="display: grid;"><span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">{</span><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">"name"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"Geo_Eye_LOD0"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#024C1A;--shiki-dark:#72F088">"path"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"Root_Body/Geo_Eye_LOD0"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span></span>
<span data-line=""><span style="--shiki-light:#024C1A;--shiki-dark:#72F088"> "type"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"UnityEngine.SkinnedMeshRenderer"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">,</span></span>
<span data-line=""><span style="--shiki-light:#024C1A;--shiki-dark:#72F088"> "properties"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">: [</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"blendShape.b_eye.eye_001"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"..."</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">]}</span></span></code></pre></figure>
<p>Animator 根節點叫 <strong><code>Root_Body</code></strong>，不是 bundle 名稱——這就是對不上的原因。</p>
<p>雜湊函數確認為 CRC32(明文)：</p>
<table tabindex="0">
<thead>
<tr>
<th>字串</th>
<th align="right">CRC32</th>
<th>在 clip 內出現</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>b_eye.eye_001</code></td>
<td align="right">1661978518</td>
<td>blendshape 首條（<strong>不帶</strong> <code>blendShape.</code> 前綴）</td>
</tr>
<tr>
<td><code>m_FadeFactor</code></td>
<td align="right">682354173</td>
<td>6 次 = 6 個 DecalProjector</td>
</tr>
<tr>
<td><code>bakeAnimationWeight</code></td>
<td align="right">3202011236</td>
<td>44 次 = 44 根 swing bone</td>
</tr>
</tbody>
</table>
<p>把 637 個 bundle 的 bindings 全部收集成<strong>全域</strong>反查表（單一 bundle 的字典只夠解自己那幾條），得到 <strong>520 個路徑 / 198 個屬性名</strong>。</p>
<p>這是這一段最可複用的一點：<strong>當 hash 對不上時，先懷疑輸入字串的形式，不要先懷疑 hash 函數。</strong> 我花在「會不會其實是別的 hash」上的時間，遠多於花在「路徑前綴會不會不一樣」上的時間，而答案是後者。</p>
<p><strong>狀態：完成。</strong></p>
<p>本節來源：<a href="#doc-ClaudeDocs-RE_NOTES_3D"><code>RE_NOTES_3D.md</code></a> §8</p>
<hr>
<h2 id="6-live2d-攻原生層-不攻上層">（6）Live2D：攻原生層，不攻上層</h2>
<h3 id="1-三條走不通的路">1. 三條走不通的路</h3>
<p>依序試過、全部失敗：猜測是 LZ4 壓縮；找 <code>Octo.dll/Octo/Loader/OctoAPI.DecryptAes</code>（誤導性成功——它解的是 API 封包不是資源）；hook IL2CPP 層的 Cubism SDK API。</p>
<p>第三條失敗的方式指出了根因：Cubism Core 是<strong>原生 C 函式庫</strong>，直接靜態連結進 <code>UnityFramework</code>，沒有獨立 <code>.framework</code>，所以 IL2CPP 層根本沒有可以 hook 的東西。</p>
<h3 id="2-改攻原生匯出符號">2. 改攻原生匯出符號</h3>
<p><code>Module.enumerateExports()</code> 列出所有以 <code>csm</code> 開頭的原生匯出（共 44 個）：</p>
<pre tabindex="0"><code>csmGetVersion
csmGetMocVersion / csmGetLatestMocVersion
csmHasMocConsistency
csmReviveMocInPlace   ← 關鍵函式
csmInitializeModelInPlace
csmUpdateModel
csmGetDrawable* 系列
</code></pre>
<p><code>csmReviveMocInPlace(void* address, unsigned int mocSize)</code> 吃「已經解密好、可解析的 moc3 資料所在記憶體位址 + 大小」兩個參數。<strong>不需要理解上層是 C# 還是 IL2CPP，也不需要碰加密演算法本身</strong>——只要這個原生函式被呼叫，當下記憶體裡的資料就是 100% 正確可用的明文 moc3。</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0" data-language="javascript" data-theme="github-light-high-contrast github-dark-high-contrast"><code data-language="javascript" data-theme="github-light-high-contrast github-dark-high-contrast" style="display: grid;"><span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">const</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> target</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> =</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> Module.</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">findExportByName</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"UnityFramework"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"csmReviveMocInPlace"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">);</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">Interceptor.</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">attach</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(target, {</span></span>
<span data-line=""><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">  onEnter</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">args</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">) {</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">    const</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> addr</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> =</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> args[</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">0</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">], </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">size</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> =</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> args[</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">1</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">].</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">toInt32</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">();</span></span>
<span data-line=""><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">    send</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">({event: </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"csmReviveMocInPlace"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, size}, addr.</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">readByteArray</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(size));</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">  }</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">});</span></span></code></pre></figure>
<p>用 <code>Module.findExportByName</code> 而非硬編位址 offset，天生免疫 ASLR 與重啟位址偏移。</p>
<h3 id="3-通用形狀">3. 通用形狀</h3>
<p>這一段的可複用形狀值得單獨寫出來：<strong>找到那個「拿明文當參數」的原生函式，在它身上等。</strong> 不管上層包了幾層加密、混淆、託管執行期，資料終究要以引擎看得懂的形式交給引擎。那個交接點就是最低成本的位置，而且它天然不隨加密方案改版。</p>
<p><code>.moc3</code> / <code>.model3</code> / <code>.physics3</code> 取得後可直接用 Cubism Editor 開，材質需依 <code>BuildModelData</code> 的資訊更名。</p>
<p><strong>狀態：完成（貼圖 atlas 另計，見下）。</strong></p>
<h3 id="4-沒解完的-貼圖-atlas">4. 沒解完的：貼圖 atlas</h3>
<p>moc3 專用的 texture atlas 至今沒拿到。依序試過七條路全部失敗（都是 Frida hook + 玩家配合觸發），最後改用純靜態定位載入路徑才找到真正的資料來源。中間有一次把 <code>set_MainTexture</code> 當成正確掛點，繼續反編譯之後推翻。</p>
<blockquote>
<p><strong>更正</strong>：<code>set_MainTexture</code> 假設證實錯誤。當時它「看起來就是那個」，而且 hook 上去也真的有東西——只是那個東西不是我要的。<strong>hook 到東西不等於 hook 對地方。</strong></p>
</blockquote>
<p><strong>狀態：未完成。剩餘選項是 Xcode Metal Frame Capture。</strong></p>
<p>本節來源：<a href="#doc-ClaudeDocs-RE_NOTES_LIVE2D"><code>RE_NOTES_LIVE2D.md</code></a></p>
<p><strong>References</strong></p>
<ul>
<li><a href="https://github.com/OpenL2D/moc3ingbird">https://github.com/OpenL2D/moc3ingbird</a>（moc3 的 ImHex pattern）</li>
</ul>
<hr>
<h2 id="7-彈簧骨物理-未完成">（7）彈簧骨物理（未完成）</h2>
<p>裙擺、緞帶、頭髮不隨動畫出貨——烘焙只涵蓋 51 根 humanoid 骨，模型實際有 126 根，少的 75 根（裙擺 31、臉頰 10、緞帶 8…）由遊戲的 Swing/Quartz 在執行期算。</p>
<p>但<strong>參數有出貨</strong>，內嵌在每個模型 bundle 的 MonoBehaviour 裡：</p>
<pre tabindex="0"><code>ActorSwingDynamicBone   掛在 _sim 骨      被模擬的骨
ActorSwingStaticBone    掛在身體骨        碰撞體
ActorSwingChain         掛在 hips         鏈結構
QuartzDriverSkirtBone   掛在 _ast 骨      程序式輔助骨
</code></pre>
<p>（catalog 的 address 搜不到這些，是因為 address 不含 MonoBehaviour 類別名——跟當初搜 Avatar 犯的是同一個錯。）</p>
<p>離線重現的積分器：</p>
<pre tabindex="0"><code>step    = min(dt, 1/60) × 40
inertia = (pos - prevPos) × (1 - damping)²
force   = CalcStiffnessPendulum(...) + childSpeed × spring
newPos  = pos + inertia + force × step
newPos.y -= mass × 0.01            重力
                                   之後：骨長硬約束，再轉成父骨的旋轉
</code></pre>
<p><code>CalcStiffnessPendulum</code>（<code>dynamicType == 0</code>，佔 6900/6940 根骨）：</p>
<pre tabindex="0"><code>delta = rotate(parent.worldRot, boneAxis)      骨的靜止方向
cos   = |dot(cur, rest)| / (|cur|·|rest|)      兩個位置向量的夾角
p     = max(0, cos - (1 - range)) / range × pendulum
return delta × (stiffness - p) × 0.01
</code></pre>
<p>座標是 <strong>animator root space</strong> 不是世界座標，所以直接用 GLB 的骨架階層算就對。</p>
<p>實作裡的積分主迴圈：</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0" data-language="python" data-theme="github-light-high-contrast github-dark-high-contrast"><code data-language="python" data-theme="github-light-high-contrast github-dark-high-contrast" style="display: grid;"><span data-line=""> </span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">            for</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> _ </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">in</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> range</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(n_sub </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">+</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> warm):</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                cur, pv </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> pos[ni], prev[ni]</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                inertia </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> (cur </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">-</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> pv) </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">*</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> (</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">1.0</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> -</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> damping) </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">**</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> 2</span></span>
<span data-line=""><span style="--shiki-light:#66707B;--shiki-dark:#BDC4CC">                # cos 是 prevPos 與 pos 的夾角 (呼叫端 childTx=-0xf0=prevPos,</span></span>
<span data-line=""><span style="--shiki-light:#66707B;--shiki-dark:#BDC4CC">                # childDefaultTx=(s13,s11,s12)=pos)。0x02793a04 把 -0xf0</span></span>
<span data-line=""><span style="--shiki-light:#66707B;--shiki-dark:#BDC4CC">                # 寫回 child.selfTx.translation, 證實它就是子骨的位置。</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">                if</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> pen </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">&#x3C;=</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> 1e-5</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> or</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> rng </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">&#x3C;=</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> 1e-5</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">:</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                    p_term </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> 0.0</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">                else</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">:</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                    na, nb </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> np.linalg.norm(pv), np.linalg.norm(cur)</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                    cosv </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> abs</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">float</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(np.dot(pv, cur))) </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">/</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> max</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(na </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">*</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> nb, </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">1e-9</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">)</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                    p_term </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> max</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">0.0</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, cosv </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">-</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> (</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">1.0</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> -</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> rng)) </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">/</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> rng </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">*</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> pen</span></span>
<span data-line=""><span style="--shiki-light:#66707B;--shiki-dark:#BDC4CC">                # delta = rotate(**當前**的 selfTx.rotation, boneAxis) —— §35 釘死:</span></span>
<span data-line=""><span style="--shiki-light:#66707B;--shiki-dark:#BDC4CC">                # selfTx 是迴圈攜帶狀態, 每個子步讀到的是上一子步的模擬結果,</span></span>
<span data-line=""><span style="--shiki-light:#66707B;--shiki-dark:#BDC4CC">                # 不是動畫給的靜止姿勢。骨的當前方向就是 (pos - 骨位置) 正規化。</span></span>
<span data-line=""><span style="--shiki-light:#66707B;--shiki-dark:#BDC4CC">                # 用動畫的 rest_dir 等於憑空多給一個遊戲裡沒有的角度回復力。</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">                if</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> args.delta_current:</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                    cd </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> cur </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">-</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> anchor      </span><span style="--shiki-light:#66707B;--shiki-dark:#BDC4CC"># §39: 原本用 WT[ni], 與骨長約束的基準不一致</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                    cn </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> float</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(np.linalg.norm(cd))</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                    dvec </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> cd </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">/</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> cn </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">if</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> cn </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">></span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> 1e-9</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> else</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> rest_dir</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">                else</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">:</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                    dvec </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> rest_dir</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                force </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> dvec </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">*</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> (stiff </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">-</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> p_term) </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">*</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> 0.01</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> +</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> csp</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">                if</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> args.vel </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">==</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> "off"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">:</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                    new </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> cur </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">+</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> inertia </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">+</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> force </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">*</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> step</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">                else</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">:</span></span>
<span data-line=""><span style="--shiki-light:#66707B;--shiki-dark:#BDC4CC">                    # §36: 0x02793408/0x02793410 讀寫 [x20+0x54] 這個累加器 ——</span></span>
<span data-line=""><span style="--shiki-light:#66707B;--shiki-dark:#BDC4CC">                    #   fmul s1, s0, s5             力 × step</span></span>
<span data-line=""><span style="--shiki-light:#66707B;--shiki-dark:#BDC4CC">                    #   fmul v13.2s, v0.2s, v2.s[0] × swingPowerWeight (實測 1.0)</span></span>
<span data-line=""><span style="--shiki-light:#66707B;--shiki-dark:#BDC4CC">                    #   fadd v4.2s, v13.2s, v0.2s   累加進狀態</span></span>
<span data-line=""><span style="--shiki-light:#66707B;--shiki-dark:#BDC4CC">                    # 力不是加到位置, 是加進狀態; 狀態才改位置。</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                    vel[ni] </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> vel[ni] </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">*</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> (</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">1.0</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> -</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> args.vel_damp) </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">+</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> force </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">*</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> step</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">                    if</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> args.vel </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">==</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> "add"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">:</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                        new </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> cur </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">+</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> inertia </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">+</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> vel[ni] </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">*</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> step</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">                    else</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">:</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                        new </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> cur </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">+</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> vel[ni] </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">*</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> step</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">                new[</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">1</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">] </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">-=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> mass </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">*</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> 0.01</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> *</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> args.gravity_scale  </span><span style="--shiki-light:#66707B;--shiki-dark:#BDC4CC"># 重力 (每個子步)</span></span></code></pre></figure>
<p><a href="https://git.siao.ai/siao/hohohololive/blob/main/sim_swing.py#L705-L742">在 SiaoHub 檢視 siao/hohohololive/sim_swing.py L705-742</a></p>
<h3 id="現況">現況</h3>
<p>驗證方式是拿遊戲真值逐幀比對——<code>bake_swing_truth.py</code> 從遊戲錄下同一個 clip 的 <code>_sim</code> 骨 local rotation，逐幀算夾角：</p>
<pre tabindex="0"><code>角度 = 2 · arccos(|dot(q_sim, q_truth)|)
</code></pre>
<p>四元數的正負號不影響姿勢，所以取絕對值。</p>
<pre tabindex="0"><code>誤差 / 真值動作幅度   110%      (100% = 彈簧骨完全不動)
</code></pre>
<p><strong>還是略差於「完全不做」。</strong> 症狀收斂到單一項：過擺 1.50 倍。已實作但預設關掉的四項（各自因為下游還缺阻尼而被懲罰）：</p>
<pre tabindex="0"><code>--quartz          QuartzDriverSkirtBone 驅動 _ast 鏈根   142%
--delta-current   delta 用當前方向而非靜止方向           153%
--collision       球 vs 錐形膠囊                         202%
--vel             力累加進速度狀態                       147%
</code></pre>
<p>仍未實作：風（<code>CalcWindPower</code>，實測 <code>windPower = 0.7</code> 是開著的）、鏈骨平滑的第四趟、多變體的 <code>_ast</code> 驅動。</p>
<blockquote>
<p><strong>更正</strong>：當初判斷「風會增加擺動、方向不對」而擱置——<strong>那是在還有四個 bug 的模型上做的判斷，該重測。</strong> 在錯誤的基線上做的排除，不算排除。</p>
</blockquote>
<p><strong>狀態：未完成。</strong> 這是整個專案唯一還開著的東西。</p>
<p>本節來源：<a href="#doc-ClaudeDocs-RE_NOTES_AVATAR"><code>RE_NOTES_AVATAR.md</code></a> · 現況見 <a href="#doc-ClaudeDocs-HANDOFF"><code>HANDOFF.md</code></a> 與 <a href="#doc-ClaudeDocs-NEXT_STEPS"><code>NEXT_STEPS.md</code></a></p>
<hr>
<h2 id="8-公開範圍">（8）公開範圍</h2>
<p>解密演算法解出來了，也寫成了完整的數學規格和可執行工具。那部分不在這裡，也不在公開 repo 裡。</p>
<p>理由是法律。台灣著作權法 §80-2 禁止提供「主要用於規避防盜拷措施之設備、器材、零件、<strong>技術或資訊</strong>」——「資訊」二字涵蓋的不只是程式碼，也涵蓋一份寫得夠清楚、讀完就能自己實作的規格文件。同條第三項有「為達成資訊間之相互操作性所為之還原工程」的例外，但一般理解是涵蓋<strong>做</strong>還原工程，不是<strong>公開發佈</strong>規避方法。灰色地帶，而我不是律師。</p>
<p>（所以本文與 sssekai 的筆記歸檔在這一點上不同——那邊會貼出完整的 key table 與 AES key/iv，這邊不會。這是我對自己所在法域的保守判斷，不是對別人做法的評價。）</p>
<p>工具切開發佈：格式解析那一半（AssetBundle 抽取、mesh／骨架、glTF 轉檔、Live2D 與 3D 動作解碼）是互通性工作，公開在 <a href="https://git.siao.ai/siao/hohohololive">https://git.siao.ai/siao/hohohololive</a>；解密那一半沒有。</p>
<p>切法本身有個轉折值得記。原本打算做常見的「拿掉金鑰、留下演算法」，但那不成立——金鑰是從 <code>address</code> 推導出來的，推導程序本身就是金鑰，沒有獨立的秘密可以拿掉。而實作裡唯一的魔術常數是<strong>單一位元組</strong>，已知明文就寫在檔頭，256 種可能是微秒級的窮舉。只遮那個常數，降低的工作量是零，卻會做出一個「看起來遮過、其實沒有」的東西。所以整層一起拿掉。</p>
<p>素材完全沒有公開，遊戲資源的版權屬於發行商，一個 byte 都不在任何我發佈的地方。</p>
<hr>
<h2 id="附-踩過的坑">（附）踩過的坑</h2>
<p><strong>用小檔測傳輸速度。</strong> 換完 cipher 後測了一個小檔，它在寫進磁碟快取的瞬間就「傳完」了。一個改動之後看到改善，不代表是那個改動造成的。</p>
<p><strong>Dump 了 234MB 不需要的記憶體。</strong> 磁碟上的 <code>UnityFramework</code> 本來就沒加密。更糟的是記憶體版本反而不能用：</p>
<pre tabindex="0"><code>磁碟    __TEXT 緊密排列，file offset == vaddr - base
記憶體  __TEXT page 對齊，兩者相差各段的對齊 padding
</code></pre>
<p>位址對不起來，解析工具直接壞掉。加密的是 metadata 不是 binary，我沒分清楚就對兩者都用了對付加密的手段。</p>
<p><strong>延後讀取 native buffer。</strong> native <code>read()</code> 的緩衝區在返回後很快被回收挪作他用，延遲讀到的都是不相關的殘留——一度誤判成查表邏輯、UTF16 字串、bplist。必須在 <code>onLeave</code> 當下同步 dump：</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0" data-language="javascript" data-theme="github-light-high-contrast github-dark-high-contrast"><code data-language="javascript" data-theme="github-light-high-contrast github-dark-high-contrast" style="display: grid;"><span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">Interceptor.</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">attach</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(Module.</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">findExportByName</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">null</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"read"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">), {</span></span>
<span data-line=""><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">  onEnter</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">args</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">) { </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">this</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">.buf </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">=</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> args[</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">1</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">]; },</span></span>
<span data-line=""><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">  onLeave</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">ret</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">) {</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">    const</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> n</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> =</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> ret.</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">toInt32</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">();</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">    if</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6"> (n </span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">></span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF"> 0</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">) </span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">send</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">({tag: </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"read"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, n}, </span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">this</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">.buf.</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">readByteArray</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(n));  </span><span style="--shiki-light:#66707B;--shiki-dark:#BDC4CC">// 當下，不能延後</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">  }</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">});</span></span></code></pre></figure>
<p><strong>fd 重用汙染追蹤表。</strong> hook <code>open()</code> 記下有興趣的 fd 卻沒在 <code>close()</code> 清掉。系統把同一個 fd 數字重新分配給別的檔案之後，會把不相關的內容（<code>UnityFS</code> 檔頭、<code>bplist00</code>）當成目標檔案的內容：</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0" data-language="javascript" data-theme="github-light-high-contrast github-dark-high-contrast"><code data-language="javascript" data-theme="github-light-high-contrast github-dark-high-contrast" style="display: grid;"><span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">Interceptor.</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">attach</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(Module.</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">findExportByName</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">null</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">, </span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF">"close"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">), {</span></span>
<span data-line=""><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">  onEnter</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(</span><span style="--shiki-light:#702C00;--shiki-dark:#FFB757">args</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">) { tracked.</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">delete</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">(args[</span><span style="--shiki-light:#023B95;--shiki-dark:#91CBFF">0</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">].</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF">toInt32</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">()); }</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">});</span></span></code></pre></figure>
<p>後面兩個最值得記，因為它們不是「我推論錯了」，是<strong>觀測方法本身在製造假資料</strong>。假資料看起來跟真資料一模一樣，我還為它建構了好幾個解釋。</p>
<hr>
<h2 id="工具鏈">工具鏈</h2>
<table tabindex="0">
<thead>
<tr>
<th>用途</th>
<th>工具</th>
</tr>
</thead>
<tbody>
<tr>
<td>USB 埠轉發</td>
<td><code>libimobiledevice</code> / <code>iproxy</code></td>
</tr>
<tr>
<td>動態 instrumentation</td>
<td>Frida（Python API，不是 CLI）</td>
</tr>
<tr>
<td>IL2CPP 解析</td>
<td><code>Il2CppInspectorRedux</code>（LukeFZ fork）</td>
</tr>
<tr>
<td>反編譯</td>
<td><code>rizin</code> + <code>rz-ghidra</code></td>
</tr>
<tr>
<td>Unity 資產</td>
<td><code>UnityPy</code>（<code>FALLBACK_UNITY_VERSION = "6000.3.0b1"</code>）</td>
</tr>
</tbody>
</table>
<ul>
<li><strong>Frida 用 Python API，不要用 CLI。</strong> CLI 有 attach timeout 的問題，<code>device.spawn() → attach() → resume()</code> 穩定得多。</li>
<li><strong><code>Il2CppInspectorRedux</code> 的 CLI 會假死。</strong> 內建的 SignalR web 服務有機會讓整個 process 卡住，所有執行緒 idle 在 <code>__psynch_cvwait</code>。用 <code>sample &#x3C;pid></code> 就能看出它不是在忙、是在等。改用更輕量的輸出選項可以繞過。</li>
</ul>
<h2 id="references">References</h2>
<ul>
<li><a href="https://github.com/mos9527/sssekai">https://github.com/mos9527/sssekai</a> · <a href="https://mos9527.com/posts/pjsk/archive-20240105/">Project SEKAI 逆向 - 筆記歸檔</a></li>
<li><a href="https://github.com/LukeFZ/Il2CppInspectorRedux">https://github.com/LukeFZ/Il2CppInspectorRedux</a></li>
<li><a href="https://github.com/rizinorg/rz-ghidra">https://github.com/rizinorg/rz-ghidra</a></li>
<li><a href="https://github.com/OpenL2D/moc3ingbird">https://github.com/OpenL2D/moc3ingbird</a></li>
<li><a href="https://protobuf.dev/programming-guides/encoding/">https://protobuf.dev/programming-guides/encoding/</a></li>
<li><a href="https://frida.re/docs/javascript-api/">https://frida.re/docs/javascript-api/</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>你好，世界</title>
      <link>https://blog.siao.ai/hello-world/</link>
      <guid isPermaLink="true">https://blog.siao.ai/hello-world/</guid>
      <pubDate>Sat, 22 Aug 2026 00:00:00 GMT</pubDate>
      <description>第一篇文章，順便驗證整條 Markdown 渲染管線。</description>
      <content:encoded><![CDATA[<p>這是第一篇文章，用來確認整條管線都通了：GFM 表格、數學公式、程式碼高亮。</p>
<h2 id="gfm-表格">GFM 表格</h2>
<table tabindex="0">
<thead>
<tr>
<th>功能</th>
<th>狀態</th>
</tr>
</thead>
<tbody>
<tr>
<td>Markdown</td>
<td>✅</td>
</tr>
<tr>
<td>數學公式</td>
<td>✅</td>
</tr>
<tr>
<td>程式碼高亮</td>
<td>✅</td>
</tr>
</tbody>
</table>
<h2 id="數學公式">數學公式</h2>
<p>質能等價：<span class="katex"><span class="katex-mathml"><math><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation>E = mc^2</annotation></semantics></math></span><span class="katex-html"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.0576em;">E</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8141em;"></span><span class="mord mathnormal">m</span><span class="mord"><span class="mord mathnormal">c</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></p>
<h2 id="程式碼">程式碼</h2>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-light:#0e1116;--shiki-dark:#f0f3f6;--shiki-light-bg:#ffffff;--shiki-dark-bg:#0a0c10" tabindex="0" data-language="js" data-theme="github-light-high-contrast github-dark-high-contrast"><code data-language="js" data-theme="github-light-high-contrast github-dark-high-contrast" style="display: grid;"><span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">export</span><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492"> function</span><span style="--shiki-light:#622CBC;--shiki-dark:#DBB7FF"> hello</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">() {</span></span>
<span data-line=""><span style="--shiki-light:#A0111F;--shiki-dark:#FF9492">  return</span><span style="--shiki-light:#032563;--shiki-dark:#ADDCFF"> "world"</span><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">;</span></span>
<span data-line=""><span style="--shiki-light:#0E1116;--shiki-dark:#F0F3F6">}</span></span></code></pre></figure>
<hr>
<p>管線都通了，接下來才是真正要寫的東西。</p>]]></content:encoded>
    </item>
  </channel>
</rss>
