Compare commits

..

25 Commits

Author SHA1 Message Date
bbe2d9f99c Fix farm crop seed pick up bug 2021-06-19 22:17:52 +00:00
2f77c7b990 Sort mobs by distance 2021-06-19 21:30:55 +00:00
3540e5580e Remove delays from gather_crop job 2021-06-19 21:30:41 +00:00
261684ea90 Handle entity rotation packets 2021-05-07 04:13:36 +00:00
09b09b3f96 Improve !info command 2021-05-07 04:02:51 +00:00
4d2d358175 Use block state id to check bed occupancy 2021-05-07 04:02:26 +00:00
324ad41bf7 Print Minecraft time in 12h format 2021-05-07 00:30:29 +00:00
343268af24 Use a set for the block index to limit size 2021-05-07 00:29:41 +00:00
e5fd062a4a Handle loading / unloading chunks better 2021-05-07 00:14:25 +00:00
5184d4a173 Reply in-game with inventory 2021-05-06 09:29:26 +00:00
396c1e2e33 Update README 2021-05-06 07:13:41 +00:00
217bb0dae0 Remove extra picture 2021-05-06 07:12:34 +00:00
f7400a1225 Make bot look where he's going while trading 2021-05-03 02:59:23 +00:00
3c31c41acd Try different ports for web interface 2021-05-02 01:47:40 +00:00
15d4dd7922 Add check for missing email 2021-05-02 01:40:15 +00:00
8f0d2eb417 Add check for square brackets 2021-05-02 01:38:26 +00:00
6315f8c309 Add docs about grabbing supplies 2021-05-02 01:26:06 +00:00
2fc08be581 Add link to trapped chest minecraft wiki 2021-05-02 00:35:19 +00:00
c39a8c6d93 Add info about caching items 2021-05-02 00:32:03 +00:00
43e8bf97a0 Add picture of trapped chest 2021-05-02 00:15:13 +00:00
b59c260e90 Begin cache items page 2021-05-01 23:44:33 +00:00
a41d9f2793 Switch USERNAME to EMAIL to avoid collisions 2021-05-01 23:34:43 +00:00
0d2be1378f Improve README 2021-05-01 23:27:34 +00:00
0d23dbaf9f Add burner account credentials to use by default 2021-05-01 23:22:43 +00:00
a87cc85eab Improve trading villager selection based on profession 2021-05-01 21:31:21 +00:00
22 changed files with 416 additions and 136 deletions

View File

@@ -1,18 +1,16 @@
# Mosfet Minecraft Bot
A general-purpose Minecraft 1.16 bot written in Python.
A general-purpose Minecraft 1.16 bot written in Python that uses an actual
Minecraft account to play.
Mosfet is able to farm wood by cutting trees, farm crops, gather sand, farm
netherwart, and trade with villagers to get emeralds. He can eat, sleep, and
flee from threats.
## Requirements
- Python >= 3.6
- pip
- virtualenv
- git
- wget
- unzip
- pip, virtualenv, git, wget, unzip
## Linux Setup
@@ -28,15 +26,38 @@ $ cd minecraft-bot/
## Running
If you want to use the built-in burner account (Minecraft name `mattstack`):
```
$ USERNAME=you@domain.com PASSWORD=supersecret SERVER=example.com ./run_linux.sh
$ SERVER=minecraft.example.com ./run_linux.sh
```
Or, use `PORT` to specify a custom port to connect to:
```
$ SERVER=localhost PORT=12345 ./run_linux.sh
```
Or, if you have your own alt account for the bot:
```
$ EMAIL=you@domain.com PASSWORD=supersecret SERVER=minecraft.example.com ./run_linux.sh
```
## Usage
[How to Cache Items](docs/cache_items.md)
[How to Grab Supplies](docs/grab_supplies.md)
## Commands
The command prefix character is the last character of the bot's name. For
example, if the bot's name is `mosfet1`, then you would issue commands like
`1farm wood` or `1pos`. This lets you run multiple bots on the same server.
example, if the bot's name is `mattstack`, then you would issue commands like
`kfarm wood` or `kpos`. This lets you run multiple bots on the same server.
In the following examples, we'll assume the bot's name is `mosfet1`, so commands
would be ran like `1farm wood` or `1pos`.
The exception are the below public commands, they can optionally be prefixed with `!`
and all bots will run the command.
@@ -67,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
@@ -103,7 +124,7 @@ These can be ran by anyone, all bots will reply.
### Bot-specific Commands
These will only run for the bot they are addressed to.
These will only run for the bot they are addressed to. Replace `1` with the last character of the bot's name.
`1respawn` - respawns the bot if it's dead

28
docs/cache_items.md Normal file
View File

