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