/*
* This file is part of OpenTTD.
* OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
* OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see .
*/
/** @file rail_gui.cpp %File for dealing with rail construction user interface */
#include "stdafx.h"
#include "gui.h"
#include "window_gui.h"
#include "station_gui.h"
#include "terraform_gui.h"
#include "viewport_func.h"
#include "command_func.h"
#include "waypoint_func.h"
#include "newgrf_station.h"
#include "company_base.h"
#include "strings_func.h"
#include "window_func.h"
#include "date_func.h"
#include "sound_func.h"
#include "company_func.h"
#include "widgets/dropdown_type.h"
#include "tunnelbridge.h"
#include "tilehighlight_func.h"
#include "spritecache.h"
#include "core/geometry_func.hpp"
#include "hotkeys.h"
#include "engine_base.h"
#include "vehicle_func.h"
#include "zoom_func.h"
#include "rail_gui.h"
#include "querystring_gui.h"
#include "sortlist_type.h"
#include "stringfilter_type.h"
#include "string_func.h"
#include "station_cmd.h"
#include "tunnelbridge_cmd.h"
#include "waypoint_cmd.h"
#include "rail_cmd.h"
#include "station_map.h"
#include "tunnelbridge_map.h"
#include "widgets/rail_widget.h"
#include "safeguards.h"
static RailType _cur_railtype; ///< Rail type of the current build-rail toolbar.
static bool _remove_button_clicked; ///< Flag whether 'remove' toggle-button is currently enabled
static DiagDirection _build_depot_direction; ///< Currently selected depot direction
static byte _waypoint_count = 1; ///< Number of waypoint types
static byte _cur_waypoint_type; ///< Currently selected waypoint type
static bool _convert_signal_button; ///< convert signal button in the signal GUI pressed
static SignalVariant _cur_signal_variant; ///< set the signal variant (for signal GUI)
static SignalType _cur_signal_type; ///< set the signal type (for signal GUI)
struct RailStationGUISettings {
Axis orientation; ///< Currently selected rail station orientation
bool newstations; ///< Are custom station definitions available?
StationClassID station_class; ///< Currently selected custom station class (if newstations is \c true )
byte station_type; ///< %Station type within the currently selected custom station class (if newstations is \c true )
byte station_count; ///< Number of custom stations (if newstations is \c true )
};
static RailStationGUISettings _railstation; ///< Settings of the station builder GUI
static void HandleStationPlacement(TileIndex start, TileIndex end);
static void ShowBuildTrainDepotPicker(Window *parent);
static void ShowBuildWaypointPicker(Window *parent);
static Window *ShowStationBuilder(Window *parent);
static void ShowSignalBuilder(Window *parent);
/**
* Check whether a station type can be build.
* @return true if building is allowed.
*/
static bool IsStationAvailable(const StationSpec *statspec)
{
if (statspec == nullptr || !HasBit(statspec->callback_mask, CBM_STATION_AVAIL)) return true;
uint16 cb_res = GetStationCallback(CBID_STATION_AVAILABILITY, 0, 0, statspec, nullptr, INVALID_TILE);
if (cb_res == CALLBACK_FAILED) return true;
return Convert8bitBooleanCallback(statspec->grf_prop.grffile, CBID_STATION_AVAILABILITY, cb_res);
}
void CcPlaySound_CONSTRUCTION_RAIL(Commands cmd, const CommandCost &result,TileIndex tile, const CommandDataBuffer &)
{
if (result.Succeeded() && _settings_client.sound.confirm) SndPlayTileFx(SND_20_CONSTRUCTION_RAIL, tile);
}
static void GenericPlaceRail(TileIndex tile, Track track)
{
if (_remove_button_clicked) {
Command::Post(STR_ERROR_CAN_T_REMOVE_RAILROAD_TRACK, CcPlaySound_CONSTRUCTION_RAIL,
tile, track);
} else {
Command::Post(STR_ERROR_CAN_T_BUILD_RAILROAD_TRACK, CcPlaySound_CONSTRUCTION_RAIL,
tile, _cur_railtype, track, _settings_client.gui.auto_remove_signals);
}
}
/**
* Try to add an additional rail-track at the entrance of a depot
* @param tile Tile to use for adding the rail-track
* @param dir Direction to check for already present tracks
* @param track Track to add
* @see CcRailDepot()
*/
static void PlaceExtraDepotRail(TileIndex tile, DiagDirection dir, Track track)
{
if (GetRailTileType(tile) == RAIL_TILE_DEPOT) return;
if (GetRailTileType(tile) == RAIL_TILE_SIGNALS && !_settings_client.gui.auto_remove_signals) return;
if ((GetTrackBits(tile) & DiagdirReachesTracks(dir)) == 0) return;
Command::Post(tile, _cur_railtype, track, _settings_client.gui.auto_remove_signals);
}
/** Additional pieces of track to add at the entrance of a depot. */
static const Track _place_depot_extra_track[12] = {
TRACK_LEFT, TRACK_UPPER, TRACK_UPPER, TRACK_RIGHT, // First additional track for directions 0..3
TRACK_X, TRACK_Y, TRACK_X, TRACK_Y, // Second additional track
TRACK_LOWER, TRACK_LEFT, TRACK_RIGHT, TRACK_LOWER, // Third additional track
};
/** Direction to check for existing track pieces. */
static const DiagDirection _place_depot_extra_dir[12] = {
DIAGDIR_SE, DIAGDIR_SW, DIAGDIR_SE, DIAGDIR_SW,
DIAGDIR_SW, DIAGDIR_NW, DIAGDIR_NE, DIAGDIR_SE,
DIAGDIR_NW, DIAGDIR_NE, DIAGDIR_NW, DIAGDIR_NE,
};
void CcRailDepot(Commands cmd, const CommandCost &result, TileIndex tile, const CommandDataBuffer &data)
{
if (result.Failed()) return;
auto [tile_, rt, dir] = EndianBufferReader::ToValue::Args>(data);
if (_settings_client.sound.confirm) SndPlayTileFx(SND_20_CONSTRUCTION_RAIL, tile);
if (!_settings_client.gui.persistent_buildingtools) ResetObjectToPlace();
tile += TileOffsByDiagDir(dir);
if (IsTileType(tile, MP_RAILWAY)) {
PlaceExtraDepotRail(tile, _place_depot_extra_dir[dir], _place_depot_extra_track[dir]);
PlaceExtraDepotRail(tile, _place_depot_extra_dir[dir + 4], _place_depot_extra_track[dir + 4]);
PlaceExtraDepotRail(tile, _place_depot_extra_dir[dir + 8], _place_depot_extra_track[dir + 8]);
}
}
/**
* Place a rail waypoint.
* @param tile Position to start dragging a waypoint.
*/
static void PlaceRail_Waypoint(TileIndex tile)
{
if (_remove_button_clicked) {
VpStartPlaceSizing(tile, VPM_X_AND_Y, DDSP_REMOVE_STATION);
return;
}
Axis axis = GetAxisForNewWaypoint(tile);
if (IsValidAxis(axis)) {
/* Valid tile for waypoints */
VpStartPlaceSizing(tile, axis == AXIS_X ? VPM_X_LIMITED : VPM_Y_LIMITED, DDSP_BUILD_STATION);
VpSetPlaceSizingLimit(_settings_game.station.station_spread);
} else {
/* Tile where we can't build rail waypoints. This is always going to fail,
* but provides the user with a proper error message. */
Command::Post(STR_ERROR_CAN_T_BUILD_TRAIN_WAYPOINT, tile, AXIS_X, 1, 1, STAT_CLASS_WAYP, 0, INVALID_STATION, false);
}
}
void CcStation(Commands cmd, const CommandCost &result, TileIndex tile, const CommandDataBuffer &)
{
if (result.Failed()) return;
if (_settings_client.sound.confirm) SndPlayTileFx(SND_20_CONSTRUCTION_RAIL, tile);
/* Only close the station builder window if the default station and non persistent building is chosen. */
if (_railstation.station_class == STAT_CLASS_DFLT && _railstation.station_type == 0 && !_settings_client.gui.persistent_buildingtools) ResetObjectToPlace();
}
/**
* Place a rail station.
* @param tile Position to place or start dragging a station.
*/
static void PlaceRail_Station(TileIndex tile)
{
if (_remove_button_clicked) {
VpStartPlaceSizing(tile, VPM_X_AND_Y_LIMITED, DDSP_REMOVE_STATION);
VpSetPlaceSizingLimit(-1);
} else if (_settings_client.gui.station_dragdrop) {
VpStartPlaceSizing(tile, VPM_X_AND_Y_LIMITED, DDSP_BUILD_STATION);
VpSetPlaceSizingLimit(_settings_game.station.station_spread);
} else {
int w = _settings_client.gui.station_numtracks;
int h = _settings_client.gui.station_platlength;
if (!_railstation.orientation) Swap(w, h);
RailStationGUISettings params = _railstation;
RailType rt = _cur_railtype;
byte numtracks = _settings_client.gui.station_numtracks;
byte platlength = _settings_client.gui.station_platlength;
bool adjacent = _ctrl_pressed;
auto proc = [=](bool test, StationID to_join) -> bool {
if (test) {
return Command::Do(CommandFlagsToDCFlags(GetCommandFlags()), tile, rt, params.orientation, numtracks, platlength, params.station_class, params.station_type, INVALID_STATION, adjacent).Succeeded();
} else {
return Command::Post(STR_ERROR_CAN_T_BUILD_RAILROAD_STATION, CcStation, tile, rt, params.orientation, numtracks, platlength, params.station_class, params.station_type, to_join, adjacent);
}
};
ShowSelectStationIfNeeded(TileArea(tile, w, h), proc);
}
}
/**
* Build a new signal or edit/remove a present signal, use CmdBuildSingleSignal() or CmdRemoveSingleSignal() in rail_cmd.cpp
*
* @param tile The tile where the signal will build or edit
*/
static void GenericPlaceSignals(TileIndex tile)
{
TrackBits trackbits = TrackStatusToTrackBits(GetTileTrackStatus(tile, TRANSPORT_RAIL, 0));
if (trackbits & TRACK_BIT_VERT) { // N-S direction
trackbits = (_tile_fract_coords.x <= _tile_fract_coords.y) ? TRACK_BIT_RIGHT : TRACK_BIT_LEFT;
}
if (trackbits & TRACK_BIT_HORZ) { // E-W direction
trackbits = (_tile_fract_coords.x + _tile_fract_coords.y <= 15) ? TRACK_BIT_UPPER : TRACK_BIT_LOWER;
}
Track track = FindFirstTrack(trackbits);
if (_remove_button_clicked) {
Command::Post(STR_ERROR_CAN_T_REMOVE_SIGNALS_FROM, CcPlaySound_CONSTRUCTION_RAIL, tile, track);
} else {
/* Which signals should we cycle through? */
SignalType cycle_start = _settings_client.gui.cycle_signal_types == SIGNAL_CYCLE_ALL && _settings_client.gui.signal_gui_mode == SIGNAL_GUI_ALL ? SIGTYPE_NORMAL : SIGTYPE_PBS;
if (FindWindowById(WC_BUILD_SIGNAL, 0) != nullptr) {
/* signal GUI is used */
Command::Post(_convert_signal_button ? STR_ERROR_SIGNAL_CAN_T_CONVERT_SIGNALS_HERE : STR_ERROR_CAN_T_BUILD_SIGNALS_HERE, CcPlaySound_CONSTRUCTION_RAIL,
tile, track, _cur_signal_type, _cur_signal_variant, _convert_signal_button, false, _ctrl_pressed, cycle_start, SIGTYPE_LAST, 0, 0);
} else {
SignalVariant sigvar = _cur_year < _settings_client.gui.semaphore_build_before ? SIG_SEMAPHORE : SIG_ELECTRIC;
Command::Post(STR_ERROR_CAN_T_BUILD_SIGNALS_HERE, CcPlaySound_CONSTRUCTION_RAIL,
tile, track, SIGTYPE_PBS_ONEWAY, sigvar, false, false, _ctrl_pressed, cycle_start, SIGTYPE_LAST, 0, 0);
}
}
}
/**
* Start placing a rail bridge.
* @param tile Position of the first tile of the bridge.
* @param w Rail toolbar window.
*/
static void PlaceRail_Bridge(TileIndex tile, Window *w)
{
if (IsBridgeTile(tile)) {
TileIndex other_tile = GetOtherTunnelBridgeEnd(tile);
Point pt = {0, 0};
w->OnPlaceMouseUp(VPM_X_OR_Y, DDSP_BUILD_BRIDGE, pt, other_tile, tile);
} else {
VpStartPlaceSizing(tile, VPM_X_OR_Y, DDSP_BUILD_BRIDGE);
}
}
/** Command callback for building a tunnel */
void CcBuildRailTunnel(Commands cmd, const CommandCost &result,TileIndex tile, const CommandDataBuffer &)
{
if (result.Succeeded()) {
if (_settings_client.sound.confirm) SndPlayTileFx(SND_20_CONSTRUCTION_RAIL, tile);
if (!_settings_client.gui.persistent_buildingtools) ResetObjectToPlace();
} else {
SetRedErrorSquare(_build_tunnel_endtile);
}
}
/**
* Toggles state of the Remove button of Build rail toolbar
* @param w window the button belongs to
*/
static void ToggleRailButton_Remove(Window *w)
{
CloseWindowById(WC_SELECT_STATION, 0);
w->ToggleWidgetLoweredState(WID_RAT_REMOVE);
w->SetWidgetDirty(WID_RAT_REMOVE);
_remove_button_clicked = w->IsWidgetLowered(WID_RAT_REMOVE);
SetSelectionRed(_remove_button_clicked);
}
/**
* Updates the Remove button because of Ctrl state change
* @param w window the button belongs to
* @return true iff the remove button was changed
*/
static bool RailToolbar_CtrlChanged(Window *w)
{
if (w->IsWidgetDisabled(WID_RAT_REMOVE)) return false;
/* allow ctrl to switch remove mode only for these widgets */
for (uint i = WID_RAT_BUILD_NS; i <= WID_RAT_BUILD_STATION; i++) {
if ((i <= WID_RAT_AUTORAIL || i >= WID_RAT_BUILD_WAYPOINT) && w->IsWidgetLowered(i)) {
ToggleRailButton_Remove(w);
return true;
}
}
return false;
}
/**
* The "remove"-button click proc of the build-rail toolbar.
* @param w Build-rail toolbar window
* @see BuildRailToolbarWindow::OnClick()
*/
static void BuildRailClick_Remove(Window *w)
{
if (w->IsWidgetDisabled(WID_RAT_REMOVE)) return;
ToggleRailButton_Remove(w);
if (_settings_client.sound.click_beep) SndPlayFx(SND_15_BEEP);
/* handle station builder */
if (w->IsWidgetLowered(WID_RAT_BUILD_STATION)) {
if (_remove_button_clicked) {
/* starting drag & drop remove */
if (!_settings_client.gui.station_dragdrop) {
SetTileSelectSize(1, 1);
} else {
VpSetPlaceSizingLimit(-1);
}
} else {
/* starting station build mode */
if (!_settings_client.gui.station_dragdrop) {
int x = _settings_client.gui.station_numtracks;
int y = _settings_client.gui.station_platlength;
if (_railstation.orientation == 0) Swap(x, y);
SetTileSelectSize(x, y);
} else {
VpSetPlaceSizingLimit(_settings_game.station.station_spread);
}
}
}
}
static void DoRailroadTrack(Track track)
{
if (_remove_button_clicked) {
Command::Post(STR_ERROR_CAN_T_REMOVE_RAILROAD_TRACK, CcPlaySound_CONSTRUCTION_RAIL,
TileVirtXY(_thd.selstart.x, _thd.selstart.y), TileVirtXY(_thd.selend.x, _thd.selend.y), track);
} else {
Command::Post(STR_ERROR_CAN_T_BUILD_RAILROAD_TRACK, CcPlaySound_CONSTRUCTION_RAIL,
TileVirtXY(_thd.selstart.x, _thd.selstart.y), TileVirtXY(_thd.selend.x, _thd.selend.y), _cur_railtype, track, _settings_client.gui.auto_remove_signals, false);
}
}
static void HandleAutodirPlacement()
{
Track trackstat = static_cast