-
Notifications
You must be signed in to change notification settings - Fork 48
/
Copy pathwindows.py
396 lines (325 loc) · 13.4 KB
/
windows.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
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
import sys
import types
from minecraft_data.v1_8 import windows as windows_by_id
from minecraft_data.v1_8 import windows_list
from spockbot.mcdata import constants, get_item_or_block
from spockbot.mcdata.blocks import Block
from spockbot.mcdata.items import Item
from spockbot.mcdata.utils import camel_case, snake_case
def make_slot_check(wanted):
"""
Creates and returns a function that takes a slot
and checks if it matches the wanted item.
Args:
wanted: function(Slot) or Slot or itemID or (itemID, metadata)
"""
if isinstance(wanted, types.FunctionType):
return wanted # just forward the slot check function
if isinstance(wanted, int):
item, meta = wanted, None
elif isinstance(wanted, Slot):
item, meta = wanted.item_id, wanted.damage # TODO compare NBT
elif isinstance(wanted, (Item, Block)):
item, meta = wanted.id, wanted.metadata
elif isinstance(wanted, str):
item_or_block = get_item_or_block(wanted, init=True)
item, meta = item_or_block.id, item_or_block.metadata
else: # wanted is (id, meta)
try:
item, meta = wanted
except TypeError:
raise ValueError('Illegal args for make_slot_check(): %s' % wanted)
return lambda slot: item == slot.item_id and meta in (None, slot.damage)
class Slot(object):
def __init__(self, window, slot_nr, id=constants.INV_ITEMID_EMPTY,
damage=0, amount=0, enchants=None):
self.window = window
self.slot_nr = slot_nr
self.item_id = id
self.damage = damage
self.amount = amount
self.nbt = enchants
self.item = get_item_or_block(self.item_id, self.damage) or Item()
def move_to_window(self, window, slot_nr):
self.window, self.slot_nr = window, slot_nr
@property
def is_empty(self):
# could also check self.item_id == constants.INV_ITEMID_EMPTY
return self.amount <= 0
def matches(self, other):
return make_slot_check(other)(self)
def stacks_with(self, other):
if self.item_id != other.item_id:
return False
if self.damage != other.damage:
return False
# raise NotImplementedError('Stacks might differ by NBT data: %s %s'
# % (self, other))
# if self.nbt != other.nbt: return False
# TODO implement stacking correctly (NBT data comparison)
return self.item.stack_size != 1
def get_dict(self):
""" Formats the slot for network packing. """
data = {'id': self.item_id}
if self.item_id != constants.INV_ITEMID_EMPTY:
data['damage'] = self.damage
data['amount'] = self.amount
if self.nbt is not None:
data['enchants'] = self.nbt
return data
def copy(self):
return Slot(self.window, self.slot_nr, self.item_id,
self.damage, self.amount, self.nbt)
def __bool__(self):
return not self.is_empty
def __repr__(self):
if self.is_empty:
s = 'empty'
else:
item = self.item
s = '%i/%i %s' % (self.amount, item.stack_size, str(item))
if self.slot_nr != -1:
s += ' at %i' % self.slot_nr
if self.window:
s += ' in %s' % self.window
return '<Slot: %s>' % s
class SlotCursor(Slot):
def __init__(self, id=constants.INV_ITEMID_EMPTY, damage=0, amount=0,
enchants=None):
class CursorWindow(object): # TODO is there a cleaner way to do this?
window_id = constants.INV_WINID_CURSOR
def __repr__(self):
return 'CursorWindow()'
super(SlotCursor, self).__init__(
CursorWindow(), constants.INV_SLOT_NR_CURSOR,
id, damage, amount, enchants)
class BaseClick(object):
def get_packet(self, inv_plugin):
"""
Called by send_click() to prepare the sent packet.
Abstract method.
Args:
inv_plugin (InventoryPlugin): inventory plugin instance
"""
raise NotImplementedError()
def apply(self, inv_plugin):
"""
Called by on_success().
Abstract method.
Args:
inv_plugin (InventoryPlugin): inventory plugin instance
"""
raise NotImplementedError()
def on_success(self, inv_plugin, emit_set_slot):
"""
Called when the click was successful
and should be applied to the inventory.
Args:
inv_plugin (InventoryPlugin): inventory plugin instance
emit_set_slot (func): function to signal a slot change,
should be InventoryPlugin().emit_set_slot
"""
self.dirty = set()
self.apply(inv_plugin)
for changed_slot in self.dirty:
emit_set_slot(changed_slot)
# helper methods, used by children
# all argument instances are modified in-place
def copy_slot_type(self, slot_from, slot_to):
slot_to.item_id, slot_to.damage = slot_from.item_id, slot_from.damage
slot_to.nbt = slot_from.nbt
self.mark_dirty(slot_to)
def swap_slots(self, slot_a, slot_b):
slot_a.item_id, slot_b.item_id = slot_b.item_id, slot_a.item_id
slot_a.damage, slot_b.damage = slot_b.damage, slot_a.damage
slot_a.amount, slot_b.amount = slot_b.amount, slot_a.amount
slot_a.nbt, slot_b.nbt = slot_b.nbt, slot_a.nbt
self.mark_dirty(slot_a)
self.mark_dirty(slot_b)
def transfer(self, from_slot, to_slot, max_amount):
transfer_amount = min(max_amount, from_slot.amount,
to_slot.item.stack_size - to_slot.amount)
if transfer_amount <= 0:
return
self.copy_slot_type(from_slot, to_slot)
to_slot.amount += transfer_amount
from_slot.amount -= transfer_amount
self.cleanup_if_empty(from_slot)
def cleanup_if_empty(self, slot):
if slot.is_empty:
empty_slot_at_same_position = Slot(slot.window, slot.slot_nr)
self.copy_slot_type(empty_slot_at_same_position, slot)
self.mark_dirty(slot)
def mark_dirty(self, slot):
self.dirty.add(slot)
class SingleClick(BaseClick):
def __init__(self, slot, button=constants.INV_BUTTON_LEFT):
self.slot = slot
self.button = button
if button not in (constants.INV_BUTTON_LEFT,
constants.INV_BUTTON_RIGHT):
raise NotImplementedError(
'Clicking with button %s not implemented' % button)
def get_packet(self, inv_plugin):
slot_nr = self.slot.slot_nr
if self.slot == inv_plugin.cursor_slot:
slot_nr = constants.INV_OUTSIDE_WINDOW
return {
'slot': slot_nr,
'button': self.button,
'mode': 0,
'clicked_item': self.slot.get_dict(),
}
def apply(self, inv_plugin):
clicked = self.slot
cursor = inv_plugin.cursor_slot
if clicked == cursor:
if self.button == constants.INV_BUTTON_LEFT:
clicked.amount = 0
elif self.button == constants.INV_BUTTON_RIGHT:
clicked.amount -= 1
self.cleanup_if_empty(clicked)
elif self.button == constants.INV_BUTTON_LEFT:
if clicked.stacks_with(cursor):
self.transfer(cursor, clicked, cursor.amount)
else:
self.swap_slots(cursor, clicked)
elif self.button == constants.INV_BUTTON_RIGHT:
if cursor.is_empty:
# transfer half, round up
self.transfer(clicked, cursor, (clicked.amount + 1) // 2)
elif clicked.is_empty or clicked.stacks_with(cursor):
self.transfer(cursor, clicked, 1)
else: # slot items do not stack
self.swap_slots(cursor, clicked)
else:
raise NotImplementedError(
'Clicking with button %s not implemented' % self.button)
class DropClick(BaseClick):
def __init__(self, slot, drop_stack=False):
self.slot = slot
self.drop_stack = drop_stack
def get_packet(self, inv_plugin):
if self.slot == inv_plugin.cursor_slot:
raise ValueError("Can't drop cursor slot, use SingleClick")
if not inv_plugin.cursor_slot.is_empty:
raise ValueError("Can't drop other slots: cursor slot is occupied")
return {
'slot': self.slot.slot_nr,
'button': 1 if self.drop_stack else 0,
'mode': 4,
'clicked_item': inv_plugin.cursor_slot.get_dict(),
}
def apply(self, inv_plugin):
if self.drop_stack:
self.slot.amount = 0
else:
self.slot.amount -= 1
self.cleanup_if_empty(self.slot)
class Window(object):
""" Base class for all inventory types. """
name = None
inv_type = None
inv_data = {}
# the arguments must have the same names as the keys in the packet dict
def __init__(self, window_id, title, slot_count,
inv_type=None, persistent_slots=None, eid=None):
assert not inv_type or inv_type == self.inv_type, \
'inv_type differs: %s instead of %s' % (inv_type, self.inv_type)
self.is_storage = slot_count > 0 # same after re-opening window
if not self.is_storage: # get number of temporary slots
window_dict = windows_by_id[inv_type]
if 'slots' in window_dict:
slot_count = max(slot['index'] + slot.get('size', 1)
for slot in window_dict['slots'])
self.window_id = window_id
self.title = title
self.eid = eid # used for horses
# window slots vary, but always end with main inventory and hotbar
# create own slots, ...
self.slots = [Slot(self, slot_nr) for slot_nr in range(slot_count)]
# ... append persistent slots (main inventory and hotbar)
if persistent_slots is None:
for slot_nr in range(constants.INV_SLOTS_PERSISTENT):
self.slots.append(Slot(self, slot_nr + slot_count))
else: # persistent slots have to be moved from other inventory
moved_slots = persistent_slots[-constants.INV_SLOTS_PERSISTENT:]
for slot_nr, moved_slot in enumerate(moved_slots):
moved_slot.move_to_window(self, slot_nr + slot_count)
self.slots.append(moved_slot)
# additional info dependent on inventory type,
# dynamically updated by server
self.properties = {}
def __repr__(self):
return '%s(window_id=%i, title=%s, slot_count=%i)' % (
self.__class__.__name__,
self.window_id, self.title, len(self.slots))
@property
def persistent_slots(self):
return self.slots[-constants.INV_SLOTS_PERSISTENT:]
@property
def inventory_slots(self):
return self.slots[
-constants.INV_SLOTS_PERSISTENT:-constants.INV_SLOTS_HOTBAR]
@property
def hotbar_slots(self):
return self.slots[-constants.INV_SLOTS_HOTBAR:]
@property
def window_slots(self):
"""All slots except inventory and hotbar. Useful for searching."""
return self.slots[:-constants.INV_SLOTS_PERSISTENT]
# Helpers for creating the window classes
def _make_window(window_dict):
"""
Creates a new class for that window and registers it at this module.
"""
cls_name = '%sWindow' % camel_case(str(window_dict['name']))
bases = (Window,)
attrs = {
'__module__': sys.modules[__name__],
'name': str(window_dict['name']),
'inv_type': str(window_dict['id']),
'inv_data': window_dict,
}
# creates function-local index and size variables
def make_slot_method(index, size=1):
if size == 1:
return lambda self: self.slots[index]
else:
return lambda self: self.slots[index:(index + size)]
for slots in window_dict.get('slots', []):
index = slots['index']
size = slots.get('size', 1)
attr_name = snake_case(str(slots['name']))
attr_name += '_slot' if size == 1 else '_slots'
slots_method = make_slot_method(index, size)
slots_method.__name__ = attr_name
attrs[attr_name] = property(slots_method)
for i, prop_name in enumerate(window_dict.get('properties', [])):
def make_prop_method(i):
return lambda self: self.properties[i]
prop_method = make_prop_method(i)
prop_name = snake_case(str(prop_name))
prop_method.__name__ = prop_name
attrs[prop_name] = property(prop_method)
cls = type(cls_name, bases, attrs)
assert not hasattr(sys.modules[__name__], cls_name), \
'Window "%s" already registered at %s' % (cls_name, __name__)
setattr(sys.modules[__name__], cls_name, cls)
return cls
# look up a class by window type ID, e.g. when opening windows
inv_types = {}
def _create_windows():
for window in windows_list:
cls = _make_window(window)
inv_types[cls.inv_type] = cls
# Create all window classes from minecraft_data
_create_windows()
# get the PlayerWindow, which was just generated during runtime
_player_window = sys.modules[__name__].PlayerWindow
# override constructor of PlayerWindow
def _player_init(self, *args, **kwargs):
super(_player_window, self).__init__(
constants.INV_WINID_PLAYER, self.name, constants.INV_SLOTS_PLAYER,
*args, **kwargs)
setattr(_player_window, '__init__', _player_init)