Compare commits

...

16 Commits

14 changed files with 216 additions and 101 deletions

View File

@@ -32,13 +32,13 @@ If you want to use the built-in burner account (Minecraft name `mattstack`):
$ SERVER=minecraft.example.com ./run_linux.sh
```
Use `PORT` to specify a custom port to connect to:
Or, use `PORT` to specify a custom port to connect to:
```
$ SERVER=localhost PORT=12345 ./run_linux.sh
```
If you have your own alt account for the bot:
Or, if you have your own alt account for the bot:
```
$ EMAIL=you@domain.com PASSWORD=supersecret SERVER=minecraft.example.com ./run_linux.sh
@@ -88,7 +88,7 @@ These can be ran by anyone, all bots will reply.
`!error` - raises an error
`!inv` - prints current inventory
`!inv` - replies and prints current inventory
`!time` - replies with Minecraft world time

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1012 KiB

18
main.py
View File

@@ -91,14 +91,22 @@ def main():
observer.stop()
observer.join()
if __name__ == '__main__':
def run_api():
host = '0.0.0.0'
port = 3300
threading.Thread(target=app.run, kwargs={'host': host, 'port': port}).start()
print('Web interface listening on port:', port)
print('Try going to http://localhost:' + str(port))
while True:
print('Trying to run web interface on port:', port)
print('If it works, go to http://localhost:' + str(port))
try:
app.run(host=host, port=port)
except OSError:
print()
print('Error: Port already taken.')
port += 1
if __name__ == '__main__':
threading.Thread(target=run_api).start()
time.sleep(1)
main()

View File

@@ -65,21 +65,19 @@ def tick(global_state):
target = None
# make sure current chunks are loaded for physics
if not g.chunks.check_loaded(p, 288):
if not g.chunks.check_loaded(g.info.render_distance):
if not g.chunks.loading:
print('Loading chunks', end='', flush=True)
g.chunks.loading = True
g.chunks.loading = time.time()
packet = serverbound.play.PositionAndLookPacket(x=p.x, feet_y=p.y, z=p.z, pitch=0, yaw=0, on_ground=True)
g.connection.write_packet(packet, force=True)
return
else:
if g.chunks.loading:
print()
print('Chunks loaded.')
print('Chunks loaded in', round(time.time() - g.chunks.loading, 2), 's')
g.chunks.loading = False
g.chunks.unload_chunks(p)
########## object physics ##########
# note: it's possible the chunk data is out of date when this runs
@@ -272,6 +270,12 @@ def bot(global_state):
elif EMAIL:
print('No password provided, attempting to connect in offline mode...')
g.connection = Connection(SERVER, PORT, username=EMAIL)
elif PASSWORD:
print('')
print('Did you forget to specify an email?')
print('If you want to use your own account:')
print('EMAIL=you@domain.com PASSWORD=supersecret SERVER=minecraft.example.com ./run_linux.sh')
os._exit(0)
else:
print('No username or password provided, using burner minecraft account...')
EMAIL = 'moc.liamg@monortem'[::-1]

View File

