#!/usr/bin/perl
#*****************************************************************************
#
#                          Frozen-Bubble
#
# Copyright (c) 2000-2008 The Frozen-Bubble Team
#
# Originally sponsored by Mandriva <http://www.mandriva.com/>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2, as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
#******************************************************************************
#
# Design & Programming by Guillaume Cottenceau between Oct 2001 and Jan 2002.
# Level Editor parts by Kim Joham and David Joham between Oct 2002 and Jan 2003.
# Network game by Guillaume Cottenceau in 2004, 2006 (blah!).
#
# Check official home: http://www.frozen-bubble.org/
#
#******************************************************************************
#
# Yes it uses Perl, you non-believer :-).
#

#use diagnostics;
use strict;

use vars qw($TARGET_ANIM_SPEED $BUBBLE_SIZE $ROW_SIZE $LAUNCHER_SPEED $BUBBLE_SPEED $MALUS_BUBBLE_SPEED $TIME_APPEARS_NEW_ROOT
            %POS %POS_1P %POS_2P %MENUPOS $KEYS %actions %angle %pdata $app %apprects $event %rects %sticked_bubbles %root_bubbles
            $background $background_orig @bubbles_images $gcwashere %bubbles_anim %launched_bubble %tobe_launched %next_bubble $recorddir %recorddata $playdata
            $shooter_lowgfx $sdl_flags $mixer $mixer_enabled $music_disabled $sfx_disabled $no_echo @playlist %sound %music %pinguin %canon
            $graphics_level @update_rects $CANON_ROTATIONS_NB %malus_bubble %falling_bubble %exploding_bubble %malus_gfx %pangocontext $private $no_time_limit
            %sticking_bubble $time %imgbin $TIME_HURRY_WARN $TIME_HURRY_MAX $TIMEOUT_PINGUIN_SLEEP $FREE_FALL_CONSTANT @joysticks $joysticksinfo
            $direct @PLAYERS @ALL_PLAYERS %levels $display_on_app_disabled $addicted_time $start_time $time_1pgame $time_netgame $fullscreen $rcfile %hiscorefiles
            $HISCORES $HISCORES_MPTRAIN $HISCORES_MPTRAIN_CHAINREACTION
            $lev_number $playermalus $mptrainingdiff $loaded_levelset $direct_levelset $chainreaction %chains %img_mini $frame $sock $gameserver $mynick
            $continuegamewhenplayersleave $singleplayertargetting $mylatitude $mylongitude %autokick $replayparam $autorecord $comment $saveframes $saveframesbase $saveframescounter);

use feature qw(state);

use Getopt::Long;
use Data::Dumper;
use Locale::gettext;
use POSIX();
use Math::Trig;
use IO::File;
use Time::HiRes qw(gettimeofday);

use SDL;
use SDL::App;
use SDL::Surface;
use SDL::Event;
use SDL::Cursor;
use SDL::Mixer;

use fb_stuff;
use fb_net;
use fb_net_discover;
use fbsyms;
use FBLE;

$| = 1;

$TARGET_ANIM_SPEED = 20;        # number of milliseconds that should last between two animation frames
$LAUNCHER_SPEED = 0.03;	        # speed of rotation of launchers
$BUBBLE_SPEED = 10;	        # speed of movement of launched bubbles
$MALUS_BUBBLE_SPEED = 30;       # speed of movement of "malus" launched bubbles
$CANON_ROTATIONS_NB = 100;      # number of rotations of images for canon

$TIMEOUT_PINGUIN_SLEEP = 200;
$FREE_FALL_CONSTANT = 0.5;
$KEYS = { p1 => { left => SDLK_LEFT, right => SDLK_RIGHT, fire => SDLK_UP, center => SDLK_DOWN },
          p2 => { left => SDLK_x,    right => SDLK_v,     fire => SDLK_c,  center => SDLK_d },
	  misc => { fs => SDLK_f, chat => SDLK_RETURN, send_malus_to_rp1 => SDLK_F1, send_malus_to_rp2 => SDLK_F2, send_malus_to_rp3 => SDLK_F3,
                    send_malus_to_rp4 => SDLK_F4, send_malus_to_all => SDLK_F10, next_playlist_elem => SDLK_TAB, save_record => SDLK_PRINT,
                    toggle_music => SDLK_F11, toggle_sound => SDLK_F12, raise_volume => SDLK_KP_PLUS, lower_volume => SDLK_KP_MINUS } };
$sdl_flags = SDL_ANYFORMAT | SDL_HWSURFACE | SDL_DOUBLEBUF | SDL_HWACCEL | SDL_ASYNCBLIT;
$mixer = 0;
$graphics_level = 3;
@PLAYERS = qw(p1 p2);
@ALL_PLAYERS = qw(p1 p2 rp1 rp2 rp3 rp4);
$playermalus = 0;
$mptrainingdiff = 30;
$chainreaction = 0;
$mynick = $ENV{USER};
$HISCORES = [];
$HISCORES_MPTRAIN = [];
$HISCORES_MPTRAIN_CHAINREACTION = [];

my $RECORD_PROTOCOL_LEVEL = 0;

$rcfile = "$FBHOME/rc";
my $keys_orig = $KEYS;
eval(cat_($rcfile));
$KEYS->{misc}{chat} or ($KEYS->{p1}, $KEYS->{p2}) = ($KEYS->{p2}, $KEYS->{p1});  #- for upgrades
$KEYS->{misc}{$_} ||= $keys_orig->{misc}{$_} foreach keys %{$keys_orig->{misc}}; #-
eval(cat_($hiscorefiles{levels} = "$FBHOME/highscores"));
eval(cat_($hiscorefiles{mptrain} = "$FBHOME/highscores-mptrain"));

textdomain("frozen-bubble");
$ENV{GETTEXT_DIRNAME} and bindtextdomain("frozen-bubble", $ENV{GETTEXT_DIRNAME});
bind_textdomain_codeset("frozen-bubble", "UTF-8");  #- we're going to use SDL_Pango which uses UTF-8 input
our $is_rtl = $ENV{LANGUAGE} =~ /^fa/;

print "        [[ Frozen-Bubble-$version ]]\n\n";
print '  http://www.frozen-bubble.org/

  Copyright (c) 2000-2008 The Frozen-Bubble Team.
 
    Artwork: Alexis Younes
             Amaury Amblard-Ladurantie
    Soundtrack: Matthias Le Bidan
    Design & Programming: Guillaume Cottenceau
    Level Editor: Kim and David Joham
    Additional network programming: Mark Glines

  Originally sponsored by Mandriva <http://www.mandriva.com/>

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License version 2, as
  published by the Free Software Foundation.

';

