Improve town zones outer highlight speed and visuals
This commit is contained in:
BIN
bin/data/cmclient-1.grf
Normal file
BIN
bin/data/cmclient-1.grf
Normal file
Binary file not shown.
@@ -614,6 +614,8 @@ zoning_cmd.cpp
|
|||||||
bitstream.cpp
|
bitstream.cpp
|
||||||
citymania/base64.h
|
citymania/base64.h
|
||||||
citymania/base64.cpp
|
citymania/base64.cpp
|
||||||
|
citymania/zoning.hpp
|
||||||
|
citymania/zoning.cpp
|
||||||
newgrf_revisions.hpp
|
newgrf_revisions.hpp
|
||||||
|
|
||||||
# Save/Load handlers
|
# Save/Load handlers
|
||||||
|
|||||||
124
src/citymania/zoning.cpp
Normal file
124
src/citymania/zoning.cpp
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
#include "../stdafx.h"
|
||||||
|
|
||||||
|
#include "zoning.hpp"
|
||||||
|
|
||||||
|
#include "../house.h"
|
||||||
|
#include "../core/math_func.hpp"
|
||||||
|
#include "../town.h"
|
||||||
|
#include "../tilearea_type.h"
|
||||||
|
#include "../viewport_func.h"
|
||||||
|
#include "../zoning.h"
|
||||||
|
|
||||||
|
namespace citymania {
|
||||||
|
|
||||||
|
struct TileZoning {
|
||||||
|
uint8 town_zone : 3;
|
||||||
|
};
|
||||||
|
|
||||||
|
TileZoning *_mz = nullptr;
|
||||||
|
|
||||||
|
void AllocateZoningMap(uint map_size) {
|
||||||
|
free(_mz);
|
||||||
|
_mz = CallocT<TileZoning>(map_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8 GetTownZone(Town *town, TileIndex tile) {
|
||||||
|
auto dist = DistanceSquare(tile, town->xy);
|
||||||
|
if (dist > town->cache.squared_town_zone_radius[HZB_TOWN_EDGE])
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
uint8 z = 1;
|
||||||
|
for (HouseZonesBits i = HZB_TOWN_OUTSKIRT; i < HZB_END; i++)
|
||||||
|
if (dist < town->cache.squared_town_zone_radius[i])
|
||||||
|
z = (uint8)i + 1;
|
||||||
|
else if (town->cache.squared_town_zone_radius[i] != 0)
|
||||||
|
break;
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8 GetAnyTownZone(TileIndex tile) {
|
||||||
|
HouseZonesBits next_zone = HZB_BEGIN;
|
||||||
|
uint8 z = 0;
|
||||||
|
|
||||||
|
Town *town;
|
||||||
|
FOR_ALL_TOWNS(town) {
|
||||||
|
uint dist = DistanceSquare(tile, town->xy);
|
||||||
|
// town code uses <= for checking town borders (tz0) but < for other zones
|
||||||
|
while (next_zone < HZB_END
|
||||||
|
&& (town->cache.squared_town_zone_radius[next_zone] == 0
|
||||||
|
|| dist <= town->cache.squared_town_zone_radius[next_zone] - (next_zone == HZB_BEGIN ? 0 : 1))
|
||||||
|
) {
|
||||||
|
if (town->cache.squared_town_zone_radius[next_zone] != 0) z = (uint8)next_zone + 1;
|
||||||
|
next_zone++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateTownZoning(Town *town, uint32 prev_edge) {
|
||||||
|
auto edge = town->cache.squared_town_zone_radius[HZB_TOWN_EDGE];
|
||||||
|
if (prev_edge && edge == prev_edge)
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto area = OrthogonalTileArea(town->xy, 1, 1);
|
||||||
|
bool recalc;
|
||||||
|
if (edge < prev_edge) {
|
||||||
|
area.Expand(IntSqrt(prev_edge));
|
||||||
|
recalc = true;
|
||||||
|
} else {
|
||||||
|
area.Expand(IntSqrt(edge));
|
||||||
|
recalc = false;
|
||||||
|
}
|
||||||
|
// TODO mark dirty only if zoning is on
|
||||||
|
TILE_AREA_LOOP(tile, area) {
|
||||||
|
uint8 group = GetTownZone(town, tile);
|
||||||
|
if (_mz[tile].town_zone > group) {
|
||||||
|
if (recalc) {
|
||||||
|
_mz[tile].town_zone = GetAnyTownZone(tile);
|
||||||
|
if (_zoning.outer == CHECKTOWNZONES)
|
||||||
|
MarkTileDirtyByTile(tile);
|
||||||
|
}
|
||||||
|
} else if (_mz[tile].town_zone < group) {
|
||||||
|
_mz[tile].town_zone = group;
|
||||||
|
if (_zoning.outer == CHECKTOWNZONES)
|
||||||
|
MarkTileDirtyByTile(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InitializeZoningMap() {
|
||||||
|
Town *town;
|
||||||
|
FOR_ALL_TOWNS(town) {
|
||||||
|
UpdateTownZoning(town, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename F>
|
||||||
|
uint8 Get(uint32 x, uint32 y, F getter) {
|
||||||
|
if (x >= MapSizeX() || y >= MapSizeY()) return 0;
|
||||||
|
return getter(_mz[TileXY(x, y)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<ZoningBorder, uint8> GetTownZoneBorder(TileIndex tile) {
|
||||||
|
auto x = TileX(tile), y = TileY(tile);
|
||||||
|
ZoningBorder res = ZoningBorder::NONE;
|
||||||
|
auto z = _mz[tile].town_zone;
|
||||||
|
if (z == 0)
|
||||||
|
return std::make_pair(res, 0);
|
||||||
|
auto getter = [](TileZoning tz) { return tz.town_zone; };
|
||||||
|
auto tr = Get(x - 1, y, getter);
|
||||||
|
auto tl = Get(x, y - 1, getter);
|
||||||
|
auto bl = Get(x + 1, y, getter);
|
||||||
|
auto br = Get(x, y + 1, getter);
|
||||||
|
if (tr < z) res |= ZoningBorder::TOP_RIGHT;
|
||||||
|
if (tl < z) res |= ZoningBorder::TOP_LEFT;
|
||||||
|
if (bl < z) res |= ZoningBorder::BOTTOM_LEFT;
|
||||||
|
if (br < z) res |= ZoningBorder::BOTTOM_RIGHT;
|
||||||
|
if (tr == z && tl == z && Get(x - 1, y - 1, getter) < z) res |= ZoningBorder::TOP_CORNER;
|
||||||
|
if (tr == z && br == z && Get(x - 1, y + 1, getter) < z) res |= ZoningBorder::RIGHT_CORNER;
|
||||||
|
if (br == z && bl == z && Get(x + 1, y + 1, getter) < z) res |= ZoningBorder::BOTTOM_CORNER;
|
||||||
|
if (tl == z && bl == z && Get(x + 1, y - 1, getter) < z) res |= ZoningBorder::LEFT_CORNER;
|
||||||
|
return std::make_pair(res, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace citymania
|
||||||
43
src/citymania/zoning.hpp
Normal file
43
src/citymania/zoning.hpp
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#ifndef CITYMANIA_ZONING_HPP
|
||||||
|
#define CITYMANIA_ZONING_HPP
|
||||||
|
|
||||||
|
#include "../core/enum_type.hpp"
|
||||||
|
#include "../tile_type.h"
|
||||||
|
#include "../town_type.h"
|
||||||
|
|
||||||
|
namespace citymania {
|
||||||
|
|
||||||
|
////enum class ZoningBorder : unt8 {
|
||||||
|
enum ZoningBorder: uint8 {
|
||||||
|
NONE = 0,
|
||||||
|
TOP_LEFT = 1,
|
||||||
|
TOP_RIGHT = 2,
|
||||||
|
BOTTOM_RIGHT = 4,
|
||||||
|
BOTTOM_LEFT = 8,
|
||||||
|
TOP_CORNER = 16,
|
||||||
|
RIGHT_CORNER = 32,
|
||||||
|
BOTTOM_CORNER = 64,
|
||||||
|
LEFT_CORNER = 128,
|
||||||
|
};
|
||||||
|
|
||||||
|
DECLARE_ENUM_AS_BIT_SET(ZoningBorder);
|
||||||
|
|
||||||
|
// enum class AdvertisementZone: uint8 {
|
||||||
|
// NONE = 0,
|
||||||
|
// LARGE = 1,
|
||||||
|
// MEDIUM = 2,
|
||||||
|
// SMALL = 3,
|
||||||
|
// };
|
||||||
|
|
||||||
|
|
||||||
|
void AllocateZoningMap(uint map_size);
|
||||||
|
void InitializeZoningMap();
|
||||||
|
|
||||||
|
void UpdateTownZoning(Town *town, uint32 prev_edge);
|
||||||
|
|
||||||
|
std::pair<ZoningBorder, uint8> GetTownZoneBorder(TileIndex tile);
|
||||||
|
std::pair<ZoningBorder, uint8> GetTownAdvertisementBorder(TileIndex tile);
|
||||||
|
|
||||||
|
} // namespace citymania
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -192,7 +192,8 @@ static void LoadSpriteTables()
|
|||||||
i++
|
i++
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
LoadGrfFile("innerhighlight.grf", SPR_INNER_HIGHLIGHT_BASE, i++);
|
// LoadGrfFile("innerhighlight.grf", SPR_INNER_HIGHLIGHT_BASE, i++);
|
||||||
|
LoadGrfFile("cmclient-1.grf", SPR_INNER_HIGHLIGHT_BASE - 4, i++);
|
||||||
|
|
||||||
/* Initialize the unicode to sprite mapping table */
|
/* Initialize the unicode to sprite mapping table */
|
||||||
InitializeUnicodeGlyphMap();
|
InitializeUnicodeGlyphMap();
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "core/alloc_func.hpp"
|
#include "core/alloc_func.hpp"
|
||||||
#include "water_map.h"
|
#include "water_map.h"
|
||||||
#include "string_func.h"
|
#include "string_func.h"
|
||||||
|
#include "citymania/zoning.hpp"
|
||||||
|
|
||||||
#include "safeguards.h"
|
#include "safeguards.h"
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ void AllocateMap(uint size_x, uint size_y)
|
|||||||
|
|
||||||
_m = CallocT<Tile>(_map_size);
|
_m = CallocT<Tile>(_map_size);
|
||||||
_me = CallocT<TileExtended>(_map_size);
|
_me = CallocT<TileExtended>(_map_size);
|
||||||
|
citymania::AllocateZoningMap(_map_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,8 @@
|
|||||||
|
|
||||||
#include "linkgraph/linkgraphschedule.h"
|
#include "linkgraph/linkgraphschedule.h"
|
||||||
|
|
||||||
|
#include "citymania/zoning.hpp"
|
||||||
|
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
#include <system_error>
|
#include <system_error>
|
||||||
|
|
||||||
@@ -962,6 +964,7 @@ static void MakeNewGameDone()
|
|||||||
|
|
||||||
CheckEngines();
|
CheckEngines();
|
||||||
CheckIndustries();
|
CheckIndustries();
|
||||||
|
citymania::InitializeZoningMap();
|
||||||
MarkWholeScreenDirty();
|
MarkWholeScreenDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -978,6 +981,7 @@ static void MakeNewGame(bool from_heightmap, bool reset_settings)
|
|||||||
static void MakeNewEditorWorldDone()
|
static void MakeNewEditorWorldDone()
|
||||||
{
|
{
|
||||||
SetLocalCompany(OWNER_NONE);
|
SetLocalCompany(OWNER_NONE);
|
||||||
|
citymania::InitializeZoningMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void MakeNewEditorWorld()
|
static void MakeNewEditorWorld()
|
||||||
|
|||||||
@@ -63,6 +63,8 @@
|
|||||||
#include "../game/game_text.hpp"
|
#include "../game/game_text.hpp"
|
||||||
#include "../table/control_codes.h"
|
#include "../table/control_codes.h"
|
||||||
|
|
||||||
|
#include "../citymania/zoning.hpp"
|
||||||
|
|
||||||
#include "saveload_internal.h"
|
#include "saveload_internal.h"
|
||||||
|
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
@@ -3267,6 +3269,7 @@ bool AfterLoadGame()
|
|||||||
|
|
||||||
AfterLoadLinkGraphs();
|
AfterLoadLinkGraphs();
|
||||||
AfterLoadFindBTProCBInfo();
|
AfterLoadFindBTProCBInfo();
|
||||||
|
citymania::InitializeZoningMap();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -317,8 +317,11 @@ static const SpriteID SPR_PALETTE_ZONING_YELLOW = SPR_INNER_HIGHLIGHT_BASE +
|
|||||||
static const SpriteID SPR_PALETTE_ZONING_PURPLE = SPR_INNER_HIGHLIGHT_BASE + 28;
|
static const SpriteID SPR_PALETTE_ZONING_PURPLE = SPR_INNER_HIGHLIGHT_BASE + 28;
|
||||||
static const SpriteID SPR_INNER_HIGHLIGHT_COUNT = 29;
|
static const SpriteID SPR_INNER_HIGHLIGHT_COUNT = 29;
|
||||||
|
|
||||||
|
static const SpriteID SPR_BORDER_HIGHLIGHT_BASE = SPR_INNER_HIGHLIGHT_BASE + SPR_INNER_HIGHLIGHT_COUNT + 1;
|
||||||
|
static const SpriteID SPR_BORDER_HIGHLIGHT_COUNT = 19 * 19;
|
||||||
|
|
||||||
/* From where can we start putting NewGRFs? */
|
/* From where can we start putting NewGRFs? */
|
||||||
static const SpriteID SPR_NEWGRFS_BASE = SPR_INNER_HIGHLIGHT_BASE + SPR_INNER_HIGHLIGHT_COUNT;
|
static const SpriteID SPR_NEWGRFS_BASE = SPR_BORDER_HIGHLIGHT_BASE + SPR_BORDER_HIGHLIGHT_COUNT;
|
||||||
|
|
||||||
/* Manager face sprites */
|
/* Manager face sprites */
|
||||||
static const SpriteID SPR_GRADIENT = 874; // background gradient behind manager face
|
static const SpriteID SPR_GRADIENT = 874; // background gradient behind manager face
|
||||||
|
|||||||
@@ -54,6 +54,8 @@
|
|||||||
#include "table/strings.h"
|
#include "table/strings.h"
|
||||||
#include "table/town_land.h"
|
#include "table/town_land.h"
|
||||||
|
|
||||||
|
#include "citymania/zoning.hpp"
|
||||||
|
|
||||||
#include "safeguards.h"
|
#include "safeguards.h"
|
||||||
|
|
||||||
bool _cb_enabled = false;
|
bool _cb_enabled = false;
|
||||||
@@ -1816,6 +1818,7 @@ static bool GrowTown(Town *t)
|
|||||||
|
|
||||||
void UpdateTownRadius(Town *t)
|
void UpdateTownRadius(Town *t)
|
||||||
{
|
{
|
||||||
|
auto prev_tz0 = t->cache.squared_town_zone_radius[0];
|
||||||
static const uint32 _town_squared_town_zone_radius_data[23][5] = {
|
static const uint32 _town_squared_town_zone_radius_data[23][5] = {
|
||||||
{ 4, 0, 0, 0, 0}, // 0
|
{ 4, 0, 0, 0, 0}, // 0
|
||||||
{ 16, 0, 0, 0, 0},
|
{ 16, 0, 0, 0, 0},
|
||||||
@@ -1855,6 +1858,8 @@ void UpdateTownRadius(Town *t)
|
|||||||
t->cache.squared_town_zone_radius[3] = mass * 5 - 5;
|
t->cache.squared_town_zone_radius[3] = mass * 5 - 5;
|
||||||
t->cache.squared_town_zone_radius[4] = mass * 3 + 5;
|
t->cache.squared_town_zone_radius[4] = mass * 3 + 5;
|
||||||
}
|
}
|
||||||
|
if (!_generating_world)
|
||||||
|
citymania::UpdateTownZoning(t, prev_tz0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UpdateTownMaxPass(Town *t)
|
void UpdateTownMaxPass(Town *t)
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "citymania/zoning.hpp"
|
||||||
|
|
||||||
|
|
||||||
Zoning _zoning = {CHECKNOTHING, CHECKNOTHING};
|
Zoning _zoning = {CHECKNOTHING, CHECKNOTHING};
|
||||||
static const SpriteID INVALID_SPRITE_ID = UINT_MAX;
|
static const SpriteID INVALID_SPRITE_ID = UINT_MAX;
|
||||||
//RED GREEN BLACK LIGHT_BLUE ORANGE WHITE YELLOW PURPLE
|
//RED GREEN BLACK LIGHT_BLUE ORANGE WHITE YELLOW PURPLE
|
||||||
@@ -268,10 +271,11 @@ SpriteID TileZoneCheckTownZones(TileIndex tile) {
|
|||||||
|
|
||||||
Town *town;
|
Town *town;
|
||||||
FOR_ALL_TOWNS(town) {
|
FOR_ALL_TOWNS(town) {
|
||||||
|
uint dist = DistanceSquare(tile, town->xy);
|
||||||
// town code uses <= for checking town borders (tz0) but < for other zones
|
// town code uses <= for checking town borders (tz0) but < for other zones
|
||||||
while (next_zone < HZB_END
|
while (next_zone < HZB_END
|
||||||
&& (town->cache.squared_town_zone_radius[next_zone] == 0
|
&& (town->cache.squared_town_zone_radius[next_zone] == 0
|
||||||
|| DistanceSquare(tile, town->xy) <= town->cache.squared_town_zone_radius[next_zone] + (next_zone == HZB_BEGIN ? 0 : 1))
|
|| dist <= town->cache.squared_town_zone_radius[next_zone] - (next_zone == HZB_BEGIN ? 0 : 1))
|
||||||
){
|
){
|
||||||
if(town->cache.squared_town_zone_radius[next_zone] != 0) tz = next_zone;
|
if(town->cache.squared_town_zone_radius[next_zone] != 0) tz = next_zone;
|
||||||
next_zone++;
|
next_zone++;
|
||||||
@@ -397,6 +401,38 @@ SpriteID TileZoningSpriteEvaluation(TileIndex tile, Owner owner, EvaluationMode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SpriteID GetTownZoneBorderColor(uint8 zone) {
|
||||||
|
switch (zone) {
|
||||||
|
default: return SPR_PALETTE_ZONING_LIGHT_BLUE; // Tz0
|
||||||
|
case 2: return SPR_PALETTE_ZONING_RED; // Tz1
|
||||||
|
case 3: return SPR_PALETTE_ZONING_YELLOW; // Tz2
|
||||||
|
case 4: return SPR_PALETTE_ZONING_GREEN; // Tz3
|
||||||
|
case 5: return SPR_PALETTE_ZONING_WHITE; // Tz4 - center
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawBorderSprites(const TileInfo *ti, citymania::ZoningBorder border, SpriteID color) {
|
||||||
|
auto b = (uint8)border & 15;
|
||||||
|
auto tile_sprite = SPR_BORDER_HIGHLIGHT_BASE + _tileh_to_sprite[ti->tileh] * 19;
|
||||||
|
if (b) {
|
||||||
|
AddSortableSpriteToDraw(tile_sprite + b - 1,
|
||||||
|
color, ti->x, ti->y, 0x10, 0x10, 1, ti->z + 7);
|
||||||
|
}
|
||||||
|
if (border & citymania::ZoningBorder::TOP_CORNER)
|
||||||
|
AddSortableSpriteToDraw(tile_sprite + 15,
|
||||||
|
color, ti->x, ti->y, 0x10, 0x10, 1, ti->z + 7);
|
||||||
|
if (border & citymania::ZoningBorder::RIGHT_CORNER)
|
||||||
|
AddSortableSpriteToDraw(tile_sprite + 16,
|
||||||
|
color, ti->x, ti->y, 0x10, 0x10, 1, ti->z + 7);
|
||||||
|
if (border & citymania::ZoningBorder::BOTTOM_CORNER)
|
||||||
|
AddSortableSpriteToDraw(tile_sprite + 17,
|
||||||
|
color, ti->x, ti->y, 0x10, 0x10, 1, ti->z + 7);
|
||||||
|
if (border & citymania::ZoningBorder::LEFT_CORNER)
|
||||||
|
AddSortableSpriteToDraw(tile_sprite + 18,
|
||||||
|
color, ti->x, ti->y, 0x10, 0x10, 1, ti->z + 7);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draw the the zoning on the tile.
|
* Draw the the zoning on the tile.
|
||||||
* @param TileInfo ti
|
* @param TileInfo ti
|
||||||
@@ -406,7 +442,14 @@ void DrawTileZoning(const TileInfo *ti) {
|
|||||||
if(_zoning.outer == CHECKNOTHING && _zoning.inner == CHECKNOTHING) return; //nothing to do
|
if(_zoning.outer == CHECKNOTHING && _zoning.inner == CHECKNOTHING) return; //nothing to do
|
||||||
if (_game_mode != GM_NORMAL || ti->tile >= MapSize() || IsTileType(ti->tile, MP_VOID)) return; //check invalid
|
if (_game_mode != GM_NORMAL || ti->tile >= MapSize() || IsTileType(ti->tile, MP_VOID)) return; //check invalid
|
||||||
if (_zoning.outer != CHECKNOTHING){
|
if (_zoning.outer != CHECKNOTHING){
|
||||||
DrawZoningSprites(SPR_SELECT_TILE, TileZoningSpriteEvaluation(ti->tile, _local_company, _zoning.outer), ti);
|
if (_zoning.outer == CHECKTOWNZONES) {
|
||||||
|
auto p = citymania::GetTownZoneBorder(ti->tile);
|
||||||
|
if (p.first && p.second) {
|
||||||
|
DrawBorderSprites(ti, p.first, GetTownZoneBorderColor(p.second));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DrawZoningSprites(SPR_SELECT_TILE, TileZoningSpriteEvaluation(ti->tile, _local_company, _zoning.outer), ti);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (_zoning.inner != CHECKNOTHING){
|
if (_zoning.inner != CHECKNOTHING){
|
||||||
DrawZoningSprites(SPR_INNER_HIGHLIGHT_BASE, TileZoningSpriteEvaluation(ti->tile, _local_company, _zoning.inner), ti);
|
DrawZoningSprites(SPR_INNER_HIGHLIGHT_BASE, TileZoningSpriteEvaluation(ti->tile, _local_company, _zoning.inner), ti);
|
||||||
|
|||||||
Reference in New Issue
Block a user