aboutsummaryrefslogtreecommitdiff
path: root/src/database-sqlite3.cpp
blob: 7bc87a7d0bb4a7493fdabd356bdb890f07386d5f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
/*
Minetest
Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

/*
SQLite format specification:
	blocks:
		(PK) INT id
		BLOB data
*/


#include "database-sqlite3.h"

#include "log.h"
#include "filesys.h"
#include "exceptions.h"
#include "settings.h"
#include "porting.h"
#include "util/string.h"
#include "content_sao.h"
#include "remoteplayer.h"

#include <cassert>

// When to print messages when the database is being held locked by another process
// Note: I've seen occasional delays of over 250ms while running minetestmapper.
#define BUSY_INFO_TRESHOLD	100	// Print first informational message after 100ms.
#define BUSY_WARNING_TRESHOLD	250	// Print warning message after 250ms. Lag is increased.
#define BUSY_ERROR_TRESHOLD	1000	// Print error message after 1000ms. Significant lag.
#define BUSY_FATAL_TRESHOLD	3000	// Allow SQLITE_BUSY to be returned, which will cause a minetest crash.
#define BUSY_ERROR_INTERVAL	10000	// Safety net: report again every 10 seconds


#define SQLRES(s, r, m) \
	if ((s) != (r)) { \
		throw DatabaseException(std::string(m) + ": " +\
				sqlite3_errmsg(m_database)); \
	}
#define SQLOK(s, m) SQLRES(s, SQLITE_OK, m)

