5 * This file is part of OpenTTD.
6 * 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.
7 * 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.
8 * 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/>.
11 /** @file zoning_cmd.cpp */
15 #include "company_func.h"
20 #include "station_base.h"
21 #include "station_type.h"
24 #include "tracerestrict.h"
25 #include "viewport_func.h"
28 #include "table/sprites.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
> _zoning_cache_inner
;
36 static btree::btree_set
<uint32
> _zoning_cache_outer
;
38 //! Enumeration of multi-part foundations.
41 FOUNDATION_PART_NONE
= 0xFF,
42 //!< Neither foundation nor groundsprite drawn yet.
43 FOUNDATION_PART_NORMAL
= 0,
44 //!< First part (normal foundation or no foundation)
45 FOUNDATION_PART_HALFTILE
= 1,
46 //!< Second part (halftile foundation)
50 void DrawTileSelectionRect(const TileInfo
* ti
, PaletteID pal
);
51 void DrawSelectionSprite(SpriteID image
, PaletteID pal
, const TileInfo
* ti
, int z_offset
, FoundationPart foundation_part
);
53 //! Detect whether this area is within the acceptance of any station.
55 //! @param area the area to search by
56 //! @param owner the owner of the stations which we need to match again
57 //! @param facility_mask one or more facilities in the mask must be present for a station to be used
58 //! @return true if a station is found
59 bool IsAreaWithinAcceptanceZoneOfStation(TileArea area
, Owner owner
, StationFacility facility_mask
)
61 const int catchment
= _settings_game
.station
.station_spread
+ (_settings_game
.station
.modified_catchment
? MAX_CATCHMENT
: CA_UNMODIFIED
);
63 StationFinder
station_finder(TileArea(TileXY(TileX(area
.tile
) - (catchment
/ 2), TileY(area
.tile
) - (catchment
/ 2)),
64 TileX(area
.tile
) + area
.w
+ catchment
, TileY(area
.tile
) + area
.h
+ catchment
));
66 for (auto iter
= station_finder
.GetStations()->Begin(); iter
!= station_finder
.GetStations()->End(); ++iter
) {
67 const Station
* station
= *iter
;
69 if (station
->owner
!= owner
|| !(station
->facilities
& facility_mask
)) continue;
71 const Rect rect
= station
->GetCatchmentRect();
73 return TileArea(TileXY(rect
.left
, rect
.top
), TileXY(rect
.right
, rect
.bottom
)).Intersects(area
);
79 //! Detect whether this tile is within the acceptance of any station.
81 //! @param tile the tile to search by
82 //! @param owner the owner of the stations
83 //! @param facility_mask one or more facilities in the mask must be present for a station to be used
84 //! @return true if a station is found
85 bool IsTileWithinAcceptanceZoneOfStation(TileIndex tile
, Owner owner
, StationFacility facility_mask
)
87 const int catchment
= _settings_game
.station
.station_spread
+ (_settings_game
.station
.modified_catchment
? MAX_CATCHMENT
: CA_UNMODIFIED
);
89 StationFinder
station_finder(TileArea(TileXY(TileX(tile
) - (catchment
/ 2), TileY(tile
) - (catchment
/ 2)),
90 catchment
, catchment
));
92 for (auto iter
= station_finder
.GetStations()->Begin(); iter
!= station_finder
.GetStations()->End(); ++iter
) {
93 const Station
* station
= *iter
;
95 if (station
->owner
!= owner
|| !(station
->facilities
& facility_mask
)) continue;
97 const Rect rect
= station
->GetCatchmentRect();
99 if (uint(rect
.left
) <= TileX(tile
) && TileX(tile
) <= uint(rect
.right
)
100 && uint(rect
.top
) <= TileY(tile
) && TileY(tile
) <= uint(rect
.bottom
)) {
108 //! Check whether the player can build in tile.
110 //! @param tile the tile to check
111 //! @param owner the company to check for
112 //! @return red if they cannot
113 SpriteID
TileZoneCheckBuildEvaluation(TileIndex tile
, Owner owner
)
115 // Let's first check for the obvious things you cannot build on.
116 switch (GetTileType(tile
)) {
121 case MP_TUNNELBRIDGE
:
122 return SPR_ZONING_INNER_HIGHLIGHT_RED
;
124 // There are only two things you can own (or some else
125 // can own) that you can still build on. i.e. roads and
128 // Add something more intelligent, check what tool the
129 // user is currently using (and if none, assume some
130 // standards), then check it against if owned by some-
131 // one else (e.g. railway on someone else's road).
132 // While that being said, it should also check if it
133 // is not possible to build railway/road on someone
134 // else's/your own road/railway (e.g. the railway track
135 // is curved or a cross).
138 if (GetTileOwner(tile
) != owner
) {
139 return SPR_ZONING_INNER_HIGHLIGHT_RED
;
141 return ZONING_INVALID_SPRITE_ID
;
145 return ZONING_INVALID_SPRITE_ID
;
149 //! Check the opinion of the local authority in the tile.
151 //! @param tile the tile to check
152 //! @param owner the company to check for
153 //! @return black if no opinion, orange if bad,
154 //! light blue if good or invalid if no town
155 SpriteID
TileZoneCheckOpinionEvaluation(TileIndex tile
, Owner owner
)
157 int opinion
= 0; // 0: No town, 1: No opinion, 2: Bad, 3: Good
158 Town
* town
= ClosestTownFromTile(tile
, _settings_game
.economy
.dist_local_authority
);
160 if (town
!= nullptr) {
161 if (HasBit(town
->have_ratings
, owner
)) {
162 opinion
= (town
->ratings
[owner
] > 0) ? 3 : 2;
170 return SPR_ZONING_INNER_HIGHLIGHT_BLACK
; // No opinion
172 return SPR_ZONING_INNER_HIGHLIGHT_ORANGE
; // Bad
174 return SPR_ZONING_INNER_HIGHLIGHT_LIGHT_BLUE
; // Good
176 return ZONING_INVALID_SPRITE_ID
; // No town
180 //! Detect whether the tile is within the catchment zone of a station.
182 //! @param tile the tile to check
183 //! @param owner the company owning the stations
184 //! @return black if within, light blue if only in acceptance zone
185 //! and nothing if no nearby station.
186 SpriteID
TileZoneCheckStationCatchmentEvaluation(TileIndex tile
, Owner owner
)
188 // Never on a station.
189 if (IsTileType(tile
, MP_STATION
)) {
190 return ZONING_INVALID_SPRITE_ID
;
193 // For provided goods
194 StationFinder
stations(TileArea(tile
, 1, 1));
196 for (auto iter
= stations
.GetStations()->Begin(); iter
!= stations
.GetStations()->End(); ++iter
) {
197 const Station
* station
= *iter
;
198 if (station
->owner
== owner
) {
199 return SPR_ZONING_INNER_HIGHLIGHT_BLACK
;
203 // For accepted goods
204 if (IsTileWithinAcceptanceZoneOfStation(tile
, owner
, ~FACIL_NONE
)) {
205 return SPR_ZONING_INNER_HIGHLIGHT_LIGHT_BLUE
;
208 return ZONING_INVALID_SPRITE_ID
;
211 //! Detect whether a building is unserved by a station of owner.
213 //! @param tile the tile to check
214 //! @param owner the company to check for
215 //! @return red if unserved, orange if only accepting, nothing if served or not a building
216 SpriteID
TileZoneCheckUnservedBuildingsEvaluation(TileIndex tile
, Owner owner
)
218 if (!IsTileType(tile
, MP_HOUSE
)) {
219 return ZONING_INVALID_SPRITE_ID
;
224 memset(&dat
, 0, sizeof(dat
));
225 AddAcceptedCargo(tile
, dat
, nullptr);
227 if (dat
[CT_MAIL
] + dat
[CT_PASSENGERS
] == 0) {
228 // Nothing is accepted, so now test if cargo is produced
229 AddProducedCargo(tile
, dat
);
231 if (dat
[CT_MAIL
] + dat
[CT_PASSENGERS
] == 0) {
232 // Total is still 0, so give up
233 return ZONING_INVALID_SPRITE_ID
;
237 StationFinder
stations(TileArea(tile
, 1, 1));
239 for (auto iter
= stations
.GetStations()->Begin(); iter
!= stations
.GetStations()->End(); ++iter
) {
240 const Station
* station
= *iter
;
241 if (station
->owner
== owner
) {
242 return ZONING_INVALID_SPRITE_ID
;
246 // For accepted goods
247 if (IsTileWithinAcceptanceZoneOfStation(tile
, owner
, ~FACIL_NONE
)) {
248 return SPR_ZONING_INNER_HIGHLIGHT_ORANGE
;
251 return SPR_ZONING_INNER_HIGHLIGHT_RED
;
254 //! Detect whether an industry is unserved by a station of owner.
256 //! @param tile the tile to check
257 //! @param owner the company to check for
258 //! @return red if unserved, orange if only accepting, nothing if served or not a building
259 SpriteID
TileZoneCheckUnservedIndustriesEvaluation(TileIndex tile
, Owner owner
)
261 if (IsTileType(tile
, MP_INDUSTRY
)) {
262 Industry
* industry
= Industry::GetByTile(tile
);
263 StationFinder
stations(industry
->location
);
265 for (auto iter
= stations
.GetStations()->Begin(); iter
!= stations
.GetStations()->End(); ++iter
) {
266 const Station
* station
= *iter
;
267 if (station
->owner
== owner
&& station
->facilities
& (~FACIL_BUS_STOP
)) {
268 return ZONING_INVALID_SPRITE_ID
;
272 // For accepted goods
273 if (IsAreaWithinAcceptanceZoneOfStation(industry
->location
, owner
, ~FACIL_BUS_STOP
)) {
274 return SPR_ZONING_INNER_HIGHLIGHT_ORANGE
;
277 return SPR_ZONING_INNER_HIGHLIGHT_RED
;
280 return ZONING_INVALID_SPRITE_ID
;
283 //! Detect whether a tile is a restricted signal tile
285 //! @param tile the tile to check
286 //! @param owner the company to check for
287 //! @return red if a restricted signal, nothing otherwise
288 SpriteID
TileZoneCheckTraceRestrictEvaluation(TileIndex tile
, Owner owner
)
290 if (IsTileType(tile
, MP_RAILWAY
) && HasSignals(tile
) && IsRestrictedSignal(tile
)) {
291 return SPR_ZONING_INNER_HIGHLIGHT_RED
;
294 return ZONING_INVALID_SPRITE_ID
;
297 //! General evaluation function; calls all the other functions depending on
300 //! @param tile Tile to be evaluated.
301 //! @param owner The current player
302 //! @param evaluation_mode The current evaluation mode.
303 //! @return The colour returned by the evaluation functions (none if no evaluation_mode).
304 SpriteID
TileZoningSpriteEvaluation(TileIndex tile
, Owner owner
, ZoningEvaluationMode evaluation_mode
)
306 switch (evaluation_mode
) {
308 return TileZoneCheckBuildEvaluation(tile
, owner
);
310 return TileZoneCheckOpinionEvaluation(tile
, owner
);
312 return TileZoneCheckStationCatchmentEvaluation(tile
, owner
);
314 return TileZoneCheckUnservedBuildingsEvaluation(tile
, owner
);
316 return TileZoneCheckUnservedIndustriesEvaluation(tile
, owner
);
317 case ZEM_TRACERESTRICT
:
318 return TileZoneCheckTraceRestrictEvaluation(tile
, owner
);
320 return ZONING_INVALID_SPRITE_ID
;
324 inline SpriteID
TileZoningSpriteEvaluationCached(TileIndex tile
, Owner owner
, ZoningEvaluationMode ev_mode
, bool is_inner
)
326 if (ev_mode
== ZEM_BUL_UNSER
&& !IsTileType(tile
, MP_HOUSE
)) return ZONING_INVALID_SPRITE_ID
;
327 if (ev_mode
== ZEM_IND_UNSER
&& !IsTileType(tile
, MP_INDUSTRY
)) return ZONING_INVALID_SPRITE_ID
;
329 if (ev_mode
>= ZEM_STA_CATCH
&& ev_mode
<= ZEM_IND_UNSER
) {
331 btree::btree_set
<uint32
>& cache
= is_inner
? _zoning_cache_inner
: _zoning_cache_outer
;
332 const auto lower_bound
= cache
.lower_bound(tile
<< 3);
334 if (lower_bound
!= cache
.end() && *lower_bound
>> 3 == tile
) {
335 switch (*lower_bound
& 7) {
337 return ZONING_INVALID_SPRITE_ID
;
339 return SPR_ZONING_INNER_HIGHLIGHT_RED
;
341 return SPR_ZONING_INNER_HIGHLIGHT_ORANGE
;
343 return SPR_ZONING_INNER_HIGHLIGHT_BLACK
;
345 return SPR_ZONING_INNER_HIGHLIGHT_LIGHT_BLUE
;
346 default: NOT_REACHED();
350 const SpriteID sprite
= TileZoningSpriteEvaluation(tile
, owner
, ev_mode
);
351 uint val
= tile
<< 3;
354 case ZONING_INVALID_SPRITE_ID
:
357 case SPR_ZONING_INNER_HIGHLIGHT_RED
:
360 case SPR_ZONING_INNER_HIGHLIGHT_ORANGE
:
363 case SPR_ZONING_INNER_HIGHLIGHT_BLACK
:
366 case SPR_ZONING_INNER_HIGHLIGHT_LIGHT_BLUE
:
369 default: NOT_REACHED();
372 cache
.insert(lower_bound
, val
);
377 return TileZoningSpriteEvaluation(tile
, owner
, ev_mode
);
380 //! Draw the the zoning on the tile.
382 //! @param tile_info the tile to draw on.
383 void DrawTileZoning(const TileInfo
* tile_info
)
385 if (IsTileType(tile_info
->tile
, MP_VOID
) || _game_mode
!= GM_NORMAL
) {
389 if (_zoning
.outer
!= ZEM_NOTHING
) {
390 const auto colour
= TileZoningSpriteEvaluationCached(tile_info
->tile
, _local_company
, _zoning
.outer
, false);
392 if (colour
!= ZONING_INVALID_SPRITE_ID
) {
393 DrawTileSelectionRect(tile_info
, colour
);
397 if (_zoning
.inner
!= ZEM_NOTHING
) {
398 const auto colour
= TileZoningSpriteEvaluationCached(tile_info
->tile
, _local_company
, _zoning
.inner
, true);
400 if (colour
!= ZONING_INVALID_SPRITE_ID
) {
401 auto sprite
= SPR_ZONING_INNER_HIGHLIGHT_BASE
;
403 if (IsHalftileSlope(tile_info
->tileh
)) {
404 DrawSelectionSprite(sprite
, colour
, tile_info
, 7 + TILE_HEIGHT
, FOUNDATION_PART_HALFTILE
);
406 sprite
+= SlopeToSpriteOffset(tile_info
->tileh
);
409 DrawSelectionSprite(sprite
, colour
, tile_info
, 7, FOUNDATION_PART_NORMAL
);
414 static uint
GetZoningModeDependantStationCoverageRadius(const Station
* station
, ZoningEvaluationMode evaluation_mode
)
416 switch (evaluation_mode
) {
418 return station
->GetCatchmentRadius();
420 return station
->GetCatchmentRadius();
422 // This is to wholly update industries partially within the region
423 return station
->GetCatchmentRadius() + 10;
429 //! Mark dirty the coverage area around a station if the current zoning mode depends on station coverage
431 //! @param station The station to use
432 //! @param mask The zoning mode mask
433 void ZoningMarkDirtyStationCoverageArea(const Station
* station
, ZoningModeMask mask
)
435 if (station
->rect
.IsEmpty()) return;
437 const uint outer_radius
= mask
& ZMM_OUTER
? GetZoningModeDependantStationCoverageRadius(station
, _zoning
.outer
) : 0;
438 const uint inner_radius
= mask
& ZMM_INNER
? GetZoningModeDependantStationCoverageRadius(station
, _zoning
.inner
) : 0;
439 const uint radius
= max
<uint
>(outer_radius
, inner_radius
);
442 Rect rect
= station
->GetCatchmentRectUsingRadius(radius
);
443 for (int y
= rect
.top
; y
<= rect
.bottom
; y
++) {
444 for (int x
= rect
.left
; x
<= rect
.right
; x
++) {
445 MarkTileDirtyByTile(TileXY(x
, y
));
448 const auto invalidate_cache_rect
= [&](btree::btree_set
<uint32
>& cache
) {
449 for (int y
= rect
.top
; y
<= rect
.bottom
; y
++) {
450 const auto lower_bound
= cache
.lower_bound(TileXY(rect
.left
, y
) << 3);
451 auto end_iter
= lower_bound
;
452 const uint end
= TileXY(rect
.right
, y
) << 3;
453 while (end_iter
!= cache
.end() && *end_iter
< end
) ++end_iter
;
454 cache
.erase(lower_bound
, end_iter
);
458 if (outer_radius
) invalidate_cache_rect(_zoning_cache_outer
);
459 if (inner_radius
) invalidate_cache_rect(_zoning_cache_inner
);
463 void ClearZoningCaches()
465 _zoning_cache_inner
.clear();
466 _zoning_cache_outer
.clear();
469 void SetZoningMode(bool inner
, ZoningEvaluationMode mode
)
471 ZoningEvaluationMode
& current_mode
= inner
? _zoning
.inner
: _zoning
.outer
;
472 btree::btree_set
<uint32
>& cache
= inner
? _zoning_cache_inner
: _zoning_cache_outer
;
474 if (current_mode
== mode
) return;
478 MarkWholeScreenDirty();