Browser Game Modding · Case Study

Tower Swap
遊戲修改實戰教學

完整記錄如何分析 HTML5 遊戲結構、找到關鍵變數、並透過修改 JavaScript 原始碼實現無限 Swap 的完整歷程

目標遊戲 Tower Swap (CrazyGames) 技術棧 JS Reverse · Tampermonkey · Firefox DevTools · Python Proxy 目的 個人娛樂,不影響其他玩家
01

前言與目標

Tower Swap 是一款結合 Match-3 配對與 Tower Defense 的益智遊戲,玩家每回合有限制的 Swap 次數來排列資源與防禦塔。這份筆記記錄了如何在不影響其他玩家的前提下,修改本機的遊戲執行環境,讓 Swap 次數不再減少,以便純粹享受規劃佈局的樂趣。

⚠️
倫理聲明:本修改僅作用於本機瀏覽器,不會影響伺服器端資料或其他玩家的排行榜。遊戲本身也有反作弊系統,高分記錄仍會被伺服器驗證。
核心目標
  • 讓每次 Swap 動作不扣除 Swap 次數
  • 遊戲啟動時給予充足的初始 Swap 數量
  • 城堡血量不歸零觸發遊戲結束(選配)
02

遊戲技術結構分析

在動手之前,需要先了解遊戲是用什麼方式跑起來的。打開 Firefox DevTools 後,發現這個遊戲有三層 iframe 巢狀結構,這是後來許多方法失效的根本原因。

www.crazygames.com/game/tower-swap
↓ iframe
games.crazygames.com/...index.html
↓ iframe (game-iframe)
tower-swap.game-files.crazygames.com/tower-swap/96/index.html
← 遊戲實際執行層,所有遊戲變數都在這裡
↓ <script src> 靜態標籤載入
game.js(壓縮後約 410KB 的單一 JS 檔)
所有遊戲邏輯都打包在這個檔案裡
🔍
關鍵觀察:遊戲存檔使用 localStorage,所有遊戲狀態(swap 數、血量、棋盤布局)都以 tb_0_m_* 為前綴存放在瀏覽器本地。但遊戲執行時使用的是記憶體內的 closure 變數,不是每次都從 localStorage 讀取。

透過 Console 觀察 gNativeSend() 的輸出,可以看到所有遊戲狀態:

CONSOLE OUTPUT
gNativeSend() savetext:=tb_0_m_swaps:=4
gNativeSend() savetext:=tb_0_m_castlehealth:=5
gNativeSend() savetext:=tb_0_m_day:=1
gNativeSend() savetext:=tb_0_m_board:=e0e0e0e0e0e0g0s0i0...
03

嘗試歷程與推理軌跡

這個過程經歷了多個方向的嘗試,每次失敗都帶來新的資訊,逐步縮小問題範圍。

