diff --git a/.github/workflows/cibuild.yml b/.github/workflows/cibuild.yml index 9f436289c5..dbac5a4179 100644 --- a/.github/workflows/cibuild.yml +++ b/.github/workflows/cibuild.yml @@ -86,6 +86,7 @@ jobs: -DNEO_COPY_LIBRARIES=OFF -DNEO_USE_SEPARATE_BUILD_INFO=ON -DNEO_INSTALL_LIBRARIES=ON + -DNEO_INSTALL_LAUNCHER=ON -DNEO_INSTALL_RESOURCES=OFF -DNEO_EXTRA_ASSETS_ALT_PATH=${{ github.workspace }}/neoAssets @@ -131,6 +132,8 @@ jobs: echo "dedicated=$(find . -regex '\.\/neo-\w*-\w*-dedicated-\w*-\w*' -printf '%f')" >> "$GITHUB_ENV" echo "dedicated_debuginfo=$(find . -regex '\.\/neo-\w*-\w*-dedicated-debuginfo-\w*-\w*' -printf '%f')" >> "$GITHUB_ENV" echo "gamedata=$(find . -regex '\.\/neo-\w*-\w*-gamedata' -printf '%f')" >> "$GITHUB_ENV" + echo "launcher=$(find . -regex '\.\/neo-\w*-\w*-launcher-\w*-\w*' -printf '%f')" >> "$GITHUB_ENV" + echo "launcher_debuginfo=$(find . -regex '\.\/neo-\w*-\w*-launcher-debuginfo-\w*-\w*' -printf '%f')" >> "$GITHUB_ENV" - name: Upload libraries uses: actions/upload-artifact@v7 @@ -168,6 +171,20 @@ jobs: path: ${{ env.install_dir }}/${{ env.gamedata }} if-no-files-found: error + - name: Upload launcher + uses: actions/upload-artifact@v7 + with: + name: ${{ env.launcher }} + path: ${{ env.install_dir }}/${{ env.launcher }} + if-no-files-found: error + + - name: Upload launcher debug information + uses: actions/upload-artifact@v7 + with: + name: ${{ env.launcher_debuginfo }} + path: ${{ env.install_dir }}/${{ env.launcher_debuginfo }} + if-no-files-found: error + windows-build: name: ${{ matrix.platform.display_name }} ${{ matrix.preset_build_type.display_name }} @@ -241,6 +258,7 @@ jobs: -DNEO_COPY_LIBRARIES=OFF -DNEO_USE_SEPARATE_BUILD_INFO=ON -DNEO_INSTALL_LIBRARIES=ON + -DNEO_INSTALL_LAUNCHER=ON -DNEO_INSTALL_RESOURCES=OFF -DNEO_EXTRA_ASSETS_ALT_PATH=${{ github.workspace }}/neoAssets @@ -286,6 +304,8 @@ jobs: echo "dedicated=$(find . -regex '\.\/neo-\w*-\w*-dedicated-\w*-\w*' -printf '%f')" >> "$GITHUB_ENV" echo "dedicated_debuginfo=$(find . -regex '\.\/neo-\w*-\w*-dedicated-debuginfo-\w*-\w*' -printf '%f')" >> "$GITHUB_ENV" echo "gamedata=$(find . -regex '\.\/neo-\w*-\w*-gamedata' -printf '%f')" >> "$GITHUB_ENV" + echo "launcher=$(find . -regex '\.\/neo-\w*-\w*-launcher-\w*-\w*' -printf '%f')" >> "$GITHUB_ENV" + echo "launcher_debuginfo=$(find . -regex '\.\/neo-\w*-\w*-launcher-debuginfo-\w*-\w*' -printf '%f')" >> "$GITHUB_ENV" - name: Upload libraries uses: actions/upload-artifact@v7 @@ -315,6 +335,20 @@ jobs: path: ${{ env.install_dir }}/${{ env.dedicated_debuginfo }} if-no-files-found: error + - name: Upload launcher + uses: actions/upload-artifact@v7 + with: + name: ${{ env.launcher }} + path: ${{ env.install_dir }}/${{ env.launcher }} + if-no-files-found: error + + - name: Upload launcher debug information + uses: actions/upload-artifact@v7 + with: + name: ${{ env.launcher_debuginfo }} + path: ${{ env.install_dir }}/${{ env.launcher_debuginfo }} + if-no-files-found: error + pack-resources: name: Windows Native Resources @@ -358,6 +392,7 @@ jobs: -DNEO_COPY_LIBRARIES=OFF -DNEO_INSTALL_LIBRARIES=OFF -DNEO_INSTALL_RESOURCES=ON + -DNEO_INSTALL_LAUNCHER=OFF - name: Install resources run: cmake --install ${{ env.build_dir }} diff --git a/.gitignore b/.gitignore index ee09bd1a5c..2ac93e819a 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,11 @@ out /game/neo/screenshots +# Mod launcher binary and the appid file it writes at runtime +/game/neo_linux64 +/game/neo_win64.exe +/game/steam_appid.txt + # Binary Assets /game/**/bin/** /game/neo/maps diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 481b1c3ef3..cd36b56868 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -47,10 +47,13 @@ else () endif () option(NEO_INSTALL_LIBRARIES "Install game libraries" OFF) option(NEO_INSTALL_RESOURCES "Install game resources" OFF) +option(NEO_INSTALL_LAUNCHER "Install the Steam mod launcher" OFF) option(NEO_USE_SEPARATE_BUILD_INFO "Use separate build info on Linux" OFF) option(NEO_ENABLE_CPACK "Enable CPack" OFF) option(NEO_USE_MEM_DEBUG "Enable USE_MEM_DEBUG defines" OFF) option(NEO_GENERATE_GAMEDATA "Generate SourceMod gamedata" ${NEO_DEDICATED}) +option(NEO_BUILD_LAUNCHER "Build the Steam mod launcher" ON) +set(NEO_MOD_APPID "3172910" CACHE STRING "Steam appid for the mod launcher's steam_appid.txt") message(STATUS "Treat compile warnings as errors: ${CMAKE_COMPILE_WARNING_AS_ERROR}") message(STATUS "CI build mode: ${NEO_CI_BUILD}") @@ -72,8 +75,11 @@ message(STATUS "Alternate location to fetch the extra assets: ${NEO_EXTRA_ASSETS message(STATUS "Directory for output libraries: ${NEO_OUTPUT_LIBRARY_PATH}") message(STATUS "Install game libraries: ${NEO_INSTALL_LIBRARIES}") message(STATUS "Install game resources: ${NEO_INSTALL_RESOURCES}") +message(STATUS "Install the Steam mod launcher: ${NEO_INSTALL_LAUNCHER}") message(STATUS "Use separate build info on Linux: ${NEO_USE_SEPARATE_BUILD_INFO}") message(STATUS "Enable CPack: ${NEO_ENABLE_CPACK}") +message(STATUS "Build the Steam mod launcher: ${NEO_BUILD_LAUNCHER}") +message(STATUS "Mod launcher appid override: ${NEO_MOD_APPID}") message("") if(NEO_COPY_LIBRARIES) @@ -211,7 +217,6 @@ if(OS_WINDOWS) _ALLOW_RUNTIME_LIBRARY_MISMATCH _ALLOW_ITERATOR_DEBUG_LEVEL_MISMATCH _ALLOW_MSC_VER_MISMATCH - fopen=dont_use_fopen _MBCS ) @@ -452,6 +457,10 @@ endif() add_subdirectory(game/server) +if(NEO_BUILD_LAUNCHER AND NOT NEO_DEDICATED) + add_subdirectory(launcher_main) +endif() + if(NEO_COPY_LIBRARIES) set(COPY_ALL_LIBS_FLAG ALL) else() @@ -585,6 +594,40 @@ if(NEO_INSTALL_LIBRARIES) endif() endif() +if(NEO_INSTALL_LAUNCHER AND NEO_BUILD_LAUNCHER AND NOT NEO_DEDICATED) + # The launcher sits at the install root, next to the neo/ game directory. + # steam_api ships with the engine binaries in bin/, so only the launcher + # itself is staged here. + set(LAUNCHER_INSTALL_PATH "${INSTALL_PATH_PREFIX}-launcher-${CMAKE_SYSTEM_NAME}-${BUILD_TYPE_NAME}") + + if(NEO_USE_SEPARATE_BUILD_INFO) + set(LAUNCHER_DEBUG_INFO_INSTALL_PATH "${INSTALL_PATH_PREFIX}-launcher-debuginfo-${CMAKE_SYSTEM_NAME}-${BUILD_TYPE_NAME}") + + if(OS_WINDOWS) + install(TARGETS launcher_main RUNTIME DESTINATION "${LAUNCHER_INSTALL_PATH}") + + install( + FILES "$" + DESTINATION "${LAUNCHER_DEBUG_INFO_INSTALL_PATH}" + ) + else() + get_target_property(LAUNCHER_STRIPPED_FILE launcher_main STRIPPED_FILE) + install( + PROGRAMS ${LAUNCHER_STRIPPED_FILE} + DESTINATION "${LAUNCHER_INSTALL_PATH}" + ) + + get_target_property(LAUNCHER_SPLIT_DEBUG_INFO_FILE launcher_main SPLIT_DEBUG_INFO_FILE) + install( + FILES ${LAUNCHER_SPLIT_DEBUG_INFO_FILE} + DESTINATION "${LAUNCHER_DEBUG_INFO_INSTALL_PATH}" + ) + endif() + else() + install(TARGETS launcher_main RUNTIME DESTINATION "${LAUNCHER_INSTALL_PATH}") + endif() +endif() + if(NEO_INSTALL_RESOURCES) install( DIRECTORY diff --git a/src/cmake/packaging.cmake b/src/cmake/packaging.cmake index d37c52cf37..b3ccdf68b9 100644 --- a/src/cmake/packaging.cmake +++ b/src/cmake/packaging.cmake @@ -10,6 +10,10 @@ if(NEO_INSTALL_RESOURCES) set(CPACK_PACKAGE_FILE_NAME "neo-${BUILD_DATE_SHORT}-${GIT_HASH}-resources") endif() +if(NEO_INSTALL_LAUNCHER) + set(CPACK_PACKAGE_FILE_NAME "neo-${BUILD_DATE_SHORT}-${GIT_HASH}-launcher-${CMAKE_SYSTEM_NAME}") +endif() + if(OS_WINDOWS) set(CPACK_GENERATOR "ZIP") else() diff --git a/src/cmake/utils.cmake b/src/cmake/utils.cmake index 6b18258b07..056a659186 100644 --- a/src/cmake/utils.cmake +++ b/src/cmake/utils.cmake @@ -134,9 +134,33 @@ function(split_debug_information) set(TARGET_OUTPUT_NAME "${PARSED_ARGS_TARGET}") endif() - set(SPLITDEBUG_SOURCE "${CMAKE_CURRENT_BINARY_DIR}/${TARGET_OUTPUT_NAME}.so") - set(SPLITDEBUG_TARGET "${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_SUBDIRECTORY}/${TARGET_OUTPUT_NAME}.so") - set(SPLITDEBUG_TARGET_DEBUG "${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_SUBDIRECTORY}/${TARGET_OUTPUT_NAME}.so.debug") + get_target_property(TARGET_PREFIX ${PARSED_ARGS_TARGET} PREFIX) + if("${TARGET_PREFIX}" STREQUAL "TARGET_PREFIX-NOTFOUND") + get_target_property(TARGET_TYPE ${PARSED_ARGS_TARGET} TYPE) + if(TARGET_TYPE STREQUAL "EXECUTABLE") + set(TARGET_PREFIX "") + elseif(TARGET_TYPE STREQUAL "SHARED_LIBRARY") + set(TARGET_PREFIX "${CMAKE_SHARED_LIBRARY_PREFIX}") + else() + message(WARNING "Unknown TARGET_TYPE value") + endif() + endif() + + get_target_property(TARGET_SUFFIX ${PARSED_ARGS_TARGET} SUFFIX) + if("${TARGET_SUFFIX}" STREQUAL "TARGET_SUFFIX-NOTFOUND") + get_target_property(TARGET_TYPE ${PARSED_ARGS_TARGET} TYPE) + if(TARGET_TYPE STREQUAL "EXECUTABLE") + set(TARGET_SUFFIX "${CMAKE_EXECUTABLE_SUFFIX}") + elseif(TARGET_TYPE STREQUAL "SHARED_LIBRARY") + set(TARGET_SUFFIX "${CMAKE_SHARED_LIBRARY_SUFFIX}") + else() + message(WARNING "Unknown TARGET_TYPE value") + endif() + endif() + + set(SPLITDEBUG_SOURCE "${CMAKE_CURRENT_BINARY_DIR}/${TARGET_PREFIX}${TARGET_OUTPUT_NAME}${TARGET_SUFFIX}") + set(SPLITDEBUG_TARGET "${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_SUBDIRECTORY}/${TARGET_PREFIX}${TARGET_OUTPUT_NAME}${TARGET_SUFFIX}") + set(SPLITDEBUG_TARGET_DEBUG "${CMAKE_CURRENT_BINARY_DIR}/${OUTPUT_SUBDIRECTORY}/${TARGET_PREFIX}${TARGET_OUTPUT_NAME}${TARGET_SUFFIX}.debug") add_custom_target( ${PARSED_ARGS_TARGET}_split_debug_information diff --git a/src/game/server/CMakeLists.txt b/src/game/server/CMakeLists.txt index 5f9434ff9a..da026598cc 100644 --- a/src/game/server/CMakeLists.txt +++ b/src/game/server/CMakeLists.txt @@ -99,6 +99,11 @@ if(OS_LINUX OR OS_MACOS) endif() if(OS_WINDOWS) + target_compile_definitions(server + PRIVATE + fopen=dont_use_fopen + ) + target_link_libraries(server PRIVATE winmm.lib diff --git a/src/launcher_main/CMakeLists.txt b/src/launcher_main/CMakeLists.txt new file mode 100644 index 0000000000..09003c64be --- /dev/null +++ b/src/launcher_main/CMakeLists.txt @@ -0,0 +1,90 @@ +# Steam mod launcher. Runs the game from the mod's own install directory +# using the mod's shipped engine binaries; the Source SDK Base 2013 MP +# install is only an asset dependency, mounted through gameinfo.txt. +if(OS_LINUX) + find_package(SDL2 REQUIRED) +endif() + +set(LAUNCHER_SOURCES main.cpp) + +if(OS_WINDOWS) + list(APPEND LAUNCHER_SOURCES launcher_main_mod_neo.rc) +endif() + +add_executable(launcher_main + ${LAUNCHER_SOURCES} +) + +# GetExecutableModName derives the default -game dir from the executable name +# by stripping from the last underscore: neo_linux64 -> -game neo. The suffix +# also avoids colliding with the game/neo directory at the install root. +if(OS_LINUX) + set_target_properties(launcher_main + PROPERTIES + OUTPUT_NAME neo_linux64 + ) +elseif(OS_WINDOWS) + set_target_properties(launcher_main + PROPERTIES + OUTPUT_NAME neo_win64 + WIN32_EXECUTABLE TRUE + ) +endif() + +if(NEO_USE_SEPARATE_BUILD_INFO) + split_debug_information(TARGET launcher_main) +endif() + +target_include_directories(launcher_main + PRIVATE + ${CMAKE_SOURCE_DIR}/public + ${CMAKE_SOURCE_DIR}/common +) + +if(OS_LINUX) + target_link_libraries(launcher_main + PRIVATE + SDL2::SDL2 + ${CMAKE_DL_LIBS} + ) + + # main.cpp defines __wrap_fopen/__wrap_fopen64 which reference __real_fopen/__real_fopen64 + target_link_options(launcher_main + PRIVATE + -Wl,--wrap=fopen + -Wl,--wrap=fopen64 + ) +endif() + +if(NEO_MOD_APPID) + target_compile_definitions(launcher_main + PRIVATE + MOD_LAUNCHER + MOD_APPID=${NEO_MOD_APPID} + ) +endif() + +if(NEO_COPY_LIBRARIES) + # Stage the standalone-install layout: launcher at the install root next to + # neo/, and the steam_api lib where the launcher dlopens it (game/bin, the + # engine binary dir). + add_custom_command(TARGET launcher_main POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + ${CMAKE_SOURCE_DIR}/../game/ + VERBATIM + ) + # The repo only carries the Linux steam_api runtime; on Windows + # lib/public/x64 has the import lib, and the launcher loads + # bin/x64/steam_api64.dll from a Source SDK Base / depot install. + if(OS_LINUX) + add_custom_command(TARGET launcher_main POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory + ${CMAKE_SOURCE_DIR}/../game/bin/linux64/ + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_SOURCE_DIR}/lib/public/linux64/libsteam_api.so + ${CMAKE_SOURCE_DIR}/../game/bin/linux64/ + VERBATIM + ) + endif() +endif() diff --git a/src/launcher_main/launcher_main_mod_neo.rc b/src/launcher_main/launcher_main_mod_neo.rc new file mode 100644 index 0000000000..9129d30401 --- /dev/null +++ b/src/launcher_main/launcher_main_mod_neo.rc @@ -0,0 +1,72 @@ +//Microsoft Developer Studio generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "afxres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (U.S.) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +#ifdef _WIN32 +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) +#endif //_WIN32 + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_LAUNCHER ICON DISCARDABLE "res\\mod_neo.ico" + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE DISCARDABLE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE DISCARDABLE +BEGIN + "#include ""afxres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE DISCARDABLE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + +#endif // English (U.S.) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/src/launcher_main/main.cpp b/src/launcher_main/main.cpp index 19dbd35e2d..e57a6bb671 100644 --- a/src/launcher_main/main.cpp +++ b/src/launcher_main/main.cpp @@ -9,6 +9,10 @@ #include #include #include +#ifdef NEO +#include +#include +#endif // NEO #endif #ifdef POSIX @@ -17,6 +21,12 @@ #include #include #include +#ifdef NEO +#include +#include +#include +#include +#endif // NEO #define MAX_PATH PATH_MAX #endif @@ -47,6 +57,9 @@ extern "C" { __declspec( dllexport ) int AmdPowerXpressRequestHighPerformance = #endif +#ifdef NEO +#include "tier1/strtools.h" // matchmakingtypes.h (via steam_api.h) uses V_strcpy_safe +#endif #include #ifdef _WIN32 @@ -95,6 +108,12 @@ static const AppId_t k_unMyModAppid = k_unSDK2013MPAppId; #endif static bool s_bInittedSteam = false; +#ifdef NEO +// Set when Steam reports the SDK Base app as not installed. The launch +// continues anyway: the engine detects the missing dependency at gameinfo +// mount and raises Steam's install dialog, same as the retail flow. +static bool s_bSDKNotInstalled = false; +#endif // NEO #ifdef _WIN32 static HMODULE s_SteamModule; @@ -226,10 +245,27 @@ static bool GetGameInstallDir( const char *pRootDir, char *pszBuf, int nBufSize if ( unLength == 0 ) { +#ifdef NEO + // No error box: letting the engine run produces its own prompt plus + // the Steam install dialog for the missing SDK. + s_bSDKNotInstalled = true; +#else MessageBox( 0, "Source SDK 2013 Multiplayer (243750) must be installed to launch this mod.", "Launcher Error", MB_OK ); +#endif // NEO return false; } +#ifdef NEO + // Steam reports the app as installed from its manifest alone; the files + // can still be missing (moved library, interrupted uninstall). Mounting + // would fail silently later, so check the directory really exists. + struct stat sdkStat; + if ( stat( pszBuf, &sdkStat ) != 0 || !( sdkStat.st_mode & S_IFDIR ) ) + { + MessageBox( 0, "The Source SDK 2013 Multiplayer (243750) files are missing from disk. Verify or reinstall it in Steam to play this mod.", "Launcher Error", MB_OK ); + return false; + } +#endif // NEO return true; } @@ -275,6 +311,56 @@ static char *GetBaseDir( const char *pszBuffer ) return basedir; } +#ifdef NEO +static const char *GetExecutableModName( char *pszExePath ) +{ + static char s_szFinalFilename[ MAX_PATH + 1 ] = "hl2"; + + char szExePath[ MAX_PATH + 1 ]; + strncpy( szExePath, pszExePath, sizeof( szExePath ) ); + szExePath[ MAX_PATH ] = 0; + + if ( !*szExePath ) + return s_szFinalFilename; + + const char *pszLastSeparator = strrchr( szExePath, '/' ); +#ifdef _WIN32 + // Steam and the shell may hand us either separator on Windows. + const char *pszLastBackslash = strrchr( szExePath, '\\' ); + if ( !pszLastSeparator || ( pszLastBackslash && pszLastBackslash > pszLastSeparator ) ) + { + pszLastSeparator = pszLastBackslash; + } +#endif + if ( !pszLastSeparator ) + return s_szFinalFilename; + + strncpy( s_szFinalFilename, pszLastSeparator + 1, sizeof( s_szFinalFilename ) ); + s_szFinalFilename[ MAX_PATH ] = 0; + char *pszLastUnderscore = NULL; + char* pszLastDot = NULL; + for ( char *ch = s_szFinalFilename; *ch != 0; ch++ ) + { + if ( *ch == '_' ) + { + pszLastUnderscore = ch; + } + + if ( *ch == '.' ) + { + pszLastDot = ch; + } + } + + if ( pszLastUnderscore ) + *pszLastUnderscore = 0; + + if ( pszLastDot ) + *pszLastDot = 0; + + return s_szFinalFilename; +} +#endif // NEO #ifdef WIN32 #include @@ -447,6 +533,17 @@ int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdL // Get the root directory the .exe is in char* pRootDir = GetBaseDir( moduleName ); +#ifdef NEO + // The game runs from its own install with its own bin\; the SDK Base + // install is only checked here because gameinfo.txt mounts its assets. + char szSDKInstallDir[4096]; + if ( !GetGameInstallDir( pRootDir, szSDKInstallDir, sizeof( szSDKInstallDir ) ) && !s_bSDKNotInstalled ) + { + return 1; + } + + const char *pBinaryGameDir = pRootDir; +#else const char *pBinaryGameDir = pRootDir; char szGameInstallDir[4096]; if ( !GetGameInstallDir( pRootDir, szGameInstallDir, 4096 ) ) @@ -457,6 +554,7 @@ int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdL pBinaryGameDir = szGameInstallDir; SetEnvironmentVariableA( "SDK_EXEC_DIR", szGameInstallDir ); +#endif // NEO #define LAUNCHER_DLL_PATH "%s\\" PLATFORM_BIN_DIR "\\launcher.dll" #define LAUNCHER_PATH "%s\\" PLATFORM_BIN_DIR @@ -486,6 +584,34 @@ int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdL return 0; } +#ifdef NEO + // Launch options carry no -game; derive the mod dir from our own + // binary name (neo_win64.exe -> neo) like the POSIX launcher does. + char szNewCmdLine[8192]; + bool bHasGame = false; + { + const char *pszScan = lpCmdLine; + while ( ( pszScan = strstr( pszScan, "-game" ) ) != NULL ) + { + const bool bStartOk = ( pszScan == lpCmdLine ) || ( pszScan[-1] == ' ' ) || ( pszScan[-1] == '\t' ); + const char chEnd = pszScan[5]; + if ( bStartOk && ( chEnd == '\0' || chEnd == ' ' || chEnd == '\t' ) ) + { + bHasGame = true; + break; + } + pszScan += 5; + } + } + + if ( !bHasGame ) + { + _snprintf( szNewCmdLine, sizeof( szNewCmdLine ), "%s -game \"%s\\%s\"", + lpCmdLine, pRootDir, GetExecutableModName( moduleName ) ); + szNewCmdLine[sizeof( szNewCmdLine ) - 1] = '\0'; + lpCmdLine = szNewCmdLine; + } +#endif // NEO LauncherMain_t main = (LauncherMain_t)GetProcAddress( launcher, "LauncherMain" ); return main( hInstance, hPrevInstance, lpCmdLine, nCmdShow ); } @@ -496,7 +622,17 @@ int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdL bool GetExePath( char *pszPath, size_t size ) { +#ifdef NEO + ssize_t nLength = readlink( "/proc/self/exe", pszPath, size - 1 ); + if ( nLength <= 0 ) + { + return false; + } + pszPath[nLength] = '\0'; + return true; +#else return readlink("/proc/self/exe", pszPath, size ) > 0; +#endif // NEO } #include @@ -567,6 +703,55 @@ static void WaitForDebuggerConnect( int argc, char *argv[], int time ) #endif // !LINUX +#ifdef NEO +// The engine resolves |appid_243750| gameinfo mounts by opening the SDK Base +// 2013 Multiplayer install path with the final word of the folder name +// lowercased ("...2013 multiplayer"). On case-sensitive filesystems that path +// does not exist, so every SDK asset mount fails silently. Give the mangled +// name a symlink to the real install so those mounts resolve. +static void CreateSDKCaseShim( const char *pszSDKInstallDir ) +{ + static const char szExpectedName[] = "Source SDK Base 2013 Multiplayer"; + static const char szMangledName[] = "Source SDK Base 2013 multiplayer"; + + const char *pszLeaf = strrchr( pszSDKInstallDir, '/' ); + if ( !pszLeaf || strcmp( pszLeaf + 1, szExpectedName ) != 0 ) + { + // Unexpected install folder name; the engine's mangling is only + // known for the stock name, so do nothing. + return; + } + + char szMangledPath[4096]; + snprintf( szMangledPath, sizeof( szMangledPath ), "%.*s/%s", + (int)( pszLeaf - pszSDKInstallDir ), pszSDKInstallDir, szMangledName ); + + // Already resolves on case-insensitive filesystems or if the link exists. + if ( access( szMangledPath, F_OK ) == 0 ) + { + return; + } + + // A leftover link can dangle (SDK moved or reinstalled elsewhere); + // replace it instead of failing with EEXIST. Anything that is not a + // symlink was not created by us, so leave it alone. + struct stat linkStat; + if ( lstat( szMangledPath, &linkStat ) == 0 ) + { + if ( !S_ISLNK( linkStat.st_mode ) ) + { + return; + } + unlink( szMangledPath ); + } + + if ( symlink( pszSDKInstallDir, szMangledPath ) != 0 ) + { + fprintf( stderr, "[NT;RE launcher] Warning: couldn't create '%s' -> '%s' (%s); SDK content may fail to mount.\n", + szMangledPath, pszSDKInstallDir, strerror( errno ) ); + } +} +#else static const char *GetExecutableModName( char *pszExePath ) { @@ -608,9 +793,130 @@ static const char *GetExecutableModName( char *pszExePath ) return s_szFinalFilename; } +#endif // NEO int main( int argc, char *argv[] ) { +#ifdef NEO + char moduleName[MAX_PATH]; + if ( !GetExePath( moduleName, sizeof( moduleName ) ) ) + { + return 1; + } + + char* pRootDir = GetBaseDir( moduleName ); + + // Run from the install root: relative engine paths and steam_appid.txt + // both resolve against the working directory. + if ( chdir( pRootDir ) != 0 ) + { + fprintf( stderr, "[NT;RE launcher] Warning: chdir(%s) failed: %s\n", pRootDir, strerror( errno ) ); + } + + // The dynamic linker only reads LD_LIBRARY_PATH at process start, and + // launcher.so's dependencies resolve through it, so prepend our bin dir + // and re-exec ourselves exactly once. + if ( !getenv( "NEO_LAUNCHER_BOOTSTRAPPED" ) ) + { + char szLibPath[8192]; + const char *pszOldPath = getenv( "LD_LIBRARY_PATH" ); + snprintf( szLibPath, sizeof( szLibPath ), "%s/" PLATFORM_BIN_DIR "%s%s", + pRootDir, pszOldPath ? ":" : "", pszOldPath ? pszOldPath : "" ); + setenv( "LD_LIBRARY_PATH", szLibPath, 1 ); + setenv( "NEO_LAUNCHER_BOOTSTRAPPED", "1", 1 ); + + // Same environment the stock launch script sets up. + setenv( "__GL_THREADED_OPTIMIZATIONS", "1", 1 ); + if ( access( "pathmatch.inf", F_OK ) == 0 ) + { + setenv( "ENABLE_PATHMATCH", "1", 1 ); + } + + execv( moduleName, argv ); + fprintf( stderr, "[NT;RE launcher] Failed to re-exec %s: %s\n", moduleName, strerror( errno ) ); + return 1; + } + + // Give a debugger a chance to attach before anything interesting runs. + WaitForDebuggerConnect( argc, argv, 30 ); + + // The stock launch script raises the fd limit; the engine leans on it. + struct rlimit fdLimit; + if ( getrlimit( RLIMIT_NOFILE, &fdLimit ) == 0 && fdLimit.rlim_cur < 2048 ) + { + fdLimit.rlim_cur = ( fdLimit.rlim_max == RLIM_INFINITY || fdLimit.rlim_max > 2048 ) ? 2048 : fdLimit.rlim_max; + setrlimit( RLIMIT_NOFILE, &fdLimit ); + } + + char szSDKInstallDir[4096]; + if ( GetGameInstallDir( pRootDir, szSDKInstallDir, sizeof( szSDKInstallDir ) ) ) + { + CreateSDKCaseShim( szSDKInstallDir ); + } + else if ( !s_bSDKNotInstalled ) + { + return 1; + } + + // Build the argument list for LauncherMain, appending a default -game + // derived from our binary name (neo_linux64 -> neo) when absent. + std::vector new_argv; + new_argv.push_back( moduleName ); + + bool bHasGame = false; + for ( int i = 1; i < argc; i++ ) + { + if ( !strcmp( argv[i], "-game" ) ) + { + bHasGame = true; + } + new_argv.push_back( argv[i] ); + } + + char szGamePath[8192]; + if ( !bHasGame ) + { + new_argv.push_back( (char *)"-game" ); + const char *pModName = GetExecutableModName( moduleName ); + snprintf( szGamePath, sizeof( szGamePath ), "%s/%s", pRootDir, pModName ); + printf( "[NT;RE launcher] Launching default game: %s\n", pModName ); + new_argv.push_back( szGamePath ); + } + + const int nLauncherArgc = (int)new_argv.size(); + new_argv.push_back( NULL ); + + char szLauncherPath[8192]; + snprintf( szLauncherPath, sizeof( szLauncherPath ), "%s/" PLATFORM_BIN_DIR "/launcher.so", pRootDir ); + + void *pLauncherModule = Launcher_LoadModule( szLauncherPath ); + if ( !pLauncherModule ) + { + char szError[sizeof( szLauncherPath ) + 2048]; + snprintf( szError, sizeof( szError ), "Failed to load %s:\n%s", szLauncherPath, dlerror() ); + MessageBox( 0, szError, "Launcher Error", MB_OK ); + return 1; + } + + LauncherMain_t pfnLauncherMain = (LauncherMain_t)Launcher_GetProcAddress( pLauncherModule, "LauncherMain" ); + if ( !pfnLauncherMain ) + { + MessageBox( 0, "LauncherMain not found in launcher.so", "Launcher Error", MB_OK ); + return 1; + } + + const int nStatus = pfnLauncherMain( nLauncherArgc, new_argv.data() ); + + // The engine requests a relaunch (e.g. video mode changes) with this + // status; the stock launch script loops on it, we re-exec fresh. + if ( nStatus == 42 ) + { + execv( moduleName, argv ); + fprintf( stderr, "[NT;RE launcher] Failed to re-exec %s after engine restart request: %s\n", moduleName, strerror( errno ) ); + } + + return nStatus; +#else char moduleName[MAX_PATH]; if ( !GetExePath( moduleName, sizeof( moduleName ) ) ) { @@ -663,6 +969,7 @@ int main( int argc, char *argv[] ) execvp( szExecutable, new_argv.data() ); return 0; +#endif // NEO } #else diff --git a/src/launcher_main/res/mod_neo.ico b/src/launcher_main/res/mod_neo.ico new file mode 100644 index 0000000000..6cdb58308f Binary files /dev/null and b/src/launcher_main/res/mod_neo.ico differ diff --git a/src/tier1/CMakeLists.txt b/src/tier1/CMakeLists.txt index 87f2ce6a92..395eb35985 100644 --- a/src/tier1/CMakeLists.txt +++ b/src/tier1/CMakeLists.txt @@ -30,6 +30,7 @@ target_include_directories(tier1 ) if(OS_WINDOWS) + target_compile_definitions(tier1 PRIVATE fopen=dont_use_fopen) target_link_libraries(tier1 PRIVATE Rpcrt4.lib Ws2_32.lib) elseif(OS_LINUX) target_link_libraries(tier1 PUBLIC