diff --git a/.github/workflows/cibuild.yml b/.github/workflows/cibuild.yml index 9d671267886..792b6fcaf2b 100644 --- a/.github/workflows/cibuild.yml +++ b/.github/workflows/cibuild.yml @@ -138,7 +138,7 @@ jobs: - name: Install macOS packages if: ${{ startsWith(matrix.platform.name, 'macOS') }} run: | - brew install ccache cmake sdl2 lzo libogg libvorbis theora openal-soft jpeg-turbo + brew install ccache cmake dylibbundler sdl2 lzo libogg libvorbis theora openal-soft jpeg-turbo - name: Install Fedora packages if: ${{ matrix.platform.name == 'Fedora' }} @@ -187,6 +187,13 @@ jobs: if: ${{ startsWith(matrix.platform.name, 'macOS') }} run: ccache -s || true + - name: Make macOS app bundle + if: ${{ startsWith(matrix.platform.name, 'macOS') && matrix.configuration == 'Release' && steps.cmake-build.outcome == 'success' }} + id: make-macos-app-bundle + run: | + bash misc/macos/make_app_bundle.sh "${{ matrix.platform.arch }}" "${{ matrix.configuration }}" + file build/artifacts/openxray*.* + # TODO: Merge this step with 'Make AppImage' once we switch to CMake 4.2 which directly supports AppImage generation with CPack # https://cmake.org/cmake/help/latest/cpack_gen/appimage.html - name: Make package @@ -209,7 +216,7 @@ jobs: file build/artifacts/openxray*.* - name: Upload OpenXRay artifact - if: ${{ steps.make-package.outcome == 'success' || steps.make-appimage.outcome == 'success' }} + if: ${{ steps.make-package.outcome == 'success' || steps.make-appimage.outcome == 'success' || steps.make-macos-app-bundle.outcome == 'success' }} uses: actions/upload-artifact@main with: name: ${{ matrix.platform.name }} ${{ matrix.configuration }} ${{ matrix.platform.arch }} (${{ matrix.platform.cc }} github-${{ github.run_number }}) diff --git a/Externals/LuaJIT-proj/CMakeLists.txt b/Externals/LuaJIT-proj/CMakeLists.txt index 9b95649219e..e418976f005 100644 --- a/Externals/LuaJIT-proj/CMakeLists.txt +++ b/Externals/LuaJIT-proj/CMakeLists.txt @@ -15,7 +15,6 @@ set(RELVER 5) set(ABIVER 5.1) set(NODOTABIVER 51) -set(CMAKE_OSX_SYSROOT "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk") set(LUAJIT_DIR ${CMAKE_SOURCE_DIR}/Externals/LuaJIT/src CACHE PATH "Location of luajit sources") option(BUILD_SHARED_LIBS "Build as shared library" ON) diff --git a/README.md b/README.md index d6ee5064be6..6aa95ee1d3e 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ However, they know about many community projects, including this, and support S. Installation instructions are on the [How to install and play](https://github.com/OpenXRay/xray-16/wiki/[EN]-How-to-install-and-play) page. +The experimental Shadow of Chernobyl procedure for Apple silicon Macs is in +[the macOS guide](misc/macos/README.md). + ## Supported game platforms - Call of Chernobyl 1.4.22. - Call of Pripyat 1.6.02. diff --git a/cmake/XRay.Compiler.GNULike.cmake b/cmake/XRay.Compiler.GNULike.cmake index a9f5c0cdfca..471b4b3fb13 100644 --- a/cmake/XRay.Compiler.GNULike.cmake +++ b/cmake/XRay.Compiler.GNULike.cmake @@ -5,8 +5,28 @@ if (APPLE) if ($ENV{MACOSX_DEPLOYMENT_TARGET}) set(CMAKE_OSX_DEPLOYMENT_TARGET $ENV{MACOSX_DEPLOYMENT_TARGET}) else() - message(NOTICE "CMAKE_OSX_DEPLOYMENT_TARGET is not set, defaulting it to your system's version: ${CMAKE_SYSTEM_VERSION}") - set(CMAKE_OSX_DEPLOYMENT_TARGET ${CMAKE_SYSTEM_VERSION}) + execute_process( + COMMAND sw_vers -productVersion + OUTPUT_VARIABLE XRAY_MACOS_PRODUCT_VERSION + RESULT_VARIABLE XRAY_SW_VERS_RESULT + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + ) + + if (XRAY_SW_VERS_RESULT STREQUAL "0" AND XRAY_MACOS_PRODUCT_VERSION MATCHES "^([0-9]+)") + set(CMAKE_OSX_DEPLOYMENT_TARGET "${CMAKE_MATCH_1}.0") + message(NOTICE "CMAKE_OSX_DEPLOYMENT_TARGET is not set, defaulting it to macOS ${CMAKE_OSX_DEPLOYMENT_TARGET}") + elseif (CMAKE_SYSTEM_VERSION MATCHES "^([0-9]+)") + set(XRAY_DARWIN_VERSION_MAJOR "${CMAKE_MATCH_1}") + if (XRAY_DARWIN_VERSION_MAJOR GREATER_EQUAL 20) + math(EXPR XRAY_MACOS_VERSION_MAJOR "${XRAY_DARWIN_VERSION_MAJOR} - 9") + set(CMAKE_OSX_DEPLOYMENT_TARGET "${XRAY_MACOS_VERSION_MAJOR}.0") + message(NOTICE "CMAKE_OSX_DEPLOYMENT_TARGET is not set, defaulting it to macOS ${CMAKE_OSX_DEPLOYMENT_TARGET}") + else() + message(NOTICE "CMAKE_OSX_DEPLOYMENT_TARGET is not set, defaulting it to 10.15") + set(CMAKE_OSX_DEPLOYMENT_TARGET 10.15) + endif() + endif() endif() endif() message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}") diff --git a/misc/macos/README.md b/misc/macos/README.md new file mode 100644 index 00000000000..797bc9ffd76 --- /dev/null +++ b/misc/macos/README.md @@ -0,0 +1,186 @@ +# Run Shadow of Chernobyl on an Apple silicon Mac + +This procedure is experimental. The main OpenXRay project does not yet list +Shadow of Chernobyl as a supported game. Use a licensed Steam copy of the +original game. + +## 1. Install the build tools + +Install the Xcode command-line tools: + +```sh +xcode-select --install +``` + +Install the required Homebrew packages: + +```sh +brew install git cmake ninja dylibbundler sdl2 lzo libogg libvorbis theora openal-soft jpeg-turbo +``` + +Install SteamCMD for macOS as described in the +[Valve SteamCMD guide](https://developer.valvesoftware.com/wiki/SteamCMD). +Make sure that the `steamcmd` command is in `PATH`. + +## 2. Get the source code + +Clone the repository with its submodules: + +```sh +git clone https://github.com/OpenXRay/xray-16.git --recurse-submodules +cd xray-16 +``` + +If you already have the repository, update its submodules: + +```sh +git submodule update --init --recursive +``` + +## 3. Get the licensed game data from Steam + +Your Steam account must own S.T.A.L.K.E.R.: Shadow of Chernobyl. The Steam app +ID is `4500`. + +Create a data directory in the repository: + +```sh +mkdir -p "$PWD/build/game-data/soc" +``` + +Download the Windows game data. Replace `YOUR_STEAM_ACCOUNT_NAME` with your +Steam account name. Do not put your password in the command. SteamCMD asks for +the password and the Steam Guard code when they are necessary. + +```sh +steamcmd \ + +@sSteamCmdForcePlatformType windows \ + +force_install_dir "$PWD/build/game-data/soc" \ + +login YOUR_STEAM_ACCOUNT_NAME \ + +app_update 4500 validate \ + +quit +``` + +The selected directory must contain the original archives from +`gamedata.db0` through `gamedata.dbd`. Keep these files unchanged. Do not +extract or decrypt them. OpenXRay detects the encrypted SoC archives and +decrypts their contents when it reads them. + +Do not add these licensed data files to Git. The application package contains +only OpenXRay files. + +## 4. Build OpenXRay + +Configure and build the Release version: + +```sh +cmake -S . -B build-macos -G Ninja -DCMAKE_BUILD_TYPE=Release +cmake --build build-macos --config Release -j "$(sysctl -n hw.logicalcpu)" +``` + +The Apple silicon build files are in `bin/arm64/Release`. + +## 5. Build the macOS application + +Create the application, ZIP file, and DMG file: + +```sh +bash misc/macos/make_app_bundle.sh arm64 Release soc +``` + +For a faster local package without a DMG file, use: + +```sh +OPENXRAY_SKIP_DMG=1 bash misc/macos/make_app_bundle.sh arm64 Release soc +``` + +The files are in `build/artifacts`. + +## 6. Install and start the game + +If `/Applications/OpenXRay SoC.app` already exists, move it to the Trash or to +a backup directory first. Do not copy a new application over an old +application. macOS can merge the two directory trees and keep old scripts. + +Copy `build/artifacts/OpenXRay SoC.app` to `/Applications`. Then start it from +Finder. + +To skip the publisher title videos for one start, run: + +```sh +open -F -n --env OPENXRAY_SKIP_INTRO=1 "/Applications/OpenXRay SoC.app" +``` + +This starts the application through macOS LaunchServices. The normal Finder +launch keeps the title videos. The `-F` option also tells macOS not to restore +old application window state. + +At the first start, select this directory when OpenXRay asks for game data: + +```text +/build/game-data/soc +``` + +OpenXRay creates its runtime directory here: + +```text +~/Library/Application Support/GSC Game World/S.T.A.L.K.E.R. - Shadow of Chernobyl +``` + +The runtime directory has links to the licensed archives and to the OpenXRay +resources in the application. Saves, settings, and logs also use this runtime +directory. + +To select a different game-data directory, run: + +```sh +"/Applications/OpenXRay SoC.app/Contents/MacOS/xr_3da" -reset_gamedata_path +``` + +## 7. Stop a process that does not close + +If the game does not close, stop only the OpenXRay SoC process: + +```sh +pkill -f '/Applications/OpenXRay SoC.app/Contents/MacOS/xr_3da' +``` + +Confirm that no game process remains: + +```sh +pgrep -fl '/Applications/OpenXRay SoC.app/Contents/MacOS/xr_3da' +``` + +The second command has no output when the process is stopped. + +## 8. Use a resizable window with borders + +Open the in-game console and run: + +```text +vid_window_mode st_opt_windowed +vid_restart +``` + +The setting is saved in `user.ltx`. Other available values include +`st_opt_windowed_borderless`, `st_opt_fullscreen`, and +`st_opt_fullscreen_borderless`. + +## 9. Get a crash log + +Logs are in: + +```text +~/Library/Application Support/GSC Game World/S.T.A.L.K.E.R. - Shadow of Chernobyl/_appdata_/logs +``` + +Send the newest `.log` file with a crash report. It usually has more useful +information than the short stack trace in the error window. + +## Known visual detail + +The black area in the upper-right part of the main menu is in the original SoC +`ui_mainmenu.dds` image. It is not a missing texture. + +The general OpenXRay build procedure is in the +[Linux and macOS build guide](https://github.com/OpenXRay/xray-16/wiki/%5BEN%5D-How-to-build-and-setup-on-Linux-and-MacOS). diff --git a/misc/macos/make_app_bundle.sh b/misc/macos/make_app_bundle.sh new file mode 100755 index 00000000000..38d65a7f9ba --- /dev/null +++ b/misc/macos/make_app_bundle.sh @@ -0,0 +1,171 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -lt 2 || $# -gt 3 ]]; then + echo "Usage: $0 [soc|cs|cop]" + exit 1 +fi + +ARCH="$1" +CONFIGURATION="$2" +GAME_VARIANT="${3:-cop}" +SKIP_DMG="${OPENXRAY_SKIP_DMG:-0}" + +case "${GAME_VARIANT}" in + soc) + APP_NAME="OpenXRay SoC" + BUNDLE_IDENTIFIER="org.openxray.xray-16.soc" + DEFAULT_COMMAND_LINE="-soc" + ;; + cs) + APP_NAME="OpenXRay CS" + BUNDLE_IDENTIFIER="org.openxray.xray-16.cs" + DEFAULT_COMMAND_LINE="-cs" + ;; + cop) + APP_NAME="OpenXRay CoP" + BUNDLE_IDENTIFIER="org.openxray.xray-16.cop" + DEFAULT_COMMAND_LINE="" + ;; + *) + echo "Unsupported game variant: ${GAME_VARIANT}" + exit 1 + ;; +esac + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" +BIN_DIR="${ROOT_DIR}/bin/${ARCH}/${CONFIGURATION}" +ARTIFACTS_DIR="${ROOT_DIR}/build/artifacts" + +APP_DIR="${ARTIFACTS_DIR}/${APP_NAME}.app" +CONTENTS_DIR="${APP_DIR}/Contents" +MACOS_DIR="${CONTENTS_DIR}/MacOS" +LIBS_DIR="${CONTENTS_DIR}/libs" +RESOURCES_DIR="${CONTENTS_DIR}/Resources" +OXR_RES_DIR="${RESOURCES_DIR}/openxray" + +if [[ ! -x "${BIN_DIR}/xr_3da" ]]; then + echo "Cannot find executable: ${BIN_DIR}/xr_3da" + exit 1 +fi + +required_tools=(install_name_tool dylibbundler ditto) +if [[ "${SKIP_DMG}" != "1" ]]; then + required_tools+=(hdiutil) +fi + +for tool in "${required_tools[@]}"; do + if ! command -v "${tool}" >/dev/null 2>&1; then + echo "Required tool is missing: ${tool}" + exit 1 + fi +done + +mkdir -p "${ARTIFACTS_DIR}" +rm -rf "${APP_DIR}" +mkdir -p "${MACOS_DIR}" "${LIBS_DIR}" "${OXR_RES_DIR}" + +cat > "${CONTENTS_DIR}/Info.plist" < + + + + CFBundleName + ${APP_NAME} + CFBundleDisplayName + ${APP_NAME} + CFBundleIdentifier + ${BUNDLE_IDENTIFIER} + CFBundlePackageType + APPL + CFBundleExecutable + xr_3da + CFBundleVersion + 1 + CFBundleShortVersionString + 1.0 + LSApplicationCategoryType + public.app-category.games + NSHighResolutionCapable + + + NSDisablePersistence + + + +PLIST + +printf 'APPL????' > "${CONTENTS_DIR}/PkgInfo" + +cp "${BIN_DIR}/xr_3da" "${MACOS_DIR}/xr_3da" +chmod +x "${MACOS_DIR}/xr_3da" + +find "${BIN_DIR}" -maxdepth 1 -type f -name '*.dylib' -exec cp {} "${LIBS_DIR}/" \; + +# Bundle only open-source engine resources from this repository. +cp "${ROOT_DIR}/res/fsgame.ltx" "${OXR_RES_DIR}/fsgame.ltx" +if [[ "${GAME_VARIANT}" == "soc" ]]; then + # Shadow of Chernobyl archives use gamedata/config instead of gamedata/configs. + sed 's#configs\\#config\\#' "${OXR_RES_DIR}/fsgame.ltx" > "${OXR_RES_DIR}/fsgame.ltx.tmp" + mv "${OXR_RES_DIR}/fsgame.ltx.tmp" "${OXR_RES_DIR}/fsgame.ltx" +fi +cp -R "${ROOT_DIR}/res/gamedata" "${OXR_RES_DIR}/gamedata" +if [[ "${GAME_VARIANT}" == "soc" ]]; then + # The repository scripts target newer game data. Use the original SoC + # scripts from the game archives, except for the main-menu compatibility fix. + find "${OXR_RES_DIR}/gamedata/scripts" -type f ! -name 'ui_main_menu.script' -delete +fi +printf '%s\n' "${DEFAULT_COMMAND_LINE}" > "${OXR_RES_DIR}/default_command_line.txt" + +# Bundle non-system dynamic libraries (Homebrew deps etc.). +dylibbundler \ + -of -cd -b \ + -x "${MACOS_DIR}/xr_3da" \ + -d "${LIBS_DIR}" \ + -s "${BIN_DIR}" \ + -s "${LIBS_DIR}" + +reset_rpaths() { + local binary="$1" + + while install_name_tool -delete_rpath "@executable_path/../libs" "${binary}" >/dev/null 2>&1; do + : + done + while install_name_tool -delete_rpath "@executable_path/../libs/" "${binary}" >/dev/null 2>&1; do + : + done + + install_name_tool -add_rpath "@executable_path/../libs" "${binary}" + codesign --force --deep --preserve-metadata=entitlements,requirements,flags,runtime --sign - "${binary}" >/dev/null +} + +# dylibbundler may leave duplicate LC_RPATH commands, which dyld rejects on newer macOS. +reset_rpaths "${MACOS_DIR}/xr_3da" +for lib in "${LIBS_DIR}"/*.dylib; do + [[ -e "${lib}" ]] || continue + reset_rpaths "${lib}" +done + +codesign --force --deep --sign - "${APP_DIR}" >/dev/null + +APP_ZIP="${ARTIFACTS_DIR}/openxray-${GAME_VARIANT}-${CONFIGURATION}-${ARCH}.app.zip" +DMG_PATH="${ARTIFACTS_DIR}/openxray-${GAME_VARIANT}-${CONFIGURATION}-${ARCH}.dmg" +DMG_ROOT="${ARTIFACTS_DIR}/dmg-root" + +rm -f "${APP_ZIP}" "${DMG_PATH}" +ditto -c -k --sequesterRsrc --keepParent "${APP_DIR}" "${APP_ZIP}" + +echo "Created:" +echo " ${APP_ZIP}" +if [[ "${SKIP_DMG}" == "1" ]]; then + echo "Skipped DMG creation (OPENXRAY_SKIP_DMG=1)" +else + rm -rf "${DMG_ROOT}" + mkdir -p "${DMG_ROOT}" + ditto "${APP_DIR}" "${DMG_ROOT}/${APP_NAME}.app" + ln -s /Applications "${DMG_ROOT}/Applications" + hdiutil create -volname "${APP_NAME} ${CONFIGURATION} ${ARCH}" -srcfolder "${DMG_ROOT}" -format UDZO -ov "${DMG_PATH}" + rm -rf "${DMG_ROOT}" + echo " ${DMG_PATH}" +fi diff --git a/res/gamedata/scripts/ui_main_menu.script b/res/gamedata/scripts/ui_main_menu.script index 1c573a269bf..0b0596fecf0 100644 --- a/res/gamedata/scripts/ui_main_menu.script +++ b/res/gamedata/scripts/ui_main_menu.script @@ -12,7 +12,9 @@ function main_menu:__init() super() self.mbox_mode = 0 self:InitControls() self:InitCallBacks() - xr_s.on_main_menu_on() --' Distemper 03.2008 -- + if xr_s and xr_s.on_main_menu_on then + xr_s.on_main_menu_on() + end end function main_menu:__finalize() @@ -24,7 +26,14 @@ function main_menu:InitControls() local xml = CScriptXmlInit() xml:ParseFile ("ui_mm_main.xml") + local is_soc = IsShadowOfChernobylMode and IsShadowOfChernobylMode() + if is_soc then + xml:InitStatic ("back_movie", self) + end xml:InitStatic ("background", self) + if is_soc then + xml:InitStatic ("fire_movie", self) + end self.shniaga = xml:InitMMShniaga("shniaga_wnd",self); self.message_box = CUIMessageBoxEx() @@ -168,7 +177,9 @@ end function main_menu:OnButton_return_game() local console = get_console() console:execute("main_menu off") - xr_s.on_main_menu_off() --' Distemper 03.2008 -- + if xr_s and xr_s.on_main_menu_off then + xr_s.on_main_menu_off() + end end function main_menu:OnButton_new_novice_game() @@ -235,7 +246,12 @@ function main_menu:OnButton_options_clicked() self.opt_dlg.owner = self end - self.opt_dlg:SetCurrentValues() + if self.opt_dlg.SetCurrentValues ~= nil then + self.opt_dlg:SetCurrentValues() + else + -- Shadow of Chernobyl names this operation UpdateControls. + self.opt_dlg:UpdateControls() + end self.opt_dlg:ShowDialog(true) self:HideDialog() self:Show(false) @@ -261,7 +277,8 @@ function main_menu:OnButton_multiplayer_clicked() -- assert(self.gs_profile) if not(self.mp_dlg) then - self.mp_dlg = ui_mp_main.mp_main(self.gs_profile:online()) + local online = self.gs_profile and self.gs_profile:online() or false + self.mp_dlg = ui_mp_main.mp_main(online) self.mp_dlg.owner = self self.mp_dlg:OnRadio_NetChanged() if (self.mp_dlg.online) then @@ -364,4 +381,4 @@ end function main_menu:OnMenuReloaded() self:OnButton_options_clicked() self.opt_dlg:OnMenuReloaded() -end \ No newline at end of file +end diff --git a/src/Layers/xrRender/ShaderResourceTraits.h b/src/Layers/xrRender/ShaderResourceTraits.h index e4425ea6bab..39a6c137481 100644 --- a/src/Layers/xrRender/ShaderResourceTraits.h +++ b/src/Layers/xrRender/ShaderResourceTraits.h @@ -159,7 +159,9 @@ static GLuint GLGeneratePipeline(pcstr name, GLuint ps, GLuint vs, GLuint gs) CHK_GL(glUseProgramStages(pp, GL_FRAGMENT_SHADER_BIT, ps)); CHK_GL(glUseProgramStages(pp, GL_VERTEX_SHADER_BIT, vs)); CHK_GL(glUseProgramStages(pp, GL_GEOMETRY_SHADER_BIT, gs)); +#ifndef XR_PLATFORM_APPLE CHK_GL(glValidateProgramPipeline(pp)); +#endif return pp; } #endif diff --git a/src/Layers/xrRenderGL/glSH_Texture.cpp b/src/Layers/xrRenderGL/glSH_Texture.cpp index d7393f78ec7..f20ca081e52 100644 --- a/src/Layers/xrRenderGL/glSH_Texture.cpp +++ b/src/Layers/xrRenderGL/glSH_Texture.cpp @@ -14,6 +14,16 @@ namespace xray::render::RENDER_NAMESPACE { +namespace +{ +void ClearGLErrors() +{ + while (glGetError() != GL_NO_ERROR) + { + } +} +} // namespace + void resptrcode_texture::create(LPCSTR _name) { _set(RImplementation.Resources->_CreateTexture(_name)); @@ -196,6 +206,8 @@ void CTexture::Load() u32 _w = pTheora->Width(false); u32 _h = pTheora->Height(false); + ClearGLErrors(); + glGenBuffers(1, &pBuffer); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pBuffer); CHK_GL(glBufferData(GL_PIXEL_UNPACK_BUFFER, flags.MemoryUsage, nullptr, GL_STREAM_DRAW)); @@ -203,7 +215,9 @@ void CTexture::Load() glGenTextures(1, &pTexture); glBindTexture(GL_TEXTURE_2D, pTexture); - CHK_GL(glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, _w, _h)); + CHK_GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0)); + CHK_GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0)); + CHK_GL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, _w, _h, 0, GL_BGRA, GL_UNSIGNED_BYTE, nullptr)); pSurface = pTexture; desc = GL_TEXTURE_2D; diff --git a/src/Layers/xrRenderGL/glr_screenshot.cpp b/src/Layers/xrRenderGL/glr_screenshot.cpp index d8af50e0995..7fa24a45ee2 100644 --- a/src/Layers/xrRenderGL/glr_screenshot.cpp +++ b/src/Layers/xrRenderGL/glr_screenshot.cpp @@ -4,6 +4,8 @@ #include "xrCore/Media/Image.hpp" #include "xrEngine/xrImage_Resampler.h" +#include + namespace xray::render::RENDER_NAMESPACE { using namespace XRay::Media; @@ -13,7 +15,6 @@ using namespace XRay::Media; #define SM_FOR_SEND_WIDTH 640 #define SM_FOR_SEND_HEIGHT 480 -// XXX: Provide full implementation void CRender::Screenshot(ScreenshotMode mode /*= SM_NORMAL*/, pcstr name /*= nullptr*/) { switch (mode) @@ -44,8 +45,47 @@ void CRender::Screenshot(ScreenshotMode mode /*= SM_NORMAL*/, pcstr name /*= nul } case SM_FOR_GAMESAVE: - // XXX: Implement + { + VERIFY(name); + + xr_vector pixels(Device.dwWidth * Device.dwHeight); + glReadPixels(0, 0, Device.dwWidth, Device.dwHeight, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); + + // OpenGL returns the bottom row first. DDS textures use the top row first. + const size_t rowSize = Device.dwWidth; + for (u32 y = 0; y < Device.dwHeight / 2; ++y) + { + auto top = pixels.begin() + y * rowSize; + auto bottom = pixels.begin() + (Device.dwHeight - y - 1) * rowSize; + std::swap_ranges(top, top + rowSize, bottom); + } + + xr_vector resized(GAMESAVE_SIZE * GAMESAVE_SIZE); + imf_Process(resized.data(), GAMESAVE_SIZE, GAMESAVE_SIZE, pixels.data(), + Device.dwWidth, Device.dwHeight, imf_box); + + gli::texture2d texture(gli::FORMAT_RGBA8_UNORM_PACK8, + gli::texture2d::extent_type(GAMESAVE_SIZE, GAMESAVE_SIZE), 1); + std::memcpy(texture.data(), resized.data(), resized.size() * sizeof(resized.front())); + + std::vector encoded; + if (!gli::save_dds(texture, encoded)) + { + Log("! Failed to encode the game-save screenshot."); + break; + } + + IWriter* fs = FS.w_open(name); + if (!fs) + { + Msg("! Failed to open the game-save screenshot file: %s", name); + break; + } + + fs->w(encoded.data(), encoded.size()); + FS.w_close(fs); break; + } default: VERIFY(!"CRender::Screenshot. This screenshot type is not supported for OGL."); diff --git a/src/xrCore/xrDebug.cpp b/src/xrCore/xrDebug.cpp index 1edcda8f26e..ca598b22bcb 100644 --- a/src/xrCore/xrDebug.cpp +++ b/src/xrCore/xrDebug.cpp @@ -25,12 +25,16 @@ # include "Debug/dxerr.h" #endif -#if defined(XR_PLATFORM_LINUX) || defined(XR_PLATFORM_APPLE) || defined(XR_PLATFORM_BSD) +#if defined(XR_PLATFORM_APPLE) +# include +# include +# include +#elif defined(XR_PLATFORM_LINUX) || defined(XR_PLATFORM_BSD) # if __has_include() # include # define PTRACE_AVAILABLE -# if defined(XR_PLATFORM_APPLE) || defined(XR_PLATFORM_BSD) +# if defined(XR_PLATFORM_BSD) # define PTRACE_TRACEME PT_TRACE_ME # define PTRACE_DETACH PT_DETACH # endif @@ -477,6 +481,15 @@ bool xrDebug::DebuggerIsPresent() { #ifdef XR_PLATFORM_WINDOWS return IsDebuggerPresent(); +#elif defined(XR_PLATFORM_APPLE) + int mib[] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid() }; + kinfo_proc processInfo{}; + size_t processInfoSize = sizeof(processInfo); + + if (sysctl(mib, std::size(mib), &processInfo, &processInfoSize, nullptr, 0) != 0) + return false; + + return (processInfo.kp_proc.p_flag & P_TRACED) != 0; #elif defined(PTRACE_AVAILABLE) if (ptrace(PTRACE_TRACEME, 0, 0, 0) == -1) return true; diff --git a/src/xrEngine/CMakeLists.txt b/src/xrEngine/CMakeLists.txt index 6ee391fa94c..540fa3dd9cc 100644 --- a/src/xrEngine/CMakeLists.txt +++ b/src/xrEngine/CMakeLists.txt @@ -407,6 +407,16 @@ target_sources(xrEngine TODO.txt ) +if (APPLE) + target_sources_grouped( + TARGET xrEngine + NAME "Platform\\macOS" + FILES + macos/GameDataResolver.cpp + macos/GameDataResolver.h + ) +endif() + target_include_directories(xrEngine PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}" diff --git a/src/xrEngine/Device_mode.cpp b/src/xrEngine/Device_mode.cpp index 7ef06408f76..3797e8f7e0b 100644 --- a/src/xrEngine/Device_mode.cpp +++ b/src/xrEngine/Device_mode.cpp @@ -6,6 +6,31 @@ xr_vector vid_monitor_token; xr_map> vid_mode_token; +namespace +{ +void FitWindowedResolutionToDisplay(u32& width, u32& height, const u32 monitor, SDL_Window* window) +{ + SDL_Rect usable; + if (SDL_GetDisplayUsableBounds(static_cast(monitor), &usable) != 0 || usable.w <= 0 || usable.h <= 0) + return; + + int top = 0; + int left = 0; + int bottom = 0; + int right = 0; + SDL_GetWindowBordersSize(window, &top, &left, &bottom, &right); + + const u32 maxWidth = static_cast(std::max(256, usable.w - left - right)); + const u32 maxHeight = static_cast(std::max(192, usable.h - top - bottom)); + if (width <= maxWidth && height <= maxHeight) + return; + + const float scale = std::min(float(maxWidth) / float(width), float(maxHeight) / float(height)); + width = std::max(256, iFloor(float(width) * scale)); + height = std::max(192, iFloor(float(height) * scale)); +} +} // namespace + void FillResolutionsForMonitor(const int monitorID) { const int modeCount = SDL_GetNumDisplayModes(monitorID); @@ -111,6 +136,17 @@ void CRenderDevice::UpdateWindowProps() ZoneScoped; const bool windowed = psDeviceMode.WindowStyle != rsFullscreen; + + // SDL window sizes describe the client area. Apply native borders first + // so SelectResolution can also reserve space for the macOS title bar. + if (psDeviceMode.WindowStyle == rsWindowed) + { + SDL_SetWindowFullscreen(m_sdlWnd, SDL_DISABLE); + SDL_SetWindowBordered(m_sdlWnd, SDL_TRUE); + SDL_SetWindowResizable(m_sdlWnd, SDL_TRUE); + SDL_PumpEvents(); + } + SelectResolution(windowed); // Changing monitor, unset fullscreen for the previous monitor @@ -161,8 +197,11 @@ void CRenderDevice::UpdateWindowProps() ImGuiIO& io = ImGui::GetIO(); - io.DisplaySize = { static_cast(psDeviceMode.Width), static_cast(psDeviceMode.Height) }; - io.DisplayFramebufferScale = ImVec2{ float(dwWidth / m_rcWindowClient.w), float(dwHeight / m_rcWindowClient.h) }; + io.DisplaySize = { static_cast(m_rcWindowClient.w), static_cast(m_rcWindowClient.h) }; + io.DisplayFramebufferScale = ImVec2{ + m_rcWindowClient.w ? float(dwWidth) / float(m_rcWindowClient.w) : 1.0f, + m_rcWindowClient.h ? float(dwHeight) / float(m_rcWindowClient.h) : 1.0f + }; } void CRenderDevice::UpdateWindowRects() @@ -199,6 +238,10 @@ void CRenderDevice::SelectResolution(const bool windowed) psDeviceMode.Height = current.h; psDeviceMode.RefreshRate = current.refresh_rate; } + else if (windowed && psDeviceMode.WindowStyle == rsWindowed) + { + FitWindowedResolutionToDisplay(psDeviceMode.Width, psDeviceMode.Height, psDeviceMode.Monitor, m_sdlWnd); + } else if (!windowed) // check if safe for fullscreen { string256 buf; diff --git a/src/xrEngine/macos/GameDataResolver.cpp b/src/xrEngine/macos/GameDataResolver.cpp new file mode 100644 index 00000000000..2fcce90cd6d --- /dev/null +++ b/src/xrEngine/macos/GameDataResolver.cpp @@ -0,0 +1,581 @@ +#include "stdafx.h" +#pragma hdrstop + +#if defined(XR_PLATFORM_APPLE) +#include "GameDataResolver.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +constexpr pcstr CompanyName = "GSC Game World"; +constexpr pcstr SavedPathFileName = "openxray_gamedata_path.txt"; +constexpr pcstr ChooseAnotherItem = "Choose another folder..."; +constexpr pcstr QuitItem = "Quit"; + +struct GameInfo +{ + std::string appSupportName; + std::string displayName; + std::vector steamNames; + std::vector gogNames; + bool supportsFlatArchives; + std::string requiredDataDescription; +}; + +bool HasCommandLineOption(pcstr commandLine, pcstr option) +{ + return commandLine && strstr(commandLine, option); +} + +std::string TrimLineEnd(std::string value) +{ + while (!value.empty() && (value.back() == '\n' || value.back() == '\r')) + value.pop_back(); + return value; +} + +std::string EnsureTrailingSlash(std::string path) +{ + if (!path.empty() && path.back() != '/') + path.push_back('/'); + return path; +} + +std::string RemoveTrailingSlash(std::string path) +{ + while (path.size() > 1 && path.back() == '/') + path.pop_back(); + return path; +} + +std::string JoinPath(const std::string& left, pcstr right) +{ + if (left.empty()) + return right ? right : ""; + + std::string result = left; + if (result.back() != '/') + result.push_back('/'); + result += right; + return result; +} + +std::string ExpandHomePath(pcstr suffix) +{ + const char* home = SDL_getenv("HOME"); + if (!home || !home[0]) + return {}; + + std::string path = home; + if (suffix && suffix[0]) + { + if (path.back() != '/' && suffix[0] != '/') + path.push_back('/'); + path += suffix; + } + return path; +} + +std::string NormalizeExistingPath(const std::string& path) +{ + char resolved[PATH_MAX]; + if (realpath(path.c_str(), resolved)) + return resolved; + return RemoveTrailingSlash(path); +} + +bool IsDirectory(const std::string& path) +{ + struct stat st; + return stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode); +} + +bool IsFile(const std::string& path) +{ + struct stat st; + return stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode); +} + +bool IsSymlink(const std::string& path) +{ + struct stat st; + return lstat(path.c_str(), &st) == 0 && S_ISLNK(st.st_mode); +} + +bool PathExistsNoFollow(const std::string& path) +{ + struct stat st; + return lstat(path.c_str(), &st) == 0; +} + +bool HasDirectoryGameData(const std::string& root) +{ + if (root.empty()) + return false; + + return IsDirectory(JoinPath(root, "levels")) && + IsDirectory(JoinPath(root, "resources")) && + IsDirectory(JoinPath(root, "localization")); +} + +bool HasSoCFlatArchives(const std::string& root) +{ + static constexpr pcstr RequiredArchives[] = { + "gamedata.db0", "gamedata.db1", "gamedata.db2", "gamedata.db3", "gamedata.db4", + "gamedata.db5", "gamedata.db6", "gamedata.db7", "gamedata.db8", "gamedata.db9", + "gamedata.dba", "gamedata.dbb", "gamedata.dbc" + }; + + if (root.empty()) + return false; + + for (pcstr archiveName : RequiredArchives) + { + if (!IsFile(JoinPath(root, archiveName))) + return false; + } + return true; +} + +bool HasRequiredGameData(const std::string& root, const GameInfo& gameInfo) +{ + return HasDirectoryGameData(root) || (gameInfo.supportsFlatArchives && HasSoCFlatArchives(root)); +} + +bool HasRuntimeLayout(const std::string& root, const GameInfo& gameInfo) +{ + return HasRequiredGameData(root, gameInfo) && + IsFile(JoinPath(root, "fsgame.ltx")) && + IsDirectory(JoinPath(root, "gamedata")); +} + +GameInfo GetGameInfo(pcstr commandLine) +{ + if (HasCommandLineOption(commandLine, "-shoc") || HasCommandLineOption(commandLine, "-soc")) + { + return { + "S.T.A.L.K.E.R. - Shadow of Chernobyl", + "S.T.A.L.K.E.R.: Shadow of Chernobyl", + { "STALKER Shadow of Chernobyl", "Stalker Shadow of Chernobyl", "S.T.A.L.K.E.R. Shadow of Chernobyl" }, + { "S.T.A.L.K.E.R. - Shadow of Chernobyl" }, + true, + "the gamedata.db* archives" + }; + } + + if (HasCommandLineOption(commandLine, "-cs")) + { + return { + "S.T.A.L.K.E.R. - Clear Sky", + "S.T.A.L.K.E.R.: Clear Sky", + { "STALKER Clear Sky", "Stalker Clear Sky", "S.T.A.L.K.E.R. Clear Sky" }, + { "S.T.A.L.K.E.R. - Clear Sky" }, + false, + "levels, resources, and localization" + }; + } + + return { + "S.T.A.L.K.E.R. - Call of Pripyat", + "S.T.A.L.K.E.R.: Call of Pripyat", + { "STALKER Call of Pripyat", "Stalker Call of Pripyat", "S.T.A.L.K.E.R. Call of Pripyat" }, + { "S.T.A.L.K.E.R. - Call of Pripyat" }, + false, + "levels, resources, and localization" + }; +} + +std::string GetPrefPath(const GameInfo& gameInfo) +{ + char* prefPath = SDL_GetPrefPath(CompanyName, gameInfo.appSupportName.c_str()); + if (!prefPath) + return {}; + + std::string result = EnsureTrailingSlash(prefPath); + SDL_free(prefPath); + return result; +} + +bool GetBundleResourcesRoot(std::string& resourcesRoot) +{ + char* basePathRaw = SDL_GetBasePath(); + if (!basePathRaw) + return false; + + std::string basePath = EnsureTrailingSlash(basePathRaw); + SDL_free(basePathRaw); + + std::string candidate = NormalizeExistingPath(JoinPath(basePath, "../Resources/openxray")); + if (!IsFile(JoinPath(candidate, "fsgame.ltx")) || !IsDirectory(JoinPath(candidate, "gamedata"))) + return false; + + resourcesRoot = candidate; + return true; +} + +std::string GetBundleNeighborRoot() +{ + char* basePathRaw = SDL_GetBasePath(); + if (!basePathRaw) + return {}; + + std::string basePath = EnsureTrailingSlash(basePathRaw); + SDL_free(basePathRaw); + return NormalizeExistingPath(JoinPath(basePath, "../../..")); +} + +std::string GetSavedRoot(const std::string& prefPath) +{ + const std::string pathFile = JoinPath(prefPath, SavedPathFileName); + FILE* file = fopen(pathFile.c_str(), "r"); + if (!file) + return {}; + + char buffer[PATH_MAX]; + const bool hasValue = fgets(buffer, sizeof(buffer), file) != nullptr; + fclose(file); + + if (!hasValue) + return {}; + + return RemoveTrailingSlash(TrimLineEnd(buffer)); +} + +void SaveRoot(const std::string& prefPath, const std::string& root) +{ + const std::string pathFile = JoinPath(prefPath, SavedPathFileName); + FILE* file = fopen(pathFile.c_str(), "w"); + if (!file) + return; + + fprintf(file, "%s\n", root.c_str()); + fclose(file); +} + +void AddCandidate(std::vector& candidates, const std::string& path, const GameInfo& gameInfo) +{ + if (!HasRequiredGameData(path, gameInfo)) + return; + + const std::string normalized = NormalizeExistingPath(path); + for (const auto& candidate : candidates) + { + if (candidate == normalized) + return; + } + candidates.emplace_back(normalized); +} + +std::vector DiscoverCandidates(const GameInfo& gameInfo, const std::string& prefPath) +{ + std::vector candidates; + + AddCandidate(candidates, prefPath, gameInfo); + AddCandidate(candidates, + ExpandHomePath(JoinPath(".local/share/GSC Game World", gameInfo.appSupportName.c_str()).c_str()), gameInfo); + + for (const auto& steamName : gameInfo.steamNames) + { + AddCandidate(candidates, + ExpandHomePath(JoinPath("Library/Application Support/Steam/steamapps/common", steamName.c_str()).c_str()), gameInfo); + AddCandidate(candidates, + ExpandHomePath(JoinPath(".local/share/Steam/steamapps/common", steamName.c_str()).c_str()), gameInfo); + AddCandidate(candidates, + ExpandHomePath(JoinPath(".steam/steam/steamapps/common", steamName.c_str()).c_str()), gameInfo); + } + + for (const auto& gogName : gameInfo.gogNames) + { + AddCandidate(candidates, ExpandHomePath(JoinPath("GOG Games", gogName.c_str()).c_str()), gameInfo); + AddCandidate(candidates, ExpandHomePath(JoinPath("Applications", gogName.c_str()).c_str()), gameInfo); + AddCandidate(candidates, JoinPath("/Applications", gogName.c_str()), gameInfo); + } + + AddCandidate(candidates, GetBundleNeighborRoot(), gameInfo); + return candidates; +} + +std::string EscapeAppleScriptString(const std::string& value) +{ + std::string result; + result.reserve(value.size() + 2); + result.push_back('"'); + for (const char c : value) + { + if (c == '\\' || c == '"') + result.push_back('\\'); + if (c == '\n' || c == '\r') + result.push_back(' '); + else + result.push_back(c); + } + result.push_back('"'); + return result; +} + +bool RunAppleScript(const std::string& script, std::string& output) +{ + char scriptPath[] = "/tmp/openxray_osascript_XXXXXX"; + const int fd = mkstemp(scriptPath); + if (fd == -1) + return false; + + FILE* file = fdopen(fd, "w"); + if (!file) + { + close(fd); + xr_unlink(scriptPath); + return false; + } + + fwrite(script.data(), 1, script.size(), file); + fclose(file); + + const std::string command = std::string("/usr/bin/osascript ") + scriptPath + " 2>/dev/null"; + FILE* pipe = popen(command.c_str(), "r"); + if (!pipe) + { + xr_unlink(scriptPath); + return false; + } + + char buffer[1024]; + output.clear(); + while (fgets(buffer, sizeof(buffer), pipe)) + output += buffer; + + const int status = pclose(pipe); + xr_unlink(scriptPath); + output = TrimLineEnd(output); + return status == 0 && !output.empty(); +} + +void ShowAppleScriptAlert(const std::string& message) +{ + std::string ignored; + RunAppleScript( + "display alert \"OpenXRay\" message " + EscapeAppleScriptString(message) + " as warning\n", + ignored); +} + +bool ChooseFolder(const GameInfo& gameInfo, std::string& selectedRoot) +{ + const std::string prompt = "Select the " + gameInfo.displayName + + " directory that contains " + gameInfo.requiredDataDescription + "."; + std::string output; + if (!RunAppleScript("POSIX path of (choose folder with prompt " + EscapeAppleScriptString(prompt) + ")\n", output)) + return false; + + selectedRoot = RemoveTrailingSlash(output); + return true; +} + +bool ChooseRootFromDialog(const GameInfo& gameInfo, const std::vector& candidates, std::string& selectedRoot) +{ + std::vector choices = candidates; + choices.emplace_back(ChooseAnotherItem); + choices.emplace_back(QuitItem); + + std::string choicesLiteral = "{"; + for (size_t i = 0; i < choices.size(); ++i) + { + if (i != 0) + choicesLiteral += ", "; + choicesLiteral += EscapeAppleScriptString(choices[i]); + } + choicesLiteral += "}"; + + const std::string defaultItem = candidates.empty() ? ChooseAnotherItem : candidates.front(); + const std::string prompt = candidates.empty() + ? "OpenXRay could not find game data automatically. Choose the " + gameInfo.displayName + " directory." + : "Choose the " + gameInfo.displayName + " game data directory."; + + const std::string script = + "set openxrayChoices to " + choicesLiteral + "\n" + "set openxraySelection to choose from list openxrayChoices with title \"OpenXRay\" with prompt " + + EscapeAppleScriptString(prompt) + " default items {" + EscapeAppleScriptString(defaultItem) + + "} OK button name \"Use Selected\" cancel button name \"Quit\"\n" + "if openxraySelection is false then\n" + " return " + EscapeAppleScriptString(QuitItem) + "\n" + "end if\n" + "return item 1 of openxraySelection\n"; + + std::string output; + if (!RunAppleScript(script, output) || output == QuitItem) + return false; + + if (output == ChooseAnotherItem) + return ChooseFolder(gameInfo, selectedRoot); + + selectedRoot = RemoveTrailingSlash(output); + return true; +} + +bool MoveExistingAside(const std::string& path) +{ + if (!PathExistsNoFollow(path)) + return true; + + for (u32 i = 0; i < 100; ++i) + { + std::string backupPath = path + ".openxray-backup"; + if (i != 0) + backupPath += std::to_string(i); + + if (PathExistsNoFollow(backupPath)) + continue; + + return rename(path.c_str(), backupPath.c_str()) == 0; + } + + return false; +} + +bool EnsureManagedSymlink(const std::string& source, const std::string& linkPath) +{ + if (source.empty() || linkPath.empty()) + return false; + + if (RemoveTrailingSlash(source) == RemoveTrailingSlash(linkPath)) + return true; + + if (IsSymlink(linkPath)) + xr_unlink(linkPath.c_str()); + else if (!MoveExistingAside(linkPath)) + return false; + + return symlink(source.c_str(), linkPath.c_str()) == 0; +} + +void LinkDirectoryIfPresent(const std::string& prefPath, const std::string& gameRoot, pcstr dirName) +{ + const std::string source = JoinPath(gameRoot, dirName); + if (!IsDirectory(source)) + return; + + const std::string linkPath = JoinPath(prefPath, dirName); + if (RemoveTrailingSlash(source) == RemoveTrailingSlash(linkPath)) + return; + + EnsureManagedSymlink(source, linkPath); +} + +void LinkFlatArchivesIfPresent(const std::string& prefPath, const std::string& gameRoot) +{ + DIR* directory = opendir(gameRoot.c_str()); + if (!directory) + return; + + while (const dirent* entry = readdir(directory)) + { + if (strncmp(entry->d_name, "gamedata.db", 11) != 0) + continue; + + const std::string source = JoinPath(gameRoot, entry->d_name); + if (IsFile(source)) + EnsureManagedSymlink(source, JoinPath(prefPath, entry->d_name)); + } + closedir(directory); +} + +bool ApplyRuntimeLayout(const std::string& prefPath, const std::string& bundleResourcesRoot, + const std::string& gameRoot, const GameInfo& gameInfo) +{ + if (!HasRequiredGameData(gameRoot, gameInfo)) + return false; + + EnsureManagedSymlink(JoinPath(bundleResourcesRoot, "fsgame.ltx"), JoinPath(prefPath, "fsgame.ltx")); + EnsureManagedSymlink(JoinPath(bundleResourcesRoot, "gamedata"), JoinPath(prefPath, "gamedata")); + + LinkDirectoryIfPresent(prefPath, gameRoot, "levels"); + LinkDirectoryIfPresent(prefPath, gameRoot, "resources"); + LinkDirectoryIfPresent(prefPath, gameRoot, "localization"); + LinkDirectoryIfPresent(prefPath, gameRoot, "mp"); + LinkDirectoryIfPresent(prefPath, gameRoot, "patches"); + if (gameInfo.supportsFlatArchives) + LinkFlatArchivesIfPresent(prefPath, gameRoot); + + return HasRuntimeLayout(prefPath, gameInfo); +} +} // namespace + +void ResolveMacOSGameDataPath(pcstr commandLine) +{ + if (HasCommandLineOption(commandLine, "-fsltx ")) + return; + + std::string bundleResourcesRoot; + if (!GetBundleResourcesRoot(bundleResourcesRoot)) + return; + + const GameInfo gameInfo = GetGameInfo(commandLine); + const std::string prefPath = GetPrefPath(gameInfo); + if (prefPath.empty()) + return; + + const bool forceSelection = HasCommandLineOption(commandLine, "-select_gamedata") || + HasCommandLineOption(commandLine, "-reset_gamedata_path"); + + const std::string savedRoot = GetSavedRoot(prefPath); + if (!forceSelection && HasRequiredGameData(savedRoot, gameInfo) && + ApplyRuntimeLayout(prefPath, bundleResourcesRoot, savedRoot, gameInfo)) + return; + + const std::vector candidates = DiscoverCandidates(gameInfo, prefPath); + std::string selectedRoot; + + while (ChooseRootFromDialog(gameInfo, candidates, selectedRoot)) + { + if (!HasRequiredGameData(selectedRoot, gameInfo)) + { + ShowAppleScriptAlert( + "The selected directory does not contain " + gameInfo.requiredDataDescription + ". " + "Please choose the root directory of a licensed game installation."); + continue; + } + + if (ApplyRuntimeLayout(prefPath, bundleResourcesRoot, selectedRoot, gameInfo)) + { + SaveRoot(prefPath, NormalizeExistingPath(selectedRoot)); + return; + } + + ShowAppleScriptAlert( + "OpenXRay could not prepare the selected directory in Application Support. " + "Check file permissions and try again."); + } + + if (HasRuntimeLayout(prefPath, gameInfo)) + return; + + if (!HasRequiredGameData(prefPath, gameInfo)) + { + const std::string message = "OpenXRay could not find required game files.\nChoose a directory that contains " + + gameInfo.requiredDataDescription + "."; + SDL_ShowSimpleMessageBox( + SDL_MESSAGEBOX_WARNING, + "OpenXRay: game files are required", + message.c_str(), + nullptr); + } + else + { + SDL_ShowSimpleMessageBox( + SDL_MESSAGEBOX_WARNING, + "OpenXRay: setup is incomplete", + "OpenXRay could not prepare bundled engine resources in Application Support.", + nullptr); + } + + std::exit(EXIT_SUCCESS); +} +#endif diff --git a/src/xrEngine/macos/GameDataResolver.h b/src/xrEngine/macos/GameDataResolver.h new file mode 100644 index 00000000000..b3147939cd3 --- /dev/null +++ b/src/xrEngine/macos/GameDataResolver.h @@ -0,0 +1,5 @@ +#pragma once + +#if defined(XR_PLATFORM_APPLE) +void ResolveMacOSGameDataPath(pcstr commandLine); +#endif diff --git a/src/xrEngine/x_ray.cpp b/src/xrEngine/x_ray.cpp index 20d2e576e1b..6e0fd268fde 100644 --- a/src/xrEngine/x_ray.cpp +++ b/src/xrEngine/x_ray.cpp @@ -18,6 +18,10 @@ #include "LightAnimLibrary.h" #include "XR_IOConsole.h" +#if defined(XR_PLATFORM_APPLE) +#include "macos/GameDataResolver.h" +#endif + #if defined(XR_PLATFORM_WINDOWS) #include "AccessibilityShortcuts.hpp" #include "Text_Console.h" @@ -253,6 +257,10 @@ CApplication::CApplication(pcstr commandLine, GameModule* game, const std::array sscanf(strstr(commandLine, fsltx) + sz, "%[^ ] ", fsgame); } +#if defined(XR_PLATFORM_APPLE) + ResolveMacOSGameDataPath(commandLine); +#endif + Core.Initialize("OpenXRay", commandLine, true, *fsgame ? fsgame : nullptr); InitSettings(); diff --git a/src/xrGame/CMakeLists.txt b/src/xrGame/CMakeLists.txt index 4e66aec4146..4240cc02196 100644 --- a/src/xrGame/CMakeLists.txt +++ b/src/xrGame/CMakeLists.txt @@ -2131,6 +2131,30 @@ target_sources(xrGame PRIVATE ui/UIAchievements.h ui/UIActorInfo.cpp ui/UIActorInfo.h + ui/UIDiaryWnd2.cpp + ui/UIDiaryWnd.h + ui/UIEncyclopediaArticleWnd.cpp + ui/UIEncyclopediaArticleWnd.h + ui/UIEncyclopediaWnd.cpp + ui/UIEncyclopediaWnd.h + ui/UIEventsWnd.cpp + ui/UIEventsWnd.h + ui/UINewsWnd.cpp + ui/UINewsWnd.h + ui/UIPdaAux.cpp + ui/UIPdaAux.h + ui/UIPdaContactsWnd.cpp + ui/UIPdaContactsWnd.h + ui/UIPdaListItem.cpp + ui/UIPdaListItem.h + ui/UIStalkersRankingWnd.cpp + ui/UIStalkersRankingWnd.h + ui/UITaskDescrWnd.cpp + ui/UITaskDescrWnd.h + ui/UITaskItem.cpp + ui/UITaskItem.h + ui/UITreeViewItem.cpp + ui/UITreeViewItem.h ui/UIActorMenu_action.cpp ui/UIActorMenu.cpp ui/UIActorMenuDeadBodySearch.cpp @@ -2522,6 +2546,22 @@ set_target_properties(xrGame PROPERTIES UNITY_BUILD_BATCH_SIZE 50 ) +set_source_files_properties( + ui/UIDiaryWnd2.cpp + ui/UIEncyclopediaArticleWnd.cpp + ui/UIEncyclopediaWnd.cpp + ui/UIEventsWnd.cpp + ui/UINewsWnd.cpp + ui/UIPdaAux.cpp + ui/UIPdaContactsWnd.cpp + ui/UIPdaListItem.cpp + ui/UIStalkersRankingWnd.cpp + ui/UITaskDescrWnd.cpp + ui/UITaskItem.cpp + ui/UITreeViewItem.cpp + PROPERTIES SKIP_UNITY_BUILD_INCLUSION ON +) + target_precompile_headers(xrGame PRIVATE $<$:StdAfx.h> diff --git a/src/xrGame/GameTask.cpp b/src/xrGame/GameTask.cpp index 8441bb9f76c..15870bacf0b 100644 --- a/src/xrGame/GameTask.cpp +++ b/src/xrGame/GameTask.cpp @@ -131,8 +131,11 @@ void CGameTask::Load(const TASK_ID& id) 0 != xr_stricmp(objective.m_icon_texture_name.c_str(), "ui\\ui_icons_task")) { objective.m_icon_rect = CUITextureMaster::GetTextureRect(objective.m_icon_texture_name.c_str()); - objective.m_icon_rect.rb.sub(objective.m_icon_rect.rb, objective.m_icon_rect.lt); - objective.m_icon_texture_name = CUITextureMaster::GetTextureFileName(objective.m_icon_texture_name.c_str()); + if (!ShadowOfChernobylMode) + { + objective.m_icon_rect.rb.sub(objective.m_icon_rect.rb, objective.m_icon_rect.lt); + objective.m_icon_texture_name = CUITextureMaster::GetTextureFileName(objective.m_icon_texture_name.c_str()); + } } else if (objective.m_icon_texture_name.size()) { @@ -365,6 +368,13 @@ void CGameTask::OnArrived() FillEncyclopedia(); CreateMapLocation(false); + for (SGameTaskObjective& objective : m_Objectives) + { + // SoC creates hidden objective locations only after the prior + // objective is complete. UpdateActiveTask performs that step. + if (!ShadowOfChernobylMode || objective.m_def_location_enabled) + objective.CreateMapLocation(false); + } } void CGameTask::FillEncyclopedia() const @@ -399,32 +409,53 @@ void SGameTaskObjective::CreateMapLocation(bool on_load) return; } + string512 ownerIdBuffer; + if (m_idx == ROOT_TASK_OBJECTIVE) + xr_strcpy(ownerIdBuffer, m_parent->m_ID.c_str()); + else + xr_sprintf(ownerIdBuffer, "%s/%u", m_parent->m_ID.c_str(), m_idx); + const shared_str ownerId = ownerIdBuffer; + + bool created = false; if (on_load) { xr_vector res; Level().MapManager().GetMapLocations(m_map_location, m_map_object_id, res); - xr_vector::iterator it = res.begin(); - xr_vector::iterator it_e = res.end(); - for (; it != it_e; ++it) + for (CMapLocation* ml : res) { - CMapLocation* ml = *it; - if (ml->m_owner_task_id == m_parent->m_ID) + if (ml->m_owner_task_id == ownerId) { m_linked_map_location = ml; break; } } + + // Older saves used only the task ID. Let the first matching objective + // claim that location. Other objectives then get separate locations. + if (!m_linked_map_location) + { + for (CMapLocation* ml : res) + { + if (ml->m_owner_task_id == m_parent->m_ID) + { + m_linked_map_location = ml; + m_linked_map_location->m_owner_task_id = ownerId; + break; + } + } + } //. m_linked_map_location = Level().MapManager().GetMapLocation(m_map_location, m_map_object_id); } - else + if (!m_linked_map_location) { m_linked_map_location = Level().MapManager().AddMapLocation(m_map_location, m_map_object_id); - m_linked_map_location->m_owner_task_id = m_parent->m_ID; + m_linked_map_location->m_owner_task_id = ownerId; + created = true; } VERIFY(m_linked_map_location); - if (!on_load) + if (!on_load || created) { if (m_map_hint.size()) { @@ -432,6 +463,13 @@ void SGameTaskObjective::CreateMapLocation(bool on_load) } m_linked_map_location->DisablePointer(); m_linked_map_location->SetSerializable(true); + if (ShadowOfChernobylMode) + { + if (m_def_location_enabled) + m_linked_map_location->EnableSpot(); + else + m_linked_map_location->DisableSpot(); + } } if (m_linked_map_location->complex_spot()) @@ -621,10 +659,26 @@ void CGameTask::save(IWriter& stream) void CGameTask::load(IReader& stream) { load_data(m_ID, stream); + + // Saved tasks are default-constructed, so load their current XML data + // after the task ID becomes available. This repairs missing legacy icon + // data in old SoC saves while saved state and progress still load below. + if (ShadowOfChernobylMode) + Load(m_ID); + + const shared_str configuredRootIcon = m_icon_texture_name; + const Frect configuredRootIconRect = m_icon_rect; + load_data(m_priority, stream); SGameTaskObjective::load(stream); - u32 count; + if (ShadowOfChernobylMode && configuredRootIcon.size()) + { + m_icon_texture_name = configuredRootIcon; + m_icon_rect = configuredRootIconRect; + } + + u32 count{}; load_data(count, stream); m_Objectives.resize(count); @@ -636,6 +690,20 @@ void CGameTask::load(IReader& stream) CommitScriptHelperContents(); CreateMapLocation(true); + for (u32 i = 0; i < m_Objectives.size(); ++i) + { + SGameTaskObjective& objective = m_Objectives[i]; + const bool hiddenLocationAvailable = + ShadowOfChernobylMode && !objective.m_def_location_enabled && i > 0 && + m_Objectives[i - 1].GetTaskState() == eTaskStateCompleted; + + if (!ShadowOfChernobylMode || objective.m_def_location_enabled || hiddenLocationAvailable) + { + objective.CreateMapLocation(true); + if (hiddenLocationAvailable && objective.LinkedMapLocation()) + objective.LinkedMapLocation()->EnableSpot(); + } + } } void SGameTaskObjective::SetIconName_script(pcstr tex) diff --git a/src/xrGame/GametaskManager.cpp b/src/xrGame/GametaskManager.cpp index 2bce9e42e19..8df05377b16 100644 --- a/src/xrGame/GametaskManager.cpp +++ b/src/xrGame/GametaskManager.cpp @@ -239,6 +239,34 @@ void CGameTaskManager::UpdateActiveTask() { std::stable_sort(GetGameTasks().begin(), GetGameTasks().end(), task_prio_pred); + if (ShadowOfChernobylMode) + { + for (const SGameTaskKey& taskKey : GetGameTasks()) + { + CGameTask* task = taskKey.game_task; + if (task->GetTaskState() != eTaskStateInProgress) + continue; + + const TASK_OBJECTIVE_ID count = task->GetObjectivesCount(); + for (TASK_OBJECTIVE_ID objectiveId = 2; objectiveId < count; ++objectiveId) + { + SGameTaskObjective& objective = task->Objective(objectiveId); + if (objective.m_def_location_enabled || objective.LinkedMapLocation() || + objective.GetTaskState() != eTaskStateInProgress || + task->Objective(objectiveId - 1).GetTaskState() != eTaskStateCompleted) + { + continue; + } + + // Use the load path so saves made by the earlier broken + // implementation can reuse their disabled hidden location. + objective.CreateMapLocation(true); + if (objective.LinkedMapLocation()) + objective.LinkedMapLocation()->EnableSpot(); + } + } + } + for (u32 i = eTaskTypeStoryline; i < eTaskTypeCount; ++i) { CGameTask* activeTask = ActiveTask(static_cast(i)); @@ -309,9 +337,20 @@ void CGameTaskManager::MapLocationRelcase(CMapLocation* ml) if (mwnd) mwnd->MapLocationRelcase(ml); - CGameTask* gt = HasGameTask(ml, false); - if (gt) - gt->RemoveMapLocations(true); + for (const SGameTaskKey& taskKey : GetGameTasks()) + { + CGameTask* task = taskKey.game_task; + const TASK_OBJECTIVE_ID count = task->GetObjectivesCount(); + for (TASK_OBJECTIVE_ID i = 0; i < count; ++i) + { + SGameTaskObjective& objective = task->Objective(i); + if (objective.LinkedMapLocation() == ml) + { + objective.RemoveMapLocations(true); + return; + } + } + } } CGameTask* CGameTaskManager::HasGameTask(const CMapLocation* ml, bool only_inprocess) @@ -322,11 +361,14 @@ CGameTask* CGameTaskManager::HasGameTask(const CMapLocation* ml, bool only_inpro for (; it != it_e; ++it) { CGameTask* gt = (*it).game_task; - if (gt->LinkedMapLocation() == ml) + const TASK_OBJECTIVE_ID count = gt->GetObjectivesCount(); + for (TASK_OBJECTIVE_ID i = 0; i < count; ++i) { - if (only_inprocess && gt->GetTaskState() != eTaskStateInProgress) + if (gt->Objective(i).LinkedMapLocation() != ml) continue; + if (only_inprocess && gt->GetTaskState() != eTaskStateInProgress) + break; return gt; } } diff --git a/src/xrGame/PDA.cpp b/src/xrGame/PDA.cpp index 6a962e84042..5fb7fcb6e4d 100644 --- a/src/xrGame/PDA.cpp +++ b/src/xrGame/PDA.cpp @@ -165,6 +165,7 @@ CInventoryOwner* CPda::GetOriginalOwner() void CPda::ActivePDAContacts(xr_vector& res) { res.clear(); + xr_vector::iterator it = m_active_contacts.begin(); xr_vector::iterator it_e = m_active_contacts.end(); @@ -176,6 +177,33 @@ void CPda::ActivePDAContacts(xr_vector& res) } } +void CPda::ActivePDAContactOwners(xr_vector& res) +{ + res.clear(); + + if (!H_Parent()) + return; + + Position().set(H_Parent()->Position()); + const float radiusSqr = _sqr(m_fRadius); + const u32 objectCount = Level().Objects.o_count(); + + for (u32 i = 0; i < objectCount; ++i) + { + IGameObject* object = Level().Objects.o_get_by_iterator(i); + if (!object || object == H_Parent()) + continue; + + CEntityAlive* entity = smart_cast(object); + CInventoryOwner* inventoryOwner = smart_cast(object); + if (!entity || !inventoryOwner || !entity->g_Alive() || entity->cast_base_monster()) + continue; + + if (Position().distance_to_sqr(entity->Position()) <= radiusSqr) + res.push_back(inventoryOwner); + } +} + void CPda::save(NET_Packet& output_packet) { inherited::save(output_packet); diff --git a/src/xrGame/PDA.h b/src/xrGame/PDA.h index 6e657c9e619..ccf45c2d2ff 100644 --- a/src/xrGame/PDA.h +++ b/src/xrGame/PDA.h @@ -44,6 +44,7 @@ class CPda : public CInventoryItemObject, public Feel::Touch bool IsOn() { return !m_bTurnedOff; } bool IsOff() { return m_bTurnedOff; } void ActivePDAContacts(xr_vector& res); + void ActivePDAContactOwners(xr_vector& res); CPda* GetPdaFromOwner(IGameObject* owner); u32 ActiveContactsNum() { return m_active_contacts.size(); } void PlayScriptFunction(); diff --git a/src/xrGame/UIZoneMap.cpp b/src/xrGame/UIZoneMap.cpp index 71f643d0b69..d9333797370 100644 --- a/src/xrGame/UIZoneMap.cpp +++ b/src/xrGame/UIZoneMap.cpp @@ -93,16 +93,19 @@ void CUIZoneMap::Init(bool motionIconAttached) if (IsGameTypeSingle()) { - CUIXmlInit::InitStatic(uiXml, "minimap:static_counter", 0, &m_Counter); - m_background.AttachChild(&m_Counter); - CUIXmlInit::InitStatic(uiXml, "minimap:static_counter:text_static", 0, &m_Counter_text); - m_Counter.AttachChild(&m_Counter_text); - - if (motionIconAttached) + m_hasCounter = CUIXmlInit::InitStatic(uiXml, "minimap:static_counter", 0, &m_Counter, false); + if (m_hasCounter) { - temp = m_Counter.GetWndPos(); - temp.mul(m_background.GetWndSize()); - m_Counter.SetWndPos(temp); + m_background.AttachChild(&m_Counter); + CUIXmlInit::InitStatic(uiXml, "minimap:static_counter:text_static", 0, &m_Counter_text); + m_Counter.AttachChild(&m_Counter_text); + + if (motionIconAttached) + { + temp = m_Counter.GetWndPos(); + temp.mul(m_background.GetWndSize()); + m_Counter.SetWndPos(temp); + } } } } @@ -122,7 +125,7 @@ void CUIZoneMap::Update() if (!pActor) return; - if (!(Device.dwFrame % 20) && IsGameTypeSingle()) + if (!(Device.dwFrame % 20) && IsGameTypeSingle() && m_hasCounter) { string16 text_str; xr_strcpy(text_str, sizeof(text_str), ""); @@ -231,4 +234,8 @@ void CUIZoneMap::OnSectorChanged(IRender_Sector::sector_id_t sector) m_activeMap->InitTextureEx(sub_texture, m_activeMap->m_shader_name.c_str()); } -void CUIZoneMap::Counter_ResetClrAnimation() { m_Counter_text.ResetColorAnimation(); } +void CUIZoneMap::Counter_ResetClrAnimation() +{ + if (m_hasCounter) + m_Counter_text.ResetColorAnimation(); +} diff --git a/src/xrGame/UIZoneMap.h b/src/xrGame/UIZoneMap.h index cb73c38265f..cd4800bf461 100644 --- a/src/xrGame/UIZoneMap.h +++ b/src/xrGame/UIZoneMap.h @@ -21,6 +21,7 @@ class CUIZoneMap CUIStatic m_Counter_text{ "Counter text" }; CUIStatic* m_clock_wnd{}; CUIStatic* m_pointerDistanceText{}; + bool m_hasCounter{}; u8 m_current_map_idx{ u8(-1) }; diff --git a/src/xrGame/actor_communication.cpp b/src/xrGame/actor_communication.cpp index f1ad8e41ae7..0d578e2dc26 100644 --- a/src/xrGame/actor_communication.cpp +++ b/src/xrGame/actor_communication.cpp @@ -65,26 +65,23 @@ void CActor::AddEncyclopediaArticle(const CInfoPortion* info_portion) const n = (article.data()->name).c_str(); callback(GameObject::eArticleInfo)(lua_game_object(), g, n, _atype); - /* XXX: Shadow of Chernobyl encyclopedia, return this code if (CurrentGameUI()) { CUIGameSP* pGameSP = smart_cast(CurrentGameUI()); - pda_section::part p = pda_section::encyclopedia; - switch (article.data()->articleType) + if (pGameSP) { - case ARTICLE_DATA::eEncyclopediaArticle: p = pda_section::encyclopedia; - break; - case ARTICLE_DATA::eJournalArticle: p = pda_section::journal; - break; - case ARTICLE_DATA::eInfoArticle: p = pda_section::info; - break; - case ARTICLE_DATA::eTaskArticle: p = pda_section::quests; - break; - default: NODEFAULT; - }; - pGameSP->PdaMenu->PdaContentsChanged(p); + pda_section::part p = pda_section::encyclopedia; + switch (article.data()->articleType) + { + case ARTICLE_DATA::eEncyclopediaArticle: p = pda_section::encyclopedia; break; + case ARTICLE_DATA::eJournalArticle: p = pda_section::journal; break; + case ARTICLE_DATA::eInfoArticle: p = pda_section::info; break; + case ARTICLE_DATA::eTaskArticle: p = pda_section::quests; break; + default: NODEFAULT; + } + pGameSP->GetPdaMenu().PdaContentsChanged(p); + } } - */ if (CurrentGameUI()) { @@ -124,6 +121,9 @@ void CActor::AddGameNews(GAME_NEWS_DATA&& news_data) GAME_NEWS_VECTOR& news_vector = game_news_registry->registry().objects(); news_vector.emplace_back(std::move(news_data)); + + if (CurrentGameUI()) + CurrentGameUI()->GetPdaMenu().PdaContentsChanged(pda_section::news); } void CActor::ClearGameNews() diff --git a/src/xrGame/ai/monsters/dog/dog_state_manager.cpp b/src/xrGame/ai/monsters/dog/dog_state_manager.cpp index ce8ed07329f..5af9aa3cd62 100644 --- a/src/xrGame/ai/monsters/dog/dog_state_manager.cpp +++ b/src/xrGame/ai/monsters/dog/dog_state_manager.cpp @@ -9,6 +9,7 @@ #include "ai/monsters/states/monster_state_help_sound.h" #include "ai/monsters/states/monster_state_hear_int_sound.h" #include "ai/monsters/states/monster_state_hitted.h" +#include "ai/monsters/states/monster_state_rest.h" #include "ai/monsters/group_states/group_state_attack.h" #include "ai/monsters/group_states/group_state_rest.h" #include "ai/monsters/group_states/group_state_eat.h" @@ -22,7 +23,10 @@ const float atack_decision_maxdist = 6.f; CStateManagerDog::CStateManagerDog(CAI_Dog* monster) : inherited(monster) { - add_state(eStateRest, xr_new>(monster)); + if (ShadowOfChernobylMode) + add_state(eStateRest, xr_new>(monster)); + else + add_state(eStateRest, xr_new>(monster)); add_state(eStatePanic, xr_new>(monster)); add_state(eStateAttack, xr_new>(monster)); add_state(eStateEat, xr_new>(monster)); diff --git a/src/xrGame/alife_object.cpp b/src/xrGame/alife_object.cpp index 8136c96a03e..971bf60b0bb 100644 --- a/src/xrGame/alife_object.cpp +++ b/src/xrGame/alife_object.cpp @@ -82,7 +82,7 @@ void CSE_ALifeObject::spawn_supplies(LPCSTR ini_string) n = _GetItemCount(V); if (n > 0) { - string64 tmp; + xr_string tmp; spawnCount = atoi(_GetItem(V, 0, tmp)); //count } @@ -114,7 +114,7 @@ void CSE_ALifeObject::spawn_supplies(LPCSTR ini_string) { pcstr ammo_class = pSettings->r_string(itmSection, "ammo_class"); pcstr ammoSec = ""; - string128 tmp; + xr_string tmp; for (int i = 0, n = _GetItemCount(ammo_class); i < n; ++i) { ammoSec = _GetItem(ammo_class, i, tmp); @@ -164,7 +164,7 @@ void CSE_ALifeObject::spawn_supplies(LPCSTR ini_string) if (V && xr_strlen(V)) { - string64 buf; + xr_string buf; j = atoi(_GetItem(V, 0, buf)); if (!j) j = 1; diff --git a/src/xrGame/alife_simulator_script.cpp b/src/xrGame/alife_simulator_script.cpp index 7f4df74754c..d400d9aedaf 100644 --- a/src/xrGame/alife_simulator_script.cpp +++ b/src/xrGame/alife_simulator_script.cpp @@ -53,6 +53,17 @@ CSE_ALifeDynamicObject* alife_object(const CALifeSimulator* self, pcstr name) { VERIFY(self); + if (!name) + { + // SoC random-task saves can use -1 for an absent defend target. The + // original script then calls object(nil) and reads optional actor spawn + // data. Use the actor as the harmless compatibility object. + if (ShadowOfChernobylMode) + return self->graph().actor(); + + return nullptr; + } + for (CALifeObjectRegistry::OBJECT_REGISTRY::const_iterator it = self->objects().objects().begin(); it != self->objects().objects().end(); ++it) { diff --git a/src/xrGame/game_news.h b/src/xrGame/game_news.h index 920861a4a3e..015e20b1920 100644 --- a/src/xrGame/game_news.h +++ b/src/xrGame/game_news.h @@ -2,6 +2,7 @@ #include "alife_space.h" #include "Common/object_interfaces.h" +#include "xrCore/_fbox2.h" #define DEFAULT_NEWS_SHOW_TIME 5000 @@ -20,6 +21,9 @@ struct GAME_NEWS_DATA : public ISerializable shared_str news_caption; shared_str news_text; shared_str texture_name; + // Keep this data transient to preserve the existing save-file format. + Frect texture_rect{}; + bool has_texture_rect{}; ALife::_TIME_ID receive_time; }; diff --git a/src/xrGame/level_script.cpp b/src/xrGame/level_script.cpp index b6fb3df8b08..940e15ceeeb 100644 --- a/src/xrGame/level_script.cpp +++ b/src/xrGame/level_script.cpp @@ -445,6 +445,9 @@ void add_actor_points_str(LPCSTR sect, LPCSTR detail_key, LPCSTR str_value) } int get_actor_points(LPCSTR sect) { return Actor()->StatisticMgr().GetSectionPoints(sect); } +extern int get_actor_ranking(); +extern void add_human_to_top_list(u16 id); +extern void remove_human_from_top_list(u16 id); #include "ActorEffector.h" void add_complex_effector(LPCSTR section, int id) { AddEffector(Actor(), id, section); } void remove_complex_effector(int id) { RemoveEffector(Actor(), id); } @@ -891,7 +894,10 @@ void CLevel::script_register(lua_State* luaState) [ def("add_points", &add_actor_points), def("add_points_str", &add_actor_points_str), - def("get_points", &get_actor_points) + def("get_points", &get_actor_points), + def("add_to_ranking", &add_human_to_top_list), + def("remove_from_ranking", &remove_human_from_top_list), + def("get_actor_ranking", &get_actor_ranking) ]; module(luaState) diff --git a/src/xrGame/map_location.cpp b/src/xrGame/map_location.cpp index 596a82e6d54..5215f03cff7 100644 --- a/src/xrGame/map_location.cpp +++ b/src/xrGame/map_location.cpp @@ -436,7 +436,8 @@ void CMapLocation::UpdateSpot(CUICustomMap* map, CMapSpot* sp) float h_ = map->GetHeading() + h; sp->SetHeading(h_); } - map->AttachChild(sp); + if (!sp->GetParent()) + map->AttachChild(sp); } if (IsGameTypeSingle()) @@ -445,7 +446,8 @@ void CMapLocation::UpdateSpot(CUICustomMap* map, CMapSpot* sp) if (s) { s->SetWndPos(sp->GetWndPos()); - map->AttachChild(s); + if (!s->GetParent()) + map->AttachChild(s); } } diff --git a/src/xrGame/script_game_object.h b/src/xrGame/script_game_object.h index ee175756185..01ff68b1d88 100644 --- a/src/xrGame/script_game_object.h +++ b/src/xrGame/script_game_object.h @@ -320,6 +320,8 @@ class CScriptGameObject bool DisableInfoPortion(LPCSTR info_id); void GiveGameNews(LPCSTR caption, LPCSTR news, LPCSTR texture_name, int delay, int show_time); + void GiveGameNews( + LPCSTR caption, LPCSTR news, LPCSTR texture_name, const Frect& texture_rect, int delay, int show_time); void GiveGameNews(LPCSTR caption, LPCSTR news, LPCSTR texture_name, int delay, int show_time, int type); void ClearGameNews() const; diff --git a/src/xrGame/script_game_object_inventory_owner.cpp b/src/xrGame/script_game_object_inventory_owner.cpp index a67ddbb4842..f506f1c0761 100644 --- a/src/xrGame/script_game_object_inventory_owner.cpp +++ b/src/xrGame/script_game_object_inventory_owner.cpp @@ -111,20 +111,30 @@ void CScriptGameObject::AddIconedTalkMessage(LPCSTR caption, LPCSTR text, LPCSTR _AddIconedTalkMessage(caption, text, texture_name, templ_name); } -void _give_news(LPCSTR caption, LPCSTR news, LPCSTR texture_name, int delay, int show_time, int type); +void _give_news( + LPCSTR caption, LPCSTR news, LPCSTR texture_name, const Frect* texture_rect, int delay, int show_time, int type); void CScriptGameObject::GiveGameNews(LPCSTR caption, LPCSTR news, LPCSTR texture_name, int delay, int show_time) { GiveGameNews(caption, news, texture_name, delay, show_time, GAME_NEWS_DATA::eNews); } +void CScriptGameObject::GiveGameNews( + LPCSTR caption, LPCSTR news, LPCSTR texture_name, const Frect& texture_rect, int delay, int show_time) +{ + Frect absolute_texture_rect = texture_rect; + absolute_texture_rect.rb.add(absolute_texture_rect.lt); + _give_news(caption, news, texture_name, &absolute_texture_rect, delay, show_time, GAME_NEWS_DATA::eNews); +} + void CScriptGameObject::GiveGameNews( LPCSTR caption, LPCSTR news, LPCSTR texture_name, int delay, int show_time, int type) { - _give_news(caption, news, texture_name, delay, show_time, type); + _give_news(caption, news, texture_name, nullptr, delay, show_time, type); } -void _give_news(LPCSTR caption, LPCSTR text, LPCSTR texture_name, int delay, int show_time, int type) +void _give_news( + LPCSTR caption, LPCSTR text, LPCSTR texture_name, const Frect* texture_rect, int delay, int show_time, int type) { GAME_NEWS_DATA news_data; news_data.m_type = (GAME_NEWS_DATA::eNewsType)type; @@ -136,6 +146,11 @@ void _give_news(LPCSTR caption, LPCSTR text, LPCSTR texture_name, int delay, int VERIFY(xr_strlen(texture_name) > 0); news_data.texture_name = texture_name; + if (texture_rect) + { + news_data.texture_rect = *texture_rect; + news_data.has_texture_rect = true; + } if (delay == 0) Actor()->AddGameNews(std::move(news_data)); diff --git a/src/xrGame/script_game_object_script3.cpp b/src/xrGame/script_game_object_script3.cpp index 2f9ef7695dd..2e0b57f33d7 100644 --- a/src/xrGame/script_game_object_script3.cpp +++ b/src/xrGame/script_game_object_script3.cpp @@ -165,12 +165,11 @@ luabind::class_& script_register_game_object2(luabind::class_ .def("disable_info_portion", &CScriptGameObject::DisableInfoPortion) .def("give_game_news", +[](CScriptGameObject* self, - pcstr news, pcstr texture_name, Frect /*tex_rect*/, int delay, int show_time) + pcstr news, pcstr texture_name, Frect texture_rect, int delay, int show_time) { // SOC give_game_news style - // tex_rect is ignored, we could add support for it back, if really needed. - // It also should be safe to pass nullptr to caption param - self->GiveGameNews(nullptr, news, texture_name, delay, show_time); + // It is safe to pass nullptr to the caption parameter. + self->GiveGameNews(nullptr, news, texture_name, texture_rect, delay, show_time); return true; }) .def("give_game_news", diff --git a/src/xrGame/ui/UIActorInfo.cpp b/src/xrGame/ui/UIActorInfo.cpp index 1710f8b2274..a027f82eab8 100644 --- a/src/xrGame/ui/UIActorInfo.cpp +++ b/src/xrGame/ui/UIActorInfo.cpp @@ -4,6 +4,7 @@ #include "xrUICore/Windows/UIFrameLineWnd.h" #include "xrUICore/Static/UIAnimatedStatic.h" +#include "xrUICore/Static/UIStatic.h" #include "Actor.h" @@ -18,6 +19,37 @@ constexpr cpcstr ACTOR_STATISTIC_XML = "actor_statistic.xml"; constexpr cpcstr ACTOR_CHARACTER_XML = "pda_dialog_character.xml"; +namespace +{ +void MoveSocActorTextPastPortrait(CUICharacterInfo& characterInfo) +{ + const CUIStatic& portrait = characterInfo.UIIcon(); + const float portraitRight = portrait.GetWndPos().x + + _max(portrait.GetWidth(), portrait.GetTextureRect().width()); + + constexpr CUICharacterInfo::UIItemType TextItems[] = { + CUICharacterInfo::eRankCaption, + CUICharacterInfo::eRank, + CUICharacterInfo::eCommunityCaption, + CUICharacterInfo::eCommunity, + CUICharacterInfo::eReputationCaption, + CUICharacterInfo::eReputation, + }; + + for (const CUICharacterInfo::UIItemType type : TextItems) + { + CUIStatic* text = characterInfo.GetIcon(type); + if (!text) + continue; + + Fvector2 position = text->GetWndPos(); + const float rightLimit = characterInfo.GetWidth() - text->GetWidth(); + position.x = _min(_max(position.x, portraitRight), rightLimit); + text->SetWndPos(position); + } +} +} // namespace + CUIActorInfoWnd::CUIActorInfoWnd() : CUIWindow(CUIActorInfoWnd::GetDebugType()) {} bool CUIActorInfoWnd::Init() @@ -72,7 +104,7 @@ bool CUIActorInfoWnd::Init() UICharacterInfo = xr_new(); UICharacterInfo->SetAutoDelete(true); UICharacterWindow->AttachChild(UICharacterInfo); - UICharacterInfo->InitCharacterInfo(UICharacterWindow->GetWndPos(), UICharacterWindow->GetWndSize(), ACTOR_CHARACTER_XML); + UICharacterInfo->InitCharacterInfo(Fvector2().set(0.0f, 0.0f), UICharacterWindow->GetWndSize(), ACTOR_CHARACTER_XML); // Элементы автоматического добавления CUIXmlInit::InitAutoStaticGroup(uiXml, "right_auto_static", 0, UICharIconFrame); @@ -87,6 +119,8 @@ void CUIActorInfoWnd::Show(bool status) if (!status) return; UICharacterInfo->InitCharacter(Actor()->ID()); + if (ShadowOfChernobylMode) + MoveSocActorTextPastPortrait(*UICharacterInfo); if (UICharIconHeader->GetTitleText()) UICharIconHeader->GetTitleText()->SetText(Actor()->Name()); FillPointsInfo(); diff --git a/src/xrGame/ui/UIActorStateInfo.cpp b/src/xrGame/ui/UIActorStateInfo.cpp index ba5c4b5bbf9..9b56b4fa4a1 100644 --- a/src/xrGame/ui/UIActorStateInfo.cpp +++ b/src/xrGame/ui/UIActorStateInfo.cpp @@ -92,6 +92,14 @@ void ui_actor_state_wnd::UpdateActorInfo(CInventoryOwner* owner) const auto& conditions = actor->conditions(); + if (ShadowOfChernobylMode) + { + m_state[stt_health]->set_progress(conditions.GetHealth() * 100.0f); + m_state[stt_psi]->set_progress(conditions.GetPsyHealth() * 100.0f); + m_state[stt_radia]->set_progress(conditions.GetRadiation() * 100.0f); + return; + } + // show stamina icon value = conditions.GetPower(); m_state[stt_stamina]->set_progress(value); diff --git a/src/xrGame/ui/UICharacterInfo.cpp b/src/xrGame/ui/UICharacterInfo.cpp index 667e56d47ec..1c375ec6e6f 100644 --- a/src/xrGame/ui/UICharacterInfo.cpp +++ b/src/xrGame/ui/UICharacterInfo.cpp @@ -20,6 +20,7 @@ #include "xrServer.h" #include "xrServerEntities/xrServer_Objects_ALife_Monsters.h" #include "UIHelper.h" +#include "xrEngine/StringTable/StringTable.h" using namespace InventoryUtilities; @@ -138,7 +139,16 @@ void CUICharacterInfo::InitCharacter(u16 id) } if (m_icons[eRank]) { - m_icons[eRank]->TextItemControl()->SetTextST(GetRankAsText(chInfo.Rank().value())); + const pcstr rankStringId = GetRankAsText(chInfo.Rank().value()); + if (ShadowOfChernobylMode) + { + xr_string rankText = CStringTable().translate(rankStringId).c_str(); + if (!rankText.empty() && rankText[0] >= 'A' && rankText[0] <= 'Z') + rankText[0] += 'a' - 'A'; + m_icons[eRank]->SetText(rankText.c_str()); + } + else + m_icons[eRank]->TextItemControl()->SetTextST(rankStringId); } if (m_icons[eCommunity]) { diff --git a/src/xrGame/ui/UIDiaryWnd.h b/src/xrGame/ui/UIDiaryWnd.h index 032267aecf3..ecc65c96b31 100644 --- a/src/xrGame/ui/UIDiaryWnd.h +++ b/src/xrGame/ui/UIDiaryWnd.h @@ -1,8 +1,8 @@ + #pragma once -/* -#include "UIWindow.h" -#include "UIWndCallback.h" +#include "xrUICore/Windows/UIWindow.h" +#include "xrUICore/Callbacks/UIWndCallback.h" #include "../encyclopedia_article_defs.h" class CUINewsWnd; class CUIFrameLineWnd; @@ -13,60 +13,68 @@ class CUITabControl; class CUIScrollView; class CUIListWnd; class CEncyclopediaArticle; +// class CUIVideoPlayerWnd; -class CUIDiaryWnd: public CUIWindow, public CUIWndCallback +class CUIDiaryWnd : public CUIWindow, public CUIWndCallback { typedef CUIWindow inherited; + enum EDiaryFilter + { + eJournal = 0, + eNews, + eNone + }; + protected: - shared_str m_currFilter; + EDiaryFilter m_currFilter; + u32 prevArticlesCount; - CUINewsWnd* m_UINewsWnd; + CUINewsWnd* m_UINewsWnd; - CUIWindow* m_UILeftWnd; - CUIWindow* m_UIRightWnd; - CUIFrameWindow* m_UILeftFrame; - CUIFrameLineWnd* m_UILeftHeader; - CUIFrameWindow* m_UIRightFrame; - CUIFrameLineWnd* m_UIRightHeader; - CUIAnimatedStatic* m_UIAnimation; - CUITabControl* m_FilterTab; - CUIListWnd* m_SrcListWnd; - CUIScrollView* m_DescrView; - CGameFont* m_pTreeRootFont; - u32 m_uTreeRootColor; - CGameFont* m_pTreeItemFont; - u32 m_uTreeItemColor; + CUIWindow* m_UILeftWnd; + CUIWindow* m_UIRightWnd; + CUIFrameWindow* m_UILeftFrame; + CUIFrameLineWnd* m_UILeftHeader; + CUIFrameWindow* m_UIRightFrame; + CUIFrameLineWnd* m_UIRightHeader; + CUIAnimatedStatic* m_UIAnimation; + CUITabControl* m_FilterTab; + CUIListWnd* m_SrcListWnd; + CUIScrollView* m_DescrView; + CGameFont* m_pTreeRootFont; + u32 m_uTreeRootColor; + CGameFont* m_pTreeItemFont; + u32 m_uTreeItemColor; - xr_vector m_sign_places; - CUIStatic* m_updatedSectionImage; - CUIStatic* m_oldSectionImage; + xr_vector m_sign_places; + CUIStatic* m_updatedSectionImage; + CUIStatic* m_oldSectionImage; - typedef xr_vector ArticlesDB; - typedef xr_vector::iterator ArticlesDB_it; - ArticlesDB m_ArticlesDB; + xr_vector m_ArticlesDB; + + void OnFilterChanged(CUIWindow*, void*); + void OnSrcListItemClicked(CUIWindow*, void*); + void UnloadJournalTab(); + void LoadJournalTab(); + void UnloadNewsTab(); + void LoadNewsTab(); + void Reload(EDiaryFilter new_filter); - void OnFilterChanged (CUIWindow*,void*); - void OnSrcListItemClicked (CUIWindow*,void*); - void UnloadJournalTab (); - void LoadJournalTab (ARTICLE_DATA::EArticleType _type); - void UnloadInfoTab (); - void LoadInfoTab (); - void UnloadNewsTab (); - void LoadNewsTab (); - void Reload (const shared_str& new_filter); public: - CUIDiaryWnd (); - virtual ~CUIDiaryWnd (); + CUIDiaryWnd(); + virtual ~CUIDiaryWnd(); - virtual void SendMessage (CUIWindow* pWnd, s16 msg, void* pData); - virtual void Draw (); - virtual void Reset (); + virtual void SendMessage(CUIWindow* pWnd, s16 msg, void* pData); + virtual void Draw(); + virtual void Reset(); - void Init (); - void AddNews (); - void MarkNewsAsRead (bool status); - virtual void Show (bool status); + void Init(); + void AddNews(); + void MarkNewsAsRead(bool status); + virtual void Show(bool status); + void FillNews(); + void ReloadJournal(); + void ResetJournal(); + void UpdateJournal(); }; - -*/ diff --git a/src/xrGame/ui/UIDiaryWnd2.cpp b/src/xrGame/ui/UIDiaryWnd2.cpp new file mode 100644 index 00000000000..fca150c2db3 --- /dev/null +++ b/src/xrGame/ui/UIDiaryWnd2.cpp @@ -0,0 +1,296 @@ +#include "StdAfx.h" +#include "UIDiaryWnd.h" +#include "xrUICore/Windows/UIFrameWindow.h" +#include "xrUICore/Windows/UIFrameLineWnd.h" +#include "UINewsWnd.h" +#include "xrUICore/Static/UIAnimatedStatic.h" +#include "UIXmlInit.h" +#include "Common/object_broker.h" +#include "xrUICore/TabControl/UITabControl.h" +#include "xrUICore/ScrollView/UIScrollView.h" +#include "xrUICore/ListWnd/UIListWnd.h" +#include "UITreeViewItem.h" +#include "UIEncyclopediaArticleWnd.h" +#include "../Level.h" +#include "../Actor.h" +#include "../alife_registry_wrappers.h" +#include "../encyclopedia_article.h" +#include "UIPdaAux.h" + +extern u32 g_pda_info_state; + +CUIDiaryWnd::CUIDiaryWnd() +{ + m_currFilter = eNone; + prevArticlesCount = 0; +} + +CUIDiaryWnd::~CUIDiaryWnd() +{ + delete_data(m_UINewsWnd); + delete_data(m_SrcListWnd); + delete_data(m_DescrView); + m_ArticlesDB.clear(); + delete_data(m_updatedSectionImage); + delete_data(m_oldSectionImage); +} + +void CUIDiaryWnd::Show(bool status) +{ + inherited::Show(status); + if (status) + Reload((EDiaryFilter)m_FilterTab->GetActiveIndex()); +} + +void RearrangeTabButtons(CUITabControl* pTab, xr_vector& vec_sign_places); + +void CUIDiaryWnd::Init() +{ + CUIXml uiXml; + bool xml_result = uiXml.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "events_new.xml"); + R_ASSERT3(xml_result, "xml file not found", "events_new.xml"); + CUIXmlInit xml_init; + + xml_init.InitWindow(uiXml, "main_wnd", 0, this); + + m_UILeftFrame = xr_new(); + m_UILeftFrame->SetAutoDelete(true); + xml_init.InitFrameWindow(uiXml, "main_wnd:left_frame", 0, m_UILeftFrame); + AttachChild(m_UILeftFrame); + + m_UILeftHeader = xr_new(); + m_UILeftHeader->SetAutoDelete(true); + xml_init.InitFrameLine(uiXml, "main_wnd:left_frame:left_frame_header", 0, m_UILeftHeader); + m_UILeftFrame->AttachChild(m_UILeftHeader); + + m_FilterTab = xr_new(); + m_FilterTab->SetAutoDelete(true); + m_UILeftHeader->AttachChild(m_FilterTab); + xml_init.InitTabControl(uiXml, "main_wnd:left_frame:left_frame_header:filter_tab", 0, m_FilterTab); + m_FilterTab->SetWindowName("filter_tab"); + Register(m_FilterTab); + AddCallbackStr("filter_tab", TAB_CHANGED, fastdelegate::MakeDelegate(this, &CUIDiaryWnd::OnFilterChanged)); + + if (uiXml.NavigateToNode("main_wnd:left_frame:left_frame_header:anim_static")) + { + m_UIAnimation = xr_new(); + m_UIAnimation->SetAutoDelete(true); + xml_init.InitAnimatedStatic(uiXml, "main_wnd:left_frame:left_frame_header:anim_static", 0, m_UIAnimation); + m_UILeftHeader->AttachChild(m_UIAnimation); + } + + m_UILeftWnd = xr_new(); + m_UILeftWnd->SetAutoDelete(true); + xml_init.InitWindow(uiXml, "main_wnd:left_frame:work_area", 0, m_UILeftWnd); + m_UILeftFrame->AttachChild(m_UILeftWnd); + + m_SrcListWnd = xr_new(); + m_SrcListWnd->SetAutoDelete(false); + xml_init.InitListWnd(uiXml, "main_wnd:left_frame:work_area:src_list", 0, m_SrcListWnd); + m_SrcListWnd->SetWindowName("src_list"); + Register(m_SrcListWnd); + AddCallbackStr("src_list", LIST_ITEM_CLICKED, fastdelegate::MakeDelegate(this, &CUIDiaryWnd::OnSrcListItemClicked)); + + xml_init.InitFont(uiXml, "main_wnd:left_frame:work_area:src_list:tree_item_font", 0, m_uTreeItemColor, m_pTreeItemFont); + R_ASSERT(m_pTreeItemFont); + xml_init.InitFont(uiXml, "main_wnd:left_frame:work_area:src_list:tree_root_font", 0, m_uTreeRootColor, m_pTreeRootFont); + R_ASSERT(m_pTreeRootFont); + + m_UIRightFrame = xr_new(); + m_UIRightFrame->SetAutoDelete(true); + xml_init.InitFrameWindow(uiXml, "main_wnd:right_frame", 0, m_UIRightFrame); + AttachChild(m_UIRightFrame); + + m_UIRightHeader = xr_new(); + m_UIRightHeader->SetAutoDelete(true); + xml_init.InitFrameLine(uiXml, "main_wnd:right_frame:right_frame_header", 0, m_UIRightHeader); + m_UIRightFrame->AttachChild(m_UIRightHeader); + + m_UIRightWnd = xr_new(); + m_UIRightWnd->SetAutoDelete(true); + xml_init.InitWindow(uiXml, "main_wnd:right_frame:work_area", 0, m_UIRightWnd); + m_UIRightFrame->AttachChild(m_UIRightWnd); + + m_UINewsWnd = xr_new(); + m_UINewsWnd->SetAutoDelete(false); + m_UINewsWnd->Init(); + + m_DescrView = xr_new(); + m_DescrView->SetAutoDelete(false); + xml_init.InitScrollView(uiXml, "main_wnd:right_frame:work_area:scroll_view", 0, m_DescrView); + + m_updatedSectionImage = xr_new(); + xml_init.InitStatic(uiXml, "updated_section_static", 0, m_updatedSectionImage); + + m_oldSectionImage = xr_new(); + xml_init.InitStatic(uiXml, "old_section_static", 0, m_oldSectionImage); + + RearrangeTabButtons(m_FilterTab, m_sign_places); +} + +void CUIDiaryWnd::SendMessage(CUIWindow* pWnd, s16 msg, void* pData) { CUIWndCallback::OnEvent(pWnd, msg, pData); } + +void CUIDiaryWnd::OnFilterChanged(CUIWindow* w, void*) { Reload((EDiaryFilter)m_FilterTab->GetActiveIndex()); } + +void CUIDiaryWnd::Reload(EDiaryFilter new_filter) +{ + //. if(m_currFilter==new_filter) return; + + switch (m_currFilter) + { + case eJournal: UnloadJournalTab(); break; + case eNews: UnloadNewsTab(); break; + }; + + m_currFilter = new_filter; + + switch (m_currFilter) + { + case eJournal: LoadJournalTab(); break; + case eNews: LoadNewsTab(); break; + }; +} + +void CUIDiaryWnd::AddNews() { m_UINewsWnd->AddNews(); } + +void CUIDiaryWnd::MarkNewsAsRead(bool status) {} + +void CUIDiaryWnd::UnloadJournalTab() +{ + m_UILeftWnd->DetachChild(m_SrcListWnd); + m_SrcListWnd->Show(false); + + m_UIRightWnd->DetachChild(m_DescrView); + m_DescrView->Show(false); + m_DescrView->Clear(); +} + +void CUIDiaryWnd::LoadJournalTab() +{ + m_UILeftWnd->AttachChild(m_SrcListWnd); + m_SrcListWnd->Show(true); + + m_UIRightWnd->AttachChild(m_DescrView); + m_DescrView->Show(true); + + UpdateJournal(); + g_pda_info_state &= ~pda_section::journal; +} + +void CUIDiaryWnd::UnloadNewsTab() +{ + m_UIRightWnd->DetachChild(m_UINewsWnd); + m_UINewsWnd->Show(false); +} + +void CUIDiaryWnd::LoadNewsTab() +{ + m_UIRightWnd->AttachChild(m_UINewsWnd); + m_UINewsWnd->Show(true); + g_pda_info_state &= ~pda_section::news; +} + +void CUIDiaryWnd::OnSrcListItemClicked(CUIWindow* w, void* p) +{ + CUITreeViewItem* pSelItem = (CUITreeViewItem*)p; + m_DescrView->Clear(); + if (!pSelItem->IsRoot()) + { + CUIEncyclopediaArticleWnd* article_info = xr_new(); + article_info->Init("encyclopedia_item.xml", "encyclopedia_wnd:objective_item"); + article_info->SetArticle(&m_ArticlesDB[pSelItem->GetValue()]); + m_DescrView->AddWindow(article_info, true); + + // Исправление отображения зеленым цветом прочитанных записей в дневнике КПК + if (!pSelItem->IsArticleReaded()) + { + if (Actor()->encyclopedia_registry->registry().objects_ptr()) + { + for (ARTICLE_VECTOR::iterator it = Actor()->encyclopedia_registry->registry().objects().begin(); it != Actor()->encyclopedia_registry->registry().objects().end(); + it++) + { + if (ARTICLE_DATA::eJournalArticle == it->article_type && m_ArticlesDB[pSelItem->GetValue()].Id() == it->article_id) + { + it->readed = true; + break; + } + } + } + } + } +} + +void draw_sign(CUIStatic* s, Fvector2& pos); +void CUIDiaryWnd::Draw() +{ + inherited::Draw(); + + m_updatedSectionImage->Update(); + m_oldSectionImage->Update(); + + Fvector2 tab_pos; + m_FilterTab->GetAbsolutePos(tab_pos); + + Fvector2 pos; + + pos = m_sign_places[eNews]; + pos.add(tab_pos); + if (g_pda_info_state & pda_section::news) + draw_sign(m_updatedSectionImage, pos); + else + draw_sign(m_oldSectionImage, pos); + + pos = m_sign_places[eJournal]; + pos.add(tab_pos); + if (g_pda_info_state & pda_section::journal) + draw_sign(m_updatedSectionImage, pos); + else + draw_sign(m_oldSectionImage, pos); +} + +void CUIDiaryWnd::Reset() +{ + inherited::Reset(); + m_UINewsWnd->Reset(); + ResetJournal(); +} + +void CUIDiaryWnd::FillNews() +{ + m_UINewsWnd->LoadNews(); + UpdateJournal(); +} + +void CUIDiaryWnd::ReloadJournal() +{ + if (Actor() && Actor()->encyclopedia_registry->registry().objects_ptr()->size() < prevArticlesCount) + ResetJournal(); +} + +void CUIDiaryWnd::ResetJournal() +{ + m_SrcListWnd->RemoveAll(); + m_ArticlesDB.clear(); + prevArticlesCount = 0; +} + +void CUIDiaryWnd::UpdateJournal() +{ + if (Actor()->encyclopedia_registry->registry().objects_ptr() && Actor()->encyclopedia_registry->registry().objects_ptr()->size() > prevArticlesCount) + { + ARTICLE_VECTOR::const_iterator it = Actor()->encyclopedia_registry->registry().objects_ptr()->begin(); + std::advance(it, prevArticlesCount); + for (; it != Actor()->encyclopedia_registry->registry().objects_ptr()->end(); it++) + { + if (it->article_type == ARTICLE_DATA::eJournalArticle) + { + auto& a = m_ArticlesDB.emplace_back(); + a.Load(it->article_id); + bool bReaded = it->readed; + CreateTreeBranch(a.data()->group, a.data()->name, m_SrcListWnd, m_ArticlesDB.size() - 1, m_pTreeRootFont, m_uTreeRootColor, m_pTreeItemFont, m_uTreeItemColor, + bReaded); + } + } + prevArticlesCount = Actor()->encyclopedia_registry->registry().objects_ptr()->size(); + } +} diff --git a/src/xrGame/ui/UIEncyclopediaArticleWnd.cpp b/src/xrGame/ui/UIEncyclopediaArticleWnd.cpp new file mode 100644 index 00000000000..f108676db4b --- /dev/null +++ b/src/xrGame/ui/UIEncyclopediaArticleWnd.cpp @@ -0,0 +1,82 @@ +#include "StdAfx.h" +#include "UIEncyclopediaArticleWnd.h" +#include "xrUICore/Static/UIStatic.h" +#include "../encyclopedia_article.h" +#include "UIXmlInit.h" +#include "xrEngine/StringTable/StringTable.h" + +CUIEncyclopediaArticleWnd::CUIEncyclopediaArticleWnd() : CUIWindow("CUIEncyclopediaArticleWnd"), m_Article(NULL) {} + +CUIEncyclopediaArticleWnd::~CUIEncyclopediaArticleWnd() {} + +void CUIEncyclopediaArticleWnd::Init(LPCSTR xml_name, LPCSTR start_from) +{ + CUIXml uiXml; + bool xml_result = uiXml.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, xml_name); + R_ASSERT3(xml_result, "xml file not found", xml_name); + + CUIXmlInit xml_init; + + string512 str; + + strcpy_s(str, sizeof(str), start_from); + xml_init.InitWindow(uiXml, str, 0, this); + + strconcat(sizeof(str), str, start_from, ":image"); + m_UIImage = xr_new(); + m_UIImage->SetAutoDelete(true); + xml_init.InitStatic(uiXml, str, 0, m_UIImage); + AttachChild(m_UIImage); + + strconcat(sizeof(str), str, start_from, ":text_cont"); + m_UIText = xr_new(); + m_UIText->SetAutoDelete(true); + xml_init.InitStatic(uiXml, str, 0, m_UIText); + if (ShadowOfChernobylMode) + m_UIText->SetTextAlignment(CGameFont::alLeft); + AttachChild(m_UIText); +} + +void CUIEncyclopediaArticleWnd::SetArticle(CEncyclopediaArticle* article) +{ + const bool has_image = article->data()->image.GetShader()->inited(); + m_UIImage->Show(has_image); + if (has_image) + { + m_UIImage->SetShader(article->data()->image.GetShader()); + m_UIImage->SetTextureRect(article->data()->image.GetTextureRect()); + m_UIImage->SetWndSize(article->data()->image.GetWndSize()); + + float img_x = (GetWidth() - m_UIImage->GetWidth()) / 2.0f; + img_x = _max(0.0f, img_x); + m_UIImage->SetWndPos(img_x, m_UIImage->GetWndPos().y); + }; + m_UIText->SetText(CStringTable().translate(article->data()->text.c_str()).c_str()); + if (ShadowOfChernobylMode) + m_UIText->SetTextAlignment(CGameFont::alLeft); + m_UIText->AdjustHeightToText(); + + AdjustLauout(); +} + +void CUIEncyclopediaArticleWnd::Draw() +{ + if (ShadowOfChernobylMode) + m_UIText->SetTextAlignment(CGameFont::alLeft); + + inherited::Draw(); +} + +void CUIEncyclopediaArticleWnd::AdjustLauout() +{ + const float image_bottom = m_UIImage->IsShown() ? m_UIImage->GetWndPos().y + m_UIImage->GetHeight() : 0.0f; + m_UIText->SetWndPos(m_UIText->GetWndPos().x, image_bottom); + SetHeight(image_bottom + m_UIText->GetHeight()); +} + +void CUIEncyclopediaArticleWnd::SetArticle(LPCSTR article) +{ + CEncyclopediaArticle A; + A.Load(article); + SetArticle(&A); +} diff --git a/src/xrGame/ui/UIEncyclopediaArticleWnd.h b/src/xrGame/ui/UIEncyclopediaArticleWnd.h new file mode 100644 index 00000000000..6932ab6176b --- /dev/null +++ b/src/xrGame/ui/UIEncyclopediaArticleWnd.h @@ -0,0 +1,27 @@ +#pragma once +#include "xrUICore/Windows/UIWindow.h" + +class CUIStatic; +class CEncyclopediaArticle; + +class CUIEncyclopediaArticleWnd : public CUIWindow +{ + typedef CUIWindow inherited; + + CUIStatic* m_UIImage; + CUIStatic* m_UIText; + CEncyclopediaArticle* m_Article; + +protected: + void AdjustLauout(); + +public: + CUIEncyclopediaArticleWnd(); + virtual ~CUIEncyclopediaArticleWnd(); + void Init(LPCSTR xml_name, LPCSTR start_from); + void SetArticle(CEncyclopediaArticle*); + void SetArticle(LPCSTR); + void Draw() override; + + pcstr GetDebugType() override { return "CUIEncyclopediaArticleWnd"; } +}; diff --git a/src/xrGame/ui/UIEncyclopediaWnd.cpp b/src/xrGame/ui/UIEncyclopediaWnd.cpp new file mode 100644 index 00000000000..4ee20528e54 --- /dev/null +++ b/src/xrGame/ui/UIEncyclopediaWnd.cpp @@ -0,0 +1,275 @@ +//============================================================================= +// Filename: UIEncyclopediaWnd.cpp +// Created by Roman E. Marchenko, vortex@gsc-game.kiev.ua +// Copyright 2004. GSC Game World +// --------------------------------------------------------------------------- +// Encyclopedia window +//============================================================================= + +#include "StdAfx.h" +#include "UIEncyclopediaWnd.h" +#include "UIXmlInit.h" +#include "xrUICore/Windows/UIFrameWindow.h" +#include "xrUICore/Windows/UIFrameLineWnd.h" +#include "xrUICore/Static/UIAnimatedStatic.h" +#include "xrUICore/ListWnd/UIListWnd.h" +#include "xrUICore/ScrollView/UIScrollView.h" +#include "UITreeViewItem.h" +#include "UIPdaAux.h" +#include "UIEncyclopediaArticleWnd.h" +#include "../encyclopedia_article.h" +#include "../alife_registry_wrappers.h" +#include "../Actor.h" +#include "Common/object_broker.h" +#include "xrEngine/StringTable/StringTable.h" + +#define ENCYCLOPEDIA_DIALOG_XML "encyclopedia.xml" + +CUIEncyclopediaWnd::CUIEncyclopediaWnd() : CUIWindow("CUIEncyclopediaWnd") { prevArticlesCount = 0; } + +CUIEncyclopediaWnd::~CUIEncyclopediaWnd() { DeleteArticles(); } + +void CUIEncyclopediaWnd::Init() +{ + CUIXml uiXml; + bool xml_result = uiXml.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, ENCYCLOPEDIA_DIALOG_XML); + R_ASSERT3(xml_result, "xml file not found", ENCYCLOPEDIA_DIALOG_XML); + + CUIXmlInit xml_init; + + xml_init.InitWindow(uiXml, "main_wnd", 0, this); + + // Load xml data + UIEncyclopediaIdxBkg = xr_new(); + UIEncyclopediaIdxBkg->SetAutoDelete(true); + AttachChild(UIEncyclopediaIdxBkg); + xml_init.InitFrameWindow(uiXml, "right_frame_window", 0, UIEncyclopediaIdxBkg); + + xml_init.InitFont(uiXml, "tree_item_font", 0, m_uTreeItemColor, m_pTreeItemFont); + R_ASSERT(m_pTreeItemFont); + xml_init.InitFont(uiXml, "tree_root_font", 0, m_uTreeRootColor, m_pTreeRootFont); + R_ASSERT(m_pTreeRootFont); + + UIEncyclopediaIdxHeader = xr_new(); + UIEncyclopediaIdxHeader->SetAutoDelete(true); + UIEncyclopediaIdxBkg->AttachChild(UIEncyclopediaIdxHeader); + xml_init.InitFrameLine(uiXml, "right_frame_line", 0, UIEncyclopediaIdxHeader); + + if (uiXml.NavigateToNode("a_static")) + { + UIAnimation = xr_new(); + UIAnimation->SetAutoDelete(true); + UIEncyclopediaIdxHeader->AttachChild(UIAnimation); + xml_init.InitAnimatedStatic(uiXml, "a_static", 0, UIAnimation); + } + + UIEncyclopediaInfoBkg = xr_new(); + UIEncyclopediaInfoBkg->SetAutoDelete(true); + AttachChild(UIEncyclopediaInfoBkg); + xml_init.InitFrameWindow(uiXml, "left_frame_window", 0, UIEncyclopediaInfoBkg); + + UIEncyclopediaInfoHeader = xr_new(); + UIEncyclopediaInfoHeader->SetAutoDelete(true); + UIEncyclopediaInfoBkg->AttachChild(UIEncyclopediaInfoHeader); + + xml_init.InitFrameLine(uiXml, "left_frame_line", 0, UIEncyclopediaInfoHeader); + UIEncyclopediaInfoHeader->GetTitleText(true)->SetEllipsis(true); + + UIArticleHeader = xr_new(); + UIArticleHeader->SetAutoDelete(true); + UIEncyclopediaInfoBkg->AttachChild(UIArticleHeader); + xml_init.InitStatic(uiXml, "article_header_static", 0, UIArticleHeader); + + UIIdxList = xr_new(); + UIIdxList->SetAutoDelete(true); + UIEncyclopediaIdxBkg->AttachChild(UIIdxList); + xml_init.InitListWnd(uiXml, "idx_list", 0, UIIdxList); + UIIdxList->SetMessageTarget(this); + UIIdxList->EnableScrollBar(true); + + UIInfoList = xr_new(); + UIInfoList->SetAutoDelete(true); + UIEncyclopediaInfoBkg->AttachChild(UIInfoList); + xml_init.InitScrollView(uiXml, "info_list", 0, UIInfoList); + + CUIXmlInit::InitAutoStaticGroup(uiXml, "left_auto_static", 0, UIEncyclopediaInfoBkg); + CUIXmlInit::InitAutoStaticGroup(uiXml, "right_auto_static", 0, UIEncyclopediaIdxBkg); +} + +#include "xrEngine/StringTable/StringTable.h" +void CUIEncyclopediaWnd::SendMessage(CUIWindow* pWnd, s16 msg, void* pData) +{ + if (UIIdxList == pWnd && LIST_ITEM_CLICKED == msg) + { + CUITreeViewItem* pTVItem = static_cast(pData); + R_ASSERT(pTVItem); + + if (pTVItem->vSubItems.size()) + { + auto& A = m_ArticlesDB[pTVItem->vSubItems[0]->GetValue()]; + + xr_string caption = ALL_PDA_HEADER_PREFIX; + caption += "/"; + caption += CStringTable().translate(A.data()->group).c_str(); + + UIEncyclopediaInfoHeader->GetTitleText(true)->SetText(caption.c_str()); + UIArticleHeader->SetTextST(A.data()->group.c_str()); + SetCurrentArtice(NULL); + } + else + { + auto& A = m_ArticlesDB[pTVItem->GetValue()]; + xr_string caption = ALL_PDA_HEADER_PREFIX; + caption += "/"; + caption += CStringTable().translate(A.data()->group).c_str(); + caption += "/"; + caption += CStringTable().translate(A.data()->name).c_str(); + + UIEncyclopediaInfoHeader->GetTitleText(true)->SetText(caption.c_str()); + SetCurrentArtice(pTVItem); + UIArticleHeader->SetTextST(A.data()->name.c_str()); + } + } + + inherited::SendMessage(pWnd, msg, pData); +} + +void CUIEncyclopediaWnd::Draw() +{ + UpdateArticles(); + inherited::Draw(); +} + +void CUIEncyclopediaWnd::ReloadArticles() +{ + if (Actor() && Actor()->encyclopedia_registry->registry().objects_ptr()->size() < prevArticlesCount) + ResetArticles(); + else + m_flags.set(eNeedReload, TRUE); +} + +void CUIEncyclopediaWnd::Show(bool status) +{ + if (status) + ReloadArticles(); + + inherited::Show(status); +} + +bool CUIEncyclopediaWnd::HasArticle(shared_str id) +{ + ReloadArticles(); + + for (auto& Art : m_ArticlesDB) + if (Art.Id() == id) + return true; + + return false; +} + +void CUIEncyclopediaWnd::DeleteArticles() +{ + UIIdxList->RemoveAll(); + m_ArticlesDB.clear(); +} + +void CUIEncyclopediaWnd::SetCurrentArtice(CUITreeViewItem* pTVItem) +{ + UIInfoList->ScrollToBegin(); + UIInfoList->Clear(); + + if (!pTVItem) + return; + + // для начала проверим, что нажатый элемент не рутовый + if (!pTVItem->IsRoot()) + { + CUIEncyclopediaArticleWnd* article_info = xr_new(); + article_info->Init("encyclopedia_item.xml", "encyclopedia_wnd:objective_item"); + article_info->SetArticle(&m_ArticlesDB[pTVItem->GetValue()]); + UIInfoList->AddWindow(article_info, true); + + // Пометим как прочитанную + if (!pTVItem->IsArticleReaded()) + { + if (Actor()->encyclopedia_registry->registry().objects_ptr()) + { + for (ARTICLE_VECTOR::iterator it = Actor()->encyclopedia_registry->registry().objects().begin(); it != Actor()->encyclopedia_registry->registry().objects().end(); + it++) + { + if (ARTICLE_DATA::eEncyclopediaArticle == it->article_type && m_ArticlesDB[pTVItem->GetValue()].Id() == it->article_id) + { + it->readed = true; + break; + } + } + } + } + } +} + +CEncyclopediaArticle* CUIEncyclopediaWnd::AddArticle(shared_str article_id, bool bReaded) +{ + for (auto& Art : m_ArticlesDB) + if (Art.Id() == article_id) + return nullptr; + + // Добавляем элемент + auto& a = m_ArticlesDB.emplace_back(); + a.Load(article_id); + return &a; +} + +void CUIEncyclopediaWnd::Reset() +{ + inherited::Reset(); + ResetArticles(); +} + +void CUIEncyclopediaWnd::ResetArticles() +{ + m_flags.set(eNeedReload, TRUE); + DeleteArticles(); + prevArticlesCount = 0; +} + +void CUIEncyclopediaWnd::FillEncyclopedia() +{ + SetCurrentArtice(nullptr); + UIEncyclopediaInfoHeader->GetTitleText(true)->SetText(""); + UIArticleHeader->SetText(""); + + ResetArticles(); + UpdateArticles(); +} + +void CUIEncyclopediaWnd::UpdateArticles() +{ + if (!m_flags.test(eNeedReload) || !Actor()) + return; + + const ARTICLE_VECTOR* articles = Actor()->encyclopedia_registry->registry().objects_ptr(); + if (!articles) + return; + + if (articles->size() < prevArticlesCount) + ResetArticles(); + + auto it = articles->begin(); + std::advance(it, prevArticlesCount); + for (; it != articles->end(); ++it) + { + if (it->article_type != ARTICLE_DATA::eEncyclopediaArticle) + continue; + + CEncyclopediaArticle* article = AddArticle(it->article_id, it->readed); + if (!article) + continue; + + CreateTreeBranch(article->data()->group, article->data()->name, UIIdxList, m_ArticlesDB.size() - 1, + m_pTreeRootFont, m_uTreeRootColor, m_pTreeItemFont, m_uTreeItemColor, it->readed); + } + + prevArticlesCount = articles->size(); + m_flags.set(eNeedReload, FALSE); +} diff --git a/src/xrGame/ui/UIEncyclopediaWnd.h b/src/xrGame/ui/UIEncyclopediaWnd.h new file mode 100644 index 00000000000..2e6d6108aed --- /dev/null +++ b/src/xrGame/ui/UIEncyclopediaWnd.h @@ -0,0 +1,78 @@ +//============================================================================= +// Filename: UIEncyclopediaWnd.h +// Created by Roman E. Marchenko, vortex@gsc-game.kiev.ua +// Copyright 2004. GSC Game World +// --------------------------------------------------------------------------- +// Encyclopedia window +//============================================================================= + +#pragma once + +#include "xrUICore/Windows/UIWindow.h" +#include "../encyclopedia_article_defs.h" + +class CEncyclopediaArticle; +class CUIFrameWindow; +class CUIFrameLineWnd; +class CUIAnimatedStatic; +class CUIStatic; +class CUIListWnd; +class CUIEncyclopediaCore; +class CUIScrollView; +class CUITreeViewItem; + +class CUIEncyclopediaWnd : public CUIWindow +{ +private: + typedef CUIWindow inherited; + enum + { + eNeedReload = (1 << 0), + }; + Flags16 m_flags; + +public: + CUIEncyclopediaWnd(); + virtual ~CUIEncyclopediaWnd(); + + virtual void Init(); + virtual void Show(bool status); + virtual void SendMessage(CUIWindow* pWnd, s16 msg, void* pData = NULL); + virtual void Draw(); + + CEncyclopediaArticle* AddArticle(shared_str, bool); + void DeleteArticles(); + bool HasArticle(shared_str); + + void ReloadArticles(); + virtual void Reset(); + + void FillEncyclopedia(); + void UpdateArticles(); + void ResetArticles(); + + pcstr GetDebugType() override { return "CUIEncyclopediaWnd"; } + +protected: + u32 prevArticlesCount; + // Элементы графического оформления + CUIFrameWindow* UIEncyclopediaIdxBkg; + CUIFrameWindow* UIEncyclopediaInfoBkg; + CUIFrameLineWnd* UIEncyclopediaIdxHeader; + CUIFrameLineWnd* UIEncyclopediaInfoHeader; + CUIAnimatedStatic* UIAnimation; + CUIStatic* UIArticleHeader; + + // Хранилище статей + xr_vector m_ArticlesDB; + + CGameFont* m_pTreeRootFont; + u32 m_uTreeRootColor; + CGameFont* m_pTreeItemFont; + u32 m_uTreeItemColor; + + CUIListWnd* UIIdxList; + CUIScrollView* UIInfoList; + + void SetCurrentArtice(CUITreeViewItem* pTVItem); +}; diff --git a/src/xrGame/ui/UIEventsWnd.cpp b/src/xrGame/ui/UIEventsWnd.cpp new file mode 100644 index 00000000000..38007a651da --- /dev/null +++ b/src/xrGame/ui/UIEventsWnd.cpp @@ -0,0 +1,306 @@ +#include "StdAfx.h" +#include "UIEventsWnd.h" +#include "xrUICore/Windows/UIFrameWindow.h" +#include "xrUICore/Windows/UIFrameLineWnd.h" +#include "xrUICore/Static/UIAnimatedStatic.h" +#include "UIMapWnd.h" +#include "xrUICore/ScrollView/UIScrollView.h" +#include "xrUICore/TabControl/UITabControl.h" +#include "UITaskDescrWnd.h" +#include "xrUICore/Buttons/UI3tButton.h" +#include "../HUDManager.h" +#include "../Level.h" +#include "../Actor.h" +#include "../GametaskManager.h" +#include "../GameTask.h" +#include "../map_manager.h" +#include "../map_location.h" +#include "xrEngine/StringTable/StringTable.h" +#include "UITaskItem.h" +#include "../alife_registry_wrappers.h" +#include "../encyclopedia_article.h" + +CUIEventsWnd::CUIEventsWnd() : CUIWindow("CUIEventsWnd") { m_flags.zero(); } + +CUIEventsWnd::~CUIEventsWnd() +{ + delete_data(m_UIMapWnd); + delete_data(m_UITaskInfoWnd); +} + +void CUIEventsWnd::Init() +{ + CUIXml uiXml; + bool xml_result = uiXml.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "pda_events.xml"); + R_ASSERT3(xml_result, "xml file not found", "pda_events.xml"); + + CUIXmlInit xml_init; + xml_init.InitWindow(uiXml, "main_wnd", 0, this); + + m_UILeftFrame = xr_new(); + m_UILeftFrame->SetAutoDelete(true); + AttachChild(m_UILeftFrame); + xml_init.InitFrameWindow(uiXml, "main_wnd:left_frame", 0, m_UILeftFrame); + + m_UILeftHeader = xr_new(); + m_UILeftHeader->SetAutoDelete(true); + m_UILeftFrame->AttachChild(m_UILeftHeader); + xml_init.InitFrameLine(uiXml, "main_wnd:left_frame:left_frame_header", 0, m_UILeftHeader); + + if (uiXml.NavigateToNode("main_wnd:left_frame:left_frame_header:anim_static")) + { + m_UIAnimation = xr_new(); + m_UIAnimation->SetAutoDelete(true); + xml_init.InitAnimatedStatic(uiXml, "main_wnd:left_frame:left_frame_header:anim_static", 0, m_UIAnimation); + m_UILeftHeader->AttachChild(m_UIAnimation); + } + + m_UIRightWnd = xr_new(); + m_UIRightWnd->SetAutoDelete(true); + AttachChild(m_UIRightWnd); + xml_init.InitWindow(uiXml, "main_wnd:right_frame", 0, m_UIRightWnd); + + m_UIMapWnd = xr_new(nullptr); + m_UIMapWnd->SetAutoDelete(false); + m_UIMapWnd->Init("pda_events.xml", "main_wnd:right_frame:map_wnd", false); + + m_UITaskInfoWnd = xr_new(); + m_UITaskInfoWnd->SetAutoDelete(false); + m_UITaskInfoWnd->Init(&uiXml, "main_wnd:right_frame:task_descr_view"); + + m_ListWnd = xr_new(); + m_ListWnd->SetAutoDelete(true); + m_UILeftFrame->AttachChild(m_ListWnd); + xml_init.InitScrollView(uiXml, "main_wnd:left_frame:list", 0, m_ListWnd); + + m_TaskFilter = xr_new(); + m_TaskFilter->SetAutoDelete(true); + m_UILeftFrame->AttachChild(m_TaskFilter); + xml_init.InitTabControl(uiXml, "main_wnd:left_frame:filter_tab", 0, m_TaskFilter); + m_TaskFilter->SetWindowName("filter_tab"); + Register(m_TaskFilter); + AddCallbackStr("filter_tab", TAB_CHANGED, fastdelegate::MakeDelegate(this, &CUIEventsWnd::OnFilterChanged)); + + m_currFilter = eActiveTask; + SetDescriptionMode(true); + + m_ui_task_item_xml.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, "job_item.xml"); +} + +void CUIEventsWnd::Update() +{ + if (m_flags.test(flNeedReload)) + { + ReloadList(false); + m_flags.set(flNeedReload, FALSE); + } + inherited::Update(); +} + +void CUIEventsWnd::Draw() { inherited::Draw(); } + +void CUIEventsWnd::SendMessage(CUIWindow* pWnd, s16 msg, void* pData) { CUIWndCallback::OnEvent(pWnd, msg, pData); } + +void CUIEventsWnd::OnFilterChanged(CUIWindow* w, void*) +{ + m_currFilter = (ETaskFilters)m_TaskFilter->GetActiveIndex(); + ReloadList(false); + if (!GetDescriptionMode()) + SetDescriptionMode(true); +} + +void CUIEventsWnd::Reload() { m_flags.set(flNeedReload, TRUE); } + +void CUIEventsWnd::ReloadList(bool bClearOnly) +{ + m_ListWnd->Clear(); + if (bClearOnly) + return; + if (!g_actor) + return; + + vGameTasks& tasks = Level().GameTaskManager().GetGameTasks(); + xr_vector game_tasks; + for (const auto& it : tasks) + { + CGameTask* task = it.game_task; + R_ASSERT(task); + R_ASSERT(task->GetObjectivesCount() > 0); + if (Filter(task)) + game_tasks.push_back(task); + } + + if (m_currFilter == eActiveTask) + std::sort(game_tasks.begin(), game_tasks.end(), [](const auto& a, const auto& b) { + if (a->m_priority == b->m_priority) + return a->m_ReceiveTime > b->m_ReceiveTime; + return a->m_priority < b->m_priority; + }); + + for (const auto& task : game_tasks) + { + CUISocTaskItem* pTaskItem = NULL; + /* + if(task->m_Objectives[0].TaskState()==eTaskUserDefined) + { + VERIFY (task->m_Objectives.size()==1); + pTaskItem = xr_new(this); + pTaskItem->SetGameTask (task, 0); + m_ListWnd->AddWindow (pTaskItem,true); + }else + */ + const u32 visible_objectives = task->GetObjectivesCount(); + + for (u32 i = 0; i < visible_objectives; ++i) + { + if (i == 0) + { + pTaskItem = xr_new(this); + } + else + { + pTaskItem = xr_new(this); + } + pTaskItem->SetGameTask(task, (u16)i); + m_ListWnd->AddWindow(pTaskItem, true); + } + } +} + +void CUIEventsWnd::Show(bool status) +{ + inherited::Show(status); + + if (GetDescriptionMode()) + m_UIMapWnd->Show(status); + else + m_UITaskInfoWnd->Show(status); + + ReloadList(status == false); +} + +bool CUIEventsWnd::Filter(CGameTask* t) +{ + const ETaskState task_state = t->ObjectiveState(ROOT_TASK_OBJECTIVE); + // bool bprimary_only = m_primary_or_all_filter_btn->GetCheck(); + + return (false /*m_currFilter==eOwnTask && task_state==eTaskUserDefined*/) || + ((true /*!bprimary_only || (bprimary_only && t->m_is_task_general)*/) && + ((m_currFilter == eAccomplishedTask && task_state == eTaskStateCompleted) || (m_currFilter == eFailedTask && task_state == eTaskStateFail) || + (m_currFilter == eActiveTask && task_state == eTaskStateInProgress))); +} + +void CUIEventsWnd::SetDescriptionMode(bool bMap) +{ + if (bMap) + { + m_descriptionTask = nullptr; + if (m_UIRightWnd->IsChild(m_UITaskInfoWnd)) + m_UIRightWnd->DetachChild(m_UITaskInfoWnd); + if (!m_UIRightWnd->IsChild(m_UIMapWnd)) + m_UIRightWnd->AttachChild(m_UIMapWnd); + } + else + { + if (m_UIRightWnd->IsChild(m_UIMapWnd)) + m_UIRightWnd->DetachChild(m_UIMapWnd); + if (!m_UIRightWnd->IsChild(m_UITaskInfoWnd)) + m_UIRightWnd->AttachChild(m_UITaskInfoWnd); + } + m_flags.set(flMapMode, bMap); +} + +bool CUIEventsWnd::GetDescriptionMode() { return !!m_flags.test(flMapMode); } + +bool CUIEventsWnd::IsTaskDescriptionShown(const CGameTask* task) const { return m_descriptionTask == task; } + +void CUIEventsWnd::ShowDescription(CGameTask* t, int idx) +{ + if (GetDescriptionMode()) + { // map + SGameTaskObjective& o = t->Objective(idx); + CMapLocation* ml = o.LinkedMapLocation(); + + if (ml && ml->SpotEnabled()) + { + ml->CalcPosition(); + m_UIMapWnd->SetTargetMap(ml->GetLevelName(), ml->GetPosition(), true); + } + + int sz = m_ListWnd->GetSize(); + + for (int i = 0; i < sz; ++i) + { + CUISocTaskItem* itm = (CUISocTaskItem*)m_ListWnd->GetItem(i); + + if ((itm->GameTask() == t) && (itm->ObjectiveIdx() == idx)) + itm->MarkSelected(true); + else + itm->MarkSelected(false); + } + } + else + { // articles + m_descriptionTask = t; + const SGameTaskObjective& rootObjective = t->Objective(ROOT_TASK_OBJECTIVE); + + m_UITaskInfoWnd->ClearAll(); + + // The task owns its explicit description article. Load it directly so + // old saves also work when their article registry is incomplete. + const shared_str explicitArticle = rootObjective.m_article_id; + if (explicitArticle.size()) + m_UITaskInfoWnd->AddArticle(explicitArticle.c_str()); + + if (Actor()->encyclopedia_registry->registry().objects_ptr()) + { + ARTICLE_VECTOR::const_iterator it = Actor()->encyclopedia_registry->registry().objects_ptr()->begin(); + + for (; it != Actor()->encyclopedia_registry->registry().objects_ptr()->end(); ++it) + { + if (ARTICLE_DATA::eTaskArticle == it->article_type) + { + if (explicitArticle.size() && it->article_id == explicitArticle) + continue; + + CEncyclopediaArticle A; + A.Load(it->article_id); + + if (t->m_ID == A.data()->group) + { + m_UITaskInfoWnd->AddArticle(&A); + } + } + } + } + } +} + +bool CUIEventsWnd::ItemHasDescription(CUISocTaskItem* itm) +{ + if (itm->ObjectiveIdx() == 0) // root + { + CGameTask* task = itm->GameTask(); + const TASK_OBJECTIVE_ID count = task->GetObjectivesCount(); + for (TASK_OBJECTIVE_ID index = 0; index < count; ++index) + { + if (task->Objective(index).LinkedMapLocation()) + return true; + } + return false; + } + else + { + SGameTaskObjective* obj = itm->Objective(); + CMapLocation* ml = obj->LinkedMapLocation(); + bool bHasLocation = (NULL != ml); + bool bIsMapMode = GetDescriptionMode(); + bool b = (bIsMapMode && bHasLocation && ml->SpotEnabled()); + return b; + } +} +void CUIEventsWnd::Reset() +{ + inherited::Reset(); + Reload(); +} diff --git a/src/xrGame/ui/UIEventsWnd.h b/src/xrGame/ui/UIEventsWnd.h new file mode 100644 index 00000000000..b576eed1ae7 --- /dev/null +++ b/src/xrGame/ui/UIEventsWnd.h @@ -0,0 +1,70 @@ +#pragma once +#include "xrUICore/Windows/UIWindow.h" +#include "xrUICore/Callbacks/UIWndCallback.h" +#include "UIXmlInit.h" + +class CUIFrameWindow; +class CUIFrameLineWnd; +class CUIAnimatedStatic; +class CUIMapWnd; +class CUI3tButton; +class CUITabControl; +class CGameTask; +class CUITaskDescrWnd; +class CUIScrollView; +class CUISocTaskItem; + +class CUIEventsWnd : public CUIWindow, public CUIWndCallback +{ + typedef CUIWindow inherited; + enum ETaskFilters + { + eActiveTask = 0, + eAccomplishedTask, + eFailedTask, + //. eOwnTask, + eMaxTask + }; + enum EEventWndFlags + { + flNeedReload = (1 << 0), + flMapMode = (1 << 1), + }; + Flags16 m_flags; + ETaskFilters m_currFilter; + CUIFrameWindow* m_UILeftFrame; + CUIWindow* m_UIRightWnd; + CUIFrameLineWnd* m_UILeftHeader; + CUIAnimatedStatic* m_UIAnimation; + CUIMapWnd* m_UIMapWnd; + CUITaskDescrWnd* m_UITaskInfoWnd; + CUIScrollView* m_ListWnd; + CUITabControl* m_TaskFilter; + CGameTask* m_descriptionTask{}; + + bool Filter(CGameTask* t); + void OnFilterChanged(CUIWindow*, void*); + void ReloadList(bool bClearOnly); + +public: + void SetDescriptionMode(bool bMap); + bool GetDescriptionMode(); + bool IsTaskDescriptionShown(const CGameTask* task) const; + void ShowDescription(CGameTask* t, int idx); + bool ItemHasDescription(CUISocTaskItem*); + +public: + CUIEventsWnd(); + virtual ~CUIEventsWnd(); + virtual void SendMessage(CUIWindow* pWnd, s16 msg, void* pData); + void Init(); + virtual void Update(); + virtual void Draw(); + virtual void Show(bool status); + void Reload(); + virtual void Reset(); + + pcstr GetDebugType() override { return "CUIEventsWnd"; } + + CUIXml m_ui_task_item_xml; +}; diff --git a/src/xrGame/ui/UIHudStatesWnd.cpp b/src/xrGame/ui/UIHudStatesWnd.cpp index 9cfcb6307e1..c519125e96f 100644 --- a/src/xrGame/ui/UIHudStatesWnd.cpp +++ b/src/xrGame/ui/UIHudStatesWnd.cpp @@ -78,13 +78,25 @@ void CUIHudStatesWnd::InitFromXml(CUIXml& xml, LPCSTR path) { ZoneScoped; - CUIXmlInit::InitWindow(xml, path, 0, this); XML_NODE stored_root = xml.GetLocalRoot(); - XML_NODE new_root = xml.NavigateToNode(path, 0); - xml.SetLocalRoot(new_root); - m_back = UIHelper::CreateStatic(xml, "back", this); + if (new_root) + { + CUIXmlInit::InitWindow(xml, path, 0, this); + xml.SetLocalRoot(new_root); + } + else if (ShadowOfChernobylMode) + { + SetWndRect({ 0, 0, UI_BASE_WIDTH, UI_BASE_HEIGHT }); + xml.SetLocalRoot(xml.GetRoot()); + } + else + { + CUIXmlInit::InitWindow(xml, path, 0, this); + } + + m_back = UIHelper::CreateStatic(xml, "back", this, false); m_back_v = UIHelper::CreateStatic(xml, "back_v", this, false); // XXX: replace with UIHelper @@ -136,12 +148,13 @@ void CUIHudStatesWnd::InitFromXml(CUIXml& xml, LPCSTR path) m_resist_back[ALife::infl_psi] = UIHelper::CreateStatic(xml, "resist_back_psi", this, false); // electra = no has CStatic!! - m_indik[ALife::infl_rad] = UIHelper::CreateStatic(xml, "indik_rad", this); - m_indik[ALife::infl_fire] = UIHelper::CreateStatic(xml, "indik_fire", this); - m_indik[ALife::infl_acid] = UIHelper::CreateStatic(xml, "indik_acid", this); - m_indik[ALife::infl_psi] = UIHelper::CreateStatic(xml, "indik_psi", this); + m_indik[ALife::infl_rad] = UIHelper::CreateStatic(xml, "indik_rad", this, false); + m_indik[ALife::infl_fire] = UIHelper::CreateStatic(xml, "indik_fire", this, false); + m_indik[ALife::infl_acid] = UIHelper::CreateStatic(xml, "indik_acid", this, false); + m_indik[ALife::infl_psi] = UIHelper::CreateStatic(xml, "indik_psi", this, false); - m_lanim_name = xml.ReadAttrib("indik_rad", 0, "light_anim", ""); + if (m_indik[ALife::infl_rad]) + m_lanim_name = xml.ReadAttrib("indik_rad", 0, "light_anim", ""); m_ui_weapon_sign_ammo = UIHelper::CreateStatic(xml, "static_ammo", weaponsParent, false); //m_ui_weapon_sign_ammo->SetEllipsis( CUIStatic::eepEnd, 2 ); @@ -150,7 +163,7 @@ void CUIHudStatesWnd::InitFromXml(CUIXml& xml, LPCSTR path) m_ui_weapon_fmj_ammo = UIHelper::CreateStatic(xml, "static_fmj_ammo", this, false); m_ui_weapon_ap_ammo = UIHelper::CreateStatic(xml, "static_ap_ammo", this, false); m_ui_weapon_third_ammo = UIHelper::CreateStatic(xml, "static_third_ammo", this, false); //Alundaio: Option to display a third ammo type - m_fire_mode = UIHelper::CreateStatic(xml, "static_fire_mode", this); + m_fire_mode = UIHelper::CreateStatic(xml, "static_fire_mode", this, false); m_ui_grenade = UIHelper::CreateStatic(xml, "static_grenade", this, false); m_ui_weapon_icon = UIHelper::CreateStatic(xml, "static_wpn_icon", weaponsParent); @@ -351,7 +364,8 @@ void CUIHudStatesWnd::UpdateActiveItemInfo(CActor* actor) item->GetBriefInfo(m_item_info); // UIWeaponBack.SetText ( str_name.c_str() ); - m_fire_mode->SetText(m_item_info.fire_mode.c_str()); + if (m_fire_mode) + m_fire_mode->SetText(m_item_info.fire_mode.c_str()); SetAmmoIcon(m_item_info.icon.c_str()); if (m_ui_weapon_cur_ammo) @@ -416,7 +430,8 @@ void CUIHudStatesWnd::UpdateActiveItemInfo(CActor* actor) } } - m_fire_mode->Show(true); + if (m_fire_mode) + m_fire_mode->Show(true); if (m_ui_grenade) { @@ -462,7 +477,8 @@ void CUIHudStatesWnd::UpdateActiveItemInfo(CActor* actor) if (m_ui_weapon_sign_ammo) m_ui_weapon_sign_ammo->Show(false); - m_fire_mode->Show(false); + if (m_fire_mode) + m_fire_mode->Show(false); if (m_ui_grenade) m_ui_grenade->Show(false); @@ -709,6 +725,9 @@ void CUIHudStatesWnd::UpdateIndicatorType(CActor* actor, ALife::EInfluenceType t return; } + if (!m_indik[type]) + return; + constexpr u32 c_white = color_rgba(255, 255, 255, 255); constexpr u32 c_green = color_rgba(0, 255, 0, 255); constexpr u32 c_yellow = color_rgba(255, 255, 0, 255); @@ -824,6 +843,12 @@ void CUIHudStatesWnd::UpdateIndicatorType(CActor* actor, ALife::EInfluenceType t } void CUIHudStatesWnd::SwitchLA(bool state, ALife::EInfluenceType type) { + if (!m_indik[type]) + { + m_cur_state_LA[type] = false; + return; + } + if (state == m_cur_state_LA[type]) { return; @@ -859,16 +884,16 @@ void CUIHudStatesWnd::DrawZoneIndicators() UpdateIndicators(actor); - if (m_indik[ALife::infl_rad]->IsShown()) + if (m_indik[ALife::infl_rad] && m_indik[ALife::infl_rad]->IsShown()) m_indik[ALife::infl_rad]->Draw(); - if (m_indik[ALife::infl_fire]->IsShown()) + if (m_indik[ALife::infl_fire] && m_indik[ALife::infl_fire]->IsShown()) m_indik[ALife::infl_fire]->Draw(); - if (m_indik[ALife::infl_acid]->IsShown()) + if (m_indik[ALife::infl_acid] && m_indik[ALife::infl_acid]->IsShown()) m_indik[ALife::infl_acid]->Draw(); - if (m_indik[ALife::infl_psi]->IsShown()) + if (m_indik[ALife::infl_psi] && m_indik[ALife::infl_psi]->IsShown()) m_indik[ALife::infl_psi]->Draw(); } @@ -881,6 +906,9 @@ void CUIHudStatesWnd::FakeUpdateIndicatorType(u8 t, float power) return; } + if (!m_indik[type]) + return; + CActor* actor = smart_cast(Level().CurrentViewEntity()); if (!actor) return; diff --git a/src/xrGame/ui/UIHudStatesWnd.h b/src/xrGame/ui/UIHudStatesWnd.h index 4f4c7358ab9..13753f8cbc2 100644 --- a/src/xrGame/ui/UIHudStatesWnd.h +++ b/src/xrGame/ui/UIHudStatesWnd.h @@ -19,38 +19,38 @@ class CUIHudStatesWnd final : public CUIWindow typedef CUIWindow inherited; //- typedef ALife::EInfluenceType EIndicatorType; - CUIStatic* m_back; - CUIStatic* m_back_v; - CUIStatic* m_back_over_arrow; - CUIStatic* m_static_health; - CUIStatic* m_static_armor; - CUIStatic* m_static_weapon; + CUIStatic* m_back{}; + CUIStatic* m_back_v{}; + CUIStatic* m_back_over_arrow{}; + CUIStatic* m_static_health{}; + CUIStatic* m_static_armor{}; + CUIStatic* m_static_weapon{}; xr_map m_resist_back; xr_map m_indik; - CUIStatic* m_ui_weapon_cur_ammo; - CUIStatic* m_ui_weapon_fmj_ammo; - CUIStatic* m_ui_weapon_ap_ammo; - CUIStatic* m_ui_weapon_third_ammo; //Alundaio - CUIStatic* m_fire_mode; - CUIStatic* m_ui_grenade; + CUIStatic* m_ui_weapon_cur_ammo{}; + CUIStatic* m_ui_weapon_fmj_ammo{}; + CUIStatic* m_ui_weapon_ap_ammo{}; + CUIStatic* m_ui_weapon_third_ammo{}; //Alundaio + CUIStatic* m_fire_mode{}; + CUIStatic* m_ui_grenade{}; II_BriefInfo m_item_info; - CUIStatic* m_ui_weapon_sign_ammo; - CUIStatic* m_ui_weapon_icon; + CUIStatic* m_ui_weapon_sign_ammo{}; + CUIStatic* m_ui_weapon_icon{}; Frect m_ui_weapon_icon_rect; - CUIProgressBar* m_ui_health_bar; - CUIProgressBar* m_ui_armor_bar; - CUIProgressBar* m_ui_stamina_bar; + CUIProgressBar* m_ui_health_bar{}; + CUIProgressBar* m_ui_armor_bar{}; + CUIProgressBar* m_ui_stamina_bar{}; - CUIProgressShape* m_progress_self; - CUIStatic* m_radia_damage; + CUIProgressShape* m_progress_self{}; + CUIStatic* m_radia_damage{}; UI_Arrow* m_arrow{}; UI_Arrow* m_arrow_shadow{}; - CUIStatic* m_bleeding; + CUIStatic* m_bleeding{}; /* CUIStatic* m_bleeding_lev1; CUIStatic* m_bleeding_lev2; diff --git a/src/xrGame/ui/UIMainIngameWnd.cpp b/src/xrGame/ui/UIMainIngameWnd.cpp index d0c83f5c0fb..652168bde4c 100644 --- a/src/xrGame/ui/UIMainIngameWnd.cpp +++ b/src/xrGame/ui/UIMainIngameWnd.cpp @@ -76,7 +76,13 @@ void CUIMainIngameWnd::Init() CUIXml uiXml; uiXml.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, MAININGAME_XML); - CUIXmlInit::InitWindow(uiXml, "main", 0, this); + if (!CUIXmlInit::InitWindow(uiXml, "main", 0, this, false)) + { + if (ShadowOfChernobylMode) + SetWndRect({ 0, 0, UI_BASE_WIDTH, UI_BASE_HEIGHT }); + else + CUIXmlInit::InitWindow(uiXml, "main", 0, this); + } Enable(false); @@ -220,7 +226,7 @@ void CUIMainIngameWnd::Init() AttachChild(UIMotionIcon); } - UIStaticDiskIO = UIHelper::CreateStatic(uiXml, "disk_io", this); + UIStaticDiskIO = UIHelper::CreateStatic(uiXml, "disk_io", this, false); if (IsGameTypeSingle() && uiXml.NavigateToNode("artefact_panel", 0)) { @@ -266,17 +272,20 @@ void CUIMainIngameWnd::Draw() CActor* pActor = smart_cast(Level().CurrentViewEntity()); // show IO icon - bool IOActive = (FS.dwOpenCounter > 0); - if (IOActive) - UIStaticDiskIO_start_time = Device.fTimeGlobal; - - if ((UIStaticDiskIO_start_time + 1.0f) < Device.fTimeGlobal) - UIStaticDiskIO->Show(false); - else + if (UIStaticDiskIO) { - u32 alpha = clampr(iFloor(255.f * (1.f - (Device.fTimeGlobal - UIStaticDiskIO_start_time) / 1.f)), 0, 255); - UIStaticDiskIO->Show(true); - UIStaticDiskIO->SetTextureColor(color_rgba(255, 255, 255, alpha)); + bool IOActive = (FS.dwOpenCounter > 0); + if (IOActive) + UIStaticDiskIO_start_time = Device.fTimeGlobal; + + if ((UIStaticDiskIO_start_time + 1.0f) < Device.fTimeGlobal) + UIStaticDiskIO->Show(false); + else + { + u32 alpha = clampr(iFloor(255.f * (1.f - (Device.fTimeGlobal - UIStaticDiskIO_start_time) / 1.f)), 0, 255); + UIStaticDiskIO->Show(true); + UIStaticDiskIO->SetTextureColor(color_rgba(255, 255, 255, alpha)); + } } FS.dwOpenCounter = 0; diff --git a/src/xrGame/ui/UIMap.cpp b/src/xrGame/ui/UIMap.cpp index a52bfde1f3f..847077ed520 100644 --- a/src/xrGame/ui/UIMap.cpp +++ b/src/xrGame/ui/UIMap.cpp @@ -21,6 +21,13 @@ CUICustomMap::CUICustomMap() : CUIStatic("Custom Map") void CUICustomMap::Initialize(shared_str name, LPCSTR sh_name) { + if (ShadowOfChernobylMode && pGameIni->section_exist(name.c_str()) && + pGameIni->line_exist(name.c_str(), "bound_rect")) + { + Init_internal(name, *pGameIni, name, sh_name); + return; + } + const CInifile* levelIni{}; if (name == g_pGameLevel->name()) levelIni = g_pGameLevel->pLevel; @@ -73,9 +80,34 @@ void CUICustomMap::Init_internal(const shared_str& name, const CInifile& pLtx, c if (pLtx.line_exist(m_name, "texture")) m_texture = pLtx.r_string(m_name, "texture"); // Override if needed + if (ShadowOfChernobylMode) + { + constexpr pcstr oldMapPrefix = "ui\\ui_map_"; + constexpr pcstr newMapPrefix = "map\\map_"; + const xr_string textureName = m_texture.c_str(); + + if (textureName.rfind(oldMapPrefix, 0) == 0) + { + const xr_string fallbackName = newMapPrefix + textureName.substr(xr_strlen(oldMapPrefix)); + const xr_string fallbackFile = fallbackName + ".dds"; + if (FS.exist("$game_textures$", fallbackFile.c_str())) + { + Msg("~ Using SoC map texture fallback [%s] for [%s]", fallbackName.c_str(), textureName.c_str()); + m_texture = fallbackName.c_str(); + } + } + } + Fvector4 tmp = pLtx.read_if_exists(sect_name, "bound_rect", {-10000.0f, -10000.0f, 10000.0f, 10000.0f}); pLtx.read_if_exists(tmp, m_name, "bound_rect"); // Override if needed + if (ShadowOfChernobylMode && pLtx.line_exist(sect_name, "x1") && pLtx.line_exist(sect_name, "x2") && + pLtx.line_exist(sect_name, "z1") && pLtx.line_exist(sect_name, "z2")) + { + tmp.set(pLtx.r_float(sect_name, "x1"), pLtx.r_float(sect_name, "z1"), + pLtx.r_float(sect_name, "x2"), pLtx.r_float(sect_name, "z2")); + } + m_shader_name = sh_name; if (!Heading()) diff --git a/src/xrGame/ui/UIMapWnd2.cpp b/src/xrGame/ui/UIMapWnd2.cpp index 20d3c1aa4f1..5746b453b8a 100644 --- a/src/xrGame/ui/UIMapWnd2.cpp +++ b/src/xrGame/ui/UIMapWnd2.cpp @@ -33,26 +33,39 @@ void CUIMapWnd::init_xml_nav(CUIXml& xml, pcstr start_from, bool critical) m_btn_nav[btn_actor] = UIHelper::Create3tButton(xml, strconcat(temp, pth, ":actor_btn" ), m_UIMainMapHeader, false); m_btn_nav[btn_zoom_more] = UIHelper::Create3tButton(xml, strconcat(temp, pth, ":zoom_in_btn" ), m_UIMainMapHeader, false); m_btn_nav[btn_zoom_less] = UIHelper::Create3tButton(xml, strconcat(temp, pth, ":zoom_out_btn" ), m_UIMainMapHeader, false); + + for (CUI3tButton* button : m_btn_nav) + { + if (button) + Register(button); + } } - AddCallback(m_btn_nav[btn_legend], BUTTON_DOWN, CUIWndCallback::void_function(this, &CUIMapWnd::OnBtnLegend_Push)); + const s16 clickEvent = ShadowOfChernobylMode ? BUTTON_CLICKED : BUTTON_DOWN; + + if (m_btn_nav[btn_legend]) + AddCallback(m_btn_nav[btn_legend], clickEvent, CUIWndCallback::void_function(this, &CUIMapWnd::OnBtnLegend_Push)); // AddCallback( m_btn_nav[btn_up]->WindowName(), BUTTON_DOWN, CUIWndCallback::void_function( this, //&CUIMapWnd::OnBtnUp_Push ) ); - AddCallback( - m_btn_nav[btn_zoom_more], BUTTON_DOWN, CUIWndCallback::void_function(this, &CUIMapWnd::OnBtnZoomMore_Push)); + if (m_btn_nav[btn_zoom_more]) + AddCallback( + m_btn_nav[btn_zoom_more], clickEvent, CUIWndCallback::void_function(this, &CUIMapWnd::OnBtnZoomMore_Push)); // AddCallback( m_btn_nav[btn_left]->WindowName(), BUTTON_DOWN, CUIWndCallback::void_function( this, //&CUIMapWnd::OnBtnLeft_Push ) ); - AddCallback(m_btn_nav[btn_actor], BUTTON_DOWN, CUIWndCallback::void_function(this, &CUIMapWnd::OnBtnActor_Push)); + if (m_btn_nav[btn_actor]) + AddCallback(m_btn_nav[btn_actor], clickEvent, CUIWndCallback::void_function(this, &CUIMapWnd::OnBtnActor_Push)); // AddCallback( m_btn_nav[btn_right]->WindowName(), BUTTON_DOWN, CUIWndCallback::void_function( this, //&CUIMapWnd::OnBtnRight_Push ) ); - AddCallback( - m_btn_nav[btn_zoom_less], BUTTON_DOWN, CUIWndCallback::void_function(this, &CUIMapWnd::OnBtnZoomLess_Push)); + if (m_btn_nav[btn_zoom_less]) + AddCallback( + m_btn_nav[btn_zoom_less], clickEvent, CUIWndCallback::void_function(this, &CUIMapWnd::OnBtnZoomLess_Push)); // AddCallback( m_btn_nav[btn_down]->WindowName(), BUTTON_DOWN, CUIWndCallback::void_function( this, //&CUIMapWnd::OnBtnDown_Push ) ); - AddCallback( - m_btn_nav[btn_zoom_reset], BUTTON_DOWN, CUIWndCallback::void_function(this, &CUIMapWnd::OnBtnZoomReset_Push)); + if (m_btn_nav[btn_zoom_reset]) + AddCallback( + m_btn_nav[btn_zoom_reset], clickEvent, CUIWndCallback::void_function(this, &CUIMapWnd::OnBtnZoomReset_Push)); } void CUIMapWnd::UpdateNav() diff --git a/src/xrGame/ui/UIMessagesWindow.cpp b/src/xrGame/ui/UIMessagesWindow.cpp index 90d0e28a17e..116d4ef3bff 100644 --- a/src/xrGame/ui/UIMessagesWindow.cpp +++ b/src/xrGame/ui/UIMessagesWindow.cpp @@ -118,6 +118,17 @@ void CUIMessagesWindow::AddIconedPdaMessage(const GAME_NEWS_DATA* news) pItem->SetColorAnimation( "ui_main_msgs_short", LA_ONLYALPHA | LA_TEXTCOLOR | LA_TEXTURECOLOR, float(news->show_time)); pItem->UIIcon.InitTexture(news->texture_name.c_str()); + if (news->has_texture_rect) + { + pItem->UIIcon.SetTextureRect(news->texture_rect); + pItem->UIIcon.SetStretchTexture(true); + } + if (ShadowOfChernobylMode) + { + Fvector2 message_pos = pItem->UIMsgText.GetWndPos(); + message_pos.x = pItem->UIIcon.GetWidth(); + pItem->UIMsgText.SetWndPos(message_pos); + } const float h1 = _max(pItem->UIIcon.GetHeight(), pItem->UIMsgText.GetWndPos().y + pItem->UIMsgText.GetHeight()); pItem->SetHeight(h1 + 3.0f); diff --git a/src/xrGame/ui/UINewsItemWnd.cpp b/src/xrGame/ui/UINewsItemWnd.cpp index 18406af4f45..bd7d9c3c5b2 100644 --- a/src/xrGame/ui/UINewsItemWnd.cpp +++ b/src/xrGame/ui/UINewsItemWnd.cpp @@ -57,6 +57,11 @@ void CUINewsItemWnd::Setup(GAME_NEWS_DATA& news_data) float h1 = m_UIText->GetWndPos().y + m_UIText->GetHeight() + 6.0f; m_UIImage->InitTexture(news_data.texture_name.c_str()); + if (news_data.has_texture_rect) + { + m_UIImage->SetTextureRect(news_data.texture_rect); + m_UIImage->SetStretchTexture(true); + } float h3 = m_UIImage->GetWndPos().y + m_UIImage->GetHeight(); h1 = _max(h1, h3); SetHeight(h1); diff --git a/src/xrGame/ui/UINewsWnd.cpp b/src/xrGame/ui/UINewsWnd.cpp new file mode 100644 index 00000000000..b4520fa98db --- /dev/null +++ b/src/xrGame/ui/UINewsWnd.cpp @@ -0,0 +1,117 @@ +#include "StdAfx.h" + +#include "UINewsWnd.h" +#include "xrUICore/XML/xrUIXmlParser.h" +#include "UIXmlInit.h" +#include "xrUICore/ui_base.h" +#include "../HUDManager.h" +#include "../Level.h" +#include "../game_news.h" +#include "../Actor.h" +#include "../alife_registry_wrappers.h" +#include "UIInventoryUtilities.h" +#include "UINewsItemWnd.h" +#include "xrUICore/ScrollView/UIScrollView.h" + +#define NEWS_XML "news.xml" +#define NEWS_TO_SHOW 50 + +CUINewsWnd::CUINewsWnd() : CUIWindow("CUINewsWnd") { m_flags.zero(); } + +CUINewsWnd::~CUINewsWnd() {} + +void CUINewsWnd::Init(LPCSTR xml_name, LPCSTR start_from) +{ + string512 pth; + + bool xml_result = uiXml.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, xml_name); + R_ASSERT3(xml_result, "xml file not found", xml_name); + CUIXmlInit xml_init; + + strconcat(sizeof(pth), pth, start_from, "list"); + xml_init.InitWindow(uiXml, pth, 0, this); + UIScrollWnd = xr_new(); + UIScrollWnd->SetAutoDelete(true); + AttachChild(UIScrollWnd); + xml_init.InitScrollView(uiXml, pth, 0, UIScrollWnd); +} + +void CUINewsWnd::Init() { Init(NEWS_XML, ""); } + +void CUINewsWnd::LoadNews() +{ + UIScrollWnd->Clear(); + + if (Actor()) + { + GAME_NEWS_VECTOR& news_vector = Actor()->game_news_registry->registry().objects(); + + // Показать только NEWS_TO_SHOW последних ньюсов + int currentNews = 0; + + for (GAME_NEWS_VECTOR::reverse_iterator it = news_vector.rbegin(); it != news_vector.rend() && currentNews < NEWS_TO_SHOW; ++it) + { + AddNewsItem(*it); + ++currentNews; + } + } + m_flags.set(eNeedAdd, FALSE); +} + +void CUINewsWnd::Update() +{ + inherited::Update(); + if (m_flags.test(eNeedAdd)) + LoadNews(); +} + +void CUINewsWnd::AddNews() +{ + m_flags.set(eNeedAdd, TRUE); +} + +void CUINewsWnd::AddNewsItem(GAME_NEWS_DATA& news_data, bool top) +{ + CUIWindow* itm = NULL; + switch (news_data.m_type) + { + case GAME_NEWS_DATA::eNews: { + CUINewsItemWnd* _itm = xr_new(); + _itm->Init(uiXml, "news_item"); + _itm->Setup(news_data); + itm = _itm; + } + break; + case GAME_NEWS_DATA::eTalk: { + CUINewsItemWnd* _itm = xr_new(); + _itm->Init(uiXml, "talk_item"); + _itm->Setup(news_data); + itm = _itm; + } + break; + }; + if (itm) + UIScrollWnd->AddWindow(itm, true); +} + +void CUINewsWnd::Show(bool status) +{ + if (status) + { + if (m_flags.test(eNeedAdd)) + LoadNews(); + } + else + { + InventoryUtilities::SendInfoToActor("ui_pda_news_hide"); + } + + inherited::Show(status); +} + +void CUINewsWnd::Reset() +{ + inherited::Reset(); + UIScrollWnd->Clear(); + m_flags.set(eNeedAdd, TRUE); +} diff --git a/src/xrGame/ui/UINewsWnd.h b/src/xrGame/ui/UINewsWnd.h new file mode 100644 index 00000000000..92ccff7960b --- /dev/null +++ b/src/xrGame/ui/UINewsWnd.h @@ -0,0 +1,36 @@ +#pragma once + +#include "xrUICore/Windows/UIWindow.h" +#include "xrUICore/XML/xrUIXmlParser.h" +class CUIScrollView; +struct GAME_NEWS_DATA; + +class CUINewsWnd : public CUIWindow +{ + typedef CUIWindow inherited; + enum eFlag + { + eNeedAdd = (1 << 0), + }; + Flags16 m_flags; + CUIXml uiXml; + +public: + CUINewsWnd(); + virtual ~CUINewsWnd(); + + void Init(); + void Init(LPCSTR xml_name, LPCSTR start_from); + void AddNews(); + void LoadNews(); + virtual void Show(bool status); + virtual void Update(); + virtual void Reset(); + + pcstr GetDebugType() override { return "CUINewsWnd"; } + + CUIScrollView* UIScrollWnd; + +private: + void AddNewsItem(GAME_NEWS_DATA& news_data, bool top = false); +}; diff --git a/src/xrGame/ui/UIOutfitSlot.cpp b/src/xrGame/ui/UIOutfitSlot.cpp index 637b67c81b2..f1d3b255f8b 100644 --- a/src/xrGame/ui/UIOutfitSlot.cpp +++ b/src/xrGame/ui/UIOutfitSlot.cpp @@ -30,7 +30,7 @@ void CUIOutfitDragDropList::SetOutfit(CUICellItem* itm) m_background->SetStretchTexture(true); - if (IsGameTypeSingle() && !itm) + if (!IsGameTypeSingle() && !itm) { IGameObject* pActor = smart_cast(Level().CurrentEntity()); diff --git a/src/xrGame/ui/UIPdaAux.cpp b/src/xrGame/ui/UIPdaAux.cpp new file mode 100644 index 00000000000..872c164485d --- /dev/null +++ b/src/xrGame/ui/UIPdaAux.cpp @@ -0,0 +1,15 @@ +//============================================================================= +// Filename: UIPdaAux.cpp +// Created by Roman E. Marchenko, vortex@gsc-game.kiev.ua +// Copyright 2004. GSC Game World +// --------------------------------------------------------------------------- +// Некоторые определения которые общие для всех диалогов ПДА +//============================================================================= + +#include "StdAfx.h" +#include "UIPdaAux.h" + +////////////////////////////////////////////////////////////////////////// + +// const char * const ALL_PDA_HEADER_PREFIX = "#root 15/FD-665#68"; +const char* const ALL_PDA_HEADER_PREFIX = "# "; diff --git a/src/xrGame/ui/UIPdaAux.h b/src/xrGame/ui/UIPdaAux.h new file mode 100644 index 00000000000..41c2f7a7a6b --- /dev/null +++ b/src/xrGame/ui/UIPdaAux.h @@ -0,0 +1,42 @@ +//============================================================================= +// Filename: UIPdaAux.h +// Created by Roman E. Marchenko, vortex@gsc-game.kiev.ua +// Copyright 2004. GSC Game World +// --------------------------------------------------------------------------- +// Некоторые определения которые общие для всех диалогов ПДА +//============================================================================= + +#pragma once + +enum EPdaTabs +{ + eptQuests = 0, + eptMap, + eptDiary, + eptContacts, + eptRanking, + eptActorStatistic, + eptEncyclopedia, + eptNoActiveTab = u16(-1) +}; + +extern const char* const ALL_PDA_HEADER_PREFIX; + +namespace pda_section +{ +enum part +{ + quests = (1 << 8), + map = (1 << 9), + diary = (1 << 10), + contacts = (1 << 11), + ranking = (1 << 12), + statistics = (1 << 13), + encyclopedia = (1 << 14), + + news = diary | (1 << 1), + info = diary | (1 << 2), + journal = diary | (1 << 3), + +}; +}; diff --git a/src/xrGame/ui/UIPdaContactsWnd.cpp b/src/xrGame/ui/UIPdaContactsWnd.cpp new file mode 100644 index 00000000000..1e579a758ac --- /dev/null +++ b/src/xrGame/ui/UIPdaContactsWnd.cpp @@ -0,0 +1,211 @@ +#include "StdAfx.h" +#include "UIPdaContactsWnd.h" +#include "UIPdaAux.h" +#include "../PDA.h" +#include "UIXmlInit.h" +#include "../Actor.h" +#include "xrUICore/Windows/UIFrameWindow.h" +#include "xrUICore/Windows/UIFrameLineWnd.h" +#include "xrUICore/Static/UIAnimatedStatic.h" +#include "xrUICore/ScrollView/UIScrollView.h" +#include "../Actor.h" +#include "xrEngine/StringTable/StringTable.h" + +#define PDA_CONTACT_HEIGHT 70 + +#define PDA_CONTACTS_XML "pda_contacts_new.xml" + +CUIPdaContactsWnd::CUIPdaContactsWnd() : CUIWindow("CUIPdaContactsWnd") { m_flags.zero(); } + +CUIPdaContactsWnd::~CUIPdaContactsWnd() {} + +void CUIPdaContactsWnd::Show(bool status) +{ + inherited::Show(status); + if (status) + { + UIDetailsWnd->Clear(); + Reload(); + } +} + +void CUIPdaContactsWnd::Init() +{ + CUIXml uiXml; + bool xml_result = uiXml.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, PDA_CONTACTS_XML); + R_ASSERT3(xml_result, "xml file not found", PDA_CONTACTS_XML); + + CUIXmlInit xml_init; + + xml_init.InitWindow(uiXml, "main_wnd", 0, this); + + UIFrameContacts = xr_new(); + UIFrameContacts->SetAutoDelete(true); + AttachChild(UIFrameContacts); + xml_init.InitFrameWindow(uiXml, "left_frame_window", 0, UIFrameContacts); + + UIContactsHeader = xr_new(); + UIContactsHeader->SetAutoDelete(true); + UIFrameContacts->AttachChild(UIContactsHeader); + xml_init.InitFrameLine(uiXml, "left_frame_line", 0, UIContactsHeader); + + UIRightFrame = xr_new(); + UIRightFrame->SetAutoDelete(true); + AttachChild(UIRightFrame); + xml_init.InitFrameWindow(uiXml, "right_frame_window", 0, UIRightFrame); + + UIRightFrameHeader = xr_new(); + UIRightFrameHeader->SetAutoDelete(true); + UIRightFrame->AttachChild(UIRightFrameHeader); + xml_init.InitFrameLine(uiXml, "right_frame_line", 0, UIRightFrameHeader); + + if (uiXml.NavigateToNode("a_static")) + { + UIAnimation = xr_new(); + UIAnimation->SetAutoDelete(true); + UIContactsHeader->AttachChild(UIAnimation); + xml_init.InitAnimatedStatic(uiXml, "a_static", 0, UIAnimation); + } + + UIListWnd = xr_new(); + UIListWnd->SetAutoDelete(true); + UIFrameContacts->AttachChild(UIListWnd); + xml_init.InitScrollView(uiXml, "list", 0, UIListWnd); + + UIDetailsWnd = xr_new(); + UIDetailsWnd->SetAutoDelete(true); + UIRightFrame->AttachChild(UIDetailsWnd); + xml_init.InitScrollView(uiXml, "detail_list", 0, UIDetailsWnd); + + CUIXmlInit::InitAutoStaticGroup(uiXml, "left_auto_static", 0, UIFrameContacts); + CUIXmlInit::InitAutoStaticGroup(uiXml, "right_auto_static", 0, UIRightFrame); +} + +void CUIPdaContactsWnd::Update() +{ + if (TRUE == m_flags.test(flNeedUpdate)) + { + RemoveAll(); + + CPda* pPda = Actor()->GetPDA(); + if (!pPda) + return; + + if (ShadowOfChernobylMode) + { + xr_vector owners; + pPda->ActivePDAContactOwners(owners); + for (CInventoryOwner* owner : owners) + AddContact(owner); + } + else + { + xr_vector pda_list; + pPda->ActivePDAContacts(pda_list); + for (CPda* pda : pda_list) + AddContact(pda, pda->GetOriginalOwnerID()); + } + + m_flags.set(flNeedUpdate, FALSE); + } + inherited::Update(); +} + +void CUIPdaContactsWnd::AddContact(CPda* pda, u16 owner_id) +{ + VERIFY(pda); + + auto pItem = xr_new(this); + UIListWnd->AddWindow(pItem, true); + pItem->Init(0, 0, UIListWnd->GetWidth(), 85); + pItem->InitCharacter(pda->GetOriginalOwner()); + pItem->m_data = (void*)pda; +} + +void CUIPdaContactsWnd::AddContact(CInventoryOwner* owner) +{ + VERIFY(owner); + + auto pItem = xr_new(this); + UIListWnd->AddWindow(pItem, true); + pItem->Init(0, 0, UIListWnd->GetWidth(), 85); + pItem->InitCharacter(owner); + pItem->m_data = (void*)owner; +} + +void CUIPdaContactsWnd::RemoveContact(CPda* pda) +{ + u32 cnt = UIListWnd->GetSize(); + + for (u32 i = 0; i < cnt; ++i) + { + CUIWindow* w = UIListWnd->GetItem(i); + CUIPdaContactItem* itm = (CUIPdaContactItem*)(w); + + if (itm->m_data == (void*)pda) + { + if (itm->GetSelected()) + UIDetailsWnd->Clear(); + UIListWnd->RemoveWindow(w); + return; + } + } +} + +//удалить все контакты из списка +void CUIPdaContactsWnd::RemoveAll() +{ + UIListWnd->Clear(); + UIDetailsWnd->Clear(); +} + +void CUIPdaContactsWnd::Reload() { m_flags.set(flNeedUpdate, TRUE); } + +void CUIPdaContactsWnd::Reset() +{ + inherited::Reset(); + Reload(); +} + +CUIPdaContactItem::~CUIPdaContactItem() {} + +extern CSE_ALifeTraderAbstract* ch_info_get_from_id(u16 id); + +#include "UICharacterInfo.h" +#include "../game_object_space.h" + +void CUIPdaContactItem::SetSelected(bool b) +{ + CUISelectable::SetSelected(b); + if (b) + { + m_cw->UIDetailsWnd->Clear(); + + CCharacterInfo chInfo; + CSE_ALifeTraderAbstract* T = ch_info_get_from_id(UIInfo->OwnerID()); + chInfo.Init(T); + + CUIStatic* pSt = xr_new(); + + pSt->SetText(CStringTable().translate(chInfo.Bio().c_str()).c_str()); + pSt->SetTextComplexMode(true); + pSt->SetWidth(m_cw->UIDetailsWnd->GetDesiredChildWidth()); + pSt->AdjustHeightToText(); + + if (m_cw->UIDetailsWnd->GetFont()) + pSt->SetFont(m_cw->UIDetailsWnd->GetFont()); + + m_cw->UIDetailsWnd->AddWindow(pSt, true); + + } +} + +bool CUIPdaContactItem::OnMouseDown(int mouse_btn) +{ + if (mouse_btn == MOUSE_1) + { + m_cw->UIListWnd->SetSelected(this); + return true; + } + return false; +} diff --git a/src/xrGame/ui/UIPdaContactsWnd.h b/src/xrGame/ui/UIPdaContactsWnd.h new file mode 100644 index 00000000000..67db030a589 --- /dev/null +++ b/src/xrGame/ui/UIPdaContactsWnd.h @@ -0,0 +1,64 @@ + +#pragma once + +#include "xrUICore/Windows/UIWindow.h" + +class CUIFrameWindow; +class CUIFrameLineWnd; +class CUIStatic; +class CUIAnimatedStatic; +class CUIScrollView; +class CPda; +class CInventoryOwner; + +class CUIPdaContactsWnd : public CUIWindow +{ +private: + typedef CUIWindow inherited; + enum + { + flNeedUpdate = (1 << 0), + }; + Flags8 m_flags; + +public: + CUIPdaContactsWnd(); + virtual ~CUIPdaContactsWnd(); + + void Init(); + + virtual void Update(); + virtual void Reset(); + + virtual void Show(bool status); + + void AddContact(CPda* pda, u16 owner_id); + void AddContact(CInventoryOwner* owner); + void RemoveContact(CPda* pda); + void RemoveAll(); + void Reload(); + + pcstr GetDebugType() override { return "CUIPdaContactsWnd"; } + + CUIScrollView* UIListWnd; + CUIScrollView* UIDetailsWnd; + +protected: + CUIFrameWindow* UIFrameContacts; + CUIFrameLineWnd* UIContactsHeader; + CUIFrameWindow* UIRightFrame; + CUIFrameLineWnd* UIRightFrameHeader; + CUIAnimatedStatic* UIAnimation; +}; + +#include "UIPdaListItem.h" +class CUIPdaContactItem : public CUIPdaListItem, public CUISelectable +{ + CUIPdaContactsWnd* m_cw; + +public: + CUIPdaContactItem(CUIPdaContactsWnd* cw) : m_cw(cw) {} + virtual ~CUIPdaContactItem(); + virtual void SetSelected(bool b); + virtual bool OnMouseDown(int mouse_btn); +}; diff --git a/src/xrGame/ui/UIPdaListItem.cpp b/src/xrGame/ui/UIPdaListItem.cpp new file mode 100644 index 00000000000..02d4391ffc0 --- /dev/null +++ b/src/xrGame/ui/UIPdaListItem.cpp @@ -0,0 +1,104 @@ +#include "StdAfx.h" + +#include "UIPdaListItem.h" +#include "../Actor.h" +#include "UIInventoryUtilities.h" +#include "xrEngine/StringTable/StringTable.h" + +#include "xrUICore/XML/xrUIXmlParser.h" +#include "UIXmlInit.h" + +#include "xrUICore/Windows/UIFrameWindow.h" +#include "../InventoryOwner.h" +#include "UICharacterInfo.h" +#include "xrUICore/Static/UIStatic.h" + +#define PDA_CONTACT_CHAR "pda_character.xml" + +CUIPdaListItem::CUIPdaListItem() : CUIWindow("CUIPdaListItem") +{ + UIMask = NULL; + UIInfo = NULL; +} + +CUIPdaListItem::~CUIPdaListItem() {} + +void CUIPdaListItem::Init(float x, float y, float width, float height) +{ + inherited::SetWndPos(Fvector2().set(x, y)); + inherited::SetWndSize(Fvector2().set(width, height)); + + CUIXml uiXml; + bool xml_result = uiXml.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, PDA_CONTACT_CHAR); + R_ASSERT2(xml_result, "xml file not found"); + + CUIXmlInit xml_init; + UIInfo = xr_new(); + UIInfo->SetAutoDelete(true); + AttachChild(UIInfo); + UIInfo->InitCharacterInfo(Fvector2().set(0, 0), Fvector2().set(width, height), PDA_CONTACT_CHAR); + + if (ShadowOfChernobylMode) + { + constexpr float iconX = 0.0f; + constexpr float infoX = 108.0f; + constexpr float valueOffset = 72.0f; + + if (CUIStatic* icon = UIInfo->GetIcon(CUICharacterInfo::eIcon)) + { + icon->SetWndPos(iconX, icon->GetWndPos().y); + icon->SetStretchTexture(true); + } + + if (CUIStatic* iconOver = UIInfo->GetIcon(CUICharacterInfo::eIconOver)) + iconOver->SetWndPos(iconX, iconOver->GetWndPos().y); + + const CUICharacterInfo::UIItemType labels[] = { + CUICharacterInfo::eName, + CUICharacterInfo::eNameCaption, + CUICharacterInfo::eRankCaption, + CUICharacterInfo::eCommunityCaption, + CUICharacterInfo::eReputationCaption, + CUICharacterInfo::eRelationCaption, + }; + for (const CUICharacterInfo::UIItemType type : labels) + { + if (CUIStatic* item = UIInfo->GetIcon(type)) + { + item->SetWndPos(infoX, item->GetWndPos().y); + item->SetWidth(width - infoX); + } + } + + const CUICharacterInfo::UIItemType values[] = { + CUICharacterInfo::eRank, + CUICharacterInfo::eCommunity, + CUICharacterInfo::eReputation, + CUICharacterInfo::eRelation, + }; + for (const CUICharacterInfo::UIItemType type : values) + { + if (CUIStatic* item = UIInfo->GetIcon(type)) + { + item->SetWndPos(infoX + valueOffset, item->GetWndPos().y); + item->SetWidth(width - infoX - valueOffset); + } + } + } + + xml_init.InitAutoStaticGroup(uiXml, "pda_char_auto_statics", 0, this); + + if (ShadowOfChernobylMode) + { + // The first automatic static is the online/activity indicator. Put it + // in the gap between the portrait and the visible name text. + if (CUIWindow* activityIndicator = FindChild("auto_static_0")) + activityIndicator->SetWndPos(106.0f, 2.5f); + } +} + +void CUIPdaListItem::InitCharacter(CInventoryOwner* pInvOwner) +{ + VERIFY(pInvOwner); + UIInfo->InitCharacter(pInvOwner->object_id()); +} diff --git a/src/xrGame/ui/UIPdaListItem.h b/src/xrGame/ui/UIPdaListItem.h new file mode 100644 index 00000000000..0de5366b29f --- /dev/null +++ b/src/xrGame/ui/UIPdaListItem.h @@ -0,0 +1,32 @@ +////////////////////////////////////////////////////////////////////// +// UIPdaListItem.h: элемент окна списка в PDA +// для отображения информации о контакте PDA +////////////////////////////////////////////////////////////////////// + +#pragma once +#include "xrUICore/Windows/UIWindow.h" + +class CUIFrameWindow; +class CUICharacterInfo; +class CInventoryOwner; + +class CUIPdaListItem : public CUIWindow +{ +private: + typedef CUIWindow inherited; + +public: + CUIPdaListItem(); + virtual ~CUIPdaListItem(); + virtual void Init(float x, float y, float width, float height); + virtual void InitCharacter(CInventoryOwner* pInvOwner); + + pcstr GetDebugType() override { return "CUIPdaListItem"; } + + void* m_data{}; + +protected: + //информация о персонаже + CUIFrameWindow* UIMask; + CUICharacterInfo* UIInfo; +}; diff --git a/src/xrGame/ui/UIPdaWnd.cpp b/src/xrGame/ui/UIPdaWnd.cpp index d6f434ad239..6ffe03fc101 100644 --- a/src/xrGame/ui/UIPdaWnd.cpp +++ b/src/xrGame/ui/UIPdaWnd.cpp @@ -29,6 +29,11 @@ #include "UIRankingWnd.h" #include "UILogsWnd.h" #include "UIScriptWnd.h" +#include "UIDiaryWnd.h" +#include "UIEncyclopediaWnd.h" +#include "UIEventsWnd.h" +#include "UIPdaContactsWnd.h" +#include "UIStalkersRankingWnd.h" #define PDA_XML "pda.xml" @@ -45,6 +50,11 @@ CUIPdaWnd::CUIPdaWnd() : CUIDialogWnd(CUIPdaWnd::GetDebugType()) pUIActorInfo = nullptr; pUIRankingWnd = nullptr; pUILogsWnd = nullptr; + pUISocTasks = nullptr; + pUISocDiary = nullptr; + pUISocContacts = nullptr; + pUISocRanking = nullptr; + pUISocEncyclopedia = nullptr; m_hint_wnd = nullptr; Init(); } @@ -63,6 +73,11 @@ CUIPdaWnd::~CUIPdaWnd() delete_data(pUIRankingWnd); if (pUILogsWnd) delete_data(pUILogsWnd); + delete_data(pUISocTasks); + delete_data(pUISocDiary); + delete_data(pUISocContacts); + delete_data(pUISocRanking); + delete_data(pUISocEncyclopedia); delete_data(m_hint_wnd); if (UINoice) delete_data(UINoice); @@ -122,13 +137,33 @@ void CUIPdaWnd::Init() if (!pUIMapWnd->Init("pda_map.xml", "map_wnd", false)) xr_delete(pUIMapWnd); - pUITaskWnd = xr_new(m_hint_wnd); - if (!pUITaskWnd->Init()) - xr_delete(pUITaskWnd); + if (ShadowOfChernobylMode) + { + pUISocTasks = xr_new(); + pUISocTasks->Init(); + + pUISocDiary = xr_new(); + pUISocDiary->Init(); + + pUISocContacts = xr_new(); + pUISocContacts->Init(); - pUIFactionWarWnd = xr_new(m_hint_wnd); - if (!pUIFactionWarWnd->Init()) - xr_delete(pUIFactionWarWnd); + pUISocRanking = xr_new(); + pUISocRanking->Init(); + + pUISocEncyclopedia = xr_new(); + pUISocEncyclopedia->Init(); + } + else + { + pUITaskWnd = xr_new(m_hint_wnd); + if (!pUITaskWnd->Init()) + xr_delete(pUITaskWnd); + + pUIFactionWarWnd = xr_new(m_hint_wnd); + if (!pUIFactionWarWnd->Init()) + xr_delete(pUIFactionWarWnd); + } pUIActorInfo = xr_new(); if (!pUIActorInfo->Init()) @@ -227,7 +262,7 @@ void CUIPdaWnd::Show(bool status) SetActiveSubdialog(m_sActiveSection); else { - cpcstr subdialog = pUIMapWnd && !pUITaskWnd ? "eptMap" : "eptTasks"; + cpcstr subdialog = pUIMapWnd && !pUITaskWnd && !pUISocTasks ? "eptMap" : "eptTasks"; SetActiveSubdialog(subdialog); UITabControl->SetActiveTab(subdialog); } @@ -283,7 +318,11 @@ void CUIPdaWnd::SetActiveSubdialog(const shared_str& section) const std::tuple availableWindowsList[] = { { "eptMap", nullptr, pUIMapWnd }, - { "eptTasks", nullptr, pUITaskWnd }, + { "eptTasks", nullptr, pUISocTasks ? static_cast(pUISocTasks) : pUITaskWnd }, + { "eptDiary", "ui_pda_events", pUISocDiary }, + { "eptContacts", "ui_pda_contacts", pUISocContacts }, + { "eptStalkersRanking", "ui_pda_ranking", pUISocRanking }, + { "eptEncyclopedia", "ui_pda_encyclopedia", pUISocEncyclopedia }, { "eptFractionWar", nullptr, pUIFactionWarWnd }, { "eptStatistics", "ui_pda_actor_info", pUIActorInfo }, { "eptRanking", nullptr, pUIRankingWnd }, @@ -420,6 +459,40 @@ void CUIPdaWnd::UpdatePda() { pUITaskWnd->ReloadTaskInfo(); } + else if (m_pActiveDialog == pUISocTasks && pUISocTasks) + { + pUISocTasks->Reload(); + } +} + +void CUIPdaWnd::PdaContentsChanged(pda_section::part type) +{ + bool showNotification = true; + + if (type == pda_section::encyclopedia && pUISocEncyclopedia) + pUISocEncyclopedia->ReloadArticles(); + else if (type == pda_section::news && pUISocDiary) + { + pUISocDiary->AddNews(); + pUISocDiary->MarkNewsAsRead(pUISocDiary->IsShown()); + } + else if (type == pda_section::quests && pUISocTasks) + pUISocTasks->Reload(); + else if (type == pda_section::contacts && pUISocContacts) + { + pUISocContacts->Reload(); + showNotification = false; + } + else if ((type == pda_section::journal || type == pda_section::info) && pUISocDiary) + pUISocDiary->ReloadJournal(); + else + showNotification = false; + + if (showNotification) + { + g_pda_info_state |= type; + CurrentGameUI()->UIMainIngameWnd->SetFlashIconState_(CUIMainIngameWnd::efiPdaTask, true); + } } void CUIPdaWnd::UpdateRankingWnd() @@ -444,6 +517,16 @@ void CUIPdaWnd::Reset() pUIRankingWnd->ResetAll(); if (pUILogsWnd) pUILogsWnd->ResetAll(); + if (pUISocTasks) + pUISocTasks->Reset(); + if (pUISocDiary) + pUISocDiary->Reset(); + if (pUISocContacts) + pUISocContacts->Reset(); + if (pUISocRanking) + pUISocRanking->Reset(); + if (pUISocEncyclopedia) + pUISocEncyclopedia->Reset(); } void CUIPdaWnd::SetCaption(pcstr text) @@ -474,6 +557,53 @@ void RearrangeTabButtons(CUITabControl* pTab) pTab->SetWndPos(pos); } +void RearrangeTabButtons(CUITabControl* pTab, xr_vector& signPlaces) +{ + const auto& buttons = *pTab->GetButtonsVector(); + signPlaces.clear(); + signPlaces.resize(buttons.size()); + + if (buttons.empty()) + return; + + Fvector2 pos = buttons.front()->GetWndPos(); + constexpr Fvector2 signSize{12.0f, 11.0f}; + + for (u32 index = 0; index < buttons.size(); ++index) + { + CUITabButton* button = buttons[index]; + + if (index != 0) + { + auto* separator = xr_new(); + separator->SetAutoDelete(true); + pTab->AttachChild(separator); + separator->SetFont(button->GetFont()); + separator->SetTextColor(color_rgba(90, 90, 90, 255)); + separator->SetText("//"); + separator->SetWndSize(button->GetWndSize()); + separator->AdjustWidthToText(); + separator->SetWndPos(pos); + pos.x += separator->GetWidth(); + } + + signPlaces[index] = pos; + signPlaces[index].y += iFloor((button->GetHeight() - signSize.y) / 2.0f); + signPlaces[index].y = static_cast(iFloor(signPlaces[index].y)); + pos.x += signSize.x; + + button->SetWndPos(pos); + button->AdjustWidthToText(); + pos.x += button->GetWidth() + 3.0f; + } +} + +void draw_sign(CUIStatic* sign, Fvector2& pos) +{ + sign->SetWndPos(pos); + sign->Draw(); +} + bool CUIPdaWnd::OnKeyboardAction(int dik, EUIMessages keyboard_action) { if (inherited::OnKeyboardAction(dik, keyboard_action)) diff --git a/src/xrGame/ui/UIPdaWnd.h b/src/xrGame/ui/UIPdaWnd.h index 4949d38dc3f..10f458e9883 100644 --- a/src/xrGame/ui/UIPdaWnd.h +++ b/src/xrGame/ui/UIPdaWnd.h @@ -1,6 +1,7 @@ #pragma once #include "UIDialogWnd.h" #include "encyclopedia_article_defs.h" +#include "UIPdaAux.h" class CInventoryOwner; class CUIFrameLineWnd; @@ -18,6 +19,11 @@ class CUIActorInfoWnd; class CUIRankingWnd; class CUILogsWnd; class CUIAnimatedStatic; +class CUIEventsWnd; +class CUIDiaryWnd; +class CUIPdaContactsWnd; +class CUIStalkersRankingWnd; +class CUIEncyclopediaWnd; class UIHint; class CUIPdaWnd final : public CUIDialogWnd @@ -50,6 +56,12 @@ class CUIPdaWnd final : public CUIDialogWnd CUIRankingWnd* pUIRankingWnd; CUILogsWnd* pUILogsWnd; + CUIEventsWnd* pUISocTasks; + CUIDiaryWnd* pUISocDiary; + CUIPdaContactsWnd* pUISocContacts; + CUIStalkersRankingWnd* pUISocRanking; + CUIEncyclopediaWnd* pUISocEncyclopedia; + virtual void Reset(); public: @@ -90,6 +102,7 @@ class CUIPdaWnd final : public CUIDialogWnd bool NeedCursor() const override; void UpdatePda(); void UpdateRankingWnd(); + void PdaContentsChanged(pda_section::part type); pcstr GetDebugType() override { return "CUIPdaWnd"; } }; diff --git a/src/xrGame/ui/UIStalkersRankingWnd.cpp b/src/xrGame/ui/UIStalkersRankingWnd.cpp new file mode 100644 index 00000000000..aad3ecf7aa8 --- /dev/null +++ b/src/xrGame/ui/UIStalkersRankingWnd.cpp @@ -0,0 +1,293 @@ +#include "StdAfx.h" +#include "UIStalkersRankingWnd.h" +#include "UIXmlInit.h" +#include "UIPdaAux.h" +#include "xrUICore/Windows/UIFrameWindow.h" +#include "xrUICore/Windows/UIFrameLineWnd.h" +#include "UIPdaListItem.h" +#include "xrUICore/Static/UIAnimatedStatic.h" +#include "xrUICore/ScrollView/UIScrollView.h" +#include "UICharacterInfo.h" +#include "../InventoryOwner.h" +#include "../Level.h" +#include "../PDA.h" +#include "../Actor.h" +#include "xrServerEntities/xrServer_Objects_ALife_Monsters.h" + +#define STALKERS_RANKING_XML "stalkers_ranking.xml" +#define STALKERS_RANKING_CHARACTER_XML "stalkers_ranking_character.xml" + +typedef xr_vector TOP_LIST; +TOP_LIST g_all_statistic_humans; + +CUIStalkersRankingWnd::CUIStalkersRankingWnd() : CUIWindow("CUIStalkersRankingWnd") {} + +void CUIStalkersRankingWnd::Init() +{ + CUIXml uiXml; + uiXml.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, STALKERS_RANKING_XML); + + CUIXmlInit xml_init; + + xml_init.InitWindow(uiXml, "main_wnd", 0, this); + + UICharIconFrame = xr_new(); + UICharIconFrame->SetAutoDelete(true); + AttachChild(UICharIconFrame); + xml_init.InitFrameWindow(uiXml, "chicon_frame_window", 0, UICharIconFrame); + + UICharIconHeader = xr_new(); + UICharIconHeader->SetAutoDelete(true); + UICharIconFrame->AttachChild(UICharIconHeader); + xml_init.InitFrameLine(uiXml, "chicon_frame_line", 0, UICharIconHeader); + + UIInfoFrame = xr_new(); + UIInfoFrame->SetAutoDelete(true); + AttachChild(UIInfoFrame); + xml_init.InitFrameWindow(uiXml, "info_frame_window", 0, UIInfoFrame); + + UIInfoHeader = xr_new(); + UIInfoHeader->SetAutoDelete(true); + UIInfoFrame->AttachChild(UIInfoHeader); + xml_init.InitFrameLine(uiXml, "info_frame_line", 0, UIInfoHeader); + + if (uiXml.NavigateToNode("a_static")) + { + UIAnimatedIcon = xr_new(); + UIAnimatedIcon->SetAutoDelete(true); + UIInfoHeader->AttachChild(UIAnimatedIcon); + xml_init.InitAnimatedStatic(uiXml, "a_static", 0, UIAnimatedIcon); + } + + UIList = xr_new(); + UIList->SetAutoDelete(true); + UIInfoFrame->AttachChild(UIList); + xml_init.InitScrollView(uiXml, "list", 0, UIList); + + UICharacterWindow = xr_new(); + UICharacterWindow->SetAutoDelete(true); + UICharIconFrame->AttachChild(UICharacterWindow); + xml_init.InitWindow(uiXml, "character_info", 0, UICharacterWindow); + + UICharacterInfo = xr_new(); + UICharacterInfo->SetAutoDelete(true); + UICharacterWindow->AttachChild(UICharacterInfo); + UICharacterInfo->InitCharacterInfo(Fvector2().set(0, 0), UICharacterWindow->GetWndSize(), STALKERS_RANKING_CHARACTER_XML); + + if (ShadowOfChernobylMode) + { + // The SoC layout starts the text at x=165. Leave a small empty column + // before it instead of drawing the portrait up to the first letter. + CUIStatic& portrait = UICharacterInfo->UIIcon(); + portrait.SetWidth(portrait.GetWidth() - 10.0f); + + if (CUIStatic* rank = UICharacterInfo->GetIcon(CUICharacterInfo::eRank)) + rank->MoveWndDelta(5.0f, 0.0f); + } + + xml_init.InitAutoStaticGroup(uiXml, "left_auto", 0, UIInfoFrame); + xml_init.InitAutoStaticGroup(uiXml, "right_auto", 0, UICharIconFrame); +} + +void CUIStalkersRankingWnd::Show(bool status) +{ + inherited::Show(status); + if (status) + FillList(); +} + +extern CSE_ALifeTraderAbstract* ch_info_get_from_id(u16 id); + +bool GreaterRankPred(const u16& h1, const u16& h2) +{ + CSE_ALifeTraderAbstract* t1 = ch_info_get_from_id(h1); + CSE_ALifeTraderAbstract* t2 = ch_info_get_from_id(h2); + if (t1 && t2) + return t1->m_rank > t2->m_rank; + else if (t1) + return true; + return false; +} + +int get_actor_ranking() +{ + std::sort(g_all_statistic_humans.begin(), g_all_statistic_humans.end(), GreaterRankPred); + TOP_LIST::iterator it = std::find(g_all_statistic_humans.begin(), g_all_statistic_humans.end(), Actor()->ID()); + if (it != g_all_statistic_humans.end()) + return (int)std::distance(g_all_statistic_humans.begin(), it); + else + return 1; +} + +void CUIStalkersRankingWnd::FillList() +{ + CUIXml uiXml; + uiXml.Load(CONFIG_PATH, UI_PATH, UI_PATH_DEFAULT, STALKERS_RANKING_XML); + + UIList->Clear(); + + uiXml.SetLocalRoot(uiXml.NavigateToNode("stalkers_list", 0)); + + if (g_all_statistic_humans.size()) + { + CSE_ALifeTraderAbstract* pActorAbstract = ch_info_get_from_id(Actor()->ID()); + int actor_place = get_actor_ranking(); + + int i = 0; + while (i < 20 && i < g_all_statistic_humans.size()) + { + u16 id = g_all_statistic_humans[i]; + CSE_ALifeTraderAbstract* pT = ch_info_get_from_id(id); + if (pT) + { + if (pT == pActorAbstract || (i == 19 && actor_place > 19)) + AddActorItem(&uiXml, actor_place + 1, pActorAbstract); + else + AddStalkerItem(&uiXml, i + 1, pT); + } + else + Msg("! [%s]: i[%d] id[%d] not a CSE_ALifeTraderAbstract", __FUNCTION__, i, id); + i++; + } + + UIList->SetSelected(UIList->GetItem(0)); + } + else + { + CUIStalkerRankingInfoItem* itm = xr_new(this); + itm->Init(&uiXml, "no_items", 0); + UIList->AddWindow(itm, true); + } +} + +void CUIStalkersRankingWnd::ShowHumanInfo(u16 id) { UICharacterInfo->InitCharacter(id); } + +void CUIStalkersRankingWnd::AddStalkerItem(CUIXml* xml, int num, CSE_ALifeTraderAbstract* t) +{ + CUIStalkerRankingInfoItem* itm = xr_new(this); + itm->Init(xml, "item_human", 0); + + std::string s = std::to_string(num) + "."; + itm->m_text1->SetText(s.c_str()); + + itm->m_text2->SetText(t->m_character_name.c_str()); + + s = std::to_string(t->m_rank); + itm->m_text3->SetText(s.c_str()); + + itm->m_humanID = t->object_id(); + UIList->AddWindow(itm, true); +} + +void CUIStalkersRankingWnd::AddActorItem(CUIXml* xml, int num, CSE_ALifeTraderAbstract* t) +{ + CUIStalkerRankingInfoItem* itm; + if (num > 19) + { + itm = xr_new(this); + itm->Init(xml, "item_ellipsis", 0); + UIList->AddWindow(itm, true); + } + + itm = xr_new(this); + itm->Init(xml, "item_actor", 0); + + std::string s = std::to_string(num) + "."; + itm->m_text1->SetText(s.c_str()); + + itm->m_text2->SetText(t->m_character_name.c_str()); + + s = std::to_string(t->m_rank); + itm->m_text3->SetText(s.c_str()); + + itm->m_humanID = t->object_id(); + UIList->AddWindow(itm, true); +} + +void CUIStalkersRankingWnd::Reset() +{ + inherited::Reset(); + g_all_statistic_humans.clear(); +} + +void remove_human_from_top_list(u16 id) +{ + TOP_LIST::iterator it = std::find(g_all_statistic_humans.begin(), g_all_statistic_humans.end(), id); + if (it != g_all_statistic_humans.end()) + g_all_statistic_humans.erase(it); +} + +void add_human_to_top_list(u16 id) +{ + CSE_ALifeTraderAbstract* t = ch_info_get_from_id(id); + if (t) + { + remove_human_from_top_list(id); + g_all_statistic_humans.push_back(id); + } + else + Msg("! [%s]: id[%d] not a CSE_ALifeTraderAbstract", __FUNCTION__, id); +} + +CUIStalkerRankingInfoItem::CUIStalkerRankingInfoItem(CUIStalkersRankingWnd* w) + : CUIWindow("CUIStalkerRankingInfoItem"), m_StalkersRankingWnd(w), m_humanID(u16(-1)) +{} + +void CUIStalkerRankingInfoItem::Init(CUIXml* xml, LPCSTR path, int idx) +{ + XML_NODE _stored_root = xml->GetLocalRoot(); + + CUIXmlInit xml_init; + xml_init.InitWindow(*xml, path, idx, this); + + xml->SetLocalRoot(xml->NavigateToNode(path, idx)); + + m_text1 = xr_new(); + m_text1->SetAutoDelete(true); + AttachChild(m_text1); + xml_init.InitStatic(*xml, "text_1", 0, m_text1); + + m_text2 = xr_new(); + m_text2->SetAutoDelete(true); + AttachChild(m_text2); + xml_init.InitStatic(*xml, "text_2", 0, m_text2); + + m_text3 = xr_new(); + m_text3->SetAutoDelete(true); + AttachChild(m_text3); + xml_init.InitStatic(*xml, "text_3", 0, m_text3); + + xml_init.InitAutoStaticGroup(*xml, "auto", 0, this); + + m_stored_alpha = color_get_A(m_text2->GetTextColor()); + xml->SetLocalRoot(_stored_root); +} + +void CUIStalkerRankingInfoItem::SetSelected(bool b) +{ + CUISelectable::SetSelected(b); + m_text1->SetTextColor(subst_alpha(m_text1->GetTextColor(), b ? 255 : m_stored_alpha)); + m_text2->SetTextColor(subst_alpha(m_text2->GetTextColor(), b ? 255 : m_stored_alpha)); + m_text3->SetTextColor(subst_alpha(m_text3->GetTextColor(), b ? 255 : m_stored_alpha)); + if (b) + { + m_StalkersRankingWnd->ShowHumanInfo(m_humanID); + } +} + +bool CUIStalkerRankingInfoItem::OnMouseDown(int mouse_btn) +{ + if (mouse_btn == MOUSE_1) + { + m_StalkersRankingWnd->GetTopList().SetSelected(this); + return true; + } + else + return false; +} + +CUIStalkerRankingElipsisItem::CUIStalkerRankingElipsisItem(CUIStalkersRankingWnd* w) : inherited(w) {} + +void CUIStalkerRankingElipsisItem::SetSelected(bool b) { return; } + +bool CUIStalkerRankingElipsisItem::OnMouseDown(int mouse_btn) { return false; } diff --git a/src/xrGame/ui/UIStalkersRankingWnd.h b/src/xrGame/ui/UIStalkersRankingWnd.h new file mode 100644 index 00000000000..61279dc7327 --- /dev/null +++ b/src/xrGame/ui/UIStalkersRankingWnd.h @@ -0,0 +1,74 @@ +#pragma once +#include "xrUICore/Windows/UIWindow.h" + +class CUIFrameWindow; +class CUIFrameLineWnd; +class CUIAnimatedStatic; +class CUIStatic; +class CUICharacterInfo; +class CUIScrollView; +class CUIXml; +class CSE_ALifeTraderAbstract; + +class CUIStalkersRankingWnd : public CUIWindow +{ + typedef CUIWindow inherited; + +public: + CUIStalkersRankingWnd(); + void Init(); + virtual void Show(bool status); + void ShowHumanDetails(); + +protected: + CUIFrameWindow* UIInfoFrame{}; + CUIFrameWindow* UICharIconFrame{}; + CUIFrameLineWnd* UIInfoHeader{}; + CUIFrameLineWnd* UICharIconHeader{}; + CUIAnimatedStatic* UIAnimatedIcon{}; + // информация о персонаже + CUIWindow* UICharacterWindow{}; + CUICharacterInfo* UICharacterInfo{}; + void FillList(); + CUIScrollView* UIList{}; + void AddStalkerItem(CUIXml* xml, int num, CSE_ALifeTraderAbstract* t); + void AddActorItem(CUIXml* xml, int num, CSE_ALifeTraderAbstract* t); + +public: + CUIScrollView& GetTopList() { return *UIList; } + void ShowHumanInfo(u16 id); + virtual void Reset(); + + pcstr GetDebugType() override { return "CUIStalkersRankingWnd"; } +}; + +class CUIStalkerRankingInfoItem : public CUIWindow, public CUISelectable +{ + CUIStalkersRankingWnd* m_StalkersRankingWnd; + u32 m_stored_alpha; + +public: + u16 m_humanID; + CUIStatic* m_text1; + CUIStatic* m_text2; + CUIStatic* m_text3; + +public: + CUIStalkerRankingInfoItem(CUIStalkersRankingWnd*); + + void Init(CUIXml* xml, LPCSTR path, int idx); + virtual void SetSelected(bool b); + virtual bool OnMouseDown(int mouse_btn); + + pcstr GetDebugType() override { return "CUIStalkerRankingInfoItem"; } +}; + +class CUIStalkerRankingElipsisItem : public CUIStalkerRankingInfoItem +{ + typedef CUIStalkerRankingInfoItem inherited; + +public: + CUIStalkerRankingElipsisItem(CUIStalkersRankingWnd*); + virtual void SetSelected(bool b); + virtual bool OnMouseDown(int mouse_btn); +}; diff --git a/src/xrGame/ui/UITalkDialogWnd.cpp b/src/xrGame/ui/UITalkDialogWnd.cpp index 66e8ef1834a..89f88ac6c3a 100644 --- a/src/xrGame/ui/UITalkDialogWnd.cpp +++ b/src/xrGame/ui/UITalkDialogWnd.cpp @@ -273,6 +273,9 @@ void CUITalkDialogWnd::AddIconedAnswer(LPCSTR caption, LPCSTR text, LPCSTR textu void CUITalkDialogWnd::AddIconedAnswer(pcstr text, pcstr texture_name, Frect texture_rect, pcstr templ_name) { + // SoC scripts pass x, y, width, and height instead of two absolute points. + texture_rect.rb.add(texture_rect.lt); + CUIAnswerItemIconed* itm = xr_new(m_uiXml, templ_name); itm->Init(text, texture_name, texture_rect); UIAnswersList->AddWindow(itm, true); @@ -284,6 +287,8 @@ void CUITalkDialogWnd::AddIconedAnswer(pcstr text, pcstr texture_name, Frect tex news_data.m_type = GAME_NEWS_DATA::eTalk; news_data.texture_name = texture_name; + news_data.texture_rect = texture_rect; + news_data.has_texture_rect = true; news_data.receive_time = Level().GetGameTime(); Actor()->game_news_registry->registry().objects().emplace_back(std::move(news_data)); diff --git a/src/xrGame/ui/UITaskDescrWnd.cpp b/src/xrGame/ui/UITaskDescrWnd.cpp new file mode 100644 index 00000000000..af31be216dd --- /dev/null +++ b/src/xrGame/ui/UITaskDescrWnd.cpp @@ -0,0 +1,63 @@ +#include "StdAfx.h" +#include "UITaskDescrWnd.h" +#include "UIXmlInit.h" +#include "xrUICore/Windows/UIFrameWindow.h" +#include "xrUICore/Windows/UIFrameLineWnd.h" +#include "xrUICore/ScrollView/UIScrollView.h" +#include "UIEncyclopediaArticleWnd.h" +#include "../encyclopedia_article.h" + +CUITaskDescrWnd::CUITaskDescrWnd() : CUIWindow("CUITaskDescrWnd") {} + +CUITaskDescrWnd::~CUITaskDescrWnd() {} + +void CUITaskDescrWnd::Init(CUIXml* doc, LPCSTR start_from) +{ + CUIXmlInit xml_init; + + xml_init.InitWindow(*doc, start_from, 0, this); + + string512 str; + + m_UIMainFrame = xr_new(); + m_UIMainFrame->SetAutoDelete(true); + AttachChild(m_UIMainFrame); + + strconcat(sizeof(str), str, start_from, ":main_frame"); + xml_init.InitFrameWindow(*doc, str, 0, m_UIMainFrame); + + //. strconcat (str,start_from,":main_frame"); + //. xml_init.InitAutoStaticGroup(*doc, str, m_UIMainFrame); + + m_UIMainHeader = xr_new(); + m_UIMainHeader->SetAutoDelete(true); + m_UIMainFrame->AttachChild(m_UIMainHeader); + strconcat(sizeof(str), str, start_from, ":main_frame:header_frame_line"); + xml_init.InitFrameLine(*doc, str, 0, m_UIMainHeader); + + m_UITaskInfoWnd = xr_new(); + m_UITaskInfoWnd->SetAutoDelete(true); + m_UIMainFrame->AttachChild(m_UITaskInfoWnd); + strconcat(sizeof(str), str, start_from, ":main_frame:scroll_view"); + xml_init.InitScrollView(*doc, str, 0, m_UITaskInfoWnd); +} + +void CUITaskDescrWnd::Draw() { inherited::Draw(); } + +void CUITaskDescrWnd::ClearAll() { m_UITaskInfoWnd->Clear(); } + +void CUITaskDescrWnd::AddArticle(LPCSTR article) +{ + CUIEncyclopediaArticleWnd* article_info = xr_new(); + article_info->Init("encyclopedia_item.xml", "events_wnd:objective_item"); + article_info->SetArticle(article); + m_UITaskInfoWnd->AddWindow(article_info, true); +} + +void CUITaskDescrWnd::AddArticle(CEncyclopediaArticle* article) +{ + CUIEncyclopediaArticleWnd* article_info = xr_new(); + article_info->Init("encyclopedia_item.xml", "events_wnd:objective_item"); + article_info->SetArticle(article); + m_UITaskInfoWnd->AddWindow(article_info, true); +} diff --git a/src/xrGame/ui/UITaskDescrWnd.h b/src/xrGame/ui/UITaskDescrWnd.h new file mode 100644 index 00000000000..634b6a82a48 --- /dev/null +++ b/src/xrGame/ui/UITaskDescrWnd.h @@ -0,0 +1,29 @@ +#pragma once +#include "xrUICore/Windows/UIWindow.h" + +class CUIScrollView; +class CUIFrameWindow; +class CUIFrameLineWnd; +class CUIXml; +class CEncyclopediaArticle; + +class CUITaskDescrWnd : public CUIWindow +{ + typedef CUIWindow inherited; + +protected: + CUIScrollView* m_UITaskInfoWnd; + CUIFrameWindow* m_UIMainFrame; + CUIFrameLineWnd* m_UIMainHeader; + +public: + CUITaskDescrWnd(); + virtual ~CUITaskDescrWnd(); + virtual void Draw(); + void Init(CUIXml* doc, LPCSTR start_from); + void ClearAll(); + void AddArticle(LPCSTR article); + void AddArticle(CEncyclopediaArticle* article); + + pcstr GetDebugType() override { return "CUITaskDescrWnd"; } +}; diff --git a/src/xrGame/ui/UITaskItem.cpp b/src/xrGame/ui/UITaskItem.cpp new file mode 100644 index 00000000000..dfd6e4626f9 --- /dev/null +++ b/src/xrGame/ui/UITaskItem.cpp @@ -0,0 +1,281 @@ +#include "StdAfx.h" +#include "UITaskItem.h" +#include "UIXmlInit.h" +#include "xrUICore/Buttons/UI3tButton.h" +#include "../GameTask.h" +#include "xrEngine/StringTable/StringTable.h" +#include "UIEventsWnd.h" +#include "xrUICore/EditBox/UIEditBoxEx.h" +#include "xrUICore/EditBox/UIEditBox.h" +#include "UIInventoryUtilities.h" +#include "xrUICore/XML/UITextureMaster.h" +#include "../map_location.h" +#include "../map_manager.h" +#include "../Level.h" +#include "../Actor.h" +#include "../GametaskManager.h" + +CUISocTaskItem::CUISocTaskItem(CUIEventsWnd* w) : m_GameTask(NULL), m_TaskObjectiveIdx(u16(-1)), m_EventsWnd(w) {} + +CUISocTaskItem::~CUISocTaskItem() {} + +void CUISocTaskItem::SetGameTask(CGameTask* gt, u16 obj_idx) +{ + m_GameTask = gt; + m_TaskObjectiveIdx = obj_idx; +} + +void CUISocTaskItem::SendMessage(CUIWindow* pWnd, s16 msg, void* pData) { CUIWndCallback::OnEvent(pWnd, msg, pData); } + +SGameTaskObjective* CUISocTaskItem::Objective() { return &m_GameTask->Objective(m_TaskObjectiveIdx); } + +void CUISocTaskItem::Init() +{ + SetWindowName("job_item"); + Register(this); + AddCallbackStr("job_item", BUTTON_CLICKED, fastdelegate::MakeDelegate(this, &CUISocTaskItem::OnItemClicked)); +} + +void CUISocTaskItem::OnItemClicked(CUIWindow*, void*) +{ + if (ObjectiveIdx() != ROOT_TASK_OBJECTIVE) + { + SGameTaskObjective* objective = Objective(); + if (objective->GetTaskState() != eTaskStateInProgress) + return; + + Level().GameTaskManager().SetActiveTask(GameTask(), ObjectiveIdx()); + m_EventsWnd->SetDescriptionMode(true); + } + + m_EventsWnd->ShowDescription(GameTask(), ObjectiveIdx()); +} + +CUITaskRootItem::CUITaskRootItem(CUIEventsWnd* w) : inherited(w) { Init(); } + +CUITaskRootItem::~CUITaskRootItem() {} + +void CUITaskRootItem::Init() +{ + inherited::Init(); + + m_taskImage = xr_new(); + m_taskImage->SetAutoDelete(true); + AttachChild(m_taskImage); + m_captionStatic = xr_new(); + m_captionStatic->SetAutoDelete(true); + AttachChild(m_captionStatic); + m_remTimeStatic = xr_new(); + m_remTimeStatic->SetAutoDelete(true); + AttachChild(m_remTimeStatic); + + m_switchDescriptionBtn = xr_new(); + m_switchDescriptionBtn->SetAutoDelete(true); + AttachChild(m_switchDescriptionBtn); + m_captionTime = xr_new(); + m_captionTime->SetAutoDelete(true); + AttachChild(m_captionTime); + + m_switchDescriptionBtn->SetWindowName("m_switchDescriptionBtn"); + Register(m_switchDescriptionBtn); + AddCallback(m_switchDescriptionBtn, BUTTON_DOWN, fastdelegate::MakeDelegate(this, &CUITaskRootItem::OnSwitchDescriptionClicked)); + + CUIXmlInit xml_init; + CUIXml& uiXml = m_EventsWnd->m_ui_task_item_xml; + xml_init.InitWindow(uiXml, "task_root_item", 0, this); + + xml_init.InitStatic(uiXml, "task_root_item:image", 0, m_taskImage); + xml_init.InitStatic(uiXml, "task_root_item:caption", 0, m_captionStatic); + xml_init.InitStatic(uiXml, "task_root_item:caption_time", 0, m_captionTime); + xml_init.InitStatic(uiXml, "task_root_item:rem_time", 0, m_remTimeStatic); + + xml_init.Init3tButton(uiXml, "task_root_item:switch_description_btn", 0, m_switchDescriptionBtn); +} + +void CUITaskRootItem::SetGameTask(CGameTask* gt, u16 obj_idx) +{ + inherited::SetGameTask(gt, obj_idx); + + CStringTable stbl; + auto& obj = m_GameTask->Objective(m_TaskObjectiveIdx); + + m_taskImage->InitTexture(obj.m_icon_texture_name.c_str()); + + if (!CUITextureMaster::ItemExist(obj.m_icon_texture_name)) + { + Frect r = obj.m_icon_rect; + // The legacy file form stores x, y, width, and height. Modern + // CUIStatic::SetTextureRect expects absolute right/bottom values. + r.x2 += r.x1; + r.y2 += r.y1; + m_taskImage->SetTextureRect(r); + } + m_taskImage->SetStretchTexture(true); + + m_captionStatic->SetText(stbl.translate(m_GameTask->m_Title).c_str()); + m_captionStatic->AdjustHeightToText(); + + xr_string txt = ""; + txt += InventoryUtilities::GetDateAsString(gt->m_ReceiveTime, InventoryUtilities::edpDateToDay, '/', true).c_str(); + txt += " "; + txt += InventoryUtilities::GetTimeAsString(gt->m_ReceiveTime, InventoryUtilities::etpTimeToMinutes).c_str(); + + m_captionTime->SetText(txt.c_str()); + m_captionTime->SetWndPos(m_captionTime->GetWndPos().x, m_captionStatic->GetWndPos().y + m_captionStatic->GetHeight() + 3.0f); + + float h = _max(m_taskImage->GetWndPos().y + m_taskImage->GetHeight(), m_captionTime->GetWndPos().y + m_captionTime->GetHeight()); + h = _max(h, m_switchDescriptionBtn->GetWndPos().y + m_switchDescriptionBtn->GetHeight()); + SetHeight(h); + + m_curr_descr_mode = m_EventsWnd->GetDescriptionMode(); + if (m_curr_descr_mode) + m_switchDescriptionBtn->InitTexture("ui_icons_newPDA_showtext"); + else + m_switchDescriptionBtn->InitTexture("ui_icons_newPDA_showmap"); + + m_remTimeStatic->Show(GameTask()->Objective(0).GetTaskState() == eTaskStateInProgress && (GameTask()->m_ReceiveTime != GameTask()->m_TimeToComplete)); + + if (m_remTimeStatic->IsShown()) + { + float _height = GetWndSize().y; + Fvector2 _pos = m_captionTime->GetWndPos(); + _pos.y += m_captionTime->GetWndSize().y; + _pos.x = m_remTimeStatic->GetWndPos().x; + + m_remTimeStatic->SetWndPos(_pos); + + _height = _max(_height, _pos.y + m_remTimeStatic->GetWndSize().y); + SetHeight(_height); + } +} + +void CUITaskRootItem::Update() +{ + inherited::Update(); + + if (m_curr_descr_mode != m_EventsWnd->GetDescriptionMode()) + { + m_curr_descr_mode = m_EventsWnd->GetDescriptionMode(); + if (m_curr_descr_mode) + m_switchDescriptionBtn->InitTexture("ui_icons_newPDA_showtext"); + else + m_switchDescriptionBtn->InitTexture("ui_icons_newPDA_showmap"); + } + + // The texture shows the current mode. Keep the button in its normal state + // so it can send the next press and return from text to the map. + m_switchDescriptionBtn->SetButtonState(CUIButton::BUTTON_NORMAL); + + if (m_remTimeStatic->IsShown()) + { + string512 buff, buff2; + InventoryUtilities::GetTimePeriodAsString(buff, sizeof(buff), Level().GetGameTime(), GameTask()->m_TimeToComplete); + sprintf_s(buff2, "%s %s", CStringTable().translate("ui_st_time_remains").c_str(), buff); + m_remTimeStatic->SetText(buff2); + } +} + +bool CUITaskRootItem::OnDbClick() { return true; } + +void CUITaskRootItem::OnSwitchDescriptionClicked(CUIWindow*, void*) +{ + const bool showMap = !m_EventsWnd->GetDescriptionMode() && m_EventsWnd->IsTaskDescriptionShown(GameTask()); + m_EventsWnd->SetDescriptionMode(showMap); + OnItemClicked(this, NULL); +} + +void CUITaskRootItem::MarkSelected(bool b) {} + +CUITaskSubItem::CUITaskSubItem(CUIEventsWnd* w) : inherited(w) { Init(); } + +CUITaskSubItem::~CUITaskSubItem() {} + +void CUITaskSubItem::Init() +{ + inherited::Init(); + CUIXml& uiXml = m_EventsWnd->m_ui_task_item_xml; + + m_stateStatic = xr_new(); + m_stateStatic->SetAutoDelete(true); + AttachChild(m_stateStatic); + m_descriptionStatic = xr_new(); + m_descriptionStatic->SetAutoDelete(true); + AttachChild(m_descriptionStatic); + m_ActiveObjectiveStatic = xr_new(); + m_ActiveObjectiveStatic->SetAutoDelete(true); + AttachChild(m_ActiveObjectiveStatic); + m_showDescriptionBtn = xr_new(); + m_showDescriptionBtn->SetAutoDelete(true); + AttachChild(m_showDescriptionBtn); + + m_showDescriptionBtn->SetWindowName("m_showDescriptionBtn"); + Register(m_showDescriptionBtn); + + AddCallback(m_showDescriptionBtn, BUTTON_DOWN, fastdelegate::MakeDelegate(this, &CUITaskSubItem::OnShowDescriptionClicked)); + + CUIXmlInit xml_init; + xml_init.InitWindow(uiXml, "task_sub_item", 0, this); + xml_init.InitStatic(uiXml, "task_sub_item:state_image", 0, m_stateStatic); + xml_init.InitStatic(uiXml, "task_sub_item:description", 0, m_descriptionStatic); + xml_init.InitStatic(uiXml, "task_sub_item:active_objecttive_image", 0, m_ActiveObjectiveStatic); + xml_init.Init3tButton(uiXml, "task_sub_item:show_descr_btn", 0, m_showDescriptionBtn); + + m_active_color = xml_init.GetColor(uiXml, "task_sub_item:description:text_colors:active", 0, 0x00); + m_failed_color = xml_init.GetColor(uiXml, "task_sub_item:description:text_colors:failed", 0, 0x00); + m_accomplished_color = xml_init.GetColor(uiXml, "task_sub_item:description:text_colors:accomplished", 0, 0x00); + m_skiped_color = xml_init.GetColor(uiXml, "task_sub_item:description:text_colors:skiped", 0, 0x00); +} + +void CUITaskSubItem::SetGameTask(CGameTask* gt, u16 obj_idx) +{ + inherited::SetGameTask(gt, obj_idx); + + CStringTable stbl; + auto& obj = m_GameTask->Objective(m_TaskObjectiveIdx); + + m_descriptionStatic->SetText(stbl.translate(obj.m_Description).c_str()); + m_descriptionStatic->AdjustHeightToText(); + float h = _max(m_ActiveObjectiveStatic->GetWndPos().y + m_ActiveObjectiveStatic->GetHeight(), m_descriptionStatic->GetWndPos().y + m_descriptionStatic->GetHeight()); + SetHeight(h); + switch (obj.GetTaskState()) + { + //. case eTaskUserDefined: + case eTaskStateInProgress: + m_stateStatic->InitTexture("ui_icons_PDA_subtask_active"); + m_descriptionStatic->SetTextColor(m_active_color); + break; + case eTaskStateFail: + m_stateStatic->InitTexture("ui_icons_PDA_subtask_failed"); + m_descriptionStatic->SetTextColor(m_failed_color); + break; + case eTaskStateCompleted: + m_stateStatic->InitTexture("ui_icons_PDA_subtask_accomplished"); + m_descriptionStatic->SetTextColor(m_accomplished_color); + break; + default: NODEFAULT; + }; +} + +void CUITaskSubItem::Update() +{ + inherited::Update(); + CGameTask* activeTask = Level().GameTaskManager().ActiveTask(); + const bool bIsActive = activeTask == m_GameTask && m_GameTask->ActiveObjectiveIdx() == m_TaskObjectiveIdx; + m_ActiveObjectiveStatic->Show(bIsActive); + m_showDescriptionBtn->Show(m_EventsWnd->ItemHasDescription(this)); +} + +bool CUITaskSubItem::OnDbClick() +{ + OnItemClicked(this, nullptr); + return true; +} + +void CUITaskSubItem::OnActiveObjectiveClicked() { OnItemClicked(this, nullptr); } + +void CUITaskSubItem::OnShowDescriptionClicked(CUIWindow*, void*) +{ + OnItemClicked(this, nullptr); +} + +void CUITaskSubItem::MarkSelected(bool b) { m_showDescriptionBtn->SetButtonState(b ? CUIButton::BUTTON_PUSHED : CUIButton::BUTTON_NORMAL); } diff --git a/src/xrGame/ui/UITaskItem.h b/src/xrGame/ui/UITaskItem.h new file mode 100644 index 00000000000..399db8f937c --- /dev/null +++ b/src/xrGame/ui/UITaskItem.h @@ -0,0 +1,136 @@ +#pragma once +#include "UIDialogWnd.h" +#include "xrUICore/ListWnd/UIListItem.h" +#include "xrUICore/Callbacks/UIWndCallback.h" + +class CGameTask; +class CUIStatic; +class CUIButton; +class SGameTaskObjective; +class CUIEventsWnd; +class CUIEditBoxEx; +class CUIEditBox; + +class CUISocTaskItem : public CUIListItem, public CUIWndCallback +{ + typedef CUIListItem inherited; + +protected: + CGameTask* m_GameTask; + u16 m_TaskObjectiveIdx; + void OnItemClicked(CUIWindow*, void*); + void Init(); + +public: + CUISocTaskItem(CUIEventsWnd* w); + virtual ~CUISocTaskItem(); + virtual void SendMessage(CUIWindow* pWnd, s16 msg, void* pData = NULL); + + virtual void SetGameTask(CGameTask* gt, u16 obj_idx); + + CGameTask* GameTask() { return m_GameTask; } + u16 ObjectiveIdx() { return m_TaskObjectiveIdx; } + SGameTaskObjective* Objective(); + + CUIEventsWnd* m_EventsWnd; +}; + +class CUITaskRootItem : public CUISocTaskItem +{ + typedef CUISocTaskItem inherited; + +protected: + CUIStatic* m_taskImage; + CUIStatic* m_captionStatic; + CUIStatic* m_captionTime; + CUIStatic* m_remTimeStatic; + CUI3tButton* m_switchDescriptionBtn; + bool m_curr_descr_mode; + void Init(); + +public: + CUITaskRootItem(CUIEventsWnd* w); + virtual ~CUITaskRootItem(); + virtual void Update(); + virtual void SetGameTask(CGameTask* gt, u16 obj_idx); + void OnSwitchDescriptionClicked(CUIWindow*, void*); + + virtual void MarkSelected(bool b); + virtual bool OnDbClick(); +}; + +class CUITaskSubItem : public CUISocTaskItem +{ + typedef CUISocTaskItem inherited; + u32 m_active_color; + u32 m_failed_color; + u32 m_accomplished_color; + u32 m_skiped_color; + +protected: + CUIStatic* m_ActiveObjectiveStatic; + CUI3tButton* m_showDescriptionBtn; + CUIStatic* m_descriptionStatic; + CUIStatic* m_stateStatic; + + void Init(); + +public: + CUITaskSubItem(CUIEventsWnd* w); + virtual ~CUITaskSubItem(); + virtual void Update(); + virtual void SetGameTask(CGameTask* gt, u16 obj_idx); + void OnActiveObjectiveClicked(); + void OnShowDescriptionClicked(CUIWindow*, void*); + virtual void MarkSelected(bool b); + virtual bool OnDbClick(); +}; +/* +class CUIUserTaskEditWnd; +class CUIUserTaskItem :public CUITaskItem +{ + typedef CUITaskItem inherited; +protected: + CUI3tButton* m_showPointerBtn; + CUI3tButton* m_showLocationBtn; + CUI3tButton* m_editTextBtn; + CUI3tButton* m_removeBtn; + CUIStatic* m_captionStatic; + CUIStatic* m_descriptionStatic; + CUIStatic* m_image; + CUIUserTaskEditWnd* m_edtWnd; + void Init (); + +public: + CUIUserTaskItem (CUIEventsWnd* w); + virtual ~CUIUserTaskItem (); + virtual void Update (); + virtual void SetGameTask (CGameTask* gt, u16 obj_idx); + void OnShowLocationClicked (); + void OnShowPointerClicked (); + void OnDescriptionChanged (); + void OnEditTextClicked (); + void OnRemoveClicked (); + + virtual bool OnDbClick () {return true;}; + virtual void MarkSelected (bool b); +}; + +class CUIUserTaskEditWnd : public CUIDialogWnd, public CUIWndCallback +{ + CUIUserTaskItem* m_userTask; + CUI3tButton* m_btnOk; + CUI3tButton* m_btnCancel; + CUIFrameWindow* m_background; + + CUIEditBox* m_editCaption; + CUIEditBoxEx* m_editDescription; +protected: + void OnOk (); + void OnCancel (); + void Init (); +public: + CUIUserTaskEditWnd (CUIUserTaskItem* itm); + virtual void SendMessage (CUIWindow* pWnd, s16 msg, void* pData = NULL); + void Start (); +};*/ diff --git a/src/xrGame/ui/UITreeViewItem.cpp b/src/xrGame/ui/UITreeViewItem.cpp new file mode 100644 index 00000000000..90fb90bfd52 --- /dev/null +++ b/src/xrGame/ui/UITreeViewItem.cpp @@ -0,0 +1,578 @@ +//============================================================================= +// Filename: UITreeViewItem.cpp +// Created by Roman E. Marchenko, vortex@gsc-game.kiev.ua +// Copyright 2004. GSC Game World +// --------------------------------------------------------------------------- +// TreeView Item class +//============================================================================= + +#include "StdAfx.h" +#include "UITreeViewItem.h" +#include "xrUICore/ListWnd/UIListWnd.h" +#include "xrEngine/StringTable/StringTable.h" + +#define UNREAD_COLOR 0xff00ff00 +#define READ_COLOR 0xffffffff + +////////////////////////////////////////////////////////////////////////// + +// Смещение относительно родителя +const int subShift = 1; +const char* const treeItemBackgroundTexture = "ui\\ui_pda_over_list"; +// Цвет непрочитанного элемента +static const u32 unreadColor = 0xff00ff00; + +////////////////////////////////////////////////////////////////////////// + +CUITreeViewItem::CUITreeViewItem() : isRoot(false), isOpened(false), iTextShift(0), pOwner(NULL), m_uUnreadedColor(UNREAD_COLOR), m_uReadedColor(READ_COLOR) +{ + // The original SoC CUIListItem constructor left-aligned list text. + // The shared modern list item now uses the centered button default. + if (ShadowOfChernobylMode) + SetTextAlignment(CGameFont::alLeft); + + AttachChild(&UIBkg); + UIBkg.InitTexture(treeItemBackgroundTexture); + UIBkg.TextureOff(); + UIBkg.SetTextureOffset(-20, 0); + SetHighlightText(false); + + m_bManualSetColor = false; +} + +////////////////////////////////////////////////////////////////////////// + +CUITreeViewItem::~CUITreeViewItem() { DeleteAllSubItems(); } + +////////////////////////////////////////////////////////////////////////// + +void CUITreeViewItem::OnRootChanged() +{ + xr_string str; + if (isRoot) + { + // Вставляем после последнего пробела перед текстом знак + или - + str = GetText(); + + xr_string::size_type pos = str.find_first_not_of(' '); + if (xr_string::npos == pos) + pos = 0; + + if (pos == 0) + { + ++iTextShift; + str.insert(0, " "); + } + else + --pos; + + if (isOpened) + // Add minus sign + str.replace(pos, 1, "-"); + else + // Add plus sign + str.replace(pos, 1, "+"); + + inherited::SetText(str.c_str()); + } + else + { + str = GetText(); + // Remove "+/-" sign + xr_string::size_type pos = str.find_first_of("+-"); + + if (pos == 0) + { + for (int i = 0; i < iTextShift; ++i) + str.insert(pos, " "); + } + else + str.replace(pos, 1, " "); + + inherited::SetText(str.c_str()); + } +} + +////////////////////////////////////////////////////////////////////////// + +void CUITreeViewItem::OnOpenClose() +{ + // Если мы не являемся узлом дерева, значит ничего не делаем + if (!isRoot) + return; + + xr_string str; + + str = GetText(); + xr_string::size_type pos = str.find_first_of("+-"); + + if (xr_string::npos != pos) + { + if (isOpened) + // Change minus sign to plus + str.replace(pos, 1, "-"); + else + // Change plus sign to minus + str.replace(pos, 1, "+"); + } + + inherited::SetText(str.c_str()); +} + +////////////////////////////////////////////////////////////////////////// + +void CUITreeViewItem::Open() +{ + // Если не рут или уже открыты, то ничего не делаем + if (!isRoot || isOpened) + return; + isOpened = true; + + // Изменяем состояние + OnOpenClose(); + + // Аттачим все подэлементы к родтельскому листбоксу + CUIListWnd* pList = smart_cast(GetParent()); + + R_ASSERT(pList); + if (!pList) + return; + + int pos = pList->GetItemPos(this); + + for (SubItems_it it = vSubItems.begin(); it != vSubItems.end(); ++it) + { + pList->AddItem(*it, ++pos); + } +} + +////////////////////////////////////////////////////////////////////////// + +void CUITreeViewItem::Close() +{ + // Если не рут или уже открыты, то ничего не делаем + if (!isRoot || !isOpened) + return; + isOpened = false; + + // Изменяем состояние + OnOpenClose(); + + // Детачим все подэлементы + CUIListWnd* pList = smart_cast(GetParent()); + + R_ASSERT(pList); + if (!pList) + return; + + int pos; + + // Сначала все закрыть + for (SubItems_it it = vSubItems.begin(); it != vSubItems.end(); ++it) + { + (*it)->Close(); + } + + // Затем все датачим + for (SubItems_it it = vSubItems.begin(); it != vSubItems.end(); ++it) + { + pos = pList->GetItemPos(*it); + pList->RemoveItem(pos); + } +} + +////////////////////////////////////////////////////////////////////////// + +void CUITreeViewItem::AddItem(CUITreeViewItem* pItem) +{ + R_ASSERT(pItem); + if (!pItem) + return; + + pItem->SetTextShift(subShift + iTextShift); + + vSubItems.push_back(pItem); + pItem->SetAutoDelete(false); + + pItem->SetOwner(this); + pItem->SetText(pItem->GetText()); +} + +////////////////////////////////////////////////////////////////////////// + +// вызывается из деструктора +void CUITreeViewItem::DeleteAllSubItems() +{ + for (auto* vSubItem : vSubItems) + { + if (!vSubItem->GetParent()) // удалям явно только если у саб айтема нету родителя, иначе он будет удален самим родителем + { + xr_delete(vSubItem); + } + } + + vSubItems.clear(); +} + +////////////////////////////////////////////////////////////////////////// + +void CUITreeViewItem::SetRoot(bool set) +{ + if (isRoot) + return; + + isRoot = set; + OnRootChanged(); +} + +////////////////////////////////////////////////////////////////////////// + +void CUITreeViewItem::SetText(LPCSTR str) +{ + xr_string s = str; + xr_string::size_type pos = s.find_first_not_of(" +-"); + + if (pos < static_cast(iTextShift)) + { + for (u32 i = 0; i < iTextShift - pos; ++i) + s.insert(0, " "); + } + else if (pos > static_cast(iTextShift)) + { + s.erase(0, pos - iTextShift); + } + + inherited::SetText(s.c_str()); +} + +////////////////////////////////////////////////////////////////////////// + +void CUITreeViewItem::SendMessage(CUIWindow* pWnd, s16 msg, void* pData) +{ + static CUITreeViewItem* pPrevFocusedItem = NULL; + + if (pWnd == this && BUTTON_CLICKED == msg) + { + if (IsRoot()) + { + IsOpened() ? Close() : Open(); + } + else + { + MarkArticleAsRead(true); + } + } + else if (pWnd == this && WINDOW_FOCUS_RECEIVED == msg) + { + UIBkg.TextureOn(); + + if (pPrevFocusedItem) + { + pPrevFocusedItem->UIBkg.TextureOff(); + } + pPrevFocusedItem = this; + } + else if (pWnd == this && WINDOW_FOCUS_LOST == msg) + { + UIBkg.TextureOff(); + pPrevFocusedItem = NULL; + } + else + inherited::SendMessage(pWnd, msg, pData); +} + +////////////////////////////////////////////////////////////////////////// + +CUITreeViewItem* CUITreeViewItem::Find(LPCSTR text) const +{ + // Пробегаемся по списку подчиненных элементов, и ищем элемент с заданным текстом + // Если среди подч. эл-тов есть root'ы, то ищем рекурсивно в них + CUITreeViewItem* pResult = NULL; + xr_string caption; + + for (SubItems::const_iterator it = vSubItems.begin(); it != vSubItems.end(); ++it) + { + caption = (*it)->GetText(); + xr_string::size_type pos = caption.find_first_not_of(" +-"); + if (pos != xr_string::npos) + { + caption.erase(0, pos); + } + + if (xr_strcmp(caption.c_str(), text) == 0) + pResult = *it; + + if ((*it)->IsRoot() && !pResult) + pResult = (*it)->Find(text); + + if (pResult) + break; + } + + return pResult; +} + +////////////////////////////////////////////////////////////////////////// + +CUITreeViewItem* CUITreeViewItem::Find(int value) const +{ + CUITreeViewItem* pResult = NULL; + + for (SubItems::const_iterator it = vSubItems.begin(); it != vSubItems.end(); ++it) + { + if ((*it)->GetValue() == value) + pResult = *it; + + if ((*it)->IsRoot() && !pResult) + pResult = (*it)->Find(value); + + if (pResult) + break; + } + + return pResult; +} + +////////////////////////////////////////////////////////////////////////// + +CUITreeViewItem* CUITreeViewItem::Find(CUITreeViewItem* pItem) const +{ + CUITreeViewItem* pResult = NULL; + + for (SubItems::const_iterator it = vSubItems.begin(); it != vSubItems.end(); ++it) + { + if ((*it)->IsRoot() && !pResult) + pResult = (*it)->Find(pItem); + else if (pItem == *it) + pResult = *it; + + if (pResult) + break; + } + + return pResult; +} + +////////////////////////////////////////////////////////////////////////// + +xr_string CUITreeViewItem::GetHierarchyAsText() +{ + xr_string name; + + if (GetOwner()) + { + name = GetOwner()->GetHierarchyAsText(); + } + + xr_string::size_type prevPos = name.size() + 1; + name += static_cast("/") + static_cast(GetText()); + + // Удаляем мусор: [ +-] + xr_string::size_type pos = name.find_first_not_of("/ +-", prevPos); + if (xr_string::npos != pos) + { + name.erase(prevPos, pos - prevPos); + } + + return name; +} + +////////////////////////////////////////////////////////////////////////// + +void CUITreeViewItem::MarkArticleAsRead(bool value) +{ + // Если элемент рутовый, то мы его маркаем его, и все чилды + if (IsRoot()) + { + m_bArticleRead = value; + if (!m_bManualSetColor) + SetItemColor(); + + for (SubItems_it it = vSubItems.begin(); it != vSubItems.end(); ++it) + { + (*it)->m_bArticleRead = value; + (*it)->SetItemColor(); + if ((*it)->IsRoot()) + (*it)->MarkArticleAsRead(value); + } + } + else + { + // Если же нет, то маркаем себя и говорим проверить свой парентовый элемент + m_bArticleRead = value; + if (!m_bManualSetColor) + SetItemColor(); + CheckParentMark(GetOwner()); + } +} + +////////////////////////////////////////////////////////////////////////// + +void CUITreeViewItem::CheckParentMark(CUITreeViewItem* pOwner) +{ + // Берем рута, смотрим на его чилдов, и если среди них есть хоть 1 + // непрочитанный, то маркаем себя как непрочитанный, и говорим провериться выше. + bool f = false; + if (pOwner && pOwner->IsRoot()) + { + for (SubItems_it it = pOwner->vSubItems.begin(); it != pOwner->vSubItems.end(); ++it) + { + if (!(*it)->IsArticleReaded()) + { + pOwner->m_bArticleRead = false; + pOwner->SetItemColor(); + f = true; + } + } + + if (!f) + { + // Если мы тут, то все артиклы прочитанны, и можно маркнуть себя как прочитанная ветвь + pOwner->m_bArticleRead = true; + pOwner->SetItemColor(); + } + + pOwner->CheckParentMark(pOwner->GetOwner()); + } +} + +////////////////////////////////////////////////////////////////////////// +// Standalone function for tree hierarchy creation +////////////////////////////////////////////////////////////////////////// + +void CreateTreeBranch(shared_str nesting, shared_str leafName, CUIListWnd* pListToAdd, int leafProperty, CGameFont* pRootFont, u32 rootColor, CGameFont* pLeafFont, u32 leafColor, + bool markRead) +{ + // Nested function emulation + class AddTreeTail_ + { + private: + CGameFont* pRootFnt; + u32 rootItemColor; + + public: + AddTreeTail_(CGameFont* f, u32 cl) : pRootFnt(f), rootItemColor(cl) {} + + CUITreeViewItem* operator()(GroupTree_it it, GroupTree& cont, CUITreeViewItem* pItemToIns) + { + // Вставляем иерархию разделов в энциклопедию + CUITreeViewItem* pNewItem = NULL; + + for (GroupTree_it it2 = it; it2 != cont.end(); ++it2) + { + pNewItem = xr_new(); + pItemToIns->AddItem(pNewItem); + pNewItem->SetFont(pRootFnt); + pNewItem->SetText(it2->c_str()); + pNewItem->SetReadedColor(rootItemColor); + pNewItem->SetRoot(true); + pItemToIns = pNewItem; + } + + return pNewItem; + } + } AddTreeTail(pRootFont, rootColor); + + //----------------------------------------------------------------------------- + // Function body + //----------------------------------------------------------------------------- + + // Начинаем алгоритм определения группы вещи в иерархии энциклопедии + R_ASSERT(nesting.size()); + R_ASSERT(pListToAdd); + R_ASSERT(pLeafFont); + R_ASSERT(pRootFont); + xr_string group = nesting.c_str(); + + // Парсим строку группы для определения вложенности + GroupTree groupTree; + + xr_string::size_type pos; + xr_string oneLevel; + + while (true) + { + pos = group.find('/'); + if (pos != xr_string::npos) + { + oneLevel.assign(group, 0, pos); + shared_str str(oneLevel.c_str()); + groupTree.push_back(CStringTable().translate(str)); + group.erase(0, pos + 1); + } + else + { + groupTree.push_back(CStringTable().translate(group.c_str())); + break; + } + } + + // Теперь ищем нет ли затребованных групп уже в наличии + CUITreeViewItem *pTVItem = NULL, *pTVItemChilds = NULL; + bool status = false; + + // Для всех рутовых элементов + for (int i = 0; i < pListToAdd->GetItemsCount(); ++i) + { + pTVItem = smart_cast(pListToAdd->GetItem(i)); + R_ASSERT(pTVItem); + + pTVItem->Close(); + + xr_string caption = pTVItem->GetText(); + // Remove "+" sign + caption.erase(0, 1); + + // Ищем не содержит ли он данной иерархии и добавляем новые элементы если не найдено + if (0 == xr_strcmp(caption.c_str(), groupTree.front().c_str())) + { + // Уже содержит. Надо искать глубже + pTVItemChilds = pTVItem; + for (GroupTree_it it = groupTree.begin() + 1; it != groupTree.end(); ++it) + { + pTVItem = pTVItemChilds->Find(it->c_str()); + // Не нашли, надо вставлять хвост списка вложенности + if (!pTVItem) + { + pTVItemChilds = AddTreeTail(it, groupTree, pTVItemChilds); + status = true; + break; + } + pTVItemChilds = pTVItem; + } + } + + if (status) + break; + } + + // Прошли все существующее дерево, и не нашли? Тогда добавляем новую иерархию + if (!pTVItemChilds) + { + pTVItemChilds = xr_new(); + pTVItemChilds->SetFont(pRootFont); + pTVItemChilds->SetText(groupTree.front().c_str()); + pTVItemChilds->SetReadedColor(rootColor); + pTVItemChilds->SetRoot(true); + pListToAdd->AddItem(pTVItemChilds); + + // Если в списке вложенности 1 элемент, то хвоста нет, и соответственно ничего не добавляем + if (groupTree.size() > 1) + pTVItemChilds = AddTreeTail(groupTree.begin() + 1, groupTree, pTVItemChilds); + } + + // К этому моменту pTVItemChilds обязательно должна быть не NULL + R_ASSERT(pTVItemChilds); + + // Cначала проверяем нет ли записи с таким названием, и добавляем если нет + // if (!pTVItemChilds->Find(*name)) + // { + pTVItem = xr_new(); + pTVItem->SetFont(pLeafFont); + pTVItem->SetReadedColor(leafColor); + pTVItem->SetText(CStringTable().translate(leafName.c_str()).c_str()); + pTVItem->SetValue(leafProperty); + pTVItemChilds->AddItem(pTVItem); + pTVItem->MarkArticleAsRead(markRead); + // } +} diff --git a/src/xrGame/ui/UITreeViewItem.h b/src/xrGame/ui/UITreeViewItem.h new file mode 100644 index 00000000000..4b3beb71840 --- /dev/null +++ b/src/xrGame/ui/UITreeViewItem.h @@ -0,0 +1,131 @@ +//============================================================================= +// Filename: UITreeViewItem.h +// Created by Roman E. Marchenko, vortex@gsc-game.kiev.ua +// Copyright 2004. GSC Game World +// --------------------------------------------------------------------------- +// TreeView Item class +//============================================================================= + +#ifndef UI_TREE_VIEW_ITEM_H_ +#define UI_TREE_VIEW_ITEM_H_ + +#pragma once + +#include "xrUICore/ListWnd/UIListItem.h" + +class CUIListWnd; + +class CUITreeViewItem : public CUIListItem +{ + typedef CUIListItem inherited; + // Являемся ли мы началом подыерархии + bool isRoot; + // Если мы рут, то этот флаг показывает открыта наша подыерархия или нет + bool isOpened; + // Смещение в пробелах + int iTextShift; + // Кому мы пренадлежим + CUITreeViewItem* pOwner; + +public: + void SetRoot(bool set); + bool IsRoot() const { return isRoot; } + + // Устанавливаем смещение текста + void SetTextShift(int delta) { iTextShift += delta; } + + // Владелец + CUITreeViewItem* GetOwner() const { return pOwner; } + void SetOwner(CUITreeViewItem* owner) { pOwner = owner; } + +protected: + // Функция вызываемая при изменении свойства рута + // для изменения визуального представления себя + virtual void OnRootChanged(); + +public: + // Раксрыть/свернуть локальнцю иерархию + void Open(); + void Close(); + bool IsOpened() const { return isOpened; } + +protected: + // Функция вызываемая при изменении cостояния открыто/закрыто + // для изменения визуального представления себя + virtual void OnOpenClose(); + +public: + // Список элементов, которые уровнем ниже нас + typedef xr_vector SubItems; + typedef SubItems::iterator SubItems_it; + SubItems vSubItems; + + CUIStatic UIBkg; + + // Добавить элемент + void AddItem(CUITreeViewItem* pItem); + // Удалить все + void DeleteAllSubItems(); + // Найти элемент с заданным именем + // Return: указатель на элемент, если нашли, либо NULL в противном случае + CUITreeViewItem* Find(LPCSTR text) const; + // Найти элемент с заданным значением + // Return: указатель на элемент, если нашли, либо NULL в противном случае + CUITreeViewItem* Find(int value) const; + // Найти заданный элемент + // Return: указатель на элемент, если нашли, либо NULL в противном случае + CUITreeViewItem* Find(CUITreeViewItem* pItem) const; + // Вернуть иерархию от верха до текущего элемента в виде строки-пути + // Рутовые элементы заканчиваются символом "/" + xr_string GetHierarchyAsText(); + + // Redefine some stuff + // ATTENTION! Для корректного функционирования значков [+-] вызов SetText + // Должен предшествовать SetRoot + virtual void SetText(LPCSTR str); + virtual void SendMessage(CUIWindow* pWnd, s16 msg, void* pData); + + // Ctor and Dtor + CUITreeViewItem(); + virtual ~CUITreeViewItem(); + + pcstr GetDebugType() override { return "CUITreeViewItem"; } + + // Устанавливаем цвет текста в зависимости от того, прочитан ли артикл + void MarkArticleAsRead(bool value); + bool IsArticleReaded() { return m_bArticleRead; } + // Цвет текста когда артикл не прочитан и не прочитан + void SetReadedColor(u32 cl) { m_uReadedColor = cl; } + void SetUnreadedColor(u32 cl) { m_uUnreadedColor = cl; } + void SetManualSetColor(bool val) { m_bManualSetColor = val; } + // Устанавливаем цвет в зависимости от состояния элемента + void SetItemColor() { m_bArticleRead ? SetTextColor(m_uReadedColor) : SetTextColor(m_uUnreadedColor); } + +private: + friend class CUITreeViewItem; + + // Применить состояние вверх по иерархии + void CheckParentMark(CUITreeViewItem* pOwner); + // Цвет текста когда артикл не прочитан + u32 m_uUnreadedColor; + // Цвет текста когда артикл не прочитан + u32 m_uReadedColor; + // Флажек состояния прочитки + bool m_bArticleRead{}; + // Если true, то MarkArticleAsRead не будет вызывать + // SetItemColor() + bool m_bManualSetColor; +}; + +////////////////////////////////////////////////////////////////////////// +// Function for automatic tree hierarchy creation +////////////////////////////////////////////////////////////////////////// + +DEF_VECTOR(GroupTree, shared_str); + +////////////////////////////////////////////////////////////////////////// + +void CreateTreeBranch(shared_str nestingTree, shared_str leafName, CUIListWnd* pListToAdd, int leafProperty, CGameFont* pRootFont, u32 rootColor, CGameFont* pLeafFont, + u32 leafColor, bool markRead); + +#endif // UI_TREE_VIEW_ITEM_H_ diff --git a/src/xrGame/ui/map_hint.cpp b/src/xrGame/ui/map_hint.cpp index 9e260b915d5..d97138153f0 100644 --- a/src/xrGame/ui/map_hint.cpp +++ b/src/xrGame/ui/map_hint.cpp @@ -89,7 +89,9 @@ void CUIMapLocationHint::SetInfoMSpot(CMapSpot* spot) CMapLocation* ml = spot->MapLocation(); CGameTask* gt = Level().GameTaskManager().HasGameTask(ml, true); - if (gt) + // SoC has only the simple map-hint layout. The detailed task layout is + // a CoP control set and its fields do not exist in hint_item.xml. + if (gt && !ShadowOfChernobylMode) SetInfoTask(gt); else SetInfoStr(ml->GetHint()); @@ -97,6 +99,18 @@ void CUIMapLocationHint::SetInfoMSpot(CMapSpot* spot) void CUIMapLocationHint::SetInfoTask(CGameTask* task) { + if (!task) + return; + + // Keep this path safe for UI layouts which do not define detailed task + // controls. This also protects custom game-data layouts. + if (!m_info["t_icon"] || !m_info["t_caption"] || !m_info["t_time"] || + !m_info["t_time_rem"] || !m_info["t_hint_text"]) + { + SetInfoStr(task->m_Title.c_str()); + return; + } + SetInfoMode(2); CUIStatic* S = m_info["t_icon"]; diff --git a/src/xrGame/xrGame.vcxproj b/src/xrGame/xrGame.vcxproj index f6e47441c5f..ca0e02f5243 100644 --- a/src/xrGame/xrGame.vcxproj +++ b/src/xrGame/xrGame.vcxproj @@ -1270,10 +1270,14 @@ + + + + @@ -1311,10 +1315,14 @@ + + + + @@ -1333,12 +1341,16 @@ + + + + @@ -3101,10 +3113,14 @@ true + + + + @@ -3185,10 +3201,14 @@ + + + + @@ -3217,12 +3237,16 @@ + + + + @@ -3398,4 +3422,4 @@ - \ No newline at end of file + diff --git a/src/xrGame/xrGame.vcxproj.filters b/src/xrGame/xrGame.vcxproj.filters index e15a23b5871..2ee64f01c35 100644 --- a/src/xrGame/xrGame.vcxproj.filters +++ b/src/xrGame/xrGame.vcxproj.filters @@ -5703,6 +5703,15 @@ UI\Common\PDA + + UI\Common\PDA + + + UI\Common\PDA + + + UI\Common\PDA + UI\Common\PDA\MAP @@ -5727,6 +5736,9 @@ UI\Common\PDA\Statistics & Rankings + + UI\Common\PDA\Statistics & Rankings + UI\Common\PDA\Tasks @@ -5736,12 +5748,36 @@ UI\Common\PDA\Tasks + + UI\Common\PDA\Tasks + + + UI\Common\PDA\Tasks + UI\Common\PDA\logs + + UI\Common\PDA\logs + + + UI\Common\PDA\logs + + + UI\Common\PDA\logs + + + UI\Common\PDA\logs + + + UI\Common\PDA\logs + UI\Common\PDA\logs\News + + UI\Common\PDA\logs\News + UI\Common\ui-item-infos @@ -8866,6 +8902,15 @@ UI\Common\PDA + + UI\Common\PDA + + + UI\Common\PDA + + + UI\Common\PDA + UI\Common\PDA\MAP @@ -8893,6 +8938,9 @@ UI\Common\PDA\Statistics & Rankings + + UI\Common\PDA\Statistics & Rankings + UI\Common\PDA\Tasks @@ -8902,12 +8950,36 @@ UI\Common\PDA\Tasks + + UI\Common\PDA\Tasks + + + UI\Common\PDA\Tasks + UI\Common\PDA\logs + + UI\Common\PDA\logs + + + UI\Common\PDA\logs + + + UI\Common\PDA\logs + + + UI\Common\PDA\logs + + + UI\Common\PDA\logs + UI\Common\PDA\logs\News + + UI\Common\PDA\logs\News + UI\Common\ui-item-infos @@ -9493,4 +9565,4 @@ - \ No newline at end of file + diff --git a/src/xrServerEntities/script_net_packet_script.cpp b/src/xrServerEntities/script_net_packet_script.cpp index 5524bd877ac..ca4e91294ba 100644 --- a/src/xrServerEntities/script_net_packet_script.cpp +++ b/src/xrServerEntities/script_net_packet_script.cpp @@ -69,7 +69,18 @@ void CScriptNetPacket::script_register(lua_State* luaState) .def("r_float", (float (NET_Packet::*)())(&NET_Packet::r_float)) .def("r_u64", (u64(NET_Packet::*)())(&NET_Packet::r_u64)) .def("r_s64", (s64(NET_Packet::*)())(&NET_Packet::r_s64)) - .def("r_u32", (u32(NET_Packet::*)())(&NET_Packet::r_u32)) + .def("r_u32", +[](NET_Packet* self) -> lua_Number + { + const u32 value = self->r_u32(); + + // SoC scripts use -1 as a u32 sentinel. In the original 32-bit + // engine, Lua received 0xffffffff as -1. Keep that behavior in + // SoC mode so original saves do not contain invalid object IDs. + if (ShadowOfChernobylMode) + return static_cast(value); + + return value; + }) .def("r_s32", (s32(NET_Packet::*)())(&NET_Packet::r_s32)) .def("r_u16", (u16(NET_Packet::*)())(&NET_Packet::r_u16)) .def("r_s16", (s16(NET_Packet::*)())(&NET_Packet::r_s16)) diff --git a/src/xrServerEntities/script_reader_script.cpp b/src/xrServerEntities/script_reader_script.cpp index d00cfde5de9..6b8968f9b38 100644 --- a/src/xrServerEntities/script_reader_script.cpp +++ b/src/xrServerEntities/script_reader_script.cpp @@ -24,7 +24,18 @@ void CScriptReader::script_register(lua_State* luaState) .def("r_float", &IReader::r_float) .def("r_u64", &IReader::r_u64) .def("r_s64", &IReader::r_s64) - .def("r_u32", &IReader::r_u32) + .def("r_u32", +[](IReader* self) -> lua_Number + { + const u32 value = self->r_u32(); + + // SoC scripts use -1 as a u32 sentinel. In the original 32-bit + // engine, Lua received 0xffffffff as -1. Keep that behavior in + // SoC mode so original saves do not contain invalid object IDs. + if (ShadowOfChernobylMode) + return static_cast(value); + + return value; + }) .def("r_s32", &IReader::r_s32) .def("r_u16", &IReader::r_u16) .def("r_s16", &IReader::r_s16) diff --git a/src/xrUICore/Buttons/UI3tButton.cpp b/src/xrUICore/Buttons/UI3tButton.cpp index 94c913c8310..3423ad1cde4 100644 --- a/src/xrUICore/Buttons/UI3tButton.cpp +++ b/src/xrUICore/Buttons/UI3tButton.cpp @@ -161,7 +161,7 @@ void CUI3tButton::DrawTexture() { if (m_background) { - m_background->SetStretchTexture(true); + m_background->SetStretchTexture(m_stretchBackgroundTexture); m_background->Draw(); } else if (m_back_frameline) diff --git a/src/xrUICore/Buttons/UI3tButton.h b/src/xrUICore/Buttons/UI3tButton.h index 25176961eb6..73af47e75ed 100644 --- a/src/xrUICore/Buttons/UI3tButton.h +++ b/src/xrUICore/Buttons/UI3tButton.h @@ -25,6 +25,7 @@ class XRUICORE_API CUI3tButton : public CUIButton virtual void InitButton(Fvector2 pos, Fvector2 size); bool InitTexture(pcstr texture, bool fatal = true) override; virtual bool InitTexture(pcstr tex_enabled, pcstr tex_disabled, pcstr tex_touched, pcstr tex_highlighted, bool fatal = true); + void SetBackgroundStretchTexture(bool value) { m_stretchBackgroundTexture = value; } //. void SetTextColor (u32 color); virtual void SetTextureOffset(float x, float y); @@ -56,6 +57,7 @@ class XRUICORE_API CUI3tButton : public CUIButton CUI_IB_FrameLineWnd* m_back_frameline; private: + bool m_stretchBackgroundTexture{ true }; void PlaySoundH(); void PlaySoundT(); diff --git a/src/xrUICore/Buttons/UICheckButton.cpp b/src/xrUICore/Buttons/UICheckButton.cpp index 8b52334eb1e..736b1b3ca5e 100644 --- a/src/xrUICore/Buttons/UICheckButton.cpp +++ b/src/xrUICore/Buttons/UICheckButton.cpp @@ -11,6 +11,8 @@ CUICheckButton::CUICheckButton() { TextItemControl()->SetTextAlignment(CGameFont::alLeft); SetButtonAsSwitch(true); + if (ShadowOfChernobylMode) + SetBackgroundStretchTexture(false); m_pDependControl = NULL; } diff --git a/src/xrUICore/Static/UIStatic.h b/src/xrUICore/Static/UIStatic.h index 3d3654d4505..28dada73977 100644 --- a/src/xrUICore/Static/UIStatic.h +++ b/src/xrUICore/Static/UIStatic.h @@ -33,7 +33,7 @@ class XRUICORE_API CUIStatic : public CUIWindow, public ITextureOwner, public CU void EnableHeading_int(bool b) { m_bHeading = b; } public: - CUIStatic(pcstr window_name); + CUIStatic(pcstr window_name = "CUIStatic"); ~CUIStatic() override; virtual void Draw(); diff --git a/src/xrUICore/Windows/UIFrameLineWnd.h b/src/xrUICore/Windows/UIFrameLineWnd.h index 7b29c45c01b..3dda48a9e7c 100644 --- a/src/xrUICore/Windows/UIFrameLineWnd.h +++ b/src/xrUICore/Windows/UIFrameLineWnd.h @@ -17,7 +17,7 @@ class XRUICORE_API CUIFrameLineWnd : public CUIWindow, public ITextureOwner flMax }; - CUIFrameLineWnd(pcstr window_name); + CUIFrameLineWnd(pcstr window_name = "CUIFrameLineWnd"); bool InitTexture(pcstr texture, bool fatal = true) override; bool InitTextureEx(pcstr texture, pcstr shader = "hud" DELIMITER "default", bool fatal = true) override; diff --git a/src/xrUICore/Windows/UIFrameWindow.h b/src/xrUICore/Windows/UIFrameWindow.h index 2a129973b9c..caa8c614455 100644 --- a/src/xrUICore/Windows/UIFrameWindow.h +++ b/src/xrUICore/Windows/UIFrameWindow.h @@ -9,7 +9,7 @@ class XRUICORE_API CUIFrameWindow : public CUIWindow, public ITextureOwner typedef CUIWindow inherited; public: - CUIFrameWindow(pcstr window_name); + CUIFrameWindow(pcstr window_name = "CUIFrameWindow"); virtual void SetTextureRect(const Frect& r){}; virtual const Frect& GetTextureRect() const { return m_tex_rect[fmBK]; } diff --git a/src/xrUICore/Windows/UIWindow.cpp b/src/xrUICore/Windows/UIWindow.cpp index b027e93af35..e745e87ee8b 100644 --- a/src/xrUICore/Windows/UIWindow.cpp +++ b/src/xrUICore/Windows/UIWindow.cpp @@ -199,11 +199,15 @@ bool CUIWindow::OnMouseAction(float x, float y, EUIMessages mouse_action) //Проверка на попадание мыши в окно, //происходит в обратном порядке, чем рисование окон //(последние в списке имеют высший приоритет) - WINDOW_LIST::reverse_iterator it = m_ChildWndList.rbegin(); - - for (; it != m_ChildWndList.rend(); ++it) + // A mouse handler can attach or detach windows while it handles this event. + // Use a snapshot so those changes do not invalidate this iteration. + const WINDOW_LIST children = m_ChildWndList; + for (auto it = children.rbegin(); it != children.rend(); ++it) { CUIWindow* w = (*it); + if (!IsChild(w)) + continue; + const Frect& wndRect = w->GetWndRect(); if (wndRect.in(cursor_pos)) { @@ -391,11 +395,13 @@ void CUIWindow::SetKeyboardCapture(CUIWindow* pChildWindow, bool capture_status) //обработка сообщений void CUIWindow::SendMessage(CUIWindow* pWnd, s16 msg, void* pData) { - //оповестить дочерние окна - for (auto it = m_ChildWndList.begin(); m_ChildWndList.end() != it; ++it) + // A child can attach or detach windows while it handles a message. + // Use a snapshot so those changes do not invalidate this iteration. + const WINDOW_LIST children = m_ChildWndList; + for (CUIWindow* child : children) { - if ((*it)->IsEnabled()) - (*it)->SendMessage(pWnd, msg, pData); + if (IsChild(child) && child->IsEnabled()) + child->SendMessage(pWnd, msg, pData); } } diff --git a/src/xrUICore/Windows/UIWindow.h b/src/xrUICore/Windows/UIWindow.h index c325e3e4589..d3edc687f81 100644 --- a/src/xrUICore/Windows/UIWindow.h +++ b/src/xrUICore/Windows/UIWindow.h @@ -13,13 +13,17 @@ class XRUICORE_API CUIWindow : public CUIDebuggable { public: - CUIWindow(pcstr window_name); + CUIWindow(pcstr window_name = "CUIWindow"); ~CUIWindow() override; virtual void SetWndPos(const Fvector2& pos) { m_wndPos.set(pos.x, pos.y); } + void SetWndPos(float x, float y) + { + SetWndPos(Fvector2().set(x, y)); + } [[nodiscard]] const Fvector2& GetWndPos() const { diff --git a/src/xrUICore/XML/UIXmlInitBase.cpp b/src/xrUICore/XML/UIXmlInitBase.cpp index 09ef2c554d1..a510200ca8a 100644 --- a/src/xrUICore/XML/UIXmlInitBase.cpp +++ b/src/xrUICore/XML/UIXmlInitBase.cpp @@ -148,6 +148,22 @@ bool CUIXmlInitBase::InitStatic(CUIXml& xml_doc, pcstr path, int index, CUIStati InitTexture(xml_doc, path, index, pWnd); InitTextureOffset(xml_doc, path, index, pWnd); + if (ShadowOfChernobylMode && strstr(xml_doc.m_xml_file_name, "ui_mm_load_dlg.xml") && + xr_strcmp(path, "back_video") == 0) + { + // The original full-screen load-dialog video is not clipped correctly + // by the modern widescreen UI path and covers the load form. + pWnd->Show(false); + } + + if (ShadowOfChernobylMode && strstr(xml_doc.m_xml_file_name, "ui_mm_load_dlg.xml") && + xr_strcmp(path, "form:picture") == 0) + { + // The SoC XML uses clipper="1" to keep a large fallback image inside + // the save preview. The current UI renderer does not support that rule. + pWnd->SetStretchTexture(true); + } + cpcstr mirroring = xml_doc.ReadAttrib(path, index, "mirror", ""); if (0 == xr_strcmp(mirroring, "h")) pWnd->GetStaticItem()->SetMirrorMode(EUIMirroring::Horisontal); @@ -811,7 +827,8 @@ bool CUIXmlInitBase::InitTabControl(CUIXml& xml_doc, pcstr path, newButton->m_btn_id = xml_doc.ReadAttrib("button", i, "id"); if (!newButton->m_btn_id.size()) { - R_ASSERT4(defaultIdsAllowed, "Tab control tab doesn't have 'id' assigned.", xml_doc.m_xml_file_name, path); + R_ASSERT4(ShadowOfChernobylMode || defaultIdsAllowed, + "Tab control tab doesn't have 'id' assigned.", xml_doc.m_xml_file_name, path); Msg("~ [%s] doesn't have `id` tag in file [%s]", xml_doc.m_xml_file_name, path); string32 temp; xr_sprintf(temp, "%d", i); diff --git a/src/xrUICore/ui_export_script.cpp b/src/xrUICore/ui_export_script.cpp index 913957bbae1..23b80a23533 100644 --- a/src/xrUICore/ui_export_script.cpp +++ b/src/xrUICore/ui_export_script.cpp @@ -7,6 +7,19 @@ #include "Cursor/UICursor.h" #include "XML/UITextureMaster.h" +namespace +{ +TEX_INFO* GetTextureInfoForScript(pcstr name, pcstr defaultName = nullptr) +{ + // SoC reads the returned object in a second Lua call. Keep it alive after + // this binding returns instead of passing a short-lived value object. + static TEX_INFO info; + info = {}; + CUITextureMaster::FindItem(name, defaultName, info); + return &info; +} +} + #include "Windows/UIWindow.h" #include "Windows/UIFrameWindow.h" #include "Windows/UIFrameLineWnd.h" @@ -56,6 +69,7 @@ void UIStyleManager::script_register(lua_State* luaState) def("GetDefaultUIPathWithDelimiter", +[] { return UI_PATH_DEFAULT_WITH_DELIMITER; }), def("GetUIPath", +[] { return UI_PATH; }), def("GetUIPathWithDelimiter", +[] { return UI_PATH_WITH_DELIMITER; }), + def("IsShadowOfChernobylMode", +[] { return ShadowOfChernobylMode; }), class_("UIStyleManager") .def("GetAllStyles", &UIStyleManager::GetToken, return_stl_iterator()) @@ -128,15 +142,10 @@ void CUITextureMaster::script_register(lua_State* luaState) return CUITextureMaster::GetTextureRect(iconName); }), - def("GetTextureInfo", +[](pcstr name) - { - return CUITextureMaster::FindItem(name); - }), + def("GetTextureInfo", +[](pcstr name) { return GetTextureInfoForScript(name); }), - def("GetTextureInfo", +[](pcstr name, pcstr defaultName) - { - return CUITextureMaster::FindItem(name, defaultName); - }), + def("GetTextureInfo", + +[](pcstr name, pcstr defaultName) { return GetTextureInfoForScript(name, defaultName); }), def("GetTextureInfo", +[](pcstr name, TEX_INFO& outValue) { diff --git a/src/xr_3da/entry_point.cpp b/src/xr_3da/entry_point.cpp index 82d7da5d3aa..e53fdfabaca 100644 --- a/src/xr_3da/entry_point.cpp +++ b/src/xr_3da/entry_point.cpp @@ -11,6 +11,45 @@ #include #endif +#if defined(XR_PLATFORM_APPLE) +#include + +namespace +{ +bool EnvironmentFlagEnabled(pcstr name) +{ + pcstr value = SDL_getenv(name); + return value && value[0] && xr_strcmp(value, "0") != 0 && xr_strcmp(value, "false") != 0 && + xr_strcmp(value, "off") != 0; +} + +std::string GetBundledDefaultCommandLine() +{ + char* basePath = SDL_GetBasePath(); + if (!basePath) + return {}; + + std::string commandLinePath = basePath; + SDL_free(basePath); + commandLinePath += "../Resources/openxray/default_command_line.txt"; + + FILE* file = fopen(commandLinePath.c_str(), "r"); + if (!file) + return {}; + + char commandLine[1024]{}; + const bool hasCommandLine = fgets(commandLine, sizeof(commandLine), file) != nullptr; + fclose(file); + + if (!hasCommandLine) + return {}; + + commandLine[strcspn(commandLine, "\r\n")] = '\0'; + return commandLine; +} +} // namespace +#endif + // Always request high performance GPU extern "C" { @@ -84,29 +123,28 @@ int main(int argc, char *argv[]) try { - char* commandLine = nullptr; - int i; - if(argc > 1) +#if defined(XR_PLATFORM_APPLE) + std::string commandLine = GetBundledDefaultCommandLine(); + if (EnvironmentFlagEnabled("OPENXRAY_SKIP_INTRO") && commandLine.find("-nointro") == std::string::npos) { - size_t sum = 1; - for(i = 1; i < argc; ++i) - sum += strlen(argv[i]) + 1; - - commandLine = (char*)xr_malloc(sum); - ZeroMemory(commandLine, sum); - - for(i = 1; i < argc; ++i) - { - strcat(commandLine, argv[i]); - strcat(commandLine, " "); - } + if (!commandLine.empty()) + commandLine += ' '; + commandLine += "-nointro"; + } +#else + std::string commandLine; +#endif - result = entry_point(commandLine); + if (!commandLine.empty()) + commandLine += ' '; - xr_free(commandLine); + for (int i = 1; i < argc; ++i) + { + commandLine += argv[i]; + commandLine += ' '; } - else - result = entry_point(""); + + result = entry_point(commandLine.c_str()); } catch (const std::overflow_error& e) {