Skip to content

zenn bak

LostMyCode edited this page Feb 14, 2024 · 1 revision

ふと思い立った。

オンラインMMORPGの老舗「REDSTONE」をパソコンにインストールせずに誰でもブラウザでプレイできたらどんなに幸せだろう?

よし、ブラウザで動くレッドストーンを作ろう!

こうしてゴールのみえない過去最大級のプロジェクト「レッドストーンブラウザ版開発計画」は始まった。

先に成果物だけ見たいという忙しい人向け

「記事を読むのめんどくさいから、できたものだけ見せて!」という方はこちらの動画を御覧ください。

YouTube: 赤石ブラウザ版 2023年末アプデ

https://youtu.be/EDfWIh6I244

このプロジェクトのソースコードはGitHubで全部公開しています。

https://github.com/LostMyCode/redstone-js

サイトにアクセスするだけでブラウザ移植版をデモプレイすることができます。

https://rs.sigr.io/


以上、忙しい人向けセクションでした。

詳細や開発の技術的な話に興味がないという方はここでブラウザバックしていただいて大丈夫です!

前提: そもそもレッドストーンとは

「メイプルストーリー」は昔からPCゲームに触れている方なら一度は耳にしたことがあると思いますが、レッドストーンも同じ時代に始まって賑わったオンラインゲームです。

正確には覚えていませんが、もう20周年ぐらいになる老舗オンラインゲームです。

察しの通りプレイヤーは年々減っており、かつての賑わいはなくパーティー狩りなどはほとんどなくなっています(ソロプレイでほとんど成り立ってしまう)

グラフィックとしては画像のような2Dゲームです。

2Dフィールド上をプレイヤーをマウス操作で移動させて、スキルを使ってモンスターを攻撃して経験値を獲得しレベリングしていくという王道のMMORPG。

実現したいこと: ブラウザでサイトを開くだけでREDSTONEをプレイしたい

単純に、何がやりたいかというとブラウザを開くだけで自分が好きなゲームが動いているところを見てみたいただそれだけです。

ブラウザで動作するということは基本的には環境問わず、MacOSでもWindowsでも、なんならパソコンじゃなくてスマホでも動作するということです。

Windowsが乗ったPCでインストールしないとプレイできなかったゲームが、ただサイトを開くだけで、あらゆる環境で動いてしまったら感動じゃないですか?

たぶん「しょーもな!」って思う方も少なくないと思いますが、個人的には「やば!イケイケじゃん!」って思ってしまうんですよね

ブラウザ移植のトリガーとなった出来事

ブラウザで動かしたいとは前々から思っていたのですが、とある海外フォーラムでREDSTONEのサーバーファイル(10年以上前のもの)が密かにリークしていることを知ったのがトリガーとなりました。

もともとredgem(だったかな?)というREDSTONEのエミュ鯖が昔存在していたので、公式の人間以外がサーバーファイルを握っているんだろうなとは思っていました。が、今頃になってサーバーファイルが表に出てくるとは驚きでした。

:::message ※エミュ鯖: エミュレーターサーバー、 何らかの手段で入手したオンラインゲームのプログラムを公式のサービスとは別に第三者が立ち上げたもの。経験値100倍など本来ではありえない設定でゲームがプレイできてしまうのでエミュ鯖を好むプレイヤーも一定数いる。なお、普通にグレー(というかアウト) :::

バイナリファイルの構造を知る機会が訪れる

サーバーファイルがあるからといって別にどうというわけではないのですが、調べてみると誰かがリバースエンジニアリングしてゲームサーバーを作り直そうとしていた残骸ソースコードも発見しました。

企業が開発、運営するような規模のゲームをリバースエンジニアリングだけですべて作り直すというのは途方もない作業であるため結局そのソースコードも未完成だったのですがゲームのマップデータが格納されたバイナリファイルを読み込む処理の記述があることに気づきました。

具体的には、地面のテクスチャ配置、オブジェクトの配置とそのテクスチャ、NPCの配置や役割などの情報が格納されたファイルがあり、そのファイルの読み込み処理を行っている部分がソースコードに書かれていたわけですね。

既存のオンラインゲームの移植難易度が高い理由の1つは、元々のゲームのバイナリファイルがそれぞれどのようなデータを持っており、さらにどのようなデータ構造で格納されているのかを知ることがかなり難しいし時間がかかるためです。

