1 /*
2  * This file is part of EasyRPG Player.
3  *
4  * EasyRPG Player is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * EasyRPG Player is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with EasyRPG Player. If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 // Headers
19 #include <fstream>
20 #include <functional>
21 #include "game_system.h"
22 #include "async_handler.h"
23 #include "game_battle.h"
24 #include "audio.h"
25 #include "baseui.h"
26 #include "bitmap.h"
27 #include "cache.h"
28 #include "output.h"
29 #include "game_ineluki.h"
30 #include "transition.h"
31 #include "main_data.h"
32 #include "player.h"
33 #include <lcf/reader_util.h>
34 #include "scene_save.h"
35 #include "scene_map.h"
36 #include "utils.h"
37 
Game_System()38 Game_System::Game_System()
39 	: dbsys(&lcf::Data::system)
40 { }
41 
SetupFromSave(lcf::rpg::SaveSystem save)42 void Game_System::SetupFromSave(lcf::rpg::SaveSystem save) {
43 	data = std::move(save);
44 }
45 
GetSaveData() const46 const lcf::rpg::SaveSystem& Game_System::GetSaveData() const {
47 	return data;
48 }
49 
IsStopFilename(StringView name,Filesystem_Stream::InputStream (* find_func)(StringView),Filesystem_Stream::InputStream & found_stream)50 bool Game_System::IsStopFilename(StringView name, Filesystem_Stream::InputStream (*find_func) (StringView), Filesystem_Stream::InputStream& found_stream) {
51 	if (name.empty() || name == "(OFF)") {
52 		found_stream = Filesystem_Stream::InputStream();
53 		return true;
54 	}
55 
56 	found_stream = find_func(name);
57 
58 	return !found_stream && (name.starts_with('(') && name.ends_with(')'));
59 }
60 
IsStopMusicFilename(StringView name,Filesystem_Stream::InputStream & found_stream)61 bool Game_System::IsStopMusicFilename(StringView name, Filesystem_Stream::InputStream& found_stream) {
62 	return IsStopFilename(name, FileFinder::OpenMusic, found_stream);
63 }
64 
IsStopSoundFilename(StringView name,Filesystem_Stream::InputStream & found_stream)65 bool Game_System::IsStopSoundFilename(StringView name, Filesystem_Stream::InputStream& found_stream) {
66 	return IsStopFilename(name, FileFinder::OpenSound, found_stream);
67 }
68 
BgmPlay(lcf::rpg::Music const & bgm)69 void Game_System::BgmPlay(lcf::rpg::Music const& bgm) {
70 	lcf::rpg::Music previous_music = data.current_music;
71 	data.current_music = bgm;
72 
73 	// Validate
74 	if (bgm.volume < 0 || bgm.volume > 100) {
75 		data.current_music.volume = Utils::Clamp<int32_t>(bgm.volume, 0, 100);
76 
77 		Output::Debug("BGM {} has invalid volume {}", bgm.name, bgm.volume);
78 	}
79 
80 	if (bgm.fadein < 0 || bgm.fadein > 10000) {
81 		data.current_music.fadein = Utils::Clamp<int32_t>(bgm.fadein, 0, 10000);
82 
83 		Output::Debug("BGM {} has invalid fadein {}", bgm.name, bgm.fadein);
84 	}
85 
86 	if (bgm.tempo < 50 || bgm.tempo > 200) {
87 		data.current_music.tempo = Utils::Clamp<int32_t>(bgm.tempo, 50, 200);
88 
89 		Output::Debug("BGM {} has invalid tempo {}", bgm.name, bgm.tempo);
90 	}
91 
92 	// (OFF) means play nothing
93 	if (!bgm.name.empty() && bgm.name != "(OFF)") {
94 		// Same music: Only adjust volume and speed
95 		if (!data.music_stopping && previous_music.name == bgm.name) {
96 			if (previous_music.volume != data.current_music.volume) {
97 				if (!bgm_pending) { // Delay if not ready
98 					Audio().BGM_Volume(data.current_music.volume);
99 				}
100 			}
101 			if (previous_music.tempo != data.current_music.tempo) {
102 				if (!bgm_pending) { // Delay if not ready
103 					Audio().BGM_Pitch(data.current_music.tempo);
104 				}
105 			}
106 		} else {
107 			Audio().BGM_Stop();
108 			bgm_pending = true;
109 			FileRequestAsync* request = AsyncHandler::RequestFile("Music", bgm.name);
110 			music_request_id = request->Bind(&Game_System::OnBgmReady, this);
111 			request->Start();
112 		}
113 	} else {
114 		BgmStop();
115 	}
116 
117 	data.music_stopping = false;
118 }
119 
BgmStop()120 void Game_System::BgmStop() {
121 	music_request_id = FileRequestBinding();
122 	data.current_music.name = "(OFF)";
123 	Audio().BGM_Stop();
124 }
125 
BgmFade(int duration,bool clear_current_music)126 void Game_System::BgmFade(int duration, bool clear_current_music) {
127 	Audio().BGM_Fade(duration);
128 	if (clear_current_music) {
129 		data.current_music.name = "(OFF)";
130 	}
131 	data.music_stopping = true;
132 }
133 
SePlay(const lcf::rpg::Sound & se,bool stop_sounds)134 void Game_System::SePlay(const lcf::rpg::Sound& se, bool stop_sounds) {
135 	if (se.name.empty()) {
136 		return;
137 	} else if (se.name == "(OFF)") {
138 		if (stop_sounds) {
139 			Audio().SE_Stop();
140 		}
141 		return;
142 	}
143 
144 	// NOTE: Yume Nikki plays hundreds of sound effects at 0% volume on startup,
145 	// probably for caching. This avoids "No free channels" warnings.
146 	if (se.volume == 0)
147 		return;
148 
149 	int32_t volume = se.volume;
150 	int32_t tempo = se.tempo;
151 
152 	// Validate
153 	if (volume < 0 || volume > 100) {
154 		Output::Debug("SE {} has invalid volume {}", se.name, volume);
155 		volume = Utils::Clamp<int32_t>(volume, 0, 100);
156 	}
157 
158 	if (tempo < 50 || tempo > 200) {
159 		Output::Debug("SE {} has invalid tempo {}", se.name, tempo);
160 		tempo = Utils::Clamp<int32_t>(se.tempo, 50, 200);
161 	}
162 
163 	FileRequestAsync* request = AsyncHandler::RequestFile("Sound", se.name);
164 	lcf::rpg::Sound se_adj = se;
165 	se_adj.volume = volume;
166 	se_adj.tempo = tempo;
167 	se_request_ids[se.name] = request->Bind(&Game_System::OnSeReady, this, se_adj, stop_sounds);
168 	if (StringView(se.name).ends_with(".script")) {
169 		// Is a Ineluki Script File
170 		request->SetImportantFile(true);
171 	}
172 	request->Start();
173 }
174 
SePlay(const lcf::rpg::Animation & animation)175 void Game_System::SePlay(const lcf::rpg::Animation &animation) {
176 	Filesystem_Stream::InputStream stream;
177 	for (const auto& anim : animation.timings) {
178 		if (!IsStopSoundFilename(anim.se.name, stream)) {
179 			SePlay(anim.se);
180 			return;
181 		}
182 	}
183 }
184 
GetSystemName()185 StringView Game_System::GetSystemName() {
186 	return !data.graphics_name.empty() ?
187 		StringView(data.graphics_name) : StringView(lcf::Data::system.system_name);
188 }
189 
OnChangeSystemGraphicReady(FileRequestResult * result)190 void Game_System::OnChangeSystemGraphicReady(FileRequestResult* result) {
191 	Cache::SetSystemName(result->file);
192 	bg_color = Cache::SystemOrBlack()->GetBackgroundColor();
193 
194 	Scene_Map* scene = (Scene_Map*)Scene::Find(Scene::Map).get();
195 
196 	if (!scene)
197 		return;
198 
199 	scene->spriteset->SystemGraphicUpdated();
200 }
201 
ReloadSystemGraphic()202 void Game_System::ReloadSystemGraphic() {
203 	FileRequestAsync* request = AsyncHandler::RequestFile("System", GetSystemName());
204 	system_request_id = request->Bind(&Game_System::OnChangeSystemGraphicReady, this);
205 	request->SetImportantFile(true);
206 	request->SetGraphicFile(true);
207 	request->Start();
208 }
209 
SetSystemGraphic(const std::string & new_system_name,lcf::rpg::System::Stretch message_stretch,lcf::rpg::System::Font font)210 void Game_System::SetSystemGraphic(const std::string& new_system_name,
211 		lcf::rpg::System::Stretch message_stretch,
212 		lcf::rpg::System::Font font) {
213 
214 	bool changed = (GetSystemName() != new_system_name);
215 
216 	data.graphics_name = new_system_name;
217 	data.message_stretch = message_stretch;
218 	data.font_id = font;
219 
220 	if (changed) {
221 		ReloadSystemGraphic();
222 	}
223 }
224 
ResetSystemGraphic()225 void Game_System::ResetSystemGraphic() {
226 	data.graphics_name = "";
227 	data.message_stretch = (lcf::rpg::System::Stretch)0;
228 	data.font_id = (lcf::rpg::System::Font)0;
229 
230 	ReloadSystemGraphic();
231 }
232 
233 
234 template <typename T>
GetAudio(const T & save,const T & db)235 static const T& GetAudio(const T& save, const T& db) {
236 	return save.name.empty() ? db : save;
237 }
238 
239 template <typename T>
SetAudio(T & save,const T & db,T update)240 static void SetAudio(T& save, const T& db, T update) {
241 	if (update == db) {
242 		// RPG_RT only clears the name, but leaves the rest of the values alone
243 		save.name = {};
244 	} else {
245 		save = std::move(update);
246 	}
247 }
248 
GetSystemBGM(int which)249 const lcf::rpg::Music& Game_System::GetSystemBGM(int which) {
250 	switch (which) {
251 		case BGM_Battle:
252 			return GetAudio(data.battle_music, dbsys->battle_music);
253 		case BGM_Victory:
254 			return GetAudio(data.battle_end_music, dbsys->battle_end_music);
255 		case BGM_Inn:
256 			return GetAudio(data.inn_music, dbsys->inn_music);
257 		case BGM_Boat:
258 			return GetAudio(data.boat_music, dbsys->boat_music);
259 		case BGM_Ship:
260 			return GetAudio(data.ship_music, dbsys->ship_music);
261 		case BGM_Airship:
262 			return GetAudio(data.airship_music, dbsys->airship_music);
263 		case BGM_GameOver:
264 			return GetAudio(data.gameover_music, dbsys->gameover_music);
265 	}
266 
267 	static lcf::rpg::Music empty;
268 	return empty;
269 }
270 
SetSystemBGM(int which,lcf::rpg::Music bgm)271 void Game_System::SetSystemBGM(int which, lcf::rpg::Music bgm) {
272 	switch (which) {
273 		case BGM_Battle:
274 			SetAudio(data.battle_music, dbsys->battle_music, std::move(bgm));
275 			break;
276 		case BGM_Victory:
277 			SetAudio(data.battle_end_music, dbsys->battle_end_music, std::move(bgm));
278 			break;
279 		case BGM_Inn:
280 			SetAudio(data.inn_music, dbsys->inn_music, std::move(bgm));
281 			break;
282 		case BGM_Boat:
283 			SetAudio(data.boat_music, dbsys->boat_music, std::move(bgm));
284 			break;
285 		case BGM_Ship:
286 			SetAudio(data.ship_music, dbsys->ship_music, std::move(bgm));
287 			break;
288 		case BGM_Airship:
289 			SetAudio(data.airship_music, dbsys->airship_music, std::move(bgm));
290 			break;
291 		case BGM_GameOver:
292 			SetAudio(data.gameover_music, dbsys->gameover_music, std::move(bgm));
293 			break;
294 	}
295 }
296 
GetSystemSE(int which)297 const lcf::rpg::Sound& Game_System::GetSystemSE(int which) {
298 	switch (which) {
299 		case SFX_Cursor:
300 			return GetAudio(data.cursor_se, dbsys->cursor_se);
301 		case SFX_Decision:
302 			return GetAudio(data.decision_se, dbsys->decision_se);
303 		case SFX_Cancel:
304 			return GetAudio(data.cancel_se, dbsys->cancel_se);
305 		case SFX_Buzzer:
306 			return GetAudio(data.buzzer_se, dbsys->buzzer_se);
307 		case SFX_BeginBattle:
308 			return GetAudio(data.battle_se, dbsys->battle_se);
309 		case SFX_Escape:
310 			return GetAudio(data.escape_se, dbsys->escape_se);
311 		case SFX_EnemyAttacks:
312 			return GetAudio(data.enemy_attack_se, dbsys->enemy_attack_se);
313 		case SFX_EnemyDamage:
314 			return GetAudio(data.enemy_damaged_se, dbsys->enemy_damaged_se);
315 		case SFX_AllyDamage:
316 			return GetAudio(data.actor_damaged_se, dbsys->actor_damaged_se);
317 		case SFX_Evasion:
318 			return GetAudio(data.dodge_se, dbsys->dodge_se);
319 		case SFX_EnemyKill:
320 			return GetAudio(data.enemy_death_se, dbsys->enemy_death_se);
321 		case SFX_UseItem:
322 			return GetAudio(data.item_se, dbsys->item_se);
323 	}
324 
325 	static lcf::rpg::Sound empty;
326 	return empty;
327 }
328 
SetSystemSE(int which,lcf::rpg::Sound sfx)329 void Game_System::SetSystemSE(int which, lcf::rpg::Sound sfx) {
330 	switch (which) {
331 		case SFX_Cursor:
332 			SetAudio(data.cursor_se, dbsys->cursor_se, std::move(sfx));
333 			break;
334 		case SFX_Decision:
335 			SetAudio(data.decision_se, dbsys->decision_se, std::move(sfx));
336 			break;
337 		case SFX_Cancel:
338 			SetAudio(data.cancel_se, dbsys->cancel_se, std::move(sfx));
339 			break;
340 		case SFX_Buzzer:
341 			SetAudio(data.buzzer_se, dbsys->buzzer_se, std::move(sfx));
342 			break;
343 		case SFX_BeginBattle:
344 			SetAudio(data.battle_se, dbsys->battle_se, std::move(sfx));
345 			break;
346 		case SFX_Escape:
347 			SetAudio(data.escape_se, dbsys->escape_se, std::move(sfx));
348 			break;
349 		case SFX_EnemyAttacks:
350 			SetAudio(data.enemy_attack_se, dbsys->enemy_attack_se, std::move(sfx));
351 			break;
352 		case SFX_EnemyDamage:
353 			SetAudio(data.enemy_damaged_se, dbsys->enemy_damaged_se, std::move(sfx));
354 			break;
355 		case SFX_AllyDamage:
356 			SetAudio(data.actor_damaged_se, dbsys->actor_damaged_se, std::move(sfx));
357 			break;
358 		case SFX_Evasion:
359 			SetAudio(data.dodge_se, dbsys->dodge_se, std::move(sfx));
360 			break;
361 		case SFX_EnemyKill:
362 			SetAudio(data.enemy_death_se, dbsys->enemy_death_se, std::move(sfx));
363 			break;
364 		case SFX_UseItem:
365 			SetAudio(data.item_se, dbsys->item_se, std::move(sfx));
366 			break;
367 	}
368 }
369 
GetMessageStretch()370 lcf::rpg::System::Stretch Game_System::GetMessageStretch() {
371 	return static_cast<lcf::rpg::System::Stretch>(!data.graphics_name.empty()
372 		? data.message_stretch
373 		: lcf::Data::system.message_stretch);
374 }
375 
GetFontId()376 lcf::rpg::System::Font Game_System::GetFontId() {
377 	return static_cast<lcf::rpg::System::Font>(!data.graphics_name.empty()
378 		? data.font_id
379 		: lcf::Data::system.font_id);
380 }
381 
GetTransition(int which)382 Transition::Type Game_System::GetTransition(int which) {
383 	int transition = 0;
384 
385 	auto get = [&](int local, int db) {
386 		return local >= 0 ? local : db;
387 	};
388 
389 	switch (which) {
390 		case Transition_TeleportErase:
391 			transition = get(data.transition_out, lcf::Data::system.transition_out);
392 			break;
393 		case Transition_TeleportShow:
394 			transition = get(data.transition_in, lcf::Data::system.transition_in);
395 			break;
396 		case Transition_BeginBattleErase:
397 			transition = get(data.battle_start_fadeout, lcf::Data::system.battle_start_fadeout);
398 			break;
399 		case Transition_BeginBattleShow:
400 			transition = get(data.battle_start_fadein, lcf::Data::system.battle_start_fadein);
401 			break;
402 		case Transition_EndBattleErase:
403 			transition = get(data.battle_end_fadeout, lcf::Data::system.battle_end_fadeout);
404 			break;
405 		case Transition_EndBattleShow:
406 			transition = get(data.battle_end_fadein, lcf::Data::system.battle_end_fadein);
407 			break;
408 		default: assert(false && "Bad transition");
409 	}
410 
411 	constexpr int num_types = 21;
412 
413 	if (transition < 0 || transition >= num_types) {
414 		Output::Warning("Invalid transition value {}", transition);
415 		transition = Utils::Clamp(transition, 0, num_types - 1);
416 	}
417 
418 	constexpr Transition::Type fades[2][num_types] = {
419 		{
420 			Transition::TransitionFadeOut,
421 			Transition::TransitionRandomBlocks,
422 			Transition::TransitionRandomBlocksDown,
423 			Transition::TransitionRandomBlocksUp,
424 			Transition::TransitionBlindClose,
425 			Transition::TransitionVerticalStripesOut,
426 			Transition::TransitionHorizontalStripesOut,
427 			Transition::TransitionBorderToCenterOut,
428 			Transition::TransitionCenterToBorderOut,
429 			Transition::TransitionScrollUpOut,
430 			Transition::TransitionScrollDownOut,
431 			Transition::TransitionScrollLeftOut,
432 			Transition::TransitionScrollRightOut,
433 			Transition::TransitionVerticalDivision,
434 			Transition::TransitionHorizontalDivision,
435 			Transition::TransitionCrossDivision,
436 			Transition::TransitionZoomIn,
437 			Transition::TransitionMosaicOut,
438 			Transition::TransitionWaveOut,
439 			Transition::TransitionCutOut,
440 			Transition::TransitionNone
441 		},
442 		{
443 			Transition::TransitionFadeIn,
444 			Transition::TransitionRandomBlocks,
445 			Transition::TransitionRandomBlocksDown,
446 			Transition::TransitionRandomBlocksUp,
447 			Transition::TransitionBlindOpen,
448 			Transition::TransitionVerticalStripesIn,
449 			Transition::TransitionHorizontalStripesIn,
450 			Transition::TransitionBorderToCenterIn,
451 			Transition::TransitionCenterToBorderIn,
452 			Transition::TransitionScrollUpIn,
453 			Transition::TransitionScrollDownIn,
454 			Transition::TransitionScrollLeftIn,
455 			Transition::TransitionScrollRightIn,
456 			Transition::TransitionVerticalCombine,
457 			Transition::TransitionHorizontalCombine,
458 			Transition::TransitionCrossCombine,
459 			Transition::TransitionZoomOut,
460 			Transition::TransitionMosaicIn,
461 			Transition::TransitionWaveIn,
462 			Transition::TransitionCutIn,
463 			Transition::TransitionNone,
464 		}
465 	};
466 
467 	return fades[which % 2][transition];
468 }
469 
SetTransition(int which,int transition)470 void Game_System::SetTransition(int which, int transition) {
471 	auto set = [&](int t, int db) {
472 		return t != db ? t : -1;
473 	};
474 	switch (which) {
475 		case Transition_TeleportErase:
476 			data.transition_out = set(transition, lcf::Data::system.transition_out);
477 			break;
478 		case Transition_TeleportShow:
479 			data.transition_in = set(transition, lcf::Data::system.transition_in);
480 			break;
481 		case Transition_BeginBattleErase:
482 			data.battle_start_fadeout = set(transition, lcf::Data::system.battle_start_fadeout);
483 			break;
484 		case Transition_BeginBattleShow:
485 			data.battle_start_fadein = set(transition, lcf::Data::system.battle_start_fadein);
486 			break;
487 		case Transition_EndBattleErase:
488 			data.battle_end_fadeout = set(transition, lcf::Data::system.battle_end_fadeout);
489 			break;
490 		case Transition_EndBattleShow:
491 			data.battle_end_fadein = set(transition, lcf::Data::system.battle_end_fadein);
492 			break;
493 		default: assert(false && "Bad transition");
494 	}
495 }
496 
OnBgmReady(FileRequestResult * result)497 void Game_System::OnBgmReady(FileRequestResult* result) {
498 	// Take from current_music, params could have changed over time
499 	bgm_pending = false;
500 
501 	Filesystem_Stream::InputStream stream;
502 	if (IsStopMusicFilename(result->file, stream)) {
503 		Audio().BGM_Stop();
504 		return;
505 	} else if (!stream) {
506 		Output::Debug("Music not found: {}", result->file);
507 		return;
508 	}
509 
510 	if (StringView(result->file).ends_with(".link")) {
511 		// Handle Ineluki's MP3 patch
512 		if (!stream) {
513 			Output::Warning("Ineluki MP3: Link read error: {}", stream.GetName());
514 			return;
515 		}
516 
517 		// The first line contains the path to the actual audio file to play
518 		std::string line;
519 		if (!Utils::ReadLine(stream, line)) {
520 			Output::Warning("Ineluki MP3: Link file is empty: {}", stream.GetName());
521 			return;
522 		}
523 		line = lcf::ReaderUtil::Recode(line, Player::encoding);
524 
525 		Output::Debug("Ineluki MP3: Link file: {} -> {}", stream.GetName(), line);
526 		std::string line_canonical = FileFinder::MakeCanonical(line, 1);
527 
528 		// Needs another Async roundtrip
529 		bgm_pending = true;
530 		FileRequestAsync *request = AsyncHandler::RequestFile(line_canonical);
531 		music_request_id = request->Bind(&Game_System::OnBgmInelukiReady, this);
532 		request->Start();
533 		return;
534 	}
535 
536 	Audio().BGM_Play(std::move(stream), data.current_music.volume, data.current_music.tempo, data.current_music.fadein);
537 }
538 
OnBgmInelukiReady(FileRequestResult * result)539 void Game_System::OnBgmInelukiReady(FileRequestResult* result) {
540 	bgm_pending = false;
541 	Audio().BGM_Play(FileFinder::Game().OpenFile(result->file), data.current_music.volume, data.current_music.tempo, data.current_music.fadein);
542 }
543 
OnSeReady(FileRequestResult * result,lcf::rpg::Sound se,bool stop_sounds)544 void Game_System::OnSeReady(FileRequestResult* result, lcf::rpg::Sound se, bool stop_sounds) {
545 	auto item = se_request_ids.find(result->file);
546 	if (item != se_request_ids.end()) {
547 		se_request_ids.erase(item);
548 	}
549 
550 	if (StringView(se.name).ends_with(".script")) {
551 		// Is a Ineluki Script File
552 		Main_Data::game_ineluki->Execute(se);
553 		return;
554 	}
555 
556 	Filesystem_Stream::InputStream stream;
557 	if (IsStopSoundFilename(result->file, stream)) {
558 		if (stop_sounds) {
559 			Audio().SE_Stop();
560 		}
561 		return;
562 	} else if (!stream) {
563 		Output::Debug("Sound not found: {}", result->file);
564 		return;
565 	}
566 
567 	Audio().SE_Play(std::move(stream), se.volume, se.tempo);
568 }
569 
IsMessageTransparent()570 bool Game_System::IsMessageTransparent() {
571 	if (Player::IsRPG2k() && Game_Battle::IsBattleRunning()) {
572 		return false;
573 	}
574 
575 	return data.message_transparent != 0;
576 }
577 
578