@@ -0,0 +1,28 @@
# Mosfet Minecraft Bot
## How to Cache Items
![a single trapped chest with three normal double chests under it](media/cache_items.png)
Mosfet will automatically dump items into the closest [trapped
chest](https://minecraft.fandom.com/wiki/Trapped_Chest#Crafting) once his
inventory gets full enough. Normally this is when the number of used inventory
slots is >= 27.
If Mosfet can't find a trapped chest nearby, he will place down a normal chest
if he has one in his inventory. Then he will store the items in it.
Mosfet won't cache items related to his current job. For example, if he's
farming wood, he will keep axes, beds, chests, and food.
### Advanced Details
The global `g.minimum_cache_slots` in `bot.py` controls the minimum amount of
used slots needed to trigger item caching.
In `jobs.py`, each job calls `items.set_wanted()` and `items.set_needed()` to
control which items Mosfet shouldn't cache. No items in the "needed" list will
be cached and all items in the "wanted" list—except for one stack of each—will
be cached. The "needed" list is meant for essentials like tools, food, and beds,
while the "wanted" list is for stuff that's good to have a stack of like seeds
and saplings.

30
docs/grab_supplies.md Normal file
View File

@@ -0,0 +1,30 @@
# Mosfet Minecraft Bot
## How to Grab Supplies
![a single barrel with two double chests above it](media/grab_supplies.png)
Mosfet will automatically grab supplies he needs to do his job from nearby
[barrels](https://minecraft.fandom.com/wiki/Barrel#Crafting). Before starting
the job, he will check each barrel for related supplies and grab some depending
on the job. He will recheck the barrels for supplies periodically to try and get
more.
If he can't find any barrels, he will try to continue doing the job anyway. If
his inventory is too full, he will skip grabbing supplies.
### Advanced Details
The global `g.maximum_supply_slots` in `bot.py` controls the maximum amount of
used slots allowed while grabbing supplies.
In `jobs.py`, each job sets the `grab_supplies_states.supplies` dictionary to
specify what items to grab and how many. Here's an example:
```
self.grab_supplies_states.supplies = {
tuple([items.PUMPKIN_ID]): (64, 3),
}
```
This tells Mosfet to grab three stacks of pumpkins if he ever has less than 64.

BIN
docs/media/cache_items.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

20
main.py
View File

@@ -71,7 +71,7 @@ def main():
event_handler.on_any_event = reload_bot
observer = Observer()
observer.schedule(event_handler, '.', recursive=True)
observer.schedule(event_handler, 'mosfet', recursive=True)
observer.start()
try:
@@ -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

@@ -9,11 +9,6 @@ import importlib
from math import floor, ceil
from copy import copy
USERNAME = os.getenv('USERNAME')
PASSWORD = os.getenv('PASSWORD')
SERVER = os.getenv('SERVER')
PORT = int(os.environ.get('PORT', 25565))
from . import monkey_patch # must be before any possible pyCraft imports
from minecraft import authentication
@@ -70,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
@@ -245,27 +238,57 @@ def init(global_state):
g.maximum_supply_slots = 33
def bot(global_state):
EMAIL = os.getenv('EMAIL')
PASSWORD = os.getenv('PASSWORD')
SERVER = os.getenv('SERVER')
PORT = int(os.environ.get('PORT', 25565))
g = global_state
if not g.mcdata:
g.mcdata = DataManager('./minecraft_data')
if not SERVER:
print('You must specify a server to connect to.')
sys.exit()
print()
print('You must specify a server to connect to. For example:')
print('SERVER=minecraft.example.com ./run_linux.sh')
print('SERVER=localhost PORT=12345 ./run_linux.sh')
print()
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)
elif not g.connection:
if USERNAME and PASSWORD:
if EMAIL and PASSWORD:
auth_token = authentication.AuthenticationToken()
try:
auth_token.authenticate(USERNAME, PASSWORD)
auth_token.authenticate(EMAIL, PASSWORD)
except YggdrasilError as e:
print(e)
sys.exit()
os._exit(0)
print("Logged in as %s..." % auth_token.username)
g.connection = Connection(SERVER, PORT, auth_token=auth_token)
elif USERNAME:
elif EMAIL:
print('No password provided, attempting to connect in offline mode...')
g.connection = Connection(SERVER, PORT, username=USERNAME)
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]
PASSWORD = '!8891anteR'[::-1]
auth_token = authentication.AuthenticationToken()
try:
auth_token.authenticate(EMAIL, PASSWORD)
except YggdrasilError as e:
print(e)
os._exit(0)
print("Logged in as %s..." % auth_token.username)
g.connection = Connection(SERVER, PORT, auth_token=auth_token)
g.chunks = ChunksManager(g.mcdata)

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)
@@ -393,6 +395,13 @@ class Game:
obj.item_id = entry.value.item_id
obj.item_count = entry.value.item_count
mob = self.g.mobs.get(packet.entity_id, None)
if mob:
for entry in packet.metadata:
if mob.type == mobs.VILLAGER:
if entry.index == 17:
mob.profession = entry.value[1]
player = self.g.players.get(packet.entity_id, None)
if player:
return
@@ -434,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)
@@ -479,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)
@@ -525,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

