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