diff --git a/CMakeLists.txt b/CMakeLists.txt index a57c27d7d5..f720ae0450 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1052,7 +1052,7 @@ if(${PLAYER_AUDIO_BACKEND} MATCHES "^(SDL2|SDL1|libretro|psvita|3ds|switch|wii|a player_find_package(NAME FluidLite CONDITION PLAYER_WITH_FLUIDLITE DEFINITION HAVE_FLUIDLITE - TARGET FluidLite::fluidlite) + TARGET "fluidlite::fluidlite;fluidlite::fluidlite-static") endif() # xmp (lite) @@ -1506,7 +1506,7 @@ if(${PLAYER_AUDIO_BACKEND} MATCHES "^(SDL2|SDL1|libretro|psvita|3ds|switch|wii)$ if(TARGET FluidSynth::libfluidsynth) list(APPEND MIDI_LIBS "FluidSynth") endif() - if(TARGET FluidLite::fluidlite) + if(TARGET fluidlite::fluidlite OR TARGET fluidlite::fluidlite-static) list(APPEND MIDI_LIBS "FluidLite") endif() if(TARGET WildMidi::libwildmidi OR TARGET WildMidi::libwildmidi-static) diff --git a/builds/cmake/Modules/FindFluidLite.cmake b/builds/cmake/Modules/FindFluidLite.cmake index df9b7e09f1..dbee65c077 100644 --- a/builds/cmake/Modules/FindFluidLite.cmake +++ b/builds/cmake/Modules/FindFluidLite.cmake @@ -9,7 +9,7 @@ # # This module defines the following :prop_tgt:`IMPORTED` targets: # -# ``FluidLite::fluidlite`` +# ``fluidlite::fluidlite`` # The ``FluidLite`` library, if found. # # Result Variables @@ -53,9 +53,9 @@ if(FLUIDLITE_FOUND) set(FLUIDLITE_LIBRARIES ${FLUIDLITE_LIBRARIES}) endif() - if(NOT TARGET FluidLite::fluidlite) - add_library(FluidLite::fluidlite UNKNOWN IMPORTED) - set_target_properties(FluidLite::fluidlite PROPERTIES + if(NOT TARGET fluidlite::fluidlite) + add_library(fluidlite::fluidlite UNKNOWN IMPORTED) + set_target_properties(fluidlite::fluidlite PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${FLUIDLITE_INCLUDE_DIRS}" IMPORTED_LOCATION "${FLUIDLITE_LIBRARY}") endif() diff --git a/resources/emscripten/emscripten-post.js b/resources/emscripten/emscripten-post.js index 7277533b83..1344420f7f 100644 --- a/resources/emscripten/emscripten-post.js +++ b/resources/emscripten/emscripten-post.js @@ -14,7 +14,7 @@ if (Module.saveFs === undefined) { } Module.initApi = function() { - Module.api_private.download_js = function(buffer, size, filename) { + Module.api_private.download_js = function (buffer, size, filename) { const blob = new Blob([Module.HEAPU8.slice(buffer, buffer + size)]); const link = document.createElement('a'); link.href = window.URL.createObjectURL(blob); @@ -23,28 +23,62 @@ Module.initApi = function() { link.remove(); } - Module.api_private.uploadSavegame_js = function(slot) { - let saveFile = document.getElementById('easyrpg_saveFile'); - if (saveFile == null) { - saveFile = document.createElement('input'); - saveFile.type = 'file'; - saveFile.id = 'easyrpg_saveFile'; - saveFile.style.display = 'none'; - saveFile.addEventListener('change', function(evt) { - const save = evt.target.files[0]; + Module.api_private.createInputElement_js = function (id, event) { + let file = document.getElementById(id); + if (file == null) { + file = document.createElement('input'); + file.type = 'file'; + file.id = id; + file.style.display = 'none'; + file.addEventListener('change', function (evt) { + const selected_file = evt.target.files[0]; const reader = new FileReader(); - reader.onload = function (file) { - const result = new Uint8Array(file.currentTarget.result); - var buf = Module._malloc(result.length); - Module.HEAPU8.set(result, buf); - Module.api_private.uploadSavegameStep2(slot, buf, result.length); - Module._free(buf); - Module.api.refreshScene(); - }; - reader.readAsArrayBuffer(save); + reader.onload = function(file) { + event(file, selected_file.name); + } + reader.readAsArrayBuffer(selected_file); }); } - saveFile.click(); + file.click(); + } + + Module.api_private.uploadSavegame_js = function (slot) { + Module.api_private.createInputElement_js('easyrpg_saveFile', function (file) { + const result = new Uint8Array(file.currentTarget.result); + var buf = Module._malloc(result.length); + Module.HEAPU8.set(result, buf); + Module.api_private.uploadSavegameStep2(slot, buf, result.length); + Module._free(buf); + Module.api.refreshScene(); + }); + } + + Module.api_private.uploadSoundfont_js = function () { + Module.api_private.createInputElement_js('easyrpg_sfFile', function (file, name) { + const result = new Uint8Array(file.currentTarget.result); + //const name_buf = Module._malloc(name.length + 1); + //stringToUTF8(name, name_buf, name.length + 1); + const content_buf = Module._malloc(result.length); + Module.HEAPU8.set(result, content_buf); + Module.api_private.uploadSoundfontStep2(name, content_buf, result.length); + //Module._free(name_buf); + Module._free(content_buf); + Module.api.refreshScene(); + }); + } + + Module.api_private.uploadFont_js = function () { + Module.api_private.createInputElement_js('easyrpg_sfFile', function (file, name) { + const result = new Uint8Array(file.currentTarget.result); + //const name_buf = Module._malloc(name.length + 1); + //stringToUTF8(name, name_buf, name.length + 1); + const content_buf = Module._malloc(result.length); + Module.HEAPU8.set(result, content_buf); + Module.api_private.uploadFontStep2(name, content_buf, result.length); + //Module._free(name_buf); + Module._free(content_buf); + Module.api.refreshScene(); + }); } } diff --git a/resources/unix/bash-completion/easyrpg-player b/resources/unix/bash-completion/easyrpg-player index 6203cac778..470cff85c7 100644 --- a/resources/unix/bash-completion/easyrpg-player +++ b/resources/unix/bash-completion/easyrpg-player @@ -14,7 +14,7 @@ _easyrpg-player () # all possible options ouropts='--autobattle-algo --battle-test --disable-audio --disable-rtp \ - --encoding --enemyai-algo --engine --fps-limit --fps-render-window --fullscreen -h --help \ + --encoding --enemyai-algo --engine --fps-limit --fullscreen -h --help \ --hide-title --load-game-id --new-game --no-vsync --project-path --rtp-path --record-input \ --replay-input --save-path --seed --show-fps --start-map-id --start-party --no-log-color \ --start-position --test-play --window -v --version' diff --git a/resources/unix/easyrpg-player.6.adoc b/resources/unix/easyrpg-player.6.adoc index 0a4081d08e..27c45b6fe5 100644 --- a/resources/unix/easyrpg-player.6.adoc +++ b/resources/unix/easyrpg-player.6.adoc @@ -62,6 +62,25 @@ NOTE: For games that only use ASCII (English games) use '1252'. - 'rpg2k3v105' - RPG Maker 2003 (v1.05 - v1.09a) - 'rpg2k3e' - RPG Maker 2003 RPG Maker 2003 (English release, v1.12) +*--font1* _FILE_:: + Path to a font to use for the first font. The system graphic of the game + determines whether font 1 or 2 is used. When no font is selected or the + selected font lacks certain glyphs the built-in pixel font is used. + +*--font1-size* _PX_:: + Size of font 1 in pixel. The default value is 12. + +*--font2* _FILE_:: + Path to a font to use for the second font. See font 1 for further information. + +*--font2-size* _PX_:: + Size of font 2 in pixel. The default value is 12. + +*--font-path* _PATH_:: + Configures the path where the settings scene looks for fonts. The user can + choose from any font in the directory. This is more flexible than using + *--font1* or *--font2* directly. The default path is 'config-path/Font'. + *--language* _LANG_:: Loads the game translation in language/'LANG' folder. @@ -166,6 +185,10 @@ NOTE: When using the game browser all games will share the same save directory! - 'widescreen' - 416x240 (16:9) - 'ultrawide' - 560x240 (21:9) +*--pause-focus-lost*:: + Pause the game when the window has no focus. Can be disabled with + *--no-pause-focus-lost*. + *--scaling* _MODE_:: How the video output is scaled. Possible options: - 'nearest' - Scale to screen size using nearest neighbour algorithm. @@ -175,8 +198,10 @@ NOTE: When using the game browser all games will share the same save directory! - 'bilinear' - Like 'nearest' but apply a bilinear filter to avoid the artifacts. *--show-fps*:: - Enable display of the frames per second counter. Can be disabled with - *--no-show-fps*. + Enable display of the frames per second counter. When in windowed mode it is + shown inside the window. When in fullscreen mode it is shown in the titlebar. + Use *--fps-render-window* to always show the counter inside the window. Can be + disabled with *--no-show-fps*. *--stretch*:: Ignore the aspect ratio and stretch video output to the entire width of the @@ -206,6 +231,10 @@ NOTE: When using the game browser all games will share the same save directory! Adds 'FILE' to the list of soundfonts used for playing MIDI files and use this one with highest precedence. The soundfont must be in SF2 format. +*--soundfont-path* _P_:: + Configures the path where the settings scene looks for soundfonts. The user + can choose from any soundfont in the directory. This is more flexible than + using *--soundfont* directly. The default path is 'config-path/Soundfont'. === Debug options diff --git a/src/audio.cpp b/src/audio.cpp index 0ce51b1f0e..a892540a81 100644 --- a/src/audio.cpp +++ b/src/audio.cpp @@ -17,6 +17,7 @@ // Headers #include "audio.h" +#include "audio_midi.h" #include "system.h" #include "baseui.h" #include "player.h" @@ -70,6 +71,21 @@ AudioInterface::AudioInterface(const Game_ConfigAudio& cfg) : cfg(cfg) { Game_ConfigAudio AudioInterface::GetConfig() const { auto acfg = cfg; acfg.Hide(); + +#if !defined(HAVE_FLUIDSYNTH) && !defined(HAVE_FLUIDLITE) + acfg.fluidsynth_midi.SetOptionVisible(false); + acfg.soundfont.SetOptionVisible(false); +#endif +#ifndef HAVE_LIBWILDMIDI + acfg.wildmidi_midi.SetOptionVisible(false); +#endif +#ifndef HAVE_NATIVE_MIDI + acfg.native_midi.SetOptionVisible(false); +#endif +#ifndef WANT_FMMIDI + acfg.fmmidi_midi.SetOptionVisible(false); +#endif + vGetConfig(acfg); return acfg; } @@ -89,3 +105,36 @@ int AudioInterface::SE_GetGlobalVolume() const { void AudioInterface::SE_SetGlobalVolume(int volume) { cfg.sound_volume.Set(volume); } + +bool AudioInterface::GetFluidsynthEnabled() const { + return cfg.fluidsynth_midi.Get(); +} + +void AudioInterface::SetFluidsynthEnabled(bool enable) { + cfg.fluidsynth_midi.Set(enable); +} + +bool AudioInterface::GetWildMidiEnabled() const { + return cfg.wildmidi_midi.Get(); +} + +void AudioInterface::SetWildMidiEnabled(bool enable) { + cfg.wildmidi_midi.Set(enable); +} + +bool AudioInterface::GetNativeMidiEnabled() const { + return cfg.native_midi.Get(); +} + +void AudioInterface::SetNativeMidiEnabled(bool enable) { + cfg.native_midi.Set(enable); +} + +std::string AudioInterface::GetFluidsynthSoundfont() const { + return cfg.soundfont.Get(); +} + +void AudioInterface::SetFluidsynthSoundfont(StringView sf) { + cfg.soundfont.Set(ToString(sf)); + MidiDecoder::ChangeFluidsynthSoundfont(sf); +} diff --git a/src/audio.h b/src/audio.h index bb0e6b7ab9..7a90137997 100644 --- a/src/audio.h +++ b/src/audio.h @@ -20,6 +20,7 @@ // Headers #include +#include "audio_generic_midiout.h" #include "filesystem_stream.h" #include "audio_secache.h" #include "game_config.h" @@ -39,6 +40,15 @@ struct AudioInterface { virtual void vGetConfig(Game_ConfigAudio& cfg) const = 0; + /** + * Instantiates and returns the Native Midi Out device. + * The Midi Out device is usually an exclusive resource. + * Implementing classes should only create one Midi Out and return the + * shared instance. + * @return Native Midi Out Device or nullptr on error + */ + virtual GenericAudioMidiOut* CreateAndGetMidiOut() { return nullptr; } + /** * Update audio. Must be called each frame. */ @@ -133,6 +143,18 @@ struct AudioInterface { int SE_GetGlobalVolume() const; void SE_SetGlobalVolume(int volume); + bool GetFluidsynthEnabled() const; + void SetFluidsynthEnabled(bool enable); + + bool GetWildMidiEnabled() const; + void SetWildMidiEnabled(bool enable); + + bool GetNativeMidiEnabled() const; + void SetNativeMidiEnabled(bool enable); + + std::string GetFluidsynthSoundfont() const; + void SetFluidsynthSoundfont(StringView sf); + protected: Game_ConfigAudio cfg; }; diff --git a/src/audio_decoder_midi.cpp b/src/audio_decoder_midi.cpp index 31b773ede7..cc40a639be 100644 --- a/src/audio_decoder_midi.cpp +++ b/src/audio_decoder_midi.cpp @@ -17,6 +17,7 @@ #include #include +#include "audio.h" #include "audio_decoder_midi.h" #include "midisequencer.h" #include "output.h" @@ -142,11 +143,11 @@ bool AudioDecoderMidi::Open(Filesystem_Stream::InputStream stream) { error_message = "Midi: Error reading file"; return false; } + seq->rewind(); tempo.clear(); tempo.emplace_back(this, midi_default_tempo); mtime = seq->get_start_skipping_silence(); - seq->play(mtime, this); if (!mididec->SupportsMidiMessages()) { if (!mididec->Open(file_buffer)) { @@ -171,7 +172,7 @@ void AudioDecoderMidi::Pause() { void AudioDecoderMidi::Resume() { paused = false; for (int i = 0; i < 16; i++) { - uint32_t msg = midimsg_volume(i, static_cast(channel_volumes[i] * volume)); + uint32_t msg = midimsg_volume(i, static_cast(channel_volumes[i] * volume * global_volume)); mididec->SendMidiMessage(msg); } } @@ -192,7 +193,7 @@ void AudioDecoderMidi::SetVolume(int new_volume) { volume = static_cast(new_volume) / 100.0f; for (int i = 0; i < 16; i++) { - uint32_t msg = midimsg_volume(i, static_cast(channel_volumes[i] * volume)); + uint32_t msg = midimsg_volume(i, static_cast(channel_volumes[i] * volume * global_volume)); mididec->SendMidiMessage(msg); } @@ -256,7 +257,7 @@ void AudioDecoderMidi::Update(std::chrono::microseconds delta) { log_volume = AdjustVolume(volume * 100.0f); } for (int i = 0; i < 16; i++) { - uint32_t msg = midimsg_volume(i, static_cast(channel_volumes[i] * volume)); + uint32_t msg = midimsg_volume(i, static_cast(channel_volumes[i] * volume * global_volume)); mididec->SendMidiMessage(msg); } last_fade_mtime = mtime; @@ -265,12 +266,24 @@ void AudioDecoderMidi::Update(std::chrono::microseconds delta) { } void AudioDecoderMidi::UpdateMidi(std::chrono::microseconds delta) { + // Only called when MidiOut is used + if (paused) { return; } mtime += std::chrono::microseconds(static_cast(delta.count() * pitch / 100)); + + if (Audio().BGM_GetGlobalVolume() / 100.0f != global_volume) { + global_volume = Audio().BGM_GetGlobalVolume() / 100.0f; + for (int i = 0; i < 16; i++) { + uint32_t msg = midimsg_volume(i, static_cast(channel_volumes[i] * volume * global_volume)); + mididec->SendMidiMessage(msg); + } + } + Update(delta); + seq->play(mtime, this); if (IsFinished() && looping) { @@ -308,6 +321,9 @@ int AudioDecoderMidi::GetTicks() const { } void AudioDecoderMidi::Reset() { + // Placed here to avoid reloading of a soundfont on shutdown + mididec->OnNewMidi(); + // Generate a MIDI reset event so the device doesn't // leave notes playing or keeps any state reset(); @@ -377,7 +393,7 @@ void AudioDecoderMidi::midi_message(int, uint_least32_t message) { // Adjust channel volume channel_volumes[channel] = value2; // Send the modified volume to midiout - message = midimsg_volume(channel, static_cast(value2 * volume)); + message = midimsg_volume(channel, static_cast(value2 * volume * global_volume)); } if (midimsg_validate(message)) { mididec->SendMidiMessage(message); diff --git a/src/audio_decoder_midi.h b/src/audio_decoder_midi.h index b72e456dc2..0fa96f8ada 100644 --- a/src/audio_decoder_midi.h +++ b/src/audio_decoder_midi.h @@ -176,6 +176,7 @@ class AudioDecoderMidi final : public AudioDecoderBase, public midisequencer::ou float pitch = 1.0f; bool paused = false; float volume = 0.0f; + float global_volume = 1.0f; // only used by midiout float log_volume = 0.0f; // as used by RPG_RT, for Midi decoder without event support bool loops_to_end = false; diff --git a/src/audio_generic.cpp b/src/audio_generic.cpp index 1135064b9e..6985cd0dc9 100644 --- a/src/audio_generic.cpp +++ b/src/audio_generic.cpp @@ -198,6 +198,19 @@ void GenericAudio::Update() { // no-op, handled by the Decode function called through a thread } +GenericAudioMidiOut* GenericAudio::CreateAndGetMidiOut() { + if (!midi_thread) { + midi_thread = std::make_unique(); + std::string status_message; + if (midi_thread->IsInitialized(status_message)) { + midi_thread->StartThread(); + } else { + midi_thread.reset(); + } + } + return midi_thread.get(); +} + void GenericAudio::SetFormat(int frequency, AudioDecoder::Format format, int channels) { output_format.frequency = frequency; output_format.format = format; @@ -217,18 +230,12 @@ bool GenericAudio::PlayOnChannel(BgmChannel& chan, Filesystem_Stream::InputStrea if (chan.id == 0 && GenericAudioMidiOut::IsSupported(filestream)) { chan.decoder.reset(); - // FIXME: Try Fluidsynth and WildMidi first - // If they work fallback to the normal AudioDecoder handler below - // There should be a way to configure the order - if (!MidiDecoder::CreateFluidsynth(true) && !MidiDecoder::CreateWildMidi(true)) { - if (!midi_thread) { - midi_thread = std::make_unique(); - if (midi_thread->IsInitialized()) { - midi_thread->StartThread(); - } else { - midi_thread.reset(); - } - } + // Order is Fluidsynth, WildMidi, Native, FmMidi + bool fluidsynth = Audio().GetFluidsynthEnabled() && MidiDecoder::CreateFluidsynth(true); + bool wildmidi = Audio().GetWildMidiEnabled() && MidiDecoder::CreateWildMidi(true); + + if (!fluidsynth && !wildmidi && Audio().GetNativeMidiEnabled()) { + CreateAndGetMidiOut(); if (midi_thread) { midi_thread->LockMutex(); @@ -300,7 +307,7 @@ void GenericAudio::Decode(uint8_t* output_buffer, int buffer_length) { if (scrap_buffer.size() != scrap_buffer_size) { scrap_buffer.resize(scrap_buffer_size); } - memset(mixer_buffer.data(), '\0', mixer_buffer.size()); + std::fill(mixer_buffer.begin(), mixer_buffer.end(), '\0'); for (unsigned i = 0; i < nr_of_bgm_channels + nr_of_se_channels; i++) { int read_bytes = 0; diff --git a/src/audio_generic.h b/src/audio_generic.h index 04755a2c7d..ab1ab3cf5e 100644 --- a/src/audio_generic.h +++ b/src/audio_generic.h @@ -61,6 +61,8 @@ class GenericAudio : public AudioInterface { void vGetConfig(Game_ConfigAudio&) const override {} + GenericAudioMidiOut* CreateAndGetMidiOut() override; + void SetFormat(int frequency, AudioDecoder::Format format, int channels); virtual void LockMutex() const = 0; diff --git a/src/audio_generic_midiout.cpp b/src/audio_generic_midiout.cpp index 31911e4142..7f75e22c2a 100644 --- a/src/audio_generic_midiout.cpp +++ b/src/audio_generic_midiout.cpp @@ -23,6 +23,7 @@ #include #include "filesystem_stream.h" #include "game_clock.h" +#include "output.h" #ifdef USE_LIBRETRO #include "platform/libretro/midiout_device.h" @@ -43,18 +44,21 @@ static struct { bool alsa = true; bool win32 = true; bool coreaudio = true; + std::string status; } works; GenericAudioMidiOut::GenericAudioMidiOut() { stop_thread.store(false); #ifdef USE_LIBRETRO + std::string libretro_status; if (works.libretro) { - auto dec = std::make_unique(); + auto dec = std::make_unique(libretro_status); if (dec->IsInitialized()) { midi_out = std::make_unique(std::move(dec)); } else { works.libretro = false; + Output::Debug(libretro_status); } } @@ -65,32 +69,39 @@ GenericAudioMidiOut::GenericAudioMidiOut() { #ifdef HAVE_ALSA if (works.alsa) { - auto dec = std::make_unique(); + auto dec = std::make_unique(works.status); if (dec->IsInitialized()) { midi_out = std::make_unique(std::move(dec)); } else { works.alsa = false; + Output::Debug(works.status); } } #elif _WIN32 if (works.win32) { - auto dec = std::make_unique(); + auto dec = std::make_unique(works.status); if (dec->IsInitialized()) { midi_out = std::make_unique(std::move(dec)); } else { works.win32 = false; + Output::Debug(works.status); } } #elif __APPLE__ if (works.coreaudio) { - auto dec = std::make_unique(); + auto dec = std::make_unique(works.status); if (dec->IsInitialized()) { midi_out = std::make_unique(std::move(dec)); } else { works.coreaudio = false; + Output::Debug(works.status); } } #endif + +#ifdef USE_LIBRETRO + works.status = libretro_status + ". " + works.status; +#endif } GenericAudioMidiOut::~GenericAudioMidiOut() { @@ -145,7 +156,8 @@ void GenericAudioMidiOut::ThreadFunction() { } } -bool GenericAudioMidiOut::IsInitialized() const { +bool GenericAudioMidiOut::IsInitialized(std::string& status_message) const { + status_message = works.status; return midi_out != nullptr; } diff --git a/src/audio_generic_midiout.h b/src/audio_generic_midiout.h index f9538a3bf1..1f252bde5f 100644 --- a/src/audio_generic_midiout.h +++ b/src/audio_generic_midiout.h @@ -52,7 +52,7 @@ class GenericAudioMidiOut final { void StartThread(); void StopThread(); void ThreadFunction(); - bool IsInitialized() const; + bool IsInitialized(std::string& status_message) const; static bool IsSupported(Filesystem_Stream::InputStream& stream); private: @@ -79,7 +79,7 @@ class GenericAudioMidiOut final { void StartThread() {}; void StopThread() {}; - bool IsInitialized() const { + bool IsInitialized(std::string&) const { return false; } diff --git a/src/audio_midi.cpp b/src/audio_midi.cpp index 011fb480b4..e4b027a3a6 100644 --- a/src/audio_midi.cpp +++ b/src/audio_midi.cpp @@ -25,6 +25,8 @@ #ifdef USE_AUDIO_RESAMPLER #include "audio_resampler.h" +#include "audio.h" + #endif void MidiDecoder::GetFormat(int& freq, AudioDecoderBase::Format& format, int& channels) const { @@ -43,17 +45,23 @@ bool MidiDecoder::SetFormat(int frequency, AudioDecoderBase::Format format, int static struct { bool fluidsynth = true; bool wildmidi = true; + std::string fluidsynth_status; + std::string wildmidi_status; } works; std::unique_ptr MidiDecoder::Create(bool resample) { std::unique_ptr mididec; - mididec = CreateFluidsynth(resample); - if (!mididec) { + if (Audio().GetFluidsynthEnabled()) { + mididec = CreateFluidsynth(resample); + } + + if (!mididec && Audio().GetWildMidiEnabled()) { mididec = CreateWildMidi(resample); - if (!mididec) { - mididec = CreateFmMidi(resample); - } + } + + if (!mididec) { + mididec = CreateFmMidi(resample); } return mididec; @@ -63,13 +71,12 @@ std::unique_ptr MidiDecoder::CreateFluidsynth(bool resample) { std::unique_ptr mididec; #if defined(HAVE_FLUIDSYNTH) || defined(HAVE_FLUIDLITE) - std::string error_message; - if (works.fluidsynth && FluidSynthDecoder::Initialize(error_message)) { + if (works.fluidsynth && FluidSynthDecoder::Initialize(works.fluidsynth_status)) { auto dec = std::make_unique(); mididec = std::make_unique(std::move(dec)); } else if (!mididec && works.fluidsynth) { - Output::Debug("{}", error_message); + Output::Debug("Fluidsynth: {}", works.fluidsynth_status); works.fluidsynth = false; } #endif @@ -87,13 +94,12 @@ std::unique_ptr MidiDecoder::CreateWildMidi(bool resample) { std::unique_ptr mididec; #ifdef HAVE_LIBWILDMIDI - std::string error_message; - if (!mididec && works.wildmidi && WildMidiDecoder::Initialize(error_message)) { + if (!mididec && works.wildmidi && WildMidiDecoder::Initialize(works.wildmidi_status)) { auto dec = std::make_unique(); mididec = std::make_unique(std::move(dec)); } else if (!mididec && works.wildmidi) { - Output::Debug("{}", error_message); + Output::Debug("WildMidi: {}", works.wildmidi_status); works.wildmidi = false; } #endif @@ -126,6 +132,39 @@ std::unique_ptr MidiDecoder::CreateFmMidi(bool resample) { return mididec; } +bool MidiDecoder::CheckFluidsynth(std::string& status_message) { + if (works.fluidsynth && works.fluidsynth_status.empty()) { + CreateFluidsynth(true); + } + + status_message = works.fluidsynth_status; + return works.fluidsynth; +} + +void MidiDecoder::ChangeFluidsynthSoundfont(StringView sf_path) { + if (!works.fluidsynth || works.fluidsynth_status.empty()) { + // Fluidsynth was not initialized yet or failed, will use the path from the config automatically + works.fluidsynth = true; + CreateFluidsynth(true); + return; + } + +#if defined(HAVE_FLUIDSYNTH) || defined(HAVE_FLUIDLITE) + // Was initialized before + works.fluidsynth = FluidSynthDecoder::ChangeGlobalSoundfont(sf_path, works.fluidsynth_status); + Output::Debug("Fluidsynth: {}", works.fluidsynth_status); +#endif +} + +bool MidiDecoder::CheckWildMidi(std::string &status_message) { + if (works.wildmidi && works.wildmidi_status.empty()) { + CreateWildMidi(true); + } + + status_message = works.wildmidi_status; + return works.wildmidi; +} + void MidiDecoder::Reset() { works.fluidsynth = true; works.wildmidi = true; diff --git a/src/audio_midi.h b/src/audio_midi.h index 82a70635e5..88a58a0c35 100644 --- a/src/audio_midi.h +++ b/src/audio_midi.h @@ -180,6 +180,11 @@ class MidiDecoder { return true; } + /** + * Notifies the sequencer that a new MIDI is about to start. + */ + virtual void OnNewMidi() {}; + /* * Does the sequencer need "sound off" messages sent to every channel between * tracks? NOTE: enabling this can break smooth fade outs between tracks. @@ -204,6 +209,24 @@ class MidiDecoder { static std::unique_ptr CreateFmMidi(bool resample); + /** + * Checks if Fluidsynth works. + * + * @param status_message Current Fluidsynth status + * @return true: Works, false: Not working + */ + static bool CheckFluidsynth(std::string& status_message); + + static void ChangeFluidsynthSoundfont(StringView sf_path); + + /** + * Checks if WildMidi works. + * + * @param status_message Current WildMidi status + * @return true: Works, false: Not working + */ + static bool CheckWildMidi(std::string& status_message); + /** * Resets the global state of the midi libraries. */ diff --git a/src/baseui.cpp b/src/baseui.cpp index cc09be92fc..482d9d02cb 100644 --- a/src/baseui.cpp +++ b/src/baseui.cpp @@ -68,7 +68,7 @@ void BaseUi::CleanDisplay() { main_surface->Clear(); } -void BaseUi::SetGameResolution(GameResolution resolution) { +void BaseUi::SetGameResolution(ConfigEnum::GameResolution resolution) { vcfg.game_resolution.Set(resolution); } @@ -85,11 +85,8 @@ Game_ConfigVideo BaseUi::GetConfig() const { cfg.window_width.Set(metrics.width); cfg.window_height.Set(metrics.height); - if (cfg.fullscreen.IsOptionVisible()) { - cfg.fps_render_window.SetLocked(cfg.fullscreen.Get()); - if (cfg.fps_render_window.IsLocked()) { - cfg.fps_render_window.SetDescription("This options requires to be in windowed mode"); - } + if (!cfg.fullscreen.IsOptionVisible()) { + cfg.fps.RemoveFromValidSet(ConfigEnum::ShowFps::Overlay); } if (cfg.vsync.IsOptionVisible() diff --git a/src/baseui.h b/src/baseui.h index 266c3ee4ad..622f14ff83 100644 --- a/src/baseui.h +++ b/src/baseui.h @@ -188,11 +188,17 @@ class BaseUi { /** @return true if we should render the fps counter to the title bar */ bool ShowFpsOnTitle() const; - /** Toggle whether we should show fps */ + /** Toggle between showing FPS or not showing FPS */ void ToggleShowFps(); - /** Toggle wheter we should show fps on the titlebar */ - void ToggleShowFpsOnTitle(); + /** Whether we should show fps */ + void SetShowFps(ConfigEnum::ShowFps fps); + + /** + * Set whether the program pauses the execution when the program focus is lost. + * @param value + */ + void SetPauseWhenFocusLost(bool value); /** * @return the minimum amount of time each physical frame should take. @@ -209,7 +215,7 @@ class BaseUi { void SetFrameLimit(int fps_limit); /** Sets the scaling mode of the window */ - virtual void SetScalingMode(ScalingMode) {}; + virtual void SetScalingMode(ConfigEnum::ScalingMode) {}; /** * Sets the game resolution settings. @@ -218,7 +224,16 @@ class BaseUi { * * @param resolution new resolution */ - void SetGameResolution(GameResolution resolution); + void SetGameResolution(ConfigEnum::GameResolution resolution); + + /** + * Opens the specified URL through the operating system. + * Opens a file browser when file:// is provided. + * + * @param url URL to open + * @return true when successful + */ + virtual bool OpenURL(StringView path) { (void)path; return false; } /** Toggles "stretch to screen width" on or off */ virtual void ToggleStretch() {}; @@ -277,7 +292,7 @@ class BaseUi { /** Touch inputs for up to five finger */ std::array touch_input; - /** */ + /** State of the 5 fingers (true touched, false not) */ std::array finger_input; /** Color for display background. */ @@ -294,6 +309,9 @@ class BaseUi { /** Ui manages frame rate externally */ bool external_frame_rate = false; + + /** Used by the F2 toggle: Remembers which configuration (ON or Overlay) was used */ + ConfigEnum::ShowFps original_fps_show_state = ConfigEnum::ShowFps::OFF; }; /** Global DisplayUi variable. */ @@ -362,19 +380,33 @@ inline std::array& BaseUi::GetTouchInput() { } inline bool BaseUi::RenderFps() const { - return vcfg.show_fps.Get() && (IsFullscreen() || vcfg.fps_render_window.Get()); + return vcfg.fps.Get() == ConfigEnum::ShowFps::Overlay || (IsFullscreen() && vcfg.fps.Get() == ConfigEnum::ShowFps::ON); } inline bool BaseUi::ShowFpsOnTitle() const { - return vcfg.show_fps.Get(); + return vcfg.fps.Get() == ConfigEnum::ShowFps::ON; } inline void BaseUi::ToggleShowFps() { - vcfg.show_fps.Toggle(); + if (vcfg.fps.Get() != ConfigEnum::ShowFps::OFF) { + original_fps_show_state = vcfg.fps.Get(); + vcfg.fps.Set(ConfigEnum::ShowFps::OFF); + } else { + if (original_fps_show_state == ConfigEnum::ShowFps::OFF) { + vcfg.fps.Set(ConfigEnum::ShowFps::ON); + } else { + vcfg.fps.Set(original_fps_show_state); + } + } +} + +inline void BaseUi::SetShowFps(ConfigEnum::ShowFps fps) { + vcfg.fps.Set(fps); + original_fps_show_state = fps; } -inline void BaseUi::ToggleShowFpsOnTitle() { - vcfg.fps_render_window.Toggle(); +inline void BaseUi::SetPauseWhenFocusLost(bool value) { + vcfg.pause_when_focus_lost.Set(value); } inline Game_Clock::duration BaseUi::GetFrameLimit() const { diff --git a/src/bitmap.cpp b/src/bitmap.cpp index d74a358e95..c7785efbd0 100644 --- a/src/bitmap.cpp +++ b/src/bitmap.cpp @@ -333,20 +333,20 @@ void Bitmap::HueChangeBlit(int x, int y, Bitmap const& src, Rect const& src_rect } Point Bitmap::TextDraw(Rect const& rect, int color, StringView text, Text::Alignment align) { - FontRef font = Font::Default(); - switch (align) { case Text::AlignLeft: return TextDraw(rect.x, rect.y, color, text); break; case Text::AlignCenter: { - Rect text_rect = Text::GetSize(*font, text); + auto f = font ? font : Font::Default(); + Rect text_rect = Text::GetSize(*f, text); int dx = rect.x + (rect.width - text_rect.width) / 2; return TextDraw(dx, rect.y, color, text); break; } case Text::AlignRight: { - Rect text_rect = Text::GetSize(*font, text); + auto f = font ? font : Font::Default(); + Rect text_rect = Text::GetSize(*f, text); int dx = rect.x + rect.width - text_rect.width; return TextDraw(dx, rect.y, color, text); break; @@ -354,30 +354,30 @@ Point Bitmap::TextDraw(Rect const& rect, int color, StringView text, Text::Align default: assert(false); } - return Point(); + return {}; } Point Bitmap::TextDraw(int x, int y, int color, StringView text, Text::Alignment align) { - auto font = Font::Default(); + auto f = font ? font : Font::Default(); auto system = Cache::SystemOrBlack(); - return Text::Draw(*this, x, y, *font, *system, color, text, align); + return Text::Draw(*this, x, y, *f, *system, color, text, align); } Point Bitmap::TextDraw(Rect const& rect, Color color, StringView text, Text::Alignment align) { - FontRef font = Font::Default(); - switch (align) { case Text::AlignLeft: return TextDraw(rect.x, rect.y, color, text); break; case Text::AlignCenter: { - Rect text_rect = Text::GetSize(*font, text); + auto f = font ? font : Font::Default(); + Rect text_rect = Text::GetSize(*f, text); int dx = rect.x + (rect.width - text_rect.width) / 2; return TextDraw(dx, rect.y, color, text); break; } case Text::AlignRight: { - Rect text_rect = Text::GetSize(*font, text); + auto f = font ? font : Font::Default(); + Rect text_rect = Text::GetSize(*f, text); int dx = rect.x + rect.width - text_rect.width; return TextDraw(dx, rect.y, color, text); break; @@ -385,12 +385,12 @@ Point Bitmap::TextDraw(Rect const& rect, Color color, StringView text, Text::Ali default: assert(false); } - return Point(); + return {}; } Point Bitmap::TextDraw(int x, int y, Color color, StringView text) { - auto font = Font::Default(); - return Text::Draw(*this, x, y, *font, color, text); + auto f = font ? font : Font::Default(); + return Text::Draw(*this, x, y, *f, color, text); } Rect Bitmap::TransformRectangle(const Transform& xform, const Rect& rect) { diff --git a/src/bitmap.h b/src/bitmap.h index b7be3f1dbf..e10fb14268 100644 --- a/src/bitmap.h +++ b/src/bitmap.h @@ -241,7 +241,7 @@ class Bitmap { Color GetColorAt(int x, int y) const; /** - * Draws text to bitmap using the Font::Default() font. + * Draws text to bitmap using the configured Font or the Font::Default() font. * * @param x x coordinate where text rendering starts. * @param y y coordinate where text rendering starts. @@ -253,7 +253,7 @@ class Bitmap { Point TextDraw(int x, int y, int color, StringView text, Text::Alignment align = Text::AlignLeft); /** - * Draws text to bitmap using the Font::Default() font. + * Draws text to bitmap using the configured Font or the Font::Default() font. * * @param rect bounding rectangle. * @param color system color index. @@ -264,7 +264,7 @@ class Bitmap { Point TextDraw(Rect const& rect, int color, StringView text, Text::Alignment align = Text::AlignLeft); /** - * Draws text to bitmap using the Font::Default() font. + * Draws text to bitmap using the configured Font or the Font::Default() font. * * @param x x coordinate where text rendering starts. * @param y y coordinate where text rendering starts. @@ -275,7 +275,7 @@ class Bitmap { Point TextDraw(int x, int y, Color color, StringView text); /** - * Draws text to bitmap using the Font::Default() font. + * Draws text to bitmap using the configured Font or the Font::Default() font. * * @param rect bounding rectangle. * @param color text color. @@ -594,6 +594,9 @@ class Bitmap { int bpp() const; int pitch() const; + FontRef GetFont() const; + void SetFont(FontRef font); + ImageOpacity ComputeImageOpacity() const; ImageOpacity ComputeImageOpacity(Rect rect) const; @@ -603,6 +606,7 @@ class Bitmap { ImageOpacity image_opacity = ImageOpacity::Alpha_8Bit; TileOpacity tile_opacity; Color bg_color, sh_color; + FontRef font; std::string filename; @@ -689,6 +693,14 @@ inline StringView Bitmap::GetFilename() const { return filename; } +inline FontRef Bitmap::GetFont() const { + return font; +} + +inline void Bitmap::SetFont(FontRef font) { + this->font = font; +} + inline int Bitmap::GetOriginalBpp() const { return original_bpp; } diff --git a/src/config_param.h b/src/config_param.h index 036503eb12..6b5e5e701c 100644 --- a/src/config_param.h +++ b/src/config_param.h @@ -19,6 +19,7 @@ #define EP_CONFIG_PARAM_H #include "string_view.h" +#include "filefinder.h" #include #include #include @@ -30,10 +31,6 @@ #include namespace { - inline const std::string& ParamValueToString(const std::string& s) { - return s; - } - inline std::string ParamValueToString(StringView s) { return ToString(s); } @@ -41,6 +38,14 @@ namespace { inline std::string ParamValueToString(int i) { return std::to_string(i); } + + inline std::string ParamValueToString(double d) { + return std::to_string(d); + } + + inline std::string ParamValueToString(bool b) { + return b ? "[ON]" : "[OFF]"; + } } /** @@ -76,7 +81,7 @@ class ConfigParamBase { } if (IsValid(value)) { - _value = std::move(value); + _value = value; return true; } return false; @@ -181,30 +186,19 @@ class ConfigParamBase { /** @return human readable representation of the value for the settings scene */ virtual std::string ValueToString() const = 0; - template ::value, int>::type = 0> - bool FromIni(const lcf::INIReader& ini) { + virtual bool FromIni(const lcf::INIReader& ini) { // FIXME: Migrate IniReader to StringView (or std::string_view with C++17) if (ini.HasValue(ToString(_config_section), ToString(_config_key))) { - Set(ini.GetString(ToString(_config_section), ToString(_config_key), T())); - return true; - } - return false; - } - - template ::value, int>::type = 0> - bool FromIni(const lcf::INIReader& ini) { - if (ini.HasValue(ToString(_config_section), ToString(_config_key))) { - Set(ini.GetInteger(ToString(_config_section), ToString(_config_key), T())); - return true; - } - return false; - } - - template ::value, int>::type = 0> - bool FromIni(const lcf::INIReader& ini) { - if (ini.HasValue(ToString(_config_section), ToString(_config_key))) { - Set(ini.GetBoolean(ToString(_config_section), ToString(_config_key), T())); - return true; + if constexpr (std::is_same_v) { + Set(ini.GetString(ToString(_config_section), ToString(_config_key), T())); + return true; + } else if constexpr (std::is_same_v) { + Set(ini.GetInteger(ToString(_config_section), ToString(_config_key), T())); + return true; + } else if constexpr (std::is_same_v) { + Set(ini.GetBoolean(ToString(_config_section), ToString(_config_key), T())); + return true; + } } return false; } @@ -337,16 +331,17 @@ class BoolConfigParam : public ConfigParamBase { return true; } - void Toggle() { + bool Toggle() { if (Get()) { Set(false); } else { Set(true); } + return Get(); } std::string ValueToString() const override { - return Get() ? "[ON]" : "[OFF]"; + return ParamValueToString(Get()); } }; @@ -404,8 +399,7 @@ class EnumConfigParam : public ConfigParamBase { return false; } - template ::value, int>::type = 0> - bool FromIni(const lcf::INIReader& ini) { + bool FromIni(const lcf::INIReader& ini) override { if (ini.HasValue(ToString(this->_config_section), ToString(this->_config_key))) { std::string s = ini.GetString(ToString(this->_config_section), ToString(this->_config_key), std::string()); for (size_t i = 0; i < _tags.size(); ++i) { @@ -442,4 +436,14 @@ class EnumConfigParam : public ConfigParamBase { } }; +class PathConfigParam : public StringConfigParam { +public: + PathConfigParam(StringView name, StringView description, StringView config_section, StringView config_key, std::string value) : + StringConfigParam(name, description, config_section, config_key, value) {} + + std::string ValueToString() const override { + return std::get<1>(FileFinder::GetPathAndFilename(Get())); + } +}; + #endif diff --git a/src/decoder_fluidsynth.cpp b/src/decoder_fluidsynth.cpp index 2c6506d023..f171ff7d2f 100644 --- a/src/decoder_fluidsynth.cpp +++ b/src/decoder_fluidsynth.cpp @@ -16,6 +16,7 @@ */ #include "system.h" +#include "audio.h" #include "decoder_fluidsynth.h" #if defined(HAVE_FLUIDSYNTH) || defined(HAVE_FLUIDLITE) @@ -93,13 +94,6 @@ static fluid_fileapi_t fluidlite_vio = { }; #endif -namespace { - std::string preferred_soundfont; - - bool once = false; - bool init = false; -} - struct FluidSynthDeleter { void operator()(fluid_settings_t* s) const { delete_fluid_settings(s); @@ -111,27 +105,20 @@ struct FluidSynthDeleter { }; namespace { + bool once = false; + bool init = false; + std::unique_ptr global_settings; std::unique_ptr global_synth; -#if defined(HAVE_FLUIDSYNTH) && FLUIDSYNTH_VERSION_MAJOR > 1 - fluid_sfloader_t* global_loader; // owned by global_settings -#endif + std::unique_ptr pending_global_synth; + int instances = 0; } -static fluid_synth_t* create_synth(std::string& error_message) { - fluid_synth_t* syn = new_fluid_synth(global_settings.get()); - if (!syn) { - error_message = "new_fluid_synth failed"; - return nullptr; - } - -#if defined(HAVE_FLUIDSYNTH) && FLUIDSYNTH_VERSION_MAJOR > 1 - fluid_synth_add_sfloader(syn, global_loader); -#endif - +static bool load_default_sf(std::string& status_message, fluid_synth_t* syn) { // Attempt loading a soundfont std::vector sf_paths; + std::string preferred_soundfont = Audio().GetFluidsynthSoundfont(); if (!preferred_soundfont.empty()) { sf_paths.emplace_back(preferred_soundfont); } @@ -139,7 +126,7 @@ static fluid_synth_t* create_synth(std::string& error_message) { #if FLUIDSYNTH_VERSION_MAJOR >= 2 char* default_sf = nullptr; - if (fluid_settings_dupstr(global_settings.get(), "synth.default-soundfont", &default_sf) == FLUID_OK) { + if (fluid_settings_dupstr(global_settings.get(), "synth.default-soundfont", &default_sf) == 0) { if (default_sf != nullptr && default_sf[0] != '\0') { sf_paths.emplace_back(default_sf); } @@ -158,26 +145,47 @@ static fluid_synth_t* create_synth(std::string& error_message) { sf_paths.insert(sf_paths.end(), sdl_sfs.begin(), sdl_sfs.end()); } +#ifdef SYSTEM_DESKTOP_LINUX_BSD_MACOS auto sf_files = {"FluidR3_GM.sf2"}; for (const auto& sf_file: sf_files) { sf_paths.emplace_back(FileFinder::MakePath("/usr/share/soundfonts", sf_file)); sf_paths.emplace_back(FileFinder::MakePath("/usr/share/sounds/sf2", sf_file)); } +#endif bool sf_load_success = false; for (const auto& sf_name: sf_paths) { if (fluid_synth_sfload(syn, sf_name.c_str(), 1) != FLUID_FAILED) { sf_load_success = true; - Output::Debug("Fluidsynth: Using soundfont {}", sf_name); + status_message = fmt::format("Using soundfont {}", sf_name); break; } } if (!sf_load_success) { - error_message = "Fluidsynth: Could not load soundfont."; + status_message = "Could not load soundfont."; + return false; + } + + return true; +} + +static fluid_synth_t* create_synth(std::string& status_message) { + fluid_synth_t* syn = new_fluid_synth(global_settings.get()); + if (!syn) { + status_message = "new_fluid_synth failed"; return nullptr; } +#if defined(HAVE_FLUIDSYNTH) && FLUIDSYNTH_VERSION_MAJOR > 1 + // Fluidsynth 1.x does not support VIO API for soundfonts + // owned by fluid_synth + auto* loader = new_fluid_defsfloader(global_settings.get()); + fluid_sfloader_set_callbacks(loader, + vio_open, vio_read, vio_seek, vio_tell, vio_close); + fluid_synth_add_sfloader(syn, loader); +#endif + fluid_synth_set_interp_method(syn, -1, FLUID_INTERP_LINEAR); return syn; @@ -190,6 +198,7 @@ FluidSynthDecoder::FluidSynthDecoder() { // Sharing is only not possible when a Midi is played as a SE (unlikely) if (instances > 1) { std::string error_message; + int unused = -1; local_synth = create_synth(error_message); if (!local_synth) { // unlikely, the SF was already allocated once @@ -210,10 +219,22 @@ FluidSynthDecoder::~FluidSynthDecoder() { } } -bool FluidSynthDecoder::Initialize(std::string& error_message) { +bool FluidSynthDecoder::Initialize(std::string& status_message) { // only initialize once until a new game starts - if (once) + if (once) { + if (!init && global_settings && !global_synth) { + global_synth.reset(create_synth(status_message)); + if (global_synth) { + if (!load_default_sf(status_message, global_synth.get())) { + global_synth.reset(); + } + } + + init = (global_synth != nullptr); + } return init; + } + once = true; #ifdef HAVE_FLUIDLITE @@ -239,19 +260,16 @@ bool FluidSynthDecoder::Initialize(std::string& error_message) { fluid_settings_setstr(global_settings.get(), "synth.chorus.active", "no"); #endif - // Fluidsynth 1.x does not support VIO API for soundfonts -#if defined(HAVE_FLUIDSYNTH) && FLUIDSYNTH_VERSION_MAJOR > 1 - // owned by fluid_settings - global_loader = new_fluid_defsfloader(global_settings.get()); - fluid_sfloader_set_callbacks(global_loader, - vio_open, vio_read, vio_seek, vio_tell, vio_close); -#endif - - global_synth.reset(create_synth(error_message)); + global_synth.reset(create_synth(status_message)); if (!global_synth) { return false; } + if (!load_default_sf(status_message, global_synth.get())) { + global_synth.reset(); + return false; + } + init = true; return init; @@ -263,10 +281,32 @@ void FluidSynthDecoder::ResetState() { global_synth.reset(); global_settings.reset(); + pending_global_synth.reset(); } -void FluidSynthDecoder::SetSoundfont(StringView sf) { - preferred_soundfont = ToString(sf); +bool FluidSynthDecoder::ChangeGlobalSoundfont(StringView sf_path, std::string& status_message) { + if (!global_synth) { + return false; + } + + pending_global_synth.reset(create_synth(status_message)); + + if (!pending_global_synth) { + return false; + } + + if (sf_path.empty()) { + return load_default_sf(status_message, pending_global_synth.get()); + } + + if (fluid_synth_sfload(pending_global_synth.get(), ToString(sf_path).c_str(), 1) != FLUID_FAILED) { + status_message = fmt::format("Using soundfont {}", sf_path); + return true; + } + + pending_global_synth.reset(); + status_message = "Could not load soundfont."; + return false; } int FluidSynthDecoder::FillBuffer(uint8_t* buffer, int length) { @@ -347,6 +387,12 @@ fluid_synth_t *FluidSynthDecoder::GetSynthInstance() { } } +void FluidSynthDecoder::OnNewMidi() { + if (pending_global_synth) { + global_synth = std::move(pending_global_synth); + } +} + bool FluidSynthDecoder::NeedsSoftReset() { return true; } diff --git a/src/decoder_fluidsynth.h b/src/decoder_fluidsynth.h index f0799413e3..42c28a9148 100644 --- a/src/decoder_fluidsynth.h +++ b/src/decoder_fluidsynth.h @@ -44,16 +44,9 @@ class FluidSynthDecoder : public MidiDecoder { FluidSynthDecoder(); ~FluidSynthDecoder() override; - static bool Initialize(std::string& error_message); + static bool Initialize(std::string& status_message); static void ResetState(); - - /** - * Sets the name of the preferred soundfont. - * Must be called before the first MIDI is played. - * - * @param sf soundfont to check for first - */ - static void SetSoundfont(StringView sf); + static bool ChangeGlobalSoundfont(StringView sf_path, std::string& status_message); int FillBuffer(uint8_t* buffer, int length) override; @@ -69,6 +62,8 @@ class FluidSynthDecoder : public MidiDecoder { #endif }; + void OnNewMidi() override; + bool NeedsSoftReset() override; private: diff --git a/src/decoder_wildmidi.cpp b/src/decoder_wildmidi.cpp index d4738e9612..a4d00cd7c8 100644 --- a/src/decoder_wildmidi.cpp +++ b/src/decoder_wildmidi.cpp @@ -84,7 +84,7 @@ WildMidiDecoder::~WildMidiDecoder() { WildMidi_Close(handle); } -bool WildMidiDecoder::Initialize(std::string& error_message) { +bool WildMidiDecoder::Initialize(std::string& status_message) { std::string config_file; bool found = false; @@ -282,10 +282,11 @@ bool WildMidiDecoder::Initialize(std::string& error_message) { // bail, if nothing found if (!found) { - error_message = "WildMidi: Could not find configuration file."; + status_message = "Could not find configuration file."; return false; } - Output::Debug("WildMidi: Using {} as configuration file...", config_file); + + status_message = fmt::format("Using {} as configuration file...", config_file); #if LIBWILDMIDI_VERSION >= 1027 // at least 0.4.3 init = (WildMidi_InitVIO(&vio, config_file.c_str(), EP_MIDI_FREQ, WILDMIDI_OPTS) == 0); @@ -294,7 +295,7 @@ bool WildMidiDecoder::Initialize(std::string& error_message) { #endif if (!init) { - error_message = std::string("WildMidi_Init() failed : ") + WildMidi_GetError(); + status_message = std::string("WildMidi_Init() failed: ") + WildMidi_GetError(); return false; } diff --git a/src/decoder_wildmidi.h b/src/decoder_wildmidi.h index 4142b3e09e..8c32e60f59 100644 --- a/src/decoder_wildmidi.h +++ b/src/decoder_wildmidi.h @@ -33,7 +33,7 @@ class WildMidiDecoder : public MidiDecoder { public: ~WildMidiDecoder(); - static bool Initialize(std::string& error_message); + static bool Initialize(std::string& status_message); static void ResetState(); // Audio Decoder interface diff --git a/src/exe_reader.cpp b/src/exe_reader.cpp index 921d91f5e2..efb2ab6e23 100644 --- a/src/exe_reader.cpp +++ b/src/exe_reader.cpp @@ -185,7 +185,7 @@ std::vector> EXEReader::GetLogos() { return {}; } - if (Player::player_config.show_startup_logos.Get() == StartupLogos::None) { + if (Player::player_config.show_startup_logos.Get() == ConfigEnum::StartupLogos::None) { return {}; } @@ -199,7 +199,7 @@ std::vector> EXEReader::GetLogos() { uint16_t xyz_logos = std::min(GetU16(xyz_base + 0x0C), 9); uint32_t xyz_logo_base = xyz_base + 0x10; - bool only_custom_logos = (Player::player_config.show_startup_logos.Get() == StartupLogos::Custom); + bool only_custom_logos = (Player::player_config.show_startup_logos.Get() == ConfigEnum::StartupLogos::Custom); std::string res_name = "LOGOX"; for (int i = 0; i <= xyz_logos; ++i) { diff --git a/src/filefinder.cpp b/src/filefinder.cpp index cc6b9004cc..e78ae58053 100644 --- a/src/filefinder.cpp +++ b/src/filefinder.cpp @@ -57,18 +57,9 @@ namespace { auto MOVIE_TYPES = { ".avi", ".mpg" }; #endif - std::string fonts_path; std::shared_ptr root_fs; FilesystemView game_fs; FilesystemView save_fs; - - constexpr const auto IMG_TYPES = Utils::MakeSvArray(".bmp", ".png", ".xyz"); - constexpr const auto MUSIC_TYPES = Utils::MakeSvArray( - ".opus", ".oga", ".ogg", ".wav", ".mid", ".midi", ".mp3", ".wma"); - constexpr const auto SOUND_TYPES = Utils::MakeSvArray( - ".opus", ".oga", ".ogg", ".wav", ".mp3", ".wma"); - constexpr const auto FONTS_TYPES = Utils::MakeSvArray(".fon", ".fnt", ".bdf", ".ttf", ".ttc", ".otf", ".woff2", ".woff"); - constexpr const auto TEXT_TYPES = Utils::MakeSvArray(".txt", ".csv", ""); // "" = Complete Filename (incl. extension) provided by the user } FilesystemView FileFinder::Game() { diff --git a/src/filefinder.h b/src/filefinder.h index f7cfe62293..3bd774afcf 100644 --- a/src/filefinder.h +++ b/src/filefinder.h @@ -37,6 +37,14 @@ * insensitive files paths. */ namespace FileFinder { + constexpr const auto IMG_TYPES = Utils::MakeSvArray(".bmp", ".png", ".xyz"); + constexpr const auto MUSIC_TYPES = Utils::MakeSvArray( + ".opus", ".oga", ".ogg", ".wav", ".mid", ".midi", ".mp3", ".wma"); + constexpr const auto SOUND_TYPES = Utils::MakeSvArray( + ".opus", ".oga", ".ogg", ".wav", ".mp3", ".wma"); + constexpr const auto FONTS_TYPES = Utils::MakeSvArray(".fon", ".fnt", ".bdf", ".ttf", ".ttc", ".otf", ".woff2", ".woff"); + constexpr const auto TEXT_TYPES = Utils::MakeSvArray(".txt", ".csv", ""); // "" = Complete Filename (incl. extension) provided by the user + /** * Quits FileFinder. */ diff --git a/src/font.cpp b/src/font.cpp index 415e53ad59..29ee2e220f 100644 --- a/src/font.cpp +++ b/src/font.cpp @@ -136,6 +136,7 @@ namespace { FTFont(Filesystem_Stream::InputStream is, int size, bool bold, bool italic); ~FTFont() override; + bool IsOk() const; Rect vGetSize(char32_t glyph) const override; GlyphRet vRender(char32_t glyph) const override; GlyphRet vRenderShaped(char32_t glyph) const override; @@ -282,6 +283,10 @@ FTFont::FTFont(Filesystem_Stream::InputStream is, int size, bool bold, bool ital FT_New_Memory_Face(library, ft_buffer.data(), ft_buffer.size(), 0, &face); + if (face == nullptr) { + return; + } + if (face->num_charmaps > 0) { // Force unicode charmap if (FT_Select_Charmap(face, FT_ENCODING_UNICODE) != 0) { @@ -319,21 +324,33 @@ FTFont::~FTFont() { } } +bool FTFont::IsOk() const { + return face; +} + Rect FTFont::vGetSize(char32_t glyph) const { auto glyph_index = FT_Get_Char_Index(face, glyph); - if (glyph_index == 0 && fallback_font) { - return fallback_font->vGetSize(glyph); + if (glyph_index == 0) { + if (fallback_font) { + return fallback_font->vGetSize(glyph); + } else { + return {0, 0, 0, current_style.size}; + } } auto load_glyph = [&](auto flags) { if (FT_Load_Glyph(face, glyph_index, flags) != FT_Err_Ok) { - Output::Error("Couldn't load FreeType character {:#x}", uint32_t(glyph)); + Output::Debug("Couldn't load FreeType character {:#x}", uint32_t(glyph)); + return false; } + return true; }; if (FT_HAS_COLOR(face)) { - load_glyph(FT_LOAD_COLOR); + if (!load_glyph(FT_LOAD_COLOR)) { + Output::Error("Broken color font for glyph {:#x}", uint32_t(glyph)); + } // When it is a color font check if the glyph is a color glyph // If it is not then reload the glyph monochrome @@ -341,7 +358,13 @@ Rect FTFont::vGetSize(char32_t glyph) const { load_glyph(FT_LOAD_MONOCHROME | FT_LOAD_TARGET_MONO); } } else { - load_glyph(FT_LOAD_MONOCHROME | FT_LOAD_TARGET_MONO); + if (!load_glyph(FT_LOAD_MONOCHROME | FT_LOAD_TARGET_MONO)) { + if (fallback_font) { + return fallback_font->vGetSize(glyph); + } else { + return {0, 0, 0, current_style.size}; + } + } } FT_GlyphSlot slot = face->glyph; @@ -360,30 +383,44 @@ Rect FTFont::vGetSize(char32_t glyph) const { Font::GlyphRet FTFont::vRender(char32_t glyph) const { auto glyph_index = FT_Get_Char_Index(face, glyph); - if (glyph_index == 0 && fallback_font) { - return fallback_font->vRender(glyph); + if (glyph_index == 0) { + if (fallback_font) { + return fallback_font->vRender(glyph); + } else { + return { {}, {0, current_style.size}, {0, 0}, false }; + } } return vRenderShaped(glyph_index); } Font::GlyphRet FTFont::vRenderShaped(char32_t glyph) const { - if (glyph == 0 && fallback_font) { - return fallback_font->vRender(glyph); + if (glyph == 0) { + if (fallback_font) { + return fallback_font->vRender(glyph); + } else { + return { {}, {0, current_style.size}, {0, 0}, false }; + } } auto render_glyph = [&](auto flags, auto mode) { if (FT_Load_Glyph(face, glyph, flags) != FT_Err_Ok) { - Output::Error("Couldn't load FreeType character {:#x}", uint32_t(glyph)); + Output::Debug("Couldn't load FreeType character {:#x}", uint32_t(glyph)); + return false; } if (FT_Render_Glyph(face->glyph, mode) != FT_Err_Ok) { - Output::Error("Couldn't render FreeType character {:#x}", uint32_t(glyph)); + Output::Debug("Couldn't render FreeType character {:#x}", uint32_t(glyph)); + return false; } + + return true; }; if (FT_HAS_COLOR(face)) { - render_glyph(FT_LOAD_COLOR, FT_RENDER_MODE_NORMAL); + if (!render_glyph(FT_LOAD_COLOR, FT_RENDER_MODE_NORMAL)) { + Output::Error("Broken color font for glyph {:#x}", uint32_t(glyph)); + } // When it is a color font check if the glyph is a color glyph // If it is not then rerender the glyph monochrome @@ -392,7 +429,13 @@ Font::GlyphRet FTFont::vRenderShaped(char32_t glyph) const { render_glyph(FT_LOAD_MONOCHROME | FT_LOAD_TARGET_MONO, FT_RENDER_MODE_MONO); } } else { - render_glyph(FT_LOAD_MONOCHROME | FT_LOAD_TARGET_MONO, FT_RENDER_MODE_MONO); + if (!render_glyph(FT_LOAD_MONOCHROME | FT_LOAD_TARGET_MONO, FT_RENDER_MODE_MONO)) { + if (fallback_font) { + return fallback_font->vRender(glyph); + } else { + return { {}, {0, current_style.size}, {0, 0}, false }; + } + } } FT_GlyphSlot slot = face->glyph; @@ -535,7 +578,7 @@ void FTFont::SetSize(int height, bool create) { hb_ft_font_set_funcs(hb_font); #endif - baseline_offset = FT_MulFix(face->ascender, face->size->metrics.y_scale) / 64; + baseline_offset = static_cast(FT_MulFix(face->ascender, face->size->metrics.y_scale) / 64); if (baseline_offset == 0) { // FIXME: Becomes 0 for FON files. How is the baseline calculated for them? baseline_offset = static_cast(height * (10.0 / 12.0)); @@ -598,7 +641,7 @@ FontRef Font::CreateFtFont(Filesystem_Stream::InputStream is, int size, bool bol if (it == ft_cache.end()) { auto ft_font = std::make_shared(std::move(is), size, bold, italic); - if (!ft_font) { + if (!ft_font || !ft_font->IsOk()) { return nullptr; } @@ -609,8 +652,6 @@ FontRef Font::CreateFtFont(Filesystem_Stream::InputStream is, int size, bool bol it->second.last_access = Game_Clock::GetFrameTime(); return it->second.font; } - - return std::make_shared(std::move(is), size, bold, italic); #else return nullptr; #endif @@ -619,10 +660,27 @@ FontRef Font::CreateFtFont(Filesystem_Stream::InputStream is, int size, bool bol void Font::ResetDefault() { SetDefault(nullptr, true); SetDefault(nullptr, false); + +#ifdef HAVE_FREETYPE + auto& cfg = Player::player_config; + if (!cfg.font1.Get().empty()) { + auto is = FileFinder::Root().OpenInputStream(cfg.font1.Get()); + SetDefault(CreateFtFont(std::move(is), cfg.font1_size.Get(), false, false), false); + } + + if (!cfg.font2.Get().empty()) { + auto is = FileFinder::Root().OpenInputStream(cfg.font2.Get()); + SetDefault(CreateFtFont(std::move(is), cfg.font2_size.Get(), false, false), true); + } + + cfg.font1.SetLocked(false); + cfg.font2.SetLocked(false); +#endif } void Font::Dispose() { - ResetDefault(); + SetDefault(nullptr, true); + SetDefault(nullptr, false); #ifdef HAVE_FREETYPE if (library) { @@ -642,6 +700,10 @@ Font::Font(StringView name, int size, bool bold, bool italic) current_style = original_style; } +StringView Font::GetName() const { + return name; +} + Rect Font::GetSize(char32_t glyph) const { if (EP_UNLIKELY(Utils::IsControlCharacter(glyph))) { if (glyph == '\n') { @@ -671,49 +733,10 @@ Point Font::Render(Bitmap& dest, int const x, int const y, const Bitmap& sys, in auto gret = vRender(glyph); - auto rect = Rect(x, y, gret.bitmap->width(), gret.bitmap->height()); - if (EP_UNLIKELY(rect.width == 0)) { + if (EP_UNLIKELY(!RenderImpl(dest, x, y, sys, color, gret))) { return {}; } - rect.x += gret.offset.x; - rect.y -= gret.offset.y; - - unsigned src_x; - unsigned src_y; - - if (color != ColorShadow) { - if (!gret.has_color && current_style.draw_shadow) { - auto shadow_rect = Rect(rect.x + 1, rect.y + 1, rect.width, rect.height); - dest.MaskedBlit(shadow_rect, *gret.bitmap, 0, 0, sys, 16, 32); - } - - src_x = color % 10 * 16 + 2; - src_y = color / 10 * 16 + 48 + 16 - 12 - gret.offset.y; - } else { - src_x = 16; - src_y = 32; - } - - if (!gret.has_color) { - if (current_style.draw_gradient) { - // When the glyph is large the system graphic color mask will be outside the rectangle - // Move the mask slightly up to avoid this - int offset = gret.bitmap->height() - gret.offset.y; - if (offset > 12) { - src_y -= offset - 12; - } - - dest.MaskedBlit(rect, *gret.bitmap, 0, 0, sys, src_x, src_y); - } else { - auto col = sys.GetColorAt(current_style.color_offset.x + src_x, current_style.color_offset.y + src_y); - auto col_bm = Bitmap::Create(gret.bitmap->width(), gret.bitmap->height(), col); - dest.MaskedBlit(rect, *gret.bitmap, 0, 0, *col_bm, 0, 0); - } - } else { - dest.Blit(rect.x, rect.y, *gret.bitmap, gret.bitmap->GetRect(), Opacity::Opaque()); - } - gret.advance.x += current_style.letter_spacing; return gret.advance; @@ -726,45 +749,117 @@ Point Font::Render(Bitmap& dest, int const x, int const y, const Bitmap& sys, in auto gret = vRenderShaped(shape.code); + if (EP_UNLIKELY(!RenderImpl(dest, x, y, sys, color, gret))) { + return {}; + } + + Point advance = { shape.advance.x + current_style.letter_spacing, shape.advance.y }; + return advance; +} + +bool Font::RenderImpl(Bitmap& dest, int const x, int const y, const Bitmap& sys, int color, const GlyphRet& gret) const { + if (EP_UNLIKELY(gret.bitmap == nullptr)) { + return false; + } + auto rect = Rect(x, y, gret.bitmap->width(), gret.bitmap->height()); if (EP_UNLIKELY(rect.width == 0)) { - return {}; + return false; } - rect.x += shape.offset.x + gret.offset.x; - rect.y -= shape.offset.y + gret.offset.y; + // Drawing position of the glyph + rect.x += gret.offset.x; + rect.y -= gret.offset.y; - unsigned src_x; - unsigned src_y; + unsigned src_x = 0; + unsigned src_y = 0; + + int glyph_height = gret.bitmap->height() - gret.offset.y; + + // Adjust how the mask is applied depending on the glyph size to prevent that + // pixels from outside of the mask color are read + // When <= 12: Will work fine + // When <= 16: Slightly adjusted (see ~20 lines below) + if (glyph_height > 16) { + // Too large for the existing mask: Resize the masks (slow) + // The mask is too small and the system graphic must be resized + // This is usually an exception and requires a custom font + const Rect shadow_color_rect = { 16, 32, 16, 16 }; + const Rect mask_color_rect = { color % 10 * 16, color / 10 * 16 + 48, 16, 16 }; + auto sys_large = Bitmap::Create(current_style.size * 2, current_style.size, false); + double zoom = current_style.size / 16.0; + // Left half of the image is the shadow, right half the mask + if (color != ColorShadow && current_style.draw_shadow) { + sys_large->ZoomOpacityBlit(0, 0, 0, 0, sys, shadow_color_rect, zoom, zoom, Opacity::Opaque()); + } + if (!gret.has_color) { + sys_large->ZoomOpacityBlit(current_style.size, 0, 0, 0, sys, mask_color_rect, zoom, zoom, Opacity::Opaque()); + } + + if (color != ColorShadow) { + // First draw the shadow, offset by one + if (!gret.has_color && current_style.draw_shadow) { + auto shadow_rect = Rect(rect.x + 1, rect.y + 1, rect.width, rect.height); + dest.MaskedBlit(shadow_rect, *gret.bitmap, 0, 0, *sys_large, 0, 0); + } + src_x = current_style.size; + src_y -= gret.offset.y; + } + + if (!gret.has_color) { + if (current_style.draw_gradient) { + dest.MaskedBlit(rect, *gret.bitmap, 0, 0, *sys_large, src_x, src_y); + } else { + auto col = sys.GetColorAt(current_style.color_offset.x + src_x, current_style.color_offset.y + src_y); + auto col_bm = Bitmap::Create(gret.bitmap->width(), gret.bitmap->height(), col); + dest.MaskedBlit(rect, *gret.bitmap, 0, 0, *col_bm, 0, 0); + } + } else { + // Color glyphs, emojis etc. + dest.Blit(rect.x, rect.y, *gret.bitmap, gret.bitmap->GetRect(), Opacity::Opaque()); + } + + return true; + } + + // Glyph fits in the mask if (color != ColorShadow) { + // First draw the shadow, offset by one if (!gret.has_color && current_style.draw_shadow) { auto shadow_rect = Rect(rect.x + 1, rect.y + 1, rect.width, rect.height); dest.MaskedBlit(shadow_rect, *gret.bitmap, 0, 0, sys, 16, 32); } src_x = color % 10 * 16 + 2; - src_y = color / 10 * 16 + 48 + 16 - 12 - shape.offset.y - gret.offset.y; - - // When the glyph is large the system graphic color mask will be outside the rectangle - // Move the mask slightly up to avoid this - int offset = gret.bitmap->height() - shape.offset.y - gret.offset.y; - if (offset > 12) { - src_y -= offset - 12; - } + src_y = color / 10 * 16 + 48 + 16 - 12 - gret.offset.y; } else { + // When the color is the shadow color do not render twice src_x = 16; src_y = 32; } if (!gret.has_color) { - dest.MaskedBlit(rect, *gret.bitmap, 0, 0, sys, src_x, src_y); + if (current_style.draw_gradient) { + // When the glyph is large the system graphic color mask will be outside the rectangle + // Move the mask slightly up to avoid this + if (glyph_height > 12) { + // Slightly too large -> Apply an offset + src_y -= glyph_height - 12; + } + + dest.MaskedBlit(rect, *gret.bitmap, 0, 0, sys, src_x, src_y); + } else { + auto col = sys.GetColorAt(current_style.color_offset.x + src_x, current_style.color_offset.y + src_y); + auto col_bm = Bitmap::Create(gret.bitmap->width(), gret.bitmap->height(), col); + dest.MaskedBlit(rect, *gret.bitmap, 0, 0, *col_bm, 0, 0); + } } else { + // Color glyphs, emojis etc. dest.Blit(rect.x, rect.y, *gret.bitmap, gret.bitmap->GetRect(), Opacity::Opaque()); } - Point advance = { shape.advance.x + current_style.letter_spacing, shape.advance.y }; - return advance; + return true; } Point Font::Render(Bitmap& dest, int x, int y, Color const& color, char32_t glyph) const { @@ -773,6 +868,9 @@ Point Font::Render(Bitmap& dest, int x, int y, Color const& color, char32_t glyp } auto gret = vRender(glyph); + if (EP_UNLIKELY(gret.bitmap == nullptr)) { + return {}; + } auto rect = Rect(x, y, gret.bitmap->width(), gret.bitmap->height()); dest.MaskedBlit(rect, *gret.bitmap, 0, 0, color); diff --git a/src/font.h b/src/font.h index 8bdb2d89e6..e3daaf1fad 100644 --- a/src/font.h +++ b/src/font.h @@ -98,6 +98,11 @@ class Font { virtual ~Font() = default; + /** + * @return Name of the font + */ + StringView GetName() const; + /** * Determines the size of a bitmap required to render a single character. * The dimensions of the Rect describe a bounding box to fit the text. @@ -257,6 +262,9 @@ class Font { Style original_style; Style current_style; FontRef fallback_font; + +private: + bool RenderImpl(Bitmap& dest, int const x, int const y, const Bitmap& sys, int color, const GlyphRet& gret) const; }; #endif diff --git a/src/game_config.cpp b/src/game_config.cpp index 48ba6a3bef..5e502ec38a 100644 --- a/src/game_config.cpp +++ b/src/game_config.cpp @@ -35,11 +35,18 @@ namespace { std::string config_path; + std::string soundfont_path; + std::string font_path; StringView config_name = "config.ini"; } void Game_ConfigPlayer::Hide() { - // Game specific settings unsupported +#ifndef HAVE_FREETYPE + font1.SetOptionVisible(false); + font1_size.SetOptionVisible(false); + font2.SetOptionVisible(false); + font2_size.SetOptionVisible(false); +#endif } void Game_ConfigVideo::Hide() { @@ -48,16 +55,15 @@ void Game_ConfigVideo::Hide() { // Always enabled by default: // - renderer (name of the renderer) - // - show_fps (Rendering of FPS, engine feature) vsync.SetOptionVisible(false); fullscreen.SetOptionVisible(false); fps_limit.SetOptionVisible(false); - fps_render_window.SetOptionVisible(false); window_zoom.SetOptionVisible(false); scaling_mode.SetOptionVisible(false); stretch.SetOptionVisible(false); touch_ui.SetOptionVisible(false); + pause_when_focus_lost.SetOptionVisible(false); game_resolution.SetOptionVisible(false); } @@ -78,7 +84,7 @@ Game_Config Game_Config::Create(CmdlineParser& cp) { Game_Config cfg; #if USE_SDL >= 2 - cfg.video.scaling_mode.Set(ScalingMode::Bilinear); + cfg.video.scaling_mode.Set(ConfigEnum::ScalingMode::Bilinear); #endif cp.Rewind(); @@ -201,6 +207,35 @@ Filesystem_Stream::InputStream Game_Config::GetGlobalConfigFileInput() { return Filesystem_Stream::InputStream(); } +FilesystemView Game_Config::GetSoundfontFilesystem() { + std::string path = soundfont_path; + if (path.empty()) { + path = FileFinder::MakePath(GetGlobalConfigFilesystem().GetFullPath(), "Soundfont"); + } + + if (!FileFinder::Root().MakeDirectory(path, true)) { + Output::Warning("Could not create soundfont path {}", path); + return {}; + } + + return FileFinder::Root().Create(path); +} + + +FilesystemView Game_Config::GetFontFilesystem() { + std::string path = font_path; + if (path.empty()) { + path = FileFinder::MakePath(GetGlobalConfigFilesystem().GetFullPath(), "Font"); + } + + if (!FileFinder::Root().MakeDirectory(path, true)) { + Output::Warning("Could not create fount path {}", path); + return {}; + } + + return FileFinder::Root().Create(path); +} + Filesystem_Stream::OutputStream Game_Config::GetGlobalConfigFileOutput() { auto fs = GetGlobalConfigFilesystem(); @@ -238,6 +273,9 @@ std::string Game_Config::GetConfigPath(CmdlineParser& cp) { } void Game_Config::LoadFromArgs(CmdlineParser& cp) { + font_path.clear(); + soundfont_path.clear(); + while (!cp.Done()) { CmdlineArg arg; long li_value = 0; @@ -257,12 +295,24 @@ void Game_Config::LoadFromArgs(CmdlineParser& cp) { video.fps_limit.Set(0); continue; } - if (cp.ParseNext(arg, 0, {"--show-fps", "--no-show-fps"})) { - video.show_fps.Set(arg.ArgIsOn()); + if (cp.ParseNext(arg, 0, "--show-fps")) { + video.fps.Set(ConfigEnum::ShowFps::ON); continue; } - if (cp.ParseNext(arg, 0, {"--fps-render-window", "--no-fps-render-window"})) { - video.fps_render_window.Set(arg.ArgIsOn()); + if (cp.ParseNext(arg, 0, "--no-show-fps")) { + video.fps.Set(ConfigEnum::ShowFps::OFF); + continue; + } + if (cp.ParseNext(arg, 0, "--fps-render-window")) { + video.fps.Set(ConfigEnum::ShowFps::Overlay); + continue; + } + if (cp.ParseNext(arg, 0, "--pause-focus-lost")) { + video.pause_when_focus_lost.Set(true); + continue; + } + if (cp.ParseNext(arg, 0, "--no-pause-focus-lost")) { + video.pause_when_focus_lost.Set(false); continue; } if (cp.ParseNext(arg, 0, "--window")) { @@ -321,6 +371,48 @@ void Game_Config::LoadFromArgs(CmdlineParser& cp) { } continue; } + if (cp.ParseNext(arg, 1, "--soundfont")) { + if (arg.NumValues() > 0) { + audio.soundfont.Set(arg.Value(0)); + } + continue; + } + if (cp.ParseNext(arg, 1, "--font1")) { + if (arg.NumValues() > 0) { + player.font1.Set(FileFinder::MakeCanonical(arg.Value(0), 0)); + } + continue; + } + if (cp.ParseNext(arg, 1, "--font1-size")) { + if (arg.ParseValue(0, li_value)) { + player.font1_size.Set(li_value); + } + continue; + } + if (cp.ParseNext(arg, 1, "--font2")) { + if (arg.NumValues() > 0) { + player.font2.Set(FileFinder::MakeCanonical(arg.Value(0), 0)); + } + continue; + } + if (cp.ParseNext(arg, 1, "--font2-size")) { + if (arg.ParseValue(0, li_value)) { + player.font2_size.Set(li_value); + } + continue; + } + if (cp.ParseNext(arg, 1, "--soundfont-path")) { + if (arg.NumValues() > 0) { + soundfont_path = FileFinder::MakeCanonical(arg.Value(0), 0); + } + continue; + } + if (cp.ParseNext(arg, 1, "--font-path")) { + if (arg.NumValues() > 0) { + font_path = FileFinder::MakeCanonical(arg.Value(0), 0); + } + continue; + } cp.SkipNext(); } @@ -337,13 +429,13 @@ void Game_Config::LoadFromStream(Filesystem_Stream::InputStream& is) { /** VIDEO SECTION */ video.vsync.FromIni(ini); video.fullscreen.FromIni(ini); - video.show_fps.FromIni(ini); - video.fps_render_window.FromIni(ini); + video.fps.FromIni(ini); video.fps_limit.FromIni(ini); video.window_zoom.FromIni(ini); video.scaling_mode.FromIni(ini); video.stretch.FromIni(ini); video.touch_ui.FromIni(ini); + video.pause_when_focus_lost.FromIni(ini); video.game_resolution.FromIni(ini); if (ini.HasValue("Video", "WindowX") && ini.HasValue("Video", "WindowY") && ini.HasValue("Video", "WindowWidth") && ini.HasValue("Video", "WindowHeight")) { @@ -356,6 +448,10 @@ void Game_Config::LoadFromStream(Filesystem_Stream::InputStream& is) { /** AUDIO SECTION */ audio.music_volume.FromIni(ini); audio.sound_volume.FromIni(ini); + audio.fluidsynth_midi.FromIni(ini); + audio.wildmidi_midi.FromIni(ini); + audio.native_midi.FromIni(ini); + audio.soundfont.FromIni(ini); /** INPUT SECTION */ input.buttons = Input::GetDefaultButtonMappings(); @@ -411,6 +507,10 @@ void Game_Config::LoadFromStream(Filesystem_Stream::InputStream& is) { player.settings_in_title.FromIni(ini); player.settings_in_menu.FromIni(ini); player.show_startup_logos.FromIni(ini); + player.font1.FromIni(ini); + player.font1_size.FromIni(ini); + player.font2.FromIni(ini); + player.font2_size.FromIni(ini); } void Game_Config::WriteToStream(Filesystem_Stream::OutputStream& os) const { @@ -419,13 +519,13 @@ void Game_Config::WriteToStream(Filesystem_Stream::OutputStream& os) const { os << "[Video]\n"; video.vsync.ToIni(os); video.fullscreen.ToIni(os); - video.show_fps.ToIni(os); - video.fps_render_window.ToIni(os); + video.fps.ToIni(os); video.fps_limit.ToIni(os); video.window_zoom.ToIni(os); video.scaling_mode.ToIni(os); video.stretch.ToIni(os); video.touch_ui.ToIni(os); + video.pause_when_focus_lost.ToIni(os); video.game_resolution.ToIni(os); // only preserve when toggling between window and fullscreen is supported @@ -442,6 +542,11 @@ void Game_Config::WriteToStream(Filesystem_Stream::OutputStream& os) const { audio.music_volume.ToIni(os); audio.sound_volume.ToIni(os); + audio.fluidsynth_midi.ToIni(os); + audio.wildmidi_midi.ToIni(os); + audio.native_midi.ToIni(os); + audio.soundfont.ToIni(os); + os << "\n"; /** INPUT SECTION */ @@ -487,6 +592,10 @@ void Game_Config::WriteToStream(Filesystem_Stream::OutputStream& os) const { player.settings_in_title.ToIni(os); player.settings_in_menu.ToIni(os); player.show_startup_logos.ToIni(os); + player.font1.ToIni(os); + player.font1_size.ToIni(os); + player.font2.ToIni(os); + player.font2_size.ToIni(os); os << "\n"; } diff --git a/src/game_config.h b/src/game_config.h index 95905c9813..770bed5311 100644 --- a/src/game_config.h +++ b/src/game_config.h @@ -33,29 +33,46 @@ class CmdlineParser; -enum class ScalingMode { - /** Nearest neighbour to fit screen */ - Nearest, - /** Like NN but only scales to integers */ - Integer, - /** Integer followed by Bilinear downscale to fit screen */ - Bilinear, +namespace ConfigEnum { + enum class ScalingMode { + /** Nearest neighbour to fit screen */ + Nearest, + /** Like NN but only scales to integers */ + Integer, + /** Integer followed by Bilinear downscale to fit screen */ + Bilinear, + }; + + enum class GameResolution { + /** 320x240 */ + Original, + /** 416x240 */ + Widescreen, + /** 560x240 */ + Ultrawide + }; + + enum class StartupLogos { + None, + Custom, + All + }; + + enum class ShowFps { + /** Do not show */ + OFF, + /** When windowed: Title bar, fullscreen: Overlay */ + ON, + /** Always overlay */ + Overlay + }; }; -enum class GameResolution { - /** 320x240 */ - Original, - /** 416x240 */ - Widescreen, - /** 560x240 */ - Ultrawide -}; - -enum class StartupLogos { - None, - Custom, - All -}; +#ifdef HAVE_FLUIDLITE +#define EP_FLUID_NAME "FluidLite" +#else +#define EP_FLUID_NAME "FluidSynth" +#endif struct Game_ConfigPlayer { StringConfigParam autobattle_algo{ "", "", "", "", "" }; @@ -63,11 +80,15 @@ struct Game_ConfigPlayer { BoolConfigParam settings_autosave{ "Save settings on exit", "Automatically save the settings on exit", "Player", "SettingsAutosave", false }; BoolConfigParam settings_in_title{ "Show settings on title screen", "Display settings menu item on the title screen", "Player", "SettingsInTitle", false }; BoolConfigParam settings_in_menu{ "Show settings in menu", "Display settings menu item on the menu screen", "Player", "SettingsInMenu", false }; - EnumConfigParam show_startup_logos{ - "Startup Logos", "Logos that are displayed on startup", "Player", "StartupLogos", StartupLogos::Custom, + EnumConfigParam show_startup_logos{ + "Startup Logos", "Logos that are displayed on startup", "Player", "StartupLogos", ConfigEnum::StartupLogos::Custom, Utils::MakeSvArray("None", "Custom", "All"), Utils::MakeSvArray("none", "custom", "all"), Utils::MakeSvArray("Do not show any additional logos", "Show custom logos bundled with the game", "Show all logos, including the original from RPG Maker")}; + PathConfigParam font1 { "Font 1", "The game chooses whether it wants font 1 or 2", "Player", "Font1", "" }; + RangeConfigParam font1_size { "Font 1 Size", "", "Player", "Font1Size", 12, 6, 16}; + PathConfigParam font2 { "Font 2", "The game chooses whether it wants font 1 or 2", "Player", "Font2", "" }; + RangeConfigParam font2_size { "Font 2 Size", "", "Player", "Font2Size", 12, 6, 16}; void Hide(); }; @@ -76,17 +97,21 @@ struct Game_ConfigVideo { LockedConfigParam renderer{ "Renderer", "The rendering engine", "auto" }; BoolConfigParam vsync{ "V-Sync", "Toggle V-Sync mode (Recommended: ON)", "Video", "Vsync", true }; BoolConfigParam fullscreen{ "Fullscreen", "Toggle between fullscreen and window mode", "Video", "Fullscreen", true }; - BoolConfigParam show_fps{ "Show FPS", "Toggle display of the FPS counter", "Video", "ShowFps", false }; - BoolConfigParam fps_render_window{ "Show FPS in Window", "Show FPS inside the window when in window mode", "Video", "FpsRenderWindow", false }; + EnumConfigParam fps{ + "FPS counter", "How to display the FPS counter", "Video", "Fps", ConfigEnum::ShowFps::OFF, + Utils::MakeSvArray("[OFF]", "[ON]", "Overlay"), + Utils::MakeSvArray("off", "on", "overlay"), + Utils::MakeSvArray("Do not show the FPS counter", "Show the FPS counter", "Always show the FPS counter inside the window")}; RangeConfigParam fps_limit{ "Frame Limiter", "Toggle the frames per second limit (Recommended: 60)", "Video", "FpsLimit", DEFAULT_FPS, 0, 99999 }; ConfigParam window_zoom{ "Window Zoom", "Toggle the window zoom level", "Video", "WindowZoom", 2 }; - EnumConfigParam scaling_mode{ "Scaling method", "How the output is scaled", "Video", "ScalingMode", ScalingMode::Nearest, + EnumConfigParam scaling_mode{ "Scaling method", "How the output is scaled", "Video", "ScalingMode", ConfigEnum::ScalingMode::Nearest, Utils::MakeSvArray("Nearest", "Integer", "Bilinear"), Utils::MakeSvArray("nearest", "integer", "bilinear"), Utils::MakeSvArray("Scale to screen size (Causes scaling artifacts)", "Scale to multiple of the game resolution", "Like Nearest, but output is blurred to avoid artifacts")}; BoolConfigParam stretch{ "Stretch", "Stretch to the width of the window/screen", "Video", "Stretch", false }; + BoolConfigParam pause_when_focus_lost{ "Pause when focus lost", "Pause the program when it is in the background", "Video", "PauseWhenFocusLost", true }; BoolConfigParam touch_ui{ "Touch Ui", "Display the touch ui", "Video", "TouchUi", true }; - EnumConfigParam game_resolution{ "Resolution", "Game resolution. Changes require a restart.", "Video", "GameResolution", GameResolution::Original, + EnumConfigParam game_resolution{ "Resolution", "Game resolution. Changes require a restart.", "Video", "GameResolution", ConfigEnum::GameResolution::Original, Utils::MakeSvArray("Original (Recommended)", "Widescreen (Experimental)", "Ultrawide (Experimental)"), Utils::MakeSvArray("original", "widescreen", "ultrawide"), Utils::MakeSvArray("The default resolution (320x240, 4:3)", "Can cause glitches (416x240, 16:9)", "Can cause glitches (560x240, 21:9)")}; @@ -103,6 +128,11 @@ struct Game_ConfigVideo { struct Game_ConfigAudio { RangeConfigParam music_volume{ "BGM Volume", "Volume of the background music", "Audio", "MusicVolume", 100, 0, 100 }; RangeConfigParam sound_volume{ "SFX Volume", "Volume of the sound effects", "Audio", "SoundVolume", 100, 0, 100 }; + BoolConfigParam fluidsynth_midi { EP_FLUID_NAME " (SF2)", "Play MIDI using SF2 soundfonts", "Audio", "Fluidsynth", true }; + BoolConfigParam wildmidi_midi { "WildMidi (GUS)", "Play MIDI using GUS patches", "Audio", "WildMidi", true }; + BoolConfigParam native_midi { "Native MIDI", "Play MIDI through the operating system ", "Audio", "NativeMidi", true }; + LockedConfigParam fmmidi_midi { "FmMidi", "Play MIDI using the built-in MIDI synthesizer", "[Always ON]" }; + PathConfigParam soundfont { "Soundfont", "Soundfont to use for " EP_FLUID_NAME, "Audio", "Soundfont", "" }; void Hide(); }; @@ -145,6 +175,19 @@ struct Game_Config { */ static FilesystemView GetGlobalConfigFilesystem(); + /** + * Returns the filesystem view to the soundfont directory + * By default this is config/Soundfont + */ + static FilesystemView GetSoundfontFilesystem(); + + /** + * Returns the filesystem view to the font directory + * By default this is config/Font + */ + static FilesystemView GetFontFilesystem(); + + /** * Returns a handle to the global config file for reading. * The file is created if it does not exist. diff --git a/src/game_windows.cpp b/src/game_windows.cpp index de678dc217..2a3850722a 100644 --- a/src/game_windows.cpp +++ b/src/game_windows.cpp @@ -355,6 +355,8 @@ void Game_Windows::Window_User::Refresh(bool& async_wait) { window->SetVisible(false); BitmapRef system; + // FIXME: Transparency setting is currently not applied to the system graphic + // Disabling transparency breaks the rendering of the system graphic if (!data.system_name.empty()) { system = Cache::System(data.system_name); } else { diff --git a/src/options.h b/src/options.h index 5f673d83aa..fc05038da0 100644 --- a/src/options.h +++ b/src/options.h @@ -52,18 +52,6 @@ /** Targeted screen bits per pixel. */ #define SCREEN_TARGET_BPP 32 -/** - * Pause the game process when the player window - * looses its focus. - */ -#define PAUSE_GAME_WHEN_FOCUS_LOST 1 - -/** - * Pause the audio process when the player window - * looses its focus. - */ -#define PAUSE_AUDIO_WHEN_FOCUS_LOST 1 - /** INI configuration filename. */ #define INI_NAME "RPG_RT.ini" #define EASYRPG_INI_NAME "EasyRPG.ini" diff --git a/src/platform/emscripten/interface.cpp b/src/platform/emscripten/interface.cpp index 10524d6d5f..a57e9c9ff7 100644 --- a/src/platform/emscripten/interface.cpp +++ b/src/platform/emscripten/interface.cpp @@ -22,8 +22,10 @@ #include #include +#include "system.h" #include "async_handler.h" #include "filefinder.h" +#include "filesystem_stream.h" #include "player.h" #include "scene_save.h" #include "output.h" @@ -53,6 +55,18 @@ void Emscripten_Interface::UploadSavegame(int slot) { }, slot); } +void Emscripten_Interface::UploadSoundfont() { + EM_ASM_INT({ + Module.api_private.uploadSoundfont_js($0); + }); +} + +void Emscripten_Interface::UploadFont() { + EM_ASM_INT({ + Module.api_private.uploadFont_js($0); + }); +} + void Emscripten_Interface::RefreshScene() { Scene::instance->Refresh(); } @@ -91,17 +105,83 @@ bool Emscripten_Interface_Private::UploadSavegameStep2(int slot, int buffer_addr return true; } +bool Emscripten_Interface_Private::UploadSoundfontStep2(std::string filename, int buffer_addr, int size) { + auto fs = Game_Config::GetSoundfontFilesystem(); + if (!fs) { + Output::Warning("Cannot access Soundfont directory"); + return false; + } + + std::string name = std::get<1>(FileFinder::GetPathAndFilename(filename)); + + // TODO: No good way to sanitize this, would require launching an entire, second fluidsynth session + if (!StringView(name).ends_with(".sf2")) { + Output::Warning("Selected file is not a valid soundfont"); + return false; + } + + { + auto os = fs.OpenOutputStream(name); + if (!os) + return false; + os.write(reinterpret_cast(buffer_addr), size); + } + + AsyncHandler::SaveFilesystem(); + + return true; +} + +bool Emscripten_Interface_Private::UploadFontStep2(std::string filename, int buffer_addr, int size) { + auto fs = Game_Config::GetFontFilesystem(); + if (!fs) { + Output::Warning("Cannot access Font directory"); + return false; + } + + std::string name = std::get<1>(FileFinder::GetPathAndFilename(filename)); + + Filesystem_Stream::InputStream is(new Filesystem_Stream::InputMemoryStreamBufView(lcf::Span(reinterpret_cast(buffer_addr), size)), filename); + if (!Font::CreateFtFont(std::move(is), 12, false, false)) { + Output::Warning("Selected file is not a valid font"); + return false; + } + + { + auto os = fs.OpenOutputStream(name); + if (!os) + return false; + os.write(reinterpret_cast(buffer_addr), size); + } + + AsyncHandler::SaveFilesystem(); + + return true; +} + // Binding code EMSCRIPTEN_BINDINGS(player_interface) { emscripten::class_("api") .class_function("requestReset", &Emscripten_Interface::Reset) .class_function("downloadSavegame", &Emscripten_Interface::DownloadSavegame) .class_function("uploadSavegame", &Emscripten_Interface::UploadSavegame) +#if defined(HAVE_FLUIDSYNTH) || defined(HAVE_FLUIDLITE) + .class_function("uploadSoundfont", &Emscripten_Interface::UploadSoundfont) +#endif +#if defined(HAVE_FREETYPE) + .class_function("uploadFont", &Emscripten_Interface::UploadFont) +#endif .class_function("refreshScene", &Emscripten_Interface::RefreshScene) .class_function("takeScreenshot", &Emscripten_Interface::TakeScreenshot) ; emscripten::class_("api_private") .class_function("uploadSavegameStep2", &Emscripten_Interface_Private::UploadSavegameStep2) +#if defined(HAVE_FLUIDSYNTH) || defined(HAVE_FLUIDLITE) + .class_function("uploadSoundfontStep2", &Emscripten_Interface_Private::UploadSoundfontStep2) +#endif +#if defined(HAVE_FREETYPE) + .class_function("uploadFontStep2", &Emscripten_Interface_Private::UploadFontStep2) +#endif ; } diff --git a/src/platform/emscripten/interface.h b/src/platform/emscripten/interface.h index b84c9b14f9..795587cc84 100644 --- a/src/platform/emscripten/interface.h +++ b/src/platform/emscripten/interface.h @@ -18,11 +18,15 @@ #ifndef EP_EMSCRIPTEN_INTERFACE_H #define EP_EMSCRIPTEN_INTERFACE_H +#include + class Emscripten_Interface { public: static bool DownloadSavegame(int slot); static void UploadSavegame(int slot); - static void RefreshScene(); + static void UploadSoundfont(); + static void UploadFont(); + static void RefreshScene(); static void TakeScreenshot(); static void Reset(); }; @@ -30,6 +34,8 @@ class Emscripten_Interface { class Emscripten_Interface_Private { public: static bool UploadSavegameStep2(int slot, int buffer_addr, int size); + static bool UploadFontStep2(std::string name, int buffer_addr, int size); + static bool UploadSoundfontStep2(std::string name, int buffer_addr, int size); }; #endif diff --git a/src/platform/libretro/midiout_device.cpp b/src/platform/libretro/midiout_device.cpp index 13960a3602..48a8006967 100644 --- a/src/platform/libretro/midiout_device.cpp +++ b/src/platform/libretro/midiout_device.cpp @@ -19,14 +19,14 @@ #include "ui.h" #include "output.h" -LibretroMidiOutDevice::LibretroMidiOutDevice() { +LibretroMidiOutDevice::LibretroMidiOutDevice(std::string& status_message) { if (!LibretroUi::environ_cb(RETRO_ENVIRONMENT_GET_MIDI_INTERFACE, &midi_out)) { - Output::Debug("libretro: GET_MIDI_INTERFACE unsupported"); + status_message = "libretro: GET_MIDI_INTERFACE unsupported"; return; } if (!midi_out.output_enabled()) { - Output::Debug("libretro: MIDI output not enabled"); + status_message = "libretro: MIDI output not enabled"; return; } diff --git a/src/platform/libretro/midiout_device.h b/src/platform/libretro/midiout_device.h index fa3c7fae4c..62a87b9f1f 100644 --- a/src/platform/libretro/midiout_device.h +++ b/src/platform/libretro/midiout_device.h @@ -27,7 +27,7 @@ */ class LibretroMidiOutDevice : public MidiDecoder { public: - LibretroMidiOutDevice(); + LibretroMidiOutDevice(std::string& status_message); void SendMidiMessage(uint32_t message) override; void SendSysExMessage(const uint8_t* data, size_t size) override; diff --git a/src/platform/linux/midiout_device_alsa.cpp b/src/platform/linux/midiout_device_alsa.cpp index e444a08257..d17bae9da1 100644 --- a/src/platform/linux/midiout_device_alsa.cpp +++ b/src/platform/linux/midiout_device_alsa.cpp @@ -19,10 +19,10 @@ #include "output.h" #include "system.h" -AlsaMidiOutDevice::AlsaMidiOutDevice() { +AlsaMidiOutDevice::AlsaMidiOutDevice(std::string& status_message) { int status = snd_seq_open(&midi_out, "default", SND_SEQ_OPEN_DUPLEX, 0); if (status < 0) { - Output::Debug("ALSA MIDI: snd_seq_open failed: {}", snd_strerror(status)); + status_message = fmt::format("ALSA MIDI: snd_seq_open failed: {}", snd_strerror(status)); return; } @@ -75,16 +75,17 @@ AlsaMidiOutDevice::AlsaMidiOutDevice() { done:; if (!candidate_found) { - Output::Debug("ALSA MIDI: No suitable client found"); + status_message = "ALSA MIDI: No suitable client found"; return; } - Output::Debug("ALSA MIDI: Using client {}:{}:{}", dst_client, dst_port_name, dst_port); + status_message = fmt::format("ALSA MIDI: Using client {}:{}:{}", dst_client, dst_port_name, dst_port); + Output::DebugStr(status_message); status = snd_seq_create_simple_port(midi_out, "Harmony", SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE, SND_SEQ_PORT_TYPE_APPLICATION); if (status < 0) { - Output::Debug("ALSA MIDI: snd_seq_create_simple_port failed: {}", snd_strerror(status)); + status_message = fmt::format("ALSA MIDI: snd_seq_create_simple_port failed: {}", snd_strerror(status)); return; } @@ -92,19 +93,19 @@ AlsaMidiOutDevice::AlsaMidiOutDevice() { status = snd_seq_connect_to(midi_out, 0, dst_client, dst_port); if (status < 0) { - Output::Debug("ALSA MIDI: snd_seq_connect_to failed: {}", snd_strerror(status)); + status_message = fmt::format("ALSA MIDI: snd_seq_connect_to failed: {}", snd_strerror(status)); return; } queue = snd_seq_alloc_named_queue(midi_out, GAME_TITLE); if (queue < 0) { - Output::Debug("ALSA MIDI: snd_seq_connect_to failed: {}", snd_strerror(queue)); + status_message = fmt::format("ALSA MIDI: snd_seq_connect_to failed: {}", snd_strerror(queue)); return; } status = snd_seq_start_queue(midi_out, queue, nullptr); if (status < 0) { - Output::Debug("ALSA MIDI: snd_seq_connect_to failed: {}", snd_strerror(status)); + status_message = fmt::format("ALSA MIDI: snd_seq_connect_to failed: {}", snd_strerror(status)); return; } diff --git a/src/platform/linux/midiout_device_alsa.h b/src/platform/linux/midiout_device_alsa.h index 7a6dffbc24..6a88d32c68 100644 --- a/src/platform/linux/midiout_device_alsa.h +++ b/src/platform/linux/midiout_device_alsa.h @@ -27,7 +27,7 @@ */ class AlsaMidiOutDevice : public MidiDecoder { public: - AlsaMidiOutDevice(); + AlsaMidiOutDevice(std::string& status_message); ~AlsaMidiOutDevice(); void SendMidiMessage(uint32_t message) override; diff --git a/src/platform/macos/midiout_device_coreaudio.cpp b/src/platform/macos/midiout_device_coreaudio.cpp index 98ee57b8d9..4bb86fc98f 100644 --- a/src/platform/macos/midiout_device_coreaudio.cpp +++ b/src/platform/macos/midiout_device_coreaudio.cpp @@ -19,10 +19,10 @@ #include "midiout_device_coreaudio.h" #include "output.h" -CoreAudioMidiOutDevice::CoreAudioMidiOutDevice() { +CoreAudioMidiOutDevice::CoreAudioMidiOutDevice(std::string& status_message) { OSStatus status = NewAUGraph(&graph); if (status != noErr) { - Output::Debug("macOS Midi: NewAUGraph failed: {}", status); + status_message = fmt::format("macOS Midi: NewAUGraph failed: {}", status); return; } AudioComponentDescription synthDesc = { @@ -70,7 +70,7 @@ CoreAudioMidiOutDevice::CoreAudioMidiOutDevice() { status = AUGraphStart(graph); if (status != noErr) { - Output::Debug("macOS Midi: AUGraphStart failed: {}", status); + status_message = fmt::format("macOS Midi: AUGraphStart failed: {}", status); return; } diff --git a/src/platform/macos/midiout_device_coreaudio.h b/src/platform/macos/midiout_device_coreaudio.h index dbcf40f5df..6880cfba0b 100644 --- a/src/platform/macos/midiout_device_coreaudio.h +++ b/src/platform/macos/midiout_device_coreaudio.h @@ -26,7 +26,7 @@ */ class CoreAudioMidiOutDevice : public MidiDecoder { public: - CoreAudioMidiOutDevice(); + CoreAudioMidiOutDevice(std::string& status_message); ~CoreAudioMidiOutDevice(); void SendMidiMessage(uint32_t message) override; diff --git a/src/platform/psvita/ui.cpp b/src/platform/psvita/ui.cpp index 7fad14ba72..397186bfa7 100644 --- a/src/platform/psvita/ui.cpp +++ b/src/platform/psvita/ui.cpp @@ -97,7 +97,7 @@ static int renderThread(unsigned int args, void* arg){ vita2d_draw_texture(touch_texture, 0, 0); } - if (vcfg_ref->scaling_mode.Get() == ScalingMode::Nearest) { + if (vcfg_ref->scaling_mode.Get() == ConfigEnum::ScalingMode::Nearest) { if (!vcfg_ref->stretch.Get()) { // 725x544 (scaled) vita2d_draw_texture_scale(gpu_texture, 117, 0, 2.266, 2.266); @@ -105,7 +105,7 @@ static int renderThread(unsigned int args, void* arg){ // 960x544 (full-stretched) vita2d_draw_texture_scale(gpu_texture, 0, 0, 3, 2.266); } - } else if (vcfg_ref->scaling_mode.Get() == ScalingMode::Integer) { + } else if (vcfg_ref->scaling_mode.Get() == ConfigEnum::ScalingMode::Integer) { if (!vcfg_ref->stretch.Get()) { // 640x480 (doubled) vita2d_draw_texture_scale(gpu_texture, 160, 32, 2.0, 2.0); @@ -271,7 +271,7 @@ void Psp2Ui::UpdateDisplay() { sceKernelSignalSema(GPU_Mutex, 1); } -void Psp2Ui::SetScalingMode(ScalingMode mode) { +void Psp2Ui::SetScalingMode(ConfigEnum::ScalingMode mode) { vcfg.scaling_mode.Set(mode); } @@ -295,7 +295,7 @@ void Psp2Ui::vGetConfig(Game_ConfigVideo& cfg) const { cfg.vsync.SetOptionVisible(true); cfg.fps_limit.SetOptionVisible(true); cfg.scaling_mode.SetOptionVisible(true); - cfg.scaling_mode.RemoveFromValidSet(ScalingMode::Bilinear); + cfg.scaling_mode.RemoveFromValidSet(ConfigEnum::ScalingMode::Bilinear); cfg.stretch.SetOptionVisible(true); cfg.touch_ui.SetOptionVisible(!is_pstv); diff --git a/src/platform/psvita/ui.h b/src/platform/psvita/ui.h index 9174a10656..5f5fc59f49 100644 --- a/src/platform/psvita/ui.h +++ b/src/platform/psvita/ui.h @@ -50,14 +50,14 @@ class Psp2Ui final : public BaseUi { /** @{ */ void UpdateDisplay() override; void ProcessEvents() override; - void SetScalingMode(ScalingMode) override; + void SetScalingMode(ConfigEnum::ScalingMode) override; void ToggleStretch() override; void ToggleTouchUi() override; void ToggleVsync() override; void vGetConfig(Game_ConfigVideo& cfg) const override; #ifdef SUPPORT_AUDIO - AudioInterface& GetAudio(); + AudioInterface& GetAudio() override; #endif /** @} */ diff --git a/src/platform/sdl/sdl2_ui.cpp b/src/platform/sdl/sdl2_ui.cpp index 6e5255166d..d3449a8154 100644 --- a/src/platform/sdl/sdl2_ui.cpp +++ b/src/platform/sdl/sdl2_ui.cpp @@ -542,7 +542,7 @@ void Sdl2Ui::ProcessEvents() { } } -void Sdl2Ui::SetScalingMode(ScalingMode mode) { +void Sdl2Ui::SetScalingMode(ConfigEnum::ScalingMode mode) { window.size_changed = true; vcfg.scaling_mode.Set(mode); } @@ -589,7 +589,7 @@ void Sdl2Ui::UpdateDisplay() { } }; - if (vcfg.scaling_mode.Get() == ScalingMode::Integer) { + if (vcfg.scaling_mode.Get() == ConfigEnum::ScalingMode::Integer) { // Integer division on purpose if (want_aspect > real_aspect) { window.scale = static_cast(window.width / main_surface->width()); @@ -634,7 +634,7 @@ void Sdl2Ui::UpdateDisplay() { SDL_RenderSetViewport(sdl_renderer, &viewport); } - if (vcfg.scaling_mode.Get() == ScalingMode::Bilinear && window.scale > 0.f) { + if (vcfg.scaling_mode.Get() == ConfigEnum::ScalingMode::Bilinear && window.scale > 0.f) { if (sdl_texture_scaled) { SDL_DestroyTexture(sdl_texture_scaled); } @@ -649,7 +649,7 @@ void Sdl2Ui::UpdateDisplay() { } SDL_RenderClear(sdl_renderer); - if (vcfg.scaling_mode.Get() == ScalingMode::Bilinear && window.scale > 0.f) { + if (vcfg.scaling_mode.Get() == ConfigEnum::ScalingMode::Bilinear && window.scale > 0.f) { // Render game texture on the scaled texture SDL_SetRenderTarget(sdl_renderer, sdl_texture_scaled); SDL_RenderClear(sdl_renderer); @@ -732,16 +732,19 @@ void Sdl2Ui::ProcessEvent(SDL_Event &evnt) { void Sdl2Ui::ProcessWindowEvent(SDL_Event &evnt) { int state = evnt.window.event; -#if PAUSE_GAME_WHEN_FOCUS_LOST + if (state == SDL_WINDOWEVENT_FOCUS_LOST) { + auto cfg = vcfg; + vGetConfig(cfg); + if (!cfg.pause_when_focus_lost.Get()) { + return; + } Player::Pause(); bool last = ShowCursor(true); -#ifndef EMSCRIPTEN // Filter SDL events until focus is regained - SDL_Event wait_event; while (SDL_WaitEvent(&wait_event)) { @@ -749,7 +752,6 @@ void Sdl2Ui::ProcessWindowEvent(SDL_Event &evnt) { break; } } -#endif ShowCursor(last); @@ -758,7 +760,7 @@ void Sdl2Ui::ProcessWindowEvent(SDL_Event &evnt) { return; } -#endif + #if defined(USE_MOUSE_OR_TOUCH) && defined(SUPPORT_MOUSE_OR_TOUCH) if (state == SDL_WINDOWEVENT_ENTER) { mouse_focus = true; @@ -1223,7 +1225,6 @@ void Sdl2Ui::vGetConfig(Game_ConfigVideo& cfg) const { #endif cfg.fullscreen.SetOptionVisible(true); cfg.fps_limit.SetOptionVisible(true); - cfg.fps_render_window.SetOptionVisible(true); #if defined(SUPPORT_ZOOM) && !defined(__ANDROID__) // An initial zoom level is needed on Android however changing it looks awful cfg.window_zoom.SetOptionVisible(true); @@ -1231,6 +1232,7 @@ void Sdl2Ui::vGetConfig(Game_ConfigVideo& cfg) const { cfg.scaling_mode.SetOptionVisible(true); cfg.stretch.SetOptionVisible(true); cfg.game_resolution.SetOptionVisible(true); + cfg.pause_when_focus_lost.SetOptionVisible(true); cfg.vsync.Set(current_display_mode.vsync); cfg.window_zoom.Set(current_display_mode.zoom); @@ -1240,15 +1242,17 @@ void Sdl2Ui::vGetConfig(Game_ConfigVideo& cfg) const { // Fullscreen is handled by the browser cfg.fullscreen.SetOptionVisible(false); cfg.fps_limit.SetOptionVisible(false); - cfg.fps_render_window.SetOptionVisible(false); cfg.window_zoom.SetOptionVisible(false); // Toggling this freezes the web player cfg.vsync.SetOptionVisible(false); + cfg.pause_when_focus_lost.Lock(false); + cfg.pause_when_focus_lost.SetOptionVisible(false); #elif defined(__WIIU__) // FIXME: Some options below may crash, better disable for now cfg.fullscreen.SetOptionVisible(false); cfg.window_zoom.SetOptionVisible(false); cfg.vsync.SetOptionVisible(false); + cfg.pause_when_focus_lost.SetOptionVisible(false); #endif } @@ -1262,3 +1266,21 @@ Rect Sdl2Ui::GetWindowMetrics() const { return window_mode_metrics; } } + +bool Sdl2Ui::OpenURL(StringView url) { +#if SDL_VERSION_ATLEAST(2, 0, 14) + if (IsFullscreen()) { + ToggleFullscreen(); + } + + if (SDL_OpenURL(ToString(url).c_str()) < 0) { + Output::Warning("Open URL {} failed: {}", url, SDL_GetError()); + return false; + } + + return true; +#else + Output::Warning("Cannot Open URL: SDL2 version too old (must be 2.0.14)"); + return false; +#endif +} diff --git a/src/platform/sdl/sdl2_ui.h b/src/platform/sdl/sdl2_ui.h index c9543f2aa9..89b9aaad75 100644 --- a/src/platform/sdl/sdl2_ui.h +++ b/src/platform/sdl/sdl2_ui.h @@ -66,10 +66,11 @@ class Sdl2Ui final : public BaseUi { void SetTitle(const std::string &title) override; bool ShowCursor(bool flag) override; void ProcessEvents() override; - void SetScalingMode(ScalingMode) override; + void SetScalingMode(ConfigEnum::ScalingMode) override; void ToggleStretch() override; void ToggleVsync() override; void vGetConfig(Game_ConfigVideo& cfg) const override; + bool OpenURL(StringView url) override; Rect GetWindowMetrics() const override; #ifdef SUPPORT_AUDIO diff --git a/src/platform/sdl/sdl_ui.cpp b/src/platform/sdl/sdl_ui.cpp index 63ed428c2a..04961a612f 100644 --- a/src/platform/sdl/sdl_ui.cpp +++ b/src/platform/sdl/sdl_ui.cpp @@ -478,8 +478,11 @@ void SdlUi::ProcessActiveEvent(SDL_Event &evnt) { int state; state = evnt.active.state; -#if PAUSE_GAME_WHEN_FOCUS_LOST if (state == SDL_APPINPUTFOCUS && !evnt.active.gain) { + if (!vcfg.pause_when_focus_lost.Get()) { + return; + } + Player::Pause(); bool last = ShowCursor(true); @@ -500,7 +503,6 @@ void SdlUi::ProcessActiveEvent(SDL_Event &evnt) { return; } -#endif } void SdlUi::ProcessKeyDownEvent(SDL_Event &evnt) { @@ -747,6 +749,8 @@ void SdlUi::vGetConfig(Game_ConfigVideo& cfg) const { #endif cfg.fullscreen.SetOptionVisible(toggle_fs_available); + cfg.pause_when_focus_lost.SetOptionVisible(toggle_fs_available); + #ifdef SUPPORT_ZOOM cfg.window_zoom.SetOptionVisible(true); cfg.window_zoom.Set(current_display_mode.zoom); diff --git a/src/platform/windows/midiout_device_win32.cpp b/src/platform/windows/midiout_device_win32.cpp index 9a231b16ea..ec005c239f 100644 --- a/src/platform/windows/midiout_device_win32.cpp +++ b/src/platform/windows/midiout_device_win32.cpp @@ -25,7 +25,7 @@ static std::string get_error_str(MMRESULT res) { return std::string(errMsg); } -Win32MidiOutDevice::Win32MidiOutDevice() { +Win32MidiOutDevice::Win32MidiOutDevice(std::string& status_message) { // TODO: Windows MIDI Mapper was removed in Windows 8. // This means it's impossible to change the default ("0") MIDI device // without third party software. We should allow specifying the MIDI device @@ -34,7 +34,7 @@ Win32MidiOutDevice::Win32MidiOutDevice() { MMRESULT err = midiOutOpen(&midi_out, device_id, 0, 0, CALLBACK_NULL); if (err != MMSYSERR_NOERROR) { - Output::Debug("Win32 midiOutOpen {} failed: ({}) {}", 0, err, get_error_str(err)); + status_message = fmt::format("Win32 midiOutOpen {} failed: ({}) {}", 0, err, get_error_str(err)); midi_out = nullptr; return; } diff --git a/src/platform/windows/midiout_device_win32.h b/src/platform/windows/midiout_device_win32.h index 2b96fd265e..d581c481a7 100644 --- a/src/platform/windows/midiout_device_win32.h +++ b/src/platform/windows/midiout_device_win32.h @@ -31,7 +31,7 @@ */ class Win32MidiOutDevice : public MidiDecoder { public: - Win32MidiOutDevice(); + Win32MidiOutDevice(std::string& status_message); ~Win32MidiOutDevice(); void SendMidiMessage(uint32_t message) override; diff --git a/src/player.cpp b/src/player.cpp index 46e20d9de9..a795f34e82 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -83,15 +83,12 @@ #include "baseui.h" #include "game_clock.h" #include "message_overlay.h" +#include "audio_midi.h" #ifdef __ANDROID__ #include "platform/android/android.h" #endif -#if defined(HAVE_FLUIDSYNTH) || defined(HAVE_FLUIDLITE) -#include "decoder_fluidsynth.h" -#endif - #ifndef EMSCRIPTEN // This is not used on Emscripten. #include "exe_reader.h" @@ -639,14 +636,6 @@ Game_Config Player::ParseCommandLine() { } continue; } -#if defined(HAVE_FLUIDSYNTH) || defined(HAVE_FLUIDLITE) - if (cp.ParseNext(arg, 1, "--soundfont")) { - if (arg.NumValues() > 0) { - FluidSynthDecoder::SetSoundfont(arg.Value(0)); - } - continue; - } -#endif if (cp.ParseNext(arg, 0, "--version", 'v')) { std::cout << GetFullVersionString() << std::endl; exit(0); @@ -676,6 +665,9 @@ void Player::CreateGameObjects() { CmdlineParser cp(arguments); game_config = Game_ConfigGame::Create(cp); + // Reinit MIDI + MidiDecoder::Reset(); + // Load the meta information file. // Note: This should eventually be split across multiple folders as described in Issue #1210 std::string meta_file = FileFinder::Game().FindFile(META_NAME); @@ -1062,12 +1054,20 @@ void Player::LoadFonts() { // Look for bundled fonts auto gothic = FileFinder::OpenFont("Font"); if (gothic) { - Font::SetDefault(Font::CreateFtFont(std::move(gothic), 12, false, false), false); + auto ft = Font::CreateFtFont(std::move(gothic), 12, false, false); + player_config.font1.SetLocked(ft != nullptr); + if (ft) { + Font::SetDefault(ft, false); + } } auto mincho = FileFinder::OpenFont("Font2"); if (mincho) { - Font::SetDefault(Font::CreateFtFont(std::move(mincho), 12, false, false), true); + auto ft = Font::CreateFtFont(std::move(mincho), 12, false, false); + player_config.font2.SetLocked(ft != nullptr); + if (ft) { + Font::SetDefault(ft, true); + } } #endif } @@ -1388,6 +1388,13 @@ Engine options: rpg2k3 - RPG Maker 2003 (v1.00 - v1.04) rpg2k3v105 - RPG Maker 2003 (v1.05 - v1.09a) rpg2k3e - RPG Maker 2003 (English release, v1.12) + --font1 FILE Font to use for the first font. The system graphic of the + game determines whether font 1 or 2 is used. + --font1-size PX Size of font 1 in pixel. The default is 12. + --font2 FILE Font to use for the second font. + --font2-size PX Size of font 2 in pixel. The default is 12. + --font-path PATH The path in which the settings scene looks for fonts. + The default is config-path/Font. --language LANG Load the game translation in language/LANG folder. --load-game-id N Skip the title scene and load SaveN.lsd (N is padded to two digits). @@ -1430,9 +1437,6 @@ Video options: --fps-limit In combination with --no-vsync sets a custom frames per second limit. The default is 60 FPS. Use --no-fps-limit to run with unlimited frames per second. - --fps-render-window Render the frames per second counter in both fullscreen - and windowed mode. - Disable with --no-fps-render-window. --fullscreen Start in fullscreen mode. --game-resolution R Force a different game resolution. This is experimental and can cause glitches or break games! @@ -1440,6 +1444,8 @@ Video options: original - 320x240 (4:3). Recommended widescreen - 416x240 (16:9) ultrawide - 560x240 (21:9) + --pause-focus-lost Pause the game when the window has no focus. + Disable with --no-pause-focus-lost. --scaling S How the video output is scaled. Options: nearest - Scale to screen size. Fast, but causes scaling @@ -1448,6 +1454,10 @@ Video options: bilinear - Like nearest, but applies a bilinear filter to avoid artifacts. --show-fps Enable display of the frames per second counter. + When in windowed mode it is shown inside the window. + When in fullscreen mode it is shown in the titlebar. + Use --fps-render-window to always show the counter inside + the window. Disable with --no-show-fps. --stretch Ignore the aspect ratio and stretch video output to the entire width of the screen. @@ -1461,6 +1471,8 @@ Audio options: --music-volume V Set volume of background music to V (0-100). --sound-volume V Set volume of sound effects to V (0-100). --soundfont FILE Soundfont in sf2 format to use when playing MIDI files. + --soundfont-path P The path in which the settings scene looks for soundfonts. + The default is config-path/Soundfont. Debug options: --battle-test N... Start a battle test. diff --git a/src/scene_logo.cpp b/src/scene_logo.cpp index e81f1ce4e8..e63cc454ca 100644 --- a/src/scene_logo.cpp +++ b/src/scene_logo.cpp @@ -63,6 +63,8 @@ void Scene_Logo::Start() { void Scene_Logo::vUpdate() { if (current_logo_index == 0 && frame_counter == 0) { + Font::ResetDefault(); + if (!DetectGame()) { // async delay for emscripten return; @@ -211,7 +213,7 @@ void Scene_Logo::DrawTextOnLogo(bool verbose) { } std::vector> Scene_Logo::LoadLogos() { - if (Player::player_config.show_startup_logos.Get() == StartupLogos::None) { + if (Player::player_config.show_startup_logos.Get() == ConfigEnum::StartupLogos::None) { return {}; } diff --git a/src/scene_settings.cpp b/src/scene_settings.cpp index bf564d0719..bd5563f992 100644 --- a/src/scene_settings.cpp +++ b/src/scene_settings.cpp @@ -87,10 +87,15 @@ void Scene_Settings::CreateMainWindow() { } void Scene_Settings::CreateOptionsWindow() { - help_window.reset(new Window_Help(Player::menu_offset_x, 0, MENU_WIDTH, 32)); + help_window = std::make_unique(Player::menu_offset_x, 0, MENU_WIDTH, 32); + help_window->SetAnimation(Window_Help::Animation::Loop); options_window = std::make_unique(Player::menu_offset_x + 32, 32, MENU_WIDTH - 64, Player::screen_height - 32 * 2); options_window->SetHelpWindow(help_window.get()); + help_window2 = std::make_unique(Player::menu_offset_x, options_window->GetBottomY(), MENU_WIDTH, 32); + help_window2->SetAnimation(Window_Help::Animation::Loop); + options_window->help_window2 = help_window2.get(); + input_window = std::make_unique(Player::menu_offset_x, 32, MENU_WIDTH, Player::screen_height - 32 * 3); input_window->SetHelpWindow(help_window.get()); @@ -140,9 +145,11 @@ void Scene_Settings::SetMode(Window_Settings::UiMode new_mode) { input_mode_window->SetVisible(false); input_help_window->SetVisible(false); help_window->SetVisible(false); + help_window2->SetVisible(false); about_window->SetVisible(false); picker_window.reset(); + font_size_window.reset(); switch (mode) { case Window_Settings::eNone: @@ -189,6 +196,10 @@ void Scene_Settings::SetMode(Window_Settings::UiMode new_mode) { } } +void Scene_Settings::Refresh() { + options_window->Refresh(); +} + void Scene_Settings::vUpdate() { if (RefreshInputEmergencyReset()) { return; @@ -196,6 +207,7 @@ void Scene_Settings::vUpdate() { main_window->Update(); help_window->Update(); + help_window2->Update(); options_window->Update(); input_window->Update(); input_mode_window->Update(); @@ -225,6 +237,7 @@ void Scene_Settings::vUpdate() { return; } + help_window2->SetFont(nullptr); options_window->Pop(); SetMode(options_window->GetMode()); if (mode == Window_Settings::eNone) { @@ -244,6 +257,8 @@ void Scene_Settings::vUpdate() { case Window_Settings::eInput: case Window_Settings::eVideo: case Window_Settings::eAudio: + case Window_Settings::eAudioMidi: + case Window_Settings::eAudioSoundfont: case Window_Settings::eLicense: case Window_Settings::eEngine: case Window_Settings::eInputButtonCategory: @@ -252,6 +267,12 @@ void Scene_Settings::vUpdate() { case Window_Settings::eInputListButtonsDeveloper: UpdateOptions(); break; + case Window_Settings::eEngineFont1: + UpdateFont(false); + break; + case Window_Settings::eEngineFont2: + UpdateFont(true); + break; case Window_Settings::eInputButtonOption: UpdateButtonOption(); break; @@ -292,7 +313,6 @@ void Scene_Settings::UpdateMain() { ); if (Input::IsTriggered(Input::DECISION)) { - Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Game_System::SFX_Decision)); auto idx = main_window->GetIndex(); if (main_window->IsItemEnabled(idx)) { @@ -436,6 +456,68 @@ void Scene_Settings::UpdateOptions() { } } +void Scene_Settings::UpdateFont(bool mincho) { + auto fs = Game_Config::GetFontFilesystem(); + + auto& last_index = options_window->GetFrame().scratch; + + if (font_size_window) { + font_size_window->SetY(options_window->GetY() + options_window->GetCursorRect().y); + font_size_window->Update(); + } + + int index = options_window->GetIndex(); + if (last_index == index) { + if (index == 0 || !help_window2->GetFont() || help_window2->GetFont()->GetCurrentStyle().size == options_window->font_size.Get()) { + // Same index or font size did not change + UpdateOptions(); + return; + } + } + last_index = index; + + if (!font_size_window) { + font_size_window = std::make_unique(options_window->GetRightX(), 0, 32, 32); + font_size_window->SetLeftArrow(true); + font_size_window->SetRightArrow(true); + font_size_window->SetAnimateArrows(true); + } + + font_size_window->SetVisible(false); + font_size_window->SetText(std::to_string(options_window->font_size.Get())); + + if (index == 0) { + // Built-In font + help_window2->Clear(); + help_window2->SetFont(Font::DefaultBitmapFont(mincho)); + help_window2->SetVisible(true); + } else if (index >= options_window->GetRowMax() - 2) { + // Sample or browse + help_window2->Clear(); + help_window2->SetFont(nullptr); + help_window2->SetVisible(true); + } else { + auto is = fs.OpenInputStream(options_window->GetCurrentOption().text); + if (is) { + auto font = Font::CreateFtFont(std::move(is), options_window->font_size.Get(), false, false); + if (font) { + help_window2->Clear(); + help_window2->SetFont(font); + help_window2->SetVisible(true); + font_size_window->SetVisible(true); + } else { + auto& opt = options_window->GetCurrentOption(); + opt.action = nullptr; + opt.value_text = "[Broken]"; + opt.help2.clear(); + options_window->DrawOption(options_window->GetIndex()); + } + } + } + + UpdateOptions(); +} + void Scene_Settings::UpdateButtonOption() { if (Input::IsTriggered(Input::DECISION)) { switch (input_mode_window->GetIndex()) { diff --git a/src/scene_settings.h b/src/scene_settings.h index cf21a8b91e..6919f6cc96 100644 --- a/src/scene_settings.h +++ b/src/scene_settings.h @@ -24,6 +24,7 @@ #include "window_command.h" #include "window_command_horizontal.h" #include "window_about.h" +#include "window_help.h" #include "window_selectable.h" #include "window_settings.h" #include "window_input_settings.h" @@ -43,6 +44,7 @@ class Scene_Settings : public Scene { Scene_Settings(); void Start() override; + void Refresh() override; void vUpdate() override; /** @@ -63,6 +65,7 @@ class Scene_Settings : public Scene { void UpdateMain(); void UpdateOptions(); + void UpdateFont(bool mincho); void UpdateButtonOption(); void UpdateButtonAdd(); void UpdateButtonRemove(); @@ -72,6 +75,7 @@ class Scene_Settings : public Scene { std::unique_ptr main_window; std::unique_ptr help_window; + std::unique_ptr help_window2; std::unique_ptr about_window; std::unique_ptr options_window; std::unique_ptr input_window; @@ -79,6 +83,7 @@ class Scene_Settings : public Scene { std::unique_ptr input_mode_window; std::unique_ptr picker_window; std::unique_ptr number_window; + std::unique_ptr font_size_window; std::unique_ptr title; FileRequestBinding request_id; diff --git a/src/scene_title.cpp b/src/scene_title.cpp index 3d4c38b07f..4dc5280a58 100644 --- a/src/scene_title.cpp +++ b/src/scene_title.cpp @@ -56,14 +56,14 @@ void Scene_Title::Start() { Player::ChangeResolution(Player::screen_width, Player::screen_height); } else { switch (DisplayUi->GetConfig().game_resolution.Get()) { - case GameResolution::Original: + case ConfigEnum::GameResolution::Original: Player::ChangeResolution(SCREEN_TARGET_WIDTH, SCREEN_TARGET_HEIGHT); break; - case GameResolution::Widescreen: + case ConfigEnum::GameResolution::Widescreen: Player::ChangeResolution(416, SCREEN_TARGET_HEIGHT); Player::game_config.fake_resolution.Set(true); break; - case GameResolution::Ultrawide: + case ConfigEnum::GameResolution::Ultrawide: Player::ChangeResolution(560, SCREEN_TARGET_HEIGHT); Player::game_config.fake_resolution.Set(true); break; @@ -92,7 +92,6 @@ void Scene_Title::CreateHelpWindow() { translate_window->SetHelpWindow(help_window.get()); } - void Scene_Title::Continue(SceneType prev_scene) { Main_Data::game_system->ResetSystemGraphic(); diff --git a/src/system.h b/src/system.h index 74b8d0b12c..c2f627d2c7 100644 --- a/src/system.h +++ b/src/system.h @@ -77,6 +77,7 @@ # define SUPPORT_TOUCH # define SUPPORT_JOYSTICK # define SUPPORT_JOYSTICK_AXIS +# define SUPPORT_FILE_BROWSER #elif defined(__SWITCH__) # define SUPPORT_JOYSTICK # define SUPPORT_JOYSTICK_AXIS @@ -93,6 +94,8 @@ # define SUPPORT_TOUCH # define SUPPORT_JOYSTICK # define SUPPORT_JOYSTICK_AXIS +# define SUPPORT_FILE_BROWSER +# define SYSTEM_DESKTOP_LINUX_BSD_MACOS #endif #ifdef USE_SDL diff --git a/src/text.cpp b/src/text.cpp index 3154b20e38..e8050f6de0 100644 --- a/src/text.cpp +++ b/src/text.cpp @@ -75,7 +75,6 @@ Point Text::Draw(Bitmap& dest, const int x, const int y, const Font& font, const dst_rect.y = y; dst_rect.width += 1; dst_rect.height += 1; // Need place for shadow - if (dst_rect.IsOutOfBounds(dest.GetWidth(), dest.GetHeight())) return { 0, 0 }; const int iy = dst_rect.y; const int ix = dst_rect.x; diff --git a/src/window.cpp b/src/window.cpp index 6da69f0373..6fc4dcba4b 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -25,7 +25,7 @@ #include "bitmap.h" #include "drawable_mgr.h" -constexpr int pause_animation_frames = 20; +constexpr int arrow_animation_frames = 20; Window::Window(Drawable::Flags flags): Drawable(Priority_Window, flags) { @@ -146,22 +146,30 @@ void Window::Draw(Bitmap& dst) { } } - if ((pause && pause_frame < pause_animation_frames && animation_frames <= 0) || down_arrow) { + auto show_arrow = [&](bool which) { + if (!which || !animate_arrows) { + return which; + } + + return (arrow_animation_frame < arrow_animation_frames) && animation_frames <= 0; + }; + + if ((pause && arrow_animation_frame < arrow_animation_frames && animation_frames <= 0) || show_arrow(down_arrow)) { Rect src_rect(40, 16, 16, 8); dst.Blit(x + width / 2 - 8, y + height - 8, *windowskin, src_rect, 255); } - if (up_arrow) { + if (show_arrow(up_arrow)) { Rect src_rect(40, 8, 16, 8); dst.Blit(x + width / 2 - 8, y, *windowskin, src_rect, 255); } - if (right_arrow) { + if (show_arrow(right_arrow)) { Rect src_rect(40, 16, 16, 8); dst.RotateZoomOpacityBlit(x + width - 8, y + height / 2 - 8, 16, 0, *windowskin, src_rect, -M_PI / 2, 1.0, 1.0, 255); } - if (left_arrow) { + if (show_arrow(left_arrow)) { Rect src_rect(40, 8, 16, 8); dst.RotateZoomOpacityBlit(x, y + height / 2 - 8, 16, 0, *windowskin, src_rect, -M_PI / 2, 1.0, 1.0, 255); } @@ -170,7 +178,7 @@ void Window::Draw(Bitmap& dst) { void Window::RefreshBackground() { background_needs_refresh = false; - BitmapRef bitmap = Bitmap::Create(width, height, false); + BitmapRef bitmap = Bitmap::Create(width, height); if (stretch) { bitmap->StretchBlit(*windowskin, Rect(0, 0, 32, 32), 255); @@ -302,8 +310,8 @@ void Window::Update() { if (active) { cursor_frame += 1; if (cursor_frame > 20) cursor_frame = 0; - if (pause) { - pause_frame = (pause_frame + 1) % (pause_animation_frames * 2); + if (pause || animate_arrows) { + arrow_animation_frame = (arrow_animation_frame + 1) % (arrow_animation_frames * 2); } } diff --git a/src/window.h b/src/window.h index 0968b49317..3f9700f360 100644 --- a/src/window.h +++ b/src/window.h @@ -20,6 +20,7 @@ // Headers #include "system.h" +#include "bitmap.h" #include "drawable.h" #include "rect.h" @@ -53,6 +54,8 @@ class Window : public Drawable { void SetLeftArrow(bool nleft_arrow); bool GetRightArrow() const; void SetRightArrow(bool nright_arrow); + bool GetAnimateArrows() const; + void SetAnimateArrows(bool nanimate_arrows); int GetX() const; void SetX(int nx); int GetY() const; @@ -61,6 +64,8 @@ class Window : public Drawable { void SetWidth(int nwidth); int GetHeight() const; void SetHeight(int nheight); + int GetRightX() const; + int GetBottomY() const; int GetOx() const; void SetOx(int nox); int GetOy() const; @@ -80,6 +85,9 @@ class Window : public Drawable { void SetOpenAnimation(int frames); void SetCloseAnimation(int frames); + FontRef GetFont() const; + void SetFont(FontRef font); + bool IsOpening() const; bool IsClosing() const; bool IsOpeningOrClosing() const; @@ -88,6 +96,7 @@ class Window : public Drawable { virtual bool IsSystemGraphicUpdateAllowed() const; unsigned long ID; + FontRef font; BitmapRef windowskin, contents; bool stretch = true; Rect cursor_rect; @@ -97,6 +106,7 @@ class Window : public Drawable { bool down_arrow = false; bool left_arrow = false; bool right_arrow = false; + bool animate_arrows = false; int x = 0; int y = 0; int width = 0; @@ -125,7 +135,7 @@ class Window : public Drawable { bool pause = false; int cursor_frame = 0; - int pause_frame = 0; + int arrow_animation_frame = 0; int animation_frames = 0; double animation_count = 0.0; double animation_increment = 0.0; @@ -153,6 +163,7 @@ inline BitmapRef Window::GetContents() const { inline void Window::SetContents(BitmapRef const& ncontents) { contents = ncontents; + contents->SetFont(font); } inline bool Window::GetStretch() const { @@ -177,7 +188,7 @@ inline bool Window::GetPause() const { inline void Window::SetPause(bool npause) { pause = npause; - pause_frame = 0; + arrow_animation_frame = 0; } inline bool Window::GetUpArrow() const { @@ -212,6 +223,14 @@ inline void Window::SetRightArrow(bool nright_arrow) { right_arrow = nright_arrow; } +inline bool Window::GetAnimateArrows() const { + return animate_arrows; +} + +inline void Window::SetAnimateArrows(bool nanimate_arrows) { + animate_arrows = nanimate_arrows; +} + inline int Window::GetX() const { return x; } @@ -236,6 +255,14 @@ inline int Window::GetHeight() const { return height; } +inline int Window::GetRightX() const { + return x + width; +} + +inline int Window::GetBottomY() const { + return y + height; +} + inline int Window::GetOx() const { return ox; } @@ -304,4 +331,15 @@ inline bool Window::IsSystemGraphicUpdateAllowed() const { return !IsClosing(); } +inline FontRef Window::GetFont() const { + return font; +} + +inline void Window::SetFont(FontRef font) { + this->font = font; + if (contents) { + contents->SetFont(font); + } +} + #endif diff --git a/src/window_help.cpp b/src/window_help.cpp index 3641a74768..b080537c1b 100644 --- a/src/window_help.cpp +++ b/src/window_help.cpp @@ -39,6 +39,9 @@ void Window_Help::SetText(std::string text, int color, Text::Alignment align, bo this->text = std::move(text); this->align = align; this->color = color; + this->text_x_scroll = 0; + this->text_x_scroll_dir = false; + this->text_x_width = text_x_offset; } } @@ -61,7 +64,7 @@ void Window_Help::AddText(std::string text, int color, Text::Alignment align, bo // Special handling for proportional fonts: If the "normal" space is already small do not half it again if (nextpos != decltype(text)::npos) { - int space_width = Text::GetSize(*Font::Default(), " ").width; + int space_width = Text::GetSize(*(font ? font : Font::Default()), " ").width; if (halfwidthspace && space_width >= 6) { text_x_offset += space_width / 2; @@ -72,3 +75,51 @@ void Window_Help::AddText(std::string text, int color, Text::Alignment align, bo } } } + +void Window_Help::SetAnimation(Window_Help::Animation animation) { + text_x_scroll = 0; + this->animation = animation; +} + +void Window_Help::UpdateScroll() { + if (animation == Animation::None) { + return; + } + + if (text_x_width <= contents->GetWidth()) { + // no need to scroll + return; + } + + if (animation == Animation::BackAndForth) { + text_x_scroll += text_x_scroll_dir ? 1 : -1; + + if ((!text_x_scroll_dir && (text_x_width + text_x_scroll) == contents->GetWidth()) || + (text_x_scroll_dir && text_x_scroll == 0)) { + text_x_scroll_dir = !text_x_scroll_dir; + } + + contents->Clear(); + text_x_offset = text_x_scroll; + AddText(text, color, align, true); + } else if (animation == Animation::Loop) { + --text_x_scroll; + + const int gap_size = 18; + + if (text_x_scroll == -text_x_width - gap_size) { + text_x_scroll = 0; + } + + contents->Clear(); + text_x_offset = text_x_scroll; + AddText(text, color, align, true); + text_x_offset += gap_size; + AddText(text, color, align, true); + } +} + +void Window_Help::Update() { + Window_Base::Update(); + UpdateScroll(); +} diff --git a/src/window_help.h b/src/window_help.h index 5965e3a387..4e290c439a 100644 --- a/src/window_help.h +++ b/src/window_help.h @@ -30,6 +30,15 @@ class Window_Help : public Window_Base { public: + enum class Animation { + /** Never scroll */ + None, + /** Scroll left and when the end of the text is reached scroll right, then repeat */ + BackAndForth, + /** Scroll left and repeat the text in an endless loop (also known as marquee) */ + Loop + }; + /** * Constructor. */ @@ -44,6 +53,18 @@ class Window_Help : public Window_Base { */ void SetText(std::string text, int color = Font::ColorDefault, Text::Alignment align = Text::AlignLeft, bool halfwidthspace = true); + /** + * Sets the scrolling animation when the text is larger than the help window. + * + * @param animation + */ + void SetAnimation(Animation animation); + + /** + * Updates the scrolling animation if set. + */ + void Update() override; + /** * Clears the window */ @@ -60,6 +81,8 @@ class Window_Help : public Window_Base { void AddText(std::string text, int color = Font::ColorDefault, Text::Alignment align = Text::AlignLeft, bool halfwidthspace = true); private: + void UpdateScroll(); + /** Text to draw. */ std::string text; /** Color of Text to draw. */ @@ -68,6 +91,13 @@ class Window_Help : public Window_Base { Text::Alignment align; /** Current text position */ int text_x_offset = 0; + /** Width of the text */ + int text_x_width = 0; + /** Current scroll position of the animation */ + int text_x_scroll = 0; + bool text_x_scroll_dir = false; + + Animation animation = Animation::None; }; #endif diff --git a/src/window_message.cpp b/src/window_message.cpp index 4c00c6e918..9c46aff0be 100644 --- a/src/window_message.cpp +++ b/src/window_message.cpp @@ -265,6 +265,12 @@ void Window_Message::InsertNewPage() { prev_char_printable = false; prev_char_waited = true; + if (GetFont()) { + page_font = GetFont(); + } else { + page_font = Font::Default(); + } + // Position the message box vertically // Game_Message::GetRealPosition() specify top/middle/bottom if (Game_Message::GetRealPosition() == 0) { @@ -318,7 +324,6 @@ void Window_Message::InsertNewPage() { ShowGoldWindow(); } } - } void Window_Message::InsertNewLine() { @@ -459,7 +464,6 @@ void Window_Message::UpdateMessage() { } auto system = Cache::SystemOrBlack(); - auto font = Font::Default(); while (true) { const auto* end = text.data() + text.size(); @@ -471,7 +475,7 @@ void Window_Message::UpdateMessage() { } if (!shape_ret.empty()) { - if (!DrawGlyph(*font, *system, shape_ret[0])) { + if (!DrawGlyph(*page_font, *system, shape_ret[0])) { continue; } @@ -498,7 +502,7 @@ void Window_Message::UpdateMessage() { const auto ch = tret.ch; if (tret.is_exfont) { - if (!DrawGlyph(*font, *system, ch, true)) { + if (!DrawGlyph(*page_font, *system, ch, true)) { text_index = text_prev; } continue; @@ -573,7 +577,7 @@ void Window_Message::UpdateMessage() { break; case '_': // Insert half size space - contents_x += Text::GetSize(*Font::Default(), " ").width / 2; + contents_x += Text::GetSize(*page_font, " ").width / 2; DebugLogText("{}: MSG HalfWait \\_"); SetWaitForCharacter(1); break; @@ -631,7 +635,7 @@ void Window_Message::UpdateMessage() { continue; } - if (font->CanShape()) { + if (page_font->CanShape()) { assert(shape_ret.empty()); auto text_index_shape = text_index; @@ -657,10 +661,10 @@ void Window_Message::UpdateMessage() { text32 += tret.ch; } - shape_ret = font->Shape(text32); + shape_ret = page_font->Shape(text32); continue; } else { - if (!DrawGlyph(*font, *system, ch, false)) { + if (!DrawGlyph(*page_font, *system, ch, false)) { text_index = text_prev; continue; } diff --git a/src/window_message.h b/src/window_message.h index 2bfb2b65a6..8d90546968 100644 --- a/src/window_message.h +++ b/src/window_message.h @@ -179,6 +179,8 @@ class Window_Message: public Window_Selectable { bool prev_char_waited = true; /** Was the previous character printable? */ bool prev_char_printable = false; + /** Active font for the current page */ + FontRef page_font; /** Used by the number input event. */ std::unique_ptr number_input_window; diff --git a/src/window_settings.cpp b/src/window_settings.cpp index cba597cedf..cbe8a30072 100644 --- a/src/window_settings.cpp +++ b/src/window_settings.cpp @@ -23,6 +23,7 @@ #include "text.h" #include "window_settings.h" #include "game_config.h" +#include "game_system.h" #include "input_buttons.h" #include "keys.h" #include "output.h" @@ -31,6 +32,12 @@ #include "player.h" #include "system.h" #include "audio.h" +#include "audio_midi.h" +#include "audio_generic_midiout.h" + +#ifdef EMSCRIPTEN +# include "platform/emscripten/interface.h" +#endif class MenuItem final : public ConfigParam { public: @@ -48,10 +55,10 @@ void Window_Settings::DrawOption(int index) { Rect rect = GetItemRect(index); contents->ClearRect(rect); - auto& option = options[index]; + auto& option = GetFrame().options[index]; bool enabled = bool(option.action); - Font::SystemColor color = enabled ? Font::ColorDefault : Font::ColorDisabled; + Font::SystemColor color = enabled ? option.color : Font::ColorDisabled; contents->TextDraw(rect, color, option.text); contents->TextDraw(rect, color, option.value_text, Text::AlignRight); @@ -112,7 +119,7 @@ Window_Settings::UiMode Window_Settings::GetMode() const { } void Window_Settings::Refresh() { - options.clear(); + GetFrame().options.clear(); switch (GetFrame().uimode) { case eNone: @@ -127,9 +134,21 @@ void Window_Settings::Refresh() { case eAudio: RefreshAudio(); break; + case eAudioMidi: + RefreshAudioMidi(); + break; + case eAudioSoundfont: + RefreshAudioSoundfont(); + break; case eEngine: RefreshEngine(); break; + case eEngineFont1: + RefreshEngineFont(false); + break; + case eEngineFont2: + RefreshEngineFont(true); + break; case eLicense: RefreshLicense(); break; @@ -145,9 +164,9 @@ void Window_Settings::Refresh() { break; } - SetItemMax(options.size()); + SetItemMax(GetFrame().options.size()); - if (GetFrame().uimode == eNone || options.empty()) { + if (GetFrame().uimode == eNone || GetFrame().options.empty()) { SetIndex(-1); } @@ -161,10 +180,17 @@ void Window_Settings::Refresh() { } void Window_Settings::UpdateHelp() { - if (index >= 0 && index < static_cast(options.size())) { - help_window->SetText(options[index].help); + if (index >= 0 && index < static_cast(GetFrame().options.size())) { + help_window->SetText(GetFrame().options[index].help); + if (help_window2) { + help_window2->SetText(GetFrame().options[index].help2); + help_window2->SetVisible(!GetFrame().options[index].help2.empty()); + } } else { help_window->SetText(""); + if (help_window2) { + help_window2->SetVisible(false); + } } } @@ -183,7 +209,7 @@ void Window_Settings::AddOption(const Param& param, if (!param.IsLocked()) { opt.action = std::forward(action); } - options.push_back(std::move(opt)); + GetFrame().options.push_back(std::move(opt)); } template @@ -205,7 +231,7 @@ void Window_Settings::AddOption(const RangeConfigParam& param, if (!param.IsLocked()) { opt.action = std::forward(action); } - options.push_back(std::move(opt)); + GetFrame().options.push_back(std::move(opt)); } template @@ -241,7 +267,7 @@ void Window_Settings::AddOption(const EnumConfigParam& param, if (!param.IsLocked()) { opt.action = std::forward(action); } - options.push_back(std::move(opt)); + GetFrame().options.push_back(std::move(opt)); } void Window_Settings::RefreshVideo() { @@ -250,14 +276,14 @@ void Window_Settings::RefreshVideo() { AddOption(cfg.renderer, [](){}); AddOption(cfg.fullscreen, [](){ DisplayUi->ToggleFullscreen(); }); AddOption(cfg.window_zoom, [](){ DisplayUi->ToggleZoom(); }); + AddOption(cfg.fps, [this](){ DisplayUi->SetShowFps(static_cast(GetCurrentOption().current_value)); }); AddOption(cfg.vsync, [](){ DisplayUi->ToggleVsync(); }); AddOption(cfg.fps_limit, [this](){ DisplayUi->SetFrameLimit(GetCurrentOption().current_value); }); - AddOption(cfg.show_fps, [](){ DisplayUi->ToggleShowFps(); }); - AddOption(cfg.fps_render_window, [](){ DisplayUi->ToggleShowFpsOnTitle(); }); AddOption(cfg.stretch, []() { DisplayUi->ToggleStretch(); }); - AddOption(cfg.scaling_mode, [this](){ DisplayUi->SetScalingMode(static_cast(GetCurrentOption().current_value)); }); + AddOption(cfg.scaling_mode, [this](){ DisplayUi->SetScalingMode(static_cast(GetCurrentOption().current_value)); }); + AddOption(cfg.pause_when_focus_lost, [cfg]() mutable { DisplayUi->SetPauseWhenFocusLost(cfg.pause_when_focus_lost.Toggle()); }); AddOption(cfg.touch_ui, [](){ DisplayUi->ToggleTouchUi(); }); - AddOption(cfg.game_resolution, [this]() { DisplayUi->SetGameResolution(static_cast(GetCurrentOption().current_value)); }); + AddOption(cfg.game_resolution, [this]() { DisplayUi->SetGameResolution(static_cast(GetCurrentOption().current_value)); }); } void Window_Settings::RefreshAudio() { @@ -265,28 +291,215 @@ void Window_Settings::RefreshAudio() { AddOption(cfg.music_volume, [this](){ Audio().BGM_SetGlobalVolume(GetCurrentOption().current_value); }); AddOption(cfg.sound_volume, [this](){ Audio().SE_SetGlobalVolume(GetCurrentOption().current_value); }); - /*AddOption("Midi Backend", LockedConfigParam("Unknown"), "", - [](){}, - "Which MIDI backend to use"); - AddOption("Midi Soundfont", LockedConfigParam("Default"), "", - [](){}, - "Which MIDI soundfont to use");*/ + if (cfg.fluidsynth_midi.IsOptionVisible() || cfg.wildmidi_midi.IsOptionVisible() || cfg.native_midi.IsOptionVisible() || cfg.fmmidi_midi.IsOptionVisible()) { + AddOption(MenuItem("MIDI drivers", "Configure MIDI playback", ""), [this]() { Push(eAudioMidi); }); + } + AddOption(cfg.soundfont, [this](){ Push(eAudioSoundfont); }); +} + +void Window_Settings::RefreshAudioMidi() { + auto cfg = Audio().GetConfig(); + + bool used = false; + + if (cfg.fluidsynth_midi.IsOptionVisible()) { + AddOption(cfg.fluidsynth_midi, []() { Audio().SetFluidsynthEnabled(Audio().GetConfig().fluidsynth_midi.Toggle()); }); + if (!MidiDecoder::CheckFluidsynth(GetFrame().options.back().help2)) { + GetFrame().options.back().text += " [Not working]"; + GetFrame().options.back().color = Font::ColorKnockout; + } else if (cfg.fluidsynth_midi.Get()) { + GetFrame().options.back().text += " [In use]"; + used = true; + } + } + + if (cfg.wildmidi_midi.IsOptionVisible()) { + AddOption(cfg.wildmidi_midi, []() { Audio().SetWildMidiEnabled(Audio().GetConfig().wildmidi_midi.Toggle()); }); + if (!MidiDecoder::CheckWildMidi(GetFrame().options.back().help2)) { + GetFrame().options.back().text += " [Not working]"; + GetFrame().options.back().color = Font::ColorKnockout; + } else if (cfg.wildmidi_midi.Get() && !used) { + GetFrame().options.back().text += " [In use]"; + used = true; + } + } + + if (cfg.native_midi.IsOptionVisible()) { + AddOption(cfg.native_midi, []() { Audio().SetNativeMidiEnabled(Audio().GetConfig().native_midi.Toggle()); }); + auto midi_out = Audio().CreateAndGetMidiOut(); + if (!midi_out || !midi_out->IsInitialized(GetFrame().options.back().help2)) { + GetFrame().options.back().text += " [Not working]"; + GetFrame().options.back().color = Font::ColorKnockout; + } else if (cfg.native_midi.Get() && !used) { + GetFrame().options.back().text += " [In use]"; + used = true; + } + } + + if (cfg.fmmidi_midi.IsOptionVisible()) { + AddOption(cfg.fmmidi_midi, []() {}); + if (!used) { + GetFrame().options.back().text += " [In use]"; + } + } + + AddOption(MenuItem("> Information <", "The first active and working option is used for MIDI", ""), [](){}); + GetFrame().options.back().help2 = "Changes take effect when a new MIDI file is played"; +} + +void Window_Settings::RefreshAudioSoundfont() { + auto fs = Game_Config::GetSoundfontFilesystem(); + + if (!fs) { + Pop(); + } + + fs.ClearCache(); + + auto acfg = Audio().GetConfig(); + AddOption(MenuItem("", "Attempt to find a suitable soundfont automatically", acfg.soundfont.Get().empty() ? "[x]" : ""), [this]() { + Audio().SetFluidsynthSoundfont({}); + Pop(); + }); + + auto list = fs.ListDirectory(); + assert(list); + + std::string sf_lower = Utils::LowerCase(Audio().GetFluidsynthSoundfont()); + for (const auto& item: *list) { + if (item.second.type == DirectoryTree::FileType::Regular && (StringView(item.first).ends_with(".sf2") || StringView(item.first).ends_with(".soundfont"))) { + AddOption(MenuItem(item.second.name, "Use this custom soundfont", StringView(sf_lower).ends_with(item.first) ? "[x]" : ""), [this, fs, item]() { + Audio().SetFluidsynthSoundfont(FileFinder::MakePath(fs.GetFullPath(), item.second.name)); + Pop(); + }); + } + } + + for (auto& opt: GetFrame().options) { + opt.help2 = "Changes take effect when a new MIDI file is played"; + } + +#ifdef EMSCRIPTEN + AddOption(MenuItem("", "Provide a soundfont from your system", ""), [fs]() { Emscripten_Interface::UploadSoundfont(); }); +#elif defined(SUPPORT_FILE_BROWSER) + AddOption(MenuItem("", "Open the soundfont directory in a file browser", ""), [fs]() { DisplayUi->OpenURL(fs.GetFullPath()); }); +#endif } void Window_Settings::RefreshEngine() { auto& cfg = Player::player_config; // FIXME: Binding &cfg is not needed and generates a warning but MSVC requires it -#ifdef _MSC_VER + AddOption(cfg.font1, [this, &cfg]() { + font_size.Set(cfg.font1_size.Get()); + Push(eEngineFont1); + GetFrame().scratch = -1; + }); + if (cfg.font1.IsLocked()) { + GetFrame().options.back().help = "This game uses a custom font"; + } + if (Main_Data::game_system->GetFontId() == lcf::rpg::System::Font_gothic) { + GetFrame().options.back().text += " [In use]"; + } + + AddOption(cfg.font2, [this, &cfg]() { + font_size.Set(cfg.font2_size.Get()); + Push(eEngineFont2); + GetFrame().scratch = -1; + }); + if (cfg.font2.IsLocked()) { + GetFrame().options.back().help = "This game uses a custom font"; + } + if (Main_Data::game_system->GetFontId() == lcf::rpg::System::Font_mincho) { + GetFrame().options.back().text += " [In use]"; + } + + AddOption(cfg.show_startup_logos, [this, &cfg](){ cfg.show_startup_logos.Set(static_cast(GetCurrentOption().current_value)); }); AddOption(cfg.settings_autosave, [&cfg](){ cfg.settings_autosave.Toggle(); }); AddOption(cfg.settings_in_title, [&cfg](){ cfg.settings_in_title.Toggle(); }); AddOption(cfg.settings_in_menu, [&cfg](){ cfg.settings_in_menu.Toggle(); }); - AddOption(cfg.show_startup_logos, [this, &cfg](){ cfg.show_startup_logos.Set(static_cast(GetCurrentOption().current_value)); }); -#else - AddOption(cfg.settings_autosave, [](){ cfg.settings_autosave.Toggle(); }); - AddOption(cfg.settings_in_title, [](){ cfg.settings_in_title.Toggle(); }); - AddOption(cfg.settings_in_menu, [](){ cfg.settings_in_menu.Toggle(); }); - AddOption(cfg.show_startup_logos, [this](){ cfg.show_startup_logos.Set(static_cast(GetCurrentOption().current_value)); }); +} + +void Window_Settings::RefreshEngineFont(bool mincho) { + auto fs = Game_Config::GetFontFilesystem(); + + if (!fs) { + Pop(); + } + + fs.ClearCache(); + + auto& cfg = Player::player_config; + + auto& setting = mincho ? cfg.font2 : cfg.font1; + + auto set_help2 = [this]() { + GetFrame().options.back().help2 = ToString(sample_text.GetDescriptions()[static_cast(sample_text.Get())]); + }; + + AddOption(MenuItem("", "Use the built-in pixel font", setting.Get().empty() ? "[x]" : ""), [this, &setting, mincho]() { + Font::SetDefault(nullptr, mincho); + setting.Set(""); + Pop(); + }); + set_help2(); + + std::string font_lower = Utils::LowerCase(Font::Default(mincho)->GetName()); + + auto list = fs.ListDirectory(); + assert(list); + for (const auto& item: *list) { + bool is_font = std::any_of(FileFinder::FONTS_TYPES.begin(), FileFinder::FONTS_TYPES.end(), [&item](const auto& ext) { + return StringView(item.first).ends_with(ext); + }); + + if (item.second.type == DirectoryTree::FileType::Regular && is_font) { + AddOption(MenuItem(item.second.name, "Use this font", StringView(font_lower).ends_with(item.first) ? "[x]" : ""), [=, &cfg, &setting]() mutable { + if (Input::IsTriggered(Input::LEFT) || Input::IsRepeated(Input::LEFT)) { + if (font_size.Get() == font_size.GetMin()) { + font_size.Set(font_size.GetMax()); + } else { + font_size.Set(font_size.Get() - 1); + } + return; + } else if (Input::IsTriggered(Input::RIGHT) || Input::IsRepeated(Input::RIGHT)) { + if (font_size.Get() == font_size.GetMax()) { + font_size.Set(font_size.GetMin()); + } else { + font_size.Set(font_size.Get() + 1); + } + return; + } + + auto is = fs.OpenInputStream(item.second.name); + if (is) { + auto font = Font::CreateFtFont(std::move(is), font_size.Get(), false, false); + if (font) { + setting.Set(FileFinder::MakePath(fs.GetFullPath(), item.second.name)); + auto& setting_size = mincho ? cfg.font2_size : cfg.font1_size; + setting_size.Set(font->GetCurrentStyle().size); + Font::SetDefault(font, mincho); + Pop(); + } + } + }); + set_help2(); + } + } + + /*AddOption(font_size, [this]() mutable { + font_size.Set(GetCurrentOption().current_value); + });*/ + + AddOption(sample_text, [this]() { + sample_text.Set(static_cast(GetCurrentOption().current_value)); + }); + set_help2(); + +#ifdef EMSCRIPTEN + AddOption(MenuItem("", "Provide a font from your system", ""), [fs]() { Emscripten_Interface::UploadFont(); }); +#elif defined(SUPPORT_FILE_BROWSER) + AddOption(MenuItem("", "Open the font directory in a file browser", ""), [fs]() { DisplayUi->OpenURL(fs.GetFullPath()); }); #endif } @@ -333,7 +546,7 @@ void Window_Settings::RefreshLicense() { #ifdef HAVE_OPUS AddOption(MenuItem("opus", "Decodes the free OPUS audio codec", "BSD"), [](){}); #endif -#ifdef HAVE_WILDMIDI +#ifdef HAVE_LIBWILDMIDI AddOption(MenuItem("WildMidi", "MIDI synthesizer", "LGPLv3+"), [](){}); #endif #ifdef HAVE_FLUIDSYNTH diff --git a/src/window_settings.h b/src/window_settings.h index 9effde03c0..26fb1988e1 100644 --- a/src/window_settings.h +++ b/src/window_settings.h @@ -42,8 +42,12 @@ class Window_Settings : public Window_Selectable { eInputButtonRemove, eVideo, eAudio, + eAudioMidi, + eAudioSoundfont, eLicense, eEngine, + eEngineFont1, + eEngineFont2, eSave, eEnd, eAbout, @@ -60,8 +64,10 @@ class Window_Settings : public Window_Selectable { std::string text; std::string value_text; std::string help; + std::string help2; std::function action; OptionMode mode; + Font::SystemColor color = Font::ColorDefault; int current_value; int original_value; int min_value; @@ -76,6 +82,7 @@ class Window_Settings : public Window_Selectable { int arg = -1; int scratch = 0; int scratch2 = 0; + std::vector