/**
  *  \file u/t_game_v3_specificationloader.cpp
  *  \brief Test for game::v3::SpecificationLoader
  */

#include "game/v3/specificationloader.hpp"

#include "t_game_v3.hpp"
#include "afl/base/countof.hpp"
#include "afl/charset/charset.hpp"
#include "afl/charset/codepage.hpp"
#include "afl/charset/codepagecharset.hpp"
#include "afl/except/fileformatexception.hpp"
#include "afl/except/fileproblemexception.hpp"
#include "afl/io/constmemorystream.hpp"
#include "afl/io/internaldirectory.hpp"
#include "afl/string/nulltranslator.hpp"
#include "afl/sys/log.hpp"
#include "game/experiencelevelset.hpp"
#include "game/hostversion.hpp"
#include "game/limits.hpp"
#include "game/playerset.hpp"
#include "game/spec/hull.hpp"
#include "game/spec/shiplist.hpp"
#include "game/test/root.hpp"

using afl::base::ConstBytes_t;
using afl::base::Ref;
using afl::charset::Charset;
using afl::charset::CodepageCharset;
using afl::except::FileFormatException;
using afl::except::FileProblemException;
using afl::io::ConstMemoryStream;
using afl::io::InternalDirectory;
using afl::string::NullTranslator;
using afl::sys::Log;
using game::ExperienceLevelSet_t;
using game::HostVersion;
using game::PlayerSet_t;
using game::Root;
using game::config::HostConfiguration;
using game::spec::Hull;
using game::spec::HullFunction;
using game::spec::HullFunctionList;
using game::spec::Mission;
using game::spec::ShipList;
using game::v3::SpecificationLoader;

namespace {
    /** Standard beamspec file. */
    static const uint8_t BEAMSPEC[] = {
        0x4c, 0x61, 0x73, 0x65, 0x72, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
        0x0a, 0x00, 0x03, 0x00, 0x58, 0x2d, 0x52, 0x61, 0x79, 0x20, 0x4c, 0x61, 0x73, 0x65, 0x72, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x01, 0x00, 0x01, 0x00, 0x0f, 0x00, 0x01, 0x00, 0x50, 0x6c, 0x61, 0x73, 0x6d, 0x61, 0x20, 0x42,
        0x6f, 0x6c, 0x74, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x05, 0x00, 0x01, 0x00,
        0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00, 0x03, 0x00, 0x0a, 0x00, 0x42, 0x6c, 0x61, 0x73,
        0x74, 0x65, 0x72, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
        0x0a, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x01, 0x00, 0x04, 0x00, 0x03, 0x00, 0x0a, 0x00, 0x19, 0x00,
        0x50, 0x6f, 0x73, 0x69, 0x74, 0x72, 0x6f, 0x6e, 0x20, 0x42, 0x65, 0x61, 0x6d, 0x20, 0x20, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x0c, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x05, 0x00, 0x03, 0x00, 0x04, 0x00,
        0x09, 0x00, 0x1d, 0x00, 0x44, 0x69, 0x73, 0x72, 0x75, 0x70, 0x74, 0x6f, 0x72, 0x20, 0x20, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0d, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x01, 0x00,
        0x04, 0x00, 0x05, 0x00, 0x1e, 0x00, 0x14, 0x00, 0x48, 0x65, 0x61, 0x76, 0x79, 0x20, 0x42, 0x6c,
        0x61, 0x73, 0x74, 0x65, 0x72, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x1f, 0x00, 0x01, 0x00,
        0x0c, 0x00, 0x0e, 0x00, 0x07, 0x00, 0x06, 0x00, 0x14, 0x00, 0x28, 0x00, 0x50, 0x68, 0x61, 0x73,
        0x65, 0x72, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
        0x23, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x1e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x1e, 0x00, 0x23, 0x00,
        0x48, 0x65, 0x61, 0x76, 0x79, 0x20, 0x44, 0x69, 0x73, 0x72, 0x75, 0x70, 0x74, 0x6f, 0x72, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x24, 0x00, 0x01, 0x00, 0x11, 0x00, 0x25, 0x00, 0x07, 0x00, 0x08, 0x00,
        0x32, 0x00, 0x23, 0x00, 0x48, 0x65, 0x61, 0x76, 0x79, 0x20, 0x50, 0x68, 0x61, 0x73, 0x65, 0x72,
        0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x36, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x37, 0x00,
        0x06, 0x00, 0x0a, 0x00, 0x23, 0x00, 0x2d, 0x00
    };

    /** Standard torpspec file. */
    static const uint8_t TORPSPEC[] = {
        0x4d, 0x61, 0x72, 0x6b, 0x20, 0x31, 0x20, 0x50, 0x68, 0x6f, 0x74, 0x6f, 0x6e, 0x20, 0x20, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00,
        0x01, 0x00, 0x04, 0x00, 0x05, 0x00, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x20, 0x74, 0x6f, 0x72,
        0x70, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x02, 0x00, 0x04, 0x00, 0x01, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00, 0x06, 0x00, 0x08, 0x00, 0x4d, 0x61, 0x72, 0x6b,
        0x20, 0x32, 0x20, 0x50, 0x68, 0x6f, 0x74, 0x6f, 0x6e, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
        0x05, 0x00, 0x04, 0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x02, 0x00, 0x03, 0x00, 0x03, 0x00,
        0x0a, 0x00, 0x47, 0x61, 0x6d, 0x6d, 0x61, 0x20, 0x42, 0x6f, 0x6d, 0x62, 0x20, 0x20, 0x20, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x00, 0x06, 0x00, 0x01, 0x00, 0x03, 0x00, 0x01, 0x00,
        0x04, 0x00, 0x03, 0x00, 0x0f, 0x00, 0x02, 0x00, 0x4d, 0x61, 0x72, 0x6b, 0x20, 0x33, 0x20, 0x50,
        0x68, 0x6f, 0x74, 0x6f, 0x6e, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0c, 0x00, 0x05, 0x00,
        0x01, 0x00, 0x01, 0x00, 0x05, 0x00, 0x02, 0x00, 0x04, 0x00, 0x09, 0x00, 0x0f, 0x00, 0x4d, 0x61,
        0x72, 0x6b, 0x20, 0x34, 0x20, 0x50, 0x68, 0x6f, 0x74, 0x6f, 0x6e, 0x20, 0x20, 0x20, 0x20, 0x20,
        0x20, 0x20, 0x0d, 0x00, 0x14, 0x00, 0x01, 0x00, 0x04, 0x00, 0x01, 0x00, 0x02, 0x00, 0x05, 0x00,
        0x0d, 0x00, 0x1e, 0x00, 0x4d, 0x61, 0x72, 0x6b, 0x20, 0x35, 0x20, 0x50, 0x68, 0x6f, 0x74, 0x6f,
        0x6e, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x1f, 0x00, 0x39, 0x00, 0x01, 0x00, 0x07, 0x00,
        0x0e, 0x00, 0x03, 0x00, 0x06, 0x00, 0x11, 0x00, 0x23, 0x00, 0x4d, 0x61, 0x72, 0x6b, 0x20, 0x36,
        0x20, 0x50, 0x68, 0x6f, 0x74, 0x6f, 0x6e, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x23, 0x00,
        0x64, 0x00, 0x01, 0x00, 0x02, 0x00, 0x07, 0x00, 0x02, 0x00, 0x07, 0x00, 0x17, 0x00, 0x28, 0x00,
        0x4d, 0x61, 0x72, 0x6b, 0x20, 0x37, 0x20, 0x50, 0x68, 0x6f, 0x74, 0x6f, 0x6e, 0x20, 0x20, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x24, 0x00, 0x78, 0x00, 0x01, 0x00, 0x03, 0x00, 0x08, 0x00, 0x03, 0x00,
        0x08, 0x00, 0x19, 0x00, 0x30, 0x00, 0x4d, 0x61, 0x72, 0x6b, 0x20, 0x38, 0x20, 0x50, 0x68, 0x6f,
        0x74, 0x6f, 0x6e, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x36, 0x00, 0xbe, 0x00, 0x01, 0x00,
        0x01, 0x00, 0x09, 0x00, 0x03, 0x00, 0x0a, 0x00, 0x23, 0x00, 0x37, 0x00
    };