❌ 嘗試一:直接存取全域變數
在 Console 嘗試直接輸入 tb_0_m_swaps = 99,得到 ReferenceError
推理:變數不在全域 window 上,被包在某個 closure 裡。
❌ 嘗試二:Tampermonkey 掃描全域物件
寫腳本掃描 window 下所有物件,尋找含有 swaps 屬性的物件。
推理:掃描結果只找到廣告相關的全域物件(googletaggoogle 等),確認遊戲變數完全被 IIFE 封鎖。
⚠️ 嘗試三:切換 iframe Context
發現 Console 有 iframe context 切換功能,找到 「Tower Swap」選項並切換進入。
推理:確認遊戲在 tower-swap.game-files.crazygames.com,但 gNativeSendtb_0_m_swaps 仍是 undefined,因為這些都在 game.js 的 IIFE closure 內。
❌ 嘗試四:攔截 gNativeSend(Object.defineProperty)
Object.defineProperty(window, 'gNativeSend', { set: fn }) 在變數被賦值時攔截。
推理:typeof gNativeSend 回傳 null,表示遊戲初始化時設為 null,之後在 closure 內部替換,我們的 setter 攔截不到。
❌ 嘗試五:攔截網路請求(fetch / XHR / WebSocket)
同時攔截 fetchXMLHttpRequestWebSocket,尋找遊戲儲存 swap 的網路通訊。
推理:做了 swap 動作後完全沒有任何網路攔截觸發,確認遊戲純本地儲存,使用 localStorage,沒有即時伺服器通訊。
⚠️ 關鍵發現:localStorage 就是存檔位置
直接查詢 localStorage,找到 57 筆資料,全部以 tb_0_m_ 為前綴,包括 tb_0_m_swaps: 8
推理:確認了儲存機制,但問題是遊戲讀取後把值存在記憶體變數 r6 裡,不再每次從 localStorage 讀,所以修改 localStorage 無法即時影響遊戲。
❌ 嘗試六:攔截 localStorage.setItem
替換 Storage.prototype.setItem,在寫入時把低 swap 值改高。
推理:攔截雖然有效(寫入值被改了),但遊戲 UI 顯示的是記憶體內的 r6 值,不是 localStorage,所以畫面上看不到任何變化。
❌ 嘗試七:透過 J 物件攔截主迴圈
從 game.js 原始碼找到 J.loopstart(updateFn, drawFn) 是遊戲主迴圈入口,嘗試替換 J._updatefunc
推理:在 Console 測試 typeof J 回傳 undefined,確認 J 物件也在 IIFE 內部,無法從外部存取。
❌ 嘗試八:fetch 攔截 game.js 載入
用 Tampermonkey 的 unsafeWindow.fetch 攔截 game.js 的下載請求,在傳回前修改原始碼。
推理:Console 沒有出現攔截訊息,因為 game.js 是用靜態 <script src> 標籤載入,不是用 fetch(),根本不會觸發我們的攔截器。
❌ 嘗試九:攔截 document.createElement
替換 document.createElement,當建立 script 標籤時攔截 src 屬性設定。
推理:<script src="game.js?113"> 是直接寫在 HTML 原始碼裡的靜態標籤,不是 JavaScript 動態建立的,所以 document-start 時這個標籤早就存在了。
✅ 突破:直接下載並分析 game.js 原始碼
從瀏覽器 Console 用 fetch 下載 game.js 並存成檔案,上傳分析。找到了所有關鍵變數和函數。
推理:既然無法從外部注入,就直接修改遊戲原始碼本身,在傳入瀏覽器之前就改掉。
✅ 最終解法:Python Proxy + Firefox 網路覆蓋
建立 Python proxy 伺服器,下載並修改 game.js 後提供,再用 Firefox DevTools 的「設定網路覆蓋」功能把 game.js 替換成本機修改版。
結果:Swap 次數永遠不再減少!
04

game.js 原始碼關鍵分析

下載 game.js 後(約 410KB 的壓縮單行檔),用 grep 找到以下關鍵結構:

