-
Notifications
You must be signed in to change notification settings - Fork 104
/
Copy pathworld.py
308 lines (272 loc) · 11.8 KB
/
world.py
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
"""
Handles a Minecraft world save using either the Anvil or McRegion format.
For more information about the world format:
https://minecraft.wiki/w/Level_format
"""
import os, glob, re
from . import region
from . import chunk
from .region import InconceivedChunk, Location
class UnknownWorldFormat(Exception):
"""Unknown or invalid world folder."""
def __init__(self, msg=""):
self.msg = msg
class _BaseWorldFolder(object):
"""
Abstract class, representing either a McRegion or Anvil world folder.
This class will use either Anvil or McRegion, with Anvil the preferred format.
Simply calling WorldFolder() will do this automatically.
"""
type = "Generic"
extension = ''
chunkclass = chunk.Chunk
def __init__(self, world_folder):
"""Initialize a WorldFolder."""
self.worldfolder = world_folder
self.regionfiles = {}
self.regions = {}
self.chunks = None
# os.listdir triggers an OSError for non-existant directories or permission errors.
# This is needed, because glob.glob silently returns no files.
os.listdir(world_folder)
self.set_regionfiles(self.get_filenames())
def get_filenames(self):
"""Find all matching file names in the world folder.
This method is private, and it's use it deprecated. Use get_regionfiles() instead."""
# Warning: glob returns a empty list if the directory is unreadable, without raising an Exception
return list(glob.glob(os.path.join(self.worldfolder,'region','r.*.*.'+self.extension)))
def set_regionfiles(self, filenames):
"""
This method directly sets the region files for this instance to use.
It assumes the filenames are in the form r.<x-digit>.<z-digit>.<extension>
"""
for filename in filenames:
# Assume that filenames have the name r.<x-digit>.<z-digit>.<extension>
m = re.match(r"r.(\-?\d+).(\-?\d+)."+self.extension, os.path.basename(filename))
if m:
x = int(m.group(1))
z = int(m.group(2))
else:
# Only raised if a .mca of .mcr file exists which does not comply to the
# r.<x-digit>.<z-digit>.<extension> filename format. This may raise false
# errors if a copy is made, e.g. "r.0.-1 copy.mca". If this is an issue, override
# get_filenames(). In most cases, it is an error, and we like to raise that.
# Changed, no longer raise error, because we want to continue the loop.
# raise UnknownWorldFormat("Unrecognized filename format %s" % os.path.basename(filename))
# TODO: log to stderr using logging facility.
pass
self.regionfiles[(x,z)] = filename
def get_regionfiles(self):
"""Return a list of full path of all region files."""
return list(self.regionfiles.values())
def nonempty(self):
"""Return True is the world is non-empty."""
return len(self.regionfiles) > 0
def get_region(self, x,z):
"""Get a region using x,z coordinates of a region. Cache results."""
if (x,z) not in self.regions or self.regions[x,z].closed:
if (x,z) in self.regionfiles:
self.regions[(x,z)] = region.RegionFile(self.regionfiles[(x,z)])
else:
# Return an empty RegionFile object
# TODO: this does not yet allow for saving of the region file
# TODO: this currently fails with a ValueError!
# TODO: generate the correct name, and create the file
# and add the fie to self.regionfiles
self.regions[(x,z)] = region.RegionFile()
self.regions[(x,z)].loc = Location(x=x,z=z)
return self.regions[(x,z)]
def iter_regions(self):
"""
Return an iterable list of all region files. Use this function if you only
want to loop through each region files once, and do not want to cache the results.
"""
# TODO: Implement BoundingBox
# TODO: Implement sort order
for x,z in self.regionfiles.keys():
close_after_use = False
if (x,z) in self.regions:
regionfile = self.regions[(x,z)]
else:
# It is not yet cached.
# Get file, but do not cache later.
regionfile = region.RegionFile(self.regionfiles[(x,z)], chunkclass = self.chunkclass)
regionfile.loc = Location(x=x,z=z)
close_after_use = True
try:
yield regionfile
finally:
if close_after_use:
regionfile.close()
def call_for_each_region(self, callback_function, boundingbox=None):
"""
Return an iterable that calls callback_function for each region file
in the world. This is equivalent to:
```
for the_region in iter_regions():
yield callback_function(the_region)
````
This function is threaded. It uses pickle to pass values between threads.
See [What can be pickled and unpickled?](https://docs.python.org/library/pickle.html#what-can-be-pickled-and-unpickled) in the Python documentation
for limitation on the output of `callback_function()`.
"""
raise NotImplementedError()
def get_nbt(self,x,z):
"""
Return a NBT specified by the chunk coordinates x,z. Raise InconceivedChunk
if the NBT file is not yet generated. To get a Chunk object, use get_chunk.
"""
rx,cx = divmod(x,32)
rz,cz = divmod(z,32)
if (rx,rz) not in self.regions and (rx,rz) not in self.regionfiles:
raise InconceivedChunk("Chunk %s,%s is not present in world" % (x,z))
nbt = self.get_region(rx,rz).get_nbt(cx,cz)
assert nbt != None
return nbt
def set_nbt(self,x,z,nbt):
"""
Set a chunk. Overrides the NBT if it already existed. If the NBT did not exists,
adds it to the Regionfile. May create a new Regionfile if that did not exist yet.
nbt must be a nbt.NBTFile instance, not a Chunk or regular TAG_Compound object.
"""
raise NotImplementedError()
# TODO: implement
def iter_nbt(self):
"""
Return an iterable list of all NBT. Use this function if you only
want to loop through the chunks once, and don't need the block or data arrays.
"""
# TODO: Implement BoundingBox
# TODO: Implement sort order
for region in self.iter_regions():
for c in region.iter_chunks():
yield c
def call_for_each_nbt(self, callback_function, boundingbox=None):
"""
Return an iterable that calls callback_function for each NBT structure
in the world. This is equivalent to:
```
for the_nbt in iter_nbt():
yield callback_function(the_nbt)
````
This function is threaded. It uses pickle to pass values between threads.
See [What can be pickled and unpickled?](https://docs.python.org/library/pickle.html#what-can-be-pickled-and-unpickled) in the Python documentation
for limitation on the output of `callback_function()`.
"""
raise NotImplementedError()
def get_chunk(self,x,z):
"""
Return a chunk specified by the chunk coordinates x,z. Raise InconceivedChunk
if the chunk is not yet generated. To get the raw NBT data, use get_nbt.
"""
return self.chunkclass(self.get_nbt(x, z))
def get_chunks(self, boundingbox=None):
"""
Return a list of all chunks. Use this function if you access the chunk
list frequently and want to cache the result.
Use iter_chunks() if you only want to loop through the chunks once or have a
very large world.
"""
if self.chunks == None:
self.chunks = list(self.iter_chunks())
return self.chunks
def iter_chunks(self):
"""
Return an iterable list of all chunks. Use this function if you only
want to loop through the chunks once or have a very large world.
Use get_chunks() if you access the chunk list frequently and want to cache
the results. Use iter_nbt() if you are concerned about speed and don't want
to parse the block data.
"""
# TODO: Implement BoundingBox
# TODO: Implement sort order
for c in self.iter_nbt():
yield self.chunkclass(c)
def chunk_count(self):
"""Return a count of the chunks in this world folder."""
c = 0
for r in self.iter_regions():
c += r.chunk_count()
return c
def get_boundingbox(self):
"""
Return minimum and maximum x and z coordinates of the chunks that
make up this world save
"""
b = BoundingBox()
for rx,rz in self.regionfiles.keys():
region = self.get_region(rx,rz)
rx,rz = 32*rx,32*rz
for cc in region.get_chunk_coords():
x,z = (rx+cc['x'],rz+cc['z'])
b.expand(x,None,z)
return b
def __repr__(self):
return "%s(%r)" % (self.__class__.__name__,self.worldfolder)
class McRegionWorldFolder(_BaseWorldFolder):
"""Represents a world save using the old McRegion format."""
type = "McRegion"
extension = 'mcr'
chunkclass = chunk.McRegionChunk
class AnvilWorldFolder(_BaseWorldFolder):
"""Represents a world save using the new Anvil format."""
type = "Anvil"
extension = 'mca'
chunkclass = chunk.AnvilChunk
class _WorldFolderFactory(object):
"""Factory class: instantiate the subclassses in order, and the first instance
whose nonempty() method returns True is returned. If no nonempty() returns True,
a UnknownWorldFormat exception is raised."""
def __init__(self, subclasses):
self.subclasses = subclasses
def __call__(self, *args, **kwargs):
for cls in self.subclasses:
wf = cls(*args, **kwargs)
if wf.nonempty(): # Check if the world is non-empty
return wf
raise UnknownWorldFormat("Empty world or unknown format")
WorldFolder = _WorldFolderFactory([AnvilWorldFolder, McRegionWorldFolder])
"""
Factory instance that returns a AnvilWorldFolder or McRegionWorldFolder
instance, or raise a UnknownWorldFormat.
"""
class BoundingBox(object):
"""A bounding box of x,y,z coordinates."""
def __init__(self, minx=None, maxx=None, miny=None, maxy=None, minz=None, maxz=None):
self.minx,self.maxx = minx, maxx
self.miny,self.maxy = miny, maxy
self.minz,self.maxz = minz, maxz
def expand(self,x,y,z):
"""
Expands the bounding
"""
if x != None:
if self.minx is None or x < self.minx:
self.minx = x
if self.maxx is None or x > self.maxx:
self.maxx = x
if y != None:
if self.miny is None or y < self.miny:
self.miny = y
if self.maxy is None or y > self.maxy:
self.maxy = y
if z != None:
if self.minz is None or z < self.minz:
self.minz = z
if self.maxz is None or z > self.maxz:
self.maxz = z
def lenx(self):
if self.maxx is None or self.minx is None:
return 0
return self.maxx-self.minx+1
def leny(self):
if self.maxy is None or self.miny is None:
return 0
return self.maxy-self.miny+1
def lenz(self):
if self.maxz is None or self.minz is None:
return 0
return self.maxz-self.minz+1
def __repr__(self):
return "%s(%s,%s,%s,%s,%s,%s)" % (self.__class__.__name__,self.minx,self.maxx,
self.miny,self.maxy,self.minz,self.maxz)