Flash 拆檔分析:靈夢的賽錢箱
某天我無意中翻到了一個 Flash 的小遊戲:靈夢的賽錢箱(霊夢の賽銭箱)。簡單來說,就是點博麗靈夢的賽錢箱。金額多少,靈夢的反應就會不同。
我就想,靈夢到底會有哪些反應呢?如果能捐一萬,會不會就可以推倒靈夢?甚至十萬 SM 玩起來?
我很好奇。
準備
正好我有用 JPEXS Free Flash Decompiler,決定試試看。另外想重溫 Flash 的話,可以試試 Ruffle 喔。
總之,需要準備以下軟體:
- Java 以執行 JPEXS Free Flash Decompiler。這裡使用 Eclipse Adoptium 實做的 OpenJDK。
- JPEXS Free Flash Decompiler。
- 能執行
.swf
檔案的程式。這裡用 Ruffle。 - Flash 檔案。也就是咱的主角靈夢的賽錢箱。原始檔名是 saisen.swf。
準備好以後用 JPEXS Free Flash Decompiler 打開 saisen.swf 檔案。應該會出現以下畫面。
啊,對,有點吵。所以快移到下面吧。
簡單看看
來看看有什麼東西吧──啊,最底下 script 耶。應該要先看這個?但……
……但不知道東西怎麼跑耶,還是先看看別的吧。那麼多可以看的資源,先看 shapes 如何?
資源
那來看看 shapes 資源吧。
shapes 這張 97 號。似乎是評分的反應?那就來看看吧。
Dependent Frames 2 裡面會連到 frames,但裡面沒有什麼實際有用的資訊。應該是幾個 sprites 合在一起?而 Dependent Characters 能連到 sprites 的 98, 123, 124 這三個。但這三個要看哪個?
先講 98 與 124 這兩個:98 就一張 sprite、124 是主畫面,直接讀會出問題。我這邊重開了三次吧。
那麼 123 呢?
很幸運,那似乎是遊戲結束,準備算結果的畫面。而且還很親切地標好了畫面名稱!texts 裡面也提供有用的線索。
對裡面的東西有信心了,就可以讀程式碼啦。
程式
剛剛第一張圖片的 script 裡面,程式是這樣寫的:
package saisen_fla
{
import flash.display.MovieClip;
import flash.events.Event;
public dynamic class MainTimeline extends MovieClip
{
public var bar_mc:MovieClip;
public var totalBytes:*;
public function MainTimeline()
{
super();
addFrameScript(0,frame1);
}
public function Loading(param1:Event) : void
{
var _loc2_:* = undefined;
var _loc3_:Number = NaN;
_loc2_ = this.root.loaderInfo.bytesLoaded;
_loc3_ = _loc2_ / totalBytes;
bar_mc.scaleX = _loc3_;
if(totalBytes == _loc2_)
{
removeEventListener(Event.ENTER_FRAME,Loading);
gotoAndStop(2);
}
else
{
gotoAndStop(1);
}
}
internal function frame1() : *
{
stop();
totalBytes = this.root.loaderInfo.bytesTotal;
addEventListener(Event.ENTER_FRAME,Loading);
}
}
}
比較有疑問的是 addFrameScript 這個。看了下是幹嘛,答案好像與觸發指定 frame 有關。不過總之,這程式看來是做遊戲載入的。我們想知道的賽錢結果沒在裡面。
這裡是神社。你懂的吧?
那麼 saisen_fla.Timeline_2
呢?
package saisen_fla
{
import adobe.utils.*;
import flash.accessibility.*;
import flash.display.*;
import flash.errors.*;
import flash.events.*;
import flash.external.*;
import flash.filters.*;
import flash.geom.*;
import flash.media.*;
import flash.net.*;
import flash.printing.*;
import flash.system.*;
import flash.text.*;
import flash.ui.*;
import flash.utils.*;
import flash.xml.*;
[Embed(source="/_assets/assets.swf", symbol="saisen_fla.Timeline_2")]
public dynamic class Timeline_2 extends MovieClip
{
public var replay_btn:SimpleButton;
public var resultSaisen:int;
public var resultReimu_mc:MovieClip;
public var startDate:*;
public var light_mc:MovieClip;
public var help_btn:SimpleButton;
public var _coin:Coin;
public var limitTime:*;
public var saisenCount_mc:MovieClip;
public var saisenbako_btn:SimpleButton;
public var saisen:int;
public var counter_mc:MovieClip;
public var toTitle_btn:SimpleButton;
public var return_btn:SimpleButton;
public var resultText:TextField;
public var start_btn:SimpleButton;
public function Timeline_2()
{
super();
addFrameScript(0,frame1,10,frame11,110,frame111,120,frame121,150,frame151);
}
internal function frame151() : *
{
stop();
if(resultSaisen == 0)
{
resultReimu_mc.gotoAndPlay("結果1");
}
else if(resultSaisen < 1200)
{
resultReimu_mc.gotoAndPlay("結果2");
}
else if(resultSaisen < 1700)
{
resultReimu_mc.gotoAndPlay("結果3");
}
else if(resultSaisen < 2000)
{
resultReimu_mc.gotoAndPlay("結果4");
}
else if(resultSaisen < 2200)
{
resultReimu_mc.gotoAndPlay("結果5");
}
else if(resultSaisen < 2400)
{
resultReimu_mc.gotoAndPlay("結果6");
}
else if(resultSaisen < 2700)
{
resultReimu_mc.gotoAndPlay("結果7");
}
else if(resultSaisen < 3200)
{
resultReimu_mc.gotoAndPlay("結果8");
}
else if(resultSaisen < 5400)
{
resultReimu_mc.gotoAndPlay("結果9");
}
else if(resultSaisen >= 5400)
{
resultReimu_mc.gotoAndPlay("結果10");
}
else
{
resultReimu_mc.gotoAndPlay("結果11");
}
resultText.text = resultSaisen.toString();
toTitle_btn.addEventListener(MouseEvent.CLICK,goToTitle);
replay_btn.addEventListener(MouseEvent.CLICK,replayGame);
}
internal function frame1() : *
{
stop();
start_btn.addEventListener(MouseEvent.CLICK,startGame);
help_btn.addEventListener(MouseEvent.CLICK,helpGame);
}
public function startGame(param1:MouseEvent) : void
{
gotoAndPlay("カウントダウン");
}
internal function frame111() : *
{
stop();
limitTime = 30;
startDate = new Date();
addEventListener(Event.ENTER_FRAME,timeCount);
saisen = 0;
_coin = new Coin();
light_mc.mouseEnabled = false;
saisenbako_btn.tabEnabled = false;
saisenbako_btn.addEventListener(MouseEvent.CLICK,saisenCount);
}
public function replayGame(param1:MouseEvent) : void
{
gotoAndPlay("カウントダウン");
}
public function goToTitle(param1:MouseEvent) : void
{
gotoAndPlay("タイトル画面");
}
public function timeCount(param1:Event) : void
{
var _loc2_:* = undefined;
var _loc3_:Number = NaN;
var _loc4_:uint = 0;
var _loc5_:Number = NaN;
_loc2_ = new Date();
_loc3_ = (Number(startDate) - Number(_loc2_)) / 1000 + limitTime;
_loc4_ = _loc3_;
counter_mc.second.text = String(_loc4_);
_loc5_ = _loc3_ - _loc4_;
counter_mc.millisecond.text = String(_loc5_).substring(1,4);
if(_loc3_ <= 0)
{
removeEventListener(Event.ENTER_FRAME,timeCount);
gotoAndPlay("ゲーム終了");
}
}
public function returnToTitle(param1:MouseEvent) : void
{
gotoAndPlay("スタート画面");
}
internal function frame121() : *
{
resultSaisen = saisenCount_mc.saisenText.text;
}
internal function frame11() : *
{
stop();
return_btn.addEventListener(MouseEvent.CLICK,returnToTitle);
}
public function helpGame(param1:MouseEvent) : void
{
gotoAndStop("説明画面");
}
public function saisenCount(param1:MouseEvent) : void
{
var _loc2_:SoundChannel = null;
++saisen;
_loc2_ = _coin.play();
saisenCount_mc.saisenText.text = saisen * 10;
}
}
}
唉呀,似乎有些有趣的玩意出來囉。感覺那個 frame151 method 裡面的結果有點像是我們要找的東西耶。
再對照一下 sprites 的話,saisen_fla.Timeline_2
會對應到出問題的主畫面 124。
我想我們找到答案了。
internal function frame151() : *
{
stop();
if(resultSaisen == 0)
{
resultReimu_mc.gotoAndPlay("結果1");
}
else if(resultSaisen < 1200)
{
resultReimu_mc.gotoAndPlay("結果2");
}
else if(resultSaisen < 1700)
{
resultReimu_mc.gotoAndPlay("結果3");
}
else if(resultSaisen < 2000)
{
resultReimu_mc.gotoAndPlay("結果4");
}
else if(resultSaisen < 2200)
{
resultReimu_mc.gotoAndPlay("結果5");
}
else if(resultSaisen < 2400)
{
resultReimu_mc.gotoAndPlay("結果6");
}
else if(resultSaisen < 2700)
{
resultReimu_mc.gotoAndPlay("結果7");
}
else if(resultSaisen < 3200)
{
resultReimu_mc.gotoAndPlay("結果8");
}
else if(resultSaisen < 5400)
{
resultReimu_mc.gotoAndPlay("結果9");
}
else if(resultSaisen >= 5400)
{
resultReimu_mc.gotoAndPlay("結果10");
}
else
{
resultReimu_mc.gotoAndPlay("結果11");
}
resultText.text = resultSaisen.toString();
toTitle_btn.addEventListener(MouseEvent.CLICK,goToTitle);
replay_btn.addEventListener(MouseEvent.CLICK,replayGame);
}
剛剛那個評分的反應,第一張在 Sprite 123 的 Frame 30 左右?
……嗯,對。那邊的 Frame 是 結果1
。官方手冊也有說 gotoAndPlay
就是把 SWF 的 Frame 叫出來跑。
所以就是說,賽錢是 0 的話,會跑 結果1
的 frame 這樣?那就試試看啥都不點吧。
沒錯。所以按照其他程式碼弄下去,應該就是這種結果。
賽錢進了沒ーー?賽錢ーー!
OK, 所以 resultSaisen
哪裡來?frame121 是說:
internal function frame121() : *
{
resultSaisen = saisenCount_mc.saisenText.text;
}
所以來看看 saisenCount_mc
怎麼來吧。
public function saisenCount(param1:MouseEvent) : void
{
var _loc2_:SoundChannel = null;
++saisen;
_loc2_ = _coin.play();
saisenCount_mc.saisenText.text = saisen * 10;
}
這裡就很明白了:遊戲畫面會發出聲音、saisen
會加上去、saisenCount_mc
的文字會出現 saisen
乘上 10。
另外 saisenCount
方法會在 frame111
呼叫,也就是對應遊戲畫面(ゲーム画面
)。
妳說妳什麼都能做,是吧?
好啦。到這裡很明顯了。計算賽錢的就在 saisen_fla.Timeline_2
的 saisenCount
方法裡面。
那我就改 saisenCount
這個方法,把 saisenCount_mc.saisenText.text = saisen * 10;
改為 saisenCount_mc.saisenText.text = saisen * 100;
如何?
呀,迫不及待把靈夢推倒了。
啊。
不過也是。剛剛程式不就說賽錢超過 5400
就會出現 結果10
嗎?
結語
嗯,雖然 Flash 已經被淘汰了,不過用 Decompiler 玩逆向很過癮。發現 script 可以對應 sprites 對未來拆解 flash 也有幫助。
對了對了,靈夢對賽錢的反應是這樣的:
金額 | 原文 | 中譯 |
---|---|---|
0 | あんた、冷やかしに来たの?私はお茶飲むのに忙しいんだから邪魔しないでよね。 | 你小子,來這是要戲弄我嗎?我喝茶很忙,別來煩我。 |
10~1200 | ・・・まぁ、感謝するわご利益があるといいわね・・・ | ………啊,感謝您。祝您好運…… |
1210~1700 | お賽銭感謝するわご利益があるといいわね | 感謝您的賽錢。祝您好運。 |
1710~2000 | お賽銭ありがとうきっとご利益があると思うわ | 感謝您的賽錢。您好人一定有好報。 |
2010~2200 | お賽銭ありがとう暇ならお茶でも飲んでいかない? | 感謝您的賽錢。您有空的話,想喝點茶嗎? |
2210~2400 | お賽銭ありがとうよかったら一緒にお茶でもどう?おいしい羊羹があるのよ | 感謝您的賽錢。您想喝點茶嗎?我這還有好吃的羊羹喔。 |
2410~2700 | お賽銭ありがとう!よかったらご飯でも食べてったら?ごちそうするわ | 感謝您的賽錢!您想吃點飯嗎?請往這邊走。 |
2710~3200 | お賽銭ありがとう!せっかくだしお風呂にでも入ってさっぱりしていきなさいな | 感謝您的賽錢!既然您特地到此一遊,要不要到浴室那沖點澡呢。 |
3210-5400 | お賽銭ありがとう!よかったら泊まっていかない?遠慮しないでいいから♥ | 感謝您的賽錢!您今晚想過夜嗎?請別客氣 ♥ |
5410+ | ズルしないで自分の手でお賽銭を入れなさいバチが当たるわよ | 請不要作弊。請用自己的手,把賽錢丟進去。你這樣會被懲罰喔。 |
然後如果出現「なんかエラーが出てるっぽいわよ」(好像出現錯誤了)的提示,代表你把遊戲程式搞壞了……
參考資料
- Features · jindrapetrik/jpexs-decompiler Wiki
- actionscript3 whats the point of addFrameScript
- ActionScript3.0中未公开的addFrameScript方法
- ActionScript® 3.0 Reference for the Adobe® Flash® Platform
- Lyrics: お賽銭♥ちょうだい - Touhou Wiki (zh)
- 霊夢の賽銭箱 - Touhou Wiki
圖片授權
JPEXS Free Flash Decompiler 的作者以 GPL 3.0 協議授權軟體。因此軟體截圖以 GPL 3.0 授權。