核心變數對應
內部變數 localStorage 鍵名 意義
H 就是 localStorage 本身(H = localStorage
V 前綴字串 "tb_0_m_"
r6 tb_0_m_swaps 當前 Swap 次數(執行時在記憶體,存檔寫 localStorage)
r4 Swap UI 動畫計數器(r4 = 3 * r6
tn tb_0_m_castlehealth 城堡血量

儲存與讀取函數:

game.js(解析後)
// 寫入函數:同時寫 localStorage 和通知平台
function _C(value, key) {
  H[V + key] = value;  // 等同 localStorage["tb_0_m_" + key] = value
  if ("web" != gPlatform) mM("savetext:=" + key + ":=" + value);
}

// 讀取函數:從 localStorage 讀
function _b(key) { return 1 * _k(key) || 0; }
function _k(key) { return H[V + key] || ""; }

// 遊戲初始化時讀取 swap(之後存在 r6 記憶體變數,不再重讀)
r4 = 3 * (r6 = _b("swaps"));
關鍵洞察: _C() 函數每次 swap 後都會把 r6 寫入 localStorage,但遊戲畫面顯示和邏輯判斷用的是記憶體裡的 r6。所以單純修改 localStorage 無法影響遊戲,必須修改 r6 本身——而唯一的方法就是修改 game.js 原始碼。

找到 Swap 被扣除的兩個地方:

game.js — 需要修改的位置
// 【修改一】dE() — 每次 swap 動作觸發,扣除 1 點
function dE() {
  nW=nH, nM=nV,
  $w() || sj==sW || sj==sU || sj==sM || (
    r6-=1, ← 改成 r6-=0
    rw=r6, r4=3*r6
  ),
  dN(), it=0, ...
}

// 【修改二】Boss 關卡計時器 — 時間到自動扣 swap
(r6>0 && (nA+=1), nA>=60 && (
  nA=0,
  (r6-=1)<0 && (r6=0), ← 改成 (r6-=0)<0&&(r6=0)
  r4=3*r6,
  (rw-=1)<0 && (rw=0)  ← 改成 (rw-=0)<0&&(rw=0)
))

// 【修改三】遊戲啟動讀取初始 swap 值
r4=3*(r6=_b("swaps"))
← 改成 r4=3*(r6=Math.max(_b("swaps"),99))
05

最終解法:完整操作步驟

瀏覽器請求 game.js
Firefox 網路覆蓋攔截
DevTools Network Override
Python Proxy 伺服器
下載原始 + 套用修改
修改後的 game.js 進入瀏覽器
r6-=0,swap 永不減少

Step 1:建立 Python Proxy 伺服器

proxy.py(Rocky Linux)
from flask import Flask, Response
import requests

app = Flask(__name__)
GAME_JS_URL = "https://tower-swap.game-files.crazygames.com/tower-swap/96/game.js?113"

@app.route("/game.js")
def patched_game_js():
    r = requests.get(GAME_JS_URL)
    code = r.text

    # 修改一:dE() 扣除
    code = code.replace("r6-=1,rw=r6,r4=3*r6", "r6-=0,rw=r6,r4=3*r6")

    # 修改二:Boss 計時器扣除
    code = code.replace(
        "(r6-=1)<0&&(r6=0),r4=3*r6,(rw-=1)<0&&(rw=0)",
        "(r6-=0)<0&&(r6=0),r4=3*r6,(rw-=0)<0&&(rw=0)"
    )

    # 修改三:初始 swap 最少 99
    code = code.replace(
        'r4=3*(r6=_b("swaps"))',
        'r4=3*(r6=Math.max(_b("swaps"),99))'
    )

    return Response(code,
        content_type="application/javascript",
        headers={"Access-Control-Allow-Origin": "*"}
    )

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=18080)

Step 2:確認 Proxy 運作

在瀏覽器開啟 http://伺服器IP:18080/game.js,用 Ctrl+F 搜尋 r6-=0,確認修改已套用。

Step 3:Firefox 網路覆蓋

驗證方法:在 Tower Swap iframe context 的 Console 輸入以下指令,確認覆蓋是否生效:
fetch('game.js?113').then(r=>r.text()).then(code=>{
  console.log('r6-=0 存在:', code.includes('r6-=0,rw=r6'));
  console.log('r6-=1 存在:', code.includes('r6-=1,rw=r6'));
});
06

方法對比總結

嘗試方法 失敗原因 結果
直接修改全域變數 所有遊戲變數在 IIFE closure 內 失敗
攔截 gNativeSend 變數為 null,closure 內部替換 失敗
攔截網路請求 遊戲用 localStorage,無網路儲存 失敗
攔截 localStorage.setItem 遊戲 UI 讀記憶體變數,不讀 localStorage 部分
攔截 J._updatefunc J 物件也在 IIFE 內,外部無法存取 失敗
fetch 攔截 game.js game.js 用靜態 <script src> 載入,非 fetch 失敗
createElement 攔截 script 標籤是靜態 HTML,不是動態建立 失敗
Python Proxy + Firefox 網路覆蓋 ✅ 成功
核心教訓:現代 HTML5 遊戲通常把所有邏輯包在一個大 IIFE 裡,從外部注入幾乎不可能。當所有執行期注入都失敗時,在原始碼進入瀏覽器之前修改它是最可靠的方向。Firefox 的「網路覆蓋」功能提供了一個不需要修改系統設定或安裝額外軟體就能實現這個目標的方法。