////===========================================================================// //// File: rp4lbe4.cc // //// Project: MUNGA Brick: Red Planet LBE Application // //// Contents: // ////---------------------------------------------------------------------------// //// Date Who Modification // //// -------- --- ---------------------------------------------------------- // //// // ////---------------------------------------------------------------------------// //// Copyright (C) 1994, Virtual World Entertainment, Inc. // //// All Rights reserved worldwide // //// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // ////===========================================================================// // #define WIN32_LEAN_AND_MEAN #define _WIN32_WINNT 0x0500 #define WINVER 0x0500 #include #include "rpl4.h" #pragma hdrstop #include "rpl4pb.h" #include "rpl4fe.h" #include "rpl4environ.h" #include "rpl4console.h" #include "rpl4lobby.h" #include "..\munga_l4\l4steamtransport.h" #include "..\munga_l4\l4splr.h" #include "..\munga_l4\l4mfdview.h" // RPWindowLayout_* #include "..\munga_l4\l4joy.h" // RPJoyConfigWizard #include "rpl4ver.h" #include "rpl4build.h" // generated: RP412_VERSION / RP412_VERSION_LONG #include "..\munga\resver.h" #include "..\munga\resource.h" // added for game status drawing support #include "..\munga\player.h" #include "..\munga\mission.h" #include "..\munga_l4\l4ctrl.h" // end add #include #include #include #include #include // the test-build expiry check #define SPOOL_SIZE 0x600000 //#define SCREEN_WIDTH 1024 //#define SCREEN_HEIGHT 768 Byte version[3] = {MAJOR_DATA_VERSION, RELEASE_VERSION, MINOR_DATA_VERSION}; char* names[] = {"InitializingState", "WaitingForEgg", "LoadingMission", "WaitingForLaunch", "LaunchingMission", "RunningMission", "EndingMission", "StoppingMission", "SuspendingMission", "ResumingMission", "AbortingMission", "CreatingMission", "ApplicationStateCount"}; //########################################################################## //################## RP4L4Application Globals ######################## //########################################################################## extern const char* const ProgName; const char* const ProgName = "rpl4"; Application *rpl4App; HWND hWnd; // // Unhandled-exception minidump: rpl4crash.dmp lands beside the exe so // a crash on a test machine hands back a stack. dbghelp loads lazily - // no link-time dependency. // static LONG WINAPI RPL4CrashDumpFilter(EXCEPTION_POINTERS *exception) { HMODULE dbghelp = LoadLibraryA("dbghelp.dll"); if (dbghelp != NULL) { typedef BOOL (WINAPI *MiniDumpWriteDumpFn)( HANDLE, DWORD, HANDLE, MINIDUMP_TYPE, PMINIDUMP_EXCEPTION_INFORMATION, PMINIDUMP_USER_STREAM_INFORMATION, PMINIDUMP_CALLBACK_INFORMATION); MiniDumpWriteDumpFn write_dump = (MiniDumpWriteDumpFn) GetProcAddress(dbghelp, "MiniDumpWriteDump"); if (write_dump != NULL) { HANDLE file = CreateFileA("rpl4crash.dmp", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (file != INVALID_HANDLE_VALUE) { MINIDUMP_EXCEPTION_INFORMATION dump_info; dump_info.ThreadId = GetCurrentThreadId(); dump_info.ExceptionPointers = exception; dump_info.ClientPointers = FALSE; write_dump(GetCurrentProcess(), GetCurrentProcessId(), file, MiniDumpNormal, &dump_info, NULL, NULL); CloseHandle(file); } } } return EXCEPTION_CONTINUE_SEARCH; } LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_CLOSE: DestroyWindow(hWnd); return 0; case WM_DESTROY: PostQuitMessage(0); return 0; case WM_SYSCOMMAND: // Alt is a flight control (reverse thrust) - releasing it must // not pop the window menu and steal keyboard focus if ((wParam & 0xFFF0) == SC_KEYMENU) { return 0; } break; case WM_EXITSIZEMOVE: // // A drag or resize just finished. Here rather than in the // cockpit's subclass so it also covers the console screen, which // is where the window gets moved before there is any cockpit to // subclass. No-op unless RP412MFDLAYOUT is save. // RPWindowLayout_Save(); return 0; } return DefWindowProc(hWnd, uMsg, wParam, lParam); } int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) { char filename[50]; strcpy_s(filename, 50, "rpl4.log"); std::ofstream logfile; logfile.open(filename); std::cout.rdbuf(logfile.rdbuf()); SetUnhandledExceptionFilter(RPL4CrashDumpFilter); // // Which build this is, before anything else can fail. The patch number // is this repository's commit count and the hash beside it names the // commit, so a log from a test machine says exactly where it came from. // A trailing '+' means the tree had uncommitted changes when it was // built. See stamp-version.ps1. // DEBUG_STREAM << "Red Planet " << RP412_VERSION_LONG << std::endl << std::flush; // load up our environment variables //controls if(getenv("L4CONTROLS") == NULL) putenv("L4CONTROLS=KEYBOARD"); if(getenv("DPLARG") == NULL) putenv("DPLARG=1"); if(getenv("L4DPLCFG") == NULL) putenv("L4DPLCFG=RPDPL.INI"); if(getenv("MULTISAMPLE") == NULL) putenv("MULTISAMPLE=0"); if(getenv("TARGETFPS") == NULL) putenv("TARGETFPS=60"); if(getenv("MAXPARTICLES") == NULL) putenv("MAXPARTICLES=8192"); // // environ.ini: written on first run and read here. The exe owns the // template rather than the packaging script laying one down on every // unzip, so a tester can drop a new build over an old folder and keep // their settings. See rpl4environ.h. // RPL4Environ_Load(); // //------------------------------------------------------------------------- // Test builds have a shelf life. // // A tester still racing a fortnight-old binary reports things that were // fixed a week ago, and the afternoon spent chasing them is gone. So the // build says plainly that it is out of date and stops, rather than // running on and being quietly wrong about what it is. // // This is a nudge, not a lock: the date comes from the machine's own // clock, so anyone determined can wind it back, and RP412NOEXPIRY=1 is // there for us when an old build has to be run on purpose. It is // deliberately not listed in environ.ini - a bypass every tester can see // is a bypass every tester will use, and then the build never goes stale // for the one person it was meant to stop. // // $expireDays in stamp-version.ps1 is what sets this, and 0 turns it off // for a real release. //------------------------------------------------------------------------- // #if RP412_EXPIRES { const char *no_expiry = getenv("RP412NOEXPIRY"); Logical overridden = (no_expiry != NULL && atoi(no_expiry) != 0); __time64_t raw_now = _time64(NULL); struct tm today; if (!overridden && _localtime64_s(&today, &raw_now) == 0) { int now_stamp = (today.tm_year + 1900) * 10000 + (today.tm_mon + 1) * 100 + today.tm_mday; int expiry_stamp = RP412_EXPIRY_YEAR * 10000 + RP412_EXPIRY_MONTH * 100 + RP412_EXPIRY_DAY; if (now_stamp > expiry_stamp) { DEBUG_STREAM << "Build expired on " << RP412_EXPIRY_TEXT << " - refusing to run\n" << std::flush; char notice[512]; sprintf(notice, "This Red Planet test build has expired.\n\n" " Build %s\n" " Expired %s\n\n" "Test builds are good for a fortnight so that nobody spends an " "afternoon chasing something that was fixed a week ago.\n\n" "Ask for the current one.", RP412_VERSION_LONG, RP412_EXPIRY_TEXT); MessageBoxA(NULL, notice, "Red Planet - test build expired", MB_OK | MB_ICONWARNING | MB_SETFOREGROUND); return 1; } // // The last few days get a line in the log, so somebody reading a // report can see the build was nearly out rather than wondering. // struct tm expiry_day; memset(&expiry_day, 0, sizeof(expiry_day)); expiry_day.tm_year = RP412_EXPIRY_YEAR - 1900; expiry_day.tm_mon = RP412_EXPIRY_MONTH - 1; expiry_day.tm_mday = RP412_EXPIRY_DAY; // // The END of the expiry day, because that is the rule the check // above enforces - the build is good for all of that date and // refuses the morning after. Anchoring at midday instead would // report one day fewer than the build actually has left. // expiry_day.tm_hour = 23; expiry_day.tm_min = 59; expiry_day.tm_sec = 59; expiry_day.tm_isdst = -1; __time64_t expiry_time = _mktime64(&expiry_day); if (expiry_time != (__time64_t) -1) { int days_left = (int)((expiry_time - raw_now) / (24 * 60 * 60)); if (days_left <= 3) { DEBUG_STREAM << "Build expires " << RP412_EXPIRY_TEXT << " (" << days_left << " day(s) left)\n" << std::flush; } } } else if (overridden) { DEBUG_STREAM << "Build expiry (" << RP412_EXPIRY_TEXT << ") waived by RP412NOEXPIRY\n" << std::flush; } } #endif DEBUG_STREAM << "L4CONTROLS=" << getenv("L4CONTROLS") << std::endl << std::flush; #ifdef RP412_STEAM // // RP412STEAM=1 (environ.ini or a Steam launch) swaps the wire to // ISteamNetworkingSockets. Failure logs the reason and the game // carries on over TCP - dev boxes without Steam are unaffected. // { const char *steam_switch = getenv("RP412STEAM"); if (steam_switch != NULL && atoi(steam_switch) != 0) { SteamNetTransport_Install(); } } #endif // GetProcessAffinityMask(GetCurrentProcess(), DWORD_PTR res = SetThreadAffinityMask(GetCurrentThread(), (DWORD_PTR)1); if (res == 0) { DWORD err = GetLastError(); DEBUG_STREAM << "Could not set thread affinity. Err Code = " << err << std::endl << std::flush; } // //---------------------------------------------------------------------- // Parse command line for an egg notation file name //---------------------------------------------------------------------- // int argc; LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc); Logical run_application = RPL4Application::ParseCommandLine(argc, argv); if (!run_application) { return 1; } else { Start_Registering(); } // //------------------------------------------------------------------------- // Create our window. This is only temporary because this will move after // the graphics are moved over. //------------------------------------------------------------------------- // WNDCLASS wc; if (!hPrevInstance) { wc.style = 0; wc.lpfnWndProc = (WNDPROC) WndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInstance; wc.hIcon = LoadIcon((HINSTANCE) NULL, IDI_APPLICATION); wc.hCursor = LoadCursor((HINSTANCE) NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); wc.lpszMenuName = NULL; wc.lpszClassName = L"MainWndClass"; if (!RegisterClass(&wc)) return FALSE; } DWORD wsStyle = WS_OVERLAPPED | WS_SYSMENU; if (L4Application::GetFullscreen()) wsStyle = WS_POPUP; hWnd = CreateWindowEx(0, L"MainWndClass", L"RPL4", WS_OVERLAPPEDWINDOW, 0, 0, L4Application::GetScreenWidth(), L4Application::GetScreenHeight(), (HWND)NULL, (HMENU)NULL, hInstance, (LPVOID)NULL); if (!hWnd) return FALSE; ShowWindow(hWnd, nShowCmd); // // Sticky placement, from the first frame the player sees. SVGA16 also // registers and reloads when it builds the cockpit, but that is not // until a mission starts - without this the console screen would come // up at the default rect with its title bar still on, and only jump // to the saved placement once a race began. -fit takes the whole // monitor and has no placement of the player's to restore. // if (!L4Application::GetFitDisplay() && !L4Application::GetFullscreen()) { RPWindowLayout_Register(hWnd, "RPL4", True); RPWindowLayout_Load(); } // //------------------------------------------------------------------------- // RP412JOYCONFIG=1 (joyconfig.bat): the joystick setup wizard, before // anything else claims the screen. It asks the player to move each // control on their stick, HOTAS or pedals and writes the joy* rows of // bindings.txt, then falls through into the game so they can try them // straight away. // // One shot: the variable is cleared from this process so a rebuilt // PadRIO later in the session cannot run the wizard a second time. //------------------------------------------------------------------------- // { const char *joyconfig = getenv("RP412JOYCONFIG"); if (joyconfig != NULL && *joyconfig != '\0' && atoi(joyconfig) != 0) { RPJoyConfigWizard(); SetEnvironmentVariableA("RP412JOYCONFIG", NULL); _putenv("RP412JOYCONFIG="); } } #if !_DEBUG // Arcade pods have no mouse - but desktop/windowed play needs the // cursor for the on-screen cockpit buttons, so hide it only when // running fullscreen. if (L4Application::GetFullscreen()) { ShowCursor(FALSE); } #endif // //------------------------------------------------------------------------- // Front-end mode: started without an egg, a network, or mission review. // The race loop below keeps everything in ONE process - setup screen, // mission, teardown, and back to the setup screen - with the local // console marshaling each race. (-egg / -net / -mr run a single pass, // exactly as before.) //------------------------------------------------------------------------- // Logical front_end_mode = L4Application::GetMissionReviewMode() == 0 && L4Application::GetNetworkCommonFlatAddress() == 0 && !L4Application::GetEggNotationFileName(); // no egg on the command line int last_launch_mode = FELaunchSingle; for (;;) { if (front_end_mode) { if (!IsWindow(hWnd)) { break; // the cockpit window is gone (closed mid-race) } // drain any WM_QUIT left over from the previous mission's // teardown so it cannot instantly close the setup screen MSG leftover; while (PeekMessage(&leftover, NULL, WM_QUIT, WM_QUIT, PM_REMOVE)) { } // lobby races: the owner publishes the score sheet, members // pull it - the same results screen shows on every machine RPL4Lobby_PushRaceResults(); RPL4Lobby_PullRaceResults(); // last race's final scores first (no-op on first boot) if (!RPL4FrontEnd_ShowResults(hInstance, hWnd)) { break; } static char frontend_egg[MAX_PATH]; if (!RPL4FrontEnd_Run(hInstance, hWnd, frontend_egg, sizeof(frontend_egg))) { break; // player closed the menu without launching } last_launch_mode = RPL4FrontEnd_LastLaunchMode(); Exit_Code = 0; if (last_launch_mode == FELaunchMember) { // // Lobby member: enter the race as a network pod. The // owner's console connects over the wire, feeds the egg, // and marshals us - no local egg, no local console. If // the owner leaves mid-race the mission ends (a race // with no console never stops on its own). // L4Application::SetNetworkCommonFlatAddress(1501); gConsoleLossEndsMission = True; } else { gConsoleLossEndsMission = False; L4Application::SetEggNotationFileName(frontend_egg); // Front-end games are marshaled by the in-process console: // it ends the race when the selected time expires. // (Hand-fed -egg runs stay unmarshaled - the developer // shortcut.) // // RP412HOSTPODS turns the launch into a hosted network // race: this pod switches to network mode (it meshes like // any pod) and the console also marshals the listed member // pods over the wire. The Steam lobby primes the same path. const char *host_pods = getenv("RP412HOSTPODS"); if (host_pods != NULL && host_pods[0] != '\0') { int host_port = 1501; const char *port_override = getenv("RP412HOSTPORT"); if (port_override != NULL && atoi(port_override) > 0) { host_port = atoi(port_override); } L4Application::SetNetworkCommonFlatAddress(host_port); if (!RPL4LocalConsole_InstallNetworkRace( RPL4FrontEnd_LastMissionSeconds(), frontend_egg, host_pods, RPL4FrontEnd_LastPilotNames())) { DEBUG_STREAM << "Network race setup failed - back to the menu\n" << std::flush; L4Application::SetNetworkCommonFlatAddress(0); continue; } } else { L4Application::SetNetworkCommonFlatAddress(0); RPL4LocalConsole_Install(RPL4FrontEnd_LastMissionSeconds()); } } } // //------------------------------------------------------------------------- // Open the resource file. The brace will help correct the scoping problem // of destruction the resource file //------------------------------------------------------------------------- // { StreamableResourceFile resources("rpl4.res", version); Check(&resources); // //---------------------------------------------- // Create and initialize the application manager //---------------------------------------------- // ApplicationManager* app_manager; Application *new_app; int review_mode = RPL4Application::GetMissionReviewMode(); if (review_mode > 0) { MissionReviewApplicationManager *spool_mgr = new MissionReviewApplicationManager(hInstance, hWnd, atoi(getenv("TARGETFPS")), (review_mode == 2) ? 1 : 2, SPOOL_SIZE); app_manager = spool_mgr; Register_Object(app_manager); // //------------------------------------------------------------------- // Create and initialize the playback application if there is a spool // file //------------------------------------------------------------------- // #if 1 CString spoolFileName = L4Application::GetSpoolFileName(); if (!spoolFileName) spoolFileName = "last.spl"; SpoolFile *spool = spool_mgr->GetStoredSpoolFile(spoolFileName); if (spool) { new_app = new RPL4PlaybackApplication(hInstance, hWnd, &resources, spool); } else { new_app = new RPL4IdleApplication(hInstance, hWnd, &resources); } Register_Object(new_app); app_manager->StartApplication(new_app); #else new_app = new RPL4PlaybackApplication(hInstance, hWnd, &resources, NULL); Register_Object(new_app); app_manager->StartApplication(new_app); #endif // //------------------------------------ // Initialize the spooling application //------------------------------------ // if (review_mode == 1) { new_app = new L4SpoolingApplication(hInstance, hWnd, &resources, RPL4); Register_Object(new_app); app_manager->StartApplication(new_app); } } else { app_manager = new ApplicationManager(hInstance, hWnd, atoi(getenv("TARGETFPS"))); Register_Object(app_manager); // //-------------------------------------- // Create and initialize the application //-------------------------------------- // Application *new_app = new RPL4Application(hInstance, hWnd, &resources); rpl4App = new_app; Register_Object(new_app); SetCursorPos(1680 / 2, 1050 / 2); app_manager->StartApplication(new_app); } // //-------------------------- // Run missions till we done //-------------------------- // app_manager->RunMissions(); rpl4App = NULL; Unregister_Object(app_manager); delete app_manager; } // //------------------------------------------------------------------------- // Single-binary race loop: if the local console ended this mission // (or we raced as a lobby member under the owner's console), go // back to the setup screen / lobby room for the next one; anything // else (user quit, -egg / -net / -mr single pass) falls out. //------------------------------------------------------------------------- // if (!front_end_mode || (last_launch_mode != FELaunchMember && !RPL4LocalConsole_MissionCompleted())) { break; } } // // Last word on the window's placement. Every finished drag has already // written it and SVGA16 writes again as the cockpit comes down, but a // session that never started a race has neither - and quitting from // the console screen is exactly how somebody would leave after moving // the window to where they want it. No-op unless the mode is save. // if (IsWindow(hWnd)) { RPWindowLayout_Save(); } RPWindowLayout_Forget(hWnd); #if !_DEBUG // symmetric with the fullscreen-only hide at startup if (L4Application::GetFullscreen()) { ShowCursor(TRUE); } #endif Stop_Registering(); return Exit_Code; }