推上 Larch
2026-10-24,新版推上 Larch。新版只放新地圖,從序幕接到第九章結局,推上去的是 921 張卡。這一章記整份專案送到平台時遇到的容量問題、最後怎麼分批、推完怎麼確認。單張地圖與播放器的限制寫在Larch RPG 引擎實測,這裡只講整包送上去的那一段。

整份送不上去
Larch 的寫入端點收的是一份專案 JSON。圖與音不在裡面:角色、地圖、CG、音效與配樂都放在公開素材庫,專案裡只留封面與一張透明的武器替代圖。會被擋的是這份 JSON 的大小。
整個專案存在 Larch 上有大小上限,含所有白板,圖與音不算。這個上限原本是 80 MB,官方在我們推這部作品的時候提高到 128 MB,不然這部放不下:完整的 921 張卡,讀回來的緊湊 JSON 約 111 MiB,超過了舊的 80 MB。
push_larch.py 的註解留著兩次失敗:整份 115 MB 送出去被 413 擋下,107 MB 也被擋過。超過 50 MB 時程式會先 gzip 再送,壓縮沒有讓它過關。程式裡只留下被擋的大小,單次請求的上限是多少,官方沒有寫,我們沒有查證,所以這一章不寫一個數字。能確定的是:整份一次送不進去,所以改成先送一半、再分批補。
後面還要補內容,容量會再吃緊,所以之後要瘦身。官方給的方向有兩個:牆與圍欄改畫進底圖、再加「通行筆刷」,這樣地圖卡裡的牆事件可以拿掉;重複的事件搬進「資料庫・共用事件」。這兩項這次都還沒做。
卡片為什麼有兩份
送出去的 project.json 裡,卡片同時存在 boards[0].nodes 與最外層 nodes,兩份是同一張清單。Larch 的預覽播放器讀最外層那一份。push_larch.py 的 slim() 註解寫得很直接:「預覽播放器讀的是最上層那份。」
2026-10-23 為了省一半大小,推送時把最外層拿掉,只送白板。專案推上去了,線上預覽停在標題,按開始遊戲進不去;預覽快照的 nodes 是 0 張。本機預覽的 serve.py 會從白板補回這兩份,所以本機測不出來。此後 verify() 讀回一定比對白板與最上層的張數,兩邊都要跟本機要送的張數一樣,對不上就停,不開預覽。
先送一半,再一批一批補
最後的作法寫在 push_larch_split.py。先把卡片照起點連線的順序排,前半用一次 PUT 送一份完整專案,其餘用「新增卡片」的端點分批 POST。前半預設最多 40 MB,這次湊到 246 張、36.4 MB;後半 675 張分五批,9.7、9.9、9.8、10.0、0.5 MB。
| 批次 | 卡數 | 送出大小 | 寫完的 ETag |
|---|---|---|---|
| 前半 PUT | 246 | 36.4 MB | 44 |
| POST 第 1 批 | 47 | 9.7 MB | 45 |
| POST 第 2 批 | 330 | 9.9 MB | 46 |
| POST 第 3 批 | 43 | 9.8 MB | 47 |
| POST 第 4 批 | 246 | 10.0 MB | 48 |
| POST 第 5 批 | 9 | 0.5 MB | 49 |
前半 PUT 花 29 秒。每次寫入都帶上一個 ETag,寫完讀回新的 ETag,下一批接著用;寫入時帶 If-Match,別條線同時改同一個專案會撞 ETag,不會悄悄蓋掉。表裡的 ETag 從 44 走到 49。
參數是上限,每批不一定裝滿。一張卡不能切開,湊到下一張會超過就換批,所以第 2 批 330 張只有 9.9 MB,第 5 批 9 張只有 0.5 MB。前半先送,是因為後面任一批失敗時,Larch 上還是一份從序幕開始、可以讀的專案。要接續時用 --resume 讀回已有的卡,只補缺的;它不會修已經寫錯的卡。後半每批只送一份卡片,讀回時白板與最上層都補到 921 張。
push_larch_split.py 的檔頭寫「失敗一律直接停,不重試」。只有 429 例外:push_larch.py 的 request() 會照 Retry-After 等完再送,因為 429 表示這次沒有寫進去,重送安全。其他錯誤直接拋出。停在半路時,專案是只有前半的那一版;上一個完整版本留在 Larch 的版本紀錄,程式在推送前也會擋下推到舊版專案。
新版只送從起點走得到的卡:
只送從起點走得到的卡:921 張(另外 0 張還沒接上,這次不送)
後來改成兩次寫入
2026-10-29 專案長到約 100 MB,又推了好幾次,才把送法改成兩次寫入,過程記在這裡。
先講最後的結果:前半 728 張一次 PUT(66.5 MB,70 秒),後半 272 張一次 POST(16.8 MB),專案讀回 1000 張、沒有重複。以前分 4 到 22 批,每批都比較慢,也比較常出錯。
為什麼分很多批反而差:POST 新增卡片的回應,是整份專案(約 100 MB)。每送一批,就要把一整份專案再下載一遍。批數越多,下載越多,Larch 對寫入的限流也越容易撞到。
我們一度以為「大批 POST 會被擋」,因為連續三次大批都看到錯誤(500、503、讀回應被截斷)。後來讀回專案才發現,那一批其實已經寫進去,錯誤只出在「讀回應」那一步,Larch 的儲存層在大量寫入之後會暫時回 502、503,約一分鐘恢復。
所以程式改了三處:
- 前半卡片盡量大(33 MB,白板與最上層各一份,送出約 67 MB,低於單次請求上限),後半一批送完。
- POST 或 PUT 的回應被截斷時,不當成失敗、也不重送(重送會寫兩次),改用 HEAD 問最新版本號,最後用讀回比對卡數把關。HEAD 只回版本號、不下載 100 MB。
- Larch 回 503 並標「可重試」時,照它說的等幾秒再送,最多 3 次。
另外三件事也寫在這裡,下次推之前要記得:
- 整份專案讀完要等組內容,期間別處存檔會讓版本號變,寫入就會 409。所以寫入前一刻再用 HEAD 問一次版本號。
- 專案裡每張地圖的資料原本是 JSON 字串,每個引號都要跳脫,多出約 28%。改成直接存物件後,專案從 115.0 MB 降到 100.1 MB,Larch 伺服器、線上播放器、本機播放器都實測讀得進,伺服器還會把等於預設值的欄位整理掉。代價是 Larch 編輯器裡打開地圖卡的「編輯地圖」可能開不了,這部分沒測過。
- 不要在推送過程中呼叫預覽網址,也不要開 Larch 編輯器分頁:兩者都可能讓版本號變動。
素材走 CDN
這次新進版控的圖有 258 張:100 張 CG、158 張立繪差分。遊戲從公開素材庫的固定 CDN 網址取圖,Larch 專案裡只留封面 xu-cg-title 與透明替代圖 xu_w-none。讀回時,專案裡有 3,760 個不同的 CDN 網址,Larch 素材只有那兩個。
圖放在 CDN,換圖不必重傳整份專案,專案 JSON 也不會被圖片撐大。代價是線上預覽要載得到 CDN;推完的檢查要把素材請求一起看。
推完一定要開線上預覽
2026-10-24 推完,開線上預覽:標題畫面有「成就」按鈕,按「開始遊戲」進得去,第一幕城牆下正常顯示。頁面錯誤 0,CDN 請求成功 7 個,1 個 mp3 回 net::ERR_ABORTED,畫面照常。聲音那一項還要人聽,不能只看請求。
本機預覽看不出最上層少一份,這件事只有線上預覽會抓到。ETag 對上只證明卡片寫進去了,還要按開始遊戲;證明不了播放器讀得到。
結果與還沒做的事
讀回 ETag 49,白板 921 張,最上層 921 張,跟本機要送的 921 張一致。遠端讀回的緊湊 JSON 約 111 MiB。線上預覽按開始遊戲進得去,第一幕正常。
這一節只說推得上去、進得去,沒有說全劇走通。這次推的是 10-24 清晨的建置;推完之後,逐格走路模擬器在這份推送用的建置仍停在第二章 B1,修正版在另一個建置走通,還沒有重新建置與推送。詳情寫在試玩與回饋。
下一個版本還能做的瘦身,是把重複的觸發與條件收進 RPG 系統的公用事件。成就已經這樣做過:最早每張地圖插一個條件事件,檔案多 8.5 MB;改成公用事件後只多約 0.18 MB(xu/game/docs/成就.md)。卡片再長下去,先跑 --dry-run 算前半與每批多大,再決定要不要調 SPLIT_FIRST_MB、SPLIT_BATCH_MB,會比送到一半才被擋好處理。