    /** Standard engspec file. */
    static const uint8_t ENGSPEC[] = {
        0x53, 0x74, 0x61, 0x72, 0x44, 0x72, 0x69, 0x76, 0x65, 0x20, 0x31, 0x20, 0x20, 0x20, 0x20, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x01, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x64, 0x00,
        0x00, 0x00, 0x20, 0x03, 0x00, 0x00, 0x8c, 0x0a, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0xd4, 0x30,
        0x00, 0x00, 0x60, 0x54, 0x00, 0x00, 0xfc, 0x85, 0x00, 0x00, 0x00, 0xc8, 0x00, 0x00, 0xc4, 0x1c,
        0x01, 0x00, 0x53, 0x74, 0x61, 0x72, 0x44, 0x72, 0x69, 0x76, 0x65, 0x20, 0x32, 0x20, 0x20, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x02, 0x00, 0x05, 0x00, 0x02, 0x00, 0x01, 0x00, 0x02, 0x00,
        0x64, 0x00, 0x00, 0x00, 0xae, 0x01, 0x00, 0x00, 0x8c, 0x0a, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00,
        0xd4, 0x30, 0x00, 0x00, 0x60, 0x54, 0x00, 0x00, 0xfc, 0x85, 0x00, 0x00, 0x00, 0xc8, 0x00, 0x00,
        0xc4, 0x1c, 0x01, 0x00, 0x53, 0x74, 0x61, 0x72, 0x44, 0x72, 0x69, 0x76, 0x65, 0x20, 0x33, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x03, 0x00, 0x03, 0x00, 0x02, 0x00, 0x05, 0x00,
        0x03, 0x00, 0x64, 0x00, 0x00, 0x00, 0xa9, 0x01, 0x00, 0x00, 0xca, 0x03, 0x00, 0x00, 0x18, 0x15,
        0x00, 0x00, 0xd4, 0x30, 0x00, 0x00, 0x60, 0x54, 0x00, 0x00, 0xfc, 0x85, 0x00, 0x00, 0x00, 0xc8,
        0x00, 0x00, 0xc4, 0x1c, 0x01, 0x00, 0x53, 0x75, 0x70, 0x65, 0x72, 0x53, 0x74, 0x61, 0x72, 0x44,
        0x72, 0x69, 0x76, 0x65, 0x20, 0x34, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x00, 0x03, 0x00, 0x03, 0x00,
        0x07, 0x00, 0x04, 0x00, 0x64, 0x00, 0x00, 0x00, 0x9f, 0x01, 0x00, 0x00, 0xac, 0x03, 0x00, 0x00,
        0xa4, 0x06, 0x00, 0x00, 0x4c, 0x1d, 0x00, 0x00, 0x50, 0x2d, 0x00, 0x00, 0xec, 0x5e, 0x00, 0x00,
        0xe0, 0x79, 0x00, 0x00, 0xc4, 0x1c, 0x01, 0x00, 0x4e, 0x6f, 0x76, 0x61, 0x20, 0x44, 0x72, 0x69,
        0x76, 0x65, 0x20, 0x35, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x19, 0x00, 0x03, 0x00,
        0x03, 0x00, 0x07, 0x00, 0x05, 0x00, 0x64, 0x00, 0x00, 0x00, 0x9f, 0x01, 0x00, 0x00, 0xac, 0x03,
        0x00, 0x00, 0xa4, 0x06, 0x00, 0x00, 0x28, 0x0a, 0x00, 0x00, 0x04, 0x29, 0x00, 0x00, 0xdc, 0x37,
        0x00, 0x00, 0x9a, 0x5b, 0x00, 0x00, 0xc4, 0x1c, 0x01, 0x00, 0x48, 0x65, 0x61, 0x76, 0x79, 0x4e,
        0x6f, 0x76, 0x61, 0x20, 0x44, 0x72, 0x69, 0x76, 0x65, 0x20, 0x36, 0x20, 0x20, 0x20, 0x35, 0x00,
        0x03, 0x00, 0x03, 0x00, 0x0f, 0x00, 0x06, 0x00, 0x64, 0x00, 0x00, 0x00, 0x9f, 0x01, 0x00, 0x00,
        0xac, 0x03, 0x00, 0x00, 0xa4, 0x06, 0x00, 0x00, 0x28, 0x0a, 0x00, 0x00, 0x95, 0x0e, 0x00, 0x00,
        0x0c, 0x30, 0x00, 0x00, 0xca, 0x53, 0x00, 0x00, 0xc4, 0x1c, 0x01, 0x00, 0x51, 0x75, 0x61, 0x6e,
        0x74, 0x61, 0x6d, 0x20, 0x44, 0x72, 0x69, 0x76, 0x65, 0x20, 0x37, 0x20, 0x20, 0x20, 0x20, 0x20,
        0xaa, 0x00, 0x03, 0x00, 0x03, 0x00, 0x0f, 0x00, 0x07, 0x00, 0x64, 0x00, 0x00, 0x00, 0x9f, 0x01,
        0x00, 0x00, 0xac, 0x03, 0x00, 0x00, 0xa4, 0x06, 0x00, 0x00, 0x28, 0x0a, 0x00, 0x00, 0x95, 0x0e,
        0x00, 0x00, 0xb4, 0x14, 0x00, 0x00, 0xfa, 0x4b, 0x00, 0x00, 0x94, 0xa7, 0x00, 0x00, 0x48, 0x79,
        0x70, 0x65, 0x72, 0x20, 0x44, 0x72, 0x69, 0x76, 0x65, 0x20, 0x38, 0x20, 0x20, 0x20, 0x20, 0x20,
        0x20, 0x20, 0xc8, 0x00, 0x03, 0x00, 0x0d, 0x00, 0x19, 0x00, 0x09, 0x00, 0x64, 0x00, 0x00, 0x00,
        0x90, 0x01, 0x00, 0x00, 0x84, 0x03, 0x00, 0x00, 0x40, 0x06, 0x00, 0x00, 0xc4, 0x09, 0x00, 0x00,
        0x10, 0x0e, 0x00, 0x00, 0x88, 0x13, 0x00, 0x00, 0x58, 0x1b, 0x00, 0x00, 0x94, 0xa7, 0x00, 0x00,
        0x54, 0x72, 0x61, 0x6e, 0x73, 0x77, 0x61, 0x72, 0x70, 0x20, 0x44, 0x72, 0x69, 0x76, 0x65, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x2c, 0x01, 0x03, 0x00, 0x10, 0x00, 0x23, 0x00, 0x0a, 0x00, 0x64, 0x00,
        0x00, 0x00, 0x90, 0x01, 0x00, 0x00, 0x84, 0x03, 0x00, 0x00, 0x40, 0x06, 0x00, 0x00, 0xc4, 0x09,
        0x00, 0x00, 0x10, 0x0e, 0x00, 0x00, 0x24, 0x13, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0xa4, 0x1f,
        0x00, 0x00
    };

