1 // Copyright 2015-2017 the openage authors. See copying.md for legal info.
2 
3 #include "game_spec.h"
4 
5 #include <tuple>
6 
7 #include "../assetmanager.h"
8 #include "../audio/error.h"
9 #include "../audio/resource_def.h"
10 #include "../engine.h"
11 #include "../gamedata/blending_mode.gen.h"
12 #include "../gamedata/string_resource.gen.h"
13 #include "../gamedata/terrain.gen.h"
14 #include "../log/log.h"
15 #include "../rng/global_rng.h"
16 #include "../unit/producer.h"
17 #include "../util/compiler.h"
18 #include "../util/strings.h"
19 #include "../util/timer.h"
20 #include "civilisation.h"
21 
22 
23 namespace openage {
24 
GameSpec(AssetManager * am)25 GameSpec::GameSpec(AssetManager *am)
26 	:
27 	assetmanager{am},
28 	gamedata_loaded{false} {
29 }
30 
~GameSpec()31 GameSpec::~GameSpec() {}
32 
33 
initialize()34 bool GameSpec::initialize() {
35 	util::Timer load_timer;
36 	load_timer.start();
37 
38 	const util::Path &asset_dir = this->assetmanager->get_asset_dir();
39 
40 	log::log(MSG(info) << "Loading game specification files...");
41 
42 	std::vector<gamedata::string_resource> string_resources = util::read_csv_file<gamedata::string_resource>(
43 		asset_dir["converted/string_resources.docx"]
44 	);
45 
46 	try {
47 		// read the packed csv file
48 		util::CSVCollection raw_gamedata{
49 			asset_dir["converted/gamedata/gamedata.docx"]
50 		};
51 
52 		// parse the original game description files
53 		this->gamedata = raw_gamedata.read<gamedata::empiresdat>(
54 			"gamedata-empiresdat.docx"
55 		);
56 
57 		this->load_terrain(this->gamedata[0]);
58 
59 		// process and load the game description files
60 		this->on_gamedata_loaded(this->gamedata[0]);
61 		this->gamedata_loaded = true;
62 	}
63 	catch (Error &exc) {
64 		// rethrow allmighty openage exceptions
65 		throw;
66 	}
67 	catch (std::exception &exc) {
68 		// unfortunately we have no idea of the std::exception backtrace
69 		throw Error{ERR << "gamedata could not be loaded: "
70 		                << util::demangle(typeid(exc).name())
71 		                << ": "<< exc.what()};
72 	}
73 
74 	log::log(MSG(info).fmt("Loading time  [data]: %5.3f s",
75 	                       load_timer.getval() / 1e9));
76 	return true;
77 }
78 
load_complete() const79 bool GameSpec::load_complete() const {
80 	return this->gamedata_loaded;
81 }
82 
get_terrain_meta()83 terrain_meta *GameSpec::get_terrain_meta() {
84 	return &this->terrain_data;
85 }
86 
get_slp_graphic(index_t slp)87 index_t GameSpec::get_slp_graphic(index_t slp) {
88 	return this->slp_to_graphic[slp];
89 }
90 
get_texture(index_t graphic_id) const91 Texture *GameSpec::get_texture(index_t graphic_id) const {
92 	if (graphic_id <= 0 || this->graphics.count(graphic_id) == 0) {
93 		log::log(MSG(dbg) << "  -> ignoring graphics_id: " << graphic_id);
94 		return nullptr;
95 	}
96 
97 	auto g = this->graphics.at(graphic_id);
98 	int slp_id = g->slp_id;
99 	if (slp_id <= 0) {
100 		log::log(MSG(dbg) << "  -> ignoring negative slp_id: " << slp_id);
101 		return nullptr;
102 	}
103 
104 	log::log(MSG(dbg) << "   slp id/name: " << slp_id << " " << g->name);
105 	std::string tex_fname = util::sformat("converted/graphics/%d.slp.png", slp_id);
106 
107 	return this->get_texture(tex_fname, true);
108 }
109 
get_texture(const std::string & file_name,bool use_metafile) const110 Texture *GameSpec::get_texture(const std::string &file_name, bool use_metafile) const {
111 	// return nullptr if the texture wasn't found (3rd param)
112 	return this->assetmanager->get_texture(file_name, use_metafile, true);
113 }
114 
get_unit_texture(index_t unit_id) const115 std::shared_ptr<UnitTexture> GameSpec::get_unit_texture(index_t unit_id) const {
116 	if (this->unit_textures.count(unit_id) == 0) {
117 		if (unit_id > 0) {
118 			log::log(MSG(dbg) << "  -> ignoring unit_id: " << unit_id);
119 		}
120 		return nullptr;
121 	}
122 	return this->unit_textures.at(unit_id);
123 }
124 
get_sound(index_t sound_id) const125 const Sound *GameSpec::get_sound(index_t sound_id) const {
126 	if (this->available_sounds.count(sound_id) == 0) {
127 		if (sound_id > 0) {
128 			log::log(MSG(dbg) << "  -> ignoring sound_id: " << sound_id);
129 		}
130 		return nullptr;
131 	}
132 	return &this->available_sounds.at(sound_id);
133 }
134 
135 
get_graphic_data(index_t grp_id) const136 const gamedata::graphic *GameSpec::get_graphic_data(index_t grp_id) const {
137 	if (this->graphics.count(grp_id) == 0) {
138 		log::log(MSG(dbg) << "  -> ignoring grp_id: " << grp_id);
139 		return nullptr;
140 	}
141 	return this->graphics.at(grp_id);
142 }
143 
get_command_data(index_t unit_id) const144 std::vector<const gamedata::unit_command *> GameSpec::get_command_data(index_t unit_id) const {
145 	if (this->commands.count(unit_id) == 0) {
146 		return std::vector<const gamedata::unit_command *>(); // empty vector
147 	}
148 	return this->commands.at(unit_id);
149 }
150 
get_civ_name(int civ_id) const151 std::string GameSpec::get_civ_name(int civ_id) const {
152 	return this->gamedata[0].civs.data[civ_id].name;
153 }
154 
create_unit_types(unit_meta_list & objects,int civ_id) const155 void GameSpec::create_unit_types(unit_meta_list &objects, int civ_id) const {
156 	if (!this->load_complete()) {
157 		return;
158 	}
159 
160 	// create projectile types first
161 	for (auto &obj : this->gamedata[0].civs.data[civ_id].units.missile.data) {
162 		this->load_missile(obj, objects);
163 	}
164 
165 	// create object unit types
166 	for (auto &obj : this->gamedata[0].civs.data[civ_id].units.object.data) {
167 		this->load_object(obj, objects);
168 	}
169 
170 	// create dead unit types
171 	for (auto &unit : this->gamedata[0].civs.data[civ_id].units.moving.data) {
172 		this->load_object(unit, objects);
173 	}
174 
175 	// create living unit types
176 	for (auto &unit : this->gamedata[0].civs.data[civ_id].units.living.data) {
177 		this->load_living(unit, objects);
178 	}
179 
180 	// create building unit types
181 	for (auto &building : this->gamedata[0].civs.data[civ_id].units.building.data) {
182 		this->load_building(building, objects);
183 	}
184 }
185 
186 
get_asset_manager() const187 AssetManager *GameSpec::get_asset_manager() const {
188 	return this->assetmanager;
189 }
190 
191 
on_gamedata_loaded(const gamedata::empiresdat & gamedata)192 void GameSpec::on_gamedata_loaded(const gamedata::empiresdat &gamedata) {
193 	const util::Path &asset_dir = this->assetmanager->get_asset_dir();
194 	util::Path sound_dir = asset_dir["converted/sounds"];
195 
196 	// create graphic id => graphic map
197 	for (auto &graphic : gamedata.graphics.data) {
198 		this->graphics[graphic.id] = &graphic;
199 		this->slp_to_graphic[graphic.slp_id] = graphic.id;
200 	}
201 
202 	log::log(INFO << "Loading textures...");
203 
204 	// create complete set of unit textures
205 	for (auto &g : this->graphics) {
206 		this->unit_textures.insert({g.first, std::make_shared<UnitTexture>(*this, g.second)});
207 	}
208 
209 	log::log(INFO << "Loading sounds...");
210 
211 	// playable sound files for the audio manager
212 	std::vector<audio::resource_def> load_sound_files;
213 
214 	// all sounds defined in the game specification
215 	for (const gamedata::sound &sound : gamedata.sounds.data) {
216 		std::vector<int> sound_items;
217 
218 		// each sound may have multiple variation,
219 		// processed in this loop
220 		// these are the single sound files.
221 		for (const gamedata::sound_item &item : sound.sound_items.data) {
222 
223 			if (item.resource_id < 0) {
224 				log::log(SPAM << "   Invalid sound resource id < 0");
225 				continue;
226 			}
227 
228 			std::string snd_filename = util::sformat("%d.opus", item.resource_id);
229 			util::Path snd_path = sound_dir[snd_filename];
230 
231 			if (not snd_path.is_file()) {
232 				continue;
233 			}
234 
235 			// single items for a sound (so that we can ramdomize it)
236 			sound_items.push_back(item.resource_id);
237 
238 			// the single sound will be loaded in the audio system.
239 			audio::resource_def resource {
240 				audio::category_t::GAME,
241 				item.resource_id,
242 				snd_path,
243 				audio::format_t::OPUS,
244 				audio::loader_policy_t::DYNAMIC
245 			};
246 			load_sound_files.push_back(resource);
247 		}
248 
249 
250 		// create test sound objects that can be played later
251 		this->available_sounds.insert({
252 			sound.id,
253 			Sound{
254 				this,
255 				std::move(sound_items)
256 			}
257 		});
258 	}
259 
260 	// TODO: move out the loading of the sound.
261 	//       this class only provides the names and locations
262 
263 	// load the requested sounds.
264 	audio::AudioManager &am = this->assetmanager->get_engine()->get_audio_manager();
265 	am.load_resources(load_sound_files);
266 
267 	// this final step occurs after loading media
268 	// as producers require both the graphics and sounds
269 	this->create_abilities(gamedata);
270 }
271 
valid_graphic_id(index_t graphic_id) const272 bool GameSpec::valid_graphic_id(index_t graphic_id) const {
273 	if (graphic_id <= 0 || this->graphics.count(graphic_id) == 0) {
274 		return false;
275 	}
276 	if (this->graphics.at(graphic_id)->slp_id <= 0) {
277 		return false;
278 	}
279 	return true;
280 }
281 
load_building(const gamedata::building_unit & building,unit_meta_list & list) const282 void GameSpec::load_building(const gamedata::building_unit &building, unit_meta_list &list) const {
283 
284 	// check graphics
285 	if (this->valid_graphic_id(building.graphic_standing0)) {
286 		auto meta_type = std::make_shared<UnitTypeMeta>("Building", building.id0, [this, &building](const Player &owner) {
287 			return std::make_shared<BuildingProducer>(owner, *this, &building);
288 		});
289 		list.emplace_back(meta_type);
290 	}
291 }
292 
load_living(const gamedata::living_unit & unit,unit_meta_list & list) const293 void GameSpec::load_living(const gamedata::living_unit &unit, unit_meta_list &list) const {
294 
295 	// check graphics
296 	if (this->valid_graphic_id(unit.dying_graphic) &&
297 		this->valid_graphic_id(unit.graphic_standing0) &&
298 		this->valid_graphic_id(unit.walking_graphics0)) {
299 		auto meta_type = std::make_shared<UnitTypeMeta>("Living", unit.id0, [this, &unit](const Player &owner) {
300 			return std::make_shared<LivingProducer>(owner, *this, &unit);
301 		});
302 		list.emplace_back(meta_type);
303 	}
304 }
305 
load_object(const gamedata::unit_object & object,unit_meta_list & list) const306 void GameSpec::load_object(const gamedata::unit_object &object, unit_meta_list &list) const {
307 
308 	// check graphics
309 	if (this->valid_graphic_id(object.graphic_standing0)) {
310 		auto meta_type = std::make_shared<UnitTypeMeta>("Object", object.id0, [this, &object](const Player &owner) {
311 			return std::make_shared<ObjectProducer>(owner, *this, &object);
312 		});
313 		list.emplace_back(meta_type);
314 	}
315 }
316 
load_missile(const gamedata::missile_unit & proj,unit_meta_list & list) const317 void GameSpec::load_missile(const gamedata::missile_unit &proj, unit_meta_list &list) const {
318 
319 	// check graphics
320 	if (this->valid_graphic_id(proj.graphic_standing0)) {
321 		auto meta_type = std::make_shared<UnitTypeMeta>("Projectile", proj.id0, [this, &proj](const Player &owner) {
322 			return std::make_shared<ProjectileProducer>(owner, *this, &proj);
323 		});
324 		list.emplace_back(meta_type);
325 	}
326 }
327 
328 
load_terrain(const gamedata::empiresdat & gamedata)329 void GameSpec::load_terrain(const gamedata::empiresdat &gamedata) {
330 
331 	// fetch blending modes
332 	util::Path convert_dir = this->assetmanager->get_asset_dir()["converted"];
333 	std::vector<gamedata::blending_mode> blending_meta = util::read_csv_file<gamedata::blending_mode>(
334 		convert_dir["blending_modes.docx"]
335 	);
336 
337 	// copy the terrain metainformation
338 	std::vector<gamedata::terrain_type> terrain_meta = gamedata.terrains.data;
339 
340 	// remove any disabled textures
341 	terrain_meta.erase(
342 		std::remove_if(
343 			terrain_meta.begin(),
344 			terrain_meta.end(),
345 			[] (const gamedata::terrain_type &t) {
346 				return not t.enabled;
347 			}
348 		),
349 		terrain_meta.end()
350 	);
351 
352 	// result attributes
353 	this->terrain_data.terrain_id_count         = terrain_meta.size();
354 	this->terrain_data.blendmode_count          = blending_meta.size();
355 	this->terrain_data.textures.resize(terrain_data.terrain_id_count);
356 	this->terrain_data.blending_masks.reserve(terrain_data.blendmode_count);
357 	this->terrain_data.terrain_id_priority_map  = std::make_unique<int[]>(
358 		this->terrain_data.terrain_id_count
359 	);
360 	this->terrain_data.terrain_id_blendmode_map = std::make_unique<int[]>(
361 		this->terrain_data.terrain_id_count
362 	);
363 	this->terrain_data.influences_buf           = std::make_unique<struct influence[]>(
364 		this->terrain_data.terrain_id_count
365 	);
366 
367 
368 	log::log(MSG(dbg) << "Terrain prefs: " <<
369 		"tiletypes=" << terrain_data.terrain_id_count << ", "
370 		"blendmodes=" << terrain_data.blendmode_count);
371 
372 	// create tile textures (snow, ice, grass, whatever)
373 	for (size_t terrain_id = 0;
374 	     terrain_id < terrain_data.terrain_id_count;
375 	     terrain_id++) {
376 
377 		auto line = &terrain_meta[terrain_id];
378 
379 		// TODO: terrain double-define check?
380 		terrain_data.terrain_id_priority_map[terrain_id]  = line->blend_priority;
381 		terrain_data.terrain_id_blendmode_map[terrain_id] = line->blend_mode;
382 
383 		// TODO: remove hardcoding and rely on nyan data
384 		auto terraintex_filename = util::sformat("converted/terrain/%d.slp.png",
385 		                                         line->slp_id);
386 
387 		auto new_texture = this->assetmanager->get_texture(terraintex_filename, true);
388 
389 		terrain_data.textures[terrain_id] = new_texture;
390 	}
391 
392 	// create blending masks (see doc/media/blendomatic)
393 	for (size_t i = 0; i < terrain_data.blendmode_count; i++) {
394 		auto line = &blending_meta[i];
395 
396 		// TODO: remove hardcodingn and use nyan data
397 		std::string mask_filename = util::sformat("converted/blendomatic/mode%02d.png",
398 		                                          line->blend_mode);
399 		terrain_data.blending_masks[i] = this->assetmanager->get_texture(mask_filename);
400 	}
401 }
402 
403 
create_abilities(const gamedata::empiresdat & gamedata)404 void GameSpec::create_abilities(const gamedata::empiresdat &gamedata) {
405 	// use game data unit commands
406 	int headers =  gamedata.unit_headers.data.size();
407 	int total = 0;
408 
409 	// it seems the index of the header indicates the unit
410 	for (int i = 0; i < headers; ++i) {
411 
412 		// init unit command vector
413 		std::vector<const gamedata::unit_command *> list;
414 
415 		// add each element
416 		auto &head = gamedata.unit_headers.data[i];
417 		for (auto &cmd : head.unit_commands.data) {
418 			total++;
419 
420 			// commands either have a class id or a unit id
421 			// log::dbg("unit command %d %d -> class %d, unit %d, resource %d", i, cmd.id, cmd.class_id, cmd.unit_id, cmd.resource_in);
422 			list.push_back(&cmd);
423 		}
424 
425 		// insert to command map
426 		this->commands[i] = list;
427 	}
428 }
429 
430 
play() const431 void Sound::play() const {
432 	if (this->sound_items.size() <= 0) {
433 		return;
434 	}
435 
436 	int rand = rng::random_range(0, this->sound_items.size());
437 	int sndid = this->sound_items.at(rand);
438 
439 	try {
440 		// TODO: buhuuuu gnargghh this has to be moved to the asset loading subsystem hnnnng
441 		audio::AudioManager &am = this->game_spec->get_asset_manager()->get_engine()->get_audio_manager();
442 
443 		if (not am.is_available()) {
444 			return;
445 		}
446 
447 		audio::Sound sound = am.get_sound(audio::category_t::GAME, sndid);
448 		sound.play();
449 	}
450 	catch (audio::Error &e) {
451 		log::log(MSG(warn) << "cannot play: " << e);
452 	}
453 }
454 
GameSpecHandle(qtsdl::GuiItemLink * gui_link)455 GameSpecHandle::GameSpecHandle(qtsdl::GuiItemLink *gui_link)
456 	:
457 	active{},
458 	asset_manager{},
459 	gui_signals{std::make_shared<GameSpecSignals>()},
460 	gui_link{gui_link} {
461 }
462 
set_active(bool active)463 void GameSpecHandle::set_active(bool active) {
464 	this->active = active;
465 
466 	this->start_loading_if_needed();
467 }
468 
set_asset_manager(AssetManager * asset_manager)469 void GameSpecHandle::set_asset_manager(AssetManager *asset_manager) {
470 	if (this->asset_manager != asset_manager) {
471 		this->asset_manager = asset_manager;
472 
473 		this->start_loading_if_needed();
474 	}
475 }
476 
is_ready() const477 bool GameSpecHandle::is_ready() const {
478 	return this->spec && this->spec->load_complete();
479 }
480 
invalidate()481 void GameSpecHandle::invalidate() {
482 	this->spec = nullptr;
483 
484 	if (this->asset_manager)
485 		this->asset_manager->check_updates();
486 
487 	this->start_loading_if_needed();
488 }
489 
announce_spec()490 void GameSpecHandle::announce_spec() {
491 	if (this->spec && this->spec->load_complete())
492 		emit this->gui_signals->game_spec_loaded(this->spec);
493 }
494 
get_spec()495 std::shared_ptr<GameSpec> GameSpecHandle::get_spec() {
496 	return this->spec;
497 }
498 
start_loading_if_needed()499 void GameSpecHandle::start_loading_if_needed() {
500 	if (this->active && this->asset_manager && !this->spec) {
501 
502 		// create the game specification
503 		this->spec = std::make_shared<GameSpec>(this->asset_manager);
504 
505 		// the load the data
506 		this->start_load_job();
507 	}
508 }
509 
start_load_job()510 void GameSpecHandle::start_load_job() {
511 	// store the shared pointers in another sharedptr
512 	// so we can pass them to the other thread
513 	auto spec_and_job = std::make_tuple(this->spec, this->gui_signals, job::Job<bool>{});
514 	auto spec_and_job_ptr = std::make_shared<decltype(spec_and_job)>(spec_and_job);
515 
516 	// lambda to be executed to actually load the data files.
517 	auto perform_load = [spec_and_job_ptr] {
518 		return std::get<std::shared_ptr<GameSpec>>(*spec_and_job_ptr)->initialize();
519 	};
520 
521 	auto load_finished = [gui_signals_ptr = this->gui_signals.get()] (job::result_function_t<bool> result) {
522 		bool load_ok;
523 		try {
524 			load_ok = result();
525 		}
526 		catch (Error &) {
527 			// TODO: display that error in the ui.
528 			throw;
529 		}
530 		catch (std::exception &) {
531 			// TODO: same here.
532 			throw Error{ERR << "gamespec loading failed!"};
533 		}
534 
535 		if (load_ok) {
536 			// send the signal that the load job was finished
537 			emit gui_signals_ptr->load_job_finished();
538 		}
539 	};
540 
541 	job::JobManager *job_mgr = this->asset_manager->get_engine()->get_job_manager();
542 
543 	std::get<job::Job<bool>>(*spec_and_job_ptr) = job_mgr->enqueue<bool>(
544 		perform_load, load_finished
545 	);
546 }
547 
548 } // openage
549