2 * This file is part of OpenTTD.
3 * 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.
4 * 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.
5 * 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 <http://www.gnu.org/licenses/>.
8 /** @file zoning_cmd.cpp */
12 #include "station_type.h"
13 #include "station_base.h"
16 #include "viewport_func.h"
18 #include "company_func.h"
20 #include "table/sprites.h"
21 #include "station_func.h"
22 #include "station_map.h"
24 #include "tracerestrict.h"
25 #include "window_func.h"
27 #include "viewport_func.h"
29 #include "animated_tile.h"
30 #include "3rdparty/cpp-btree/btree_set.h"
33 static const SpriteID ZONING_INVALID_SPRITE_ID
= UINT_MAX
;
35 static btree::btree_set
<uint32_t> _zoning_cache_inner
;
36 static btree::btree_set
<uint32_t> _zoning_cache_outer
;
39 * Draw the zoning sprites.
41 * @param SpriteID image
43 * @param SpriteID colour
44 * the colour of the zoning
48 void DrawZoningSprites(SpriteID image
, SpriteID colour
, const TileInfo
*ti
)
50 if (colour
!= ZONING_INVALID_SPRITE_ID
) {
51 AddSortableSpriteToDraw(image
+ ti
->tileh
, colour
, ti
->x
, ti
->y
, 0x10, 0x10, 1, ti
->z
+ 7);
56 * Detect whether this area is within the acceptance of any station.
58 * @param TileArea area
59 * the area to search by
61 * the owner of the stations which we need to match again
62 * @param StationFacility facility_mask
63 * one or more facilities in the mask must be present for a station to be used
64 * @return true if a station is found
66 bool IsAreaWithinAcceptanceZoneOfStation(TileArea area
, Owner owner
, StationFacility facility_mask
)
68 StationFinder
morestations(area
);
70 for (const Station
*st
: morestations
.GetStations()) {
71 if (st
->owner
!= owner
|| !(st
->facilities
& facility_mask
)) continue;
72 Rect rect
= st
->GetCatchmentRect();
73 return TileArea(TileXY(rect
.left
, rect
.top
), TileXY(rect
.right
, rect
.bottom
)).Intersects(area
);
80 * Check whether the player can build in tile.
82 * @param TileIndex tile
84 * @return red if they cannot
86 SpriteID
TileZoneCheckBuildEvaluation(TileIndex tile
, Owner owner
)
88 /* Let's first check for the obvious things you cannot build on */
89 switch (GetTileType(tile
)) {
95 return SPR_ZONING_INNER_HIGHLIGHT_RED
;
97 /* There are only two things you can own (or some else
98 * can own) that you can still build on. i.e. roads and
101 * Add something more intelligent, check what tool the
102 * user is currently using (and if none, assume some
103 * standards), then check it against if owned by some-
104 * one else (e.g. railway on someone else's road).
105 * While that being said, it should also check if it
106 * is not possible to build railway/road on someone
107 * else's/your own road/railway (e.g. the railway track
108 * is curved or a cross).
112 if (GetTileOwner(tile
) != owner
) {
113 return SPR_ZONING_INNER_HIGHLIGHT_RED
;
115 return ZONING_INVALID_SPRITE_ID
;
119 return ZONING_INVALID_SPRITE_ID
;
124 * Check the opinion of the local authority in the tile.
126 * @param TileIndex tile
128 * @return black if no opinion, orange if bad,
129 * light blue if good or invalid if no town
131 SpriteID
TileZoneCheckOpinionEvaluation(TileIndex tile
, Owner owner
)
133 int opinion
= 0; // 0: no town, 1: no opinion, 2: bad, 3: good
134 Town
*town
= ClosestTownFromTile(tile
, _settings_game
.economy
.dist_local_authority
);
136 if (town
!= nullptr) {
137 if (HasBit(town
->have_ratings
, owner
)) {
138 opinion
= (town
->ratings
[owner
] > 0) ? 3 : 2;
145 case 1: return SPR_ZONING_INNER_HIGHLIGHT_BLACK
; // no opinion
146 case 2: return SPR_ZONING_INNER_HIGHLIGHT_ORANGE
; // bad
147 case 3: return SPR_ZONING_INNER_HIGHLIGHT_LIGHT_BLUE
; // good
148 default: return ZONING_INVALID_SPRITE_ID
; // no town
153 * Detect whether the tile is within the catchment zone of a station.
155 * @param TileIndex tile
157 * @param open_window_only
158 * only use stations which have their station window open
159 * @return black if within, light blue if only in acceptance zone
160 * and nothing if no nearby station.
162 SpriteID
TileZoneCheckStationCatchmentEvaluation(TileIndex tile
, Owner owner
, bool open_window_only
)
164 // Never on a station.
165 if (IsTileType(tile
, MP_STATION
)) {
166 return ZONING_INVALID_SPRITE_ID
;
169 StationFinder
stations(TileArea(tile
, 1, 1));
171 for (const Station
*st
: stations
.GetStations()) {
172 if (st
->owner
== owner
) {
173 if (!open_window_only
|| FindWindowById(WC_STATION_VIEW
, st
->index
) != nullptr) {
174 return SPR_ZONING_INNER_HIGHLIGHT_LIGHT_BLUE
;
179 return ZONING_INVALID_SPRITE_ID
;
183 * Detect whether a building is unserved by a station of owner.
185 * @param TileIndex tile
187 * @return red if unserved, orange if only accepting, nothing if served or not
190 SpriteID
TileZoneCheckUnservedBuildingsEvaluation(TileIndex tile
, Owner owner
)
192 if (!IsTileType(tile
, MP_HOUSE
)) {
193 return ZONING_INVALID_SPRITE_ID
;
196 auto has_town_cargo
= [&](const CargoArray
&dat
) {
197 for (CargoID cid
: SetCargoBitIterator(CargoSpec::town_production_cargo_mask
[TPE_PASSENGERS
] | CargoSpec::town_production_cargo_mask
[TPE_MAIL
])) {
198 if (dat
[cid
] > 0) return true;
205 AddAcceptedCargo(tile
, dat
, nullptr);
206 if (!has_town_cargo(dat
)) {
207 /* nothing is accepted, so now test if cargo is produced */
208 AddProducedCargo(tile
, dat
);
209 if (!has_town_cargo(dat
)) {
210 /* still don't have town cargo, so give up */
211 return ZONING_INVALID_SPRITE_ID
;
215 StationFinder
stations(TileArea(tile
, 1, 1));
217 for (const Station
*st
: stations
.GetStations()) {
218 if (st
->owner
== owner
) {
219 return ZONING_INVALID_SPRITE_ID
;
223 return SPR_ZONING_INNER_HIGHLIGHT_RED
;
227 * Detect whether an industry is unserved by a station of owner.
229 * @param TileIndex tile
231 * @return red if unserved, orange if only accepting, nothing if served or not
234 SpriteID
TileZoneCheckUnservedIndustriesEvaluation(TileIndex tile
, Owner owner
)
236 if (IsTileType(tile
, MP_INDUSTRY
)) {
237 const Industry
*ind
= Industry::GetByTile(tile
);
238 if (ind
->neutral_station
!= nullptr) return ZONING_INVALID_SPRITE_ID
;
240 for (const Station
*st
: ind
->stations_near
) {
241 if (st
->owner
== owner
) {
242 if (st
->facilities
& (~(FACIL_BUS_STOP
| FACIL_TRUCK_STOP
)) || st
->facilities
== (FACIL_BUS_STOP
| FACIL_TRUCK_STOP
)) {
243 return ZONING_INVALID_SPRITE_ID
;
244 } else if (st
->facilities
& (FACIL_BUS_STOP
| FACIL_TRUCK_STOP
)) {
245 for (const auto &p
: ind
->Produced()) {
246 if (p
.cargo
!= INVALID_CARGO
&& st
->facilities
& (IsCargoInClass(p
.cargo
, CC_PASSENGERS
) ? FACIL_BUS_STOP
: FACIL_TRUCK_STOP
)) {
247 return ZONING_INVALID_SPRITE_ID
;
250 for (const auto &a
: ind
->Accepted()) {
251 if (a
.cargo
!= INVALID_CARGO
&& st
->facilities
& (IsCargoInClass(a
.cargo
, CC_PASSENGERS
) ? FACIL_BUS_STOP
: FACIL_TRUCK_STOP
)) {
252 return ZONING_INVALID_SPRITE_ID
;
259 return SPR_ZONING_INNER_HIGHLIGHT_RED
;
262 return ZONING_INVALID_SPRITE_ID
;
266 * Detect whether a tile is a restricted signal tile
268 * @param TileIndex tile
270 * @return red if a restricted signal, nothing otherwise
272 SpriteID
TileZoneCheckTraceRestrictEvaluation(TileIndex tile
, Owner owner
)
274 if (IsTileType(tile
, MP_RAILWAY
) && HasSignals(tile
) && IsRestrictedSignal(tile
)) {
275 return SPR_ZONING_INNER_HIGHLIGHT_RED
;
277 if (IsTunnelBridgeWithSignalSimulation(tile
) && IsTunnelBridgeRestrictedSignal(tile
)) {
278 return SPR_ZONING_INNER_HIGHLIGHT_RED
;
281 return ZONING_INVALID_SPRITE_ID
;
285 * Detect whether a tile is a restricted signal tile
287 * @param TileIndex tile
289 * @return red if a restricted signal, nothing otherwise
291 inline SpriteID
TileZoneCheckRoadGridEvaluation(TileIndex tile
, uint grid_size
)
293 const bool x_grid
= (TileX(tile
) % grid_size
== 0);
294 const bool y_grid
= (TileY(tile
) % grid_size
== 0);
295 if (x_grid
|| y_grid
) {
296 return SPR_ZONING_INNER_HIGHLIGHT_LIGHT_BLUE
;
298 return ZONING_INVALID_SPRITE_ID
;
303 * Detect whether a tile is a one way road
305 * @param TileIndex tile
307 * @return red if a restricted signal, nothing otherwise
309 inline SpriteID
TileZoneCheckOneWayRoadEvaluation(TileIndex tile
)
311 if (!MayHaveRoad(tile
)) return ZONING_INVALID_SPRITE_ID
;
313 RoadCachedOneWayState rcows
= GetRoadCachedOneWayState(tile
);
316 return ZONING_INVALID_SPRITE_ID
;
317 case RCOWS_NO_ACCESS
:
318 return SPR_ZONING_INNER_HIGHLIGHT_RED
;
319 case RCOWS_NON_JUNCTION_A
:
320 case RCOWS_NON_JUNCTION_B
:
321 if (IsTileType(tile
, MP_STATION
)) {
322 return SPR_ZONING_INNER_HIGHLIGHT_GREEN
;
323 } else if (IsNormalRoadTile(tile
) && GetDisallowedRoadDirections(tile
) != DRD_NONE
) {
324 return SPR_ZONING_INNER_HIGHLIGHT_LIGHT_BLUE
;
325 } else if (IsRoadBridgeTile(tile
) && GetBridgeDisallowedRoadDirections(tile
) != DRD_NONE
) {
326 return SPR_ZONING_INNER_HIGHLIGHT_LIGHT_BLUE
;
328 return SPR_ZONING_INNER_HIGHLIGHT_PURPLE
;
330 case RCOWS_SIDE_JUNCTION
:
331 return SPR_ZONING_INNER_HIGHLIGHT_ORANGE
;
332 case RCOWS_SIDE_JUNCTION_NO_EXIT
:
333 return SPR_ZONING_INNER_HIGHLIGHT_YELLOW
;
337 inline SpriteID
TileZoneDebugWaterFlood(TileIndex tile
)
339 if (IsNonFloodingWaterTile(tile
)) {
340 return SPR_ZONING_INNER_HIGHLIGHT_YELLOW
;
342 return ZONING_INVALID_SPRITE_ID
;
345 inline SpriteID
TileZoneDebugWaterRegion(TileIndex tile
)
347 extern uint
GetWaterRegionTileDebugColourIndex(TileIndex tile
);
348 uint colour_index
= GetWaterRegionTileDebugColourIndex(tile
);
349 if (colour_index
== 0) {
350 return ZONING_INVALID_SPRITE_ID
;
352 return std::min
<SpriteID
>(SPR_ZONING_INNER_HIGHLIGHT_RED
+ colour_index
- 1, SPR_ZONING_INNER_HIGHLIGHT_YELLOW
);
356 inline SpriteID
TileZoneDebugTropicZone(TileIndex tile
)
358 switch (GetTropicZone(tile
)) {
359 case TROPICZONE_DESERT
:
360 return SPR_ZONING_INNER_HIGHLIGHT_YELLOW
;
361 case TROPICZONE_RAINFOREST
:
362 return SPR_ZONING_INNER_HIGHLIGHT_LIGHT_BLUE
;
364 return ZONING_INVALID_SPRITE_ID
;
368 inline SpriteID
TileZoneDebugAnimatedTile(TileIndex tile
)
370 if (_animated_tiles
.find(tile
) != _animated_tiles
.end()) {
371 return SPR_ZONING_INNER_HIGHLIGHT_YELLOW
;
373 return ZONING_INVALID_SPRITE_ID
;
377 * General evaluation function; calls all the other functions depending on
380 * @param TileIndex tile
381 * Tile to be evaluated.
384 * @param ZoningEvaluationMode ev_mode
385 * The current evaluation mode.
386 * @return The colour returned by the evaluation functions (none if no ev_mode).
388 SpriteID
TileZoningSpriteEvaluation(TileIndex tile
, Owner owner
, ZoningEvaluationMode ev_mode
)
391 case ZEM_CAN_BUILD
: return TileZoneCheckBuildEvaluation(tile
, owner
);
392 case ZEM_AUTHORITY
: return TileZoneCheckOpinionEvaluation(tile
, owner
);
393 case ZEM_STA_CATCH
: return TileZoneCheckStationCatchmentEvaluation(tile
, owner
, false);
394 case ZEM_STA_CATCH_WIN
: return TileZoneCheckStationCatchmentEvaluation(tile
, owner
, true);
395 case ZEM_BUL_UNSER
: return TileZoneCheckUnservedBuildingsEvaluation(tile
, owner
);
396 case ZEM_IND_UNSER
: return TileZoneCheckUnservedIndustriesEvaluation(tile
, owner
);
397 case ZEM_TRACERESTRICT
: return TileZoneCheckTraceRestrictEvaluation(tile
, owner
);
398 case ZEM_2x2_GRID
: return TileZoneCheckRoadGridEvaluation(tile
, 3);
399 case ZEM_3x3_GRID
: return TileZoneCheckRoadGridEvaluation(tile
, 4);
400 case ZEM_ONE_WAY_ROAD
: return TileZoneCheckOneWayRoadEvaluation(tile
);
402 case ZEM_DBG_WATER_FLOOD
: return TileZoneDebugWaterFlood(tile
);
403 case ZEM_DBG_WATER_REGION
: return TileZoneDebugWaterRegion(tile
);
404 case ZEM_DBG_TROPIC_ZONE
: return TileZoneDebugTropicZone(tile
);
405 case ZEM_DBG_ANIMATED_TILE
: return TileZoneDebugAnimatedTile(tile
);
407 default: return ZONING_INVALID_SPRITE_ID
;
411 inline SpriteID
TileZoningSpriteEvaluationCached(TileIndex tile
, Owner owner
, ZoningEvaluationMode ev_mode
, bool is_inner
)
413 if (owner
== COMPANY_SPECTATOR
&& (ev_mode
== ZEM_CAN_BUILD
|| (ev_mode
>= ZEM_STA_CATCH
&& ev_mode
<= ZEM_IND_UNSER
))) return ZONING_INVALID_SPRITE_ID
;
414 if (ev_mode
== ZEM_BUL_UNSER
&& !IsTileType(tile
, MP_HOUSE
)) return ZONING_INVALID_SPRITE_ID
;
415 if (ev_mode
== ZEM_IND_UNSER
&& !IsTileType(tile
, MP_INDUSTRY
)) return ZONING_INVALID_SPRITE_ID
;
416 if (ev_mode
>= ZEM_STA_CATCH
&& ev_mode
<= ZEM_IND_UNSER
) {
418 btree::btree_set
<uint32_t> &cache
= is_inner
? _zoning_cache_inner
: _zoning_cache_outer
;
419 auto iter
= cache
.lower_bound(tile
<< 3);
420 if (iter
!= cache
.end() && *iter
>> 3 == tile
) {
422 case 0: return ZONING_INVALID_SPRITE_ID
;
423 case 1: return SPR_ZONING_INNER_HIGHLIGHT_RED
;
424 case 2: return SPR_ZONING_INNER_HIGHLIGHT_ORANGE
;
425 case 3: return SPR_ZONING_INNER_HIGHLIGHT_BLACK
;
426 case 4: return SPR_ZONING_INNER_HIGHLIGHT_LIGHT_BLUE
;
427 default: NOT_REACHED();
430 SpriteID s
= TileZoningSpriteEvaluation(tile
, owner
, ev_mode
);
431 uint val
= tile
<< 3;
433 case ZONING_INVALID_SPRITE_ID
: val
|= 0; break;
434 case SPR_ZONING_INNER_HIGHLIGHT_RED
: val
|= 1; break;
435 case SPR_ZONING_INNER_HIGHLIGHT_ORANGE
: val
|= 2; break;
436 case SPR_ZONING_INNER_HIGHLIGHT_BLACK
: val
|= 3; break;
437 case SPR_ZONING_INNER_HIGHLIGHT_LIGHT_BLUE
: val
|= 4; break;
438 default: NOT_REACHED();
440 cache
.insert(iter
, val
);
444 return TileZoningSpriteEvaluation(tile
, owner
, ev_mode
);
449 * Draw the the zoning on the tile.
452 * the tile to draw on.
454 void DrawTileZoning(const TileInfo
*ti
)
456 if (IsTileType(ti
->tile
, MP_VOID
) || _game_mode
!= GM_NORMAL
) {
460 if (_zoning
.outer
!= ZEM_NOTHING
) {
461 const SpriteID colour
= TileZoningSpriteEvaluationCached(ti
->tile
, _local_company
, _zoning
.outer
, false);
463 if (colour
!= ZONING_INVALID_SPRITE_ID
) {
464 DrawTileSelectionRect(ti
, colour
);
468 if (_zoning
.inner
!= ZEM_NOTHING
) {
469 const SpriteID colour
= TileZoningSpriteEvaluationCached(ti
->tile
, _local_company
, _zoning
.inner
, true);
471 if (colour
!= ZONING_INVALID_SPRITE_ID
) {
472 SpriteID sprite
= SPR_ZONING_INNER_HIGHLIGHT_BASE
;
474 if (IsHalftileSlope(ti
->tileh
)) {
475 const int INF
= 1000;
476 static const SubSprite sub_sprites
[4] = {
477 { -INF
, -INF
, 32 - 33, INF
}, // CORNER_W, clip 33 pixels from right
478 { -INF
, 0 + 22, INF
, INF
}, // CORNER_S, clip 22 pixels from top
479 { -31 + 34, -INF
, INF
, INF
}, // CORNER_E, clip 34 pixels from left
480 { -INF
, -INF
, INF
, 30 - 8 } // CORNER_N, clip 8 pixels from bottom
483 DrawSelectionSprite(sprite
, colour
, ti
, 7 + TILE_HEIGHT
, FOUNDATION_PART_HALFTILE
, 0, 0, &(sub_sprites
[GetHalftileSlopeCorner(ti
->tileh
)]));
485 sprite
+= SlopeToSpriteOffset(ti
->tileh
);
487 DrawSelectionSprite(sprite
, colour
, ti
, 7, FOUNDATION_PART_NORMAL
);
492 static uint
GetZoningModeDependantStationCoverageRadius(const Station
*st
, ZoningEvaluationMode ev_mode
)
495 case ZEM_STA_CATCH
: return st
->GetCatchmentRadius();
496 case ZEM_STA_CATCH_WIN
: return st
->GetCatchmentRadius();
497 case ZEM_BUL_UNSER
: return st
->GetCatchmentRadius();
498 case ZEM_IND_UNSER
: return st
->GetCatchmentRadius() + 10; // this is to wholly update industries partially within the region
504 * Mark dirty the coverage area around a station if the current zoning mode depends on station coverage
506 * @param const Station *st
509 void ZoningMarkDirtyStationCoverageArea(const Station
*st
, ZoningModeMask mask
)
511 if (st
->rect
.IsEmpty()) return;
513 uint outer_radius
= mask
& ZMM_OUTER
? GetZoningModeDependantStationCoverageRadius(st
, _zoning
.outer
) : 0;
514 uint inner_radius
= mask
& ZMM_INNER
? GetZoningModeDependantStationCoverageRadius(st
, _zoning
.inner
) : 0;
515 uint radius
= std::max
<uint
>(outer_radius
, inner_radius
);
517 extern const Station
*_viewport_highlight_station
;
518 if (_viewport_highlight_station
== st
) {
519 radius
= std::max
<uint
>(radius
, st
->GetCatchmentRadius());
523 Rect rect
= st
->GetCatchmentRectUsingRadius(radius
);
524 for (int y
= rect
.top
; y
<= rect
.bottom
; y
++) {
525 for (int x
= rect
.left
; x
<= rect
.right
; x
++) {
526 MarkTileDirtyByTile(TileXY(x
, y
), VMDF_NOT_MAP_MODE
);
529 auto invalidate_cache_rect
= [&](btree::btree_set
<uint32_t> &cache
) {
530 for (int y
= rect
.top
; y
<= rect
.bottom
; y
++) {
531 auto iter
= cache
.lower_bound(TileXY(rect
.left
, y
) << 3);
532 auto end_iter
= iter
;
533 uint end
= (TileXY(rect
.right
, y
) + 1) << 3;
534 while (end_iter
!= cache
.end() && *end_iter
< end
) ++end_iter
;
535 cache
.erase(iter
, end_iter
);
538 if (outer_radius
) invalidate_cache_rect(_zoning_cache_outer
);
539 if (inner_radius
) invalidate_cache_rect(_zoning_cache_inner
);
543 void ZoningStationWindowOpenClose(const Station
*st
)
545 ZoningModeMask mask
= ZMM_NOTHING
;
546 if (_zoning
.inner
== ZEM_STA_CATCH_WIN
) mask
|= ZMM_INNER
;
547 if (_zoning
.outer
== ZEM_STA_CATCH_WIN
) mask
|= ZMM_OUTER
;
548 if (mask
!= ZMM_NOTHING
) ZoningMarkDirtyStationCoverageArea(st
, mask
);
551 void ZoningTownAuthorityRatingChange()
553 ZoningModeMask mask
= ZMM_NOTHING
;
554 if (_zoning
.inner
== ZEM_AUTHORITY
) mask
|= ZMM_INNER
;
555 if (_zoning
.outer
== ZEM_AUTHORITY
) mask
|= ZMM_OUTER
;
556 if (mask
!= ZMM_NOTHING
) {
557 MarkWholeNonMapViewportsDirty();
561 void ClearZoningCaches()
563 _zoning_cache_inner
.clear();
564 _zoning_cache_outer
.clear();
567 void SetZoningMode(bool inner
, ZoningEvaluationMode mode
)
569 ZoningEvaluationMode
¤t_mode
= inner
? _zoning
.inner
: _zoning
.outer
;
570 btree::btree_set
<uint32_t> &cache
= inner
? _zoning_cache_inner
: _zoning_cache_outer
;
572 if (current_mode
== mode
) return;
576 MarkWholeNonMapViewportsDirty();
577 PostZoningModeChange();
580 void PostZoningModeChange()
582 extern bool _mark_tile_dirty_on_road_cache_one_way_state_update
;
583 _mark_tile_dirty_on_road_cache_one_way_state_update
= (_zoning
.inner
== ZEM_ONE_WAY_ROAD
) || (_zoning
.outer
== ZEM_ONE_WAY_ROAD
);