Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minecraft Low Level API: First draft #44

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions minecraft/core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .core import Minecraft

__all__ = ['Minecraft']
69 changes: 69 additions & 0 deletions minecraft/core/connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
from __future__ import print_function, division, absolute_import, unicode_literals

import errno
import socket
import select
import logging

from . import exceptions


logger = logging.getLogger(__name__)


class Connection(object):
def __init__(self, host, port):
"""
TCP socket connection to a Minecraft Pi game. Default port is 4711.
"""
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
self.socket.connect((host, port))
except socket.error as e:
if e.errno != errno.ECONNREFUSED:
# Not the error we are looking for, re-raise
raise e
msg = 'Could not connect to Minecraft server at %s:%s (connection refused).'
raise exceptions.ConnectionError(msg % (host, port))

self.last_sent = ''

def drain(self):
"""
Drain the socket of incoming data.
"""
while True:
readable, _, _ = select.select([self.socket], [], [], 0.0)
if not readable:
break
data = self.socket.recv(1500)
logger.debug('Drained data: <%s>', data.strip())
logger.debug('Last message: <%s>', self.last_sent.strip())

def send(self, func, *args):
"""
Send data. Note that a trailing newline '\n' is added here.
"""
s = '%s(%s)\n' % (func, ','.join(map(str, args)))
self.drain()
self.last_sent = s
self.socket.sendall(s.encode('ascii'))
logger.info('Sent: %s', s)

def receive(self):
"""
Receive data. Note that the trailing newline '\n' is trimmed.
"""
s = self.socket.makefile('r').readline().rstrip('\n')
logger.info('Read: %s', s)
if s == 'Fail':
raise exceptions.APIError('%s failed' % self.last_sent.strip())
return s

def send_receive(self, func, *args):
"""
Send and receive data.
"""
self.send(func, *args)
return self.receive()
120 changes: 120 additions & 0 deletions minecraft/core/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-
"""
Low level Minecraft API client library.

This is a re-implementation of a low level client library for the Minecraft
API. It tries to stay close to the API calls. Basic data types are preferred
over custom types in here.

"""
from __future__ import print_function, division, absolute_import, unicode_literals

import logging

from .connection import Connection


logger = logging.getLogger(__name__)


class Command(object):
"""
The command base class.
"""
_func_prefix = ''
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love to find a nicer name for this, it took me a few minutes to track down what it's used for. However I'm not sure what a nicer name would be…

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you know a better one, let me know :)


def __init__(self, connection):
self._conn = connection

def _send(self, func, *args):
full_func = '%s.%s' % (self._func_prefix, func)
self._conn.send(full_func, *args)

def _send_receive(self, func, *args):
full_func = '%s.%s' % (self._func_prefix, func)
return self._conn.send_receive(full_func, *args)


class WorldCommand(Command):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to suffix the command classes with the word command. Too many commands!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I'll remove them.

_func_prefix = 'world'

def get_block(self, x, y, z):
value = self._send_receive('getBlock', x, y, z)
return int(value)

def set_block(self, x, y, z, block_type):
self._send('setBlock', x, y, z, block_type)

def set_blocks(self, x1, y1, z1, x2, y2, z2, block_type):
self._send('setBlocks', x1, y1, z1, x2, y2, z2, block_type)

def get_height(self, x, z):
value = self._send_receive('getHeight', x, z)
return int(value)

def save_checkpoint(self):
self._send('checkpoint.save')

def restore_checkpoint(self):
self._send('checkpoint.restore')

def setting(self):
# TODO what does this do?
pass


class ChatCommand(Command):
_func_prefix = 'chat'

def say(self, message):
self._send('post', message)
pass


class PlayerCommand(Command):
_func_prefix = 'player'

def get_tile(self):
value = self._send_receive('getTile')
return [int(x) for x in value.split(',')]

def set_tile(self, x, y, z):
self._send('setTile', x, y, z)

def get_pos(self):
value = self._send_receive('getPos')
return [float(x) for x in value.split(',')]

def set_pos(self, x, y, z):
self._send('setPos', x, y, z)


class CameraCommand(Command):
_func_prefix = 'camera.mode'

def set_normal(self):
self._send('setNormal')

def set_third_person(self):
self._send('setThirdPerson')

def set_fixed(self):
self._send('setFixed')

def set_pos(self, x, y, z):
self._send('setPos', x, y, z)


class Minecraft(object):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would World be a better name for this? It would make the API look like this:

from minecraft import World

mc = World()

What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would certainly avoid the "ambiguity" of minecraft.Minecraft, which is probably useful

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it creates ambiguity between mc (which is World()) and mc.world.

Also, after removing the Command Suffixes (https://github.com/py3minepi/py3minepi/pull/44#discussion_r18411837), there is actually already a class called World in this module.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about combining them? The Minecraft class is currently very small after all.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So something like this?

from minecraft.core import World

world = World()
world.get_block(...)
world.set_block(...)
world.chat.say('hi')
world.player.get_pos()

In that case the "entry point" would not just be a registry for subnamespaces, but a command itself. What do @doismellburning @hashbangstudio think about this? I'm -0 on it.


def __init__(self, host='127.0.0.1', port=4711):
logger.info('Initializing connection to %s:%d...', host, port)
self._conn = Connection(host, port)
logger.info('Loading world commands...')
self.world = WorldCommand(self._conn)
logger.info('Loading chat commands...')
self.chat = ChatCommand(self._conn)
logger.info('Loading player commands...')
self.player = PlayerCommand(self._conn)
logger.info('Loading camera commands...')
self.camera = CameraCommand(self._conn)
26 changes: 26 additions & 0 deletions minecraft/core/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
Exceptions to be used in py3minepi.
"""
from __future__ import print_function, division, absolute_import, unicode_literals


class ConnectionError(RuntimeError):
"""
Raised if something goes wrong with the connection.
"""
pass


class APIError(RuntimeError):
"""
Can be used if there are problems with the API.
"""
pass


class ValidationError(ValueError):
"""
Used for validation purposes.
"""
pass