intro_of_simutrans_development

トップページに戻る

改造2: 市道化の可否を道路1タイルずつ設定する

2つめのアプローチは,市道化の可否を道路1タイルずつ指定する方式である.道路建設ツールをctrlキーを押しながらクリックすると,下図のように市道化防止ボタンが登場する.市道化防止ボタンが押された状態でその道路を建設すると,その道路についてのみ市道化が禁止される.

先ほどの改造とは全く別の改造になるので,gitで新しいブランチを用意しよう.コマンドラインで下のようにすれば,「cityroad_button」という名前のブランチを作成し,同時にそのブランチに切り替えることができる.cityroad_buttonブランチはspeed_thresholdブランチからではなく,masterブランチから派生させることに注意されたい.

1
2
git checkout master
git checkout -b cityroad_button

まず,市道化の可否はどうすればいいのだろう.道路オブジェクトに市道化可否フラグを持たせて,stadt_t::update_city_street()の中で対象道路に市道化防止フラグが立っているか検査してから市道化処理を行えば良さそうである.道路に変数をもたせたので,それをセーブデータに保存することも必要だ.変数をセーブデータから出し入れするにはrwdr()関数を使えばよいのであった.あとは,GUIから市道化可否フラグを設定できればよい.フラグは道路建設ツールを使って設定すると良いだろう.したがって,道路建設ツールがユーザーに対して設定ウィンドウを提供する.ユーザーが実際に道路を建設するときに,ツールが一緒に道路オブジェクトにフラグを設定すればよい.

ここまでで基本的な方針はついた.やることはもう分かっているので,あとは編集したいクラスが書かれているファイルを見つけ,編集すべき関数を見つけ,内容を理解し,コードを書き換えればよい.道路を建設するツールは何というクラスなのだろうか.クラスや関数の探し方は既に解説したとおりである.動作主体を考え,その単語で横断検索をかけ,あるいはディレクトリ構造からたどっていく.さあ,コードの海へ漕ぎ出そう.


独力での実装の旅を続けていると,このトピックではいくつかの難所に遭遇するはずである.これ以降はそれを一つ一つ解説していく.

street flagとrdwrの整備

本改造では,市道化をする時にその道路が市道化可能か判断をする.このため,道路オブジェクトに市道化可否を表すフラグ変数が必要である.まずは,道路オブジェクトに市道化可否のフラグを設け,rdwrするところまで進もう.道路オブジェクトを定義するクラスはboden/wege/strasse.hで定義されているstrasse_t(strasseはドイツ語で道路の意味である.)である.このクラスに変数street_flagsを定義し,そのgetter・setter関数を定義する.変数と関数を下のコードのように追加する.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class strasse_t : public weg_t
{
public:
  // enumを定義
  enum { AVOID_CITYROAD = 0x01 };
  
private:
  // bool変数として定義するのではなく8bit整数として定義する
  uint8 street_flags;
  
public:
uint8 get_street_flag() const { return street_flags; }
void set_street_flag(uint8 s) { street_flags = s; }
bool get_avoid_cityroad() const { return street_flags&AVOID_CITYROAD; }
void set_avoid_cityroad(bool s) { s ? street_flags |= AVOID_CITYROAD : street_flags &= ~AVOID_CITYROAD; }

};

bool変数で市道化の可否を定義してもよかったのだが,あえてuint8変数として定義した.このようにすれば,後でフラグを追加するときにenumとgetter・setterの追加で済む.このテクニックはsimutransでは多数箇所で用いられている.例えば,roadsign_desc_t(descriptor/roadsign_desc.h)を見ると,同じように各フラグがuint8変数1つにまとめられていることがわかる.

つづいて,boden/wege/strasse.ccを改変しよう.strasse.ccでは先ほどstrasse_tに定義したstreet_flagsを初期化し,セーブデータに読み書きできるようにする.セーブデータの読み書きはrdwr()関数に書けば良いのであった.strasse_tのコンストラクタは引数ナシのものとセーブファイルオブジェクトをとるものがあることに注意しよう.セーブファイルオブジェクトを引数に取るコンストラクタでrdwr()が呼ばれていることがわかる.下のコードの5行目および14〜19行目が,boden/wege/strasse.ccにおける追記内容である.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
strasse_t::strasse_t() : weg_t()
{
  set_gehweg(false);
  set_desc(default_strasse);
  street_flags = 0;
}

