Line data Source code
1 : /* Copyright (C) 2022 Wildfire Games.
2 : * This file is part of 0 A.D.
3 : *
4 : * 0 A.D. is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 2 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * 0 A.D. is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include "precompiled.h"
19 :
20 : #include "MessageHandler.h"
21 :
22 : #include "../CommandProc.h"
23 :
24 : #include "graphics/Patch.h"
25 : #include "graphics/TerrainTextureManager.h"
26 : #include "graphics/TerrainTextureEntry.h"
27 : #include "graphics/Terrain.h"
28 : #include "graphics/TextureManager.h"
29 : #include "ps/Game.h"
30 : #include "ps/World.h"
31 : #include "lib/tex/tex.h"
32 : #include "ps/Filesystem.h"
33 : #include "simulation2/Simulation2.h"
34 : #include "simulation2/components/ICmpPathfinder.h"
35 : #include "simulation2/components/ICmpTerrain.h"
36 : #include "simulation2/helpers/Grid.h"
37 :
38 : #include "../Brushes.h"
39 : #include "../DeltaArray.h"
40 : #include "../View.h"
41 :
42 : #include <queue>
43 :
44 : namespace AtlasMessage
45 : {
46 :
47 : namespace
48 : {
49 :
50 0 : sTerrainTexturePreview MakeEmptyTerrainTexturePreview()
51 : {
52 0 : sTerrainTexturePreview preview{};
53 0 : preview.name = std::wstring();
54 0 : preview.loaded = false;
55 0 : preview.imageHeight = 0;
56 0 : preview.imageWidth = 0;
57 0 : preview.imageData = {};
58 0 : return preview;
59 : }
60 :
61 0 : bool CompareTerrain(const sTerrainTexturePreview& a, const sTerrainTexturePreview& b)
62 : {
63 0 : return (wcscmp(a.name.c_str(), b.name.c_str()) < 0);
64 : }
65 :
66 0 : sTerrainTexturePreview GetPreview(CTerrainTextureEntry* tex, size_t width, size_t height)
67 : {
68 0 : sTerrainTexturePreview preview;
69 0 : preview.name = tex->GetTag().FromUTF8();
70 :
71 0 : const size_t previewBPP = 3;
72 0 : std::vector<u8> buffer(width * height * previewBPP);
73 :
74 : // It's not good to shrink the entire texture to fit the small preview
75 : // window, since it's the fine details in the texture that are
76 : // interesting; so just go down one mipmap level, then crop a chunk
77 : // out of the middle.
78 :
79 0 : std::shared_ptr<u8> fileData;
80 : size_t fileSize;
81 0 : Tex texture;
82 : const bool canUsePreview =
83 0 : !tex->GetDiffuseTexturePath().empty() &&
84 0 : g_VFS->LoadFile(tex->GetDiffuseTexturePath(), fileData, fileSize) == INFO::OK &&
85 0 : texture.decode(fileData, fileSize) == INFO::OK &&
86 : // Check that we can fit the texture into the preview size before any transform.
87 0 : texture.m_Width >= width && texture.m_Height >= height &&
88 : // Transform to a single format that we can process.
89 0 : texture.transform_to((texture.m_Flags | TEX_MIPMAPS) & ~(TEX_DXT | TEX_GREY | TEX_BGR)) == INFO::OK &&
90 0 : (texture.m_Bpp == 24 || texture.m_Bpp == 32);
91 0 : if (canUsePreview)
92 : {
93 0 : size_t level = 0;
94 0 : while ((texture.m_Width >> (level + 1)) >= width && (texture.m_Height >> (level + 1)) >= height && level < texture.GetMIPLevels().size())
95 0 : ++level;
96 : // Extract the middle section (as a representative preview),
97 : // and copy into buffer.
98 0 : u8* data = texture.GetMIPLevels()[level].data;
99 0 : ENSURE(data);
100 0 : const size_t levelWidth = texture.m_Width >> level;
101 0 : const size_t levelHeight = texture.m_Height >> level;
102 0 : const size_t dataShiftX = (levelWidth - width) / 2;
103 0 : const size_t dataShiftY = (levelHeight - height) / 2;
104 0 : for (size_t y = 0; y < height; ++y)
105 0 : for (size_t x = 0; x < width; ++x)
106 : {
107 0 : const size_t bufferOffset = (y * width + x) * previewBPP;
108 0 : const size_t dataOffset = ((y + dataShiftY) * levelWidth + x + dataShiftX) * texture.m_Bpp / 8;
109 0 : buffer[bufferOffset + 0] = data[dataOffset + 0];
110 0 : buffer[bufferOffset + 1] = data[dataOffset + 1];
111 0 : buffer[bufferOffset + 2] = data[dataOffset + 2];
112 : }
113 0 : preview.loaded = true;
114 : }
115 : else
116 : {
117 : // Too small to preview. Just use a flat color instead.
118 0 : const u32 baseColor = tex->GetBaseColor();
119 0 : for (size_t i = 0; i < width * height; ++i)
120 : {
121 0 : buffer[i * previewBPP + 0] = (baseColor >> 16) & 0xff;
122 0 : buffer[i * previewBPP + 1] = (baseColor >> 8) & 0xff;
123 0 : buffer[i * previewBPP + 2] = (baseColor >> 0) & 0xff;
124 : }
125 0 : preview.loaded = tex->GetTexture()->IsLoaded();
126 : }
127 :
128 0 : preview.imageWidth = width;
129 0 : preview.imageHeight = height;
130 0 : preview.imageData = buffer;
131 :
132 0 : return preview;
133 : }
134 :
135 : } // anonymous namespace
136 :
137 0 : QUERYHANDLER(GetTerrainGroups)
138 : {
139 0 : const CTerrainTextureManager::TerrainGroupMap &groups = g_TexMan.GetGroups();
140 0 : std::vector<std::wstring> groupNames;
141 0 : for (CTerrainTextureManager::TerrainGroupMap::const_iterator it = groups.begin(); it != groups.end(); ++it)
142 0 : groupNames.push_back(it->first.FromUTF8());
143 0 : msg->groupNames = groupNames;
144 0 : }
145 :
146 0 : QUERYHANDLER(GetTerrainGroupTextures)
147 : {
148 0 : std::vector<std::wstring> names;
149 :
150 0 : CTerrainGroup* group = g_TexMan.FindGroup(CStrW(*msg->groupName).ToUTF8());
151 0 : if (group)
152 : {
153 0 : for (std::vector<CTerrainTextureEntry*>::const_iterator it = group->GetTerrains().begin(); it != group->GetTerrains().end(); ++it)
154 0 : names.emplace_back((*it)->GetTag().FromUTF8());
155 : }
156 0 : std::sort(names.begin(), names.end());
157 0 : msg->names = names;
158 0 : }
159 :
160 0 : QUERYHANDLER(GetTerrainGroupPreviews)
161 : {
162 0 : std::vector<sTerrainTexturePreview> previews;
163 :
164 0 : CTerrainGroup* group = g_TexMan.FindGroup(CStrW(*msg->groupName).ToUTF8());
165 0 : for (std::vector<CTerrainTextureEntry*>::const_iterator it = group->GetTerrains().begin(); it != group->GetTerrains().end(); ++it)
166 : {
167 0 : previews.push_back(GetPreview(*it, msg->imageWidth, msg->imageHeight));
168 : }
169 :
170 : // Sort the list alphabetically by name
171 0 : std::sort(previews.begin(), previews.end(), CompareTerrain);
172 0 : msg->previews = previews;
173 0 : }
174 :
175 0 : QUERYHANDLER(GetTerrainPassabilityClasses)
176 : {
177 0 : CmpPtr<ICmpPathfinder> cmpPathfinder(*AtlasView::GetView_Game()->GetSimulation2(), SYSTEM_ENTITY);
178 0 : if (cmpPathfinder)
179 : {
180 0 : std::map<std::string, pass_class_t> nonPathfindingClasses, pathfindingClasses;
181 0 : cmpPathfinder->GetPassabilityClasses(nonPathfindingClasses, pathfindingClasses);
182 :
183 0 : std::vector<std::wstring> classNames;
184 0 : for (std::map<std::string, pass_class_t>::iterator it = nonPathfindingClasses.begin(); it != nonPathfindingClasses.end(); ++it)
185 0 : classNames.push_back(CStr(it->first).FromUTF8());
186 0 : msg->classNames = classNames;
187 : }
188 0 : }
189 :
190 0 : QUERYHANDLER(GetTerrainTexture)
191 : {
192 : ssize_t x, y;
193 0 : g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace();
194 0 : g_CurrentBrush.GetCentre(x, y);
195 :
196 0 : CTerrain* terrain = g_Game->GetWorld()->GetTerrain();
197 0 : CMiniPatch* tile = terrain->GetTile(x, y);
198 0 : if (tile)
199 : {
200 0 : CTerrainTextureEntry* tex = tile->GetTextureEntry();
201 0 : msg->texture = tex->GetTag().FromUTF8();
202 : }
203 : else
204 : {
205 0 : msg->texture = std::wstring();
206 : }
207 0 : }
208 :
209 0 : QUERYHANDLER(GetTerrainTexturePreview)
210 : {
211 0 : CTerrainTextureEntry* tex = g_TexMan.FindTexture(CStrW(*msg->name).ToUTF8());
212 0 : if (tex)
213 0 : msg->preview = GetPreview(tex, msg->imageWidth, msg->imageHeight);
214 : else
215 0 : msg->preview = MakeEmptyTerrainTexturePreview();
216 0 : }
217 :
218 : //////////////////////////////////////////////////////////////////////////
219 :
220 : namespace
221 : {
222 :
223 : struct TerrainTile
224 : {
225 0 : TerrainTile(CTerrainTextureEntry* t, ssize_t p) : tex(t), priority(p) {}
226 : CTerrainTextureEntry* tex;
227 : ssize_t priority;
228 : };
229 :
230 0 : class TerrainArray : public DeltaArray2D<TerrainTile>
231 : {
232 : public:
233 0 : void Init()
234 : {
235 0 : m_Terrain = g_Game->GetWorld()->GetTerrain();
236 0 : m_VertsPerSide = g_Game->GetWorld()->GetTerrain()->GetVerticesPerSide();
237 0 : }
238 :
239 0 : void UpdatePriority(ssize_t x, ssize_t y, CTerrainTextureEntry* tex, ssize_t priorityScale, ssize_t& priority)
240 : {
241 0 : CMiniPatch* tile = m_Terrain->GetTile(x, y);
242 0 : if (!tile)
243 0 : return; // tile was out-of-bounds
244 :
245 : // If this tile matches the current texture, we just want to match its
246 : // priority; otherwise we want to exceed its priority
247 0 : if (tile->GetTextureEntry() == tex)
248 0 : priority = std::max(priority, tile->GetPriority()*priorityScale);
249 : else
250 0 : priority = std::max(priority, tile->GetPriority()*priorityScale + 1);
251 : }
252 :
253 0 : CTerrainTextureEntry* GetTexEntry(ssize_t x, ssize_t y)
254 : {
255 0 : if (size_t(x) >= size_t(m_VertsPerSide-1) || size_t(y) >= size_t(m_VertsPerSide-1))
256 0 : return NULL;
257 :
258 0 : return get(x, y).tex;
259 : }
260 :
261 0 : ssize_t GetPriority(ssize_t x, ssize_t y)
262 : {
263 0 : if (size_t(x) >= size_t(m_VertsPerSide-1) || size_t(y) >= size_t(m_VertsPerSide-1))
264 0 : return 0;
265 :
266 0 : return get(x, y).priority;
267 : }
268 :
269 0 : void PaintTile(ssize_t x, ssize_t y, CTerrainTextureEntry* tex, ssize_t priority)
270 : {
271 : // Ignore out-of-bounds tiles
272 0 : if (size_t(x) >= size_t(m_VertsPerSide-1) || size_t(y) >= size_t(m_VertsPerSide-1))
273 0 : return;
274 :
275 0 : set(x,y, TerrainTile(tex, priority));
276 : }
277 :
278 0 : ssize_t GetTilesPerSide()
279 : {
280 0 : return m_VertsPerSide-1;
281 : }
282 :
283 : protected:
284 0 : TerrainTile getOld(ssize_t x, ssize_t y)
285 : {
286 0 : CMiniPatch* mp = m_Terrain->GetTile(x, y);
287 0 : ENSURE(mp);
288 0 : return TerrainTile(mp->Tex, mp->Priority);
289 : }
290 0 : void setNew(ssize_t x, ssize_t y, const TerrainTile& val)
291 : {
292 0 : CMiniPatch* mp = m_Terrain->GetTile(x, y);
293 0 : ENSURE(mp);
294 0 : mp->Tex = val.tex;
295 0 : mp->Priority = val.priority;
296 0 : }
297 :
298 : CTerrain* m_Terrain;
299 : ssize_t m_VertsPerSide;
300 : };
301 :
302 : }
303 :
304 0 : BEGIN_COMMAND(PaintTerrain)
305 : {
306 : TerrainArray m_TerrainDelta;
307 : ssize_t m_i0, m_j0, m_i1, m_j1; // dirtied tiles (inclusive lower bound, exclusive upper)
308 :
309 0 : cPaintTerrain()
310 0 : {
311 0 : m_TerrainDelta.Init();
312 0 : }
313 :
314 0 : void MakeDirty()
315 : {
316 0 : g_Game->GetWorld()->GetTerrain()->MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_INDICES);
317 0 : }
318 :
319 0 : void Do()
320 : {
321 0 : g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace();
322 :
323 : ssize_t x0, y0;
324 0 : g_CurrentBrush.GetBottomLeft(x0, y0);
325 :
326 0 : CTerrainTextureEntry* texentry = g_TexMan.FindTexture(CStrW(*msg->texture).ToUTF8());
327 0 : if (! texentry)
328 : {
329 0 : debug_warn(L"Can't find texentry"); // TODO: nicer error handling
330 0 : return;
331 : }
332 :
333 : // Priority system: If the new tile should have a high priority,
334 : // set it to one plus the maximum priority of all surrounding tiles
335 : // that aren't included in the brush (so that it's definitely the highest).
336 : // Similar for low priority.
337 0 : ssize_t priorityScale = (msg->priority == ePaintTerrainPriority::HIGH ? +1 : -1);
338 0 : ssize_t priority = 0;
339 :
340 0 : for (ssize_t dy = -1; dy < g_CurrentBrush.m_H+1; ++dy)
341 : {
342 0 : for (ssize_t dx = -1; dx < g_CurrentBrush.m_W+1; ++dx)
343 : {
344 0 : if (!(g_CurrentBrush.Get(dx, dy) > 0.5f)) // ignore tiles that will be painted over
345 0 : m_TerrainDelta.UpdatePriority(x0+dx, y0+dy, texentry, priorityScale, priority);
346 : }
347 : }
348 :
349 0 : for (ssize_t dy = 0; dy < g_CurrentBrush.m_H; ++dy)
350 : {
351 0 : for (ssize_t dx = 0; dx < g_CurrentBrush.m_W; ++dx)
352 : {
353 0 : if (g_CurrentBrush.Get(dx, dy) > 0.5f) // TODO: proper solid brushes
354 0 : m_TerrainDelta.PaintTile(x0+dx, y0+dy, texentry, priority*priorityScale);
355 : }
356 : }
357 :
358 0 : m_i0 = x0 - 1;
359 0 : m_j0 = y0 - 1;
360 0 : m_i1 = x0 + g_CurrentBrush.m_W + 1;
361 0 : m_j1 = y0 + g_CurrentBrush.m_H + 1;
362 0 : MakeDirty();
363 : }
364 :
365 0 : void Undo()
366 : {
367 0 : m_TerrainDelta.Undo();
368 0 : MakeDirty();
369 0 : }
370 :
371 0 : void Redo()
372 : {
373 0 : m_TerrainDelta.Redo();
374 0 : MakeDirty();
375 0 : }
376 :
377 0 : void MergeIntoPrevious(cPaintTerrain* prev)
378 : {
379 0 : prev->m_TerrainDelta.OverlayWith(m_TerrainDelta);
380 0 : prev->m_i0 = std::min(prev->m_i0, m_i0);
381 0 : prev->m_j0 = std::min(prev->m_j0, m_j0);
382 0 : prev->m_i1 = std::max(prev->m_i1, m_i1);
383 0 : prev->m_j1 = std::max(prev->m_j1, m_j1);
384 0 : }
385 : };
386 0 : END_COMMAND(PaintTerrain)
387 :
388 : //////////////////////////////////////////////////////////////////////////
389 :
390 0 : BEGIN_COMMAND(ReplaceTerrain)
391 : {
392 : TerrainArray m_TerrainDelta;
393 : ssize_t m_i0, m_j0, m_i1, m_j1; // dirtied tiles (inclusive lower bound, exclusive upper)
394 :
395 0 : cReplaceTerrain()
396 0 : {
397 0 : m_TerrainDelta.Init();
398 0 : }
399 :
400 0 : void MakeDirty()
401 : {
402 0 : g_Game->GetWorld()->GetTerrain()->MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_INDICES);
403 0 : }
404 :
405 0 : void Do()
406 : {
407 0 : g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace();
408 :
409 : ssize_t x0, y0;
410 0 : g_CurrentBrush.GetBottomLeft(x0, y0);
411 :
412 0 : m_i0 = m_i1 = x0;
413 0 : m_j0 = m_j1 = y0;
414 :
415 0 : CTerrainTextureEntry* texentry = g_TexMan.FindTexture(CStrW(*msg->texture).ToUTF8());
416 0 : if (! texentry)
417 : {
418 0 : debug_warn(L"Can't find texentry"); // TODO: nicer error handling
419 0 : return;
420 : }
421 :
422 0 : CTerrainTextureEntry* replacedTex = m_TerrainDelta.GetTexEntry(x0, y0);
423 :
424 : // Don't bother if we're not making a change
425 0 : if (texentry == replacedTex)
426 : {
427 0 : return;
428 : }
429 :
430 0 : ssize_t tiles = m_TerrainDelta.GetTilesPerSide();
431 :
432 0 : for (ssize_t j = 0; j < tiles; ++j)
433 : {
434 0 : for (ssize_t i = 0; i < tiles; ++i)
435 : {
436 0 : if (m_TerrainDelta.GetTexEntry(i, j) == replacedTex)
437 : {
438 0 : m_i0 = std::min(m_i0, i-1);
439 0 : m_j0 = std::min(m_j0, j-1);
440 0 : m_i1 = std::max(m_i1, i+2);
441 0 : m_j1 = std::max(m_j1, j+2);
442 0 : m_TerrainDelta.PaintTile(i, j, texentry, m_TerrainDelta.GetPriority(i, j));
443 : }
444 : }
445 : }
446 :
447 0 : MakeDirty();
448 : }
449 :
450 0 : void Undo()
451 : {
452 0 : m_TerrainDelta.Undo();
453 0 : MakeDirty();
454 0 : }
455 :
456 0 : void Redo()
457 : {
458 0 : m_TerrainDelta.Redo();
459 0 : MakeDirty();
460 0 : }
461 : };
462 0 : END_COMMAND(ReplaceTerrain)
463 :
464 : //////////////////////////////////////////////////////////////////////////
465 :
466 0 : BEGIN_COMMAND(FillTerrain)
467 : {
468 : TerrainArray m_TerrainDelta;
469 : ssize_t m_i0, m_j0, m_i1, m_j1; // dirtied tiles (inclusive lower bound, exclusive upper)
470 :
471 0 : cFillTerrain()
472 0 : {
473 0 : m_TerrainDelta.Init();
474 0 : }
475 :
476 0 : void MakeDirty()
477 : {
478 0 : g_Game->GetWorld()->GetTerrain()->MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_INDICES);
479 0 : }
480 :
481 0 : void Do()
482 : {
483 0 : g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace();
484 :
485 : ssize_t x0, y0;
486 0 : g_CurrentBrush.GetBottomLeft(x0, y0);
487 :
488 0 : m_i0 = m_i1 = x0;
489 0 : m_j0 = m_j1 = y0;
490 :
491 0 : CTerrainTextureEntry* texentry = g_TexMan.FindTexture(CStrW(*msg->texture).ToUTF8());
492 0 : if (! texentry)
493 : {
494 0 : debug_warn(L"Can't find texentry"); // TODO: nicer error handling
495 0 : return;
496 : }
497 :
498 0 : CTerrainTextureEntry* replacedTex = m_TerrainDelta.GetTexEntry(x0, y0);
499 :
500 : // Don't bother if we're not making a change
501 0 : if (texentry == replacedTex)
502 : {
503 0 : return;
504 : }
505 :
506 0 : ssize_t tiles = m_TerrainDelta.GetTilesPerSide();
507 :
508 : // Simple 4-way flood fill algorithm using queue and a grid to keep track of visited tiles,
509 : // almost as fast as loop for filling whole map, much faster for small patches
510 0 : SparseGrid<bool> visited(tiles, tiles);
511 0 : std::queue<std::pair<u16, u16> > queue;
512 :
513 : // Initial tile
514 0 : queue.push(std::make_pair((u16)x0, (u16)y0));
515 0 : visited.set(x0, y0, true);
516 :
517 0 : while(!queue.empty())
518 : {
519 : // Check front of queue
520 0 : std::pair<u16, u16> t = queue.front();
521 0 : queue.pop();
522 0 : u16 i = t.first;
523 0 : u16 j = t.second;
524 :
525 0 : if (m_TerrainDelta.GetTexEntry(i, j) == replacedTex)
526 : {
527 : // Found a tile to replace: adjust bounds and paint it
528 0 : m_i0 = std::min(m_i0, (ssize_t)i-1);
529 0 : m_j0 = std::min(m_j0, (ssize_t)j-1);
530 0 : m_i1 = std::max(m_i1, (ssize_t)i+2);
531 0 : m_j1 = std::max(m_j1, (ssize_t)j+2);
532 0 : m_TerrainDelta.PaintTile(i, j, texentry, m_TerrainDelta.GetPriority(i, j));
533 :
534 : // Visit 4 adjacent tiles (could visit 8 if we want to count diagonal adjacency)
535 0 : if (i > 0 && !visited.get(i-1, j))
536 : {
537 0 : visited.set(i-1, j, true);
538 0 : queue.push(std::make_pair(i-1, j));
539 : }
540 0 : if (i < (tiles-1) && !visited.get(i+1, j))
541 : {
542 0 : visited.set(i+1, j, true);
543 0 : queue.push(std::make_pair(i+1, j));
544 : }
545 0 : if (j > 0 && !visited.get(i, j-1))
546 : {
547 0 : visited.set(i, j-1, true);
548 0 : queue.push(std::make_pair(i, j-1));
549 : }
550 0 : if (j < (tiles-1) && !visited.get(i, j+1))
551 : {
552 0 : visited.set(i, j+1, true);
553 0 : queue.push(std::make_pair(i, j+1));
554 : }
555 : }
556 : }
557 :
558 0 : MakeDirty();
559 : }
560 :
561 0 : void Undo()
562 : {
563 0 : m_TerrainDelta.Undo();
564 0 : MakeDirty();
565 0 : }
566 :
567 0 : void Redo()
568 : {
569 0 : m_TerrainDelta.Redo();
570 0 : MakeDirty();
571 0 : }
572 : };
573 0 : END_COMMAND(FillTerrain)
574 :
575 : } // namespace AtlasMessage
|