攻略サイトで、普通にプレイしていたら分からない詳細な情報を事細かに記載してくれているものがありますが、あれはゲームのバイナリファイルを地道に解析して抜き出した情報を見やすく可視化してくれたものだったりします。(って考えるとすごいなぁ)

ファイルの構造が分かればハードルはかなり下がる

オンラインゲームのバイナリファイルの構造は、よく目にするファイルとは違ってオリジナルなものも多いのでそれを何も知らない状態から理解するというのはかなり大変なことですが、その取っ掛かり部分が拾ったソースコードに書かれていたわけなのでハードルはかなり下がりました。

それでもまだ全然難易度高いですけどね

JavaScriptでもバイナリを読み取ることはできるので、例に習って同じようにデータを読み取っていけばマップデータのテクスチャ情報や配置情報は得られるわけです。

それをもとにCanvasにイメージを描画すれば、ブラウザでゲームのマップを表示できるということです。

もともとブラウザで動くオンラインゲーム(おもにioゲームと呼ばれる系列のもの)の開発・運営はよくやっていたので、元となる情報さえあれば!と思っていたところにコレだったのでやるしかありませんでした笑

REDSTONEバイナリ解析の先駆者が存在した

実はテクスチャファイル(画像データが入ったファイル)の読み取り処理は拾ったソースコードには記載されていなかったので構造がわからずだったのですが、幸いなことに約10年前に1から解析をしてテクスチャをブラウザで表示できるViewerを作って公開していた方がいらっしゃいました。

作成者はこぅさんという方です。

ブラウザで開いて、REDSTONEのテクスチャファイルをドラッグ&ドロップするだけで表示できてしまうという優れものです。 10年前にこんなものを作り上げている方がいたなんて!すごすぎる!

もう何も手を加えずともテクスチャをブラウザで読み込める仕組みがそこにあったので、素直にこの部分については拝借することにしました。(一応Twitterでもご本人様にDMさせていただきました)


これらの要因が重なって、ついにブラウザ版REDSTONEの開発が始動しました。

本プロジェクトの技術選定

言語: JavaScript

ブラウザで動かす時点でJavaScript一択でした。rustやc++で書いてwasmにして~という手もあるかもしれませんが、あえてそうする必要性を感じなかったためナシ。

TypeScriptで書きたい気持ちもありつつ、型定義などで時間取られるよりはとりあえず殴り書きして動くとこまでを目指したいと思い今回は見送り。(というか毎回とりあえず動かそうが先行するので、いつも通りの見送りw)

ベース構成: webpack5 + babel + webpack-dev-server

js書いたらバンドルしてホットリロードしてくれるだけの最小構成。

描画:pixi.jsライブラリを利用 標準のCanvasAPI使うのでも良かったのですが、pixi.jsというライブラリを採用することに。

PixiJSはウェブブラウザのcanvas要素に描画する、クロスブラウザ対応の軽量なJavaScriptライブラリ。JavaScript から GPUを扱うWebGL技術を2Dに特化して平易に利用できる。

webglは3D描画向けという認識が強いですが、pixi.jsは2D描画をwebgl使ってできるのでモノによっては標準のCanvasAPIよりも高パフォーマンスでヌルヌル動くものを作れます。

とはいえそこはあまり重要視してなくて、単純に描画処理書くときにpixi.js使ったほうが何やってるか理解しやすいし簡単だったという過去の経験をもとに効率重視という意味での採用です。

終わりなきリバースエンジニアリング

誰かが書き直したソースコードも未完成だったため、すべての答えがそこにあるわけではなく自分で解析して新たに答えを見つける必要がありました。

「どうやらマップデータにはタイル情報らしきものが、ファイルの先頭から◯◯バイト目に格納されているっぽい」 という確証のない手がかり。

画像↓

このように、タイル情報らしきバイト列がありました。

そしてもう一つ、地面のタイルテクスチャがたくさん格納されているファイルがある という手がかり。 画像↓

画像のように、ずらーっとたくさんの小さなタイルが格納されているファイルがあったのです。

ここで1つの仮説を立てます。

「マップデータのタイル情報らしき部分に記載されている"数値"と、"タイル画像の番号"が対応しているのかも?」

この仮説を検証すべく、数値と対応する番号のタイル画像を順番に並べてみました。

すると・・・

おぉ!ゲームマップ(街)の地面が完成しましたね!

これで仮説はおそらく正しいのだとわかります。


