From db7170c37c009c0ea9c00abcd02282ed3a859af7 Mon Sep 17 00:00:00 2001 From: Irmen de Jong Date: Sun, 9 Jul 2017 12:22:52 +0200 Subject: [PATCH] optimized text output processing for web interface (no more needless polls when no text is available) --- ideas/ideas.txt | 1 - tale/driver.py | 3 ++- tale/driver_if.py | 1 + tale/player.py | 18 +++++++------- tale/tio/__init__.py | 2 +- tale/tio/console_io.py | 4 ++-- tale/tio/if_browser_io.py | 50 +++++++++++++++++++++++++++------------ tale/tio/iobase.py | 2 +- tale/tio/tkinter_io.py | 4 ++-- 9 files changed, 54 insertions(+), 31 deletions(-) diff --git a/ideas/ideas.txt b/ideas/ideas.txt index 57f816b5..1199ac62 100644 --- a/ideas/ideas.txt +++ b/ideas/ideas.txt @@ -24,7 +24,6 @@ Concepts for multiplayer MUD mode (and not really for single player I.F.): General ideas/TODO: ------------------- -[html] in wsgi_handle_eventsource create an event in the io class to wait on new text rather than polling every 0.1 s [code] Flesh out more items/templates in items.basic. Take ideas from objects in other mudlibs [code] adding 'a' automatically sometimes gives strange results, "you take a someone's wallet"... "you take a some cash"... [feature] combat system. diff --git a/tale/driver.py b/tale/driver.py index b30f7720..4d42b89a 100644 --- a/tale/driver.py +++ b/tale/driver.py @@ -297,7 +297,8 @@ def start(self, game_file_or_path: str) -> None: x._bind_target(self.zones) self.unbound_exits = [] sys.excepthook = util.excepthook # install custom verbose crash reporter - self.start_main_loop() # doesn't exit! + self.start_main_loop() # doesn't exit! (unless game is killed) + self._stop_driver() def start_main_loop(self): raise NotImplementedError diff --git a/tale/driver_if.py b/tale/driver_if.py index 5729e945..33d02c43 100644 --- a/tale/driver_if.py +++ b/tale/driver_if.py @@ -59,6 +59,7 @@ def start_main_loop(self): driver_thread.start() connection.singleplayer_mainloop() # this doesn't return! (unless you CTRL-C it) self._stop_mainloop = True + connection.destroy() def show_motd(self, player: Player, notify_no_motd: bool=False) -> None: pass # no motd in IF mode diff --git a/tale/player.py b/tale/player.py index 2784b176..dd61c150 100644 --- a/tale/player.py +++ b/tale/player.py @@ -8,7 +8,7 @@ import queue import time from threading import Event -from typing import Any, Sequence, Tuple, IO, Dict, Set, List, Union +from typing import Any, Sequence, Tuple, IO, Optional, Set, List, Union from . import base from . import hints @@ -336,16 +336,18 @@ def __init__(self, player: Player=None, io: IoAdapterBase=None) -> None: self.io = io self.need_new_input_prompt = True - def get_output(self) -> str: + def get_output(self) -> Optional[str]: """ Gets the accumulated output lines, formats them nicely, and clears the buffer. If there is nothing to be outputted, None is returned. """ - formatted = self.io.render_output(self.player._output.get_paragraphs(), - width=self.player.screen_width, indent=self.player.screen_indent) - if formatted and self.player.transcript: - self.player.transcript.write(formatted) - return formatted or None + paragraphs = self.player._output.get_paragraphs() + if paragraphs: + formatted = self.io.render_output(paragraphs, width=self.player.screen_width, indent=self.player.screen_indent) + if formatted and self.player.transcript: + self.player.transcript.write(formatted) + return formatted or None + return None @property def last_output_line(self) -> str: @@ -412,7 +414,7 @@ def critical_error(self) -> None: self.io.critical_error() def singleplayer_mainloop(self) -> None: - self.io.singleplayer_mainloop(self) # this does not return + self.io.singleplayer_mainloop(self) # this does not return, unless game is closed def pause(self, unpause: bool=False) -> None: self.io.pause(unpause) diff --git a/tale/tio/__init__.py b/tale/tio/__init__.py index 98be191e..30f682b4 100644 --- a/tale/tio/__init__.py +++ b/tale/tio/__init__.py @@ -8,4 +8,4 @@ DEFAULT_SCREEN_WIDTH = 72 DEFAULT_SCREEN_INDENT = 2 -DEFAULT_SCREEN_DELAY = 0 # XXX 40 +DEFAULT_SCREEN_DELAY = 40 diff --git a/tale/tio/console_io.py b/tale/tio/console_io.py index b4cefc68..84c342c8 100644 --- a/tale/tio/console_io.py +++ b/tale/tio/console_io.py @@ -8,7 +8,7 @@ import signal import sys import threading -from typing import Iterable, Tuple, Any, Optional, List +from typing import Sequence, Tuple, Any, Optional, List try: import prompt_toolkit from prompt_toolkit.contrib.completers import WordCompleter @@ -146,7 +146,7 @@ def abort_all_input(self, player: Player) -> None: sys.stderr.flush() os.kill(os.getpid(), signal.SIGINT) - def render_output(self, paragraphs: Iterable[Tuple[str, bool]], **params: Any) -> Optional[str]: + def render_output(self, paragraphs: Sequence[Tuple[str, bool]], **params: Any) -> Optional[str]: """ Render (format) the given paragraphs to a text representation. It doesn't output anything to the screen yet; it just returns the text string. diff --git a/tale/tio/if_browser_io.py b/tale/tio/if_browser_io.py index a0cbfb3a..10d90fba 100644 --- a/tale/tio/if_browser_io.py +++ b/tale/tio/if_browser_io.py @@ -10,8 +10,8 @@ from email.utils import formatdate, parsedate from hashlib import md5 from html import escape as html_escape -from threading import Lock -from typing import Iterable, Tuple, Any, Optional, Dict, Callable, List +from threading import Lock, Event +from typing import Iterable, Sequence, Tuple, Any, Optional, Dict, Callable, List from urllib.parse import parse_qs from wsgiref.simple_server import make_server, WSGIRequestHandler, WSGIServer @@ -65,14 +65,20 @@ def __init__(self, player_connection: PlayerConnection, wsgi_server: WSGIServer) self.__html_to_browser = [] # type: List[str] # the lines that need to be displayed in the player's browser self.__html_special = [] # type: List[str] # special out of band commands (such as 'clear') self.__html_to_browser_lock = Lock() + self.__new_html_available = Event() + + def destroy(self) -> None: + self.__new_html_available.set() def append_html_to_browser(self, text: str) -> None: with self.__html_to_browser_lock: self.__html_to_browser.append(text) + self.__new_html_available.set() def append_html_special(self, text: str) -> None: with self.__html_to_browser_lock: self.__html_special.append(text) + self.__new_html_available.set() def get_html_to_browser(self) -> List[str]: with self.__html_to_browser_lock: @@ -84,6 +90,10 @@ def get_html_special(self) -> List[str]: special, self.__html_special = self.__html_special, [] return special + def wait_html_available(self): + self.__new_html_available.wait() + self.__new_html_available.clear() + def singleplayer_mainloop(self, player_connection: PlayerConnection) -> None: """mainloop for the web browser interface for single player mode""" import webbrowser @@ -112,21 +122,27 @@ def pause(self, unpause: bool=False) -> None: def clear_screen(self) -> None: self.append_html_special("clear") - def render_output(self, paragraphs: Iterable[Tuple[str, bool]], **params: Any) -> Optional[str]: - for text, formatted in paragraphs: - text = self.convert_to_html(text) - if text == "\n": - text = "
" - if formatted: - self.__html_to_browser.append("

