1minimap.cpp

main 532 lines · 16.4 KB Raw
Alex Alex Commit Client Upload Part 2 19/10/2026 00:06
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 (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 += 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 += 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)(MMBLOCK_SIZE * fscale),
140 (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 int subDiv = 64;
155 int subSize = MMBLOCK_SIZE / subDiv;
156 float subDestSizeX = (float)dest.width() / (float)subDiv;
157 float subDestSizeY = (float)dest.height() / (float)subDiv;
158 float subSrcSizeX = (float)src.width() / (float)subDiv;
159 float subSrcSizeY = (float)src.height() / (float)subDiv;
160
161 for (int sy = 0; sy < subDiv; ++sy) {
162 int yStart = (int)(sy * subDestSizeY);
163 int yEnd = (int)((sy + 1) * subDestSizeY);
164 int syStart = (int)(sy * subSrcSizeY);
165 int syEnd = (int)((sy + 1) * subSrcSizeY);
166
167 for (int sx = 0; sx < subDiv; ++sx) {
168 int xStart = (int)(sx * subDestSizeX);
169 int xEnd = (int)((sx + 1) * subDestSizeX);
170
171 Rect subDest(dest.left() + xStart, dest.top() + yStart,
172 xEnd - xStart, yEnd - yStart);
173
174 // Check if all four corners of subDest are within the circle
175 bool inside = true;
176 Point corners[4] = {subDest.topLeft(), subDest.topRight(),
177 subDest.bottomLeft(), subDest.bottomRight()};
178 for (const Point &p : corners) {
179 int dx = p.x - circleX;
180 int dy = p.y - circleY;
181 if (dx * dx + dy * dy > circleR2) {
182 inside = false;
183 break;
184 }
185 }
186 if (!inside)
187 continue;
188
189 int sxStart = (int)(sx * subSrcSizeX);
190 int sxEnd = (int)((sx + 1) * subSrcSizeX);
191
192 Rect subSrc(src.left() + sxStart, src.top() + syStart,
193 sxEnd - sxStart, syEnd - syStart);
194
195 // Rect clip: clip subDest to screenRect
196 if (!screenRect.contains(subDest)) {
197 if (!subDest.intersects(screenRect))
198 continue;
199 }
200
201 g_drawQueue->addTexturedRect(subDest, tex, subSrc);
202 }
203 }
204 }
205 }
206 }
207
208 g_drawQueue->setClip(drawQueueStart, screenRect);
209}
210
211Point Minimap::getTilePoint(const Position &pos, const Rect &screenRect,
212 const Position &mapCenter, float scale) {
213 if (screenRect.isEmpty() || pos.z != mapCenter.z)
214 return Point(-1, -1);
215
216 Rect mapRect = calcMapRect(screenRect, mapCenter, scale);
217 Point off =
218 Point((mapRect.size() * scale).toPoint() - screenRect.size().toPoint()) /
219 2;
220 Point posoff = (Point(pos.x, pos.y) - mapRect.topLeft()) * scale;
221 return posoff + screenRect.topLeft() - off + (Point(1, 1) * scale) / 2;
222}
223
224Position Minimap::getTilePosition(const Point &point, const Rect &screenRect,
225 const Position &mapCenter, float scale) {
226 if (screenRect.isEmpty())
227 return Position();
228
229 Rect mapRect = calcMapRect(screenRect, mapCenter, scale);
230 Point off =
231 Point((mapRect.size() * scale).toPoint() - screenRect.size().toPoint()) /
232 2;
233 Point pos2d =
234 (point - screenRect.topLeft() + off) / scale + mapRect.topLeft();
235 return Position(pos2d.x, pos2d.y, mapCenter.z);
236}
237
238Rect Minimap::getTileRect(const Position &pos, const Rect &screenRect,
239 const Position &mapCenter, float scale) {
240 if (screenRect.isEmpty() || pos.z != mapCenter.z)
241 return Rect();
242
243 int tileSize = g_sprites.spriteSize() * scale;
244 Rect tileRect(0, 0, tileSize, tileSize);
245 tileRect.moveCenter(getTilePoint(pos, screenRect, mapCenter, scale));
246 return tileRect;
247}
248
249Rect Minimap::calcMapRect(const Rect &screenRect, const Position &mapCenter,
250 float scale) {
251 int w = screenRect.width() / scale,
252 h = std::ceil(screenRect.height() / scale);
253 Rect mapRect(0, 0, w, h);
254 mapRect.moveCenter(Point(mapCenter.x, mapCenter.y));
255 return mapRect;
256}
257
258void Minimap::updateTile(const Position &pos, const TilePtr &tile) {
259 MinimapTile minimapTile;
260 if (tile) {
261 minimapTile.color = tile->getMinimapColorByte();
262 minimapTile.flags |= MinimapTileWasSeen;
263 if (!tile->isWalkable(true))
264 minimapTile.flags |= MinimapTileNotWalkable;
265 if (!tile->isPathable())
266 minimapTile.flags |= MinimapTileNotPathable;
267 minimapTile.speed =
268 std::min<int>((int)std::ceil(tile->getGroundSpeed() / 10.0f), 255);
269 } else {
270 minimapTile.color = 255;
271 minimapTile.flags |= MinimapTileEmpty;
272 minimapTile.speed = 1;
273 }
274
275 if (minimapTile != MinimapTile()) {
276 MinimapBlock &block = getBlock(pos);
277 Point offsetPos = getBlockOffset(Point(pos.x, pos.y));
278 block.updateTile(pos.x - offsetPos.x, pos.y - offsetPos.y, minimapTile);
279 block.justSaw();
280 }
281}
282
283const MinimapTile &Minimap::getTile(const Position &pos) {
284 static MinimapTile nulltile;
285 if (pos.z <= Otc::MAX_Z && hasBlock(pos)) {
286 MinimapBlock &block = getBlock(pos);
287 Point offsetPos = getBlockOffset(Point(pos.x, pos.y));
288 return block.getTile(pos.x - offsetPos.x, pos.y - offsetPos.y);
289 }
290 return nulltile;
291}
292
293std::pair<MinimapBlock_ptr, MinimapTile>
294Minimap::threadGetTile(const Position &pos) {
295 std::lock_guard<std::mutex> lock(m_lock);
296 static MinimapTile nulltile;
297
298 if (pos.z <= Otc::MAX_Z && hasBlock(pos)) {
299 MinimapBlock_ptr block = m_tileBlocks[pos.z][getBlockIndex(pos)];
300 if (block) {
301 Point offsetPos = getBlockOffset(Point(pos.x, pos.y));
302 return std::make_pair(
303 block, block->getTile(pos.x - offsetPos.x, pos.y - offsetPos.y));
304 }
305 }
306 return std::make_pair(nullptr, nulltile);
307}
308
309bool Minimap::loadImage(const std::string &fileName, const Position &topLeft,
310 float colorFactor) {
311 if (colorFactor <= 0.01f)
312 colorFactor = 1.0f;
313
314 try {
315 ImagePtr image = Image::load(fileName);
316
317 uint8 waterc = Color::to8bit(std::string("#3300cc"));
318
319 // non pathable colors
320 Color nonPathableColors[] = {
321 std::string("#ffff00"), // yellow
322 };
323
324 // non walkable colors
325 Color nonWalkableColors[] = {
326 std::string("#000000"), // oil, black
327 std::string("#006600"), // trees, dark green
328 std::string("#ff3300"), // walls, red
329 std::string("#666666"), // mountain, grey
330 std::string("#ff6600"), // lava, orange
331 std::string("#00ff00"), // positon
332 std::string("#ccffff"), // ice, very light blue
333 };
334
335 for (int y = 0; y < image->getHeight(); ++y) {
336 for (int x = 0; x < image->getWidth(); ++x) {
337 Color color = *(uint32 *)image->getPixel(x, y);
338 uint8 c = Color::to8bit(color * colorFactor);
339 int flags = 0;
340
341 if (c == waterc || color.a() == 0) {
342 flags |= MinimapTileNotWalkable;
343 c = 255; // alpha
344 }
345
346 if (flags != 0) {
347 for (Color &col : nonWalkableColors) {
348 if (col == color) {
349 flags |= MinimapTileNotWalkable;
350 break;
351 }
352 }
353 }
354
355 if (flags != 0) {
356 for (Color &col : nonPathableColors) {
357 if (col == color) {
358 flags |= MinimapTileNotPathable;
359 break;
360 }
361 }
362 }
363
364 if (c == 255)
365 continue;
366
367 Position pos(topLeft.x + x, topLeft.y + y, topLeft.z);
368 MinimapBlock &block = getBlock(pos);
369 Point offsetPos = getBlockOffset(Point(pos.x, pos.y));
370 MinimapTile &tile =
371 block.getTile(pos.x - offsetPos.x, pos.y - offsetPos.y);
372 if (!(tile.flags & MinimapTileWasSeen)) {
373 tile.color = c;
374 tile.flags = flags;
375 block.mustUpdate();
376 }
377 }
378 }
379 return true;
380 } catch (stdext::exception &e) {
381 g_logger.error(stdext::format("failed to load OTMM minimap: %s", e.what()));
382 return false;
383 }
384}
385
386void Minimap::saveImage(const std::string &fileName, const Rect &mapRect) {
387 // TODO
388}
389
390bool Minimap::loadOtmm(const std::string &fileName) {
391 try {
392 FileStreamPtr fin = g_resources.openFile(
393 fileName, g_game.getFeature(Otc::GameDontCacheFiles));
394 if (!fin)
395 stdext::throw_exception("unable to open file");
396
397 uint32 signature = fin->getU32();
398 if (signature != OTMM_SIGNATURE)
399 stdext::throw_exception("invalid OTMM file");
400
401 uint16 start = fin->getU16();
402 uint16 version = fin->getU16();
403 fin->getU32(); // flags
404
405 switch (version) {
406 case 1: {
407 fin->getString(); // description
408 break;
409 }
410 default:
411 stdext::throw_exception("OTMM version not supported");
412 }
413
414 fin->seek(start);
415
416 uint blockSize = MMBLOCK_SIZE * MMBLOCK_SIZE * sizeof(MinimapTile);
417 std::vector<uchar> compressBuffer(compressBound(blockSize));
418 std::vector<uchar> decompressBuffer(blockSize);
419
420 while (true) {
421 Position pos;
422 pos.x = fin->getU16();
423 pos.y = fin->getU16();
424 pos.z = fin->getU8();
425
426 // end of file or file is corrupted
427 if (!pos.isValid() || pos.z >= Otc::MAX_Z + 1)
428 break;
429
430 MinimapBlock &block = getBlock(pos);
431 ulong len = fin->getU16();
432 ulong destLen = blockSize;
433 fin->read(compressBuffer.data(), len);
434 int ret = uncompress(decompressBuffer.data(), &destLen,
435 compressBuffer.data(), len);
436 if (ret != Z_OK || destLen != blockSize)
437 break;
438
439 memcpy((uchar *)&block.getTiles(), decompressBuffer.data(), blockSize);
440 block.mustUpdate();
441 block.justSaw();
442 }
443
444 fin->close();
445 return true;
446 } catch (stdext::exception &e) {
447 g_logger.error(stdext::format("failed to load OTMM minimap: %s", e.what()));
448 return false;
449 }
450}
451
452void Minimap::saveOtmm(const std::string &fileName) {
453 try {
454 stdext::timer saveTimer;
455
456#ifndef ANDROID
457 std::string tmpFileName = fileName;
458 tmpFileName += ".tmp";
459 FileStreamPtr fin = g_resources.createFile(tmpFileName);
460#else
461 FileStreamPtr fin = g_resources.createFile(fileName);
462#endif
463
464 // TODO: compression flag with zlib
465 uint32 flags = 0;
466
467 // header
468 fin->addU32(OTMM_SIGNATURE);
469 fin->addU16(0); // data start, will be overwritten later
470 fin->addU16(OTMM_VERSION);
471 fin->addU32(flags);
472
473 // version 1 header
474 fin->addString("OTMM 1.0"); // description
475
476 // go back and rewrite where the map data starts
477 uint32 start = fin->tell();
478 fin->seek(4);
479 fin->addU16(start);
480 fin->seek(start);
481
482 uint blockSize = MMBLOCK_SIZE * MMBLOCK_SIZE * sizeof(MinimapTile);
483 std::vector<uchar> compressBuffer(compressBound(blockSize));
484 const int COMPRESS_LEVEL = 3;
485
486 for (uint8_t z = 0; z <= Otc::MAX_Z; ++z) {
487 for (auto &it : m_tileBlocks[z]) {
488 int index = it.first;
489 MinimapBlock &block = *it.second;
490 if (!block.wasSeen())
491 continue;
492
493 Position pos = getIndexPosition(index, z);
494 fin->addU16(pos.x);
495 fin->addU16(pos.y);
496 fin->addU8(pos.z);
497
498 ulong len = blockSize;
499 int ret =
500 compress2(compressBuffer.data(), &len, (uchar *)&block.getTiles(),
501 blockSize, COMPRESS_LEVEL);
502 VALIDATE(ret == Z_OK);
503 fin->addU16(len);
504 fin->write(compressBuffer.data(), len);
505 }
506 }
507
508 // end of file
509 Position invalidPos;
510 fin->addU16(invalidPos.x);
511 fin->addU16(invalidPos.y);
512 fin->addU8(invalidPos.z);
513
514 fin->flush();
515
516 fin->close();
517#ifndef ANDROID
518 std::filesystem::path filePath(g_resources.getWriteDir()),
519 tmpFilePath(g_resources.getWriteDir());
520 filePath += fileName;
521 tmpFilePath += tmpFileName;
522 if (std::filesystem::file_size(tmpFilePath) > 1024) {
523 std::filesystem::rename(tmpFilePath, filePath);
524 }
525#endif
526 } catch (stdext::exception &e) {
527 g_logger.error(stdext::format("failed to save OTMM minimap: %s", e.what()));
528 } catch (std::exception &e) {
529 g_logger.error(stdext::format("failed to save OTMM minimap: %s", e.what()));
530 }
531}
532
Top