-
Notifications
You must be signed in to change notification settings - Fork 7
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
base: master
Are you sure you want to change the base?
Changes from 4 commits
49265d4
d7772d2
caa891f
67e0f37
981df3b
85e21fd
4e17ddf
55b8e56
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from .core import Minecraft | ||
|
||
__all__ = ['Minecraft'] |
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() |
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 = '' | ||
|
||
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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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! There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would
What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would certainly avoid the "ambiguity" of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But it creates ambiguity between Also, after removing the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about combining them? The There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) |
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 |
There was a problem hiding this comment.
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…
There was a problem hiding this comment.
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 :)