說起來這已經是我快接近兩年前(2020年9月)做的 side project 了,最近想說把資料整理一下發個文。
Hordes.io

先簡單介紹一下這個遊戲,它就是一個經典的 MMORPG,等級、技能、道具、職業、組隊、團戰...等基本的要素都有,但是沒有像一般 MMORPG 那般廣大的地圖、豐富的 NPC 跟細緻的模型,並且可 以直接用網頁瀏覽器遊玩。
有一陣子我還蠻享受這個遊戲的,甚至有考慮剋金,不過營運方過於側重 PvP,不定期舉辦的活動也給我一種 Minecraft 伺服器那種娛樂 OP 吃力不討好的感覺,不過那又是另外一段故事了,總之最後就棄坑了。
動機
個人在 MMORPG 最享受兩種角色:補師跟生產者(偏偏正是主流遊戲較為不重視的兩種類型_(:3」∠)_),而在這遊戲確實滿足了我對補師職業的喜好,不過並沒有生產系統,所有道具與裝備都是透過打怪掉落,並且玩家與玩家之間的交易只能透過和重生點的商人對話開啟拍賣界面:

而且遊戲系統還會抽上架手續費,換句話說遊戲的經濟系統不夠強大,讓我「想對它做點什麼」,而這幾個前提成立下:
- 遊戲是以 Javascript 跑在網頁上的
- 有集中交易的拍賣功能
讓我「能對它做點什麼」,讓我手癢想把市場資料倒出來畫折線圖分析。
這世界上有兩樣東西能讓我開心:
- Data
- More data
by 我那特別喜歡 Data 的一部分靈魂
正文
一張圖解釋我做了什麼,流程大致上是:
- 逆向工程遊戲的 Javascript
- 在當中嵌入程式碼來獲取拍賣資訊
- 執行遊戲時以修改過得 client.js 取代官方原本的版本
- 修改過得腳本會把攔截到的拍賣資訊回傳到資料庫
- 分析資料
逆向工程主程式
Javascript 是直譯語言,即便一般開發者會經過 minify, uglify 等步驟降低腳本的可讀性,但是獲得程式後仍然是明碼可讀的,而遊戲的主程式是放在這個路徑中:
https://hordes.io/client.js?v=4305950
把程式下載後經過 beautify 加上縮排,再透過 Javascript 的基本語法跟一些沒有被混淆的變數名稱加上一些技巧來試圖了解程式運作,具體怎麼做的我就不贅述了,任何會寫 Javascript 的人應該都知道這些方法(?)
透過逆向工程了解程式運作之後就能添加程式碼來達成我的目的,比如:把拍賣資料送到我的資料庫。
fetch("/api/item/get", {
method: "POST",
body: JSON.stringify({
auction: 1,
ids: t
})
}).then(async t => {
const e = await t.json();
let items = [];
//edition
//e: object from json string
e.fail ? console.error(e) : (
s(11, P.length = 0, P), r.forEach((t, s) => {
const i = e.find(e => e.id === t.dbid);
i && (i.store = C[s] || (C[s] = Wt()), i.store.temp = t, t.hydrate(i), P.push(i.store));
let item = {
item_id: i.id,
price: i.auctionprice,
amount: (i.stacks ===null)?1:i.stacks,
tier: i.tier,
type: i.type,
upgrade: i.upgrade,
attributes:Object.fromEntries(i.store.temp.stats),
posted_by: i.name,
posted_at: i.auction
};
items.push(item);
//console.log(JSON.stringify(item));
}),
//console.log(JSON.stringify(items)),
fetch("http://0.0.0.0:8989/stack", {
method: 'POST', // *GET, POST, PUT, DELETE, etc.
mode: 'cors', // no-cors, *cors, same-origin
headers: {
'Content-Type': 'text/plain'
// 'Content-Type': 'application/x-www-form-urlencoded',
},
body: JSON.stringify(items) // body data type must match "Content-Type" header
}),
s(6, g = !0)
)
})