#define PREPARE_STATEMENT(name, query) \
	SQLOK(sqlite3_prepare_v2(m_database, query, -1, &m_stmt_##name, NULL),\
		"Failed to prepare query '" query "'")

#define SQLOK_ERRSTREAM(s, m)                           \
	if ((s) != SQLITE_OK) {                             \
		errorstream << (m) << ": "                      \
			<< sqlite3_errmsg(m_database) << std::endl; \
	}

#define FINALIZE_STATEMENT(statement) SQLOK_ERRSTREAM(sqlite3_finalize(statement), \
	"Failed to finalize " #statement)

int Database_SQLite3::busyHandler(void *data, int count)
{
	s64 &first_time = reinterpret_cast<s64 *>(data)[0];
	s64 &prev_time = reinterpret_cast<s64 *>(data)[1];
	s64 cur_time = porting::getTimeMs();

	if (count == 0) {
		first_time = cur_time;
		prev_time = first_time;
	} else {
		while (cur_time < prev_time)
			cur_time += s64(1)<<32;
	}

	if (cur_time - first_time < BUSY_INFO_TRESHOLD) {
		; // do nothing
	} else if (cur_time - first_time >= BUSY_INFO_TRESHOLD &&
			prev_time - first_time < BUSY_INFO_TRESHOLD) {
		infostream << "SQLite3 database has been locked for "
			<< cur_time - first_time << " ms." << std::endl;
	} else if (cur_time - first_time >= BUSY_WARNING_TRESHOLD &&
			prev_time - first_time < BUSY_WARNING_TRESHOLD) {
		warningstream << "SQLite3 database has been locked for "
			<< cur_time - first_time << " ms." << std::endl;
	} else if (cur_time - first_time >= BUSY_ERROR_TRESHOLD &&
			prev_time - first_time < BUSY_ERROR_TRESHOLD) {
		errorstream << "SQLite3 database has been locked for "
			<< cur_time - first_time << " ms; this causes lag." << std::endl;
	} else if (cur_time - first_time >= BUSY_FATAL_TRESHOLD &&
			prev_time - first_time < BUSY_FATAL_TRESHOLD) {
		errorstream << "SQLite3 database has been locked for "
			<< cur_time - first_time << " ms - giving up!" << std::endl;
	} else if ((cur_time - first_time) / BUSY_ERROR_INTERVAL !=
			(prev_time - first_time) / BUSY_ERROR_INTERVAL) {
		// Safety net: keep reporting every BUSY_ERROR_INTERVAL
		errorstream << "SQLite3 database has been locked for "
			<< (cur_time - first_time) / 1000 << " seconds!" << std::endl;
	}

	prev_time = cur_time;

	// Make sqlite transaction fail if delay exceeds BUSY_FATAL_TRESHOLD
	return cur_time - first_time < BUSY_FATAL_TRESHOLD;
}


Database_SQLite3::Database_SQLite3(const std::string &savedir, const std::string &dbname) :
	m_database(NULL),
	m_initialized(false),
	m_savedir(savedir),
	m_dbname(dbname),
	m_stmt_begin(NULL),
	m_stmt_end(NULL)
{
}

void Database_SQLite3::beginSave()
{
	verifyDatabase();
	SQLRES(sqlite3_step(m_stmt_begin), SQLITE_DONE,
		"Failed to start SQLite3 transaction");
	sqlite3_reset(m_stmt_begin);
}

void Database_SQLite3::endSave()
{
	verifyDatabase();
	SQLRES(sqlite3_step(m_stmt_end), SQLITE_DONE,
		"Failed to commit SQLite3 transaction");
	sqlite3_reset(m_stmt_end);
}

void Database_SQLite3::openDatabase()
{
	if (m_database) return;

	std::string dbp = m_savedir + DIR_DELIM + m_dbname + ".sqlite";

	// Open the database connection

	if (!fs::CreateAllDirs(m_savedir)) {
		infostream << "Database_SQLite3: Failed to create directory \""
			<< m_savedir << "\"" << std::endl;
		throw FileNotGoodException("Failed to create database "
				"save directory");
	}

	bool needs_create = !fs::PathExists(dbp);

	SQLOK(sqlite3_open_v2(dbp.c_str(), &m_database,
			SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL),
		std::string("Failed to open SQLite3 database file ") + dbp);

	SQLOK(sqlite3_busy_handler(m_database, Database_SQLite3::busyHandler,
		m_busy_handler_data), "Failed to set SQLite3 busy handler");

	if (needs_create) {
		createDatabase();
	}

	std::string query_str = std::string("PRAGMA synchronous = ")
			 + itos(g_settings->getU16("sqlite_synchronous"));
	SQLOK(sqlite3_exec(m_database, query_str.c_str(), NULL, NULL, NULL),
		"Failed to modify sqlite3 synchronous mode");
	SQLOK(sqlite3_exec(m_database, "PRAGMA foreign_keys = ON", NULL, NULL, NULL),
		"Failed to enable sqlite3 foreign key support");
}

void Database_SQLite3::verifyDatabase()
{
	if (m_initialized) return;

	openDatabase();

	PREPARE_STATEMENT(begin, "BEGIN;");
	PREPARE_STATEMENT(end, "COMMIT;");

	initStatements();

	m_initialized = true;
}

Database_SQLite3::~Database_SQLite3()
{
	FINALIZE_STATEMENT(m_stmt_begin)
	FINALIZE_STATEMENT(m_stmt_end)

	SQLOK_ERRSTREAM(sqlite3_close(m_database), "Failed to close database");
}

/*
 * Map database
 */

MapDatabaseSQLite3::MapDatabaseSQLite3(const std::string &savedir):
	Database_SQLite3(savedir, "map"),
	MapDatabase(),
	m_stmt_read(NULL),
	m_stmt_write(NULL),
	m_stmt_list(NULL),
	m_stmt_delete(NULL)
{

}

MapDatabaseSQLite3::~MapDatabaseSQLite3()
{
	FINALIZE_STATEMENT(m_stmt_read)
	FINALIZE_STATEMENT(m_stmt_write)
	FINALIZE_STATEMENT(m_stmt_list)
	FINALIZE_STATEMENT(m_stmt_delete)
}


void MapDatabaseSQLite3::createDatabase()
{
	assert(m_database); // Pre-condition

	SQLOK(sqlite3_exec(m_database,
		"CREATE TABLE IF NOT EXISTS `blocks` (\n"
			"	`pos` INT PRIMARY KEY,\n"
			"	`data` BLOB\n"
			");\n",
		NULL, NULL, NULL),
		"Failed to create database table");
}

void MapDatabaseSQLite3::initStatements()
{
	PREPARE_STATEMENT(read, "SELECT `data` FROM `blocks` WHERE `pos` = ? LIMIT 1");
#ifdef __ANDROID__
	PREPARE_STATEMENT(write,  "INSERT INTO `blocks` (`pos`, `data`) VALUES (?, ?)");
#else
	PREPARE_STATEMENT(write, "REPLACE INTO `blocks` (`pos`, `data`) VALUES (?, ?)");
#endif
	PREPARE_STATEMENT(delete, "DELETE FROM `blocks` WHERE `pos` = ?");
	PREPARE_STATEMENT(list, "SELECT `pos` FROM `blocks`");

	verbosestream << "ServerMap: SQLite3 database opened." << std::endl;
}

inline void MapDatabaseSQLite3::bindPos(sqlite3_stmt *stmt, const v3s16 &pos, int index)
{
	SQLOK(sqlite3_bind_int64(stmt, index, getBlockAsInteger(pos)),
		"Internal error: failed to bind query at " __FILE__ ":" TOSTRING(__LINE__));
}

bool MapDatabaseSQLite3::deleteBlock(const v3s16 &pos)
{
	verifyDatabase();

	bindPos(m_stmt_delete, pos);

	bool good = sqlite3_step(m_stmt_delete) == SQLITE_DONE;
	sqlite3_reset(m_stmt_delete);

	if (!good) {
		warningstream << "deleteBlock: Block failed to delete "
			<< PP(pos) << ": " << sqlite3_errmsg(m_database) << std::endl;
	}
	return good;
}

bool MapDatabaseSQLite3::saveBlock(const v3s16 &pos, const std::string &data)
{
	verifyDatabase();

#ifdef __ANDROID__
	/**
	 * Note: For some unknown reason SQLite3 fails to REPLACE blocks on Android,
	 * deleting them and then inserting works.
	 */
	bindPos(m_stmt_read, pos);

	if (sqlite3_step(m_stmt_read) == SQLITE_ROW) {
		deleteBlock(pos);
	}
	sqlite3_reset(m_stmt_read);
#endif

	bindPos(m_stmt_write, pos);
	SQLOK(sqlite3_bind_blob(m_stmt_write, 2, data.data(), data.size(), NULL),
		"Internal error: failed to bind query at " __FILE__ ":" TOSTRING(__LINE__));

	SQLRES(sqlite3_step(m_stmt_write), SQLITE_DONE, "Failed to save block")
	sqlite3_reset(m_stmt_write);

	return true;
}

void MapDatabaseSQLite3::loadBlock(const v3s16 &pos, std::string *block)
{
	verifyDatabase();

	bindPos(m_stmt_read, pos);

	if (sqlite3_step(m_stmt_read) != SQLITE_ROW) {
		sqlite3_reset(m_stmt_read);
		return;
	}

	const char *data = (const char *) sqlite3_column_blob(m_stmt_read, 0);
	size_t len = sqlite3_column_bytes(m_stmt_read, 0);

	*block = (data) ? std::string(data, len) : "";

	sqlite3_step(m_stmt_read);
	// We should never get more than 1 row, so ok to reset
	sqlite3_reset(m_stmt_read);
}

void MapDatabaseSQLite3::listAllLoadableBlocks(std::vector<v3s16> &dst)
{
	verifyDatabase();

	while (sqlite3_step(m_stmt_list) == SQLITE_ROW)
		dst.push_back(getIntegerAsBlock(sqlite3_column_int64(m_stmt_list, 0)));

	sqlite3_reset(m_stmt_list);
}

/*
 * Player Database
 */

PlayerDatabaseSQLite3::PlayerDatabaseSQLite3(const std::string &savedir):
	Database_SQLite3(savedir, "players"),
	PlayerDatabase(),
	m_stmt_player_load(NULL),
	m_stmt_player_add(NULL),
	m_stmt_player_update(NULL),
	m_stmt_player_remove(NULL),
	m_stmt_player_list(NULL),
	m_stmt_player_load_inventory(NULL),
	m_stmt_player_load_inventory_items(NULL),
	m_stmt_player_add_inventory(NULL),
	m_stmt_player_add_inventory_items(NULL),
	m_stmt_player_remove_inventory(NULL),
	m_stmt_player_remove_inventory_items(NULL),
	m_stmt_player_metadata_load(NULL),
	m_stmt_player_metadata_remove(NULL),
	m_stmt_player_metadata_add(NULL)
{

}
PlayerDatabaseSQLite3::~PlayerDatabaseSQLite3()
{
	FINALIZE_STATEMENT(m_stmt_player_load)
	FINALIZE_STATEMENT(m_stmt_player_add)
	FINALIZE_STATEMENT(m_stmt_player_update)
	FINALIZE_STATEMENT(m_stmt_player_remove)
	FINALIZE_STATEMENT(m_stmt_player_list)
	FINALIZE_STATEMENT(m_stmt_player_add_inventory)
	FINALIZE_STATEMENT(m_stmt_player_add_inventory_items)
	FINALIZE_STATEMENT(m_stmt_player_remove_inventory)
	FINALIZE_STATEMENT(m_stmt_player_remove_inventory_items)
	FINALIZE_STATEMENT(m_stmt_player_load_inventory)
	FINALIZE_STATEMENT(m_stmt_player_load_inventory_items)
	FINALIZE_STATEMENT(m_stmt_player_metadata_load)
	FINALIZE_STATEMENT(m_stmt_player_metadata_add)
	FINALIZE_STATEMENT(m_stmt_player_metadata_remove)
};


void PlayerDatabaseSQLite3::createDatabase()
{
	assert(m_database); // Pre-condition

	SQLOK(sqlite3_exec(m_database,
		"CREATE TABLE IF NOT EXISTS `player` ("
			"`name` VARCHAR(50) NOT NULL,"
			"`pitch` NUMERIC(11, 4) NOT NULL,"
			"`yaw` NUMERIC(11, 4) NOT NULL,"
			"`posX` NUMERIC(11, 4) NOT NULL,"
			"`posY` NUMERIC(11, 4) NOT NULL,"
			"`posZ` NUMERIC(11, 4) NOT NULL,"
			"`hp` INT NOT NULL,"
			"`breath` INT NOT NULL,"
			"`creation_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,"
			"`modification_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,"
			"PRIMARY KEY (`name`));",
		NULL, NULL, NULL),
		"Failed to create player table");

	SQLOK(sqlite3_exec(m_database,
		"CREATE TABLE IF NOT EXISTS `player_metadata` ("
			"    `player` VARCHAR(50) NOT NULL,"
			"    `metadata` VARCHAR(256) NOT NULL,"
			"    `value` TEXT,"
			"    PRIMARY KEY(`player`, `metadata`),"
			"    FOREIGN KEY (`player`) REFERENCES player (`name`) ON DELETE CASCADE );",
		NULL, NULL, NULL),
		"Failed to create player metadata table");

	SQLOK(sqlite3_exec(m_database,
		"CREATE TABLE IF NOT EXISTS `player_inventories` ("
			"   `player` VARCHAR(50) NOT NULL,"
			"	`inv_id` INT NOT NULL,"
			"	`inv_width` INT NOT NULL,"
			"	`inv_name` TEXT NOT NULL DEFAULT '',"
			"	`inv_size` INT NOT NULL,"
			"	PRIMARY KEY(player, inv_id),"
			"   FOREIGN KEY (`player`) REFERENCES player (`name`) ON DELETE CASCADE );",
		NULL, NULL, NULL),
		"Failed to create player inventory table");

	SQLOK(sqlite3_exec(m_database,
		"CREATE TABLE `player_inventory_items` ("
			"   `player` VARCHAR(50) NOT NULL,"
			"	`inv_id` INT NOT NULL,"
			"	`slot_id` INT NOT NULL,"
			"	`item` TEXT NOT NULL DEFAULT '',"
			"	PRIMARY KEY(player, inv_id, slot_id),"
			"   FOREIGN KEY (`player`) REFERENCES player (`name`) ON DELETE CASCADE );",
		NULL, NULL, NULL),
		"Failed to create player inventory items table");
}

void PlayerDatabaseSQLite3::initStatements()
{
	PREPARE_STATEMENT(player_load, "SELECT `pitch`, `yaw`, `posX`, `posY`, `posZ`, `hp`, "
		"`breath`"
		"FROM `player` WHERE `name` = ?")
	PREPARE_STATEMENT(player_add, "INSERT INTO `player` (`name`, `pitch`, `yaw`, `posX`, "
		"`posY`, `posZ`, `hp`, `breath`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
	PREPARE_STATEMENT(player_update, "UPDATE `player` SET `pitch` = ?, `yaw` = ?, "
			"`posX` = ?, `posY` = ?, `posZ` = ?, `hp` = ?, `breath` = ?, "
			"`modification_date` = CURRENT_TIMESTAMP WHERE `name` = ?")
	PREPARE_STATEMENT(player_remove, "DELETE FROM `player` WHERE `name` = ?")
	PREPARE_STATEMENT(player_list, "SELECT `name` FROM `player`")

	PREPARE_STATEMENT(player_add_inventory, "INSERT INTO `player_inventories` "
		"(`player`, `inv_id`, `inv_width`, `inv_name`, `inv_size`) VALUES (?, ?, ?, ?, ?)")
	PREPARE_STATEMENT(player_add_inventory_items, "INSERT INTO `player_inventory_items` "
		"(`player`, `inv_id`, `slot_id`, `item`) VALUES (?, ?, ?, ?)")
	PREPARE_STATEMENT(player_remove_inventory, "DELETE FROM `player_inventories` "
		"WHERE `player` = ?")
	PREPARE_STATEMENT(player_remove_inventory_items, "DELETE FROM `player_inventory_items` "
		"WHERE `player` = ?")
	PREPARE_STATEMENT(player_load_inventory, "SELECT `inv_id`, `inv_width`, `inv_name`, "
		"`inv_size` FROM `player_inventories` WHERE `player` = ? ORDER BY inv_id")
	PREPARE_STATEMENT(player_load_inventory_items, "SELECT `slot_id`, `item` "
		"FROM `player_inventory_items` WHERE `player` = ? AND `inv_id` = ?")

	PREPARE_STATEMENT(player_metadata_load, "SELECT `metadata`, `value` FROM "
		"`player_metadata` WHERE `player` = ?")
	PREPARE_STATEMENT(player_metadata_add, "INSERT INTO `player_metadata` "
		"(`player`, `metadata`, `value`) VALUES (?, ?, ?)")
	PREPARE_STATEMENT(player_metadata_remove, "DELETE FROM `player_metadata` "
		"WHERE `player` = ?")
	verbosestream << "ServerEnvironment: SQLite3 database opened (players)." << std::endl;
}

bool PlayerDatabaseSQLite3::playerDataExists(const std::string &name)
{
	verifyDatabase();
	str_to_sqlite(m_stmt_player_load, 1, name);
	bool res = (sqlite3_step(m_stmt_player_load) == SQLITE_ROW);
	sqlite3_reset(m_stmt_player_load);
	return res;
}

void PlayerDatabaseSQLite3::savePlayer(RemotePlayer *player)
{
	PlayerSAO* sao = player->getPlayerSAO();
	sanity_check(sao);

	const v3f &pos = sao->getBasePosition();
	// Begin save in brace is mandatory
	if (!playerDataExists(player->getName())) {
		beginSave();
		str_to_sqlite(m_stmt_player_add, 1, player->getName());
		double_to_sqlite(m_stmt_player_add, 2, sao->getPitch());
		double_to_sqlite(m_stmt_player_add, 3, sao->getYaw());
		double_to_sqlite(m_stmt_player_add, 4, pos.X);
		double_to_sqlite(m_stmt_player_add, 5, pos.Y);
		double_to_sqlite(m_stmt_player_add, 6, pos.Z);
		int64_to_sqlite(m_stmt_player_add, 7, sao->getHP());
		int64_to_sqlite(m_stmt_player_add, 8, sao->getBreath());

		sqlite3_vrfy(sqlite3_step(m_stmt_player_add), SQLITE_DONE);
		sqlite3_reset(m_stmt_player_add);
	} else {
		beginSave();
		double_to_sqlite(m_stmt_player_update, 1, sao->getPitch());
		double_to_sqlite(m_stmt_player_update, 2, sao->getYaw());
		double_to_sqlite(m_stmt_player_update, 3, pos.X);
		double_to_sqlite(m_stmt_player_update, 4, pos.Y);
		double_to_sqlite(m_stmt_player_update, 5, pos.Z);
		int64_to_sqlite(m_stmt_player_update, 6, sao->getHP());
		int64_to_sqlite(m_stmt_player_update, 7, sao->getBreath());
		str_to_sqlite(m_stmt_player_update, 8, player->getName());

		sqlite3_vrfy(sqlite3_step(m_stmt_player_update), SQLITE_DONE);
		sqlite3_reset(m_stmt_player_update);
	}

	// Write player inventories
	str_to_sqlite(m_stmt_player_remove_inventory, 1, player->getName());
	sqlite3_vrfy(sqlite3_step(m_stmt_player_remove_inventory), SQLITE_DONE);
	sqlite3_reset(m_stmt_player_remove_inventory);

	str_to_sqlite(m_stmt_player_remove_inventory_items, 1, player->getName());
	sqlite3_vrfy(sqlite3_step(m_stmt_player_remove_inventory_items), SQLITE_DONE);
	sqlite3_reset(m_stmt_player_remove_inventory_items);

	std::vector<const InventoryList*> inventory_lists = sao->getInventory()->getLists();
	for (u16 i = 0; i < inventory_lists.size(); i++) {
		const InventoryList* list = inventory_lists[i];

		str_to_sqlite(m_stmt_player_add_inventory, 1, player->getName());
		int_to_sqlite(m_stmt_player_add_inventory, 2, i);
		int_to_sqlite(m_stmt_player_add_inventory, 3, list->getWidth());
		str_to_sqlite(m_stmt_player_add_inventory, 4, list->getName());
		int_to_sqlite(m_stmt_player_add_inventory, 5, list->getSize());
		sqlite3_vrfy(sqlite3_step(m_stmt_player_add_inventory), SQLITE_DONE);
		sqlite3_reset(m_stmt_player_add_inventory);

		for (u32 j = 0; j < list->getSize(); j++) {
			std::ostringstream os;
			list->getItem(j).serialize(os);
			std::string itemStr = os.str();

			str_to_sqlite(m_stmt_player_add_inventory_items, 1, player->getName());
			int_to_sqlite(m_stmt_player_add_inventory_items, 2, i);
			int_to_sqlite(m_stmt_player_add_inventory_items, 3, j);
			str_to_sqlite(m_stmt_player_add_inventory_items, 4, itemStr);
			sqlite3_vrfy(sqlite3_step(m_stmt_player_add_inventory_items), SQLITE_DONE);
			sqlite3_reset(m_stmt_player_add_inventory_items);
		}
	}

	str_to_sqlite(m_stmt_player_metadata_remove, 1, player->getName());
	sqlite3_vrfy(sqlite3_step(m_stmt_player_metadata_remove), SQLITE_DONE);
	sqlite3_reset(m_stmt_player_metadata_remove);

	const PlayerAttributes &attrs = sao->getExtendedAttributes();
	for (PlayerAttributes::const_iterator it = attrs.begin(); it != attrs.end(); ++it) {
		str_to_sqlite(m_stmt_player_metadata_add, 1, player->getName());
		str_to_sqlite(m_stmt_player_metadata_add, 2, it->first);
		str_to_sqlite(m_stmt_player_metadata_add, 3, it->second);
		sqlite3_vrfy(sqlite3_step(m_stmt_player_metadata_add), SQLITE_DONE);
		sqlite3_reset(m_stmt_player_metadata_add);
	}

	endSave();
}

bool PlayerDatabaseSQLite3::loadPlayer(RemotePlayer *player, PlayerSAO *sao)
{
	verifyDatabase();

	str_to_sqlite(m_stmt_player_load, 1, player->getName());
	if (sqlite3_step(m_stmt_player_load) != SQLITE_ROW) {
		sqlite3_reset(m_stmt_player_load);
		return false;
	}
	sao->setPitch(sqlite_to_float(m_stmt_player_load, 0));
	sao->setYaw(sqlite_to_float(m_stmt_player_load, 1));
	sao->setBasePosition(sqlite_to_v3f(m_stmt_player_load, 2));
	sao->setHPRaw((s16) MYMIN(sqlite_to_int(m_stmt_player_load, 5), S16_MAX));
	sao->setBreath((u16) MYMIN(sqlite_to_int(m_stmt_player_load, 6), U16_MAX), false);
	sqlite3_reset(m_stmt_player_load);

	// Load inventory
	str_to_sqlite(m_stmt_player_load_inventory, 1, player->getName());
	while (sqlite3_step(m_stmt_player_load_inventory) == SQLITE_ROW) {
		InventoryList *invList = player->inventory.addList(
			sqlite_to_string(m_stmt_player_load_inventory, 2),
			sqlite_to_uint(m_stmt_player_load_inventory, 3));
		invList->setWidth(sqlite_to_uint(m_stmt_player_load_inventory, 1));

		u32 invId = sqlite_to_uint(m_stmt_player_load_inventory, 0);

		str_to_sqlite(m_stmt_player_load_inventory_items, 1, player->getName());
		int_to_sqlite(m_stmt_player_load_inventory_items, 2, invId);
		while (sqlite3_step(m_stmt_player_load_inventory_items) == SQLITE_ROW) {
			const std::string itemStr = sqlite_to_string(m_stmt_player_load_inventory_items, 1);
			if (itemStr.length() > 0) {
				ItemStack stack;
				stack.deSerialize(itemStr);
				invList->addItem(sqlite_to_uint(m_stmt_player_load_inventory_items, 0), stack);
			}
		}
		sqlite3_reset(m_stmt_player_load_inventory_items);
	}

	sqlite3_reset(m_stmt_player_load_inventory);

	str_to_sqlite(m_stmt_player_metadata_load, 1, sao->getPlayer()->getName());
	while (sqlite3_step(m_stmt_player_metadata_load) == SQLITE_ROW) {
		std::string attr = sqlite_to_string(m_stmt_player_metadata_load, 0);
		std::string value = sqlite_to_string(m_stmt_player_metadata_load, 1);

		sao->setExtendedAttribute(attr, value);
	}
	sqlite3_reset(m_stmt_player_metadata_load);
	return true;
}

bool PlayerDatabaseSQLite3::removePlayer(const std::string &name)
{
	if (!playerDataExists(name))
		return false;

	str_to_sqlite(m_stmt_player_remove, 1, name);
	sqlite3_vrfy(sqlite3_step(m_stmt_player_remove), SQLITE_DONE);
	sqlite3_reset(m_stmt_player_remove);
	return true;
}

void PlayerDatabaseSQLite3::listPlayers(std::vector<std::string> &res)
{
	verifyDatabase();

	while (sqlite3_step(m_stmt_player_list) == SQLITE_ROW)
		res.push_back(sqlite_to_string(m_stmt_player_list, 0));

	sqlite3_reset(m_stmt_player_list);
}
one.png^mineral_coal.png" "stone.png^mineral_coal.png^crack1" - If texture specified by name is found from cache, return the cached id. - Otherwise generate the texture, add to cache and return id. Recursion is used to find out the largest found part of the texture and continue based on it. The id 0 points to a NULL texture. It is returned in case of error. */ u32 getTextureIdDirect(const std::string &name); // Finds out the name of a cached texture. std::string getTextureName(u32 id); /* If texture specified by the name pointed by the id doesn't exist, create it, then return the cached texture. Can be called from any thread. If called from some other thread and not found in cache, the call is queued to the main thread for processing. */ AtlasPointer getTexture(u32 id); AtlasPointer getTexture(const std::string &name) { return getTexture(getTextureId(name)); } // Gets a separate texture video::ITexture* getTextureRaw(const std::string &name) { AtlasPointer ap = getTexture(name + "^[forcesingle"); return ap.atlas; } // Gets a separate texture atlas pointer AtlasPointer getTextureRawAP(const AtlasPointer &ap) { return getTexture(getTextureName(ap.id) + "^[forcesingle"); } // Returns a pointer to the irrlicht device virtual IrrlichtDevice* getDevice() { return m_device; } // Update new texture pointer and texture coordinates to an // AtlasPointer based on it's texture id void updateAP(AtlasPointer &ap); bool isKnownSourceImage(const std::string &name) { bool is_known = false; bool cache_found = m_source_image_existence.get(name, &is_known); if(cache_found) return is_known; // Not found in cache; find out if a local file exists is_known = (getTexturePath(name) != ""); m_source_image_existence.set(name, is_known); return is_known; } // Processes queued texture requests from other threads. // Shall be called from the main thread. void processQueue(); // Insert an image into the cache without touching the filesystem. // Shall be called from the main thread. void insertSourceImage(const std::string &name, video::IImage *img); // Rebuild images and textures from the current set of source images // Shall be called from the main thread. void rebuildImagesAndTextures(); // Build the main texture atlas which contains most of the // textures. void buildMainAtlas(class IGameDef *gamedef); private: // The id of the thread that is allowed to use irrlicht directly threadid_t m_main_thread; // The irrlicht device IrrlichtDevice *m_device; // Cache of source images // This should be only accessed from the main thread SourceImageCache m_sourcecache; // Thread-safe cache of what source images are known (true = known) MutexedMap<std::string, bool> m_source_image_existence; // A texture id is index in this array. // The first position contains a NULL texture. std::vector<SourceAtlasPointer> m_atlaspointer_cache; // Maps a texture name to an index in the former. std::map<std::string, u32> m_name_to_id; // The two former containers are behind this mutex JMutex m_atlaspointer_cache_mutex; // Main texture atlas. This is filled at startup and is then not touched. video::IImage *m_main_atlas_image; video::ITexture *m_main_atlas_texture; // Queued texture fetches (to be processed by the main thread) RequestQueue<std::string, u32, u8, u8> m_get_texture_queue; }; IWritableTextureSource* createTextureSource(IrrlichtDevice *device) { return new TextureSource(device); } TextureSource::TextureSource(IrrlichtDevice *device): m_device(device), m_main_atlas_image(NULL), m_main_atlas_texture(NULL) { assert(m_device); m_atlaspointer_cache_mutex.Init(); m_main_thread = get_current_thread_id(); // Add a NULL AtlasPointer as the first index, named "" m_atlaspointer_cache.push_back(SourceAtlasPointer("")); m_name_to_id[""] = 0; } TextureSource::~TextureSource() { } u32 TextureSource::getTextureId(const std::string &name) { //infostream<<"getTextureId(): \""<<name<<"\""<<std::endl; { /* See if texture already exists */ JMutexAutoLock lock(m_atlaspointer_cache_mutex); std::map<std::string, u32>::iterator n; n = m_name_to_id.find(name); if(n != m_name_to_id.end()) { return n->second; } } /* Get texture */ if(get_current_thread_id() == m_main_thread) { return getTextureIdDirect(name); } else { infostream<<"getTextureId(): Queued: name=\""<<name<<"\""<<std::endl; // We're gonna ask the result to be put into here ResultQueue<std::string, u32, u8, u8> result_queue; // Throw a request in m_get_texture_queue.add(name, 0, 0, &result_queue); infostream<<"Waiting for texture from main thread, name=\"" <<name<<"\""<<std::endl; try { // Wait result for a second GetResult<std::string, u32, u8, u8> result = result_queue.pop_front(1000); // Check that at least something worked OK assert(result.key == name); return result.item; } catch(ItemNotFoundException &e) { infostream<<"Waiting for texture timed out."<<std::endl; return 0; } } infostream<<"getTextureId(): Failed"<<std::endl; return 0; } // Overlay image on top of another image (used for cracks) void overlay(video::IImage *image, video::IImage *overlay); // Draw an image on top of an another one, using the alpha channel of the // source image static void blit_with_alpha(video::IImage *src, video::IImage *dst, v2s32 src_pos, v2s32 dst_pos, v2u32 size); // Brighten image void brighten(video::IImage *image); // Parse a transform name u32 parseImageTransform(const std::string& s); // Apply transform to image dimension core::dimension2d<u32> imageTransformDimension(u32 transform, core::dimension2d<u32> dim); // Apply transform to image data void imageTransform(u32 transform, video::IImage *src, video::IImage *dst); /* Generate image based on a string like "stone.png" or "[crack0". if baseimg is NULL, it is created. Otherwise stuff is made on it. */ bool generate_image(std::string part_of_name, video::IImage *& baseimg, IrrlichtDevice *device, SourceImageCache *sourcecache); /* Generates an image from a full string like "stone.png^mineral_coal.png^[crack0". This is used by buildMainAtlas(). */ video::IImage* generate_image_from_scratch(std::string name, IrrlichtDevice *device, SourceImageCache *sourcecache); /* This method generates all the textures */ u32 TextureSource::getTextureIdDirect(const std::string &name) { //infostream<<"getTextureIdDirect(): name=\""<<name<<"\""<<std::endl; // Empty name means texture 0 if(name == "") { infostream<<"getTextureIdDirect(): name is empty"<<std::endl; return 0; } /* Calling only allowed from main thread */ if(get_current_thread_id() != m_main_thread) { errorstream<<"TextureSource::getTextureIdDirect() " "called not from main thread"<<std::endl; return 0; } /* See if texture already exists */ { JMutexAutoLock lock(m_atlaspointer_cache_mutex); std::map<std::string, u32>::iterator n; n = m_name_to_id.find(name); if(n != m_name_to_id.end()) { /*infostream<<"getTextureIdDirect(): \""<<name <<"\" found in cache"<<std::endl;*/ return n->second; } } /*infostream<<"getTextureIdDirect(): \""<<name <<"\" NOT found in cache. Creating it."<<std::endl;*/ /* Get the base image */ char separator = '^'; /* This is set to the id of the base image. If left 0, there is no base image and a completely new image is made. */ u32 base_image_id = 0; // Find last meta separator in name s32 last_separator_position = -1; for(s32 i=name.size()-1; i>=0; i--) { if(name[i] == separator) { last_separator_position = i; break; } } /* If separator was found, construct the base name and make the base image using a recursive call */ std::string base_image_name; if(last_separator_position != -1) { // Construct base name base_image_name = name.substr(0, last_separator_position); /*infostream<<"getTextureIdDirect(): Calling itself recursively" " to get base image of \""<<name<<"\" = \"" <<base_image_name<<"\""<<std::endl;*/ base_image_id = getTextureIdDirect(base_image_name); } //infostream<<"base_image_id="<<base_image_id<<std::endl; video::IVideoDriver* driver = m_device->getVideoDriver(); assert(driver); video::ITexture *t = NULL; /* An image will be built from files and then converted into a texture. */ video::IImage *baseimg = NULL; // If a base image was found, copy it to baseimg if(base_image_id != 0) { JMutexAutoLock lock(m_atlaspointer_cache_mutex); SourceAtlasPointer ap = m_atlaspointer_cache[base_image_id]; video::IImage *image = ap.atlas_img; if(image == NULL) { infostream<<"getTextureIdDirect(): WARNING: NULL image in " <<"cache: \""<<base_image_name<<"\"" <<std::endl; } else { core::dimension2d<u32> dim = ap.intsize; baseimg = driver->createImage(video::ECF_A8R8G8B8, dim); core::position2d<s32> pos_to(0,0); core::position2d<s32> pos_from = ap.intpos; image->copyTo( baseimg, // target v2s32(0,0), // position in target core::rect<s32>(pos_from, dim) // from ); /*infostream<<"getTextureIdDirect(): Loaded \"" <<base_image_name<<"\" from image cache" <<std::endl;*/ } } /* Parse out the last part of the name of the image and act according to it */ std::string last_part_of_name = name.substr(last_separator_position+1); //infostream<<"last_part_of_name=\""<<last_part_of_name<<"\""<<std::endl; // Generate image according to part of name if(!generate_image(last_part_of_name, baseimg, m_device, &m_sourcecache)) { errorstream<<"getTextureIdDirect(): " "failed to generate \""<<last_part_of_name<<"\"" <<std::endl; } // If no resulting image, print a warning if(baseimg == NULL) { errorstream<<"getTextureIdDirect(): baseimg is NULL (attempted to" " create texture \""<<name<<"\""<<std::endl; } if(baseimg != NULL) { // Create texture from resulting image t = driver->addTexture(name.c_str(), baseimg); } /* Add texture to caches (add NULL textures too) */ JMutexAutoLock lock(m_atlaspointer_cache_mutex); u32 id = m_atlaspointer_cache.size(); AtlasPointer ap(id); ap.atlas = t; ap.pos = v2f(0,0); ap.size = v2f(1,1); ap.tiled = 0; core::dimension2d<u32> baseimg_dim(0,0); if(baseimg) baseimg_dim = baseimg->getDimension(); SourceAtlasPointer nap(name, ap, baseimg, v2s32(0,0), baseimg_dim); m_atlaspointer_cache.push_back(nap); m_name_to_id[name] = id; /*infostream<<"getTextureIdDirect(): " <<"Returning id="<<id<<" for name \""<<name<<"\""<<std::endl;*/ return id; } std::string TextureSource::getTextureName(u32 id) { JMutexAutoLock lock(m_atlaspointer_cache_mutex); if(id >= m_atlaspointer_cache.size()) { errorstream<<"TextureSource::getTextureName(): id="<<id <<" >= m_atlaspointer_cache.size()=" <<m_atlaspointer_cache.size()<<std::endl; return ""; } return m_atlaspointer_cache[id].name; } AtlasPointer TextureSource::getTexture(u32 id) { JMutexAutoLock lock(m_atlaspointer_cache_mutex); if(id >= m_atlaspointer_cache.size()) return AtlasPointer(0, NULL); return m_atlaspointer_cache[id].a; } void TextureSource::updateAP(AtlasPointer &ap) { AtlasPointer ap2 = getTexture(ap.id); ap = ap2; } void TextureSource::processQueue() { /* Fetch textures */ if(!m_get_texture_queue.empty()) { GetRequest<std::string, u32, u8, u8> request = m_get_texture_queue.pop(); /*infostream<<"TextureSource::processQueue(): " <<"got texture request with " <<"name=\""<<request.key<<"\"" <<std::endl;*/ GetResult<std::string, u32, u8, u8> result; result.key = request.key; result.callers = request.callers; result.item = getTextureIdDirect(request.key); request.dest->push_back(result); } } void TextureSource::insertSourceImage(const std::string &name, video::IImage *img) { //infostream<<"TextureSource::insertSourceImage(): name="<<name<<std::endl; assert(get_current_thread_id() == m_main_thread); m_sourcecache.insert(name, img, true, m_device->getVideoDriver()); m_source_image_existence.set(name, true); } void TextureSource::rebuildImagesAndTextures() { JMutexAutoLock lock(m_atlaspointer_cache_mutex); /*// Oh well... just clear everything, they'll load sometime. m_atlaspointer_cache.clear(); m_name_to_id.clear();*/ video::IVideoDriver* driver = m_device->getVideoDriver(); // Remove source images from textures to disable inheriting textures // from existing textures /*for(u32 i=0; i<m_atlaspointer_cache.size(); i++){ SourceAtlasPointer *sap = &m_atlaspointer_cache[i]; sap->atlas_img->drop(); sap->atlas_img = NULL; }*/ // Recreate textures for(u32 i=0; i<m_atlaspointer_cache.size(); i++){ SourceAtlasPointer *sap = &m_atlaspointer_cache[i]; video::IImage *img = generate_image_from_scratch(sap->name, m_device, &m_sourcecache); // Create texture from resulting image video::ITexture *t = NULL; if(img) t = driver->addTexture(sap->name.c_str(), img); // Replace texture sap->a.atlas = t; sap->a.pos = v2f(0,0); sap->a.size = v2f(1,1); sap->a.tiled = 0; sap->atlas_img = img; sap->intpos = v2s32(0,0); sap->intsize = img->getDimension(); } } void TextureSource::buildMainAtlas(class IGameDef *gamedef) { assert(gamedef->tsrc() == this); INodeDefManager *ndef = gamedef->ndef(); infostream<<"TextureSource::buildMainAtlas()"<<std::endl; //return; // Disable (for testing) video::IVideoDriver* driver = m_device->getVideoDriver(); assert(driver); JMutexAutoLock lock(m_atlaspointer_cache_mutex); // Create an image of the right size core::dimension2d<u32> max_dim = driver->getMaxTextureSize(); core::dimension2d<u32> atlas_dim(2048,2048); atlas_dim.Width = MYMIN(atlas_dim.Width, max_dim.Width); atlas_dim.Height = MYMIN(atlas_dim.Height, max_dim.Height); video::IImage *atlas_img = driver->createImage(video::ECF_A8R8G8B8, atlas_dim); //assert(atlas_img); if(atlas_img == NULL) { errorstream<<"TextureSource::buildMainAtlas(): Failed to create atlas " "image; not building texture atlas."<<std::endl; return; } /* Grab list of stuff to include in the texture atlas from the main content features */ std::set<std::string> sourcelist; for(u16 j=0; j<MAX_CONTENT+1; j++) { if(j == CONTENT_IGNORE || j == CONTENT_AIR) continue; const ContentFeatures &f = ndef->get(j); for(u32 i=0; i<6; i++) { std::string name = f.tiledef[i].name; sourcelist.insert(name); } } infostream<<"Creating texture atlas out of textures: "; for(std::set<std::string>::iterator i = sourcelist.begin(); i != sourcelist.end(); ++i) { std::string name = *i; infostream<<"\""<<name<<"\" "; } infostream<<std::endl; // Padding to disallow texture bleeding // (16 needed if mipmapping is used; otherwise less will work too) s32 padding = 16; s32 column_padding = 16; s32 column_width = 256; // Space for 16 pieces of 16x16 textures /* First pass: generate almost everything */ core::position2d<s32> pos_in_atlas(0,0); pos_in_atlas.X = column_padding; pos_in_atlas.Y = padding; for(std::set<std::string>::iterator i = sourcelist.begin(); i != sourcelist.end(); ++i) { std::string name = *i; // Generate image by name video::IImage *img2 = generate_image_from_scratch(name, m_device, &m_sourcecache); if(img2 == NULL) { errorstream<<"TextureSource::buildMainAtlas(): " <<"Couldn't generate image \""<<name<<"\""<<std::endl; continue; } core::dimension2d<u32> dim = img2->getDimension(); // Don't add to atlas if image is too large core::dimension2d<u32> max_size_in_atlas(64,64); if(dim.Width > max_size_in_atlas.Width || dim.Height > max_size_in_atlas.Height) { infostream<<"TextureSource::buildMainAtlas(): Not adding " <<"\""<<name<<"\" because image is large"<<std::endl; continue; } // Wrap columns and stop making atlas if atlas is full if(pos_in_atlas.Y + dim.Height > atlas_dim.Height) { if(pos_in_atlas.X > (s32)atlas_dim.Width - column_width - column_padding){ errorstream<<"TextureSource::buildMainAtlas(): " <<"Atlas is full, not adding more textures." <<std::endl; break; } pos_in_atlas.Y = padding; pos_in_atlas.X += column_width + column_padding*2; } /*infostream<<"TextureSource::buildMainAtlas(): Adding \""<<name <<"\" to texture atlas"<<std::endl;*/ // Tile it a few times in the X direction u16 xwise_tiling = column_width / dim.Width; if(xwise_tiling > 16) // Limit to 16 (more gives no benefit) xwise_tiling = 16; for(u32 j=0; j<xwise_tiling; j++) { // Copy the copy to the atlas /*img2->copyToWithAlpha(atlas_img, pos_in_atlas + v2s32(j*dim.Width,0), core::rect<s32>(v2s32(0,0), dim), video::SColor(255,255,255,255), NULL);*/ img2->copyTo(atlas_img, pos_in_atlas + v2s32(j*dim.Width,0), core::rect<s32>(v2s32(0,0), dim), NULL); } // Copy the borders a few times to disallow texture bleeding for(u32 side=0; side<2; side++) // top and bottom for(s32 y0=0; y0<padding; y0++) for(s32 x0=0; x0<(s32)xwise_tiling*(s32)dim.Width; x0++) { s32 dst_y; s32 src_y; if(side==0) { dst_y = y0 + pos_in_atlas.Y + dim.Height; src_y = pos_in_atlas.Y + dim.Height - 1; } else { dst_y = -y0 + pos_in_atlas.Y-1; src_y = pos_in_atlas.Y; } s32 x = x0 + pos_in_atlas.X; video::SColor c = atlas_img->getPixel(x, src_y); atlas_img->setPixel(x,dst_y,c); } for(u32 side=0; side<2; side++) // left and right for(s32 x0=0; x0<column_padding; x0++) for(s32 y0=-padding; y0<(s32)dim.Height+padding; y0++) { s32 dst_x; s32 src_x; if(side==0) { dst_x = x0 + pos_in_atlas.X + dim.Width*xwise_tiling; src_x = pos_in_atlas.X + dim.Width*xwise_tiling - 1; } else { dst_x = -x0 + pos_in_atlas.X-1; src_x = pos_in_atlas.X; } s32 y = y0 + pos_in_atlas.Y; s32 src_y = MYMAX((int)pos_in_atlas.Y, MYMIN((int)pos_in_atlas.Y + (int)dim.Height - 1, y)); s32 dst_y = y; video::SColor c = atlas_img->getPixel(src_x, src_y); atlas_img->setPixel(dst_x,dst_y,c); } img2->drop(); /* Add texture to caches */ bool reuse_old_id = false; u32 id = m_atlaspointer_cache.size(); // Check old id without fetching a texture std::map<std::string, u32>::iterator n; n = m_name_to_id.find(name); // If it exists, we will replace the old definition if(n != m_name_to_id.end()){ id = n->second; reuse_old_id = true; /*infostream<<"TextureSource::buildMainAtlas(): " <<"Replacing old AtlasPointer"<<std::endl;*/ } // Create AtlasPointer AtlasPointer ap(id); ap.atlas = NULL; // Set on the second pass ap.pos = v2f((float)pos_in_atlas.X/(float)atlas_dim.Width, (float)pos_in_atlas.Y/(float)atlas_dim.Height); ap.size = v2f((float)dim.Width/(float)atlas_dim.Width, (float)dim.Width/(float)atlas_dim.Height); ap.tiled = xwise_tiling; // Create SourceAtlasPointer and add to containers SourceAtlasPointer nap(name, ap, atlas_img, pos_in_atlas, dim); if(reuse_old_id) m_atlaspointer_cache[id] = nap; else m_atlaspointer_cache.push_back(nap); m_name_to_id[name] = id; // Increment position pos_in_atlas.Y += dim.Height + padding * 2; } /* Make texture */ video::ITexture *t = driver->addTexture("__main_atlas__", atlas_img); assert(t); /* Second pass: set texture pointer in generated AtlasPointers */ for(std::set<std::string>::iterator i = sourcelist.begin(); i != sourcelist.end(); ++i) { std::string name = *i; if(m_name_to_id.find(name) == m_name_to_id.end()) continue; u32 id = m_name_to_id[name]; //infostream<<"id of name "<<name<<" is "<<id<<std::endl; m_atlaspointer_cache[id].a.atlas = t; } /* Write image to file so that it can be inspected */ /*std::string atlaspath = porting::path_user + DIR_DELIM + "generated_texture_atlas.png"; infostream<<"Removing and writing texture atlas for inspection to " <<atlaspath<<std::endl; fs::RecursiveDelete(atlaspath); driver->writeImageToFile(atlas_img, atlaspath.c_str());*/ } video::IImage* generate_image_from_scratch(std::string name, IrrlichtDevice *device, SourceImageCache *sourcecache) { /*infostream<<"generate_image_from_scratch(): " "\""<<name<<"\""<<std::endl;*/ video::IVideoDriver* driver = device->getVideoDriver(); assert(driver); /* Get the base image */ video::IImage *baseimg = NULL; char separator = '^'; // Find last meta separator in name s32 last_separator_position = name.find_last_of(separator); //if(last_separator_position == std::npos) // last_separator_position = -1; /*infostream<<"generate_image_from_scratch(): " <<"last_separator_position="<<last_separator_position <<std::endl;*/ /* If separator was found, construct the base name and make the base image using a recursive call */ std::string base_image_name; if(last_separator_position != -1) { // Construct base name base_image_name = name.substr(0, last_separator_position); /*infostream<<"generate_image_from_scratch(): Calling itself recursively" " to get base image of \""<<name<<"\" = \"" <<base_image_name<<"\""<<std::endl;*/ baseimg = generate_image_from_scratch(base_image_name, device, sourcecache); } /* Parse out the last part of the name of the image and act according to it */ std::string last_part_of_name = name.substr(last_separator_position+1); //infostream<<"last_part_of_name=\""<<last_part_of_name<<"\""<<std::endl; // Generate image according to part of name if(!generate_image(last_part_of_name, baseimg, device, sourcecache)) { errorstream<<"generate_image_from_scratch(): " "failed to generate \""<<last_part_of_name<<"\"" <<std::endl; return NULL; } return baseimg; } bool generate_image(std::string part_of_name, video::IImage *& baseimg, IrrlichtDevice *device, SourceImageCache *sourcecache) { video::IVideoDriver* driver = device->getVideoDriver(); assert(driver); // Stuff starting with [ are special commands if(part_of_name.size() == 0 || part_of_name[0] != '[') { video::IImage *image = sourcecache->getOrLoad(part_of_name, device); if(image == NULL) { if(part_of_name != ""){ errorstream<<"generate_image(): Could not load image \"" <<part_of_name<<"\""<<" while building texture"<<std::endl; errorstream<<"generate_image(): Creating a dummy" <<" image for \""<<part_of_name<<"\""<<std::endl; } // Just create a dummy image //core::dimension2d<u32> dim(2,2); core::dimension2d<u32> dim(1,1); image = driver->createImage(video::ECF_A8R8G8B8, dim); assert(image); /*image->setPixel(0,0, video::SColor(255,255,0,0)); image->setPixel(1,0, video::SColor(255,0,255,0)); image->setPixel(0,1, video::SColor(255,0,0,255)); image->setPixel(1,1, video::SColor(255,255,0,255));*/ image->setPixel(0,0, video::SColor(255,myrand()%256, myrand()%256,myrand()%256)); /*image->setPixel(1,0, video::SColor(255,myrand()%256, myrand()%256,myrand()%256)); image->setPixel(0,1, video::SColor(255,myrand()%256, myrand()%256,myrand()%256)); image->setPixel(1,1, video::SColor(255,myrand()%256, myrand()%256,myrand()%256));*/ } // If base image is NULL, load as base. if(baseimg == NULL) { //infostream<<"Setting "<<part_of_name<<" as base"<<std::endl; /* Copy it this way to get an alpha channel. Otherwise images with alpha cannot be blitted on images that don't have alpha in the original file. */ core::dimension2d<u32> dim = image->getDimension(); baseimg = driver->createImage(video::ECF_A8R8G8B8, dim); image->copyTo(baseimg); image->drop(); } // Else blit on base. else { //infostream<<"Blitting "<<part_of_name<<" on base"<<std::endl; // Size of the copied area core::dimension2d<u32> dim = image->getDimension(); //core::dimension2d<u32> dim(16,16); // Position to copy the blitted to in the base image core::position2d<s32> pos_to(0,0); // Position to copy the blitted from in the blitted image core::position2d<s32> pos_from(0,0); // Blit /*image->copyToWithAlpha(baseimg, pos_to, core::rect<s32>(pos_from, dim), video::SColor(255,255,255,255), NULL);*/ blit_with_alpha(image, baseimg, pos_from, pos_to, dim); // Drop image image->drop(); } } else { // A special texture modification /*infostream<<"generate_image(): generating special " <<"modification \""<<part_of_name<<"\"" <<std::endl;*/ /* This is the simplest of all; it just adds stuff to the name so that a separate texture is created. It is used to make textures for stuff that doesn't want to implement getting the texture from a bigger texture atlas. */ if(part_of_name == "[forcesingle") { // If base image is NULL, create a random color if(baseimg == NULL) { core::dimension2d<u32> dim(1,1); baseimg = driver->createImage(video::ECF_A8R8G8B8, dim); assert(baseimg); baseimg->setPixel(0,0, video::SColor(255,myrand()%256, myrand()%256,myrand()%256)); } } /* [crackN Adds a cracking texture */ else if(part_of_name.substr(0,6) == "[crack") { if(baseimg == NULL) { errorstream<<"generate_image(): baseimg==NULL " <<"for part_of_name=\""<<part_of_name <<"\", cancelling."<<std::endl; return false; } // Crack image number and overlay option s32 progression = 0; bool use_overlay = false; if(part_of_name.substr(6,1) == "o") { progression = stoi(part_of_name.substr(7)); use_overlay = true; } else { progression = stoi(part_of_name.substr(6)); use_overlay = false; } // Size of the base image core::dimension2d<u32> dim_base = baseimg->getDimension(); /* Load crack image. It is an image with a number of cracking stages horizontally tiled. */ video::IImage *img_crack = sourcecache->getOrLoad( "crack_anylength.png", device); if(img_crack && progression >= 0) { // Dimension of original image core::dimension2d<u32> dim_crack = img_crack->getDimension(); // Count of crack stages s32 crack_count = dim_crack.Height / dim_crack.Width; // Limit progression if(progression > crack_count-1) progression = crack_count-1; // Dimension of a single crack stage core::dimension2d<u32> dim_crack_cropped( dim_crack.Width, dim_crack.Width ); // Create cropped and scaled crack images video::IImage *img_crack_cropped = driver->createImage( video::ECF_A8R8G8B8, dim_crack_cropped); video::IImage *img_crack_scaled = driver->createImage( video::ECF_A8R8G8B8, dim_base); if(img_crack_cropped && img_crack_scaled) { // Crop crack image v2s32 pos_crack(0, progression*dim_crack.Width); img_crack->copyTo(img_crack_cropped, v2s32(0,0), core::rect<s32>(pos_crack, dim_crack_cropped)); // Scale crack image by copying img_crack_cropped->copyToScaling(img_crack_scaled); // Copy or overlay crack image if(use_overlay) { overlay(baseimg, img_crack_scaled); } else { /*img_crack_scaled->copyToWithAlpha( baseimg, v2s32(0,0), core::rect<s32>(v2s32(0,0), dim_base), video::SColor(255,255,255,255));*/ blit_with_alpha(img_crack_scaled, baseimg, v2s32(0,0), v2s32(0,0), dim_base); } } if(img_crack_scaled) img_crack_scaled->drop(); if(img_crack_cropped) img_crack_cropped->drop(); img_crack->drop(); } } /* [combine:WxH:X,Y=filename:X,Y=filename2 Creates a bigger texture from an amount of smaller ones */ else if(part_of_name.substr(0,8) == "[combine") { Strfnd sf(part_of_name); sf.next(":"); u32 w0 = stoi(sf.next("x")); u32 h0 = stoi(sf.next(":")); infostream<<"combined w="<<w0<<" h="<<h0<<std::endl; core::dimension2d<u32> dim(w0,h0); if(baseimg == NULL) { baseimg = driver->createImage(video::ECF_A8R8G8B8, dim); baseimg->fill(video::SColor(0,0,0,0)); } while(sf.atend() == false) { u32 x = stoi(sf.next(",")); u32 y = stoi(sf.next("=")); std::string filename = sf.next(":"); infostream<<"Adding \""<<filename <<"\" to combined ("<<x<<","<<y<<")" <<std::endl; video::IImage *img = sourcecache->getOrLoad(filename, device); if(img) { core::dimension2d<u32> dim = img->getDimension(); infostream<<"Size "<<dim.Width <<"x"<<dim.Height<<std::endl; core::position2d<s32> pos_base(x, y); video::IImage *img2 = driver->createImage(video::ECF_A8R8G8B8, dim); img->copyTo(img2); img->drop(); /*img2->copyToWithAlpha(baseimg, pos_base, core::rect<s32>(v2s32(0,0), dim), video::SColor(255,255,255,255), NULL);*/ blit_with_alpha(img2, baseimg, v2s32(0,0), pos_base, dim); img2->drop(); } else { infostream<<"img==NULL"<<std::endl; } } } /* "[brighten" */ else if(part_of_name.substr(0,9) == "[brighten") { if(baseimg == NULL) { errorstream<<"generate_image(): baseimg==NULL " <<"for part_of_name=\""<<part_of_name <<"\", cancelling."<<std::endl; return false; } brighten(baseimg); } /* "[noalpha" Make image completely opaque. Used for the leaves texture when in old leaves mode, so that the transparent parts don't look completely black when simple alpha channel is used for rendering. */ else if(part_of_name.substr(0,8) == "[noalpha") { if(baseimg == NULL) { errorstream<<"generate_image(): baseimg==NULL " <<"for part_of_name=\""<<part_of_name <<"\", cancelling."<<std::endl; return false; } core::dimension2d<u32> dim = baseimg->getDimension(); // Set alpha to full for(u32 y=0; y<dim.Height; y++) for(u32 x=0; x<dim.Width; x++) { video::SColor c = baseimg->getPixel(x,y); c.setAlpha(255); baseimg->setPixel(x,y,c); } } /* "[makealpha:R,G,B" Convert one color to transparent. */ else if(part_of_name.substr(0,11) == "[makealpha:") { if(baseimg == NULL) { errorstream<<"generate_image(): baseimg==NULL " <<"for part_of_name=\""<<part_of_name <<"\", cancelling."<<std::endl; return false; } Strfnd sf(part_of_name.substr(11)); u32 r1 = stoi(sf.next(",")); u32 g1 = stoi(sf.next(",")); u32 b1 = stoi(sf.next("")); std::string filename = sf.next(""); core::dimension2d<u32> dim = baseimg->getDimension(); /*video::IImage *oldbaseimg = baseimg; baseimg = driver->createImage(video::ECF_A8R8G8B8, dim); oldbaseimg->copyTo(baseimg); oldbaseimg->drop();*/ // Set alpha to full for(u32 y=0; y<dim.Height; y++) for(u32 x=0; x<dim.Width; x++) { video::SColor c = baseimg->getPixel(x,y); u32 r = c.getRed(); u32 g = c.getGreen(); u32 b = c.getBlue(); if(!(r == r1 && g == g1 && b == b1)) continue; c.setAlpha(0); baseimg->setPixel(x,y,c); } } /* "[transformN" Rotates and/or flips the image. N can be a number (between 0 and 7) or a transform name. Rotations are counter-clockwise. 0 I identity 1 R90 rotate by 90 degrees 2 R180 rotate by 180 degrees 3 R270 rotate by 270 degrees 4 FX flip X 5 FXR90 flip X then rotate by 90 degrees 6 FY flip Y 7 FYR90 flip Y then rotate by 90 degrees Note: Transform names can be concatenated to produce their product (applies the first then the second). The resulting transform will be equivalent to one of the eight existing ones, though (see: dihedral group). */ else if(part_of_name.substr(0,10) == "[transform") { if(baseimg == NULL) { errorstream<<"generate_image(): baseimg==NULL " <<"for part_of_name=\""<<part_of_name <<"\", cancelling."<<std::endl; return false; } u32 transform = parseImageTransform(part_of_name.substr(10)); core::dimension2d<u32> dim = imageTransformDimension( transform, baseimg->getDimension()); video::IImage *image = driver->createImage( baseimg->getColorFormat(), dim); assert(image); imageTransform(transform, baseimg, image); baseimg->drop(); baseimg = image; } /* [inventorycube{topimage{leftimage{rightimage In every subimage, replace ^ with &. Create an "inventory cube". NOTE: This should be used only on its own. Example (a grass block (not actually used in game): "[inventorycube{grass.png{mud.png&grass_side.png{mud.png&grass_side.png" */ else if(part_of_name.substr(0,14) == "[inventorycube") { if(baseimg != NULL) { errorstream<<"generate_image(): baseimg!=NULL " <<"for part_of_name=\""<<part_of_name <<"\", cancelling."<<std::endl; return false; } str_replace_char(part_of_name, '&', '^'); Strfnd sf(part_of_name); sf.next("{"); std::string imagename_top = sf.next("{"); std::string imagename_left = sf.next("{"); std::string imagename_right = sf.next("{"); // Generate images for the faces of the cube video::IImage *img_top = generate_image_from_scratch( imagename_top, device, sourcecache); video::IImage *img_left = generate_image_from_scratch( imagename_left, device, sourcecache); video::IImage *img_right = generate_image_from_scratch( imagename_right, device, sourcecache); assert(img_top && img_left && img_right); // Create textures from images video::ITexture *texture_top = driver->addTexture( (imagename_top + "__temp__").c_str(), img_top); video::ITexture *texture_left = driver->addTexture( (imagename_left + "__temp__").c_str(), img_left); video::ITexture *texture_right = driver->addTexture( (imagename_right + "__temp__").c_str(), img_right); assert(texture_top && texture_left && texture_right); // Drop images img_top->drop(); img_left->drop(); img_right->drop(); /* Draw a cube mesh into a render target texture */ scene::IMesh* cube = createCubeMesh(v3f(1, 1, 1)); setMeshColor(cube, video::SColor(255, 255, 255, 255)); cube->getMeshBuffer(0)->getMaterial().setTexture(0, texture_top); cube->getMeshBuffer(1)->getMaterial().setTexture(0, texture_top); cube->getMeshBuffer(2)->getMaterial().setTexture(0, texture_right); cube->getMeshBuffer(3)->getMaterial().setTexture(0, texture_right); cube->getMeshBuffer(4)->getMaterial().setTexture(0, texture_left); cube->getMeshBuffer(5)->getMaterial().setTexture(0, texture_left); core::dimension2d<u32> dim(64,64); std::string rtt_texture_name = part_of_name + "_RTT"; v3f camera_position(0, 1.0, -1.5); camera_position.rotateXZBy(45); v3f camera_lookat(0, 0, 0); core::CMatrix4<f32> camera_projection_matrix; // Set orthogonal projection camera_projection_matrix.buildProjectionMatrixOrthoLH( 1.65, 1.65, 0, 100); video::SColorf ambient_light(0.2,0.2,0.2); v3f light_position(10, 100, -50); video::SColorf light_color(0.5,0.5,0.5); f32 light_radius = 1000; video::ITexture *rtt = generateTextureFromMesh( cube, device, dim, rtt_texture_name, camera_position, camera_lookat, camera_projection_matrix, ambient_light, light_position, light_color, light_radius); // Drop mesh cube->drop(); // Free textures of images driver->removeTexture(texture_top); driver->removeTexture(texture_left); driver->removeTexture(texture_right); if(rtt == NULL) { baseimg = generate_image_from_scratch( imagename_top, device, sourcecache); return true; } // Create image of render target video::IImage *image = driver->createImage(rtt, v2s32(0,0), dim); assert(image); baseimg = driver->createImage(video::ECF_A8R8G8B8, dim); if(image) { image->copyTo(baseimg); image->drop(); } } /* [lowpart:percent:filename Adds the lower part of a texture */ else if(part_of_name.substr(0,9) == "[lowpart:") { Strfnd sf(part_of_name); sf.next(":"); u32 percent = stoi(sf.next(":")); std::string filename = sf.next(":"); //infostream<<"power part "<<percent<<"%% of "<<filename<<std::endl; if(baseimg == NULL) baseimg = driver->createImage(video::ECF_A8R8G8B8, v2u32(16,16)); video::IImage *img = sourcecache->getOrLoad(filename, device); if(img) { core::dimension2d<u32> dim = img->getDimension(); core::position2d<s32> pos_base(0, 0); video::IImage *img2 = driver->createImage(video::ECF_A8R8G8B8, dim); img->copyTo(img2); img->drop(); core::position2d<s32> clippos(0, 0); clippos.Y = dim.Height * (100-percent) / 100; core::dimension2d<u32> clipdim = dim; clipdim.Height = clipdim.Height * percent / 100 + 1; core::rect<s32> cliprect(clippos, clipdim); img2->copyToWithAlpha(baseimg, pos_base, core::rect<s32>(v2s32(0,0), dim), video::SColor(255,255,255,255), &cliprect); img2->drop(); } } /* [verticalframe:N:I Crops a frame of a vertical animation. N = frame count, I = frame index */ else if(part_of_name.substr(0,15) == "[verticalframe:") { Strfnd sf(part_of_name); sf.next(":"); u32 frame_count = stoi(sf.next(":")); u32 frame_index = stoi(sf.next(":")); if(baseimg == NULL){ errorstream<<"generate_image(): baseimg!=NULL " <<"for part_of_name=\""<<part_of_name <<"\", cancelling."<<std::endl; return false; } v2u32 frame_size = baseimg->getDimension(); frame_size.Y /= frame_count; video::IImage *img = driver->createImage(video::ECF_A8R8G8B8, frame_size); if(!img){ errorstream<<"generate_image(): Could not create image " <<"for part_of_name=\""<<part_of_name <<"\", cancelling."<<std::endl; return false; } // Fill target image with transparency img->fill(video::SColor(0,0,0,0)); core::dimension2d<u32> dim = frame_size; core::position2d<s32> pos_dst(0, 0); core::position2d<s32> pos_src(0, frame_index * frame_size.Y); baseimg->copyToWithAlpha(img, pos_dst, core::rect<s32>(pos_src, dim), video::SColor(255,255,255,255), NULL); // Replace baseimg baseimg->drop(); baseimg = img; } else { errorstream<<"generate_image(): Invalid " " modification: \""<<part_of_name<<"\""<<std::endl; } } return true; } void overlay(video::IImage *image, video::IImage *overlay) { /* Copy overlay to image, taking alpha into account. Where image is transparent, don't copy from overlay. Images sizes must be identical. */ if(image == NULL || overlay == NULL) return; core::dimension2d<u32> dim = image->getDimension(); core::dimension2d<u32> dim_overlay = overlay->getDimension(); assert(dim == dim_overlay); for(u32 y=0; y<dim.Height; y++) for(u32 x=0; x<dim.Width; x++) { video::SColor c1 = image->getPixel(x,y); video::SColor c2 = overlay->getPixel(x,y); u32 a1 = c1.getAlpha(); u32 a2 = c2.getAlpha(); if(a1 == 255 && a2 != 0) { c1.setRed((c1.getRed()*(255-a2) + c2.getRed()*a2)/255); c1.setGreen((c1.getGreen()*(255-a2) + c2.getGreen()*a2)/255); c1.setBlue((c1.getBlue()*(255-a2) + c2.getBlue()*a2)/255); } image->setPixel(x,y,c1); } } /* Draw an image on top of an another one, using the alpha channel of the source image This exists because IImage::copyToWithAlpha() doesn't seem to always work. */ static void blit_with_alpha(video::IImage *src, video::IImage *dst, v2s32 src_pos, v2s32 dst_pos, v2u32 size) { for(u32 y0=0; y0<size.Y; y0++) for(u32 x0=0; x0<size.X; x0++) { s32 src_x = src_pos.X + x0; s32 src_y = src_pos.Y + y0; s32 dst_x = dst_pos.X + x0; s32 dst_y = dst_pos.Y + y0; video::SColor src_c = src->getPixel(src_x, src_y); video::SColor dst_c = dst->getPixel(dst_x, dst_y); dst_c = src_c.getInterpolated(dst_c, (float)src_c.getAlpha()/255.0f); dst->setPixel(dst_x, dst_y, dst_c); } } void brighten(video::IImage *image) { if(image == NULL) return; core::dimension2d<u32> dim = image->getDimension(); for(u32 y=0; y<dim.Height; y++) for(u32 x=0; x<dim.Width; x++) { video::SColor c = image->getPixel(x,y); c.setRed(0.5 * 255 + 0.5 * (float)c.getRed()); c.setGreen(0.5 * 255 + 0.5 * (float)c.getGreen()); c.setBlue(0.5 * 255 + 0.5 * (float)c.getBlue()); image->setPixel(x,y,c); } } u32 parseImageTransform(const std::string& s) { int total_transform = 0; std::string transform_names[8]; transform_names[0] = "i"; transform_names[1] = "r90"; transform_names[2] = "r180"; transform_names[3] = "r270"; transform_names[4] = "fx"; transform_names[6] = "fy"; std::size_t pos = 0; while(pos < s.size()) { int transform = -1; for(int i = 0; i <= 7; ++i) { const std::string &name_i = transform_names[i]; if(s[pos] == ('0' + i)) { transform = i; pos++; break; } else if(!(name_i.empty()) && lowercase(s.substr(pos, name_i.size())) == name_i) { transform = i; pos += name_i.size(); break; } } if(transform < 0) break; // Multiply total_transform and transform in the group D4 int new_total = 0; if(transform < 4) new_total = (transform + total_transform) % 4; else new_total = (transform - total_transform + 8) % 4; if((transform >= 4) ^ (total_transform >= 4)) new_total += 4; total_transform = new_total; } return total_transform; } core::dimension2d<u32> imageTransformDimension(u32 transform, core::dimension2d<u32> dim) { if(transform % 2 == 0) return dim; else return core::dimension2d<u32>(dim.Height, dim.Width); } void imageTransform(u32 transform, video::IImage *src, video::IImage *dst) { if(src == NULL || dst == NULL) return; core::dimension2d<u32> srcdim = src->getDimension(); core::dimension2d<u32> dstdim = dst->getDimension(); assert(dstdim == imageTransformDimension(transform, srcdim)); assert(transform >= 0 && transform <= 7); /* Compute the transformation from source coordinates (sx,sy) to destination coordinates (dx,dy). */ int sxn = 0; int syn = 2; if(transform == 0) // identity sxn = 0, syn = 2; // sx = dx, sy = dy else if(transform == 1) // rotate by 90 degrees ccw sxn = 3, syn = 0; // sx = (H-1) - dy, sy = dx else if(transform == 2) // rotate by 180 degrees sxn = 1, syn = 3; // sx = (W-1) - dx, sy = (H-1) - dy else if(transform == 3) // rotate by 270 degrees ccw sxn = 2, syn = 1; // sx = dy, sy = (W-1) - dx else if(transform == 4) // flip x sxn = 1, syn = 2; // sx = (W-1) - dx, sy = dy else if(transform == 5) // flip x then rotate by 90 degrees ccw sxn = 2, syn = 0; // sx = dy, sy = dx else if(transform == 6) // flip y sxn = 0, syn = 3; // sx = dx, sy = (H-1) - dy else if(transform == 7) // flip y then rotate by 90 degrees ccw sxn = 3, syn = 1; // sx = (H-1) - dy, sy = (W-1) - dx for(u32 dy=0; dy<dstdim.Height; dy++) for(u32 dx=0; dx<dstdim.Width; dx++) { u32 entries[4] = {dx, dstdim.Width-1-dx, dy, dstdim.Height-1-dy}; u32 sx = entries[sxn]; u32 sy = entries[syn]; video::SColor c = src->getPixel(sx,sy); dst->setPixel(dx,dy,c); } }