ここでは成功した例しか書いていませんが、立てた 仮説 が正しいことより間違っていることのほうが断然多いです。

なので、この作業をしているだけで1日が終わったりすることも。。。

さらに、仮説を検証するのも一苦労だったりします

:::message

一連の流れ

バイナリエディタでマップデータを一部書き換える          ↓ ゲームを開いて該当マップに移動する          ↓ 書き換える前となにが変わったかを見て、書き換えた箇所が何を意味するものなのか仮説を立てる(たとえば、オブジェクトの位置が変わったなら座標情報だと考えられる)          ↓ ブラウザで、同じ箇所を読み取って、オブジェクトを描画する際のx, y座標として読み取った値を反映させてみる          ↓ 描画されたものが、期待通りの位置にあればOK、だめならやり直し

:::

他にもゲームの実行ファイル(exe)がどのようにマップデータを読み取っているのか探るべくリバースエンジニアリングツールを用いたりもしますが、詳細は割愛。

正直、ここらへんはほとんど経験がなくて限界だったのでコミュニティに助けを求めてヒントやリバースエンジニアリングの手法を教えてもらいました。。

でも本当にここは大変な作業です。

終りが見えないし、時には正解を得られないこともあるのでそれっぽくできたら妥協した部分も少なくありませんでした。

マップデータをもとにCanvasに描画

そして、なんとかマップデータをもとにブラウザ上にタイル、オブジェクト、NPCなどを配置することができました。

マップのグラフィックに関してはほぼほぼ移植できたことになります。

これにプラスしてプレイヤーを配置し、マップを走り回れるようになった時点で出したのが以下の記事です。

https://zenn.dev/aespa/articles/d18ba87870e558

正直マップを走れるだけなのでゲーム性もなにもありませんw それでもここまで到達できたのは嬉しかったので一つの区切りとしていました。

pixi.jsライブラリを使って描画をしているのですが、役に立ったのは AnimatedSprite でした。

レッドストーンのマップ上のオブジェクトには噴水などアニメーションを繰り返すオブジェクトがあります。

これをブラウザ描画で再現するときに AnimatedSprite がとても役立ちました。

// https://pixijs.download/dev/docs/PIXI.AnimatedSprite.html

import { AnimatedSprite, Texture } from 'pixi.js';

const alienImages = [
    'image_sequence_01.png',
    'image_sequence_02.png',
    'image_sequence_03.png',
    'image_sequence_04.png',
];
const textureArray = [];

for (let i = 0; i < 4; i++)
{
    const texture = Texture.from(alienImages[i]);
    textureArray.push(texture);
}

const animatedSprite = new AnimatedSprite(textureArray);

このように、画像ごとに作った複数のPIXI.Textureを配列で渡してやることで勝手にアニメーションしてくれるスプライトが作成できます。

速度を指定するだけで、あとは自動でフレームが更新されていくのでとても楽です。

ついにスキルを使ってモンスターを狩れるようになった年末アプデ

今回のアップデートで、ついにブラウザ移植版でもマップ上にいるモンスターと戦闘できるようになりました。

https://youtu.be/EDfWIh6I244

やっとゲーム性が出てきました(まだまだですが)

レッドストーン本家と違って、ブラウザ版は調整し放題なので貼った動画のようにチート級のことができてしまいます。

ここまでど派手な改変はエミュ鯖でも見れない景色でしょう笑

今回のアプデも、これまでの変更もすべてGitHub上でソースコードを公開しています。 https://github.com/LostMyCode/redstone-js

バイナリファイルの構造体の読み取りでハマったこと

今回はプレイヤーがスキルを使用してモンスターを攻撃する実装のために、スキルデータが格納されたバイナリファイルを読み込む処理を追加しました。

ザックリ言うと、このファイルにはスキルの数と、その数分のスキル構造体が格納されています。

構造体は以下のようなイメージです