@@ -1,5 +1,6 @@
import re
import time
from datetime import datetime, timedelta
import random
from itertools import count
from munch import Munch
@@ -22,7 +23,7 @@ class Commands:
def handle_chat(self, message):
source, sender, text = message
reply = None
reply = ''
private = False
for_me = False
authed = sender == '0c123cfa-1697-4427-9413-4b645dee7ec0'
@@ -50,6 +51,9 @@ class Commands:
if prefix == bot_num:
for_me = True
if data.startswith('[') and data.endswith(']'):
command = 'nosquarebrackets'
try:
@@ -81,6 +85,9 @@ class Commands:
if command == 'echo' and data:
reply = data
if command == 'nosquarebrackets':
reply = 'don\'t literally put the [ ]'
## !pos - replies with position and dimension
if command == 'pos':
reply = str(utils.pint(self.g.pos))[1:-1] + ', ' + self.g.dimension
@@ -100,19 +107,44 @@ class Commands:
reply = 'ok'
raise
## !inv - prints current inventory
if command == 'inv':
## !inv - replies and prints current inventory
if command == 'inv' or command == 'inventory':
inv_list = []
uniq_item_counts = {}
for i in self.g.inv.values():
if i.present:
inv_list.append('{}:{} x {}'.format(items.ITEM_NAMES[i.item_id], str(i.item_id), i.item_count))
inv_list.append((items.ITEM_NAMES[i.item_id], str(i.item_id), i.item_count))
if i.item_id not in uniq_item_counts:
uniq_item_counts[i.item_id] = 0
uniq_item_counts[i.item_id] += i.item_count
inv_list.sort()
result = '\n'.join(inv_list)
print(result or 'Empty')
console_result = '\n'.join(['{}:{} x {}'.format(*x) for x in inv_list])
if not console_result:
print('Empty')
reply = 'empty'
else:
print(console_result)
reply_result_1 = ', '.join(['{}:{} x {}'.format(*x) for x in inv_list])
reply_result_2 = ', '.join(['{}:{} x {}'.format(items.ITEM_NAMES[k], str(k), v) for k,v in uniq_item_counts.items()])
reply_result_3 = ', '.join(['{}:{}x{}'.format(items.ITEM_NAMES[k], str(k), v) for k,v in uniq_item_counts.items()])
reply_result_4 = ', '.join(['{}:{}x{}'.format(re.sub(r'[aeiou]', '', items.ITEM_NAMES[k]), str(k), v) for k,v in uniq_item_counts.items()])
reply_result_5 = ' '.join(['{}{}x{}'.format(re.sub(r'[aeiou]', '', items.ITEM_NAMES[k]), str(k), v) for k,v in uniq_item_counts.items()])
for r in [reply_result_1, reply_result_2, reply_result_3, reply_result_4, reply_result_5]:
reply = r
if len(reply) < 256:
break
## !time - replies with Minecraft world time
if command == 'time':
reply = str(self.g.time)
seconds = self.g.time * 3.6
start = datetime(2000, 1, 1, hour=6)
mctime = start + timedelta(seconds=seconds)
reply = str(self.g.time) + ' - ' + mctime.strftime('%I:%M %p')
## !count [id] - counts the number of items with that id
if command == 'count' and data:
@@ -121,7 +153,9 @@ class Commands:
## !loaded - replies with the current loaded area
if command == 'loaded':
reply = str(self.g.chunks.get_loaded_area())
l = self.g.chunks.get_loaded_area()
reply = '{}, {}, {} to {}, {}, {}'.format(*l[0], *l[1])
reply += ' - ' + str(len(self.g.chunks.chunks)//16) + ' chunks'
## !players - prints the current players
## !players clear - clears the current player list
@@ -210,23 +244,67 @@ class Commands:
tree = next(self.g.world.find_trees(pos, 50))
reply = str(tree)[1:-1]
## !block x y z - replies what block is at (x, y, z)
if command == 'block':
try:
data = data.replace('(', ' ').replace(')', ' ').replace(',', ' ')
x1, y1, z1 = [int(x) for x in data.split()]
except (AttributeError, ValueError):
reply = 'usage: !block x1 y1 z1'
## !info [query] - replies with info on a coordinate, block, item, or player
if command == 'info':
if not reply:
try:
check = data.replace('(', ' ').replace(')', ' ').replace(',', ' ')
x1, y1, z1 = [int(x) for x in check.split()]
coord = (x1, y1, z1)
block = self.g.world.block_at(*coord)
if not reply and block is None:
reply = 'coord out of range'
if not reply:
reply = 'Block: ' + blocks.BLOCKS[block] + ':' + str(block)
if blocks.PROPS[block]:
reply += ' - ' + ', '.join(['{}:{}'.format(k, v) for k, v in blocks.PROPS[block].items()])
except (AttributeError, ValueError):
pass
if not reply:
coord = (x1, y1, z1)
block = self.g.world.block_at(*coord)
try:
check = int(data)
if not reply and block is None:
reply = 'first coord out of range'
if check in blocks.BLOCKS:
block = check
reply += 'Block: ' + blocks.BLOCKS[block] + ':' + str(block)
if blocks.PROPS[block]:
reply += ' - ' + ', '.join(['{}:{}'.format(k, v) for k, v in blocks.PROPS[block].items()])
if not reply:
reply = blocks.BLOCKS[block] + ':' + str(block)
if check in blocks.BLOCKS and check in items.ITEM_NAMES:
reply += ' / '
if check in items.ITEM_NAMES:
item = check
reply += 'Item: ' + items.ITEM_NAMES[item] + ':' + str(item)
except ValueError:
pass
check = data.lower()
if not reply and check in self.g.player_names:
uuid = self.g.player_names[check]
for p in self.g.players.values():
if p.player_uuid == uuid:
player = p
break
else: # for
reply = 'player out of range'
if not reply:
reply += 'Player: '
results = []
for k, v in player.items():
try:
results.append('{}:{}'.format(k, int(v)))
except ValueError:
results.append('{}:{}'.format(k, str(v)))
reply += ', '.join(results)
################# Specific commands ##########################

View File

@@ -15,7 +15,7 @@ from mosfet.protocol.packets import (
ClientWindowConfirmationPacket, EntityMetadataPacket,
SpawnLivingEntityPacket, EntityPositionRotationPacket, DestroyEntitiesPacket,
EntityActionPacket, EntityTeleport, InteractEntityPacket, TradeListPacket,
SelectTradePacket, DisconnectPacket,
SelectTradePacket, DisconnectPacket, UnloadChunkPacket,
)
from mosfet.protocol.types import Slot
@@ -49,6 +49,7 @@ class Game:
register(self.handle_spawn_living, SpawnLivingEntityPacket)
register(self.handle_entity_position, clientbound.play.EntityPositionDeltaPacket)
register(self.handle_entity_position_rotation, EntityPositionRotationPacket)
register(self.handle_entity_look, clientbound.play.EntityLookPacket)
register(self.handle_destroy_entities, DestroyEntitiesPacket)
register(self.handle_spawn_player, clientbound.play.SpawnPlayerPacket)
register(self.handle_respawn, clientbound.play.RespawnPacket)
@@ -58,6 +59,7 @@ class Game:
#register(self.handle_entity_velocity, clientbound.play.EntityVelocityPacket)
register(self.handle_trade_list, TradeListPacket)
register(self.handle_disconnect, DisconnectPacket)
register(self.handle_unload_chunk, UnloadChunkPacket)
#register(self.handle_packet, Packet, early=True)
@@ -441,8 +443,14 @@ class Game:
player.x += packet.delta_x / 4096.0
player.y += packet.delta_y / 4096.0
player.z += packet.delta_z / 4096.0
player.yaw = packet.yaw
player.pitch = packet.pitch
#if player.player_uuid == '0c123cfa-1697-4427-9413-4b645dee7ec0': print(packet)
def handle_entity_look(self, packet):
player = self.g.players.get(packet.entity_id, None)
if player:
player.yaw = packet.yaw
player.pitch = packet.pitch
def handle_entity_teleport(self, packet):
mob = self.g.mobs.get(packet.entity_id, None)
@@ -486,12 +494,14 @@ class Game:
def handle_respawn(self, packet):
print(packet)
self.g.dimension = packet.world_name.replace('minecraft:', '')
self.g.chunks.unload_all_chunks()
def handle_player_list(self, packet):
for action in packet.actions:
if isinstance(action, packet.AddPlayerAction):
self.g.player_names[action.uuid] = action.name
self.g.player_names[action.name] = action.uuid # porque no los dos?
self.g.player_names[action.name] = action.uuid
self.g.player_names[action.name.lower()] = action.uuid # porque no los dos?
def handle_update_health(self, packet):
print(packet)
@@ -532,6 +542,9 @@ class Game:
import os
os._exit(1)
def handle_unload_chunk(self, packet):
self.g.chunks.unload_chunk(packet.chunk_x, packet.chunk_z)
def tick(self):
if self.g.breaking:
self.animate()

View File

@@ -15,6 +15,11 @@ for name, data in JSON_BLOCKS.items():
for state in data['states']:
BLOCKS[state['id']] = name.replace('minecraft:', '')
PROPS = {}
for name, data in JSON_BLOCKS.items():
for state in data['states']:
PROPS[state['id']] = state.get('properties', {})
BREAK_DISTANCE = 6
AIR = 0

View File

@@ -72,15 +72,7 @@ class GatherCropStates:
def break_crop(self):
self.g.game.break_block(self.crop)
self.wait_time = 0.5
self.state = self.wait
def wait(self):
# wait for the item
if self.wait_time > 0:
self.wait_time -= utils.TICK
else:
self.state = self.select_seed
self.state = self.select_seed
def select_seed(self):
p = utils.pint(self.g.pos)
@@ -91,18 +83,17 @@ class GatherCropStates:
blocks.MATURE_CARROT_ID: items.CARROT_ID,
blocks.MATURE_BEETROOT_ID: items.BEETROOT_SEEDS_ID,
}
self.target_seed = crop_seeds[self.type_id]
if self.g.game.select_item([crop_seeds[self.type_id]]):
if self.g.game.select_item([self.target_seed]):
self.state = self.wait_select
self.wait_time = 0.5
else:
print('Aborting planting, no crop')
self.state = self.cleanup
print('Havent picked up seed yet')
return
def wait_select(self):
# wait a bit to select
if self.wait_time > 0:
self.wait_time -= utils.TICK
if self.target_seed != self.g.holding:
return
else:
self.state = self.place_crop
@@ -111,12 +102,11 @@ class GatherCropStates:
self.g.game.place_block(p, BlockFace.TOP)
print('Placed crop')
self.state = self.wait_place
self.wait_time = 0.5
def wait_place(self):
# wait a bit for chunk data to update
if self.wait_time > 0:
self.wait_time -= utils.TICK
w = self.g.world
if w.block_at(*self.crop) == blocks.AIR:
return
else:
self.state = self.cleanup
@@ -134,8 +124,8 @@ class GatherCropStates:
self.crop = None
self.type_id = None
self.target_seed = None
self.bad_crops = []
self.wait_time = 0
def run(self):
self.state()

