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