diff --git a/README.md b/README.md index 99fb4a0..77aef6e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,16 @@ # Pythonized CC Tweaked (ComputerCraft) API -1. Enable localhost in mod server config +**Warning**: CPython can't build safe sandboxes for arbitrary untrusted code +[(read more)](https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html). +Never use code in this repo if you don't trust your players! + +1. Download and install wheel from github releases + + ```sh + pip install computercraft-*.whl + ``` + +2. Enable localhost in mod server config In case of singleplayer it's located inside your saves folder. In case of multiplayer check your server folder. @@ -13,80 +23,90 @@ action = "allow" # change here deny to allow ``` -2. Create module named `examplemod.py`: +3. Start python server: - ```python - async def hello(api): - await api.print('Hello world!') - ``` - -3. Start a server: - - ```bash - python -m computercraft.server examplemod + ```sh + python -m computercraft.server ``` 4. In minecraft, open up any computer and type: - ```bash + ```sh wget http://127.0.0.1:8080/ py - py hello + py ``` + Now you have python REPL in computercraft! + To quit REPL type `exit()` and press enter. + `py` is short Lua program that interacts with the server. -Argument is the name of coroutine inside the module. -`api` object contains almost everything *as is* in ComputerCraft documentation: +`cc` module contains almost everything *as is* in ComputerCraft documentation: ```python -async def program(api): - await api.disk.eject('right') - await api.print(await api.os.getComputerLabel()) - # ... -``` +from cc import disk, os -Using python coroutines allows launching commands in parallel, effectively replacing `parallel` API: - -```python -async def program(api): - # Since os.sleep is mostly waiting for events, it doesn't block execution of parallel threads - # and this snippet takes approximately 2 seconds to complete. - await asyncio.gather(api.os.sleep(2), api.os.sleep(2)) +disk.eject('right') +print(os.getComputerLabel()) ``` Opening a file: ```python -async def program(api): - async with api.fs.open('filename', 'r') as f: - async for line in f: - await api.print(line) +from cc import fs + +with fs.open('filename', 'r') as f: + for line in f: + print(line) ``` -Capturing event: +Waiting for event: ```python -async def program(api): - async with api.os.captureEvent('timer') as timer_event_queue: - timer_id = await api.os.startTimer(2) - async for etid, *_ in timer_event_queue: - if etid == timer_id: - await api.print('Timer reached') - break +from cc import os + +timer_id = os.startTimer(2) +while True: + e = os.pullEvent('timer') + if e[1] == timer_id: + print('Timer reached') + break ``` Using modems: ```python -async def program(api): - modem = await api.peripheral.wrap('back') - listen_channel = 5 - async with modem.receive(listen_channel) as q: - async for msg in q: - await api.print(repr(msg)) - if msg.content == 'stop': - break - else: - await modem.transmit(msg.reply_channel, listen_channel, msg.content) +from cc import peripheral + +modem = peripheral.wrap('back') +listen_channel = 5 +# this automatically opens and closes modem on listen_channel +for msg in modem.receive(listen_channel): + print(repr(msg)) + if msg.content == 'stop': + break + else: + modem.transmit(msg.reply_channel, listen_channel, msg.content) ``` -More examples can be found in `testmod.py`. +Using parallel: + +```python +from cc import parallel, os + +def fn(): + os.sleep(2) + print('done') + +parallel.waitForAll(fn, fn, fn) +``` + +Importing in-game files as modules: + +```python +from cc import import_file + +p = import_file('/disk/program.py') # absolute +m = import_file('lib.py', __file__) # relative to current file +``` + +More examples can be found in repository. diff --git a/computercraft/back.lua b/computercraft/back.lua index 70a895d..f851ab6 100644 --- a/computercraft/back.lua +++ b/computercraft/back.lua @@ -56,6 +56,11 @@ while true do ycounts[msg.task_id] = 0 end end + elseif msg.action == 'drop' then + for _, task_id in ipairs(msg.task_ids) do + tasks[task_id] = nil + ycounts[task_id] = nil + end elseif msg.action == 'sub' then event_sub[msg.event] = true elseif msg.action == 'unsub' then @@ -88,12 +93,12 @@ while true do }) del_tasks[task_id] = true else - ycounts[msg.task_id] = ycounts[msg.task_id] + 1 + ycounts[task_id] = ycounts[task_id] + 1 end end for task_id in pairs(del_tasks) do tasks[task_id] = nil - ycounts[msg.task_id] = nil + ycounts[task_id] = nil end end diff --git a/computercraft/lua.py b/computercraft/lua.py index 7967d0f..ee6f93d 100644 --- a/computercraft/lua.py +++ b/computercraft/lua.py @@ -73,3 +73,11 @@ def lua_args(*params): idx = -1 params = params[:idx + 1] return ', '.join(lua_value(p) for p in params) + + +def lua_call(name, *params): + return '{}({})'.format(name, lua_args(*params)) + + +def return_lua_call(name, *params): + return 'return ' + lua_call(name, *params) diff --git a/computercraft/server.py b/computercraft/server.py index ab1202e..28cc5bc 100644 --- a/computercraft/server.py +++ b/computercraft/server.py @@ -1,173 +1,15 @@ +import argparse import asyncio import json -import string -from aiohttp import web, WSMsgType -from contextlib import asynccontextmanager -from traceback import print_exc from os.path import join, dirname, abspath -from importlib import import_module -import argparse -from .subapis.root import RootAPIMixin -from .lua import lua_string -from . import rproc +from aiohttp import web, WSMsgType -from .subapis.colors import ColorsAPI -from .subapis.commands import CommandsAPI -from .subapis.disk import DiskAPI -from .subapis.fs import FSAPI -from .subapis.gps import GpsAPI -from .subapis.help import HelpAPI -from .subapis.keys import KeysAPI -from .subapis.multishell import MultishellAPI -from .subapis.os import OSAPI -from .subapis.paintutils import PaintutilsAPI -from .subapis.peripheral import PeripheralAPI -from .subapis.pocket import PocketAPI -from .subapis.rednet import RednetAPI -from .subapis.redstone import RedstoneAPI -from .subapis.settings import SettingsAPI -from .subapis.shell import ShellAPI -from .subapis.term import TermAPI -from .subapis.textutils import TextutilsAPI -from .subapis.turtle import TurtleAPI -from .subapis.window import WindowAPI +from .sess import CCSession THIS_DIR = dirname(abspath(__file__)) LUA_FILE = join(THIS_DIR, 'back.lua') -DIGITS = string.digits + string.ascii_lowercase - - -def base36(n): - r = '' - while n: - r += DIGITS[n % 36] - n //= 36 - return r[::-1] - - -class CCAPI(RootAPIMixin): - def __init__(self, nid, program, sender): - self._id = nid - self._task_autoid = 1 - self._result_locks = {} - self._result_values = {} - self._result_queues = {} - self._event_to_tids = {} - self._tid_to_event = {} - self._sender = sender - - self.colors = ColorsAPI(self, 'colors') - self.commands = CommandsAPI(self, 'commands') - self.disk = DiskAPI(self, 'disk') - self.fs = FSAPI(self, 'fs') - self.gps = GpsAPI(self, 'gps') - self.help = HelpAPI(self, 'help') - self.keys = KeysAPI(self, 'keys') - self.multishell = MultishellAPI(self, 'multishell') - self.os = OSAPI(self, 'os') - self.paintutils = PaintutilsAPI(self, 'paintutils') - self.peripheral = PeripheralAPI(self, 'peripheral') - self.pocket = PocketAPI(self, 'pocket') - self.rednet = RednetAPI(self, 'rednet') - self.redstone = RedstoneAPI(self, 'redstone') - self.settings = SettingsAPI(self, 'settings') - self.shell = ShellAPI(self, 'shell') - self.term = TermAPI(self, 'term') - self.textutils = TextutilsAPI(self, 'textutils') - self.turtle = TurtleAPI(self, 'turtle') - self.window = WindowAPI(self, 'window') - - async def prog_wrap(): - err = None - cancel = False - try: - await program(self) - except asyncio.CancelledError: - print('program {} cancelled'.format(self._id)) - print_exc() - err = 'program has been cancelled' - cancel = True - except Exception as e: - print('program {} crashed: {} {}'.format(self._id, type(e), e)) - print_exc() - err = type(e).__name__ + ': ' + str(e) - else: - print('program {} finished'.format(self._id)) - finally: - if not cancel: - c = {'action': 'close'} - if err is not None: - c['error'] = err - await self._sender(c) - - self._task = asyncio.create_task(prog_wrap()) - - def cancel(self): - self._task.cancel() - - def _new_task_id(self) -> str: - task_id = base36(self._task_autoid) - self._task_autoid += 1 - return task_id - - async def _eval(self, lua_code, immediate=False): - task_id = self._new_task_id() - self._result_locks[task_id] = asyncio.Event() - await self._sender({ - 'action': 'task', - 'task_id': task_id, - 'code': lua_code, - 'immediate': immediate, - }) - await self._result_locks[task_id].wait() - del self._result_locks[task_id] - result = self._result_values.pop(task_id) - print('{} → {}'.format(lua_code, repr(result))) - return result - - async def eval(self, lua_code): - return await self._eval(lua_code, True) - - async def eval_coro(self, lua_code): - return rproc.coro(await self._eval(lua_code, False)) - - async def _start_queue(self, event): - task_id = self._new_task_id() - self._result_queues[task_id] = asyncio.Queue() - es = self._event_to_tids.setdefault(event, set()) - if not es: - await self._sender({ - 'action': 'sub', - 'event': event, - }) - es.add(task_id) - self._tid_to_event[task_id] = event - return self._result_queues[task_id], task_id - - async def _stop_queue(self, task_id): - event = self._tid_to_event[task_id] - del self._result_queues[task_id] - del self._tid_to_event[task_id] - self._event_to_tids[event].discard(task_id) - if not self._event_to_tids[event]: - await self._sender({ - 'action': 'unsub', - 'event': event, - }) - - @asynccontextmanager - async def _create_temp_object(self, create_expr: str, finalizer_template: str = ''): - fid = self._new_task_id() - var = 'temp[{}]'.format(lua_string(fid)) - await self.eval_coro('{} = {}'.format(var, create_expr)) - try: - yield var - finally: - finalizer_template += '; {e} = nil' - finalizer_template = finalizer_template.lstrip(' ;') - await self.eval_coro(finalizer_template.format(e=var)) class CCApplication(web.Application): @@ -188,46 +30,34 @@ class CCApplication(web.Application): 'error': 'protocol error', }) return None - if len(msg['args']) == 0: - await ws.send_json({ - 'action': 'close', - 'error': 'arguments required', - }) - return None - module = import_module(self['source_module']) - program = getattr(module, msg['args'][0], None) - if program is None: - await ws.send_json({ - 'action': 'close', - 'error': "program doesn't exist", - }) - return None - return CCAPI(msg['computer'], program, ws.send_json) + def sender(data): + asyncio.create_task(ws.send_json(data)) + + sess = CCSession(msg['computer'], sender) + if msg['args']: + sess.run_program(msg['args'][0]) + else: + sess.run_repl() + return sess async def ws(self, request): ws = web.WebSocketResponse() await ws.prepare(request) - api = await self._launch_program(ws) - if api is not None: - try: - async for msg in self._json_messages(ws): - if msg['action'] == 'event': - for task_id in api._event_to_tids.get(msg['event'], ()): - await api._result_queues[task_id].put(msg['params']) - elif msg['action'] == 'task_result': - api._result_values[msg['task_id']] = msg['result'] - api._result_locks[msg['task_id']].set() - # print(msg['task_id'], msg['yields']) - else: - await ws.send_json({ - 'action': 'close', - 'error': 'protocol error', - }) - break - finally: - api.cancel() + sess = await self._launch_program(ws) + if sess is not None: + async for msg in self._json_messages(ws): + if msg['action'] == 'event': + pass + elif msg['action'] == 'task_result': + sess.on_task_result(msg['task_id'], msg['result']) + else: + await ws.send_json({ + 'action': 'close', + 'error': 'protocol error', + }) + break return ws @@ -245,16 +75,13 @@ class CCApplication(web.Application): ) return web.Response(text=fcont) - def initialize(self, source_module): - self['source_module'] = source_module - self['exchange'] = {} + def initialize(self): self.router.add_get('/', self.backdoor) self.router.add_get('/ws/', self.ws) def main(): parser = argparse.ArgumentParser() - parser.add_argument('module', help='Module used as source for programs') parser.add_argument('--host') parser.add_argument('--port', type=int, default=8080) args = parser.parse_args() @@ -266,7 +93,7 @@ def main(): app = CCApplication() app['port'] = args.port - app.initialize(args.module) + app.initialize() web.run_app(app, **app_kw) diff --git a/computercraft/sess.py b/computercraft/sess.py new file mode 100644 index 0000000..ae075e5 --- /dev/null +++ b/computercraft/sess.py @@ -0,0 +1,315 @@ +import asyncio +import string +import sys +from code import InteractiveConsole +from contextlib import contextmanager +from functools import partial +from importlib import import_module +from importlib.abc import MetaPathFinder, Loader +from importlib.machinery import ModuleSpec +from itertools import count +from traceback import format_exc +from types import ModuleType + +from greenlet import greenlet, getcurrent as get_current_greenlet + +from .lua import lua_string, lua_call, return_lua_call +from . import rproc + + +__all__ = ( + 'CCSession', + 'get_current_session', + 'eval_lua', + 'lua_context_object', +) + + +def debug(*args): + sys.__stdout__.write(' '.join(map(str, args)) + '\n') + sys.__stdout__.flush() + + +DIGITS = string.digits + string.ascii_lowercase + + +def base36(n): + r = '' + while n: + r += DIGITS[n % 36] + n //= 36 + return r[::-1] + + +def _is_global_greenlet(): + return not hasattr(get_current_greenlet(), 'cc_greenlet') + + +def get_current_session(): + try: + return get_current_greenlet().cc_greenlet._sess + except AttributeError: + raise RuntimeError('Computercraft function was called outside context') + + +class StdFileProxy: + def __init__(self, native): + self._native = native + + def read(self, size=-1): + if _is_global_greenlet(): + return self._native.read(size) + else: + raise RuntimeError( + "Computercraft environment doesn't support stdin read method") + + def readline(self, size=-1): + if _is_global_greenlet(): + return self._native.readline(size) + else: + if size is not None and size >= 0: + raise RuntimeError( + "Computercraft environment doesn't support " + "stdin readline method with parameter") + return rproc.string(eval_lua( + return_lua_call('io.read') + )) + + def write(self, s): + if _is_global_greenlet(): + return self._native.write(s) + else: + return rproc.nil(eval_lua( + lua_call('io.write', s) + )) + + def fileno(self): + if _is_global_greenlet(): + return self._native.fileno() + else: + # preventing use of gnu readline here + # https://github.com/python/cpython/blob/master/Python/bltinmodule.c#L1970 + raise AttributeError + + def __getattr__(self, name): + return getattr(self._native, name) + + +class ComputerCraftFinder(MetaPathFinder): + @staticmethod + def find_spec(fullname, path, target=None): + if fullname == 'cc': + return ModuleSpec(fullname, ComputerCraftLoader, is_package=True) + if fullname.startswith('cc.'): + return ModuleSpec(fullname, ComputerCraftLoader, is_package=False) + + +class ComputerCraftLoader(Loader): + @staticmethod + def create_module(spec): + sn = spec.name.split('.', 1) + assert sn[0] == 'cc' + if len(sn) == 1: + sn.append('_pkg') + rawmod = import_module('.' + sn[1], 'computercraft.subapis') + mod = ModuleType(spec.name) + for k in rawmod.__all__: + setattr(mod, k, getattr(rawmod, k)) + return mod + + @staticmethod + def exec_module(module): + pass + + +def install_import_hook(): + import sys + sys.meta_path.append(ComputerCraftFinder) + + +install_import_hook() +sys.stdin = StdFileProxy(sys.__stdin__) +sys.stdout = StdFileProxy(sys.__stdout__) +sys.stderr = StdFileProxy(sys.__stderr__) + + +def eval_lua(lua_code, immediate=False): + result = get_current_session()._server_greenlet.switch({ + 'code': lua_code, + 'immediate': immediate, + }) + # do not uncomment this, or use sys.__stdout__.write + # print('{} → {}'.format(lua_code, repr(result))) + if not immediate: + result = rproc.coro(result) + return result + + +@contextmanager +def lua_context_object(create_expr: str, finalizer_template: str = ''): + sess = get_current_session() + fid = sess.create_task_id() + var = 'temp[{}]'.format(lua_string(fid)) + eval_lua('{} = {}'.format(var, create_expr)) + try: + yield var + finally: + finalizer_template += '; {e} = nil' + finalizer_template = finalizer_template.lstrip(' ;') + eval_lua(finalizer_template.format(e=var)) + + +def eval_lua_method_factory(obj): + def method(name, *params): + return eval_lua(return_lua_call(obj + name, *params)) + return method + + +class CCGreenlet: + def __init__(self, body_fn, sess=None): + if sess is None: + self._sess = get_current_session() + else: + self._sess = sess + + self._task_id = self._sess.create_task_id() + self._sess._greenlets[self._task_id] = self + + parent_g = get_current_greenlet() + if parent_g is self._sess._server_greenlet: + self._parent = None + debug(self._task_id, 'G.__init__', 'parent None') + else: + self._parent = parent_g.cc_greenlet + self._parent._children.add(self._task_id) + debug(self._task_id, 'G.__init__', 'parent', self._parent._task_id) + + self._children = set() + self._g = greenlet(body_fn) + self._g.cc_greenlet = self + + def detach_children(self): + if self._children: + debug(self._task_id, 'G.detach_children', self._children) + ch = list(self._children) + self._children.clear() + self._sess.drop(ch) + + def _on_death(self, error=None): + debug(self._task_id, 'G._on_death', error) + self._sess._greenlets.pop(self._task_id, None) + self.detach_children() + if error is not None: + if error is True: + error = {} + self._sess._sender({'action': 'close', **error}) + if self._parent is not None: + self._parent._children.discard(self._task_id) + + def defer_switch(self, *args, **kwargs): + asyncio.get_running_loop().call_soon( + partial(self.switch, *args, **kwargs)) + + def switch(self, *args, **kwargs): + # switch must be called from server greenlet + assert get_current_greenlet() is self._sess._server_greenlet + debug(self._task_id, 'G.switch', args, kwargs) + try: + task = self._g.switch(*args, **kwargs) + except SystemExit: + debug(self._task_id, 'G.switch: ', 'SystemExit') + self._on_death(True) + return + except Exception as e: + debug(self._task_id, 'G.switch: ', 'Exception', e, type(e), format_exc(limit=None, chain=False)) + self._on_death({'error': format_exc(limit=None, chain=False)}) + return + + debug(self._task_id, 'G.switch: result', repr(task)) + + # lua_eval call or simply idle + if isinstance(task, dict): + x = self + while x._g.dead: + x = x._parent + tid = x._task_id + debug(self._task_id, 'G.switch: start task for', tid) + self._sess._sender({ + 'action': 'task', + 'task_id': tid, + **task, + }) + else: + debug(self._task_id, f'G.switch: {"dead" if self._g.dead else "idle"}') + + if self._g.dead: + if self._parent is None: + self._on_death(True) + else: + self._on_death() + + +class CCSession: + def __init__(self, computer_id, sender): + # computer_id is unique identifier of a CCSession + self._computer_id = computer_id + self._tid_allocator = map(base36, count(start=1)) + self._sender = sender + self._greenlets = {} + self._server_greenlet = get_current_greenlet() + self._program_greenlet = None + + def on_task_result(self, task_id, result): + assert get_current_greenlet() is self._server_greenlet + if task_id not in self._greenlets: + # ignore for dropped tasks + return + debug('on_task_result', task_id, result) + self._greenlets[task_id].switch(result) + + def create_task_id(self): + return next(self._tid_allocator) + + def drop(self, task_ids): + def collect(task_id): + yield task_id + g = self._greenlets.pop(task_id) + for tid in g._children: + yield from collect(tid) + + all_tids = [] + for task_id in task_ids: + all_tids.extend(collect(task_id)) + + debug('Sess.drop', all_tids) + + self._sender({ + 'action': 'drop', + 'task_ids': all_tids, + }) + + def _run_sandboxed_greenlet(self, fn): + self._program_greenlet = CCGreenlet(fn, sess=self) + self._program_greenlet.switch() + + def run_program(self, program): + def _run_program(): + p, code = eval_lua(''' +local p = fs.combine(shell.dir(), {}) +if not fs.exists(p) then return nil end +if fs.isDir(p) then return nil end +local f = fs.open(p, 'r') +local code = f.readAll() +f.close() +return p, code +'''.lstrip().format(lua_string(program))) + cc = compile(code, p, 'exec') + exec(cc, {'__file__': p}) + + self._run_sandboxed_greenlet(_run_program) + + def run_repl(self): + def _repl(): + InteractiveConsole(locals={}).interact() + + self._run_sandboxed_greenlet(_repl) diff --git a/computercraft/subapis/_pkg.py b/computercraft/subapis/_pkg.py new file mode 100644 index 0000000..5da475d --- /dev/null +++ b/computercraft/subapis/_pkg.py @@ -0,0 +1,60 @@ +from types import ModuleType + +from ..errors import LuaException +from ..lua import lua_string +from ..rproc import boolean, option_string +from ..sess import eval_lua + + +__all__ = ( + 'import_file', + 'is_commands', + 'is_multishell', + 'is_turtle', + 'is_pocket', + 'eval_lua', + 'LuaException', +) + + +def import_file(path: str, relative_to: str = None): + mod = ModuleType(path) + mod.__file__ = path + path_expr = lua_string(path) + if relative_to is not None: + path_expr = 'fs.combine(fs.getDir({}), {})'.format( + lua_string(relative_to), + path_expr, + ) + source = option_string(eval_lua(''' +local p = {} +if not fs.exists(p) then return nil end +if fs.isDir(p) then return nil end +f = fs.open(p, "r") +local src = f.readAll() +f.close() +return src +'''.lstrip().format( + path_expr, + ))) + if source is None: + raise ImportError('File not found: {}'.format(path)) + cc = compile(source, mod.__name__, 'exec') + exec(cc, vars(mod)) + return mod + + +def is_commands() -> bool: + return boolean(eval_lua('return commands ~= nil')) + + +def is_multishell() -> bool: + return boolean(eval_lua('return multishell ~= nil')) + + +def is_turtle() -> bool: + return boolean(eval_lua('return turtle ~= nil')) + + +def is_pocket() -> bool: + return boolean(eval_lua('return pocket ~= nil')) diff --git a/computercraft/subapis/base.py b/computercraft/subapis/base.py index 4150f82..cad83ba 100644 --- a/computercraft/subapis/base.py +++ b/computercraft/subapis/base.py @@ -1,18 +1,15 @@ from ..lua import LuaExpr, lua_args +from ..sess import eval_lua class BaseSubAPI(LuaExpr): - def __init__(self, cc, lua_expr): - self._cc = cc + def __init__(self, lua_expr): self._lua_expr = lua_expr def get_expr_code(self): return self._lua_expr - async def _send(self, method, *params): - return await self._method(method, *params) - - async def _method(self, name, *params): - return await self._cc.eval_coro('return {}.{}({})'.format( - self._lua_expr, name, lua_args(*params), + def _method(self, name, *params): + return eval_lua('return {}.{}({})'.format( + self.get_expr_code(), name, lua_args(*params), )) diff --git a/computercraft/subapis/colors.py b/computercraft/subapis/colors.py index f6e846d..932f37f 100644 --- a/computercraft/subapis/colors.py +++ b/computercraft/subapis/colors.py @@ -1,64 +1,100 @@ from typing import Tuple -from .base import BaseSubAPI from ..rproc import boolean, integer, tuple3_number +from ..sess import eval_lua_method_factory -class ColorsAPI(BaseSubAPI): - white = 0x1 - orange = 0x2 - magenta = 0x4 - lightBlue = 0x8 - yellow = 0x10 - lime = 0x20 - pink = 0x40 - gray = 0x80 - lightGray = 0x100 - cyan = 0x200 - purple = 0x400 - blue = 0x800 - brown = 0x1000 - green = 0x2000 - red = 0x4000 - black = 0x8000 +method = eval_lua_method_factory('colors.') - # use these chars for term.blit - chars = { - '0': white, - '1': orange, - '2': magenta, - '3': lightBlue, - '4': yellow, - '5': lime, - '6': pink, - '7': gray, - '8': lightGray, - '9': cyan, - 'a': purple, - 'b': blue, - 'c': brown, - 'd': green, - 'e': red, - 'f': black, - } - def __iter__(self): - for c in self.chars.values(): - yield c +__all__ = ( + 'white', + 'orange', + 'magenta', + 'lightBlue', + 'yellow', + 'lime', + 'pink', + 'gray', + 'lightGray', + 'cyan', + 'purple', + 'blue', + 'brown', + 'green', + 'red', + 'black', + 'combine', + 'subtract', + 'test', + 'packRGB', + 'unpackRGB', + 'chars', + 'iter_colors', +) - # combine, subtract and test are mostly for redstone.setBundledOutput - async def combine(self, *colors: int) -> int: - return integer(await self._send('combine', *colors)) +white = 0x1 +orange = 0x2 +magenta = 0x4 +lightBlue = 0x8 +yellow = 0x10 +lime = 0x20 +pink = 0x40 +gray = 0x80 +lightGray = 0x100 +cyan = 0x200 +purple = 0x400 +blue = 0x800 +brown = 0x1000 +green = 0x2000 +red = 0x4000 +black = 0x8000 - async def subtract(self, color_set: int, *colors: int) -> int: - return integer(await self._send('subtract', color_set, *colors)) - async def test(self, colors: int, color: int) -> bool: - return boolean(await self._send('test', colors, color)) +# combine, subtract and test are mostly for redstone.setBundledOutput - async def packRGB(self, r: float, g: float, b: float) -> int: - return integer(await self._send('packRGB', r, g, b)) +def combine(*colors: int) -> int: + return integer(method('combine', *colors)) - async def unpackRGB(self, rgb: int) -> Tuple[float, float, float]: - return tuple3_number(await self._send('unpackRGB', rgb)) + +def subtract(color_set: int, *colors: int) -> int: + return integer(method('subtract', color_set, *colors)) + + +def test(colors: int, color: int) -> bool: + return boolean(method('test', colors, color)) + + +def packRGB(r: float, g: float, b: float) -> int: + return integer(method('packRGB', r, g, b)) + + +def unpackRGB(rgb: int) -> Tuple[float, float, float]: + return tuple3_number(method('unpackRGB', rgb)) + + +# use these chars for term.blit +chars = { + '0': white, + '1': orange, + '2': magenta, + '3': lightBlue, + '4': yellow, + '5': lime, + '6': pink, + '7': gray, + '8': lightGray, + '9': cyan, + 'a': purple, + 'b': blue, + 'c': brown, + 'd': green, + 'e': red, + 'f': black, +} + + +def iter_colors(self): + for c in self.chars.values(): + yield c diff --git a/computercraft/subapis/commands.py b/computercraft/subapis/commands.py index 8128d07..6c834ac 100644 --- a/computercraft/subapis/commands.py +++ b/computercraft/subapis/commands.py @@ -1,24 +1,37 @@ from typing import Tuple, List, Optional -from .base import BaseSubAPI from ..rproc import tuple3_integer, any_dict, any_list, array_string, fact_tuple, boolean, option_integer +from ..sess import eval_lua_method_factory command_result = fact_tuple(boolean, array_string, option_integer, tail_nils=1) +method = eval_lua_method_factory('commands.') -class CommandsAPI(BaseSubAPI): - async def exec(self, command: str) -> Tuple[bool, List[str], Optional[int]]: - return command_result(await self._send('exec', command)) +__all__ = ( + 'exec', + 'list', + 'getBlockPosition', + 'getBlockInfo', + 'getBlockInfos', +) - async def list(self) -> List[str]: - return array_string(await self._send('list')) - async def getBlockPosition(self) -> Tuple[int, int, int]: - return tuple3_integer(await self._send('getBlockPosition')) +def exec(command: str) -> Tuple[bool, List[str], Optional[int]]: + return command_result(method('exec', command)) - async def getBlockInfo(self, x: int, y: int, z: int) -> dict: - return any_dict(await self._send('getBlockInfo', x, y, z)) - async def getBlockInfos(self, x1: int, y1: int, z1: int, x2: int, y2: int, z2: int) -> List[dict]: - return any_list(await self._send('getBlockInfos', x1, y1, z1, x2, y2, z2)) +def list() -> List[str]: + return array_string(method('list')) + + +def getBlockPosition() -> Tuple[int, int, int]: + return tuple3_integer(method('getBlockPosition')) + + +def getBlockInfo(x: int, y: int, z: int) -> dict: + return any_dict(method('getBlockInfo', x, y, z)) + + +def getBlockInfos(x1: int, y1: int, z1: int, x2: int, y2: int, z2: int) -> List[dict]: + return any_list(method('getBlockInfos', x1, y1, z1, x2, y2, z2)) diff --git a/computercraft/subapis/disk.py b/computercraft/subapis/disk.py index 5540b55..b9afd48 100644 --- a/computercraft/subapis/disk.py +++ b/computercraft/subapis/disk.py @@ -1,39 +1,66 @@ from typing import Optional, Union -from .base import BaseSubAPI from ..rproc import boolean, nil, option_integer, option_string, option_string_bool +from ..sess import eval_lua_method_factory -class DiskAPI(BaseSubAPI): - async def isPresent(self, side: str) -> bool: - return boolean(await self._send('isPresent', side)) +method = eval_lua_method_factory('disk.') - async def hasData(self, side: str) -> bool: - return boolean(await self._send('hasData', side)) - async def getMountPath(self, side: str) -> Optional[str]: - return option_string(await self._send('getMountPath', side)) +__all__ = ( + 'isPresent', + 'hasData', + 'getMountPath', + 'setLabel', + 'getLabel', + 'getID', + 'hasAudio', + 'getAudioTitle', + 'playAudio', + 'stopAudio', + 'eject', +) - async def setLabel(self, side: str, label: str): - return nil(await self._send('setLabel', side, label)) - async def getLabel(self, side: str) -> Optional[str]: - return option_string(await self._send('getLabel', side)) +def isPresent(side: str) -> bool: + return boolean(method('isPresent', side)) - async def getID(self, side: str) -> Optional[int]: - return option_integer(await self._send('getID', side)) - async def hasAudio(self, side: str) -> bool: - return boolean(await self._send('hasAudio', side)) +def hasData(side: str) -> bool: + return boolean(method('hasData', side)) - async def getAudioTitle(self, side: str) -> Optional[Union[bool, str]]: - return option_string_bool(await self._send('getAudioTitle', side)) - async def playAudio(self, side: str): - return nil(await self._send('playAudio', side)) +def getMountPath(side: str) -> Optional[str]: + return option_string(method('getMountPath', side)) - async def stopAudio(self, side: str): - return nil(await self._send('stopAudio', side)) - async def eject(self, side: str): - return nil(await self._send('eject', side)) +def setLabel(side: str, label: str): + return nil(method('setLabel', side, label)) + + +def getLabel(side: str) -> Optional[str]: + return option_string(method('getLabel', side)) + + +def getID(side: str) -> Optional[int]: + return option_integer(method('getID', side)) + + +def hasAudio(side: str) -> bool: + return boolean(method('hasAudio', side)) + + +def getAudioTitle(side: str) -> Optional[Union[bool, str]]: + return option_string_bool(method('getAudioTitle', side)) + + +def playAudio(side: str): + return nil(method('playAudio', side)) + + +def stopAudio(side: str): + return nil(method('stopAudio', side)) + + +def eject(side: str): + return nil(method('eject', side)) diff --git a/computercraft/subapis/fs.py b/computercraft/subapis/fs.py index deac6b9..19eaee4 100644 --- a/computercraft/subapis/fs.py +++ b/computercraft/subapis/fs.py @@ -1,10 +1,11 @@ -from contextlib import asynccontextmanager +from contextlib import contextmanager from typing import Optional, List, Union from .base import BaseSubAPI from ..errors import LuaException -from ..lua import lua_args +from ..lua import lua_call from ..rproc import boolean, string, integer, nil, array_string, option_string, option_integer, fact_scheme_dict +from ..sess import eval_lua_method_factory, lua_context_object attribute = fact_scheme_dict({ @@ -16,9 +17,9 @@ attribute = fact_scheme_dict({ class SeekMixin: - async def seek(self, whence: str = None, offset: int = None) -> int: + def seek(self, whence: str = None, offset: int = None) -> int: # whence: set, cur, end - r = await self._send('seek', whence, offset) + r = self._method('seek', whence, offset) if isinstance(r, list): assert r[0] is False raise LuaException(r[1]) @@ -26,23 +27,25 @@ class SeekMixin: class ReadHandle(BaseSubAPI): - async def read(self, count: int = None) -> Optional[Union[str, int]]: - r = await self._send('read', count) + # TODO: binary handle must return bytes instead string + + def read(self, count: int = None) -> Optional[Union[str, int]]: + r = self._method('read', count) return option_integer(r) if count is None else option_string(r) - async def readLine(self) -> Optional[str]: - return option_string(await self._send('readLine')) + def readLine(self) -> Optional[str]: + return option_string(self._method('readLine')) - async def readAll(self) -> str: - return string(await self._send('readAll')) + def readAll(self) -> str: + return string(self._method('readAll')) - def __aiter__(self): + def __iter__(self): return self - async def __anext__(self): - line = await self.readLine() + def __next__(self): + line = self.readLine() if line is None: - raise StopAsyncIteration + raise StopIteration return line @@ -51,101 +54,144 @@ class BinaryReadHandle(ReadHandle, SeekMixin): class WriteHandle(BaseSubAPI): - async def write(self, text: str): - return nil(await self._send('write', text)) + def write(self, text: str): + return nil(self._method('write', text)) - async def writeLine(self, text: str): - return nil(await self._send('writeLine', text)) + def writeLine(self, text: str): + return nil(self._method('writeLine', text)) - async def flush(self): - return nil(await self._send('flush')) + def flush(self): + return nil(self._method('flush')) class BinaryWriteHandle(WriteHandle, SeekMixin): pass -class FSAPI(BaseSubAPI): - async def list(self, path: str) -> List[str]: - return array_string(await self._send('list', path)) +method = eval_lua_method_factory('fs.') - async def exists(self, path: str) -> bool: - return boolean(await self._send('exists', path)) - async def isDir(self, path: str) -> bool: - return boolean(await self._send('isDir', path)) +__all__ = ( + 'list', + 'exists', + 'isDir', + 'isReadOnly', + 'getDrive', + 'getSize', + 'getFreeSpace', + 'getCapacity', + 'makeDir', + 'move', + 'copy', + 'delete', + 'combine', + 'open', + 'find', + 'getDir', + 'getName', + 'isDriveRoot', + 'complete', + 'attributes', +) - async def isReadOnly(self, path: str) -> bool: - return boolean(await self._send('isReadOnly', path)) - async def getDrive(self, path: str) -> Optional[str]: - return option_string(await self._send('getDrive', path)) +def list(path: str) -> List[str]: + return array_string(method('list', path)) - async def getSize(self, path: str) -> int: - return integer(await self._send('getSize', path)) - async def getFreeSpace(self, path: str) -> int: - return integer(await self._send('getFreeSpace', path)) +def exists(path: str) -> bool: + return boolean(method('exists', path)) - async def getCapacity(self, path: str) -> int: - return integer(await self._send('getCapacity', path)) - async def makeDir(self, path: str): - return nil(await self._send('makeDir', path)) +def isDir(path: str) -> bool: + return boolean(method('isDir', path)) - async def move(self, fromPath: str, toPath: str): - return nil(await self._send('move', fromPath, toPath)) - async def copy(self, fromPath: str, toPath: str): - return nil(await self._send('copy', fromPath, toPath)) +def isReadOnly(path: str) -> bool: + return boolean(method('isReadOnly', path)) - async def delete(self, path: str): - return nil(await self._send('delete', path)) - async def combine(self, basePath: str, localPath: str) -> str: - return string(await self._send('combine', basePath, localPath)) +def getDrive(path: str) -> Optional[str]: + return option_string(method('getDrive', path)) - @asynccontextmanager - async def open(self, path: str, mode: str): - ''' - Usage: - async with api.fs.open('filename', 'w') as f: - await f.writeLine('textline') +def getSize(path: str) -> int: + return integer(method('getSize', path)) - async with api.fs.open('filename', 'r') as f: - async for line in f: - ... - ''' - create_expr = '{}.open({})'.format( - self.get_expr_code(), - lua_args(path, mode), - ) - fin_tpl = '{e}.close()' - async with self._cc._create_temp_object(create_expr, fin_tpl) as var: - if 'b' in mode: - hcls = BinaryReadHandle if 'r' in mode else BinaryWriteHandle - else: - hcls = ReadHandle if 'r' in mode else WriteHandle - yield hcls(self._cc, var) - async def find(self, wildcard: str) -> List[str]: - return array_string(await self._send('find', wildcard)) +def getFreeSpace(path: str) -> int: + return integer(method('getFreeSpace', path)) - async def getDir(self, path: str) -> str: - return string(await self._send('getDir', path)) - async def getName(self, path: str) -> str: - return string(await self._send('getName', path)) +def getCapacity(path: str) -> int: + return integer(method('getCapacity', path)) - async def isDriveRoot(self, path: str) -> bool: - return boolean(await self._send('isDriveRoot', path)) - async def complete( - self, partialName: str, path: str, includeFiles: bool = None, includeDirs: bool = None, - ) -> List[str]: - return array_string(await self._send( - 'complete', partialName, path, includeFiles, includeDirs)) +def makeDir(path: str): + return nil(method('makeDir', path)) - async def attributes(self, path: str) -> dict: - return attribute(await self._send('attributes', path)) + +def move(fromPath: str, toPath: str): + return nil(method('move', fromPath, toPath)) + + +def copy(fromPath: str, toPath: str): + return nil(method('copy', fromPath, toPath)) + + +def delete(path: str): + return nil(method('delete', path)) + + +def combine(basePath: str, localPath: str) -> str: + return string(method('combine', basePath, localPath)) + + +@contextmanager +def open(path: str, mode: str): + ''' + Usage: + + with fs.open('filename', 'w') as f: + f.writeLine('textline') + + with fs.open('filename', 'r') as f: + for line in f: + ... + ''' + with lua_context_object( + lua_call('fs.open', path, mode), + '{e}.close()', + ) as var: + if 'b' in mode: + hcls = BinaryReadHandle if 'r' in mode else BinaryWriteHandle + else: + hcls = ReadHandle if 'r' in mode else WriteHandle + yield hcls(var) + + +def find(wildcard: str) -> List[str]: + return array_string(method('find', wildcard)) + + +def getDir(path: str) -> str: + return string(method('getDir', path)) + + +def getName(path: str) -> str: + return string(method('getName', path)) + + +def isDriveRoot(path: str) -> bool: + return boolean(method('isDriveRoot', path)) + + +def complete( + partialName: str, path: str, includeFiles: bool = None, includeDirs: bool = None, +) -> List[str]: + return array_string(method( + 'complete', partialName, path, includeFiles, includeDirs)) + + +def attributes(path: str) -> dict: + return attribute(method('attributes', path)) diff --git a/computercraft/subapis/gps.py b/computercraft/subapis/gps.py index 16f81c7..9a770e9 100644 --- a/computercraft/subapis/gps.py +++ b/computercraft/subapis/gps.py @@ -1,15 +1,22 @@ from typing import Tuple, Optional -from .base import BaseSubAPI from ..lua import LuaNum from ..rproc import tuple3_number, fact_option +from ..sess import eval_lua_method_factory option_tuple3_number = fact_option(tuple3_number) +method = eval_lua_method_factory('gps.') -class GpsAPI(BaseSubAPI): - CHANNEL_GPS = 65534 +__all__ = ( + 'CHANNEL_GPS', + 'locate', +) - async def locate(self, timeout: LuaNum = None, debug: bool = None) -> Optional[Tuple[LuaNum, LuaNum, LuaNum]]: - return option_tuple3_number(await self._send('locate', timeout, debug)) + +CHANNEL_GPS = 65534 + + +def locate(timeout: LuaNum = None, debug: bool = None) -> Optional[Tuple[LuaNum, LuaNum, LuaNum]]: + return option_tuple3_number(method('locate', timeout, debug)) diff --git a/computercraft/subapis/help.py b/computercraft/subapis/help.py index d24569b..3b551f1 100644 --- a/computercraft/subapis/help.py +++ b/computercraft/subapis/help.py @@ -1,21 +1,36 @@ from typing import Optional, List -from .base import BaseSubAPI from ..rproc import string, nil, array_string, option_string +from ..sess import eval_lua_method_factory -class HelpAPI(BaseSubAPI): - async def path(self) -> str: - return string(await self._send('path')) +method = eval_lua_method_factory('help.') - async def setPath(self, path: str): - return nil(await self._send('setPath', path)) - async def lookup(self, topic: str) -> Optional[str]: - return option_string(await self._send('lookup', topic)) +__all__ = ( + 'path', + 'setPath', + 'lookup', + 'topics', + 'completeTopic', +) - async def topics(self) -> List[str]: - return array_string(await self._send('topics')) - async def completeTopic(self, topicPrefix: str) -> List[str]: - return array_string(await self._send('completeTopic', topicPrefix)) +def path() -> str: + return string(method('path')) + + +def setPath(path: str): + return nil(method('setPath', path)) + + +def lookup(topic: str) -> Optional[str]: + return option_string(method('lookup', topic)) + + +def topics() -> List[str]: + return array_string(method('topics')) + + +def completeTopic(topicPrefix: str) -> List[str]: + return array_string(method('completeTopic', topicPrefix)) diff --git a/computercraft/subapis/keys.py b/computercraft/subapis/keys.py index be32d10..cf5e6ef 100644 --- a/computercraft/subapis/keys.py +++ b/computercraft/subapis/keys.py @@ -1,19 +1,28 @@ from typing import Optional -from .base import BaseSubAPI from ..lua import lua_string from ..rproc import option_integer, option_string +from ..sess import eval_lua, eval_lua_method_factory -class KeysAPI(BaseSubAPI): - async def getCode(self, name: str) -> Optional[int]: - # replaces properties - # keys.space → await api.keys.getCode('space') - return option_integer(await self._cc.eval_coro(''' -if type({module}[{key}]) == 'number' then - return {module}[{key}] +method = eval_lua_method_factory('keys.') + + +__all__ = ( + 'getCode', + 'getName', +) + + +def getCode(name: str) -> Optional[int]: + # replaces properties + # keys.space → keys.getCode('space') + return option_integer(eval_lua(''' +if type(keys[{key}]) == 'number' then + return keys[{key}] end -return nil'''.format(module=self.get_expr_code(), key=lua_string(name)))) +return nil'''.format(key=lua_string(name)))) - async def getName(self, code: int) -> Optional[str]: - return option_string(await self._send('getName', code)) + +def getName(code: int) -> Optional[str]: + return option_string(method('getName', code)) diff --git a/computercraft/subapis/mixins.py b/computercraft/subapis/mixins.py index 0aabcb1..c8843a5 100644 --- a/computercraft/subapis/mixins.py +++ b/computercraft/subapis/mixins.py @@ -5,56 +5,56 @@ from ..rproc import boolean, nil, integer, tuple3_number, tuple2_integer class TermMixin: - async def write(self, text: str): - return nil(await self._send('write', text)) + def write(self, text: str): + return nil(self._method('write', text)) - async def blit(self, text: str, textColors: str, backgroundColors: str): - return nil(await self._send('blit', text, textColors, backgroundColors)) + def blit(self, text: str, textColors: str, backgroundColors: str): + return nil(self._method('blit', text, textColors, backgroundColors)) - async def clear(self): - return nil(await self._send('clear')) + def clear(self): + return nil(self._method('clear')) - async def clearLine(self): - return nil(await self._send('clearLine')) + def clearLine(self): + return nil(self._method('clearLine')) - async def getCursorPos(self) -> Tuple[int, int]: - return tuple2_integer(await self._send('getCursorPos')) + def getCursorPos(self) -> Tuple[int, int]: + return tuple2_integer(self._method('getCursorPos')) - async def setCursorPos(self, x: int, y: int): - return nil(await self._send('setCursorPos', x, y)) + def setCursorPos(self, x: int, y: int): + return nil(self._method('setCursorPos', x, y)) - async def getCursorBlink(self) -> bool: - return boolean(await self._send('getCursorBlink')) + def getCursorBlink(self) -> bool: + return boolean(self._method('getCursorBlink')) - async def setCursorBlink(self, value: bool): - return nil(await self._send('setCursorBlink', value)) + def setCursorBlink(self, value: bool): + return nil(self._method('setCursorBlink', value)) - async def isColor(self) -> bool: - return boolean(await self._send('isColor')) + def isColor(self) -> bool: + return boolean(self._method('isColor')) - async def getSize(self) -> Tuple[int, int]: - return tuple2_integer(await self._send('getSize')) + def getSize(self) -> Tuple[int, int]: + return tuple2_integer(self._method('getSize')) - async def scroll(self, lines: int): - return nil(await self._send('scroll', lines)) + def scroll(self, lines: int): + return nil(self._method('scroll', lines)) - async def setTextColor(self, colorID: int): - return nil(await self._send('setTextColor', colorID)) + def setTextColor(self, colorID: int): + return nil(self._method('setTextColor', colorID)) - async def getTextColor(self) -> int: - return integer(await self._send('getTextColor')) + def getTextColor(self) -> int: + return integer(self._method('getTextColor')) - async def setBackgroundColor(self, colorID: int): - return nil(await self._send('setBackgroundColor', colorID)) + def setBackgroundColor(self, colorID: int): + return nil(self._method('setBackgroundColor', colorID)) - async def getBackgroundColor(self) -> int: - return integer(await self._send('getBackgroundColor')) + def getBackgroundColor(self) -> int: + return integer(self._method('getBackgroundColor')) - async def getPaletteColor(self, colorID: int) -> Tuple[float, float, float]: - return tuple3_number(await self._send('getPaletteColor', colorID)) + def getPaletteColor(self, colorID: int) -> Tuple[float, float, float]: + return tuple3_number(self._method('getPaletteColor', colorID)) - async def setPaletteColor(self, colorID: int, r: float, g: float, b: float): - return nil(await self._send('setPaletteColor', colorID, r, g, b)) + def setPaletteColor(self, colorID: int, r: float, g: float, b: float): + return nil(self._method('setPaletteColor', colorID, r, g, b)) class TermTarget(LuaExpr): diff --git a/computercraft/subapis/multishell.py b/computercraft/subapis/multishell.py index eeb393e..2b5ddf8 100644 --- a/computercraft/subapis/multishell.py +++ b/computercraft/subapis/multishell.py @@ -1,27 +1,46 @@ from typing import Optional -from .base import BaseSubAPI from ..rproc import integer, nil, boolean, option_string +from ..sess import eval_lua_method_factory -class MultishellAPI(BaseSubAPI): - async def getCurrent(self) -> int: - return integer(await self._send('getCurrent')) +method = eval_lua_method_factory('multishell.') - async def getCount(self) -> int: - return integer(await self._send('getCount')) - async def launch(self, environment: dict, programPath: str, *args: str) -> int: - return integer(await self._send('launch', environment, programPath, *args)) +__all__ = ( + 'getCurrent', + 'getCount', + 'launch', + 'setTitle', + 'getTitle', + 'setFocus', + 'getFocus', +) - async def setTitle(self, tabID: int, title: str): - return nil(await self._send('setTitle', tabID, title)) - async def getTitle(self, tabID: int) -> Optional[str]: - return option_string(await self._send('getTitle', tabID)) +def getCurrent() -> int: + return integer(method('getCurrent')) - async def setFocus(self, tabID: int) -> bool: - return boolean(await self._send('setFocus', tabID)) - async def getFocus(self) -> int: - return integer(await self._send('getFocus')) +def getCount() -> int: + return integer(method('getCount')) + + +def launch(environment: dict, programPath: str, *args: str) -> int: + return integer(method('launch', environment, programPath, *args)) + + +def setTitle(tabID: int, title: str): + return nil(method('setTitle', tabID, title)) + + +def getTitle(tabID: int) -> Optional[str]: + return option_string(method('getTitle', tabID)) + + +def setFocus(tabID: int) -> bool: + return boolean(method('setFocus', tabID)) + + +def getFocus() -> int: + return integer(method('getFocus')) diff --git a/computercraft/subapis/os.py b/computercraft/subapis/os.py index 730e85a..f848093 100644 --- a/computercraft/subapis/os.py +++ b/computercraft/subapis/os.py @@ -1,114 +1,115 @@ -from contextlib import asynccontextmanager from typing import Optional, List -from .base import BaseSubAPI from ..lua import LuaNum -from ..rproc import nil, string, option_string, number, integer, boolean +from ..rproc import nil, string, option_string, number, integer, boolean, any_list +from ..sess import eval_lua_method_factory -class CCEventQueue: - def __init__(self, q): - self._q = q - self.filter = self.default_filter - - @staticmethod - def default_filter(msg): - return True, msg - - async def __aiter__(self): - while True: - msg = await self._q.get() - emit, msg = self.filter(msg) - if emit: - yield msg +method = eval_lua_method_factory('os.') -class OSAPI(BaseSubAPI): - async def version(self) -> str: - return string(await self._send('version')) +__all__ = ( + 'version', + 'getComputerID', + 'getComputerLabel', + 'setComputerLabel', + 'run', + 'pullEvent', + 'pullEventRaw', + 'queueEvent', + 'clock', + 'time', + 'day', + 'epoch', + 'sleep', + 'startTimer', + 'cancelTimer', + 'setAlarm', + 'cancelAlarm', + 'shutdown', + 'reboot', +) - async def getComputerID(self) -> int: - return integer(await self._send('getComputerID')) - async def getComputerLabel(self) -> Optional[str]: - return option_string(await self._send('getComputerLabel')) +def version() -> str: + return string(method('version')) - async def setComputerLabel(self, label: Optional[str]): - return nil(await self._send('setComputerLabel', label)) - async def run(self, environment: dict, programPath: str, *args: List[str]): - return boolean(await self._send('run', environment, programPath, *args)) +def getComputerID() -> int: + return integer(method('getComputerID')) - @asynccontextmanager - async def captureEvent(self, targetEvent: str) -> CCEventQueue: - ''' - Use this function instead loop over pullEvent/pullEventRaw. - This makes a queue capable of effective event translation, excluding roundtrip delay. - E.g.: - 1. You start short timer using os.startTimer(). - 2. Then you call os.pullEvent() in loop to catch timer event. +def getComputerLabel() -> Optional[str]: + return option_string(method('getComputerLabel')) - There exist some dead intervals of time, while pullEvent is going to be transferred to python side. - Lua side can receive and discard timer event since there's no consumer for it. - captureEvent gives you reliable way of receiving events without losses. - Register queue before firing a timer, start a timer, listen for messages in queue. +def setComputerLabel(label: Optional[str]): + return nil(method('setComputerLabel', label)) - # it's significant here: start queue before starting a timer - async with api.os.captureEvent('timer') as timer_queue: - myTimer = await api.os.startTimer(3) - async for etid, in timer_queue: - if etid == myTimer: - await api.print('Timer reached') - break - ''' - q, tid = await self._cc._start_queue(targetEvent) - try: - yield CCEventQueue(q) - finally: - await self._cc._stop_queue(tid) - async def queueEvent(self, event: str, *params): - return nil(await self._send('queueEvent', event, *params)) +def run(environment: dict, programPath: str, *args: List[str]): + return boolean(method('run', environment, programPath, *args)) - async def clock(self) -> LuaNum: - # number of game ticks * 0.05, roughly seconds - return number(await self._send('clock')) - # regarding ingame parameter below: - # python has great stdlib to deal with real current time - # we keep here only in-game time methods and parameters +def pullEvent(event: str) -> tuple: + return tuple(any_list(method('pullEvent', event))) - async def time(self) -> LuaNum: - # in hours 0..24 - return number(await self._send('time', 'ingame')) - async def day(self) -> int: - return integer(await self._send('day', 'ingame')) +def pullEventRaw(event: str) -> tuple: + return tuple(any_list(method('pullEventRaw', event))) - async def epoch(self) -> int: - return integer(await self._send('epoch', 'ingame')) - async def sleep(self, seconds: LuaNum): - return nil(await self._send('sleep', seconds)) +def queueEvent(event: str, *params): + return nil(method('queueEvent', event, *params)) - async def startTimer(self, timeout: LuaNum) -> int: - return integer(await self._send('startTimer', timeout)) - async def cancelTimer(self, timerID: int): - return nil(await self._send('cancelTimer', timerID)) +def clock() -> LuaNum: + # number of game ticks * 0.05, roughly seconds + return number(method('clock')) - async def setAlarm(self, time: LuaNum) -> int: - # takes time of the day in hours 0..24 - # returns integer alarmID - return integer(await self._send('setAlarm', time)) - async def cancelAlarm(self, alarmID: int): - return nil(await self._send('cancelAlarm', alarmID)) +# regarding ingame parameter below: +# python has great stdlib to deal with real current time +# we keep here only in-game time methods and parameters - async def shutdown(self): - return nil(await self._send('shutdown')) +def time() -> LuaNum: + # in hours 0..24 + return number(method('time', 'ingame')) - async def reboot(self): - return nil(await self._send('reboot')) + +def day() -> int: + return integer(method('day', 'ingame')) + + +def epoch() -> int: + return integer(method('epoch', 'ingame')) + + +def sleep(seconds: LuaNum): + return nil(method('sleep', seconds)) + + +def startTimer(timeout: LuaNum) -> int: + return integer(method('startTimer', timeout)) + + +def cancelTimer(timerID: int): + return nil(method('cancelTimer', timerID)) + + +def setAlarm(time: LuaNum) -> int: + # takes time of the day in hours 0..24 + # returns integer alarmID + return integer(method('setAlarm', time)) + + +def cancelAlarm(alarmID: int): + return nil(method('cancelAlarm', alarmID)) + + +def shutdown(): + return nil(method('shutdown')) + + +def reboot(): + return nil(method('reboot')) diff --git a/computercraft/subapis/paintutils.py b/computercraft/subapis/paintutils.py index a8d28ac..9d0e825 100644 --- a/computercraft/subapis/paintutils.py +++ b/computercraft/subapis/paintutils.py @@ -1,30 +1,47 @@ from typing import List -from .base import BaseSubAPI from ..rproc import nil, integer, fact_array +from ..sess import eval_lua_method_factory array_2d_integer = fact_array(fact_array(integer)) +method = eval_lua_method_factory('paintutils.') -class PaintutilsAPI(BaseSubAPI): - async def parseImage(self, data: str) -> List[List[int]]: - return array_2d_integer(await self._send('parseImage', data)) +__all__ = ( + 'parseImage', + 'loadImage', + 'drawPixel', + 'drawLine', + 'drawBox', + 'drawFilledBox', + 'drawImage', +) - async def loadImage(self, path: str) -> List[List[int]]: - return array_2d_integer(await self._send('loadImage', path)) - async def drawPixel(self, x: int, y: int, color: int = None): - return nil(await self._send('drawPixel', x, y, color)) +def parseImage(data: str) -> List[List[int]]: + return array_2d_integer(method('parseImage', data)) - async def drawLine(self, startX: int, startY: int, endX: int, endY: int, color: int = None): - return nil(await self._send('drawLine', startX, startY, endX, endY, color)) - async def drawBox(self, startX: int, startY: int, endX: int, endY: int, color: int = None): - return nil(await self._send('drawBox', startX, startY, endX, endY, color)) +def loadImage(path: str) -> List[List[int]]: + return array_2d_integer(method('loadImage', path)) - async def drawFilledBox(self, startX: int, startY: int, endX: int, endY: int, color: int = None): - return nil(await self._send('drawFilledBox', startX, startY, endX, endY, color)) - async def drawImage(self, image: List[List[int]], xPos: int, yPos: int): - return nil(await self._send('drawImage', image, xPos, yPos)) +def drawPixel(x: int, y: int, color: int = None): + return nil(method('drawPixel', x, y, color)) + + +def drawLine(startX: int, startY: int, endX: int, endY: int, color: int = None): + return nil(method('drawLine', startX, startY, endX, endY, color)) + + +def drawBox(startX: int, startY: int, endX: int, endY: int, color: int = None): + return nil(method('drawBox', startX, startY, endX, endY, color)) + + +def drawFilledBox(startX: int, startY: int, endX: int, endY: int, color: int = None): + return nil(method('drawFilledBox', startX, startY, endX, endY, color)) + + +def drawImage(image: List[List[int]], xPos: int, yPos: int): + return nil(method('drawImage', image, xPos, yPos)) diff --git a/computercraft/subapis/parallel.py b/computercraft/subapis/parallel.py new file mode 100644 index 0000000..c0c3219 --- /dev/null +++ b/computercraft/subapis/parallel.py @@ -0,0 +1,39 @@ +from ..sess import debug, CCGreenlet, get_current_greenlet + + +__all__ = ( + 'waitForAny', + 'waitForAll', +) + + +def waitForAny(*task_fns): + pgl = get_current_greenlet().cc_greenlet + sess = pgl._sess + + gs = [CCGreenlet(fn) for fn in task_fns] + for g in gs: + g.defer_switch() + + try: + result = sess._server_greenlet.switch() + debug('waitForAny switch result', result) + finally: + pgl.detach_children() + + +def waitForAll(*task_fns): + pgl = get_current_greenlet().cc_greenlet + sess = pgl._sess + + gs = [CCGreenlet(fn) for fn in task_fns] + for g in gs: + g.defer_switch() + + try: + for _ in range(len(task_fns)): + result = sess._server_greenlet.switch() + debug('waitForAll switch result', result) + debug('waitForAll finish') + finally: + pgl.detach_children() diff --git a/computercraft/subapis/peripheral.py b/computercraft/subapis/peripheral.py index 684ae40..18d9938 100644 --- a/computercraft/subapis/peripheral.py +++ b/computercraft/subapis/peripheral.py @@ -1,96 +1,92 @@ -from contextlib import asynccontextmanager from dataclasses import dataclass from typing import Optional, List, Tuple, Any, Union -from .base import BaseSubAPI from .mixins import TermMixin, TermTarget from .turtle import craft_result -from ..lua import LuaNum, lua_args +from ..lua import LuaNum, lua_args, return_lua_call from ..rproc import ( boolean, nil, integer, string, option_integer, option_string, tuple2_integer, array_string, option_string_bool, try_result, ) +from ..sess import eval_lua, eval_lua_method_factory class BasePeripheral: # NOTE: is not LuaExpr, you can't pass peripheral as parameter + # TODO: to fix this we can supply separate lua expr, result of .wrap() - def __init__(self, cc, lua_method_expr, *prepend_params): - self._cc = cc + def __init__(self, lua_method_expr, *prepend_params): self._lua_method_expr = lua_method_expr self._prepend_params = prepend_params - async def _send(self, method, *params): - return await self._method(method, *params) - - async def _method(self, name, *params): - return await self._cc.eval_coro('return {}({})'.format( + def _method(self, name, *params): + return eval_lua(return_lua_call( self._lua_method_expr, - lua_args(*self._prepend_params, name, *params), + *self._prepend_params, name, *params, )) class CCDrive(BasePeripheral): - async def isDiskPresent(self) -> bool: - return boolean(await self._send('isDiskPresent')) + def isDiskPresent(self) -> bool: + return boolean(self._method('isDiskPresent')) - async def getDiskLabel(self) -> Optional[str]: - return option_string(await self._send('getDiskLabel')) + def getDiskLabel(self) -> Optional[str]: + return option_string(self._method('getDiskLabel')) - async def setDiskLabel(self, label: str): - return nil(await self._send('setDiskLabel', label)) + def setDiskLabel(self, label: str): + return nil(self._method('setDiskLabel', label)) - async def hasData(self) -> bool: - return boolean(await self._send('hasData')) + def hasData(self) -> bool: + return boolean(self._method('hasData')) - async def getMountPath(self) -> Optional[str]: - return option_string(await self._send('getMountPath')) + def getMountPath(self) -> Optional[str]: + return option_string(self._method('getMountPath')) - async def hasAudio(self) -> bool: - return boolean(await self._send('hasAudio')) + def hasAudio(self) -> bool: + return boolean(self._method('hasAudio')) - async def getAudioTitle(self) -> Optional[Union[bool, str]]: - return option_string_bool(await self._send('getAudioTitle')) + def getAudioTitle(self) -> Optional[Union[bool, str]]: + return option_string_bool(self._method('getAudioTitle')) - async def playAudio(self): - return nil(await self._send('playAudio')) + def playAudio(self): + return nil(self._method('playAudio')) - async def stopAudio(self): - return nil(await self._send('stopAudio')) + def stopAudio(self): + return nil(self._method('stopAudio')) - async def ejectDisk(self): - return nil(await self._send('ejectDisk')) + def ejectDisk(self): + return nil(self._method('ejectDisk')) - async def getDiskID(self) -> Optional[int]: - return option_integer(await self._send('getDiskID')) + def getDiskID(self) -> Optional[int]: + return option_integer(self._method('getDiskID')) class CCMonitor(BasePeripheral, TermMixin): - async def getTextScale(self) -> int: - return integer(await self._send('getTextScale')) + def getTextScale(self) -> int: + return integer(self._method('getTextScale')) - async def setTextScale(self, scale: int): - return nil(await self._send('setTextScale', scale)) + def setTextScale(self, scale: int): + return nil(self._method('setTextScale', scale)) class ComputerMixin: - async def turnOn(self): - return nil(await self._send('turnOn')) + def turnOn(self): + return nil(self._method('turnOn')) - async def shutdown(self): - return nil(await self._send('shutdown')) + def shutdown(self): + return nil(self._method('shutdown')) - async def reboot(self): - return nil(await self._send('reboot')) + def reboot(self): + return nil(self._method('reboot')) - async def getID(self) -> int: - return integer(await self._send('getID')) + def getID(self) -> int: + return integer(self._method('getID')) - async def getLabel(self) -> Optional[str]: - return option_string(await self._send('getLabel')) + def getLabel(self) -> Optional[str]: + return option_string(self._method('getLabel')) - async def isOn(self) -> bool: - return boolean(await self._send('isOn')) + def isOn(self) -> bool: + return boolean(self._method('isOn')) class CCComputer(BasePeripheral, ComputerMixin): @@ -109,48 +105,45 @@ class ModemMessage: class ModemMixin: - async def isOpen(self, channel: int) -> bool: - return boolean(await self._send('isOpen', channel)) + def isOpen(self, channel: int) -> bool: + return boolean(self._method('isOpen', channel)) - async def open(self, channel: int): - return nil(await self._send('open', channel)) + def open(self, channel: int): + return nil(self._method('open', channel)) - async def close(self, channel: int): - return nil(await self._send('close', channel)) + def close(self, channel: int): + return nil(self._method('close', channel)) - async def closeAll(self): - return nil(await self._send('closeAll')) + def closeAll(self): + return nil(self._method('closeAll')) - async def transmit(self, channel: int, replyChannel: int, message: Any): - return nil(await self._send('transmit', channel, replyChannel, message)) + def transmit(self, channel: int, replyChannel: int, message: Any): + return nil(self._method('transmit', channel, replyChannel, message)) - async def isWireless(self) -> bool: - return boolean(await self._send('isWireless')) + def isWireless(self) -> bool: + return boolean(self._method('isWireless')) @property def _side(self): return self._prepend_params[0] - def _mk_recv_filter(self, channel): - def filter(msg): - if msg[0] != self._side: - return False, None - if msg[1] != channel: - return False, None - return True, ModemMessage(*msg[2:]) - return filter + def receive(self, channel: int): + from .os import pullEvent - @asynccontextmanager - async def receive(self, channel: int): - if await self.isOpen(channel): + if self.isOpen(channel): raise Exception('Channel is busy') - await self.open(channel) + + self.open(channel) try: - async with self._cc.os.captureEvent('modem_message') as q: - q.filter = self._mk_recv_filter(channel) - yield q + while True: + evt = pullEvent('modem_message') + if evt[0] != self._side: + continue + if evt[1] != channel: + continue + yield ModemMessage(*evt[2:]) finally: - await self.close(channel) + self.close(channel) class CCWirelessModem(BasePeripheral, ModemMixin): @@ -158,28 +151,27 @@ class CCWirelessModem(BasePeripheral, ModemMixin): class CCWiredModem(BasePeripheral, ModemMixin): - async def getNameLocal(self) -> Optional[str]: - return option_string(await self._send('getNameLocal')) + def getNameLocal(self) -> Optional[str]: + return option_string(self._method('getNameLocal')) - async def getNamesRemote(self) -> List[str]: - return array_string(await self._send('getNamesRemote')) + def getNamesRemote(self) -> List[str]: + return array_string(self._method('getNamesRemote')) - async def getTypeRemote(self, peripheralName: str) -> Optional[str]: - return option_string(await self._send('getTypeRemote', peripheralName)) + def getTypeRemote(self, peripheralName: str) -> Optional[str]: + return option_string(self._method('getTypeRemote', peripheralName)) - async def isPresentRemote(self, peripheralName: str) -> bool: - return boolean(await self._send('isPresentRemote', peripheralName)) + def isPresentRemote(self, peripheralName: str) -> bool: + return boolean(self._method('isPresentRemote', peripheralName)) - async def wrapRemote(self, peripheralName: str) -> Optional[BasePeripheral]: + def wrapRemote(self, peripheralName: str) -> Optional[BasePeripheral]: # use instead getMethodsRemote and callRemote # NOTE: you can also use peripheral.wrap(peripheralName) - ptype = await self.getTypeRemote(peripheralName) + ptype = self.getTypeRemote(peripheralName) if ptype is None: return None return TYPE_MAP[ptype]( - self._cc, self._lua_method_expr, *self._prepend_params, 'callRemote', peripheralName, ) @@ -188,36 +180,36 @@ class CCWiredModem(BasePeripheral, ModemMixin): class CCPrinter(BasePeripheral): - async def newPage(self) -> bool: - return boolean(await self._send('newPage')) + def newPage(self) -> bool: + return boolean(self._method('newPage')) - async def endPage(self) -> bool: - return boolean(await self._send('endPage')) + def endPage(self) -> bool: + return boolean(self._method('endPage')) - async def write(self, text: str): - return nil(await self._send('write', text)) + def write(self, text: str): + return nil(self._method('write', text)) - async def setCursorPos(self, x: int, y: int): - return nil(await self._send('setCursorPos', x, y)) + def setCursorPos(self, x: int, y: int): + return nil(self._method('setCursorPos', x, y)) - async def getCursorPos(self) -> Tuple[int, int]: - return tuple2_integer(await self._send('getCursorPos')) + def getCursorPos(self) -> Tuple[int, int]: + return tuple2_integer(self._method('getCursorPos')) - async def getPageSize(self) -> Tuple[int, int]: - return tuple2_integer(await self._send('getPageSize')) + def getPageSize(self) -> Tuple[int, int]: + return tuple2_integer(self._method('getPageSize')) - async def setPageTitle(self, title: str): - return nil(await self._send('setPageTitle', title)) + def setPageTitle(self, title: str): + return nil(self._method('setPageTitle', title)) - async def getPaperLevel(self) -> int: - return integer(await self._send('getPaperLevel')) + def getPaperLevel(self) -> int: + return integer(self._method('getPaperLevel')) - async def getInkLevel(self) -> int: - return integer(await self._send('getInkLevel')) + def getInkLevel(self) -> int: + return integer(self._method('getInkLevel')) class CCSpeaker(BasePeripheral): - async def playNote(self, instrument: str, volume: int = 1, pitch: int = 1) -> bool: + def playNote(self, instrument: str, volume: int = 1, pitch: int = 1) -> bool: # instrument: # https://minecraft.gamepedia.com/Note_Block#Instruments # bass @@ -238,28 +230,28 @@ class CCSpeaker(BasePeripheral): # volume 0..3 # pitch 0..24 - return boolean(await self._send('playNote', instrument, volume, pitch)) + return boolean(self._method('playNote', instrument, volume, pitch)) - async def playSound(self, sound: str, volume: int = 1, pitch: int = 1): + def playSound(self, sound: str, volume: int = 1, pitch: int = 1): # volume 0..3 # pitch 0..2 - return boolean(await self._send('playSound', sound, volume, pitch)) + return boolean(self._method('playSound', sound, volume, pitch)) class CCCommandBlock(BasePeripheral): - async def getCommand(self) -> str: - return string(await self._send('getCommand')) + def getCommand(self) -> str: + return string(self._method('getCommand')) - async def setCommand(self, command: str): - return nil(await self._send('setCommand', command)) + def setCommand(self, command: str): + return nil(self._method('setCommand', command)) - async def runCommand(self): - return try_result(await self._send('runCommand')) + def runCommand(self): + return try_result(self._method('runCommand')) class CCWorkbench(BasePeripheral): - async def craft(self, quantity: int = 64) -> bool: - return craft_result(await self._send('craft', quantity)) + def craft(self, quantity: int = 64) -> bool: + return craft_result(self._method('craft', quantity)) TYPE_MAP = { @@ -274,34 +266,48 @@ TYPE_MAP = { } -class PeripheralAPI(BaseSubAPI): - async def isPresent(self, side: str) -> bool: - return boolean(await self._send('isPresent', side)) +method = eval_lua_method_factory('peripheral.') - async def getType(self, side: str) -> Optional[str]: - return option_string(await self._send('getType', side)) - async def getNames(self) -> List[str]: - return array_string(await self._send('getNames')) +__all__ = ( + 'isPresent', + 'getType', + 'getNames', + 'wrap', + 'get_term_target', +) - # use instead getMethods and call - async def wrap(self, side: str) -> Optional[BasePeripheral]: - ptype = await self.getType(side) - if ptype is None: - return None - m = self.get_expr_code() + '.call' +def isPresent(side: str) -> bool: + return boolean(method('isPresent', side)) - if ptype == 'modem': - if boolean(await self._send('call', side, 'isWireless')): - return CCWirelessModem(self._cc, m, side) - else: - return CCWiredModem(self._cc, m, side) + +def getType(side: str) -> Optional[str]: + return option_string(method('getType', side)) + + +def getNames() -> List[str]: + return array_string(method('getNames')) + + +# use instead getMethods and call +def wrap(side: str) -> Optional[BasePeripheral]: + ptype = getType(side) + if ptype is None: + return None + + m = 'peripheral.call' + + if ptype == 'modem': + if boolean(method('call', side, 'isWireless')): + return CCWirelessModem(m, side) else: - return TYPE_MAP[ptype](self._cc, m, side) + return CCWiredModem(m, side) + else: + return TYPE_MAP[ptype](m, side) - def get_term_target(self, side: str) -> TermTarget: - return TermTarget('{}.wrap({})'.format( - self.get_expr_code(), - lua_args(side), - )) + +def get_term_target(side: str) -> TermTarget: + return TermTarget('peripheral.wrap({})'.format( + lua_args(side), + )) diff --git a/computercraft/subapis/pocket.py b/computercraft/subapis/pocket.py index ede7a26..ddcb599 100644 --- a/computercraft/subapis/pocket.py +++ b/computercraft/subapis/pocket.py @@ -1,10 +1,19 @@ -from .base import BaseSubAPI from ..rproc import flat_try_result +from ..sess import eval_lua_method_factory -class PocketAPI(BaseSubAPI): - async def equipBack(self): - return flat_try_result(await self._send('equipBack')) +method = eval_lua_method_factory('pocket.') - async def unequipBack(self) -> bool: - return flat_try_result(await self._send('unequipBack')) + +__all__ = ( + 'equipBack', + 'unequipBack', +) + + +def equipBack(): + return flat_try_result(method('equipBack')) + + +def unequipBack(): + return flat_try_result(method('unequipBack')) diff --git a/computercraft/subapis/rednet.py b/computercraft/subapis/rednet.py index d23d7be..6230416 100644 --- a/computercraft/subapis/rednet.py +++ b/computercraft/subapis/rednet.py @@ -1,8 +1,8 @@ from typing import Any, List, Optional, Tuple, Union -from .base import BaseSubAPI from ..lua import LuaNum from ..rproc import nil, integer, option_string, boolean, array_integer, option_integer, fact_option, fact_tuple +from ..sess import eval_lua_method_factory recv_result = fact_option(fact_tuple( @@ -11,45 +11,69 @@ recv_result = fact_option(fact_tuple( option_string, tail_nils=1, )) +method = eval_lua_method_factory('rednet.') -class RednetAPI(BaseSubAPI): - CHANNEL_REPEAT = 65533 - CHANNEL_BROADCAST = 65535 +__all__ = ( + 'CHANNEL_REPEAT', + 'CHANNEL_BROADCAST', + 'open', + 'close', + 'send', + 'broadcast', + 'receive', + 'isOpen', + 'host', + 'unhost', + 'lookup', +) - async def open(self, side: str): - return nil(await self._send('open', side)) - async def close(self, side: str = None): - return nil(await self._send('close', side)) +CHANNEL_REPEAT = 65533 +CHANNEL_BROADCAST = 65535 - async def send(self, receiverID: int, message: Any, protocol: str = None) -> bool: - return boolean(await self._send('send', receiverID, message, protocol)) - async def broadcast(self, message: Any, protocol: str = None): - return nil(await self._send('broadcast', message, protocol)) +def open(side: str): + return nil(method('open', side)) - async def receive( - self, protocolFilter: str = None, timeout: LuaNum = None, - ) -> Optional[Tuple[int, Any, Optional[str]]]: - return recv_result(await self._send('receive', protocolFilter, timeout)) - async def isOpen(self, side: str = None) -> bool: - return boolean(await self._send('isOpen', side)) +def close(side: str = None): + return nil(method('close', side)) - async def host(self, protocol: str, hostname: str): - return nil(await self._send('host', protocol, hostname)) - async def unhost(self, protocol: str): - return nil(await self._send('unhost', protocol)) +def send(receiverID: int, message: Any, protocol: str = None) -> bool: + return boolean(method('send', receiverID, message, protocol)) - async def lookup(self, protocol: str, hostname: str = None) -> Union[Optional[int], List[int]]: - result = await self._send('lookup', protocol, hostname) - if hostname is None: - if result is None: - return [] - if isinstance(result, list): - return array_integer(result) - return [integer(result)] - else: - return option_integer(result) + +def broadcast(message: Any, protocol: str = None): + return nil(method('broadcast', message, protocol)) + + +def receive( + self, protocolFilter: str = None, timeout: LuaNum = None, +) -> Optional[Tuple[int, Any, Optional[str]]]: + return recv_result(method('receive', protocolFilter, timeout)) + + +def isOpen(side: str = None) -> bool: + return boolean(method('isOpen', side)) + + +def host(protocol: str, hostname: str): + return nil(method('host', protocol, hostname)) + + +def unhost(protocol: str): + return nil(method('unhost', protocol)) + + +def lookup(protocol: str, hostname: str = None) -> Union[Optional[int], List[int]]: + result = method('lookup', protocol, hostname) + if hostname is None: + if result is None: + return [] + if isinstance(result, list): + return array_integer(result) + return [integer(result)] + else: + return option_integer(result) diff --git a/computercraft/subapis/redstone.py b/computercraft/subapis/redstone.py index 386a9d6..27c2850 100644 --- a/computercraft/subapis/redstone.py +++ b/computercraft/subapis/redstone.py @@ -1,41 +1,68 @@ from typing import List -from .base import BaseSubAPI from ..rproc import boolean, nil, integer, array_string +from ..sess import eval_lua_method_factory -class RedstoneAPI(BaseSubAPI): - async def getSides(self) -> List[str]: - return array_string(await self._send('getSides')) +method = eval_lua_method_factory('redstone.') - async def getInput(self, side: str) -> bool: - return boolean(await self._send('getInput', side)) - async def setOutput(self, side: str, value: bool): - return nil(await self._send('setOutput', side, value)) +__all__ = ( + 'getSides', + 'getInput', + 'setOutput', + 'getOutput', + 'getAnalogInput', + 'setAnalogOutput', + 'getAnalogOutput', + 'getBundledInput', + 'setBundledOutput', + 'getBundledOutput', + 'testBundledInput', +) - async def getOutput(self, side: str) -> bool: - return boolean(await self._send('getOutput', side)) - async def getAnalogInput(self, side: str) -> int: - return integer(await self._send('getAnalogInput', side)) +def getSides(self) -> List[str]: + return array_string(method('getSides')) - async def setAnalogOutput(self, side: str, strength: int): - return nil(await self._send('setAnalogOutput', side, strength)) - async def getAnalogOutput(self, side: str) -> int: - return integer(await self._send('getAnalogOutput', side)) +def getInput(side: str) -> bool: + return boolean(method('getInput', side)) - # bundled cables are not available in vanilla - async def getBundledInput(self, side: str) -> int: - return integer(await self._send('getBundledInput', side)) +def setOutput(side: str, value: bool): + return nil(method('setOutput', side, value)) - async def setBundledOutput(self, side: str, colors: int): - return nil(await self._send('setBundledOutput', side, colors)) - async def getBundledOutput(self, side: str) -> int: - return integer(await self._send('getBundledOutput', side)) +def getOutput(side: str) -> bool: + return boolean(method('getOutput', side)) - async def testBundledInput(self, side: str, color: int) -> bool: - return boolean(await self._send('testBundledInput', side, color)) + +def getAnalogInput(side: str) -> int: + return integer(method('getAnalogInput', side)) + + +def setAnalogOutput(side: str, strength: int): + return nil(method('setAnalogOutput', side, strength)) + + +def getAnalogOutput(side: str) -> int: + return integer(method('getAnalogOutput', side)) + + +# bundled cables are not available in vanilla + +def getBundledInput(side: str) -> int: + return integer(method('getBundledInput', side)) + + +def setBundledOutput(side: str, colors: int): + return nil(method('setBundledOutput', side, colors)) + + +def getBundledOutput(side: str) -> int: + return integer(method('getBundledOutput', side)) + + +def testBundledInput(side: str, color: int) -> bool: + return boolean(method('testBundledInput', side, color)) diff --git a/computercraft/subapis/root.py b/computercraft/subapis/root.py deleted file mode 100644 index a9e5eef..0000000 --- a/computercraft/subapis/root.py +++ /dev/null @@ -1,22 +0,0 @@ -from ..lua import lua_args -from ..rproc import nil, boolean, string - - -class RootAPIMixin: - async def print(self, *args): - return nil(await self.eval_coro('print({})'.format(lua_args(*args)))) - - async def read_line(self) -> str: - return string(await self.eval_coro('return io.read()')) - - async def has_commands_api(self) -> bool: - return boolean(await self.eval_coro('return commands ~= nil')) - - async def has_multishell_api(self) -> bool: - return boolean(await self.eval_coro('return multishell ~= nil')) - - async def has_turtle_api(self) -> bool: - return boolean(await self.eval_coro('return turtle ~= nil')) - - async def is_pocket(self) -> bool: - return boolean(await self.eval_coro('return pocket ~= nil')) diff --git a/computercraft/subapis/settings.py b/computercraft/subapis/settings.py index c3e7d86..6610514 100644 --- a/computercraft/subapis/settings.py +++ b/computercraft/subapis/settings.py @@ -1,7 +1,7 @@ from typing import Any, List -from .base import BaseSubAPI from ..rproc import nil, boolean, string, array_string, fact_scheme_dict +from ..sess import eval_lua_method_factory setting = fact_scheme_dict({ @@ -12,42 +12,65 @@ setting = fact_scheme_dict({ 'type': string, 'value': lambda v: v, }) +method = eval_lua_method_factory('settings.') -class SettingsAPI(BaseSubAPI): - async def define(self, name: str, description: str = None, default: Any = None, type: str = None): - options = {} - if description is not None: - options['description'] = description - if default is not None: - options['default'] = default - if type is not None: - options['type'] = type - return nil(await self._send('define', name, options)) +__all__ = ( + 'define', + 'undefine', + 'getDetails', + 'set', + 'get', + 'unset', + 'clear', + 'getNames', + 'load', + 'save', +) - async def undefine(self, name: str): - return nil(await self._send('undefine', name)) - async def getDetails(self, name: str) -> dict: - return setting(await self._send('getDetails', name)) +def define(name: str, description: str = None, default: Any = None, type: str = None): + options = {} + if description is not None: + options['description'] = description + if default is not None: + options['default'] = default + if type is not None: + options['type'] = type + return nil(method('define', name, options)) - async def set(self, name: str, value: Any): - return nil(await self._send('set', name, value)) - async def get(self, name: str, default: Any = None) -> Any: - return await self._send('get', name, default) +def undefine(name: str): + return nil(method('undefine', name)) - async def unset(self, name: str): - return nil(await self._send('unset', name)) - async def clear(self): - return nil(await self._send('clear')) +def getDetails(name: str) -> dict: + return setting(method('getDetails', name)) - async def getNames(self) -> List[str]: - return array_string(await self._send('getNames')) - async def load(self, path: str = None) -> bool: - return boolean(await self._send('load', path)) +def set(name: str, value: Any): + return nil(method('set', name, value)) - async def save(self, path: str = None) -> bool: - return boolean(await self._send('save', path)) + +def get(name: str, default: Any = None) -> Any: + return method('get', name, default) + + +def unset(name: str): + return nil(method('unset', name)) + + +def clear(): + return nil(method('clear')) + + +def getNames() -> List[str]: + return array_string(method('getNames')) + + +def load(path: str = None) -> bool: + return boolean(method('load', path)) + + +def save(path: str = None) -> bool: + return boolean(method('save', path)) diff --git a/computercraft/subapis/shell.py b/computercraft/subapis/shell.py index af33d46..f71f9c7 100644 --- a/computercraft/subapis/shell.py +++ b/computercraft/subapis/shell.py @@ -1,72 +1,112 @@ from typing import List, Dict, Optional -from .base import BaseSubAPI from ..rproc import nil, string, boolean, integer, array_string, fact_mono_dict, option_string +from ..sess import eval_lua_method_factory map_string_string = fact_mono_dict(string, string) +method = eval_lua_method_factory('shell.') -class ShellAPI(BaseSubAPI): - async def exit(self): - return nil(await self._send('exit')) +__all__ = ( + 'exit', + 'dir', + 'setDir', + 'path', + 'setPath', + 'resolve', + 'resolveProgram', + 'aliases', + 'setAlias', + 'clearAlias', + 'programs', + 'getRunningProgram', + 'run', + 'execute', + 'openTab', + 'switchTab', + 'complete', + 'completeProgram', +) - async def dir(self) -> str: - return string(await self._send('dir')) - async def setDir(self, path: str): - return nil(await self._send('setDir', path)) +def exit(self): + return nil(method('exit')) - async def path(self) -> str: - return string(await self._send('path')) - async def setPath(self, path: str): - return nil(await self._send('setPath', path)) +def dir(self) -> str: + return string(method('dir')) - async def resolve(self, localPath: str) -> str: - return string(await self._send('resolve', localPath)) - async def resolveProgram(self, name: str) -> Optional[str]: - return option_string(await self._send('resolveProgram', name)) +def setDir(path: str): + return nil(method('setDir', path)) - async def aliases(self) -> Dict[str, str]: - return map_string_string(await self._send('aliases')) - async def setAlias(self, alias: str, program: str): - return nil(await self._send('setAlias', alias, program)) +def path(self) -> str: + return string(method('path')) - async def clearAlias(self, alias: str): - return nil(await self._send('clearAlias', alias)) - async def programs(self, showHidden: bool = None) -> List[str]: - return array_string(await self._send('programs', showHidden)) +def setPath(path: str): + return nil(method('setPath', path)) - async def getRunningProgram(self) -> str: - return string(await self._send('getRunningProgram')) - async def run(self, command: str, *args: str) -> bool: - return boolean(await self._send('run', command, *args)) +def resolve(localPath: str) -> str: + return string(method('resolve', localPath)) - async def execute(self, command: str, *args: str) -> bool: - return boolean(await self._send('execute', command, *args)) - async def openTab(self, command: str, *args: str) -> int: - return integer(await self._send('openTab', command, *args)) +def resolveProgram(name: str) -> Optional[str]: + return option_string(method('resolveProgram', name)) - async def switchTab(self, tabID: int): - return nil(await self._send('switchTab', tabID)) - async def complete(self, prefix: str) -> List[str]: - return array_string(await self._send('complete', prefix)) +def aliases(self) -> Dict[str, str]: + return map_string_string(method('aliases')) - async def completeProgram(self, prefix: str) -> List[str]: - return array_string(await self._send('completeProgram', prefix)) - # these functions won't be implemented - # it's far better to keep this in lua code +def setAlias(alias: str, program: str): + return nil(method('setAlias', alias, program)) - # setCompletionFunction - # getCompletionInfo - # we can create callbacks to python code, but this will require - # connection to python, and will break the shell if python disconnects +def clearAlias(alias: str): + return nil(method('clearAlias', alias)) + + +def programs(showHidden: bool = None) -> List[str]: + return array_string(method('programs', showHidden)) + + +def getRunningProgram(self) -> str: + return string(method('getRunningProgram')) + + +def run(command: str, *args: str) -> bool: + return boolean(method('run', command, *args)) + + +def execute(command: str, *args: str) -> bool: + return boolean(method('execute', command, *args)) + + +def openTab(command: str, *args: str) -> int: + return integer(method('openTab', command, *args)) + + +def switchTab(tabID: int): + return nil(method('switchTab', tabID)) + + +def complete(prefix: str) -> List[str]: + return array_string(method('complete', prefix)) + + +def completeProgram(prefix: str) -> List[str]: + return array_string(method('completeProgram', prefix)) + +# TODO: ? +# these functions won't be implemented +# it's far better to keep this in lua code + +# setCompletionFunction +# getCompletionInfo + +# we can create callbacks to python code, but this will require +# connection to python, and will break the shell if python disconnects diff --git a/computercraft/subapis/term.py b/computercraft/subapis/term.py index 5fbf27f..8874889 100644 --- a/computercraft/subapis/term.py +++ b/computercraft/subapis/term.py @@ -1,30 +1,75 @@ -from contextlib import asynccontextmanager +from contextlib import contextmanager from typing import Tuple from .base import BaseSubAPI from .mixins import TermMixin, TermTarget -from ..lua import lua_args +from ..lua import lua_call from ..rproc import tuple3_number +from ..sess import eval_lua_method_factory, lua_context_object class TermAPI(BaseSubAPI, TermMixin): - async def nativePaletteColor(self, colorID: int) -> Tuple[float, float, float]: - return tuple3_number(await self._send('nativePaletteColor', colorID)) + pass - @asynccontextmanager - async def redirect(self, target: TermTarget): - create_expr = '{}.redirect({})'.format( - self.get_expr_code(), - lua_args(target), - ) - fin_tpl = '{}.redirect({{e}})'.format( - self.get_expr_code(), - ) - async with self._cc._create_temp_object(create_expr, fin_tpl): - yield - def get_current_target(self) -> TermTarget: - return TermTarget('{}.current()'.format(self.get_expr_code())) +method = eval_lua_method_factory('term.') +tapi = TermAPI('term') - def get_native_target(self) -> TermTarget: - return TermTarget('{}.native()'.format(self.get_expr_code())) + +__all__ = ( + 'write', + 'blit', + 'clear', + 'clearLine', + 'getCursorPos', + 'setCursorPos', + 'getCursorBlink', + 'isColor', + 'getSize', + 'scroll', + 'setTextColor', + 'getTextColor', + 'setBackgroundColor', + 'getBackgroundColor', + 'getPaletteColor', + 'setPaletteColor', +) + + +write = tapi.write +blit = tapi.blit +clear = tapi.clear +clearLine = tapi.clearLine +getCursorPos = tapi.getCursorPos +setCursorPos = tapi.setCursorPos +getCursorBlink = tapi.getCursorBlink +isColor = tapi.isColor +getSize = tapi.getSize +scroll = tapi.scroll +setTextColor = tapi.setTextColor +getTextColor = tapi.getTextColor +setBackgroundColor = tapi.setBackgroundColor +getBackgroundColor = tapi.getBackgroundColor +getPaletteColor = tapi.getPaletteColor +setPaletteColor = tapi.setPaletteColor + + +def nativePaletteColor(colorID: int) -> Tuple[float, float, float]: + return tuple3_number(method('nativePaletteColor', colorID)) + + +@contextmanager +def redirect(target: TermTarget): + with lua_context_object( + lua_call('term.redirect', target), + 'term.redirect({e})' + ): + yield + + +def get_current_target() -> TermTarget: + return TermTarget('term.current()') + + +def get_native_target() -> TermTarget: + return TermTarget('term.native()') diff --git a/computercraft/subapis/textutils.py b/computercraft/subapis/textutils.py index 8235d7b..8513764 100644 --- a/computercraft/subapis/textutils.py +++ b/computercraft/subapis/textutils.py @@ -1,39 +1,59 @@ from typing import List, Union -from .base import BaseSubAPI from ..lua import LuaNum from ..rproc import nil, string, integer +from ..sess import eval_lua_method_factory -class TextutilsAPI(BaseSubAPI): - async def slowWrite(self, text: str, rate: LuaNum = None): - return nil(await self._send('slowWrite', text, rate)) +method = eval_lua_method_factory('textutils.') - async def slowPrint(self, text: str, rate: LuaNum = None): - return nil(await self._send('slowPrint', text, rate)) - async def formatTime(self, time: LuaNum, twentyFourHour: bool = None) -> str: - return string(await self._send('formatTime', time, twentyFourHour)) +__all__ = ( + 'slowWrite', + 'slowPrint', + 'formatTime', + 'tabulate', + 'pagedTabulate', + 'pagedPrint', + 'complete', +) - async def tabulate(self, *rows_and_colors: Union[list, int]): - return nil(await self._send('tabulate', *rows_and_colors)) - async def pagedTabulate(self, *rows_and_colors: Union[list, int]): - return nil(await self._send('pagedTabulate', *rows_and_colors)) +def slowWrite(text: str, rate: LuaNum = None): + return nil(method('slowWrite', text, rate)) - async def pagedPrint(self, text: str, freeLines: int = None) -> int: - return integer(await self._send('pagedPrint', text, freeLines)) - def complete(self, partial: str, possible: List[str]) -> List[str]: - return [p[len(partial):] for p in possible if p.startswith(partial)] +def slowPrint(text: str, rate: LuaNum = None): + return nil(method('slowPrint', text, rate)) - # Questionable to implement - # serialize - # unserialize - # Will not implement, use pythonic equivalents - # serializeJSON - # unserializeJSON - # urlEncode - # json_null - # empty_json_array +def formatTime(time: LuaNum, twentyFourHour: bool = None) -> str: + return string(method('formatTime', time, twentyFourHour)) + + +def tabulate(*rows_and_colors: Union[list, int]): + return nil(method('tabulate', *rows_and_colors)) + + +def pagedTabulate(*rows_and_colors: Union[list, int]): + return nil(method('pagedTabulate', *rows_and_colors)) + + +def pagedPrint(text: str, freeLines: int = None) -> int: + return integer(method('pagedPrint', text, freeLines)) + + +def complete(partial: str, possible: List[str]) -> List[str]: + return [p[len(partial):] for p in possible if p.startswith(partial)] + + +# Questionable to implement +# serialize +# unserialize + +# Will not implement, use pythonic equivalents +# serializeJSON +# unserializeJSON +# urlEncode +# json_null +# empty_json_array diff --git a/computercraft/subapis/turtle.py b/computercraft/subapis/turtle.py index 344473a..0870979 100644 --- a/computercraft/subapis/turtle.py +++ b/computercraft/subapis/turtle.py @@ -1,10 +1,11 @@ from typing import Optional -from .base import BaseSubAPI from ..errors import LuaException from ..rproc import integer, boolean, fact_option, any_dict, flat_try_result +from ..sess import eval_lua_method_factory +method = eval_lua_method_factory('turtle.') option_any_dict = fact_option(any_dict) @@ -54,132 +55,220 @@ def always_true(r): return None -class TurtleAPI(BaseSubAPI): - async def craft(self, quantity: int = 64) -> bool: - return craft_result(await self._send('craft', quantity)) +__all__ = ( + 'craft', + 'forward', + 'back', + 'up', + 'down', + 'turnLeft', + 'turnRight', + 'select', + 'getSelectedSlot', + 'getItemCount', + 'getItemSpace', + 'getItemDetail', + 'equipLeft', + 'equipRight', + 'attack', + 'attackUp', + 'attackDown', + 'dig', + 'digUp', + 'digDown', + 'place', + 'placeUp', + 'placeDown', + 'detect', + 'detectUp', + 'detectDown', + 'inspect', + 'inspectUp', + 'inspectDown', + 'compare', + 'compareUp', + 'compareDown', + 'compareTo', + 'drop', + 'dropUp', + 'dropDown', + 'suck', + 'suckUp', + 'suckDown', + 'refuel', + 'getFuelLevel', + 'getFuelLimit', + 'transferTo', +) - async def forward(self) -> bool: - return move_result(await self._send('forward')) - async def back(self) -> bool: - return move_result(await self._send('back')) +def craft(quantity: int = 64) -> bool: + return craft_result(method('craft', quantity)) - async def up(self) -> bool: - return move_result(await self._send('up')) - async def down(self) -> bool: - return move_result(await self._send('down')) +def forward() -> bool: + return move_result(method('forward')) - async def turnLeft(self): - return always_true(await self._send('turnLeft')) - async def turnRight(self): - return always_true(await self._send('turnRight')) +def back() -> bool: + return move_result(method('back')) - async def select(self, slotNum: int): - return always_true(await self._send('select', slotNum)) - async def getSelectedSlot(self) -> int: - return integer(await self._send('getSelectedSlot')) +def up() -> bool: + return move_result(method('up')) - async def getItemCount(self, slotNum: int = None) -> int: - return integer(await self._send('getItemCount', slotNum)) - async def getItemSpace(self, slotNum: int = None) -> int: - return integer(await self._send('getItemSpace', slotNum)) +def down() -> bool: + return move_result(method('down')) - async def getItemDetail(self, slotNum: int = None) -> dict: - return option_any_dict(await self._send('getItemDetail', slotNum)) - async def equipLeft(self): - return always_true(await self._send('equipLeft')) +def turnLeft(): + return always_true(method('turnLeft')) - async def equipRight(self): - return always_true(await self._send('equipRight')) - async def attack(self) -> bool: - return attack_result(await self._send('attack')) +def turnRight(): + return always_true(method('turnRight')) - async def attackUp(self) -> bool: - return attack_result(await self._send('attackUp')) - async def attackDown(self) -> bool: - return attack_result(await self._send('attackDown')) +def select(slotNum: int): + return always_true(method('select', slotNum)) - async def dig(self) -> bool: - return dig_result(await self._send('dig')) - async def digUp(self) -> bool: - return dig_result(await self._send('digUp')) +def getSelectedSlot() -> int: + return integer(method('getSelectedSlot')) - async def digDown(self) -> bool: - return dig_result(await self._send('digDown')) - async def place(self, signText: str = None) -> bool: - return place_result(await self._send('place', signText)) +def getItemCount(slotNum: int = None) -> int: + return integer(method('getItemCount', slotNum)) - async def placeUp(self) -> bool: - return place_result(await self._send('placeUp')) - async def placeDown(self) -> bool: - return place_result(await self._send('placeDown')) +def getItemSpace(slotNum: int = None) -> int: + return integer(method('getItemSpace', slotNum)) - async def detect(self) -> bool: - return boolean(await self._send('detect')) - async def detectUp(self) -> bool: - return boolean(await self._send('detectUp')) +def getItemDetail(slotNum: int = None) -> dict: + return option_any_dict(method('getItemDetail', slotNum)) - async def detectDown(self) -> bool: - return boolean(await self._send('detectDown')) - async def inspect(self) -> Optional[dict]: - return inspect_result(await self._send('inspect')) +def equipLeft(): + return always_true(method('equipLeft')) - async def inspectUp(self) -> Optional[dict]: - return inspect_result(await self._send('inspectUp')) - async def inspectDown(self) -> Optional[dict]: - return inspect_result(await self._send('inspectDown')) +def equipRight(): + return always_true(method('equipRight')) - async def compare(self) -> bool: - return boolean(await self._send('compare')) - async def compareUp(self) -> bool: - return boolean(await self._send('compareUp')) +def attack() -> bool: + return attack_result(method('attack')) - async def compareDown(self) -> bool: - return boolean(await self._send('compareDown')) - async def compareTo(self, slot: int) -> bool: - return boolean(await self._send('compareTo', slot)) +def attackUp() -> bool: + return attack_result(method('attackUp')) - async def drop(self, count: int = None) -> bool: - return drop_result(await self._send('drop', count)) - async def dropUp(self, count: int = None) -> bool: - return drop_result(await self._send('dropUp', count)) +def attackDown() -> bool: + return attack_result(method('attackDown')) - async def dropDown(self, count: int = None) -> bool: - return drop_result(await self._send('dropDown', count)) - async def suck(self, amount: int = None) -> bool: - return suck_result(await self._send('suck', amount)) +def dig() -> bool: + return dig_result(method('dig')) - async def suckUp(self, amount: int = None) -> bool: - return suck_result(await self._send('suckUp', amount)) - async def suckDown(self, amount: int = None) -> bool: - return suck_result(await self._send('suckDown', amount)) +def digUp() -> bool: + return dig_result(method('digUp')) - async def refuel(self, quantity: int = None): - return flat_try_result(await self._send('refuel', quantity)) - async def getFuelLevel(self) -> int: - return integer(await self._send('getFuelLevel')) +def digDown() -> bool: + return dig_result(method('digDown')) - async def getFuelLimit(self) -> int: - return integer(await self._send('getFuelLimit')) - async def transferTo(self, slot: int, quantity: int = None) -> bool: - return transfer_result(await self._send('transferTo', slot, quantity)) +def place(signText: str = None) -> bool: + return place_result(method('place', signText)) + + +def placeUp() -> bool: + return place_result(method('placeUp')) + + +def placeDown() -> bool: + return place_result(method('placeDown')) + + +def detect() -> bool: + return boolean(method('detect')) + + +def detectUp() -> bool: + return boolean(method('detectUp')) + + +def detectDown() -> bool: + return boolean(method('detectDown')) + + +def inspect() -> Optional[dict]: + return inspect_result(method('inspect')) + + +def inspectUp() -> Optional[dict]: + return inspect_result(method('inspectUp')) + + +def inspectDown() -> Optional[dict]: + return inspect_result(method('inspectDown')) + + +def compare() -> bool: + return boolean(method('compare')) + + +def compareUp() -> bool: + return boolean(method('compareUp')) + + +def compareDown() -> bool: + return boolean(method('compareDown')) + + +def compareTo(slot: int) -> bool: + return boolean(method('compareTo', slot)) + + +def drop(count: int = None) -> bool: + return drop_result(method('drop', count)) + + +def dropUp(count: int = None) -> bool: + return drop_result(method('dropUp', count)) + + +def dropDown(count: int = None) -> bool: + return drop_result(method('dropDown', count)) + + +def suck(amount: int = None) -> bool: + return suck_result(method('suck', amount)) + + +def suckUp(amount: int = None) -> bool: + return suck_result(method('suckUp', amount)) + + +def suckDown(amount: int = None) -> bool: + return suck_result(method('suckDown', amount)) + + +def refuel(quantity: int = None): + return flat_try_result(method('refuel', quantity)) + + +def getFuelLevel() -> int: + return integer(method('getFuelLevel')) + + +def getFuelLimit() -> int: + return integer(method('getFuelLimit')) + + +def transferTo(slot: int, quantity: int = None) -> bool: + return transfer_result(method('transferTo', slot, quantity)) diff --git a/computercraft/subapis/window.py b/computercraft/subapis/window.py index a1fbbd7..4486898 100644 --- a/computercraft/subapis/window.py +++ b/computercraft/subapis/window.py @@ -1,43 +1,49 @@ -from contextlib import asynccontextmanager +from contextlib import contextmanager from typing import Tuple -from ..lua import lua_args +from ..lua import lua_call from ..rproc import nil, tuple2_integer, tuple3_string +from ..sess import eval_lua_method_factory, lua_context_object from .base import BaseSubAPI from .mixins import TermMixin, TermTarget class CCWindow(BaseSubAPI, TermMixin): - async def setVisible(self, visibility: bool): - return nil(await self._send('setVisible', visibility)) + def setVisible(self, visibility: bool): + return nil(self._method('setVisible', visibility)) - async def redraw(self): - return nil(await self._send('redraw')) + def redraw(self): + return nil(self._method('redraw')) - async def restoreCursor(self): - return nil(await self._send('restoreCursor')) + def restoreCursor(self): + return nil(self._method('restoreCursor')) - async def getPosition(self) -> Tuple[int, int]: - return tuple2_integer(await self._send('getPosition')) + def getPosition(self) -> Tuple[int, int]: + return tuple2_integer(self._method('getPosition')) - async def reposition(self, x: int, y: int, width: int = None, height: int = None, parent: TermTarget = None): - return nil(await self._send('reposition', x, y, width, height, parent)) + def reposition(self, x: int, y: int, width: int = None, height: int = None, parent: TermTarget = None): + return nil(self._method('reposition', x, y, width, height, parent)) - async def getLine(self, y: int) -> Tuple[str, str, str]: - return tuple3_string(await self._send('getLine', y)) + def getLine(self, y: int) -> Tuple[str, str, str]: + return tuple3_string(self._method('getLine', y)) def get_term_target(self) -> TermTarget: return TermTarget(self.get_expr_code()) -class WindowAPI(BaseSubAPI): - @asynccontextmanager - async def create( - self, parentTerm: TermTarget, x: int, y: int, width: int, height: int, visible: bool = None, - ) -> CCWindow: - create_expr = '{}.create({})'.format( - self.get_expr_code(), - lua_args(parentTerm, x, y, width, height, visible), - ) - async with self._cc._create_temp_object(create_expr) as var: - yield CCWindow(self._cc, var) +method = eval_lua_method_factory('window.') + + +__all__ = ( + 'create', +) + + +@contextmanager +def create( + parentTerm: TermTarget, x: int, y: int, width: int, height: int, visible: bool = None, +) -> CCWindow: + with lua_context_object( + lua_call('window.create', parentTerm, x, y, width, height, visible), + ) as var: + yield CCWindow(var) diff --git a/examples/__init__.py b/examples/__init__.py deleted file mode 100644 index 4dfdfb8..0000000 --- a/examples/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .hello import program as hello - - -__all__ = (hello, ) diff --git a/examples/_lib.py b/examples/_lib.py new file mode 100644 index 0000000..a722fb3 --- /dev/null +++ b/examples/_lib.py @@ -0,0 +1,135 @@ +from contextlib import contextmanager +from time import monotonic +from types import FunctionType + +from cc import eval_lua + + +@contextmanager +def assert_raises(etype, message=None): + try: + yield + except Exception as e: + assert isinstance(e, etype), repr(e) + if message is not None: + assert e.args == (message, ) + else: + raise AssertionError(f'Exception of type {etype} was not raised') + + +@contextmanager +def assert_takes_time(at_least, at_most): + t = monotonic() + yield + dt = monotonic() - t + # print(at_least, '<=', dt, '<=', at_most) + assert at_least <= dt <= at_most + + +class AnyInstanceOf: + def __init__(self, cls): + self.c = cls + + def __eq__(self, other): + return isinstance(other, self.c) + + +def step(text): + input(f'{text} [enter]') + + +def get_object_table(objname): + r = eval_lua(f""" +local r = {{}} +for k in pairs({objname}) do + local t = type({objname}[k]) + if r[t] == nil then r[t] = {{}} end + if t == 'number' or t == 'boolean' or t == 'string' then + r[t][k] = {objname}[k] + else + r[t][k] = true + end +end +return r""", immediate=True) + assert len(r) == 1 + return r[0] + + +def get_class_table(cls): + items = { + k: v for k, v in vars(cls).items() + if not k.startswith('_') + } + nums = { + k: v for k, v in items.items() + if isinstance(v, (int, float)) + } + methods = { + k: True for k, v in items.items() + if isinstance(v, FunctionType) + } + r = {} + if nums: + r['number'] = nums + if methods: + r['function'] = methods + return r + + +def get_multiclass_table(*cls): + result = {} + for c in cls: + for k, v in get_class_table(c).items(): + result.setdefault(k, {}).update(v) + return result + + +def term_step(text): + from cc import colors, term + + for color in colors.iter_colors(): + r, g, b = term.nativePaletteColor(color) + term.setPaletteColor(color, r, g, b) + term.setBackgroundColor(colors.black) + term.setTextColor(colors.white) + term.clear() + term.setCursorPos(1, 1) + term.setCursorBlink(True) + step(text) + + +def _computer_peri(place_thing, thing): + from cc import peripheral + + side = 'left' + + step( + f'Place {place_thing} on {side} side of computer\n' + "Don't turn it on!", + ) + + c = peripheral.wrap(side) + assert c is not None + + from computercraft.subapis.peripheral import ComputerMixin + tbl = get_object_table(f'peripheral.wrap("{side}")') + assert get_class_table(ComputerMixin) == tbl + + assert c.isOn() is False + assert isinstance(c.getID(), int) + assert c.getLabel() is None + assert c.turnOn() is None + + step(f'{thing.capitalize()} must be turned on now') + + assert c.shutdown() is None + + step(f'{thing.capitalize()} must shutdown') + + step(f'Now turn on {thing} manually and enter some commands') + + assert c.reboot() is None + + step(f'{thing.capitalize()} must reboot') + + print('Test finished successfully') diff --git a/examples/hello.py b/examples/hello.py index 7c686cb..60f08aa 100644 --- a/examples/hello.py +++ b/examples/hello.py @@ -1,2 +1 @@ -async def program(api): - await api.print('Hello world!') +print('Hello world!') diff --git a/examples/id.py b/examples/id.py new file mode 100644 index 0000000..b1924c1 --- /dev/null +++ b/examples/id.py @@ -0,0 +1,5 @@ +from cc import os + +print('ID', os.getComputerID()) +print('Label', os.getComputerLabel()) +print('Version', os.version()) diff --git a/examples/modem_server.py b/examples/modem_server.py new file mode 100644 index 0000000..e732f0e --- /dev/null +++ b/examples/modem_server.py @@ -0,0 +1,12 @@ +from cc import peripheral + + +side = 'back' +m = peripheral.wrap(side) +listen_channel = 5 +for msg in m.receive(listen_channel): + print(repr(msg)) + if msg.content == 'stop': + break + else: + m.transmit(msg.reply_channel, listen_channel, msg.content) diff --git a/examples/move.py b/examples/move.py new file mode 100644 index 0000000..4f6cf3a --- /dev/null +++ b/examples/move.py @@ -0,0 +1,9 @@ +from cc import is_turtle, turtle + +if not is_turtle(): + print('Turtle required!') + exit() + +for _ in range(4): + turtle.forward() + turtle.turnLeft() diff --git a/examples/read.py b/examples/read.py new file mode 100644 index 0000000..ebe209b --- /dev/null +++ b/examples/read.py @@ -0,0 +1,2 @@ +line = input() +print(f'Entered line: {line}') diff --git a/examples/test_colors.py b/examples/test_colors.py new file mode 100644 index 0000000..8367281 --- /dev/null +++ b/examples/test_colors.py @@ -0,0 +1,38 @@ +from cc import import_file, colors + +_lib = import_file('_lib.py', __file__) + + +tbl = _lib.get_object_table('colors') + +# use packRGB and unpackRGB +del tbl['function']['rgb8'] + +tbl['function']['iter_colors'] = True + +assert _lib.get_class_table(colors) == tbl + +cs = colors.combine( + colors.orange, + colors.cyan, + colors.pink, + colors.brown, +) +assert isinstance(cs, int) +cs = colors.subtract(cs, colors.brown, colors.green) +assert isinstance(cs, int) +assert cs == colors.combine( + colors.orange, + colors.cyan, + colors.pink, +) +assert colors.test(cs, colors.red) is False +assert colors.test(cs, colors.cyan) is True + +assert colors.packRGB(0.7, 0.2, 0.6) == 0xb23399 +r, g, b = colors.unpackRGB(0xb23399) +assert 0.68 < r < 0.72 +assert 0.18 < g < 0.22 +assert 0.58 < b < 0.62 + +print('Test finished successfully') diff --git a/examples/test_commands.py b/examples/test_commands.py new file mode 100644 index 0000000..c4a83d2 --- /dev/null +++ b/examples/test_commands.py @@ -0,0 +1,55 @@ +from cc import import_file, commands + +_lib = import_file('_lib.py', __file__) +AnyInstanceOf = _lib.AnyInstanceOf + + +tbl = _lib.get_object_table('commands.native') +# remove in favor of exec +del tbl['function']['execAsync'] +assert _lib.get_class_table(commands) == tbl + +xyz = commands.getBlockPosition() + +assert len(xyz) == 3 +for c in xyz: + assert isinstance(c, int) + +expected_binfo = { + 'state': { + 'state': AnyInstanceOf(str), + 'facing': AnyInstanceOf(str), + }, + 'name': 'computercraft:computer_command', + 'nbt': { + 'x': xyz[0], + 'y': xyz[1], + 'z': xyz[2], + 'ComputerId': AnyInstanceOf(int), + 'id': 'computercraft:computer_command', + 'On': 1, + }, + 'tags': {}, +} + +assert commands.getBlockInfo(*xyz) == expected_binfo +assert commands.getBlockInfos(*xyz, *xyz) == [expected_binfo] + +cmdlist = commands.list() + +assert len(cmdlist) > 0 +for c in cmdlist: + assert isinstance(c, str) + +assert commands.exec('say Hello!') == (True, [], AnyInstanceOf(int)) + +d = commands.exec('tp hajejndlasksdkelefsns fjeklaskslekffjslas') +assert d[0] is False + +d = commands.exec('difficulty') +assert d[0] is True +assert len(d[1]) == 1 +assert d[1][0].startswith('The difficulty is ') +assert isinstance(d[2], int) + +print('Test finished successfully') diff --git a/examples/test_disk.py b/examples/test_disk.py new file mode 100644 index 0000000..66aa5be --- /dev/null +++ b/examples/test_disk.py @@ -0,0 +1,86 @@ +from cc import LuaException, import_file, disk + +_lib = import_file('_lib.py', __file__) +step, assert_raises = _lib.step, _lib.assert_raises + + +s = 'right' + +assert _lib.get_class_table(disk) == _lib.get_object_table('disk') + +step(f'Make sure there is no disk drive at {s} side') + +assert disk.isPresent(s) is False +assert disk.hasData(s) is False +assert disk.getMountPath(s) is None +assert disk.setLabel(s, 'text') is None +assert disk.getLabel(s) is None +assert disk.getID(s) is None +assert disk.hasAudio(s) is False +assert disk.getAudioTitle(s) is None +assert disk.playAudio(s) is None +assert disk.stopAudio(s) is None +assert disk.eject(s) is None + +step(f'Place empty disk drive at {s} side') + +assert disk.isPresent(s) is False +assert disk.hasData(s) is False +assert disk.getMountPath(s) is None +assert disk.setLabel(s, 'text') is None +assert disk.getLabel(s) is None +assert disk.getID(s) is None +assert disk.hasAudio(s) is False +assert disk.getAudioTitle(s) is False # False instead None! +assert disk.playAudio(s) is None +assert disk.stopAudio(s) is None +assert disk.eject(s) is None + +step('Put new CC diskette into disk drive') + +assert disk.isPresent(s) is True +assert disk.hasData(s) is True +assert isinstance(disk.getMountPath(s), str) +assert isinstance(disk.getID(s), int) + +assert disk.getLabel(s) is None +assert disk.setLabel(s, 'label') is None +assert disk.getLabel(s) == 'label' +assert disk.setLabel(s, None) is None +assert disk.getLabel(s) is None + +assert disk.hasAudio(s) is False +assert disk.getAudioTitle(s) is None +assert disk.playAudio(s) is None +assert disk.stopAudio(s) is None + +assert disk.eject(s) is None + +step('Put any audio disk into disk drive') + +assert disk.isPresent(s) is True +assert disk.hasData(s) is False +assert disk.getMountPath(s) is None +assert disk.getID(s) is None +assert disk.hasAudio(s) is True + +label = disk.getAudioTitle(s) +assert isinstance(label, str) +assert label != 'label' +print(f'Label is {label}') +assert disk.getLabel(s) == label +with assert_raises(LuaException): + assert disk.setLabel(s, 'label') is None +with assert_raises(LuaException): + assert disk.setLabel(s, None) is None +# no effect +assert disk.getLabel(s) == label + +assert disk.playAudio(s) is None + +step('Audio must be playing now') + +assert disk.stopAudio(s) is None +assert disk.eject(s) is None + +print('Test finished successfully') diff --git a/examples/test_fs.py b/examples/test_fs.py new file mode 100644 index 0000000..05e685d --- /dev/null +++ b/examples/test_fs.py @@ -0,0 +1,231 @@ +from cc import LuaException, import_file, fs + +_lib = import_file('_lib.py', __file__) +assert_raises, AnyInstanceOf = _lib.assert_raises, _lib.AnyInstanceOf + + +assert _lib.get_class_table(fs) == _lib.get_object_table('fs') + +for name in ('tdir', 'tfile'): + if fs.exists(name): + fs.delete(name) + +assert fs.makeDir('tdir') is None +with fs.open('tfile', 'w') as f: + f.writeLine('textline') + +dlist = set(fs.list('.')) + +assert {'tdir', 'tfile', 'rom'}.issubset(dlist) +assert fs.list('tdir') == [] + +capacity = fs.getCapacity('.') +free = fs.getFreeSpace('.') +assert isinstance(capacity, int) +assert isinstance(free, int) +assert free < capacity +assert free > 0 +assert capacity > 0 + +assert fs.exists('tdir') is True +assert fs.exists('tfile') is True +assert fs.exists('doesnotexist') is False + +assert fs.isDir('tdir') is True +assert fs.isDir('tfile') is False +assert fs.isDir('doesnotexist') is False + +assert fs.isReadOnly('rom') is True +assert fs.isReadOnly('tdir') is False +assert fs.isReadOnly('tfile') is False +assert fs.isReadOnly('doesnotexist') is False + +assert fs.getDrive('rom') == 'rom' +assert fs.getDrive('tdir') == 'hdd' +assert fs.getDrive('tfile') == 'hdd' +assert fs.getDrive('doesnotexist') is None + +assert fs.isDriveRoot('/') is True +assert fs.isDriveRoot('rom') is True +assert fs.isDriveRoot('tdir') is False +assert fs.isDriveRoot('tfile') is False +assert fs.isDriveRoot('doesnotexist') is True # wtf? + +assert fs.getName('a/b/c/d') == 'd' +assert fs.getName('a/b/c/') == 'c' +assert fs.getName('/a/b/c/d') == 'd' +assert fs.getName('///a/b/c/d') == 'd' +assert fs.getName('') == 'root' # wtf? +assert fs.getName('/') == 'root' +assert fs.getName('///') == 'root' +assert fs.getName('.') == 'root' +assert fs.getName('..') == '..' +assert fs.getName('../../..') == '..' + +assert fs.getDir('a/b/c/d') == 'a/b/c' +assert fs.getDir('a/b/c/') == 'a/b' +assert fs.getDir('/a/b/c/d') == 'a/b/c' +assert fs.getDir('///a/b/c/d') == 'a/b/c' +assert fs.getDir('') == '..' +assert fs.getDir('/') == '..' +assert fs.getDir('///') == '..' +assert fs.getDir('.') == '..' +assert fs.getDir('..') == '' +assert fs.getDir('../../..') == '../..' + +assert fs.combine('a', 'b') == 'a/b' +assert fs.combine('a/', 'b') == 'a/b' +assert fs.combine('a//', 'b') == 'a/b' +assert fs.combine('a/', '/b') == 'a/b' +assert fs.combine('a/b/c', '..') == 'a/b' +assert fs.combine('a/b/c', '../..') == 'a' +assert fs.combine('a/b/c', '../../..') == '' +assert fs.combine('a/b/c', '../../../..') == '..' +assert fs.combine('a/b/c', '../../../../..') == '../..' +assert fs.combine('/a/b/c', '../../../../..') == '../..' +assert fs.combine('a/b/c', '////') == 'a/b/c' +assert fs.combine('a/b/c', '.') == 'a/b/c' +assert fs.combine('a/b/c', './.') == 'a/b/c' +assert fs.combine('a/b/c', './../.') == 'a/b' + +assert fs.getSize('tfile') == 9 +assert fs.getSize('tdir') == 0 +with assert_raises(LuaException): + fs.getSize('doesnotexist') + +assert fs.move('tfile', 'tdir/apple') is None +assert fs.list('tdir') == ['apple'] +assert fs.copy('tdir/apple', 'tdir/banana') is None +assert fs.list('tdir/') == ['apple', 'banana'] +assert fs.copy('tdir/apple', 'tdir/cherry') is None + +assert fs.getSize('tdir') == 0 + +dlist = set(fs.find('*')) +assert 'tdir' in dlist +assert 'rom' in dlist +assert 'tfile' not in dlist +assert 'tdir/apple' not in dlist + +dlist = set(fs.find('tdir/*')) +assert dlist == {'tdir/apple', 'tdir/banana', 'tdir/cherry'} + +dlist = set(fs.find('tdir/*a*')) +assert dlist == {'tdir/apple', 'tdir/banana'} + +dlist = set(fs.find('**')) +assert 'tdir' in dlist +assert 'tdir/apple' not in dlist # not recursive + +dlist = set(fs.list('')) +assert 'tfile' not in dlist +assert 'tdir' in dlist +assert 'rom' in dlist + +dlist = set(fs.list('tdir')) +assert dlist == {'apple', 'banana', 'cherry'} + +assert fs.attributes('tdir/banana') == { + 'created': AnyInstanceOf(int), + 'modification': AnyInstanceOf(int), + 'isDir': False, + 'size': 9, +} +assert fs.attributes('tdir') == { + 'created': AnyInstanceOf(int), + 'modification': AnyInstanceOf(int), + 'isDir': True, + 'size': 0, +} +with assert_raises(LuaException): + fs.attributes('doesnotexist') + +assert fs.complete('ba', 'tdir') == ['nana'] +assert fs.complete('ap', 'tdir') == ['ple'] +assert fs.complete('c', 'tdir') == ['herry'] +assert fs.complete('td', '') == ['ir/', 'ir'] +assert fs.complete('td', '', includeDirs=True) == ['ir/', 'ir'] +assert fs.complete('td', '', includeDirs=False) == ['ir/'] # wtf? +assert fs.complete('ap', 'tdir', includeFiles=True) == ['ple'] +assert fs.complete('ap', 'tdir', includeFiles=False) == [] + +assert fs.getSize('tdir/banana') == 9 +with fs.open('tdir/banana', 'r') as f: + assert _lib.get_object_table(f.get_expr_code()) == {'function': { + 'close': True, + 'read': True, + 'readLine': True, + 'readAll': True, + }} + assert f.read(4) == 'text' + assert f.readLine() == 'line' + assert f.read(1) is None + assert f.readLine() is None + assert f.readAll() == '' + assert f.readAll() == '' +assert fs.getSize('tdir/banana') == 9 +with fs.open('tdir/banana', 'a') as f: + assert _lib.get_object_table(f.get_expr_code()) == {'function': { + 'close': True, + 'write': True, + 'writeLine': True, + 'flush': True, + }} + assert f.write('x') is None +assert fs.getSize('tdir/banana') == 10 +with fs.open('tdir/banana', 'w') as f: + pass +assert fs.getSize('tdir/banana') == 0 # truncate +with fs.open('tdir/banana', 'w') as f: + assert _lib.get_object_table(f.get_expr_code()) == {'function': { + 'close': True, + 'write': True, + 'writeLine': True, + 'flush': True, + }} + assert f.write('Bro') is None + assert f.writeLine('wn fox jumps') is None + assert fs.getSize('tdir/banana') == 0 # changes are not on a disk + assert f.flush() is None + assert fs.getSize('tdir/banana') == len('Brown fox jumps\n') + assert f.write('ov') is None + assert f.write('er ') is None + assert f.write('a lazy') is None + assert f.writeLine(' dog.') is None +assert fs.getSize('tdir/banana') > 9 +with fs.open('tdir/banana', 'r') as f: + assert f.readAll() == 'Brown fox jumps\nover a lazy dog.' # no newline? +with assert_raises(LuaException): + with fs.open('tdir/banana', 'rw') as f: + pass + +assert fs.exists('tdir/banana') is True + +with fs.open('tdir/binfile', 'wb') as f: + assert f.write('a' * 9) is None + assert f.seek() == 9 + assert f.seek('set', 0) == 0 + assert f.write('b' * 3) is None + assert f.seek('cur', -1) == 2 + assert f.write('c' * 3) is None + assert f.seek('end') == 9 + assert f.write('d' * 3) is None + with assert_raises(LuaException): + f.seek('set', -10) + +with fs.open('tdir/binfile', 'rb') as f: + assert f.readAll() == 'bbcccaaaaddd' + +with fs.open('tdir/binfile', 'rb') as f: + assert isinstance(f.read(), int) + +with fs.open('tdir/binfile', 'r') as f: + assert [line async for line in f] == ['bbcccaaaaddd'] + +assert fs.delete('tdir') is None +assert fs.delete('tfile') is None +assert fs.delete('doesnotexist') is None + +assert fs.exists('tdir/banana') is False + +print('Test finished successfully') diff --git a/examples/test_gps_basic_computer.py b/examples/test_gps_basic_computer.py new file mode 100644 index 0000000..5685a0d --- /dev/null +++ b/examples/test_gps_basic_computer.py @@ -0,0 +1,18 @@ +from cc import import_file, gps + +_lib = import_file('_lib.py', __file__) + + +assert _lib.get_class_table(gps) == _lib.get_object_table('gps') + +assert gps.locate() is None + +_lib.step('Attach wireless modem to computer') + +assert gps.locate() is None + +assert gps.locate(debug=True) is None + +assert gps.locate(timeout=5, debug=True) is None + +print('Test finished successfully') diff --git a/examples/test_gps_command_computer.py b/examples/test_gps_command_computer.py new file mode 100644 index 0000000..4afc10c --- /dev/null +++ b/examples/test_gps_command_computer.py @@ -0,0 +1,14 @@ +from cc import import_file, gps + +_lib = import_file('_lib.py', __file__) + + +assert _lib.get_class_table(gps) == _lib.get_object_table('gps') + +assert gps.locate() == ( + _lib.AnyInstanceOf(int), + _lib.AnyInstanceOf(int), + _lib.AnyInstanceOf(int), +) + +print('Test finished successfully') diff --git a/examples/test_help.py b/examples/test_help.py new file mode 100644 index 0000000..950b264 --- /dev/null +++ b/examples/test_help.py @@ -0,0 +1,29 @@ +from cc import import_file, help + +_lib = import_file('_lib.py', __file__) + + +assert _lib.get_class_table(help) == _lib.get_object_table('help') + +help.setPath('/rom/help') + +assert help.path() == '/rom/help' + +assert help.lookup('disk') == 'rom/help/disk.txt' +assert help.lookup('abracadabra') is None + +ts = help.topics() +assert isinstance(ts, list) +assert len(ts) > 2 +# print(ts) +assert 'disk' in ts + +assert help.completeTopic('di') == ['sk'] +assert help.completeTopic('abracadabra') == [] + +assert help.setPath('/kek') is None +assert help.path() == '/kek' +assert help.topics() == ['index'] +assert help.setPath('/rom/help') is None + +print('Test finished successfully') diff --git a/examples/test_keys.py b/examples/test_keys.py new file mode 100644 index 0000000..bdf40cf --- /dev/null +++ b/examples/test_keys.py @@ -0,0 +1,20 @@ +from cc import keys + + +a = keys.getCode('a') +space = keys.getCode('space') +enter = keys.getCode('enter') +assert keys.getCode('doesnotexist') is None +assert keys.getCode('getName') is None +assert isinstance(a, int) +assert isinstance(space, int) +assert isinstance(enter, int) + +assert keys.getName(a) == 'a' +assert keys.getName(space) == 'space' +assert keys.getName(enter) == 'enter' + +# for i in range(255): +# print(i, keys.getName(i)) + +print('Test finished successfully') diff --git a/examples/test_multishell.py b/examples/test_multishell.py new file mode 100644 index 0000000..fb26241 --- /dev/null +++ b/examples/test_multishell.py @@ -0,0 +1,30 @@ +import random + +from cc import import_file, multishell + +_lib = import_file('_lib.py', __file__) + + +assert _lib.get_class_table(multishell) == _lib.get_object_table('multishell') + +_lib.step('Close all additional shells') + +assert multishell.getCount() == 1 +assert multishell.getCurrent() == 1 +assert multishell.getFocus() == 1 +assert isinstance(multishell.getTitle(1), str) + +title = f'new title {random.randint(1, 1000000)}' +assert multishell.setTitle(1, title) is None +assert multishell.getTitle(1) == title + +assert multishell.setFocus(1) is True +assert multishell.setFocus(0) is False +assert multishell.setFocus(2) is False + +assert multishell.getTitle(2) is None + +assert multishell.launch({}, 'rom/programs/fun/hello.lua') == 2 +assert isinstance(multishell.getTitle(2), str) + +print('Test finished successfully') diff --git a/examples/test_os.py b/examples/test_os.py new file mode 100644 index 0000000..aa80fc1 --- /dev/null +++ b/examples/test_os.py @@ -0,0 +1,61 @@ +from cc import import_file, os + +_lib = import_file('_lib.py', __file__) + + +tbl = _lib.get_object_table('os') + +# use methods with get* +del tbl['function']['computerID'] +del tbl['function']['computerLabel'] + +# we are in python world, loading lua modules is useless +del tbl['function']['loadAPI'] +del tbl['function']['unloadAPI'] + +# remove complex date formatting function in favor of python stdlib +del tbl['function']['date'] + +assert _lib.get_class_table(os) == tbl + + +with _lib.assert_takes_time(1.5, 3): + timer_id = os.startTimer(2) + while True: + e = os.pullEvent('timer') + if e[1] == timer_id: + print('Timer reached') + break + + +timer_id = os.startTimer(20) +assert isinstance(timer_id, int) +assert os.cancelTimer(timer_id) is None +assert os.cancelTimer(timer_id) is None + +alarm_id = os.setAlarm(0.0) +assert isinstance(alarm_id, int) +assert os.cancelAlarm(alarm_id) is None +assert os.cancelAlarm(alarm_id) is None + +with _lib.assert_takes_time(1.5, 3): + assert os.sleep(2) is None + +assert (os.version()).startswith('CraftOS ') +assert isinstance(os.getComputerID(), int) + +assert os.setComputerLabel(None) is None +assert os.getComputerLabel() is None +assert os.setComputerLabel('altair') is None +assert os.getComputerLabel() == 'altair' +assert os.setComputerLabel(None) is None +assert os.getComputerLabel() is None + +assert isinstance(os.epoch(), int) +assert isinstance(os.day(), int) +assert isinstance(os.time(), (int, float)) +assert isinstance(os.clock(), (int, float)) + +assert os.run({}, 'rom/programs/fun/hello.lua') is True + +print('Test finished successfully') diff --git a/examples/test_paintutils.py b/examples/test_paintutils.py new file mode 100644 index 0000000..f9c6be9 --- /dev/null +++ b/examples/test_paintutils.py @@ -0,0 +1,72 @@ +from cc import import_file, fs, os, term, colors, paintutils + +_lib = import_file('_lib.py', __file__) + + +pixels = ''' +0000000030030033333333330000000003000000000000000 +0333300000000033333333300000000000333333000000330 +0803000000803033333333000000000000880330300003000 +0800800030330333333333000300883000888880000033000 +3333000000003333333333300080038880000080000888003 +33333ddd3333333333333333300000333330000000000d033 +333dddddd3333333333333333333333333333333333ddd333 +3333ccdd333333333333344444444333333333333dddddd33 +333cc33d3333333333334444444444333333333335d3cc33d +5ddc33333333333333344444444444433333333333333cd55 +dddc555d3333333333344444444444433333333333d5dc5dd +d5dd5dd4bbbbbbbbb999b00b00300b3bb9999bbbb4ddddddd +ddd55444bb999993bbb33390b030bb9999bbbbbbb444ddddd +55dd44bbbbbbbbbbbbb9bb3003003bbb339bbbbbbbb44444d +dd444bbbbbbbbbbb99933bbb0030b999bbbbbbbbbbbbbbb44 +444bbbbbbbbbbbbbbb9bbb33b309933bbbbbbbbbbbbbbbbbb +bbbbbbbbbbbbbbbbbbbb9bbbb3bbbb99bbbbbbbbbbbbbbbbb +bbbbbbbbbbbbbbbbbbbbbb399399bbbbbbbbbbbbbbbbbbbbb +'''.strip() + + +assert _lib.get_class_table(paintutils) == _lib.get_object_table('paintutils') + +with fs.open('img.nfp', 'w') as f: + f.write(pixels) + +int_pixels = paintutils.loadImage('img.nfp') +assert len(int_pixels) > 0 +assert len(int_pixels[0]) > 0 +assert paintutils.parseImage(pixels) == int_pixels + +assert paintutils.drawImage(int_pixels, 1, 1) is None + +os.sleep(2) + +term.setTextColor(colors.white) +term.setBackgroundColor(colors.black) +term.clear() +term.setBackgroundColor(colors.green) + +by = 3 +bx = 3 + +assert paintutils.drawPixel(bx, by) is None +assert paintutils.drawPixel(bx + 1, by, colors.red) is None + +bx += 5 + +assert paintutils.drawLine(bx, by, bx + 3, by + 3) is None +assert paintutils.drawLine(bx + 3, by, bx, by + 3, colors.red) is None + +bx += 5 +assert paintutils.drawBox(bx, by, bx + 3, by + 3) is None +bx += 5 +assert paintutils.drawBox(bx, by, bx + 3, by + 3, colors.red) is None + +bx += 5 +assert paintutils.drawFilledBox(bx, by, bx + 3, by + 3) is None +bx += 5 +assert paintutils.drawFilledBox(bx, by, bx + 3, by + 3, colors.red) is None + +term.setCursorPos(1, by + 6) + +os.sleep(2) + +print('Test finished successfully') diff --git a/examples/test_parallel.py b/examples/test_parallel.py new file mode 100644 index 0000000..e50c2e8 --- /dev/null +++ b/examples/test_parallel.py @@ -0,0 +1,135 @@ +from cc import import_file, os, parallel + +_lib = import_file('_lib.py', __file__) +assert_takes_time, assert_raises = _lib.assert_takes_time, _lib.assert_raises + + +tags = set() + + +def partial(tag, fn, *args): + def wrap(): + tags.add(tag) + return fn(*args) + return wrap + + +all_parallels = [ + ('waitForAll', parallel.waitForAll), + ('waitForAny', parallel.waitForAny), +] + + +for name, fn in all_parallels: + tags.clear() + with assert_takes_time(1.5, 3): + # Since os.sleep is mostly waiting for events, it doesn't block execution of parallel threads + # and this snippet takes approximately 2 seconds to complete. + fn( + partial('a', os.sleep, 2), + partial('b', os.sleep, 2), + partial('c', os.sleep, 2), + ) + assert tags == {'a', 'b', 'c'} + print(name, 'OK') + + +for name, fn in all_parallels: + tags.clear() + tts = (0, 1) if name == 'waitForAny' else (1.5, 3) + with assert_takes_time(*tts): + fn( + partial('fast', os.version), + partial('s1', os.sleep, 2), + partial('s2', os.sleep, 2), + ) + assert tags == {'fast', 's1', 's2'} + print(name, 'fast OK') + + +def breaks_fast(etype): + os.sleep(0.5) + raise etype + + +def breaks_slow(etype): + os.sleep(3) + raise etype + + +tags.clear() +with assert_takes_time(0, 1): + parallel.waitForAny( + partial('fast', os.version), + partial('bomb', breaks_slow, IndexError), + ) +assert tags == {'fast', 'bomb'} +print('waitForAny fast success OK') + + +tags.clear() +with assert_takes_time(2.5, 3.8): + with assert_raises(IndexError): + parallel.waitForAll( + partial('fast', os.version), + partial('bomb', breaks_slow, IndexError), + ) +assert tags == {'fast', 'bomb'} +print('waitForAll waits for bomb OK') + + +for name, fn in all_parallels: + tags.clear() + with assert_takes_time(0.4, 1.2): + with assert_raises(ValueError): + fn( + partial('v', breaks_fast, ValueError), + partial('s', os.sleep, 2), + partial('i', breaks_slow, IndexError), + ) + os.sleep(4) + assert tags == {'v', 's', 'i'} + print(name + ' handles error OK') + + +for name, fn in all_parallels: + tags.clear() + with assert_takes_time(1.5, 3): + fn( + partial('1_s', os.sleep, 2), + partial( + '1_p', + fn, + partial('2_s', os.sleep, 2), + partial( + '2_p', + fn, + partial('3_s', os.sleep, 2), + ), + ), + ) + assert tags == {'1_s', '1_p', '2_s', '2_p', '3_s'} + print('Nested', name, 'OK') + + +def nested_err(): + parallel.waitForAll( + partial('n_v', breaks_fast, ValueError), + partial('n_s', os.sleep, 2), + partial('n_i', breaks_slow, IndexError), + ) + + +tags.clear() +with assert_takes_time(0.4, 1.2): + with assert_raises(ValueError): + parallel.waitForAll( + nested_err, + partial('s', os.sleep, 2), + partial('i', breaks_slow, IndexError), + ) +assert tags == {'s', 'i', 'n_v', 'n_s', 'n_i'} +print('Nested errors OK') + + +print('Test finished successfully') diff --git a/examples/test_peripheral.py b/examples/test_peripheral.py new file mode 100644 index 0000000..3e1dbf9 --- /dev/null +++ b/examples/test_peripheral.py @@ -0,0 +1,38 @@ +from cc import import_file, peripheral + +_lib = import_file('_lib.py', __file__) + + +tbl = _lib.get_object_table('peripheral') + +# use wrap +del tbl['function']['getMethods'] +del tbl['function']['call'] + +# TODO: support these methods +del tbl['function']['getName'] +del tbl['function']['find'] + +assert _lib.get_class_table(peripheral) == tbl + +_lib.step('Remove all peripherals') + +side = 'top' + +assert peripheral.getNames() == [] +assert peripheral.getType(side) is None +assert peripheral.isPresent(side) is False +assert peripheral.wrap(side) is None + +_lib.step(f'Put disk drive on {side} side of computer') + +assert peripheral.getNames() == [side] +assert peripheral.getType(side) == 'drive' +assert peripheral.isPresent(side) is True +d = peripheral.wrap(side) +assert d is not None +assert d.isDiskPresent() is False + +print('Remove disk drive') + +print('Test finished successfully') diff --git a/examples/test_peripheral_commandblock.py b/examples/test_peripheral_commandblock.py new file mode 100644 index 0000000..aae8e2a --- /dev/null +++ b/examples/test_peripheral_commandblock.py @@ -0,0 +1,31 @@ +from computercraft.subapis.peripheral import CCCommandBlock +from cc import LuaException, import_file, peripheral + +_lib = import_file('_lib.py', __file__) + + +side = 'left' + +_lib.step(f'Attach command block at {side} side of computer') + +m = peripheral.wrap(side) + +tbl = _lib.get_object_table(f'peripheral.wrap("{side}")') +assert _lib.get_class_table(CCCommandBlock) == tbl + +assert m.getCommand() == '' +assert m.setCommand('say Hello from python side') is None +assert m.getCommand() == 'say Hello from python side' +assert m.runCommand() is None + +assert m.setCommand('time query daytime') is None +assert m.getCommand() == 'time query daytime' +assert m.runCommand() is None + +assert m.setCommand('') is None +assert m.getCommand() == '' +with _lib.assert_raises(LuaException): + m.runCommand() + +print('You must have seen chat message') +print('Test finished successfully') diff --git a/examples/test_peripheral_computer.py b/examples/test_peripheral_computer.py new file mode 100644 index 0000000..6623484 --- /dev/null +++ b/examples/test_peripheral_computer.py @@ -0,0 +1,5 @@ +from cc import import_file + +_lib = import_file('_lib.py', __file__) + +_lib._computer_peri('another computer', 'computer') diff --git a/examples/test_peripheral_disk.py b/examples/test_peripheral_disk.py new file mode 100644 index 0000000..c1b7605 --- /dev/null +++ b/examples/test_peripheral_disk.py @@ -0,0 +1,76 @@ +from computercraft.subapis.peripheral import CCDrive +from cc import LuaException, import_file, peripheral + +_lib = import_file('_lib.py', __file__) + + +side = 'left' + +_lib.step(f'Put empty disk drive on {side} side of computer') + +d = peripheral.wrap(side) +assert d is not None + +tbl = _lib.get_object_table(f'peripheral.wrap("{side}")') +assert _lib.get_class_table(CCDrive) == tbl + +assert d.isDiskPresent() is False +assert d.hasData() is False +assert d.getMountPath() is None +assert d.setDiskLabel('text') is None +assert d.getDiskLabel() is None +assert d.getDiskID() is None +assert d.hasAudio() is False +assert d.getAudioTitle() is False # False instead None! +assert d.playAudio() is None +assert d.stopAudio() is None +assert d.ejectDisk() is None + +_lib.step('Put new CC diskette into disk drive') + +assert d.isDiskPresent() is True +assert d.hasData() is True +assert isinstance(d.getMountPath(), str) +assert isinstance(d.getDiskID(), int) + +assert d.getDiskLabel() is None +assert d.setDiskLabel('label') is None +assert d.getDiskLabel() == 'label' +assert d.setDiskLabel(None) is None +assert d.getDiskLabel() is None + +assert d.hasAudio() is False +assert d.getAudioTitle() is None +assert d.playAudio() is None +assert d.stopAudio() is None + +assert d.ejectDisk() is None + +_lib.step('Put any audio disk into disk drive') + +assert d.isDiskPresent() is True +assert d.hasData() is False +assert d.getMountPath() is None +assert d.getDiskID() is None +assert d.hasAudio() is True + +label = d.getAudioTitle() +assert isinstance(label, str) +assert label != 'label' +print(f'Label is {label}') +assert d.getDiskLabel() == label +with _lib.assert_raises(LuaException): + d.setDiskLabel('label') +with _lib.assert_raises(LuaException): + d.setDiskLabel(None) +# no effect +assert d.getDiskLabel() == label + +assert d.playAudio() is None + +_lib.step('Audio must be playing now') + +assert d.stopAudio() is None +assert d.ejectDisk() is None + +print('Test finished successfully') diff --git a/examples/test_peripheral_modem.py b/examples/test_peripheral_modem.py new file mode 100644 index 0000000..d3ec842 --- /dev/null +++ b/examples/test_peripheral_modem.py @@ -0,0 +1,49 @@ +from cc import import_file, parallel, peripheral + +_lib = import_file('_lib.py', __file__) + + +# do this test twice: for wired and wireless modems + +side = 'back' + +_lib.step( + f'Attach modem to {side} side of computer\n' + f'Place another computer with similar modem at {side} side\n' + 'In case of wired modems connect them\n' + 'On another computer start py modem_server.py' +) + +m = peripheral.wrap(side) + +remote_channel = 5 +local_channel = 7 +messages = [] + + +def _send(): + m.transmit(remote_channel, local_channel, 1) + m.transmit(remote_channel, local_channel, 'hi') + m.transmit(remote_channel, local_channel, {'data': 5}) + m.transmit(remote_channel, local_channel, 'stop') + + +def _recv(): + assert m.isOpen(local_channel) is False + for msg in m.receive(local_channel): + assert m.isOpen(local_channel) is True + assert msg.reply_channel == remote_channel + assert msg.distance > 0 + messages.append(msg.content) + if len(messages) == 3: + break + + +parallel.waitForAll(_recv, _send) + +assert messages == [1, 'hi', {'data': 5}] +assert m.isOpen(local_channel) is False +assert m.closeAll() is None +assert isinstance(m.isWireless(), bool) + +print('Test finished successfully') diff --git a/examples/test_peripheral_monitor.py b/examples/test_peripheral_monitor.py new file mode 100644 index 0000000..a7a01a6 --- /dev/null +++ b/examples/test_peripheral_monitor.py @@ -0,0 +1,144 @@ +from computercraft.subapis.peripheral import CCMonitor +from computercraft.subapis.mixins import TermMixin +from cc import import_file, colors, os, peripheral + +_lib = import_file('_lib.py', __file__) + + +side = 'left' + +_lib.step( + 'Use advanced computer and monitor for colors\n' + f'Place single block monitor on {side} side of computer', +) + +m = peripheral.wrap(side) +assert m is not None + + +tbl = _lib.get_object_table(f'peripheral.wrap("{side}")') + +# remove British method names to make API lighter +del tbl['function']['getBackgroundColour'] +del tbl['function']['getPaletteColour'] +del tbl['function']['getTextColour'] +del tbl['function']['isColour'] +del tbl['function']['setBackgroundColour'] +del tbl['function']['setPaletteColour'] +del tbl['function']['setTextColour'] +# NOTE: peripheral doesn't have nativePaletteColor method + +assert _lib.get_multiclass_table(TermMixin, CCMonitor) == tbl + + +assert m.getSize() == (7, 5) +assert m.isColor() is True +assert m.setTextColor(colors.white) is None +assert m.setBackgroundColor(colors.black) is None +assert m.clear() is None +assert m.setCursorPos(1, 1) is None +assert m.getCursorPos() == (1, 1) +assert m.write('Alpha') is None +assert m.getCursorPos() == (6, 1) +assert m.setCursorBlink(False) is None +assert m.getCursorBlink() is False +assert m.setCursorBlink(True) is None +assert m.getCursorBlink() is True + +_lib.step('You must have seen word Alpha with blinking cursor') + +assert m.clear() is None +assert m.setCursorBlink(False) is None +for offs, (tc, bc) in enumerate(( + (colors.lime, colors.green), + (colors.yellow, colors.brown), + (colors.red, colors.orange), +), start=1): + assert m.setTextColor(tc) is None + assert m.getTextColor() == tc + assert m.setBackgroundColor(bc) is None + assert m.getBackgroundColor() == bc + assert m.setCursorPos(offs, offs) is None + assert m.getCursorPos() == (offs, offs) + assert m.write('text') is None +assert m.setBackgroundColor(colors.black) is None +os.sleep(1) +for i in range(2): + assert m.scroll(-1) is None + os.sleep(0.5) +for i in range(2): + assert m.scroll(1) is None + os.sleep(0.5) + +_lib.step('You must have seen three texts with different colors scrolling') + +assert m.setTextColor(colors.white) is None +assert m.setBackgroundColor(colors.black) is None +assert m.clear() is None +for i in range(1, 5): + assert m.setCursorPos(1, i) is None + assert m.write((str(i) + ' ') * 4) is None +os.sleep(2) +for i in range(2, 5, 2): + assert m.setCursorPos(1, i) is None + assert m.clearLine() is None + +_lib.step('You must have seen some lines disappearing') + +assert m.setBackgroundColor(colors.black) is None +assert m.clear() is None +assert m.setCursorPos(1, 1) is None +assert m.blit( + 'rainbow', + 'e14d3ba', + 'fffffff', +) is None +assert m.setCursorPos(1, 2) is None +assert m.blit( + 'rainbow', + '0000000', + 'e14d3ba', +) is None + +_lib.step('You must have seen per-letter colored text') + +assert m.setBackgroundColor(colors.black) is None +assert m.setTextColor(colors.white) is None +assert m.getTextScale() == 1 +assert m.setTextScale(5) is None +assert m.getTextScale() == 5 +assert m.setCursorPos(1, 1) is None +assert m.clear() is None +assert m.getSize() == (1, 1) +assert m.write('AAA') is None + +_lib.step('You must have seen single large letter A') + +assert m.setTextScale(1) is None +assert m.setBackgroundColor(colors.white) is None +assert m.clear() is None +for i, color in enumerate(colors.iter_colors()): + m.setPaletteColor(color, i / 15, 0, 0) +assert m.setCursorPos(1, 1) is None +assert m.blit( + ' redtex', + '0123456', + '0000000', +) is None +assert m.setCursorPos(1, 2) is None +assert m.blit( + 'tappear', + '789abcd', + '0000000', +) is None +assert m.setCursorPos(1, 3) is None +assert m.blit( + 's!', + 'ef', + '00', +) is None + +_lib.step('You must have seen different shades of red made using palettes') + +print('Remove monitor') +print('Test finished successfully') diff --git a/examples/test_peripheral_printer.py b/examples/test_peripheral_printer.py new file mode 100644 index 0000000..e167ebd --- /dev/null +++ b/examples/test_peripheral_printer.py @@ -0,0 +1,108 @@ +from computercraft.subapis.peripheral import CCPrinter +from cc import LuaException, import_file, peripheral + +_lib = import_file('_lib.py', __file__) +assert_raises = _lib.assert_raises + + +side = 'left' + +_lib.step(f'Attach empty printer at {side} side of computer') + +m = peripheral.wrap(side) + +tbl = _lib.get_object_table(f'peripheral.wrap("{side}")') +assert _lib.get_class_table(CCPrinter) == tbl + +assert m.getPaperLevel() == 0 +assert m.getInkLevel() == 0 + +# no paper +assert m.newPage() is False +# page not started +with assert_raises(LuaException): + m.endPage() +with assert_raises(LuaException): + m.write('test') +with assert_raises(LuaException): + m.setCursorPos(2, 2) +with assert_raises(LuaException): + m.getCursorPos() +with assert_raises(LuaException): + m.getPageSize() +with assert_raises(LuaException): + m.setPageTitle('title') + +_lib.step('Put paper into printer') +paper_level = m.getPaperLevel() +assert paper_level > 0 +# no ink +assert m.newPage() is False + +_lib.step('Put ink into printer') +ink_level = m.getInkLevel() +assert ink_level > 0 + +assert m.newPage() is True +assert m.getPaperLevel() < paper_level +assert m.getInkLevel() < ink_level + +assert m.setCursorPos(2, 2) is None +assert m.getCursorPos() == (2, 2) +assert m.setCursorPos(1, 1) is None +assert m.getCursorPos() == (1, 1) +assert m.setPageTitle('Green bottles') is None +assert m.getPageSize() == (25, 21) + + +async def row(n=1): + _, r = m.getCursorPos() + m.setCursorPos(1, r + n) + + +def split_text(text, max_width=25): + for i in range(0, len(text), max_width): + yield text[i:i + max_width] + + +def split_by_words(text, max_width=25): + stack = [] + stack_len = 0 + for word in text.split(' '): + assert len(word) <= max_width + with_word = len(word) if stack_len == 0 else stack_len + 1 + len(word) + if with_word > max_width: + yield ' '.join(stack) + stack.clear() + stack_len = 0 + else: + stack.append(word) + stack_len = with_word + if stack: + yield ' '.join(stack) + + +def multiline_write(text): + _, r = m.getCursorPos() + for pt in split_by_words(text): + assert m.setCursorPos(1, r) is None + assert m.write(pt) is None + r += 1 + assert m.setCursorPos(1, r) is None + + +assert m.write('Green bottles'.center(25)) is None +row(2) + +x = 2 +while x > 0: + multiline_write(f'{x} green bottles hanging on the wall') + multiline_write(f'{x} green bottles hanging on the wall') + multiline_write('if one green bottle accidently falls') + x -= 1 + multiline_write(f'there will be {x} hanging on the wall') + row() + +assert m.endPage() is True + +print('Test finished successfully') diff --git a/examples/test_peripheral_remote.py b/examples/test_peripheral_remote.py new file mode 100644 index 0000000..262daa1 --- /dev/null +++ b/examples/test_peripheral_remote.py @@ -0,0 +1,43 @@ +from cc import import_file, peripheral + +_lib = import_file('_lib.py', __file__) + + +side = 'back' + +_lib.step(f'Attach and disable (right-click) wired modem at {side} side') + +m = peripheral.wrap(side) +assert m.isWireless() is False +assert m.getNameLocal() is None + +_lib.step(f'Enable (right-click) wired modem at {side} side') + +assert isinstance(m.getNameLocal(), str) + +_lib.step('Connect networked speaker peripheral & enable its modem') + +names = m.getNamesRemote() +assert isinstance(names, list) +assert len(names) > 0 +speaker = [] +for n in names: + assert isinstance(n, str) + if n.startswith('speaker_'): + speaker.append(n) +assert len(speaker) == 1 +speaker = speaker[0] + +assert m.isPresentRemote('doesnotexist') is False +assert m.getTypeRemote('doesnotexist') is None + +assert m.isPresentRemote(speaker) is True +assert m.getTypeRemote(speaker) == 'speaker' + +assert m.wrapRemote('doesnotexist') is None +s = m.wrapRemote(speaker) + +assert s.playSound('minecraft:entity.player.levelup') is True + +print('You must have heard levelup sound') +print('Test finished successfully') diff --git a/examples/test_peripheral_speaker.py b/examples/test_peripheral_speaker.py new file mode 100644 index 0000000..336450e --- /dev/null +++ b/examples/test_peripheral_speaker.py @@ -0,0 +1,34 @@ +import random + +from computercraft.subapis.peripheral import CCSpeaker +from cc import import_file, os, peripheral + +_lib = import_file('_lib.py', __file__) + + +side = 'left' + +_lib.step(f'Attach speaker at {side} side of computer') + +m = peripheral.wrap(side) + + +tbl = _lib.get_object_table(f'peripheral.wrap("{side}")') +assert _lib.get_class_table(CCSpeaker) == tbl + +for _ in range(48): + assert m.playNote( + random.choice([ + 'bass', 'basedrum', 'bell', 'chime', 'flute', 'guitar', 'hat', + 'snare', 'xylophone', 'iron_xylophone', 'pling', 'banjo', + 'bit', 'didgeridoo', 'cow_bell', + ]), + 3, + random.randint(0, 24) + ) is True + os.sleep(0.2) + +assert m.playSound('minecraft:entity.player.levelup') is True + +print('You must have heard notes and sounds') +print('Test finished successfully') diff --git a/examples/test_peripheral_turtle.py b/examples/test_peripheral_turtle.py new file mode 100644 index 0000000..044835e --- /dev/null +++ b/examples/test_peripheral_turtle.py @@ -0,0 +1,5 @@ +from cc import import_file + +_lib = import_file('_lib.py', __file__) + +_lib._computer_peri('turtle', 'turtle') diff --git a/examples/test_pocket.py b/examples/test_pocket.py new file mode 100644 index 0000000..57a7ed2 --- /dev/null +++ b/examples/test_pocket.py @@ -0,0 +1,27 @@ +from cc import LuaException, import_file, pocket, peripheral + +_lib = import_file('_lib.py', __file__) + + +assert peripheral.isPresent('back') is False + +tbl = _lib.get_object_table('pocket') +assert _lib.get_class_table(pocket) == tbl + +_lib.step('Clean inventory from any pocket upgrades') + +with _lib.assert_raises(LuaException): + pocket.equipBack() +with _lib.assert_raises(LuaException): + pocket.unequipBack() +assert peripheral.isPresent('back') is False + +_lib.step('Put any pocket upgrade to inventory') + +assert pocket.equipBack() is None +assert peripheral.isPresent('back') is True + +assert pocket.unequipBack() is None +assert peripheral.isPresent('back') is False + +print('Test finished successfully') diff --git a/examples/test_reboot.py b/examples/test_reboot.py new file mode 100644 index 0000000..54820bf --- /dev/null +++ b/examples/test_reboot.py @@ -0,0 +1,5 @@ +from cc import os + + +assert os.reboot() is None +print('Test finished successfully') diff --git a/examples/test_redirect_to_local_monitor.py b/examples/test_redirect_to_local_monitor.py new file mode 100644 index 0000000..10fe60c --- /dev/null +++ b/examples/test_redirect_to_local_monitor.py @@ -0,0 +1,16 @@ +from cc import import_file, colors, term, peripheral + +_lib = import_file('_lib.py', __file__) + + +side = 'left' +_lib.step(f'Attach 3x3 color monitor to {side} side of computer') + +with term.redirect(peripheral.get_term_target(side)): + term.setBackgroundColor(colors.green) + term.setTextColor(colors.white) + term.clear() + term.setCursorPos(1, 1) + print('Redirected to monitor') + +print('Test finished successfully') diff --git a/examples/test_redirect_to_remote_monitor.py b/examples/test_redirect_to_remote_monitor.py new file mode 100644 index 0000000..db9f1b1 --- /dev/null +++ b/examples/test_redirect_to_remote_monitor.py @@ -0,0 +1,26 @@ +from cc import import_file, colors, term, peripheral + +_lib = import_file('_lib.py', __file__) + + +side = 'back' +_lib.step(f'Attach wired modem to {side} side of computer') + +mod = peripheral.wrap(side) + +_lib.step('Connect remote monitor using wires, activate its modem') + +for name in mod.getNamesRemote(): + if mod.getTypeRemote(name) == 'monitor': + break +else: + assert False + +with term.redirect(peripheral.get_term_target(name)): + term.setBackgroundColor(colors.blue) + term.setTextColor(colors.white) + term.clear() + term.setCursorPos(1, 1) + print('Redirected to monitor') + +print('Test finished successfully') diff --git a/examples/test_redirect_to_window.py b/examples/test_redirect_to_window.py new file mode 100644 index 0000000..703c761 --- /dev/null +++ b/examples/test_redirect_to_window.py @@ -0,0 +1,30 @@ +from contextlib import ExitStack + +from cc import colors, term, window + + +w, h = term.getSize() +with ExitStack() as stack: + left = stack.enter_context(window.create( + term.get_current_target(), + 1, 1, w // 2, h, True, + )) + right = stack.enter_context(window.create( + term.get_current_target(), + w // 2 + 1, 1, w // 2, h, True, + )) + with term.redirect(left.get_term_target()): + term.setBackgroundColor(colors.green) + term.setTextColor(colors.white) + term.clear() + term.setCursorPos(1, h // 2) + print('Left part') + with term.redirect(right.get_term_target()): + term.setBackgroundColor(colors.red) + term.setTextColor(colors.yellow) + term.clear() + term.setCursorPos(1, h // 2) + print('Right part') + print('Default terminal restored') + +print('Test finished successfully') diff --git a/examples/test_rednet.py b/examples/test_rednet.py new file mode 100644 index 0000000..bb1457c --- /dev/null +++ b/examples/test_rednet.py @@ -0,0 +1,62 @@ +from cc import LuaException, import_file, os, rednet, parallel + +_lib = import_file('_lib.py', __file__) +step, assert_raises = _lib.step, _lib.assert_raises + + +tbl = _lib.get_object_table('rednet') +del tbl['function']['run'] +assert _lib.get_class_table(rednet) == tbl + +side = 'back' + +step(f'Attach modem to {side} side of computer') + +assert rednet.isOpen(side) is False +assert rednet.isOpen() is False + +with assert_raises(LuaException): + rednet.close('doesnotexist') + +assert rednet.close(side) is None + +with assert_raises(LuaException): + rednet.open('doesnotexist') + +assert rednet.open(side) is None +assert rednet.isOpen(side) is True + +with assert_raises(LuaException): + # disallowed hostname + rednet.host('helloproto', 'localhost') +assert rednet.host('helloproto', 'alpha') is None + +cid = os.getComputerID() + +assert rednet.lookup('helloproto', 'localhost') == cid +assert rednet.lookup('helloproto') == [cid] +assert rednet.lookup('nonexistent', 'localhost') is None +assert rednet.lookup('nonexistent') == [] + +assert rednet.unhost('helloproto') is None + +assert rednet.send(cid + 100, 'message', 'anyproto') is True +assert rednet.broadcast('message', 'anyproto') is None + +assert rednet.receive(timeout=1) is None + + +def _send(): + assert rednet.send(cid, 'message') is True + + +def _recv(): + assert rednet.receive(timeout=1) == (cid, 'message', None) + + +parallel.waitForAll(_send, _recv) + +assert rednet.close() is None +assert rednet.isOpen(side) is False + +print('Test finished successfully') diff --git a/examples/test_redstone.py b/examples/test_redstone.py new file mode 100644 index 0000000..5a8ab47 --- /dev/null +++ b/examples/test_redstone.py @@ -0,0 +1,55 @@ +from cc import import_file, os, redstone + +_lib = import_file('_lib.py', __file__) + + +tbl = _lib.get_object_table('redstone') + +# remove British method names to make API lighter +del tbl['function']['getAnalogueInput'] +del tbl['function']['getAnalogueOutput'] +del tbl['function']['setAnalogueOutput'] + +assert _lib.get_class_table(redstone) == tbl + +assert set(redstone.getSides()) == {'top', 'bottom', 'front', 'back', 'left', 'right'} + +_lib.step('Remove all the redstone from sides of computer') + +side = 'top' + +assert redstone.setOutput(side, True) is None +assert redstone.getOutput(side) is True +assert redstone.getAnalogOutput(side) == 15 +assert redstone.setOutput(side, False) is None +assert redstone.getOutput(side) is False +assert redstone.getAnalogOutput(side) == 0 + +assert redstone.setAnalogOutput(side, 7) is None +assert redstone.getAnalogOutput(side) == 7 +assert redstone.getOutput(side) is True +assert redstone.setAnalogOutput(side, 15) is None +assert redstone.getAnalogOutput(side) == 15 +assert redstone.setAnalogOutput(side, 0) is None +assert redstone.getAnalogOutput(side) == 0 +assert redstone.getOutput(side) is False + +assert redstone.getInput(side) is False +assert redstone.getAnalogInput(side) == 0 + +_lib.step(f'Put redstone block on {side} side of computer') + +assert redstone.getInput(side) is True +assert redstone.getAnalogInput(side) > 0 + +_lib.step(f'Remove redstone block\nPut piston on {side} side of computer') + +assert redstone.getInput(side) is False +assert redstone.getAnalogInput(side) == 0 +assert redstone.setOutput(side, True) is None +os.sleep(2) +assert redstone.setOutput(side, False) is None + +print('Piston must have been activated\nRemove piston') + +print('Test finished successfully') diff --git a/examples/test_settings.py b/examples/test_settings.py new file mode 100644 index 0000000..b0197c6 --- /dev/null +++ b/examples/test_settings.py @@ -0,0 +1,101 @@ +from cc import LuaException, import_file, fs, settings + +_lib = import_file('_lib.py', __file__) +step, assert_raises = _lib.step, _lib.assert_raises + + +assert _lib.get_class_table(settings) == _lib.get_object_table('settings') + +step('Settings will be cleared') + +assert settings.clear() is None +# names are not empty, there are system settings +assert isinstance(settings.getNames(), list) + +assert settings.define('test.a') is None +assert settings.define('test.b', description='b') is None +assert settings.define('test.c', type='string') is None +assert settings.define('test.d', default=42) is None + +assert settings.getDetails('test.a') == { + 'changed': False, +} +assert settings.getDetails('test.b') == { + 'changed': False, + 'description': 'b', +} +assert settings.getDetails('test.c') == { + 'changed': False, + 'type': 'string', +} +assert settings.getDetails('test.d') == { + 'changed': False, + 'default': 42, + 'value': 42, +} + +# redefining +assert settings.define('test.a', type='number', default=11) is None + +assert settings.getDetails('test.a') == { + 'changed': False, + 'type': 'number', + 'default': 11, + 'value': 11, +} + +assert settings.get('test.a') == 11 +assert settings.set('test.a', 12) is None +assert settings.get('test.a') == 12 +with assert_raises(LuaException): + settings.set('test.a', 'text') +assert settings.get('test.a') == 12 +assert settings.unset('test.a') is None +assert settings.get('test.a') == 11 + +assert settings.set('test.c', 'hello') is None + +assert {'test.a', 'test.b', 'test.c', 'test.d'}.issubset(set(settings.getNames())) + +assert settings.undefine('test.a') is None +assert settings.undefine('test.b') is None +assert settings.undefine('test.c') is None +assert settings.undefine('test.d') is None + +assert 'test.c' in settings.getNames() +assert settings.get('test.c') == 'hello' +assert settings.getDetails('test.c') == { + 'changed': True, + 'value': 'hello', +} + +assert settings.unset('test.c') is None + +assert settings.get('test.c') is None +assert settings.getDetails('test.c') == { + 'changed': False, +} + +assert {'test.a', 'test.b', 'test.c', 'test.d'} & set(settings.getNames()) == set() + +assert settings.set('test.e', [9, 'text', False]) is None +assert settings.get('test.e') == [9, 'text', False] +assert settings.clear() is None +assert settings.get('test.e') is None + +fs.delete('.settings') + +assert settings.load() is False +assert settings.save() is True +assert settings.load() is True + +fs.delete('.settings') + +assert settings.set('key', 84) is None + +assert settings.save('sfile') is True +assert settings.load('sfile') is True + +fs.delete('sfile') + +print('Test finished successfully') diff --git a/examples/test_shell.py b/examples/test_shell.py new file mode 100644 index 0000000..1f03bb4 --- /dev/null +++ b/examples/test_shell.py @@ -0,0 +1,61 @@ +from cc import import_file, shell + +_lib = import_file('_lib.py', __file__) + + +tbl = _lib.get_object_table('shell') +del tbl['function']['setCompletionFunction'] +del tbl['function']['getCompletionInfo'] +assert _lib.get_class_table(shell) == tbl + +assert shell.complete('ls ro') == ['m/', 'm'] +assert shell.completeProgram('lu') == ['a'] + +ps = shell.programs() +assert 'shutdown' in ps + +als = shell.aliases() +assert 'ls' in als +assert als['ls'] == 'list' +assert 'xls' not in als +assert shell.setAlias('xls', 'list') is None +als = shell.aliases() +assert 'xls' in als +assert als['xls'] == 'list' +assert shell.clearAlias('xls') is None +als = shell.aliases() +assert 'xls' not in als + +assert shell.getRunningProgram() == 'py' + +assert shell.resolveProgram('doesnotexist') is None +assert shell.resolveProgram('hello') == 'rom/programs/fun/hello.lua' + +assert shell.dir() == '' +assert shell.resolve('doesnotexist') == 'doesnotexist' +assert shell.resolve('startup.lua') == 'startup.lua' +assert shell.setDir('rom') is None +assert shell.dir() == 'rom' +assert shell.resolve('startup.lua') == 'rom/startup.lua' +assert shell.setDir('') is None + +assert isinstance(shell.path(), str) +assert shell.setPath(shell.path()) is None + +assert shell.execute('hello') is True +assert shell.run('hello') is True +assert shell.execute('doesnotexist') is False +assert shell.run('doesnotexist') is False + +tab = shell.openTab('hello') +assert isinstance(tab, int) + +_lib.step(f'Program has been launched in tab {tab}') + +assert shell.switchTab(tab) is None + +_lib.step('Computer will shutdown after test due to shell.exit') + +assert shell.exit() is None + +print('Test finished successfully') diff --git a/examples/test_shutdown.py b/examples/test_shutdown.py new file mode 100644 index 0000000..8ce2357 --- /dev/null +++ b/examples/test_shutdown.py @@ -0,0 +1,5 @@ +from cc import os + + +assert os.shutdown() is None +print('Test finished successfully') diff --git a/examples/test_term.py b/examples/test_term.py new file mode 100644 index 0000000..74cc980 --- /dev/null +++ b/examples/test_term.py @@ -0,0 +1,108 @@ +from cc import import_file, colors, os, term +from computercraft.subapis.mixins import TermMixin + +_lib = import_file('_lib.py', __file__) + + +tbl = _lib.get_object_table('term') + +# not defined in TermMixin +del tbl['function']['redirect'] +del tbl['function']['current'] +del tbl['function']['native'] + +# remove British method names to make API lighter +del tbl['function']['getBackgroundColour'] +del tbl['function']['getPaletteColour'] +del tbl['function']['getTextColour'] +del tbl['function']['isColour'] +del tbl['function']['nativePaletteColour'] +del tbl['function']['setBackgroundColour'] +del tbl['function']['setPaletteColour'] +del tbl['function']['setTextColour'] + +assert _lib.get_class_table(TermMixin) == tbl + +_lib.step( + 'Detach all monitors\n' + 'Use advanced computer for colors\n' + 'Screen will be cleared' +) + +assert term.getSize() == (51, 19) +assert term.isColor() is True +assert term.clear() is None +assert term.setCursorPos(1, 1) is None +assert term.getCursorPos() == (1, 1) +assert term.write('Alpha') is None +assert term.getCursorPos() == (6, 1) +assert term.setCursorBlink(False) is None +assert term.getCursorBlink() is False +assert term.setCursorBlink(True) is None +assert term.getCursorBlink() is True +os.sleep(2) + +_lib.term_step('You must have seen word Alpha with blinking cursor') + +assert term.clear() is None +for offs, (tc, bc) in enumerate(( + (colors.lime, colors.green), + (colors.yellow, colors.brown), + (colors.red, colors.orange), +), start=1): + assert term.setTextColor(tc) is None + assert term.getTextColor() == tc + assert term.setBackgroundColor(bc) is None + assert term.getBackgroundColor() == bc + assert term.setCursorPos(offs * 2, offs) is None + assert term.getCursorPos() == (offs * 2, offs) + assert term.write('text with colors') is None +assert term.setBackgroundColor(colors.black) is None +os.sleep(1) +for i in range(3): + assert term.scroll(-2) is None + os.sleep(0.5) +for i in range(6): + assert term.scroll(1) is None + os.sleep(0.25) + +_lib.term_step('You must have seen three texts with different colors scrolling') + +assert term.clear() is None +for i in range(1, 10): + assert term.setCursorPos(1, i) is None + assert term.write((str(i) + ' ') * 10) is None +os.sleep(2) +for i in range(2, 10, 2): + assert term.setCursorPos(1, i) is None + assert term.clearLine() is None +os.sleep(2) + +_lib.term_step('You must have seen some lines disappearing') + +assert term.clear() is None +assert term.setCursorPos(1, 1) is None +assert term.blit( + 'rainbowrainbow', + 'e14d3ba0000000', + 'fffffffe14d3ba', +) is None +os.sleep(3) + +_lib.term_step('You must have seen per-letter colored text') + +assert term.setBackgroundColor(colors.white) is None +assert term.clear() is None +assert term.setCursorPos(1, 1) is None +for i, color in enumerate(colors): + term.setPaletteColor(color, i / 15, 0, 0) +assert term.blit( + ' redtextappears!', + '0123456789abcdef', + '0000000000000000', +) is None +os.sleep(3) + +_lib.term_step('You must have seen different shades of red made using palettes') + +print('Test finished successfully') diff --git a/examples/test_textutils.py b/examples/test_textutils.py new file mode 100644 index 0000000..a05bce0 --- /dev/null +++ b/examples/test_textutils.py @@ -0,0 +1,52 @@ +from cc import colors, textutils + + +assert textutils.slowWrite('write ') is None +assert textutils.slowWrite('write ', 5) is None +assert textutils.slowPrint('print') is None +assert textutils.slowPrint('print', 5) is None + +assert textutils.formatTime(0) == '0:00 AM' +assert textutils.formatTime(0, True) == '0:00' + +table = [ + colors.red, + ['Planet', 'Distance', 'Mass'], + colors.gray, + ['Mercury', '0.387', '0.055'], + colors.lightGray, + ['Venus', '0.723', '0.815'], + colors.green, + ['Earth', '1.000', '1.000'], + colors.red, + ['Mars', '1.524', '0.107'], + colors.orange, + ['Jupiter', '5.203', '318'], + colors.yellow, + ['Saturn', '9.537', '95'], + colors.cyan, + ['Uranus', '19.191', '14.5'], + colors.blue, + ['Neptune', '30.069', '17'], + colors.white, +] + +assert textutils.tabulate(*table) is None + +lines = textutils.pagedPrint(''' +Lorem ipsum dolor sit amet, consectetur adipiscing elit. +Suspendisse feugiat diam et velit aliquam, nec porttitor eros facilisis. +Nulla facilisi. +Sed eget dui vel tellus aliquam fermentum. +Aliquam sed lorem congue, dignissim nulla in, porta diam. +Aliquam erat volutpat. +'''.strip()) +assert isinstance(lines, int) +assert lines > 0 + +assert textutils.pagedTabulate(*table[:-1], *table[2:-1], *table[2:]) is None + +assert textutils.complete('co', ['command', 'row', 'column']) == [ + 'mmand', 'lumn'] + +print('Test finished successfully') diff --git a/examples/test_turtle.py b/examples/test_turtle.py new file mode 100644 index 0000000..35c58d2 --- /dev/null +++ b/examples/test_turtle.py @@ -0,0 +1,248 @@ +from cc import LuaException, import_file, turtle, peripheral + +_lib = import_file('_lib.py', __file__) +assert_raises, step = _lib.assert_raises, _lib.step + + +tbl = _lib.get_object_table('turtle') +assert tbl['table'] == {'native': True} +del tbl['table'] +tbl['function'].setdefault('craft', True) +assert _lib.get_class_table(turtle) == tbl + +flimit = turtle.getFuelLimit() +assert isinstance(flimit, int) +assert flimit > 0 + +flevel = turtle.getFuelLevel() +assert isinstance(flevel, int) +assert 0 <= flevel <= flimit + +assert turtle.select(2) is None +assert turtle.getSelectedSlot() == 2 +with assert_raises(LuaException): + turtle.select(0) +assert turtle.select(1) is None +assert turtle.getSelectedSlot() == 1 + +step('Put 3 coals into slot 1') + +assert turtle.getItemCount() == 3 +assert turtle.getItemCount(1) == 3 + +assert turtle.getItemDetail() == { + 'count': 3, + 'name': 'minecraft:coal', +} +assert turtle.getItemDetail(1) == { + 'count': 3, + 'name': 'minecraft:coal', +} + +assert turtle.getItemSpace() == 61 +assert turtle.getItemSpace(1) == 61 + +assert turtle.refuel(1) is None + +assert turtle.getFuelLevel() > flevel +flevel = turtle.getFuelLevel() +assert turtle.getItemCount() == 2 + +assert turtle.refuel() is None + +assert turtle.getFuelLevel() > flevel +assert turtle.getItemCount() == 0 + +with assert_raises(LuaException): + turtle.refuel(1) +with assert_raises(LuaException): + turtle.refuel() + +step('Remove blocks in front/below/above turtle') + +assert turtle.detect() is False +assert turtle.detectUp() is False +assert turtle.detectDown() is False + +assert turtle.inspect() is None +assert turtle.inspectUp() is None +assert turtle.inspectDown() is None + +step('Put cobblestone blocks in front/below/above turtle') + +assert turtle.detect() is True +assert turtle.detectUp() is True +assert turtle.detectDown() is True + +for c in [ + turtle.inspect(), + turtle.inspectUp(), + turtle.inspectDown() +]: + assert isinstance(c, dict) + assert c['name'] == 'minecraft:cobblestone' + +assert turtle.select(1) is None +assert turtle.getItemCount() == 0 +assert turtle.equipLeft() is None + +assert turtle.select(2) is None +assert turtle.getItemCount() == 0 +assert turtle.equipRight() is None + +if ( + turtle.getItemCount(1) != 0 + or turtle.getItemCount(2) != 0 +): + step('Remove all items from slots 1 and 2') + +assert turtle.select(1) is None +if turtle.getItemDetail(1) != { + 'count': 1, + 'name': 'minecraft:diamond_pickaxe', +}: + step('Put fresh diamond pickaxe at slot 1') + +assert turtle.equipLeft() is None + +assert turtle.dig() is True +assert turtle.dig() is False +assert turtle.digUp() is True +assert turtle.digUp() is False +assert turtle.digDown() is True +assert turtle.digDown() is False + +assert turtle.getItemCount() == 3 + +assert turtle.forward() is True +assert turtle.back() is True +assert turtle.up() is True +assert turtle.down() is True +assert turtle.turnLeft() is None +assert turtle.turnRight() is None + +assert turtle.place() is True +assert turtle.place() is False +assert turtle.placeUp() is True +assert turtle.placeUp() is False +assert turtle.placeDown() is True +with assert_raises(LuaException, 'No items to place'): + turtle.placeDown() + +step('Put 3 cobblestone blocks to slot 1') + +assert turtle.getItemCount(1) == 3 +assert turtle.getItemCount(2) == 0 + +assert turtle.compare() is True +assert turtle.compareUp() is True +assert turtle.compareDown() is True + +assert turtle.select(2) is None + +assert turtle.compare() is False +assert turtle.compareUp() is False +assert turtle.compareDown() is False + +assert turtle.select(1) is None + +assert turtle.transferTo(2, 1) is True +assert turtle.getItemCount(1) == 2 +assert turtle.getItemCount(2) == 1 +assert turtle.compareTo(2) is True + +assert turtle.transferTo(2) is True +assert turtle.getItemCount(1) == 0 +assert turtle.getItemCount(2) == 3 +assert turtle.compareTo(2) is False + +assert turtle.select(2) is None +assert turtle.transferTo(1) is True +assert turtle.select(1) is None +assert turtle.dig() is True +assert turtle.digUp() is True +assert turtle.digDown() is True +assert turtle.getItemCount() == 6 + +assert turtle.drop(1) is True +assert turtle.dropUp(1) is True +assert turtle.dropDown(1) is True +assert turtle.getItemCount() == 3 +assert turtle.drop() is True +assert turtle.getItemCount() == 0 +assert turtle.drop() is False + +step( + 'Collect dropped cobblestone\n' + 'Drop stack of sticks right in front of the turtle\n' + 'Its better to build 1-block room then throw sticks there', +) + +assert turtle.suck(1) is True +assert turtle.getItemCount() == 1 +assert turtle.suck() is True +assert turtle.getItemCount() == 64 +assert turtle.suck() is False +assert turtle.drop() is True +assert turtle.getItemCount() == 0 + +step( + 'Collect dropped sticks\n' + 'Drop stack of sticks right below the turtle\n' + 'Its better to build 1-block room then throw sticks there', +) + +assert turtle.suckDown(1) is True +assert turtle.getItemCount() == 1 +assert turtle.suckDown() is True +assert turtle.getItemCount() == 64 +assert turtle.suckDown() is False +assert turtle.dropDown() is True +assert turtle.getItemCount() == 0 + +step( + 'Collect dropped sticks\n' + 'Drop stack of sticks right above the turtle\n' + 'Its better to build 1-block room then throw sticks there', +) + +assert turtle.suckUp(1) is True +assert turtle.getItemCount() == 1 +assert turtle.suckUp() is True +assert turtle.getItemCount() == 64 +assert turtle.suckUp() is False +assert turtle.dropUp() is True +assert turtle.getItemCount() == 0 + + +def craft1(): + return turtle.craft() + + +def craft2(): + c = peripheral.wrap('right') + return await c.craft() + + +step('Put crafting table into slot 1') +assert turtle.select(1) is None +assert turtle.equipRight() is None + +for craft_fn in craft1, craft2: + step( + 'Clean inventory of turtle\n' + 'Put 8 cobblestones into slot 1', + ) + + assert turtle.select(1) is None + assert craft_fn() is False + for idx in [2, 3, 5, 7, 9, 10, 11]: + assert turtle.transferTo(idx, 1) + assert craft_fn() is True + assert craft_fn() is False + assert turtle.getItemDetail() == { + 'count': 1, + 'name': 'minecraft:furnace', + } + +print('Test finished successfully') diff --git a/examples/test_turtle_attack.py b/examples/test_turtle_attack.py new file mode 100644 index 0000000..f91e20b --- /dev/null +++ b/examples/test_turtle_attack.py @@ -0,0 +1,34 @@ +from cc import import_file, turtle + +_lib = import_file('_lib.py', __file__) + + +_lib.step( + 'NOTE: this test is unreliable\n' + 'Build 1x1x1 stone cage in front of turtle\n' + 'Spawn here a chicken', +) + +assert turtle.attack() is True +assert turtle.attack() is True +assert turtle.attack() is False + +_lib.step( + 'Build 1x1x1 stone cage below turtle\n' + 'Spawn here a chicken', +) + +assert turtle.attackDown() is True +assert turtle.attackDown() is True +assert turtle.attackDown() is False + +_lib.step( + 'Build 1x1x1 stone cage above turtle\n' + 'Spawn here a chicken', +) + +assert turtle.attackUp() is True +assert turtle.attackUp() is True +assert turtle.attackUp() is False + +print('Test finished successfully') diff --git a/examples/test_window.py b/examples/test_window.py new file mode 100644 index 0000000..7c10456 --- /dev/null +++ b/examples/test_window.py @@ -0,0 +1,36 @@ +from cc import colors, term, os, window + + +with window.create( + term.get_current_target(), + 15, 5, 5, 5, False, +) as win: + assert win.getPosition() == (15, 5) + assert win.getSize() == (5, 5) + + win.setBackgroundColor(colors.red) + win.clear() + win.setVisible(True) + + os.sleep(1) + + win.setVisible(False) + win.setCursorPos(1, 1) + win.setTextColor(colors.yellow) + win.write('*********') + win.setVisible(True) + + os.sleep(1) + + term.clear() + + os.sleep(1) + + win.redraw() + assert win.getLine(1) == ('*****', '44444', 'eeeee') + + # draws immediately + win.reposition(21, 5) + win.reposition(27, 5) + +print('Test finished successfully') diff --git a/requirements.txt b/requirements.txt index ee4ba4f..c30b004 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ aiohttp +greenlet diff --git a/setup.cfg b/setup.cfg index 9fea9ef..0afb1ad 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,3 @@ [flake8] max-line-length = 120 -ignore = I,C812,N802,N803,N815,W503 +ignore = I,C812,N802,N803,N815,N816,W503 diff --git a/setup.py b/setup.py index c03a7fa..1c8abb8 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ if is_register_command(argv[1:]): setup( name='computercraft', - version='0.2.0', + version='0.3.0', description='Pythonization of ComputerCraft Minecraft mod. Write Python instead Lua!', long_description=longdesc, url='https://github.com/neumond/python-computer-craft', @@ -38,7 +38,7 @@ setup( keywords='computercraft minecraft', packages=['computercraft', 'computercraft.subapis'], package_data={'computercraft': ['back.lua']}, - install_requires=['aiohttp'], + install_requires=['aiohttp', 'greenlet'], entry_points={ 'console_scripts': ['computercraft = computercraft.server:main'], }, diff --git a/testmod.py b/testmod.py deleted file mode 100644 index f14f4b6..0000000 --- a/testmod.py +++ /dev/null @@ -1,2171 +0,0 @@ -import asyncio -import random -from contextlib import contextmanager, AsyncExitStack -from time import monotonic -from types import FunctionType - -from computercraft.errors import LuaException - - -async def hello(api): - await api.print('Hello world!') - - -async def id(api): - await api.print('ID', await api.os.getComputerID()) - await api.print('Label', await api.os.getComputerLabel()) - await api.print('Version', await api.os.version()) - - -async def move(api): - for _ in range(4): - await api.turtle.forward() - await api.turtle.turnLeft() - - -async def t1(api): - await api.print('kek') - result = await api.eval("return 'jopa\\njopa'") - await api.print(f'{result}') - raise IndexError - - -async def read(api): - line = await api.read_line() - await api.print(f'Entered line: {line}') - - -@contextmanager -def assert_raises(etype, message=None): - try: - yield - except Exception as e: - assert isinstance(e, etype) - if message is not None: - assert e.args == (message, ) - else: - raise AssertionError(f'Exception of type {etype} was not raised') - - -@contextmanager -def assert_takes_time(at_least, at_most): - t = monotonic() - yield - dt = monotonic() - t - # print(at_least, '<=', dt, '<=', at_most) - assert at_least <= dt <= at_most - - -class AnyInstanceOf: - def __init__(self, cls): - self.c = cls - - def __eq__(self, other): - return isinstance(other, self.c) - - -async def step(api, text): - await api.print(f'{text} [enter]') - await api.read_line() - - -async def get_object_table(api, objname): - r = await api.eval(f""" -local r = {{}} -for k in pairs({objname}) do - local t = type({objname}[k]) - if r[t] == nil then r[t] = {{}} end - if t == 'number' or t == 'boolean' or t == 'string' then - r[t][k] = {objname}[k] - else - r[t][k] = true - end -end -return r""") - assert len(r) == 1 - return r[0] - - -def get_class_table(cls): - items = { - k: v for k, v in vars(cls).items() - if not k.startswith('_') - } - nums = { - k: v for k, v in items.items() - if isinstance(v, (int, float)) - } - methods = { - k: True for k, v in items.items() - if isinstance(v, FunctionType) - } - r = {} - if nums: - r['number'] = nums - if methods: - r['function'] = methods - return r - - -def get_multiclass_table(*cls): - result = {} - for c in cls: - for k, v in get_class_table(c).items(): - result.setdefault(k, {}).update(v) - return result - - -async def test_colors_api(api): - tbl = await get_object_table(api, 'colors') - - # use packRGB and unpackRGB - del tbl['function']['rgb8'] - - assert get_class_table(api.colors.__class__) == tbl - - cs = await api.colors.combine( - api.colors.orange, - api.colors.cyan, - api.colors.pink, - api.colors.brown, - ) - assert isinstance(cs, int) - cs = await api.colors.subtract(cs, api.colors.brown, api.colors.green) - assert isinstance(cs, int) - assert cs == await api.colors.combine( - api.colors.orange, - api.colors.cyan, - api.colors.pink, - ) - assert await api.colors.test(cs, api.colors.red) is False - assert await api.colors.test(cs, api.colors.cyan) is True - - assert await api.colors.packRGB(0.7, 0.2, 0.6) == 0xb23399 - r, g, b = await api.colors.unpackRGB(0xb23399) - assert 0.68 < r < 0.72 - assert 0.18 < g < 0.22 - assert 0.58 < b < 0.62 - - await api.print('Test finished successfully') - - -async def test_disk_api(api): - s = 'right' - - assert get_class_table(api.disk.__class__) \ - == await get_object_table(api, 'disk') - - await step(api, f'Make sure there is no disk drive at {s} side') - - assert await api.disk.isPresent(s) is False - assert await api.disk.hasData(s) is False - assert await api.disk.getMountPath(s) is None - assert await api.disk.setLabel(s, 'text') is None - assert await api.disk.getLabel(s) is None - assert await api.disk.getID(s) is None - assert await api.disk.hasAudio(s) is False - assert await api.disk.getAudioTitle(s) is None - assert await api.disk.playAudio(s) is None - assert await api.disk.stopAudio(s) is None - assert await api.disk.eject(s) is None - - await step(api, f'Place empty disk drive at {s} side') - - assert await api.disk.isPresent(s) is False - assert await api.disk.hasData(s) is False - assert await api.disk.getMountPath(s) is None - assert await api.disk.setLabel(s, 'text') is None - assert await api.disk.getLabel(s) is None - assert await api.disk.getID(s) is None - assert await api.disk.hasAudio(s) is False - assert await api.disk.getAudioTitle(s) is False # False instead None! - assert await api.disk.playAudio(s) is None - assert await api.disk.stopAudio(s) is None - assert await api.disk.eject(s) is None - - await step(api, 'Put new CC diskette into disk drive') - - assert await api.disk.isPresent(s) is True - assert await api.disk.hasData(s) is True - assert isinstance(await api.disk.getMountPath(s), str) - assert isinstance(await api.disk.getID(s), int) - - assert await api.disk.getLabel(s) is None - assert await api.disk.setLabel(s, 'label') is None - assert await api.disk.getLabel(s) == 'label' - assert await api.disk.setLabel(s, None) is None - assert await api.disk.getLabel(s) is None - - assert await api.disk.hasAudio(s) is False - assert await api.disk.getAudioTitle(s) is None - assert await api.disk.playAudio(s) is None - assert await api.disk.stopAudio(s) is None - - assert await api.disk.eject(s) is None - - await step(api, 'Put any audio disk into disk drive') - - assert await api.disk.isPresent(s) is True - assert await api.disk.hasData(s) is False - assert await api.disk.getMountPath(s) is None - assert await api.disk.getID(s) is None - assert await api.disk.hasAudio(s) is True - - label = await api.disk.getAudioTitle(s) - assert isinstance(label, str) - assert label != 'label' - await api.print(f'Label is {label}') - assert await api.disk.getLabel(s) == label - with assert_raises(LuaException): - assert await api.disk.setLabel(s, 'label') is None - with assert_raises(LuaException): - assert await api.disk.setLabel(s, None) is None - # no effect - assert await api.disk.getLabel(s) == label - - assert await api.disk.playAudio(s) is None - - await step(api, 'Audio must be playing now') - - assert await api.disk.stopAudio(s) is None - assert await api.disk.eject(s) is None - - await api.print('Test finished successfully') - - -async def test_commands_api(api): - tbl = await get_object_table(api, 'commands.native') - # remove in favor of exec - del tbl['function']['execAsync'] - assert get_class_table(api.commands.__class__) == tbl - - xyz = await api.commands.getBlockPosition() - - assert len(xyz) == 3 - for c in xyz: - assert isinstance(c, int) - - expected_binfo = { - 'state': { - 'state': AnyInstanceOf(str), - 'facing': AnyInstanceOf(str), - }, - 'name': 'computercraft:computer_command', - 'nbt': { - 'x': xyz[0], - 'y': xyz[1], - 'z': xyz[2], - 'ComputerId': AnyInstanceOf(int), - 'id': 'computercraft:computer_command', - 'On': 1, - }, - 'tags': {}, - } - - assert await api.commands.getBlockInfo(*xyz) == expected_binfo - assert await api.commands.getBlockInfos(*xyz, *xyz) == [expected_binfo] - - cmdlist = await api.commands.list() - - assert len(cmdlist) > 0 - for c in cmdlist: - assert isinstance(c, str) - - assert await api.commands.exec('say Hello!') == (True, [], AnyInstanceOf(int)) - - d = await api.commands.exec('tp hajejndlasksdkelefsns fjeklaskslekffjslas') - assert d[0] is False - - d = await api.commands.exec('difficulty') - assert d[0] is True - assert len(d[1]) == 1 - assert d[1][0].startswith('The difficulty is ') - assert isinstance(d[2], int) - - await api.print('Test finished successfully') - - -async def test_fs_api(api): - assert get_class_table(api.fs.__class__) \ - == await get_object_table(api, 'fs') - - for name in ('tdir', 'tfile'): - if await api.fs.exists(name): - await api.fs.delete(name) - - assert await api.fs.makeDir('tdir') is None - async with api.fs.open('tfile', 'w') as f: - await f.writeLine('textline') - - dlist = set(await api.fs.list('.')) - - assert {'tdir', 'tfile', 'rom'}.issubset(dlist) - assert await api.fs.list('tdir') == [] - - capacity = await api.fs.getCapacity('.') - free = await api.fs.getFreeSpace('.') - assert isinstance(capacity, int) - assert isinstance(free, int) - assert free < capacity - assert free > 0 - assert capacity > 0 - - assert await api.fs.exists('tdir') is True - assert await api.fs.exists('tfile') is True - assert await api.fs.exists('doesnotexist') is False - - assert await api.fs.isDir('tdir') is True - assert await api.fs.isDir('tfile') is False - assert await api.fs.isDir('doesnotexist') is False - - assert await api.fs.isReadOnly('rom') is True - assert await api.fs.isReadOnly('tdir') is False - assert await api.fs.isReadOnly('tfile') is False - assert await api.fs.isReadOnly('doesnotexist') is False - - assert await api.fs.getDrive('rom') == 'rom' - assert await api.fs.getDrive('tdir') == 'hdd' - assert await api.fs.getDrive('tfile') == 'hdd' - assert await api.fs.getDrive('doesnotexist') is None - - assert await api.fs.isDriveRoot('/') is True - assert await api.fs.isDriveRoot('rom') is True - assert await api.fs.isDriveRoot('tdir') is False - assert await api.fs.isDriveRoot('tfile') is False - assert await api.fs.isDriveRoot('doesnotexist') is True # wtf? - - assert await api.fs.getName('a/b/c/d') == 'd' - assert await api.fs.getName('a/b/c/') == 'c' - assert await api.fs.getName('/a/b/c/d') == 'd' - assert await api.fs.getName('///a/b/c/d') == 'd' - assert await api.fs.getName('') == 'root' # wtf? - assert await api.fs.getName('/') == 'root' - assert await api.fs.getName('///') == 'root' - assert await api.fs.getName('.') == 'root' - assert await api.fs.getName('..') == '..' - assert await api.fs.getName('../../..') == '..' - - assert await api.fs.getDir('a/b/c/d') == 'a/b/c' - assert await api.fs.getDir('a/b/c/') == 'a/b' - assert await api.fs.getDir('/a/b/c/d') == 'a/b/c' - assert await api.fs.getDir('///a/b/c/d') == 'a/b/c' - assert await api.fs.getDir('') == '..' - assert await api.fs.getDir('/') == '..' - assert await api.fs.getDir('///') == '..' - assert await api.fs.getDir('.') == '..' - assert await api.fs.getDir('..') == '' - assert await api.fs.getDir('../../..') == '../..' - - assert await api.fs.combine('a', 'b') == 'a/b' - assert await api.fs.combine('a/', 'b') == 'a/b' - assert await api.fs.combine('a//', 'b') == 'a/b' - assert await api.fs.combine('a/', '/b') == 'a/b' - assert await api.fs.combine('a/b/c', '..') == 'a/b' - assert await api.fs.combine('a/b/c', '../..') == 'a' - assert await api.fs.combine('a/b/c', '../../..') == '' - assert await api.fs.combine('a/b/c', '../../../..') == '..' - assert await api.fs.combine('a/b/c', '../../../../..') == '../..' - assert await api.fs.combine('/a/b/c', '../../../../..') == '../..' - assert await api.fs.combine('a/b/c', '////') == 'a/b/c' - assert await api.fs.combine('a/b/c', '.') == 'a/b/c' - assert await api.fs.combine('a/b/c', './.') == 'a/b/c' - assert await api.fs.combine('a/b/c', './../.') == 'a/b' - - assert await api.fs.getSize('tfile') == 9 - assert await api.fs.getSize('tdir') == 0 - with assert_raises(LuaException): - await api.fs.getSize('doesnotexist') - - assert await api.fs.move('tfile', 'tdir/apple') is None - assert await api.fs.list('tdir') == ['apple'] - assert await api.fs.copy('tdir/apple', 'tdir/banana') is None - assert await api.fs.list('tdir/') == ['apple', 'banana'] - assert await api.fs.copy('tdir/apple', 'tdir/cherry') is None - - assert await api.fs.getSize('tdir') == 0 - - dlist = set(await api.fs.find('*')) - assert 'tdir' in dlist - assert 'rom' in dlist - assert 'tfile' not in dlist - assert 'tdir/apple' not in dlist - - dlist = set(await api.fs.find('tdir/*')) - assert dlist == {'tdir/apple', 'tdir/banana', 'tdir/cherry'} - - dlist = set(await api.fs.find('tdir/*a*')) - assert dlist == {'tdir/apple', 'tdir/banana'} - - dlist = set(await api.fs.find('**')) - assert 'tdir' in dlist - assert 'tdir/apple' not in dlist # not recursive - - dlist = set(await api.fs.list('')) - assert 'tfile' not in dlist - assert 'tdir' in dlist - assert 'rom' in dlist - - dlist = set(await api.fs.list('tdir')) - assert dlist == {'apple', 'banana', 'cherry'} - - assert await api.fs.attributes('tdir/banana') == { - 'created': AnyInstanceOf(int), - 'modification': AnyInstanceOf(int), - 'isDir': False, - 'size': 9, - } - assert await api.fs.attributes('tdir') == { - 'created': AnyInstanceOf(int), - 'modification': AnyInstanceOf(int), - 'isDir': True, - 'size': 0, - } - with assert_raises(LuaException): - await api.fs.attributes('doesnotexist') - - assert await api.fs.complete('ba', 'tdir') == ['nana'] - assert await api.fs.complete('ap', 'tdir') == ['ple'] - assert await api.fs.complete('c', 'tdir') == ['herry'] - assert await api.fs.complete('td', '') == ['ir/', 'ir'] - assert await api.fs.complete('td', '', includeDirs=True) == ['ir/', 'ir'] - assert await api.fs.complete('td', '', includeDirs=False) == ['ir/'] # wtf? - assert await api.fs.complete('ap', 'tdir', includeFiles=True) == ['ple'] - assert await api.fs.complete('ap', 'tdir', includeFiles=False) == [] - - assert await api.fs.getSize('tdir/banana') == 9 - async with api.fs.open('tdir/banana', 'r') as f: - assert await get_object_table(api, f.get_expr_code()) == {'function': { - 'close': True, - 'read': True, - 'readLine': True, - 'readAll': True, - }} - assert await f.read(4) == 'text' - assert await f.readLine() == 'line' - assert await f.read(1) is None - assert await f.readLine() is None - assert await f.readAll() == '' - assert await f.readAll() == '' - assert await api.fs.getSize('tdir/banana') == 9 - async with api.fs.open('tdir/banana', 'a') as f: - assert await get_object_table(api, f.get_expr_code()) == {'function': { - 'close': True, - 'write': True, - 'writeLine': True, - 'flush': True, - }} - assert await f.write('x') is None - assert await api.fs.getSize('tdir/banana') == 10 - async with api.fs.open('tdir/banana', 'w') as f: - pass - assert await api.fs.getSize('tdir/banana') == 0 # truncate - async with api.fs.open('tdir/banana', 'w') as f: - assert await get_object_table(api, f.get_expr_code()) == {'function': { - 'close': True, - 'write': True, - 'writeLine': True, - 'flush': True, - }} - assert await f.write('Bro') is None - assert await f.writeLine('wn fox jumps') is None - assert await api.fs.getSize('tdir/banana') == 0 # changes are not on a disk - assert await f.flush() is None - assert await api.fs.getSize('tdir/banana') == len('Brown fox jumps\n') - assert await f.write('ov') is None - assert await f.write('er ') is None - assert await f.write('a lazy') is None - assert await f.writeLine(' dog.') is None - assert await api.fs.getSize('tdir/banana') > 9 - async with api.fs.open('tdir/banana', 'r') as f: - assert await f.readAll() == 'Brown fox jumps\nover a lazy dog.' # no newline? - with assert_raises(LuaException): - async with api.fs.open('tdir/banana', 'rw') as f: - pass - - assert await api.fs.exists('tdir/banana') is True - - async with api.fs.open('tdir/binfile', 'wb') as f: - assert await f.write('a' * 9) is None - assert await f.seek() == 9 - assert await f.seek('set', 0) == 0 - assert await f.write('b' * 3) is None - assert await f.seek('cur', -1) == 2 - assert await f.write('c' * 3) is None - assert await f.seek('end') == 9 - assert await f.write('d' * 3) is None - with assert_raises(LuaException): - await f.seek('set', -10) - - async with api.fs.open('tdir/binfile', 'rb') as f: - assert await f.readAll() == 'bbcccaaaaddd' - - async with api.fs.open('tdir/binfile', 'rb') as f: - assert isinstance(await f.read(), int) - - async with api.fs.open('tdir/binfile', 'r') as f: - assert [line async for line in f] == ['bbcccaaaaddd'] - - assert await api.fs.delete('tdir') is None - assert await api.fs.delete('tfile') is None - assert await api.fs.delete('doesnotexist') is None - - assert await api.fs.exists('tdir/banana') is False - - await api.print('Test finished successfully') - - -async def test_gps_basic_computer(api): - assert get_class_table(api.gps.__class__) \ - == await get_object_table(api, 'gps') - - assert await api.gps.locate() is None - - await step(api, 'Attach wireless modem to computer') - - assert await api.gps.locate() is None - - assert await api.gps.locate(debug=True) is None - - assert await api.gps.locate(timeout=5, debug=True) is None - - await api.print('Test finished successfully') - - -async def test_gps_command_computer(api): - assert get_class_table(api.gps.__class__) \ - == await get_object_table(api, 'gps') - - assert await api.gps.locate() == ( - AnyInstanceOf(int), - AnyInstanceOf(int), - AnyInstanceOf(int), - ) - - await api.print('Test finished successfully') - - -async def test_keys_api(api): - a = await api.keys.getCode('a') - space = await api.keys.getCode('space') - enter = await api.keys.getCode('enter') - assert await api.keys.getCode('doesnotexist') is None - assert await api.keys.getCode('getName') is None - assert isinstance(a, int) - assert isinstance(space, int) - assert isinstance(enter, int) - - assert await api.keys.getName(a) == 'a' - assert await api.keys.getName(space) == 'space' - assert await api.keys.getName(enter) == 'enter' - - # for i in range(255): - # print(i, await api.keys.getName(i)) - - await api.print('Test finished successfully') - - -async def test_help_api(api): - assert get_class_table(api.help.__class__) \ - == await get_object_table(api, 'help') - - await api.help.setPath('/rom/help') - - assert await api.help.path() == '/rom/help' - - assert await api.help.lookup('disk') == 'rom/help/disk.txt' - assert await api.help.lookup('abracadabra') is None - - ts = await api.help.topics() - assert isinstance(ts, list) - assert len(ts) > 2 - # print(ts) - assert 'disk' in ts - - assert await api.help.completeTopic('di') == ['sk'] - assert await api.help.completeTopic('abracadabra') == [] - - assert await api.help.setPath('/kek') is None - assert await api.help.path() == '/kek' - assert await api.help.topics() == ['index'] - assert await api.help.setPath('/rom/help') is None - - await api.print('Test finished successfully') - - -async def test_reboot(api): - assert await api.os.reboot() is None - await api.print('Test finished successfully') - - -async def test_shutdown(api): - assert await api.os.shutdown() is None - await api.print('Test finished successfully') - - -async def test_os_api(api): - tbl = await get_object_table(api, 'os') - - # use methods with get* - del tbl['function']['computerID'] - del tbl['function']['computerLabel'] - - # use captureEvent - del tbl['function']['pullEvent'] - del tbl['function']['pullEventRaw'] - - # we are in python world, loading lua modules is useless - del tbl['function']['loadAPI'] - del tbl['function']['unloadAPI'] - - # remove complex date formatting function in favor of python stdlib - del tbl['function']['date'] - - tbl['function']['captureEvent'] = True - - assert get_class_table(api.os.__class__) == tbl - - with assert_takes_time(1.5, 3): - async with api.os.captureEvent('timer') as timer_queue: - timer_id = await api.os.startTimer(2) - async for etid, *_ in timer_queue: - if etid == timer_id: - await api.print('Timer reached') - break - - timer_id = await api.os.startTimer(20) - assert isinstance(timer_id, int) - assert await api.os.cancelTimer(timer_id) is None - assert await api.os.cancelTimer(timer_id) is None - - alarm_id = await api.os.setAlarm(0.0) - assert isinstance(alarm_id, int) - assert await api.os.cancelAlarm(alarm_id) is None - assert await api.os.cancelAlarm(alarm_id) is None - - with assert_takes_time(1.5, 3): - assert await api.os.sleep(2) is None - - assert (await api.os.version()).startswith('CraftOS ') - assert isinstance(await api.os.getComputerID(), int) - - assert await api.os.setComputerLabel(None) is None - assert await api.os.getComputerLabel() is None - assert await api.os.setComputerLabel('altair') is None - assert await api.os.getComputerLabel() == 'altair' - assert await api.os.setComputerLabel(None) is None - assert await api.os.getComputerLabel() is None - - assert isinstance(await api.os.epoch(), int) - assert isinstance(await api.os.day(), int) - assert isinstance(await api.os.time(), (int, float)) - assert isinstance(await api.os.clock(), (int, float)) - - assert await api.os.run({}, 'rom/programs/fun/hello.lua') is True - - await api.print('Test finished successfully') - - -async def test_parallel(api): - with assert_takes_time(1.5, 3): - # Since os.sleep is mostly waiting for events, it doesn't block execution of parallel threads - # and this snippet takes approximately 2 seconds to complete. - await asyncio.gather(api.os.sleep(2), api.os.sleep(2)) - - await api.print('Test finished successfully') - - -async def term_step(api, text): - for color in api.colors: - r, g, b = await api.term.nativePaletteColor(color) - await api.term.setPaletteColor(color, r, g, b) - await api.term.setBackgroundColor(api.colors.black) - await api.term.setTextColor(api.colors.white) - await api.term.clear() - await api.term.setCursorPos(1, 1) - await api.term.setCursorBlink(True) - await step(api, text) - - -async def test_term_api(api): - from computercraft.subapis.mixins import TermMixin - - tbl = await get_object_table(api, 'term') - - # not defined in TermMixin - del tbl['function']['redirect'] - del tbl['function']['current'] - del tbl['function']['native'] - - # remove British method names to make API lighter - del tbl['function']['getBackgroundColour'] - del tbl['function']['getPaletteColour'] - del tbl['function']['getTextColour'] - del tbl['function']['isColour'] - del tbl['function']['nativePaletteColour'] - del tbl['function']['setBackgroundColour'] - del tbl['function']['setPaletteColour'] - del tbl['function']['setTextColour'] - - assert get_class_table(TermMixin) == tbl - - await step(api, 'Detach all monitors\nUse advanced computer for colors\nScreen will be cleared') - - assert await api.term.getSize() == (51, 19) - assert await api.term.isColor() is True - assert await api.term.clear() is None - assert await api.term.setCursorPos(1, 1) is None - assert await api.term.getCursorPos() == (1, 1) - assert await api.term.write('Alpha') is None - assert await api.term.getCursorPos() == (6, 1) - assert await api.term.setCursorBlink(False) is None - assert await api.term.getCursorBlink() is False - assert await api.term.setCursorBlink(True) is None - assert await api.term.getCursorBlink() is True - await asyncio.sleep(2) - - await term_step(api, 'You must have seen word Alpha with blinking cursor') - - assert await api.term.clear() is None - for offs, (tc, bc) in enumerate(( - (api.colors.lime, api.colors.green), - (api.colors.yellow, api.colors.brown), - (api.colors.red, api.colors.orange), - ), start=1): - assert await api.term.setTextColor(tc) is None - assert await api.term.getTextColor() == tc - assert await api.term.setBackgroundColor(bc) is None - assert await api.term.getBackgroundColor() == bc - assert await api.term.setCursorPos(offs * 2, offs) is None - assert await api.term.getCursorPos() == (offs * 2, offs) - assert await api.term.write('text with colors') is None - assert await api.term.setBackgroundColor(api.colors.black) is None - await asyncio.sleep(1) - for i in range(3): - assert await api.term.scroll(-2) is None - await asyncio.sleep(0.5) - for i in range(6): - assert await api.term.scroll(1) is None - await asyncio.sleep(0.25) - - await term_step(api, 'You must have seen three texts with different colors scrolling') - - assert await api.term.clear() is None - for i in range(1, 10): - assert await api.term.setCursorPos(1, i) is None - assert await api.term.write((str(i) + ' ') * 10) is None - await asyncio.sleep(2) - for i in range(2, 10, 2): - assert await api.term.setCursorPos(1, i) is None - assert await api.term.clearLine() is None - await asyncio.sleep(2) - - await term_step(api, 'You must have seen some lines disappearing') - - assert await api.term.clear() is None - assert await api.term.setCursorPos(1, 1) is None - assert await api.term.blit( - 'rainbowrainbow', - 'e14d3ba0000000', - 'fffffffe14d3ba', - ) is None - await asyncio.sleep(3) - - await term_step(api, 'You must have seen per-letter colored text') - - assert await api.term.setBackgroundColor(api.colors.white) is None - assert await api.term.clear() is None - assert await api.term.setCursorPos(1, 1) is None - for i, color in enumerate(api.colors): - await api.term.setPaletteColor(color, i / 15, 0, 0) - assert await api.term.blit( - ' redtextappears!', - '0123456789abcdef', - '0000000000000000', - ) is None - await asyncio.sleep(3) - - await term_step(api, 'You must have seen different shades of red made using palettes') - - await api.print('Test finished successfully') - - -async def test_settings_api(api): - tbl = await get_object_table(api, 'settings') - assert get_class_table(api.settings.__class__) == tbl - - await step(api, 'Settings will be cleared') - - assert await api.settings.clear() is None - # names are not empty, there are system settings - assert isinstance(await api.settings.getNames(), list) - - assert await api.settings.define('test.a') is None - assert await api.settings.define('test.b', description='b') is None - assert await api.settings.define('test.c', type='string') is None - assert await api.settings.define('test.d', default=42) is None - - assert await api.settings.getDetails('test.a') == { - 'changed': False, - } - assert await api.settings.getDetails('test.b') == { - 'changed': False, - 'description': 'b', - } - assert await api.settings.getDetails('test.c') == { - 'changed': False, - 'type': 'string', - } - assert await api.settings.getDetails('test.d') == { - 'changed': False, - 'default': 42, - 'value': 42, - } - - # redefining - assert await api.settings.define('test.a', type='number', default=11) is None - - assert await api.settings.getDetails('test.a') == { - 'changed': False, - 'type': 'number', - 'default': 11, - 'value': 11, - } - - assert await api.settings.get('test.a') == 11 - assert await api.settings.set('test.a', 12) is None - assert await api.settings.get('test.a') == 12 - with assert_raises(LuaException): - await api.settings.set('test.a', 'text') - assert await api.settings.get('test.a') == 12 - assert await api.settings.unset('test.a') is None - assert await api.settings.get('test.a') == 11 - - assert await api.settings.set('test.c', 'hello') is None - - assert {'test.a', 'test.b', 'test.c', 'test.d'}.issubset(set(await api.settings.getNames())) - - assert await api.settings.undefine('test.a') is None - assert await api.settings.undefine('test.b') is None - assert await api.settings.undefine('test.c') is None - assert await api.settings.undefine('test.d') is None - - assert 'test.c' in await api.settings.getNames() - assert await api.settings.get('test.c') == 'hello' - assert await api.settings.getDetails('test.c') == { - 'changed': True, - 'value': 'hello', - } - - assert await api.settings.unset('test.c') is None - - assert await api.settings.get('test.c') is None - assert await api.settings.getDetails('test.c') == { - 'changed': False, - } - - assert {'test.a', 'test.b', 'test.c', 'test.d'} & set(await api.settings.getNames()) == set() - - assert await api.settings.set('test.e', [9, 'text', False]) is None - assert await api.settings.get('test.e') == [9, 'text', False] - assert await api.settings.clear() is None - assert await api.settings.get('test.e') is None - - await api.fs.delete('.settings') - - assert await api.settings.load() is False - assert await api.settings.save() is True - assert await api.settings.load() is True - - await api.fs.delete('.settings') - - assert await api.settings.set('key', 84) is None - - assert await api.settings.save('sfile') is True - assert await api.settings.load('sfile') is True - - await api.fs.delete('sfile') - - await api.print('Test finished successfully') - - -async def test_redstone_api(api): - tbl = await get_object_table(api, 'redstone') - - # remove British method names to make API lighter - del tbl['function']['getAnalogueInput'] - del tbl['function']['getAnalogueOutput'] - del tbl['function']['setAnalogueOutput'] - - assert get_class_table(api.redstone.__class__) == tbl - - assert set(await api.redstone.getSides()) == {'top', 'bottom', 'front', 'back', 'left', 'right'} - - await step(api, 'Remove all the redstone from sides of computer') - - side = 'top' - - assert await api.redstone.setOutput(side, True) is None - assert await api.redstone.getOutput(side) is True - assert await api.redstone.getAnalogOutput(side) == 15 - assert await api.redstone.setOutput(side, False) is None - assert await api.redstone.getOutput(side) is False - assert await api.redstone.getAnalogOutput(side) == 0 - - assert await api.redstone.setAnalogOutput(side, 7) is None - assert await api.redstone.getAnalogOutput(side) == 7 - assert await api.redstone.getOutput(side) is True - assert await api.redstone.setAnalogOutput(side, 15) is None - assert await api.redstone.getAnalogOutput(side) == 15 - assert await api.redstone.setAnalogOutput(side, 0) is None - assert await api.redstone.getAnalogOutput(side) == 0 - assert await api.redstone.getOutput(side) is False - - assert await api.redstone.getInput(side) is False - assert await api.redstone.getAnalogInput(side) == 0 - - await step(api, f'Put redstone block on {side} side of computer') - - assert await api.redstone.getInput(side) is True - assert await api.redstone.getAnalogInput(side) > 0 - - await step(api, f'Remove redstone block\nPut piston on {side} side of computer') - - assert await api.redstone.getInput(side) is False - assert await api.redstone.getAnalogInput(side) == 0 - assert await api.redstone.setOutput(side, True) is None - await asyncio.sleep(2) - assert await api.redstone.setOutput(side, False) is None - - await api.print('Piston must have been activated\nRemove piston') - - await api.print('Test finished successfully') - - -async def test_peripheral_api(api): - tbl = await get_object_table(api, 'peripheral') - - # use wrap - del tbl['function']['getMethods'] - del tbl['function']['call'] - - # TODO: support these methods - del tbl['function']['getName'] - del tbl['function']['find'] - - assert get_class_table(api.peripheral.__class__) == tbl - - await step(api, 'Remove all peripherals') - - side = 'top' - - assert await api.peripheral.getNames() == [] - assert await api.peripheral.getType(side) is None - assert await api.peripheral.isPresent(side) is False - assert await api.peripheral.wrap(side) is None - - await step(api, f'Put disk drive on {side} side of computer') - - assert await api.peripheral.getNames() == [side] - assert await api.peripheral.getType(side) == 'drive' - assert await api.peripheral.isPresent(side) is True - d = await api.peripheral.wrap(side) - assert d is not None - assert await d.isDiskPresent() is False - - await api.print('Remove disk drive') - - await api.print('Test finished successfully') - - -async def test_disk_peripheral(api): - side = 'left' - - await step(api, f'Put empty disk drive on {side} side of computer') - - d = await api.peripheral.wrap(side) - assert d is not None - - from computercraft.subapis.peripheral import CCDrive - tbl = await get_object_table(api, f'peripheral.wrap("{side}")') - assert get_class_table(CCDrive) == tbl - - assert await d.isDiskPresent() is False - assert await d.hasData() is False - assert await d.getMountPath() is None - assert await d.setDiskLabel('text') is None - assert await d.getDiskLabel() is None - assert await d.getDiskID() is None - assert await d.hasAudio() is False - assert await d.getAudioTitle() is False # False instead None! - assert await d.playAudio() is None - assert await d.stopAudio() is None - assert await d.ejectDisk() is None - - await step(api, 'Put new CC diskette into disk drive') - - assert await d.isDiskPresent() is True - assert await d.hasData() is True - assert isinstance(await d.getMountPath(), str) - assert isinstance(await d.getDiskID(), int) - - assert await d.getDiskLabel() is None - assert await d.setDiskLabel('label') is None - assert await d.getDiskLabel() == 'label' - assert await d.setDiskLabel(None) is None - assert await d.getDiskLabel() is None - - assert await d.hasAudio() is False - assert await d.getAudioTitle() is None - assert await d.playAudio() is None - assert await d.stopAudio() is None - - assert await d.ejectDisk() is None - - await step(api, 'Put any audio disk into disk drive') - - assert await d.isDiskPresent() is True - assert await d.hasData() is False - assert await d.getMountPath() is None - assert await d.getDiskID() is None - assert await d.hasAudio() is True - - label = await d.getAudioTitle() - assert isinstance(label, str) - assert label != 'label' - await api.print(f'Label is {label}') - assert await d.getDiskLabel() == label - with assert_raises(LuaException): - assert await d.setDiskLabel('label') is None - with assert_raises(LuaException): - assert await d.setDiskLabel(None) is None - # no effect - assert await d.getDiskLabel() == label - - assert await d.playAudio() is None - - await step(api, 'Audio must be playing now') - - assert await d.stopAudio() is None - assert await d.ejectDisk() is None - - await api.print('Test finished successfully') - - -async def test_monitor_peripheral(api): - side = 'left' - - await step( - api, - 'Use advanced computer and monitor for colors\n' - f'Place single block monitor on {side} side of computer', - ) - - m = await api.peripheral.wrap(side) - assert m is not None - - from computercraft.subapis.peripheral import CCMonitor - from computercraft.subapis.mixins import TermMixin - - tbl = await get_object_table(api, f'peripheral.wrap("{side}")') - - # remove British method names to make API lighter - del tbl['function']['getBackgroundColour'] - del tbl['function']['getPaletteColour'] - del tbl['function']['getTextColour'] - del tbl['function']['isColour'] - del tbl['function']['setBackgroundColour'] - del tbl['function']['setPaletteColour'] - del tbl['function']['setTextColour'] - # NOTE: peripheral doesn't have nativePaletteColor method - - assert get_multiclass_table(TermMixin, CCMonitor) == tbl - - assert await m.getSize() == (7, 5) - assert await m.isColor() is True - assert await m.setTextColor(api.colors.white) is None - assert await m.setBackgroundColor(api.colors.black) is None - assert await m.clear() is None - assert await m.setCursorPos(1, 1) is None - assert await m.getCursorPos() == (1, 1) - assert await m.write('Alpha') is None - assert await m.getCursorPos() == (6, 1) - assert await m.setCursorBlink(False) is None - assert await m.getCursorBlink() is False - assert await m.setCursorBlink(True) is None - assert await m.getCursorBlink() is True - - await step(api, 'You must have seen word Alpha with blinking cursor') - - assert await m.clear() is None - assert await m.setCursorBlink(False) is None - for offs, (tc, bc) in enumerate(( - (api.colors.lime, api.colors.green), - (api.colors.yellow, api.colors.brown), - (api.colors.red, api.colors.orange), - ), start=1): - assert await m.setTextColor(tc) is None - assert await m.getTextColor() == tc - assert await m.setBackgroundColor(bc) is None - assert await m.getBackgroundColor() == bc - assert await m.setCursorPos(offs, offs) is None - assert await m.getCursorPos() == (offs, offs) - assert await m.write('text') is None - assert await m.setBackgroundColor(api.colors.black) is None - await asyncio.sleep(1) - for i in range(2): - assert await m.scroll(-1) is None - await asyncio.sleep(0.5) - for i in range(2): - assert await m.scroll(1) is None - await asyncio.sleep(0.5) - - await step(api, 'You must have seen three texts with different colors scrolling') - - assert await m.setTextColor(api.colors.white) is None - assert await m.setBackgroundColor(api.colors.black) is None - assert await m.clear() is None - for i in range(1, 5): - assert await m.setCursorPos(1, i) is None - assert await m.write((str(i) + ' ') * 4) is None - await asyncio.sleep(2) - for i in range(2, 5, 2): - assert await m.setCursorPos(1, i) is None - assert await m.clearLine() is None - - await step(api, 'You must have seen some lines disappearing') - - assert await m.setBackgroundColor(api.colors.black) is None - assert await m.clear() is None - assert await m.setCursorPos(1, 1) is None - assert await m.blit( - 'rainbow', - 'e14d3ba', - 'fffffff', - ) is None - assert await m.setCursorPos(1, 2) is None - assert await m.blit( - 'rainbow', - '0000000', - 'e14d3ba', - ) is None - - await step(api, 'You must have seen per-letter colored text') - - assert await m.setBackgroundColor(api.colors.black) is None - assert await m.setTextColor(api.colors.white) is None - assert await m.getTextScale() == 1 - assert await m.setTextScale(5) is None - assert await m.getTextScale() == 5 - assert await m.setCursorPos(1, 1) is None - assert await m.clear() is None - assert await m.getSize() == (1, 1) - assert await m.write('AAA') is None - - await step(api, 'You must have seen single large letter A') - - assert await m.setTextScale(1) is None - assert await m.setBackgroundColor(api.colors.white) is None - assert await m.clear() is None - for i, color in enumerate(api.colors): - await m.setPaletteColor(color, i / 15, 0, 0) - assert await m.setCursorPos(1, 1) is None - assert await m.blit( - ' redtex', - '0123456', - '0000000', - ) is None - assert await m.setCursorPos(1, 2) is None - assert await m.blit( - 'tappear', - '789abcd', - '0000000', - ) is None - assert await m.setCursorPos(1, 3) is None - assert await m.blit( - 's!', - 'ef', - '00', - ) is None - - await step(api, 'You must have seen different shades of red made using palettes') - - await api.print('Remove monitor') - await api.print('Test finished successfully') - - -async def _computer_peri(api, place_thing, thing): - side = 'left' - - await step( - api, - f'Place {place_thing} on {side} side of computer\n' - "Don't turn it on!", - ) - - c = await api.peripheral.wrap(side) - assert c is not None - - from computercraft.subapis.peripheral import ComputerMixin - tbl = await get_object_table(api, f'peripheral.wrap("{side}")') - assert get_class_table(ComputerMixin) == tbl - - assert await c.isOn() is False - assert isinstance(await c.getID(), int) - assert await c.getLabel() is None - assert await c.turnOn() is None - - await step(api, f'{thing.capitalize()} must be turned on now') - - assert await c.shutdown() is None - - await step(api, f'{thing.capitalize()} must shutdown') - - await step(api, f'Now turn on {thing} manually and enter some commands') - - assert await c.reboot() is None - - await step(api, f'{thing.capitalize()} must reboot') - - await api.print('Test finished successfully') - - -async def test_computer_peripheral(api): - await _computer_peri(api, 'another computer', 'computer') - - -async def test_turtle_peripheral(api): - await _computer_peri(api, 'turtle', 'turtle') - - -async def modem_server(api): - side = 'back' - m = await api.peripheral.wrap(side) - listen_channel = 5 - async with m.receive(listen_channel) as q: - async for msg in q: - await api.print(repr(msg)) - if msg.content == 'stop': - break - else: - await m.transmit(msg.reply_channel, listen_channel, msg.content) - - -async def test_modem_peripheral(api): - # do this test twice: for wired and wireless modems - - side = 'back' - - await step( - api, - f'Attach modem to {side} side of computer\n' - f'Place another computer with similar modem at {side} side\n' - 'In case of wired modems connect them\n' - 'On another computer start py modem_server' - ) - - m = await api.peripheral.wrap(side) - - remote_channel = 5 - local_channel = 7 - - assert await m.isOpen(local_channel) is False - async with m.receive(local_channel) as q: - assert await m.isOpen(local_channel) is True - await m.transmit(remote_channel, local_channel, 1) - await m.transmit(remote_channel, local_channel, 'hi') - await m.transmit(remote_channel, local_channel, {'data': 5}) - await m.transmit(remote_channel, local_channel, 'stop') - - messages = [] - async for msg in q: - assert msg.reply_channel == remote_channel - assert msg.distance > 0 - messages.append(msg.content) - if len(messages) == 3: - break - - assert messages == [1, 'hi', {'data': 5}] - assert await m.isOpen(local_channel) is False - assert await m.closeAll() is None - assert isinstance(await m.isWireless(), bool) - - await api.print('Test finished successfully') - - -async def test_printer_peripheral(api): - side = 'left' - - await step(api, f'Attach empty printer at {side} side of computer') - - m = await api.peripheral.wrap(side) - - from computercraft.subapis.peripheral import CCPrinter - tbl = await get_object_table(api, f'peripheral.wrap("{side}")') - assert get_class_table(CCPrinter) == tbl - - assert await m.getPaperLevel() == 0 - assert await m.getInkLevel() == 0 - - # no paper - assert await m.newPage() is False - # page not started - with assert_raises(LuaException): - await m.endPage() - with assert_raises(LuaException): - await m.write('test') - with assert_raises(LuaException): - await m.setCursorPos(2, 2) - with assert_raises(LuaException): - await m.getCursorPos() - with assert_raises(LuaException): - await m.getPageSize() - with assert_raises(LuaException): - await m.setPageTitle('title') - - await step(api, 'Put paper into printer') - paper_level = await m.getPaperLevel() - assert paper_level > 0 - # no ink - assert await m.newPage() is False - - await step(api, 'Put ink into printer') - ink_level = await m.getInkLevel() - assert ink_level > 0 - - assert await m.newPage() is True - assert await m.getPaperLevel() < paper_level - assert await m.getInkLevel() < ink_level - - assert await m.setCursorPos(2, 2) is None - assert await m.getCursorPos() == (2, 2) - assert await m.setCursorPos(1, 1) is None - assert await m.getCursorPos() == (1, 1) - assert await m.setPageTitle('Green bottles') is None - assert await m.getPageSize() == (25, 21) - - async def row(n=1): - _, r = await m.getCursorPos() - await m.setCursorPos(1, r + n) - - def split_text(text, max_width=25): - for i in range(0, len(text), max_width): - yield text[i:i + max_width] - - def split_by_words(text, max_width=25): - stack = [] - stack_len = 0 - for word in text.split(' '): - assert len(word) <= max_width - with_word = len(word) if stack_len == 0 else stack_len + 1 + len(word) - if with_word > max_width: - yield ' '.join(stack) - stack.clear() - stack_len = 0 - else: - stack.append(word) - stack_len = with_word - if stack: - yield ' '.join(stack) - - async def multiline_write(text): - _, r = await m.getCursorPos() - for pt in split_by_words(text): - assert await m.setCursorPos(1, r) is None - assert await m.write(pt) is None - r += 1 - assert await m.setCursorPos(1, r) is None - - assert await m.write('Green bottles'.center(25)) is None - await row(2) - - x = 2 - while x > 0: - await multiline_write(f'{x} green bottles hanging on the wall') - await multiline_write(f'{x} green bottles hanging on the wall') - await multiline_write('if one green bottle accidently falls') - x -= 1 - await multiline_write(f'there will be {x} hanging on the wall') - await row() - - assert await m.endPage() is True - - await api.print('Test finished successfully') - - -async def test_speaker_peripheral(api): - side = 'left' - - await step(api, f'Attach speaker at {side} side of computer') - - m = await api.peripheral.wrap(side) - - from computercraft.subapis.peripheral import CCSpeaker - tbl = await get_object_table(api, f'peripheral.wrap("{side}")') - assert get_class_table(CCSpeaker) == tbl - - for _ in range(48): - assert await m.playNote( - random.choice([ - 'bass', 'basedrum', 'bell', 'chime', 'flute', 'guitar', 'hat', - 'snare', 'xylophone', 'iron_xylophone', 'pling', 'banjo', - 'bit', 'didgeridoo', 'cow_bell', - ]), - 3, - random.randint(0, 24) - ) is True - await asyncio.sleep(0.2) - - assert await m.playSound('minecraft:entity.player.levelup') is True - - await api.print('You must have heard notes and sounds') - await api.print('Test finished successfully') - - -async def test_commandblock_peripheral(api): - side = 'left' - - await step(api, f'Attach command block at {side} side of computer') - - m = await api.peripheral.wrap(side) - - from computercraft.subapis.peripheral import CCCommandBlock - tbl = await get_object_table(api, f'peripheral.wrap("{side}")') - assert get_class_table(CCCommandBlock) == tbl - - assert await m.getCommand() == '' - assert await m.setCommand('say Hello from python side') is None - assert await m.getCommand() == 'say Hello from python side' - assert await m.runCommand() is None - - assert await m.setCommand('time query daytime') is None - assert await m.getCommand() == 'time query daytime' - assert await m.runCommand() is None - - assert await m.setCommand('') is None - assert await m.getCommand() == '' - with assert_raises(LuaException): - await m.runCommand() - - await api.print('You must have seen chat message') - await api.print('Test finished successfully') - - -async def test_modem_wrap(api): - side = 'back' - - await step(api, f'Attach and disable (right-click) wired modem at {side} side') - - m = await api.peripheral.wrap(side) - assert await m.isWireless() is False - assert await m.getNameLocal() is None - - await step(api, f'Enable (right-click) wired modem at {side} side') - - assert isinstance(await m.getNameLocal(), str) - - await step(api, 'Connect networked speaker peripheral & enable its modem') - - names = await m.getNamesRemote() - assert isinstance(names, list) - assert len(names) > 0 - speaker = [] - for n in names: - assert isinstance(n, str) - if n.startswith('speaker_'): - speaker.append(n) - assert len(speaker) == 1 - speaker = speaker[0] - - assert await m.isPresentRemote('doesnotexist') is False - assert await m.getTypeRemote('doesnotexist') is None - - assert await m.isPresentRemote(speaker) is True - assert await m.getTypeRemote(speaker) == 'speaker' - - assert await m.wrapRemote('doesnotexist') is None - s = await m.wrapRemote(speaker) - - assert await s.playSound('minecraft:entity.player.levelup') is True - - await api.print('You must have heard levelup sound') - await api.print('Test finished successfully') - - -async def test_textutils(api): - assert await api.textutils.slowWrite('write ') is None - assert await api.textutils.slowWrite('write ', 5) is None - assert await api.textutils.slowPrint('print') is None - assert await api.textutils.slowPrint('print', 5) is None - - assert await api.textutils.formatTime(0) == '0:00 AM' - assert await api.textutils.formatTime(0, True) == '0:00' - - table = [ - api.colors.red, - ['Planet', 'Distance', 'Mass'], - api.colors.gray, - ['Mercury', '0.387', '0.055'], - api.colors.lightGray, - ['Venus', '0.723', '0.815'], - api.colors.green, - ['Earth', '1.000', '1.000'], - api.colors.red, - ['Mars', '1.524', '0.107'], - api.colors.orange, - ['Jupiter', '5.203', '318'], - api.colors.yellow, - ['Saturn', '9.537', '95'], - api.colors.cyan, - ['Uranus', '19.191', '14.5'], - api.colors.blue, - ['Neptune', '30.069', '17'], - api.colors.white, - ] - - assert await api.textutils.tabulate(*table) is None - - lines = await api.textutils.pagedPrint(''' -Lorem ipsum dolor sit amet, consectetur adipiscing elit. -Suspendisse feugiat diam et velit aliquam, nec porttitor eros facilisis. -Nulla facilisi. -Sed eget dui vel tellus aliquam fermentum. -Aliquam sed lorem congue, dignissim nulla in, porta diam. -Aliquam erat volutpat. - '''.strip()) - assert isinstance(lines, int) - assert lines > 0 - - assert await api.textutils.pagedTabulate(*table[:-1], *table[2:-1], *table[2:]) is None - - assert api.textutils.complete('co', ['command', 'row', 'column']) == [ - 'mmand', 'lumn'] - - await api.print('Test finished successfully') - - -async def test_pocket(api): - assert await api.peripheral.isPresent('back') is False - - from computercraft.subapis.pocket import PocketAPI - tbl = await get_object_table(api, 'pocket') - assert get_class_table(PocketAPI) == tbl - - await step(api, 'Clean inventory from any pocket upgrades') - - with assert_raises(LuaException): - await api.pocket.equipBack() - with assert_raises(LuaException): - await api.pocket.unequipBack() - assert await api.peripheral.isPresent('back') is False - - await step(api, 'Put any pocket upgrade to inventory') - - assert await api.pocket.equipBack() is None - assert await api.peripheral.isPresent('back') is True - - assert await api.pocket.unequipBack() is None - assert await api.peripheral.isPresent('back') is False - - await api.print('Test finished successfully') - - -async def test_multishell(api): - from computercraft.subapis.multishell import MultishellAPI - tbl = await get_object_table(api, 'multishell') - assert get_class_table(MultishellAPI) == tbl - - await step(api, 'Close all additional shells') - - assert await api.multishell.getCount() == 1 - assert await api.multishell.getCurrent() == 1 - assert await api.multishell.getFocus() == 1 - assert isinstance(await api.multishell.getTitle(1), str) - - title = f'new title {random.randint(1, 1000000)}' - assert await api.multishell.setTitle(1, title) is None - assert await api.multishell.getTitle(1) == title - - assert await api.multishell.setFocus(1) is True - assert await api.multishell.setFocus(0) is False - assert await api.multishell.setFocus(2) is False - - assert await api.multishell.getTitle(2) is None - - assert await api.multishell.launch({}, 'rom/programs/fun/hello.lua') == 2 - assert isinstance(await api.multishell.getTitle(2), str) - - await api.print('Test finished successfully') - - -async def test_shell(api): - from computercraft.subapis.shell import ShellAPI - tbl = await get_object_table(api, 'shell') - - del tbl['function']['setCompletionFunction'] - del tbl['function']['getCompletionInfo'] - assert get_class_table(ShellAPI) == tbl - - assert await api.shell.complete('ls ro') == ['m/', 'm'] - assert await api.shell.completeProgram('lu') == ['a'] - - ps = await api.shell.programs() - assert 'shutdown' in ps - - als = await api.shell.aliases() - assert 'ls' in als - assert als['ls'] == 'list' - assert 'xls' not in als - assert await api.shell.setAlias('xls', 'list') is None - als = await api.shell.aliases() - assert 'xls' in als - assert als['xls'] == 'list' - assert await api.shell.clearAlias('xls') is None - als = await api.shell.aliases() - assert 'xls' not in als - - assert await api.shell.getRunningProgram() == 'py' - - assert await api.shell.resolveProgram('doesnotexist') is None - assert await api.shell.resolveProgram('hello') == 'rom/programs/fun/hello.lua' - - assert await api.shell.dir() == '' - assert await api.shell.resolve('doesnotexist') == 'doesnotexist' - assert await api.shell.resolve('startup.lua') == 'startup.lua' - assert await api.shell.setDir('rom') is None - assert await api.shell.dir() == 'rom' - assert await api.shell.resolve('startup.lua') == 'rom/startup.lua' - assert await api.shell.setDir('') is None - - assert isinstance(await api.shell.path(), str) - assert await api.shell.setPath(await api.shell.path()) is None - - assert await api.shell.execute('hello') is True - assert await api.shell.run('hello') is True - assert await api.shell.execute('doesnotexist') is False - assert await api.shell.run('doesnotexist') is False - - tab = await api.shell.openTab('hello') - assert isinstance(tab, int) - - await step(api, f'Program has been launched in tab {tab}') - - assert await api.shell.switchTab(tab) is None - - await step(api, 'Computer will shutdown after test due to shell.exit') - - assert await api.shell.exit() is None - - await api.print('Test finished successfully') - - -async def test_window(api): - async with api.window.create( - api.term.get_current_target(), - 15, 5, 5, 5, False, - ) as win: - assert await win.getPosition() == (15, 5) - assert await win.getSize() == (5, 5) - - await win.setBackgroundColor(api.colors.red) - await win.clear() - await win.setVisible(True) - - await asyncio.sleep(1) - - await win.setVisible(False) - await win.setCursorPos(1, 1) - await win.setTextColor(api.colors.yellow) - await win.write('*********') - await win.setVisible(True) - - await asyncio.sleep(1) - - await api.term.clear() - - await asyncio.sleep(1) - - await win.redraw() - assert await win.getLine(1) == ('*****', '44444', 'eeeee') - - # draws immediately - await win.reposition(21, 5) - await win.reposition(27, 5) - - await api.print('Test finished successfully') - - -async def test_redirect_to_window(api): - w, h = await api.term.getSize() - async with AsyncExitStack() as stack: - left = await stack.enter_async_context(api.window.create( - api.term.get_current_target(), - 1, 1, w // 2, h, True, - )) - right = await stack.enter_async_context(api.window.create( - api.term.get_current_target(), - w // 2 + 1, 1, w // 2, h, True, - )) - async with api.term.redirect(left.get_term_target()): - await api.term.setBackgroundColor(api.colors.green) - await api.term.setTextColor(api.colors.white) - await api.term.clear() - await api.term.setCursorPos(1, h // 2) - await api.print('Left part') - async with api.term.redirect(right.get_term_target()): - await api.term.setBackgroundColor(api.colors.red) - await api.term.setTextColor(api.colors.yellow) - await api.term.clear() - await api.term.setCursorPos(1, h // 2) - await api.print('Right part') - await api.print('Default terminal restored') - - await api.print('Test finished successfully') - - -async def test_redirect_to_local_monitor(api): - side = 'left' - await step(api, f'Attach 3x3 color monitor to {side} side of computer') - - async with api.term.redirect(api.peripheral.get_term_target(side)): - await api.term.setBackgroundColor(api.colors.green) - await api.term.setTextColor(api.colors.white) - await api.term.clear() - await api.term.setCursorPos(1, 1) - await api.print('Redirected to monitor') - - await api.print('Test finished successfully') - - -async def test_redirect_to_remote_monitor(api): - side = 'back' - await step(api, f'Attach wired modem to {side} side of computer') - - mod = await api.peripheral.wrap(side) - - await step(api, 'Connect remote monitor using wires, activate its modem') - - for name in await mod.getNamesRemote(): - if await mod.getTypeRemote(name) == 'monitor': - break - else: - assert False - - async with api.term.redirect(api.peripheral.get_term_target(name)): - await api.term.setBackgroundColor(api.colors.blue) - await api.term.setTextColor(api.colors.white) - await api.term.clear() - await api.term.setCursorPos(1, 1) - await api.print('Redirected to monitor') - - await api.print('Test finished successfully') - - -pixels = ''' -0000000030030033333333330000000003000000000000000 -0333300000000033333333300000000000333333000000330 -0803000000803033333333000000000000880330300003000 -0800800030330333333333000300883000888880000033000 -3333000000003333333333300080038880000080000888003 -33333ddd3333333333333333300000333330000000000d033 -333dddddd3333333333333333333333333333333333ddd333 -3333ccdd333333333333344444444333333333333dddddd33 -333cc33d3333333333334444444444333333333335d3cc33d -5ddc33333333333333344444444444433333333333333cd55 -dddc555d3333333333344444444444433333333333d5dc5dd -d5dd5dd4bbbbbbbbb999b00b00300b3bb9999bbbb4ddddddd -ddd55444bb999993bbb33390b030bb9999bbbbbbb444ddddd -55dd44bbbbbbbbbbbbb9bb3003003bbb339bbbbbbbb44444d -dd444bbbbbbbbbbb99933bbb0030b999bbbbbbbbbbbbbbb44 -444bbbbbbbbbbbbbbb9bbb33b309933bbbbbbbbbbbbbbbbbb -bbbbbbbbbbbbbbbbbbbb9bbbb3bbbb99bbbbbbbbbbbbbbbbb -bbbbbbbbbbbbbbbbbbbbbb399399bbbbbbbbbbbbbbbbbbbbb -'''.strip() - - -async def test_paintutils(api): - from computercraft.subapis.paintutils import PaintutilsAPI - tbl = await get_object_table(api, 'paintutils') - assert get_class_table(PaintutilsAPI) == tbl - - async with api.fs.open('img.nfp', 'w') as f: - await f.write(pixels) - - # from pprint import pprint - int_pixels = await api.paintutils.loadImage('img.nfp') - assert len(int_pixels) > 0 - assert len(int_pixels[0]) > 0 - assert await api.paintutils.parseImage(pixels) == int_pixels - - assert await api.paintutils.drawImage(int_pixels, 1, 1) is None - - await asyncio.sleep(2) - - await api.term.setTextColor(api.colors.white) - await api.term.setBackgroundColor(api.colors.black) - await api.term.clear() - await api.term.setBackgroundColor(api.colors.green) - - by = 3 - bx = 3 - - assert await api.paintutils.drawPixel(bx, by) is None - assert await api.paintutils.drawPixel(bx + 1, by, api.colors.red) is None - - bx += 5 - - assert await api.paintutils.drawLine(bx, by, bx + 3, by + 3) is None - assert await api.paintutils.drawLine(bx + 3, by, bx, by + 3, api.colors.red) is None - - bx += 5 - assert await api.paintutils.drawBox(bx, by, bx + 3, by + 3) is None - bx += 5 - assert await api.paintutils.drawBox(bx, by, bx + 3, by + 3, api.colors.red) is None - - bx += 5 - assert await api.paintutils.drawFilledBox(bx, by, bx + 3, by + 3) is None - bx += 5 - assert await api.paintutils.drawFilledBox(bx, by, bx + 3, by + 3, api.colors.red) is None - - await api.term.setCursorPos(1, by + 6) - - await asyncio.sleep(2) - - await api.print('Test finished successfully') - - -async def test_rednet(api): - from computercraft.subapis.rednet import RednetAPI - tbl = await get_object_table(api, 'rednet') - del tbl['function']['run'] - assert get_class_table(RednetAPI) == tbl - - side = 'back' - - await step(api, f'Attach modem to {side} side of computer') - - assert await api.rednet.isOpen(side) is False - assert await api.rednet.isOpen() is False - - with assert_raises(LuaException): - await api.rednet.close('doesnotexist') - - assert await api.rednet.close(side) is None - - with assert_raises(LuaException): - await api.rednet.open('doesnotexist') - - assert await api.rednet.open(side) is None - assert await api.rednet.isOpen(side) is True - - with assert_raises(LuaException): - # disallowed hostname - await api.rednet.host('helloproto', 'localhost') - assert await api.rednet.host('helloproto', 'alpha') is None - - cid = await api.os.getComputerID() - - assert await api.rednet.lookup('helloproto', 'localhost') == cid - assert await api.rednet.lookup('helloproto') == [cid] - assert await api.rednet.lookup('nonexistent', 'localhost') is None - assert await api.rednet.lookup('nonexistent') == [] - - assert await api.rednet.unhost('helloproto') is None - - assert await api.rednet.send(cid + 100, 'message', 'anyproto') is True - assert await api.rednet.broadcast('message', 'anyproto') is None - - assert await api.rednet.receive(timeout=1) is None - assert await asyncio.gather( - api.rednet.receive(timeout=1), - api.rednet.send(cid, 'message'), - ) == [(cid, 'message', None), True] - - assert await api.rednet.close() is None - assert await api.rednet.isOpen(side) is False - - await api.print('Test finished successfully') - - -async def test_turtle(api): - from computercraft.subapis.turtle import TurtleAPI - tbl = await get_object_table(api, 'turtle') - assert tbl['table'] == {'native': True} - del tbl['table'] - tbl['function'].setdefault('craft', True) - assert get_class_table(TurtleAPI) == tbl - - flimit = await api.turtle.getFuelLimit() - assert isinstance(flimit, int) - assert flimit > 0 - - flevel = await api.turtle.getFuelLevel() - assert isinstance(flevel, int) - assert 0 <= flevel <= flimit - - assert await api.turtle.select(2) is None - assert await api.turtle.getSelectedSlot() == 2 - with assert_raises(LuaException): - await api.turtle.select(0) - assert await api.turtle.select(1) is None - assert await api.turtle.getSelectedSlot() == 1 - - await step(api, 'Put 3 coals into slot 1') - - assert await api.turtle.getItemCount() == 3 - assert await api.turtle.getItemCount(1) == 3 - - assert await api.turtle.getItemDetail() == { - 'count': 3, - 'name': 'minecraft:coal', - } - assert await api.turtle.getItemDetail(1) == { - 'count': 3, - 'name': 'minecraft:coal', - } - - assert await api.turtle.getItemSpace() == 61 - assert await api.turtle.getItemSpace(1) == 61 - - assert await api.turtle.refuel(1) is None - - assert await api.turtle.getFuelLevel() > flevel - flevel = await api.turtle.getFuelLevel() - assert await api.turtle.getItemCount() == 2 - - assert await api.turtle.refuel() is None - - assert await api.turtle.getFuelLevel() > flevel - assert await api.turtle.getItemCount() == 0 - - with assert_raises(LuaException): - await api.turtle.refuel(1) - with assert_raises(LuaException): - await api.turtle.refuel() - - await step(api, 'Remove blocks in front/below/above turtle') - - assert await api.turtle.detect() is False - assert await api.turtle.detectUp() is False - assert await api.turtle.detectDown() is False - - assert await api.turtle.inspect() is None - assert await api.turtle.inspectUp() is None - assert await api.turtle.inspectDown() is None - - await step(api, 'Put cobblestone blocks in front/below/above turtle') - - assert await api.turtle.detect() is True - assert await api.turtle.detectUp() is True - assert await api.turtle.detectDown() is True - - for c in [ - await api.turtle.inspect(), - await api.turtle.inspectUp(), - await api.turtle.inspectDown() - ]: - assert isinstance(c, dict) - assert c['name'] == 'minecraft:cobblestone' - - assert await api.turtle.select(1) is None - assert await api.turtle.getItemCount() == 0 - assert await api.turtle.equipLeft() is None - - assert await api.turtle.select(2) is None - assert await api.turtle.getItemCount() == 0 - assert await api.turtle.equipRight() is None - - if ( - await api.turtle.getItemCount(1) != 0 - or await api.turtle.getItemCount(2) != 0 - ): - await step(api, 'Remove all items from slots 1 and 2') - - assert await api.turtle.select(1) is None - if await api.turtle.getItemDetail(1) != { - 'count': 1, - 'name': 'minecraft:diamond_pickaxe', - }: - await step(api, 'Put fresh diamond pickaxe at slot 1') - - assert await api.turtle.equipLeft() is None - - assert await api.turtle.dig() is True - assert await api.turtle.dig() is False - assert await api.turtle.digUp() is True - assert await api.turtle.digUp() is False - assert await api.turtle.digDown() is True - assert await api.turtle.digDown() is False - - assert await api.turtle.getItemCount() == 3 - - assert await api.turtle.forward() is True - assert await api.turtle.back() is True - assert await api.turtle.up() is True - assert await api.turtle.down() is True - assert await api.turtle.turnLeft() is None - assert await api.turtle.turnRight() is None - - assert await api.turtle.place() is True - assert await api.turtle.place() is False - assert await api.turtle.placeUp() is True - assert await api.turtle.placeUp() is False - assert await api.turtle.placeDown() is True - with assert_raises(LuaException, 'No items to place'): - await api.turtle.placeDown() - - await step(api, 'Put 3 cobblestone blocks to slot 1') - - assert await api.turtle.getItemCount(1) == 3 - assert await api.turtle.getItemCount(2) == 0 - - assert await api.turtle.compare() is True - assert await api.turtle.compareUp() is True - assert await api.turtle.compareDown() is True - - assert await api.turtle.select(2) is None - - assert await api.turtle.compare() is False - assert await api.turtle.compareUp() is False - assert await api.turtle.compareDown() is False - - assert await api.turtle.select(1) is None - - assert await api.turtle.transferTo(2, 1) is True - assert await api.turtle.getItemCount(1) == 2 - assert await api.turtle.getItemCount(2) == 1 - assert await api.turtle.compareTo(2) is True - - assert await api.turtle.transferTo(2) is True - assert await api.turtle.getItemCount(1) == 0 - assert await api.turtle.getItemCount(2) == 3 - assert await api.turtle.compareTo(2) is False - - assert await api.turtle.select(2) is None - assert await api.turtle.transferTo(1) is True - assert await api.turtle.select(1) is None - assert await api.turtle.dig() is True - assert await api.turtle.digUp() is True - assert await api.turtle.digDown() is True - assert await api.turtle.getItemCount() == 6 - - assert await api.turtle.drop(1) is True - assert await api.turtle.dropUp(1) is True - assert await api.turtle.dropDown(1) is True - assert await api.turtle.getItemCount() == 3 - assert await api.turtle.drop() is True - assert await api.turtle.getItemCount() == 0 - assert await api.turtle.drop() is False - - await step( - api, - 'Collect dropped cobblestone\n' - 'Drop stack of sticks right in front of the turtle\n' - 'Its better to build 1-block room then throw sticks there', - ) - - assert await api.turtle.suck(1) is True - assert await api.turtle.getItemCount() == 1 - assert await api.turtle.suck() is True - assert await api.turtle.getItemCount() == 64 - assert await api.turtle.suck() is False - assert await api.turtle.drop() is True - assert await api.turtle.getItemCount() == 0 - - await step( - api, - 'Collect dropped sticks\n' - 'Drop stack of sticks right below the turtle\n' - 'Its better to build 1-block room then throw sticks there', - ) - - assert await api.turtle.suckDown(1) is True - assert await api.turtle.getItemCount() == 1 - assert await api.turtle.suckDown() is True - assert await api.turtle.getItemCount() == 64 - assert await api.turtle.suckDown() is False - assert await api.turtle.dropDown() is True - assert await api.turtle.getItemCount() == 0 - - await step( - api, - 'Collect dropped sticks\n' - 'Drop stack of sticks right above the turtle\n' - 'Its better to build 1-block room then throw sticks there', - ) - - assert await api.turtle.suckUp(1) is True - assert await api.turtle.getItemCount() == 1 - assert await api.turtle.suckUp() is True - assert await api.turtle.getItemCount() == 64 - assert await api.turtle.suckUp() is False - assert await api.turtle.dropUp() is True - assert await api.turtle.getItemCount() == 0 - - async def craft1(): - return await api.turtle.craft() - - async def craft2(): - c = await api.peripheral.wrap('right') - return await c.craft() - - await step(api, 'Put crafting table into slot 1') - assert await api.turtle.select(1) is None - assert await api.turtle.equipRight() is None - - for craft_fn in craft1, craft2: - await step( - api, - 'Clean inventory of turtle\n' - 'Put 8 cobblestones into slot 1', - ) - - assert await api.turtle.select(1) is None - assert await craft_fn() is False - for idx in [2, 3, 5, 7, 9, 10, 11]: - assert await api.turtle.transferTo(idx, 1) - assert await craft_fn() is True - assert await craft_fn() is False - assert await api.turtle.getItemDetail() == { - 'count': 1, - 'name': 'minecraft:furnace', - } - - await api.print('Test finished successfully') - - -async def test_turtle_attack(api): - await step( - api, - 'NOTE: this test is unreliable\n' - 'Build 1x1x1 stone cage in front of turtle\n' - 'Spawn here a chicken', - ) - - assert await api.turtle.attack() is True - assert await api.turtle.attack() is True - assert await api.turtle.attack() is False - - await step( - api, - 'Build 1x1x1 stone cage below turtle\n' - 'Spawn here a chicken', - ) - - assert await api.turtle.attackDown() is True - assert await api.turtle.attackDown() is True - assert await api.turtle.attackDown() is False - - await step( - api, - 'Build 1x1x1 stone cage above turtle\n' - 'Spawn here a chicken', - ) - - assert await api.turtle.attackUp() is True - assert await api.turtle.attackUp() is True - assert await api.turtle.attackUp() is False - - await api.print('Test finished successfully') - - -# vector won't be implemented, use python equivalent