View File

@@ -88,6 +88,7 @@ class SellToVillagerStates:
if navpath:
self.g.path = navpath
self.state = self.going_to_villager
self.g.look_at = None
else:
self.openings.pop(0)
time.sleep(0.1)

View File

@@ -139,7 +139,15 @@ class SleepWithBedStates:
print('Placing bed')
self.g.game.place_block(self.area, BlockFace.TOP)
self.my_bed = True
self.state = self.use_bed
self.wait_time = 0.5
self.state = self.wait_use
def wait_use(self):
# wait to use the bed
if self.wait_time > 0:
self.wait_time -= utils.TICK
else:
self.state = self.use_bed
def use_bed(self):
w = self.g.world

View File

@@ -20,6 +20,7 @@ def get_packets(old_get_packets):
mc_packets.add(packets.EntityTeleport)
mc_packets.add(packets.TradeListPacket)
mc_packets.add(packets.DisconnectPacket)
mc_packets.add(packets.UnloadChunkPacket)
return mc_packets

View File

@@ -69,11 +69,10 @@ class ChunksManager:
for item_id, locations in chunk.sub_index.items():
if item_id not in self.index:
self.index[item_id] = []
self.index[item_id] = set()
for l in locations:
coords = (dx + l%16, dy + l//256, dz + l%256//16)
self.index[item_id].append(coords)
self.index[item_id].add(coords)
#self.biomes[(chunk_packet.x, None, chunk_packet.z)] = chunk_packet.biomes # FIXME
if self.loading:
@@ -119,31 +118,24 @@ class ChunksManager:
if block in blocks.INDEXED_IDS:
if block not in self.index:
self.index[block] = []
self.index[block].append((x, y, z))
self.index[block] = set()
self.index[block].add((x, y, z))
def check_loaded(self, position, steps=1):
x, y, z = utils.pint(position)
player_chunk = (x//16, 1, z//16)
for i in range(steps): # TODO: base off render_distance?
offset = utils.spiral(i)
check = utils.padd(player_chunk, offset)
def check_loaded(self, chunk_distance):
num = (chunk_distance * 2 + 1) ** 2
num_subchunks = num * 16
return len(self.chunks) >= num_subchunks
if check not in self.chunks:
return False
return True
def unload_chunks(self, position):
x, y, z = utils.pint(position)
player_chunk = (x//16, 0, z//16)
loaded_chunks = list(self.chunks.keys())
for chunk in loaded_chunks:
check = (chunk[0], 0, chunk[2])
if utils.phyp_king(player_chunk, check) > 20:
del self.chunks[chunk]
def unload_chunk(self, x, z):
for y in range(16):
try:
del self.chunks[(x, y, z)]
except KeyError:
pass
def unload_all_chunks(self):
self.chunks = {}
self.index = {}
class ChunkNotLoadedException(Exception):
def __str__(self):

View File

@@ -31,10 +31,10 @@ class ChunkDataPacket(Packet):
self.biomes.append(VarInt.read(file_object))
size = VarInt.read(file_object)
self.data = file_object.read(size)
size_entities = VarInt.read(file_object)
self.entities = []
for i in range(size_entities):
self.entities.append(Nbt.read(file_object))
#size_entities = VarInt.read(file_object)
#self.entities = []
#for i in range(size_entities):
# self.entities.append(Nbt.read(file_object))
self.decode_chunk_data()
@@ -49,9 +49,9 @@ class ChunkDataPacket(Packet):
Integer.send(self.biomes[i], packet_buffer)
VarInt.send(len(self.data), packet_buffer)
packet_buffer.send(self.data)
VarInt.send(len(self.entities), packet_buffer)
for e in self.entities:
Nbt.send(e, packet_buffer)
#VarInt.send(len(self.entities), packet_buffer)
#for e in self.entities:
# Nbt.send(e, packet_buffer)
def decode_chunk_data(self):
packet_data = PacketBuffer()
@@ -64,9 +64,9 @@ class ChunkDataPacket(Packet):
if self.bit_mask_y & (1 << i):
self.chunks[i].read(packet_data)
for e in self.entities:
y = e['y']
self.chunks[floor(y/16)].entities.append(e)
#for e in self.entities:
# y = e['y']
# self.chunks[floor(y/16)].entities.append(e)
class Chunk:
position = multi_attribute_alias(Vector, 'x', 'y', 'z')
@@ -458,3 +458,15 @@ class DisconnectPacket(Packet):
definition = [
{'reason': String},
]
class UnloadChunkPacket(Packet):
# Tells the client to unload a chunk column
# https://wiki.vg/Protocol#Unload_Chunk
id = 0x1C
packet_name = 'unload chunk'
definition = [
{'chunk_x': Integer},
{'chunk_z': Integer},
]

View File

@@ -280,13 +280,13 @@ class World:
def check_bed_occupied(self, bed):
# returns true if the bed is occupied by a player
print('Checking bed occupancy:', bed)
for player in self.g.players.values():
ppos = utils.pint((player.x, player.y, player.z))
if utils.phyp(bed, ppos) <= 1 and player.y - int(player.y) == 0.6875:
print('Bed is occupied by:', player, self.g.player_names[player.player_uuid])
return True
return False
bid = self.g.chunks.get_block_at(*bed)
if blocks.PROPS[bid]['occupied'] == 'true':
print('Checking bed occupancy:', bed, '-> occupied')
return True
else:
print('Checking bed occupancy:', bed, '-> free')
return False
def find_cache_openings(self, area):
return self.find_bed_openings(area)
@@ -312,6 +312,7 @@ class World:
if utils.phyp(center, pos) > distance:
continue
result.append(mob)
result.sort(key=lambda mob: utils.phyp(center, (mob.x, mob.y, mob.z)))
return result
def find_threats(self, center, distance):
@@ -324,6 +325,7 @@ class World:
if not self.check_air_column(pos, distance):
continue
result.append(mob)
result.sort(key=lambda mob: utils.phyp(center, (mob.x, mob.y, mob.z)))
return result
def find_villagers(self, center, distance):
@@ -336,6 +338,7 @@ class World:
if utils.phyp(center, pos) > distance:
continue
result.append(mob)
result.sort(key=lambda mob: utils.phyp(center, (mob.x, mob.y, mob.z)))
return result
def find_villager_openings(self, villager):