/* * Copyright (C) 2004 Daniel Heck * Copyright (C) 2006, 2007 Ronald Lamprecht * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * 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., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "client.hh" #include "game.hh" #include "display.hh" #include "options.hh" #include "server.hh" #include "gui/HelpMenu.hh" #include "main.hh" #include "gui/GameMenu.hh" #include "sound.hh" #include "player.hh" #include "world.hh" #include "nls.hh" #include "StateManager.hh" #include "lev/Index.hh" #include "lev/PersistentIndex.hh" #include "lev/Proxy.hh" #include "lev/RatingManager.hh" #include "lev/ScoreManager.hh" #include "ecl_sdl.hh" #include "enet/enet.h" #include #include #include #include #include using namespace enigma::client; using namespace ecl; using namespace std; #ifdef ANDROID #include #endif #include "client_internal.hh" /* -------------------- Auxiliary functions -------------------- */ namespace { /*! Display a message and change the current mouse speed. */ void set_mousespeed (double speed) { int s = round_nearest(speed); options::SetMouseSpeed (s); s = round_nearest (options::GetMouseSpeed ()); Msg_ShowText(strf(_("Mouse speed: %d"), s), false, 2.0); } /*! Generate the message that is displayed when the level starts. */ string displayedLevelInfo (lev::Proxy *level) { std::string text; std::string tmp; tmp = level->getLocalizedString("title"); if (tmp.empty()) tmp = _("Another nameless level"); text = string("\"")+ tmp +"\""; tmp = level->getAuthor(); if (!tmp.empty()) text += _(" by ") + tmp; tmp = level->getLocalizedString("subtitle"); if (!tmp.empty() && tmp != "subtitle") text += string(" - \"")+ tmp + "\""; tmp = level->getCredits(false); if (!tmp.empty()) text += string(" - Credits: ")+ tmp; tmp = level->getDedication(false); if (!tmp.empty()) text += string(" - Dedication: ")+ tmp; return text; } } /* -------------------- Variables -------------------- */ namespace { Client client_instance; const char HSEP = '^'; // history separator (use character that user cannot use) } #define CLIENT client_instance /* -------------------- Client class -------------------- */ Client::Client() : m_state (cls_idle), m_levelname(), m_hunt_against_time(0), m_cheater(false), m_user_input() { m_network_host = 0; } Client::~Client() { network_stop(); } bool Client::network_start() { if (m_network_host) return true; m_network_host = enet_host_create (NULL, 1 /* only allow 1 outgoing connection */, 57600 / 8 /* 56K modem with 56 Kbps downstream bandwidth */, 14400 / 8 /* 56K modem with 14 Kbps upstream bandwidth */); if (m_network_host == NULL) { fprintf (stderr, "An error occurred while trying to create an ENet client host.\n"); return false; } // ----- Connect to server ENetAddress sv_address; ENetPeer *m_server; /* Connect to some.server.net:1234. */ enet_address_set_host (&sv_address, "localhost"); sv_address.port = 12345; /* Initiate the connection, allocating the two channels 0 and 1. */ m_server = enet_host_connect (m_network_host, &sv_address, 2); if (m_server == NULL) { fprintf (stderr, "No available peers for initiating an ENet connection.\n"); return false; } // Wait up to 5 seconds for the connection attempt to succeed. ENetEvent event; if (enet_host_service (m_network_host, &event, 5000) > 0 && event.type == ENET_EVENT_TYPE_CONNECT) { fprintf (stderr, "Connection to some.server.net:1234 succeeded."); return true; } else { /* Either the 5 seconds are up or a disconnect event was */ /* received. Reset the peer in the event the 5 seconds */ /* had run out without any significant event. */ enet_peer_reset (m_server); m_server = 0; fprintf (stderr, "Connection to localhost:12345 failed."); return false; } } void Client::network_stop () { if (m_network_host) enet_host_destroy (m_network_host); if (m_server) enet_peer_reset (m_server); } /* ---------- Event handling ---------- */ void Client::handle_events() { SDL_Event e; while (SDL_PollEvent(&e)) { switch (e.type) { case SDL_KEYDOWN: on_keydown(e); break; case SDL_MOUSEMOTION: #ifndef ANDROID if (abs(e.motion.xrel) > 300 || abs(e.motion.yrel) > 300) { fprintf(stderr, "mouse event with %i, %i\n", e.motion.xrel, e.motion.yrel); } else server::Msg_MouseForce (options::GetDouble("MouseSpeed") * V2 (e.motion.xrel, e.motion.yrel)); #endif break; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: on_mousebutton(e); break; case SDL_ACTIVEEVENT: { update_mouse_button_state(); if (e.active.gain == 0 && !video::IsFullScreen()) show_menu(); break; } case SDL_VIDEOEXPOSE: { display::RedrawAll(video::GetScreen()); break; } case SDL_QUIT: client::Msg_Command("abort"); break; } } } void Client::update_mouse_button_state() { int b = SDL_GetMouseState(0, 0); player::InhibitPickup((b & SDL_BUTTON(1)) || (b & SDL_BUTTON(3))); } void Client::on_mousebutton(SDL_Event &e) { if (e.button.state == SDL_PRESSED) { if (e.button.button == 1) { // left mousebutton -> activate first item in inventory server::Msg_ActivateItem (); } else if (e.button.button == 3|| e.button.button == 4) { // right mousebutton, wheel down -> rotate inventory rotate_inventory(+1); } else if (e.button.button == 5) { // wheel down -> inverse rotate inventory rotate_inventory(-1); } } update_mouse_button_state(); } void Client::rotate_inventory (int direction) { m_user_input = ""; STATUSBAR->hide_text(); player::RotateInventory(direction); } /* -------------------- Console related -------------------- */ class HistoryProxy { static int instances; public: static string content; HistoryProxy(); ~HistoryProxy() { if (!--instances) app.state->setProperty("CommandHistory", content); } }; string HistoryProxy::content; int HistoryProxy::instances = 0; HistoryProxy::HistoryProxy() { if (!instances++) { content = app.state->getString("CommandHistory"); if (content.find(HSEP) == string::npos) content = string(1, HSEP); } } static void user_input_history_append(const string& text, bool at_end = true) { HistoryProxy history; size_t old_pos = history.content.find(string(1, HSEP)+text+HSEP); if (old_pos != string::npos) history.content.erase(old_pos, text.length()+1); if (at_end) history.content += text+HSEP; else history.content = string(1, HSEP)+text+history.content; } void Client::process_userinput() { if (m_user_input != "") { STATUSBAR->hide_text(); string commands = m_user_input; user_input_history_append(m_user_input); m_user_input = ""; size_t sep_pos; while ((sep_pos = commands.find_first_of(';')) != string::npos) { string first_command = commands.substr(0, sep_pos); commands.erase(0, sep_pos+1); server::Msg_Command (first_command); } server::Msg_Command (commands); // last command } } void Client::user_input_append (char c) { m_user_input += c; Msg_ShowText (m_user_input, false); } void Client::user_input_backspace () { if (!m_user_input.empty()) { m_user_input.erase (m_user_input.size()-1, 1); if (!m_user_input.empty()) { // still not empty Msg_ShowText (m_user_input, false); } else { // empty STATUSBAR->hide_text(); } } } void Client::user_input_previous () { HistoryProxy history; size_t last_start = history.content.find_last_of(HSEP, history.content.length()-2); if (last_start != string::npos) { string prev_input = history.content.substr(last_start+1, history.content.length()-last_start-2); history.content.erase(last_start+1); user_input_history_append(m_user_input, false); m_user_input = prev_input; if (m_user_input.empty()) STATUSBAR->hide_text(); else Msg_ShowText (m_user_input, false); } } void Client::user_input_next () { HistoryProxy history; size_t first_end = history.content.find_first_of(HSEP, 1); if (first_end != string::npos) { string next_input = history.content.substr(1, first_end-1); history.content.erase(0, first_end); user_input_history_append(m_user_input); m_user_input = next_input; if (m_user_input.empty()) STATUSBAR->hide_text(); else Msg_ShowText (m_user_input, false); } } void Client::on_keydown(SDL_Event &e) { SDLKey keysym = e.key.keysym.sym; SDLMod keymod = e.key.keysym.mod; if (keymod & KMOD_CTRL) { switch (keysym) { case SDLK_a: server::Msg_Command ("restart"); break; case SDLK_F3: if (keymod & KMOD_SHIFT) { // force a reload from file lev::Proxy * curProxy = lev::Proxy::loadedLevel(); if (curProxy != NULL) curProxy->release(); server::Msg_Command ("restart"); } default: break; }; } else if (keymod & KMOD_ALT) { switch (keysym) { case SDLK_x: abort(); break; case SDLK_t: if (enigma::WizardMode) { Screen *scr = video::GetScreen(); ecl::TintRect(scr->get_surface (), display::GetGameArea(), 100, 100, 100, 0); scr->update_all(); } break; case SDLK_s: if (enigma::WizardMode) { server::Msg_Command ("god"); } break; case SDLK_RETURN: { video::TempInputGrab (false); video::ToggleFullscreen (); sdl::FlushEvents(); } break; default: break; }; } else { switch (keysym) { case SDLK_ESCAPE: show_menu(); break; case SDLK_LEFT: set_mousespeed(options::GetMouseSpeed() - 1); break; case SDLK_RIGHT: set_mousespeed(options::GetMouseSpeed() + 1); break; #ifdef ANDROID case SDLK_LCTRL: #endif case SDLK_TAB: rotate_inventory(+1); break; case SDLK_F1: show_help(); break; case SDLK_F2: // display hint break; case SDLK_F3: if (keymod & KMOD_SHIFT) server::Msg_Command ("restart"); else server::Msg_Command ("suicide"); break; case SDLK_F4: Msg_AdvanceLevel(lev::ADVANCE_STRICTLY); break; case SDLK_F5: Msg_AdvanceLevel(lev::ADVANCE_UNSOLVED); break; case SDLK_F6: Msg_JumpBack(); break; case SDLK_F10: { lev::Proxy *level = lev::Proxy::loadedLevel(); std::string basename = std::string("screenshots/") + level->getLocalSubstitutionLevelPath(); std::string fname = basename + ".png"; std::string fullPath; int i = 1; while (app.resourceFS->findFile(fname, fullPath)) { fname = basename + ecl::strf("#%d", i++) + ".png"; } std::string savePath = app.userImagePath + "/" + fname; video::Screenshot(savePath); break; } case SDLK_RETURN: process_userinput(); break; case SDLK_BACKSPACE: user_input_backspace(); break; case SDLK_UP: user_input_previous(); break; case SDLK_DOWN: user_input_next(); break; default: if (e.key.keysym.unicode && (e.key.keysym.unicode & 0xff80) == 0) { char ascii = static_cast(e.key.keysym.unicode & 0x7f); if (isalnum (ascii) || strchr(" .-!\"$%&/()=?{[]}\\#'+*~_,;.:<>|", ascii)) // don't add '^' or change history code { user_input_append(ascii); } } break; } } } static const char *helptext_ingame[] = { N_("Left mouse button:"), N_("Activate/drop leftmost inventory item"), N_("Right mouse button:"), N_("Rotate inventory items"), N_("Escape:"), N_("Show game menu"), N_("F1:"), N_("Show this help"), N_("F3:"), N_("Kill current marble"), N_("Shift+F3:"), N_("Restart the current level"), N_("F4:"), N_("Skip to next level"), N_("F5:"), 0, // see below N_("F6:"), N_("Jump back to last level"), N_("F10:"), N_("Make screenshot"), N_("Left/right arrow:"), N_("Change mouse speed"), N_("Alt+x:"), N_("Return to level menu"), // N_("Alt+Return:"), N_("Switch between fullscreen and window"), 0 }; void Client::show_help() { server::Msg_Pause (true); video::TempInputGrab grab(false); helptext_ingame[15] = app.state->getInt("NextLevelMode") == lev::NEXT_LEVEL_NOT_BEST ? _("Skip to next level for best score hunt") : _("Skip to next unsolved level"); video::ShowMouse(); gui::displayHelp(helptext_ingame, 200); video::HideMouse(); update_mouse_button_state(); if (m_state == cls_game) display::RedrawAll(video::GetScreen()); server::Msg_Pause (false); game::ResetGameTimer(); if (app.state->getInt("NextLevelMode") == lev::NEXT_LEVEL_NOT_BEST) server::Msg_Command ("restart"); // inhibit cheating } void Client::show_menu() { server::Msg_Pause (true); ecl::Screen *screen = video::GetScreen(); video::TempInputGrab grab (false); video::ShowMouse(); { int x, y; display::GetReferencePointCoordinates(&x, &y); enigma::gui::GameMenu(x, y).manage(); } video::HideMouse(); update_mouse_button_state(); if (m_state == cls_game) display::RedrawAll(screen); server::Msg_Pause (false); game::ResetGameTimer(); } void Client::draw_screen() { switch (m_state) { case cls_error: { Screen *scr = video::GetScreen(); GC gc (scr->get_surface()); blit(gc, 0,0, enigma::GetImage("menu_bg", ".jpg")); Font *f = enigma::GetFont("menufont"); vector lines; ecl::split_copy (m_error_message, '\n', back_inserter(lines)); int x = 60; int y = 60; int yskip = 25; const video::VMInfo *vminfo = video::GetInfo(); int width = vminfo->width - 120; for (unsigned i=0; irender(gc, x, y, lines[i].substr(0,breakPos).c_str()); y += yskip; if (breakPos != lines[i].size()) { // process rest of line lines[i] = lines[i].substr(breakPos); } else { // process next line i++; } } scr->update_all(); scr->flush_updates(); break; } default: break; } } std::string Client::init_hunted_time() { std::string hunted; m_hunt_against_time = 0; if (app.state->getInt("NextLevelMode") == lev::NEXT_LEVEL_NOT_BEST) { lev::Index *ind = lev::Index::getCurrentIndex(); lev::ScoreManager *scm = lev::ScoreManager::instance(); lev::Proxy *curProxy = ind->getCurrent(); lev::RatingManager *ratingMgr = lev::RatingManager::instance(); int difficulty = app.state->getInt("Difficulty"); int wr_time = ratingMgr->getBestScore(curProxy, difficulty); int best_user_time = scm->getBestUserScore(curProxy, difficulty); if (best_user_time>0 && (wr_time == -1 || best_user_time0) { m_hunt_against_time = wr_time; hunted = ratingMgr->getBestScoreHolder(curProxy, difficulty); } // STATUSBAR->set_timerstart(-m_hunt_against_time); } return hunted; } void Client::tick (double dtime) { const double timestep = 0.01; // 10ms #ifdef ANDROID Sint32 joy_x, joy_y; SDL_Joystick *joy; #endif switch (m_state) { case cls_idle: break; case cls_preparing_game: { #ifdef ANDROID // calibrate the orientation sensor, using the current position as zero // TODO: average the values over some period of time? joy = SDL_JoystickOpen(0); SDL_JoystickUpdate(); if(joy != NULL) { m_joy_x0 = SDL_JoystickGetAxis(joy,0); m_joy_y0 = SDL_JoystickGetAxis(joy,1); } #endif video::TransitionEffect *fx = m_effect.get(); if (fx && !fx->finished()) { fx->tick (dtime); } else { m_effect.reset(); server::Msg_StartGame(); m_state = cls_game; m_timeaccu = 0; m_total_game_time = 0; sdl::FlushEvents(); } break; } case cls_game: #ifdef ANDROID // joystick/accelerometer control joy = SDL_JoystickOpen(0); SDL_JoystickUpdate(); if(joy != NULL) { joy_x = SDL_JoystickGetAxis(joy,0) - m_joy_x0; joy_y = SDL_JoystickGetAxis(joy,1) - m_joy_y0; server::Msg_MouseForce(options::GetDouble("MouseSpeed") * -dtime/3000.0 * V2 (joy_x*sqrt(abs(joy_x)), joy_y*sqrt(abs(joy_y)))); // use joy**1.5 to allow more flexible (non-linear) control } #endif if (app.state->getInt("NextLevelMode") == lev::NEXT_LEVEL_NOT_BEST) { int old_second = round_nearest (m_total_game_time); int second = round_nearest (m_total_game_time + dtime); if (m_hunt_against_time && old_second <= m_hunt_against_time) { if (second > m_hunt_against_time) { // happens exactly once when par has passed by lev::Index *ind = lev::Index::getCurrentIndex(); lev::ScoreManager *scm = lev::ScoreManager::instance(); lev::Proxy *curProxy = ind->getCurrent(); lev::RatingManager *ratingMgr = lev::RatingManager::instance(); int difficulty = app.state->getInt("Difficulty"); int wr_time = ratingMgr->getBestScore(curProxy, difficulty); int best_user_time = scm->getBestUserScore(curProxy, difficulty); string message; if (wr_time>0 && (best_user_time<0 || best_user_time>wr_time)) { message = string(_("Too slow for ")) + ratingMgr->getBestScoreHolder(curProxy, difficulty) + ".. [Ctrl-A]"; } else { message = string(_("You are slow today.. [Ctrl-A]")); } client::Msg_PlaySound("shatter", 1.0); Msg_ShowText(message, true, 2.0); } else { if (old_second= (m_hunt_against_time-5) || // at least 5 seconds second >= round_nearest (m_hunt_against_time*.8))) // or the last 20% before par { client::Msg_PlaySound("pickup", 1.0); } } } } m_total_game_time += dtime; STATUSBAR->set_time (m_total_game_time); // fall through case cls_finished: { m_timeaccu += dtime; for (;m_timeaccu >= timestep; m_timeaccu -= timestep) { display::Tick (timestep); } display::Redraw(video::GetScreen()); handle_events(); break; } case cls_gamemenu: break; case cls_gamehelp: break; case cls_abort: break; case cls_error: { SDL_Event e; while (SDL_PollEvent(&e)) { switch (e.type) { case SDL_KEYDOWN: case SDL_QUIT: client::Msg_Command("abort"); break; } } } break; } } void Client::level_finished() { lev::Index *ind = lev::Index::getCurrentIndex(); lev::ScoreManager *scm = lev::ScoreManager::instance(); lev::Proxy *curProxy = ind->getCurrent(); lev::RatingManager *ratingMgr = lev::RatingManager::instance(); int difficulty = app.state->getInt("Difficulty"); int wr_time = ratingMgr->getBestScore(curProxy, difficulty); int best_user_time = scm->getBestUserScore(curProxy, difficulty); string par_name = ratingMgr->getBestScoreHolder(curProxy, difficulty); int par_time = ratingMgr->getParScore(curProxy, difficulty); int level_time = round_nearest (m_total_game_time); string text; bool timehunt_restart = false; if (wr_time > 0) { if (best_user_time<0 || best_user_time>wr_time) { if (level_time == wr_time) text = string(_("Exactly the world record of "))+par_name+"!"; else if (level_time0) { if (level_time == best_user_time) { text = _("Again your personal record..."); if (app.state->getInt("NextLevelMode") == lev::NEXT_LEVEL_NOT_BEST) timehunt_restart = true; // when hunting yourself: Equal is too slow } else if (level_time= 0 && level_time <= par_time) text = _("New personal record - better than par!"); else if (par_time >= 0) text = _("New personal record, but over par!"); else text = _("New personal record!"); } if (app.state->getInt("NextLevelMode") == lev::NEXT_LEVEL_NOT_BEST && (wr_time>0 || best_user_time>0)) { bool with_par = best_user_time == -1 || (wr_time >0 && wr_time0) { if (best_user_time>0 && level_time (behind/60)%100, behind%60); if (with_par) text += _("behind world record."); else text += _("behind your record."); timehunt_restart = true; // time hunt failed -> repeat level } } if (text.length() == 0) { if (par_time >= 0 && level_time <= par_time) text = _("Level finished - better than par!"); else if (par_time >= 0) text = _("Level finished, but over par!"); else text = _("Level finished!"); } if (m_cheater) text += _(" Cheater!"); Msg_ShowText (text, false); if (!m_cheater) { scm->updateUserScore(curProxy, difficulty, level_time); // save score (just in case Enigma crashes when loading next level) lev::ScoreManager::instance()->save(); } if (timehunt_restart) server::Msg_Command("restart"); else m_state = cls_finished; } #ifdef _MSC_VER #define snprintf _snprintf #endif void Client::level_loaded(bool isRestart) { lev::Index *ind = lev::Index::getCurrentIndex(); lev::ScoreManager *scm = lev::ScoreManager::instance(); lev::Proxy *curProxy = ind->getCurrent(); // update window title video::SetCaption(ecl::strf(_("Enigma pack %s - level #%d: %s"), ind->getName().c_str(), ind->getCurrentLevel(), curProxy->getTitle().c_str()).c_str()); string hunted = init_hunted_time(); // sets m_hunt_against_time (used below) // show level information (name, author, etc.) { string displayed_info = ""; if (m_hunt_against_time>0) { if (hunted == "you") displayed_info = _("Your record: "); else displayed_info = _("World record to beat: "); displayed_info += ecl::strf("%d:%02d", (m_hunt_against_time/60)%100, m_hunt_against_time%60); //+ _(" by ") +hunted; // makes the string too long in many levels Msg_ShowText (displayed_info, true, 4.0); } else { displayed_info = displayedLevelInfo(curProxy); Msg_ShowText (displayed_info, true, 2.0); } } sound::FadeoutMusic(); if (options::GetBool("InGameMusic")) { sound::PlayMusic (options::GetString("LevelMusicFile")); } else { sound::StopMusic(); } // start screen transition GC gc(video::BackBuffer()); display::DrawAll(gc); m_effect.reset (video::MakeEffect ((isRestart ? video::TM_SQUARES : video::TM_PUSH_RANDOM), video::BackBuffer())); m_cheater = false; m_state = cls_preparing_game; } void Client::handle_message (Message *m) { // @@@ unused switch (m->type) { case CLMSG_LEVEL_LOADED: break; default: fprintf (stderr, "Unhandled client event: %d\n", m->type); break; } } void Client::error (const string &text) { m_error_message = text; m_state = cls_error; draw_screen(); } /* -------------------- Functions -------------------- */ bool client::NetworkStart() { return CLIENT.network_start(); } void client::Msg_LevelLoaded(bool isRestart) { CLIENT.level_loaded(isRestart); } void client::Tick (double dtime) { CLIENT.tick (dtime); sound::Tick (dtime); } void client::Stop() { CLIENT.stop (); } void client::Msg_AdvanceLevel (lev::LevelAdvanceMode mode) { lev::Index *ind = lev::Index::getCurrentIndex(); // log last played level lev::PersistentIndex::addCurrentToHistory(); if (ind->advanceLevel(mode)) { // now we may advance server::Msg_LoadLevel(ind->getCurrent(), false); } else client::Msg_Command("abort"); } void client::Msg_JumpBack() { // log last played level lev::PersistentIndex::addCurrentToHistory(); server::Msg_JumpBack(); } bool client::AbortGameP() { return CLIENT.abort_p(); } void client::Msg_Command(const string& cmd) { if (cmd == "abort") { CLIENT.abort(); } else if (cmd == "level_finished") { client::Msg_PlaySound("finished", 1.0); CLIENT.level_finished(); } else if (cmd == "cheater") { CLIENT.mark_cheater(); } else if (cmd == "easy_going") { CLIENT.easy_going(); } else { enigma::Log << "Warning: Client received unknown command '" << cmd << "'\n"; } } void client::Msg_PlayerPosition (unsigned iplayer, const V2 &pos) { if (iplayer == (unsigned)player::CurrentPlayer()) { sound::SetListenerPosition (pos); display::SetReferencePoint (pos); } } void client::Msg_PlaySound (const std::string &wavfile, const ecl::V2 &pos, double relative_volume) { sound::EmitSoundEvent (wavfile.c_str(), pos, relative_volume); } void client::Msg_PlaySound (const std::string &wavfile, double relative_volume) { sound::EmitSoundEvent (wavfile.c_str(), V2(), relative_volume); } void client::Msg_Sparkle (const ecl::V2 &pos) { display::AddEffect (pos, "ring-anim"); } void client::Msg_ShowText (const std::string &text, bool scrolling, double duration) { STATUSBAR->show_text (text, scrolling, duration); } void client::Msg_Error (const std::string &text) { CLIENT.error (text); }