|
4 | 4 | # The main window for the "Create Your Own Adventure" game. It shows the |
5 | 5 | # welcome screen until the AI used to generate the story is configured, then |
6 | 6 | # either the world creation flow or, when a game is already in progress, the |
7 | | -# game itself. |
| 7 | +# game itself. Like the E-book viewer and the editor, the game is a program |
| 8 | +# of its own: it is normally started as a separate process, either from the |
| 9 | +# command line or by the calibre GUI via |
| 10 | +# job_manager.launch_gui_app('cyoa'), and keeps running after the calibre |
| 11 | +# GUI that launched it is closed. |
8 | 12 | # Run with: calibre-debug -c 'from calibre.gui2.cyoa.main import main; main()' |
9 | 13 |
|
10 | | -from qt.core import QFont, QIcon, QSize, QStackedWidget |
| 14 | +import os |
| 15 | +import sys |
| 16 | +from collections.abc import Sequence |
| 17 | +from contextlib import closing |
| 18 | + |
| 19 | +from qt.core import QFont, QIcon, QSize, QStackedWidget, Qt |
11 | 20 |
|
12 | 21 | from calibre.ai.cyoa import PROTAGONIST_ID, GeneratedWorld, StoryStyle, start_game |
13 | 22 | from calibre.constants import CYOA_APP_UID, islinux |
14 | | -from calibre.gui2 import Application, error_dialog, gprefs |
| 23 | +from calibre.gui2 import Application, error_dialog, gprefs, setup_gui_option_parser |
15 | 24 | from calibre.gui2.cyoa import data |
16 | 25 | from calibre.gui2.cyoa.game import GameWidget |
17 | 26 | from calibre.gui2.cyoa.welcome import WelcomeWidget |
18 | 27 | from calibre.gui2.cyoa.world import CreateWorldWidget |
| 28 | +from calibre.gui2.listener import Listener, send_message_in_process |
19 | 29 | from calibre.gui2.main_window import MainWindow |
| 30 | +from calibre.ptempfile import reset_base_dir |
| 31 | +from calibre.utils.config import OptionParser |
| 32 | +from calibre.utils.ipc import cyoa_socket_address |
20 | 33 | from calibre.utils.localization import _ |
| 34 | +from calibre.utils.lock import SingleInstance |
| 35 | + |
| 36 | +# Only one process can play at a time, as two of them would overwrite each |
| 37 | +# other's auto-saved game, see main(). |
| 38 | +SINGLE_INSTANCE_NAME = 'calibre_cyoa' |
21 | 39 |
|
22 | 40 |
|
23 | 41 | class CYOAMainWindow(MainWindow): |
@@ -90,22 +108,90 @@ def abandon_game(self) -> None: |
90 | 108 | data.set_current_game('') |
91 | 109 | self.show_appropriate_page() |
92 | 110 |
|
| 111 | + def message_from_other_instance(self, msg: bytes) -> None: |
| 112 | + # Another process was started to play the game. Since there can be |
| 113 | + # only one, it asks this one to come to the front instead, see |
| 114 | + # main(). |
| 115 | + self.raise_and_focus() |
| 116 | + |
| 117 | + |
| 118 | +def option_parser() -> OptionParser: |
| 119 | + from calibre.gui2.main_window import option_parser as base_option_parser |
| 120 | + |
| 121 | + parser = base_option_parser( |
| 122 | + _( |
| 123 | + '''\ |
| 124 | +%prog [options] |
| 125 | +
|
| 126 | +Play a "Create Your Own Adventure" game, in which the story is written by an AI as you play it. |
| 127 | +''' |
| 128 | + ) |
| 129 | + ) |
| 130 | + setup_gui_option_parser(parser) |
| 131 | + return parser |
| 132 | + |
| 133 | + |
| 134 | +def run_gui(app: Application, listener: Listener | None = None) -> None: |
| 135 | + w = CYOAMainWindow() |
| 136 | + w.set_exception_handler() |
| 137 | + app.shutdown_signal_received.connect(w.close) |
| 138 | + if listener is not None: |
| 139 | + listener.message_received.connect(w.message_from_other_instance, type=Qt.ConnectionType.QueuedConnection) |
| 140 | + w.show() |
| 141 | + app.exec() |
| 142 | + del w |
| 143 | + |
93 | 144 |
|
94 | | -def main() -> None: |
95 | | - override = 'calibre-ebook-viewer' if islinux else None |
96 | | - app = Application([], override_program_name=override, windows_app_uid=CYOA_APP_UID) |
| 145 | +def main(args: Sequence[str] = sys.argv) -> None: |
| 146 | + # Ensure the game can continue to be played if the calibre GUI that |
| 147 | + # launched it is closed, which would otherwise take its temporary |
| 148 | + # directory away. |
| 149 | + os.environ.pop('CALIBRE_WORKER_TEMP_DIR', None) |
| 150 | + reset_base_dir() |
| 151 | + args = list(args) |
| 152 | + override = 'calibre-cyoa' if islinux else None |
| 153 | + app = Application(args, override_program_name=override, windows_app_uid=CYOA_APP_UID) |
| 154 | + option_parser().parse_args(args) |
97 | 155 | fi = gprefs['font'] |
98 | 156 | if fi is not None: |
99 | 157 | font = QFont(*(fi[:4])) |
100 | 158 | s = gprefs.get('font_stretch', None) |
101 | 159 | if s is not None: |
102 | 160 | font.setStretch(s) |
103 | 161 | app.setFont(font) |
104 | | - w = CYOAMainWindow() |
105 | | - w.set_exception_handler() |
106 | | - w.show() |
107 | | - app.exec() |
108 | | - del w |
| 162 | + app.setWindowIcon(QIcon.ic('ai.png')) |
| 163 | + # Two processes playing at the same time would overwrite each other's |
| 164 | + # auto-saved game, so a second launch asks the one already running to |
| 165 | + # come to the front and exits. |
| 166 | + with SingleInstance(SINGLE_INSTANCE_NAME) as si: |
| 167 | + if not si: |
| 168 | + try: |
| 169 | + send_message_in_process(b'raise-window', address=cyoa_socket_address()) |
| 170 | + except Exception as err: |
| 171 | + error_dialog( |
| 172 | + None, |
| 173 | + _('Failed to connect'), |
| 174 | + _('Could not connect to the already running game window, try restarting it.'), |
| 175 | + det_msg=str(err), |
| 176 | + show=True, |
| 177 | + ) |
| 178 | + raise SystemExit(1) |
| 179 | + else: |
| 180 | + try: |
| 181 | + listener = Listener(address=cyoa_socket_address(), parent=app) |
| 182 | + listener.start_listening() |
| 183 | + except Exception as err: |
| 184 | + error_dialog( |
| 185 | + None, |
| 186 | + _('Failed to start listener'), |
| 187 | + _('Could not start the listener used to ensure only one game is played at a time. Try rebooting your computer.'), |
| 188 | + det_msg=str(err), |
| 189 | + show=True, |
| 190 | + ) |
| 191 | + run_gui(app) |
| 192 | + else: |
| 193 | + with closing(listener): |
| 194 | + run_gui(app, listener) |
109 | 195 | del app |
110 | 196 |
|
111 | 197 |
|
|
0 commit comments