1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3#include "layer_tiles.h"
4
5#include <engine/keys.h>
6#include <engine/shared/map.h>
7#include <game/editor/editor.h>
8#include <game/editor/editor_actions.h>
9
10#include <iterator>
11#include <numeric>
12
13#include "image.h"
14
15CLayerTiles::CLayerTiles(CEditor *pEditor, int w, int h) :
16 CLayer(pEditor)
17{
18 m_Type = LAYERTYPE_TILES;
19 m_aName[0] = '\0';
20 m_Width = w;
21 m_Height = h;
22 m_Image = -1;
23 m_Game = 0;
24 m_Color.r = 255;
25 m_Color.g = 255;
26 m_Color.b = 255;
27 m_Color.a = 255;
28 m_ColorEnv = -1;
29 m_ColorEnvOffset = 0;
30
31 m_Tele = 0;
32 m_Speedup = 0;
33 m_Front = 0;
34 m_Switch = 0;
35 m_Tune = 0;
36 m_AutoMapperConfig = -1;
37 m_Seed = 0;
38 m_AutoAutoMap = false;
39
40 m_pTiles = new CTile[m_Width * m_Height];
41 mem_zero(block: m_pTiles, size: (size_t)m_Width * m_Height * sizeof(CTile));
42}
43
44CLayerTiles::CLayerTiles(const CLayerTiles &Other) :
45 CLayer(Other)
46{
47 m_Width = Other.m_Width;
48 m_Height = Other.m_Height;
49 m_pTiles = new CTile[m_Width * m_Height];
50 mem_copy(dest: m_pTiles, source: Other.m_pTiles, size: (size_t)m_Width * m_Height * sizeof(CTile));
51
52 m_Image = Other.m_Image;
53 m_Game = Other.m_Game;
54 m_Color = Other.m_Color;
55 m_ColorEnv = Other.m_ColorEnv;
56 m_ColorEnvOffset = Other.m_ColorEnvOffset;
57
58 m_AutoMapperConfig = Other.m_AutoMapperConfig;
59 m_Seed = Other.m_Seed;
60 m_AutoAutoMap = Other.m_AutoAutoMap;
61 m_Tele = Other.m_Tele;
62 m_Speedup = Other.m_Speedup;
63 m_Front = Other.m_Front;
64 m_Switch = Other.m_Switch;
65 m_Tune = Other.m_Tune;
66
67 mem_copy(dest: m_aFileName, source: Other.m_aFileName, size: IO_MAX_PATH_LENGTH);
68}
69
70CLayerTiles::~CLayerTiles()
71{
72 delete[] m_pTiles;
73}
74
75CTile CLayerTiles::GetTile(int x, int y)
76{
77 return m_pTiles[y * m_Width + x];
78}
79
80void CLayerTiles::SetTile(int x, int y, CTile Tile)
81{
82 auto CurrentTile = m_pTiles[y * m_Width + x];
83 SetTileIgnoreHistory(x, y, Tile);
84 RecordStateChange(x, y, Previous: CurrentTile, Tile);
85}
86
87void CLayerTiles::SetTileIgnoreHistory(int x, int y, CTile Tile) const
88{
89 m_pTiles[y * m_Width + x] = Tile;
90}
91
92void CLayerTiles::RecordStateChange(int x, int y, CTile Previous, CTile Tile)
93{
94 if(!m_TilesHistory[y][x].m_Changed)
95 m_TilesHistory[y][x] = STileStateChange{.m_Changed: true, .m_Previous: Previous, .m_Current: Tile};
96 else
97 m_TilesHistory[y][x].m_Current = Tile;
98}
99
100void CLayerTiles::PrepareForSave()
101{
102 for(int y = 0; y < m_Height; y++)
103 for(int x = 0; x < m_Width; x++)
104 m_pTiles[y * m_Width + x].m_Flags &= TILEFLAG_XFLIP | TILEFLAG_YFLIP | TILEFLAG_ROTATE;
105
106 if(m_Image != -1 && m_Color.a == 255)
107 {
108 for(int y = 0; y < m_Height; y++)
109 for(int x = 0; x < m_Width; x++)
110 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];
111 }
112}
113
114void CLayerTiles::ExtractTiles(int TilemapItemVersion, const CTile *pSavedTiles, size_t SavedTilesSize) const
115{
116 const size_t DestSize = (size_t)m_Width * m_Height;
117 if(TilemapItemVersion >= CMapItemLayerTilemap::TILE_SKIP_MIN_VERSION)
118 CMap::ExtractTiles(pDest: m_pTiles, DestSize, pSrc: pSavedTiles, SrcSize: SavedTilesSize);
119 else if(SavedTilesSize >= DestSize)
120 mem_copy(dest: m_pTiles, source: pSavedTiles, size: DestSize * sizeof(CTile));
121}
122
123void CLayerTiles::MakePalette() const
124{
125 for(int y = 0; y < m_Height; y++)
126 for(int x = 0; x < m_Width; x++)
127 m_pTiles[y * m_Width + x].m_Index = y * 16 + x;
128}
129
130void CLayerTiles::Render(bool Tileset)
131{
132 IGraphics::CTextureHandle Texture;
133 if(m_Image >= 0 && (size_t)m_Image < m_pEditor->m_Map.m_vpImages.size())
134 Texture = m_pEditor->m_Map.m_vpImages[m_Image]->m_Texture;
135 else if(m_Game)
136 Texture = m_pEditor->GetEntitiesTexture();
137 else if(m_Front)
138 Texture = m_pEditor->GetFrontTexture();
139 else if(m_Tele)
140 Texture = m_pEditor->GetTeleTexture();
141 else if(m_Speedup)
142 Texture = m_pEditor->GetSpeedupTexture();
143 else if(m_Switch)
144 Texture = m_pEditor->GetSwitchTexture();
145 else if(m_Tune)
146 Texture = m_pEditor->GetTuneTexture();
147 Graphics()->TextureSet(Texture);
148
149 ColorRGBA ColorEnv = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
150 CEditor::EnvelopeEval(TimeOffsetMillis: m_ColorEnvOffset, Env: m_ColorEnv, Result&: ColorEnv, Channels: 4, pUser: m_pEditor);
151 const ColorRGBA Color = ColorRGBA(m_Color.r / 255.0f, m_Color.g / 255.0f, m_Color.b / 255.0f, m_Color.a / 255.0f).Multiply(Other: ColorEnv);
152
153 Graphics()->BlendNone();
154 m_pEditor->RenderTools()->RenderTilemap(pTiles: m_pTiles, w: m_Width, h: m_Height, Scale: 32.0f, Color, RenderFlags: LAYERRENDERFLAG_OPAQUE);
155 Graphics()->BlendNormal();
156 m_pEditor->RenderTools()->RenderTilemap(pTiles: m_pTiles, w: m_Width, h: m_Height, Scale: 32.0f, Color, RenderFlags: LAYERRENDERFLAG_TRANSPARENT);
157
158 // Render DDRace Layers
159 if(!Tileset)
160 {
161 if(m_Tele)
162 m_pEditor->RenderTools()->RenderTeleOverlay(pTele: static_cast<CLayerTele *>(this)->m_pTeleTile, w: m_Width, h: m_Height, Scale: 32.0f);
163 if(m_Speedup)
164 m_pEditor->RenderTools()->RenderSpeedupOverlay(pSpeedup: static_cast<CLayerSpeedup *>(this)->m_pSpeedupTile, w: m_Width, h: m_Height, Scale: 32.0f);
165 if(m_Switch)
166 m_pEditor->RenderTools()->RenderSwitchOverlay(pSwitch: static_cast<CLayerSwitch *>(this)->m_pSwitchTile, w: m_Width, h: m_Height, Scale: 32.0f);
167 if(m_Tune)
168 m_pEditor->RenderTools()->RenderTuneOverlay(pTune: static_cast<CLayerTune *>(this)->m_pTuneTile, w: m_Width, h: m_Height, Scale: 32.0f);
169 }
170}
171
172int CLayerTiles::ConvertX(float x) const { return (int)(x / 32.0f); }
173int CLayerTiles::ConvertY(float y) const { return (int)(y / 32.0f); }
174
175void CLayerTiles::Convert(CUIRect Rect, RECTi *pOut) const
176{
177 pOut->x = ConvertX(x: Rect.x);
178 pOut->y = ConvertY(y: Rect.y);
179 pOut->w = ConvertX(x: Rect.x + Rect.w + 31) - pOut->x;
180 pOut->h = ConvertY(y: Rect.y + Rect.h + 31) - pOut->y;
181}
182
183void CLayerTiles::Snap(CUIRect *pRect) const
184{
185 RECTi Out;
186 Convert(Rect: *pRect, pOut: &Out);
187 pRect->x = Out.x * 32.0f;
188 pRect->y = Out.y * 32.0f;
189 pRect->w = Out.w * 32.0f;
190 pRect->h = Out.h * 32.0f;
191}
192
193void CLayerTiles::Clamp(RECTi *pRect) const
194{
195 if(pRect->x < 0)
196 {
197 pRect->w += pRect->x;
198 pRect->x = 0;
199 }
200
201 if(pRect->y < 0)
202 {
203 pRect->h += pRect->y;
204 pRect->y = 0;
205 }
206
207 if(pRect->x + pRect->w > m_Width)
208 pRect->w = m_Width - pRect->x;
209
210 if(pRect->y + pRect->h > m_Height)
211 pRect->h = m_Height - pRect->y;
212
213 if(pRect->h < 0)
214 pRect->h = 0;
215 if(pRect->w < 0)
216 pRect->w = 0;
217}
218
219bool CLayerTiles::IsEntitiesLayer() const
220{
221 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;
222}
223
224bool CLayerTiles::IsEmpty(const std::shared_ptr<CLayerTiles> &pLayer)
225{
226 for(int y = 0; y < pLayer->m_Height; y++)
227 {
228 for(int x = 0; x < pLayer->m_Width; x++)
229 {
230 int Index = pLayer->GetTile(x, y).m_Index;
231 if(Index)
232 {
233 if(pLayer->m_Game)
234 {
235 if(m_pEditor->m_AllowPlaceUnusedTiles || IsValidGameTile(Index))
236 return false;
237 }
238 else if(pLayer->m_Front)
239 {
240 if(m_pEditor->m_AllowPlaceUnusedTiles || IsValidFrontTile(Index))
241 return false;
242 }
243 else
244 return false;
245 }
246 }
247 }
248
249 return true;
250}
251
252void CLayerTiles::BrushSelecting(CUIRect Rect)
253{
254 Graphics()->TextureClear();
255 Graphics()->QuadsBegin();
256 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.4f);
257 Snap(pRect: &Rect);
258 IGraphics::CQuadItem QuadItem(Rect.x, Rect.y, Rect.w, Rect.h);
259 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
260 Graphics()->QuadsEnd();
261 char aBuf[16];
262 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d⨯%d", ConvertX(x: Rect.w), ConvertY(y: Rect.h));
263 TextRender()->Text(x: Rect.x + 3.0f, y: Rect.y + 3.0f, Size: m_pEditor->m_ShowPicker ? 15.0f : m_pEditor->MapView()->ScaleLength(Value: 15.0f), pText: aBuf, LineWidth: -1.0f);
264}
265
266template<typename T>
267static void InitGrabbedLayer(std::shared_ptr<T> pLayer, CLayerTiles *pThisLayer)
268{
269 pLayer->m_pEditor = pThisLayer->m_pEditor;
270 pLayer->m_Image = pThisLayer->m_Image;
271 pLayer->m_Game = pThisLayer->m_Game;
272 pLayer->m_Front = pThisLayer->m_Front;
273 pLayer->m_Tele = pThisLayer->m_Tele;
274 pLayer->m_Speedup = pThisLayer->m_Speedup;
275 pLayer->m_Switch = pThisLayer->m_Switch;
276 pLayer->m_Tune = pThisLayer->m_Tune;
277 if(pThisLayer->m_pEditor->m_BrushColorEnabled)
278 {
279 pLayer->m_Color = pThisLayer->m_Color;
280 pLayer->m_Color.a = 255;
281 }
282};
283
284int CLayerTiles::BrushGrab(std::shared_ptr<CLayerGroup> pBrush, CUIRect Rect)
285{
286 RECTi r;
287 Convert(Rect, pOut: &r);
288 Clamp(pRect: &r);
289
290 if(!r.w || !r.h)
291 return 0;
292
293 // create new layers
294 if(this->m_Tele)
295 {
296 std::shared_ptr<CLayerTele> pGrabbed = std::make_shared<CLayerTele>(args&: m_pEditor, args&: r.w, args&: r.h);
297 InitGrabbedLayer(pLayer: pGrabbed, pThisLayer: this);
298
299 pBrush->AddLayer(pLayer: pGrabbed);
300
301 // copy the tiles
302 for(int y = 0; y < r.h; y++)
303 for(int x = 0; x < r.w; x++)
304 pGrabbed->m_pTiles[y * pGrabbed->m_Width + x] = GetTile(x: r.x + x, y: r.y + y);
305
306 // copy the tele data
307 if(!m_pEditor->Input()->KeyIsPressed(Key: KEY_SPACE))
308 for(int y = 0; y < r.h; y++)
309 for(int x = 0; x < r.w; x++)
310 {
311 pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x] = static_cast<CLayerTele *>(this)->m_pTeleTile[(r.y + y) * m_Width + (r.x + x)];
312 if(IsValidTeleTile(Index: pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Type))
313 {
314 if(IsTeleTileNumberUsed(Index: pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Type, Checkpoint: false))
315 m_pEditor->m_TeleNumber = pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Number;
316 else if(IsTeleTileNumberUsed(Index: pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Type, Checkpoint: true))
317 m_pEditor->m_TeleCheckpointNumber = pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Number;
318 }
319 }
320
321 pGrabbed->m_TeleNum = m_pEditor->m_TeleNumber;
322 pGrabbed->m_TeleCheckpointNum = m_pEditor->m_TeleCheckpointNumber;
323
324 str_copy(dst&: pGrabbed->m_aFileName, src: m_pEditor->m_aFileName);
325 }
326 else if(this->m_Speedup)
327 {
328 std::shared_ptr<CLayerSpeedup> pGrabbed = std::make_shared<CLayerSpeedup>(args&: m_pEditor, args&: r.w, args&: r.h);
329 InitGrabbedLayer(pLayer: pGrabbed, pThisLayer: this);
330
331 pBrush->AddLayer(pLayer: pGrabbed);
332
333 // copy the tiles
334 for(int y = 0; y < r.h; y++)
335 for(int x = 0; x < r.w; x++)
336 pGrabbed->m_pTiles[y * pGrabbed->m_Width + x] = GetTile(x: r.x + x, y: r.y + y);
337
338 // copy the speedup data
339 if(!m_pEditor->Input()->KeyIsPressed(Key: KEY_SPACE))
340 for(int y = 0; y < r.h; y++)
341 for(int x = 0; x < r.w; x++)
342 {
343 pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x] = static_cast<CLayerSpeedup *>(this)->m_pSpeedupTile[(r.y + y) * m_Width + (r.x + x)];
344 if(IsValidSpeedupTile(Index: pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_Type))
345 {
346 m_pEditor->m_SpeedupAngle = pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_Angle;
347 m_pEditor->m_SpeedupForce = pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_Force;
348 m_pEditor->m_SpeedupMaxSpeed = pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_MaxSpeed;
349 }
350 }
351 pGrabbed->m_SpeedupForce = m_pEditor->m_SpeedupForce;
352 pGrabbed->m_SpeedupMaxSpeed = m_pEditor->m_SpeedupMaxSpeed;
353 pGrabbed->m_SpeedupAngle = m_pEditor->m_SpeedupAngle;
354 str_copy(dst&: pGrabbed->m_aFileName, src: m_pEditor->m_aFileName);
355 }
356 else if(this->m_Switch)
357 {
358 std::shared_ptr<CLayerSwitch> pGrabbed = std::make_shared<CLayerSwitch>(args&: m_pEditor, args&: r.w, args&: r.h);
359 InitGrabbedLayer(pLayer: pGrabbed, pThisLayer: this);
360
361 pBrush->AddLayer(pLayer: pGrabbed);
362
363 // copy the tiles
364 for(int y = 0; y < r.h; y++)
365 for(int x = 0; x < r.w; x++)
366 pGrabbed->m_pTiles[y * pGrabbed->m_Width + x] = GetTile(x: r.x + x, y: r.y + y);
367
368 // copy the switch data
369 if(!m_pEditor->Input()->KeyIsPressed(Key: KEY_SPACE))
370 for(int y = 0; y < r.h; y++)
371 for(int x = 0; x < r.w; x++)
372 {
373 pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x] = static_cast<CLayerSwitch *>(this)->m_pSwitchTile[(r.y + y) * m_Width + (r.x + x)];
374 if(IsValidSwitchTile(Index: pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x].m_Type))
375 {
376 m_pEditor->m_SwitchNum = pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x].m_Number;
377 m_pEditor->m_SwitchDelay = pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x].m_Delay;
378 }
379 }
380 pGrabbed->m_SwitchNumber = m_pEditor->m_SwitchNum;
381 pGrabbed->m_SwitchDelay = m_pEditor->m_SwitchDelay;
382 str_copy(dst&: pGrabbed->m_aFileName, src: m_pEditor->m_aFileName);
383 }
384
385 else if(this->m_Tune)
386 {
387 std::shared_ptr<CLayerTune> pGrabbed = std::make_shared<CLayerTune>(args&: m_pEditor, args&: r.w, args&: r.h);
388 InitGrabbedLayer(pLayer: pGrabbed, pThisLayer: this);
389
390 pBrush->AddLayer(pLayer: pGrabbed);
391
392 // copy the tiles
393 for(int y = 0; y < r.h; y++)
394 for(int x = 0; x < r.w; x++)
395 pGrabbed->m_pTiles[y * pGrabbed->m_Width + x] = GetTile(x: r.x + x, y: r.y + y);
396
397 // copy the tiles
398 if(!m_pEditor->Input()->KeyIsPressed(Key: KEY_SPACE))
399 for(int y = 0; y < r.h; y++)
400 for(int x = 0; x < r.w; x++)
401 {
402 pGrabbed->m_pTuneTile[y * pGrabbed->m_Width + x] = static_cast<CLayerTune *>(this)->m_pTuneTile[(r.y + y) * m_Width + (r.x + x)];
403 if(IsValidTuneTile(Index: pGrabbed->m_pTuneTile[y * pGrabbed->m_Width + x].m_Type))
404 {
405 m_pEditor->m_TuningNum = pGrabbed->m_pTuneTile[y * pGrabbed->m_Width + x].m_Number;
406 }
407 }
408 pGrabbed->m_TuningNumber = m_pEditor->m_TuningNum;
409 str_copy(dst&: pGrabbed->m_aFileName, src: m_pEditor->m_aFileName);
410 }
411 else if(this->m_Front)
412 {
413 std::shared_ptr<CLayerFront> pGrabbed = std::make_shared<CLayerFront>(args&: m_pEditor, args&: r.w, args&: r.h);
414 InitGrabbedLayer(pLayer: pGrabbed, pThisLayer: this);
415
416 pBrush->AddLayer(pLayer: pGrabbed);
417
418 // copy the tiles
419 for(int y = 0; y < r.h; y++)
420 for(int x = 0; x < r.w; x++)
421 pGrabbed->m_pTiles[y * pGrabbed->m_Width + x] = GetTile(x: r.x + x, y: r.y + y);
422 str_copy(dst&: pGrabbed->m_aFileName, src: m_pEditor->m_aFileName);
423 }
424 else
425 {
426 std::shared_ptr<CLayerTiles> pGrabbed = std::make_shared<CLayerFront>(args&: m_pEditor, args&: r.w, args&: r.h);
427 InitGrabbedLayer(pLayer: pGrabbed, pThisLayer: this);
428
429 pBrush->AddLayer(pLayer: pGrabbed);
430
431 // copy the tiles
432 for(int y = 0; y < r.h; y++)
433 for(int x = 0; x < r.w; x++)
434 pGrabbed->m_pTiles[y * pGrabbed->m_Width + x] = GetTile(x: r.x + x, y: r.y + y);
435 str_copy(dst&: pGrabbed->m_aFileName, src: m_pEditor->m_aFileName);
436 }
437
438 return 1;
439}
440
441void CLayerTiles::FillSelection(bool Empty, std::shared_ptr<CLayer> pBrush, CUIRect Rect)
442{
443 if(m_Readonly || (!Empty && pBrush->m_Type != LAYERTYPE_TILES))
444 return;
445
446 Snap(pRect: &Rect);
447
448 int sx = ConvertX(x: Rect.x);
449 int sy = ConvertY(y: Rect.y);
450 int w = ConvertX(x: Rect.w);
451 int h = ConvertY(y: Rect.h);
452
453 std::shared_ptr<CLayerTiles> pLt = std::static_pointer_cast<CLayerTiles>(r: pBrush);
454
455 bool Destructive = m_pEditor->m_BrushDrawDestructive || Empty || IsEmpty(pLayer: pLt);
456
457 for(int y = 0; y < h; y++)
458 {
459 for(int x = 0; x < w; x++)
460 {
461 int fx = x + sx;
462 int fy = y + sy;
463
464 if(fx < 0 || fx >= m_Width || fy < 0 || fy >= m_Height)
465 continue;
466
467 bool HasTile = GetTile(x: fx, y: fy).m_Index;
468 if(!Empty && pLt->GetTile(x: x % pLt->m_Width, y: y % pLt->m_Height).m_Index == TILE_THROUGH_CUT)
469 {
470 if(m_Game && m_pEditor->m_Map.m_pFrontLayer)
471 {
472 HasTile = HasTile || m_pEditor->m_Map.m_pFrontLayer->GetTile(x: fx, y: fy).m_Index;
473 }
474 else if(m_Front)
475 {
476 HasTile = HasTile || m_pEditor->m_Map.m_pGameLayer->GetTile(x: fx, y: fy).m_Index;
477 }
478 }
479
480 if(!Destructive && HasTile)
481 continue;
482
483 SetTile(x: fx, y: fy, Tile: Empty ? CTile{.m_Index: TILE_AIR} : pLt->m_pTiles[(y * pLt->m_Width + x % pLt->m_Width) % (pLt->m_Width * pLt->m_Height)]);
484 }
485 }
486 FlagModified(x: sx, y: sy, w, h);
487}
488
489void CLayerTiles::BrushDraw(std::shared_ptr<CLayer> pBrush, float wx, float wy)
490{
491 if(m_Readonly)
492 return;
493
494 //
495 std::shared_ptr<CLayerTiles> pTileLayer = std::static_pointer_cast<CLayerTiles>(r: pBrush);
496 int sx = ConvertX(x: wx);
497 int sy = ConvertY(y: wy);
498
499 bool Destructive = m_pEditor->m_BrushDrawDestructive || IsEmpty(pLayer: pTileLayer);
500
501 for(int y = 0; y < pTileLayer->m_Height; y++)
502 for(int x = 0; x < pTileLayer->m_Width; x++)
503 {
504 int fx = x + sx;
505 int fy = y + sy;
506
507 if(fx < 0 || fx >= m_Width || fy < 0 || fy >= m_Height)
508 continue;
509
510 bool HasTile = GetTile(x: fx, y: fy).m_Index;
511 if(pTileLayer->GetTile(x, y).m_Index == TILE_THROUGH_CUT)
512 {
513 if(m_Game && m_pEditor->m_Map.m_pFrontLayer)
514 {
515 HasTile = HasTile || m_pEditor->m_Map.m_pFrontLayer->GetTile(x: fx, y: fy).m_Index;
516 }
517 else if(m_Front)
518 {
519 HasTile = HasTile || m_pEditor->m_Map.m_pGameLayer->GetTile(x: fx, y: fy).m_Index;
520 }
521 }
522
523 if(!Destructive && HasTile)
524 continue;
525
526 SetTile(x: fx, y: fy, Tile: pTileLayer->GetTile(x, y));
527 }
528
529 FlagModified(x: sx, y: sy, w: pTileLayer->m_Width, h: pTileLayer->m_Height);
530}
531
532void CLayerTiles::BrushFlipX()
533{
534 BrushFlipXImpl(pTiles: m_pTiles);
535
536 if(m_Tele || m_Speedup || m_Tune)
537 return;
538
539 bool Rotate = !(m_Game || m_Front || m_Switch) || m_pEditor->m_AllowPlaceUnusedTiles;
540 for(int y = 0; y < m_Height; y++)
541 for(int x = 0; x < m_Width; x++)
542 if(!Rotate && !IsRotatableTile(Index: m_pTiles[y * m_Width + x].m_Index))
543 m_pTiles[y * m_Width + x].m_Flags = 0;
544 else
545 m_pTiles[y * m_Width + x].m_Flags ^= (m_pTiles[y * m_Width + x].m_Flags & TILEFLAG_ROTATE) ? TILEFLAG_YFLIP : TILEFLAG_XFLIP;
546}
547
548void CLayerTiles::BrushFlipY()
549{
550 BrushFlipYImpl(pTiles: m_pTiles);
551
552 if(m_Tele || m_Speedup || m_Tune)
553 return;
554
555 bool Rotate = !(m_Game || m_Front || m_Switch) || m_pEditor->m_AllowPlaceUnusedTiles;
556 for(int y = 0; y < m_Height; y++)
557 for(int x = 0; x < m_Width; x++)
558 if(!Rotate && !IsRotatableTile(Index: m_pTiles[y * m_Width + x].m_Index))
559 m_pTiles[y * m_Width + x].m_Flags = 0;
560 else
561 m_pTiles[y * m_Width + x].m_Flags ^= (m_pTiles[y * m_Width + x].m_Flags & TILEFLAG_ROTATE) ? TILEFLAG_XFLIP : TILEFLAG_YFLIP;
562}
563
564void CLayerTiles::BrushRotate(float Amount)
565{
566 int Rotation = (round_to_int(f: 360.0f * Amount / (pi * 2)) / 90) % 4; // 0=0°, 1=90°, 2=180°, 3=270°
567 if(Rotation < 0)
568 Rotation += 4;
569
570 if(Rotation == 1 || Rotation == 3)
571 {
572 // 90° rotation
573 CTile *pTempData = new CTile[m_Width * m_Height];
574 mem_copy(dest: pTempData, source: m_pTiles, size: (size_t)m_Width * m_Height * sizeof(CTile));
575 CTile *pDst = m_pTiles;
576 bool Rotate = !(m_Game || m_Front) || m_pEditor->m_AllowPlaceUnusedTiles;
577 for(int x = 0; x < m_Width; ++x)
578 for(int y = m_Height - 1; y >= 0; --y, ++pDst)
579 {
580 *pDst = pTempData[y * m_Width + x];
581 if(!Rotate && !IsRotatableTile(Index: pDst->m_Index))
582 pDst->m_Flags = 0;
583 else
584 {
585 if(pDst->m_Flags & TILEFLAG_ROTATE)
586 pDst->m_Flags ^= (TILEFLAG_YFLIP | TILEFLAG_XFLIP);
587 pDst->m_Flags ^= TILEFLAG_ROTATE;
588 }
589 }
590
591 std::swap(a&: m_Width, b&: m_Height);
592 delete[] pTempData;
593 }
594
595 if(Rotation == 2 || Rotation == 3)
596 {
597 BrushFlipX();
598 BrushFlipY();
599 }
600}
601
602std::shared_ptr<CLayer> CLayerTiles::Duplicate() const
603{
604 return std::make_shared<CLayerTiles>(args: *this);
605}
606
607const char *CLayerTiles::TypeName() const
608{
609 return "tiles";
610}
611
612void CLayerTiles::Resize(int NewW, int NewH)
613{
614 CTile *pNewData = new CTile[NewW * NewH];
615 mem_zero(block: pNewData, size: (size_t)NewW * NewH * sizeof(CTile));
616
617 // copy old data
618 for(int y = 0; y < minimum(a: NewH, b: m_Height); y++)
619 mem_copy(dest: &pNewData[y * NewW], source: &m_pTiles[y * m_Width], size: minimum(a: m_Width, b: NewW) * sizeof(CTile));
620
621 // replace old
622 delete[] m_pTiles;
623 m_pTiles = pNewData;
624 m_Width = NewW;
625 m_Height = NewH;
626
627 // resize tele layer if available
628 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))
629 m_pEditor->m_Map.m_pTeleLayer->Resize(NewW, NewH);
630
631 // resize speedup layer if available
632 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))
633 m_pEditor->m_Map.m_pSpeedupLayer->Resize(NewW, NewH);
634
635 // resize front layer
636 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))
637 m_pEditor->m_Map.m_pFrontLayer->Resize(NewW, NewH);
638
639 // resize switch layer if available
640 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))
641 m_pEditor->m_Map.m_pSwitchLayer->Resize(NewW, NewH);
642
643 // resize tune layer if available
644 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))
645 m_pEditor->m_Map.m_pTuneLayer->Resize(NewW, NewH);
646}
647
648void CLayerTiles::Shift(int Direction)
649{
650 ShiftImpl(pTiles: m_pTiles, Direction, ShiftBy: m_pEditor->m_ShiftBy);
651}
652
653void CLayerTiles::ShowInfo()
654{
655 float ScreenX0, ScreenY0, ScreenX1, ScreenY1;
656 Graphics()->GetScreen(pTopLeftX: &ScreenX0, pTopLeftY: &ScreenY0, pBottomRightX: &ScreenX1, pBottomRightY: &ScreenY1);
657 Graphics()->TextureSet(Texture: m_pEditor->Client()->GetDebugFont());
658 Graphics()->QuadsBegin();
659
660 int StartY = maximum(a: 0, b: (int)(ScreenY0 / 32.0f) - 1);
661 int StartX = maximum(a: 0, b: (int)(ScreenX0 / 32.0f) - 1);
662 int EndY = minimum(a: (int)(ScreenY1 / 32.0f) + 1, b: m_Height);
663 int EndX = minimum(a: (int)(ScreenX1 / 32.0f) + 1, b: m_Width);
664
665 for(int y = StartY; y < EndY; y++)
666 for(int x = StartX; x < EndX; x++)
667 {
668 int c = x + y * m_Width;
669 if(m_pTiles[c].m_Index)
670 {
671 char aBuf[4];
672 if(m_pEditor->m_ShowTileInfo == CEditor::SHOW_TILE_HEXADECIMAL)
673 {
674 str_hex(dst: aBuf, dst_size: sizeof(aBuf), data: &m_pTiles[c].m_Index, data_size: 1);
675 aBuf[2] = '\0'; // would otherwise be a space
676 }
677 else
678 {
679 str_from_int(value: m_pTiles[c].m_Index, dst&: aBuf);
680 }
681 m_pEditor->Graphics()->QuadsText(x: x * 32, y: y * 32, Size: 16.0f, pText: aBuf);
682
683 char aFlags[4] = {m_pTiles[c].m_Flags & TILEFLAG_XFLIP ? 'X' : ' ',
684 m_pTiles[c].m_Flags & TILEFLAG_YFLIP ? 'Y' : ' ',
685 m_pTiles[c].m_Flags & TILEFLAG_ROTATE ? 'R' : ' ',
686 0};
687 m_pEditor->Graphics()->QuadsText(x: x * 32, y: y * 32 + 16, Size: 16.0f, pText: aFlags);
688 }
689 x += m_pTiles[c].m_Skip;
690 }
691
692 Graphics()->QuadsEnd();
693 Graphics()->MapScreen(TopLeftX: ScreenX0, TopLeftY: ScreenY0, BottomRightX: ScreenX1, BottomRightY: ScreenY1);
694}
695
696CUi::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox)
697{
698 CUIRect Button;
699
700 const bool EntitiesLayer = IsEntitiesLayer();
701
702 std::shared_ptr<CLayerGroup> pGroup = m_pEditor->m_Map.m_vpGroups[m_pEditor->m_SelectedGroup];
703
704 // Game tiles can only be constructed if the layer is relative to the game layer
705 if(!EntitiesLayer && !(pGroup->m_OffsetX % 32) && !(pGroup->m_OffsetY % 32) && pGroup->m_ParallaxX == 100 && pGroup->m_ParallaxY == 100)
706 {
707 pToolBox->HSplitBottom(Cut: 12.0f, pTop: pToolBox, pBottom: &Button);
708 static int s_GameTilesButton = 0;
709 if(m_pEditor->DoButton_Editor(pId: &s_GameTilesButton, pText: "Game tiles", Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Constructs game tiles from this layer"))
710 m_pEditor->PopupSelectGametileOpInvoke(x: m_pEditor->Ui()->MouseX(), y: m_pEditor->Ui()->MouseY());
711 const int Selected = m_pEditor->PopupSelectGameTileOpResult();
712 int Result = Selected;
713 switch(Selected)
714 {
715 case 4:
716 Result = TILE_THROUGH_CUT;
717 break;
718 case 5:
719 Result = TILE_FREEZE;
720 break;
721 case 6:
722 Result = TILE_UNFREEZE;
723 break;
724 case 7:
725 Result = TILE_DFREEZE;
726 break;
727 case 8:
728 Result = TILE_DUNFREEZE;
729 break;
730 case 9:
731 Result = TILE_TELECHECKIN;
732 break;
733 case 10:
734 Result = TILE_TELECHECKINEVIL;
735 break;
736 case 11:
737 Result = TILE_LFREEZE;
738 break;
739 case 12:
740 Result = TILE_LUNFREEZE;
741 break;
742 default:
743 break;
744 }
745 if(Result > -1)
746 {
747 const int OffsetX = -pGroup->m_OffsetX / 32;
748 const int OffsetY = -pGroup->m_OffsetY / 32;
749
750 static const char *s_apGametileOpNames[] = {
751 "Air",
752 "Hookable",
753 "Death",
754 "Unhookable",
755 "Hookthrough",
756 "Freeze",
757 "Unfreeze",
758 "Deep Freeze",
759 "Deep Unfreeze",
760 "Blue Check-Tele",
761 "Red Check-Tele",
762 "Live Freeze",
763 "Live Unfreeze",
764 };
765
766 std::vector<std::shared_ptr<IEditorAction>> vpActions;
767 std::shared_ptr<CLayerTiles> pGLayer = m_pEditor->m_Map.m_pGameLayer;
768 int GameLayerIndex = std::find(first: m_pEditor->m_Map.m_pGameGroup->m_vpLayers.begin(), last: m_pEditor->m_Map.m_pGameGroup->m_vpLayers.end(), val: pGLayer) - m_pEditor->m_Map.m_pGameGroup->m_vpLayers.begin();
769
770 if(Result != TILE_TELECHECKIN && Result != TILE_TELECHECKINEVIL)
771 {
772 if(pGLayer->m_Width < m_Width + OffsetX || pGLayer->m_Height < m_Height + OffsetY)
773 {
774 std::map<int, std::shared_ptr<CLayer>> savedLayers;
775 savedLayers[LAYERTYPE_TILES] = pGLayer->Duplicate();
776 savedLayers[LAYERTYPE_GAME] = savedLayers[LAYERTYPE_TILES];
777
778 int PrevW = pGLayer->m_Width;
779 int PrevH = pGLayer->m_Height;
780 const int NewW = pGLayer->m_Width < m_Width + OffsetX ? m_Width + OffsetX : pGLayer->m_Width;
781 const int NewH = pGLayer->m_Height < m_Height + OffsetY ? m_Height + OffsetY : pGLayer->m_Height;
782 pGLayer->Resize(NewW, NewH);
783 vpActions.push_back(x: std::make_shared<CEditorActionEditLayerTilesProp>(args&: m_pEditor, args&: m_pEditor->m_SelectedGroup, args&: GameLayerIndex, args: ETilesProp::PROP_WIDTH, args&: PrevW, args: NewW));
784 const std::shared_ptr<CEditorActionEditLayerTilesProp> &Action1 = std::static_pointer_cast<CEditorActionEditLayerTilesProp>(r: vpActions[vpActions.size() - 1]);
785 vpActions.push_back(x: std::make_shared<CEditorActionEditLayerTilesProp>(args&: m_pEditor, args&: m_pEditor->m_SelectedGroup, args&: GameLayerIndex, args: ETilesProp::PROP_HEIGHT, args&: PrevH, args: NewH));
786 const std::shared_ptr<CEditorActionEditLayerTilesProp> &Action2 = std::static_pointer_cast<CEditorActionEditLayerTilesProp>(r: vpActions[vpActions.size() - 1]);
787
788 Action1->SetSavedLayers(savedLayers);
789 Action2->SetSavedLayers(savedLayers);
790 }
791
792 int Changes = 0;
793 for(int y = OffsetY < 0 ? -OffsetY : 0; y < m_Height; y++)
794 {
795 for(int x = OffsetX < 0 ? -OffsetX : 0; x < m_Width; x++)
796 {
797 if(GetTile(x, y).m_Index)
798 {
799 const CTile ResultTile = {.m_Index: (unsigned char)Result};
800 pGLayer->SetTile(x: x + OffsetX, y: y + OffsetY, Tile: ResultTile);
801 Changes++;
802 }
803 }
804 }
805
806 vpActions.push_back(x: std::make_shared<CEditorBrushDrawAction>(args&: m_pEditor, args&: m_pEditor->m_SelectedGroup));
807 char aDisplay[256];
808 str_format(buffer: aDisplay, buffer_size: sizeof(aDisplay), format: "Construct '%s' game tiles (x%d)", s_apGametileOpNames[Selected], Changes);
809 m_pEditor->m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionBulk>(args&: m_pEditor, args&: vpActions, args&: aDisplay, args: true));
810 }
811 else
812 {
813 if(!m_pEditor->m_Map.m_pTeleLayer)
814 {
815 std::shared_ptr<CLayerTele> pLayer = std::make_shared<CLayerTele>(args&: m_pEditor, args&: m_Width, args&: m_Height);
816 m_pEditor->m_Map.MakeTeleLayer(pLayer);
817 m_pEditor->m_Map.m_pGameGroup->AddLayer(pLayer);
818
819 vpActions.push_back(x: std::make_shared<CEditorActionAddLayer>(args&: m_pEditor, args&: m_pEditor->m_SelectedGroup, args: m_pEditor->m_Map.m_pGameGroup->m_vpLayers.size() - 1));
820
821 if(m_Width != pGLayer->m_Width || m_Height > pGLayer->m_Height)
822 {
823 std::map<int, std::shared_ptr<CLayer>> savedLayers;
824 savedLayers[LAYERTYPE_TILES] = pGLayer->Duplicate();
825 savedLayers[LAYERTYPE_GAME] = savedLayers[LAYERTYPE_TILES];
826
827 int NewW = pGLayer->m_Width;
828 int NewH = pGLayer->m_Height;
829 if(m_Width > pGLayer->m_Width)
830 {
831 NewW = m_Width;
832 }
833 if(m_Height > pGLayer->m_Height)
834 {
835 NewH = m_Height;
836 }
837
838 int PrevW = pGLayer->m_Width;
839 int PrevH = pGLayer->m_Height;
840 pLayer->Resize(NewW, NewH);
841 vpActions.push_back(x: std::make_shared<CEditorActionEditLayerTilesProp>(args&: m_pEditor, args&: m_pEditor->m_SelectedGroup, args&: GameLayerIndex, args: ETilesProp::PROP_WIDTH, args&: PrevW, args&: NewW));
842 const std::shared_ptr<CEditorActionEditLayerTilesProp> &Action1 = std::static_pointer_cast<CEditorActionEditLayerTilesProp>(r: vpActions[vpActions.size() - 1]);
843 vpActions.push_back(x: std::make_shared<CEditorActionEditLayerTilesProp>(args&: m_pEditor, args&: m_pEditor->m_SelectedGroup, args&: GameLayerIndex, args: ETilesProp::PROP_HEIGHT, args&: PrevH, args&: NewH));
844 const std::shared_ptr<CEditorActionEditLayerTilesProp> &Action2 = std::static_pointer_cast<CEditorActionEditLayerTilesProp>(r: vpActions[vpActions.size() - 1]);
845
846 Action1->SetSavedLayers(savedLayers);
847 Action2->SetSavedLayers(savedLayers);
848 }
849 }
850
851 std::shared_ptr<CLayerTele> pTLayer = m_pEditor->m_Map.m_pTeleLayer;
852 int TeleLayerIndex = std::find(first: m_pEditor->m_Map.m_pGameGroup->m_vpLayers.begin(), last: m_pEditor->m_Map.m_pGameGroup->m_vpLayers.end(), val: pTLayer) - m_pEditor->m_Map.m_pGameGroup->m_vpLayers.begin();
853
854 if(pTLayer->m_Width < m_Width + OffsetX || pTLayer->m_Height < m_Height + OffsetY)
855 {
856 std::map<int, std::shared_ptr<CLayer>> savedLayers;
857 savedLayers[LAYERTYPE_TILES] = pTLayer->Duplicate();
858 savedLayers[LAYERTYPE_TELE] = savedLayers[LAYERTYPE_TILES];
859
860 int PrevW = pTLayer->m_Width;
861 int PrevH = pTLayer->m_Height;
862 int NewW = pTLayer->m_Width < m_Width + OffsetX ? m_Width + OffsetX : pTLayer->m_Width;
863 int NewH = pTLayer->m_Height < m_Height + OffsetY ? m_Height + OffsetY : pTLayer->m_Height;
864 pTLayer->Resize(NewW, NewH);
865 std::shared_ptr<CEditorActionEditLayerTilesProp> Action1, Action2;
866 vpActions.push_back(x: Action1 = std::make_shared<CEditorActionEditLayerTilesProp>(args&: m_pEditor, args&: m_pEditor->m_SelectedGroup, args&: TeleLayerIndex, args: ETilesProp::PROP_WIDTH, args&: PrevW, args&: NewW));
867 vpActions.push_back(x: Action2 = std::make_shared<CEditorActionEditLayerTilesProp>(args&: m_pEditor, args&: m_pEditor->m_SelectedGroup, args&: TeleLayerIndex, args: ETilesProp::PROP_HEIGHT, args&: PrevH, args&: NewH));
868
869 Action1->SetSavedLayers(savedLayers);
870 Action2->SetSavedLayers(savedLayers);
871 }
872
873 int Changes = 0;
874 for(int y = OffsetY < 0 ? -OffsetY : 0; y < m_Height; y++)
875 {
876 for(int x = OffsetX < 0 ? -OffsetX : 0; x < m_Width; x++)
877 {
878 if(GetTile(x, y).m_Index)
879 {
880 auto TileIndex = (y + OffsetY) * pTLayer->m_Width + x + OffsetX;
881 Changes++;
882
883 STeleTileStateChange::SData Previous{
884 .m_Number: pTLayer->m_pTeleTile[TileIndex].m_Number,
885 .m_Type: pTLayer->m_pTeleTile[TileIndex].m_Type,
886 .m_Index: pTLayer->m_pTiles[TileIndex].m_Index};
887
888 pTLayer->m_pTiles[TileIndex].m_Index = TILE_AIR + Result;
889 pTLayer->m_pTeleTile[TileIndex].m_Number = 1;
890 pTLayer->m_pTeleTile[TileIndex].m_Type = TILE_AIR + Result;
891
892 STeleTileStateChange::SData Current{
893 .m_Number: pTLayer->m_pTeleTile[TileIndex].m_Number,
894 .m_Type: pTLayer->m_pTeleTile[TileIndex].m_Type,
895 .m_Index: pTLayer->m_pTiles[TileIndex].m_Index};
896
897 pTLayer->RecordStateChange(x, y, Previous, Current);
898 }
899 }
900 }
901
902 vpActions.push_back(x: std::make_shared<CEditorBrushDrawAction>(args&: m_pEditor, args&: m_pEditor->m_SelectedGroup));
903 char aDisplay[256];
904 str_format(buffer: aDisplay, buffer_size: sizeof(aDisplay), format: "Construct 'tele' game tiles (x%d)", Changes);
905 m_pEditor->m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionBulk>(args&: m_pEditor, args&: vpActions, args&: aDisplay, args: true));
906 }
907 }
908 }
909
910 if(m_pEditor->m_Map.m_pGameLayer.get() != this)
911 {
912 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)
913 {
914 pToolBox->HSplitBottom(Cut: 2.0f, pTop: pToolBox, pBottom: nullptr);
915 pToolBox->HSplitBottom(Cut: 12.0f, pTop: pToolBox, pBottom: &Button);
916 if(m_Seed != 0)
917 {
918 CUIRect ButtonAuto;
919 Button.VSplitRight(Cut: 16.0f, pLeft: &Button, pRight: &ButtonAuto);
920 Button.VSplitRight(Cut: 2.0f, pLeft: &Button, pRight: nullptr);
921 static int s_AutoMapperButtonAuto = 0;
922 if(m_pEditor->DoButton_Editor(pId: &s_AutoMapperButtonAuto, pText: "A", Checked: m_AutoAutoMap, pRect: &ButtonAuto, Flags: 0, pToolTip: "Automatically run automap after modifications."))
923 {
924 m_AutoAutoMap = !m_AutoAutoMap;
925 FlagModified(x: 0, y: 0, w: m_Width, h: m_Height);
926 if(!m_TilesHistory.empty()) // Sometimes pressing that button causes the automap to run so we should be able to undo that
927 {
928 // record undo
929 m_pEditor->m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionTileChanges>(args&: m_pEditor, args&: m_pEditor->m_SelectedGroup, args&: m_pEditor->m_vSelectedLayers[0], args: "Auto map", args&: m_TilesHistory));
930 ClearHistory();
931 }
932 }
933 }
934
935 static int s_AutoMapperButton = 0;
936 if(m_pEditor->DoButton_Editor(pId: &s_AutoMapperButton, pText: "Automap", Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Run the automapper"))
937 {
938 m_pEditor->m_Map.m_vpImages[m_Image]->m_AutoMapper.Proceed(pLayer: this, ConfigId: m_AutoMapperConfig, Seed: m_Seed);
939 // record undo
940 m_pEditor->m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionTileChanges>(args&: m_pEditor, args&: m_pEditor->m_SelectedGroup, args&: m_pEditor->m_vSelectedLayers[0], args: "Auto map", args&: m_TilesHistory));
941 ClearHistory();
942 return CUi::POPUP_CLOSE_CURRENT;
943 }
944 }
945 }
946
947 int Color = PackColor(Color: m_Color);
948
949 CProperty aProps[] = {
950 {.m_pName: "Width", .m_Value: m_Width, .m_Type: PROPTYPE_INT, .m_Min: 1, .m_Max: 100000},
951 {.m_pName: "Height", .m_Value: m_Height, .m_Type: PROPTYPE_INT, .m_Min: 1, .m_Max: 100000},
952 {.m_pName: "Shift", .m_Value: 0, .m_Type: PROPTYPE_SHIFT, .m_Min: 0, .m_Max: 0},
953 {.m_pName: "Shift by", .m_Value: m_pEditor->m_ShiftBy, .m_Type: PROPTYPE_INT, .m_Min: 1, .m_Max: 100000},
954 {.m_pName: "Image", .m_Value: m_Image, .m_Type: PROPTYPE_IMAGE, .m_Min: 0, .m_Max: 0},
955 {.m_pName: "Color", .m_Value: Color, .m_Type: PROPTYPE_COLOR, .m_Min: 0, .m_Max: 0},
956 {.m_pName: "Color Env", .m_Value: m_ColorEnv + 1, .m_Type: PROPTYPE_ENVELOPE, .m_Min: 0, .m_Max: 0},
957 {.m_pName: "Color TO", .m_Value: m_ColorEnvOffset, .m_Type: PROPTYPE_INT, .m_Min: -1000000, .m_Max: 1000000},
958 {.m_pName: "Auto Rule", .m_Value: m_AutoMapperConfig, .m_Type: PROPTYPE_AUTOMAPPER, .m_Min: m_Image, .m_Max: 0},
959 {.m_pName: "Seed", .m_Value: m_Seed, .m_Type: PROPTYPE_INT, .m_Min: 0, .m_Max: 1000000000},
960 {.m_pName: nullptr},
961 };
962
963 if(EntitiesLayer) // remove the image and color properties if this is a game layer
964 {
965 aProps[(int)ETilesProp::PROP_IMAGE].m_pName = nullptr;
966 aProps[(int)ETilesProp::PROP_COLOR].m_pName = nullptr;
967 aProps[(int)ETilesProp::PROP_AUTOMAPPER].m_pName = nullptr;
968 }
969 if(m_Image == -1)
970 {
971 aProps[(int)ETilesProp::PROP_AUTOMAPPER].m_pName = nullptr;
972 aProps[(int)ETilesProp::PROP_SEED].m_pName = nullptr;
973 }
974
975 static int s_aIds[(int)ETilesProp::NUM_PROPS] = {0};
976 int NewVal = 0;
977 auto [State, Prop] = m_pEditor->DoPropertiesWithState<ETilesProp>(pToolbox: pToolBox, pProps: aProps, pIds: s_aIds, pNewVal: &NewVal);
978
979 static CLayerTilesPropTracker s_Tracker(m_pEditor);
980 s_Tracker.Begin(pObject: this, Prop, State);
981 m_pEditor->m_EditorHistory.BeginBulk();
982
983 if(Prop == ETilesProp::PROP_WIDTH && NewVal > 1)
984 {
985 if(NewVal > 1000 && !m_pEditor->m_LargeLayerWasWarned)
986 {
987 m_pEditor->m_PopupEventType = CEditor::POPEVENT_LARGELAYER;
988 m_pEditor->m_PopupEventActivated = true;
989 m_pEditor->m_LargeLayerWasWarned = true;
990 }
991 Resize(NewW: NewVal, NewH: m_Height);
992 }
993 else if(Prop == ETilesProp::PROP_HEIGHT && NewVal > 1)
994 {
995 if(NewVal > 1000 && !m_pEditor->m_LargeLayerWasWarned)
996 {
997 m_pEditor->m_PopupEventType = CEditor::POPEVENT_LARGELAYER;
998 m_pEditor->m_PopupEventActivated = true;
999 m_pEditor->m_LargeLayerWasWarned = true;
1000 }
1001 Resize(NewW: m_Width, NewH: NewVal);
1002 }
1003 else if(Prop == ETilesProp::PROP_SHIFT)
1004 {
1005 Shift(Direction: NewVal);
1006 }
1007 else if(Prop == ETilesProp::PROP_SHIFT_BY)
1008 {
1009 m_pEditor->m_ShiftBy = NewVal;
1010 }
1011 else if(Prop == ETilesProp::PROP_IMAGE)
1012 {
1013 m_Image = NewVal;
1014 if(NewVal == -1)
1015 {
1016 m_Image = -1;
1017 }
1018 else
1019 {
1020 m_Image = NewVal % m_pEditor->m_Map.m_vpImages.size();
1021 m_AutoMapperConfig = -1;
1022
1023 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)
1024 {
1025 m_pEditor->m_PopupEventType = CEditor::POPEVENT_IMAGEDIV16;
1026 m_pEditor->m_PopupEventActivated = true;
1027 m_Image = -1;
1028 }
1029 }
1030 }
1031 else if(Prop == ETilesProp::PROP_COLOR)
1032 {
1033 m_Color.r = (NewVal >> 24) & 0xff;
1034 m_Color.g = (NewVal >> 16) & 0xff;
1035 m_Color.b = (NewVal >> 8) & 0xff;
1036 m_Color.a = NewVal & 0xff;
1037 }
1038 else if(Prop == ETilesProp::PROP_COLOR_ENV)
1039 {
1040 int Index = clamp(val: NewVal - 1, lo: -1, hi: (int)m_pEditor->m_Map.m_vpEnvelopes.size() - 1);
1041 const int Step = (Index - m_ColorEnv) % 2;
1042 if(Step != 0)
1043 {
1044 for(; Index >= -1 && Index < (int)m_pEditor->m_Map.m_vpEnvelopes.size(); Index += Step)
1045 {
1046 if(Index == -1 || m_pEditor->m_Map.m_vpEnvelopes[Index]->GetChannels() == 4)
1047 {
1048 m_ColorEnv = Index;
1049 break;
1050 }
1051 }
1052 }
1053 }
1054 else if(Prop == ETilesProp::PROP_COLOR_ENV_OFFSET)
1055 {
1056 m_ColorEnvOffset = NewVal;
1057 }
1058 else if(Prop == ETilesProp::PROP_SEED)
1059 {
1060 m_Seed = NewVal;
1061 }
1062 else if(Prop == ETilesProp::PROP_AUTOMAPPER)
1063 {
1064 if(m_Image >= 0 && m_pEditor->m_Map.m_vpImages[m_Image]->m_AutoMapper.ConfigNamesNum() > 0 && NewVal >= 0)
1065 m_AutoMapperConfig = NewVal % m_pEditor->m_Map.m_vpImages[m_Image]->m_AutoMapper.ConfigNamesNum();
1066 else
1067 m_AutoMapperConfig = -1;
1068 }
1069
1070 s_Tracker.End(Prop, State);
1071
1072 // Check if modified property could have an effect on automapper
1073 if(HasAutomapEffect(Prop))
1074 {
1075 FlagModified(x: 0, y: 0, w: m_Width, h: m_Height);
1076
1077 // Record undo if automapper was ran
1078 if(m_AutoAutoMap && !m_TilesHistory.empty())
1079 {
1080 m_pEditor->m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionTileChanges>(args&: m_pEditor, args&: m_pEditor->m_SelectedGroup, args&: m_pEditor->m_vSelectedLayers[0], args: "Auto map", args&: m_TilesHistory));
1081 ClearHistory();
1082 }
1083 }
1084
1085 // End undo bulk, taking the first action display as the displayed text in the history
1086 // This is usually the resulting text of the edit layer tiles prop action
1087 // Since we may also squeeze a tile changes action, we want both to appear as one, thus using a bulk
1088 m_pEditor->m_EditorHistory.EndBulk(DisplayToUse: 0);
1089
1090 return CUi::POPUP_KEEP_OPEN;
1091}
1092
1093CUi::EPopupMenuFunctionResult CLayerTiles::RenderCommonProperties(SCommonPropState &State, CEditor *pEditor, CUIRect *pToolbox, std::vector<std::shared_ptr<CLayerTiles>> &vpLayers, std::vector<int> &vLayerIndices)
1094{
1095 if(State.m_Modified)
1096 {
1097 CUIRect Commit;
1098 pToolbox->HSplitBottom(Cut: 20.0f, pTop: pToolbox, pBottom: &Commit);
1099 static int s_CommitButton = 0;
1100 if(pEditor->DoButton_Editor(pId: &s_CommitButton, pText: "Commit", Checked: 0, pRect: &Commit, Flags: 0, pToolTip: "Applies the changes"))
1101 {
1102 bool HasModifiedSize = (State.m_Modified & SCommonPropState::MODIFIED_SIZE) != 0;
1103 bool HasModifiedColor = (State.m_Modified & SCommonPropState::MODIFIED_COLOR) != 0;
1104
1105 std::vector<std::shared_ptr<IEditorAction>> vpActions;
1106 int j = 0;
1107 int GroupIndex = pEditor->m_SelectedGroup;
1108 for(auto &pLayer : vpLayers)
1109 {
1110 int LayerIndex = vLayerIndices[j++];
1111 if(HasModifiedSize)
1112 {
1113 std::map<int, std::shared_ptr<CLayer>> SavedLayers;
1114 SavedLayers[LAYERTYPE_TILES] = pLayer->Duplicate();
1115 if(pLayer->m_Game || pLayer->m_Front || pLayer->m_Switch || pLayer->m_Speedup || pLayer->m_Tune || pLayer->m_Tele)
1116 { // Need to save all entities layers when any entity layer
1117 if(pEditor->m_Map.m_pFrontLayer && !pLayer->m_Front)
1118 SavedLayers[LAYERTYPE_FRONT] = pEditor->m_Map.m_pFrontLayer->Duplicate();
1119 if(pEditor->m_Map.m_pTeleLayer && !pLayer->m_Tele)
1120 SavedLayers[LAYERTYPE_TELE] = pEditor->m_Map.m_pTeleLayer->Duplicate();
1121 if(pEditor->m_Map.m_pSwitchLayer && !pLayer->m_Switch)
1122 SavedLayers[LAYERTYPE_SWITCH] = pEditor->m_Map.m_pSwitchLayer->Duplicate();
1123 if(pEditor->m_Map.m_pSpeedupLayer && !pLayer->m_Speedup)
1124 SavedLayers[LAYERTYPE_SPEEDUP] = pEditor->m_Map.m_pSpeedupLayer->Duplicate();
1125 if(pEditor->m_Map.m_pTuneLayer && !pLayer->m_Tune)
1126 SavedLayers[LAYERTYPE_TUNE] = pEditor->m_Map.m_pTuneLayer->Duplicate();
1127 if(!pLayer->m_Game)
1128 SavedLayers[LAYERTYPE_GAME] = pEditor->m_Map.m_pGameLayer->Duplicate();
1129 }
1130
1131 int PrevW = pLayer->m_Width;
1132 int PrevH = pLayer->m_Height;
1133 pLayer->Resize(NewW: State.m_Width, NewH: State.m_Height);
1134
1135 if(PrevW != State.m_Width)
1136 {
1137 std::shared_ptr<CEditorActionEditLayerTilesProp> pAction;
1138 vpActions.push_back(x: pAction = std::make_shared<CEditorActionEditLayerTilesProp>(args&: pEditor, args&: GroupIndex, args&: LayerIndex, args: ETilesProp::PROP_WIDTH, args&: PrevW, args&: State.m_Width));
1139 pAction->SetSavedLayers(SavedLayers);
1140 }
1141
1142 if(PrevH != State.m_Height)
1143 {
1144 std::shared_ptr<CEditorActionEditLayerTilesProp> pAction;
1145 vpActions.push_back(x: pAction = std::make_shared<CEditorActionEditLayerTilesProp>(args&: pEditor, args&: GroupIndex, args&: LayerIndex, args: ETilesProp::PROP_HEIGHT, args&: PrevH, args&: State.m_Height));
1146 pAction->SetSavedLayers(SavedLayers);
1147 }
1148 }
1149
1150 if(HasModifiedColor && !pLayer->IsEntitiesLayer())
1151 {
1152 int Color = 0;
1153 Color |= pLayer->m_Color.r << 24;
1154 Color |= pLayer->m_Color.g << 16;
1155 Color |= pLayer->m_Color.b << 8;
1156 Color |= pLayer->m_Color.a;
1157
1158 pLayer->m_Color.r = (State.m_Color >> 24) & 0xff;
1159 pLayer->m_Color.g = (State.m_Color >> 16) & 0xff;
1160 pLayer->m_Color.b = (State.m_Color >> 8) & 0xff;
1161 pLayer->m_Color.a = State.m_Color & 0xff;
1162
1163 vpActions.push_back(x: std::make_shared<CEditorActionEditLayerTilesProp>(args&: pEditor, args&: GroupIndex, args&: LayerIndex, args: ETilesProp::PROP_COLOR, args&: Color, args&: State.m_Color));
1164 }
1165
1166 pLayer->FlagModified(x: 0, y: 0, w: pLayer->m_Width, h: pLayer->m_Height);
1167 }
1168 State.m_Modified = 0;
1169
1170 char aDisplay[256];
1171 str_format(buffer: aDisplay, buffer_size: sizeof(aDisplay), format: "Edit %d layers common properties: %s", (int)vpLayers.size(), HasModifiedColor && HasModifiedSize ? "color, size" : (HasModifiedColor ? "color" : "size"));
1172 pEditor->m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionBulk>(args&: pEditor, args&: vpActions, args&: aDisplay));
1173 }
1174 }
1175 else
1176 {
1177 for(auto &pLayer : vpLayers)
1178 {
1179 if(pLayer->m_Width > State.m_Width)
1180 State.m_Width = pLayer->m_Width;
1181 if(pLayer->m_Height > State.m_Height)
1182 State.m_Height = pLayer->m_Height;
1183 }
1184 }
1185
1186 {
1187 CUIRect Warning;
1188 pToolbox->HSplitTop(Cut: 13.0f, pTop: &Warning, pBottom: pToolbox);
1189 Warning.HMargin(Cut: 0.5f, pOtherRect: &Warning);
1190
1191 pEditor->TextRender()->TextColor(rgb: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
1192 SLabelProperties Props;
1193 Props.m_MaxWidth = Warning.w;
1194 pEditor->Ui()->DoLabel(pRect: &Warning, pText: "Editing multiple layers", Size: 9.0f, Align: TEXTALIGN_ML, LabelProps: Props);
1195 pEditor->TextRender()->TextColor(rgb: ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
1196 pToolbox->HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: pToolbox);
1197 }
1198
1199 CProperty aProps[] = {
1200 {.m_pName: "Width", .m_Value: State.m_Width, .m_Type: PROPTYPE_INT, .m_Min: 1, .m_Max: 100000},
1201 {.m_pName: "Height", .m_Value: State.m_Height, .m_Type: PROPTYPE_INT, .m_Min: 1, .m_Max: 100000},
1202 {.m_pName: "Shift", .m_Value: 0, .m_Type: PROPTYPE_SHIFT, .m_Min: 0, .m_Max: 0},
1203 {.m_pName: "Shift by", .m_Value: pEditor->m_ShiftBy, .m_Type: PROPTYPE_INT, .m_Min: 1, .m_Max: 100000},
1204 {.m_pName: "Color", .m_Value: State.m_Color, .m_Type: PROPTYPE_COLOR, .m_Min: 0, .m_Max: 0},
1205 {.m_pName: nullptr},
1206 };
1207
1208 static int s_aIds[(int)ETilesCommonProp::NUM_PROPS] = {0};
1209 int NewVal = 0;
1210 auto [PropState, Prop] = pEditor->DoPropertiesWithState<ETilesCommonProp>(pToolbox, pProps: aProps, pIds: s_aIds, pNewVal: &NewVal);
1211
1212 static CLayerTilesCommonPropTracker s_Tracker(pEditor);
1213 s_Tracker.m_vpLayers = vpLayers;
1214 s_Tracker.m_vLayerIndices = vLayerIndices;
1215
1216 s_Tracker.Begin(pObject: nullptr, Prop, State: PropState);
1217
1218 if(Prop == ETilesCommonProp::PROP_WIDTH && NewVal > 1)
1219 {
1220 if(NewVal > 1000 && !pEditor->m_LargeLayerWasWarned)
1221 {
1222 pEditor->m_PopupEventType = CEditor::POPEVENT_LARGELAYER;
1223 pEditor->m_PopupEventActivated = true;
1224 pEditor->m_LargeLayerWasWarned = true;
1225 }
1226 State.m_Width = NewVal;
1227 }
1228 else if(Prop == ETilesCommonProp::PROP_HEIGHT && NewVal > 1)
1229 {
1230 if(NewVal > 1000 && !pEditor->m_LargeLayerWasWarned)
1231 {
1232 pEditor->m_PopupEventType = CEditor::POPEVENT_LARGELAYER;
1233 pEditor->m_PopupEventActivated = true;
1234 pEditor->m_LargeLayerWasWarned = true;
1235 }
1236 State.m_Height = NewVal;
1237 }
1238 else if(Prop == ETilesCommonProp::PROP_SHIFT)
1239 {
1240 for(auto &pLayer : vpLayers)
1241 pLayer->Shift(Direction: NewVal);
1242 }
1243 else if(Prop == ETilesCommonProp::PROP_SHIFT_BY)
1244 {
1245 pEditor->m_ShiftBy = NewVal;
1246 }
1247 else if(Prop == ETilesCommonProp::PROP_COLOR)
1248 {
1249 State.m_Color = NewVal;
1250 }
1251
1252 s_Tracker.End(Prop, State: PropState);
1253
1254 if(Prop == ETilesCommonProp::PROP_WIDTH || Prop == ETilesCommonProp::PROP_HEIGHT)
1255 {
1256 State.m_Modified |= SCommonPropState::MODIFIED_SIZE;
1257 }
1258 else if(Prop == ETilesCommonProp::PROP_COLOR)
1259 {
1260 State.m_Modified |= SCommonPropState::MODIFIED_COLOR;
1261 }
1262
1263 return CUi::POPUP_KEEP_OPEN;
1264}
1265
1266void CLayerTiles::FlagModified(int x, int y, int w, int h)
1267{
1268 m_pEditor->m_Map.OnModify();
1269 if(m_Seed != 0 && m_AutoMapperConfig != -1 && m_AutoAutoMap && m_Image >= 0)
1270 {
1271 m_pEditor->m_Map.m_vpImages[m_Image]->m_AutoMapper.ProceedLocalized(pLayer: this, ConfigId: m_AutoMapperConfig, Seed: m_Seed, X: x, Y: y, Width: w, Height: h);
1272 }
1273}
1274
1275void CLayerTiles::ModifyImageIndex(FIndexModifyFunction Func)
1276{
1277 Func(&m_Image);
1278}
1279
1280void CLayerTiles::ModifyEnvelopeIndex(FIndexModifyFunction Func)
1281{
1282 Func(&m_ColorEnv);
1283}
1284
1285void CLayerTiles::ShowPreventUnusedTilesWarning()
1286{
1287 if(!m_pEditor->m_PreventUnusedTilesWasWarned)
1288 {
1289 m_pEditor->m_PopupEventType = CEditor::POPEVENT_PREVENTUNUSEDTILES;
1290 m_pEditor->m_PopupEventActivated = true;
1291 m_pEditor->m_PreventUnusedTilesWasWarned = true;
1292 }
1293}
1294
1295bool CLayerTiles::HasAutomapEffect(ETilesProp Prop)
1296{
1297 switch(Prop)
1298 {
1299 case ETilesProp::PROP_WIDTH:
1300 case ETilesProp::PROP_HEIGHT:
1301 case ETilesProp::PROP_SHIFT:
1302 case ETilesProp::PROP_IMAGE:
1303 case ETilesProp::PROP_AUTOMAPPER:
1304 case ETilesProp::PROP_SEED:
1305 return true;
1306 default:
1307 return false;
1308 }
1309 return false;
1310}
1311