Review: Needs Fixing code, running

Review in the diff comments. JSON seems to be generated properly, but I don't 
have the website set up, so I am unable to test with your other branch there.

Diff comments:

> 
> === modified file 'src/CMakeLists.txt'
> --- src/CMakeLists.txt        2016-02-06 11:11:24 +0000
> +++ src/CMakeLists.txt        2016-05-14 07:36:19 +0000
> @@ -82,6 +82,7 @@
>  add_subdirectory(third_party)
>  add_subdirectory(ui_basic)
>  add_subdirectory(ui_fsmenu)
> +add_subdirectory(website)

A CMake option would be nice. The following code works for me:

option(BUILD_WEBSITE_TOOLS "Build website-related tools" ON)
...
if (BUILD_WEBSITE_TOOLS)
  add_subdirectory(website)
endif (BUILD_WEBSITE_TOOLS)

>  add_subdirectory(wui)
>  
>  # TODO(unknown): Ideally widelands_ball_of_mud shouldn't exist, everything 
> should be in a
> 
> === renamed file 'src/logic/map_info.cc' => 'src/website/map_info.cc'
> === added file 'src/website/map_object_info.cc'
> --- src/website/map_object_info.cc    1970-01-01 00:00:00 +0000
> +++ src/website/map_object_info.cc    2016-05-14 07:36:19 +0000
> @@ -0,0 +1,520 @@
> +/*
> + * Copyright (C) 2016 by the Widelands Development Team
> + *
> + * 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., 675 Mass Ave, Cambridge, MA 02139, USA.
> + *
> + */
> +
> +#include <memory>
> +
> +#include <SDL.h>
> +#include <boost/algorithm/string.hpp>
> +#include <boost/format.hpp>
> +#include <boost/lexical_cast.hpp>
> +
> +#include "base/i18n.h"
> +#include "base/log.h"
> +#include "base/macros.h"
> +#include "config.h"
> +#include "graphic/graphic.h"
> +#include "io/filesystem/filesystem.h"
> +#include "io/filesystem/layered_filesystem.h"
> +#include "io/filewrite.h"
> +#include "logic/editor_game_base.h"
> +#include "logic/map_objects/tribes/tribes.h"
> +#include "logic/map_objects/world/world.h"
> +#include "sound/sound_handler.h"
> +
> +using namespace Widelands;
> +
> +namespace  {
> +
> +/*
> + ==========================================================
> + SETUP
> + ==========================================================
> + */
> +
> +
> +// Setup the static objects Widelands needs to operate and initializes 
> systems.
> +void initialize(const std::string& output_path) {
> +     i18n::set_locale("en");
> +
> +     if (SDL_Init(SDL_INIT_VIDEO) != 0) {
> +             throw wexception("Unable to initialize SDL: %s", 
> SDL_GetError());
> +     }
> +
> +     g_fs = new LayeredFileSystem();
> +     g_fs->add_file_system(&FileSystem::create(INSTALL_DATADIR));
> +
> +     FileSystem* out_filesystem = &FileSystem::create(output_path);
> +     g_fs->add_file_system(out_filesystem);

I think adding out_filesystem to g_fs is undesired, because the game now 
prioritizes loading data (such as tribe descriptions) from output_path and not 
the actual data path! Seperating out_filesystem from g_fs should also allow 
output paths relative to the working directory of the executable. The 
write_tribes, write_buildings, write_wares and write_workers functions should 
NOT need to have output_path passed to them then.

> +
> +     // We don't really need graphics or sound here, but we will get error 
> messages
> +     // when they aren't initialized
> +     g_gr = new Graphic();
> +     g_gr->initialize(Graphic::TraceGl::kNo, 1, 1, false);
> +
> +     g_sound_handler.init();
> +     g_sound_handler.nosound_ = true;
> +}
> +
> +/*
> + ==========================================================
> + SPECIALIZED FILEWRITE
> + ==========================================================
> + */
> +
> +// Defines some convenience writing functions for the JSON format
> +class JSONFileWrite : public FileWrite {
> +public:
> +     JSONFileWrite() : FileWrite(), level_(0) {}
> +
> +     void write_string(const std::string& s, bool use_indent = false) {
> +             std::string writeme = s;
> +             if (use_indent) {
> +                     for (int i = 0; i < level_; ++i) {
> +                             writeme = (boost::format("   %s") % 
> writeme).str();
> +                     }
> +             }
> +             data(writeme.c_str(), writeme.size());
> +     }
> +     void write_key(const std::string& key) {
> +             write_string((boost::format("\"%s\":\n") % key).str(), true);
> +     }
> +     void write_value_string(const std::string& quoted_value) {
> +             write_string((boost::format("\"%s\"") % quoted_value).str(), 
> true);
> +     }
> +     void write_key_value(const std::string& key, const std::string& 
> quoted_value) {
> +             write_string((boost::format("\"%s\": %s") % key % 
> quoted_value).str(), true);
> +     }
> +     void write_key_value_string(const std::string& key, const std::string& 
> value) {
> +             std::string quoted_value = value;
> +             boost::replace_all(quoted_value, "\"", "\\\"");
> +             write_key_value(key, "\"" + value + "\"");
> +     }
> +     void write_key_value_int(const std::string& key, const int value) {
> +             write_key_value(key, boost::lexical_cast<std::string>(value));
> +     }
> +     void open_brace() {
> +             write_string("{\n", true);
> +             ++level_;
> +     }
> +     // JSON hates a final comma. This defaults to having NO comma.
> +     void close_brace(bool precede_newline = false, int current = 0, int 
> total = 0) {
> +             --level_;
> +             if (precede_newline) {
> +                     write_string("\n");
> +             }
> +             if (current < total - 1) {
> +                     write_string("},\n", true);
> +             } else {
> +                     write_string("}", true);
> +             }
> +     }
> +     void open_array(const std::string& name) {
> +             write_string((boost::format("\"%s\":[\n") % name).str(), true);
> +             ++level_;
> +     }
> +     // JSON hates a final comma. This defaults to having NO comma.
> +     void close_array(int current = 0, int total = 0) {
> +             --level_;
> +             write_string("\n");
> +             if (current < total - 1) {
> +                     write_string("],\n", true);
> +             } else {
> +                     write_string("]\n", true);
> +             }
> +     }
> +     // JSON hates a final comma. This defaults to having a comma.
> +     void close_element(int current = -2, int total = 0) {
> +             if (current < total - 1) {
> +                     write_string(",\n");
> +             }
> +     }
> +private:
> +     int level_;
> +};
> +
> +/*
> + ==========================================================
> + BUILDINGS
> + ==========================================================
> + */
> +
> +void write_buildings(const TribeDescr& tribe, EditorGameBase& egbase, const 
> std::string& output_path) {
> +
> +     log("\n==================\nWriting buildings:\n==================\n");
> +     JSONFileWrite fw;
> +     fw.open_brace(); // Main
> +     fw.open_array("buildings"); // Buildings
> +
> +     // We don't want any partially finished buildings
> +     std::vector<const BuildingDescr*> buildings;
> +     for (const DescriptionIndex& index : tribe.buildings()) {
> +             const BuildingDescr* building = tribe.get_building_descr(index);
> +             if (building->type() != MapObjectType::CONSTRUCTIONSITE &&
> +                      building->type() != MapObjectType::DISMANTLESITE) {
> +                     buildings.push_back(building);
> +             }
> +     }
> +
> +     // Now write
> +     for (size_t i = 0; i < buildings.size(); ++i) {
> +             const BuildingDescr& building = *buildings[i];
> +             log(" %s", building.name().c_str());
> +             fw.open_brace(); // Building
> +
> +             fw.write_key_value_string("name", building.name());
> +             fw.close_element();
> +             fw.write_key_value_string("descname", building.descname());
> +             fw.close_element();
> +             fw.write_key_value_string("icon", 
> building.representative_image_filename());
> +             fw.close_element();
> +
> +             // Conditional stuff in between, so we won't run into trouble 
> with the commas.
> +
> +             // Buildcost
> +             if (building.is_buildable()) {
> +                     fw.open_array("buildcost"); // Buildcost
> +                     size_t buildcost_counter = 0;
> +                     for (WareAmount buildcost : building.buildcost()) {
> +                             const WareDescr& ware = 
> *tribe.get_ware_descr(buildcost.first);
> +                             fw.open_brace(); // Buildcost
> +                             fw.write_key_value_string("name", ware.name());
> +                             fw.close_element();
> +                             fw.write_key_value_int("amount", 
> buildcost.second);
> +                             fw.close_brace(true, buildcost_counter, 
> building.buildcost().size()); // Buildcost
> +                             ++buildcost_counter;
> +                     }
> +                     fw.close_array(1, 5); // Buildcost - we need a comma
> +             }
> +
> +             if (building.is_enhanced()) {
> +                     fw.write_key_value_string("enhanced", 
> tribe.get_building_descr(building.enhanced_from())->name());
> +                     fw.close_element();
> +             }
> +
> +             if (building.enhancement() != INVALID_INDEX) {
> +                     fw.write_key_value_string("enhancement", 
> tribe.get_building_descr(building.enhancement())->name());
> +                     fw.close_element();
> +             }
> +
> +             if (upcast(ProductionSiteDescr const, productionsite, 
> &building)) {
> +                     // Produces
> +                     if (productionsite->output_ware_types().size() > 0) {
> +                             fw.open_array("produced_wares"); // Produces
> +                             size_t produces_counter = 0;
> +                             for (DescriptionIndex ware_index : 
> productionsite->output_ware_types()) {
> +                                     
> fw.write_value_string(tribe.get_ware_descr(ware_index)->name());
> +                                     fw.close_element(produces_counter, 
> productionsite->output_ware_types().size());
> +                                     ++produces_counter;
> +                             }
> +                             fw.close_array(1, 5); // Produces - we need a 
> comma
> +                     }
> +                     if (productionsite->output_worker_types().size() > 0) {
> +                             fw.open_array("produced_workers"); // Produces
> +                             size_t produces_counter = 0;
> +                             for (DescriptionIndex worker_index : 
> productionsite->output_worker_types()) {
> +                                     
> fw.write_value_string(tribe.get_worker_descr(worker_index)->name());
> +                                     fw.close_element(produces_counter, 
> productionsite->output_worker_types().size());
> +                                     ++produces_counter;
> +                             }
> +                             fw.close_array(1, 5); // Produces - we need a 
> comma
> +                     }
> +
> +                     // Consumes
> +                     if (productionsite->inputs().size() > 0) {
> +                             fw.open_array("stored_wares"); // Consumes
> +                             size_t consumes_counter = 0;
> +                             for (WareAmount input : 
> productionsite->inputs()) {
> +                                     const WareDescr& ware = 
> *tribe.get_ware_descr(input.first);
> +                                     fw.open_brace(); // Input
> +                                     fw.write_key_value_string("name", 
> ware.name());
> +                                     fw.close_element();
> +                                     fw.write_key_value_int("amount", 
> input.second);
> +                                     fw.close_brace(true, consumes_counter, 
> productionsite->inputs().size()); // Input
> +                                     ++consumes_counter;
> +                             }
> +                             fw.close_array(1, 5); // Consumes - we need a 
> comma
> +                     }
> +
> +                     fw.open_array("workers"); // Workers
> +                     size_t worker_counter = 0;
> +                     for (WareAmount input : 
> productionsite->working_positions()) {
> +                             const WorkerDescr& worker = 
> *tribe.get_worker_descr(input.first);
> +                             fw.open_brace(); // Worker
> +                             fw.write_key_value_string("name", 
> worker.name());
> +                             fw.close_element();
> +                             fw.write_key_value_int("amount", input.second);
> +                             fw.close_brace(true, worker_counter, 
> productionsite->working_positions().size()); // Worker
> +                             ++worker_counter;
> +                     }
> +                     fw.close_array(1, 5); // Workers - we need a comma
> +             } else if (upcast(MilitarySiteDescr const, militarysite, 
> &building)) {
> +                     fw.write_key_value_int("conquers", 
> militarysite->get_conquers());
> +                     fw.close_element();
> +                     fw.write_key_value_int("max_soldiers", 
> militarysite->get_max_number_of_soldiers());
> +                     fw.close_element();
> +                     fw.write_key_value_int("heal_per_second", 
> militarysite->get_heal_per_second());
> +                     fw.close_element();
> +             }
> +
> +             switch (building.type()) {
> +             case MapObjectType::PRODUCTIONSITE:
> +                     fw.write_key_value_string("type", "productionsite");
> +                     break;
> +             case MapObjectType::WAREHOUSE:
> +                     fw.write_key_value_string("type", "warehouse");
> +                     break;
> +             case MapObjectType::MILITARYSITE:
> +                     fw.write_key_value_string("type", "militarysite");
> +                     break;
> +             case MapObjectType::TRAININGSITE:
> +                     fw.write_key_value_string("type", "trainingsite");
> +                     break;
> +             default:
> +                     NEVER_HERE();
> +             }
> +             fw.close_element();
> +
> +             // Size
> +             if (building.type() == MapObjectType::WAREHOUSE &&
> +                      !building.is_buildable() && !building.is_enhanced()) {
> +                             fw.write_key_value_string("size", 
> "headquarters");
> +             } else if (building.get_ismine()) {
> +                     fw.write_key_value_string("size", "mine");
> +             } else if (building.get_isport()) {
> +                     fw.write_key_value_string("size", "port");
> +             } else {
> +                     fw.write_key_value_string("size", 
> BaseImmovable::size_to_string(building.get_size()));
> +             }
> +             fw.close_element();
> +
> +             // Helptext
> +             try {
> +                     std::unique_ptr<LuaTable> table(
> +                             
> egbase.lua().run_script("tribes/scripting/mapobject_info/building_helptext.lua"));
> +                     std::unique_ptr<LuaCoroutine> 
> cr(table->get_coroutine("func"));
> +                     cr->push_arg(building.helptext_script());
> +                     cr->resume();
> +                     const std::string help_text = cr->pop_string();
> +                     fw.write_key_value_string("helptext", help_text);
> +             } catch (LuaError& err) {
> +                     fw.write_key_value_string("helptext", err.what());
> +             }
> +
> +             fw.close_brace(true, i, buildings.size()); // Building
> +     }
> +     fw.close_array(); // Buildings
> +     fw.close_brace(); // Main
> +     fw.write(*g_fs, (boost::format("%s/%s_buildings.json") % output_path % 
> tribe.name()).str().c_str());
> +     log("\n");
> +}
> +
> +/*
> + ==========================================================
> + WARES
> + ==========================================================
> + */
> +
> +void write_wares(const TribeDescr& tribe, EditorGameBase& egbase, const 
> std::string& output_path) {
> +     log("\n===============\nWriting wares:\n===============\n");
> +     JSONFileWrite fw;
> +     fw.open_brace(); // Main
> +
> +     fw.open_array("wares"); // Wares
> +     size_t counter = 0;
> +     const size_t no_of_wares = tribe.wares().size();
> +     for (DescriptionIndex ware_index : tribe.wares()) {
> +             const WareDescr& ware = *tribe.get_ware_descr(ware_index);
> +             log(" %s", ware.name().c_str());
> +             fw.open_brace();
> +             fw.write_key_value_string("name", ware.name());
> +             fw.close_element();
> +             fw.write_key_value_string("descname", ware.descname());
> +             fw.close_element();
> +             fw.write_key_value_string("icon", ware.icon_filename());
> +             fw.close_element();
> +
> +             // Helptext
> +             try {
> +                     std::unique_ptr<LuaTable> table(
> +                             
> egbase.lua().run_script("tribes/scripting/mapobject_info/ware_helptext.lua"));
> +                     std::unique_ptr<LuaCoroutine> 
> cr(table->get_coroutine("func"));
> +                     cr->push_arg(tribe.name());
> +                     cr->push_arg(ware.helptext_script());
> +                     cr->resume();
> +                     const std::string help_text = cr->pop_string();
> +                     fw.write_key_value_string("helptext", help_text);
> +             } catch (LuaError& err) {
> +                     fw.write_key_value_string("helptext", err.what());
> +             }
> +             fw.close_brace(true, counter, no_of_wares); // Ware
> +             ++counter;
> +     }
> +     fw.close_array(); // Wares
> +
> +     fw.close_brace(); // Main
> +     fw.write(*g_fs, (boost::format("%s/%s_wares.json") % output_path % 
> tribe.name()).str().c_str());
> +     log("\n");
> +}
> +
> +/*
> + ==========================================================
> + WORKERS
> + ==========================================================
> + */
> +
> +void write_workers(const TribeDescr& tribe, EditorGameBase& egbase, const 
> std::string& output_path) {
> +     log("\n================\nWriting workers:\n================\n");
> +     JSONFileWrite fw;
> +     fw.open_brace(); // Main
> +
> +     fw.open_array("workers"); // Workers
> +     size_t counter = 0;
> +     const size_t no_of_workers = tribe.workers().size();
> +     for (DescriptionIndex worker_index : tribe.workers()) {
> +             const WorkerDescr& worker = 
> *tribe.get_worker_descr(worker_index);
> +             log(" %s", worker.name().c_str());
> +             fw.open_brace();
> +             fw.write_key_value_string("name", worker.name());
> +             fw.close_element();
> +             fw.write_key_value_string("descname", worker.descname());
> +             fw.close_element();
> +             fw.write_key_value_string("icon", worker.icon_filename());
> +             fw.close_element();
> +
> +             // Helptext
> +             try {
> +                     std::unique_ptr<LuaTable> table(
> +                             
> egbase.lua().run_script("tribes/scripting/mapobject_info/worker_helptext.lua"));
> +                     std::unique_ptr<LuaCoroutine> 
> cr(table->get_coroutine("func"));
> +                     cr->push_arg(worker.helptext_script());
> +                     cr->resume();
> +                     const std::string help_text = cr->pop_string();
> +                     fw.write_key_value_string("helptext", help_text);
> +             } catch (LuaError& err) {
> +                     fw.write_key_value_string("helptext", err.what());
> +             }
> +
> +             if (worker.becomes() != INVALID_INDEX) {
> +                     fw.close_element();
> +                     const WorkerDescr& becomes = 
> *tribe.get_worker_descr(worker.becomes());
> +                     fw.write_key("becomes");
> +                     fw.open_brace();
> +                     fw.write_key_value_string("name", becomes.name());
> +                     fw.close_element();
> +                     fw.write_key_value_int("experience", 
> worker.get_needed_experience());
> +                     fw.close_brace(true);
> +             }
> +             fw.close_brace(true, counter, no_of_workers); // Worker
> +             ++counter;
> +     }
> +     fw.close_array(); // Workers
> +
> +     fw.close_brace(); // Main
> +     fw.write(*g_fs, (boost::format("%s/%s_workers.json") % output_path % 
> tribe.name()).str().c_str());
> +     log("\n");
> +}
> +
> +/*
> + ==========================================================
> + TRIBES
> + ==========================================================
> + */
> +
> +void add_tribe_info(const TribeBasicInfo& tribe_info, JSONFileWrite* fw) {
> +             fw->write_key_value_string("name", tribe_info.name);
> +             fw->close_element();
> +             fw->write_key_value_string("descname", tribe_info.descname);
> +             fw->close_element();
> +             fw->write_key_value_string("author", tribe_info.author);
> +             fw->close_element();
> +             fw->write_key_value_string("tooltip", tribe_info.tooltip);
> +             fw->close_element();
> +             fw->write_key_value_string("icon", tribe_info.icon);
> +}
> +
> +void write_tribes(EditorGameBase& egbase, const std::string& output_path) {
> +     JSONFileWrite fw;
> +     fw.open_brace(); // Main
> +     fw.open_array("tribes"); // Tribes
> +
> +     /// Tribes
> +     egbase.mutable_tribes()->postload(); // Make sure that all values have 
> been set.
> +     const Tribes& tribes = egbase.tribes();
> +
> +     std::vector<TribeBasicInfo> tribeinfos = tribes.get_all_tribeinfos();
> +     for (size_t tribe_index = 0; tribe_index < tribeinfos.size(); 
> ++tribe_index) {
> +             const TribeBasicInfo& tribe_info = tribeinfos[tribe_index];
> +             log("\n\n=========================\nWriting tribe: 
> %s\n=========================\n",
> +                      tribe_info.name.c_str());
> +
> +             fw.open_brace(); // TribeDescr
> +             add_tribe_info(tribe_info, &fw);
> +             fw.close_brace(true, tribe_index, tribeinfos.size()); // 
> TribeDescr
> +
> +              // These go in separate files
> +
> +             JSONFileWrite fw_tribe;
> +             fw_tribe.open_brace(); // TribeDescr
> +             add_tribe_info(tribe_info, &fw_tribe);
> +             fw_tribe.close_brace(true); // TribeDescr
> +             fw_tribe.write(*g_fs,
> +                            (boost::format("%s/tribe_%s.json") % output_path 
> % tribe_info.name).str().c_str());
> +
> +             const TribeDescr& tribe =
> +                             
> *tribes.get_tribe_descr(tribes.tribe_index(tribe_info.name));
> +
> +             write_buildings(tribe, egbase, output_path);
> +             write_wares(tribe, egbase, output_path);
> +             write_workers(tribe, egbase, output_path);
> +     }
> +     fw.close_array(); // Tribes
> +     fw.close_brace(); // Main
> +     fw.write(*g_fs, (boost::format("%s/tribes.json") % 
> output_path).str().c_str());
> +}
> +
> +}  // namespace
> +
> +/*
> + ==========================================================
> + MAIN
> + ==========================================================
> + */
> +
> +int main(int argc, char ** argv)
> +{
> +     if (!(2 <= argc && argc <= 3)) {

This allows 1 or 2 arguments (not counting argv[0]), but only one is ever used?

> +             log("Usage: %s <existing-absolute-output-path>\n", argv[0]);
> +             return 1;
> +     }
> +
> +     const std::string output_path = argv[argc - 1];
> +
> +     try {
> +             initialize(output_path);
> +             EditorGameBase egbase(nullptr);
> +             write_tribes(egbase, output_path);
> +     }
> +     catch (std::exception& e) {
> +             log("Exception: %s.\n", e.what());
> +             g_sound_handler.shutdown();
> +             return 1;
> +     }
> +     g_sound_handler.shutdown();
> +     return 0;
> +}
> 
> === added file 'utils/validate_json.py'
> --- utils/validate_json.py    1970-01-01 00:00:00 +0000
> +++ utils/validate_json.py    2016-05-14 07:36:19 +0000
> @@ -0,0 +1,54 @@
> +#!/usr/bin/env python
> +# encoding: utf-8
> +
> +import codecs
> +import json
> +import os.path
> +import sys
> +
> +# Tests if the .json files in the directories on the bottom are valid JSON 
> files
> +
> +def validate_files_in_path(source_path):
> +
> +     if (not os.path.isdir(source_path)):
> +             print("Error: Path " + source_path + " not found.")
> +             sys.exit(1)
> +
> +     source_files = sorted(os.listdir(source_path), key=str.lower)
> +
> +     print("Reading JSON files in:\n   " + source_path)
> +     failed = 0
> +     for source_filename in source_files:
> +             file_path = source_path + "/" + source_filename
> +             if source_filename.endswith(".json"):
> +                     source_file = open(file_path, "r")
> +                     try:
> +                             dataset = json.load(source_file)
> +                     except ValueError as err:
> +                             failed = failed + 1
> +                             print("\n   Error reading " + source_filename + 
> ":");
> +                             print("      " + str(err))
> +
> +     if failed == 0:
> +             print("\nAll JSON files are OK.")
> +     else:
> +             if failed == 1:
> +                     print("\n" + str(failed) + " file is not valid JSON!");
> +             else:
> +                     print("\n" + str(failed) + " files are not valid 
> JSON!");
> +     return failed < 1
> +
> +sucess = False
> +
> +if (len(sys.argv) == 2):
> +     base_path = os.path.normpath(sys.argv[1])
> +     if (not os.path.exists(base_path) or os.path.isfile(base_path)):
> +             base_path = 
> os.path.abspath(os.path.join(os.path.dirname(__file__),os.path.pardir),base_path)

I get an error here:
TypeError: abspath() takes exactly 1 argument (2 given)

> +
> +     if (os.path.exists(base_path) and not os.path.isfile(base_path)):
> +             success = validate_files_in_path(os.path.normpath(base_path))
> +
> +if success:
> +     sys.exit(0)
> +else:
> +     sys.exit(1)


-- 
https://code.launchpad.net/~widelands-dev/widelands/map_object_info/+merge/287409
Your team Widelands Developers is subscribed to branch 
lp:~widelands-dev/widelands/map_object_info.

_______________________________________________
Mailing list: https://launchpad.net/~widelands-dev
Post to     : [email protected]
Unsubscribe : https://launchpad.net/~widelands-dev
More help   : https://help.launchpad.net/ListHelp

Reply via email to