mirror of
https://github.com/OpenRCT2/OpenRCT2
synced 2026-01-22 14:24:33 +01:00
Merge pull request #10119 from duncanspumpkin/maze_place_ga
Maze Place Track Game Action
This commit is contained in:
@@ -991,77 +991,15 @@ void game_load_or_quit_no_save_prompt()
|
||||
}
|
||||
|
||||
GAME_COMMAND_POINTER* new_game_command_table[GAME_COMMAND_COUNT] = {
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
game_command_place_track_design,
|
||||
nullptr,
|
||||
game_command_place_maze_design,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
NULL,
|
||||
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, game_command_place_track_design,
|
||||
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
|
||||
nullptr, nullptr, NULL,
|
||||
};
|
||||
|
||||
@@ -68,7 +68,7 @@ enum GAME_COMMAND
|
||||
GAME_COMMAND_SET_RESEARCH_FUNDING, // GA
|
||||
GAME_COMMAND_PLACE_TRACK_DESIGN,
|
||||
GAME_COMMAND_START_MARKETING_CAMPAIGN, // GA
|
||||
GAME_COMMAND_PLACE_MAZE_DESIGN,
|
||||
GAME_COMMAND_PLACE_MAZE_DESIGN, // GA
|
||||
GAME_COMMAND_PLACE_BANNER, // GA
|
||||
GAME_COMMAND_REMOVE_BANNER, // GA
|
||||
GAME_COMMAND_SET_SCENERY_COLOUR, // GA
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "LargeSceneryRemoveAction.hpp"
|
||||
#include "LargeScenerySetColourAction.hpp"
|
||||
#include "LoadOrQuitAction.hpp"
|
||||
#include "MazePlaceTrackAction.hpp"
|
||||
#include "MazeSetTrackAction.hpp"
|
||||
#include "NetworkModifyGroupAction.hpp"
|
||||
#include "ParkEntranceRemoveAction.hpp"
|
||||
@@ -103,6 +104,7 @@ namespace GameActions
|
||||
Register<FootpathSceneryPlaceAction>();
|
||||
Register<FootpathSceneryRemoveAction>();
|
||||
Register<GuestSetNameAction>();
|
||||
Register<MazePlaceTrackAction>();
|
||||
Register<MazeSetTrackAction>();
|
||||
Register<NetworkModifyGroupAction>();
|
||||
Register<ParkMarketingAction>();
|
||||
|
||||
207
src/openrct2/actions/MazePlaceTrackAction.hpp
Normal file
207
src/openrct2/actions/MazePlaceTrackAction.hpp
Normal file
@@ -0,0 +1,207 @@
|
||||
/*****************************************************************************
|
||||
* Copyright (c) 2014-2019 OpenRCT2 developers
|
||||
*
|
||||
* For a complete list of all authors, please refer to contributors.md
|
||||
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
||||
*
|
||||
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
||||
*****************************************************************************/
|
||||
#pragma once
|
||||
|
||||
#include "../management/Finance.h"
|
||||
#include "../ride/RideData.h"
|
||||
#include "../ride/TrackData.h"
|
||||
#include "GameAction.h"
|
||||
|
||||
DEFINE_GAME_ACTION(MazePlaceTrackAction, GAME_COMMAND_PLACE_MAZE_DESIGN, GameActionResult)
|
||||
{
|
||||
private:
|
||||
CoordsXYZ _loc;
|
||||
NetworkRideId_t _rideIndex{ RIDE_ID_NULL };
|
||||
uint16_t _mazeEntry{ 0 };
|
||||
|
||||
public:
|
||||
MazePlaceTrackAction() = default;
|
||||
|
||||
MazePlaceTrackAction(CoordsXYZ location, NetworkRideId_t rideIndex, uint16_t mazeEntry)
|
||||
: _loc(location)
|
||||
, _rideIndex(rideIndex)
|
||||
, _mazeEntry(mazeEntry)
|
||||
{
|
||||
}
|
||||
|
||||
void Serialise(DataSerialiser & stream) override
|
||||
{
|
||||
GameAction::Serialise(stream);
|
||||
stream << DS_TAG(_loc) << DS_TAG(_rideIndex) << DS_TAG(_mazeEntry);
|
||||
}
|
||||
|
||||
GameActionResult::Ptr Query() const override
|
||||
{
|
||||
auto res = std::make_unique<GameActionResult>();
|
||||
|
||||
res->Position.x = _loc.x + 8;
|
||||
res->Position.y = _loc.y + 8;
|
||||
res->Position.z = _loc.z;
|
||||
res->ExpenditureType = RCT_EXPENDITURE_TYPE_RIDE_CONSTRUCTION;
|
||||
res->ErrorTitle = STR_RIDE_CONSTRUCTION_CANT_CONSTRUCT_THIS_HERE;
|
||||
if (!map_check_free_elements_and_reorganise(1))
|
||||
{
|
||||
res->Error = GA_ERROR::NO_FREE_ELEMENTS;
|
||||
res->ErrorMessage = STR_TILE_ELEMENT_LIMIT_REACHED;
|
||||
return res;
|
||||
}
|
||||
if ((_loc.z & 0xF) != 0)
|
||||
{
|
||||
res->Error = GA_ERROR::UNKNOWN;
|
||||
res->ErrorMessage = STR_CONSTRUCTION_ERR_UNKNOWN;
|
||||
return res;
|
||||
}
|
||||
|
||||
if (!map_is_location_owned(_loc) && !gCheatsSandboxMode)
|
||||
{
|
||||
res->Error = GA_ERROR::NOT_OWNED;
|
||||
res->ErrorMessage = STR_LAND_NOT_OWNED_BY_PARK;
|
||||
return res;
|
||||
}
|
||||
|
||||
auto surfaceElement = map_get_surface_element_at(_loc);
|
||||
if (surfaceElement == nullptr)
|
||||
{
|
||||
res->Error = GA_ERROR::UNKNOWN;
|
||||
res->ErrorMessage = STR_INVALID_SELECTION_OF_OBJECTS;
|
||||
return res;
|
||||
}
|
||||
|
||||
uint8_t baseHeight = _loc.z / 8;
|
||||
uint8_t clearanceHeight = (_loc.z + 32) / 8;
|
||||
|
||||
int8_t heightDifference = baseHeight - surfaceElement->base_height;
|
||||
if (heightDifference >= 0 && !gCheatsDisableSupportLimits)
|
||||
{
|
||||
heightDifference = heightDifference >> 1;
|
||||
|
||||
if (heightDifference > RideData5[RIDE_TYPE_MAZE].max_height)
|
||||
{
|
||||
res->Error = GA_ERROR::TOO_HIGH;
|
||||
res->ErrorMessage = STR_TOO_HIGH_FOR_SUPPORTS;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
money32 clearCost = 0;
|
||||
|
||||
if (!map_can_construct_with_clear_at(
|
||||
floor2(_loc.x, 32), floor2(_loc.y, 32), baseHeight, clearanceHeight, &map_place_non_scenery_clear_func,
|
||||
{ 0b1111, 0 }, GetFlags(), &clearCost, CREATE_CROSSING_MODE_NONE))
|
||||
{
|
||||
return MakeResult(GA_ERROR::NO_CLEARANCE, res->ErrorTitle, gGameCommandErrorText, gCommonFormatArgs);
|
||||
}
|
||||
|
||||
if (gMapGroundFlags & ELEMENT_IS_UNDERWATER)
|
||||
{
|
||||
res->Error = GA_ERROR::NO_CLEARANCE;
|
||||
res->ErrorMessage = STR_RIDE_CANT_BUILD_THIS_UNDERWATER;
|
||||
return res;
|
||||
}
|
||||
|
||||
if (gMapGroundFlags & ELEMENT_IS_UNDERGROUND)
|
||||
{
|
||||
res->Error = GA_ERROR::NO_CLEARANCE;
|
||||
res->ErrorMessage = STR_CAN_ONLY_BUILD_THIS_ABOVE_GROUND;
|
||||
return res;
|
||||
}
|
||||
|
||||
auto ride = get_ride(_rideIndex);
|
||||
if (ride == nullptr || ride->type == RIDE_TYPE_NULL)
|
||||
{
|
||||
res->Error = GA_ERROR::INVALID_PARAMETERS;
|
||||
res->ErrorMessage = STR_INVALID_SELECTION_OF_OBJECTS;
|
||||
return res;
|
||||
}
|
||||
|
||||
money32 price = (((RideTrackCosts[ride->type].track_price * TrackPricing[TRACK_ELEM_MAZE]) >> 16));
|
||||
res->Cost = clearCost + price / 2 * 10;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
GameActionResult::Ptr Execute() const override
|
||||
{
|
||||
auto res = std::make_unique<GameActionResult>();
|
||||
|
||||
res->Position.x = _loc.x + 8;
|
||||
res->Position.y = _loc.y + 8;
|
||||
res->Position.z = _loc.z;
|
||||
res->ExpenditureType = RCT_EXPENDITURE_TYPE_RIDE_CONSTRUCTION;
|
||||
res->ErrorTitle = STR_RIDE_CONSTRUCTION_CANT_CONSTRUCT_THIS_HERE;
|
||||
|
||||
auto ride = get_ride(_rideIndex);
|
||||
if (ride == nullptr)
|
||||
{
|
||||
res->Error = GA_ERROR::INVALID_PARAMETERS;
|
||||
res->ErrorMessage = STR_NONE;
|
||||
return res;
|
||||
}
|
||||
|
||||
if (!map_check_free_elements_and_reorganise(1))
|
||||
{
|
||||
res->Error = GA_ERROR::NO_FREE_ELEMENTS;
|
||||
res->ErrorMessage = STR_NONE;
|
||||
return res;
|
||||
}
|
||||
|
||||
uint32_t flags = GetFlags();
|
||||
if (!(flags & GAME_COMMAND_FLAG_GHOST))
|
||||
{
|
||||
footpath_remove_litter(_loc.x, _loc.y, _loc.z);
|
||||
wall_remove_at(floor2(_loc.x, 32), floor2(_loc.y, 32), _loc.z, _loc.z + 32);
|
||||
}
|
||||
|
||||
uint8_t baseHeight = _loc.z / 8;
|
||||
uint8_t clearanceHeight = (_loc.z + 32) / 8;
|
||||
|
||||
money32 clearCost = 0;
|
||||
if (!map_can_construct_with_clear_at(
|
||||
floor2(_loc.x, 32), floor2(_loc.y, 32), baseHeight, clearanceHeight, &map_place_non_scenery_clear_func,
|
||||
{ 0b1111, 0 }, GetFlags() | GAME_COMMAND_FLAG_APPLY, &clearCost, CREATE_CROSSING_MODE_NONE))
|
||||
{
|
||||
return MakeResult(GA_ERROR::NO_CLEARANCE, res->ErrorTitle, gGameCommandErrorText, gCommonFormatArgs);
|
||||
}
|
||||
|
||||
money32 price = (((RideTrackCosts[ride->type].track_price * TrackPricing[TRACK_ELEM_MAZE]) >> 16));
|
||||
res->Cost = clearCost + price / 2 * 10;
|
||||
|
||||
uint16_t flooredX = floor2(_loc.x, 32);
|
||||
uint16_t flooredY = floor2(_loc.y, 32);
|
||||
|
||||
auto tileElement = tile_element_insert({ _loc.x / 32, _loc.y / 32, baseHeight }, 0b1111);
|
||||
assert(tileElement != nullptr);
|
||||
|
||||
tileElement->clearance_height = clearanceHeight + 4;
|
||||
tileElement->SetType(TILE_ELEMENT_TYPE_TRACK);
|
||||
|
||||
tileElement->AsTrack()->SetTrackType(TRACK_ELEM_MAZE);
|
||||
tileElement->AsTrack()->SetRideIndex(_rideIndex);
|
||||
tileElement->AsTrack()->SetMazeEntry(_mazeEntry);
|
||||
|
||||
if (flags & GAME_COMMAND_FLAG_GHOST)
|
||||
{
|
||||
tileElement->SetGhost(true);
|
||||
}
|
||||
|
||||
map_invalidate_tile_full(flooredX, flooredY);
|
||||
|
||||
ride->maze_tiles++;
|
||||
ride->stations[0].Height = tileElement->base_height;
|
||||
ride->stations[0].Start.xy = 0;
|
||||
|
||||
if (ride->maze_tiles == 1)
|
||||
{
|
||||
ride->overall_view.x = flooredX / 32;
|
||||
ride->overall_view.y = flooredY / 32;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
};
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "../actions/FootpathRemoveAction.hpp"
|
||||
#include "../actions/LargeSceneryPlaceAction.hpp"
|
||||
#include "../actions/LargeSceneryRemoveAction.hpp"
|
||||
#include "../actions/MazePlaceTrackAction.hpp"
|
||||
#include "../actions/RideEntranceExitPlaceAction.hpp"
|
||||
#include "../actions/RideSetSetting.hpp"
|
||||
#include "../actions/RideSetVehiclesAction.hpp"
|
||||
@@ -1353,9 +1354,11 @@ static int32_t track_design_place_maze(TrackDesign* td6, int16_t x, int16_t y, i
|
||||
|
||||
gGameCommandErrorTitle = STR_RIDE_CONSTRUCTION_CANT_CONSTRUCT_THIS_HERE;
|
||||
|
||||
cost = game_do_command(
|
||||
mapCoord.x, flags | (maze_entry & 0xFF) << 8, mapCoord.y, ride->id | (maze_entry & 0xFF00),
|
||||
GAME_COMMAND_PLACE_MAZE_DESIGN, z, 0);
|
||||
auto mazePlace = MazePlaceTrackAction({ mapCoord, z }, ride->id, maze_entry);
|
||||
mazePlace.SetFlags(flags);
|
||||
auto res = flags & GAME_COMMAND_FLAG_APPLY ? GameActions::ExecuteNested(&mazePlace)
|
||||
: GameActions::QueryNested(&mazePlace);
|
||||
cost = res->Error == GA_ERROR::OK ? res->Cost : MONEY32_UNDEFINED;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2068,135 +2071,6 @@ static money32 place_track_design(int16_t x, int16_t y, int16_t z, uint8_t flags
|
||||
return cost;
|
||||
}
|
||||
|
||||
static money32 place_maze_design(uint8_t flags, Ride* ride, uint16_t mazeEntry, int16_t x, int16_t y, int16_t z)
|
||||
{
|
||||
gCommandExpenditureType = RCT_EXPENDITURE_TYPE_RIDE_CONSTRUCTION;
|
||||
gCommandPosition.x = x + 8;
|
||||
gCommandPosition.y = y + 8;
|
||||
gCommandPosition.z = z;
|
||||
if (!map_check_free_elements_and_reorganise(1))
|
||||
{
|
||||
return MONEY32_UNDEFINED;
|
||||
}
|
||||
|
||||
if ((z & 15) != 0)
|
||||
{
|
||||
return MONEY32_UNDEFINED;
|
||||
}
|
||||
|
||||
if (!(flags & GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED))
|
||||
{
|
||||
if (game_is_paused() && !gCheatsBuildInPauseMode)
|
||||
{
|
||||
gGameCommandErrorText = STR_CONSTRUCTION_NOT_POSSIBLE_WHILE_GAME_IS_PAUSED;
|
||||
return MONEY32_UNDEFINED;
|
||||
}
|
||||
}
|
||||
|
||||
if (flags & GAME_COMMAND_FLAG_APPLY)
|
||||
{
|
||||
if (!(flags & GAME_COMMAND_FLAG_GHOST))
|
||||
{
|
||||
footpath_remove_litter(x, y, z);
|
||||
wall_remove_at(floor2(x, 32), floor2(y, 32), z, z + 32);
|
||||
}
|
||||
}
|
||||
|
||||
if (!gCheatsSandboxMode)
|
||||
{
|
||||
if (!map_is_location_owned({ x, y, z }))
|
||||
{
|
||||
return MONEY32_UNDEFINED;
|
||||
}
|
||||
}
|
||||
|
||||
// Check support height
|
||||
if (!gCheatsDisableSupportLimits)
|
||||
{
|
||||
auto surfaceElement = map_get_surface_element_at({ x, y });
|
||||
uint8_t supportZ = (z + 32) >> 3;
|
||||
if (supportZ > surfaceElement->base_height)
|
||||
{
|
||||
uint8_t supportHeight = (supportZ - surfaceElement->base_height) / 2;
|
||||
uint8_t maxSupportHeight = RideData5[RIDE_TYPE_MAZE].max_height;
|
||||
if (supportHeight > maxSupportHeight)
|
||||
{
|
||||
gGameCommandErrorText = STR_TOO_HIGH_FOR_SUPPORTS;
|
||||
return MONEY32_UNDEFINED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
money32 cost = 0;
|
||||
// Clearance checks
|
||||
if (!gCheatsDisableClearanceChecks)
|
||||
{
|
||||
int32_t fx = floor2(x, 32);
|
||||
int32_t fy = floor2(y, 32);
|
||||
int32_t fz0 = z >> 3;
|
||||
int32_t fz1 = fz0 + 4;
|
||||
|
||||
if (!map_can_construct_with_clear_at(
|
||||
fx, fy, fz0, fz1, &map_place_non_scenery_clear_func, { 0b1111, 0 }, flags, &cost, CREATE_CROSSING_MODE_NONE))
|
||||
{
|
||||
return MONEY32_UNDEFINED;
|
||||
}
|
||||
|
||||
uint8_t elctgaw = gMapGroundFlags;
|
||||
if (elctgaw & ELEMENT_IS_UNDERWATER)
|
||||
{
|
||||
gGameCommandErrorText = STR_RIDE_CANT_BUILD_THIS_UNDERWATER;
|
||||
return MONEY32_UNDEFINED;
|
||||
}
|
||||
if (elctgaw & ELEMENT_IS_UNDERGROUND)
|
||||
{
|
||||
gGameCommandErrorText = STR_CAN_ONLY_BUILD_THIS_ABOVE_GROUND;
|
||||
return MONEY32_UNDEFINED;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate price
|
||||
money32 price = 0;
|
||||
if (!(gParkFlags & PARK_FLAGS_NO_MONEY))
|
||||
{
|
||||
price = RideTrackCosts[ride->type].track_price * TrackPricing[TRACK_ELEM_MAZE];
|
||||
price = (price >> 17) * 10;
|
||||
}
|
||||
|
||||
cost += price;
|
||||
|
||||
if (flags & GAME_COMMAND_FLAG_APPLY)
|
||||
{
|
||||
// Place track element
|
||||
int32_t fx = floor2(x, 32);
|
||||
int32_t fy = floor2(y, 32);
|
||||
int32_t fz = z >> 3;
|
||||
TileElement* tileElement = tile_element_insert({ fx >> 5, fy >> 5, fz }, 0b1111);
|
||||
tileElement->clearance_height = fz + 4;
|
||||
tileElement->SetType(TILE_ELEMENT_TYPE_TRACK);
|
||||
tileElement->AsTrack()->SetTrackType(TRACK_ELEM_MAZE);
|
||||
tileElement->AsTrack()->SetRideIndex(ride->id);
|
||||
tileElement->AsTrack()->SetMazeEntry(mazeEntry);
|
||||
if (flags & GAME_COMMAND_FLAG_GHOST)
|
||||
{
|
||||
tileElement->SetGhost(true);
|
||||
}
|
||||
|
||||
map_invalidate_element(fx, fy, tileElement);
|
||||
|
||||
ride->maze_tiles++;
|
||||
ride->stations[0].Height = tileElement->base_height;
|
||||
ride->stations[0].Start.xy = 0;
|
||||
if (ride->maze_tiles == 1)
|
||||
{
|
||||
ride->overall_view.x = fx / 32;
|
||||
ride->overall_view.y = fy / 32;
|
||||
}
|
||||
}
|
||||
|
||||
return cost;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* rct2: 0x006D13FE
|
||||
@@ -2214,19 +2088,6 @@ void game_command_place_track_design(
|
||||
*edi = rideIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* rct2: 0x006CDEE4
|
||||
*/
|
||||
void game_command_place_maze_design(
|
||||
int32_t* eax, int32_t* ebx, int32_t* ecx, int32_t* edx, [[maybe_unused]] int32_t* esi, int32_t* edi,
|
||||
[[maybe_unused]] int32_t* ebp)
|
||||
{
|
||||
auto ride = get_ride(*edx & 0xFF);
|
||||
*ebx = place_maze_design(
|
||||
*ebx & 0xFF, ride, ((*ebx >> 8) & 0xFF) | (((*edx >> 8) & 0xFF) << 8), *eax & 0xFFFF, *ecx & 0xFFFF, *edi & 0xFFFF);
|
||||
}
|
||||
|
||||
#pragma region Track Design Preview
|
||||
|
||||
/**
|
||||
|
||||
@@ -207,8 +207,6 @@ int32_t place_virtual_track(
|
||||
|
||||
void game_command_place_track_design(
|
||||
int32_t* eax, int32_t* ebx, int32_t* ecx, int32_t* edx, int32_t* esi, int32_t* edi, int32_t* ebp);
|
||||
void game_command_place_maze_design(
|
||||
int32_t* eax, int32_t* ebx, int32_t* ecx, int32_t* edx, int32_t* esi, int32_t* edi, int32_t* ebp);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Track design preview
|
||||
|
||||
Reference in New Issue
Block a user