aboutsummaryrefslogtreecommitdiff
path: root/src/util/enriched_string.cpp
blob: 762d094eba0d6b8f1c20f50887f00e3c78dd84c3 (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
/*
Copyright (C) 2013 xyz, Ilya Zhuravlev <whatever@xyz.is>
Copyright (C) 2016 Nore, Nathanaël Courant <nore@mesecons.net>

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.
*/

#include "enriched_string.h"
#include "util/string.h"
#include "debug.h"
#include "log.h"

using namespace irr::video;

EnrichedString::EnrichedString()
{
	clear();
}

EnrichedString::EnrichedString(const std::wstring &string,
		const std::vector<SColor> &colors)
{
	clear();
	m_string = string;
	m_colors = colors;
}

EnrichedString::EnrichedString(const std::wstring &s, const SColor &color)
{
	clear();
	addAtEnd(translate_string(s), color);
}

EnrichedString::EnrichedString(const wchar_t *str, const SColor &color)
{
	clear();
	addAtEnd(translate_string(std::wstring(str)), color);
}

void EnrichedString::clear()
{
	m_string.clear();
	m_colors.clear();
	m_has_background = false;
	m_default_length = 0;
	m_default_color = irr::video::SColor(255, 255, 255, 255);
	m_background = irr::video::SColor(0, 0, 0, 0);
}

void EnrichedString::operator=(const wchar_t *str)
{
	clear();
	addAtEnd(translate_string(std::wstring(str)), m_default_color);
}

void EnrichedString::addAtEnd(const std::wstring &s, const SColor &initial_color)
{
	SColor color(initial_color);
	bool use_default = (m_default_length == m_string.size() &&
		color == m_default_color);

	size_t i = 0;
	while (i < s.length()) {
		if (s[i] != L'\x1b') {
			m_string += s[i];
			m_colors.push_back(color);
			++i;
			continue;
		}
		++i;
		size_t start_index = i;
		size_t length;
		if (i == s.length()) {
			break;
		}
		if (s[i] == L'(') {
			++i;
			++start_index;
			while (i < s.length() && s[i] != L')') {
				if (s[i] == L'\\') {
					++i;
				}
				++i;
			}
			length = i - start_index;
			++i;
		} else {
			++i;
			length = 1;
		}
		std::wstring escape_sequence(s, start_index, length);
		std::vector<std::wstring> parts = split(escape_sequence, L'@');
		if (parts[0] == L"c") {
			if (parts.size() < 2) {
				continue;
			}
			parseColorString(wide_to_utf8(parts[1]), color, true);

			// No longer use default color after first escape
			if (use_default) {
				m_default_length = m_string.size();
				use_default = false;
			}
		} else if (parts[0] == L"b") {
			if (parts.size() < 2) {
				continue;
			}
			parseColorString(wide_to_utf8(parts[1]), m_background, true);
			m_has_background = true;
		}
	}

	// Update if no escape character was found
	if (use_default)
		m_default_length = m_string.size();
}

void EnrichedString::addChar(const EnrichedString &source, size_t i)
{
	m_string += source.m_string[i];
	m_colors.push_back(source.m_colors[i]);
}

void EnrichedString::addCharNoColor(wchar_t c)
{
	m_string += c;
	if (m_colors.empty()) {
		m_colors.emplace_back(m_default_color);
	} else {
		m_colors.push_back(m_colors[m_colors.size() - 1]);
	}
}

EnrichedString EnrichedString::operator+(const EnrichedString &other) const
{
	EnrichedString result = *this;
	result += other;
	return result;
}

void EnrichedString::operator+=(const EnrichedString &other)
{
	bool update_default_color = m_default_length == m_string.size();

	m_string += other.m_string;
	m_colors.insert(m_colors.end(), other.m_colors.begin(), other.m_colors.end());

	if (update_default_color) {
		m_default_length += other.m_default_length;
		updateDefaultColor();
	}
}

EnrichedString EnrichedString::substr(size_t pos, size_t len) const
{
	if (pos >= m_string.length())
		return EnrichedString();

	if (len == std::string::npos || pos + len > m_string.length())
		len = m_string.length() - pos;

	EnrichedString str(
		m_string.substr(pos, len),
		std::vector<SColor>(m_colors.begin() + pos, m_colors.begin() + pos + len)
	);

	str.m_has_background = m_has_background;
	str.m_background = m_background;

	if (pos < m_default_length)
		str.m_default_length = std::min(m_default_length - pos, str.size());
	str.setDefaultColor(m_default_color);
	return str;
}

const wchar_t *EnrichedString::c_str() const
{
	return m_string.c_str();
}

const std::vector<SColor> &EnrichedString::getColors() const
{
	return m_colors;
}

const std::wstring &EnrichedString::getString() const
{
	return m_string;
}

void EnrichedString::setDefaultColor(const irr::video::SColor &color)
{
	m_default_color = color;
	updateDefaultColor();
}

void EnrichedString::updateDefaultColor()
{
	sanity_check(m_default_length <= m_colors.size());

	for (size_t i = 0; i < m_default_length; ++i)
		m_colors[i] = m_default_color;
}
hl opt">(h, 16) if(i > 32767): i -= 65536 return i def int_to_hex3(i): if(i < 0): return "%03X" % (i + 4096) else: return "%03X" % i def int_to_hex4(i): if(i < 0): return "%04X" % (i + 65536) else: return "%04X" % i def getBlockAsInteger(p): return p[2]*16777216 + p[1]*4096 + p[0] def unsignedToSigned(i, max_positive): if i < max_positive: return i else: return i - 2*max_positive def getIntegerAsBlock(i): x = unsignedToSigned(i % 4096, 2048) i = int((i - x) / 4096) y = unsignedToSigned(i % 4096, 2048) i = int((i - y) / 4096) z = unsignedToSigned(i % 4096, 2048) return x,y,z def limit(i, l, h): if(i > h): i = h if(i < l): i = l return i def readU8(f): return ord(f.read(1)) def readU16(f): return ord(f.read(1))*256 + ord(f.read(1)) def readU32(f): return ord(f.read(1))*256*256*256 + ord(f.read(1))*256*256 + ord(f.read(1))*256 + ord(f.read(1)) def readS32(f): return unsignedToSigned(ord(f.read(1))*256*256*256 + ord(f.read(1))*256*256 + ord(f.read(1))*256 + ord(f.read(1)), 2**31) usagetext = """minetestmapper.py [options] -i/--input <world_path> -o/--output <output_image.png> --bgcolor <color> --scalecolor <color> --playercolor <color> --origincolor <color> --drawscale --drawplayers --draworigin --drawunderground Color format: '#000000'""" def usage(): print(usagetext) try: opts, args = getopt.getopt(sys.argv[1:], "hi:o:", ["help", "input=", "output=", "bgcolor=", "scalecolor=", "origincolor=", "playercolor=", "draworigin", "drawplayers", "drawscale", "drawunderground"]) except getopt.GetoptError as err: # print help information and exit: print(str(err)) # will print something like "option -a not recognized" usage() sys.exit(2) path = None output = "map.png" border = 0 scalecolor = "black" bgcolor = "white" origincolor = "red" playercolor = "red" drawscale = False drawplayers = False draworigin = False drawunderground = False sector_xmin = -1500 / 16 sector_xmax = 1500 / 16 sector_zmin = -1500 / 16 sector_zmax = 1500 / 16 for o, a in opts: if o in ("-h", "--help"): usage() sys.exit() elif o in ("-i", "--input"): path = a elif o in ("-o", "--output"): output = a elif o == "--bgcolor": bgcolor = ImageColor.getrgb(a) elif o == "--scalecolor": scalecolor = ImageColor.getrgb(a) elif o == "--playercolor": playercolor = ImageColor.getrgb(a) elif o == "--origincolor": origincolor = ImageColor.getrgb(a) elif o == "--drawscale": drawscale = True border = 40 elif o == "--drawplayers": drawplayers = True elif o == "--draworigin": draworigin = True elif o == "--drawunderground": drawunderground = True else: assert False, "unhandled option" if path is None: print("Please select world path (eg. -i ../worlds/yourworld) (or use --help)") sys.exit(1) if path[-1:] != "/" and path[-1:] != "\\": path = path + "/" # Load color information for the blocks. colors = {} try: f = file("colors.txt") except IOError: f = file(os.path.join(os.path.dirname(__file__), "colors.txt")) for line in f: values = string.split(line) if len(values) < 4: continue identifier = values[0] is_hex = True for c in identifier: if c not in "0123456789abcdefABCDEF": is_hex = False break if is_hex: colors[int(values[0], 16)] = ( int(values[1]), int(values[2]), int(values[3])) else: colors[values[0]] = ( int(values[1]), int(values[2]), int(values[3])) f.close() #print("colors: "+repr(colors)) #sys.exit(1) xlist = [] zlist = [] # List all sectors to memory and calculate the width and heigth of the # resulting picture. conn = None cur = None if os.path.exists(path + "map.sqlite"): import sqlite3 conn = sqlite3.connect(path + "map.sqlite") cur = conn.cursor() cur.execute("SELECT `pos` FROM `blocks`") while True: r = cur.fetchone() if not r: break x, y, z = getIntegerAsBlock(r[0]) if x < sector_xmin or x > sector_xmax: continue if z < sector_zmin or z > sector_zmax: continue xlist.append(x) zlist.append(z) if os.path.exists(path + "sectors2"): for filename in os.listdir(path + "sectors2"): for filename2 in os.listdir(path + "sectors2/" + filename): x = hex_to_int(filename) z = hex_to_int(filename2) if x < sector_xmin or x > sector_xmax: continue if z < sector_zmin or z > sector_zmax: continue xlist.append(x) zlist.append(z) if os.path.exists(path + "sectors"): for filename in os.listdir(path + "sectors"): x = hex4_to_int(filename[:4]) z = hex4_to_int(filename[-4:]) if x < sector_xmin or x > sector_xmax: continue if z < sector_zmin or z > sector_zmax: continue xlist.append(x) zlist.append(z) if len(xlist) == 0 or len(zlist) == 0: print("World does not exist.") sys.exit(1) # Get rid of doubles xlist, zlist = zip(*sorted(set(zip(xlist, zlist)))) minx = min(xlist) minz = min(zlist) maxx = max(xlist) maxz = max(zlist) w = (maxx - minx) * 16 + 16 h = (maxz - minz) * 16 + 16 print("Result image (w=" + str(w) + " h=" + str(h) + ") will be written to " + output) im = Image.new("RGB", (w + border, h + border), bgcolor) draw = ImageDraw.Draw(im) impix = im.load() stuff = {} unknown_node_names = [] unknown_node_ids = [] starttime = time.time() CONTENT_WATER = 2 def content_is_ignore(d): return d in [0, "ignore"] def content_is_water(d): return d in [2, 9] def content_is_air(d): return d in [126, 127, 254, "air"] def read_content(mapdata, version, datapos): if version >= 24: return (mapdata[datapos*2] << 8) | (mapdata[datapos*2 + 1]) elif version >= 20: if mapdata[datapos] < 0x80: return mapdata[datapos] else: return (mapdata[datapos] << 4) | (mapdata[datapos + 0x2000] >> 4) elif 16 <= version < 20: return TRANSLATION_TABLE.get(mapdata[datapos], mapdata[datapos]) else: raise Exception("Unsupported map format: " + str(version)) def read_mapdata(mapdata, version, pixellist, water, day_night_differs, id_to_name): global stuff # oh my :-) global unknown_node_names global unknown_node_ids if(len(mapdata) < 4096): print("bad: " + xhex + "/" + zhex + "/" + yhex + " " + \ str(len(mapdata))) else: chunkxpos = xpos * 16 chunkypos = ypos * 16 chunkzpos = zpos * 16 content = 0 datapos = 0 for (x, z) in reversed(pixellist): for y in reversed(range(16)): datapos = x + y * 16 + z * 256 content = read_content(mapdata, version, datapos) # Try to convert id to name try: content = id_to_name[content] except KeyError: pass if content_is_ignore(content): pass elif content_is_air(content): pass elif content_is_water(content): water[(x, z)] += 1 # Add dummy stuff for drawing sea without seabed stuff[(chunkxpos + x, chunkzpos + z)] = ( chunkypos + y, content, water[(x, z)], day_night_differs) elif content in colors: # Memorize information on the type and height of # the block and for drawing the picture. stuff[(chunkxpos + x, chunkzpos + z)] = ( chunkypos + y, content, water[(x, z)], day_night_differs) pixellist.remove((x, z)) break else: if type(content) == str: if content not in unknown_node_names: unknown_node_names.append(content) #print("unknown node: %s/%s/%s x: %d y: %d z: %d block name: %s" # % (xhex, zhex, yhex, x, y, z, content)) else: if content not in unknown_node_ids: unknown_node_ids.append(content) #print("unknown node: %s/%s/%s x: %d y: %d z: %d block id: %x" # % (xhex, zhex, yhex, x, y, z, content)) # Go through all sectors. for n in range(len(xlist)): #if n > 500: # break if n % 200 == 0: nowtime = time.time() dtime = nowtime - starttime try: n_per_second = 1.0 * n / dtime except ZeroDivisionError: n_per_second = 0 if n_per_second != 0: seconds_per_n = 1.0 / n_per_second time_guess = seconds_per_n * len(xlist) remaining_s = time_guess - dtime remaining_minutes = int(remaining_s / 60) remaining_s -= remaining_minutes * 60 print("Processing sector " + str(n) + " of " + str(len(xlist)) + " (" + str(round(100.0 * n / len(xlist), 1)) + "%)" + " (ETA: " + str(remaining_minutes) + "m " + str(int(remaining_s)) + "s)") xpos = xlist[n] zpos = zlist[n] xhex = int_to_hex3(xpos) zhex = int_to_hex3(zpos) xhex4 = int_to_hex4(xpos) zhex4 = int_to_hex4(zpos) sector1 = xhex4.lower() + zhex4.lower() sector2 = xhex.lower() + "/" + zhex.lower() ylist = [] sectortype = "" if cur: psmin = getBlockAsInteger((xpos, -2048, zpos)) psmax = getBlockAsInteger((xpos, 2047, zpos)) cur.execute("SELECT `pos` FROM `blocks` WHERE `pos`>=? AND `pos`<=? AND (`pos` - ?) % 4096 = 0", (psmin, psmax, psmin)) while True: r = cur.fetchone() if not r: break pos = getIntegerAsBlock(r[0])[1] ylist.append(pos) sectortype = "sqlite" try: for filename in os.listdir(path + "sectors/" + sector1): if(filename != "meta"): pos = int(filename, 16) if(pos > 32767): pos -= 65536 ylist.append(pos) sectortype = "old" except OSError: pass if sectortype == "": try: for filename in os.listdir(path + "sectors2/" + sector2): if(filename != "meta"): pos = int(filename, 16) if(pos > 32767): pos -= 65536 ylist.append(pos) sectortype = "new" except OSError: pass if sectortype == "": continue ylist.sort() # Make a list of pixels of the sector that are to be looked for. pixellist = [] water = {} for x in range(16): for z in range(16): pixellist.append((x, z)) water[(x, z)] = 0 # Go through the Y axis from top to bottom. for ypos in reversed(ylist): try: #print("("+str(xpos)+","+str(ypos)+","+str(zpos)+")") yhex = int_to_hex4(ypos) if sectortype == "sqlite": ps = getBlockAsInteger((xpos, ypos, zpos)) cur.execute("SELECT `data` FROM `blocks` WHERE `pos`==? LIMIT 1", (ps,)) r = cur.fetchone() if not r: continue f = cStringIO.StringIO(r[0]) else: if sectortype == "old": filename = path + "sectors/" + sector1 + "/" + yhex.lower() else: filename = path + "sectors2/" + sector2 + "/" + yhex.lower() f = file(filename, "rb") # Let's just memorize these even though it's not really necessary. version = readU8(f) flags = f.read(1) #print("version="+str(version)) #print("flags="+str(version)) # Check flags is_underground = ((ord(flags) & 1) != 0) day_night_differs = ((ord(flags) & 2) != 0) lighting_expired = ((ord(flags) & 4) != 0) generated = ((ord(flags) & 8) != 0) #print("is_underground="+str(is_underground)) #print("day_night_differs="+str(day_night_differs)) #print("lighting_expired="+str(lighting_expired)) #print("generated="+str(generated)) if version >= 22: content_width = readU8(f) params_width = readU8(f) # Node data dec_o = zlib.decompressobj() try: mapdata = array.array("B", dec_o.decompress(f.read())) except: mapdata = [] # Reuse the unused tail of the file f.close(); f = cStringIO.StringIO(dec_o.unused_data) #print("unused data: "+repr(dec_o.unused_data)) # zlib-compressed node metadata list dec_o = zlib.decompressobj() try: metaliststr = array.array("B", dec_o.decompress(f.read())) # And do nothing with it except: metaliststr = [] # Reuse the unused tail of the file f.close(); f = cStringIO.StringIO(dec_o.unused_data) #print("* dec_o.unused_data: "+repr(dec_o.unused_data)) data_after_node_metadata = dec_o.unused_data if version <= 21: # mapblockobject_count readU16(f) if version == 23: readU8(f) # Unused node timer version (always 0) if version == 24: ver = readU8(f) if ver == 1: num = readU16(f) for i in range(0,num): readU16(f) readS32(f) readS32(f) static_object_version = readU8(f) static_object_count = readU16(f) for i in range(0, static_object_count): # u8 type (object type-id) object_type = readU8(f) # s32 pos_x_nodes * 10000 pos_x_nodes = readS32(f)/10000 # s32 pos_y_nodes * 10000 pos_y_nodes = readS32(f)/10000 # s32 pos_z_nodes * 10000 pos_z_nodes = readS32(f)/10000 # u16 data_size data_size = readU16(f) # u8[data_size] data data = f.read(data_size) timestamp = readU32(f) #print("* timestamp="+str(timestamp)) id_to_name = {} if version >= 22: name_id_mapping_version = readU8(f) num_name_id_mappings = readU16(f) #print("* num_name_id_mappings: "+str(num_name_id_mappings)) for i in range(0, num_name_id_mappings): node_id = readU16(f) name_len = readU16(f) name = f.read(name_len) #print(str(node_id)+" = "+name) id_to_name[node_id] = name # Node timers if version >= 25: timer_size = readU8(f) num = readU16(f) for i in range(0,num): readU16(f) readS32(f) readS32(f) read_mapdata(mapdata, version, pixellist, water, day_night_differs, id_to_name) # After finding all the pixels in the sector, we can move on to # the next sector without having to continue the Y axis. if(len(pixellist) == 0): break except Exception as e: print("Error at ("+str(xpos)+","+str(ypos)+","+str(zpos)+"): "+str(e)) sys.stdout.write("Block data: ") for c in r[0]: sys.stdout.write("%2.2x "%ord(c)) sys.stdout.write(os.linesep) sys.stdout.write("Data after node metadata: ") for c in data_after_node_metadata: sys.stdout.write("%2.2x "%ord(c)) sys.stdout.write(os.linesep) traceback.print_exc() print("Drawing image") # Drawing the picture starttime = time.time() n = 0 for (x, z) in stuff.iterkeys(): if n % 500000 == 0: nowtime = time.time() dtime = nowtime - starttime try: n_per_second = 1.0 * n / dtime except ZeroDivisionError: n_per_second = 0 if n_per_second != 0: listlen = len(stuff) seconds_per_n = 1.0 / n_per_second time_guess = seconds_per_n * listlen remaining_s = time_guess - dtime remaining_minutes = int(remaining_s / 60) remaining_s -= remaining_minutes * 60 print("Drawing pixel " + str(n) + " of " + str(listlen) + " (" + str(round(100.0 * n / listlen, 1)) + "%)" + " (ETA: " + str(remaining_minutes) + "m " + str(int(remaining_s)) + "s)") n += 1 (r, g, b) = colors[stuff[(x, z)][1]] dnd = stuff[(x, z)][3] # day/night differs? if not dnd and not drawunderground: if stuff[(x, z)][2] > 0: # water (r, g, b) = colors[CONTENT_WATER] else: continue # Comparing heights of a couple of adjacent blocks and changing # brightness accordingly. try: c = stuff[(x, z)][1] c1 = stuff[(x - 1, z)][1] c2 = stuff[(x, z + 1)][1] dnd1 = stuff[(x - 1, z)][3] dnd2 = stuff[(x, z + 1)][3] if not dnd: d = -69 elif not content_is_water(c1) and not content_is_water(c2) and \ not content_is_water(c):