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