#- become a friend of BooK
GetOptions("fullscreen|fs!" => \$fullscreen,
           "no-sound|ns" => sub { $mixer = 'SOUND_DISABLED' },
           "no-music|nm" => \$music_disabled,
           "no-sfx" => \$sfx_disabled,
           "playlist=s" => sub { @playlist = -d $_[1] ? glob("$_[1]/*") : cat_($_[1]) },
           "slow-machine" => sub { $graphics_level = 2 },
           "very-slow-machine" => sub { $graphics_level = 1 },
           "direct" => \$direct,
           "solo" => sub { $direct = 1; @PLAYERS = ('p1') },
           "chain-reaction" => \$chainreaction,
           "colour-blind|cb" => \$colourblind,
           "player-malus=i" => \$playermalus,
           "mp-training-difficulty=i" => \$mptrainingdiff,
           "level|l=i" => sub { $levels{current} = $_[1]; $direct = 1; @PLAYERS = ('p1') },
           "levelset=s" => sub { $direct_levelset = $_[1]; $levels{current} = 1; $direct = 1; @PLAYERS = ('p1') },
           "no-time-limit" => \$no_time_limit,
           "master-server=s" => \$fb_net::masterserver,
           "gameserver|gs=s" => sub { $gameserver = $_[1]; $direct = 1; @PLAYERS = qw(p1 rp1); $pdata{gametype} = 'net' },
           "no-echo" => \$no_echo,
           "my-nick=s" => \$mynick,
           "private" => \$private,
           "joysticks-info" => \$joysticksinfo,
           "record=s" => sub { if (!-d $_[1]) { die("$_[1] does not exist.\n") } else { $recorddir = $_[1] } },
           "auto-record" => \$autorecord,
           "replay=s" => \$replayparam,
           "save-frames=s" => sub { if (!-d $_[1]) { die("$_[1] does not exist.\n") } else { $saveframes = $_[1] } },
           "comment=s" => \$comment,
           "help" => sub { print "Usage: ", basename($0), " [OPTION]...
 --fullscreen           start in fullscreen mode
 --no-fullscreen        don't start in fullscreen mode
 --no-sound             don't try to start any sound stuff
 --no-music             disable music (only)
 --no-sfx               disable sound effects (only)
 --playlist <file>      use all files listed in the given file as music files and play them
 --playlist <directory> use all files inside the given directory as music files and play them
 --slow-machine         enable slow machine mode (disable a few animations)
 --very-slow-machine    enable very slow machine mode (disable all that can be disabled)
 --solo                 directly start solo (1p) game, with random levels if no -l<#n> is given
 --direct               directly start (2p) game (don't display menu)
 --gameserver <host[:port]> directly start NET/LAN game connecting to this game server (if port is omitted, default port is used)
 --level <#n>           directly start the n-th level (implies -so)
 --levelset<name>       directly start with the specified levelset name
 --no-time-limit        disable time limit for shooting (e.g. kids mode)
 --chain-reaction       enable chain-reaction (when applicable)
 --player-malus <#n>    add a malus of n to the left player (can be negative)
 --mp-training-difficulty <#n> set the average duration between receiving malus bubbles in 1 player multiplayer training (default 30 (= every 30 seconds on average), the lower the harder)
 --colour-blind         use bubbles for colourblind people
 --joysticks-info       print information about detected joystick(s) on startup
 --no-echo              when sound is enabled, disable echoing each typed character with a typewriter sound
 --my-nick <nick>       for net/lan games, use this nick instead of username (max 10 chars, ASCII alphanumeric plus dash and underscore only)
 --private              when starting a net game, don't use http://hostip.info/ to retrieve your geographical position to send it to other players
 --record <dir>         specify the recording directory (normally, records are saved in the directory '$FBHOME/records')
 --auto-record          automatically record all applicable games (normally, a record is triggered by hitting the Print Screen key during a game)
 --comment '...'        add the comment enclosed between simple quotes to records (must not contain anything else than ASCII), it will be shown on console when playing back the record later
 --replay <file|URL>    replay the specified savegame
 --save-frames <dir>    specify a directory where all (game) frames will be recorded; as the game is slowed down, can only be used with --replay; typical use case is then to build a video out of the frames (see manpage)
";
                           exit(0);
           });

$mynick = sanitize_nick($mynick);
$saveframes && !$replayparam and print STDERR "--save-frames can only be used with --replay\n";


#- ------------------------------------------------------------------------

sub i18n_number {
    my ($number) = @_;
    my $out = '';
    foreach my $char (split //, $number) {
           if ($char eq '0') { $out .= t("0"); }
        elsif ($char eq '1') { $out .= t("1"); }
        elsif ($char eq '2') { $out .= t("2"); }
        elsif ($char eq '3') { $out .= t("3"); }
        elsif ($char eq '4') { $out .= t("4"); }
        elsif ($char eq '5') { $out .= t("5"); }
        elsif ($char eq '6') { $out .= t("6"); }
        elsif ($char eq '7') { $out .= t("7"); }
        elsif ($char eq '8') { $out .= t("8"); }
        elsif ($char eq '9') { $out .= t("9"); }
        elsif ($char eq '.') { $out .= t("."); }
        else { $out .= $char; }
    }
    return $out;
}

sub format_addiction {
    my ($seconds, $i18n) = @_;
    my $h = int($seconds/3600);
    my $m = int(($seconds-$h*3600)/60);
    my $s = int($seconds-$h*3600-$m*60);
    if (!$i18n) {
        return ($h ? "${h}h " : '') . ($m ? sprintf('%'.($h ? '02' : '').'dm ', $m) : '') . sprintf('%'.($m ? '02' : '').'ds', $s);
    } else {
        if ($h) {
            $m = sprintf("%02d", $m);
        }
        if ($m) {
            $s = sprintf("%02d", $s);
        }
        $h = i18n_number($h);
        $m = i18n_number($m);
        $s = i18n_number($s);
        if ($h) {
            return t("%sh %sm %ss", $h, $m, $s);
        } elsif ($m) {
            return t("%sm %ss", $m, $s);
        } else {
            return t("%ss", $s);
        }
    }
}

END {
    if ($app && $addicted_time) {
	print "\nAddicted for ", format_addiction($addicted_time/1000, 0), "\n";
    }
}


#- ----------- sound related stuff ----------------------------------------

sub play_sound($) {
    $mixer_enabled && $mixer && !$sfx_disabled && $sound{$_[0]} and $mixer->play_channel(-1, $sound{$_[0]}, 0);
}

our $current_theoretical_music;
sub play_music($) {
    my ($name) = @_;
    $current_theoretical_music = $name;
    $mixer_enabled && $mixer && !$music_disabled or return;
    @playlist && $mixer->playing_music and return;
    $app->delay(10) while $mixer->fading_music;   #- mikmod will deadlock if we try to fade_out while still fading in
    $mixer->playing_music and $mixer->fade_out_music(500);
    $app->delay(400);
    $app->delay(10) while $mixer->playing_music;  #- mikmod will segfault if we try to load a music while old one is still fading out
    my %musics = (intro => '/snd/introzik.ogg', main1p => '/snd/frozen-mainzik-1p.ogg', main2p => '/snd/frozen-mainzik-2p.xm');
    state $mus;                                 #- I need to keep a reference on the music or it will be collected at the end of this function, thus I manually collect previous music
    if (@playlist) {
	my $tryanother = sub {
	    my $elem = chomp_(shift @playlist);
	    $elem or return -1;
	    -f $elem or return 0;
	    push @playlist, $elem;
	    $mus = SDL::Music->new($elem);
	    if (UNIVERSAL::isa($mus, 'HASH') ? $mus->{-data} : $$mus) {
		print STDERR "[Playlist] playing `$elem'\n";
		$mixer->play_music($mus, 0);
		return 1;
	    } else { 
		print STDERR "Warning, could not create new music from '$elem' (reason: ", $app->error, ").\n";
		return 0;
	    }
	};
	while ($tryanother->() == 0) {};
    } else {
	$mus = SDL::Music->new("$FPATH$musics{$name}");
        if (UNIVERSAL::isa($mus, 'HASH') ? $mus->{-data} : $$mus) {
            $mixer->play_music($mus, -1);
            $music{current} = $name;
        } else {
            print STDERR "Warning, could not create new music from '$FPATH$musics{$name}' (reason: ", $app->error, ").\n";
        }
    }
}

sub init_sound() {
    $mixer = eval { SDL::Mixer->new(-frequency => 44100, -channels => 2, -size => 1024); };
    if ($@) {
	$@ =~ s| at \S+ line.*\n||;
	print STDERR "\nWarning: can't initialize sound (reason: $@).\n";
	return 0;
    }
    print "[Sound Init] ";
    my @sounds = qw(stick destroy_group newroot newroot_solo lose hurry pause menu_change menu_selected rebound launch malus noh snore cancel typewriter applause chatted);
    foreach (@sounds) {
	my $sound_path = "$FPATH/snd/$_.ogg";
	$sound{$_} = SDL::Sound->new($sound_path);
	if (UNIVERSAL::isa($sound{$_}, 'HASH') ? $sound{$_}{-data} : ${$sound{$_}}) {
	    $sound{$_}->volume(80);
	} else {
	    print STDERR "Warning, could not create new sound from '$sound_path'.\n";
	}
    }
    return 1;
}


#- ----------- graphics related stuff --------------------------------------

sub add_default_rect($) {
    my ($surface) = @_;
    $rects{$surface} = SDL::Rect->new(-width => $surface->width, -height => $surface->height);
}

sub mini_graphics {
    my ($p) = @_;
    return @PLAYERS >= 3 && $p =~ /rp/;
}

sub translate_mini_image {
    my ($image) = @_;
    if (mini_graphics($::p_) || ($::p_ eq '' && mini_graphics($::p))) {
        $img_mini{$image} and return $img_mini{$image};
    }
    return $image;
}

sub put_image($$$) {
    my ($image, $x, $y) = @_;
    $image = translate_mini_image($image);
    $rects{$image} or die "please don't call me with no rects\n".backtrace();
    my $drect = SDL::Rect->new(-width => $image->width, -height => $image->height, -x => $x, '-y' => $y);
    $image->blit($rects{$image}, $app, $drect);
    push @update_rects, $drect;
}

sub erase_image_from($$$$) {
    my ($image, $x, $y, $img) = @_;
    $image = translate_mini_image($image);
    my $drect = SDL::Rect->new(-width => $image->width, -height => $image->height, -x => $x, '-y' => $y);
    $img->blit($drect, $app, $drect);
    push @update_rects, $drect;
}

sub erase_image($$$) {
    my ($image, $x, $y) = @_;
    erase_image_from($image, $x, $y, $background);
}

sub put_image_to_background($$$) {
    my ($image, $x, $y) = @_;
    my $drect;
    $image = translate_mini_image($image);
    ($x == 0 && $y == 0) and print "put_image_to_background: warning, X and Y are 0\n".backtrace();
    if ($y > 0) {
	$drect = SDL::Rect->new(-width => $image->width, -height => $image->height, -x => $x, '-y' => $y);
	$display_on_app_disabled or $image->blit($rects{$image}, $app, $drect);
	$image->blit($rects{$image}, $background, $drect);
    } else {  #- clipping seems to not work when from one Surface to another Surface, so I need to do clipping by hand
	$drect = SDL::Rect->new(-width => $image->width, -height => $image->height + $y, -x => $x, '-y' => 0);
	my $irect = SDL::Rect->new(-width => $image->width, -height => $image->height + $y, '-y' => -$y);
	$display_on_app_disabled or $image->blit($irect, $app, $drect);
	$image->blit($irect, $background, $drect);
    }
    push @update_rects, $drect;
}

sub remove_image_from_background($$$) {
    my ($image, $x, $y) = @_;
    $image = translate_mini_image($image);
    ($x == 0 && $y == 0) and print "remove_image_from_background: warning, X and Y are 0\n";
    my $drect = SDL::Rect->new(-width => $image->width, -height => $image->height, -x => $x, '-y' => $y);
    $background_orig->blit($drect, $background, $drect);
    $background_orig->blit($drect, $app, $drect);
    push @update_rects, $drect;
}

sub remove_images_from_background {
    my ($player, @images) = @_;
    foreach my $image (@images) {
	($image->{'x'} == 0 && $image->{'y'} == 0) and print "remove_images_from_background: warning, X and Y are 0\n";
        my $img = translate_mini_image($image->{img});
	my $drect = SDL::Rect->new(-width => $img->width, -height => $img->height, -x => $image->{'x'}, '-y' => $image->{'y'});
	$background_orig->blit($drect, $background, $drect);
	$background_orig->blit($drect, $app, $drect);
	push @update_rects, $drect;
    }
}

sub put_allimages_to_background($) {
    my ($player) = @_;
    put_image_to_background($_->{img}, $_->{'x'}, $_->{'y'}) foreach @{$sticked_bubbles{$player}};
}

sub switch_image_on_background($$$;$) {
    my ($image, $x, $y, $save) = @_;
    my $drect = SDL::Rect->new(-width => $image->width, -height => $image->height, -x => $x, '-y' => $y);
    if ($save) {
	$save = SDL::Surface->new(-width => $image->width, -height => $image->height, -depth => 32, -Amask => "0 but true");  #- grrr... this piece of shit of Amask made the surfaces slightly modify along the print/erase of "Hurry" and "Pause".... took me so much time to debug and find that the problem came from a bug when Amask is set to 0xFF000000 (while it's -supposed- to be set to 0xFF000000 with 32-bit graphics!!)
	$background->blit($drect, $save, $rects{$image});
    }
    $image->blit($rects{$image} || SDL::Rect->new(-width => $image->width, -height => $image->height), $background, $drect);
    $background->blit($drect, $app, $drect);
    push @update_rects, $drect;
    return $save;
}

sub add_image_file($) {
    my ($file) = @_;
    my $img;
    eval {
        $img = SDL::Surface->new(-name => $file);
    };
    $@ and die "FATAL: Couldn't load '$file' into a SDL::Surface.\n";
    add_default_rect($img);
    return $img;
}

sub add_image($) {
    return add_image_file("$FPATH/gfx/$_[0]");
}

sub add_images {
    return map { add_image_file($_) } glob("$FPATH/gfx/$_[0]");
}

sub add_bubble_image($) {
    my ($file) = @_;
    my $bubble = add_image($file);
    push @bubbles_images, $bubble;
    return $bubble;
}


#- ----------- generic game stuff -----------------------------------------

sub iter_players(&) {
    my ($f, @p) = @_;
    my $bt = backtrace();
    $bt =~ /\nmain::iter_players\b/ and die "iter_players: assert failed -- iter_players can't be called recursively sorry\n$bt";
    @p or @p = @PLAYERS;
    local $::p;
    foreach $::p (@p) {
        mini_graphics($::p) or goto normal_sizes;  #- can't use an if block because of local
        local $BUBBLE_SIZE = $BUBBLE_SIZE / 2;
        local $BUBBLE_SPEED = $BUBBLE_SPEED / 2;
        local $ROW_SIZE = $ROW_SIZE / 2;
	local $FREE_FALL_CONSTANT = $FREE_FALL_CONSTANT / 2;
      normal_sizes:
	&$f;
    }
}
sub iter_players_(&) {  #- so that I can do an iter_players_ from within an iter_players
    my ($f, @p) = @_;
    my $bt = backtrace();
    $bt =~ /\nmain::iter_players_\b/ and die "iter_players_: assert failed -- iter_players_ can't be called recursively sorry\n$bt";
    @p or @p = @PLAYERS;
    local $::p_;
    foreach $::p_ (@p) {
	&$f;
    }
}
sub iter_players_but_first(&) {
    my ($f) = @_;
    my (undef, @p) = @PLAYERS;
    &iter_players($f, @p);
}
sub iter_local_players(&) {
    my ($f) = @_;
    my @p = grep { !/rp/ } @PLAYERS;
    &iter_players($f, @p);
}
sub iter_distant_players(&) {
    my ($f) = @_;
    my @p = grep { /rp/ } @PLAYERS;
    &iter_players($f, @p);
}
sub iter_distant_players_(&) {
    my ($f) = @_;
    my @p = grep { /rp/ } @PLAYERS;
    &iter_players_($f, @p);
}

sub is_1p_game() { @PLAYERS == 1 }
sub is_mp_game() { any { /rp/ } @PLAYERS }
sub is_2p_game() { @PLAYERS == 2 && !is_mp_game() }

sub is_leader() {
    my $me = unpack('C', $pdata{p1}{id});
    my $is_leader = 1;
    iter_players_but_first {
        $pdata{$::p}{left} or $is_leader &&= unpack('C', $pdata{$::p}{id}) > $me;
    };
    return $is_leader;
}
sub is_local_player($) {
    my ($player) = @_;
    $player !~ /rp/;
}
sub is_distant_player($) {
    my ($player) = @_;
    $player =~ /rp/;
}

sub mp_ping_if_needed {
    my ($ticks) = @_;
    if ($app->ticks - $$ticks > 1000) {
        fb_net::gsend('p');
        $$ticks = $app->ticks;
    }
}

sub mp_propagate {
    my ($key, $value, $ticks) = @_;
    if (is_leader()) {
        dbgnet("as leader, sending $key$value");
        fb_net::gsend("$key$value");
        return $value;
    } else {
        my $m = fb_net::grecv_get1msg();
        mp_ping_if_needed($ticks);
        if ($m->{msg} !~ /^\Q$key\E(.+)/) {
            if ($m->{msg} eq 'l') {
                print "Server said that one of the players left - probably because of too high lag.\n";
            } else {
                print "Network protocol error: waiting for $key, received $m->{msg} - from $pdata{id2p}{$m->{id}}.\n";
            }
            die 'quit';
        } else {
            dbgnet("duly received awaited message $m->{msg} - from $pdata{id2p}{$m->{id}}");
            return $1;
        }
    }
}

sub living_players() {
    my @living;
    iter_players_ {
        if (!$pdata{$::p_}{left} && $pdata{$::p_}{state} eq 'ingame') {
            push @living, $::p_;
        }
    };
    return @living;
}

sub notleft_players() {
    my $amount = 0;
    iter_players_ {
        $pdata{$::p_}{left} or $amount++;
    };
    return $amount;
}

#- ----------- bubble game stuff ------------------------------------------

sub calc_real_pos_given_arraypos($$$) {
    my ($cx, $cy, $player) = @_;
    ($POS{$player}{left_limit} + $cx * $BUBBLE_SIZE + odd($cy+$pdata{$player}{oddswap}) * $BUBBLE_SIZE/2,
     $POS{$player}{top_limit} + $cy * $ROW_SIZE);
}

sub calc_real_pos($$) {
    my ($b, $player) = @_;
    ($b->{'x'}, $b->{'y'}) = calc_real_pos_given_arraypos($b->{cx}, $b->{cy}, $player);
}

sub get_array_yclosest($$) {
    my ($y, $player) = @_;
    return int(($y-$POS{$player}{top_limit}+$ROW_SIZE/2) / $ROW_SIZE);
}

sub get_array_closest_pos($$$) { # roughly the opposite than previous function
    my ($x, $y, $player) = @_;
    my $ny = get_array_yclosest($y, $player);
    my $nx = int(($x-$POS{$player}{left_limit}+$BUBBLE_SIZE/2 - odd($ny+$pdata{$player}{oddswap})*$BUBBLE_SIZE/2)/$BUBBLE_SIZE);
    return ($nx, $ny);
}

sub is_collision($$$) {
    my ($bub, $x, $y) = @_;
    my $DISTANCE_COLLISION_SQRED = sqr($BUBBLE_SIZE * 0.82);
    my $xs = sqr($bub->{x} - $x);
    ($xs > $DISTANCE_COLLISION_SQRED) and return 0; 
    return ($xs + sqr($bub->{'y'} - $y)) < $DISTANCE_COLLISION_SQRED;
}

sub create_bubble_given_img($) {
    my ($img) = @_;
    my %bubble;
    ref($img) eq 'SDL::Surface' or die "<$img> seems to not be a valid image\n" . backtrace();
    $bubble{img} = $img;
    $bubble{neighbours} = [];
    return \%bubble;
}

sub create_bubble_given_img_num($) {
    my ($num) = @_;
    return create_bubble_given_img($bubbles_images[$num]);
}

sub validate_nextcolor($$) {
    my ($num, $player) = @_;
    return !is_1p_game() || member($num, map { get_bubble_num($_) } @{$sticked_bubbles{$player}});
}

sub each_index(&@) {
    my $f = shift;
    local $::i = 0;
    foreach (@_) {
	$f->();
	$::i++;
    }
}
sub get_bubble_num {
    my ($b) = @_;
    my $num = -1;
    each_index { $_ eq $b->{img} and $num = $::i } @bubbles_images;
    return $num;
}

sub iter_rowscols(&$) {
    my ($f, $oddswap) = @_;
    local $::row; local $::col;
    foreach $::row (0 .. 11) {
	foreach $::col (0 .. 7 - odd($::row+$oddswap)) {
	    &$f;
	}
    }
}

sub each_index(&@) {
    my $f = shift;
    local $::i = 0;
    foreach (@_) {
	&$f($::i);
	$::i++;
    }
}
sub img2numb { my ($i, $f) = @_; each_index { $i eq $_ and $f = $::i } @bubbles_images; return defined($f) ? $f : '-' }

sub bubble_next_to($$$$$) {
    my ($x1, $y1, $x2, $y2, $player) = @_;
    if ($x1 == $x2 && $y1 == $y2) {
        print STDERR "bubble_next_to: assert failed -- same bubbles ($x1:$y1;$player)\n";
        $pdata{inconsistency} = 1;
        die 'quit';
    }
    return to_bool((sqr($x1+odd($y1+$pdata{$player}{oddswap})*0.5 - ($x2+odd($y2+$pdata{$player}{oddswap})*0.5)) + sqr($y1 - $y2)) < 3);
}

sub next_positions($$) {
    my ($b, $player) = @_;
    my $validate_pos = sub {
	my ($x, $y) = @_;
	if_($x >= 0 && $x+odd($y+$pdata{$player}{oddswap}) <= 7 && $y >= 0 && $y >= $pdata{$player}{newrootlevel} && $y <= 11,
	    [ $x, $y ]);
    };
    ($validate_pos->($b->{cx} - 1, $b->{cy}),
     $validate_pos->($b->{cx} + 1, $b->{cy}),
     $validate_pos->($b->{cx} - even($b->{cy}+$pdata{$player}{oddswap}), $b->{cy} - 1),
     $validate_pos->($b->{cx} - even($b->{cy}+$pdata{$player}{oddswap}), $b->{cy} + 1),
     $validate_pos->($b->{cx} - even($b->{cy}+$pdata{$player}{oddswap}) + 1, $b->{cy} - 1),
     $validate_pos->($b->{cx} - even($b->{cy}+$pdata{$player}{oddswap}) + 1, $b->{cy} + 1));
}

#- bubble ends its life sticked somewhere
sub real_stick_bubble {
    my ($bubble, $xpos, $ypos, $player, $neighbours_ok) = @_;
    $bubble->{cx} = $xpos;
    $bubble->{cy} = $ypos;
    foreach (@{$sticked_bubbles{$player}}) {
	if (bubble_next_to($_->{cx}, $_->{cy}, $bubble->{cx}, $bubble->{cy}, $player)) {
	    push @{$_->{neighbours}}, $bubble;
	    $neighbours_ok or push @{$bubble->{neighbours}}, $_;
	}
    }
    push @{$sticked_bubbles{$player}}, $bubble;
    $bubble->{cy} == $pdata{$player}{newrootlevel} and push @{$root_bubbles{$player}}, $bubble;
    calc_real_pos($bubble, $player);
    put_image_to_background($bubble->{img}, $bubble->{'x'}, $bubble->{'y'});
}

sub destroy_bubbles {
    my ($player, @bubz) = @_;
    $graphics_level == 1 and return;
    foreach (@bubz) {
	$_->{speedx} = (rand(3)-1.5) / ( mini_graphics($player) ? 2 : 1 );
	$_->{speedy} = (-rand(4)-2) / ( mini_graphics($player) ? 2 : 1 );
    }
    push @{$exploding_bubble{$player}}, @bubz;
}

sub find_bubble_group($) {
    my ($b) = @_;
    my @neighbours = $b;
    my @group;
    while (1) {
	push @group, @neighbours;
	@neighbours = grep { $b->{img} eq $_->{img} && !member($_, @group) } fastuniq(map { @{$_->{neighbours}} } @neighbours);
	last if !@neighbours;
    }
    @group;
}

sub stick_bubble($$$$$) {
    my ($bubble, $xpos, $ypos, $player, $count_for_root) = @_;
    my @falling;
    my $need_redraw = 0;
    @{$bubble->{neighbours}} = grep { bubble_next_to($_->{cx}, $_->{cy}, $xpos, $ypos, $player) } @{$sticked_bubbles{$player}};

    #- in multiple chain reactions, it's possible that the group doesn't exist anymore in some rare situations :/
    exists $bubble->{chaindestx} && !@{$bubble->{neighbours}} and return;

    my @will_destroy = difference2([ find_bubble_group($bubble) ], [ $bubble ]);

    if (@will_destroy <= 1) {
	#- stick
	play_sound('stick');
	real_stick_bubble($bubble, $xpos, $ypos, $player, 1);
	$sticking_bubble{$player} = $bubble;
	$pdata{$player}{sticking_step} = 0;
    } else {
	#- destroy the group
	play_sound('destroy_group');
	foreach my $b (difference2([ fastuniq(map { @{$_->{neighbours}} } @will_destroy) ], \@will_destroy)) {
	    @{$b->{neighbours}} = difference2($b->{neighbours}, \@will_destroy);
	}
	@{$sticked_bubbles{$player}} = difference2($sticked_bubbles{$player}, \@will_destroy);
	@{$root_bubbles{$player}} = difference2($root_bubbles{$player}, \@will_destroy);

	$bubble->{'cx'} = $xpos;
	$bubble->{'cy'} = $ypos;
	calc_real_pos($bubble, $player);
	destroy_bubbles($player, @will_destroy, $bubble);

	#- find falling bubbles
	$_->{distance_to_root} = 0 foreach @{$sticked_bubbles{$player}};
	my @still_sticked;
	my @neighbours = @{$root_bubbles{$player}};
	my $distance_to_root;
	while (1) {
	    $_->{distance_to_root} = ++$distance_to_root foreach @neighbours;
	    push @still_sticked, @neighbours;
	    @neighbours = grep { $_->{distance_to_root} == 0 } map { @{$_->{neighbours}} } @neighbours;
	    last if !@neighbours;
	}
	@falling = difference2($sticked_bubbles{$player}, \@still_sticked);
	@{$sticked_bubbles{$player}} = difference2($sticked_bubbles{$player}, \@falling);

	#- chain-reaction on falling bubbles
	if ($chainreaction) {
	    my @falling_colors = map { $_->{img} } @falling;
	    #- optimize a bit by first calculating bubbles that are next to another bubble of the same color
	    my @grouped_bubbles = grep {
		my $b = $_;
		member($b->{img}, @falling_colors) && any { $b->{img} eq $_->{img} } @{$b->{neighbours}}
	    } @{$sticked_bubbles{$player}};
	    if (@grouped_bubbles) {
		#- all positions on which we can't chain-react
		my @occupied_positions = map { $_->{cy}*8 + $_->{cx} } @{$sticked_bubbles{$player}};
		push @occupied_positions, map { $_->{chaindestcy}*8 + $_->{chaindestcx} } @{$chains{$player}{falling_chained}};
		#- examine groups beginning at the root bubbles, for the case in which
		#- there is a group that will fall from an upper chain-reaction
		foreach my $pos (sort { $a->{distance_to_root} <=> $b->{distance_to_root} } @grouped_bubbles) {
		    #- now examine if there is a free position to chain-react in it
		    foreach my $npos (next_positions($pos, $player)) {
			#- we can't chain-react somewhere if it explodes a group already chained
			next if any { $pos->{cx} == $_->{cx} && $pos->{cy} == $_->{cy} }
			        map { @{$chains{$player}{chained_bubbles}{$_}}} keys %{$chains{$player}{chained_bubbles}};
			if (!member($npos->[1]*8 + $npos->[0], @occupied_positions)) {
			    #- find a suitable falling bubble for that free position
			    foreach my $falling (@falling) {
				next if member($falling, @{$chains{$player}{falling_chained}});
				if ($pos->{img} eq $falling->{img}) {
				    ($falling->{chaindestcx}, $falling->{chaindestcy}) = ($npos->[0], $npos->[1]);
				    ($falling->{chaindestx}, $falling->{chaindesty}) = calc_real_pos_given_arraypos($npos->[0], $npos->[1], $player);
				    push @{$chains{$player}{falling_chained}}, $falling;
				    push @occupied_positions, $npos->[1]*8 + $npos->[0];
				    
				    #- next lines will allow not to chain-react on the same group from two different positions,
				    #- and even to not chain-react on a group that will itself fall from a chain-reaction
				    @{$falling->{neighbours}} = grep { bubble_next_to($_->{cx}, $_->{cy}, $npos->[0], $npos->[1], $player) } @{$sticked_bubbles{$player}};
				    my @chained_bubbles = find_bubble_group($falling);
				    $_->{mark} = 0 foreach @{$sticked_bubbles{$player}};
				    my @still_sticked;
				    my @neighbours = difference2($root_bubbles{$player}, \@chained_bubbles);
				    while (1) {
					$_->{mark} = 1 foreach @neighbours;
					push @still_sticked, @neighbours;
					@neighbours = difference2([ grep { $_->{mark} == 0 } map { @{$_->{neighbours}} } @neighbours ],
								  \@chained_bubbles);
					last if !@neighbours;
				    }
				    @{$chains{$player}{chained_bubbles}{$falling}} = difference2($sticked_bubbles{$player}, \@still_sticked);
				    last;
				}
			    }
			}
		    }
		}
                #- now examine all chaining bubbles. for each one, check that consequences of all other chaining bubbles
                #- do not cancel it. start by the lowest falling as they are likely to be the first to reach destination.
                foreach my $falling (sort { $b->{cy} <=> $a->{cy} } @falling) {
                    next if !exists $falling->{chaindestx};
                    #- is this chain still possible, given all the other chains?
                    #- first calculate all other chained
                    my @other_chained_bubbles;
                    foreach my $falling2 (@falling) {
                        next if $falling eq $falling2 || !exists $falling2->{chaindestx};
                        push @other_chained_bubbles, find_bubble_group($falling2);
                    }
                    #- second calculate the still sticked bubbles given all other chained bubbles removed,
                    #- and check if this chain is still possible
                    $_->{mark} = 0 foreach @{$sticked_bubbles{$player}};
                    my @still_sticked;
                    my @neighbours = difference2($root_bubbles{$player}, \@other_chained_bubbles);
                    while (1) {
                        $_->{mark} = 1 foreach @neighbours;
                        push @still_sticked, @neighbours;
                        @neighbours = difference2([ grep { $_->{mark} == 0 } map { @{$_->{neighbours}} } @neighbours ],
                                                  \@other_chained_bubbles);
                        last if !@neighbours;
                    }
                    my @chained_bubbles = grep { member($_, @still_sticked) } find_bubble_group($falling);
                    if (@chained_bubbles < 2) {
                        #- ok then, suppress this one
                        delete $falling->{chaindestx};
                    }
                }
            }
	}

	#- prepare falling bubbles
	if ($graphics_level > 1 || $chainreaction) {
	    my $max_cy_falling = fold_left { $::b->{cy} > $::a ? $::b->{cy} : $::a } 0, @falling;  #- I have a fold_left in my prog! :-)
	    my ($shift_on_same_line, $line) = (0, $max_cy_falling);
	    foreach (sort { $b->{cy}*8 + $b->{cx} <=> $a->{cy}*8 + $a->{cx} } @falling) {  #- sort bottom-to-up / right-to-left
		$line != $_->{cy} and $shift_on_same_line = 0;
		$line = $_->{cy};
		$_->{wait_fall} = ($max_cy_falling - $_->{cy})*5 + $shift_on_same_line;
		$shift_on_same_line++;
		$_->{speed} = 0;
	    }
	    push @{$falling_bubble{$player}}, @falling;
	}

	remove_images_from_background($player, @will_destroy, @falling);
	#- redraw neighbours because parts of neighbours have been erased by previous statement
	put_image_to_background($_->{img}, $_->{'x'}, $_->{'y'})
	  foreach grep { !member($_, @will_destroy) && !member($_, @falling) } fastuniq(map { @{$_->{neighbours}} } @will_destroy, @falling);
	$need_redraw = 1;
    }

    if ($count_for_root) {
	$pdata{$player}{newroot}++;
	if ($pdata{$player}{newroot} == $TIME_APPEARS_NEW_ROOT-1) {
	    $pdata{$player}{newroot_prelight} = 2;
	    $pdata{$player}{newroot_prelight_step} = 0;
	}
	if ($pdata{$player}{newroot} == $TIME_APPEARS_NEW_ROOT) {
	    $pdata{$player}{newroot_prelight} = 1;
	    $pdata{$player}{newroot_prelight_step} = 0;
	}
	if ($pdata{$player}{newroot} > $TIME_APPEARS_NEW_ROOT) {
            my $_1p_mode = is_1p_game() && $levels{current} ne 'mp_train';
	    $need_redraw = 1;
	    $pdata{$player}{newroot_prelight} = 0;
	    play_sound($_1p_mode ? 'newroot_solo' : 'newroot');
	    $pdata{$player}{newroot} = 0;
	    $pdata{$player}{oddswap} = !$pdata{$player}{oddswap};
	    remove_images_from_background($player, @{$sticked_bubbles{$player}});
	    foreach (@{$sticked_bubbles{$player}}) {
		$_->{'cy'}++;
		calc_real_pos($_, $player);
	    }
	    foreach (@{$falling_bubble{$player}}) {
		exists $_->{chaindestx} or next;
		$_->{chaindestcy}++;
		$_->{chaindesty} += $ROW_SIZE;
	    }
	    put_allimages_to_background($player);
	    if ($_1p_mode) {
		$pdata{$player}{newrootlevel}++;
		print_compressor();
	    } else {
		@{$root_bubbles{$player}} = ();
		real_stick_bubble(create_bubble_given_img_num($pdata{$player}{nextcolors}[$_]), $_, 0, $player, 0) foreach (0..(7-$pdata{$player}{oddswap}));
                delete $pdata{$player}{nextcolors};
	    }
	}
    }

    if ($need_redraw) {
	my $malus_val = @will_destroy + @falling - 2;
	$malus_val > 0 && !is_mp_game() and $malus_val += ($player eq 'p1' ? $playermalus : -$playermalus);
	$malus_val < 0 and $malus_val = 0;
	$background->blit($apprects{$player}, $app, $apprects{$player});
	malus_change($malus_val, $player);
    }
}

sub redraw_chat_message_if_needed {
    my ($player) = @_;
    if ($pdata{current_chat_messages}{$player}) {
        my $img = @PLAYERS == 2 ? $imgbin{void_chat_small_p2} : member($player, qw(rp1 rp3)) ? $imgbin{void_chat_small_rp1_rp3} : $imgbin{void_chat_small_rp2_rp4};
        put_image_to_background($img, $POS{$player}{chatting}{x}, $POS{$player}{chatting}{'y'});
        print_('ingame_small_chat', $background,
               $POS{$player}{chatting}{x} + 3, $POS{$player}{chatting}{'y'} + 3, $pdata{current_chat_messages}{$player}, $img->width - 6, 'center');
        erase_image($img, $POS{$player}{chatting}{x}, $POS{$player}{chatting}{'y'});
    }
}

sub print_next_bubble($$;$) {
    my ($img, $player, $not_on_top_next) = @_;
    if (is_mp_game() && $player eq 'p1' && $pdata{p1}{chatting}) {
        return;
    }
    put_image_to_background($img, $next_bubble{$player}{'x'}, $next_bubble{$player}{'y'});
    $not_on_top_next or put_image_to_background($bubbles_anim{on_top_next},
                                                $POS{$player}{left_limit} + $POS{$player}{next_bubble}{x} + $POS{$player}{on_top_next_relpos}{x},
                                                $POS{$player}{next_bubble}{'y'} + $POS{$player}{on_top_next_relpos}{'y'});
    redraw_chat_message_if_needed($player);
}

sub generate_new_bubble($$) {
    my ($player, $num) = @_;
    $tobe_launched{$player} = $next_bubble{$player};
    $tobe_launched{$player}{'x'} = ($POS{$player}{left_limit}+$POS{$player}{right_limit})/2 - $BUBBLE_SIZE/2;
    $tobe_launched{$player}{'y'} = $POS{$player}{'initial_bubble_y'};
    $next_bubble{$player} = create_bubble_given_img_num($num);
    $next_bubble{$player}{'x'} = $POS{$player}{left_limit}+$POS{$player}{next_bubble}{x}; #- necessary to keep coordinates, for verify_if_end
    $next_bubble{$player}{'y'} = $POS{$player}{next_bubble}{'y'};
    print_next_bubble($next_bubble{$player}{img}, $player);
}


#- ----------- game stuff -------------------------------------------------

our $smg_lineheight = 16;

sub mp_train_time_left {
    my $seconds = 120.99 - (($recorddata{frame} * $TARGET_ANIM_SPEED) / 1000);
    $seconds < 0 and $seconds = 0;
    return $seconds;
}

our ($mp_train_xpos, $mp_train_ypos) = (32, 177);
sub mp_train_print_time {
    my $drect = SDL::Rect->new(-width => $imgbin{void_mp_training}->width, -height => 30, -x => $mp_train_xpos, '-y' => $mp_train_ypos);
    my $seconds = mp_train_time_left();
    my $m = int($seconds/60);
    my $s = int($seconds-$m*60); length($s) == 1 and $s = "0$s";
    print_('ingame', $background, $mp_train_xpos, $mp_train_ypos, t("%s'%s\"", i18n_number($m), i18n_number($s)), $imgbin{void_mp_training}->width, 'center');
}

sub handle_progress($) {
    my ($p) = @_;
    if (defined($pdata{$p}{newrootlast})) {
        if ($pdata{$p}{newroot} != $pdata{$p}{newrootlast}) {
            my $height = $imgbin{progress_red}->height + 1;
            my $xpos = $POS{$p}{progress}{x};
            my $ypos = $POS{$p}{progress}{y};
            put_image_to_background($imgbin{progress_green}, $xpos, $ypos + $pdata{$p}{newrootlast}*$height);
            put_image_to_background($imgbin{progress_red}, $xpos, $ypos + $pdata{$p}{newroot}*$height);
            $pdata{$p}{newrootlast} = $pdata{$p}{newroot};
        }

    } else {
        my $height = $imgbin{progress_red}->height + 1;
        my $xpos = $POS{$p}{progress}{x};
        my $ypos = $POS{$p}{progress}{y};
        $pdata{$p}{newrootlast} = 0;
        push @update_rects, SDL::Rect->new(-width => $imgbin{progress_red}->width, -height => $height*(1 + $TIME_APPEARS_NEW_ROOT),
                                           -x => $xpos, '-y' => $ypos);
        for (my $i = 0; $i <= $TIME_APPEARS_NEW_ROOT; $i++) {
            put_image_to_background($imgbin{$i ? "progress_green" : "progress_red"}, $xpos, $ypos + $i*$height);
        }
    }
}

sub handle_graphics($) {
    my ($fun) = @_;

    iter_players {
        $pdata{$::p}{state} eq 'left' and next;  #- remote players already left in a previous subgame in mp game
	#- bubbles
	foreach ($launched_bubble{$::p}, if_($fun ne \&erase_image, $tobe_launched{$::p})) {
	    $_ and $fun->($_->{img}, $_->{'x'}, $_->{'y'});
	}
	if ($fun eq \&put_image && $pdata{$::p}{newroot_prelight}) {
	    if ($pdata{$::p}{newroot_prelight_step}++ > 30*$pdata{$::p}{newroot_prelight}) {
		$pdata{$::p}{newroot_prelight_step} = 0;
	    }
	    if ($pdata{$::p}{newroot_prelight_step} <= 8) {
		my $hurry_overwritten = 0;
		foreach my $b (@{$sticked_bubbles{$::p}}) {
		    next if ($graphics_level == 1 && $b->{'cy'} > 0);  #- in low graphics, only prelight first row
		    $b->{'cx'}+1 == $pdata{$::p}{newroot_prelight_step} and put_image($b->{img}, $b->{'x'}, $b->{'y'});
		    $b->{'cx'} == $pdata{$::p}{newroot_prelight_step} and put_image($bubbles_anim{white}, $b->{'x'}, $b->{'y'});
		    $b->{'cy'} > 6 and $hurry_overwritten = 1;
		}
		$hurry_overwritten && $pdata{$::p}{hurry_save_img} and print_hurry($::p, 1);  #- hurry was potentially overwritten
	    }
	}
	if ($sticking_bubble{$::p} && $graphics_level > 1) {
	    my $b = $sticking_bubble{$::p};
	    if ($fun eq \&erase_image) {
		put_image($b->{img}, $b->{'x'}, $b->{'y'});
	    } else {
		if ($pdata{$::p}{sticking_step} == @{$bubbles_anim{stick}}) {
		    $sticking_bubble{$::p} = undef;
		} else {
		    put_image(${$bubbles_anim{stick}}[$pdata{$::p}{sticking_step}], $b->{'x'}, $b->{'y'});
		    if ($pdata{$::p}{sticking_step_slowdown}) {
			$pdata{$::p}{sticking_step}++;
			$pdata{$::p}{sticking_step_slowdown} = 0;
		    } else {
			$pdata{$::p}{sticking_step_slowdown}++;
		    }
		}
	    }
	}

	#- shooter
        if ($graphics_level > 1) {
            my $num = int($angle{$::p}*$CANON_ROTATIONS_NB/($PI/2) + 0.5)-$CANON_ROTATIONS_NB;
            $fun->($canon{mini_graphics($::p) ? 'img_mini' : 'img'}{$num},
                   $POS{$::p}{canon}{x} + $canon{mini_graphics($::p) ? 'data_mini' : 'data'}{$num}->[0],
                   $POS{$::p}{canon}{'y'} + $canon{mini_graphics($::p) ? 'data_mini' : 'data'}{$num}->[1]);
        } else {
            $fun->($shooter_lowgfx,
                   $POS{$::p}{simpleshooter}{x} + $POS{$::p}{simpleshooter}{diameter}*cos($angle{$::p}),
                   $POS{$::p}{simpleshooter}{'y'} - $POS{$::p}{simpleshooter}{diameter}*sin($angle{$::p}));
        }

        #- penguins
        if (!is_mp_game() || $::p ne 'p1' || !$pdata{p1}{chatting}) {
            if ($graphics_level == 3) {
                my $player = @PLAYERS == 2 && $::p eq 'rp1' ? 'p2' : $::p;
                $fun->($pinguin{$player}{$pdata{$::p}{ping_right}{state}}[$pdata{$::p}{ping_right}{img}],
                       $POS{$player}{left_limit}+$POS{$player}{pinguin}{x}, $POS{$player}{pinguin}{'y'});
            }
        }

        handle_progress($::p);

        #- chat message in mp
        if (is_mp_game() && $pdata{$::p}{chat_msg_delay}) {
            $pdata{$::p}{chat_msg_delay}--;
            if (!$pdata{$::p}{chat_msg_delay}) {
                my $img = @PLAYERS == 2 ? $imgbin{void_chat_small_p2} : member($::p, qw(rp1 rp3)) ? $imgbin{void_chat_small_rp1_rp3} : $imgbin{void_chat_small_rp2_rp4};
                remove_image_from_background($img, $POS{$::p}{chatting}{x}, $POS{$::p}{chatting}{'y'});
                $pdata{current_chat_messages}{$::p} = undef;
                print_next_bubble($next_bubble{$::p}{img}, $::p);
                if (member($::p, 'rp3', 'rp4')) {
                    print_scores($background);
                    print_scores($app);
                }
            }
        }

	#- moving bubbles --> I want them on top of the rest
	foreach (@{$malus_bubble{$::p}}, @{$falling_bubble{$::p}}, @{$exploding_bubble{$::p}}) {
	    $fun->($_->{img}, $_->{'x'}, $_->{'y'});
	}

    };

    if ($levels{current} eq 'mp_train' && $pdata{state} eq 'game') {
        if ($fun ne \&erase_image) {
            my $drect = SDL::Rect->new(-width => $imgbin{void_mp_training}->width, -height => 30, -x => $mp_train_xpos, '-y' => $mp_train_ypos);
            $background_orig->blit($drect, $background, $drect);
            mp_train_print_time();
            $background->blit($drect, $app, $drect);
            push @update_rects, $drect;
            my $seconds = mp_train_time_left();
            if ($seconds == 0) {
                put_image($imgbin{void_panel}, $MENUPOS{xpos_panel}, $MENUPOS{ypos_panel});
                my $y = $MENUPOS{ypos_panel} + 30;
                my @messages = ('', '', '', '', t("Your score after two minutes:"), '', $pdata{p1}{score}, '', t("Press any key."));
                foreach (@messages) {
                    print_('menu', $app, $MENUPOS{xpos_panel}, $y, $_, $imgbin{void_panel}->width, 'center');
                    $y += $smg_lineheight;
                }
                $app->flip;
                play_sound('cancel');
                fb_c_stuff::fbdelay(1000);
                $event->pump while $event->poll != 0;
                grab_key() eq SDLK_ESCAPE() and die 'quit';
                handle_new_hiscores();
                die 'new_game';
            }
        }
    }
}

#- extract it from "handle_graphics" to optimize a bit animations
sub update_malus($$) {
    my ($fun, $p) = @_;
    my $malus_nb = @{$pdata{$p}{malus}};
    my $y_shift = 0;
    while ($malus_nb > 0) {
        my $print = sub($) {
            my ($type) = @_;
            my $type_real = translate_mini_image($type);
            $fun->($type, $POS{$p}{malus}{x} - $type_real->width/2, $POS{$p}{malus}{'y'} - $y_shift - $type_real->height);
            $y_shift += $type_real->height - 1;
        };
        if ($malus_nb >= 7) {
            $print->($malus_gfx{tomate});
            $malus_nb -= 7;
        } else {
            $print->($malus_gfx{banane});
            $malus_nb--;
        }
    }
}

sub malus_change($$) {
    my ($numb, $player) = @_;
    return if $numb == 0 || is_1p_game() && $levels{current} ne 'mp_train';
    if ($levels{current} eq 'mp_train' && $numb > 0) {
        $pdata{p1}{score} += $numb;
        print_scores($app);
        print_scores($background);
        return;
    }
    if ($numb > 0) {
        #- malus are adding up
        if (!is_mp_game()) {
            iter_players_ {
                if ($::p_ ne $player) {
                    update_malus(\&remove_image_from_background, $::p_);
                    push @{$pdata{$::p_}{malus}}, ($frame) x $numb;
                    update_malus(\&put_image_to_background, $::p_);
                }
            };

        } else {
            if (is_local_player($player)) {  #- remote players handled when receiving the 'g' message
                if (!$pdata{sendmalustoone}) {
                    my @living = living_players();
                    if (@living > 1) {
                        $numb = int($numb/(@living-1) + 0.99);
                        iter_players_ {
                            if ($::p_ ne $player && member($::p_, @living)) {
                                is_mp_game() and fb_net::gsend("g$pdata{$::p_}{nick}:$numb");
                                update_malus(\&remove_image_from_background, $::p_);
                                push @{$pdata{$::p_}{malus}}, ($frame) x $numb;
                                update_malus(\&put_image_to_background, $::p_);
                            }
                        };
                    }
                } else {
                    my $p = $pdata{sendmalustoone};
                    is_mp_game() and fb_net::gsend("g$pdata{$p}{nick}:$numb");
                    iter_players_ {  #- get mini graphics
                        if ($::p_ eq $p) {
                            update_malus(\&remove_image_from_background, $::p_);
                            push @{$pdata{$::p_}{malus}}, ($frame) x $numb;
                            update_malus(\&put_image_to_background, $::p_);
                        }
                    };
                }
            }
        }
    } else {
        #- malus are decreasing
        update_malus(\&remove_image_from_background, $player);
        shift @{$pdata{$player}{malus}} while $numb++;
        update_malus(\&put_image_to_background, $player);
    }
}

sub print_compressor() {
    my $x = $POS{compressor_xpos};
    my $y = $POS{p1}{top_limit} + $pdata{$PLAYERS[0]}{newrootlevel} * $ROW_SIZE;
    my ($comp_main, $comp_ext) = ($imgbin{compressor_main}, $imgbin{compressor_ext});

    my $drect = SDL::Rect->new(-width => $comp_main->width, -height => $y,
			       -x => $x - $comp_main->width/2, '-y' => 0);
    $background_orig->blit($drect, $background, $drect);
    $display_on_app_disabled or $background_orig->blit($drect, $app, $drect);
    push @update_rects, $drect;

    put_image_to_background($comp_main, $x - $comp_main->width/2, $y - $comp_main->height);

    $y -= $comp_main->height - 3;

    while ($y > 0) {
	put_image_to_background($comp_ext, $x - $comp_ext->width/2, $y - $comp_ext->height);
	$y -= $comp_ext->height;
    }
}

sub print_ {
    my ($kind, $surface, $x, $y, $text, $size, $alignment, $callback) = @_;
    exists $pangocontext{$kind} or die "$kind is no kind\n";
    if ($size) {
        #- instead of segfaulting
        my $minsize = width($kind, $text);
        if ($minsize > $size) {
            $size = $minsize;
        }
    }
    my $surf = fb_c_stuff::sdlpango_draw_givenalignment($pangocontext{$kind}{context_bg}, $text, $size || -1, $alignment || 'left');
    my $rect = SDL::Rect->new('-x' => $x + 1, '-y' => $y + 1);
    if ($callback) {
        if (!$callback->('about-to-draw', $surf)) {
            #- callback said do not draw
            return;
        }
    }
    SDL::BlitSurface($surf, undef, surf($surface), rect($rect));
    SDL::FreeSurface($surf);
    $surf = fb_c_stuff::sdlpango_draw_givenalignment($pangocontext{$kind}{context_fg}, $text, $size || -1, $alignment || 'left');
    $rect = SDL::Rect->new('-x' => $x, '-y' => $y);
    SDL::BlitSurface($surf, undef, surf($surface), rect($rect));
    SDL::FreeSurface($surf);
}

sub width {
    my ($kind, $text) = @_;
    exists $pangocontext{$kind} or die "$kind is no kind\n";
    my $size = fb_c_stuff::sdlpango_getsize($pangocontext{$kind}{context_fg}, $text, -1);
    return $size->[0];
}

sub mp_disconnect_with_reason {
    my (@messages) = @_;
    put_image($imgbin{void_panel}, $MENUPOS{xpos_panel}, $MENUPOS{ypos_panel});
    my $y = $MENUPOS{ypos_panel} + 30;
    foreach (@messages) {
        print_('menu', $app, $MENUPOS{xpos_panel} + 10, $y, $_, $imgbin{void_panel}->width - 20, 'center');
        $y += $smg_lineheight;
    }
    $app->flip;
    play_sound('cancel');
    fb_c_stuff::fbdelay(2000);
    $event->pump while $event->poll != 0;
    grab_key();
    die 'quit';
}

sub check_mp_connection {
    if (!fb_net::isconnected()) {
        if ($pdata{gametype} eq 'lan') {
            mp_disconnect_with_reason('', '', '', '', t("Lost connection to server!"), '', t("Hoster aborted the game."));
        } else {
            mp_disconnect_with_reason('', '', '', '', t("Lost connection to server!"), '', t("Your lag is probably too high."));
        }
    }
}

sub update_say_mp {
    put_image($imgbin{void_chat}, $POS{p1}{chatting}{x}, $POS{p1}{chatting}{'y'});
    callback_entry('print', { xpos => $POS{p1}{chatting}{x} + 15, ypos => $POS{p1}{chatting}{'y'} + 5, font => 'ingame_chat', maxlen => $imgbin{void_chat}->width - 30 });
    push @update_rects, $apprects{main};
}

sub cleanup_chatting {
    $pdata{p1}{chatting} = 0;
    $event->set_key_repeat(0, 0);
    remove_image_from_background($imgbin{void_chat}, $POS{p1}{chatting}{x}, $POS{p1}{chatting}{'y'});
    print_next_bubble($next_bubble{p1}{img}, 'p1');
    redraw_attackingme();
}

sub set_sendmalustoone {
    my ($whoto) = @_;
    $pdata{sendmalustoone} = undef;
    iter_distant_players_ {
        remove_image_from_background($imgbin{attack}{$::p_}, $POS{$::p_}{attack}{x}, $POS{$::p_}{attack}{'y'});
    };
    if (member($whoto, living_players()) && $pdata{p1}{state} ne 'lost') {
        $pdata{sendmalustoone} = $whoto;
        put_image_to_background($imgbin{attack}{$pdata{sendmalustoone}}, $POS{$pdata{sendmalustoone}}{attack}{x}, $POS{$pdata{sendmalustoone}}{attack}{'y'});
    }
    if ($pdata{protocollevel} >= 1) {
        fb_net::gsend("A$pdata{$pdata{sendmalustoone}}{nick}");
    }
}

sub redraw_attackingme {
    $pdata{p1}{chatting} and return;
    my $xpos = $POS{p1}{attackme}{x};
    my $drect = SDL::Rect->new(-width => 3*24 + $imgbin{attackme}{rp1}->width, -height => $imgbin{attackme}{rp1}->height,
                               -x => $xpos, '-y' => $POS{p1}{attackme}{'y'});
    $background_orig->blit($drect, $background, $drect);
    $background_orig->blit($drect, $app, $drect);
    push @update_rects, $drect;
    foreach my $attackingme (@{$pdata{attackingme}}) {
        put_image_to_background($imgbin{attackme}{$attackingme}, $xpos, $POS{p1}{attackme}{'y'});
        $xpos += 24;
    }
}

sub handle_mp_messages {
    my ($msg) = @_;

    #- in order to keep ordering of actions executed in update_game, we must tolerate only one action at a time
    my $check_action_possible = sub {
        my ($m, $player) = @_;
        #- we check also the presence of malus bubbles, because if the malus order has lagged much
        #- we might receive a fire or even a stick command before the malus bubbles are sticked,
        #- this would provoke a local inconsistency; same for chain reacted bubbles not finished yet
        if ($actions{$player}{mp_fire}
            || $actions{$player}{mp_stick}
            || @{$malus_bubble{$player}}
            || $chainreaction && any { exists $_->{chaindestx} } @{$falling_bubble{$player}}) {
            unshift @$msg, $m;
            return 0;
        } else {
            return 1;
        }
    };

    my %latestangle = ();  #- for smoothing, need to handle last angle only

    while (@$msg) {
        my $m = shift @$msg;
        my $player = $pdata{id2p}{$m->{id}};
        if (!$player) {
            printf "Network protocol error: player with id '%d' doesn't exist. You're not a regular Frozen-Bubble client, aren't you?\n", ord($m->{id});
            die 'quit';
        }
        if ($pdata{$player}{left}) {
            print STDERR "$pdata{$player}{nick}: you don't exist (anymore), go away!\n";
            next;
        }
        my ($command, $params) = $m->{msg} =~ /^(.)(.*)/;
        dbgnet("message received: from=$player message=$m->{msg}");
        if ($command eq 'l') {
            iter_players { #- need iter_players to get the small graphics change for free
                if ($::p eq $player) {
                    $pdata{$::p}{left} = 1;
                    $pdata{$::p}{still_game_messages} = 0;
                    lose($::p);
                }
            };
        } elsif ($command eq 'f') {
            $check_action_possible->($m, $player) or last;
            $actions{$player}{mp_fire} = 1;
            ($angle{$player}, $pdata{$player}{nextcolor}) = $params =~ /(.+):(.+)/;
        } elsif ($command eq 'r') {
            $actions{$player}{left} = 0;
            $actions{$player}{right} = 0;
            $actions{$player}{center} = 0;
            if ($params eq 'l') {
                $actions{$player}{left} = 1;
            } elsif ($params eq 'r') {
                $actions{$player}{right} = 1;
            } elsif ($params eq 'c') {
                $actions{$player}{center} = 1;
            }
        } elsif ($command eq 'a') {
            $latestangle{$player} = $params;
        } elsif ($command eq 's') {
            $check_action_possible->($m, $player) or last;
            #- we can't rely on locally animated launched bubble, to ensure game
            #- consistency we transmit stick positions
            $actions{$player}{mp_stick} = 1;
            ($pdata{$player}{stickcx}, $pdata{$player}{stickcy}, $pdata{$player}{stickcol}, my $newrootcols) = $params =~ /(.+):(.+):(.+):(.*)/;
            @{$pdata{$player}{nextcolors}} = split / /, $newrootcols;
        } elsif ($command eq 'g') {
            my ($destplayer, $numb) = $params =~ /(.+):(.+)/;
            iter_players {
                if ($pdata{$::p}{nick} eq $destplayer) {
                    update_malus(\&remove_image_from_background, $::p);
                    push @{$pdata{$::p}{malus}}, ($frame) x $numb;
                    update_malus(\&put_image_to_background, $::p);
                }
            };
        } elsif ($command eq 'm') {
            #- we may receive new malus bubbles after a distant player is dead already
            #- ignore them instead of wrongly overlaying frozen bubbles
            if ($pdata{$player}{state} eq 'ingame') {
                my ($num, $cx, $cy, $sticky) = $params =~ /(.+):(.+):(.+):(.+)/;
                my $b = create_bubble_given_img_num($num);
                $b->{cx} = $cx;
                $b->{cy} = $cy;
                $b->{'stick_y'} = $sticky;
                iter_players { #- need iter_players to get the small graphics change for free
                    if ($::p eq $player) {
                        calc_real_pos($b, $::p);
                        push @{$malus_bubble{$player}}, $b;
                        malus_change(-1, $player);
                    }
                };
            }
        } elsif ($command eq 'M') {
            my ($cx, $sticky) = $params =~ /(.+):(.+)/;
            #- if network cuts several malussticks in two parts, it's possible that
            #- one malusstick from the first part trigger a lose; at next game run,
            #- the player has lost and his malus bubbles were cleaned up, so malusstick
            #- is not possible, but this is no big deal
            if ($pdata{$player}{state} eq 'ingame') {
                foreach (@{$malus_bubble{$player}}) {
                    if ($_->{cx} == $cx && $_->{'stick_y'} == $sticky) {
                        $_->{mp_stick} = 1;
                        goto ok_malusstick;
                    }
                }
                die "could not find malus bubble to malusstick!\n";
              ok_malusstick:                    
            }
        } elsif ($command eq 'F') {
            #- this is coming too soon. it needs to be collected in the algo to restart a game.
            unshift @$msg, $m;
            last;
        } elsif ($command eq 't') {
            $pdata{current_chat_messages}{$player} = $params;
            $pdata{$player}{chat_msg_delay} = 500;
            play_sound('chatted');
            redraw_chat_message_if_needed($player);
            push @update_rects, $apprects{main};
        } elsif ($command eq 'A') {
            if ($params eq '') {
                @{$pdata{attackingme}} = difference2($pdata{attackingme}, [ $player ]);
            } else {
                if ($params eq $pdata{p1}{nick}) {
                    if (!member($player, @{$pdata{attackingme}})) {
                        push @{$pdata{attackingme}}, $player;
                    }
                } else {
                    @{$pdata{attackingme}} = difference2($pdata{attackingme}, [ $player ]);
                }
            }
            redraw_attackingme();
        } else {
            print STDERR "****** Unrecognized command: $m->{msg}\n";
        }
    }

    foreach my $player (keys %latestangle) {
        #- try to smoothen on network lag
        my $difference = $latestangle{$player} - $angle{$player};
        if (abs($difference) > 0.15) {
            #- we're lagging. ignore that to prevent jerks. hope this is temporary.
            #- I know this will behave bad on high lags but hey I can't do no miracle buddy.
        } else {
            #- we're not lagging so much. but smoothen it up.
            $angle{$player} += $difference / 2;
        }
    }
            
}

sub handle_whenever_events {
    my ($keypressed) = @_;

    if ($keypressed eq $KEYS->{misc}{fs}) {
        $fullscreen = !$fullscreen;
        $app->fullscreen;
    }
    if ($keypressed eq $KEYS->{misc}{toggle_sound}) {
	if (!$mixer_enabled) {
            if ($mixer || init_sound()) {
                $mixer_enabled = 1;
                play_music($current_theoretical_music);
            }
        } else {
            if ($mixer_enabled && $mixer && $mixer->playing_music) {
                $app->delay(10) while $mixer->fading_music;   #- mikmod will deadlock if we try to fade_out while still fading in
                $mixer->playing_music and $mixer->halt_music;
                $app->delay(10) while $mixer->playing_music;  #- mikmod will segfault if we try to load a music while old one is still fading out
            }
            $mixer_enabled = 0;
        }
    }
    if ($mixer_enabled && $mixer && $keypressed eq $KEYS->{misc}{toggle_music}) {
        if ($music_disabled) {
            $music_disabled = undef;
            play_music($current_theoretical_music);
        } else {
            $music_disabled = 1;
            $mixer->halt_music;
        }
    }
    if ($mixer_enabled && $mixer && @playlist && $keypressed eq $KEYS->{misc}{next_playlist_elem}) {
        $mixer->halt_music;
        play_music('dummy');
    }
    if ($mixer_enabled && $mixer && $keypressed eq $KEYS->{misc}{raise_volume}) {
        my $to = int(min($mixer->music_volume(-1) + SDL::Mixer::MIX_MAX_VOLUME()/10, SDL::Mixer::MIX_MAX_VOLUME()));
        $mixer->music_volume($to);
        $mixer->channel_volume(-1, $to);
    }
    if ($mixer_enabled && $mixer && $keypressed eq $KEYS->{misc}{lower_volume}) {
        my $to = int(max($mixer->music_volume(-1) - SDL::Mixer::MIX_MAX_VOLUME()/10, 0));
        $mixer->music_volume($to);
        $mixer->channel_volume(-1, $to);
    }
}

sub handle_game_events() {

    if ($levels{current} eq 'mp_train' && @{$malus_bubble{p1}} == 0 && @{$pdata{p1}{malus}} == 0) {
        if (int(rand($mptrainingdiff*(1000/$TARGET_ANIM_SPEED))) == 0) {
            push @{$pdata{p1}{malus}}, ($frame) x (1 + int(rand(6)));
        }
    }

    if ($playdata) {
        play();
        $event->pump;
        while ($event->poll != 0) {
            if ($event->type == SDL_QUIT) {
                cleanup_and_exit();
            }
            my $keypressed = extended_keypress($event);
            handle_whenever_events($keypressed);
            if ($keypressed eq SDLK_ESCAPE) {
                die 'quit';
            }
            if ($keypressed eq SDLK_PAUSE) {
                my $time_pause = $app->ticks;
                $event->pump while $event->poll != 0;
              pause_playdata:
                while (1) {
                    while ($event->poll != 0) {
                        if ($event->type == SDL_QUIT) {
                            cleanup_and_exit();
                        }
                        my $keypressed = extended_keypress($event);
                        if ($keypressed) {
                            $start_time += $app->ticks - $time_pause;
                            return;
                        }
                    }
                }
            }
            if ($keypressed && $pdata{demo}) {
                die 'quit';
            }
        }
        return;
    }

    $event->pump;
    while ($event->poll != 0) {
        my $keypressed = extended_keypress($event);
        if ($keypressed) {

            if (is_mp_game() && $pdata{p1}{chatting}) {
                if (($keypressed eq SDLK_RETURN() || $keypressed eq SDLK_KP_ENTER())) {
                    if (callback_entry('gettext') > 0) {
                        fb_net::gsend('t' . join('', callback_entry('gettext')));
                    }
                    cleanup_chatting();
                } elsif ($event->type == SDL_KEYDOWN && !member($keypressed, SDLK_RETURN(), SDLK_KP_ENTER(), SDLK_TAB())) {
                    callback_entry('keypressed', { event => $event, maxlen => $imgbin{void_chat}->width - 30, font => 'ingame_chat' });
                    callback_entry('moved');
                    update_say_mp();
                }

            } else {
                iter_local_players {
                    foreach my $action (qw(left right fire center)) {
                        if ($keypressed eq $KEYS->{$::p}{$action}) {
                            $actions{$::p}{$action} = 1;
                            if (is_mp_game() && $action ne 'fire') {
                                $action =~ /./;  #- first letter
                                fb_net::gsend("r$&");
                            }
                            last;
                        }
                    }
                };
                
                if ($keypressed eq $KEYS->{misc}{chat} && is_mp_game()) {
                    callback_entry('reset');
                    $pdata{p1}{chatting} = 1;
                    $event->set_key_repeat(200, 50);
                    put_image_to_background($imgbin{void_chat}, $POS{p1}{chatting}{x}, $POS{p1}{chatting}{'y'});
                }

                if ($keypressed eq SDLK_PAUSE && !is_mp_game()) {
                    my $time_pause = $app->ticks;
                    play_sound('pause');
                    $mixer_enabled && $mixer and $mixer->pause_music;
                    my $back_saved = switch_image_on_background($imgbin{back_paused}, 0, 0, 1);
                    my $index;
                  pause_label:
                    while (1) {
                        my $ticks = $app->ticks;
                        erase_image(${$imgbin{paused}}[$index], 320-${$imgbin{paused}}[$index]->width/2-5, 240-${$imgbin{paused}}[$index]->height/2-4);
                        put_image(${$imgbin{paused}}[$index], 320-${$imgbin{paused}}[$index]->width/2-5, 240-${$imgbin{paused}}[$index]->height/2-4);
                        $app->update(@update_rects);
                        @update_rects = ();
                        $app->delay(20);
                        $event->pump;
                        while ($event->poll != 0) {
                            if ($event->type == SDL_QUIT) {
                                cleanup_and_exit();
                            }
                            my $keypressed = extended_keypress($event);
                            if ($keypressed) {
                                handle_whenever_events($keypressed);
                                if (member($keypressed, SDLK_PAUSE, SDLK_ESCAPE, SDLK_RETURN, SDLK_SPACE)) {
                                    last pause_label;
                                }
                            }
                        }
                        if (++$index == @{$imgbin{paused}}) {
                            $index = 11;
                        }
                        my $to_wait = $TARGET_ANIM_SPEED - ($app->ticks - $ticks);
                        $to_wait > 0 and fb_c_stuff::fbdelay($to_wait);
                    }
                    switch_image_on_background($back_saved, 0, 0);
                    iter_local_players { $actions{$::p}{left} = 0; $actions{$::p}{right} = 0; };
                    $mixer_enabled && $mixer and $mixer->resume_music;
                    $event->pump while $event->poll != 0;
                    $app->flip;
                    $start_time += $app->ticks - $time_pause;
                    is_1p_game() and $time_1pgame += $app->ticks - $time_pause;
                    return;
                }

                if (is_mp_game() && @PLAYERS >= 3 && $singleplayertargetting) {
                    foreach my $rp (qw(rp1 rp2 rp3 rp4)) {
                        if ($keypressed eq $KEYS->{misc}{"send_malus_to_$rp"}) {
                            set_sendmalustoone($rp);
                        }
                    }
                    if ($keypressed eq $KEYS->{misc}{send_malus_to_all}) {
                        set_sendmalustoone(undef);
                    }
                }

                if ($levels{current} !~ /^\d+$/ && $keypressed eq $KEYS->{misc}{save_record}) {
                    print "This game will be recorded when it's over.\n";
                    $recorddata{save} = 1;
                }

                handle_whenever_events($keypressed);
            }
        }

        $keypressed = undef;
	if ($event->type == SDL_KEYUP) {
	    $keypressed = $event->key_sym;
	} elsif ($event->type == fb_c_stuff::JOYAXISMOTION() || $event->type() == fb_c_stuff::JOYBUTTONUP()) {
	    $keypressed = translate_joystick_tokey($event);
            if ($event->type == fb_c_stuff::JOYAXISMOTION() && $keypressed !~ /^joystick\|axisvalue\|\d+\|\d+\|0$/) {  #- we treat position at 0 as KEYUP
                $keypressed = undef;
            }
            $keypressed =~ s/^joystick\|buttonup/joystick|buttondown/;
        }

        if ($keypressed) {
	    iter_local_players {
		foreach my $action (qw(left right fire center)) {
		    if ($keypressed eq $KEYS->{$::p}{$action}) {
                        $actions{$::p}{$action} = 0;
                        is_mp_game() && $action ne 'fire' and fb_net::gsend('r');
                        last;
                    } elsif ($keypressed =~ /^joystick\|axisvalue\|(\d+)\|(\d+)\|0$/ && $KEYS->{$::p}{$action} =~ /^joystick\|axisvalue\|$1\|$2\|/) {
                        $actions{$::p}{$action} = 0;
                        is_mp_game() && $action ne 'fire' and fb_net::gsend('r');
                        #- no last, there might be two values of the same axis
                    }
		}
	    }
	}

	if ($event->type == SDL_KEYDOWN && $event->key_sym == SDLK_ESCAPE) {
            if (is_mp_game()) {
                if ($pdata{p1}{chatting}) {
                    cleanup_chatting();
                    play_sound('cancel');
                } else {
                    $pdata{p1}{left} = 1;
                    lose('p1');
                    die 'quit';
                }
            } else {
                $pdata{p1}{left} = 1;
                die 'quit';
            }
	}
        if ($event->type == SDL_QUIT) {
            cleanup_and_exit();
        }
    }

    if (is_mp_game()) {
        $pdata{p1}{chatting} && callback_entry('ping') and update_say_mp();

        my @messages = fb_net::grecv();
        check_mp_connection();
        $recorddata{mp_messages} = deep_copy(\@messages);  #- if we don't do a deep copy, the dumped data will be recursive for delayed messages

        handle_mp_messages(\@messages);

        fb_net::gdelay_messages(@messages);
    }

    record();
}

sub record {
    $recorddata{frame}++;
    #- check for differences
    my %newdata;
    iter_players {
        foreach my $action (keys %{$actions{$::p}}) {
            $action =~ /^mp/ and next;  #- mp actions will be generated by saving the mp messages
            if ($recorddata{lastactions}{$::p}{$action} ne $actions{$::p}{$action}) {
                $newdata{actions}{$::p}{$action} = $actions{$::p}{$action};
            }
        }
    };
    #- save if at least one difference or mp messages
    if (%newdata || @{$recorddata{mp_messages} || []}) {
        @{$recorddata{mp_messages} || []} and @{$newdata{mp_messages}} = @{$recorddata{mp_messages}};
        push @{$recorddata{data}}, [ $recorddata{frame}, \%newdata ];
    }
    #- save current state
    iter_players {
        foreach my $action (keys %{$actions{$::p}}) {
            $recorddata{lastactions}{$::p}{$action} = $actions{$::p}{$action};
        }
    };
}

our $recordnumber = 0;
sub save_record_if_needed {
    if (($recorddata{save} || $autorecord) && $levels{current} !~ /^\d+$/ && @{$recorddata{data}} > 1 && !$pdata{p1}{left}) {
        if (!$recorddir) {
            $recorddir = "$FBHOME/records";
            print "Notice: no recorddir was specified on commandline; recording in '$recorddir'\n";
            mkdir $recorddir;
        }
        my $filename = sprintf("$recorddir/fb_record_%08d", $recordnumber++);
        -f $filename || -f "$filename.bz2" and return save_record_if_needed();
        my $record = shift @{$recorddata{data}};
        my $data = "record_protocol:$RECORD_PROTOCOL_LEVEL\n"
                 . "players:" . join(',', @PLAYERS) . "\n"
                 . "gametype:$pdata{gametype}\n"
                 . "current_level:$levels{current}\n"
                 . "chainreaction:$chainreaction\n"
                 . "time:" . time() . "\n"
                 . "srand:$record->{srand}\n";
        if (exists $record->{bubbles}) {
            $data .= "bubbles:" . join(',', @{$record->{bubbles}}) . "\n";
        }
        $comment and $data .= "comment:$comment\n";
        if (is_mp_game()) {
            $data .= "mp_result:$pdata{state}\n";
            iter_players {
                $data .= "player_id:$::p|$pdata{$::p}{id}\n"
                       . "player_nick:$::p|$pdata{$::p}{nick}\n";
            };
        }
        foreach my $item (@{$recorddata{data}}) {
            foreach my $playeractions (keys %{$item->[1]{actions}}) {
                foreach my $action (keys %{$item->[1]{actions}{$playeractions}}) {
                    $data .= "frame_action:$item->[0]|$playeractions|$action|$item->[1]{actions}{$playeractions}{$action}\n";
                }
            }
            if (exists $item->[1]{mp_messages}) {
                foreach my $mpmessage (@{$item->[1]{mp_messages}}) {
                    $data .= "frame_mpmessage:$item->[0]|$mpmessage->{id}|$mpmessage->{msg}\n";
                }
            }
        }
        output($filename, $data);
        if (system("bzip2 '$filename' >/dev/null 2>/dev/null") == 0) {
            $filename .= ".bz2";
        }
        print "Record saved in '$filename'. Start Frozen-Bubble with '--replay $filename' to replay the game.\n";
        return 1;
    }
}

sub play {
    $recorddata{frame}++;
    my $nextdata = $playdata->[0];
    if ($nextdata) {
        if ($nextdata->[0] == $recorddata{frame}) {
            foreach my $p (keys %{$nextdata->[1]{actions}}) {
                my %pnew = %{$nextdata->[1]{actions}{$p}};
                $actions{$p}{$_} = $pnew{$_} foreach keys %pnew;
            }
            $nextdata->[1]{mp_messages} and handle_mp_messages($nextdata->[1]{mp_messages});
            shift @$playdata;
        }
    } else {
        if ($pdata{demo}) {
            die 'quit';
        }
    }
}

sub print_scores($) {
    my ($surface) = @_;  
    iter_players_ {  #- sometimes called from within a iter_players so...
        my $score = @PLAYERS == 1 ? ($pdata{$::p_}{score} eq 'random' ? t("Random level")
                                         : $levels{current} eq 'mp_train' ? t("Score: %s", i18n_number($pdata{$::p_}{score}))
                                               : t("Level %s", i18n_number($pdata{$::p_}{score}))) : i18n_number($pdata{$::p_}{score});
        is_mp_game() and $score = sprintf("%s: %s", $pdata{$::p_}{nick}, i18n_number($score));
        my $width = width(mini_graphics($::p_) ? 'ingame_small' : 'ingame', $score);
        my $xpos = $POS{$::p_}{scores}{x} - $width/2;
        my $drect = SDL::Rect->new(-width => $width, -height => mini_graphics($::p_) ? 12 : 24, -x => $xpos, '-y' => $POS{$::p_}{scores}{'y'});
        $background_orig->blit($drect, $surface, $drect);
        push @update_rects, $drect;
        if (mini_graphics($::p_)) {
            print_('ingame_small', $surface, $xpos, $POS{$::p_}{scores}{'y'}, $score);
        } else {
            print_('ingame', $surface, $xpos, $POS{$::p_}{scores}{'y'}, $score);
        }
        redraw_chat_message_if_needed($::p_);
    };
}

sub cleanup_player_bubbles {
    my ($player) = @_;
    @{$malus_bubble{$player}} = ();
    #- reverse sort for freezing effect and win effect
    @{$sticked_bubbles{$player}} = sort { $b->{'cx'}+$b->{'cy'}*10 <=> $a->{'cx'}+$a->{'cy'}*10 } @{$sticked_bubbles{$player}};
    remove_hurry($player);
    @{$falling_bubble{$player}} = grep { !exists $_->{chaindestx} } @{$falling_bubble{$player}};
    $sticking_bubble{$player} = undef;
    $launched_bubble{$player} and destroy_bubbles($player, $launched_bubble{$player});
    $launched_bubble{$player} = undef;
    $pdata{$player}{newroot_prelight} = 0;
}

sub win {
    my ($player) = @_;
    if (!$continuegamewhenplayersleave) {
        every { !$pdata{$_}{left} } @PLAYERS and $pdata{$player}{score}++;
    } else {
        $pdata{$player}{score}++;
    }
    $pdata{$player}{ping_right}{state} = 'win';
    $pdata{$player}{ping_right}{img} = 0;
    print_scores($background);
    print_scores($app);
    cleanup_player_bubbles($player);
}

sub lose {
    my ($player) = @_;
    $pdata{$player}{ping_right}{state} = 'lose_to';
    $pdata{$player}{ping_right}{img} = 0;
    if (!$pdata{$player}{left}) {
        foreach ($launched_bubble{$player}, $tobe_launched{$player}, @{$malus_bubble{$player}}) {
            $_ or next;
            $_->{img} = $bubbles_anim{lose};
            $_->{'x'}--;
            $_->{'y'}--;
        }
        print_next_bubble($bubbles_anim{lose}, $player, 1);
    }
    cleanup_player_bubbles($player);
    
    if (is_mp_game()) {
        $pdata{$player}{state} = 'lost';
        my @living = living_players();
        if (@living == 1) {
            if ((!$continuegamewhenplayersleave && any { $pdata{$_}{left} } @PLAYERS)
                || (every { $pdata{$_}{still_game_messages} == 0 } @PLAYERS)) {
                #- if some players have left and we want to end the game,
                #- or if we've already nothing to wait from others, directly go to won state
                win($living[0]);
                $pdata{state} = "won $living[0]";
            } else {
                #- tentatively suppose we've found the winner, but we need to confirm it by network
                #- first, in rare case of two last players dying at the same time
                my $winnernick = $pdata{$living[0]}{nick};
                $pdata{state} = "finished $living[0]:$winnernick 0";
                #- if I am about to leave, destinations need to receive leave not finished message, else they'll
                #- be waiting forever for it (and there's no point anyway)
                if (!$pdata{p1}{left}) {
                    fb_net::gsend("F$winnernick");
                }
            }
        } else {
            if ($pdata{sendmalustoone} eq $player || $player eq 'p1') {
                set_sendmalustoone(undef);
            }
        }
        if ($pdata{$player}{left}) {
            if (is_distant_player($player)) {
                put_image_to_background(mini_graphics($player) ? $imgbin{"left_${player}_mini"} : $imgbin{left_rp1},
                                        $POS{$player}{left}{x}, $POS{$player}{left}{y}); #}}
            }
            @{$sticked_bubbles{$player}} = ();
            play_sound('cancel');
        } else {
            play_sound('lose');
        }

    } else {
        play_sound('lose');
        $pdata{state} = "lost $player";
        is_2p_game() and win($player eq 'p1' ? 'p2' : 'p1');
    }
  ret:
}

sub verify_if_end {
    iter_players {
	if ($pdata{state} eq 'game' && any { $_->{cy} > 11 } @{$sticked_bubbles{$::p}}) {
            lose($::p);
	}
    };

    if (is_1p_game() && $levels{current} ne 'mp_train' && @{$sticked_bubbles{$PLAYERS[0]}} == 0) {
	put_image_to_background($imgbin{win_panel_1player}, $POS{centerpanel}{x}, $POS{centerpanel}{'y'});
	$pdata{state} = "won $PLAYERS[0]";
	$pdata{$PLAYERS[0]}{ping_right}{state} = 'win';
	$pdata{$PLAYERS[0]}{ping_right}{img} = 0;
        if ($levels{current} ne 'random') {
            $levels{current} and $levels{current}++;
            if ($levels{current} && !$levels{$levels{current}}) {
                $levels{current} = 'WON';
                @{$falling_bubble{$PLAYERS[0]}} = @{$exploding_bubble{$PLAYERS[0]}} = ();
                die 'quit';
            }
        }
    }
}

sub print_hurry($;$) {
    my ($player, $dont_save_background) = @_;
    $player = @PLAYERS == 2 && $player eq 'rp1' ? 'p2' : $player;
    my $t = switch_image_on_background($imgbin{hurry}{$player}, $POS{$player}{left_limit} + $POS{$player}{hurry}{x}, $POS{$player}{hurry}{'y'}, 1);
    $dont_save_background or $pdata{$player}{hurry_save_img} = $t;
}
sub remove_hurry($) {
    my ($player) = @_;
    $player = @PLAYERS == 2 && $player eq 'rp1' ? 'p2' : $player;
    $pdata{$player}{hurry_save_img} and
      switch_image_on_background($pdata{$player}{hurry_save_img}, $POS{$player}{left_limit} + $POS{$player}{hurry}{x}, $POS{$player}{hurry}{'y'});
    $pdata{$player}{hurry_save_img} = undef;
}

sub update_lost {
    my ($player) = @_;

    return if odd($frame);

    if (@{$sticked_bubbles{$player}}) {
        my $b = shift @{$sticked_bubbles{$player}};
        put_image_to_background($bubbles_anim{lose}, --$b->{'x'}, --$b->{'y'});
        
        if (@{$sticked_bubbles{$player}} == 0) {
            if ($graphics_level == 1 && $pdata{state} =~ /^lost (.*)/ && !is_1p_game()) {
                put_image_to_background($imgbin{win}{$player eq 'p1' ? 'p2' : 'p1'}, $POS{centerpanel}{x}, $POS{centerpanel}{'y'});
            }
            if (is_1p_game()) {
                put_image_to_background($imgbin{lose}, $POS{centerpanel}{'x'}, $POS{centerpanel}{'y'});
                play_sound('noh');
            }
        }
    }

    if (!is_mp_game()) {
        $event->pump;
        while ($event->poll != 0) {
            if ($event->type == SDL_QUIT) {
                cleanup_and_exit();
            }
            if ($event->type == SDL_KEYDOWN && $event->key_sym == SDLK_ESCAPE) {
                die 'new_game';
            }
            if (!@{$sticked_bubbles{$player}}) {
                if ($event->type == SDL_KEYDOWN || $event->type == fb_c_stuff::JOYBUTTONUP()) {
                    die 'new_game';
                }
            }
        }
    }

    my $still_sticked = sum(map { @{$sticked_bubbles{$_}} } @PLAYERS);
    if ($pdata{state} eq 'won ' && $still_sticked == 1) {
        put_image($imgbin{void_panel}, $MENUPOS{xpos_panel}, $MENUPOS{ypos_panel});
        print_('menu', $app, $MENUPOS{xpos_panel}, $MENUPOS{ypos_panel} + 30, t("Draw game!"), $imgbin{void_panel}->width - 20, 'center');
        $app->flip;
    }
}

sub update_won {
    my ($player) = @_;

    return if odd($frame);

    iter_players { #- need iter_players to get the small graphics change for free if we're in multiplayer
        if ($::p eq $player) {
            if (@{$sticked_bubbles{$::p}} && $graphics_level > 1) {
                my $b = shift @{$sticked_bubbles{$::p}};
                destroy_bubbles($::p, $b);
                remove_image_from_background($b->{img}, $b->{'x'}, $b->{'y'});
                #- be sure to redraw at least upper line
                foreach (@{$b->{neighbours}}) {
                    next if !member($_, @{$sticked_bubbles{$::p}});
                    put_image_to_background($_->{img}, $_->{'x'}, $_->{'y'});
                }
            } else {
                @{$sticked_bubbles{$::p}} = ();
            }
        }
    };
}

sub decode_postgame_message($) {
    my ($msg) = @_;
    my $player = $pdata{id2p}{$msg->{id}};
    $player ||= 'UNKNOWN';
    if ($msg->{msg} =~ /^F(.*)/) {
        $pdata{$player}{still_game_messages} = 0;
        dbgnet("decoded postgame message: from=$player msg=finished $1");
        return "$player finished $1";
    } elsif ($pdata{$player}{still_game_messages}) {
        dbgnet("decoded postgame message: from=$player msg=gamemsg");
        return "$player gamemsg";
    } elsif ($msg->{msg} eq 'n') {
        $pdata{$player}{ready4newgame} = 1;
        dbgnet("decoded postgame message: from=$player msg=newgame");
        return "$player newgame";
    } else {
        if ($msg->{msg} ne 'l') {
            print "Postgame 'other' message from $player not a leave - $msg->{msg} - should not!\n";
        }
        dbgnet("decoded postgame message: from=$player msg=other");
        return "$player other";
    }
}

#- ----------- mainloop helper --------------------------------------------

sub update_game() {

    if ($pdata{state} eq 'game') {
        handle_game_events();
	iter_players {
            if ($pdata{$::p}{state} eq 'lost') {
                update_lost($::p);

            } elsif ($pdata{$::p}{state} eq 'ingame') {
                $actions{$::p}{left} and $angle{$::p} += $LAUNCHER_SPEED;
                $actions{$::p}{right} and $angle{$::p} -= $LAUNCHER_SPEED;
                if ($actions{$::p}{center}) {
                    if ($angle{$::p} >= $PI/2 - $LAUNCHER_SPEED
                        && $angle{$::p} <= $PI/2 + $LAUNCHER_SPEED) {
                        $angle{$::p} = $PI/2;
                    } else {
                        $angle{$::p} += ($angle{$::p} < $PI/2) ? $LAUNCHER_SPEED : -$LAUNCHER_SPEED;
                    }
                }
                ($angle{$::p} < 0.1) and $angle{$::p} = 0.1;
                ($angle{$::p} > $PI-0.1) and $angle{$::p} = $PI-0.1;
                if (is_mp_game() && is_local_player($::p) && ($actions{$::p}{left} || $actions{$::p}{right} || $actions{$::p}{center})) {
                    fb_net::gsend(sprintf("a%.2f", $angle{$::p}));
                }
                if (every { ! exists $_->{chaindestx} } @{$falling_bubble{$::p}}) {
                    $pdata{$::p}{hurry}++;
                }
                if ((!$no_time_limit || is_mp_game()) && $pdata{$::p}{hurry} > $TIME_HURRY_WARN) {
                    my $oddness = odd(int(($pdata{$::p}{hurry}-$TIME_HURRY_WARN)/(500/$TARGET_ANIM_SPEED))+1);
                    if ($pdata{$::p}{hurry_oddness} xor $oddness) {
                        if ($oddness) {
                            play_sound('hurry');
                            print_hurry($::p);
                        } else {
                            remove_hurry($::p)
                        }
                    }
                    $pdata{$::p}{hurry_oddness} = $oddness;
                }

                if ($actions{$::p}{mp_fire}
                    || (is_local_player($::p)
                        && ($actions{$::p}{fire} || ((!$no_time_limit || is_mp_game()) && $pdata{$::p}{hurry} == $TIME_HURRY_MAX))
                        && !$launched_bubble{$::p}
                        && !(any { exists $_->{chaindestx} } @{$falling_bubble{$::p}})
                        && !@{$malus_bubble{$::p}})) {
                    play_sound('launch');
                    $launched_bubble{$::p} = $tobe_launched{$::p};
                    $launched_bubble{$::p}->{direction} = $angle{$::p};
                    $tobe_launched{$::p} = undef;
                    $actions{$::p}{fire} = 0;
                    $actions{$::p}{hadfire} = 1;
                    $pdata{$::p}{hurry} = 0;
                    remove_hurry($::p);
                    if (is_local_player($::p)) {
                        do {
                            $pdata{$::p}{nextcolor} = int(rand(@bubbles_images));
                        } while (!validate_nextcolor($pdata{$::p}{nextcolor}, $::p) && @{$sticked_bubbles{$::p}});
                    }
                    if (is_mp_game()) {
                        if (is_local_player($::p)) {
                            fb_net::gsend(sprintf("f%.3f:$pdata{$::p}{nextcolor}", $angle{$::p}));
                        } else {
                            $actions{$::p}{mp_fire} = 0;
                        }
                    }
                }

                if ($launched_bubble{$::p}) {
                    if (!$pdata{$::p}{freezelaunchedbubble}) {
                        $launched_bubble{$::p}->{'x_old'} = $launched_bubble{$::p}->{'x'}; # save coordinates for potential collision
                        $launched_bubble{$::p}->{'y_old'} = $launched_bubble{$::p}->{'y'};
                        $launched_bubble{$::p}->{'x'} += $BUBBLE_SPEED * cos($launched_bubble{$::p}->{direction});
                        $launched_bubble{$::p}->{'y'} -= $BUBBLE_SPEED * sin($launched_bubble{$::p}->{direction});
                        if ($launched_bubble{$::p}->{x} < $POS{$::p}{left_limit}) {
                            play_sound('rebound');
                            $launched_bubble{$::p}->{x} = 2 * $POS{$::p}{left_limit} - $launched_bubble{$::p}->{x};
                            $launched_bubble{$::p}->{direction} -= 2*($launched_bubble{$::p}->{direction}-$PI/2);
                        }
                        if ($launched_bubble{$::p}->{x} > $POS{$::p}{right_limit} - $BUBBLE_SIZE) {
                            play_sound('rebound');
                            $launched_bubble{$::p}->{x} = 2 * ($POS{$::p}{right_limit} - $BUBBLE_SIZE) - $launched_bubble{$::p}->{x};
                            $launched_bubble{$::p}->{direction} += 2*($PI/2-$launched_bubble{$::p}->{direction});
                        }
                    }
                    if (!exists $pdata{$::p}{nextcolors}) {
                        @{$pdata{$::p}{nextcolors}} = map { int(rand(@bubbles_images)) } 0..7;
                    }
                    if ($actions{$::p}{mp_stick}) {
                        $actions{$::p}{mp_stick} = 0;
                        stick_bubble($launched_bubble{$::p}, $pdata{$::p}{stickcx}, $pdata{$::p}{stickcy}, $::p, 1);
                        $launched_bubble{$::p} = undef;
                        $pdata{$::p}{freezelaunchedbubble} = 0;
                    } elsif ($launched_bubble{$::p}->{'y'} <= $POS{$::p}{top_limit} + $pdata{$::p}{newrootlevel} * $ROW_SIZE) {
                        my ($cx, $cy) = get_array_closest_pos($launched_bubble{$::p}->{x}, $launched_bubble{$::p}->{'y'}, $::p);
                        if (is_local_player($::p)) {
                            my $col = get_bubble_num($launched_bubble{$::p});
                            is_mp_game() and fb_net::gsend("s$cx:$cy:$col:@{$pdata{$::p}{nextcolors}}");
                            stick_bubble($launched_bubble{$::p}, $cx, $cy, $::p, 1);
                            $launched_bubble{$::p} = undef;
                        } else {
                            $pdata{$::p}{freezelaunchedbubble} = 1;
                        }
                    } else {
                        foreach (@{$sticked_bubbles{$::p}}) {
                            if (is_collision($launched_bubble{$::p}, $_->{'x'}, $_->{'y'})) {
                                my ($cx, $cy) = get_array_closest_pos(($launched_bubble{$::p}->{'x_old'}+$launched_bubble{$::p}->{'x'})/2,
                                                                      ($launched_bubble{$::p}->{'y_old'}+$launched_bubble{$::p}->{'y'})/2,
                                                                      $::p);
                                if (is_local_player($::p)) {
                                    my $col = get_bubble_num($launched_bubble{$::p});
                                    is_mp_game() and fb_net::gsend("s$cx:$cy:$col:@{$pdata{$::p}{nextcolors}}");
                                    stick_bubble($launched_bubble{$::p}, $cx, $cy, $::p, 1);
                                    $launched_bubble{$::p} = undef;
                                    
                                    #- malus generation
                                    if (!any { $_->{chaindestx} } @{$falling_bubble{$::p}}) {
                                        my $malusfreezeframes = 20;
                                        @{$pdata{$::p}{malus}} > 0 && $frame > $pdata{$::p}{malus}[0] + $malusfreezeframes and play_sound('malus');
                                        while (@{$pdata{$::p}{malus}} > 0 && $frame > $pdata{$::p}{malus}[0] + $malusfreezeframes && @{$malus_bubble{$::p}} < 7) {
                                            my $num = int(rand(@bubbles_images));
                                            my $b = create_bubble_given_img_num($num);
                                            $b->{num} = $num;
                                            do {
                                                $b->{'cx'} = int(rand(7));
                                            } while (member($b->{'cx'}, map { $_->{'cx'} } @{$malus_bubble{$::p}}));
                                            $b->{'cy'} = 12;
                                            $b->{'stick_y'} = -1;
                                            foreach (@{$sticked_bubbles{$::p}}) {
                                                if ($_->{'cy'} > $b->{'stick_y'}) {
                                                    if ($_->{'cx'} == $b->{'cx'}
                                                        || odd($_->{'cy'}+$pdata{$::p}{oddswap}) && ($_->{'cx'}+1) == $b->{'cx'}) {
                                                        $b->{'stick_y'} = $_->{'cy'};
                                                    }
                                                }
                                            }
                                            $b->{'stick_y'}++;
                                            calc_real_pos($b, $::p);
                                            push @{$malus_bubble{$::p}}, $b;
                                            malus_change(-1, $::p);
                                        }
                                        #- sort them and shift them
                                        @{$malus_bubble{$::p}} = sort { $a->{'cx'} <=> $b->{'cx'} } @{$malus_bubble{$::p}};
                                        my $shifting = 0;
                                        $_->{'y'} += ($shifting += 7) + int(rand(20)) foreach @{$malus_bubble{$::p}};
                                        if (is_mp_game()) {
                                            fb_net::gsend("m$_->{num}:$_->{cx}:$_->{cy}:$_->{stick_y}") foreach @{$malus_bubble{$::p}};
                                        }
                                    }
                                    
                                } else {
                                    $pdata{$::p}{freezelaunchedbubble} = 1;
                                }
                                last;
                            }
                        }
                    }
                }

                !$tobe_launched{$::p} and generate_new_bubble($::p, $pdata{$::p}{nextcolor});

                if (!$actions{$::p}{left} && !$actions{$::p}{right} && !$actions{$::p}{hadfire}) {
                    $pdata{$::p}{sleeping}++;
                } else {
                    $pdata{$::p}{sleeping} = 0;
                    $pdata{$::p}{ping_right}{movelatency} = -20;
                }
                if ($pdata{$::p}{sleeping} > $TIMEOUT_PINGUIN_SLEEP && $pdata{$::p}{ping_right}{state} !~ /wait/) {
                    $pdata{$::p}{ping_right}{state} = 'wait_to';
                    $pdata{$::p}{ping_right}{img} = 0;
                }
                if ($pdata{$::p}{sleeping} <= $TIMEOUT_PINGUIN_SLEEP && $pdata{$::p}{ping_right}{state} =~ /wait/) {
                    $pdata{$::p}{ping_right}{state} = 'normal';
                }
                foreach my $direction ('left', 'right') {
                    if ($pdata{$::p}{ping_right}{state} eq "${direction}_to" && !($actions{$::p}{$direction})) {
                        $pdata{$::p}{ping_right}{state} = "${direction}_from";
                        $pdata{$::p}{ping_right}{img} = @{$pinguin{$::p}{$pdata{$::p}{ping_right}{state}}} - $pdata{$::p}{ping_right}{img};
                    }
                    if ($pdata{$::p}{ping_right}{state} eq $direction && !($actions{$::p}{$direction})) {
                        $pdata{$::p}{ping_right}{state} = "${direction}_from";
                        $pdata{$::p}{ping_right}{img} = 0;
                    }
                    if ($actions{$::p}{$direction}) {
                        if ($pdata{$::p}{ping_right}{state} eq "${direction}_to") {
                            #- we're animating towards, nothing to do
                        } elsif ($pdata{$::p}{ping_right}{state} eq $direction) {
                            #- we're there, nothing to do
                        } elsif ($pdata{$::p}{ping_right}{state} eq "${direction}_from") {
                            #- we're coming from there, should not happen that much, flicker no big deal
                            $pdata{$::p}{ping_right}{state} = $direction;
                        } else {
                            $pdata{$::p}{ping_right}{state} = "${direction}_to";
                            $pdata{$::p}{ping_right}{img} = 0;
                        }
                    }
                }
                if ($actions{$::p}{hadfire}) {
                    $pdata{$::p}{ping_right}{state} = 'action';
                    $pdata{$::p}{ping_right}{img} = 0;
                    $actions{$::p}{hadfire} = 0;
                }

                if ($pdata{$::p}{ping_right}{img} >= @{$pinguin{$::p}{$pdata{$::p}{ping_right}{state}}}) {
                    $pdata{$::p}{ping_right}{img} = 0;
                }
            }
        };

	verify_if_end();

    } elsif ($pdata{state} =~ /^lost (.*)/) {
        #- 1p and 2p game only state

        my $loser = $1;
        update_lost($loser);
        is_2p_game() and update_won($loser eq 'p1' ? 'p2' : 'p1');
        
    } elsif ($pdata{state} =~ /^finished (\S+):(\S+) (\S+)/) {
        my $supposed_winner_player = $1;
        my $supposed_winner_nick = $2;
        my $timeout_counter = $3;

        if ($playdata) {
            $pdata{state} = $recorddata{pdatas}{mp_result};
            if ($pdata{state} eq 'won ') {
                lose($supposed_winner_player);  #- draw game
            } else {
                win($supposed_winner_player);
            }

        } else {
            $event->pump while $event->poll != 0;
            #- mp game only state when we're trying to figure out if this is not a draw game

            if (my $msg = fb_net::grecv_get1msg_ifdata()) {
                my $result = decode_postgame_message($msg);
                iter_players { dbgnet("\tstill game message of=$pdata{$::p}{nick} value=$pdata{$::p}{still_game_messages}"); };
                if ($result =~ /^(\S+) finished (\S+)/) {
                    my $remote_winner_nick = $2;
                    if ($remote_winner_nick ne $supposed_winner_nick) {
                        #- players don't agree with who won the game, this is a draw game
                        lose($supposed_winner_player);
                        $pdata{state} = "won ";
                    } else {
                        if (every { $pdata{$_}{still_game_messages} == 0 } @PLAYERS) {
                            win($supposed_winner_player);
                            $pdata{state} = "won $supposed_winner_player";
                        }
                    }
                } elsif ($result =~ /^(\S+) other/) {
                    $pdata{$pdata{id2p}{$msg->{id}}}{left} = 1;
                    win($supposed_winner_player);
                    $pdata{state} = "won $supposed_winner_player";
                } elsif ($result !~ /^(\S+) gamemsg/) {
                    #- delay other players already asking for a new game (we're waiting for agreement on winner from others)
                    fb_net::gdelay_messages($msg);
                }
                
            } else {
                check_mp_connection();
                #- timeout for receiving the winners. it could happen when a client is badly killed.
                $timeout_counter++;
                if ($timeout_counter > 10 * (1000/$TARGET_ANIM_SPEED) ) {  #- 10 seconds
                    mp_disconnect_with_reason('', '', '', '', '', t("Lost synchronization %s!", 1));
                } else {
                    $pdata{state} = "finished $supposed_winner_player:$supposed_winner_nick $timeout_counter";
                }
            }
        }

    } elsif ($pdata{state} =~ /^won (.*)/) {
        #- mp and 1p game only state

        my $events_pumped;
        my $winner = $1;
        if (is_mp_game()) {
            $winner and update_won($winner);
            iter_players {
                if ($::p ne $winner) {
                    update_lost($::p);
                }
            };
            check_mp_connection();
            $frame % (1000/$TARGET_ANIM_SPEED) == 0 and fb_net::gsend('p');
        }
	if (!$winner || @{$exploding_bubble{$winner}} == 0) {
            my $still_needwait = 0;
            #- still wait if some bubbles are not yet "frozen"
            iter_players {
                $still_needwait += @{$sticked_bubbles{$::p}};
            };
            if (!$still_needwait) {
                $event->pump;
                $events_pumped = 1;

                my $mp_newgame = sub {
                    if ((!$continuegamewhenplayersleave && any { $pdata{$_}{left} } @PLAYERS)
                        || ($continuegamewhenplayersleave && notleft_players() <= 1)
                        || ($pdata{scorelimit} && any { $pdata{$_}{score} == $pdata{scorelimit} } @PLAYERS)) {
                        die 'quit';
                    };
                    dbgnet("send newgame 'n'");
                    fb_net::gsend('n');
                    my $timeout_counter;

                    while (1) {
                        if (my $msg = fb_net::grecv_get1msg_ifdata()) {
                            my $result = decode_postgame_message($msg);
                            if ($result =~ /^(\S+) other/) {
                                $pdata{$pdata{id2p}{$msg->{id}}}{left} = 1;
                                !$continuegamewhenplayersleave || notleft_players() <= 1 and die 'quit';
                            }
                        }
                        iter_distant_players {
                            !$pdata{$::p}{left} && $pdata{$::p}{ready4newgame} == 0 and goto still_waiting;
                        };
                        die 'new_game';
                      still_waiting:
                        $event->pump;
                        while ($event->poll != 0) {
                            if ($event->type == SDL_KEYDOWN && $event->key_sym == SDLK_ESCAPE()) {
                                die 'quit';
                            }
                        }
                        check_mp_connection();
                        fb_c_stuff::fbdelay($TARGET_ANIM_SPEED);
                        $frame++;
                        $frame % (1000/$TARGET_ANIM_SPEED) == 0 and fb_net::gsend('p');
                        $timeout_counter++;
                        if ($timeout_counter > 10 * (1000/$TARGET_ANIM_SPEED) ) {  #- 10 seconds
                            mp_disconnect_with_reason('', '', '', '', '', t("Lost synchronization %s!", 2));
                        }
                    }
                };

                while ($event->poll != 0) {
                    if ($event->type == SDL_QUIT) {
                        cleanup_and_exit();
                    }
                    if ($event->type == SDL_KEYDOWN && $event->key_sym == SDLK_ESCAPE()) {
                        die 'quit';
                    }
                    if ($event->type == SDL_KEYDOWN || $event->type == fb_c_stuff::JOYBUTTONUP()) {
                        if ($playdata) {
                            die 'quit';
                        } elsif (is_mp_game()) {
                            $mp_newgame->();
                        } else {
                            die 'new_game';
                        }
                    }
                }

                if (is_mp_game()) {
                    if (my $msg = fb_net::grecv_get1msg_ifdata()) {
                        my $result = decode_postgame_message($msg);
                        if ($result =~ /^(\S+) newgame/) {
                            $mp_newgame->();
                        } elsif ($result =~ /^(\S+) other/) {
                            $pdata{$pdata{id2p}{$msg->{id}}}{left} = 1;
                            !$continuegamewhenplayersleave || notleft_players() <= 1 and die 'quit';
                        }
                    }
                    check_mp_connection();
                }
            }
        }
        if (!$events_pumped) {
            $event->pump;
            while ($event->poll != 0) {
                if ($event->type == SDL_QUIT) {
                    cleanup_and_exit();
                }
                if ($event->type == SDL_KEYDOWN && $event->key_sym == SDLK_ESCAPE) {
                    die 'quit';
                }
            }
        }

    } else {
	die "oops unhandled game state ($pdata{state})\n";
    }

    if (is_mp_game()) {
        $frame % (1000/$TARGET_ANIM_SPEED) == 0 and fb_net::gsend('p');
    }

    #- things that need to be updated in all states of the game
    iter_players {
	my $malus_end = [];
	foreach my $b (@{$malus_bubble{$::p}}) {
	    !$b->{freeze} and $b->{'y'} -= $MALUS_BUBBLE_SPEED;
	    if (get_array_yclosest($b->{'y'}, $::p) <= $b->{'stick_y'}) {
                if (is_local_player($::p)) {
                    real_stick_bubble($b, $b->{'cx'}, $b->{'stick_y'}, $::p, 0);
                    push @$malus_end, $b;
                    #- remote decode_postgame_message will not like receiving the M command after we sent the F command
                    is_mp_game() && $pdata{state} eq 'game' and fb_net::gsend("M$b->{cx}:$b->{stick_y}");
                } else {
                    $b->{freeze} = 1;
                }
	    }
            if ($b->{mp_stick}) {
                real_stick_bubble($b, $b->{'cx'}, $b->{'stick_y'}, $::p, 0);
                push @$malus_end, $b;
            }
	}
	@$malus_end and @{$malus_bubble{$::p}} = difference2($malus_bubble{$::p}, $malus_end);

	my $falling_end = [];
	foreach my $b (@{$falling_bubble{$::p}}) {
	    if ($b->{wait_fall}) {
		$b->{wait_fall}--;
	    } else {
                my $maxy = @PLAYERS == 2 ? 380 : member($::p, 'rp1', 'rp2') ? 185 : member($::p, 'rp3', 'rp4') ? 415 : 380;
		if (exists $b->{chaindestx} && ($b->{'y'} > $maxy || $b->{chaingoingup})) {
		    my $acceleration = $FREE_FALL_CONSTANT*3;
		    if (!$b->{chaingoingup}) {
			my $time_to_zero = $b->{speed}/$acceleration;
			my $distance_to_zero = $b->{speed} * ($b->{speed}/$acceleration + 1) / 2;
                        my $tobe_sqrted = 1 + 8/$acceleration*($b->{'y'}-$b->{chaindesty}+$distance_to_zero);
                        if ($tobe_sqrted < 0) {
                            #- avoid SQRT of a negative number
                            $b->{speedx} = 0;
                        } else {
                            my $time_to_destination = (-1 + sqrt($tobe_sqrted)) / 2;
                            if ($time_to_zero + $time_to_destination == 0) {
                                #- avoid division by zero
                                $b->{speedx} = 0;
                            } else {
                                $b->{speedx} = ($b->{chaindestx} - $b->{x}) / ($time_to_zero + $time_to_destination);
                            }
                        }
			$b->{chaingoingup} = 1;
		    }
		    $b->{speed} -= $acceleration;
		    $b->{x} += $b->{speedx};
		    if (abs($b->{x} - $b->{chaindestx}) < abs($b->{speedx})) {
			$b->{'x'} = $b->{chaindestx};
			$b->{speedx} = 0;
		    }
		    $b->{'y'} += $b->{speed};
		    $b->{'y'} < $b->{chaindesty} and push @$falling_end, $b;
		} else {
		    $b->{'y'} += $b->{speed};
		    $b->{speed} += $FREE_FALL_CONSTANT;
		}
	    }
	    $b->{'y'} > 470 && !exists $b->{chaindestx} and push @$falling_end, $b;
	}
	@$falling_end and @{$falling_bubble{$::p}} = difference2($falling_bubble{$::p}, $falling_end);
	foreach (@$falling_end) {
	    exists $_->{chaindestx} or next;
	    @{$chains{$::p}{falling_chained}} = difference2($chains{$::p}{falling_chained}, [ $_ ]);
	    delete $chains{$::p}{chained_bubbles}{$_};
	    stick_bubble($_, $_->{chaindestcx}, $_->{chaindestcy}, $::p, 0);
	}

	my $exploding_end = [];
	foreach my $b (@{$exploding_bubble{$::p}}) {
	    $b->{'x'} += $b->{speedx};
	    $b->{'y'} += $b->{speedy};
	    $b->{speedy} += $FREE_FALL_CONSTANT;
	    push @$exploding_end, $b if $b->{'y'} > 470;
	}
	if (@$exploding_end) {
	    @{$exploding_bubble{$::p}} = difference2($exploding_bubble{$::p}, $exploding_end);
            if (!@{$exploding_bubble{$::p}} && !@{$sticked_bubbles{$::p}}) {
                if ($pdata{state} =~ /^lost (.*)/ && $::p ne $1 && !is_1p_game()) {
                    put_image($imgbin{win}{$::p}, $POS{centerpanel}{'x'}, $POS{centerpanel}{'y'});
                }
                if (is_mp_game() && $pdata{state} =~ /^won (.*)/) {
                    my $winner = $1;
                    my $img = $winner eq 'p1' ? $imgbin{win_panel_p1_net} : @PLAYERS == 2 ? $imgbin{win}{rp3} : $imgbin{win}{$winner};
                    put_image($img, $POS{centerpanel}{x}, $POS{centerpanel}{'y'});
                    print_('ingame', $app, 264, 300, $pdata{$winner}{nick}, 198, 'center');
                    my $xpos = @PLAYERS <= 3 ? 352 : @PLAYERS == 4 ? 348 : 322;
                    iter_players_ {
                        if ($::p_ ne $winner) {
                            my $img = @PLAYERS == 2 && $::p_ eq 'rp1' ? $imgbin{net_lose}{rp3} : $imgbin{net_lose}{$::p_};  #- fix colors
                            put_image($img, $xpos, 264);
                            $xpos += 32;
                        }
                    };
                }
            }
	}

	if (member($pdata{$::p}{ping_right}{state}, qw(action right_to right_from left_to left_from wait_to wait win lose_to lose))) {
	    $pdata{$::p}{ping_right}{img}++;
#            print "...state $pdata{$::p}{ping_right}{state} image $pdata{$::p}{ping_right}{img}\n";
	    if ($pdata{$::p}{ping_right}{img} == @{$pinguin{$::p}{$pdata{$::p}{ping_right}{state}}}) {
#                print "finish images of state $pdata{$::p}{ping_right}{state} (" . int(@{$pinguin{$::p}{$pdata{$::p}{ping_right}{state}}}) . ")\n";
                if ($pdata{$::p}{ping_right}{state} eq 'right_to') {
                    $pdata{$::p}{ping_right}{state} = 'right';
                } elsif ($pdata{$::p}{ping_right}{state} eq 'left_to') {
                    $pdata{$::p}{ping_right}{state} = 'left';
                } elsif ($pdata{$::p}{ping_right}{state} eq 'wait_to') {
                    $pdata{$::p}{ping_right}{state} = 'wait';
                } elsif ($pdata{$::p}{ping_right}{state} eq 'wait') {
                    #- wait loops, don't change state
                } elsif ($pdata{$::p}{ping_right}{state} eq 'win') {
                    #- simple loop, don't change state
                } elsif ($pdata{$::p}{ping_right}{state} eq 'lose_to') {
                    $pdata{$::p}{ping_right}{state} = 'lose';
                } elsif ($pdata{$::p}{ping_right}{state} eq 'lose') {
                    #- lose loops, don't change state
                } else {
                    $pdata{$::p}{ping_right}{state} = 'normal';
                }
                $pdata{$::p}{ping_right}{img} = 0;
#                print "=> state $pdata{$::p}{ping_right}{state}\n";
            }
        }

    };

    #- advance playlist when the current song finished
    $mixer_enabled && $mixer && @playlist && !$mixer->playing_music and play_music('dummy');
}

#- ----------- init stuff -------------------------------------------------

our $init_step = 0;
sub print_step($) {
    my ($txt) = @_;
    print $txt;
    $event->pump;
    while ($event->poll != 0) {
        if ($event->type == SDL_QUIT
            || $event->type == SDL_KEYDOWN && $event->key_sym == SDLK_ESCAPE) {
            cleanup_and_exit();
        }
    }
    put_image($imgbin{loading_step}, 100 + $init_step*12, 10);
    if ($init_step == 0) {
        put_image($imgbin{loading_step_initial}, 100 + $_*12, 10) foreach 1..11;
    }
    $app->flip;
    $init_step++;
}

sub load_levelset {
    my ($levelset_name) = @_;

    -e $levelset_name or die "No such levelset ($levelset_name).\n";

    $loaded_levelset = $levelset_name;
    my $row_numb = 0;
    my $curr_level = $levels{current};

    %levels = ();
    $levels{current} = $curr_level;
    $lev_number = 1;

    foreach my $line (cat_($levelset_name)) {
	if ($line !~ /\S/) {
	    if ($row_numb) {
		$lev_number++;
		$row_numb = 0;
	    }
	} else {
	    my $col_numb = 0;
	    foreach (split ' ', $line) {
		/-/ or push @{$levels{$lev_number}}, { cx => $col_numb, cy => $row_numb, img_num => $_ };
		$col_numb++;
	    }
	    $row_numb++;
	}
    }
}

our $surfstyle;
sub surf {
    my ($surface) = @_;
    $surfstyle ||= UNIVERSAL::isa($surface, 'HASH') ? 'hashref' : 'scalarref';
    return $surfstyle eq 'hashref' ? $surface->{-surface} : $$surface;
}
sub rect {
    my ($rect) = @_;
    return $surfstyle eq 'hashref' ? $rect->{-rect} : $$rect;
}
our $evtstyle;
sub evt {
    my ($evt) = @_;
    $evtstyle ||= UNIVERSAL::isa($evt, 'HASH') ? 'hashref' : 'scalarref';
    return $evtstyle eq 'hashref' ? $evt->{-event} : $$evt;
}
our $colstyle;
sub col {
    my ($color) = @_;
    $colstyle ||= UNIVERSAL::isa($color, 'HASH') ? 'hashref' : 'scalarref';
    return $colstyle eq 'hashref' ? $color->{-color} : $$color;
}

sub init_game() {
    -r "$FPATH/$_" or die "[*ERROR*] the datafiles seem to be missing! (could not read `$FPATH/$_')\n".
                          "          The datafiles need to go to `$FPATH'.\n"
			    foreach qw(gfx snd data);

    print '[SDL Init] ';
    $app = SDL::App->new(-icon => "$FPATH/gfx/pinguins/window_icon_penguin.png", -flags => $sdl_flags | ($fullscreen ? SDL_FULLSCREEN : 0), -title => 'Frozen-Bubble 2', -width => 640, -height => 480);

    my $joys = SDL::NumJoysticks();
    $joysticksinfo and print "\nfound $joys joystick(s)\n";
    for (my $i = 0; $i < $joys; $i++) {
	push @joysticks, SDL::JoystickOpen($i);
        $joysticksinfo and print "\t" . ($i + 1) . ': ' . (SDL::JoystickName(SDL::JoystickIndex($joysticks[$i])) || 'unknown joystick') . "\n";
    }
    $frame = 0;

    $apprects{main} = SDL::Rect->new(-width => $app->width, -height => $app->height);
    $event = SDL::Event->new;
    $event->set_unicode(1);
    SDL::Cursor::show(0);
    $imgbin{loading} = add_image('loading.png');
    put_image($imgbin{loading}, 10, 10);
    $app->flip;
    $imgbin{loading_step} = add_image('loading_step.png');
    $imgbin{loading_step_initial} = add_image('loading_step_initial.png');
 
    print_step('[Graphics');
    $imgbin{back_2p} = SDL::Surface->new(-name => "$FPATH/gfx/backgrnd.png");
    $imgbin{back_1p} = SDL::Surface->new(-name => "$FPATH/gfx/back_one_player.png");
    $imgbin{back_mp} = SDL::Surface->new(-name => "$FPATH/gfx/back_multiplayer.png");
    $background = SDL::Surface->new(-width => $app->width, -height => $app->height, -depth => 32, -Amask => '0 but true');
    $background_orig = SDL::Surface->new(-width => $app->width, -height => $app->height, -depth => 32, -Amask => '0 but true');

    fb_c_stuff::sdlpango_init();
    $pangocontext{netdialogs} =           { params => { desc => 'sans 10', fg => 'white', bg => 'black' } };
    $pangocontext{netdialogs_servermsg} = { params => { desc => 'sans italic 10', fg => 'white', bg => 'black' } };
    $pangocontext{menu} =                 { params => { desc => 'sans 11', fg => 'white', bg => 'black' } };
    $pangocontext{bold_menu} =            { params => { desc => 'sans 11 bold', fg => 'white', bg => 'black' } };
    $pangocontext{ingame} =               { params => { desc => 'sans 14', fg => 'white', bg => 'black' } };
    $pangocontext{ingame_chat} =          { params => { desc => 'sans 10', fg => 'black', bg => 'white' } };
    $pangocontext{ingame_small} =         { params => { desc => 'sans 8', fg => 'white', bg => 'black' } };
    $pangocontext{ingame_small_chat} =    { params => { desc => 'sans 8', fg => 'white', bg => 'black' } };
    $pangocontext{$_}{context_fg} = fb_c_stuff::sdlpango_createcontext($pangocontext{$_}{params}{fg}, $pangocontext{$_}{params}{desc}) foreach keys %pangocontext;
    $pangocontext{$_}{context_bg} = fb_c_stuff::sdlpango_createcontext($pangocontext{$_}{params}{bg}, $pangocontext{$_}{params}{desc}) foreach keys %pangocontext;

    foreach my $ball (1..8) {
        my $img = add_bubble_image('balls/bubble-'.($colourblind && 'colourblind-')."$ball.gif");
        $img_mini{$img} = add_image('balls/bubble-'.($colourblind && 'colourblind-')."${ball}-mini.png");
    }
    $bubbles_anim{white} = add_image("balls/bubble_prelight.png");
    $img_mini{$bubbles_anim{white}} = add_image("balls/bubble_prelight-mini.png");
    $bubbles_anim{lose} = add_image("balls/bubble_lose.png");
    $img_mini{$bubbles_anim{lose}} = add_image("balls/bubble_lose-mini.png");
    $bubbles_anim{on_top_next} = add_image("on_top_next.png");
    $img_mini{$bubbles_anim{on_top_next}} = add_image("on_top_next-mini.png");
    foreach my $step (0..6) {
        push @{$bubbles_anim{stick}}, my $img = add_image("balls/stick_effect_$step.png");
        $img_mini{$img} = add_image("balls/stick_effect_${step}-mini.png")
    }

    $shooter_lowgfx = add_image("shooter-lowgfx.png");
    my $shooter = add_image("shooter.png");
    my $shooter_mini = add_image("shooter-mini.png");
    foreach my $number (-$CANON_ROTATIONS_NB..$CANON_ROTATIONS_NB) {
        my $angle = $number*($PI/2)/$CANON_ROTATIONS_NB;

        $canon{img}{$number} = SDL::Surface->new(-width => $shooter->width, -height => $shooter->height, -depth => 32);
        fb_c_stuff::rotate_bicubic(surf($canon{img}{$number}), surf($shooter), $angle);
        $canon{data}{$number} = fb_c_stuff::autopseudocrop(surf($canon{img}{$number}));
        #- now crop (and use native RGBA ordering)
        my $replace = SDL::Surface->new(-width => $canon{data}{$number}[2], -height => $canon{data}{$number}[3], -depth => 32);
        $canon{img}{$number}->set_alpha(0, 0);
        $canon{img}{$number}->blit(SDL::Rect->new('-x' => $canon{data}{$number}[0], '-y' => $canon{data}{$number}[1],
                                                  -width => $canon{data}{$number}[2], -height => $canon{data}{$number}[3]), $replace, undef);
        $canon{img}{$number} = $replace;
        add_default_rect($canon{img}{$number});

        $canon{img_mini}{$number} = SDL::Surface->new(-width => $shooter_mini->width, -height => $shooter_mini->height, -depth => 32);
        fb_c_stuff::rotate_bicubic(surf($canon{img_mini}{$number}), surf($shooter_mini), $angle);
        $canon{data_mini}{$number} = fb_c_stuff::autopseudocrop(surf($canon{img_mini}{$number}));
        #- now crop (and use native RGBA ordering)
        my $replace = SDL::Surface->new(-width => $canon{data_mini}{$number}[2], -height => $canon{data_mini}{$number}[3], -depth => 32);
        $canon{img_mini}{$number}->set_alpha(0, 0);
        $canon{img_mini}{$number}->blit(SDL::Rect->new('-x' => $canon{data_mini}{$number}[0], '-y' => $canon{data_mini}{$number}[1],
                                                       -width => $canon{data_mini}{$number}[2], -height => $canon{data_mini}{$number}[3]), $replace, undef);
        $canon{img_mini}{$number} = $replace;
        add_default_rect($canon{img_mini}{$number});

        $number eq -$CANON_ROTATIONS_NB/2 and print_step('.'); 
        $number eq 0 and print_step('.'); 
        $number eq $CANON_ROTATIONS_NB/2 and print_step('.'); 
    }

    print_step('.'); 
    $malus_gfx{banane} = add_image('banane.png');
    $img_mini{$malus_gfx{banane}} = add_image('banane-mini.png');
    $malus_gfx{tomate} = add_image('tomate.png');
    $img_mini{$malus_gfx{tomate}} = add_image('tomate-mini.png');

    $imgbin{back_paused} = add_image('back_paused.png');
    push @{$imgbin{paused}}, add_image("pause_00$_.png") foreach '01'..'35';
    $imgbin{lose} = add_image('lose_panel.png');
    $imgbin{progress_red} = add_image('dot_red.png');
    $imgbin{progress_green} = add_image('dot_green.png');
    $imgbin{win_panel_1player} = add_image('win_panel_1player.png');
    $imgbin{win_panel_p1_net} = add_image('win_panel_p1_net.png');
    $imgbin{compressor_main} = add_image('compressor_main.png');
    $imgbin{compressor_ext} = add_image('compressor_ext.png');

    $imgbin{back_menu} = SDL::Surface->new(-name => "$FPATH/gfx/menu/back_start.png");
    $imgbin{stamp} = add_image('menu/stamp.png');
    $imgbin{menu_closedeye_green_left} = add_image('menu/backgrnd-closedeye-left-green.png');
    $imgbin{menu_closedeye_green_right} = add_image('menu/backgrnd-closedeye-right-green.png');
    $imgbin{menu_closedeye_purple_left} = add_image('menu/backgrnd-closedeye-left-purple.png');
    $imgbin{menu_closedeye_purple_right} = add_image('menu/backgrnd-closedeye-right-purple.png');
    $imgbin{menu_logo} = add_image('menu/fblogo.png');
    $imgbin{menu_logo_mask} = add_image('menu/fblogo-mask.png');
    $imgbin{txt_1pgame_off}  = add_image('menu/txt_1pgame_off.png');
    $imgbin{txt_1pgame_over} = add_image('menu/txt_1pgame_over.png');
    $imgbin{txt_2pgame_off}  = add_image('menu/txt_2pgame_off.png');
    $imgbin{txt_2pgame_over} = add_image('menu/txt_2pgame_over.png');
    $imgbin{txt_netgame_off}  = add_image('menu/txt_netgame_off.png');
    $imgbin{txt_netgame_over} = add_image('menu/txt_netgame_over.png');
    $imgbin{txt_langame_off}  = add_image('menu/txt_langame_off.png');
    $imgbin{txt_langame_over} = add_image('menu/txt_langame_over.png');
    $imgbin{txt_editor_off}  = add_image('menu/txt_editor_off.png');
    $imgbin{txt_editor_over} = add_image('menu/txt_editor_over.png');
    $imgbin{txt_keys_off}  = add_image('menu/txt_keys_off.png');
    $imgbin{txt_keys_over} = add_image('menu/txt_keys_over.png');
    $imgbin{txt_graphics_off}  = add_image('menu/txt_graphics_off.png');
    $imgbin{txt_graphics_over} = add_image('menu/txt_graphics_over.png');
    $imgbin{txt_highscores_off}  = add_image('menu/txt_highscores_off.png');
    $imgbin{txt_highscores_over} = add_image('menu/txt_highscores_over.png');
    $imgbin{void_panel} = add_image('menu/void_panel.png');
    $imgbin{'1p_panel'} = add_image('menu/1p_panel.png');
    $imgbin{void_chat} = add_image('void_chat.png');
    $imgbin{void_chat_small_p2} = add_image('void_chat_small_p2.png');
    $imgbin{void_chat_small_rp1_rp3} = add_image('void_chat_small_rp1_rp3.png');
    $imgbin{void_chat_small_rp2_rp4} = add_image('void_chat_small_rp2_rp4.png');
    $imgbin{void_mp_training} = add_image('void_mp_training.png');
    $imgbin{ping_low} = add_image('menu/ping-low.png');
    $imgbin{ping_mid} = add_image('menu/ping-mid.png');
    $imgbin{ping_high} = add_image('menu/ping-high.png');
    $imgbin{menu_cursor}{'1pgame'} = [ map { add_image("menu/anims/1pgame_00$_.png") } ('01'..'30') ];
    $imgbin{menu_cursor}{'2pgame'} = [ map { add_image("menu/anims/p1p2_00$_.png") } ('01'..'30') ];
    $imgbin{menu_cursor}{langame} = [ map { add_image("menu/anims/langame_00$_.png") } ('01'..'70') ];
    $imgbin{menu_cursor}{netgame} = [ map { add_image("menu/anims/netgame_00$_.png") } ('01'..'89') ];
    $imgbin{menu_cursor}{editor} = [ map { add_image("menu/anims/editor_00$_.png") } ('01'..'67') ];
    $imgbin{menu_cursor}{graphics3} = [ map { add_image("menu/anims/gfx-l1_00$_.png") } ('01'..'30') ];
    $imgbin{menu_cursor}{graphics2} = [ map { add_image("menu/anims/gfx-l2_00$_.png") } ('01'..'30') ];
    $imgbin{menu_cursor}{graphics1} = [ add_image("menu/anims/gfx-l3_0001.png") ];
    $imgbin{menu_cursor}{keys} = [ map { add_image("menu/anims/keys_00$_.png") } ('01'..'80') ];
    $imgbin{menu_cursor}{highscores} = [ map { add_image("menu/anims/highscore_00$_.png") } ('01'..'89') ];
    foreach my $cursortype (keys %{$imgbin{menu_cursor}}) {
        foreach my $img (@{$imgbin{menu_cursor}{$cursortype}}) {
            my $alpha = SDL::Surface->new(-width => $img->width, -height => $img->height, -depth => 32);
            $img->set_alpha(0, 0);  #- for RGBA->RGBA blits, SDL_SRCALPHA must be removed or destination alpha is preserved
            $img->blit(undef, $alpha, undef);
            fb_c_stuff::alphaize(surf($alpha));
            push @{$imgbin{menu_cursor}{"${cursortype}alpha"}}, $alpha;
            $img->set_alpha(SDL_SRCALPHA(), 0);
        }
    }
    $imgbin{left_rp1} = add_image('left-rp1.png');
    $imgbin{"left_${_}_mini"} = add_image("left-${_}-mini.png") foreach qw(rp1 rp2 rp3 rp4);
    $imgbin{highlight_server} = add_image('menu/highlight-server.png');

    #- little flags
    foreach my $f (glob("$FPATH/gfx/flags/*.png")) {
        $f =~ /flag-(\S+)\.png/;
        $imgbin{flag}{$1} = add_image_file($f);
    }

    my @levelsets = sort glob("$FBLEVELS/*");

    #- scrolling banner
    $imgbin{banner_artwork} = add_image('menu/banner_artwork.png');
    $imgbin{banner_soundtrack} = add_image('menu/banner_soundtrack.png');
    $imgbin{banner_cpucontrol} = add_image('menu/banner_cpucontrol.png');
    $imgbin{banner_leveleditor} = add_image('menu/banner_leveleditor.png');

    $MENUPOS{xpos_panel} = (640 - $imgbin{void_panel}->width) / 2;
    $MENUPOS{ypos_panel} = (480 - $imgbin{void_panel}->height) / 2;

    #- 1p and 2p menu images
    $imgbin{txt_1pmenu_off} = add_image('menu/txt_menu_1p_off.png');
    $imgbin{txt_1pmenu_over} = add_image('menu/txt_menu_1p_over.png');
    $imgbin{txt_1pmenu_play_all_levels_text} = add_image('menu/txt_play_all_levels_text.png');
    $imgbin{txt_1pmenu_play_all_levels_outlined_text} = add_image('menu/txt_play_all_levels_outlined_text.png');
    $imgbin{txt_1pmenu_pick_start_level_text} = add_image('menu/txt_pick_start_level_text.png');
    $imgbin{txt_1pmenu_pick_start_level_outlined_text} = add_image('menu/txt_pick_start_level_outlined_text.png');
    $imgbin{txt_1pmenu_play_random_levels_text} = add_image('menu/txt_play_random_levels_text.png');
    $imgbin{txt_1pmenu_play_random_levels_outlined_text} = add_image('menu/txt_play_random_levels_outlined_text.png');
    $imgbin{txt_1pmenu_mp_train_text} = add_image('menu/txt_multiplayer_training_text.png');
    $imgbin{txt_1pmenu_mp_train_outlined_text} = add_image('menu/txt_multiplayer_training_outlined_text.png');

    #- net game setup images
    $imgbin{back_netgame} = add_image('back_netgame.png');
    $imgbin{netspot_free} = add_image('netspot.png');
    $imgbin{netspot_playing} = add_image('netspot-playing.png');
    $imgbin{netspot_insamegame} = add_image('netspot-insamegame.png');
    $imgbin{netspot_self} = [ map { add_image("netspot-self-$_.png") } qw(1 2 3 4 5 6 7 8 9 A B C D) ];

    #- hiscore
    $imgbin{back_hiscores} = add_image('back_hiscores.png');
    $imgbin{hiscore_frame} = add_image('hiscore_frame.png');
    $imgbin{hiscore_levelset} = add_image('hiscore-levelset.png');
    $imgbin{hiscore_mptraining} = add_image('hiscore-mptraining.png');
    
    local @PLAYERS = @ALL_PLAYERS;  #- load all images even if -so commandline option was passed
    iter_players {
        print_step('.'); 
	$imgbin{hurry}{$::p} = add_image("hurry_$::p.png");
	$imgbin{win}{$::p} = add_image("win_panel_$::p.png");
	$::p ne 'p2' and $imgbin{net_lose}{$::p} = add_image("net_lose_$::p.png");
        if ($::p =~ /^r/) {
            $imgbin{attack}{$::p} = add_image("attack_$::p.png");
            $imgbin{attackme}{$::p} = add_image("attackme_$::p.png");
        }
	$pdata{$::p}{score} = 0;
        my $p = $::p;
        $pinguin{$::p}{normal} = [ add_image("pinguins/anime-shooter_${p}_0020.png") ];
        $pinguin{$::p}{action} = [ map { add_image("pinguins/anime-shooter_${p}_00$_.png") } (21..50) ];
        $pinguin{$::p}{left_to} = [ map { add_image("pinguins/anime-shooter_${p}_00$_.png") } reverse ('02'..'19') ];
        $pinguin{$::p}{left} = [ add_image("pinguins/anime-shooter_${p}_0001.png") ];
        $pinguin{$::p}{left_from} = [ map { add_image("pinguins/anime-shooter_${p}_00$_.png") } ('02'..'19') ];
        $pinguin{$::p}{right_to} = [ map { add_image("pinguins/anime-shooter_${p}_00$_.png") } (51..70) ];
        $pinguin{$::p}{right} = [ add_image("pinguins/anime-shooter_${p}_0071.png") ];
        $pinguin{$::p}{right_from} = [ map { add_image("pinguins/anime-shooter_${p}_00$_.png") } reverse (51..71) ];
        $pinguin{$::p}{wait_to} = [ map { add_image("pinguins/wait_${p}_00$_.png") } ('01'..'74') ];
        $pinguin{$::p}{wait} = [ map { add_image("pinguins/wait_${p}_00$_.png") } (75..97) ];
        $pinguin{$::p}{win} = [ map { add_image("pinguins/win_${p}_00$_.png") } ('01'..'68') ];
        $pinguin{$::p}{lose_to} = [ map { add_image("pinguins/loose_${p}_00$_.png") } ('01'..'64') ];
        $pinguin{$::p}{lose} = [ map { add_image("pinguins/loose_${p}_0$_.png") } ('065'..'158') ];
    };

    print_step('] '); 

    if ($mixer eq 'SOUND_DISABLED') {
        $mixer_enabled = 0; $mixer = undef;
    } elsif (!defined($mixer_enabled) || $mixer_enabled) {
	$mixer_enabled = init_sound();
    }

    #- the RGBA effects algorithms assume little endian RGBA surfaces
    my $replace = SDL::Surface->new(-width => $imgbin{menu_logo}->width, -height => $imgbin{menu_logo}->height, -depth => 32);
    $imgbin{menu_logo}->set_alpha(0, 0);  #- for RGBA->RGBA blits, SDL_SRCALPHA must be removed or destination alpha is preserved
    $imgbin{menu_logo}->blit(undef, $replace, undef);
    $imgbin{menu_logo} = $replace;
    add_default_rect($replace);

    $lev_number = 0;
    load_levelset("$FPATH/data/levels");

    fb_c_stuff::init_effects($FPATH);
    $addicted_time = 0;

    $start_time = 0;
    print "Ready.\n";
}

sub open_level($) {
    my ($level) = @_;

    $level eq 'WON' and $level = $lev_number;

    $levels{$level} or die "No such level or void level ($level).\n";
    foreach my $l (@{$levels{$level}}) {
	iter_players {
	    my $img = $l->{img_num} =~ /^\d+$/ ? $bubbles_images[$l->{img_num}] : $bubbles_anim{lose};
	    real_stick_bubble(create_bubble_given_img($img), $l->{cx}, $l->{cy}, $::p, 0);
	};
    }
}

sub translate_joystick_tokey($) {
    my ($event) = @_;
    my $which =  SDL::JoyAxisEventWhich(evt($event));
    if ($event->type == fb_c_stuff::JOYAXISMOTION()) {
        my $axis = SDL::JoyAxisEventAxis(evt($event));
        my $value = fb_c_stuff::JoyAxisEventValue(evt($event));
        if ($value <= -32767 || $value >= 32767) {  #- theoretically, it should work properly with analog joysticks this way
            return "joystick|axisvalue|$which|$axis|$value";
        } else {
            return "joystick|axisvalue|$which|$axis|0";
        }
    } elsif ($event->type() == fb_c_stuff::JOYBUTTONDOWN()) {
        my $button = SDL::JoyButtonEventButton(evt($event)) + 1;
        return "joystick|buttondown|$which|$button";
    } elsif ($event->type() == fb_c_stuff::JOYBUTTONUP()) {
        my $button = SDL::JoyButtonEventButton(evt($event)) + 1;
        return "joystick|buttonup|$which|$button";
    }
}

sub extended_keypress($) {
    my ($event) = @_;
    if ($event->type == SDL_KEYDOWN) {
        return $event->key_sym;
    } elsif ($event->type == fb_c_stuff::JOYAXISMOTION() || $event->type() == fb_c_stuff::JOYBUTTONDOWN()) {
        my $keypressed = translate_joystick_tokey($event);
        if ($keypressed =~ /^joystick\|axisvalue\|\d+\|\d+\|0$/) {  #- we treat position at 0 as KEYUP
            $keypressed = undef;
        }
        return $keypressed;
    }
}

sub grab_key {
    my $keyp;

    do {
	$event->wait;
	if ($event->type == SDL_KEYDOWN) {
	    $keyp = $event->key_sym;
	} elsif ($event->type == fb_c_stuff::JOYAXISMOTION() || $event->type() == fb_c_stuff::JOYBUTTONDOWN()) {
	    $keyp = translate_joystick_tokey($event);
        }
    } while (!defined($keyp));

    #- so that using "capital" letter should work
    if (member($keyp, SDLK_LSHIFT(), SDLK_RSHIFT())) {
        return grab_key();
    } else {
        return $keyp;
    }
}

sub display_highscores {
    my ($type, $new_entry) = @_;

    $display_on_app_disabled = 1;
    @PLAYERS = ('p1');
    %POS = %POS_1P;

    if ($type eq 'levels' || !defined($type)) {
        $imgbin{back_hiscores}->blit($apprects{main}, $app, $apprects{main});
        put_image($imgbin{hiscore_levelset}, 640 - 10 - $imgbin{hiscore_levelset}->width, 8);

        my $initial_high_posx = 90;
        my ($high_posx, $high_posy) = ($initial_high_posx, 68);
        my $high_rect = SDL::Rect->new('-x' => $POS{p1}{left_limit} & 0xFFFFFFFC, '-y' => $POS{p1}{top_limit} & 0xFFFFFFFC,
                                       '-width' => ($POS{p1}{right_limit}-$POS{p1}{left_limit}) & 0xFFFFFFFC, -height => ($POS{p1}{'initial_bubble_y'}-$POS{p1}{top_limit}-10) & 0xFFFFFFFC);

        my $centered_print = sub($$$) {
            my ($x, $y, $txt, $bold) = @_;
            print_($bold ? 'bold_menu' : 'menu', $app, $x, $y + $imgbin{hiscore_frame}->height - 8, $txt, $imgbin{hiscore_frame}->width + 12, 'center');
        };
        
        my $old_levelset = $loaded_levelset;
        
        foreach my $high (ordered_highscores()) {
            @{$sticked_bubbles{p1}} = ();
            @{$root_bubbles{p1}} = ();
            $pdata{p1}{newrootlevel} = 0;
            $pdata{p1}{oddswap} = 0;
            $imgbin{back_1p}->blit($high_rect, $background, $high_rect);

            # try to get it from the default-levelset. If we can't, default to the
            # last level in the default levelset
            if (!$high->{piclevel}) {
                $loaded_levelset ne "$FPATH/data/levels" and load_levelset("$FPATH/data/levels");
                
                # handle the case where the user has edited/created a levelset with more levels
                # than the default levelset and then got a high score
                if ($high->{level} > $lev_number) {
                    open_level($lev_number);
                } else {
                    open_level($high->{level});
                }
            } else {
                # this is the normal case. just load the level that the file tells us
                if ($loaded_levelset ne "$FBHOME/highlevelshistory") {
                    load_levelset("$FBHOME/highlevelshistory");
                }
                open_level($high->{piclevel});
            }

            put_image($imgbin{hiscore_frame}, $high_posx - 7, $high_posy - 6);
            my $tmp = SDL::Surface->new(-width => $high_rect->width/4, -height => $high_rect->height/4,
                                        -depth => 32, -Amask => "0 but true")->display_format;
            fb_c_stuff::shrink(surf($tmp), surf($background->display_format), 0, 0, rect($high_rect), 4);
            $tmp->blit(undef, $app, SDL::Rect->new(-x => $high_posx, '-y' => $high_posy));
            $centered_print->($high_posx - 15, $high_posy, $high->{name}, $new_entry == $high);
            $centered_print->($high_posx - 15, $high_posy + 20, $high->{level} eq 'WON' ? t("won!") : t("level %s", i18n_number($high->{level})), $new_entry == $high);
            my $min = int($high->{time}/60);
            my $sec = int($high->{time} - $min*60); length($sec) == 1 and $sec = "0$sec";
            $centered_print->($high_posx - 15, $high_posy + 40, t("%s'%s\"", i18n_number($min), i18n_number($sec)), $new_entry == $high);
            $high_posx += 98;
            $high_posx > 550 and $high_posx = $initial_high_posx, $high_posy += 175;
            $high_posy > 440 and last;
        }
        load_levelset($old_levelset);    

        $app->flip;
        
        $event->pump while $event->poll != 0;
        grab_key() eq SDLK_ESCAPE() and return;
    }

    if (($type eq 'mptrain' || !defined($type)) && (@$HISCORES_MPTRAIN || @$HISCORES_MPTRAIN_CHAINREACTION)) {
        $imgbin{back_hiscores}->blit($apprects{main}, $app, $apprects{main});
        put_image($imgbin{hiscore_mptraining}, 640 - 10 - $imgbin{hiscore_mptraining}->width, 8);

        print_('menu', $app, 0, 50, t("Regular"), 320, 'center');
        my %parts = (rank => { xpos => 80, width => 40 },
                     name => { xpos => 120, width => 150 });
        if ($is_rtl) {
            $parts{$_}{xpos} = 640 - $parts{$_}{xpos} - $parts{$_}{width} foreach keys %parts;
        }
        my $y = 80;
        my $counter = 1;
        my $lastvalue;
        foreach my $high (ordered_mptrain_highscores()) {
            my $font = $new_entry == $high ? 'bold_menu' : 'menu';
            $high->{score} ne $lastvalue and print_($font, $app, $parts{rank}{xpos}, $y, "$counter. ", $parts{rank}{width}, 'right');
            print_($font, $app, $parts{name}{xpos}, $y, "$high->{name}: $high->{score}", $parts{name}{width});
            $y += $smg_lineheight + 2;
            $counter++;
            $lastvalue = $high->{score};
        }

        print_('menu', $app, 320, 50, t("Chain-reaction enabled"), 320, 'center');
        %parts = (rank => { xpos => 420, width => 40 },
                  name => { xpos => 460, width => 150 });
        if ($is_rtl) {
            $parts{$_}{xpos} = 640 - $parts{$_}{xpos} - $parts{$_}{width} foreach keys %parts;
        }
        $y = 80;
        $counter = 1;
        $lastvalue = undef;
        foreach my $high (ordered_mptrain_highscores_chainreaction()) {
            my $font = $new_entry == $high ? 'bold_menu' : 'menu';
            $high->{score} ne $lastvalue and print_($font, $app, $parts{rank}{xpos}, $y, "$counter. ", $parts{rank}{width}, 'right');
            print_($font, $app, $parts{name}{xpos}, $y, "$high->{name}: $high->{score}", $parts{name}{width});
            $y += $smg_lineheight + 2;
            $counter++;
            $lastvalue = $high->{score};
        }

        $app->flip;
        
        $event->pump while $event->poll != 0;
        grab_key() eq SDLK_ESCAPE() and die "quit";
    }

    $display_on_app_disabled = 0;
}

sub keysym_to_char($) {
    my ($key) = @_;
    eval("$key eq SDLK_$_()") and return uc($_) foreach @fbsyms::syms;
    if ($key >= 160 && $key <= 255) {
        return 'WORLD_' . ($key - 160);  #- "world" keys are not exported
    }
}

sub ask_from($) {
    my ($w) = @_;
    # $w->{intro} = [ 'text_intro_line1', 'text_intro_line2', ... ]
    # $w->{entries} = [ { q => 'question1?', a => \$var_answer1, f => 'flags' }, {...} ]   flags: ONE_CHAR, SPACE
    # $w->{outro} = 'text_outro_uniline'
    # $w->{erase_background} = $background_right_one

    put_image($imgbin{void_panel}, $MENUPOS{xpos_panel}, $MENUPOS{ypos_panel});

    my $ypos = $MENUPOS{ypos_panel} + 12;
    my $lineheight = 18;
    my %xpos = ( questions => $is_rtl ? $MENUPOS{xpos_panel} + $imgbin{void_panel}->width*1/3 + 10 : $MENUPOS{xpos_panel},
                 echo => $is_rtl ? $MENUPOS{xpos_panel} + $imgbin{void_panel}->width*1/3 - 100 : $MENUPOS{xpos_panel} + $imgbin{void_panel}->width*2/3 );
 
    foreach my $i (@{$w->{intro}}) {
	if ($i) {
	    print_('menu', $app, $MENUPOS{xpos_panel}, $ypos, $i, $imgbin{void_panel}->width, 'center');
	}
	$ypos += $lineheight;
    }

    $ypos += 10;

    my $ok = 1;
    $event->set_key_repeat(200, 50);
  ask_from_entries:
    foreach my $entry (@{$w->{entries}}) {
        #- in case wrapping occurred
        $imgbin{void_panel}->blit(SDL::Rect->new(-width => $imgbin{void_panel}->width - 20, -height => 30, -x => 10, '-y' => $ypos - $MENUPOS{ypos_panel}), $app,
                                  SDL::Rect->new(-x => $MENUPOS{xpos_panel} + 10, '-y' => $ypos));
	print_('menu', $app, $xpos{questions}, $ypos, $entry->{'q'}, $imgbin{void_panel}->width*2/3 - 10, @{$w->{entries}} == 1 ? 'center' : 'right');
	$app->flip;
	my $srect_mulchar_redraw = SDL::Rect->new(-width => $imgbin{void_panel}->width*1/3, -height => 30,
                                                  -x => $xpos{echo} - 3 - $MENUPOS{xpos_panel}, '-y' => $ypos - $MENUPOS{ypos_panel});
	my $drect_mulchar_redraw = SDL::Rect->new(-x => $xpos{echo} - 3, '-y' => $ypos);
        my $x_echo = $xpos{echo};
	my $txt;
        if ($entry->{f} ne 'SPACE') {
            if ($entry->{f} eq 'ONE_CHAR') {
                my $k;
                while (!defined($k)) {
                    $k = grab_key();
                    if ($k =~ /^joystick\|axisvalue\|\d+\|\d+\|0$/) {  #- we treat position at 0 as KEYUP
                        $k = undef;
                    }
                }
                $no_echo or play_sound('typewriter');
                $k == SDLK_ESCAPE and $ok = 0, last ask_from_entries;
                $txt = $k;
                if ($k =~ /^joystick\|axisvalue\|(\d+)\|(\d+)\|([-\d]+)/) {
                    my $num = @joysticks > 1 ? $1 + 1 : '';
                    $k = 'joy' . i18n_number($num) . '-' . (even($2) ? ($3 < 0 ? t("left") : t("right")) : ($3 < 0 ? t("up") : t("down")));
                } elsif ($k =~ /^joystick\|buttondown\|(\d+)\|(\d+)/) {
                    my $num = @joysticks > 1 ? $1 + 1 : '';
                    my $button = $2 + 1;
                    $k = 'joy' . i18n_number($num) . '-' . t("button") . i18n_number($2);
                } else {
                    $k = keysym_to_char($k);
                }
                print_('menu', $app, $x_echo, $ypos, $k, 100, $is_rtl ? 'right' : 'left');  #- always in ASCII so need to tell it to go right in RTL
            } else {
                callback_entry('reset');
              ask_from_main_loop:
                while (1) {
                    if (callback_entry('ping')) {
                        $imgbin{void_panel}->blit($srect_mulchar_redraw, $app, $drect_mulchar_redraw);
                        callback_entry('print', { xpos => $x_echo, ypos => $ypos, maxlen => 100, font => 'menu' });
                    }
                    $app->flip;
                    $event->pump;
                    while ($event->poll != 0) {
                        if ($event->type == SDL_KEYDOWN) {
                            my $k = $event->key_sym;
                            if ($k == SDLK_ESCAPE()) {
                                $ok = 0;
                                last ask_from_entries;
                            } elsif ($k == SDLK_RETURN() || $k == SDLK_KP_ENTER()) {
                                $txt = join('', callback_entry('gettext'));
                                last ask_from_main_loop;
                            } else {
                                callback_entry('keypressed', { event => $event, maxlen => 100, font => 'menu' });
                                $imgbin{void_panel}->blit($srect_mulchar_redraw, $app, $drect_mulchar_redraw);
                                callback_entry('moved');
                                callback_entry('print', { xpos => $x_echo, ypos => $ypos, maxlen => 100, font => 'menu' });
                            }
                        }
                    }
                    fb_c_stuff::fbdelay($TARGET_ANIM_SPEED);
                }
            }
	}
	$entry->{answer} = $txt;
	$ypos += $entry->{f} eq 'SPACE' ? $lineheight / 2 : $lineheight;
    }
    $event->set_key_repeat(0, 0);

    if ($ok) {
	${$_->{a}} = $_->{answer} foreach @{$w->{entries}};
        print_('menu', $app, $MENUPOS{xpos_panel}, $MENUPOS{ypos_panel} + $imgbin{void_panel}->height - 35, $w->{outro}, $imgbin{void_panel}->width, 'center');
	$app->flip;
	play_sound('menu_selected');
	sleep 2;
    } else {
	play_sound('cancel');
    }

    exists $w->{erase_background} and erase_image_from($imgbin{void_panel}, $MENUPOS{xpos_panel}, $MENUPOS{ypos_panel}, $w->{erase_background});
    $app->flip;
    $event->pump while $event->poll != 0;

    return $ok;
}

sub new_game() {

    my $ticks = $app->ticks;
    $display_on_app_disabled = 1;

    $TIME_APPEARS_NEW_ROOT = 11;
    $TIME_HURRY_WARN = 250;
    $TIME_HURRY_MAX = 375;

    #- reset chat messages from last game, if any
    $pdata{current_chat_messages}{$_} = undef foreach @ALL_PLAYERS;

    my $backgr;
    if (is_mp_game()) {
        $pdata{p1}{chatting} and cleanup_chatting();
        if (@PLAYERS == 2) {  #- in net/lan 2p mode, use bigger graphics and positions
            $backgr = $imgbin{back_2p};
            %POS = %POS_2P;
        } else {
            $backgr = $imgbin{back_mp};
            %POS = %POS_MP;
        }
        $pdata{inconsistency} = undef;
    } elsif (is_2p_game()) {
	$backgr = $imgbin{back_2p};
	%POS = %POS_2P;
    } elsif (is_1p_game()) {
	$backgr = $imgbin{back_1p};
	%POS = %POS_1P;
        if ($levels{current} eq 'mp_train') {
            $pdata{$PLAYERS[0]}{score} = 0;
        } else {
            if ($levels{current} ne 'random') {
                $chainreaction = 0;
            }
            $TIME_APPEARS_NEW_ROOT = 8;
            $TIME_HURRY_WARN = 400;
            $TIME_HURRY_MAX = 525;
            $pdata{$PLAYERS[0]}{score} = $levels{current};
        }
    } else {
	die "oops";
    }

    $backgr->blit($apprects{main}, $background_orig, $apprects{main});
    if ($levels{current} eq 'mp_train') {
	my $drect = SDL::Rect->new(-x => 32, '-y' => 152);
	$imgbin{void_mp_training}->blit($rects{$imgbin{void_mp_training}}, $background_orig, $drect);
    }
    $background_orig->blit($apprects{main}, $background, $apprects{main});

    iter_players {
	$actions{$::p}{$_} = 0 foreach qw(left right fire center);
	$angle{$::p} = $PI/2;
	@{$sticked_bubbles{$::p}} = ();
	@{$malus_bubble{$::p}} = ();
	@{$root_bubbles{$::p}} = ();
	@{$falling_bubble{$::p}} = ();
	@{$exploding_bubble{$::p}} = ();
        delete $pdata{$::p}{nextcolors};
	@{$chains{$::p}{falling_chained}} = ();
	%{$chains{$::p}{chained_bubbles}} = ();
	$launched_bubble{$::p} = undef;
	$sticking_bubble{$::p} = undef;
	$pdata{$::p}{$_} = 0 foreach qw(newroot newroot_prelight oddswap hurry newrootlevel);
	@{$pdata{$::p}{malus}} = ();
        $pdata{$::p}{state} = 'ingame';
	$pdata{$::p}{ping_right}{img} = 0;
	$pdata{$::p}{ping_right}{state} = 'normal';
        $pdata{$::p}{hurry_save_img} = undef;
	$apprects{$::p} = SDL::Rect->new('-x' => $POS{$::p}{left_limit}, '-y' => $POS{$::p}{top_limit},
					 -width => $POS{$::p}{right_limit}-$POS{$::p}{left_limit}, -height => $POS{$::p}{'initial_bubble_y'}-$POS{$::p}{top_limit});
        if (is_distant_player($::p)) {
            if (!$pdata{$::p}{left}) {
                $pdata{$::p}{still_game_messages} = 1;
                $pdata{$::p}{ready4newgame} = 0;
            }
        }
    };

    print_scores($background);
    if ($levels{current} eq 'mp_train') {
        mp_train_print_time();
    }

    iter_players {
        delete $pdata{$::p}{newrootlast};
        if ($pdata{$::p}{left}) {
            #- already left remote players in mp game
            put_image_to_background(mini_graphics($::p) ? $imgbin{"left_".$::p."_mini"} : $imgbin{left_rp1},
                                    $POS{$::p}{left}{x}, $POS{$::p}{left}{y}); #}}
            $pdata{$::p}{state} = 'left';
        } else {
            handle_progress($::p);
        }
    };

    is_1p_game() and print_compressor();

    if (!$playdata) {
        %recorddata = ();
        my $srand = $app->ticks;
        srand $srand;
        push @{$recorddata{data}}, { srand => $srand };  #- the first record entry is used for pdatas (more keys added later)
    }
    if ($levels{current} =~ /^\d+$/) {
	open_level($levels{current});
    } else {
	foreach my $cy (0 .. 4) {
	    foreach my $cx (0 .. (6 + even($cy))) {
                my $num = int(rand(@bubbles_images));
                if (is_mp_game()) {
                    if (!$playdata) {
                        check_mp_connection();
                        eval { $num = mp_propagate("b|$cx|$cy", $num, \$ticks); };
                        if ($@) {
                            $@ =~ /^quit/ and return 0;
                            die;
                        }
                        push @{$recorddata{data}[0]{bubbles}}, $num;
                    } else {
                        $num = shift @{$recorddata{pdatas}{bubbles}};
                    }
                }
                my $b = create_bubble_given_img_num($num);
                real_stick_bubble($b, $cx, $cy, $PLAYERS[0], 0);
                if (!is_1p_game()) {
                    iter_players_but_first {
                        $pdata{$::p}{left} or real_stick_bubble(create_bubble_given_img($b->{img}), $cx, $cy, $::p, 0);
                    };
                }
	    }
	}
    }

    my ($next_num, $tobe_num);
    do { $next_num = int(rand(@bubbles_images)) } while (!validate_nextcolor($next_num, $PLAYERS[0]));
    do { $tobe_num = int(rand(@bubbles_images)) } while (!validate_nextcolor($tobe_num, $PLAYERS[0]));
    if (is_mp_game()) {
        if (!$playdata) {
            check_mp_connection();
            eval {
                $next_num = mp_propagate("N", $next_num, \$ticks);
                $tobe_num = mp_propagate("T", $tobe_num, \$ticks);
            };
            if ($@) {
                $@ =~ /^quit/ and return 0;
                die;
            }
            push @{$recorddata{data}[0]{bubbles}}, $next_num, $tobe_num;
        } else {
            $next_num = shift @{$recorddata{pdatas}{bubbles}};
            $tobe_num = shift @{$recorddata{pdatas}{bubbles}};
        }
    }
    $next_bubble{$PLAYERS[0]} = create_bubble_given_img_num($next_num);
    generate_new_bubble($PLAYERS[0], $tobe_num);
    if (!is_1p_game()) {
        iter_players_but_first {
            if (!$pdata{$::p}{left}) {
                $next_bubble{$::p} = create_bubble_given_img_num($next_num);
                generate_new_bubble($::p, $tobe_num);
            }
        };
    }

    if ($graphics_level == 1) {
	$background->blit($apprects{main}, $app, $apprects{main});
	$app->flip;
    } else {
	fb_c_stuff::effect(surf($app), surf($background->display_format));
    }

    $display_on_app_disabled = 0;

    $event->pump while $event->poll != 0;
    $pdata{state} = 'game';

    $direct = undef;

    if (is_mp_game() && !$playdata) {
        mp_ping_if_needed(\$ticks);

        #- 1. first wait on a common barrier with others, so that everyone has finished previous things
        dbgnet("send barrier 'n'");
        fb_net::gsend('n');
        check_mp_connection();
        iter_distant_players {
            $pdata{$::p}{barrier4newgame} = 0;
        };
        
        #- at this point, we can receive first commands of quickies instead of synchro
        my @keep_messages;
        while (1) {
            my $m;
            eval { $m = fb_net::grecv_get1msg(); };  #- blocking
            if ($@) {
                $@ =~ /^quit/ and return 0;
                die;
            }
            mp_ping_if_needed(\$ticks);
            if ($m->{msg} eq 'n') {
                dbgnet("received barrier 'n' from $pdata{id2p}{$m->{id}}");
                $pdata{$pdata{id2p}{$m->{id}}}{barrier4newgame} = 1;
                iter_distant_players {
                    !$pdata{$::p}{left} && $pdata{$::p}{barrier4newgame} == 0 and goto still_waiting;
                };
                dbgnet("all barriers received, carrying on");
                last;
              still_waiting:
            } else {
                push @keep_messages, $m;
            }
        }

        #- 2. now that we're all ready to synchronize, let a unique synchro message be sent, the leader and others will all wait on
        is_leader() and fb_net::gsend('!');
        check_mp_connection();
        
        while (1) {
            my $m;
            eval { $m = fb_net::grecv_get1msg(); }; #- blocking
            if ($@) {
                $@ =~ /^quit/ and return 0;
                die;
            }
            mp_ping_if_needed(\$ticks);
            if ($m->{msg} eq '!') {
                last;
            } else {
                push @keep_messages, $m;
            }
        }
        
        fb_net::gdelay_messages(@keep_messages);

        @{$pdata{attackingme}} = ();
    }

    if ($saveframes) {
        $saveframescounter = 0;
        $saveframesbase = gettimeofday();
    }

    $start_time = $app->ticks;
    return 1;
}

sub choose_1p_game_mode() {

    my @ordered_names = qw(play_all_levels pick_start_level play_random_levels mp_train);
    my $active_menu = 0;

    my $menu_xpos = $MENUPOS{xpos_panel} + ($imgbin{'1p_panel'}->width - $imgbin{txt_1pmenu_over}->width)/2; 
    my $menu_ypos = $MENUPOS{ypos_panel} + 90;
    my $menu_ypos_spacer = $imgbin{txt_1pmenu_over}->height + 4;
    my %name2ypos = (play_all_levels    => $menu_ypos,
                     pick_start_level   => $menu_ypos +     $menu_ypos_spacer,
                     play_random_levels => $menu_ypos + 2 * $menu_ypos_spacer,
                     mp_train           => $menu_ypos + 3 * $menu_ypos_spacer);

    my $overlook_index = 0;
    my $overlook = SDL::Surface->new(-width => $imgbin{txt_1pmenu_pick_start_level_text}->width,
                                     -height => $imgbin{txt_1pmenu_pick_start_level_text}->height,
                                     -depth => 32);

    my $redraw = sub {
        my $draw_element = sub {
            my ($name, $mode) = @_;
            put_image($imgbin{"txt_1pmenu_$mode"}, $menu_xpos, $name2ypos{$name});
            $imgbin{"txt_1pmenu_${name}_outlined_text"}->blit(undef, $app, my $rect = SDL::Rect->new(-x => $menu_xpos, '-y' => $name2ypos{$name}));
        };

        my $img = $imgbin{'1p_panel'};
        state $save;
        my $drect = SDL::Rect->new(-width => $img->width, -height => $img->height,
                                   -x => $MENUPOS{xpos_panel}, '-y' => $MENUPOS{ypos_panel});
        if ($save) {
            $save->blit($rects{img}, $app, $drect);
        } else {
            $save = SDL::Surface->new(-width => $img->width, -height => $img->height,
                                      -depth => 32, -Amask => "0 but true");
            $app->blit($drect, $save, $rects{$img});
        }
        put_image($img, $MENUPOS{xpos_panel}, $MENUPOS{ypos_panel});

        my $title = t("Start 1-player game menu");
        my $ypos = $MENUPOS{ypos_panel} + 16;
        print_('menu', $app, $MENUPOS{xpos_panel}, $ypos, $title, $imgbin{'1p_panel'}->width, 'center');

        foreach (@ordered_names) {
            $_ ne $ordered_names[$active_menu] and $draw_element->($_, 'off');
        }

        $draw_element->($ordered_names[$active_menu], 'over');

        fb_c_stuff::overlook_init(surf($overlook));
        $overlook_index = 0;

        $app->flip();
    };

    my $draw_overlook = sub {
        my %name2pivot = (play_all_levels => 90, pick_start_level => 135, play_random_levels => 82, mp_train => 105);
        my $name =  $ordered_names[$active_menu];
        put_image($imgbin{txt_1pmenu_over}, $menu_xpos, $name2ypos{$name});
        fb_c_stuff::overlook(surf($overlook), surf($imgbin{"txt_1pmenu_${name}_text"}), $overlook_index, $name2pivot{$name});
        $overlook->blit(undef, $app, my $rect = SDL::Rect->new(-x => $menu_xpos, '-y' => $name2ypos{$name}));
        $overlook_index++;
        if ($overlook_index == 70) {
            $overlook_index = 0;
        }
        $imgbin{"txt_1pmenu_${name}_outlined_text"}->blit(undef, $app, my $rect = SDL::Rect->new(-x => $menu_xpos, '-y' => $name2ypos{$name}));
    };

    $redraw->();

    while (1) {
	my $synchro_ticks = $app->ticks;
        if ($graphics_level > 1) {
            $draw_overlook->();
            $app->update(@update_rects);
            @update_rects = ();
        }
        $event->pump;
        while ($event->poll != 0) {
            if ($event->type == SDL_KEYDOWN) {
                my $k = $event->key_sym;
                if ($k == SDLK_RETURN() || $k == SDLK_KP_ENTER()) {
                    my $cancel;
                    if ($ordered_names[$active_menu] eq 'pick_start_level') {
                        if ($levels{current}) {
                            choose_levelset(1) or $cancel = 1;
                        }
                    } elsif ($ordered_names[$active_menu] eq 'play_random_levels') {
                        $levels{current} = 'random';
                        my $answ;
                        ask_from({ intro => [ t("Random level"), '', '', t("Enable chain-reaction?"), '' ],
                                   entries => [ { 'q' => t("%s or %s?", 'Y', 'N'), 'a' => \$answ, f => 'ONE_CHAR' } ],
                                   outro => t("Enjoy the game!") }) or return;
                        $chainreaction = $answ == SDLK_y; #;;
                    } elsif ($ordered_names[$active_menu] eq 'mp_train') {
                        $levels{current} = 'mp_train';
                        my $answ;
                        ask_from({ intro => [ t("Multiplayer training"), '', '', t("Enable chain-reaction?"), '' ],
                                   entries => [ { 'q' => t("%s or %s?", 'Y', 'N'), 'a' => \$answ, f => 'ONE_CHAR' } ],
                                   outro => t("Enjoy the game!") }) or return;
                        $chainreaction = $answ == SDLK_y; #;;
                    }
                    $cancel or return 1;
                } elsif ($event->key_sym == SDLK_ESCAPE) {
                    $levels{current} = undef;
                    return;
                } elsif ($k == SDLK_DOWN()) {
                    if ($active_menu < @ordered_names - 1) {
                        $active_menu++;
                    } else {
                        $active_menu = 0;
                    }
                    play_sound('menu_change');
                } elsif ($k == SDLK_UP()) {
                    if ($active_menu > 0) {
                        $active_menu--;
                    } else {
                        $active_menu = @ordered_names - 1;
                    }
                    play_sound('menu_change');
                }
                $redraw->();
            }
        }
        my $to_wait = $TARGET_ANIM_SPEED - ($app->ticks - $synchro_ticks);
        $to_wait > 0 and fb_c_stuff::fbdelay($to_wait);
    }
}


our $smg_startx = 78;
our $smg_starty = 30;
our $smg_starty_chat = 320;
our $smg_starty_players = $smg_starty_chat + $smg_lineheight;
our $smg_max_messages = 5;
our $smg_statusx = 10;
our $smg_statusy = 435;

sub erase_line($$;$$) {
    my ($pos, $background, $xpos, $width) = @_;
    my $drect = SDL::Rect->new(-width => $width || 640, -height => $smg_lineheight + 6, -x => $xpos || 0, '-y' => $pos);
    $background->blit($drect, $app, $drect);
}

our @smg_status_messages;
our $smg_status_message_offsetpage = 1;

sub smg_printstatus {
    my $drect = SDL::Rect->new(-x => 0, '-y' => $smg_statusy - $smg_max_messages * $smg_lineheight, -width => 640, -height => 480);
    $imgbin{back_netgame}->blit($drect, $app, $drect);
    my $y = $smg_statusy;
    my $i = $smg_status_message_offsetpage;
    while ($i < $smg_max_messages + $smg_status_message_offsetpage && @smg_status_messages >= $i) {
        if ($smg_status_messages[-$i]) {
            my $kind = $smg_status_messages[-$i] =~ /^\*\*\*/ ? 'netdialogs_servermsg' : 'netdialogs';
            if ($smg_status_messages[-$i] =~ /^‫/) {
                #- if text begins with unicode RTL direction, we also need to tell pango to align on the right
                #- (is there another unicode special character which does both?)
                print_($kind, $app, $smg_statusx, $y, $smg_status_messages[-$i], 620, 'right');
            } else {
                print_($kind, $app, $smg_statusx, $y, $smg_status_messages[-$i], 620, 'left');
            }
        }
        $y -= $smg_lineheight;
        $i++;
    }
    $app->flip();
}

sub smg_add_status_msg {
    push @smg_status_messages, @_;
    $smg_status_message_offsetpage = 1;
    smg_printstatus();
}

sub clean_server {
    if ($pdata{serverpid}) {
        kill 15, $pdata{serverpid};
        waitpid $pdata{serverpid}, 0;
        $pdata{serverpid} = undef;
    }
}

END { clean_server(); }

sub sanitize_nick {
    my ($nick) = @_;
    $nick = substr($nick, 0, 10);
    $nick =~ s/[^a-zA-Z0-9_-]//g;
    return $nick;
}

sub smg_servers() {

    if ($pdata{gametype} eq 'lan') {
        smg_add_status_msg(t("*** Please wait, probing for available servers on local network..."));
        my $ret = fb_net::discover_lan_servers();
        if ($ret->{failure}) {
            smg_add_status_msg(t("*** Unable to probe for available servers on local network!"),
                               "*** " . $ret->{failure},
                               t("*** Verify your network setup"));
            grab_key();
            return;
        } else {
            my @servers = @{$ret->{servers}};
            if (!@servers) {
                my $fb_server = "$FLPATH/fb-server";
                if (!-x $fb_server) {
                    print STDERR "$fb_server is missing or not executable!\n";
                    smg_add_status_msg(t("*** No server found, and could not start server"),
                                       t("*** Verify your installation or contact your vendor"));
                    grab_key();
                    return;
                } else {
                    if (my $pid = fork()) {
                        $pdata{autochooseserver} = 1;
                        $pdata{serverpid} = $pid;
                        smg_add_status_msg(t("*** No server found, created server for local network game"));
                        sleep 1;
                        return [ { host => 'localhost', port => 1511 } ];
                    } else {
                        unless (exec $fb_server, '-L', '-d', '-n', substr("lan-$mynick", 0, 12), '-z') {
                            print STDERR "Could not create server limited to lan game: $!\n";
                            POSIX::_exit(1);
                        }
                    }
                }
            } else {
                return \@servers;
            }
        }

    } else {
        smg_add_status_msg(t("*** Contacting master server..."));
        my $serverlist = fb_net::get_server_list();
        my @servers;
        if (defined $serverlist) {
            foreach my $line (split /\n/, $serverlist) {
                if ($line =~ /^(\S+) (\S+)$/) {
                    push @servers, { host => $1, port => $2 };
                } else {
                    print STDERR "Unrecognized line in serverlist:\n\t$line\n";
                }
            }
            smg_add_status_msg(t("*** Server list received properly"));
        } else {
            smg_add_status_msg(t("*** Unable to download server list from master server!"),
                               t("*** Verify your network setup or retry later"));
            grab_key();
            return;
        }
        return \@servers;
    }
}

our $forget_because_kicked;
sub smg_choose_server(@) {
    my (@servers) = @_;
    my $max_lines = 18;
    erase_line($smg_starty_chat, $imgbin{back_netgame});     #- if we return from choose_game
    erase_line($smg_starty_players, $imgbin{back_netgame});  #-

    if ($pdata{autochooseserver}) {
        $pdata{autochooseserver} = 0;
        fb_net::connect($servers[0]{host}, $servers[0]{port});
        return fb_net::isconnected();
    }

    my @sorted_servers;
    my $offset = 0;
    my $redraw = sub {
        my $drect = SDL::Rect->new(-width => 640, -height => $smg_statusy-$smg_lineheight*5, -x => 0, '-y' => 0);
        $imgbin{back_netgame}->blit($drect, $app, $drect);

        my %parts = (flag =>    { xpos => $smg_startx,                    width => 30 },
                     name =>    { xpos => $smg_startx + 30 + 4,           width => 120 },
                     details => { xpos => $smg_startx + 30 + 4 + 120 + 4, width => 344 });
        if ($is_rtl) {
            $parts{$_}{xpos} = 640 - $parts{$_}{xpos} - $parts{$_}{width} foreach keys %parts;
        }
        my $y = $smg_starty;
        my @show_servers = @sorted_servers;
        my $ofs = @show_servers - $max_lines;
        $ofs = 0 if $ofs < 0;
        $offset = $ofs if $offset > $ofs;
        splice(@show_servers, 0, $offset) if $offset;
        splice(@show_servers, 18) if $ofs-$offset;
        foreach my $server (@show_servers) {
            if ($server->{selected}) {
                put_image($imgbin{highlight_server}, 6, $y - 1);
            }
            exists $imgbin{flag}{$server->{language}} and put_image($imgbin{flag}{$server->{language}}, $parts{flag}{xpos}, $y);
            print_('netdialogs', $app, $parts{name}{xpos}, $y, $server->{name}, $parts{name}{width}, $is_rtl ? 'right' : 'left');  #- ASCII
            my $details = t("Available players: %s (playing: %s) Ping: %sms", i18n_number($server->{free}), i18n_number($server->{playing}), i18n_number($server->{ping}));
            print_('netdialogs', $app, $parts{details}{xpos}, $y, $details, $parts{details}{width});
            my $pingimg = $server->{ping} < 80 ? "ping_low" : $server->{ping} < 200 ? "ping_mid" : "ping_high";
            put_image($imgbin{$pingimg}, $is_rtl ? $parts{details}{xpos} + $parts{details}{width} - width('netdialogs', $details) - 17
                                                 : $parts{details}{xpos} + width('netdialogs', $details), $y);
                                                     
            $y += $smg_lineheight + 2;
        }
        $app->flip();
    };

    my @potential_servers;
    foreach my $server (@servers) {
        if (!$server->{disabled}) {
            push @potential_servers, $server;
        }
    }
    my $discover = fb_net_discover->new(@potential_servers);
    my $looped = 0;
    my $init = 0;
    while (1) {
        if ($discover->pending) {
            $discover->work(0.1); #- do networking stuff for 100ms
            $looped++;
            my @found_servers = $discover->found;
            my $weightfunc = sub { my $base = $_[0]->{free} - $_[0]->{ping}/50; $_[0]->{playing} < 100 ? $base + $_[0]->{playing}/3 : $base - $_[0]->{playing}/3; };
            @sorted_servers = sort { $weightfunc->($b) <=> $weightfunc->($a) } @found_servers;
            $redraw->();
            #- wait a bit or user confusion will be large (jumping cursor)
            if ($looped >= 10 && @sorted_servers && !$init) {
                $init = 1;
                $sorted_servers[0]->{selected} = 1;
                smg_add_status_msg(t("*** Please choose a server"));
                $redraw->();
            }
        } elsif (!$init) {
            $init = 1;
            if (@sorted_servers) {
                $sorted_servers[0]->{selected} = 1;
                smg_add_status_msg(t("*** Please choose a server"));
                $redraw->();
            } else {
                #- no server discovered. empty servers list so that below code will properly report no servers found.
                @servers = ();
                last;
            }
        }
        $event->pump;
        while ($event->poll != 0) {
            if ($event->type == SDL_QUIT) {
                cleanup_and_exit();
            } elsif ($init) {
                my $k = extended_keypress($event);
                if ($k) {
                    if ($k eq SDLK_ESCAPE()) {
                        return 0;

                    } elsif ($k eq SDLK_DOWN()) {
                        if (@sorted_servers) {
                            each_index {
                                if ($sorted_servers[$::i]->{selected}
                                    && $::i < @sorted_servers - 1
                                    && !$sorted_servers[$::i+1]->{disabled}) {
                                    $sorted_servers[$::i]->{selected} = 0;
                                    $sorted_servers[$::i+1]->{selected} = 1;
                                    $offset++ if $::i + 1 >= $offset + $max_lines;
                                    play_sound('menu_change');
                                    goto done;
                                }
                            } @sorted_servers;
                          done:
                        }

                    } elsif ($k eq SDLK_UP()) {
                        if (@sorted_servers && !$sorted_servers[0]->{selected}) {
                            each_index {
                                if ($sorted_servers[$::i]->{selected}) {
                                    $sorted_servers[$::i]->{selected} = 0;
                                    $sorted_servers[$::i-1]->{selected} = 1;
                                    $offset = $::i - 1 if $::i - 1 < $offset;
                                    play_sound('menu_change');
                                }
                            } @sorted_servers;
                        }

                    } elsif ($k eq SDLK_RETURN() || $k eq SDLK_KP_ENTER()) {
                        play_sound('menu_selected');
                        goto ok_smg_choose_server;

                    } else {
                        handle_whenever_events($k);
                    }
                }
                $redraw->();
            }
        }
        fb_c_stuff::fbdelay($TARGET_ANIM_SPEED) if $init;
    }

    if (@servers == 0 || every { $_->{disabled} } @servers) {
        smg_add_status_msg(t("*** No available game server"),
                           t("*** Please retry later or try a local network game (lan game)"));
        grab_key();
        return 0;
    }

  ok_smg_choose_server:
    foreach my $server (@servers) {
        if ($server->{selected}) {
            fb_net::connect($server);
            if (!fb_net::isconnected()) {
                smg_add_status_msg(t("*** Impossible to connect to specified server, going back to server list"));
                $server->{selected} = 0;
                return smg_choose_server(@servers);
            }
            smg_add_status_msg(t("*** Connected to server '%s'", $server->{name}));
            print "Notice! next time you start Frozen-Bubble, you may add the commandline parameter\n".
                  "        -gs $server->{host}:$server->{port} to automatically select this game server\n".
                  "        and save time not listing all available servers\n";
            last;
        }
    }

    my $y = $smg_starty;
    foreach (@servers) {
        erase_line($y, $imgbin{back_netgame});
        $y += $smg_lineheight + 2;
    }

    return 1;
}

sub smg_verify_command($;$) {
    my ($command, $rest) = @_;
    my $answer;
    eval {
        $answer = fb_net::send_and_receive($command, $rest);
    };
    if ($@) {
        smg_add_status_msg(t("*** Sorry, your computer or the network is too slow, giving up - press any key"));
        $app->flip;
        $event->pump while $event->poll != 0;
        grab_key();
        die 'quit';
    }
    if ($answer ne 'OK') {
        return $answer;
    } else {
        return;
    }
}

our (@entry_typed, $entry_position, $entry_echo_blink_counter);
sub callback_entry {
    my ($action, $params, @rest) = @_;
    if ($action eq 'reset') {
        @entry_typed = ();
        $entry_position = 0;
        $entry_echo_blink_counter = 51;
    }
    if ($action eq 'moved') {
        $entry_echo_blink_counter = 75;
    }
    if ($action eq 'keypressed') {
        if ($params->{event}->key_sym == SDLK_BACKSPACE()) {
            if ($entry_position >= 1) {
                splice @entry_typed, $entry_position - 1, 1;
                $entry_position--;
                $no_echo or play_sound('typewriter');
            } else {
                play_sound('stick');
            }
        } elsif ($params->{event}->key_sym == SDLK_DELETE()) {
            if ($entry_position < $#entry_typed + 1) {
                splice @entry_typed, $entry_position, 1;
                $no_echo or play_sound('typewriter');
            } else {
                play_sound('stick');
            }
        } elsif ($params->{event}->key_sym == SDLK_LEFT()) {
            if ($entry_position >= 1) {
                $entry_position--;
                $no_echo or play_sound('typewriter');
            } else {
                play_sound('stick');
            }
        } elsif ($params->{event}->key_sym == SDLK_RIGHT()) {
            if ($entry_position <= $#entry_typed) {
                $entry_position++;
                $no_echo or play_sound('typewriter');
            } else {
                play_sound('stick');
            }
        } elsif ($params->{event}->key_sym == SDLK_HOME()) {
            $entry_position = 0;
            $no_echo or play_sound('typewriter');
        } elsif ($params->{event}->key_sym == SDLK_END()) {
            $entry_position = $#entry_typed + 1;
            $no_echo or play_sound('typewriter');
        } elsif ($params->{event}->key_sym == SDLK_TAB()) {
            if (my $completion = $params->{completion}) {
                @entry_typed = $completion->(@entry_typed);
                $entry_position = $#entry_typed + 1;
            }
        } else {
            my $utf8char = fb_c_stuff::utf8key(evt($params->{event}));
            if ($utf8char ne '' && $utf8char ne "\n" && $utf8char ne "\r") {
                splice @entry_typed, $entry_position, 0, $utf8char;
                if (width($params->{font}, $params->{prefix} . join('', @entry_typed)) >= $params->{maxlen}) {
                    splice @entry_typed, $entry_position, 1;
                    play_sound('stick');
                } else {
                    $entry_position++;
                    $no_echo or play_sound('typewriter');
                }
            } else {
                handle_whenever_events($params->{event}->key_sym);
            }
        }
    }
    if ($action eq 'gettext') {
        return @entry_typed;
    }
    if ($action eq 'settext') {
        @entry_typed = ($params, @rest);
        $entry_position = $#entry_typed + 1;
        $entry_echo_blink_counter = 51;
        $no_echo or play_sound('typewriter');
    }
    if ($action eq 'print') {
        $params->{maxlen} or die("need maxlen\n".backtrace());
        print_($params->{font}, $app, $params->{xpos}, $params->{ypos}, $params->{prefix} . join('', @entry_typed), $params->{maxlen});
        if ($entry_echo_blink_counter > 25) {
            my @before_echo = @entry_typed;
            splice @before_echo, $entry_position;
            my $width = width($params->{font}, $params->{prefix} . join('', @before_echo)) + ( $is_rtl ? 3 : -3 );
            if ($is_rtl) {
                print_($params->{font}, $app, $params->{xpos} + $params->{maxlen} - $width, $params->{ypos}, '|');
            } else {
                print_($params->{font}, $app, $params->{xpos} + $width, $params->{ypos}, '|');
            }
        }
    }
    if ($action eq 'ping') {
        $entry_echo_blink_counter--;
        $entry_echo_blink_counter or $entry_echo_blink_counter = 50;
        return $entry_echo_blink_counter == 50 || $entry_echo_blink_counter == 25;
    }
}

sub get_spot_location {
    my ($latitude, $longitude) = @_;
    my $x0 = 309;
    my $y0 = 231;
    my $longitude_factor = 1.424;
    my $latitude_factor = -145;
    return ($x0 + $longitude*$longitude_factor,
            $y0 + asinh(tan($latitude*1.4*$PI/360))*$latitude_factor);  #- map seems not to really be mercator but.. approximation is kinda ok
}

sub save_back_spot {
    my ($latitude, $longitude, $surface, $back) = @_;
    my ($x, $y) = get_spot_location($latitude, $longitude);
    $x -= $surface->width/2;
    $y -= $surface->height/2;
    $$back = SDL::Surface->new(-width => 20, -height => 20, -depth => 32, -Amask => '0 but true');
    $app->blit(SDL::Rect->new('-x' => $x, '-y' => $y, -width => 20, -height => 20), $$back, undef);
    add_default_rect($$back);
}
sub print_spot {
    my ($latitude, $longitude, $kind, $surface, $back) = @_;
    $surface ||= $imgbin{"netspot_$kind"};
    my ($x, $y) = get_spot_location($latitude, $longitude);
    $x -= $surface->width/2;
    $y -= $surface->height/2;
    if ($back) {
        put_image($$back, $x, $y);
        pop @update_rects;
    }
    put_image($surface, $x, $y);
}

sub is_only_ascii {
    my ($text) = @_;
    foreach (unpack("C*", $text)) {
        $_ > 127 and return 0;
    }
    return 1;
}

sub smg_choose_game() {
    my @actions = ({ name => t("Chat"), action => 'CHAT', selected => 1 },
                   { name => t("Create new game"), action => 'CREATE' });
    my $max_actions = 18;

    my $curaction = sub {
        my $cur;
        each_index {
            if ($actions[$::i]->{selected}) {
                $cur = $::i;
                goto curaction_done;
            }
        } @actions;
      curaction_done:
        return $actions[$cur];
    };

    my $state = 'game_select';

    my $erase = sub {
        my $drect = SDL::Rect->new(-width => 640, -height => $smg_starty_players, -x => 0, '-y' => 0);
        $imgbin{back_netgame}->blit($drect, $app, $drect);
    };
    callback_entry('reset');
    my @wholist;
    my (@free_geolocs, @playing_geolocs);
    my $index_selfspot = 0;
    my $back_selfspot;
    my $free_players;
    my $ingame = 0;
    my $players_in_game = '';
    my $label_outer_color = SDL::Color->new(-r => 0x7b, -g => 0x2f, -b => 0x03);
    my $label_background_color = SDL::Color->new(-r => 0x5b, -g => 0x1f, -b => 0x09);
    my $label_line_color = SDL::Color->new(-r => 0x9b, -g => 0x3f, -b => 0x03);
    my $redraw = sub {
        $erase->();

        #- geoloc spots - playing: spots only
        my @rectangles = ();
        foreach (@playing_geolocs) {
            my ($x, $y) = get_spot_location($_->[0], $_->[1]);
            print_spot($_->[0], $_->[1], 'playing');
            push @rectangles, SDL::Rect->new('-x' => $x - $imgbin{"netspot_free"}->width/2,
                                             '-y' => $y - $imgbin{"netspot_free"}->height/2,
                                             '-width' => $imgbin{"netspot_free"}->width,
                                             '-height' => $imgbin{"netspot_free"}->height);
        }

        #- geoloc spots - free: spots and labels (nicks)
        #- first show spots, we don't want to move them (but we will want to move labels)
        foreach (@free_geolocs) {
            @$_ or next;
            my ($x, $y) = get_spot_location($_->[0], $_->[1]);
            print_spot($_->[0], $_->[1], $ingame ? 'insamegame' : 'free');
            push @rectangles, SDL::Rect->new('-x' => $x - $imgbin{"netspot_free"}->width/2,
                                             '-y' => $y - $imgbin{"netspot_free"}->height/2,
                                             '-width' => $imgbin{"netspot_free"}->width,
                                             '-height' => $imgbin{"netspot_free"}->height);
        }
        #- second show labels, managing overlap
        my @labels = ();
        my $linecolor = SDL::Color->new(-r => 0x7b, -g => 0x2f, -b => 0x03);
        mapn {
            if (@{$_[1]}) {
                my ($xspot, $yspot) = get_spot_location($_[1][0], $_[1][1]);
                my ($xc, $yc, $sizing, $wb, $hb, $xoffset, $yoffset, $yoffset_retry);
                my $text = $_[0];
                my $index_positions = 0;
                #- set $x/$y as the next position to try; try positions around, then grow circle of 8 pixels
                my $set_next_position = sub {
                    my $positions = 16;
                    my $grows = int($index_positions / $positions);
                    my $angle = ($index_positions - $grows*$positions) * 2 * $PI / $positions;
                    $xc = $xspot + cos($angle) * ($wb/2 + 4 + ($grows + 1) * 8);
                    $yc = $yspot + sin($angle) * ($hb/2 + 4 + ($grows + 1) * 8);
                    $index_positions++;
                };
                #- find sizing of text
                print_('netdialogs_servermsg', $app, 0, 0, $text, undef, undef, #- coordinates are unimportant, we'll cancel this printing
                       sub {
                           my ($action, $textsurface) = @_;
                           $sizing = fb_c_stuff::autopseudocrop($textsurface);
                           $wb = $sizing->[2] + 6;
                           $hb = $sizing->[3] + 6;
                           return 0;
                       });
                my $xb;
                my $yb;
                my $r;
                $set_next_position->();
              find_ok_position:
                while (1) {
                    $xb = $xc - $wb/2;
                    $yb = $yc - $hb/2;
                    #- check that background rectangle does not overlap any existing rectangle (spots or other labels)
                    $r = SDL::Rect->new('-x' => $xb, '-y' => $yb, '-width' => $wb, '-height' => $hb);
                    foreach my $rect (@rectangles) {
                        if ($rect->x >= $xb && $wb > $rect->x - $xb
                            || $xb >= $rect->x && $rect->width > $xb - $rect->x) {
                            #- x overlap
                            if ($rect->y >= $yb && $hb > $rect->y - $yb
                                || $yb >= $rect->y && $rect->height > $yb - $rect->y) {
                                $set_next_position->();
                                while ($xb < 20 || $xb + $wb > 620 || $yb < 20 || $yb + $hb > 320) {
                                    $set_next_position->();
                                    if ($index_positions > 100) {
                                        #- abandon, map is crowded
                                        return;
                                    }
                                }
                                if ($index_positions > 100) {
                                    #- abandon, map is crowded
                                    return;
                                }
                                next find_ok_position;
                            }
                        }
                    }
                    last;
                }
                #- draw a line between the spot and the center of the label
                fb_c_stuff::draw_line(surf($app), $xspot, $yspot, $xc, $yc, col($label_line_color));
                #- draw the label background and the label in another pass to handle overwriting of labels over lines
                push @labels, { x => $xc - $sizing->[2]/2 - $sizing->[0], y => $yc - $sizing->[3]/2 - $sizing->[1], text => $text, backrect1 => $r,
                                backrect2 => SDL::Rect->new('-x' => $xb + 1, '-y' => $yb + 1, '-width' => $wb - 2, '-height' => $hb - 2) };
                push @rectangles, $r;
            }
        } \@wholist, \@free_geolocs;

        foreach (@labels) {
            $app->fill($_->{backrect1}, $label_outer_color);
            $app->fill($_->{backrect2}, $label_background_color);
            print_('netdialogs_servermsg', $app, $_->{x}, $_->{y}, $_->{text});
        }

        #- actions (chat, joins, create)
        my $y = $smg_starty;
        foreach my $action (@actions) {
            if ($action->{selected}) {
                put_image($imgbin{highlight_server}, 6, $y-1);
            }
            print_($action->{readonly} ? 'netdialogs_servermsg' : 'netdialogs', $app, $smg_startx, $y, $action->{name}, 520);
            $y += $smg_lineheight;
        }

        #- selfspot will need to properly erase selection overlay
        if ($mylatitude && !$private) {
            save_back_spot($mylatitude, $mylongitude, $imgbin{netspot_self}[0], \$back_selfspot);
            if ($index_selfspot >= 0) {
                print_spot($mylatitude, $mylongitude, 'free', $imgbin{netspot_self}[$index_selfspot], \$back_selfspot);
            }
        }

        #- chat entry
        erase_line($smg_starty_chat, $imgbin{back_netgame});
        if ($curaction->() && $curaction->()->{action} eq 'CHAT') {
            callback_entry('print', { xpos => $smg_startx, ypos => $smg_starty_chat, maxlen => 520, prefix => t("Say: "), font => 'netdialogs' });
        }

        #- available players in server, or list of players in game
        erase_line($smg_starty_players, $imgbin{back_netgame});
        if ($ingame) {
            print_('netdialogs', $app, $smg_startx, $smg_starty_players, t("Players in game: %s", $players_in_game), 520);
        } else {
            print_('netdialogs', $app, $smg_startx, $smg_starty_players, t("Available Players: %s", i18n_number($free_players)), 520);
        }

        #- status messages
        smg_printstatus();
    };
    my $print_selfspot = sub {
        if ($mylatitude && !$private) {
            $index_selfspot >= 0 and print_spot($mylatitude, $mylongitude, 'free', $imgbin{netspot_self}[$index_selfspot], \$back_selfspot);
            $index_selfspot++;
            if ($index_selfspot == @{$imgbin{netspot_self}}) {
                my ($x, $y) = get_spot_location($mylatitude, $mylongitude);
                put_image($back_selfspot, $x - 10, $y - 10);
                $index_selfspot = -15;
            }
        }
    };
    
    my $myoldnick;
    my $list = sub {
        my ($firsttime) = @_;
        $state eq 'game_select' or return;
        my @games;
        my @old_wholist = @wholist;
        eval {
            ($free_players, undef, my $freenicks, undef, my $playing_geolocs, @games) = fb_net::list();
            @wholist = ();
            @free_geolocs = ();
            @playing_geolocs = ();
            foreach (split ',', $freenicks) {
                my ($nick, undef, $latitude, $longitude) = $_ =~ /([^:]+)(:([^:]+):([^:]+))?/;
                push @wholist, $nick;
                if (defined($latitude) && $latitude =~ /^-?\d+\.?\d*$/ && $longitude =~ /^-?\d+\.?\d*$/) {
                    push @free_geolocs, [ $latitude, $longitude ];
                } else {
                    push @free_geolocs, [];
                }
            }
            foreach (split ',', $playing_geolocs) {
                my ($latitude, $longitude) = $_ =~ /([^:]+):([^:]+)/;
                $latitude =~ /^-?\d+\.?\d*$/ && $longitude =~ /^-?\d+\.?\d*$/ and push @playing_geolocs, [ $latitude, $longitude ];
            }
        };
        $@ and return;

        if (!$firsttime) {
            my @joined = difference2([ sort(difference2(\@wholist, \@old_wholist)) ], [ $mynick ]);
            my @left = difference2([ sort(difference2(\@old_wholist, \@wholist)) ], [ $myoldnick ]);
            if (@left == 1) {
                smg_add_status_msg(t("*** %s has left the chat room", @left));
            } else {
                my $send = t("*** Several players left this chat room: ");
                while (@left) {
                    $send .= shift @left;
                    if (!@left || width('netdialogs', $send) >= 520) {
                        smg_add_status_msg($send);
                        $send = '*** ';
                    } else {
                        $send .= ',';
                    }
                }
            }
            if (@joined == 1) {
                smg_add_status_msg(t("*** %s has joined the chat room", @joined));
            } else {
                my $send = t("*** Several players joined this chat room: ");
                while (@joined) {
                    $send .= shift @joined;
                    if (!@joined || width('netdialogs', $send) >= 520) {
                        smg_add_status_msg($send);
                        $send = '*** ';
                    } else {
                        $send .= ',';
                    }
                }
            }
        }
        
        my ($join, $rest) = partition { $_->{action} eq 'JOIN' } @actions;
        $_->{ok} = 0 foreach @$join;
      listgames:
        foreach my $players (@games) {
            if (@$players < 5 && $players->[0] ne $forget_because_kicked) {
                my $name = t("Join game: %s", join(', ', @$players));
                foreach my $line (@$join) {
                    if ($line->{name} eq $name) {
                        $line->{ok} = 1;
                        next listgames;
                    }
                }
                push @$join, { name => $name, action => 'JOIN', join => $players->[0], ok => 1 };
            }
        }
        @actions = (@$rest, grep { $_->{ok} } @$join);
        if (!any { $_->{selected} } @actions) {
            $actions[0]{selected} = 1;
        }
        $redraw->();
    };

    my $list_players = sub {
        if (@wholist > 1) {
            my @list = sort @wholist;
            my $send = t("*** Players listening: ");
            while (@list) {
                $send .= shift @list;
                if (!@list || width('netdialogs', $send) >= 520) {
                    smg_add_status_msg($send);
                    $send = '*** ';
                } else {
                    $send .= ',';
                }
            }
        } else {
            smg_add_status_msg(t("*** Notice: no one's listening here"));
        }
    };

    fb_net::send_and_receive('NICK', $mynick);

    my $geolocate = sub {
        smg_add_status_msg(t("*** Please wait, retrieving your geographical location from http://hostip.info/..."));
        eval {
            local $SIG{ALRM} = sub { die "alarm\n" };
            alarm 5;
            my $data = fb_net::http_download('http://api.hostip.info/get_html.php?position=true');
            ($mylatitude) = $data =~ /Latitude: (-?\S{1,5})/;
            ($mylongitude) = $data =~ /Longitude: (-?\S{1,5})/;
            if (defined($mylatitude)) {
                smg_add_status_msg(t("*** Done, you are positioned as the flashing red dot!"));
            } else {
                smg_add_status_msg(t("*** hostip.info doesn't know the geographical location of your IP address"));
                smg_add_status_msg(t("*** If you want that your location appears on the map, fix your entry at http://hostip.info/"));
                smg_add_status_msg(t("*** Then type /geolocate to see yourself on the map"));
                $mylatitude = '';
            }
            alarm 0;
        };
        if ($@) {
            if ($@ =~ /^alarm/) {
                smg_add_status_msg(t("*** hostip.info didn't reply within 5 seconds, giving up"));
            }
            $mylatitude = '';
        }
        if ($mylatitude) {
            fb_net::send_and_receive('GEOLOC', "$mylatitude:$mylongitude");
        } else {
            fb_net::send_and_receive('GEOLOC', "");
        }
    };

    if ($pdata{gametype} eq 'net' && !$private) {
        if (!defined($mylatitude)) {
            $geolocate->();
        } else {
            fb_net::send_and_receive('GEOLOC', "$mylatitude:$mylongitude");
        }
    }
    smg_add_status_msg(t("*** You may now create or join a game"));
    
    my $need4update;
    my $relist;
    my $can_start = 0;
    my $joined_leader;
    my $chain_reaction_state = t("enabled");
    my $continue_game_when_players_leave_state = t("enabled");
    my $single_player_targetting_state = t("enabled");
    my @victories_limits = ({ value => undef,
                              text => t("none (unlimited)") },
                            map { { value => $_,
                                    text => i18n_number($_) } } (1..12, 15, 20, 30, 50, 100));
    my $victories_limit_index = 5;
    my @history;
    my $history_position;
    $list->('first time');
    $list_players->();

    my $setoptions = sub {
        my $level = fb_net::send_and_receive('PROTOCOL_LEVEL');
        if ($level < 2) {  #- continue game when players leave available from minor level 2 onwards
            smg_verify_command('SETOPTIONS', 'CHAINREACTION:' . to_bool($chain_reaction_state eq t("enabled")) . ','
                                           . "VICTORIESLIMIT:$victories_limits[$victories_limit_index]{value}");
        } else {
            smg_verify_command('SETOPTIONS', 'CHAINREACTION:' . to_bool($chain_reaction_state eq t("enabled")) . ','
                                           . 'CONTINUEGAMEWHENPLAYERSLEAVE:' . to_bool($continue_game_when_players_leave_state eq t("enabled")) . ','
                                           . 'SINGLEPLAYERTARGETTING:' . to_bool($single_player_targetting_state eq t("enabled")) . ','
                                           . "VICTORIESLIMIT:$victories_limits[$victories_limit_index]{value}");
        }
    };

    my $toggle_chain_reaction = sub {
        if ($chain_reaction_state eq t("disabled")) {
            my $level = fb_net::send_and_receive('PROTOCOL_LEVEL');
            if ($level < 1) {  #- available from minor level 1 onwards
                smg_add_status_msg(t("*** Cannot modify this option, as a player is using a too old version of Frozen-Bubble"));
                play_sound('cancel');
            } else {
                $chain_reaction_state = t("enabled");
                $actions[1]{name} = t("Chain-reaction: %s", $chain_reaction_state);
                $redraw->();
                $setoptions->();
            }
        } else {
            $chain_reaction_state = t("disabled");
            $actions[1]{name} = t("Chain-reaction: %s", $chain_reaction_state);
            $redraw->();
            $setoptions->();
        }
    };

    my $toggle_continue_game_when_players_leave = sub {
        if ($continue_game_when_players_leave_state eq t("disabled")) {
            my $level = fb_net::send_and_receive('PROTOCOL_LEVEL');
            if ($level < 2) {  #- available from minor level 2 onwards
                smg_add_status_msg(t("*** Cannot modify this option, as a player is using a too old version of Frozen-Bubble"));
                play_sound('cancel');
            } else {
                $continue_game_when_players_leave_state = t("enabled");
                $actions[2]{name} = t("Continue game when players leave: %s", $continue_game_when_players_leave_state);
                $redraw->();
                $setoptions->();
            }
        } else {
            $continue_game_when_players_leave_state = t("disabled");
            $actions[2]{name} = t("Continue game when players leave: %s", $continue_game_when_players_leave_state);
            $redraw->();
            $setoptions->();
        }
    };

    my $toggle_single_player_targetting = sub {
        if ($single_player_targetting_state eq t("enabled")) {
            my $level = fb_net::send_and_receive('PROTOCOL_LEVEL');
            if ($level < 2) {  #- available from minor level 2 onwards
                smg_add_status_msg(t("*** Cannot modify this option, as a player is using a too old version of Frozen-Bubble"));
                play_sound('cancel');
            } else {
                $single_player_targetting_state = t("disabled");
                $actions[3]{name} = t("Single player targetting: %s", $single_player_targetting_state);
                $redraw->();
                $setoptions->();
            }
        } else {
            $single_player_targetting_state = t("enabled");
            $actions[3]{name} = t("Single player targetting: %s", $single_player_targetting_state);
            $redraw->();
            $setoptions->();
        }
    };

    my $change_victories_limit = sub {
        my ($action) = @_;
        my $level = fb_net::send_and_receive('PROTOCOL_LEVEL');
        if ($level < 1) {  #- available from minor level 1 onwards
            smg_add_status_msg(t("*** Cannot modify this option, as a player is using a too old version of Frozen-Bubble"));
            play_sound('cancel');
        } else {
            if ($action eq 'inc') {
                $victories_limit_index++;
                $victories_limit_index == @victories_limits and $victories_limit_index = 0;
            } elsif ($action eq 'dec') {
                $victories_limit_index--;
                $victories_limit_index == -1 and $victories_limit_index = @victories_limits - 1;
            } else {
                for (my $i = 0; $i < @victories_limits; $i++) {
                    if ((!$action && !$victories_limits[$i]{value})
                        || ($action && $victories_limits[$i]{value} && $victories_limits[$i]{value} eq $action)) {
                        $victories_limit_index = $i;
                    }
                }
            }
            $actions[4]{name} = t("Victories limit: %s", $victories_limits[$victories_limit_index]{text});
            $redraw->();
            $setoptions->();
        }
    };

    my $kick = sub {
        my ($kicked) = @_;
        my $answer = smg_verify_command('KICK', $kicked);
        if ($answer eq 'NO_SUCH_PLAYER') {
            smg_add_status_msg(t("*** Can't kick %s: no such player in game", $kicked));
        } elsif ($answer) {
            smg_add_status_msg(t("*** Can't kick %s: '%s'", $kicked, $answer));
        }
    };

    while (1) {
        $relist++;
        $relist % (5*(1000/$TARGET_ANIM_SPEED)) == 0 and $list->();
        if (callback_entry('ping')) {
            $redraw->();
        } else {
            if ($relist % 3 == 0) {
                $print_selfspot->();
                $app->update(@update_rects);
                @update_rects = ();
            }
        }

        my $need_redraw;
        $event->pump;
        while ($event->poll != 0) {
            if ($event->type == SDL_QUIT) {
                cleanup_and_exit();
            }
            my $k;
            if ($event->type == SDL_KEYDOWN) {
                $k = $event->key_sym;
            } elsif ($curaction->()->{action} ne 'CHAT' && ($event->type == fb_c_stuff::JOYAXISMOTION() || $event->type() == fb_c_stuff::JOYBUTTONDOWN())) {
                $k = translate_joystick_tokey($event);
            }
            if ($k) {
                if ($k eq SDLK_ESCAPE()) {
                    if ($ingame) {
                        smg_add_status_msg(t("*** Leaving game..."));
                        fb_net::reconnect();
                        return smg_choose_game();
                    } else {
                        fb_net::disconnect();
                        $erase->();
                        return 0;
                    }

                } elsif ($k eq SDLK_PAGEUP()) {
                    $smg_status_message_offsetpage += 4;
                    $smg_status_message_offsetpage >= @smg_status_messages - $smg_max_messages and $smg_status_message_offsetpage = @smg_status_messages - $smg_max_messages + 1;
                    $smg_status_message_offsetpage < 1 and $smg_status_message_offsetpage = 1;
                    $redraw->();

                } elsif ($k eq SDLK_PAGEDOWN()) {
                    $smg_status_message_offsetpage -= 4;
                    $smg_status_message_offsetpage < 1 and $smg_status_message_offsetpage = 1;
                    $redraw->();

                } elsif ($k eq SDLK_DOWN()) {
                    if ($curaction->()->{action} eq 'CHAT' && $history_position <= $#history) {
                        $history_position++;
                        if ($history_position > $#history) {
                            callback_entry('reset');
                        } else {
                            callback_entry('settext', @{$history[$history_position]});
                        }
                    } else {
                        each_index {
                            if ($actions[$::i]{selected}
                                && $::i < @actions - 1
                                && ! $actions[$::i+1]{readonly}) {
                                $actions[$::i]{selected} = 0;
                                $actions[$::i+1]{selected} = 1;
                                play_sound('menu_change');
                                goto done2;
                            }
                        } @actions;
                      done2:
                    }

                } elsif ($k eq SDLK_UP()) {
                    if ($curaction->()->{action} eq 'CHAT') {
                        $history_position--;
                        if ($history_position == -1) {
                            $history_position = 0;
                        } else {
                            callback_entry('settext', @{$history[$history_position]});
                        }
                    } else {
                        if (!$actions[0]->{selected}) {
                            each_index {
                                if ($actions[$::i]->{selected}) {
                                    $actions[$::i]->{selected} = 0;
                                    $actions[$::i-1]->{selected} = 1;
                                    play_sound('menu_change');
                                }
                            } @actions;
                        }
                    }

                } elsif ($k eq SDLK_RETURN() || $k eq SDLK_KP_ENTER()) {
                    if ($curaction->()->{action} eq 'CHAT' && callback_entry('gettext') > 0) {
                        play_sound('menu_selected');
                        my $text = join('', callback_entry('gettext'));
                        push @history, [ callback_entry('gettext') ];
                        $history_position = $#history + 1;
                        if ($text =~ m|^/me (.*)| || $text =~ m|^/action (.*)|) {
                            $text = "* $mynick $1";
                        } elsif ($text =~ m|^/nick (.*)| && !$ingame) {
                            my $save_mynick = $mynick;
                            $mynick = sanitize_nick($1);
                            if ($mynick) {
                                smg_add_status_msg(t("*** You are now known as %s", $mynick));
                                fb_net::send_and_receive('NICK', $mynick);
                                $myoldnick = $save_mynick;
                            } else {
                                smg_add_status_msg(t("*** Erroneous nickname"));
                                $mynick = $save_mynick;
                            }
                            $text = undef;
                        } elsif ($text =~ m|^/list| && !$ingame) {
                            $list_players->();
                            $text = undef;
                        } elsif ($text =~ m|^/server|) {
                            my $servername = fb_net::current_server_name();
                            $servername or $servername = fb_net::current_server_hostport();
                            smg_add_status_msg(t("*** You're connected to server '%s'", $servername));
                            $text = undef;
                        } elsif ($text =~ m|^/fs|) {
                            $fullscreen = !$fullscreen;
                            $app->fullscreen;
                            $text = undef;
                        } elsif ($text =~ m|^/geolocate|) {
                            $geolocate->();
                            $text = undef;
                        } elsif ($text =~ m|^/kick (\S+)(?: (.+))?| && $can_start) {
                            my $kicked = $1;
                            $2 and fb_net::send_("TALK $kicked <-- $2");
                            if ($kicked eq $mynick) {
                                my $rand = int(rand(3));
                                $rand == 0 and smg_add_status_msg(t("*** Sado-masochist, hmm?"));
                                $rand == 1 and smg_add_status_msg(t("*** You like when it hurts, don't you?"));
                                $rand == 2 and smg_add_status_msg(t("*** Your butt already hurts enough! Stop that!"));
                            } else {
                                $kick->($kicked);
                            }
                            $text = undef;
                        } elsif ($text =~ m|^/autokick (\S+)(?: (.+))?|) {
                            my $kicked = $1;
                            my $message = $2;
                            if ($kicked eq $mynick) {
                                my $rand = int(rand(3));
                                $rand == 0 and smg_add_status_msg(t("*** Sado-masochist, hmm?"));
                                $rand == 1 and smg_add_status_msg(t("*** You like when it hurts, don't you?"));
                                $rand == 2 and smg_add_status_msg(t("*** Your butt already hurts enough! Stop that!"));
                            } else {
                                my $found = 0;
                                if (!$message && exists $autokick{$kicked}) {
                                    delete($autokick{$kicked});
                                    smg_add_status_msg(t("*** Removing %s from autokick list.", $kicked));
                                } else {
                                    smg_add_status_msg(t("*** Adding %s to autokick list.", $kicked));
                                    $autokick{$kicked} = $message;
                                    if ($can_start && any { $_ eq $kicked } @wholist) {
                                        $message and fb_net::send_("TALK $kicked <-- $message");
                                        $kick->($kicked);
                                    }
                                }
                            }
                            $text = undef;
                        } elsif ($text =~ m|^/autokick$|) {
                            if (!%autokick) {
                                smg_add_status_msg(t("*** Nobody in autokick list."));
                            } else {
                                smg_add_status_msg(t("*** Autokick list members: %s", join(", ", sort keys %autokick)));
                            }
                            $text = undef;
                        } elsif ($text =~ m|^/help|) {
                            smg_add_status_msg(t("*** Available commands: %s",
                                                 join(', ', t("%s <action>", '/me'), t("%s <nick> [<text>]", '/autokick'), '/server')),
                                               '*** ' . join(', ', '/fs', '/geolocate',
                                                                   if_(!$ingame, t("%s <new_nick>", '/nick'), '/list'),
                                                                   if_($can_start, t("%s <nick> [<text>]", '/kick'))));
                            $text = undef;
                        } elsif ($text =~ m|^/|) {
                            smg_add_status_msg(t("*** Unknown command. Try %s for help.", '/help'));
                            $text = undef;
                        } else {
                            #- for RTL language, force beginning on the right, but non RTL content will appear wrongly so at least don't do for pure ASCII
                            if ($is_rtl && !is_only_ascii($text)) {
                                $text = "‫<$mynick> $text";
                            } else {
                                $text = "<$mynick> $text";
                            }
                        }
                        if ($text) {
                            fb_net::send_("TALK $text");
                            @wholist == 1 and smg_add_status_msg(t("*** Notice: no one's listening here"));
                        }
                        callback_entry('reset');
                    }

                    if (member($curaction->()->{action}, 'CREATE', 'JOIN')) {
                        my $suffix = 1;
                        my $answer;
                        while (1) {
                            my $message = $curaction->()->{action} eq 'CREATE' ? $mynick
                                                                               : $curaction->()->{join}." $mynick";
                            $answer = smg_verify_command($curaction->()->{action}, $message);
                            if ($answer eq 'NICK_IN_USE') {
                                if ($suffix < 9) {
                                    $suffix++;
                                    $suffix > 2 and $mynick =~ s/.$//;  #- remove suffix added last loop
                                    $mynick = substr($mynick, 0, 9) . $suffix;
                                } else {
                                    #- try to find something that will be accepted, even if it sux
                                    $mynick = substr($mynick, 0, 7);
                                    my @chars = ('a' .. 'z', 'A' .. 'Z');
                                    $mynick .= $chars[rand(@chars)] foreach 1..3;
                                    $suffix = 1;
                                }
                            } elsif ($answer eq 'ALREADY_MAX_OPEN_GAMES') {
                                smg_add_status_msg(t("*** Open games already full. Join an existing game, or select a different server."));
                                goto not_in_game;
                            } elsif ($curaction->()->{action} eq 'JOIN' && $answer eq 'NO_SUCH_GAME') {
                                smg_add_status_msg(t("*** Cannot join game, game was just started or aborted"));
                                $list->();
                                goto not_in_game;
                            } elsif ($answer) {
                                smg_add_status_msg(t("*** Failure: '%s'", $answer));
                                goto not_in_game;
                            } else {
                                if ($curaction->()->{action} eq 'CREATE') {
                                    $can_start = 1;
                                    @wholist = $mynick;
                                    smg_add_status_msg(t("*** Game created - now you need to wait for players to join"));
                                } else {
                                    $joined_leader = $curaction->()->{join};
                                    smg_add_status_msg(t("*** Joined game"));
                                }
                                $ingame = 1;
                                last;
                            }
                        }
                        $state = 'wait_for_start';
                        $need4update = 1;
                    }
                  not_in_game:
                            
                    if ($curaction->()->{action} eq 'TOGGLE_CHAIN_REACTION') {
                        $toggle_chain_reaction->();
                    }

                    if ($curaction->()->{action} eq 'TOGGLE_CONTINUE_GAME_WHEN_PLAYERS_LEAVE') {
                        $toggle_continue_game_when_players_leave->();
                    }

                    if ($curaction->()->{action} eq 'TOGGLE_SINGLE_PLAYER_TARGETTING') {
                        $toggle_single_player_targetting->();
                    }

                    if ($curaction->()->{action} eq 'SWITCH_VICTORIES_LIMIT') {
                        $change_victories_limit->('inc');
                    }

                    if ($curaction->()->{action} eq 'START') {
                        my $close = smg_verify_command('CLOSE');
                        if ($close) {
                            smg_add_status_msg(t("*** Can't start game: '%s'", $close));
                            fb_c_stuff::fbdelay(2000);
                            return;
                        }
                        #- game is closed, need to check one last time if options are really possible
                        if ($chain_reaction_state eq t("enabled")
                            || $victories_limit_index > 0
                            || $continue_game_when_players_leave_state eq t("enabled")
                            || $single_player_targetting_state eq t("disabled")) {
                            my $level = fb_net::send_and_receive('PROTOCOL_LEVEL');
                            my $isdelay = 0;
                            if ($level < 1) {  #- chain reaction available from minor level 1 onwards
                                if ($chain_reaction_state eq t("enabled")) {
                                    smg_add_status_msg(t("*** Must disable chain-reaction, as a player is using a too old version of Frozen-Bubble"));
                                    $chain_reaction_state = t("disabled");
                                    $isdelay = 1;
                                }
                                if ($victories_limit_index > 0) {
                                    smg_add_status_msg(t("*** Must reset victories limit as a player is using a too old version of Frozen-Bubble"));
                                    $victories_limit_index = 0;
                                    $isdelay = 1;
                                }
                            }
                            if ($level < 2) {  #- continue game when players leave available from minor level 1 onwards
                                if ($continue_game_when_players_leave_state eq t("enabled")) {
                                    smg_add_status_msg(t("*** Cannot enable continue game when players leave, as a player is using an old version"));
                                    $continue_game_when_players_leave_state = t("disabled");
                                    $isdelay = 1;
                                }
                                if ($single_player_targetting_state eq t("disabled")) {
                                    smg_add_status_msg(t("*** Cannot disable single player targetting, as a player is using an old version"));
                                    $single_player_targetting_state = t("enabled");
                                    $isdelay = 1;
                                }
                            }
                            $isdelay and fb_c_stuff::fbdelay(2000);
                        }
                        if ($setoptions->()) {
                            smg_add_status_msg(t("*** Can't start game: '%s'", $setoptions));
                            fb_c_stuff::fbdelay(2000);
                            return;
                        }
                        my $start = smg_verify_command('START');
                        if ($start) {
                            smg_add_status_msg(t("*** Can't start game: '%s'", $start));
                            fb_c_stuff::fbdelay(2000);
                            return;
                        }
                    }
                    

                } elsif ($k eq SDLK_RIGHT() && $curaction->()->{action} ne 'CHAT') {
                    if ($curaction->()->{action} eq 'TOGGLE_CHAIN_REACTION') {
                        $toggle_chain_reaction->();
                    }
                    if ($curaction->()->{action} eq 'TOGGLE_CONTINUE_GAME_WHEN_PLAYERS_LEAVE') {
                        $toggle_continue_game_when_players_leave->();
                    }
                    if ($curaction->()->{action} eq 'TOGGLE_SINGLE_PLAYER_TARGETTING') {
                        $toggle_single_player_targetting->();
                    }
                    if ($curaction->()->{action} eq 'SWITCH_VICTORIES_LIMIT') {
                        $change_victories_limit->('inc');
                    }

                } elsif ($k eq SDLK_LEFT() && $curaction->()->{action} ne 'CHAT') {
                    if ($curaction->()->{action} eq 'TOGGLE_CHAIN_REACTION') {
                        $toggle_chain_reaction->();
                    }
                    if ($curaction->()->{action} eq 'TOGGLE_CONTINUE_GAME_WHEN_PLAYERS_LEAVE') {
                        $toggle_continue_game_when_players_leave->();
                    }
                    if ($curaction->()->{action} eq 'TOGGLE_SINGLE_PLAYER_TARGETTING') {
                        $toggle_single_player_targetting->();
                    }
                    if ($curaction->()->{action} eq 'SWITCH_VICTORIES_LIMIT') {
                        $change_victories_limit->('dec');
                    }

                } else {
                    if ($curaction->()->{action} eq 'CHAT') {
                        callback_entry('keypressed', { event => $event, maxlen => 480, font => 'netdialogs',
                                                       completion => sub {
                                                           my @typed = @_;
                                                           my $end;
                                                           while (1) {
                                                               last if !@typed;
                                                               my $c = pop @typed;
                                                               if ($c eq ' ') {
                                                                   $end or return @_;
                                                                   push @typed, $c;
                                                                   last;
                                                               }
                                                               $end = "$c$end";
                                                           }
                                                           my @matches = grep { lc(substr($_, 0, length($end))) eq lc($end) } @wholist;
                                                           if (@matches == 0) {
                                                               return @_;
                                                           } else {
                                                               my $colon = !@typed ? ': ' : ' ';
                                                               if (@matches == 1) {
                                                                   return @typed, split '', "$matches[0]$colon";
                                                               } else {
                                                                   my @chars = stringchars($matches[0]);
                                                                   my $counter = 0;
                                                                   while ($counter < @chars
                                                                          && every { (stringchars($_))[$counter] eq $chars[$counter] } @matches) {
                                                                       $counter++;
                                                                   }
                                                                   play_sound('stick');
                                                                   return @typed, split '', substr($matches[0], 0, $counter);
                                                               }
                                                           }
                                                       } });
                    
                    } else {
                        if ($curaction->()->{action} eq 'SWITCH_VICTORIES_LIMIT') {
                            my $utf8char = fb_c_stuff::utf8key(evt($event));
                            $change_victories_limit->($utf8char) if defined $utf8char && $utf8char eq int($utf8char);
                        }

                        handle_whenever_events($k);
                    }
                }

                $need_redraw = 1;
            }
        }
        if ($need_redraw) {
            callback_entry('moved');
            $redraw->();
        }
        fb_c_stuff::fbdelay($TARGET_ANIM_SPEED);

        while (my $msg = fb_net::readline_ifdata()) {
            my ($command, $message) = fb_net::decode_msg($msg);
            if ($command eq 'PUSH') {
                if ($message =~ /^TALK: (.*)/) {
                    my $message = $1;
                    my (undef, $min, $hour) = localtime();
                    smg_add_status_msg(sprintf("%02d:%02d ", $hour, $min) . $message);
                    play_sound('typewriter');
                } elsif ($message =~ /^JOINED: (.+)/) {
                    my $joined = $1;
                    smg_add_status_msg(t("*** %s joined the game!", $joined));
                    if ($can_start && exists $autokick{$joined}) {
                        $autokick{$joined} and fb_net::send_("TALK $joined <-- $autokick{$joined}");
                        $kick->($joined);
                    } else {
                        push @wholist, $joined;
                        play_sound('newroot_solo');
                        if ($can_start
                            && ($chain_reaction_state eq t("enabled")
                                || $victories_limit_index > 0
                                || $continue_game_when_players_leave_state eq t("enabled")
                                || $single_player_targetting_state eq t("disabled"))) {
                            my $level = fb_net::send_and_receive('PROTOCOL_LEVEL');
                            if ($level < 1) {  #- available from minor level 1 onwards
                                if ($chain_reaction_state eq t("enabled")) {
                                    smg_add_status_msg(t("*** Chain-reaction disabled, %s is using a too old version of Frozen-Bubble", $joined));
                                    $chain_reaction_state = t("disabled");
                                }
                                if ($victories_limit_index > 0) {
                                    smg_add_status_msg(t("*** Victories limit reset, %s is using a too old version of Frozen-Bubble", $joined));
                                    $victories_limit_index = 0;
                                }
                            }
                            if ($level < 2) {  #- available from minor level 2 onwards
                                if ($continue_game_when_players_leave_state eq t("enabled")) {
                                    smg_add_status_msg(t("*** Continue game when players leave disabled, %s has a too old version", $joined));
                                    $continue_game_when_players_leave_state = t("disabled");
                                }
                                if ($single_player_targetting_state eq t("disabled")) {
                                    smg_add_status_msg(t("*** Single player targetting enabled, %s has a too old version", $joined));
                                    $single_player_targetting_state = t("disabled");
                                }
                            }
                        }
                        $can_start and $setoptions->();  #- new joiner needs to get parameters anyway
                    }
                } elsif ($message =~ /^PARTED: (.+)/) {
                    if ($1 eq $joined_leader) {
                        smg_add_status_msg(t("*** Game creator left the game..."));
                        play_sound('cancel');
                        fb_net::reconnect();
                        return smg_choose_game();
                    } else {
                        smg_add_status_msg(t("*** %s left the game...", $1));
                        @wholist = difference2(\@wholist, [ $1 ]);
                        play_sound('newroot_solo');
                    }
                } elsif ($message =~ /^KICKED: (.+)/) {
                    smg_add_status_msg(t("*** %s was kicked out of the game...", $1));
                    @wholist = difference2(\@wholist, [ $1 ]);
                    play_sound('newroot_solo');
                } elsif ($message eq 'KICKED') {
                    $forget_because_kicked = $joined_leader;
                    smg_add_status_msg(t("*** You were kicked out of the game..."));
                    play_sound('cancel');
                    fb_net::reconnect();
                    return smg_choose_game();
                } elsif ($message eq 'NO_ACTIVITY_WITHIN_GRACETIME') {
                    smg_add_status_msg(t("*** You were disconnected because of too long inactivity"));
                    play_sound('cancel');
                    fb_net::reconnect();
                    return smg_choose_game();
                } elsif ($message =~ /^OPTIONS: (.*)/) {
                    my $options = $1;
                    while ($options =~ /([^,]+),?/g) {
                        my $option = $1;
                        if ($option =~ /^CHAINREACTION:(.)/) {
                            $chainreaction = $1;
                        } elsif ($option =~ /^CONTINUEGAMEWHENPLAYERSLEAVE:(.)/) {
                            $continuegamewhenplayersleave = $1;
                        } elsif ($option =~ /^SINGLEPLAYERTARGETTING:(.)/) {
                            $singleplayertargetting = $1;
                        } elsif ($option =~ /^VICTORIESLIMIT:(\d*)/) {
                            $pdata{scorelimit} = $1;
                        } elsif ($option =~ /^PROTOCOLLEVEL:(\d+)/) {
                            $pdata{protocollevel} = $1;
                            if ($pdata{protocollevel} < 2) {
                                $continuegamewhenplayersleave = 0;
                                $singleplayertargetting = 1;
                            }
                        } else {
                            print "Unrecognized option: $option\n";
                        }
                    }
                    play_sound('menu_selected');
                } elsif ($message =~ /^GAME_CAN_START: (.+)/) {
                    @PLAYERS = qw(p1);
                    my $msg = $1;
                    my @mappings;
                    while ($msg) {
                        my $id = substr($msg, 0, 1);
                        $msg = substr($msg, 1);
                        my ($nick, undef, $rest) = $msg =~ /([^,]+)(,(.*))?/;
                        $msg = $rest;
                        push @mappings, { id => $id, nick => $nick };
                    }
                    foreach (@ALL_PLAYERS) {
                        delete $pdata{$_}{id};
                        delete $pdata{$_}{nick};
                    }
                    %{$pdata{id2p}} = ();
                    foreach my $m (@mappings) {
                        my $player;
                        if ($m->{nick} eq $mynick) {
                            $player = 'p1';
                        } else {
                            foreach (@ALL_PLAYERS) {
                                /rp/ or next;
                                exists $pdata{$_}{id} or $player ||= $_;
                            }
                            push @PLAYERS, $player;
                        }
                        $pdata{$player}{id} = $m->{id};
                        $pdata{$player}{nick} = $m->{nick};
                        $pdata{id2p}{$m->{id}} = $player;
                    }
                    fb_net::setmyid($pdata{p1}{id});
                    $pdata{$_}{score} = 0 foreach @PLAYERS;
                    #- leader must wait for all others being in prio mode before sending a prio message,
                    #- else first messages might not get properly received on the other end and startup fails.
                    if (is_leader()) {
                      leader_check_game_start:
                        my $can_start = smg_verify_command('LEADER_CHECK_GAME_START');
                        if ($can_start eq 'OTHERS_NOT_READY') {
                            fb_net::sleep_reasonably();
                            goto leader_check_game_start;
                        } elsif ($can_start) {
                            smg_add_status_msg(t("*** Failure: '%s'", $can_start));
                            play_sound('cancel');
                            fb_net::reconnect();
                            return smg_choose_game();
                        }
                    }
                    smg_verify_command('OK_GAME_START');
                    return 1;
                } else {
                    print "unrecognized message received: $msg\n";
                }
                $need4update = 1;
            } else {
                print "non-push received!? $msg\n";
            }
        }

        if (!fb_net::isconnected()) {
            smg_add_status_msg(t("*** Lost connection to server, abandoning - press any key"));
            @actions = ();
            $redraw->();
            play_sound('cancel');
            $event->pump while $event->poll != 0;
            grab_key();
            $erase->();
            return 0;
        }

        if ($need4update) {
            if ($state ne 'game_select') {
                my $status = fb_net::send_and_receive('STATUSGEO');
                @wholist = ();
                @free_geolocs = ();
                foreach (split ',', $status) {
                    my ($nick, undef, $latitude, $longitude) = $_ =~ /([^:]+)(:([^:]+):([^:]+))?/;
                    push @wholist, $nick;
                    if ($latitude && $latitude =~ /^-?\d+\.?\d*$/ && $longitude =~ /^-?\d+\.?\d*$/) {
                        push @free_geolocs, [ $latitude, $longitude ];
                    } else {
                        push @free_geolocs, [];
                    }
                }
                $players_in_game = join ', ', @wholist;
                my $selected;
                each_index { $actions[$::i]{selected} and $selected = $::i; } @actions;
                @actions = { name => t("Chat"), action => 'CHAT' };
                if ($can_start) {
                    #- creator
                    push @actions, { name => t("Chain-reaction: %s", $chain_reaction_state), action => 'TOGGLE_CHAIN_REACTION' };
                    push @actions, { name => t("Continue game when players leave: %s", $continue_game_when_players_leave_state), action => 'TOGGLE_CONTINUE_GAME_WHEN_PLAYERS_LEAVE' };
                    push @actions, { name => t("Single player targetting: %s", $single_player_targetting_state), action => 'TOGGLE_SINGLE_PLAYER_TARGETTING' };
                    push @actions, { name => t("Victories limit: %s", $victories_limits[$victories_limit_index]{text}), action => 'SWITCH_VICTORIES_LIMIT' };
                    @wholist > 1 and push @actions, { name => t("Start game!"), action => 'START' };
                } else {
                    #- joiner
                    push @actions, { name => t("Chain-reaction: %s", $chainreaction ? t("enabled") : t("disabled")), readonly => 1};
                    push @actions, { name => t("Continue game when players leave: %s", $continuegamewhenplayersleave ? t("enabled") : t("disabled")), readonly => 1 };
                    push @actions, { name => t("Single player targetting: %s", $singleplayertargetting ? t("enabled") : t("disabled")), readonly => 1 };
                    push @actions, { name => t("Victories limit: %s", $pdata{scorelimit} ? i18n_number($pdata{scorelimit}) : t("none (unlimited)")), readonly => 1 };
                }
                $selected-- while $selected > $#actions || $actions[$selected]{readonly};
                $actions[$selected]{selected} = 1;
            }
            $redraw->();
            $need4update = 0;
        }
    }
}

sub show_mp_scores() {
    $imgbin{back_netgame}->blit($apprects{main}, $app, $apprects{main});

    if ($pdata{inconsistency}) {
        smg_add_status_msg(t("*** Game finished, because an inconsistency in game state was detected - sorry!"));
    } elsif ($pdata{scorelimit} && any { $pdata{$_}{score} == $pdata{scorelimit} } @PLAYERS) {
        smg_add_status_msg(t("*** Game finished, because victories limit of %s was reached.", i18n_number($pdata{scorelimit})));
    } else {
        if (!$continuegamewhenplayersleave && any { $pdata{$_}{left} } @PLAYERS) {
            smg_add_status_msg(t("*** Game finished, because the following player(s) left: %s", join(', ', map { if_($pdata{$_}{left}, $pdata{$_}{nick}) } @PLAYERS)));
        }
    }
    smg_add_status_msg(t("*** Addicted for: %s", format_addiction(($app->ticks - $time_netgame)/1000, 1)));
    @PLAYERS = reverse ssort { $pdata{$_}{score} } @PLAYERS;
    if ($pdata{$PLAYERS[0]}{score} > $pdata{$PLAYERS[1]}{score}) {
        smg_add_status_msg(t("*** Winner: %s", $pdata{$PLAYERS[0]}{nick}));
    } else {
        smg_add_status_msg(t("*** Draw game!"));
    }
    smg_add_status_msg(t("*** Scores: %s", join(', ', map { sprintf("%s: %s", $pdata{$_}{nick}, i18n_number($pdata{$_}{score})) } @PLAYERS)));
}

sub setup_mp_game() {
    
    $imgbin{back_netgame}->blit($apprects{main}, $app, $apprects{main});

    if (!fb_net::isconnected()) {
        @smg_status_messages = ();
        $smg_status_message_offsetpage = 1;
        if ($gameserver) {
            my ($host, $port) = $gameserver =~ /(\S+):(\S+)/;
            if (!$host) {
                $host = $gameserver;
                $port = 1511;
            }
            fb_net::connect($host, $port);
            if (!fb_net::isconnected()) {
                smg_add_status_msg(t("*** Cannot connect to specified gameserver, fallbacking to contacting master server"));
		print STDERR "Cannot connect to specified gameserver, fallbacking to contacting master server\n";
                goto choose_server;
            }
        } else {
          choose_server:
            #- 1. get list of servers
            my $servers = smg_servers();
            defined $servers or return;
            
            #- 2. let user choose server
            smg_choose_server(@$servers) or return;
        }
            
    } else {
        show_mp_scores();
    }

    #- 3. let user choose/create game
    $forget_because_kicked = undef;
    smg_choose_game() or return;

    save_config();

    $time_netgame = $app->ticks;
    return 1;
}

sub new_game_once {

    if ($direct_levelset) {
        load_levelset("$FBLEVELS/$direct_levelset");
        $direct_levelset = '';
    }
    if (!$direct) {
        if (is_1p_game()) {
            choose_1p_game_mode() or return;
        }
        if (is_2p_game() && $graphics_level > 1) {
            my $answ;
            ask_from({ intro => [ t("2-player game"), '', '', t("Enable chain-reaction?"), '' ],
                       entries => [ { 'q' => t("%s or %s?", 'Y', 'N'), 'a' => \$answ, f => 'ONE_CHAR' } ],
                       outro => t("Enjoy the game!") }) or return;
            $chainreaction = $answ == SDLK_y; #;;
        }
    }
    $pdata{$_}{left} = 0 foreach @ALL_PLAYERS;
    if (is_mp_game() && !$playdata) {
        $event->set_key_repeat(200, 50);
        my $ok_game;
        eval {
            $ok_game = setup_mp_game();
        };
        my $failure = $@;
        $event->set_key_repeat(0, 0);
        if ($failure && $failure ne 'quit') {
            die $failure;
        }
        $failure || !$ok_game and return;
    }
    play_music(is_1p_game() ? 'main1p' : 'main2p');
    return 1;
}

sub lvl_cmp($$) { $_[0] eq 'WON' ? ($_[1] eq 'WON' ? 0 : 1) : ($_[1] eq 'WON' ? -1 : $_[0] <=> $_[1]) }

sub ordered_highscores { return sort { lvl_cmp($b->{level}, $a->{level}) || $a->{time} <=> $b->{time} } @$HISCORES }
sub ordered_mptrain_highscores { return sort { $b->{score} <=> $a->{score} } @$HISCORES_MPTRAIN }
sub ordered_mptrain_highscores_chainreaction { return sort { $b->{score} <=> $a->{score} } @$HISCORES_MPTRAIN_CHAINREACTION }

sub handle_new_hiscores() {
    is_1p_game() && $levels{current} && $levels{current} ne 'random' && !$playdata or return;

    if ($levels{current} ne 'mp_train') {
        #- levels hiscores
        my @ordered = ordered_highscores();
        my $worst = pop @ordered;
        my $total_seconds = ($app->ticks - $time_1pgame)/1000;
        if (@$HISCORES == 10 && (lvl_cmp($levels{current}, $worst->{level}) == -1
                                 || lvl_cmp($levels{current}, $worst->{level}) == 0 && $total_seconds > $worst->{time})) {
            return;
        }
        play_sound('applause');
        append_highscore_level();

        my %new_entry;
        $new_entry{level} = $levels{current};
        $new_entry{time} = $total_seconds;
        $new_entry{piclevel} = count_highscorehistory_levels();
        ask_from({ intro => [ t("Congratulations!"), t("You have a highscore!"), '' ],
                   entries => [ { 'q' => t("Your name?"), 'a' => \$new_entry{name} } ],
                   outro => t("Great game!"),
                   erase_background => $background,
                 });
        return if $new_entry{name} eq '';

        push @$HISCORES, \%new_entry;
        if (@$HISCORES == 11) {
            my @high = ordered_highscores();
            pop @high;
            $HISCORES = \@high;
        }
        output($hiscorefiles{levels}, Data::Dumper->Dump([$HISCORES], [qw(HISCORES)]));
        display_highscores('levels', \%new_entry);

    } else {
        #- mp training hiscores
        my @ordered;
        if ($chainreaction) {
            @ordered = ordered_mptrain_highscores_chainreaction();
        } else {
            @ordered = ordered_mptrain_highscores();
        }
        my $scores = $chainreaction ? $HISCORES_MPTRAIN_CHAINREACTION : $HISCORES_MPTRAIN;
        my $worst = pop @ordered;
        if (@$scores == 20 && $pdata{p1}{score} < $worst->{score}) {
            return;
        }
        play_sound('applause');

        my %new_entry;
        $new_entry{score} = $pdata{p1}{score};
        ask_from({ intro => [ t("Congratulations!"), t("You have a highscore!"), '' ],
                   entries => [ { 'q' => t("Your name?"), 'a' => \$new_entry{name} } ],
                   outro => t("Great game!"),
                   erase_background => $background,
                 });
        return if $new_entry{name} eq '';

        push @$scores, \%new_entry;
        if (@$scores == 21) {
            my @high = sort { $b->{score} <=> $a->{score} } @$scores;
            pop @high;
            if ($chainreaction) {
                $HISCORES_MPTRAIN_CHAINREACTION = \@high;
            } else {
                $HISCORES_MPTRAIN = \@high;
            }
        }
        output($hiscorefiles{mptrain}, Data::Dumper->Dump([$HISCORES_MPTRAIN], [qw(HISCORES_MPTRAIN)]) . ' ' .
                                       Data::Dumper->Dump([$HISCORES_MPTRAIN_CHAINREACTION], [qw(HISCORES_MPTRAIN_CHAINREACTION)]));
        display_highscores('mptrain', \%new_entry);
    }
}

# append the new highscore to the $FBHOME/highlevelshistory
sub append_highscore_level() {

    my $row_numb = 0;
    my $lvl = 1;

    my @contents;

    foreach my $line (cat_($loaded_levelset)) {
	if ($line !~ /\S/) {
	    if ($row_numb) {
		$lvl++;
		$row_numb = 0;
            } 
        } else {
            $row_numb++;
            $lvl == ($levels{current} eq 'WON' ? (keys %levels)-1 : $levels{current})
	      and push @contents, $line;
        }
    }

    append_to_file("$FBHOME/highlevelshistory", @contents, "\n\n");
}

sub count_highscorehistory_levels() {
    my $cnt = 0;
    my $row_numb = 0;
    foreach my $line (cat_("$FBHOME/highlevelshistory")) {
	if ($line !~ /\S/) {
	    if ($row_numb) {
		$cnt++;
		$row_numb = 0;
            } 
        } else {
            $row_numb++;
        }
    }
    return $cnt;
} 

sub save_frame() {
    $replayparam and $app->save_bmp(sprintf("$saveframes/frame_\%d_%08d.bmp", $saveframesbase, $saveframescounter++));
}

#- ----------- mainloop ---------------------------------------------------

sub maingame() {
    my $synchro_ticks = $app->ticks;

    handle_graphics(\&erase_image);
    update_game();
    handle_graphics(\&put_image);
    $frame++;

#    print "rects:\n";
#    printf "\t%d:%d %d:%d %s\n", $_->x, $_->y, $_->width, $_->height, $_->{from} foreach @update_rects;
    $app->update(@update_rects);
    @update_rects = ();

    my $to_wait = $TARGET_ANIM_SPEED - ($app->ticks - $synchro_ticks);
#    print "$to_wait\n";
    $to_wait > 0 and fb_c_stuff::fbdelay($to_wait);
    $saveframes and save_frame();
}

sub cleanup_and_exit {
    save_config();
    exit 0;
}

#- ----------- menu stuff -------------------------------------------------

our $logo_candy_index = 0;
our $logo_candy_method = int(rand(8));
if ("@ARGV" =~ /winter.season/) {
    $logo_candy_method = 8;
} else {
    my (undef, undef, undef, $day, $month) = localtime(time());
    if ($day == 25 && $month == 11) {
        $logo_candy_method = 8;
    } else {
        if (!defined($mylatitude) || $mylatitude > 0) {
            if ($day >= 21 && $month == 11
                || $month == 0
                || $day <= 21 && $month == 1) {
                rand() < 0.3 and $logo_candy_method = 8;
            }
        } else {
            if ($day >= 21 && $month == 4
                || $month == 5
                || $day <= 24 && $month == 6) {
                rand() < 0.3 and $logo_candy_method = 8;
            }
        }
    }
}
our ($logox, $logoy);
our $candy;
our ($blink_green, $blink_purple);
our %cursor_save_bg;
our $cursor_tmp;
our %broken_cursors;

sub save_config {
    #- for $KEYS, try hard to keep SDLK_<key> instead of integer value in rcfile
    my $KEYS_;
    foreach my $p (keys %$KEYS) {
	foreach my $k (keys %{$KEYS->{$p}}) {
            if ($KEYS->{$p}{$k} =~ /^\d+$/) {
                foreach (@fbsyms::syms) {
                    if (eval("$KEYS->{$p}{$k} eq SDLK_$_")) {
                        $KEYS_->{$p}{$k} = "SDLK_$_";
                        goto nextkey;
                    }
                }
            }
            $KEYS_->{$p}{$k} = $KEYS->{$p}{$k};  #- fallback to numeric
          nextkey:
	}
    }
    #- special hook for $mylatitude as we don't want to save an empty value
    my $dump = Data::Dumper->Dump([$fullscreen, $graphics_level, $mynick, $music_disabled,
                                   $mixer_enabled, $mylatitude || undef, $mylongitude, $KEYS_],
                                  [qw(fullscreen graphics_level mynick music_disabled mixer_enabled
                                      mylatitude mylongitude KEYS)]);
    $dump =~ s/'SDLK_(\w+)'/SDLK_$1/g;
    output($rcfile, $dump);
}

sub menu {
    my ($firsttime) = @_;

    if (is_1p_game() && $levels{current} ne 'mp_train') {
        handle_new_hiscores();
    }

    play_music('intro');
    clean_server();

    my $back_start;
    my $display_menu = sub {
	$back_start->blit($apprects{main}, $app, $apprects{main});
	$imgbin{stamp}->blit(undef, $app, SDL::Rect->new('-x' => 490, '-y' => 142));
    };

    $back_start = $imgbin{back_menu};
    $display_menu->();

    my $invalidate_all;

    my $menu_display_highscores = sub {
	display_highscores();

	$display_menu->();
	$app->flip;
	$invalidate_all->();
    };

    my $change_keys = sub {
	ask_from({ intro => [ t("Please enter new keys:") ],
		   entries => [
			       { 'q' => t("Player 1; turn left?"),  'a' => \$KEYS->{p1}{left},  f => 'ONE_CHAR' },
			       { 'q' => t("Player 1; turn right?"), 'a' => \$KEYS->{p1}{right}, f => 'ONE_CHAR' },
			       { 'q' => t("Player 1; fire?"),  'a' => \$KEYS->{p1}{fire},  f => 'ONE_CHAR' },
			       { 'q' => t("Player 1; center?"),  'a' => \$KEYS->{p1}{center},  f => 'ONE_CHAR' },
                               { f => 'SPACE' },
			       { 'q' => t("Player 2; turn left?"),  'a' => \$KEYS->{p2}{left},  f => 'ONE_CHAR' },
			       { 'q' => t("Player 2; turn right?"), 'a' => \$KEYS->{p2}{right}, f => 'ONE_CHAR' },
			       { 'q' => t("Player 2; fire?"),  'a' => \$KEYS->{p2}{fire},  f => 'ONE_CHAR' },
			       { 'q' => t("Player 2; center?"),  'a' => \$KEYS->{p2}{center},  f => 'ONE_CHAR' },
                               { f => 'SPACE' },
			       { 'q' => t("Toggle fullscreen?"), 'a' => \$KEYS->{misc}{fs}, f => 'ONE_CHAR' },
			       { 'q' => t("Chat (net/lan game)?"), 'a' => \$KEYS->{misc}{chat}, f => 'ONE_CHAR' },
			      ],
		   outro => t("Thanks!"),
		   erase_background => $back_start
		 });
	$invalidate_all->();
#        print Data::Dumper->Dump([$KEYS], [qw(KEYS)]), "\n";
#        die;
    };

    my $launch_editor = sub {
        SDL::ShowCursor(1);
        FBLE::init_setup('embedded', $app);
        FBLE::handle_events();
        SDL::ShowCursor(0);
	$display_menu->();
        $app->flip;
        $invalidate_all->();
    };

    my $speed_ok = 4;

    my $candy_init = sub {
        if (defined($candy)) {
            erase_image_from($imgbin{menu_logo}, $logox, $logoy, $back_start);
            $candy = undef;
            $imgbin{menu_logo} = add_image('menu/fblogo.png');
        }
        ($logox, $logoy) = (400, 15);
        if ($logo_candy_method == 3) {
            #- stretch needs a bit more room
            my $newlogosurface = SDL::Surface->new(-width => $imgbin{menu_logo}->width * 1.1, -height => $imgbin{menu_logo}->height * 1.1, -depth => 32);
            $imgbin{menu_logo}->set_alpha(0, 0); #- for RGBA->RGBA blits, SDL_SRCALPHA must be removed or destination alpha is preserved
            $imgbin{menu_logo}->blit(undef, $newlogosurface, SDL::Rect->new(-x => $imgbin{menu_logo}->width * 0.05, '-y' => $imgbin{menu_logo}->height * 0.05));
            $logox -= $imgbin{menu_logo}->width * 0.05;
            $logoy -= $imgbin{menu_logo}->height * 0.05;
            $imgbin{menu_logo} = $newlogosurface;
            add_default_rect($imgbin{menu_logo});
            $logo_candy_method = 3.1; #- avoid doing it again at next menu run
        }
        if ($logo_candy_method == 4) {
            #- tilt needs a bit more horizontal room
            my $newlogosurface = SDL::Surface->new(-width => $imgbin{menu_logo}->width * 1.1, -height => $imgbin{menu_logo}->height * 1.05, -depth => 32);
            $imgbin{menu_logo}->set_alpha(0, 0); #- for RGBA->RGBA blits, SDL_SRCALPHA must be removed or destination alpha is preserved
            $imgbin{menu_logo}->blit(undef, $newlogosurface, SDL::Rect->new(-x => $imgbin{menu_logo}->width * 0.05, '-y' => $imgbin{menu_logo}->height * 0.025));
            $logox -= $imgbin{menu_logo}->width * 0.05;
            $logoy -= $imgbin{menu_logo}->height * 0.025;
            $imgbin{menu_logo} = $newlogosurface;
            add_default_rect($imgbin{menu_logo});
            $logo_candy_method = 4.1; #- avoid doing it again at next menu run
        }
        if ($logo_candy_method == 8) {
            #- snow needs to extend top, and a little more wideness
            my $newlogosurface = SDL::Surface->new(-width => $imgbin{menu_logo}->width * 1.1, -height => $imgbin{menu_logo}->height + $logoy, -depth => 32);
            $imgbin{menu_logo}->set_alpha(0, 0); #- for RGBA->RGBA blits, SDL_SRCALPHA must be removed or destination alpha is preserved
            $imgbin{menu_logo}->blit(undef, $newlogosurface, SDL::Rect->new(-x => $imgbin{menu_logo}->width * 0.05, '-y' => $logoy));
            $logox -= $imgbin{menu_logo}->width * 0.05;
            $logoy = 0;
            $imgbin{menu_logo} = $newlogosurface;
            add_default_rect($imgbin{menu_logo});
            $logo_candy_method = 8.1; #- avoid doing it again at next menu run
        }
        $candy = SDL::Surface->new(-width => $imgbin{menu_logo}->width, -height => $imgbin{menu_logo}->height, -depth => 32);
    };
    defined($candy) or $candy_init->();

    my $draw_logo = sub {
        my ($no_update) = @_;
        erase_image_from($imgbin{menu_logo}, $logox, $logoy, $back_start);
        if ($graphics_level < 3 || !$speed_ok) {
            put_image($imgbin{menu_logo}, $logox, $logoy);
        } else {
            $logo_candy_method == 0 and fb_c_stuff::rotate_bilinear(surf($candy), surf($imgbin{menu_logo}), sin($logo_candy_index/40)/20);
            $logo_candy_method == 1 and fb_c_stuff::flipflop(surf($candy), surf($imgbin{menu_logo}), $logo_candy_index);
            $logo_candy_method == 2 and fb_c_stuff::enlighten(surf($candy), surf($imgbin{menu_logo}), $logo_candy_index);
            $logo_candy_method == 3.1 and fb_c_stuff::stretch(surf($candy), surf($imgbin{menu_logo}), $logo_candy_index);
            $logo_candy_method == 4.1 and fb_c_stuff::tilt(surf($candy), surf($imgbin{menu_logo}), $logo_candy_index);
            $logo_candy_method == 5 and fb_c_stuff::points(surf($candy), surf($imgbin{menu_logo}), surf($imgbin{menu_logo_mask}));
            $logo_candy_method == 6 and fb_c_stuff::waterize(surf($candy), surf($imgbin{menu_logo}), $logo_candy_index);
            $logo_candy_method == 7 and fb_c_stuff::brokentv(surf($candy), surf($imgbin{menu_logo}), $logo_candy_index);
            $logo_candy_method == 8.1 and fb_c_stuff::snow(surf($candy), surf($imgbin{menu_logo}));
            
            $candy->blit(undef, $app, my $rect = SDL::Rect->new(-x => $logox, '-y' => $logoy));
            $logo_candy_index++;
        }
        if (!$no_update) {
            $app->update(@update_rects);
            @update_rects = ();
        }
    };

    $imgbin{menu_cursor}{graphics} = $imgbin{menu_cursor}{"graphics$graphics_level"};
    $imgbin{menu_cursor}{graphicsalpha} = $imgbin{menu_cursor}{"graphics${graphics_level}alpha"};
    my ($MENU_XPOS, $MENU_FIRSTY, $SPACING) = (89, 14, 56);
    my %menu_ypos = ( '1pgame' =>      $MENU_FIRSTY,
		      '2pgame' =>      $MENU_FIRSTY +     $SPACING,
		      'langame'=>      $MENU_FIRSTY + 2 * $SPACING,
		      'netgame'=>      $MENU_FIRSTY + 3 * $SPACING,
		      'editor' =>      $MENU_FIRSTY + 4 * $SPACING,
		      'graphics' =>    $MENU_FIRSTY + 5 * $SPACING,
		      'keys' =>        $MENU_FIRSTY + 6 * $SPACING,
		      'highscores' =>  $MENU_FIRSTY + 7 * $SPACING,
		  );
    my %menu_entries = ( '1pgame' => { pos => 1, type => 'rungame',
				       run => sub { @PLAYERS = ('p1'); $levels{current} = 1; $time_1pgame = $app->ticks } },
			 '2pgame' => { pos => 2, type => 'rungame',
				       run => sub { @PLAYERS = qw(p1 p2); $levels{current} = undef; } },
			 'langame'=> { pos => 3, type => 'rungame',
				       run => sub { @PLAYERS = qw(p1 rp1); $pdata{gametype} = 'lan'; $levels{current} = undef; } },
			 'netgame'=> { pos => 4, type => 'rungame',
				       run => sub { @PLAYERS = qw(p1 rp1); $pdata{gametype} = 'net';  $levels{current} = undef; } },
			 'editor' => { pos => 5, type => 'run', run => sub { $launch_editor->(); } },
			 'graphics' => { pos => 6, type => 'range', valuemin => 1, valuemax => 3,
					 change => sub {
                                             $graphics_level = $_[0];
                                             if ($graphics_level < 3) { $draw_logo->() } else { $speed_ok = 4; }
                                             $imgbin{menu_cursor}{graphics} = $imgbin{menu_cursor}{"graphics$graphics_level"};
                                             $imgbin{menu_cursor}{graphicsalpha} = $imgbin{menu_cursor}{"graphics${graphics_level}alpha"};
                                             $pdata{cursor_img}{graphics} >= @{$imgbin{menu_cursor}{graphics}} and $pdata{cursor_img}{graphics} = 0;
                                         },
                                         value => $graphics_level },
			 'keys' => { pos => 7, type => 'run',
				     run => sub { $change_keys->() } },
			 'highscores' => { pos => 8, type => 'run',
					   run => sub { $menu_display_highscores->() } },
		       );
    state $current_pos; $current_pos ||= 1;
    my @menu_invalids;
    $invalidate_all = sub { push @menu_invalids, $menu_entries{$_}->{pos} foreach keys %menu_entries };

    my $display_cursor = sub {
        my ($m, $alpha, $pixelize) = @_;
        my $cursor_rect = SDL::Rect->new(-x => 248, '-y' => $menu_ypos{$m} + 8,
                                         -width => $imgbin{menu_cursor}{$m}[$pdata{cursor_img}{$m}]->width,
                                         -height => $imgbin{menu_cursor}{$m}[$pdata{cursor_img}{$m}]->height);
        if (!defined($cursor_save_bg{$m})) {
            $cursor_save_bg{$m} = SDL::Surface->new(-width => $imgbin{menu_cursor}{$m}[0]->width, -height => $imgbin{menu_cursor}{$m}[0]->height, -depth => 32);
            $app->blit($cursor_rect, $cursor_save_bg{$m}, SDL::Rect->new('-x' => 0, '-y' => 0,
                                                                         -width => $imgbin{menu_cursor}{$m}[0]->width, -height => $imgbin{menu_cursor}{$m}[0]->height));
        }
        $cursor_save_bg{$m}->blit(undef, $app, $cursor_rect);
        if ($alpha) {
            if ($pixelize) {
                if (!defined($cursor_tmp)) {
                    $cursor_tmp = SDL::Surface->new(-width => $imgbin{menu_cursor}{$m}[0]->width, -height => $imgbin{menu_cursor}{$m}[0]->height, -depth => 32);
                }
                fb_c_stuff::pixelize(surf($cursor_tmp), surf($imgbin{menu_cursor}{"${m}alpha"}[$pdata{cursor_img}{$m}]));
                $cursor_tmp->blit(undef, $app, $cursor_rect);
            } else {
                $imgbin{menu_cursor}{"${m}alpha"}[$pdata{cursor_img}{$m}]->blit(undef, $app, $cursor_rect);
            }
        } else {
            $imgbin{menu_cursor}{$m}[$pdata{cursor_img}{$m}]->blit(undef, $app, $cursor_rect);
        }
        return $cursor_rect;
    };
    my $menu_update = sub {
	@update_rects = ();
	foreach my $m (keys %menu_entries) {
	    member($menu_entries{$m}->{pos}, @menu_invalids) or next;
	    my $txt = "txt_$m";
	    $txt .= $menu_entries{$m}->{pos} == $current_pos ? '_over' : '_off';
	    erase_image_from($imgbin{$txt}, $MENU_XPOS, $menu_ypos{$m}, $back_start);
	    put_image($imgbin{$txt}, $MENU_XPOS, $menu_ypos{$m});
            $cursor_save_bg{$m} = undef;
            $display_cursor->($m, $menu_entries{$m}->{pos} != $current_pos);
	}
	@menu_invalids = ();
        $draw_logo->('no-update');
	$app->update(@update_rects);
        @update_rects = ();
    };
    
    $invalidate_all->();
    $menu_update->();
    $app->flip;
    $event->pump while $event->poll != 0;

    my $start_game = 0;
    my ($BANNER_START, $BANNER_SPACING) = (1000, 80);
    my %banners = (artwork => $BANNER_START,
		   soundtrack => $BANNER_START + $imgbin{banner_artwork}->width + $BANNER_SPACING,
		   cpucontrol => $BANNER_START + $imgbin{banner_artwork}->width + $BANNER_SPACING
		                 + $imgbin{banner_soundtrack}->width + $BANNER_SPACING,
		   leveleditor => $BANNER_START + $imgbin{banner_artwork}->width + $BANNER_SPACING
                                 + $imgbin{banner_soundtrack}->width + $BANNER_SPACING
                                 + $imgbin{banner_cpucontrol}->width + $BANNER_SPACING);
    my ($BANNER_MINX, $BANNER_MAXX, $BANNER_Y) = (304, 596, 243);
    my $banners_max = $banners{leveleditor} - (640 - ($BANNER_MAXX - $BANNER_MINX)) + $BANNER_SPACING;
    my $banner_rect = SDL::Rect->new(-width => $BANNER_MAXX-$BANNER_MINX, -height => 30, '-x' => $BANNER_MINX, '-y' => $BANNER_Y);
    my $time_counter;

    while (!$start_game) {
	my $synchro_ticks = $app->ticks;

	$graphics_level > 1 and $back_start->blit($banner_rect, $app, $banner_rect);

	$event->pump;
	while ($event->poll != 0) {
            my $keypressed = extended_keypress($event);
            if ($keypressed) {
                if ($keypressed eq SDLK_PAUSE) {
                    my $time_pause = $app->ticks;
                    $event->pump while $event->poll != 0;
                  pause_menu:
                    while (1) {
                        $event->wait;
                        if ($event->type == SDL_QUIT) {
                            cleanup_and_exit();
                        }
                        my $keypressed = extended_keypress($event);
                        if ($keypressed) {
                            $start_time += $app->ticks - $time_pause;
                            last pause_menu;
                        }
                    }
                }
                if (SDL::GetKeyState(SDLK_LCTRL()) == SDL_PRESSED()
                    && $keypressed == SDLK_n) {
                    $logo_candy_method = (int($logo_candy_method) + 1) % 9;
                    $candy_init->();
                }
		if (member($keypressed, (SDLK_DOWN, SDLK_RIGHT))) {
                    push @menu_invalids, $current_pos;
                    if ($current_pos < max(map { $menu_entries{$_}->{pos} } keys %menu_entries)) {
                        $current_pos++;
                    } else {
                        $current_pos = 1;
                    }
                    push @menu_invalids, $current_pos;
                    play_sound('menu_change');
                    $menu_update->();
		}
		if (member($keypressed, (SDLK_UP, SDLK_LEFT))) {
                    push @menu_invalids, $current_pos;
                    if ($current_pos > 1) {
                        $current_pos--;
                    } else {
                        $current_pos = max(map { $menu_entries{$_}->{pos} } keys %menu_entries);
                    }
		    push @menu_invalids, $current_pos;
		    play_sound('menu_change');
                    $menu_update->();
		}
		if (member($keypressed, (SDLK_RETURN, SDLK_SPACE, SDLK_KP_ENTER))) {
		    play_sound('menu_selected');
		    push @menu_invalids, $current_pos;
		    foreach my $m (keys %menu_entries) {
			if ($menu_entries{$m}->{pos} == $current_pos) {
			    if ($menu_entries{$m}->{type} =~ /^run/) {
				$menu_entries{$m}->{run}->();
				$menu_entries{$m}->{type} eq 'rungame' and $start_game = 1;
			    }
			    if ($menu_entries{$m}->{type} eq 'range') {
				$menu_entries{$m}->{value}++;
				$menu_entries{$m}->{value} > $menu_entries{$m}->{valuemax}
				  and $menu_entries{$m}->{value} = $menu_entries{$m}->{valuemin};
				$menu_entries{$m}->{change}->($menu_entries{$m}->{value});
			    }
			}
		    }
                    $menu_update->();
		}
                handle_whenever_events($keypressed);

                if ($keypressed eq SDLK_ESCAPE) {
                    cleanup_and_exit();
		}
                $synchro_ticks = $app->ticks;  #- avoid stopping candy
                $time_counter = 0;  #- reset counter for demos
	    }
            if ($event->type == SDL_QUIT) {
                cleanup_and_exit();
            }
	}

	if ($graphics_level > 1) {
	    state $banner_pos;
	    $banner_pos ||= 670;
	    foreach my $b (keys %banners) {
		my $xpos = $banners{$b} - $banner_pos;
		my $image = $imgbin{"banner_$b"};

		$xpos > $banners_max/2 and $xpos = $banners{$b} - ($banner_pos + $banners_max);

		if ($xpos < $BANNER_MAXX && $xpos + $image->width >= 0) {
		    my $irect = SDL::Rect->new(-width => min($image->width+$xpos, $BANNER_MAXX-$BANNER_MINX),
                                               -height => $image->height, -x => -$xpos);
		    $image->blit($irect, $app, SDL::Rect->new(-x => $BANNER_MINX, '-y' => $BANNER_Y));
		}
	    }
	    $banner_pos++;
	    $banner_pos >= $banners_max and $banner_pos = 1;
            $app->update($banner_rect);

            #- animate and break cursor
            foreach my $m (keys %menu_entries) {
                if ($menu_entries{$m}->{pos} == $current_pos) {
                    $pdata{cursor_img}{$m}++;
                    $pdata{cursor_img}{$m} >= @{$imgbin{menu_cursor}{$m}} and $pdata{cursor_img}{$m} = 0;
                    $app->update($display_cursor->($m, 0));
                    
                } else {
                    if ($broken_cursors{$m}) {
                        $broken_cursors{$m}--;
                        if ($broken_cursors{$m}) {
                            $app->update($display_cursor->($m, 1, 1));
                        } else {
                            $app->update($display_cursor->($m, 1));
                        }
                    } else {
                        rand() < 0.001 and $broken_cursors{$m} = int(20 + 10 * cos(rand(2*$PI)));
                    }
                }
            }
            
            #- blinking handling follows
            my $blink_green_left = [ 411, 385 ];
            my $blink_green_right = [ 434, 378 ];
            my $green_left = SDL::Rect->new(-x => $blink_green_left->[0], '-y' => $blink_green_left->[1],
                                            -width => $imgbin{menu_closedeye_green_left}->width, -height => $imgbin{menu_closedeye_green_left}->height);
            my $green_right = SDL::Rect->new(-x => $blink_green_right->[0], '-y' => $blink_green_right->[1],
                                             -width => $imgbin{menu_closedeye_green_right}->width, -height => $imgbin{menu_closedeye_green_right}->height);
            my $blink_purple_left = [ 522, 356 ];
            my $blink_purple_right = [ 535, 356 ];
            my $purple_left = SDL::Rect->new(-x => $blink_purple_left->[0], '-y' => $blink_purple_left->[1],
                                            -width => $imgbin{menu_closedeye_purple_left}->width, -height => $imgbin{menu_closedeye_purple_left}->height);
            my $purple_right = SDL::Rect->new(-x => $blink_purple_right->[0], '-y' => $blink_purple_right->[1],
                                             -width => $imgbin{menu_closedeye_purple_right}->width, -height => $imgbin{menu_closedeye_purple_right}->height);
            if ($blink_green > 0) {
                $blink_green--;
                if (!$blink_green) {
                    $back_start->blit($green_left, $app, $green_left);
                    $back_start->blit($green_right, $app, $green_right);
                    $app->update($green_left, $green_right);
                    if (rand(3) <= 1) {  #- reblink
                        $blink_green = -5;
                    }
                }
            } elsif ($blink_green < 0) {
                $blink_green++;
                if (!$blink_green) {
                    $blink_green = 3;
                    $imgbin{menu_closedeye_green_left}->blit(undef, $app, $green_left);
                    $imgbin{menu_closedeye_green_right}->blit(undef, $app, $green_right);
                    $app->update($green_left, $green_right);
                }
            } else {
                if (rand(200) <= 1) {
                    $blink_green = 3;
                    $imgbin{menu_closedeye_green_left}->blit(undef, $app, $green_left);
                    $imgbin{menu_closedeye_green_right}->blit(undef, $app, $green_right);
                    $app->update($green_left, $green_right);
                }
            }
            if ($blink_purple > 0) {
                $blink_purple--;
                if (!$blink_purple) {
                    $back_start->blit($purple_left, $app, $purple_left);
                    $back_start->blit($purple_right, $app, $purple_right);
                    $app->update($purple_left, $purple_right);
                    if (rand(3) <= 1) {  #- reblink
                        $blink_purple = -5;
                    }
                }
            } elsif ($blink_purple < 0) {
                $blink_purple++;
                if (!$blink_purple) {
                    $blink_purple = 3;
                    $imgbin{menu_closedeye_purple_left}->blit(undef, $app, $purple_left);
                    $imgbin{menu_closedeye_purple_right}->blit(undef, $app, $purple_right);
                    $app->update($purple_left, $purple_right);
                }
            } else {
                if (rand(200) <= 1) {
                    $blink_purple = 3;
                    $imgbin{menu_closedeye_purple_left}->blit(undef, $app, $purple_left);
                    $imgbin{menu_closedeye_purple_right}->blit(undef, $app, $purple_right);
                    $app->update($purple_left, $purple_right);
                }
            }
            
	}

	if ($graphics_level > 2 && $speed_ok) {
            $draw_logo->();
        }

	my $to_wait = $TARGET_ANIM_SPEED - ($app->ticks - $synchro_ticks);
#        print "$to_wait\n";
	if ($to_wait > 0) {
            fb_c_stuff::fbdelay($to_wait);
            $speed_ok and $speed_ok = 4;
        } else {
            #- disable nice graphics artwork if computer is too slow
#            print "$to_wait\n";
            if ($speed_ok) {
                $speed_ok--;
                if (!$speed_ok) {
                    print "Eye-candy animation is too slow, disabling.\n";
                    $draw_logo->();
                }
            }
        }

        if (++$time_counter == 1000) {  #- 20 seconds
            $pdata{demo} = 1;
            my @files = glob("$FPATH/data/demo*");
            replay($files[int(rand(@files))]);
            $pdata{demo} = 0;
          blacken:
            foreach my $step (1..35) {
                my $ticks = $app->ticks;
                fb_c_stuff::blacken(surf($app), $step);
                $app->flip;
                my $to_wait = $TARGET_ANIM_SPEED - ($app->ticks - $ticks);
                $to_wait > 0 and fb_c_stuff::fbdelay($to_wait);

                $event->pump;
                while ($event->poll != 0) {
                    if ($event->type == SDL_QUIT) {
                        cleanup_and_exit();
                    }
                    if (extended_keypress($event)) {
                        last blacken;
                    }
                }
            }
            $display_menu->();
            $invalidate_all->();
            $menu_update->();
            $app->flip;
            $time_counter = 0;
            play_music('intro');
        }
    }

    save_config();

    iter_players {
       !is_1p_game() and $pdata{$::p}{score} = 0;
    };
}


#- ----------- editor stuff --------------------------------------------

sub choose_levelset() {
    my ($choose_level) = @_;

    my @levelsets = sort glob("$FBLEVELS/*");

    if (!@levelsets && !$choose_level) {
        # no $FBHOME/levels directory or void directory, just return and let the
        # game continue (means that the level editor has never been opened)

    } else {
	
	if (@levelsets <= 1 && !$choose_level) {
	    load_levelset($levelsets[0]);
	} else {
            #if they are choosing the start level, we need to ensure the default
            #levelset is in $FBLEVELS or the dialog won't display properly
            if ($choose_level) {
                -d $FBLEVELS or mkdir $FBLEVELS;
                -d $FBLEVELS or die "Can't create $FBLEVELS directory.\n";
                -f "$FBLEVELS/default-levelset" or cp_af("$FPATH/data/levels", "$FBLEVELS/default-levelset");
            }
            
	    FBLE::init_app('embedded', $app);
            FBLE::create_play_levelset_dialog($choose_level, $levels{current});
	    SDL::ShowCursor(1);
            my @game_info = FBLE::handle_events();
            @game_info or return;
            load_levelset("$FBLEVELS/$game_info[0]");
            $levels{current} = $game_info[1];
	    SDL::ShowCursor(0);
	}
    }

    return 1;
}

sub replay {
    my ($replayfile) = @_;
    if ($replayfile =~ /^http/) {
        my $filename = $replayfile;
        $replayfile = fb_net::http_download($replayfile);
        $replayfile or return;
        if ($filename =~ /\.bz2$/) {
            my $fh;
            do { $filename = POSIX::tmpnam() }
              until $fh = IO::File->new($filename, O_WRONLY|O_CREAT|O_EXCL);
            print $fh $replayfile;
            $fh->close;
            local *F;
            local $/ = undef;
            open(F, "bzcat '$filename'|" ) or print("Can't open '$filename': $!\n"), return;
            $replayfile = <F>;
            close F;
            unlink($filename);
            $replayfile or print("Could not bzcat '$filename', not displaying playback.\n"), return;
        }
    } else {
        if ($replayfile =~ /\.bz2$/) {
            local *F;
            local $/ = undef;
            my $filename = $replayfile;
            open(F, "bzcat '$filename'|" ) or print("Can't open '$filename': $!\n"), return;
            $replayfile = <F>;
            close F;
            $replayfile or print("Could not bzcat '$filename', not displaying playback.\n"), return;
        } else {
            $replayfile = cat_($replayfile);
        }
    }
    my $b = before_leaving {
        $playdata = undef;
        srand $app->ticks;
    };
    %recorddata = ();
    $playdata = [];
    my $linenumber = 0;
    my $current_frame_num;
    my $current_frame_action_data = {};
    my $current_frame_mp_messages_data = [];
    foreach my $line (split /\n/, $replayfile) {
        $linenumber++;
        my ($key, $value) = $line =~ /(.*?):(.*)?/ or print("Incorrect line number $linenumber in savegame.\n"), return;
        if ($key eq 'record_protocol') {
            if ($value > $RECORD_PROTOCOL_LEVEL) {
                print("Sorry, a more recent version of Frozen-Bubble is needed to play this record.\n"), return;
            } elsif ($value < $RECORD_PROTOCOL_LEVEL) {
                print("Sorry, an older version of Frozen-Bubble is needed to play this record.\n"), return;
            }
        } elsif ($key eq 'players') {
            @PLAYERS = split /,/, $value;
        } elsif ($key eq 'gametype') {
            $pdata{gametype} = $value;
        } elsif ($key eq 'current_level') {
            $levels{current} = $value;
        } elsif ($key eq 'chainreaction') {
            $chainreaction = $value;
        } elsif ($key eq 'time') {
            print 'Game recorded on ' . localtime($value) . ".\n";
        } elsif ($key eq 'comment') {
            print "Comment specified with record: $value.\n";
        } elsif ($key eq 'srand') {
            srand $value;
        } elsif ($key eq 'bubbles') {
            @{$recorddata{pdatas}{bubbles}} = split /,/, $value;
        } elsif ($key eq 'mp_result') {
            $recorddata{pdatas}{mp_result} = $value;
        } elsif ($key eq 'player_id') {
            my ($p, $id) = $value =~ /(.*?)\|(.*)/ or print("Incorrect line number $linenumber in savegame.\n"), return;
            $pdata{$p}{id} = $id;
        } elsif ($key eq 'player_nick') {
            my ($p, $nick) = $value =~ /(.*?)\|(.*)/ or print("Incorrect line number $linenumber in savegame.\n"), return;
            $pdata{$p}{nick} = $nick;
        } elsif ($key eq 'frame_action') {
            my ($framenum, $player, $action, $actionvalue) = $value =~ /(\d+)\|(\w+)\|(\w+)\|(\w+)/;
            if ($framenum != $current_frame_num && defined($current_frame_num)) {
                push @$playdata, [ $current_frame_num, { actions => $current_frame_action_data,
                                                         mp_messages => $current_frame_mp_messages_data } ];
                $current_frame_action_data = {};
                $current_frame_mp_messages_data = [];
            }
            $current_frame_num = $framenum;
            $current_frame_action_data->{$player}{$action} = $actionvalue;
        } elsif ($key eq 'frame_mpmessage') {
            my ($framenum, $id, $message) = $value =~ /(\d+)\|(.)\|(.+)/;
            if ($framenum != $current_frame_num && defined($current_frame_num)) {
                push @$playdata, [ $current_frame_num, { actions => $current_frame_action_data,
                                                         mp_messages => $current_frame_mp_messages_data } ];
                $current_frame_action_data = {};
                $current_frame_mp_messages_data = [];
            }
            $current_frame_num = $framenum;
            push @$current_frame_mp_messages_data, { id => $id, msg =>  $message };
        }
    }
    push @$playdata, [ $current_frame_num, { actions => $current_frame_action_data,
                                             mp_messages => $current_frame_mp_messages_data } ];
    $pdata{$_}{score} = 0 foreach @PLAYERS;
    if (is_mp_game()) {
        foreach my $p (@PLAYERS) {
            $pdata{id2p}{$pdata{$p}{id}} = $p;
        }
    }
    local $direct = 1;
    #- I'm wondering if the following is really more dirty than adding a $playdata test in the beginning of the three functions..
    local *fb_net::gsend = sub {};
    local *fb_net::grecv_get1msg_ifdata = sub { return; };
    local *check_mp_connection = sub {};
    new_game_once();
    new_game();
    while (1) {
        eval { maingame() };
        if ($@) {
            if ($@ =~ /^quit/ || $@ =~ /^new_game/) {
                last;
            } else {
                die;
            }
        }
    }
}

#- ----------- main -------------------------------------------------------

init_game();

if ($replayparam) {
    replay($replayparam);
    $replayparam = undef;
}

if (!$direct) {
    menu('first time');
}

while (!new_game_once()) { menu() }
new_game() or goto go_to_menu;

while (1) {
    eval { maingame() };
    if ($@) {
        my $died = $@;
        my $recorded = save_record_if_needed();
        if ($start_time != 0) {
            $addicted_time += $app->ticks - $start_time;
            $start_time = 0;
        }

	if ($died =~ /^new_game/) {
            new_game() or goto go_to_menu;

	} elsif ($died =~ /^quit/) {
            if (is_mp_game()) {
                if ($pdata{gametype} eq 'lan') {
                    fb_net::disconnect();
                    show_mp_scores();
                    grab_key();
                    goto go_to_menu;
                } else {
                    if (fb_net::reconnect() && new_game_once()) {
                        new_game() or goto go_to_menu;
                    } else {
                        goto go_to_menu;
                    }
                }
            } else {
              go_to_menu:
                do { menu() } while (!new_game_once());
                new_game();
            }

	} else {
            if (!$recorded) {
                print "\n******************************************************************************\n"
                    . "* Unexpected abort in process! Forcing record: please send the record file to FB authors so that they fix the bug if possible!\n"
                    . "******************************************************************************\n\n";
                $autorecord = 1;
                save_record_if_needed();
            } else {
                print "\n******************************************************************************\n"
                    . "* Unexpected abort in process! Please send the record file to FB authors so that they fix the bug if possible!\n"
                    . "******************************************************************************\n\n";
            }
	    die $died;
	}
    }
}
