Minor changes: next_filename() is now in filesystem.cpp and more robust against unexpected filenames, and the seg fault that Boucman found when watching MP games is fixed.
http://stats.wesnoth.org is starting to look more useful, but the graphs are going to need more love, eg. HttT has too many scenarios to fit the full graph on the screen: http://stats.wesnoth.org/?W_PLAYER=1&W_VERSION=1.1-svn You'll need a browser that understands SVG (or a plugin). Cheers! Rusty. Index: src/upload_log.cpp =================================================================== --- src/upload_log.cpp (revision 0) +++ src/upload_log.cpp (revision 0) @@ -0,0 +1,288 @@ +/* $Id$ */ +/* + Copyright (C) 2005 by Rusty Russell <[EMAIL PROTECTED]> + Part of the Battle for Wesnoth Project http://www.wesnoth.org/ + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY. + + See the COPYING file for more details. +*/ +#include "global.hpp" + +#define GETTEXT_DOMAIN "wesnoth" + +#include "gettext.hpp" +#include "filesystem.hpp" +#include "preferences.hpp" +#include "serialization/parser.hpp" +#include "show_dialog.hpp" +#include "upload_log.hpp" +#include "wesconfig.h" +#include "wml_separators.hpp" + +#include "SDL_net.h" + +#include <vector> +#include <string> + +#define TARGET_HOST "stats.wesnoth.org" +#define TARGET_URL "/upload.cgi" +#define TARGET_PORT 80 + +struct upload_log::thread_info upload_log::thread_; + +// On exit, kill the upload thread if it's still going. +upload_log::manager::~manager() +{ + threading::thread *t = thread_.t; + if (t) + t->kill(); +} + +static void send_string(TCPsocket sock, const std::string &str) +{ + if (SDLNet_TCP_Send(sock, (void *)str.c_str(), str.length()) + != str.length()) { + throw network::error(""); + } +} + +// Function which runs in a background thread to upload logs to server. +// Uses http POST to port 80 for maximum firewall penetration & other-end +// compatibility. +static int upload_logs(void *_ti) +{ + TCPsocket sock = NULL; + upload_log::thread_info *ti = (upload_log::thread_info *)_ti; + + const char *header = + "POST " TARGET_URL " HTTP/1.1\n" + "Host: " TARGET_HOST "\n" + "User-Agent: Wesnoth " VERSION "\n" + "Content-Type: text/plain\n"; + + try { + std::vector<std::string> files; + + // These are sorted: send them one at a time until we get to lastfile. + get_files_in_dir(get_upload_dir(), &files, NULL, ENTIRE_FILE_PATH); + + IPaddress ip; + if (SDLNet_ResolveHost(&ip, TARGET_HOST, TARGET_PORT) == 0) { + std::vector<std::string>::iterator i; + for (i = files.begin(); i!=files.end() && *i!=ti->lastfile; i++) { + std::string contents; + int resplen; + char response[strlen("HTTP/1.1 2")]; + + contents = read_file(*i); + + sock = SDLNet_TCP_Open(&ip); + if (!sock) + break; + send_string(sock, header); + send_string(sock, "Content-length: "); + send_string(sock, lexical_cast<std::string>(contents.length())); + send_string(sock, "\n\n"); + send_string(sock, contents.c_str()); + + if (SDLNet_TCP_Recv(sock, response, sizeof(response)) + != sizeof(response)) + break; + // Must be version 1.x, must start with 2 (eg. 200) for success + if (memcmp(response, "HTTP/1.", strlen("HTTP/1.")) != 0) + break; + if (memcmp(response+8, " 2", strlen(" 2")) != 0) + break; + + delete_directory(*i); + SDLNet_TCP_Close(sock); + sock = NULL; + } + } + } catch(...) { } + + if (sock) + SDLNet_TCP_Close(sock); + ti->t = NULL; + return 0; +} + +// Currently only enabled when playing campaigns. +upload_log::upload_log(bool enable) : game_(NULL), enabled_(enable) +{ + filename_ = next_filename(get_upload_dir()); + if (preferences::upload_log() && !thread_.t) { + // Thread can outlive us; it uploads everything up to the next + // filename, and unsets thread_.t when it's finished. + thread_.lastfile = filename_; + thread_.t = new threading::thread(upload_logs, &thread_); + } + + config_["version"] = VERSION; + config_["format_version"] = "1"; + config_["id"] = preferences::upload_id(); +} + +upload_log::~upload_log() +{ + // If last game has a conclusion, add it. + if (game_finished(game_)) + config_.add_child("game", *game_); + + if (enabled_ && !config_.empty()) { + std::ostream *out = ostream_file(filename_); + write(*out, config_); + delete out; + + // Try to upload latest log before exit. + if (preferences::upload_log() && !thread_.t) { + thread_.lastfile = next_filename(get_upload_dir()); + thread_.t = new threading::thread(upload_logs, &thread_); + } + } +} + +bool upload_log::game_finished(config *game) +{ + if (!game) + return false; + + return game->child("victory") || game->child("defeat") || game->child("quit"); +} + +config &upload_log::add_game_result(const std::string &str, int turn) +{ + config &child = game_->add_child(str); + child["time"] = lexical_cast<std::string>(SDL_GetTicks() / 1000); + child["end_turn"] = lexical_cast<std::string>(turn); + return child; +} + +// User starts a game (may be new campaign or saved). +void upload_log::start(game_state &state, const team &team, + int team_number, + const unit_map &units, + const t_string &turn, + int num_turns) +{ + const config *player_conf; + std::vector<const unit*> all_units; + + // If we have a previous game which is finished, add it. + if (game_finished(game_)) + config_.add_child("game", *game_); + + game_ = new config(); + (*game_)["time"] = lexical_cast<std::string>(SDL_GetTicks() / 1000); + (*game_)["campaign"] = state.campaign_define; + (*game_)["difficulty"] = state.difficulty; + (*game_)["scenario"] = state.scenario; + if (!state.version.empty()) + (*game_)["version"] = state.version; + if (!turn.empty()) + (*game_)["start_turn"] = turn; + (*game_)["gold"] = lexical_cast<std::string>(team.gold()); + (*game_)["num_turns"] = lexical_cast<std::string>(num_turns); + + // We seem to have to walk the map to find some units, and the player's + // available_units for the rest. + for (unit_map::const_iterator un = units.begin(); un != units.end(); ++un){ + if (un->second.side() == team_number) { + all_units.push_back(&un->second); + } + } + + // FIXME: Assumes first player is "us"; is that valid? + player_info &player = state.players.begin()->second; + for (std::vector<unit>::iterator it = player.available_units.begin(); + it != player.available_units.end(); + ++it) { + all_units.push_back(&*it); + } + + // Record details of any special units. + std::vector<const unit*>::const_iterator i; + for (i = all_units.begin(); i != all_units.end(); ++i) { + if ((*i)->can_recruit()) { + config &sp = game_->add_child("special-unit"); + sp["name"] = (*i)->name(); + sp["level"] = lexical_cast<std::string>((*i)->type().level()); + sp["experience"] = lexical_cast<std::string>((*i)->experience()); + } + } + + // Record summary of all units. + config &summ = game_->add_child("units-by-level"); + bool higher_units = true; + for (int level = 0; higher_units; level++) { + std::map<std::string, int> tally; + + higher_units = false; + for (i = all_units.begin(); i != all_units.end(); ++i) { + if ((*i)->type().level() > level) + higher_units = true; + else if ((*i)->type().level() == level) { + if (tally.find((*i)->type().id()) == tally.end()) + tally[(*i)->type().id()] = 1; + else + tally[(*i)->type().id()]++; + } + } + if (!tally.empty()) { + config &tc = summ.add_child(lexical_cast<std::string>(level)); + for (std::map<std::string, int>::iterator t = tally.begin(); + t != tally.end(); + t++) { + config &uc = tc.add_child(t->first); + uc["count"] = lexical_cast<std::string>(t->second); + } + } + } +} + +// User finishes a scenario. +void upload_log::defeat(int turn) +{ + add_game_result("defeat", turn); +} + +void upload_log::victory(int turn, int gold) +{ + config &e = add_game_result("victory", turn); + e["gold"] = lexical_cast<std::string>(gold); +} + +void upload_log::quit(int turn) +{ + std::string turnstr = lexical_cast<std::string>(turn); + + // We only record the quit if they've actually played a turn. + if (!game_ || game_->get_attribute("start_turn") == turnstr || turn == 1) + return; + + add_game_result("quit", turn); +} + +void upload_log_dialog::show_beg_dialog(display& disp) +{ + std::vector<gui::check_item> options; + + options.push_back(gui::check_item("Enable summary uploads", + preferences::upload_log())); + + std::string msg = std::string(_("Wesnoth relies on volunteers like yourself for feedback, especially beginners and new players. Wesnoth keeps summaries of your games: you can help us improve game play by giving permission to send these summaries (anonymously) to wesnoth.org.\n")) + + _("You can see the summaries to be sent in ") + + get_upload_dir() + "\n" + + _("You can view the results at http://stats.wesnoth.org.\n"); + + gui::show_dialog(disp, NULL, + _("Help us make Wesnoth better for you!"), + msg, + gui::OK_ONLY, + NULL, NULL, "", NULL, 0, NULL, &options); + preferences::set_upload_log(options.front().checked); +} Index: src/playlevel.cpp =================================================================== --- src/playlevel.cpp (revision 9403) +++ src/playlevel.cpp (working copy) @@ -19,7 +19,7 @@ //#include "dialogs.hpp" //#include "events.hpp" #include "filesystem.hpp" -//#include "game_errors.hpp" +#include "game_errors.hpp" //#include "gamestatus.hpp" #include "gettext.hpp" #include "game_events.hpp" @@ -42,6 +42,7 @@ #include "statistics.hpp" #include "tooltips.hpp" //#include "unit_display.hpp" +#include "upload_log.hpp" //#include "util.hpp" //#include "video.hpp" @@ -118,7 +119,8 @@ LEVEL_RESULT play_level(const game_data& gameinfo, const config& game_config, config const* level, CVideo& video, game_state& state_of_game, - const std::vector<config*>& story) + const std::vector<config*>& story, + upload_log &log) { //if the recorder has no event, adds an "game start" event to the //recorder, whose only goal is to initialize the RNG @@ -304,6 +306,14 @@ //instead of starting a fresh one const bool loading_game = lvl["playing_team"].empty() == false; + // log before prestart events: they do weird things. + if (first_human_team != -1) { + log.start(state_of_game, teams[first_human_team], + first_human_team+1, units, + loading_game ? state_of_game.get_variable("turn_number") : "", + num_turns); + } + //pre-start events must be executed before any GUI operation, //as those may cause the display to be refreshed. if(!loading_game) { @@ -584,6 +594,10 @@ } } //end for loop + } catch(game::load_game_exception& e) { + // Loading a new game is effectively a quit. + log.quit(status.turn()); + throw; } catch(end_level_exception& end_level) { bool obs = team_manager.is_observer(); if (end_level.result == DEFEAT || end_level.result == VICTORY) { @@ -603,8 +617,10 @@ } if(end_level.result == QUIT) { + log.quit(status.turn()); return end_level.result; } else if(end_level.result == DEFEAT) { + log.defeat(status.turn()); try { game_events::fire("defeat"); } catch(end_level_exception&) { @@ -623,6 +639,10 @@ } catch(end_level_exception&) { } + if (end_level.result == VICTORY && first_human_team != -1) { + log.victory(status.turn(), teams[first_human_team].gold()); + } + if(state_of_game.scenario == (*level)["id"]) { state_of_game.scenario = (*level)["next_scenario"]; } Index: src/filesystem.cpp =================================================================== --- src/filesystem.cpp (revision 9403) +++ src/filesystem.cpp (working copy) @@ -54,6 +54,8 @@ #include <algorithm> #include <fstream> #include <iostream> +#include <iomanip> +#include <sstream> #include <set> #include "wesconfig.h" @@ -68,7 +70,6 @@ #define ERR_FS LOG_STREAM(err, filesystem) #ifdef USE_ZIPIOS -#include <sstream> #include <zipios++/collcoll.h> #include <zipios++/dircoll.h> #include <zipios++/zipfile.h> @@ -303,6 +304,12 @@ return get_dir(dir_path); } +std::string get_upload_dir() +{ + const std::string dir_path = get_user_data_dir() + "/upload"; + return get_dir(dir_path); +} + std::string get_dir(const std::string& dir_path) { #ifdef _WIN32 @@ -617,6 +624,32 @@ return buf.st_mtime; } +//return the next ordered full filename within this directory +std::string next_filename(const std::string &dirname) +{ + std::vector<std::string> files; + std::stringstream fname; + unsigned int num = 1; + + // These are sorted, so we can simply add one to last one. + get_files_in_dir(dirname, &files); + + // Make sure we skip over any files we didn't create ourselves. + std::vector<std::string>::reverse_iterator i; + for (i = files.rbegin(); i != files.rend(); ++i) { + if (i->length() == 8) { + try { + num = lexical_cast<int>(*i)+1; + break; + } catch (bad_lexical_cast &c) { + } + } + } + + fname << std::setw(8) << std::setfill('0') << num; + return dirname + "/" + fname.str(); +} + file_tree_checksum::file_tree_checksum() : nfiles(0), sum_size(0), modified(0) {} Index: src/upload_log.hpp =================================================================== --- src/upload_log.hpp (revision 0) +++ src/upload_log.hpp (revision 0) @@ -0,0 +1,69 @@ +/* $Id$ */ +/* + Copyright (C) 2005 by Rusty Russell <[EMAIL PROTECTED]> + Part of the Battle for Wesnoth Project http://www.wesnoth.org/ + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY. + + See the COPYING file for more details. +*/ + +#ifndef UPLOAD_LOG_H_INCLUDED +#define UPLOAD_LOG_H_INCLUDED +#include "config.hpp" +#include "display.hpp" +#include "gamestatus.hpp" +#include "team.hpp" +#include "thread.hpp" +#include "tstring.hpp" +#include "unit.hpp" + +struct upload_log +{ + struct manager { + manager() { }; + ~manager(); + }; + + // We only enable logging when playing campaigns. + upload_log(bool enable); + ~upload_log(); + + // User starts a game (may be new campaign or saved). + void start(game_state &state, const team &team, + int team_number, const unit_map &map, const t_string &turn, + int num_turns); + + // User finishes a level. + void defeat(int turn); + void victory(int turn, int gold); + void quit(int turn); + + // Argument passed to upload thread. + struct thread_info { + threading::thread *t; + std::string lastfile; + }; + +private: + config &add_game_result(const std::string &str, int turn); + bool game_finished(config *game); + + static struct thread_info thread_; + + config config_; + config *game_; + std::string filename_; + bool enabled_; +}; + +namespace upload_log_dialog +{ + // Please please please upload stats? + void show_beg_dialog(display& disp); +}; + +#endif // UPLOAD_LOG_H_INCLUDED Index: src/playlevel.hpp =================================================================== --- src/playlevel.hpp (revision 9403) +++ src/playlevel.hpp (working copy) @@ -16,6 +16,7 @@ class config; class CVideo; struct game_state; +class upload_log; #include "game_config.hpp" #include "unit_types.hpp" @@ -40,7 +41,8 @@ LEVEL_RESULT play_level(const game_data& gameinfo, const config& terrain_config, config const* level, CVideo& video, game_state& state_of_game, - const std::vector<config*>& story); + const std::vector<config*>& story, + upload_log &log); namespace play{ void place_sides_in_preferred_locations(gamemap& map, const config::child_list& sides); Index: src/titlescreen.cpp =================================================================== --- src/titlescreen.cpp (revision 9403) +++ src/titlescreen.cpp (working copy) @@ -203,14 +203,16 @@ N_("TitleScreen button^Load"), N_("TitleScreen button^Language"), N_("TitleScreen button^Preferences"), + N_("TitleScreen button^Help Wesnoth"), N_("About"), - N_("TitleScreen button^Quit") }; + N_("TitleScreen button^Quit") }; static const char* help_button_labels[] = { N_("Start a tutorial to familiarize yourself with the game"), N_("Start a new single player campaign"), N_("Play multiplayer (hotseat, LAN, or Internet), or a single scenario against the AI"), N_("Load a single player saved game"), N_("Change the language"), N_("Configure the game's settings"), + N_("Help Wesnoth by sending us information"), N_("View the credits"), N_("Quit the game") }; @@ -222,7 +224,7 @@ #ifdef USE_TINY_GUI const int menu_yincr = 15; #else - const int menu_yincr = 40; + const int menu_yincr = 35; #endif const int padding = game_config::title_buttons_padding; Index: src/Makefile.am =================================================================== --- src/Makefile.am (revision 9403) +++ src/Makefile.am (working copy) @@ -103,6 +103,7 @@ unit.cpp \ unit_display.cpp \ unit_types.cpp \ + upload_log.cpp \ variable.cpp \ video.cpp \ serialization/binary_or_text.cpp \ Index: src/playcampaign.cpp =================================================================== --- src/playcampaign.cpp (revision 9403) +++ src/playcampaign.cpp (working copy) @@ -120,6 +120,7 @@ LEVEL_RESULT play_game(display& disp, game_state& state, const config& game_config, const game_data& units_data, CVideo& video, + upload_log &log, io_type_t io_type) { std::string type = state.campaign_type; @@ -200,7 +201,7 @@ if (state.label.empty()) state.label = (*scenario)["name"]; - LEVEL_RESULT res = play_level(units_data,game_config,scenario,video,state,story); + LEVEL_RESULT res = play_level(units_data,game_config,scenario,video,state,story,log); //LEVEL_RESULT res = play_scenario(units_data,game_config,scenario,video,state,story); state.snapshot = config(); Index: src/multiplayer.cpp =================================================================== --- src/multiplayer.cpp (revision 9403) +++ src/multiplayer.cpp (working copy) @@ -30,6 +30,7 @@ #include "video.hpp" #include "statistics.hpp" #include "serialization/string_utils.hpp" +#include "upload_log.hpp" #define LOG_NW LOG_STREAM(info, network) @@ -236,6 +237,7 @@ mp::ui::result res; game_state state; network_game_manager m; + upload_log nolog(false); gamelist.clear(); statistics::fresh_stats(); @@ -257,7 +259,7 @@ switch (res) { case mp::ui::PLAY: - play_game(disp, state, game_config, data, disp.video(), IO_CLIENT); + play_game(disp, state, game_config, data, disp.video(), nolog, IO_CLIENT); recorder.clear(); break; @@ -278,6 +280,7 @@ network::server_manager::TRY_CREATE_SERVER : network::server_manager::NO_SERVER); network_game_manager m; + upload_log nolog(false); gamelist.clear(); statistics::fresh_stats(); @@ -298,7 +301,7 @@ switch (res) { case mp::ui::PLAY: - play_game(disp, state, game_config, data, disp.video(), IO_SERVER); + play_game(disp, state, game_config, data, disp.video(), nolog, IO_SERVER); recorder.clear(); break; Index: src/game.cpp =================================================================== --- src/game.cpp (revision 9403) +++ src/game.cpp (working copy) @@ -51,6 +51,7 @@ #include "titlescreen.hpp" #include "util.hpp" #include "unit_types.hpp" +#include "upload_log.hpp" #include "unit.hpp" #include "video.hpp" #include "wassert.hpp" @@ -106,6 +107,7 @@ bool change_language(); void show_preferences(); + void show_upload_begging(); enum RELOAD_GAME_DATA { RELOAD_DATA, NO_RELOAD_DATA }; void play_game(RELOAD_GAME_DATA reload=RELOAD_DATA); @@ -139,6 +141,7 @@ const image::manager image_manager_; const events::event_context main_event_context_; const hotkey::manager hotkey_manager_; + const upload_log::manager upload_log_manager_; binary_paths_manager paths_manager_; bool test_mode_, multiplayer_mode_, no_gui_; @@ -417,7 +420,8 @@ state_.scenario = "test"; try { - ::play_game(disp(),state_,game_config_,units_data_,video_); + upload_log nolog(false); + ::play_game(disp(),state_,game_config_,units_data_,video_,nolog); } catch(game::load_game_exception& e) { loaded_game_ = e.game; loaded_game_show_replay_ = e.show_replay; @@ -576,8 +580,9 @@ } try { + upload_log nolog(false); state_.snapshot = level; - ::play_game(disp(),state_,game_config_,units_data_,video_); + ::play_game(disp(),state_,game_config_,units_data_,video_,nolog); //play_level(units_data_,game_config_,&level,video_,state_,story); } catch(game::error& e) { std::cerr << "caught error: '" << e.message << "'\n"; @@ -1242,6 +1247,13 @@ disp().redraw_everything(); } +void game_controller::show_upload_begging() +{ + upload_log_dialog::show_beg_dialog(disp()); + + disp().redraw_everything(); +} + //this function reads the game configuration, searching for valid cached copies first void game_controller::read_game_cfg(const preproc_map& defines, config& cfg, bool use_cache) { @@ -1454,7 +1466,12 @@ const binary_paths_manager bin_paths_manager(game_config_); try { - const LEVEL_RESULT result = ::play_game(disp(),state_,game_config_,units_data_,video_); + // Only record log for single-player games & tutorial. + upload_log log(state_.campaign_type.empty() + || state_.campaign_type == "scenario" + || state_.campaign_type == "tutorial"); + + const LEVEL_RESULT result = ::play_game(disp(),state_,game_config_,units_data_,video_, log); // don't show The End for multiplayer scenario // change this if MP campaigns are implemented if(result == VICTORY && (state_.campaign_type.empty() || state_.campaign_type != "multiplayer")) { @@ -1731,6 +1748,9 @@ } else if(res == gui::SHOW_ABOUT) { about::show_about(game.disp()); continue; + } else if(res == gui::BEG_FOR_UPLOAD) { + game.show_upload_begging(); + continue; } if (recorder.at_end()){ Index: src/filesystem.hpp =================================================================== --- src/filesystem.hpp (revision 9403) +++ src/filesystem.hpp (working copy) @@ -51,6 +51,7 @@ std::string get_cache_dir(); std::string get_intl_dir(); std::string get_screenshot_dir(); +std::string get_upload_dir(); std::string get_user_data_dir(); std::string get_cwd(); @@ -78,6 +79,9 @@ //function to get the creation time of a file time_t file_create_time(const std::string& fname); +//return the next ordered full filename within this directory +std::string next_filename(const std::string &dirname); + struct file_tree_checksum { file_tree_checksum(); Index: src/preferences.cpp =================================================================== --- src/preferences.cpp (revision 9403) +++ src/preferences.cpp (working copy) @@ -769,6 +769,29 @@ fps = value; } +bool upload_log() +{ + return prefs["upload_log"] == "yes"; +} + +void set_upload_log(bool value) +{ + prefs["upload_log"] = value ? "yes" : "no"; +} + +const std::string &upload_id() +{ + // We create a unique id for each person, *when asked for* to increase + // randomness. + if (prefs["upload_id"] == "") { + srand(time(NULL)); + prefs["upload_id"] + = lexical_cast<std::string>(rand()) + + lexical_cast<std::string>(SDL_GetTicks()); + } + return prefs["upload_id"]; +} + bool compress_saves() { return prefs["compress_saves"] != "no"; Index: src/titlescreen.hpp =================================================================== --- src/titlescreen.hpp (revision 9403) +++ src/titlescreen.hpp (working copy) @@ -20,7 +20,7 @@ namespace gui { enum TITLE_RESULT { TUTORIAL = 0, NEW_CAMPAIGN, MULTIPLAYER, LOAD_GAME, - CHANGE_LANGUAGE, EDIT_PREFERENCES, SHOW_ABOUT, QUIT_GAME, TITLE_CONTINUE }; + CHANGE_LANGUAGE, EDIT_PREFERENCES, BEG_FOR_UPLOAD, SHOW_ABOUT, QUIT_GAME, TITLE_CONTINUE }; TITLE_RESULT show_title(display& screen, config& tips_of_day, int* ntip); Index: src/playcampaign.hpp =================================================================== --- src/playcampaign.hpp (revision 9403) +++ src/playcampaign.hpp (working copy) @@ -22,6 +22,7 @@ class config; struct game_data; class CVideo; +class upload_log; enum io_type_t { IO_NONE, @@ -31,6 +32,7 @@ LEVEL_RESULT play_game(display& disp, game_state& state, const config& game_config, const game_data& units_data, CVideo& video, + upload_log &log, io_type_t io_type=IO_NONE); Index: src/preferences.hpp =================================================================== --- src/preferences.hpp (revision 9403) +++ src/preferences.hpp (working copy) @@ -178,6 +178,10 @@ bool flip_time(); void set_flip_time(bool value); + bool upload_log(); + void set_upload_log(bool value); + const std::string &upload_id(); + // Multiplayer functions bool chat_timestamp(); void set_chat_timestamp(bool value); Index: data/tips.cfg =================================================================== --- data/tips.cfg (revision 9403) +++ data/tips.cfg (working copy) @@ -35,3 +35,4 @@ Gaining an AMLA becomes progressively harder for each AMLA the unit receives, however. Thus, it is often more useful to try to advance your lower level units." +tip_of_day_34= _ "You can send the Wesnoth Project an anonymous summary of your progress using the Help Wesnoth button. This information is vital so we can adjust campaign difficulty." -- ccontrol: http://ozlabs.org/~rusty/ccontrol
