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/graphics.h>
8#include <engine/keys.h>
9#include <engine/shared/config.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_AutoAutomapper = 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_AutoAutomapper = Other.m_AutoAutomapper;
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::MakePalette() const
141{
142 for(int y = 0; y < m_Height; y++)
143 for(int x = 0; x < m_Width; x++)
144 m_pTiles[y * m_Width + x].m_Index = y * 16 + x;
145}
146
147void CLayerTiles::Render(const CEditorMap *pRenderMap)
148{
149 IGraphics::CTextureHandle Texture;
150 if(m_HasGame)
151 {
152 Texture = Editor()->GetEntitiesTexture();
153 }
154 else if(m_HasFront)
155 {
156 Texture = Editor()->GetFrontTexture();
157 }
158 else if(m_HasTele)
159 {
160 Texture = Editor()->GetTeleTexture();
161 }
162 else if(m_HasSpeedup)
163 {
164 Texture = Editor()->GetSpeedupTexture();
165 }
166 else if(m_HasSwitch)
167 {
168 Texture = Editor()->GetSwitchTexture();
169 }
170 else if(m_HasTune)
171 {
172 Texture = Editor()->GetTuneTexture();
173 }
174 else if(m_Image >= 0 && (size_t)m_Image < pRenderMap->m_vpImages.size())
175 {
176 const auto &pImage = pRenderMap->m_vpImages[m_Image];
177 if(pImage->m_Width % 16 == 0 && pImage->m_Height % 16 == 0)
178 {
179 Texture = pImage->m_Texture;
180 }
181 }
182 Graphics()->TextureSet(Texture);
183
184 ColorRGBA ColorEnv = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
185 pRenderMap->m_EnvelopeEvaluator.EnvelopeEval(TimeOffsetMillis: m_ColorEnvOffset, EnvelopeIndex: m_ColorEnv, Result&: ColorEnv, Channels: 4);
186 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);
187
188 Graphics()->BlendNone();
189 Editor()->RenderMap()->RenderTilemap(pTiles: m_pTiles, w: m_Width, h: m_Height, Scale: 32.0f, Color, RenderFlags: LAYERRENDERFLAG_OPAQUE);
190 Graphics()->BlendNormal();
191 Editor()->RenderMap()->RenderTilemap(pTiles: m_pTiles, w: m_Width, h: m_Height, Scale: 32.0f, Color, RenderFlags: LAYERRENDERFLAG_TRANSPARENT);
192
193 // Render DDRace Layers
194 if(m_RenderOverlays)
195 {
196 int OverlayRenderFlags = (g_Config.m_ClTextEntitiesEditor ? OVERLAYRENDERFLAG_TEXT : 0) | OVERLAYRENDERFLAG_EDITOR;
197 if(m_HasTele)
198 Editor()->RenderMap()->RenderTeleOverlay(pTele: static_cast<CLayerTele *>(this)->m_pTeleTile, w: m_Width, h: m_Height, Scale: 32.0f, OverlayRenderFlags);
199 if(m_HasSpeedup)
200 Editor()->RenderMap()->RenderSpeedupOverlay(pSpeedup: static_cast<CLayerSpeedup *>(this)->m_pSpeedupTile, w: m_Width, h: m_Height, Scale: 32.0f, OverlayRenderFlags);
201 if(m_HasSwitch)
202 Editor()->RenderMap()->RenderSwitchOverlay(pSwitch: static_cast<CLayerSwitch *>(this)->m_pSwitchTile, w: m_Width, h: m_Height, Scale: 32.0f, OverlayRenderFlags);
203 if(m_HasTune)
204 Editor()->RenderMap()->RenderTuneOverlay(pTune: static_cast<CLayerTune *>(this)->m_pTuneTile, w: m_Width, h: m_Height, Scale: 32.0f, OverlayRenderFlags);
205 }
206}
207
208int CLayerTiles::ConvertX(float x) const { return (int)(x / 32.0f); }
209int CLayerTiles::ConvertY(float y) const { return (int)(y / 32.0f); }
210
211void CLayerTiles::Convert(CUIRect Rect, CIntRect *pOut) const
212{
213 pOut->x = ConvertX(x: Rect.x);
214 pOut->y = ConvertY(y: Rect.y);
215 pOut->w = ConvertX(x: Rect.x + Rect.w + 31) - pOut->x;
216 pOut->h = ConvertY(y: Rect.y + Rect.h + 31) - pOut->y;
217}
218
219void CLayerTiles::Snap(CUIRect *pRect) const
220{
221 CIntRect Out;
222 Convert(Rect: *pRect, pOut: &Out);
223 pRect->x = Out.x * 32.0f;
224 pRect->y = Out.y * 32.0f;
225 pRect->w = Out.w * 32.0f;
226 pRect->h = Out.h * 32.0f;
227}
228
229void CLayerTiles::Clamp(CIntRect *pRect) const
230{
231 if(pRect->x < 0)
232 {
233 pRect->w += pRect->x;
234 pRect->x = 0;
235 }
236
237 if(pRect->y < 0)
238 {
239 pRect->h += pRect->y;
240 pRect->y = 0;
241 }
242
243 if(pRect->x + pRect->w > m_Width)
244 pRect->w = m_Width - pRect->x;
245
246 if(pRect->y + pRect->h > m_Height)
247 pRect->h = m_Height - pRect->y;
248
249 if(pRect->h < 0)
250 pRect->h = 0;
251 if(pRect->w < 0)
252 pRect->w = 0;
253}
254
255bool CLayerTiles::IsEntitiesLayer() const
256{
257 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;
258}
259
260bool CLayerTiles::IsEmpty() const
261{
262 for(int y = 0; y < m_Height; y++)
263 {
264 for(int x = 0; x < m_Width; x++)
265 {
266 if(GetTile(x, y).m_Index != 0)
267 {
268 return false;
269 }
270 }
271 }
272 return true;
273}
274
275void CLayerTiles::BrushSelecting(CUIRect Rect)
276{
277 Snap(pRect: &Rect);
278
279 Rect.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.4f), Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
280
281 char aBuf[16];
282 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d⨯%d", ConvertX(x: Rect.w), ConvertY(y: Rect.h));
283 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);
284}
285
286template<typename T>
287static void InitGrabbedLayer(std::shared_ptr<T> &pLayer, CLayerTiles *pThisLayer)
288{
289 pLayer->m_Image = pThisLayer->m_Image;
290 pLayer->m_HasGame = pThisLayer->m_HasGame;
291 pLayer->m_HasFront = pThisLayer->m_HasFront;
292 pLayer->m_HasTele = pThisLayer->m_HasTele;
293 pLayer->m_HasSpeedup = pThisLayer->m_HasSpeedup;
294 pLayer->m_HasSwitch = pThisLayer->m_HasSwitch;
295 pLayer->m_HasTune = pThisLayer->m_HasTune;
296 if(pThisLayer->Editor()->m_BrushColorEnabled)
297 {
298 pLayer->m_Color = pThisLayer->m_Color;
299 pLayer->m_Color.a = 255;
300 }
301}
302
303int CLayerTiles::BrushGrab(CLayerGroup *pBrush, CUIRect Rect)
304{
305 CIntRect r;
306 Convert(Rect, pOut: &r);
307 Clamp(pRect: &r);
308
309 if(!r.w || !r.h)
310 return 0;
311
312 // create new layers
313 if(m_HasTele)
314 {
315 std::shared_ptr<CLayerTele> pGrabbed = std::make_shared<CLayerTele>(args: pBrush->Map(), args&: r.w, args&: r.h);
316 InitGrabbedLayer(pLayer&: pGrabbed, pThisLayer: this);
317
318 pBrush->AddLayer(pLayer: pGrabbed);
319
320 for(int y = 0; y < r.h; y++)
321 {
322 for(int x = 0; x < r.w; x++)
323 {
324 // copy the tiles
325 pGrabbed->m_pTiles[y * pGrabbed->m_Width + x] = GetTile(x: r.x + x, y: r.y + y);
326
327 // copy the tele data
328 if(!Editor()->Input()->KeyIsPressed(Key: KEY_SPACE))
329 {
330 pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x] = static_cast<CLayerTele *>(this)->m_pTeleTile[(r.y + y) * m_Width + (r.x + x)];
331 unsigned char TgtIndex = pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Type;
332 if(IsValidTeleTile(Index: TgtIndex))
333 {
334 if(IsTeleTileNumberUsed(Index: TgtIndex, Checkpoint: false))
335 Editor()->m_TeleNumber = pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Number;
336 else if(IsTeleTileNumberUsed(Index: TgtIndex, Checkpoint: true))
337 Editor()->m_TeleCheckpointNumber = pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Number;
338 }
339 }
340 else
341 {
342 const CTile &Tile = pGrabbed->m_pTiles[y * pGrabbed->m_Width + x];
343 if(IsValidTeleTile(Index: Tile.m_Index) && IsTeleTileNumberUsedAny(Index: Tile.m_Index))
344 {
345 pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Type = Tile.m_Index;
346 pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Number = IsTeleTileCheckpoint(Index: Tile.m_Index) ? Editor()->m_TeleCheckpointNumber : Editor()->m_TeleNumber;
347 }
348 }
349 }
350 }
351
352 pGrabbed->m_TeleNumber = Editor()->m_TeleNumber;
353 pGrabbed->m_TeleCheckpointNumber = Editor()->m_TeleCheckpointNumber;
354
355 str_copy(dst&: pGrabbed->m_aFilename, src: pGrabbed->Map()->m_aFilename);
356 }
357 else if(m_HasSpeedup)
358 {
359 std::shared_ptr<CLayerSpeedup> pGrabbed = std::make_shared<CLayerSpeedup>(args: pBrush->Map(), args&: r.w, args&: r.h);
360 InitGrabbedLayer(pLayer&: pGrabbed, pThisLayer: this);
361
362 pBrush->AddLayer(pLayer: pGrabbed);
363
364 for(int y = 0; y < r.h; y++)
365 {
366 for(int x = 0; x < r.w; x++)
367 {
368 // copy the tiles
369 pGrabbed->m_pTiles[y * pGrabbed->m_Width + x] = GetTile(x: r.x + x, y: r.y + y);
370
371 // copy the speedup data
372 if(!Editor()->Input()->KeyIsPressed(Key: KEY_SPACE))
373 {
374 pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x] = static_cast<CLayerSpeedup *>(this)->m_pSpeedupTile[(r.y + y) * m_Width + (r.x + x)];
375 if(IsValidSpeedupTile(Index: pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_Type))
376 {
377 Editor()->m_SpeedupAngle = pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_Angle;
378 Editor()->m_SpeedupForce = pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_Force;
379 Editor()->m_SpeedupMaxSpeed = pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_MaxSpeed;
380 }
381 }
382 else
383 {
384 const CTile &Tile = pGrabbed->m_pTiles[y * pGrabbed->m_Width + x];
385 if(IsValidSpeedupTile(Index: Tile.m_Index))
386 {
387 pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_Type = Tile.m_Index;
388 pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_Angle = Editor()->m_SpeedupAngle;
389 pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_Force = Editor()->m_SpeedupForce;
390 pGrabbed->m_pSpeedupTile[y * pGrabbed->m_Width + x].m_MaxSpeed = Editor()->m_SpeedupMaxSpeed;
391 }
392 }
393 }
394 }
395
396 pGrabbed->m_SpeedupForce = Editor()->m_SpeedupForce;
397 pGrabbed->m_SpeedupMaxSpeed = Editor()->m_SpeedupMaxSpeed;
398 pGrabbed->m_SpeedupAngle = Editor()->m_SpeedupAngle;
399 str_copy(dst&: pGrabbed->m_aFilename, src: pGrabbed->Map()->m_aFilename);
400 }
401 else if(m_HasSwitch)
402 {
403 std::shared_ptr<CLayerSwitch> pGrabbed = std::make_shared<CLayerSwitch>(args: pBrush->Map(), args&: r.w, args&: r.h);
404 InitGrabbedLayer(pLayer&: pGrabbed, pThisLayer: this);
405
406 pBrush->AddLayer(pLayer: pGrabbed);
407
408 for(int y = 0; y < r.h; y++)
409 {
410 for(int x = 0; x < r.w; x++)
411 {
412 // copy the tiles
413 pGrabbed->m_pTiles[y * pGrabbed->m_Width + x] = GetTile(x: r.x + x, y: r.y + y);
414
415 // copy the switch data
416 if(!Editor()->Input()->KeyIsPressed(Key: KEY_SPACE))
417 {
418 pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x] = static_cast<CLayerSwitch *>(this)->m_pSwitchTile[(r.y + y) * m_Width + (r.x + x)];
419 if(IsValidSwitchTile(Index: pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x].m_Type))
420 {
421 Editor()->m_SwitchNumber = pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x].m_Number;
422 Editor()->m_SwitchDelay = pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x].m_Delay;
423 }
424 }
425 else
426 {
427 const CTile &Tile = pGrabbed->m_pTiles[y * pGrabbed->m_Width + x];
428 if(IsValidSwitchTile(Index: Tile.m_Index))
429 {
430 pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x].m_Type = Tile.m_Index;
431 pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x].m_Number = Editor()->m_SwitchNumber;
432 pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x].m_Delay = Editor()->m_SwitchDelay;
433 pGrabbed->m_pSwitchTile[y * pGrabbed->m_Width + x].m_Flags = Tile.m_Flags;
434 }
435 }
436 }
437 }
438
439 pGrabbed->m_SwitchNumber = Editor()->m_SwitchNumber;
440 pGrabbed->m_SwitchDelay = Editor()->m_SwitchDelay;
441 str_copy(dst&: pGrabbed->m_aFilename, src: pGrabbed->Map()->m_aFilename);
442 }
443
444 else if(m_HasTune)
445 {
446 std::shared_ptr<CLayerTune> pGrabbed = std::make_shared<CLayerTune>(args: pBrush->Map(), args&: r.w, args&: r.h);
447 InitGrabbedLayer(pLayer&: pGrabbed, pThisLayer: this);
448
449 pBrush->AddLayer(pLayer: pGrabbed);
450
451 // copy the tiles
452 for(int y = 0; y < r.h; y++)
453 {
454 for(int x = 0; x < r.w; x++)
455 {
456 pGrabbed->m_pTiles[y * pGrabbed->m_Width + x] = GetTile(x: r.x + x, y: r.y + y);
457
458 if(!Editor()->Input()->KeyIsPressed(Key: KEY_SPACE))
459 {
460 pGrabbed->m_pTuneTile[y * pGrabbed->m_Width + x] = static_cast<CLayerTune *>(this)->m_pTuneTile[(r.y + y) * m_Width + (r.x + x)];
461 if(IsValidTuneTile(Index: pGrabbed->m_pTuneTile[y * pGrabbed->m_Width + x].m_Type))
462 {
463 Editor()->m_TuningNumber = pGrabbed->m_pTuneTile[y * pGrabbed->m_Width + x].m_Number;
464 }
465 }
466 else
467 {
468 const CTile &Tile = pGrabbed->m_pTiles[y * pGrabbed->m_Width + x];
469 if(IsValidTuneTile(Index: Tile.m_Index))
470 {
471 pGrabbed->m_pTuneTile[y * pGrabbed->m_Width + x].m_Type = Tile.m_Index;
472 pGrabbed->m_pTuneTile[y * pGrabbed->m_Width + x].m_Number = Editor()->m_TuningNumber;
473 }
474 }
475 }
476 }
477
478 pGrabbed->m_TuningNumber = Editor()->m_TuningNumber;
479 str_copy(dst&: pGrabbed->m_aFilename, src: pGrabbed->Map()->m_aFilename);
480 }
481 else // game, front and tiles layers
482 {
483 std::shared_ptr<CLayerTiles> pGrabbed;
484 if(m_HasGame)
485 {
486 pGrabbed = std::make_shared<CLayerGame>(args: pBrush->Map(), args&: r.w, args&: r.h);
487 }
488 else if(m_HasFront)
489 {
490 pGrabbed = std::make_shared<CLayerFront>(args: pBrush->Map(), args&: r.w, args&: r.h);
491 }
492 else
493 {
494 pGrabbed = std::make_shared<CLayerTiles>(args: pBrush->Map(), args&: r.w, args&: r.h);
495 }
496 InitGrabbedLayer(pLayer&: pGrabbed, pThisLayer: this);
497
498 pBrush->AddLayer(pLayer: pGrabbed);
499
500 // copy the tiles
501 for(int y = 0; y < r.h; y++)
502 for(int x = 0; x < r.w; x++)
503 pGrabbed->m_pTiles[y * pGrabbed->m_Width + x] = GetTile(x: r.x + x, y: r.y + y);
504 str_copy(dst&: pGrabbed->m_aFilename, src: pGrabbed->Map()->m_aFilename);
505 }
506
507 return 1;
508}
509
510void CLayerTiles::FillSelection(bool Empty, CLayer *pBrush, CUIRect Rect)
511{
512 if(m_Readonly || (!Empty && pBrush->m_Type != LAYERTYPE_TILES))
513 return;
514
515 Snap(pRect: &Rect);
516
517 int sx = ConvertX(x: Rect.x);
518 int sy = ConvertY(y: Rect.y);
519 int w = ConvertX(x: Rect.w);
520 int h = ConvertY(y: Rect.h);
521
522 CLayerTiles *pLt = static_cast<CLayerTiles *>(pBrush);
523
524 bool Destructive = Editor()->m_BrushDrawDestructive || Empty || pLt->IsEmpty();
525
526 for(int y = 0; y < h; y++)
527 {
528 for(int x = 0; x < w; x++)
529 {
530 int fx = x + sx;
531 int fy = y + sy;
532
533 if(fx < 0 || fx >= m_Width || fy < 0 || fy >= m_Height)
534 continue;
535
536 bool HasTile = GetTile(x: fx, y: fy).m_Index;
537 if(!Empty && pLt->GetTile(x: x % pLt->m_Width, y: y % pLt->m_Height).m_Index == TILE_THROUGH_CUT)
538 {
539 if(m_HasGame && Map()->m_pFrontLayer)
540 {
541 HasTile = HasTile || Map()->m_pFrontLayer->GetTile(x: fx, y: fy).m_Index;
542 }
543 else if(m_HasFront)
544 {
545 HasTile = HasTile || Map()->m_pGameLayer->GetTile(x: fx, y: fy).m_Index;
546 }
547 }
548
549 if(!Destructive && HasTile)
550 continue;
551
552 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)]);
553 }
554 }
555 FlagModified(x: sx, y: sy, w, h);
556}
557
558void CLayerTiles::BrushDraw(CLayer *pBrush, vec2 WorldPos)
559{
560 if(m_Readonly)
561 return;
562
563 CLayerTiles *pTileLayer = static_cast<CLayerTiles *>(pBrush);
564 int sx = ConvertX(x: WorldPos.x);
565 int sy = ConvertY(y: WorldPos.y);
566
567 bool Destructive = Editor()->m_BrushDrawDestructive || pTileLayer->IsEmpty();
568
569 for(int y = 0; y < pTileLayer->m_Height; y++)
570 for(int x = 0; x < pTileLayer->m_Width; x++)
571 {
572 int fx = x + sx;
573 int fy = y + sy;
574
575 if(fx < 0 || fx >= m_Width || fy < 0 || fy >= m_Height)
576 continue;
577
578 bool HasTile = GetTile(x: fx, y: fy).m_Index;
579 if(pTileLayer->CLayerTiles::GetTile(x, y).m_Index == TILE_THROUGH_CUT)
580 {
581 if(m_HasGame && Map()->m_pFrontLayer)
582 {
583 HasTile = HasTile || Map()->m_pFrontLayer->GetTile(x: fx, y: fy).m_Index;
584 }
585 else if(m_HasFront)
586 {
587 HasTile = HasTile || Map()->m_pGameLayer->GetTile(x: fx, y: fy).m_Index;
588 }
589 }
590
591 if(!Destructive && HasTile)
592 continue;
593
594 SetTile(x: fx, y: fy, Tile: pTileLayer->CLayerTiles::GetTile(x, y));
595 }
596
597 FlagModified(x: sx, y: sy, w: pTileLayer->m_Width, h: pTileLayer->m_Height);
598}
599
600void CLayerTiles::BrushFlipX()
601{
602 BrushFlipXImpl(pTiles: m_pTiles);
603
604 if(m_HasTele || m_HasSpeedup || m_HasTune)
605 return;
606
607 bool Rotate = !(m_HasGame || m_HasFront || m_HasSwitch) || Editor()->IsAllowPlaceUnusedTiles();
608 for(int y = 0; y < m_Height; y++)
609 for(int x = 0; x < m_Width; x++)
610 if(!Rotate && !IsRotatableTile(Index: m_pTiles[y * m_Width + x].m_Index))
611 m_pTiles[y * m_Width + x].m_Flags = 0;
612 else
613 m_pTiles[y * m_Width + x].m_Flags ^= (m_pTiles[y * m_Width + x].m_Flags & TILEFLAG_ROTATE) ? TILEFLAG_YFLIP : TILEFLAG_XFLIP;
614}
615
616void CLayerTiles::BrushFlipY()
617{
618 BrushFlipYImpl(pTiles: m_pTiles);
619
620 if(m_HasTele || m_HasSpeedup || m_HasTune)
621 return;
622
623 bool Rotate = !(m_HasGame || m_HasFront || m_HasSwitch) || Editor()->IsAllowPlaceUnusedTiles();
624 for(int y = 0; y < m_Height; y++)
625 for(int x = 0; x < m_Width; x++)
626 if(!Rotate && !IsRotatableTile(Index: m_pTiles[y * m_Width + x].m_Index))
627 m_pTiles[y * m_Width + x].m_Flags = 0;
628 else
629 m_pTiles[y * m_Width + x].m_Flags ^= (m_pTiles[y * m_Width + x].m_Flags & TILEFLAG_ROTATE) ? TILEFLAG_XFLIP : TILEFLAG_YFLIP;
630}
631
632void CLayerTiles::BrushRotate(float Amount)
633{
634 int Rotation = (round_to_int(f: 360.0f * Amount / (pi * 2)) / 90) % 4; // 0=0°, 1=90°, 2=180°, 3=270°
635 if(Rotation < 0)
636 Rotation += 4;
637
638 if(Rotation == 1 || Rotation == 3)
639 {
640 // 90° rotation
641 CTile *pTempData = new CTile[m_Width * m_Height];
642 mem_copy(dest: pTempData, source: m_pTiles, size: (size_t)m_Width * m_Height * sizeof(CTile));
643 CTile *pDst = m_pTiles;
644 bool Rotate = !(m_HasGame || m_HasFront) || Editor()->IsAllowPlaceUnusedTiles();
645 for(int x = 0; x < m_Width; ++x)
646 for(int y = m_Height - 1; y >= 0; --y, ++pDst)
647 {
648 *pDst = pTempData[y * m_Width + x];
649 if(!Rotate && !IsRotatableTile(Index: pDst->m_Index))
650 pDst->m_Flags = 0;
651 else
652 {
653 if(pDst->m_Flags & TILEFLAG_ROTATE)
654 pDst->m_Flags ^= (TILEFLAG_YFLIP | TILEFLAG_XFLIP);
655 pDst->m_Flags ^= TILEFLAG_ROTATE;
656 }
657 }
658
659 std::swap(a&: m_Width, b&: m_Height);
660 delete[] pTempData;
661 }
662
663 if(Rotation == 2 || Rotation == 3)
664 {
665 BrushFlipX();
666 BrushFlipY();
667 }
668}
669
670std::shared_ptr<CLayer> CLayerTiles::Duplicate() const
671{
672 return std::make_shared<CLayerTiles>(args: *this);
673}
674
675const char *CLayerTiles::TypeName() const
676{
677 return "tiles";
678}
679
680void CLayerTiles::Resize(int NewW, int NewH)
681{
682 CTile *pNewData = new CTile[NewW * NewH];
683 mem_zero(block: pNewData, size: (size_t)NewW * NewH * sizeof(CTile));
684
685 // copy old data
686 for(int y = 0; y < std::min(a: NewH, b: m_Height); y++)
687 mem_copy(dest: &pNewData[y * NewW], source: &m_pTiles[y * m_Width], size: std::min(a: m_Width, b: NewW) * sizeof(CTile));
688
689 // replace old
690 delete[] m_pTiles;
691 m_pTiles = pNewData;
692 m_Width = NewW;
693 m_Height = NewH;
694
695 // resize tele layer if available
696 if(m_HasGame && Map()->m_pTeleLayer && (Map()->m_pTeleLayer->m_Width != NewW || Map()->m_pTeleLayer->m_Height != NewH))
697 Map()->m_pTeleLayer->Resize(NewW, NewH);
698
699 // resize speedup layer if available
700 if(m_HasGame && Map()->m_pSpeedupLayer && (Map()->m_pSpeedupLayer->m_Width != NewW || Map()->m_pSpeedupLayer->m_Height != NewH))
701 Map()->m_pSpeedupLayer->Resize(NewW, NewH);
702
703 // resize front layer
704 if(m_HasGame && Map()->m_pFrontLayer && (Map()->m_pFrontLayer->m_Width != NewW || Map()->m_pFrontLayer->m_Height != NewH))
705 Map()->m_pFrontLayer->Resize(NewW, NewH);
706
707 // resize switch layer if available
708 if(m_HasGame && Map()->m_pSwitchLayer && (Map()->m_pSwitchLayer->m_Width != NewW || Map()->m_pSwitchLayer->m_Height != NewH))
709 Map()->m_pSwitchLayer->Resize(NewW, NewH);
710
711 // resize tune layer if available
712 if(m_HasGame && Map()->m_pTuneLayer && (Map()->m_pTuneLayer->m_Width != NewW || Map()->m_pTuneLayer->m_Height != NewH))
713 Map()->m_pTuneLayer->Resize(NewW, NewH);
714}
715
716void CLayerTiles::Shift(EShiftDirection Direction)
717{
718 ShiftImpl(pTiles: m_pTiles, Direction, ShiftBy: Map()->m_ShiftBy);
719}
720
721void CLayerTiles::ShowInfo()
722{
723 CScreenRect ScreenRect = Graphics()->GetScreen();
724 Graphics()->TextureSet(Texture: Editor()->Client()->GetDebugFont());
725 Graphics()->QuadsBegin();
726
727 int StartY = std::max(a: 0, b: (int)(ScreenRect.m_TopLeft.y / 32.0f) - 1);
728 int StartX = std::max(a: 0, b: (int)(ScreenRect.m_TopLeft.x / 32.0f) - 1);
729 int EndY = std::min(a: (int)(ScreenRect.m_BottomRight.y / 32.0f) + 1, b: m_Height);
730 int EndX = std::min(a: (int)(ScreenRect.m_BottomRight.x / 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(ScreenRect);
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: x + OffsetX, y: y + OffsetY, 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_AutoAutomapper, pRect: &ButtonAuto, Flags: BUTTONFLAG_LEFT, pToolTip: "Automatically run the automapper after modifications."))
1009 {
1010 m_AutoAutomapper = !m_AutoAutomapper;
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_AutoAutomapper && !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_AutoAutomapper && 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
1363bool CLayerTiles::IsEnvelopeUsed(int EnvelopeIndex) const
1364{
1365 return m_ColorEnv == EnvelopeIndex;
1366}
1367
1368bool CLayerTiles::IsImageUsed(int ImageIndex) const
1369{
1370 return m_Image == ImageIndex;
1371}
1372
1373void CLayerTiles::ModifyImageIndex(const FIndexModifyFunction &IndexModifyFunction)
1374{
1375 IndexModifyFunction(&m_Image);
1376}
1377
1378void CLayerTiles::ModifyEnvelopeIndex(const FIndexModifyFunction &IndexModifyFunction)
1379{
1380 IndexModifyFunction(&m_ColorEnv);
1381}
1382
1383void CLayerTiles::ShowPreventUnusedTilesWarning()
1384{
1385 if(!Editor()->m_PreventUnusedTilesWasWarned)
1386 {
1387 Editor()->m_PopupEventType = CEditor::POPEVENT_PREVENTUNUSEDTILES;
1388 Editor()->m_PopupEventActivated = true;
1389 Editor()->m_PreventUnusedTilesWasWarned = true;
1390 }
1391}
1392
1393bool CLayerTiles::HasAutomapEffect(ETilesProp Prop)
1394{
1395 switch(Prop)
1396 {
1397 case ETilesProp::WIDTH:
1398 case ETilesProp::HEIGHT:
1399 case ETilesProp::SHIFT:
1400 case ETilesProp::IMAGE:
1401 case ETilesProp::AUTOMAPPER:
1402 case ETilesProp::SEED:
1403 return true;
1404 default:
1405 return false;
1406 }
1407 return false;
1408}
1409