    /** Standard hullspec file, first 11 hulls. */
    static const uint8_t SHORTENED_HULLSPEC[] = {
        0x4f, 0x55, 0x54, 0x52, 0x49, 0x44, 0x45, 0x52, 0x20, 0x43, 0x4c, 0x41, 0x53, 0x53, 0x20, 0x53,
        0x43, 0x4f, 0x55, 0x54, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x09, 0x00,
        0x01, 0x00, 0x28, 0x00, 0x14, 0x00, 0x05, 0x00, 0x04, 0x01, 0xb4, 0x00, 0x01, 0x00, 0x4b, 0x00,
        0x01, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x32, 0x00, 0x4e, 0x4f, 0x43, 0x54,
        0x55, 0x52, 0x4e, 0x45, 0x20, 0x43, 0x4c, 0x41, 0x53, 0x53, 0x20, 0x44, 0x45, 0x53, 0x54, 0x52,
        0x4f, 0x59, 0x45, 0x52, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0a, 0x00, 0x01, 0x00, 0x32, 0x00,
        0x19, 0x00, 0x07, 0x00, 0xb4, 0x00, 0xbe, 0x00, 0x01, 0x00, 0x5a, 0x00, 0x02, 0x00, 0x32, 0x00,
        0x00, 0x00, 0x02, 0x00, 0x04, 0x00, 0x46, 0x00, 0x42, 0x4f, 0x48, 0x45, 0x4d, 0x49, 0x41, 0x4e,
        0x20, 0x43, 0x4c, 0x41, 0x53, 0x53, 0x20, 0x53, 0x55, 0x52, 0x56, 0x45, 0x59, 0x20, 0x53, 0x48,
        0x49, 0x50, 0x20, 0x20, 0x20, 0x20, 0x0d, 0x00, 0x01, 0x00, 0x14, 0x00, 0x0a, 0x00, 0x03, 0x00,
        0xb4, 0x00, 0x46, 0x00, 0x02, 0x00, 0x20, 0x00, 0x03, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x02, 0x00, 0x28, 0x00, 0x56, 0x45, 0x4e, 0x44, 0x45, 0x54, 0x54, 0x41, 0x20, 0x43, 0x4c, 0x41,
        0x53, 0x53, 0x20, 0x46, 0x52, 0x49, 0x47, 0x41, 0x54, 0x45, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
        0x20, 0x20, 0x0e, 0x00, 0x01, 0x00, 0x17, 0x00, 0x0c, 0x00, 0x39, 0x00, 0x8c, 0x00, 0x4f, 0x00,
        0x02, 0x00, 0x64, 0x00, 0x05, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, 0x00, 0xaa, 0x00,
        0x4e, 0x45, 0x42, 0x55, 0x4c, 0x41, 0x20, 0x43, 0x4c, 0x41, 0x53, 0x53, 0x20, 0x43, 0x52, 0x55,
        0x49, 0x53, 0x45, 0x52, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x10, 0x00,
        0x01, 0x00, 0x3d, 0x00, 0x2a, 0x00, 0x49, 0x00, 0xd6, 0x01, 0xae, 0x01, 0x02, 0x00, 0xaa, 0x00,
        0x06, 0x00, 0x5e, 0x01, 0x00, 0x00, 0x04, 0x00, 0x04, 0x00, 0x86, 0x01, 0x42, 0x41, 0x4e, 0x53,
        0x48, 0x45, 0x45, 0x20, 0x43, 0x4c, 0x41, 0x53, 0x53, 0x20, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f,
        0x59, 0x45, 0x52, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x12, 0x00, 0x01, 0x00, 0x2f, 0x00,
        0x16, 0x00, 0x35, 0x00, 0x8c, 0x00, 0x50, 0x01, 0x02, 0x00, 0x78, 0x00, 0x06, 0x00, 0x50, 0x00,
        0x00, 0x00, 0x02, 0x00, 0x04, 0x00, 0x6e, 0x00, 0x4c, 0x4f, 0x4b, 0x49, 0x20, 0x43, 0x4c, 0x41,
        0x53, 0x53, 0x20, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x45, 0x52, 0x20, 0x20, 0x20, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x06, 0x00, 0x01, 0x00, 0x14, 0x00, 0x0a, 0x00, 0x2b, 0x00,
        0x8c, 0x00, 0x09, 0x01, 0x02, 0x00, 0x65, 0x00, 0x08, 0x00, 0x50, 0x00, 0x00, 0x00, 0x04, 0x00,
        0x02, 0x00, 0xaa, 0x00, 0x45, 0x52, 0x4f, 0x53, 0x20, 0x43, 0x4c, 0x41, 0x53, 0x53, 0x20, 0x52,
        0x45, 0x53, 0x45, 0x41, 0x52, 0x43, 0x48, 0x20, 0x56, 0x45, 0x53, 0x53, 0x45, 0x4c, 0x20, 0x20,
        0x20, 0x20, 0x1d, 0x00, 0x01, 0x00, 0x03, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x6e, 0x00, 0x4e, 0x00,
        0x02, 0x00, 0x23, 0x00, 0x04, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x1e, 0x00,
        0x42, 0x52, 0x59, 0x4e, 0x48, 0x49, 0x4c, 0x44, 0x20, 0x43, 0x4c, 0x41, 0x53, 0x53, 0x20, 0x45,
        0x53, 0x43, 0x4f, 0x52, 0x54, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x07, 0x00,
        0x01, 0x00, 0x2d, 0x00, 0x05, 0x00, 0x23, 0x00, 0x8c, 0x00, 0xa2, 0x00, 0x01, 0x00, 0x5a, 0x00,
        0x07, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x64, 0x00, 0x41, 0x52, 0x4b, 0x48,
        0x41, 0x4d, 0x20, 0x43, 0x4c, 0x41, 0x53, 0x53, 0x20, 0x46, 0x52, 0x49, 0x47, 0x41, 0x54, 0x45,
        0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x14, 0x00, 0x01, 0x00, 0x2b, 0x00,
        0x0c, 0x00, 0x43, 0x00, 0x78, 0x00, 0x48, 0x01, 0x02, 0x00, 0x96, 0x00, 0x08, 0x00, 0x5a, 0x00,
        0x00, 0x00, 0x03, 0x00, 0x06, 0x00, 0x46, 0x00, 0x54, 0x48, 0x4f, 0x52, 0x20, 0x43, 0x4c, 0x41,
        0x53, 0x53, 0x20, 0x46, 0x52, 0x49, 0x47, 0x41, 0x54, 0x45, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
        0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0b, 0x00, 0x01, 0x00, 0x37, 0x00, 0x23, 0x00, 0x59, 0x00,
        0xa0, 0x00, 0x72, 0x01, 0x02, 0x00, 0xad, 0x00, 0x09, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x08, 0x00,
        0x01, 0x00, 0x82, 0x00
    };