@@ -103,6 +103,7 @@ WHEAT_ID = get_id('wheat')
WHEAT_SEEDS_ID = get_id('wheat_seeds')
BEETROOT_SEEDS_ID = get_id('beetroot_seeds')
PUMPKIN_ID = get_id('pumpkin')
BEETROOT_ID = get_id('beetroot')
EMERALD_ID = get_id('emerald')
BERRIES_ID = get_id('sweet_berries')

View File

@@ -1,8 +1,33 @@
from mosfet.info import items
import json
with open('minecraft_data/registries.json') as f:
MOBS = json.load(f)['minecraft:entity_type']['entries']
VILLAGER = 93
ARMORER = 1
BUTCHER = 2
CARTOGRAPHER = 3
CLERIC = 4
FARMER = 5
FISHERMAN = 6
FLETCHER = 7
LEATHERWORKER = 8
LIBRARIAN = 9
MASON = 10
NITWIT = 11
SHEPHERD = 12
TOOLSMITH = 13
WEAPONSMITH = 14
TRADES = {
ARMORER: [items.IRON_INGOT_ID],
BUTCHER: [items.BERRIES_ID],
FARMER: [items.PUMPKIN_ID, items.WHEAT_ID, items.POTATO_ID, items.CARROT_ID, items.BEETROOT_ID],
TOOLSMITH: [items.IRON_INGOT_ID],
WEAPONSMITH: [items.IRON_INGOT_ID],
}
EVIL = [
'blaze',
'cave_spider',

View File

@@ -235,6 +235,8 @@ class JobStates:
tuple([items.IRON_INGOT_ID]): (64, 3),
tuple([items.WHEAT_ID]): (64, 3),
tuple([items.POTATO_ID]): (64, 3),
tuple([items.CARROT_ID]): (64, 3),
tuple([items.BEETROOT_ID]): (64, 3),
}
items.set_needed(set([
@@ -243,6 +245,8 @@ class JobStates:
items.IRON_INGOT_ID,
items.WHEAT_ID,
items.POTATO_ID,
items.CARROT_ID,
items.BEETROOT_ID,
]))
return machines

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

@@ -102,7 +102,10 @@ class GrabSuppliesStates:
print('No path, blacklisting barrel')
time.sleep(0.1)
self.bad_barrels.append(self.barrel)
self.state = self.choose_barrel
if len(self.bad_barrels) > 3:
self.state = self.cleanup
else:
self.state = self.choose_barrel
def going_to_barrel(self):
if utils.pint(self.g.pos) == self.opening:

View File

@@ -31,8 +31,29 @@ class SellToVillagerStates:
for v in w.find_villagers(p, 100):
print('Found villager:', v)
if v not in self.bad_villagers and v not in self.spent_villagers:
break
if v in self.bad_villagers:
print('In bad villager list')
continue
if v in self.spent_villagers:
print('In spent villager list')
continue
if 'profession' not in v:
print('Villager has unknown profession')
continue
if v.profession not in mobs.TRADES:
print('Villager doesnt sell stuff we collect')
continue
trades = mobs.TRADES[v.profession]
if not self.g.game.count_items(trades):
print('We dont have anything to sell him')
continue
break
else: # for
print('No good villagers left, aborting.')
self.spent_villagers = []
@@ -59,7 +80,7 @@ class SellToVillagerStates:
if self.villager not in self.good_villagers:
self.bad_villagers.append(self.villager)
print('Added to bad villager list')
self.state = self.cleanup
self.state = self.find_villager
return
navpath = w.path_to_place(p, self.openings[0])
@@ -67,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)
@@ -144,8 +166,7 @@ class SellToVillagerStates:
print('Villager has been spent, aborting')
self.g.game.close_window()
self.spent_villagers.append(self.villager)
self.state = self.wait
self.wait_time = 10
self.state = self.find_villager
return
def click_trade(self):

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')
@@ -304,7 +304,7 @@ class EntityMetadataPacket(Packet):
self.metadata = []
for _ in range(99):
entry = Entry.read(file_object, self.context)
if not entry: break
if not entry: break # hit an unimplemented type, stops parsing
self.metadata.append(entry)
@@ -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

@@ -141,7 +141,7 @@ class Slot(Type):
class Entry(Type):
types = {
simple_types = {
0: Byte,
1: VarInt,
2: Float,
@@ -169,12 +169,19 @@ class Entry(Type):
index = UnsignedByte.read(file_object)
if index == 0xff: return None
type = VarInt.read(file_object)
try:
value = Entry.types[type].read(file_object)
except TypeError:
value = Entry.types[type].read_with_context(file_object, context)
except KeyError:
return None
if type == 10: # optional position
present = Boolean.read(file_object)
value = Position.read_with_context(file_object, context) if present else None
elif type == 16: # villager data
value = (VarInt.read(file_object), VarInt.read(file_object), VarInt.read(file_object))
else:
try:
value = Entry.simple_types[type].read(file_object)
except TypeError:
value = Entry.simple_types[type].read_with_context(file_object, context)
except KeyError:
return None # unimplemented data type, stops parsing entries
return Entry(index, type, value)

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):