SpaceCadetPinball/SpaceCadetPinball/winmain.cpp

1196 lines
31 KiB
C++
Raw Normal View History

2020-11-05 16:44:34 +01:00
#include "pch.h"
#include "winmain.h"
2020-11-06 14:56:32 +01:00
#include "control.h"
2020-11-06 14:56:32 +01:00
#include "fullscrn.h"
2020-12-02 18:12:34 +01:00
#include "midi.h"
2020-11-05 16:44:34 +01:00
#include "options.h"
2020-11-06 14:56:32 +01:00
#include "pb.h"
#include "render.h"
#include "Sound.h"
#include "translations.h"
#include "font_selection.h"
2020-11-05 16:44:34 +01:00
constexpr const char* winmain::Version;
SDL_Window* winmain::MainWindow = nullptr;
SDL_Renderer* winmain::Renderer = nullptr;
ImGuiIO* winmain::ImIO = nullptr;
int winmain::return_value = 0;
bool winmain::bQuit = false;
bool winmain::activated = false;
int winmain::DispFrameRate = 0;
bool winmain::DispGRhistory = false;
bool winmain::single_step = false;
bool winmain::has_focus = true;
int winmain::last_mouse_x;
int winmain::last_mouse_y;
int winmain::mouse_down;
bool winmain::no_time_loss = false;
bool winmain::restart = false;
gdrv_bitmap8* winmain::gfr_display = nullptr;
bool winmain::ShowAboutDialog = false;
bool winmain::ShowImGuiDemo = false;
bool winmain::ShowSpriteViewer = false;
bool winmain::LaunchBallEnabled = true;
bool winmain::HighScoresEnabled = true;
bool winmain::DemoActive = false;
2021-10-30 09:12:30 +02:00
int winmain::MainMenuHeight = 0;
std::string winmain::FpsDetails, winmain::PrevSdlError;
unsigned winmain::PrevSdlErrorCount = 0;
double winmain::UpdateToFrameRatio;
winmain::DurationMs winmain::TargetFrameTime;
optionsStruct& winmain::Options = options::Options;
2021-11-18 15:58:53 +01:00
winmain::DurationMs winmain::SpinThreshold = DurationMs(0.005);
WelfordState winmain::SleepState{};
int winmain::WinMain(LPCSTR lpCmdLine)
2020-11-05 16:44:34 +01:00
{
std::set_new_handler(memalloc_failure);
2020-11-05 16:44:34 +01:00
printf("Game version: %s\n", Version);
printf("Command line: %s\n", lpCmdLine);
printf("Compiled with: SDL %d.%d.%d;", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_PATCHLEVEL);
printf(" SDL_mixer %d.%d.%d;", SDL_MIXER_MAJOR_VERSION, SDL_MIXER_MINOR_VERSION, SDL_MIXER_PATCHLEVEL);
2022-12-11 07:32:40 +01:00
printf(" ImGui %s %s\n", IMGUI_VERSION, ImGuiRender);
// SDL init
SDL_SetMainReady();
if (SDL_Init(SDL_INIT_TIMER | SDL_INIT_AUDIO | SDL_INIT_VIDEO |
SDL_INIT_EVENTS | SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) < 0)
2020-11-05 16:44:34 +01:00
{
pb::ShowMessageBox(SDL_MESSAGEBOX_ERROR, "Could not initialize SDL2", SDL_GetError());
return 1;
}
2020-11-05 16:44:34 +01:00
pb::quickFlag = strstr(lpCmdLine, "-quick") != nullptr;
2020-11-05 16:44:34 +01:00
// SDL window
SDL_Window* window = SDL_CreateWindow
(
pb::get_rc_string(Msg::STRING139),
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
800, 556,
SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE
);
MainWindow = window;
if (!window)
2020-11-06 14:56:32 +01:00
{
pb::ShowMessageBox(SDL_MESSAGEBOX_ERROR, "Could not create window", SDL_GetError());
return 1;
}
2021-10-30 09:12:30 +02:00
// If HW fails, fallback to SW SDL renderer.
SDL_Renderer* renderer = nullptr;
2021-11-18 15:58:53 +01:00
auto swOffset = strstr(lpCmdLine, "-sw") != nullptr ? 1 : 0;
for (int i = swOffset; i < 2 && !renderer; i++)
2021-10-30 09:12:30 +02:00
{
Renderer = renderer = SDL_CreateRenderer
(
window,
-1,
i == 0 ? SDL_RENDERER_ACCELERATED : SDL_RENDERER_SOFTWARE
);
}
if (!renderer)
{
pb::ShowMessageBox(SDL_MESSAGEBOX_ERROR, "Could not create renderer", SDL_GetError());
return 1;
}
2021-10-30 09:12:30 +02:00
SDL_RendererInfo rendererInfo{};
if (!SDL_GetRendererInfo(renderer, &rendererInfo))
printf("Using SDL renderer: %s\n", rendererInfo.name);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "nearest");
auto prefPath = SDL_GetPrefPath("", "SpaceCadetPinball");
auto basePath = SDL_GetBasePath();
// SDL mixer init
bool mixOpened = false, noAudio = strstr(lpCmdLine, "-noaudio") != nullptr;
if (!noAudio)
{
if ((Mix_Init(MIX_INIT_MID_Proxy) & MIX_INIT_MID_Proxy) == 0)
{
printf("Could not initialize SDL MIDI, music might not work.\nSDL Error: %s\n", SDL_GetError());
SDL_ClearError();
}
if (Mix_OpenAudio(MIX_DEFAULT_FREQUENCY, MIX_DEFAULT_FORMAT, 2, 1024) != 0)
{
printf("Could not open audio device, continuing without audio.\nSDL Error: %s\n", SDL_GetError());
SDL_ClearError();
}
else
mixOpened = true;
}
do
{
restart = false;
// ImGui init
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
ImIO = &io;
auto iniPath = std::string(prefPath) + "imgui_pb.ini";
io.IniFilename = iniPath.c_str();
// First option initialization step: just load settings from .ini. Needs ImGui context.
options::InitPrimary();
if (!Options.FontFileName.empty())
{
ImVector<ImWchar> ranges;
translations::GetGlyphRange(&ranges);
ImFontConfig fontConfig{};
// ToDo: further tweak font options, maybe try imgui_freetype
fontConfig.OversampleV = 2;
fontConfig.OversampleH = 4;
// ToDo: improve font file test, checking if file exists is not enough
auto fontLoaded = false;
auto fileName = Options.FontFileName.c_str();
auto fileHandle = fopenu(fileName, "rb");
if (fileHandle)
{
fclose(fileHandle);
// ToDo: Bind font size to UI scale
if (io.Fonts->AddFontFromFileTTF(fileName, 13.f, &fontConfig, ranges.Data))
fontLoaded = true;
}
if (!fontLoaded)
printf("Failed to load font: %s, using embedded font.\n", fileName);
io.Fonts->Build();
}
2022-12-11 07:32:40 +01:00
ImGui_Render_Init(renderer);
ImGui::StyleColorsDark();
ImGui_ImplSDL2_InitForSDLRenderer(window, Renderer);
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard | ImGuiConfigFlags_NavEnableGamepad;
// Data search order: WD, executable path, user pref path, platform specific paths.
std::vector<const char*> searchPaths
{
{
"",
basePath,
prefPath
}
};
searchPaths.insert(searchPaths.end(), std::begin(PlatformDataPaths), std::end(PlatformDataPaths));
pb::SelectDatFile(searchPaths);
// Second step: run updates that depend on .DAT file selection
options::InitSecondary();
Sound::Init(mixOpened, Options.SoundChannels, Options.Sounds, Options.SoundVolume);
if (!mixOpened)
Options.Sounds = false;
if (!midi::music_init(mixOpened, Options.MusicVolume))
Options.Music = false;
if (pb::init())
{
std::string message = "The .dat file is missing.\n"
"Make sure that the game data is present in any of the following locations:\n";
for (auto path : searchPaths)
{
if (path)
{
message = message + (path[0] ? path : "working directory") + "\n";
}
}
pb::ShowMessageBox(SDL_MESSAGEBOX_ERROR, "Could not load game data", message.c_str());
return 1;
}
2020-11-06 14:56:32 +01:00
fullscrn::init();
pb::reset_table();
pb::firsttime_setup();
2020-11-06 14:56:32 +01:00
if (strstr(lpCmdLine, "-fullscreen"))
{
Options.FullScreen = true;
}
if (!Options.FullScreen)
{
auto resInfo = &fullscrn::resolution_array[fullscrn::GetResolution()];
SDL_SetWindowSize(MainWindow, resInfo->TableWidth, resInfo->TableHeight);
}
SDL_ShowWindow(window);
fullscrn::set_screen_mode(Options.FullScreen);
if (strstr(lpCmdLine, "-demo"))
pb::toggle_demo();
else
pb::replay_level(false);
MainLoop();
options::uninit();
midi::music_shutdown();
Sound::Close();
pb::uninit();
2022-12-11 07:32:40 +01:00
ImGui_Render_Shutdown();
ImGui_ImplSDL2_Shutdown();
ImGui::DestroyContext();
2020-11-06 14:56:32 +01:00
}
while (restart);
2020-11-06 14:56:32 +01:00
if (!noAudio)
{
if (mixOpened)
Mix_CloseAudio();
Mix_Quit();
}
SDL_free(basePath);
SDL_free(prefPath);
delete gfr_display;
gfr_display = nullptr;
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
2020-11-07 16:41:14 +01:00
return return_value;
}
void winmain::MainLoop()
{
bQuit = false;
unsigned updateCounter = 0, frameCounter = 0;
auto frameStart = Clock::now();
double UpdateToFrameCounter = 0;
DurationMs sleepRemainder(0), frameDuration(TargetFrameTime);
auto prevTime = frameStart;
2020-11-06 14:56:32 +01:00
while (true)
{
if (DispFrameRate)
{
auto curTime = Clock::now();
if (curTime - prevTime > DurationMs(1000))
{
char buf[60];
auto elapsedSec = DurationMs(curTime - prevTime).count() * 0.001;
snprintf(buf, sizeof buf, "Updates/sec = %02.02f Frames/sec = %02.02f ",
updateCounter / elapsedSec, frameCounter / elapsedSec);
SDL_SetWindowTitle(MainWindow, buf);
FpsDetails = buf;
frameCounter = updateCounter = 0;
prevTime = curTime;
}
}
2020-11-06 14:56:32 +01:00
if (!ProcessWindowMessages() || bQuit)
break;
if (has_focus)
{
if (mouse_down)
{
2021-10-10 16:13:43 +02:00
int x, y, w, h;
SDL_GetMouseState(&x, &y);
SDL_GetWindowSize(MainWindow, &w, &h);
float dx = static_cast<float>(last_mouse_x - x) / static_cast<float>(w);
float dy = static_cast<float>(y - last_mouse_y) / static_cast<float>(h);
2021-10-10 16:13:43 +02:00
pb::ballset(dx, dy);
// Original creates continuous mouse movement with mouse capture.
// Alternative solution: mouse warp at window edges.
int xMod = 0, yMod = 0;
if (x == 0 || x >= w - 1)
xMod = w - 2;
if (y == 0 || y >= h - 1)
yMod = h - 2;
if (xMod != 0 || yMod != 0)
{
// Mouse warp does not work over remote desktop or in some VMs
x = abs(x - xMod);
y = abs(y - yMod);
SDL_WarpMouseInWindow(MainWindow, x, y);
}
2021-10-10 16:13:43 +02:00
last_mouse_x = x;
last_mouse_y = y;
}
if (!single_step && !no_time_loss)
{
auto dt = static_cast<float>(frameDuration.count());
pb::frame(dt);
if (DispGRhistory)
{
auto width = 300;
auto height = 64, halfHeight = height / 2;
if (!gfr_display)
{
gfr_display = new gdrv_bitmap8(width, height, false);
gfr_display->CreateTexture("nearest", SDL_TEXTUREACCESS_STREAMING);
}
gdrv::ScrollBitmapHorizontal(gfr_display, -1);
gdrv::fill_bitmap(gfr_display, 1, halfHeight, width - 1, 0, ColorRgba::Black()); // Background
// Target
gdrv::fill_bitmap(gfr_display, 1, halfHeight, width - 1, halfHeight, ColorRgba::White());
auto target = static_cast<float>(TargetFrameTime.count());
auto scale = halfHeight / target;
auto diffHeight = std::min(static_cast<int>(std::round(std::abs(target - dt) * scale)), halfHeight);
auto yOffset = dt < target ? halfHeight : halfHeight - diffHeight;
gdrv::fill_bitmap(gfr_display, 1, diffHeight, width - 1, yOffset, ColorRgba::Red()); // Target diff
}
updateCounter++;
}
no_time_loss = false;
if (UpdateToFrameCounter >= UpdateToFrameRatio)
{
ImGui_ImplSDL2_NewFrame();
2022-12-11 07:32:40 +01:00
ImGui_Render_NewFrame();
ImGui::NewFrame();
RenderUi();
SDL_RenderClear(Renderer);
2021-12-26 11:25:25 +01:00
// Alternative clear hack, clear might fail on some systems
// Todo: remove original clear, if save for all platforms
SDL_RenderFillRect(Renderer, nullptr);
render::PresentVScreen();
ImGui::Render();
2022-12-11 07:32:40 +01:00
ImGui_Render_RenderDrawData(ImGui::GetDrawData());
SDL_RenderPresent(Renderer);
frameCounter++;
UpdateToFrameCounter -= UpdateToFrameRatio;
}
auto sdlError = SDL_GetError();
if (sdlError[0] || !PrevSdlError.empty())
{
if (sdlError[0])
SDL_ClearError();
// Rate limit duplicate SDL error messages.
if (sdlError != PrevSdlError)
{
PrevSdlError = sdlError;
if (PrevSdlErrorCount > 0)
{
printf("SDL Error: ^ Previous Error Repeated %u Times\n", PrevSdlErrorCount + 1);
PrevSdlErrorCount = 0;
}
if (sdlError[0])
printf("SDL Error: %s\n", sdlError);
}
else
{
PrevSdlErrorCount++;
}
}
auto updateEnd = Clock::now();
auto targetTimeDelta = TargetFrameTime - DurationMs(updateEnd - frameStart) - sleepRemainder;
TimePoint frameEnd;
if (targetTimeDelta > DurationMs::zero() && !Options.UncappedUpdatesPerSecond)
{
2021-11-18 15:58:53 +01:00
if (Options.HybridSleep)
HybridSleep(targetTimeDelta);
else
std::this_thread::sleep_for(targetTimeDelta);
frameEnd = Clock::now();
}
else
{
frameEnd = updateEnd;
}
// Limit duration to 2 * target time
sleepRemainder = Clamp(DurationMs(frameEnd - updateEnd) - targetTimeDelta, -TargetFrameTime,
TargetFrameTime);
frameDuration = std::min<DurationMs>(DurationMs(frameEnd - frameStart), 2 * TargetFrameTime);
frameStart = frameEnd;
UpdateToFrameCounter++;
}
2020-11-06 14:56:32 +01:00
}
if (PrevSdlErrorCount > 0)
{
printf("SDL Error: ^ Previous Error Repeated %u Times\n", PrevSdlErrorCount);
}
2020-11-06 14:56:32 +01:00
}
void winmain::RenderUi()
2020-11-06 14:56:32 +01:00
{
// A minimal window with a button to prevent menu lockout.
if (!Options.ShowMenu)
{
ImGui::SetNextWindowPos(ImVec2{});
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2{10, 0});
if (ImGui::Begin("main", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoBackground |
ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoFocusOnAppearing))
{
ImGui::PushID(1);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4{});
if (ImGui::Button("Menu"))
{
options::toggle(Menu1::Show_Menu);
}
ImGui::PopStyleColor(1);
ImGui::PopID();
}
ImGui::End();
ImGui::PopStyleVar();
// This window can not loose nav focus for some reason, clear it manually.
if (ImGui::IsKeyDown(ImGuiKey_Escape) || ImGui::IsKeyDown(ImGuiKey_GamepadFaceRight))
ImGui::FocusWindow(nullptr);
}
// No demo window in release to save space
#ifndef NDEBUG
if (ShowImGuiDemo)
ImGui::ShowDemoWindow(&ShowImGuiDemo);
#endif
if (Options.ShowMenu && ImGui::BeginMainMenuBar())
2020-12-02 18:12:34 +01:00
{
int currentMenuHeight = static_cast<int>(ImGui::GetWindowSize().y);
if (MainMenuHeight != currentMenuHeight)
{
// Get the height of the main menu bar and update screen coordinates
MainMenuHeight = currentMenuHeight;
fullscrn::window_size_changed();
}
if (ImGui::BeginMenu(pb::get_rc_string(Msg::Menu1_Game)))
2020-12-02 18:12:34 +01:00
{
if (ImGui::MenuItem(pb::get_rc_string(Msg::Menu1_New_Game), "F2"))
2020-12-02 18:12:34 +01:00
{
new_game();
2020-12-02 18:12:34 +01:00
}
if (ImGui::MenuItem(pb::get_rc_string(Msg::Menu1_Launch_Ball), nullptr, false, LaunchBallEnabled))
{
end_pause();
pb::launch_ball();
}
if (ImGui::MenuItem(pb::get_rc_string(Msg::Menu1_Pause_Resume_Game), "F3"))
2020-12-02 18:12:34 +01:00
{
pause();
2020-12-02 18:12:34 +01:00
}
ImGui::Separator();
2020-12-02 18:12:34 +01:00
if (ImGui::MenuItem(pb::get_rc_string(Msg::Menu1_High_Scores), nullptr, false, HighScoresEnabled))
2020-12-02 18:12:34 +01:00
{
2022-01-05 09:38:50 +01:00
pause(false);
pb::high_scores();
2020-12-02 18:12:34 +01:00
}
if (ImGui::MenuItem(pb::get_rc_string(Msg::Menu1_Demo), nullptr, DemoActive))
2020-12-02 18:12:34 +01:00
{
end_pause();
pb::toggle_demo();
2020-12-02 18:12:34 +01:00
}
if (ImGui::MenuItem(pb::get_rc_string(Msg::Menu1_Exit)))
{
SDL_Event event{SDL_QUIT};
SDL_PushEvent(&event);
}
ImGui::EndMenu();
2020-12-02 18:12:34 +01:00
}
if (ImGui::BeginMenu(pb::get_rc_string(Msg::Menu1_Options)))
2020-12-02 18:12:34 +01:00
{
if (ImGui::MenuItem("Show Menu", "F9", Options.ShowMenu))
{
options::toggle(Menu1::Show_Menu);
}
if (ImGui::MenuItem(pb::get_rc_string(Msg::Menu1_Full_Screen), "F4", Options.FullScreen))
{
options::toggle(Menu1::Full_Screen);
}
if (ImGui::BeginMenu(pb::get_rc_string(Msg::Menu1_Select_Players)))
{
if (ImGui::MenuItem(pb::get_rc_string(Msg::Menu1_1Player), nullptr, Options.Players == 1))
{
options::toggle(Menu1::OnePlayer);
new_game();
}
if (ImGui::MenuItem(pb::get_rc_string(Msg::Menu1_2Players), nullptr, Options.Players == 2))
{
options::toggle(Menu1::TwoPlayers);
new_game();
}
if (ImGui::MenuItem(pb::get_rc_string(Msg::Menu1_3Players), nullptr, Options.Players == 3))
{
options::toggle(Menu1::ThreePlayers);
new_game();
}
if (ImGui::MenuItem(pb::get_rc_string(Msg::Menu1_4Players), nullptr, Options.Players == 4))
{
options::toggle(Menu1::FourPlayers);
new_game();
}
ImGui::EndMenu();
}
if (ImGui::MenuItem(pb::get_rc_string(Msg::Menu1_Player_Controls), "F8"))
2020-12-02 18:12:34 +01:00
{
2022-01-05 09:38:50 +01:00
pause(false);
options::ShowControlDialog();
}
if (ImGui::BeginMenu("Language"))
{
auto currentLanguage = translations::GetCurrentLanguage();
for (auto& item : translations::Languages)
{
if (ImGui::MenuItem(item.DisplayName, nullptr, currentLanguage->Language == item.Language))
{
translations::SetCurrentLanguage(item.ShortName);
Restart();
}
}
ImGui::EndMenu();
}
ImGui::Separator();
if (ImGui::BeginMenu("Audio"))
{
if (ImGui::MenuItem("Sound", "F5", Options.Sounds))
{
options::toggle(Menu1::Sounds);
}
Implement stereo sound. (#138) * Implement stereo sound. Original Space Cadet has mono sound. To achieve stereo, the following steps were accomplished: - Add a game option to turn on/off stereo sound. Default is on. - TPinballComponent objects were extended with a method called get_coordinates() that returns a single 2D point, approximating the on-screen position of the object, re-mapped between 0 and 1 vertically and horizontally, {0, 0} being at the top-left. - For static objects like bumpers and lights, the coordinate refers to the geometric center of the corresponding graphic sprite, and is precalculated at initialization. - For ball objects, the coordinate refers to the geometric center of the ball, calculated during play when requested. - Extend all calls to sound-playing methods so that they include a TPinballComponent* argument that refers to the sound source, e.g. where the sound comes from. For instance, when a flipper is activated, its method call to emit a sound now includes a reference to the flipper object; when a ball goes under a SkillShotGate, its method call to emit a sound now includes a reference to the corresponding light; and so on. For some cases, like light rollovers, the sound source is taken from the ball that triggered the light rollover. For other cases, like holes, flags and targets, the sound source is taken from the object itself. For some special cases like ramp activation, sound source is taken from the nearest light position that makes sense. For all game-progress sounds, like mission completion sounds or ball drain sounds, the sound source is undefined (set to nullptr), and the Sound::PlaySound() method takes care of positioning them at a default location, where speakers on a pinball machine normally are. - Make the Sound::PlaySound() method accept a new argument, a TPinballComponent reference, as described above. If the stereo option is turned on, the Sound::PlaySound() method calls the get_coordinates() method of the TPinballComponent reference to get the sound position. This project uses SDL_mixer and there is a function called Mix_SetPosition() that allows placing a sound in the stereo field, by giving it a distance and an angle. We arbitrarily place the player's ears at the bottom of the table; we set the ears' height to half a table's length. Intensity of the stereo effect is directly related to this value; the farther the player's ears from the table, the narrowest the stereo picture gets, and vice-versa. From there we have all we need to calculate distance and angle; we do just that and position all the sounds. * Copy-paste typo fix.
2022-05-30 09:35:29 +02:00
if (ImGui::MenuItem("Stereo Sound Effects", nullptr, Options.SoundStereo))
{
options::toggle(Menu1::SoundStereo);
}
ImGui::TextUnformatted("Sound Volume");
if (ImGui::SliderInt("##Sound Volume", &Options.SoundVolume, options::MinVolume, options::MaxVolume,
"%d",
ImGuiSliderFlags_AlwaysClamp))
2020-12-02 18:12:34 +01:00
{
Sound::SetVolume(Options.SoundVolume);
}
ImGui::TextUnformatted("Sound Channels");
if (ImGui::SliderInt("##Sound Channels", &Options.SoundChannels, options::MinSoundChannels,
options::MaxSoundChannels, "%d", ImGuiSliderFlags_AlwaysClamp))
{
Sound::SetChannels(Options.SoundChannels);
}
ImGui::Separator();
if (ImGui::MenuItem(pb::get_rc_string(Msg::Menu1_Music), "F6", Options.Music))
{
options::toggle(Menu1::Music);
}
ImGui::TextUnformatted("Music Volume");
if (ImGui::SliderInt("##Music Volume", &Options.MusicVolume, options::MinVolume, options::MaxVolume,
"%d",
ImGuiSliderFlags_AlwaysClamp))
{
midi::SetVolume(Options.MusicVolume);
2020-12-02 18:12:34 +01:00
}
ImGui::EndMenu();
2020-12-02 18:12:34 +01:00
}
if (ImGui::BeginMenu("Graphics"))
{
if (ImGui::MenuItem("Change Font"))
{
font_selection::ShowDialog();
}
if (ImGui::MenuItem(pb::get_rc_string(Msg::Menu1_WindowUniformScale), nullptr, Options.UniformScaling))
{
options::toggle(Menu1::WindowUniformScale);
}
if (ImGui::MenuItem("Linear Filtering", nullptr, Options.LinearFiltering))
{
options::toggle(Menu1::WindowLinearFilter);
}
if (ImGui::MenuItem("Integer Scaling", nullptr, Options.IntegerScaling))
{
options::toggle(Menu1::WindowIntegerScale);
}
ImGui::DragFloat("UI Scale", &ImIO->FontGlobalScale, 0.005f, 0.8f, 5,
"%.2f", ImGuiSliderFlags_AlwaysClamp);
ImGui::Separator();
2021-10-02 06:42:08 +02:00
char buffer[80]{};
auto changed = false;
if (ImGui::MenuItem("Set Default UPS/FPS"))
{
changed = true;
Options.UpdatesPerSecond = options::DefUps;
Options.FramesPerSecond = options::DefFps;
}
if (ImGui::SliderInt("UPS", &Options.UpdatesPerSecond, options::MinUps, options::MaxUps, "%d",
ImGuiSliderFlags_AlwaysClamp))
{
changed = true;
Options.FramesPerSecond = std::min(Options.UpdatesPerSecond, Options.FramesPerSecond);
}
if (ImGui::SliderInt("FPS", &Options.FramesPerSecond, options::MinFps, options::MaxFps, "%d",
ImGuiSliderFlags_AlwaysClamp))
{
changed = true;
Options.UpdatesPerSecond = std::max(Options.UpdatesPerSecond, Options.FramesPerSecond);
}
2021-10-02 06:42:08 +02:00
snprintf(buffer, sizeof buffer - 1, "Uncapped UPS (FPS ratio %02.02f)", UpdateToFrameRatio);
if (ImGui::MenuItem(buffer, nullptr, Options.UncappedUpdatesPerSecond))
2021-10-02 06:42:08 +02:00
{
Options.UncappedUpdatesPerSecond ^= true;
2021-10-02 06:42:08 +02:00
}
2021-11-18 15:58:53 +01:00
if (ImGui::MenuItem("Precise Sleep", nullptr, Options.HybridSleep))
{
Options.HybridSleep ^= true;
SleepState = WelfordState{};
SpinThreshold = DurationMs::zero();
}
2021-10-02 06:42:08 +02:00
if (changed)
{
UpdateFrameRate();
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu(pb::get_rc_string(Msg::Menu1_Table_Resolution)))
{
char buffer[20]{};
auto resolutionStringId = Msg::Menu1_UseMaxResolution_640x480;
switch (fullscrn::GetMaxResolution())
{
case 0: resolutionStringId = Msg::Menu1_UseMaxResolution_640x480;
break;
case 1: resolutionStringId = Msg::Menu1_UseMaxResolution_800x600;
break;
case 2: resolutionStringId = Msg::Menu1_UseMaxResolution_1024x768;
break;
}
auto maxResText = pb::get_rc_string(resolutionStringId);
if (ImGui::MenuItem(maxResText, nullptr, Options.Resolution == -1))
{
options::toggle(Menu1::MaximumResolution);
}
for (auto i = 0; i <= fullscrn::GetMaxResolution(); i++)
{
auto& res = fullscrn::resolution_array[i];
snprintf(buffer, sizeof buffer - 1, "%d x %d", res.ScreenWidth, res.ScreenHeight);
if (ImGui::MenuItem(buffer, nullptr, Options.Resolution == i))
{
options::toggle(static_cast<Menu1>(static_cast<int>(Menu1::R640x480) + i));
}
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Game Data"))
{
if (ImGui::MenuItem("Prefer 3DPB Data", nullptr, Options.Prefer3DPBGameData))
{
options::toggle(Menu1::Prefer3DPBGameData);
}
ImGui::EndMenu();
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu(pb::get_rc_string(Msg::Menu1_Help)))
{
#ifndef NDEBUG
if (ImGui::MenuItem("ImGui Demo", nullptr, ShowImGuiDemo))
2020-12-02 18:12:34 +01:00
{
ShowImGuiDemo ^= true;
2020-12-02 18:12:34 +01:00
}
#endif
if (ImGui::MenuItem("Sprite Viewer", nullptr, ShowSpriteViewer))
{
2022-01-05 09:38:50 +01:00
if (!ShowSpriteViewer)
pause(false);
ShowSpriteViewer ^= true;
}
if (pb::cheat_mode && ImGui::MenuItem("Frame Times", nullptr, DispGRhistory))
{
DispGRhistory ^= true;
}
if (ImGui::MenuItem("Debug Overlay", nullptr, Options.DebugOverlay))
{
Options.DebugOverlay ^= true;
}
if (Options.DebugOverlay && ImGui::BeginMenu("Overlay Options"))
{
if (ImGui::MenuItem("Box Grid", nullptr, Options.DebugOverlayGrid))
Options.DebugOverlayGrid ^= true;
if (ImGui::MenuItem("Ball Depth Grid", nullptr, Options.DebugOverlayBallDepthGrid))
Options.DebugOverlayBallDepthGrid ^= true;
if (ImGui::MenuItem("Sprite Positions", nullptr, Options.DebugOverlaySprites))
Options.DebugOverlaySprites ^= true;
if (ImGui::MenuItem("All Edges", nullptr, Options.DebugOverlayAllEdges))
Options.DebugOverlayAllEdges ^= true;
if (ImGui::MenuItem("Ball Position", nullptr, Options.DebugOverlayBallPosition))
Options.DebugOverlayBallPosition ^= true;
if (ImGui::MenuItem("Ball Box Edges", nullptr, Options.DebugOverlayBallEdges))
Options.DebugOverlayBallEdges ^= true;
if (ImGui::MenuItem("Sound Positions", nullptr, Options.DebugOverlaySounds))
Options.DebugOverlaySounds ^= true;
if (ImGui::MenuItem("Apply Collision Mask", nullptr, Options.DebugOverlayCollisionMask))
Options.DebugOverlayCollisionMask ^= true;
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Cheats"))
{
if (ImGui::MenuItem("hidden test", nullptr, pb::cheat_mode))
pb::PushCheat("hidden test");
if (ImGui::MenuItem("1max"))
pb::PushCheat("1max");
if (ImGui::MenuItem("bmax", nullptr, control::table_unlimited_balls))
pb::PushCheat("bmax");
if (ImGui::MenuItem("gmax"))
pb::PushCheat("gmax");
if (ImGui::MenuItem("rmax"))
pb::PushCheat("rmax");
if (pb::FullTiltMode && ImGui::MenuItem("quote"))
pb::PushCheat("quote");
if (ImGui::MenuItem("easy mode", nullptr, control::easyMode))
pb::PushCheat("easy mode");
ImGui::EndMenu();
}
ImGui::Separator();
2021-02-09 16:09:44 +01:00
if (ImGui::MenuItem(pb::get_rc_string(Msg::Menu1_About_Pinball)))
{
2022-01-05 09:38:50 +01:00
pause(false);
ShowAboutDialog = true;
}
ImGui::EndMenu();
}
if (DispFrameRate && !FpsDetails.empty())
if (ImGui::BeginMenu(FpsDetails.c_str()))
ImGui::EndMenu();
ImGui::EndMainMenuBar();
}
a_dialog();
high_score::RenderHighScoreDialog();
font_selection::RenderDialog();
if (ShowSpriteViewer)
render::SpriteViewer(&ShowSpriteViewer);
options::RenderControlDialog();
if (DispGRhistory)
RenderFrameTimeDialog();
// Print game texts on the sidebar
gdrv::grtext_draw_ttext_in_box();
}
int winmain::event_handler(const SDL_Event* event)
{
ImGui_ImplSDL2_ProcessEvent(event);
if (ImIO->WantCaptureMouse && !options::WaitingForInput())
{
if (mouse_down)
{
mouse_down = 0;
SDL_SetWindowGrab(MainWindow, SDL_FALSE);
}
switch (event->type)
{
case SDL_MOUSEMOTION:
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP:
case SDL_MOUSEWHEEL:
return 1;
default: ;
}
}
if (ImIO->WantCaptureKeyboard && !options::WaitingForInput())
{
switch (event->type)
{
case SDL_KEYDOWN:
case SDL_KEYUP:
case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP:
return 1;
default: ;
2020-12-02 18:12:34 +01:00
}
}
switch (event->type)
2020-11-06 14:56:32 +01:00
{
case SDL_QUIT:
end_pause();
bQuit = true;
fullscrn::shutdown();
return_value = 0;
return 0;
case SDL_KEYUP:
pb::InputUp({InputTypes::Keyboard, event->key.keysym.sym});
break;
case SDL_KEYDOWN:
if (!event->key.repeat)
pb::InputDown({InputTypes::Keyboard, event->key.keysym.sym});
switch (event->key.keysym.sym)
{
case SDLK_ESCAPE:
if (Options.FullScreen)
options::toggle(Menu1::Full_Screen);
SDL_MinimizeWindow(MainWindow);
break;
case SDLK_F2:
new_game();
break;
case SDLK_F3:
pause();
break;
case SDLK_F4:
options::toggle(Menu1::Full_Screen);
break;
case SDLK_F5:
options::toggle(Menu1::Sounds);
break;
case SDLK_F6:
options::toggle(Menu1::Music);
break;
case SDLK_F8:
2022-01-05 09:38:50 +01:00
pause(false);
options::ShowControlDialog();
break;
case SDLK_F9:
options::toggle(Menu1::Show_Menu);
break;
default:
break;
}
if (!pb::cheat_mode)
break;
switch (event->key.keysym.sym)
{
case SDLK_g:
DispGRhistory ^= true;
break;
case SDLK_o:
{
auto plt = new ColorRgba[4 * 256];
auto pltPtr = &plt[10]; // first 10 entries are system colors hardcoded in display_palette()
for (int i1 = 0, i2 = 0; i1 < 256 - 10; ++i1, i2 += 8)
{
unsigned char blue = i2, redGreen = i2;
if (i2 > 255)
{
blue = 255;
redGreen = i1;
}
*pltPtr++ = ColorRgba{blue, redGreen, redGreen, 0};
}
gdrv::display_palette(plt);
delete[] plt;
}
break;
case SDLK_y:
SDL_SetWindowTitle(MainWindow, "Pinball");
DispFrameRate = DispFrameRate == 0;
break;
case SDLK_F1:
pb::frame(10);
break;
case SDLK_F10:
single_step ^= true;
if (!single_step)
no_time_loss = true;
break;
default:
break;
}
break;
case SDL_MOUSEBUTTONDOWN:
2020-11-06 14:56:32 +01:00
{
bool noInput = false;
switch (event->button.button)
2020-11-06 14:56:32 +01:00
{
case SDL_BUTTON_LEFT:
if (pb::cheat_mode)
{
mouse_down = 1;
last_mouse_x = event->button.x;
last_mouse_y = event->button.y;
SDL_SetWindowGrab(MainWindow, SDL_TRUE);
noInput = true;
}
break;
default:
break;
2020-11-06 14:56:32 +01:00
}
if (!noInput)
pb::InputDown({InputTypes::Mouse, event->button.button});
2020-11-06 14:56:32 +01:00
}
break;
case SDL_MOUSEBUTTONUP:
{
bool noInput = false;
switch (event->button.button)
{
case SDL_BUTTON_LEFT:
if (mouse_down)
{
mouse_down = 0;
SDL_SetWindowGrab(MainWindow, SDL_FALSE);
noInput = true;
}
break;
default:
break;
}
if (!noInput)
pb::InputUp({InputTypes::Mouse, event->button.button});
}
break;
case SDL_WINDOWEVENT:
switch (event->window.event)
{
case SDL_WINDOWEVENT_FOCUS_GAINED:
case SDL_WINDOWEVENT_TAKE_FOCUS:
case SDL_WINDOWEVENT_SHOWN:
activated = true;
Sound::Activate();
if (Options.Music && !single_step)
midi::music_play();
no_time_loss = true;
has_focus = true;
break;
case SDL_WINDOWEVENT_FOCUS_LOST:
case SDL_WINDOWEVENT_HIDDEN:
activated = false;
fullscrn::activate(0);
Options.FullScreen = false;
Sound::Deactivate();
midi::music_stop();
has_focus = false;
pb::loose_focus();
break;
case SDL_WINDOWEVENT_SIZE_CHANGED:
case SDL_WINDOWEVENT_RESIZED:
fullscrn::window_size_changed();
break;
default: ;
}
break;
case SDL_JOYDEVICEADDED:
if (SDL_IsGameController(event->jdevice.which))
{
SDL_GameControllerOpen(event->jdevice.which);
}
break;
case SDL_JOYDEVICEREMOVED:
{
SDL_GameController* controller = SDL_GameControllerFromInstanceID(event->jdevice.which);
if (controller)
{
SDL_GameControllerClose(controller);
}
}
break;
case SDL_CONTROLLERBUTTONDOWN:
pb::InputDown({InputTypes::GameController, event->cbutton.button});
switch (event->cbutton.button)
{
case SDL_CONTROLLER_BUTTON_START:
pause();
break;
case SDL_CONTROLLER_BUTTON_BACK:
if (single_step)
{
SDL_Event event{SDL_QUIT};
SDL_PushEvent(&event);
}
break;
default: ;
}
break;
case SDL_CONTROLLERBUTTONUP:
pb::InputUp({InputTypes::GameController, event->cbutton.button});
break;
default: ;
2020-11-06 14:56:32 +01:00
}
return 1;
}
int winmain::ProcessWindowMessages()
{
static auto idleWait = 0;
SDL_Event event;
if (has_focus)
2020-11-06 14:56:32 +01:00
{
idleWait = static_cast<int>(TargetFrameTime.count());
while (SDL_PollEvent(&event))
{
if (!event_handler(&event))
return 0;
}
return 1;
2020-11-06 14:56:32 +01:00
}
// Progressively wait longer when transitioning to idle
idleWait = std::min(idleWait + static_cast<int>(TargetFrameTime.count()), 500);
if (SDL_WaitEventTimeout(&event, idleWait))
{
idleWait = static_cast<int>(TargetFrameTime.count());
return event_handler(&event);
}
return 1;
2020-11-05 16:44:34 +01:00
}
2020-11-07 16:41:14 +01:00
void winmain::memalloc_failure()
2020-11-05 16:44:34 +01:00
{
2020-12-02 18:12:34 +01:00
midi::music_stop();
Sound::Close();
const char* caption = pb::get_rc_string(Msg::STRING270);
const char* text = pb::get_rc_string(Msg::STRING279);
pb::ShowMessageBox(SDL_MESSAGEBOX_ERROR, caption, text);
std::exit(1);
2020-11-05 16:44:34 +01:00
}
2020-11-07 16:41:14 +01:00
void winmain::a_dialog()
2020-11-07 16:41:14 +01:00
{
if (ShowAboutDialog == true)
{
ShowAboutDialog = false;
ImGui::OpenPopup(pb::get_rc_string(Msg::STRING204));
}
2020-11-07 16:41:14 +01:00
bool unused_open = true;
if (ImGui::BeginPopupModal(pb::get_rc_string(Msg::STRING204), &unused_open, ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::TextUnformatted(pb::get_rc_string(Msg::STRING139));
ImGui::TextUnformatted("Original game by Cinematronics, Microsoft");
ImGui::Separator();
ImGui::TextUnformatted("Decompiled -> Ported to SDL");
ImGui::Text("Version %s", Version);
if (ImGui::SmallButton("Project home: https://github.com/k4zmu2a/SpaceCadetPinball"))
{
#if SDL_VERSION_ATLEAST(2, 0, 14)
// Relatively new feature, skip with older SDL
SDL_OpenURL("https://github.com/k4zmu2a/SpaceCadetPinball");
#endif
}
ImGui::Separator();
2020-11-07 16:41:14 +01:00
if (ImGui::Button("Ok"))
{
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
2020-11-07 16:41:14 +01:00
}
2020-12-02 18:12:34 +01:00
void winmain::end_pause()
{
if (single_step)
{
pb::pause_continue();
no_time_loss = true;
2020-12-02 18:12:34 +01:00
}
}
void winmain::new_game()
{
end_pause();
2022-01-05 09:38:50 +01:00
pb::replay_level(false);
2020-12-02 18:12:34 +01:00
}
2022-01-05 09:38:50 +01:00
void winmain::pause(bool toggle)
2020-12-02 18:12:34 +01:00
{
2022-01-05 09:38:50 +01:00
if (toggle || !single_step)
{
pb::pause_continue();
no_time_loss = true;
}
2020-12-02 18:12:34 +01:00
}
void winmain::Restart()
{
restart = true;
SDL_Event event{SDL_QUIT};
SDL_PushEvent(&event);
}
void winmain::UpdateFrameRate()
{
// UPS >= FPS
auto fps = Options.FramesPerSecond, ups = Options.UpdatesPerSecond;
UpdateToFrameRatio = static_cast<double>(ups) / fps;
TargetFrameTime = DurationMs(1000.0 / ups);
}
void winmain::RenderFrameTimeDialog()
{
if (!gfr_display)
return;
ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2{300, 70});
if (ImGui::Begin("Frame Times", &DispGRhistory, ImGuiWindowFlags_NoScrollbar))
{
auto target = static_cast<float>(TargetFrameTime.count());
auto scale = 1 / (gfr_display->Height / 2 / target);
2021-11-18 15:58:53 +01:00
auto spin = Options.HybridSleep ? static_cast<float>(SpinThreshold.count()) : 0;
ImGui::Text("Target frame time:%03.04fms, 1px:%03.04fms, SpinThreshold:%03.04fms",
target, scale, spin);
gfr_display->BlitToTexture();
auto region = ImGui::GetContentRegionAvail();
ImGui::Image(gfr_display->Texture, region);
}
ImGui::End();
ImGui::PopStyleVar();
}
2021-11-18 15:58:53 +01:00
void winmain::HybridSleep(DurationMs sleepTarget)
{
2021-11-18 15:58:53 +01:00
static constexpr double StdDevFactor = 0.5;
// This nice concept is from https://blat-blatnik.github.io/computerBear/making-accurate-sleep-function/
// Sacrifices some CPU time for smaller frame time jitter
while (sleepTarget > SpinThreshold)
{
auto start = Clock::now();
std::this_thread::sleep_for(DurationMs(1));
auto end = Clock::now();
auto actualDuration = DurationMs(end - start);
sleepTarget -= actualDuration;
// Update expected sleep duration using Welford's online algorithm
// With bad timer, this will run away to 100% spin
SleepState.Advance(actualDuration.count());
SpinThreshold = DurationMs(SleepState.mean + SleepState.GetStdDev() * StdDevFactor);
}
// spin lock
for (auto start = Clock::now(); DurationMs(Clock::now() - start) < sleepTarget;);
}