    /** Standard truehull file.
        Will not work with SHORTENED_HULLSPEC because it refers to nonexistant hulls. */
    static const uint8_t TRUEHULL[] = {
        0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x10, 0x00, 0x08, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06, 0x00,
        0x11, 0x00, 0x09, 0x00, 0x0a, 0x00, 0x0d, 0x00, 0x07, 0x00, 0x0b, 0x00, 0x0c, 0x00, 0x13, 0x00,
        0x68, 0x00, 0x12, 0x00, 0x14, 0x00, 0x69, 0x00, 0x0f, 0x00, 0x18, 0x00, 0x0e, 0x00, 0x10, 0x00,
        0x15, 0x00, 0x16, 0x00, 0x08, 0x00, 0x04, 0x00, 0x11, 0x00, 0x19, 0x00, 0x07, 0x00, 0x31, 0x00,
        0x68, 0x00, 0x12, 0x00, 0x17, 0x00, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x0f, 0x00, 0x1b, 0x00, 0x0e, 0x00, 0x10, 0x00, 0x1a, 0x00, 0x20, 0x00, 0x3b, 0x00, 0x1c, 0x00,
        0x30, 0x00, 0x11, 0x00, 0x1e, 0x00, 0x21, 0x00, 0x1f, 0x00, 0x2f, 0x00, 0x68, 0x00, 0x12, 0x00,
        0x1d, 0x00, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x22, 0x00, 0x28, 0x00, 0x0e, 0x00,
        0x10, 0x00, 0x24, 0x00, 0x3b, 0x00, 0x25, 0x00, 0x26, 0x00, 0x11, 0x00, 0x1e, 0x00, 0x21, 0x00,
        0x27, 0x00, 0x29, 0x00, 0x68, 0x00, 0x12, 0x00, 0x23, 0x00, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x0f, 0x00, 0x01, 0x00, 0x2c, 0x00, 0x22, 0x00, 0x28, 0x00, 0x0e, 0x00, 0x10, 0x00, 0x2b, 0x00,
        0x2d, 0x00, 0x3b, 0x00, 0x2a, 0x00, 0x2e, 0x00, 0x30, 0x00, 0x26, 0x00, 0x11, 0x00, 0x2f, 0x00,
        0x68, 0x00, 0x12, 0x00, 0x32, 0x00, 0x69, 0x00, 0x33, 0x00, 0x0f, 0x00, 0x39, 0x00, 0x55, 0x00,
        0x36, 0x00, 0x10, 0x00, 0x37, 0x00, 0x3a, 0x00, 0x11, 0x00, 0x38, 0x00, 0x34, 0x00, 0x68, 0x00,
        0x12, 0x00, 0x35, 0x00, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x0f, 0x00, 0x42, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x3c, 0x00, 0x41, 0x00, 0x3b, 0x00, 0x3e, 0x00,
        0x11, 0x00, 0x3d, 0x00, 0x40, 0x00, 0x3f, 0x00, 0x68, 0x00, 0x12, 0x00, 0x43, 0x00, 0x69, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x47, 0x00, 0x49, 0x00, 0x4d, 0x00,
        0x48, 0x00, 0x0e, 0x00, 0x10, 0x00, 0x44, 0x00, 0x4b, 0x00, 0x4c, 0x00, 0x11, 0x00, 0x45, 0x00,
        0x4a, 0x00, 0x68, 0x00, 0x12, 0x00, 0x46, 0x00, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x0f, 0x00, 0x51, 0x00, 0x55, 0x00, 0x10, 0x00, 0x52, 0x00, 0x54, 0x00, 0x53, 0x00, 0x11, 0x00,
        0x4e, 0x00, 0x50, 0x00, 0x68, 0x00, 0x12, 0x00, 0x4f, 0x00, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x62, 0x00, 0x67, 0x00, 0x57, 0x00,
        0x0e, 0x00, 0x10, 0x00, 0x5b, 0x00, 0x58, 0x00, 0x5d, 0x00, 0x5a, 0x00, 0x64, 0x00, 0x11, 0x00,
        0x56, 0x00, 0x5c, 0x00, 0x65, 0x00, 0x59, 0x00, 0x68, 0x00, 0x12, 0x00, 0x5e, 0x00, 0x69, 0x00,
        0x0f, 0x00, 0x62, 0x00, 0x67, 0x00, 0x5f, 0x00, 0x0e, 0x00, 0x10, 0x00, 0x60, 0x00, 0x61, 0x00,
        0x64, 0x00, 0x2a, 0x00, 0x11, 0x00, 0x56, 0x00, 0x5c, 0x00, 0x65, 0x00, 0x66, 0x00, 0x59, 0x00,
        0x68, 0x00, 0x12, 0x00, 0x63, 0x00, 0x69, 0x00
    };

    /** Shortened truehull file. Same as TRUEHULL, but nonexistant hulls have been blanked out. */
    static const uint8_t SHORTENED_TRUEHULL[] = {
        0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00, 0x08, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06, 0x00,
        0x00, 0x00, 0x09, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    };

