Esempio n. 1
0
void InitialGameStateExtractor::extractBuildings(GameState &state, UString bldFileName,
                                                 sp<City> city)
{
	auto &data = this->ufo2p;

	auto fileName = "xcom3/ufodata/" + bldFileName + ".bld";

	auto inFile = fw().data->fs.open(fileName);
	if (!inFile)
	{
		LogError("Failed to open \"%s\"", fileName.c_str());
	}
	auto fileSize = inFile.size();
	auto bldCount = fileSize / sizeof(struct bld_file_entry);

	LogInfo("Loading %lu buildings from %s", (unsigned long)bldCount, fileName.c_str());

	for (unsigned i = 0; i < bldCount; i++)
	{
		struct bld_file_entry entry;
		inFile.read((char *)&entry, sizeof(entry));

		auto b = mksp<Building>();
		b->name = data.building_names->get(entry.name_idx);
		b->owner = {&state, data.get_org_id(entry.owner_idx)};
		// Our rects are exclusive of p2
		b->bounds = {entry.x0, entry.y0, entry.x1 + 1, entry.y1 + 1};
		auto id =
		    UString::format("%s%s", Building::getPrefix().c_str(), canon_string(b->name).c_str());

		city->buildings[id] = b;
	}
}
Esempio n. 2
0
void InitialGameStateExtractor::extractBuildingFunctions(GameState &state) const
{
	auto &data = this->ufo2p;

	for (unsigned i = 0; i < data.building_functions->count(); i++)
	{
		auto f = mksp<BuildingFunction>();
		f->name = data.building_functions->get(i);
		if (i < data.infiltration_speed_building->count())
		{
			f->infiltrationSpeed = data.infiltration_speed_building->get(i).speed;
		}
		if (i < buildingFunctionDetectionWeights.size())
		{
			f->detectionWeight = buildingFunctionDetectionWeights[i];
		}
		auto id = format("%s%s", BuildingFunction::getPrefix(), canon_string(f->name));
		auto ped = format("%s%s", UfopaediaEntry::getPrefix(), canon_string(f->name));
		f->ufopaedia_entry = {&state, ped};
		state.building_functions[id] = f;
	}
}
void InitialGameStateExtractor::extractBuildings(GameState &state, UString bldFileName,
                                                 sp<City> city, bool alienBuilding)
{
	auto &data = this->ufo2p;

	auto fileName = "xcom3/ufodata/" + bldFileName + ".bld";

	auto inFile = fw().data->fs.open(fileName);
	if (!inFile)
	{
		LogError("Failed to open \"%s\"", fileName.cStr());
	}
	auto fileSize = inFile.size();
	auto bldCount = fileSize / sizeof(struct BldFileEntry);

	LogInfo("Loading %lu buildings from %s", (unsigned long)bldCount, fileName.cStr());

	for (unsigned i = 0; i < bldCount; i++)
	{
		struct BldFileEntry entry;
		inFile.read((char *)&entry, sizeof(entry));

		auto b = mksp<Building>();
		if (alienBuilding)
		{
			LogInfo("Alien bld %d func %d", entry.name_idx, entry.function_idx);
			// FIXME: albld.bld seems to have unexpected name_idx and function_idx?
			b->name = data.alien_building_names->get(i);
			b->function = b->name;
		}
		else
		{
			b->name = data.building_names->get(entry.name_idx);
			b->function = data.building_functions->get(entry.function_idx);
		}
		b->owner = {&state, data.getOrgId(entry.owner_idx)};
		// Our rects are exclusive of p2
		// Shift position by 20 tiles
		b->bounds = {entry.x0 + 20, entry.y0 + 20, entry.x1 + 21, entry.y1 + 21};
		auto id = UString::format("%s%s", Building::getPrefix(), canon_string(b->name));

		city->buildings[id] = b;
	}
}
Esempio n. 4
0
void InitialGameStateExtractor::extractBuildings(GameState &state, UString bldFileName,
                                                 sp<City> city, bool alienBuilding) const
{
	auto &data = this->ufo2p;

	auto fileName = "xcom3/ufodata/" + bldFileName + ".bld";

	auto inFile = fw().data->fs.open(fileName);
	if (!inFile)
	{
		LogError("Failed to open \"%s\"", fileName);
	}
	auto fileSize = inFile.size();
	auto bldCount = fileSize / sizeof(struct BldFileEntry);

	LogInfo("Loading %lu buildings from %s", (unsigned long)bldCount, fileName);

	for (unsigned i = 0; i < bldCount; i++)
	{
		struct BldFileEntry entry;
		inFile.read((char *)&entry, sizeof(entry));

		auto b = mksp<Building>();
		if (alienBuilding)
		{
			b->name = data.alien_building_names->get(entry.function_idx);
			b->function = {&state,
			               format("%s%s", BuildingFunction::getPrefix(), canon_string(b->name))};
			LogInfo("Alien bld %d %s func %d %s", entry.name_idx, b->name, entry.function_idx,
			        b->function.id);

			b->accessTopic = {&state, format("RESEARCH_UNLOCK_ALIEN_BUILDING_%d", i)};
			if (i < 9)
			{
				b->researchUnlock.emplace_back(&state,
				                               format("RESEARCH_UNLOCK_ALIEN_BUILDING_%d", i + 1));
			}
			else
			{
				b->victory = true;
			}

			// Load crew
			auto crew = ufo2p.crew_alien_building->get(entry.function_idx);
			UFO2P::fillCrew(state, crew, b->preset_crew);
		}
		else
		{
			b->name = data.building_names->get(entry.name_idx);
			b->function = {&state,
			               format("%s%s", BuildingFunction::getPrefix(),
			                      canon_string(data.building_functions->get(entry.function_idx)))};
		}
		int battle_map_index = entry.function_idx - 1 + (alienBuilding ? 39 : 0);
		// Fix battle index for buildings that use other maps
		switch (battle_map_index)
		{
			// 24 Car Factory uses 25 Flying Car Factory
			case 23:
				battle_map_index = 24;
				break;
			// 13 Car Park is not used in vanilla
			// Still, we should provide a reasonable substitute
			case 12:
				battle_map_index = 10; // 11 Procreation Park
				break;
			// 16 Hotel is not used in vanilla
			// Still, we should provide a reasonable substitute
			case 15:
				battle_map_index = 14; // 15 Luxury Appartments
				break;
			// 17 Atmosphere Processor is not used in vanilla
			// Still, we should provide a reasonable substitute
			case 16:
				battle_map_index = 19; // 20 Water Purifier
				break;
			// 29 Ruins is not used in vanilla
			// Still, we should provide a reasonable substitute
			case 28:
				battle_map_index = 10; // 11 Procreation Park
				break;
				// 31 Space Ship is not used in vanilla
				// Still, we should provide a reasonable substitutecase 31:
				battle_map_index = 7; // 08 Space Port
				break;
			// 34 Outdoor Parks is not used in vanilla
			// Still, we should provide a reasonable substitute
			case 33:
				battle_map_index = 10; // 11 Procreation Park
				break;
			default:
				break;
		}
		b->battle_map = {
		    &state, format("%s%s", BattleMap::getPrefix(), this->battleMapPaths[battle_map_index])};
		b->owner = {&state, data.getOrgId(entry.owner_idx)};
		// Our rects are exclusive of p2
		// Shift position by 20 tiles
		b->bounds = {entry.x0 + 20, entry.y0 + 20, entry.x1 + 21, entry.y1 + 21};
		auto id = format("%s%s", Building::getPrefix(), canon_string(b->name));
		b->city = {&state, city->id};
		city->buildings[id] = b;
	}
}
Esempio n. 5
0
void InitialGameStateExtractor::extractResearch(GameState &state, Difficulty)
{
	auto &data = this->ufo2p;
	for (unsigned i = 0; i < data.research_data->count(); i++)
	{
		auto rdata = data.research_data->get(i);

		auto r = mksp<ResearchTopic>();

		r->name = data.research_names->get(i);
		auto id = ResearchTopic::getPrefix() + canon_string(r->name);
		r->description = data.research_descriptions->get(i);
		r->ufopaedia_entry = "";
		r->man_hours = rdata.skillHours;
		r->man_hours_progress = 0;
		switch (rdata.researchGroup)
		{
			case 0:
				r->type = ResearchTopic::Type::BioChem;
				break;
			case 1:
				r->type = ResearchTopic::Type::Physics;
				break;
			default:
				LogError("Unexpected researchGroup 0x%02x for research item %s",
				         (unsigned)rdata.researchGroup, id.cStr());
		}
		switch (rdata.labSize)
		{
			case 0:
				r->required_lab_size = ResearchTopic::LabSize::Small;
				break;
			case 1:
				r->required_lab_size = ResearchTopic::LabSize::Large;
				break;
			default:
				LogError("Unexpected labSize 0x%02x for research item %s", (unsigned)rdata.labSize,
				         id.cStr());
		}
		// FIXME: this assumed all listed techs are reqired, which is not true for some topics
		// (It's possible that an unknown member in ResearchData marks this, or it's done
		// in-code)
		// This should be fixed up in the patch.

		ResearchDependency dependency;
		dependency.type = ResearchDependency::Type::All;

		for (int pre = 0; pre < 3; pre++)
		{

			if (rdata.prereqTech[pre] != 0xffff)
			{
				auto prereqId = ResearchTopic::getPrefix() +
				                canon_string(data.research_names->get(rdata.prereqTech[pre]));
				dependency.topics.emplace(StateRef<ResearchTopic>{&state, prereqId});
			}
		}

		r->dependencies.research.push_back(dependency);

		r->score = rdata.score;

		if (state.research.topics.find(id) != state.research.topics.end())
		{
			LogError("Multiple research topics with ID \"%s\"", id.cStr());
		}
		state.research.topics[id] = r;
// FIXME: The ufopaedia entries here don't seem to directly map to the IDs we're currently using?
// May also be a many:1 ratio (e.g. the "alien gas" research topic unlocks multiple ufopaedia
// entries) making this more complex
#if 0

		auto ufopaediaEntryID = "PAEDIAENTRY_" + canon_string(r->name);
		auto ufopaediaCatID =
		    "PAEDIACATEGORY_" + canon_string(data.ufopaedia_group->get(rdata.ufopaediaGroup));
		auto paediaCat = state.ufopaedia[ufopaediaCatID];
		if (!paediaCat)
		{
			state.ufopaedia[ufopaediaCatID] = mksp<UfopaediaCategory>();
			paediaCat = state.ufopaedia[ufopaediaCatID];
		}
		auto paediaEntry = paediaCat->entries[ufopaediaEntryID];
		if (!paediaEntry)
		{
			paediaCat->entries[ufopaediaEntryID] = mksp<UfopaediaEntry>();
			paediaEntry = paediaCat->entries[ufopaediaEntryID];
		}
		if (paediaEntry->required_research)
		{
			LogError("Multiple required research for UFOPaedia topic \"%s\" - \"%s\" and \"%s\"",
			         ufopaediaEntryID.cStr(), r->name.cStr(),
			         paediaEntry->required_research->name.cStr());
		}
		paediaEntry->required_research = {&state, id};
#endif
	}
	state.research.updateTopicList();
}
void InitialGameStateExtractor::extractBuildings(GameState &state, UString bldFileName,
                                                 sp<City> city, bool alienBuilding) const
{
	auto &data = this->ufo2p;

	auto fileName = "xcom3/ufodata/" + bldFileName + ".bld";

	auto inFile = fw().data->fs.open(fileName);
	if (!inFile)
	{
		LogError("Failed to open \"%s\"", fileName);
	}
	auto fileSize = inFile.size();
	auto bldCount = fileSize / sizeof(struct BldFileEntry);

	LogInfo("Loading %lu buildings from %s", (unsigned long)bldCount, fileName);

	for (unsigned i = 0; i < bldCount; i++)
	{
		struct BldFileEntry entry;
		inFile.read((char *)&entry, sizeof(entry));

		auto b = mksp<Building>();
		if (alienBuilding)
		{
			LogInfo("Alien bld %d func %d", entry.name_idx, entry.function_idx);
			// FIXME: albld.bld seems to have unexpected name_idx and function_idx?
			b->name = data.alien_building_names->get(i);
			b->function = b->name;
		}
		else
		{
			b->name = data.building_names->get(entry.name_idx);
			b->function = data.building_functions->get(entry.function_idx);
		}
		int battle_map_index = entry.function_idx - 1 + (alienBuilding ? 39 : 0);
		// Fix battle index for buildings that use other maps
		switch (battle_map_index)
		{
			// 24 Car Factory uses 25 Flying Car Factory
			case 23:
				battle_map_index = 24;
				break;
			// 13 Car Park is not used in vanilla
			// Still, we should provide a reasonable substitute
			case 12:
				battle_map_index = 10; // 11 Procreation Park
				break;
			// 16 Hotel is not used in vanilla
			// Still, we should provide a reasonable substitute
			case 15:
				battle_map_index = 14; // 15 Luxury Appartments
				break;
			// 17 Atmosphere Processor is not used in vanilla
			// Still, we should provide a reasonable substitute
			case 16:
				battle_map_index = 19; // 20 Water Purifier
				break;
			// 29 Ruins is not used in vanilla
			// Still, we should provide a reasonable substitute
			case 28:
				battle_map_index = 10; // 11 Procreation Park
				break;
				// 31 Space Ship is not used in vanilla
				// Still, we should provide a reasonable substitutecase 31:
				battle_map_index = 7; // 08 Space Port
				break;
			// 34 Outdoor Parks is not used in vanilla
			// Still, we should provide a reasonable substitute
			case 33:
				battle_map_index = 10; // 11 Procreation Park
				break;
		}
		b->battle_map = {
		    &state, format("%s%s", BattleMap::getPrefix(), this->battleMapPaths[battle_map_index])};
		b->owner = {&state, data.getOrgId(entry.owner_idx)};
		// Our rects are exclusive of p2
		// Shift position by 20 tiles
		b->bounds = {entry.x0 + 20, entry.y0 + 20, entry.x1 + 21, entry.y1 + 21};
		auto id = format("%s%s", Building::getPrefix(), canon_string(b->name));

		city->buildings[id] = b;
	}
}
Esempio n. 7
0
int main(int argc, char **argv)
{
	UFO2P data;
	tinyxml2::XMLDocument doc;

	doc.InsertFirstChild(doc.NewDeclaration());

	auto *node = doc.NewElement("openapoc");
	doc.InsertEndChild(node);

	auto *parentNode = node;

	node = doc.NewElement("vehicles");
	parentNode->InsertFirstChild(node);

	parentNode = node;

	std::cerr << "Number of vehicle strings: " << data.vehicle_names->readStrings.size() << "\n";

	for (int i = 0; i < data.vehicle_data->count(); i++)
	{
		auto v = data.vehicle_data->get(i);
		node = doc.NewElement("vehicle");
		parentNode->InsertEndChild(node);

		std::string id = canon_string(data.vehicle_names->get(i));
		node->SetAttribute("id", id.c_str());

		node->SetAttribute("name", std::string("STR_" + id + "_NAME").c_str());
		node->SetAttribute("manufacturer", canon_string(data.organisation_names->get(v.manufacturer)).c_str());

		if (v.movement_type == 0)
		{
			node->SetAttribute("type", "GROUND");
		}
		else if (v.movement_type == 1)
		{
			if (v.animation_type == 0)
			{
				node->SetAttribute("type", "UFO");
				int animFrames = UFOAnimationFrames[id];
				auto *animNode = doc.NewElement("animation");
				node->InsertEndChild(animNode);

				for (int i = 0; i < animFrames; i++)
				{
					auto *frameNode = doc.NewElement("frame");
					animNode->InsertEndChild(frameNode);
					std::stringstream ss;
					ss << "PCK:UFODATA/SAUCER.PCK:UFODATA/SAUCER.TAB:" << std::dec << (v.graphic_frame + i) << ":UFODATA/PAL_01.DAT";
					frameNode->SetText(ss.str().c_str());
				}

				auto *crashNode = doc.NewElement("crashed");
				node->InsertEndChild(crashNode);
				std::stringstream ss;
				ss << "PCK:UFODATA/SAUCER.PCK:UFODATA/SAUCER.TAB:" << std::dec << (v.graphic_frame + animFrames) << ":UFODATA/PAL_01.DAT";
				crashNode->SetText(ss.str().c_str());

			}
			else
			{
				node->SetAttribute("type", "FLYING");
				std::vector<std::string> direction = {"N","NE","E","SE","S","SW","W","NW"};
				std::vector<std::string> banking = {"flat","decending","ascending","banking_right","banking_left"};
				int image_offset = 0;
				for (auto &bank : banking)
				{
					auto *bankNode = doc.NewElement(bank.c_str());
					node->InsertEndChild(bankNode);
					for (auto &dir : direction)
					{
						std::stringstream ss;
						ss << "PCK:UFODATA/SAUCER.PCK:UFODATA/SAUCER.TAB:" << std::dec << (v.graphic_frame + image_offset++) << ":UFODATA/PAL_01.DAT";
						auto *dirNode = doc.NewElement(dir.c_str());
						bankNode->InsertEndChild(dirNode);
						dirNode->SetText(ss.str().c_str());

					}
					//XXX HACK - The space liner doesn't have banking/ascending/decendimg images
					if (id == std::string("SPACE_LINER"))
						break;
				}
			}
		}
		else
		{
			node->SetAttribute("type", "UNKNOWN");
		}
	}

	doc.SaveFile("vehicles.xml");

	doc.Clear();
	doc.InsertFirstChild(doc.NewDeclaration());
	node = doc.NewElement("openapoc");
	doc.InsertEndChild(node);
	parentNode = node;

	for (int i = 0; i < data.vehicle_data->count(); i++)
	{
		std::string id = canon_string(data.vehicle_names->get(i));
		node = doc.NewElement("string");
		parentNode->InsertEndChild(node);
		node->SetAttribute("id", std::string("STR_" + id + "_NAME").c_str());

		auto *enNode = doc.NewElement("en");
		node->InsertFirstChild(enNode);
		enNode->SetText(data.vehicle_names->get(i).c_str());
	}

	doc.SaveFile("vehicles_strings.xml");

	return 0;
}
Esempio n. 8
0
void InitialGameStateExtractor::extractAgentTypes(GameState &state, Difficulty)
{
	const int HUMAN_FEMALE_PORTRAIT_START = 0;
	const int HUMAN_FEMALE_PORTRAIT_END = 30;
	// const int HYBRID_FEMALE_PORTRAIT_START = 30;
	// const int HYBRID_FEMALE_PORTRAIT_END = 35;
	const int HUMAN_MALE_PORTRAIT_START = 35;
	const int HUMAN_MALE_PORTRAIT_END = 65;
	// const int HYBRID_MALE_PORTRAIT_START = 65;
	// const int HYBRID_MALE_PORTRAIT_END = 70;
	// const int ANDROID_MALE_PORTRAIT_START = 70;
	// const int ANDROID_MALE_PORTRAIT_END = 75;
	const int ALIEN_PORTRAIT_OFFSET = 74;

	const int UNIT_TYPE_BIOCHEMIST = 1;
	const int UNIT_TYPE_ENGINEER = 2;
	const int UNIT_TYPE_QUANTUM_PHYSIST = 3;
	const int UNIT_TYPE_UPPER_CLASS_FEMALE_1 = 16;
	const int UNIT_TYPE_UPPER_CLASS_FEMALE_2 = 17;
	const int UNIT_TYPE_UPPER_CLASS_FEMALE_3 = 18;
	const int UNIT_TYPE_CIVILIAN_FEMALE_1 = 22;
	const int UNIT_TYPE_CIVILIAN_FEMALE_2 = 23;
	const int UNIT_TYPE_CIVILIAN_FEMALE_3 = 24;
	const int UNIT_TYPE_LOWER_CLASS_FEMALE_1 = 31;
	const int UNIT_TYPE_LOWER_CLASS_FEMALE_2 = 32;
	const int UNIT_TYPE_LOWER_CLASS_FEMALE_3 = 33;

	const int UNIT_TYPE_FIRST_ALIEN_ENTRY = 34;

	const UString loftempsFile = "xcom3/tacdata/loftemps.dat";
	const UString loftempsTab = "xcom3/tacdata/loftemps.tab";

	auto &data_t = this->tacp;
	auto &data_u = this->ufo2p;

	// Portraits

	auto portraitSmallTabFileName = UString("xcom3/ufodata/agntico.tab");
	auto portraitSmallTabFile = fw().data->fs.open(portraitSmallTabFileName);
	if (!portraitSmallTabFile)
	{
		LogError("Failed to open small portrait TAB file \"%s\"", portraitSmallTabFileName.cStr());
		return;
	}
	size_t portraitSmallCount = portraitSmallTabFile.size() / 4;

	auto portraitLargeTabFileName = UString("xcom3/ufodata/photo.tab");
	auto portraitLargeTabFile = fw().data->fs.open(portraitLargeTabFileName);
	if (!portraitLargeTabFile)
	{
		LogError("Failed to open Large portrait TAB file \"%s\"", portraitLargeTabFileName.cStr());
		return;
	}
	size_t portraitLargeCount = portraitLargeTabFile.size() / 4;

	std::vector<AgentPortrait> portraits;

	for (unsigned i = 0; i < portraitSmallCount; i++)
	{
		auto p = AgentPortrait();
		p.icon = fw().data->loadImage(UString::format(
		    "PCK:xcom3/ufodata/agntico.pck:xcom3/ufodata/agntico.tab:%d:xcom3/ufodata/pal_01.dat",
		    i));
		if (i < portraitLargeCount)
			p.photo = fw().data->loadImage(UString::format(
			    "PCK:xcom3/ufodata/photo.pck:xcom3/ufodata/photo.tab:%d:xcom3/ufodata/pal_01.dat",
			    i));
		portraits.push_back(p);
	}

	// Unit Types

	// X-Com Agents are hand-filled in the patch xml, because they're not stored in the files
	// Therefore, we skip #0
	for (unsigned i = 1; i < data_u.agent_types->count(); i++)
	{
		auto a = mksp<AgentType>();
		auto data = data_u.agent_types->get(i);

		a->name = data_u.agent_type_names->get(i);
		UString id = UString::format("%s%s", AgentType::getPrefix(), canon_string(a->name));

		a->id = id;

		switch (i)
		{
			case UNIT_TYPE_BIOCHEMIST:
				a->role = AgentType::Role::BioChemist;
				a->playable = true;
				break;
			case UNIT_TYPE_ENGINEER:
				a->role = AgentType::Role::Engineer;
				a->playable = true;
				break;
			case UNIT_TYPE_QUANTUM_PHYSIST:
				a->role = AgentType::Role::Physicist;
				a->playable = true;
				break;
			default:
				a->role = AgentType::Role::Soldier;
				a->playable = false;
				break;
		}
		switch (i)
		{
			case UNIT_TYPE_BIOCHEMIST:
			case UNIT_TYPE_ENGINEER:
			case UNIT_TYPE_QUANTUM_PHYSIST:
				a->possible_genders.insert(a->possible_genders.end(), AgentType::Gender::Male);
				a->possible_genders.insert(a->possible_genders.end(), AgentType::Gender::Female);
				a->gender_chance[AgentType::Gender::Male] = 1;
				a->gender_chance[AgentType::Gender::Female] = 1;
				for (unsigned p = HUMAN_MALE_PORTRAIT_START; p < HUMAN_MALE_PORTRAIT_END; p++)
					a->portraits[AgentType::Gender::Male][p - HUMAN_MALE_PORTRAIT_START] =
					    portraits[p];
				for (unsigned p = HUMAN_FEMALE_PORTRAIT_START; p < HUMAN_FEMALE_PORTRAIT_END; p++)
					a->portraits[AgentType::Gender::Female][p - HUMAN_FEMALE_PORTRAIT_START] =
					    portraits[p];
				break;
			case UNIT_TYPE_UPPER_CLASS_FEMALE_1:
			case UNIT_TYPE_UPPER_CLASS_FEMALE_2:
			case UNIT_TYPE_UPPER_CLASS_FEMALE_3:
			case UNIT_TYPE_CIVILIAN_FEMALE_1:
			case UNIT_TYPE_CIVILIAN_FEMALE_2:
			case UNIT_TYPE_CIVILIAN_FEMALE_3:
			case UNIT_TYPE_LOWER_CLASS_FEMALE_1:
			case UNIT_TYPE_LOWER_CLASS_FEMALE_2:
			case UNIT_TYPE_LOWER_CLASS_FEMALE_3:
				a->possible_genders.insert(a->possible_genders.end(), AgentType::Gender::Female);
				a->gender_chance[AgentType::Gender::Female] = 1;
				a->portraits[AgentType::Gender::Female][0] = portraits[data.image];
				break;
			default:
				a->possible_genders.insert(a->possible_genders.end(), AgentType::Gender::Male);
				a->gender_chance[AgentType::Gender::Male] = 1;
				// Aliens start at id 34, their portraits start at 75,
				// first alien has image index of 1 rather than 0, so we shift by 74
				if (i >= UNIT_TYPE_FIRST_ALIEN_ENTRY)
					a->portraits[AgentType::Gender::Male][0] =
					    portraits[data.image + ALIEN_PORTRAIT_OFFSET];
				else
					a->portraits[AgentType::Gender::Male][0] = portraits[data.image];
				break;
		}

		a->min_stats.accuracy = 100 - data.accuracy_base - data.accuracy_inc;
		a->max_stats.accuracy = 100 - data.accuracy_base;
		a->min_stats.biochem_skill = data.biochemistry_base;
		a->max_stats.biochem_skill = data.biochemistry_base + data.biochemistry_inc;
		a->min_stats.bravery = data.bravery_base * 10;
		a->max_stats.bravery = data.bravery_base * 10 + data.bravery_inc * 10;
		a->min_stats.engineering_skill = data.engineering_base;
		a->max_stats.engineering_skill = data.engineering_base + data.engineering_inc;
		a->min_stats.health = data.health_base;
		a->max_stats.health = data.health_base + data.health_inc;
		a->min_stats.physics_skill = data.quantum_physics_base;
		a->max_stats.physics_skill = data.quantum_physics_base + data.quantum_physics_inc;
		a->min_stats.psi_attack = data.psi_attack_base;
		a->max_stats.psi_attack = data.psi_attack_base + data.psi_attack_inc;
		a->min_stats.psi_defence = data.psi_defense_base;
		a->max_stats.psi_defence = data.psi_defense_base + data.psi_defense_inc;
		a->min_stats.psi_energy = data.psi_energy_base;
		a->max_stats.psi_energy = data.psi_energy_base + data.psi_energy_inc;
		a->min_stats.reactions = data.reactions_base;
		a->max_stats.reactions = data.reactions_base + data.reactions_inc;
		a->min_stats.speed = data.speed_base;
		a->max_stats.speed = data.speed_base + data.speed_inc;
		a->min_stats.stamina = data.stamina_base;
		a->max_stats.stamina = data.stamina_base + data.stamina_inc;
		a->min_stats.strength = data.strength_base;
		a->max_stats.strength = data.strength_base + data.strength_inc;

		switch (data.movement_type)
		{
			case AGENT_MOVEMENT_TYPE_STATIONARY:
				a->movement_type = AgentType::MovementType::Stationary;
				break;
			case AGENT_MOVEMENT_TYPE_STANDART:
				a->movement_type = AgentType::MovementType::Standart;
				break;
			case AGENT_MOVEMENT_TYPE_FLYING:
				a->movement_type = AgentType::MovementType::Flying;
				break;
			case AGENT_MOVEMENT_TYPE_STANDART_LARGE:
				a->movement_type = AgentType::MovementType::StandartLarge;
				break;
			case AGENT_MOVEMENT_TYPE_FLYING_LARGE:
				a->movement_type = AgentType::MovementType::FlyingLarge;
				break;
		}
		a->large = (data.loftemps_height > 40) ||
		           (a->movement_type == AgentType::MovementType::StandartLarge) ||
		           (a->movement_type == AgentType::MovementType::FlyingLarge);

		// FIXME: Need to scale voxelmap to fit twice the size
		a->voxelMap = a->large ? mksp<VoxelMap>(Vec3<int>{48, 48, 40})
		                       : mksp<VoxelMap>(Vec3<int>{24, 24, 20});
		if (data.loftemps_idx != 0)
		{
			for (int slice = 0; slice < data.loftemps_height / 2; slice++)
			{
				auto lofString =
				    UString::format("LOFTEMPS:%s:%s:%u", loftempsFile.cStr(), loftempsTab.cStr(),
				                    (unsigned int)data.loftemps_idx);
				a->voxelMap->slices[slice] = fw().data->loadVoxelSlice(lofString);
			}
		}

		a->armor[AgentType::BodyPart::Body] = data.armor_body;
		a->armor[AgentType::BodyPart::Helmet] = data.armor_head;
		a->armor[AgentType::BodyPart::LeftArm] = data.armor_left;
		a->armor[AgentType::BodyPart::Legs] = data.armor_leg;
		a->armor[AgentType::BodyPart::RightArm] = data.armor_right;
		a->damage_modifier = {
		    &state,
		    UString::format("%s%s", DamageModifier::getPrefix(),
		                    canon_string(data_t.damage_modifier_names->get(data.damage_modifier)))};
		a->inventory = data.inventory == 1;
		if (!data.inventory)
		{
			if (data.equipment_sets[0] != 0xff)
			{
				// Equipment sets (built-in) have complex structure with several possible items and
				// weapons and clips defined, and unit types have 5 equipment sets to choose from,
				// but all that is irrelevant. Game only uses equipment sets for aliens who have no
				// inventory, they are never defined for anyone else without inventory, all 5 sets
				// are always the same and all sets only contain a built-in weapon which is always
				// self-recharging. Therefore, we only need to store built-in weapons.
				auto es_data = data_t.agent_equipment_set_built_in->get(data.equipment_sets[0]);

				if (es_data.weapons[0].weapon_idx != 0xffffffff)
					a->built_in_weapon_right = {
					    &state, UString::format("%s%s", AEquipmentType::getPrefix(),
					                            canon_string(data_u.agent_equipment_names->get(
					                                es_data.weapons[0].weapon_idx)))};
				if (es_data.weapons[1].weapon_idx != 0xffffffff)
					a->built_in_weapon_left = {
					    &state, UString::format("%s%s", AEquipmentType::getPrefix(),
					                            canon_string(data_u.agent_equipment_names->get(
					                                es_data.weapons[1].weapon_idx)))};
			}
			// FIXME: Fill layouts for units without inventory
			// That is, give them left and right hand slots!
			// std::list<EquipmentLayoutSlot> equipment_layout_slots;
		}
		else
		{
			// FIXME: Fill layouts for units with inventory
			// std::list<EquipmentLayoutSlot> equipment_layout_slots;
		}

		a->can_improve = false;
		a->score = data.score;

		// FIXME: Extract agent animation and frames

		state.agent_types[id] = a;
	}
}