1/*
2 * Copyright (c) 2010-2017 OTClient <https://github.com/edubart/otclient>
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a copy
5 * of this software and associated documentation files (the "Software"), to deal
6 * in the Software without restriction, including without limitation the rights
7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 * copies of the Software, and to permit persons to whom the Software is
9 * furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 * THE SOFTWARE.
21 */
22
23#include "minimap.h"
24#include "game.h"
25#include "spritemanager.h"
26#include "tile.h"
27
28#include <framework/core/filestream.h>
29#include <framework/core/resourcemanager.h>
30#include <framework/graphics/framebuffermanager.h>
31#include <framework/graphics/image.h>
32#include <framework/graphics/painter.h>
33#include <framework/graphics/texture.h>
34#include <zlib.h>
35
36#include <framework/util/stats.h>
37
38Minimap g_minimap;
39
40void MinimapBlock::clean() {
41 m_tiles.fill(MinimapTile());
42 m_texture.reset();
43 m_mustUpdate = false;
44}
45
46void MinimapBlock::update() {
47 if (!m_mustUpdate)
48 return;
49
50 ImagePtr image(new Image(Size(MMBLOCK_SIZE, MMBLOCK_SIZE)));
51
52 bool shouldDraw = false;
53 for (int x = 0; x < MMBLOCK_SIZE; ++x) {
54 for (int y = 0; y < MMBLOCK_SIZE; ++y) {
55 uint8 c = getTile(x, y).color;
56 Color col = Color::alpha;
57 if (c != 255) {
58 col = Color::from8bit(c);
59 shouldDraw = true;
60 }
61 image->setPixel(x, y, col);
62 }
63 }
64
65 if (shouldDraw) {
66 m_texture = TexturePtr(new Texture(image));
67 } else
68 m_texture.reset();
69
70 m_mustUpdate = false;
71}
72
73void MinimapBlock::updateTile(int x, int y, const MinimapTile &tile) {
74 if (m_tiles[getTileIndex(x, y)].color != tile.color)
75 m_mustUpdate = true;
76
77 m_tiles[getTileIndex(x, y)] = tile;
78}
79
80void Minimap::init() {}
81
82void Minimap::terminate() { clean(); }
83
84void Minimap::clean() {
85 std::lock_guard<std::mutex> lock(m_lock);
86 for (int i = 0; i <= Otc::MAX_Z; ++i)
87 m_tileBlocks[i].clear();
88}
89
90void Minimap::draw(const Rect &screenRect, const Position &mapCenter,
91 float scale, const Color &color) {
92 if (screenRect.isEmpty())
93 return;
94
95 Rect mapRect =
96 calcMapRect(screenRect, mapCenter,
97 scale); // Draw background (disabled to avoid square shape)
98 // g_drawQueue->addFilledRect(screenRect, color);
99
100 if ((int)MMBLOCK_SIZE * scale <= 1 || !mapCenter.isMapPosition()) {
101 return;
102 }
103
104 size_t drawQueueStart = g_drawQueue->size();
105 Point blockOff = getBlockOffset(mapRect.topLeft());
106 Point off =
107 Point((mapRect.size() * scale).toPoint() - screenRect.size().toPoint()) /
108 2;
109 Point start =
110 screenRect.topLeft() - (mapRect.topLeft() - blockOff) * scale - off;
111
112 // Circle clip: compute center and squared radius from screenRect
113 int circleX = screenRect.left() + screenRect.width() / 2;
114 int circleY = screenRect.top() + screenRect.height() / 2;
115 int circleR = std::min(screenRect.width(), screenRect.height()) / 2;
116 int circleR2 = circleR * circleR;
117
118 for (int y = blockOff.y, ys = start.y; ys < screenRect.bottom();
119 y += MMBLOCK_SIZE, ys += (int)MMBLOCK_SIZE * scale) {
120 if (y < 0 || y >= 65536)
121 continue;
122
123 for (int x = blockOff.x, xs = start.x; xs < screenRect.right();
124 x += MMBLOCK_SIZE, xs += (int)MMBLOCK_SIZE * scale) {
125 if (x < 0 || x >= 65536)
126 continue;
127
128 Position blockPos(x, y, mapCenter.z);
129 if (!hasBlock(blockPos))
130 continue;
131
132 MinimapBlock &block = getBlock(Position(x, y, mapCenter.z));
133 block.update();
134
135 const TexturePtr &tex = block.getTexture();
136 if (tex) {
137 Rect src(0, 0, MMBLOCK_SIZE, MMBLOCK_SIZE);
138 float fscale = (float)scale;
139 Rect dest(xs, ys, (int)((int)MMBLOCK_SIZE * fscale),
140 (int)((int)MMBLOCK_SIZE * fscale));
141
142 // Circle check: skip tiles whose nearest point to circle center
143 // is outside the circle radius (corners won't render outside ring).
144 {
145 int nearX = std::max(dest.left(), std::min(circleX, dest.right()));
146 int nearY = std::max(dest.top(), std::min(circleY, dest.bottom()));
147 int dx = nearX - circleX;
148 int dy = nearY - circleY;
149 if (dx * dx + dy * dy > circleR2)
150 continue;
151 }
152
153 // Sub-block drawing for smoother circular edges.
154 // We use a simple pixel-based approach to ensure no gaps between
155 // sub-rects.
156 int subDiv = (scale > 2.0f) ? 128 : 64;
157
158 for (int sy = 0; sy < subDiv; ++sy) {
159 // Calculate source and destination Y coordinates using ratios to
160 // avoid gaps
161 int iyStart = (int)((float)sy * (float)dest.height() / (float)subDiv);
162 int iyEnd =
163 (int)((float)(sy + 1) * (float)dest.height() / (float)subDiv);
164 int isyStart = (int)((float)sy * (float)src.height() / (float)subDiv);
165 int isyEnd =
166 (int)((float)(sy + 1) * (float)src.height() / (float)subDiv);
167
168 for (int sx = 0; sx < subDiv; ++sx) {
169 // Calculate source and destination X coordinates using ratios
170 int ixStart =
171 (int)((float)sx * (float)dest.width() / (float)subDiv);
172 int ixEnd =
173 (int)((float)(sx + 1) * (float)dest.width() / (float)subDiv);
174 int isxStart =
175 (int)((float)sx * (float)src.width() / (float)subDiv);
176 int isxEnd =
177 (int)((float)(sx + 1) * (float)src.width() / (float)subDiv);
178
179 Rect subDest(dest.left() + ixStart, dest.top() + iyStart,
180 std::max<int>(1, ixEnd - ixStart),
181 std::max<int>(1, iyEnd - iyStart));
182
183 if (subDest.isEmpty())
184 continue;
185
186 // Simple center check for circular clipping
187 Point center = subDest.center();
188 int dx = center.x - circleX;
189 int dy = center.y - circleY;
190 if (dx * dx + dy * dy > circleR2) {
191 // Edge smoothing for high zoom
192 if (!(scale > 3.0f && (std::abs(dx * dx + dy * dy - circleR2) <
193 (int)(scale * 2.0f)))) {
194 continue;
195 }
196 }
197
198 Rect subSrc(src.left() + isxStart, src.top() + isyStart,
199 std::max<int>(1, isxEnd - isxStart),
200 std::max<int>(1, isyEnd - isyStart));
201
202 // Rect clip: clip subDest to screenRect
203 if (!screenRect.contains(subDest)) {
204 if (!subDest.intersects(screenRect))
205 continue;
206 }
207
208 g_drawQueue->addTexturedRect(subDest, tex, subSrc);
209 }
210 }
211 }
212 }
213 }
214
215 g_drawQueue->setClip(drawQueueStart, screenRect);
216}
217
218Point Minimap::getTilePoint(const Position &pos, const Rect &screenRect,
219 const Position &mapCenter, float scale) {
220 if (screenRect.isEmpty() || pos.z != mapCenter.z)
221 return Point(-1, -1);
222
223 Rect mapRect = calcMapRect(screenRect, mapCenter, scale);
224 Point off =
225 Point((mapRect.size() * scale).toPoint() - screenRect.size().toPoint()) /
226 2;
227 Point posoff = (Point(pos.x, pos.y) - mapRect.topLeft()) * scale;
228 return posoff + screenRect.topLeft() - off + (Point(1, 1) * scale) / 2;
229}
230
231Position Minimap::getTilePosition(const Point &point, const Rect &screenRect,
232 const Position &mapCenter, float scale) {
233 if (screenRect.isEmpty())
234 return Position();
235
236 Rect mapRect = calcMapRect(screenRect, mapCenter, scale);
237 Point off =
238 Point((mapRect.size() * scale).toPoint() - screenRect.size().toPoint()) /
239 2;
240 Point pos2d =
241 (point - screenRect.topLeft() + off) / scale + mapRect.topLeft();
242 return Position(pos2d.x, pos2d.y, mapCenter.z);
243}
244
245Rect Minimap::getTileRect(const Position &pos, const Rect &screenRect,
246 const Position &mapCenter, float scale) {
247 if (screenRect.isEmpty() || pos.z != mapCenter.z)
248 return Rect();
249
250 int tileSize = g_sprites.spriteSize() * scale;
251 Rect tileRect(0, 0, tileSize, tileSize);
252 tileRect.moveCenter(getTilePoint(pos, screenRect, mapCenter, scale));
253 return tileRect;
254}
255
256Rect Minimap::calcMapRect(const Rect &screenRect, const Position &mapCenter,
257 float scale) {
258 int w = screenRect.width() / scale,
259 h = std::ceil(screenRect.height() / scale);
260 Rect mapRect(0, 0, w, h);
261 mapRect.moveCenter(Point(mapCenter.x, mapCenter.y));
262 return mapRect;
263}
264
265void Minimap::updateTile(const Position &pos, const TilePtr &tile) {
266 MinimapTile minimapTile;
267 if (tile) {
268 minimapTile.color = tile->getMinimapColorByte();
269 minimapTile.flags |= MinimapTileWasSeen;
270 if (!tile->isWalkable(true))
271 minimapTile.flags |= MinimapTileNotWalkable;
272 if (!tile->isPathable())
273 minimapTile.flags |= MinimapTileNotPathable;
274 minimapTile.speed =
275 std::min<int>((int)std::ceil(tile->getGroundSpeed() / 10.0f), 255);
276 } else {
277 minimapTile.color = 255;
278 minimapTile.flags |= MinimapTileEmpty;
279 minimapTile.speed = 1;
280 }
281
282 if (minimapTile != MinimapTile()) {
283 MinimapBlock &block = getBlock(pos);
284 Point offsetPos = getBlockOffset(Point(pos.x, pos.y));
285 block.updateTile(pos.x - offsetPos.x, pos.y - offsetPos.y, minimapTile);
286 block.justSaw();
287 }
288}
289
290const MinimapTile &Minimap::getTile(const Position &pos) {
291 static MinimapTile nulltile;
292 if (pos.z <= Otc::MAX_Z && hasBlock(pos)) {
293 MinimapBlock &block = getBlock(pos);
294 Point offsetPos = getBlockOffset(Point(pos.x, pos.y));
295 return block.getTile(pos.x - offsetPos.x, pos.y - offsetPos.y);
296 }
297 return nulltile;
298}
299
300std::pair<MinimapBlock_ptr, MinimapTile>
301Minimap::threadGetTile(const Position &pos) {
302 std::lock_guard<std::mutex> lock(m_lock);
303 static MinimapTile nulltile;
304
305 if (pos.z <= Otc::MAX_Z && hasBlock(pos)) {
306 MinimapBlock_ptr block = m_tileBlocks[pos.z][getBlockIndex(pos)];
307 if (block) {
308 Point offsetPos = getBlockOffset(Point(pos.x, pos.y));
309 return std::make_pair(
310 block, block->getTile(pos.x - offsetPos.x, pos.y - offsetPos.y));
311 }
312 }
313 return std::make_pair(nullptr, nulltile);
314}
315
316bool Minimap::loadImage(const std::string &fileName, const Position &topLeft,
317 float colorFactor) {
318 if (colorFactor <= 0.01f)
319 colorFactor = 1.0f;
320
321 try {
322 ImagePtr image = Image::load(fileName);
323
324 uint8 waterc = Color::to8bit(std::string("#3300cc"));
325
326 // non pathable colors
327 Color nonPathableColors[] = {
328 std::string("#ffff00"), // yellow
329 };
330
331 // non walkable colors
332 Color nonWalkableColors[] = {
333 std::string("#000000"), // oil, black
334 std::string("#006600"), // trees, dark green
335 std::string("#ff3300"), // walls, red
336 std::string("#666666"), // mountain, grey
337 std::string("#ff6600"), // lava, orange
338 std::string("#00ff00"), // positon
339 std::string("#ccffff"), // ice, very light blue
340 };
341
342 for (int y = 0; y < image->getHeight(); ++y) {
343 for (int x = 0; x < image->getWidth(); ++x) {
344 Color color = *(uint32 *)image->getPixel(x, y);
345 uint8 c = Color::to8bit(color * colorFactor);
346 int flags = 0;
347
348 if (c == waterc || color.a() == 0) {
349 flags |= MinimapTileNotWalkable;
350 c = 255; // alpha
351 }
352
353 if (flags != 0) {
354 for (Color &col : nonWalkableColors) {
355 if (col == color) {
356 flags |= MinimapTileNotWalkable;
357 break;
358 }
359 }
360 }
361
362 if (flags != 0) {
363 for (Color &col : nonPathableColors) {
364 if (col == color) {
365 flags |= MinimapTileNotPathable;
366 break;
367 }
368 }
369 }
370
371 if (c == 255)
372 continue;
373
374 Position pos(topLeft.x + x, topLeft.y + y, topLeft.z);
375 MinimapBlock &block = getBlock(pos);
376 Point offsetPos = getBlockOffset(Point(pos.x, pos.y));
377 MinimapTile &tile =
378 block.getTile(pos.x - offsetPos.x, pos.y - offsetPos.y);
379 if (!(tile.flags & MinimapTileWasSeen)) {
380 tile.color = c;
381 tile.flags = flags;
382 block.mustUpdate();
383 }
384 }
385 }
386 return true;
387 } catch (stdext::exception &e) {
388 g_logger.error(stdext::format("failed to load OTMM minimap: %s", e.what()));
389 return false;
390 }
391}
392
393void Minimap::saveImage(const std::string &fileName, const Rect &mapRect) {
394 // TODO
395}
396
397bool Minimap::loadOtmm(const std::string &fileName) {
398 try {
399 FileStreamPtr fin = g_resources.openFile(
400 fileName, g_game.getFeature(Otc::GameDontCacheFiles));
401 if (!fin)
402 stdext::throw_exception("unable to open file");
403
404 uint32 signature = fin->getU32();
405 if (signature != OTMM_SIGNATURE)
406 stdext::throw_exception("invalid OTMM file");
407
408 uint16 start = fin->getU16();
409 uint16 version = fin->getU16();
410 fin->getU32(); // flags
411
412 switch (version) {
413 case 1: {
414 fin->getString(); // description
415 break;
416 }
417 default:
418 stdext::throw_exception("OTMM version not supported");
419 }
420
421 fin->seek(start);
422
423 uint blockSize = MMBLOCK_SIZE * MMBLOCK_SIZE * sizeof(MinimapTile);
424 std::vector<uchar> compressBuffer(compressBound(blockSize));
425 std::vector<uchar> decompressBuffer(blockSize);
426
427 while (true) {
428 Position pos;
429 pos.x = fin->getU16();
430 pos.y = fin->getU16();
431 pos.z = fin->getU8();
432
433 // end of file or file is corrupted
434 if (!pos.isValid() || pos.z >= Otc::MAX_Z + 1)
435 break;
436
437 MinimapBlock &block = getBlock(pos);
438 ulong len = fin->getU16();
439 ulong destLen = blockSize;
440 fin->read(compressBuffer.data(), len);
441 int ret = uncompress(decompressBuffer.data(), &destLen,
442 compressBuffer.data(), len);
443 if (ret != Z_OK || destLen != blockSize)
444 break;
445
446 memcpy((uchar *)&block.getTiles(), decompressBuffer.data(), blockSize);
447 block.mustUpdate();
448 block.justSaw();
449 }
450
451 fin->close();
452 return true;
453 } catch (stdext::exception &e) {
454 g_logger.error(stdext::format("failed to load OTMM minimap: %s", e.what()));
455 return false;
456 }
457}
458
459void Minimap::saveOtmm(const std::string &fileName) {
460 try {
461 stdext::timer saveTimer;
462
463#ifndef ANDROID
464 std::string tmpFileName = fileName;
465 tmpFileName += ".tmp";
466 FileStreamPtr fin = g_resources.createFile(tmpFileName);
467#else
468 FileStreamPtr fin = g_resources.createFile(fileName);
469#endif
470
471 // TODO: compression flag with zlib
472 uint32 flags = 0;
473
474 // header
475 fin->addU32(OTMM_SIGNATURE);
476 fin->addU16(0); // data start, will be overwritten later
477 fin->addU16(OTMM_VERSION);
478 fin->addU32(flags);
479
480 // version 1 header
481 fin->addString("OTMM 1.0"); // description
482
483 // go back and rewrite where the map data starts
484 uint32 start = fin->tell();
485 fin->seek(4);
486 fin->addU16(start);
487 fin->seek(start);
488
489 uint blockSize = MMBLOCK_SIZE * MMBLOCK_SIZE * sizeof(MinimapTile);
490 std::vector<uchar> compressBuffer(compressBound(blockSize));
491 const int COMPRESS_LEVEL = 3;
492
493 for (uint8_t z = 0; z <= Otc::MAX_Z; ++z) {
494 for (auto &it : m_tileBlocks[z]) {
495 int index = it.first;
496 MinimapBlock &block = *it.second;
497 if (!block.wasSeen())
498 continue;
499
500 Position pos = getIndexPosition(index, z);
501 fin->addU16(pos.x);
502 fin->addU16(pos.y);
503 fin->addU8(pos.z);
504
505 ulong len = blockSize;
506 int ret =
507 compress2(compressBuffer.data(), &len, (uchar *)&block.getTiles(),
508 blockSize, COMPRESS_LEVEL);
509 VALIDATE(ret == Z_OK);
510 fin->addU16(len);
511 fin->write(compressBuffer.data(), len);
512 }
513 }
514
515 // end of file
516 Position invalidPos;
517 fin->addU16(invalidPos.x);
518 fin->addU16(invalidPos.y);
519 fin->addU8(invalidPos.z);
520
521 fin->flush();
522
523 fin->close();
524#ifndef ANDROID
525 std::filesystem::path filePath(g_resources.getWriteDir()),
526 tmpFilePath(g_resources.getWriteDir());
527 filePath += fileName;
528 tmpFilePath += tmpFileName;
529 if (std::filesystem::file_size(tmpFilePath) > 1024) {
530 std::filesystem::rename(tmpFilePath, filePath);
531 }
532#endif
533 } catch (stdext::exception &e) {
534 g_logger.error(stdext::format("failed to save OTMM minimap: %s", e.what()));
535 } catch (std::exception &e) {
536 g_logger.error(stdext::format("failed to save OTMM minimap: %s", e.what()));
537 }
538}
539