    /** Shortened hullfunc.cc file.
        Mostly plaintext but I allowed me to sneak a CP437 character in:
            0,a,Alchemy
             d = Alchemy: 3 sup -> 1 T/D/M
             e = FCode "NAL": disable function
             e = FCode "alX": make only X
             e = FCode "naX" (PHost): do not make X
             s = 105
             p = alchemy
            3,t,HeatsTo50
             d = Terraforming: heats to 50F
             s = 3
             p = heatsto50
            4,t,CoolsTo50
             d = Terraforming: cools to 50F
             e = If planet is warmer than 50\xB0...
            s = 8; */
    static const uint8_t HULLFUNC[] = {
        0x30, 0x2c, 0x61, 0x2c, 0x41, 0x6c, 0x63, 0x68, 0x65, 0x6d, 0x79, 0x0a, 0x20, 0x64, 0x20, 0x3d,
        0x20, 0x41, 0x6c, 0x63, 0x68, 0x65, 0x6d, 0x79, 0x3a, 0x20, 0x33, 0x20, 0x73, 0x75, 0x70, 0x20,
        0x2d, 0x3e, 0x20, 0x31, 0x20, 0x54, 0x2f, 0x44, 0x2f, 0x4d, 0x0a, 0x20, 0x65, 0x20, 0x3d, 0x20,
        0x46, 0x43, 0x6f, 0x64, 0x65, 0x20, 0x22, 0x4e, 0x41, 0x4c, 0x22, 0x3a, 0x20, 0x64, 0x69, 0x73,
        0x61, 0x62, 0x6c, 0x65, 0x20, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x0a, 0x20, 0x65,
        0x20, 0x3d, 0x20, 0x46, 0x43, 0x6f, 0x64, 0x65, 0x20, 0x22, 0x61, 0x6c, 0x58, 0x22, 0x3a, 0x20,
        0x6d, 0x61, 0x6b, 0x65, 0x20, 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x58, 0x0a, 0x20, 0x65, 0x20, 0x3d,
        0x20, 0x46, 0x43, 0x6f, 0x64, 0x65, 0x20, 0x22, 0x6e, 0x61, 0x58, 0x22, 0x20, 0x28, 0x50, 0x48,
        0x6f, 0x73, 0x74, 0x29, 0x3a, 0x20, 0x64, 0x6f, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x6d, 0x61, 0x6b,
        0x65, 0x20, 0x58, 0x0a, 0x20, 0x73, 0x20, 0x3d, 0x20, 0x31, 0x30, 0x35, 0x0a, 0x20, 0x70, 0x20,
        0x3d, 0x20, 0x61, 0x6c, 0x63, 0x68, 0x65, 0x6d, 0x79, 0x0a, 0x33, 0x2c, 0x74, 0x2c, 0x48, 0x65,
        0x61, 0x74, 0x73, 0x54, 0x6f, 0x35, 0x30, 0x0a, 0x20, 0x64, 0x20, 0x3d, 0x20, 0x54, 0x65, 0x72,
        0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x69, 0x6e, 0x67, 0x3a, 0x20, 0x68, 0x65, 0x61, 0x74, 0x73,
        0x20, 0x74, 0x6f, 0x20, 0x35, 0x30, 0x46, 0x0a, 0x20, 0x73, 0x20, 0x3d, 0x20, 0x33, 0x0a, 0x20,
        0x70, 0x20, 0x3d, 0x20, 0x68, 0x65, 0x61, 0x74, 0x73, 0x74, 0x6f, 0x35, 0x30, 0x0a, 0x34, 0x2c,
        0x74, 0x2c, 0x43, 0x6f, 0x6f, 0x6c, 0x73, 0x54, 0x6f, 0x35, 0x30, 0x0a, 0x20, 0x64, 0x20, 0x3d,
        0x20, 0x54, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x69, 0x6e, 0x67, 0x3a, 0x20, 0x63,
        0x6f, 0x6f, 0x6c, 0x73, 0x20, 0x74, 0x6f, 0x20, 0x35, 0x30, 0x46, 0x0a, 0x20, 0x65, 0x20, 0x3d,
        0x20, 0x49, 0x66, 0x20, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x74, 0x20, 0x69, 0x73, 0x20, 0x77, 0x61,
        0x72, 0x6d, 0x65, 0x72, 0x20, 0x74, 0x68, 0x61, 0x6e, 0x20, 0x35, 0x30, 0xb0, 0x2e, 0x2e, 0x2e,
        0x0a, 0x73, 0x20, 0x3d, 0x20, 0x38, 0x0a
    };

    /** Test driver: set up a test environment using addStream(),
        then use load() to test it, then examine it. */
    struct Environment {
        Ref<InternalDirectory> dir;
        afl::base::Ref<Root> root;
        ShipList list;

        Environment()
            : dir(InternalDirectory::create("dir")),
              root(game::test::makeRoot(HostVersion(HostVersion::PHost, MKVERSION(4,0,0)))),
              list()
            { }

        void addStream(const char* name, ConstBytes_t data)
            { dir->addStream(name, *new ConstMemoryStream(data)); }

        bool load()
            {
                std::auto_ptr<Charset> charset(new CodepageCharset(afl::charset::g_codepage437));
                NullTranslator tx;
                Log log;
                bool result = false;
                SpecificationLoader(dir, charset, tx, log).loadShipList(list, *root, std::auto_ptr<game::StatusTask_t>(game::makeResultTask(result)))->call();
                return result;
            }
    };

    template<typename T>
    struct TestCase {
        const char* fileContent;
        T result;
    };
}

/** Test standard case: load ship list from minimum files.
    A: prepare directory with some files. Load them.
    E: correct data successfully loaded */
void
TestGameV3SpecificationLoader::testStandard()
{
    Environment env;
    env.addStream("beamspec.dat", BEAMSPEC);
    env.addStream("torpspec.dat", TORPSPEC);
    env.addStream("engspec.dat", ENGSPEC);
    env.addStream("hullspec.dat", SHORTENED_HULLSPEC);
    env.addStream("truehull.dat", SHORTENED_TRUEHULL);
    TS_ASSERT_EQUALS(env.load(), true);

    TS_ASSERT_EQUALS(env.list.beams().size(), 10);
    TS_ASSERT_EQUALS(env.list.launchers().size(), 10);
    TS_ASSERT_EQUALS(env.list.engines().size(), 9);
    TS_ASSERT_EQUALS(env.list.hulls().size(), 11);

    TS_ASSERT_EQUALS(env.list.beams().get(1)->getName(env.list.componentNamer()),      "Laser");
    TS_ASSERT_EQUALS(env.list.beams().get(10)->getName(env.list.componentNamer()),     "Heavy Phaser");
    TS_ASSERT_EQUALS(env.list.launchers().get(1)->getName(env.list.componentNamer()),  "Mark 1 Photon");
    TS_ASSERT_EQUALS(env.list.launchers().get(10)->getName(env.list.componentNamer()), "Mark 8 Photon");
    TS_ASSERT_EQUALS(env.list.engines().get(1)->getName(env.list.componentNamer()),    "StarDrive 1");
    TS_ASSERT_EQUALS(env.list.engines().get(9)->getName(env.list.componentNamer()),    "Transwarp Drive");
    TS_ASSERT_EQUALS(env.list.hulls().get(1)->getName(env.list.componentNamer()),      "OUTRIDER CLASS SCOUT");
    TS_ASSERT_EQUALS(env.list.hulls().get(11)->getName(env.list.componentNamer()),     "THOR CLASS FRIGATE");

    TS_ASSERT_EQUALS(env.list.hullAssignments().getHullFromIndex(env.root->hostConfiguration(), 1, 1), 1);
}

