Added galtack_loadable.py for real; moved universe display stuff there
[galtack.git] / galtack_client.py
blob3332da65bbce4b735f34d0682144f2bff805e6ae
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 """
4 GalTacK - An unofficial PyGTK client to the Galcon multiplayer game.
5 """
6 # Copyright (C) 2007 Michael Carter
7 # Copyright (C) 2007 Felix Rabe <public@felixrabe.textdriven.com>
9 # Permission is hereby granted, free of charge, to any person obtaining a
10 # copy of this software and associated documentation files (the
11 # "Software"), to deal in the Software without restriction, including
12 # without limitation the rights to use, copy, modify, merge, publish,
13 # distribute, sublicense, and/or sell copies of the Software, and to permit
14 # persons to whom the Software is furnished to do so, subject to the
15 # following conditions:
17 # The above copyright notice and this permission notice shall be included
18 # in all copies or substantial portions of the Software.
20 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
21 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
23 # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
24 # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
25 # OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
26 # THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28 # Recommended line length or text width: 75 characters.
30 galtack_version = (0,2,0)
31 galtack_version_str = ".".join(map(str, galtack_version))
33 import optparse
34 import math
35 PI2 = math.pi * 2
36 import time
38 from PyGTKShell.Config import _config
39 _config["main-loop-integrate-twisted"] = True
40 from PyGTKShell.RawConsole import *
41 arrow_cursor = gtk.gdk.Cursor(gtk.gdk.TOP_LEFT_ARROW)
42 watch_cursor = gtk.gdk.Cursor(gtk.gdk.WATCH)
44 from twisted.internet import reactor
46 import galtack.net
47 class GaltackClient(
48 galtack.net.GaltackClientRecvCmdLoggerMixin,
49 galtack.net.GaltackClientSendCmdLoggerMixin,
50 galtack.net.GaltackClientUniverseTrackerMixin,
51 galtack.net.GaltackClientHousekeeperMixin,
52 galtack.net.GaltackClientBase,
53 ): pass
56 ### CUSTOMIZED STUFF FROM PYGTK SHELL [START]
59 ## RawConsole
62 class RawConsoleWithIntroMixin(RawConsoleBase):
63 """
64 Raw console running an example script on initialization.
66 Customized for GalTacK.
67 """
69 def __init__(self, *a, **kw):
70 super(RawConsoleWithIntroMixin, self).__init__(*a, **kw)
71 buf = self.code_view.get_buffer()
72 msg = a_("Press F5 or Ctrl+E to execute this code.")
73 buf('# -*- coding: utf-8 -*-\n' +
74 '# %s\n' % msg +
75 'from galtack_client import *\n' +
76 'o = console.output_view.get_buffer()\n' +
77 'o.set_text("")\n' +
78 'c = window.galtack_client\n' +
79 'o(dir())\n')
82 class RawConsoleWithIntro(
83 RawConsoleCenterInitMixin,
84 RawConsoleWithIntroMixin,
85 RawConsole,
86 ): pass
89 class WindowWithRawConsole(Window):
90 """
91 Window with a RawConsole in it and starting at a reasonable size.
92 """
94 def __init__(self, *a, **kw):
95 super(WindowWithRawConsole, self).__init__(*a, **kw)
96 self.set_title("PyGTK Shell RawConsole")
97 self.set_default_size(400, 400)
98 self.raw_console = self(RawConsoleWithIntro())
101 class WindowF12RawConsoleMixin(Window):
103 Window opening a Window containing a RawConsole when F12 is pressed.
106 def __init__(self, *a, **kw):
107 super(WindowF12RawConsoleMixin, self).__init__()
108 self.connect("key-press-event", self.__cb_key_press_event)
110 def __cb_key_press_event(self, window, event):
111 if (KeyPressEval("F12"))(event):
112 rc = WindowWithRawConsole().raw_console
113 rc.code_view.textview_userexec_namespace["window"] = self
114 return True
115 return False
118 class WindowF12RawConsole(
119 WindowF12RawConsoleMixin,
120 Window,
121 ): pass
124 ### CUSTOMIZED STUFF FROM PYGTK SHELL [END]
127 def print_errback(failure):
128 failure.printTraceback()
129 return None
132 def reload_loadable():
133 m = sys.modules.get("galtack_loadable", None)
134 if m:
135 reload(m)
136 else:
137 import galtack_loadable as m
138 m.run_loadable()
141 class GaltackChatWindow(WindowF12RawConsoleMixin):
143 A Window to allow chatting in Galcon, following the Galcon client
144 protocol.
147 def __init__(self, prev_window, options, user_info, server_info):
148 self.__class__._instance = self
149 self.__prev_window = prev_window
150 self.__options = options
151 self.__user_info = user_info
152 self.__server_info = server_info
154 super(GaltackChatWindow, self).__init__()
155 self.set_default_size(400, 300)
156 n, o = server_info["name"], server_info["owner"]
157 if n: n += " - "
158 self.set_title("Chat (%s%s) - GalTacK" % (n, o))
159 self.connect("delete-event", self.__cb_delete_event)
161 outer_vbox, inner_vbox = gnome_hig(self)
163 sw = inner_vbox(Frame())(ScrolledWindow())
164 self.output_view = sw(TextView())
165 self.__buf = self.output_view.get_buffer()
166 self.__buf.create_mark("end", self.__buf.get_end_iter(), False)
167 self.__buf.connect("insert-text", self.__cb_insert_text)
168 self.output_view.set_editable(False)
169 hbox = gnome_hig(inner_vbox(HBox(), False, False))
170 self.input_entry = hbox(Entry())
171 self.send_button = hbox(Button("_Send"), False, False)
172 self.send_button.connect("clicked", self.__cb_send_clicked)
173 self.leave_button = hbox(Button("_Leave"), False, False)
174 self.leave_button.connect("clicked", self.__cb_leave_clicked)
175 self.quit_button = hbox(Button("_Quit"), False, False)
176 self.quit_button.connect("clicked", self.__cb_quit_clicked)
178 self.send_button.set_property("can-default", True)
179 gobject.idle_add(self.send_button.grab_default)
180 gobject.idle_add(self.input_entry.grab_focus)
182 C = GaltackClient
183 self.galtack_client = C(self.__options,
184 self.__user_info, self.__server_info)
185 r = self.galtack_client.register_command_callback
186 r("close", self.__cmd_close)
187 r("message", self.__cmd_message)
188 r("start", self.__cmd_start)
189 r("stop", self.__cmd_stop)
190 reactor.listenUDP(0, self.galtack_client)
192 def f(): # TODO: implement this properly after login
193 self.galtack_client.send_commands((1, "status", "away"))
194 return False
195 gobject.timeout_add(500, f)
197 def __cb_delete_event(self, widget, event):
198 self.leave_button.clicked()
199 return True
201 def __cb_insert_text(self, textbuffer, iter, text, length):
202 mark = textbuffer.get_mark("end")
203 if not mark:
204 return False
205 self.output_view.scroll_to_mark(mark, 0.0)
206 return False
208 def __cb_leave_clicked(self, button):
209 self.set_sensitive(False)
210 deferred = self.galtack_client.logout()
211 deferred.addCallback(self.__cb_left)
212 deferred.addErrback(self.__eb_left)
214 def __cb_left(self, ignored_arg):
215 # GaltackServerListWindow._instance.show()
216 self.__prev_window.show()
217 self.destroy()
219 def __eb_left(self, ignored_arg):
220 self.set_sensitive(True)
222 def __cb_quit_clicked(self, button):
223 self.set_sensitive(False)
224 deferred = self.galtack_client.logout()
225 deferred.addCallback(self.__cb_left_quit)
226 deferred.addErrback(self.__cb_left_quit)
228 def __cb_left_quit(self, ignored_arg):
229 self.destroy()
231 def __cb_send_clicked(self, button):
232 msg = self.input_entry.get_text()
233 self.galtack_client.send_commands((1, "message", msg))
234 self.input_entry.set_text("")
236 def __cmd_message(self, command):
237 sender, message = command[3:]
238 snd = ""
239 if sender: snd = " " + sender
240 self.__buf("%s%s %s\n" % (time.strftime("%X"), snd, message))
242 def __cmd_close(self, command):
243 self.galtack_client.send_commands((1, "[CLOSE]"))
244 gobject.timeout_add(500, main_loop_quit) # TODO: await ACK
246 def __cmd_stop(self, command):
247 self.__buf("(stop)\n")
249 def __cmd_start(self, command):
250 self.__buf("(start)\n")
253 class GaltackServerListWindow(WindowF12RawConsoleMixin):
255 Window displaying the list of Galcon servers.
258 NEXT_CLASS = GaltackChatWindow
260 def __init__(self, prev_window, options, user_info):
261 self.__class__._instance = self
262 self.__prev_window = prev_window
263 self.__options = options
264 super(GaltackServerListWindow, self).__init__()
265 self.__user_info = user_info
266 self.set_title("Galcon Server List - GalTacK")
267 self.set_default_size(600, 400)
268 self.set_position(gtk.WIN_POS_CENTER)
270 outer_vbox, self.__inner_vbox = gnome_hig(self)
272 hbox = gnome_hig(self.__inner_vbox(HBox(), False, False))
273 self.__version_label = hbox(LeftLabel(""), False, False)
274 self.set_current_version()
275 self.__version_label.set_sensitive(False)
277 self.__refresh_button = hbox(Button("_Refresh List"), False, False,
278 pack_end = True)
279 self.__refresh_button.connect("clicked", self.__cb_refresh)
281 sw = self.__inner_vbox(Frame())(ScrolledWindow())
282 self.__treeview = sw(TreeView())
283 self.__treeview.set_sensitive(False)
284 self.__treeview.set_property("rules-hint", True)
285 self.__treeview.connect("row-activated", self.__cb_row_activated)
286 cb = self.__cb_selection_changed
287 self.__treeview.get_selection().connect("changed", cb)
289 for i, spec in enumerate(galtack.net.ServerInfo.COL_SPEC):
290 if not spec["visible"]: continue
291 col = gtk.TreeViewColumn(spec["caption"])
292 col.set_reorderable(True)
293 col.set_sort_column_id(i)
294 self.__treeview.append_column(col)
295 cell = gtk.CellRendererText()
296 col.pack_start(cell, True)
297 col.add_attribute(cell, "text", i)
298 col.add_attribute(cell, "sensitive", 0)
300 hbox = gnome_hig(self.__inner_vbox(HBox(), False, False))
301 self.__join_button = hbox(Button("_Join"))
302 self.__join_button.set_sensitive(False)
303 self.__join_button.connect("clicked", self.__cb_join)
305 self.back_button = hbox(Button("_Back"), False, False)
306 self.back_button.connect("clicked", self.__cb_back)
308 self.__statusbar = outer_vbox(Statusbar(), False, False)
309 self.__statusbar_cid = cid = self.__statusbar.get_context_id("msg")
311 self.__server_password_prompt = dlg = Dialog(
312 "Server Password Required", self, gtk.DIALOG_MODAL,
313 (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
314 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)
316 dlg.set_default_response(gtk.RESPONSE_ACCEPT)
317 dlg.set_default_size(300, -1)
318 vbox = gnome_hig(VBox()) # I want my own VBox here ;-)
319 vbox.set_border_width(6)
320 dlg.vbox.pack_start(vbox)
321 self.__server_password_prompt_label = vbox(LeftLabel())
322 self.__server_password_prompt_entry = vbox(Entry())
323 self.__server_password_prompt_entry.set_visibility(False)
325 def set_current_version(self, current_version = None):
326 self.__version_label.set_sensitive(False)
327 if current_version is None:
328 self.__version_label.set_text("Current version: ???")
329 else:
330 self.__version_label.set_text("Current version: %s" %
331 ".".join(current_version))
332 self.__version_label.set_sensitive(True)
334 def set_server_info_list(self, server_info_list = None):
335 types = galtack.net.ServerInfo.get_col_types()
336 model = gtk.ListStore(*types)
337 self.__treeview.set_sensitive(False)
338 if server_info_list is not None:
339 for server_info in server_info_list:
340 model.append(server_info.get_data_tuple())
341 self.__treeview.set_sensitive(True)
342 self.__treeview.set_model(model)
344 def __cb_refresh(self, button):
345 self.window.set_cursor(watch_cursor)
346 self.__inner_vbox.set_sensitive(False)
347 self.__statusbar.push(self.__statusbar_cid,
348 "Retrieving server list...")
349 deferred = galtack.net.get_server_list(self.__user_info)
350 deferred.addCallback(self.__server_list_callback)
351 deferred.addErrback(self.__server_list_errback)
353 def __server_list_callback(self, (current_version, server_info_list)):
354 self.set_current_version(current_version)
355 self.set_server_info_list(server_info_list)
356 self.__statusbar.pop(self.__statusbar_cid)
357 self.__inner_vbox.set_sensitive(True)
358 self.window.set_cursor(arrow_cursor)
360 def __server_list_errback(self, failure):
361 self.set_sensitive(True)
362 self.window.set_cursor(arrow_cursor)
363 cid = self.__statusbar_cid
364 sbar = self.__statusbar
365 sbar.pop(cid)
366 gtk.gdk.beep()
367 mid = sbar.push(cid, "Retrieving server list failed")
368 gobject.timeout_add(4000, lambda: sbar.remove(cid, mid))
369 return failure
371 def __cb_selection_changed(self, treesel):
372 model, iter = treesel.get_selected()
373 if iter is None:
374 ok_to_join = False
375 else:
376 data = galtack.net.ServerInfo(*model[iter])
377 ok_to_join = data.bots_ok
378 self.__join_button.set_sensitive(ok_to_join)
380 def __cb_row_activated(self, treeview, path, view_column):
381 self.__cb_join(self.__join_button)
383 def __cb_join(self, button):
384 treesel = self.__treeview.get_selection()
385 model, iter = treesel.get_selected()
386 server_info = galtack.net.ServerInfo(*model[iter])
387 if server_info.pwd_protected:
388 self.__run_server_password_prompt(server_info)
389 return None
390 self.join_with_server_info(server_info)
392 def __cb_back(self, button):
393 # self.GaltackLoginWindow._instance.show()
394 self.__prev_window.show()
395 self.destroy()
397 def __run_server_password_prompt(self, server_info):
398 n, o = server_info["name"], server_info["owner"]
399 if n: n += " - "
400 s = "Password for %s%s:" % (n, o)
401 self.__server_password_prompt_label.set_text(s)
402 response_id = self.__server_password_prompt.run()
403 if response_id != gtk.RESPONSE_ACCEPT: return None
404 passwd = self.__server_password_prompt_entry.get_text()
405 server_info["passwd"] = passwd
406 self.join_with_server_info(server_info)
408 def join_with_server_info(self, server_info):
409 chat_window = self.NEXT_CLASS(self, self.__options,
410 self.__user_info, server_info)
411 gobject.idle_add(self.hide)
414 class GaltackLoginWindow(WindowF12RawConsoleMixin):
416 A Window asking the user for Galcon login details and log in.
419 NEXT_CLASS = GaltackServerListWindow
421 def __init__(self, options):
422 self.__class__._instance = self
423 self.__options = options
424 super(GaltackLoginWindow, self).__init__()
425 self.set_title("GalTacK Login")
426 self.set_default_size(400, -1)
427 self.set_position(gtk.WIN_POS_CENTER)
429 outer_vbox, self.inner_vbox = gnome_hig(self)
431 table = gnome_hig(self.inner_vbox(Table(), False, False))
433 xop = {"xoptions": gtk.FILL}
434 table.add_rows()
435 label = table.attach_cell(LeftLabel("_Email Address:"), **xop)
436 self.email_entry = table.attach_cell(Entry())
437 label.set_mnemonic_widget(self.email_entry)
438 if self.__options.email is not None:
439 self.email_entry.set_text(self.__options.email)
441 table.add_rows()
442 label = table.attach_cell(LeftLabel("_Username:"), **xop)
443 self.name_entry = table.attach_cell(Entry())
444 label.set_mnemonic_widget(self.name_entry)
445 if self.__options.user is not None:
446 self.name_entry.set_text(self.__options.user)
448 table.add_rows()
449 label = table.attach_cell(LeftLabel("_Password:"), **xop)
450 self.passwd_entry = table.attach_cell(Entry())
451 label.set_mnemonic_widget(self.passwd_entry)
452 self.passwd_entry.set_visibility(False)
453 if self.__options.password is not None:
454 self.passwd_entry.set_text(self.__options.password)
456 table.add_rows()
457 hbox = gnome_hig(table.attach_row(HBox()))
458 self.__login_button = hbox(Button("_Sign In"))
459 self.__login_button.connect("clicked", self.__cb_login)
460 self.__login_button.set_property("can-default", True)
461 gobject.idle_add(self.__login_button.grab_default)
463 self.__quit_button = hbox(Button("_Quit"), False, False)
464 self.__quit_button.connect("clicked", self.__cb_quit)
466 self.statusbar = outer_vbox(Statusbar(), False, False)
467 self.statusbar_cid = cid = self.statusbar.get_context_id("msg")
468 mid = self.statusbar.push(cid, "Enter your login details")
469 gobject.timeout_add(4000, lambda: self.statusbar.remove(cid, mid))
471 def __get_user_info(self):
472 email = self.email_entry.get_text()
473 name = self.name_entry.get_text()
474 passwd = self.passwd_entry.get_text()
475 platform = "linux2"
476 version = "1.2.1"
477 return galtack.net.UserInfo(email, name, passwd, platform, version)
479 def __cb_login(self, button):
480 self.window.set_cursor(watch_cursor)
481 self.inner_vbox.set_sensitive(False)
482 self.statusbar.push(self.statusbar_cid,
483 "Retrieving server list...")
484 user_info = self.__get_user_info()
485 deferred = galtack.net.get_server_list(user_info)
486 deferred.addCallbacks(self.__server_list_callback,
487 self.__server_list_errback)
488 deferred.addErrback(print_errback)
490 def __cb_quit(self, button):
491 main_loop_quit()
493 def __server_list_callback(self, (current_version, server_info_list)):
494 w = self.NEXT_CLASS(self, self.__options, self.__get_user_info())
495 w.set_current_version(current_version)
496 w.set_server_info_list(server_info_list)
497 gobject.idle_add(self.hide)
498 self.inner_vbox.set_sensitive(True)
499 self.window.set_cursor(arrow_cursor)
500 cid = self.statusbar_cid
501 self.statusbar.pop(cid)
503 def __server_list_errback(self, failure):
504 failure.trap(galtack.net.ServerListException)
505 self.inner_vbox.set_sensitive(True)
506 self.window.set_cursor(arrow_cursor)
507 cid = self.statusbar_cid
508 self.statusbar.pop(cid)
509 gtk.gdk.beep()
510 mid = self.statusbar.push(cid, "Failure: %s" %
511 failure.getErrorMessage())
512 gobject.timeout_add(4000, lambda: self.statusbar.remove(cid, mid))
513 return None
515 @classmethod # for potential inheritance
516 def _modify_option_parser(cls, option_parser):
517 option_parser.add_option("-e", "--email", metavar = "ADDRESS",
518 help = "login email address")
519 option_parser.add_option("-u", "--user",
520 help = "login username")
521 option_parser.add_option("-p", "--password",
522 help = "login password")
523 return option_parser
526 def main(argv):
527 option_parser = optparse.OptionParser(prog = "GalTacK",
528 version = "%%prog %s" %
529 galtack_version_str)
530 option_parser = GaltackLoginWindow._modify_option_parser(option_parser)
531 option_parser = GaltackClient._modify_option_parser(option_parser)
532 options, arguments = option_parser.parse_args(argv[1:])
533 GaltackLoginWindow(options)
534 main_loop_run()
535 return 0
537 if __name__ == "__main__":
538 import sys
539 sys.exit(main(sys.argv))