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