/** Test that bad indexes in truehull are correctly refused.
    A: prepare directory with some files, including shortened hullspec and regular truehull. Load them.
    E: loading fails */
void
TestGameV3SpecificationLoader::testTruehullVerification()
{
    Environment env;
    env.addStream("beamspec.dat", BEAMSPEC);
    env.addStream("torpspec.dat", TORPSPEC);
    env.addStream("engspec.dat", ENGSPEC);
    env.addStream("hullspec.dat", SHORTENED_HULLSPEC);
    env.addStream("truehull.dat", TRUEHULL);

    TS_ASSERT_EQUALS(env.load(), false);
}

/** Test loading with hullfuncs.
    A: prepare directory with some files and a hullfunc.cc file. Load them.
    E: correct data successfully loaded */
void
TestGameV3SpecificationLoader::testLoadHullfunc()
{
    Environment env;
    env.addStream("beamspec.dat", BEAMSPEC);
    env.addStream("torpspec.dat", TORPSPEC);
    env.addStream("engspec.dat",  ENGSPEC);
    env.addStream("hullspec.dat", SHORTENED_HULLSPEC);
    env.addStream("truehull.dat", SHORTENED_TRUEHULL);
    env.addStream("hullfunc.cc",  HULLFUNC);
    TS_ASSERT_EQUALS(env.load(), true);

    // Verify hull functions (already mostly tested in BasicHullFunctionList test)
    TS_ASSERT(env.list.basicHullFunctions().getFunctionById(3) != 0);
    TS_ASSERT_EQUALS(env.list.basicHullFunctions().getFunctionById(3)->getName(), "HeatsTo50");
    TS_ASSERT(env.list.basicHullFunctions().getFunctionById(4) != 0);
    TS_ASSERT_EQUALS(env.list.basicHullFunctions().getFunctionById(4)->getName(), "CoolsTo50");
    TS_ASSERT_EQUALS(env.list.basicHullFunctions().getFunctionById(4)->getExplanation(), "If planet is warmer than 50\xC2\xB0...");

    // Verify assignment to ships: 4 is default-assigned to 8 as hull ability
    const Hull& cooler = *env.list.hulls().get(8);
    TS_ASSERT_EQUALS(cooler.getHullFunctions(true).getPlayersThatCan(
                         4,
                         env.list.modifiedHullFunctions(),
                         env.list.basicHullFunctions(),
                         env.root->hostConfiguration(),
                         cooler,
                         ExperienceLevelSet_t::allUpTo(game::MAX_EXPERIENCE_LEVELS),
                         true),
                     PlayerSet_t::allUpTo(game::MAX_PLAYERS));
    TS_ASSERT_EQUALS(cooler.getHullFunctions(false).getPlayersThatCan(
                         4,
                         env.list.modifiedHullFunctions(),
                         env.list.basicHullFunctions(),
                         env.root->hostConfiguration(),
                         cooler,
                         ExperienceLevelSet_t::allUpTo(game::MAX_EXPERIENCE_LEVELS),
                         true),
                     PlayerSet_t());
}

/** Test missing mandatory files cases.
    A: prepare directory with some files, but omit one. Load them.
    E: loading fails */
void
TestGameV3SpecificationLoader::testMissing()
{
    {
        Environment env;
        // no beamspec
        env.addStream("torpspec.dat", TORPSPEC);
        env.addStream("engspec.dat", ENGSPEC);
        env.addStream("hullspec.dat", SHORTENED_HULLSPEC);
        env.addStream("truehull.dat", SHORTENED_TRUEHULL);
        TS_ASSERT_EQUALS(env.load(), false);
    }
    {
        Environment env;
        env.addStream("beamspec.dat", BEAMSPEC);
        // no torpspec
        env.addStream("engspec.dat", ENGSPEC);
        env.addStream("hullspec.dat", SHORTENED_HULLSPEC);
        env.addStream("truehull.dat", SHORTENED_TRUEHULL);
        TS_ASSERT_EQUALS(env.load(), false);
    }
    {
        Environment env;
        env.addStream("beamspec.dat", BEAMSPEC);
        env.addStream("torpspec.dat", TORPSPEC);
        // no engspec
        env.addStream("hullspec.dat", SHORTENED_HULLSPEC);
        env.addStream("truehull.dat", SHORTENED_TRUEHULL);
        TS_ASSERT_EQUALS(env.load(), false);
    }
    {
        Environment env;
        env.addStream("beamspec.dat", BEAMSPEC);
        env.addStream("torpspec.dat", TORPSPEC);
        env.addStream("engspec.dat", ENGSPEC);
        // no hullspec
        env.addStream("truehull.dat", SHORTENED_TRUEHULL);
        TS_ASSERT_EQUALS(env.load(), false);
    }
    {
        Environment env;
        env.addStream("beamspec.dat", BEAMSPEC);
        env.addStream("torpspec.dat", TORPSPEC);
        env.addStream("engspec.dat", ENGSPEC);
        env.addStream("hullspec.dat", SHORTENED_HULLSPEC);
        // no truehull
        TS_ASSERT_EQUALS(env.load(), false);
    }
}

/** Test hullfunc assignments (%hullfunc section).
    A: prepare directory with some files, including a hullfunc.txt file. Load them.
    E: verify correct result */