struct __cppobj CSkillDefine
{
  unsigned __int16 m_wSerial;                                              // offset: 0000, size: 0002
  unsigned __int16 m_wIconIndex;                                           // offset: 0002, size: 0002
  unsigned __int16 m_wType;                                                // offset: 0004, size: 0002
  unsigned __int16 m_wAction;                                              // offset: 0006, size: 0002
  unsigned __int16 m_wAction2;                                             // offset: 0008, size: 0002
  unsigned __int16 m_wOverlapAction;                                       // offset: 000A, size: 0002
  unsigned __int16 m_wOverlapAction2;                                      // offset: 000C, size: 0002
  unsigned __int16 m_wReiterationDamageCountSyncWithOverlapAction;         // offset: 000E, size: 0002
  unsigned __int16 m_wEnableJob;                                           // offset: 0010, size: 0002
  unsigned __int16 m_wSpeed;                                               // offset: 0012, size: 0002
  ...

ちなみにこの構造体はリバースエンジニアリングツールを用いて抜き出したものです。

一番最初のメンバ m_wSerial は型が unsigned int16 なので、2bytes であることがわかります。

なので、バイナリファイルの構造体の開始位置から 2bytes 読み取ればそれが m_wSerial の値となります。で、次の m_wIconIndexunsigned int16 で 2bytes なので開始位置から 2bytes 進んだ位置から 2bytes 読み取れば m_wIconIndex の値が得られます。

型通りに順番に読み取っていけばいいんだー!と思っていたのですが、途中からどうも読み取った値が正しくないことに気づきました。

あれ?上から順番に正確に読み取っていたのに・・・?

ここでかなり時間を浪費してしまったのですが、REDSTONEがもともとC++で開発されたものでC++では構造体を扱うときにアラインメントというものを意識しなければならないようなのです。

例えばこの例では、構造体のメンバは char, int, char, short なので構造体のサイズは char(1byte) + int(4bytes) + char(1byte) + short(2bytes) で 8bytes だろうと思いますが実は 12bytes なんですよね。

データが隙間なくギチギチに詰まっているものだと思っていたのですが、メモリ空間上では最適化のために適切に隙間(パディング)が作られます。

ファイルに格納された構造体データも、メモリにコピーするだけで各メンバに値を割り当てられるように同じように隙間を保持したままにされていると考えられます。

普段JavaScriptばかり触っているとこれらのことを意識する機会はほぼないので、ここで初めて知ることとなりました。

そんなわけで、「char型を読み取ったから 1byte 進めて 次の値を読み取ればいいんだ!」という考えで順番に読み取ってしまうと詰むわけです。

以下のコードはJavaScriptでの構造体読み取り処理の一部です。 このように、パディングが入る箇所があるのでその部分はスキップするという風に書いていかねばなりません。

        this.dodgeAngle = br.readUInt16LE();
        this.hitAngleRange = br.readUInt16LE();
        this.hitAngleRangePerLevel = br.readUInt16LE();
        this.dodgeDistance = br.readUInt16LE();
        this.paletteIndex = br.readUInt16LE();

        br.offset += 2; // padding

        this.enchantedEffectMask = br.readUInt32LE();
        this.enchantedImage = br.readUInt16LE();
        this.dustImageRange = br.readUInt16LE();

JavaScriptでは普通はこんな難しいことを意識しなくていい分、逆に大変でした。 (C++だったら構造体作ってそこにファイルに格納されているデータを流し込めばいいだけなのに、JSだと1つずつ読み取ってパディングが入っている箇所も考えないといけないため)

難しそうだったので全部JSで書いたけど、C++で書いてEmscriptenでWebAssemblyにコンパイルするほうが簡単だったりするのかな?未だに最適解がわかりません。。

致命: そもそも過疎ゲーなので見向きもされない

ここまでいろんな困難がありながらも、REDSTONEのブラウザ移植プロジェクトを進めてきたわけです。 しかし、残念なことに本家が20年くらい経って過疎化が進んでいるゲームゆえブラウザでプレイできるようになっても見向きもされないわけです笑

悲しいですね!

それでも、自分が身につけたものでここまで出来るんだとわかったし挑戦するのは楽しいです。

次はもう少しHOTな分野で、なにか面白いことやってみたいですね。

おわりに

マップの描画だけでなく、スキルと狩りまで実装できたんだ。

つまり不可能なことはない!!

以上です。

無料オンラインゲーム「レッドストーン」をプレイしたことある人は、この機会にぜひブラウザ版もお試しあれ

ブラウザ版: https://rs.sigr.io/

ソースコード全部公開してます

GitHub: https://github.com/LostMyCode/redstone-js

:::message

今後の展開(本当に実装するかは未定)

  • 他のキャラクター(職業)実装
  • 使えるスキル増やす
  • オンライン化
  • 装備
  • プレイヤーステータス, HP, CP

:::


画像引用元: Microcontroller Embedded C Programming Lecture 149| Calculating structure size manually with and without padding

Structures in C: From Basics to Memory Alignment