~ubuntu-branches/ubuntu/saucy/amsn/saucy

« back to all changes in this revision

Viewing changes to plugins/games/Games.tcl

  • Committer: Bazaar Package Importer
  • Author(s): Devid Antonio Filoni
  • Date: 2010-04-13 23:21:29 UTC
  • mfrom: (1.1.11 upstream) (3.1.8 sid)
  • Revision ID: james.westby@ubuntu.com-20100413232129-vgpx20brdd2qavs7
Tags: 0.98.3-0ubuntu1
* Merge from Debian unstable (LP: #449072), remaining Ubuntu changes:
  - add 08_use_aplay_for_sound.dpatch patch by Festor Wailon Dacoba to use
    aplay to play sounds
  + debian/control:
    - modify iceweasel to firefox | abrowser in amsn Suggests field
    - add xdg-utils and gstreamer0.10-nice to amsn Depends field
    - mofify sox to alsa-utils in amsn Suggests field as we are now using
      aplay
* New upstream release (LP: #562619), tarball repacked according to
  debian/README.source.
* Fix missing-debian-source-format lintian warning.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#############################################################################
 
2
#  ::Games => play games with aMSN.                                                                                     #
 
3
#  ======================================================================== #
 
4
#   Play games with aMSN.                                                                                                       #
 
5
#                                                                                                                                                       #
 
6
#       Original author: JeeBee <jonne_z REM0VEatTH1S users.sourceforge.net>    #
 
7
#       Contributors:                                                                                                                   #
 
8
#   - Thanks to Tjikkun for the many usefull tips about the protocol used       #
 
9
#     and for his time to discuss the plugin with me.                                           #
 
10
#   - Thanks to Scapor for testing with me, begin enthusiastic and having       #
 
11
#     such a long nickname I immediately knew I had to truncate this.           #
 
12
#   - Thanks to Billiob (and Tjikkun, who said just use 1500) for                       #
 
13
#         determining the maximum packet size before the msn protocol fails             #
 
14
#         (Billiob determined it to be slightly more than 1660). I use 1500             #
 
15
#         to be on the safe side (and I'm not counting exact).                                  #
 
16
#       - Thanks to Mary for testing with me, Spanish translation,                              #
 
17
#         and sharing ideas to improve this plugin.                                                             #
 
18
#       - Thanks to Youness for a very impressive ten minutes of testing                #
 
19
#         (Youness? You sure it was only 10 minutes???)                                                 #
 
20
#         in which he suggested to use x-clientcaps, advertising when opponent  #
 
21
#         does not have the plugin and even tried playing with three players    #
 
22
#         in which he addressed some *multi*-player issues.                                             #
 
23
#       - Thanks to Jeroen for drawing the Hangman images.                                              #
 
24
#############################################################################
 
25
 
 
26
# FIXME: Injecting code is not possible as I don't use eval/exec/file open etc on 
 
27
# incoming data. However, how can I verify that messages are not duplicated (whether 
 
28
# or not on purpose) or just that messages are sent that are valid but sended on 
 
29
# purpose by malicious opponents or intruders that by some reason know the $gameID? 
 
30
# E.g. it shouldn't be TOO easy to send a "PLAYER=me,SCORE=infinite" command.
 
31
# One way would be to implement a message counter that increments on both sides and
 
32
# must always be equal? Anyone reading this who thinks this is important?
 
33
 
 
34
# FIXME: In a conference, if 1 participant does not have the plugin loaded/installed,
 
35
# we do not get enough Pongs and do not send an invite. Should we set this to a minimum?
 
36
# Using x-clientcaps could solve this ...
 
37
#
 
38
#Youness: btw, JeeBee, how you know it's a user that supports it.. I (hoping the  
 
39
#even exists) propose you send a message at user JOIN just like the  
 
40
#x-clientcaps of type application/x-amsngames-init with games supportes,  
 
41
#version info, etc.. just so you know whether the user has it or not, and  
 
42
#which games /version he has... so you will always know (without need for  
 
43
#timeout) if user supports it..
 
44
 
 
45
# FIXME: Use version checking to avoid starting games that don't exist
 
46
# and other games-plugin incompatibilities.
 
47
 
 
48
namespace eval ::Games {
 
49
  # Where to obtain aMSN and the Games plugin?
 
50
  variable amsn_url  "http://amsn.sf.net"
 
51
  variable games_url "http://sf.net/tracker/index.php?func=detail&aid=1414743&group_id=54091&atid=733148"
 
52
  # games_url soon something like "http://amsn.sf.net/plugins.php#Games" or #games ?
 
53
 
 
54
  variable dir ""
 
55
  variable TwoPlayerGames {"Dots_and_Boxes" "Hangman" "Chess"}
 
56
  variable MultiPlayerGames {"Sketch"}
 
57
  variable version
 
58
  variable timeout_len 10000
 
59
  variable max_nick_len 30
 
60
  variable max_packet_size 1500
 
61
 
 
62
  array set CurrentGames {}
 
63
  array set OpenChallenges {} 
 
64
  # Game     - "$gameID,game"
 
65
  # GameInit - "$gameID,init"
 
66
  # Opponent - "$gameID,chatid"
 
67
  # Timeout  - "$gameID,timeout"
 
68
 
 
69
  ###########################################################################
 
70
  # ::Init (dir)                                                                                                        #
 
71
  # ----------------------------------------------------------------------- #
 
72
  # Registration & initialization of the plugin                                                         #
 
73
  ###########################################################################
 
74
  proc Init { dir } {
 
75
    variable TwoPlayerGames
 
76
        variable MultiPlayerGames
 
77
    variable version
 
78
 
 
79
    set ::Games::dir $dir
 
80
        if {[catch {::plugins::pluginVersion} version]} {
 
81
          # Setting version number manually for aMSN 0.95
 
82
          # Make sure this value matches the one used in plugininfo.xml
 
83
          set version "0.20"
 
84
        }
 
85
 
 
86
    ::plugins::RegisterPlugin "Games"
 
87
 
 
88
    # Register events
 
89
    ::plugins::RegisterEvent "Games" PacketReceived PacketReceived
 
90
    ::plugins::RegisterEvent "Games" chatmenu edit_menu
 
91
 
 
92
    # Load language files
 
93
    set langdir [file join $dir "lang"]
 
94
    set lang [::config::getGlobalKey language]
 
95
    load_lang en $langdir
 
96
    load_lang $lang $langdir
 
97
 
 
98
    # Source the games
 
99
    foreach game [concat $TwoPlayerGames $MultiPlayerGames] {
 
100
      if { [catch { source [file join $dir "$game.tcl"] } res] } {
 
101
        msg_box "[trans load_game_failed] ($game.tcl):\n$res"
 
102
        return 0
 
103
      }
 
104
    }
 
105
 
 
106
    # Handle plugin configuration
 
107
    ::Games::config_array
 
108
    ::Games::configlist_values
 
109
 
 
110
        log "Games plugin version $version loaded."
 
111
  }
 
112
 
 
113
  ###########################################################################
 
114
  # ::edit_menu                                                                                                                         #
 
115
  # ----------------------------------------------------------------------- #
 
116
  # Edit chat window menu                                                                                                       #
 
117
  ###########################################################################
 
118
  proc edit_menu { event evpar } {
 
119
    upvar 2 $evpar newvar
 
120
    set window_name $newvar(window_name)
 
121
    set menu_name $newvar(menu_name)
 
122
 
 
123
    # Add games menu
 
124
    menu ${menu_name}.gmenu -tearoff 0
 
125
    menu ${menu_name}.gmenu.gmenu_two -tearoff 0
 
126
    menu ${menu_name}.gmenu.gmenu_multi -tearoff 0
 
127
    ${menu_name} add cascade -label "[trans Games]" -menu ${menu_name}.gmenu
 
128
    ${menu_name}.gmenu add cascade -label "[trans TwoPlayerGames]" -menu ${menu_name}.gmenu.gmenu_two
 
129
    ${menu_name}.gmenu add cascade -label "[trans MultiPlayerGames]" -menu ${menu_name}.gmenu.gmenu_multi
 
130
    # Add two-player games
 
131
    foreach game $::Games::TwoPlayerGames {
 
132
      ${menu_name}.gmenu.gmenu_two add command -label "[trans $game]" \
 
133
        -command "::Games::StartGame \[::ChatWindow::getCurrentTab $window_name\] $game p2"
 
134
    }
 
135
    # Add multi-player games
 
136
    foreach game $::Games::MultiPlayerGames {
 
137
      ${menu_name}.gmenu.gmenu_multi add command -label "[trans $game]" \
 
138
        -command "::Games::StartGame \[::ChatWindow::getCurrentTab $window_name\] $game pn"
 
139
    }
 
140
        # 0 or 1 disables TwoPlayerGames or MultiPlayerGames respectively
 
141
        #${menu_name}.gmenu entryconfigure 1 -state disabled
 
142
  }
 
143
 
 
144
  ###########################################################################
 
145
  # ::trans                                                                                                                                     #
 
146
  # ----------------------------------------------------------------------- #
 
147
  # Because the individual games are not registered as plugins, they use        #
 
148
  # this namespace's language files and have to call proc trans via this        #
 
149
  # proc, so ::plugins::calledFrom knows it should use our language file.       #
 
150
  ###########################################################################
 
151
  proc trans { key args } {
 
152
        # eval trans $key $args -->> DON'T USE THIS !!!
 
153
        # (could be exploited by Tcl-code in a nickname!!!)
 
154
        eval [linsert $args 0 ::trans $key]
 
155
  }
 
156
 
 
157
  ###########################################################################
 
158
  # ::load_lang                                                                                                                         #
 
159
  # ----------------------------------------------------------------------- #
 
160
  # Some games want to have their own dictionaries and abuse language files     #
 
161
  # to achieve this. This function provides an easy wrapper to load a           #
 
162
  # language file (the issue here is the same als proc trans, see above).       #
 
163
  ###########################################################################
 
164
  proc load_lang {langKey langDir} {
 
165
        ::load_lang $langKey $langDir
 
166
  }
 
167
 
 
168
  ###########################################################################
 
169
  # ::StartGame                                                                                                                         #
 
170
  # ----------------------------------------------------------------------- #
 
171
  # Start selected game                                                                                                         #
 
172
  ###########################################################################
 
173
  proc StartGame { windowtab game gametype } {
 
174
    variable OpenChallenges
 
175
        variable timeout_len
 
176
 
 
177
    set chatid [::ChatWindow::Name $windowtab]
 
178
    set gameID [GenerateGameID $chatid $gametype]
 
179
        set user_list [::MSN::usersInChat $chatid]
 
180
 
 
181
        if {[string first "p2" $gameID] == 0 && \
 
182
        [string first "::MSN::SB" $chatid] == -1} {
 
183
          # Two-player game
 
184
          set game_init [::Games::${game}::init_game $gameID \
 
185
                [::config::getKey login] $chatid]
 
186
        } elseif {[string first "pn" $gameID] == 0 && \
 
187
              [string first "::MSN::SB" $chatid] > -1} {
 
188
          # Multi-player game
 
189
          set game_init [::Games::${game}::init_game $gameID [::config::getKey login]]
 
190
        } else {
 
191
          WrongGameType $chatid
 
192
          set game_init ""
 
193
    }
 
194
 
 
195
    if {"$game_init" != ""} {
 
196
          # Set timers for Ping message to timeout_len milliseconds
 
197
          set timeouts {}
 
198
          foreach user $user_list {
 
199
                lappend timeouts $user
 
200
                lappend timeouts [after $timeout_len [list ::Games::pongReceived $gameID $user 0]]
 
201
          }
 
202
 
 
203
      # Add this Invite to OpenChallenges
 
204
      array set OpenChallenges [list \
 
205
        "$gameID,game"     "$game" \
 
206
                "$gameID,host"     "[config::getKey login]" \
 
207
        "$gameID,init"     "$game_init" \
 
208
        "$gameID,chatid"   "$chatid" \
 
209
        "$gameID,timeouts" "$timeouts" ]
 
210
 
 
211
          # First thing to do is sent a Ping to see if your opponent has our
 
212
          # Games plugin loaded as well.
 
213
          send_via_queue $gameID "Ping"
 
214
    }
 
215
  }
 
216
 
 
217
  ###########################################################################
 
218
  # ::pongReceived                                                                                                      #
 
219
  # ----------------------------------------------------------------------- #
 
220
  # Opponent also has this (or compatible) plugin loaded.                                       #
 
221
  # Do a version check, warn if version differs, then continue                          #
 
222
  # by sending our invitation                                                                                           #
 
223
  ###########################################################################
 
224
  proc pongReceived { gameID sender success {oppVersion ""} } {
 
225
    variable OpenChallenges
 
226
 
 
227
    if {[info exists OpenChallenges($gameID,chatid)] && \
 
228
        $OpenChallenges($gameID,host) == [config::getKey login]} {
 
229
          if {$success} {
 
230
        # Turn off timer of sender
 
231
                set i 0
 
232
                foreach {user timer} $OpenChallenges($gameID,timeouts) {
 
233
                  if {"$user" == "$sender"} {
 
234
                        after cancel $timer
 
235
                        set OpenChallenges($gameID,timeouts) \
 
236
                          [lreplace $OpenChallenges($gameID,timeouts) $i [expr {$i+1}]]
 
237
                        break
 
238
                  }
 
239
                  set i [expr {$i+2}]
 
240
                }
 
241
                # If all timeouts are gone, all Pongs are received correctly
 
242
                if {[llength $OpenChallenges($gameID,timeouts)] == 0} {
 
243
                  # Send the game invitation
 
244
                  send_via_queue $gameID "Invite" \
 
245
                        "$OpenChallenges($gameID,game),$OpenChallenges($gameID,init)"
 
246
                  ::Games::InvitationSent $gameID
 
247
                }
 
248
                ::Games::VersionConflict $gameID $sender $oppVersion
 
249
          } else {
 
250
                # Timout occured, apparantly sender doesn't have plugin loaded
 
251
                ::Games::PluginNotLoaded $gameID $sender $OpenChallenges($gameID,chatid)
 
252
 
 
253
                # Remove from OpenChallenges
 
254
        array unset OpenChallenges "$gameID,*"
 
255
          }
 
256
    }
 
257
  }
 
258
 
 
259
  ###########################################################################
 
260
  # ::SendMove                                                                                                          #
 
261
  # ----------------------------------------------------------------------- #
 
262
  # Send move to opponent                                                                                                       #
 
263
  ###########################################################################
 
264
  proc SendMove { gameID move } {
 
265
    send_via_queue $gameID "Move" "$move"
 
266
  }
 
267
 
 
268
  ###########################################################################
 
269
  # ::SendQuit                                                                                                          #
 
270
  # ----------------------------------------------------------------------- #
 
271
  # Inform opponent that we quit our game                                                                       #
 
272
  ###########################################################################
 
273
  proc SendQuit { gameID } {
 
274
    send_via_queue $gameID "Quit"
 
275
  }
 
276
 
 
277
  ###########################################################################
 
278
  # ::DeInit (dir)                                                                                              #
 
279
  # ----------------------------------------------------------------------- #
 
280
  # Plugin is unloaded                                                                                                          #
 
281
  ###########################################################################
 
282
  proc DeInit { } {
 
283
  }
 
284
 
 
285
  ###########################################################################
 
286
  # ::log message                                                                                                       #
 
287
  # ----------------------------------------------------------------------- #
 
288
  # Add a log message to plugins-log window                                                             #
 
289
  # Type Alt-P to get that window                                                                       #
 
290
  ###########################################################################
 
291
  proc log {message} {
 
292
 
 
293
    plugins_log Games $message
 
294
    #puts "log: $message"
 
295
  }
 
296
 
 
297
  ###########################################################################
 
298
  # ::config_array                                                                                                      #
 
299
  # ----------------------------------------------------------------------- #
 
300
  # Add config array with default values                                                                        #
 
301
  ###########################################################################
 
302
  proc config_array {} {
 
303
        variable TwoPlayerGames
 
304
        variable MultiPlayerGames
 
305
 
 
306
        array set ::Games::config {}
 
307
        foreach game [concat $TwoPlayerGames $MultiPlayerGames] {
 
308
          set game_config [::Games::${game}::config_array]
 
309
          foreach {key value} $game_config {
 
310
                set ::Games::config($key) $value
 
311
          }
 
312
        }
 
313
  }
 
314
 
 
315
  ###########################################################################
 
316
  # ::configlist_values                                                                                         #
 
317
  # ----------------------------------------------------------------------- #
 
318
  # List of items for config window                                                                             #
 
319
  ###########################################################################
 
320
  proc configlist_values {} {
 
321
        set ::Games::configlist \
 
322
          [list [list frame ::Games::build_config_frame]]
 
323
  }
 
324
 
 
325
  # A tab for each individual game (that wants one)
 
326
  proc build_config_frame { w } {
 
327
        variable TwoPlayerGames
 
328
        variable MultiPlayerGames
 
329
 
 
330
    set nb [NoteBook $w.nb -side top]
 
331
        set p 0
 
332
        set raised ""
 
333
        foreach game [concat $TwoPlayerGames $MultiPlayerGames] {
 
334
      $nb insert $p w$game -text [trans $game]
 
335
          set pane [$w.nb getframe w$game]
 
336
          if { [::Games::${game}::build_config $pane] == 1 } {
 
337
            incr p
 
338
            if {"$raised" == ""} {
 
339
                  set raised w$game
 
340
            }
 
341
          } else {
 
342
                # Oops, game did not want configuration items, remove pane
 
343
                $nb delete w$game
 
344
          }
 
345
        }
 
346
 
 
347
    pack $nb -fill both -expand 1 -in $w
 
348
    $nb raise $raised
 
349
  }
 
350
 
 
351
  ###########################################################################
 
352
  # ::PacketReceived                                                                                            #
 
353
  # ----------------------------------------------------------------------- #
 
354
  # Process incoming amsn-games packets                                                                         #
 
355
  ###########################################################################
 
356
  proc PacketReceived { event evpar } {
 
357
    variable OpenChallenges
 
358
    variable CurrentGames
 
359
 
 
360
    upvar 2 $evpar args
 
361
    upvar 2 $args(chatid) chatid
 
362
    upvar 2 $args(msg) packet
 
363
        # Find out who sended the message
 
364
        if {[info exists evpar(typer)]} {
 
365
          upvar 2 $args(typer) typer 
 
366
        } else { 
 
367
          upvar 2 typer typer 
 
368
        }
 
369
 
 
370
    # Check for the correct Content-type
 
371
    set header "[$packet getHeader Content-Type]"
 
372
    if {[string first "text/x-amsngames" $header] == -1} {
 
373
      return
 
374
    }
 
375
 
 
376
        set oppVersion "[$packet getHeader VERSION]"
 
377
    set gameID     "[$packet getHeader GAMEID]"
 
378
    set msgType    "[encoding convertfrom utf-8 [$packet getHeader MSGTYPE]]"
 
379
    set msgBody    "[encoding convertfrom utf-8 [$packet getBody]]"
 
380
 
 
381
        #log "Game command $msgType -> $msgBody"
 
382
 
 
383
        if {[regexp {[a-zA-Z0-9 _,=@\.]*} $msgBody]} {
 
384
      # Incoming message matches regular expression, continue processing
 
385
 
 
386
          # FIXME: add a Withdraw message? But what do we do then in case
 
387
          # our opponent accepts our challenge while the Withdraw message is
 
388
          # under way? Idea: send a Withdraw message to remove the pending
 
389
          # Invite and a Quit message right after that?
 
390
      
 
391
      # FIXME: Unsupported msg not yet supported
 
392
 
 
393
      switch -exact $msgType {
 
394
                "Ping" {
 
395
                  # Ping()
 
396
                  # If a ping is received, send a pong back to inform opponent that we
 
397
                  # indeed have this (or compatible) plugin loaded
 
398
 
 
399
                  array set OpenChallenges [list \
 
400
                        "$gameID,chatid"        $chatid \
 
401
                        "$gameID,host"          $typer ]
 
402
 
 
403
                  send_via_queue $gameID "Pong"
 
404
                }
 
405
                "Pong" {
 
406
                  # Pong()
 
407
                  # Opponent sent a Pong, he has our (or compatible) plugin loaded.
 
408
                  pongReceived $gameID $typer 1 $oppVersion
 
409
                }
 
410
        "Invite" {
 
411
          # Invite(Game,GameInit)
 
412
          set idx [string first "," $msgBody]
 
413
          if { $idx == -1 } { return }
 
414
          set game [string range $msgBody 0 [expr {$idx-1}]]
 
415
          set game_init [string range $msgBody [expr {$idx+1}] end]
 
416
 
 
417
                  # Add this Invite to OpenChallenges
 
418
                  array set OpenChallenges [list \
 
419
                        "$gameID,game"   "$game" \
 
420
                        "$gameID,init"   "$game_init" \
 
421
                        "$gameID,chatid" "$chatid" \
 
422
                        "$gameID,host"   "$typer" ]
 
423
 
 
424
          AcceptOrRefuse $gameID $oppVersion
 
425
        }
 
426
        "Accept" {
 
427
          # Accept(Game,GameInit)
 
428
          set idx [string first "," $msgBody]
 
429
          if { $idx == -1 } { return }
 
430
          set game [string range $msgBody 0 [expr {$idx-1}]]
 
431
          set game_init [string range $msgBody [expr {$idx+1}] end]
 
432
          ::Games::InvitationResponse $gameID $typer 1 $game_init
 
433
        }
 
434
        "Decline" {
 
435
          # Decline(Game,GameInit)
 
436
          ::Games::InvitationResponse $gameID $typer 0
 
437
        }
 
438
        "Move" {
 
439
          # Move(TheMove)
 
440
          if {[info exists CurrentGames($gameID,chatid)]} {
 
441
                        set game $CurrentGames($gameID,game)
 
442
                        ::Games::${game}::opponent_moves $gameID $typer $msgBody
 
443
          } else {
 
444
            log "Move $msgBody received for a non-existing game $gameID"
 
445
          }
 
446
        }
 
447
        "Quit" {
 
448
          # Quit()
 
449
          if {[info exists CurrentGames($gameID,chatid)]} {
 
450
                        set game $CurrentGames($gameID,game)
 
451
                        ::Games::${game}::opponent_quits $gameID $typer
 
452
          } else {
 
453
            log "Quit message received for a non-existing game $gameID"
 
454
          }
 
455
        }
 
456
        default {
 
457
          log "Unknown command: $msgType"
 
458
        }
 
459
      }
 
460
    } else {
 
461
          log "Message body does not match our regular expression: $msgBody"
 
462
        } 
 
463
  }
 
464
 
 
465
  proc InvitationSent { gameID } {
 
466
        variable OpenChallenges
 
467
        variable version
 
468
 
 
469
    if {![info exists OpenChallenges($gameID,chatid)]} {
 
470
      log "InvitationSent called with unknown gameID $gameID"
 
471
    } else {
 
472
          set chatid    $OpenChallenges($gameID,chatid)
 
473
          set game      $OpenChallenges($gameID,game)
 
474
          set game_init $OpenChallenges($gameID,init)
 
475
 
 
476
          ::amsn::WinWrite $chatid "\n" green
 
477
          ::amsn::WinWriteIcon $chatid greyline 3
 
478
          ::amsn::WinWrite $chatid "\n" green
 
479
          ::amsn::WinWriteIcon $chatid butinvite 3 2
 
480
          ::amsn::WinWrite $chatid "[timestamp] [trans invite_sent] " green
 
481
          ::amsn::WinWrite $chatid "[trans $game] " red
 
482
          ::amsn::WinWrite $chatid "(${game_init})\n" green
 
483
          ::amsn::WinWriteIcon $chatid greyline 3
 
484
    }
 
485
  }
 
486
 
 
487
  proc VersionConflict { gameID opponent oppVersion } {
 
488
        variable OpenChallenges
 
489
        variable version
 
490
 
 
491
    if {![info exists OpenChallenges($gameID,chatid)]} {
 
492
      log "VersionConflict called with unknown gameID $gameID"
 
493
    } else {
 
494
          set chatid    $OpenChallenges($gameID,chatid)
 
495
 
 
496
          if {$version != $oppVersion} {
 
497
                ::amsn::WinWrite $chatid \
 
498
                  "[trans incompatible_version $version $opponent $oppVersion]\n" red
 
499
          }
 
500
        }
 
501
  }
 
502
 
 
503
  proc WrongGameType {chatid} {
 
504
        ::amsn::WinWrite $chatid "\n[trans wrong_game_type]\n" red
 
505
  }
 
506
 
 
507
  # Accept or Decline message received
 
508
  proc InvitationResponse { gameID sender response { game_init "" } } {
 
509
        variable OpenChallenges
 
510
    variable CurrentGames
 
511
 
 
512
    if {![info exists OpenChallenges($gameID,chatid)]} {
 
513
      log "InvitationResponse called with unknown gameID $gameID"
 
514
    } else {
 
515
          set chatid    $OpenChallenges($gameID,chatid)
 
516
          set game      $OpenChallenges($gameID,game)
 
517
          set started   [info exists CurrentGames($gameID,chatid)]
 
518
 
 
519
          ::amsn::WinWrite $chatid "\n" green
 
520
          ::amsn::WinWriteIcon $chatid greyline 3
 
521
          ::amsn::WinWrite $chatid "\n" green
 
522
          ::amsn::WinWriteIcon $chatid butinvite 3 2
 
523
          if {$response == 1} {
 
524
                # chatid accepted the invitation
 
525
                ::amsn::WinWrite $chatid "[timestamp] [trans accepted_invitation [getNick $sender]] " green
 
526
                ::amsn::WinWrite $chatid "[trans $game] " red
 
527
                ::amsn::WinWrite $chatid "(${game_init})\n" green
 
528
                if {$OpenChallenges($gameID,host) == [::config::getKey login]} {
 
529
 
 
530
                  # Add game to CurrentGames
 
531
                  array set CurrentGames [list \
 
532
                        "$gameID,game"   "$game" \
 
533
                        "$gameID,init"   "$game_init" \
 
534
                        "$gameID,chatid" "$chatid" ]
 
535
 
 
536
                  if {[string first "p2" $gameID] == 0} {
 
537
                        # Two-player game
 
538
                        array unset OpenChallenges "$gameID,*"
 
539
                        # Process opponent's init string
 
540
                        ::Games::${game}::set_init_game $gameID $game_init \
 
541
                          [::config::getKey login] $chatid 
 
542
                        ::Games::${game}::start_game $gameID
 
543
                  } elseif {!$started} {
 
544
                        # Multi-player game (this is our first Accept)
 
545
                        ::Games::${game}::start_game $gameID
 
546
                        ::Games::${game}::add_player $gameID [::config::getKey login]
 
547
                        ::Games::${game}::add_player $gameID $sender
 
548
                  } else {
 
549
                        # Multi-player game (already started)
 
550
                        ::Games::${game}::add_player $gameID $sender
 
551
                  }
 
552
                } else {
 
553
                  # We are not the host of the game
 
554
                  if {[string first "pn" $gameID] == 0} {
 
555
                        ::Games::${game}::add_player $gameID $sender
 
556
                  }
 
557
                }
 
558
          } else {
 
559
                # chatid declined the invitation
 
560
                ::amsn::WinWrite $chatid "[timestamp] [trans declined_invitation [getNick $sender]] " green
 
561
                ::amsn::WinWrite $chatid "[trans $game] " red
 
562
                ::amsn::WinWrite $chatid "($OpenChallenges($gameID,init))\n" green
 
563
          }
 
564
          ::amsn::WinWriteIcon $chatid greyline 3
 
565
    }
 
566
  }
 
567
 
 
568
  proc AcceptOrRefuse { gameID oppVersion } {
 
569
        variable OpenChallenges
 
570
 
 
571
    if {![info exists OpenChallenges($gameID,chatid)]} {
 
572
      log "AcceptOrRefuse called with unknown gameID $gameID"
 
573
    } else {
 
574
          set chatid    $OpenChallenges($gameID,chatid)
 
575
          set host      $OpenChallenges($gameID,host)
 
576
          set game      $OpenChallenges($gameID,game)
 
577
          set game_init $OpenChallenges($gameID,init)
 
578
 
 
579
          # Check whether we have our conversation window open for WinWrites
 
580
          set win_name [::ChatWindow::For $chatid]
 
581
          if { [::ChatWindow::For $chatid] == 0 || ![winfo exists $win_name]} {
 
582
                set win_name [::ChatWindow::MakeFor $chatid]
 
583
          }
 
584
 
 
585
          ::MSN::ChatQueue $chatid \
 
586
                [list ::Games::AcceptOrRefuse_wrapped $gameID $chatid $host $game $game_init $oppVersion]
 
587
    }
 
588
  }
 
589
 
 
590
  proc AcceptOrRefuse_wrapped { gameID chatid host game game_init oppVersion } {
 
591
    variable version
 
592
 
 
593
    # Grey line
 
594
    ::amsn::WinWrite $chatid "\n" green
 
595
    ::amsn::WinWriteIcon $chatid greyline 3
 
596
    ::amsn::WinWrite $chatid "\n" green
 
597
 
 
598
    ::amsn::WinWriteIcon $chatid butinvite 3 2
 
599
    # Show invitation
 
600
    ::amsn::WinWrite $chatid "[trans wants_to_play [getNick $host]] " green
 
601
    ::amsn::WinWrite $chatid "[trans $game] " red
 
602
    ::amsn::WinWrite $chatid "(${game_init})" green
 
603
 
 
604
    # Accept and refuse actions
 
605
    ::amsn::WinWrite $chatid " - (" green
 
606
    ::amsn::WinWriteClickable $chatid "[trans Accept]" \
 
607
      [list ::Games::InvitationAnswered $gameID $host 1] \
 
608
      "acceptgame$gameID"
 
609
    ::amsn::WinWrite $chatid " / " green
 
610
    ::amsn::WinWriteClickable $chatid "[trans Reject]" \
 
611
      [list ::Games::InvitationAnswered $gameID $host 0] \
 
612
      "rejectgame$gameID"
 
613
    ::amsn::WinWrite $chatid ")\n" green
 
614
 
 
615
    # Grey line
 
616
    ::amsn::WinWriteIcon $chatid greyline 3
 
617
 
 
618
        if {$version != $oppVersion} {
 
619
          ::amsn::WinWrite $chatid \
 
620
                "\n[trans incompatible_version $version $host $oppVersion]\n" red
 
621
        }
 
622
  }
 
623
 
 
624
  proc PluginNotLoaded { gameID sender chatid } {
 
625
        variable timeout_len
 
626
        variable amsn_url
 
627
        variable games_url
 
628
 
 
629
    # Grey line
 
630
    ::amsn::WinWrite $chatid "\n" green
 
631
    ::amsn::WinWriteIcon $chatid greyline 3
 
632
    ::amsn::WinWrite $chatid "\n" green
 
633
 
 
634
    ::amsn::WinWriteIcon $chatid butinvite 3 2
 
635
    # Show invitation
 
636
    ::amsn::WinWrite $chatid "[trans missing_plugin [getNick $sender]] " green
 
637
    ::amsn::WinWrite $chatid "([trans timeout [expr {$timeout_len/1000}]])." green
 
638
 
 
639
    # Grey line
 
640
    ::amsn::WinWriteIcon $chatid greyline 3
 
641
 
 
642
        # Send aMSN/Games plugin advertisement
 
643
        ::MSN::messageTo $chatid \
 
644
      "[trans advertise [getNick [::config::getKey login]] [getNick $sender] $amsn_url $games_url]" 0 ""
 
645
  }
 
646
 
 
647
  proc InvitationAnswered { gameID host answer } {
 
648
        variable OpenChallenges
 
649
    variable CurrentGames
 
650
 
 
651
    if {![info exists OpenChallenges($gameID,chatid)]} {
 
652
      log "InvitationAnswered called with unknown gameID $gameID"
 
653
    } else {
 
654
          set chatid    $OpenChallenges($gameID,chatid)
 
655
          set game      $OpenChallenges($gameID,game)
 
656
          set game_init $OpenChallenges($gameID,init)
 
657
 
 
658
          # Get the chatwindow name
 
659
          set win_name [::ChatWindow::For $chatid]
 
660
          if { [::ChatWindow::For $chatid] == 0} {
 
661
                return 0
 
662
          }
 
663
 
 
664
          # Disable items in the chatwindow
 
665
          [::ChatWindow::GetOutText ${win_name}] tag configure "acceptgame$gameID" \
 
666
                -foreground #808080 -font bplainf -underline false
 
667
          [::ChatWindow::GetOutText ${win_name}] tag bind "acceptgame$gameID" <Enter> ""
 
668
          [::ChatWindow::GetOutText ${win_name}] tag bind "acceptgame$gameID" <Leave> ""
 
669
          [::ChatWindow::GetOutText ${win_name}] tag bind "acceptgame$gameID" <Button1-ButtonRelease> ""
 
670
 
 
671
          [::ChatWindow::GetOutText ${win_name}] tag configure "rejectgame$gameID" \
 
672
                -foreground #808080 -font bplainf -underline false
 
673
          [::ChatWindow::GetOutText ${win_name}] tag bind "rejectgame$gameID" <Enter> ""
 
674
          [::ChatWindow::GetOutText ${win_name}] tag bind "rejectgame$gameID" <Leave> ""
 
675
          [::ChatWindow::GetOutText ${win_name}] tag bind "rejectgame$gameID" <Button1-ButtonRelease> ""
 
676
 
 
677
          [::ChatWindow::GetOutText ${win_name}] conf -cursor left_ptr
 
678
 
 
679
          if {$answer == 1} {
 
680
        # Add game to CurrentGames
 
681
                array set CurrentGames [list \
 
682
                  "$gameID,game"   "$game" \
 
683
                  "$gameID,host"   "$host" \
 
684
                  "$gameID,init"   "$game_init" \
 
685
                  "$gameID,chatid" "$chatid" ]
 
686
                if {[string first "p2" $gameID] == 0} {
 
687
          # Remove from OpenChallenges
 
688
          array unset OpenChallenges "$gameID,*"
 
689
                  # Two-player game
 
690
                  set game_init [::Games::${game}::set_init_game \
 
691
                        $gameID $game_init [::config::getKey login] $chatid]
 
692
                } else {
 
693
                  # Multi-player game
 
694
                  ::Games::${game}::set_init_game $gameID $host $game_init
 
695
                  ::Games::${game}::add_player $gameID [::config::getKey login]
 
696
                  ::Games::${game}::add_player $gameID $host
 
697
                }
 
698
                # Invitation accepted
 
699
                send_via_queue $gameID "Accept" "${game},${game_init}"
 
700
                ::Games::${game}::start_game $gameID
 
701
          } else {
 
702
                # Invitation rejected
 
703
                send_via_queue $gameID "Decline" "${game},${game_init}"
 
704
      }
 
705
    }
 
706
  }
 
707
 
 
708
  proc getNick { chatid } {
 
709
    variable max_nick_len
 
710
 
 
711
        if {$chatid == [::config::getKey login]} {
 
712
          set nick [::abook::getPersonal MFN]
 
713
        } else {
 
714
          set nick [::abook::getDisplayNick $chatid]
 
715
        }
 
716
        
 
717
        # Now truncate nick so it is no longer than max_nick_len
 
718
        if {[expr {[string length $nick] > $max_nick_len}]} {
 
719
          set nick [string range $nick 0 [expr {$max_nick_len - 3}]]
 
720
          set nick "$nick..."
 
721
        }
 
722
 
 
723
        return $nick
 
724
  }
 
725
 
 
726
  ###########################################################################
 
727
  # ::send_via_queue                                                                                            #
 
728
  # ----------------------------------------------------------------------- #
 
729
  # Send a message to the opponent                                                                              #
 
730
  ###########################################################################
 
731
  proc send_via_queue { gameID msgType {msgBody ""} } {
 
732
    variable OpenChallenges
 
733
    variable CurrentGames
 
734
 
 
735
    if {[info exists CurrentGames($gameID,chatid)]} {
 
736
      set chatid $CurrentGames($gameID,chatid)
 
737
    } elseif {[info exists OpenChallenges($gameID,chatid)]} {
 
738
      set chatid $OpenChallenges($gameID,chatid)
 
739
    } else {
 
740
      log "send_via_queue called with non-existing gameID $gameID"
 
741
    }
 
742
 
 
743
    ::MSN::ChatQueue $chatid \
 
744
      [list ::Games::send_packet $chatid $gameID $msgType $msgBody]
 
745
  }
 
746
 
 
747
  ###########################################################################
 
748
  # ::send_packet                                                                                                       #
 
749
  # ----------------------------------------------------------------------- #
 
750
  # Here we send a packet to our opponent(s). Also, we make sure here that      #
 
751
  # packets do not exceed the maximum length. For some types of messages        #
 
752
  # we split the message to multiple messages.                                                          #
 
753
  ###########################################################################
 
754
  proc send_packet { chatid gameID msgType msgBody } {
 
755
        variable version
 
756
        variable max_packet_size
 
757
    set sbn [::MSN::SBFor $chatid]
 
758
 
 
759
        set done 0
 
760
        set msg ""
 
761
 
 
762
        while {!$done} {
 
763
 
 
764
          if {[string length $msgBody] < $max_packet_size} {
 
765
                set done 1
 
766
                set msg $msgBody
 
767
          } else {
 
768
                # Try to split the packet up in smaller ones
 
769
                set coords_idx [string last "COORDS=" $msgBody]
 
770
                if {$coords_idx > -1} {
 
771
                  # Rest of this message is space separated x y coordinates
 
772
                  set msg [string range $msgBody 0 [expr {$coords_idx + 6}]]
 
773
                  set coords [string range $msgBody [expr {$coords_idx + 7}] end]
 
774
                  set msgBody $msg
 
775
                  set state 0
 
776
                  foreach {x y} $coords {
 
777
                        if {$state == 0 && [string length $msg] < $max_packet_size} {
 
778
                          # We can still send more
 
779
                          set msg "${msg}${x} ${y} "
 
780
                          set last_x $x ; set last_y $y
 
781
                        } else {
 
782
                          # To be send later
 
783
                          if {$state == 0} {
 
784
                                set state 1
 
785
                                # Duplicate last coordinate
 
786
                                set msgBody "${msgBody}${last_x} ${last_y} "
 
787
                          }
 
788
                          set msgBody "${msgBody}${x} ${y} "
 
789
                        }
 
790
                  }
 
791
                } else {
 
792
                  log "Packet (size [string length $msgBody]) to large to send and I do not know \
 
793
               how to split it up, packet type is $msgType."
 
794
                  return
 
795
                }
 
796
          }
 
797
 
 
798
          set packet "MIME-Version: 1.0\r\n"
 
799
          set packet "${packet}Content-Type: text/x-amsngames; charset=utf-8\r\n"
 
800
          set packet "${packet}VERSION: $version\r\n"
 
801
          set packet "${packet}GAMEID: $gameID\r\n"
 
802
          set packet "${packet}MSGTYPE: [encoding convertto utf-8 $msgType]\r\n\r\n"
 
803
          set packet "${packet}[encoding convertto utf-8 $msg]"
 
804
          set packet_len [string length $packet]
 
805
 
 
806
          ::MSN::WriteSBNoNL $sbn "MSG" "U $packet_len\r\n$packet"
 
807
          #log "Sending (size $packet_len): $packet"
 
808
        }
 
809
  }
 
810
 
 
811
  ###########################################################################
 
812
  # ::GenerateGameID                                                                                            #
 
813
  # ----------------------------------------------------------------------- #
 
814
  # Generate a world-wide unique gameID                                                                         #
 
815
  ###########################################################################
 
816
  proc GenerateGameID { chatid gametype } {
 
817
    # FIXME: I'm afraid this is tcl 8.4 specific
 
818
    set gameID "$gametype$chatid[clock clicks -milliseconds]"
 
819
    return $gameID
 
820
  }
 
821
 
 
822
  proc myRand {min max} {
 
823
    return [expr {int($min + rand() * (1+$max-$min))}]
 
824
  }
 
825
 
 
826
  # Here's my resize-less version of aMSN's moveinscreen.
 
827
  # I need this because the original version's resizing seems to
 
828
  # interfere with "pack forget" and repack statements I want to use.
 
829
  proc moveinscreen {window {mindist 0}} {
 
830
        update
 
831
        # Check whether window exists
 
832
        if {![winfo exists $window]} {
 
833
          return
 
834
        }
 
835
        set winx [winfo width $window]
 
836
        set winy [winfo height $window]
 
837
        set scrx [winfo screenwidth .]
 
838
        set scry [winfo screenheight .]
 
839
        set winpx [winfo x $window]
 
840
        set winpy [winfo y $window]
 
841
 
 
842
        # Check if the window is too large to fit on the screen
 
843
        if { [expr {$winx > ($scrx-(2*$mindist))}] } {
 
844
          set winx [expr {$scrx-(2*$mindist)}]
 
845
        }
 
846
        if { [expr {$winy > ($scry-(2*$mindist))}] } {
 
847
          set winy [expr {$scry-(2*$mindist)}]
 
848
        }
 
849
 
 
850
        # Check if the window is positioned off the screen
 
851
        if { [expr {$winpx + $winx > ($scrx-$mindist)}] } {
 
852
          set winpx [expr {$scrx-$mindist-$winx}]
 
853
        }
 
854
        if { [expr {$winpx < $mindist}] } {
 
855
          set winpx $mindist
 
856
        }
 
857
        if { [expr {$winpy + $winy > ($scry-$mindist)}] } {
 
858
          set winpy [expr {$scry-$mindist-$winy}]
 
859
        }
 
860
        if { [expr {$winpy < $mindist}] } {
 
861
          set winpy $mindist
 
862
        }
 
863
 
 
864
        wm geometry $window "+${winpx}+${winpy}"
 
865
  }
 
866
}