void
TestGameV3SpecificationLoader::testHullfuncAssignment()
{
    const TestCase<PlayerSet_t> TESTCASES[] = {
        // Base case
        { "Init = Clear\n"
          "Hull = 3\n"
          "Function = Alchemy\n"
          "PlayersAllowed = *\n",
          PlayerSet_t::allUpTo(game::MAX_PLAYERS) },

        // "+" instead of "*"
        { "Init = Clear\n"
          "Hull = 3\n"
          "Function = Alchemy\n"
          "PlayersAllowed = +\n",
          PlayerSet_t::allUpTo(game::MAX_PLAYERS) },

        // "3 + 4" means the same as "+"
        { "Init = Clear\n"
          "Hull = 3\n"
          "Function = Alchemy\n"
          "PlayersAllowed = 3 + 4\n",
          PlayerSet_t::allUpTo(game::MAX_PLAYERS) },

        // "3 +4" is 3 and 4
        { "Init = Clear\n"
          "Hull = 3\n"
          "Function = Alchemy\n"
          "PlayersAllowed = 3 +4\n",
          PlayerSet_t() + 3 + 4 },

        // Alternative way of selecting the hull
        { "Init = Clear\n"
          "Hull = Bohemian\n"
          "Function = Alchemy\n"
          "PlayersAllowed = *\n",
          PlayerSet_t::allUpTo(game::MAX_PLAYERS) },

        // Alternative way of selecting the function
        { "Init = Clear\n"
          "Hull = 3\n"
          "Function = 0\n"
          "PlayersAllowed = *\n",
          PlayerSet_t::allUpTo(game::MAX_PLAYERS) },

        // Using races
        { "Init = Clear\n"
          "Hull = 3\n"
          "Function = Alchemy\n"
          "RacesAllowed = +\n",
          PlayerSet_t::allUpTo(game::MAX_PLAYERS) },

        // Using races
        { "Init = Clear\n"
          "Hull = 3\n"
          "Function = Alchemy\n"
          "RacesAllowed = 4\n",
          PlayerSet_t(4) },

        // All but...
        { "Init = Clear\n"
          "Hull = 3\n"
          "Function = Alchemy\n"
          "RacesAllowed = * -2\n",
          PlayerSet_t::allUpTo(game::MAX_PLAYERS) - 2 },
    };

    for (size_t i = 0; i < countof(TESTCASES); ++i) {
        Environment env;
        env.addStream("beamspec.dat", BEAMSPEC);
        env.addStream("torpspec.dat", TORPSPEC);
        env.addStream("engspec.dat",  ENGSPEC);
        env.addStream("hullspec.dat", SHORTENED_HULLSPEC);
        env.addStream("truehull.dat", SHORTENED_TRUEHULL);
        env.addStream("hullfunc.cc",  HULLFUNC);
        env.addStream("hullfunc.txt", afl::string::toBytes(TESTCASES[i].fileContent));
        TS_ASSERT_EQUALS(env.load(), true);

        const Hull& hull = *env.list.hulls().get(3);
        TSM_ASSERT_EQUALS(TESTCASES[i].fileContent,
                          hull.getHullFunctions(true).getPlayersThatCan(
                              0,
                              env.list.modifiedHullFunctions(),
                              env.list.basicHullFunctions(),
                              env.root->hostConfiguration(),
                              hull,
                              ExperienceLevelSet_t::allUpTo(game::MAX_EXPERIENCE_LEVELS),
                              true),
                          TESTCASES[i].result);
    }
}

/** Test hullfunc assignments (%hullfunc section), assign to ship.
    A: prepare directory with some files, including a shiplist.txt file (for a change...). Load them.
    E: verify correct result */
void
TestGameV3SpecificationLoader::testHullfuncAssignmentShip()
{
    const char*const FILE_CONTENT =
        "%hullfunc\n"
        "Init = Clear\n"
        "Hull = 3\n"
        "Function = Alchemy\n"
        "AssignTo = Ship\n"
        "PlayersAllowed = 7\n";

    Environment env;
    env.addStream("beamspec.dat", BEAMSPEC);
    env.addStream("torpspec.dat", TORPSPEC);
    env.addStream("engspec.dat",  ENGSPEC);
    env.addStream("hullspec.dat", SHORTENED_HULLSPEC);
    env.addStream("truehull.dat", SHORTENED_TRUEHULL);
    env.addStream("hullfunc.cc",  HULLFUNC);
    env.addStream("shiplist.txt", afl::string::toBytes(FILE_CONTENT));
    TS_ASSERT_EQUALS(env.load(), true);

    const Hull& hull = *env.list.hulls().get(3);
    TS_ASSERT_EQUALS(hull.getHullFunctions(true).getPlayersThatCan(
                         0,
                         env.list.modifiedHullFunctions(),
                         env.list.basicHullFunctions(),
                         env.root->hostConfiguration(),
                         hull,
                         ExperienceLevelSet_t::allUpTo(game::MAX_EXPERIENCE_LEVELS),
                         true),
                     PlayerSet_t());
    TS_ASSERT_EQUALS(hull.getHullFunctions(false).getPlayersThatCan(
                         0,
                         env.list.modifiedHullFunctions(),
                         env.list.basicHullFunctions(),
                         env.root->hostConfiguration(),
                         hull,
                         ExperienceLevelSet_t::allUpTo(game::MAX_EXPERIENCE_LEVELS),
                         true),
                     PlayerSet_t(7));
}

/** Test hullfunc assignments (%hullfunc section), using PlayerRace configuration.
    A: prepare directory with some files, including a hullfunc.txt file. Load them.
    E: verify correct result */
void
TestGameV3SpecificationLoader::testHullfuncAssignmentPlayerRace()
{
    const TestCase<PlayerSet_t> TESTCASES[] = {
        // Base case
        { "Init = Clear\n"
          "Hull = 3\n"
          "Function = Alchemy\n"
          "PlayersAllowed = *\n",
          PlayerSet_t::allUpTo(game::MAX_PLAYERS) },

        // One race
        { "Init = Clear\n"
          "Hull = 3\n"
          "Function = Alchemy\n"
          "RacesAllowed = 2\n",
          PlayerSet_t() + 6 + 7 },

        // Multiple races
        { "Init = Clear\n"
          "Hull = 3\n"
          "Function = Alchemy\n"
          "RacesAllowed = 1 2\n",
          PlayerSet_t() + 1 + 2 + 6 + 7 },

        // All but...
        { "Init = Clear\n"
          "Hull = 3\n"
          "Function = Alchemy\n"
          "RacesAllowed = * -2\n",
          PlayerSet_t::allUpTo(game::MAX_PLAYERS) - 6 - 7 },
    };

    for (size_t i = 0; i < countof(TESTCASES); ++i) {
        Environment env;
        env.root->hostConfiguration()[HostConfiguration::PlayerRace].set("1,1,4,4,4,2,2,9,9,9,9");
        env.addStream("beamspec.dat", BEAMSPEC);
        env.addStream("torpspec.dat", TORPSPEC);
        env.addStream("engspec.dat",  ENGSPEC);
        env.addStream("hullspec.dat", SHORTENED_HULLSPEC);
        env.addStream("truehull.dat", SHORTENED_TRUEHULL);
        env.addStream("hullfunc.cc",  HULLFUNC);
        env.addStream("hullfunc.txt", afl::string::toBytes(TESTCASES[i].fileContent));
        TS_ASSERT_EQUALS(env.load(), true);

        const Hull& hull = *env.list.hulls().get(3);
        TSM_ASSERT_EQUALS(TESTCASES[i].fileContent,
                          hull.getHullFunctions(true).getPlayersThatCan(
                              0,
                              env.list.modifiedHullFunctions(),
                              env.list.basicHullFunctions(),
                              env.root->hostConfiguration(),
                              hull,
                              ExperienceLevelSet_t::allUpTo(game::MAX_EXPERIENCE_LEVELS),
                              true),
                          TESTCASES[i].result);
    }
}

/** Test hullfunc assignments (%hullfunc section), using Level assignment.
    A: prepare directory with some files, including a hullfunc.txt file. Load them.
    E: verify correct result */
