Simutrans のプレイヤー管理システムは、ゲーム内の複数の企業(プレイヤー)を管理するためのシステムです。人間プレイヤー、AI プレイヤー、公共事業プレイヤーを統一的に扱い、財務管理、権限制御、ネットワーク同期などを提供します。
プレイヤー管理システムは、以下のような用途で使用されます:
karte_t (ワールド)
↓
player_t[MAX_PLAYER_COUNT] (プレイヤー配列)
↓
finance_t (財務管理)
simlinemgmt_t (路線管理)
関連ファイル:
src/simutrans/player/simplay.{cc,h} - プレイヤーの実装src/simutrans/player/finance.{cc,h} - 財務システムsrc/simutrans/world/simworld.{cc,h} - ワールドとの連携src/simutrans/dataobj/scenario.{cc,h} - シナリオとの連携enum {
EMPTY = 0, // 空きスロット
HUMAN = 1, // 人間プレイヤー
AI_GOODS = 2, // 貨物輸送 AI
AI_PASSENGER = 3, // 旅客輸送 AI
AI_SCRIPTED = 4, // スクリプト制御 AI
MAX_AI,
PASSWORD_PROTECTED = 128 // パスワード保護フラグ
};
| プレイヤー番号 | 説明 | 定数 |
|---|---|---|
| 0 | 人間プレイヤー(主) | PLAYER_HUMAN_NR |
| 1 | 公共事業プレイヤー | PLAYER_PUBLIC_NR |
| 2-15 | 追加プレイヤー(AI) | - |
最大プレイヤー数: MAX_PLAYER_COUNT (通常 16)
class player_t {
protected:
char player_name_buf[256]; // プレイヤー名
finance_t *finance; // 財務データ
uint16 player_age; // 企業年齢
uint8 player_color_1, player_color_2; // カラースキーム
uint8 player_nr; // プレイヤー番号
bool active; // AI として有効か
bool locked; // ロック状態
bool unlock_pending; // アンロック待機中
pwd_hash_t pwd_hash; // パスワードハッシュ
sint32 headquarter_level; // 本社レベル
koord headquarter_pos; // 本社位置
sint32 scenario_completion; // シナリオ達成率
public:
simlinemgmt_t simlinemgmt; // 路線管理
};
各プレイヤーは独自の財務オブジェクトを持ちます:
class finance_t {
private:
player_t *player; // 所有プレイヤー
sint64 account_balance; // 口座残高
sint64 account_overdrawn; // 借金期間(月数)
// 財務履歴(過去 12 ヶ月 + 過去 12 年)
sint64 finance_history_year[MAX_HISTORY_YEARS][MAX_HISTORY_TYPES];
sint64 finance_history_month[MAX_HISTORY_MONTHS][MAX_HISTORY_TYPES];
};
| タイプ | 説明 |
|---|---|
CONSTRUCTION_COST |
建設費用 |
VEHICLE_REVENUE |
車両収益 |
VEHICLE_RUNNING_COSTS |
車両運行費 |
WAY_TOLL |
道路使用料 |
POWERLINES |
送電線収益・費用 |
OPERATIONS_PROFIT |
営業利益 |
MAINTENANCE |
維持費 |
ASSETS |
総資産 |
CASH |
現金 |
NET_WEALTH |
純資産 |
PROFIT |
純利益 |
// 口座残高の操作
void book_account(sint64 amount);
sint64 get_account_balance() const;
double get_account_balance_as_double() const;
// 借金状態のチェック
int get_account_overdrawn() const;
bool has_money_or_assets() const;
bool can_afford(sint64 cost) const;
// 履歴の取得
sint64 get_finance_history_year(int year, int type) const;
sint64 get_finance_history_month(int month, int type) const;
// 資産の更新
void calc_assets();
void update_assets(sint64 delta, waytype_t wt);
プレイヤーは以下の条件で倒産します:
NET_WEALTH < 0)bool player_t::new_month() {
// 財務更新
finance->new_month();
// 純資産がマイナスか確認
sint64 net_wealth = finance->get_net_wealth();
if (net_wealth < 0) {
account_overdrawn++;
if (account_overdrawn >= 3) {
// 3ヶ月連続でマイナス → 倒産
ai_bankrupt();
return false; // プレイヤーを削除
}
} else {
account_overdrawn = 0;
}
return true; // 継続
}
void player_t::ai_bankrupt() {
// すべての資産を削除
// - 車両を売却
// - 停留所を削除
// - 路線を削除
// - インフラを削除
// プレイヤーをロック
locked = true;
// メッセージを表示
world->get_message()->add_message(
player_name_buf + " has gone bankrupt!",
koord::invalid,
message_t::general
);
}
プレイヤーは以下の状態を持ちます:
bool is_locked() const { return locked; }
bool is_unlock_pending() const { return unlock_pending; }
void unlock(bool unlock_, bool unlock_pending_ = false) {
locked = !unlock_;
unlock_pending = unlock_pending_;
}
ロック状態の意味:
class player_t {
private:
pwd_hash_t pwd_hash; // SHA1 ハッシュ
public:
void check_unlock(const pwd_hash_t& hash) {
if (pwd_hash == hash) {
unlock(true);
}
}
pwd_hash_t& access_password_hash() {
return pwd_hash;
}
};
パスワードハッシュの保存:
class karte_t {
private:
pwd_hash_t player_password_hash[MAX_PLAYER_COUNT];
public:
void rdwr_player_password_hashes(loadsave_t *file) {
for (int i = 0; i < MAX_PLAYER_COUNT; i++) {
player_password_hash[i].rdwr(file);
}
}
};
class player_t {
private:
sint32 headquarter_level; // 0 = なし, 1-5 = レベル
koord headquarter_pos; // マップ上の位置
public:
void add_headquarter(short hq_level, koord hq_pos) {
headquarter_level = hq_level;
headquarter_pos = hq_pos;
}
koord get_headquarter_pos() const {
return headquarter_pos;
}
short get_headquarter_level() const {
return headquarter_level;
}
};
// ツールから呼び出される
tool_t::work() {
// 本社建設地点を選択
koord pos = get_selected_position();
// プレイヤーに本社を追加
player->add_headquarter(1, pos);
// 建物オブジェクトを配置
gebaeude_t* hq = new gebaeude_t(pos, player, desc);
world->set_building(pos, hq);
}
プレイヤーは画面上に浮かぶメッセージを表示できます:
class player_t {
private:
class income_message_t {
public:
koord pos; // 表示位置
sint64 amount; // 金額
uint32 alter; // 経過時間
void * operator new(size_t);
void operator delete(void *p);
};
slist_tpl<income_message_t *> messages;
public:
void add_money_message(sint64 amount, koord k);
void display_messages();
void age_messages(uint32 delta_t);
};
void player_t::display_messages() {
for (income_message_t* msg : messages) {
if (msg->alter < MAX_AGE) {
// 画面座標に変換
scr_coord screen_pos = world->get_viewport()->get_screen_coord(msg->pos);
// 金額をフォーマット
char buffer[64];
money_to_string(buffer, msg->amount);
// 画面に描画
display_proportional_clip(
screen_pos.x, screen_pos.y,
buffer,
ALIGN_LEFT,
msg->amount >= 0 ? COL_GREEN : COL_RED,
true
);
}
}
}
AI プレイヤーは player_t から派生したクラスで実装されます:
class ai_goods_t : public player_t {
public:
virtual void step() override; // 毎フレーム呼ばれる
virtual bool new_month() override; // 月次処理
virtual uint8 get_ai_id() const override { return AI_GOODS; }
};
class ai_passenger_t : public player_t {
public:
virtual void step() override;
virtual bool new_month() override;
virtual uint8 get_ai_id() const override { return AI_PASSENGER; }
};
void ai_goods_t::step() {
if (!active) return;
// 1. 収益性の高いルートを探す
find_profitable_routes();
// 2. 車両を購入・配置
buy_and_deploy_vehicles();
// 3. 赤字路線を削除
remove_unprofitable_routes();
// 4. インフラを拡張
expand_infrastructure();
}
class player_t {
private:
sint32 scenario_completion; // 0-100
public:
sint32 get_scenario_completion() const {
return scenario_completion;
}
void set_scenario_completion(sint32 percent) {
scenario_completion = clamp(percent, 0, 100);
}
};
// Squirrel スクリプト
function step() {
local player = world.get_player(0)
local completion = calculate_player_progress(player)
player.set_scenario_completion(completion)
}
マルチプレイヤーでは、パスワードハッシュがクライアント間で同期されます:
void karte_t::rdwr_player_password_hashes(loadsave_t *file) {
for (int i = 0; i < MAX_PLAYER_COUNT; i++) {
player_password_hash[i].rdwr(file);
}
}
const pwd_hash_t& karte_t::get_player_password_hash(uint8 player_nr) const {
return player_password_hash[player_nr];
}
void karte_t::clear_player_password_hashes() {
for (int i = 0; i < MAX_PLAYER_COUNT; i++) {
player_password_hash[i].clear();
}
}
void karte_t::call_change_player_tool(
uint8 cmd,
uint8 player_nr,
uint16 param,
bool scripted_call
) {
enum change_player_tool_cmds {
new_player = 1,
toggle_freeplay,
delete_player,
// ...
};
switch (cmd) {
case new_player:
// 新しいプレイヤーを作成
create_new_player(player_nr, param);
break;
case delete_player:
// プレイヤーを削除
remove_player(player_nr);
break;
}
}
公共事業プレイヤー(player 1)は特別な役割を持ちます:
bool player_t::is_public_service() const {
return player_nr == PLAYER_PUBLIC_NR;
}
player_t* karte_t::get_public_player() const {
return players[PLAYER_PUBLIC_NR];
}
機能:
// 道路を建設する際
if (builder->is_public_road()) {
// 公共事業プレイヤーが所有
owner = world->get_public_player();
} else {
// アクティブプレイヤーが所有
owner = world->get_active_player();
}
プレイヤーは最近の操作を取り消すことができます:
class player_t {
private:
waytype_t undo_type; // 対象の Waytype
vector_tpl<koord3d> last_built; // 建設リスト
public:
void init_undo(waytype_t wtype, unsigned short max);
void add_undo(koord3d k);
sint64 undo();
};
sint64 player_t::undo() {
sint64 cost = 0;
// 逆順で削除
while (!last_built.empty()) {
koord3d pos = last_built.pop_back();
// 建設物を削除
cost += remove_construction_at(pos);
}
return cost;
}
各プレイヤーは 2 色のカラースキームを持ちます:
class player_t {
private:
uint8 player_color_1, player_color_2;
public:
uint8 get_player_color1() const { return player_color_1; }
uint8 get_player_color2() const { return player_color_2; }
void set_player_color(uint8 col1, uint8 col2) {
player_color_1 = col1;
player_color_2 = col2;
}
};
void settings_t::set_player_color_to_default(player_t* player) {
static const uint8 default_colors[][2] = {
{28, 255}, // Player 0: Blue
{86, 255}, // Player 1 (Public): Yellow
{135, 255}, // Player 2: Red
{76, 255}, // Player 3: Purple
// ...
};
uint8 nr = player->get_player_nr();
player->set_player_color(
default_colors[nr][0],
default_colors[nr][1]
);
}
class karte_t {
private:
player_t *active_player; // 現在操作中のプレイヤー
uint8 active_player_nr; // プレイヤー番号
public:
player_t* get_active_player() const {
return active_player;
}
uint8 get_active_player_nr() const {
return active_player_nr;
}
void switch_active_player(uint8 nr) {
if (nr < MAX_PLAYER_COUNT && players[nr]) {
active_player = players[nr];
active_player_nr = nr;
}
}
};
void player_t::rdwr(loadsave_t *file) {
// 基本情報
file->rdwr_str(player_name_buf);
file->rdwr_short(player_age);
file->rdwr_byte(player_nr);
file->rdwr_byte(player_color_1);
file->rdwr_byte(player_color_2);
// 財務データ
finance->rdwr(file);
// 本社情報
headquarter_pos.rdwr(file);
file->rdwr_long(headquarter_level);
// パスワードハッシュ
pwd_hash.rdwr(file);
// 路線管理
simlinemgmt.rdwr(file);
// シナリオ進捗
file->rdwr_long(scenario_completion);
}
void player_t::debug_print() {
dbg->message("player_t", "Player %d: %s",
player_nr, player_name_buf);
dbg->message(" Balance: %lld",
finance->get_account_balance());
dbg->message(" Net Wealth: %lld",
finance->get_net_wealth());
dbg->message(" Active: %d, Locked: %d",
active, locked);
}
// デバッグコンソールから
debug_player(0) // プレイヤー 0 の情報を表示
set_money(0, 1000000) // プレイヤー 0 に資金を追加
lock_player(2) // プレイヤー 2 をロック
// 良い例: 月次処理
bool player_t::new_month() {
// 重い計算は月に1回
finance->calc_assets();
finance->new_month();
return true;
}
// 悪い例: 毎フレーム重い処理
void player_t::step() {
// これは避ける
finance->calc_assets(); // 重い!
}
src/simutrans/player/simplay.{cc,h}src/simutrans/player/finance.{cc,h}src/simutrans/player/ai_{goods,passenger,scripted}.{cc,h}src/simutrans/world/simworld.{cc,h}Simutrans のプレイヤー管理システムは、人間プレイヤー、AI、公共事業を統一的に扱う包括的なシステムです。財務管理、権限制御、ネットワーク同期など、多様な機能を提供しており、シングルプレイヤーからマルチプレイヤー、シナリオまで幅広く対応しています。
プレイヤーシステムの理解は、Simutrans の内部構造を把握する上で重要であり、MOD 開発や AI の実装にも役立ちます。