ddnet/src/game/editor/mapitems/layer_tiles.cpp

1278 lines
41 KiB
C++
Raw Normal View History

2010-11-20 10:37:14 +00:00
/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com. */
#include "layer_tiles.h"
#include <engine/keys.h>
#include <engine/shared/map.h>
2023-10-02 13:14:38 +00:00
#include <game/editor/editor.h>
#include <game/editor/editor_actions.h>
#include <iterator>
#include <numeric>
#include "image.h"
CLayerTiles::CLayerTiles(CEditor *pEditor, int w, int h) :
CLayer(pEditor)
2008-01-12 12:27:55 +00:00
{
2010-05-29 07:25:38 +00:00
m_Type = LAYERTYPE_TILES;
m_aName[0] = '\0';
2010-05-29 07:25:38 +00:00
m_Width = w;
m_Height = h;
m_Image = -1;
m_Game = 0;
m_Color.r = 255;
m_Color.g = 255;
m_Color.b = 255;
m_Color.a = 255;
m_ColorEnv = -1;
m_ColorEnvOffset = 0;
m_Tele = 0;
m_Speedup = 0;
m_Front = 0;
m_Switch = 0;
m_Tune = 0;
m_AutoMapperConfig = -1;
m_Seed = 0;
m_AutoAutoMap = false;
m_pTiles = new CTile[m_Width * m_Height];
mem_zero(m_pTiles, (size_t)m_Width * m_Height * sizeof(CTile));
2008-01-12 12:27:55 +00:00
}
CLayerTiles::CLayerTiles(const CLayerTiles &Other) :
CLayer(Other)
{
m_Width = Other.m_Width;
m_Height = Other.m_Height;
m_pTiles = new CTile[m_Width * m_Height];
mem_copy(m_pTiles, Other.m_pTiles, (size_t)m_Width * m_Height * sizeof(CTile));
m_Image = Other.m_Image;
m_Game = Other.m_Game;
m_Color = Other.m_Color;
m_ColorEnv = Other.m_ColorEnv;
m_ColorEnvOffset = Other.m_ColorEnvOffset;
m_AutoMapperConfig = Other.m_AutoMapperConfig;
m_Seed = Other.m_Seed;
m_AutoAutoMap = Other.m_AutoAutoMap;
m_Tele = Other.m_Tele;
m_Speedup = Other.m_Speedup;
m_Front = Other.m_Front;
m_Switch = Other.m_Switch;
m_Tune = Other.m_Tune;
mem_copy(m_aFileName, Other.m_aFileName, IO_MAX_PATH_LENGTH);
}
2010-05-29 07:25:38 +00:00
CLayerTiles::~CLayerTiles()
2008-01-12 12:27:55 +00:00
{
delete[] m_pTiles;
2008-01-12 12:27:55 +00:00
}
CTile CLayerTiles::GetTile(int x, int y)
{
return m_pTiles[y * m_Width + x];
}
2023-07-10 19:27:51 +00:00
void CLayerTiles::SetTile(int x, int y, CTile Tile)
2023-10-02 13:14:38 +00:00
{
auto CurrentTile = m_pTiles[y * m_Width + x];
SetTileIgnoreHistory(x, y, Tile);
RecordStateChange(x, y, CurrentTile, Tile);
}
void CLayerTiles::SetTileIgnoreHistory(int x, int y, CTile Tile) const
{
2023-07-10 19:27:51 +00:00
m_pTiles[y * m_Width + x] = Tile;
}
2023-10-02 13:14:38 +00:00
void CLayerTiles::RecordStateChange(int x, int y, CTile Previous, CTile Tile)
{
if(!m_TilesHistory[y][x].m_Changed)
m_TilesHistory[y][x] = STileStateChange{true, Previous, Tile};
else
m_TilesHistory[y][x].m_Current = Tile;
}
2010-05-29 07:25:38 +00:00
void CLayerTiles::PrepareForSave()
2008-03-29 11:44:03 +00:00
{
2010-05-29 07:25:38 +00:00
for(int y = 0; y < m_Height; y++)
for(int x = 0; x < m_Width; x++)
m_pTiles[y * m_Width + x].m_Flags &= TILEFLAG_XFLIP | TILEFLAG_YFLIP | TILEFLAG_ROTATE;
2008-03-29 11:44:03 +00:00
if(m_Image != -1 && m_Color.a == 255)
2008-03-29 11:44:03 +00:00
{
2010-05-29 07:25:38 +00:00
for(int y = 0; y < m_Height; y++)
for(int x = 0; x < m_Width; x++)
m_pTiles[y * m_Width + x].m_Flags |= m_pEditor->m_Map.m_vpImages[m_Image]->m_aTileFlags[m_pTiles[y * m_Width + x].m_Index];
2008-03-29 11:44:03 +00:00
}
}
void CLayerTiles::ExtractTiles(int TilemapItemVersion, const CTile *pSavedTiles, size_t SavedTilesSize) const
2017-12-11 08:50:15 +00:00
{
const size_t DestSize = (size_t)m_Width * m_Height;
if(TilemapItemVersion >= CMapItemLayerTilemap::TILE_SKIP_MIN_VERSION)
CMap::ExtractTiles(m_pTiles, DestSize, pSavedTiles, SavedTilesSize);
else if(SavedTilesSize >= DestSize)
mem_copy(m_pTiles, pSavedTiles, DestSize * sizeof(CTile));
2017-12-11 08:50:15 +00:00
}
void CLayerTiles::MakePalette() const
2008-01-12 12:27:55 +00:00
{
2010-05-29 07:25:38 +00:00
for(int y = 0; y < m_Height; y++)
for(int x = 0; x < m_Width; x++)
m_pTiles[y * m_Width + x].m_Index = y * 16 + x;
2008-01-12 12:27:55 +00:00
}
2018-08-19 17:05:42 +00:00
void CLayerTiles::Render(bool Tileset)
2008-01-12 12:27:55 +00:00
{
IGraphics::CTextureHandle Texture;
if(m_Image >= 0 && (size_t)m_Image < m_pEditor->m_Map.m_vpImages.size())
Texture = m_pEditor->m_Map.m_vpImages[m_Image]->m_Texture;
else if(m_Game)
Texture = m_pEditor->GetEntitiesTexture();
else if(m_Front)
Texture = m_pEditor->GetFrontTexture();
else if(m_Tele)
Texture = m_pEditor->GetTeleTexture();
else if(m_Speedup)
Texture = m_pEditor->GetSpeedupTexture();
else if(m_Switch)
Texture = m_pEditor->GetSwitchTexture();
else if(m_Tune)
Texture = m_pEditor->GetTuneTexture();
Graphics()->TextureSet(Texture);
ColorRGBA Color = ColorRGBA(m_Color.r / 255.0f, m_Color.g / 255.0f, m_Color.b / 255.0f, m_Color.a / 255.0f);
Graphics()->BlendNone();
m_pEditor->RenderTools()->RenderTilemap(m_pTiles, m_Width, m_Height, 32.0f, Color, LAYERRENDERFLAG_OPAQUE,
CEditor::EnvelopeEval, m_pEditor, m_ColorEnv, m_ColorEnvOffset);
Graphics()->BlendNormal();
m_pEditor->RenderTools()->RenderTilemap(m_pTiles, m_Width, m_Height, 32.0f, Color, LAYERRENDERFLAG_TRANSPARENT,
CEditor::EnvelopeEval, m_pEditor, m_ColorEnv, m_ColorEnvOffset);
// Render DDRace Layers
2018-08-19 17:05:42 +00:00
if(!Tileset)
{
if(m_Tele)
m_pEditor->RenderTools()->RenderTeleOverlay(static_cast<CLayerTele *>(this)->m_pTeleTile, m_Width, m_Height, 32.0f);
2018-08-19 17:05:42 +00:00
if(m_Speedup)
m_pEditor->RenderTools()->RenderSpeedupOverlay(static_cast<CLayerSpeedup *>(this)->m_pSpeedupTile, m_Width, m_Height, 32.0f);
2018-08-19 17:05:42 +00:00
if(m_Switch)
m_pEditor->RenderTools()->RenderSwitchOverlay(static_cast<CLayerSwitch *>(this)->m_pSwitchTile, m_Width, m_Height, 32.0f);
2018-08-19 17:05:42 +00:00
if(m_Tune)
m_pEditor->RenderTools()->RenderTuneOverlay(static_cast<CLayerTune *>(this)->m_pTuneTile, m_Width, m_Height, 32.0f);
2018-08-19 17:05:42 +00:00
}
2008-01-12 12:27:55 +00:00
}
int CLayerTiles::ConvertX(float x) const { return (int)(x / 32.0f); }
int CLayerTiles::ConvertY(float y) const { return (int)(y / 32.0f); }
2008-01-12 12:27:55 +00:00
void CLayerTiles::Convert(CUIRect Rect, RECTi *pOut) const
2008-01-12 12:27:55 +00:00
{
2010-05-29 07:25:38 +00:00
pOut->x = ConvertX(Rect.x);
pOut->y = ConvertY(Rect.y);
pOut->w = ConvertX(Rect.x + Rect.w + 31) - pOut->x;
pOut->h = ConvertY(Rect.y + Rect.h + 31) - pOut->y;
2008-01-12 12:27:55 +00:00
}
void CLayerTiles::Snap(CUIRect *pRect) const
2008-01-12 12:27:55 +00:00
{
2010-05-29 07:25:38 +00:00
RECTi Out;
Convert(*pRect, &Out);
pRect->x = Out.x * 32.0f;
pRect->y = Out.y * 32.0f;
pRect->w = Out.w * 32.0f;
pRect->h = Out.h * 32.0f;
2008-01-12 12:27:55 +00:00
}
void CLayerTiles::Clamp(RECTi *pRect) const
2008-01-12 12:27:55 +00:00
{
2010-05-29 07:25:38 +00:00
if(pRect->x < 0)
2008-01-12 12:27:55 +00:00
{
2010-05-29 07:25:38 +00:00
pRect->w += pRect->x;
pRect->x = 0;
2008-01-12 12:27:55 +00:00
}
2010-05-29 07:25:38 +00:00
if(pRect->y < 0)
2008-01-12 12:27:55 +00:00
{
2010-05-29 07:25:38 +00:00
pRect->h += pRect->y;
pRect->y = 0;
2008-01-12 12:27:55 +00:00
}
if(pRect->x + pRect->w > m_Width)
2010-05-29 07:25:38 +00:00
pRect->w = m_Width - pRect->x;
2008-01-12 12:27:55 +00:00
if(pRect->y + pRect->h > m_Height)
2010-05-29 07:25:38 +00:00
pRect->h = m_Height - pRect->y;
2010-05-29 07:25:38 +00:00
if(pRect->h < 0)
pRect->h = 0;
if(pRect->w < 0)
pRect->w = 0;
2008-01-12 12:27:55 +00:00
}
bool CLayerTiles::IsEntitiesLayer() const
{
return m_pEditor->m_Map.m_pGameLayer.get() == this || m_pEditor->m_Map.m_pTeleLayer.get() == this || m_pEditor->m_Map.m_pSpeedupLayer.get() == this || m_pEditor->m_Map.m_pFrontLayer.get() == this || m_pEditor->m_Map.m_pSwitchLayer.get() == this || m_pEditor->m_Map.m_pTuneLayer.get() == this;
}
bool CLayerTiles::IsEmpty(const std::shared_ptr<CLayerTiles> &pLayer)
{
for(int y = 0; y < pLayer->m_Height; y++)
{
for(int x = 0; x < pLayer->m_Width; x++)
{
int Index = pLayer->GetTile(x, y).m_Index;
if(Index)
{
if(pLayer->m_Game)
{
if(m_pEditor->m_AllowPlaceUnusedTiles || IsValidGameTile(Index))
return false;
}
else if(pLayer->m_Front)
{
if(m_pEditor->m_AllowPlaceUnusedTiles || IsValidFrontTile(Index))
return false;
}
else
return false;
}
}
}
return true;
}
2010-05-29 07:25:38 +00:00
void CLayerTiles::BrushSelecting(CUIRect Rect)
2008-01-12 12:27:55 +00:00
{
Graphics()->TextureClear();
2010-05-29 07:25:38 +00:00
m_pEditor->Graphics()->QuadsBegin();
m_pEditor->Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.4f);
Snap(&Rect);
IGraphics::CQuadItem QuadItem(Rect.x, Rect.y, Rect.w, Rect.h);
m_pEditor->Graphics()->QuadsDrawTL(&QuadItem, 1);
m_pEditor->Graphics()->QuadsEnd();
char aBuf[16];
str_format(aBuf, sizeof(aBuf), "%d%d", ConvertX(Rect.w), ConvertY(Rect.h));
TextRender()->Text(Rect.x + 3.0f, Rect.y + 3.0f, m_pEditor->m_ShowPicker ? 15.0f : m_pEditor->MapView()->ScaleLength(15.0f), aBuf, -1.0f);
2008-01-12 12:27:55 +00:00
}
template<typename T>
static void InitGrabbedLayer(std::shared_ptr<T> pLayer, CLayerTiles *pThisLayer)
{
pLayer->m_pEditor = pThisLayer->m_pEditor;
pLayer->m_Image = pThisLayer->m_Image;
pLayer->m_Game = pThisLayer->m_Game;
pLayer->m_Front = pThisLayer->m_Front;
pLayer->m_Tele = pThisLayer->m_Tele;
pLayer->m_Speedup = pThisLayer->m_Speedup;
pLayer->m_Switch = pThisLayer->m_Switch;
pLayer->m_Tune = pThisLayer->m_Tune;
if(pThisLayer->m_pEditor->m_BrushColorEnabled)
{
pLayer->m_Color = pThisLayer->m_Color;
pLayer->m_Color.a = 255;
}
};
int CLayerTiles::BrushGrab(std::shared_ptr<CLayerGroup> pBrush, CUIRect Rect)
2008-01-12 12:27:55 +00:00
{
RECTi r;
2010-05-29 07:25:38 +00:00
Convert(Rect, &r);
Clamp(&r);
2008-01-12 12:27:55 +00:00
if(!r.w || !r.h)
return 0;
2008-01-12 12:27:55 +00:00
// create new layers
2018-08-19 17:05:42 +00:00
if(this->m_Tele)
{
std::shared_ptr<CLayerTele> pGrabbed = std::make_shared<CLayerTele>(m_pEditor, r.w, r.h);
InitGrabbedLayer(pGrabbed, this);
pBrush->AddLayer(pGrabbed);
// copy the tiles
for(int y = 0; y < r.h; y++)
for(int x = 0; x < r.w; x++)
pGrabbed->m_pTiles[y * pGrabbed->m_Width + x] = GetTile(r.x + x, r.y + y);
// copy the tele data
if(!m_pEditor->Input()->KeyIsPressed(KEY_SPACE))
for(int y = 0; y < r.h; y++)
for(int x = 0; x < r.w; x++)
{
pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x] = static_cast<CLayerTele *>(this)->m_pTeleTile[(r.y + y) * m_Width + (r.x + x)];
if(IsValidTeleTile(pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Type))
2020-05-15 22:42:11 +00:00
{
if(IsTeleTileNumberUsed(pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Type, false))
m_pEditor->m_TeleNumber = pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Number;
else if(IsTeleTileNumberUsed(pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Type, true))
m_pEditor->m_TeleCheckpointNumber = pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Number;
2020-05-15 22:42:11 +00:00
}
}
2010-12-07 15:51:59 +00:00
pGrabbed->m_TeleNum = m_pEditor->m_TeleNumber;
pGrabbed->m_TeleCheckpointNum = m_pEditor->m_TeleCheckpointNumber;
str_copy(pGrabbed->m_aFileName, m_pEditor->m_aFileName);
}
2018-08-19 17:05:42 +00:00
else if(this->m_Speedup)
{
std::shared_ptr<CLayerSpeedup> pGrabbed = std::make_shared<CLayerSpeedup>(m_pEditor, r.w, r.h);
InitGrabbedLayer(pGrabbed, this);
pBrush->AddLayer(pGrabbed);
// copy the tiles
for(int y = 0; y < r.h; y++)
for(int x = 0; x < r.w; x++)
pGrabbed->m_pTiles[y * pGrabbed->m_Width + x] = GetTile(r.x + x, r.y + y);
// copy the speedup data
if(!m_pEditor->Input()->KeyIsPressed(KEY_SPACE))
for(int y = 0; y < r.h; y++)
for(int x = 0; x < r.w; x++)
{
pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x] = static_cast<CLayerSpeedup *>(this)->m_pSpeedupTile[(r.y + y) * m_Width + (r.x + x)];
if(IsValidSpeedupTile(pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_Type))
{
m_pEditor->m_SpeedupAngle = pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_Angle;
m_pEditor->m_SpeedupForce = pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_Force;
m_pEditor->m_SpeedupMaxSpeed = pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_MaxSpeed;
}
}
2010-12-04 11:54:53 +00:00
pGrabbed->m_SpeedupForce = m_pEditor->m_SpeedupForce;
pGrabbed->m_SpeedupMaxSpeed = m_pEditor->m_SpeedupMaxSpeed;
pGrabbed->m_SpeedupAngle = m_pEditor->m_SpeedupAngle;
str_copy(pGrabbed->m_aFileName, m_pEditor->m_aFileName);
}
2018-08-19 17:05:42 +00:00
else if(this->m_Switch)
{
std::shared_ptr<CLayerSwitch> pGrabbed = std::make_shared<CLayerSwitch>(m_pEditor, r.w, r.h);
InitGrabbedLayer(pGrabbed, this);
pBrush->AddLayer(pGrabbed);
// copy the tiles
for(int y = 0; y < r.h; y++)
for(int x = 0; x < r.w; x++)
pGrabbed->m_pTiles[y * pGrabbed->m_Width + x] = GetTile(r.x + x, r.y + y);
// copy the switch data
if(!m_pEditor->Input()->KeyIsPressed(KEY_SPACE))
for(int y = 0; y < r.h; y++)
for(int x = 0; x < r.w; x++)
{
pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x] = static_cast<CLayerSwitch *>(this)->m_pSwitchTile[(r.y + y) * m_Width + (r.x + x)];
if(IsValidSwitchTile(pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x].m_Type))
2010-11-22 20:43:22 +00:00
{
m_pEditor->m_SwitchNum = pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x].m_Number;
m_pEditor->m_SwitchDelay = pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x].m_Delay;
2010-11-22 20:43:22 +00:00
}
}
2010-12-07 15:51:59 +00:00
pGrabbed->m_SwitchNumber = m_pEditor->m_SwitchNum;
2010-12-04 11:54:53 +00:00
pGrabbed->m_SwitchDelay = m_pEditor->m_SwitchDelay;
str_copy(pGrabbed->m_aFileName, m_pEditor->m_aFileName);
}
2015-07-09 00:08:14 +00:00
2018-08-19 17:05:42 +00:00
else if(this->m_Tune)
{
std::shared_ptr<CLayerTune> pGrabbed = std::make_shared<CLayerTune>(m_pEditor, r.w, r.h);
InitGrabbedLayer(pGrabbed, this);
pBrush->AddLayer(pGrabbed);
2015-07-09 00:08:14 +00:00
// copy the tiles
for(int y = 0; y < r.h; y++)
for(int x = 0; x < r.w; x++)
pGrabbed->m_pTiles[y * pGrabbed->m_Width + x] = GetTile(r.x + x, r.y + y);
// copy the tiles
if(!m_pEditor->Input()->KeyIsPressed(KEY_SPACE))
for(int y = 0; y < r.h; y++)
for(int x = 0; x < r.w; x++)
{
pGrabbed->m_pTuneTile[y * pGrabbed->m_Width + x] = static_cast<CLayerTune *>(this)->m_pTuneTile[(r.y + y) * m_Width + (r.x + x)];
if(IsValidTuneTile(pGrabbed->m_pTuneTile[y * pGrabbed->m_Width + x].m_Type))
{
m_pEditor->m_TuningNum = pGrabbed->m_pTuneTile[y * pGrabbed->m_Width + x].m_Number;
}
}
pGrabbed->m_TuningNumber = m_pEditor->m_TuningNum;
str_copy(pGrabbed->m_aFileName, m_pEditor->m_aFileName);
}
2018-08-19 17:05:42 +00:00
else if(this->m_Front)
{
std::shared_ptr<CLayerFront> pGrabbed = std::make_shared<CLayerFront>(m_pEditor, r.w, r.h);
InitGrabbedLayer(pGrabbed, this);
pBrush->AddLayer(pGrabbed);
// copy the tiles
for(int y = 0; y < r.h; y++)
for(int x = 0; x < r.w; x++)
pGrabbed->m_pTiles[y * pGrabbed->m_Width + x] = GetTile(r.x + x, r.y + y);
str_copy(pGrabbed->m_aFileName, m_pEditor->m_aFileName);
}
else
{
std::shared_ptr<CLayerTiles> pGrabbed = std::make_shared<CLayerFront>(m_pEditor, r.w, r.h);
InitGrabbedLayer(pGrabbed, this);
pBrush->AddLayer(pGrabbed);
// copy the tiles
for(int y = 0; y < r.h; y++)
for(int x = 0; x < r.w; x++)
pGrabbed->m_pTiles[y * pGrabbed->m_Width + x] = GetTile(r.x + x, r.y + y);
str_copy(pGrabbed->m_aFileName, m_pEditor->m_aFileName);
}
2008-01-12 12:27:55 +00:00
return 1;
}
void CLayerTiles::FillSelection(bool Empty, std::shared_ptr<CLayer> pBrush, CUIRect Rect)
2008-01-12 12:27:55 +00:00
{
2018-10-30 17:56:46 +00:00
if(m_Readonly || (!Empty && pBrush->m_Type != LAYERTYPE_TILES))
return;
Snap(&Rect);
2010-05-29 07:25:38 +00:00
int sx = ConvertX(Rect.x);
int sy = ConvertY(Rect.y);
int w = ConvertX(Rect.w);
int h = ConvertY(Rect.h);
std::shared_ptr<CLayerTiles> pLt = std::static_pointer_cast<CLayerTiles>(pBrush);
bool Destructive = m_pEditor->m_BrushDrawDestructive || Empty || IsEmpty(pLt);
for(int y = 0; y < h; y++)
2010-05-29 07:25:38 +00:00
{
for(int x = 0; x < w; x++)
2010-05-29 07:25:38 +00:00
{
int fx = x + sx;
int fy = y + sy;
2010-05-29 07:25:38 +00:00
if(fx < 0 || fx >= m_Width || fy < 0 || fy >= m_Height)
continue;
bool HasTile = GetTile(fx, fy).m_Index;
Fix buffer-overflow in editor on shift-clicking brush 1. Open any map, including an empty one. 2. Select a brush, e.g. size 2x2. 3. Shift click to repeat the brush over a larger area, e.g. 10x10. 4. This causes a buffer-overflow / crash with ASAN: ``` ================================================================= ==4826==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6060000f5040 at pc 0x55db7d0aa743 bp 0x7fffe4e191f0 sp 0x7fffe4e191e0 READ of size 4 at 0x6060000f5040 thread T0 0 0x55db7d0aa742 in CLayerTiles::GetTile(int, int) src/game/editor/layer_tiles.cpp:50 1 0x55db7d0d23e1 in CLayerTiles::FillSelection(bool, CLayer*, CUIRect) src/game/editor/layer_tiles.cpp:437 2 0x55db7cf196e9 in CEditor::DoMapEditor(CUIRect) src/game/editor/editor.cpp:2641 3 0x55db7cfa7755 in CEditor::Render() src/game/editor/editor.cpp:5747 4 0x55db7cfd2a56 in CEditor::OnRender() src/game/editor/editor.cpp:6437 5 0x55db7c23e02d in CClient::Run() src/engine/client/client.cpp:3374 6 0x55db7c2a9f7b in main src/engine/client/client.cpp:4762 0x6060000f5040 is located 0 bytes to the right of 64-byte region [0x6060000f5000,0x6060000f5040) allocated by thread T0 here: 0 0x7f9b21db5787 in operator new[](unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cc:107 1 0x55db7d0a9a86 in CLayerTiles::CLayerTiles(int, int) src/game/editor/layer_tiles.cpp:39 2 0x55db7d0cf0ed in CLayerTiles::BrushGrab(CLayerGroup*, CUIRect) src/game/editor/layer_tiles.cpp:387 3 0x55db7cf18191 in CEditor::DoMapEditor(CUIRect) src/game/editor/editor.cpp:2612 4 0x55db7cfa7755 in CEditor::Render() src/game/editor/editor.cpp:5747 5 0x55db7cfd2a56 in CEditor::OnRender() src/game/editor/editor.cpp:6437 6 0x55db7c23e02d in CClient::Run() src/engine/client/client.cpp:3374 SUMMARY: AddressSanitizer: heap-buffer-overflow src/game/editor/layer_tiles.cpp:50 in CLayerTiles::GetTile(int, int) Shadow bytes around the buggy address: 0x0c0c800169b0: 00 00 00 fa fa fa fa fa 00 00 00 00 00 00 00 fa 0x0c0c800169c0: fa fa fa fa fd fd fd fd fd fd fd fa fa fa fa fa 0x0c0c800169d0: fd fd fd fd fd fd fd fa fa fa fa fa fd fd fd fd 0x0c0c800169e0: fd fd fd fd fa fa fa fa fd fd fd fd fd fd fd fa 0x0c0c800169f0: fa fa fa fa fd fd fd fd fd fd fd fa fa fa fa fa =>0x0c0c80016a00: 00 00 00 00 00 00 00 00[fa]fa fa fa fd fd fd fd 0x0c0c80016a10: fd fd fd fa fa fa fa fa fd fd fd fd fd fd fd fa 0x0c0c80016a20: fa fa fa fa fd fd fd fd fd fd fd fa fa fa fa fa 0x0c0c80016a30: fd fd fd fd fd fd fd fa fa fa fa fa fd fd fd fd 0x0c0c80016a40: fd fd fd fa fa fa fa fa fd fd fd fd fd fd fd fa 0x0c0c80016a50: fa fa fa fa fd fd fd fd fd fd fd fa fa fa fa fa Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb Shadow gap: cc ==4826==ABORTING ```
2022-08-04 21:39:40 +00:00
if(!Empty && pLt->GetTile(x % pLt->m_Width, y % pLt->m_Height).m_Index == TILE_THROUGH_CUT)
{
if(m_Game && m_pEditor->m_Map.m_pFrontLayer)
{
HasTile = HasTile || m_pEditor->m_Map.m_pFrontLayer->GetTile(fx, fy).m_Index;
}
else if(m_Front)
{
HasTile = HasTile || m_pEditor->m_Map.m_pGameLayer->GetTile(fx, fy).m_Index;
}
}
if(!Destructive && HasTile)
continue;
SetTile(fx, fy, Empty ? CTile{TILE_AIR} : pLt->m_pTiles[(y * pLt->m_Width + x % pLt->m_Width) % (pLt->m_Width * pLt->m_Height)]);
2010-05-29 07:25:38 +00:00
}
}
FlagModified(sx, sy, w, h);
2010-05-29 07:25:38 +00:00
}
void CLayerTiles::BrushDraw(std::shared_ptr<CLayer> pBrush, float wx, float wy)
2010-05-29 07:25:38 +00:00
{
if(m_Readonly)
2008-03-20 23:59:41 +00:00
return;
2008-01-12 12:27:55 +00:00
//
std::shared_ptr<CLayerTiles> pTileLayer = std::static_pointer_cast<CLayerTiles>(pBrush);
2010-05-29 07:25:38 +00:00
int sx = ConvertX(wx);
int sy = ConvertY(wy);
bool Destructive = m_pEditor->m_BrushDrawDestructive || IsEmpty(pTileLayer);
for(int y = 0; y < pTileLayer->m_Height; y++)
for(int x = 0; x < pTileLayer->m_Width; x++)
2008-01-12 12:27:55 +00:00
{
int fx = x + sx;
int fy = y + sy;
if(fx < 0 || fx >= m_Width || fy < 0 || fy >= m_Height)
2008-01-12 12:27:55 +00:00
continue;
bool HasTile = GetTile(fx, fy).m_Index;
if(pTileLayer->GetTile(x, y).m_Index == TILE_THROUGH_CUT)
{
if(m_Game && m_pEditor->m_Map.m_pFrontLayer)
{
HasTile = HasTile || m_pEditor->m_Map.m_pFrontLayer->GetTile(fx, fy).m_Index;
}
else if(m_Front)
{
HasTile = HasTile || m_pEditor->m_Map.m_pGameLayer->GetTile(fx, fy).m_Index;
}
}
if(!Destructive && HasTile)
continue;
SetTile(fx, fy, pTileLayer->GetTile(x, y));
2008-01-12 12:27:55 +00:00
}
FlagModified(sx, sy, pTileLayer->m_Width, pTileLayer->m_Height);
2008-01-12 12:27:55 +00:00
}
2010-05-29 07:25:38 +00:00
void CLayerTiles::BrushFlipX()
2008-01-12 12:27:55 +00:00
{
BrushFlipXImpl(m_pTiles);
2008-01-12 12:27:55 +00:00
if(m_Tele || m_Speedup || m_Tune)
return;
bool Rotate = !(m_Game || m_Front || m_Switch) || m_pEditor->m_AllowPlaceUnusedTiles;
for(int y = 0; y < m_Height; y++)
for(int x = 0; x < m_Width; x++)
if(!Rotate && !IsRotatableTile(m_pTiles[y * m_Width + x].m_Index))
m_pTiles[y * m_Width + x].m_Flags = 0;
else
m_pTiles[y * m_Width + x].m_Flags ^= (m_pTiles[y * m_Width + x].m_Flags & TILEFLAG_ROTATE) ? TILEFLAG_YFLIP : TILEFLAG_XFLIP;
2008-01-12 12:27:55 +00:00
}
2010-05-29 07:25:38 +00:00
void CLayerTiles::BrushFlipY()
2008-01-12 12:27:55 +00:00
{
BrushFlipYImpl(m_pTiles);
2008-01-12 12:27:55 +00:00
if(m_Tele || m_Speedup || m_Tune)
return;
bool Rotate = !(m_Game || m_Front || m_Switch) || m_pEditor->m_AllowPlaceUnusedTiles;
for(int y = 0; y < m_Height; y++)
for(int x = 0; x < m_Width; x++)
if(!Rotate && !IsRotatableTile(m_pTiles[y * m_Width + x].m_Index))
m_pTiles[y * m_Width + x].m_Flags = 0;
else
m_pTiles[y * m_Width + x].m_Flags ^= (m_pTiles[y * m_Width + x].m_Flags & TILEFLAG_ROTATE) ? TILEFLAG_XFLIP : TILEFLAG_YFLIP;
}
void CLayerTiles::BrushRotate(float Amount)
{
int Rotation = (round_to_int(360.0f * Amount / (pi * 2)) / 90) % 4; // 0=0°, 1=90°, 2=180°, 3=270°
if(Rotation < 0)
Rotation += 4;
if(Rotation == 1 || Rotation == 3)
{
2014-03-19 09:26:38 +00:00
// 90° rotation
CTile *pTempData = new CTile[m_Width * m_Height];
mem_copy(pTempData, m_pTiles, (size_t)m_Width * m_Height * sizeof(CTile));
CTile *pDst = m_pTiles;
bool Rotate = !(m_Game || m_Front) || m_pEditor->m_AllowPlaceUnusedTiles;
for(int x = 0; x < m_Width; ++x)
for(int y = m_Height - 1; y >= 0; --y, ++pDst)
{
*pDst = pTempData[y * m_Width + x];
if(!Rotate && !IsRotatableTile(pDst->m_Index))
pDst->m_Flags = 0;
else
{
if(pDst->m_Flags & TILEFLAG_ROTATE)
pDst->m_Flags ^= (TILEFLAG_YFLIP | TILEFLAG_XFLIP);
pDst->m_Flags ^= TILEFLAG_ROTATE;
}
}
2022-05-15 18:44:05 +00:00
std::swap(m_Width, m_Height);
delete[] pTempData;
}
if(Rotation == 2 || Rotation == 3)
{
BrushFlipX();
BrushFlipY();
}
2008-01-12 12:27:55 +00:00
}
std::shared_ptr<CLayer> CLayerTiles::Duplicate() const
{
return std::make_shared<CLayerTiles>(*this);
}
2023-10-02 13:14:38 +00:00
const char *CLayerTiles::TypeName() const
{
return "tiles";
}
2010-05-29 07:25:38 +00:00
void CLayerTiles::Resize(int NewW, int NewH)
2008-01-12 12:27:55 +00:00
{
CTile *pNewData = new CTile[NewW * NewH];
mem_zero(pNewData, (size_t)NewW * NewH * sizeof(CTile));
2008-01-12 12:27:55 +00:00
// copy old data
2019-04-26 19:36:49 +00:00
for(int y = 0; y < minimum(NewH, m_Height); y++)
mem_copy(&pNewData[y * NewW], &m_pTiles[y * m_Width], minimum(m_Width, NewW) * sizeof(CTile));
2008-01-12 12:27:55 +00:00
// replace old
delete[] m_pTiles;
2010-05-29 07:25:38 +00:00
m_pTiles = pNewData;
m_Width = NewW;
m_Height = NewH;
// resize tele layer if available
if(m_Game && m_pEditor->m_Map.m_pTeleLayer && (m_pEditor->m_Map.m_pTeleLayer->m_Width != NewW || m_pEditor->m_Map.m_pTeleLayer->m_Height != NewH))
m_pEditor->m_Map.m_pTeleLayer->Resize(NewW, NewH);
// resize speedup layer if available
if(m_Game && m_pEditor->m_Map.m_pSpeedupLayer && (m_pEditor->m_Map.m_pSpeedupLayer->m_Width != NewW || m_pEditor->m_Map.m_pSpeedupLayer->m_Height != NewH))
m_pEditor->m_Map.m_pSpeedupLayer->Resize(NewW, NewH);
// resize front layer
if(m_Game && m_pEditor->m_Map.m_pFrontLayer && (m_pEditor->m_Map.m_pFrontLayer->m_Width != NewW || m_pEditor->m_Map.m_pFrontLayer->m_Height != NewH))
m_pEditor->m_Map.m_pFrontLayer->Resize(NewW, NewH);
// resize switch layer if available
if(m_Game && m_pEditor->m_Map.m_pSwitchLayer && (m_pEditor->m_Map.m_pSwitchLayer->m_Width != NewW || m_pEditor->m_Map.m_pSwitchLayer->m_Height != NewH))
m_pEditor->m_Map.m_pSwitchLayer->Resize(NewW, NewH);
2015-07-09 00:08:14 +00:00
// resize tune layer if available
if(m_Game && m_pEditor->m_Map.m_pTuneLayer && (m_pEditor->m_Map.m_pTuneLayer->m_Width != NewW || m_pEditor->m_Map.m_pTuneLayer->m_Height != NewH))
m_pEditor->m_Map.m_pTuneLayer->Resize(NewW, NewH);
2008-01-12 12:27:55 +00:00
}
void CLayerTiles::Shift(int Direction)
{
2022-06-05 20:03:16 +00:00
ShiftImpl(m_pTiles, Direction, m_pEditor->m_ShiftBy);
}
2008-01-12 12:27:55 +00:00
void CLayerTiles::ShowInfo()
{
float ScreenX0, ScreenY0, ScreenX1, ScreenY1;
Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1);
Graphics()->TextureSet(m_pEditor->Client()->GetDebugFont());
Graphics()->QuadsBegin();
int StartY = maximum(0, (int)(ScreenY0 / 32.0f) - 1);
int StartX = maximum(0, (int)(ScreenX0 / 32.0f) - 1);
int EndY = minimum((int)(ScreenY1 / 32.0f) + 1, m_Height);
int EndX = minimum((int)(ScreenX1 / 32.0f) + 1, m_Width);
for(int y = StartY; y < EndY; y++)
for(int x = StartX; x < EndX; x++)
{
int c = x + y * m_Width;
if(m_pTiles[c].m_Index)
{
char aBuf[4];
if(m_pEditor->m_ShowTileInfo == CEditor::SHOW_TILE_HEXADECIMAL)
{
str_hex(aBuf, sizeof(aBuf), &m_pTiles[c].m_Index, 1);
aBuf[2] = '\0'; // would otherwise be a space
}
else
{
str_from_int(m_pTiles[c].m_Index, aBuf);
}
m_pEditor->Graphics()->QuadsText(x * 32, y * 32, 16.0f, aBuf);
char aFlags[4] = {m_pTiles[c].m_Flags & TILEFLAG_XFLIP ? 'X' : ' ',
m_pTiles[c].m_Flags & TILEFLAG_YFLIP ? 'Y' : ' ',
m_pTiles[c].m_Flags & TILEFLAG_ROTATE ? 'R' : ' ',
0};
m_pEditor->Graphics()->QuadsText(x * 32, y * 32 + 16, 16.0f, aFlags);
}
x += m_pTiles[c].m_Skip;
}
Graphics()->QuadsEnd();
Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1);
}
CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox)
2008-01-12 12:27:55 +00:00
{
2010-05-29 07:25:38 +00:00
CUIRect Button;
2011-08-11 08:59:14 +00:00
const bool EntitiesLayer = IsEntitiesLayer();
std::shared_ptr<CLayerGroup> pGroup = m_pEditor->m_Map.m_vpGroups[m_pEditor->m_SelectedGroup];
// Game tiles can only be constructed if the layer is relative to the game layer
if(!EntitiesLayer && !(pGroup->m_OffsetX % 32) && !(pGroup->m_OffsetY % 32) && pGroup->m_ParallaxX == 100 && pGroup->m_ParallaxY == 100)
2008-09-07 08:30:49 +00:00
{
pToolBox->HSplitBottom(12.0f, pToolBox, &Button);
2023-04-01 21:16:55 +00:00
static int s_GameTilesButton = 0;
if(m_pEditor->DoButton_Editor(&s_GameTilesButton, "Game tiles", 0, &Button, 0, "Constructs game tiles from this layer"))
m_pEditor->PopupSelectGametileOpInvoke(m_pEditor->UI()->MouseX(), m_pEditor->UI()->MouseY());
int Result = m_pEditor->PopupSelectGameTileOpResult();
switch(Result)
2016-05-04 21:37:02 +00:00
{
case 4:
Result = TILE_THROUGH_CUT;
break;
case 5:
Result = TILE_FREEZE;
break;
case 6:
Result = TILE_UNFREEZE;
break;
case 7:
Result = TILE_DFREEZE;
break;
case 8:
Result = TILE_DUNFREEZE;
break;
case 9:
Result = TILE_TELECHECKIN;
break;
case 10:
Result = TILE_TELECHECKINEVIL;
break;
case 11:
Result = TILE_LFREEZE;
break;
case 12:
Result = TILE_LUNFREEZE;
break;
default:
break;
}
if(Result > -1)
{
2023-04-01 21:16:55 +00:00
const int OffsetX = -pGroup->m_OffsetX / 32;
const int OffsetY = -pGroup->m_OffsetY / 32;
2023-10-02 13:14:38 +00:00
static const char *s_apGametileOpNames[] = {
"Air",
"Hookable",
"Death",
"Unhookable",
"Hookthrough",
"Freeze",
"Unfreeze",
"Deep Freeze",
"Deep Unfreeze",
"Blue Check-Tele",
"Red Check-Tele",
"Live Freeze",
"Live Unfreeze",
};
std::vector<std::shared_ptr<IEditorAction>> vpActions;
std::shared_ptr<CLayerTiles> pGLayer = m_pEditor->m_Map.m_pGameLayer;
int GameLayerIndex = std::find(m_pEditor->m_Map.m_pGameGroup->m_vpLayers.begin(), m_pEditor->m_Map.m_pGameGroup->m_vpLayers.end(), pGLayer) - m_pEditor->m_Map.m_pGameGroup->m_vpLayers.begin();
if(Result != TILE_TELECHECKIN && Result != TILE_TELECHECKINEVIL)
{
if(pGLayer->m_Width < m_Width + OffsetX || pGLayer->m_Height < m_Height + OffsetY)
{
2023-10-02 13:14:38 +00:00
std::map<int, std::shared_ptr<CLayer>> savedLayers;
savedLayers[LAYERTYPE_TILES] = pGLayer->Duplicate();
savedLayers[LAYERTYPE_GAME] = savedLayers[LAYERTYPE_TILES];
int PrevW = pGLayer->m_Width;
int PrevH = pGLayer->m_Height;
2023-04-01 21:16:55 +00:00
const int NewW = pGLayer->m_Width < m_Width + OffsetX ? m_Width + OffsetX : pGLayer->m_Width;
const int NewH = pGLayer->m_Height < m_Height + OffsetY ? m_Height + OffsetY : pGLayer->m_Height;
pGLayer->Resize(NewW, NewH);
2023-10-02 13:14:38 +00:00
vpActions.push_back(std::make_shared<CEditorActionEditLayerTilesProp>(m_pEditor, m_pEditor->m_SelectedGroup, GameLayerIndex, ETilesProp::PROP_WIDTH, PrevW, NewW));
const std::shared_ptr<CEditorActionEditLayerTilesProp> &Action1 = std::static_pointer_cast<CEditorActionEditLayerTilesProp>(vpActions[vpActions.size() - 1]);
vpActions.push_back(std::make_shared<CEditorActionEditLayerTilesProp>(m_pEditor, m_pEditor->m_SelectedGroup, GameLayerIndex, ETilesProp::PROP_HEIGHT, PrevH, NewH));
const std::shared_ptr<CEditorActionEditLayerTilesProp> &Action2 = std::static_pointer_cast<CEditorActionEditLayerTilesProp>(vpActions[vpActions.size() - 1]);
Action1->SetSavedLayers(savedLayers);
Action2->SetSavedLayers(savedLayers);
}
2023-10-02 13:14:38 +00:00
int Changes = 0;
for(int y = OffsetY < 0 ? -OffsetY : 0; y < m_Height; y++)
2023-04-01 21:16:55 +00:00
{
for(int x = OffsetX < 0 ? -OffsetX : 0; x < m_Width; x++)
2023-04-01 21:16:55 +00:00
{
if(GetTile(x, y).m_Index)
{
2023-04-01 21:16:55 +00:00
const CTile ResultTile = {(unsigned char)Result};
pGLayer->SetTile(x + OffsetX, y + OffsetY, ResultTile);
2023-10-02 13:14:38 +00:00
Changes++;
}
2023-04-01 21:16:55 +00:00
}
}
2023-10-02 13:14:38 +00:00
vpActions.push_back(std::make_shared<CEditorBrushDrawAction>(m_pEditor, m_pEditor->m_SelectedGroup));
char aDisplay[256];
str_format(aDisplay, sizeof(aDisplay), "Construct '%s' game tiles (x%d)", s_apGametileOpNames[Result], Changes);
m_pEditor->m_EditorHistory.RecordAction(std::make_shared<CEditorActionBulk>(m_pEditor, vpActions, aDisplay, true));
}
else
{
if(!m_pEditor->m_Map.m_pTeleLayer)
{
2023-10-02 13:14:38 +00:00
std::shared_ptr<CLayerTele> pLayer = std::make_shared<CLayerTele>(m_pEditor, m_Width, m_Height);
m_pEditor->m_Map.MakeTeleLayer(pLayer);
m_pEditor->m_Map.m_pGameGroup->AddLayer(pLayer);
2023-10-02 13:14:38 +00:00
vpActions.push_back(std::make_shared<CEditorActionAddLayer>(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_Map.m_pGameGroup->m_vpLayers.size() - 1));
if(m_Width != pGLayer->m_Width || m_Height > pGLayer->m_Height)
{
std::map<int, std::shared_ptr<CLayer>> savedLayers;
savedLayers[LAYERTYPE_TILES] = pGLayer->Duplicate();
savedLayers[LAYERTYPE_GAME] = savedLayers[LAYERTYPE_TILES];
int NewW = pGLayer->m_Width;
int NewH = pGLayer->m_Height;
if(m_Width > pGLayer->m_Width)
{
NewW = m_Width;
}
if(m_Height > pGLayer->m_Height)
{
NewH = m_Height;
}
int PrevW = pGLayer->m_Width;
int PrevH = pGLayer->m_Height;
pLayer->Resize(NewW, NewH);
vpActions.push_back(std::make_shared<CEditorActionEditLayerTilesProp>(m_pEditor, m_pEditor->m_SelectedGroup, GameLayerIndex, ETilesProp::PROP_WIDTH, PrevW, NewW));
const std::shared_ptr<CEditorActionEditLayerTilesProp> &Action1 = std::static_pointer_cast<CEditorActionEditLayerTilesProp>(vpActions[vpActions.size() - 1]);
vpActions.push_back(std::make_shared<CEditorActionEditLayerTilesProp>(m_pEditor, m_pEditor->m_SelectedGroup, GameLayerIndex, ETilesProp::PROP_HEIGHT, PrevH, NewH));
const std::shared_ptr<CEditorActionEditLayerTilesProp> &Action2 = std::static_pointer_cast<CEditorActionEditLayerTilesProp>(vpActions[vpActions.size() - 1]);
Action1->SetSavedLayers(savedLayers);
Action2->SetSavedLayers(savedLayers);
}
}
std::shared_ptr<CLayerTele> pTLayer = m_pEditor->m_Map.m_pTeleLayer;
2023-10-02 13:14:38 +00:00
int TeleLayerIndex = std::find(m_pEditor->m_Map.m_pGameGroup->m_vpLayers.begin(), m_pEditor->m_Map.m_pGameGroup->m_vpLayers.end(), pTLayer) - m_pEditor->m_Map.m_pGameGroup->m_vpLayers.begin();
if(pTLayer->m_Width < m_Width + OffsetX || pTLayer->m_Height < m_Height + OffsetY)
{
2023-10-02 13:14:38 +00:00
std::map<int, std::shared_ptr<CLayer>> savedLayers;
savedLayers[LAYERTYPE_TILES] = pTLayer->Duplicate();
savedLayers[LAYERTYPE_TELE] = savedLayers[LAYERTYPE_TILES];
int PrevW = pTLayer->m_Width;
int PrevH = pTLayer->m_Height;
int NewW = pTLayer->m_Width < m_Width + OffsetX ? m_Width + OffsetX : pTLayer->m_Width;
int NewH = pTLayer->m_Height < m_Height + OffsetY ? m_Height + OffsetY : pTLayer->m_Height;
pTLayer->Resize(NewW, NewH);
2023-10-02 13:14:38 +00:00
std::shared_ptr<CEditorActionEditLayerTilesProp> Action1, Action2;
vpActions.push_back(Action1 = std::make_shared<CEditorActionEditLayerTilesProp>(m_pEditor, m_pEditor->m_SelectedGroup, TeleLayerIndex, ETilesProp::PROP_WIDTH, PrevW, NewW));
vpActions.push_back(Action2 = std::make_shared<CEditorActionEditLayerTilesProp>(m_pEditor, m_pEditor->m_SelectedGroup, TeleLayerIndex, ETilesProp::PROP_HEIGHT, PrevH, NewH));
Action1->SetSavedLayers(savedLayers);
Action2->SetSavedLayers(savedLayers);
}
2023-10-02 13:14:38 +00:00
int Changes = 0;
for(int y = OffsetY < 0 ? -OffsetY : 0; y < m_Height; y++)
2023-04-01 21:16:55 +00:00
{
for(int x = OffsetX < 0 ? -OffsetX : 0; x < m_Width; x++)
2023-04-01 21:16:55 +00:00
{
if(GetTile(x, y).m_Index)
{
2023-10-02 13:14:38 +00:00
auto TileIndex = (y + OffsetY) * pTLayer->m_Width + x + OffsetX;
Changes++;
STeleTileStateChange::SData Previous{
pTLayer->m_pTeleTile[TileIndex].m_Number,
pTLayer->m_pTeleTile[TileIndex].m_Type,
pTLayer->m_pTiles[TileIndex].m_Index};
pTLayer->m_pTiles[TileIndex].m_Index = TILE_AIR + Result;
pTLayer->m_pTeleTile[TileIndex].m_Number = 1;
pTLayer->m_pTeleTile[TileIndex].m_Type = TILE_AIR + Result;
STeleTileStateChange::SData Current{
pTLayer->m_pTeleTile[TileIndex].m_Number,
pTLayer->m_pTeleTile[TileIndex].m_Type,
pTLayer->m_pTiles[TileIndex].m_Index};
pTLayer->RecordStateChange(x, y, Previous, Current);
}
2023-04-01 21:16:55 +00:00
}
}
2023-10-02 13:14:38 +00:00
vpActions.push_back(std::make_shared<CEditorBrushDrawAction>(m_pEditor, m_pEditor->m_SelectedGroup));
char aDisplay[256];
str_format(aDisplay, sizeof(aDisplay), "Construct 'tele' game tiles (x%d)", Changes);
m_pEditor->m_EditorHistory.RecordAction(std::make_shared<CEditorActionBulk>(m_pEditor, vpActions, aDisplay, true));
}
}
2008-01-13 22:03:32 +00:00
}
if(m_pEditor->m_Map.m_pGameLayer.get() != this)
{
2023-04-01 21:16:55 +00:00
if(m_Image >= 0 && (size_t)m_Image < m_pEditor->m_Map.m_vpImages.size() && m_pEditor->m_Map.m_vpImages[m_Image]->m_AutoMapper.IsLoaded() && m_AutoMapperConfig != -1)
{
pToolBox->HSplitBottom(2.0f, pToolBox, nullptr);
pToolBox->HSplitBottom(12.0f, pToolBox, &Button);
if(m_Seed != 0)
{
CUIRect ButtonAuto;
Button.VSplitRight(16.0f, &Button, &ButtonAuto);
Button.VSplitRight(2.0f, &Button, nullptr);
2023-04-01 21:16:55 +00:00
static int s_AutoMapperButtonAuto = 0;
if(m_pEditor->DoButton_Editor(&s_AutoMapperButtonAuto, "A", m_AutoAutoMap, &ButtonAuto, 0, "Automatically run automap after modifications."))
{
m_AutoAutoMap = !m_AutoAutoMap;
FlagModified(0, 0, m_Width, m_Height);
2023-10-02 13:14:38 +00:00
if(!m_TilesHistory.empty()) // Sometimes pressing that button causes the automap to run so we should be able to undo that
{
// record undo
m_pEditor->m_EditorHistory.RecordAction(std::make_shared<CEditorActionTileChanges>(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], "Auto map", m_TilesHistory));
2023-10-02 13:14:38 +00:00
ClearHistory();
}
}
}
2023-04-01 21:16:55 +00:00
static int s_AutoMapperButton = 0;
if(m_pEditor->DoButton_Editor(&s_AutoMapperButton, "Automap", 0, &Button, 0, "Run the automapper"))
{
m_pEditor->m_Map.m_vpImages[m_Image]->m_AutoMapper.Proceed(this, m_AutoMapperConfig, m_Seed);
2023-10-02 13:14:38 +00:00
// record undo
m_pEditor->m_EditorHistory.RecordAction(std::make_shared<CEditorActionTileChanges>(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], "Auto map", m_TilesHistory));
2023-10-02 13:14:38 +00:00
ClearHistory();
return CUI::POPUP_CLOSE_CURRENT;
}
}
}
2023-10-02 13:14:38 +00:00
int Color = PackColor(m_Color);
2010-05-29 07:25:38 +00:00
CProperty aProps[] = {
2019-04-17 19:48:23 +00:00
{"Width", m_Width, PROPTYPE_INT_SCROLL, 1, 100000},
{"Height", m_Height, PROPTYPE_INT_SCROLL, 1, 100000},
2011-03-20 16:04:03 +00:00
{"Shift", 0, PROPTYPE_SHIFT, 0, 0},
2019-04-17 19:48:23 +00:00
{"Shift by", m_pEditor->m_ShiftBy, PROPTYPE_INT_SCROLL, 1, 100000},
{"Image", m_Image, PROPTYPE_IMAGE, 0, 0},
2011-03-20 16:04:03 +00:00
{"Color", Color, PROPTYPE_COLOR, 0, 0},
{"Color Env", m_ColorEnv + 1, PROPTYPE_ENVELOPE, 0, 0},
{"Color TO", m_ColorEnvOffset, PROPTYPE_INT_SCROLL, -1000000, 1000000},
{"Auto Rule", m_AutoMapperConfig, PROPTYPE_AUTOMAPPER, m_Image, 0},
{"Seed", m_Seed, PROPTYPE_INT_SCROLL, 0, 1000000000},
{nullptr},
2008-01-12 12:27:55 +00:00
};
if(EntitiesLayer) // remove the image and color properties if this is a game layer
{
2023-10-02 13:14:38 +00:00
aProps[(int)ETilesProp::PROP_IMAGE].m_pName = nullptr;
aProps[(int)ETilesProp::PROP_COLOR].m_pName = nullptr;
aProps[(int)ETilesProp::PROP_AUTOMAPPER].m_pName = nullptr;
}
if(m_Image == -1)
{
2023-10-02 13:14:38 +00:00
aProps[(int)ETilesProp::PROP_AUTOMAPPER].m_pName = nullptr;
aProps[(int)ETilesProp::PROP_SEED].m_pName = nullptr;
}
2023-10-02 13:14:38 +00:00
static int s_aIds[(int)ETilesProp::NUM_PROPS] = {0};
2010-05-29 07:25:38 +00:00
int NewVal = 0;
2023-10-02 13:14:38 +00:00
auto [State, Prop] = m_pEditor->DoPropertiesWithState<ETilesProp>(pToolBox, aProps, s_aIds, &NewVal);
2023-10-02 13:14:38 +00:00
static CLayerTilesPropTracker s_Tracker(m_pEditor);
s_Tracker.Begin(this, Prop, State);
if(Prop == ETilesProp::PROP_WIDTH && NewVal > 1)
{
if(NewVal > 1000 && !m_pEditor->m_LargeLayerWasWarned)
{
m_pEditor->m_PopupEventType = CEditor::POPEVENT_LARGELAYER;
m_pEditor->m_PopupEventActivated = true;
m_pEditor->m_LargeLayerWasWarned = true;
}
2010-05-29 07:25:38 +00:00
Resize(NewVal, m_Height);
}
2023-10-02 13:14:38 +00:00
else if(Prop == ETilesProp::PROP_HEIGHT && NewVal > 1)
{
if(NewVal > 1000 && !m_pEditor->m_LargeLayerWasWarned)
{
m_pEditor->m_PopupEventType = CEditor::POPEVENT_LARGELAYER;
m_pEditor->m_PopupEventActivated = true;
m_pEditor->m_LargeLayerWasWarned = true;
}
2010-05-29 07:25:38 +00:00
Resize(m_Width, NewVal);
}
2023-10-02 13:14:38 +00:00
else if(Prop == ETilesProp::PROP_SHIFT)
2023-04-01 21:16:55 +00:00
{
Shift(NewVal);
2023-04-01 21:16:55 +00:00
}
2023-10-02 13:14:38 +00:00
else if(Prop == ETilesProp::PROP_SHIFT_BY)
2023-04-01 21:16:55 +00:00
{
m_pEditor->m_ShiftBy = NewVal;
2023-04-01 21:16:55 +00:00
}
2023-10-02 13:14:38 +00:00
else if(Prop == ETilesProp::PROP_IMAGE)
2008-09-07 08:30:49 +00:00
{
m_Image = NewVal;
if(NewVal == -1)
2008-09-07 08:30:49 +00:00
{
m_Image = -1;
2008-09-07 08:30:49 +00:00
}
else
{
m_Image = NewVal % m_pEditor->m_Map.m_vpImages.size();
m_AutoMapperConfig = -1;
2020-06-20 23:14:36 +00:00
if(m_pEditor->m_Map.m_vpImages[m_Image]->m_Width % 16 != 0 || m_pEditor->m_Map.m_vpImages[m_Image]->m_Height % 16 != 0)
2020-06-20 23:14:36 +00:00
{
m_pEditor->m_PopupEventType = CEditor::POPEVENT_IMAGEDIV16;
2020-06-20 23:14:36 +00:00
m_pEditor->m_PopupEventActivated = true;
m_Image = -1;
2020-06-20 23:14:36 +00:00
}
}
2008-09-07 08:30:49 +00:00
}
2023-10-02 13:14:38 +00:00
else if(Prop == ETilesProp::PROP_COLOR)
{
m_Color.r = (NewVal >> 24) & 0xff;
m_Color.g = (NewVal >> 16) & 0xff;
m_Color.b = (NewVal >> 8) & 0xff;
m_Color.a = NewVal & 0xff;
}
2023-10-02 13:14:38 +00:00
else if(Prop == ETilesProp::PROP_COLOR_ENV)
{
int Index = clamp(NewVal - 1, -1, (int)m_pEditor->m_Map.m_vpEnvelopes.size() - 1);
2023-04-01 21:16:55 +00:00
const int Step = (Index - m_ColorEnv) % 2;
if(Step != 0)
{
for(; Index >= -1 && Index < (int)m_pEditor->m_Map.m_vpEnvelopes.size(); Index += Step)
2023-04-01 21:16:55 +00:00
{
if(Index == -1 || m_pEditor->m_Map.m_vpEnvelopes[Index]->GetChannels() == 4)
{
m_ColorEnv = Index;
break;
}
2023-04-01 21:16:55 +00:00
}
}
}
2023-10-02 13:14:38 +00:00
else if(Prop == ETilesProp::PROP_COLOR_ENV_OFFSET)
2023-04-01 21:16:55 +00:00
{
m_ColorEnvOffset = NewVal;
2023-04-01 21:16:55 +00:00
}
2023-10-02 13:14:38 +00:00
else if(Prop == ETilesProp::PROP_SEED)
2023-04-01 21:16:55 +00:00
{
m_Seed = NewVal;
2023-04-01 21:16:55 +00:00
}
2023-10-02 13:14:38 +00:00
else if(Prop == ETilesProp::PROP_AUTOMAPPER)
{
if(m_Image >= 0 && m_pEditor->m_Map.m_vpImages[m_Image]->m_AutoMapper.ConfigNamesNum() > 0 && NewVal >= 0)
m_AutoMapperConfig = NewVal % m_pEditor->m_Map.m_vpImages[m_Image]->m_AutoMapper.ConfigNamesNum();
else
m_AutoMapperConfig = -1;
}
2023-04-01 21:16:55 +00:00
if(Prop != ETilesProp::PROP_NONE && Prop != ETilesProp::PROP_SHIFT_BY)
{
FlagModified(0, 0, m_Width, m_Height);
}
2023-10-02 13:14:38 +00:00
s_Tracker.End(Prop, State);
return CUI::POPUP_KEEP_OPEN;
2008-01-12 12:27:55 +00:00
}
2008-01-19 12:32:08 +00:00
2023-10-02 13:14:38 +00:00
CUI::EPopupMenuFunctionResult CLayerTiles::RenderCommonProperties(SCommonPropState &State, CEditor *pEditor, CUIRect *pToolbox, std::vector<std::shared_ptr<CLayerTiles>> &vpLayers, std::vector<int> &vLayerIndices)
{
if(State.m_Modified)
2020-02-28 15:25:27 +00:00
{
CUIRect Commit;
pToolbox->HSplitBottom(20.0f, pToolbox, &Commit);
static int s_CommitButton = 0;
if(pEditor->DoButton_Editor(&s_CommitButton, "Commit", 0, &Commit, 0, "Applies the changes"))
{
2023-10-02 13:14:38 +00:00
bool HasModifiedSize = (State.m_Modified & SCommonPropState::MODIFIED_SIZE) != 0;
bool HasModifiedColor = (State.m_Modified & SCommonPropState::MODIFIED_COLOR) != 0;
std::vector<std::shared_ptr<IEditorAction>> vpActions;
int j = 0;
int GroupIndex = pEditor->m_SelectedGroup;
for(auto &pLayer : vpLayers)
{
2023-10-02 13:14:38 +00:00
int LayerIndex = vLayerIndices[j++];
if(HasModifiedSize)
{
std::map<int, std::shared_ptr<CLayer>> SavedLayers;
SavedLayers[LAYERTYPE_TILES] = pLayer->Duplicate();
if(pLayer->m_Game || pLayer->m_Front || pLayer->m_Switch || pLayer->m_Speedup || pLayer->m_Tune || pLayer->m_Tele)
{ // Need to save all entities layers when any entity layer
if(pEditor->m_Map.m_pFrontLayer && !pLayer->m_Front)
SavedLayers[LAYERTYPE_FRONT] = pEditor->m_Map.m_pFrontLayer->Duplicate();
if(pEditor->m_Map.m_pTeleLayer && !pLayer->m_Tele)
SavedLayers[LAYERTYPE_TELE] = pEditor->m_Map.m_pTeleLayer->Duplicate();
if(pEditor->m_Map.m_pSwitchLayer && !pLayer->m_Switch)
SavedLayers[LAYERTYPE_SWITCH] = pEditor->m_Map.m_pSwitchLayer->Duplicate();
if(pEditor->m_Map.m_pSpeedupLayer && !pLayer->m_Speedup)
SavedLayers[LAYERTYPE_SPEEDUP] = pEditor->m_Map.m_pSpeedupLayer->Duplicate();
if(pEditor->m_Map.m_pTuneLayer && !pLayer->m_Tune)
SavedLayers[LAYERTYPE_TUNE] = pEditor->m_Map.m_pTuneLayer->Duplicate();
if(!pLayer->m_Game)
SavedLayers[LAYERTYPE_GAME] = pEditor->m_Map.m_pGameLayer->Duplicate();
}
int PrevW = pLayer->m_Width;
int PrevH = pLayer->m_Height;
pLayer->Resize(State.m_Width, State.m_Height);
2020-02-28 15:25:27 +00:00
2023-10-02 13:14:38 +00:00
if(PrevW != State.m_Width)
{
std::shared_ptr<CEditorActionEditLayerTilesProp> pAction;
vpActions.push_back(pAction = std::make_shared<CEditorActionEditLayerTilesProp>(pEditor, GroupIndex, LayerIndex, ETilesProp::PROP_WIDTH, PrevW, State.m_Width));
pAction->SetSavedLayers(SavedLayers);
}
if(PrevH != State.m_Height)
{
std::shared_ptr<CEditorActionEditLayerTilesProp> pAction;
vpActions.push_back(pAction = std::make_shared<CEditorActionEditLayerTilesProp>(pEditor, GroupIndex, LayerIndex, ETilesProp::PROP_HEIGHT, PrevH, State.m_Height));
pAction->SetSavedLayers(SavedLayers);
}
}
if(HasModifiedColor && !pLayer->IsEntitiesLayer())
{
2023-10-02 13:14:38 +00:00
int Color = 0;
Color |= pLayer->m_Color.r << 24;
Color |= pLayer->m_Color.g << 16;
Color |= pLayer->m_Color.b << 8;
Color |= pLayer->m_Color.a;
pLayer->m_Color.r = (State.m_Color >> 24) & 0xff;
pLayer->m_Color.g = (State.m_Color >> 16) & 0xff;
pLayer->m_Color.b = (State.m_Color >> 8) & 0xff;
pLayer->m_Color.a = State.m_Color & 0xff;
2023-10-02 13:14:38 +00:00
vpActions.push_back(std::make_shared<CEditorActionEditLayerTilesProp>(pEditor, GroupIndex, LayerIndex, ETilesProp::PROP_COLOR, Color, State.m_Color));
}
2020-02-28 15:25:27 +00:00
pLayer->FlagModified(0, 0, pLayer->m_Width, pLayer->m_Height);
}
State.m_Modified = 0;
2023-10-02 13:14:38 +00:00
char aDisplay[256];
str_format(aDisplay, sizeof(aDisplay), "Edit %d layers common properties: %s", (int)vpLayers.size(), HasModifiedColor && HasModifiedSize ? "color, size" : (HasModifiedColor ? "color" : "size"));
pEditor->m_EditorHistory.RecordAction(std::make_shared<CEditorActionBulk>(pEditor, vpActions, aDisplay));
2020-02-28 15:25:27 +00:00
}
}
else
{
for(auto &pLayer : vpLayers)
{
if(pLayer->m_Width > State.m_Width)
State.m_Width = pLayer->m_Width;
if(pLayer->m_Height > State.m_Height)
State.m_Height = pLayer->m_Height;
}
2020-02-28 15:25:27 +00:00
}
2020-02-27 15:02:36 +00:00
{
CUIRect Warning;
pToolbox->HSplitTop(13.0f, &Warning, pToolbox);
Warning.HMargin(0.5f, &Warning);
pEditor->TextRender()->TextColor(ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
2022-03-11 16:34:48 +00:00
SLabelProperties Props;
Props.m_MaxWidth = Warning.w;
pEditor->UI()->DoLabel(&Warning, "Editing multiple layers", 9.0f, TEXTALIGN_ML, Props);
2020-02-27 15:02:36 +00:00
pEditor->TextRender()->TextColor(ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
pToolbox->HSplitTop(2.0f, nullptr, pToolbox);
2020-02-27 15:02:36 +00:00
}
CProperty aProps[] = {
{"Width", State.m_Width, PROPTYPE_INT_SCROLL, 1, 100000},
{"Height", State.m_Height, PROPTYPE_INT_SCROLL, 1, 100000},
{"Shift", 0, PROPTYPE_SHIFT, 0, 0},
2020-02-28 12:33:32 +00:00
{"Shift by", pEditor->m_ShiftBy, PROPTYPE_INT_SCROLL, 1, 100000},
{"Color", State.m_Color, PROPTYPE_COLOR, 0, 0},
{nullptr},
};
2023-10-02 13:14:38 +00:00
static int s_aIds[(int)ETilesCommonProp::NUM_PROPS] = {0};
int NewVal = 0;
2023-10-02 13:14:38 +00:00
auto [PropState, Prop] = pEditor->DoPropertiesWithState<ETilesCommonProp>(pToolbox, aProps, s_aIds, &NewVal);
static CLayerTilesCommonPropTracker s_Tracker(pEditor);
s_Tracker.m_vpLayers = vpLayers;
s_Tracker.m_vLayerIndices = vLayerIndices;
2023-10-02 13:14:38 +00:00
s_Tracker.Begin(nullptr, Prop, PropState);
if(Prop == ETilesCommonProp::PROP_WIDTH && NewVal > 1)
{
if(NewVal > 1000 && !pEditor->m_LargeLayerWasWarned)
{
pEditor->m_PopupEventType = CEditor::POPEVENT_LARGELAYER;
pEditor->m_PopupEventActivated = true;
pEditor->m_LargeLayerWasWarned = true;
}
State.m_Width = NewVal;
}
2023-10-02 13:14:38 +00:00
else if(Prop == ETilesCommonProp::PROP_HEIGHT && NewVal > 1)
{
if(NewVal > 1000 && !pEditor->m_LargeLayerWasWarned)
{
pEditor->m_PopupEventType = CEditor::POPEVENT_LARGELAYER;
pEditor->m_PopupEventActivated = true;
pEditor->m_LargeLayerWasWarned = true;
}
State.m_Height = NewVal;
}
2023-10-02 13:14:38 +00:00
else if(Prop == ETilesCommonProp::PROP_SHIFT)
{
for(auto &pLayer : vpLayers)
pLayer->Shift(NewVal);
}
2023-10-02 13:14:38 +00:00
else if(Prop == ETilesCommonProp::PROP_SHIFT_BY)
2023-04-01 21:16:55 +00:00
{
2020-02-28 12:33:32 +00:00
pEditor->m_ShiftBy = NewVal;
2023-04-01 21:16:55 +00:00
}
2023-10-02 13:14:38 +00:00
else if(Prop == ETilesCommonProp::PROP_COLOR)
2020-02-27 15:02:36 +00:00
{
State.m_Color = NewVal;
2020-02-27 15:02:36 +00:00
}
2023-10-02 13:14:38 +00:00
s_Tracker.End(Prop, PropState);
if(Prop == ETilesCommonProp::PROP_WIDTH || Prop == ETilesCommonProp::PROP_HEIGHT)
2023-04-01 21:16:55 +00:00
{
State.m_Modified |= SCommonPropState::MODIFIED_SIZE;
2023-04-01 21:16:55 +00:00
}
2023-10-02 13:14:38 +00:00
else if(Prop == ETilesCommonProp::PROP_COLOR)
2023-04-01 21:16:55 +00:00
{
State.m_Modified |= SCommonPropState::MODIFIED_COLOR;
2023-04-01 21:16:55 +00:00
}
return CUI::POPUP_KEEP_OPEN;
}
void CLayerTiles::FlagModified(int x, int y, int w, int h)
{
Autosave copy of current editor map periodically to `auto` folder A copy of the map currently open in the editor is saved every 10 minutes to the `maps/auto` folder (interval configurable, see below). The automatically saved map uses the filename of the original map with an additional timestamp. Per map name 10 autosaves are kept in the `auto` folder before old autosaves will be deleted (number configurable, see below). Add config variable `ed_autosave_interval` (0 - 240, default 10) to configure the interval in minutes at which a copy of the current editor map is automatically saved to the 'auto' folder. Add config variable `ed_autosave_max` (0 - 1000, default 10) to configure the maximum number of autosaves that are kept per map name (0 = no limit). Autosaving will not take place in the 5 seconds immediately after the map was last modified by the user, to avoid interrupting the user with the autosave. This will only delay autosaving for up to 1 minute though, so autosaves are not prevented entirely, should the user continuously edit the map. When the editor is reopened after being closed for more than 10 seconds, the autosave timer will be adjusted to compensate for the time that was not spent on editing in the editor. When the map is saved manually by the user the autosave file is also updated, if it's outdated by at least half of the configured autosave interval. This ensures that autosaves are always available as a periodic backup of the map. When a copy of the current map is saved, this does not update the autosave and will also no longer reset the modified state. The modified state should reflect whether changes have been made that are not saved to the current map file. As saving a copy does not update the current file, the modified state should not be reset in this case. Closes #6693.
2023-06-23 15:39:05 +00:00
m_pEditor->m_Map.OnModify();
if(m_Seed != 0 && m_AutoMapperConfig != -1 && m_AutoAutoMap && m_Image >= 0)
{
m_pEditor->m_Map.m_vpImages[m_Image]->m_AutoMapper.ProceedLocalized(this, m_AutoMapperConfig, m_Seed, x, y, w, h);
}
}
void CLayerTiles::ModifyImageIndex(FIndexModifyFunction Func)
2008-01-19 12:32:08 +00:00
{
2010-05-29 07:25:38 +00:00
Func(&m_Image);
2008-01-19 12:32:08 +00:00
}
void CLayerTiles::ModifyEnvelopeIndex(FIndexModifyFunction Func)
2008-01-19 12:32:08 +00:00
{
Func(&m_ColorEnv);
2008-01-19 12:32:08 +00:00
}
void CLayerTiles::ShowPreventUnusedTilesWarning()
{
if(!m_pEditor->m_PreventUnusedTilesWasWarned)
{
m_pEditor->m_PopupEventType = CEditor::POPEVENT_PREVENTUNUSEDTILES;
m_pEditor->m_PopupEventActivated = true;
m_pEditor->m_PreventUnusedTilesWasWarned = true;
}
}