void
TestGameV3SpecificationLoader::testHullfuncAssignmentLevel()
{
    const TestCase<ExperienceLevelSet_t> TESTCASES[] = {
        // Base case
        { "Init = Clear\n"
          "Hull = 3\n"
          "Function = Alchemy\n"
          "PlayersAllowed = *\n",
          ExperienceLevelSet_t::allUpTo(game::MAX_EXPERIENCE_LEVELS) },

        // Level 3 and more
        { "Init = Clear\n"
          "Hull = 3\n"
          "Function = Alchemy\n"
          "Level = 3\n"
          "PlayersAllowed = *\n",
          ExperienceLevelSet_t::allUpTo(game::MAX_EXPERIENCE_LEVELS) - 0 - 1 - 2 },

        // Level 3 and more, alternative form
        { "Init = Clear\n"
          "Hull = 3\n"
          "Function = Alchemy\n"
          "Level = 3-\n"
          "PlayersAllowed = *\n",
          ExperienceLevelSet_t::allUpTo(game::MAX_EXPERIENCE_LEVELS) - 0 - 1 - 2 },

        // Level 3 only
        { "Init = Clear\n"
          "Hull = 3\n"
          "Function = Alchemy\n"
          "Level = 3-3\n"
          "PlayersAllowed = *\n",
          ExperienceLevelSet_t(3) },

        // Level 0-2 only
        { "Init = Clear\n"
          "Hull = 3\n"
          "Function = Alchemy\n"
          "Level = 0-2\n"
          "PlayersAllowed = *\n",
          ExperienceLevelSet_t::allUpTo(2) },

        // Level 1-2 only
        { "Init = Clear\n"
          "Hull = 3\n"
          "Function = Alchemy\n"
          "Level = 1-2\n"
          "PlayersAllowed = *\n",
          ExperienceLevelSet_t::allUpTo(2) - 0 },

        // Error is ignored
        { "Init = Clear\n"
          "Hull = 3\n"
          "Function = Alchemy\n"
          "Level = 0-2\n"
          "Level = error\n"
          "PlayersAllowed = *\n",
          ExperienceLevelSet_t::allUpTo(2) },
    };

    for (size_t i = 0; i < countof(TESTCASES); ++i) {
        Environment env;
        env.addStream("beamspec.dat", BEAMSPEC);
        env.addStream("torpspec.dat", TORPSPEC);
        env.addStream("engspec.dat",  ENGSPEC);
        env.addStream("hullspec.dat", SHORTENED_HULLSPEC);
        env.addStream("truehull.dat", SHORTENED_TRUEHULL);
        env.addStream("hullfunc.cc",  HULLFUNC);
        env.addStream("hullfunc.txt", afl::string::toBytes(TESTCASES[i].fileContent));
        TS_ASSERT_EQUALS(env.load(), true);

        const Hull& hull = *env.list.hulls().get(3);
        HullFunctionList out;
        hull.getHullFunctions(true).getAll(out,
                                           env.list.modifiedHullFunctions(),
                                           env.root->hostConfiguration(),
                                           hull,
                                           PlayerSet_t::allUpTo(game::MAX_PLAYERS),
                                           ExperienceLevelSet_t::allUpTo(game::MAX_EXPERIENCE_LEVELS),
                                           HullFunction::AssignedToHull);

        const HullFunction* p = 0;
        for (HullFunctionList::Iterator_t it = out.begin(), end = out.end(); it != end; ++it) {
            if (it->getBasicFunctionId() == 0) {
                p = &*it;
            }
        }
        TS_ASSERT(p != 0);
        TS_ASSERT_EQUALS(p->getLevels(), TESTCASES[i].result);
    }
}

void
TestGameV3SpecificationLoader::testLoadFCodes()
{
    const char*const FC_FILE =
        "alm,sra,Alchemy Molybdenum only\n"
        "ee1,s-1,Cancel alliance with %1\n";

    const char*const XTRA_FILE =
        "lfm X";

    Environment env;
    env.addStream("beamspec.dat", BEAMSPEC);
    env.addStream("torpspec.dat", TORPSPEC);
    env.addStream("engspec.dat",  ENGSPEC);
    env.addStream("hullspec.dat", SHORTENED_HULLSPEC);
    env.addStream("truehull.dat", SHORTENED_TRUEHULL);
    env.addStream("fcodes.cc", afl::string::toBytes(FC_FILE));
    env.addStream("xtrfcode.txt", afl::string::toBytes(XTRA_FILE));
    TS_ASSERT_EQUALS(env.load(), true);

    // Verify: given codes need to be special
    // (parsing details are checked in FriendlyCodeList test)
    TS_ASSERT(env.list.friendlyCodes().isSpecial("alm", false));
    TS_ASSERT(env.list.friendlyCodes().isSpecial("ee1", false));
    TS_ASSERT(env.list.friendlyCodes().isSpecial("lfm", false));
    TS_ASSERT(env.list.friendlyCodes().isSpecial("XYZ", false));

    TS_ASSERT(!env.list.friendlyCodes().isSpecial("alt", false));
}

void
TestGameV3SpecificationLoader::testLoadMissions()
{
    const char*const MSN_FILE =
        "1,,Explore\n"
        "c = System.Host$<>1\n"
        "2,,Mine Sweep\n"
        "\ts = M-Sweep\n"
        "7,!h#,Tow a ship\n"
        " t = \"Towing \" # ShipNameAndId(Mission.Tow)\n"
        " s = Tow\n";

    Environment env;
    env.addStream("beamspec.dat", BEAMSPEC);
    env.addStream("torpspec.dat", TORPSPEC);
    env.addStream("engspec.dat",  ENGSPEC);
    env.addStream("hullspec.dat", SHORTENED_HULLSPEC);
    env.addStream("truehull.dat", SHORTENED_TRUEHULL);
    env.addStream("mission.cc", afl::string::toBytes(MSN_FILE));
    TS_ASSERT_EQUALS(env.load(), true);

    const Mission* p = env.list.missions().findMissionByNumber(1, PlayerSet_t::allUpTo(game::MAX_PLAYERS));
    TS_ASSERT(p != 0);
    TS_ASSERT_EQUALS(p->getName(), "Explore");

    p = env.list.missions().findMissionByNumber(7, PlayerSet_t::allUpTo(game::MAX_PLAYERS));
    TS_ASSERT(p != 0);
    TS_ASSERT_EQUALS(p->getName(), "Tow a ship");
    TS_ASSERT_EQUALS(p->getShortName(), "Tow");
    TS_ASSERT_EQUALS(p->getHotkey(), '7');
}

void
TestGameV3SpecificationLoader::testOpenSpecFile()
{
    Environment env;
    env.addStream("beamspec.dat", BEAMSPEC);

    std::auto_ptr<Charset> charset(new CodepageCharset(afl::charset::g_codepage437));
    NullTranslator tx;
    Log log;
    SpecificationLoader testee(env.dir, charset, tx, log);

    // Existing file
    TS_ASSERT_EQUALS(testee.openSpecificationFile("beamspec.dat")->getSize(), sizeof(BEAMSPEC));

    // Nonexistant file
    TS_ASSERT_THROWS(testee.openSpecificationFile("other.dat"), afl::except::FileProblemException);
}