void strasse_t::rdwr(loadsave_t *file)
{
	xml_tag_t s( file, "strasse_t" );

	weg_t::rdwr(file);
  
  // street_flagsの読み書き
  if(  file->is_version_atleast(121, 1)  ) {
    file->rdwr_byte(street_flags);
  } else {
    street_flags = 0;
  }
}

street_flagsを設定したので,市道化処理を行うupdate_city_street()street_flagsを参照するようにする.simcity.ccにあるupdate_city_street()を下のコードのように変更する.8行目にあるif文の条件式の中に市道化可否フラグの項が追加されたことがわかるだろう.street_flagsstrasse_tで定義しているので,下のコードの6行目では道路オブジェクトをweg_tではなくstrasse_tで扱っていることに注意しよう.

1
2
3
4
5
6
7
8
9
10
11
bool update_city_street(koord pos)
{
	const way_desc_t* cr = world()->get_city_road();
	for(  int i=0;  i<8;  i++  ) {
		if(  grund_t *gr = world()->lookup_kartenboden(pos+neighbors[i])  ) {
			if(  strasse_t* const weg = (strasse_t*)(gr->get_weg(road_wt))  ) {
				// Check if any changes are needed.
				if(  (!weg->hat_gehweg()  ||  weg->get_desc() != cr)  &&  !weg->get_avoid_cityroad()  ) {
          // 市道化する
					player_t *sp = weg->get_owner();
(以下省略)

市道化可否を道路建設に反映

道路オブジェクトに市道化可否フラグを設け,update_city_street()でそれを参照するようにした.あとは,ユーザーが市道化の可否をGUIで設定し,それが道路建設時に適切に反映されるようにすればよい.まずは,ユーザーが設定した市道化可否を道路建設に反映するようにしよう.ここでもやることは同じ.クラスを特定し,関数を特定し,内容を理解し,改変を加える.まずは自分でコードを読み,「道路建設がどのようにして行われているのか」を把握することにチャンレジしよう.


何度か述べたとおり,Simutransで道路を建設しているのは「道路建設ツール」である.ソースコードフォルダを眺めていると,simtool.hというファイルが見当たる.このヘッダファイルを開いて,眺めてみよう.tool_remover_ttool_raise_tといったクラスの宣言が見られる.simtool.hはSimutransにおける種々のツールクラスを定義したファイルである.

この中に道路建設ツールをあらわすクラスがあるはずである.「道路」「建設」であるから,way,weg,build,construct,street,strasseなど思いつく限りの関連ワードでsimtool.hの中身を検索してみよう.すると,tool_build_way_tというクラスが見つかった.なお,その後ろのほうにtool_build_cityroadtool_build_bridge_ttool_build_tunnel_tといったクラスも見つけることができる.それぞれ市道,橋,トンネルの建設クラスであろう.

tool_build_way_tに話を戻そう.クラスの機能・性質を知るために,まずはヘッダファイルを読むのであった.25行ほどであるから一つ一つの関数についてその名前から機能を想像してみよう.例えば,calc_route()は道路建設ツールでルート検索をしているのだから,与えられた起点終点座標について建設ルートを計算する関数であろう.init()はその名の通りツールオブジェクトの初期化関数であるようだ.do_work()は「仕事をする」なので,実際に道路建設をする関数であると想像できる.名前やコメントだけではイマイチわからず,その内容を知りたいときは実装(.cc)ファイルを読んで中身の処理を理解しよう.

ともかく,我々は今「市道化可否を道路建設に反映」したいので,実際に道路建設をする関数do_work()の中身が気になる.そこで,simtool.ccにかかれているtool_build_way_t::do_work()を読むことにしよう.

1
2
3
4
5
6
7
8
9
10
11
12
const char *tool_build_way_t::do_work( player_t *player, const koord3d &start, const koord3d &end )
{
	way_builder_t bauigel(player);
	calc_route( bauigel, start, end );
	if(  bauigel.get_route().get_count()>1  ) {
		welt->mute_sound(true);
		bauigel.build();
		welt->mute_sound(false);
		return NULL;
	}
	return "";
}

上のコードの3行目でway_builder_t型の変数を生成している.その後建設ルートを計算し,ルートが有効であれば3行目で生成したway_builder_t型変数のbuild()を呼び出している.ここから,道路建設の実体はway_builder_tクラスにあることがわかった.そこで,way_builder_tクラスを調査しよう.way_builder_tのヘッダファイルはbauer/wegbauer.hである.(bauerはドイツ語でbuilderという意味であるからファイルの場所を推定するのは容易であろう.)

bauer/wegbauer.hを見ると,way_builder_tは多数の変数と関数を持った大きなクラスであることがわかる.このままヘッダファイルを眺めていてもナニをすればよくわからないが,とりあえず上のコードの7行目で呼ばれた関数build()は発見することができる.我々はこの処理の中身が知りたいのであるから,bauer/wegbauer.ccに書かれたway_builder_t::build()を読むことにしよう.この関数を読むとその中ほどに下のコードような記述がある.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
switch(bautyp&bautyp_mask) {
  case wasser:
  case schiene:
  case schiene_tram: // Dario: Tramway
  case monorail:
  case maglev:
  case narrowgauge:
  case luft:
    DBG_MESSAGE("way_builder_t::build", "schiene");
    build_track();
    break;
  case strasse:
    build_road();
    DBG_MESSAGE("way_builder_t::build", "strasse");
    break;
  case leitung:
    build_powerline();
    break;
  case river:
    build_river();
    break;
  default:
    break;
}

way_builder_tオブジェクトのbautyp変数に予めwaytypeが設定されていて,それに応じて呼ばれる関数が変化する.今回は道路建設ツールの改造なのでbuild_road()が呼ばれる.どうやらこの関数が道路建設の本体のようである.(ちなみに,bautyptool_build_way_t::do_work()の4行目calc_routeで設定されている.気になる人はcalc_route()も読んでほしい.)

way_builder_t::build_road()を読んでみよう.同じくbauer/weg_bauer.ccに記述されている.80行程度のコードでありここに貼ることはしないので,お手元のコードを眺めながら読み進めてほしい.冒頭で市道・undoまわりの処理をした後,予め計算した建設ルート1マスずつに対してfor文で処理を回していく.タイルが高架タイルorトンネルタイルなら処理をスキップする.既存道路をアップグレードするのか新規建設するのかで場合分けをし,それぞれについて道路オブジェクトのdescriptorを設定したりownerを設定したりしている.最後に再描画処理を呼ぶ.

simtool.hの探索から始まって,ようやく道路建設処理の全体が見えた.ここまでの流れを簡単に整理しておこう.tool_build_way_tが道路建設ツールであり,そのdo_work()関数が道路建設を行う.do_work()way_builder_tに建設処理を丸投げしており,build()を経由してbuild_road()が呼ばれる.build_road()は予め設定された建設ルートにそって1マスずつ道路オブジェクトを配置し,道路オブジェクトに対して設定をしていく.

処理の全体がわかった今,ユーザーがウィンドウ経由で設定したstreet_flagsstrasse_tで定義したのをおぼえているだろうか)を道路オブジェクトに反映させたい.tool_build_way_tstreet_flags変数を設けた上で(あとでこれをGUIで編集できるようにする),改めて自分で手を動かしてコード改変にチャレンジしてみよう.


まずは,way_builderから手を付けよう.build()build_road()はそれ自体は引数を取らず,予めオブジェクトに設定しておいた値を読んで処理する方式である.そこで,まずはway_builder_tクラスに変数street_flagを設定する.bauer/wegbauer.hに下のコードのように追記することで,street_flagを定義し,setter関数を設ける.getter関数を定義していないのは,tool_build_way_tway_builder_tに建設作業を投げて実際に建設を行う際にway_builder_tstreet_flagを読み出す必要が無いからである.

1
2
3
4
5
6
7
8
class way_builder_t
{
private:
  uint8 street_flag;
  
public:
  void set_street_flag(uint8 a) { street_flag = a; }
};

次にbuild_road()でこのstreet_flagを道路オブジェクトに設定していく.道路オブジェクトに対する操作なので,位置関係的にはdescriptorを設定している行の前後あたりに書けばよいだろう.以下に一部省略したway_builder_t::build_road()のコードを示す.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
void way_builder_t::build_road() {
  // 市道やundoまわりの処理.掲載省略.
	for(  uint32 i=0;  i<get_count();  i++  ) {
    if((i&3)==0) {
			INT_CHECK( "wegbauer 1584" );
		}

		const koord k = route[i].get_2d();
		grund_t* gr = welt->lookup(route[i]);
		sint64 cost = 0;

		bool extend = gr->weg_erweitern(road_wt, route.get_short_ribi(i));

		// 橋やトンネルの場合はstreet_flagだけアップデートする
		if(gr->get_typ()==grund_t::brueckenboden  ||  gr->get_typ()==grund_t::tunnelboden) {
      strasse_t* str = (strasse_t*)gr->get_weg(road_wt);
			str->set_street_flag(street_flag);
      continue;
		}

		if(extend) {
      // 型はweg_tではなくstrasse_tにする
			strasse_t * weg = (strasse_t*)(gr->get_weg(road_wt));

      if(gr->get_typ()==grund_t::monorailboden && (bautyp&elevated_flag)==0) {
        // 高架の場合street_flagだけアップデートする
				weg->set_street_flag(street_flag);
			}
			// keep faster ways or if it is the same way ... (@author prissi)
			else if((weg->get_desc()==desc  &&  weg->get_street_flag()==street_flag  )  ||  keep_existing_ways  ||  (keep_existing_city_roads  &&  weg->hat_gehweg())  ||  (keep_existing_faster_ways  &&  weg->get_desc()->get_topspeed()>desc->get_topspeed())  ||  (player_builder!=NULL  &&  weg->is_deletable(player_builder)!=NULL)) {
				//nothing to be done
			}
			else {
				// we take ownership => we take care to maintain the roads completely ...
				player_t *s = weg->get_owner();
				player_t::add_maintenance(s, -weg->get_desc()->get_maintenance(), weg->get_desc()->get_finance_waytype());
				// cost is the more expensive one, so downgrading is between removing and new building
				cost -= max( weg->get_desc()->get_price(), desc->get_price() );
        // street_flagの設定
        weg->set_street_flag(street_flag);
				weg->set_desc(desc);
        // 以下省略
			}
		}
		else {
			// make new way
			strasse_t * str = new strasse_t();
			str->set_desc(desc);
      // street_flagの設定
      str->set_street_flag(street_flag);
			str->set_gehweg(add_sidewalk);
			// 以下省略
		}
		// 再描画まわりの処理.掲載省略.
	} // for
}

上のコードでは,16行目でトンネルや橋の場合について,26行目で高架の場合についてもstreet_flagをアップデートするようにした.この都合でif文の構造が変わっていることに注意されたい.道路置き換えの場合は39行目,新規建設の場合は49行目でそれぞれstreet_flagを設定している.編集したのはこの4点で,それ以外はもとのコードと同じである.

あとは,tool_build_way_tway_builder_tに道路建設をさせるときにstreet_flagway_builder_tオブジェクトに設定すればよい.

1
2
3
4
5
6
7
8
class tool_build_way_t : public two_click_tool_t {
protected:
  uint8 street_flag = 0;
  
pubic:
  void set_street_flag (uint8 a) { street_flag = a; }
  uint8 get_street_flag() const { return street_flag; }
};

tool_build_way_tstreet_flag変数を設けた(上のコード)上で,tool_build_way_t::do_work()を下のコードのように変更すればよいであろう.5行目でstreet_flagの設定を行っている.

1
2
3
4
5
6
7
8
9
10
11
12
13
const char *tool_build_way_t::do_work( player_t *player, const koord3d &start, const koord3d &end )
{
	way_builder_t bauigel(player);
	calc_route( bauigel, start, end );
  bauigel.set_street_flag(street_flag);
	if(  bauigel.get_route().get_count()>1  ) {
		welt->mute_sound(true);
		bauigel.build();
		welt->mute_sound(false);
		return NULL;
	}
	return "";
}

street_flagをGUIで設定する

道路オブジェクトに市道化防止フラグは設けた.道路建設ツールが道路オブジェクトにフラグを正しく設定できるようにもした.あとは,ユーザが市道化防止フラグをGUIで設定できるようにすればよい.我々が実現したいのはページ冒頭の図のようなウィンドウである.ctrlキーを押せばウィンドウがポップアップしてきて,ユーザーがその中のボタンを押せばそれがtool_build_way_tstreet_flag変数に反映されればよい.そうすれば,建設時にtool_build_way_t::do_work()street_flagway_builder_tに適切に渡してくれる.

それでは,ctrlキーを押しながら道路建設ツールを呼んだときウィンドウをポップアップして市道化防止オプションボタンを表示するにはどうすればいいのだろうか?どうすればいいのかよくわからないときは, 似たようなことをやっている事例を探してきて,そこのコードをコピーする のがスムーズな方法である.特に,GUIまわりのコードは内容を詳細に理解しようとするよりもとりあえず動いている既存のコードをコピペして使ってしまうほうが開発がスムーズに進行する.では,「ctrlキーを押しながらツールを呼ぶとウィンドウが出てきてオプションボタンを押せる」ことと似たようなことをやっているのは何だろう?

それは,上図のような信号・標識の間隔設定ウィンドウである.したがって,信号・標識がどのようにしてこのような機能を実現しているのかを理解すればよい.まずは,simtool.hから信号設置ツールのクラスを見つけてみよう.signal, signといった関連ワードで検索をかけてみるとtool_build_roadsign_tというクラスが見つかる.これが信号・標識を設置するツールである.ヘッダファイルでtool_build_roadsign_tを眺めてもあまりかわりばえしないので,実装ファイルを覗く必要がある.とりあえず,初期化関数っぽいtool_build_roadsign_t::init()をのぞいてみるとしよう.下のコードがそれである.

1
2
3
4
5
6
7
8
9
10
11
bool tool_build_roadsign_t::init( player_t *player)
{
	desc = roadsign_t::find_desc(default_param);
	// take default values from players settings
	current = signal[player->get_player_nr()];

	if (is_ctrl_pressed()  &&  can_use_gui()) {
		create_win(new signal_spacing_frame_t(player, this), w_info, (ptrdiff_t)this);
	}
	return two_click_tool_t::init(player)  &&  (desc!=NULL);
}

7〜9行目に注目してほしい.ctrlキーが押されていたらウィンドウを作れと書いてある.そのウィンドウはsignal_spacing_frame_tで定義されている.ctrlキーを押しながらツールをクリックしたらウィンドウが出てくるようにするには,ウィンドウクラスを定義した上で,上のコードの7〜9行目のようなコードを書けばよいことがわかった.

では,signal_spacing_frame_tを調査しよう.このクラスはgui/signal_spacing.hに書かれている.まずは定石どおりにヘッダファイルを見てほしい.private修飾子の中には信号間隔や設置オプションといったこのウィンドウで編集するパラメータ,player,呼び出し元ツール,numberinputやlabel,buttonといったguiコンポーネントの宣言が並ぶ.GUI系クラスのヘッダファイルで用意すべき変数は大きく分けて「制御するパラメータ」,「player・呼び出し元ツールといった決まった変数」,そして「ウィンドウで使うguiコンポーネント」の3つである.public修飾子の中にはコンストラクタ,action_triggered(),ヘルプファイル名を返す関数が並ぶ.

それでは,gui/signal_spacing.ccを調査しよう.お手元で当該ファイルを開きながら以下を読み進めてほしい.ここできちんと記述する必要がある関数はコンストラクタとaction_triggered()の2つである.まずはaction_triggered()の方から見てみよう.この関数は登録したguiコンポーネントに何らかのイベントが発生したとき呼ばれる関数である.action_triggered()には引数としてイベントを引き起こしたguiコンポーネントcompと,そのイベントの値が渡されてくる.今回は,値の方は使っていないのでcompに注目すればよい.ウィンドウ内のguiコンポーネントとcompと比較し,一致すればそのコンポーネントに対して処理を行う.例えば,comp == &remove_buttonであれば,removeの状態を反転させ,ボタンに反映させている.最後に呼び出し元ツールに変更した値を代入している(return true;の1行前).Simutransで提供されるbuttonオブジェクトはボタンを押しても勝手に状態が反転するわけではないことに注意されたい.

つづいて,コンストラクタについて調査しよう.渡されたplayer, 呼び出し元ツールなどを整理した後,各guiコンポーネントの配置処理を行っていることがわかるだろう.例えば,押しボタンの配置処理なら下のコードが配置処理のひとかたまりとなる.

1
2
3
4
remove_button.init( button_t::square_state, "remove interm. signals");
remove_button.add_listener(this);
remove_button.pressed = remove;
add_component( &remove_button, 3);

このコードでは1行目でremove_buttonを初期化(引数としてボタンの種類と,ボタン横のラベル文字列を取る)している.2行目でlistenerに登録(これによってaction_triggered()が呼ばれるようになる)する.3行目で押されたか状態を設定し,4行目でウィンドウにguiコンポーネントを登録する.

以上で,信号・標識設置ツールがどのようにしてGUIによる値設定を可能にしているのかが理解できた.toolのinit()でctrlキーが押されていたらウィンドウを生成し,ウィンドウ内ではguiコンポーネントを並べ,適切に初期化し,ボタンが押されたらそれに対して反応してユーザーが設定した値をtoolに格納する.このやりかたをそっくりそのままtool_build_way_tでもやればいいのである.

なお,設置ツール初期化時に必要に応じてウィンドウを生成するだけでは,設置ツールが使われなくなったときに自動でウィンドウが消えてくれない.tool_build_roadsign_tにはexit()という関数が実装されており(下のコード),そこでウィンドウの破壊処理が行われている.init()exit()の呼び出しはこの手のツール共通の親クラスであるtwo_click_tool_tで書かれているので,我々はexit()の中にウィンドウ破壊処理を書くだけでよい.忘れずにtool_build_way_tにも移植しよう.

1
2
3
4
5
bool tool_build_roadsign_t::exit( player_t *player )
{
	destroy_win((ptrdiff_t)this);
	return two_click_tool_t::exit(player);
}

例によって以下の解説に入る前に,まずは自力でGUIダイアログの実装にチャレンジしてほしい.


まずは,GUI画面を記述するファイルを作ろう.先ほどの信号・標識設置の場合ではsignal_spacing.h・.ccに相当する.ここではファイル名はroad_config.h・.ccとする.ヘッダファイル(gui/road_config.h)はgui/signal_spacing.hを参考にすると以下のようになる.今回置くguiコンポーネントは市道化防止ボタンただ一つだけである.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef road_config_h
#define road_config_h

#include "gui_frame.h"
#include "components/action_listener.h"

class button_t;
class tool_build_way_t;
class player_t;

class road_config_frame_t : public gui_frame_t, private action_listener_t
{
private:
	static uint8 street_flag;
	player_t *player;
	tool_build_way_t* tool;
	button_t button;

public:
	road_config_frame_t( player_t *, tool_build_way_t * );
	bool action_triggered(gui_action_creator_t*, value_t) OVERRIDE;
	const char * get_help_filename() const { return "road_config.txt"; }
};

#endif

つづいて,gui/signal_spacing.ccを参考にしてgui/road_config.ccを書く.実装する関数はコンストラクタとaction_triggered()の2つである.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include "components/gui_button.h"

#include "road_config.h"
#include "../simtool.h"
#include "../boden/wege/strasse.h"


uint8 road_config_frame_t::street_flag = 0;

road_config_frame_t::road_config_frame_t(player_t *player_, tool_build_way_t* tool_) :
	gui_frame_t( translator::translate("configure road") )
{
  player = player_;
  tool = tool_;
  street_flag = tool->get_street_flag();
  
  set_table_layout(1,0);
  button.init( button_t::square_state, "avoid becoming cityroad");
  button.add_listener(this);
  button.pressed = street_flag & strasse_t::AVOID_CITYROAD;
  add_component( &button );
  reset_min_windowsize();
  set_windowsize(get_min_windowsize());
}

bool road_config_frame_t::action_triggered( gui_action_creator_t *comp, value_t)
{
	if( comp == &button ) {
    button.pressed = !button.pressed;
    if(  button.pressed  ) {
      // AVOID_CITYROADをONにする
      street_flag |= strasse_t::AVOID_CITYROAD;
    } else {
      // AVOID_CITYROADをOFFにする
      street_flag &= ~(strasse_t::AVOID_CITYROAD);
    }
    tool->set_street_flag(street_flag);
	}
	return true;
}

上のコードではstrasse_tのenumを使うため5行目でstrasse.hをincludeしている.13〜15行目で変数の初期化を行い,17〜23行目でbuttonの配置をしている.26行目からaction_triggeredの記述が始まり,ボタンの状態を反転した上でstreet_flagのビット演算をしている.37行目でそれを呼び出し元ツールに戻している.

road_config_frame_tクラスが書き上がったので,tool_build_way_tから呼び出してあげよう.ウィンドウを閉じるために必要なexit()関数はtool_build_way_tに実装されていない(オーバーライドされていない)ので,信号・標識と同じようにpublic属性でヘッダファイル(simtool.h)で宣言する.(下のコード)

1
bool exit(player_t*) OVERRIDE;

simtool.ccは下のコードのように改変すればよい.road_config.hを新しく作り,それを使うので1行目でincludeしている.tool_build_way_t::init()の中にウィンドウ作成処理を記した.exit()関数も新しくtool_build_way_tに実装(15行目以降)した.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "gui/road_config.h"

bool tool_build_way_t::init( player_t *player )
{
	two_click_tool_t::init( player );
  if (is_ctrl_pressed()  &&  can_use_gui()) {
    create_win(new road_config_frame_t(player, this), w_info, (ptrdiff_t)this);
  }
  
  if( ok_sound == NO_SOUND ) {
    ok_sound = SFX_CASH;
  }
(以下省略)

bool tool_build_way_t::exit( player_t *player )
{
	destroy_win((ptrdiff_t)this);
	return two_click_tool_t::exit(player);
}

これでウィンドウから市道化可否をON/OFFできるようになり,我々の目的は達成された,はずである.最後に,simversion.hのSIM_SAVE_MINORを忘れずに1増やしておこう.(strasse_trdwr()で使ったバージョン番号と合っているか確認する.)

ところが,嬉々としてコンパイルを実行すると残念ながら次のエラーに出くわすであろう.

1
2
3
4
5
6
7
===> LD  /Users/XXXX/sim
Undefined symbols for architecture x86_64:
  "road_config_frame_t::road_config_frame_t(player_t*, tool_build_way_t*, bool)", referenced from:
      tool_build_way_t::init(player_t*, bool) in simtool.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [/Users/XXXX/sim] Error 1

symbols not found,つまり,コンパイラが「road_config_frame_tとか知りません」と怒っている.これはさきほど作成したroad_config.ccがMakefileに登録されておらず,このファイルに対するコンパイルが行われていないからである.よって,road_config.ccをコンパイルするようMakefileに追記しなければならない.

simutransソースコードディレクトリのトップ層にMakefileというファイルがあるので,それを開くとおよそ200〜500行目にわたって

1
2
3
4
SOURCES += gui/convoi_filter_frame.cc
SOURCES += gui/convoi_frame.cc
SOURCES += gui/convoi_info_t.cc
SOURCES += gui/convoy_item.cc

のように,ccファイルたちがコンパイル対象として追加されている.よって,road_config.ccをコンパイル対象として追加するためにはMakefileの適当な場所に

1
SOURCES += gui/road_config.cc

と追記すればよい.これでコンパイルが通ったはずだ.起動してctrlキーを押しながら道路建設アイコンをクリックすれば,下図のようなウィンドウで市道化をコントロールできるはずである.

ネットワークゲーム対応

先の節でこの改造は一応の完成を見た,ということになった.しかし,実はこのままの状態で世の中にリリースするとユーザーから不具合報告が来ることになる.現時点で残っている不具合は以下の2つである.

1つめの問題はさほど深刻ではないので放置するとして,ネットワークゲームで市道化防止が機能しないのは大きな問題である.しかし,どうしてこのようなことになってしまったのだろうか.バグを調査して,修正しよう.今回の不具合はネットワークゲームでの不具合である.ネットワークゲームのデバッグをするにはローカルでサーバとクライアントを建てればよい.サーバーは-serverオプションをつけてsimutransを起動し,クライアントは接続先に127.0.0.1(ローカル・ループバック・アドレス)を指定すればよい.

そもそもtool_build_way_t::do_work()street_flagは正しく設定されているのだろうか.それを調べるところから始めよう.tool_build_way_t::do_work()の5行目(street_flagを設定している行.下のコードを参照のこと)にブレークポイントを仕掛ける.(例:その行がsimtool.ccの2438行目であれば,b simtool.cc:2438 とgdbに入力)

1
2
3
4
5
6
7
8
const char *tool_build_way_t::do_work( player_t *player, const koord3d &start, const koord3d &end )
{
  way_builder_t bauigel(player);
  calc_route( bauigel, start, end );
  bauigel.set_street_flag(street_flag); //ここにブレークポイントを設定する
  if(  bauigel.get_route().get_count()>1  ) {
    welt->mute_sound(true);
    bauigel.build();

ブレークポイントを設定したら「r -server」でsimutransを起動.市道化防止を有効にして道路を建設してみよう.道路を建設するとブレークポイントに当たってsimutransがフリーズし,gdbが入力を受け付けるようになる.ここでstreet_flagの値を出力してみよう.デバッガでstreet_flagを出力させると,市道化防止を有効にしたはずなのになんとstreet_flagは0だと表示された.

1
2
(gdb) p street_flag
(uint8) $0 = '\0'

なるほど.では,ctrlキーを押して出てくるあのウィンドウからstreet_flagがきちんと設定されていなかったのではないか.もしくは外部からstreet_flagが0に書き換えられたのではないか.そこで,tool_build_way_t::set_street_flag()に下のコードの3行目のようにprintf文を仕込む.これで,外部からstreet_flagが操作されれば,それはprintf文によって出力されることになる.

1
2
3
4
5
6
class tool_build_way_t : public two_click_tool_t {
  void set_street_flag(uint8 a) {
		printf("flag:%d\n", a);
		street_flag = a;
	}
};

ところが,これをコンパイルしネットワークモードで実行し,ウィンドウから市道化防止フラグをONにして道路を建設してもコンソール画面にはflag:1と出力される.すなはち,tool_build_way_tstreet_flagはただ一度「1」に変更されただけという結果である.

tool_build_way_tstreet_flagはたしかに設定ウィンドウによって正しく1になった.しかし,do_work()の段階では0になってしまう.外部からの値操作がないとすると,tool_build_way_tで値が書き換えられたのだろうか?そうアタリをつけて原因箇所を探ってみても,残念ながら手がかりは得られない.ここで発想の転換が必要である.市道化防止設定ウィンドウを扱っているtool_build_way_tオブジェクトと,do_work()を実行しているtool_build_way_tオブジェクトは,実は別モノなのではないかと.

そこで,それぞれのオブジェクトのメモリ上のアドレスを見てあげることにしよう.これもprintfで見てあげればよい.set_street_flag()において先ほど設定値をprintf出力したが,オブジェクトアドレスも下のコードのように追加で出力する.

1
2
3
4
5
6
class tool_build_way_t : public two_click_tool_t {
  void set_street_flag(uint8 a) {
		printf("flag:%d, addr:%d\n", a, this);
		street_flag = a;
	}
};

同時にdo_work()を実行しているオブジェクトのアドレスも出力する.

1
2
3
4
5
6
const char *tool_build_way_t::do_work( player_t *player, const koord3d &start, const koord3d &end )
{
	printf("do_work: addr:%d\n", this);
	way_builder_t bauigel(player);
	calc_route( bauigel, start, end );
(以下省略)

これをコンパイルし,ネットワークモードで起動して市道化防止を設定し道路建設を行うと以下の結果を得る.

1
2
flag:1, addr:11225504
do_work: addr:719768224

2つのアドレスは異なっている.すなはち,市道化防止設定ウィンドウ経由でstreet_flagを編集したオブジェクトと,do_workを実行しているオブジェクトは別モノなのである.だから,do_workを実行するとstreet_flagの変更は反映されなかったのだ.

ちなみに,ローカルゲームの場合はこの2つのアドレス出力は一致する.オブジェクトが一致しているのでstreet_flagは正しく設定されていたのである.実は,ネットワークゲームで道路建設をするとき,クライアントは道路建設コマンドを発行しているだけである.サーバーはクライアントの発行したコマンドをネットワークごしに受け取り,do_work()を実行したらその結果を全てのクライアントに配信しているのである.市道化防止設定ウィンドウを扱っていたtool_build_way_tオブジェクトはクライアント側のオブジェクト,do_work()を実行したオブジェクトはサーバー側のオブジェクトなのである.一般に,ネットワークゲームではユーザーの相手をするツールオブジェクトと実際に動作を行うツールオブジェクトは別モノである.

これではツールが何かしらのパラメータを使って作業を行おうとしても,それを実行するオブジェクトに情報が伝わらないので困ってしまう.そこで,simutransではrdwr_custom_data()という関数が用意されている.これは親クラスtool_tが提供している関数であり,セーブデータ読み書きのrdwr()のようにこの中でネットワーク越しにパラメータを読み書きする関数である.rdwr_custom_data()tool_build_way_tではオーバーライドされていないが,tool_build_bridge_tではオーバーライドされているのでこれを真似て実装しよう.

ヘッダファイルでrdwr_custom_data()を宣言し,実装ファイルで実装してあげればよい.それぞれ下のように追記する.これでめでたくネットワークゲームでも機能するようになった.

1
2
3
4
// simtool.h
class tool_build_way_t : public two_click_tool_t {
  void rdwr_custom_data(memory_rw_t*) OVERRIDE;
};
1
2
3
4
5
6
7
8
// simtool.cc
void tool_build_way_t::rdwr_custom_data(memory_rw_t *packet)
{
	two_click_tool_t::rdwr_custom_data(packet);
	uint8 i = street_flag;
	packet->rdwr_byte(i);
	street_flag = i;
}

tool系の開発をするときはこのようにネットワークゲームでも所望の動作をするか細心の注意を払って開発する必要がある.