" + text + "

\n") - else: - self.__html_to_browser.append("
" + text + "
\n") + def render_output(self, paragraphs: Sequence[Tuple[str, bool]], **params: Any) -> Optional[str]: + if not paragraphs: + return None + with self.__html_to_browser_lock: + for text, formatted in paragraphs: + text = self.convert_to_html(text) + if text == "\n": + text = "
" + if formatted: + self.__html_to_browser.append("

" + text + "

\n") + else: + self.__html_to_browser.append("
" + text + "
\n") + self.__new_html_available.set() return None # the output is pushed to the browser via a buffer, rather than printed to a screen def output(self, *lines: str) -> None: super().output(*lines) - for line in lines: - self.output_no_newline(line) + with self.__html_to_browser_lock: + for line in lines: + self.output_no_newline(line) + self.__new_html_available.set() def output_no_newline(self, text: str) -> None: super().output_no_newline(text) @@ -134,6 +150,7 @@ def output_no_newline(self, text: str) -> None: if text == "\n": text = "
" self.__html_to_browser.append("

" + text + "

\n") + self.__new_html_available.set() def convert_to_html(self, line: str) -> str: """Convert style tags to html""" @@ -304,7 +321,11 @@ def wsgi_handle_eventsource(self, environ: Dict[str, Any], parameters: Dict[str, return self.wsgi_internal_server_error_json(start_response, "not logged in") start_response('200 OK', [('Content-Type', 'text/event-stream; charset=utf-8'), ('Cache-Control', 'no-cache')]) - while self.driver.is_running() and conn.io and conn.player: + while self.driver.is_running(): + if conn.io and conn.player: + conn.io.wait_html_available() + if not conn.io or not conn.player: + break html = conn.io.get_html_to_browser() special = conn.io.get_html_special() if html or special: @@ -317,7 +338,6 @@ def wsgi_handle_eventsource(self, environ: Dict[str, Any], parameters: Dict[str, result = "event: text\nid: {event_id}\ndata: {data}\n\n"\ .format(event_id=str(time.time()), data=json.dumps(response)) yield result.encode("utf-8") - time.sleep(0.1) # @todo add event on conn.io so we don't have to poll it for new text def wsgi_handle_tabcomplete(self, environ: Dict[str, Any], parameters: Dict[str, str], start_response: WsgiStartResponseType) -> Iterable[bytes]: diff --git a/tale/tio/iobase.py b/tale/tio/iobase.py index 3ee04751..29096552 100644 --- a/tale/tio/iobase.py +++ b/tale/tio/iobase.py @@ -69,7 +69,7 @@ def abort_all_input(self, player) -> None: """abort any blocking input, if at all possible""" pass - def render_output(self, paragraphs: Iterable[Tuple[str, bool]], **params: Any) -> Optional[str]: + def render_output(self, paragraphs: Sequence[Tuple[str, bool]], **params: Any) -> Optional[str]: """ Render (format) the given paragraphs to a text representation. It doesn't output anything to the screen yet; it just returns the text string. diff --git a/tale/tio/tkinter_io.py b/tale/tio/tkinter_io.py index d2978c8a..8499fe7c 100644 --- a/tale/tio/tkinter_io.py +++ b/tale/tio/tkinter_io.py @@ -12,7 +12,7 @@ import tkinter import tkinter.font import tkinter.messagebox -from typing import Iterable, Tuple, Any, Optional +from typing import Sequence, Tuple, Any, Optional from . import iobase from .. import vfs @@ -64,7 +64,7 @@ def abort_all_input(self, player) -> None: """abort any blocking input, if at all possible""" player.store_input_line("") - def render_output(self, paragraphs: Iterable[Tuple[str, bool]], **params: Any) -> Optional[str]: + def render_output(self, paragraphs: Sequence[Tuple[str, bool]], **params: Any) -> Optional[str]: """ Render (format) the given paragraphs to a text representation. It doesn't output anything to the screen yet; it just returns the text string.