Felix departed
[galtack.git] / galtack_client.py
blob31ca6473d220740a44c0ceb4b4fa1a077f40d4d2
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 'ld = reload_loadable(c)\n' +
80 'o("GaltackClient instance: %r\\n" % c)\n' +
81 'o("Loadable return value: %r\\n" % ld)\n' +
82 'c.send_commands((1, "message", "hi folks"))\n'
86 class RawConsoleWithIntro(
87 RawConsoleCenterInitMixin,
88 RawConsoleWithIntroMixin,
89 RawConsole,
90 ): pass
93 class WindowWithRawConsole(Window):
94 """
95 Window with a RawConsole in it and starting at a reasonable size.
96 """
98 def __init__(self, *a, **kw):
99 super(WindowWithRawConsole, self).__init__(*a, **kw)
100 self.set_title("PyGTK Shell RawConsole")
101 self.set_default_size(550, 400)
102 self.raw_console = self(RawConsoleWithIntro())
105 class WindowF5RawConsoleMixin(Window):
107 Window opening a Window containing a RawConsole when F5 is pressed.
110 def __init__(self, *a, **kw):
111 super(WindowF5RawConsoleMixin, self).__init__()
112 self.connect("key-press-event", self.__cb_key_press_event)
114 def __cb_key_press_event(self, window, event):
115 if (KeyPressEval("F5"))(event):
116 rc = WindowWithRawConsole().raw_console
117 rc.code_view.textview_userexec_namespace["window"] = self
118 return True
119 return False
122 class WindowF5RawConsole(
123 WindowF5RawConsoleMixin,
124 Window,
125 ): pass
128 ### CUSTOMIZED STUFF FROM PYGTK SHELL [END]
131 def print_errback(failure):
132 failure.printTraceback()
133 return None
136 def reload_loadable(*a, **kw):
137 m = sys.modules.get("galtack_loadable", None)
138 if m:
139 reload(m)
140 else:
141 import galtack_loadable as m
142 return m.run_loadable(*a, **kw)
145 class GaltackChatWindow(WindowF5RawConsoleMixin):
147 A Window to allow chatting in Galcon, following the Galcon client
148 protocol.
151 def __init__(self, prev_window, options, user_info, server_info):
152 self.__class__._instance = self
153 self.__prev_window = prev_window
154 self.__options = options
155 self.__user_info = user_info
156 self.__server_info = server_info
158 super(GaltackChatWindow, self).__init__()
159 self.set_default_size(550, 300)
160 n, o = server_info["name"], server_info["owner"]
161 if n: n += " - "
162 self.set_title("Chat (%s%s) - GalTacK" % (n, o))
163 self.connect("delete-event", self.__cb_delete_event)
165 outer_vbox, inner_vbox = gnome_hig(self)
167 sw = inner_vbox(Frame())(ScrolledWindow())
168 self.output_view = sw(TextView())
169 self.__buf = self.output_view.get_buffer()
170 self.__buf("Hint: Press <F5> to execute arbitrary Python code.\n")
171 self.__buf.create_mark("end", self.__buf.get_end_iter(), False)
172 self.__buf.connect("insert-text", self.__cb_insert_text)
173 self.output_view.set_editable(False)
174 hbox = gnome_hig(inner_vbox(HBox(), False, False))
175 self.input_entry = hbox(Entry())
176 self.send_button = hbox(Button("_Send"), False, False)
177 self.send_button.connect("clicked", self.__cb_send_clicked)
178 self.leave_button = hbox(Button("_Leave"), False, False)
179 self.leave_button.connect("clicked", self.__cb_leave_clicked)
180 self.quit_button = hbox(Button("_Quit"), False, False)
181 self.quit_button.connect("clicked", self.__cb_quit_clicked)
183 self.send_button.set_property("can-default", True)
184 gobject.idle_add(self.send_button.grab_default)
185 gobject.idle_add(self.input_entry.grab_focus)
187 C = GaltackClient
188 self.galtack_client = C(self.__options,
189 self.__user_info, self.__server_info)
190 r = self.galtack_client.register_command_callback
191 r("close", self.__cmd_close)
192 r("message", self.__cmd_message)
193 r("start", self.__cmd_start)
194 r("stop", self.__cmd_stop)
195 reactor.listenUDP(0, self.galtack_client)
197 def f(): # TODO: implement this properly after login
198 self.galtack_client.send_commands((1, "status", "away"))
199 return False
200 gobject.timeout_add(500, f)
202 def __cb_delete_event(self, widget, event):
203 self.leave_button.clicked()
204 return True
206 def __cb_insert_text(self, textbuffer, iter, text, length):
207 mark = textbuffer.get_mark("end")
208 if not mark:
209 return False
210 self.output_view.scroll_to_mark(mark, 0.0)
211 return False
213 def __cb_leave_clicked(self, button):
214 self.set_sensitive(False)
215 deferred = self.galtack_client.logout()
216 deferred.addCallback(self.__cb_left)
217 deferred.addErrback(self.__eb_left)
219 def __cb_left(self, ignored_arg):
220 # GaltackServerListWindow._instance.show()
221 self.__prev_window.show()
222 self.destroy()
224 def __eb_left(self, ignored_arg):
225 self.set_sensitive(True)
227 def __cb_quit_clicked(self, button):
228 self.set_sensitive(False)
229 deferred = self.galtack_client.logout()
230 deferred.addCallback(self.__cb_left_quit)
231 deferred.addErrback(self.__cb_left_quit)
233 def __cb_left_quit(self, ignored_arg):
234 self.destroy()
236 def __cb_send_clicked(self, button):
237 msg = self.input_entry.get_text()
238 self.galtack_client.send_commands((1, "message", msg))
239 self.input_entry.set_text("")
241 def __cmd_message(self, command):
242 sender, message = command[3:]
243 snd = ""
244 if sender: snd = " " + sender
245 self.__buf("%s%s %s\n" % (time.strftime("%X"), snd, message))
247 def __cmd_close(self, command):
248 self.galtack_client.send_commands((1, "[CLOSE]"))
249 gobject.timeout_add(500, main_loop_quit) # TODO: await ACK
251 def __cmd_stop(self, command):
252 self.__buf("(stop)\n")
254 def __cmd_start(self, command):
255 self.__buf("(start)\n")
258 class GaltackServerListWindow(Window):
260 Window displaying the list of Galcon servers.
263 NEXT_CLASS = GaltackChatWindow
265 def __init__(self, prev_window, options, user_info):
266 self.__class__._instance = self
267 self.__prev_window = prev_window
268 self.__options = options
269 super(GaltackServerListWindow, self).__init__()
270 self.__user_info = user_info
271 self.set_title("Galcon Server List - GalTacK")
272 self.set_default_size(600, 400)
273 self.set_position(gtk.WIN_POS_CENTER)
275 outer_vbox, self.__inner_vbox = gnome_hig(self)
277 hbox = gnome_hig(self.__inner_vbox(HBox(), False, False))
278 self.__version_label = hbox(LeftLabel(""), False, False)
279 self.set_current_version()
280 self.__version_label.set_sensitive(False)
282 self.__refresh_button = hbox(Button("_Refresh List"), False, False,
283 pack_end = True)
284 self.__refresh_button.connect("clicked", self.__cb_refresh)
286 sw = self.__inner_vbox(Frame())(ScrolledWindow())
287 self.__treeview = sw(TreeView())
288 self.__treeview.set_sensitive(False)
289 self.__treeview.set_property("rules-hint", True)
290 self.__treeview.connect("row-activated", self.__cb_row_activated)
291 cb = self.__cb_selection_changed
292 self.__treeview.get_selection().connect("changed", cb)
294 for i, spec in enumerate(galtack.net.ServerInfo.COL_SPEC):
295 if not spec["visible"]: continue
296 col = gtk.TreeViewColumn(spec["caption"])
297 col.set_reorderable(True)
298 col.set_sort_column_id(i)
299 self.__treeview.append_column(col)
300 cell = gtk.CellRendererText()
301 col.pack_start(cell, True)
302 col.add_attribute(cell, "text", i)
303 col.add_attribute(cell, "sensitive", 0)
305 hbox = gnome_hig(self.__inner_vbox(HBox(), False, False))
306 self.__join_button = hbox(Button("_Join"))
307 self.__join_button.set_sensitive(False)
308 self.__join_button.connect("clicked", self.__cb_join)
310 self.back_button = hbox(Button("_Back"), False, False)
311 self.back_button.connect("clicked", self.__cb_back)
313 self.__statusbar = outer_vbox(Statusbar(), False, False)
314 self.__statusbar_cid = cid = self.__statusbar.get_context_id("msg")
316 self.__server_password_prompt = dlg = Dialog(
317 "Server Password Required", self, gtk.DIALOG_MODAL,
318 (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
319 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)
321 dlg.set_default_response(gtk.RESPONSE_ACCEPT)
322 dlg.set_default_size(300, -1)
323 vbox = gnome_hig(VBox()) # I want my own VBox here ;-)
324 vbox.set_border_width(6)
325 dlg.vbox.pack_start(vbox)
326 self.__server_password_prompt_label = vbox(LeftLabel())
327 self.__server_password_prompt_entry = vbox(Entry())
328 self.__server_password_prompt_entry.set_visibility(False)
330 def set_current_version(self, current_version = None):
331 self.__version_label.set_sensitive(False)
332 if current_version is None:
333 self.__version_label.set_text("Current version: ???")
334 else:
335 self.__version_label.set_text("Current version: %s" %
336 ".".join(current_version))
337 self.__version_label.set_sensitive(True)
339 def set_server_info_list(self, server_info_list = None):
340 types = galtack.net.ServerInfo.get_col_types()
341 model = gtk.ListStore(*types)
342 self.__treeview.set_sensitive(False)
343 if server_info_list is not None:
344 for server_info in server_info_list:
345 model.append(server_info.get_data_tuple())
346 self.__treeview.set_sensitive(True)
347 self.__treeview.set_model(model)
349 def __cb_refresh(self, button):
350 self.window.set_cursor(watch_cursor)
351 self.__inner_vbox.set_sensitive(False)
352 self.__statusbar.push(self.__statusbar_cid,
353 "Retrieving server list...")
354 deferred = galtack.net.get_server_list(self.__user_info)
355 deferred.addCallback(self.__server_list_callback)
356 deferred.addErrback(self.__server_list_errback)
358 def __server_list_callback(self, (current_version, server_info_list)):
359 self.set_current_version(current_version)
360 self.set_server_info_list(server_info_list)
361 self.__statusbar.pop(self.__statusbar_cid)
362 self.__inner_vbox.set_sensitive(True)
363 self.window.set_cursor(arrow_cursor)
365 def __server_list_errback(self, failure):
366 self.set_sensitive(True)
367 self.window.set_cursor(arrow_cursor)
368 cid = self.__statusbar_cid
369 sbar = self.__statusbar
370 sbar.pop(cid)
371 gtk.gdk.beep()
372 mid = sbar.push(cid, "Retrieving server list failed")
373 gobject.timeout_add(4000, lambda: sbar.remove(cid, mid))
374 return failure
376 def __cb_selection_changed(self, treesel):
377 model, iter = treesel.get_selected()
378 if iter is None:
379 ok_to_join = False
380 else:
381 data = galtack.net.ServerInfo(*model[iter])
382 ok_to_join = data.bots_ok
383 self.__join_button.set_sensitive(ok_to_join)
385 def __cb_row_activated(self, treeview, path, view_column):
386 self.__cb_join(self.__join_button)
388 def __cb_join(self, button):
389 treesel = self.__treeview.get_selection()
390 model, iter = treesel.get_selected()
391 server_info = galtack.net.ServerInfo(*model[iter])
392 if server_info.pwd_protected:
393 self.__run_server_password_prompt(server_info)
394 return None
395 self.join_with_server_info(server_info)
397 def __cb_back(self, button):
398 # self.GaltackLoginWindow._instance.show()
399 self.__prev_window.show()
400 self.destroy()
402 def __run_server_password_prompt(self, server_info):
403 n, o = server_info["name"], server_info["owner"]
404 if n: n += " - "
405 s = "Password for %s%s:" % (n, o)
406 self.__server_password_prompt_label.set_text(s)
407 response_id = self.__server_password_prompt.run()
408 if response_id != gtk.RESPONSE_ACCEPT: return None
409 passwd = self.__server_password_prompt_entry.get_text()
410 server_info["passwd"] = passwd
411 self.join_with_server_info(server_info)
413 def join_with_server_info(self, server_info):
414 chat_window = self.NEXT_CLASS(self, self.__options,
415 self.__user_info, server_info)
416 gobject.idle_add(self.hide)
419 class GaltackLoginWindow(Window):
421 A Window asking the user for Galcon login details and log in.
424 NEXT_CLASS = GaltackServerListWindow
426 def __init__(self, options):
427 self.__class__._instance = self
428 self.__options = options
429 super(GaltackLoginWindow, self).__init__()
430 self.set_title("GalTacK Login")
431 self.set_default_size(400, -1)
432 self.set_position(gtk.WIN_POS_CENTER)
434 outer_vbox, self.inner_vbox = gnome_hig(self)
436 table = gnome_hig(self.inner_vbox(Table(), False, False))
438 xop = {"xoptions": gtk.FILL}
439 table.add_rows()
440 label = table.attach_cell(LeftLabel("_Email Address:"), **xop)
441 self.email_entry = table.attach_cell(Entry())
442 label.set_mnemonic_widget(self.email_entry)
443 if self.__options.email is not None:
444 self.email_entry.set_text(self.__options.email)
446 table.add_rows()
447 label = table.attach_cell(LeftLabel("_Username:"), **xop)
448 self.name_entry = table.attach_cell(Entry())
449 label.set_mnemonic_widget(self.name_entry)
450 if self.__options.user is not None:
451 self.name_entry.set_text(self.__options.user)
453 table.add_rows()
454 label = table.attach_cell(LeftLabel("_Password:"), **xop)
455 self.passwd_entry = table.attach_cell(Entry())
456 label.set_mnemonic_widget(self.passwd_entry)
457 self.passwd_entry.set_visibility(False)
458 if self.__options.password is not None:
459 self.passwd_entry.set_text(self.__options.password)
461 table.add_rows()
462 hbox = gnome_hig(table.attach_row(HBox()))
463 self.__login_button = hbox(Button("_Sign In"))
464 self.__login_button.connect("clicked", self.__cb_login)
465 self.__login_button.set_property("can-default", True)
466 gobject.idle_add(self.__login_button.grab_default)
468 self.__quit_button = hbox(Button("_Quit"), False, False)
469 self.__quit_button.connect("clicked", self.__cb_quit)
471 self.statusbar = outer_vbox(Statusbar(), False, False)
472 self.statusbar_cid = cid = self.statusbar.get_context_id("msg")
473 mid = self.statusbar.push(cid, "Enter your login details")
474 gobject.timeout_add(4000, lambda: self.statusbar.remove(cid, mid))
476 def __get_user_info(self):
477 email = self.email_entry.get_text()
478 name = self.name_entry.get_text()
479 passwd = self.passwd_entry.get_text()
480 platform = "linux2"
481 version = "1.2.1"
482 return galtack.net.UserInfo(email, name, passwd, platform, version)
484 def __cb_login(self, button):
485 self.window.set_cursor(watch_cursor)
486 self.inner_vbox.set_sensitive(False)
487 self.statusbar.push(self.statusbar_cid,
488 "Retrieving server list...")
489 user_info = self.__get_user_info()
490 deferred = galtack.net.get_server_list(user_info)
491 deferred.addCallbacks(self.__server_list_callback,
492 self.__server_list_errback)
493 deferred.addErrback(print_errback)
495 def __cb_quit(self, button):
496 main_loop_quit()
498 def __server_list_callback(self, (current_version, server_info_list)):
499 w = self.NEXT_CLASS(self, self.__options, self.__get_user_info())
500 w.set_current_version(current_version)
501 w.set_server_info_list(server_info_list)
502 gobject.idle_add(self.hide)
503 self.inner_vbox.set_sensitive(True)
504 self.window.set_cursor(arrow_cursor)
505 cid = self.statusbar_cid
506 self.statusbar.pop(cid)
508 def __server_list_errback(self, failure):
509 failure.trap(galtack.net.ServerListException)
510 self.inner_vbox.set_sensitive(True)
511 self.window.set_cursor(arrow_cursor)
512 cid = self.statusbar_cid
513 self.statusbar.pop(cid)
514 gtk.gdk.beep()
515 mid = self.statusbar.push(cid, "Failure: %s" %
516 failure.getErrorMessage())
517 gobject.timeout_add(4000, lambda: self.statusbar.remove(cid, mid))
518 return None
520 @classmethod # for potential inheritance
521 def _modify_option_parser(cls, option_parser):
522 option_parser.add_option("-e", "--email", metavar = "ADDRESS",
523 help = "login email address")
524 option_parser.add_option("-u", "--user",
525 help = "login username")
526 option_parser.add_option("-p", "--password",
527 help = "login password")
528 return option_parser
531 def main(argv):
532 option_parser = optparse.OptionParser(prog = "GalTacK",
533 version = "%%prog %s" %
534 galtack_version_str)
535 option_parser = GaltackLoginWindow._modify_option_parser(option_parser)
536 option_parser = GaltackClient._modify_option_parser(option_parser)
537 options, arguments = option_parser.parse_args(argv[1:])
538 GaltackLoginWindow(options)
539 main_loop_run()
540 return 0
542 if __name__ == "__main__":
543 import sys
544 sys.exit(main(sys.argv))