-
Notifications
You must be signed in to change notification settings - Fork 81
/
mclevel.py
294 lines (221 loc) · 10.7 KB
/
mclevel.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
# -*- coding: utf-8 -*-
"""
MCLevel interfaces
Sample usage:
import mclevel
# Call mclevel.fromFile to identify and open any of these four file formats:
#
# Classic levels - gzipped serialized java objects. Returns an instance of MCJavalevel
# Indev levels - gzipped NBT data in a single file. Returns an MCIndevLevel
# Schematics - gzipped NBT data in a single file. Returns an MCSchematic.
# MCSchematics have the special method rotateLeft which will reorient torches, stairs, and other tiles appropriately.
# Alpha levels - world folder structure containing level.dat and chunk folders. Single or Multiplayer.
# Can accept a path to the world folder or a path to the level.dat. Returns an MCInfdevOldLevel
# Load a Classic level.
level = mclevel.fromFile("server_level.dat");
# fromFile identified the file type and returned a MCJavaLevel. MCJavaLevel doesn't actually know any java. It guessed the
# location of the Blocks array by starting at the end of the file and moving backwards until it only finds valid blocks.
# It also doesn't know the dimensions of the level. This is why you have to tell them to MCEdit via the filename.
# This works here too: If the file were 512 wide, 512 long, and 128 high, I'd have to name it "server_level_512_512_128.dat"
#
# This is one area for improvement.
# Classic and Indev levels have all of their blocks in one place.
blocks = level.Blocks
# Sand to glass.
blocks[blocks == level.materials.Sand.ID] = level.materials.Glass.ID
# Save the file with another name. This only works for non-Alpha levels.
level.saveToFile("server_level_glassy.dat");
# Load an Alpha world
# Loading an Alpha world immediately scans the folder for chunk files. This takes longer for large worlds.
ourworld = mclevel.fromFile("C:\\Minecraft\\OurWorld");
# Convenience method to load a numbered world from the saves folder.
world1 = mclevel.loadWorldNumber(1);
# Find out which chunks are present. Doing this will scan the chunk folders the
# first time it is used. If you already know where you want to be, skip to
# world1.getChunk(xPos, zPos)
chunkPositions = list(world1.allChunks)
# allChunks returns an iterator that yields a (xPos, zPos) tuple for each chunk
xPos, zPos = chunkPositions[0];
# retrieve an AnvilChunk object. this object will load and decompress
# the chunk as needed, and remember whether it needs to be saved or relighted
chunk = world1.getChunk(xPos, zPos)
### Access the data arrays of the chunk like so:
# Take note that the array is indexed x, z, y. The last index corresponds to
# height or altitude.
blockType = chunk.Blocks[0,0,64]
chunk.Blocks[0,0,64] = 1
# Access the chunk's Entities and TileEntities as arrays of TAG_Compound as
# they appear in the save format.
# Entities usually have Pos, Health, and id
# TileEntities usually have tileX, tileY, tileZ, and id
# For more information, google "Chunk File Format"
for entity in chunk.Entities:
if entity["id"].value == "Spider":
entity["Health"].value = 50
# Accessing one byte at a time from the Blocks array is very slow in Python.
# To get around this, we have methods to access multiple bytes at once.
# The first technique is slicing. You can use slicing to restrict your access
# to certain depth levels, or to extract a column or a larger section from the
# array. Standard python slice notation is used.
# Set the top half of the array to 0. The : says to use the entire array along
# that dimension. The syntax []= indicates we are overwriting part of the array
chunk.Blocks[:,:,64:] = 0
# Using [] without = creates a 'view' on part of the array. This is not a
# copy, it is a reference to a portion of the original array.
midBlocks = chunk.Blocks[:,:,32:64]
# Here's a gotcha: You can't just write 'midBlocks = 0' since it will replace
# the 'midBlocks' reference itself instead of accessing the array. Instead, do
# this to access and overwrite the array using []= syntax.
midBlocks[:] = 0
# The second is masking. Using a comparison operator ( <, >, ==, etc )
# against the Blocks array will return a 'mask' that we can use to specify
# positions in the array.
# Create the mask from the result of the equality test.
fireBlocks = ( chunk.Blocks==world.materials.Fire.ID )
# Access Blocks using the mask to set elements. The syntax is the same as
# using []= with slices
chunk.Blocks[fireBlocks] = world.materials.Leaves.ID
# You can also combine mask arrays using logical operations (&, |, ^) and use
# the mask to access any other array of the same shape.
# Here we turn all trees into birch trees.
# Extract a mask from the Blocks array to find the locations of tree trunks.
# Or | it with another mask to find the locations of leaves.
# Use the combined mask to access the Data array and set those locations to birch
# Note that the Data, BlockLight, and SkyLight arrays have been
# unpacked from 4-bit arrays to numpy uint8 arrays. This makes them much easier
# to work with.
treeBlocks = ( chunk.Blocks == world.materials.Wood.ID )
treeBlocks |= ( chunk.Blocks == world.materials.Leaves.ID )
chunk.Data[treeBlocks] = 2 # birch
# The chunk doesn't know you've changed any of that data. Call chunkChanged()
# to let it know. This will mark the chunk for lighting calculation,
# recompression, and writing to disk. It will also immediately recalculate the
# chunk's HeightMap and fill the SkyLight only with light falling straight down.
# These are relatively fast and were added here to aid MCEdit.
chunk.chunkChanged();
# To recalculate all of the dirty lights in the world, call generateLights
world.generateLights();
# Move the player and his spawn
world.setPlayerPosition( (0, 67, 0) ) # add 3 to make sure his head isn't in the ground.
world.setPlayerSpawnPosition( (0, 64, 0) )
# Save the level.dat and any chunks that have been marked for writing to disk
# This also compresses any chunks marked for recompression.
world.saveInPlace();
# Advanced use:
# The getChunkSlices method returns an iterator that returns slices of chunks within the specified range.
# the slices are returned as tuples of (chunk, slices, point)
# chunk: The AnvilChunk object we're interested in.
# slices: A 3-tuple of slice objects that can be used to index chunk's data arrays
# point: A 3-tuple of floats representing the relative position of this subslice within the larger slice.
#
# Take caution:
# the point tuple is ordered (x,y,z) in accordance with the tuples used to initialize a bounding box
# however, the slices tuple is ordered (x,z,y) for easy indexing into the arrays.
# Here is an old version of MCInfdevOldLevel.fillBlocks in its entirety:
def fillBlocks(self, box, blockType, blockData = 0):
chunkIterator = self.getChunkSlices(box)
for (chunk, slices, point) in chunkIterator:
chunk.Blocks[slices] = blockType
chunk.Data[slices] = blockData
chunk.chunkChanged();
Copyright 2010 David Rio Vierra
"""
from indev import MCIndevLevel
from infiniteworld import MCInfdevOldLevel
from javalevel import MCJavaLevel
from logging import getLogger
from mclevelbase import saveFileDir
import nbt
from numpy import fromstring
import os
from pocket import PocketWorld
from schematic import INVEditChest, MCSchematic, ZipSchematic
import sys
import traceback
log = getLogger(__name__)
class LoadingError(RuntimeError):
pass
def fromFile(filename, loadInfinite=True, readonly=False):
''' The preferred method for loading Minecraft levels of any type.
pass False to loadInfinite if you'd rather not load infdev levels.
'''
log.info(u"Identifying " + filename)
if not filename:
raise IOError("File not found: " + filename)
if not os.path.exists(filename):
raise IOError("File not found: " + filename)
if ZipSchematic._isLevel(filename):
log.info("Zipfile found, attempting zipped infinite level")
lev = ZipSchematic(filename)
log.info("Detected zipped Infdev level")
return lev
if PocketWorld._isLevel(filename):
return PocketWorld(filename)
if MCInfdevOldLevel._isLevel(filename):
log.info(u"Detected Infdev level.dat")
if loadInfinite:
return MCInfdevOldLevel(filename=filename, readonly=readonly)
else:
raise ValueError("Asked to load {0} which is an infinite level, loadInfinite was False".format(os.path.basename(filename)))
if os.path.isdir(filename):
raise ValueError("Folder {0} was not identified as a Minecraft level.".format(os.path.basename(filename)))
f = file(filename, 'rb')
rawdata = f.read()
f.close()
if len(rawdata) < 4:
raise ValueError("{0} is too small! ({1}) ".format(filename, len(rawdata)))
data = fromstring(rawdata, dtype='uint8')
if not data.any():
raise ValueError("{0} contains only zeroes. This file is damaged beyond repair.")
if MCJavaLevel._isDataLevel(data):
log.info(u"Detected Java-style level")
lev = MCJavaLevel(filename, data)
lev.compressed = False
return lev
#ungzdata = None
compressed = True
unzippedData = None
try:
unzippedData = nbt.gunzip(rawdata)
except Exception, e:
log.info(u"Exception during Gzip operation, assuming {0} uncompressed: {1!r}".format(filename, e))
if unzippedData is None:
compressed = False
unzippedData = rawdata
#data =
data = unzippedData
if MCJavaLevel._isDataLevel(data):
log.info(u"Detected compressed Java-style level")
lev = MCJavaLevel(filename, data)
lev.compressed = compressed
return lev
try:
root_tag = nbt.load(buf=data)
except Exception, e:
log.info(u"Error during NBT load: {0!r}".format(e))
log.info(traceback.format_exc())
log.info(u"Fallback: Detected compressed flat block array, yzx ordered ")
try:
lev = MCJavaLevel(filename, data)
lev.compressed = compressed
return lev
except Exception, e2:
raise LoadingError(("Multiple errors encountered", e, e2), sys.exc_info()[2])
else:
if MCIndevLevel._isTagLevel(root_tag):
log.info(u"Detected Indev .mclevel")
return MCIndevLevel(root_tag, filename)
if MCSchematic._isTagLevel(root_tag):
log.info(u"Detected Schematic.")
return MCSchematic(root_tag=root_tag, filename=filename)
if INVEditChest._isTagLevel(root_tag):
log.info(u"Detected INVEdit inventory file")
return INVEditChest(root_tag=root_tag, filename=filename)
raise IOError("Cannot detect file type.")
def loadWorld(name):
filename = os.path.join(saveFileDir, name)
return fromFile(filename)
def loadWorldNumber(i):
#deprecated
filename = u"{0}{1}{2}{3}{1}".format(saveFileDir, os.sep, u"World", i)
return fromFile(filename)