spritemanager.cpp

main 519 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 "spritemanager.h"
24#include "game.h"
25#include <framework/core/resourcemanager.h>
26#include <framework/core/filestream.h>
27#include <framework/graphics/image.h>
28#include <framework/graphics/atlas.h>
29#include <framework/util/crypt.h>
30#include <framework/util/pngunpacker.h>
31
32SpriteManager g_sprites;
33
34SpriteManager::SpriteManager()
35{
36 m_spritesCount = 0;
37 m_signature = 0;
38}
39
40void SpriteManager::terminate()
41{
42 unload();
43}
44
45bool SpriteManager::loadSpr(std::string file)
46{
47 m_spritesCount = 0;
48 m_signature = 0;
49 m_loaded = false;
50 m_sprites.clear();
51 m_spritesFile = nullptr;
52
53 auto cwmFile = g_resources.guessFilePath(file, "cwm");
54 if (g_resources.fileExists(cwmFile)) {
55 m_isHdMod = true;
56 return loadCwmSpr(cwmFile);
57 }
58
59 auto sprFile = g_resources.guessFilePath(file, "spr");
60 if (g_resources.fileExists(sprFile)) {
61 return loadCasualSpr(sprFile);
62 }
63
64 return false;
65}
66
67#ifdef WITH_ENCRYPTION
68
69void SpriteManager::saveSpr(std::string fileName)
70{
71 if (!m_loaded)
72 stdext::throw_exception("failed to save, spr is not loaded");
73 if (!m_spritesFile)
74 stdext::throw_exception("not allowed");
75
76 try {
77 FileStreamPtr fin = g_resources.createFile(fileName);
78 if (!fin)
79 stdext::throw_exception(stdext::format("failed to open file '%s' for write", fileName));
80
81 fin->addU32(m_signature);
82 if (g_game.getFeature(Otc::GameSpritesU32))
83 fin->addU32(m_spritesCount);
84 else
85 fin->addU16(m_spritesCount);
86
87 uint32 offset = fin->tell();
88 uint32 spriteAddress = offset + 4 * m_spritesCount;
89 for (int i = 1; i <= m_spritesCount; i++)
90 fin->addU32(0);
91
92 for (int i = 1; i <= m_spritesCount; i++) {
93 m_spritesFile->seek((i - 1) * 4 + m_spritesOffset);
94 uint32 fromAdress = m_spritesFile->getU32();
95 if (fromAdress != 0) {
96 fin->seek(offset + (i - 1) * 4);
97 fin->addU32(spriteAddress);
98 fin->seek(spriteAddress);
99
100 m_spritesFile->seek(fromAdress);
101 fin->addU8(m_spritesFile->getU8());
102 fin->addU8(m_spritesFile->getU8());
103 fin->addU8(m_spritesFile->getU8());
104
105 uint16 dataSize = m_spritesFile->getU16();
106 fin->addU16(dataSize);
107 std::vector<char> spriteData(m_spriteSize * m_spriteSize);
108 m_spritesFile->read(spriteData.data(), dataSize);
109 fin->write(spriteData.data(), dataSize);
110
111 spriteAddress = fin->tell();
112 }
113 //TODO: Check for overwritten sprites.
114 }
115
116 fin->flush();
117 fin->close();
118 } catch (std::exception& e) {
119 g_logger.error(stdext::format("Failed to save '%s': %s", fileName, e.what()));
120 }
121}
122
123void SpriteManager::saveSpr64(std::string fileName)
124{
125 if (!m_loaded)
126 stdext::throw_exception("failed to save, spr is not loaded");
127 if (!m_spritesFile || m_spriteSize != 32)
128 stdext::throw_exception("not allowed");
129
130 try {
131 FileStreamPtr fin = g_resources.createFile(fileName);
132 if (!fin)
133 stdext::throw_exception(stdext::format("failed to open file '%s' for write", fileName));
134
135 fin->addU32(m_signature);
136 if (g_game.getFeature(Otc::GameSpritesU32))
137 fin->addU32(m_spritesCount);
138 else
139 fin->addU16(m_spritesCount);
140
141 uint32 offset = fin->tell();
142 for (int i = 1; i <= m_spritesCount; i++)
143 fin->addU32(0);
144
145 for (int i = 1; i <= m_spritesCount; i++) {
146 ImagePtr sprite = getSpriteImage(i);
147 if (!sprite) {
148 continue;
149 }
150 sprite = sprite->upscale();
151
152 uint32 spriteAddress = fin->tell();
153 fin->seek(offset + (i - 1) * 4);
154 fin->addU32(spriteAddress);
155 fin->seek(spriteAddress);
156
157 fin->addU8(0xff);
158 fin->addU8(0x00);
159 fin->addU8(0xff);
160
161 uint8_t* pixels = sprite->getPixelData();
162 int pixelCount = sprite->getPixelCount() * 4;
163 std::vector<uint8_t> buffer(pixelCount + 1024, 0);
164 int bufferPos = 0;
165
166 int skipedPixels = 0;
167 for (int i = 0; i < pixelCount; ) {
168 int transparent = 0, colored = 0;
169 for (int j = i; j < pixelCount; j += 4) {
170 if (pixels[j + 3] == 0x00) {
171 if (colored != 0) break;
172 transparent += 1;
173 } else {
174 colored += 1;
175 }
176 }
177
178 *(uint16_t*)(buffer.data() + bufferPos) = transparent;
179 bufferPos += 2;
180 *(uint16_t*)(buffer.data() + bufferPos) = colored;
181 bufferPos += 2;
182
183 i += transparent * 4;
184
185 for (int c = 0; c < colored; ++c) {
186 buffer[bufferPos++] = pixels[i];
187 buffer[bufferPos++] = pixels[i + 1];
188 buffer[bufferPos++] = pixels[i + 2];
189 i += 4;
190 }
191 }
192
193 fin->addU16(bufferPos);
194 fin->write(buffer.data(), bufferPos);
195 }
196
197 fin->flush();
198 fin->close();
199 } catch (std::exception& e) {
200 g_logger.error(stdext::format("Failed to save '%s': %s", fileName, e.what()));
201 }
202}
203
204void SpriteManager::encryptSprites(std::string fileName)
205{
206 if (!m_loaded)
207 stdext::throw_exception("failed to save, spr is not loaded");
208
209 try {
210 FileStreamPtr fin = g_resources.createFile(fileName);
211 if (!fin)
212 stdext::throw_exception(stdext::format("failed to open file '%s' for write", fileName));
213
214 const char otcv8Signature[] = "OTV8";
215 fin->addU32(*((uint32_t*)otcv8Signature));
216 fin->addU32(m_signature);
217 fin->addU32(m_spritesCount);
218
219 for (int i = 1; i <= m_spritesCount; i++) {
220 ImagePtr sprite = getSpriteImage(i);
221 if (!sprite) {
222 fin->addU16(0);
223 continue;
224 }
225 uint8_t* pixels = sprite->getPixelData();
226 int pixelCount = sprite->getPixelCount() * 4;
227 std::vector<uint8_t> buffer(pixelCount + 1024, 0);
228 int bufferPos = 0;
229
230 bool hasAlpha = false;
231 for (int i = 3; i < pixelCount; i += 4) {
232 if (pixels[i] != 0x00 && pixels[i] != 0xFF) {
233 hasAlpha = true;
234 break;
235 }
236 }
237
238 buffer[bufferPos++] = (hasAlpha ? 1 : 0);
239 int skipedPixels = 0;
240 for (int i = 0; i < pixelCount; ) {
241 int transparent = 0, colored = 0;
242 for (int j = i; j < pixelCount; j += 4) {
243 if (pixels[j + 3] == 0x00) {
244 if (colored != 0) break;
245 transparent += 1;
246 } else {
247 colored += 1;
248 }
249 }
250
251 *(uint16_t*)(buffer.data() + bufferPos) = transparent;
252 bufferPos += 2;
253 *(uint16_t*)(buffer.data() + bufferPos) = colored;
254 bufferPos += 2;
255
256 i += transparent * 4;
257
258 for (int c = 0; c < colored; ++c) {
259 buffer[bufferPos++] = pixels[i];
260 buffer[bufferPos++] = pixels[i + 1];
261 buffer[bufferPos++] = pixels[i + 2];
262 if (hasAlpha) {
263 buffer[bufferPos++] = pixels[i + 3];
264 }
265 i += 4;
266 }
267 }
268
269 g_crypt.bencrypt(buffer.data(), bufferPos, (uint64_t)m_signature + i);
270 fin->addU16(bufferPos);
271 fin->write(buffer.data(), bufferPos);
272 }
273
274 fin->flush();
275 fin->close();
276 }
277 catch (std::exception& e) {
278 g_logger.error(stdext::format("Failed to save '%s': %s", fileName, e.what()));
279 }
280}
281
282void SpriteManager::dumpSprites(std::string dir)
283{
284 if (dir.empty()) {
285 g_logger.error("Empty dir for sprites dump");
286 return;
287 }
288 g_resources.makeDir(dir);
289 for (int i = 1; i <= m_spritesCount; i++) {
290 auto img = getSpriteImage(i);
291 if (!img) continue;
292 img->savePNG(dir + "/" + std::to_string(i) + ".png");
293 }
294}
295
296#endif
297
298void SpriteManager::unload()
299{
300 m_spritesCount = 0;
301 m_signature = 0;
302 m_spritesFile = nullptr;
303 m_sprites.clear();
304}
305
306ImagePtr SpriteManager::getSpriteImage(int id)
307{
308 if (m_isHdMod) {
309 return getSpriteImageHd(id);
310 }
311 else {
312 return getSpriteImageCasual(id);
313 }
314}
315
316bool SpriteManager::loadCasualSpr(std::string file)
317{
318 m_spriteSize = 32u;
319 try {
320 file = g_resources.guessFilePath(file, "spr");
321
322 m_spritesFile = g_resources.openFile(file, true);
323
324 m_signature = m_spritesFile->getU32();
325 if (m_signature == *((uint32_t*)"OTV8")) {
326 m_signature = m_spritesFile->getU32();
327 m_spritesCount = m_spritesFile->getU32();
328 m_sprites.resize(m_spritesCount + 1);
329 for (int i = 1; i <= m_spritesCount; ++i) {
330 int bufferSize = m_spritesFile->getU16();
331 if (bufferSize == 0) continue;
332 m_sprites[i].resize(bufferSize + 1);
333 m_sprites[i][0] = 0;
334 m_spritesFile->read(m_sprites[i].data() + 1, bufferSize);
335 }
336 m_spritesFile = nullptr;
337 }
338 else {
339 m_spritesCount = g_game.getFeature(Otc::GameSpritesU32) ? m_spritesFile->getU32() : m_spritesFile->getU16();
340 m_spritesOffset = m_spritesFile->tell();
341 }
342 m_loaded = true;
343 g_lua.callGlobalField("g_sprites", "onLoadSpr", file);
344 return true;
345 }
346 catch (std::exception& e) {
347 m_spritesFile = nullptr;
348 m_sprites.clear();
349 g_logger.error(stdext::format("Failed to load sprites from '%s': %s", file, e.what()));
350 return false;
351 }
352}
353
354bool SpriteManager::loadCwmSpr(std::string file)
355{
356 try {
357 auto inFilePath = g_resources.guessFilePath(file, "cwm");
358 auto spritesFile = g_resources.openFile(inFilePath, g_game.getFeature(Otc::GameDontCacheFiles));
359
360 uint8_t version = spritesFile->getU8();
361 if (version != 0x01) {
362 g_logger.error(stdext::format("Invalid CWM file version - %s", file));
363 return false;
364 }
365
366 m_spriteSize = spritesFile->getU16();
367 m_cachedData = std::move(PngUnpacker::unpack(spritesFile));
368 m_spritesCount = m_cachedData.size();
369
370 if (m_spritesCount == 0) {
371 g_logger.error(stdext::format("Failed to load sprites from '%s' - no sprites", file));
372 return false;
373 }
374
375 m_loaded = true;
376 return true;
377 }
378 catch (stdext::exception& e) {
379 g_logger.error(stdext::format("Failed to load sprites from '%s': %s", file, e.what()));
380 return false;
381 }
382
383 return false;
384}
385
386ImagePtr SpriteManager::getSpriteImageCasual(int id)
387{
388 try {
389 int spriteDataSize = m_spriteSize * m_spriteSize * 4;
390
391 if (!m_sprites.empty()) {
392 if (id >= (int)m_sprites.size())
393 return nullptr;
394 auto& buffer = m_sprites[id];
395 if (buffer.size() < 5)
396 return nullptr;
397 if (buffer[0] == 0) {
398 buffer[0] = 1;
399 g_crypt.bdecrypt(buffer.data() + 1, buffer.size() - 1, (uint64_t)m_signature + id);
400 }
401
402 if (buffer[1] > 1) {
403 stdext::throw_exception("Invalid sprite encryption");
404 }
405
406 bool hasAlpha = (buffer[1] == 1);
407
408 ImagePtr image(new Image(Size(m_spriteSize, m_spriteSize)));
409 uint8* pixels = image->getPixelData();
410 int writePos = 0;
411
412 size_t bufferPos = 2;
413 while (bufferPos != buffer.size()) {
414 uint16_t transparentPixels = *(uint16_t*)(&buffer[bufferPos]);
415 bufferPos += 2;
416 uint16_t coloredPixels = *(uint16_t*)(&buffer[bufferPos]);
417 bufferPos += 2;
418
419 writePos += transparentPixels * 4;
420 for (int i = 0; i < coloredPixels; ++i) {
421 pixels[writePos++] = buffer[bufferPos++];
422 pixels[writePos++] = buffer[bufferPos++];
423 pixels[writePos++] = buffer[bufferPos++];
424 if (hasAlpha) {
425 pixels[writePos] = buffer[bufferPos++];
426 }
427 else {
428 pixels[writePos] = 0xFF;
429 }
430 writePos += 1;
431 }
432 }
433
434 return image;
435 }
436
437 if (id == 0 || !m_spritesFile)
438 return nullptr;
439
440 m_spritesFile->seek(((id - 1) * 4) + m_spritesOffset);
441
442 uint32 spriteAddress = m_spritesFile->getU32();
443
444 // no sprite? return an empty texture
445 if (spriteAddress == 0)
446 return nullptr;
447
448 m_spritesFile->seek(spriteAddress);
449
450 // color key
451 m_spritesFile->getU8();
452 m_spritesFile->getU8();
453 m_spritesFile->getU8();
454
455 uint16 pixelDataSize = m_spritesFile->getU16();
456
457 ImagePtr image(new Image(Size(m_spriteSize, m_spriteSize)));
458
459 uint8* pixels = image->getPixelData();
460 int writePos = 0;
461 bool useAlpha = g_game.getFeature(Otc::GameSpritesAlphaChannel);
462
463 std::vector<uint8> data(pixelDataSize);
464 if (pixelDataSize > 0)
465 m_spritesFile->read(data.data(), pixelDataSize);
466
467 // decompress pixels
468 size_t read = 0;
469 while (read + 4 <= data.size() && writePos < spriteDataSize) {
470 uint16 transparentPixels = data[read] | (data[read + 1] << 8);
471 uint16 coloredPixels = data[read + 2] | (data[read + 3] << 8);
472 read += 4;
473
474 writePos += transparentPixels * 4;
475 if (writePos >= spriteDataSize)
476 break;
477
478 if (useAlpha) {
479 size_t count = std::min<size_t>({ static_cast<size_t>(coloredPixels) * 4, static_cast<size_t>(spriteDataSize - writePos), data.size() - read });
480 memcpy(&pixels[writePos], &data[read], count);
481 writePos += coloredPixels * 4;
482 read += static_cast<size_t>(coloredPixels) * 4;
483 }
484 else {
485 for (int i = 0; i < coloredPixels && writePos < spriteDataSize && read + 3 <= data.size(); i++) {
486 pixels[writePos + 0] = data[read];
487 pixels[writePos + 1] = data[read + 1];
488 pixels[writePos + 2] = data[read + 2];
489 pixels[writePos + 3] = 0xFF;
490 writePos += 4;
491 read += 3;
492 }
493 }
494 }
495
496 return image;
497 }
498 catch (stdext::exception& e) {
499 g_logger.error(stdext::format("Failed to get sprite id %d: %s", id, e.what()));
500 return nullptr;
501 }
502}
503
504ImagePtr SpriteManager::getSpriteImageHd(int id)
505{
506 if (id == 0 || !m_loaded)
507 return nullptr;
508
509 if (m_cachedData.find(id) == m_cachedData.end())
510 {
511 return nullptr;
512 }
513
514 try {
515 return Image::loadPNG(m_cachedData[id].data(), m_cachedData[id].size());
516 } catch (...) {}
517 return nullptr;
518}
519
Top