diff --git a/.github/workflows/engine-build.yml b/.github/workflows/engine-build.yml index b3b571f04..78594db3c 100644 --- a/.github/workflows/engine-build.yml +++ b/.github/workflows/engine-build.yml @@ -91,6 +91,10 @@ jobs: cp thirdparty/miniaudio/LICENSE artifact/OpenTS_THIRD_PARTY_LICENSES/miniaudio.txt cp thirdparty/licenses/stb-vorbis.txt artifact/OpenTS_THIRD_PARTY_LICENSES/stb-vorbis.txt cp thirdparty/licenses/lzo.txt artifact/OpenTS_THIRD_PARTY_LICENSES/lzo.txt + cp thirdparty/RmlUi/LICENSE.txt artifact/OpenTS_THIRD_PARTY_LICENSES/rmlui.txt + cp thirdparty/freetype/docs/FTL.TXT artifact/OpenTS_THIRD_PARTY_LICENSES/freetype.txt + cp thirdparty/licenses/zlib.txt artifact/OpenTS_THIRD_PARTY_LICENSES/zlib.txt + cp thirdparty/imgui/LICENSE.txt artifact/OpenTS_THIRD_PARTY_LICENSES/dear-imgui.txt cat thirdparty/licenses/khronos-vulkan-notice.txt \ thirdparty/bgfx.cmake/bimg/3rdparty/astc-encoder/LICENSE.txt \ > artifact/OpenTS_THIRD_PARTY_LICENSES/khronos-vulkan.txt diff --git a/.gitmodules b/.gitmodules index 0af59cec6..2446d1322 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,15 @@ [submodule "thirdparty/miniaudio"] path = thirdparty/miniaudio url = https://github.com/mackron/miniaudio.git +[submodule "thirdparty/SDL"] + path = thirdparty/SDL + url = https://github.com/libsdl-org/SDL.git +[submodule "thirdparty/RmlUi"] + path = thirdparty/RmlUi + url = https://github.com/mikke89/RmlUi.git +[submodule "thirdparty/freetype"] + path = thirdparty/freetype + url = https://github.com/freetype/freetype.git +[submodule "thirdparty/imgui"] + path = thirdparty/imgui + url = https://github.com/ocornut/imgui.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ac8022f7..a263e577b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,7 @@ option(OPENTS_OFFICIAL_BUILD "Build as an official release of the declared versi option(OPENTS_EXPERIMENTAL_CLANG_CL "Build with clang-cl using the MSVC ABI" OFF) option(OPENTS_EXPERIMENTAL_X64 "Configure an unsupported 64-bit Windows build" OFF) +option(OPENTS_EXPERIMENTAL_NATIVE "Configure a native build for the host platform" OFF) if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") @@ -34,14 +35,24 @@ elseif(MSVC) "writes are not interchangeable with a supported 32-bit build's, and nothing in " "the version stamp distinguishes them.") endif() +elseif(OPENTS_EXPERIMENTAL_NATIVE) + message(STATUS "OpenTS: configuring an unsupported native build for the host platform.") + + # The tree declares imports and exports with __declspec throughout, which clang only + # accepts with the Microsoft extensions enabled. + add_compile_options(-fms-extensions) else() message(FATAL_ERROR "OpenTS requires the Visual Studio 2022 MSVC toolchain. " "For the unsupported clang-cl experiment, configure with " - "-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/clang-cl-msvc.cmake.") + "-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/clang-cl-msvc.cmake. " + "For the unsupported native build, configure with " + "-DOPENTS_EXPERIMENTAL_NATIVE=ON.") endif() -enable_language(RC) +if(WIN32) + enable_language(RC) +endif() set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -83,6 +94,11 @@ add_custom_target(OpenTSBuildStamp ALL ) add_subdirectory(thirdparty) + +if(NOT WIN32) + add_subdirectory(platform) +endif() + add_subdirectory(code) add_subdirectory(code/language) diff --git a/README.md b/README.md index 44e6bacc4..080f09adc 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ OpenTS is a community-led, open-source reconstruction of *Command & Conquer: Tiberian Sun*. Instead of patching or extending the retail executable, it rebuilds the engine as a standalone program. +This fork carries a native Apple Silicon macOS build of that engine. See +[Running on macOS](#running-on-macos) for the data script, the build and how to +start it. Everything below describes OpenTS itself and applies here too. + OpenTS gives equal weight to two goals: maintaining a playable engine and providing a capable platform for modding and engine development. Work on one goal should not come at the expense of the other. @@ -70,6 +74,59 @@ UTF-8 code page, which needs Windows 10 version 1903 or newer. Older Windows keeps its own code page, so game text still shows, but a path or file name holding a character that code page lacks may fail. +## Running on macOS + +This fork builds and runs the engine natively on Apple Silicon. The game data +still comes from a copy of Tiberian Sun you own. + +Install the tools: + +```bash +brew install cmake ninja +brew install --cask steamcmd +``` + +Fetch the game data from your own Steam account: + +```bash +./scripts/get-assets.sh +``` + +The script downloads app 2229880 into `Run/`, skips the Windows executables the +engine replaces, and checks that the archives startup needs actually arrived. +Steam Guard prompts for a code on first login. **Quit the Steam desktop client +first**: steamcmd shares its data directory, and a running client holds a lock +that makes steamcmd hang after "Verifying installation..." with no error. Set +`OPENTS_GAME_DIR` to put the data somewhere other than `Run/`. + +Build the engine: + +```bash +git submodule update --init --recursive +cmake -S . -B build/native -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DOPENTS_EXPERIMENTAL_NATIVE=ON +cmake --build build/native --target OpenTS +``` + +Build the `OpenTS` target rather than everything: the C++ test harnesses pass +MSVC-only flags and link `kernel32`, so they do not configure here. + +Run it: + +```bash +build/native/bin/Game -DATADIR=Run -USERDIR=build/native/user +``` + +`-USERDIR` is where saves, `SUN.INI` and logs are written, so pointing it at a +scratch directory leaves an existing install untouched. Full screen is a +setting in the display options screen. + +Windows supplies the window, message loop and cursor itself; every other +platform gets them from `platform/win32compat`, which serves the Win32 surface +the engine is written against out of [SDL](https://github.com/libsdl-org/SDL). +[Building OpenTS](docs/BUILDING.md) covers the build in full. + ## Documentation The [OpenTS manual](https://opents-developers.github.io/OpenTS/) documents diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index b767b00a1..20732d7d8 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -16,6 +16,10 @@ remains under its own license and copyright notices. | [miniaudio](https://github.com/mackron/miniaudio) | Audio device output, resampling, and WAV, FLAC, and MP3 decoding | MIT-0 or Unlicense | | [stb_vorbis](https://github.com/nothings/stb) | Ogg Vorbis decoding, bundled with miniaudio | MIT or Unlicense | | [LZO](https://www.oberhumer.com/opensource/lzo/) | LZO1X compression for maps, saves, and network blocks | GPL-2.0-or-later | +| [RmlUi](https://github.com/mikke89/RmlUi) | User interface documents, styling, and layout | MIT | +| [FreeType](https://freetype.org/) | Glyph rasterization for the RmlUi font engine | FTL | +| [zlib](https://zlib.net/) | Compressed font stream support, bundled with FreeType | zlib | +| [Dear ImGui](https://github.com/ocornut/imgui) | Developer tooling on the UI shell | MIT | The source checkout keeps the license texts under `thirdparty/`. Binary packages reproduce the license texts for the components used by OpenTS under diff --git a/cmake/StringTable.cmake b/cmake/StringTable.cmake new file mode 100644 index 000000000..30eafda6e --- /dev/null +++ b/cmake/StringTable.cmake @@ -0,0 +1,183 @@ +# Builds the portable string table from the Windows resource script. +# +# The resource script is the only place the strings live, and only the RC compiler turns it +# into a module resource. A host without an RC compiler still needs the same strings, so this +# script reads the STRINGTABLE blocks out of the script, resolves each symbolic identifier +# through the header the script itself includes, and writes a flat data file the game reads +# at runtime. The Windows build is untouched and keeps using the compiled resource. +# +# Run with: +# cmake -DRC_FILE= -DHEADER_FILE= -DOUTPUT= -P StringTable.cmake +# +# NAME_TABLE names a second output, the table that turns a string's symbolic name back into +# its identifier. UI documents reference a string by name, and this keeps the resource script +# the only place a name and a number are paired. +# +# Encoding: the resource script carries "#pragma code_page(65001)", so its bytes are already +# UTF-8 and are copied through unchanged. The data file is UTF-8 for the same reason. + +if(NOT DEFINED RC_FILE OR NOT DEFINED HEADER_FILE) + message(FATAL_ERROR "StringTable.cmake needs RC_FILE and HEADER_FILE") +endif() + +if(NOT DEFINED OUTPUT AND NOT DEFINED NAME_TABLE) + message(FATAL_ERROR "StringTable.cmake needs OUTPUT, NAME_TABLE, or both") +endif() + +# A semicolon separates list elements everywhere in this language, and at least one string +# contains one. It is carried as a marker through every step that treats text as a list and +# put back only when the record is written. No string contains "@", which is what makes the +# marker safe to pick. +set(SEMICOLON_MARK "@OPENTS_SEMI@") +set(BACKSLASH_MARK "@OPENTS_BSLASH@") + +# --------------------------------------------------------------------------------------------- +# The identifiers. The resource script names its strings symbolically and the header gives each +# name its number. +# --------------------------------------------------------------------------------------------- + +file(READ "${HEADER_FILE}" HEADER_TEXT) +string(REGEX MATCHALL "#define[ \t]+[A-Za-z_][A-Za-z0-9_]*[ \t]+[0-9]+" HEADER_DEFINES "${HEADER_TEXT}") + +foreach(define IN LISTS HEADER_DEFINES) + string(REGEX MATCH "#define[ \t]+([A-Za-z_][A-Za-z0-9_]*)[ \t]+([0-9]+)" ignored "${define}") + set("ID_${CMAKE_MATCH_1}" "${CMAKE_MATCH_2}") +endforeach() + +list(LENGTH HEADER_DEFINES DEFINE_COUNT) + +# --------------------------------------------------------------------------------------------- +# The strings. Only the STRINGTABLE blocks are read; the dialog templates in the same script +# hold quoted text that is not a string resource. +# --------------------------------------------------------------------------------------------- + +file(READ "${RC_FILE}" RC_TEXT) +string(REPLACE ";" "${SEMICOLON_MARK}" RC_TEXT "${RC_TEXT}") +string(REPLACE "\r" "" RC_TEXT "${RC_TEXT}") +string(REPLACE "\n" ";" RC_LINES "${RC_TEXT}") + +set(IN_TABLE FALSE) +set(IN_BODY FALSE) +set(PENDING_NAME "") +set(RECORD_COUNT 0) +set(RECORDS "") +set(NAMES "") +set(MISSING "") + +foreach(line IN LISTS RC_LINES) + if(NOT IN_TABLE) + if(line MATCHES "^STRINGTABLE([ \t]|$)") + set(IN_TABLE TRUE) + endif() + continue() + endif() + + if(NOT IN_BODY) + if(line MATCHES "^BEGIN[ \t]*$") + set(IN_BODY TRUE) + endif() + continue() + endif() + + if(line MATCHES "^END[ \t]*$") + set(IN_TABLE FALSE) + set(IN_BODY FALSE) + set(PENDING_NAME "") + continue() + endif() + + set(name "") + set(raw "") + set(have_string FALSE) + + if(line MATCHES "^[ \t]*([A-Za-z_][A-Za-z0-9_]*)[ \t]+\"(.*)\"[ \t]*$") + set(name "${CMAKE_MATCH_1}") + set(raw "${CMAKE_MATCH_2}") + set(have_string TRUE) + elseif(line MATCHES "^[ \t]*([A-Za-z_][A-Za-z0-9_]*)[ \t]*$") + set(PENDING_NAME "${CMAKE_MATCH_1}") + continue() + elseif(line MATCHES "^[ \t]*\"(.*)\"[ \t]*$") + set(name "${PENDING_NAME}") + set(raw "${CMAKE_MATCH_1}") + set(have_string TRUE) + set(PENDING_NAME "") + elseif(line MATCHES "^[ \t]*$") + continue() + else() + message(FATAL_ERROR "StringTable.cmake did not understand a STRINGTABLE line: ${line}") + endif() + + if(NOT have_string) + continue() + endif() + + if(name STREQUAL "") + message(FATAL_ERROR "StringTable.cmake found a string with no identifier: ${line}") + endif() + + if(NOT DEFINED "ID_${name}") + list(APPEND MISSING "${name}") + continue() + endif() + + # The escapes the resource compiler understands, in the order that keeps an escaped + # backslash from being read twice. + string(REPLACE "\\\\" "${BACKSLASH_MARK}" raw "${raw}") + string(REPLACE "\\n" "\n" raw "${raw}") + string(REPLACE "\\r" "\r" raw "${raw}") + string(REPLACE "\\t" "\t" raw "${raw}") + string(REPLACE "\\\"" "\"" raw "${raw}") + string(REPLACE "\"\"" "\"" raw "${raw}") + string(REPLACE "${BACKSLASH_MARK}" "\\" raw "${raw}") + string(REPLACE "${SEMICOLON_MARK}" ";" raw "${raw}") + + string(LENGTH "${raw}" length) + + # Length-prefixed, so a string that contains a newline needs no escaping of its own. + string(APPEND RECORDS "${ID_${name}} ${length}\n${raw}\n") + string(APPEND NAMES "OPENTS_STRING_NAME(\"${name}\", ${ID_${name}})\n") + math(EXPR RECORD_COUNT "${RECORD_COUNT} + 1") +endforeach() + +if(IN_TABLE) + message(FATAL_ERROR "StringTable.cmake reached the end of ${RC_FILE} inside a STRINGTABLE") +endif() + +if(MISSING) + list(REMOVE_DUPLICATES MISSING) + string(REPLACE ";" ", " MISSING_TEXT "${MISSING}") + message(FATAL_ERROR "StringTable.cmake found no identifier in ${HEADER_FILE} for: ${MISSING_TEXT}") +endif() + +if(RECORD_COUNT EQUAL 0) + message(FATAL_ERROR "StringTable.cmake found no strings in ${RC_FILE}") +endif() + +if(DEFINED OUTPUT) + get_filename_component(OUTPUT_DIR "${OUTPUT}" DIRECTORY) + if(OUTPUT_DIR) + file(MAKE_DIRECTORY "${OUTPUT_DIR}") + endif() + + file(WRITE "${OUTPUT}" "OPENTS-STRINGS 1\n${RECORD_COUNT}\n${RECORDS}") + + message(STATUS "String table: ${RECORD_COUNT} strings from ${DEFINE_COUNT} identifiers -> ${OUTPUT}") +endif() + +# The name table is a list of invocations rather than a declaration, so the including file +# decides what a pair becomes and the header carries no storage of its own. +if(DEFINED NAME_TABLE) + get_filename_component(NAME_TABLE_DIR "${NAME_TABLE}" DIRECTORY) + if(NAME_TABLE_DIR) + file(MAKE_DIRECTORY "${NAME_TABLE_DIR}") + endif() + + file(WRITE "${NAME_TABLE}" + "// Generated from language.rc by cmake/StringTable.cmake. Do not edit.\n" + "// Each line pairs a string resource's symbolic name with its identifier.\n" + "\n" + "${NAMES}") + + message(STATUS "String names: ${RECORD_COUNT} names -> ${NAME_TABLE}") +endif() diff --git a/code/CMakeLists.txt b/code/CMakeLists.txt index 137d06855..d803c3f42 100644 --- a/code/CMakeLists.txt +++ b/code/CMakeLists.txt @@ -2,7 +2,7 @@ set(CMAKE_CONFIGURATION_TYPES Debug Release CACHE STRING "" FORCE) # OpenTS supports 32-bit (x86) builds. A 64-bit build is an unsupported experiment. -if(NOT CMAKE_SIZEOF_VOID_P EQUAL 4 AND NOT OPENTS_EXPERIMENTAL_X64) +if(WIN32 AND NOT CMAKE_SIZEOF_VOID_P EQUAL 4 AND NOT OPENTS_EXPERIMENTAL_X64) message(FATAL_ERROR "OpenTS must be built as 32-bit x86. Reconfigure with -A Win32. " "For the unsupported 64-bit experiment, configure with " @@ -95,42 +95,53 @@ set_target_properties(OpenTS PROPERTIES # message(STATUS "${PROJECT_NAME}: Applying compiler flags...") -set(OPENTS_COMPILE_OPTIONS +if(MSVC OR OPENTS_EXPERIMENTAL_CLANG_CL) + set(OPENTS_COMPILE_OPTIONS - # ================= DEBUG ================= - $<$: + # ================= DEBUG ================= + $<$: - # ---------- C++ ---------- - # /arch:SSE2 -- IEEE-754 single precision, no x87 excess precision. - # /fp:precise -- no reassociation of the engine's accumulations. - # /utf-8 -- sources and literals are UTF-8 whatever the build machine's code page. - $<$: - /Zi /Od /RTC1 /GR $<$:/MP> /EHsc /Oy- /MTd /Zc:__cplusplus /arch:SSE2 /fp:precise /utf-8 - > - - # ---------- C ---------- - $<$: - /Zi /Od /RTC1 $<$:/MP> /Oy- /arch:SSE2 /fp:precise /utf-8 - > - > + # ---------- C++ ---------- + # /arch:SSE2 -- IEEE-754 single precision, no x87 excess precision. + # /fp:precise -- no reassociation of the engine's accumulations. + # /utf-8 -- sources and literals are UTF-8 whatever the build machine's code page. + $<$: + /Zi /Od /RTC1 /GR $<$:/MP> /EHsc /Oy- /MTd /Zc:__cplusplus /arch:SSE2 /fp:precise /utf-8 + > - # ================= RELEASE ================= - $<$: - - # ---------- C++ ---------- - # /arch:SSE2 -- IEEE-754 single precision, no x87 excess precision. - # /fp:precise -- no reassociation of the engine's accumulations. - # /utf-8 -- sources and literals are UTF-8 whatever the build machine's code page. - $<$: - /Zi /O2 /GF /GR $<$:/MP> /EHsc /MT /Zc:__cplusplus /arch:SSE2 /fp:precise /utf-8 + # ---------- C ---------- + $<$: + /Zi /Od /RTC1 $<$:/MP> /Oy- /arch:SSE2 /fp:precise /utf-8 + > > - # ---------- C ---------- - $<$: - /Zi /O2 /GF $<$:/MP> /arch:SSE2 /fp:precise /utf-8 + # ================= RELEASE ================= + $<$: + + # ---------- C++ ---------- + # /arch:SSE2 -- IEEE-754 single precision, no x87 excess precision. + # /fp:precise -- no reassociation of the engine's accumulations. + # /utf-8 -- sources and literals are UTF-8 whatever the build machine's code page. + $<$: + /Zi /O2 /GF /GR $<$:/MP> /EHsc /MT /Zc:__cplusplus /arch:SSE2 /fp:precise /utf-8 + > + + # ---------- C ---------- + $<$: + /Zi /O2 /GF $<$:/MP> /arch:SSE2 /fp:precise /utf-8 + > > - > -) + ) +else() + # The native host build carries no MSVC switch spellings. Only the floating-point + # contract matters for simulation determinism, so it is the one that is restated. + set(OPENTS_COMPILE_OPTIONS + -g + -ffp-contract=off + -fno-strict-aliasing + -Wno-everything + ) +endif() target_compile_options(OpenTS PRIVATE ${OPENTS_COMPILE_OPTIONS}) @@ -149,6 +160,12 @@ target_include_directories(OpenTS PRIVATE "${OPENTS_GENERATED_DIR}" ) +# Windows supplies the window, the message loop and the cursor itself. Every other target +# gets them from the compatibility layer, which carries the headers as well as the code. +if(NOT WIN32) + target_link_libraries(OpenTS PRIVATE win32compat) +endif() + # The generated build stamp has to exist before anything compiles. add_dependencies(OpenTS OpenTSBuildStamp) @@ -160,7 +177,37 @@ set_source_files_properties("${CMAKE_CURRENT_SOURCE_DIR}/bgfxbackend.cpp" PROPER "${BGFX_ROOT}/include;${CMAKE_SOURCE_DIR}/thirdparty/bgfx.cmake/bx/include;${BGFX_ROOT}/examples/common/imgui" COMPILE_DEFINITIONS "BX_CONFIG_DEBUG=$,1,$>" - COMPILE_OPTIONS "/Zc:preprocessor" + COMPILE_OPTIONS "$<$:/Zc:preprocessor>" +) + +# The UI shell is scoped the same way: only code/ui sees RmlUi and ImGui, and uirender.cpp +# alone adds bgfx to that, so no other translation unit carries any of their headers or +# build settings. uitexture.cpp reads images through bimg, which bgfx already carries. +file(GLOB OPENTS_UI_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/ui/*.cpp") +set_source_files_properties(${OPENTS_UI_SOURCES} PROPERTIES + INCLUDE_DIRECTORIES + "${CMAKE_SOURCE_DIR}/thirdparty/RmlUi/Include;${CMAKE_SOURCE_DIR}/thirdparty/imgui" + COMPILE_OPTIONS "$<$:/Zc:preprocessor>" +) + +# The bitmap font engine derives from RmlUi's own engine, whose header sits in the library's +# Source tree rather than its Include tree, so that one path goes on that one file. +set_property(SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/ui/uifont.cpp" APPEND PROPERTY + INCLUDE_DIRECTORIES "${CMAKE_SOURCE_DIR}/thirdparty/RmlUi/Source" +) + +set_property(SOURCE + "${CMAKE_CURRENT_SOURCE_DIR}/ui/uirender.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/ui/uitexture.cpp" + APPEND PROPERTY INCLUDE_DIRECTORIES + "${BGFX_ROOT}/include;${CMAKE_SOURCE_DIR}/thirdparty/bgfx.cmake/bx/include;${CMAKE_SOURCE_DIR}/thirdparty/bgfx.cmake/bimg/include;${BGFX_ROOT}/examples/common/imgui" +) + +set_property(SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/ui/uirender.cpp" APPEND PROPERTY + COMPILE_DEFINITIONS "BX_CONFIG_DEBUG=$,1,$>" +) +set_property(SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/ui/uitexture.cpp" APPEND PROPERTY + COMPILE_DEFINITIONS "BX_CONFIG_DEBUG=$,1,$>" ) # bx rewrites __stdcall while its headers are being parsed by clang-cl. Force the @@ -174,16 +221,16 @@ endif() message(STATUS "${PROJECT_NAME}: Adding compilier definitions...") target_compile_definitions(OpenTS PRIVATE - WIN32 - _WINDOWS NOMINMAX - - # Compiles Blowfish into the binary instead of reaching it through the COM object in - # blowfish.dll. It decides the layout of BlowfishEngine, so every translation unit has - # to agree on it and it belongs on the compile line rather than in a header. - NO_BLOWFISH_DLL ) +if(WIN32) + target_compile_definitions(OpenTS PRIVATE + WIN32 + _WINDOWS + ) +endif() + # # --------------------------------------------------------- # Windows libraries and linker options @@ -207,8 +254,15 @@ target_link_libraries(OpenTS PRIVATE bgfx bx bimg + bimg_decode miniaudio lzo + rmlui + imgui +) + +if(WIN32) + target_link_libraries(OpenTS PRIVATE comctl32 dbghelp iphlpapi @@ -218,7 +272,8 @@ target_link_libraries(OpenTS PRIVATE ws2_32 kernel32 user32 gdi32 winspool comdlg32 advapi32 shell32 ole32 oleaut32 uuid odbc32 odbccp32 -) + ) +endif() if(MSVC) target_link_options(OpenTS PRIVATE @@ -272,41 +327,34 @@ foreach(f ${OPENTS_SRC}) endif() endforeach() -# List of all interface filenames (headers + C stubs) -set(INTERFACE_FILES - iblockci.h iblockci_i.c - iblowfish.h iblowfish_i.c - iflyctrl.h iflyctrl_i.c - ilinkstm.h - iloco.h iloco_i.c - ilocos.h ilocos_i.c - ipiggy.h ipiggy_i.c - isun.h isun_i.c +# The interface headers the locomotors are written against. +source_group("Interface Files" FILES + "${CMAKE_CURRENT_SOURCE_DIR}/iflyctrl.h" + "${CMAKE_CURRENT_SOURCE_DIR}/iloco.h" + "${CMAKE_CURRENT_SOURCE_DIR}/ipiggy.h" ) -# Convert to full paths relative to source dir -set(FULL_INTERFACE_FILES "") -foreach(f IN LISTS INTERFACE_FILES) - list(APPEND FULL_INTERFACE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/${f}") -endforeach() - -# Add them to the "Interface Files" group and exclude *_i.c from build -foreach(f IN LISTS FULL_INTERFACE_FILES) - - # Put into VS filter - source_group("Interface Files" FILES "${f}") - - # Exclude *_i.c from build, but DO NOT mark headers as header-only - if(f MATCHES "_i\\.c$") - set_source_files_properties("${f}" PROPERTIES HEADER_FILE_ONLY TRUE) - endif() - -endforeach() - # General source files source_group("Source Files" REGULAR_EXPRESSION ".*\\.(c|cpp)$") source_group("Header Files" REGULAR_EXPRESSION ".*\\.(h|hpp)$") +# +# --------------------------------------------------------- +# Populate the game data directory +# --------------------------------------------------------- +# +# The build no longer copies itself into the game directory, but the shipped UI documents +# are game data the engine resolves under the data directory, so they are placed there. +message(STATUS "${PROJECT_NAME}: Creating post-build commands...") + +# The shipped UI documents, styles and font travel with the game data. They are read +# through the game's file system, so this directory is what the search path points at. +add_custom_command(TARGET OpenTS POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_SOURCE_DIR}/ui" + "${OPENTS_GAME_DIR}/ui" +) + # # --------------------------------------------------------- # Visual Studio startup project @@ -320,4 +368,6 @@ set_property(GLOBAL PROPERTY VS_STARTUP_PROJECT OpenTS) set(NATVIS_FILE "${CMAKE_CURRENT_SOURCE_DIR}/sun.natvis") source_group("Natvis Files" FILES ${NATVIS_FILE}) target_sources(OpenTS PRIVATE ${NATVIS_FILE}) -target_link_options(OpenTS PRIVATE "/NATVIS:${NATVIS_FILE}") +if(MSVC) + target_link_options(OpenTS PRIVATE "/NATVIS:${NATVIS_FILE}") +endif() diff --git a/code/abstract.cpp b/code/abstract.cpp index 355435def..72bdafea2 100644 --- a/code/abstract.cpp +++ b/code/abstract.cpp @@ -58,7 +58,6 @@ /// AbstractClass::AbstractClass(void) : ID(-1), - RefCount(0), Dirty(false) { } @@ -107,75 +106,13 @@ void AbstractClass::Create_ID(void) } -/// -/// Fetches a COM interface pointer from this object. -/// This is the IUnknown implementation shared by every game object. Abstract -/// objects expose IUnknown, IPersistStream and IPersist; the save game system -/// reaches the whole object hierarchy through them. -/// -/// The identifier of the interface being asked for. -/// Receives the interface pointer, or NULL when the -/// interface is not supported. -/// -/// Returns with S_OK when the interface was supplied. Otherwise E_NOINTERFACE is -/// returned for an unsupported interface, or E_POINTER when no output pointer was given. -/// -HRESULT STDMETHODCALLTYPE AbstractClass::QueryInterface(REFIID riid, LPVOID * ppvObject) -{ - if (ppvObject == NULL) { - return(E_POINTER); - } - - *ppvObject = NULL; - - if (riid == IID_IUnknown) { - *ppvObject = (IUnknown *)(IPersistStream *)this; - } - if (riid == IID_IPersistStream) { - *ppvObject = (IPersistStream *)this; - } - if (riid == IID_IPersist) { - *ppvObject = (IPersist *)this; - } - if (*ppvObject == NULL) { - return(E_NOINTERFACE); - } - - AddRef(); - return(S_OK); -} - - -/// -/// Satisfies the IUnknown reference count contract. -/// The game owns its objects outright and they outlive any interface pointer -/// handed out, so nothing is actually counted. -/// -/// Returns with the reference count, which is always one. -ULONG STDMETHODCALLTYPE AbstractClass::AddRef(void) -{ - return(1); -} - - -/// -/// Satisfies the IUnknown release contract. -/// Releasing an interface never destroys a game object -- see AddRef. -/// -/// Returns with the reference count, which is always one. -ULONG STDMETHODCALLTYPE AbstractClass::Release(void) -{ - return(1); -} - - /// /// Writes this object to the save stream. /// /// The stream to write to. /// Should the object be marked clean once it has been written? -/// Returns with S_OK when the object was written, otherwise a failure code. -HRESULT STDMETHODCALLTYPE AbstractClass::Save(IStream * stream, BOOL cleardirty) +/// bool; Was the record written whole? +bool AbstractClass::Save(SaveStreamClass & stream, bool cleardirty) { return(Save_Members(stream, cleardirty)); } @@ -185,8 +122,8 @@ HRESULT STDMETHODCALLTYPE AbstractClass::Save(IStream * stream, BOOL cleardirty) /// Reads this object back from the save stream. /// /// The stream to read from. -/// Returns with S_OK when the object was read, otherwise a failure code. -HRESULT STDMETHODCALLTYPE AbstractClass::Load(IStream * stream) +/// bool; Was the record read whole? +bool AbstractClass::Load(SaveStreamClass & stream) { return(Load_Members(stream)); } @@ -199,28 +136,16 @@ HRESULT STDMETHODCALLTYPE AbstractClass::Load(IStream * stream) /// /// The stream to write to. /// Should the object be marked clean once it has been written? -/// Returns with S_OK when the record was written, otherwise a failure code. -HRESULT AbstractClass::Save_Members(IStream * stream, BOOL cleardirty) +/// bool; Was the record written whole? +bool AbstractClass::Save_Members(SaveStreamClass & stream, bool cleardirty) { - if (stream == NULL) { - return(E_POINTER); - } - SwizzleIDType id = Swizzler.ID_Of(this); - - HRESULT result = stream->Write(&id, sizeof(id), NULL); - if (FAILED(result)) { - return(result); + stream.Serialize(id); + Serialize(stream); + if (!stream.Was_Error() && cleardirty) { + Dirty = false; } - - SaveStreamClass savestream(stream, SaveStreamClass::MODE_SAVE); - Serialize(savestream); - - if (SUCCEEDED(savestream.Result()) && cleardirty) { - Dirty = false; - } - - return(savestream.Result()); + return(!stream.Was_Error()); } @@ -230,31 +155,24 @@ HRESULT AbstractClass::Save_Members(IStream * stream, BOOL cleardirty) /// save game can be remapped onto this object, and the members follow. /// /// The stream to read from. -/// Returns with S_OK when the record was read, otherwise a failure code. -HRESULT AbstractClass::Load_Members(IStream * stream) +/// bool; Was the record read whole? +bool AbstractClass::Load_Members(SaveStreamClass & stream) { - if (stream == NULL) { - return(E_POINTER); + SwizzleIDType id = 0; + stream.Serialize(id); + if (stream.Was_Error()) { + return(false); } - - SwizzleIDType id; - - HRESULT result = stream->Read(&id, sizeof(id), NULL); - if (FAILED(result)) { - return(result); - } - Swizzle_Here_I_Am(id, this); - SaveStreamClass savestream(stream, SaveStreamClass::MODE_LOAD); - savestream.Set_Context(typeid(*this).name(), id); - Serialize(savestream); + // A nested record borrows the stream, so the owner's context is put back afterwards. + char const * const outertype = stream.Context_Type(); + SwizzleIDType const outerid = stream.Context_ID(); + stream.Set_Context(typeid(*this).name(), id); + Serialize(stream); + stream.Set_Context(outertype, outerid); - if (SUCCEEDED(savestream.Result())) { - Post_Load(); - } - - return(savestream.Result()); + return(!stream.Was_Error()); } @@ -274,25 +192,10 @@ void AbstractClass::Post_Load(void) void AbstractClass::Serialize(SaveStreamClass & stream) { stream.Serialize(ID); - // RefCount -- belongs to the running session rather than the record. stream.Serialize(Dirty); } -/// -/// Fetches the number of bytes that Save will write. -/// A record is as long as the members a class names, so the count is not known before -/// the members have been written. Nothing in the game asks for it, so rather than -/// walk the object twice this reports that the size cannot be supplied. -/// -/// Receives the maximum size, in bytes. -/// Returns with E_NOTIMPL. -HRESULT STDMETHODCALLTYPE AbstractClass::GetSizeMax(ULARGE_INTEGER *pcbSize) -{ - return(E_NOTIMPL); -} - - /// /// Folds this object's state into a running CRC. /// The multiplayer sync check walks every object each frame and accumulates its @@ -335,23 +238,6 @@ bool AbstractClass::Is_Techno(void) const } -/// -/// Determines if this object has changed since it was last saved. -/// -/// Returns with S_OK when the object is dirty, or S_FALSE when it is not. -HRESULT AbstractClass::IsDirty(void) -{ - /* - * Per IPersistStream::IsDirty specifications this method returns S_OK to indicate that the object has changed. - * Otherwise, it returns S_FALSE. - */ - if (Dirty) { - return(S_OK); - } - return(S_FALSE); -} - - /// /// Resets this object to its start of scenario state. /// The bare abstract object carries no scenario state, so there is nothing to do. diff --git a/code/abstract.h b/code/abstract.h index 8e9278fe0..46d3ebf59 100644 --- a/code/abstract.h +++ b/code/abstract.h @@ -39,7 +39,7 @@ #include "house.hh" #include "rtti.hh" -#include +#include "persist.h" class AbstractTypeClass; class CRCEngine; @@ -62,7 +62,7 @@ class MonoClass; ** This class is the base class for all game objects that have an existence on the ** battlefield. */ -class AbstractClass : public IPersistStream +class AbstractClass : public IPersistent { public: @@ -74,8 +74,8 @@ class AbstractClass : public IPersistStream * the members are read -- dropping a registration keyed by the identity the read * is about to replace, say. */ - HRESULT Save_Members(IStream * stream, BOOL cleardirty); - HRESULT Load_Members(IStream * stream); + bool Save_Members(SaveStreamClass & stream, bool cleardirty); + bool Load_Members(SaveStreamClass & stream); public: @@ -87,16 +87,9 @@ class AbstractClass : public IPersistStream __declspec(property(get = Fetch_RTTI)) RTTIType RTTI; int ID; - /* - * This is the count of outstanding COM references to this object. Only projectiles - * are genuinely reference counted -- everything else answers 1 to AddRef and to - * Release -- so elsewhere it merely rides along, preserved by hand across a load. - */ - LONG RefCount; - /* * If this object has changed since it was last written out, then this flag will be - * true. Save clears it on request and IsDirty reports it, as IPersistStream asks. + * true. Save clears it on request. */ bool Dirty; @@ -106,14 +99,9 @@ class AbstractClass : public IPersistStream AbstractClass(void); virtual ~AbstractClass(void); - virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID * ppvObject) override; - virtual ULONG STDMETHODCALLTYPE AddRef(void) override; - virtual ULONG STDMETHODCALLTYPE Release(void) override; - virtual HRESULT STDMETHODCALLTYPE IsDirty(void) override; - virtual HRESULT STDMETHODCALLTYPE Load(IStream * stream) override; - virtual HRESULT STDMETHODCALLTYPE Save(IStream * stream, BOOL cleardirty) override; - virtual HRESULT STDMETHODCALLTYPE GetSizeMax(ULARGE_INTEGER *pcbSize) override; + virtual bool Load(SaveStreamClass & stream) override; + virtual bool Save(SaveStreamClass & stream, bool cleardirty) override; virtual int What_Am_I(void) const; virtual int Fetch_ID(void) const; @@ -122,7 +110,6 @@ class AbstractClass : public IPersistStream AbstractClass & operator = (const AbstractClass & that) { ID = that.ID; - RefCount = that.RefCount; Dirty = that.Dirty; return(*this); } @@ -137,9 +124,9 @@ class AbstractClass : public IPersistStream /* * Restores whatever the record could not carry -- artwork fetched by name, tables * shared with other objects, registrations that depend on the loaded identity. - * Load_Members calls this once the members are in place, so a base class fixup - * runs even when the load was entered through a derived class. An implementation - * chains to its base first and never touches the stream. + * Load_Object calls this once the record has been checked, so an object never takes + * its place in the map or a side table while its record is still in doubt. An + * implementation chains to its base first and never touches the stream. */ virtual void Post_Load(void); diff --git a/code/abuffer.h b/code/abuffer.h index f7d31a0f9..353228a3f 100644 --- a/code/abuffer.h +++ b/code/abuffer.h @@ -11,6 +11,8 @@ #include "rect.h" +#include + class Surface; class ABuffer diff --git a/code/actionline.cpp b/code/actionline.cpp index 6f4b55a34..c0535dbc3 100644 --- a/code/actionline.cpp +++ b/code/actionline.cpp @@ -7,6 +7,7 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ +#include "hostclock.h" #include "always.h" #include "actionline.h" @@ -74,7 +75,7 @@ void Draw_Action_Line_Segment(Surface & surface, Coord const & start, Coord cons for (int index = 0; index < PATTERN_LENGTH; index++) { pattern[index] = ((index / dash_length) & 1) == 0; } - int offset = (dash_rate > 0) ? ((-(int)timeGetTime() / dash_rate) & (PATTERN_LENGTH - 1)) : (7 * Frame % PATTERN_LENGTH); + int offset = (dash_rate > 0) ? ((-(int)Host_Milliseconds() / dash_rate) & (PATTERN_LENGTH - 1)) : (7 * Frame % PATTERN_LENGTH); // A thick line is two rows; its shadow sits below both. int rows = style.IsThick ? 2 : 1; diff --git a/code/addon.cpp b/code/addon.cpp index 4a97f3f84..c795d0ad5 100644 --- a/code/addon.cpp +++ b/code/addon.cpp @@ -15,9 +15,7 @@ #include "data.h" #include "init.h" #include "language/language.h" -#include "ownrdraw.h" - -INT_PTR CALLBACK Select_Game_Type_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); +#include "ui/uigametype.h" int AvailableAddOns = 1 << ADDON_BASE_GAME; int ActiveAddOns = 1 << ADDON_BASE_GAME; @@ -55,79 +53,27 @@ AddonType operator--(AddonType & val) /// bool; Should the game carry on? Returns false if the player backed out. bool Select_Game_Type_Dialog(AddonType &type) { - int retval; - type = ADDON_BASE_GAME; if (Addon_Installed(ADDON_ANY)) { - HWND dialog = OwnerDraw::Begin_Dialog(IDD_SELECT_GAME_TYPE, Select_Game_Type_Dialog_Proc); - if (dialog != 0) { - - SetWindowLongPtr(dialog, DWLP_USER, (LONG_PTR)&retval); - OwnerDraw::Display_Dialog(dialog); - - retval = -1; - while (retval == -1) { - if (OwnerDraw::Dialog_Message_Handler() == true) { - break; - } - - Title_Screen_Restore(false); - } - - ShowWindow(dialog, SW_HIDE); - UpdateWindow(MainWindow); - OwnerDraw::End_Dialog(dialog); - ActiveAddOns = 1 << ADDON_BASE_GAME; - - switch (retval) { - default: - type = ADDON_BASE_GAME; - break; - - case IDC_GAMETYPE_FIRESTORM: - Enable_Addon(ADDON_FIRESTORM); - type = ADDON_FIRESTORM; - break; - - case IDCANCEL: - return(false); - } + UIGameTypePresenterClass screen; + screen.Refresh(); + + if (UI_Game_Type_Screen(screen).Outcome == UIResult::OUTCOME_FAILED_TO_OPEN) { + Set_Required_Addon(type); + return(true); } - Set_Required_Addon(type); - return(true); + int addon = ADDON_BASE_GAME; + bool const carry_on = screen.Apply(addon); + type = (AddonType)addon; + return(carry_on); } return(true); } -/// -/// Handles the messages for the game type selection dialog. -/// This routine stashes the control that the player pressed into the caller's result -/// variable, which is what lets the dialog loop know it can stop. -/// -INT_PTR CALLBACK Select_Game_Type_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - int * retval; - - INT_PTR rc = OwnerDraw::Default_Dialog_Proc(window, message, wparam, lparam); - - if (rc == 0) { - switch (message) { - case WM_COMMAND: - retval = (int *)GetWindowLongPtr(window, DWLP_USER); - *retval = LOWORD(wparam); - break; - } - rc = 0; - } - - return(rc); -} - - /// /// Rebuilds the installed and active addon sets from the expansion rules files present. /// diff --git a/code/aircraft.cpp b/code/aircraft.cpp index d4314b432..b234f90b1 100644 --- a/code/aircraft.cpp +++ b/code/aircraft.cpp @@ -89,7 +89,6 @@ * _Counts_As_Civ_Evac -- Is the specified object a candidate for civilian evac logic? * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "aircraft.h" @@ -221,7 +220,7 @@ AircraftClass::AircraftClass(AircraftTypeClass const * type, HouseClass * house) Create_ID(); if (Class != NULL) { - Locomotion.CreateInstance(Class->Locomotor); + Locomotion = Create_Locomotor(Class->Locomotor); Locomotion->Link_To_Object(this); } @@ -266,48 +265,6 @@ void AircraftClass::Init(void) } -/// -/// Fetches the requested interface from this aircraft. -/// Aircraft add the fly control interface to the set that every game object supports, so -/// that the flying locomotor can interrogate them about how they wish to be flown. -/// -/// The identifier of the interface being asked for. -/// Pointer to the pointer to fill in with the interface. -/// Returns with S_OK if the interface was supplied. -HRESULT STDMETHODCALLTYPE AircraftClass::QueryInterface(struct _GUID const &guid, void **ppv) -{ - HRESULT res = BASECLASS::QueryInterface(guid, ppv); - if (FAILED(res)) { - if (guid == IID_IFlyControl) { - *ppv = (IFlyControl *)(this); - } - res = S_OK; - AddRef(); - } - return(res); -} - - -/// -/// Adds a reference to this aircraft. -/// -/// Returns with the new number of references outstanding. -ULONG STDMETHODCALLTYPE AircraftClass::AddRef(void) -{ - return(BASECLASS::AddRef()); -} - - -/// -/// Releases a reference to this aircraft. -/// -/// Returns with the number of references still outstanding. -ULONG STDMETHODCALLTYPE AircraftClass::Release(void) -{ - return(BASECLASS::Release()); -} - - /*********************************************************************************************** * AircraftClass::Unlimbo -- Removes an aircraft from the limbo state. * * * @@ -1397,8 +1354,7 @@ void AircraftClass::Drop_Off_Cargo(void) unit->IsOnBridge = false; } - unit->Locomotion.Release(); - unit->Locomotion = ILocomotionPtr(unit->TClass->Locomotor); + unit->Locomotion = Create_Locomotor(unit->TClass->Locomotor); unit->Locomotion->Link_To_Object(unit); if (!unit->Unlimbo(coord)) { @@ -3932,8 +3888,8 @@ void AircraftClass::Read_INI(CCINIClass const & ini) /// again once that identity has arrived. /// /// The stream to read this object from. -/// Returns with S_OK if the aircraft was loaded successfully. -HRESULT STDMETHODCALLTYPE AircraftClass::Load(IStream * stream) +/// bool; Was the record read whole? +bool AircraftClass::Load(SaveStreamClass & stream) { TargetTracker.Remove_Index(Fetch_ID()); return(BASECLASS::Load(stream)); @@ -4029,7 +3985,7 @@ void AircraftClass::Detach(AbstractClass const * target, bool all) /// can pick up or set down its cargo. /// /// Returns with the height above ground level to settle at. -LONG STDMETHODCALLTYPE AircraftClass::Landing_Altitude(void) +LONG AircraftClass::Landing_Altitude(void) { if (Class->IsCarryall && !Cargo.Is_Something_Attached() && In_Radio_Contact()) { BuildingClass * bptr = (BuildingClass *)Contact_With_Whom(); @@ -4057,7 +4013,7 @@ LONG STDMETHODCALLTYPE AircraftClass::Landing_Altitude(void) /// while loaded, or settles into the default parked pose. /// /// Returns with the facing to land at. -LONG STDMETHODCALLTYPE AircraftClass::Landing_Direction(void) +LONG AircraftClass::Landing_Direction(void) { TechnoClass * tptr = Contact_With_Whom(); if (tptr != NULL) { @@ -4076,7 +4032,7 @@ LONG STDMETHODCALLTYPE AircraftClass::Landing_Direction(void) /// empty one. /// /// Returns with true if there is cargo aboard this aircraft. -BOOL STDMETHODCALLTYPE AircraftClass::Is_Loaded(void) +BOOL AircraftClass::Is_Loaded(void) { return(Cargo.Is_Something_Attached()); } @@ -4088,7 +4044,7 @@ BOOL STDMETHODCALLTYPE AircraftClass::Is_Loaded(void) /// from a hover. Only a visible and unguided projectile is suited to strafing. /// /// Returns with true if the aircraft should make strafing attack runs. -LONG STDMETHODCALLTYPE AircraftClass::Is_Strafe(void) +LONG AircraftClass::Is_Strafe(void) { const WeaponDataStruct * data = Get_Class_Weapon_Data(0); if (data == NULL) { @@ -4114,7 +4070,7 @@ LONG STDMETHODCALLTYPE AircraftClass::Is_Strafe(void) /// to an attack run. /// /// Returns with true if the aircraft must hold its present heading. -LONG STDMETHODCALLTYPE AircraftClass::Is_Locked(void) +LONG AircraftClass::Is_Locked(void) { return(IsLockedStraight); } @@ -4233,18 +4189,9 @@ RTTIType AircraftClass::Fetch_RTTI(void) const } -/// -/// Fetches the class identifier of this object. -/// This routine is part of the persistence support. The save/load machinery uses the -/// class identifier to recreate an object of the correct type when a game is restored. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE AircraftClass::GetClassID(CLSID * retval) +ClassID AircraftClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_AircraftClass; - return(S_OK); + return(ClassID_AircraftClass); } diff --git a/code/aircraft.h b/code/aircraft.h index 73a4baa03..591ddd970 100644 --- a/code/aircraft.h +++ b/code/aircraft.h @@ -59,23 +59,20 @@ class AircraftClass : public FootClass, public IFlyControl AircraftClass(AircraftTypeClass const * type = NULL, HouseClass * house = NULL); virtual ~AircraftClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; - virtual HRESULT STDMETHODCALLTYPE Load(IStream * stream) override; + virtual ClassID Class_ID(void) const override; + virtual bool Load(SaveStreamClass & stream) override; virtual void Serialize(SaveStreamClass & stream) override; virtual void Post_Load(void) override; - virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID * ppvObject) override; - virtual ULONG STDMETHODCALLTYPE AddRef(void) override; - virtual ULONG STDMETHODCALLTYPE Release(void) override; /* * IFlyControl methods. */ - virtual LONG STDMETHODCALLTYPE Landing_Altitude(void) override; - virtual LONG STDMETHODCALLTYPE Landing_Direction(void) override; - virtual BOOL STDMETHODCALLTYPE Is_Loaded(void) override; - virtual LONG STDMETHODCALLTYPE Is_Strafe(void) override; - virtual LONG STDMETHODCALLTYPE Is_Locked(void) override; + virtual LONG Landing_Altitude(void) override; + virtual LONG Landing_Direction(void) override; + virtual BOOL Is_Loaded(void) override; + virtual LONG Is_Strafe(void) override; + virtual LONG Is_Locked(void) override; virtual void Init(void) override; virtual void Detach(AbstractClass const * target, bool all = true) override; diff --git a/code/airctype.cpp b/code/airctype.cpp index c86250758..6d3453a59 100644 --- a/code/airctype.cpp +++ b/code/airctype.cpp @@ -45,7 +45,6 @@ * AircraftTypeClass::operator new -- Allocates an aircraft type object from special pool. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "airctype.h" @@ -327,18 +326,9 @@ void AircraftTypeClass::Serialize(SaveStreamClass & stream) } -/// -/// Fetches the class identifier of the aircraft type. -/// The save game machinery asks each object for this identifier so that it can create an -/// object of the right class again when the game is loaded. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if there was nowhere to put the answer. -HRESULT STDMETHODCALLTYPE AircraftTypeClass::GetClassID(CLSID * retval) +ClassID AircraftTypeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_AircraftTypeClass; - return(S_OK); + return(ClassID_AircraftTypeClass); } diff --git a/code/airctype.h b/code/airctype.h index 8f462fbf2..913c8e1a8 100644 --- a/code/airctype.h +++ b/code/airctype.h @@ -57,7 +57,7 @@ class AircraftTypeClass : public TechnoTypeClass AircraftTypeClass(char const * ininame = NULL); virtual ~AircraftTypeClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; virtual void Post_Load(void) override; diff --git a/code/aitrig.cpp b/code/aitrig.cpp index 8b0f19074..cfd2391dc 100644 --- a/code/aitrig.cpp +++ b/code/aitrig.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "aitrig.h" @@ -92,17 +91,9 @@ AITriggerTypeClass::~AITriggerTypeClass(void) } -/// -/// Fetches the class identifier of this object. -/// The save game system uses this identifier to work out which class to build when the -/// object is read back in. -/// -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE AITriggerTypeClass::GetClassID(CLSID * retval) +ClassID AITriggerTypeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_AITriggerTypeClass; - return(S_OK); + return(ClassID_AITriggerTypeClass); } diff --git a/code/aitrig.h b/code/aitrig.h index 6d141b801..62dd872b1 100644 --- a/code/aitrig.h +++ b/code/aitrig.h @@ -60,7 +60,7 @@ class AITriggerTypeClass : public AbstractTypeClass AITriggerTypeClass(const char *name = NULL); ~AITriggerTypeClass(void); - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; static AITriggerTypeClass * Find_Or_Make(char const * ininame); diff --git a/code/alphashp.cpp b/code/alphashp.cpp index b7cc98727..dcd3b66ec 100644 --- a/code/alphashp.cpp +++ b/code/alphashp.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "alphashp.h" @@ -93,18 +92,9 @@ AlphaShapeClass::~AlphaShapeClass(void) } -/// -/// Fetches the class identifier used to persist this object. -/// The save system writes this identifier ahead of the object data so that the loader -/// knows what kind of object to reconstruct. -/// -/// Pointer to the buffer that will receive the class identifier. -/// Returns with S_OK, or E_POINTER if no buffer was supplied. -HRESULT STDMETHODCALLTYPE AlphaShapeClass::GetClassID(CLSID * retval) +ClassID AlphaShapeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_AlphaShapeClass; - return(S_OK); + return(ClassID_AlphaShapeClass); } diff --git a/code/alphashp.h b/code/alphashp.h index fc891a87d..04a67befa 100644 --- a/code/alphashp.h +++ b/code/alphashp.h @@ -34,7 +34,7 @@ class AlphaShapeClass : public AbstractClass AlphaShapeClass(void); ~AlphaShapeClass(void); - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/always.h b/code/always.h index 042ded5c2..932856832 100644 --- a/code/always.h +++ b/code/always.h @@ -91,3 +91,187 @@ #ifndef _stricmp #define _stricmp stricmp #endif + + +/* +** Define some Windows specific values that are used throghout the games +*/ +#ifndef _WIN32 + +#define _MAX_DRIVE 3 +#define _MAX_DIR 256 +#define _MAX_FNAME 255 +#define _MAX_EXT 8 +#define _MAX_PATH 512 +#define MAX_PATH _MAX_PATH +#define _CONTROL 0x20 // space, first non-control character in ASCII + +#undef _stricmp +#define stricmp strcasecmp +#define _stricmp strcasecmp +#define strnicmp strncasecmp +#define _strnicmp strncasecmp +#define memicmp strncasecmp +#define _memicmp strncasecmp +#define __cdecl + +#include +#include +#include +#include +#include + +/// The USER32 formatter the game uses for short strings. Windows caps its output at 1024 +/// characters, so the substitute caps it at the same place rather than at the buffer. +inline static int wvsprintf(char* buffer, const char* format, va_list args) +{ + return(vsnprintf(buffer, 1024, format, args)); +} + +inline static int wsprintf(char* buffer, const char* format, ...) +{ + va_list args; + va_start(args, format); + int const result = wvsprintf(buffer, format, args); + va_end(args); + return(result); +} + +inline static long filelength(int handle) +{ + off_t const here = lseek(handle, 0, SEEK_CUR); + if (here < 0) { + return(-1); + } + off_t const end = lseek(handle, 0, SEEK_END); + lseek(handle, here, SEEK_SET); + return((long)end); +} + +inline static int freopen_s(FILE** stream, const char* path, const char* mode, FILE* old) +{ + if (stream == NULL) { + return(-1); + } + *stream = freopen(path, mode, old); + return(*stream != NULL ? 0 : -1); +} + +inline static void _makepath(char* path, const char* drive, const char* dir, const char* fname, const char* ext) +{ + if (!path) { + return; + } + + path[0] = '\0'; + + if (drive && drive[0] != '\0') { + sprintf(path + strlen(path), "%c:", drive[0]); + } + + if (dir && dir[0] != '\0') { + char const last = dir[strlen(dir) - 1]; + sprintf(path + strlen(path), "%s%s", dir, (last == '/' || last == '\\') ? "" : "/"); + } + + if (fname && fname[0] != '\0') { + sprintf(path + strlen(path), "%s", fname); + } + + if (ext && ext[0] != '\0') { + sprintf(path + strlen(path), "%s%s", (ext[0] == '.' ? "" : "."), ext); + } +} + +/// +/// Splits a path into the components the caller asked for. +/// Every non-null component is written, empty where the path has nothing to put in it, so a +/// caller may ask for any subset. Each buffer must hold its documented _MAX_ size, and the +/// extension carries its leading dot as the Microsoft routine's does. +/// +inline static void _splitpath(const char* path, char* drive, char* dir, char* fname, char* ext) +{ + if (drive) drive[0] = '\0'; + if (dir) dir[0] = '\0'; + if (fname) fname[0] = '\0'; + if (ext) ext[0] = '\0'; + + if (!path) { + return; + } + + // A path read out of a game file was written on Windows, so both separators and a drive + // letter are recognised whatever the host uses. + const char* start = path; + if (path[0] != '\0' && path[1] == ':') { + if (drive) { + drive[0] = path[0]; + drive[1] = ':'; + drive[2] = '\0'; + } + start = path + 2; + } + + const char* slash = NULL; + for (const char* scan = start; *scan != '\0'; ++scan) { + if (*scan == '/' || *scan == '\\') { + slash = scan; + } + } + + const char* base = (slash != NULL) ? slash + 1 : start; + + if (dir && slash != NULL) { + size_t length = (size_t)(base - start); + if (length > _MAX_DIR - 1) length = _MAX_DIR - 1; + memcpy(dir, start, length); + dir[length] = '\0'; + } + + const char* dot = strrchr(base, '.'); + + if (fname) { + size_t length = (dot != NULL) ? (size_t)(dot - base) : strlen(base); + if (length > _MAX_FNAME - 1) length = _MAX_FNAME - 1; + memcpy(fname, base, length); + fname[length] = '\0'; + } + + if (ext && dot != NULL) { + size_t length = strlen(dot); + if (length > _MAX_EXT - 1) length = _MAX_EXT - 1; + memcpy(ext, dot, length); + ext[length] = '\0'; + } +} + +inline static char* strupr(char* str) +{ + char* ret = str; + while (*str != '\0') { + *str = toupper(*str); + ++str; + } + return(ret); +} + +inline static void strrev(char* str) +{ + int len = strlen(str); + + for (int i = 0; i < len / 2; i++) { + char c = str[i]; + str[i] = str[len - i - 1]; + str[len - i - 1] = c; + } +} + +inline static void _strlwr(char* str) +{ + while (*str != '\0') { + *str = tolower(*str); + ++str; + } +} + +#endif // not _WIN32 diff --git a/code/anim.cpp b/code/anim.cpp index 62116e0ce..a8e78ca84 100644 --- a/code/anim.cpp +++ b/code/anim.cpp @@ -50,7 +50,6 @@ * Shorten_Attached_Anims -- Reduces attached animation durations. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "anim.h" @@ -245,7 +244,7 @@ AnimClass::AnimClass(AnimTypeClass const * type, Coord const & coord, int timede /// /// Constructs a blank animation object. /// This constructor serves the load system, which creates an empty animation through the -/// class factory and then fills it in from the save game. The animation joins the master +/// class table and then fills it in from the save game. The animation joins the master /// animation list but has no type and is nowhere on the map. /// AnimClass::AnimClass(void) : @@ -1729,18 +1728,9 @@ void AnimClass::Post_Load_Game(void) } -/// -/// Fetches the persistent class identifier for animation objects. -/// The save and load machinery uses this identifier to recreate the right kind of object -/// when a game is restored. -/// -/// Pointer to the location to store the class identifier at. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE AnimClass::GetClassID(CLSID * retval) +ClassID AnimClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_AnimClass; - return(S_OK); + return(ClassID_AnimClass); } diff --git a/code/anim.h b/code/anim.h index 8be08019f..8cf2c763a 100644 --- a/code/anim.h +++ b/code/anim.h @@ -64,7 +64,7 @@ class AnimClass : public ObjectClass, public StageClass AnimClass(void); virtual ~AnimClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; virtual void Post_Load(void) override; diff --git a/code/animtype.cpp b/code/animtype.cpp index 95ed94f4d..8e4d29d6d 100644 --- a/code/animtype.cpp +++ b/code/animtype.cpp @@ -38,7 +38,6 @@ * AnimTypeClass::operator delete -- Returns an anim type class object back to the pool. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "animtype.h" @@ -577,18 +576,9 @@ void AnimTypeClass::Serialize(SaveStreamClass & stream) } -/// -/// Fetches the persistent class identifier of this object. -/// This routine is used by the save game machinery to recognize an animation type when it -/// comes back off the stream. -/// -/// Pointer to the place to store the class identifier. -/// Returns with S_OK, or E_POINTER if there was nowhere to store the answer. -HRESULT STDMETHODCALLTYPE AnimTypeClass::GetClassID(CLSID * retval) +ClassID AnimTypeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_AnimTypeClass; - return(S_OK); + return(ClassID_AnimTypeClass); } diff --git a/code/animtype.h b/code/animtype.h index 11d63eed9..1b2432bf8 100644 --- a/code/animtype.h +++ b/code/animtype.h @@ -430,7 +430,7 @@ class AnimTypeClass : public ObjectTypeClass AnimTypeClass(char const * ininame = NULL); virtual ~AnimTypeClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; virtual void Post_Load(void) override; diff --git a/code/arraylist.h b/code/arraylist.h index 5cf6ecfb8..057887723 100644 --- a/code/arraylist.h +++ b/code/arraylist.h @@ -20,7 +20,7 @@ #include #include #include -#include +#include template diff --git a/code/audio/audiodecode.h b/code/audio/audiodecode.h index e4c393bfa..661924abf 100644 --- a/code/audio/audiodecode.h +++ b/code/audio/audiodecode.h @@ -40,6 +40,11 @@ struct AUDChunkHeaderType { }; #pragma pack(pop) +static_assert(sizeof(AUDHeaderType) == 12, "AUD file header layout changed"); +static_assert(offsetof(AUDHeaderType, Flags) == 10, "AUD file header layout changed"); +static_assert(sizeof(AUDChunkHeaderType) == 8, "AUD chunk header layout changed"); +static_assert(offsetof(AUDChunkHeaderType, Magic) == 4, "AUD chunk header layout changed"); + enum AudCodecType : uint8_t { AUD_CODEC_PCM = 0, diff --git a/code/autosave.cpp b/code/autosave.cpp index 5aa53d2f0..c38f85f04 100644 --- a/code/autosave.cpp +++ b/code/autosave.cpp @@ -7,6 +7,8 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ +#include "always.h" + #include "autosave.h" #include diff --git a/code/backendviews.hh b/code/backendviews.hh new file mode 100644 index 000000000..8f99ebf5c --- /dev/null +++ b/code/backendviews.hh @@ -0,0 +1,26 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The bgfx views the frame and the overlays are drawn through. bgfx renders views in +// ascending order, so these numbers are the draw order: the magnify pass has to precede +// the present pass for the present to sample this frame's output rather than the last +// one's, and both have to precede the overlays for the overlays to land on top. +// +// bgfxbackend.cpp owns the first two and code/ui the last two. They share this header so +// that neither can renumber a view the other draws through. + +#pragma once + + +enum BackendViewType { + BACKEND_VIEW_PRESCALE = 0, + BACKEND_VIEW_PRESENT = 1, + BACKEND_VIEW_UI = 2, + BACKEND_VIEW_DEV = 3, +}; diff --git a/code/base.cpp b/code/base.cpp index b4344eb5a..c3d566cbb 100644 --- a/code/base.cpp +++ b/code/base.cpp @@ -547,31 +547,6 @@ void BaseClass::Write_INI(CCINIClass & ini, char const * hname) /// Reads the base back in from a save game. /// /// Returns with the result reported by the stream read. -HRESULT STDMETHODCALLTYPE BaseClass::Load(IStream *stream) -{ - SaveStreamClass savestream(stream, SaveStreamClass::MODE_LOAD); - savestream.Set_Context("BaseClass"); - Serialize(savestream); - return(savestream.Result()); -} - - -/// -/// Writes the base out to a save game. -/// -/// Returns with the result reported by the stream write. -HRESULT STDMETHODCALLTYPE BaseClass::Save(IStream * stream) -{ - SaveStreamClass savestream(stream, SaveStreamClass::MODE_SAVE); - Serialize(savestream); - return(savestream.Result()); -} - - -/// -/// Lists the members the base plan holds. -/// -/// The stream carrying the members. void BaseClass::Serialize(SaveStreamClass & stream) { stream.Serialize(Nodes); diff --git a/code/base.h b/code/base.h index 352064766..7c8a0ed0b 100644 --- a/code/base.h +++ b/code/base.h @@ -37,7 +37,6 @@ #include "house.hh" #include "struct.hh" -#include class CCINIClass; @@ -103,8 +102,6 @@ class BaseClass */ void Read_INI(CCINIClass const & ini, char const * hname); void Write_INI(CCINIClass & ini, char const * hname); - virtual HRESULT STDMETHODCALLTYPE Load(IStream * stream); - virtual HRESULT STDMETHODCALLTYPE Save(IStream * stream); void Serialize(SaveStreamClass & stream); virtual void Compute_CRC(CRCEngine &) const; diff --git a/code/bgfxbackend.cpp b/code/bgfxbackend.cpp index af72c26a9..19450ce4c 100644 --- a/code/bgfxbackend.cpp +++ b/code/bgfxbackend.cpp @@ -12,6 +12,8 @@ #include "bgfxbackend.h" +#include "backendviews.hh" + #include "dbgprint.h" #include "except.h" @@ -24,6 +26,7 @@ #include #include +#include #include #include #include @@ -36,12 +39,8 @@ static const bgfx::EmbeddedShader _EmbeddedShaders[] = { }; -// The view that magnifies the frame when the pixel art filter needs an intermediate -// target, and the one that draws onto the window. Views render in ascending order, so -// the magnify pass must carry the lower id for the present pass to sample its output -// from this frame rather than the last one. -static const bgfx::ViewId VIEW_PRESCALE = 0; -static const bgfx::ViewId VIEW_PRESENT = 1; +static const bgfx::ViewId VIEW_PRESCALE = BACKEND_VIEW_PRESCALE; +static const bgfx::ViewId VIEW_PRESENT = BACKEND_VIEW_PRESENT; static bool _Initialized = false; @@ -79,6 +78,50 @@ struct BackendVertex // bgfx reports lost devices and shader failures through this rather than a return code, // so the engine would otherwise present to a black window with no explanation. +#ifndef _WIN32 +// The renderer reaches for two Win32 services that have no POSIX spelling. Trace output goes +// to the standard error stream, and the aligned allocator keeps the raw block address and the +// usable size in the two words ahead of the address it hands back. +static void OutputDebugString(char const * text) +{ + fputs(text != NULL ? text : "", stderr); +} + +static void _aligned_free(void * ptr) +{ + if (ptr != NULL) { + std::free(((void **)ptr)[-2]); + } +} + +static void * _aligned_realloc(void * ptr, std::size_t size, std::size_t alignment) +{ + if (alignment < sizeof(void *)) { + alignment = sizeof(void *); + } + + std::size_t const header = 2 * sizeof(void *); + void * raw = std::malloc(size + alignment + header); + if (raw == NULL) { + return(NULL); + } + + std::uintptr_t const base = (std::uintptr_t)raw + header; + void * aligned = (void *)((base + alignment - 1) & ~(std::uintptr_t)(alignment - 1)); + ((void **)aligned)[-2] = raw; + ((std::size_t *)aligned)[-1] = size; + + if (ptr != NULL) { + std::size_t const previous = ((std::size_t *)ptr)[-1]; + std::memcpy(aligned, ptr, previous < size ? previous : size); + _aligned_free(ptr); + } + + return(aligned); +} +#endif + + class BackendCallback : public bgfx::CallbackI { public: @@ -470,7 +513,9 @@ void Backend_On_Resize(int drawablewidth, int drawableheight) /// How wide the frame is drawn. /// How tall the frame is drawn. /// How the frame is filtered when it is drawn larger than it is. -void Backend_Present(void const * pixels, int pitch, int destx, int desty, int destwidth, int destheight, BackendScaleMode mode) +/// Has the frame changed since the last present? A present made only +/// to redraw an overlay leaves the frame texture as it is. +void Backend_Present(void const * pixels, int pitch, int destx, int desty, int destwidth, int destheight, BackendScaleMode mode, bool upload) { if (!_Initialized || pixels == NULL || !bgfx::isValid(_FrameTexture)) { return; @@ -481,7 +526,9 @@ void Backend_Present(void const * pixels, int pitch, int destx, int desty, int d return; } - if (_FrameIs565) { + if (!upload) { + // Nothing to do: the texture still holds the frame the last present uploaded. + } else if (_FrameIs565) { bgfx::updateTexture2D(_FrameTexture, 0, 0, 0, 0, (uint16_t)_FrameWidth, (uint16_t)_FrameHeight, bgfx::copy(pixels, (uint32_t)(_FrameHeight * pitch)), (uint16_t)pitch); } else if (_ConvertBuffer != NULL) { for (int y = 0; y < _FrameHeight; y++) { @@ -535,6 +582,19 @@ void Backend_Present(void const * pixels, int pitch, int destx, int desty, int d bool flipv = from_prescale && bgfx::getCaps()->originBottomLeft; Submit_Quad(VIEW_PRESENT, source, (float)destx, (float)desty, (float)destwidth, (float)destheight, samplerflags, flipv); +} + + +/// +/// Ends the frame the last present started, putting everything submitted to it on screen. +/// This is the only call to bgfx::frame() in the program; whatever draws between the +/// present and here shares the frame with the game's own image. +/// +void Backend_End_Frame(void) +{ + if (!_Initialized) { + return; + } bgfx::frame(); } diff --git a/code/bgfxbackend.h b/code/bgfxbackend.h index 8f3cb4184..cc37e0ea1 100644 --- a/code/bgfxbackend.h +++ b/code/bgfxbackend.h @@ -39,8 +39,10 @@ void Backend_Shutdown(void); bool Backend_Set_Frame_Size(int width, int height); void Backend_On_Resize(int drawablewidth, int drawableheight); -// Uploads the frame and presents it. The pixels are 16 bit 565 and stay owned by the -// caller; they are consumed before this returns. -void Backend_Present(void const * pixels, int pitch, int destx, int desty, int destwidth, int destheight, BackendScaleMode mode); +// Uploads the frame and submits it. The pixels are 16 bit 565 and stay owned by the +// caller; they are consumed before this returns. Nothing reaches the screen until +// Backend_End_Frame, so an overlay drawn in between shares the frame. +void Backend_Present(void const * pixels, int pitch, int destx, int desty, int destwidth, int destheight, BackendScaleMode mode, bool upload); +void Backend_End_Frame(void); char const * Backend_Renderer_Name(void); diff --git a/code/blight.cpp b/code/blight.cpp index 7850a58b5..a45f459db 100644 --- a/code/blight.cpp +++ b/code/blight.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "blight.h" @@ -280,17 +279,9 @@ void BuildingLightClass::AI(void) } -/// -/// Fetches the class identifier of this object. -/// This routine is used by the save game code so that an object of the right kind can -/// be created when the game is loaded back in. -/// -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE BuildingLightClass::GetClassID(CLSID * retval) +ClassID BuildingLightClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_BuildingLightClass; - return(S_OK); + return(ClassID_BuildingLightClass); } diff --git a/code/blight.h b/code/blight.h index c12e01e09..5a5ba3731 100644 --- a/code/blight.h +++ b/code/blight.h @@ -25,7 +25,7 @@ class BuildingLightClass : public ObjectClass BuildingLightClass(TechnoClass * owner = NULL); virtual ~BuildingLightClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/blowfish.cpp b/code/blowfish.cpp index cc9f746f1..91ec2c67e 100644 --- a/code/blowfish.cpp +++ b/code/blowfish.cpp @@ -39,15 +39,11 @@ #include "blowfish.h" -#ifndef NO_BLOWFISH_DLL -#include "iblowfish.h" -#endif #include -#ifdef NO_BLOWFISH_DLL /* ** Byte order controlled long integer. This integer is constructed ** so that character 0 (C0) is the most significant byte of the @@ -63,7 +59,6 @@ typedef union { unsigned char C0; } Char; } Int; -#endif /// @@ -73,11 +68,7 @@ typedef union { /// /// You must submit the key before calling the encrypt or decrypt routines. BlowfishEngine::BlowfishEngine(void) : -#ifndef NO_BLOWFISH_DLL - BlockCypher(CLSID_BlowfishObject) -#else IsKeyed(false) -#endif { } @@ -99,11 +90,9 @@ BlowfishEngine::BlowfishEngine(void) : *=============================================================================================*/ BlowfishEngine::~BlowfishEngine(void) { -#ifdef NO_BLOWFISH_DLL if (IsKeyed) { Submit_Key(NULL, 0); } -#endif } @@ -133,10 +122,6 @@ BlowfishEngine::~BlowfishEngine(void) *=============================================================================================*/ void BlowfishEngine::Submit_Key(void const * key, int length) { -#ifndef NO_BLOWFISH_DLL - BlockCypher->Set_Key(length, key); - return; -#else assert(length <= MAX_KEY_LENGTH); /* @@ -210,7 +195,6 @@ void BlowfishEngine::Submit_Key(void const * key, int length) } IsKeyed = true; -#endif } @@ -238,10 +222,6 @@ void BlowfishEngine::Submit_Key(void const * key, int length) *=============================================================================================*/ int BlowfishEngine::Encrypt(void const * plaintext, int length, void * cyphertext) { -#ifndef NO_BLOWFISH_DLL - BlockCypher->Encrypt(length, plaintext, cyphertext); - return(length); -#else if (plaintext == 0 || length == 0) { return(0); } @@ -281,7 +261,6 @@ int BlowfishEngine::Encrypt(void const * plaintext, int length, void * cyphertex memmove(cyphertext, plaintext, length); } return(length); -#endif } @@ -309,10 +288,6 @@ int BlowfishEngine::Encrypt(void const * plaintext, int length, void * cyphertex *=============================================================================================*/ int BlowfishEngine::Decrypt(void const * cyphertext, int length, void * plaintext) { -#ifndef NO_BLOWFISH_DLL - BlockCypher->Decrypt(length, cyphertext, plaintext); - return(length); -#else if (cyphertext == 0 || length == 0) { return(0); } @@ -352,11 +327,9 @@ int BlowfishEngine::Decrypt(void const * cyphertext, int length, void * plaintex memmove(plaintext, cyphertext, length); } return(length); -#endif } -#ifdef NO_BLOWFISH_DLL /*********************************************************************************************** * BlowfishEngine::Process_Block -- Process a block of data using Blowfish algorithm. * * * @@ -631,4 +604,3 @@ unsigned int const BlowfishEngine::S_Init[4][UCHAR_MAX+1] = { 0x90D4F869U,0xA65CDEA0U,0x3F09252DU,0xC208E69FU,0xB74E6132U,0xCE77E25BU,0x578FDFE3U,0x3AC372E6U } }; -#endif diff --git a/code/blowfish.h b/code/blowfish.h index 3cbc2d7c6..cb994291f 100644 --- a/code/blowfish.h +++ b/code/blowfish.h @@ -31,16 +31,11 @@ #pragma once +#ifdef _WIN32 #include "win.h" - -/// Names and comments from TLBs +#endif #include -#ifndef NO_BLOWFISH_DLL -#include "iblockci.h" -#include -_COM_SMARTPTR_TYPEDEF(IBlockCipher, __uuidof(IBlockCipher)); -#endif /* ** This engine will process data blocks by encryption and decryption. @@ -70,14 +65,6 @@ class BlowfishEngine { enum {MAX_KEY_LENGTH=56}; private: -#ifndef NO_BLOWFISH_DLL - /* - * This points to the block cipher object that performs the actual key setup and - * block processing. Where the cipher is available as a component, this engine is - * only a convenience wrapper around it and keeps no tables of its own. - */ - IBlockCipherPtr BlockCypher; -#else bool IsKeyed; void Sub_Key_Encrypt(unsigned int & left, unsigned int & right); @@ -107,5 +94,4 @@ class BlowfishEngine { ** S-Box tables (four). */ unsigned int bf_S[4][UCHAR_MAX+1]; -#endif }; diff --git a/code/brain.cpp b/code/brain.cpp index b09549d86..ef7aae44c 100644 --- a/code/brain.cpp +++ b/code/brain.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "brain.h" @@ -48,18 +47,9 @@ NeuronClass::~NeuronClass(void) } -/// -/// Fetches the class identifier of this object. -/// This routine is used by the save game system so that it knows what kind of object to -/// construct when the stream is read back in. -/// -/// Pointer to the place to store the class identifier. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE NeuronClass::GetClassID(CLSID * retval) +ClassID NeuronClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_NeuronClass; - return(S_OK); + return(ClassID_NeuronClass); } @@ -155,19 +145,12 @@ bool BrainClass::Add_Neuron(NeuronClass *neuron) /// Saves this brain to the save game stream. /// /// Should the neurons be marked clean once they are written? -/// -/// Returns with S_OK when the brain was written, E_POINTER when no stream was supplied, -/// or the stream's own failure code. -/// -HRESULT BrainClass::Save(IStream * stream, BOOL cleardirty) +/// bool; Was the record written whole? +bool BrainClass::Save(SaveStreamClass & stream, bool cleardirty) { - if (stream == NULL) { - return(E_POINTER); - } - SaveStreamClass savestream(stream, SaveStreamClass::MODE_SAVE); - Serialize(savestream, cleardirty); - return(savestream.Result()); + Serialize(stream, cleardirty); + return(!stream.Was_Error()); } @@ -176,20 +159,13 @@ HRESULT BrainClass::Save(IStream * stream, BOOL cleardirty) /// Whatever neurons the brain was holding are destroyed first, so the stream's neurons /// entirely replace them. /// -/// -/// Returns with S_OK when the brain was read, E_POINTER when no stream was supplied, or -/// the stream's own failure code. -/// -HRESULT BrainClass::Load(IStream * stream) +/// bool; Was the record read whole? +bool BrainClass::Load(SaveStreamClass & stream) { - if (stream == NULL) { - return(E_POINTER); - } - SaveStreamClass savestream(stream, SaveStreamClass::MODE_LOAD); - savestream.Set_Context("BrainClass"); - Serialize(savestream); - return(savestream.Result()); + stream.Set_Context("BrainClass"); + Serialize(stream); + return(!stream.Was_Error()); } @@ -200,7 +176,7 @@ HRESULT BrainClass::Load(IStream * stream) /// /// The stream carrying the members. /// Should the neurons be marked clean once they are written? -void BrainClass::Serialize(SaveStreamClass & stream, BOOL cleardirty) +void BrainClass::Serialize(SaveStreamClass & stream, bool cleardirty) { int count = Neurons.Count(); stream.Serialize(count); @@ -212,10 +188,10 @@ void BrainClass::Serialize(SaveStreamClass & stream, BOOL cleardirty) for (int i = 0; i < count && !stream.Was_Error(); i++) { if (stream.Is_Loading()) { NeuronClass * neuron = new NeuronClass; - neuron->Load(stream.Get_Stream()); + neuron->Load(stream); Add_Neuron(neuron); } else { - Neurons[i]->Save(stream.Get_Stream(), cleardirty); + Neurons[i]->Save(stream, cleardirty); } } // MinCount -- the limits a brain was prepared with rather than anything it accumulated. diff --git a/code/brain.h b/code/brain.h index 9287f630c..198a18a5f 100644 --- a/code/brain.h +++ b/code/brain.h @@ -24,7 +24,7 @@ class NeuronClass : public AbstractClass NeuronClass(void); virtual ~NeuronClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual RTTIType Fetch_RTTI(void) const override { return(RTTI_NEURON); } @@ -62,10 +62,10 @@ class BrainClass void Init(int min, int max); bool Add_Neuron(NeuronClass *neuron); - HRESULT Load(IStream * stream); - HRESULT Save(IStream * stream, BOOL cleardirty); + bool Load(SaveStreamClass & stream); + bool Save(SaveStreamClass & stream, bool cleardirty); - void Serialize(SaveStreamClass & stream, BOOL cleardirty = FALSE); + void Serialize(SaveStreamClass & stream, bool cleardirty = false); private: /* diff --git a/code/building.cpp b/code/building.cpp index 4e9c1ee30..77fc34c57 100644 --- a/code/building.cpp +++ b/code/building.cpp @@ -102,7 +102,6 @@ * BuildingClass::~BuildingClass -- Destructor for building type objects. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "building.h" @@ -138,7 +137,7 @@ #include "house.h" #include "houstype.h" #include "iloco.h" -#include "ilocos.h" +#include "classids.h" #include "incdec.h" #include "infantry.h" #include "infatype.h" @@ -5533,10 +5532,8 @@ int BuildingClass::Do_MISSION_REPAIR(void) ** distance check. Fixed-wing aircraft are very inaccurate with ** their landings. */ - IPersistPtr persist(tech->Locomotion); - CLSID clsid; - persist->GetClassID(&clsid); - bool hover = (clsid == CLSID_HoverLocomotion) != 0; + ClassID const clsid = Locomotion_Class_ID(tech->Locomotion.get()); + bool hover = (clsid == ClassID_HoverLocomotion) != 0; if (hover) { distance = 0x96; } @@ -5993,7 +5990,7 @@ int BuildingClass::Do_MISSION_MISSILE(void) Status = DONE; return(1); } else { - bullet->Release(); + delete bullet; Begin_Mode(BSTATE_IDLE); // keep the door closed. Assign_Mission(MISSION_GUARD); return(4 * TICKS_PER_SECOND); @@ -6253,27 +6250,25 @@ int BuildingClass::Do_MISSION_UNLOAD(void) if (unit) { unit->Assign_Mission(MISSION_MOVE); - IPersistPtr persist(unit->Locomotion); - CLSID clsid; - persist->GetClassID(&clsid); + ClassID const clsid = Locomotion_Class_ID(unit->Locomotion.get()); - if (clsid == CLSID_TunnelLocomotion) { - IPiggybackPtr piggy(unit->Locomotion); + if (clsid == ClassID_TunnelLocomotion) { + IPiggyback * piggy = Piggyback_Of(unit->Locomotion.get()); if (piggy != NULL && piggy->Is_Piggybacking()) { - piggy->End_Piggyback(&unit->Locomotion); + unit->Locomotion = piggy->End_Piggyback(); } - ILocomotionPtr walk(CLSID_DriveLocomotion); + std::unique_ptr walk = Create_Locomotor(ClassID_DriveLocomotion); walk->Link_To_Object(unit); - piggy = IPiggybackPtr(walk); + piggy = Piggyback_Of(walk.get()); if (piggy != NULL) { - piggy->Begin_Piggyback(unit->Locomotion); - unit->Locomotion = walk; + piggy->Begin_Piggyback(std::move(unit->Locomotion)); + unit->Locomotion = std::move(walk); unit->Locomotion->Force_Track(DriveLocomotionClass::OUT_OF_WEAPON_FACTORY, coord); } else { int damage = unit->Strength; unit->Take_Damage(damage, 0, Rule->C4Warhead, NULL, true); } - } else if (clsid != CLSID_DriveLocomotion) { + } else if (clsid != ClassID_DriveLocomotion) { unit->Assign_Destination(&Map[Get_Cell() + Cell(3, 1)]); } else { Coord cs; @@ -8845,9 +8840,8 @@ void BuildingClass::Clear_Occupy_Bit(Coord const & coord) /// since the one it is about to be given is the one it was saved with. Post_Load enters it /// again once that identity has arrived. /// -/// Returns with S_OK if the building was read, or the failure code from the -/// underlying stream. -HRESULT STDMETHODCALLTYPE BuildingClass::Load(IStream *stream) +/// bool; Was the record read whole? +bool BuildingClass::Load(SaveStreamClass & stream) { TargetTracker.Remove_Index(Fetch_ID()); return(BASECLASS::Load(stream)); @@ -10348,18 +10342,9 @@ void BuildingClass::Discharge_Turret(void) } -/// -/// Fetches the persistent class identifier for this building. -/// This routine is part of the persistence support. The save code writes this identifier -/// ahead of the object so that the loader knows what kind of object to create. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE BuildingClass::GetClassID(CLSID * retval) +ClassID BuildingClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_BuildingClass; - return(S_OK); + return(ClassID_BuildingClass); } diff --git a/code/building.h b/code/building.h index fd605bf5f..3dbbd841e 100644 --- a/code/building.h +++ b/code/building.h @@ -357,8 +357,8 @@ class BuildingClass : public TechnoClass BuildingClass(BuildingTypeClass const * type = NULL, HouseClass * house = NULL); virtual ~BuildingClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; - virtual HRESULT STDMETHODCALLTYPE Load(IStream * stream) override; + virtual ClassID Class_ID(void) const override; + virtual bool Load(SaveStreamClass & stream) override; virtual void Serialize(SaveStreamClass & stream) override; virtual void Post_Load(void) override; diff --git a/code/builtype.cpp b/code/builtype.cpp index 72fdc7395..478a06b25 100644 --- a/code/builtype.cpp +++ b/code/builtype.cpp @@ -55,7 +55,6 @@ * BuildingTypeClass::operator new -- Allocates a building type object from the special heap.* * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "builtype.h" @@ -1942,18 +1941,9 @@ void BuildingTypeClass::Serialize(SaveStreamClass & stream) } -/// -/// Fetches the class identifier of this object. -/// This routine is used by the save game system so that it knows what kind of object to -/// construct when the stream is read back in. -/// -/// Pointer to the class ID to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE BuildingTypeClass::GetClassID(CLSID * retval) +ClassID BuildingTypeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_BuildingTypeClass; - return(S_OK); + return(ClassID_BuildingTypeClass); } diff --git a/code/builtype.h b/code/builtype.h index 00c63bcd9..4019ff5ad 100644 --- a/code/builtype.h +++ b/code/builtype.h @@ -855,7 +855,7 @@ class BuildingTypeClass : public TechnoTypeClass BuildingTypeClass(char const * ininame = NULL); virtual ~BuildingTypeClass() override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; virtual void Post_Load(void) override; diff --git a/code/bullet.cpp b/code/bullet.cpp index fd5ef3660..40c62322f 100644 --- a/code/bullet.cpp +++ b/code/bullet.cpp @@ -47,7 +47,6 @@ * BulletClass::~BulletClass -- Destructor for bullet objects. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "bullet.h" @@ -95,7 +94,6 @@ #include -extern ULONG COMRefCount; /*********************************************************************************************** @@ -1461,35 +1459,6 @@ void BulletClass::Serialize(SaveStreamClass & stream) } -/// -/// Takes out a reference on this projectile. -/// This is the IUnknown implementation used by the COM machinery that owns projectiles. -/// -/// Returns with the number of references now outstanding. -ULONG STDMETHODCALLTYPE BulletClass::AddRef(void) -{ - COMRefCount++; - return(InterlockedIncrement(&RefCount)); -} - - -/// -/// Drops a reference to this projectile. -/// This is the IUnknown implementation. The projectile deletes itself when the last -/// reference to it is released. -/// -/// Returns with the number of references still outstanding. -ULONG STDMETHODCALLTYPE BulletClass::Release(void) -{ - COMRefCount--; - ULONG count = InterlockedDecrement(&RefCount); - if (count == 0) { - delete this; - } - return(count); -} - - /// /// Can this projectile steer toward its target? /// The flight logic calls this routine to decide whether the projectile should be turned @@ -1507,9 +1476,7 @@ bool BulletClass::Is_Homing(void) const /// /// Creates a projectile and fills in the data for the shot. -/// This routine is used by the weapon firing code in place of a bare new -- projectiles are -/// COM objects, so the instance must come from the class factory. The projectile is inert -/// until it is unlimboed with a starting position and velocity. +/// The projectile is inert until it is unlimboed with a starting position and velocity. /// /// The object that fired the shot. It receives credit for any kill. /// The damage the projectile will inflict when it detonates. @@ -1518,12 +1485,7 @@ bool BulletClass::Is_Homing(void) const /// made. BulletClass * Create_Bullet(BulletTypeClass const *type, AbstractClass *target, TechnoClass *payback, int strength, WarheadTypeClass const *warhead, int max_speed, int range, bool bright) { - LPVOID unk = NULL; - if (FAILED(CoCreateInstance(CLSID_BulletClass, NULL, CLSCTX_INPROC_SERVER|CLSCTX_INPROC_HANDLER|CLSCTX_LOCAL_SERVER, IID_IUnknown, &unk))) { - return(NULL); - } - - BulletClass * bullet = (BulletClass *)unk; + BulletClass * bullet = new BulletClass; bullet->Set_Bullet_Data(type, target, payback, strength, warhead, max_speed, range, bright); return(bullet); } @@ -1570,17 +1532,9 @@ RTTIType BulletClass::Fetch_RTTI(void) const } -/// -/// Fetches the class identifier for this projectile. -/// This is the IPersist implementation the save and load machinery uses to recognize which -/// kind of object it is about to read back from the stream. -/// -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE BulletClass::GetClassID(CLSID * retval) +ClassID BulletClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_BulletClass; - return(S_OK); + return(ClassID_BulletClass); } diff --git a/code/bullet.h b/code/bullet.h index a51519ad8..4f2f2aa2a 100644 --- a/code/bullet.h +++ b/code/bullet.h @@ -52,8 +52,6 @@ class BulletClass : public ObjectClass typedef ObjectClass BASECLASS; public: - virtual ULONG STDMETHODCALLTYPE AddRef(void) override; - virtual ULONG STDMETHODCALLTYPE Release(void) override; public: @@ -78,7 +76,7 @@ class BulletClass : public ObjectClass BulletClass(void); virtual ~BulletClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/bullettype.cpp b/code/bullettype.cpp index 4fd298dcc..0c19cd2db 100644 --- a/code/bullettype.cpp +++ b/code/bullettype.cpp @@ -38,7 +38,6 @@ * BulletTypeClass::operator new -- Allocates a bullet type object from the special heap. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "bullettype.h" @@ -356,15 +355,9 @@ void BulletTypeClass::Serialize(SaveStreamClass & stream) } -/// -/// Fetches the class identifier that the save game code stores for this object. -/// -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE BulletTypeClass::GetClassID(CLSID * retval) +ClassID BulletTypeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_BulletTypeClass; - return(S_OK); + return(ClassID_BulletTypeClass); } diff --git a/code/bullettype.h b/code/bullettype.h index 9a8c96831..ecd3ceaec 100644 --- a/code/bullettype.h +++ b/code/bullettype.h @@ -237,7 +237,7 @@ class BulletTypeClass : public ObjectTypeClass BulletTypeClass(char const * name = NULL); virtual ~BulletTypeClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; virtual void Post_Load(void) override; diff --git a/code/campaign.cpp b/code/campaign.cpp index 2731a716a..8ac83dc87 100644 --- a/code/campaign.cpp +++ b/code/campaign.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "campaign.h" @@ -126,18 +125,9 @@ void Read_Battle_INI(CCINIClass const & ini) } -/// -/// Fetches the class identifier of this object. -/// This routine is required of every persistent object so that the save game loader -/// can recognize what to construct when the object is read back. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE CampaignClass::GetClassID(CLSID * retval) +ClassID CampaignClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_CampaignClass; - return(S_OK); + return(ClassID_CampaignClass); } diff --git a/code/campaign.h b/code/campaign.h index a59c4da89..585345e8d 100644 --- a/code/campaign.h +++ b/code/campaign.h @@ -23,7 +23,7 @@ class CampaignClass : public AbstractTypeClass CampaignClass(char const * name = NULL); virtual ~CampaignClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/cdfile.cpp b/code/cdfile.cpp index 11ef777ac..0c3194616 100644 --- a/code/cdfile.cpp +++ b/code/cdfile.cpp @@ -42,6 +42,7 @@ #include "cdfile.h" #include +#include /* ** Pointer to the first search path record. @@ -180,7 +181,7 @@ void CDFileClass::Set_User_Path(char const * path) break; default: - UserPath += '\\'; + UserPath += std::filesystem::path::preferred_separator; break; } } @@ -459,105 +460,3 @@ int CDFileClass::Delete(void) return(BASECLASS::Delete()); } - - -HANDLE FindFileHandle = INVALID_HANDLE_VALUE; - -/// -/// Begins a search for the files matching the wildcard specified. -/// This routine will look in the current directory first and then work along the search -/// drive list, settling on the first drive that has a match. Only ordinary files qualify; -/// directories and system, hidden, or temporary files are passed over. Any search still -/// in progress is closed off first. -/// -/// The wildcard to search for; filled in with the file found. -/// bool; Was a matching file found? -/// Be sure that the buffer is big enough to hold the filename returned. -bool CDFileClass::Find_First_File(char *fname) -{ - WIN32_FIND_DATAA fb; - char scan_path[MAX_PATH]; - SearchDriveType *entry; - - if (fname) { - - Find_Close(); - - strcpy(scan_path, fname); - - HANDLE file_handle = ::FindFirstFile(scan_path, &fb); - if (file_handle != INVALID_HANDLE_VALUE && !(fb.dwFileAttributes & (FILE_ATTRIBUTE_TEMPORARY|FILE_ATTRIBUTE_DIRECTORY|FILE_ATTRIBUTE_SYSTEM|FILE_ATTRIBUTE_HIDDEN))) { - - strcpy(fname, fb.cFileName); - FindFileHandle = file_handle; - - return(true); - } - - entry = First; - - if (entry != NULL) { - - while (true) { - - strcpy(scan_path, entry->Path); - strcat(scan_path, fname); - - file_handle = ::FindFirstFile(scan_path, &fb); - if (file_handle != INVALID_HANDLE_VALUE && !(fb.dwFileAttributes & (FILE_ATTRIBUTE_TEMPORARY|FILE_ATTRIBUTE_DIRECTORY|FILE_ATTRIBUTE_SYSTEM|FILE_ATTRIBUTE_HIDDEN))) { - break; - } - - entry = (SearchDriveType *)entry->Next; - if (entry == NULL) { - return(false); - } - } - - strcpy(fname, fb.cFileName); - FindFileHandle = file_handle; - - return(true); - } - } - return(false); -} - - -/// -/// Fetches the next file that matches the search in progress. -/// This routine continues the scan begun by Find_First_File, working through the rest of -/// the matches on whichever drive that routine settled upon. -/// -/// Buffer to fill in with the name of the file found. -/// bool; Was another matching file found? -/// Be sure that the buffer is big enough to hold the filename returned. -bool CDFileClass::Find_Next_File(char *buffer) -{ - WIN32_FIND_DATAA fb; - - if (buffer) { - - if (FindFileHandle != INVALID_HANDLE_VALUE && ::FindNextFile(FindFileHandle, &fb) == TRUE) { - strcpy(buffer, fb.cFileName); - return(true); - } - - buffer[0] = '\0'; - } - return(false); -} - - -/// -/// Closes off the file search that is in progress. -/// Call this routine when the results of a Find_First_File scan are no longer wanted, so -/// that the search handle held on the game's behalf is given back to the system. -/// -void CDFileClass::Find_Close(void) -{ - if (FindFileHandle != INVALID_HANDLE_VALUE) { - FindClose(FindFileHandle); - FindFileHandle = INVALID_HANDLE_VALUE; - } -} diff --git a/code/cdfile.h b/code/cdfile.h index 9f1b34a16..913968b70 100644 --- a/code/cdfile.h +++ b/code/cdfile.h @@ -72,10 +72,6 @@ class CDFileClass : public BufferIOFileClass static void Set_User_Path(char const * path); static char const * User_Path(void); - static bool Find_First_File(char *buffer); - static bool Find_Next_File(char *buffer); - static void Find_Close(void); - private: char const * Capture_Name(char const * filename); diff --git a/code/cell.cpp b/code/cell.cpp index eca014b12..40f782f8f 100644 --- a/code/cell.cpp +++ b/code/cell.cpp @@ -74,7 +74,6 @@ * CellClass::Wall_Update -- Updates the imagery for wall objects in cell. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "cell.h" @@ -5168,18 +5167,9 @@ void CellClass::Detach(AbstractClass const * target) } -/// -/// Fetches the class identifier of this object. -/// This is the persistence requirement that lets the save system recognize a cell when a -/// saved game is read back in. -/// -/// Pointer to the location to store the class identifier in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE CellClass::GetClassID(CLSID * retval) +ClassID CellClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_CellClass; - return(S_OK); + return(ClassID_CellClass); } diff --git a/code/cell.h b/code/cell.h index 790b5b7dc..c5bda32e7 100644 --- a/code/cell.h +++ b/code/cell.h @@ -517,7 +517,7 @@ class CellClass : public AbstractClass CellClass(void); virtual ~CellClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; virtual void Post_Load(void) override; diff --git a/code/classfactory.cpp b/code/classfactory.cpp new file mode 100644 index 000000000..63d70e527 --- /dev/null +++ b/code/classfactory.cpp @@ -0,0 +1,67 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#include "always.h" + +#include "classfactory.h" +#include "dbgprint.h" + +#include + +namespace { + +struct ClassEntryType { + ClassID Class; + ClassCreatorType Creator; +}; + +std::vector Classes; + +} // namespace + + +// A later registration of the same identifier wins, as the last class object +// published did before. +void Register_Class(ClassID const & classid, ClassCreatorType creator) +{ + for (ClassEntryType & entry : Classes) { + if (entry.Class == classid) { + entry.Creator = creator; + return; + } + } + Classes.push_back({ classid, creator }); +} + + +void Unregister_Classes(void) +{ + Classes.clear(); +} + + +/// +/// Creates a new object of the registered class named by the identifier. +/// +/// The object, owned by the caller, or NULL with a debug line naming the +/// identifier when no class was registered for it. +IPersistent * Create_Object(ClassID const & classid) +{ + for (ClassEntryType const & entry : Classes) { + if (entry.Class == classid) { + return(entry.Creator()); + } + } + + DebugString("No class is registered for {%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}\n", + (unsigned long)classid.Data1, (unsigned int)classid.Data2, (unsigned int)classid.Data3, + classid.Data4[0], classid.Data4[1], classid.Data4[2], classid.Data4[3], + classid.Data4[4], classid.Data4[5], classid.Data4[6], classid.Data4[7]); + return(NULL); +} diff --git a/code/classfactory.h b/code/classfactory.h index f11d77047..23830735c 100644 --- a/code/classfactory.h +++ b/code/classfactory.h @@ -9,113 +9,18 @@ #pragma once -template -class TClassFactory : public IClassFactory -{ - public: - TClassFactory(void); - - STDMETHOD(QueryInterface)(REFIID riid, void **ppvObj); - STDMETHOD_(ULONG, AddRef)(void); - STDMETHOD_(ULONG, Release)(void); - - STDMETHOD(CreateInstance)(IUnknown *pUnkOuter, REFIID riid, void **ppbObj); - STDMETHOD(LockServer)(BOOL fLock); - - private: - /* - * This is the number of outstanding references to this factory, counting both the - * interface pointers handed out and any server locks taken. The factory deletes - * itself once the count falls back to zero. - */ - LONG RefCount; -}; - - -template -TClassFactory::TClassFactory(void) : - RefCount(0) -{ -} - - -template -STDMETHODIMP TClassFactory::QueryInterface(REFIID riid, void **ppvObj) -{ - if (ppvObj == NULL) { - return(E_POINTER); - } - - *ppvObj = NULL; - - if (riid == IID_IUnknown) { - *ppvObj = (void *)((IClassFactory *)this); - } else if (riid == IID_IClassFactory) { - *ppvObj = (void *)((IClassFactory *)this); - } - - if (*ppvObj == NULL) { - return(E_NOINTERFACE); - } - - ((IClassFactory *)this)->AddRef(); - - return(S_OK); -} +#include "persist.h" +// The classes a saved game or a unit type can name by class identifier. Startup +// registers each one; nothing is created for an identifier nobody registered. +typedef IPersistent * (* ClassCreatorType)(void); -template -ULONG TClassFactory::AddRef(void) -{ - return(InterlockedIncrement(&RefCount)); -} - - -template -ULONG TClassFactory::Release(void) -{ - int count = InterlockedDecrement(&RefCount); - if (count == 0) { - delete this; - } - - return(count); -} - - -template -STDMETHODIMP TClassFactory::CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppvObj) -{ - if (ppvObj == NULL) { - return(E_INVALIDARG); - } - - *ppvObj = NULL; - if (pUnkOuter != NULL) { - return(CLASS_E_NOAGGREGATION); - } - - T *obj = new T(); - if (obj == NULL) { - return(E_OUTOFMEMORY); - } - - HRESULT hr = obj->QueryInterface(riid, ppvObj); - if (FAILED(hr)) { - delete obj; - } - - return(hr); -} - +void Register_Class(ClassID const & classid, ClassCreatorType creator); +void Unregister_Classes(void); +IPersistent * Create_Object(ClassID const & classid); template -HRESULT STDMETHODCALLTYPE TClassFactory::LockServer(BOOL fLock) +void Register_Class(ClassID const & classid) { - if (fLock) { - RefCount++; - } else { - RefCount--; - } - return(S_OK); + Register_Class(classid, []() -> IPersistent * { return(new T); }); } diff --git a/code/classid.h b/code/classid.h new file mode 100644 index 000000000..42474d286 --- /dev/null +++ b/code/classid.h @@ -0,0 +1,35 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#pragma once + +#include + +// The identity a persistent class is saved and named by. The sixteen bytes are those of +// the COM class identifier the class once registered, kept as they are because saved +// games and the Locomotor= key carry them. +struct ClassID +{ + unsigned int Data1; + unsigned short Data2; + unsigned short Data3; + unsigned char Data4[8]; +}; + +static_assert(sizeof(ClassID) == 16, "a class identifier is sixteen bytes on disk"); + +inline bool operator==(ClassID const & left, ClassID const & right) +{ + return(std::memcmp(&left, &right, sizeof(ClassID)) == 0); +} + +inline bool operator!=(ClassID const & left, ClassID const & right) +{ + return(!(left == right)); +} diff --git a/code/classids.cpp b/code/classids.cpp new file mode 100644 index 000000000..0b88b27d7 --- /dev/null +++ b/code/classids.cpp @@ -0,0 +1,82 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#include "classids.h" + +ClassID const ClassID_HouseClass = {0xD9D4A910,0x87C6,0x11D1,{0xB7,0x07,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; +ClassID const ClassID_SuperWeaponTypeClass = {0x0CF2BCE7,0x36E4,0x11D2,{0xB8,0xD8,0x00,0x60,0x08,0xC8,0x09,0xED}}; +ClassID const ClassID_SuperWeaponClass = {0xD7F754C6,0x391C,0x11D2,{0x9B,0x64,0x00,0x10,0x4B,0x97,0x2F,0xE8}}; +ClassID const ClassID_UnitTypeClass = {0xDCBD42EA,0x0546,0x11D2,{0xAC,0xA4,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_InfantryTypeClass = {0xAE8B33D8,0x061C,0x11D2,{0xAC,0xA4,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_AircraftTypeClass = {0xAE8B33D9,0x061C,0x11D2,{0xAC,0xA4,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_BuildingTypeClass = {0xAE8B33DB,0x061C,0x11D2,{0xAC,0xA4,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_BulletTypeClass = {0x5AF2CE77,0x0634,0x11D2,{0xAC,0xA4,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_TerrainTypeClass = {0x5AF2CE7B,0x0634,0x11D2,{0xAC,0xA4,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_IsometricTileTypeClass = {0x5AF2CE7A,0x0634,0x11D2,{0xAC,0xA4,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_OverlayTypeClass = {0x5AF2CE79,0x0634,0x11D2,{0xAC,0xA4,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_SmudgeTypeClass = {0x5AF2CE78,0x0634,0x11D2,{0xAC,0xA4,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_AnimTypeClass = {0xAE8B33DA,0x061C,0x11D2,{0xAC,0xA4,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_HouseTypeClass = {0x1DD43928,0x046B,0x11D2,{0xAC,0xA4,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_IsometricTileClass = {0x0E272DC0,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; +ClassID const ClassID_VoxelAnimClass = {0x0E272DC1,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; +ClassID const ClassID_AircraftClass = {0x0E272DC2,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; +ClassID const ClassID_AnimClass = {0x0E272DC3,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; +ClassID const ClassID_InfantryClass = {0x0E272DC4,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; +ClassID const ClassID_SmudgeClass = {0x0E272DC5,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; +ClassID const ClassID_BuildingClass = {0x0E272DC6,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; +ClassID const ClassID_OverlayClass = {0x0E272DC7,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; +ClassID const ClassID_ParticleSystemClass = {0x0E272DC8,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; +ClassID const ClassID_ParticleSystemTypeClass = {0x703E044A,0x0FB1,0x11D2,{0x81,0x72,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_BulletClass = {0x0E272DC9,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; +ClassID const ClassID_UnitClass = {0x0E272DCA,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; +ClassID const ClassID_ParticleClass = {0x0E272DCC,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; +ClassID const ClassID_ParticleTypeClass = {0x703E044B,0x0FB1,0x11D2,{0x81,0x72,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_WaveClass = {0x0E272DCD,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; +ClassID const ClassID_BuildingLightClass = {0x54822258,0xD8A8,0x11D1,{0xB4,0x62,0x00,0x60,0x97,0xC6,0xA9,0x79}}; +ClassID const ClassID_TerrainClass = {0x0E272DCE,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; +ClassID const ClassID_TubeClass = {0x0B4CA41C,0xB3A7,0x11D1,{0xB4,0x57,0x00,0x60,0x97,0xC6,0xA9,0x79}}; +ClassID const ClassID_TeamClass = {0x0E272DCF,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; +ClassID const ClassID_TaskForceClass = {0x61DE341E,0x0774,0x11D2,{0xAC,0xA5,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_TeamTypeClass = {0xD1DBA64E,0x0778,0x11D2,{0xAC,0xA5,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_VoxelAnimTypeClass = {0x2EBB6D66,0x0D4D,0x11D2,{0x81,0x72,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_ScriptClass = {0x42F3A646,0x0789,0x11D2,{0xAC,0xA5,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_ScriptTypeClass = {0x42F3A647,0x0789,0x11D2,{0xAC,0xA5,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_TagClass = {0x54F6E432,0x09ED,0x11D2,{0xAC,0xA5,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_TagTypeClass = {0x54F6E433,0x09ED,0x11D2,{0xAC,0xA5,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_TriggerClass = {0xC02D1590,0x0A2A,0x11D2,{0xAC,0xA7,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_TriggerTypeClass = {0xC02D1591,0x0A2A,0x11D2,{0xAC,0xA7,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_ActionClass = {0x4F0EC392,0x0A55,0x11D2,{0xAC,0xA7,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_EventClass = {0x4F0EC393,0x0A55,0x11D2,{0xAC,0xA7,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_FactoryClass = {0x34ECD9A8,0x0AB0,0x11D2,{0xAC,0xA7,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_WeaponTypeClass = {0x9FD219CA,0x0F7B,0x11D2,{0x81,0x72,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_WarheadTypeClass = {0xA8C54DA4,0x0F7B,0x11D2,{0x81,0x72,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_WaypointPath = {0xF73125BA,0x1054,0x11D2,{0x81,0x72,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_LightSource = {0x6F9C48F0,0x1207,0x11D2,{0x81,0x74,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_CampaignClass = {0xFFDAC848,0x1517,0x11D2,{0x81,0x75,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_SideClass = {0xC53DD372,0x151E,0x11D2,{0x81,0x75,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_TiberiumClass = {0xC53DD373,0x151E,0x11D2,{0x81,0x75,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_CellClass = {0xC1BF99CE,0x1A8C,0x11D2,{0x81,0x75,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_EMPulseClass = {0xB825CB22,0x200E,0x11D2,{0x9F,0xA9,0x00,0x60,0x08,0x9A,0xD4,0x58}}; +ClassID const ClassID_TacticalMapClass = {0xCF56B38A,0x240D,0x11D2,{0x81,0x7C,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_AITriggerTypeClass = {0xBA093524,0x4CF4,0x11D2,{0xBC,0x26,0x00,0x10,0x4B,0x8F,0xB0,0x4D}}; +ClassID const ClassID_AITriggerClass = {0x03C4CE76,0x4CF5,0x11D2,{0xBC,0x26,0x00,0x10,0x4B,0x8F,0xB0,0x4D}}; +ClassID const ClassID_NeuronClass = {0x241AB316,0x4CF5,0x11D2,{0xBC,0x26,0x00,0x10,0x4B,0x8F,0xB0,0x4D}}; +ClassID const ClassID_FoggedObjectClass = {0x1C470B0E,0x69D7,0x11D2,{0xB8,0xF2,0x00,0x60,0x08,0xC8,0x09,0xED}}; +ClassID const ClassID_AlphaShapeClass = {0x623C7584,0x74E7,0x11D2,{0xB8,0xF5,0x00,0x60,0x08,0xC8,0x09,0xED}}; +ClassID const ClassID_VeinholeMonsterClass = {0x5192D06A,0xC632,0x11D2,{0xB9,0x0B,0x00,0x60,0x08,0xC8,0x09,0xED}}; +ClassID const ClassID_DriveLocomotion = {0x4A582741,0x9839,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; +ClassID const ClassID_HoverLocomotion = {0x4A582742,0x9839,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; +ClassID const ClassID_TunnelLocomotion = {0x4A582743,0x9839,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; +ClassID const ClassID_WalkLocomotion = {0x4A582744,0x9839,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; +ClassID const ClassID_BallisticLocomotion = {0x4A582745,0x9839,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; +ClassID const ClassID_FlyerLocomotion = {0x4A582746,0x9839,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; +ClassID const ClassID_TeleportLocomotion = {0x4A582747,0x9839,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; +ClassID const ClassID_MechLocomotion = {0x55D141B8,0xDB94,0x11D1,{0xAC,0x98,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_JumpjetLocomotion = {0x92612C46,0xF71F,0x11D1,{0xAC,0x9F,0x00,0x60,0x08,0x05,0x5B,0xB5}}; +ClassID const ClassID_LevitateLocomotion = {0x3DC0B295,0x6546,0x11D3,{0x80,0xB0,0x00,0x90,0x27,0x92,0x49,0x4C}}; diff --git a/code/classids.h b/code/classids.h new file mode 100644 index 000000000..e86803bed --- /dev/null +++ b/code/classids.h @@ -0,0 +1,85 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#pragma once + +#include "classid.h" + +// The identifiers of every class a saved game or a Locomotor= key can name. +extern ClassID const ClassID_HouseClass; +extern ClassID const ClassID_SuperWeaponTypeClass; +extern ClassID const ClassID_SuperWeaponClass; +extern ClassID const ClassID_UnitTypeClass; +extern ClassID const ClassID_InfantryTypeClass; +extern ClassID const ClassID_AircraftTypeClass; +extern ClassID const ClassID_BuildingTypeClass; +extern ClassID const ClassID_BulletTypeClass; +extern ClassID const ClassID_TerrainTypeClass; +extern ClassID const ClassID_IsometricTileTypeClass; +extern ClassID const ClassID_OverlayTypeClass; +extern ClassID const ClassID_SmudgeTypeClass; +extern ClassID const ClassID_AnimTypeClass; +extern ClassID const ClassID_HouseTypeClass; +extern ClassID const ClassID_IsometricTileClass; +extern ClassID const ClassID_VoxelAnimClass; +extern ClassID const ClassID_AircraftClass; +extern ClassID const ClassID_AnimClass; +extern ClassID const ClassID_InfantryClass; +extern ClassID const ClassID_SmudgeClass; +extern ClassID const ClassID_BuildingClass; +extern ClassID const ClassID_OverlayClass; +extern ClassID const ClassID_ParticleSystemClass; +extern ClassID const ClassID_ParticleSystemTypeClass; +extern ClassID const ClassID_BulletClass; +extern ClassID const ClassID_UnitClass; +extern ClassID const ClassID_ParticleClass; +extern ClassID const ClassID_ParticleTypeClass; +extern ClassID const ClassID_WaveClass; +extern ClassID const ClassID_BuildingLightClass; +extern ClassID const ClassID_TerrainClass; +extern ClassID const ClassID_TubeClass; +extern ClassID const ClassID_TeamClass; +extern ClassID const ClassID_TaskForceClass; +extern ClassID const ClassID_TeamTypeClass; +extern ClassID const ClassID_VoxelAnimTypeClass; +extern ClassID const ClassID_ScriptClass; +extern ClassID const ClassID_ScriptTypeClass; +extern ClassID const ClassID_TagClass; +extern ClassID const ClassID_TagTypeClass; +extern ClassID const ClassID_TriggerClass; +extern ClassID const ClassID_TriggerTypeClass; +extern ClassID const ClassID_ActionClass; +extern ClassID const ClassID_EventClass; +extern ClassID const ClassID_FactoryClass; +extern ClassID const ClassID_WeaponTypeClass; +extern ClassID const ClassID_WarheadTypeClass; +extern ClassID const ClassID_WaypointPath; +extern ClassID const ClassID_LightSource; +extern ClassID const ClassID_CampaignClass; +extern ClassID const ClassID_SideClass; +extern ClassID const ClassID_TiberiumClass; +extern ClassID const ClassID_CellClass; +extern ClassID const ClassID_EMPulseClass; +extern ClassID const ClassID_TacticalMapClass; +extern ClassID const ClassID_AITriggerTypeClass; +extern ClassID const ClassID_AITriggerClass; +extern ClassID const ClassID_NeuronClass; +extern ClassID const ClassID_FoggedObjectClass; +extern ClassID const ClassID_AlphaShapeClass; +extern ClassID const ClassID_VeinholeMonsterClass; +extern ClassID const ClassID_DriveLocomotion; +extern ClassID const ClassID_HoverLocomotion; +extern ClassID const ClassID_TunnelLocomotion; +extern ClassID const ClassID_WalkLocomotion; +extern ClassID const ClassID_BallisticLocomotion; +extern ClassID const ClassID_FlyerLocomotion; +extern ClassID const ClassID_TeleportLocomotion; +extern ClassID const ClassID_MechLocomotion; +extern ClassID const ClassID_JumpjetLocomotion; +extern ClassID const ClassID_LevitateLocomotion; diff --git a/code/connect.h b/code/connect.h index c4edbcb6e..6f76829cb 100644 --- a/code/connect.h +++ b/code/connect.h @@ -99,6 +99,9 @@ #include "combuf.h" #include "netadmit.h" +#include +#include + /* ********************************** Defines ********************************** */ @@ -120,6 +123,9 @@ struct CommHeaderType { }; #pragma pack(pop) +static_assert(sizeof(CommHeaderType) == 7, "Packet header layout changed"); +static_assert(offsetof(CommHeaderType, PacketID) == 3, "Packet header layout changed"); + /* ***************************** Class Declaration ***************************** */ diff --git a/code/conquer.cpp b/code/conquer.cpp index 5772abb79..2c3f6fc7d 100644 --- a/code/conquer.cpp +++ b/code/conquer.cpp @@ -62,6 +62,9 @@ * Is_Aftermath_Installed -- Function to determine the availability of the AM expansion. * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ +#include +#include + #include "always.h" #include "conquer.h" @@ -110,6 +113,7 @@ #include "savemgr.h" #include "scenario.h" #include "session.h" +#include "ui/uishell.h" #include "sidebar.h" #include "sounddlg.h" #include "stats.h" @@ -131,7 +135,9 @@ #include #include #include +#ifdef _WIN32 #include +#endif #include #include @@ -353,6 +359,7 @@ void Main_Game(int argc, char * argv[]) int ret = Init_Game(argc, argv); if (ret) { if (ret < 0) { +#ifdef _WIN32 MSGBOXPARAMS params; params.cbSize = sizeof(MSGBOXPARAMS); params.hwndOwner = MainWindow; @@ -365,6 +372,10 @@ void Main_Game(int argc, char * argv[]) params.lpfnMsgBoxCallback = NULL; params.dwLanguageId = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT); MessageBoxIndirect(¶ms); +#else + // No host message box yet, so the failure is reported where the launch happened. + fprintf(stderr, "%s: %s\n", Fetch_String(TXT_SHORT_TITLE), Fetch_String(TXT_INITGAME_FAILED)); +#endif } return; } @@ -528,6 +539,13 @@ void Main_Game(int argc, char * argv[]) *=============================================================================================*/ void Call_Back(void) { + /* + * Overlay maintenance. This and Main_Loop are the shell's service points, so a screen + * outside a game -- a menu, a dialog driver, a loading wait -- keeps its documents + * laid out and animating without a loop of its own. + */ + UI_Tick(); + /* ** Music and speech maintenance */ @@ -1201,25 +1219,26 @@ TechnoTypeClass const * Fetch_Techno_Type(RTTIType type, int id) *=========================================================================*/ unsigned int Disk_Space_Available(void) { - ULARGE_INTEGER freebytecount; // Free bytes on disk available to caller (caller may not have access to entire disk). - DebugString("Checking available disk space\n"); /* * Measured where the game's saved games will actually go, which is not the current * directory once a player has one of their own. */ - std::string const user_directory = User_File_Write_Name(""); - LPCTSTR const disk = user_directory.empty() ? NULL : user_directory.c_str(); + std::string user_directory = User_File_Write_Name(""); + if (user_directory.empty()) { + user_directory = "."; + } - if (!GetDiskFreeSpaceEx(disk, &freebytecount, NULL, NULL)) { - DWORD const error = GetLastError(); - DebugString("GetDiskFreeSpaceEx failed with error code %d - %s\n", error, Last_Error_Text(error)); + std::error_code error; + std::filesystem::space_info const space = std::filesystem::space(user_directory, error); + if (error) { + DebugString("Free disk space unreadable - %s\n", error.message().c_str()); return(0); } // The kilobyte count saturates rather than wrapping. - unsigned int const diskspace = (unsigned int)std::min(freebytecount.QuadPart / 1024, UINT_MAX); + unsigned int const diskspace = (unsigned int)std::min((unsigned long long)space.available / 1024, UINT_MAX); DebugString("Free disk space is %u Mb\n", diskspace / 1024); return(diskspace); } diff --git a/code/cstream.cpp b/code/cstream.cpp deleted file mode 100644 index 65061d9da..000000000 --- a/code/cstream.cpp +++ /dev/null @@ -1,540 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2026 OpenTS contributors - * - * See LICENSE.md for applicable additional terms and warranty disclaimers. - ******************************************************************************/ - -#include "always.h" - -#include "cstream.h" - -#include - -extern ULONG COMRefCount; - -/// -/// Creates a compressing stream object. -/// This routine prepares the working buffers that the LZO codec needs. The object starts -/// out with no storage stream of its own to compress through. -/// -/// Call Link_Stream to attach a storage stream before reading or writing. -CStreamClass::CStreamClass(void) : - StreamPtr(NULL), - RefCount(0), - IsReading(false), - IsWriting(false), - CurOffset(0), - DataBuffer(new unsigned char[BUFFER_SIZE]), - StreamBuffer(new unsigned char[STREAM_BUFFER_SIZE]), - LZODictionary(new unsigned char[LZO1X_1_MEM_COMPRESS]) -{ - BlockHead.CompSize = BUFFER_SIZE - 1; -} - - -/// -/// Destroys the compressing stream object. -/// Any storage stream still attached is unlinked first, so that whatever is left in the -/// work buffer is compressed out rather than lost. -/// -CStreamClass::~CStreamClass(void) -{ - IUnknown **unk = NULL; - if (StreamPtr) { - Unlink_Stream(unk); - } - - delete [] LZODictionary; - LZODictionary = NULL; - delete [] DataBuffer; - DataBuffer = NULL; - delete [] StreamBuffer; - StreamBuffer = NULL; -} - - -/// -/// Takes out a reference on this stream object. -/// -/// Returns with the new reference count. -ULONG CStreamClass::AddRef(void) -{ - COMRefCount++; - return(InterlockedIncrement(&RefCount)); -} - - -/// -/// Releases a reference to this stream object. -/// This routine will destroy the object once the last outstanding reference has been -/// given up. -/// -/// Returns with the number of references that remain. -ULONG CStreamClass::Release(void) -{ - COMRefCount--; - ULONG i = InterlockedDecrement(&RefCount); - - if (i == 0) { - delete this; - } - - return(i); -} - - -/// -/// Fetches an alternate interface to this stream object. -/// The IUnknown, IStream and ILinkStream interfaces are the ones supported. A successful -/// query takes out a reference on this object for the caller. -/// -/// The identifier of the interface being asked for. -/// Pointer to the interface pointer to fill in. -/// Returns with S_OK, or E_NOINTERFACE if the interface is not supported. -LONG CStreamClass::QueryInterface(REFIID riid, LPVOID * ppvObject) -{ - if (ppvObject == NULL) { - return(E_POINTER); - } - - *ppvObject = NULL; - if (riid == IID_IUnknown) { - *ppvObject = this; - } - if (riid == IID_IStream) { - *ppvObject = (IStream *)this; - } - if (riid == IID_ILinkStream) { - *ppvObject = (ILinkStream *)this; - } - if (*ppvObject == NULL) { - return(E_NOINTERFACE); - } - //reinterpret_cast(*ppvObject)->AddRef(); - this->AddRef(); - return(S_OK); -} - - -/// -/// Attaches the storage stream this object compresses through. -/// Use this routine to bind the compressor to the real stream that the compressed blocks -/// will be written to or read back from. Only one stream may be attached at a time. -/// -/// Pointer to the object to fetch the storage stream from. -/// Returns with S_OK, or E_FAIL if a stream is already attached. -HRESULT CStreamClass::Link_Stream(IUnknown *stream) -{ - if (stream == NULL) { - return(E_POINTER); - } - - if (StreamPtr != NULL) { - return(E_FAIL); - } - - HRESULT hr = stream->QueryInterface(__uuidof(IStream), (void **)&stream); - if (FAILED(hr)) { - /// &StreamPtr; - StreamPtr.Attach(NULL, false); - } else { - StreamPtr.Attach((IStream *)stream, false); - } - - if (FAILED(hr) && (hr != E_NOINTERFACE)) { - _com_issue_error(hr); - } - - return(S_OK); -} - - -/// -/// Detaches the underlying storage stream from this object. -/// Anything still sitting in the work buffer is compressed out first and the storage -/// stream is committed, so the caller gets back a complete stream. -/// -/// Pointer to fill in with the released stream, or NULL if the caller -/// does not want it. -/// Returns with S_OK, or E_FAIL if no stream was attached. A commit failure is -/// returned as it stands. -HRESULT CStreamClass::Unlink_Stream(IUnknown **stream) -{ - Compress(); - - if (StreamPtr == NULL) { - return(E_FAIL); - } - - if (stream != NULL) { - StreamPtr->AddRef(); - *stream = StreamPtr; - } - - HRESULT hr = StreamPtr->Commit(0); - if (SUCCEEDED(hr)) { - StreamPtr.Release(); - } else { - return(hr); - } - - return(S_OK); -} - - -/// -/// Reads and decompresses data from the underlying stream. -/// This routine pulls whole compressed blocks out of the storage stream and doles out -/// pieces of the decompressed result until the caller's request has been satisfied. -/// -/// Pointer to the buffer to fill with the data read. -/// The number of bytes to read. -/// Pointer to fill in with the number of bytes read, or NULL if the -/// count is not wanted. -/// Returns with S_OK, or an error code if the data could not be read. -/// A stream that is being written cannot also be read. -HRESULT CStreamClass::Read(void *pv, ULONG cb, ULONG *pcbRead) -{ - int read_size; - int left; - - read_size = cb; - left = cb; - - if (pv == NULL) { - return(E_POINTER); - } - - if (read_size < 0) { - return(E_INVALIDARG); - } - - if (StreamPtr == NULL) { - return(E_FAIL); - } - - if (IsWriting) { - return(E_FAIL); - } - - IsReading = true; - - if (pcbRead != NULL) { - *pcbRead = 0; - } - - while (left > 0) { - - int offset = CurOffset; - if (offset > 0) { - int len = left; - if (left >= offset) { - len = CurOffset; - } - memmove(pv, (char *)DataBuffer + BlockHead.UncompSize - offset, len); - pv = (char *)pv + len; - left -= len; - CurOffset -= len; - } - - if (left == 0) { - break; - } - - ULONG read = 0; - - HRESULT hr = StreamPtr->Read(&BlockHead, sizeof(BlockHead), &read); - if (FAILED(hr)) { - return(hr); - } - - if (read != sizeof(BlockHead)) { - return(E_FAIL); - } - - if (BlockHead.CompSize > STREAM_BUFFER_SIZE) { - return(E_FAIL); - } - - hr = StreamPtr->Read(StreamBuffer, BlockHead.CompSize, &read); - if (FAILED(hr)) { - return(hr); - } - - unsigned int inlen = BlockHead.CompSize; - if (read != inlen) { - return(E_FAIL); - } - lzo_byte *out = (lzo_byte *)DataBuffer; - lzo_byte *in = (lzo_byte *)StreamBuffer; - lzo_uint out_len = BUFFER_SIZE; - if (lzo1x_decompress_safe(in, inlen, out, &out_len, NULL) != LZO_E_OK) { - return(E_FAIL); - } - // Compress records the whole buffer size rather than the block's own length, so only - // the decompressor's count says how much of the buffer is real. - BlockHead.UncompSize = out_len; - CurOffset = out_len; - } - - if (pcbRead != NULL) { - *pcbRead = read_size; - } - - return(S_OK); -} - - -/// -/// Compresses data out to the underlying stream. -/// This routine gathers the caller's data into a work buffer and hands it to the -/// compressor a block at a time, so what reaches the storage stream is a run of -/// compressed blocks rather than the raw bytes. -/// -/// Pointer to the data to write. -/// The number of bytes to write. -/// Pointer to fill in with the number of bytes accepted, or NULL -/// if the count is not wanted. -/// Returns with S_OK, or an error code if the data could not be written. -/// A stream that is being read cannot also be written. The trailing partial -/// block does not reach the storage stream until the object is flushed or unlinked. -HRESULT CStreamClass::Write(const void *pv, ULONG cb, ULONG *pcbWritten) -{ - unsigned char *ptr; - int write_size; - int left; - int result; - int temp_size; - - ptr = (unsigned char *)pv; - write_size = cb; - left = cb; - - if (pv == NULL) { - return(E_POINTER); - } - - if (write_size < 0) { - return(E_INVALIDARG); - } - - if (StreamPtr == NULL) { - return(E_FAIL); - } - - if (IsReading) { - return(E_FAIL); - } - - IsWriting = true; - - if (cb != 0) { - if (pcbWritten != NULL) { - *pcbWritten = 0; - } - - if (CurOffset > 0) { - if (write_size >= BUFFER_SIZE - CurOffset) { - write_size = BUFFER_SIZE - CurOffset; - } - - memmove((unsigned char *)DataBuffer + CurOffset, ptr, write_size); - temp_size = write_size + CurOffset; - - ptr += write_size; - left -= write_size; - CurOffset = temp_size; - - if (CurOffset == BUFFER_SIZE) { - result = Compress(DataBuffer, CurOffset); - if (result < 0) { - return(result); - } - CurOffset = 0; - } - - write_size = cb; - } - while (left >= BUFFER_SIZE) { - result = Compress(ptr, BUFFER_SIZE); - if (result < 0) { - return(result); - } - left -= BUFFER_SIZE; - ptr += BUFFER_SIZE; - } - - if (left > 0) { - memmove((unsigned char *)DataBuffer, ptr, left); - write_size = cb; - CurOffset = left; - } - - if (pcbWritten) { - *pcbWritten = write_size; - } - } - - return(S_OK); -} - - -/// -/// Moves the file pointer of the underlying stream. -/// -/// Returns with S_OK, or E_FAIL if a transfer is already under way. -/// Seeking is refused once reading or writing has begun, since the compressor -/// keeps state that a seek would invalidate. -HRESULT CStreamClass::Seek(LARGE_INTEGER dlibMove, DWORD dwOrigin, ULARGE_INTEGER *plibNewPosition) -{ - if (IsReading || IsWriting) { - return(E_FAIL); - } - - return(StreamPtr->Seek(dlibMove, dwOrigin, plibNewPosition)); -} - - -/// -/// Sets the size of the underlying stream. -/// -/// Returns with S_OK, or E_FAIL if a transfer is already under way. -/// Resizing is refused once reading or writing has begun. -HRESULT CStreamClass::SetSize(ULARGE_INTEGER libNewSize) -{ - if (IsReading || IsWriting) { - return(E_FAIL); - } - - return(StreamPtr->SetSize(libNewSize)); -} - - -/// -/// Copies data from this stream over to another stream. -/// The request is handed straight to the underlying stream, so it is the compressed -/// bytes that get copied rather than the data they stand for. -/// -/// Returns with the result of the underlying stream's copy request. -HRESULT CStreamClass::CopyTo(IStream *pstm, ULARGE_INTEGER cb, ULARGE_INTEGER *pcbRead, ULARGE_INTEGER *pcbWritten) -{ - return(StreamPtr->CopyTo(pstm, cb, pcbRead, pcbWritten)); -} - - -/// -/// Commits any pending changes to the underlying stream. -/// -/// Returns with the result of the underlying stream's commit request. -HRESULT CStreamClass::Commit(DWORD grfCommitFlags) -{ - return(StreamPtr->Commit(grfCommitFlags)); -} - -/// -/// Discards any uncommitted changes to the stream. -/// -/// Returns with the result of the underlying stream's revert request. -HRESULT CStreamClass::Revert(void) -{ - return(StreamPtr->Revert()); -} - - -/// -/// Locks a byte range of the underlying stream. -/// -/// Returns with the result of the underlying stream's lock request. -HRESULT CStreamClass::LockRegion(ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, DWORD dwLockType) -{ - return(StreamPtr->LockRegion(libOffset, cb, dwLockType)); -} - - -/// -/// Releases a lock on a byte range of the underlying stream. -/// -/// Returns with the result of the underlying stream's unlock request. -HRESULT CStreamClass::UnlockRegion(ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, DWORD dwLockType) -{ - return(StreamPtr->UnlockRegion(libOffset, cb, dwLockType)); -} - - -/// -/// Fetches the statistics of the underlying stream. -/// -/// Returns with the result of the underlying stream's stat request. -HRESULT CStreamClass::Stat(STATSTG *pstatstg, DWORD grfStatFlag) -{ - return(StreamPtr->Stat(pstatstg, grfStatFlag)); -} - - -/// -/// Creates a second stream object over the same storage. -/// The clone is made by the underlying stream, so it is a plain stream rather than a -/// compressing one. -/// -/// Returns with the result of the underlying stream's clone request. -HRESULT CStreamClass::Clone(IStream **ppstm) -{ - return(StreamPtr->Clone(ppstm)); -} - - -/// -/// Compresses a buffer out as a single stream block. -/// This is the low level routine that runs the buffer through the LZO compressor and -/// writes the block header and the compressed bytes to the underlying stream. -/// -/// Pointer to the data to compress. -/// The number of bytes to compress. -/// Returns with S_OK, or an error code if the block could not be written. -HRESULT CStreamClass::Compress(void *in_buffer, ULONG length) -{ - HRESULT hr; - lzo_uint out_len = length; - lzo1x_1_compress((lzo_byte *)in_buffer, length, (lzo_byte *)StreamBuffer, &out_len, (lzo_byte *)LZODictionary); - BlockHead.UncompSize = BUFFER_SIZE; - length = 0; - BlockHead.CompSize = out_len; - - hr = StreamPtr->Write(&BlockHead, sizeof(BlockHead), &length); - - if (SUCCEEDED(hr)) { - if (length != sizeof(BlockHead)) { - return(E_FAIL); - } - - hr = StreamPtr->Write(StreamBuffer, out_len, &length); - if (SUCCEEDED(hr)) { - hr = length != out_len ? (unsigned int)E_FAIL : 0; - } - } - - return(hr); -} - - -/// -/// Flushes any buffered data out as a compressed block. -/// Use this routine to make sure the tail end of a write actually reaches the stream. -/// It does nothing if there is nothing left over to flush. -/// -/// Returns with S_OK, or an error code if the block could not be written. -HRESULT CStreamClass::Compress(void) -{ - if (IsWriting && CurOffset > 0) { - if (StreamPtr == NULL) { - return(E_FAIL); - } - if (CurOffset > 0) { - return(Compress(DataBuffer, CurOffset)); - } - } - return(S_OK); -} diff --git a/code/cstream.h b/code/cstream.h deleted file mode 100644 index e344687e9..000000000 --- a/code/cstream.h +++ /dev/null @@ -1,120 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2026 OpenTS contributors - * - * See LICENSE.md for applicable additional terms and warranty disclaimers. - ******************************************************************************/ - -#pragma once - -#include "ilinkstm.h" - -#include -#include - -class CStreamClass : public IStream, public ILinkStream -{ - public: - virtual LONG STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID * ppvObject) override; - virtual ULONG STDMETHODCALLTYPE AddRef(void) override; - virtual ULONG STDMETHODCALLTYPE Release(void) override; - - virtual HRESULT STDMETHODCALLTYPE Read(void *pv, ULONG cb, ULONG *pcbRead) override; - virtual HRESULT STDMETHODCALLTYPE Write(const void *pv, ULONG cb, ULONG *pcbWritten) override; - - virtual HRESULT STDMETHODCALLTYPE Seek(LARGE_INTEGER dlibMove, DWORD dwOrigin, ULARGE_INTEGER *plibNewPosition) override; - virtual HRESULT STDMETHODCALLTYPE SetSize(ULARGE_INTEGER libNewSize) override; - virtual HRESULT STDMETHODCALLTYPE CopyTo(IStream *pstm, ULARGE_INTEGER cb, ULARGE_INTEGER *pcbRead, ULARGE_INTEGER *pcbWritten) override; - virtual HRESULT STDMETHODCALLTYPE Commit(DWORD grfCommitFlags) override; - virtual HRESULT STDMETHODCALLTYPE Revert() override; - virtual HRESULT STDMETHODCALLTYPE LockRegion(ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, DWORD dwLockType) override; - virtual HRESULT STDMETHODCALLTYPE UnlockRegion(ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, DWORD dwLockType) override; - virtual HRESULT STDMETHODCALLTYPE Stat(STATSTG *pstatstg, DWORD grfStatFlag) override; - virtual HRESULT STDMETHODCALLTYPE Clone(IStream **ppstm) override; - - virtual HRESULT STDMETHODCALLTYPE Link_Stream(IUnknown *stream) override; - virtual HRESULT STDMETHODCALLTYPE Unlink_Stream(IUnknown **stream) override; - - public: - CStreamClass(void); - virtual ~CStreamClass(void); - - HRESULT Compress(void *in_buffer, ULONG length); - HRESULT Compress(void); - - enum { - BUFFER_SIZE = 64*1024, - - /* - * LZO1X-1 can expand a block rather than shrink it, so the compressed side is - * sized for its worst case. - */ - STREAM_BUFFER_SIZE = BUFFER_SIZE + BUFFER_SIZE/16 + 64 + 3, - }; - - private: - /* - * This points to the stream the compressed data actually travels over. Nothing can be - * read or written until one has been linked in, and the link is broken again once the - * stream has been committed. - */ - IStreamPtr StreamPtr; - - /* - * This is the COM reference count for this object. The stream destroys itself once - * the last reference to it has been released. - */ - LONG RefCount; - - /* - * These flags record which direction the stream has been committed to. The first read - * or write sets one of them, and from that point on the opposite operation is - * refused, as are seeking and resizing. - */ - bool IsReading; - bool IsWriting; - - /* - * This is how much of the data buffer is currently in play, expressed in bytes. While - * reading it counts down the part of the decompressed block not yet handed out; while - * writing it counts up the bytes waiting to be compressed. - */ - int CurOffset; - - /* - * This is the working buffer that holds the data in its uncompressed form, one - * block's worth at a time. - */ - void *DataBuffer; - - /* - * This is the working buffer that holds a block in its compressed form, on its way to - * or from the linked stream. - */ - void *StreamBuffer; - - /* - * This is the scratch memory the LZO compressor keeps its dictionary in. It is of no - * interest outside the compression call itself. - */ - void *LZODictionary; - - /* - * This is the header of the block currently being read or written. Every block on the - * stream is preceded by one, so the reader knows how much compressed data to pull in - * and how far it will expand. - */ - struct BlockHeader { - /* - * This is the number of bytes the block occupies on the stream, compressed. - */ - unsigned int CompSize; - - /* - * This is the number of bytes the block expands to once decompressed. - */ - unsigned int UncompSize; - } BlockHead; -}; diff --git a/code/data.cpp b/code/data.cpp index 8c6db02b9..f58bef195 100644 --- a/code/data.cpp +++ b/code/data.cpp @@ -39,7 +39,7 @@ #include "utf8.h" -#include +#include HINSTANCE LanguageResources; diff --git a/code/dbgprint.cpp b/code/dbgprint.cpp index 41bcbb097..c629d2cc9 100644 --- a/code/dbgprint.cpp +++ b/code/dbgprint.cpp @@ -262,13 +262,13 @@ static void Init_Locked(void) Delete_Files_Older_Than(DebugDirectory, "DEBUG_*.LOG", DEBUG_LOG_MAX_AGE_DAYS); - snprintf(DebugFileName, sizeof(DebugFileName), "%s\\DEBUG_%s.LOG", DebugDirectory, timestamp); + snprintf(DebugFileName, sizeof(DebugFileName), "%s/DEBUG_%s.LOG", DebugDirectory, timestamp); DebugFile = CreateFile(DebugFileName, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL); // A second process started in the same second must not disturb the first one's log. if (DebugFile == INVALID_HANDLE_VALUE) { - snprintf(DebugFileName, sizeof(DebugFileName), "%s\\DEBUG_%s_%lu.LOG", + snprintf(DebugFileName, sizeof(DebugFileName), "%s/DEBUG_%s_%lu.LOG", DebugDirectory, timestamp, GetCurrentProcessId()); DebugFile = CreateFile(DebugFileName, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL); diff --git a/code/dbgprint.h b/code/dbgprint.h index e24a002d2..8851907fb 100644 --- a/code/dbgprint.h +++ b/code/dbgprint.h @@ -15,7 +15,11 @@ #if defined(_WIN32) +#ifdef _WIN32 #include +#else +#define _Printf_format_string_ +#endif #else diff --git a/code/deploymentconfig.cpp b/code/deploymentconfig.cpp index 614bb5af5..4f8caf97d 100644 --- a/code/deploymentconfig.cpp +++ b/code/deploymentconfig.cpp @@ -20,7 +20,7 @@ static char const * const ConfigName = "OPENTS.INI"; /* * The folders the file itself is looked for in, relative to the data directory. */ -static char const * const ConfigProbes[] = {"", "INI\\", "MIX\\"}; +static char const * const ConfigProbes[] = {"", "INI/", "MIX/"}; void DeploymentConfigClass::Read_INI(INIClass const & ini) diff --git a/code/desyncdlg.cpp b/code/desyncdlg.cpp index e35413d10..50d689df0 100644 --- a/code/desyncdlg.cpp +++ b/code/desyncdlg.cpp @@ -7,6 +7,7 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ +#include "hostclock.h" #include "always.h" #include "desyncdlg.h" @@ -29,44 +30,22 @@ #include "mpload.h" #include "netdlg.h" #include "netglobal.h" -#include "ownrdraw.h" #include "savemgr.h" #include "session.h" -#include "srfcache.h" +#include "ui/uishell.h" #include "syncreport.h" #include "win.h" -#include "windlg.h" #include "winfix.h" -#include #include #include #include -namespace { - - // Column positions within the player list, in the units the lobby's lists use. - constexpr int HOST_COLUMN_X = 2; - constexpr int NAME_COLUMN_X = 20; - constexpr int STATUS_COLUMN_WIDTH = 56; - constexpr int CHAT_BACKLOG_MAX = 50; - - - // The dialog's pixel size follows the presentation layout, so the column is measured. - int Status_Column_X(HWND list) - { - RECT rect = {}; - GetClientRect(list, &rect); - return(rect.right - STATUS_COLUMN_WIDTH); - } - -} // namespace - /// -/// Shows the dialog and pumps it until the master has decided, or this player has quit. Game +/// Shows the screen and runs it until the master has decided, or this player has quit. Game /// logic is halted for the duration; chat, sign-offs, heartbeats and the master's decision /// still come through, since the network is serviced the whole time. /// @@ -74,70 +53,27 @@ DesyncDialogClass::OutcomeType DesyncDialogClass::Run(void) { DebugString("Out-of-sync dialog opening on frame %d\n", Frame); - // A raised suspension makes a nested dialog's pump service the network instead of the game. + // A raised suspension makes the runner's pump service the network instead of the game. TacticalActive = false; Session.Suspended++; - Decision = 0; - ContinueReceived = false; - CountdownActive = false; - QuitEnabled = false; - LastCountdownSecond = -1; - ChatBacklog.clear(); - OpenedAt = Monotonic_Milliseconds(); - State.Begin(OpenedAt); - - Create_Dialog(); + IsRunning = true; + Screen.Open(); + UI_Set_Desync_Screen(&Screen); OutcomeType outcome = OutcomeType::Continue; - if (Window == NULL) { - DebugString("The out-of-sync dialog could not be created; continuing\n"); - } else { - while (true) { - Call_Back(); - - if (Decision == IDC_DESYNC_QUIT) { - outcome = OutcomeType::Quit; - break; - } - - std::int64_t now = Monotonic_Milliseconds(); - if (!IsHostDialog && !QuitEnabled && now - OpenedAt >= DesyncClass::QUIT_DELAY_MS) { - QuitEnabled = true; - EnableWindow(GetDlgItem(Window, IDC_DESYNC_QUIT), TRUE); - } - - if (!CountdownActive && SaveManager.MultiplayerLoad.Is_Pending()) { - Start_Countdown(); - } - - if (CountdownActive) { - Update_Countdown_Text(); - InvalidateRect(Window, NULL, FALSE); - if (SaveManager.MultiplayerLoad.Is_Due(now)) { - outcome = OutcomeType::Load; - break; - } - } else if (ContinueReceived || Decision == IDC_DESYNC_CONTINUE) { - if (Decision == IDC_DESYNC_CONTINUE) { - Send_Continue(); - } - outcome = OutcomeType::Continue; - break; - } else if (Decision == IDC_DESYNC_LOAD) { - EnableWindow(Window, FALSE); - SaveManager.Multiplayer_Load_Prompt(); - EnableWindow(Window, TRUE); - SetFocus(GetDlgItem(Window, IDC_DESYNC_PLAYER_LIST)); - } - - Decision = 0; - Sleep(10); - } + UI_Desync_Run(Screen); + UI_Desync_Close_View(); + + switch (Screen.Outcome) { + case UIDesyncPresenterClass::OUTCOME_LOAD: outcome = OutcomeType::Load; break; + case UIDesyncPresenterClass::OUTCOME_QUIT: outcome = OutcomeType::Quit; break; + default: outcome = OutcomeType::Continue; break; } - Destroy_Dialog(); + UI_Set_Desync_Screen(NULL); + IsRunning = false; Session.Suspended--; TacticalActive = true; @@ -150,16 +86,8 @@ DesyncDialogClass::OutcomeType DesyncDialogClass::Run(void) void DesyncDialogClass::Service(void) { - if (!Is_Active()) { - return; - } - - std::int64_t now = Monotonic_Milliseconds(); - if (State.Heartbeat_Is_Due(now)) { - Send_Heartbeat(); - State.Heartbeat_Sent(now); - } - Check_Timeouts(); + // The runner services the screen itself, so the network maintenance has nothing of its + // own to do here while the screen is up. } @@ -169,9 +97,7 @@ void DesyncDialogClass::Notify_Chat(char const * name, char const * text) return; } - char buffer[MAX_MESSAGE_LENGTH + MAX_MESSAGE_PREFIX]; - std::snprintf(buffer, sizeof(buffer), "%s: %s", name, text); - Append_Chat_Line(buffer); + Screen.Record_Chat(name, text); } @@ -181,16 +107,7 @@ void DesyncDialogClass::Notify_Player_Left(int house, char const * name) return; } - State.Mark_Left(house, name); - - if (name != NULL && name[0] != '\0') { - char buffer[128]; - std::snprintf(buffer, sizeof(buffer), Fetch_String(TXT_LEFT_GAME), name); - Append_Chat_Line(buffer); - } - - Update_Player_List(); - Become_Host_If_Promoted(); + Screen.Player_Left(house, name); } @@ -200,15 +117,14 @@ void DesyncDialogClass::Notify_Continue(void) return; } - DebugString("The master chose to continue without the players out of sync\n"); - ContinueReceived = true; + Screen.Master_Decided_To_Continue(); } void DesyncDialogClass::Notify_Heartbeat(int house) { if (Is_Active()) { - State.Heard(house, Monotonic_Milliseconds()); + Screen.Heartbeat_Heard(house); } } @@ -219,494 +135,5 @@ void DesyncDialogClass::Notify_Master_Changed(void) return; } - Update_Player_List(); - Become_Host_If_Promoted(); -} - - -/// -/// Creates the variant the local player gets: the decision dialog for the master, the wait -/// dialog for everyone else. -/// -void DesyncDialogClass::Create_Dialog(void) -{ - IsHostDialog = Session.Am_I_Master(); - int const id = IsHostDialog ? IDD_DESYNC_HOST : IDD_DESYNC_WAIT; - - Window = WS_Create_Dialog(ProgramInstance, id, MainWindow, Dialog_Proc, FALSE); - if (Window == NULL) { - return; - } - - Fit_To_Screen(); - Center_Window_Within_Window(Window); - - RECT placed; - GetWindowRect(Window, &placed); - MapWindowPoints(HWND_DESKTOP, MainWindow, (POINT *)&placed, 2); - DebugString("Out-of-sync dialog placed at %d,%d size %dx%d in a %dx%d view\n", - placed.left, placed.top, placed.right - placed.left, placed.bottom - placed.top, VideoModeWidth, VideoModeHeight); - - // The name column goes first: the list draws each row's own string in the first column added. - HWND list = GetDlgItem(Window, IDC_DESYNC_PLAYER_LIST); - if (list != NULL) { - int const status_x = Status_Column_X(list); - SendMessage(list, OD_ADDCOLUMN, status_x - NAME_COLUMN_X - 6, NAME_COLUMN_X); - SendMessage(list, OD_ADDCOLUMN, 0, HOST_COLUMN_X); - SendMessage(list, OD_ADDCOLUMN, 0, status_x); - } - Update_Player_List(); - - if (IsHostDialog) { - bool const can_load = SaveManager.Multiplayer_Load_Is_Allowed() && MultiplayerLoadOptionsClass().Files_Present(); - EnableWindow(GetDlgItem(Window, IDC_DESYNC_LOAD), can_load && !CountdownActive); - EnableWindow(GetDlgItem(Window, IDC_DESYNC_CONTINUE), !CountdownActive); - } else { - EnableWindow(GetDlgItem(Window, IDC_DESYNC_QUIT), QuitEnabled); - } - - Refill_Chat_List(); - - HWND edit = GetDlgItem(Window, IDC_DESYNC_CHAT_EDIT); - if (edit != NULL) { - SetWindowText(edit, Fetch_String(TXT_CHAT_HINT)); - ChatPlaceholderActive = true; - } - - if (CountdownActive) { - ShowWindow(GetDlgItem(Window, IDC_DESYNC_COUNTDOWN_TEXT), SW_SHOW); - ShowWindow(GetDlgItem(Window, IDC_DESYNC_COUNTDOWN_BAR), SW_SHOW); - Update_Countdown_Text(); - } - - MouseCursor->Hide_Mouse(); - ShowWindow(Window, SW_SHOWNORMAL); - UpdateWindow(Window); - MouseCursor->Show_Mouse(); - - // The player list takes the focus, or the dialog would hand it to the chat box and clear the hint. - SetForegroundWindow(Window); - SetFocus(GetDlgItem(Window, IDC_DESYNC_PLAYER_LIST)); -} - - -void DesyncDialogClass::Destroy_Dialog(void) -{ - if (Window != NULL) { - WS_Destroy_Dialog(Window, 0); - Window = NULL; - } -} - - -/// -/// Takes the excess height out of the chat list when the presented dialog is taller than the -/// screen, and moves everything below the list up by the same amount. -/// -void DesyncDialogClass::Fit_To_Screen(void) -{ - RECT dialog_rect; - GetWindowRect(Window, &dialog_rect); - int const dialog_height = dialog_rect.bottom - dialog_rect.top; - if (dialog_height <= VideoModeHeight) { - return; - } - - HWND chat = GetDlgItem(Window, IDC_DESYNC_CHAT_LIST); - if (chat == NULL) { - return; - } - RECT chat_rect; - GetWindowRect(chat, &chat_rect); - int const chat_height = chat_rect.bottom - chat_rect.top; - - int const delta = std::min(dialog_height - VideoModeHeight, chat_height * 2 / 3); - SetWindowPos(chat, NULL, 0, 0, chat_rect.right - chat_rect.left, chat_height - delta, - SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); - - for (int id : {IDC_DESYNC_CHAT_EDIT, IDC_DESYNC_COUNTDOWN_TEXT, IDC_DESYNC_COUNTDOWN_BAR, - IDC_DESYNC_LOAD, IDC_DESYNC_CONTINUE, IDC_DESYNC_QUIT}) { - HWND control = GetDlgItem(Window, id); - if (control != NULL) { - RECT rect; - GetWindowRect(control, &rect); - MapWindowPoints(HWND_DESKTOP, Window, (POINT *)&rect, 1); - SetWindowPos(control, NULL, rect.left, rect.top - delta, 0, 0, - SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); - } - } - - SetWindowPos(Window, NULL, 0, 0, dialog_rect.right - dialog_rect.left, dialog_height - delta, - SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); -} - - -/// -/// Replaces the wait dialog with the decision dialog once this machine has become master, -/// unless a load is already counting down, when there is nothing left to decide. -/// -void DesyncDialogClass::Become_Host_If_Promoted(void) -{ - if (!Is_Active() || IsHostDialog || CountdownActive || !Session.Am_I_Master()) { - return; - } - - DebugString("This machine is the new master; switching to the decision dialog\n"); - Destroy_Dialog(); - Create_Dialog(); -} - - -void DesyncDialogClass::Update_Player_List(void) -{ - if (!Is_Active()) { - return; - } - - HWND list = GetDlgItem(Window, IDC_DESYNC_PLAYER_LIST); - if (list == NULL) { - return; - } - - ListBox_ResetContent(list); - - int const status_x = Status_Column_X(list); - int const master = Session.Master_Player_ID(); - - for (int house = 0; house < MAX_PLAYERS && house < Houses.Count(); house++) { - HouseClass const * housep = Houses[house]; - bool const left = State.Has_Left(house); - - // A player who left stays listed, though their seat is no longer human. - if (housep == NULL || (!housep->IsHuman && !left)) { - continue; - } - - // The roster entry is gone by now, so the kept name is the only copy while the list rebuilds. - char const * name = left && State.Left_Name(house)[0] != '\0' ? State.Left_Name(house) : housep->IniName.c_str(); - int const row = ListBox_AddString(list, name); - if (row < 0) { - continue; - } - - if (house == master) { - OwnerDraw::CellData host; - host.type = OwnerDraw::CellData::SURFACE; - host.surf = SurfaceCache.GetSurface("wolhost.pcx"); - host.hint.set(""); - SendMessage(list, OD_SETCELL, MAKEWPARAM(HOST_COLUMN_X, row), (LPARAM)&host); - } - - int text = TXT_OK; - COLORREF color = RGB(0, 200, 0); - if (left) { - text = TXT_SYNC_STATUS_LEFT; - color = RGB(200, 0, 0); - } else if (Sync_Is_Out_Of_Sync(house)) { - text = TXT_SYNC_STATUS_OUT; - color = RGB(200, 200, 0); - } - - OwnerDraw::CellData status; - status.type = OwnerDraw::CellData::TEXT; - status.string.set(Fetch_String(text)); - status.hint.set(""); - status.color = color; - SendMessage(list, OD_SETCELL, MAKEWPARAM(status_x, row), (LPARAM)&status); - } - - InvalidateRect(list, NULL, FALSE); -} - - -void DesyncDialogClass::Refill_Chat_List(void) -{ - if (!Is_Active()) { - return; - } - - HWND list = GetDlgItem(Window, IDC_DESYNC_CHAT_LIST); - if (list == NULL) { - return; - } - - ListBox_ResetContent(list); - for (std::string const & line : ChatBacklog) { - ListBox_AddString(list, line.c_str()); - } - ListBox_SetTopIndex(list, ListBox_GetCount(list) - 1); -} - - -void DesyncDialogClass::Append_Chat_Line(char const * line) -{ - ChatBacklog.emplace_back(line); - if (ChatBacklog.size() > CHAT_BACKLOG_MAX) { - ChatBacklog.erase(ChatBacklog.begin()); - } - - if (!Is_Active()) { - return; - } - - HWND list = GetDlgItem(Window, IDC_DESYNC_CHAT_LIST); - if (list == NULL) { - return; - } - - ListBox_AddString(list, line); - while (ListBox_GetCount(list) > CHAT_BACKLOG_MAX) { - ListBox_DeleteString(list, 0); - } - ListBox_SetTopIndex(list, ListBox_GetCount(list) - 1); -} - - -void DesyncDialogClass::Send_Chat(void) -{ - if (!Is_Active() || ChatPlaceholderActive) { - return; - } - - HWND edit = GetDlgItem(Window, IDC_DESYNC_CHAT_EDIT); - if (edit == NULL) { - return; - } - - char buffer[MAX_MESSAGE_LENGTH]; - GetWindowText(edit, buffer, sizeof(buffer)); - if (buffer[0] == '\0') { - return; - } - - SetWindowText(edit, ""); - SetFocus(edit); - - Session.MessageScope = ChatScopeType::Everyone; - Session.MessageAddress = IPXAddressClass(); - Chat_Send(buffer); -} - - -void DesyncDialogClass::On_Chat_Edit_Focus(bool gained) -{ - if (!Is_Active()) { - return; - } - - HWND edit = GetDlgItem(Window, IDC_DESYNC_CHAT_EDIT); - if (edit == NULL) { - return; - } - - if (gained && ChatPlaceholderActive) { - SetWindowText(edit, ""); - ChatPlaceholderActive = false; - } else if (!gained && GetWindowTextLength(edit) == 0) { - SetWindowText(edit, Fetch_String(TXT_CHAT_HINT)); - ChatPlaceholderActive = true; - } -} - - -void DesyncDialogClass::Send_Heartbeat(void) -{ - if (PlayerPtr == NULL || Session.Players.Count() == 0) { - return; - } - - GlobalPacketType packet; - NetGlobal::Initialize_Packet(packet, NET_DESYNC_HEARTBEAT); - std::snprintf(packet.Name, sizeof(packet.Name), "%s", Session.Players[0]->Name); - - for (int index = 1; index < Session.Players.Count(); index++) { - Ipx.Send_Global_Message(&packet, sizeof(packet), 0, &Session.Players[index]->Address); - } - Ipx.Service(); -} - - -void DesyncDialogClass::Send_Continue(void) -{ - DebugString("Telling every seat to continue without the players out of sync\n"); - - GlobalPacketType packet; - NetGlobal::Initialize_Packet(packet, NET_DESYNC_CONTINUE); - std::snprintf(packet.Name, sizeof(packet.Name), "%s", Session.Players[0]->Name); - - for (int index = 1; index < Session.Players.Count(); index++) { - Ipx.Send_Global_Message(&packet, sizeof(packet), 1, &Session.Players[index]->Address); - Ipx.Service(); - } -} - - -/// -/// Drops the seats that have fallen silent, so a machine that died without a sign-off neither -/// holds up the decision nor lingers in the seats a later load reconciles. -/// -void DesyncDialogClass::Check_Timeouts(void) -{ - std::int64_t const now = Monotonic_Milliseconds(); - - for (int index = Session.Players.Count() - 1; index >= 1; index--) { - int const house = Session.Players[index]->Player.ID; - if (!State.Is_Silent(house, now)) { - continue; - } - - DebugString("No heartbeat from %s (house %d) for %d seconds; dropping the seat\n", - Session.Players[index]->Name, house, (int)(DesyncClass::HEARTBEAT_TIMEOUT_MS / 1000)); - - std::string const name = Session.Players[index]->Name; - Destroy_Connection(house, 1); - Notify_Player_Left(house, name.c_str()); - } -} - - -void DesyncDialogClass::Start_Countdown(void) -{ - DebugString("Counting down to the multiplayer load\n"); - - CountdownActive = true; - LastCountdownSecond = -1; - - if (!Is_Active()) { - return; - } - - Append_Chat_Line(Fetch_String(TXT_LOADING_SAVED_GAME)); - - ShowWindow(GetDlgItem(Window, IDC_DESYNC_COUNTDOWN_TEXT), SW_SHOW); - ShowWindow(GetDlgItem(Window, IDC_DESYNC_COUNTDOWN_BAR), SW_SHOW); - Update_Countdown_Text(); - - if (IsHostDialog) { - EnableWindow(GetDlgItem(Window, IDC_DESYNC_LOAD), FALSE); - EnableWindow(GetDlgItem(Window, IDC_DESYNC_CONTINUE), FALSE); - } - - InvalidateRect(Window, NULL, FALSE); -} - - -void DesyncDialogClass::Update_Countdown_Text(void) -{ - if (!Is_Active() || !CountdownActive || !SaveManager.MultiplayerLoad.Is_Pending()) { - return; - } - - int const seconds = SaveManager.MultiplayerLoad.Seconds_Left(Monotonic_Milliseconds()); - if (seconds == LastCountdownSecond) { - return; - } - LastCountdownSecond = seconds; - - char buffer[128]; - std::snprintf(buffer, sizeof(buffer), - Fetch_String(seconds == 1 ? TXT_LOADING_IN_SECOND : TXT_LOADING_IN_SECONDS), seconds); - SetDlgItemText(Window, IDC_DESYNC_COUNTDOWN_TEXT, buffer); -} - - -/// -/// Draws the countdown bar over its placeholder the way the reconnect dialog draws its sync -/// bars: shrinking, and green to yellow to red as the load nears. -/// -void DesyncDialogClass::Draw_Countdown_Bar(HWND window) -{ - if (!CountdownActive || !SaveManager.MultiplayerLoad.Is_Pending()) { - return; - } - - HWND bar = GetDlgItem(window, IDC_DESYNC_COUNTDOWN_BAR); - if (bar == NULL) { - return; - } - - RECT winrect; - Get_Display_Rect(bar, &winrect); - - Rect bar_rect; - bar_rect.X = winrect.left; - bar_rect.Y = winrect.top; - bar_rect.Width = winrect.right - winrect.left; - bar_rect.Height = winrect.bottom - winrect.top; - - int const total = (int)MultiplayerLoadClass::COUNTDOWN_MS; - int const remaining = std::clamp((int)SaveManager.MultiplayerLoad.Milliseconds_Left(Monotonic_Milliseconds()), 0, total); - int const elapsed = total - remaining; - - unsigned short color = DSurface::Build_Hicolor_Pixel(0, 200, 0); - if (elapsed > total * 2 / 5) { - color = DSurface::Build_Hicolor_Pixel(200, 200, 0); - if (elapsed > total * 4 / 5) { - color = DSurface::Build_Hicolor_Pixel(200, 0, 0); - } - } - - bar_rect.Width = std::max(6, bar_rect.Width * remaining / total); - - AlternateSurface->Fill_Rect(AlternateSurface->Get_Rect(), bar_rect, color); -} - - -INT_PTR CALLBACK DesyncDialogClass::Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - switch (message) { - case WM_INITDIALOG: - OwnerDraw::Subclass_Dialog(window, 0); - break; - - case WM_DRAWITEM: - OwnerDraw::Draw_Item((DRAWITEMSTRUCT *)lparam); - return(TRUE); - - case WM_PAINT: - OwnerDraw::Draw_Dialog_Back(window); - DesyncDialog.Draw_Countdown_Bar(window); - ValidateRect(window, NULL); - break; - - case WM_MOVING: - return(On_WM_MOVING(window, wparam, lparam)); - - case WM_CTLCOLORMSGBOX: - case WM_CTLCOLOREDIT: - case WM_CTLCOLORLISTBOX: - case WM_CTLCOLORBTN: - case WM_CTLCOLORDLG: - case WM_CTLCOLORSCROLLBAR: - case WM_CTLCOLORSTATIC: - return((INT_PTR)GetStockObject(BLACK_BRUSH)); - - case WM_ERASEBKGND: - return(TRUE); - - case WM_COMMAND: - switch (LOWORD(wparam)) { - case IDC_DESYNC_LOAD: - case IDC_DESYNC_CONTINUE: - case IDC_DESYNC_QUIT: - DesyncDialog.Decision = LOWORD(wparam); - break; - - // Enter in the chat box arrives as IDOK, since the dialog has no default button. - case IDOK: - DesyncDialog.Send_Chat(); - break; - - case IDC_DESYNC_CHAT_EDIT: - if (HIWORD(wparam) == EN_SETFOCUS) { - DesyncDialog.On_Chat_Edit_Focus(true); - } else if (HIWORD(wparam) == EN_KILLFOCUS) { - DesyncDialog.On_Chat_Edit_Focus(false); - } - break; - } - break; - } - - return(FALSE); + Screen.Master_Changed(); } diff --git a/code/desyncdlg.h b/code/desyncdlg.h index ac03ec61b..58c6c1749 100644 --- a/code/desyncdlg.h +++ b/code/desyncdlg.h @@ -9,7 +9,7 @@ #pragma once -#include "desync.h" +#include "ui/uidesync.h" #include "win.h" #include @@ -17,7 +17,7 @@ #include /* - * The dialog shown when a network game goes out of sync. The master chooses to load a saved + * The screen shown when a network game goes out of sync. The master chooses to load a saved * game, to continue without the players out of sync, or to quit; everyone else waits. Both * variants list the players with their state and carry a chat box. Game logic is halted while * it is up, and the network is kept alive with heartbeats. @@ -34,7 +34,7 @@ class DesyncDialogClass // Blocks until a decision has been made; the network is serviced throughout. OutcomeType Run(void); - bool Is_Active(void) const {return(Window != NULL);} + bool Is_Active(void) const {return(IsRunning);} // Sends the heartbeat and drops silent players; called from the network maintenance // so that both outlive a nested dialog's message loop. @@ -48,34 +48,10 @@ class DesyncDialogClass void Notify_Master_Changed(void); private: - void Create_Dialog(void); - void Destroy_Dialog(void); - void Fit_To_Screen(void); - void Become_Host_If_Promoted(void); - void Update_Player_List(void); - void Refill_Chat_List(void); - void Append_Chat_Line(char const * line); - void Send_Chat(void); - void On_Chat_Edit_Focus(bool gained); - void Send_Heartbeat(void); - void Send_Continue(void); - void Check_Timeouts(void); - void Start_Countdown(void); - void Update_Countdown_Text(void); - void Draw_Countdown_Bar(HWND window); - static INT_PTR CALLBACK Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); + // The screen's whole behavior. + UIDesyncPresenterClass Screen; - HWND Window = NULL; - bool IsHostDialog = false; - int Decision = 0; - bool ContinueReceived = false; - bool ChatPlaceholderActive = false; - bool CountdownActive = false; - bool QuitEnabled = false; - std::int64_t OpenedAt = 0; - int LastCountdownSecond = -1; - DesyncClass State; - std::vector ChatBacklog; + bool IsRunning = false; }; extern DesyncDialogClass DesyncDialog; diff --git a/code/display.cpp b/code/display.cpp index bd4712eb6..b9775bef5 100644 --- a/code/display.cpp +++ b/code/display.cpp @@ -3859,14 +3859,13 @@ LRESULT DisplayClass::Windows_Message_Proc(HWND hWnd, UINT Msg, WPARAM wParam, L /// Loads the display layers from the save game stream. /// /// The stream to read the layers from. -/// Returns with S_OK if every layer was read, otherwise the failure code of the -/// layer that could not be read. -HRESULT DisplayClass::Load(IStream * stream) +/// bool; Was the record read whole? +bool DisplayClass::Load(SaveStreamClass & stream) { - HRESULT result = S_OK; + bool result = true; for (LayerType layer = LAYER_FIRST; layer < LAYER_COUNT; layer++) { result = Layer[layer].Load(stream); - if (FAILED(result)) break; + if (!result) break; } return(result); } @@ -3876,14 +3875,13 @@ HRESULT DisplayClass::Load(IStream * stream) /// Saves the display layers to the save game stream. /// /// The stream to write the layers to. -/// Returns with S_OK if every layer was written, otherwise the failure code of the -/// layer that could not be written. -HRESULT DisplayClass::Save(IStream * stream) +/// bool; Was the record written whole? +bool DisplayClass::Save(SaveStreamClass & stream) { - HRESULT result = S_OK; + bool result = true; for (LayerType layer = LAYER_FIRST; layer < LAYER_COUNT; layer++) { result = Layer[layer].Save(stream); - if (FAILED(result)) break; + if (!result) break; } return(result); } diff --git a/code/display.h b/code/display.h index 3c346c1f7..a2ac6197c 100644 --- a/code/display.h +++ b/code/display.h @@ -65,8 +65,8 @@ class DisplayClass: public MapClass friend class Tactical; public: - virtual HRESULT Load(IStream * stream); - virtual HRESULT Save(IStream * stream); + virtual bool Load(SaveStreamClass & stream); + virtual bool Save(SaveStreamClass & stream); virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/drawhelp.cpp b/code/drawhelp.cpp new file mode 100644 index 000000000..321759285 --- /dev/null +++ b/code/drawhelp.cpp @@ -0,0 +1,994 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#include "always.h" + +#include "drawhelp.h" + +#include "_surface.h" +#include "_xmouse.h" +#include "arraylist.h" +#include "dbgprint.h" +#include "dict.h" +#include "dsurface.h" +#include "hsv.h" +#include "misc.h" +#include "rgb.h" +#include "srfcache.h" +#include "utf8.h" +#include "wstring.h" + +#include +#include +#include +#include + + +extern unsigned int Wstring_Hash(Wstring & string); + + +COLORREF ODColorText = RGB(112, 255, 0); + +unsigned short ODRComponentMask; +unsigned short ODGComponentMask; +unsigned short ODBComponentMask; + + +static bool ODGetFontMetrics(char const * font_name, ODFontMetrics * metrics); +static void ODDrawCharRemap(Surface & dst_surf, const char * text, int max_chars, Rect const & rect, char const * font_name, COLORREF color, char flags, int char_spacing); +static int ODColorToHiColor(COLORREF color); +static void ODBuildRemapColors(COLORREF color, unsigned char const * palette, RGBClass * out); + + +/// +/// Reads a picture into the surface cache if it is not there already. +/// The cache is a pure lookup and never loads anything itself. The routine that filled it +/// up front went with the owner-draw dialogs, and no other point in startup is both after +/// the mix files are mounted and before every surviving screen paints, so a picture is read +/// when it is first asked for. A name that could not be read is not tried again. +/// +/// True to reduce the picture to the red component of its own +/// palette, which is how a coverage sheet is stored. +/// bool; Is the picture in the cache? +static bool ODCacheImage(char const * name, int bpp, bool red_channel) +{ + static std::set _attempted; + + if (SurfaceCache.GetSurface(name) != NULL) { + return(true); + } + if (!_attempted.insert(std::string(name)).second) { + return(false); + } + + if (!SurfaceCache.CachePCX(name, bpp, red_channel)) { + DebugString("TS: %s could not be read.\n", name); + return(false); + } + return(true); +} + + +/// +/// Reads a remap font's two sheets into the surface cache. +/// The index sheet keeps its palette indices and its palette; the alpha sheet is reduced to +/// the red component of its own palette, which is the coverage each pixel carries. +/// +static void ODCacheFontSheets(char const * font_name) +{ + char name[64]; + + snprintf(name, sizeof(name), "%si.pcx", font_name); + ODCacheImage(name, 1, false); + + snprintf(name, sizeof(name), "%sa.pcx", font_name); + ODCacheImage(name, 1, true); +} + + +/// +/// Fetches one of the dialog system's pictures, reading it on first request. +/// +/// File name of the .PCX, which is also its name in the cache. +/// Returns with the cached surface, or NULL if the picture could not be read. The +/// surface stays owned by the cache. +Surface * OD_Fetch_Image(char const * name) +{ + ODCacheImage(name, 2, false); + return(SurfaceCache.GetSurface(name)); +} + + +/// +/// Maps a decoded code point onto the glyph the remap sheets index by. +/// +/// Returns with the Windows-1252 code of the glyph, or that of '?' for a code +/// point the sheets do not carry. +unsigned char OD_Font_Glyph(char32_t code) +{ + if (code < ' ') { + return((unsigned char)code); + } + int index = UTF8::Windows_1252_Glyph(code); + return((unsigned char)(index < 0 ? '?' : index)); +} + + +static unsigned char OD_Glyph(char32_t code) +{ + return(OD_Font_Glyph(code)); +} + + +/// +/// Sets up the color component masks used for blending. +/// The masks depend on how the display surface packs its pixels, so this routine cannot +/// run until the video mode is known. +/// +static void ODInitMasks(void) +{ + ODRComponentMask = 255; + ODRComponentMask = ODRComponentMask >> DSurface::Get_Red_Left(); + ODRComponentMask <<= DSurface::Get_Red_Right(); + + ODGComponentMask = 255; + ODGComponentMask = ODGComponentMask >> DSurface::Get_Green_Left(); + ODGComponentMask <<= DSurface::Get_Green_Right(); + + ODBComponentMask = 255; + ODBComponentMask = ODBComponentMask >> DSurface::Get_Blue_Left(); + ODBComponentMask <<= DSurface::Get_Blue_Right(); +} + + +/// +/// Converts a Windows color reference into a display pixel. +/// The dialog colors are all written as RGB() values, so they have to be packed into the +/// pixel layout of the display surface before anything can be drawn with them. +/// +/// Returns with the packed pixel value. An all-ones color is passed through +/// unchanged. +static int ODColorToHiColor(COLORREF color) +{ + if (color == 0xFFFFFFFF) { + return(0xFFFFFFFF); + } + /// Do not replace the union with direct byte extraction. It improves several callers and + /// breaks ProgressBarCtrlProc, which is otherwise exact -- and an exact caller outranks the + /// partial ones. + union { + struct { + unsigned int red : 8; + unsigned int green : 8; + unsigned int blue : 8; + unsigned int a : 8; + }; + int v; + } c; + + c.v = color; + + return(DSurface::Build_Hicolor_Pixel(c.red, c.green, c.blue)); +} + + +/// +/// Draws word wrapped text with a remapped bitmap font. +/// This routine breaks the text into lines that will fit the rectangle -- honoring the +/// newlines already in it and breaking at a space wherever one can be found -- and hands +/// each line in turn to ODDrawCharRemap. +/// +/// The base name of the font sheets to draw with. +/// The OD_DRAW_CHAR alignment flags to lay each line out with. +/// The extra spacing to insert between characters. +int OD_Draw_Text_Remap(Surface & surface, const char * text, Rect const & rect, char const * name, COLORREF color, int flags, int char_spacing) +{ + int line_len = strlen(text); + char const * line_ptr = text; + Rect draw_rect = rect; + + ODFontMetrics data; + if (!ODGetFontMetrics(name, &data)) { + return(0); + } + + while (line_len) { + if (line_ptr) { + char const * nl_ptr = strchr(line_ptr, '\n'); + if (nl_ptr) { + int nl_len = (int)(nl_ptr - line_ptr) + 1; + if (line_len >= nl_len) { + line_len = nl_len; + } + } + } + + if ((unsigned char)*line_ptr <= ' ') { + ++line_ptr; + if (--line_len == 0) { + return(0); + } + } + + int text_width = 0; + for (char const * cursor = text; cursor - text < line_len; ) { + text_width += char_spacing + data.charWidths[OD_Glyph(UTF8::Decode(cursor))]; + } + + if (text_width > draw_rect.Width - draw_rect.X) { + int fallback = (int)UTF8::Boundary_Before(line_ptr, line_len - 1); + int cut = line_len - 1; + + flags &= ~4; + + while (cut > 0) { + if ((unsigned char)line_ptr[cut] <= ' ') { + break; + } + --cut; + } + if (cut > 0) { + line_len = cut; + if (cut != -1) { + continue; + } + } + + line_len = fallback; + } else { + ODDrawCharRemap(surface, line_ptr, line_len, draw_rect, name, color, (char)flags, char_spacing); + line_ptr += line_len; + draw_rect.Y += data.glyphHeight; + line_len = strlen(line_ptr); + } + } + + return(0); +} + + +/// +/// Determines how strongly a hue should be remapped. +/// The font remapper uses this to pull its hue shift back around the primary colors, so +/// that text tinted near one of them does not swing away from the color asked for. +/// +/// The hue to compute the factor for. +/// Returns with the scale factor; the nearer the hue sits to a primary, the smaller +/// it gets. +static float ODCalcTextRemapFactor(int hue) +{ + float val = 1.0f; + + int arr[3]; + arr[0] = 43; + arr[1] = 128; + arr[2] = 213; + + for (int i = 0; i < 3; i++) { + int value = arr[i]; + + if (hue > value - 16 && hue <= value) { + val = float(value - hue); + val *= (1.0f / 16); + val *= (60.0f / 100); + val += (40.0f / 100); + } else if (hue > value && hue <= value + 16) { + val = float(hue - value); + val *= (1.0f / 16); + val *= (60.0f / 100); + val += (40.0f / 100); + } + } + return(val); +} + + +/// +/// Shifts a font sheet's palette toward the color text is asked to be drawn in. +/// The sheets hold an intensity ramp of their own hue, so each entry keeps its own +/// saturation and value and is pulled around to the requested hue rather than replaced +/// by it. +/// +/// The 768-byte palette of the font's index sheet. +/// Receives one remapped color for each of the 256 palette entries. +static void ODBuildRemapColors(COLORREF color, unsigned char const * palette, RGBClass * out) +{ + RGBClass remap_rgb((unsigned char)color, (unsigned char)(color >> 8), (unsigned char)(color >> 16)); + HSVClass remap_hsv = remap_rgb; + + int hue = remap_hsv.Get_Hue(); + + int end = int(hue + 15.0); + float min_factor = 1.0f; + for (int i = int(hue - 15.0); i <= end; ++i) { + float factor = ODCalcTextRemapFactor(i); + if (factor < min_factor) { + min_factor = factor; + } + } + + unsigned char sat = (unsigned char)remap_hsv.Get_Saturation(); + unsigned char val = (unsigned char)remap_hsv.Get_Value(); + + float hue_float = (float)hue; + unsigned char const * pal = palette; + for (int i = 0; i < 256; ++i) { + RGBClass pal_rgb; + pal_rgb.Set_Red(pal[0]); + pal_rgb.Set_Green(pal[1]); + pal_rgb.Set_Blue(pal[2]); + HSVClass pal_hsv = pal_rgb; + + HSVClass out_hsv = pal_hsv; + out_hsv.Set_Hue((unsigned char)(int)(hue_float - (int)(68.0f - pal_hsv.Get_Hue()) * min_factor)); + out_hsv.Set_Saturation((unsigned char)((sat * out_hsv.Get_Saturation()) >> 8)); + out_hsv.Set_Value((unsigned char)((val * out_hsv.Get_Value()) >> 8)); + + out[i] = out_hsv; + pal += 3; + } +} + + +/// +/// Draws a line of text with a remapped bitmap font. +/// This routine builds a table that shifts the font's own palette toward the color asked +/// for and then alpha blends each character onto the destination surface. It is the low +/// level draw that all of the owner-draw remapped text ends up going through. +/// +/// The maximum number of characters of the text to draw. +/// The rectangle to align the text within. +/// The base name of the font sheets to draw with. +/// The OD_DRAW_CHAR alignment flags to lay the text out with. +/// The extra spacing to insert between characters. +static void ODDrawCharRemap(Surface & dst_surf, const char *text, int max_chars, Rect const & rect, char const *font_name, COLORREF color, char flags, int char_spacing) +{ + int i; + Rect draw_rect = rect; + + ODCacheFontSheets(font_name); + + char name_i[64]; + strcpy(name_i, font_name); + strcat(name_i, "i.pcx"); + + char palette[768]; + Surface *sheet_i = SurfaceCache.GetSurface(name_i, palette); + if (sheet_i == NULL) { + return; + } + + char name_a[64]; + strcpy(name_a, font_name); + strcat(name_a, "a.pcx"); + + Surface *sheet_a = SurfaceCache.GetSurface(name_a, NULL); + if (sheet_a == NULL) { + return; + } + + RGBClass remap_colors[256]; + ODBuildRemapColors(color, (unsigned char *)palette, remap_colors); + + unsigned short remap_table[256]; + for (i = 0; i < 256; ++i) { + int packed = (((remap_colors[i].Get_Blue() << 8) | remap_colors[i].Get_Green()) << 8) | remap_colors[i].Get_Red(); + remap_table[i] = (unsigned short)ODColorToHiColor(packed); + } + + ODFontMetrics font_data; + if (!ODGetFontMetrics(font_name, &font_data)) { + return; + } + + if ((int)strlen(text) < max_chars) { + max_chars = strlen(text); + } + + int total_width = 0; + for (char const * cursor = text; cursor - text < max_chars; ) { + total_width += font_data.charWidths[OD_Glyph(UTF8::Decode(cursor))] + char_spacing; + } + + if ((flags & OD_DRAW_CHAR_FLAG_HORIZONTAL_CENTER) != 0) { + draw_rect.X += (draw_rect.Width - draw_rect.X - total_width) / 2; + } else if ((flags & OD_DRAW_CHAR_ALIGN_FLAG_RIGHT) != 0) { + draw_rect.X = draw_rect.Width - total_width - 1; + } + + if ((flags & OD_DRAW_CHAR_FLAG_VERTICAL_CENTER) != 0) { + draw_rect.Y = draw_rect.Y + (draw_rect.Height - font_data.glyphHeight - draw_rect.Y) / 2; + } + + draw_rect.Y -= font_data.topMargin; + --draw_rect.X; + + unsigned char *src_i = (unsigned char *)sheet_i->Lock(); + unsigned char *src_a = (unsigned char *)sheet_a->Lock(); + unsigned char *dst = (unsigned char *)dst_surf.Lock(); + + if (src_i != NULL && src_a != NULL && dst != NULL) { + int cell_w = font_data.glyphWidth + font_data.leftMargin; + int cell_h = font_data.glyphHeight + font_data.topMargin; + int chars_per_row = sheet_i->Get_Width() / (font_data.glyphWidth + font_data.leftMargin); + int dst_stride = dst_surf.Stride() / 2; + + /* + * The two sheets are separate allocations, so the coverage sheet cannot be indexed + * by an offset from the color sheet: the difference between two unrelated pointers + * does not fit an int on a 64-bit host, which is what the inherited code stored it + * in. Each sheet is walked through its own pointer and its own stride instead. + */ + int index_stride = sheet_i->Stride(); + int alpha_stride = sheet_a->Stride(); + + int x = draw_rect.X; + for (char const * cursor = text; cursor - text < max_chars; ) { + + unsigned char index = OD_Glyph(UTF8::Decode(cursor)); + if (index <= ' ') { + x += font_data.charWidths[index] + char_spacing; + } else { + int glyph = index + 1; + int src_x = (glyph % chars_per_row) * cell_w; + int src_y = (glyph / chars_per_row) * cell_h; + + int src_y_end = src_y + cell_h; + unsigned char *alpha_col = src_a + (src_y * alpha_stride + src_x); + unsigned char *index_col = src_i + (src_y * index_stride + src_x); + unsigned char *dst_col = dst + 2 * (dst_stride * draw_rect.Y + x); + + for (int sx = src_x; sx < src_x + cell_w; ++sx) { + if (src_y < src_y_end) { + unsigned short *dst_px = (unsigned short *)dst_col; + unsigned char *alpha_px = alpha_col; + unsigned char *index_px = index_col; + + int sy = src_y_end - src_y; + do { + unsigned char alpha = *alpha_px; + if (alpha != 0) { + *dst_px = OD_Blend_Color(*dst_px, remap_table[*index_px], alpha); + } + + dst_px += dst_stride; + alpha_px += alpha_stride; + index_px += index_stride; + --sy; + } while (sy != 0); + } + + ++alpha_col; + ++index_col; + dst_col += 2; + } + + x += font_data.charWidths[index] + char_spacing; + } + } + } + + if (&dst_surf != NULL) { + dst_surf.Unlock(); + } + sheet_a->Unlock(); + sheet_i->Unlock(); +} + + +/// +/// Fetches the metrics of a remappable bitmap font. +/// This routine measures the font's sheet -- the margins, the size of a character cell and +/// the inked width of every character -- so that the remap text routines know how to lay +/// characters out. Measuring is expensive, so the result is kept by font name. +/// +/// The base name of the font, without the sheet suffix. +/// Buffer to fill in with the measurements. +/// bool; Were the metrics available? +static bool ODGetFontMetrics(char const * font_name, ODFontMetrics * metrics) +{ + static Dictionary metricsDict(Wstring_Hash); + + char buf[64]; + strcpy(buf, font_name); + strcat(buf, "a.pcx"); + + Wstring name; + name = (char *)font_name; + name.toLower(); + + ODFontMetrics * found = NULL; + if (metricsDict.getPointer(name, &found)) { + if (metrics != NULL) { + *metrics = *found; + return(true); + } + } + + DebugString("TS: Computing font metrics....\n"); + + ODFontMetrics temp; + memset(&temp, 0, sizeof(temp)); + + ODCacheFontSheets(font_name); + + char palette[768]; + Surface * surf = SurfaceCache.GetSurface(buf, palette); + if (surf == NULL) { + return(false); + } + + char * basePtr = (char *)surf->Lock(); + int stride = surf->Stride(); + + /* + * ---------------------------------------------------------------- + * Vertical metrics: topMargin = blank rows above the glyph row, + * glyphHeight = inked rows (probed at column 4). + * ---------------------------------------------------------------- + */ + temp.topMargin = 0; + while (temp.topMargin < surf->Get_Height()) { + if (basePtr[stride * temp.topMargin + 4] != 0) break; + ++temp.topMargin; + } + int y = temp.topMargin; + while (y < surf->Get_Height()) { + if (basePtr[stride * y + 4] == 0) break; + ++y; + ++temp.glyphHeight; + } + + /* + * ---------------------------------------------------------------- + * Horizontal metrics: leftMargin = blank columns before the glyphs, + * glyphWidth = inked columns (probed along row 'top'). + * ---------------------------------------------------------------- + */ + temp.leftMargin = 0; + while (temp.leftMargin < surf->Get_Width()) { + if (basePtr[stride * temp.topMargin + temp.leftMargin] != 0) break; + ++temp.leftMargin; + } + int left = temp.leftMargin; + + int x; + x = left; + while (x < surf->Get_Width()) { + if (basePtr[stride * temp.topMargin + x] == 0) break; + ++x; + ++temp.glyphWidth; + } + + /* + * ---------------------------------------------------------------- + * Compute per-character metrics + * ---------------------------------------------------------------- + */ + int width = surf->Get_Width(); + int charsPerRow = width / (left + temp.glyphWidth); + for (int ch = 0; ch < 256; ++ch) { + + int left = temp.leftMargin; + int fontHeight = temp.glyphHeight; + int top = temp.topMargin; + int fontWidth = temp.glyphWidth; + + int glyphY = top + (fontHeight + top) * ((ch + 1) / charsPerRow); + int glyphX = left + (left + fontWidth) * ((ch + 1) % charsPerRow); + + int first = -1; + int last = 0; + + for (int x = glyphX; x < glyphX + fontWidth; ++x) { + int nonEmpty = 0; + for (int y = glyphY; y < glyphY + fontHeight; ++y) { + if (basePtr[stride * y + x] != 0) ++nonEmpty; + } + if (nonEmpty) { + last = x; + if (first == -1) first = x; + } + } + + if (first != -1) { + temp.charWidths[ch] = (last - first + 1); + } else { + temp.charWidths[ch] = (fontWidth / 3 + 1); + } + } + + surf->Unlock(); + + /* + * ---------------------------------------------------------------- + * Store result in caller's buffer + * ---------------------------------------------------------------- + */ + memcpy(metrics, &temp, sizeof(ODFontMetrics)); + + metricsDict.add(name, temp); + + return(true); +} + + +/// +/// Measures a remap font. +/// +/// The base name of the font, without the sheet suffix. +/// bool; Could the font's sheets be read? +bool OD_Font_Metrics(char const * font_name, ODFontMetrics & metrics) +{ + return(ODGetFontMetrics(font_name, &metrics)); +} + + +/// +/// Composes a remap font's glyph sheet into premultiplied RGBA pixels. +/// The sheets are combined the way ODDrawCharRemap combines them per pixel: the coverage +/// sheet supplies alpha and the index sheet a palette entry shifted toward the color asked +/// for. The result is what that blend produces over black, so a caller can hand it to a +/// renderer that blends premultiplied source over what is already there. +/// +/// Receives width * height * 4 bytes in RGBA order. +/// bool; Could the font's sheets be read? +bool OD_Font_Sheet(char const * font_name, COLORREF color, int & width, int & height, std::vector & pixels) +{ + ODCacheFontSheets(font_name); + + char name_i[64]; + snprintf(name_i, sizeof(name_i), "%si.pcx", font_name); + + char palette[768]; + Surface * sheet_i = SurfaceCache.GetSurface(name_i, palette); + if (sheet_i == NULL) { + return(false); + } + + char name_a[64]; + snprintf(name_a, sizeof(name_a), "%sa.pcx", font_name); + + Surface * sheet_a = SurfaceCache.GetSurface(name_a, NULL); + if (sheet_a == NULL) { + return(false); + } + + width = sheet_i->Get_Width(); + height = sheet_i->Get_Height(); + if (width <= 0 || height <= 0 || sheet_a->Get_Width() < width || sheet_a->Get_Height() < height) { + return(false); + } + + RGBClass remap_colors[256]; + ODBuildRemapColors(color, (unsigned char *)palette, remap_colors); + + unsigned char * src_i = (unsigned char *)sheet_i->Lock(); + unsigned char * src_a = (unsigned char *)sheet_a->Lock(); + if (src_i == NULL || src_a == NULL) { + if (src_i != NULL) { + sheet_i->Unlock(); + } + if (src_a != NULL) { + sheet_a->Unlock(); + } + return(false); + } + + int index_stride = sheet_i->Stride(); + int alpha_stride = sheet_a->Stride(); + + pixels.assign((std::size_t)width * height * 4, 0); + + for (int y = 0; y < height; ++y) { + unsigned char const * row_i = src_i + (std::size_t)index_stride * y; + unsigned char const * row_a = src_a + (std::size_t)alpha_stride * y; + unsigned char * out = pixels.data() + (std::size_t)width * y * 4; + + for (int x = 0; x < width; ++x) { + unsigned char alpha = row_a[x]; + if (alpha != 0) { + RGBClass const & rgb = remap_colors[row_i[x]]; + out[0] = (unsigned char)((rgb.Get_Red() * alpha + 127) / 255); + out[1] = (unsigned char)((rgb.Get_Green() * alpha + 127) / 255); + out[2] = (unsigned char)((rgb.Get_Blue() * alpha + 127) / 255); + out[3] = alpha; + } + out += 4; + } + } + + sheet_a->Unlock(); + sheet_i->Unlock(); + return(true); +} + + +/// +/// Draws a line of text onto a surface. +/// This routine borrows a device context from the surface, unlocking it as often as it +/// must beforehand, and lets Windows put the text out aligned within the rectangle given. +/// Nothing is drawn while the game does not hold the focus. +/// +/// The number of characters of the text to draw. +/// The surface to draw upon, or NULL to draw on the alternate +/// surface. +/// Returns with the pixel width of the text. +int OD_Draw_Text(COLORREF color, HFONT font, Rect const & rect, const char * text, int len, int x_alignment, int y_alignment, Surface * surface) +{ + if (!GameInFocus && !WindowedMode) { + return(0); + } + + DSurface *destsurf = (DSurface *)surface; + if (!surface) { + destsurf = (DSurface *)AlternateSurface; + } + + int lock_count = 0; + while (destsurf->Is_Locked()) { + lock_count++; + destsurf->Unlock(); + } + + SIZE text_size; + + HDC hDC = destsurf->GetDC(); + if (hDC) { + + if (font) { + SelectObject(hDC, font); + } + + SetTextColor(hDC, color); + SetBkMode(hDC, TRANSPARENT); + + GetTextExtentPoint32(hDC, text, len, &text_size); + + int x_offset = rect.X; + int y_offset = rect.Y; + + if (x_alignment == OD_TEXT_ALIGN_MIN) { + x_offset += (rect.Width - text_size.cx + 1) / 2; + } else if (x_alignment == OD_TEXT_ALIGN_CENTER) { + x_offset += (text_size.cx + 1) / -2; + } else if (x_alignment == OD_TEXT_ALIGN_MAX) { + x_offset += -1 - text_size.cx; + } + + if (y_alignment == OD_TEXT_ALIGN_MIN) { + y_offset += (rect.Height - text_size.cy + 1) / 2; + } else if (y_alignment == OD_TEXT_ALIGN_CENTER) { + y_offset += (text_size.cy + 1) / -2; + } else if (y_alignment == OD_TEXT_ALIGN_MAX) { + y_offset += -1 - text_size.cy; + } + + TextOut(hDC, x_offset, y_offset, text, len); + destsurf->ReleaseDC(hDC); + } else { + text_size.cx = 0; + } + + while (lock_count) { + destsurf->Lock(); + lock_count--; + } + + return(text_size.cx); +} + + + +/// +/// Fetches a window's rectangle relative to the main game window. +/// The dialog layout code works in the main window's client space rather than in screen +/// coordinates, so it uses this routine in place of GetWindowRect. +/// +/// Receives the window rectangle, offset into the main window's +/// client area. +/// bool; Was the window rectangle available? +BOOL Get_Display_Rect(HWND window, LPRECT rect) +{ + RECT client; + BOOL res = GetWindowRect(window, rect); + if (!res) { + return(res); + } + GetClientRect(MainWindow, &client); + ClientToScreen(MainWindow, (LPPOINT)&client); + rect->left -= client.left; + rect->right -= client.left; + rect->top -= client.top; + rect->bottom -= client.top; + return(res); +} + + +struct EzFont { + char FaceName[128]; + int DeciPtWidth; + int DeciPtHeight; + int Attributes; + HFONT FontHandle; +}; + +static ArrayList g_EzFonts; + + +/// derived from MSDN "Moving Your Game to Windows, Part III" ttfont.cpp + +#define EZ_ATTR_BOLD 1 +#define EZ_ATTR_ITALIC 2 +#define EZ_ATTR_UNDERLINE 4 +#define EZ_ATTR_STRIKEOUT 8 + +static HFONT Ez_Create_Font(HDC hdc, const char * face_name, int decipt_width, int decipt_height, int attributes); + + +/// +/// Fetches a font of the typeface and point size requested. +/// This routine keeps every font it has built, so repeated requests for the same +/// description hand back the same handle rather than burning another GDI object. +/// The dialog drawing code calls this routine wherever it needs a font. +/// +/// The device context to build the font for. If this is NULL, the +/// font is only looked up and never created. +/// The character width in tenths of a point. +/// The character height in tenths of a point. +/// Bit flags of the EZ_ATTR_ style attributes to apply. +/// Returns with a handle to the font, or NULL if it was neither cached nor +/// able to be created. +/// The returned handle stays owned by the font cache. Do not delete it. +HFONT WS_Get_Font(HDC hdc, const char * face_name, int decipt_width, int decipt_height, int attributes) +{ + EzFont font; + + for (int index = 0; index < g_EzFonts.length(); index++) { + g_EzFonts.get(font, index); + if (!strcmp(font.FaceName, face_name) && font.DeciPtWidth == decipt_width && font.DeciPtHeight == decipt_height && font.Attributes == attributes) { + return(font.FontHandle); + } + } + + if (hdc == NULL) { + return(NULL); + } + + HFONT hFont = Ez_Create_Font(hdc, face_name, decipt_width, decipt_height, attributes); + + if (hFont == NULL) { + return(NULL); + } + + strcpy(font.FaceName, face_name); + font.DeciPtWidth = decipt_width; + font.DeciPtHeight = decipt_height; + font.Attributes = attributes; + font.FontHandle = hFont; + + if (g_EzFonts.addTail(font)) { + return(hFont); + } + + return(NULL); +} + + + +/// +/// Creates a font of the typeface and point size requested. +/// This routine maps the requested decipoint dimensions through the device context's +/// current transform, so the font it builds matches the coordinate space the caller +/// draws in. Use WS_Get_Font in preference to this routine -- that one caches its fonts. +/// +/// The device context the font is to be built for. +/// The character width in tenths of a point. Zero lets the +/// typeface choose its own aspect. +/// The character height in tenths of a point. +/// Bit flags of the EZ_ATTR_ style attributes to apply. +/// Returns with a handle to the font created, or NULL if it could not be +/// created. +/// The caller takes ownership of the font handle. +static HFONT Ez_Create_Font(HDC hdc, const char * face_name, int decipt_width, + int decipt_height, int attributes) +{ + HFONT hFont ; + LOGFONT lf ; + POINT pt ; + TEXTMETRIC tm ; + + SaveDC (hdc) ; + + SetGraphicsMode (hdc, GM_ADVANCED) ; + ModifyWorldTransform (hdc, NULL, MWT_IDENTITY) ; + SetViewportOrgEx (hdc, 0, 0, NULL) ; + SetWindowOrgEx (hdc, 0, 0, NULL) ; + + pt.x = decipt_width ; + pt.y = decipt_height ; + + DPtoLP (hdc, &pt, 1) ; + + lf.lfHeight = -pt.y ; + lf.lfWidth = 0 ; + lf.lfEscapement = 0 ; + lf.lfOrientation = 0 ; + lf.lfWeight = attributes & EZ_ATTR_BOLD ? 700 : 0 ; + lf.lfItalic = attributes & EZ_ATTR_ITALIC ? 1 : 0 ; + lf.lfUnderline = attributes & EZ_ATTR_UNDERLINE ? 1 : 0 ; + lf.lfStrikeOut = attributes & EZ_ATTR_STRIKEOUT ? 1 : 0 ; + lf.lfCharSet = ANSI_CHARSET ; + lf.lfOutPrecision = 0 ; + lf.lfClipPrecision = 0 ; + lf.lfQuality = 0 ; + lf.lfPitchAndFamily = 0 ; + + strcpy (lf.lfFaceName, face_name) ; + + hFont = CreateFontIndirect (&lf) ; + + if (decipt_width != 0) { + hFont = (HFONT) SelectObject (hdc, hFont) ; + GetTextMetrics (hdc, &tm) ; + DeleteObject (SelectObject (hdc, hFont)) ; + lf.lfWidth = (int) (tm.tmAveCharWidth * + fabs (pt.x) / fabs (pt.y) + 0.5); + hFont = CreateFontIndirect (&lf) ; + } + + RestoreDC (hdc, -1); + return(hFont); +} + + +static int _pointer_depth; + + +/// +/// Hands the mouse pointer to the host while a screen of its own is shown. +/// With the game's mouse released, WM_SETCURSOR falls through to the window class and the +/// host draws an arrow. A front end has no game pointer of its own, so without this a +/// screen shows none. +/// +/// Each call must be matched by a call to Recapture_Pointer. +void Release_Pointer_To_Host(void) +{ + if (MouseCursor != nullptr && MouseCursor->Is_Captured()) { + MouseCursor->Release_Mouse(); + } + + _pointer_depth++; +} + + +/// +/// Takes the pointer back once the last screen holding it has gone. +/// +void Recapture_Pointer(void) +{ + if (_pointer_depth > 0) { + _pointer_depth--; + } + + if (_pointer_depth == 0 && MouseCursor != nullptr && !MouseCursor->Is_Captured()) { + MouseCursor->Capture_Mouse(); + } +} + + +/// +/// Builds the color masks the blending helpers paint with. +/// The masks depend on how the display surface packs its pixels, so the video mode has to +/// be up before this runs. +/// +void Prepare_Draw_Resources(void) +{ + ODInitMasks(); +} diff --git a/code/drawhelp.h b/code/drawhelp.h new file mode 100644 index 000000000..3525ee7b7 --- /dev/null +++ b/code/drawhelp.h @@ -0,0 +1,82 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#pragma once + +#include "surface.h" +#include "win.h" + +#include + +/* + * Drawing and window helpers shared by the screens that draw into the game's own + * surfaces. The OD_ and WS_ names are inherited from the owner-draw dialogs these + * routines were first written for; nothing here has anything to do with a dialog. + */ + +#define OD_TEXT_ALIGN_MIN 1 +#define OD_TEXT_ALIGN_CENTER 2 +#define OD_TEXT_ALIGN_MAX 3 + +/// Flags for OD_Draw_Text_Remap. +#define OD_DRAW_CHAR_FLAG_HORIZONTAL_CENTER 1 +#define OD_DRAW_CHAR_ALIGN_FLAG_RIGHT 2 +#define OD_DRAW_CHAR_FLAG_VERTICAL_CENTER 4 + +/* + * Measurements of one of the remap fonts, derived from its own artwork. The sheets carry no + * metrics table, so every figure here is probed out of the ink. + */ +struct ODFontMetrics { + int charWidths[256]; /// inked width of each character, indexed by character code + int glyphWidth; /// width of the inked part of a glyph cell + int glyphHeight; /// height of the inked part of a glyph cell + int topMargin; /// blank rows above each row of glyphs + int leftMargin; /// blank columns before each glyph +}; + +int OD_Draw_Text_Remap(Surface & surface, const char * string, Rect const & rect, char const * name, COLORREF color, int flags, int char_spacing); + +Surface * OD_Fetch_Image(char const * name); + +bool OD_Font_Metrics(char const * font_name, ODFontMetrics & metrics); +bool OD_Font_Sheet(char const * font_name, COLORREF color, int & width, int & height, std::vector & pixels); +unsigned char OD_Font_Glyph(char32_t code); +int OD_Draw_Text(COLORREF color, HFONT font, Rect const & rect, const char * text, int len, int x_alignment, int y_alignment, Surface * surface); + +HFONT WS_Get_Font(HDC hdc, const char * face_name, int decipt_width, int decipt_height, int attributes); + +BOOL Get_Display_Rect(HWND window, LPRECT rect); + +void Prepare_Draw_Resources(void); + +void Release_Pointer_To_Host(void); +void Recapture_Pointer(void); + +extern COLORREF ODColorText; + +extern unsigned short ODRComponentMask; +extern unsigned short ODGComponentMask; +extern unsigned short ODBComponentMask; + + +/// +/// Blends a color over a display pixel. +/// +/// How much of the color to mix in, from 0 to 255. +inline unsigned short OD_Blend_Color(unsigned short pixel, unsigned short color, unsigned char alpha) +{ + unsigned blend_color_alpha = alpha; + unsigned blend_pixel_alpha = 255 - alpha; + + unsigned short r = ((((pixel & ODRComponentMask) * blend_pixel_alpha) + ((color & ODRComponentMask) * blend_color_alpha)) >> 8) & ODRComponentMask; + unsigned short g = ((((pixel & ODGComponentMask) * blend_pixel_alpha) + ((color & ODGComponentMask) * blend_color_alpha)) >> 8) & ODGComponentMask; + unsigned short b = (((pixel & ODBComponentMask) * blend_pixel_alpha) + ((color & ODBComponentMask) * blend_color_alpha)) >> 8; + return((unsigned short)(r | g | b)); +} diff --git a/code/drive.cpp b/code/drive.cpp index 538c71bc9..53acce07a 100644 --- a/code/drive.cpp +++ b/code/drive.cpp @@ -68,6 +68,7 @@ #include "inline.h" #include "overtype.h" #include "rules.h" +#include "saveload.h" #include "savestream.h" #include "tube.h" #include "unit.h" @@ -117,7 +118,7 @@ DriveLocomotionClass::DriveLocomotionClass(void) : TargetSpeed(0), TrackNumber(-1), TrackIndex(-1), - Piggybacker(NULL) + Piggybacker() { } @@ -131,68 +132,10 @@ DriveLocomotionClass::~DriveLocomotionClass(void) } -/// -/// Fetches the class identifier of whichever locomotor is driving the unit. -/// That is the identifier of the locomotor riding along on this driver when there is one, -/// and the driver's own otherwise. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK if the identifier was supplied, E_FAIL if the locomotor -/// could not be asked, or E_POINTER if no destination was supplied. -HRESULT DriveLocomotionClass::Piggyback_CLSID(CLSID * classid) -{ - if (classid == NULL) { - return(E_POINTER); - } - - if (Piggybacker != NULL) { - IPersistPtr ptr(Piggybacker); - if (ptr == NULL) { - return(E_FAIL); - } - return(ptr->GetClassID(classid)); - } - - IPersistPtr ptr(this); - if (ptr == NULL) { - return(E_FAIL); - } - return(ptr->GetClassID(classid)); -} - - -/// -/// Fetches an interface supported by this locomotor. -/// The driver answers for the piggyback interface on top of whatever the base locomotor -/// already supports. -/// -/// The identifier of the interface asked for. -/// Pointer to the interface pointer to fill in. -/// Returns with S_OK if the interface was supplied, otherwise -/// E_NOINTERFACE. -HRESULT STDMETHODCALLTYPE DriveLocomotionClass::QueryInterface(REFIID riid, LPVOID * ppvObject) -{ - HRESULT result = BASECLASS::QueryInterface(riid, ppvObject); - - if (result == E_NOINTERFACE) { - if (riid == IID_IPiggyback) { - *ppvObject = (IPiggyback*)this; - } - if (*ppvObject == NULL) { - result = E_NOINTERFACE; - } else { - AddRef(); - result = S_OK; - } - } - return(result); -} - - /// /// Lists the members this driver carries. /// A locomotor riding along on this one is a separate persistent object rather than a -/// member, so it still travels framed by OLE and is recreated as the class it was saved as. +/// member, so it travels as a record of its own and is recreated as the class it was saved as. /// /// The stream carrying the members. void DriveLocomotionClass::Serialize(SaveStreamClass & stream) @@ -220,10 +163,9 @@ void DriveLocomotionClass::Serialize(SaveStreamClass & stream) if (haspiggy) { if (stream.Is_Saving()) { - IPersistStreamPtr persist(Piggybacker); - OleSaveToStream(persist, stream.Get_Stream()); + Save_Object(stream, Piggybacker.get()); } else { - OleLoadFromStream(stream.Get_Stream(), IID_ILocomotion, (LPVOID *)&Piggybacker); + Piggybacker = Load_Locomotor(stream); } } // TrackControl -- constant tables shared by every driver. @@ -237,19 +179,15 @@ void DriveLocomotionClass::Serialize(SaveStreamClass & stream) /// A unit that must travel in some special manner -- through a tunnel, or aboard a /// carrier -- keeps its driver but lets the special locomotor move it for the duration. /// -/// The locomotor that is to take over the unit. -/// Returns with S_OK if the locomotor was taken on, E_FAIL if one is already -/// riding, or E_POINTER if none was supplied. -HRESULT STDMETHODCALLTYPE DriveLocomotionClass::Begin_Piggyback(ILocomotion *pointer) +/// The locomotor that is to take over the unit. +/// bool; Was the locomotor taken on? One already carrying a locomotor refuses. +bool DriveLocomotionClass::Begin_Piggyback(std::unique_ptr carried) { - if (pointer == NULL) { - return(E_POINTER); - } - if (Piggybacker == NULL) { - Piggybacker = pointer; - return(S_OK); + if (carried == NULL || Piggybacker != NULL) { + return(false); } - return(E_FAIL); + Piggybacker = std::move(carried); + return(true); } @@ -258,20 +196,10 @@ HRESULT STDMETHODCALLTYPE DriveLocomotionClass::Begin_Piggyback(ILocomotion *poi /// The riding locomotor is detached and given up, leaving this driver in sole charge of /// the unit once more. /// -/// Pointer to the locomotor pointer to fill in. -/// Returns with S_OK if a locomotor was handed back, S_FALSE if there was none -/// riding, or E_POINTER if no destination was supplied. -HRESULT DriveLocomotionClass::End_Piggyback(ILocomotion **pointer) +/// Returns with the locomotor that was riding, or nothing when none was. +std::unique_ptr DriveLocomotionClass::End_Piggyback(void) { - if (pointer == NULL) { - return(E_POINTER); - } - if (Piggybacker != NULL) { - *pointer = Piggybacker; - Piggybacker.Detach(); - return(S_OK); - } - return(S_FALSE); + return(std::move(Piggybacker)); } @@ -282,7 +210,7 @@ HRESULT DriveLocomotionClass::End_Piggyback(ILocomotion **pointer) /// back only once the unit has settled. /// /// bool; Is it safe to end the piggyback? -boolean DriveLocomotionClass::Is_Ok_To_End(void) +bool DriveLocomotionClass::Is_Ok_To_End(void) { if (!Is_Moving() && (Piggybacker != NULL && IsLocomotorUnlocked)) { return(true); @@ -345,7 +273,7 @@ void DriveLocomotionClass::Set_Slope(int ramp) /// when it is first placed on the map. /// /// The ramp the unit is to be sitting on. -void STDMETHODCALLTYPE DriveLocomotionClass::Force_New_Slope(int ramp) +void DriveLocomotionClass::Force_New_Slope(int ramp) { PreviousRamp = ramp; CurrentRamp = ramp; @@ -359,7 +287,7 @@ void STDMETHODCALLTYPE DriveLocomotionClass::Force_New_Slope(int ramp) /// between cells, even if it is not making any headway at this moment. /// /// bool; Is the unit under way or owing a move? -boolean STDMETHODCALLTYPE DriveLocomotionClass::Is_Moving(void) +bool DriveLocomotionClass::Is_Moving(void) { if (DestinationCoord != COORD_NONE) { return(true); @@ -377,7 +305,7 @@ boolean STDMETHODCALLTYPE DriveLocomotionClass::Is_Moving(void) /// has been given a destination but has not gotten rolling yet does not. /// /// bool; Is the unit moving right now? -boolean STDMETHODCALLTYPE DriveLocomotionClass::Is_Moving_Now(void) +bool DriveLocomotionClass::Is_Moving_Now(void) { if (LinkedTo->PrimaryFacing.Is_Rotating()) { return(true); @@ -394,7 +322,7 @@ boolean STDMETHODCALLTYPE DriveLocomotionClass::Is_Moving_Now(void) /// /// Returns with the destination coordinate, or COORD_NONE if the unit has /// nowhere it must be. -Coord STDMETHODCALLTYPE DriveLocomotionClass::Destination(void) +Coord DriveLocomotionClass::Destination(void) { return(DestinationCoord); } @@ -405,7 +333,7 @@ Coord STDMETHODCALLTYPE DriveLocomotionClass::Destination(void) /// /// Returns with the coordinate being driven toward. A unit that is not under /// way returns its current position instead. -Coord STDMETHODCALLTYPE DriveLocomotionClass::Head_To_Coord(void) +Coord DriveLocomotionClass::Head_To_Coord(void) { if (HeadToCoord != COORD_NONE) { return(HeadToCoord); @@ -420,7 +348,7 @@ Coord STDMETHODCALLTYPE DriveLocomotionClass::Head_To_Coord(void) /// raised to the deck, since that is where the vehicle will actually end up driving. /// /// The location to drive to. -void STDMETHODCALLTYPE DriveLocomotionClass::Move_To(Coord to) +void DriveLocomotionClass::Move_To(Coord to) { if (LinkedTo->StunDuration <= 0) { DestinationCoord = to; @@ -438,7 +366,7 @@ void STDMETHODCALLTYPE DriveLocomotionClass::Move_To(Coord to) /// The destination is given up and the driver begins slowing down. A train engine passes /// the order back along the line so that every car it is pulling stops with it. /// -void STDMETHODCALLTYPE DriveLocomotionClass::Stop_Moving(void) +void DriveLocomotionClass::Stop_Moving(void) { if (HeadToCoord != COORD_NONE) { if (LinkedTo->TClass->IsTrain) { @@ -489,7 +417,7 @@ BOOL DriveLocomotionClass::Is_Angled(void) const /// /// Pointer to the voxel cache key to be updated. May be NULL. /// Returns with the matrix the unit is to be rendered through. -Matrix3D STDMETHODCALLTYPE DriveLocomotionClass::Draw_Matrix(int *key) +Matrix3D DriveLocomotionClass::Draw_Matrix(int *key) { Matrix3D m; @@ -554,7 +482,7 @@ Matrix3D STDMETHODCALLTYPE DriveLocomotionClass::Draw_Matrix(int *key) /// The driver adopts the slope of the cell the vehicle appears on straight away, so that /// a unit unlimboed onto a ramp is never seen tilting itself into place. /// -void STDMETHODCALLTYPE DriveLocomotionClass::Unlimbo(void) +void DriveLocomotionClass::Unlimbo(void) { Force_New_Slope(LinkedTo->Get_Cell_Ptr()->Ramp); } @@ -584,7 +512,7 @@ void STDMETHODCALLTYPE DriveLocomotionClass::Unlimbo(void) * 09/26/1993 JLB : Created. * * 04/15/1994 JLB : Converted to member function. * *=============================================================================================*/ -boolean STDMETHODCALLTYPE DriveLocomotionClass::Process(void) +bool DriveLocomotionClass::Process(void) { Set_Slope(LinkedTo->Get_Cell_Ptr()->Ramp); @@ -771,7 +699,7 @@ void DriveLocomotionClass::Mark_Track(Coord const & headto, MarkType type) * HISTORY: * * 03/17/1995 JLB : Created. * *=============================================================================================*/ -void STDMETHODCALLTYPE DriveLocomotionClass::Force_Track(int track, Coord coord) +void DriveLocomotionClass::Force_Track(int track, Coord coord) { assert(LinkedTo->IsActive); @@ -2122,24 +2050,15 @@ bool DriveLocomotionClass::Incoming(Cell cell) /// Fetches the display layer the driving unit belongs to. /// /// Returns with LAYER_GROUND, since a driving unit travels on the ground. -LayerType STDMETHODCALLTYPE DriveLocomotionClass::In_Which_Layer(void) +LayerType DriveLocomotionClass::In_Which_Layer(void) { return(LAYER_GROUND); } -/// -/// Fetches the class identifier of this locomotor. -/// The persistence system uses this to know which locomotor to create when the unit is -/// loaded back in. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE DriveLocomotionClass::GetClassID(CLSID * retval) +ClassID DriveLocomotionClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_DriveLocomotion; - return(S_OK); + return(ClassID_DriveLocomotion); } @@ -2148,7 +2067,7 @@ HRESULT STDMETHODCALLTYPE DriveLocomotionClass::GetClassID(CLSID * retval) /// A driving vehicle sits at the depth of the ground it is standing on. /// /// Returns with the adjustment to apply to the unit's draw depth. -int STDMETHODCALLTYPE DriveLocomotionClass::Z_Adjust(void) +int DriveLocomotionClass::Z_Adjust(void) { return(0); } @@ -2158,7 +2077,7 @@ int STDMETHODCALLTYPE DriveLocomotionClass::Z_Adjust(void) /// Fetches the depth gradient the unit is to be drawn with. /// /// Returns with the gradient the base locomotor asks for. -ZGradientType STDMETHODCALLTYPE DriveLocomotionClass::Z_Gradient(void) +ZGradientType DriveLocomotionClass::Z_Gradient(void) { return(BASECLASS::Z_Gradient()); } @@ -2188,7 +2107,7 @@ bool DriveLocomotionClass::Abandon_Navigation(void) /// be told about every cell of the track it is committed to. /// /// The MarkType to apply to the cells occupied. -void STDMETHODCALLTYPE DriveLocomotionClass::Mark_All_Occupation_Bits(int mark) +void DriveLocomotionClass::Mark_All_Occupation_Bits(int mark) { if (HeadToCoord != COORD_NONE) { Mark_Track(HeadToCoord, (MarkType)mark); @@ -2204,7 +2123,7 @@ void STDMETHODCALLTYPE DriveLocomotionClass::Mark_All_Occupation_Bits(int mark) /// /// The location to test against. /// bool; Is the unit moving there? -boolean STDMETHODCALLTYPE DriveLocomotionClass::Is_Moving_Here(Coord to) +bool DriveLocomotionClass::Is_Moving_Here(Coord to) { Coord coord = Head_To_Coord(); @@ -2250,7 +2169,7 @@ boolean STDMETHODCALLTYPE DriveLocomotionClass::Is_Moving_Here(Coord to) /// such a hop is due, but performs none of it. /// /// bool; Will the driver jump tracks? -boolean STDMETHODCALLTYPE DriveLocomotionClass::Will_Jump_Tracks(void) +bool DriveLocomotionClass::Will_Jump_Tracks(void) { /// This repeats the track jump test that While_Moving performs. assert(LinkedTo->IsActive); @@ -2304,7 +2223,7 @@ boolean STDMETHODCALLTYPE DriveLocomotionClass::Will_Jump_Tracks(void) /// While locked, this driver will not report itself ready to end a piggyback, so a /// temporary locomotor riding on top of it keeps control of the unit. /// -void STDMETHODCALLTYPE DriveLocomotionClass::Lock(void) +void DriveLocomotionClass::Lock(void) { IsLocomotorUnlocked = false; } @@ -2315,7 +2234,7 @@ void STDMETHODCALLTYPE DriveLocomotionClass::Lock(void) /// This is the counterpart to Lock. The driver may once again report itself ready to /// end a piggyback. /// -void STDMETHODCALLTYPE DriveLocomotionClass::Unlock(void) +void DriveLocomotionClass::Unlock(void) { IsLocomotorUnlocked = true; } @@ -2326,7 +2245,7 @@ void STDMETHODCALLTYPE DriveLocomotionClass::Unlock(void) /// /// Returns with the track control number, or -1 if the unit is not on a /// track. -int STDMETHODCALLTYPE DriveLocomotionClass::Get_Track_Number(void) +int DriveLocomotionClass::Get_Track_Number(void) { return(TrackNumber); } @@ -2337,7 +2256,7 @@ int STDMETHODCALLTYPE DriveLocomotionClass::Get_Track_Number(void) /// /// Returns with the index into the track the unit has reached, or -1 if the /// unit is not following one. -int STDMETHODCALLTYPE DriveLocomotionClass::Get_Track_Index(void) +int DriveLocomotionClass::Get_Track_Index(void) { return(TrackIndex); } @@ -2347,32 +2266,12 @@ int STDMETHODCALLTYPE DriveLocomotionClass::Get_Track_Index(void) /// Fetches the movement the driver has banked up along its track. /// /// Returns with the accumulated movement not yet spent advancing the unit. -int STDMETHODCALLTYPE DriveLocomotionClass::Get_Speed_Accum(void) +int DriveLocomotionClass::Get_Speed_Accum(void) { return(SpeedAccum); } -/// -/// Adds a reference to this locomotor. -/// -/// Returns with the reference count once the new reference is counted. -ULONG STDMETHODCALLTYPE DriveLocomotionClass::AddRef(void) -{ - return(BASECLASS::AddRef()); -} - - -/// -/// Releases a reference to this locomotor. -/// -/// Returns with the reference count remaining after the release. -ULONG STDMETHODCALLTYPE DriveLocomotionClass::Release(void) -{ - return(BASECLASS::Release()); -} - - /*************************************************************************** ** Smooth turn track tables. These are coordinate offsets from the center ** of the destination cell. These are the raw tracks that are modified diff --git a/code/drive.h b/code/drive.h index 5011c55ce..f830beda6 100644 --- a/code/drive.h +++ b/code/drive.h @@ -40,6 +40,8 @@ #include "matrix3d.h" #include "timer.h" +#include + #include "mark.hh" /**************************************************************************** @@ -58,43 +60,39 @@ class DriveLocomotionClass : public LocomotionClass, public IPiggyback DriveLocomotionClass(void); virtual ~DriveLocomotionClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; - virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID * ppvObject) override; - virtual ULONG STDMETHODCALLTYPE AddRef(void) override; - virtual ULONG STDMETHODCALLTYPE Release(void) override; - - virtual boolean STDMETHODCALLTYPE Is_Moving(void) override; - virtual Coord STDMETHODCALLTYPE Destination(void) override; - virtual Coord STDMETHODCALLTYPE Head_To_Coord(void) override; - virtual Matrix3D STDMETHODCALLTYPE Draw_Matrix(int *key) override; - virtual int STDMETHODCALLTYPE Z_Adjust(void) override; - virtual ZGradientType STDMETHODCALLTYPE Z_Gradient(void) override; - virtual boolean STDMETHODCALLTYPE Process(void) override; - virtual void STDMETHODCALLTYPE Move_To(Coord to) override; - virtual void STDMETHODCALLTYPE Stop_Moving(void) override; - virtual void STDMETHODCALLTYPE Do_Turn(DirType coord) override; - virtual void STDMETHODCALLTYPE Unlimbo(void) override; - virtual void STDMETHODCALLTYPE Force_Track(int track, Coord coord) override; - virtual LayerType STDMETHODCALLTYPE In_Which_Layer(void) override; - virtual void STDMETHODCALLTYPE Force_New_Slope(int ramp) override; - virtual boolean STDMETHODCALLTYPE Is_Moving_Now(void) override; - virtual void STDMETHODCALLTYPE Mark_All_Occupation_Bits(int mark) override; - virtual boolean STDMETHODCALLTYPE Is_Moving_Here(Coord to) override; - virtual boolean STDMETHODCALLTYPE Will_Jump_Tracks(void) override; - virtual void STDMETHODCALLTYPE Lock(void) override; - virtual void STDMETHODCALLTYPE Unlock(void) override; - virtual int STDMETHODCALLTYPE Get_Track_Number(void) override; - virtual int STDMETHODCALLTYPE Get_Track_Index(void) override; - virtual int STDMETHODCALLTYPE Get_Speed_Accum(void) override; - - virtual HRESULT STDMETHODCALLTYPE Begin_Piggyback(ILocomotion * pointer) override; - virtual HRESULT STDMETHODCALLTYPE End_Piggyback(ILocomotion ** pointer) override; - virtual boolean STDMETHODCALLTYPE Is_Ok_To_End(void) override; - virtual HRESULT STDMETHODCALLTYPE Piggyback_CLSID(GUID * classid) override; - virtual boolean STDMETHODCALLTYPE Is_Piggybacking(void) override {return(Piggybacker != NULL);} + + virtual bool Is_Moving(void) override; + virtual Coord Destination(void) override; + virtual Coord Head_To_Coord(void) override; + virtual Matrix3D Draw_Matrix(int *key) override; + virtual int Z_Adjust(void) override; + virtual ZGradientType Z_Gradient(void) override; + virtual bool Process(void) override; + virtual void Move_To(Coord to) override; + virtual void Stop_Moving(void) override; + virtual void Do_Turn(DirType coord) override; + virtual void Unlimbo(void) override; + virtual void Force_Track(int track, Coord coord) override; + virtual LayerType In_Which_Layer(void) override; + virtual void Force_New_Slope(int ramp) override; + virtual bool Is_Moving_Now(void) override; + virtual void Mark_All_Occupation_Bits(int mark) override; + virtual bool Is_Moving_Here(Coord to) override; + virtual bool Will_Jump_Tracks(void) override; + virtual void Lock(void) override; + virtual void Unlock(void) override; + virtual int Get_Track_Number(void) override; + virtual int Get_Track_Index(void) override; + virtual int Get_Speed_Accum(void) override; + + virtual bool Begin_Piggyback(std::unique_ptr carried) override; + virtual std::unique_ptr End_Piggyback(void) override; + virtual bool Is_Ok_To_End(void) override; + virtual bool Is_Piggybacking(void) override {return(Piggybacker != NULL);} /*--------------------------------------------------------------------- ** Member function prototypes. @@ -245,7 +243,7 @@ class DriveLocomotionClass : public LocomotionClass, public IPiggyback * driver answers for it rather than for itself. If NULL, this driver is in sole * charge of the unit. */ - ILocomotionPtr Piggybacker; + std::unique_ptr Piggybacker; /*--------------------------------------------------------------------- ** Member function prototypes. diff --git a/code/droppod.cpp b/code/droppod.cpp index 0e74e2fc8..dbb375f2b 100644 --- a/code/droppod.cpp +++ b/code/droppod.cpp @@ -23,6 +23,7 @@ #include "house.h" #include "map.h" #include "rules.h" +#include "saveload.h" #include "savestream.h" #include "sun.h" #include "weapon.h" @@ -38,7 +39,7 @@ DropPodLocomotionClass::DropPodLocomotionClass(void) : BASECLASS(), Direction(DPOD_DIR_NE), DestinationCoord(COORD_NONE), - Piggybacker(NULL) + Piggybacker() { } @@ -55,7 +56,7 @@ DropPodLocomotionClass::~DropPodLocomotionClass(void) /// Is the drop pod in motion? /// A pod exists only for the duration of its fall, so it always reports movement. /// -boolean STDMETHODCALLTYPE DropPodLocomotionClass::Is_Moving(void) +bool DropPodLocomotionClass::Is_Moving(void) { return(true); } @@ -66,7 +67,7 @@ boolean STDMETHODCALLTYPE DropPodLocomotionClass::Is_Moving(void) /// /// Returns with the landing coordinate, or COORD_NONE if no destination has /// been assigned yet. -Coord STDMETHODCALLTYPE DropPodLocomotionClass::Destination(void) +Coord DropPodLocomotionClass::Destination(void) { return(DestinationCoord); } @@ -79,7 +80,7 @@ Coord STDMETHODCALLTYPE DropPodLocomotionClass::Destination(void) /// passenger is unlimboed, or destroyed along with its surroundings if there is nowhere /// for it to stand. /// -boolean STDMETHODCALLTYPE DropPodLocomotionClass::Process(void) +bool DropPodLocomotionClass::Process(void) { Coord coord = LinkedTo->PositionCoord; Coord smoke_coord = coord; @@ -117,8 +118,10 @@ boolean STDMETHODCALLTYPE DropPodLocomotionClass::Process(void) coord = linked->PositionCoord; linked->Limbo(); - AddRef(); - End_Piggyback(&LinkedTo->Locomotion); + // Handing the carried locomotor back makes this pod unowned, so it holds itself + // until the landing is finished and is deleted on return. + std::unique_ptr const self = std::move(LinkedTo->Locomotion); + LinkedTo->Locomotion = End_Piggyback(); if (!linked->Unlimbo(coord, DIR_N)) { Explosion_Damage(coord, 100, LinkedTo, Rule->C4Warhead); @@ -132,7 +135,6 @@ boolean STDMETHODCALLTYPE DropPodLocomotionClass::Process(void) linked->Commence(); linked->Scatter(COORD_NONE); } - Release(); } else { LinkedTo->PositionCoord = coord; WeaponTypeClass const * weapon = Rule->DropPodWeapon; @@ -163,7 +165,7 @@ boolean STDMETHODCALLTYPE DropPodLocomotionClass::Process(void) /// has a destination ignores any later request. /// /// The coordinate the pod should land on. -void STDMETHODCALLTYPE DropPodLocomotionClass::Move_To(Coord to) +void DropPodLocomotionClass::Move_To(Coord to) { if (DestinationCoord == COORD_NONE) { @@ -213,22 +215,16 @@ void STDMETHODCALLTYPE DropPodLocomotionClass::Move_To(Coord to) } -/// -/// Fetches the class ID that this locomotor is persisted under. -/// -/// Returns with S_OK, or E_POINTER if no return pointer was supplied. -HRESULT STDMETHODCALLTYPE DropPodLocomotionClass::GetClassID(CLSID * retval) +ClassID DropPodLocomotionClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_BallisticLocomotion; - return(S_OK); + return(ClassID_BallisticLocomotion); } /// /// Lists the members this drop pod locomotor carries. /// The locomotor set aside while the pod descends is a separate persistent object rather -/// than a member, so it still travels framed by OLE and is recreated as the class it was +/// than a member, so it travels as a record of its own and is recreated as the class it was /// saved as. /// /// The stream carrying the members. @@ -244,10 +240,9 @@ void DropPodLocomotionClass::Serialize(SaveStreamClass & stream) if (haspiggy) { if (stream.Is_Saving()) { - IPersistStreamPtr persist(Piggybacker); - OleSaveToStream(persist, stream.Get_Stream()); + Save_Object(stream, Piggybacker.get()); } else { - OleLoadFromStream(stream.Get_Stream(), IID_ILocomotion, (LPVOID *)&Piggybacker); + Piggybacker = Load_Locomotor(stream); } } } @@ -257,7 +252,7 @@ void DropPodLocomotionClass::Serialize(SaveStreamClass & stream) /// Stops the pod's descent. /// A pod cannot be halted in mid air, so this request is quietly ignored. /// -void STDMETHODCALLTYPE DropPodLocomotionClass::Stop_Moving(void) +void DropPodLocomotionClass::Stop_Moving(void) { // empty } @@ -268,18 +263,15 @@ void STDMETHODCALLTYPE DropPodLocomotionClass::Stop_Moving(void) /// The drop pod holds on to the locomotor it displaces so that the object can be given /// it back when the pod touches down. /// -/// The locomotor to carry. -/// Returns with S_OK, or E_FAIL if something is already being carried. -HRESULT STDMETHODCALLTYPE DropPodLocomotionClass::Begin_Piggyback(ILocomotion * pointer) +/// The locomotor that is to take over the unit. +/// bool; Was the locomotor taken on? One already carrying a locomotor refuses. +bool DropPodLocomotionClass::Begin_Piggyback(std::unique_ptr carried) { - if (pointer == NULL) { - return(E_POINTER); + if (carried == NULL || Piggybacker != NULL) { + return(false); } - if (Piggybacker == NULL) { - Piggybacker = pointer; - return(S_OK); - } - return(E_FAIL); + Piggybacker = std::move(carried); + return(true); } @@ -288,19 +280,10 @@ HRESULT STDMETHODCALLTYPE DropPodLocomotionClass::Begin_Piggyback(ILocomotion * /// The pod gives up its hold without destroying the locomotor, so the object can resume /// using it once the pod has landed. /// -/// Pointer to the location that receives the carried locomotor. -/// Returns with S_OK, or S_FALSE if nothing was being carried. -HRESULT STDMETHODCALLTYPE DropPodLocomotionClass::End_Piggyback(ILocomotion ** pointer) +/// Returns with the locomotor that was riding, or nothing when none was. +std::unique_ptr DropPodLocomotionClass::End_Piggyback(void) { - if (pointer == NULL) { - return(E_POINTER); - } - if (Piggybacker != NULL) { - *pointer = Piggybacker; - Piggybacker.Detach(); - return(S_OK); - } - return(S_FALSE); + return(std::move(Piggybacker)); } @@ -309,7 +292,7 @@ HRESULT STDMETHODCALLTYPE DropPodLocomotionClass::End_Piggyback(ILocomotion ** p /// The carried locomotor may only be given control back once the pod has come to rest. /// /// bool; May the carried locomotor take over again? -boolean STDMETHODCALLTYPE DropPodLocomotionClass::Is_Ok_To_End(void) +bool DropPodLocomotionClass::Is_Ok_To_End(void) { if (!Is_Moving() && Piggybacker != NULL) { return(true); @@ -318,77 +301,22 @@ boolean STDMETHODCALLTYPE DropPodLocomotionClass::Is_Ok_To_End(void) } -/// -/// Fetches an interface pointer from the drop pod locomotor. -/// This routine extends the base locomotor's interface set with IPiggyback, which is how -/// the pod carries the object's real locomotor while it falls. -/// -/// Returns with S_OK, or E_NOINTERFACE if this object does not offer the -/// interface asked for. -HRESULT STDMETHODCALLTYPE DropPodLocomotionClass::QueryInterface(REFIID riid, LPVOID * ppvObject) -{ - HRESULT result = BASECLASS::QueryInterface(riid, ppvObject); - - if (result == E_NOINTERFACE) { - if (riid == IID_IPiggyback) { - *ppvObject = (IPiggyback*)this; - } - if (*ppvObject == NULL) { - result = E_NOINTERFACE; - } else { - AddRef(); - result = S_OK; - } - } - return(result); -} - - /// /// Determines which display layer the pod belongs in. /// A pod is always falling, so it draws along with the other airborne objects right up /// until it lands and gives its object back. /// -LayerType STDMETHODCALLTYPE DropPodLocomotionClass::In_Which_Layer(void) +LayerType DropPodLocomotionClass::In_Which_Layer(void) { return(LAYER_AIR); } -/// -/// Fetches the class ID of the locomotor being carried. -/// The save system uses this to record which locomotor is to be restored underneath the -/// drop pod. When nothing is being carried, the pod supplies its own class ID instead. -/// -/// Returns with S_OK, or an error code if the class ID could not be -/// determined. -HRESULT STDMETHODCALLTYPE DropPodLocomotionClass::Piggyback_CLSID(GUID * classid) -{ - if (classid == NULL) { - return(E_POINTER); - } - - if (Piggybacker != NULL) { - IPersistPtr ptr(Piggybacker); - if (ptr == NULL) { - return(E_FAIL); - } - return(ptr->GetClassID(classid)); - } - - IPersistPtr ptr(this); - if (ptr == NULL) { - return(E_FAIL); - } - return(ptr->GetClassID(classid)); -} - - /// /// Fetches the drawing code for the drop pod. /// The renderer uses this to choose the artwork that suits the pod's approach. /// -int STDMETHODCALLTYPE DropPodLocomotionClass::Drawing_Code(void) +int DropPodLocomotionClass::Drawing_Code(void) { return((unsigned)Direction % 2); } diff --git a/code/droppod.h b/code/droppod.h index ecd9da0a7..1727e5354 100644 --- a/code/droppod.h +++ b/code/droppod.h @@ -16,6 +16,8 @@ #include "ipiggy.h" #include "loco.h" +#include + class DropPodLocomotionClass : public LocomotionClass, public IPiggyback { @@ -29,27 +31,23 @@ class DropPodLocomotionClass : public LocomotionClass, public IPiggyback DropPodLocomotionClass(void); virtual ~DropPodLocomotionClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; - virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID * ppvObject) override; - virtual ULONG STDMETHODCALLTYPE AddRef(void) override {return(BASECLASS::AddRef());} - virtual ULONG STDMETHODCALLTYPE Release(void) override {return(BASECLASS::Release());} - virtual boolean STDMETHODCALLTYPE Is_Moving(void) override; - virtual Coord STDMETHODCALLTYPE Destination(void) override; - virtual void STDMETHODCALLTYPE Move_To(Coord to) override; - virtual void STDMETHODCALLTYPE Stop_Moving(void) override; - virtual boolean STDMETHODCALLTYPE Process(void) override; - virtual LayerType STDMETHODCALLTYPE In_Which_Layer(void) override; - virtual int STDMETHODCALLTYPE Drawing_Code(void) override; + virtual bool Is_Moving(void) override; + virtual Coord Destination(void) override; + virtual void Move_To(Coord to) override; + virtual void Stop_Moving(void) override; + virtual bool Process(void) override; + virtual LayerType In_Which_Layer(void) override; + virtual int Drawing_Code(void) override; - virtual HRESULT STDMETHODCALLTYPE Begin_Piggyback(ILocomotion * pointer) override; - virtual HRESULT STDMETHODCALLTYPE End_Piggyback(ILocomotion ** pointer) override; - virtual boolean STDMETHODCALLTYPE Is_Ok_To_End(void) override; - virtual HRESULT STDMETHODCALLTYPE Piggyback_CLSID(GUID * classid) override; - virtual boolean STDMETHODCALLTYPE Is_Piggybacking(void) override {return(Piggybacker != NULL);} + virtual bool Begin_Piggyback(std::unique_ptr carried) override; + virtual std::unique_ptr End_Piggyback(void) override; + virtual bool Is_Ok_To_End(void) override; + virtual bool Is_Piggybacking(void) override {return(Piggybacker != NULL);} private: enum DropPodDirType { @@ -78,5 +76,5 @@ class DropPodLocomotionClass : public LocomotionClass, public IPiggyback * handed back the moment the pod touches ground, so that the object resumes moving * the way its type normally does. */ - ILocomotionPtr Piggybacker; + std::unique_ptr Piggybacker; }; diff --git a/code/dropship.cpp b/code/dropship.cpp index 960b13d19..b4e02ed6e 100644 --- a/code/dropship.cpp +++ b/code/dropship.cpp @@ -30,6 +30,7 @@ #include "dsurface.h" #include "font.h" #include "globals.h" +#include "hostclock.h" #include "house.h" #include "infatype.h" #include "keyboard.h" @@ -247,7 +248,7 @@ struct CrossDissolveEffect FromSurface(from_surface), ToSurface(to_surface), Alpha(0), - StartTime(::timeGetTime()), + StartTime(Host_Milliseconds()), FromBlitter(from_blitter), ToBlitter(to_blitter), WasDrawn(false) @@ -557,7 +558,7 @@ void Dropship_Screen(void) DynamicVectorClass button_fades; - unsigned int money_display_time = ::timeGetTime(); + unsigned int money_display_time = Host_Milliseconds(); unsigned int screen_start_time = money_display_time; int loadout_anim_frame = 0; unsigned int last_input_time = 0; @@ -613,7 +614,7 @@ void Dropship_Screen(void) KeyNumType input = button_list->Input(); if (!recent_click && (input & KN_BUTTON) != 0) { input = (KeyNumType)(input & ~KN_BUTTON); - last_input_time = ::timeGetTime(); + last_input_time = Host_Milliseconds(); if (input <= _cameo_count && selected_count < dropship_count * SLOT_PER_DROPSHIP) { int candidate_index = cameo_top + input - 1; @@ -650,13 +651,13 @@ void Dropship_Screen(void) int light_row = (input - 1) / 2; if (light_frame[light_row] == (unsigned int)-1) { light_frame[light_row] = 0; - light_start[light_row] = ::timeGetTime(); + light_start[light_row] = Host_Milliseconds(); } force_light_redraw = true; for (j = 0; ; ++j) { if (j >= button_fades.Count()) { - ButtonFadeEffect *fade = new ButtonFadeEffect(::timeGetTime(), candidate_index, 127.0f, -1.0f, false); + ButtonFadeEffect *fade = new ButtonFadeEffect(Host_Milliseconds(), candidate_index, 127.0f, -1.0f, false); if (usage_index != -1 && Scen->AllowableUnitCounts[usage_index] >= Scen->AllowableUnitMaximums[usage_index]) { force_info_redraw = true; fade->StopAtLow = true; @@ -709,7 +710,7 @@ void Dropship_Screen(void) if (Scen->AllowableUnitCounts[usage_index] >= Scen->AllowableUnitMaximums[usage_index]) { for (j = 0; j < candidates.Count(); ++j) { if (candidates[j] == selections[remove_index]) { - ButtonFadeEffect *fade = new ButtonFadeEffect(::timeGetTime(), j, 127.0f, -1.0f, false); + ButtonFadeEffect *fade = new ButtonFadeEffect(Host_Milliseconds(), j, 127.0f, -1.0f, false); fade->Direction = 1.0f; fade->Alpha = 63.0f; button_fades.Add(fade); @@ -780,7 +781,7 @@ void Dropship_Screen(void) for (i = 0; i < ARRAY_SIZE(_green_light_ys); ++i) { if (light_frame[i] != (unsigned int)-1) { int frame = light_frame[i]; - int next_frame = (timeGetTime() - light_start[i]) / _light_rate; + int next_frame = (Host_Milliseconds() - light_start[i]) / _light_rate; if (next_frame != frame || force_light_redraw) { int light_y = y + _green_light_ys[i]; int light_x = x + _light_x; @@ -816,14 +817,14 @@ void Dropship_Screen(void) ButtonFadeEffect *effect = button_fades[i]; int alpha; if (effect->Direction < 0.0f) { - alpha = 127 - (_fade_rate * timeGetTime() - _fade_rate * effect->StartTime) / _fade_scale; + alpha = 127 - (_fade_rate * Host_Milliseconds() - _fade_rate * effect->StartTime) / _fade_scale; if (alpha < _fade_low) { alpha = _fade_low; effect->Direction = 1.0f; - effect->StartTime = timeGetTime(); + effect->StartTime = Host_Milliseconds(); } } else { - alpha = (_fade_rate * timeGetTime() - _fade_rate * effect->StartTime) / _fade_scale + _fade_low; + alpha = (_fade_rate * Host_Milliseconds() - _fade_rate * effect->StartTime) / _fade_scale + _fade_low; if (alpha > 127) { alpha = 127; } @@ -867,7 +868,7 @@ void Dropship_Screen(void) --j; } - int alpha = std::min(255ul, (_dissolve_rate * timeGetTime() - _dissolve_rate * effect->StartTime) / _dissolve_scale); + int alpha = std::min(255u, (_dissolve_rate * Host_Milliseconds() - _dissolve_rate * effect->StartTime) / _dissolve_scale); if (alpha != effect->Alpha || overlap_drawn) { effect->Alpha = alpha; @@ -899,17 +900,17 @@ void Dropship_Screen(void) } } - recent_click = (::timeGetTime() - last_input_time) < _click_delay; + recent_click = (Host_Milliseconds() - last_input_time) < _click_delay; int loadout_count = loadout_shape->Get_Count(); - int next_loadout_frame = ((::timeGetTime() - screen_start_time) / _loadout_rate) % loadout_count; + int next_loadout_frame = ((Host_Milliseconds() - screen_start_time) / _loadout_rate) % loadout_count; if (next_loadout_frame != loadout_anim_frame) { loadout_anim_frame = next_loadout_frame; Draw_Shape(*HiddenSurface, *drawer_dropship, loadout_shape, loadout_anim_frame, Point2D(0, 0), Rect(Point2D(x, y) + Point2D(_loadout_x, _loadout_y), loadout_shape->Get_Width(), loadout_shape->Get_Height()), SHAPE_NORMAL); redraw = true; } - unsigned int now = ::timeGetTime(); + unsigned int now = Host_Milliseconds(); if ((unsigned int)money != money_display) { unsigned int elapsed = now - money_display_time; if (elapsed >= _money_rate) { @@ -933,7 +934,7 @@ void Dropship_Screen(void) } if (pilot_frame >= 0) { - int next_pilot_frame = (::timeGetTime() - pilot_start_time) / _pilot_rate; + int next_pilot_frame = (Host_Milliseconds() - pilot_start_time) / _pilot_rate; if (next_pilot_frame != pilot_frame) { if (next_pilot_frame < pilotlight_shape->Get_Count()) { pilot_frame = next_pilot_frame; @@ -950,7 +951,7 @@ void Dropship_Screen(void) if (pilot_timer == 0) { if (Scen->RandomNumber(0, INT_MAX - 1) / (double)(INT_MAX - 1) < _pilot_chance) { pilot_frame = 0; - pilot_start_time = ::timeGetTime(); + pilot_start_time = Host_Milliseconds(); Draw_Shape(*HiddenSurface, *drawer_dropship, pilotlight_shape, 0, Point2D(0, 0), Rect(x + _pilot_x, y + _pilot_y, pilotlight_shape->Get_Width(), pilotlight_shape->Get_Height()), SHAPE_NORMAL); redraw = true; } diff --git a/code/dsurface.cpp b/code/dsurface.cpp index 4f7e1d7c3..30039be49 100644 --- a/code/dsurface.cpp +++ b/code/dsurface.cpp @@ -55,7 +55,9 @@ #include "video.h" #include +#include #include +#include #include extern bool GameInFocus; @@ -111,6 +113,16 @@ DSurface::DSurface(int width, int height) : GDIBuffer(NULL), Pitch(0) { +#ifndef _WIN32 + /* + * The DIB section exists so that GDI can draw into the same pixels the software + * blitter does. Where there is no GDI, the pixels are ordinary memory: the rows are + * kept four-byte aligned so that the blitter sees the pitch it does on Windows. + */ + Pitch = ((width * 2) + 3) & ~3; + GDIBuffer = new(std::nothrow) unsigned char[(std::size_t)Pitch * (std::size_t)height](); + return; +#else /* * BITMAPINFO carries room for a single color entry, but a bitfields bitmap is * described by three masks following the header, so the header is declared with @@ -159,6 +171,7 @@ DSurface::DSurface(int width, int height) : } else { Pitch = width * 2; } +#endif } @@ -178,6 +191,11 @@ DSurface::DSurface(int width, int height) : *=============================================================================================*/ DSurface::~DSurface(void) { +#ifndef _WIN32 + delete[] (unsigned char *)GDIBuffer; + GDIBuffer = NULL; + return; +#else /* * GDI will not free a bitmap that is still selected into a context, so the one the * context started with has to go back first. @@ -197,6 +215,7 @@ DSurface::~DSurface(void) } GDIBuffer = NULL; +#endif } diff --git a/code/egos.cpp b/code/egos.cpp index 8c00d0db5..9746ae5e6 100644 --- a/code/egos.cpp +++ b/code/egos.cpp @@ -56,12 +56,11 @@ #include "gscreen.h" #include "language/language.h" #include "misc.h" -#include "ownrdraw.h" +#include "drawhelp.h" #include "scheme.h" #include "theme.h" #include "utf8.h" #include "vector.h" -#include "windlg.h" #include "color.hh" #include "dialog.hh" diff --git a/code/empulse.cpp b/code/empulse.cpp index 501892e45..964bd2293 100644 --- a/code/empulse.cpp +++ b/code/empulse.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "empulse.h" @@ -283,18 +282,9 @@ void EMPulseClass::Compute_CRC(CRCEngine &crc) const } -/// -/// Fetches the class identifier used to persist this object. -/// The save system writes this identifier ahead of the object data so that the loader -/// knows what kind of object to reconstruct. -/// -/// Pointer to the buffer that will receive the class identifier. -/// Returns with S_OK, or E_POINTER if no buffer was supplied. -HRESULT STDMETHODCALLTYPE EMPulseClass::GetClassID(CLSID * retval) +ClassID EMPulseClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_EMPulseClass; - return(S_OK); + return(ClassID_EMPulseClass); } diff --git a/code/empulse.h b/code/empulse.h index c059e0d4a..d04dc713f 100644 --- a/code/empulse.h +++ b/code/empulse.h @@ -30,7 +30,7 @@ class EMPulseClass : public AbstractClass virtual RTTIType Fetch_RTTI(void) const override {return(RTTI_EMPULSE);} - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/enviro.cpp b/code/enviro.cpp index 2f6cfe9aa..ab82c1097 100644 --- a/code/enviro.cpp +++ b/code/enviro.cpp @@ -110,12 +110,11 @@ void EnvironmentClass::Restore(void) /// restored, before the scenario itself is brought back. /// /// Returns with the result reported by the stream read. -HRESULT EnvironmentClass::Load(IStream * stream) +bool EnvironmentClass::Load(SaveStreamClass & stream) { - SaveStreamClass savestream(stream, SaveStreamClass::MODE_LOAD); - savestream.Set_Context("EnvironmentClass"); - Serialize(savestream); - return(savestream.Result()); + stream.Set_Context("EnvironmentClass"); + Serialize(stream); + return(!stream.Was_Error()); } @@ -123,11 +122,10 @@ HRESULT EnvironmentClass::Load(IStream * stream) /// Writes the carry over environment out to a save game. /// /// Returns with the result reported by the stream write. -HRESULT EnvironmentClass::Save(IStream * stream) +bool EnvironmentClass::Save(SaveStreamClass & stream) { - SaveStreamClass savestream(stream, SaveStreamClass::MODE_SAVE); - Serialize(savestream); - return(savestream.Result()); + Serialize(stream); + return(!stream.Was_Error()); } diff --git a/code/enviro.h b/code/enviro.h index 01cf06eec..89b46fa4c 100644 --- a/code/enviro.h +++ b/code/enviro.h @@ -13,7 +13,6 @@ #include "diff.hh" -#include class SaveStreamClass; @@ -26,8 +25,8 @@ class EnvironmentClass void Store(void); void Restore(void); - HRESULT Load(IStream * stream); - HRESULT Save(IStream * stream); + bool Load(SaveStreamClass & stream); + bool Save(SaveStreamClass & stream); void Serialize(SaveStreamClass & stream); diff --git a/code/event.cpp b/code/event.cpp index de3ffbba6..62a6d2164 100644 --- a/code/event.cpp +++ b/code/event.cpp @@ -639,6 +639,7 @@ EventClass::EventClass(int index, unsigned char type, void * ptr, unsigned int s if (index >= 0) { ID = index; Type = type; + Data.Variable.Slot = 0; Data.Variable.Pointer = ptr; Data.Variable.Size = size; Frame = ::Frame; diff --git a/code/event.h b/code/event.h index 5a3aaf575..ca3d4bb2e 100644 --- a/code/event.h +++ b/code/event.h @@ -42,6 +42,7 @@ #include "mph.hh" #include "speed.hh" +#include #include /* @@ -213,8 +214,9 @@ class EventClass ** bloating the size of this union (and thus all other event types). */ struct { + std::uint32_t Slot; + std::uint32_t Size; void * Pointer; - unsigned int Size; } Variable; // @@ -267,3 +269,16 @@ class EventClass static char const * EventNames[LAST_EVENT]; }; #pragma pack(pop) + +// A whole event travels in a network packet and a replay file, so its record size and the +// position of every field the packet reader indexes are fixed by the format. The Variable arm +// keeps the four-byte payload slot the record has always reserved and carries the live pointer +// past the bytes ADDPLAYER puts on the wire, so Size stays at offset 4 at every pointer width. +static_assert(sizeof(EventClass) == 46, "Event record layout changed"); +static_assert(offsetof(EventClass, Frame) == 1, "Event record layout changed"); +static_assert(offsetof(EventClass, IsExecuted) == 5, "Event record layout changed"); +static_assert(offsetof(EventClass, ID) == 6, "Event record layout changed"); +static_assert(offsetof(EventClass, Data) == 10, "Event record layout changed"); +static_assert(sizeof(EventClass::Data) == 36, "Event record layout changed"); +static_assert(offsetof(EventClass, Data.Variable.Size) == 14, "ADDPLAYER wire offset changed"); +static_assert(offsetof(EventClass, Data.Variable.Pointer) == 18, "ADDPLAYER wire offset changed"); diff --git a/code/except.h b/code/except.h index 1474442f9..f309bc927 100644 --- a/code/except.h +++ b/code/except.h @@ -35,7 +35,11 @@ #include "win.h" +#ifdef _WIN32 #include +#else +#define _Printf_format_string_ +#endif // Posted to the main window so that a requested test fault happens inside window procedure // dispatch, which the operating system unwinds differently from an ordinary call. @@ -53,6 +57,10 @@ #define _Printf_format_string_ +// The window procedure switches on this message whether or not the handler that posts it +// was built, so the two branches have to agree on its value. +#define WM_EXCEPTION_TEST (WM_APP + 0x54) + #endif void Install_Exception_Handler(void); diff --git a/code/factory.cpp b/code/factory.cpp index 2ce156040..efe17eec9 100644 --- a/code/factory.cpp +++ b/code/factory.cpp @@ -47,7 +47,6 @@ * FactoryClass::~FactoryClass -- Default destructor for factory objects. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "factory.h" @@ -632,17 +631,9 @@ bool FactoryClass::Completed(void) } -/// -/// Fetches the class identifier for a factory. -/// This routine is part of the persistence interface. The save game loader uses the -/// identifier to know what kind of object to create before handing it the stream. -/// -/// Returns with S_OK, or E_POINTER if no return location was supplied. -HRESULT STDMETHODCALLTYPE FactoryClass::GetClassID(CLSID * retval) +ClassID FactoryClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_FactoryClass; - return(S_OK); + return(ClassID_FactoryClass); } diff --git a/code/factory.h b/code/factory.h index 7886611ef..473c80021 100644 --- a/code/factory.h +++ b/code/factory.h @@ -54,7 +54,7 @@ class FactoryClass : public AbstractClass, private StageClass FactoryClass(void); ~FactoryClass(void); - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/file.cpp b/code/file.cpp new file mode 100644 index 000000000..d2e727d33 --- /dev/null +++ b/code/file.cpp @@ -0,0 +1,72 @@ +#include "file.h" +#include + +#ifndef _WIN32 +static void Resolve_File_Single(char* fname) +{ + Find_File_Data* ffblk; + + ffblk = Find_File_Data::CreateFindData(); + + if (ffblk == nullptr) { + return; + } + + size_t name_len = strlen(fname); + + if (ffblk->FindFirst(fname) && name_len == strlen(ffblk->GetFullName())) { + strncpy(fname, ffblk->GetFullName(), name_len + 1); + } + + delete ffblk; +} +#endif + +void Resolve_File(char* fname) +{ +#ifndef _WIN32 + // step through each sub-directory before going for the win + char* next = fname; + while (next = strchr(next, '/')) { + *next = '\0'; + Resolve_File_Single(fname); + *next++ = '/'; + } + + Resolve_File_Single(fname); +#endif +} + +bool Find_First(const char* fname, unsigned int mode, Find_File_Data** ffblk) +{ + if (ffblk == nullptr) { + return false; + } + *ffblk = nullptr; + + *ffblk = Find_File_Data::CreateFindData(); + if ((*ffblk)->FindFirst(fname)) { + return true; + } + + delete *ffblk; + *ffblk = nullptr; + + return false; +} + +bool Find_Next(Find_File_Data* ffblk) +{ + if (ffblk == nullptr) { + return false; + } + + return ffblk->FindNext(); +} + +void Find_Close(Find_File_Data* ffblk) +{ + if (ffblk != nullptr) { + delete ffblk; + } +} diff --git a/code/file.h b/code/file.h new file mode 100644 index 000000000..05af6c231 --- /dev/null +++ b/code/file.h @@ -0,0 +1,188 @@ +// +// Copyright 2020 Electronic Arts Inc. +// +// TiberianDawn.DLL and RedAlert.dll and corresponding source code is free +// software: you can redistribute it and/or modify it under the terms of +// the GNU General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. + +// TiberianDawn.DLL and RedAlert.dll and corresponding source code is distributed +// in the hope that it will be useful, but with permitted additional restrictions +// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT +// distributed with this program. You should have received a copy of the +// GNU General Public License along with permitted additional restrictions +// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection + +/*************************************************************************** + ** C O N F I D E N T I A L --- W E S T W O O D A S S O C I A T E S ** + *************************************************************************** + * * + * Project Name : Library - Filio header stuff. * + * * + * File Name : FILE.H * + * * + * Programmer : Scott K. Bowen * + * * + * Start Date : September 13, 1993 * + * * + * Last Update : April 11, 1994 * + * * + *-------------------------------------------------------------------------* + * Functions: * + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + +#pragma once + +/*=========================================================================*/ +/* File IO system defines and enumerations */ +/*=========================================================================*/ + +#define XMAXPATH 80 + +/* +** These are the Open_File, Read_File, and Seek_File constants. +*/ +#ifndef READ +#define READ 1 // Read access. +#endif +#ifndef WRITE +#define WRITE 2 // Write access. +#endif +#ifndef SEEK_SET +#define SEEK_SET 0 // Seek from start of file. +#define SEEK_CUR 1 // Seek relative from current location. +#define SEEK_END 2 // Seek from end of file. +#endif + +typedef enum +{ + FILEB_PROCESSED = 8, // Was the packed file header of this file processed? + FILEB_PRELOAD, // Scan for and make file resident at WWDOS_Init time? + FILEB_RESIDENT, // Make resident at Open_File time? + FILEB_FLUSH, // Un-resident at Close_File time? + FILEB_PACKED, // Is this file packed? + FILEB_KEEP, // Don't ever flush this resident file? + FILEB_PRIORITY, // Flush this file last? + + FILEB_LAST +} FileFlags_Type; + +#define FILEF_NONE 0 +#define FILEF_PROCESSED (1 << FILEB_PROCESSED) +#define FILEF_PRELOAD (1 << FILEB_PRELOAD) +#define FILEF_RESIDENT (1 << FILEB_RESIDENT) +#define FILEF_FLUSH (1 << FILEB_FLUSH) +#define FILEF_PACKED (1 << FILEB_PACKED) +#define FILEF_KEEP (1 << FILEB_KEEP) +#define FILEF_PRIORITY (1 << FILEB_PRIORITY) + +/* +** These errors are returned by WWDOS_Init(). All errors encountered are +** or'd together so there may be more then one error returned. Not all +** errors are fatal, such as the cache errors. +*/ +typedef enum +{ + FI_SUCCESS = 0x00, + FI_CACHE_TOO_BIG = 0x01, + FI_CACHE_ALREADY_INIT = 0x02, + FI_FILEDATA_FILE_NOT_FOUND = 0x04, + FI_FILEDATA_TOO_BIG = 0x08, + FI_SEARCH_PATH_NOT_FOUND = 0x10, + FI_STARTUP_PATH_NOT_FOUND = 0x20, + FI_NO_CACHE_FOR_PRELOAD = 0x40, + FI_FILETABLE_NOT_INIT = 0x80, +} FileInitErrorType; + +/* +** These are the errors that are detected by the File I/O system and +** passed to the io error routine. +*/ +// lint -strong(AJX,FileErrorType) +typedef enum +{ + CANT_CREATE_FILE, + BAD_OPEN_MODE, + COULD_NOT_OPEN, + TOO_MANY_FILES, + CLOSING_NON_HANDLE, + READING_NON_HANDLE, + WRITING_NON_HANDLE, + SEEKING_NON_HANDLE, + SEEKING_BAD_OFFSET, + WRITING_RESIDENT, + UNKNOWN_INDEX, + DID_NOT_CLOSE, + FATAL_ERROR, + FILE_NOT_LISTED, + FILE_LENGTH_MISMATCH, + INTERNAL_ERROR, + MAKE_RESIDENT_ZERO_SIZE, + RESIDENT_SORT_FAILURE, + + NUMBER_OF_ERRORS /* MAKE SURE THIS IS THE LAST ENTRY */ +} FileErrorType; + +/*=========================================================================*/ +/* File IO system structures */ +/*=========================================================================*/ + +// lint -strong(AJX,FileDataType) +typedef struct +{ + char* Name; // File name (include sub-directory but not volume). + int Size; // File size (0=indeterminate). + void* Ptr; // Resident file pointer. + int Start; // Starting offset in DOS handle file. + unsigned char Disk; // Disk number location. + unsigned char OpenCount; // Count of open locks on resident file. + unsigned short Flag; // File control flags. +} FileDataType; + +/*=========================================================================*/ +/* FIle IO system globals. */ +/*=========================================================================*/ + +// These are cpp errors in funtions declarations JULIO JEREZ + +// extern FileDataType FileData[]; +// extern BYTE ExecPath[XMAXPATH + 1]; +// extern BYTE DataPath[XMAXPATH + 1]; +// extern BYTE StartPath[XMAXPATH + 1]; +// extern BOOL UseCD; + +// The correct syntax is NO TYPE MODIFIER APPLY TO DATA DECLARATIONS +extern FileDataType FileData[]; +extern char ExecPath[XMAXPATH + 1]; +extern char DataPath[XMAXPATH + 1]; +extern char StartPath[XMAXPATH + 1]; + +/*=========================================================================*/ +/* The following prototypes are for the file: file.cpp */ +/*=========================================================================*/ + +void Resolve_File(char* fname); + +class Find_File_Data +{ +public: + static Find_File_Data* CreateFindData(); + + virtual ~Find_File_Data() + { + } + virtual const char* GetName() const = 0; + virtual const char* GetFullName() const + { + return nullptr; + }; + virtual unsigned int GetTime() const = 0; + + virtual bool FindFirst(const char* fname) = 0; + virtual bool FindNext() = 0; + virtual void Close() = 0; +}; + +extern bool Find_First(const char* fname, unsigned int mode, Find_File_Data** ffblk); +extern bool Find_Next(Find_File_Data* ffblk); +extern void Find_Close(Find_File_Data* ffblk); diff --git a/code/file_posix.cpp b/code/file_posix.cpp new file mode 100644 index 000000000..5625dcbf6 --- /dev/null +++ b/code/file_posix.cpp @@ -0,0 +1,133 @@ +#ifndef _WIN32 +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include "file.h" + +#include +#include +#include +#include +#include +#include +#include + +class Find_File_Data_Posix : public Find_File_Data +{ +public: + Find_File_Data_Posix(); + virtual ~Find_File_Data_Posix(); + + virtual const char* GetName() const; + virtual const char* GetFullName() const + { + return DirEntry != nullptr ? FullName : nullptr; + } + virtual unsigned int GetTime() const; + + virtual bool FindFirst(const char* fname); + virtual bool FindNext(); + virtual void Close(); + +private: + DIR* Directory; + struct dirent* DirEntry; + const char* FileFilter; + char FullName[PATH_MAX]; + char DirName[PATH_MAX]; + + bool FindNextWithFilter(); +}; + +Find_File_Data_Posix::Find_File_Data_Posix() + : Directory(nullptr) + , DirEntry(nullptr) +{ +} + +Find_File_Data_Posix::~Find_File_Data_Posix() +{ + Close(); +} + +const char* Find_File_Data_Posix::GetName() const +{ + if (DirEntry == nullptr) { + return nullptr; + } + return DirEntry->d_name; +} + +unsigned int Find_File_Data_Posix::GetTime() const +{ + if (DirEntry == nullptr) { + return 0; + } + struct stat buf = {0}; + if (stat(FullName, &buf) != 0) { + return false; + } + return buf.st_mtime; +} + +bool Find_File_Data_Posix::FindNextWithFilter() +{ + while (true) { + DirEntry = readdir(Directory); + if (DirEntry == nullptr) { + return false; + } + if (fnmatch(FileFilter, DirEntry->d_name, FNM_PATHNAME | FNM_CASEFOLD) == 0) { + strcpy(FullName, DirName); + strcat(FullName, DirEntry->d_name); + break; + } + } + return true; +} + +bool Find_File_Data_Posix::FindFirst(const char* fname) +{ + Close(); + FullName[0] = '\0'; + DirName[0] = '\0'; + + // split directory and file from the path + char* fdir = strrchr((char*)fname, '/'); + if (fdir != nullptr) { + strncat(DirName, fname, (fdir - fname + 1)); + FileFilter = fdir + 1; + Directory = opendir(DirName); + } else { + FileFilter = fname; + Directory = opendir("."); + } + + if (Directory == nullptr) { + return false; + } + + return FindNextWithFilter(); +} + +bool Find_File_Data_Posix::FindNext() +{ + if (Directory == nullptr) { + return false; + } + return FindNextWithFilter(); +} + +void Find_File_Data_Posix::Close() +{ + if (Directory != nullptr) { + closedir(Directory); + Directory = nullptr; + } +} + +Find_File_Data* Find_File_Data::CreateFindData() +{ + return new Find_File_Data_Posix(); +} +#endif diff --git a/code/file_win.cpp b/code/file_win.cpp new file mode 100644 index 000000000..76779ddff --- /dev/null +++ b/code/file_win.cpp @@ -0,0 +1,76 @@ +#ifdef _WIN32 +#include "file.h" + +#include +#include + +class Find_File_Data_Win : public Find_File_Data +{ +public: + Find_File_Data_Win(); + virtual ~Find_File_Data_Win(); + + virtual const char* GetName() const; + virtual unsigned int GetTime() const; + + virtual bool FindFirst(const char* fname); + virtual bool FindNext(); + virtual void Close(); + +private: + HANDLE FindHandle; + WIN32_FIND_DATAA FindData; +}; + +Find_File_Data_Win::Find_File_Data_Win() + : FindHandle(INVALID_HANDLE_VALUE) + , FindData({0}) +{ +} + +Find_File_Data_Win::~Find_File_Data_Win() +{ + Close(); +} + +const char* Find_File_Data_Win::GetName() const +{ + return FindData.cFileName; +} + +unsigned int Find_File_Data_Win::GetTime() const +{ + ULARGE_INTEGER ull; + ull.LowPart = FindData.ftLastWriteTime.dwLowDateTime; + ull.HighPart = FindData.ftLastWriteTime.dwHighDateTime; + return (unsigned int)(ull.QuadPart / 10000000ULL - 11644473600ULL); +} + +bool Find_File_Data_Win::FindFirst(const char* fname) +{ + FindHandle = FindFirstFileA(fname, &FindData); + return (FindHandle != INVALID_HANDLE_VALUE); +} + +bool Find_File_Data_Win::FindNext() +{ + if (FindHandle == INVALID_HANDLE_VALUE) { + return false; + } + + return (FindNextFileA(FindHandle, &FindData) != FALSE); +} + +void Find_File_Data_Win::Close() +{ + if (FindHandle != INVALID_HANDLE_VALUE) { + FindClose(FindHandle); + FindHandle = INVALID_HANDLE_VALUE; + } +} + +Find_File_Data* Find_File_Data::CreateFindData() +{ + return new Find_File_Data_Win(); +} +#endif diff --git a/code/fly.cpp b/code/fly.cpp index 0b64fe905..2cf7d741f 100644 --- a/code/fly.cpp +++ b/code/fly.cpp @@ -117,7 +117,7 @@ FlyLocomotionClass::~FlyLocomotionClass(void) /// an aircraft that has been told to go somewhere but has yet to build up any speed. /// /// bool; Is the aircraft moving or trying to? -boolean STDMETHODCALLTYPE FlyLocomotionClass::Is_Moving(void) +bool FlyLocomotionClass::Is_Moving(void) { return(IsMoving || LinkedTo->PitchAngle > 0); } @@ -129,7 +129,7 @@ boolean STDMETHODCALLTYPE FlyLocomotionClass::Is_Moving(void) /// the aircraft has any speed at all. /// /// bool; Is the aircraft moving right now? -boolean STDMETHODCALLTYPE FlyLocomotionClass::Is_Moving_Now(void) +bool FlyLocomotionClass::Is_Moving_Now(void) { if (CurrentSpeed == 0) { return(false); @@ -143,7 +143,7 @@ boolean STDMETHODCALLTYPE FlyLocomotionClass::Is_Moving_Now(void) /// /// Returns with the destination coordinate. If the aircraft is not going /// anywhere, COORD_NONE is returned. -Coord STDMETHODCALLTYPE FlyLocomotionClass::Destination(void) +Coord FlyLocomotionClass::Destination(void) { if (Is_Moving()) { return(DestinationCoord); @@ -159,7 +159,7 @@ Coord STDMETHODCALLTYPE FlyLocomotionClass::Destination(void) /// disposed of if it has wandered off the edge of the world. /// /// bool; Is the aircraft still under way? -boolean STDMETHODCALLTYPE FlyLocomotionClass::Process(void) +bool FlyLocomotionClass::Process(void) { if (!IsLanding && !IsTakingOff && TargetSpeed >= 1.0 && FlightLevel == 0) { FlightLevel = LinkedTo->TClass->Flight_Level(); @@ -234,7 +234,7 @@ boolean STDMETHODCALLTYPE FlyLocomotionClass::Process(void) /// destination is a request to stop, which brings a flying aircraft down to land. /// /// The coordinate to head for, or COORD_NONE to stop and land. -void STDMETHODCALLTYPE FlyLocomotionClass::Move_To(Coord to) +void FlyLocomotionClass::Move_To(Coord to) { if (((Coord)to).As_Cell() != DestinationCoord.As_Cell() || !IsLanding) { @@ -242,7 +242,7 @@ void STDMETHODCALLTYPE FlyLocomotionClass::Move_To(Coord to) if ((Coord)to == COORD_NONE) { int landing_altitude = 0; - IFlyControlPtr flyctrl(LinkedTo); + IFlyControl * const flyctrl = dynamic_cast(LinkedTo); if (flyctrl != NULL) { landing_altitude = flyctrl->Landing_Altitude(); } @@ -261,7 +261,7 @@ void STDMETHODCALLTYPE FlyLocomotionClass::Move_To(Coord to) DestinationCoord.Z = LinkedTo->TClass->Flight_Level() + Map.Get_Height_GL(to); } - IFlyControlPtr flyctrl(LinkedTo); + IFlyControl * const flyctrl = dynamic_cast(LinkedTo); int landing_altitude = 0; if (flyctrl != NULL) { landing_altitude = flyctrl->Landing_Altitude(); @@ -286,7 +286,7 @@ void STDMETHODCALLTYPE FlyLocomotionClass::Move_To(Coord to) /// assigns that as the new destination. An aircraft with nowhere at all to go is destroyed /// rather than left loitering in an illegal spot. /// -void STDMETHODCALLTYPE FlyLocomotionClass::Stop_Moving(void) +void FlyLocomotionClass::Stop_Moving(void) { if (Is_Moving()) { @@ -608,7 +608,7 @@ void FlyLocomotionClass::Movement_AI(void) if (current_height < FlightLevel && LinkedTo->Strength > 0) { bool is_loaded = false; - IFlyControlPtr flyctrl(LinkedTo); + IFlyControl * const flyctrl = dynamic_cast(LinkedTo); if (flyctrl != NULL) { is_loaded = flyctrl->Is_Loaded() != 0; } @@ -698,7 +698,7 @@ void FlyLocomotionClass::Movement_AI(void) } if (LinkedTo->Strength > 0 && Is_In_Flight() && DestinationCoord != COORD_NONE) { - IFlyControlPtr flyctrl(LinkedTo); + IFlyControl * const flyctrl = dynamic_cast(LinkedTo); if (!Needs_To_Land()) { TargetSpeed = 1.0; @@ -881,7 +881,7 @@ bool FlyLocomotionClass::Process_Take_Off(void) height -= BRIDGE_LEPTON_HEIGHT; } - IFlyControlPtr flyctrl(LinkedTo); + IFlyControl * const flyctrl = dynamic_cast(LinkedTo); int landing_altitude = 0; if (flyctrl) { landing_altitude = flyctrl->Landing_Altitude(); @@ -962,7 +962,7 @@ bool FlyLocomotionClass::Process_Landing(void) TargetSpeed = 0; int landing_altitude = 0; - IFlyControlPtr flyctrl(LinkedTo); + IFlyControl * const flyctrl = dynamic_cast(LinkedTo); if (flyctrl) { landing_altitude = flyctrl->Landing_Altitude(); } @@ -1072,7 +1072,7 @@ bool FlyLocomotionClass::Process_Landing(void) /// Returns with the distance remaining to the destination. int FlyLocomotionClass::Nearing_Target(bool stage_approach, Coord coord) { - IFlyControlPtr flyctrl(LinkedTo); + IFlyControl * const flyctrl = dynamic_cast(LinkedTo); /* * A strafing aircraft that is over an ammo-bearing attack run should ignore the @@ -1303,7 +1303,7 @@ int FlyLocomotionClass::Nearing_Target(bool stage_approach, Coord coord) /// Optional cache key for the resulting orientation. It may be NULL, and /// is set to -1 for an attitude that is not worth caching. /// Returns with the matrix to draw the aircraft with. -Matrix3D STDMETHODCALLTYPE FlyLocomotionClass::Draw_Matrix(int * key) +Matrix3D FlyLocomotionClass::Draw_Matrix(int * key) { Matrix3D mtx; mtx.Make_Identity(); @@ -1398,10 +1398,10 @@ Matrix3D STDMETHODCALLTYPE FlyLocomotionClass::Draw_Matrix(int * key) /// look pinned in place while it hovers. Dropships and grounded aircraft do not bob. /// /// Returns with the pixel offset to shift the aircraft by. -Point2D STDMETHODCALLTYPE FlyLocomotionClass::Draw_Point(void) +Point2D FlyLocomotionClass::Draw_Point(void) { int y = 0; - IFlyControlPtr flyctrl(LinkedTo); + IFlyControl * const flyctrl = dynamic_cast(LinkedTo); int landing_altitude = 0; if (flyctrl) { @@ -1421,7 +1421,7 @@ Point2D STDMETHODCALLTYPE FlyLocomotionClass::Draw_Point(void) /// The shadow is drawn where the aircraft's position puts it, so no adjustment is needed. /// /// Returns with the pixel offset to shift the shadow by. -Point2D STDMETHODCALLTYPE FlyLocomotionClass::Shadow_Point(void) +Point2D FlyLocomotionClass::Shadow_Point(void) { return(Point2D(0, 0)); } @@ -1470,7 +1470,7 @@ void FlyLocomotionClass::Land(void) /// Optional cache key for the shadow orientation. It may be NULL, and a /// value of -1 marks the shadow as not worth caching. /// Returns with the matrix to draw the shadow with. -Matrix3D STDMETHODCALLTYPE FlyLocomotionClass::Shadow_Matrix(int * key) +Matrix3D FlyLocomotionClass::Shadow_Matrix(int * key) { int ramp = Map[(Coord const &)LinkedTo->PositionCoord].Ramp; if (LinkedTo->TClass->IsDropship) { @@ -1493,7 +1493,7 @@ Matrix3D STDMETHODCALLTYPE FlyLocomotionClass::Shadow_Matrix(int * key) /// This routine snaps the body around immediately rather than rotating it over time. /// /// The facing to set the aircraft body to. -void STDMETHODCALLTYPE FlyLocomotionClass::Do_Turn(DirType coord) +void FlyLocomotionClass::Do_Turn(DirType coord) { LinkedTo->SecondaryFacing.Set(coord); } @@ -1515,18 +1515,9 @@ bool FlyLocomotionClass::Is_In_Flight(void) } -/// -/// Fetches the class identifier of this locomotor. -/// This routine is used by the save and load machinery so that it knows which locomotor to -/// create when the owning object is restored. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE FlyLocomotionClass::GetClassID(CLSID * retval) +ClassID FlyLocomotionClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_FlyerLocomotion; - return(S_OK); + return(ClassID_FlyerLocomotion); } @@ -1559,7 +1550,7 @@ void FlyLocomotionClass::Serialize(SaveStreamClass & stream) /// /// Returns with LAYER_GROUND while the aircraft is on the deck, or LAYER_TOP once /// it is above it. -LayerType STDMETHODCALLTYPE FlyLocomotionClass::In_Which_Layer(void) +LayerType FlyLocomotionClass::In_Which_Layer(void) { return(LinkedTo->HeightAGL <= 0 ? LAYER_GROUND : LAYER_TOP); } @@ -1572,7 +1563,7 @@ LayerType STDMETHODCALLTYPE FlyLocomotionClass::In_Which_Layer(void) /// stop, so that it falls out of the sky rather than coasting on to its objective. /// /// bool; Was the power successfully cut? -boolean STDMETHODCALLTYPE FlyLocomotionClass::Power_Off(void) +bool FlyLocomotionClass::Power_Off(void) { if (Is_Moving()) { Tumble(); @@ -1587,7 +1578,7 @@ boolean STDMETHODCALLTYPE FlyLocomotionClass::Power_Off(void) /// Is the aircraft still under power? /// /// bool; Does the aircraft still have power? -boolean STDMETHODCALLTYPE FlyLocomotionClass::Is_Powered(void) +bool FlyLocomotionClass::Is_Powered(void) { return(BASECLASS::Is_Powered()); } @@ -1599,7 +1590,7 @@ boolean STDMETHODCALLTYPE FlyLocomotionClass::Is_Powered(void) /// by one. /// /// bool; Does an ion storm affect this aircraft? -boolean STDMETHODCALLTYPE FlyLocomotionClass::Is_Ion_Sensitive(void) +bool FlyLocomotionClass::Is_Ion_Sensitive(void) { return(!LinkedTo->TClass->IsHunterSeeker); } @@ -1625,7 +1616,7 @@ void FlyLocomotionClass::Tumble(void) /// Fetches the speed the aircraft is currently traveling at. /// /// Returns with the distance the aircraft will cover in one game frame. -int STDMETHODCALLTYPE FlyLocomotionClass::Apparent_Speed(void) +int FlyLocomotionClass::Apparent_Speed(void) { return(LinkedTo->TClass->MaxSpeed * CurrentSpeed); } @@ -1638,7 +1629,7 @@ int STDMETHODCALLTYPE FlyLocomotionClass::Apparent_Speed(void) /// /// Returns with the status code for taking off, landing, moving, or sitting /// idle. -int STDMETHODCALLTYPE FlyLocomotionClass::Get_Status(void) +int FlyLocomotionClass::Get_Status(void) { if (IsLanding) { return(1); @@ -1660,7 +1651,7 @@ int STDMETHODCALLTYPE FlyLocomotionClass::Get_Status(void) /// targets in a multiplay game so that the drone makes a nuisance of itself where it will /// be noticed. /// -void STDMETHODCALLTYPE FlyLocomotionClass::Acquire_Hunter_Seeker_Target(void) +void FlyLocomotionClass::Acquire_Hunter_Seeker_Target(void) { if (LinkedTo->TarCom == NULL) { @@ -1722,7 +1713,7 @@ bool FlyLocomotionClass::Needs_To_Land(void) return(true); } - IFlyControlPtr flyctrl(LinkedTo); + IFlyControl * const flyctrl = dynamic_cast(LinkedTo); if (flyctrl != NULL && !flyctrl->Is_Strafe()) { return(true); } @@ -1751,7 +1742,7 @@ bool FlyLocomotionClass::Is_Locked_To_Straight_Flight(void) return(true); } - IFlyControlPtr flyctrl(LinkedTo); + IFlyControl * const flyctrl = dynamic_cast(LinkedTo); if (flyctrl) { if (flyctrl->Is_Locked()) { return(true); diff --git a/code/fly.h b/code/fly.h index 96ffe8cc3..3981beb9d 100644 --- a/code/fly.h +++ b/code/fly.h @@ -58,28 +58,28 @@ class FlyLocomotionClass : public LocomotionClass FlyLocomotionClass(void); virtual ~FlyLocomotionClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; - virtual boolean STDMETHODCALLTYPE Is_Moving(void) override; - virtual boolean STDMETHODCALLTYPE Is_Moving_Now(void) override; - virtual Coord STDMETHODCALLTYPE Destination(void) override; - virtual void STDMETHODCALLTYPE Move_To(Coord to) override; - virtual void STDMETHODCALLTYPE Stop_Moving(void) override; - virtual boolean STDMETHODCALLTYPE Process(void) override; - virtual Matrix3D STDMETHODCALLTYPE Draw_Matrix(int *key) override; - virtual Point2D STDMETHODCALLTYPE Draw_Point(void) override; - virtual Point2D STDMETHODCALLTYPE Shadow_Point(void) override; - virtual Matrix3D STDMETHODCALLTYPE Shadow_Matrix(int *key) override; - virtual void STDMETHODCALLTYPE Do_Turn(DirType coord) override; - virtual LayerType STDMETHODCALLTYPE In_Which_Layer(void) override; - virtual boolean STDMETHODCALLTYPE Power_Off(void) override; - virtual boolean STDMETHODCALLTYPE Is_Powered(void) override; - virtual boolean STDMETHODCALLTYPE Is_Ion_Sensitive(void) override; - virtual int STDMETHODCALLTYPE Apparent_Speed(void) override; - virtual int STDMETHODCALLTYPE Get_Status(void) override; - virtual void STDMETHODCALLTYPE Acquire_Hunter_Seeker_Target(void) override; + virtual bool Is_Moving(void) override; + virtual bool Is_Moving_Now(void) override; + virtual Coord Destination(void) override; + virtual void Move_To(Coord to) override; + virtual void Stop_Moving(void) override; + virtual bool Process(void) override; + virtual Matrix3D Draw_Matrix(int *key) override; + virtual Point2D Draw_Point(void) override; + virtual Point2D Shadow_Point(void) override; + virtual Matrix3D Shadow_Matrix(int *key) override; + virtual void Do_Turn(DirType coord) override; + virtual LayerType In_Which_Layer(void) override; + virtual bool Power_Off(void) override; + virtual bool Is_Powered(void) override; + virtual bool Is_Ion_Sensitive(void) override; + virtual int Apparent_Speed(void) override; + virtual int Get_Status(void) override; + virtual void Acquire_Hunter_Seeker_Target(void) override; /*--------------------------------------------------------------------- ** Member function prototypes. diff --git a/code/fog.cpp b/code/fog.cpp index 76acc1e3c..b1dbdb91e 100644 --- a/code/fog.cpp +++ b/code/fog.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "fog.h" @@ -593,18 +592,9 @@ RTTIType FoggedObjectClass::Fetch_RTTI(void) const } -/// -/// Fetches the class ID of this object. -/// This routine is part of the persistence interface the save game system uses to -/// recreate objects of the right kind when a game is loaded. -/// -/// Pointer to the class ID to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE FoggedObjectClass::GetClassID(CLSID * retval) +ClassID FoggedObjectClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_FoggedObjectClass; - return(S_OK); + return(ClassID_FoggedObjectClass); } diff --git a/code/fog.h b/code/fog.h index 873145049..938207fd1 100644 --- a/code/fog.h +++ b/code/fog.h @@ -40,7 +40,7 @@ class FoggedObjectClass : public AbstractClass FoggedObjectClass(TerrainClass * object); virtual ~FoggedObjectClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; virtual void Post_Load(void) override; diff --git a/code/foot.cpp b/code/foot.cpp index d2b26c127..068dd7be2 100644 --- a/code/foot.cpp +++ b/code/foot.cpp @@ -115,6 +115,7 @@ #include "partsys.h" #include "revent.h" #include "rules.h" +#include "saveload.h" #include "savestream.h" #include "session.h" #include "swizzle.h" @@ -604,7 +605,6 @@ void FootClass::Advance_Path(int count) } - /*********************************************************************************************** * FootClass::Mission_Move -- AI process for moving a vehicle to its destination. * * * @@ -1132,10 +1132,8 @@ void FootClass::Approach_Target(void) */ bool flyer = (RTTI == RTTI_AIRCRAFT); - CLSID clsid; - IPersistPtr persist(Locomotion); - persist->GetClassID(&clsid); - if (clsid == CLSID_JumpjetLocomotion) { + ClassID const clsid = Locomotion_Class_ID(Locomotion.get()); + if (clsid == ClassID_JumpjetLocomotion) { flyer = true; } @@ -1875,9 +1873,9 @@ bool FootClass::Enter_Idle_Mode(bool, bool resume_waypoint) } bool was_piggybacking = false; - IPiggybackPtr piggy(Locomotion); + IPiggyback * piggy = Piggyback_Of(Locomotion.get()); if (piggy != NULL && piggy->Is_Ok_To_End()) { - piggy->End_Piggyback(&Locomotion); + Locomotion = piggy->End_Piggyback(); was_piggybacking = true; } @@ -2333,9 +2331,9 @@ int FootClass::Do_MISSION_ENTER(void) Enter_Idle_Mode(); } else { if (NavCom == NULL && RouteQueue.Count() > 0 ) { - IPiggybackPtr piggy(Locomotion); + IPiggyback * piggy = Piggyback_Of(Locomotion.get()); if (piggy != NULL && piggy->Is_Ok_To_End()) { - piggy->End_Piggyback(&Locomotion); + Locomotion = piggy->End_Piggyback(); } if (RouteQueue.Count() > 0) { Assign_Destination(RouteQueue[0], false); @@ -2387,11 +2385,9 @@ void FootClass::Assign_Destination(AbstractClass * target, bool) ParticleSystems[ATTACHED_PARTICLE_FIRE] = NULL; } - CLSID locoid; - IPersistPtr persist(Locomotion); - persist->GetClassID(&locoid); + ClassID const locoid = Locomotion_Class_ID(Locomotion.get()); - if (locoid == CLSID_HoverLocomotion && PathDelay == 0) { + if (locoid == ClassID_HoverLocomotion && PathDelay == 0) { PathDelay = 1; } @@ -3317,10 +3313,10 @@ void FootClass::AI(void) Scatter(Coord(0,0,0), true); } - IPiggybackPtr piggy(Locomotion); + IPiggyback * piggy = Piggyback_Of(Locomotion.get()); if (piggy != NULL) { if (piggy->Is_Ok_To_End()) { - piggy->End_Piggyback(&Locomotion); + Locomotion = piggy->End_Piggyback(); } } @@ -3412,7 +3408,7 @@ ZGradientType FootClass::Get_Z_Gradient(void) const /// void FootClass::Draw_Voxel_Shadow(VoxelDataStruct const & voxeldata, int layer_index, int key, VoxelIndexClass * cache, Rect const & cliprect, Point2D const & point, Matrix3D const & matrix, bool force_cache) const { - if (Locomotion != NULL && Locomotion->Is_To_Have_Shadow() == (boolean)true) { + if (Locomotion != NULL && Locomotion->Is_To_Have_Shadow() == (bool)true) { Point2D drawpoint = point; if (Locomotion != NULL) { drawpoint = Point2D(Locomotion->Shadow_Point()) + point; @@ -3519,19 +3515,13 @@ void FootClass::Serialize(SaveStreamClass & stream) stream.Serialize(BlockagePathDelay); /* - * The locomotor is a COM sub-object rather than a member, so it persists itself onto - * the raw stream through OLE. The one being replaced is released first, since loading - * hands back a fresh interface pointer rather than filling this one in. + * The locomotor is a sub-object rather than a member, so it travels as a record of + * its own. */ if (stream.Is_Saving()) { - IPersistStreamPtr persist(Locomotion); - OleSaveToStream(persist, stream.Get_Stream()); + Save_Object(stream, Locomotion.get()); } else { - if (Locomotion != NULL) { - ((ILocomotion *)Locomotion)->Release(); - } - Locomotion.Detach(); - OleLoadFromStream(stream.Get_Stream(), IID_ILocomotion, (LPVOID *)&Locomotion); + Locomotion = Load_Locomotor(stream); } stream.Serialize(HeadToCoord); @@ -3595,12 +3585,12 @@ void FootClass::Set_Coord(Coord const & coord) /// void FootClass::Link_DropPod(void) { - ILocomotionPtr locomotion = Locomotion; - ILocomotionPtr ballistic(CLSID_BallisticLocomotion); + std::unique_ptr locomotion = std::move(Locomotion); + std::unique_ptr ballistic = Create_Locomotor(ClassID_BallisticLocomotion); ballistic->Link_To_Object(this); - IPiggybackPtr piggy(ballistic); - piggy->Begin_Piggyback(locomotion); - Locomotion = ballistic; + IPiggyback * piggy = Piggyback_Of(ballistic.get()); + piggy->Begin_Piggyback(std::move(locomotion)); + Locomotion = std::move(ballistic); } @@ -4766,12 +4756,9 @@ void FootClass::Delete_Me(void) /// bool; Is the object in the air? bool FootClass::In_Air(void) const { - IPersistPtr loco(Locomotion); + ClassID const clsid = Locomotion_Class_ID(Locomotion.get()); - CLSID clsid; - loco->GetClassID(&clsid); - - if (clsid == CLSID_HoverLocomotion) { + if (clsid == ClassID_HoverLocomotion) { return(false); } @@ -4790,10 +4777,7 @@ bool FootClass::On_Ground(void) const if (BASECLASS::On_Ground()) { return(true); } - IPersistPtr loco(Locomotion); - - CLSID clsid; - loco->GetClassID(&clsid); + ClassID const clsid = Locomotion_Class_ID(Locomotion.get()); - return(IsDown && clsid == CLSID_HoverLocomotion); + return(IsDown && clsid == ClassID_HoverLocomotion); } diff --git a/code/foot.h b/code/foot.h index 246b96e08..5bbb1502f 100644 --- a/code/foot.h +++ b/code/foot.h @@ -37,6 +37,7 @@ #include "team.h" #include "techno.h" +#include #include class UnitClass; @@ -210,7 +211,7 @@ class FootClass : public TechnoClass * handed to a ballistic locomotor and a unit crossing a tunnel walks -- so all * movement is asked of this interface rather than of the type's setting. */ - ILocomotionPtr Locomotion; + std::unique_ptr Locomotion; /* ** This is the coordinate that the unit is heading to diff --git a/code/gamedirs.cpp b/code/gamedirs.cpp index d26045e8e..e42e0f87c 100644 --- a/code/gamedirs.cpp +++ b/code/gamedirs.cpp @@ -14,7 +14,14 @@ #include "cdfile.h" #include "dbgprint.h" +// Included after the file classes: it defines READ and WRITE as macros that would otherwise +// swallow the identically named enumerators in wwfile.h. +#include "file.h" + +#include #include +#include +#include #include /* @@ -61,7 +68,7 @@ static std::string Terminate_Path(std::string const & path) return(path); default: - return(path + '\\'); + return(path + (char)std::filesystem::path::preferred_separator); } } @@ -120,9 +127,21 @@ char const * Game_Directory_Error(void) static bool Is_Directory(std::string const & path) { - DWORD attributes = GetFileAttributes(path.c_str()); + std::error_code error; + + return(std::filesystem::is_directory(path, error)); +} + + +/// +/// Makes a directory, reporting success when it is already there. +/// +static bool Make_Directory(std::string const & path) +{ + std::error_code error; + std::filesystem::create_directory(path, error); - return(attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0); + return(!error || Is_Directory(path)); } @@ -220,7 +239,7 @@ std::vector Parse_Search_Folders(char const * list) bool Apply_Game_Directories(void) { if (!UserDirectory.empty()) { - if (!Is_Directory(UserDirectory) && !CreateDirectory(UserDirectory.c_str(), NULL)) { + if (!Is_Directory(UserDirectory) && !Make_Directory(UserDirectory)) { Report_Directory_Error("user", UserDirectory); return(false); } @@ -238,6 +257,13 @@ bool Apply_Game_Directories(void) DebugString("[GameDirs] Data directory is %s.\n", DataDirectory.c_str()); } + // Shipped UI documents, styles, images and fonts sit in ui/ beside the executable. + // Adding the directory to the search paths is what lets them resolve by bare name, so + // the same file loads from there or from a mix and a mod can override either. + std::string const uipath = Terminate_Path(Data_Directory() + "ui"); + CDFileClass::Add_Search_Drive(uipath.c_str()); + DebugString("[GameDirs] UI directory is %s.\n", uipath.c_str()); + return(true); } @@ -277,9 +303,9 @@ std::string Saved_Game_Name(char const * filename) { std::string const folder = UserDirectory + SavedGamesFolder; - CreateDirectory(folder.c_str(), NULL); + Make_Directory(folder); - return(folder + '\\' + filename); + return(folder + (char)std::filesystem::path::preferred_separator + filename); } @@ -287,31 +313,36 @@ static void Scan_Folder(char const * prefix, char const * pattern, std::vectorGetName(); + if (found == NULL) { + continue; + } + + std::error_code error; + if (std::filesystem::is_directory(std::string(prefix) + found, error)) { continue; } bool present = false; for (std::string const & existing : names) { - if (Is_Same_Path(existing, block.cFileName)) { + if (Is_Same_Path(existing, found)) { present = true; break; } } if (!present) { - names.push_back(block.cFileName); + names.push_back(found); } - } while (FindNextFile(handle, &block)); + } while (Find_Next(block)); - FindClose(handle); + Find_Close(block); } diff --git a/code/gamedlg.cpp b/code/gamedlg.cpp index 4e1cc50e6..833864969 100644 --- a/code/gamedlg.cpp +++ b/code/gamedlg.cpp @@ -44,13 +44,16 @@ #include "globals.h" #include "init.h" #include "language/language.h" -#include "ownrdraw.h" #include "queue.h" #include "session.h" #include "techno.h" +#include "ui/uigamecontrols.h" #include "special.hh" +#include +#include + int GameSpeedNames[OptionsClass::MAX_SPEED_SETTING] = { TXT_SLOWEST, TXT_SLOWER, @@ -84,9 +87,6 @@ int GameDifficultyNames[OptionsClass::MAX_DIFFICULTY_SETTING] = { }; -INT_PTR CALLBACK Game_Controls_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); -void Game_Controls_Dialog_On_COMMAND(HWND window, UINT message, WPARAM wparam, LPARAM lparam); - /*********************************************************************************************** * OptionsClass::Process -- Handles all the options graphic interface. * * * @@ -101,276 +101,15 @@ void Game_Controls_Dialog_On_COMMAND(HWND window, UINT message, WPARAM wparam, L *=============================================================================================*/ void GameControlsClass::Dialog(void) { - int res = -1; - DebugString("GameControls: GameSpeed = %d, ScrollRate = %d, Detail = %d\n", Options.GameSpeed, Options.ScrollRate, Options.DetailLevel); - if (GameActive == true) { - if (Session.Type == GAME_INTERNET) { - _Dialog = OwnerDraw::Begin_Dialog(IDD_OPT_CTRL_GAME_WOL, Game_Controls_Dialog_Proc); - } else { - _Dialog = OwnerDraw::Begin_Dialog(IDD_OPT_CTRL_GAME_MP, Game_Controls_Dialog_Proc); - } - } else { - _Dialog = OwnerDraw::Begin_Dialog(IDD_OPT_CTRL_GAME_SP, Game_Controls_Dialog_Proc); - } - - if (_Dialog) { - - SetWindowLongPtr(_Dialog, DWLP_USER, (LONG_PTR)&res); - - OwnerDraw::Display_Dialog(_Dialog); + UIGameControlsPresenterClass screen; + screen.Refresh(); - while (res == -1) { - if (OwnerDraw::Dialog_Message_Handler() == true) { - res = 2; - } - if (!GameActive) { - Title_Screen_Restore(); - } - } - if (res == 1) { - Set(); - Options.Save_Settings(); - } - - OwnerDraw::End_Dialog(_Dialog); + if (UI_Game_Controls_Screen(screen).Outcome != UIResult::OUTCOME_FAILED_TO_OPEN && screen.Commits()) { + screen.Apply(); + Options.Save_Settings(); } DebugString("GameControls: GameSpeed = %d, ScrollRate = %d, Detail = %d\n", Options.GameSpeed, Options.ScrollRate, Options.DetailLevel); } - - -/// -/// Sets the game options from the game controls dialog. -/// This routine is called when the player accepts the dialog. Each control is asked for -/// its current value and the answer is handed to the option it governs, along with any -/// notification the rest of the game needs -- the map is told to rebuild its cell drawers -/// when the detail level changes, and a game speed change during a network game is issued -/// as an event so that every player stays in step. -/// -void GameControlsClass::Set(void) -{ - HWND handle; - - handle = GetDlgItem(_Dialog, IDC_GAME_SPEED_SLIDER); - if (handle) { - int gamespeed = (OptionsClass::MAX_SPEED_SETTING-1) - Slider_GetPos(handle); - if (Options.GameSpeed != gamespeed) { - if (GameActive == true && Session.Type != GAME_NORMAL && Session.Type != GAME_SKIRMISH) { - OutList.push_back(EventClass(PlayerPtr->HeapID, EventClass::GAMESPEED, gamespeed)); - } else { - Options.GameSpeed = gamespeed; - } - } - } - - handle = GetDlgItem(_Dialog, IDC_SCROLL_SPEED_SLIDER); - if (handle) { - Options.ScrollRate = (OptionsClass::MAX_SCROLL_SETTING-1) - Slider_GetPos(handle); - } - - handle = GetDlgItem(_Dialog, IDC_DETAIL_LEVEL_SLIDER); - if (handle) { - int detailevel = Slider_GetPos(handle); - if (Options.DetailLevel != detailevel) { - Options.DetailLevel = detailevel; - Map.Reinit_Cell_Drawers(); - } - } - - handle = GetDlgItem(_Dialog, IDC_SIDEBAR_TEXT); - if (handle) { - bool cameotext = Button_GetCheck(handle) == TRUE; - if (Options.SidebarCameoText != cameotext) { - Options.SidebarCameoText = cameotext; - Map.Toggle_Cameo_Text(cameotext); - } - } - - handle = GetDlgItem(_Dialog, IDC_TARGET_LINES); - if (handle) { - Options.ActionLines = Button_GetCheck(handle) == TRUE; - TechnoClass::Set_Action_Lines(Options.ActionLines); - } - - handle = GetDlgItem(_Dialog, IDC_TOOLTIPS); - if (handle) { - Options.ToolTips = Button_GetCheck(handle) == TRUE; - if (ToolTips != NULL && GameActive == true) { - ToolTips->Activate(Options.ToolTips); - } - } - - handle = GetDlgItem(_Dialog, IDC_SCROLL_COASTING); - if (handle) { - Options.ScrollMethod = Button_GetCheck(handle) == TRUE ? 0 : 1; - } - - handle = GetDlgItem(_Dialog, IDC_EDGE_SCROLL); - if (handle) { - Options.AutoScroll = Button_GetCheck(handle) == TRUE; - } - - if (GameActive == false) { - handle = GetDlgItem(_Dialog, IDC_DIFFICULTY_SLIDER); - if (handle) { - Options.Difficulty = Slider_GetPos(handle); - } - } -} - - -/// -/// Handles the messages sent to the game controls dialog. -/// This routine gives the ownerdraw layer first refusal on every message. Anything it -/// leaves alone is used to prime the sliders and check boxes from the current options, to -/// track the label alongside a slider the player is dragging, and to route commands on to -/// Game_Controls_Dialog_On_COMMAND. -/// -/// Returns with a non-zero value if the message was consumed by the ownerdraw -/// layer. -INT_PTR CALLBACK Game_Controls_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - HWND handle; - int index; - - INT_PTR rc = OwnerDraw::Default_Dialog_Proc(window, message, wparam, lparam); - if (rc == 0) { - switch (message) { - case WM_INITDIALOG: - handle = GetDlgItem(window, IDC_GAME_SPEED_SLIDER); - if (handle) { - SendMessage(handle, OD_TRACKNUMBERS, 0, 0); - Slider_SetRange(handle, 0, (OptionsClass::MAX_SPEED_SETTING-1)); - Slider_SetPos(handle, (OptionsClass::MAX_SPEED_SETTING-1) - Options.GameSpeed); - } - - handle = GetDlgItem(window, IDC_SCROLL_SPEED_SLIDER); - if (handle) { - SendMessage(handle, OD_TRACKNUMBERS, 0, 0); - Slider_SetRange(handle, 0, (OptionsClass::MAX_SCROLL_SETTING-1)); - Slider_SetPos(handle, (OptionsClass::MAX_SCROLL_SETTING-1) - Options.ScrollRate); - } - - handle = GetDlgItem(window, IDC_DETAIL_LEVEL_SLIDER); - if (handle) { - SendMessage(handle, OD_TRACKNUMBERS, 0, 0); - Slider_SetRange(handle, 0, (OptionsClass::MAX_DETAIL_SETTING-1)); - Slider_SetPos(handle, Options.DetailLevel); - } - - handle = GetDlgItem(window, IDC_SIDEBAR_TEXT); - if (handle) { - Button_SetCheck(handle, Options.SidebarCameoText != false); - } - - handle = GetDlgItem(window, IDC_TARGET_LINES); - if (handle) { - Button_SetCheck(handle, Options.ActionLines != false); - } - - handle = GetDlgItem(window, IDC_TOOLTIPS); - if (handle) { - Button_SetCheck(handle, Options.ToolTips != false); - } - - handle = GetDlgItem(window, IDC_SCROLL_COASTING); - if (handle) { - Button_SetCheck(handle, Options.ScrollMethod == 0); - } - - handle = GetDlgItem(window, IDC_EDGE_SCROLL); - if (handle) { - Button_SetCheck(handle, Options.AutoScroll != false); - } - - if (GameActive == true) { - handle = GetDlgItem(window, IDC_OPT_SOUND_BTN); - if (handle) { - EnableWindow(handle, AudioEngine.Is_Available()); - } - } else { - handle = GetDlgItem(window, IDC_DIFFICULTY_SLIDER); - if (handle) { - SendMessage(handle, OD_TRACKNUMBERS, 0, 0); - Slider_SetRange(handle, 0, (OptionsClass::MAX_DIFFICULTY_SETTING-1)); - Slider_SetPos(handle, Options.Difficulty); - } - } - break; - - case WM_COMMAND: - Game_Controls_Dialog_On_COMMAND(window, LOWORD(wparam), 0, HIWORD(wparam)); - break; - - case WM_HSCROLL: - if (LOWORD(wparam) == SB_THUMBTRACK) { - index = HIWORD(wparam); - int name; - - handle = 0; - if ((HWND)lparam == GetDlgItem(window, IDC_GAME_SPEED_SLIDER)) { - name = GameSpeedNames[index]; - handle = GetDlgItem(window, IDC_GAME_SPEED_LABEL); - } else if ((HWND)lparam == GetDlgItem(window, IDC_SCROLL_SPEED_SLIDER)) { - name = GameScrollSpeedNames[index]; - handle = GetDlgItem(window, IDC_SCROLL_SPEED_LABEL); - } else if ((HWND)lparam == GetDlgItem(window, IDC_DETAIL_LEVEL_SLIDER)) { - name = GameDetailLevelNames[index]; - handle = GetDlgItem(window, IDC_DETAIL_LEVEL_LABEL); - } else if (GameActive == false && (HWND)lparam == GetDlgItem(window, IDC_DIFFICULTY_SLIDER)) { - name = GameDifficultyNames[index]; - handle = GetDlgItem(window, IDC_DIFFICULTY_LABEL); - } - if (handle) { - SetWindowText(handle, Fetch_String(name)); - } - } - break; - } - rc = 0; - } - return(rc); -} - - -/// -/// Handles the button presses of the game controls dialog. -/// This routine is called by the dialog procedure whenever a control notifies it. The -/// answer is stored back through the result pointer the dialog was created with, which is -/// what releases GameControlsClass::Dialog from its message loop. -/// -/// The game controls dialog window. -/// The identifier of the control that was activated. -/// The notification code the control sent. -void Game_Controls_Dialog_On_COMMAND(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - int* retval = (int *)GetWindowLongPtr(window, DWLP_USER); - - switch ((INT)message) { - case IDC_OPT_KEYBOARD_BTN: - if (lparam == 0 && GameActive == true) { - SpecialDialog = SDLG_KEYBOARD; - *retval = IDOK; - } - break; - - case IDC_OPT_SOUND_BTN: - if (lparam == 0 && GameActive == true) { - SpecialDialog = SDLG_SOUND; - *retval = IDOK; - } - break; - - case IDOK: - if (lparam == 0) { - *retval = IDOK; - } - break; - - case IDCANCEL: - *retval = IDCANCEL; - break; - } -} diff --git a/code/gamedlg.h b/code/gamedlg.h index 959267548..b37149fac 100644 --- a/code/gamedlg.h +++ b/code/gamedlg.h @@ -50,15 +50,4 @@ class GameControlsClass { return(GameDifficultyNames[difficulty]); } - - private: - void Set(void); - - private: - /* - * This is the window handle of the game controls dialog while it is displayed. The - * player's settings are read back off its controls, so the handle is only - * meaningful between the dialog being created and destroyed. - */ - HWND _Dialog; }; diff --git a/code/gametime.cpp b/code/gametime.cpp index a4ff06d15..37298e69c 100644 --- a/code/gametime.cpp +++ b/code/gametime.cpp @@ -37,6 +37,7 @@ #include "gametime.h" +#include "hostclock.h" #include "win.h" //========================================================================== @@ -59,7 +60,7 @@ GameTimeClass Game_Time; *=========================================================================*/ GameTimeClass::GameTimeClass( void ) { - game_start_time = timeGetTime(); + game_start_time = Host_Milliseconds(); } @@ -81,7 +82,7 @@ unsigned int GameTimeClass::Get_Time( void ) unsigned int curr_windows_time; unsigned int game_time; - curr_windows_time = timeGetTime(); + curr_windows_time = Host_Milliseconds(); if ( curr_windows_time <= game_start_time ) { // Handles the case if the windows time wraps while playing the game. game_time = MAX_ULONG - game_start_time + curr_windows_time; diff --git a/code/globals.cpp b/code/globals.cpp index 5b6a9a89a..d1e88541f 100644 --- a/code/globals.cpp +++ b/code/globals.cpp @@ -29,23 +29,10 @@ *---------------------------------------------------------------------------------------------* * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" -/// create all com interfaces here -#include "iblowfish.h" -#include "iblowfish_i.c" #include "sun.h" -#include "isun_i.c" -#include "ilocos.h" -#include "ilocos_i.c" -#include "ipiggy.h" -#include "ipiggy_i.c" -#include "iblockci.h" -#include "iblockci_i.c" -#include "iflyctrl.h" -#include "iflyctrl_i.c" -#undef INCLUDE_COM +#include "classids.h" #include "_voxel.h" #include "globals.h" @@ -264,7 +251,6 @@ bool AllowVoice = true; int Frame = 0; -int _dialog_count = 0; /*************************************************************************** diff --git a/code/globals.h b/code/globals.h index 4216938a4..635265475 100644 --- a/code/globals.h +++ b/code/globals.h @@ -243,7 +243,6 @@ extern int NewMaxAheadFrame2; extern bool VisceroidsAsSnoBees; extern bool Just4Fun; -extern int _dialog_count; extern int Seed; extern int CustomSeed; extern bool IgnoreInput; diff --git a/code/goptions.cpp b/code/goptions.cpp index 865a0b35d..ceb3bba5c 100644 --- a/code/goptions.cpp +++ b/code/goptions.cpp @@ -42,72 +42,35 @@ #include "gamedlg.h" #include "language/language.h" #include "loaddlg.h" -#include "ownrdraw.h" #include "queue.h" #include "restate.h" #include "savemgr.h" #include "scenario.h" #include "stats.h" +#include "ui/uiabort.h" +#include "ui/uigameoptions.h" #include "special.hh" -void Game_Options_On_INITDIALOG(HWND window); -INT_PTR CALLBACK Game_Options_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); -INT_PTR CALLBACK Abort_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); -void Abort_Dialog_On_COMMAND(HWND window, UINT message, WPARAM wparam, LPARAM lparam); -/// -/// Displays the in game options dialog. -/// This routine is used by the special dialog handler when the player calls up the options -/// screen. Which dialog appears depends on the kind of game in progress. Game input stays -/// locked out for as long as the dialog is up, and if the player asked for the mission -/// briefing it is restated on the way out. -/// -void Game_Options_Dialog(void) -{ - int rc = 0; - - HWND dialog; - if (Session.Type == GAME_NORMAL || Session.Type == GAME_SKIRMISH) { - dialog = OwnerDraw::Begin_Dialog(IDD_OPT_CTRL_SP, Game_Options_Dialog_Proc); - } else if (Session.Type == GAME_INTERNET) { - dialog = OwnerDraw::Begin_Dialog(IDD_OPT_CTRL_WOL, Game_Options_Dialog_Proc); - } else { - dialog = OwnerDraw::Begin_Dialog(IDD_OPT_CTRL_MP, Game_Options_Dialog_Proc); - } - - IgnoreInput = true; - Keyboard->Clear(); - - if (dialog) { - - SetWindowLongPtr(dialog, DWLP_USER, (LONG_PTR)&rc); - - OwnerDraw::Display_Dialog(dialog); - - while (rc == 0) { - if (OwnerDraw::Dialog_Message_Handler() == true) { - rc = IDOK; - } - } - OwnerDraw::End_Dialog(dialog); - } +// What the driver does on the way out. The briefing is restated after the screen has gone, +// which is where the dialog driver restated it. +static void Game_Options_Finish(UIGameOptionsPresenterClass const & screen) +{ Keyboard->Clear(); - if (rc == IDC_BRIEFING) { + if (screen.Choice == UIGameOptionsPresenterClass::CHOICE_BRIEFING) { Restate_Mission(Scen); } IgnoreInput = Scen->IsInputLocked; - if (rc == IDC_LOAD_GAME) { - if (IDC_LOAD_GAME) { - if (MouseCursor->Is_Hidden() == false && Scen->IsInputLocked == 1) { - Hide_Mouse(); - } else if (MouseCursor->Is_Hidden() == true && Scen->IsInputLocked == 0) { - Show_Mouse(); - } + if (screen.Choice == UIGameOptionsPresenterClass::CHOICE_LOADED) { + if (MouseCursor->Is_Hidden() == false && Scen->IsInputLocked == 1) { + Hide_Mouse(); + } else if (MouseCursor->Is_Hidden() == true && Scen->IsInputLocked == 0) { + Show_Mouse(); } } @@ -116,235 +79,43 @@ void Game_Options_Dialog(void) /// -/// Handles messages for the in game options dialog. -/// This routine offers every message to the owner draw system first. What is left it uses -/// to service the option buttons -- save, load, delete, briefing, resume, abort and -/// settings -- either acting on them directly or noting the player's choice for -/// Game_Options_Dialog to deal with once the dialog comes down. Dragging the game speed or -/// connection quality slider updates the label beside it. +/// Displays the in game options dialog. +/// This routine is used by the special dialog handler when the player calls up the options +/// screen. Which layout appears depends on the kind of game in progress. Game input stays +/// locked out for as long as the screen is up, and if the player asked for the mission +/// briefing it is restated on the way out. /// -/// Returns with TRUE if the owner draw system consumed the message. -INT_PTR CALLBACK Game_Options_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) +void Game_Options_Dialog(void) { - static int GameConnectionQualityNames[] = { - TXT_WORST_CONNECTION, - TXT_POOR_CONNECTION, - TXT_GOOD_CONNECTION, - TXT_BEST_CONNECTION - }; - - INT_PTR rc = OwnerDraw::Default_Dialog_Proc(window, message, wparam, lparam); + UIGameOptionsPresenterClass screen; + screen.Refresh(); - HWND handle; - - if (rc) { - return(rc); - } - - switch (message) { - - case WM_INITDIALOG: - Game_Options_On_INITDIALOG(window); - break; - - case WM_COMMAND: { - int code = HIWORD(wparam); - int* retval = (int *)GetWindowLongPtr(window, DWLP_USER); - - switch (LOWORD(wparam)) { - - case IDC_SAVE_GAME: - if (!code) { - if (Session.Type == GAME_NORMAL || Session.Type == GAME_SKIRMISH) { - ShowWindow(window, SW_HIDE); - UpdateWindow(MainWindow); - char description[512]; - strcpy(description, Scen->Description); - LoadOptionsClass().Save(description); - Game_Options_On_INITDIALOG(window); - ShowWindow(window, SW_SHOW); - UpdateWindow(window); - } else if (SaveManager.Is_Multiplayer_Saving_Allowed()) { - OutList.push_back(EventClass(PlayerPtr->HeapID, EventClass::SAVEGAME)); - *retval = IDC_SAVE_GAME; - } - } - break; - - case IDC_LOAD_GAME: - if (!code) { - if (Session.Type == GAME_NORMAL || Session.Type == GAME_SKIRMISH) { - ShowWindow(window, SW_HIDE); - UpdateWindow(MainWindow); - if (LoadOptionsClass().Load()) { - *retval = IDC_LOAD_GAME; - } else { - ShowWindow(window, SW_SHOW); - UpdateWindow(window); - } - } else if (SaveManager.Multiplayer_Load_Is_Allowed()) { - // A list opened from in here would sit inside the main loop and stall the - // match; the menu loop opens it between frames instead. - SpecialDialog = SDLG_LOAD; - *retval = IDC_LOAD_GAME; - } - } - break; - - case IDC_BRIEFING: - if (!code) { - *retval = IDC_BRIEFING; - } - break; - - case IDC_DELETE_GAME: - if (!code) { - ShowWindow(window, SW_HIDE); - UpdateWindow(MainWindow); - LoadOptionsClass().Delete(); - Game_Options_On_INITDIALOG(window); - ShowWindow(window, SW_SHOW); - UpdateWindow(window); - } - break; - - case IDC_RESUME_MISSION: - if (!code) { - if (Session.Type == GAME_INTERNET) { - handle = GetDlgItem(window, IDC_CTRLWOL_CONNECTION); - if (handle) { - int fudge = 3 - SendMessage(handle, TBM_GETPOS, 0, 0); - if (fudge != Session.LatencyFudge) { - OutList.push_back(EventClass(PlayerPtr->HeapID, EventClass::LATENCYFUDGE, fudge)); - DebugString("LATENCYFUDGE event created - %d\n", fudge); - } - } - handle = GetDlgItem(window, IDC_GAME_SPEED_SLIDER); - if (handle) { - int speed = (OptionsClass::MAX_SPEED_SETTING-1) - SendMessage(handle, TBM_GETPOS, 0, 0); - if (Options.GameSpeed != speed) { - OutList.push_back(EventClass(PlayerPtr->HeapID, EventClass::GAMESPEED, speed)); - } - } - } - *retval = IDOK; - } - break; - - case IDC_ABORT_MISSION: - if (!code) { - if (Session.Type == GAME_INTERNET) { - SpecialDialog = SDLG_SURRENDER; - if (!WestwoodOnline_Tournament) { - SpecialDialog = SDLG_ABORT; - } - } else { - SpecialDialog = SDLG_ABORT; - } - *retval = IDCANCEL; - } - break; - - case IDC_GAME_CONTROLS: - if (!code) { - SpecialDialog = SDLG_SETTINGS; - *retval = IDOK; - } - break; - - default: - break; - } - break; - } - - case WM_HSCROLL: { - if (LOWORD(wparam) == SB_THUMBTRACK) { - int pos = HIWORD(wparam); - int textid; - - if ((HWND)lparam == GetDlgItem(window, IDC_GAME_SPEED_SLIDER)) { - textid = GameSpeedNames[pos]; - handle = GetDlgItem(window, IDC_GAME_SPEED_LABEL); - } else if ((HWND)lparam == GetDlgItem(window, IDC_CTRLWOL_CONNECTION)) { - textid = GameConnectionQualityNames[pos]; - handle = GetDlgItem(window, IDC_SCROLL_SPEED_LABEL); - } else { - break; - } - - if (handle) { - Static_SetText(handle, Fetch_String(textid)); - } - } - break; - } + IgnoreInput = true; + Keyboard->Clear(); - default: - break; - } + UI_Game_Options_Screen(screen); - return(FALSE); + Game_Options_Finish(screen); } -/// -/// Prepares the controls of the game options dialog. -/// This routine is called when the dialog is created, and again whenever a save or delete -/// has changed what is on disk. It decides which buttons the current game type allows the -/// player to use and primes the game speed and connection quality sliders. -/// -void Game_Options_On_INITDIALOG(HWND window) +// Maps the screen's choice onto the value the special dialog handler expects. A screen that +// never opened answers zero, which is what the driver's own result was left at. +static int Abort_Choice_Result(UIAbortPresenterClass const & screen) { - HWND handle; - - if (Session.Type == GAME_NORMAL || Session.Type == GAME_SKIRMISH) { - bool present = LoadOptionsClass().Files_Present(); - - handle = GetDlgItem(window, IDC_LOAD_GAME); - if (handle) { - EnableWindow(handle, present); - } - - handle = GetDlgItem(window, IDC_DELETE_GAME); - if (handle) { - EnableWindow(handle, present); - } - } - - if (Session.Type != GAME_NORMAL && Session.Type != GAME_SKIRMISH) { - handle = GetDlgItem(window, IDC_SAVE_GAME); - if (handle) { - EnableWindow(handle, SaveManager.Is_Multiplayer_Saving_Allowed()); - } + switch (screen.Choice) { + case UIAbortPresenterClass::CHOICE_QUIT: + return(IDOK); - handle = GetDlgItem(window, IDC_LOAD_GAME); - if (handle) { - EnableWindow(handle, SaveManager.Multiplayer_Load_Is_Allowed() && MultiplayerLoadOptionsClass().Files_Present()); - } - } + case UIAbortPresenterClass::CHOICE_RESTART: + return(IDABORT); - if (Session.Type == GAME_INTERNET) { + case UIAbortPresenterClass::CHOICE_CANCEL: + return(IDCANCEL); - handle = GetDlgItem(window, IDC_CTRLWOL_CONNECTION); - if (handle) { - SetSliderRangeAndPos(handle, 0, 3, 3 - Session.LatencyFudge); - } - - handle = GetDlgItem(window, IDC_GAME_SPEED_SLIDER); - if (handle) { - Slider_SetRange(handle, 0, OptionsClass::MAX_SPEED_SETTING-1); - Slider_SetPos(handle, (OptionsClass::MAX_SPEED_SETTING-1) - Options.GameSpeed); - } - } - - if (Session.Type == GAME_SKIRMISH) { - handle = GetDlgItem(window, IDC_BRIEFING); - if (handle) { - EnableWindow(handle, FALSE); - } + default: + return(0); } - } @@ -358,90 +129,9 @@ void Game_Options_On_INITDIALOG(HWND window) /// IDCANCEL to carry on playing. int Abort_Dialog(void) { - int rc = 0; - - HWND dialog = OwnerDraw::Begin_Dialog(IDD_MISSION_ABORT, Abort_Dialog_Proc); - - if (dialog) { - - SetWindowLongPtr(dialog, DWLP_USER, (LONG_PTR)&rc); - - OwnerDraw::Display_Dialog(dialog); - - while (rc == 0) { - if (OwnerDraw::Dialog_Message_Handler() == true) { - rc = IDOK; - } - } - OwnerDraw::End_Dialog(dialog); - } - return(rc); -} - - -/// -/// Handles messages for the abort mission dialog. -/// This routine offers every message to the owner draw system first. What is left it uses -/// to relabel the restart button as a surrender for a multiplayer game, and to pass button -/// presses along to Abort_Dialog_On_COMMAND. -/// -/// Returns with the result of the owner draw default dialog handler. -INT_PTR CALLBACK Abort_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - HWND handle; - - INT_PTR rc = OwnerDraw::Default_Dialog_Proc(window, message, wparam, lparam); - if (rc == 0) { - switch (message) { - case WM_INITDIALOG: - handle = GetDlgItem(window, IDC_RESTART_MISSION); - if (Session.Type != GAME_NORMAL) { - SetWindowText(handle, Fetch_String(TXT_SURRENDER)); - if (PlayerPtr->IsDefeated || PlayerPtr->IsToWin || PlayerPtr->IsToLose || PlayerPtr->IsToDie) { - EnableWindow(handle, FALSE); - } - } - break; + UIAbortPresenterClass screen; + screen.Refresh(); + UI_Abort_Screen(screen); - case WM_COMMAND: - Abort_Dialog_On_COMMAND(window, LOWORD(wparam), 0, HIWORD(wparam)); - break; - } - rc = 0; - } - return(rc); -} - - -/// -/// Handles a button press in the abort mission dialog. -/// This routine records the player's choice in the result variable that Abort_Dialog -/// attached to the dialog window, which is what ends the dialog's message pump. -/// -/// The control identifier of the button that was pressed. -/// The notification code that came with the button press. -void Abort_Dialog_On_COMMAND(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - int* retval = (int *)GetWindowLongPtr(window, DWLP_USER); - - switch ((int)message) { - case IDC_ABORT_MISSION: - if (lparam == 0) { - *retval = IDOK; - } - break; - - case IDC_RESTART_MISSION: - if (lparam == 0) { - *retval = IDABORT; - } - break; - - case IDOK: - case IDCANCEL: - if (lparam == 0) { - *retval = IDCANCEL; - } - break; - } + return(Abort_Choice_Result(screen)); } diff --git a/code/grphmenu.cpp b/code/grphmenu.cpp index 9f814fbf9..ba2238e4b 100644 --- a/code/grphmenu.cpp +++ b/code/grphmenu.cpp @@ -19,7 +19,7 @@ #include "ini.h" #include "keyboard.h" #include "msanim.h" -#include "ownrdraw.h" +#include "drawhelp.h" #include "theme.h" GraphicMenu * _Graphic_Menu(INIClass const & ini, const char * name); @@ -164,7 +164,7 @@ int GraphicMenu::Presentation(void) { Theme.Play_Song(Theme.From_Name(ThemeName.Peek())); - OwnerDraw::Capture_Mouse(); + Release_Pointer_To_Host(); HiddenSurface->Fill(0); AlternateSurface->Fill(0); @@ -224,7 +224,7 @@ int GraphicMenu::Presentation(void) item->Action(&Engine); } - OwnerDraw::Release_Mouse(); + Recapture_Pointer(); Theme.Fade_Out(); diff --git a/code/gscreen.cpp b/code/gscreen.cpp index 30316d80b..b5e9fee1d 100644 --- a/code/gscreen.cpp +++ b/code/gscreen.cpp @@ -450,27 +450,6 @@ void GScreenClass::Blit_Display(void) } -/// -/// Repaints the dialog controls that the last frame drew over. -/// The dialogs are ordinary child windows that paint themselves onto the game's own -/// surfaces, so a frame put on top of them takes their pixels with it. Windows is asked -/// to repaint them straight away, and the controls are grandchildren of the main window -/// rather than children, so the whole subtree has to be included. -/// -void Heal_Dialog_Controls(void) -{ - if (_dialog_count <= 0 || MainWindow == NULL) { - return; - } - - for (HWND child = GetWindow(MainWindow, GW_CHILD); child != NULL; child = GetWindow(child, GW_HWNDNEXT)) { - if (IsWindowVisible(child)) { - RedrawWindow(child, NULL, NULL, RDW_INVALIDATE|RDW_UPDATENOW|RDW_ERASE|RDW_ALLCHILDREN); - } - } -} - - /// /// Presents a rendered surface onto the visible surface. /// This is the low level routine that gets a finished frame in front of the player. The @@ -578,7 +557,6 @@ void Update_Visible_Surface(Surface *surface, Rect *rect) */ VisibleSurface->Blit_From(dest_rect, *surface, src_rect, false, true); - Heal_Dialog_Controls(); Video_Present_If_Dirty(); } diff --git a/code/gscreen.h b/code/gscreen.h index 37f9623cb..d091d889b 100644 --- a/code/gscreen.h +++ b/code/gscreen.h @@ -144,4 +144,3 @@ class GScreenClass }; void Update_Visible_Surface(Surface *surface = HiddenSurface, Rect *rect = NULL); -void Heal_Dialog_Controls(void); diff --git a/code/hostclock.h b/code/hostclock.h new file mode 100644 index 000000000..396d494a3 --- /dev/null +++ b/code/hostclock.h @@ -0,0 +1,36 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#pragma once + +#include +#include +#include + +// The engine's coarse clock, in milliseconds from a clock that only ever moves forward. +// Every caller measures an interval with it and none depends on where it starts. The +// reading wraps roughly every forty nine days, so compare differences and not the +// readings themselves. +inline uint32_t Host_Milliseconds(void) +{ + return((uint32_t)std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); +} + + +// Yields the processor for at least the requested number of milliseconds. A zero wait gives +// up the rest of the current time slice without pausing, which is what the frame loops use. +inline void Host_Sleep(unsigned int milliseconds) +{ + if (milliseconds == 0) { + std::this_thread::yield(); + return; + } + std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds)); +} diff --git a/code/house.cpp b/code/house.cpp index a288589d9..e187c2d08 100644 --- a/code/house.cpp +++ b/code/house.cpp @@ -128,7 +128,6 @@ * HouseClass::Random_Cell_In_Zone -- Find a (technically) legal cell in the zone specified. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "house.h" @@ -690,8 +689,15 @@ HouseClass::~HouseClass (void) } SuperWeapon.Clear(); + // A tag removes itself from this list as it dies; a slot a failed load left empty is + // removed here, or the list would never drain. while (HouseTags.Count() > 0) { - delete HouseTags[0]; + TagClass * const tag = HouseTags[0]; + if (tag == NULL) { + HouseTags.Delete_Index(0); + } else { + delete tag; + } } AbstractTypePtrTracker.Delete(this); @@ -6432,8 +6438,8 @@ void HouseClass::Compute_CRC(CRCEngine & crc) const /// record, so they are disposed of before the saved members are read over the top of them. /// /// The stream to read the house from. -/// Returns with S_OK, or the failure code reported by the stream. -HRESULT STDMETHODCALLTYPE HouseClass::Load(IStream *stream) +/// bool; Was the record read whole? +bool HouseClass::Load(SaveStreamClass & stream) { while (SuperWeapon.Count()) { delete SuperWeapon[0]; @@ -6623,18 +6629,9 @@ void HouseClass::Serialize(SaveStreamClass & stream) } -/// -/// Fetches the class identifier of this object. -/// This routine is part of the persistence contract. The load system uses the identifier to -/// discover which class to build when the object is read back in. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE HouseClass::GetClassID(CLSID * retval) +ClassID HouseClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_HouseClass; - return(S_OK); + return(ClassID_HouseClass); } @@ -9206,30 +9203,6 @@ void HouseClass::AI_Drop_Pods(SuperClass * super) } -/// -/// Adds a reference to this house. -/// Houses are permanent heap objects rather than reference counted ones, so this routine -/// exists only to satisfy the IUnknown contract. -/// -/// Returns with the reference count, which is always one. -ULONG STDMETHODCALLTYPE HouseClass::AddRef(void) -{ - return(1); -} - - -/// -/// Releases a reference to this house. -/// Houses are permanent heap objects rather than reference counted ones, so this routine -/// exists only to satisfy the IUnknown contract. It never destroys the house. -/// -/// Returns with the reference count, which is always one. -ULONG STDMETHODCALLTYPE HouseClass::Release(void) -{ - return(1); -} - - /// /// Fetches the RTTI type of this object. /// diff --git a/code/house.h b/code/house.h index 7f4e50526..fa42696b2 100644 --- a/code/house.h +++ b/code/house.h @@ -735,13 +735,11 @@ class HouseClass : public AbstractClass HouseClass(HouseTypeClass const * type = NULL); virtual ~HouseClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; - virtual HRESULT STDMETHODCALLTYPE Load(IStream * stream) override; + virtual ClassID Class_ID(void) const override; + virtual bool Load(SaveStreamClass & stream) override; virtual void Serialize(SaveStreamClass & stream) override; - virtual ULONG STDMETHODCALLTYPE AddRef(void) override; - virtual ULONG STDMETHODCALLTYPE Release(void) override; int Available_Money(void); int Available_Storage(void); @@ -1060,8 +1058,6 @@ class HouseClass : public AbstractClass BuildChoiceClass(UrgencyType urgency=URGENCY_NONE, StructType structure=STRUCT_NONE) : Urgency(urgency), Structure(structure) {}; bool operator==(BuildChoiceClass const & ) const {return(false);} bool operator!=(BuildChoiceClass const & ) const {return(true);} - HRESULT Save(IStream *) const {return(S_OK);}; - HRESULT Load(IStream *) {return(S_OK);}; }; static DynamicVectorClass BuildChoice; diff --git a/code/houstype.cpp b/code/houstype.cpp index 593e2be76..9083169e3 100644 --- a/code/houstype.cpp +++ b/code/houstype.cpp @@ -39,7 +39,6 @@ * HouseTypeClass::operator new -- Allocates a house type class object from special heap. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "houstype.h" @@ -231,17 +230,6 @@ void HouseTypeClass::Compute_CRC(CRCEngine & crc) const } -/// -/// Determines if this house type has been changed since it was last saved. -/// House types are written out wholesale rather than on demand, so the answer never varies. -/// -/// Returns with S_OK. -HRESULT STDMETHODCALLTYPE HouseTypeClass::IsDirty(void) -{ - return(false); -} - - /// /// Lists the members this house type carries. /// @@ -270,49 +258,9 @@ void HouseTypeClass::Serialize(SaveStreamClass & stream) } -/// -/// Fetches the requested interface from this house type. -/// House types serve up the persistence and RTTI interfaces that the save game system asks -/// them for. -/// -/// Returns with S_OK, or E_NOINTERFACE if the interface is not supported. -HRESULT STDMETHODCALLTYPE HouseTypeClass::QueryInterface(REFIID riid, LPVOID * ppvObject) +ClassID HouseTypeClass::Class_ID(void) const { - if (ppvObject == NULL) { - return(E_POINTER); - } - - *ppvObject = NULL; - - if (riid == IID_IUnknown) { - *ppvObject = (IUnknown *)(IPersistStream *)this; - } - if (riid == IID_IPersist) { - *ppvObject = (IPersistStream *)this; - } - if (riid == IID_IPersistStream) { - *ppvObject = (IPersist *)this; - } - if (*ppvObject == NULL) { - return(E_NOINTERFACE); - } - - AddRef(); - return(S_OK); -} - - -/// -/// Fetches the class identifier of this object. -/// The save game system stores this identifier so that the object can be recreated as the -/// correct class when the game is loaded. -/// -/// Returns with S_OK, or E_POINTER if no return pointer was supplied. -HRESULT STDMETHODCALLTYPE HouseTypeClass::GetClassID(CLSID * retval) -{ - if (retval == NULL) return(E_POINTER); - *retval = CLSID_HouseTypeClass; - return(S_OK); + return(ClassID_HouseTypeClass); } @@ -349,25 +297,3 @@ int HouseTypeClass::Fetch_Heap_ID(void) const } -/// -/// Adds a reference to this house type. -/// House types are not reference counted -- they live for the duration of the game, so this -/// routine exists only to satisfy the IUnknown contract. -/// -/// Returns with the reference count, which is always one. -ULONG STDMETHODCALLTYPE HouseTypeClass::AddRef(void) -{ - return(1); -} - - -/// -/// Releases a reference to this house type. -/// House types are not reference counted -- they live for the duration of the game, so this -/// routine exists only to satisfy the IUnknown contract. -/// -/// Returns with the reference count, which is always one. -ULONG STDMETHODCALLTYPE HouseTypeClass::Release(void) -{ - return(1); -} diff --git a/code/houstype.h b/code/houstype.h index ef3cb3d95..0020ea187 100644 --- a/code/houstype.h +++ b/code/houstype.h @@ -102,12 +102,8 @@ class HouseTypeClass : public AbstractTypeClass HouseTypeClass(char const * ininame = NULL); virtual ~HouseTypeClass() override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; - virtual HRESULT STDMETHODCALLTYPE IsDirty(void) override; + virtual ClassID Class_ID(void) const override; - virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID * ppvObject) override; - virtual ULONG STDMETHODCALLTYPE AddRef(void) override; - virtual ULONG STDMETHODCALLTYPE Release(void) override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/hover.cpp b/code/hover.cpp index b108e633d..369b36fc2 100644 --- a/code/hover.cpp +++ b/code/hover.cpp @@ -66,12 +66,11 @@ HoverLocomotionClass::HoverLocomotionClass(void) : /// /// Pointer to the object this locomotor will drive. /// Returns with the result of the attach operation. -HRESULT STDMETHODCALLTYPE HoverLocomotionClass::Link_To_Object(void *pointer) +void HoverLocomotionClass::Link_To_Object(void *pointer) { - HRESULT res = BASECLASS::Link_To_Object(pointer); + BASECLASS::Link_To_Object(pointer); FacingClass face(2 * LinkedTo->TClass->ROT); Facing = face; - return(res); } @@ -141,7 +140,7 @@ void HoverLocomotionClass::Gravity_AI(void) /// /// Pointer to the render cache key to update; may be NULL. /// Returns with the matrix to render the object with. -Matrix3D STDMETHODCALLTYPE HoverLocomotionClass::Draw_Matrix(int *key) +Matrix3D HoverLocomotionClass::Draw_Matrix(int *key) { if (!Is_Powered()) { int ramp = Map[(Coord const &)(LinkedTo->PositionCoord)].Ramp; @@ -164,7 +163,7 @@ Matrix3D STDMETHODCALLTYPE HoverLocomotionClass::Draw_Matrix(int *key) /// water, and applies the bob and sag of the hover cushion. /// /// bool; Is the object still moving? -boolean STDMETHODCALLTYPE HoverLocomotionClass::Process(void) +bool HoverLocomotionClass::Process(void) { if (Is_Moving() && Is_Moving1()) { Motion_AI(); @@ -288,7 +287,7 @@ boolean STDMETHODCALLTYPE HoverLocomotionClass::Process(void) /// Does the object have a move order outstanding? /// /// bool; Is the object either headed somewhere or bound for a destination? -boolean STDMETHODCALLTYPE HoverLocomotionClass::Is_Moving(void) +bool HoverLocomotionClass::Is_Moving(void) { return(DestinationCoord != COORD_NONE || HeadToCoord != COORD_NONE); } @@ -300,7 +299,7 @@ boolean STDMETHODCALLTYPE HoverLocomotionClass::Is_Moving(void) /// moving, but it is not moving now. /// /// bool; Is the object traveling at this moment? -boolean STDMETHODCALLTYPE HoverLocomotionClass::Is_Moving_Now(void) +bool HoverLocomotionClass::Is_Moving_Now(void) { return(Is_Moving() && Height != 0.0); } @@ -311,7 +310,7 @@ boolean STDMETHODCALLTYPE HoverLocomotionClass::Is_Moving_Now(void) /// /// Returns with the destination coordinate, or COORD_NONE if the object has no /// move order outstanding. -Coord STDMETHODCALLTYPE HoverLocomotionClass::Destination(void) +Coord HoverLocomotionClass::Destination(void) { if (DestinationCoord != COORD_NONE) { return(DestinationCoord); @@ -325,7 +324,7 @@ Coord STDMETHODCALLTYPE HoverLocomotionClass::Destination(void) /// /// Returns with the intermediate destination, or with the object's current /// position if it is not headed anywhere. -Coord STDMETHODCALLTYPE HoverLocomotionClass::Head_To_Coord(void) +Coord HoverLocomotionClass::Head_To_Coord(void) { if (HeadToCoord != COORD_NONE) { return(HeadToCoord); @@ -341,7 +340,7 @@ Coord STDMETHODCALLTYPE HoverLocomotionClass::Head_To_Coord(void) /// the drive is started if the object is not already under way. /// /// The coordinate to move to. -void STDMETHODCALLTYPE HoverLocomotionClass::Move_To(Coord to) +void HoverLocomotionClass::Move_To(Coord to) { DestinationCoord = to; if (Is_Powered() && Is_Ion_Sensitive() && IonStormClass::Is_Ion_Storm_Active()) { @@ -678,7 +677,7 @@ void HoverLocomotionClass::Motion_AI(void) /// The object will still coast into the spot it has already reserved, but it will not /// carry on toward its former destination once it arrives. /// -void STDMETHODCALLTYPE HoverLocomotionClass::Stop_Moving(void) +void HoverLocomotionClass::Stop_Moving(void) { if (DestinationCoord != HeadToCoord) { DestinationCoord = COORD_NONE; @@ -918,7 +917,7 @@ void HoverLocomotionClass::Start_Of_Move(int num) /// and slews as it sinks rather than dropping neatly in place. /// /// bool; Was the power turned off? -boolean STDMETHODCALLTYPE HoverLocomotionClass::Power_Off(void) +bool HoverLocomotionClass::Power_Off(void) { if (Is_Powered() && LinkedTo->CurrentMission != MISSION_SLEEP) { Do_Shove(); @@ -937,7 +936,7 @@ boolean STDMETHODCALLTYPE HoverLocomotionClass::Power_Off(void) /// way onto the ground, so it is treated as powered for as long as it has height to lose. /// /// bool; Is the object still under power? -boolean STDMETHODCALLTYPE HoverLocomotionClass::Is_Powered(void) +bool HoverLocomotionClass::Is_Powered(void) { if (!BASECLASS::Is_Powered() && LinkedTo->HeightAGL <= 0) { return(false); @@ -953,7 +952,7 @@ boolean STDMETHODCALLTYPE HoverLocomotionClass::Is_Powered(void) /// units across the factory doorway. /// /// bool; Should an ion storm cut this object's power? -boolean STDMETHODCALLTYPE HoverLocomotionClass::Is_Ion_Sensitive(void) +bool HoverLocomotionClass::Is_Ion_Sensitive(void) { BuildingClass *bptr; if (LinkedTo->In_Radio_Contact()) { @@ -997,7 +996,7 @@ boolean STDMETHODCALLTYPE HoverLocomotionClass::Is_Ion_Sensitive(void) /// /// The direction to push the object toward. /// bool; Was the object pushed? -boolean STDMETHODCALLTYPE HoverLocomotionClass::Push(DirType dir) +bool HoverLocomotionClass::Push(DirType dir) { if (Is_Powered() && !WasPushed) { @@ -1042,7 +1041,7 @@ boolean STDMETHODCALLTYPE HoverLocomotionClass::Push(DirType dir) /// /// The direction to shove the object toward. /// bool; Was the object shoved? -boolean STDMETHODCALLTYPE HoverLocomotionClass::Shove(DirType dir) +bool HoverLocomotionClass::Shove(DirType dir) { if (Push(dir)) { Do_Shove(); @@ -1067,18 +1066,9 @@ void HoverLocomotionClass::Do_Shove(void) } -/// -/// Fetches the class identifier of this locomotor. -/// The persistence system uses this identifier to create a locomotor of the right kind -/// when the object it drives is loaded back in. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE HoverLocomotionClass::GetClassID(CLSID * retval) +ClassID HoverLocomotionClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_HoverLocomotion; - return(S_OK); + return(ClassID_HoverLocomotion); } @@ -1109,7 +1099,7 @@ void HoverLocomotionClass::Serialize(SaveStreamClass & stream) /// layer with ordinary vehicles. /// /// Returns with the layer this object belongs in. -LayerType STDMETHODCALLTYPE HoverLocomotionClass::In_Which_Layer(void) +LayerType HoverLocomotionClass::In_Which_Layer(void) { return(LAYER_GROUND); } @@ -1140,7 +1130,7 @@ void HoverLocomotionClass::Start(void) /// /// The marking operation to perform; MARK_UP releases the cell, /// anything else reserves it. -void STDMETHODCALLTYPE HoverLocomotionClass::Mark_All_Occupation_Bits(int mark) +void HoverLocomotionClass::Mark_All_Occupation_Bits(int mark) { if (mark == MARK_UP) { Coord coord = Head_To_Coord(); @@ -1159,7 +1149,7 @@ void STDMETHODCALLTYPE HoverLocomotionClass::Mark_All_Occupation_Bits(int mark) /// /// The coordinate to compare the current destination against. /// bool; Is the object moving to this location? -boolean STDMETHODCALLTYPE HoverLocomotionClass::Is_Moving_Here(Coord to) +bool HoverLocomotionClass::Is_Moving_Here(Coord to) { Coord coord = Head_To_Coord(); diff --git a/code/hover.h b/code/hover.h index ab7bce672..4b22c0869 100644 --- a/code/hover.h +++ b/code/hover.h @@ -36,28 +36,28 @@ class HoverLocomotionClass : public LocomotionClass HoverLocomotionClass(void); virtual ~HoverLocomotionClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; - virtual HRESULT STDMETHODCALLTYPE Link_To_Object(void *pointer) override; - virtual boolean STDMETHODCALLTYPE Is_Moving(void) override; - virtual Coord STDMETHODCALLTYPE Destination(void) override; - virtual Coord STDMETHODCALLTYPE Head_To_Coord(void) override; - virtual Matrix3D STDMETHODCALLTYPE Draw_Matrix(int *key) override; - virtual boolean STDMETHODCALLTYPE Process(void) override; - virtual void STDMETHODCALLTYPE Move_To(Coord to) override; - virtual void STDMETHODCALLTYPE Stop_Moving(void) override; - virtual void STDMETHODCALLTYPE Do_Turn(DirType coord) override; - virtual boolean STDMETHODCALLTYPE Power_Off(void) override; - virtual boolean STDMETHODCALLTYPE Is_Powered(void) override; - virtual boolean STDMETHODCALLTYPE Is_Ion_Sensitive(void) override; - virtual boolean STDMETHODCALLTYPE Push(DirType dir) override; - virtual boolean STDMETHODCALLTYPE Shove(DirType dir) override; - virtual LayerType STDMETHODCALLTYPE In_Which_Layer(void) override; - virtual boolean STDMETHODCALLTYPE Is_Moving_Now(void) override; - virtual void STDMETHODCALLTYPE Mark_All_Occupation_Bits(int mark) override; - virtual boolean STDMETHODCALLTYPE Is_Moving_Here(Coord to) override; + virtual void Link_To_Object(void *pointer) override; + virtual bool Is_Moving(void) override; + virtual Coord Destination(void) override; + virtual Coord Head_To_Coord(void) override; + virtual Matrix3D Draw_Matrix(int *key) override; + virtual bool Process(void) override; + virtual void Move_To(Coord to) override; + virtual void Stop_Moving(void) override; + virtual void Do_Turn(DirType coord) override; + virtual bool Power_Off(void) override; + virtual bool Is_Powered(void) override; + virtual bool Is_Ion_Sensitive(void) override; + virtual bool Push(DirType dir) override; + virtual bool Shove(DirType dir) override; + virtual LayerType In_Which_Layer(void) override; + virtual bool Is_Moving_Now(void) override; + virtual void Mark_All_Occupation_Bits(int mark) override; + virtual bool Is_Moving_Here(Coord to) override; private: diff --git a/code/iblockci.h b/code/iblockci.h deleted file mode 100644 index 0240dffd4..000000000 --- a/code/iblockci.h +++ /dev/null @@ -1,27 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2026 OpenTS contributors - * - * See LICENSE.md for applicable additional terms and warranty disclaimers. - ******************************************************************************/ - -#pragma once - -#include - -/// Names and comments from TLBs - -EXTERN_C const IID IID_IBlockCipher; - -MIDL_INTERFACE("E0113100-6A7C-11D1-B6F9-00A024DDAFD1") -IBlockCipher : public IUnknown -{ -public: - virtual HRESULT STDMETHODCALLTYPE Set_Key(LONG keylength, const void *key) = 0; - virtual HRESULT STDMETHODCALLTYPE get_Max_Key_Length(LONG *length) = 0; - virtual HRESULT STDMETHODCALLTYPE get_Block_Size(LONG *length) = 0; - virtual HRESULT STDMETHODCALLTYPE Encrypt(LONG length, const void *plaintext, void *cyphertext) = 0; - virtual HRESULT STDMETHODCALLTYPE Decrypt(LONG length, const void *cyphertext, void *plaintext) = 0; -}; diff --git a/code/iblockci_i.c b/code/iblockci_i.c deleted file mode 100644 index 750d2597f..000000000 --- a/code/iblockci_i.c +++ /dev/null @@ -1,52 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2026 OpenTS contributors - * - * See LICENSE.md for applicable additional terms and warranty disclaimers. - ******************************************************************************/ - -/* this file contains the actual definitions of */ -/* the IIDs and CLSIDs */ - -/* link this file in with the server and any clients */ - - -/* File created by MIDL compiler version X.XX.XXXX */ -/* at XXX XXX XX XX:XX:XX XXXX - */ -/* Compiler settings for XXXX.idl: - Oicf (OptLev=i2), W1, Zp8, env=Win32, ms_ext, c_ext - error checks: none -*/ -//@@MIDL_FILE_HEADING( ) -#ifdef __cplusplus -extern "C"{ -#endif - - -#ifndef __IID_DEFINED__ -#define __IID_DEFINED__ - -typedef struct _IID -{ - unsigned long x; - unsigned short s1; - unsigned short s2; - unsigned char c[8]; -} IID; - -#endif // __IID_DEFINED__ - -#ifndef CLSID_DEFINED -#define CLSID_DEFINED -typedef IID CLSID; -#endif // CLSID_DEFINED - -const IID IID_IBlockCipher = {0xE0113100,0x6A7C,0x11D1,{0xB6,0xF9,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -#ifdef __cplusplus -} -#endif diff --git a/code/iblowfish.h b/code/iblowfish.h deleted file mode 100644 index 4c01ee694..000000000 --- a/code/iblowfish.h +++ /dev/null @@ -1,17 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2026 OpenTS contributors - * - * See LICENSE.md for applicable additional terms and warranty disclaimers. - ******************************************************************************/ - -#pragma once - -#include - -/// Names and comments from TLBs - -EXTERN_C const IID LIBID_BlowfishLibrary; -EXTERN_C const CLSID CLSID_BlowfishObject; diff --git a/code/iblowfish_i.c b/code/iblowfish_i.c deleted file mode 100644 index 71ad0728b..000000000 --- a/code/iblowfish_i.c +++ /dev/null @@ -1,55 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2026 OpenTS contributors - * - * See LICENSE.md for applicable additional terms and warranty disclaimers. - ******************************************************************************/ - -/* this file contains the actual definitions of */ -/* the IIDs and CLSIDs */ - -/* link this file in with the server and any clients */ - - -/* File created by MIDL compiler version X.XX.XXXX */ -/* at XXX XXX XX XX:XX:XX XXXX - */ -/* Compiler settings for XXXX.idl: - Oicf (OptLev=i2), W1, Zp8, env=Win32, ms_ext, c_ext - error checks: none -*/ -//@@MIDL_FILE_HEADING( ) -#ifdef __cplusplus -extern "C"{ -#endif - - -#ifndef __IID_DEFINED__ -#define __IID_DEFINED__ - -typedef struct _IID -{ - unsigned long x; - unsigned short s1; - unsigned short s2; - unsigned char c[8]; -} IID; - -#endif // __IID_DEFINED__ - -#ifndef CLSID_DEFINED -#define CLSID_DEFINED -typedef IID CLSID; -#endif // CLSID_DEFINED - -const IID LIBID_BlowfishLibrary = {0xE7F91750,0x8861,0x11d1,{0xB7,0x07,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_BlowfishObject = {0x1440ad10,0x6aa8,0x11d1,{0xb6,0xf9,0x00,0xa0,0x24,0xdd,0xaf,0xd1}}; - - -#ifdef __cplusplus -} -#endif diff --git a/code/iff.h b/code/iff.h index fc4eec5c0..8cdb39882 100644 --- a/code/iff.h +++ b/code/iff.h @@ -31,6 +31,9 @@ #pragma once +#include +#include + class Buffer; #define LZW_SUPPORTED FALSE @@ -76,11 +79,15 @@ enum CompressionType { struct CompHeaderType { char Method; // Compression method (CompressionType). char pad; // Reserved pad byte (always 0). - int Size; // Size of the uncompressed data. - short Skip; // Number of bytes to skip before data. + std::int32_t Size; // Size of the uncompressed data. + std::int16_t Skip; // Number of bytes to skip before data. }; #pragma pack(pop) +static_assert(sizeof(CompHeaderType) == 8, "Compressed block header layout changed"); +static_assert(offsetof(CompHeaderType, Size) == 2, "Compressed block header layout changed"); +static_assert(offsetof(CompHeaderType, Skip) == 6, "Compressed block header layout changed"); + /*=========================================================================*/ /* The following prototypes are for the file: IFF.CPP */ diff --git a/code/iflyctrl.h b/code/iflyctrl.h index 958e59d47..527143e0f 100644 --- a/code/iflyctrl.h +++ b/code/iflyctrl.h @@ -9,43 +9,35 @@ #pragma once -#include +#include "win.h" -/// Names and comments from TLBs -EXTERN_C const IID IID_IFlyControl; -MIDL_INTERFACE("820F501C-4F39-11D2-9B70-00104B972FE8") -IFlyControl : public IUnknown +struct IFlyControl { -public: /* * Landing altitude */ - virtual LONG STDMETHODCALLTYPE Landing_Altitude(void) = 0; + virtual LONG Landing_Altitude(void) = 0; /* * Lading direction */ - virtual LONG STDMETHODCALLTYPE Landing_Direction(void) = 0; + virtual LONG Landing_Direction(void) = 0; /* * Loaded with cargo? */ - virtual BOOL STDMETHODCALLTYPE Is_Loaded(void) = 0; + virtual BOOL Is_Loaded(void) = 0; /* * Does it strafe over the target rather than hover? */ - virtual LONG STDMETHODCALLTYPE Is_Strafe(void) = 0; + virtual LONG Is_Strafe(void) = 0; /* * Is the aircraft locked into straight flight? */ - virtual LONG STDMETHODCALLTYPE Is_Locked(void) = 0; + virtual LONG Is_Locked(void) = 0; }; -/* - * IFlyControl com smart pointer declaration. - */ -_COM_SMARTPTR_TYPEDEF(IFlyControl, __uuidof(IFlyControl)); diff --git a/code/iflyctrl_i.c b/code/iflyctrl_i.c deleted file mode 100644 index 994c2ab42..000000000 --- a/code/iflyctrl_i.c +++ /dev/null @@ -1,52 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2026 OpenTS contributors - * - * See LICENSE.md for applicable additional terms and warranty disclaimers. - ******************************************************************************/ - -/* this file contains the actual definitions of */ -/* the IIDs and CLSIDs */ - -/* link this file in with the server and any clients */ - - -/* File created by MIDL compiler version X.XX.XXXX */ -/* at XXX XXX XX XX:XX:XX XXXX - */ -/* Compiler settings for XXXX.idl: - Oicf (OptLev=i2), W1, Zp8, env=Win32, ms_ext, c_ext - error checks: none -*/ -//@@MIDL_FILE_HEADING( ) -#ifdef __cplusplus -extern "C"{ -#endif - - -#ifndef __IID_DEFINED__ -#define __IID_DEFINED__ - -typedef struct _IID -{ - unsigned long x; - unsigned short s1; - unsigned short s2; - unsigned char c[8]; -} IID; - -#endif // __IID_DEFINED__ - -#ifndef CLSID_DEFINED -#define CLSID_DEFINED -typedef IID CLSID; -#endif // CLSID_DEFINED - -const IID IID_IFlyControl = {0x820F501C,0x4F39,0x11D2,{0x9B,0x70,0x00,0x10,0x4B,0x97,0x2F,0xE8}}; - - -#ifdef __cplusplus -} -#endif diff --git a/code/ilinkstm.h b/code/ilinkstm.h deleted file mode 100644 index 055b91573..000000000 --- a/code/ilinkstm.h +++ /dev/null @@ -1,29 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2026 OpenTS contributors - * - * See LICENSE.md for applicable additional terms and warranty disclaimers. - ******************************************************************************/ - -#pragma once - -#include - -/// Names and comments from TLBs - -EXTERN_C const IID IID_ILinkStream; - -MIDL_INTERFACE("0D5CD78E-6470-11D2-9B74-00104B972FE8") -ILinkStream : public IUnknown -{ -public: - virtual HRESULT STDMETHODCALLTYPE Link_Stream(IUnknown *stream) = 0; - virtual HRESULT STDMETHODCALLTYPE Unlink_Stream(IUnknown **stream) = 0; -}; - -/* - * ILinkStream com smart pointer declaration. - */ -//_COM_SMARTPTR_TYPEDEF(ILinkStream, __uuidof(ILinkStream)); diff --git a/code/iloco.h b/code/iloco.h index 1d43d5e35..19f01d2bf 100644 --- a/code/iloco.h +++ b/code/iloco.h @@ -20,251 +20,245 @@ #include "visual.hh" #include "zgrad.hh" -#include +#include -/// Names and comments from TLBs -EXTERN_C const IID IID_ILocomotion; /* * Game object locomotion handler. */ -MIDL_INTERFACE("070F3290-9841-11D1-B709-00A024DDAFD1") -ILocomotion : public IUnknown +struct ILocomotion { -public: + virtual ~ILocomotion(void) {} + /* * Links object to locomotor. */ - virtual HRESULT STDMETHODCALLTYPE Link_To_Object(void *pointer) = 0; + virtual void Link_To_Object(void *pointer) = 0; /* * Sees if object is moving. */ - virtual boolean STDMETHODCALLTYPE Is_Moving(void) = 0; + virtual bool Is_Moving(void) = 0; /* * Fetches destination coordinate. */ - virtual Coord STDMETHODCALLTYPE Destination(void) = 0; + virtual Coord Destination(void) = 0; /* * Fetches immediate (next cell) destination coordinate. */ - virtual Coord STDMETHODCALLTYPE Head_To_Coord(void) = 0; + virtual Coord Head_To_Coord(void) = 0; /* * Determine if specific cell can be entered. */ - virtual MoveType STDMETHODCALLTYPE Can_Enter_Cell(Cell cell) = 0; + virtual MoveType Can_Enter_Cell(Cell cell) = 0; /* * Should object cast a shadow? */ - virtual boolean STDMETHODCALLTYPE Is_To_Have_Shadow(void) = 0; + virtual bool Is_To_Have_Shadow(void) = 0; /* * Fetch voxel draw matrix. */ - virtual Matrix3D STDMETHODCALLTYPE Draw_Matrix(int *key) = 0; + virtual Matrix3D Draw_Matrix(int *key) = 0; /* * Fetch shadow draw matrix. */ - virtual Matrix3D STDMETHODCALLTYPE Shadow_Matrix(int *key) = 0; + virtual Matrix3D Shadow_Matrix(int *key) = 0; /* * Draw point center location. */ - virtual Point2D STDMETHODCALLTYPE Draw_Point(void) = 0; + virtual Point2D Draw_Point(void) = 0; /* * Shadow draw point center location. */ - virtual Point2D STDMETHODCALLTYPE Shadow_Point(void) = 0; + virtual Point2D Shadow_Point(void) = 0; /* * Visual character for drawing. */ - virtual VisualType STDMETHODCALLTYPE Visual_Character(boolean flag) = 0; + virtual VisualType Visual_Character(bool flag) = 0; /* * Z adjust control value. */ - virtual int STDMETHODCALLTYPE Z_Adjust(void) = 0; + virtual int Z_Adjust(void) = 0; /* * Z gradient control value. */ - virtual ZGradientType STDMETHODCALLTYPE Z_Gradient(void) = 0; + virtual ZGradientType Z_Gradient(void) = 0; /* * Process movement of object. */ - virtual boolean STDMETHODCALLTYPE Process(void) = 0; + virtual bool Process(void) = 0; /* * Instruct to move to location specified. */ - virtual void STDMETHODCALLTYPE Move_To(Coord to) = 0; + virtual void Move_To(Coord to) = 0; /* * Stop moving at first opportunity. */ - virtual void STDMETHODCALLTYPE Stop_Moving(void) = 0; + virtual void Stop_Moving(void) = 0; /* * Try to face direction specified. */ - virtual void STDMETHODCALLTYPE Do_Turn(DirType coord) = 0; + virtual void Do_Turn(DirType coord) = 0; /* * Object is appearing in the world. */ - virtual void STDMETHODCALLTYPE Unlimbo(void) = 0; + virtual void Unlimbo(void) = 0; /* * Special tilting AI function. */ - virtual void STDMETHODCALLTYPE Tilt_Pitch_AI(void) = 0; + virtual void Tilt_Pitch_AI(void) = 0; /* * Locomotor becomes powered. */ - virtual boolean STDMETHODCALLTYPE Power_On(void) = 0; + virtual bool Power_On(void) = 0; /* * Locomotor loses power. */ - virtual boolean STDMETHODCALLTYPE Power_Off(void) = 0; + virtual bool Power_Off(void) = 0; /* * Is locomotor powered? */ - virtual boolean STDMETHODCALLTYPE Is_Powered(void) = 0; + virtual bool Is_Powered(void) = 0; /* * Is locomotor sensitive to ion storms? */ - virtual boolean STDMETHODCALLTYPE Is_Ion_Sensitive(void) = 0; + virtual bool Is_Ion_Sensitive(void) = 0; /* * Push object in direction specified. */ - virtual boolean STDMETHODCALLTYPE Push(DirType dir) = 0; + virtual bool Push(DirType dir) = 0; /* * Shove object (with spin) in direction specified. */ - virtual boolean STDMETHODCALLTYPE Shove(DirType dir) = 0; + virtual bool Shove(DirType dir) = 0; /* * Force drive track -- special case only. */ - virtual void STDMETHODCALLTYPE Force_Track(int track, Coord coord) = 0; + virtual void Force_Track(int track, Coord coord) = 0; /* * What display layer is it located in. */ - virtual LayerType STDMETHODCALLTYPE In_Which_Layer(void) = 0; + virtual LayerType In_Which_Layer(void) = 0; /* * Don't use this function. */ - virtual void STDMETHODCALLTYPE Force_Immediate_Destination(Coord coord) = 0; + virtual void Force_Immediate_Destination(Coord coord) = 0; /* * Force a voxel unit to a given slope. Used in cratering. */ - virtual void STDMETHODCALLTYPE Force_New_Slope(int ramp) = 0; + virtual void Force_New_Slope(int ramp) = 0; /* * Is it actually moving across the ground this very second? */ - virtual boolean STDMETHODCALLTYPE Is_Moving_Now(void) = 0; + virtual bool Is_Moving_Now(void) = 0; /* * Actual current speed of object expressed as leptons per game frame. */ - virtual int STDMETHODCALLTYPE Apparent_Speed(void) = 0; + virtual int Apparent_Speed(void) = 0; /* * Special drawing feedback code (locomotor specific meaning) */ - virtual int STDMETHODCALLTYPE Drawing_Code(void) = 0; + virtual int Drawing_Code(void) = 0; /* * Queries if any locomotor specific state prevents the object from firing. */ - virtual FireErrorType STDMETHODCALLTYPE Can_Fire(void) = 0; + virtual FireErrorType Can_Fire(void) = 0; /* * Queries the general state of the locomotor. */ - virtual int STDMETHODCALLTYPE Get_Status(void) = 0; + virtual int Get_Status(void) = 0; /* * Forces a hunter seeker droid to find a target. */ - virtual void STDMETHODCALLTYPE Acquire_Hunter_Seeker_Target(void) = 0; + virtual void Acquire_Hunter_Seeker_Target(void) = 0; /* * Is this object surfacing? */ - virtual boolean STDMETHODCALLTYPE Is_Surfacing(void) = 0; + virtual bool Is_Surfacing(void) = 0; /* * Lifts all occupation bits associated with the object off the map */ - virtual void STDMETHODCALLTYPE Mark_All_Occupation_Bits(int mark) = 0; + virtual void Mark_All_Occupation_Bits(int mark) = 0; /* * Is this object in the process of moving into this coord. */ - virtual boolean STDMETHODCALLTYPE Is_Moving_Here(Coord to) = 0; + virtual bool Is_Moving_Here(Coord to) = 0; /* * Will this object jump tracks? */ - virtual boolean STDMETHODCALLTYPE Will_Jump_Tracks(void) = 0; + virtual bool Will_Jump_Tracks(void) = 0; /* * Infantry moving query function */ - virtual boolean STDMETHODCALLTYPE Is_Really_Moving_Now(void) = 0; + virtual bool Is_Really_Moving_Now(void) = 0; /* * Falsifies the IsReallyMoving flag in WalkLocomotionClass */ - virtual void STDMETHODCALLTYPE Stop_Movement_Animation(void) = 0; + virtual void Stop_Movement_Animation(void) = 0; /* * Locks the locomotor from being deleted */ - virtual void STDMETHODCALLTYPE Lock(void) = 0; + virtual void Lock(void) = 0; /* * Unlocks the locomotor from being deleted */ - virtual void STDMETHODCALLTYPE Unlock(void) = 0; + virtual void Unlock(void) = 0; /* * Queries internal variables */ - virtual int STDMETHODCALLTYPE Get_Track_Number(void) = 0; + virtual int Get_Track_Number(void) = 0; /* * Queries internal variables */ - virtual int STDMETHODCALLTYPE Get_Track_Index(void) = 0; + virtual int Get_Track_Index(void) = 0; /* * Queries internal variables */ - virtual int STDMETHODCALLTYPE Get_Speed_Accum(void) = 0; + virtual int Get_Speed_Accum(void) = 0; }; -/* - * ILocomtion com smart pointer declaration. - */ -_COM_SMARTPTR_TYPEDEF(ILocomotion, __uuidof(ILocomotion)); diff --git a/code/iloco_i.c b/code/iloco_i.c deleted file mode 100644 index cecc3d209..000000000 --- a/code/iloco_i.c +++ /dev/null @@ -1,52 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2026 OpenTS contributors - * - * See LICENSE.md for applicable additional terms and warranty disclaimers. - ******************************************************************************/ - -/* this file contains the actual definitions of */ -/* the IIDs and CLSIDs */ - -/* link this file in with the server and any clients */ - - -/* File created by MIDL compiler version X.XX.XXXX */ -/* at XXX XXX XX XX:XX:XX XXXX - */ -/* Compiler settings for XXXX.idl: - Oicf (OptLev=i2), W1, Zp8, env=Win32, ms_ext, c_ext - error checks: none -*/ -//@@MIDL_FILE_HEADING( ) -#ifdef __cplusplus -extern "C"{ -#endif - - -#ifndef __IID_DEFINED__ -#define __IID_DEFINED__ - -typedef struct _IID -{ - unsigned long x; - unsigned short s1; - unsigned short s2; - unsigned char c[8]; -} IID; - -#endif // __IID_DEFINED__ - -#ifndef CLSID_DEFINED -#define CLSID_DEFINED -typedef IID CLSID; -#endif // CLSID_DEFINED - -const IID IID_ILocomotion = {0x070F3290,0x9841,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -#ifdef __cplusplus -} -#endif diff --git a/code/ilocos.h b/code/ilocos.h deleted file mode 100644 index ee939a7e0..000000000 --- a/code/ilocos.h +++ /dev/null @@ -1,27 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2026 OpenTS contributors - * - * See LICENSE.md for applicable additional terms and warranty disclaimers. - ******************************************************************************/ - -#pragma once - -#include "iloco.h" - -/// Names and comments from TLBs - -EXTERN_C const IID LIBID_LocomotionLibrary; - -EXTERN_C const CLSID CLSID_DriveLocomotion; -EXTERN_C const CLSID CLSID_HoverLocomotion; -EXTERN_C const CLSID CLSID_TunnelLocomotion; -EXTERN_C const CLSID CLSID_WalkLocomotion; -EXTERN_C const CLSID CLSID_BallisticLocomotion; -EXTERN_C const CLSID CLSID_FlyerLocomotion; -EXTERN_C const CLSID CLSID_TeleportLocomotion; -EXTERN_C const CLSID CLSID_MechLocomotion; -EXTERN_C const CLSID CLSID_JumpjetLocomotion; -EXTERN_C const CLSID CLSID_LevitateLocomotion; diff --git a/code/ilocos_i.c b/code/ilocos_i.c deleted file mode 100644 index a1fb1faf0..000000000 --- a/code/ilocos_i.c +++ /dev/null @@ -1,83 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2026 OpenTS contributors - * - * See LICENSE.md for applicable additional terms and warranty disclaimers. - ******************************************************************************/ - -/* this file contains the actual definitions of */ -/* the IIDs and CLSIDs */ - -/* link this file in with the server and any clients */ - - -/* File created by MIDL compiler version X.XX.XXXX */ -/* at XXX XXX XX XX:XX:XX XXXX - */ -/* Compiler settings for XXXX.idl: - Oicf (OptLev=i2), W1, Zp8, env=Win32, ms_ext, c_ext - error checks: none -*/ -//@@MIDL_FILE_HEADING( ) -#ifdef __cplusplus -extern "C"{ -#endif - - -#ifndef __IID_DEFINED__ -#define __IID_DEFINED__ - -typedef struct _IID -{ - unsigned long x; - unsigned short s1; - unsigned short s2; - unsigned char c[8]; -} IID; - -#endif // __IID_DEFINED__ - -#ifndef CLSID_DEFINED -#define CLSID_DEFINED -typedef IID CLSID; -#endif // CLSID_DEFINED - - -const IID LIBID_LocomotionLibrary = {0x4A582740,0x9839,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_DriveLocomotion = {0x4A582741,0x9839,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_HoverLocomotion = {0x4A582742,0x9839,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_TunnelLocomotion = {0x4A582743,0x9839,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_WalkLocomotion = {0x4A582744,0x9839,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_BallisticLocomotion = {0x4A582745,0x9839,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_FlyerLocomotion = {0x4A582746,0x9839,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_TeleportLocomotion = {0x4A582747,0x9839,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_MechLocomotion = {0x55D141B8,0xDB94,0x11D1,{0xAC,0x98,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_JumpjetLocomotion = {0x92612C46,0xF71F,0x11D1,{0xAC,0x9F,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_LevitateLocomotion = {0x3DC0B295,0x6546,0x11D3,{0x80,0xB0,0x00,0x90,0x27,0x92,0x49,0x4C}}; - - -#ifdef __cplusplus -} -#endif diff --git a/code/infantry.cpp b/code/infantry.cpp index f8f11f6ae..907572bb8 100644 --- a/code/infantry.cpp +++ b/code/infantry.cpp @@ -79,7 +79,6 @@ * InfantryClass::~InfantryClass -- Default destructor for infantry units. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "infantry.h" @@ -108,7 +107,7 @@ #include "goptions.h" #include "house.h" #include "houstype.h" -#include "ilocos.h" +#include "classids.h" #include "incdec.h" #include "infatype.h" #include "inline.h" @@ -250,7 +249,7 @@ InfantryClass::InfantryClass(InfantryTypeClass const * type, HouseClass * house) Init(); if (Class != NULL) { - Locomotion.CreateInstance(Class->Locomotor, NULL, CLSCTX_ALL); + Locomotion = Create_Locomotor(Class->Locomotor); Locomotion->Link_To_Object(this); } @@ -632,11 +631,9 @@ void InfantryClass::Draw_It(Point2D const & xpoint, Rect const & cliprect) const Cell cell = Get_Target_Cell(); if (CurrentTube == -1) { - IPersistPtr persist = Locomotion; - CLSID clsid; - persist->GetClassID(&clsid); + ClassID const clsid = Locomotion_Class_ID(Locomotion.get()); - if (HeightAGL > 0 && clsid == CLSID_BallisticLocomotion) { + if (HeightAGL > 0 && clsid == ClassID_BallisticLocomotion) { ShapeSet const * shapefile = (ShapeSet const *)MFCD::Retrieve("POD.SHP"); Point2D spoint = xpoint + Point2D(Locomotion->Shadow_Point()); Draw_Shape( @@ -1174,10 +1171,8 @@ void InfantryClass::Assign_Destination(AbstractClass * target, bool immediate) } if (target != NULL && Class->IsJumpJet && Locomotion->Is_Moving()) { - IPersistPtr persist(Locomotion); - CLSID clsid; - persist->GetClassID(&clsid); - if (clsid == CLSID_WalkLocomotion) { + ClassID const clsid = Locomotion_Class_ID(Locomotion.get()); + if (clsid == ClassID_WalkLocomotion) { NavQueue.Add_Head(target); target = Get_Target_Cell_Ptr(); if (target != NULL && ((CellClass *)target)->IsUnderBridge) { @@ -1190,26 +1185,26 @@ void InfantryClass::Assign_Destination(AbstractClass * target, bool immediate) bool should_fly = Should_JumpJet_Fly(Destination_Coord().As_Cell(), target->Center_Coord().As_Cell()); if (Is_JumpJet()) { if (!should_fly) { - IPiggybackPtr piggy(Locomotion); + IPiggyback * piggy = Piggyback_Of(Locomotion.get()); if (piggy != NULL) { if (piggy->Is_Piggybacking() && piggy->Is_Ok_To_End()) { - piggy->End_Piggyback(&Locomotion); + Locomotion = piggy->End_Piggyback(); } } - ILocomotionPtr walk(CLSID_WalkLocomotion); + std::unique_ptr walk = Create_Locomotor(ClassID_WalkLocomotion); walk->Link_To_Object(this); - piggy = IPiggybackPtr(walk); + piggy = Piggyback_Of(walk.get()); if (piggy != NULL) { - piggy->Begin_Piggyback(Locomotion); - Locomotion = walk; + piggy->Begin_Piggyback(std::move(Locomotion)); + Locomotion = std::move(walk); } } } else { if (should_fly) { - IPiggybackPtr piggy(Locomotion); + IPiggyback * piggy = Piggyback_Of(Locomotion.get()); if (piggy != NULL) { if (piggy->Is_Piggybacking() && piggy->Is_Ok_To_End()) { - piggy->End_Piggyback(&Locomotion); + Locomotion = piggy->End_Piggyback(); } } } @@ -3943,8 +3938,8 @@ void InfantryClass::Clear_Occupy_Bit(Coord const & coord) /// since the one it is about to be given is the one it was saved with. Post_Load enters it /// again once that identity has arrived. /// -/// Returns with S_OK if the object was read successfully. -HRESULT STDMETHODCALLTYPE InfantryClass::Load(IStream * stream) +/// bool; Was the record read whole? +bool InfantryClass::Load(SaveStreamClass & stream) { TargetTracker.Remove_Index(Fetch_ID()); return(BASECLASS::Load(stream)); @@ -4154,15 +4149,15 @@ bool InfantryClass::JumpJet_To_Walk(void) if (path_length >= 4) return(false); if (Is_JumpJet()) { - IPiggybackPtr piggy(Locomotion); + IPiggyback * piggy = Piggyback_Of(Locomotion.get()); if (piggy != NULL && !piggy->Is_Piggybacking()) { - ILocomotionPtr walk(CLSID_WalkLocomotion); + std::unique_ptr walk = Create_Locomotor(ClassID_WalkLocomotion); walk->Link_To_Object(this); - piggy = IPiggybackPtr(walk); + piggy = Piggyback_Of(walk.get()); if (piggy != NULL) { Path[0] = FACING_NONE; - piggy->Begin_Piggyback(Locomotion); - Locomotion = walk; + piggy->Begin_Piggyback(std::move(Locomotion)); + Locomotion = std::move(walk); Locomotion->Move_To(NavCom->Center_Coord()); return(true); } @@ -4197,10 +4192,8 @@ bool InfantryClass::Is_JumpJet(void) const return(false); } - IPersistPtr persist(Locomotion); - CLSID clsid; - persist->GetClassID(&clsid); - return((clsid == CLSID_JumpjetLocomotion) ? true : false); + ClassID const clsid = Locomotion_Class_ID(Locomotion.get()); + return((clsid == ClassID_JumpjetLocomotion) ? true : false); } @@ -4303,18 +4296,9 @@ int InfantryClass::Do_MISSION_GUARD(void) } -/// -/// Fetches the class identifier used to persist this object. -/// The save system records this identifier alongside the object data so that the -/// correct kind of object can be created again when the stream is read back. -/// -/// Pointer to the buffer to fill in with the class identifier. -/// Returns with S_OK, or E_POINTER if no buffer was supplied. -HRESULT STDMETHODCALLTYPE InfantryClass::GetClassID(CLSID * retval) +ClassID InfantryClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_InfantryClass; - return(S_OK); + return(ClassID_InfantryClass); } diff --git a/code/infantry.h b/code/infantry.h index fc6183008..1540aa8e8 100644 --- a/code/infantry.h +++ b/code/infantry.h @@ -126,8 +126,8 @@ class InfantryClass : public FootClass InfantryClass(InfantryTypeClass const * type = NULL, HouseClass * house = NULL); virtual ~InfantryClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; - virtual HRESULT STDMETHODCALLTYPE Load(IStream * stream) override; + virtual ClassID Class_ID(void) const override; + virtual bool Load(SaveStreamClass & stream) override; virtual void Serialize(SaveStreamClass & stream) override; virtual void Post_Load(void) override; diff --git a/code/infatype.cpp b/code/infatype.cpp index e4c42f96b..9d707ff34 100644 --- a/code/infatype.cpp +++ b/code/infatype.cpp @@ -45,7 +45,6 @@ * InfantryTypeClass::operator new -- Allocate an infanty type class object. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "infatype.h" @@ -513,17 +512,9 @@ void InfantryTypeClass::Serialize(SaveStreamClass & stream) } -/// -/// Fetches the class identifier of this object. -/// The save game system stores this identifier so that the object can be recreated as the -/// correct class when the game is loaded. -/// -/// Returns with S_OK, or E_POINTER if no return pointer was supplied. -HRESULT STDMETHODCALLTYPE InfantryTypeClass::GetClassID(CLSID * retval) +ClassID InfantryTypeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_InfantryTypeClass; - return(S_OK); + return(ClassID_InfantryTypeClass); } diff --git a/code/infatype.h b/code/infatype.h index 340f22dcc..e1b717d6c 100644 --- a/code/infatype.h +++ b/code/infatype.h @@ -172,7 +172,7 @@ class InfantryTypeClass : public TechnoTypeClass InfantryTypeClass(char const * ininame = NULL); virtual ~InfantryTypeClass() override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; virtual void Post_Load(void) override; diff --git a/code/ini.cpp b/code/ini.cpp index 4db0ee6f2..7b2874133 100644 --- a/code/ini.cpp +++ b/code/ini.cpp @@ -957,6 +957,65 @@ int INIClass::Get_Int(char const * section, char const * entry, int defvalue) co } +// A class identifier as the registry writes it, braces optional: eight, four, four, four +// and twelve hexadecimal digits separated by hyphens. +static bool Parse_ClassID(char const * text, ClassID & clsid) +{ + char digits[40]; + unsigned int length = 0; + + for (char const * ptr = text; *ptr != '\0'; ptr++) { + if (*ptr == '{' || *ptr == '}') { + continue; + } + if (length >= sizeof(digits) - 1) { + return(false); + } + digits[length++] = *ptr; + } + digits[length] = '\0'; + + unsigned int data1 = 0; + unsigned int data2 = 0; + unsigned int data3 = 0; + unsigned int data4[8] = { 0 }; + int const scanned = sscanf(digits, "%8x-%4x-%4x-%2x%2x-%2x%2x%2x%2x%2x%2x", + &data1, &data2, &data3, + &data4[0], &data4[1], &data4[2], &data4[3], + &data4[4], &data4[5], &data4[6], &data4[7]); + if (scanned != 11 || length != 36) { + return(false); + } + + clsid.Data1 = data1; + clsid.Data2 = (unsigned short)data2; + clsid.Data3 = (unsigned short)data3; + for (int index = 0; index < 8; index++) { + clsid.Data4[index] = (unsigned char)data4[index]; + } + return(true); +} + + +// The buffer holds the 38 characters of the braced form and its terminator. +static void Format_ClassID(ClassID const & clsid, char * text) +{ + sprintf(text, "{%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}", + (unsigned long)clsid.Data1, (unsigned int)clsid.Data2, (unsigned int)clsid.Data3, + clsid.Data4[0], clsid.Data4[1], clsid.Data4[2], clsid.Data4[3], + clsid.Data4[4], clsid.Data4[5], clsid.Data4[6], clsid.Data4[7]); +} + + +/// +/// Stores a class identifier into the INI database. +/// This routine will convert the identifier into its printable brace and hyphen form before +/// storing it, so that the resulting entry stays readable and can be edited by hand. +/// +/// The identifier for the section that the entry will be placed in. +/// The entry identifier to tag to the class identifier specified. +/// The class identifier to store. +/// bool; Was the class identifier placed into the INI database? /// /// Fetches a class identifier from the specified section. /// This routine will fetch the printable form of a class identifier from the entry and @@ -968,15 +1027,13 @@ int INIClass::Get_Int(char const * section, char const * entry, int defvalue) co /// The default identifier to use if the entry could not be found. /// Returns with the class identifier specified in the INI database or else returns /// the default value. -CLSID const INIClass::Get_CLSID(char const * section, char const * entry, CLSID defvalue) const +ClassID const INIClass::Get_ClassID(char const * section, char const * entry, ClassID defvalue) const { char buffer[128]; if (Get_String(section, entry, "", buffer, sizeof(buffer))) { - wchar_t olestr[128]; - MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, buffer, -1, olestr, ARRAY_SIZE(olestr)); - CLSID clsid; - if (SUCCEEDED(CLSIDFromString(olestr, &clsid))) { + ClassID clsid; + if (Parse_ClassID(buffer, clsid)) { return(clsid); } } @@ -984,26 +1041,10 @@ CLSID const INIClass::Get_CLSID(char const * section, char const * entry, CLSID } -/// -/// Stores a class identifier into the INI database. -/// This routine will convert the identifier into its printable brace and hyphen form before -/// storing it, so that the resulting entry stays readable and can be edited by hand. -/// -/// The identifier for the section that the entry will be placed in. -/// The entry identifier to tag to the class identifier specified. -/// The class identifier to store. -/// bool; Was the class identifier placed into the INI database? -bool INIClass::Put_CLSID(char const * section, char const * entry, CLSID const & value) +bool INIClass::Put_ClassID(char const * section, char const * entry, ClassID const & value) { - char buffer[128]; - LPOLESTR olestr = NULL; - - StringFromCLSID(value, &olestr); - if (WideCharToMultiByte(CP_ACP, 0, olestr, -1, buffer, sizeof(buffer), NULL, NULL) == 0) { - /// BUG, return not used - GetLastError(); - } - SysFreeString(olestr); + char buffer[40]; + Format_ClassID(value, buffer); return(Put_String(section, entry, buffer)); } diff --git a/code/ini.h b/code/ini.h index a4f33badd..ac28a63f9 100644 --- a/code/ini.h +++ b/code/ini.h @@ -34,7 +34,7 @@ #include "crc.h" #include "index.h" -#include +#include "classid.h" #include #include #include @@ -113,7 +113,7 @@ class INIClass { TPoint3D const Get_Point(char const * section, char const * entry, TPoint3D const & defvalue) const; TPoint2D const Get_Point(char const * section, char const * entry, TPoint2D const & defvalue) const; TPoint3D const Get_Point(char const * section, char const * entry, TPoint3D const & defvalue) const; - CLSID const Get_CLSID(char const * section, char const * entry, CLSID defvalue) const; + ClassID const Get_ClassID(char const * section, char const * entry, ClassID defvalue) const; /* ** Put a data type to the section and entry specified. @@ -130,7 +130,7 @@ class INIClass { bool Put_Point(char const * section, char const * entry, TPoint3D const & value); bool Put_Point(char const * section, char const * entry, TPoint3D const & value); bool Put_Point(char const * section, char const * entry, TPoint2D const & value); - bool Put_CLSID(char const * section, char const * entry, CLSID const & value); + bool Put_ClassID(char const * section, char const * entry, ClassID const & value); // Callers size the buffers they hand to Get_String from this. It does not bound a line // of the file; the reader keeps a line of any length. diff --git a/code/init.cpp b/code/init.cpp index fe9a7cb8a..0f550bd03 100644 --- a/code/init.cpp +++ b/code/init.cpp @@ -115,7 +115,10 @@ #include "gamedirs.h" #include "gamedlg.h" #include "getcpu.h" +#include "ui/uishell.h" +#include "ui/uiversion.h" #include "globals.h" +#include "hostclock.h" #include "houstype.h" #include "incdec.h" #include "infatype.h" @@ -145,7 +148,6 @@ #include "overlay.h" #include "overtype.h" #include "ovrlight.h" -#include "ownrdraw.h" #include "partsys.h" #include "pcx.h" #include "queue.h" @@ -185,6 +187,7 @@ #include "vqoption.h" #include "wave.h" #include "waypoint.h" +#include "winfix.h" #include "winstub.h" #include "wsproto.h" #include "wspudp.h" @@ -192,6 +195,8 @@ #include "bench.hh" #include "scrnsel.hh" +#include "ui/uicampaign.h" +#include "ui/uimainmenu.h" #include #include @@ -202,10 +207,6 @@ extern VoxelDataStruct DropPodVoxel; -struct ChooseCampaignStruct { - CampaignType ChosenCampaign; - bool ChoiceMade; -}; /********************************************************************** ** Optional parameter control for special options. @@ -275,7 +276,7 @@ static CheatEntryStruct CheatEntries[] = { }; static void Cheat_Disable(void); -static bool Cheat_Key_Process(char chr); +bool Cheat_Key_Process(char chr); static void Cheat_Version_Suffix(char * string); @@ -415,6 +416,11 @@ int Init_Game(int , char * []) Options.Load_Settings(); SaveManager.Autosave.Set_Interval(Options.AutoSaveInterval); + // The session speed starts at the player's own saved setting rather than at zero, which + // is the "fastest" end of the scale. The skirmish screen seeds its slider from it and so + // opened every match at that end, whatever the player had settled on. + Session.Options.GameSpeed = Options.GameSpeed; + /* ** Initialize the animation system. */ @@ -715,138 +721,6 @@ void Prepare_Side_Roster(void) } -/// -/// Can this campaign be played with the addons that are enabled? -/// A base game campaign is offered only when no addon is running, and an addon's own -/// campaign only when that particular addon is running. -/// -/// The campaign to be tested. -/// bool; Is the campaign available for the player to select? -static bool Campaign_Available(CampaignClass * campaign) -{ - if (Addon_Enabled(ADDON_ANY) == true) { - if (campaign->RequiredAddon == ADDON_BASE_GAME) { - return(false); - } - if (Addon_Enabled((AddonType)campaign->RequiredAddon)) { - return(true); - } - return(false); - } - - if (campaign->RequiredAddon == ADDON_BASE_GAME) { - return(true); - } - - return(false); -} - - -/// -/// Handles the messages for the campaign choice dialog. -/// This routine lists the campaigns that the player is entitled to play, drives the -/// difficulty slider, and leaves the choice where Choose_Campaign will collect it. -/// -static INT_PTR CALLBACK Campaign_Choice_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - HWND item; - struct ChooseCampaignStruct * state; - - INT_PTR rc; - rc = OwnerDraw::Default_Dialog_Proc(window, message, wparam, lparam); - - if (rc) { - return(rc); - } - - switch (message) { - - case WM_INITDIALOG: - item = GetDlgItem(window, IDC_LIST); - - if (item != NULL) { - DebugString("Initializing Choose_Campaign() Dialog.\n"); - for (int index = 0; index < Campaigns.Count(); index++) { - CampaignClass * campaign = Campaigns[index]; - - if (!Campaign_Available(campaign)) { - DebugString("\tSkipping Campaign [%d] - %s\n", index, campaign->Description); - continue; - } - - DebugString("\tAdding Campaign [%d] - %s\n", index, campaign->Description); - int pos = ListBox_AddString(item, campaign->Description); - ListBox_SetItemData(item, pos, index); - } - - ListBox_SetCurSel(item, 0); - } - - item = GetDlgItem(window, IDC_DIFFICULTY_SLIDER); - - if (item != NULL) { - SendMessage(item, OD_TRACKNUMBERS, 0, 0); - Slider_SetRange(item, 0,2); - Slider_SetPos(item, Options.Difficulty); - } - break; - - case WM_COMMAND: - switch (LOWORD(wparam)) { - case IDOK: - if (HIWORD(wparam) == BN_CLICKED) { - state = (ChooseCampaignStruct *)GetWindowLongPtr(window, DWLP_USER); - - if (state != NULL) { - item = GetDlgItem(window, IDC_LIST); - - if (item != NULL) { - int pos = ListBox_GetCurSel(item); - state->ChosenCampaign = (CampaignType)ListBox_GetItemData(item, pos); - state->ChoiceMade = true; - } - } - - item = GetDlgItem(window, IDC_DIFFICULTY_SLIDER); - - if (item != NULL) { - Options.Difficulty = Slider_GetPos(item); - } - } - break; - - case IDCANCEL: - if (HIWORD(wparam) == BN_CLICKED) { - state = (ChooseCampaignStruct *)GetWindowLongPtr(window, DWLP_USER); - - if (state != NULL) { - state->ChosenCampaign = CAMPAIGN_NONE; - state->ChoiceMade = true; - } - } - - break; - } - break; - - case WM_HSCROLL: { - int diff = HIWORD(wparam); - int stringID = 0; - - if ((HWND)lparam == GetDlgItem(window, IDC_DIFFICULTY_SLIDER)) { - stringID = GameDifficultyNames[diff]; - item = GetDlgItem(window, IDC_DIFFICULTY_LABEL); - Static_SetText(item, Fetch_String(stringID)); - } - break; - } - - default: - break; - } - - return(FALSE); -} /// @@ -857,12 +731,6 @@ static INT_PTR CALLBACK Campaign_Choice_Dialog_Proc(HWND window, UINT message, W /// Returns with the campaign chosen, or CAMPAIGN_NONE if the player backed out. static CampaignType Choose_Campaign(void) { - HWND dialog; - struct ChooseCampaignStruct state; - - state.ChoiceMade = false; - state.ChosenCampaign = CAMPAIGN_NONE; - if (Campaigns.Count() == 0) { Init_Campaigns(); @@ -871,25 +739,12 @@ static CampaignType Choose_Campaign(void) } } - dialog = OwnerDraw::Begin_Dialog(IDD_CAMPAIGN, Campaign_Choice_Dialog_Proc); - - if (dialog != NULL) { - SetWindowLongPtr(dialog, DWLP_USER, (LONG_PTR) &state); - - OwnerDraw::Move_Dialog(dialog, -1, (HiddenSurface->Get_Height() - 400) / 2 + 147); - OwnerDraw::Display_Dialog(dialog); + UICampaignPresenterClass screen; + screen.Refresh(); - while (state.ChoiceMade == false) { - if (OwnerDraw::Dialog_Message_Handler() == true) { - break; - } - Title_Screen_Restore(); - } + UI_Campaign_Screen(screen); - OwnerDraw::End_Dialog(dialog); - } - - return(state.ChosenCampaign); + return((CampaignType)screen.Chosen()); } @@ -1915,7 +1770,8 @@ void Init_Random(void) */ if (Session.Type == GAME_NORMAL || Session.Type == GAME_SKIRMISH) { - #ifdef WIN32 + // The alternative is the DOS build's timer, which no target this tree builds for has. + #ifndef __DOS__ /* ** Gather some "random" bits from the system timer. Actually, only the ** low order millisecond bits are secure. The other bits could be @@ -1958,7 +1814,7 @@ void Init_Random(void) Seed = CustomSeed; } else { CryptRandom.Get(&Seed, sizeof(Seed)); - Seed = GetTickCount(); + Seed = Host_Milliseconds(); //srand(time(NULL)); //Seed = rand(); } @@ -2993,104 +2849,36 @@ bool Cheat_Key_Process(char chr) /// -/// Handles the messages for the version information dialog. -/// This routine fills the list box with the game's title, its version numbers, the build -/// stamp, and a description of the processor it finds itself running upon. It is the -/// first thing to ask for when a player reports a problem. +/// Displays the version information screen. +/// This routine does not return until the player dismisses the screen, and keeps the +/// title screen alive behind it while it waits. /// -INT_PTR CALLBACK Version_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) +void Version_Dialog(void) { - HWND handle; - int *res; - char buffer[256]; - - INT_PTR rc = OwnerDraw::Default_Dialog_Proc(window, message, wparam, lparam); - - if (rc) { - return(rc); - } - - res = (int *)GetWindowLongPtr(window, DWLP_USER); - - switch (message) { - case WM_INITDIALOG: - handle = GetDlgItem(window, IDC_VERSION_INFO); - - if (Addon_Installed(ADDON_FIRESTORM) == true) { - strcpy(buffer, Fetch_String(TXT_SHORT_TITLE)); - strcat(buffer, ": "); - strcat(buffer, Get_Addon_Title(ADDON_FIRESTORM)); - ListBox_AddString(handle, buffer); - } else { - ListBox_AddString(handle, Fetch_String(TXT_SHORT_TITLE)); - } - - sprintf(buffer, "Version %s", Version_Name()); - ListBox_AddString(handle, buffer); - - sprintf(buffer, "Internal Version %s", VerNum.Version_Name()); - ListBox_AddString(handle, buffer); - -#ifdef _DEBUG - sprintf(buffer, "Debug Build: %s - %s", OPENTS_BUILD_DESCRIPTION, OPENTS_COMMIT_DATE); -#else - sprintf(buffer, "Release Build: %s - %s", OPENTS_BUILD_DESCRIPTION, OPENTS_COMMIT_DATE); -#endif - ListBox_AddString(handle, buffer); - - // The braces keep the 'case' label from jumping over these initializations. - { - int cpu_type = 5; - char vendor[32]; - vendor[0] = '\0'; - Get_CPU_Type(cpu_type, vendor, sizeof(vendor) - 1); - - sprintf(buffer, "CPU vendor: %s", vendor); - ListBox_AddString(handle, buffer); - } - - Get_Language_Version(buffer); - ListBox_AddString(handle, buffer); - break; - - case WM_COMMAND: - switch (LOWORD(wparam)) { - case IDCANCEL: - case IDOK: - *res = LOWORD(wparam); - break; - } - break; - } - - return(FALSE); + UI_Version_Screen(); } /// -/// Displays the version information dialog. -/// This routine does not return until the player dismisses the dialog, and keeps the -/// title screen alive behind it while it waits. +/// Puts the title screen behind the menu. /// -void Version_Dialog(void) +static void Draw_Title_Screen(void) { - HWND dialog; - int res = 0; - - dialog = OwnerDraw::Begin_Dialog(IDD_VERSION, Version_Dialog_Proc); + char * menu = Get_New_Menu()->Background; + Load_Title_Screen(menu, HiddenSurface, &CCPalette); + Draw_Version_Text(HiddenSurface); + Update_Visible_Surface(); +} - if (dialog != NULL) { - SetWindowLongPtr(dialog, DWLP_USER, (LONG_PTR)&res); - OwnerDraw::Display_Dialog(dialog); - while (res == 0) { - if (OwnerDraw::Dialog_Message_Handler() == true) { - break; - } - Title_Screen_Restore(); - } - OwnerDraw::End_Dialog(dialog); - } +/// +/// Seeds the cryptographic random number generator from the clock. +/// +static void Seed_Crypto_Random(void) +{ + SYSTEMTIME t; + GetSystemTime(&t); + CryptRandom.Seed_Byte(t.wMilliseconds); } @@ -3111,141 +2899,31 @@ void Version_Dialog(void) *=========================================================================*/ int Main_Menu(unsigned int timeout) { - HWND dialog; int retval = SEL_NONE; timeout = 0; - dialog = OwnerDraw::Begin_Dialog(IDD_MAIN_MENU, Main_Menu_Dialog_Proc); - assert(dialog != NULL); + UIMainMenuPresenterClass screen; + screen.Refresh(); - if (dialog != NULL) { - SetWindowLongPtr(dialog, DWLP_USER, (LONG_PTR)&retval); - char *menu = Get_New_Menu()->Background; - Load_Title_Screen(menu, HiddenSurface, &CCPalette); - Draw_Version_Text(HiddenSurface); - Update_Visible_Surface(); - OwnerDraw::Move_Dialog(dialog, -1, (HiddenSurface->Get_Height() - 400) / 2 + 147); - OwnerDraw::Display_Dialog(dialog); - SetFocus(MainWindow); - - do { - if (OwnerDraw::Dialog_Message_Handler() == true) { - retval = SEL_EXIT; - } - - Title_Screen_Restore(); - - if (Keyboard->Check()) { - KeyNumType input = Keyboard->Get(); - - switch ((unsigned int)input) { - case (KN_V | KN_CTRL_BIT): - ShowWindow(dialog, SW_HIDE); - UpdateWindow(MainWindow); - Version_Dialog(); - ShowWindow(dialog, SW_SHOW); - UpdateWindow(dialog); - SetFocus(MainWindow); - break; - - case VK_C | KN_CTRL_BIT | KN_ALT_BIT: - retval = SEL_VIEW_CREDITS; - break; - - default: - if ((input & KN_RLSE_BIT) == 0) { - if (Cheat_Key_Process((char)input) == true) { - Sound_Effect(Rule->OptionsChanged); - Title_Screen_Restore(true); - } - } - break; - } - } - } - while (retval == SEL_NONE); + Draw_Title_Screen(); - OwnerDraw::End_Dialog(dialog); + UIResult const result = UI_Main_Menu_Screen(screen); - /* - * Seed cryptographic random number generator. - */ - SYSTEMTIME t; - GetSystemTime(&t); - CryptRandom.Seed_Byte(t.wMilliseconds); - } else { - retval = SEL_EXIT; + // A session that ended underneath the screen leaves the menu, which is what the driver's + // own exit intent did for the same condition. + if (result.Outcome == UIResult::OUTCOME_SESSION_ENDED) { + screen.Choice = UIMainMenuPresenterClass::CHOICE_EXIT; } + retval = screen.Selection(); + Seed_Crypto_Random(); + SetFocus(MainWindow); return(retval); } -/// -/// Handles the messages for the main menu dialog. -/// This routine records the button the player pressed into the result that Main_Menu is -/// waiting upon, and greys out the load button when there is nothing to load. -/// -INT_PTR CALLBACK Main_Menu_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - int * res; - - INT_PTR rc = OwnerDraw::Default_Dialog_Proc(window, message, wparam, lparam); - if (rc) { - return(rc); - } - - res = (int *) GetWindowLongPtr(window, DWLP_USER); - - switch (message) { - case WM_INITDIALOG: { - HWND control = GetDlgItem(window, IDC_LOAD_MISSION); - if (control) { - if (LoadOptionsClass().Files_Present() == true) { - EnableWindow(control, TRUE); - return(FALSE); - } - EnableWindow(control, FALSE); - } - } - break; - - case WM_COMMAND: { - switch (LOWORD(wparam)) { - case IDC_OPTIONS: - *res = SEL_OPTIONS; - break; - - case IDC_EXIT_GAME: - *res = SEL_EXIT; - break; - - case IDC_INTRO: - *res = SEL_INTRO; - break; - - case IDC_NEWCAMPAIGN: - *res = SEL_CAMPAIGN_GAME; - break; - - case IDC_MULTIPLAYER_GAME: - *res = SEL_MULTIPLAYER_GAME; - break; - - case IDC_LOAD_MISSION: - *res = SEL_LOAD_GAME; - break; - } - } - break; - } - - return(false); -} - - /// /// Redraws the title screen if the display surfaces have been lost. /// The menu dialogs call this routine from their message loops, so that the background @@ -6054,7 +5732,7 @@ void Delete_All_Objects(void) } Process_Deferred_Deletion(); while (Bullets.Count()) { - Bullets[0]->Release(); + delete Bullets[0]; } Process_Deferred_Deletion(); while (Objects.Count()) { diff --git a/code/init.h b/code/init.h index ca3fd1ce2..c8d4ccb39 100644 --- a/code/init.h +++ b/code/init.h @@ -41,6 +41,10 @@ void Reset_Selection_Filters(void); void Title_Screen_Restore(bool force=false); +// Spells a cheat word out one character at a time and answers when one is completed. The +// main menu screen owns this; it is not a key binding and never reaches the command list. +bool Cheat_Key_Process(char chr); + void Init_Campaigns(void); void Prepare_Theater_Roster(void); diff --git a/code/ion.cpp b/code/ion.cpp index c4ce335a1..4348c797e 100644 --- a/code/ion.cpp +++ b/code/ion.cpp @@ -78,11 +78,10 @@ void IonStormClass::Init(void) /// Saves the ion storm state to the save game stream. /// /// Returns with the result reported by the stream write. -HRESULT IonStormClass::Save(IStream * stream) +bool IonStormClass::Save(SaveStreamClass & stream) { - SaveStreamClass savestream(stream, SaveStreamClass::MODE_SAVE); - Serialize(savestream); - return(savestream.Result()); + Serialize(stream); + return(!stream.Was_Error()); } @@ -92,12 +91,11 @@ HRESULT IonStormClass::Save(IStream * stream) /// Returns with the result reported by the stream read. /// Only the bookkeeping is restored here. Post_Load_Game must still call /// Apply_Secondary_Effect to put the world back into its storm bound state. -HRESULT IonStormClass::Load(IStream * stream) +bool IonStormClass::Load(SaveStreamClass & stream) { - SaveStreamClass savestream(stream, SaveStreamClass::MODE_LOAD); - savestream.Set_Context("IonStormClass"); - Serialize(savestream); - return(savestream.Result()); + stream.Set_Context("IonStormClass"); + Serialize(stream); + return(!stream.Was_Error()); } diff --git a/code/ion.h b/code/ion.h index b3dab97f2..149f25c5e 100644 --- a/code/ion.h +++ b/code/ion.h @@ -13,17 +13,18 @@ #include "theme.hh" -#include class SaveStreamClass; +#include "win.h" + class ShapeSet; class IonStormClass { public: static void Init(void); - static HRESULT Save(IStream * stream); - static HRESULT Load(IStream * stream); + static bool Save(SaveStreamClass & stream); + static bool Load(SaveStreamClass & stream); static void Serialize(SaveStreamClass & stream); diff --git a/code/ipiggy.h b/code/ipiggy.h index 74c1d8216..3afbd6f18 100644 --- a/code/ipiggy.h +++ b/code/ipiggy.h @@ -11,43 +11,33 @@ #include "iloco.h" -#include -/// Names and comments from TLBs - -EXTERN_C const IID IID_IPiggyback; - -MIDL_INTERFACE("92FEA800-A184-11D1-B70A-00A024DDAFD1") -IPiggyback : public IUnknown +struct IPiggyback { -public: /* * Piggybacks a locomotor onto this one. */ - virtual HRESULT STDMETHODCALLTYPE Begin_Piggyback(ILocomotion * pointer) = 0; + virtual bool Begin_Piggyback(std::unique_ptr carried) = 0; /* - * End piggyback process and restore locomotor interface pointer. + * Hands the carried locomotor back, or nothing when none is carried. */ - virtual HRESULT STDMETHODCALLTYPE End_Piggyback(ILocomotion ** pointer) = 0; + virtual std::unique_ptr End_Piggyback(void) = 0; /* * Is it ok to end the piggyback process? */ - virtual boolean STDMETHODCALLTYPE Is_Ok_To_End(void) = 0; - - /* - * Fetches piggybacked locomotor class ID. - */ - virtual HRESULT STDMETHODCALLTYPE Piggyback_CLSID(GUID * classid) = 0; + virtual bool Is_Ok_To_End(void) = 0; /* * Is it currently piggy backing another locomotor? */ - virtual boolean STDMETHODCALLTYPE Is_Piggybacking(void) = 0; + virtual bool Is_Piggybacking(void) = 0; }; -/* - * IPiggyback com smart pointer declaration. - */ -_COM_SMARTPTR_TYPEDEF(IPiggyback, __uuidof(IPiggyback)); + +// The piggyback side of a locomotor, or NULL when it cannot carry one. +inline IPiggyback * Piggyback_Of(ILocomotion * locomotion) +{ + return(dynamic_cast(locomotion)); +} diff --git a/code/ipiggy_i.c b/code/ipiggy_i.c deleted file mode 100644 index 46006be43..000000000 --- a/code/ipiggy_i.c +++ /dev/null @@ -1,52 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2026 OpenTS contributors - * - * See LICENSE.md for applicable additional terms and warranty disclaimers. - ******************************************************************************/ - -/* this file contains the actual definitions of */ -/* the IIDs and CLSIDs */ - -/* link this file in with the server and any clients */ - - -/* File created by MIDL compiler version X.XX.XXXX */ -/* at XXX XXX XX XX:XX:XX XXXX - */ -/* Compiler settings for XXXX.idl: - Oicf (OptLev=i2), W1, Zp8, env=Win32, ms_ext, c_ext - error checks: none -*/ -//@@MIDL_FILE_HEADING( ) -#ifdef __cplusplus -extern "C"{ -#endif - - -#ifndef __IID_DEFINED__ -#define __IID_DEFINED__ - -typedef struct _IID -{ - unsigned long x; - unsigned short s1; - unsigned short s2; - unsigned char c[8]; -} IID; - -#endif // __IID_DEFINED__ - -#ifndef CLSID_DEFINED -#define CLSID_DEFINED -typedef IID CLSID; -#endif // CLSID_DEFINED - -const IID IID_IPiggyback = {0x92FEA800,0xA184,0x11D1,{0xB7,0x0A,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -#ifdef __cplusplus -} -#endif diff --git a/code/ipxgconn.h b/code/ipxgconn.h index 7d53e4c26..eeb864c1b 100644 --- a/code/ipxgconn.h +++ b/code/ipxgconn.h @@ -74,6 +74,9 @@ #include "ipxconn.h" +#include +#include + #pragma pack(push,1) /* @@ -93,6 +96,9 @@ struct GlobalHeaderType { }; #pragma pack(pop) +static_assert(sizeof(GlobalHeaderType) == 9, "Global packet header layout changed"); +static_assert(offsetof(GlobalHeaderType, ProductID) == 7, "Global packet header layout changed"); + /* ***************************** Class Declaration ***************************** */ diff --git a/code/isotile.cpp b/code/isotile.cpp index 709b3c71b..8f67c3d9d 100644 --- a/code/isotile.cpp +++ b/code/isotile.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "isotile.h" @@ -227,18 +226,9 @@ RTTIType IsometricTileClass::Fetch_RTTI(void) const } -/// -/// Fetches the class identifier of this object. -/// This routine is part of the persistence contract and is called by the save system -/// when it must record what kind of object it is about to write out. -/// -/// Pointer to the buffer that will receive the class identifier. -/// Returns with S_OK, or E_POINTER if no buffer was supplied. -HRESULT STDMETHODCALLTYPE IsometricTileClass::GetClassID(CLSID * retval) +ClassID IsometricTileClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_IsometricTileClass; - return(S_OK); + return(ClassID_IsometricTileClass); } diff --git a/code/isotile.h b/code/isotile.h index f99cc2683..6833b02d1 100644 --- a/code/isotile.h +++ b/code/isotile.h @@ -15,7 +15,6 @@ #include "isotype.hh" -#include class IsometricTileTypeClass; @@ -26,7 +25,7 @@ class IsometricTileClass : public ObjectClass IsometricTileClass(IsometricTileType type, Cell const &cell); virtual ~IsometricTileClass() override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/isotype.cpp b/code/isotype.cpp index ed7132a33..69e05dc5f 100644 --- a/code/isotype.cpp +++ b/code/isotype.cpp @@ -11,7 +11,6 @@ * disclaimers apply; see LICENSE.md. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "isotype.h" @@ -1597,6 +1596,10 @@ struct IsoBlitState { unsigned short HalfbrightMask; }; +// Held only in memory, so the pointers may be any width, but the field count is fixed. The +// members after the pointers measure 46 bytes and pad out to 48 at either width. +static_assert(sizeof(IsoBlitState) == 15 * sizeof(void *) + 48, "Isometric blit state layout changed"); + IsoBlitState IsoDrawData; unsigned short _iso_row_offsets[ISO_DRAW_WIDTH*ISO_DRAW_HEIGHT]; @@ -2792,16 +2795,9 @@ void IsometricTileTypeClass::Serialize(SaveStreamClass & stream) } -/// -/// Fetches the class identifier that this tile type persists under. -/// -/// Receives the class identifier. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE IsometricTileTypeClass::GetClassID(CLSID * retval) +ClassID IsometricTileTypeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_IsometricTileTypeClass; - return(S_OK); + return(ClassID_IsometricTileTypeClass); } diff --git a/code/isotype.h b/code/isotype.h index c7de7f9a7..52c72f69d 100644 --- a/code/isotype.h +++ b/code/isotype.h @@ -22,6 +22,8 @@ #include "isotype.hh" #include "land.hh" +#include +#include #include class LightConvertClass; @@ -79,6 +81,10 @@ struct IsoTileRecord */ unsigned int IsRandomized:1; + // The flag bits fill a whole 32-bit word on disk. Without this padding a compiler that + // packs a following byte into the same word reads the rest of the record four bytes early. + unsigned int :29; + /* * This is the number of height levels this sub-tile lifts the cell it covers, so that a * tile laid across rising ground raises each of its cells by the right amount. @@ -106,6 +112,11 @@ struct IsoTileRecord }; #pragma pack() +static_assert(sizeof(IsoTileRecord) == 52, "Isometric tile record layout changed"); +static_assert(offsetof(IsoTileRecord, ExtraZOffset) == 16, "Isometric tile record layout changed"); +static_assert(offsetof(IsoTileRecord, Height) == 40, "Isometric tile record layout changed"); +static_assert(offsetof(IsoTileRecord, LowColor) == 43, "Isometric tile record layout changed"); + #pragma pack(4) class IsoTileSet { @@ -175,6 +186,7 @@ class IsoTileSet private: IsoTileRecord const * Record_At(int index) const { + static_assert(offsetof(IsoTileSet, TileOffsets) == 16, "Isometric tile set header layout changed"); if (TileOffsets[index] == 0) { return(NULL); } @@ -191,6 +203,8 @@ class IsoTileSet }; #pragma pack() +static_assert(sizeof(IsoTileSet) == 20, "Isometric tile set header layout changed"); + /**************************************************************************** ** The tile type objects are controlled by this class. It specifies the form @@ -206,7 +220,7 @@ class IsometricTileTypeClass : public ObjectTypeClass IsometricTileTypeClass(IsometricTileType type = ISOTILE_CLEAR, int unknown1 = 0, unsigned char unknown2 = 0, char const *ininame = NULL, bool skip_registration = false); virtual ~IsometricTileTypeClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; virtual void Post_Load(void) override; diff --git a/code/isun.h b/code/isun.h deleted file mode 100644 index c46215471..000000000 --- a/code/isun.h +++ /dev/null @@ -1,80 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2026 OpenTS contributors - * - * See LICENSE.md for applicable additional terms and warranty disclaimers. - ******************************************************************************/ - -#pragma once - -#include - -/// Names and comments from TLBs - -#define GAME_VERNAME TEXT("Tiberian Sun") - -EXTERN_C const IID IID_ILinkStream; -EXTERN_C const CLSID CLSID_CompressStream; -EXTERN_C const CLSID CLSID_HouseClass; -EXTERN_C const CLSID CLSID_SuperWeaponTypeClass; -EXTERN_C const CLSID CLSID_SuperWeaponClass; -EXTERN_C const CLSID CLSID_UnitTypeClass; -EXTERN_C const CLSID CLSID_InfantryTypeClass; -EXTERN_C const CLSID CLSID_AircraftTypeClass; -EXTERN_C const CLSID CLSID_BuildingTypeClass; -EXTERN_C const CLSID CLSID_BulletTypeClass; -EXTERN_C const CLSID CLSID_TerrainTypeClass; -EXTERN_C const CLSID CLSID_IsometricTileTypeClass; -EXTERN_C const CLSID CLSID_OverlayTypeClass; -EXTERN_C const CLSID CLSID_SmudgeTypeClass; -EXTERN_C const CLSID CLSID_AnimTypeClass; -EXTERN_C const CLSID CLSID_HouseTypeClass; -EXTERN_C const CLSID CLSID_IsometricTileClass; -EXTERN_C const CLSID CLSID_VoxelAnimClass; -EXTERN_C const CLSID CLSID_AircraftClass; -EXTERN_C const CLSID CLSID_AnimClass; -EXTERN_C const CLSID CLSID_InfantryClass; -EXTERN_C const CLSID CLSID_SmudgeClass; -EXTERN_C const CLSID CLSID_BuildingClass; -EXTERN_C const CLSID CLSID_OverlayClass; -EXTERN_C const CLSID CLSID_ParticleSystemClass; -EXTERN_C const CLSID CLSID_ParticleSystemTypeClass; -EXTERN_C const CLSID CLSID_BulletClass; -EXTERN_C const CLSID CLSID_UnitClass; -EXTERN_C const CLSID CLSID_ParticleClass; -EXTERN_C const CLSID CLSID_ParticleTypeClass; -EXTERN_C const CLSID CLSID_WaveClass; -EXTERN_C const CLSID CLSID_BuildingLightClass; -EXTERN_C const CLSID CLSID_TerrainClass; -EXTERN_C const CLSID CLSID_TubeClass; -EXTERN_C const CLSID CLSID_TeamClass; -EXTERN_C const CLSID CLSID_TaskForceClass; -EXTERN_C const CLSID CLSID_TeamTypeClass; -EXTERN_C const CLSID CLSID_VoxelAnimTypeClass; -EXTERN_C const CLSID CLSID_ScriptClass; -EXTERN_C const CLSID CLSID_ScriptTypeClass; -EXTERN_C const CLSID CLSID_TagClass; -EXTERN_C const CLSID CLSID_TagTypeClass; -EXTERN_C const CLSID CLSID_TriggerClass; -EXTERN_C const CLSID CLSID_TriggerTypeClass; -EXTERN_C const CLSID CLSID_ActionClass; -EXTERN_C const CLSID CLSID_EventClass; -EXTERN_C const CLSID CLSID_FactoryClass; -EXTERN_C const CLSID CLSID_WeaponTypeClass; -EXTERN_C const CLSID CLSID_WarheadTypeClass; -EXTERN_C const CLSID CLSID_WaypointPath; -EXTERN_C const CLSID CLSID_LightSource; -EXTERN_C const CLSID CLSID_CampaignClass; -EXTERN_C const CLSID CLSID_SideClass; -EXTERN_C const CLSID CLSID_TiberiumClass; -EXTERN_C const CLSID CLSID_CellClass; -EXTERN_C const CLSID CLSID_EMPulseClass; -EXTERN_C const CLSID CLSID_TacticalMapClass; -EXTERN_C const CLSID CLSID_AITriggerTypeClass; -EXTERN_C const CLSID CLSID_AITriggerClass; -EXTERN_C const CLSID CLSID_NeuronClass; -EXTERN_C const CLSID CLSID_FoggedObjectClass; -EXTERN_C const CLSID CLSID_AlphaShapeClass; -EXTERN_C const CLSID CLSID_VeinholeMonsterClass; diff --git a/code/isun_i.c b/code/isun_i.c deleted file mode 100644 index d667c8d25..000000000 --- a/code/isun_i.c +++ /dev/null @@ -1,238 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2026 OpenTS contributors - * - * See LICENSE.md for applicable additional terms and warranty disclaimers. - ******************************************************************************/ - -/* this file contains the actual definitions of */ -/* the IIDs and CLSIDs */ - -/* link this file in with the server and any clients */ - - -/* File created by MIDL compiler version X.XX.XXXX */ -/* at XXX XXX XX XX:XX:XX XXXX - */ -/* Compiler settings for XXXX.idl: - Oicf (OptLev=i2), W1, Zp8, env=Win32, ms_ext, c_ext - error checks: none -*/ -//@@MIDL_FILE_HEADING( ) -#ifdef __cplusplus -extern "C"{ -#endif - - -#ifndef __IID_DEFINED__ -#define __IID_DEFINED__ - -typedef struct _IID -{ - unsigned long x; - unsigned short s1; - unsigned short s2; - unsigned char c[8]; -} IID; - -#endif // __IID_DEFINED__ - -#ifndef CLSID_DEFINED -#define CLSID_DEFINED -typedef IID CLSID; -#endif // CLSID_DEFINED - -const IID IID_ILinkStream = {0x0D5CD78E,0x6470,0x11D2,{0x9B,0x74,0x00,0x10,0x4B,0x97,0x2F,0xE8}}; - - -const CLSID CLSID_CompressStream = {0xB48FA168,0x646F,0x11D2,{0x9B,0x74,0x00,0x10,0x4B,0x97,0x2F,0xE8}}; - - -const CLSID CLSID_HouseClass = {0xD9D4A910,0x87C6,0x11D1,{0xB7,0x07,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_SuperWeaponTypeClass = {0x0CF2BCE7,0x36E4,0x11D2,{0xB8,0xD8,0x00,0x60,0x08,0xC8,0x09,0xED}}; - - -const CLSID CLSID_SuperWeaponClass = {0xD7F754C6,0x391C,0x11D2,{0x9B,0x64,0x00,0x10,0x4B,0x97,0x2F,0xE8}}; - - -const CLSID CLSID_UnitTypeClass = {0xDCBD42EA,0x0546,0x11D2,{0xAC,0xA4,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_InfantryTypeClass = {0xAE8B33D8,0x061C,0x11D2,{0xAC,0xA4,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_AircraftTypeClass = {0xAE8B33D9,0x061C,0x11D2,{0xAC,0xA4,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_BuildingTypeClass = {0xAE8B33DB,0x061C,0x11D2,{0xAC,0xA4,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_BulletTypeClass = {0x5AF2CE77,0x0634,0x11D2,{0xAC,0xA4,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_TerrainTypeClass = {0x5AF2CE7B,0x0634,0x11D2,{0xAC,0xA4,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_IsometricTileTypeClass = {0x5AF2CE7A,0x0634,0x11D2,{0xAC,0xA4,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_OverlayTypeClass = {0x5AF2CE79,0x0634,0x11D2,{0xAC,0xA4,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_SmudgeTypeClass = {0x5AF2CE78,0x0634,0x11D2,{0xAC,0xA4,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_AnimTypeClass = {0xAE8B33DA,0x061C,0x11D2,{0xAC,0xA4,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_HouseTypeClass = {0x1DD43928,0x046B,0x11D2,{0xAC,0xA4,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_IsometricTileClass = {0x0E272DC0,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_VoxelAnimClass = {0x0E272DC1,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_AircraftClass = {0x0E272DC2,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_AnimClass = {0x0E272DC3,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_InfantryClass = {0x0E272DC4,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_SmudgeClass = {0x0E272DC5,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_BuildingClass = {0x0E272DC6,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_OverlayClass = {0x0E272DC7,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_ParticleSystemClass = {0x0E272DC8,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_ParticleSystemTypeClass = {0x703E044A,0x0FB1,0x11D2,{0x81,0x72,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_BulletClass = {0x0E272DC9,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_UnitClass = {0x0E272DCA,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_ParticleClass = {0x0E272DCC,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_ParticleTypeClass = {0x703E044B,0x0FB1,0x11D2,{0x81,0x72,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_WaveClass = {0x0E272DCD,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_BuildingLightClass = {0x54822258,0xD8A8,0x11D1,{0xB4,0x62,0x00,0x60,0x97,0xC6,0xA9,0x79}}; - - -const CLSID CLSID_TerrainClass = {0x0E272DCE,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_TubeClass = {0x0B4CA41C,0xB3A7,0x11D1,{0xB4,0x57,0x00,0x60,0x97,0xC6,0xA9,0x79}}; - - -const CLSID CLSID_TeamClass = {0x0E272DCF,0x9C0F,0x11D1,{0xB7,0x09,0x00,0xA0,0x24,0xDD,0xAF,0xD1}}; - - -const CLSID CLSID_TaskForceClass = {0x61DE341E,0x0774,0x11D2,{0xAC,0xA5,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_TeamTypeClass = {0xD1DBA64E,0x0778,0x11D2,{0xAC,0xA5,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_VoxelAnimTypeClass = {0x2EBB6D66,0x0D4D,0x11D2,{0x81,0x72,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_ScriptClass = {0x42F3A646,0x0789,0x11D2,{0xAC,0xA5,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_ScriptTypeClass = {0x42F3A647,0x0789,0x11D2,{0xAC,0xA5,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_TagClass = {0x54F6E432,0x09ED,0x11D2,{0xAC,0xA5,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_TagTypeClass = {0x54F6E433,0x09ED,0x11D2,{0xAC,0xA5,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_TriggerClass = {0xC02D1590,0x0A2A,0x11D2,{0xAC,0xA7,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_TriggerTypeClass = {0xC02D1591,0x0A2A,0x11D2,{0xAC,0xA7,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_ActionClass = {0x4F0EC392,0x0A55,0x11D2,{0xAC,0xA7,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_EventClass = {0x4F0EC393,0x0A55,0x11D2,{0xAC,0xA7,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_FactoryClass = {0x34ECD9A8,0x0AB0,0x11D2,{0xAC,0xA7,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_WeaponTypeClass = {0x9FD219CA,0x0F7B,0x11D2,{0x81,0x72,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_WarheadTypeClass = {0xA8C54DA4,0x0F7B,0x11D2,{0x81,0x72,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_WaypointPath = {0xF73125BA,0x1054,0x11D2,{0x81,0x72,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_LightSource = {0x6F9C48F0,0x1207,0x11D2,{0x81,0x74,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_CampaignClass = {0xFFDAC848,0x1517,0x11D2,{0x81,0x75,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_SideClass = {0xC53DD372,0x151E,0x11D2,{0x81,0x75,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_TiberiumClass = {0xC53DD373,0x151E,0x11D2,{0x81,0x75,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_CellClass = {0xC1BF99CE,0x1A8C,0x11D2,{0x81,0x75,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_EMPulseClass = {0xB825CB22,0x200E,0x11D2,{0x9F,0xA9,0x00,0x60,0x08,0x9A,0xD4,0x58}}; - - -const CLSID CLSID_TacticalMapClass = {0xCF56B38A,0x240D,0x11D2,{0x81,0x7C,0x00,0x60,0x08,0x05,0x5B,0xB5}}; - - -const CLSID CLSID_AITriggerTypeClass = {0xBA093524,0x4CF4,0x11D2,{0xBC,0x26,0x00,0x10,0x4B,0x8F,0xB0,0x4D}}; - - -const CLSID CLSID_AITriggerClass = {0x03C4CE76,0x4CF5,0x11D2,{0xBC,0x26,0x00,0x10,0x4B,0x8F,0xB0,0x4D}}; - - -const CLSID CLSID_NeuronClass = {0x241AB316,0x4CF5,0x11D2,{0xBC,0x26,0x00,0x10,0x4B,0x8F,0xB0,0x4D}}; - - -const CLSID CLSID_FoggedObjectClass = {0x1C470B0E,0x69D7,0x11D2,{0xB8,0xF2,0x00,0x60,0x08,0xC8,0x09,0xED}}; - - -const CLSID CLSID_AlphaShapeClass = {0x623C7584,0x74E7,0x11D2,{0xB8,0xF5,0x00,0x60,0x08,0xC8,0x09,0xED}}; - - -const CLSID CLSID_VeinholeMonsterClass = {0x5192D06A,0xC632,0x11D2,{0xB9,0x0B,0x00,0x60,0x08,0xC8,0x09,0xED}}; - - -#ifdef __cplusplus -} -#endif diff --git a/code/jumpjet.cpp b/code/jumpjet.cpp index 717456b26..61fd2c653 100644 --- a/code/jumpjet.cpp +++ b/code/jumpjet.cpp @@ -64,7 +64,7 @@ JumpjetLocomotionClass::~JumpjetLocomotionClass(void) /// This asks whether the unit has a destination, not whether it happens to be in the air. /// /// bool; Does the jumpjet have somewhere to be? -boolean STDMETHODCALLTYPE JumpjetLocomotionClass::Is_Moving(void) +bool JumpjetLocomotionClass::Is_Moving(void) { return(IsMoving); } @@ -75,7 +75,7 @@ boolean STDMETHODCALLTYPE JumpjetLocomotionClass::Is_Moving(void) /// /// Returns with the destination coordinate. Otherwise, COORD_NONE is /// returned. -Coord STDMETHODCALLTYPE JumpjetLocomotionClass::Destination(void) +Coord JumpjetLocomotionClass::Destination(void) { if (Is_Moving()) { return(HeadToCoord); @@ -92,7 +92,7 @@ Coord STDMETHODCALLTYPE JumpjetLocomotionClass::Destination(void) /// and resubmits the object to the map when its display layer changes. An ion storm will /// bring down anything caught off the ground. /// -boolean STDMETHODCALLTYPE JumpjetLocomotionClass::Process(void) +bool JumpjetLocomotionClass::Process(void) { LayerType layer = In_Which_Layer(); @@ -167,7 +167,7 @@ boolean STDMETHODCALLTYPE JumpjetLocomotionClass::Process(void) /// /// The coordinate to fly to, or COORD_NONE to give the unit no /// destination at all. -void STDMETHODCALLTYPE JumpjetLocomotionClass::Move_To(Coord to) +void JumpjetLocomotionClass::Move_To(Coord to) { if (HeadToCoord != COORD_NONE && CurrentState != GROUNDED && IsLanding) { LinkedTo->Clear_Occupy_Bit(HeadToCoord); @@ -200,7 +200,7 @@ void STDMETHODCALLTYPE JumpjetLocomotionClass::Move_To(Coord to) /// it could put down in. A unit with nowhere at all to land is destroyed rather than left /// hanging in the air. /// -void STDMETHODCALLTYPE JumpjetLocomotionClass::Stop_Moving(void) +void JumpjetLocomotionClass::Stop_Moving(void) { if (IsMoving) { if (HeadToCoord != COORD_NONE && CurrentState != GROUNDED && IsLanding) { @@ -230,23 +230,15 @@ void STDMETHODCALLTYPE JumpjetLocomotionClass::Stop_Moving(void) /// through the locomotor's own facing tracker. /// /// The direction the unit should be facing. -void STDMETHODCALLTYPE JumpjetLocomotionClass::Do_Turn(DirType coord) +void JumpjetLocomotionClass::Do_Turn(DirType coord) { LinkedTo->PrimaryFacing.Set(coord); } -/// -/// Fetches the class identifier of this locomotor. -/// The persistence machinery uses the identifier to build the right kind of locomotor back -/// when a save game is loaded. -/// -/// Returns with S_OK, or E_POINTER if there is nowhere to put the answer. -HRESULT STDMETHODCALLTYPE JumpjetLocomotionClass::GetClassID(CLSID * retval) +ClassID JumpjetLocomotionClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_JumpjetLocomotion; - return(S_OK); + return(ClassID_JumpjetLocomotion); } @@ -277,7 +269,7 @@ void JumpjetLocomotionClass::Serialize(SaveStreamClass & stream) /// measured from the bridge deck rather than from the ground. /// /// Returns with the layer this object should be drawn in. -LayerType STDMETHODCALLTYPE JumpjetLocomotionClass::In_Which_Layer(void) +LayerType JumpjetLocomotionClass::In_Which_Layer(void) { int height = LinkedTo->HeightAGL; if (!LinkedTo->IsOnBridge) { @@ -479,7 +471,7 @@ void JumpjetLocomotionClass::Process_Unknown(void) /// not it has been given a destination. /// /// bool; Is the jumpjet in flight toward somewhere? -boolean STDMETHODCALLTYPE JumpjetLocomotionClass::Is_Moving_Now(void) +bool JumpjetLocomotionClass::Is_Moving_Now(void) { if (CurrentState != GROUNDED && CurrentState != HOVERING) { return(true); @@ -684,7 +676,7 @@ int JumpjetLocomotionClass::Desired_Flight_Level(void) const /// that reservation is given up when the object is lifted off the map. /// /// The marking operation being performed, such as MARK_UP. -void STDMETHODCALLTYPE JumpjetLocomotionClass::Mark_All_Occupation_Bits(int mark) +void JumpjetLocomotionClass::Mark_All_Occupation_Bits(int mark) { if (mark == MARK_UP) { Coord headto = Head_To_Coord(); @@ -702,7 +694,7 @@ void STDMETHODCALLTYPE JumpjetLocomotionClass::Mark_All_Occupation_Bits(int mark /// destination. /// /// Returns with the coordinate being flown to. -Coord STDMETHODCALLTYPE JumpjetLocomotionClass::Head_To_Coord(void) +Coord JumpjetLocomotionClass::Head_To_Coord(void) { if (CurrentState == GROUNDED) { return(LinkedTo->PositionCoord); diff --git a/code/jumpjet.h b/code/jumpjet.h index e8bc9d07e..5a3872031 100644 --- a/code/jumpjet.h +++ b/code/jumpjet.h @@ -25,20 +25,20 @@ class JumpjetLocomotionClass : public LocomotionClass JumpjetLocomotionClass(void); virtual ~JumpjetLocomotionClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; - virtual boolean STDMETHODCALLTYPE Is_Moving(void) override; - virtual Coord STDMETHODCALLTYPE Destination(void) override; - virtual Coord STDMETHODCALLTYPE Head_To_Coord(void) override; - virtual boolean STDMETHODCALLTYPE Process(void) override; - virtual void STDMETHODCALLTYPE Move_To(Coord to) override; - virtual void STDMETHODCALLTYPE Stop_Moving(void) override; - virtual void STDMETHODCALLTYPE Do_Turn(DirType coord) override; - virtual LayerType STDMETHODCALLTYPE In_Which_Layer(void) override; - virtual boolean STDMETHODCALLTYPE Is_Moving_Now(void) override; - virtual void STDMETHODCALLTYPE Mark_All_Occupation_Bits(int mark) override; + virtual bool Is_Moving(void) override; + virtual Coord Destination(void) override; + virtual Coord Head_To_Coord(void) override; + virtual bool Process(void) override; + virtual void Move_To(Coord to) override; + virtual void Stop_Moving(void) override; + virtual void Do_Turn(DirType coord) override; + virtual LayerType In_Which_Layer(void) override; + virtual bool Is_Moving_Now(void) override; + virtual void Mark_All_Occupation_Bits(int mark) override; /*--------------------------------------------------------------------- ** Member function prototypes. diff --git a/code/keyboard.cpp b/code/keyboard.cpp index fbae09bbf..7e43d4737 100644 --- a/code/keyboard.cpp +++ b/code/keyboard.cpp @@ -801,3 +801,71 @@ int WWKeyboardClass::Noop(void) const { return(0); } + + +/// +/// Converts a key code into its printable name. +/// This routine is used by the hotkey control to show a binding the way the player's own +/// keyboard layout names it, with the modifier names spelled out ahead of the key. +/// +/// The key, complete with its modifier bits, to spell out. +/// Buffer to build the name in. +/// Be sure that the buffer is big enough for the modifier names as well. +int Build_Hotkey_String(KeyNumType key, char * buffer) +{ + char key_name[32]; + unsigned char modifier = HIBYTE(key); + + buffer[0] = '\0'; + + UINT lparam; + + /// (p << 16) - places the scan code into bits 16-23. + /// (1 << 0) - purpose unknown; Windows does not document this bit. + /// (1 << 24) - Extended-key bit. Distinguishes some keys on an enhanced keyboard. + /// (1 << 25) - "Don't care" bit. Should not distinguish between left and right ctrl and shift keys. + + if ((modifier & (WWKEY_ALT_BIT >> 8)) != 0) { + lparam = MapVirtualKey(VK_MENU, 0) ; + lparam = (lparam << 16); + lparam |= (1 << 0); + lparam |= (1 << 25); + GetKeyNameText(lparam, key_name, sizeof(key_name)); + strcat(buffer, key_name); + strcat(buffer, "+"); + } + + if ((modifier & (WWKEY_CTRL_BIT >> 8)) != 0) { + lparam = MapVirtualKey(VK_CONTROL, 0); + lparam = (lparam << 16); + lparam |= (1 << 0); + lparam |= (1 << 25); + GetKeyNameText(lparam, key_name, sizeof(key_name)); + strcat(buffer, key_name); + strcat(buffer, "+"); + } + + if ((modifier & (WWKEY_SHIFT_BIT >> 8)) != 0) { + lparam = MapVirtualKey(VK_SHIFT, 0); + lparam = (lparam << 16); + lparam |= (1 << 0); + lparam |= (1 << 25); + GetKeyNameText(lparam, key_name, sizeof(key_name)); + strcat(buffer, key_name); + strcat(buffer, "+"); + } + + lparam = MapVirtualKey(key & 0xFF, 0); + lparam = (lparam << 16); + lparam |= (1 << 0); + lparam |= (1 << 25); + + if ((modifier & (WWKEY_RLS_BIT >> 8)) != 0) { + lparam |= (1 << 24); + } + + GetKeyNameText(lparam, key_name, sizeof(key_name)); + strcat(buffer, key_name); + + return(0); +} diff --git a/code/keyboard.h b/code/keyboard.h index 018a4e63a..2866e38ac 100644 --- a/code/keyboard.h +++ b/code/keyboard.h @@ -671,3 +671,7 @@ struct KeyboardClass : public WWKeyboardClass int Mouse_X(void) {return(Get_Mouse_X());}; int Mouse_Y(void) {return(Get_Mouse_Y());}; }; + + +// Spells a key, with its modifiers, into a human readable name. +int Build_Hotkey_String(KeyNumType key, char * buffer); diff --git a/code/language/CMakeLists.txt b/code/language/CMakeLists.txt index 070782148..d934f84ae 100644 --- a/code/language/CMakeLists.txt +++ b/code/language/CMakeLists.txt @@ -21,7 +21,65 @@ if(MSVC) endif() # The game loads this library by name from its own directory, so it is built beside the -# executable rather than copied there. +# executable rather than copied there. A shared library is a runtime artifact only on +# Windows; everywhere else it is placed by the library output path. set_target_properties(Language PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" ) + +# A UI document names a string by its symbolic name, so the same script that reads the resource +# script writes the table that turns a name back into its identifier. Every platform needs it, +# because every platform builds the UI shell. +set(OPENTS_STRING_NAMES "${OPENTS_GENERATED_DIR}/stringnames.hh") + +add_custom_command( + OUTPUT "${OPENTS_STRING_NAMES}" + COMMAND ${CMAKE_COMMAND} + "-DRC_FILE=${CMAKE_CURRENT_SOURCE_DIR}/language.rc" + "-DHEADER_FILE=${CMAKE_CURRENT_SOURCE_DIR}/language.h" + "-DNAME_TABLE=${OPENTS_STRING_NAMES}" + -P "${CMAKE_SOURCE_DIR}/cmake/StringTable.cmake" + DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/language.rc" + "${CMAKE_CURRENT_SOURCE_DIR}/language.h" + "${CMAKE_SOURCE_DIR}/cmake/StringTable.cmake" + COMMENT "Generating the string name table from language.rc" + VERBATIM +) + +add_custom_target(OpenTSStringNames ALL DEPENDS "${OPENTS_STRING_NAMES}") +add_dependencies(OpenTS OpenTSStringNames) + +# Only the resource compiler turns the string table in language.rc into a module resource, so a +# host without one reads the same strings out of a flat data file generated from the same +# script. The Windows build neither generates nor ships it and keeps using LoadString. +if(NOT WIN32) + set(OPENTS_STRING_TABLE "${OPENTS_GENERATED_DIR}/Language.dat") + + add_custom_command( + OUTPUT "${OPENTS_STRING_TABLE}" + COMMAND ${CMAKE_COMMAND} + "-DRC_FILE=${CMAKE_CURRENT_SOURCE_DIR}/language.rc" + "-DHEADER_FILE=${CMAKE_CURRENT_SOURCE_DIR}/language.h" + "-DOUTPUT=${OPENTS_STRING_TABLE}" + -P "${CMAKE_SOURCE_DIR}/cmake/StringTable.cmake" + DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/language.rc" + "${CMAKE_CURRENT_SOURCE_DIR}/language.h" + "${CMAKE_SOURCE_DIR}/cmake/StringTable.cmake" + COMMENT "Generating the portable string table from language.rc" + VERBATIM + ) + + add_custom_target(OpenTSStringTable ALL + DEPENDS "${OPENTS_STRING_TABLE}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/bin" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${OPENTS_STRING_TABLE}" + "${CMAKE_BINARY_DIR}/bin/Language.dat" + VERBATIM + ) + + add_dependencies(OpenTS OpenTSStringTable) +endif() diff --git a/code/language/language.rc b/code/language/language.rc index d62aa6be9..7ee56f271 100644 --- a/code/language/language.rc +++ b/code/language/language.rc @@ -115,1499 +115,6 @@ END #endif // APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Dialog -// - -IDD_MISSION_ABORT DIALOG DISCARDABLE 0, 0, 256, 63 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW,174,37,60,14 - CTEXT "Do you want to abort the mission?",-1,22,12,212,19, - SS_CENTERIMAGE | NOT WS_GROUP - CONTROL "Abort",IDC_ABORT_MISSION,"Button",BS_OWNERDRAW,22,37,60, - 14 - CONTROL "Restart",IDC_RESTART_MISSION,"Button",BS_OWNERDRAW,98, - 37,60,14 -END - -IDD_OPT_CONFIRM_MODE DIALOG DISCARDABLE 0, 0, 239, 70 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "OK",IDOK,"Button",BS_OWNERDRAW,111,44,50,14 - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW,167,44,50,14 - LTEXT "Click OK to keep this display mode or wait and your old display settings will be restored.", - IDC_CONFIRM_MODE_TEXT,22,12,195,26,NOT WS_GROUP -END - -IDD_OPT_KEYBOARD DIALOGEX 0, 0, 336, 208 -STYLE WS_CHILD -FONT 8, "MS Sans Serif", 0, 0, 0x1 -BEGIN - LTEXT "Category:",-1,22,27,146,9,SS_CENTERIMAGE | NOT WS_GROUP - COMBOBOX IDC_KEY_CATEGORY,22,42,138,146,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_SORT | CBS_HASSTRINGS - LTEXT "Commands:",-1,168,27,146,8,SS_CENTERIMAGE | NOT - WS_GROUP - LISTBOX IDC_KEY_COMMANDS,168,41,146,104,LBS_SORT | - LBS_OWNERDRAWFIXED | LBS_HASSTRINGS | - LBS_NOINTEGRALHEIGHT | NOT WS_BORDER - LTEXT "Press new shortcut key:",-1,22,119,128,9,SS_CENTERIMAGE | - NOT WS_GROUP - GROUPBOX "Description:",-1,22,57,138,57 - LTEXT "",IDC_KEY_DESCRIPTION,29,68,127,42,NOT WS_GROUP, - WS_EX_TRANSPARENT - CONTROL "HotKey1",IDC_KEY_HOTKEY,"msctls_hotkey32",WS_BORDER,22, - 131,85,14 - CONTROL "Assign",IDC_KEY_ASSIGN,"Button",BS_OWNERDRAW,110,132,50, - 14 - CONTROL "OK",IDOK,"Button",BS_OWNERDRAW,187,181,50,14 - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW,264,181,50,14 - CTEXT "Customize Keyboard",-1,22,12,292,11,SS_CENTERIMAGE | - NOT WS_GROUP - LTEXT "Currently assigned to:",-1,22,151,146,11,SS_CENTERIMAGE | - NOT WS_GROUP - LTEXT "Current shortcut:",-1,168,151,146,11,SS_CENTERIMAGE | - NOT WS_GROUP - LTEXT "",IDC_KEY_ASSIGNED_TO,22,166,146,10,SS_CENTERIMAGE | - NOT WS_GROUP - LTEXT "",IDC_KEY_CURRENT_SHORTCUT,168,166,146,10, - SS_CENTERIMAGE | NOT WS_GROUP - CONTROL "Reset All",IDC_KEY_RESET_ALL,"Button",BS_OWNERDRAW,22, - 181,54,14 -END - -IDD_OPT_DISPLAY DIALOG DISCARDABLE 0, 0, 229, 196 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "OK",IDOK,"Button",BS_OWNERDRAW,22,170,62,14 - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW,145,170,62,14 - LISTBOX IDC_DISPLAY_RESLIST,22,37,185,110,LBS_OWNERDRAWFIXED | - LBS_HASSTRINGS | LBS_NOINTEGRALHEIGHT | NOT WS_BORDER - CTEXT "Resolution Modes",IDC_DISPLAY_RESLABEL,22,25,185,10, - SS_CENTERIMAGE | NOT WS_GROUP - CTEXT "Display Options:",-1,22,12,185,9,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Stretch movies to fit resolution",IDC_STRETCH_MOVIES, - "Button",BS_AUTOCHECKBOX | BS_FLAT,22,153,185,10 -END - -IDD_DROPSHIP_LIMITS DIALOGEX 0, 0, 230, 140 -STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU -CAPTION "Dropship Loadout Limits" -FONT 8, "MS Sans Serif", 0, 0, 0x1 -BEGIN - DEFPUSHBUTTON "OK",IDC_DROPSHIP_OK,101,114,50,14 - PUSHBUTTON "Cancel",IDC_CANCEL,158,113,50,14 - CONTROL "List1",IDC_DROPSHIP_LIST,"SysListView32",LVS_REPORT | - LVS_SORTASCENDING | LVS_EDITLABELS | WS_BORDER | - WS_TABSTOP,7,7,202,98,0,HIDC_DROPSHIP_LIST -END - -IDD_DROPSHIP_LIMIT DIALOG DISCARDABLE 0, 0, 203, 71 -STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU -CAPTION "Dropship Unit Limit" -FONT 8, "MS Sans Serif" -BEGIN - DEFPUSHBUTTON "OK",IDC_DROPSHIP_OK,78,48,50,14 - PUSHBUTTON "Cancel",IDC_CANCEL,139,48,50,14 - LTEXT "Unit Name",IDC_DROPSHIP_UNITNAME_LABEL,19,7,55,10 - EDITTEXT IDC_DROPSHIP_LIMIT_EDIT,19,24,49,14,ES_AUTOHSCROLL - LTEXT "(Use -1 to signify an unlimited supply)",-1,73,25,123, - 12 -END - -IDD_OPT_CTRL_GAME_MP DIALOG DISCARDABLE 0, 0, 292, 175 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Options Menu",1,"Button",BS_OWNERDRAW,196,149,77,14 - CONTROL "Keyboard",IDC_OPT_KEYBOARD_BTN,"Button",BS_OWNERDRAW, - 109,149,77,14 - CONTROL "Sound",IDC_OPT_SOUND_BTN,"Button",BS_OWNERDRAW,22,149, - 77,14 - CONTROL "Slider1",IDC_GAME_SPEED_SLIDER,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,90,12,128,15 - RTEXT "Game Speed:",-1,22,12,63,15,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Slider2",IDC_SCROLL_SPEED_SLIDER,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,90,43,128,15 - RTEXT "Scroll Rate:",-1,22,43,63,15,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Slider3",IDC_DETAIL_LEVEL_SLIDER,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,90,74,128,15 - RTEXT "Visual Details:",-1,22,74,63,15,SS_CENTERIMAGE | NOT - WS_GROUP - LTEXT "Higher",IDC_DETAIL_LEVEL_LABEL,224,74,50,15, - SS_CENTERIMAGE | NOT WS_GROUP - LTEXT "Faster",IDC_SCROLL_SPEED_LABEL,224,43,50,15, - SS_CENTERIMAGE | NOT WS_GROUP - LTEXT "Faster",IDC_GAME_SPEED_LABEL,224,12,50,15, - SS_CENTERIMAGE | NOT WS_GROUP - CONTROL "Sidebar Text",IDC_SIDEBAR_TEXT,"Button",BS_AUTOCHECKBOX | - BS_FLAT,22,94,119,10 - CONTROL "Target Lines",IDC_TARGET_LINES,"Button",BS_AUTOCHECKBOX | - BS_FLAT,22,112,119,10 - CONTROL "Tooltips",IDC_TOOLTIPS,"Button",BS_AUTOCHECKBOX | - BS_FLAT,147,94,127,10 - CONTROL "Scroll Coasting",IDC_SCROLL_COASTING,"Button", - BS_AUTOCHECKBOX | BS_FLAT,147,112,127,10 - CONTROL "Edge Scrolling",IDC_EDGE_SCROLL,"Button", - BS_AUTOCHECKBOX | BS_FLAT,22,130,119,10 -END - -IDD_OPT_CTRL_GAME_SP DIALOG DISCARDABLE 0, 0, 292, 179 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Main Menu",1,"Button",BS_OWNERDRAW,81,153,130,14 - CONTROL "Slider1",IDC_GAME_SPEED_SLIDER,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,80,12,148,13 - LTEXT "Game Speed",-1,22,12,58,13,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Slider2",IDC_SCROLL_SPEED_SLIDER,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,80,34,148,13 - LTEXT "Scroll Rate",-1,22,34,58,13,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Slider3",IDC_DETAIL_LEVEL_SLIDER,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,80,56,148,13 - LTEXT "Visual Details",-1,22,56,58,13,SS_CENTERIMAGE | NOT - WS_GROUP - RTEXT "Higher",IDC_DETAIL_LEVEL_LABEL,229,56,45,13, - SS_CENTERIMAGE | NOT WS_GROUP - RTEXT "Faster",IDC_SCROLL_SPEED_LABEL,229,34,45,13, - SS_CENTERIMAGE | NOT WS_GROUP - RTEXT "Faster",IDC_GAME_SPEED_LABEL,229,12,45,13, - SS_CENTERIMAGE | NOT WS_GROUP - CONTROL "Slider3",IDC_DIFFICULTY_SLIDER,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,80,78,148,13 - LTEXT "Difficulty",-1,22,78,58,13,SS_CENTERIMAGE | NOT - WS_GROUP - RTEXT "Harder",IDC_DIFFICULTY_LABEL,229,78,45,13, - SS_CENTERIMAGE | NOT WS_GROUP - CONTROL "Cameo Text",IDC_SIDEBAR_TEXT,"Button",BS_AUTOCHECKBOX | - BS_FLAT,22,103,124,10 - CONTROL "Target Lines",IDC_TARGET_LINES,"Button",BS_AUTOCHECKBOX | - BS_FLAT,22,119,124,10 - CONTROL "Tooltips",IDC_TOOLTIPS,"Button",BS_AUTOCHECKBOX | - BS_FLAT,146,103,128,10 - CONTROL "Scroll Coasting",IDC_SCROLL_COASTING,"Button", - BS_AUTOCHECKBOX | BS_FLAT,146,119,128,10 - CONTROL "Edge Scrolling",IDC_EDGE_SCROLL,"Button", - BS_AUTOCHECKBOX | BS_FLAT,22,135,124,10 -END - -IDD_GAME_SETTINGS_FLAGS DIALOG DISCARDABLE 0, 0, 220, 77 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Bases",IDC_BASES,"Button",BS_AUTOCHECKBOX,5,18,102,10 - CONTROL "Harvester Truce",IDC_HARVTRUCE,"Button",BS_AUTOCHECKBOX, - 5,61,203,10 - CONTROL "Allies allowed",IDC_ALLIES,"Button",BS_AUTOCHECKBOX,5,4, - 103,10 - LTEXT "Time Limit",-1,0,67,50,10,NOT WS_VISIBLE | WS_DISABLED - LTEXT "Kill Limit",-1,112,67,50,10,NOT WS_VISIBLE | - WS_DISABLED - CONTROL "Slider1",IDC_TIMELIMIT,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS | NOT WS_VISIBLE | WS_DISABLED,43,64,64,13 - CONTROL "Slider2",IDC_KILLLIMIT,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS | NOT WS_VISIBLE | WS_DISABLED,149,64,64,13 - CONTROL "Bridges Destroyable",IDC_BRIDGE_DESTROY,"Button", - BS_AUTOCHECKBOX,5,47,204,10 - CONTROL "Re-Deployable MCV",IDC_REDEPLOY_MCV,"Button", - BS_AUTOCHECKBOX | WS_TABSTOP,5,32,206,11 - CONTROL "Fog Of War",IDC_FOG_OF_WAR,"Button",BS_AUTOCHECKBOX,118, - 4,93,10 - CONTROL "Crates",IDC_CRATES,"Button",BS_AUTOCHECKBOX,118,18,95, - 10 -END - -IDD_GAME_SETTINGS_SLIDERS DIALOG DISCARDABLE 0, 0, 220, 77 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Slider1",IDC_UNITCOUNT,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS,105,33,70,13 - LTEXT "Unit Count",-1,45,35,60,10 - CONTROL "Slider2",IDC_TECHLEVEL,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS,105,48,70,13 - CONTROL "Slider3",IDC_CREDITS,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS,105,3,70,13 - LTEXT "Tech Level",-1,45,50,60,10 - LTEXT "Credits",-1,45,5,60,10 - LTEXT "AI Players",-1,45,65,60,10 - CONTROL "Slider3",IDC_AIPLAYERS,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS,105,63,70,13 - LTEXT "Difficulty",-1,45,20,60,10 - CONTROL "Slider3",IDC_GAMESET_DIFFICULTY,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,105,18,70,13 -END - -IDD_OPT_MAIN DIALOG DISCARDABLE 0, 0, 200, 148 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Main Menu",IDC_OPTMAIN_MAINMENU,"Button",BS_OWNERDRAW | - BS_CENTER,37,120,126,18 - CONTROL "Display",IDC_OPTMAIN_DISPLAY,"Button",BS_OWNERDRAW | - BS_CENTER,37,32,126,18 - CONTROL "Keyboard",IDC_OPTMAIN_KEYBOARD,"Button",BS_OWNERDRAW | - BS_CENTER,37,76,126,18 - CONTROL "Sound",IDC_OPTMAIN_SOUND,"Button",BS_OWNERDRAW | - BS_CENTER,37,54,126,18 - CONTROL "Game Settings",IDC_OPTMAIN_GAME_SETTINGS,"Button", - BS_OWNERDRAW | BS_CENTER,37,10,126,18 -END - -IDD_MAIN_MENU DIALOG DISCARDABLE 0, 0, 204, 147 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Exit Game",IDC_EXIT_GAME,"Button",BS_OWNERDRAW,37,117, - 130,18 - CONTROL "New Campaign",IDC_NEWCAMPAIGN,"Button",BS_OWNERDRAW,37, - 12,130,18 - CONTROL "Load Mission",IDC_LOAD_MISSION,"Button",BS_OWNERDRAW,37, - 33,130,18 - CONTROL "Multiplayer Game",IDC_MULTIPLAYER_GAME,"Button", - BS_OWNERDRAW,37,54,130,18 - CONTROL "Intro / Sneak Peek",IDC_INTRO,"Button",BS_OWNERDRAW,37, - 75,130,18 - CONTROL "Options",IDC_OPTIONS,"Button",BS_OWNERDRAW,37,96,130,18 -END - -IDD_MAPGEN DIALOG DISCARDABLE 0, 0, 424, 242 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "OK",IDOK,"Button",BS_OWNERDRAW,265,220,65,14 - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW,337,220,65,14 - COMBOBOX IDC_MAPGEN_ENVIRONMENT,95,8,100,103,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_SORT | CBS_HASSTRINGS - CONTROL "Slider1",IDC_MAPGEN_HILLS,"msctls_trackbar32",TBS_TOP, - 95,104,100,14 - LTEXT "Hills:",-1,22,104,74,14,SS_CENTERIMAGE | NOT WS_GROUP - CONTROL "Slider1",IDC_MAPGEN_WATER,"msctls_trackbar32",TBS_TOP, - 95,161,100,14 - LTEXT "Players:",-1,22,47,74,14,SS_CENTERIMAGE | NOT WS_GROUP - LTEXT "Map Width:",-1,206,8,65,12,SS_CENTERIMAGE | NOT - WS_GROUP - LTEXT "Map Height:",-1,205,25,65,12,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Slider1",IDC_MAPGEN_VEGETATION,"msctls_trackbar32", - TBS_TOP,95,180,100,14 - CONTROL "Slider1",IDC_MAPGEN_CITIES,"msctls_trackbar32",TBS_TOP, - 95,199,100,14 - CONTROL "Slider1",IDC_MAPGEN_TIBERIUM_FIELDS,"msctls_trackbar32", - TBS_TOP,95,142,100,14 - LTEXT "Tiberium Fields:",-1,22,142,74,14,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Slider1",IDC_MAPGEN_ACCESSIBILITY,"msctls_trackbar32", - TBS_TOP,95,85,100,14 - LTEXT "Environment:",-1,22,8,66,14,SS_CENTERIMAGE | NOT - WS_GROUP - EDITTEXT IDC_MAPGEN_DIMENSION_EDIT,345,25,57,12,ES_NUMBER | NOT - WS_VISIBLE | WS_DISABLED | NOT WS_BORDER | NOT - WS_TABSTOP - CONTROL "Load Map",IDC_MAPGEN_LOAD_MAP,"Button",BS_OWNERDRAW,100, - 220,65,14 - CONTROL "Save Map",IDC_MAPGEN_SAVE_MAP,"Button",BS_OWNERDRAW,22, - 220,65,14 - COMBOBOX IDC_MAPGEN_TIME_OF_DAY,95,25,100,101,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_SORT | CBS_HASSTRINGS - LTEXT "Water:",-1,22,161,74,14,SS_CENTERIMAGE | NOT WS_GROUP - LTEXT "Time of Day:",-1,22,27,66,14,SS_CENTERIMAGE | NOT - WS_GROUP - LTEXT "Vegetation:",-1,22,180,74,14,SS_CENTERIMAGE | NOT - WS_GROUP - LTEXT "Cities:",-1,22,199,74,14,SS_CENTERIMAGE | NOT WS_GROUP - LTEXT "Accessability:",-1,22,85,74,14,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Preview Map",IDC_MAPGEN_PREVIEW,"Button",BS_OWNERDRAW, - 314,193,88,14 - GROUPBOX "",IDC_PREVIEW_FRAME,205,47,197,134 - CTEXT "Preview",-1,208,101,194,16,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Surprise Me",IDC_MAPGEN_SURPRISE,"Button",BS_OWNERDRAW, - 205,193,88,14 - CONTROL "Delete Map",IDC_MAPGEN_DELETE_MAP,"Button",BS_OWNERDRAW, - 178,220,65,14 - CONTROL "Slider1",IDC_MAPGEN_CLIFFS,"msctls_trackbar32",TBS_TOP, - 95,66,100,14 - LTEXT "Cliffs:",-1,22,66,74,14,SS_CENTERIMAGE | NOT WS_GROUP - CONTROL "Slider1",IDC_MAPGEN_PLAYERS,"msctls_trackbar32",TBS_TOP, - 95,47,100,14 - CONTROL "Slider1",IDC_MAPGEN_TIBERIUM_AMOUNT,"msctls_trackbar32", - TBS_TOP,95,123,100,14 - LTEXT "Tiberium Amount:",-1,22,123,74,14,SS_CENTERIMAGE | NOT - WS_GROUP - COMBOBOX IDC_MAPGEN_MAP_WIDTH,277,8,63,103,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_HASSTRINGS - COMBOBOX IDC_MAPGEN_MAP_HEIGHT,277,25,63,103,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_HASSTRINGS -END - -IDD_MSGBOX_1 DIALOG DISCARDABLE 0, 0, 218, 64 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW | NOT WS_VISIBLE, - 83,38,50,14 - CTEXT "",IDC_MSGBOX_TEXT,22,12,174,23,SS_CENTERIMAGE | NOT - WS_GROUP -END - -IDD_MSGBOX_3 DIALOG DISCARDABLE 0, 0, 260, 84 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW,178,58,60,14 - CTEXT "Do you want to abort the mission?",IDC_MSGBOX_TEXT,22, - 12,216,38,SS_CENTERIMAGE | NOT WS_GROUP - CONTROL "OK",IDC_MSGBOX_OK,"Button",BS_OWNERDRAW,22,58,60,14 - CONTROL "Button 3",IDC_MSGBOX_BTN3,"Button",BS_OWNERDRAW,100,58, - 60,14 -END - -IDD_MISSION_DELETE DIALOG DISCARDABLE 0, 0, 302, 198 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Delete",1,"Button",BS_OWNERDRAW | WS_TABSTOP,173,172,50, - 14 - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW | WS_TABSTOP,230, - 172,50,14 - LISTBOX IDC_MISSION_DELETE_LIST,22,42,258,122,LBS_OWNERDRAWFIXED | - LBS_HASSTRINGS | LBS_NOINTEGRALHEIGHT | NOT WS_BORDER | - WS_TABSTOP - CTEXT "DELETE",-1,22,12,258,8,SS_CENTERIMAGE | NOT WS_GROUP - LTEXT "Description",-1,22,26,121,8,SS_CENTERIMAGE | NOT - WS_GROUP - CTEXT "Mission",-1,149,26,33,8,SS_CENTERIMAGE | NOT WS_VISIBLE | - WS_DISABLED | NOT WS_GROUP - CTEXT "Time Stamp",-1,196,26,71,8,SS_CENTERIMAGE | NOT - WS_GROUP -END - -IDD_MISSION_LOAD DIALOG DISCARDABLE 0, 0, 302, 198 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Load",1,"Button",BS_OWNERDRAW | WS_TABSTOP,171,171,50, - 14 - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW | WS_TABSTOP,230, - 171,50,14 - LISTBOX IDC_MISSION_LOAD_LIST,22,40,258,124,LBS_OWNERDRAWFIXED | - LBS_HASSTRINGS | LBS_NOINTEGRALHEIGHT | NOT WS_BORDER - CTEXT "LOAD",-1,22,12,258,8,SS_CENTERIMAGE | NOT WS_GROUP - LTEXT "Description",-1,22,26,121,8,SS_CENTERIMAGE | NOT - WS_GROUP - CTEXT "Mission",-1,149,26,39,8,SS_CENTERIMAGE | NOT WS_VISIBLE | - WS_DISABLED | NOT WS_GROUP - CTEXT "Time Stamp",-1,196,26,72,8,SS_CENTERIMAGE | NOT - WS_GROUP -END - -IDD_MISSION_SAVE DIALOG DISCARDABLE 0, 0, 302, 198 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Save",1,"Button",BS_OWNERDRAW | WS_TABSTOP,174,172,50, - 14 - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW | WS_TABSTOP,230, - 172,50,14 - LISTBOX IDC_MISSION_SAVE_LIST,22,42,258,105,LBS_OWNERDRAWFIXED | - LBS_HASSTRINGS | LBS_NOINTEGRALHEIGHT | NOT WS_BORDER | - WS_TABSTOP - CTEXT "SAVE",-1,22,12,258,8,SS_CENTERIMAGE | NOT WS_GROUP - EDITTEXT IDC_MISSION_SAVE_DESC,22,154,258,14,NOT WS_BORDER | NOT - WS_TABSTOP - LTEXT "Description",-1,22,26,121,8,SS_CENTERIMAGE | NOT - WS_GROUP - CTEXT "Mission",-1,149,26,33,8,SS_CENTERIMAGE | NOT WS_VISIBLE | - WS_DISABLED | NOT WS_GROUP - CTEXT "Time Stamp",-1,199,26,66,8,SS_CENTERIMAGE | NOT - WS_GROUP -END - -IDD_MODEM_GUEST DIALOG DISCARDABLE 0, 0, 426, 238 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - EDITTEXT IDC_INPUT,15,218,268,12,ES_MULTILINE | ES_AUTOHSCROLL | - ES_WANTRETURN | NOT WS_BORDER | NOT WS_TABSTOP - LTEXT "Name:",-1,15,7,49,12,SS_CENTERIMAGE | NOT WS_GROUP - COMBOBOX IDC_MODEM_YOURSIDE,68,24,83,90,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_HASSTRINGS - COMBOBOX IDC_MODEM_YOURCOLOR,68,41,83,146,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_HASSTRINGS - LTEXT "Side:",-1,15,24,49,12,SS_CENTERIMAGE | NOT WS_GROUP - LTEXT "Color:",-1,15,41,49,12,SS_CENTERIMAGE | NOT WS_GROUP - CONTROL "Accept",1,"Button",BS_OWNERDRAW | WS_DISABLED,289,218, - 58,14 - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW,353,218,58,14 - LISTBOX IDC_PMESSAGES,15,130,268,84,NOT LBS_NOTIFY | - LBS_OWNERDRAWFIXED | LBS_HASSTRINGS | - LBS_NOINTEGRALHEIGHT | LBS_NOSEL | NOT WS_BORDER - LTEXT "None",IDC_SCENARIONAME,35,112,241,12,SS_CENTERIMAGE | - NOT WS_GROUP - LTEXT "Map:",-1,15,112,23,12,SS_CENTERIMAGE | NOT WS_GROUP - EDITTEXT IDC_NAME,68,7,83,12,ES_MULTILINE | ES_AUTOHSCROLL | - ES_WANTRETURN | NOT WS_BORDER | NOT WS_TABSTOP - LTEXT "Opponent:",-1,15,61,122,12,SS_CENTERIMAGE | NOT - WS_GROUP - LTEXT "????",IDC_MODEM_OPPONENT,15,78,125,12,SS_CENTERIMAGE | - NOT WS_GROUP - LTEXT "Unit Count:",-1,157,39,49,12,SS_CENTERIMAGE | - WS_DISABLED | NOT WS_GROUP - CONTROL "Slider1",IDC_MODEM_UNITCOUNT,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS | WS_DISABLED,210,39,83,12 - LTEXT "Tech Level:",-1,157,7,49,12,SS_CENTERIMAGE | - WS_DISABLED | NOT WS_GROUP - CONTROL "Slider2",IDC_TECHLEVEL,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS | WS_DISABLED,210,7,83,12 - LTEXT "Credits:",-1,157,23,49,12,SS_CENTERIMAGE | WS_DISABLED | - NOT WS_GROUP - CONTROL "Slider4",IDC_CREDITS,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS | WS_DISABLED,210,23,83,12 - CONTROL "Slider4",IDC_AIPLAYERS,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS | WS_DISABLED,210,55,83,12 - LTEXT "AI Players:",-1,157,55,49,12,SS_CENTERIMAGE | - WS_DISABLED | NOT WS_GROUP - GROUPBOX "",IDC_PREVIEW_FRAME,289,127,122,80 - CONTROL "Slider4",IDC_AILEVEL_SLIDER,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS | WS_DISABLED,210,71,83,12 - LTEXT "AI Level:",-1,157,71,49,12,SS_CENTERIMAGE | WS_DISABLED | - NOT WS_GROUP - CONTROL "Slider4",IDC_GAME_SPEED_SLIDER,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS | WS_DISABLED,210,87,83,12 - LTEXT "Game Speed",-1,157,87,49,12,SS_CENTERIMAGE | - WS_DISABLED | NOT WS_GROUP - CONTROL "Bases",IDC_MODEM_BASES,"Button",BS_AUTOCHECKBOX | - BS_FLAT | WS_DISABLED,301,19,110,12 - CONTROL "Crates",IDC_GOODIES,"Button",BS_AUTOCHECKBOX | BS_FLAT | - WS_DISABLED,301,31,110,12 - CONTROL "Fog Of War",IDC_MODEM_FOG,"Button",BS_AUTOCHECKBOX | - BS_FLAT | WS_DISABLED,301,55,110,12 - CONTROL "Bridges Destroyable",IDC_BRIDGE_DESTROY,"Button", - BS_AUTOCHECKBOX | BS_FLAT | WS_DISABLED,301,43,110,12 - CONTROL "Harvester Truce",IDC_HARVTRUCE,"Button",BS_AUTOCHECKBOX | - BS_FLAT | WS_DISABLED,301,67,110,12 - CONTROL "Re-Deployable MCV",IDC_MODEM_REDEPLOY_MCV,"Button", - BS_AUTOCHECKBOX | BS_FLAT | WS_DISABLED,301,7,110,12 - CONTROL "Short Game",IDC_SHORT_GAME,"Button",BS_AUTOCHECKBOX | - BS_FLAT | WS_DISABLED,301,79,110,12 - CONTROL "Multi Engineer",IDC_MULTI_ENGINEER,"Button", - BS_AUTOCHECKBOX | BS_FLAT | WS_DISABLED,301,91,110,12 -END - -IDD_MODEM_HOST DIALOG DISCARDABLE 0, 0, 426, 243 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - EDITTEXT IDC_INPUT,15,223,268,12,ES_MULTILINE | ES_AUTOHSCROLL | - ES_WANTRETURN | NOT WS_BORDER | NOT WS_TABSTOP - LTEXT "Name:",-1,15,7,49,12,SS_CENTERIMAGE | NOT WS_GROUP - COMBOBOX IDC_MODEM_YOURSIDE,68,24,83,90,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_HASSTRINGS - COMBOBOX IDC_MODEM_YOURCOLOR,68,41,83,146,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_HASSTRINGS - LTEXT "Side:",-1,15,24,49,12,SS_CENTERIMAGE | NOT WS_GROUP - LTEXT "Color:",-1,15,41,49,12,SS_CENTERIMAGE | NOT WS_GROUP - CONTROL "Go!",1,"Button",BS_OWNERDRAW | WS_DISABLED,289,223,58, - 14 - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW,353,223,58,14 - LISTBOX IDC_PMESSAGES,15,128,268,91,NOT LBS_NOTIFY | - LBS_OWNERDRAWFIXED | LBS_HASSTRINGS | - LBS_NOINTEGRALHEIGHT | LBS_NOSEL | NOT WS_BORDER - LTEXT "None",IDC_SCENARIONAME,36,110,242,12,SS_CENTERIMAGE | - NOT WS_GROUP - CONTROL "Multiplayer Map",IDC_MULTIMAP,"Button",BS_OWNERDRAW,299, - 204,100,14 - LTEXT "Map:",-1,15,110,32,12,SS_CENTERIMAGE | NOT WS_GROUP - EDITTEXT IDC_NAME,68,7,83,12,ES_MULTILINE | ES_WANTRETURN | NOT - WS_BORDER | NOT WS_TABSTOP - LTEXT "Opponent:",-1,15,63,122,12,SS_CENTERIMAGE | NOT - WS_GROUP - LTEXT "????",IDC_MODEM_OPPONENT,15,80,129,12,SS_CENTERIMAGE | - NOT WS_GROUP - CONTROL "Bases",IDC_MODEM_BASES,"Button",BS_AUTOCHECKBOX | - BS_FLAT,301,19,110,12 - CONTROL "Crates",IDC_GOODIES,"Button",BS_AUTOCHECKBOX | BS_FLAT, - 301,31,110,12 - CONTROL "Fog Of War",IDC_MODEM_FOG,"Button",BS_AUTOCHECKBOX | - BS_FLAT,301,55,110,12 - CONTROL "Bridges Destroyable",IDC_BRIDGE_DESTROY,"Button", - BS_AUTOCHECKBOX | BS_FLAT,301,43,110,12 - GROUPBOX "",IDC_PREVIEW_FRAME,289,123,122,75 - LTEXT "Unit Count:",-1,157,39,49,12,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Slider1",IDC_MODEM_UNITCOUNT,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,210,39,83,12 - LTEXT "Tech Level:",-1,157,7,49,12,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Slider2",IDC_TECHLEVEL,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS,210,7,83,12 - LTEXT "Credits:",-1,157,23,49,12,SS_CENTERIMAGE | NOT WS_GROUP - CONTROL "Slider4",IDC_CREDITS,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS,210,23,83,12 - CONTROL "Slider4",IDC_AIPLAYERS,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS,210,55,83,12 - LTEXT "AI Players:",-1,157,55,49,12,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Slider4",IDC_AILEVEL_SLIDER,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,210,71,83,12 - LTEXT "AI Level:",-1,157,71,49,12,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Harvester Truce",IDC_HARVTRUCE,"Button",BS_AUTOCHECKBOX | - BS_FLAT,301,67,110,12 - CONTROL "Re-Deployable MCV",IDC_MODEM_REDEPLOY_MCV,"Button", - BS_AUTOCHECKBOX | BS_FLAT,301,7,110,12 - CONTROL "Short Game",IDC_SHORT_GAME,"Button",BS_AUTOCHECKBOX | - BS_FLAT,301,79,110,12 - LTEXT "Game Speed",-1,157,88,49,12,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Slider4",IDC_GAME_SPEED_SLIDER,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,210,87,83,12 - CONTROL "Multi Engineer",IDC_MULTI_ENGINEER,"Button", - BS_AUTOCHECKBOX | BS_FLAT,301,91,94,12 -END - -IDD_MPLAYER_SELECT_GAME DIALOG DISCARDABLE 0, 0, 204, 144 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Main Menu",2,"Button",BS_OWNERDRAW | WS_TABSTOP,37,112, - 130,18 - CTEXT "Select Multiplayer Game",-1,37,12,130,12,SS_CENTERIMAGE - CONTROL "Internet",IDC_INTERNET,"Button",BS_OWNERDRAW | - WS_TABSTOP,37,28,130,18 - CONTROL "Modem / Serial",IDC_MODEMSERIAL,"Button",BS_OWNERDRAW | - WS_TABSTOP,37,49,130,18 - CONTROL "Network",IDC_NETWORK,"Button",BS_OWNERDRAW | WS_TABSTOP, - 37,70,130,18 - CONTROL "Skirmish",IDC_SKIRMISH,"Button",BS_OWNERDRAW | - WS_TABSTOP,37,91,130,18 -END - -IDD_MPLAYER_HOST DIALOGEX 0, 0, 426, 240 -STYLE WS_CHILD -FONT 8, "MS Sans Serif", 0, 0, 0x1 -BEGIN - EDITTEXT IDC_INPUT,18,199,256,12,ES_MULTILINE | ES_WANTRETURN | - NOT WS_BORDER - LTEXT "Players:",-1,18,41,57,12,SS_CENTERIMAGE | NOT WS_GROUP - CONTROL "Kick a user from your game",IDC_KICK,"Button", - BS_OWNERDRAW,18,215,20,18 - COMBOBOX IDC_YOURSIDE,73,6,76,90,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_HASSTRINGS - COMBOBOX IDC_YOURCOLOR,73,22,76,146,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_HASSTRINGS - LTEXT "Your Side:",-1,18,8,50,12,SS_CENTERIMAGE | NOT WS_GROUP - LTEXT "Your Color:",-1,18,23,52,12,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Go!",IDC_GO,"Button",BS_OWNERDRAW,354,219,54,14 - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW,291,219,53,14 - LISTBOX IDC_USERS,18,56,150,67,LBS_MULTIPLESEL | - LBS_OWNERDRAWFIXED | LBS_HASSTRINGS | - LBS_NOINTEGRALHEIGHT | NOT WS_BORDER - LISTBOX IDC_PMESSAGES,18,130,256,65,NOT LBS_NOTIFY | - LBS_OWNERDRAWFIXED | LBS_HASSTRINGS | - LBS_NOINTEGRALHEIGHT | LBS_NOSEL | NOT WS_BORDER - GROUPBOX "",IDC_PREVIEW_FRAME,281,138,126,73,0,0, - HIDC_PREVIEW_FRAME - LTEXT "None",IDC_SCENARIONAME,255,8,153,10 - CONTROL "Multiplayer Map",IDC_MULTIMAP,"Button",BS_OWNERDRAW,160, - 5,90,14 - CONTROL "Bases",IDC_BASES,"Button",BS_AUTOCHECKBOX,300,49,108,10 - CONTROL "Harvester Truce",IDC_HARVTRUCE,"Button",BS_AUTOCHECKBOX, - 300,37,108,10 - CONTROL "Allies allowed",IDC_ALLIES,"Button",BS_AUTOCHECKBOX,300, - 25,105,10 - RTEXT "AI Players:",-1,166,44,58,10 - CONTROL "Slider3",IDC_AIPLAYERS,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS,227,43,66,12 - CONTROL "Slider1",IDC_UNITCOUNT,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS,227,75,66,12 - RTEXT "Unit Count:",-1,166,76,58,10 - CONTROL "Slider2",IDC_TECHLEVEL,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS,227,91,66,12 - CONTROL "Slider3",IDC_CREDITS,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS,227,107,66,12 - RTEXT "Tech Level:",-1,166,92,58,10 - RTEXT "Credits:",-1,166,108,58,10 - CONTROL "Fog Of War",IDC_FOG_OF_WAR,"Button",BS_AUTOCHECKBOX,300, - 73,108,10 - CONTROL "Bridges Destroyable",IDC_BRIDGE_DESTROY,"Button", - BS_AUTOCHECKBOX,300,85,108,10 - CONTROL "Crates",IDC_CRATES,"Button",BS_AUTOCHECKBOX,300,97,108, - 10 - RTEXT "AI Level:",-1,166,60,58,10 - CONTROL "Slider3",IDC_AILEVEL_SLIDER,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,227,59,66,12 - CONTROL "Re-Deployable MCV",IDC_REDEPLOY_MCV,"Button", - BS_AUTOCHECKBOX | WS_TABSTOP,300,61,108,10 - CONTROL "Short Game",IDC_SHORT_GAME,"Button",BS_AUTOCHECKBOX,300, - 109,108,10 - CONTROL "Slider1",IDC_GAME_SPEED_SLIDER,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,227,27,66,12 - RTEXT "Game Speed:",-1,166,28,58,10 - CONTROL "Multi Engineer",IDC_MULTI_ENGINEER,"Button", - BS_AUTOCHECKBOX,300,121,108,10 -END - -IDD_MPLAYER_GUEST DIALOGEX 0, 0, 426, 240 -STYLE WS_CHILD -FONT 8, "MS Sans Serif", 0, 0, 0x1 -BEGIN - LTEXT "Map:",-1,149,7,26,10,SS_CENTERIMAGE | NOT WS_GROUP - LTEXT "Players:",-1,18,42,50,12,SS_CENTERIMAGE | NOT WS_GROUP - COMBOBOX IDC_YOURSIDE,66,7,76,101,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_HASSTRINGS - COMBOBOX IDC_YOURCOLOR,66,24,76,145,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_HASSTRINGS - LTEXT "Your Side:",-1,18,7,50,12,SS_CENTERIMAGE | NOT WS_GROUP - LTEXT "Your Color:",-1,18,24,50,12,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Accept",IDC_ACCEPT,"Button",BS_OWNERDRAW,350,215,58,18 - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW,279,215,58,18 - LTEXT "None",IDC_SCENARIONAME,176,7,232,10,SS_CENTERIMAGE | - NOT WS_GROUP - LISTBOX IDC_USERS,18,56,150,68,LBS_MULTIPLESEL | - LBS_OWNERDRAWFIXED | LBS_HASSTRINGS | - LBS_NOINTEGRALHEIGHT | NOT WS_BORDER - CONTROL "Bases",IDC_BASES,"Button",BS_AUTOCHECKBOX | WS_DISABLED, - 300,49,108,10 - CONTROL "Harvester Truce",IDC_HARVTRUCE,"Button",BS_AUTOCHECKBOX | - WS_DISABLED,300,37,108,10 - CONTROL "Allies allowed",IDC_ALLIES,"Button",BS_AUTOCHECKBOX | - WS_DISABLED,300,25,105,10 - RTEXT "AI Players",-1,162,44,57,10,WS_DISABLED - CONTROL "Slider3",IDC_AIPLAYERS,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS | WS_DISABLED,227,43,66,12 - CONTROL "Slider1",IDC_UNITCOUNT,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS | WS_DISABLED,227,75,66,12 - RTEXT "Unit Count",-1,162,76,57,10,WS_DISABLED - CONTROL "Slider2",IDC_TECHLEVEL,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS | WS_DISABLED,227,91,66,12 - CONTROL "Slider3",IDC_CREDITS,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS | WS_DISABLED,227,107,66,12 - RTEXT "Tech Level",-1,162,92,57,10,WS_DISABLED - RTEXT "Credits",-1,162,108,57,10,WS_DISABLED - CONTROL "Fog Of War",IDC_FOG_OF_WAR,"Button",BS_AUTOCHECKBOX | - WS_DISABLED,300,73,108,10 - CONTROL "Bridges Destroyable",IDC_BRIDGE_DESTROY,"Button", - BS_AUTOCHECKBOX | WS_DISABLED,300,85,108,10 - CONTROL "Crates",IDC_CRATES,"Button",BS_AUTOCHECKBOX | - WS_DISABLED,300,97,108,10 - RTEXT "AI Level",-1,162,60,57,10,WS_DISABLED - CONTROL "Slider3",IDC_AILEVEL_SLIDER,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS | WS_DISABLED,227,59,66,12 - CONTROL "Re-Deployable MCV",IDC_REDEPLOY_MCV,"Button", - BS_AUTOCHECKBOX | WS_DISABLED | WS_TABSTOP,300,61,108,10 - EDITTEXT IDC_INPUT,18,199,256,12,ES_MULTILINE | ES_WANTRETURN | - NOT WS_BORDER - LISTBOX IDC_PMESSAGES,18,130,256,65,NOT LBS_NOTIFY | - LBS_OWNERDRAWFIXED | LBS_HASSTRINGS | - LBS_NOINTEGRALHEIGHT | LBS_NOSEL | NOT WS_BORDER - GROUPBOX "",IDC_PREVIEW_FRAME,281,138,126,73,0,0, - HIDC_PREVIEW_FRAME - CONTROL "Short Game",IDC_SHORT_GAME,"Button",BS_AUTOCHECKBOX | - WS_DISABLED,300,109,84,10 - CONTROL "Slider3",IDC_GAME_SPEED_SLIDER,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS | WS_DISABLED,227,27,66,12 - RTEXT "Game Speed",-1,162,28,57,10,WS_DISABLED - CONTROL "Multi Engineer",IDC_MULTI_ENGINEER,"Button", - BS_AUTOCHECKBOX | WS_DISABLED,300,121,108,10 -END - -IDD_MPLAYER_GAME_LIST DIALOG DISCARDABLE 0, 0, 426, 240 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Join",IDC_GAMELIST_JOIN,"Button",BS_OWNERDRAW,264,214, - 62,18 - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW,181,214,62,18 - CONTROL "New",IDC_GAMELIST_NEW,"Button",BS_OWNERDRAW,345,214,62, - 18 - LISTBOX IDC_GAMELIST,294,27,113,68,LBS_OWNERDRAWFIXED | - LBS_HASSTRINGS | LBS_NOINTEGRALHEIGHT | NOT WS_BORDER - LISTBOX IDC_USERS,294,111,113,100,LBS_OWNERDRAWFIXED | - LBS_HASSTRINGS | LBS_NOINTEGRALHEIGHT | NOT WS_BORDER - LISTBOX IDC_PMESSAGES,19,27,266,166,LBS_OWNERDRAWFIXED | - LBS_HASSTRINGS | LBS_NOINTEGRALHEIGHT | LBS_NOSEL | NOT - WS_BORDER - EDITTEXT IDC_INPUT,19,198,266,12,ES_MULTILINE | ES_WANTRETURN | - NOT WS_BORDER - EDITTEXT IDC_YOURNAME,89,8,68,12,NOT WS_BORDER - LTEXT "Games:",-1,296,14,50,10 - LTEXT "Players:",-1,296,99,50,10 - LTEXT "Your Name:",-1,24,9,62,10 -END - -IDD_OPT_CTRL_SP DIALOG DISCARDABLE 0, 0, 209, 140 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Resume Mission",IDC_RESUME_MISSION,"Button", - BS_OWNERDRAW,47,114,115,14 - CONTROL "Restate Briefing",IDC_BRIEFING,"Button",BS_OWNERDRAW,47, - 29,115,14 - CONTROL "Load Game",IDC_LOAD_GAME,"Button",BS_OWNERDRAW,47,46, - 115,14 - CONTROL "Save Game",IDC_SAVE_GAME,"Button",BS_OWNERDRAW,47,63, - 115,14 - CONTROL "Delete Game",IDC_DELETE_GAME,"Button",BS_OWNERDRAW,47, - 80,115,14 - CONTROL "Game Controls",IDC_GAME_CONTROLS,"Button",BS_OWNERDRAW, - 47,12,115,14 - CONTROL "Abort Mission",IDC_ABORT_MISSION,"Button",BS_OWNERDRAW, - 47,97,115,14 -END - -IDD_OPT_CTRL_MP DIALOG DISCARDABLE 0, 0, 209, 75 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Resume Mission",IDC_RESUME_MISSION,"Button", - BS_OWNERDRAW,55,48,99,14 - CONTROL "Game Controls",IDC_GAME_CONTROLS,"Button",BS_OWNERDRAW, - 55,12,99,14 - CONTROL "Abort Mission",IDC_ABORT_MISSION,"Button",BS_OWNERDRAW, - 55,30,99,14 -END - -IDD_SERIAL_PHONE_LIST DIALOG DISCARDABLE 0, 0, 344, 167 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Dial",1,"Button",BS_OWNERDRAW,209,141,50,14 - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW,272,141,50,14 - CTEXT "Phone List",-1,22,12,300,11,SS_CENTERIMAGE | NOT - WS_GROUP - LISTBOX IDC_PHONE_LIST,96,28,226,84,LBS_OWNERDRAWFIXED | - LBS_HASSTRINGS | LBS_NOINTEGRALHEIGHT | NOT WS_BORDER - EDITTEXT IDC_PHONE_NAME,96,120,226,14,ES_MULTILINE | - ES_AUTOHSCROLL | ES_WANTRETURN | NOT WS_BORDER | NOT - WS_TABSTOP - CONTROL "Add",IDC_SERIAL_ADD,"Button",BS_OWNERDRAW,22,28,69,14 - CONTROL "Delete",IDC_PHONE_DELETE,"Button",BS_OWNERDRAW,22,70,69, - 14 - CONTROL "Edit",IDC_PHONE_EDIT,"Button",BS_OWNERDRAW,22,49,69,14 - LTEXT "Phone Number:",-1,22,120,67,14,SS_CENTERIMAGE | NOT - WS_GROUP -END - -IDD_SERIAL_PHONE_ENTRY DIALOG DISCARDABLE 0, 0, 247, 136 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Save",1,"Button",BS_OWNERDRAW,117,110,50,14 - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW,175,110,50,14 - CTEXT "Phonebook Entry",-1,22,12,203,12,SS_CENTERIMAGE | NOT - WS_GROUP - EDITTEXT IDC_PHONE_NAME,76,35,149,14,ES_MULTILINE | - ES_AUTOHSCROLL | ES_WANTRETURN | NOT WS_BORDER | NOT - WS_TABSTOP - RTEXT "Name:",-1,22,35,47,14,SS_CENTERIMAGE | NOT WS_GROUP - RTEXT "Number:",-1,22,56,47,14,SS_CENTERIMAGE | NOT WS_GROUP - EDITTEXT IDC_PHONE_NUMBER,76,56,149,14,ES_MULTILINE | - ES_AUTOHSCROLL | ES_WANTRETURN | NOT WS_BORDER | NOT - WS_TABSTOP - CONTROL "Default",IDC_PHONE_DEFAULT,"Button",BS_AUTOCHECKBOX | - BS_FLAT,28,84,95,12 - CONTROL "Custom",IDC_PHONE_CUSTOM,"Button",BS_AUTOCHECKBOX | - BS_FLAT,123,84,96,12 - GROUPBOX "Settings:",-1,22,73,203,30 -END - -IDD_PROGRESS_WAIT DIALOGEX 0, 0, 192, 53 -STYLE WS_CHILD -FONT 8, "MS Sans Serif", 0, 0, 0x1 -BEGIN - GROUPBOX "",IDC_PROGRESS_BAR_FRAME,46,26,100,15,0,0, - HIDC_PROGRESS_BAR_FRAME - CTEXT "Working - Please Wait",IDC_PROGRESS_TEXT,22,12,148,11, - SS_CENTERIMAGE | NOT WS_GROUP -END - -IDD_MPLAYER_DISCONNECT DIALOG DISCARDABLE 0, 0, 339, 220 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW | WS_TABSTOP,22, - 194,295,14 - LISTBOX IDC_DISCONNECT_MESSAGES,22,103,295,84,NOT LBS_NOTIFY | - LBS_OWNERDRAWFIXED | LBS_HASSTRINGS | - LBS_NOINTEGRALHEIGHT | LBS_NOSEL | NOT WS_BORDER - CONTROL "Player 1",IDC_DISCONNECT_PLAYER1,"Button",BS_OWNERDRAW | - WS_TABSTOP,22,12,90,14 - CONTROL "Player 3",IDC_DISCONNECT_PLAYER3,"Button",BS_OWNERDRAW | - WS_TABSTOP,22,48,90,14 - CONTROL "Player 2",IDC_DISCONNECT_PLAYER2,"Button",BS_OWNERDRAW | - WS_TABSTOP,22,30,90,14 - CONTROL "Player 4",IDC_DISCONNECT_PLAYER4,"Button",BS_OWNERDRAW | - WS_TABSTOP,22,66,90,14 - CONTROL "Player 5",IDC_DISCONNECT_PLAYER5,"Button",BS_OWNERDRAW | - WS_TABSTOP,175,12,90,14 - CONTROL "Player 6",IDC_DISCONNECT_PLAYER6,"Button",BS_OWNERDRAW | - WS_TABSTOP,175,30,90,14 - CONTROL "Player 7",IDC_DISCONNECT_PLAYER7,"Button",BS_OWNERDRAW | - WS_TABSTOP,175,48,90,14 - CONTROL "Player 8",IDC_DISCONNECT_PLAYER8,"Button",BS_OWNERDRAW | - WS_TABSTOP,175,66,90,14 - GROUPBOX "",IDC_DISCONNECT_PLAYER1_BOX,118,12,40,10 - GROUPBOX "",IDC_DISCONNECT_PLAYER2_BOX,118,30,40,10 - GROUPBOX "",IDC_DISCONNECT_PLAYER3_BOX,118,48,40,10 - GROUPBOX "",IDC_DISCONNECT_PLAYER4_BOX,118,66,40,10 - GROUPBOX "",IDC_DISCONNECT_PLAYER5_BOX,271,12,40,10 - GROUPBOX "",IDC_DISCONNECT_PLAYER6_BOX,271,30,40,10 - GROUPBOX "",IDC_DISCONNECT_PLAYER7_BOX,271,48,40,10 - GROUPBOX "",IDC_DISCONNECT_PLAYER8_BOX,271,66,40,10 - LTEXT "Time Remaining:",IDC_DISCONNECT_TIME_REMAINING,29,88, - 279,8,NOT WS_GROUP -END - -IDD_DESYNC_HOST DIALOG DISCARDABLE 0, 0, 360, 264 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CTEXT "Synchronization Error",IDC_DESYNC_HEADER,30,10,280,10, - NOT WS_GROUP - LTEXT "Players:",-1,30,23,100,10,NOT WS_GROUP - LISTBOX IDC_DESYNC_PLAYER_LIST,30,35,120,105,NOT LBS_NOTIFY | - LBS_OWNERDRAWFIXED | LBS_HASSTRINGS | - LBS_NOINTEGRALHEIGHT | LBS_NOSEL | NOT WS_BORDER - LTEXT "The game has gone out of sync.\r\n\r\nPress ""Load Game"" to load a saved game from this session, re-syncing the game for all players.\r\n\r\nPress ""Continue"" to continue playing without the desynced players. They will continue in a separate game session.", - -1,160,25,180,82,NOT WS_GROUP - LTEXT "Press ""Quit"" to exit the game.",-1,160,127,180,22, - NOT WS_GROUP - LISTBOX IDC_DESYNC_CHAT_LIST,30,145,300,60,NOT LBS_NOTIFY | - LBS_OWNERDRAWFIXED | LBS_HASSTRINGS | - LBS_NOINTEGRALHEIGHT | LBS_NOSEL | NOT WS_BORDER - EDITTEXT IDC_DESYNC_CHAT_EDIT,30,207,299,12,ES_AUTOHSCROLL | - NOT WS_BORDER - LTEXT "Loading in 5 seconds...", - IDC_DESYNC_COUNTDOWN_TEXT,30,223,140,10,NOT WS_GROUP | - NOT WS_VISIBLE - GROUPBOX "",IDC_DESYNC_COUNTDOWN_BAR,180,222,149,12,NOT WS_VISIBLE - CONTROL "Load Game",IDC_DESYNC_LOAD,"Button",BS_OWNERDRAW | - WS_TABSTOP,30,239,70,12 - CONTROL "Continue",IDC_DESYNC_CONTINUE,"Button",BS_OWNERDRAW | - WS_TABSTOP,145,239,70,12 - CONTROL "Quit",IDC_DESYNC_QUIT,"Button",BS_OWNERDRAW | - WS_TABSTOP,260,239,70,12 -END - -IDD_DESYNC_WAIT DIALOG DISCARDABLE 0, 0, 360, 264 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CTEXT "Synchronization Error",IDC_DESYNC_HEADER,30,10,280,10, - NOT WS_GROUP - LTEXT "Players:",-1,30,23,100,10,NOT WS_GROUP - LISTBOX IDC_DESYNC_PLAYER_LIST,30,35,120,105,NOT LBS_NOTIFY | - LBS_OWNERDRAWFIXED | LBS_HASSTRINGS | - LBS_NOINTEGRALHEIGHT | LBS_NOSEL | NOT WS_BORDER - LTEXT "The game has gone out of sync.\r\n\r\nIf there are saves available from this session, the game host can attempt to load a save to re-sync the game.\r\n\r\nAlternatively, the host can choose for the desynced players to continue playing in separate game sessions.", - -1,160,25,180,82,NOT WS_GROUP - LTEXT "Please wait while the host is making a decision.",-1,160, - 127,180,22, - NOT WS_GROUP - LISTBOX IDC_DESYNC_CHAT_LIST,30,145,300,60,NOT LBS_NOTIFY | - LBS_OWNERDRAWFIXED | LBS_HASSTRINGS | - LBS_NOINTEGRALHEIGHT | LBS_NOSEL | NOT WS_BORDER - EDITTEXT IDC_DESYNC_CHAT_EDIT,30,207,299,12,ES_AUTOHSCROLL | - NOT WS_BORDER - LTEXT "Loading in 5 seconds...", - IDC_DESYNC_COUNTDOWN_TEXT,30,223,140,10,NOT WS_GROUP | - NOT WS_VISIBLE - GROUPBOX "",IDC_DESYNC_COUNTDOWN_BAR,180,222,149,12,NOT WS_VISIBLE - CONTROL "Quit",IDC_DESYNC_QUIT,"Button",BS_OWNERDRAW | - WS_TABSTOP | WS_DISABLED,145,239,70,12 -END - -IDD_SELECT_SERIAL DIALOG DISCARDABLE 0, 0, 191, 147 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW,37,115,117,18 - CONTROL "Dial",IDC_SERIAL_DIAL,"Button",BS_OWNERDRAW,37,27,117, - 18 - CONTROL "Answer",IDC_SERIAL_ANSWER,"Button",BS_OWNERDRAW,37,49, - 117,18 - CONTROL "Null Modem",IDC_SERIAL_NULLMODEM,"Button",BS_OWNERDRAW, - 37,71,117,18 - CONTROL "Settings",IDC_SERIAL_SETTINGS_BTN,"Button",BS_OWNERDRAW, - 37,93,117,18 - CTEXT "Modem / Serial",-1,37,12,117,8,SS_CENTERIMAGE | NOT - WS_GROUP -END - -IDD_SERIAL_SETTINGS DIALOG DISCARDABLE 0, 0, 361, 225 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW,289,199,50,14 - CTEXT "Serial Settings",-1,22,12,317,11,SS_CENTERIMAGE | NOT - WS_GROUP - COMBOBOX IDC_SERIAL_PORT,22,38,144,30,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_HASSTRINGS - LTEXT "Port:",-1,22,25,144,12,SS_CENTERIMAGE | NOT WS_GROUP - COMBOBOX IDC_SERIAL_BAUD,22,70,144,30,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_HASSTRINGS - LTEXT "Baud:",-1,22,54,63,12,SS_CENTERIMAGE | NOT WS_GROUP - COMBOBOX IDC_SERIAL_CALLWAITING,180,38,144,30,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_HASSTRINGS - EDITTEXT IDC_SERIAL_CALLWAITING_EDIT,180,38,129,12,ES_MULTILINE | - ES_WANTRETURN | NOT WS_VISIBLE | NOT WS_BORDER | NOT - WS_TABSTOP - CONTROL "Pulse Dial",IDC_SERIAL_PULSE,"Button",BS_AUTOCHECKBOX | - BS_NOTIFY | BS_FLAT,180,55,68,10 - CONTROL "Tone Dial",IDC_SERIAL_TONE,"Button",BS_AUTOCHECKBOX | - BS_NOTIFY | BS_FLAT,180,69,69,10 - CONTROL "Save",1,"Button",BS_OWNERDRAW,226,199,50,14 - EDITTEXT IDC_SERIAL_INITSTRING,88,122,245,12,ES_MULTILINE | - ES_WANTRETURN | NOT WS_BORDER | NOT WS_TABSTOP - LISTBOX IDC_SERIAL_INITLIST,88,138,245,52,LBS_OWNERDRAWFIXED | - LBS_HASSTRINGS | LBS_NOINTEGRALHEIGHT | NOT WS_BORDER - EDITTEXT IDC_SERIAL_PORT_EDIT,22,39,129,12,ES_MULTILINE | - ES_WANTRETURN | NOT WS_VISIBLE | NOT WS_BORDER | NOT - WS_TABSTOP - LTEXT "Call Waiting:",-1,180,25,144,12,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Delete",IDC_SERIAL_INIT_DELETE,"Button",BS_OWNERDRAW,28, - 142,55,14 - CONTROL "Add",IDC_SERIAL_ADD,"Button",BS_OWNERDRAW,28,122,55,14 - CONTROL "Error Correction",IDC_SERIAL_ERROR_CORRECTION,"Button", - BS_AUTOCHECKBOX | BS_NOTIFY | BS_FLAT,180,83,144,10 - CONTROL "Data Compression",IDC_SERIAL_DATA_COMPRESSION,"Button", - BS_AUTOCHECKBOX | BS_NOTIFY | BS_FLAT,180,97,145,10 - GROUPBOX "Init String",-1,22,108,317,86 -END - -IDD_SKIRMISH DIALOG DISCARDABLE 0, 0, 426, 240 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - LTEXT "Name:",-1,22,12,78,8,NOT WS_GROUP - EDITTEXT IDC_SKIRMISH_NAME,22,24,78,12,NOT WS_BORDER | NOT - WS_TABSTOP - LTEXT "Side:",-1,22,42,78,8,NOT WS_GROUP - COMBOBOX IDC_SKIRMISH_SIDE,22,52,78,74,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_HASSTRINGS - LTEXT "Color:",-1,22,71,78,8,NOT WS_GROUP - COMBOBOX IDC_SKIRMISH_COLOR,22,82,78,73,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_HASSTRINGS | WS_VSCROLL - LTEXT "Unit Count:",IDC_SKIRMISH_UNITCOUNT_LABEL,298,18,100,8, - NOT WS_GROUP - CONTROL "Slider1",IDC_SKIRMISH_UNITCOUNT,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,298,30,100,14 - LTEXT "Credits:",IDC_SKIRMISH_CREDITS_LABEL,298,47,100,8,NOT - WS_GROUP - CONTROL "Slider3",IDC_SKIRMISH_CREDITS,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,298,59,100,14 - LTEXT "Tech Level:",IDC_SKIRMISH_TECHLEVEL_LABEL,298,76,100,8, - NOT WS_GROUP - CONTROL "Slider2",IDC_SKIRMISH_TECHLEVEL,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,298,88,100,14 - LTEXT "AI Level:",IDC_SKIRMISH_AILEVEL_LABEL,298,105,100,8,NOT - WS_GROUP - CONTROL "Slider4",IDC_DIFFICULTY_SLIDER,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,298,117,100,14 - CONTROL "OK",IDOK,"Button",BS_OWNERDRAW,294,214,50,14 - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW,354,214,50,14 - CONTROL "Slider4",IDC_SKIRMISH_AIPLAYERS,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,298,146,100,14 - LTEXT "AI Players:",IDC_SKIRMISH_AIPLAYERS_LABEL,298,134,100,8, - NOT WS_GROUP - CONTROL "Multiplay Map",IDC_MULTIMAP,"Button",BS_OWNERDRAW,294, - 197,110,14 - GROUPBOX "",IDC_PREVIEW_FRAME,22,122,215,106 - LTEXT "Map:",-1,22,97,36,10,NOT WS_GROUP - LTEXT "None",IDC_SCENARIONAME,22,111,268,10,NOT WS_GROUP - CTEXT "Preview",-1,22,165,215,13,SS_CENTERIMAGE | NOT WS_GROUP - GROUPBOX "",-1,294,12,110,181 - CONTROL "Bases",IDC_SKIRMISH_BASES,"Button",BS_AUTOCHECKBOX,123, - 21,157,10 - CONTROL "Crates",IDC_SKIRMISH_CRATES,"Button",BS_AUTOCHECKBOX, - 123,33,157,10 - CONTROL "Fog Of War",IDC_SKIRMISH_FOG,"Button",BS_AUTOCHECKBOX, - 123,45,157,10 - CONTROL "Bridges Destroyable",IDC_SKIRMISH_BRIDGES,"Button", - BS_AUTOCHECKBOX,123,57,157,10 - GROUPBOX "",-1,117,12,167,96 - CONTROL "Re-Deployable MCV",IDC_REDEPLOY_MCV,"Button", - BS_AUTOCHECKBOX,123,69,157,10 - CONTROL "Short Game",IDC_SHORT_GAME,"Button",BS_AUTOCHECKBOX,123, - 81,157,10 - CONTROL "Slider4",IDC_GAME_SPEED_SLIDER,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,298,175,100,14 - LTEXT "Game Speed",IDC_SKIRMISH_GAMESPEED_LABEL,298,163,100,8, - NOT WS_GROUP - CONTROL "Multi Engineer",IDC_MULTI_ENGINEER,"Button", - BS_AUTOCHECKBOX,123,93,108,10 -END - -IDD_SOUND_OPTIONS_DIALOG DIALOG DISCARDABLE 0, 0, 294, 215 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "OK",IDOK,"Button",BS_OWNERDRAW | WS_TABSTOP,210,189,62, - 14 - CONTROL "Slider1",IDC_MUSIC_VOLUME,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS | WS_TABSTOP,97,12,175,15 - CONTROL "Slider2",IDC_SOUND_VOLUME,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS | WS_TABSTOP,97,34,175,15 - RTEXT "Music Volume:",-1,22,12,70,15,SS_CENTERIMAGE | NOT - WS_GROUP - RTEXT "Sound Volume:",-1,22,34,70,15,SS_CENTERIMAGE - LISTBOX IDC_SOUND_TRACKLIST,97,82,175,99,LBS_OWNERDRAWFIXED | - LBS_HASSTRINGS | LBS_NOINTEGRALHEIGHT | NOT WS_BORDER | - WS_TABSTOP - CONTROL "Play",IDC_SOUND_PLAY,"Button",BS_OWNERDRAW | WS_TABSTOP, - 22,86,70,14 - CONTROL "Stop",IDC_SOUND_STOP,"Button",BS_OWNERDRAW | WS_TABSTOP, - 22,113,70,14 - RTEXT "Voice Volume:",-1,22,56,70,15,SS_CENTERIMAGE - CONTROL "Slider2",IDC_VOICE_VOLUME,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS | WS_TABSTOP,97,56,175,15 - CONTROL "Shuffle",IDC_SOUND_SHUFFLE,"Button",BS_AUTOCHECKBOX | - BS_FLAT,22,140,70,14 - CONTROL "Repeat",IDC_SOUND_REPEAT,"Button",BS_AUTOCHECKBOX | - BS_FLAT,22,167,70,14 -END - -IDD_SOUND_OPTIONS_DIALOG_LITE DIALOG DISCARDABLE 0, 0, 294, 112 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - RTEXT "Music Volume:",-1,22,17,70,15,SS_CENTERIMAGE - CONTROL "Slider1",IDC_MUSIC_VOLUME,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS | WS_TABSTOP,99,17,173,15 - RTEXT "Sound Volume:",-1,22,40,71,15,SS_CENTERIMAGE - CONTROL "Slider2",IDC_SOUND_VOLUME,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS | WS_TABSTOP,99,40,173,15 - CONTROL "OK",IDOK,"Button",BS_OWNERDRAW | BS_CENTER | WS_TABSTOP, - 115,86,62,14 - RTEXT "Voice Volume:",-1,22,62,71,15,SS_CENTERIMAGE - CONTROL "Slider2",IDC_VOICE_VOLUME,"msctls_trackbar32",TBS_BOTH | - TBS_NOTICKS | WS_TABSTOP,99,62,173,15 -END - -IDD_VERSION DIALOG DISCARDABLE 0, 0, 272, 106 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "OK",IDOK,"Button",BS_OWNERDRAW | BS_CENTER | WS_TABSTOP, - 111,80,50,14 - LISTBOX IDC_VERSION_INFO,22,12,228,55,LBS_OWNERDRAWFIXED | - LBS_HASSTRINGS | LBS_NOINTEGRALHEIGHT | LBS_NOSEL | NOT - WS_BORDER | WS_TABSTOP -END - -IDD_MSGBOX_3_SMALL DIALOG DISCARDABLE 0, 0, 290, 90 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "OK",IDOK,"Button",BS_OWNERDRAW | NOT WS_VISIBLE,120,64, - 50,14 - CTEXT "Msg",IDC_MSGBOX_TEXT,22,12,246,40 - CONTROL "No",7,"Button",BS_OWNERDRAW | NOT WS_VISIBLE,70,64,50, - 14 - CONTROL "Yes",6,"Button",BS_OWNERDRAW | NOT WS_VISIBLE,169,64,50, - 14 -END - -IDD_CAMPAIGN DIALOG DISCARDABLE 0, 0, 246, 149 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CTEXT "Select Campaign:",-1,22,12,206,10,SS_CENTERIMAGE | NOT - WS_GROUP - LISTBOX IDC_LIST,22,28,206,51,LBS_OWNERDRAWFIXED | - LBS_HASSTRINGS | LBS_NOINTEGRALHEIGHT | NOT WS_BORDER - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW,178,121,50,16 - CONTROL "OK",IDOK,"Button",BS_OWNERDRAW,121,121,50,16 - CONTROL "Slider3",IDC_DIFFICULTY_SLIDER,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,22,98,206,15 - CONTROL "Difficulty",-1,"Static",SS_LEFTNOWORDWRAP | - SS_CENTERIMAGE,22,84,136,15 - RTEXT "Harder",IDC_DIFFICULTY_LABEL,162,84,66,15, - SS_CENTERIMAGE | NOT WS_GROUP -END - -IDD_EXCEPTION DIALOG DISCARDABLE 0, 0, 294, 231 -STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION -CAPTION "Tiberian Sun Has Encountered Difficulty" -FONT 8, "MS Sans Serif" -BEGIN - PUSHBUTTON "Debug",IDC_EXCEPT_DEBUG,115,200,60,14,BS_CENTER - PUSHBUTTON "Main Menu",IDC_EXCEPT_MAINMENU,217,200,60,14 - CTEXT "Tiberian Sun has encountered a problem.\nSee the file DEBUG.TXT for details.", - -1,7,7,280,26,NOT WS_GROUP - DEFPUSHBUTTON "Quit",IDC_EXCEPT_QUIT,19,200,60,14 - EDITTEXT IDC_EXCEPT_DETAILS,17,53,261,140,ES_MULTILINE | - ES_NOHIDESEL | ES_READONLY | ES_WANTRETURN | WS_VSCROLL | - WS_HSCROLL - LTEXT "Details:",IDC_EXCEPT_DETAILS_LABEL,19,41,45,8 -END - -IDD_EXCEPTION_SIMPLE DIALOG DISCARDABLE 0, 0, 252, 82 -STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION -CAPTION "Tiberian Sun" -FONT 8, "MS Sans Serif" -BEGIN - CTEXT "Tiberian Sun has encountered an internal error",-1,7,11, - 238,8,NOT WS_GROUP - CTEXT "and is unable to continue normally.", - IDC_EXCEPT_DETAILS_LABEL,7,19,238,8,NOT WS_GROUP - CTEXT "Please visit our website at http://www.westwood.com", - IDC_EXCEPT_WEBSITE,7,36,238,8,NOT WS_GROUP - PUSHBUTTON "OK",IDC_EXCEPT_QUIT,83,61,79,14 - CTEXT "for the latest updates and technical support.",-1,7,44, - 238,8,NOT WS_GROUP -END - -IDD_MPLAYER_SELECT_GAME_FS DIALOG DISCARDABLE 0, 0, 197, 146 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CTEXT "Select Multiplayer Game",-1,30,8,130,12,SS_CENTERIMAGE | - NOT WS_GROUP - CONTROL "Main Menu",2,"Button",BS_OWNERDRAW,30,118,130,18 - CONTROL "Internet",IDC_INTERNET,"Button",BS_OWNERDRAW,30,22,130, - 18 - CONTROL "Modem / Serial",IDC_MODEMSERIAL,"Button",BS_OWNERDRAW, - 30,60,130,18 - CONTROL "Network",IDC_NETWORK,"Button",BS_OWNERDRAW,30,79,130,18 - CONTROL "Skirmish",IDC_SKIRMISH,"Button",BS_OWNERDRAW,30,98,130, - 18 - CONTROL "World Domination! (Internet)",IDC_WORLDDOM,"Button", - BS_OWNERDRAW,30,41,130,18 -END - -IDD_SELECT_GAME_TYPE DIALOG DISCARDABLE 0, 0, 228, 108 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Main Menu",2,"Button",BS_OWNERDRAW,62,82,104,14 - CTEXT "Select Game Type",-1,22,12,184,19,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Tiberian Sun (Original)",IDC_GAMETYPE_ORIGINAL,"Button", - BS_OWNERDRAW,62,42,104,14 - CONTROL "Firestorm",IDC_GAMETYPE_FIRESTORM,"Button",BS_OWNERDRAW, - 62,62,104,14 -END - -IDD_MAPGEN_FS DIALOG DISCARDABLE 0, 0, 426, 242 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "OK",IDOK,"Button",BS_OWNERDRAW,265,220,65,14 - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW,337,220,65,14 - COMBOBOX IDC_MAPGEN_ENVIRONMENT,79,8,100,103,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_SORT | CBS_HASSTRINGS - CONTROL "Slider1",IDC_MAPGEN_HILLS,"msctls_trackbar32",TBS_TOP, - 95,92,100,14 - LTEXT "Hills:",-1,22,92,74,14,SS_CENTERIMAGE | NOT WS_GROUP - CONTROL "Slider1",IDC_MAPGEN_WATER,"msctls_trackbar32",TBS_TOP, - 95,143,100,14 - LTEXT "Players:",-1,22,41,74,14,SS_CENTERIMAGE | NOT WS_GROUP - LTEXT "Map Width:",-1,187,8,53,12,SS_CENTERIMAGE | NOT - WS_GROUP - LTEXT "Map Height:",-1,186,25,53,12,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Slider1",IDC_MAPGEN_VEGETATION,"msctls_trackbar32", - TBS_TOP,95,160,100,14 - CONTROL "Slider1",IDC_MAPGEN_CITIES,"msctls_trackbar32",TBS_TOP, - 95,177,100,14 - CONTROL "Slider1",IDC_MAPGEN_TIBERIUM_FIELDS,"msctls_trackbar32", - TBS_TOP,95,126,100,14 - LTEXT "Tiberium Fields:",-1,22,126,74,14,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Slider1",IDC_MAPGEN_ACCESSIBILITY,"msctls_trackbar32", - TBS_TOP,95,75,100,14 - LTEXT "Environment:",-1,22,7,53,14,SS_CENTERIMAGE | NOT - WS_GROUP - EDITTEXT IDC_MAPGEN_DIMENSION_EDIT,22,206,57,12,ES_NUMBER | NOT - WS_VISIBLE | WS_DISABLED | NOT WS_BORDER | NOT - WS_TABSTOP - CONTROL "Load Map",IDC_MAPGEN_LOAD_MAP,"Button",BS_OWNERDRAW,100, - 220,65,14 - CONTROL "Save Map",IDC_MAPGEN_SAVE_MAP,"Button",BS_OWNERDRAW,22, - 220,65,14 - COMBOBOX IDC_MAPGEN_TIME_OF_DAY,79,25,100,101,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_SORT | CBS_HASSTRINGS - LTEXT "Water:",-1,22,143,74,14,SS_CENTERIMAGE | NOT WS_GROUP - LTEXT "Time of Day:",-1,22,24,53,14,SS_CENTERIMAGE | NOT - WS_GROUP - LTEXT "Vegetation:",-1,22,160,74,14,SS_CENTERIMAGE | NOT - WS_GROUP - LTEXT "Cities:",-1,22,177,74,14,SS_CENTERIMAGE | NOT WS_GROUP - LTEXT "Accessability:",-1,22,75,74,14,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Preview Map",IDC_MAPGEN_PREVIEW,"Button",BS_OWNERDRAW, - 316,200,88,14 - GROUPBOX "",IDC_PREVIEW_FRAME,207,57,197,134 - CTEXT "Preview",-1,210,123,194,16,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Surprise Me",IDC_MAPGEN_SURPRISE,"Button",BS_OWNERDRAW, - 205,200,88,14 - CONTROL "Delete Map",IDC_MAPGEN_DELETE_MAP,"Button",BS_OWNERDRAW, - 178,220,65,14 - CONTROL "Slider1",IDC_MAPGEN_CLIFFS,"msctls_trackbar32",TBS_TOP, - 95,58,100,14 - LTEXT "Cliffs:",-1,22,58,74,14,SS_CENTERIMAGE | NOT WS_GROUP - CONTROL "Slider1",IDC_MAPGEN_PLAYERS,"msctls_trackbar32",TBS_TOP, - 95,41,100,14 - CONTROL "Slider1",IDC_MAPGEN_TIBERIUM_AMOUNT,"msctls_trackbar32", - TBS_TOP,95,109,100,14 - LTEXT "Tiberium Amount:",-1,22,109,74,14,SS_CENTERIMAGE | NOT - WS_GROUP - COMBOBOX IDC_MAPGEN_MAP_WIDTH,248,8,63,103,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_HASSTRINGS - COMBOBOX IDC_MAPGEN_MAP_HEIGHT,248,26,63,103,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_HASSTRINGS - CONTROL "Transitions",IDC_MAPGEN_TRANSITIONS,"Button", - BS_AUTOCHECKBOX,320,26,84,10 - CONTROL "Ion Storms",IDC_MAPGEN_ION_STORMS,"Button", - BS_AUTOCHECKBOX,320,8,84,10 - CONTROL "Slider1",IDC_MAPGEN_VEINHOLES,"msctls_trackbar32", - TBS_TOP,95,194,100,14 - LTEXT "Veinholes:",-1,22,194,74,14,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Lifeforms",IDC_MAPGEN_LIFEFORMS,"Button", - BS_AUTOCHECKBOX,320,44,84,10 -END - -IDD_WDT_PICK_CLAN DIALOG DISCARDABLE 0, 0, 206, 163 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CTEXT "World Domination Tour games use your Battle Clan affiliation to determine which side you are fighting for. If you do not wish to join a Battle Clan, select which side you wish to fight for.", - -1,15,11,180,48,NOT WS_GROUP - CONTROL "Join Battle Clan",IDC_PICKCLAN_JOIN,"Button", - BS_OWNERDRAW,27,64,150,19 - CONTROL "GDI",IDC_PICKCLAN_GDI,"Button",BS_OWNERDRAW,27,87,150, - 19 - CONTROL "Nod",IDC_PICKCLAN_NOD,"Button",BS_OWNERDRAW,27,110,150, - 19 - CONTROL "Cancel",IDC_CANCEL,"Button",BS_OWNERDRAW,27, - 133,150,19 -END - -IDD_MAPGEN_WDT DIALOG DISCARDABLE 0, 0, 426, 242 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "OK",IDOK,"Button",BS_OWNERDRAW,265,220,65,14 - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW,337,220,65,14 - COMBOBOX IDC_MAPGEN_ENVIRONMENT,79,8,100,103,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_SORT | CBS_HASSTRINGS - CONTROL "Slider1",IDC_MAPGEN_HILLS,"msctls_trackbar32",TBS_TOP, - 95,92,100,14 - LTEXT "Hills:",-1,22,92,74,14,SS_CENTERIMAGE | NOT WS_GROUP - CONTROL "Slider1",IDC_MAPGEN_WATER,"msctls_trackbar32",TBS_TOP, - 95,143,100,14 - LTEXT "Map Width:",-1,187,8,53,12,SS_CENTERIMAGE | NOT - WS_GROUP - LTEXT "Map Height:",-1,186,25,53,12,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Slider1",IDC_MAPGEN_VEGETATION,"msctls_trackbar32", - TBS_TOP,95,160,100,14 - CONTROL "Slider1",IDC_MAPGEN_CITIES,"msctls_trackbar32",TBS_TOP, - 95,177,100,14 - CONTROL "Slider1",IDC_MAPGEN_TIBERIUM_FIELDS,"msctls_trackbar32", - TBS_TOP,95,126,100,14 - LTEXT "Tiberium Fields:",-1,22,126,74,14,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Slider1",IDC_MAPGEN_ACCESSIBILITY,"msctls_trackbar32", - TBS_TOP,95,75,100,14 - LTEXT "Environment:",-1,22,7,53,14,SS_CENTERIMAGE | NOT - WS_GROUP - EDITTEXT IDC_MAPGEN_DIMENSION_EDIT,22,206,57,12,ES_NUMBER | NOT - WS_VISIBLE | WS_DISABLED | NOT WS_BORDER | NOT - WS_TABSTOP - CONTROL "Load Map",IDC_MAPGEN_LOAD_MAP,"Button",BS_OWNERDRAW,100, - 220,65,14 - CONTROL "Save Map",IDC_MAPGEN_SAVE_MAP,"Button",BS_OWNERDRAW,22, - 220,65,14 - COMBOBOX IDC_MAPGEN_TIME_OF_DAY,79,25,100,101,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_SORT | CBS_HASSTRINGS - LTEXT "Water:",-1,22,143,74,14,SS_CENTERIMAGE | NOT WS_GROUP - LTEXT "Time of Day:",-1,22,24,53,14,SS_CENTERIMAGE | NOT - WS_GROUP - LTEXT "Vegetation:",-1,22,160,74,14,SS_CENTERIMAGE | NOT - WS_GROUP - LTEXT "Cities:",-1,22,177,74,14,SS_CENTERIMAGE | NOT WS_GROUP - LTEXT "Accessability:",-1,22,75,74,14,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Preview Map",IDC_MAPGEN_PREVIEW,"Button",BS_OWNERDRAW, - 316,198,88,14 - GROUPBOX "",IDC_PREVIEW_FRAME,207,56,197,134 - CTEXT "Preview",-1,210,122,194,16,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Surprise Me",IDC_MAPGEN_SURPRISE,"Button",BS_OWNERDRAW, - 205,198,88,14 - CONTROL "Delete Map",IDC_MAPGEN_DELETE_MAP,"Button",BS_OWNERDRAW, - 178,220,65,14 - CONTROL "Slider1",IDC_MAPGEN_CLIFFS,"msctls_trackbar32",TBS_TOP, - 95,58,100,14 - LTEXT "Cliffs:",-1,22,58,74,14,SS_CENTERIMAGE | NOT WS_GROUP - CONTROL "Slider1",IDC_MAPGEN_TIBERIUM_AMOUNT,"msctls_trackbar32", - TBS_TOP,95,109,100,14 - LTEXT "Tiberium Amount:",-1,22,109,74,14,SS_CENTERIMAGE | NOT - WS_GROUP - COMBOBOX IDC_MAPGEN_MAP_WIDTH,248,8,63,103,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_HASSTRINGS - COMBOBOX IDC_MAPGEN_MAP_HEIGHT,248,26,63,103,CBS_DROPDOWNLIST | - CBS_OWNERDRAWFIXED | CBS_HASSTRINGS - CONTROL "Transitions",IDC_MAPGEN_TRANSITIONS,"Button", - BS_AUTOCHECKBOX,320,25,84,10 - CONTROL "Ion Storms",IDC_MAPGEN_ION_STORMS,"Button", - BS_AUTOCHECKBOX,320,8,84,10 - CONTROL "Slider1",IDC_MAPGEN_VEINHOLES,"msctls_trackbar32", - TBS_TOP,96,194,100,14 - LTEXT "Veinholes:",-1,22,194,74,14,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "1 on 1",IDC_WDT_1ON1,"Button",BS_AUTOCHECKBOX,22,44,73, - 10 - CONTROL "2 on 2",IDC_WDT_2ON2,"Button",BS_AUTOCHECKBOX,102,44,73, - 10 - CONTROL "Lifeforms",IDC_MAPGEN_LIFEFORMS,"Button", - BS_AUTOCHECKBOX,320,42,84,10 -END - -IDD_OPT_CTRL_WOL DIALOG DISCARDABLE 0, 0, 340, 185 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Resume Mission",IDC_RESUME_MISSION,"Button", - BS_OWNERDRAW,120,76,99,14 - CONTROL "Game Controls",IDC_GAME_CONTROLS,"Button",BS_OWNERDRAW, - 120,8,99,14 - CONTROL "Load Game",IDC_LOAD_GAME,"Button",BS_OWNERDRAW, - 120,25,99,14 - CONTROL "Save Game",IDC_SAVE_GAME,"Button",BS_OWNERDRAW, - 120,42,99,14 - CONTROL "Abort Mission",IDC_ABORT_MISSION,"Button",BS_OWNERDRAW, - 120,59,99,14 - CONTROL "Slider1",IDC_GAME_SPEED_SLIDER,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,95,132,148,13 - LTEXT "Game Speed",-1,39,132,58,13,SS_CENTERIMAGE | NOT - WS_GROUP - RTEXT "Faster",IDC_GAME_SPEED_LABEL,247,132,45,13, - SS_CENTERIMAGE | NOT WS_GROUP - CONTROL "Slider1",IDC_CTRLWOL_CONNECTION,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,95,110,148,13 - LTEXT "Connection",-1,39,110,58,13,SS_CENTERIMAGE | NOT - WS_GROUP - RTEXT "Better",IDC_SCROLL_SPEED_LABEL,247,110,45,13, - SS_CENTERIMAGE | NOT WS_GROUP - GROUPBOX "Internet Game Controls",-1,28,92,283,68 -END - -IDD_MPLAYER_SELECT_MAP DIALOGEX 0, 0, 360, 200 -STYLE WS_CHILD -FONT 8, "MS Sans Serif", 0, 0, 0x1 -BEGIN - CONTROL "OK",IDOK,"Button",BS_OWNERDRAW | WS_TABSTOP,22,174,50, - 14 - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW | WS_TABSTOP,288, - 174,50,14 - LISTBOX IDC_SELECTMAP_LIST,22,26,175,142,LBS_OWNERDRAWFIXED | - LBS_HASSTRINGS | LBS_NOINTEGRALHEIGHT | NOT WS_BORDER | - WS_TABSTOP - CTEXT "Select Multiplayer Map",-1,22,12,316,12,SS_CENTERIMAGE | - NOT WS_GROUP - GROUPBOX "",IDC_PREVIEW_FRAME,209,59,129,80,0,0, - HIDC_PREVIEW_FRAME - CONTROL "Create Random Map",IDC_CREATE_RANDOM_MAP,"Button", - BS_OWNERDRAW | WS_TABSTOP,132,174,106,14 -END - -IDD_MPLAYER_SELECT_MAP_SIMPLE DIALOG DISCARDABLE 0, 0, 219, 200 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "OK",IDOK,"Button",BS_OWNERDRAW | WS_TABSTOP,22,174,50, - 14 - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW | WS_TABSTOP,147, - 174,50,14 - LISTBOX IDC_SELECTMAP_LIST,22,26,175,143,LBS_OWNERDRAWFIXED | - LBS_HASSTRINGS | LBS_NOINTEGRALHEIGHT | NOT WS_BORDER | - WS_TABSTOP - CTEXT "Select Multiplayer Map",-1,22,12,175,12,SS_CENTERIMAGE | - NOT WS_GROUP -END - -IDD_MSGBOX_2 DIALOG DISCARDABLE 0, 0, 290, 90 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "OK",IDOK,"Button",BS_OWNERDRAW,22,64,50,14 - CTEXT "Msg",IDC_MSGBOX_TEXT,22,12,246,40 - CONTROL "Cancel",IDCANCEL,"Button",BS_OWNERDRAW,218,64,50,14 -END - -IDD_MSGBOX_3_LARGE DIALOG DISCARDABLE 0, 0, 360, 160 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "OK",IDOK,"Button",BS_OWNERDRAW | NOT WS_VISIBLE,155,134, - 50,14 - CTEXT "Msg",IDC_MSGBOX_TEXT,22,12,316,112 - CONTROL "No",7,"Button",BS_OWNERDRAW | NOT WS_VISIBLE,105,134,50, - 14 - CONTROL "Yes",6,"Button",BS_OWNERDRAW | NOT WS_VISIBLE,204,134, - 50,14 -END - -IDD_OPT_CTRL_GAME_WOL DIALOG DISCARDABLE 0, 0, 294, 144 -STYLE WS_CHILD -FONT 8, "MS Sans Serif" -BEGIN - CONTROL "Options Menu",1,"Button",BS_OWNERDRAW,196,118,77,14 - CONTROL "Keyboard",IDC_OPT_KEYBOARD_BTN,"Button",BS_OWNERDRAW, - 109,118,77,14 - CONTROL "Sound",IDC_OPT_SOUND_BTN,"Button",BS_OWNERDRAW,22,118, - 77,14 - CONTROL "Slider2",IDC_SCROLL_SPEED_SLIDER,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,90,12,128,15 - RTEXT "Scroll Rate:",-1,22,12,63,15,SS_CENTERIMAGE | NOT - WS_GROUP - CONTROL "Slider3",IDC_DETAIL_LEVEL_SLIDER,"msctls_trackbar32", - TBS_BOTH | TBS_NOTICKS,90,43,128,15 - RTEXT "Visual Details:",-1,22,43,63,15,SS_CENTERIMAGE | NOT - WS_GROUP - LTEXT "Higher",IDC_DETAIL_LEVEL_LABEL,226,43,50,15, - SS_CENTERIMAGE | NOT WS_GROUP - LTEXT "Faster",IDC_SCROLL_SPEED_LABEL,226,12,50,15, - SS_CENTERIMAGE | NOT WS_GROUP - CONTROL "Sidebar Text",IDC_SIDEBAR_TEXT,"Button",BS_AUTOCHECKBOX | - BS_FLAT,22,63,119,10 - CONTROL "Target Lines",IDC_TARGET_LINES,"Button",BS_AUTOCHECKBOX | - BS_FLAT,22,81,119,10 - CONTROL "Tooltips",IDC_TOOLTIPS,"Button",BS_AUTOCHECKBOX | - BS_FLAT,147,63,129,10 - CONTROL "Scroll Coasting",IDC_SCROLL_COASTING,"Button", - BS_AUTOCHECKBOX | BS_FLAT,147,81,129,10 - CONTROL "Edge Scrolling",IDC_EDGE_SCROLL,"Button", - BS_AUTOCHECKBOX | BS_FLAT,22,99,119,10 -END - - -///////////////////////////////////////////////////////////////////////////// -// -// Dialog Info -// - ///////////////////////////////////////////////////////////////////////////// // // String Table diff --git a/code/layer.cpp b/code/layer.cpp index 13ef01533..4c91cf6d9 100644 --- a/code/layer.cpp +++ b/code/layer.cpp @@ -151,17 +151,12 @@ int LayerClass::Sorted_Add(ObjectClass const * const object) /// are written by their own owners -- only the layer's object pointers are recorded here, /// to be swizzled back into real addresses when the game is loaded. /// -/// Returns with S_OK if the layer was written. Otherwise, the failure code from -/// the stream is returned. -HRESULT LayerClass::Save(IStream * stream) +/// bool; Was the record written whole? +bool LayerClass::Save(SaveStreamClass & stream) { - if (stream == NULL) { - return(E_POINTER); - } - SaveStreamClass savestream(stream, SaveStreamClass::MODE_SAVE); - DynamicVectorClass::Serialize(savestream); - return(savestream.Result()); + DynamicVectorClass::Serialize(stream); + return(!stream.Was_Error()); } @@ -171,16 +166,11 @@ HRESULT LayerClass::Save(IStream * stream) /// reconstructed. Whatever the layer was holding is discarded and the object pointers are /// read back, so they do not become usable until the swizzle pass has run. /// -/// Returns with S_OK if the layer was read. Otherwise, the failure code from the -/// stream is returned. -HRESULT LayerClass::Load(IStream * stream) +/// bool; Was the record read whole? +bool LayerClass::Load(SaveStreamClass & stream) { - if (stream == NULL) { - return(E_POINTER); - } - SaveStreamClass savestream(stream, SaveStreamClass::MODE_LOAD); - savestream.Set_Context("LayerClass"); - DynamicVectorClass::Serialize(savestream); - return(savestream.Result()); + stream.Set_Context("LayerClass"); + DynamicVectorClass::Serialize(stream); + return(!stream.Was_Error()); } diff --git a/code/layer.h b/code/layer.h index ffa129bc1..25dc07af0 100644 --- a/code/layer.h +++ b/code/layer.h @@ -40,8 +40,8 @@ class ObjectClass; class LayerClass : public DynamicVectorClass { public: - HRESULT Load(IStream * stream); - HRESULT Save(IStream * stream); + bool Load(SaveStreamClass & stream); + bool Save(SaveStreamClass & stream); public: diff --git a/code/levitate.cpp b/code/levitate.cpp index 28836e322..616718351 100644 --- a/code/levitate.cpp +++ b/code/levitate.cpp @@ -77,9 +77,9 @@ LevitateLocomotionClass::LevitateLocomotionClass(void) : /// /// Pointer to the object this locomotor will drive. /// Returns with the result of the attach operation. -HRESULT LevitateLocomotionClass::Link_To_Object(void *pointer) +void LevitateLocomotionClass::Link_To_Object(void *pointer) { - return(BASECLASS::Link_To_Object(pointer)); + BASECLASS::Link_To_Object(pointer); } @@ -825,7 +825,7 @@ bool LevitateLocomotionClass::Needs_New_Target(void) /// the vertical hover (Hover_AI). /// /// True while the unit is still moving. -boolean LevitateLocomotionClass::Process(void) +bool LevitateLocomotionClass::Process(void) { State_AI(); @@ -846,7 +846,7 @@ boolean LevitateLocomotionClass::Process(void) /// Reports whether the locomotor is in any state other than STATE_IDLE. /// /// True while moving. -boolean LevitateLocomotionClass::Is_Moving(void) +bool LevitateLocomotionClass::Is_Moving(void) { return(State != STATE_IDLE); } @@ -856,7 +856,7 @@ boolean LevitateLocomotionClass::Is_Moving(void) /// Reports whether the locomotor is in any state other than STATE_IDLE (identical to Is_Moving). /// /// True while moving. -boolean LevitateLocomotionClass::Is_Moving_Now(void) +bool LevitateLocomotionClass::Is_Moving_Now(void) { return(State != STATE_IDLE); } @@ -892,18 +892,9 @@ void LevitateLocomotionClass::Stop(void) } -/// -/// Fetches the class identifier of this locomotor. -/// The persistence system uses this identifier to create a locomotor of the right kind -/// when the object it drives is loaded back in. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT LevitateLocomotionClass::GetClassID(CLSID * retval) +ClassID LevitateLocomotionClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_LevitateLocomotion; - return(S_OK); + return(ClassID_LevitateLocomotion); } diff --git a/code/levitate.h b/code/levitate.h index bd273ba6b..e5bd84473 100644 --- a/code/levitate.h +++ b/code/levitate.h @@ -28,18 +28,18 @@ class LevitateLocomotionClass : public LocomotionClass LevitateLocomotionClass(void); virtual ~LevitateLocomotionClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; - virtual HRESULT STDMETHODCALLTYPE Link_To_Object(void *pointer) override; - virtual boolean STDMETHODCALLTYPE Is_Moving(void) override; - virtual Coord STDMETHODCALLTYPE Destination(void) override; - virtual Coord STDMETHODCALLTYPE Head_To_Coord(void) override; - virtual boolean STDMETHODCALLTYPE Process(void) override; - virtual LayerType STDMETHODCALLTYPE In_Which_Layer(void) override; - virtual boolean STDMETHODCALLTYPE Is_Moving_Now(void) override; - virtual void STDMETHODCALLTYPE Mark_All_Occupation_Bits(int mark) override; + virtual void Link_To_Object(void *pointer) override; + virtual bool Is_Moving(void) override; + virtual Coord Destination(void) override; + virtual Coord Head_To_Coord(void) override; + virtual bool Process(void) override; + virtual LayerType In_Which_Layer(void) override; + virtual bool Is_Moving_Now(void) override; + virtual void Mark_All_Occupation_Bits(int mark) override; /*--------------------------------------------------------------------- diff --git a/code/light.cpp b/code/light.cpp index 8ff5eacec..40c6bb860 100644 --- a/code/light.cpp +++ b/code/light.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "light.h" @@ -278,16 +277,9 @@ void LightSourceClass::Compute_CRC(CRCEngine & crc) const } -/// -/// Fetches the class identifier this object is persisted under. -/// -/// Destination for the class identifier. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE LightSourceClass::GetClassID(CLSID * retval) +ClassID LightSourceClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_LightSource; - return(S_OK); + return(ClassID_LightSource); } diff --git a/code/light.h b/code/light.h index 0e875e507..9585de25c 100644 --- a/code/light.h +++ b/code/light.h @@ -25,7 +25,7 @@ class LightSourceClass : public AbstractClass LightSourceClass(void); virtual ~LightSourceClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/loaddlg.cpp b/code/loaddlg.cpp index 7d5467f38..c0272021b 100644 --- a/code/loaddlg.cpp +++ b/code/loaddlg.cpp @@ -38,6 +38,9 @@ * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ #include "always.h" +#include "_keyboar.h" +#include "keyboard.h" +#include "ui/uimessagebox.h" #include "autosave.h" @@ -52,12 +55,12 @@ #include "init.h" #include "language/language.h" #include "msgbox.h" -#include "ownrdraw.h" #include "saveload.h" #include "savemgr.h" #include "savever.h" #include "scenario.h" #include "session.h" +#include "ui/uisavebrowser.h" #include "win.h" #include @@ -163,208 +166,6 @@ bool LoadOptionsClass::Delete(void) } -/// -/// Handles a control notification from the load game dialog. -/// This routine records how the player left the dialog, so that the processing loop -/// knows whether a game was chosen or the player backed out. -/// -/// The identifier of the control that was activated. -/// Window handle of the control that was activated. -/// The notification code that accompanied the control. -void LoadOptionsClass::Load_Dialog_On_WM_COMMAND(HWND window, WPARAM wparam, LPARAM lparam, int id) -{ - LoadOptionsClass * _this = (LoadOptionsClass *)GetWindowLongPtr(window, DWLP_USER); - switch ((int)wparam) { - case IDC_MISSION_LOAD_LIST: - if (id == 2 && ListBox_GetCount((HWND)lparam) > 0) { - _this->State = STATE_OK; - } - break; - - case IDOK: - case IDCANCEL: - if (id == 0) { - _this->State = (LoadDialogState)wparam; - } - break; - } -} - - -/// -/// Handles a control notification from the save game dialog. -/// Picking a game in the list copies its description into the edit field, so that the -/// player can save over an existing game without typing the name out again. The buttons -/// record how the player left the dialog. -/// -/// The identifier of the control that was activated. -/// Window handle of the control that was activated. -/// The notification code that accompanied the control. -void LoadOptionsClass::Save_Dialog_On_WM_COMMAND(HWND window, WPARAM wparam, LPARAM lparam, int id) -{ - LoadOptionsClass * _this = (LoadOptionsClass *)GetWindowLongPtr(window, DWLP_USER); - switch ((int)wparam) { - case IDC_MISSION_SAVE_LIST: - - /* - ** If the user clicks on the list, see if the there is a new current - ** item; if so, and if we're in SAVE mode, copy the list item into - ** the save-game description field. - */ - if (id == 1 && ListBox_GetCount((HWND)lparam) > 0) { - int row = ListBox_GetCurSel((HWND)lparam); - if (row != LB_ERR) { - - /* - ** Copy the game's description, UNLESS it's the empty slot; if - ** it is, set the edit buffer to empty. - */ - FileEntryClass * fdata = (FileEntryClass *)ListBox_GetItemData((HWND)lparam, row); - if (fdata->Valid) { - SetWindowText(GetDlgItem(window, IDC_MISSION_SAVE_DESC), fdata->Descr); - } else if (_this->Description != NULL) { - SetWindowText(GetDlgItem(window, IDC_MISSION_SAVE_DESC), _this->Description); - } - SetFocus(GetDlgItem(window, IDC_MISSION_SAVE_DESC)); - Edit_SetSel(GetDlgItem(window, IDC_MISSION_SAVE_DESC), 0, -1); - } - } - break; - - case IDOK: - case IDCANCEL: - if (id == 0) { - _this->State = (LoadDialogState)wparam; - } - break; - } -} - - -/// -/// Handles a control notification from the delete game dialog. -/// This routine records how the player left the dialog, so that the processing loop -/// knows whether to go ahead with the deletion. -/// -/// The identifier of the control that was activated. -/// The notification code that accompanied the control. -void LoadOptionsClass::Delete_Dialog_On_WM_COMMAND(HWND window, WPARAM wparam, LPARAM lparam, int id) -{ - LoadOptionsClass * _this = (LoadOptionsClass *)GetWindowLongPtr(window, DWLP_USER); - switch ((int)wparam) { - case IDOK: - case IDCANCEL: - if (id == 0) { - _this->State = (LoadDialogState)wparam; - } - break; - } -} - - -/// -/// Handles messages for the load game dialog. -/// The owner draw system is given first refusal on every message. What is left over is -/// used to set up the file list columns and to pass control activity along to the -/// command handler. -/// -/// Returns with the message result, or FALSE if nothing here dealt with it. -INT_PTR CALLBACK LoadOptionsClass::Load_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - INT_PTR rc = OwnerDraw::Default_Dialog_Proc(window, message, wparam, lparam); - - if (rc == 0) { - - switch (message) { - case WM_MOVING: - return(On_WM_MOVING(window, wparam, lparam)); - - case WM_COMMAND: - Load_Dialog_On_WM_COMMAND(window, LOWORD(wparam), lparam, HIWORD(wparam)); - break; - - case OD_SUBCLASSED: - SendDlgItemMessage(window, IDC_MISSION_LOAD_LIST, OD_ADDCOLUMN, 0xF9, 2); - SendDlgItemMessage(window, IDC_MISSION_LOAD_LIST, OD_ADDCOLUMN, 0x38, 255); - SendDlgItemMessage(window, IDC_MISSION_LOAD_LIST, OD_ADDCOLUMN, 0, 315); - break; - } - return(FALSE); - } - return(rc); -} - - -/// -/// Handles messages for the save game dialog. -/// The owner draw system is given first refusal on every message. What is left over is -/// used to set up the file list columns, cap the length of the description the player -/// may type, and pass control activity along to the command handler. -/// -/// Returns with the message result, or FALSE if nothing here dealt with it. -INT_PTR CALLBACK LoadOptionsClass::Save_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - INT_PTR rc = OwnerDraw::Default_Dialog_Proc(window, message, wparam, lparam); - - if (rc == 0) { - - switch (message) { - case WM_MOVING: - return(On_WM_MOVING(window, wparam, lparam)); - - case WM_COMMAND: - Save_Dialog_On_WM_COMMAND(window, LOWORD(wparam), lparam, HIWORD(wparam)); - break; - - case WM_INITDIALOG: - SendMessage(GetDlgItem(window, IDC_MISSION_SAVE_DESC), EM_SETLIMITTEXT, 79, 0); - break; - - case OD_SUBCLASSED: - SendDlgItemMessage(window, IDC_MISSION_SAVE_LIST, OD_ADDCOLUMN, 0xF9, 2); - SendDlgItemMessage(window, IDC_MISSION_SAVE_LIST, OD_ADDCOLUMN, 0x38, 255); - SendDlgItemMessage(window, IDC_MISSION_SAVE_LIST, OD_ADDCOLUMN, 0, 315); - break; - } - return(FALSE); - } - return(rc); -} - - -/// -/// Handles messages for the delete game dialog. -/// The owner draw system is given first refusal on every message. What is left over is -/// used to set up the file list columns and to pass control activity along to the -/// command handler. -/// -/// Returns with the message result, or FALSE if nothing here dealt with it. -INT_PTR CALLBACK LoadOptionsClass::Delete_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - INT_PTR rc = OwnerDraw::Default_Dialog_Proc(window, message, wparam, lparam); - - if (rc == 0) { - - switch (message) { - case WM_COMMAND: - Delete_Dialog_On_WM_COMMAND(window, LOWORD(wparam), lparam, HIWORD(wparam)); - break; - - case WM_MOVING: - return(On_WM_MOVING(window, wparam, lparam)); - - case OD_SUBCLASSED: - SendDlgItemMessage(window, IDC_MISSION_DELETE_LIST, OD_ADDCOLUMN, 0xF9, 2); - SendDlgItemMessage(window, IDC_MISSION_DELETE_LIST, OD_ADDCOLUMN, 0x38, 255); - SendDlgItemMessage(window, IDC_MISSION_DELETE_LIST, OD_ADDCOLUMN, 0, 315); - break; - } - return(FALSE); - } - return(rc); -} - - /// /// Is a saved game of this name already there? Asked before one is written, since a name the /// folder holds is written over rather than added to. @@ -375,6 +176,21 @@ static bool Saved_Game_Exists(char const * name) } +/*********************************************************************************************** + * LoadOptionsClass::Process -- main processing routine * + * * + * INPUT: * + * none. * + * * + * OUTPUT: * + * false = User cancelled, true = operation completed * + * * + * WARNINGS: * + * none. * + * * + * HISTORY: * + * 02/14/1995 BR : Created. * + *=============================================================================================*/ /*********************************************************************************************** * LoadOptionsClass::Process -- main processing routine * * * @@ -392,183 +208,30 @@ static bool Saved_Game_Exists(char const * name) *=============================================================================================*/ bool LoadOptionsClass::Dialog(void) { - /* - ** Dialog variables - */ - HWND dialog = 0; - HWND list = 0; - - char buffer[256]; - - switch (Style) { - case LOAD: - dialog = OwnerDraw::Begin_Dialog(IDD_MISSION_LOAD, Load_Dialog_Proc); - list = GetDlgItem(dialog, IDC_MISSION_LOAD_LIST); - break; - - case SAVE: - if (Disk_Space_Available() < MinSpaceRequired) { - WWMessageBox().Process(TXT_DISKFULL, TXT_OK, TXT_NONE, TXT_NONE); - return(false); - } - dialog = OwnerDraw::Begin_Dialog(IDD_MISSION_SAVE, Save_Dialog_Proc); - list = GetDlgItem(dialog, IDC_MISSION_SAVE_LIST); - break; - - case WWDELETE: - dialog = OwnerDraw::Begin_Dialog(IDD_MISSION_DELETE, Delete_Dialog_Proc); - list = GetDlgItem(dialog, IDC_MISSION_DELETE_LIST); - break; + UISaveBrowserPresenterClass::StyleType style = UISaveBrowserPresenterClass::STYLE_LOAD; + if (Style == SAVE) { + style = UISaveBrowserPresenterClass::STYLE_SAVE; + } else if (Style == WWDELETE) { + style = UISaveBrowserPresenterClass::STYLE_DELETE; } - State = STATE_PENDING; - - if (dialog) { + UISaveBrowserPresenterClass screen(*this, style); - /* - ** Initialize. - */ - SetWindowLongPtr(dialog, DWLP_USER, (LONG_PTR)this); - - if (list != 0) { - Fill_List(list); - EnableWindow(GetDlgItem(dialog, 1), bool(ListBox_GetCount(list) > 0)); - } - - OwnerDraw::Display_Dialog(dialog); + if (!screen.Can_Open()) { + return(false); + } - /* - ** Main Processing Loop. - */ - do { - while (State == STATE_PENDING) { - if (OwnerDraw::Dialog_Message_Handler() == true) { - State = STATE_CLOSE; - } - - /* - ** Invoke game callback. - */ - if (Callback) { - Callback(); - } - - /* - ** If we have just received input focus again after running in the background then - ** we need to redraw. - */ - if (!GameActive) { - Title_Screen_Restore(0); - } - } + screen.Refresh(); - if (State == STATE_OK) { - LRESULT row = ListBox_GetCurSel(list); - - if (row != LB_ERR) { - FileEntryClass * entry = (FileEntryClass *)ListBox_GetItemData(list, row); - - /* - ** Process input. - */ - switch (Style) { - /* - ** Load: if load fails, present a message, and stay in the dialog - ** to allow the user to try another game - */ - case LOAD: { - if (entry->Num != -1) { - Init_Campaigns(); - } - - ShowWindow(dialog, SW_HIDE); - UpdateWindow(MainWindow); - - if (!Load_File(entry->Filename)) { - WWMessageBox().Process(TXT_ERROR_LOADING_GAME, TXT_OK, TXT_NONE, TXT_NONE); - ShowWindow(dialog, SW_SHOW); - State = STATE_PENDING; - } - break; - } - - /* - ** Save: Save the game & exit the dialog - */ - case SAVE: { - GetWindowText(GetDlgItem(dialog, IDC_MISSION_SAVE_DESC), buffer, DESCRIP_MAX+36); - - if (strlen(buffer) == 0) { - WWMessageBox().Process(TXT_MUSTENTER_DESCRIPTION, TXT_OK, TXT_NONE, TXT_NONE); - SetFocus(GetDlgItem(dialog, IDC_MISSION_SAVE_DESC)); - Edit_SetSel(GetDlgItem(dialog, IDC_MISSION_SAVE_DESC), -1, -1); - State = STATE_PENDING; - break; - } - - const char * filename = NULL; - char test_filename[256]; - - if (entry && entry->Valid) { - filename = entry->Filename; - } else { - Pick_Filename(test_filename); - filename = test_filename; - } - - if (filename != NULL) { - bool exists = Saved_Game_Exists(filename); - if (exists && WWMessageBox()._Process(TXT_CONFIRM_SAVE, 1, TXT_YES, TXT_NO, TXT_NONE)) - State = STATE_PENDING; - else { - if (!Save_File(filename, buffer)) { - WWMessageBox().Process(TXT_ERROR_SAVING_GAME, TXT_OK, TXT_NONE, TXT_NONE); - State = STATE_PENDING; - } else { - int confirmation = Save_Confirmation(); - if (confirmation != TXT_NONE) { - WWMessageBox().Process(confirmation, TXT_OK, TXT_NONE, TXT_NONE); - } - if (Description) { - strcpy(Description, buffer); - } - } - } - } - break; - } - - /* - ** Delete: delete the file & stay in the dialog, to allow the user - ** to delete multiple files. - */ - case WWDELETE: { - sprintf(buffer, "%s\n%s", Fetch_String(TXT_DELETE_FILE_QUERY), entry->Descr); - - if (!WWMessageBox()._Process(buffer, 1, TXT_YES, TXT_NO, TXT_NONE)) { - Delete_File(entry->Filename); - ListBox_DeleteString(list, row); - ListBox_SetCurSel(list, 0); - if (ListBox_GetCount(list) > 0) { - State = STATE_PENDING; - break; - } - } else { - State = STATE_PENDING; - } - break; - } - } - } - } - } while (State == STATE_PENDING); + State = STATE_PENDING; + if (UI_Save_Browser_Screen(screen).Outcome != UIResult::OUTCOME_FAILED_TO_OPEN) { Clear_List(); - - OwnerDraw::End_Dialog(dialog); } - return(State == STATE_OK ? true : false); + State = screen.Accepted() ? STATE_OK : STATE_CLOSE; + + return(screen.Accepted()); } @@ -617,7 +280,7 @@ void LoadOptionsClass::Clear_List(void) /*********************************************************************************************** - * LoadOptionsClass::Fill_List -- fills the list box & GameNum arrays * + * LoadOptionsClass::Build_List -- reads the folder into the file list * * * * INPUT: * * none. * @@ -632,9 +295,8 @@ void LoadOptionsClass::Clear_List(void) * 02/14/1995 BR : Created. * * 06/25/1995 JLB : Shows which saved games are "(old)". * *=============================================================================================*/ -void LoadOptionsClass::Fill_List(HWND window) +void LoadOptionsClass::Build_List(void) { - OwnerDraw::CellData thecell; FileEntryClass * fdata = NULL; // for adding entries to 'Files' WIN32_FIND_DATAA ff; // for FindFirstFile @@ -722,60 +384,6 @@ void LoadOptionsClass::Fill_List(HWND window) ** Now sort the list in order of Date/Time (newest first, oldest last) */ qsort((void *)(&Files[0]), Files.Count(), sizeof(class FileEntryClass *), LoadOptionsClass::Compare); - - ListBox_ResetContent(window); - - /* - ** Now add every file's name to the list box - */ - for (int i = 0; i < Files.Count(); i++) { - fdata = Files[i]; - - int row = ListBox_AddString(window, fdata); - - if (fdata->Type != GAME_NORMAL) { - thecell.type = OwnerDraw::CellData::TEXT; - thecell.string.set("*"); - SendMessage(window, OD_SETCELL, MAKEWPARAM(200, row), (LPARAM)&thecell); - } - - if (fdata->DateTime.dwHighDateTime != -1 && fdata->DateTime.dwLowDateTime != -1) { - FILETIME ft; - SYSTEMTIME time; - FileTimeToLocalFileTime(&fdata->DateTime, &ft); - FileTimeToSystemTime(&ft, &time); - GetDateFormat(LANG_USER_DEFAULT, TIME_NOMINUTESORSECONDS, &time, NULL, buffer, sizeof(buffer)); - thecell.type = OwnerDraw::CellData::TEXT; - thecell.string.set(buffer); - SendMessage(window, OD_SETCELL, MAKEWPARAM(255, row), (LPARAM)&thecell); - GetTimeFormat(LANG_USER_DEFAULT, TIME_NOSECONDS, &time, NULL, buffer, sizeof(buffer)); - thecell.type = OwnerDraw::CellData::TEXT; - thecell.string.set(buffer); - SendMessage(window, OD_SETCELL, MAKEWPARAM(315, row), (LPARAM)&thecell); - } - - ListBox_SetItemData(window, row, (LPARAM)fdata); - } - - switch (Style) { - case LOAD: { - for (int i = 0; i < Files.Count(); i++) { - if (Files[i]->Valid) { - ListBox_SetCurSel(window, i); - ListBox_SetTopIndex(window, i); - break; - } - } - } - break; - - case SAVE: - case WWDELETE: - ListBox_SetCurSel(window, 0); - ListBox_SetTopIndex(window, 0); - break; - } - } } @@ -846,21 +454,20 @@ int __cdecl LoadOptionsClass::Compare(const void * p1, const void * p2) /// /// Restores the game held in the file specified. -/// A message box is displayed while the load runs, and the scenario is taken out of +/// A wait box is displayed while the load runs, and the scenario is taken out of /// play first so that nothing tries to tick while the game state is being replaced. /// /// bool; Was the game loaded? bool LoadOptionsClass::Load_File(const char * file_name) { - HWND dialog = OwnerDraw::Custom_Message_Box(Fetch_String(TXT_LOADING), NULL, NULL); - if (dialog != 0) { - OwnerDraw::Display_Dialog(dialog); - } + bool const box = UI_Wait_Box_Open(Fetch_String(TXT_LOADING), NULL, NULL); + Keyboard->Clear(); ScenarioActive = false; TacticalActive = false; bool loaded = Load_Game(file_name); - if (dialog != 0) { - OwnerDraw::End_Dialog(dialog); + if (box) { + Keyboard->Clear(); + UI_Wait_Box_Close(); } return(loaded); } @@ -868,21 +475,20 @@ bool LoadOptionsClass::Load_File(const char * file_name) /// /// Saves the current game to the file specified. -/// A message box is displayed while the save runs, since writing a save game takes long +/// A wait box is displayed while the save runs, since writing a save game takes long /// enough that the player would otherwise think the game had locked up. /// /// The description to record alongside the saved game. /// bool; Was the game saved? bool LoadOptionsClass::Save_File(const char * file_name, const char * descr) { - HWND dialog = OwnerDraw::Custom_Message_Box(Fetch_String(TXT_SAVING_GAME), NULL, NULL); - if (dialog != 0) { - OwnerDraw::Display_Dialog(dialog); - } + bool const box = UI_Wait_Box_Open(Fetch_String(TXT_SAVING_GAME), NULL, NULL); + Keyboard->Clear(); bool saved = SaveManager.Request_Save_Game(file_name, descr, false, SaveManagerClass::NoticeType::Requested); - if (dialog != 0) { - OwnerDraw::End_Dialog(dialog); + if (box) { + Keyboard->Clear(); + UI_Wait_Box_Close(); } return(saved); } diff --git a/code/loaddlg.h b/code/loaddlg.h index 24af0f976..f1a849350 100644 --- a/code/loaddlg.h +++ b/code/loaddlg.h @@ -69,6 +69,10 @@ class FileEntryClass { class LoadOptionsClass { + // The screen's behavior, which reads the file list, the suggested description and the + // save confirmation this class owns. + friend class UISaveBrowserPresenterClass; + public: /* ** This defines the style of the dialog @@ -102,6 +106,10 @@ class LoadOptionsClass void Pick_Filename(char * file_name); bool Files_Present(void); + // Reads the folder into Files, newest first. The control the list is shown in is a + // view's business, so it is not touched here. + void Build_List(void); + virtual bool Load_File(const char * file_name); virtual bool Save_File(const char * file_name, const char * descr); virtual bool Delete_File(const char * file_name); @@ -112,7 +120,6 @@ class LoadOptionsClass ** Internal routines */ void Clear_List (void); // clears the list & game # array - void Fill_List (HWND window); // fills the list & game # array int Num_From_Ext (char *fname); // translates filename to file # static int __cdecl Compare(const void *p1, const void *p2); // for qsort() @@ -127,13 +134,7 @@ class LoadOptionsClass /* * These handlers are members so that they can reach the dialog's protected data. */ - static void Load_Dialog_On_WM_COMMAND(HWND window, WPARAM wparam, LPARAM lparam, int id); - static void Save_Dialog_On_WM_COMMAND(HWND window, WPARAM wparam, LPARAM lparam, int id); - static void Delete_Dialog_On_WM_COMMAND(HWND window, WPARAM wparam, LPARAM lparam, int id); - static INT_PTR CALLBACK Load_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); - static INT_PTR CALLBACK Save_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); - static INT_PTR CALLBACK Delete_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); /* ** This is the requested style of the dialog diff --git a/code/loco.cpp b/code/loco.cpp index 7030a98fd..5027f2916 100644 --- a/code/loco.cpp +++ b/code/loco.cpp @@ -14,10 +14,13 @@ #include "_map.h" #include "_tactica.h" #include "cell.h" +#include "classfactory.h" #include "coord.h" +#include "dbgprint.h" #include "foot.h" #include "globals.h" #include "map.h" +#include "saveload.h" #include "savestream.h" #include "swizzle.h" #include "tactical.h" @@ -28,8 +31,8 @@ #include "zgrad.hh" #include +#include -extern ULONG COMRefCount; /// @@ -41,8 +44,7 @@ extern ULONG COMRefCount; LocomotionClass::LocomotionClass(void) : LinkedTo(NULL), IsPowered(true), - Dirty(true), - RefCount(0) + Dirty(true) { } @@ -62,11 +64,9 @@ LocomotionClass::~LocomotionClass(void) /// offers depends on it having been called first. /// /// Pointer to the foot class object this locomotor will carry about. -/// Returns with S_OK, since the attachment cannot fail. -HRESULT STDMETHODCALLTYPE LocomotionClass::Link_To_Object(void *pointer) +void LocomotionClass::Link_To_Object(void *pointer) { LinkedTo = (FootClass *)pointer; - return(S_OK); } @@ -79,7 +79,7 @@ HRESULT STDMETHODCALLTYPE LocomotionClass::Link_To_Object(void *pointer) /// Optional cache key for the voxel renderer, which the facing is folded /// into. May be NULL, and a key of -1 means the drawing is not to be cached. /// Returns with the matrix to transform the object by. -Matrix3D STDMETHODCALLTYPE LocomotionClass::Draw_Matrix(int *key) +Matrix3D LocomotionClass::Draw_Matrix(int *key) { Matrix3D draw_matrix(true); @@ -101,7 +101,7 @@ Matrix3D STDMETHODCALLTYPE LocomotionClass::Draw_Matrix(int *key) /// Optional cache key for the voxel renderer, which the slope and facing are /// folded into. May be NULL, and a key of -1 means the shadow is not to be cached. /// Returns with the matrix to transform the shadow by. -Matrix3D STDMETHODCALLTYPE LocomotionClass::Shadow_Matrix(int *key) +Matrix3D LocomotionClass::Shadow_Matrix(int *key) { int ramp = Map[LinkedTo->Get_Coord()].Ramp; @@ -122,7 +122,7 @@ Matrix3D STDMETHODCALLTYPE LocomotionClass::Shadow_Matrix(int *key) /// down by however far the object is flying above the terrain. /// /// Returns with the pixel offset to shift the shadow by when drawing. -Point2D STDMETHODCALLTYPE LocomotionClass::Shadow_Point(void) +Point2D LocomotionClass::Shadow_Point(void) { Point2D pt; @@ -139,7 +139,7 @@ Point2D STDMETHODCALLTYPE LocomotionClass::Shadow_Point(void) /// more. /// /// bool; Is the locomotor powered after the change? -boolean STDMETHODCALLTYPE LocomotionClass::Power_On(void) +bool LocomotionClass::Power_On(void) { IsPowered = true; return(Is_Powered()); @@ -152,7 +152,7 @@ boolean STDMETHODCALLTYPE LocomotionClass::Power_On(void) /// by an EMP pulse or its owner loses base power. /// /// bool; Is the locomotor powered after the change? -boolean STDMETHODCALLTYPE LocomotionClass::Power_Off(void) +bool LocomotionClass::Power_Off(void) { IsPowered = false; return(Is_Powered()); @@ -165,7 +165,7 @@ boolean STDMETHODCALLTYPE LocomotionClass::Power_Off(void) /// loss of base power leaves a unit stranded. /// /// bool; Is the locomotor powered? -boolean STDMETHODCALLTYPE LocomotionClass::Is_Powered(void) +bool LocomotionClass::Is_Powered(void) { return(IsPowered); } @@ -177,79 +177,42 @@ boolean STDMETHODCALLTYPE LocomotionClass::Is_Powered(void) /// this routine so that a storm can bring their objects down. /// /// bool; Is the locomotor sensitive to ion storms? -boolean STDMETHODCALLTYPE LocomotionClass::Is_Ion_Sensitive(void) +bool LocomotionClass::Is_Ion_Sensitive(void) { return(false); } -/// -/// Adds a reference to this locomotor. -/// Anything that holds on to a locomotor takes a reference first, which keeps the -/// locomotor alive until that holder releases it again. -/// -/// Returns with the number of references now outstanding. -ULONG STDMETHODCALLTYPE LocomotionClass::AddRef(void) +std::unique_ptr Create_Locomotor(ClassID const & classid) { - ++COMRefCount; - return(InterlockedIncrement(&RefCount)); + IPersistent * const object = Create_Object(classid); + ILocomotion * const locomotion = dynamic_cast(object); + if (locomotion == NULL) { + delete object; + } + return(std::unique_ptr(locomotion)); } -/// -/// Releases a reference to this locomotor. -/// When the last reference goes away the locomotor destroys itself, so the caller must -/// not touch its pointer afterward. -/// -/// Returns with the number of references still outstanding. -ULONG STDMETHODCALLTYPE LocomotionClass::Release(void) +std::unique_ptr Load_Locomotor(SaveStreamClass & stream) { - --COMRefCount; - - ULONG count = InterlockedDecrement(&RefCount); - if (count == 0) { - delete this; + SwizzleManagerClass::MarkType const mark = Swizzler.Mark(); + IPersistent * const object = Load_Object(stream); + ILocomotion * const locomotion = dynamic_cast(object); + if (object != NULL && locomotion == NULL) { + DebugString("Save record of %s at %u is not a locomotor\n", typeid(*object).name(), stream.Offset()); + Swizzler.Abandon(mark); + delete object; + stream.Fail(); } - return(count); + return(std::unique_ptr(locomotion)); } -/// -/// Fetches one of the interfaces this locomotor implements. -/// A locomotor answers to IUnknown, IPersist, IPersistStream, and ILocomotion. Any other -/// interface asked for is refused. -/// -/// The identifier of the interface being asked for. -/// Pointer to the location to store the interface pointer in. -/// Returns with S_OK, or E_NOINTERFACE if the interface is not supported. -/// An interface fetched successfully carries a reference. The caller must release -/// it when finished with it. -LONG STDMETHODCALLTYPE LocomotionClass::QueryInterface(REFIID riid, LPVOID *ppvObject) +ClassID Locomotion_Class_ID(ILocomotion * locomotion) { - if (ppvObject == NULL) { - return(E_POINTER); - } - - *ppvObject = NULL; - - if (riid == IID_IUnknown) { - *ppvObject = (IUnknown *)(ILocomotion *)this; - } - if (riid == IID_IPersistStream) { - *ppvObject = (IPersistStream *)this; - } - if (riid == IID_ILocomotion) { - *ppvObject = (ILocomotion *)this; - } - if (riid == IID_IPersist) { - *ppvObject = (IPersist *)this; - } - if (*ppvObject == NULL) { - return(E_NOINTERFACE); - } - - AddRef(); - return(S_OK); + IPersistent const * const persist = dynamic_cast(locomotion); + return(persist != NULL ? persist->Class_ID() : ClassID()); } @@ -259,32 +222,15 @@ LONG STDMETHODCALLTYPE LocomotionClass::QueryInterface(REFIID riid, LPVOID *ppvO /// swizzle manager remap every pointer to it when the game is loaded again. /// /// Should the locomotor be marked as no longer needing a save? -/// Returns with the result of the write, or E_POINTER if no stream was supplied. -HRESULT STDMETHODCALLTYPE LocomotionClass::Save(IStream * stream, BOOL cleardirty) +/// Returns with the result of the write. +bool LocomotionClass::Save(SaveStreamClass & stream, bool cleardirty) { - if (stream == NULL) { - return(E_POINTER); /// E_INVALIDARG - } - return(Save_Members(stream, cleardirty)); } -/// -/// Loads the locomotor back from a save game stream. -/// The locomotor announces its new address to the swizzle manager before its data is -/// read in, so that every saved pointer to it can be remapped and its link back to the -/// object it drives can be restored. The reference count belongs to the running session -/// rather than to the saved state, so it survives the load untouched. -/// -/// The stream to read the locomotor back from. -/// Returns with the result of the read, or E_POINTER if no stream was supplied. -HRESULT STDMETHODCALLTYPE LocomotionClass::Load(IStream * stream) +bool LocomotionClass::Load(SaveStreamClass & stream) { - if (stream == NULL) { - return(E_POINTER); /// E_INVALIDARG - } - return(Load_Members(stream)); } @@ -296,28 +242,16 @@ HRESULT STDMETHODCALLTYPE LocomotionClass::Load(IStream * stream) /// /// The stream to write to. /// Should the locomotor be marked clean once it has been written? -/// Returns with S_OK when the record was written, otherwise a failure code. -HRESULT LocomotionClass::Save_Members(IStream * stream, BOOL cleardirty) +/// bool; Was the record written whole? +bool LocomotionClass::Save_Members(SaveStreamClass & stream, bool cleardirty) { - if (stream == NULL) { - return(E_POINTER); - } - SwizzleIDType id = Swizzler.ID_Of(this); - - HRESULT result = stream->Write(&id, sizeof(id), NULL); - if (FAILED(result)) { - return(result); - } - - SaveStreamClass savestream(stream, SaveStreamClass::MODE_SAVE); - Serialize(savestream); - - if (SUCCEEDED(savestream.Result()) && cleardirty) { + stream.Serialize(id); + Serialize(stream); + if (!stream.Was_Error() && cleardirty) { Dirty = false; } - - return(savestream.Result()); + return(!stream.Was_Error()); } @@ -327,46 +261,33 @@ HRESULT LocomotionClass::Save_Members(IStream * stream, BOOL cleardirty) /// save game can be remapped onto this locomotor, and the members follow. /// /// The stream to read from. -/// Returns with S_OK when the record was read, otherwise a failure code. -HRESULT LocomotionClass::Load_Members(IStream * stream) +/// bool; Was the record read whole? +bool LocomotionClass::Load_Members(SaveStreamClass & stream) { - if (stream == NULL) { - return(E_POINTER); + SwizzleIDType id = 0; + stream.Serialize(id); + if (stream.Was_Error()) { + return(false); } - - SwizzleIDType id; - - HRESULT result = stream->Read(&id, sizeof(id), NULL); - if (FAILED(result)) { - return(result); - } - assert(id != 0); Swizzle_Here_I_Am(id, this); - SaveStreamClass savestream(stream, SaveStreamClass::MODE_LOAD); - savestream.Set_Context(typeid(*this).name(), id); - Serialize(savestream); + char const * const outertype = stream.Context_Type(); + SwizzleIDType const outerid = stream.Context_ID(); + stream.Set_Context(typeid(*this).name(), id); + Serialize(stream); + stream.Set_Context(outertype, outerid); - if (SUCCEEDED(savestream.Result())) { - Post_Load(); - } - - return(savestream.Result()); + return(!stream.Was_Error()); } -/// -/// Lists the members every locomotor carries. -/// -/// The stream carrying the members. void LocomotionClass::Serialize(SaveStreamClass & stream) { stream.Serialize(LinkedTo); stream.Serialize(IsPowered); stream.Serialize(Dirty); - // RefCount -- belongs to the running session rather than the record. } @@ -379,27 +300,13 @@ void LocomotionClass::Post_Load(void) } -/// -/// Fetches the number of bytes needed to save this locomotor. -/// A record is as long as the members a class names, so the count is not known before -/// the members have been written. Nothing in the game asks for it, so rather than -/// walk the locomotor twice this reports that the size cannot be supplied. -/// -/// Pointer to the value to fill in with the required byte count. -/// Returns with E_NOTIMPL. -LONG STDMETHODCALLTYPE LocomotionClass::GetSizeMax(ULARGE_INTEGER *pcbSize) -{ - return(E_NOTIMPL); -} - - /// /// Asks the object to step out of the way in the direction specified. /// This routine is used when another object needs the cell this one happens to be /// occupying. The base locomotor cannot be moved and declines. /// /// bool; Did the object step out of the way? -boolean STDMETHODCALLTYPE LocomotionClass::Push(DirType dir) +bool LocomotionClass::Push(DirType dir) { return(false); } @@ -411,7 +318,7 @@ boolean STDMETHODCALLTYPE LocomotionClass::Push(DirType dir) /// displaced to clear the way. The base locomotor will not budge. /// /// bool; Was the object shoved out of the way? -boolean STDMETHODCALLTYPE LocomotionClass::Shove(DirType dir) +bool LocomotionClass::Shove(DirType dir) { return(false); } @@ -422,7 +329,7 @@ boolean STDMETHODCALLTYPE LocomotionClass::Shove(DirType dir) /// Locomotors that rock their object about -- over bumps, on landing, or when it takes a /// hit -- use this routine to ease the body back toward level. /// -void STDMETHODCALLTYPE LocomotionClass::Tilt_Pitch_AI(void) +void LocomotionClass::Tilt_Pitch_AI(void) { } @@ -433,7 +340,7 @@ void STDMETHODCALLTYPE LocomotionClass::Tilt_Pitch_AI(void) /// against the terrain it is traveling over. The base locomotor needs no such favor. /// /// Returns with the depth adjustment to apply when drawing the object. -int STDMETHODCALLTYPE LocomotionClass::Z_Adjust(void) +int LocomotionClass::Z_Adjust(void) { return(0); } @@ -445,7 +352,7 @@ int STDMETHODCALLTYPE LocomotionClass::Z_Adjust(void) /// shape. The base locomotor reports the upright case. /// /// Returns with the Z gradient to render the object with. -ZGradientType STDMETHODCALLTYPE LocomotionClass::Z_Gradient(void) +ZGradientType LocomotionClass::Z_Gradient(void) { return(ZGRAD_90DEG); } @@ -457,7 +364,7 @@ ZGradientType STDMETHODCALLTYPE LocomotionClass::Z_Gradient(void) /// otherwise leaves plain sight. The base locomotor never alters the appearance. /// /// Returns with the visual character to render the object with. -VisualType STDMETHODCALLTYPE LocomotionClass::Visual_Character(boolean flag) +VisualType LocomotionClass::Visual_Character(bool flag) { return(VISUAL_NORMAL); } @@ -469,7 +376,7 @@ VisualType STDMETHODCALLTYPE LocomotionClass::Visual_Character(boolean flag) /// locomotor makes its object bob, hop, or sink. The base locomotor draws in place. /// /// Returns with the pixel offset to shift the object by when drawing. -Point2D STDMETHODCALLTYPE LocomotionClass::Draw_Point(void) +Point2D LocomotionClass::Draw_Point(void) { Point2D pt; pt.X = 0; @@ -484,7 +391,7 @@ Point2D STDMETHODCALLTYPE LocomotionClass::Draw_Point(void) /// otherwise hidden -- will override this routine to suppress the shadow. /// /// bool; Should a shadow be drawn for the object? -boolean STDMETHODCALLTYPE LocomotionClass::Is_To_Have_Shadow(void) +bool LocomotionClass::Is_To_Have_Shadow(void) { return(true); } @@ -497,7 +404,7 @@ boolean STDMETHODCALLTYPE LocomotionClass::Is_To_Have_Shadow(void) /// unrestricted and welcomes every cell. /// /// Returns with the move legality of the cell. -MoveType STDMETHODCALLTYPE LocomotionClass::Can_Enter_Cell(Cell cell) +MoveType LocomotionClass::Can_Enter_Cell(Cell cell) { return(MOVE_OK); } @@ -509,7 +416,7 @@ MoveType STDMETHODCALLTYPE LocomotionClass::Can_Enter_Cell(Cell cell) /// outside code must dictate exactly where the object ends up next. /// /// The coordinate the object should head to immediately. -void STDMETHODCALLTYPE LocomotionClass::Force_Immediate_Destination(Coord coord) +void LocomotionClass::Force_Immediate_Destination(Coord coord) { } @@ -521,7 +428,7 @@ void STDMETHODCALLTYPE LocomotionClass::Force_Immediate_Destination(Coord coord) /// /// The track number the object should be placed onto. /// The coordinate to treat as the start of the track. -void STDMETHODCALLTYPE LocomotionClass::Force_Track(int track, Coord coord) +void LocomotionClass::Force_Track(int track, Coord coord) { } @@ -531,7 +438,7 @@ void STDMETHODCALLTYPE LocomotionClass::Force_Track(int track, Coord coord) /// This gives derived locomotors their chance to pick up a starting facing, slope, or /// altitude from the ground the object has just arrived on. /// -void STDMETHODCALLTYPE LocomotionClass::Unlimbo(void) +void LocomotionClass::Unlimbo(void) { } @@ -541,7 +448,7 @@ void STDMETHODCALLTYPE LocomotionClass::Unlimbo(void) /// The base locomotor has no body of its own to rotate, so the request goes unheeded. /// /// The direction that the object should come to face. -void STDMETHODCALLTYPE LocomotionClass::Do_Turn(DirType coord) +void LocomotionClass::Do_Turn(DirType coord) { } @@ -551,7 +458,7 @@ void STDMETHODCALLTYPE LocomotionClass::Do_Turn(DirType coord) /// This routine is called when the object must give up on wherever it was going. Derived /// locomotors use it to abandon their journey and bring the object to a legal rest. /// -void STDMETHODCALLTYPE LocomotionClass::Stop_Moving(void) +void LocomotionClass::Stop_Moving(void) { } @@ -561,7 +468,7 @@ void STDMETHODCALLTYPE LocomotionClass::Stop_Moving(void) /// This is how the object hands its locomotor a new place to go. The base locomotor /// cannot move anything, so the request is quietly ignored. /// -void STDMETHODCALLTYPE LocomotionClass::Move_To(Coord to) +void LocomotionClass::Move_To(Coord to) { } @@ -573,7 +480,7 @@ void STDMETHODCALLTYPE LocomotionClass::Move_To(Coord to) /// is already at rest. /// /// bool; Is the locomotor at rest, with nothing further to do? -boolean STDMETHODCALLTYPE LocomotionClass::Process(void) +bool LocomotionClass::Process(void) { return(true); } @@ -585,7 +492,7 @@ boolean STDMETHODCALLTYPE LocomotionClass::Process(void) /// destination at all. /// /// Returns with the destination coordinate, or COORD_NONE if there is none. -Coord STDMETHODCALLTYPE LocomotionClass::Destination(void) +Coord LocomotionClass::Destination(void) { Coord coord; coord.X = COORD_NONE.X; @@ -601,7 +508,7 @@ Coord STDMETHODCALLTYPE LocomotionClass::Destination(void) /// has nowhere to go, it reports the object's own position. /// /// Returns with the coordinate currently being moved toward. -Coord STDMETHODCALLTYPE LocomotionClass::Head_To_Coord(void) +Coord LocomotionClass::Head_To_Coord(void) { return(LinkedTo->PositionCoord); } @@ -613,7 +520,7 @@ Coord STDMETHODCALLTYPE LocomotionClass::Head_To_Coord(void) /// The base locomotor never carries its object anywhere, so it always answers no. /// /// bool; Is the object moving? -boolean STDMETHODCALLTYPE LocomotionClass::Is_Moving(void) +bool LocomotionClass::Is_Moving(void) { return(false); } @@ -625,7 +532,7 @@ boolean STDMETHODCALLTYPE LocomotionClass::Is_Moving(void) /// Only locomotors that tilt their object with the ground need to act on it. /// /// The ramp type of the slope the object should now conform to. -void STDMETHODCALLTYPE LocomotionClass::Force_New_Slope(int ramp) +void LocomotionClass::Force_New_Slope(int ramp) { } @@ -636,7 +543,7 @@ void STDMETHODCALLTYPE LocomotionClass::Force_New_Slope(int ramp) /// currently traveling. The base locomotor has no preference. /// /// Returns with the drawing code, or zero for the ordinary presentation. -int STDMETHODCALLTYPE LocomotionClass::Drawing_Code(void) +int LocomotionClass::Drawing_Code(void) { return(0); } @@ -648,7 +555,7 @@ int STDMETHODCALLTYPE LocomotionClass::Drawing_Code(void) /// will override this routine. The base locomotor never stands in the way. /// /// Returns with the reason firing is disallowed, or FIRE_OK if it is permitted. -FireErrorType STDMETHODCALLTYPE LocomotionClass::Can_Fire(void) +FireErrorType LocomotionClass::Can_Fire(void) { return(FIRE_OK); } @@ -660,14 +567,8 @@ FireErrorType STDMETHODCALLTYPE LocomotionClass::Can_Fire(void) /// visible motion differs from the object's logical speed will override this routine. /// /// Returns with the apparent speed of the linked object. -int STDMETHODCALLTYPE LocomotionClass::Apparent_Speed(void) +int LocomotionClass::Apparent_Speed(void) { return(LinkedTo->Current_Speed()); } - -/// Unlike the other interface identifiers, this one is defined in the locomotion module. -#define INITGUID -#undef DEFINE_GUID -#include -#include "iloco_i.c" diff --git a/code/loco.h b/code/loco.h index ba9f5f627..24ae81d76 100644 --- a/code/loco.h +++ b/code/loco.h @@ -10,71 +10,80 @@ #pragma once #include "coord.h" -#include "ilocos.h" +#include "classids.h" +#include "iloco.h" +#include "persist.h" class FootClass; class SaveStreamClass; -class LocomotionClass : public IPersistStream, public ILocomotion +// The class identifier of a locomotor reached through its locomotion interface, or +// all zero when it is not one of ours. +ClassID Locomotion_Class_ID(ILocomotion * locomotion); + +// A new, unlinked locomotor of the registered class, or nothing when the identifier +// names no locomotor. +std::unique_ptr Create_Locomotor(ClassID const & classid); + +// The locomotor whose record is next in the stream, or nothing when the record names +// something that is not one, which fails the stream. +std::unique_ptr Load_Locomotor(SaveStreamClass & stream); + + +class LocomotionClass : public IPersistent, public ILocomotion { public: LocomotionClass(void); virtual ~LocomotionClass(void); - virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID *ppvObj) override; - virtual ULONG STDMETHODCALLTYPE AddRef() override; - virtual ULONG STDMETHODCALLTYPE Release() override; - - virtual LONG STDMETHODCALLTYPE IsDirty(void) override {return(Dirty ? S_OK : S_FALSE);} - virtual HRESULT STDMETHODCALLTYPE Load(IStream * stream) override; - virtual HRESULT STDMETHODCALLTYPE Save(IStream * stream, BOOL cleardirty) override; - virtual LONG STDMETHODCALLTYPE GetSizeMax(ULARGE_INTEGER *pcbSize) override; - - virtual HRESULT STDMETHODCALLTYPE Link_To_Object(void *object) override; - virtual boolean STDMETHODCALLTYPE Is_Moving(void) override; - virtual Coord STDMETHODCALLTYPE Destination(void) override; - virtual Coord STDMETHODCALLTYPE Head_To_Coord(void) override; - virtual MoveType STDMETHODCALLTYPE Can_Enter_Cell(Cell cell) override; - virtual boolean STDMETHODCALLTYPE Is_To_Have_Shadow(void) override; - virtual Matrix3D STDMETHODCALLTYPE Draw_Matrix(int *key) override; - virtual Matrix3D STDMETHODCALLTYPE Shadow_Matrix(int *key) override; - virtual Point2D STDMETHODCALLTYPE Draw_Point(void) override; - virtual Point2D STDMETHODCALLTYPE Shadow_Point(void) override; - virtual VisualType STDMETHODCALLTYPE Visual_Character(boolean flag) override; - virtual int STDMETHODCALLTYPE Z_Adjust(void) override; - virtual ZGradientType STDMETHODCALLTYPE Z_Gradient(void) override; - virtual boolean STDMETHODCALLTYPE Process(void) override; - virtual void STDMETHODCALLTYPE Move_To(Coord to) override; - virtual void STDMETHODCALLTYPE Stop_Moving(void) override; - virtual void STDMETHODCALLTYPE Do_Turn(DirType coord) override; - virtual void STDMETHODCALLTYPE Unlimbo(void) override; - virtual void STDMETHODCALLTYPE Tilt_Pitch_AI(void) override; - virtual boolean STDMETHODCALLTYPE Power_On(void) override; - virtual boolean STDMETHODCALLTYPE Power_Off(void) override; - virtual boolean STDMETHODCALLTYPE Is_Powered(void) override; - virtual boolean STDMETHODCALLTYPE Is_Ion_Sensitive(void) override; - virtual boolean STDMETHODCALLTYPE Push(DirType dir) override; - virtual boolean STDMETHODCALLTYPE Shove(DirType dir) override; - virtual void STDMETHODCALLTYPE Force_Track(int track, Coord coord) override; - virtual void STDMETHODCALLTYPE Force_Immediate_Destination(Coord coord) override; - virtual void STDMETHODCALLTYPE Force_New_Slope(int ramp) override; - virtual boolean STDMETHODCALLTYPE Is_Moving_Now(void) override {return(Is_Moving());} - virtual int STDMETHODCALLTYPE Apparent_Speed(void) override; - virtual int STDMETHODCALLTYPE Drawing_Code(void) override; - virtual FireErrorType STDMETHODCALLTYPE Can_Fire(void) override; - virtual int STDMETHODCALLTYPE Get_Status() override {return(0);} - virtual void STDMETHODCALLTYPE Acquire_Hunter_Seeker_Target(void) override {} - virtual boolean STDMETHODCALLTYPE Is_Surfacing() override {return(false);} - virtual void STDMETHODCALLTYPE Mark_All_Occupation_Bits(int mark) override {} - virtual boolean STDMETHODCALLTYPE Is_Moving_Here(Coord to) override {return(false);} - virtual boolean STDMETHODCALLTYPE Will_Jump_Tracks(void) override {return(false);} - virtual boolean STDMETHODCALLTYPE Is_Really_Moving_Now(void) override {return(Is_Moving_Now());} - virtual void STDMETHODCALLTYPE Stop_Movement_Animation(void) override {} - virtual void STDMETHODCALLTYPE Lock(void) override {} - virtual void STDMETHODCALLTYPE Unlock(void) override {} - virtual int STDMETHODCALLTYPE Get_Track_Number(void) override {return(-1);} - virtual int STDMETHODCALLTYPE Get_Track_Index(void) override {return(-1);} - virtual int STDMETHODCALLTYPE Get_Speed_Accum(void) override {return(-1);} + virtual bool Load(SaveStreamClass & stream) override; + virtual bool Save(SaveStreamClass & stream, bool cleardirty) override; + + virtual void Link_To_Object(void *object) override; + virtual bool Is_Moving(void) override; + virtual Coord Destination(void) override; + virtual Coord Head_To_Coord(void) override; + virtual MoveType Can_Enter_Cell(Cell cell) override; + virtual bool Is_To_Have_Shadow(void) override; + virtual Matrix3D Draw_Matrix(int *key) override; + virtual Matrix3D Shadow_Matrix(int *key) override; + virtual Point2D Draw_Point(void) override; + virtual Point2D Shadow_Point(void) override; + virtual VisualType Visual_Character(bool flag) override; + virtual int Z_Adjust(void) override; + virtual ZGradientType Z_Gradient(void) override; + virtual bool Process(void) override; + virtual void Move_To(Coord to) override; + virtual void Stop_Moving(void) override; + virtual void Do_Turn(DirType coord) override; + virtual void Unlimbo(void) override; + virtual void Tilt_Pitch_AI(void) override; + virtual bool Power_On(void) override; + virtual bool Power_Off(void) override; + virtual bool Is_Powered(void) override; + virtual bool Is_Ion_Sensitive(void) override; + virtual bool Push(DirType dir) override; + virtual bool Shove(DirType dir) override; + virtual void Force_Track(int track, Coord coord) override; + virtual void Force_Immediate_Destination(Coord coord) override; + virtual void Force_New_Slope(int ramp) override; + virtual bool Is_Moving_Now(void) override {return(Is_Moving());} + virtual int Apparent_Speed(void) override; + virtual int Drawing_Code(void) override; + virtual FireErrorType Can_Fire(void) override; + virtual int Get_Status() override {return(0);} + virtual void Acquire_Hunter_Seeker_Target(void) override {} + virtual bool Is_Surfacing() override {return(false);} + virtual void Mark_All_Occupation_Bits(int mark) override {} + virtual bool Is_Moving_Here(Coord to) override {return(false);} + virtual bool Will_Jump_Tracks(void) override {return(false);} + virtual bool Is_Really_Moving_Now(void) override {return(Is_Moving_Now());} + virtual void Stop_Movement_Animation(void) override {} + virtual void Lock(void) override {} + virtual void Unlock(void) override {} + virtual int Get_Track_Number(void) override {return(-1);} + virtual int Get_Track_Index(void) override {return(-1);} + virtual int Get_Speed_Accum(void) override {return(-1);} /* @@ -85,9 +94,9 @@ class LocomotionClass : public IPersistStream, public ILocomotion virtual void Serialize(SaveStreamClass & stream); /* - * Restores whatever the record could not carry. Load_Members calls this once the - * members are in place, so a base class fixup runs even when the load was entered - * through a derived class. + * Restores whatever the record could not carry. Load_Object calls this once the + * record has been checked, so a locomotor never takes its place while its record + * is still in doubt. */ virtual void Post_Load(void); @@ -98,8 +107,8 @@ class LocomotionClass : public IPersistStream, public ILocomotion * from its Load and Save; the record is the swizzle identity followed by whatever * members the class names. */ - HRESULT Save_Members(IStream * stream, BOOL cleardirty); - HRESULT Load_Members(IStream * stream); + bool Save_Members(SaveStreamClass & stream, bool cleardirty); + bool Load_Members(SaveStreamClass & stream); protected: /* @@ -121,11 +130,4 @@ class LocomotionClass : public IPersistStream, public ILocomotion * persistence machinery never assumes a locomotor is already safely on disk. */ bool Dirty; - - /* - * This is the number of outstanding references to this locomotor. Releasing the - * last one destroys the locomotor, which is how its lifetime is managed through - * the COM interfaces it presents. - */ - LONG RefCount; }; diff --git a/code/logic.cpp b/code/logic.cpp index 2935b2a1e..3d1129fe3 100644 --- a/code/logic.cpp +++ b/code/logic.cpp @@ -79,12 +79,6 @@ #include -/* - * Global COM reference count. - */ -ULONG COMRefCount = 0; - - unsigned FramesThisSecond=0; unsigned LastFramesPerSecond=0; unsigned TotalFrames=0; diff --git a/code/mainloop.cpp b/code/mainloop.cpp index c3e70e9f3..90c0b7e9a 100644 --- a/code/mainloop.cpp +++ b/code/mainloop.cpp @@ -37,6 +37,7 @@ #include "fog.h" #include "globals.h" #include "goptions.h" +#include "hostclock.h" #include "ipxmgr.h" #include "language/language.h" #include "logic.h" @@ -60,6 +61,7 @@ #include "theme.h" #include "timer.h" #include "tracker.h" +#include "ui/uishell.h" #include "bench.hh" #include "special.hh" @@ -160,10 +162,10 @@ static void Check_For_Focus_Loss(void) { while (!GameInFocus) { if (Session.Type == GAME_NORMAL || Session.Type == GAME_SKIRMISH) { - Sleep(500); + Host_Sleep(500); Windows_Message_Handler(); } else { - Sleep(10); + Host_Sleep(10); Windows_Message_Handler(); break; } @@ -208,10 +210,10 @@ bool Main_Loop(void) #else while (!GameInFocus) { if (Session.Type == GAME_NORMAL || Session.Type == GAME_SKIRMISH) { - Sleep(500); + Host_Sleep(500); Windows_Message_Handler(); } else { - Sleep(10); + Host_Sleep(10); Windows_Message_Handler(); break; } @@ -228,7 +230,7 @@ bool Main_Loop(void) // // Initialize our AI processing timer // - Session.ProcessTimer = timeGetTime();/// TickCount; + Session.ProcessTimer = Host_Milliseconds();/// TickCount; if (Session.TrapCheckHeap) { Debug_Trap_Check_Heap = true; @@ -292,7 +294,12 @@ bool Main_Loop(void) } } } else { - FrameTimer = Options.GameSpeed; + /* + * A game speed of zero is the "fastest" setting, which asks for no delay at all. The + * period display paced the loop on its own; a modern one does not, so hold the same + * 60 frames a second the network pacing in Queue_AI already reads that setting as. + */ + FrameTimer = std::max(1, Options.GameSpeed); } /* @@ -300,6 +307,7 @@ bool Main_Loop(void) */ if (!Session.Play) { if (SpecialDialog == SDLG_NONE && GameInFocus) { + UI_Tick(); Map.Input(input, x, y); if (input) { Keyboard_Process(input); @@ -347,7 +355,7 @@ bool Main_Loop(void) // // Measure how long it took to process the AI // - Session.ProcessTicks += std::min(1000, (timeGetTime() - Session.ProcessTimer)); // (TickCount - Session.ProcessTimer) + Session.ProcessTicks += std::min(1000, (Host_Milliseconds() - Session.ProcessTimer)); // (TickCount - Session.ProcessTimer) Session.ProcessFrames++; /* @@ -598,13 +606,13 @@ void Sync_Delay(void) TacticalMap->AI(); Map.Render(); } else { - Sleep(0); + Host_Sleep(0); } if (!NetFrameTimer()) { break; } } - Sleep(0); + Host_Sleep(0); } } else { while (FrameTimer) { @@ -621,9 +629,9 @@ void Sync_Delay(void) } } if (GameInFocus || (Session.Type != GAME_NORMAL && Session.Type != GAME_SKIRMISH)) { - Sleep(0); + Host_Sleep(0); } else { - Sleep(16 * FrameTimer); + Host_Sleep(16 * FrameTimer); } } } diff --git a/code/mainopt.cpp b/code/mainopt.cpp index 4534159ad..8b4aceee3 100644 --- a/code/mainopt.cpp +++ b/code/mainopt.cpp @@ -29,23 +29,21 @@ #include "mixfile.h" #include "msgbox.h" #include "newmenu.h" -#include "ownrdraw.h" #include "sidebar.h" #include "sounddlg.h" #include "stimer.h" #include "surface.h" +#include "winstub.h" #include "wwmouse.h" +#include "ui/uidisplayconfirm.h" +#include "ui/uidisplayoptions.h" +#include "ui/uimainoptions.h" #include "color.hh" -INT_PTR CALLBACK Main_Options_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); -INT_PTR CALLBACK Display_Options_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); bool Change_Display_Mode(int width, int height); bool Test_Display_Mode_Dialog(int width, int height); -INT_PTR CALLBACK Test_Display_Mode_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); - -GameOptionsClass TempOptions; /// @@ -57,126 +55,73 @@ GameOptionsClass TempOptions; /// Game logic is suspended for the duration of this routine. void Main_Options_Dialog(void) { - bool old_game_active = GameActive; - GameActive = false; - - HWND main_handle; - LONG main_rc; - - HWND in_handle; - LONG in_rc; + UIMainOptionsPresenterClass screen; + screen.Begin(); + screen.Refresh(); while (true) { - do { - main_rc = -1; - main_handle = OwnerDraw::Begin_Dialog(IDD_OPT_MAIN, Main_Options_Dialog_Proc); - } while (main_handle == 0); - SetWindowLongPtr(main_handle, DWLP_USER, (LONG_PTR)&main_rc); - - OwnerDraw::Move_Dialog(main_handle, -1, (HiddenSurface->Get_Height() - 400) / 2 + 147); - OwnerDraw::Display_Dialog(main_handle); - - while (main_rc < 0) { - if (OwnerDraw::Dialog_Message_Handler() == true) { - break; - } - Title_Screen_Restore(); - } + // The screen is opened again on each pass round the family, so what the last close + // left behind is cleared first. + screen.Result.reset(); + screen.IsClosing = false; + screen.Choice = UIMainOptionsPresenterClass::CHOICE_NONE; - OwnerDraw::End_Dialog(main_handle); - - switch (main_rc) { - case IDC_OPTMAIN_SOUND: - SoundControlsClass().Dialog(); - break; - - case IDC_OPTMAIN_DISPLAY: { - while (true) { - do { - TempOptions = Options; - in_rc = -1; - in_handle = OwnerDraw::Begin_Dialog(IDD_OPT_DISPLAY, Display_Options_Dialog_Proc); - } while (in_handle == 0); - SetWindowLongPtr(in_handle, DWLP_USER, (LONG_PTR)&in_rc); - OwnerDraw::Display_Dialog(in_handle); - - while (in_rc < 0) { - if (OwnerDraw::Dialog_Message_Handler() == true) { - break; - } - Title_Screen_Restore(); - } - - OwnerDraw::End_Dialog(in_handle); - - if (in_rc != 1) { - break; - } - if (TempOptions.ScreenWidth == Options.ScreenWidth && TempOptions.ScreenHeight == Options.ScreenHeight) { - break; - } - - if (WWMessageBox().Process(TXT_ABOUT_TO_TRY_MODE, TXT_OK, TXT_CANCEL) == 0) { - if (!Test_Display_Mode_Dialog(TempOptions.ScreenWidth, TempOptions.ScreenHeight)) { - continue; - } - Options.ScreenWidth = TempOptions.ScreenWidth; - Options.ScreenHeight = TempOptions.ScreenHeight; - } - - break; - } - } + if (UI_Main_Options_Screen(screen).Outcome == UIResult::OUTCOME_FAILED_TO_OPEN) { break; + } - case IDC_OPTMAIN_KEYBOARD: - Options.Hotkey_Dialog(); - break; - - case IDC_OPTMAIN_GAME_SETTINGS: - GameControlsClass().Dialog(); - break; - - default: - Options.Save_Settings(); - GameActive = old_game_active; - return; + if (screen.Exits()) { + break; } + + // The sub-screen runs with this one gone, which is the coexistence rule the driver + // already kept. + screen.Run_Pending(); } + + screen.End(); } /// -/// Handles the main options dialog. -/// This routine reports the button the player pressed back to the options dialog driver so -/// that it can bring up the appropriate sub dialog. The sound button is disabled when there -/// is no audio hardware to talk to. +/// Brings up the display options and offers a chosen resolution as a trial. +/// A mode the player refuses, or does not answer for, brings the screen straight back up +/// with the old resolution in force; anything else leaves. /// -INT_PTR CALLBACK Main_Options_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) +void Display_Options_Dialog(void) { - int *result; - HWND handle; + while (true) { + UIDisplayOptionsPresenterClass screen; + screen.Refresh(); - INT_PTR rc = OwnerDraw::Default_Dialog_Proc(window, message, wparam, lparam); - if (rc == 0) { - result = (int *)GetWindowLongPtr(window, DWLP_USER); - switch (message) { + if (UI_Display_Options_Screen(screen).Outcome == UIResult::OUTCOME_FAILED_TO_OPEN) { + break; + } - case WM_COMMAND: - *result = LOWORD(wparam); - break; + if (screen.Choice != UIDisplayOptionsPresenterClass::CHOICE_ACCEPT) { + break; + } - case WM_INITDIALOG: - handle = GetDlgItem(window, IDC_OPTMAIN_SOUND); - if (handle) { - EnableWindow(handle, AudioEngine.Is_Available()); - } - break; + // The window mode is not staged and is not offered as a trial: the player can see + // at once whether the screen is covered, and the frame is unchanged either way. + if (screen.Fullscreen != Options.Fullscreen) { + Options.Fullscreen = screen.Fullscreen; + Set_Window_Fullscreen(Options.Fullscreen); + } + if (!screen.Wants_Mode_Change()) { + break; } - return(0); + + if (WWMessageBox().Process(TXT_ABOUT_TO_TRY_MODE, TXT_OK, TXT_CANCEL) == 0) { + if (!Test_Display_Mode_Dialog(screen.StagedWidth, screen.StagedHeight)) { + continue; + } + screen.Commit(); + } + + break; } - return(rc); } @@ -310,6 +255,22 @@ bool Change_Display_Mode(int width, int height) } +// Leaves the tried mode in place or puts the old one back, whichever view answered. +static bool Keep_Or_Reset_Display_Mode(int width, int height, bool accepted) +{ + if (!accepted) { + DebugString("Resetting display mode @ %dx%d\n", Options.ScreenWidth, Options.ScreenHeight); + Change_Display_Mode(Options.ScreenWidth, Options.ScreenHeight); + LogicalSurface = HiddenSurface; + return(false); + } + + DebugString("Keeping display mode @ %dx%d\n", width, height); + LogicalSurface = HiddenSurface; + return(true); +} + + /// /// Tries a display mode out and asks the player to confirm it. /// This routine switches to the requested mode and puts up a confirmation dialog. If the @@ -321,8 +282,6 @@ bool Change_Display_Mode(int width, int height) /// bool; Was the new display mode accepted and left in place? bool Test_Display_Mode_Dialog(int width, int height) { - int rc = -1; - DebugString("Testing display mode @ %dx%d\n", width, height); Hide_Mouse(); HiddenSurface->Fill(TBLACK); @@ -337,173 +296,13 @@ bool Test_Display_Mode_Dialog(int width, int height) Show_Mouse(); Draw_Menu_Background(); - HWND dialog = OwnerDraw::Begin_Dialog(IDD_OPT_CONFIRM_MODE, Test_Display_Mode_Dialog_Proc); - if (dialog) { - SetWindowLongPtr(dialog, DWLP_USER, (LONG_PTR)&rc); - OwnerDraw::Display_Dialog(dialog); - - CDTimerClass timer = 10 * TIMER_SECOND; - while (rc < 0) { - if (OwnerDraw::Dialog_Message_Handler() == true) { - break; - } - Title_Screen_Restore(); - if (timer <= 0) { - PostMessage(dialog, WM_COMMAND, WM_DESTROY, 0); - timer = 5 * TIMER_SECOND; - } - } - - OwnerDraw::End_Dialog(dialog); - if (rc != IDOK) { - DebugString("Resetting display mode @ %dx%d\n", Options.ScreenWidth, Options.ScreenHeight); - Change_Display_Mode(Options.ScreenWidth, Options.ScreenHeight); - LogicalSurface = HiddenSurface; - return(false); - } - } - - DebugString("Keeping display mode @ %dx%d\n", width, height); - LogicalSurface = HiddenSurface; - return(true); -} + UIDisplayConfirmPresenterClass screen; + screen.Refresh(); + // A mode whose confirmation could not be shown is refused, because the screen it would + // have been read on may be the unreadable one. + bool const accepted = UI_Display_Confirm_Screen(screen).Outcome != UIResult::OUTCOME_FAILED_TO_OPEN + && screen.Choice == UIDisplayConfirmPresenterClass::CHOICE_ACCEPT; -/// -/// Handles the mode confirmation dialog. -/// This routine records the button the player pressed so that the mode test can tell -/// whether the new resolution was accepted or rejected. -/// -INT_PTR CALLBACK Test_Display_Mode_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - int * result; - int id; - - INT_PTR rc = OwnerDraw::Default_Dialog_Proc(window, message, wparam, lparam); - if (rc == 0) { - result = (int *)GetWindowLongPtr(window, DWLP_USER); - switch (message) { - case WM_COMMAND: - id = LOWORD(wparam); - if (id > 0 && id <= IDCANCEL) { - *result = LOWORD(wparam); - } - break; - } - return(0); - } - return(rc); -} - - -/// -/// Handles the display options dialog messages. -/// This routine fills the resolution list with the display modes the hardware reports, -/// remembers which one the player picked, and tracks the movie stretching preference. The -/// chosen resolution is staged in the temporary options so that it can be tested before -/// being made permanent. -/// -static __forceinline BOOL Display_Options_Dialog_Body(HWND window, UINT message, WPARAM wparam) -{ - enum { - MIN_WIDTH = 640, - MIN_HEIGHT = 400, - MAX_WIDTH = 4096, - MAX_HEIGHT = 4096, - }; - - static int * _modes = NULL; - static int _current_mode = -1; - static int _previous_mode = -1; - static bool _initialized = true; - - int * result = (int *)GetWindowLongPtr(window, DWLP_USER); - switch (message) { - case WM_COMMAND: - switch (LOWORD(wparam)) { - default: - return(0); - - case IDC_DISPLAY_RESLIST: { - HWND list = GetDlgItem(window, IDC_DISPLAY_RESLIST); - _current_mode = ListBox_GetCurSel(list); - } - return(0); - - case IDOK: { - if (_previous_mode != _current_mode) { - Center_Window_Within_Window(window, MainWindow); - HWND list = GetDlgItem(window, IDC_DISPLAY_RESLIST); - if (list) { - int index = ListBox_GetItemData(list, _current_mode); - int * modes = &_modes[2 * index]; - TempOptions.ScreenWidth = modes[0]; - TempOptions.ScreenHeight = modes[1]; - } - } - HWND button = GetDlgItem(window, IDC_STRETCH_MOVIES); - if (button) { - Options.StretchMovies = Button_GetCheck(button) == BST_CHECKED; - } - } - break; - - case IDCANCEL: - break; - } - delete [] _modes; - *result = LOWORD(wparam); - break; - - case WM_INITDIALOG: { - HWND list = GetDlgItem(window, IDC_DISPLAY_RESLIST); - _modes = EnumDisplayModes(MIN_WIDTH, MIN_HEIGHT, MAX_WIDTH, MAX_HEIGHT); - int * modes = _modes; - int item_index = 0; - int initial_mode = -1; - int mode_index = 0; - if (modes != NULL) { - while (*modes != 0) { - int width = *modes++; - int height = *modes++; - if (width == TempOptions.ScreenWidth && height == TempOptions.ScreenHeight) { - initial_mode = mode_index; - } - char buffer[64]; - sprintf(buffer, "%d x %d", width, height); - int index = ListBox_AddString(list, buffer); - ListBox_SetItemData(list, index, item_index); - mode_index++; - item_index++; - } - } - ListBox_SetCurSel(list, initial_mode); - _initialized = true; - _current_mode = initial_mode; - _previous_mode = initial_mode; - - HWND button = GetDlgItem(window, IDC_STRETCH_MOVIES); - if (button) { - Button_SetCheck(button, Options.StretchMovies != false); - } - } - break; - - } - return(0); -} - - -/// -/// Handles the display options dialog. -/// This routine gives the owner draw dialog system first refusal on the message and only -/// deals with what it leaves behind. -/// -INT_PTR CALLBACK Display_Options_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - INT_PTR rc = OwnerDraw::Default_Dialog_Proc(window, message, wparam, lparam); - if (rc == 0) { - return(Display_Options_Dialog_Body(window, message, wparam)); - } - return(rc); + return(Keep_Or_Reset_Display_Mode(width, height, accepted)); } diff --git a/code/mainopt.h b/code/mainopt.h index 10c4a7726..9bf10cefb 100644 --- a/code/mainopt.h +++ b/code/mainopt.h @@ -11,3 +11,8 @@ bool Change_Display_Mode(int width, int height); void Main_Options_Dialog(void); + +// The display options and the mode trial the family reaches through them. The loop stays +// with the driver, because only a view knows how to bring the screen back up after a mode +// the player refused. +void Display_Options_Dialog(void); diff --git a/code/mapgen.cpp b/code/mapgen.cpp index a60e88511..4e5762f7a 100644 --- a/code/mapgen.cpp +++ b/code/mapgen.cpp @@ -42,7 +42,6 @@ #include "netshare.h" #include "nodes.h" #include "overtype.h" -#include "ownrdraw.h" #include "pcx.h" #include "progress.h" #include "rules.h" @@ -54,6 +53,8 @@ #include "terrtype.h" #include "tiberium.h" #include "trigtype.h" +#include "ui/uimapgen.h" +#include "ui/uishell.h" #include "unit.h" #include "unittype.h" #include "vector.h" @@ -70,7 +71,6 @@ bool (*RMGCallback)() = MapGen_Call_Back; -INT_PTR CALLBACK Map_Seed_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); double Random_Fraction(void); @@ -3254,41 +3254,19 @@ MapGeneratorClass::~MapGeneratorClass(void) /// skirmish or multiplayer game begins. It does not return until the player accepts the map /// or gives up on it, and the title screen behind is kept alive in the meantime. /// -/// Progress callback to run while the dialog is up. -/// Returns with the dialog result -- 1 if the player accepted the map, 2 if the -/// dialog was canceled, and 0 if it could not be opened at all. +/// Progress callback to run while the screen is up. +/// Returns with the screen's result -- 1 if the player accepted the map, 2 if the +/// screen was canceled, and 0 if it could not be opened at all. int Do_Random_Map_Dialog(bool (*callback)()) { - WDTTerritory *wdt = NULL; LONG res = 0; - if (Session.Type == GAME_INTERNET && Session.IsWDT) { - wdt = WDT_Get_Territory(Session.WDTTerritory); - } - - HWND dialog; - if (Addon_Enabled(ADDON_FIRESTORM)) { - dialog = OwnerDraw::Begin_Dialog(wdt != NULL ? IDD_MAPGEN_WDT : IDD_MAPGEN_FS, Map_Seed_Dialog_Proc); - } else { - dialog = OwnerDraw::Begin_Dialog(IDD_MAPGEN, Map_Seed_Dialog_Proc); - } + UIMapGenPresenterClass screen; + screen.Open(callback); - if (dialog) { - RMGCallback = callback; - RandomMapGen.SeedData.Callback = callback; - SetWindowLongPtrA(dialog, DWLP_USER, (LONG_PTR)&res); - OwnerDraw::Display_Dialog(dialog); - while (res == 0) { - if (OwnerDraw::Dialog_Message_Handler() == 1) { - break; - } - if (callback != NULL) { - callback(); - } - Title_Screen_Restore(false); - } - OwnerDraw::End_Dialog(dialog); - } + RMGCallback = callback; + RandomMapGen.SeedData.Callback = callback; + res = UI_MapGen_Run(screen); RMGCallback = MapGen_Call_Back; RandomMapGen.SeedData.Callback = NULL; @@ -3395,603 +3373,6 @@ void Clean_Up_RMCache(void) } -/// -/// Generates the random map the player has asked for. -/// This routine is what the map generator dialog's preview and generate buttons come down -/// to. A map that has been built for these exact settings before is kept in a cache and -/// merely fetched back, so flipping between two seeds costs nothing the second time. The -/// finished preview is left in RandMap.img for the lobby to show. -/// -/// The dialog to show generation progress within. -/// Progress callback to run while the map is being built. -void Do_Random_Map(HWND dialog, bool (*callback)()) -{ - if (Session.Type == GAME_INTERNET && Session.IsWDT && WDT_Get_Territory(Session.WDTTerritory) != NULL) { - RandomMapGen.SeedData.NumPlayers = 4; - } - char *digest = CalcRandomMapDigest(); - char name[128]; - memset(name, 0, sizeof(name)); - strncpy(name, "rmcache\\", sizeof(name)); - strncat(name, digest, sizeof(name)); - delete digest; - strncat(name, ".mmp", sizeof(name)); - DebugString("Cache filename is %s\n", name); - CCFileClass cfile(name); - - if (cfile.Is_Available()) { - if (RandomMapGen.MapPreview == NULL) { - RandomMapGen.MapPreview = new MapPreviewClass; - } - if (RandomMapGen.MapPreview->Read_PCX_Preview(name)) { - RawFileClass file("RandMap.img"); - Write_PCX_File(file, *RandomMapGen.MapPreview->Get_Preview_Surface(), &GamePalette); - delete RandomMapGen.MapPreview; - RandomMapGen.MapPreview = NULL; - return; - } - - delete RandomMapGen.MapPreview; - RandomMapGen.MapPreview = NULL; - } - - RMGCallback = callback; - RandomMapGen.SeedData.Callback = callback; - if (RandomMapGen.SeedData.Seed == -1) { - RandomMapGen.SeedData.Seed = Sim_Random_Pick(0U, 65535U); - } - RandomMapGen.Generate_Random_Map(true, dialog); - RandomMapGen.MapPreview->Create_Preview(); - - if (RandomMapGen.MapSeeder != NULL) { - delete RandomMapGen.MapSeeder; - } - - RandomMapGen.MapSeeder = new MapSeedClass; - memcpy(RandomMapGen.MapSeeder, &RandomMapGen.SeedData, sizeof(RandomMapGen.SeedData)); - - Scen->Set_Scenario_Name(Fetch_String(TXT_RANDOM_MAP_DESCRIPTION)); - Title_Screen_Restore(); - - if (RandomMapGen.MapPreview != NULL) { - if (RandomMapGen.MapPreview->Get_Preview_Surface() != NULL) { - RawFileClass file("RandMap.img"); - Write_PCX_File(file, *RandomMapGen.MapPreview->Get_Preview_Surface(), &GamePalette); - WIN32_FIND_DATA ff; - if (FindFirstFile("rmcache", &ff) == INVALID_HANDLE_VALUE) { - CreateDirectory("rmcache", 0); - } - CopyFile("RandMap.img", name, FALSE); - Clean_Up_RMCache(); - } - delete RandomMapGen.MapPreview; - RandomMapGen.MapPreview = NULL; - } - - if (RandomMapGen.MapSeeder != NULL) { - delete RandomMapGen.MapSeeder; - RandomMapGen.MapSeeder = NULL; - } -} - - -/// -/// Dialog procedure for the random map generator ("Map Seed") dialog. -/// Handles previewing, generating, saving, loading and deleting random maps, and randomizing -/// the generator settings. The dialog's result code is written through the DWLP_USER -/// pointer set up by Do_Random_Map_Dialog so that writing it ends that dialog's modal -/// message loop. -/// -/// Handle to the dialog window. -/// Window message identifier. -/// Message-specific first parameter. -/// Message-specific second parameter. -/// TRUE if the message was processed, FALSE otherwise. -INT_PTR CALLBACK Map_Seed_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - static int _unused = -1; - - INT_PTR result = OwnerDraw::Default_Dialog_Proc(window, message, wparam, lparam); - if (result) { - return(result); - } - - LONG * state = (LONG *)GetWindowLongPtrA(window, DWLP_USER); - - switch (message) { - - /* - * Repaint the map preview, if one exists. - */ - case WM_PAINT: - if (RandomMapGen.MapPreview != NULL) { - RandomMapGen.MapPreview->Blit_Preview(window); - } - ValidateRect(window, NULL); - return(0); - - /* - * Initialize the dialog controls from the current seed settings. - */ - case WM_INITDIALOG: { - _unused = -1; - HWND handle = GetDlgItem(window, IDC_MAPGEN_PREVIEW); - if (Debug_Map) { - EnableWindow(handle, false); - } else { - EnableWindow(handle, true); - } - if (RandomMapGen.SeedData.Seed == -1) { - RandomMapGen.SeedData.Seed = Sim_Random_Pick(0U, 65535U); - } - RandomMapGen.SeedData.Set_Settings(window); - - bool enable = RandomMapGen.SeedData.Files_Present(); - handle = GetDlgItem(window, IDC_MAPGEN_LOAD_MAP); - if (handle != NULL) { - EnableWindow(handle, enable); - } - handle = GetDlgItem(window, IDC_MAPGEN_DELETE_MAP); - if (handle == NULL) { - return(0); - } - EnableWindow(handle, enable); - return(0); - } - - case WM_COMMAND: - switch (LOWORD(wparam)) { - - /* - * Generate the map and accept the dialog. - */ - case IDOK: - RandomMapGen.SeedData.Get_Settings(window); - if (Debug_Map) { - RandomMapGen.Generate_Random_Map(false, window); - Scen->Set_Scenario_Name(Fetch_String(TXT_RANDOM_MAP_DESCRIPTION)); - Write_Scenario_INI("RandMap.Map", true); - } else { - if (RandomMapGen.MapPreview == NULL || RandomMapGen.MapPreview->Get_Preview_Surface() == NULL) { - RandomMapGen.Generate_Random_Map(true, window); - if (Debug_Map) { - Scen->Set_Scenario_Name(Fetch_String(TXT_RANDOM_MAP_DESCRIPTION)); - Write_Scenario_INI("RandMap.Map", true); - } - } - } - *state = 1; - return(1); - - /* - * Cancel the dialog. - */ - case IDCANCEL: - *state = 2; - return(1); - - /* - * Load a saved map seed. - */ - case IDC_MAPGEN_LOAD_MAP: - RandomMapGen.SeedData.Get_Settings(window); - if (RandomMapGen.SeedData.LoadOptionsClass::Load() == true) { - PostMessageA(window, WM_COMMAND, MAKEWPARAM(IDC_MAPGEN_PREVIEW, BN_CLICKED), (LPARAM)GetDlgItem(window, IDC_MAPGEN_PREVIEW)); - } - RandomMapGen.SeedData.Set_Settings(window); - return(0); - - /* - * Save the current map seed. - */ - case IDC_MAPGEN_SAVE_MAP: { - RandomMapGen.SeedData.Get_Settings(window); - RandomMapGen.SeedData.MapDescription[0] = '\0'; - RandomMapGen.SeedData.LoadOptionsClass::Save(RandomMapGen.SeedData.MapDescription); - - bool enable = RandomMapGen.SeedData.Files_Present(); - HWND handle = GetDlgItem(window, IDC_MAPGEN_LOAD_MAP); - if (handle != NULL) { - EnableWindow(handle, enable); - } - handle = GetDlgItem(window, IDC_MAPGEN_DELETE_MAP); - if (handle == NULL) { - return(0); - } - EnableWindow(handle, enable); - return(0); - } - - /* - * Delete the saved map seed. - */ - case IDC_MAPGEN_DELETE_MAP: { - RandomMapGen.SeedData.Get_Settings(window); - RandomMapGen.SeedData.LoadOptionsClass::Delete(); - - bool enable = RandomMapGen.SeedData.Files_Present(); - HWND handle = GetDlgItem(window, IDC_MAPGEN_LOAD_MAP); - if (handle != NULL) { - EnableWindow(handle, enable); - } - handle = GetDlgItem(window, IDC_MAPGEN_DELETE_MAP); - if (handle == NULL) { - return(0); - } - EnableWindow(handle, enable); - return(0); - } - - /* - * Build and display a preview of the current map seed. - */ - case IDC_MAPGEN_PREVIEW: - RandomMapGen.SeedData.Get_Settings(window); - RandomMapGen.Generate_Random_Map(true, window); - RandomMapGen.MapPreview->Create_Preview(); - if (RandomMapGen.MapSeeder != NULL) { - delete RandomMapGen.MapSeeder; - } - RandomMapGen.MapSeeder = new MapSeedClass; - memcpy(RandomMapGen.MapSeeder, &RandomMapGen.SeedData, sizeof(MapSeedClass)); - PostMessageA(window, WM_PAINT, 0, 0); - return(0); - - /* - * Randomize the generator settings. - */ - case IDC_MAPGEN_SURPRISE: - RandomMapGen.SeedData.Get_Settings(window); - RandomMapGen.SeedData.Randomize(); - RandomMapGen.SeedData.Set_Settings(window); - return(0); - - default: - return(0); - } - - default: - return(0); - } -} - - -/// -/// Reads the map generator dialog into these settings. -/// This routine is called before a preview or a generate, so that whatever the player has -/// dialed in on the controls becomes the seed the generator works from. The settings taken -/// off the dialog are run through Fixup_Settings, so an impossible combination can never -/// reach the generator. The Firestorm settings are cleared away when that addon is absent. -/// -/// The map generator dialog to read. -void MapSeedClass::Get_Settings(HWND dialog) -{ - WDTTerritory * wdt = NULL; - if (Session.Type == GAME_INTERNET && Session.IsWDT) { - wdt = WDT_Get_Territory(Session.WDTTerritory); - } - - HWND handle; - char str[30]; - - handle = GetDlgItem(dialog, IDC_MAPGEN_ENVIRONMENT); - Biome = ComboBox_GetItemData(handle, ComboBox_GetCurSel(handle)); - - handle = GetDlgItem(dialog, IDC_MAPGEN_TIME_OF_DAY); - Time = ComboBox_GetItemData(handle, ComboBox_GetCurSel(handle)); - - handle = GetDlgItem(dialog, IDC_MAPGEN_MAP_WIDTH); - Width = ComboBox_GetItemData(handle, ComboBox_GetCurSel(handle)); - - handle = GetDlgItem(dialog, IDC_MAPGEN_MAP_HEIGHT); - Height = ComboBox_GetItemData(handle, ComboBox_GetCurSel(handle)); - - handle = GetDlgItem(dialog, IDC_MAPGEN_DIMENSION_EDIT); - GetWindowText(handle, str, ARRAY_SIZE(str)); - Seed = atoi(str); - - handle = GetDlgItem(dialog, IDC_MAPGEN_TIBERIUM_AMOUNT); - Tiberium = Slider_GetPos(handle); - - if (wdt != NULL) { - NumPlayers = 4; - } else { - handle = GetDlgItem(dialog, IDC_MAPGEN_PLAYERS); - NumPlayers = Slider_GetPos(handle); - } - - handle = GetDlgItem(dialog, IDC_MAPGEN_HILLS); - Hills = Slider_GetPos(handle); - - handle = GetDlgItem(dialog, IDC_MAPGEN_WATER); - WaterAmount = Slider_GetPos(handle); - - handle = GetDlgItem(dialog, IDC_MAPGEN_CLIFFS); - Cliffs = Slider_GetPos(handle); - - handle = GetDlgItem(dialog, IDC_MAPGEN_VEGETATION); - Vegetation = Slider_GetPos(handle); - - handle = GetDlgItem(dialog, IDC_MAPGEN_CITIES); - Cities = Slider_GetPos(handle); - - handle = GetDlgItem(dialog, IDC_MAPGEN_ACCESSIBILITY); - Accessibility = Slider_GetPos(handle); - - handle = GetDlgItem(dialog, IDC_MAPGEN_TIBERIUM_FIELDS); - TiberiumLayout = Slider_GetPos(handle); - - TiberiumWildlife = 0; - VeinholeMonsters = 0; - UseIonStorms = false; - UseTransitions = false; - UseBlueTiberium = false; - - if (Addon_Enabled(ADDON_FIRESTORM)) { - handle = GetDlgItem(dialog, IDC_MAPGEN_LIFEFORMS); - if (handle != NULL) { - TiberiumWildlife = Button_GetCheck(handle) == BST_CHECKED ? 30 : 0; - } - - handle = GetDlgItem(dialog, IDC_MAPGEN_VEINHOLES); - if (handle != NULL) { - VeinholeMonsters = Slider_GetPos(handle); - } - - handle = GetDlgItem(dialog, IDC_MAPGEN_ION_STORMS); - if (handle != NULL) { - UseIonStorms = Button_GetCheck(handle) == BST_CHECKED; - } - - handle = GetDlgItem(dialog, IDC_MAPGEN_TRANSITIONS); - if (handle != NULL) { - UseTransitions = Button_GetCheck(handle) == BST_CHECKED; - } - - UseBlueTiberium = (double)Tiberium > 0.75; - } - - Fixup_Settings(); -} - - -/// -/// Fills the map generator dialog in from these settings. -/// This routine is the counterpart of Get_Settings, and is called whenever the dialog must -/// show a different set of options -- when it first appears, after a randomize, and after a -/// load. In a tournament game the controls are further restricted, or locked outright, to -/// whatever the territory permits the player to meddle with. -/// -/// The map generator dialog to fill in. -void MapSeedClass::Set_Settings(HWND dialog) -{ - static char _win_name[24]; - - static int _biome_names[BIOME_COUNT] = { - TXT_BIOME_TUNDRA, - TXT_BIOME_TAIGA, - TXT_BIOME_TEMPERATE, - TXT_BIOME_DESERT, - TXT_BIOME_MUTATED - }; - - static int _time_names[TIME_OF_DAY_COUNT] = { - TXT_TIME_MORNING, - TXT_TIME_AFTERNOON, - TXT_TIME_DUSK, - TXT_TIME_NIGHT - }; - - static int _map_size_names[MAPSIZE_COUNT] = { - TXT_MAPSIZE_SMALL, - TXT_MAPSIZE_MEDIUM, - TXT_MAPSIZE_LARGE, - TXT_MAPSIZE_VERY_LARGE - }; - - WDTTerritory *wdt = NULL; - if (Session.Type == GAME_INTERNET && Session.IsWDT) { - wdt = WDT_Get_Territory(Session.WDTTerritory); - } - - Fixup_Settings(); - - HWND handle; - LRESULT item; - int i; - - handle = GetDlgItem(dialog, IDC_MAPGEN_ENVIRONMENT); - while (SendMessageA(handle, CB_GETCOUNT, 0, 0) > 0) { - SendMessageA(handle, CB_DELETESTRING, 0, 0); - } - for (i = BIOME_FIRST; i < BIOME_COUNT; i++) { - if (i != BIOME_MUTATED || Addon_Enabled(ADDON_FIRESTORM)) { - item = SendMessageA(handle, CB_ADDSTRING, 0, (LPARAM)Fetch_String(_biome_names[i])); - SendMessageA(handle, CB_SETITEMDATA, item, i); - } - } - item = SendMessageA(handle, CB_FINDSTRING, 0, (LPARAM)Fetch_String(_biome_names[Biome])); - SendMessageA(handle, CB_SETCURSEL, item, 0); - - handle = GetDlgItem(dialog, IDC_MAPGEN_TIME_OF_DAY); - while (SendMessageA(handle, CB_GETCOUNT, 0, 0) > 0) { - SendMessageA(handle, CB_DELETESTRING, 0, 0); - } - for (i = TIME_OF_DAY_FIRST; i < TIME_OF_DAY_COUNT; i++) { - item = SendMessageA(handle, CB_ADDSTRING, 0, (LPARAM)Fetch_String(_time_names[i])); - SendMessageA(handle, CB_SETITEMDATA, item, i); - } - item = SendMessageA(handle, CB_FINDSTRING, 0, (LPARAM)Fetch_String(_time_names[Time])); - SendMessageA(handle, CB_SETCURSEL, item, 0); - - handle = GetDlgItem(dialog, IDC_MAPGEN_MAP_WIDTH); - while (SendMessageA(handle, CB_GETCOUNT, 0, 0) > 0) { - SendMessageA(handle, CB_DELETESTRING, 0, 0); - } - for (i = 0; i < MAPSIZE_COUNT; i++) { - item = SendMessageA(handle, CB_ADDSTRING, 0, (LPARAM)Fetch_String(_map_size_names[i])); - SendMessageA(handle, CB_SETITEMDATA, item, i); - } - item = SendMessageA(handle, CB_FINDSTRING, 0, (LPARAM)Fetch_String(_map_size_names[Width])); - SendMessageA(handle, CB_SETCURSEL, item, 0); - - handle = GetDlgItem(dialog, IDC_MAPGEN_MAP_HEIGHT); - while (SendMessageA(handle, CB_GETCOUNT, 0, 0) > 0) { - SendMessageA(handle, CB_DELETESTRING, 0, 0); - } - for (i = 0; i < MAPSIZE_COUNT; i++) { - item = SendMessageA(handle, CB_ADDSTRING, 0, (LPARAM)Fetch_String(_map_size_names[i])); - SendMessageA(handle, CB_SETITEMDATA, item, i); - } - item = SendMessageA(handle, CB_FINDSTRING, 0, (LPARAM)Fetch_String(_map_size_names[Height])); - SendMessageA(handle, CB_SETCURSEL, item, 0); - - handle = GetDlgItem(dialog, IDC_MAPGEN_DIMENSION_EDIT); - sprintf(_win_name, "%d", Seed); - SetWindowTextA(handle, _win_name); - if (wdt != NULL) { - EnableWindow(handle, wdt->UserModSeed ? TRUE : FALSE); - } - - handle = GetDlgItem(dialog, IDC_MAPGEN_TIBERIUM_AMOUNT); - if (wdt != NULL) { - Set_Scroll_Bar(handle, wdt->TiberiumAmountMin, wdt->TiberiumAmountMax, Tiberium, wdt->UserModTiberiumAmount); - - CheckDlgButton(dialog, IDC_WDT_1ON1, FALSE); - CheckDlgButton(dialog, IDC_WDT_2ON2, TRUE); - EnableWindow(GetDlgItem(dialog, IDC_WDT_1ON1), FALSE); - EnableWindow(GetDlgItem(dialog, IDC_WDT_2ON2), FALSE); - } else { - Set_Scroll_Bar(handle, 1, 100, Tiberium, TRUE); - - handle = GetDlgItem(dialog, IDC_MAPGEN_PLAYERS); - Set_Scroll_Bar(handle, 2, MAX_PLAYERS, NumPlayers, TRUE); - } - - handle = GetDlgItem(dialog, IDC_MAPGEN_HILLS); - if (wdt != NULL) { - Set_Scroll_Bar(handle, wdt->HillsMin, wdt->HillsMax, Hills, wdt->UserModHills); - } else { - Set_Scroll_Bar(handle, 0, 100, Hills, TRUE); - } - - handle = GetDlgItem(dialog, IDC_MAPGEN_WATER); - if (wdt != NULL) { - Set_Scroll_Bar(handle, wdt->WaterMin, wdt->WaterMax, WaterAmount, wdt->UserModWater); - } else { - Set_Scroll_Bar(handle, 0, 100, WaterAmount, TRUE); - } - - handle = GetDlgItem(dialog, IDC_MAPGEN_CLIFFS); - if (wdt != NULL) { - Set_Scroll_Bar(handle, wdt->CliffsMin, wdt->CliffsMax, Cliffs, wdt->UserModCliffs); - } else { - Set_Scroll_Bar(handle, 0, 100, Cliffs, TRUE); - } - - handle = GetDlgItem(dialog, IDC_MAPGEN_VEGETATION); - if (wdt != NULL) { - Set_Scroll_Bar(handle, wdt->VegetationMin, wdt->VegetationMax, Vegetation, wdt->UserModVegetation); - } else { - Set_Scroll_Bar(handle, 0, 100, Vegetation, TRUE); - } - - handle = GetDlgItem(dialog, IDC_MAPGEN_CITIES); - if (wdt != NULL) { - Set_Scroll_Bar(handle, wdt->CitiesMin, wdt->CitiesMax, Cities, wdt->UserModCities); - } else { - Set_Scroll_Bar(handle, 0, 100, Cities, TRUE); - } - - handle = GetDlgItem(dialog, IDC_MAPGEN_TIBERIUM_FIELDS); - if (wdt != NULL) { - Set_Scroll_Bar(handle, wdt->TiberiumFieldsMin, wdt->TiberiumFieldsMax, TiberiumLayout, wdt->UserModTiberiumFields); - } else { - Set_Scroll_Bar(handle, 0, 100, TiberiumLayout, TRUE); - } - - handle = GetDlgItem(dialog, IDC_MAPGEN_ACCESSIBILITY); - if (wdt != NULL) { - Set_Scroll_Bar(handle, wdt->AccessibilityMin, wdt->AccessibilityMax, Accessibility, wdt->UserModAccessability); - - EnableWindow(GetDlgItem(dialog, IDC_MAPGEN_ENVIRONMENT), wdt->UserModBiome ? TRUE : FALSE); - EnableWindow(GetDlgItem(dialog, IDC_MAPGEN_TIME_OF_DAY), wdt->UserModTime ? TRUE : FALSE); - EnableWindow(GetDlgItem(dialog, IDC_MAPGEN_MAP_WIDTH), wdt->UserModWidth ? TRUE : FALSE); - EnableWindow(GetDlgItem(dialog, IDC_MAPGEN_MAP_HEIGHT), wdt->UserModHeight ? TRUE : FALSE); - - Set_Checkbox(GetDlgItem(dialog, IDC_MAPGEN_LIFEFORMS), TiberiumWildlife > 0, wdt->UserModTiberiumCreatures); - - Set_Scroll_Bar(GetDlgItem(dialog, IDC_MAPGEN_VEINHOLES), 0, 5, VeinholeMonsters, wdt->UserModVeinholeMonsters); - - Set_Checkbox(GetDlgItem(dialog, IDC_MAPGEN_TRANSITIONS), UseTransitions, wdt->UserModTimeTransitions); - - Set_Checkbox(GetDlgItem(dialog, IDC_MAPGEN_ION_STORMS), UseIonStorms, TRUE); - - if (!wdt->UserModBiome && !wdt->UserModTime && !wdt->UserModCliffs && !wdt->UserModAccessability && - !wdt->UserModHills && !wdt->UserModTiberiumAmount && !wdt->UserModTiberiumFields && !wdt->UserModWater && - !wdt->UserModVegetation && !wdt->UserModCities && !wdt->UserModWidth && !wdt->UserModHeight && - !wdt->UserModVeinholeMonsters) { - EnableWindow(GetDlgItem(dialog, IDC_MAPGEN_SURPRISE), FALSE); - } - - } else { - - Set_Scroll_Bar(handle, 0, 100, Accessibility, TRUE); - - Set_Checkbox(GetDlgItem(dialog, IDC_MAPGEN_LIFEFORMS), TiberiumWildlife > 0, TRUE); - - Set_Scroll_Bar(GetDlgItem(dialog, IDC_MAPGEN_VEINHOLES), 0, 5, VeinholeMonsters, TRUE); - - Set_Checkbox(GetDlgItem(dialog, IDC_MAPGEN_TRANSITIONS), UseTransitions, TRUE); - - Set_Checkbox(GetDlgItem(dialog, IDC_MAPGEN_ION_STORMS), UseIonStorms, TRUE); - } - - InvalidateRect(dialog, 0, 0); -} - - -/// -/// Sets up one of the map generation sliders. -/// This routine is used by Set_Settings to point a slider at the span of values its setting -/// is permitted to take. A setting with nothing left to choose between is shown disabled -/// rather than hidden, so the dialog keeps its shape. -/// -/// The slider control to set up. -/// The lowest value the slider may be dragged to. -/// The highest value the slider may be dragged to. -/// Where the thumb should sit. -/// Should the player be allowed to move this slider? -void MapSeedClass::Set_Scroll_Bar(HWND handle, unsigned int min, unsigned int max, int position, bool enable) -{ - if (max <= min) { - EnableWindow(handle, FALSE); - Slider_SetRange(handle, 0, 100); - Slider_SetPos(handle, position); - } else { - EnableWindow(handle, enable); - Slider_SetRange(handle, min, max); - Slider_SetPos(handle, position); - } -} - - -/// -/// Sets up one of the map generation checkboxes. -/// This routine is the companion of Set_Scroll_Bar, and is used by Set_Settings to show a -/// setting the dialog offers as a simple yes or no. A setting the player is not allowed to -/// touch is shown disabled rather than hidden, so the dialog keeps its shape. -/// -/// The checkbox control to set up. -/// Should the box be shown checked? -/// Should the player be allowed to change this setting? -void MapSeedClass::Set_Checkbox(HWND handle, bool state, bool enable) -{ - Button_SetCheck(handle, state != 0); - Button_Enable(handle, enable); -} - - /// /// Rolls a fresh set of map generation settings. /// This routine is what the dialog's randomize button calls, handing the player a whole new @@ -4703,8 +4084,16 @@ double Sample_Truncated_Normal(double mean, double scale, double lower_bound, do /// /// Should the scenario be rebuilt from scratch and the preview redrawn /// between phases? -/// The map generator dialog to repaint as the preview is refreshed. -void MapGeneratorClass::Generate_Random_Map(bool full_init, HWND dialog) +/// +/// Puts the freshly drawn preview on screen while a map is being built. +/// +static void Repaint_Map_Preview(void) +{ + UI_MapGen_Preview_Changed(); +} + + +void MapGeneratorClass::Generate_Random_Map(bool full_init) { if (RMGCallback != NULL) RMGCallback(); @@ -4731,7 +4120,7 @@ void MapGeneratorClass::Generate_Random_Map(bool full_init, HWND dialog) if (full_init) { RandomMapGen.MapPreview->Create_Preview(); - SendMessage(dialog, WM_PAINT, 0, 0); + Repaint_Map_Preview(); } if (RMGCallback != NULL) RMGCallback(); @@ -4748,7 +4137,7 @@ void MapGeneratorClass::Generate_Random_Map(bool full_init, HWND dialog) if (full_init) { RandomMapGen.MapPreview->Create_Preview(); - SendMessage(dialog, WM_PAINT, 0, 0); + Repaint_Map_Preview(); } if (RMGCallback != NULL) RMGCallback(); @@ -4764,7 +4153,7 @@ void MapGeneratorClass::Generate_Random_Map(bool full_init, HWND dialog) if (full_init) { RandomMapGen.MapPreview->Create_Preview(); - SendMessage(dialog, WM_PAINT, 0, 0); + Repaint_Map_Preview(); } if (RMGCallback != NULL) RMGCallback(); @@ -4791,7 +4180,7 @@ void MapGeneratorClass::Generate_Random_Map(bool full_init, HWND dialog) if (full_init) { RandomMapGen.MapPreview->Create_Preview(); - SendMessage(dialog, WM_PAINT, 0, 0); + Repaint_Map_Preview(); } if (RMGCallback != NULL) RMGCallback(); @@ -4852,7 +4241,7 @@ void MapGeneratorClass::Generate_Random_Map(bool full_init, HWND dialog) if (full_init) { RandomMapGen.MapPreview->Create_Preview(); - SendMessage(dialog, WM_PAINT, 0, 0); + Repaint_Map_Preview(); } if (RMGCallback != NULL) RMGCallback(); @@ -4871,7 +4260,7 @@ void MapGeneratorClass::Generate_Random_Map(bool full_init, HWND dialog) if (full_init) { RandomMapGen.MapPreview->Create_Preview(); - SendMessage(dialog, WM_PAINT, 0, 0); + Repaint_Map_Preview(); } if (RMGCallback != NULL) RMGCallback(); @@ -4895,7 +4284,7 @@ void MapGeneratorClass::Generate_Random_Map(bool full_init, HWND dialog) if (full_init) { RandomMapGen.MapPreview->Create_Preview(); - SendMessage(dialog, WM_PAINT, 0, 0); + Repaint_Map_Preview(); } if (RMGCallback != NULL) RMGCallback(); @@ -4918,7 +4307,7 @@ void MapGeneratorClass::Generate_Random_Map(bool full_init, HWND dialog) if (full_init) { RandomMapGen.MapPreview->Create_Preview(); - SendMessage(dialog, WM_PAINT, 0, 0); + Repaint_Map_Preview(); } ScenarioInit--; diff --git a/code/mapgen.h b/code/mapgen.h index e72914c36..7b3f0da0e 100644 --- a/code/mapgen.h +++ b/code/mapgen.h @@ -352,10 +352,6 @@ class MapSeedClass : public LoadOptionsClass /* * Dialog interaction. */ - void Get_Settings(HWND dialog); - void Set_Settings(HWND dialog); - void Set_Scroll_Bar(HWND handle, unsigned int min, unsigned int max, int position, bool enable); - void Set_Checkbox(HWND handle, bool state, bool enable); /* * Settings adjustment. @@ -497,7 +493,7 @@ class MapGeneratorClass /* * Top-level generation and housekeeping. */ - void Generate_Random_Map(bool full_init, HWND dialog); + void Generate_Random_Map(bool full_init); void Init_Map(bool full_init); void Cleanup(void); void Update_Progress(int percent_progress); @@ -683,7 +679,6 @@ inline bool My_In_Radar(Cell const &cell) y + x <= MapRegionClass::MapEndDiagonal) ? true : false; } -void Do_Random_Map(HWND, bool (*callback)()); int Do_Random_Map_Dialog(bool (*callback)()); extern MapRegionClass::CellData *RMGCellData; diff --git a/code/mech.cpp b/code/mech.cpp index ee1d17cd5..0ddf517a6 100644 --- a/code/mech.cpp +++ b/code/mech.cpp @@ -56,7 +56,7 @@ MechLocomotionClass::~MechLocomotionClass(void) /// Has the mech been given somewhere to walk to? /// /// bool; Is the mech under movement orders? -boolean STDMETHODCALLTYPE MechLocomotionClass::Is_Moving(void) +bool MechLocomotionClass::Is_Moving(void) { return(IsMoving); } @@ -67,7 +67,7 @@ boolean STDMETHODCALLTYPE MechLocomotionClass::Is_Moving(void) /// /// Returns with the destination assigned, or COORD_NONE if the unit has not been /// given one. -Coord STDMETHODCALLTYPE MechLocomotionClass::Destination(void) +Coord MechLocomotionClass::Destination(void) { if (Is_Moving()) { return(DestinationCoord); @@ -81,7 +81,7 @@ Coord STDMETHODCALLTYPE MechLocomotionClass::Destination(void) /// /// Returns with the location being stepped into, or the unit's own location if it /// is not part way between cells. -Coord STDMETHODCALLTYPE MechLocomotionClass::Head_To_Coord(void) +Coord MechLocomotionClass::Head_To_Coord(void) { if (HeadToCoord != COORD_NONE) { return(HeadToCoord); @@ -95,7 +95,7 @@ Coord STDMETHODCALLTYPE MechLocomotionClass::Head_To_Coord(void) /// This is the locomotor's entry point from the owning unit's AI. /// /// bool; Does the mech still have somewhere to walk to? -boolean STDMETHODCALLTYPE MechLocomotionClass::Process(void) +bool MechLocomotionClass::Process(void) { Movement_AI(true); return(Is_Moving()); @@ -108,7 +108,7 @@ boolean STDMETHODCALLTYPE MechLocomotionClass::Process(void) /// raised to the deck above it, since that is where a walking unit can actually get to. /// /// The location to walk to. -void STDMETHODCALLTYPE MechLocomotionClass::Move_To(Coord to) +void MechLocomotionClass::Move_To(Coord to) { if (LinkedTo->StunDuration <= 0) { Coord coord = to; @@ -126,7 +126,7 @@ void STDMETHODCALLTYPE MechLocomotionClass::Move_To(Coord to) /// A unit caught part way between cells is left in motion so that it finishes the step it /// is taking before coming to rest. /// -void STDMETHODCALLTYPE MechLocomotionClass::Stop_Moving(void) +void MechLocomotionClass::Stop_Moving(void) { DestinationCoord = COORD_NONE; if (HeadToCoord == COORD_NONE) { @@ -155,7 +155,7 @@ void MechLocomotionClass::Do_Turn(DirType coord) /// destination -- it will walk there and then pick its path up again. /// /// The location to step into immediately. -void STDMETHODCALLTYPE MechLocomotionClass::Force_Immediate_Destination(Coord coord) +void MechLocomotionClass::Force_Immediate_Destination(Coord coord) { HeadToCoord = coord; } @@ -651,18 +651,9 @@ bool MechLocomotionClass::Mark_Head_To(Coord const & coord) } -/// -/// Fetches the class identifier of this locomotor. -/// The persistence layer uses this identifier to create a locomotor of the right kind -/// when a saved game is loaded. -/// -/// Pointer to the buffer to fill in with the class identifier. -/// Returns with S_OK, or E_POINTER if no buffer was supplied. -HRESULT STDMETHODCALLTYPE MechLocomotionClass::GetClassID(CLSID * retval) +ClassID MechLocomotionClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_MechLocomotion; - return(S_OK); + return(ClassID_MechLocomotion); } @@ -684,7 +675,7 @@ void MechLocomotionClass::Serialize(SaveStreamClass & stream) /// Fetches the display layer that the mech is rendered in. /// /// Returns with the layer appropriate to a unit that walks on the ground. -LayerType STDMETHODCALLTYPE MechLocomotionClass::In_Which_Layer(void) +LayerType MechLocomotionClass::In_Which_Layer(void) { return(LAYER_GROUND); } @@ -696,7 +687,7 @@ LayerType STDMETHODCALLTYPE MechLocomotionClass::In_Which_Layer(void) /// standing still -- blocked, or waiting on a path -- is not moving now. /// /// bool; Is the mech turning or walking right now? -boolean STDMETHODCALLTYPE MechLocomotionClass::Is_Moving_Now(void) +bool MechLocomotionClass::Is_Moving_Now(void) { if (LinkedTo->PrimaryFacing.Is_Rotating()) { return(true); @@ -714,7 +705,7 @@ boolean STDMETHODCALLTYPE MechLocomotionClass::Is_Moving_Now(void) /// it is walking toward stays reserved for it. /// /// The marking operation to perform; MARK_UP releases the cell. -void STDMETHODCALLTYPE MechLocomotionClass::Mark_All_Occupation_Bits(int mark) +void MechLocomotionClass::Mark_All_Occupation_Bits(int mark) { if (mark == MARK_UP) { LinkedTo->Clear_Occupy_Bit((Coord)Head_To_Coord()); @@ -731,7 +722,7 @@ void STDMETHODCALLTYPE MechLocomotionClass::Mark_All_Occupation_Bits(int mark) /// /// The location to test against. /// bool; Is the mech heading into that location? -boolean STDMETHODCALLTYPE MechLocomotionClass::Is_Moving_Here(Coord to) +bool MechLocomotionClass::Is_Moving_Here(Coord to) { Coord coord = Head_To_Coord(); diff --git a/code/mech.h b/code/mech.h index 9a0a1cab6..cb7a6b6ec 100644 --- a/code/mech.h +++ b/code/mech.h @@ -27,22 +27,22 @@ class MechLocomotionClass : public LocomotionClass MechLocomotionClass(void); virtual ~MechLocomotionClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; - virtual boolean STDMETHODCALLTYPE Is_Moving(void) override; - virtual Coord STDMETHODCALLTYPE Destination(void) override; - virtual Coord STDMETHODCALLTYPE Head_To_Coord(void) override; - virtual boolean STDMETHODCALLTYPE Process(void) override; - virtual void STDMETHODCALLTYPE Move_To(Coord to) override; - virtual void STDMETHODCALLTYPE Stop_Moving(void) override; - virtual void STDMETHODCALLTYPE Do_Turn(DirType coord) override; - virtual LayerType STDMETHODCALLTYPE In_Which_Layer(void) override; - virtual void STDMETHODCALLTYPE Force_Immediate_Destination(Coord coord) override; - virtual boolean STDMETHODCALLTYPE Is_Moving_Now(void) override; - virtual void STDMETHODCALLTYPE Mark_All_Occupation_Bits(int mark) override; - virtual boolean STDMETHODCALLTYPE Is_Moving_Here(Coord to) override; + virtual bool Is_Moving(void) override; + virtual Coord Destination(void) override; + virtual Coord Head_To_Coord(void) override; + virtual bool Process(void) override; + virtual void Move_To(Coord to) override; + virtual void Stop_Moving(void) override; + virtual void Do_Turn(DirType coord) override; + virtual LayerType In_Which_Layer(void) override; + virtual void Force_Immediate_Destination(Coord coord) override; + virtual bool Is_Moving_Now(void) override; + virtual void Mark_All_Occupation_Bits(int mark) override; + virtual bool Is_Moving_Here(Coord to) override; /*--------------------------------------------------------------------- ** Member function prototypes. diff --git a/code/milsectmr.cpp b/code/milsectmr.cpp index f77953f99..093bc71df 100644 --- a/code/milsectmr.cpp +++ b/code/milsectmr.cpp @@ -13,6 +13,7 @@ #include "dbgprint.h" #include "getcpu.h" +#include "hostclock.h" #include "mpu.h" #include "win.h" @@ -29,8 +30,9 @@ /// /// Creates the millisecond timer and works out how to drive it. /// This routine will ask the processor for its clock rate so that the cycle counter can be -/// scaled into milliseconds. Machines that will not report a rate fall back to the Windows -/// multimedia timer, whose resolution is raised to one millisecond for the life of the timer. +/// scaled into milliseconds. Machines that will not report a rate fall back to the host +/// clock instead. The resolution raised here no longer sharpens that reading, but the +/// request is process wide and the game's waits are rounded up to whatever is in force. /// MillisecondTimerClass::MillisecondTimerClass(void) { @@ -39,7 +41,10 @@ MillisecondTimerClass::MillisecondTimerClass(void) unsigned int low = Get_CPU_Rate(high); if (low == 0 && high == 0) { + // Windows only; no other host has a resolution to bid for. +#ifdef _WIN32 timeBeginPeriod(PERIOD_RESOLUTION); +#endif } else { double dl = low; @@ -61,7 +66,10 @@ MillisecondTimerClass::MillisecondTimerClass(void) MillisecondTimerClass::~MillisecondTimerClass(void) { if (Frequency != 1.0) { + // Windows only; no other host has a resolution to bid for. +#ifdef _WIN32 timeEndPeriod(PERIOD_RESOLUTION); +#endif } } @@ -70,8 +78,7 @@ MillisecondTimerClass::~MillisecondTimerClass(void) /// Fetches the current time, expressed in milliseconds. /// This routine is used every time the timer is read. The processor's own cycle counter /// supplies the value when the machine is new enough to have one, since it is both cheaper -/// and finer grained than the system timer. Otherwise the Windows multimedia timer is -/// consulted instead. +/// and finer grained than the host clock. Otherwise the host clock is read instead. /// /// Returns with the current time in milliseconds. MillisecondTimerClass::operator double () const @@ -93,5 +100,5 @@ MillisecondTimerClass::operator double () const return(LI_TO_DBL(dh, dl) / Frequency); } - return(timeGetTime()); + return(Host_Milliseconds()); } diff --git a/code/milsectmr.h b/code/milsectmr.h index 3228f85d2..45dc6b0bc 100644 --- a/code/milsectmr.h +++ b/code/milsectmr.h @@ -22,7 +22,7 @@ class MillisecondTimerClass /* * This is the number of processor clock cycles that pass in one millisecond, and * the raw cycle count is divided by it to yield a time. If it is 1.0, then the - * processor would not report its rate and the multimedia timer is read instead. + * processor would not report its rate and the host clock is read instead. */ double Frequency; }; diff --git a/code/mixfile.cpp b/code/mixfile.cpp index 9a5db0d37..e9c2fa55c 100644 --- a/code/mixfile.cpp +++ b/code/mixfile.cpp @@ -69,7 +69,7 @@ ** with the mixfile system. */ //template -List MixFileClass::List; +List MixFileClass::MixList; /// template class MixFileClass; @@ -185,7 +185,7 @@ MixFileClass::MixFileClass(char const * filename, PKey const * key) : /* ** Attach to list of mixfiles. */ - List.Add_Tail(this); + MixList.Add_Tail(this); } @@ -303,7 +303,7 @@ void const * MixFileClass::Retrieve(char const * filename) *=============================================================================================*/ MixFileClass * MixFileClass::Finder(char const * filename) { - MixFileClass * ptr = List.First(); + MixFileClass * ptr = MixList.First(); while (ptr->Is_Valid()) { char path[_MAX_PATH]; char name[_MAX_FNAME]; @@ -534,7 +534,10 @@ bool MixFileClass::Offset(char const * filename, void ** realptr, MixFileClass * */ /// Can't call strupr on a const string. - int crc = (CRCEngine()(strupr((char *)filename), strlen(filename))); //Calculate_CRC(strupr((char *)filename), strlen(filename)); + char filename_upper[_MAX_PATH]; + strcpy(filename_upper, filename); + strupr(filename_upper); + int crc = (CRCEngine()(filename_upper, strlen(filename_upper))); //Calculate_CRC(strupr((char *)filename), strlen(filename)); SubBlock key; key.CRC = crc; @@ -542,7 +545,7 @@ bool MixFileClass::Offset(char const * filename, void ** realptr, MixFileClass * /* ** Sweep through all registered mixfiles, trying to find the file in question. */ - ptr = List.First(); + ptr = MixList.First(); while (ptr->Is_Valid()) { SubBlock * block; @@ -560,7 +563,10 @@ bool MixFileClass::Offset(char const * filename, void ** realptr, MixFileClass * if (realptr != NULL && ptr->Data != NULL) { *realptr = (char *)ptr->Data + block->Offset; } - if (ptr->Data == NULL && offset != NULL) { + // The block's own offset is measured from the data section. The offset reported + // here is measured from the start of the archive file, cached or not, because + // that is the only thing a caller can seek to in the file it then opens. + if (offset != NULL) { *offset += ptr->DataStart; } return(true); diff --git a/code/mixfile.h b/code/mixfile.h index 12af50954..af937829f 100644 --- a/code/mixfile.h +++ b/code/mixfile.h @@ -20,6 +20,8 @@ #include "listnode.h" #include +#include +#include class PKey; @@ -79,11 +81,14 @@ class MixFileClass : public Node */ #pragma pack(1) struct FileHeader { - short count; - int size; + std::int16_t count; + std::int32_t size; }; #pragma pack() + static_assert(sizeof(FileHeader) == 6, "Mixfile header layout changed"); + static_assert(offsetof(FileHeader, size) == 2, "Mixfile header layout changed"); + /* ** The number of files within the mixfile. */ @@ -111,5 +116,5 @@ class MixFileClass : public Node */ void * Data; // Pointer to raw data. - static List List; + static List MixList; }; diff --git a/code/mouse.cpp b/code/mouse.cpp index ea7bdc390..8d07ec161 100644 --- a/code/mouse.cpp +++ b/code/mouse.cpp @@ -52,6 +52,7 @@ #include "mixfile.h" #include "overtype.h" #include "rawfile.h" +#include "saveload.h" #include "savestream.h" #include "scenario.h" #include "shapeset.h" @@ -392,17 +393,17 @@ void MouseClass::Init_Clear(void) /// back into it. Object pointers within the restored state are remapped by the swizzle /// manager, and the theater specific type data is reinitialized to match the scenario. /// -/// Returns with S_OK if the map was loaded, otherwise the stream error. -HRESULT MouseClass::Load(IStream * stream) +/// bool; Was the record read whole? +bool MouseClass::Load(SaveStreamClass & stream) { int i; - HRESULT result = BASECLASS::Load(stream); - if (SUCCEEDED(result)) { + bool result = BASECLASS::Load(stream); + if (result) { int theater; - result = stream->Read(&theater, sizeof(theater), NULL); - if (FAILED(result)) { - return(result); + stream.Serialize(theater); + if (stream.Was_Error()) { + return(false); } LastTheater = THEATER_NONE; @@ -435,12 +436,10 @@ HRESULT MouseClass::Load(IStream * stream) Array.Clear(); - SaveStreamClass savestream(stream, SaveStreamClass::MODE_LOAD); - savestream.Set_Context("MouseClass"); - Serialize(savestream); - result = savestream.Result(); - if (FAILED(result)) { - return(result); + stream.Set_Context("MouseClass"); + Serialize(stream); + if (stream.Was_Error()) { + return(false); } /* @@ -478,23 +477,22 @@ HRESULT MouseClass::Load(IStream * stream) SubzoneConnectionHashTable[i] = new SUBZONE_CONNECTION_HASH_SET(20, 256, SubzoneHash); } - result = stream->Read(CellZones, sizeof(*CellZones) * CellZoneCount, NULL); - if (FAILED(result)) { - return(result); + stream.Serialize_Bytes(CellZones, (int)(sizeof(*CellZones) * CellZoneCount)); + if (stream.Was_Error()) { + return(false); } for (i = 0; i < MZONE_COUNT; i++) { Zones[i] = new unsigned short[ZoneCount]; - result = stream->Read(Zones[i], sizeof(unsigned short) * ZoneCount, NULL); - if (FAILED(result)) { - return(result); + stream.Serialize_Bytes(Zones[i], (int)(sizeof(unsigned short) * ZoneCount)); + if (stream.Was_Error()) { + return(false); } } - savestream.Serialize(ZoneConnections); - result = savestream.Result(); - if (FAILED(result)) { - return(result); + stream.Serialize(ZoneConnections); + if (stream.Was_Error()) { + return(false); } for (i = 0; i < Array.Length(); i++) { @@ -502,13 +500,14 @@ HRESULT MouseClass::Load(IStream * stream) Array[i] = NULL; } int count; - result = stream->Read(&count, sizeof(count), NULL); - if (FAILED(result)) { - return(result); + stream.Serialize(count); + if (stream.Was_Error()) { + return(false); } for (i = 0; i < count; i++) { - LPVOID ptr; - OleLoadFromStream(stream, IID_IUnknown, &ptr); + if (Load_Object(stream) == NULL) { + return(false); + } } TerrainTypeClass::Init(Scen->Theater); @@ -525,7 +524,7 @@ HRESULT MouseClass::Load(IStream * stream) DraggedWaypoint = NULL; LastTheater = Scen->Theater; - result = S_OK; + result = true; } return(result); } @@ -535,45 +534,42 @@ HRESULT MouseClass::Load(IStream * stream) /// Saves the map layer to a save game stream. /// This routine writes the theater, the members of the whole display chain, the zone tables /// and zone connections, and then every valid cell, in the order that Load expects to find -/// them. The cells persist themselves through OLE, so each one writes its own contents. +/// them. Each cell writes its own contents as a record of its own. /// -/// Returns with S_OK if the map was written, otherwise the stream error. -HRESULT MouseClass::Save(IStream * stream) +/// bool; Was the record written whole? +bool MouseClass::Save(SaveStreamClass & stream) { int i; int count; - HRESULT result = BASECLASS::Save(stream); - if (SUCCEEDED(result)) { + bool result = BASECLASS::Save(stream); + if (result) { int theater = Scen->Theater; - result = stream->Write(&theater, sizeof(theater), NULL); - if (FAILED(result)) { - return(result); + stream.Serialize(theater); + if (stream.Was_Error()) { + return(false); } - SaveStreamClass savestream(stream, SaveStreamClass::MODE_SAVE); - Serialize(savestream); - result = savestream.Result(); - if (FAILED(result)) { - return(result); + Serialize(stream); + if (stream.Was_Error()) { + return(false); } - result = stream->Write(CellZones, sizeof(*CellZones) * CellZoneCount, NULL); - if (FAILED(result)) { - return(result); + stream.Serialize_Bytes(CellZones, (int)(sizeof(*CellZones) * CellZoneCount)); + if (stream.Was_Error()) { + return(false); } for (i = 0; i < MZONE_COUNT; i++) { - result = stream->Write(Zones[i], sizeof(unsigned short) * ZoneCount, NULL); - if (FAILED(result)) { - return(result); + stream.Serialize_Bytes(Zones[i], (int)(sizeof(unsigned short) * ZoneCount)); + if (stream.Was_Error()) { + return(false); } } - savestream.Serialize(ZoneConnections); - result = savestream.Result(); - if (FAILED(result)) { - return(result); + stream.Serialize(ZoneConnections); + if (stream.Was_Error()) { + return(false); } count = 0; @@ -586,16 +582,16 @@ HRESULT MouseClass::Save(IStream * stream) } cptr = Iterate(); } - result = stream->Write(&count, sizeof(count), NULL); - if (FAILED(result)) { - return(result); + stream.Serialize(count); + if (stream.Was_Error()) { + return(false); } Reset_Iterator(); cptr = Iterate(); while (cptr != NULL) { Cell cell = cptr->CellID; if (Is_Valid(cell)) { - OleSaveToStream(cptr, stream); + Save_Object(stream, cptr); count--; } cptr = Iterate(); @@ -604,7 +600,7 @@ HRESULT MouseClass::Save(IStream * stream) return(result); } - result = S_OK; + result = true; } return(result); } diff --git a/code/mouse.h b/code/mouse.h index 9a2da1d17..09d260c65 100644 --- a/code/mouse.h +++ b/code/mouse.h @@ -42,8 +42,8 @@ class MouseClass: public ScrollClass typedef ScrollClass BASECLASS; public: - virtual HRESULT Load(IStream * stream) override; - virtual HRESULT Save(IStream * stream) override; + virtual bool Load(SaveStreamClass & stream) override; + virtual bool Save(SaveStreamClass & stream) override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/movie.cpp b/code/movie.cpp index 8876d902c..3c8d1dc40 100644 --- a/code/movie.cpp +++ b/code/movie.cpp @@ -184,10 +184,9 @@ void _Play_Movie(char const * name, ThemeType theme) /// void Play_Movie(VQType vq, ThemeType theme, bool clrscrn, bool stretch) { - static char _buf[20]; + static char _buf[MOVIE_FILENAME_SIZE]; if (vq != VQ_NONE) { - strcpy(_buf, Movies[vq]); - strcpy(_buf + strlen(Movies[vq]), ".VQA"); + snprintf(_buf, sizeof(_buf), "%s.VQA", Movies[vq]); Play_Movie(_buf, theme, clrscrn, stretch, true); } } @@ -219,10 +218,9 @@ void Play_Ingame_Movie(const char * name) /// void Play_Ingame_Movie(VQType vq) { - static char _buf[20]; + static char _buf[MOVIE_FILENAME_SIZE]; if (vq != VQ_NONE) { - strcpy(_buf, Movies[vq]); - strcpy(_buf + strlen(Movies[vq]), ".VQA"); + snprintf(_buf, sizeof(_buf), "%s.VQA", Movies[vq]); Play_Ingame_Movie(_buf); } } diff --git a/code/movie.h b/code/movie.h index 9a067300c..1bfa77abb 100644 --- a/code/movie.h +++ b/code/movie.h @@ -20,6 +20,10 @@ template class DynamicVectorClass; extern DynamicVectorClass Movies; +// Room for the longest name the movie registry accepts, its ".VQA" and the terminator. +// RulesClass::Do_Movies reads a name into a 32 byte buffer, so 31 characters can arrive. +inline constexpr int MOVIE_FILENAME_SIZE = 36; + void Play_Movie(char const * name, ThemeType theme=THEME_NONE, bool clrscrn_after=true, bool stretch=true, bool clrscrn_before=true); void Play_Movie(VQType vq, ThemeType theme=THEME_NONE, bool clrscrn=true, bool stretch=true); void Play_Ingame_Movie(VQType vq); diff --git a/code/mplayer.cpp b/code/mplayer.cpp index 9fbd31f15..7aca35fa8 100644 --- a/code/mplayer.cpp +++ b/code/mplayer.cpp @@ -45,13 +45,11 @@ #include "addon.h" #include "init.h" #include "msgbox.h" -#include "ownrdraw.h" #include "session.h" +#include "ui/uimpselect.h" class ListClass; -INT_PTR CALLBACK Select_MPlayer_Game_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); - /// /// Prompts the player for which kind of multiplayer game to start. /// @@ -64,95 +62,18 @@ GameType Select_MPlayer_Game (void) return(retval); } - HWND dialog; - - if (Addon_Installed(ADDON_FIRESTORM) == ADDON_FIRESTORM) { - dialog = OwnerDraw::Begin_Dialog(IDD_MPLAYER_SELECT_GAME_FS, Select_MPlayer_Game_Dialog_Proc); - } else { - dialog = OwnerDraw::Begin_Dialog(IDD_MPLAYER_SELECT_GAME, Select_MPlayer_Game_Dialog_Proc); - } - + UIMPSelectPresenterClass screen; + screen.Refresh(); - if (dialog) { - - int rc; - SetWindowLongPtr(dialog, DWLP_USER, (LONG_PTR)&rc); - - bool process = true; - while (process) { - OwnerDraw::Move_Dialog(dialog, -1, (HiddenSurface->Get_Height() - 400) / 2 + 147); - OwnerDraw::Display_Dialog(dialog); - rc = -1; - while (rc == -1) { - if (OwnerDraw::Dialog_Message_Handler() == true) { - break; - } - Title_Screen_Restore(); - } - - ShowWindow(dialog, SW_HIDE); - UpdateWindow(MainWindow); - switch (rc) { - case IDC_NETWORK: - retval = GAME_IPX; - break; - case IDC_SKIRMISH: - retval = GAME_SKIRMISH; - break; - default: - retval = GAME_NORMAL; - process = false; - break; - } - if (retval != GAME_NORMAL) { - break; - } - } - - OwnerDraw::End_Dialog(dialog); + if (UI_MPlayer_Select_Screen(screen).Outcome != UIResult::OUTCOME_FAILED_TO_OPEN) { + retval = (GameType)screen.Session_Type(); Session.Read_Scenario_Descriptions(); } + return(retval); } /* end of Select_MPlayer_Game */ -/// -/// Handles the messages for the multiplayer game type dialog. -/// -/// Returns with the result of the ownerdraw handler, or false when the message was -/// left unhandled. -INT_PTR CALLBACK Select_MPlayer_Game_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - int * retval; - HWND handle; - - INT_PTR rc = OwnerDraw::Default_Dialog_Proc(window, message, wparam, lparam); - - if (message == WM_INITDIALOG) { - // Neither the online service these led to nor the tour it hosted can be reached, - // so the buttons are left on the dialog but never answer. - handle = GetDlgItem(window, IDC_INTERNET); - if (handle) { - EnableWindow(handle, FALSE); - } - handle = GetDlgItem(window, IDC_WORLDDOM); - if (handle) { - EnableWindow(handle, FALSE); - } - } - - if (rc != 0) { - return(rc); - } - - if (message == WM_COMMAND) { - retval = (int *)GetWindowLongPtr(window, DWLP_USER); - *retval = LOWORD(wparam); - } - return(false); -} - - /*************************************************************************** * Surrender_Dialog -- Prompts user for surrendering * * * diff --git a/code/mpscore.cpp b/code/mpscore.cpp index 447d0b280..2495417f0 100644 --- a/code/mpscore.cpp +++ b/code/mpscore.cpp @@ -35,7 +35,6 @@ #include "session.h" #include "stats.h" #include "surface.h" -#include "windlg.h" #include "winstub.h" #include "color.hh" @@ -183,8 +182,6 @@ bool MultiScore::Multi_Presentation(void) return(false); } - while (WS_Destroy_Dialog(0, 0)) { } - if (Init() == true) { Keyboard->Clear(); Callback(); diff --git a/code/mpu.cpp b/code/mpu.cpp index 316273cb5..67383d095 100644 --- a/code/mpu.cpp +++ b/code/mpu.cpp @@ -39,6 +39,29 @@ #include #include +#ifndef _WIN32 +#include + +/// The monotonic clock stands in for the Windows performance counter. Both are read only as +/// a difference over a fixed frequency, so a nanosecond tick answers the same question. +static BOOL QueryPerformanceFrequency(LARGE_INTEGER* result) +{ + result->QuadPart = 1000000000LL; + return(TRUE); +} + +static BOOL QueryPerformanceCounter(LARGE_INTEGER* result) +{ + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) { + result->QuadPart = 0; + return(FALSE); + } + result->QuadPart = (LONGLONG)now.tv_sec * 1000000000LL + (LONGLONG)now.tv_nsec; + return(TRUE); +} +#endif + typedef union { LARGE_INTEGER LargeInt; struct QuadPart { diff --git a/code/msanim.cpp b/code/msanim.cpp index 535a9e6a1..3da373d3d 100644 --- a/code/msanim.cpp +++ b/code/msanim.cpp @@ -25,7 +25,7 @@ #include "mixfile.h" #include "movies.h" #include "msfont.h" -#include "ownrdraw.h" +#include "drawhelp.h" #include "pcx.h" #include "shapeset.h" #include "srfcache.h" diff --git a/code/msengine.cpp b/code/msengine.cpp index f5f835255..5d34ccb2c 100644 --- a/code/msengine.cpp +++ b/code/msengine.cpp @@ -7,6 +7,7 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ +#include "hostclock.h" #include "always.h" #include "msengine.h" @@ -377,7 +378,7 @@ void MSEngine::Wait_Delay(int delay) timer.Start(); } - Sleep(0); + Host_Sleep(0); } while (timer.Value() > 0); @@ -406,7 +407,7 @@ void MSEngine::Wait_For_Focus(void) while (!GameInFocus) { DebugString("MSEngine - Sleeping\n"); - Sleep(500); + Host_Sleep(500); Windows_Message_Handler(); } diff --git a/code/msgbox.cpp b/code/msgbox.cpp index 9877bd534..301242322 100644 --- a/code/msgbox.cpp +++ b/code/msgbox.cpp @@ -38,13 +38,9 @@ #include "data.h" #include "globals.h" #include "init.h" -#include "ownrdraw.h" +#include "ui/uimessagebox.h" #include "winfix.h" -INT_PTR CALLBACK Message_Box_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); -void Message_Box_On_WM_COMMAND(HWND window, int id, int control, int notify_code); - -int _default_response = 0; /*********************************************************************************************** * WWMessageBox::Process -- pops up a message with yes/no, etc * @@ -71,150 +67,18 @@ int _default_response = 0; * 05/18/1995 JLB : Uses new font and dialog style. * * 08/24/1995 JLB : Handles three buttons. * *=============================================================================================*/ -#define BUTTON_1 IDC_MSGBOX_OK -#define BUTTON_2 IDCANCEL -#define BUTTON_3 IDC_MSGBOX_BTN3 -#define BUTTON_FLAG 0x8000 int WWMessageBox::_Process(const char * msg, int defresponse, const char * b1txt, const char * b2txt, const char * b3txt, bool preserve) { - int retval = -1; - int numbuttons = 0; - - _default_response = defresponse; - - HWND dialog = OwnerDraw::Begin_Dialog(IDD_MSGBOX_3, Message_Box_Proc); - - if (dialog != NULL) { - SetWindowLongPtr(dialog, DWLP_USER, (LONG_PTR)&retval); - - if (msg != NULL && msg[0] != '\0') { - SetDlgItemText(dialog, IDC_MSGBOX_TEXT, msg); - } - - if (b1txt != NULL && b1txt[0] != '\0') { - SetDlgItemText(dialog, BUTTON_1, b1txt); - numbuttons = 1; - } else { - EnableWindow(GetDlgItem(dialog, BUTTON_1), FALSE); - ShowWindow(GetDlgItem(dialog, BUTTON_1), SW_HIDE); - } - - if (b2txt != NULL && b2txt[0] != '\0') { - SetDlgItemText(dialog, BUTTON_2, b2txt); - numbuttons = 2; - } else { - EnableWindow(GetDlgItem(dialog, BUTTON_2), FALSE); - ShowWindow(GetDlgItem(dialog, BUTTON_2), SW_HIDE); - } - - if (b3txt != NULL && b3txt[0] != '\0') { - SetDlgItemText(dialog, BUTTON_3, b3txt); - numbuttons = 3; - } else { - EnableWindow(GetDlgItem(dialog, BUTTON_3), FALSE); - ShowWindow(GetDlgItem(dialog, BUTTON_3), SW_HIDE); + UIResult const result = UI_Message_Box_Screen(msg, defresponse, b1txt, b2txt, b3txt); - if (numbuttons == 1) { - RECT rect; - GetWindowRect(GetDlgItem(dialog, BUTTON_3), &rect); - ScreenToClient(dialog, (LPPOINT)&rect); - ScreenToClient(dialog, (LPPOINT)&rect.right); - MoveWindow(GetDlgItem(dialog, BUTTON_1), rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, FALSE); - } - } - - OwnerDraw::Display_Dialog(dialog); - - if (numbuttons > 0) { - while (retval < 0) { - if (OwnerDraw::Dialog_Message_Handler() == true) { - break; - } - - if (!GameActive) { - Title_Screen_Restore(); - } - } - } else { - retval = 0; - } - - OwnerDraw::End_Dialog(dialog); + // A session that ended under the box, and a box that could not be shown at all, are + // both reported as the -1 the dialog driver left its caller holding. + if (result.Outcome == UIResult::OUTCOME_SESSION_ENDED || + result.Outcome == UIResult::OUTCOME_FAILED_TO_OPEN) { + return(-1); } - return(retval); -} - - -/// -/// Handles the dialog messages for the message box. -/// This routine gives the owner draw system first crack at every message and only steps -/// in for the button notifications and the window dragging that it does not already deal -/// with. -/// -/// BOOL; Was the message handled here? -INT_PTR CALLBACK Message_Box_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - INT_PTR rc = OwnerDraw::Default_Dialog_Proc(window, message, wparam, lparam); - - if (rc == FALSE) { - switch (message) { - - case WM_COMMAND: - Message_Box_On_WM_COMMAND(window, LOWORD(wparam), 0, HIWORD(wparam)); - rc = FALSE; - break; - - case WM_MOVING: - rc = On_WM_MOVING(window, wparam, lparam); - break; - - default: - rc = FALSE; - break; - } - } - - return(rc); -} - - -/// -/// Handles a button press within the message box dialog. -/// This routine records which of the buttons the player picked in the result slot that -/// the message box is waiting on. The Enter key arrives here as IDOK and yields the -/// default response the caller asked for. -/// -/// The identifier of the control that sent the notification. -void Message_Box_On_WM_COMMAND(HWND window, int id, int control, int notify_code) -{ - int *retval = (int*)GetWindowLongPtr(window, DWLP_USER); - switch (id) { - - case IDOK: - if (notify_code == BN_CLICKED) { - *retval = _default_response; - } - break; - - case BUTTON_1: - if (notify_code == BN_CLICKED) { - *retval = 0; - } - break; - - case IDCANCEL: - if (notify_code == BN_CLICKED) { - *retval = 1; - } - break; - - case BUTTON_3: - if (notify_code == BN_CLICKED) { - *retval = 2; - } - break; - } + return(result.Value); } diff --git a/code/msgloop.cpp b/code/msgloop.cpp index dcbe981cb..b95ca0db6 100644 --- a/code/msgloop.cpp +++ b/code/msgloop.cpp @@ -28,9 +28,7 @@ *---------------------------------------------------------------------------------------------* * Functions: * * Add_Accelerator -- Adds a keyboard accelerator to the message handler. * - * Add_Modeless_Dialog -- Adds a modeless dialog box to the message handler. * * Remove_Accelerator -- Removes an accelerator from the message processor. * - * Remove_Modeless_Dialog -- Removes the dialog box from the message tracking handler. * * Windows_Message_Handler -- Handles windows message. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ @@ -49,7 +47,6 @@ ** box handles and then determining if the windows message applies to the dialog box. If it ** does, then the default message handling should not be performed. */ -DynamicVectorClass _ModelessDialogs; /* @@ -112,19 +109,7 @@ void Windows_Message_Handler(void) ToolTips->Message_Handler(&msg); } - /* - ** Pass the windows message through any modeless dialogs that may - ** be active. If one of the dialogs processes the message, then - ** it must not be processed by the normal window message handler. - */ bool processed = false; - for (int index = 0; index < _ModelessDialogs.Count(); index++) { - if (IsDialogMessage(_ModelessDialogs[index], &msg)) { - processed = true; - break; - } - } - if (processed) continue; /* ** Pass the message through any loaded accelerators. If the message @@ -168,74 +153,6 @@ void Windows_Message_Handler(void) } -/*********************************************************************************************** - * Add_Modeless_Dialog -- Adds a modeless dialog box to the message handler. * - * * - * When a modeless dialog box becomes active, the messages processed by the main message * - * handler must be handled different. This routine is used to inform the message handler * - * that a dialog box is active and messages must be fed to it as appropriate. * - * * - * INPUT: dialog -- Handle to the modeless dialog box. * - * * - * OUTPUT: none * - * * - * WARNINGS: The modeless dialog box must be removed from the tracking system by calling * - * Remove_Modeless_Dialog. Failure to do so when the dialog is destroyed will * - * result in undefined behavior. * - * * - * HISTORY: * - * 05/17/1997 JLB : Created. * - *=============================================================================================*/ -void Add_Modeless_Dialog(HWND dialog) -{ - _ModelessDialogs.Add(dialog); -} - - -/*********************************************************************************************** - * Remove_Modeless_Dialog -- Removes the dialog box from the message tracking handler. * - * * - * This routine must be called when a modeless dialog is being removed. * - * * - * INPUT: dialog -- Handle to the modeless dialog that was previously submitted to * - * Add_Modeless_Dialog(). * - * * - * OUTPUT: none * - * * - * WARNINGS: Failure to call this routine will result in undefined behavior when the dialog * - * is destroyed. * - * * - * HISTORY: * - * 05/17/1997 JLB : Created. * - *=============================================================================================*/ -void Remove_Modeless_Dialog(HWND dialog) -{ - _ModelessDialogs.Delete(dialog); -} - - -/// -/// Fetches a tracked modeless dialog by its window title. -/// This routine searches the dialogs submitted by Add_Modeless_Dialog for one whose caption -/// matches the name given. Use this routine when only the title of the dialog is known. -/// -/// The window title of the dialog to look for. -/// Returns with the handle of the matching dialog, or NULL if no tracked dialog -/// carries that title. -HWND Get_Modeless_Dialog_From_Name(const char *name) -{ - static char _wname[100]; - - for (int i = 0; i < _ModelessDialogs.Count(); i++) { - GetWindowText(_ModelessDialogs[i], _wname, sizeof(_wname) - 1); - if (!strcmp(_wname, name)) { - return(_ModelessDialogs[i]); - } - } - return(NULL); -} - - /*********************************************************************************************** * Add_Accelerator -- Adds a keyboard accelerator to the message handler. * * * diff --git a/code/msgloop.h b/code/msgloop.h index af1c7d712..f6d4fdd74 100644 --- a/code/msgloop.h +++ b/code/msgloop.h @@ -36,11 +36,6 @@ // Main message handler. void Windows_Message_Handler(void); -// Modeless dialog box support routines. -void Remove_Modeless_Dialog(HWND dialog); -void Add_Modeless_Dialog(HWND dialog); -HWND Get_Modeless_Dialog_From_Name(const char *name); - // Accelerator keys support routines. void Add_Accelerator(HWND window, HACCEL accelerator); void Remove_Accelerator(HACCEL accelerator); diff --git a/code/mstimer.cpp b/code/mstimer.cpp index fceb0fc3a..02e68c307 100644 --- a/code/mstimer.cpp +++ b/code/mstimer.cpp @@ -11,17 +11,22 @@ #include "mstimer.h" +#include "hostclock.h" #include "win.h" /// -/// Asks Windows for one millisecond timer resolution. -/// This routine is called when the timer is created so that the readings it hands out -/// are fine grained enough for the game to pace itself by. +/// Requests one millisecond timer resolution for as long as this object exists. +/// The reading itself no longer needs it, since hostclock.h answers that from a clock of +/// its own. The request is process wide, though, and every wait the game paces itself with +/// is rounded up to whatever resolution is in force. /// MillisecondSystemTimerClass::MillisecondSystemTimerClass(void) { + // Windows only; no other host has a resolution to bid for. +#ifdef _WIN32 timeBeginPeriod(1); +#endif } @@ -32,7 +37,10 @@ MillisecondSystemTimerClass::MillisecondSystemTimerClass(void) /// MillisecondSystemTimerClass::~MillisecondSystemTimerClass(void) { + // Windows only; no other host has a resolution to bid for. +#ifdef _WIN32 timeEndPeriod(1); +#endif } @@ -41,10 +49,10 @@ MillisecondSystemTimerClass::~MillisecondSystemTimerClass(void) /// This is the sampling routine that the timer templates call whenever they need to /// know how much time has passed. /// -/// Returns with the number of milliseconds elapsed since Windows started. +/// Returns with the host clock's millisecond reading. int MillisecondSystemTimerClass::operator () (void) const { - return(timeGetTime()); + return(Host_Milliseconds()); } @@ -52,8 +60,8 @@ int MillisecondSystemTimerClass::operator () (void) const /// Converts the timer into its current millisecond reading. /// This routine lets the timer object be used wherever a plain time value is expected. /// -/// Returns with the number of milliseconds elapsed since Windows started. +/// Returns with the host clock's millisecond reading. MillisecondSystemTimerClass::operator int (void) const { - return(timeGetTime()); + return(Host_Milliseconds()); } diff --git a/code/netdlg2.cpp b/code/netdlg2.cpp index abd5ef0b5..dacc3accf 100644 --- a/code/netdlg2.cpp +++ b/code/netdlg2.cpp @@ -33,9 +33,9 @@ #include "mplayer.h" #include "msgbox.h" #include "netdlg.h" +#include "netglobal.h" #include "netshare.h" #include "newmenu.h" -#include "ownrdraw.h" #include "rules.h" #include "scenario.h" #include "sendfile.h" @@ -43,7 +43,8 @@ #include "stimer.h" #include "timer.h" #include "utf8.h" -#include "windlg.h" +#include "ui/uilobby.h" +#include "ui/uishell.h" #include "winstub.h" #include "wsproto.h" @@ -55,12 +56,9 @@ */ static int Request_To_Join(int join_index); static void Unjoin_Game(int game_index); -static void Send_Join_Queries(int gamenow, int playernow, int chatnow, int init = 0); static void Get_Join_Responses(void); +static bool Lobby_Seat_Is_Valid(int house, int color); -INT_PTR CALLBACK MPlayer_Guest_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); -INT_PTR CALLBACK MPlayer_Game_List_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); -INT_PTR CALLBACK MPlayer_Host_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); bool Net2ReadyToGo(int load_game); int CurGame; @@ -78,51 +76,112 @@ int Net2_g_Col_Accept; int Net2_g_Col_Name; int Net2_g_Col_House; +// The lobby screen the driver is running. The dialog procedures read its view-model and +// queue intents against it; the driver executes the queue after its pump returns. +// code/ui/uilobby.cpp holds the pointer, because a message produced away from a screen has +// to reach the same model. +static UILobbyPresenterClass * Lobby_Screen(void) +{ + return(UI_Lobby_Screen()); +} + + +// Is the lobby being shown through RmlUi? Latched when the lobby opens, the way every +// migrated screen latches its selection at screen entry. + + +int Net2LobbyScreenID(void) +{ + UILobbyPresenterClass const * const screen = Lobby_Screen(); + + if (screen == NULL) { + return(0); + } + + switch (screen->Showing) { + case UILobbyPresenterClass::SCREEN_GAME_LIST: return(IDD_MPLAYER_GAME_LIST); + case UILobbyPresenterClass::SCREEN_HOST: return(IDD_MPLAYER_HOST); + case UILobbyPresenterClass::SCREEN_GUEST: return(IDD_MPLAYER_GUEST); + default: return(0); + } +} + /// -/// Fills a side box with the multiplayable countries, each entry carrying its country index. +/// Shows one of the lobby's three screens. +/// The screen the lobby moved away from stays alive underneath, which is what the lobby's +/// own dialogs did. /// -void Fill_Country_Box(HWND combo) +static void Lobby_Open_Screen(UILobbyPresenterClass::ScreenType kind) { - SendMessage(combo, CB_RESETCONTENT, 0, 0); - for (int index = 0; index < HouseTypes.Count(); index++) { - HouseTypeClass * house = HouseTypes[index]; - if (house->IsMultiplay) { - LRESULT item = SendMessage(combo, CB_INSERTSTRING, (WPARAM)-1, (LPARAM)(char const *)house->GivenName); - SendMessage(combo, CB_SETITEMDATA, item, index); - } + UILobbyPresenterClass * const screen = Lobby_Screen(); + if (screen == NULL) { + return; + } + + screen->Showing = kind; + + // What the legacy dialog's WM_INITDIALOG did before it put anything on a control. + switch (kind) { + case UILobbyPresenterClass::SCREEN_GAME_LIST: screen->Open(); break; + case UILobbyPresenterClass::SCREEN_HOST: screen->Open_Host(); break; + case UILobbyPresenterClass::SCREEN_GUEST: screen->Open_Guest(); break; + default: break; } } /// -/// Fetches the country behind a side box's selection. +/// One pass of the lobby's own maintenance, which both of its drivers run through the +/// presenter's Service. /// -/// Returns with the country index, or the first country with nothing selected. -int Country_From_Box(HWND combo) +void Net2ServiceLobby(void) { - LRESULT item = SendMessage(combo, CB_GETCURSEL, 0, 0); - if (item == CB_ERR) { - return(HOUSE_FIRST); + Ipx.Service(); + Call_Back(); + Ipx.Service(); + Title_Screen_Restore(); + + if (Net2LobbyScreenID() == 0) { + return; } - LRESULT country = SendMessage(combo, CB_GETITEMDATA, item, 0); - return(country == CB_ERR ? HOUSE_FIRST : (int)country); + + Send_Join_Queries(false, false, false, false); + Get_Join_Responses(); + if (Net2LobbyScreenID() == IDD_MPLAYER_HOST) { + PumpGameopts(false); + } + Net2ServiceGameList(); } /// -/// Selects the entry of a side box carrying the given country, or the first entry when none does. +/// Maps a lobby answer onto the control identifier the driver loop already tests, the way +/// every migrated screen's wrapper maps its outcome onto the value its caller expects. /// -void Select_Country_In_Box(HWND combo, int country) +static int Lobby_Response_Identifier(UILobbyPresenterClass::ResponseType response) { - LRESULT count = SendMessage(combo, CB_GETCOUNT, 0, 0); - for (LRESULT item = 0; item < count; item++) { - if (SendMessage(combo, CB_GETITEMDATA, item, 0) == country) { - SendMessage(combo, CB_SETCURSEL, item, 0); - return; - } + switch (response) { + case UILobbyPresenterClass::RESPONSE_CANCEL: return(IDCANCEL); + case UILobbyPresenterClass::RESPONSE_JOIN: return(IDC_GAMELIST_JOIN); + case UILobbyPresenterClass::RESPONSE_NEW: return(IDC_GAMELIST_NEW); + case UILobbyPresenterClass::RESPONSE_GO: return(IDC_GO); + default: return(0); + } +} + + +/// +/// Answers the lobby driver from the network rather than from a button. +/// The runner returns on a result or a suspension, so a screen answered from inside its own +/// service steps aside; otherwise the driver never gets its pass back to act on the answer. +/// +static void Net2AnswerLobby(int response) +{ + _netresponse = response; + if (Lobby_Screen() != NULL) { + Lobby_Screen()->Answered = true; } - SendMessage(combo, CB_SETCURSEL, count > 0 ? 0 : (WPARAM)-1, 0); } @@ -135,9 +194,27 @@ void Select_Country_In_Box(HWND combo, int country) /// The player doing the asking, so that its own color is not counted /// against it. /// Returns with the color the player should use. +/// +/// Is a seat a peer asked for one the game can actually give it? +/// The house indexes the house type list and the color indexes the color table when the +/// scenario starts, and both arrive from another machine. +/// +static bool Lobby_Seat_Is_Valid(int house, int color) +{ + return(house >= 0 && house < HouseTypes.Count() && color >= 0 && color < MAX_MPLAYER_COLORS); +} + + int Net2FirstFreeColor(int reqcolor, int index) { int color; + + // The requested color may have come off the network, and every later step of this + // routine keeps a color in range only because the first one is. + if (reqcolor < 0 || reqcolor >= MAX_MPLAYER_COLORS) { + reqcolor = 0; + } + while (1) { int taken = 0; color = reqcolor; @@ -195,99 +272,11 @@ void Net2DisplayUsers(void) /// void _Net2DisplayUsers(void) { - int i; - int color; - char hname[128]; - char info[128]; - Surface * surf = NULL; - - HWND win=WS_Top_Window(); - - HWND userwin=GetDlgItem(win,IDC_USERS); - - if (win==NULL || userwin==NULL) { + if (Lobby_Screen() == NULL) { return; } - OwnerDraw::CellData thecell; - - int topindex=SendDlgItemMessage(win, IDC_USERS, LB_GETTOPINDEX, 0, 0); - - SendDlgItemMessage(win, IDC_USERS, OD_DISABLEPAINT, 0, TRUE); - - Dictionary lbdict(Wstring_Hash); - LBSaveSelections(userwin, lbdict); - - SendDlgItemMessage(win, IDC_USERS, LB_RESETCONTENT, NULL, NULL); - - if (CurGame == 0) { - SendDlgItemMessage(win, IDC_USERS, LB_INSERTSTRING, (WPARAM)0, (LPARAM)Session.Handle); - - for (i = 1; i < Session.Chat.Count(); i++) { - SendDlgItemMessage(win, IDC_USERS, LB_INSERTSTRING, (WPARAM)i, (LPARAM)Session.Chat[i]->Name); - } - - } else { - - for (i = 0; i < Session.Players.Count(); i++) { - int type = 0; - - if (!strcmp(Session.Players[i]->Name, Session.GameName)) { - Session.Players[i]->Player.Status = 1; - type = 2; - } else if (Session.Players[i]->Player.Status != 0) { - type = 1; - } - - sprintf(info, "%s", Session.Players[i]->Name); - - // Only two icons ship, so every side past the first borrows the second's. - int country = Session.Players[i]->Player.House; - SideType side = country >= HOUSE_FIRST && country < HouseTypes.Count() ? HouseTypes[country]->Side : SIDE_NONE; - if (side == SIDE_GDI) { - sprintf(hname, "%s", Fetch_String(TXT_GDI)); - surf = SurfaceCache.GetSurface("gdii.pcx"); - } else if (side == SIDE_NOD || side == SIDE_NONE) { - sprintf(hname, "%s", Fetch_String(TXT_NOD)); - surf = SurfaceCache.GetSurface("nodi.pcx"); - } else { - sprintf(hname, "%s", (char const *)HouseTypes[country]->GivenName); - surf = SurfaceCache.GetSurface("nodi.pcx"); - } - - SendDlgItemMessage(win, IDC_USERS, LB_INSERTSTRING, (WPARAM) -1, (LPARAM)info); - - color = PlayerColorTable[Session.Players[i]->Player.Color]; - - thecell.type = OwnerDraw::CellData::PRIMARY; - thecell.color = color; - thecell.hint.set(""); - SendDlgItemMessage(win, IDC_USERS, OD_SETCELL, MAKEWPARAM(Net2_g_Col_Name,i),(LPARAM)&thecell); - - thecell.type = OwnerDraw::CellData::SURFACE; - thecell.hint.set(hname); - thecell.surf = surf; - SendDlgItemMessage(win, IDC_USERS, OD_SETCELL, MAKEWPARAM(Net2_g_Col_House,i),(LPARAM)&thecell); - - thecell.hint.set(""); - thecell.type = OwnerDraw::CellData::SURFACE; - if (type == 2) { - thecell.surf=SurfaceCache.GetSurface("wolhost.pcx"); - } else if (type != 0) { - thecell.surf=SurfaceCache.GetSurface("wolacpt.pcx"); - } else { - thecell.type = OwnerDraw::CellData::INVALID; - } - SendDlgItemMessage(win, IDC_USERS, OD_SETCELL, MAKEWPARAM(Net2_g_Col_Accept,i),(LPARAM)&thecell); - } - - } - - LBRestoreSelections(userwin, lbdict); - SendDlgItemMessage(win, IDC_USERS, LB_SETTOPINDEX, (WPARAM)topindex, 0); - SendDlgItemMessage(win, IDC_USERS, OD_DISABLEPAINT, 0, 0); - InvalidateRect(userwin, NULL, 0); - UpdateWindow(userwin); + Lobby_Screen()->Build_User_Rows(); } @@ -375,44 +364,11 @@ void Net2ServiceGameList(void) /// void Net2DisplayGameList(void) { - char buffer[80]; - - HWND window = WS_Top_Window(); - - int count = Session.Games.Count(); - if (CurGame >= count) { - CurGame = count - 1; - Send_Join_Queries(0, 1, 0, 0); - } - - if (CurGame < 0) { - CurGame = 0; - } - - int top = SendDlgItemMessage(window, IDC_GAMELIST, LB_GETTOPINDEX, 0, 0); - - SendDlgItemMessage(window, IDC_GAMELIST, OD_DISABLEPAINT, 0, 1); - SendDlgItemMessage(window, IDC_GAMELIST, LB_RESETCONTENT, 0, 0); - SendDlgItemMessage(window, IDC_GAMELIST, LB_INSERTSTRING, -1, (LPARAM)Fetch_String(TXT_LOBBY)); - - for (int i = 1; i < Session.Games.Count(); i++) { - NodeNameType *node = Session.Games[i]; - if (node->Game.IsOpen) { - sprintf(buffer, Fetch_String(TXT_THATGUYS_GAME), node); - } else { - sprintf(buffer, Fetch_String(TXT_THATGUYS_GAME_BRACKET), node); - } - SendDlgItemMessage(window, IDC_GAMELIST, LB_INSERTSTRING, -1, (LPARAM)buffer); + if (Lobby_Screen() == NULL) { + return; } - int idx = CurGame; - SendDlgItemMessage(window, IDC_GAMELIST, LB_SETCURSEL, idx, 0); - SendDlgItemMessage(window, IDC_GAMELIST, LB_SETTOPINDEX, top, 0); - SendDlgItemMessage(window, IDC_GAMELIST, OD_DISABLEPAINT, 0, 0); - - HWND handle = GetDlgItem(window, IDC_GAMELIST); - InvalidateRect(handle, NULL, FALSE); - UpdateWindow(handle); + Lobby_Screen()->Build_Game_Rows(); } @@ -539,6 +495,10 @@ int Net2SetHouseAndColor(char *who, int house, int color) int offset = -1; int retval = 0; + if (!Lobby_Seat_Is_Valid(house, color)) { + return(0); + } + for (int i = 0; i < Session.Players.Count(); i++) { if (strcmp(Session.Players[i]->Name, who) == 0) { offset = i; @@ -556,15 +516,7 @@ int Net2SetHouseAndColor(char *who, int house, int color) } if (offset == 0) { - HWND win=WS_Find_Dialog(IDD_MPLAYER_HOST); Session.PrefColor = color; - if (win == NULL) { - win = WS_Find_Dialog(IDD_MPLAYER_GUEST); - } - if ((SendDlgItemMessage(win,IDC_YOURCOLOR,CB_GETCURSEL,0,0) != color) && - (SendDlgItemMessage(win,IDC_YOURCOLOR,CB_GETDROPPEDSTATE,0,0) == FALSE)) { - SendDlgItemMessage(win,IDC_YOURCOLOR,CB_SETCURSEL,color,0); - } } if (offset == 0) { @@ -713,12 +665,10 @@ bool Net2Remote_Connect(void) Net2GameStarted = false; - OwnerDraw::Register_Control_Classes(); + UILobbyPresenterClass screen; + UI_Set_Lobby_Screen(&screen); - HWND game_list_dialog = WS_Create_Dialog(ProgramInstance, IDD_MPLAYER_GAME_LIST, MainWindow, MPlayer_Game_List_Dialog_Proc, FALSE); - Center_Window_Within_Window(game_list_dialog); - OwnerDraw::Subclass_Dialog(game_list_dialog, 0); - ShowWindow(game_list_dialog, SW_SHOWNORMAL); + Lobby_Open_Screen(UILobbyPresenterClass::SCREEN_GAME_LIST); Net2DisplayUsers(); _netresponse = 0; @@ -734,30 +684,12 @@ bool Net2Remote_Connect(void) // Pop up the network Join/New dialog //..................................................................... while (_netresponse == 0) { - Ipx.Service(); - Sleep(0); - Call_Back(); - Ipx.Service(); - Title_Screen_Restore(); - - MSG msg; - while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { - TranslateMessage(&msg); - DispatchMessage(&msg); - } - - Call_Back(); - if (_netresponse != 0) { - break; - } + UI_Lobby_Run(screen); - if (WS_Top_Window()) { - Send_Join_Queries(false, false, false, false); - Get_Join_Responses(); - if (WS_Top_Window_ID() == IDD_MPLAYER_HOST) { - PumpGameopts(false); - } - Net2ServiceGameList(); + screen.Result.reset(); + if (screen.Response != UILobbyPresenterClass::RESPONSE_NONE) { + _netresponse = Lobby_Response_Identifier(screen.Response); + screen.Response = UILobbyPresenterClass::RESPONSE_NONE; } } @@ -766,32 +698,29 @@ bool Net2Remote_Connect(void) //..................................................................... if (_netresponse == IDCANCEL) { Session.Write_MultiPlayer_Settings(); - if (WS_Top_Window_ID() == IDD_MPLAYER_GAME_LIST) { + if (Net2LobbyScreenID() == IDD_MPLAYER_GAME_LIST) { if (JoinState > JOIN_NOTHING) { Unjoin_Game(CurGame); Ipx.Service(); } - WS_Destroy_Dialog(NULL, 0); + UI_Lobby_Close_Views(); Clear_Vector(&Session.Players); Clear_Vector(&Session.Games); Clear_Vector(&Session.Chat); Session.NetOpen = false; Ipx.Service(); + UI_Set_Lobby_Screen(NULL); return(false); } - if (WS_Top_Window_ID() == IDD_MPLAYER_HOST) { + if (Net2LobbyScreenID() == IDD_MPLAYER_HOST) { Unjoin_Game(CurGame); JoinState = JOIN_NOTHING; - WS_Destroy_Dialog(WS_Top_Window(), 0); - game_list_dialog = WS_Create_Dialog(ProgramInstance, IDD_MPLAYER_GAME_LIST, MainWindow, MPlayer_Game_List_Dialog_Proc, 0); - Center_Window_Within_Window(game_list_dialog); - OwnerDraw::Subclass_Dialog(game_list_dialog, 0); - ShowWindow(game_list_dialog, SW_SHOWNORMAL); + Lobby_Open_Screen(UILobbyPresenterClass::SCREEN_GAME_LIST); Send_Join_Queries(false, false, true, false); } - if (WS_Top_Window_ID() == IDD_MPLAYER_GUEST) { + if (Net2LobbyScreenID() == IDD_MPLAYER_GUEST) { //............................................................... // If we're joined to a game, make extra sure the other players in // that game know I'm exiting; send my SIGN_OFF as an ack-required @@ -836,14 +765,10 @@ bool Net2Remote_Connect(void) Session.GameName[0] = '\0'; JoinState = JOIN_NOTHING; - WS_Destroy_Dialog(0, 0); _netresponse = 0; CurGame = 0; Clear_Vector(&Session.Players); - game_list_dialog = WS_Create_Dialog(ProgramInstance, IDD_MPLAYER_GAME_LIST, MainWindow, MPlayer_Game_List_Dialog_Proc, 0); - Center_Window_Within_Window(game_list_dialog); - OwnerDraw::Subclass_Dialog(game_list_dialog, 0); - ShowWindow(game_list_dialog, SW_SHOWNORMAL); + Lobby_Open_Screen(UILobbyPresenterClass::SCREEN_GAME_LIST); } } @@ -897,7 +822,6 @@ bool Net2Remote_Connect(void) Session.PlayingAgainstVersion = VerNum.Version_Number(); Set_Scenario_Info_From_Index(Session.Options.ScenarioIndex); - WS_Destroy_Dialog(NULL, NULL); _netresponse = 0; //------------------------------------------------------------------------ @@ -931,15 +855,11 @@ bool Net2Remote_Connect(void) // Pop up the New Network Game dialog; if user selects OK, return // 'true'; otherwise, return to the Join Dialog. //.................................................................. - HWND host_dialog = WS_Create_Dialog(ProgramInstance, IDD_MPLAYER_HOST, MainWindow, MPlayer_Host_Dialog_Proc, 0); - Center_Window_Within_Window(host_dialog); - OwnerDraw::Subclass_Dialog(host_dialog, 0); - SendMessage(host_dialog, OD_SETTOP, 0, 1); - ShowWindow(host_dialog, SW_SHOWNORMAL); + Lobby_Open_Screen(UILobbyPresenterClass::SCREEN_HOST); } } - if (_netresponse != 1 || WS_Top_Window_ID() != IDD_MPLAYER_GUEST) { + if (_netresponse != 1 || Net2LobbyScreenID() != IDD_MPLAYER_GUEST) { if (_netresponse == IDC_GO) { Net2GameStarted = 0; Session.Write_MultiPlayer_Settings(); @@ -951,7 +871,8 @@ bool Net2Remote_Connect(void) if (Session.Players.Count() == 1) { PMessagePrintf(-1, Fetch_String(TXT_ONLY_ONE)); _netresponse = 0; - EnableWindow(GetDlgItem(WS_Top_Window(), IDC_GO), TRUE); + screen.CanStart = true; + Net2GameStarted = false; } if (_netresponse == IDC_GO) { @@ -959,8 +880,9 @@ bool Net2Remote_Connect(void) if (Session.Players[i]->Player.Status == 0) { PMessagePrintf(-1, Fetch_String(TXT_ACCEPTFIRST)); _netresponse = 0; - EnableWindow(GetDlgItem(WS_Top_Window(), IDC_GO), TRUE); - break; + screen.CanStart = true; + Net2GameStarted = false; + break; } } } @@ -972,7 +894,7 @@ bool Net2Remote_Connect(void) * The guest accepted the host's "go" -- tear down the dialogs, run * the pregame setup, compute the packet timing and leave the loop. */ - while (WS_Destroy_Dialog(NULL, 0) == true) {} + UI_Lobby_Close_Views(); _netresponse = 0; PregameSetup(); @@ -995,10 +917,16 @@ bool Net2Remote_Connect(void) break; } + // The AI player count this check adds in has always been zero: it is read from the + // game list dialog, whose template carries no IDC_AIPLAYERS, so the track bar it asks + // is not there to answer. Preserved rather than repaired, and reported separately. + int const ai_players = 0; + int waypoints = RandomMapWaypointCount(Session.Options.ScenarioIndex); - if (waypoints < SendDlgItemMessage(game_list_dialog, IDC_AIPLAYERS, TBM_GETPOS, 0, 0) + Session.Players.Count()) { + if (waypoints < ai_players + Session.Players.Count()) { PMessagePrintf(-1, Fetch_String(TXT_SCENARIO_TOO_SMALL)); - EnableWindow(GetDlgItem(WS_Top_Window(), IDC_GO), TRUE); + screen.CanStart = true; + Net2GameStarted = false; _netresponse = 0; } else { if (_netresponse != IDC_GO) { @@ -1120,7 +1048,7 @@ bool Net2Remote_Connect(void) Hide_Mouse(); Draw_Menu_Background(); Show_Mouse(); - WS_Destroy_Dialog(NULL, 0); + UI_Lobby_Close_Views(); break; } } @@ -1128,786 +1056,13 @@ bool Net2Remote_Connect(void) Session.NetOpen = false; Session.Write_MultiPlayer_Settings(); + UI_Lobby_Close_Views(); + UI_Set_Lobby_Screen(NULL); return(true); } /* end of Remote_Connect */ -/// -/// Handles the multiplayer game list dialog. -/// This is the lobby a player lands in before hosting or joining anything. It keeps the -/// game and user lists current, carries the lobby chat, and records which button was -/// pressed so that the driver loop knows whether to move on to the host or guest dialog. -/// -/// Returns with TRUE if the message was consumed by this dialog. -INT_PTR CALLBACK MPlayer_Game_List_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - switch (message) { - - case WM_INITDIALOG: { - CurGame = 0; - Net2IsGameListActive = 1; - - SendDlgItemMessage(window, IDC_YOURNAME, EM_SETLIMITTEXT, 16, 0); - SetWindowText(GetDlgItem(window, IDC_YOURNAME), Session.Handle); - - Session.Options.ScenarioDescription[0] = '\0'; - Session.ColorIdx = Session.PrefColor; - - Clear_Vector(&Session.Games); - Clear_Vector(&Session.Players); - Clear_Vector(&Session.Chat); - - NodeNameType * who = new NodeNameType; - strcpy(who->Name, Session.Handle); - who->Chat.LastTime = 0; - who->Chat.LastChance = 0; - who->Chat.Color = Session.GPacket.PlayerInfo.Color; - Session.Chat.Add(who); - - NodeNameType * game = new NodeNameType; - strcpy(game->Name, ""); - game->Game.IsOpen = 0; - game->Game.LastTime = 0; - Session.Games.Add(game); - - Send_Join_Queries(true, false, true, true); - return(0); - } - - case WM_COMMAND: { - switch (LOWORD(wparam)) { - - case IDC_YOURNAME: { - char name_buf[64]; - - SendDlgItemMessage(window, IDC_YOURNAME, WM_GETTEXT, 63, (LPARAM)name_buf); - - if (strcmp(name_buf, Session.Handle)) { - if (UTF8::Copy(Session.Handle, sizeof(Session.Handle), name_buf) < strlen(name_buf)) { - SetDlgItemText(window, IDC_YOURNAME, Session.Handle); - } - Send_Join_Queries(0, 0, 1, 0); - _Net2DisplayUsers(); - } - - return(0); - } - - case IDCANCEL: { - _netresponse = IDCANCEL; - return(0); - } - - case IDC_GAMELIST_NEW: { - _netresponse = IDC_GAMELIST_NEW; - return(0); - } - - case IDC_INPUT: { - char text[260]; - - SendDlgItemMessage(window, IDC_INPUT, WM_GETTEXT, 256, (LPARAM)text); - - int len = strlen(text); - - if (HIWORD(wparam) == EN_MAXTEXT) { - SendDlgItemMessage(window, IDC_INPUT, WM_SETTEXT, 0, (LPARAM) ""); - - if (len > 2) { - - PMessagePrintf(ColorMe, "[%s] %s", Session.Handle, text); - - GlobalPacketType gpacket; - memset(&gpacket, 0, sizeof(gpacket)); - - gpacket.Command = NET_MESSAGE; - strcpy(gpacket.Name, Session.Handle); - strcpy(gpacket.Message.Buf, text); - gpacket.Message.Color = Session.ColorIdx; - gpacket.Message.NameCRC = Compute_Name_CRC(Session.GameName); - - if (JoinState == JOIN_CONFIRMED) { - for (int i = 1; i < Session.Players.Count(); ++i) { - Ipx.Send_Global_Message(&gpacket, sizeof(gpacket), 1, &Session.Players[i]->Address); - Call_Back(); - } - } else { - for (int i = 1; i < Session.Chat.Count(); ++i) { - Ipx.Send_Global_Message(&gpacket, sizeof(gpacket), 1, &Session.Chat[i]->Address); - Call_Back(); - } - } - } - } - - return(0); - } - - case IDC_YOURCOLOR: { - if (HIWORD(wparam) == LBN_SELCHANGE) { - Session.ColorIdx = SendDlgItemMessage(window, IDC_YOURCOLOR, LB_GETCURSEL, 0, 0); - } - return(0); - } - - case IDC_GAMELIST: { - - if (JoinState > JOIN_NOTHING) { - return(0); - } - - int old_game = CurGame; - LRESULT sel = SendDlgItemMessage(window, IDC_GAMELIST, LB_GETCURSEL, 0, 0); - - if (sel >= 0 && Net2IsGameListActive) { - CurGame = sel; - strcpy(Session.GameName, Session.Games[sel]->Name); - } - - if (HIWORD(wparam) == LBN_SELCHANGE) { - Clear_Vector(&Session.Players); - - if (old_game != CurGame) { - Send_Join_Queries(1, 1, 1, 0); - } - - _Net2DisplayUsers(); - return(0); - } - - if (HIWORD(wparam) == LBN_DBLCLK) { - _netresponse = IDC_GAMELIST_JOIN; - return(0); - } - - return(0); - } - - case IDC_GAMELIST_JOIN: { - _netresponse = IDC_GAMELIST_JOIN; - return(0); - } - } - - return(0); - } - - case OD_SUBCLASSED: { - Net2DisplayGameList(); - _Net2DisplayUsers(); - OwnerDraw::Draw_Dialog_Back(window); - return(0); - } - - case WM_DRAWITEM: - OwnerDraw::Draw_Item((DRAWITEMSTRUCT *)lparam); - return(1); - - case WM_PAINT: - OwnerDraw::Draw_Dialog_Back(window); - ValidateRect(window, NULL); - return(0); - - case WM_ERASEBKGND: - return(1); - } - - return(0); -} - - -/// -/// Handles the multiplayer host dialog. -/// This is the setup dialog belonging to the player who created the game. It owns the -/// game option controls, the scenario picker, and the player list along with the means to -/// kick somebody out of it -- and finally the button that starts the match. -/// -/// Returns with TRUE if the message was consumed by this dialog. -INT_PTR CALLBACK MPlayer_Host_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - /* - * ------------------------------------------------------------------------ - * When 'Net2GameStarted' is set the game is in progress; this whole - * preliminary dispatch is skipped. Option checkbox and slider changes - * are applied here first, and then the message falls through to the - * full dispatch. - * ------------------------------------------------------------------------ - */ - if (!Net2GameStarted) { - - switch (message) { - - case WM_COMMAND: - - switch (LOWORD(wparam)) { - - /* - * ................................................................ - * Bases. Turning bases off also forces the "short game" option off. - * ................................................................ - */ - case IDC_BASES: - Session.Options.Bases = false; - if (SendDlgItemMessage(window, IDC_BASES, BM_GETCHECK, 0, 0) == BST_CHECKED) { - Session.Options.Bases = true; - } else if (!Session.Options.Bases) { - Session.Options.ShortGame = false; - SendDlgItemMessage(window, IDC_SHORT_GAME, BM_SETCHECK, 0, 0); - } - break; - - /* - * ................................................................ - * Redeploy MCV. - * ................................................................ - */ - case IDC_REDEPLOY_MCV: - Session.Options.MCVRedeploy = false; - if (SendDlgItemMessage(window, IDC_REDEPLOY_MCV, BM_GETCHECK, 0, 0) == BST_CHECKED) { - Session.Options.MCVRedeploy = true; - } - break; - - /* - * ................................................................ - * Crates / goodies. - * ................................................................ - */ - case IDC_CRATES: - Session.Options.Goodies = false; - if (SendDlgItemMessage(window, IDC_CRATES, BM_GETCHECK, 0, 0) == BST_CHECKED) { - Session.Options.Goodies = true; - } - break; - - /* - * ................................................................ - * Short game. Turning the short game on also forces bases on. - * ................................................................ - */ - case IDC_SHORT_GAME: - Session.Options.ShortGame = false; - if (SendDlgItemMessage(window, IDC_SHORT_GAME, BM_GETCHECK, 0, 0) == BST_CHECKED) { - Session.Options.ShortGame = true; - if (!Session.Options.Bases) { - Session.Options.Bases = true; - SendDlgItemMessage(window, IDC_BASES, BM_SETCHECK, 1, 0); - } - } - break; - - /* - * ................................................................ - * Multiplayer engineers. - * ................................................................ - */ - case IDC_MULTI_ENGINEER: - Session.Options.CrapEngineers = false; - if (SendDlgItemMessage(window, IDC_MULTI_ENGINEER, BM_GETCHECK, 0, 0) == BST_CHECKED) { - Session.Options.CrapEngineers = true; - } - break; - - /* - * ................................................................ - * Bridge destruction. - * ................................................................ - */ - case IDC_BRIDGE_DESTROY: - Session.Options.BridgeDestruction = false; - if (SendDlgItemMessage(window, IDC_BRIDGE_DESTROY, BM_GETCHECK, 0, 0) == BST_CHECKED) { - Session.Options.BridgeDestruction = true; - } - break; - - /* - * ................................................................ - * Allies allowed. - * ................................................................ - */ - case IDC_ALLIES: - Session.Options.AlliesAllowed = false; - if (SendDlgItemMessage(window, IDC_ALLIES, BM_GETCHECK, 0, 0) == BST_CHECKED) { - Session.Options.AlliesAllowed = true; - } - break; - - /* - * ................................................................ - * Harvester truce. - * ................................................................ - */ - case IDC_HARVTRUCE: - Session.Options.HarvTruce = false; - if (SendDlgItemMessage(window, IDC_HARVTRUCE, BM_GETCHECK, 0, 0) == BST_CHECKED) { - Session.Options.HarvTruce = true; - } - break; - - /* - * ................................................................ - * Fog of war. - * ................................................................ - */ - case IDC_FOG_OF_WAR: - Session.Options.FogOfWar = false; - if (SendDlgItemMessage(window, IDC_FOG_OF_WAR, BM_GETCHECK, 0, 0) == BST_CHECKED) { - Session.Options.FogOfWar = true; - } - break; - } - break; - - /* - * .................................................................... - * The option sliders are read back per-control here; the values are - * all unconditionally re-read by the main WM_HSCROLL/WM_VSCROLL - * handler below. - * .................................................................... - */ - case WM_HSCROLL: - if (GetDlgItem(window, IDC_AILEVEL_SLIDER) == (HWND)lparam) { - Session.Options.AIDifficulty = (DiffType)SendDlgItemMessage(window, IDC_AILEVEL_SLIDER, TBM_GETPOS, 0, 0); - } - if (GetDlgItem(window, IDC_AIPLAYERS) == (HWND)lparam) { - Session.Options.AIPlayers = SendDlgItemMessage(window, IDC_AIPLAYERS, TBM_GETPOS, 0, 0); - } - if (GetDlgItem(window, IDC_UNITCOUNT) == (HWND)lparam) { - Session.Options.UnitCount = SendDlgItemMessage(window, IDC_UNITCOUNT, TBM_GETPOS, 0, 0); - } - if (GetDlgItem(window, IDC_TECHLEVEL) == (HWND)lparam) { - BuildLevel = SendDlgItemMessage(window, IDC_TECHLEVEL, TBM_GETPOS, 0, 0); - } - if (GetDlgItem(window, IDC_CREDITS) == (HWND)lparam) { - Session.Options.Credits = SendDlgItemMessage(window, IDC_CREDITS, TBM_GETPOS, 0, 0); - } - if (GetDlgItem(window, IDC_GAME_SPEED_SLIDER) == (HWND)lparam) { - Session.Options.GameSpeed = 6 - SendDlgItemMessage(window, IDC_GAME_SPEED_SLIDER, TBM_GETPOS, 0, 0); - } - break; - - /* - * .................................................................... - * Refresh the game option controls. - * .................................................................... - */ - case WM_INITDIALOG: - case OD_SUBCLASSED: - DisplayGameopts(window, 1); - break; - } - } - - switch (message) { - - case WM_DESTROY: - return(0); - - case WM_DRAWITEM: - OwnerDraw::Draw_Item((DRAWITEMSTRUCT *)lparam); - - case WM_ERASEBKGND: - return(TRUE); - - case WM_PAINT: - OwnerDraw::Draw_Dialog_Back(window); - if (MultiplayerMapPreview != NULL) { - MultiplayerMapPreview->Blit_Preview(window); - } - ValidateRect(window, NULL); - return(0); - - case WM_INITDIALOG: { - VerNum.Init_Clipping(); - - srand(NonCriticalRandomNumber(1, 0x7FFF)); - Seed = rand(); - - Set_Scenario_Info_From_Index(0); - Session.Options.ScenarioIndex = 0; - - Center_Window_Within_Window(window); - - Fill_Country_Box(GetDlgItem(window, IDC_YOURSIDE)); - Select_Country_In_Box(GetDlgItem(window, IDC_YOURSIDE), Session.House); - - SendDlgItemMessage(window, IDC_YOURCOLOR, CB_RESETCONTENT, 0, 0); - - SendDlgItemMessage(window, IDC_YOURCOLOR, CB_INSERTSTRING, -1, (LPARAM)Fetch_String(TXT_GOLD)); - SendDlgItemMessage(window, IDC_YOURCOLOR, CB_INSERTSTRING, -1, (LPARAM)Fetch_String(TXT_RED)); - SendDlgItemMessage(window, IDC_YOURCOLOR, CB_INSERTSTRING, -1, (LPARAM)Fetch_String(TXT_BLUE)); - SendDlgItemMessage(window, IDC_YOURCOLOR, CB_INSERTSTRING, -1, (LPARAM)Fetch_String(TXT_GREEN)); - SendDlgItemMessage(window, IDC_YOURCOLOR, CB_INSERTSTRING, -1, (LPARAM)Fetch_String(TXT_ORANGE)); - SendDlgItemMessage(window, IDC_YOURCOLOR, CB_INSERTSTRING, -1, (LPARAM)Fetch_String(TXT_SKY_BLUE)); - SendDlgItemMessage(window, IDC_YOURCOLOR, CB_INSERTSTRING, -1, (LPARAM)Fetch_String(TXT_PURPLE)); - SendDlgItemMessage(window, IDC_YOURCOLOR, CB_INSERTSTRING, -1, (LPARAM)Fetch_String(TXT_PINK)); - - SendDlgItemMessage(window, IDC_YOURCOLOR, CB_SETCURSEL, Session.ColorIdx, 0); - - SendDlgItemMessage(window, IDC_AILEVEL_SLIDER, LB_RESETCONTENT, 0, 0); - - for (int j = 0; j < Session.Scenarios.Count(); ++j) { - SendDlgItemMessage(window, IDC_AILEVEL_SLIDER, LB_INSERTSTRING, -1, (LPARAM)Session.Scenarios[j]); - } - - SendDlgItemMessage(window, IDC_SCENARIONAME, WM_SETTEXT, 0, (LPARAM)Session.Options.ScenarioDescription); - - Update_Network_Dialog_Preview(window); - - SendMessage(window, WM_COMMAND, MAKEWPARAM(IDC_YOURSIDE, CBN_SELCHANGE), (LPARAM)GetDlgItem(window, IDC_YOURSIDE)); - SendMessage(window, WM_COMMAND, MAKEWPARAM(IDC_YOURCOLOR, CBN_SELCHANGE), (LPARAM)GetDlgItem(window, IDC_YOURCOLOR)); - - return(0); - } - - case WM_HSCROLL: - case WM_VSCROLL: { - if (Net2GameStarted) return(0); - - Session.Options.UnitCount = SendDlgItemMessage(window, IDC_UNITCOUNT, TBM_GETPOS, 0, 0); - BuildLevel = SendDlgItemMessage(window, IDC_TECHLEVEL, TBM_GETPOS, 0, 0); - Session.Options.Credits = SendDlgItemMessage(window, IDC_CREDITS, TBM_GETPOS, 0, 0); - Session.Options.AIPlayers = SendDlgItemMessage(window, IDC_AIPLAYERS, TBM_GETPOS, 0, 0); - Session.Options.AIDifficulty = (DiffType)SendDlgItemMessage(window, IDC_AILEVEL_SLIDER, TBM_GETPOS, 0, 0); - Session.Options.GameSpeed = 6 - SendDlgItemMessage(window, IDC_GAME_SPEED_SLIDER, TBM_GETPOS, 0, 0); - - return(0); - } - - case WM_COMMAND: { - switch (LOWORD(wparam)) { - - case IDC_YOURSIDE: - if (HIWORD(wparam) == CBN_SELCHANGE && !Net2GameStarted) { - Session.House = Country_From_Box(GetDlgItem(window, IDC_YOURSIDE)); - Session.Players[0]->Player.House = Session.House; - - PumpGameopts(1, 0); - _Net2DisplayUsers(); - } - return(0); - - case IDC_INPUT: - { - char text[260]; - SendDlgItemMessage(window, IDC_INPUT, WM_GETTEXT, 256, (LPARAM)text); - - int len = strlen(text); - - if (HIWORD(wparam) != EN_MAXTEXT) return(0); - - SendDlgItemMessage(window, IDC_INPUT, WM_SETTEXT, 0, (LPARAM)""); - - if (len <= 2) return(0); - - PMessagePrintf(ColorMe, "[%s] %s", Session.Handle, text); - - GlobalPacketType gpacket; - memset(&gpacket, 0, sizeof(gpacket)); - - gpacket.Command = NET_MESSAGE; - strcpy(gpacket.Name, Session.Handle); - strcpy(gpacket.Message.Buf, text); - gpacket.Message.Color = Session.ColorIdx; - gpacket.Message.NameCRC = Compute_Name_CRC(Session.GameName); - - - if (JoinState == JOIN_CONFIRMED) { - for (int i = 1; i < Session.Players.Count(); ++i) { - Ipx.Send_Global_Message(&gpacket, sizeof(gpacket), 1, &Session.Players[i]->Address); - Call_Back(); - } - } else { - for (int i = 1; i < Session.Chat.Count(); ++i) { - Ipx.Send_Global_Message(&gpacket, sizeof(gpacket), 1, &Session.Chat[i]->Address); - Call_Back(); - } - } - - return(0); - } - - case IDCANCEL: { - if (Net2GameStarted) return(0); - - if (MultiplayerMapPreview != NULL) { - delete MultiplayerMapPreview; - MultiplayerMapPreview = NULL; - } - - _netresponse = IDCANCEL; - return(0); - } - - /* - * .................................................................... - * The user picked a color. Resolve it against the colors already in - * use, skipping our own slot (index 0). If the chosen color is taken, - * bump forward (mod 8) until a free color is found; if that changed - * the color from what the user wanted, warn and re-resolve starting - * from our previous color. - * .................................................................... - */ - case IDC_YOURCOLOR: - if (HIWORD(wparam) == CBN_SELCHANGE && !Net2GameStarted) { - - int old_color = Session.ColorIdx; - - Session.ColorIdx = SendDlgItemMessage(window, IDC_YOURCOLOR, CB_GETCURSEL, 0, 0); - Session.PrefColor = Session.ColorIdx; - - int newcolor; - int found; - int color = Session.ColorIdx; - for (;;) { - int count = 0; - newcolor = color; - found = FALSE; - - while (count < Session.Players.Count()) { - if (count != 0 && Session.Players[count]->Player.Color == color) { - color++; - found = TRUE; - } - count++; - } - - if (!found) break; - - color %= MAX_MPLAYER_COLORS; - } - - int resolved = newcolor; - if (newcolor != Session.ColorIdx) { - PMessagePrintf(ColorSystem, Fetch_String(TXT_COLOR_IN_USE)); - - color = old_color; - for (;;) { - int count = 0; - old_color = color; - found = FALSE; - - while (count < Session.Players.Count()) { - if (count != 0 && Session.Players[count]->Player.Color == color) { - color++; - found = TRUE; - } - count++; - } - - if (!found) break; - - color %= MAX_MPLAYER_COLORS; - } - - resolved = old_color; - } - - Session.ColorIdx = resolved; - Session.Players[0]->Player.Color = resolved; - - SendDlgItemMessage(window, IDC_YOURCOLOR, CB_SETCURSEL, Session.ColorIdx, 0); - - _Net2DisplayUsers(); - PumpGameopts(1, 0); - } - return(0); - - case IDC_CRATES: - if (Net2GameStarted) return(0); - Session.Options.Goodies = false; - if (SendDlgItemMessage(window, IDC_CRATES, BM_GETCHECK, 0, 0) != BST_CHECKED) return(0); - Session.Options.Goodies = true; - return(0); - - case IDC_BASES: - if (Net2GameStarted) return(0); - Session.Options.Bases = false; - if (SendDlgItemMessage(window, IDC_BASES, BM_GETCHECK, 0, 0) != BST_CHECKED) return(0); - Session.Options.Bases = true; - return(0); - - case IDC_SHORT_GAME: - if (Net2GameStarted) return(0); - Session.Options.ShortGame = false; - if (SendDlgItemMessage(window, IDC_SHORT_GAME, BM_GETCHECK, 0, 0) != BST_CHECKED) return(0); - Session.Options.ShortGame = true; - return(0); - - /* - * .................................................................... - * Toggle the "go" (start game) flag. Disable the button and signal - * the driver loop to begin the game. - * .................................................................... - */ - case IDC_GO: - EnableWindow(GetDlgItem(window, IDC_GO), FALSE); - Net2GameStarted = true; - _netresponse = IDC_GO; - return(0); - - /* - * .................................................................... - * Kick the selected players from the game. The host list-box (on the - * host dialog) holds the player rows; capture the selected names into - * a dictionary, then for each name (other than our own) find the - * matching player and send a NET_REJECT_JOIN kick packet. - * .................................................................... - */ - case IDC_KICK: - { - HWND userwin = GetDlgItem(WS_Find_Dialog(IDD_MPLAYER_HOST), IDC_USERS); - - Wstring name; - Dictionary lbdict(Wstring_Hash); - - LBSaveSelections(userwin, lbdict); - - while (lbdict.getEntries()) { - bool value; - lbdict.removeAny(name, value); - - if (strcmp(name.get(), Session.Handle)) { - int index = -1; - for (int i = 0; i < Session.Players.Count(); ++i) { - if (!strcmp(name.get(), Session.Players[i]->Name)) { - index = i; - break; - } - } - - if (index != -1) { - memset(&Session.GPacket, 0, sizeof(Session.GPacket)); - Session.GPacket.Command = NET_REJECT_JOIN; - Session.GPacket.Reject.Why = (int)REJECT_BY_OWNER; - Ipx.Send_Global_Message(&Session.GPacket, 455, 1, &Session.Players[index]->Address); - } - } - } - - SendMessage(userwin, LB_SELITEMRANGE, 0, MAKELPARAM(0, -1)); - _Net2DisplayUsers(); - return(0); - } - - /* - * .................................................................... - * Pick a different scenario. If "RandMap.Sed" is chosen, rebuild the - * map preview from "RandMap.img". - * .................................................................... - */ - case IDC_MULTIMAP: { - if (Net2GameStarted) return(0); - - int old = Session.Options.ScenarioIndex; - - ShowWindow(window, SW_HIDE); - IsRandomMap = false; - - if (Scenario_Dialog(MainWindow) == 2) { - Session.Options.ScenarioIndex = old; - Set_Scenario_Info_From_Index(Session.Options.ScenarioIndex); - Update_Network_Dialog_Preview(window); - IsRandomMap = true; - ShowWindow(window, SW_SHOW); - - if (!stricmp((char *)Session.Scenarios[Session.Options.ScenarioIndex] + DESCRIP_MAX, "RandMap.Sed")) { - if (MultiplayerMapPreview != NULL) { - delete MultiplayerMapPreview; - } - MultiplayerMapPreview = new MapPreviewClass; - MultiplayerMapPreview->Read_PCX_Preview("RandMap.img"); - if (MultiplayerMapPreview->Get_Preview_Surface() == NULL) { - Update_Network_Dialog_Preview(window); - } - InvalidateRect(window, NULL, FALSE); - } else { - Update_Network_Dialog_Preview(window); - } - - PumpGameopts(1, 0); - InvalidateRect(window, NULL, FALSE); - } else { - if (!Set_Scenario_Info_From_Index(Session.Options.ScenarioIndex)) { - Session.Options.ScenarioIndex = old; - } - IsRandomMap = true; - ShowWindow(window, SW_SHOW); - - SendDlgItemMessage(window, IDC_SCENARIONAME, WM_SETTEXT, 0, (LPARAM)Session.Options.ScenarioDescription); - - if (!stricmp((char *)Session.Scenarios[Session.Options.ScenarioIndex] + DESCRIP_MAX, "RandMap.Sed")) { - if (MultiplayerMapPreview != NULL) { - delete MultiplayerMapPreview; - } - MultiplayerMapPreview = new MapPreviewClass; - MultiplayerMapPreview->Read_PCX_Preview("RandMap.img"); - if (MultiplayerMapPreview->Get_Preview_Surface() == NULL) { - Update_Network_Dialog_Preview(window); - } - InvalidateRect(window, NULL, FALSE); - } else { - Update_Network_Dialog_Preview(window); - } - } - - return(0); - } - - case IDC_BRIDGE_DESTROY: - if (Net2GameStarted) return(0); - Session.Options.BridgeDestruction = false; - if (SendDlgItemMessage(window, IDC_BRIDGE_DESTROY, BM_GETCHECK, 0, 0) != BST_CHECKED) return(0); - Session.Options.BridgeDestruction = true; - return(0); - - case IDC_MULTI_ENGINEER: - if (Net2GameStarted) return(0); - Session.Options.CrapEngineers = false; - if (SendDlgItemMessage(window, IDC_MULTI_ENGINEER, BM_GETCHECK, 0, 0) != BST_CHECKED) return(0); - Session.Options.CrapEngineers = true; - return(0); - - default: - return(0); - } - } - - case OD_SUBCLASSED: { - Net2_g_Col_Accept = 5; - Net2_g_Col_Name = 45; - Net2_g_Col_House = 25; - - SendDlgItemMessage(window, IDC_USERS, OD_ADDCOLUMN, 0, Net2_g_Col_Name); - SendDlgItemMessage(window, IDC_USERS, OD_ADDCOLUMN, 0, Net2_g_Col_House); - SendDlgItemMessage(window, IDC_USERS, OD_ADDCOLUMN, 0, Net2_g_Col_Accept); - SendDlgItemMessage(window, IDC_USERS, OD_TOOLTIPS, 0, 1); - - for (int i = 0; i < ARRAY_SIZE(PlayerColorTable); i++) { - SendDlgItemMessage(window, IDC_YOURCOLOR, OD_SETCOLOR, i, (LPARAM)PlayerColorTable[i]); - } - - SendDlgItemMessage(window, IDC_KICK, OD_TOOLTIPS, 0, 1); - SendDlgItemMessage(window, IDC_KICK, OD_SETIMAGE, 0, (LPARAM)SurfaceCache.GetSurface("woukick.pcx")); - SendDlgItemMessage(window, IDC_KICK, OD_SETALTIMAGE, 0, (LPARAM)SurfaceCache.GetSurface("wodkick.pcx")); - - _Net2DisplayUsers(); - Net2DisplayGameList(); - return(0); - } - - case OD_GETTIPTEXT: { - HWND ctrl = GetDlgItem(window, wparam); - GetWindowText(ctrl, (LPSTR)lparam, 127); - return(0); - } - } - - return(0); -} - - /*************************************************************************** * Request_To_Join -- Sends a JOIN request packet to game owner * * * @@ -2127,7 +1282,7 @@ static void Unjoin_Game(int game_index) * 02/14/1995 BR : Created. * * 04/15/1995 BRR : Created. * *=============================================================================================*/ -static void Send_Join_Queries(int gamenow, int playernow, int chatnow, int init) +void Send_Join_Queries(int gamenow, int playernow, int chatnow, int init) { GlobalPacketType packet = {}; @@ -2161,7 +1316,7 @@ static void Send_Join_Queries(int gamenow, int playernow, int chatnow, int init) if (!game_timer || gamenow) { game_timer = GAME_QUERY_TIME; - if ((WS_Top_Window_ID() != IDD_MPLAYER_HOST) || gamenow) { + if ((Net2LobbyScreenID() != IDD_MPLAYER_HOST) || gamenow) { memset (&packet, 0, sizeof(GlobalPacketType)); packet.Command = NET_QUERY_GAME; @@ -2298,6 +1453,19 @@ bool Process_Global_Packet(GlobalPacketType *packet, IPXAddressClass *address) } /* end of Process_Global_Packet */ +static NetGlobal::RejectionCounters LobbyPacketRejections; + + +/// Records a rejected lobby global packet. +static void Record_Lobby_Packet_Rejection(NetGlobal::DecodeError error) +{ + NetGlobal::RejectionRecord const record = LobbyPacketRejections.Record(error); + if (record.ShouldLog) { + DebugString("Lobby global packet drop [%s]: %u\n", NetGlobal::Error_Name(error), record.Count); + } +} + + /*********************************************************************************************** * Get_Join_Responses -- sends queries for the Join Dialog * * * @@ -2360,6 +1528,20 @@ static void Get_Join_Responses(void) continue; } + NetGlobal::DecodeError const admission = NetGlobal::Validate_Lobby_Packet(Session.GPacket, static_cast(Session.GPacketlen)); + if (admission != NetGlobal::DecodeError::NONE) { + Record_Lobby_Packet_Rejection(admission); + continue; + } + + if (Session.GPacket.Command == NET_QUERY_JOIN || Session.GPacket.Command == NET_ANSWER_PLAYER + || Session.GPacket.Command == NET_CONFIRM_JOIN) { + if (!Lobby_Seat_Is_Valid(Session.GPacket.PlayerInfo.House, Session.GPacket.PlayerInfo.Color)) { + Record_Lobby_Packet_Rejection(NetGlobal::DecodeError::INVALID_HOUSE); + continue; + } + } + //------------------------------------------------------------------------ // If we're joined in a game, handle the packet in a standard way; otherwise, // don't answer standard queries. @@ -2369,14 +1551,14 @@ static void Get_Join_Responses(void) } if (Session.GPacket.Command==NET_PREVIEW_MODE) { - if (WS_Top_Window_ID() == IDD_MPLAYER_GUEST) { + if (Net2LobbyScreenID() == IDD_MPLAYER_GUEST) { Receive_Random_Map_Preview(); } continue; } if (Session.GPacket.Command==NET_REQ_PREVIEW) { - if (WS_Top_Window_ID() == IDD_MPLAYER_HOST) { + if (Net2LobbyScreenID() == IDD_MPLAYER_HOST) { Send_Preview_To_Guests(); } continue; @@ -2418,16 +1600,16 @@ static void Get_Join_Responses(void) //............................................................ if (JoinState < JOIN_CONFIRMED) { if (Session.Games[i]->Game.IsOpen) { - wsprintf(txt,Fetch_String(TXT_S_FORMED_NEW_GAME), + snprintf(txt, sizeof(txt), Fetch_String(TXT_S_FORMED_NEW_GAME), Session.GPacket.Name); Sound_Effect(Rule->GameForming); } else { - wsprintf(txt,Fetch_String(TXT_GAME_NOW_IN_PROGRESS), + snprintf(txt, sizeof(txt), Fetch_String(TXT_GAME_NOW_IN_PROGRESS), Session.GPacket.Name); Sound_Effect(Rule->GameClosed); } - PMessagePrintf(ColorSystem, txt); + PMessagePrintf(ColorSystem, "%s", txt); } } break; @@ -2466,9 +1648,9 @@ static void Get_Join_Responses(void) // now available. //.................................................................. if (Session.GPacket.GameInfo.IsOpen && JoinState < JOIN_CONFIRMED) { - wsprintf(txt,Fetch_String(TXT_S_FORMED_NEW_GAME), + snprintf(txt, sizeof(txt), Fetch_String(TXT_S_FORMED_NEW_GAME), Session.GPacket.Name); - PMessagePrintf(ColorSystem, txt); + PMessagePrintf(ColorSystem, "%s", txt); Sound_Effect(Rule->GameForming); } @@ -2494,7 +1676,11 @@ static void Get_Join_Responses(void) // house into the existing entry, in case they've changed it without // our knowledge; set the 'found' flag so we won't create a new entry. //.................................................................. - if (Session.Players[i]->Address==Session.GAddress) { + // The name settles it as well as the address, because the join path + // keys on the name and refuses a duplicate one, so a player already + // on the roster is this player however many addresses he answers from. + if (Session.Players[i]->Address==Session.GAddress + || !strcmp(Session.Players[i]->Name, Session.GPacket.Name)) { found = 1; break; } @@ -2526,7 +1712,7 @@ static void Get_Join_Responses(void) //.................................................................. who = new NodeNameType; UTF8::Copy(who->Name, sizeof(who->Name), Session.GPacket.Name); - strcpy(who->Player.Serial, Session.GPacket.Serial); + UTF8::Copy(who->Player.Serial, sizeof(who->Player.Serial), Session.GPacket.Serial); who->Address = Session.GAddress; who->Player.House = Session.GPacket.PlayerInfo.House; who->Player.Color = Session.GPacket.PlayerInfo.Color; @@ -2547,7 +1733,9 @@ static void Get_Join_Responses(void) NodeNameType * player = Session.Players[i]; if (strcmp(player->Name,Session.GameName) && player->Player.Status != 0) { player->Player.Status = 0; - EnableWindow(GetDlgItem(GameoptWindow(), IDC_ACCEPT), TRUE); + if (Lobby_Screen() != NULL) { + Lobby_Screen()->CanAccept = true; + } } } @@ -2571,7 +1759,7 @@ static void Get_Join_Responses(void) if (Session.GPacket.Command==NET_CONFIRM_JOIN) { if ( JoinState != JOIN_CONFIRMED) { JoinState = JOIN_CONFIRMED; - strcpy (Session.GameName, Session.GPacket.Name); + UTF8::Copy(Session.GameName, sizeof(Session.GameName), Session.GPacket.Name); Session.House = Session.GPacket.PlayerInfo.House; Session.ColorIdx = Session.GPacket.PlayerInfo.Color; @@ -2587,12 +1775,8 @@ static void Get_Join_Responses(void) Session.Players.Add (who); Net2IsGameListActive = false; - WS_Destroy_Dialog(0, 0); _netresponse = 0; - dialog = WS_Create_Dialog(ProgramInstance, IDD_MPLAYER_GUEST, MainWindow, MPlayer_Guest_Dialog_Proc, FALSE); - Center_Window_Within_Window(dialog); - OwnerDraw::Subclass_Dialog(dialog, 0); - ShowWindow(dialog, SW_SHOWNORMAL); + Lobby_Open_Screen(UILobbyPresenterClass::SCREEN_GUEST); display_users = true; Send_Join_Queries(1, 1, 1, 0); @@ -2685,10 +1869,10 @@ static void Get_Join_Responses(void) item = (char *)Fetch_String(TXT_SERIAL_DUP); } if (item) { - ODMessageBox(item, 0, Net2Callback, 0); + ODMessageBox(item, 0, Net2Callback); } - if ( WS_Top_Window_ID() != IDD_MPLAYER_GAME_LIST ) { - _netresponse = IDCANCEL; + if ( Net2LobbyScreenID() != IDD_MPLAYER_GAME_LIST ) { + Net2AnswerLobby(IDCANCEL); } Send_Join_Queries (0, 0, 1, 0); } @@ -2723,6 +1907,9 @@ static void Get_Join_Responses(void) tok = strtok(opts, ","); if (tok) { newhouse = atol(tok); + if (newhouse < 0 || newhouse >= HouseTypes.Count()) { + newhouse = oldhouse; + } Session.Players[i]->Player.House = newhouse; } @@ -2775,8 +1962,8 @@ static void Get_Join_Responses(void) //............................................................... if (i==CurGame) { Clear_Vector (&Session.Players); - if (WS_Top_Window_ID() != IDD_MPLAYER_GAME_LIST && WS_Top_Window_ID() == IDD_MPLAYER_GUEST) { - _netresponse = 2; + if (Net2LobbyScreenID() != IDD_MPLAYER_GAME_LIST && Net2LobbyScreenID() == IDD_MPLAYER_GUEST) { + Net2AnswerLobby(2); } } @@ -2868,7 +2055,9 @@ static void Get_Join_Responses(void) NodeNameType * player = Session.Players[i]; if (strcmp(player->Name,Session.GameName) && player->Player.Status != 0) { player->Player.Status = 0; - EnableWindow(GetDlgItem(GameoptWindow(), IDC_ACCEPT), TRUE); + if (Lobby_Screen() != NULL) { + Lobby_Screen()->CanAccept = true; + } } } } @@ -2885,11 +2074,11 @@ static void Get_Join_Responses(void) Session.MaxAhead = Session.GPacket.ResponseTime.OneWay; Session.HostAddress = Session.GAddress; Session.NumPlayers = Session.Players.Count(); - _netresponse = IDOK; + Net2AnswerLobby(IDOK); if (Session.GPacket.Command==NET_GO) { JoinState = JOIN_GAME_START; if (!Net2ReadyToGo(0)) { - _netresponse = 2; + Net2AnswerLobby(2); Net2GameStarted = false; } else { Net2GameStarted = true; @@ -2922,8 +2111,11 @@ static void Get_Join_Responses(void) //..................................................................... else { for (i = 0; i < Session.Chat.Count(); i++) { - if (Session.Chat[i]->Address==Session.GAddress) { - strcpy (Session.Chat[i]->Name, Session.GPacket.Name); + // The announcement names its sender, and that identifies the machine + // however many addresses its packets reach us from. + if (Session.Chat[i]->Chat.ID == Session.GPacket.Chat.ID + || Session.Chat[i]->Address==Session.GAddress) { + UTF8::Copy(Session.Chat[i]->Name, sizeof(Session.Chat[i]->Name), Session.GPacket.Name); Session.Chat[i]->Chat.LastTime = TickCount; Session.Chat[i]->Chat.LastChance = 0; Session.Chat[i]->Chat.Color = Session.GPacket.Chat.Color; @@ -2942,6 +2134,7 @@ static void Get_Join_Responses(void) who->Chat.LastTime = TickCount; who->Chat.LastChance = 0; who->Chat.Color = Session.GPacket.Chat.Color; + who->Chat.ID = Session.GPacket.Chat.ID; Session.Chat.Add (who); } @@ -3157,7 +2350,7 @@ static void Get_Join_Responses(void) UTF8::Copy(who->Name, sizeof(who->Name), Session.GPacket.Name); who->Address = Session.GAddress; who->Player.House = Session.GPacket.PlayerInfo.House; - strcpy(who->Player.Serial, Session.GPacket.Serial); + UTF8::Copy(who->Player.Serial, sizeof(who->Player.Serial), Session.GPacket.Serial); //.................................................................. // Set player's color; if requested color isn't used, give it to him; @@ -3313,187 +2506,3 @@ bool Net2ReadyToGo(int load_game) return(true); } - - -/// -/// Handles the multiplayer guest dialog. -/// This is the setup dialog a player works in after joining somebody else's game. The -/// guest picks a side and a color here, chats with the rest of the players, and tells the -/// host when it is happy for the game to begin. -/// -/// Returns with TRUE if the message was consumed by this dialog. -INT_PTR CALLBACK MPlayer_Guest_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - switch (message) { - - case WM_INITDIALOG: { - Fill_Country_Box(GetDlgItem(window, IDC_YOURSIDE)); - Select_Country_In_Box(GetDlgItem(window, IDC_YOURSIDE), Session.House); - - SendDlgItemMessage(window, IDC_YOURCOLOR, CB_RESETCONTENT, 0, 0); - - SendDlgItemMessage(window, IDC_YOURCOLOR, CB_INSERTSTRING, (WPARAM)-1, (LPARAM)Fetch_String(TXT_GOLD)); - SendDlgItemMessage(window, IDC_YOURCOLOR, CB_INSERTSTRING, (WPARAM)-1, (LPARAM)Fetch_String(TXT_RED)); - SendDlgItemMessage(window, IDC_YOURCOLOR, CB_INSERTSTRING, (WPARAM)-1, (LPARAM)Fetch_String(TXT_BLUE)); - SendDlgItemMessage(window, IDC_YOURCOLOR, CB_INSERTSTRING, (WPARAM)-1, (LPARAM)Fetch_String(TXT_GREEN)); - SendDlgItemMessage(window, IDC_YOURCOLOR, CB_INSERTSTRING, (WPARAM)-1, (LPARAM)Fetch_String(TXT_ORANGE)); - SendDlgItemMessage(window, IDC_YOURCOLOR, CB_INSERTSTRING, (WPARAM)-1, (LPARAM)Fetch_String(TXT_SKY_BLUE)); - SendDlgItemMessage(window, IDC_YOURCOLOR, CB_INSERTSTRING, (WPARAM)-1, (LPARAM)Fetch_String(TXT_PURPLE)); - SendDlgItemMessage(window, IDC_YOURCOLOR, CB_INSERTSTRING, (WPARAM)-1, (LPARAM)Fetch_String(TXT_PINK)); - - SendDlgItemMessage(window, IDC_YOURCOLOR, CB_SETCURSEL, Session.ColorIdx, 0); - - EnableWindow(GetDlgItem(window, IDC_ACCEPT), FALSE); - - int self_index = -1; - for (int i = 0; i < Session.Players.Count(); ++i) { - if (!strcmp(Session.Players[i]->Name, Session.Handle)) { - self_index = i; - } - } - - if (self_index != -1) { - Session.Players[self_index]->Player.Status = 0; - } - - _Net2DisplayUsers(); - Session.Options.ScenarioDescription[0] = '\0'; - - return(0); - } - - case WM_COMMAND: { - switch (LOWORD(wparam)) { - - case IDC_ACCEPT: { - Session.Players[0]->Player.Status = 1; - - char dest[64]; - sprintf(dest, "A1"); - SendPublicGameopts(dest); - - EnableWindow(GetDlgItem(window, IDC_ACCEPT), FALSE); - InvalidateRect(GetDlgItem(window, IDC_ACCEPT), NULL, FALSE); - - _Net2DisplayUsers(); - return(0); - } - - case IDC_YOURSIDE: - case IDC_YOURCOLOR: { - if (HIWORD(wparam) == CBN_SELCHANGE) { - int color = (int)SendDlgItemMessage(window, IDC_YOURCOLOR, CB_GETCURSEL, 0, 0); - - int house = Country_From_Box(GetDlgItem(window, IDC_YOURSIDE)); - - Session.PrefColor = color; - - char dest[64]; - sprintf(dest, "R%d,%d", house, color); - SendPrivateGameopts(Session.GameName, dest); - } - return(0); - } - - case IDCANCEL: { - if (!Net2GameStarted) { - _netresponse = IDCANCEL; - } - return(0); - } - - case IDC_INPUT: { - char text[260]; - - SendDlgItemMessage(window, IDC_INPUT, WM_GETTEXT, 256, (LPARAM)text); - - int len = strlen(text); - if (HIWORD(wparam) == EN_MAXTEXT) { - SendDlgItemMessage(window, IDC_INPUT, WM_SETTEXT, 0, (LPARAM)""); - - if (len > 2) { - PMessagePrintf(ColorMe, "[%s] %s", Session.Handle, text); - - GlobalPacketType gpacket; - memset(&gpacket, 0, sizeof(gpacket)); - - gpacket.Command = NET_MESSAGE; - strcpy(gpacket.Name, Session.Handle); - strcpy(gpacket.Message.Buf, text); - gpacket.Message.Color = Session.ColorIdx; - gpacket.Message.NameCRC = Compute_Name_CRC(Session.GameName); - - if (JoinState == JOIN_CONFIRMED) { - for (int i = 1; i < Session.Players.Count(); ++i) { - Ipx.Send_Global_Message(&gpacket, sizeof(gpacket), 1, &Session.Players[i]->Address); - Call_Back(); - } - } else { - for (int i = 1; i < Session.Chat.Count(); ++i) { - Ipx.Send_Global_Message(&gpacket, sizeof(gpacket), 1, &Session.Chat[i]->Address); - Call_Back(); - } - } - } - } - - return(0); - } - } - - return(0); - } - - case OD_SUBCLASSED: { - Net2_g_Col_Accept = 5; - Net2_g_Col_Name = 45; - Net2_g_Col_House = 25; - - SendDlgItemMessage(window, IDC_USERS, OD_ADDCOLUMN, 0, 45); - SendDlgItemMessage(window, IDC_USERS, OD_ADDCOLUMN, 0, Net2_g_Col_House); - SendDlgItemMessage(window, IDC_USERS, OD_ADDCOLUMN, 0, Net2_g_Col_Accept); - SendDlgItemMessage(window, IDC_USERS, OD_TOOLTIPS, 0, 1); - - for (int i = 0; i < ARRAY_SIZE(PlayerColorTable); i++) { - SendDlgItemMessage(window, IDC_YOURCOLOR, OD_SETCOLOR, i, (LPARAM)PlayerColorTable[i]); - } - - _Net2DisplayUsers(); - Net2DisplayGameList(); - DisplayGameopts(window, 1); - - HWND combo = GetDlgItem(window, IDC_YOURSIDE); - SendMessage(window, WM_COMMAND, MAKEWPARAM(IDC_YOURSIDE, CBN_SELCHANGE), (LPARAM)combo); - - return(0); - } - - case WM_DRAWITEM: - OwnerDraw::Draw_Item((DRAWITEMSTRUCT *)lparam); - return(1); - - case WM_DESTROY: { - if (MultiplayerMapPreview != NULL) { - delete MultiplayerMapPreview; - MultiplayerMapPreview = 0; - } - return(0); - } - - case WM_PAINT: { - OwnerDraw::Draw_Dialog_Back(window); - - if (MultiplayerMapPreview) { - MultiplayerMapPreview->Blit_Preview(window); - } - - ValidateRect(window, NULL); - return(0); - } - - case WM_ERASEBKGND: - return(1); - } - - return(0); -} diff --git a/code/netdlg2.h b/code/netdlg2.h index 5b5d4baf5..490b752e3 100644 --- a/code/netdlg2.h +++ b/code/netdlg2.h @@ -15,13 +15,31 @@ #include "win.h" +#include "netdlg.h" + struct GlobalPacketType; class IPXAddressClass; +/* +** The lobby's own state, shared by the game list, host and guest screens. +*/ +extern int CurGame; +extern JoinStateType JoinState; +extern bool Net2IsGameListActive; + +void Send_Join_Queries(int gamenow, int playernow, int chatnow, int init = 0); +void Net2ServiceGameList(void); + +// One pass of the lobby's own maintenance: service the transport, answer the join protocol, +// broadcast the host's options and age out what has gone quiet. Both of the lobby's drivers +// run this once per pass, through the presenter's Service. +void Net2ServiceLobby(void); + +// Which of the lobby's three screens is up, as its dialog identifier, or 0 when none is. +// A document has no window, so this answers for both presentations. +int Net2LobbyScreenID(void); + int Net2FirstFreeColor(int reqcolor, int index); -void Fill_Country_Box(HWND combo); -int Country_From_Box(HWND combo); -void Select_Country_In_Box(HWND combo, int country); bool Net2Callback(void); void Net2DisplayUsers(void); bool Net2Init_Network(void); diff --git a/code/netglobal.cpp b/code/netglobal.cpp index 1f9e4d394..cb309336c 100644 --- a/code/netglobal.cpp +++ b/code/netglobal.cpp @@ -193,6 +193,54 @@ namespace NetGlobal } + /// + /// Validates a lobby global packet before dispatch. + /// The lobby's handlers copy, compare and print the packet's fixed wire strings, and every + /// one of those reads runs off the end of its field when a peer omits the terminator. The + /// command set is deliberately not policed here: a lobby command this routine did not + /// expect already falls through the dispatcher's final empty arm. + /// + DecodeError Validate_Lobby_Packet(GlobalPacketType const & packet, std::size_t packet_length) + { + if (packet_length != PACKET_SIZE) { + return(DecodeError::INVALID_LENGTH); + } + + // Every lobby sender fills Name from a terminated handle, and most of the dispatcher's + // arms compare or copy it, so it is required of all of them. + if (!Has_Terminator(packet.Name, sizeof(packet.Name))) { + return(DecodeError::UNTERMINATED_NAME); + } + + switch (packet.Command) { + case NET_ANSWER_PLAYER: + case NET_QUERY_JOIN: + if (!Has_Terminator(packet.Serial, sizeof(packet.Serial))) { + return(DecodeError::UNTERMINATED_SERIAL); + } + break; + + case NET_MESSAGE: + if (!Has_Terminator(packet.Message.Buf, sizeof(packet.Message.Buf))) { + return(DecodeError::UNTERMINATED_MESSAGE); + } + break; + + case NET_PUB_GAMEOPT: + case NET_PRIV_GAMEOPT: + if (!Has_Terminator(packet.Options.Buf, sizeof(packet.Options.Buf))) { + return(DecodeError::UNTERMINATED_OPTIONS); + } + break; + + default: + break; + } + + return(DecodeError::NONE); + } + + /// Counts a rejection and selects sparse diagnostics. RejectionRecord RejectionCounters::Record(DecodeError error) noexcept { @@ -228,7 +276,10 @@ namespace NetGlobal case DecodeError::SENDER_NOT_MEMBER: return("sender is not a session member"); case DecodeError::UNTERMINATED_NAME: return("unterminated player name"); case DecodeError::UNTERMINATED_MESSAGE: return("unterminated message"); + case DecodeError::UNTERMINATED_SERIAL: return("unterminated serial number"); + case DecodeError::UNTERMINATED_OPTIONS: return("unterminated game options"); case DecodeError::INVALID_COLOR: return("invalid session-member color"); + case DecodeError::INVALID_HOUSE: return("invalid session-member house"); case DecodeError::INVALID_PROGRESS: return("invalid progress value"); case DecodeError::INVALID_KICK_PLAYER: return("invalid kick player"); case DecodeError::SELF_KICK: return("self kick proposal"); diff --git a/code/netglobal.h b/code/netglobal.h index c9ccd29c1..1dc5766fc 100644 --- a/code/netglobal.h +++ b/code/netglobal.h @@ -27,7 +27,10 @@ namespace NetGlobal SENDER_NOT_MEMBER, UNTERMINATED_NAME, UNTERMINATED_MESSAGE, + UNTERMINATED_SERIAL, + UNTERMINATED_OPTIONS, INVALID_COLOR, + INVALID_HOUSE, INVALID_PROGRESS, INVALID_KICK_PLAYER, SELF_KICK, @@ -94,5 +97,6 @@ namespace NetGlobal void Initialize_Packet(GlobalPacketType & packet, NetCommandType command) noexcept; EndpointResolution Resolve_Sender(Endpoint const & sender, std::span roster) noexcept; DecodeError Validate_In_Game_Packet(GlobalPacketType const & packet, std::size_t packet_length, ValidationContext const & context); + DecodeError Validate_Lobby_Packet(GlobalPacketType const & packet, std::size_t packet_length); char const * Error_Name(DecodeError error) noexcept; } diff --git a/code/netpacket.cpp b/code/netpacket.cpp index 136309a3d..e521b1c76 100644 --- a/code/netpacket.cpp +++ b/code/netpacket.cpp @@ -41,6 +41,11 @@ namespace NetPacket constexpr std::size_t MEGAMISSION_WHOM_OFFSET = offsetof(MegaMissionType, Whom); constexpr std::size_t MEGAMISSION_WHOM_SIZE = sizeof(std::declval().Whom); + // A packet places the ADDPLAYER payload size where the sender's own union put it, so the + // offset belongs to the wire format rather than to the build. The Variable arm reserves a + // four-byte payload slot ahead of it at every pointer width. + static_assert(VARIABLE_SIZE_OFFSET == 4, "ADDPLAYER wire offset changed"); + static_assert(std::is_standard_layout_v); static_assert(std::is_trivially_copyable_v); static_assert(EventClass::LAST_EVENT <= (std::numeric_limits::max)()); diff --git a/code/netshare.cpp b/code/netshare.cpp index 46d0cafaa..e4700b186 100644 --- a/code/netshare.cpp +++ b/code/netshare.cpp @@ -10,6 +10,10 @@ #include "always.h" #include "netshare.h" +#include "ui/uilobby.h" +#include "ui/uimessagebox.h" +#include "ui/uiscenariopick.h" +#include "ui/uishell.h" #include "_rules.h" #include "conquer.h" @@ -26,14 +30,12 @@ #include "netdlg.h" #include "netdlg2.h" #include "newmenu.h" -#include "ownrdraw.h" #include "rules.h" #include "scenario.h" #include "sendfile.h" #include "session.h" #include "stimer.h" #include "wdtnet.h" -#include "windlg.h" #include "utf8.h" #include "worlddom.h" #include "wstring.h" @@ -100,30 +102,6 @@ unsigned int Wstring_Hash(Wstring & string) } -/// -/// Fetches the game options dialog that is currently up. -/// The same options are presented by four different dialogs depending on how the game was -/// started. Use this routine rather than trying to remember which one the player is looking -/// at. -/// -/// Returns with the handle of the open game options dialog. NULL is returned if -/// none of them is up. -HWND GameoptWindow(void) -{ - HWND dialog; - - dialog = WS_Find_Dialog(IDD_MPLAYER_HOST); - if (dialog) { - return(dialog); - } - dialog = WS_Find_Dialog(IDD_MPLAYER_GUEST); - if (dialog) { - return(dialog); - } - return(0); -} - - /// /// Prints a formatted chat message to the player. /// This routine hunts down the topmost dialog that has somewhere to show public and private @@ -142,177 +120,12 @@ void __cdecl PMessagePrintf(int color, const char * fmt, ...) vsprintf(buffer, fmt, va); va_end(va); - if (WS_Top_Window() != 0) { - HWND top = WS_Top_Window(); - HWND msg = GetDlgItem(top, IDC_PMESSAGES); - while (msg == 0) { - top = WS_Next_Lower_Dialog(top); - if (top == 0) { - msg = 0; - break; - } - msg = GetDlgItem(top, IDC_PMESSAGES); - } - - if (msg != 0) { - _DrawMessage(color, buffer, msg); - } - } -} - - -/// -/// Prints a formatted system message to the player. -/// This routine hunts down the topmost dialog that has somewhere to show system messages and -/// puts the text there, so the caller does not have to know which dialog the player is -/// looking at. If no dialog wants system messages, the message is quietly dropped. -/// -/// Color to display the message in, or -1 for the default. -/// Printf style format string for the message. -void __cdecl SMessagePrintf(int color, const char * fmt, ...) -{ - va_list va; - static char buffer[1024]; - memset(buffer, 0, sizeof(buffer)); - - va_start(va, fmt); - vsprintf(buffer, fmt, va); - va_end(va); - - if (WS_Top_Window() != 0) { - HWND top = WS_Top_Window(); - HWND msg = GetDlgItem(top, IDC_SMESSAGES); - while (msg == 0) { - top = WS_Next_Lower_Dialog(top); - if (top == 0) { - msg = 0; - break; - } - msg = GetDlgItem(top, IDC_SMESSAGES); - } - - if (msg != 0) { - _DrawMessage(color, buffer, msg); - } - } -} - - -/// -/// Draws a message into a message list box. -/// This routine word wraps the message to the width of the list box and adds each resulting -/// line as its own entry, so that a long chat message stays readable. Embedded newlines -/// break the text as well. -/// -/// Color to display the message in, or -1 for the list box -/// default. -/// The text to display. -/// The message list box to display the text in. -void _DrawMessage(int color, const char * message, HWND window) -{ - RECT rect; - - int length = strlen(message); - - int offset = 18; - Get_Display_Rect(window, &rect); - if (SendMessage(window, OD_HASATTACHED, 0, 0)) { - offset = 1; - } - - HDC hdc = GetDC(window); - SendMessage(window, OD_RESTOREDC, 0, (LPARAM)hdc); - - while (length) { - if (message != NULL) { - char const * newline = strchr(message, '\n'); - if (newline != NULL) { - int linelen = newline - message + 1; - if (length >= linelen) { - length = linelen; - } - } - } - - SIZE size; - GetTextExtentPoint32(hdc, message, length, &size); - int maxWidth = rect.right - rect.left - offset; - - if (size.cx >= maxWidth - 4) { - int reduceBy; - if (size.cx / 2 > maxWidth) { - reduceBy = 10; - length -= reduceBy; - } else { - reduceBy = 1; - } - - int found = -1; - int idx = length - 1; - - while (idx > 0) { - if (!isgraph((unsigned char)message[idx])) { - found = idx; - break; - } - idx--; - } - - if (found == -1) { - length -= reduceBy; - found = length; - } - length = found; - } else { - _SetMessageString(window, message, length, color); - message += length; - length = strlen(message); - } + // The line is recorded where it is composed rather than where it is drawn, so a + // presentation that draws a different number of times cannot lose one or repeat one. + if (UILobbyPresenterClass * const lobby = UI_Lobby_Screen()) { + lobby->Record_Message(color, buffer); } - ReleaseDC(window, hdc); -} - - -/// -/// Adds a single line of text to a message list box. -/// This is the low level routine that _DrawMessage uses once it has decided where the text -/// should break. The list box is capped, so a long game does not pile up messages without -/// limit. -/// -/// The message list box to add the line to. -/// The text to add; only the leading characters are taken. -/// Number of characters of the message to add. -/// Color to display the line in, or -1 for the list box default. -void _SetMessageString(HWND window, const char * message, int length, int color) -{ - static char buffer[1024]; - memset(buffer, 0, sizeof(buffer)); - strncpy(buffer, message, length); - char * line_end = strchr(buffer, '\r'); - if (line_end != NULL) { - line_end[0] = '\0'; - } else { - line_end = strchr(buffer, '\n'); - if (line_end != NULL) { - line_end[0] = '\0'; - } - } - - int old = SendMessage(window, OD_DISABLEPAINT, 0, 1); - int topindex = ListBox_GetCount(window); - if (topindex > 500) { - ListBox_DeleteString(window, 0); - topindex--; - } - - int index = ListBox_InsertString(window, -1, buffer); - if (color != -1) { - SendMessage(window, OD_SETCOLOR, index, color); - } - - ListBox_SetTopIndex(window, topindex); - SendMessage(window, OD_DISABLEPAINT, 0, old); } @@ -363,162 +176,32 @@ int CountAliveTeams(HouseClass * house) /// message. /// The button layout to use; MB_OK, MB_OKCANCEL or MB_YESNO. /// Idle routine to poll while the box is up. -/// Should the large version of the box be used? /// Returns with the control ID of the button the player pressed. Zero is returned /// if there was nothing to display. -int ODMessageBox(const char * text, int type, bool (*callback)(void), bool large) +int ODMessageBox(const char * text, int type, bool (*callback)(void)) { if (text != NULL && strlen(text) > 0) { - HWND dialog; + + // The box carries the captions the three templates held and the caller's poll goes to + // the screen's service, which is what the dialog's wait loop did with it. + char const * const ok = Fetch_String(TXT_OK); + char const * first = ok; + char const * second = NULL; if (type == MB_OKCANCEL) { - dialog = WS_Create_Dialog(ProgramInstance, IDD_MSGBOX_2, MainWindow, ODMessageBox_Proc, false); - } else { - if (large) { - dialog = WS_Create_Dialog(ProgramInstance, IDD_MSGBOX_3_LARGE, MainWindow, ODMessageBox_Proc, false); - } else { - dialog = WS_Create_Dialog(ProgramInstance, IDD_MSGBOX_3_SMALL, MainWindow, ODMessageBox_Proc, false); - } - if (type == MB_OK) { - HWND ok = GetDlgItem(dialog, IDOK); - SetWindowLong(ok, GWL_STYLE, GetWindowLong(ok, GWL_STYLE) | WS_VISIBLE); - } - if (type == MB_YESNO) { - HWND yes = GetDlgItem(dialog, IDYES); - SetWindowLong(yes, GWL_STYLE, GetWindowLong(yes, GWL_STYLE) | WS_VISIBLE); - HWND no = GetDlgItem(dialog, IDNO); - SetWindowLong(no, GWL_STYLE, GetWindowLong(no, GWL_STYLE) | WS_VISIBLE); - } + second = Fetch_String(TXT_CANCEL); + } else if (type == MB_YESNO) { + first = Fetch_String(TXT_YES); + second = Fetch_String(TXT_NO); } - Center_Window_Within_Window(dialog); - SendDlgItemMessage(dialog, IDC_MSGBOX_TEXT, WM_SETTEXT, 0, (LPARAM)text); - OwnerDraw::Subclass_Dialog(dialog, 0); - ShowWindow(dialog, SW_NORMAL); - return(WS_Wait_Dialog(dialog, callback)); - } - return(0); -} + UIResult const result = UI_Message_Box_Screen(text, 0, first, second, NULL, callback); -/// -/// Handles the messages for the owner drawn message box. -/// This routine paints the box through the owner draw system and tears it down with -/// whichever of the buttons the player pressed. -/// -/// Returns with TRUE if the message was dealt with here, FALSE otherwise. -INT_PTR CALLBACK ODMessageBox_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - switch (message) { - case WM_DRAWITEM: - OwnerDraw::Draw_Item((LPDRAWITEMSTRUCT)lparam); - return(TRUE); - - case WM_PAINT: - OwnerDraw::Draw_Dialog_Back(window); - ValidateRect(window, NULL); - return(TRUE); - - case WM_ERASEBKGND: - return(TRUE); - - case WM_COMMAND: - switch (LOWORD(wparam)) { - case IDOK: - case IDCANCEL: - case IDYES: - case IDNO: - WS_Destroy_Dialog(window, LOWORD(wparam)); - return(TRUE); - } - break; - } - return(FALSE); -} - - -/// -/// Displays the current game options in the setup dialog. -/// This routine pushes the session options out to the sliders and check boxes. On the -/// initializing pass it also establishes the slider ranges and greys out whichever options a -/// World Domination Tour territory refuses to let the players meddle with. -/// -/// The game options dialog to update. -/// Is this the first call for a freshly created dialog? -void DisplayGameopts(HWND window, BOOL initialize) -{ - #define MP_MIN_MONEY 2500 - - if (initialize) { - if (Session.Type == GAME_INTERNET && Session.IsWDT) { - WDTTerritory * territory = WDT_Get_Territory(Session.WDTTerritory); - if (territory != NULL) { - EnableWindow(GetDlgItem(window, IDC_YOURSIDE), FALSE); - EnableWindow(GetDlgItem(window, IDC_AIPLAYERS), FALSE); - EnableWindow(GetDlgItem(window, IDC_AILEVEL_SLIDER), FALSE); /// AI Difficulty - - if (!territory->UserModUnitCount) { - EnableWindow(GetDlgItem(window, IDC_UNITCOUNT), FALSE); - } - if (!territory->UserModTechLevel) { - EnableWindow(GetDlgItem(window, IDC_TECHLEVEL), FALSE); - } - if (!territory->UserModCredits) { - EnableWindow(GetDlgItem(window, IDC_CREDITS), FALSE); - } - if (!territory->UserModAlliances) { - EnableWindow(GetDlgItem(window, IDC_ALLIES), FALSE); - } - if (!territory->UserModHarvesterTruce) { - EnableWindow(GetDlgItem(window, IDC_HARVTRUCE), FALSE); - } - if (!territory->UserModBases) { - EnableWindow(GetDlgItem(window, IDC_BASES), FALSE); - } - if (!territory->UserModMCVRedeploy) { - EnableWindow(GetDlgItem(window, IDC_REDEPLOY_MCV), FALSE); /// Re-Deployable MCV - } - if (!territory->UserModFogOfWar) { - EnableWindow(GetDlgItem(window, IDC_FOG_OF_WAR), FALSE); /// Fog of War - } - if (!territory->UserModBridgeDestruction) { - EnableWindow(GetDlgItem(window, IDC_BRIDGE_DESTROY), FALSE); - } - if (!territory->UserModCrates) { - EnableWindow(GetDlgItem(window, IDC_CRATES), FALSE); - } - if (!territory->UserModShortGame) { - EnableWindow(GetDlgItem(window, IDC_SHORT_GAME), FALSE); /// Short Game - } - if (!territory->UserModCrapEngineer) { - EnableWindow(GetDlgItem(window, IDC_MULTI_ENGINEER), FALSE); /// Crap Engineers - } - } + if (type == MB_YESNO) { + return(result.Value == 0 ? IDYES : IDNO); } - SendDlgItemMessage(window, IDC_UNITCOUNT, TBM_SETRANGE, TRUE, MAKELONG(1, 10)); - SendDlgItemMessage(window, IDC_TECHLEVEL, TBM_SETRANGE, TRUE, MAKELONG(1, MPLAYER_BUILD_LEVEL_MAX)); - SendDlgItemMessage(window, IDC_CREDITS, TBM_SETRANGE, TRUE, MAKELONG(MP_MIN_MONEY, Rule->MPMaxMoney)); - SendDlgItemMessage(window, IDC_CREDITS, OD_SETTRACKSTEP, 0, 100); - SendDlgItemMessage(window, IDC_AIPLAYERS, TBM_SETRANGE, TRUE, MAKELONG(0, 6)); - SendDlgItemMessage(window, IDC_AILEVEL_SLIDER, TBM_SETRANGE, TRUE, MAKELONG(0, 2)); /// AI Difficulty - SendDlgItemMessage(window, IDC_GAME_SPEED_SLIDER, TBM_SETRANGE, TRUE, MAKELONG(0, 6)); /// Game Speed + return(result.Value == 0 ? IDOK : IDCANCEL); } - - SendDlgItemMessage(window, IDC_UNITCOUNT, TBM_SETPOS, TRUE, Session.Options.UnitCount); - SendDlgItemMessage(window, IDC_TECHLEVEL, TBM_SETPOS, TRUE, BuildLevel); - SendDlgItemMessage(window, IDC_CREDITS, TBM_SETPOS, TRUE, Session.Options.Credits); - SendDlgItemMessage(window, IDC_AIPLAYERS, TBM_SETPOS, TRUE, Session.Options.AIPlayers); - SendDlgItemMessage(window, IDC_AILEVEL_SLIDER, TBM_SETPOS, TRUE, Session.Options.AIDifficulty); - SendDlgItemMessage(window, IDC_GAME_SPEED_SLIDER, TBM_SETPOS, TRUE, 6 - Session.Options.GameSpeed); - - int button_state[] = { BST_UNCHECKED, BST_CHECKED }; - SendDlgItemMessage(window, IDC_BRIDGE_DESTROY, BM_SETCHECK, button_state[Session.Options.BridgeDestruction], 0); - SendDlgItemMessage(window, IDC_FOG_OF_WAR, BM_SETCHECK, button_state[Session.Options.FogOfWar], 0); - SendDlgItemMessage(window, IDC_CRATES, BM_SETCHECK, button_state[Session.Options.Goodies], 0); - SendDlgItemMessage(window, IDC_ALLIES, BM_SETCHECK, button_state[Session.Options.AlliesAllowed], 0); - SendDlgItemMessage(window, IDC_HARVTRUCE, BM_SETCHECK, button_state[Session.Options.HarvTruce], 0); - SendDlgItemMessage(window, IDC_BASES, BM_SETCHECK, button_state[Session.Options.Bases], 0); - SendDlgItemMessage(window, IDC_REDEPLOY_MCV, BM_SETCHECK, button_state[Session.Options.MCVRedeploy], 0); - SendDlgItemMessage(window, IDC_SHORT_GAME, BM_SETCHECK, button_state[Session.Options.ShortGame], 0); - SendDlgItemMessage(window, IDC_MULTI_ENGINEER, BM_SETCHECK, button_state[Session.Options.CrapEngineers], 0); + return(0); } @@ -740,7 +423,6 @@ bool DecodePubGameopt(char * options, char * name) return(false); } - SendDlgItemMessage(GameoptWindow(), IDC_USERS, OD_DISABLEPAINT, 0, 1); DebugString("Decoding game options %s\n", options); token = strtok(token, ","); @@ -853,7 +535,7 @@ bool DecodePubGameopt(char * options, char * name) if (stricmp(Session.ScenarioFileName, token) != 0) { same_scenario = false; } - strncpy(Session.ScenarioFileName, token, sizeof(Session.ScenarioFileName)); + UTF8::Copy(Session.ScenarioFileName, sizeof(Session.ScenarioFileName), token); strcpy(Scen->ScenarioName, Session.ScenarioFileName); } @@ -862,7 +544,7 @@ bool DecodePubGameopt(char * options, char * name) if (strcmp(Session.ScenarioDigest, digest) != 0) { same_scenario = false; } - strncpy(Session.ScenarioDigest, digest, sizeof(Session.ScenarioDigest)-1); + UTF8::Copy(Session.ScenarioDigest, sizeof(Session.ScenarioDigest), digest); } if (!same_scenario || strlen(Session.Options.ScenarioDescription) == 0) { @@ -877,15 +559,13 @@ bool DecodePubGameopt(char * options, char * name) } } if (!found && scenario_description != NULL) { - strcpy(Session.Options.ScenarioDescription, scenario_description); + UTF8::Copy(Session.Options.ScenarioDescription, sizeof(Session.Options.ScenarioDescription), scenario_description); } if (stricmp(Session.ScenarioFileName, RANDOM_MAP_FILE_NAME) == 0) { strcpy(Session.Options.ScenarioDescription, Fetch_String(TXT_RANDOM_MAP_DESCRIPTION)); } } - SendDlgItemMessage(GameoptWindow(), IDC_SCENARIONAME, WM_SETTEXT, 0, (LPARAM)Session.Options.ScenarioDescription); - Scen->Scenario = -1; Frame = 0; Session.CommProtocol = DEFAULT_COMM_PROTOCOL; @@ -896,7 +576,7 @@ bool DecodePubGameopt(char * options, char * name) if (!same_scenario) { DebugString("Not same scenario..."); - Update_Network_Dialog_Preview(GameoptWindow()); + Rebuild_Network_Map_Preview(); } if (digest == NULL) { @@ -932,7 +612,11 @@ bool DecodePubGameopt(char * options, char * name) free(string); - DisplayGameopts(GameoptWindow(), false); + // The settings the host sent are on the model where they arrived, not where a control is + // written, so a presentation that is not a window sees them too. + if (UILobbyPresenterClass * const lobby = UI_Lobby_Screen()) { + lobby->Options_Received(); + } if (_last_unit_count != Session.Options.UnitCount) do_decode = true; if (_last_tech_level != BuildLevel) do_decode = true; @@ -962,19 +646,13 @@ bool DecodePubGameopt(char * options, char * name) char buffer[64]; sprintf(buffer, "A0"); SendPublicGameopts(buffer); + } - EnableWindow(GetDlgItem(GameoptWindow(), IDC_ACCEPT), TRUE); - InvalidateRect(GetDlgItem(GameoptWindow(), IDC_ACCEPT), NULL, FALSE); - } else { - if (!IsWindowEnabled(GetDlgItem(GameoptWindow(), IDC_ACCEPT))) { - EnableWindow(GetDlgItem(GameoptWindow(), IDC_ACCEPT), TRUE); - InvalidateRect(GetDlgItem(GameoptWindow(), IDC_ACCEPT), NULL, FALSE); - } + if (UILobbyPresenterClass * const lobby = UI_Lobby_Screen()) { + lobby->CanAccept = true; } } - SendDlgItemMessage(GameoptWindow(), IDC_USERS, OD_DISABLEPAINT, 0, 0); - Net2DisplayUsers(); _last_unit_count = Session.Options.UnitCount; @@ -1001,83 +679,6 @@ bool DecodePubGameopt(char * options, char * name) } -/// -/// Saves the current selection of a list box. -/// The multiplayer dialogs rebuild their list boxes from scratch whenever the game state -/// changes. Call this routine first so that whatever the player had highlighted can be put -/// back afterwards. -/// -/// The list box to record the selection of. -/// The dictionary to record the selected entries into. -void LBSaveSelections(HWND listbox, Dictionary & lbdict) -{ - int count = ListBox_GetCount(listbox); - char buffer[128]; - Wstring key; - - if (count) { - int style = GetWindowLong(listbox, GWL_STYLE); - if (style & LBS_MULTIPLESEL) { - for (int i = 0; i < count; i++) { - if (ListBox_GetSel(listbox, i) != 0) { - ListBox_GetText(listbox, i, buffer); - key = buffer; - bool value = true; - lbdict.add(key, value); - } - } - } else if (!(style & LBS_NOSEL)) { - int index = ListBox_GetCurSel(listbox); - if (index >= 0) { - buffer[0] = '\0'; - ListBox_GetText(listbox, index, buffer); - key = buffer; - bool value = true; - lbdict.add(key, value); - } - } - } -} - - -/// -/// Restores a list box selection that was saved earlier. -/// This routine is the other half of LBSaveSelections. Call it once the list box has been -/// refilled to put the player's highlight back where it was. -/// -/// The list box to restore the selection within. -/// The dictionary the selection was saved into. -void LBRestoreSelections(HWND listbox, Dictionary & lbdict) -{ - int count = ListBox_GetCount(listbox); - Wstring key; - - if (count) { - int style = GetWindowLong(listbox, GWL_STYLE); - if (style & LBS_MULTIPLESEL) { - for (int i = 0; i < count; i++) { - char buffer[128]; - ListBox_GetText(listbox, i, buffer); - key = buffer; - if (lbdict.contains(key)) { - ListBox_SetSel(listbox, TRUE, i); - } - } - } else if (!(style & LBS_NOSEL)) { - bool value; - if (lbdict.removeAny(key, value)) { - char buffer[128]; - strcpy(buffer, key.get()); - int index = ListBox_FindStringExact(listbox, -1, buffer); - if (index != LB_ERR) { - ListBox_SetCurSel(listbox, index); - } - } - } - } -} - - /// /// Fetches the number of starting positions a scenario offers. /// This routine is used to check that there is somewhere to put every player before a map is @@ -1117,148 +718,20 @@ int RandomMapWaypointCount(int index) static int LastPreviewedScenario; -static int OriginalScenario; -static HWND ScenarioPick; - - -/// -/// Handles the idle processing while the map selection dialog is up. -/// This routine keeps the preview in step with whichever map is highlighted and pumps the -/// network layer the session is using, so that a game sitting in the lobby does not stall -/// while the host browses for a scenario. -/// -/// bool; Should the dialog be shut down? -bool Scenario_Select_Callback(void) -{ - int index = SendDlgItemMessage(ScenarioPick, IDC_AILEVEL_SLIDER, LB_GETCURSEL, 0, 0); - if (index != LastPreviewedScenario && index != -1) { - Set_Scenario_Info_From_Index(index); - if (stricmp(Session.Scenarios[index]->Get_Filename(), "RandMap.Sed") == 0) { - delete MultiplayerMapPreview; - MultiplayerMapPreview = new MapPreviewClass; - MultiplayerMapPreview->Read_PCX_Preview("RandMap.img"); - if (MultiplayerMapPreview->Get_Preview_Surface() == NULL) { - Update_Network_Dialog_Preview(ScenarioPick); - } - InvalidateRect(ScenarioPick, NULL, FALSE); - } else { - Update_Network_Dialog_Preview(ScenarioPick); - } - LastPreviewedScenario = index; - Session.Options.ScenarioIndex = OriginalScenario; - Set_Scenario_Info_From_Index(OriginalScenario); - } - if (Session.Type == GAME_IPX || Session.Type == GAME_INTERNET) { - return(Net2Callback()); - } - Call_Back(); - return(false); -} - -INT_PTR CALLBACK Scenario_DlgProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); /// -/// Brings up the multiplayer map selection dialog. -/// Use this routine to let the host choose the scenario for the game. The dialog does not -/// return until the player settles on a map or backs out. +/// Runs the map selection screen. +/// This is the entry a screen uses rather than a dialog, because a presenter names no +/// window. /// -/// The window to parent and center the dialog against. -/// Returns with the control ID that dismissed the dialog, either IDOK or -/// IDCANCEL. -int Scenario_Dialog(HWND top) +/// bool; Did the player settle on a map? +bool Pick_Scenario_Screen(void) { - Hide_Mouse(); - Draw_Menu_Background(); - Show_Mouse(); - ScenarioPick = WS_Create_Dialog(ProgramInstance, IDD_MPLAYER_SELECT_MAP, top, Scenario_DlgProc, FALSE); - Center_Window_Within_Window(ScenarioPick); - OwnerDraw::Subclass_Dialog(ScenarioPick, 0); - ShowWindow(ScenarioPick, SW_NORMAL); - return(WS_Wait_Dialog(ScenarioPick, Scenario_Select_Callback)); -} - - -/// -/// Handles the messages for the multiplayer map selection dialog. -/// This routine fills the map list, paints the preview of the highlighted map, and services -/// the random map generator button. -/// -/// Returns with TRUE if the message was dealt with here, FALSE to leave it to the -/// dialog manager. -INT_PTR CALLBACK Scenario_DlgProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - switch (message) { - case WM_NCDESTROY: - On_WM_NCDESTROY(window); - break; - - case WM_PAINT: - OwnerDraw::Draw_Dialog_Back(window); - if (MultiplayerMapPreview) { - MultiplayerMapPreview->Blit_Preview(window); - } - ValidateRect(window, NULL); - break; - - case WM_ERASEBKGND: - return(TRUE); - - case WM_DRAWITEM: - OwnerDraw::Draw_Item((DRAWITEMSTRUCT *)lparam); - return(TRUE); - - case WM_COMMAND: - switch (LOWORD(wparam)) { - case IDC_AILEVEL_SLIDER: - return(FALSE); - - case IDOK: { - int index = SendDlgItemMessage(window, IDC_AILEVEL_SLIDER, LB_GETCURSEL, 0, 0); - Session.Options.ScenarioIndex = std::max(0, index); - WS_Destroy_Dialog(window, IDOK); - SendDlgItemMessage(GameoptWindow(), IDC_SCENARIONAME, WM_SETTEXT, 0, (LPARAM)Session.Scenarios[Session.Options.ScenarioIndex]); - break; - } + UIScenarioPickPresenterClass screen; + screen.Refresh(); - case IDCANCEL: - WS_Destroy_Dialog(window, IDCANCEL); - break; - - case IDC_CREATE_RANDOM_MAP: { - ShowWindow(window, SW_HIDE); - int scenario = CreateRandomMap(); - if (scenario != -1) { - SendDlgItemMessage(window, IDC_AILEVEL_SLIDER, LB_RESETCONTENT, 0, 0); - for (int i = 0; i < Session.Scenarios.Count(); i++) { - SendDlgItemMessage(window, IDC_AILEVEL_SLIDER, LB_INSERTSTRING, -1, (LPARAM)Session.Scenarios[i]); - } - SendDlgItemMessage(window, IDC_AILEVEL_SLIDER, LB_SETCURSEL, scenario, 0); - Set_Scenario_Info_From_Index(scenario); - if (!MultiplayerMapPreview->Get_Preview_Surface()) { - Update_Network_Dialog_Preview(window); - } - Session.Options.ScenarioIndex = OriginalScenario; - Set_Scenario_Info_From_Index(OriginalScenario); - } - ShowWindow(window, SW_SHOW); - break; - } - } - break; - - case OD_SUBCLASSED: { - SendDlgItemMessage(window, IDC_AILEVEL_SLIDER, LB_RESETCONTENT, 0, 0); - for (int i = 0; i < Session.Scenarios.Count(); i++) { - SendDlgItemMessage(window, IDC_AILEVEL_SLIDER, LB_INSERTSTRING, -1, (LPARAM)Session.Scenarios[i]); - } - SendDlgItemMessage(window, IDC_AILEVEL_SLIDER, LB_SETCURSEL, Session.Options.ScenarioIndex, 0); - OriginalScenario = Session.Options.ScenarioIndex; - LastPreviewedScenario = -1; - break; - } - } - return(FALSE); + return(UI_Scenario_Pick_Screen(screen).Outcome == UIResult::OUTCOME_ACCEPTED); } @@ -1295,20 +768,18 @@ void PregameSetup(void) /// -/// Updates the map preview shown in a network game dialog. -/// This routine is called whenever the selected scenario changes. A guest that does not have -/// the scenario locally asks the host for a preview instead of building one, so the picture -/// may not appear until that download arrives. +/// Rebuilds the map preview for the scenario the session currently names. +/// This is the half of the update that owns the preview itself, split from the half that +/// tells a window to repaint, so a presentation that is not a window can ask for it. /// -/// The dialog window that displays the preview. -void Update_Network_Dialog_Preview(HWND win) +void Rebuild_Network_Map_Preview(void) { delete MultiplayerMapPreview; MultiplayerMapPreview = NULL; switch (Session.Type) { case GAME_IPX: - if (WS_Top_Window_ID() == IDD_MPLAYER_GUEST && !Find_Local_Scenario(Session.ScenarioFileName, Session.ScenarioFileLength, Session.ScenarioDigest, Session.ScenarioIsOfficial)) { + if (Net2LobbyScreenID() == IDD_MPLAYER_GUEST && !Find_Local_Scenario(Session.ScenarioFileName, Session.ScenarioFileLength, Session.ScenarioDigest, Session.ScenarioIsOfficial)) { GlobalPacketType packet; memset(&packet, 0, sizeof(packet)); packet.Command = NET_REQ_PREVIEW; @@ -1342,7 +813,6 @@ void Update_Network_Dialog_Preview(HWND win) MultiplayerMapPreview = new MapPreviewClass; if (MultiplayerMapPreview != NULL) { MultiplayerMapPreview->Read_INI_Preview(Session.ScenarioFileName); - InvalidateRect(win, NULL, FALSE); } } @@ -1353,6 +823,11 @@ void Update_Network_Dialog_Preview(HWND win) /// ready, the compressed preview file is downloaded, and the decompressed image becomes the /// preview shown in the multiplayer dialog. /// +// The largest paletted preview block a host may declare, which is far above the picture the +// game itself makes and well below a length that could not be allocated. +static int const MAX_PREVIEW_BLOCK_SIZE = 4 * 1024 * 1024; + + void Receive_Random_Map_Preview(void) { Ipx.Set_Timing(50, -1, 5000); @@ -1386,25 +861,47 @@ void Receive_Random_Map_Preview(void) DebugString("Loading the compressed preview image\n"); CDFileClass file(preview_name); int size = file.Size(); + + // The file came from the host, so its declared decompressed length is checked before it + // is used to size anything. + if (size <= (int)sizeof(int)) { + DebugString("Preview file is too short to carry a length\n"); + Ipx.Set_Timing(TIMER_SECOND / 2, -1, 10 * TIMER_SECOND); + return; + } + char * buffer = new char[size]; file.Read(buffer, size); int preview_size = ((int *)buffer)[0]; + if (preview_size <= 0 || preview_size > MAX_PREVIEW_BLOCK_SIZE) { + DebugString("Preview file declares an unusable length of %d bytes\n", preview_size); + delete [] buffer; + Ipx.Set_Timing(TIMER_SECOND / 2, -1, 10 * TIMER_SECOND); + return; + } + DebugString("Decompressing the preview image\n"); - BufferStraw bstraw(&((int *)buffer)[1], size); + BufferStraw bstraw(&((int *)buffer)[1], size - (int)sizeof(int)); LZOStraw lzostraw(LZOStraw::DECOMPRESS); lzostraw.Get_From(&bstraw); - char * preview = new char[2 * preview_size]; - lzostraw.Get(preview, preview_size); + char * preview = new char[preview_size]; + int const decompressed = lzostraw.Get(preview, preview_size); DebugString("Creating the new preview surface\n"); if (MultiplayerMapPreview) { delete MultiplayerMapPreview; } MultiplayerMapPreview = new MapPreviewClass; - MultiplayerMapPreview->Create_Preview_Surface(preview); - InvalidateRect(WS_Top_Window(), NULL, FALSE); + if (!MultiplayerMapPreview->Create_Preview_Surface(preview, decompressed)) { + DebugString("Preview block does not describe a usable picture\n"); + } + // The picture arrived from the host rather than being rebuilt here, so the screen showing + // it is told directly; the dialog got the same news from an InvalidateRect. + if (UILobbyPresenterClass * const lobby = UI_Lobby_Screen()) { + lobby->PreviewGeneration++; + } DebugString("Cleaning up the temporary decompression buffers\n"); delete [] preview; diff --git a/code/netshare.h b/code/netshare.h index c71393c09..bde469c63 100644 --- a/code/netshare.h +++ b/code/netshare.h @@ -16,39 +16,34 @@ class HouseClass; -int ODMessageBox(const char *text, int type, bool (*callback)(void), bool large = false); -INT_PTR CALLBACK ODMessageBox_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); +int ODMessageBox(const char *text, int type, bool (*callback)(void)); bool Set_Scenario_Info_From_Index(int index); void Commit_Session_Specials(void); void PregameSetup(void); -void Update_Network_Dialog_Preview(HWND win); +void Rebuild_Network_Map_Preview(void); + +// Runs the map selection screen and reports whether the player settled on a map. This is +// the entry a screen uses, because a presenter names no window. +bool Pick_Scenario_Screen(void); void Receive_Random_Map_Preview(void); void Send_Preview_To_Guests(void); int CountAliveTeams(HouseClass * house); int RandomMapWaypointCount(int index); -int Scenario_Dialog(HWND hWndParent); unsigned int Wstring_Hash(Wstring & string); void __cdecl PMessagePrintf(int color, const char * fmt, ...); -void __cdecl SMessagePrintf(int color, const char * fmt, ...); -void _DrawMessage(int color, const char * msg, HWND window); -void _SetMessageString(HWND window, const char * msg, int len, int color); -HWND GameoptWindow(void); void PumpGameopts(bool, bool = false); bool DecodePubGameopt(char * options, char * name); void SendPublicGameopts(char const * options); void SendPrivateGameopts(char const * player, char const * options); -void DisplayGameopts(HWND window, BOOL initialize); -void LBSaveSelections(HWND win, Dictionary & lbdict); -void LBRestoreSelections(HWND win, Dictionary & lbdict); char * CalcRandomMapDigest(void); int CreateRandomMap(void); diff --git a/code/netsocket.h b/code/netsocket.h index 397752e5d..d2c938e42 100644 --- a/code/netsocket.h +++ b/code/netsocket.h @@ -81,6 +81,11 @@ class SocketClass virtual void Close(void) = 0; virtual bool Is_Open(void) const = 0; + // The port the socket ended up bound to, in host order, which for a + // socket opened on port zero is the one the platform chose. Zero while + // the socket is closed. + virtual unsigned short Bound_Port(void) const = 0; + virtual bool Set_Broadcast(bool enable) = 0; virtual bool Set_Buffer_Sizes(int receive, int send) = 0; @@ -137,7 +142,7 @@ class NullSocketClass : public SocketClass void Set_Interfaces(std::vector interfaces) { Interfaces = std::move(interfaces); } - unsigned short Bound_Port(void) const { return(Port); } + unsigned short Bound_Port(void) const override { return(Port); } private: std::vector Inbound; diff --git a/code/netsocket_null.cpp b/code/netsocket_null.cpp index 9d55eb037..8c35f5412 100644 --- a/code/netsocket_null.cpp +++ b/code/netsocket_null.cpp @@ -25,6 +25,7 @@ bool NullSocketClass::Open(unsigned short port) void NullSocketClass::Close(void) { Opened = false; + Port = 0; Inbound.clear(); NextInbound = 0; } diff --git a/code/netsocket_posix.cpp b/code/netsocket_posix.cpp index 735be65e7..37d23bf26 100644 --- a/code/netsocket_posix.cpp +++ b/code/netsocket_posix.cpp @@ -60,6 +60,7 @@ class PosixSocketClass : public SocketClass bool Open(unsigned short port) override; void Close(void) override; bool Is_Open(void) const override { return(Socket >= 0); } + unsigned short Bound_Port(void) const override { return(BoundPort); } bool Set_Broadcast(bool enable) override; bool Set_Buffer_Sizes(int receive, int send) override; @@ -72,6 +73,7 @@ class PosixSocketClass : public SocketClass private: int Socket = -1; + unsigned short BoundPort = 0; }; @@ -110,6 +112,15 @@ bool PosixSocketClass::Open(unsigned short port) return(false); } + // A port of zero was bound to whatever the platform had spare, so ask for the + // one it chose; the receive pass recognizes our own broadcast by it. + BoundPort = port; + sockaddr_in bound = {}; + socklen_t bound_len = sizeof(bound); + if (getsockname(Socket, reinterpret_cast(&bound), &bound_len) == 0) { + BoundPort = ntohs(bound.sin_port); + } + // The transport polls, so the socket must never wait on a call. int nonblocking = 1; if (ioctl(Socket, FIONBIO, &nonblocking) < 0) { @@ -136,6 +147,7 @@ void PosixSocketClass::Close(void) close(Socket); Socket = -1; } + BoundPort = 0; } diff --git a/code/netsocket_win32.cpp b/code/netsocket_win32.cpp index 460b43ba6..0822de9dc 100644 --- a/code/netsocket_win32.cpp +++ b/code/netsocket_win32.cpp @@ -80,6 +80,7 @@ class WinsockSocketClass : public SocketClass bool Open(unsigned short port) override; void Close(void) override; bool Is_Open(void) const override { return(Socket != INVALID_SOCKET); } + unsigned short Bound_Port(void) const override { return(BoundPort); } bool Set_Broadcast(bool enable) override; bool Set_Buffer_Sizes(int receive, int send) override; @@ -92,6 +93,7 @@ class WinsockSocketClass : public SocketClass private: SOCKET Socket = INVALID_SOCKET; + unsigned short BoundPort = 0; bool Started = false; }; @@ -140,6 +142,15 @@ bool WinsockSocketClass::Open(unsigned short port) return(false); } + // A port of zero was bound to whatever Winsock had spare, so ask for the one it + // chose; the receive pass recognizes our own broadcast by it. + BoundPort = port; + sockaddr_in bound = {}; + int bound_len = sizeof(bound); + if (getsockname(Socket, reinterpret_cast(&bound), &bound_len) == 0) { + BoundPort = ntohs(bound.sin_port); + } + // The transport polls, so the socket must never wait on a call. u_long nonblocking = 1; if (ioctlsocket(Socket, FIONBIO, &nonblocking) == SOCKET_ERROR) { @@ -166,6 +177,7 @@ void WinsockSocketClass::Close(void) closesocket(Socket); Socket = INVALID_SOCKET; } + BoundPort = 0; } diff --git a/code/options.cpp b/code/options.cpp index 74ce00f6a..426f74ff4 100644 --- a/code/options.cpp +++ b/code/options.cpp @@ -76,7 +76,6 @@ #include "language/language.h" #include "mouse.h" #include "msgbox.h" -#include "ownrdraw.h" #include "rules.h" #include "session.h" #include "techno.h" @@ -84,6 +83,7 @@ #include "vector.h" #include "video.h" #include "vox.h" +#include "ui/uikeyboard.h" #include "diff.hh" @@ -588,253 +588,17 @@ int OptionsClass::Normalize_Volume(int volume) const } -/* - * Internal state-machine messages the hotkey-configuration dialog posts to - * itself. wParam/lParam are unused; each just triggers the matching refresh. - */ -#define HKD_FILL_COMMANDS (WM_USER + 100) /// rebuild the command listbox for the selected category -#define HKD_SHOW_COMMAND (WM_USER + 101) /// refresh the description / assigned-key panel for the selected command -#define HKD_APPLY_HOTKEY (WM_USER + 102) /// assign the hotkey edit's key to the selected command -#define HKD_REINIT (WM_USER + 103) /// full refresh: repopulate the category combo and reset - - -/// -/// Handles the messages for the keyboard configuration dialog. -/// This routine drives the category, command and hotkey controls, and hands the reassigned -/// keys back to the hotkey command list. Accepting the dialog writes the assignments out to -/// KEYBOARD.INI; canceling puts the previous assignments back. -/// -/// Returns with TRUE if the message was consumed by this dialog. -INT_PTR CALLBACK Hotkey_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - char buffer[64]; - int * retval; - static int current_selection = -1; - - INT_PTR result = OwnerDraw::Default_Dialog_Proc(window, message, wparam, lparam); - if (result) { - return(result); - } - - retval = (int *)GetWindowLongPtr(window, DWLP_USER); - - switch (message) { - case WM_COMMAND: - switch (LOWORD(wparam)) { - case IDOK: - if (HIWORD(wparam) == BN_CLICKED) { - CCINIClass ini; - ini.Clear(); - - for (int i = 0; i < HotkeyCommands.Count(); i++) { - CommandClass const * cmd = HotkeyCommands.Fetch_By_Position(i); - int key = HotkeyCommands.Fetch_ID_By_Position(i); - ini.Put_Int("Hotkey", cmd->Get_Unique_Name(), key); - } - - CDFileClass file("Keyboard.ini"); - ini.Save(file, false); - *retval = IDOK; - return(TRUE); - } - break; - - case IDCANCEL: - if (HIWORD(wparam) == BN_CLICKED) { - Init_Hotkeys(); - *retval = 2; - return(TRUE); - } - break; - - case IDC_KEY_COMMANDS: - if (HIWORD(wparam) == LBN_SELCHANGE) { - SendMessage(window, HKD_SHOW_COMMAND, 0, 0); - HWND hotkey = GetDlgItem(window, IDC_KEY_HOTKEY); - if (hotkey != NULL) { - SetFocus(hotkey); - return(TRUE); - } - } - break; - - case IDC_KEY_ASSIGN: - SendMessage(window, HKD_APPLY_HOTKEY, 0, 0); - SendMessage(window, HKD_SHOW_COMMAND, 0, 0); - return(TRUE); - - case IDC_KEY_HOTKEY: - if (HIWORD(wparam) == EN_CHANGE) { - int key = SendMessage((HWND)lparam, HKM_GETHOTKEY, 0, 0); - char const * key_name; - if (HotkeyCommands.Is_Present(key)) { - key_name = HotkeyCommands[key]->Get_Display_Name(); - if (key_name == NULL) { - key_name = ""; - } - } else { - key_name = ""; - } - HWND hotkey_name = GetDlgItem(window, IDC_KEY_ASSIGNED_TO); - SetWindowText(hotkey_name, key_name); - return(TRUE); - } - break; - - case IDC_KEY_RESET_ALL: - if (HIWORD(wparam) == BN_CLICKED) { - if (WWMessageBox()._Process(TXT_RESET_HOTKEYS, IDOK, TXT_YES, TXT_NO, TXT_NONE, false) == 0) { - DebugString("Deleting users KEYBOARD.INI\n"); - // Only the player's own file is discarded; the defaults a - // deployment ships are what the reset falls back on. - CCFileClass file("KEYBOARD.INI"); - file.Delete(); - Init_Hotkeys(); - SendMessage(window, HKD_REINIT, 0, 0); - return(TRUE); - } - } - break; - - case IDC_KEY_CATEGORY: - if (HIWORD(wparam) == CBN_SELCHANGE) { - SendMessage(window, HKD_FILL_COMMANDS, 0, 0); - return(TRUE); - } - break; - } - return(TRUE); - - case HKD_APPLY_HOTKEY: { - HWND list_commands = GetDlgItem(window, IDC_KEY_COMMANDS); - int selection = ListBox_GetCurSel(list_commands); - if (selection != LB_ERR) { - CommandClass const * cmd = (CommandClass const *)ListBox_GetItemData(list_commands, selection); - for (int i = 0; i < HotkeyCommands.Count(); i++) { - if (HotkeyCommands.Fetch_By_Position(i) == cmd) { - HotkeyCommands.Remove_Index(HotkeyCommands.Fetch_ID_By_Position(i)); - break; - } - } - HWND hotkey = GetDlgItem(window, IDC_KEY_HOTKEY); - int key = SendMessage(hotkey, HKM_GETHOTKEY, 0, 0); - if (key != 0) { - HotkeyCommands.Remove_Index(key); - HotkeyCommands.Add_Index(key, cmd); - return(TRUE); - } - } - return(TRUE); - } - - case HKD_SHOW_COMMAND: { - HWND list_commands = GetDlgItem(window, IDC_KEY_COMMANDS); - int selection = ListBox_GetCurSel(list_commands); - if (selection != LB_ERR) { - CommandClass const * cmd = (CommandClass const *)ListBox_GetItemData(list_commands, selection); - HWND description = GetDlgItem(window, IDC_KEY_DESCRIPTION); - SetWindowText(description, cmd->Get_Description()); - - int key = 0; - for (int i = 0; i < HotkeyCommands.Count(); i++) { - if (HotkeyCommands.Fetch_By_Position(i) == cmd) { - key = HotkeyCommands.Fetch_ID_By_Position(i); - break; - } - } - - HWND key_label = GetDlgItem(window, IDC_KEY_CURRENT_SHORTCUT); - Build_Hotkey_String((KeyNumType)key, buffer); - SetWindowText(key_label, buffer); - - HWND hotkey = GetDlgItem(window, IDC_KEY_HOTKEY); - SendMessage(hotkey, HKM_SETHOTKEY, 0, 0); - - HWND hotkey_name = GetDlgItem(window, IDC_KEY_ASSIGNED_TO); - SetWindowText(hotkey_name, ""); - return(TRUE); - } - return(TRUE); - } - - case HKD_FILL_COMMANDS: { - HWND cmb_category = GetDlgItem(window, IDC_KEY_CATEGORY); - if (ComboBox_GetCurSel(cmb_category) != current_selection) { - current_selection = ComboBox_GetCurSel(cmb_category); - GetWindowText(cmb_category, buffer, sizeof(buffer)); - HWND list_commands = GetDlgItem(window, IDC_KEY_COMMANDS); - ListBox_ResetContent(list_commands); - for (int i = 0; i < AllCommands.Count(); i++) { - CommandClass const * cmd = AllCommands[i]; - if (stricmp(cmd->Get_Category(), buffer) == 0) { - int index = ListBox_AddString(list_commands, cmd->Get_Display_Name()); - if (index != LB_ERR) { - ListBox_SetItemData(list_commands, index, (LPARAM)cmd); - } - } - } - HWND description = GetDlgItem(window, IDC_KEY_DESCRIPTION); - SetWindowText(description, ""); - ListBox_SetCurSel(description, 0); - SendMessage(window, HKD_SHOW_COMMAND, 0, 0); - return(TRUE); - } - return(TRUE); - } - - case HKD_REINIT: { - HWND cmb_category = GetDlgItem(window, IDC_KEY_CATEGORY); - ComboBox_ResetContent(cmb_category); - for (int i = 0; i < AllCommands.Count(); i++) { - CommandClass const * cmd = AllCommands[i]; - const char * s = cmd->Get_Category(); - if (ComboBox_FindString(cmb_category, 0, s) == CB_ERR) { - s = cmd->Get_Category(); - ComboBox_AddString(cmb_category, s); - } - } - ComboBox_SetCurSel(cmb_category, 0); - SendMessage(window, HKD_FILL_COMMANDS, 0, 0); - current_selection = -1; - return(TRUE); - } - - case WM_INITDIALOG: - SendMessage(window, HKD_REINIT, 0, 0); - return(FALSE); - } - - return(FALSE); -} - - /// /// Displays the keyboard configuration dialog. -/// This routine brings up the hotkey assignment dialog and does not return until the player -/// dismisses it. The title screen is kept refreshed while the dialog is up outside of a +/// This routine brings up the hotkey assignment screen and does not return until the player +/// dismisses it. The title screen is kept refreshed while the screen is up outside of a /// game. /// bool OptionsClass::Hotkey_Dialog(void) { - HWND handle; - int res = -1; - - handle = OwnerDraw::Begin_Dialog(IDD_OPT_KEYBOARD, Hotkey_Dialog_Proc); - - if (handle != NULL) { - SetWindowLongPtr(handle, DWLP_USER, (LONG_PTR)&res); - OwnerDraw::Display_Dialog(handle); - - while (res < 0) { - if (OwnerDraw::Dialog_Message_Handler() == true) { - res = 2; - } - if (!GameActive) { - Title_Screen_Restore(); - } - } - OwnerDraw::End_Dialog(handle); - } + UIKeyboardPresenterClass screen; + screen.Refresh(); + UI_Keyboard_Screen(screen); return(true); } diff --git a/code/overlay.cpp b/code/overlay.cpp index 2000810a1..2de57e657 100644 --- a/code/overlay.cpp +++ b/code/overlay.cpp @@ -36,7 +36,6 @@ * OverlayClass::new -- Allocates a overlay object from pool * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "overlay.h" diff --git a/code/overlay.h b/code/overlay.h index 7ee7f1254..bf5a122a3 100644 --- a/code/overlay.h +++ b/code/overlay.h @@ -32,7 +32,7 @@ #pragma once -#include "isun.h" +#include "classids.h" #include "object.h" #include "overlay.hh" @@ -63,7 +63,7 @@ class OverlayClass : public ObjectClass OverlayClass(OverlayTypeClass const * ttype, Cell const & pos = CELL_NONE, HousesType = HOUSE_NONE); virtual ~OverlayClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override {if (retval == NULL) return(E_POINTER);*retval = CLSID_OverlayClass;return(S_OK);} + virtual ClassID Class_ID(void) const override {return(ClassID_OverlayClass);} virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/overtype.cpp b/code/overtype.cpp index c00b1614a..8ce1f2cdd 100644 --- a/code/overtype.cpp +++ b/code/overtype.cpp @@ -46,7 +46,6 @@ * OverlayTypeClass::operator new -- Allocate an overlay type class object from pool. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "overtype.h" @@ -482,17 +481,9 @@ void OverlayTypeClass::Serialize(SaveStreamClass & stream) } -/// -/// Fetches the class identifier of this object. -/// The save system asks for this so that it knows which class to construct when the object -/// is read back out of a save file. -/// -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE OverlayTypeClass::GetClassID(CLSID * retval) +ClassID OverlayTypeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_OverlayTypeClass; - return(S_OK); + return(ClassID_OverlayTypeClass); } diff --git a/code/overtype.h b/code/overtype.h index 1920dd7f3..b80e1f56b 100644 --- a/code/overtype.h +++ b/code/overtype.h @@ -159,7 +159,7 @@ class OverlayTypeClass: public ObjectTypeClass OverlayTypeClass(char const * ininame = NULL); ~OverlayTypeClass(void); - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; virtual void Post_Load(void) override; diff --git a/code/ownrdraw.cpp b/code/ownrdraw.cpp deleted file mode 100644 index 7d7592e10..000000000 --- a/code/ownrdraw.cpp +++ /dev/null @@ -1,7019 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2026 OpenTS contributors - * - * See LICENSE.md for applicable additional terms and warranty disclaimers. - ******************************************************************************/ - -#include "always.h" - -#include "ownrdraw.h" - -#include "_keyboar.h" -#include "_mixfile.h" -#include "_rules.h" -#include "_surface.h" -#include "_xmouse.h" -#include "arraylist.h" -#include "bsurface.h" -#include "conquer.h" -#include "data.h" -#include "dbgprint.h" -#include "dict.h" -#include "audio/audioengine.h" -#include "dsurface.h" -#include "globals.h" -#include "goptions.h" -#include "hsv.h" -#include "keyboard.h" -#include "language/language.h" -#include "mainloop.h" -#include "misc.h" -#include "msgroute.h" -#include "vidscale.h" -#include "video.h" -#include "mixfile.h" -#include "msgloop.h" -#include "rgb.h" -#include "rules.h" -#include "session.h" -#include "srfcache.h" -#include "theme.h" -#include "utf8.h" -#include "voc.h" -#include "vox.h" -#include "windlg.h" - -#include -#include -#include -#include - - - -using namespace OwnerDraw; - -extern unsigned int Wstring_Hash(Wstring & string); - - -int _mouse_counter; -int _surface_count; - -/* - * globals - */ -int ODBorderThickness; -int ODColorSteps; -int ODScrollBarAdj; -COLORREF ODColorText; -COLORREF ODColorTextDim; -COLORREF ODColorDisabled; -COLORREF ODColorFrame; -COLORREF ODListBoxColor; -static COLORREF ODTooltipBoxColor; -COLORREF ODColorUnused1; - -/* - * fonts - */ -HFONT ODFontPtr; -HFONT ODListFontPtr; -char const * ODFontName = "MS Sans Serif"; -char const * ODListFontName = "MS Sans Serif"; -int ODFontSize = 14; -int ODListFontSize = 12; - - -#define RECT_WIDTH(rc) ((rc).right - (rc).left) -#define RECT_HEIGHT(rc) ((rc).bottom - (rc).top) - - -void ODDrawDimmedBackground(Rect const & rect, HWND hWndc); -void ODDrawGradientRect(Rect const & rect, Surface & surf, int color, int scale); -void ODDrawBevelDarken(Rect const & rect, Surface & surf, int xpos, int ypos); - -void ODInitMasks(void); -void ODCacheImages(void); -int ODColorToHiColor(COLORREF color); - - -/* - * private forward declarations - */ -LRESULT CALLBACK ComboDropWinCtrlProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam); -LRESULT CALLBACK CtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); -LRESULT CALLBACK DefaultCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); -LRESULT CALLBACK ButtonCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); -LRESULT CALLBACK TextBoxCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); -LRESULT CALLBACK EditBoxCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); -LRESULT CALLBACK StaticCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); -LRESULT CALLBACK CheckBoxCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); -LRESULT CALLBACK ComboBoxCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); -LRESULT CALLBACK ListBoxCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); -LRESULT CALLBACK ScrollBarCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); -LRESULT CALLBACK ProgressBarCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); -LRESULT CALLBACK TrackBarCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); -LRESULT CALLBACK GroupBoxCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); -LRESULT CALLBACK HotkeyCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); -INT_PTR CALLBACK Custom_Message_Box_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); - -BOOL CALLBACK ODRemoveFromDict(HWND window, LPARAM); -int WINAPI ODUpdateWindowRect(HWND window, RECT *rect); -bool ODGetFontMetrics(char const *font_name, FontMetrics *metrics); -int ODDrawTextBG(Surface & surface, LPCSTR string, LPRECT rect, HGDIOBJ font, COLORREF color, UINT format); -void ODFillRectTrans(Rect const & rect, Surface & surf, int color, int trans); -void ODDrawArrowBitmap(Surface & surface, Rect const & rect, BOOL upward, BOOL pressed); -void ODDrawEdgeGlows(Surface & surface, Rect const & rect, BOOL raised, int count, int left_alpha, int top_alpha, int right_alpha, int bottom_alpha); -BOOL CALLBACK ODAddWindowToList(HWND window, ArrayList * list); - -BOOL CALLBACK SetUserData2(HWND window, LPARAM lparam); -BOOL CALLBACK InitializeCtrl(HWND window, LPARAM lparam); -void ODDrawCharRemap(Surface & dst_surf, const char *text, int max_chars, Rect const & rect, char const *font_name, COLORREF color, char flags, int char_spacing); - - -// The dialog fonts are 256-cell sheets in Windows-1252 order; a code point without a cell -// draws as '?'. -static unsigned char OD_Glyph(char32_t code) -{ - if (code < ' ') { - return((unsigned char)code); - } - int index = UTF8::Windows_1252_Glyph(code); - return((unsigned char)(index < 0 ? '?' : index)); -} - - -/////////////////////////////////// - -/// -/// Constructs an empty list box cell. -/// -OwnerDraw::CellData::CellData(void) -{ - type = CellData::INVALID; - color = -1; - pingtime = -1; - surf = NULL; -} - - -/// -/// Fetches the hash value for a window handle. -/// This is the hash routine handed to the dictionaries that key their entries by the -/// window handle of a subclassed control. -/// -/// Returns with the handle itself, taken as an unsigned value. -unsigned int Hash_HWND(HWND &key) -{ - return((unsigned int)(uintptr_t)key); -} - - -/// -/// Fetches the hash value for a control message key. -/// This is the hash routine handed to the dictionary CtrlProc keeps of the messages it is -/// already in the middle of handling. -/// -/// Returns with the hash value formed from the window and the message. -unsigned int Hash_CtrlMsg(CtrlMsgData &key) -{ - return((unsigned int)((uintptr_t)key.message * (uintptr_t)key.window)); -} - - -/// -/// Determines if two control message keys refer to the same thing. -/// -/// bool; Do both keys name the same window and the same message? -bool OwnerDraw::CtrlMsgData::operator ==(CtrlMsgData const & that) const -{ - if (that.window == window && that.message == message){ - return(true); - } - return(false); -} - - -/// -/// Sets the owner-draw metrics and colors to their defaults. -/// This routine establishes the border thickness, the blend strength and the family of -/// colors that every owner-draw control paints with. Each control asks for it as it is -/// subclassed, so the values are always current. -/// -void OwnerDraw::Initialize(void) -{ - ODBorderThickness = 1; - ODColorSteps = 40; - ODScrollBarAdj = 127; - ODColorText = RGB(112,255,0); - ODColorTextDim = RGB(16,144,16); - ODColorDisabled = RGB(144,144,144); - ODColorFrame = RGB(78, 182, 220); - ODListBoxColor = RGB(34,80,97); - ODTooltipBoxColor = RGB(11, 27, 34); - ODColorUnused1 = RGB(22, 55, 68); -} - - - -SurfaceCacheClass SurfaceCache; - -/* - * OriginalWndProcs contains original Win32 procs. - * CustomWndProcs contains per-control custom procs. - * The main proc for all controls is CtrlProc, it calls the custom proc, - * and the custom proc usually calls the Win32 proc. - */ -Dictionary OriginalWndProcs(Hash_HWND); -Dictionary CustomWndProcs(Hash_HWND); -Dictionary ODWinData(Hash_HWND); - - -/// -/// Registers the window classes the owner-draw system needs. -/// The combo box drop-down is a class of its own rather than a stock control, so it has to -/// be registered with Windows before any dialog is subclassed. Later calls do nothing. -/// -void OwnerDraw::Register_Control_Classes(void) -{ - static int registered = 0; - if (registered == 1) { - return; - } else { - registered = 1; - WNDCLASS wc; - memset(&wc, 0, sizeof(wc)); - wc.style = CS_HREDRAW | CS_VREDRAW; - wc.lpfnWndProc = ComboDropWinCtrlProc; - wc.cbClsExtra = 0; - wc.cbWndExtra = 0; - wc.hInstance = ProgramInstance; - wc.hIcon = NULL; - wc.hCursor = NULL; - wc.hbrBackground = NULL; - wc.lpszMenuName = "ComboDropWin"; - wc.lpszClassName = "ComboDropWin"; - RegisterClass(&wc); - } -} - -static HWND _dropdown_window = NULL; -static HWND _dropdown_owner = NULL; - - -/// -/// Handles the messages for a combo box drop-down window. -/// The dropped list is a window of its own rather than a stock Windows list, so that it -/// can be painted over a dimmed copy of the dialog background. This routine tracks the -/// item under the mouse, attaches or removes a scroll bar as the item count demands, and -/// folds the list back into the owning combo box once a selection is made. -/// -/// Returns with zero for the messages handled here; otherwise with the result of -/// the default window procedure. -static LRESULT CALLBACK ComboDropWinCtrlProc_Internal(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam); - - -/// -/// Hands the drop-down its messages, in frame coordinates, and presents what it paints. -/// -LRESULT CALLBACK ComboDropWinCtrlProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) -{ - LPARAM translated_lparam; - if (Route_Mouse_Message(hWnd, Msg, wParam, lParam, &translated_lparam)) { - return(0); - } - - LRESULT result = ComboDropWinCtrlProc_Internal(hWnd, Msg, wParam, translated_lparam); - - if (Msg == WM_PAINT) { - Video_Present_If_Dirty(); - } - - return(result); -} - - -static LRESULT CALLBACK ComboDropWinCtrlProc_Internal(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) -{ - static HBRUSH SolidBrush = CreateSolidBrush(RGB(48,96,48)); - static HWND OwnerComboHandle; - (void)SolidBrush; - - RECT rect; - Get_Display_Rect(hWnd, &rect); - - RECT client; - GetClientRect(hWnd, &client); - - RECT parent_rect; - memset(&parent_rect, 0, sizeof(parent_rect)); - - HWND hWndParent = NULL; - WinData * parent_data = NULL; - - int scrollbar_width = 2 * ODBorderThickness + 18; - int need_scrollbar = -1; - int max_top_index = 0; - - if (OwnerComboHandle) { - hWndParent = GetParent(OwnerComboHandle); - } - - if (hWndParent) { - ODWinData.getPointer(hWndParent, &parent_data); - Get_Display_Rect(hWndParent, &parent_rect); - } - - _dropdown_owner = hWndParent; - _dropdown_window = hWnd; - - WinData * data = NULL; - WinData * master_data = NULL; - - ODWinData.getPointer(hWnd, &data); - if (data == NULL) { - DebugString("ComboBox dropdown windata = NULL\n"); - } - - if (OwnerComboHandle) { - ODWinData.getPointer(OwnerComboHandle, &master_data); - } - - if (Msg != CB_GETCOUNT && Msg != CB_GETITEMHEIGHT && Msg != WM_VSCROLL) { - LRESULT item_count = SendMessage(OwnerComboHandle, CB_GETCOUNT, 0, 0); - LRESULT item_height = SendMessage(OwnerComboHandle, CB_GETITEMHEIGHT, 0, 0); - if (item_height <= 1) { - item_height = 1; - } - - need_scrollbar = (item_count * item_height > client.bottom - client.top); - max_top_index = item_count - (client.bottom - client.top) / item_height; - - if (data) { - if ((UINT_PTR)data->attachedWindow > 1) { - SCROLLINFO info; - info.cbSize = sizeof(SCROLLINFO); - info.fMask = SIF_RANGE | SIF_POS; - info.nMin = 0; - info.nMax = max_top_index; - info.nPos = data->ComboDrop.scrollTop; - SendMessage(data->attachedWindow, SBM_SETSCROLLINFO, 0, (LPARAM)&info); - } - if (data->attachedWindow != NULL) { - BringWindowToTop(data->attachedWindow); - } - } - } - - switch (Msg) { - case WM_ERASEBKGND: - return(0); - - case WM_CREATE: - SetCapture(hWnd); - OwnerComboHandle = *(HWND *)lParam; - return(0); - - case WM_PAINT: { - LRESULT item_count = SendMessage(OwnerComboHandle, CB_GETCOUNT, 0, 0); - LRESULT item_height = SendMessage(OwnerComboHandle, CB_GETITEMHEIGHT, 0, 0); - int client_height = client.bottom - client.top; - int selected_index = data->ComboDrop.selection; - - Rect dest_rect(rect.left, rect.top, rect.right - rect.left, client_height); - int source_x = rect.left - parent_rect.left; - int source_y = rect.top - parent_rect.top; - - if (data->cachedSurface == NULL) { - Rect dst(0, 0, client.right, client.bottom); - Rect src(source_x, source_y, client.right, client.bottom); - BSurface * surface = new BSurface(client.right, client.bottom, 2); - data->cachedSurface = surface; - ++_surface_count; - - if (parent_data != NULL && parent_data->cachedSurface != NULL) { - surface->Blit_From(dst, *parent_data->cachedSurface, src, false, true); - } - - int total = client.right * client.bottom; - unsigned short * pixels = (unsigned short *)surface->Lock(); - for (int i = 0; i < total; ++i) { - pixels[i] = OD_Blend_Color(pixels[i], 0, 180); - } - surface->Unlock(); - } - - { - Rect src(0, 0, rect.right - rect.left, client_height); - AlternateSurface->Blit_From(dest_rect, *data->cachedSurface, src, false, true); - } - - { - Rect border(rect.left + 1, rect.top + 1, rect.right - rect.left - 2, client_height - 2); - OD_Draw_Rect(*AlternateSurface, border, 1, 0xFFFFFFFF); - } - - FontMetrics font_data; - int have_font = ODGetFontMetrics("dlgsys", &font_data); - - int index = data->ComboDrop.scrollTop; - if (index < item_count) { - int row_base = item_height * index; - int row = row_base; - - while (1) { - int left = rect.left + 1; - int width = rect.right - rect.left - 2; - int top = row - row_base + rect.top; - - if (item_height + row - row_base > client_height) { - break; - } - - char text[128]; - SendMessage(OwnerComboHandle, CB_GETLBTEXT, (WPARAM)index, (LPARAM)text); - - if (index == selected_index) { - Rect fill(left, top + 1, width, item_height - 2); - int fill_color = ODListBoxColor; - if (fill_color != -1) { - fill_color = ODColorToHiColor(fill_color); - } - AlternateSurface->Fill_Rect(fill, fill_color); - } - - COLORREF text_color = ODColorText; - if (index < 50 && master_data != NULL && master_data->ComboBox.itemColors[index] != -1) { - text_color = master_data->ComboBox.itemColors[index]; - } - - if (have_font) { - int text_width = 0; - for (char const * cursor = text; *cursor; ) { - text_width += font_data.charWidths[OD_Glyph(UTF8::Decode(cursor))]; - } - - int max_width = width - 10; - int ellipsis_width = 3 * font_data.charWidths['.']; - int clipped = 0; - - if (text_width > max_width) { - while (strlen(text) > 0) { - char * last = UTF8::Previous(text, text + strlen(text)); - text_width -= font_data.charWidths[OD_Glyph(UTF8::Peek(last))]; - *last = '\0'; - if (!clipped) { - text_width += ellipsis_width; - } - clipped = 1; - if (text_width <= max_width) { - strcat(text, "..."); - break; - } - } - } - } - - RECT text_rect; - text_rect.left = left + 3; - text_rect.top = top; - text_rect.right = left + width; - text_rect.bottom = top + item_height; - OD_Draw_Text_Remap(*AlternateSurface, text, *(Rect *)&text_rect, "dlgsys", text_color, 4, 0); - - ++index; - row += item_height; - if (index >= item_count) { - break; - } - } - } - - AlternateSurface->Lock(); - VisibleSurface->Lock(); - - RECT src_rect; - src_rect.left = rect.left; - src_rect.top = rect.top; - src_rect.right = rect.right - rect.left; - src_rect.bottom = client_height; - - RECT window_rect; - GetWindowRect(hWnd, &window_rect); - - RECT dst_rect = src_rect; - - VisibleSurface->Blit_From(*(Rect *)&dst_rect, *AlternateSurface, *(Rect *)&src_rect, false, true); - - VisibleSurface->Unlock(); - AlternateSurface->Unlock(); - - ValidateRect(hWnd, NULL); - return(0); - } - - case WM_NCDESTROY: - _dropdown_window = NULL; - _dropdown_owner = NULL; - ReleaseCapture(); - break; - - case WM_VSCROLL: { - LRESULT top_index = SendMessage(data->attachedWindow, SBM_GETPOS, 0, 0); - if (top_index != SendMessage(hWnd, CB_GETTOPINDEX, 0, 0)) { - SendMessage(hWnd, CB_SETTOPINDEX, top_index, 0); - } - break; - } - - case CB_GETTOPINDEX: - if (data) { - return(data->ComboDrop.scrollTop); - } - break; - - case WM_MOUSEMOVE: { - int x = (unsigned short)LOWORD(lParam); - int y = (unsigned short)HIWORD(lParam); - - if (x >= 0 && y >= 0 && x < client.right && y < client.bottom) { - LRESULT item_height = SendMessage(OwnerComboHandle, CB_GETITEMHEIGHT, 0, 0); - LRESULT item_count = SendMessage(OwnerComboHandle, CB_GETCOUNT, 0, 0); - int index = y / item_height; - if (data) { - index += data->ComboDrop.scrollTop; - } - - int clamped = index < 0 ? 0 : index; - int max_index = (int)item_count - 1; - if (max_index < clamped) { - clamped = max_index; - } - - if (data->ComboDrop.selection != clamped) { - InvalidateRect(hWnd, NULL, FALSE); - } - data->ComboDrop.selection = clamped; - return(0); - } - return(0); - } - - case CB_SETTOPINDEX: { - int new_top = (int)wParam; - LRESULT item_count = SendMessage(OwnerComboHandle, CB_GETCOUNT, 0, 0); - LRESULT item_height = SendMessage(OwnerComboHandle, CB_GETITEMHEIGHT, 0, 0); - if (!item_count || !item_height) { - break; - } - - int visible = (client.bottom - client.top) / item_height; - if (new_top < 0) { - new_top = 0; - } - - int max_top = (int)item_count - visible; - if (max_top > 0) { - if (new_top > max_top) { - new_top = max_top; - } - } else { - new_top = 0; - } - - if (new_top != data->ComboDrop.scrollTop) { - data->ComboDrop.scrollTop = new_top; - InvalidateRect(hWnd, NULL, FALSE); - } - return(0); - } - - case WM_LBUTTONDOWN: - case WM_LBUTTONDBLCLK: { - int x = (unsigned short)LOWORD(lParam); - int y = (unsigned short)HIWORD(lParam); - - if (x > client.right && x < client.right + data->scrollBarWidth && y > 0 && y < client.bottom) { - SendMessage(data->attachedWindow, Msg, wParam, lParam); - break; - } - - Sound_Effect(Rule->GenericClick); - - if (x >= 0 && y >= 0 && x <= client.right && y <= client.bottom) { - LRESULT item_height = SendMessage(OwnerComboHandle, CB_GETITEMHEIGHT, 0, 0); - LRESULT item_count = SendMessage(OwnerComboHandle, CB_GETCOUNT, 0, 0); - - int index = y / item_height; - if (data) { - index += data->ComboDrop.scrollTop; - } - - int clamped = index < 0 ? 0 : index; - int max_index = (int)item_count - 1; - if (max_index < clamped) { - clamped = max_index; - } - - SendMessage(OwnerComboHandle, CB_SETCURSEL, clamped, 0); - - if (need_scrollbar) { - DestroyWindow(data->attachedWindow); - } - - ReleaseCapture(); - SendMessage(OwnerComboHandle, CB_SHOWDROPDOWN, FALSE, 0); - - SendMessage( - hWndParent, - WM_COMMAND, - MAKEWPARAM((UINT)GetWindowLong(OwnerComboHandle, GWL_ID), CBN_SELCHANGE), - (LPARAM)OwnerComboHandle); - return(0); - } - - if (need_scrollbar) { - DestroyWindow(data->attachedWindow); - } - ReleaseCapture(); - SendMessage(OwnerComboHandle, CB_SHOWDROPDOWN, FALSE, 0); - return(0); - } - - case OD_DROPSUBCLASSED: { - WinData * drop_data = NULL; - ODWinData.getPointer(hWnd, &drop_data); - data->ComboDrop.selection = SendMessage(OwnerComboHandle, CB_GETCURSEL, 0, 0); - return(0); - } - - default: - break; - } - - if (need_scrollbar == 1) { - if (data && !data->attachedWindow) { - - data->attachedWindow = (HWND)1; - GetWindowLong(hWnd, GWL_ID); - - hWndParent = GetParent(hWnd); - - RECT parent_display_rect; - Get_Display_Rect(hWndParent, &parent_display_rect); - - RECT drop_display_rect; - Get_Display_Rect(hWnd, &drop_display_rect); - - int left = drop_display_rect.left - parent_display_rect.left; - int top = drop_display_rect.top - parent_display_rect.top; - - HWND scroll_wnd = CreateWindowEx( - 0, - "Scrollbar", - NULL, - 0x50010001u, - left + client.right - scrollbar_width, - top + client.top, - scrollbar_width, - drop_display_rect.bottom - drop_display_rect.top, - hWndParent, - NULL, - ProgramInstance, - NULL); - - data->attachedWindow = scroll_wnd; - data->scrollBarWidth = scrollbar_width; - - InitializeCtrl(scroll_wnd, 0); - - WinData *scroll_data = NULL; - ODWinData.getPointer(scroll_wnd, &scroll_data); - scroll_data->ownerWindow = hWnd; - - SCROLLINFO info; - info.cbSize = sizeof(SCROLLINFO); - info.fMask = SIF_RANGE | SIF_POS; - info.nMin = 0; - info.nMax = max_top_index; - info.nPos = data->ComboDrop.scrollTop; - SendMessage(scroll_wnd, SBM_SETSCROLLINFO, 0, (LPARAM)&info); - - SetWindowPos( - hWnd, - NULL, - 0, - 0, - (drop_display_rect.right - drop_display_rect.left) - scrollbar_width, - drop_display_rect.bottom - drop_display_rect.top, - SWP_NOMOVE); - - ShowWindow(scroll_wnd, SW_SHOW); - BringWindowToTop(scroll_wnd); - InvalidateRect(scroll_wnd, NULL, FALSE); - UpdateWindow(scroll_wnd); - - SendMessage(scroll_wnd, OD_SETTOP, (WPARAM)scroll_wnd, 1); - SendMessage(scroll_wnd, OD_SETKEEPCAPTURE, 0, 1); - - RECT validate_rect; - validate_rect.left = left; - validate_rect.top = top; - validate_rect.right = left + client.right + 1; - validate_rect.bottom = top + client.bottom + 1; - ValidateRect(hWndParent, &validate_rect); - } - - return(DefWindowProc(hWnd, Msg, wParam, lParam)); - } - - if (need_scrollbar == 0 && data && data->attachedWindow && !data->paintDisabled) { - HWND scroll_wnd = data->attachedWindow; - DestroyWindow(scroll_wnd); - - OriginalWndProcs.remove(scroll_wnd); - ODWinData.remove(scroll_wnd); - CustomWndProcs.remove(scroll_wnd); - - data->attachedWindow = NULL; - data->scrollBarWidth = 0; - - SetWindowPos( - hWnd, - NULL, - 0, - 0, - ODBorderThickness + client.right - client.left + scrollbar_width + 1, - client.bottom + 2 * ODBorderThickness - client.top, - SWP_NOMOVE); - - hWndParent = GetParent(hWnd); - Get_Display_Rect(hWndParent, &parent_rect); - - RECT validate_rect; - validate_rect.left = rect.left - parent_rect.left - 1; - validate_rect.top = rect.top - parent_rect.top - 1; - validate_rect.right = rect.left - parent_rect.left + client.right + scrollbar_width; - validate_rect.bottom = rect.top - parent_rect.top + client.bottom + 1; - ValidateRect(hWndParent, &validate_rect); - } - - return(DefWindowProc(hWnd, Msg, wParam, lParam)); -} - - -/// -/// Subclasses a dialog and every one of its controls for owner drawing. -/// This is the routine a dialog procedure calls when it is first created. Painting is -/// suppressed across the whole window while the controls are being hooked, so the dialog -/// never flickers through its stock Windows appearance on the way. -/// -/// Caller supplied value to store with each control for its own use. -bool OwnerDraw::Subclass_Dialog(HWND window, LPARAM lparam) -{ - OwnerDraw::Register_Control_Classes(); - - EnumChildWindows(window, SetUserData2, 1); - SetUserData2(window, 1); - - EnumChildWindows(window, InitializeCtrl, lparam); - InitializeCtrl(window, lparam); - - EnumChildWindows(window, SetUserData2, 0); - SetUserData2(window, 0); - return(true); -} - - -/// -/// Sets the paint suppression flag for one window. -/// This is the enumeration callback Subclass_Dialog uses to silence, and later re-enable, -/// painting across a whole dialog. A window with no owner-draw record yet is given one. -/// -/// Should painting be suppressed for this window? -BOOL CALLBACK SetUserData2(HWND window, LPARAM lparam) -{ - WinData * data = NULL; - WinData temp; - - if (!ODWinData.getPointer(window, &data)) { - memset(&temp, 0, sizeof(temp)); - ODWinData.add(window, temp); - ODWinData.getPointer(window, &data); - } - - data->paintDisabled = lparam; - - return(TRUE); -} - - -/// -/// Builds the artwork, masks and fonts every owner-draw control paints with. Later calls do -/// nothing, so anything drawing with those resources may ask for them. -/// -/// Supplies the display context the fonts are made against. -void OwnerDraw::Prepare_Resources(HWND window) -{ - Initialize(); - - static int _inited = false; - if (!_inited) { - ODInitMasks(); - ODCacheImages(); - HDC hdc = GetDC(window); - ODFontPtr = WS_Get_Font(hdc, ODFontName, 0, ODFontSize, 0); - ODListFontPtr = WS_Get_Font(hdc, ODListFontName, 0, ODListFontSize, 0); - ReleaseDC(window, hdc); - _inited = 1; - } -} - - -/// -/// Prepares one control for owner drawing. -/// This is the enumeration callback Subclass_Dialog uses. The control's window class and style -/// pick the custom procedure that will paint it, its original procedure is displaced by -/// CtrlProc and remembered, and the control is then told that it has been subclassed. The -/// shared fonts, color masks and cached artwork are built on the first control to arrive. -/// -/// Caller supplied value to store with the control for its own use. -BOOL CALLBACK InitializeCtrl(HWND window, LPARAM lparam) -{ - char class_name[128]; - GetClassName(window, class_name, sizeof(class_name)); - - LONG style = GetWindowLong(window, GWL_STYLE); - - RECT rect1; - Get_Display_Rect(window, &rect1); - RECT rect2; - GetClientRect(window, &rect2); - - OwnerDraw::Prepare_Resources(window); - - WNDPROC customProc = NULL; - - if (strcmp(class_name, WC_SCROLLBAR) == 0) { - customProc = ScrollBarCtrlProc; - } else if (strcmp(class_name, WC_LISTBOX) == 0) { - customProc = ListBoxCtrlProc; - } else if (strcmp(class_name, WC_COMBOBOX) == 0) { - customProc = ComboBoxCtrlProc; - } else if (strcmp(class_name, TRACKBAR_CLASS) == 0) { - customProc = TrackBarCtrlProc; - } else if (strcmp(class_name, PROGRESS_CLASS) == 0) { - customProc = ProgressBarCtrlProc; - } else if (strcmp(class_name, WC_EDIT) == 0) { - customProc = EditBoxCtrlProc; - } else if (strcmp(class_name, WC_STATIC) == 0) { - customProc = StaticCtrlProc; - } else if (strcmp(class_name, WC_TABCONTROL) == 0) { - customProc = TextBoxCtrlProc; - } else if (strcmp(class_name, WC_BUTTON) == 0) { - if ((style & BS_GROUPBOX) == BS_GROUPBOX) { - customProc = GroupBoxCtrlProc; - } else if ((style & BS_OWNERDRAW) == BS_OWNERDRAW) { - customProc = ButtonCtrlProc; - } else if ((style & BS_AUTOCHECKBOX) == BS_AUTOCHECKBOX) { - customProc = CheckBoxCtrlProc; - } - } else if (strcmp(class_name, HOTKEY_CLASS) == 0) { - customProc = HotkeyCtrlProc; - } else { - customProc = DefaultCtrlProc; - } - - WNDPROC originalProc = (WNDPROC)SetWindowLongPtr(window, GWLP_WNDPROC, (LONG_PTR)CtrlProc); - - if (!CustomWndProcs.contains(window)) { - CustomWndProcs.add(window, customProc); - } - - if (!OriginalWndProcs.contains(window)) { - OriginalWndProcs.add(window, originalProc); - } - - WinData * data = NULL; - WinData temp; - - if (!ODWinData.getPointer(window, &data)) { - memset(&temp, 0, sizeof(temp)); - ODWinData.add(window, temp); - ODWinData.getPointer(window, &data); - } - - data->userData = lparam; - - SendMessage(window, OD_SUBCLASSED, 0, 0); - - return(TRUE); -} - - -/// -/// Removes a dialog and all of its controls from the owner-draw dictionaries. -/// Use this routine as a subclassed dialog is torn down. Windows is free to hand the same -/// handles out again, and a stale entry would misdirect the next dialog to use them. -/// -BOOL ODCleanupDicts(HWND window) -{ - EnumChildWindows(window, ODRemoveFromDict, 0); - ODRemoveFromDict(window, 0); - - return(TRUE); -} - - -/// -/// Removes one window from the owner-draw dictionaries. -/// This is the enumeration callback ODCleanupDicts uses. It drops the window's original -/// procedure, its custom procedure and its owner-draw record. -/// -BOOL CALLBACK ODRemoveFromDict(HWND window, LPARAM) -{ - OriginalWndProcs.remove(window); - ODWinData.remove(window); - CustomWndProcs.remove(window); - - return(TRUE); -} - - -Tooltip ODTooltip; -int ODLastTooltipTime; - - -/// -/// Constructs an empty tooltip. -/// -Tooltip::Tooltip(void) -{ - bounds.Set(0,0,0,0); - background = NULL; - text[0] = '\0'; - isActive = false; - isHidden = false; - window = (HWND)NULL; -} - - -/// -/// Starts displaying a tooltip over the given area. -/// Any tooltip already on screen is taken down first, since there is only ever one. The -/// area underneath is saved so the tooltip can be hidden again without the dialog having -/// to repaint itself. -/// -/// The area of the screen the tooltip is to occupy. -/// The text to display, or NULL for the placeholder text. -/// The control the tooltip belongs to. -/// bool; Was the tooltip displayed? -bool OwnerDraw::Start_Tooltip(Rect const & rect, char const * text, HWND window) -{ - OwnerDraw::End_Tooltip(); - - ODLastTooltipTime = time(NULL); - - sprintf(ODTooltip.text, "Tool Tip"); - - if (text != NULL) { - strcpy(ODTooltip.text, text); - } - - ODTooltip.bounds = rect; - ODTooltip.window = window; - ODTooltip.isActive = true; - - return(OwnerDraw::Show_Tooltip(true)); -} - - -/// -/// Draws the tooltip onto the visible surface. -/// Use this routine to bring the tooltip back after a repaint has forced it into hiding. -/// The background is only captured when the caller asks for it, since redrawing the -/// tooltip must not save the tooltip as its own background. -/// -/// Should the area under the tooltip be captured first? -/// bool; Was the tooltip drawn? -bool OwnerDraw::Show_Tooltip(bool save_background) -{ - if (ODTooltip.isActive == false) { - return(false); - } - - if (save_background) { - if (ODTooltip.background != NULL) { - delete ODTooltip.background; - } - ODTooltip.background = NULL; - - Surface * backgd = new BSurface(ODTooltip.bounds.Width, ODTooltip.bounds.Height, 2); - ODTooltip.background = backgd; - - Rect drect(0, 0, ODTooltip.bounds.Width, ODTooltip.bounds.Height); - backgd->Blit_From(drect, *VisibleSurface, ODTooltip.bounds); - } - - ODTooltip.isHidden = false; - - Rect rect = ODTooltip.bounds; - - int color = ODColorToHiColor(ODTooltipBoxColor); - - VisibleSurface->Lock(); - VisibleSurface->Fill_Rect(rect, color); - VisibleSurface->Unlock(); - - ODDrawBevelDarken(rect, *VisibleSurface, 8, 4); - - OD_Draw_Text(ODColorText, ODFontPtr, rect, ODTooltip.text, strlen(ODTooltip.text), 1, 1, VisibleSurface); - rect.X += 1; - rect.Y += 1; - rect.Width -= 2; - rect.Height -= 2; - OD_Draw_Rect(*VisibleSurface, rect, 1, -1); - - return(true); -} - - -/// -/// Hides the tooltip by restoring the area it covered. -/// The tooltip stays active while hidden, so it can be put back with Show_Tooltip once -/// whatever prompted the hide has finished painting. -/// -/// bool; Was the tooltip hidden? -bool OwnerDraw::Hide_Tooltip(void) -{ - if (ODTooltip.isActive == false) { - return(false); - } - - if (ODTooltip.isHidden == (int)true) { - return(false); - } - - if (ODTooltip.background == NULL) { - return(false); - } - - Rect drect = ODTooltip.bounds; - Rect srect(0,0, ODTooltip.bounds.Width, ODTooltip.bounds.Height); - VisibleSurface->Blit_From(drect, *ODTooltip.background, srect); - ODTooltip.isHidden = true; - return(true); -} - - -/// -/// Takes the tooltip down for good. -/// The area it covered is restored and the saved background released. Use this routine -/// when the mouse leaves the control, or when the control itself is going away. -/// -/// bool; Was there a tooltip to take down? -bool OwnerDraw::End_Tooltip(void) -{ - if (ODTooltip.isActive == false) { - return(false); - } - - OwnerDraw::Hide_Tooltip(); - - if (ODTooltip.background != NULL) { - delete ODTooltip.background; - } - ODTooltip.background = NULL; - - ODTooltip.isActive = false; - ODTooltip.isHidden = false; - return(true); -} - - -/// -/// Handles every message sent to a subclassed control. -/// This is the procedure that displaces the stock Windows procedure of each control, and -/// it is the heart of the owner-draw system. It enforces the modal window stack, guards -/// against a message re-entering the same control, drives the tooltip and hover timers, -/// accumulates the area that has to reach the screen, and then hands the message to the -/// control's own custom procedure -- which is usually the one that calls the original -/// Windows procedure. -/// -/// Returns with the result of the control's custom procedure, or zero when the -/// message was swallowed here. -static LRESULT CALLBACK CtrlProc_Internal(HWND window, UINT message, WPARAM wparam, LPARAM lparam); - - -/// -/// Hands a control its messages, in frame coordinates, and puts what it paints on screen. -/// The controls draw into the game's own surfaces rather than into their windows, so -/// every repaint has to be followed by a present to be seen. -/// -LRESULT CALLBACK CtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - LPARAM translated_lparam; - if (Route_Mouse_Message(window, message, wparam, lparam, &translated_lparam)) { - return(0); - } - - LRESULT result = CtrlProc_Internal(window, message, wparam, translated_lparam); - - if (message == WM_PAINT) { - Video_Present_If_Dirty(); - } - - return(result); -} - - -static LRESULT CALLBACK CtrlProc_Internal(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - static POINT min_update_rect = {0xFFFFFF, 0xFFFFFF}; - static POINT max_update_rect; - static int num_rect_updates; - - /* - * Tracks whether the game window had focus on the previous call so the - * controls can be refreshed when focus is regained. - */ - static bool was_in_focus = true; - - /* - * Set to 1 around the SetWindowPos() call issued by the OD_SETTOP handler, - * and checked by the WM_WINDOWPOSCHANGING handler so the engine does not - * fight its own z-order change. - */ - static char in_programmatic_reorder; - - if (message == WM_SETCURSOR) { - return(1); - } - - WNDPROC specific_proc = NULL; - CustomWndProcs.getValue(window, specific_proc); - - WNDPROC original_proc = NULL; - OriginalWndProcs.getValue(window, original_proc); - - if (message == WM_SYSKEYUP && wparam == VK_TAB) { - SendMessage(MainWindow, WM_SYSKEYUP, VK_TAB, lparam); - } - - LRESULT result = 0; - BOOL show_tooltip = FALSE; - int anim_state = 0; - - static Dictionary ctrlmessages(Hash_CtrlMsg); - - CtrlMsgData key; - key.window = window; - key.message = message; - - RECT display_rect; - Get_Display_Rect(window, &display_rect); - RECT window_rect; - GetWindowRect(window, &window_rect); - int offset_x = 0; - int offset_y = 0; - - bool mouse_over = false; - - static ArrayList hwndarray; - - HWND owner; - WinData *ownerdata; - int state; - - /* - * When a window has been pushed to the top (modal) via OD_SETTOP, swallow - * mouse messages that are not directed at it or one of its children. - */ - if (hwndarray.length() != 0) { - HWND topwindow; - hwndarray.getTail(topwindow); - - BOOL allow = FALSE; - BOOL forward = FALSE; - if (GetParent(window) == NULL) { - allow = TRUE; - } - if (GetWindowLong(window, GWL_ID) <= 0) { - allow = TRUE; - } - - HWND parent = window; - while (parent != NULL) { - if (parent == topwindow) { - allow = TRUE; - break; - } - parent = GetParent(parent); - } - - if (message < WM_MOUSEMOVE || message > WM_MBUTTONDBLCLK) { - forward = TRUE; - } - if (message >= WM_NCMOUSEMOVE && message <= WM_KEYLAST) { - forward = FALSE; - } - if (message == WM_SYSKEYUP || message == WM_SYSKEYDOWN || message == WM_SYSCOMMAND || message == WM_SYSCHAR) { - forward = TRUE; - } - if (message == OD_GETTIPTEXT || message == WM_TIMER || message == OD_GETCELLTIP) { - forward = FALSE; - } - if (!allow && !forward) { - return(0); - } - } - - /* - * Guard against re-entrant processing of the same message for the same - * window. A handful of messages are allowed to re-enter. - */ - if (ctrlmessages.contains(key)) { - if (message != WM_COMMAND && message != WM_SYSKEYDOWN && message != WM_SYSKEYUP - && message != WM_SYSCOMMAND && message != WM_SYSCHAR) { - return(0); - } - } - - bool processing = true; - ctrlmessages.remove(key); - ctrlmessages.add(key, processing); - - RECT client_rect; - GetClientRect(window, &client_rect); - RECT disp_rect; - Get_Display_Rect(window, &disp_rect); - - bool in_focus = GameInFocus; - - bool is_paint = false; - if (message == WM_PAINT) { - is_paint = true; - // A windowed game keeps presenting without the focus, so its dialogs keep painting too. - if (!in_focus && !WindowedMode) { - ValidateRect(window, NULL); - ctrlmessages.remove(key); - return(0); - } - } - - if (in_focus == true && !was_in_focus) { - // The dialogs skipped their paints while the focus was away, so they need one too. - RedrawWindow(MainWindow, NULL, NULL, RDW_INVALIDATE | RDW_ALLCHILDREN); - in_focus = GameInFocus; - } - was_in_focus = in_focus; - - RECT update_rect; - if (is_paint) { - ++num_rect_updates; - GetUpdateRect(window, &update_rect, FALSE); - update_rect.left += disp_rect.left; - update_rect.right += disp_rect.left; - update_rect.top += disp_rect.top; - update_rect.bottom += disp_rect.top; - } - - WinData *data = NULL; - ODWinData.getPointer(window, &data); - - if (message == OD_HASATTACHED) { - result = (data->attachedWindow != NULL); - goto cleanup; - } - - /* - * Save the device context's current font / colors so they can be - * restored by OD_RESTOREDC. - */ - if (message == OD_SAVEDC) { - HDC hdc = (HDC)lparam; - HGDIOBJ old = SelectObject(hdc, GetStockObject(SYSTEM_FONT)); - data->font = (HFONT)old; - SelectObject(hdc, old); - data->bkMode = GetBkMode(hdc); - data->bkColor = GetBkColor(hdc); - data->textColor = GetTextColor(hdc); - result = 1; - goto cleanup; - } - - /* - * Restore the device context state saved by OD_SAVEDC. - */ - if (message == OD_RESTOREDC) { - HDC hdc = (HDC)lparam; - SelectObject(hdc, data->font); - SetBkMode(hdc, data->bkMode); - SetBkColor(hdc, data->bkColor); - SetTextColor(hdc, data->textColor); - result = 1; - goto cleanup; - } - - /* - * Toggle paint suppression for this control (and its linked window). - */ - if (message == OD_DISABLEPAINT) { - { - HWND linked = (HWND)data->attachedWindow; - result = data->paintDisabled; - data->paintDisabled = lparam; - if (linked != NULL) { - WinData *linkeddata = NULL; - ODWinData.getPointer((HWND &)data->attachedWindow, &linkeddata); - if (linkeddata != NULL) { - linkeddata->paintDisabled = lparam; - } - } - } - - call_custom_proc: - if (specific_proc != NULL) { - result = CallWindowProc(specific_proc, window, message, wparam, lparam); - } - - after_proc: - if (message == WM_NCDESTROY) { - On_WM_NCDESTROY(window); - ODRemoveFromDict(window, 0); - } - goto cleanup; - } - - /* - * Push a window to the top of the modal stack. - */ - if (message == OD_SETTOP) { - HWND topwindow = NULL; - hwndarray.getTail(topwindow); - result = (LRESULT)topwindow; - - HWND target = window; - if (wparam) { - target = (HWND)wparam; - } - - HWND found = NULL; - int scan = 0; - for (int index = 0; index < hwndarray.length(); index++) { - hwndarray.get(found, scan); - if (found == target) { - hwndarray.remove(scan); - } else { - scan++; - } - } - - if (lparam) { - hwndarray.addTail(target); - in_programmatic_reorder = 1; - SetWindowPos(target, NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); - in_programmatic_reorder = 0; - } - goto cleanup; - } - - if (hwndarray.length() != 0) { - - /* - * Keep the modal window pinned on top when Windows tries to reorder it. - */ - if (message == WM_WINDOWPOSCHANGING) { - if (in_programmatic_reorder != 1) { - HWND found = NULL; - for (int index = 0; index < hwndarray.length(); index++) { - hwndarray.get(found, index); - if (found == window) { - WINDOWPOS *wp = (WINDOWPOS *)lparam; - if (index == hwndarray.length() - 1 && wp->hwndInsertAfter != NULL && (wp->flags & SWP_NOZORDER) == 0) { - wp->hwndInsertAfter = NULL; - InvalidateRect(window, NULL, FALSE); - result = 0; - } else { - wp->flags |= SWP_NOOWNERZORDER | SWP_NOZORDER; - InvalidateRect(window, NULL, FALSE); - result = 0; - } - goto cleanup; - } - } - } - goto call_custom_proc; - } - - /* - * Drop a destroyed window from the modal stack. - */ - if (message == WM_DESTROY) { - HWND found = NULL; - int scan = 0; - for (int index = 0; index < hwndarray.length(); index++) { - hwndarray.get(found, scan); - if (found == window) { - hwndarray.remove(scan); - } else { - scan++; - } - } - } - } - - /* - * Tear down the tooltip when its owning window is destroyed, hidden, or - * loses focus. - */ - if (window == ODTooltip.window - && (message == WM_NCDESTROY || message == WM_SHOWWINDOW || message == WM_KILLFOCUS) - && ODTooltip.isActive) { - OwnerDraw::End_Tooltip(); - } - - if (message == WM_ERASEBKGND) { - result = 1; - goto cleanup; - } else if (message == WM_SETFOCUS) { - if (specific_proc == ButtonCtrlProc || specific_proc == ListBoxCtrlProc) { - SetFocus((HWND)wparam); - } - } else if (message == WM_SHOWWINDOW) { - if (wparam == 0) { - data->animState = 0; - } - } else if (message == OD_SETIMAGE) { - result = (LRESULT)data->image; - data->image = (Surface *)lparam; - goto cleanup; - } else if (message == OD_SETALTIMAGE) { - result = (LRESULT)data->altImage; - data->altImage = (Surface *)lparam; - goto cleanup; - } else if (message == OD_TOOLTIPS) { - result = data->toolTipsEnabled; - data->toolTipsEnabled = lparam; - goto cleanup; - } - - if (data->toolTipsEnabled) { - - if (message == WM_TIMER || (message >= WM_MOUSEMOVE && message <= WM_MOUSELAST)) { - Rect corner; - GetWindowRect(window, (LPRECT)&corner); - if (WindowFromPoint(*(POINT *)&corner) == window) { - mouse_over = true; - } - } - - if (data->toolTipsEnabled && mouse_over) { - - if (message == WM_MOUSEMOVE) { - int mx = LOWORD(lparam); - int my = HIWORD(lparam); - - if (specific_proc == ListBoxCtrlProc) { - if (OwnerDraw::End_Tooltip()) { - ReleaseCapture(); - } - KillTimer(window, 0); - } - - if (mx >= 0 && my >= 0 && mx < client_rect.right && my < client_rect.bottom) { - if ((wparam & 0x13) == 0) { - UINT delay = 1000; - if (time(NULL) - ODLastTooltipTime <= 1) { - delay = 300; - } - SetTimer(window, 0, delay, NULL); - } - } else { - if (OwnerDraw::End_Tooltip()) { - ReleaseCapture(); - } - KillTimer(window, 0); - } - - } else { - if (message >= WM_MOUSEMOVE && message <= WM_MBUTTONDBLCLK) { - if (OwnerDraw::End_Tooltip()) { - ReleaseCapture(); - } - KillTimer(window, 0); - } - - if (message == WM_TIMER) { - POINT pt; - Get_Logical_Cursor_Pos(window, pt); - int mx = pt.x; - int my = pt.y; - - BOOL inside = FALSE; - if (pt.x >= 0 && pt.y >= 0 && pt.x < client_rect.right && pt.y < client_rect.bottom) { - inside = TRUE; - } - - CHAR buf[128]; - memset(buf, 0, sizeof(buf)); - if (inside) { - WPARAM ctrl_id = GetWindowLong(window, GWL_ID); - HWND parent = GetParent(window); - SendMessage(parent, OD_GETTIPTEXT, ctrl_id, (LPARAM)buf); - if (strlen(buf) == 0) { - SendMessage(window, OD_GETCELLTIP, MAKELONG(mx, my), (LPARAM)buf); - } - - if (strcmp(buf, ODTooltip.text) != 0 && ODTooltip.isActive) { - if (OwnerDraw::End_Tooltip()) { - ReleaseCapture(); - } - } - - if (!OwnerDraw::Show_Tooltip(false)) { - if (strlen(buf)) { - HDC hdc = GetDC(window); - HFONT font = WS_Get_Font(hdc, ODFontName, 0, ODFontSize, 0); - if (font != NULL) { - SelectObject(hdc, font); - } - SetTextColor(hdc, ODColorText); - SetBkMode(hdc, TRANSPARENT); - SIZE size; - GetTextExtentPoint32(hdc, buf, strlen(buf), &size); - ReleaseDC(window, hdc); - - POINT cursor; - Get_Logical_Cursor_Pos(NULL, cursor); - Rect tooltip_rect; - tooltip_rect.X = cursor.x; - tooltip_rect.Y = cursor.y + 16; - tooltip_rect.Width = size.cx + 8; - tooltip_rect.Height = size.cy + 6; - - Rect mainclient = VisibleSurface->Get_Rect(); - if (tooltip_rect.Width + tooltip_rect.X >= mainclient.Width) { - tooltip_rect.X = mainclient.Width - tooltip_rect.Width; - } - if (tooltip_rect.Height + tooltip_rect.Y >= mainclient.Height) { - tooltip_rect.Y = mainclient.Height - tooltip_rect.Height; - } - - SetCapture(window); - OwnerDraw::Start_Tooltip(tooltip_rect, buf, window); - } - } else { - SetCapture(window); - } - } - } - } - } - } - - if (is_paint) { - - /* - * Paint path: when painting is disabled, just tell the control to repaint - * its frame. - */ - if (data->paintDisabled) { - ValidateRect(window, NULL); - result = CallWindowProc(specific_proc, window, OD_REFRESHNOPAINT, wparam, lparam); - } else { - - /* - * If this paint overlaps the tooltip, hide the tooltip first and re-show it - * once painting is finished. - */ - { - RECT tooltip_rect; - tooltip_rect.left = ODTooltip.bounds.X; - tooltip_rect.right = ODTooltip.bounds.Width + ODTooltip.bounds.X + 1; - tooltip_rect.top = ODTooltip.bounds.Y; - tooltip_rect.bottom = ODTooltip.bounds.Height + ODTooltip.bounds.Y + 1; - if (num_rect_updates == 1) { - RECT intersect; - if (IntersectRect(&intersect, &disp_rect, &tooltip_rect)) { - if (ODTooltip.isActive && ODTooltip.isHidden != 1 && ODTooltip.background != NULL) { - Rect drect = ODTooltip.bounds; - Rect srect(0, 0, ODTooltip.bounds.Width, ODTooltip.bounds.Height); - VisibleSurface->Blit_From(drect, *ODTooltip.background, srect); - ODTooltip.isHidden = true; - show_tooltip = true; - } - } - } - } - - if (min_update_rect.x >= disp_rect.left) { - min_update_rect.x = disp_rect.left; - } - if (min_update_rect.y >= disp_rect.top) { - min_update_rect.y = disp_rect.top; - } - if (max_update_rect.x <= disp_rect.right) { - max_update_rect.x = disp_rect.right; - } - if (max_update_rect.y <= disp_rect.bottom) { - max_update_rect.y = disp_rect.bottom; - } - - /* - * Walk up to the owning dialog window (the first ancestor that draws its own - * background) and read its animation state. - */ - owner = window; - while (owner != NULL) { - if (GetWindowLongPtr(owner, DWLP_DLGPROC) != 0) { - break; - } - owner = GetParent(owner); - } - - ownerdata = NULL; - if (owner != NULL) { - ODWinData.getPointer(owner, &ownerdata); - } - - if (owner == window) { - if (ownerdata->animState < 1) { - state = 1; - anim_state = 1; - goto call_proc_paint; - } - state = ownerdata->animState; - } else { - if (ownerdata == NULL) { - state = anim_state; - ValidateRect(window, NULL); - result = 1; - goto finalize_paint; - } - state = ownerdata->animState; - } - - anim_state = state; - if (state < 1) { - ValidateRect(window, NULL); - result = 1; - } else { - - call_proc_paint: - result = CallWindowProc(specific_proc, window, message, wparam, lparam); - if (ownerdata != NULL) { - ownerdata->animState = state; - } - - { - ArrayList children; - EnumChildWindows(window, (WNDENUMPROC)ODAddWindowToList, (LPARAM)&children); - - HWND combo_owner = NULL; - HWND child = NULL; - for (int index = 0; index < children.length(); index++) { - children.get(child, index); - - WNDPROC childproc = NULL; - OriginalWndProcs.getValue(child, childproc); - - if (childproc != (WNDPROC)ComboDropWinCtrlProc) { - InvalidateRect(child, NULL, FALSE); - UpdateWindow(child); - } else { - combo_owner = child; - } - } - - if (combo_owner != NULL) { - InvalidateRect(combo_owner, NULL, FALSE); - UpdateWindow(combo_owner); - } else if (_dropdown_window != NULL) { - if (_dropdown_owner == owner) { - Rect drop_rect; - Get_Display_Rect(_dropdown_window, (LPRECT)&drop_rect); - RECT intersect; - if (IntersectRect(&intersect, &disp_rect, (const RECT *)&drop_rect)) { - InvalidateRect(_dropdown_window, NULL, FALSE); - UpdateWindow(_dropdown_window); - } - } - } - } - - state = anim_state; - } - - finalize_paint: - if (state > 0) { - HWND sibling = owner; - while (window == owner || num_rect_updates == 1) { - if (sibling == NULL) { - break; - } - sibling = GetWindow(sibling, GW_HWNDPREV); - if (sibling == NULL) { - break; - } - if (GetWindowLongPtr(sibling, DWLP_DLGPROC)) { - Rect sibling_rect; - Get_Display_Rect(sibling, (LPRECT)&sibling_rect); - RECT intersect; - if (IntersectRect(&intersect, &disp_rect, (const RECT *)&sibling_rect)) { - InvalidateRect(sibling, NULL, FALSE); - UpdateWindow(sibling); - break; - } - } - } - } - } - goto after_proc; - } else { - goto call_custom_proc; - } - -cleanup: - ctrlmessages.remove(key); - - if (is_paint) { - if (GetWindowLongPtr(window, DWLP_DLGPROC)) { - if (num_rect_updates > 1) { - data->animState = 2; - } - } - - if (--num_rect_updates == 0) { - if (!data->paintDisabled && anim_state >= 1) { - - Rect rect2; - Rect screen_rect; - rect2.X = min_update_rect.x; - rect2.Y = min_update_rect.y; - rect2.Width = max_update_rect.x - min_update_rect.x; - rect2.Height = max_update_rect.y - min_update_rect.y; - screen_rect.X = min_update_rect.x; - screen_rect.Y = min_update_rect.y; - screen_rect.Width = max_update_rect.x - min_update_rect.x; - screen_rect.Height = max_update_rect.y - min_update_rect.y; - - if (GetWindowLongPtr(window, DWLP_DLGPROC) && data->animState == 1) { - - /* - * Animated dialog reveal -- the screen wipes open from the - * center outward with sliding "leftbar"/"rightbar" edges. - */ - if (Options.SoundVolume > 0.0) { - AudioEngine.Play_Sample(MixFileClass::Retrieve("EMBLEM.AUD"), AUDIO_GROUP_SFX, 64.0f / 255.0f, 255); - } - - struct _timeb start_time; - _ftime(&start_time); - - int half = rect2.Width / 2; - int frame = 0; - int center = rect2.Width / 2 + rect2.X; - - Surface *leftbar = SurfaceCache.GetSurface("leftbar.pcx", 0); - Surface *rightbar = SurfaceCache.GetSurface("rightbar.pcx", 0); - - Rect barsrc(0, 0, leftbar->Get_Width(), leftbar->Get_Height()); - Rect bardst = barsrc; - - int bar_width = leftbar->Get_Width(); - int bar_height = leftbar->Get_Height(); - - int step = 0; - int counter = 0; - int left_x = center - 12; - - while (step < half) { - { - int advance = bar_width; - if (bar_width >= step) { - advance = step; - } - - /* - * Reveal the next slice on the left of the wipe. The slice - * is one stride (12 pixels) wider than the bar so that it - * completely covers the bar stamped by the previous frame. - */ - int reveal_x = left_x; - rect2.X = left_x; - int width_cache = advance + 12; - rect2.Width = advance + 12; - int reveal_w = advance + 12; - if (left_x < min_update_rect.x) { - reveal_x = min_update_rect.x; - rect2.X = min_update_rect.x; - reveal_w = reveal_w + left_x - min_update_rect.x; - rect2.Width = reveal_w; - } - screen_rect.Width = reveal_w; - screen_rect.Height = rect2.Height; - screen_rect.X = offset_x + reveal_x; - screen_rect.Y = offset_y + rect2.Y; - VisibleSurface->Lock(); - AlternateSurface->Lock(); - VisibleSurface->Blit_From(screen_rect, *AlternateSurface, rect2); - AlternateSurface->Unlock(); - VisibleSurface->Unlock(); - - for (int y = 0; y < rect2.Height; y += bar_height) { - int bar_x = rect2.X - bar_width; - if (bar_x < rect2.X) { - bar_x = rect2.X; - } - bardst.Y = y + rect2.Y; - if (bar_height + y >= rect2.Height) { - int clip = rect2.Height - bar_height - y; - barsrc.Height += clip; - bardst.Height += clip; - } - screen_rect = bardst; - screen_rect.Y += offset_y; - screen_rect.X = bar_x; - screen_rect.X += offset_x; - VisibleSurface->Blit_From(screen_rect, *leftbar, barsrc); - barsrc.Height = bar_height; - bardst.Height = bar_height; - } - - /* - * Reveal the matching slice on the right of the wipe. - */ - int right_dst = center + step - advance; - rect2.Width = width_cache; - rect2.X = right_dst; - if (width_cache + right_dst >= max_update_rect.x) { - rect2.Width = max_update_rect.x - right_dst; - } - screen_rect.X = offset_x + rect2.X; - screen_rect.Y = offset_y + rect2.Y; - screen_rect.Width = rect2.Width; - screen_rect.Height = rect2.Height; - VisibleSurface->Lock(); - AlternateSurface->Lock(); - VisibleSurface->Blit_From(screen_rect, *AlternateSurface, rect2); - VisibleSurface->Unlock(); - AlternateSurface->Unlock(); - - for (int ry = 0; ry < rect2.Height; ry += bar_height) { - int bar_x = rect2.X + rect2.Width + bar_width; - if (bar_x > rect2.X + rect2.Width - bar_width) { - bar_x = rect2.X + rect2.Width - bar_width; - } - bardst.Y = ry + rect2.Y; - if (ry + bar_height >= rect2.Height) { - int clip = rect2.Height - ry - bar_height; - bardst.Height += clip; - barsrc.Height += clip; - } - screen_rect = bardst; - screen_rect.Y += offset_y; - screen_rect.X = bar_x; - screen_rect.X += offset_x; - VisibleSurface->Blit_From(screen_rect, *rightbar, barsrc); - barsrc.Height = bar_height; - bardst.Height = bar_height; - } - - struct _timeb now; - _ftime(&now); - frame++; - int wait = start_time.millitm + frame * (40 - counter / half) + 1000 * (start_time.time - now.time) - now.millitm; - if (wait > 0) { - Sleep(wait); - } - - if (AudioEngine.Is_Available() && GameInFocus == true) { - AudioEngine.Sound_Callback(); - Theme.AI(); - Speak_AI(); - } - - /* - * The whole animation runs inside one paint, so each step - * has to reach the screen from here. - */ - Video_Present_If_Dirty(); - - Sleep(0); - - step += 12; - counter += 240; - left_x -= 12; - }; - } - - data->animState = 2; - - Rect whole_rect; - whole_rect.X = min_update_rect.x; - whole_rect.Y = min_update_rect.y; - whole_rect.Width = max_update_rect.x - min_update_rect.x; - whole_rect.Height = max_update_rect.y - min_update_rect.y; - screen_rect.X = offset_x + min_update_rect.x; - screen_rect.Y = offset_y + min_update_rect.y; - screen_rect.Width = max_update_rect.x - min_update_rect.x; - screen_rect.Height = max_update_rect.y - min_update_rect.y; - VisibleSurface->Lock(); - AlternateSurface->Lock(); - VisibleSurface->Blit_From(screen_rect, *AlternateSurface, whole_rect); - AlternateSurface->Unlock(); - VisibleSurface->Unlock(); - - ArrayList children; - EnumChildWindows(window, (WNDENUMPROC)ODAddWindowToList, (LPARAM)&children); - HWND child = NULL; - for (int index = 0; index < children.length(); index++) { - children.get(child, index); - SendMessage(child, OD_ACTIVATE, 0, 0); - } - - } else { - - /* - * Unanimated update -- copy the dirty rectangle straight from - * the back buffer to the screen. - */ - data->animState = 2; - screen_rect.Width = rect2.Width; - screen_rect.Height = rect2.Height; - screen_rect.Y = offset_y + rect2.Y; - screen_rect.X = offset_x + rect2.X; - VisibleSurface->Lock(); - AlternateSurface->Lock(); - VisibleSurface->Blit_From(screen_rect, *AlternateSurface, rect2); - AlternateSurface->Unlock(); - VisibleSurface->Unlock(); - } - } - - min_update_rect.x = 0xFFFFFF; - min_update_rect.y = 0xFFFFFF; - max_update_rect.x = 0; - max_update_rect.y = 0; - } - } - - if (show_tooltip) { - OwnerDraw::Show_Tooltip(true); - } - - if (message == WM_INITDIALOG) { - result = 0; - } - return(result); -} - - -/// -/// Handles the messages for a control with no owner-draw procedure of its own. -/// This is the fallback custom procedure InitializeCtrl hands to any control class the -/// owner-draw system does not paint. Only the edit coloring message is claimed, so that a -/// child edit box picks up the dialog's own font and text color. -/// -/// Returns with a null background brush for the edit coloring message; otherwise -/// with the result of the original window procedure. -LRESULT CALLBACK DefaultCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - WNDPROC proc = NULL; - - OriginalWndProcs.getValue(window, proc); - - RECT rect1; - Get_Display_Rect(window, &rect1); - - RECT rect2; - GetClientRect(window, &rect2); - - WinData *data = NULL; - ODWinData.getPointer(window, &data); - - if (message == WM_CTLCOLOREDIT) { - HDC hdc = (HDC)wparam; - HWND ctrl = (HWND)lparam; - SetTextColor(hdc, ODColorText); - SetBkMode(hdc, TRANSPARENT); - - HFONT font = WS_Get_Font(hdc, ODFontName, 0, ODFontSize, 0); - if (font != NULL) { - SendMessage(ctrl, WM_SETFONT, (WPARAM)font, 0); - } - - return((LRESULT)GetStockObject(NULL_BRUSH)); - } - - return(CallWindowProc(proc, window, message, wparam, lparam)); -} - - -/// -/// Handles the messages for an owner-drawn push button. -/// The button is painted either from an image supplied by the dialog or from the button -/// artwork fitted to the control, with the caption drawn over it and the whole control -/// dimmed while it is disabled. The click sound is played as the button goes down. -/// -/// Returns with the result of the original window procedure for anything this -/// routine does not handle itself. -LRESULT CALLBACK ButtonCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - WNDPROC proc = NULL; - OriginalWndProcs.getValue(window, proc); - - RECT drect; - memset(&drect, 0, sizeof(drect)); - Get_Display_Rect(window, &drect); - - Rect origin(drect.left, drect.top, drect.right - drect.left, drect.bottom - drect.top); - - RECT crect; - memset(&crect, 0, sizeof(crect)); - GetClientRect(window, &crect); - - WinData * data = NULL; - ODWinData.getPointer(window, &data); - - int state = data->Button.state; - LONG style = GetWindowLong(window, GWL_STYLE); - - switch (message) { - - case WM_PAINT: { - Rect rect = origin; - COLORREF color = ODColorText; - - /* - * Lazily build the render-cache surface that holds the dimmed - * background captured behind the button. - */ - if (data->cachedSurface == NULL) { - BSurface * surface = new BSurface(crect.right + 1, crect.bottom + 1, 2); - data->cachedSurface = surface; - _surface_count++; - - Rect dest; - Rect src; - dest.X = 0; - dest.Y = 0; - dest.Width = crect.right + 1; - dest.Height = crect.bottom + 1; - src.X = drect.left; - src.Y = drect.top; - src.Width = crect.right + 1; - src.Height = crect.bottom + 1; - - surface->Blit_From(dest, *AlternateSurface, src); - } - - static char _prev_state = 'u'; - - if (data->image != NULL) { - - /* - * A user image was supplied -- blit it directly, choosing the - * pressed variant when the button is down. - */ - Surface * image = data->image; - if ((state & 1) && data->altImage != NULL) { - image = data->altImage; - } - - Rect src = rect; - src.X = 0; - src.Y = 0; - AlternateSurface->Blit_From(rect, *image, src); - - } else { - - /* - * No image -- draw the button from its skin pieces and play the - * click sound when it first goes down. - */ - char updown = 'u'; - if (state & 1) { - updown = 'd'; - } - if (style & WS_DISABLED) { - updown = 'u'; - } else if (updown == 'd' && _prev_state == 'u') { - Sound_Effect(Rule->GenericClick); - } - - int widths[2] = {7, 7}; - _prev_state = updown; - int margins[2] = {10, 10}; - int heights[2] = {24, 30}; - - int index = 0; - for (unsigned int i = 0; i < 2; i++) { - if (heights[i] > rect.Height && i != 0) { - break; - } - index = i; - } - - int height = heights[index]; - int w = widths[index]; - int margin = margins[index]; - - /* - * Restore the cached background before drawing the skin. - */ - if (data->cachedSurface != NULL) { - AlternateSurface->Blit_From( - Rect(drect.left, drect.top, crect.right + 1, crect.bottom + 1), - *data->cachedSurface, - Rect(0, 0, crect.right + 1, crect.bottom + 1)); - InvalidateRect(window, NULL, FALSE); - } - - origin.Y += (rect.Height - height) / 2; - if (state & 1) { - origin.Y += 2; - } - - Rect destrect; - Rect sourcerect; - char buffer[40]; - - sprintf(buffer, "b%c%c_li%d.pcx", updown, 'e', height); - Surface * left = SurfaceCache.GetSurface(buffer); - origin.Height = left->Get_Height(); - destrect = origin; - destrect.Width = w; - sourcerect.Width = w; - sourcerect.Y = 0; - sourcerect.X = 0; - destrect.Height = height; - sourcerect.Height = height; - AlternateSurface->Blit_From(destrect, *left, sourcerect); - - sprintf(buffer, "b%c%c_mi%d.pcx", updown, 'e', height); - Surface * mid = SurfaceCache.GetSurface(buffer); - sourcerect = origin; - sourcerect.X += w; - sourcerect.Width -= margin; - sourcerect.Height = mid->Get_Height(); - SurfaceCache.Draw(sourcerect, *AlternateSurface, *mid, 0, 0); - - sprintf(buffer, "b%c%c_ri%d.pcx", updown, 'e', height); - Surface * right = SurfaceCache.GetSurface(buffer); - destrect = origin; - destrect.X += origin.Width - margin; - destrect.Width = margin; - destrect.Height = right->Get_Height(); - sourcerect.Height = destrect.Height; - sourcerect.Width = destrect.Width; - sourcerect.Y = 0; - sourcerect.X = 0; - AlternateSurface->Blit_From(destrect, *right, sourcerect); - } - - /* - * Render the caption text (no user image case only). - */ - if (data->image == NULL) { - RECT client; - GetClientRect(window, &client); - static char buffer2[256]; - GetWindowText(window, buffer2, 256); - - Rect text_rect( - origin.X, - origin.Y + 1, - origin.Width + origin.X - 2, - origin.Height + origin.Y - 2); - if (state & 1) { - text_rect.X += 2; - text_rect.Y += 4; - } - OD_Draw_Text_Remap(*AlternateSurface, buffer2, text_rect, "dlgsys", color, 5, 0); - } - - if (style & WS_DISABLED) { - ODFillRectTrans(rect, *AlternateSurface, 0, 128); - } - ValidateRect(window, NULL); - } - /// Fall through. - - case WM_ACTIVATE: - case WM_KILLFOCUS: - case WM_MOUSEACTIVATE: - return(0); - - default: - return(CallWindowProc(proc, window, message, wparam, lparam)); - } -} - - -/// -/// Handles the messages for an owner-drawn tab control. -/// The body of the control is painted as dimmed dialog background and each tab is built -/// from its corner and middle artwork, with the caption drawn over it in the remapped -/// font. The active tab gets the brighter text color. -/// -/// Returns with the result of the original window procedure for anything this -/// routine does not paint itself. -LRESULT CALLBACK TextBoxCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - static char string1[64]; - - WinData * data = NULL; - ODWinData.getPointer(window, &data); - - WNDPROC proc = NULL; - OriginalWndProcs.getValue(window, proc); - - LRESULT result = 0; - - if (message == WM_ERASEBKGND) { - return(1); - } - - if (message == WM_NCPAINT) { - return(0); - } - - if (message == OD_SUBCLASSED) { - RECT rect; - Get_Display_Rect(window, &rect); - - Surface * surf = SurfaceCache.GetSurface("tab_tlu.pcx"); - SendMessage(window, TCM_SETITEMSIZE, 0, MAKELPARAM(89, surf->Get_Height() - 1)); - - if (proc) { - return(CallWindowProc(proc, window, message, wparam, lparam)); - } - return(result); - } - - if (message == WM_PAINT) { - RECT winrect; - Get_Display_Rect(window, &winrect); - - TabCtrl_GetItemCount(window); - - RECT tabrect; - TabCtrl_GetItemRect(window, 0, &tabrect); - TabCtrl_GetItemRect(window, 0, &tabrect); - - int y = winrect.top + tabrect.bottom - tabrect.top + 3; - - Rect dimrect; - dimrect.X = winrect.left; - dimrect.Y = y; - dimrect.Width = winrect.right - winrect.left; - dimrect.Height = winrect.bottom - y; - - winrect.top = y; - - ODDrawDimmedBackground(dimrect, window); - ValidateRect(window, NULL); - - /* - * Refilled with the full display rect, although nothing reads it before - * the tab loop overwrites it again. - */ - dimrect.X = winrect.left; - dimrect.Y = winrect.top; - dimrect.Width = winrect.right - winrect.left; - dimrect.Height = winrect.bottom - winrect.top; - - Rect rect; - Rect src; - - Surface * image = SurfaceCache.GetSurface("tab_fml.pcx"); - if (image) { - image->Get_Height(); - rect.Width = image->Get_Width(); - rect.X = winrect.left; - rect.Y = winrect.top; - rect.Height = winrect.bottom - winrect.top; - SurfaceCache.Draw(rect, *AlternateSurface, *image, 0, 0); - } - - image = SurfaceCache.GetSurface("tab_fmr.pcx"); - if (image) { - image->Get_Height(); - rect.Width = image->Get_Width(); - rect.X = winrect.right - rect.Width; - rect.Y = winrect.top; - rect.Height = winrect.bottom - winrect.top; - SurfaceCache.Draw(rect, *AlternateSurface, *image, 0, 0); - } - - image = SurfaceCache.GetSurface("tab_ftm.pcx"); - if (image) { - int height = image->Get_Height(); - image->Get_Width(); - rect.X = winrect.left; - rect.Y = winrect.top; - rect.Width = winrect.right - winrect.left; - rect.Height = height; - SurfaceCache.Draw(rect, *AlternateSurface, *image, 0, 0); - } - - image = SurfaceCache.GetSurface("tab_fbm.pcx"); - if (image) { - int height = image->Get_Height(); - image->Get_Width(); - rect.X = winrect.left; - rect.Y = winrect.bottom - height; - rect.Width = winrect.right - winrect.left; - rect.Height = height; - SurfaceCache.Draw(rect, *AlternateSurface, *image, 0, 0); - } - - image = SurfaceCache.GetSurface("tab_ftl.pcx"); - if (image) { - int height = image->Get_Height(); - int width = image->Get_Width(); - rect.Y = winrect.top; - rect.X = winrect.left; - src.X = 0; - src.Y = 0; - src.Width = width; - src.Height = height; - rect.Width = width; - rect.Height = height; - AlternateSurface->Blit_From(rect, *image, src); - } - - image = SurfaceCache.GetSurface("tab_ftr.pcx"); - if (image) { - int height = image->Get_Height(); - int width = image->Get_Width(); - rect.Y = winrect.top; - rect.X = winrect.right - width; - src.X = 0; - src.Y = 0; - src.Width = width; - src.Height = height; - rect.Width = width; - rect.Height = height; - AlternateSurface->Blit_From(rect, *image, src); - } - - image = SurfaceCache.GetSurface("tab_fbl.pcx"); - if (image) { - int height = image->Get_Height(); - int width = image->Get_Width(); - rect.X = winrect.left; - rect.Y = winrect.bottom - height; - src.X = 0; - src.Y = 0; - src.Width = width; - src.Height = height; - rect.Width = width; - rect.Height = height; - AlternateSurface->Blit_From(rect, *image, src); - } - - image = SurfaceCache.GetSurface("tab_fbr.pcx"); - if (image) { - int height = image->Get_Height(); - int width = image->Get_Width(); - rect.Y = winrect.bottom - height; - rect.X = winrect.right - width; - src.X = 0; - src.Y = 0; - src.Width = width; - src.Height = height; - rect.Width = width; - rect.Height = height; - AlternateSurface->Blit_From(rect, *image, src); - } - - int current = TabCtrl_GetCurSel(window); - Get_Display_Rect(window, &winrect); - int tab = current + 1; - int itab = tab; - - while (true) { - RECT itemrect; - - while (!TabCtrl_GetItemRect(window, tab, &itemrect)) { - itab = 0; - tab = 0; - } - - TC_ITEM item; - memset(&item, 0, sizeof(item)); - item.pszText = string1; - item.cchTextMax = sizeof(string1); - strcpy(item.pszText, "Title"); - item.mask = TCIF_TEXT; - TabCtrl_GetItem(window, tab, &item); - - char state = 'd'; - if (tab == current) { - state = 'u'; - } - - LONG left = itemrect.left; - if (itemrect.left >= 6) { - left = 6; - } - - Rect tab_rect; - tab_rect.X = winrect.left + itemrect.left - left; - tab_rect.Y = itemrect.top + winrect.top; - - LONG right = itemrect.left; - if (itemrect.left >= 6) { - right = 6; - } - tab_rect.Width = itemrect.right + right - itemrect.left; - tab_rect.Height = itemrect.bottom - itemrect.top; - - char fname[64]; - Surface * tab_lu = SurfaceCache.GetSurface("tab_tlu.pcx"); - sprintf(fname, "tab_tm%c.pcx", state); - image = SurfaceCache.GetSurface(fname); - if (image) { - int height = image->Get_Height(); - image->Get_Width(); - int width = tab_rect.Width - 2 * tab_lu->Get_Width(); - - rect.X = tab_rect.X + tab_lu->Get_Width(); - rect.Y = tab_rect.Y; - rect.Width = width; - rect.Height = height; - SurfaceCache.Draw(rect, *AlternateSurface, *image, 0, 0); - } - - sprintf(fname, "tab_tl%c.pcx", state); - image = SurfaceCache.GetSurface(fname); - if (image) { - int height = image->Get_Height(); - int width = image->Get_Width(); - rect.X = tab_rect.X; - rect.Y = tab_rect.Y; - rect.Width = width; - rect.Height = height; - SurfaceCache.DrawTrans(rect, *AlternateSurface, *image, (255 >> DSurface::RedLeft << DSurface::RedRight) | (255u >> DSurface::BlueLeft << DSurface::BlueRight)); - } - - sprintf(fname, "tab_tr%c.pcx", state); - image = SurfaceCache.GetSurface(fname); - if (image) { - int height = image->Get_Height(); - Rect trrect; - trrect.Width = image->Get_Width(); - trrect.X = tab_rect.X + tab_rect.Width - trrect.Width; - trrect.Y = tab_rect.Y; - trrect.Height = height; - SurfaceCache.DrawTrans(trrect, *AlternateSurface, *image, (255 >> DSurface::RedLeft << DSurface::RedRight) | (255u >> DSurface::BlueLeft << DSurface::BlueRight)); - } - - if (item.pszText) { - strcpy(string1, item.pszText); - } - - Rect text_rect; - text_rect.X = tab_rect.X; - text_rect.Width = tab_rect.X + tab_rect.Width; - text_rect.Y = tab_rect.Y + 6; - text_rect.Height = tab_rect.Y + tab_rect.Height; - - COLORREF color = ODColorTextDim; - if (itab == current) { - color = ODColorText; - } - OD_Draw_Text_Remap(*AlternateSurface, string1, text_rect, "dlgsys", color, 5, 0); - - ValidateRect(window, &itemrect); - if (itab == current) { - break; - } - - itab++; - tab = itab; - } - - ValidateRect(window, NULL); - return(result); - } - - if (proc) { - return(CallWindowProc(proc, window, message, wparam, lparam)); - } - return(result); -} - - -/// -/// Handles the messages for an owner-drawn edit control. -/// The control is inset inside its border and held out of the dialog's tab order until it -/// is deliberately activated, so that a stray keystroke cannot land in it. Return and tab -/// are intercepted -- return notifies the parent dialog, tab moves on to the next control. -/// -/// Returns with the result of the original window procedure for anything this -/// routine does not handle itself. -LRESULT CALLBACK EditBoxCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - WNDPROC proc = NULL; - OriginalWndProcs.getValue(window, proc); - - WinData * data = NULL; - ODWinData.getPointer(window, &data); - - if (GetFocus() == window && !data->Edit.focusEnabled) { - data->Edit.focusPending = 1; - SetFocus(MainWindow); - } - - LONG style = GetWindowLong(window, GWL_STYLE); - char class_name[32]; - - if (message != WM_KEYUP && message != WM_KEYDOWN || wparam != VK_TAB) { - - if (message == OD_SUBCLASSED) { - RECT window_rect; - GetWindowRect(window, &window_rect); - - RECT client_rect; - GetClientRect(window, &client_rect); - - RECT parent_rect; - GetWindowRect(GetParent(window), &parent_rect); - - MoveWindow(window, window_rect.left - parent_rect.left + 1, window_rect.top - parent_rect.top + 1, client_rect.right - 2, client_rect.bottom - 2, FALSE); - - if (GetFocus() == window) { - data->Edit.focusPending = 1; - SetFocus(MainWindow); - } - - if (style & WS_TABSTOP) { - data->Edit.hadTabStop = 1; - SetWindowLong(window, GWL_STYLE, style & ~WS_TABSTOP); - } - } - - else if (message == WM_SETFOCUS) { - SendMessage(window, EM_SETSEL, (WPARAM)-1, (LPARAM)-1); - if (!data->Edit.focusEnabled) { - PostMessage(window, OD_REFOCUS, 0, 0); - } - goto invalidate_and_default; - } - - else if (message == WM_GETTEXT) { - LRESULT text_len = CallWindowProc(proc, window, WM_GETTEXT, wparam, lparam); - char * buffer = new char[text_len + 2]; - memset(buffer, 0, text_len + 2); - - int out_len = 0; - int i = 0; - for (; i < text_len; ++i) { - char ch = ((char *)lparam)[i]; - if (ch != '\r' && ch != '\n') { - buffer[out_len++] = ch; - } - } - - if (i != out_len) { - strcat(buffer, "\r\n"); - out_len += 2; - } - - strcpy((char *)lparam, buffer); - delete[] buffer; - return(out_len); - } - - else if (message == WM_CHAR) { - if (wparam == VK_RETURN) { - if (style & ES_MULTILINE) { - WPARAM len = SendMessage(window, WM_GETTEXTLENGTH, 0, 0) + 3; - char * text = new char[len]; - SendMessage(window, WM_GETTEXT, len, (LPARAM)text); - strcat(text, "\r\n"); - SendMessage(window, WM_SETTEXT, 0, (LPARAM)text); - delete[] text; - - SendMessage(GetParent(window), WM_COMMAND, (unsigned short)GetWindowLong(window, GWL_ID) | 0x5010000, (LPARAM)window); - return(0); - } - } else if (wparam == VK_TAB) { - HWND next = window; - HWND tab_item = GetNextDlgTabItem(GetParent(window), window, FALSE); - if (tab_item != NULL) { - next = tab_item; - } - SetFocus(next); - return(0); - } - goto call_default; - } - - else if (message == OD_ACTIVATE) { - - int focus_state = data->Edit.focusPending; - data->Edit.focusEnabled = 1; - if (focus_state) { - SetFocus(window); - data->Edit.focusPending = 0; - } - if (data->Edit.hadTabStop) { - SetWindowLong(window, GWL_STYLE, style | WS_TABSTOP); - } - } - - else { - if (message == WM_PAINT || message == WM_ERASEBKGND) { - RECT display_rect; - Get_Display_Rect(window, &display_rect); - - GetClassName(GetParent(window), class_name, sizeof(class_name)/2); - bool is_combo = strcmp(class_name, "ComboBox") == 0; - - RECT update_rect; - if (message == WM_PAINT && GetUpdateRect(window, &update_rect, FALSE)) { - update_rect.right += display_rect.left; - update_rect.left += display_rect.left; - update_rect.top += display_rect.top; - update_rect.bottom += display_rect.top; - } - - Rect draw_rect; - draw_rect.X = display_rect.left; - draw_rect.Y = display_rect.top; - draw_rect.Width = display_rect.right - display_rect.left + 1; - draw_rect.Height = display_rect.bottom - display_rect.top + 1; - - ODDrawDimmedBackground(draw_rect, window); - if (!is_combo) { - OD_Draw_Rect(*AlternateSurface, draw_rect, 1, 0xFFFFFFFF); - } - - static char _buffer[512]; - SendMessage(window, WM_GETTEXT, 500, (LPARAM)_buffer); - - Rect text_rect; - text_rect.X = display_rect.left; - text_rect.Y = display_rect.top; - text_rect.Width = 0; - text_rect.Height = 0; - - if (GetWindowLong(window, GWL_STYLE) & ES_PASSWORD) { - for (int i = 0; i < (int)strlen(_buffer); ++i) { - _buffer[i] = '*'; - } - } - - OD_Draw_Text(ODColorText, ODFontPtr, text_rect, _buffer, strlen(_buffer), 0, 0, 0); - - WPARAM em_wparam; - LPARAM em_lparam; - unsigned int sel = ((unsigned int)SendMessage(window, EM_GETSEL, (WPARAM)&em_wparam, (LPARAM)&em_lparam)) >> 16; - if (HIWORD(sel) > LOWORD(sel)) { - sel >>= 16; - } - - int charidx = LOWORD(sel); - if (charidx < (int)strlen(_buffer)) { - SendMessage(window, EM_POSFROMCHAR, charidx, 0); - } - - ValidateRect(window, NULL); - } - - if (message == WM_CONTEXTMENU) { - return(1); - } - - if (message == WM_MOUSEMOVE) { - return(1); - } - - if (message == WM_KEYDOWN || message == WM_KEYUP || message == WM_SYSKEYDOWN || message == WM_SYSKEYUP || message == WM_SYSCHAR || message == WM_SYSDEADCHAR || message == WM_KILLFOCUS || message == WM_LBUTTONDOWN) { - invalidate_and_default: - GetClassName(GetParent(window), class_name, sizeof(class_name)); - if (strcmp(class_name, "ComboBox") == 0) { - InvalidateRect(GetParent(window), NULL, FALSE); - } - InvalidateRect(window, NULL, FALSE); - } - - call_default: - return(CallWindowProc(proc, window, message, wparam, lparam)); - } - } - return(0); -} - - -/// -/// Handles the messages for an owner-drawn static text control. -/// The caption is kept in the control's owner-draw record rather than in the window, so it -/// can be drawn in the remapped font over a cached copy of the dialog background. The -/// dialog can recolor the text at any time with OD_SETCOLOR. -/// -/// Returns with the result of the original window procedure for anything this -/// routine does not handle itself. -LRESULT CALLBACK StaticCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - WinData * data = NULL; - - switch (message) { - - case WM_SETTEXT: { - ODWinData.getPointer(window, &data); - - delete[] data->Static.text; - - const char * text = (const char *)lparam; - unsigned int text_len = strlen(text) + 1; - char * copy = new char[text_len]; - memset(copy, 0, text_len); - strcpy(copy, text); - - data->Static.text = copy; - - Surface * cachedSurf = data->cachedSurface; - if (cachedSurf != NULL) { - RECT drect; - Get_Display_Rect(window, &drect); - - RECT crect; - GetClientRect(window, &crect); - - Rect dst_rect; - dst_rect.X = drect.left; - dst_rect.Y = drect.top; - dst_rect.Width = crect.right + 1; - dst_rect.Height = crect.bottom + 1; - - Rect src_rect; - src_rect.X = 0; - src_rect.Y = 0; - src_rect.Width = crect.right + 1; - src_rect.Height = crect.bottom + 1; - - AlternateSurface->Blit_From(dst_rect, *cachedSurf, src_rect); - InvalidateRect(window, NULL, FALSE); - } - return(1); - } - - case WM_DESTROY: { - ODWinData.getPointer(window, &data); - - if (data->Static.text != NULL) { - delete[] data->Static.text; - data->Static.text = NULL; - } - if (data->cachedSurface != NULL) { - delete data->cachedSurface; - data->cachedSurface = NULL; - } - break; - } - - case WM_MOVE: - case WM_SIZE: - case WM_WINDOWPOSCHANGED: { - if (ODWinData.getPointer(window, &data)) { - if (data != NULL) { - delete data->cachedSurface; - data->cachedSurface = NULL; - } - } - InvalidateRect(window, NULL, FALSE); - return(0); - } - - case WM_GETTEXT: { - ODWinData.getPointer(window, &data); - - const char * text = data->Static.text; - if (strlen(text) + 1 < wparam) { - wparam = strlen(text) + 1; - } - strncpy((char *)lparam, text, wparam); - return(wparam); - } - - case WM_PAINT: { - char text[2048]; - text[2047] = '\0'; - - ODWinData.getPointer(window, &data); - - if (data->cachedSurface == NULL) { - RECT drect; - Get_Display_Rect(window, &drect); - - RECT crect; - GetClientRect(window, &crect); - - BSurface * surf = new BSurface(crect.right + 1, crect.bottom + 1, 2); - data->cachedSurface = surf; - ++_surface_count; - - Rect dst_rect; - dst_rect.X = 0; - dst_rect.Y = 0; - dst_rect.Width = crect.right + 1; - dst_rect.Height = crect.bottom + 1; - - Rect src_rect; - src_rect.X = drect.left; - src_rect.Y = drect.top; - src_rect.Width = crect.right + 1; - src_rect.Height = crect.bottom + 1; - - surf->Blit_From(dst_rect, *AlternateSurface, src_rect); - } - - Rect text_rect; - Get_Display_Rect(window, (LPRECT)&text_rect); - - GetWindowText(window, text, 2047); - - LONG style = GetWindowLong(window, GWL_STYLE); - int draw_flags = 16; - - if (style & SS_CENTER) { - draw_flags = 17; - } else if (style & SS_RIGHT) { - draw_flags = 18; - } - - COLORREF color = data->Static.textColor; - if ((style & WS_DISABLED) != 0) { - color = ODColorDisabled; - } - - OD_Draw_Text_Remap(*AlternateSurface, text, text_rect, "dlgsys", color, draw_flags, 0); - - ValidateRect(window, NULL); - return(0); - } - - case OD_SUBCLASSED: { - WNDPROC proc = NULL; - ODWinData.getPointer(window, &data); - OriginalWndProcs.getValue(window, proc); - - unsigned int len = SendMessage(window, WM_GETTEXTLENGTH, 0, 0) + 1; - char * text = new char[len]; - memset(text, 0, len); - - CallWindowProc(proc, window, WM_GETTEXT, len, (LPARAM)text); - - data->Static.text = text; - data->Static.textColor = ODColorText; - return(0); - } - - case OD_SETCOLOR: { - ODWinData.getPointer(window, &data); - - if (data != NULL) { - if ((COLORREF)lparam != data->Static.textColor) { - InvalidateRect(window, NULL, FALSE); - } - if (lparam == -1) { - data->Static.textColor = ODColorText; - } else { - data->Static.textColor = lparam; - } - } - return(0); - } - - default: - break; - } - - WNDPROC proc = NULL; - OriginalWndProcs.getValue(window, proc); - return(CallWindowProc(proc, window, message, wparam, lparam)); -} - - -/// -/// Handles the messages for an owner-drawn check box. -/// The checked state is kept in the control's owner-draw record and painted from the check -/// box artwork, dimmed when the control is disabled. A click on the box itself toggles the -/// state, plays the click sound and notifies the parent dialog. -/// -/// Returns with the result of the original window procedure for anything this -/// routine does not handle itself. -LRESULT CALLBACK CheckBoxCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - WinData* data = NULL; - ODWinData.getPointer(window, &data); - - switch (message) { - - case BM_GETCHECK: { - return(data->CheckBox.checkState); - } - - case WM_SETFOCUS: - case WM_KILLFOCUS: { - InvalidateRect(window, NULL, FALSE); - break; - } - - - case WM_PAINT: { - /* - * DEAD CODE - */ - RECT cr; - GetClientRect(window, &cr); - Rect trect; - Get_Display_Rect(window, (LPRECT)&trect); - int somebool = 0; - if (data->CheckBox.checkState == 1) { - somebool = 1; - } - RECT disprect; - Get_Display_Rect(window, (LPRECT)&disprect); - Rect drect; - drect.X = disprect.left; - drect.Y = disprect.top; - drect.Width = 18; - drect.Height = 18; - int style = GetWindowLong(window, GWL_STYLE); - char letter = 'u'; - if (somebool) { - letter = 'c'; - } - char buf[64]; - sprintf(buf, "c%ce_i.pcx", letter); - Surface *image = SurfaceCache.GetSurface(buf); - int image_height = image->Get_Height(); - Rect srect; - srect.Width = image->Get_Width(); - srect.X = 0; - srect.Y = 0; - srect.Height = image_height; - AlternateSurface->Blit_From(drect, *image, srect); - - if (style & WS_DISABLED) { - ODFillRectTrans(drect, *AlternateSurface, 0, 128); - } - - static char _wintext[128]; - GetWindowText(window, _wintext, sizeof(_wintext) - 1); - - trect.X += 20; - trect.Width -= 20; - COLORREF color = ODColorText; - if (style & WS_DISABLED) { - color = ODColorDisabled; - } - OD_Draw_Text_Remap(*AlternateSurface, _wintext, trect, "dlgsys", color, 4, 0); - ValidateRect(window, NULL); - return(0); - } - - case BM_SETCHECK: { - data->CheckBox.checkState = wparam; - InvalidateRect(window, NULL, FALSE); - return(0); - } - - case WM_LBUTTONDOWN: - case WM_LBUTTONDBLCLK: { - int xpos = (unsigned short)LOWORD(lparam); - int ypos = (unsigned short)HIWORD(lparam); - if (xpos < 18 && ypos < 18) { - int checked = data->CheckBox.checkState != 1; - data->CheckBox.checkState = checked; - InvalidateRect(window, NULL, FALSE); - Sound_Effect(Rule->GenericClick); - HWND parent = GetParent(window); - SendMessage(parent, WM_COMMAND, MAKEWPARAM(GetWindowLong(window, GWL_ID), checked), (LPARAM)window); - return(0); - } else { - return(0); - } - } - - case OD_SUBCLASSED: { - WNDPROC subproc = NULL; - OriginalWndProcs.getValue(window, subproc); - data->CheckBox.checkState = CallWindowProc(subproc, window, BM_GETCHECK, 0L, 0L); - break; - } - } - - WNDPROC proc = NULL; - OriginalWndProcs.getValue(window, proc); - return(CallWindowProc(proc, window, message, wparam, lparam)); -} - - -/// -/// Handles the messages for an owner-drawn combo box. -/// The closed box is painted with its arrow button and the text of the current selection. -/// A click on the arrow drops the list, which is a ComboDropWin window created and -/// destroyed here rather than the stock Windows list. -/// -/// Returns with the result of the original window procedure for anything this -/// routine does not handle itself. -LRESULT CALLBACK ComboBoxCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - static char _buffer[256]; - RECT display_rect; - RECT client_rect; - RECT window_rect; - Get_Display_Rect(window, &display_rect); - GetClientRect(window, &client_rect); - GetWindowRect(window, &window_rect); - - WinData * data = NULL; - ODWinData.getPointer(window, &data); - - switch (message) { - case WM_LBUTTONDOWN: - case WM_LBUTTONDBLCLK: { - Sound_Effect(Rule->GenericClick); - if ((unsigned short)LOWORD(lparam) > client_rect.right - 20) { - LRESULT dropped = SendMessage(window, CB_GETDROPPEDSTATE, 0, 0); - PostMessage(window, CB_SHOWDROPDOWN, (WPARAM)(dropped != 1), 0); - } - return(0); - } - - case WM_ERASEBKGND: - return(0); - - case WM_DESTROY: - SendMessage(window, CB_SHOWDROPDOWN, FALSE, 0); - break; - - case WM_PAINT: { - LRESULT dropped = SendMessage(window, CB_GETDROPPEDSTATE, 0, 0); - RECT dropped_rect; - RECT wrect; - GetWindowRect(window, &wrect); /// result is not used - SendMessage(window, CB_GETDROPPEDCONTROLRECT, 0, (LPARAM)&dropped_rect); - - Rect rect; - rect.X = display_rect.left; - rect.Y = display_rect.top; - rect.Width = display_rect.right - display_rect.left; - rect.Height = 24; - - dropped_rect.top += (display_rect.bottom - display_rect.top + 1); - dropped_rect.bottom = dropped_rect.bottom + display_rect.top - display_rect.bottom - 1; - - int width = display_rect.right - display_rect.left; - int height = display_rect.bottom - display_rect.top; - - HWND parent = GetParent(window); - WinData * parent_data = NULL; - if (parent) { - ODWinData.getPointer(parent, &parent_data); - } - - RECT parent_rect; - Get_Display_Rect(parent, &parent_rect); - - Rect dst_rect(0, 0, width, height); - Rect src_rect(0, 0, width, height); - if (parent_data != NULL && parent_data->cachedSurface != NULL) { - src_rect.X = display_rect.left - parent_rect.left; - src_rect.Y = display_rect.top - parent_rect.top; - } - - if (data->cachedSurface == NULL) { - Surface * surface = new BSurface(width, height, 2); - data->cachedSurface = surface; - _surface_count++; - - if (parent_data != NULL && parent_data->cachedSurface != NULL) { - surface->Blit_From(dst_rect, *parent_data->cachedSurface, src_rect, false, true); - } - - int total = width * height; - unsigned short * surfptr = (unsigned short *)surface->Lock(); - if (total > 0) { - for (int i = 0; i < total; ++i) { - surfptr[i] = OD_Blend_Color(surfptr[i], 0, ODColorSteps); - } - } - if (surfptr != NULL) { - surface->Unlock(); - } - } - - ODDrawDimmedBackground(rect, window); - ODFillRectTrans(rect, *AlternateSurface, 0, 128); - OD_Draw_Rect(*AlternateSurface, rect, 1, 0xFFFFFFFF); - - Rect arrow_rect; - arrow_rect.X = display_rect.right - 19; - arrow_rect.Y = rect.Y + 1; - arrow_rect.Width = rect.Width; - arrow_rect.Height = rect.Height; - ODDrawArrowBitmap(*AlternateSurface, arrow_rect, dropped, dropped); - - LONG style = GetWindowLong(window, GWL_STYLE); - if ((style & WS_DISABLED) != 0) { - ODFillRectTrans(rect, *AlternateSurface, 0, 128); - } - - if ((style & 3) == 3) { - sprintf(_buffer, "NULL"); - GetWindowText(window, _buffer, sizeof(_buffer)); - - COLORREF text_color = ODColorText; - if ((style & WS_DISABLED) != 0) { - text_color = ODColorDisabled; - } - - FontMetrics font_data; - if (ODGetFontMetrics("dlgsys", &font_data)) { - int text_width = 0; - int max_width = client_rect.right - 28; - int ellipsis_width = 3 * font_data.charWidths['.']; - bool clipped = false; - for (char const * cursor = _buffer; *cursor; ) { - text_width += font_data.charWidths[OD_Glyph(UTF8::Decode(cursor))]; - } - - if (text_width >= max_width) { - while (strlen(_buffer) > 0) { - char * last = UTF8::Previous(_buffer, _buffer + strlen(_buffer)); - text_width -= font_data.charWidths[OD_Glyph(UTF8::Peek(last))]; - *last = '\0'; - if (!clipped) { - text_width += ellipsis_width; - } - clipped = true; - if (text_width < max_width) { - strcat(_buffer, "..."); - break; - } - } - } - } - - RECT text_rect; - text_rect.left = display_rect.left + 2; - text_rect.right = display_rect.right; - text_rect.top = display_rect.top + 3; - text_rect.bottom = display_rect.bottom; - OD_Draw_Text_Remap(*AlternateSurface, _buffer, *(Rect *)&text_rect, "dlgsys", text_color, 4, 0); - ValidateRect(window, NULL); - return(0); - } - - ValidateRect(window, NULL); - return(0); - } - - case CB_SHOWDROPDOWN: { - int result = 1; - if (wparam == 0) { - if (data->ComboBox.dropdown != NULL) { - ReleaseCapture(); - HWND parent = GetParent(window); - SendMessage(parent, OD_SETTOP, (WPARAM)data->ComboBox.dropdown, 0); - - HWND dropdown_window = data->ComboBox.dropdown; - DestroyWindow(dropdown_window); - - WinData * dropdown_data = NULL; - ODWinData.getPointer(dropdown_window, &dropdown_data); - if (dropdown_data != NULL && dropdown_data->cachedSurface != NULL) { - delete dropdown_data->cachedSurface; - dropdown_data->cachedSurface = NULL; - _surface_count--; - } - - ODWinData.remove(dropdown_window); - data->ComboBox.dropdown = NULL; - } - return(result); - } - - if (data->ComboBox.dropdown != NULL) { - return(result); - } - - SetFocus(window); - - RECT parent_rect; - Get_Display_Rect(GetParent(window), &parent_rect); - - int count = (int)SendMessage(window, CB_GETCOUNT, 0, 0); - int item_height = (int)SendMessage(window, CB_GETITEMHEIGHT, 0, 0); - int dropdown_height = count * item_height + 4; - - if (display_rect.bottom + dropdown_height > parent_rect.bottom - 2 * item_height) { - dropdown_height = parent_rect.bottom - (2 * item_height + 4) - display_rect.bottom; - if (dropdown_height < item_height) { - dropdown_height = parent_rect.bottom - display_rect.bottom; - dropdown_height -= dropdown_height % item_height; - } - } - - RECT parent_display; - RECT combo_display; - Get_Display_Rect(GetParent(window), &parent_display); - Get_Display_Rect(window, &combo_display); - - int x = combo_display.left - parent_display.left; - int y = client_rect.bottom + combo_display.top - parent_display.top + 2; - int w = client_rect.right; - HWND dropdown_window = CreateWindowEx( - 0, - "ComboDropWin", - NULL, - WS_CHILD, - x, - y, - w, - dropdown_height, - GetParent(window), - NULL, - ProgramInstance, - window); - - WinData * dropdown_data = NULL; - if (!ODWinData.getPointer(dropdown_window, &dropdown_data)) { - WinData tmp; - memset(&tmp, 0, sizeof(tmp)); - ODWinData.add(dropdown_window, tmp); - } - - SendMessage(dropdown_window, OD_DROPSUBCLASSED, 0, 0); - SendMessage(GetParent(window), OD_SETTOP, (WPARAM)dropdown_window, 1); - SetCapture(dropdown_window); - ShowWindow(dropdown_window, 1); - - data->ComboBox.dropdown = dropdown_window; - return(result); - } - - case OD_SUBCLASSED: { - if (data->itemHeightSet) { - if (SendMessage(window, CB_GETITEMHEIGHT, 0, 0) == ODFontSize + 6) { - memset(data->ComboBox.itemColors, 0xFF, sizeof(data->ComboBox.itemColors)); - break; - } - } - - SendMessage(window, CB_SETITEMHEIGHT, (WPARAM)-1, ODFontSize + 2); - SendMessage(window, CB_SETITEMHEIGHT, 0, ODFontSize + 6); - data->itemHeightSet = 1; - memset(data->ComboBox.itemColors, 0xFF, sizeof(data->ComboBox.itemColors)); - break; - } - - case OD_SETCOLOR: - if ((unsigned int)wparam <= 50) { - data->ComboBox.itemColors[wparam] = lparam; - } - break; - } - - WNDPROC proc = NULL; - OriginalWndProcs.getValue(window, proc); - return(CallWindowProc(proc, window, message, wparam, lparam)); -} - - -/// -/// Handles the messages for an owner-drawn list box. -/// Besides repainting the stock list box, this routine provides the multi-column list the -/// dialogs are built around: the columns and the text, color and icon of each cell live in -/// the control's owner-draw record. A scroll bar is attached to, or taken away from, the -/// list as its contents demand. -/// -/// Returns with the result of the original window procedure for anything this -/// routine does not handle itself. -LRESULT CALLBACK ListBoxCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - HWND parent = GetParent(window); - int needs_scrollbar = -1; - int max_position = 0; - char call_default = 1; - LRESULT result = 0; - char string[512]; - - WinData * data = NULL; - ODWinData.getPointer(window, &data); - - int scrollbar_width = 2 * ODBorderThickness + 18; - - RECT client_rect; - GetClientRect(window, &client_rect); - RECT display_rect; - Get_Display_Rect(window, &display_rect); - display_rect.right -= ODBorderThickness; - display_rect.left += ODBorderThickness; - - Rect content_rect; - content_rect.X = display_rect.left; - client_rect.right -= 2 * ODBorderThickness; - content_rect.Width = client_rect.right - client_rect.left; - display_rect.top += ODBorderThickness; - content_rect.Y = display_rect.top; - client_rect.bottom -= 2 * ODBorderThickness; - display_rect.bottom -= ODBorderThickness; - content_rect.Height = client_rect.bottom - client_rect.top; - - /* - * Keep the attached scrollbar synchronized with the listbox state. This is skipped for - * the few messages that are queried while computing the scroll state (to avoid recursion). - */ - if (message != LB_GETCOUNT && message != LB_GETITEMHEIGHT && message != WM_VSCROLL) { - int count = SendMessage(window, LB_GETCOUNT, 0, 0); - int item_height = SendMessage(window, LB_GETITEMHEIGHT, 0, 0); - if (item_height <= 1) { - item_height = 1; - } - needs_scrollbar = (count * item_height > client_rect.bottom - client_rect.top); - max_position = count - (client_rect.bottom - client_rect.top) / item_height; - if ((uintptr_t)data->attachedWindow > 1) { - SCROLLINFO info; - info.fMask = SIF_RANGE | SIF_POS; - info.nMin = 0; - info.nMax = max_position; - info.nPos = data->ListBox.topIndex; - info.cbSize = sizeof(SCROLLINFO); - SendMessage(data->attachedWindow, SBM_SETSCROLLINFO, 0, (LPARAM)&info); - } - } - - switch (message) { - case WM_ERASEBKGND: - return(0); - - case WM_PAINT: { - call_default = 0; - ODDrawDimmedBackground(content_rect, window); - OD_Draw_Rect(*AlternateSurface, content_rect, 1, 0xFFFFFFFF); - - int fill_color = ODColorToHiColor(ODListBoxColor); - - RECT update_rect; - if (!GetUpdateRect(window, &update_rect, FALSE)) { - break; - } - - int count = SendMessage(window, LB_GETCOUNT, 0, 0); - int index = SendMessage(window, LB_GETTOPINDEX, 0, 0); - while (index < count) { - RECT item_rect; - if (SendMessage(window, LB_GETITEMRECT, index, (LPARAM)&item_rect) != -1) { - if (client_rect.top + item_rect.bottom > client_rect.bottom) { - break; - } - SendMessage(window, LB_GETTEXT, index, (LPARAM)string); - - ArrayList * columns = data->ListBox.columns; - if (columns != NULL) { - if (SendMessage(window, LB_GETSEL, index, 0) > 0) { - Rect fill; - fill.X = item_rect.left + display_rect.left; - fill.Y = item_rect.top + display_rect.top; - fill.Width = item_rect.right - item_rect.left; - fill.Height = item_rect.bottom - item_rect.top; - AlternateSurface->Fill_Rect(fill, fill_color); - } - - HDC dc = GetDC(window); - for (int col = 0; col < columns->length(); col++) { - ColumnData * column = NULL; - columns->getPointer(&column, col); - CellData * cell = NULL; - column->cells.getPointer(&cell, index); - if (cell == NULL || cell->type == CellData::INVALID) { - continue; - } - - if (cell->type == CellData::TEXT || cell->type == CellData::PRIMARY) { - Rect text_rect; - text_rect.X = display_rect.left + item_rect.left + column->xPos; - text_rect.Y = display_rect.top + item_rect.top; - text_rect.Width = 0; - text_rect.Height = 0; - if (cell->type == CellData::TEXT) { - strcpy(string, cell->string.get()); - } else if (cell->type == CellData::PRIMARY) { - SendMessage(window, LB_GETTEXT, index, (LPARAM)string); - } - COLORREF text_color = cell->color; - if (text_color == -1) { - text_color = ODColorText; - } - int max_width = column->width; - if (max_width == 0) { - max_width = 0xFFFF; - } - if (display_rect.right - max_width - column->xPos - item_rect.left - display_rect.left < 0) { - max_width = display_rect.right - column->xPos - item_rect.left - display_rect.left; - } - SendMessage(window, OD_RESTOREDC, 0, (LPARAM)dc); - SIZE ellipsis_size; - GetTextExtentPoint32(dc, "...", strlen("..."), &ellipsis_size); - SIZE text_size; - GetTextExtentPoint32(dc, string, strlen(string), &text_size); - if (text_size.cx > max_width) { - while (true) { - int len = strlen(string); - if (!len) { - break; - } - string[strlen(string) - 1] = '\0'; - GetTextExtentPoint32(dc, string, strlen(string), &text_size); - text_size.cx += ellipsis_size.cx; - if (text_size.cx <= max_width) { - strcat(string, "..."); - break; - } - } - } - OD_Draw_Text(text_color, data->font, text_rect, string, strlen(string), 0, 0, NULL); - } else if (cell->type == CellData::SURFACE) { - Surface * surface = cell->surf; - if (surface != NULL) { - int height = surface->Get_Height(); - int width = surface->Get_Width(); - Rect surface_rect; - surface_rect.Width = width; - surface_rect.X = display_rect.left + item_rect.left + column->xPos; - surface_rect.Height = height; - surface_rect.Y = display_rect.top + item_rect.top + (item_rect.bottom - item_rect.top - height) / 2; - SurfaceCache.DrawTrans(surface_rect, *AlternateSurface, *surface, (255 >> DSurface::RedLeft << DSurface::RedRight) | (255u >> DSurface::BlueLeft << DSurface::BlueRight)); - } - } else { - int ping = cell->pingtime; - Rect ping_rect; - ping_rect.X = display_rect.left + item_rect.left + column->xPos; - ping_rect.Y = display_rect.top + item_rect.top; - ping_rect.Width = 28; - ping_rect.Height = 12; - unsigned color; - if (ping < 300) { - color = DSurface::Build_Hicolor_Pixel(0, 192, 0); - } else if (ping < 500) { - color = DSurface::Build_Hicolor_Pixel(192, 192, 0); - } else { - color = DSurface::Build_Hicolor_Pixel(192, 0, 0); - } - ODDrawGradientRect(ping_rect, *AlternateSurface, color, (ping << 16) / 1000); - } - } - ReleaseDC(window, dc); - } else { - ArrayList * row_colors = data->ListBox.rowColors; - COLORREF text_color; - int * color_ptr = NULL; - if (row_colors == NULL || !row_colors->getPointer(&color_ptr, index) || *color_ptr == -1) { - text_color = ODColorText; - } else { - text_color = *color_ptr; - } - - if (SendMessage(window, LB_GETSEL, index, 0) > 0) { - RECT fill; - fill.left = item_rect.left + display_rect.left; - fill.top = item_rect.top + display_rect.top; - fill.bottom = item_rect.bottom - item_rect.top; - fill.right = item_rect.right - item_rect.left; - AlternateSurface->Fill_Rect(*(Rect *)&fill, fill_color); - } - - Rect text_rect; - int max_width = item_rect.right - item_rect.left; - text_rect.X = item_rect.left + display_rect.left + 2; - text_rect.Y = display_rect.top + item_rect.top; - text_rect.Width = 0; - text_rect.Height = 0; - if (item_rect.right == item_rect.left) { - max_width = 0xFFFF; - } - HDC dc = GetDC(window); - if (data->font != NULL) { - SelectObject(dc, data->font); - } - SIZE ellipsis_size; - GetTextExtentPoint32(dc, "...", strlen("..."), &ellipsis_size); - SIZE text_size; - GetTextExtentPoint32(dc, string, strlen(string), &text_size); - if (text_size.cx > max_width && text_size.cx + ellipsis_size.cx > max_width) { - while (true) { - int len = strlen(string); - if (!len) { - break; - } - string[strlen(string) - 1] = '\0'; - GetTextExtentPoint32(dc, string, strlen(string), &text_size); - if (text_size.cx + ellipsis_size.cx <= max_width) { - strcat(string, "..."); - break; - } - } - } - OD_Draw_Text(text_color, data->font, text_rect, string, strlen(string), 0, 0, NULL); - } - } - index++; - } - ValidateRect(window, &update_rect); - break; - } - - case WM_SIZE: { - if ((uintptr_t)data->attachedWindow > 1) { - RECT parent_display; - Get_Display_Rect(GetParent(window), &parent_display); - Rect win_display; - Get_Display_Rect(window, (LPRECT)&win_display); - MoveWindow(data->attachedWindow, win_display.Width - parent_display.left, win_display.Y - parent_display.top, scrollbar_width, win_display.Height - win_display.Y, TRUE); - } - Surface * surface = data->cachedSurface; - if (surface != NULL) { - if ((unsigned short)lparam != surface->Get_Width() || HIWORD(lparam) != surface->Get_Height()) { - WinData * cache_data = NULL; - ODWinData.getPointer(window, &cache_data); - if (cache_data != NULL && cache_data->cachedSurface != NULL) { - delete cache_data->cachedSurface; - cache_data->cachedSurface = NULL; - _surface_count--; - } - } - } - break; - } - - case WM_SETFONT: { - HDC dc = GetDC(window); - TEXTMETRIC tm; - GetTextMetrics(dc, &tm); - ReleaseDC(window, dc); - SendMessage(window, LB_SETITEMHEIGHT, (WPARAM)-1, (unsigned short)(LOWORD(tm.tmHeight) + 2)); - data->font = (HFONT)wparam; - return(0); - } - - case WM_VSCROLL: { - call_default = 0; - LRESULT position = SendMessage(data->attachedWindow, SBM_GETPOS, 0, 0); - if (position != SendMessage(window, LB_GETTOPINDEX, 0, 0)) { - SendMessage(window, LB_SETTOPINDEX, position, 0); - } - break; - } - - case LB_ADDSTRING: - wparam = (WPARAM)-1; - /// Fall through to insert with an append position. - case LB_INSERTSTRING: { - int position = (int)wparam; - if (position != -1) { - ArrayList * row_colors = data->ListBox.rowColors; - if (row_colors != NULL) { - int color = -1; - row_colors->add(color, position); - } - ArrayList * sel_states = data->ListBox.selStates; - if (sel_states != NULL) { - int selected = 0; - sel_states->add(selected, position); - } - } - if (position < 0) { - position = SendMessage(window, LB_GETCOUNT, 0, 0); - } - - ArrayList * columns = data->ListBox.columns; - CellData cell; - if (columns != NULL) { - for (int col = 0; col < columns->length(); col++) { - - /* - * The first column added implicitly holds the row's listbox string, - * so the new row gets a PRIMARY cell there and INVALID cells elsewhere. - */ - cell.type = (col == 0) ? CellData::PRIMARY : CellData::INVALID; - ColumnData * column = NULL; - columns->getPointer(&column, col); - column->cells.add(cell, position); - } - } - break; - } - - case LB_SETSEL: { - int index = (int)lparam; - if (index < -1) { - return(-1); - } - ArrayList * sel_states = data->ListBox.selStates; - if (sel_states == NULL) { - sel_states = new ArrayList; - data->ListBox.selStates = sel_states; - } - if (index >= SendMessage(window, LB_GETCOUNT, 0, 0) - 1) { - index = SendMessage(window, LB_GETCOUNT, 0, 0) - 1; - } - if (index >= sel_states->length()) { - int unselected = 0; - sel_states->setSize(index + 1, unselected); - } - if (index == -1) { - for (int i = 0; i < sel_states->length(); i++) { - int selected = (int)wparam; - sel_states->replace(selected, i); - } - } else { - int selected = (int)wparam; - sel_states->replace(selected, index); - } - parent = GetParent(window); - SendMessage(parent, WM_COMMAND, (GetWindowLong(window, GWL_ID) & 0xFFFF) | 0x10000, (LPARAM)window); - InvalidateRect(window, NULL, FALSE); - return(0); - } - - case LB_GETSEL: { - ArrayList * sel_states = data->ListBox.selStates; - if (sel_states == NULL) { - return(0); - } - if ((int)wparam >= sel_states->length()) { - return(0); - } - int * selected = NULL; - if (!sel_states->getPointer(&selected, (int)wparam)) { - return(0); - } - return(*selected); - } - - case LB_SETCURSEL: { - int index = (int)wparam; - if ((int)wparam >= -1 && index < SendMessage(window, LB_GETCOUNT, 0, 0)) { - if (data->ListBox.curSel != -1) { - SendMessage(window, LB_SETSEL, 0, data->ListBox.curSel); - } - data->ListBox.curSel = index; - if (index != -1) { - SendMessage(window, LB_SETSEL, TRUE, index); - } - } - parent = GetParent(window); - SendMessage(parent, WM_COMMAND, (GetWindowLong(window, GWL_ID) & 0xFFFF) | 0x10000, (LPARAM)window); - InvalidateRect(window, NULL, FALSE); - return(0); - } - - case LB_GETCURSEL: - return(data->ListBox.curSel); - - case LB_DELETESTRING: { - ArrayList * row_colors = data->ListBox.rowColors; - if (row_colors != NULL && row_colors->length() > (int)wparam) { - row_colors->remove((int)wparam); - } - ArrayList * sel_states = data->ListBox.selStates; - if (sel_states != NULL && sel_states->length() > (int)wparam) { - sel_states->remove((int)wparam); - } - ArrayList * columns = data->ListBox.columns; - if (columns != NULL) { - for (int col = 0; col < columns->length(); col++) { - ColumnData * column = NULL; - columns->getPointer(&column, col); - if (column->cells.length() != 0) { - column->cells.remove((int)wparam); - } - } - } - break; - } - - case WM_NCDESTROY: - case LB_RESETCONTENT: { - ArrayList * row_colors = data->ListBox.rowColors; - if (row_colors != NULL) { - delete row_colors; - } - ArrayList * sel_states = data->ListBox.selStates; - if (sel_states != NULL) { - delete sel_states; - } - data->ListBox.rowColors = 0; - data->ListBox.selStates = 0; - data->ListBox.topIndex = 0; - data->ListBox.curSel = -1; - - ArrayList * columns = data->ListBox.columns; - if (columns != NULL) { - for (int col = 0; col < columns->length(); col++) { - ColumnData * column = NULL; - columns->getPointer(&column, col); - column->cells.clear(); - } - } - - if (message == WM_NCDESTROY) { - if (columns != NULL) { - delete columns; - } - data->ListBox.columns = NULL; - } else { - parent = GetParent(window); - SendMessage(parent, WM_COMMAND, (GetWindowLong(window, GWL_ID) & 0xFFFF) | 0x10000, (LPARAM)window); - } - break; - } - - case LB_GETTOPINDEX: - return(data->ListBox.topIndex); - - case LB_SETTOPINDEX: { - int index = (int)wparam; - int count = SendMessage(window, LB_GETCOUNT, 0, 0); - int item_height = SendMessage(window, LB_GETITEMHEIGHT, 0, 0); - if (!count || !item_height) { - break; - } - int visible = (client_rect.bottom - client_rect.top) / item_height; - if (index < 0) { - index = 0; - } - if (count - visible <= 0) { - index = 0; - } else if (index > count - visible) { - index = count - visible; - } - if (index != data->ListBox.topIndex) { - data->ListBox.topIndex = index; - InvalidateRect(window, NULL, FALSE); - } - return(0); - } - - case LB_SELITEMRANGE: { - int last = HIWORD(lparam); - int first = LOWORD(lparam); - int count = SendMessage(window, LB_GETCOUNT, 0, 0); - if (last < 0 || last < first) { - return(-1); - } - if (last >= count) { - last = count - 1; - } - ArrayList * sel_states = data->ListBox.selStates; - if (sel_states == NULL) { - sel_states = new ArrayList; - data->ListBox.selStates = sel_states; - } - if (last >= sel_states->length()) { - int unselected = 0; - sel_states->setSize(last + 1, unselected); - } - for (int i = first; i <= last; i++) { - int selected = (int)wparam; - sel_states->replace(selected, i); - } - parent = GetParent(window); - SendMessage(parent, WM_COMMAND, (GetWindowLong(window, GWL_ID) & 0xFFFF) | 0x10000, (LPARAM)window); - return(0); - } - - case LB_GETSELCOUNT: { - ArrayList * sel_states = data->ListBox.selStates; - if (sel_states == NULL) { - return(0); - } - int count = sel_states->length(); - int selected = 0; - int total = 0; - for (int i = 0; i < count; i++) { - int * value = NULL; - if (sel_states->getPointer(&value, i)) { - selected = *value; - } - if (selected) { - total++; - } - } - return(total); - } - - case LB_GETSELITEMS: { - ArrayList * sel_states = data->ListBox.selStates; - int total = 0; - if (sel_states != NULL) { - int selected = 0; - if (sel_states->length() > 0) { - int * out = (int *)lparam; - int max = (int)wparam; - for (int i = 0; i < sel_states->length(); i++) { - int * value = NULL; - if (sel_states->getPointer(&value, i)) { - selected = *value; - } - if (selected) { - *out = i; - total++; - out++; - } - if (total >= max) { - break; - } - } - } - } - return(total); - } - - case LB_GETITEMRECT: { - int index = (int)wparam; - if (index < data->ListBox.topIndex) { - return(-1); - } - if (index >= SendMessage(window, LB_GETCOUNT, 0, 0)) { - return(-1); - } - int item_height = SendMessage(window, LB_GETITEMHEIGHT, 0, 0); - int relative = index - data->ListBox.topIndex; - if (relative > (client_rect.bottom - client_rect.top) / item_height) { - return(-1); - } - RECT * out = (RECT *)lparam; - out->top = relative * item_height; - out->bottom = relative * item_height + item_height; - out->left = client_rect.left; - out->right = client_rect.right - client_rect.left; - return(0); - } - - case WM_LBUTTONDOWN: { - int top = SendMessage(window, LB_GETTOPINDEX, 0, 0); - int item_height = SendMessage(window, LB_GETITEMHEIGHT, 0, 0); - int index = top + (int)HIWORD(lparam) / item_height; - LONG style = GetWindowLong(window, GWL_STYLE); - SetFocus(window); - int paint_disabled = SendMessage(window, OD_DISABLEPAINT, 0, 1); - if ((style & LBS_MULTIPLESEL) != 0) { - int select = (SendMessage(window, LB_GETSEL, index, 0) == 0); - Sound_Effect(Rule->GenericClick); - SendMessage(window, LB_SETSEL, select, index); - InvalidateRect(window, NULL, FALSE); - } else if ((style & LBS_NOSEL) == 0) { - Sound_Effect(Rule->GenericClick); - SendMessage(window, LB_SETCURSEL, index, 0); - InvalidateRect(window, NULL, FALSE); - } - SendMessage(window, OD_DISABLEPAINT, 0, paint_disabled); - parent = GetParent(window); - SendMessage(parent, WM_COMMAND, (GetWindowLong(window, GWL_ID) & 0xFFFF) | 0x10000, (LPARAM)window); - return(0); - } - - case WM_LBUTTONDBLCLK: - PostMessage(parent, WM_COMMAND, (GetWindowLong(window, GWL_ID) & 0xFFFF) | 0x20000, (LPARAM)window); - return(0); - - case WM_RBUTTONDOWN: - SendMessage(window, LB_SETSEL, 0, -1); - SendMessage(window, LB_SETCURSEL, (WPARAM)-1, 0); - parent = GetParent(window); - SendMessage(parent, WM_COMMAND, (GetWindowLong(window, GWL_ID) & 0xFFFF) | 0x10000, (LPARAM)window); - InvalidateRect(window, NULL, FALSE); - return(0); - - case OD_GETCELLTIP: { - int count = SendMessage(window, LB_GETCOUNT, 0, 0); - int item_height = SendMessage(window, LB_GETITEMHEIGHT, 0, 0); - ArrayList * columns = data->ListBox.columns; - if (columns != NULL) { - int row = data->ListBox.topIndex + (int)HIWORD(wparam) / item_height; - int best_column = -1; - int best_x = 0; - ColumnData * column = NULL; - for (int col = 0; col < columns->length(); col++) { - columns->getPointer(&column, col); - if (column->xPos <= (int)LOWORD(wparam)) { - if (column->xPos > best_x) { - best_column = col; - best_x = column->xPos; - } - } - } - if (best_column != -1 && row >= 0 && row < count) { - column = NULL; - columns->getPointer(&column, best_column); - if (column != NULL && row < column->cells.length()) { - CellData * cell = NULL; - column->cells.getPointer(&cell, row); - if (cell != NULL && lparam != 0) { - strcpy((char *)lparam, cell->hint.get()); - return(strlen(cell->hint.get()) == 0); - } - } - } - } - return(0); - } - - case OD_ADDCOLUMN: { - ArrayList * columns = data->ListBox.columns; - if (columns == NULL) { - columns = new ArrayList; - data->ListBox.columns = columns; - } - ColumnData * column = NULL; - int col = 0; - while (col < columns->length()) { - columns->getPointer(&column, col); - if (column->xPos == lparam) { - return(lparam); - } - col++; - } - ColumnData new_column; - new_column.xPos = lparam; - new_column.width = wparam; - int length = columns->length(); - columns->add(new_column, length); - return(lparam); - } - - case OD_REMOVECOLUMN: { - ArrayList * columns = data->ListBox.columns; - if (columns != NULL) { - ColumnData * column = NULL; - for (int col = 0; col < columns->length(); col++) { - columns->getPointer(&column, col); - if (column->xPos == lparam) { - columns->remove(col); - return(lparam); - } - } - } - return(-1); - } - - case OD_SETCELL: { - ArrayList * columns = data->ListBox.columns; - if (columns == NULL) { - return(-1); - } - int column_id = LOWORD(wparam); - int row = HIWORD(wparam); - ColumnData * column = NULL; - int found = -1; - for (int col = 0; col < columns->length(); col++) { - columns->getPointer(&column, col); - if (column->xPos == column_id) { - found = col; - break; - } - } - if (found == -1) { - return(-1); - } - if (row < 0 || row >= SendMessage(window, LB_GETCOUNT, 0, 0)) { - return(-1); - } - CellData filler; - if (found == 0) { - filler.type = CellData::PRIMARY; - } - if (row >= column->cells.length()) { - column->cells.setSize(row + 1, filler); - } - column->cells.replace(*(CellData *)lparam, row); - return(column_id); - } - - case OD_SETCOLOR: { - ArrayList * row_colors = data->ListBox.rowColors; - if (row_colors == NULL) { - row_colors = new ArrayList; - data->ListBox.rowColors = row_colors; - } - int index = (int)wparam; - if (index >= row_colors->length()) { - int blank = -1; - row_colors->setSize(index + 1, blank); - } - int color = (int)lparam; - row_colors->replace(color, index); - InvalidateRect(window, NULL, FALSE); - break; - } - - case OD_SUBCLASSED: - data->ListBox.curSel = -1; - data->font = ODListFontPtr; - SendMessage(window, LB_SETITEMHEIGHT, (WPARAM)-1, (unsigned short)(ODListFontSize + 2)); - break; - } - - /* - * Create or destroy the attached scrollbar based on whether the listbox needs one, - * then forward the message to the original Win32 listbox procedure. - */ - if (needs_scrollbar == 1) { - if (data->attachedWindow == NULL) { - data->attachedWindow = (HWND)1; - parent = GetParent(window); - RECT parent_display; - Get_Display_Rect(parent, &parent_display); - RECT win_display; - Get_Display_Rect(window, &win_display); - int x = win_display.left - parent_display.left; - int y = win_display.top - parent_display.top; - data->attachedWindow = CreateWindowEx(0, "Scrollbar", NULL, 0x50010001, - win_display.left - parent_display.left - scrollbar_width + client_rect.right + 1, - client_rect.top + win_display.top - parent_display.top, - scrollbar_width, win_display.bottom - win_display.top, - parent, NULL, ProgramInstance, NULL); - data->scrollBarWidth = scrollbar_width; - InitializeCtrl(data->attachedWindow, 0); - - WinData * sb_data = NULL; - ODWinData.getPointer(data->attachedWindow, &sb_data); - sb_data->ownerWindow = window; - - SCROLLINFO info; - info.nMax = max_position; - info.fMask = SIF_RANGE | SIF_POS; - info.nMin = 0; - info.nPos = data->ListBox.topIndex; - info.cbSize = sizeof(SCROLLINFO); - SendMessage(data->attachedWindow, SBM_SETSCROLLINFO, 0, (LPARAM)&info); - - SetWindowPos(window, NULL, 0, 0, win_display.right - win_display.left - scrollbar_width, win_display.bottom - win_display.top, SWP_NOMOVE); - ShowWindow(data->attachedWindow, SW_SHOW); - BringWindowToTop(data->attachedWindow); - InvalidateRect(data->attachedWindow, NULL, FALSE); - UpdateWindow(data->attachedWindow); - - Rect validate_rect; - validate_rect.X = x; - validate_rect.Y = y; - validate_rect.Width = x + client_rect.right + 1; - validate_rect.Height = client_rect.bottom + y + 1; - ValidateRect(parent, (const RECT *)&validate_rect); - } - } else if (needs_scrollbar == 0 && data->attachedWindow != NULL && !data->paintDisabled) { - DestroyWindow(data->attachedWindow); - HWND scrollbar = data->attachedWindow; - ODRemoveFromDict(scrollbar, 0); - data->attachedWindow = NULL; - data->scrollBarWidth = 0; - SetWindowPos(window, NULL, 0, 0, client_rect.right + ODBorderThickness - client_rect.left + scrollbar_width + 1, client_rect.bottom + 2 * ODBorderThickness - client_rect.top, SWP_NOMOVE); - parent = GetParent(window); - RECT parent_display; - Get_Display_Rect(parent, &parent_display); - Rect validate_rect; - validate_rect.X = display_rect.left - parent_display.left - 1; - validate_rect.Y = display_rect.top - parent_display.top - 1; - validate_rect.Width = scrollbar_width + client_rect.right + display_rect.left - parent_display.left; - validate_rect.Height = display_rect.top - parent_display.top + client_rect.bottom + 1; - ValidateRect(parent, (const RECT *)&validate_rect); - } - - WNDPROC original_proc = NULL; - OriginalWndProcs.getValue(window, original_proc); - if (call_default) { - result = CallWindowProc(original_proc, window, message, wparam, lparam); - } - - if (message == WM_NCDESTROY) { - ArrayList * row_colors = data->ListBox.rowColors; - if (row_colors != NULL) { - delete row_colors; - } - ArrayList * sel_states = data->ListBox.selStates; - if (sel_states != NULL) { - delete sel_states; - } - } - - return(result); -} - - -/// -/// Handles the messages for an owner-drawn scroll bar. -/// The grip is sized against the scroll range and dragged directly with the mouse, while -/// the arrow buttons repeat on a timer for as long as they are held. The owner is told of -/// the new position as it changes, which is what lets a list box scroll under the mouse. -/// -/// Returns with the result of the original window procedure for anything this -/// routine does not handle itself. -LRESULT CALLBACK ScrollBarCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - RECT client_rect; - RECT display_rect; - GetClientRect(window, &client_rect); - Get_Display_Rect(window, &display_rect); - - client_rect.right -= 2 * ODBorderThickness; - client_rect.bottom -= 2 * ODBorderThickness; - - display_rect.left += ODBorderThickness; - display_rect.right -= ODBorderThickness; - display_rect.top += ODBorderThickness; - display_rect.bottom -= ODBorderThickness; - - WinData * data = NULL; - ODWinData.getPointer(window, &data); - - bool keep_parent_capture = false; - if (data->ScrollBar.keepCapture) { - keep_parent_capture = true; - } - int dragging = data->ScrollBar.dragging; - int range = data->ScrollBar.range; - int message_result = data->ScrollBar.result; - int position = data->ScrollBar.position; - int up_pressed = data->ScrollBar.upPressed; - int down_pressed = data->ScrollBar.downPressed; - - if (!range) { - range = 100; - } - - int scroll_code = 0; - int grip_top = 0; - int grip_bottom = 0; - - int width = client_rect.right - client_rect.left; - int travel_height = client_rect.bottom - client_rect.top - 44; - int grip_height = (int)((double)travel_height - log((double)(range + 1)) * (double)travel_height * 0.2); - if (grip_height <= 14) { - grip_height = 14; - } - - int travel = travel_height - grip_height; - if (travel <= 1) { - travel = 1; - } - - if (message < TBM_GETPOS || message == OD_REFRESHNOPAINT) { - if (!dragging) { - grip_top = position * travel / range + client_rect.top + 22; - grip_bottom = grip_top + grip_height; - } else { - POINT cursor; - Get_Logical_Cursor_Pos(window, cursor); - - grip_top = cursor.y - grip_height / 2; - if (grip_top < 22) { - grip_top = 22; - } - - if (client_rect.bottom - grip_height - 22 < grip_top) { - grip_top = client_rect.bottom - grip_height - 22; - } - - grip_bottom = grip_top + grip_height; - scroll_code = SB_THUMBTRACK; - position = range * (grip_top - 22) / travel; - } - } - - switch (message) { - case SBM_GETPOS: - return(position); - - case SBM_SETPOS: - if ((int)wparam <= range && (int)wparam > 0) { - position = (int)wparam; - } - break; - - case SBM_SETRANGE: - range = (int)lparam; - if (position > range) { - position = range; - } - break; - - case SBM_SETSCROLLINFO: { - SCROLLINFO * info = (SCROLLINFO *)lparam; - range = info->nMax; - position = info->nPos; - break; - } - - case WM_NCHITTEST: - case WM_GETDLGCODE: { - WNDPROC proc = NULL; - OriginalWndProcs.getValue(window, proc); - return(CallWindowProc(proc, window, message, wparam, lparam)); - } - - case WM_ERASEBKGND: - return(0); - - case WM_PAINT: { - if (data->paintDisabled) { - return(0); - } - - /* - * The source rect and the blit dest rect get reused for every blit - * below; only the Draw and edge glow calls get their own rects. - */ - Rect src_rect; - Rect full_rect; - full_rect.X = display_rect.left; - full_rect.Y = display_rect.top; - full_rect.Width = client_rect.right; - full_rect.Height = client_rect.bottom; - src_rect.X = 0; - src_rect.Y = 0; - src_rect.Width = client_rect.right; - src_rect.Height = client_rect.bottom; - - HWND parent = GetParent(window); - WinData * parent_data = NULL; - if (parent != NULL) { - ODWinData.getPointer(parent, &parent_data); - } - - RECT parent_display; - Get_Display_Rect(parent, &parent_display); - - Rect source_rect = src_rect; - if (parent_data != NULL && parent_data->cachedSurface != NULL) { - source_rect.X = display_rect.left + source_rect.X - parent_display.left; - source_rect.Y += display_rect.top - parent_display.top; - } - - if (data->cachedSurface != NULL) { - if (data->cachedSurface->Get_Width() != client_rect.right || data->cachedSurface->Get_Height() != client_rect.bottom) { - delete data->cachedSurface; - data->cachedSurface = NULL; - } - } - - if (data->cachedSurface == NULL) { - BSurface * background = new BSurface(client_rect.right, client_rect.bottom, 2); - data->cachedSurface = background; - ++_surface_count; - - if (parent_data != NULL && parent_data->cachedSurface != NULL) { - background->Blit_From(src_rect, *parent_data->cachedSurface, source_rect); - } - - int pixel_count = client_rect.bottom * client_rect.right; - unsigned short * pixels = (unsigned short *)background->Lock(); - unsigned short * ptr = pixels; - for (int i = pixel_count; i > 0; --i) { - *ptr = OD_Blend_Color(*ptr, 0xFFFF, ODColorSteps); - ptr++; - } - if (pixels != NULL) { - background->Unlock(); - } - } - - if (parent_data != NULL && parent_data->cachedSurface != NULL) { - AlternateSurface->Blit_From(full_rect, *parent_data->cachedSurface, source_rect); - } - - OD_Draw_Rect(*AlternateSurface, full_rect, ODBorderThickness, 0xFFFFFFFF); - - Rect grip_rect; - grip_rect.X = display_rect.left; - full_rect.X = display_rect.left; - grip_rect.Y = grip_top + display_rect.top; - grip_rect.Width = client_rect.right; - grip_rect.Height = grip_bottom - grip_top; - full_rect.Y = grip_top + display_rect.top; - full_rect.Width = client_rect.right; - full_rect.Height = grip_bottom - grip_top; - src_rect.Width = client_rect.right; - src_rect.Height = grip_bottom - grip_top; - src_rect.X = 0; - src_rect.Y = grip_top; - - Surface * grip_center = SurfaceCache.GetSurface("sbgripm.pcx", NULL); - if (grip_center != NULL) { - grip_rect.Width = grip_center->Get_Width(); - } - SurfaceCache.Draw(grip_rect, *AlternateSurface, *grip_center, 0, 0); - - Rect grip_src(0, 0, grip_center->Get_Width(), grip_center->Get_Height()); - - Surface * grip_top_surf = SurfaceCache.GetSurface("sbgript.pcx", NULL); - if (grip_top_surf != NULL) { - full_rect.Height = grip_top_surf->Get_Height(); - } - AlternateSurface->Blit_From(full_rect, *grip_top_surf, grip_src); - - Surface * grip_bottom_surf = SurfaceCache.GetSurface("sbgripb.pcx", NULL); - if (grip_bottom_surf != NULL) { - full_rect.Y = display_rect.top + grip_bottom - grip_bottom_surf->Get_Height(); - } - AlternateSurface->Blit_From(full_rect, *grip_bottom_surf, grip_src); - - Rect up_rect; - up_rect.X = display_rect.left; - full_rect.X = display_rect.left; - src_rect.X = display_rect.left; - up_rect.Y = display_rect.top; - full_rect.Y = display_rect.top; - up_rect.Width = client_rect.right; - up_rect.Height = 22; - full_rect.Width = client_rect.right; - full_rect.Height = 22; - src_rect.Width = client_rect.right; - src_rect.Height = 22; - src_rect.X = 0; - src_rect.Y = 0; - AlternateSurface->Blit_From(full_rect, *data->cachedSurface, src_rect); - ODDrawEdgeGlows(*AlternateSurface, up_rect, up_pressed == 0, 2, ODScrollBarAdj, ODScrollBarAdj, ODScrollBarAdj, ODScrollBarAdj); - ODDrawArrowBitmap(*AlternateSurface, up_rect, 1, up_pressed); - - Rect down_rect; - down_rect.X = display_rect.left; - full_rect.X = display_rect.left; - down_rect.Y = display_rect.bottom - 22; - full_rect.Y = display_rect.bottom - 22; - down_rect.Width = client_rect.right; - down_rect.Height = 22; - full_rect.Width = client_rect.right; - full_rect.Height = 22; - src_rect.Width = client_rect.right; - src_rect.Height = 22; - src_rect.X = 0; - src_rect.Y = client_rect.bottom - 22; - AlternateSurface->Blit_From(full_rect, *data->cachedSurface, src_rect); - ODDrawEdgeGlows(*AlternateSurface, down_rect, down_pressed == 0, 2, ODScrollBarAdj, ODScrollBarAdj, ODScrollBarAdj, ODScrollBarAdj); - ODDrawArrowBitmap(*AlternateSurface, down_rect, 0, down_pressed); - - ValidateRect(window, NULL); - break; - } - - case WM_TIMER: { - POINT cursor; - Get_Logical_Cursor_Pos(window, cursor); - - up_pressed = 0; - down_pressed = 0; - - if (message_result && cursor.x > client_rect.right - width) { - if (cursor.y < 22) { - if (!dragging) { - up_pressed = 1; - } - if (position != 0) { - scroll_code = SB_LINEUP; - --position; - } - } else if (cursor.y > client_rect.bottom - 22) { - if (!dragging) { - down_pressed = 1; - } - if (position + 1 <= range) { - scroll_code = SB_LINEDOWN; - ++position; - } - } - } - - SetTimer(window, 0, 0x19, NULL); - break; - } - - case WM_MOUSEMOVE: { - if (dragging) { - RECT rect; - rect.left = client_rect.right - width; - rect.top = client_rect.top; - rect.right = client_rect.right; - rect.bottom = client_rect.bottom; - InvalidateRect(window, &rect, FALSE); - } - - if (wparam & MK_LBUTTON) { - break; - } - } - - case WM_LBUTTONUP: - message_result = 0; - dragging = 0; - if (up_pressed || down_pressed) { - InvalidateRect(window, NULL, FALSE); - } - up_pressed = 0; - down_pressed = 0; - KillTimer(window, 0); - ReleaseCapture(); - if (keep_parent_capture) { - SetCapture(data->ownerWindow); - } - scroll_code = SB_ENDSCROLL; - break; - - case WM_LBUTTONDOWN: - case WM_LBUTTONDBLCLK: { - if (message == WM_LBUTTONDOWN) { - message_result = 1; - SetCapture(window); - SetTimer(window, 0, 0x1F4, NULL); - } else { - message_result = 0; - dragging = 0; - KillTimer(window, 0); - ReleaseCapture(); - if (keep_parent_capture) { - SetCapture(data->ownerWindow); - } - } - - int xpos = (unsigned short)LOWORD(lparam); - int ypos = (unsigned short)HIWORD(lparam); - int repeat = (message == WM_LBUTTONDBLCLK) ? 2 : 1; - - up_pressed = 0; - down_pressed = 0; - - while (repeat > 0) { - if (xpos > (client_rect.right - width)) { - if (ypos < 22 && position) { - up_pressed = 1; - scroll_code = SB_LINEUP; - --position; - } else { - bool skip_thumb_logic = false; - if (ypos > client_rect.bottom - 22) { - if (position + 1 <= range) { - down_pressed = 1; - scroll_code = SB_LINEDOWN; - ++position; - skip_thumb_logic = true; - } - } - - if (!skip_thumb_logic) { - if (ypos < grip_top || ypos >= grip_bottom) { - grip_top = ypos - grip_height / 2; - if (grip_top < 22) { - grip_top = 22; - } - - int max_top = client_rect.bottom - grip_height - 22; - if (max_top < grip_top) { - grip_top = max_top; - } - - grip_bottom = grip_top + grip_height; - scroll_code = SB_THUMBTRACK; - position = range * (grip_top - 22) / travel; - } else if (message == WM_LBUTTONDOWN) { - dragging = 1; - } - } - } - } - --repeat; - } - break; - } - - case OD_SETKEEPCAPTURE: - if (lparam != 0) { - if (data != NULL) { - data->ScrollBar.keepCapture = 1; - } - } else { - if (data != NULL) { - data->ScrollBar.keepCapture = 0; - } - } - break; - } - - int send_notify = 0; - if (position != data->ScrollBar.position || range != data->ScrollBar.range) { - if (data->ownerWindow != NULL) { - send_notify = 1; - } - } - - data->ScrollBar.position = position; - data->ScrollBar.result = message_result; - data->ScrollBar.dragging = dragging; - data->ScrollBar.range = range; - data->ScrollBar.upPressed = up_pressed; - data->ScrollBar.downPressed = down_pressed; - - if (send_notify) { - SendMessage(data->ownerWindow, WM_VSCROLL, MAKEWPARAM(scroll_code, (unsigned short)position), (LPARAM)window); - InvalidateRect(window, NULL, FALSE); - } - - return(0); -} - - -/// -/// Handles the messages for an owner-drawn progress bar. -/// The bar is drawn as a gradient over a cached copy of the dialog background, scaled -/// against the range the dialog set. A position outside that range is clamped rather than -/// refused. -/// -/// Returns with zero; nothing this control is sent needs to reach the original -/// window procedure. -LRESULT CALLBACK ProgressBarCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - RECT rect; - Get_Display_Rect(window, &rect); - - OwnerDraw::WinData * data = NULL; - - ODWinData.getPointer(window, &data); - - switch (message) { - case OD_SUBCLASSED: { - data->ProgressBar.maximum = 100; - break; - } - - case PBM_SETRANGE: { - data->ProgressBar.minimum = LOWORD(lparam); - data->ProgressBar.maximum = HIWORD(lparam); - break; - } - - case PBM_SETPOS: { - int pos = (int)wparam; - if (pos < data->ProgressBar.minimum) { - pos = data->ProgressBar.minimum; - } - if (pos > data->ProgressBar.maximum) { - pos = data->ProgressBar.maximum; - } - data->ProgressBar.position = pos; - - InvalidateRect(window, NULL, FALSE); - break; - } - - case WM_PAINT: { - Rect sourcerect(0, 0, rect.right - rect.left + 1, rect.bottom - rect.top + 1); - Rect destrect(rect.left, rect.top, rect.right - rect.left + 1, rect.bottom - rect.top + 1); - - if (data->cachedSurface == NULL) { - Surface * surf = new BSurface(rect.right - rect.left + 1, rect.bottom - rect.top + 1, 2); - data->cachedSurface = surf; - _surface_count++; - surf->Blit_From(sourcerect, *AlternateSurface, destrect); - } - - AlternateSurface->Blit_From(destrect, *data->cachedSurface, sourcerect); - int pos = (data->ProgressBar.position * 65536) / (data->ProgressBar.maximum - data->ProgressBar.minimum); - int color = ODColorToHiColor(0x000000FF); - ODDrawGradientRect(destrect, *AlternateSurface, color, pos); - ValidateRect(window, NULL); - break; - } - } - - return(0); -} - - -/// -/// Handles the messages for an owner-drawn track bar. -/// The grip is dragged directly with the mouse and snapped to whatever step the dialog -/// asked for, with the current value optionally printed alongside the track. The parent -/// dialog is notified as the value changes, not merely when the drag ends. -/// -/// Returns with the result of the original window procedure for anything this -/// routine does not handle itself. -LRESULT CALLBACK TrackBarCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - bool play_click = true; - - RECT client_rect; - RECT display_rect; - GetClientRect(window, &client_rect); - Get_Display_Rect(window, &display_rect); - int number_width = 50; - - WinData * data = NULL; - ODWinData.getPointer(window, &data); - - int message_result = data->TrackBar.result; - int dragging = data->TrackBar.dragging; - int range = data->TrackBar.range; - int value = data->TrackBar.value; - int minimum = data->TrackBar.minimum; - int maximum; - int thumb_pos = data->TrackBar.thumbPos; - int step = data->TrackBar.step; - int show_numbers = data->TrackBar.showNumbers; - - if (!show_numbers) { - number_width = 0; - } - - int slider_width = client_rect.right - client_rect.left - number_width - 13; - if (slider_width <= 1) { - slider_width = 1; - } - - if (!range) { - WNDPROC proc = NULL; - OriginalWndProcs.getValue(window, proc); - - int range_max = CallWindowProc(proc, window, TBM_GETRANGEMAX, 0, 0); - minimum = CallWindowProc(proc, window, TBM_GETRANGEMIN, 0, 0); - range = range_max - minimum; - value = CallWindowProc(proc, window, TBM_GETPOS, 0, 0) - minimum; - thumb_pos = value * slider_width / range; - if (!range) { - range = 100; - } - - data->TrackBar.thumbPos = thumb_pos; - data->TrackBar.range = range; - data->TrackBar.value = value; - data->TrackBar.minimum = minimum; - data->TrackBar.step = step; - data->TrackBar.showNumbers = show_numbers; - } - - if (!step) { - number_width = 50; - step = 1; - show_numbers = 1; - } - - LONG grip_left; - LONG grip_right; - if (!dragging) { - grip_left = client_rect.left + thumb_pos + 1; - grip_right = grip_left + 12; - } else { - POINT point; - Get_Logical_Cursor_Pos(window, point); - - int xpos = point.x - 6; - if (xpos < 1) { - xpos = 1; - } - - int max_x = client_rect.right - number_width - 12; - if (max_x < xpos) { - xpos = max_x; - } - - int idx = ((range + 1) * (xpos - 1)) / slider_width; - if (idx >= range) { - idx = range; - } - - value = step * ((minimum + idx) / step) - minimum; - thumb_pos = value * slider_width / range; - grip_left = thumb_pos + 1; - grip_right = grip_left + 12; - } - - switch (message) { - case WM_NCHITTEST: - case WM_GETDLGCODE: { - WNDPROC proc = NULL; - OriginalWndProcs.getValue(window, proc); - return(CallWindowProc(proc, window, message, wparam, lparam)); - } - - case WM_ENABLE: - InvalidateRect(window, NULL, FALSE); - break; - - case WM_PAINT: { - Get_Display_Rect(window, &display_rect); - Rect disp_rect(display_rect.left, display_rect.top, display_rect.right - display_rect.left, display_rect.bottom - display_rect.top); - LONG style = GetWindowLong(window, GWL_STYLE); - - if (data->cachedSurface == NULL) { - RECT disp_copy; - RECT client_copy; - Get_Display_Rect(window, &disp_copy); - GetClientRect(window, &client_copy); - - BSurface * surf = new BSurface(client_copy.right + 1, client_copy.bottom + 1, 2); - data->cachedSurface = surf; - _surface_count++; - - Rect src_rect(0, 0, client_copy.right + 1, client_copy.bottom + 1); - Rect dst_rect(disp_copy.left, disp_copy.top, client_copy.right + 1, client_copy.bottom + 1); - surf->Blit_From(src_rect, *AlternateSurface, dst_rect); - ODFillRectTrans(src_rect, *surf, 0, 180); - } - - if (data->cachedSurface != NULL) { - RECT disp_copy; - RECT client_copy; - Get_Display_Rect(window, &disp_copy); - GetClientRect(window, &client_copy); - - Rect src_rect(0, 0, client_copy.right + 1, client_copy.bottom + 1); - Rect dst_rect(disp_copy.left, disp_copy.top, client_copy.right + 1, client_copy.bottom + 1); - AlternateSurface->Blit_From(dst_rect, *data->cachedSurface, src_rect); - } - - if (show_numbers) { - Surface * center = SurfaceCache.GetSurface("trofm.pcx", NULL); - Rect center_rect; - center_rect.Height = center->Get_Height(); - center_rect.Width = number_width; - center_rect.X = disp_rect.X + disp_rect.Width - number_width; - center_rect.Y = disp_rect.Y; - SurfaceCache.Draw(center_rect, *AlternateSurface, *center, 0, 0); - - Surface * left = SurfaceCache.GetSurface("trofl.pcx", NULL); - Rect left_src(0, 0, left->Get_Width(), left->Get_Height()); - Rect left_dst(disp_rect.X + disp_rect.Width - number_width, disp_rect.Y, left_src.Width, left_src.Height); - AlternateSurface->Blit_From(left_dst, *left, left_src); - - Surface * right = SurfaceCache.GetSurface("trofr.pcx", NULL); - Rect right_src(0, 0, right->Get_Width(), right->Get_Height()); - Rect right_dst(disp_rect.X + disp_rect.Width - right_src.Width, disp_rect.Y, right_src.Width, right_src.Height); - AlternateSurface->Blit_From(right_dst, *right, right_src); - } - - Rect grip_rect(grip_left + display_rect.left, display_rect.top, grip_right - grip_left, display_rect.bottom - display_rect.top); - Surface * grip = SurfaceCache.GetSurface("trakgrip.pcx", NULL); - Rect grip_src(0, 0, grip->Get_Width(), grip->Get_Height()); - AlternateSurface->Blit_From(grip_rect, *grip, grip_src); - - int frame_color; - if (ODColorFrame == -1) { - frame_color = -1; - } else { - frame_color = ODColorToHiColor(ODColorFrame); - } - - if (style & WS_DISABLED) { - frame_color = ODColorDisabled; - if (ODColorDisabled != -1) { - frame_color = ODColorToHiColor(ODColorDisabled); - } - } - - Rect frame_rect(disp_rect.X, disp_rect.Y, disp_rect.Width - number_width, disp_rect.Height); - OD_Draw_Rect(*AlternateSurface, frame_rect, 1, frame_color); - - if (style & WS_DISABLED) { - ODFillRectTrans(disp_rect, *AlternateSurface, 0, 128); - } - - if (show_numbers) { - char buffer[16]; - sprintf(buffer, "%d", step * ((value + minimum) / step)); - - COLORREF text_color = ODColorText; - if (style & WS_DISABLED) { - text_color = ODColorDisabled; - } - - RECT text_rect; - text_rect.left = display_rect.right - 49; - text_rect.top = display_rect.top; - text_rect.right = display_rect.right; - text_rect.bottom = display_rect.bottom; - - OD_Draw_Text_Remap(*AlternateSurface, buffer, *(Rect *)&text_rect, "dlgsys", text_color, 5, 0); - } - - ValidateRect(window, NULL); - break; - } - - case WM_ERASEBKGND: - return(0); - - case WM_MOUSEMOVE: - if (dragging) { - RECT rect = client_rect; - InvalidateRect(window, &rect, FALSE); - } - if (wparam & MK_LBUTTON) { - break; - } - - case WM_LBUTTONUP: - message_result = 0; - dragging = 0; - ReleaseCapture(); - break; - - case WM_LBUTTONDOWN: - case WM_LBUTTONDBLCLK: { - if (message == WM_LBUTTONDOWN) { - message_result = 1; - SetCapture(window); - } else { - message_result = 0; - ReleaseCapture(); - } - - int xpos = (unsigned short)LOWORD(lparam); - int ypos = (unsigned short)HIWORD(lparam); - if (ypos > client_rect.bottom - 18) { - if (xpos >= grip_left && xpos < grip_right) { - if (message == WM_LBUTTONDOWN) { - dragging = 1; - } - } else { - int x = xpos - 6; - if (x < 1) { - x = 1; - } - - int max_x = client_rect.right - number_width - 12; - if (max_x < x) { - x = max_x; - } - - int idx = ((range + 1) * (x - 1)) / slider_width; - if (idx >= range) { - idx = range; - } - - value = step * ((minimum + idx) / step) - minimum; - thumb_pos = value * slider_width / range; - } - } - break; - } - - case TBM_GETPOS: - return(step * ((minimum + value) / step)); - - case TBM_SETPOS: - if (lparam - minimum <= range && lparam - minimum >= 0) { - value = lparam - minimum; - } - play_click = false; - thumb_pos = value * slider_width / range; - break; - - case TBM_SETRANGE: - minimum = (unsigned short)LOWORD(lparam); - maximum = (unsigned short)HIWORD(lparam); - range = maximum - minimum; - if (value > range) { - value = range; - } - if (value < minimum) { - value = minimum; - } - play_click = false; - thumb_pos = value * slider_width / range; - break; - - case OD_SETTRACKSTEP: - step = lparam; - break; - - case OD_TRACKNUMBERS: - show_numbers = lparam; - break; - - case OD_TRACKSILENT: - data->TrackBar.clickSuppress = (wparam == 0); - break; - } - - int changed = 0; - if (value != data->TrackBar.value || range != data->TrackBar.range || minimum != data->TrackBar.minimum) { - changed = 1; - } - - data->TrackBar.result = message_result; - data->TrackBar.dragging = dragging; - data->TrackBar.thumbPos = thumb_pos; - data->TrackBar.range = range; - data->TrackBar.value = value; - data->TrackBar.minimum = minimum; - data->TrackBar.step = step; - data->TrackBar.showNumbers = show_numbers; - - if (changed) { - InvalidateRect(window, NULL, FALSE); - - HWND parent = GetParent(window); - SendMessage(parent, WM_HSCROLL, MAKEWPARAM(TB_THUMBTRACK, (unsigned short)(value + minimum)), (LPARAM)window); - - if (play_click == true && !data->TrackBar.clickSuppress) { - Sound_Effect(Rule->GenericClick); - } - } - - return(0); -} - - -/// -/// Handles the messages for an owner-drawn group box. -/// The frame is drawn as four lines with a gap left in the top edge for the caption, which -/// is written through the surface's device context in the dialog font. -/// -/// Returns with the result of the original window procedure for anything this -/// routine does not paint itself. -LRESULT CALLBACK GroupBoxCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - switch (message) { - case WM_PAINT: { - int locks = 0; - while (((DSurface *)AlternateSurface)->Is_Locked()) { - locks++; - AlternateSurface->Unlock(); - } - - HDC hdc = ((DSurface *)AlternateSurface)->GetDC(); - SelectObject(hdc, ODFontPtr); - SetTextColor(hdc, ODColorText); - SetBkMode(hdc, TRANSPARENT); - - char text[256]; - GetWindowText(window, text, sizeof(text)); - - SIZE text_size; - GetTextExtentPoint32(hdc, text, strlen(text), &text_size); - - RECT rect; - Get_Display_Rect(window, &rect); - - int y = rect.top + text_size.cy / 2; - TextOut(hdc, rect.left + 10, rect.top, text, strlen(text)); - - ((DSurface *)AlternateSurface)->ReleaseDC(hdc); - - while (locks > 0) { - ((DSurface *)AlternateSurface)->Lock(); - locks--; - } - - int color = ODColorToHiColor(ODColorFrame); - - AlternateSurface->Draw_Line(Point2D(rect.left, y), Point2D(rect.left + 8, y), color); - - /// With an empty caption the top edge is one full-width line, so the segment - /// starts at rect.left. Do NOT change this to rect.right: it draws a visibly - /// broken, empty top edge. That change has been made and backed out twice. - int spacing = 12; - if (text_size.cx == 0) { - spacing = 0; - } - - AlternateSurface->Draw_Line(Point2D(text_size.cx + rect.left + spacing, y), Point2D(rect.right, y), color); - AlternateSurface->Draw_Line(Point2D(rect.left, y), Point2D(rect.left, rect.bottom), color); - AlternateSurface->Draw_Line(Point2D(rect.left, rect.bottom), Point2D(rect.right, rect.bottom), color); - AlternateSurface->Draw_Line(Point2D(rect.right, y), Point2D(rect.right, rect.bottom), color); - - ValidateRect(window, NULL); - return(0); - } - - case WM_ERASEBKGND: - return(1); - - case WM_NCPAINT: - return(0); - } - - WNDPROC proc = NULL; - OriginalWndProcs.getValue(window, proc); - return(CallWindowProc(proc, window, message, wparam, lparam)); -} - - -/// -/// Handles the messages for an owner-drawn hotkey control. -/// The key the control currently holds is spelled out by Build_Hotkey_String and drawn -/// inside a border, over a cached copy of the dialog background. -/// -/// Returns with the result of the original window procedure for anything this -/// routine does not paint itself. -LRESULT CALLBACK HotkeyCtrlProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - switch (message) { - case WM_PAINT: { - WinData* data = NULL; - RECT rect; - Get_Display_Rect(window, &rect); - Rect alternate_rect(rect.left, rect.top, rect.right - rect.left + 1, rect.bottom - rect.top + 1); - Rect win_rect(0, 0, alternate_rect.Width, alternate_rect.Height); - ODWinData.getPointer(window, &data); - if (data->cachedSurface == NULL) { - BSurface * surf = new BSurface(alternate_rect.Width, alternate_rect.Height, 2); - data->cachedSurface = surf; - _surface_count++; - surf->Blit_From(win_rect, *AlternateSurface, alternate_rect); - } - - char string[64]; - int key = SendMessage(window, HKM_GETHOTKEY, 0, 0); - Build_Hotkey_String((KeyNumType)key, string); - - if (data->cachedSurface != NULL) { - AlternateSurface->Blit_From(alternate_rect, *data->cachedSurface, win_rect); - } - OD_Draw_Rect(*AlternateSurface, alternate_rect, 1, 0xFFFFFFFF); - if (strlen(string)) { - rect.left += 4; - rect.top += 4; - rect.right -= 4; - rect.bottom -= 4; - ODDrawTextBG(*AlternateSurface, string, &rect, ODFontPtr, ODColorText, DT_SINGLELINE|DT_VCENTER); - } - ValidateRect(window, NULL); - return(0); - } - - case WM_ERASEBKGND: - return(1); - - case WM_NCPAINT: - return(0); - } - - WNDPROC proc = NULL; - OriginalWndProcs.getValue(window, proc); - return(CallWindowProc(proc, window, message, wparam, lparam)); -} - - -/// -/// Converts a key code into its printable name. -/// This routine is used by the hotkey control to show a binding the way the player's own -/// keyboard layout names it, with the modifier names spelled out ahead of the key. -/// -/// The key, complete with its modifier bits, to spell out. -/// Buffer to build the name in. -/// Be sure that the buffer is big enough for the modifier names as well. -int Build_Hotkey_String(KeyNumType key, char * buffer) -{ - char key_name[32]; - unsigned char modifier = HIBYTE(key); - - buffer[0] = '\0'; - - UINT lparam; - - /// (p << 16) - places the scan code into bits 16-23. - /// (1 << 0) - purpose unknown; Windows does not document this bit. - /// (1 << 24) - Extended-key bit. Distinguishes some keys on an enhanced keyboard. - /// (1 << 25) - "Don't care" bit. Should not distinguish between left and right ctrl and shift keys. - - if ((modifier & (WWKEY_ALT_BIT >> 8)) != 0) { - lparam = MapVirtualKey(VK_MENU, 0) ; - lparam = (lparam << 16); - lparam |= (1 << 0); - lparam |= (1 << 25); - GetKeyNameText(lparam, key_name, sizeof(key_name)); - strcat(buffer, key_name); - strcat(buffer, "+"); - } - - if ((modifier & (WWKEY_CTRL_BIT >> 8)) != 0) { - lparam = MapVirtualKey(VK_CONTROL, 0); - lparam = (lparam << 16); - lparam |= (1 << 0); - lparam |= (1 << 25); - GetKeyNameText(lparam, key_name, sizeof(key_name)); - strcat(buffer, key_name); - strcat(buffer, "+"); - } - - if ((modifier & (WWKEY_SHIFT_BIT >> 8)) != 0) { - lparam = MapVirtualKey(VK_SHIFT, 0); - lparam = (lparam << 16); - lparam |= (1 << 0); - lparam |= (1 << 25); - GetKeyNameText(lparam, key_name, sizeof(key_name)); - strcat(buffer, key_name); - strcat(buffer, "+"); - } - - lparam = MapVirtualKey(key & 0xFF, 0); - lparam = (lparam << 16); - lparam |= (1 << 0); - lparam |= (1 << 25); - - if ((modifier & (WWKEY_RLS_BIT >> 8)) != 0) { - lparam |= (1 << 24); - } - - GetKeyNameText(lparam, key_name, sizeof(key_name)); - strcat(buffer, key_name); - - return(0); -} - -unsigned short ODRComponentMask; -unsigned short ODGComponentMask; -unsigned short ODBComponentMask; - - -/// -/// Sets up the color component masks used for blending. -/// The masks depend on how the display surface packs its pixels, so this routine cannot -/// run until the video mode is known. -/// -void ODInitMasks(void) -{ - ODRComponentMask = 255; - ODRComponentMask = ODRComponentMask >> DSurface::Get_Red_Left(); - ODRComponentMask <<= DSurface::Get_Red_Right(); - - ODGComponentMask = 255; - ODGComponentMask = ODGComponentMask >> DSurface::Get_Green_Left(); - ODGComponentMask <<= DSurface::Get_Green_Right(); - - ODBComponentMask = 255; - ODBComponentMask = ODBComponentMask >> DSurface::Get_Blue_Left(); - ODBComponentMask <<= DSurface::Get_Blue_Right(); -} - - -/// -/// Loads the artwork the owner-draw controls are built from. -/// Every button, tab, arrow, grip and check box piece is pulled into the surface cache up -/// front, so that no control has to reach for the disk while a dialog is painting. -/// -void ODCacheImages(void) -{ - SurfaceCache.CachePCX("dbak6440.pcx"); - SurfaceCache.CachePCX("gdii.pcx"); - SurfaceCache.CachePCX("nodi.pcx"); - SurfaceCache.CachePCX("arrow_uu.pcx"); - SurfaceCache.CachePCX("arrow_ud.pcx"); - SurfaceCache.CachePCX("arrow_du.pcx"); - SurfaceCache.CachePCX("arrow_dd.pcx"); - SurfaceCache.CachePCX("leftbar.pcx"); - SurfaceCache.CachePCX("rightbar.pcx"); - SurfaceCache.CachePCX("trakgrip.pcx"); - SurfaceCache.CachePCX("sbgript.pcx"); - SurfaceCache.CachePCX("sbgripm.pcx"); - SurfaceCache.CachePCX("sbgripb.pcx"); - SurfaceCache.CachePCX("bar_ll.pcx"); - SurfaceCache.CachePCX("bar_lr.pcx"); - SurfaceCache.CachePCX("bar_ul.pcx"); - SurfaceCache.CachePCX("bar_ur.pcx"); - SurfaceCache.CachePCX("dlgsysi.pcx", 1); - SurfaceCache.CachePalettedPCX("dlgsysa.pcx"); - SurfaceCache.CachePCX("wouban.pcx"); - SurfaceCache.CachePCX("wodban.pcx"); - SurfaceCache.CachePCX("wouleave.pcx"); - SurfaceCache.CachePCX("wodleave.pcx"); - SurfaceCache.CachePCX("wousqlch.pcx"); - SurfaceCache.CachePCX("wodsqlch.pcx"); - SurfaceCache.CachePCX("woudcon.pcx"); - SurfaceCache.CachePCX("woddcon.pcx"); - SurfaceCache.CachePCX("woukick.pcx"); - SurfaceCache.CachePCX("wodkick.pcx"); - SurfaceCache.CachePCX("wouhelp.pcx"); - SurfaceCache.CachePCX("wodhelp.pcx"); - SurfaceCache.CachePCX("woufind.pcx"); - SurfaceCache.CachePCX("wodfind.pcx"); - SurfaceCache.CachePCX("wouopt.pcx"); - SurfaceCache.CachePCX("wodopt.pcx"); - SurfaceCache.CachePCX("woutrny.pcx"); - SurfaceCache.CachePCX("wodtrny.pcx"); - SurfaceCache.CachePCX("wouclan.pcx"); - SurfaceCache.CachePCX("wodclan.pcx"); - SurfaceCache.CachePCX("woufgame.pcx"); - SurfaceCache.CachePCX("wodfgame.pcx"); - SurfaceCache.CachePCX("wouact.pcx"); - SurfaceCache.CachePCX("wodact.pcx"); - SurfaceCache.CachePCX("wouref.pcx"); - SurfaceCache.CachePCX("wodref.pcx"); - SurfaceCache.CachePCX("tab_tlu.pcx"); - SurfaceCache.CachePCX("tab_tmu.pcx"); - SurfaceCache.CachePCX("tab_tru.pcx"); - SurfaceCache.CachePCX("tab_tld.pcx"); - SurfaceCache.CachePCX("tab_tmd.pcx"); - SurfaceCache.CachePCX("tab_trd.pcx"); - SurfaceCache.CachePCX("tab_ftl.pcx"); - SurfaceCache.CachePCX("tab_ftr.pcx"); - SurfaceCache.CachePCX("tab_ftm.pcx"); - SurfaceCache.CachePCX("tab_fbr.pcx"); - SurfaceCache.CachePCX("tab_fbl.pcx"); - SurfaceCache.CachePCX("tab_fbm.pcx"); - SurfaceCache.CachePCX("tab_fmr.pcx"); - SurfaceCache.CachePCX("tab_fml.pcx"); - SurfaceCache.CachePCX("woloper.pcx"); - SurfaceCache.CachePCX("wolsqlch.pcx"); - SurfaceCache.CachePCX("woltrny.pcx"); - SurfaceCache.CachePCX("woluser.pcx"); - SurfaceCache.CachePCX("wolvoice.pcx"); - SurfaceCache.CachePCX("wolpriv.pcx"); - SurfaceCache.CachePCX("wolacpt.pcx"); - SurfaceCache.CachePCX("wolhost.pcx"); - SurfaceCache.CachePCX("wolclan.pcx"); - SurfaceCache.CachePCX("dnarrowp.pcx"); - SurfaceCache.CachePCX("uparrowp.pcx"); - SurfaceCache.CachePCX("dnarrowr.pcx"); - SurfaceCache.CachePCX("uparrowr.pcx"); - SurfaceCache.CachePCX("trofl.pcx"); - SurfaceCache.CachePCX("trofm.pcx"); - SurfaceCache.CachePCX("trofr.pcx"); - SurfaceCache.CachePCX("sb_psh_u.pcx"); - SurfaceCache.CachePCX("sb_psh_d.pcx"); - SurfaceCache.CachePCX("sb_rel_u.pcx"); - SurfaceCache.CachePCX("sb_rel_d.pcx"); - SurfaceCache.CachePCX("bst_chkd.pcx"); - SurfaceCache.CachePCX("bst_uchk.pcx"); - SurfaceCache.CachePCX("bst_chkg.pcx"); - SurfaceCache.CachePCX("bst_uckg.pcx"); - SurfaceCache.CachePCX("ccd_i.pcx"); - SurfaceCache.CachePCX("cce_i.pcx"); - SurfaceCache.CachePCX("cud_i.pcx"); - SurfaceCache.CachePCX("cue_i.pcx"); - SurfaceCache.CachePCX("bue_li30.pcx"); - SurfaceCache.CachePCX("bue_mi30.pcx"); - SurfaceCache.CachePCX("bue_ri30.pcx"); - SurfaceCache.CachePCX("bde_li30.pcx"); - SurfaceCache.CachePCX("bde_mi30.pcx"); - SurfaceCache.CachePCX("bde_ri30.pcx"); - SurfaceCache.CachePCX("bud_li30.pcx"); - SurfaceCache.CachePCX("bud_mi30.pcx"); - SurfaceCache.CachePCX("bud_ri30.pcx"); - SurfaceCache.CachePCX("bue_li24.pcx"); - SurfaceCache.CachePCX("bue_mi24.pcx"); - SurfaceCache.CachePCX("bue_ri24.pcx"); - SurfaceCache.CachePCX("bde_li24.pcx"); - SurfaceCache.CachePCX("bde_mi24.pcx"); - SurfaceCache.CachePCX("bde_ri24.pcx"); - SurfaceCache.CachePCX("bud_li24.pcx"); - SurfaceCache.CachePCX("bud_mi24.pcx"); - SurfaceCache.CachePCX("bud_ri24.pcx"); -} - - -/// -/// Draws a single blended line. -/// This is the low level routine behind the frames the owner-draw controls are built from. -/// Every pixel along the line is blended toward the color rather than replaced, so the -/// dialog artwork still shows through the frame. -/// -/// The surface to draw upon. -/// One end of the line. -/// The other end of the line. -/// The raw color value to blend toward. -/// The blend strength, 0 through 255. -/// bool; Was the line drawn? -bool ODDrawEdgeGlow(Surface & surf, Point2D const & start, Point2D const & end, int color, unsigned char steps) -{ - Point2D startpoint = start; - Point2D endpoint = end; - - if (startpoint.X > endpoint.X) { - std::swap(startpoint, endpoint); - } - - int bpp = surf.Bytes_Per_Pixel(); - void * buffer = surf.Lock(startpoint); - if (buffer != NULL) { - unsigned short blend_color = (unsigned short)color; - - if (startpoint.Y == endpoint.Y) { - /* - * Simplest of the blits, straight horizontal line. - */ - if (bpp == 1) { - memset(buffer, color, endpoint.X - startpoint.X + 1); - } else { - for (int i = 0; i <= endpoint.X - startpoint.X; i++) { - *((unsigned short *)buffer + i) = OD_Blend_Color(*((unsigned short *)buffer + i), blend_color, steps); - } - } - } else if (startpoint.X == endpoint.X) { - int pitch = startpoint.Y > endpoint.Y ? -surf.Stride() : surf.Stride(); - - /* - * Straight vertical line. - */ - int dy = abs(endpoint.Y - startpoint.Y); - for (int i = 0; i <= dy; i++) { - if (bpp == 1) { - *(unsigned char *)buffer = color; - } else { - *(unsigned short *)buffer = OD_Blend_Color(*(unsigned short *)buffer, blend_color, steps); - } - buffer = (unsigned char *)buffer + pitch; - } - } else { - /* - * Distances to x and y. - */ - int dx = endpoint.X - startpoint.X; - int dy = endpoint.Y - startpoint.Y; - /* - * The line isn't straight so we need to do some maths. - */ - int pitch = surf.Stride(); - if (dy < 0) { - pitch = -pitch; - } - - dy = abs(dy); - int dx2 = 2 * dx; - int dy2 = 2 * dy; - - if (dx > dy) { - /* - * The slope is not steep. - */ - int delta = dy2 - dx; - - /* - * Plot low line. - */ - for (int i = 0; i < dx; i++) { - if (bpp == 1) { - *((unsigned char *)buffer + i) = color; - } else { - *((unsigned short *)buffer + i) = OD_Blend_Color(*((unsigned short *)buffer + i), blend_color, steps); - } - - if (delta > 0) { - buffer = (unsigned char *)buffer + pitch; - delta -= dx2; - } - - delta += dy2; - } - } else { - /* - * The slope is steep. - */ - int delta = dx2 - dy; - int k = 0; - - /* - * Plot high line. - */ - for (int i = 0; i < dy; i++) { - if (bpp == 1) { - *((unsigned char *)buffer + k) = color; - } else { - *((unsigned short *)buffer + k) = OD_Blend_Color(*((unsigned short *)buffer + k), blend_color, steps); - } - - if (delta > 0) { - k++; - delta -= dy2; - } - - delta += dx2; - buffer = (unsigned char *)buffer + pitch; - } - } - } - - surf.Unlock(); - return(true); - } - return(false); -} - - -/// -/// Draws a blended frame around a rectangle. -/// Each of the four edges is blended with its own strength, which is what gives a control -/// its raised or sunken look. Every corner pixel is left to a single edge so that no pixel -/// is blended twice and shows up darker than its neighbors. -/// -/// The surface to draw upon. -/// The rectangle to frame. -/// Should the frame appear raised rather than sunken? -/// How many nested frames to draw, working inward. -/// The blend strength for the left edge. -/// The blend strength for the top edge. -/// The blend strength for the right edge. -/// The blend strength for the bottom edge. -void ODDrawEdgeGlows(Surface & surface, Rect const & rect, BOOL raised, int count, int left_alpha, int top_alpha, int right_alpha, int bottom_alpha) -{ - /* - * The function flips these when the frame is sunken, which is how callers - * choose between a raised vs. sunken style (i.e., highlight/shadow swapped). - */ - - int color2 = 0xFFFF; // used on top & left edges (default: lighter) - int color1 = 0; // used on bottom & right edges (default: darker) - - /* - * When the frame is sunken, invert highlight/shadow: - * - top/left become darker - * - bottom/right become lighter - */ - if (!raised) { - color1 = 0xFFFF; - color2 = 0; - } - - for (int i = 0; i < count; i++) { - - Point2D end1; - Point2D start1; - Point2D end2; - Point2D start2; - Point2D end3; - Point2D start3; - Point2D end4; - Point2D start4; - - /* - * Top edge (left -> right). The "-2" keeps this top line from touching the - * top-right corner pixel, avoiding overdraw with the right edge drawn below. - */ - end1.X = rect.Width - i + rect.X - 2; - start1.X = i + rect.X; - end1.Y = i + rect.Y; - start1.Y = i + rect.Y; - ODDrawEdgeGlow(surface, start1, end1, color2, top_alpha); - - /* - * Bottom edge (left -> right). - */ - end2.X = rect.Width - i + rect.X - 1; - start2.X = i + rect.X; - end2.Y = rect.Y - i + rect.Height - 1; - start2.Y = end2.Y; - ODDrawEdgeGlow(surface, start2, end2, color1, bottom_alpha); - - /* - * Left edge (top -> bottom). The "+1" on the top avoids double-hitting the - * top-left corner pixel (the top edge already handled it). - */ - end3.X = rect.X + i; - start3.X = end3.X; - start3.Y = rect.Y + i + 1; - end3.Y = rect.Y - i + rect.Height - 1; - ODDrawEdgeGlow(surface, start3, end3, color2, left_alpha); - - /* - * Right edge (top -> bottom). The "-2" on the bottom avoids double-hitting - * the bottom-right corner pixel (the bottom edge already handled it). - */ - end4.X = rect.X - i + rect.Width - 1; - start4.X = end4.X; - start4.Y = i + rect.Y; - end4.Y = rect.Y - i + rect.Height - 2; - ODDrawEdgeGlow(surface, start4, end4, color1, right_alpha); - } -} - - -/// -/// Draws a scroll arrow bitmap. -/// The image is picked out of the surface cache by direction and press state, and drawn at -/// its own size from the top left corner of the rectangle given. -/// -/// The surface to draw upon. -/// The area whose top left corner the arrow is drawn from. -/// Should the upward pointing arrow be used? -/// Is the arrow button currently held down? -void ODDrawArrowBitmap(Surface & surface, Rect const & rect, BOOL upward, BOOL pressed) -{ - char fname[32]; - - char state = 'r'; - if (pressed) { - state = 'p'; - } - - if (upward) { - sprintf(fname, "uparrow%c.pcx", state); - } else { - sprintf(fname, "dnarrow%c.pcx", state); - } - - Surface * arrowSurface = SurfaceCache.GetSurface(fname); - - Rect destRect = rect; - destRect.Width = arrowSurface->Get_Width(); - destRect.Height = arrowSurface->Get_Height(); - - Rect srcRect; - srcRect.Y = 0; - srcRect.X = 0; - srcRect.Width = arrowSurface->Get_Width(); - srcRect.Height = arrowSurface->Get_Height(); - - surface.Blit_From(destRect, *arrowSurface, srcRect); -} - - -/// -/// Converts a Windows color reference into a display pixel. -/// The dialog colors are all written as RGB() values, so they have to be packed into the -/// pixel layout of the display surface before anything can be drawn with them. -/// -/// Returns with the packed pixel value. An all-ones color is passed through -/// unchanged. -int ODColorToHiColor(COLORREF color) -{ - if (color == 0xFFFFFFFF) { - return(0xFFFFFFFF); - } - /// Do not replace the union with direct byte extraction. It improves several callers and - /// breaks ProgressBarCtrlProc, which is otherwise exact -- and an exact caller outranks the - /// partial ones. - union { - struct { - unsigned int red : 8; - unsigned int green : 8; - unsigned int blue : 8; - unsigned int a : 8; - }; - int v; - } c; - - c.v = color; - - return(DSurface::Build_Hicolor_Pixel(c.red, c.green, c.blue)); -} - - -/// -/// Draws a rectangular outline around an area. -/// The outline is drawn outside the rectangle given, thickening outward as the offset -/// grows. Use this routine for the plain frames the owner-draw controls sit inside. -/// -/// How far beyond the rectangle, in pixels, the outline reaches. -/// The raw color to draw with, or -1 for the common frame color. -void OD_Draw_Rect(Surface & surf, Rect const & rect, int offset, int color) -{ - if (color == -1) { - color = ODColorToHiColor(ODColorFrame); - } - - Rect work; - work.X = rect.X - offset; - work.Y = rect.Y - offset; - work.Width = 2 * offset + rect.Width; - work.Height = 2 * offset + rect.Height; - - for (int i = 0; i < offset; i++) { - - Point2D end1; - Point2D start1; - Point2D end2; - Point2D start2; - Point2D end3; - Point2D start3; - Point2D end4; - Point2D start4; - - /* - * The same eight-point ring that ODDrawEdgeGlows walks. - */ - end1.X = work.Width - i + work.X - 2; - start1.X = i + work.X; - end1.Y = i + work.Y; - start1.Y = i + work.Y; - surf.Draw_Line(start1, end1, color); - - end2.X = work.Width - i + work.X - 1; - start2.X = i + work.X; - end2.Y = work.Y - i + work.Height - 1; - start2.Y = end2.Y; - surf.Draw_Line(start2, end2, color); - - end3.X = work.X + i; - start3.X = end3.X; - start3.Y = work.Y + i + 1; - end3.Y = work.Y - i + work.Height - 1; - surf.Draw_Line(start3, end3, color); - - end4.X = work.X - i + work.Width - 1; - start4.X = end4.X; - start4.Y = i + work.Y; - end4.Y = work.Y - i + work.Height - 2; - surf.Draw_Line(start4, end4, color); - } -} - - -/// -/// Draws the bevelled border of an owner-draw button. -/// Each of the four edges is drawn in its own shade of green, so that the border reads as -/// a raised frame rather than a flat outline. The frame is drawn outside the rectangle. -/// -/// How far beyond the rectangle, in pixels, the border reaches. -void ODDrawButtonRect(Surface & surf, Rect const & rect, int offset) -{ - Rect work = rect; - - int color1 = DSurface::Build_Hicolor_Pixel(39, 248, 116); - int color2 = DSurface::Build_Hicolor_Pixel(19, 123, 57); - int color3 = DSurface::Build_Hicolor_Pixel(30, 186, 87); - int color4 = DSurface::Build_Hicolor_Pixel(24, 153, 71); - - work.X += -1 - offset; - work.Y += -1 - offset; - work.Width += 2 * offset + 2; - int y2 = 2 * offset + 2 + work.Height; - - for (int i = 0; i < offset; i++) { - - Point2D end1; - Point2D start1; - Point2D end2; - Point2D start2; - Point2D end3; - Point2D start3; - Point2D end4; - Point2D start4; - - /* - * The same eight-point ring as OD_Draw_Rect / ODDrawEdgeGlows, but each - * edge gets its own shade so the border reads as a bevelled button. - * r.Height is never written back - y2 carries the grown height. - */ - end1.X = work.Width - i + work.X - 2; - start1.X = i + work.X; - end1.Y = i + work.Y; - start1.Y = i + work.Y; - surf.Draw_Line(start1, end1, color1); - - end2.X = work.Width - i + work.X - 1; - start2.X = i + work.X; - end2.Y = work.Y - i + y2 - 1; - start2.Y = end2.Y; - surf.Draw_Line(start2, end2, color2); - - end3.X = work.X + i; - start3.X = end3.X; - start3.Y = work.Y + i + 1; - end3.Y = work.Y - i + y2 - 1; - surf.Draw_Line(start3, end3, color3); - - end4.X = work.X - i + work.Width - 1; - start4.X = end4.X; - start4.Y = i + work.Y; - end4.Y = work.Y - i + y2 - 2; - surf.Draw_Line(start4, end4, color4); - } -} - - -/// -/// Draws text onto a surface with the Windows text formatter. -/// This routine borrows a device context from the surface, unlocking it as often as it -/// must beforehand, and lets Windows lay the string out within the rectangle given. -/// -/// The DrawText formatting flags to lay the string out with. -/// Returns with the pixel width of the string. -int ODDrawTextBG(Surface & surface, LPCSTR string, LPRECT rect, HGDIOBJ font, COLORREF color, UINT format) -{ - int locks = 0; - - while (((DSurface &)surface).Is_Locked()) { - locks++; - surface.Unlock(); - } - - HDC hdc = ((DSurface &)surface).GetDC(); - - SelectObject(hdc, font); - SetTextColor(hdc, color); - SetBkMode(hdc, TRANSPARENT); - - SIZE char_size; - GetTextExtentPoint32(hdc, string, strlen(string), &char_size); - DrawText(hdc, string, strlen(string), rect, format); - - ((DSurface &)surface).ReleaseDC(hdc); - - while (locks > 0) { - ((DSurface &)surface).Lock(); - locks--; - } - - return(char_size.cx); -} - - -/// -/// Draws word wrapped text with a remapped bitmap font. -/// This routine breaks the text into lines that will fit the rectangle -- honoring the -/// newlines already in it and breaking at a space wherever one can be found -- and hands -/// each line in turn to ODDrawCharRemap. -/// -/// The base name of the font sheets to draw with. -/// The OD_DRAW_CHAR alignment flags to lay each line out with. -/// The extra spacing to insert between characters. -int OD_Draw_Text_Remap(Surface & surface, const char * text, Rect const & rect, char const * name, COLORREF color, int flags, int char_spacing) -{ - int line_len = strlen(text); - char const * line_ptr = text; - Rect draw_rect = rect; - - FontMetrics data; - if (!ODGetFontMetrics(name, &data)) { - return(0); - } - - while (line_len) { - if (line_ptr) { - char const * nl_ptr = strchr(line_ptr, '\n'); - if (nl_ptr) { - int nl_len = (int)(nl_ptr - line_ptr) + 1; - if (line_len >= nl_len) { - line_len = nl_len; - } - } - } - - if ((unsigned char)*line_ptr <= ' ') { - ++line_ptr; - if (--line_len == 0) { - return(0); - } - } - - int text_width = 0; - for (char const * cursor = text; cursor - text < line_len; ) { - text_width += char_spacing + data.charWidths[OD_Glyph(UTF8::Decode(cursor))]; - } - - if (text_width > draw_rect.Width - draw_rect.X) { - int fallback = (int)UTF8::Boundary_Before(line_ptr, line_len - 1); - int cut = line_len - 1; - - flags &= ~4; - - while (cut > 0) { - if ((unsigned char)line_ptr[cut] <= ' ') { - break; - } - --cut; - } - if (cut > 0) { - line_len = cut; - if (cut != -1) { - continue; - } - } - - line_len = fallback; - } else { - ODDrawCharRemap(surface, line_ptr, line_len, draw_rect, name, color, (char)flags, char_spacing); - line_ptr += line_len; - draw_rect.Y += data.glyphHeight; - line_len = strlen(line_ptr); - } - } - - return(0); -} - - -/// -/// Determines how strongly a hue should be remapped. -/// The font remapper uses this to pull its hue shift back around the primary colors, so -/// that text tinted near one of them does not swing away from the color asked for. -/// -/// The hue to compute the factor for. -/// Returns with the scale factor; the nearer the hue sits to a primary, the smaller -/// it gets. -float ODCalcTextRemapFactor(int hue) -{ - float val = 1.0f; - - int arr[3]; - arr[0] = 43; - arr[1] = 128; - arr[2] = 213; - - for (int i = 0; i < 3; i++) { - int value = arr[i]; - - if (hue > value - 16 && hue <= value) { - val = float(value - hue); - val *= (1.0f / 16); - val *= (60.0f / 100); - val += (40.0f / 100); - } else if (hue > value && hue <= value + 16) { - val = float(hue - value); - val *= (1.0f / 16); - val *= (60.0f / 100); - val += (40.0f / 100); - } - } - return(val); -} - - -/// -/// Draws a line of text with a remapped bitmap font. -/// This routine builds a table that shifts the font's own palette toward the color asked -/// for and then alpha blends each character onto the destination surface. It is the low -/// level draw that all of the owner-draw remapped text ends up going through. -/// -/// The maximum number of characters of the text to draw. -/// The rectangle to align the text within. -/// The base name of the font sheets to draw with. -/// The OD_DRAW_CHAR alignment flags to lay the text out with. -/// The extra spacing to insert between characters. -void ODDrawCharRemap(Surface & dst_surf, const char *text, int max_chars, Rect const & rect, char const *font_name, COLORREF color, char flags, int char_spacing) -{ - int i; - Rect draw_rect = rect; - - char name_i[64]; - strcpy(name_i, font_name); - strcat(name_i, "i.pcx"); - - char palette[768]; - Surface *sheet_i = SurfaceCache.GetSurface(name_i, palette); - if (sheet_i == NULL) { - return; - } - - char name_a[64]; - strcpy(name_a, font_name); - strcat(name_a, "a.pcx"); - - Surface *sheet_a = SurfaceCache.GetSurface(name_a, NULL); - if (sheet_a == NULL) { - return; - } - - RGBClass remap_rgb((unsigned char)color, (unsigned char)(color >> 8), (unsigned char)(color >> 16)); - HSVClass remap_hsv = remap_rgb; - RGBClass pal_rgb; - HSVClass out_hsv; - - int hue = remap_hsv.Get_Hue(); - - int end = int(hue + 15.0); - float min_factor = 1.0f; - for (i = int(hue - 15.0); i <= end; ++i) { - float factor = ODCalcTextRemapFactor(i); - if (factor < min_factor) { - min_factor = factor; - } - } - - unsigned char sat = (unsigned char)remap_hsv.Get_Saturation(); - unsigned char val = (unsigned char)remap_hsv.Get_Value(); - - unsigned short remap_table[256]; - float hue_float = (float)hue; - unsigned char *pal = (unsigned char *)&palette; - for (i = 0; i < 256; ++i) { - pal_rgb.Set_Red(pal[0]); - pal_rgb.Set_Green(pal[1]); - pal_rgb.Set_Blue(pal[2]); - HSVClass pal_hsv = pal_rgb; - - /* - * Start from the palette entry's HSV and adjust each channel. The - * wholesale copy is fully overwritten below. - */ - out_hsv = pal_hsv; - out_hsv.Set_Hue((unsigned char)(int)(hue_float - (int)(68.0f - pal_hsv.Get_Hue()) * min_factor)); - out_hsv.Set_Saturation((unsigned char)((sat * out_hsv.Get_Saturation()) >> 8)); - out_hsv.Set_Value((unsigned char)((val * out_hsv.Get_Value()) >> 8)); - - RGBClass out_rgb = out_hsv; - pal_rgb = out_rgb; - - int packed = (((out_rgb.Get_Blue() << 8) | out_rgb.Get_Green()) << 8) | out_rgb.Get_Red(); - remap_table[i] = (unsigned short)ODColorToHiColor(packed); - pal += 3; - } - - FontMetrics font_data; - if (!ODGetFontMetrics(font_name, &font_data)) { - return; - } - - if ((int)strlen(text) < max_chars) { - max_chars = strlen(text); - } - - int total_width = 0; - for (char const * cursor = text; cursor - text < max_chars; ) { - total_width += font_data.charWidths[OD_Glyph(UTF8::Decode(cursor))] + char_spacing; - } - - if ((flags & OD_DRAW_CHAR_FLAG_HORIZONTAL_CENTER) != 0) { - draw_rect.X += (draw_rect.Width - draw_rect.X - total_width) / 2; - } else if ((flags & OD_DRAW_CHAR_ALIGN_FLAG_RIGHT) != 0) { - draw_rect.X = draw_rect.Width - total_width - 1; - } - - if ((flags & OD_DRAW_CHAR_FLAG_VERTICAL_CENTER) != 0) { - draw_rect.Y = draw_rect.Y + (draw_rect.Height - font_data.glyphHeight - draw_rect.Y) / 2; - } - - draw_rect.Y -= font_data.topMargin; - --draw_rect.X; - - unsigned char *src_i = (unsigned char *)sheet_i->Lock(); - unsigned char *src_a = (unsigned char *)sheet_a->Lock(); - unsigned char *dst = (unsigned char *)dst_surf.Lock(); - - if (src_i != NULL && src_a != NULL && dst != NULL) { - int cell_w = font_data.glyphWidth + font_data.leftMargin; - int cell_h = font_data.glyphHeight + font_data.topMargin; - int chars_per_row = sheet_i->Get_Width() / (font_data.glyphWidth + font_data.leftMargin); - int dst_stride = dst_surf.Stride() / 2; - int src_stride = sheet_i->Stride(); - - int x = draw_rect.X; - for (char const * cursor = text; cursor - text < max_chars; ) { - - unsigned char index = OD_Glyph(UTF8::Decode(cursor)); - if (index <= ' ') { - x += font_data.charWidths[index] + char_spacing; - } else { - int glyph = index + 1; - int src_x = (glyph % chars_per_row) * cell_w; - int src_y = (glyph / chars_per_row) * cell_h; - - int src_y_end = src_y + cell_h; - int src_delta = src_i - src_a; - unsigned char *alpha_col = src_a + (src_y * src_stride + src_x); - unsigned char *dst_col = dst + 2 * (dst_stride * draw_rect.Y + x); - - for (int sx = src_x; sx < src_x + cell_w; ++sx) { - if (src_y < src_y_end) { - unsigned short *dst_px = (unsigned short *)dst_col; - unsigned char *alpha_px = alpha_col; - - int sy = src_y_end - src_y; - do { - unsigned char alpha = *alpha_px; - if (alpha != 0) { - unsigned char index = alpha_px[src_delta]; - *dst_px = OD_Blend_Color(*dst_px, remap_table[index], alpha); - } - - dst_px += dst_stride; - alpha_px += src_stride; - --sy; - } while (sy != 0); - } - - ++alpha_col; - dst_col += 2; - } - - x += font_data.charWidths[index] + char_spacing; - } - } - } - - if (&dst_surf != NULL) { - dst_surf.Unlock(); - } - sheet_a->Unlock(); - sheet_i->Unlock(); -} - - -/// -/// Fetches the metrics of a remappable bitmap font. -/// This routine measures the font's sheet -- the margins, the size of a character cell and -/// the inked width of every character -- so that the remap text routines know how to lay -/// characters out. Measuring is expensive, so the result is kept by font name. -/// -/// The base name of the font, without the sheet suffix. -/// Buffer to fill in with the measurements. -/// bool; Were the metrics available? -bool ODGetFontMetrics(char const * font_name, FontMetrics * metrics) -{ - static Dictionary metricsDict(Wstring_Hash); - - char buf[64]; - strcpy(buf, font_name); - strcat(buf, "a.pcx"); - - Wstring name; - name = (char *)font_name; - name.toLower(); - - FontMetrics * found = NULL; - if (metricsDict.getPointer(name, &found)) { - if (metrics != NULL) { - *metrics = *found; - return(true); - } - } - - DebugString("TS: Computing font metrics....\n"); - - FontMetrics temp; - memset(&temp, 0, sizeof(temp)); - - char palette[768]; - Surface * surf = SurfaceCache.GetSurface(buf, palette); - if (surf == NULL) { - return(false); - } - - char * basePtr = (char *)surf->Lock(); - int stride = surf->Stride(); - - /* - * ---------------------------------------------------------------- - * Vertical metrics: topMargin = blank rows above the glyph row, - * glyphHeight = inked rows (probed at column 4). - * ---------------------------------------------------------------- - */ - temp.topMargin = 0; - while (temp.topMargin < surf->Get_Height()) { - if (basePtr[stride * temp.topMargin + 4] != 0) break; - ++temp.topMargin; - } - int y = temp.topMargin; - while (y < surf->Get_Height()) { - if (basePtr[stride * y + 4] == 0) break; - ++y; - ++temp.glyphHeight; - } - - /* - * ---------------------------------------------------------------- - * Horizontal metrics: leftMargin = blank columns before the glyphs, - * glyphWidth = inked columns (probed along row 'top'). - * ---------------------------------------------------------------- - */ - temp.leftMargin = 0; - while (temp.leftMargin < surf->Get_Width()) { - if (basePtr[stride * temp.topMargin + temp.leftMargin] != 0) break; - ++temp.leftMargin; - } - int left = temp.leftMargin; - - int x; - x = left; - while (x < surf->Get_Width()) { - if (basePtr[stride * temp.topMargin + x] == 0) break; - ++x; - ++temp.glyphWidth; - } - - /* - * ---------------------------------------------------------------- - * Compute per-character metrics - * ---------------------------------------------------------------- - */ - int width = surf->Get_Width(); - int charsPerRow = width / (left + temp.glyphWidth); - for (int ch = 0; ch < 256; ++ch) { - - int left = temp.leftMargin; - int fontHeight = temp.glyphHeight; - int top = temp.topMargin; - int fontWidth = temp.glyphWidth; - - int glyphY = top + (fontHeight + top) * ((ch + 1) / charsPerRow); - int glyphX = left + (left + fontWidth) * ((ch + 1) % charsPerRow); - - int first = -1; - int last = 0; - - for (int x = glyphX; x < glyphX + fontWidth; ++x) { - int nonEmpty = 0; - for (int y = glyphY; y < glyphY + fontHeight; ++y) { - if (basePtr[stride * y + x] != 0) ++nonEmpty; - } - if (nonEmpty) { - last = x; - if (first == -1) first = x; - } - } - - if (first != -1) { - temp.charWidths[ch] = (last - first + 1); - } else { - temp.charWidths[ch] = (fontWidth / 3 + 1); - } - } - - surf->Unlock(); - - /* - * ---------------------------------------------------------------- - * Store result in caller's buffer - * ---------------------------------------------------------------- - */ - memcpy(metrics, &temp, sizeof(FontMetrics)); - - metricsDict.add(name, temp); - - return(true); -} - - -/// -/// Draws a line of text onto a surface. -/// This routine borrows a device context from the surface, unlocking it as often as it -/// must beforehand, and lets Windows put the text out aligned within the rectangle given. -/// Nothing is drawn while the game does not hold the focus. -/// -/// The number of characters of the text to draw. -/// The surface to draw upon, or NULL to draw on the alternate -/// surface. -/// Returns with the pixel width of the text. -int OD_Draw_Text(COLORREF color, HFONT font, Rect const & rect, const char * text, int len, int x_alignment, int y_alignment, Surface * surface) -{ - if (!GameInFocus && !WindowedMode) { - return(0); - } - - DSurface *destsurf = (DSurface *)surface; - if (!surface) { - destsurf = (DSurface *)AlternateSurface; - } - - int lock_count = 0; - while (destsurf->Is_Locked()) { - lock_count++; - destsurf->Unlock(); - } - - SIZE text_size; - - HDC hDC = destsurf->GetDC(); - if (hDC) { - - if (font) { - SelectObject(hDC, font); - } - - SetTextColor(hDC, color); - SetBkMode(hDC, TRANSPARENT); - - GetTextExtentPoint32(hDC, text, len, &text_size); - - int x_offset = rect.X; - int y_offset = rect.Y; - - if (x_alignment == OD_TEXT_ALIGN_MIN) { - x_offset += (rect.Width - text_size.cx + 1) / 2; - } else if (x_alignment == OD_TEXT_ALIGN_CENTER) { - x_offset += (text_size.cx + 1) / -2; - } else if (x_alignment == OD_TEXT_ALIGN_MAX) { - x_offset += -1 - text_size.cx; - } - - if (y_alignment == OD_TEXT_ALIGN_MIN) { - y_offset += (rect.Height - text_size.cy + 1) / 2; - } else if (y_alignment == OD_TEXT_ALIGN_CENTER) { - y_offset += (text_size.cy + 1) / -2; - } else if (y_alignment == OD_TEXT_ALIGN_MAX) { - y_offset += -1 - text_size.cy; - } - - TextOut(hDC, x_offset, y_offset, text, len); - destsurf->ReleaseDC(hDC); - } else { - text_size.cx = 0; - } - - while (lock_count) { - destsurf->Lock(); - lock_count--; - } - - return(text_size.cx); -} - - -/// -/// Handles the owner-draw item message for a control. -/// The controls paint themselves through the game's own surfaces rather than through a -/// device context, so this routine only records the item state that Windows handed over -/// and then forces the control to repaint. -/// -void OwnerDraw::Draw_Item(LPDRAWITEMSTRUCT drawit) -{ - if (VisibleSurface != NULL && AlternateSurface != NULL) { - RECT rect1; - Get_Display_Rect(drawit->hwndItem, &rect1); - RECT rect2; - GetClientRect(drawit->hwndItem, &rect2); - - if (GetWindowLong(drawit->hwndItem, GWL_STYLE) & WS_BORDER) { - int x = GetSystemMetrics(SM_CXBORDER); - int y = GetSystemMetrics(SM_CYBORDER); - rect1.left += x; - rect1.right -= x; - rect1.top += y; - rect1.bottom -= y; - } - - if (drawit->CtlType == ODT_BUTTON) { - WinData * data = NULL; - HWND window = drawit->hwndItem; - - ODWinData.getPointer(window, &data); - - data->DrawItem.itemState = drawit->itemState; - - InvalidateRect(window, &drawit->rcItem, FALSE); - UpdateWindow(window); - } - } -} - - -/// -/// Draws a dimmed copy of the parent's background behind a control. -/// This routine takes the piece of the parent window that the control covers, darkens it -/// and keeps the result on a surface cached against the control, giving the control a -/// smoked glass look. Later calls simply blit the cached copy. -/// -void ODDrawDimmedBackground(Rect const & rect, HWND hWnd) -{ - WinData * winData = NULL; - WinData * parentWinData = NULL; - - ODWinData.getPointer(hWnd, &winData); - - RECT rcClient; - GetClientRect(hWnd, &rcClient); - - RECT dispChild; - Get_Display_Rect(hWnd, &dispChild); - - Surface * surface = winData->cachedSurface; - if (surface == NULL) { - surface = new BSurface(rcClient.right + 1, rcClient.bottom + 1, 2); - winData->cachedSurface = surface; - ++_surface_count; - - Rect dstFull(0, 0, rcClient.right + 1, rcClient.bottom + 1); - - HWND parent = GetParent(hWnd); - ODWinData.getPointer(parent, &parentWinData); - - Surface * parentSurf = parentWinData->cachedSurface; - if (parentSurf) { - RECT dispParent; - Get_Display_Rect(parent, &dispParent); - - Rect srcRel; - srcRel.X = dispChild.left - dispParent.left; - srcRel.Y = dispChild.top - dispParent.top; - srcRel.Width = RECT_WIDTH(dispChild) + 1; - srcRel.Height = RECT_HEIGHT(dispChild) + 1; - - surface->Blit_From(dstFull, *parentSurf, srcRel); - } else { - Surface *srcSurf = VisibleSurface; - - Rect srcAbs; - srcAbs.X = dispChild.left; - srcAbs.Y = dispChild.top; - srcAbs.Width = RECT_WIDTH(dispChild) + 1; - srcAbs.Height = RECT_HEIGHT(dispChild) + 1; - if (srcSurf) { - surface->Blit_From(dstFull, *srcSurf, srcAbs); - } - } - - unsigned short* surfptr = (unsigned short*)surface->Lock(); - if (surfptr) { - for (int i = 0; i < surface->Get_Width() * surface->Get_Height(); ++i) { - surfptr[i] = OD_Blend_Color(surfptr[i], 0, 180); - } - - surface->Unlock(); - } - } - - Rect src; - src.Width = rect.Width; - src.Height = rect.Height; - src.Y = rect.Y - dispChild.top; - src.X = rect.X - dispChild.left; - - AlternateSurface->Blit_From(rect, *surface, src); -} - - -/// -/// Draws a rectangle that fades in from left to right. -/// The fill runs from the left edge as far as the progress value asks for, blending into -/// whatever is already on the surface instead of overwriting it. This is what gives the -/// progress bar its soft leading edge. -/// -/// The fill position, as a 16.16 fraction of the rectangle width. -void ODDrawGradientRect(Rect const & rect, Surface & surface, int color, int progress) -{ - int fade = 1; - int run = (rect.Width * progress) >> 16; - - if (run < 0) return; - if (run == 0) run = 1; - - unsigned short * surfptr = (unsigned short*)surface.Lock(); - if (!surfptr) return; - - for (int row = 0; row < rect.Height; row++) { - int idx = rect.X + (rect.Y + row) * (surface.Stride() / 2); - - int q_div4 = rect.Height / 4; /// a quarter of the height, rounded down - if (row == (q_div4 * 3)) { - fade = 1; - } - if (row == q_div4) { - fade = 0; - } - - if (run > 0) { - unsigned short * pixptr = &surfptr[idx]; - for (int i = 0; i < run; i++) { - if (fade) { - pixptr[i] = OD_Blend_Color(pixptr[i], color, (255 * (i + 1)) / rect.Width); - } else { - pixptr[i] = color; - } - } - } - } - - surface.Unlock(); -} - - -/// -/// Darkens the left and top edges of a rectangle. -/// This routine halves the intensity of every pixel it covers, which is what gives the -/// tooltip frame its shadowed bevel. -/// -/// The width of the darkened band down the left edge. -/// The height of the darkened band across the top edge. -void ODDrawBevelDarken(Rect const & rect, Surface & surface, int xpos, int ypos) -{ - unsigned short * surfptr = (unsigned short *)surface.Lock(); - - if (surfptr != NULL) { - - int stride = surface.Stride() / 2; - - /* - * The half-intensity mask is respelled at each store with per-component - * drift: the green term reuses one load of its mask, the red term's - * second read carries a cast (defeating the load reuse), and the blue - * term is missing the "& mask" entirely (harmless, since blue is the - * low bit field). - * NOTE: this is NOT OD_Blend_Color(px, 0, ...) - the blend helper goes - * through the alpha statics with multiplies and a >>8 (visible at the - * real blend-to-black sites); the binary here has only the shift/mask - * arithmetic, and the per-component drift proves it was hand-written. - */ - int y; - for (y = rect.Y; y < rect.Y + rect.Height; ++y) { - int index = y * stride + rect.X; - for (int x = rect.X; x < rect.X + xpos; ++x) { - unsigned short px = surfptr[index]; - - surfptr[index] = (px >> 1) & (((ODRComponentMask >> 1) & (unsigned int)ODRComponentMask) | ((ODGComponentMask >> 1) & ODGComponentMask) | (ODBComponentMask >> 1)); - index++; - } - } - - for (y = rect.Y; y < rect.Y + ypos; ++y) { - int index = y * stride + rect.X + xpos; - for (int x = rect.X + xpos; x < rect.Width + rect.X; ++x) { - unsigned short px = surfptr[index]; - - surfptr[index] = (px >> 1) & (((ODRComponentMask >> 1) & (unsigned int)ODRComponentMask) | ((ODGComponentMask >> 1) & ODGComponentMask) | (ODBComponentMask >> 1)); - index++; - } - } - - surface.Unlock(); - } -} - - -/// -/// Fills a rectangle with a translucent color. -/// Each pixel of the area is blended toward the color given rather than replaced by it, -/// so whatever was already drawn there still shows through. -/// -/// The strength of the blend, from 0 for invisible to 255 for solid. -void ODFillRectTrans(Rect const & rect, Surface & surf, int color, int trans) -{ - unsigned short * surfptr = (unsigned short *)surf.Lock(); - - if (surfptr != NULL) { - - int strideWords = surf.Stride() / 2; - - for (int y = rect.Y; y < rect.Height + rect.Y; y++) { - int rowIndex = strideWords * y; - - for (int x = rect.X; x < rect.X + rect.Width; x++) { - surfptr[rowIndex + x] = OD_Blend_Color(surfptr[rowIndex + x], color, trans); - } - - rowIndex += strideWords; - } - - surf.Unlock(); - } -} - - -/// -/// Draws the decorative bit strand along a rectangle. -/// This routine scatters the small "bits" graphics along the top and bottom edges of the -/// area, picking a random variant for each one so that no two dialogs look quite alike. -/// -/// The surface to draw the strand upon. -void ODDrawBitsStrand(Rect const & rect, Surface & surface) -{ - unsigned char paldata[768]; - Surface *img = SurfaceCache.GetSurface("bits_i.pcx", paldata); - Surface *mask = SurfaceCache.GetSurface("bits_a.pcx", NULL); - int index = 0; - SurfaceCacheConvertPalette(paldata); - Rect work = rect; - int rnd; - - for (int x = 0; x < rect.Width; x += 10) { - - rnd = rand() % 8; - work.Set(rect.X+x, rect.Y+1, 10, 12); - SurfaceCache.DrawMasked(work, surface, *img, *mask, paldata, 0, 10 * rnd, 0); - - rnd = rand() % 8; - work.Set(rect.X+x, rect.Y+rect.Height-16, 10, 12); - SurfaceCache.DrawMasked(work, surface, *img, *mask, paldata, 0, 10 * rnd, 0); - - index++; - - if ((index % 6) == 0) { - x += 10; - } - } -} - - -/// -/// Draws the backdrop of an owner-draw dialog. -/// The first call composes the whole backdrop -- wallpaper, side bars, corner pieces and -/// the glowing border -- onto a surface cached on the dialog. Every later call simply -/// blits that cached surface, which is what makes repainting a dialog cheap. -/// -void OwnerDraw::Draw_Dialog_Back(HWND window) -{ - WinData * entry = NULL; - PaletteClass rgb; - - /// Find dictionary entry for this dialog (must exist by invariant set during WM_INITDIALOG). - if (ODWinData.getEntries() != 0) { - ODWinData.getPointer(window, &entry); - } - - /* - * Compute client rect and display rect; expand to max of each dimension. - */ - RECT rcClient; - ::GetClientRect(window, &rcClient); - - RECT rcDisp; - Get_Display_Rect(window, &rcDisp); - - Rect rFull; - rFull.Set(rcDisp.left, rcDisp.top, rcDisp.right - rcDisp.left, rcDisp.bottom - rcDisp.top); - - if (rFull.Width <= rcClient.right) { - rFull.Width = rcClient.right; - } - if (rFull.Height <= rcClient.bottom) { - rFull.Height = rcClient.bottom; - } - - Surface * surf = entry->cachedSurface; - if (surf == NULL) { - surf = new BSurface(rcClient.right, rcClient.bottom, 2); - entry->cachedSurface = surf; - ++_surface_count; - - // The art is 640x400 and centered on the screen; a dialog reaching past it keeps black there. - surf->Fill(0); - - Surface * back = SurfaceCache.GetSurface("dbak6440.pcx"); - - Rect dst = rFull; - Rect src = rFull; - - dst -= Point2D(rcDisp.left, rcDisp.top); - - if (VideoModeWidth > back->Get_Width()) { - src.X += (VideoModeWidth - back->Get_Width()) / -2; - } - if (VideoModeHeight > back->Get_Height()) { - src.Y += (VideoModeHeight - back->Get_Height()) / -2; - } - - surf->Blit_From(dst, *back, src); - - /// Side bars - Surface * leftbar = SurfaceCache.GetSurface("leftbar.pcx"); - Surface * rightbar = SurfaceCache.GetSurface("rightbar.pcx"); - - Rect work; - if (leftbar != 0) { - work = rFull; - work.Width = leftbar->Get_Width(); - work.X = 0; work.Y = 0; - SurfaceCache.Draw(work, *surf, *leftbar); - } - if (rightbar != 0) { - work = rFull; - work.X = work.Width - rightbar->Get_Width(); - work.Y = 0; - work.Width = rightbar->Get_Width(); - SurfaceCache.Draw(work, *surf, *rightbar); - } - - Surface * bar_ul = SurfaceCache.GetSurface("bar_ul.pcx"); - Rect srcCorner; - srcCorner.Y = 0; - srcCorner.X = 0; - srcCorner.Width = bar_ul->Get_Width(); - srcCorner.Height = bar_ul->Get_Height(); - - /// dst rect used for each corner - Rect dstCorner; - dstCorner.X = 0; - dstCorner.Y = 0; - dstCorner.Width = srcCorner.Width; - dstCorner.Height = srcCorner.Height; - - surf->Blit_From(dstCorner, *bar_ul, srcCorner); - - Surface * bar_ll = SurfaceCache.GetSurface("bar_ll.pcx"); - dstCorner.Y = rFull.Height - srcCorner.Height; - surf->Blit_From(dstCorner, *bar_ll, srcCorner); - - Surface * bar_ur = SurfaceCache.GetSurface("bar_ur.pcx"); - dstCorner.Y = 0; - dstCorner.X = rFull.Width - srcCorner.Width; - surf->Blit_From(dstCorner, *bar_ur, srcCorner); - - Surface * bar_lr = SurfaceCache.GetSurface("bar_lr.pcx"); - dstCorner.Y = rFull.Height - srcCorner.Height; - surf->Blit_From(dstCorner, *bar_lr, srcCorner); - - // Edge glow loops (layers 0..15) - for (int layer = 0; layer < 16; ++layer) { - - /* - * Top edge between left and right bars. The bar widths are re-fetched - * at every use - Get_Width is virtual, so the four calls per pass - * cannot be a cached local. - */ - Point2D p2(layer + leftbar->Get_Width(), layer); - Point2D p3(rFull.Width - layer - rightbar->Get_Width() - 1, layer); - - /// The alpha fades from 96 down to 6 as the layers move inward. The - /// parameter is an unsigned char, so the whole thing is computed in - /// 8-bit arithmetic and homed as a byte. - ODDrawEdgeGlow(*surf, p2, p3, 0xFFFF, 96 - 6 * layer); - - // Bottom edge - p3.Y = rFull.Height - layer - 1; - p2.Y = p3.Y; - ODDrawEdgeGlow(*surf, p2, p3, 0xFFFF, 96 - 6 * layer); - - /// Left vertical - p2.X = layer + leftbar->Get_Width(); - p3.X = p2.X; - p2.Y = layer + 1; - p3.Y = rFull.Height - layer - 2; - ODDrawEdgeGlow(*surf, p2, p3, 0xFFFF, 96 - 6 * layer); - - /// Right vertical - p2.X = rFull.Width - layer - rightbar->Get_Width() - 1; - p3.X = p2.X; - ODDrawEdgeGlow(*surf, p2, p3, 0xFFFF, 96 - 6 * layer); - } - } - - /// Final blit to AlternateSurface unless flagged as "captured" (UserData2 != 0) - if (surf != NULL) { - if (entry->paintDisabled == 0) { - Rect srcOnXs = rFull; - srcOnXs -= Point2D(rcDisp.left, rcDisp.top); - AlternateSurface->Blit_From(rFull, *surf, srcOnXs); - } - } -} - - -/// -/// Frees the cached background of a window that is going away. -/// Owner-draw windows keep the picture of whatever sits behind them on a cached surface. -/// This routine hands that surface back as the window is destroyed. -/// -void On_WM_NCDESTROY(HWND window) -{ - WinData *data; - if (ODWinData.getPointer(window, &data)) { - if (data != NULL && data->cachedSurface != NULL) { - delete data->cachedSurface; - data->cachedSurface = NULL; - _surface_count--; - } - } -} - - -/// -/// Repaints part of a window right away. -/// The area is marked as needing painting and the paint is forced through, rather than -/// waiting for Windows to get round to it. -/// -/// The area of the window to repaint, or NULL for the whole window. -int WINAPI ODUpdateWindowRect(HWND window, RECT *rect) -{ - InvalidateRect(window, rect, FALSE); - UpdateWindow(window); - return(1); -} - - -/// -/// Adds a window to a list of windows. -/// This routine is the enumeration callback used when a caller needs a snapshot of the -/// child windows of a dialog. -/// -/// The list to append the window to. -BOOL CALLBACK ODAddWindowToList(HWND window, ArrayList * list) -{ - if (list != NULL) { - list->add(window, list->length()); - } - return(TRUE); -} - - -/// -/// Takes the mouse away from the game so that a dialog may use it. -/// The game cursor gives up its capture, leaving Windows free to drive the dialog and its -/// controls. -/// -/// Returns with the number of captures now outstanding. -/// Each call must be matched by a call to Release_Mouse. -int OwnerDraw::Capture_Mouse(void) -{ - if (MouseCursor != NULL) { - if (MouseCursor->Is_Captured() == true) { - MouseCursor->Release_Mouse(); - } - } - _mouse_counter++; - return(_mouse_counter); -} - - -/// -/// Gives the mouse back to the game. -/// This routine undoes one Capture_Mouse. Only when the last dialog has finished with the -/// mouse does the game cursor take it back. -/// -/// Returns with the number of captures still outstanding. -int OwnerDraw::Release_Mouse(void) -{ - if (_mouse_counter > 0) { - _mouse_counter--; - } - if (_mouse_counter == 0) { - if (MouseCursor != NULL) { - if (!MouseCursor->Is_Captured()) { - MouseCursor->Capture_Mouse(); - } - } - } - return(_mouse_counter); -} - - -/// -/// Creates a modeless dialog from a resource template. -/// This routine fetches the dialog template out of the game resources, creates the dialog -/// and makes it the top window of the dialog stack. The mouse is captured for it. -/// -/// The resource ID of the dialog template to create. -/// The dialog procedure that will drive the dialog. -/// Returns with the handle of the new dialog, or NULL if it could not be -/// created. -/// Every dialog begun with this routine must be finished with End_Dialog. -HWND OwnerDraw::Begin_Dialog(int id, DLGPROC proc) -{ - LPCDLGTEMPLATE templ = (LPCDLGTEMPLATE)Fetch_Resource(MAKEINTRESOURCE(id), (LPCSTR)RT_DIALOG); - if (templ == NULL) { - return(NULL); - } - - int idx = g_DialogCount; - g_Dialogs[idx].handle = NULL; - g_Dialogs[idx].id = 0; - g_DialogCount++; - - HWND handle = CreateDialogIndirectParam(ProgramInstance, templ, MainWindow, proc, 0); - if (handle == NULL) { - g_DialogCount--; - return(NULL); - } - - g_Dialogs[idx].handle = handle; - g_Dialogs[idx].id = LOWORD(id); - - Capture_Mouse(); - - Add_Modeless_Dialog(handle); - - g_TopWindow = handle; - g_TopWindowID = LOWORD(id); - - return(handle); -} - - -/// -/// Shuts down a dialog and forgets about it. -/// This routine destroys the window and takes it off the stack of open dialogs, handing -/// the focus back to whichever dialog was underneath it -- or to the main game window once -/// the last dialog has gone. The mouse capture taken by Begin_Dialog is given back here. -/// -void OwnerDraw::End_Dialog(HWND window) -{ - Keyboard->Clear(); - DestroyWindow(window); - - for (int index = 0; index < g_DialogCount; index++) { - if (g_Dialogs[index].handle == window) { - memmove(&g_Dialogs[index], &g_Dialogs[index + 1], sizeof(WSDialogStruct) * (g_DialogCount - (index + 1))); - - g_Dialogs[g_DialogCount - 1].handle = NULL; - g_Dialogs[g_DialogCount - 1].id = 0; - - --g_DialogCount; - - if (g_DialogCount > 0) { - g_TopWindow = g_Dialogs[g_DialogCount - 1].handle; - g_TopWindowID = g_Dialogs[g_DialogCount - 1].id; - - SetForegroundWindow(g_TopWindow); - SetFocus(g_TopWindow); - } else { - g_TopWindow = NULL; - g_TopWindowID = 0; - - SetForegroundWindow(MainWindow); - SetFocus(MainWindow); - } - break; - } - } - - UpdateWindow(MainWindow); - Release_Mouse(); -} - - -/// -/// Displays a dialog that has already been created. -/// This routine makes the dialog visible, brings it to the front and flushes the game -/// keyboard so that no stale keystrokes leak into it. -/// -void OwnerDraw::Display_Dialog(HWND window) -{ - ShowWindow(window, SW_SHOWNORMAL); - SetForegroundWindow(window); - Keyboard->Clear(); -} - - -/// -/// Handles the messages common to every owner-draw dialog. -/// A dialog procedure hands its messages to this routine first and deals with them itself -/// only when they come back unclaimed. This is where the subclassing, centering, -/// background painting and control coloring that all dialogs share is performed. -/// -/// Returns with the message result, or zero if the caller should handle it. -INT_PTR OwnerDraw::Default_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - switch (message) { - case WM_DRAWITEM: - Draw_Item((DRAWITEMSTRUCT*)lparam); - return(1); - - case WM_DESTROY: - Remove_Modeless_Dialog(window); - --_dialog_count; - SetFocus(MainWindow); - return(0); - - case WM_PAINT: - Draw_Dialog_Back(window); - ValidateRect(window, 0); - return(0); - - case WM_ERASEBKGND: - return(1); - - case WM_INITDIALOG: - ++_dialog_count; - Subclass_Dialog(window, 0); - Resize_Dialogs(window); - Center_Window_Within_Window(window); - SetFocus(window); - return(0); - - case WM_CTLCOLORMSGBOX: - case WM_CTLCOLOREDIT: - case WM_CTLCOLORLISTBOX: - case WM_CTLCOLORBTN: - case WM_CTLCOLORDLG: - case WM_CTLCOLORSCROLLBAR: - case WM_CTLCOLORSTATIC: - return((INT_PTR)GetStockObject(BLACK_BRUSH)); - - case OD_SUBCLASSED: - SendMessage(window, OD_SETTOP, (WPARAM)window, 1); - return(0); - - default: - return(0); - } -} - - -/// -/// Services the game while a dialog is up. -/// A dialog's own message pump calls this routine every pass. It dispatches the pending -/// Windows messages and then either runs the game logic loop -- so that a multiplayer -/// game keeps up while the dialog is showing -- or just the maintenance callback. -/// -/// bool; Has the game ended, so that the dialog should be shut down? -bool OwnerDraw::Dialog_Message_Handler(void) -{ - static bool inmainloop = false; - - Windows_Message_Handler(); - - if (Session.Type != GAME_NORMAL && Session.Type != GAME_SKIRMISH && !Session.NetOpen && !Session.Suspended) { - if (!inmainloop) { - inmainloop = true; - bool end = Main_Loop(); - inmainloop = false; - if (end) { - return(true); - } - } - } else { - Call_Back(); - } - - return(false); -} - - -/// -/// Moves a dialog to the position specified. -/// The position is relative to the client area of the main game window rather than to the -/// desktop, and the dialog keeps its current size. -/// -/// The horizontal position to move to, or -1 to leave it where it is. -/// The vertical position to move to, or -1 to leave it where it is. -/// Returns with non-zero if the dialog was moved. -int OwnerDraw::Move_Dialog(HWND window, int x, int y) -{ - int xpos; - int ypos; - - RECT rect1; - rect1.left = 0; - rect1.top = 0; - rect1.right = VideoModeWidth; - rect1.bottom = VideoModeHeight; - - ClientToScreen(MainWindow, (LPPOINT)&rect1); - ClientToScreen(MainWindow, (LPPOINT)&rect1.right); - - RECT rect2; - GetWindowRect(window, &rect2); - - rect2.right -= rect2.left; - rect2.bottom -= rect2.top; - - if (x == -1) { - xpos = rect2.left - rect1.left; - } else { - xpos = x; - } - rect2.left = xpos; - - if (y == -1) { - ypos = rect2.top - rect1.top; - } else { - ypos = y; - } - rect2.top = ypos; - - return(MoveWindow(window, rect2.left, rect2.top, rect2.right, rect2.bottom, FALSE)); -} - - -/// -/// Creates the custom message box dialog. -/// This routine brings the modeless message box up, captures the mouse and makes the box -/// the top window. The second button stays hidden unless a caption is supplied for it. -/// -/// The message text to show in the box. -/// The caption for the cancel button, or NULL to leave it hidden. -/// Flag to be raised if the player cancels the box. -/// Returns with the handle of the message box, or NULL if it could not be -/// created. -HWND OwnerDraw::Custom_Message_Box(const char *btn1txt, const char *btn2txt, bool * cancelled) -{ - HWND dlg = OwnerDraw::Begin_Dialog(IDD_MSGBOX_1, Custom_Message_Box_Proc); - - SetWindowLongPtr(dlg, DWLP_USER, (LONG_PTR)cancelled); - SetDlgItemText(dlg, IDC_MSGBOX_TEXT, btn1txt); - - if (btn2txt) { - HWND handle = GetDlgItem(dlg, IDCANCEL); - SetWindowText(handle, btn2txt); - EnableWindow(handle, TRUE); - ShowWindow(handle, SW_SHOW); - } - - return(dlg); -} - - -/// -/// Handles the messages for a custom message box. -/// This routine lets the default dialog handler have first refusal of the message and -/// then watches for the cancel button. Cancelling feeds an escape key to the game -/// keyboard and raises the flag the box was opened with. -/// -/// Returns with the message result, or zero when the message was consumed here. -INT_PTR CALLBACK Custom_Message_Box_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - INT_PTR res = Default_Dialog_Proc(window, message, wparam, lparam); - - if (res == 0) { - if (message == WM_COMMAND && wparam == IDCANCEL) { - bool * cancelled = (bool *)GetWindowLongPtr(window, DWLP_USER); - if (cancelled) { - Keyboard->Put(KN_ESC); - *cancelled = true; - } - } - return(0); - } - return(res); -} - - -/// -/// Sets the text displayed by a custom message box. -/// Use this routine to change the prompt of a message box that is already on the screen. -/// The box is repainted before the routine returns. -/// -void OwnerDraw::Set_Custom_Message_Box_Text(HWND window, LPCSTR text) -{ - SetDlgItemText(window, IDC_MSGBOX_TEXT, text); - UpdateWindow(window); -} diff --git a/code/ownrdraw.h b/code/ownrdraw.h deleted file mode 100644 index 5d6b10ccc..000000000 --- a/code/ownrdraw.h +++ /dev/null @@ -1,509 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2026 OpenTS contributors - * - * See LICENSE.md for applicable additional terms and warranty disclaimers. - ******************************************************************************/ - -#pragma once - -#include "arraylist.h" -#include "keyboard.h" -#include "surface.h" -#include "win.h" -#include "winfix.h" -#include "wstring.h" - - -namespace OwnerDraw { - - struct CellData { - CellData(void); - CellData(CellData const & that) : type(that.type), string(that.string), hint(that.hint), color(that.color), surf(that.surf), pingtime(that.pingtime) {} - CellData const & operator=(CellData & that) - { - type = that.type; - string = that.string; - hint = that.hint; - color = that.color; - surf = that.surf; - pingtime = that.pingtime; - return(*this); - } - - enum DataType { - INVALID, - TEXT, - SURFACE, - PING, - PRIMARY, - }; - - /* - * This specifies what kind of content the cell holds, and hence how it is drawn. - * A cell constructs as INVALID, and an INVALID cell contributes nothing to the row. - */ - DataType type; - - /* - * This is the text drawn in a TEXT cell. A PRIMARY cell ignores it and takes its - * text from the list box item itself instead. - */ - Wstring string; - - /* - * This is the tooltip text for the cell, shown when the mouse rests over this - * column of the row. If empty, then the cell has no tooltip. - */ - Wstring hint; - - /* - * This is the color the cell's text is drawn in. If -1, then the default list text - * color is used instead. - */ - int color; - - /* - * Pointer to the image drawn in a SURFACE cell, centered vertically in the row. The - * cell does not own the surface. - */ - Surface * surf; - - /* - * This is the round trip time in milliseconds, drawn as a bar in a PING cell. The - * bar fills in proportion to one second, and turns yellow at 300 and red at 500. - */ - int pingtime; - }; - - struct ColumnData { - ColumnData(void) : xPos(0), width(0), cells() {} - ColumnData(ColumnData & that) : xPos(that.xPos), width(that.width), cells(that.cells) {} - - /* - * This is the offset of the column from the left edge of a row, expressed in pixels. - */ - int xPos; - - /* - * This is the width available to the column, expressed in pixels. Text wider than - * this is truncated with an ellipsis. If zero, then the width is unlimited. - */ - int width; - - /* - * These are the column's cells, one for each row of the list box and indexed by row - * number. A row with no cell here draws nothing in this column. - */ - ArrayList cells; - }; - - struct Tooltip { - - /* - * This is the area of the screen the tooltip covers. - */ - Rect bounds; - - /* - * Pointer to a saved copy of the screen underneath the tooltip. Hiding the tooltip - * blits this back, so the dialog beneath never has to repaint itself. - */ - Surface * background; - - /* - * This is the text displayed in the tooltip. If the owning control supplies none, - * then the placeholder "Tool Tip" is used. - */ - char text[128]; - - /* - * If a tooltip currently exists, then this flag will be true. It stays true while - * the tooltip is hidden, and is only cleared when the tooltip is taken down. - */ - int isActive; - - /* - * If the tooltip is active but has been erased from the screen, then this flag will - * be true. A hidden tooltip can be put back without saving the screen again. - */ - int isHidden; - - /* - * This is the control the tooltip belongs to. The tooltip is taken down when that - * control is destroyed, hidden, or loses the keyboard focus. - */ - HWND window; - - Tooltip(void); - }; - - /* - * Key of the dictionary CtrlProc keeps of the messages it is already in the - * middle of handling, so that a control cannot re-enter its own handler. - */ - struct CtrlMsgData { - /* - * These two together identify one message in flight -- the message code and the - * control it was sent to. - */ - UINT message; - HWND window; - - bool operator ==(CtrlMsgData const & that) const; - }; - - /* - * Measurements of one of the remap fonts, cached by ODGetFontMetrics. - */ - struct FontMetrics { - int charWidths[256]; /// inked width of each character, indexed by character code - int glyphWidth; /// width of the inked part of a glyph cell - int glyphHeight; /// height of the inked part of a glyph cell - int topMargin; /// blank rows above each row of glyphs - int leftMargin; /// blank columns before each glyph - }; - - /* - * Per-control data block stored by value in ODWinData, keyed by the control's HWND. - * A common header and footer are shared by the framework (CtrlProc) and every control; - * the middle is a union overlaid differently by each control type. - */ - struct WinData { - - /* - * These fields precede the per-control union and are common to every control type. - * They are maintained by the framework rather than by any one control's handler. - */ - LPARAM userData; /// value handed in when the control was subclassed; nothing reads it back - int scrollBarWidth; /// width of the attached scrollbar, or zero if there is none - HWND ownerWindow; /// in a scrollbar's own record, the control that created it - HWND attachedWindow; /// attached scrollbar or dropdown; 1 while one is being created - Surface *cachedSurface; /// cached copy of the control's backdrop, rebuilt when it resizes - Surface *image; /// image the control draws (OD_SETIMAGE) - Surface *altImage; /// image drawn instead while the control is pressed (OD_SETALTIMAGE) - int itemHeightSet; /// combo box: the item height has already been set up once - int paintDisabled; /// suppress painting, for this control and its attached window (OD_DISABLEPAINT) - int toolTipsEnabled; /// show a tooltip while the mouse rests over the control (OD_TOOLTIPS) - - /* - * Per-control state. The same bytes mean different things per control type. - */ - union { - struct { /// list box -- columns, colors and selection - ArrayList *rowColors; /// per-row background color, -1 for the default (OD_SETCOLOR) - ArrayList *selStates; /// per-row selected flag, indexed alongside rowColors - int topIndex; /// the row drawn at the top of the visible area - int curSel; /// the currently selected row, or -1 if there is none - ArrayList *columns; /// the columns the rows are divided into (OD_ADDCOLUMN) - } ListBox; - - struct { /// scroll bar -- range, thumb position and arrows - int result; /// the left button is being held down on the scrollbar - int dragging; /// the grip is being dragged with the mouse - int range; /// number of scroll positions, defaulting to 100 - int position; /// the current scroll position within the range - int upPressed; /// the up arrow is held, and is drawn depressed - int downPressed; /// the down arrow is held, and is drawn depressed - int keepCapture; /// hand the mouse back to the owner on release (OD_SETKEEPCAPTURE) - } ScrollBar; - - struct { /// track bar (slider) -- range, value and thumb - int result; /// the left button is being held down on the track bar - int dragging; /// the thumb is being dragged with the mouse - int range; /// span of the value, the maximum less the minimum - int value; /// the current value, measured up from the minimum - int minimum; /// the low end of the value range - int thumbPos; /// how far along the slider the thumb is drawn, in pixels - int step; /// the increment a click applies; reported values snap to it - int showNumbers; /// draw the value as a number beside the slider (OD_TRACKNUMBERS) - int clickSuppress; /// stay silent rather than click when the value changes (OD_TRACKSILENT) - } TrackBar; - - struct { /// combo box -- the closed control; see ComboDrop - int selIndex; /// Unused - int reserved2C; /// Unused - int reserved30; /// Unused - HWND dropdown; /// the open dropdown window, or NULL while the list is closed - int reserved38[6]; /// Unused - COLORREF itemColors[50]; /// per-item text color, -1 for the default (OD_SETCOLOR) - } ComboBox; - - struct { /// static text -- the owner-drawn caption - char *text; /// the control's own copy of its caption, freed when it is destroyed - COLORREF textColor; /// the color the caption is drawn in (OD_SETCOLOR) - } Static; - - struct { /// owner-draw button - int state; /// item state from WM_DRAWITEM; ODS_SELECTED draws altImage - } Button; - - struct { /// auto-checkbox -- toggles itself when clicked - int checkState; /// whether the box is checked (BM_GETCHECK / BM_SETCHECK) - } CheckBox; - - struct { /// edit box -- focus / tab-stop state during dialog reveal - int reserved28; /// Unused - int reserved2C; /// Unused - int focusPending; /// focus arrived before the reveal finished, so OD_ACTIVATE must apply it - int focusEnabled; /// clear until OD_ACTIVATE; while clear, focus is deflected to MainWindow - int hadTabStop; /// WS_TABSTOP was stripped when subclassing, and OD_ACTIVATE restores it - } Edit; - - struct { /// combo box dropdown window - int selection; /// the highlighted item, seeded from the owning combo box - int reserved2C; /// Unused - int scrollTop; /// the first item visible in the dropped-down list - } ComboDrop; - - struct { /// the item state any owner-draw control was last asked to draw - int itemState; /// item state from WM_DRAWITEM; the same field as Button::state - } DrawItem; - - struct { /// progress bar -- the filled proportion of the bar - int minimum; /// the value at which the bar reads as empty - int maximum; /// the value at which the bar reads as full, 100 by default - int position; /// the current value, clamped to the range - } ProgressBar; - - unsigned char _size[0x100]; /// pads the union out so that the footer lands at the right offset - }; - - /* - * These fields follow the per-control union and are common to every control type. - * They hold the device context state CtrlProc saves and restores around a paint. - */ - HFONT font; /// the control's font, or the object OD_SAVEDC displaced out of the device context - int bkMode; /// the background mode saved by OD_SAVEDC - COLORREF bkColor; /// the background color saved by OD_SAVEDC - COLORREF textColor; /// the text color saved by OD_SAVEDC - int field_138; /// Unused - int animState; /// reveal state of the owning dialog -- 0 hidden, 1 awaiting the animated reveal, 2 shown - }; - - void Initialize(void); - void Prepare_Resources(HWND window); - void Register_Control_Classes(void); - bool Subclass_Dialog(HWND window, LPARAM lParam); - - void Draw_Item(LPDRAWITEMSTRUCT drawit); - void Draw_Dialog_Back(HWND window); - - int Capture_Mouse(void); - int Release_Mouse(void); - - HWND Begin_Dialog(int id, DLGPROC proc); - void Display_Dialog(HWND window); - int Move_Dialog(HWND window, int x, int y); - void End_Dialog(HWND window); - - INT_PTR Default_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); - bool Dialog_Message_Handler(void); - - bool Start_Tooltip(Rect const & rect, char const * text, HWND window); - bool Show_Tooltip(bool save_background); - bool Hide_Tooltip(void); - bool End_Tooltip(void); - - HWND Custom_Message_Box(const char * btn1txt, const char * btn2txt = NULL, bool * cancelled = NULL); - void Set_Custom_Message_Box_Text(HWND window, LPCSTR text); -}; - - -#define OD_TEXT_ALIGN_MIN 1 -#define OD_TEXT_ALIGN_CENTER 2 -#define OD_TEXT_ALIGN_MAX 3 - -int OD_Draw_Text_Remap(Surface & surface, const char * string, Rect const & rect, char const * name, COLORREF color, int flags, int char_spacing); -int OD_Draw_Text(COLORREF color, HFONT font, Rect const & rect, const char * text, int len, int x_alignment, int y_alignment, Surface * surface); -void OD_Draw_Rect(Surface & surf, Rect const & rect, int offset, int color); - -void On_WM_NCDESTROY(HWND window); - -int Build_Hotkey_String(KeyNumType key, char * buffer); - -extern COLORREF ODColorText; -extern COLORREF ODColorTextDim; -extern COLORREF ODColorDisabled; -extern COLORREF ODColorFrame; -extern COLORREF ODListBoxColor; -extern COLORREF ODColorUnused1; - - -/// Flags for ODDrawCharRemap. -#define OD_DRAW_CHAR_FLAG_HORIZONTAL_CENTER 1 -#define OD_DRAW_CHAR_ALIGN_FLAG_RIGHT 2 -#define OD_DRAW_CHAR_FLAG_VERTICAL_CENTER 4 - - -extern unsigned short ODRComponentMask; -extern unsigned short ODGComponentMask; -extern unsigned short ODBComponentMask; - -inline unsigned short OD_Blend_Color(unsigned short pixel, unsigned short color, unsigned char alpha) -{ - static unsigned blend_color_alpha; - static unsigned blend_pixel_alpha; - - blend_color_alpha = alpha; - blend_pixel_alpha = 255 - alpha; - - unsigned short r = ((((pixel & ODRComponentMask) * blend_pixel_alpha) + ((color & ODRComponentMask) * blend_color_alpha)) >> 8) & ODRComponentMask; - unsigned short g = ((((pixel & ODGComponentMask) * blend_pixel_alpha) + ((color & ODGComponentMask) * blend_color_alpha)) >> 8) & ODGComponentMask; - unsigned short b = (((pixel & ODBComponentMask) * blend_pixel_alpha) + ((color & ODBComponentMask) * blend_color_alpha)) >> 8; - return((unsigned short)(r | g | b)); -} - - -/* - * Owner-draw control messages, handled by CtrlProc and the per-control - * *CtrlProc functions in ownrdraw.cpp. Each is WM_USER + offset. The comment - * above each says what it does, which control(s) handle it, and its - * wParam / lParam / return contract. - */ - -/* - * Initialize a freshly subclassed control's WinData. Per-control: listbox sets item height/font, - * combobox sets item height and clears item colors, static captures its caption, progress bar sets - * max=100, edit strips WS_TABSTOP and deflects focus. - * Used by: all controls. Input: none. Output: none. - */ -#define OD_SUBCLASSED (WM_USER + 151) - -/* - * Set a per-element color. listbox = per-row background (wParam = row); combobox = per-item text - * color (wParam = item 0..50); static = text color (wParam ignored, lParam == -1 resets to default). - * Used by: listbox, combobox, static. Input: wParam = element index, lParam = COLORREF. Output: none. - */ -#define OD_SETCOLOR (WM_USER + 152) - -/* - * Enable or disable hover tooltips for the control. - * Used by: all controls. Input: lParam = BOOL. Output: previous enabled state. - */ -#define OD_TOOLTIPS (WM_USER + 154) - -/* - * Ask the parent dialog for a control's tooltip text; the dialog copies it into the lParam buffer. - * Used by: sent by the framework to the parent dialog. Input: wParam = control ID, lParam = char[]. Output: none. - */ -#define OD_GETTIPTEXT (WM_USER + 155) - -/* - * Set the control's primary user image (WinData::Image). - * Used by: all controls. Input: lParam = Surface*. Output: previous Image. - */ -#define OD_SETIMAGE (WM_USER + 156) - -/* - * Suppress painting for the control and its attached child window. - * Used by: all controls. Input: lParam = BOOL. Output: previous state. - */ -#define OD_DISABLEPAINT (WM_USER + 157) - -/* - * Restore the font / bk-mode / bk-color / text-color into the HDC that were saved by OD_SAVEDC. - * Used by: all controls. Input: lParam = HDC. Output: 1. - */ -#define OD_RESTOREDC (WM_USER + 158) - -/* - * Save the HDC's current font / bk-mode / bk-color / text-color into WinData for OD_RESTOREDC. - * Used by: all controls. Input: lParam = HDC. Output: 1. - */ -#define OD_SAVEDC (WM_USER + 159) - -/* - * Query whether the control has an attached child window (scrollbar / dropdown). - * Used by: all controls. Input: none. Output: BOOL. - */ -#define OD_HASATTACHED (WM_USER + 160) - -/* - * Non-painting refresh tick: sent in place of WM_PAINT while painting is disabled, so the control can - * update grip/scroll state without drawing. - * Used by: scrollbar, listbox (sent by CtrlProc). Input: forwarded wParam/lParam. Output: none. - */ -#define OD_REFRESHNOPAINT (WM_USER + 165) - -/* - * Add a column to the multi-column listbox. - * Used by: listbox. Input: wParam = column width, lParam = column x-position (also the id). Output: the x-position (existing one if a column is already there). - */ -#define OD_ADDCOLUMN (WM_USER + 166) - -/* - * Remove the column whose x-position matches lParam. - * Used by: listbox. Input: lParam = column x-position/id. Output: the id, or -1 if not found. - */ -#define OD_REMOVECOLUMN (WM_USER + 167) - -/* - * Set the contents of one listbox cell. - * Used by: listbox. Input: wParam = MAKEWPARAM(columnId, row), lParam = CellData*. Output: column id, or -1 on failure. - */ -#define OD_SETCELL (WM_USER + 168) - -/* - * Push a window onto the modal z-order stack and pin it above its siblings. - * Used by: all controls (CtrlProc). Input: wParam = target HWND (0 = self), lParam = BOOL (1 = add and raise, 0 = remove). Output: previous top window. - */ -#define OD_SETTOP (WM_USER + 169) - -/* - * Set the control's alternate image (WinData::AltImage, e.g. the pressed/hover button surface). - * Used by: all controls. Input: lParam = Surface*. Output: previous AltImage. - */ -#define OD_SETALTIMAGE (WM_USER + 170) - -/* - * Set the trackbar step (the increment applied per click). - * Used by: trackbar. Input: lParam = INT step. Output: none. - */ -#define OD_SETTRACKSTEP (WM_USER + 171) - -/* - * Show or hide the numeric value drawn beside the trackbar. - * Used by: trackbar. Input: lParam = BOOL. Output: none. - */ -#define OD_TRACKNUMBERS (WM_USER + 172) - -/* - * Hit-test a listbox cell at a client point and copy its text into the buffer (for the per-cell tooltip). - * Used by: listbox. Input: wParam = MAKELPARAM(x, y), lParam = char[]. Output: non-zero when the cell text is empty. - */ -#define OD_GETCELLTIP (WM_USER + 173) - -/* - * Suppress the click sound on trackbar value changes. - * Used by: trackbar. Input: wParam = BOOL (0 = silent). Output: none. - */ -#define OD_TRACKSILENT (WM_USER + 174) - -/* - * Sent to a dialog's children once the animated reveal finishes; edit controls re-enable focus/tab-stop - * and apply any focus that arrived during the animation. - * Used by: edit controls. Input: none. Output: none. - */ -#define OD_ACTIVATE (WM_USER + 175) - -/* - * No dedicated handler: re-enters the control's WndProc so the edit-box focus-deflection at function - * entry runs again (used after focus changes). - * Used by: edit controls. Input: none. Output: none. - */ -#define OD_REFOCUS (WM_USER + 176) - -/* - * Set the scrollbar's "keep parent capture" flag. - * Used by: scrollbar. Input: lParam = BOOL. Output: none. - */ -#define OD_SETKEEPCAPTURE (WM_USER + 177) - -/* - * Initialize a combo-box dropdown window; seeds its highlighted selection from the owner combo. - * Used by: combo-box dropdown (ComboDropWinCtrlProc). Input: none. Output: none. - */ -#define OD_DROPSUBCLASSED (WM_USER + 1000) diff --git a/code/particle.cpp b/code/particle.cpp index 6aeefabef..f5e660884 100644 --- a/code/particle.cpp +++ b/code/particle.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "particle.h" @@ -929,10 +928,10 @@ void ParticleClass::Serialize(SaveStreamClass & stream) /// /// The stream to write this particle to. /// Should the modified flag be cleared once written? -/// Returns with S_OK if the particle was written successfully. -HRESULT STDMETHODCALLTYPE ParticleClass::Save(IStream * stream, BOOL cleardirty) +/// bool; Was the record written whole? +bool ParticleClass::Save(SaveStreamClass & stream, bool cleardirty) { - HRESULT result = BASECLASS::Save(stream, cleardirty); + bool result = BASECLASS::Save(stream, cleardirty); WasSaved = true; return(result); } @@ -993,18 +992,9 @@ int ParticleClass::Shape_Number(void) const } -/// -/// Fetches the class identifier of this object. -/// The persistence code uses this identifier to recreate the correct object when the -/// save file is loaded back in. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE ParticleClass::GetClassID(CLSID * retval) +ClassID ParticleClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_ParticleClass; - return(S_OK); + return(ClassID_ParticleClass); } diff --git a/code/particle.h b/code/particle.h index b7b32a9c9..a8c16b9e0 100644 --- a/code/particle.h +++ b/code/particle.h @@ -31,8 +31,8 @@ class ParticleClass : public ObjectClass ParticleClass(ParticleTypeClass const * type, Coord const & origin, Coord const & target = COORD_NONE, ParticleSystemClass * partsys = NULL); virtual ~ParticleClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; - virtual HRESULT STDMETHODCALLTYPE Save(IStream * stream, BOOL cleardirty) override; + virtual ClassID Class_ID(void) const override; + virtual bool Save(SaveStreamClass & stream, bool cleardirty) override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/partsys.cpp b/code/partsys.cpp index cf5f1d608..9629812fe 100644 --- a/code/partsys.cpp +++ b/code/partsys.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "partsys.h" @@ -872,18 +871,9 @@ void ParticleSystemClass::Compute_CRC(CRCEngine & crc) const } -/// -/// Fetches the class identifier for this object. -/// This routine is part of the persistence support. The save process records the -/// identifier so that the load process knows what kind of object to build. -/// -/// Pointer to the class identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE ParticleSystemClass::GetClassID(CLSID * retval) +ClassID ParticleSystemClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_ParticleSystemClass; - return(S_OK); + return(ClassID_ParticleSystemClass); } diff --git a/code/partsys.h b/code/partsys.h index 91db45788..41bc50380 100644 --- a/code/partsys.h +++ b/code/partsys.h @@ -31,7 +31,7 @@ class ParticleSystemClass : public ObjectClass ParticleSystemClass(void); virtual ~ParticleSystemClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/pcx.h b/code/pcx.h index 36a252ba7..4b9b85415 100644 --- a/code/pcx.h +++ b/code/pcx.h @@ -36,6 +36,7 @@ #include "wwfile.h" #include +#include #pragma pack(push,1) struct RGB { @@ -67,6 +68,13 @@ struct PCX_HEADER }; #pragma pack(pop) +static_assert(sizeof(RGB) == 3, "PCX palette entry layout changed"); +static_assert(sizeof(PCX_HEADER) == 128, "PCX file header layout changed"); +static_assert(offsetof(PCX_HEADER, x) == 4, "PCX file header layout changed"); +static_assert(offsetof(PCX_HEADER, ega_palette) == 16, "PCX file header layout changed"); +static_assert(offsetof(PCX_HEADER, byte_per_line) == 66, "PCX file header layout changed"); +static_assert(offsetof(PCX_HEADER, filler) == 74, "PCX file header layout changed"); + bool Read_PCX_Size(FileClass & file, int & width, int & height); Surface * Read_PCX_File(FileClass & file_handle, PaletteClass * palette=NULL, void * buff=NULL, int size=0); bool Write_PCX_File(FileClass & file, Surface & pic, PaletteClass * palette); diff --git a/code/persist.h b/code/persist.h new file mode 100644 index 000000000..1b9524367 --- /dev/null +++ b/code/persist.h @@ -0,0 +1,30 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#pragma once + +#include "win.h" + +#include "classid.h" + +class SaveStreamClass; + +// What a saved game asks of an object it carries: the class identifier the record is +// tagged with, and the record itself. +struct IPersistent +{ + virtual ~IPersistent(void) {} + + virtual ClassID Class_ID(void) const = 0; + virtual bool Load(SaveStreamClass & stream) = 0; + // Restores what the record could not carry, once the record has been checked; an object + // takes its place in the map or a side table here, never while its record is still in doubt. + virtual void Post_Load(void) {} + virtual bool Save(SaveStreamClass & stream, bool cleardirty) = 0; +}; diff --git a/code/preview.cpp b/code/preview.cpp index a7ba40107..0fc2e9638 100644 --- a/code/preview.cpp +++ b/code/preview.cpp @@ -23,13 +23,12 @@ #include "lzopipe.h" #include "lzostraw.h" #include "overtype.h" -#include "ownrdraw.h" +#include "drawhelp.h" #include "pcx.h" #include "scenario.h" #include "surface.h" #include "tactical.h" #include "terrain.h" -#include "windlg.h" #include "xpipe.h" #include "xstraw.h" @@ -544,8 +543,19 @@ unsigned * MapPreviewClass::Create_Paletted_Preview(int colorcount, int & size) /// arrives already packed, rather than being rendered from the map that is loaded. /// /// Pointer to the paletted preview block to expand. -void MapPreviewClass::Create_Preview_Surface(char * buffer) +// The palette a paletted preview carries is indexed by one byte per pixel, so a count above +// this one could never be reached and is taken as a malformed block. +static int const MAX_PREVIEW_COLORS = 256; + + +bool MapPreviewClass::Create_Preview_Surface(char * buffer, int length) { + // A block that arrived from another machine declares its own extents, so they are + // checked against the bytes that came with it before anything is read through them. + if (buffer == NULL || length < (int)(sizeof(Header) + sizeof(int))) { + return(false); + } + int * header = (int *)buffer; int width = *header++; @@ -553,20 +563,32 @@ void MapPreviewClass::Create_Preview_Surface(char * buffer) int colorcount = *header; unsigned short *palette = (unsigned short *)header; + if (width <= 0 || height <= 0 || colorcount <= 0 || colorcount > MAX_PREVIEW_COLORS) { + return(false); + } + + long long const block_offset = (long long)colorcount * (long long)sizeof(unsigned short) + (long long)sizeof(Header) + (long long)sizeof(int); + if (block_offset + (long long)width * (long long)height > (long long)length) { + return(false); + } + if (SurfacePtr != NULL) { delete SurfacePtr; } SurfacePtr = new DSurface(width, height); SurfacePtr->Fill(TBLACK); - int offset = (colorcount * sizeof(unsigned short)) + sizeof(Header) + sizeof(int); + int offset = (int)block_offset; unsigned char * indexptr = (unsigned char *)buffer + offset; for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { - unsigned short entry = palette[*indexptr++ + 2]; + int const index = *indexptr++; + unsigned short entry = palette[(index < colorcount ? index : colorcount - 1) + 2]; int color = DSurface::Build_Hicolor_Pixel((entry >> 4) & 0x00F0, entry & 0x00F0, 16 * (entry & 0x000F)); SurfacePtr->Put_Pixel_Clip(Point2D(x, y), color, SurfacePtr->Get_Rect()); } } + + return(true); } diff --git a/code/preview.h b/code/preview.h index f63ba77fb..2b30c3f1c 100644 --- a/code/preview.h +++ b/code/preview.h @@ -34,7 +34,7 @@ class MapPreviewClass void Blit_Preview(HWND window); unsigned * Create_Paletted_Preview(int, int & size); - void Create_Preview_Surface(char * buffer); + bool Create_Preview_Surface(char * buffer, int length); XSurface * Get_Preview_Surface(void) { return(SurfacePtr); } private: diff --git a/code/progress.cpp b/code/progress.cpp index 3fc416d72..70ae4e649 100644 --- a/code/progress.cpp +++ b/code/progress.cpp @@ -23,13 +23,13 @@ #include "language/language.h" #include "lightcon.h" #include "mixfile.h" -#include "ownrdraw.h" #include "scheme.h" #include "session.h" #include "shapeset.h" #include "surface.h" +#include "ui/uiprogress.h" +#include "ui/uishell.h" #include "voc.h" -#include "windlg.h" #include @@ -49,6 +49,7 @@ ProgressScreenClass::ProgressScreenClass(void) Shape = NULL; Background = NULL; IsActive = false; + IsOverlay = false; for (int i = 0; i < MAX_PLAYERS; i++) { PlayerProgress[i] = 0; } @@ -83,7 +84,7 @@ void ProgressScreenClass::Initialize(double progress, int count, bool usedialog) IsActive = true; if (usedialog) { - if (Dialog == NULL) { + if (!Has_Dialog()) { Begin_Dialog(); } } else { @@ -135,6 +136,10 @@ void ProgressScreenClass::Set_Graphic_Data(const char * progbar, const char * ba Pos.Y = pt.Y; } + if (progbar != NULL && IsOverlay) { + UI_Progress_Wait_Set_Bar(progbar); + } + if (progbar != NULL) { Shape = (ShapeSet *)MFCD::Retrieve(progbar); if (Shape != NULL) { @@ -149,9 +154,6 @@ void ProgressScreenClass::Set_Graphic_Data(const char * progbar, const char * ba } rect.Width = rect.Width + 2; rect.Height = rect.Height + 2; - if (PlayerCount == 1 && Dialog != 0) { - HiddenSurface->Draw_Rect(rect, NormalDrawer->Convert_Pixel(15)); - } if (PlayerCount != 1) { pt.X = rect.X - 80; pt.Y = rect.Y; @@ -197,15 +199,15 @@ double ProgressScreenClass::Get_Current_Progress(void) const /// -/// Draws the progress screen. -/// This routine paints a progress bar for every player being tracked, and in the single -/// player case announces the next loading message as the work passes each milestone. The -/// progress percent routines and the dialog's paint handler call it. +/// Announces the loading messages the job has just passed. +/// Each message is printed and its notification sound played once, when the progress first +/// reaches the threshold that names it. This is an effect of the progress moving rather +/// than of the screen being drawn, so a repaint cannot repeat a message and a presentation +/// that repaints a different number of times cannot lose one. /// -/// The screen position to draw at, or Point2D(-1,-1) to use the -/// position established by Set_Graphic_Data. -/// Nothing is drawn until Initialize has been called. -void ProgressScreenClass::Display_Progress(Point2D xpt) +/// The full screen single player presentation is the only one that shows these; +/// the dialog presentation and the multiplayer bars carry no messages. +void ProgressScreenClass::Announce_Milestones(void) { static struct { int Progress; @@ -221,46 +223,57 @@ void ProgressScreenClass::Display_Progress(Point2D xpt) { 100, TXT_LOADING_GAME1H } }; + if (!IsActive || PlayerCount != 1 || Has_Dialog() || Shape == NULL) { + return; + } + + int const progress = PlayerProgress[0]; + int const percent = Percentage; + + if (progress <= percent) { + return; + } + + for (int j = 0; j < ARRAY_SIZE(_progress_messages); j++) { + if (_progress_messages[j].Progress <= progress && _progress_messages[j].Progress > percent) { + Fancy_Text_Print(Fetch_String(_progress_messages[j].Text), *HiddenSurface, HiddenSurface->Get_Rect(), Pos + Point2D(0, 10 * j), Fetch_Scheme_By_Name("Green"), 0, TextPrintType(TPF_NOSHADOW|TPF_EFNT)); + Sound_Effect(VocClass::From_Name("Notify"), 0.4f); + Percentage = _progress_messages[j].Progress; + Update_Visible_Surface(); + break; + } + } +} + + +/// +/// Draws the progress screen. +/// This routine paints a progress bar for every player being tracked. Drawing it again +/// changes nothing else: the loading messages and the clamp that used to happen here now +/// belong to the progress moving. +/// +/// The screen position to draw at, or Point2D(-1,-1) to use the +/// position established by Set_Graphic_Data. +/// Nothing is drawn until Initialize has been called. The full screen single +/// player presentation draws no bar at all, which is what it has always done -- its +/// progress is shown by the messages Announce_Milestones prints. +void ProgressScreenClass::Display_Progress(Point2D xpt) +{ + if (IsOverlay) { + return; + } + if (IsActive) { Point2D pt = xpt; - Surface *surface; - if (Dialog == 0) { - surface = HiddenSurface; - } else { - surface = AlternateSurface; - } + Surface *surface = HiddenSurface; ConvertClass * drawer = NormalDrawer; for (int i = 0; i < PlayerCount; i++) { - if (PlayerProgress[i] > MainProgress) { - PlayerProgress[i] = MainProgress; - } if (Shape != NULL) { if (pt == Point2D(-1,-1)) { if (PlayerCount == 1) { - if (Dialog) { - RECT crect; - Get_Display_Rect(GetDlgItem(Dialog, IDC_PROGRESS_BAR_FRAME), &crect); - pt = Point2D(crect.left + (crect.right - crect.left) / 2, crect.top + (crect.bottom - crect.top) / 2); - } else { - int progress = PlayerProgress[i]; - int percent = Percentage; - if (progress > percent) { - for (int j = 0; j < ARRAY_SIZE(_progress_messages); j++) { - if (_progress_messages[j].Progress <= progress && _progress_messages[j].Progress > percent) { - Fancy_Text_Print(Fetch_String(_progress_messages[j].Text), *HiddenSurface, HiddenSurface->Get_Rect(), Pos + Point2D(0, 10 * j), Fetch_Scheme_By_Name("Green"), 0, TextPrintType(TPF_NOSHADOW|TPF_EFNT)); - Sound_Effect(VocClass::From_Name("Notify"), 0.4f); - Percentage = _progress_messages[j].Progress; - if (surface == HiddenSurface) { - Update_Visible_Surface(); - } - break; - } - } - } - return; - } + return; } else { pt = Point2D(Pos.X, Pos.Y + (10 * i)); drawer = ColorSchemes[Session.Color_Index_To_Scheme(Session.Players[i]->Player.Color)]->Converter; @@ -318,11 +331,7 @@ void ProgressScreenClass::Set_Progress_Percent(int index, double value, Point2D PlayerProgress[index] = (MainProgress / 100.0) * value; if (PlayerProgress[index] != prog1) { - if (Dialog != NULL) { - SendMessage(Dialog, WM_PAINT, 0, 0); - } else { - Display_Progress(pt); - } + Progress_Changed(pt); } } @@ -341,62 +350,55 @@ void ProgressScreenClass::Add_Progress_Percent(int index, double value, Point2D PlayerProgress[index] += (MainProgress / 100.0) * value; if (PlayerProgress[index] != prog1) { - if (Dialog != NULL) { - SendMessage(Dialog, WM_PAINT, 0, 0); - } else { - Display_Progress(pt); - } + Progress_Changed(pt); } } /// -/// Creates the progress dialog. -/// This routine brings up the owner draw progress dialog and gives it its first -/// paint. Initialize() calls it when the caller asks for the dialog presentation -/// rather than the full screen one. +/// Carries out what a moved gauge asks for: the job may not be more than finished, the +/// messages it has just passed are announced, and the screen is drawn again. /// -void ProgressScreenClass::Begin_Dialog(void) +/// The screen position the caller asked the bars be drawn at. +void ProgressScreenClass::Progress_Changed(Point2D pt) { - Dialog = OwnerDraw::Begin_Dialog(IDD_PROGRESS_WAIT, ProgressScreenClass::Dialog_Proc); - if (Dialog != NULL) { - SetWindowLongPtr(Dialog, DWLP_USER, (LONG_PTR)this); - OwnerDraw::Display_Dialog(Dialog); - SendMessage(Dialog, WM_PAINT, 0, 0); + for (int i = 0; i < PlayerCount; i++) { + if (PlayerProgress[i] > MainProgress) { + PlayerProgress[i] = MainProgress; + } + } + + if (pt == Point2D(-1,-1)) { + Announce_Milestones(); + } + + if (IsOverlay) { + UI_Progress_Wait_Set_Progress(Get_Current_Progress(0)); + } else { + Display_Progress(pt); } } /// -/// Takes down the progress dialog. -/// This routine is used when the progress screen is finished with the dialog -/// presentation. It is harmless to call when no dialog was ever created. +/// Opens the progress screen. +/// Initialize() calls this routine when the caller asks for the windowed presentation +/// rather than the full screen one. /// -void ProgressScreenClass::End_Dialog(void) +void ProgressScreenClass::Begin_Dialog(void) { - if (Dialog != NULL) { - OwnerDraw::End_Dialog(Dialog); - Dialog = NULL; - } + IsOverlay = UI_Progress_Wait_Open(); } /// -/// Handles the messages sent to the progress dialog. -/// This routine gives the owner draw default dialog procedure first refusal on every -/// message, and repaints the progress display itself when a paint request comes back -/// unhandled. +/// Takes down the progress screen. +/// It is harmless to call this routine when no screen was ever opened. /// -/// Returns with the dialog result, zero if the message was left unhandled. -INT_PTR CALLBACK ProgressScreenClass::Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) +void ProgressScreenClass::End_Dialog(void) { - INT_PTR res = OwnerDraw::Default_Dialog_Proc(window, message, wparam, lparam); - if (res == 0) { - if (message == WM_PAINT) { - ProgressScreenClass *screen = (ProgressScreenClass *)GetWindowLongPtr(window, DWLP_USER); - screen->Display_Progress(); - } - res = 0; + if (IsOverlay) { + UI_Progress_Wait_Close(); + IsOverlay = false; } - return(res); } diff --git a/code/progress.h b/code/progress.h index 094397b12..f69f60b1c 100644 --- a/code/progress.h +++ b/code/progress.h @@ -40,13 +40,19 @@ class ProgressScreenClass int Get_Bar_Width(void) const; + void Announce_Milestones(void); + void Progress_Changed(Point2D pt = Point2D(-1,-1)); + void Set_Graphic_Data(const char * progbar, const char * background = NULL, const char * string = NULL, Point2D pt=Point2D(-1,-1)); void Display_Progress(Point2D pt = Point2D(-1,-1)); void Begin_Dialog(void); void End_Dialog(void); + + // Is the dialog presentation up, whichever of the two it is? + bool Has_Dialog(void) const { return(IsOverlay); } + private: - static INT_PTR CALLBACK Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); public: /* @@ -97,11 +103,11 @@ class ProgressScreenClass char PlayerCount; /* - * Handle of the progress dialog, or NULL when the progress is presented on the full - * screen instead. The dialog is used where the game must keep a window up while it - * works rather than take the screen over. + * If the progress is presented as a screen of its own rather than on the full + * screen, then this flag will be true. That screen draws its own frame and bar, so + * the routines that paint into the game's surfaces stand aside for it. */ - HWND Dialog; + bool IsOverlay; /* * This is the center of the progress bar display, expressed in screen pixels. A job diff --git a/code/psystype.cpp b/code/psystype.cpp index 4a6cae08f..439a4fdd9 100644 --- a/code/psystype.cpp +++ b/code/psystype.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "psystype.h" @@ -181,18 +180,9 @@ void ParticleSystemTypeClass::Compute_CRC(CRCEngine & crc) const } -/// -/// Fetches the class identifier of this object. -/// This routine is part of the persistence interface. The save system records the class -/// ID so that the right kind of object can be manufactured when the game is reloaded. -/// -/// Pointer to the class ID to be filled in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE ParticleSystemTypeClass::GetClassID(CLSID * retval) +ClassID ParticleSystemTypeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_ParticleSystemTypeClass; - return(S_OK); + return(ClassID_ParticleSystemTypeClass); } diff --git a/code/psystype.h b/code/psystype.h index 3e1b60e9f..c9d1fb379 100644 --- a/code/psystype.h +++ b/code/psystype.h @@ -25,7 +25,7 @@ class ParticleSystemTypeClass : public ObjectTypeClass ParticleSystemTypeClass(char const * ininame = NULL); virtual ~ParticleSystemTypeClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/ptype.cpp b/code/ptype.cpp index 4ff73a7d5..5644fea15 100644 --- a/code/ptype.cpp +++ b/code/ptype.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "ptype.h" @@ -220,18 +219,9 @@ void ParticleTypeClass::Compute_CRC(CRCEngine & crc) const } -/// -/// Fetches the class identifier of this object. -/// The save game system uses this identifier to know which kind of object to build -/// when the stream is read back in. -/// -/// Pointer to the location to store the class identifier. -/// Returns with S_OK, or E_POINTER if no storage location was supplied. -HRESULT STDMETHODCALLTYPE ParticleTypeClass::GetClassID(CLSID * retval) +ClassID ParticleTypeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_ParticleTypeClass; - return(S_OK); + return(ClassID_ParticleTypeClass); } diff --git a/code/ptype.h b/code/ptype.h index fd5f29303..23599468b 100644 --- a/code/ptype.h +++ b/code/ptype.h @@ -28,7 +28,7 @@ class ParticleTypeClass : public ObjectTypeClass ParticleTypeClass(char const * ininame = NULL); virtual ~ParticleTypeClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; virtual void Post_Load(void) override; diff --git a/code/queue.cpp b/code/queue.cpp index 83b9bc2ad..83fabec35 100644 --- a/code/queue.cpp +++ b/code/queue.cpp @@ -133,7 +133,6 @@ #include "opents_build.h" #include "overlay.h" #include "overtype.h" -#include "ownrdraw.h" #include "particle.h" #include "partsys.h" #include "psystype.h" @@ -162,6 +161,7 @@ #include "trigger.h" #include "trigtype.h" #include "tube.h" +#include "ui/uireconnect.h" #include "unit.h" #include "unittype.h" #include "vanim.h" @@ -170,7 +170,6 @@ #include "warhead.h" #include "waypoint.h" #include "weapon.h" -#include "windlg.h" #include "winstub.h" #include "wsproto.h" @@ -323,7 +322,6 @@ static int Process_Reconnect_Dialog(CDTimerClass *timeout_time BasicTimerClass *timer); static int Handle_Timeout(ConnManClass *net, FrameSyncStruct *their); static void Stop_Game(bool=false); -INT_PTR CALLBACK Reconnect_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); static void Close_Reconnect_Dialog(void); void Kick_Player_Now(ConnManClass *net, int kickee, FrameSyncStruct * their, bool error); bool Cast_Kick_Vote(int kicker, int kickee); @@ -2297,16 +2295,12 @@ static int Process_Reconnect_Dialog(CDTimerClass *timeout_time { static int displayed_time = 0; // time value currently displayed - static HWND disconnect_dialog; /// the disconnect/kick dialog - static int disconnect_return; /// set to IDCANCEL by Reconnect_Dialog_Proc int new_time; - int oldest_index; // index of person requiring a reconnect - int i,j; - char buf[256]; // for dialog text + int i; //------------------------------------------------------------------------ - /// Update the frame-sync progress info for Draw_Sync_Bars. + /// Update the frame-sync progress info the screen's bars are drawn from. //------------------------------------------------------------------------ SyncWaitElapsed = *timer; for (i = 0; i < num_conn; i++) { @@ -2314,133 +2308,60 @@ static int Process_Reconnect_Dialog(CDTimerClass *timeout_time } //------------------------------------------------------------------------ - /// The first time through, create the disconnect/kick dialog. + /// The first time through, open the screen. //------------------------------------------------------------------------ if (fresh) { TacticalActive = false; - disconnect_return = -1; - disconnect_dialog = WS_Create_Dialog(ProgramInstance, IDD_MPLAYER_DISCONNECT, MainWindow, Reconnect_Dialog_Proc, true); - Center_Window_Within_Window(disconnect_dialog); - if (disconnect_dialog) { - SetWindowLongPtr(disconnect_dialog, DWLP_USER, (LONG_PTR)&disconnect_return); - MouseCursor->Hide_Mouse(); - ShowWindow(disconnect_dialog, SW_SHOWNORMAL); - UpdateWindow(disconnect_dialog); - MouseCursor->Show_Mouse(); + + int frames[ARRAY_SIZE(SyncBarFrameSync)]; + int reported = 0; + for (i = 0; i < num_conn && i < (int)ARRAY_SIZE(frames); i++) { + frames[reported++] = their[i].frame; } + + UI_Reconnect_Open(reconn != 0, frames, reported); } - //------------------------------------------------------------------------ - /// If the user hit Cancel, bail out of the game. - //------------------------------------------------------------------------ - if (disconnect_return == IDCANCEL) { - WS_Destroy_Dialog(disconnect_dialog, false); - TacticalActive = true; - Map.Flag_To_Redraw(GS_REDRAW_ALL); - return(1); + UIReconnectPresenterClass * const screen = UI_Reconnect_Screen(); + if (screen == NULL) { + return(0); } + unsigned timings[ARRAY_SIZE(SyncBarFrameSync)]; + for (i = 0; i < (int)ARRAY_SIZE(timings); i++) { + timings[i] = SyncBarFrameSync[i].timing; + } + screen->Update_Bars((unsigned)SyncWaitElapsed, timings, (int)ARRAY_SIZE(timings)); + //------------------------------------------------------------------------ // Convert the timer to seconds //------------------------------------------------------------------------ new_time = *timeout_timer / TIMER_SECOND; //------------------------------------------------------------------------ - // If the timer has changed, or 'fresh' is set, redraw the dialog + // If the timer has changed, or 'fresh' is set, tell the screen //------------------------------------------------------------------------ if (fresh || new_time != displayed_time) { displayed_time = new_time; + screen->Set_Time_Remaining(displayed_time); + } - HWND item = GetDlgItem(disconnect_dialog, IDC_DISCONNECT_TIME_REMAINING); - if (item) { - sprintf(buf, Fetch_String(TXT_TIME_ALLOWED), displayed_time); - SendMessage(item, WM_SETTEXT, 0, (LPARAM)buf); - } - if (!(displayed_time & 1)) { - PostMessage(disconnect_dialog, WM_PAINT, 0, 0); - } + UI_Reconnect_Service(); - /* - * On creation, discard any stale kick proposals, clear the vote - * tallies, and fill the message list box. - */ - if (fresh) { - while (Session.KickProposals.Count()) { - delete Session.KickProposals[0]; - Session.KickProposals.Delete_Index(0); - } - memset(Session.KickVoteCount, 0, sizeof(Session.KickVoteCount)); - memset(Session.KickVoteWho, 0xFF, sizeof(Session.KickVoteWho)); - - HWND listbox = GetDlgItem(disconnect_dialog, IDC_DISCONNECT_MESSAGES); - if (listbox) { - if (reconn) { - //............................................................... - // Find the index of the person we're trying to reconnect to - //............................................................... - j = 0x7fffffff; - oldest_index = 0; - for (i = 0; i < num_conn; i++) { - if (their[i].frame < j) { - j = their[i].frame; - oldest_index = i; - } - } - if (Session.Type == GAME_IPX || Session.Type == GAME_INTERNET) { - sprintf(buf, Fetch_String(TXT_RECONNECTING_TO), Ipx.Connection_Name(Ipx.Connection_ID(oldest_index))); - } else { - sprintf(buf, Fetch_String(TXT_RECONNECTING_TO), Session.Players[1]->Name); - } - ListBox_AddString(listbox, buf); - ListBox_AddString(listbox, ""); - if (Session.Type == GAME_INTERNET) { - ListBox_AddString(listbox, Fetch_String(TXT_RECONNECT_HELP3)); - ListBox_AddString(listbox, Fetch_String(TXT_RECONNECT_HELP3B)); - ListBox_AddString(listbox, Fetch_String(TXT_RECONNECT_HELP3C)); - } - ListBox_AddString(listbox, Fetch_String(TXT_RECONNECT_HELP2)); - if (Session.Type == GAME_INTERNET) { - ListBox_AddString(listbox, Fetch_String(TXT_RECONNECT_HELP2B)); - } else if (Session.Type == GAME_IPX) { - ListBox_AddString(listbox, Fetch_String(TXT_RECONNECT_HELP4)); - } - ListBox_AddString(listbox, ""); - ListBox_AddString(listbox, Fetch_String(TXT_RECONNECT_HELP5)); - ListBox_AddString(listbox, Fetch_String(TXT_RECONNECT_HELP1)); - ListBox_AddString(listbox, ""); - } else { - sprintf(buf, Fetch_String(TXT_WAITING_FOR_CONNECTIONS)); - ListBox_AddString(listbox, buf); - } - } - } + //------------------------------------------------------------------------ + /// If the user gave up, bail out of the game. + //------------------------------------------------------------------------ + if (screen->Cancelled) { + UI_Reconnect_Close(); + TacticalActive = true; + Map.Flag_To_Redraw(GS_REDRAW_ALL); + return(1); } return(0); } // end of Process_Reconnect_Dialog -static int SyncNameButtonControlsIDs[MAX_PLAYERS] = { - IDC_DISCONNECT_PLAYER1, - IDC_DISCONNECT_PLAYER2, - IDC_DISCONNECT_PLAYER3, - IDC_DISCONNECT_PLAYER4, - IDC_DISCONNECT_PLAYER5, - IDC_DISCONNECT_PLAYER6, - IDC_DISCONNECT_PLAYER7, - IDC_DISCONNECT_PLAYER8 -}; -static int SyncBarControlIDs[MAX_PLAYERS] = { - IDC_DISCONNECT_PLAYER1_BOX, - IDC_DISCONNECT_PLAYER2_BOX, - IDC_DISCONNECT_PLAYER3_BOX, - IDC_DISCONNECT_PLAYER4_BOX, - IDC_DISCONNECT_PLAYER5_BOX, - IDC_DISCONNECT_PLAYER6_BOX, - IDC_DISCONNECT_PLAYER7_BOX, - IDC_DISCONNECT_PLAYER8_BOX -}; - /// /// Fetches the connection index for a player. @@ -2451,50 +2372,6 @@ static int Connection_Index(int player) { return(Ipx.Connection_Index(player)); } - - -/// -/// Draws the frame sync bars on the reconnect dialog. -/// Every player in the game gets a bar that shrinks and changes color as the wait on that -/// player drags on, so the humans can see who the game is actually stalled on. -/// -/// The reconnect dialog that owns the bar controls. -void Draw_Sync_Bars(HWND window) -{ - for (int i = 0; i < Session.Players.Count(); i++) { - RECT bar_winrect; - Get_Display_Rect(GetDlgItem(window, SyncBarControlIDs[i]), &bar_winrect); - - Rect bar_rect; - bar_rect.X = bar_winrect.left; - bar_rect.Y = bar_winrect.top; - bar_rect.Width = bar_winrect.right - bar_winrect.left; - bar_rect.Height = bar_winrect.bottom - bar_winrect.top; - - int playerid = Connection_Index(Session.Players[i]->Player.ID); - - unsigned progress; - if (i == 0) { - progress = 0; - } else { - progress = SyncWaitElapsed - SyncBarFrameSync[playerid].timing; - } - - unsigned short color = DSurface::Build_Hicolor_Pixel(0, 200, 0); - if (progress > 240) { - color = DSurface::Build_Hicolor_Pixel(200, 200, 0); - if (progress > 480) { - color = DSurface::Build_Hicolor_Pixel(200, 0, 0); - } - } - - int w = std::max(100 - (int)(100 * progress / 1200), 0) * bar_rect.Width; - bar_rect.Width = std::max(6, w / 100); - - AlternateSurface->Fill_Rect(AlternateSurface->Get_Rect(), bar_rect, color); - } -} - bool Cast_Kick_Vote(int kicker, int kickee); @@ -2558,6 +2435,13 @@ static bool Kick_Proposal_Already_Pending(int kicker, int kickee) } +bool Kick_Vote_Is_Possible(int kicker, int kickee) +{ + return(Current_Player_From_ID(kicker) != NULL && Current_Player_From_ID(kickee) != NULL + && !Kick_Vote_Already_Cast(kicker, kickee)); +} + + /// /// Removes a departing player as both a kick target and a voter, including pending proposals. /// @@ -2599,75 +2483,6 @@ void Forget_Kick_Player(int player) } } -/// -/// Trims the message list box and scrolls it to the end. -/// Use this routine after adding a line to the reconnect dialog's message list, so that -/// the list stays a manageable length and the newest message stays in view. -/// -/// The list box to trim. -void ListBox_Trim(HWND listbox) -{ - int string_count = ListBox_GetCount(listbox); - if (string_count > 50) { - ListBox_DeleteString(listbox, 0); - string_count--; - } - ListBox_SetTopIndex(listbox, string_count - 1); -} - - -/// -/// Proposes that a player be kicked out of the game. -/// This routine is called when one of the kick buttons on the reconnect dialog is pressed. -/// The proposal is sent to every other player and the local vote is cast right away. -/// Proposing to kick yourself, or to kick anybody at all during a tournament game, earns -/// nothing but a message in the dialog. -/// -/// The reconnect dialog to report the outcome in. -/// Index into the session player list of the one to be kicked. -void Propose_Kick_Player(HWND window, int id) -{ - if (id < 0 || id >= Session.Players.Count()) { - return; - } - - DebugString("Propose_Kick_Player %d - %s. Local id is %d\n", id, Session.Players[id]->Name, Session.Players[0]->Player.ID); - HWND listbox = GetDlgItem(window, IDC_DISCONNECT_MESSAGES); - - if (id == 0) { - ListBox_AddString(listbox, Fetch_String(TXT_RECONNECT_KICK_SELF)); - ListBox_Trim(listbox); - return; - } - - if (Session.Type == GAME_INTERNET && WestwoodOnline_Tournament) { - ListBox_AddString(listbox, Fetch_String(TXT_CANT_KICK)); - ListBox_Trim(listbox); - return; - } - - int const kicker = Session.Players[0]->Player.ID; - int const kickee = Session.Players[id]->Player.ID; - if (Current_Player_From_ID(kicker) == NULL || Current_Player_From_ID(kickee) == NULL - || Kick_Vote_Already_Cast(kicker, kickee)) { - return; - } - - GlobalPacketType gpacket; - NetGlobal::Initialize_Packet(gpacket, NET_PROPOSE_KICK); - strncpy(gpacket.Name, Session.Players[0]->Name, ARRAY_SIZE(gpacket.Name) - 1); - gpacket.Name[ARRAY_SIZE(gpacket.Name) - 1] = '\0'; - gpacket.Kick.KickerID = static_cast(kicker); - gpacket.Kick.KickeeID = static_cast(kickee); - - for (int i = 1; i < Session.Players.Count(); i++) { - DebugString("Sending kick proposal to %s\n", Session.Players[i]->Name); - Ipx.Send_Global_Message(&gpacket, sizeof(gpacket), 1, &Session.Players[i]->Address); - } - - Cast_Kick_Vote(kicker, kickee); -} - /// /// Handles a kick proposal arriving from another player. @@ -2745,126 +2560,16 @@ bool Cast_Kick_Vote(int kicker, int kickee) snprintf(buffer, sizeof(buffer), Fetch_String(TXT_RECONNECT_KICK_RECEIVED), kicker_player->Name, kickee_player->Name); - HWND topwindow = WS_Top_Window(); - HWND listbox = GetDlgItem(topwindow, IDC_DISCONNECT_MESSAGES); - ListBox_AddString(listbox, buffer); - ListBox_Trim(listbox); + UIReconnectPresenterClass * const screen = UI_Reconnect_Screen(); + if (screen != NULL) { + screen->Record_Message(buffer); + } } return(true); } -/// -/// Handles the messages for the reconnect dialog. -/// This is the dialog that appears when the game stalls waiting on somebody. It paints the -/// per-player sync bars and offers a kick button for each player in the game. -/// -INT_PTR CALLBACK Reconnect_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - int * rc = (int *)GetWindowLongPtr(window, DWLP_USER); - - switch (message) { - case IDCANCEL: - Remove_Modeless_Dialog(window); - break; - - case WM_DRAWITEM: - OwnerDraw::Draw_Item((DRAWITEMSTRUCT *)lparam); - return(TRUE); - - case WM_PAINT: - OwnerDraw::Draw_Dialog_Back(window); - Draw_Sync_Bars(window); - ValidateRect(window, NULL); - break; - - case WM_INITDIALOG: { - OwnerDraw::Subclass_Dialog(window, 0); - Center_Window_Within_Window(window); - Add_Modeless_Dialog(window); - - int i; - for (i = 0; i < MAX_PLAYERS; i++) { - HWND button = GetDlgItem(window, SyncNameButtonControlsIDs[i]); - EnableWindow(button, FALSE); - HWND bar = GetDlgItem(window, SyncBarControlIDs[i]); - EnableWindow(bar, FALSE); - } - - for (i = 0; i < MAX_PLAYERS; i++) { - HWND button = GetDlgItem(window, SyncNameButtonControlsIDs[i]); - HWND bar = GetDlgItem(window, SyncBarControlIDs[i]); - if (i < Session.Players.Count()) { - SendMessage(button, WM_SETTEXT, 0, (LPARAM)Session.Players[i]->Name); - EnableWindow(button, TRUE); - EnableWindow(bar, TRUE); - } else { - DestroyWindow(button); - DestroyWindow(bar); - } - } - break; - } - - case WM_MOVING: - return(On_WM_MOVING(window, wparam, lparam)); - - case WM_CTLCOLORMSGBOX: - case WM_CTLCOLOREDIT: - case WM_CTLCOLORLISTBOX: - case WM_CTLCOLORBTN: - case WM_CTLCOLORDLG: - case WM_CTLCOLORSCROLLBAR: - case WM_CTLCOLORSTATIC: - return((INT_PTR)GetStockObject(BLACK_BRUSH)); - - case WM_ERASEBKGND: - return(TRUE); - - case WM_COMMAND: - switch (LOWORD(wparam)) { - case IDC_DISCONNECT_PLAYER1: - Propose_Kick_Player(window, 0); - break; - - case IDC_DISCONNECT_PLAYER2: - Propose_Kick_Player(window, 1); - break; - - case IDC_DISCONNECT_PLAYER3: - Propose_Kick_Player(window, 2); - break; - - case IDC_DISCONNECT_PLAYER4: - Propose_Kick_Player(window, 3); - break; - - case IDC_DISCONNECT_PLAYER5: - Propose_Kick_Player(window, 4); - break; - - case IDC_DISCONNECT_PLAYER6: - Propose_Kick_Player(window, 5); - break; - - case IDC_DISCONNECT_PLAYER7: - Propose_Kick_Player(window, 6); - break; - - case IDC_DISCONNECT_PLAYER8: - Propose_Kick_Player(window, 7); - break; - - case IDCANCEL: - *rc = IDCANCEL; - break; - } - break; - } - - return(FALSE); -} /// The name comes from the TS demo build, which ships this routine with symbols. @@ -2877,11 +2582,12 @@ INT_PTR CALLBACK Reconnect_Dialog_Proc(HWND window, UINT message, WPARAM wparam, static void Close_Reconnect_Dialog(void) { //------------------------------------------------------------------------ - // If the reconnect dialog was shown, force the map to redraw. + // If the reconnect screen was shown, force the map to redraw. //------------------------------------------------------------------------ - HWND dialog = WS_Find_Dialog(IDD_MPLAYER_DISCONNECT); - if (dialog) { - WS_Destroy_Dialog(dialog, false); + bool const shown = UI_Reconnect_Has_View(); + UI_Reconnect_Close(); + + if (shown) { TacticalActive = true; Map.Flag_To_Redraw(GS_REDRAW_ALL); Map.Render(); diff --git a/code/queue.h b/code/queue.h index 539252dd0..cf145e9cc 100644 --- a/code/queue.h +++ b/code/queue.h @@ -56,5 +56,10 @@ NetGlobal::DecodeError Kick_Packet_Received(int kicker, int kickee); void Forget_Kick_Player(int player); +// Is a vote to kick worth putting to the other players? False when either side is no longer +// in the session or the voter has already cast this vote, which is what stopped a repeated +// press from sending the proposal again. +bool Kick_Vote_Is_Possible(int kicker, int kickee); + extern BasicTimerClass SentFrameSyncTimer; extern int SentFrameSyncCount; diff --git a/code/rawfile.cpp b/code/rawfile.cpp index 609822d7b..3520e5724 100644 --- a/code/rawfile.cpp +++ b/code/rawfile.cpp @@ -50,13 +50,24 @@ #include "always.h" #include "rawfile.h" +#include "file.h" #include #include #include #include -#include -#include + +#ifndef _WIN32 +#include +#include +#include +#include +#define _unlink unlink +#define fopen(x, y) fopen(x, y) +#else +#include +#include +#endif /*********************************************************************************************** @@ -78,10 +89,9 @@ RawFileClass::~RawFileClass(void) { Close(); - if (Allocated && Filename) { + if (Filename) { free((char *)Filename); ((char *&)Filename) = 0; - Allocated = false; } } @@ -135,12 +145,13 @@ RawFileClass::RawFileClass(char const * filename) : Rights(0), BiasStart(0), BiasLength(-1), - Handle(NULL_HANDLE), - Filename(filename), + Handle(nullptr), + Filename(nullptr), Date(0), Time(0), - Allocated(false) + LastAccessType(0) { + Set_Name(filename); } @@ -166,10 +177,9 @@ RawFileClass::RawFileClass(char const * filename) : *=============================================================================================*/ char const * RawFileClass::Set_Name(char const * filename) { - if (Filename != NULL && Allocated) { + if (Filename != NULL) { free((char *)Filename); - Filename = NULL; - Allocated = false; + Filename = nullptr; } if (filename == NULL) return(NULL); @@ -181,7 +191,18 @@ char const * RawFileClass::Set_Name(char const * filename) Error(ENOMEM, false, filename); return(NULL); } - Allocated = true; + + /* + ** If we ever save this file, make sure we save it in lowercase but + ** if Resolve_File finds an actual file on-disk we use the real name + ** instead. + */ + _strlwr(Filename); + + /* + ** Try to locate an existing file ignoring case, updates Filename + */ + Resolve_File(Filename); return(Filename); } @@ -236,6 +257,7 @@ int RawFileClass::Open(char const * filename, int rights) int RawFileClass::Open(int rights) { Close(); + LastAccessType = 0; /* ** Verify that there is a filename associated with this file object. If not, then this is a @@ -267,23 +289,24 @@ int RawFileClass::Open(int rights) ** an invalid access code. */ default: + errno = EINVAL; break; case READ: - Handle = CreateFile(Filename, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, - NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL); + Handle = fopen(Filename, "rb"); break; case WRITE: - Handle = CreateFile(Filename, GENERIC_WRITE, 0, - NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + Handle = fopen(Filename, "wb"); break; case READ|WRITE: - // SKB 5/13/99 use OPEN_ALWAYS instead of CREATE_ALWAYS so that files + // SKB 5/13/99 try "r+" first before using "w+" so that files // does not get destroyed. - Handle = CreateFile(Filename, GENERIC_READ | GENERIC_WRITE, 0, - NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + Handle = fopen(Filename, "r+b"); + if (Handle == nullptr) { + Handle = fopen(Filename, "w+b"); + } break; } @@ -299,10 +322,10 @@ int RawFileClass::Open(int rights) ** For the case of the file cannot be found, then allow a retry. All other cases ** are fatal. */ - if (Handle == NULL_HANDLE) { + if (Handle == nullptr) { return(false); -// Error(GetLastError(), false, Filename); +// Error(errno, false, Filename); // continue; } break; @@ -354,22 +377,18 @@ bool RawFileClass::Is_Available(int forced) ** CD-ROM, this routine will return a failure condition. In all but the missing file ** condition, go through the normal error recover channels. */ - for (;;) { - Handle = CreateFile(Filename, GENERIC_READ, FILE_SHARE_READ, - NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); - if (Handle == NULL_HANDLE) { - return(false); - } - break; + Handle = fopen(Filename, "r"); + if (Handle == nullptr) { + return(false); } /* ** Since the file could be opened, then close it and return that the file exists. */ - if (!CloseHandle(Handle)) { - Error(GetLastError(), false, Filename); + if (fclose(Handle) != 0) { + Error(errno, false, Filename); } - Handle = NULL_HANDLE; + Handle = nullptr; return(true); } @@ -401,14 +420,20 @@ void RawFileClass::Close(void) ** Try to close the file. If there was an error (who knows what that could be), then ** call the error routine. */ - if (!CloseHandle(Handle)) { - Error(GetLastError(), false, Filename); + if (fclose(Handle) != 0) { + Error(errno, false, Filename); } /* ** At this point the file must have been closed. Mark the file as empty and return. */ - Handle = NULL_HANDLE; + Handle = nullptr; + + /* + ** Clear any positioning information incase class is reused to open another file. + */ + BiasStart = 0; + BiasLength = -1; } } @@ -464,18 +489,24 @@ int RawFileClass::Read(void * buffer, int size) size = size < remainder ? size : remainder; } + if (!opened && LastAccessType != 0 && LastAccessType != READ) { + if (fseek(Handle, ftell(Handle), SEEK_SET) < 0) { + Error(errno, false, Filename); + return(0); + } + } + LastAccessType = READ; + int total = 0; while (size > 0) { - bytesread = 0; - - if (!ReadFile(Handle, buffer, size, &(DWORD &)bytesread, NULL)) { - buffer = (char *)buffer + bytesread; + clearerr(Handle); + bytesread = fread(buffer, 1, size, Handle); + if (ferror(Handle)) { size -= bytesread; total += bytesread; - Error(GetLastError(), true, Filename); + Error(errno, true, Filename); continue; } - buffer = (char *)buffer + bytesread; size -= bytesread; total += bytesread; if (bytesread == 0) break; @@ -526,8 +557,18 @@ int RawFileClass::Write(void const * buffer, int size) opened = true; } - if (!WriteFile(Handle, buffer, size, &(DWORD &)byteswritten, NULL)) { - Error(GetLastError(), false, Filename); + if (!opened && LastAccessType != 0 && LastAccessType != WRITE) { + if (fseek(Handle, ftell(Handle), SEEK_SET) < 0) { + Error(errno, false, Filename); + return(0); + } + } + LastAccessType = WRITE; + + clearerr(Handle); + byteswritten = fwrite(buffer, 1, size, Handle); + if (ferror(Handle)) { + Error(errno, false, Filename); } /* @@ -660,15 +701,30 @@ int RawFileClass::Size(void) */ if (Is_Open()) { - size = GetFileSize(Handle, NULL); - /* - ** If there was in internal error, then call the error function. + ** With stdio we seek to end to obtain the length, then reset the position back. */ - if (size == 0xFFFFFFFF) { - Error(GetLastError(), false, Filename); + clearerr(Handle); + + int position = ftell(Handle); + if (position < 0) { + Error(errno, false, Filename); + return(0); } + if (fseek(Handle, 0, SEEK_END) < 0) { + Error(errno, false, Filename); + return(0); + } + + size = ftell(Handle); + + if (fseek(Handle, position, SEEK_SET) < 0) { + Error(errno, false, Filename); + return(0); + } + LastAccessType = 0; + } else { /* @@ -777,8 +833,8 @@ int RawFileClass::Delete(void) return(false); } - if (!DeleteFile(Filename)) { - Error(GetLastError(), false, Filename); + if (_unlink(Filename) < 0) { + Error(errno, false, Filename); return(false); } break; @@ -809,14 +865,38 @@ int RawFileClass::Delete(void) *=============================================================================================*/ unsigned int RawFileClass::Get_Date_Time(void) { - BY_HANDLE_FILE_INFORMATION info; +#ifdef _WIN32 + if (RawFileClass::Is_Open()) { + BY_HANDLE_FILE_INFORMATION info; + HANDLE osHandle = (HANDLE)_get_osfhandle(_fileno(Handle)); + + if (osHandle != INVALID_HANDLE_VALUE && + GetFileInformationByHandle(osHandle, &info)) { + WORD dosdate; + WORD dostime; + FileTimeToDosDateTime(&info.ftLastWriteTime, &dosdate, &dostime); + return((dosdate << 16) | dostime); + } + } +#else + struct stat statbuf; + + if (stat(Filename, &statbuf) == 0) { + struct tm *parsed_time = localtime(&statbuf.st_mtime); + + if (parsed_time != NULL) { + Date = (((parsed_time->tm_year - 80) & ((1 << 7) - 1)) << 9) | + (((parsed_time->tm_mon + 1) & ((1 << 4) - 1)) << 5) | + (parsed_time->tm_mday & ((1 << 5) - 1)); - if (GetFileInformationByHandle(Handle, &info)) { - WORD dosdate; - WORD dostime; - FileTimeToDosDateTime(&info.ftLastWriteTime, &dosdate, &dostime); - return((dosdate << 16) | dostime); + Time = ((parsed_time->tm_hour & ((1 << 5) - 1)) << 11) | + ((parsed_time->tm_min & ((1 << 6) - 1)) << 5) | + ((parsed_time->tm_sec >> 1) & ((1 << 5) - 1)); + + return(Date << 16 | Time); + } } +#endif return(0); } @@ -838,16 +918,45 @@ unsigned int RawFileClass::Get_Date_Time(void) *=============================================================================================*/ bool RawFileClass::Set_Date_Time(unsigned int datetime) { +#ifdef _WIN32 if (RawFileClass::Is_Open()) { BY_HANDLE_FILE_INFORMATION info; + HANDLE osHandle = (HANDLE)_get_osfhandle(_fileno(Handle)); - if (GetFileInformationByHandle(Handle, &info)) { + if (osHandle != INVALID_HANDLE_VALUE && + GetFileInformationByHandle(osHandle, &info)) { FILETIME filetime; if (DosDateTimeToFileTime((WORD)(datetime >> 16), (WORD)(datetime & 0x0FFFF), &filetime)) { - return(SetFileTime(Handle, &info.ftCreationTime, &filetime, &filetime) != 0); + return(SetFileTime(osHandle, &info.ftCreationTime, &filetime, &filetime) != 0); } } } +#else + struct tm input_time = { 0 }; + time_t unix_time; + + Date = (datetime >> 16) & 0xFFFF; + Time = datetime & 0xFFFF; + + input_time.tm_year = ((Date >> 9) & ((1 << 7) - 1)) + 80; + input_time.tm_mon = ((Date >> 5) & ((1 << 4) - 1)) - 1; + input_time.tm_mday = Date & ((1 << 5) - 1); + + input_time.tm_hour = (Time >> 11) & ((1 << 5) - 1); + input_time.tm_min = (Time >> 5) & ((1 << 6) - 1); + input_time.tm_sec = (Time & ((1 << 5) - 1)) << 1; + + input_time.tm_isdst = -1; + + unix_time = mktime(&input_time); + if (unix_time >= 0) { + struct utimbuf buf = { 0 }; + buf.actime = unix_time; + buf.modtime = unix_time; + + return(utime(Filename, &buf) == 0); + } +#endif return(false); } @@ -923,30 +1032,25 @@ int RawFileClass::Raw_Seek(int pos, int dir) */ if (!Is_Open()) { Error(EBADF, false, Filename); - return(0); - } - - switch (dir) { - case SEEK_SET: - dir = FILE_BEGIN; - break; + } else { - case SEEK_CUR: - dir = FILE_CURRENT; - break; + clearerr(Handle); - case SEEK_END: - dir = FILE_END; - break; - } - pos = SetFilePointer(Handle, pos, NULL, dir); + /* + ** If pos == 0 and dir == SEEK_CUR, fseek should basically do nothing. + ** However, some very bad implementations (like the Nintendo DS's libfat) + ** just goes back to the beginning of the file and iterate it until it + ** finds the current position, which is awful. So instead of doing that, + ** guard this case so that sequential ::Read's do not take too much time. + */ + if (!(pos == 0 && dir == SEEK_CUR)) { + if (fseek(Handle, pos, dir) < 0) { + Error(errno, false, Filename); + } + LastAccessType = 0; + } - /* - ** If there was an error in the seek, then bail with an error condition. - */ - if (pos == 0xFFFFFFFF) { - Error(GetLastError(), false, Filename); - return(0); + pos = ftell(Handle); } /* diff --git a/code/rawfile.h b/code/rawfile.h index a35451f09..52cf7b12a 100644 --- a/code/rawfile.h +++ b/code/rawfile.h @@ -41,10 +41,8 @@ #include #include #include -#include +#include -#define NULL_HANDLE INVALID_HANDLE_VALUE -#define HANDLE_TYPE HANDLE #ifndef WWERROR #define WWERROR -1 #endif @@ -95,7 +93,7 @@ class RawFileClass : public FileClass virtual bool Set_Date_Time(unsigned int datetime); virtual void Error(int error, int canretry = false, char const * filename=NULL) override; void Bias(int start, int length=-1); - HANDLE_TYPE Get_File_Handle(void) { return(Handle); }; + FILE *Get_File_Handle(void) { return(Handle); }; /* ** These bias values enable a sub-portion of a file to appear as if it @@ -118,15 +116,19 @@ class RawFileClass : public FileClass private: /* - ** This is the low level DOS handle. A -1 indicates an empty condition. + ** This is the file handle. A nullptr indicates an empty condition. */ - HANDLE_TYPE Handle; + FILE *Handle; /* - ** This points to the filename as a NULL terminated string. It may point to either a - ** constant or an allocated string as indicated by the "Allocated" flag. + ** This points to a copy of the filename as a NULL terminated string. */ - char const * Filename; + char *Filename; + + /* + ** The type of the last file access operation. Reset by fseek(). + */ + int LastAccessType; // // file date and time are in the following formats: @@ -141,15 +143,6 @@ class RawFileClass : public FileClass // unsigned short Date; unsigned short Time; - - /* - ** Filenames that were assigned as part of the construction process - ** are not allocated. It is assumed that the filename string is a - ** constant in that case and thus making duplication unnecessary. - ** This value will be non-zero if the filename has be allocated - ** (using strdup()). - */ - bool Allocated; }; @@ -195,11 +188,11 @@ inline RawFileClass::RawFileClass(void) : Rights(READ), BiasStart(0), BiasLength(-1), - Handle(INVALID_HANDLE_VALUE), - Filename(0), + Handle(nullptr), + Filename(nullptr), Date(0), Time(0), - Allocated(false) + LastAccessType(0) { } @@ -221,5 +214,5 @@ inline RawFileClass::RawFileClass(void) : *=============================================================================================*/ inline bool RawFileClass::Is_Open(void) const { - return(Handle != INVALID_HANDLE_VALUE); + return(Handle != nullptr); } diff --git a/code/reinf.cpp b/code/reinf.cpp index 540a86469..4cfb87095 100644 --- a/code/reinf.cpp +++ b/code/reinf.cpp @@ -50,7 +50,7 @@ #include "foot.h" #include "globals.h" #include "house.h" -#include "ilocos.h" +#include "classids.h" #include "incdec.h" #include "inline.h" #include "mouse.h" @@ -533,7 +533,7 @@ inline bool _Can_Burrow(FootClass * object) { while (object != NULL) { TechnoTypeClass const * tclass = object->TClass; - if (tclass->Locomotor != CLSID_TunnelLocomotion) { + if (tclass->Locomotor != ClassID_TunnelLocomotion) { return(false); } object = (FootClass *)object->Next; diff --git a/code/restate.cpp b/code/restate.cpp index 752485b16..b58742e78 100644 --- a/code/restate.cpp +++ b/code/restate.cpp @@ -29,7 +29,7 @@ #include "msanim.h" #include "msengine.h" #include "msfont.h" -#include "ownrdraw.h" +#include "drawhelp.h" #include "rules.h" #include "scenario.h" #include "srfcache.h" @@ -118,7 +118,10 @@ class MyButton : public TextButtonClass { char buffer[40]; sprintf(buffer, "b%ce_li%d.pcx", IsPressed != false ? 'd' : 'u', height); - Surface * image = SurfaceCache.GetSurface(buffer); + Surface * image = OD_Fetch_Image(buffer); + if (image == NULL) { + return; + } origin.Height = image->Get_Height(); dest_rect = origin; dest_rect.Width = small_width; @@ -130,7 +133,10 @@ class MyButton : public TextButtonClass { HiddenSurface->Blit_From(dest_rect, *image, source_rect); sprintf(buffer, "b%ce_mi%d.pcx", IsPressed != false ? 'd' : 'u', height); - image = SurfaceCache.GetSurface(buffer); + image = OD_Fetch_Image(buffer); + if (image == NULL) { + return; + } rect = origin; rect.X += small_width; rect.Width -= width; @@ -138,7 +144,10 @@ class MyButton : public TextButtonClass { SurfaceCache.Draw(rect, *HiddenSurface, *image, 0, 0); sprintf(buffer, "b%ce_ri%d.pcx", IsPressed != false ? 'd' : 'u', height); - image = SurfaceCache.GetSurface(buffer); + image = OD_Fetch_Image(buffer); + if (image == NULL) { + return; + } dest_rect = origin; dest_rect.X += origin.Width - width; dest_rect.Width = width; diff --git a/code/revent.cpp b/code/revent.cpp index 8d0a51dee..dedf27157 100644 --- a/code/revent.cpp +++ b/code/revent.cpp @@ -366,20 +366,19 @@ void RadarEventClass::Get_Event_Rect(Point2D (& event_rect)[4]) const /// /// The stream to write the radar events to. /// bool; Were the events written successfully? -bool RadarEventClass::Save(IStream * stream) +bool RadarEventClass::Save(SaveStreamClass & stream) { - SaveStreamClass savestream(stream, SaveStreamClass::MODE_SAVE); int count = RadarEvents.Count(); - savestream.Serialize(count); + stream.Serialize(count); for (int index = 0; index < count; index++) { - RadarEvents[index]->Serialize(savestream); + RadarEvents[index]->Serialize(stream); } - savestream.Serialize(LastRadarEventCell); + stream.Serialize(LastRadarEventCell); - return(SUCCEEDED(savestream.Result())); + return(!stream.Was_Error()); } @@ -390,27 +389,26 @@ bool RadarEventClass::Save(IStream * stream) /// /// The stream to read the radar events from. /// bool; Were the events read successfully? -bool RadarEventClass::Load(IStream * stream) +bool RadarEventClass::Load(SaveStreamClass & stream) { for (int i = RadarEvents.Count() - 1; i >= 0; i--) { delete RadarEvents[i]; RadarEvents.Delete_Index(i); } - SaveStreamClass savestream(stream, SaveStreamClass::MODE_LOAD); - savestream.Set_Context("RadarEventClass"); + stream.Set_Context("RadarEventClass"); int count = 0; - savestream.Serialize(count); + stream.Serialize(count); for (int index = 0; index < count; index++) { RadarEventClass * event = new RadarEventClass(RADAREVENT_NONE, Cell(0, 0)); - event->Serialize(savestream); + event->Serialize(stream); } - savestream.Serialize(LastRadarEventCell); + stream.Serialize(LastRadarEventCell); - return(SUCCEEDED(savestream.Result())); + return(!stream.Was_Error()); } diff --git a/code/revent.h b/code/revent.h index 4c3264376..7f1aee6e5 100644 --- a/code/revent.h +++ b/code/revent.h @@ -18,15 +18,15 @@ #include "revent.hh" -struct IStream; +class SaveStreamClass; class SaveStreamClass; template class DynamicVectorClass; class RadarEventClass { public: - static bool Save(IStream * stream); - static bool Load(IStream * stream); + static bool Save(SaveStreamClass & stream); + static bool Load(SaveStreamClass & stream); public: RadarEventClass(RadarEventType event, Cell cell); diff --git a/code/rgb.h b/code/rgb.h index 66f76ab6d..b17332ba7 100644 --- a/code/rgb.h +++ b/code/rgb.h @@ -53,6 +53,8 @@ struct RGBStruct }; #pragma pack() +static_assert(sizeof(RGBStruct) == 3, "Palette entry layout changed"); + /* ** Each color entry is represented by this class. It holds the values for the color diff --git a/code/rules.cpp b/code/rules.cpp index 0721f0374..30ae73fea 100644 --- a/code/rules.cpp +++ b/code/rules.cpp @@ -2090,10 +2090,9 @@ bool RulesClass::Do_Movies(CCINIClass const & ini) /// /// Writes the rule data out to a save game stream. /// -void RulesClass::Save(IStream * stream) +void RulesClass::Save(SaveStreamClass & stream) { - SaveStreamClass savestream(stream, SaveStreamClass::MODE_SAVE); - Serialize(savestream); + Serialize(stream); } @@ -2102,11 +2101,10 @@ void RulesClass::Save(IStream * stream) /// /// Be sure the object heaps have been loaded before calling this routine, since /// the pointer swizzle needs them. -void RulesClass::Load(IStream * stream) +void RulesClass::Load(SaveStreamClass & stream) { - SaveStreamClass savestream(stream, SaveStreamClass::MODE_LOAD); - savestream.Set_Context("RulesClass"); - Serialize(savestream); + stream.Set_Context("RulesClass"); + Serialize(stream); } diff --git a/code/rules.h b/code/rules.h index 1b8eaa80c..f05b06927 100644 --- a/code/rules.h +++ b/code/rules.h @@ -138,8 +138,8 @@ class RulesClass bool Do_Movies(CCINIClass const & ini); bool Objects(CCINIClass const & ini); - void Save(IStream * stream); - void Load(IStream * stream); + void Save(SaveStreamClass & stream); + void Load(SaveStreamClass & stream); void Serialize(SaveStreamClass & stream); diff --git a/code/savefile.cpp b/code/savefile.cpp new file mode 100644 index 000000000..e8e3738f5 --- /dev/null +++ b/code/savefile.cpp @@ -0,0 +1,571 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#include "savefile.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace { + +unsigned char const Signature[4] = { 'O', 'T', 'S', 'V' }; + +constexpr unsigned int FLAG_LZO = 0x0001; +constexpr unsigned int FIELD_HEADER_SIZE = 8; +constexpr unsigned int MAX_FIELD_LENGTH = 0x10000; +// No game state comes near this, and a header asking for more is asking for memory. +constexpr unsigned int MAX_CONTENT_LENGTH = 0x10000000; +// A listing is a dozen short fields; a table beyond this is not one. +constexpr unsigned int MAX_TABLE_LENGTH = 0x100000; + + +unsigned int Get_U16(unsigned char const * from) +{ + return((unsigned int)from[0] | ((unsigned int)from[1] << 8)); +} + + +unsigned int Get_U32(unsigned char const * from) +{ + return((unsigned int)from[0] | ((unsigned int)from[1] << 8) + | ((unsigned int)from[2] << 16) | ((unsigned int)from[3] << 24)); +} + + +void Put_U16(unsigned char * into, unsigned int value) +{ + into[0] = (unsigned char)(value & 0xFF); + into[1] = (unsigned char)((value >> 8) & 0xFF); +} + + +void Put_U32(unsigned char * into, unsigned int value) +{ + into[0] = (unsigned char)(value & 0xFF); + into[1] = (unsigned char)((value >> 8) & 0xFF); + into[2] = (unsigned char)((value >> 16) & 0xFF); + into[3] = (unsigned char)((value >> 24) & 0xFF); +} + + +void Append(std::vector & into, void const * data, unsigned int length) +{ + unsigned char const * bytes = (unsigned char const *)data; + into.insert(into.end(), bytes, bytes + length); +} + + +// Sizes a buffer the header asked for, and says so rather than throw when the process +// cannot hold it. +bool Reserve(std::vector & buffer, std::size_t length) +{ + try { + buffer.resize(length); + } catch (std::bad_alloc const &) { + buffer.clear(); + return(false); + } + return(true); +} + + +bool Read_Range(std::FILE * file, void * into, unsigned int length) +{ + unsigned char * cursor = (unsigned char *)into; + + while (length > 0) { + std::size_t const got = std::fread(cursor, 1, length, file); + if (got == 0) return(false); + cursor += got; + length -= (unsigned int)got; + } + + return(true); +} + + +bool Write_Range(std::FILE * file, void const * data, unsigned int length) +{ + unsigned char const * cursor = (unsigned char const *)data; + + while (length > 0) { + unsigned int const block = (length > 0x100000) ? 0x100000 : length; + std::size_t const written = std::fwrite(cursor, 1, block, file); + if (written != block) return(false); + cursor += written; + length -= block; + } + + return(true); +} + + +/// +/// Reports the length of an open file without moving the caller's read position. +/// +/// The length in bytes, or -1. +long File_Length(std::FILE * file) +{ + long const here = std::ftell(file); + if (here < 0 || std::fseek(file, 0, SEEK_END) != 0) return(-1); + long const end = std::ftell(file); + std::fseek(file, here, SEEK_SET); + return(end); +} + + +struct HeaderType { + unsigned int Version; + unsigned int Flags; + unsigned int TableLength; + unsigned int ContentOffset; + unsigned int StoredLength; + unsigned int ContentLength; + unsigned int ContentCRC; + unsigned int HeaderCRC; +}; + + +// The header checksum continues over the field table, so a listing can verify what it +// reads without touching the content. +unsigned int Header_CRC(unsigned char const * header, unsigned char const * table, unsigned int length) +{ + return(SaveFileClass::Checksum(table, length, SaveFileClass::Checksum(header, SaveFileClass::HEADER_SIZE - 4))); +} + + +// Decides everything the first 32 bytes can decide, in the order a caller wants to +// hear about it: not ours, a version we do not read, or damage. +SaveFileClass::ResultType Parse_Header(unsigned char const * bytes, unsigned int available, HeaderType & header) +{ + if (available < sizeof(Signature) || memcmp(bytes, Signature, sizeof(Signature)) != 0) { + return(SaveFileClass::RESULT_NOT_A_SAVE); + } + if (available < SaveFileClass::HEADER_SIZE) { + return(SaveFileClass::RESULT_CORRUPT); + } + + header.Version = Get_U16(bytes + 4); + header.Flags = Get_U16(bytes + 6); + header.TableLength = Get_U32(bytes + 8); + header.ContentOffset = Get_U32(bytes + 12); + header.StoredLength = Get_U32(bytes + 16); + header.ContentLength = Get_U32(bytes + 20); + header.ContentCRC = Get_U32(bytes + 24); + header.HeaderCRC = Get_U32(bytes + 28); + + if (header.Version == 0 || header.Version > SaveFileClass::FORMAT_VERSION) { + return(SaveFileClass::RESULT_UNSUPPORTED_VERSION); + } + if ((header.Flags & ~FLAG_LZO) != 0) { + return(SaveFileClass::RESULT_UNSUPPORTED_VERSION); + } + if (header.TableLength > MAX_TABLE_LENGTH) { + return(SaveFileClass::RESULT_CORRUPT); + } + if (header.ContentOffset != SaveFileClass::HEADER_SIZE + header.TableLength) { + return(SaveFileClass::RESULT_CORRUPT); + } + if (header.StoredLength > MAX_CONTENT_LENGTH || header.ContentLength > MAX_CONTENT_LENGTH) { + return(SaveFileClass::RESULT_CORRUPT); + } + + return(SaveFileClass::RESULT_OK); +} + +} // namespace + + +SaveFileClass::SaveFileClass(void) +{ +} + + +unsigned int SaveFileClass::Checksum(unsigned char const * data, unsigned int length, unsigned int seed) +{ + static unsigned int table[256]; + static bool ready = false; + + if (!ready) { + for (unsigned int index = 0; index < 256; index++) { + unsigned int value = index; + for (int bit = 0; bit < 8; bit++) { + value = (value & 1) ? (0xEDB88320u ^ (value >> 1)) : (value >> 1); + } + table[index] = value; + } + ready = true; + } + + unsigned int crc = ~seed; + for (unsigned int index = 0; index < length; index++) { + crc = table[(crc ^ data[index]) & 0xFF] ^ (crc >> 8); + } + + return(~crc); +} + + +char const * SaveFileClass::Result_Text(ResultType result) +{ + switch (result) { + case RESULT_OK: return("ok"); + case RESULT_MISSING: return("the file is missing"); + case RESULT_NOT_A_SAVE: return("the file is not a saved game"); + case RESULT_UNSUPPORTED_VERSION: return("the file uses a format version this build does not read"); + case RESULT_CORRUPT: return("the file is damaged"); + case RESULT_WRITE_FAILED: return("the file could not be written"); + case RESULT_NO_MEMORY: return("there is not enough memory to read the file"); + case RESULT_TOO_LARGE: return("the game state is larger than a saved game can hold"); + } + return("unknown"); +} + + +SaveFileClass::FieldType const * SaveFileClass::Find(int id, int kind) const +{ + for (FieldType const & field : Fields) { + if (field.ID == id && field.Kind == kind) return(&field); + } + return(NULL); +} + + +void SaveFileClass::Set(int id, int kind, void const * data, unsigned int length) +{ + for (FieldType & field : Fields) { + if (field.ID == id && field.Kind == kind) { + field.Bytes.assign((unsigned char const *)data, (unsigned char const *)data + length); + return; + } + } + + FieldType field; + field.ID = id; + field.Kind = kind; + field.Bytes.assign((unsigned char const *)data, (unsigned char const *)data + length); + Fields.push_back(field); +} + + +void SaveFileClass::Set_String(int id, char const * text) +{ + if (text == NULL) text = ""; + Set(id, FIELD_STRING, text, (unsigned int)strlen(text)); +} + + +void SaveFileClass::Set_Int(int id, int value) +{ + unsigned char bytes[4]; + Put_U32(bytes, (unsigned int)value); + Set(id, FIELD_INT, bytes, sizeof(bytes)); +} + + +void SaveFileClass::Set_Time(int id, FILETIME const & time) +{ + unsigned char bytes[8]; + Put_U32(bytes, time.dwLowDateTime); + Put_U32(bytes + 4, time.dwHighDateTime); + Set(id, FIELD_TIME, bytes, sizeof(bytes)); +} + + +// A string that does not fit is truncated to what does; the result is always terminated. +bool SaveFileClass::Get_String(int id, char * text, int size) const +{ + if (text == NULL || size <= 0) return(false); + + FieldType const * const field = Find(id, FIELD_STRING); + if (field == NULL) { + text[0] = '\0'; + return(false); + } + + unsigned int length = (unsigned int)field->Bytes.size(); + if (length > (unsigned int)(size - 1)) { + // A cut never splits a UTF-8 sequence, so a shortened description stays text. + length = (unsigned int)(size - 1); + while (length > 0 && (field->Bytes[length] & 0xC0) == 0x80) length--; + } + memcpy(text, field->Bytes.data(), length); + text[length] = '\0'; + + return(true); +} + + +bool SaveFileClass::Get_Int(int id, int * value) const +{ + FieldType const * const field = Find(id, FIELD_INT); + if (field == NULL || field->Bytes.size() != 4) return(false); + + if (value != NULL) *value = (int)Get_U32(field->Bytes.data()); + return(true); +} + + +bool SaveFileClass::Get_Time(int id, FILETIME * time) const +{ + FieldType const * const field = Find(id, FIELD_TIME); + if (field == NULL || field->Bytes.size() != 8) return(false); + + if (time != NULL) { + time->dwLowDateTime = Get_U32(field->Bytes.data()); + time->dwHighDateTime = Get_U32(field->Bytes.data() + 4); + } + return(true); +} + + +void SaveFileClass::Clear_Fields(void) +{ + Fields.clear(); +} + + +void SaveFileClass::Serialize_Fields(std::vector & table) const +{ + table.clear(); + + for (FieldType const & field : Fields) { + unsigned char head[FIELD_HEADER_SIZE]; + Put_U16(head, (unsigned int)field.ID); + Put_U16(head + 2, (unsigned int)field.Kind); + Put_U32(head + 4, (unsigned int)field.Bytes.size()); + Append(table, head, sizeof(head)); + Append(table, field.Bytes.data(), (unsigned int)field.Bytes.size()); + } +} + + +SaveFileClass::ResultType SaveFileClass::Parse_Fields(unsigned char const * table, unsigned int length) +{ + Fields.clear(); + + unsigned int offset = 0; + while (offset < length) { + if (length - offset < FIELD_HEADER_SIZE) return(RESULT_CORRUPT); + + FieldType field; + field.ID = (int)Get_U16(table + offset); + field.Kind = (int)Get_U16(table + offset + 2); + unsigned int const bytes = Get_U32(table + offset + 4); + offset += FIELD_HEADER_SIZE; + + if (bytes > MAX_FIELD_LENGTH || bytes > length - offset) return(RESULT_CORRUPT); + field.Bytes.assign(table + offset, table + offset + bytes); + offset += bytes; + + Fields.push_back(field); + } + + return(RESULT_OK); +} + + +// The file lands under its final name only once every byte is on disk, so a save +// interrupted at any point leaves the previous file untouched. +SaveFileClass::ResultType SaveFileClass::Write(char const * path) const +{ + if (path == NULL) return(RESULT_WRITE_FAILED); + + // The reader's limits bind the writer too, so a save this build writes is one it reads, + // and one it cannot write leaves the file on disk alone. + if (Content.size() > MAX_CONTENT_LENGTH) return(RESULT_TOO_LARGE); + for (FieldType const & field : Fields) { + if (field.Bytes.size() > MAX_FIELD_LENGTH) return(RESULT_TOO_LARGE); + } + + std::vector table; + Serialize_Fields(table); + if (table.size() > MAX_TABLE_LENGTH) return(RESULT_TOO_LARGE); + + std::vector stored; + unsigned int flags = 0; + + if (!Content.empty()) { + std::vector work(LZO1X_MEM_COMPRESS); + stored.resize(Content.size() + Content.size() / 16 + 64 + 3); + + lzo_uint packed = 0; + int const status = lzo1x_1_compress(Content.data(), (lzo_uint)Content.size(), + stored.data(), &packed, work.data()); + + if (status == LZO_E_OK && packed < Content.size()) { + stored.resize((std::size_t)packed); + flags |= FLAG_LZO; + } else { + stored = Content; + } + } + + std::vector image(HEADER_SIZE); + unsigned char * const header = image.data(); + memcpy(header, Signature, sizeof(Signature)); + Put_U16(header + 4, FORMAT_VERSION); + Put_U16(header + 6, flags); + Put_U32(header + 8, (unsigned int)table.size()); + Put_U32(header + 12, HEADER_SIZE + (unsigned int)table.size()); + Put_U32(header + 16, (unsigned int)stored.size()); + Put_U32(header + 20, (unsigned int)Content.size()); + Put_U32(header + 24, Checksum(stored.data(), (unsigned int)stored.size())); + Put_U32(header + 28, Header_CRC(header, table.data(), (unsigned int)table.size())); + + image.insert(image.end(), table.begin(), table.end()); + image.insert(image.end(), stored.begin(), stored.end()); + + std::string const temporary = std::string(path) + ".tmp"; + + std::FILE * const file = std::fopen(temporary.c_str(), "wb"); + if (file == NULL) return(RESULT_WRITE_FAILED); + + bool ok = Write_Range(file, image.data(), (unsigned int)image.size()); + if (ok) ok = (std::fflush(file) == 0); + if (std::fclose(file) != 0) ok = false; + + if (ok) { + std::error_code error; + std::filesystem::rename(temporary, path, error); + ok = !error; + } + + if (!ok) { + std::error_code error; + std::filesystem::remove(temporary, error); + return(RESULT_WRITE_FAILED); + } + + return(RESULT_OK); +} + + +SaveFileClass::ResultType SaveFileClass::Read(char const * path) +{ + Fields.clear(); + Content.clear(); + + if (path == NULL) return(RESULT_MISSING); + + std::FILE * const file = std::fopen(path, "rb"); + if (file == NULL) return(RESULT_MISSING); + + // The header is judged before anything the file's size could ask for is allocated. + unsigned char head[HEADER_SIZE]; + unsigned int const got = (unsigned int)std::fread(head, 1, HEADER_SIZE, file); + bool const ok = !std::ferror(file); + + HeaderType header; + ResultType result = ok ? Parse_Header(head, got, header) : RESULT_CORRUPT; + + std::vector image; + if (result == RESULT_OK) { + long const length = File_Length(file); + unsigned int const size = (unsigned int)length; + if (length < 0 || size != header.ContentOffset + header.StoredLength) { + result = RESULT_CORRUPT; + } else if (!Reserve(image, size)) { + result = RESULT_NO_MEMORY; + } else { + memcpy(image.data(), head, HEADER_SIZE); + if (!Read_Range(file, image.data() + HEADER_SIZE, size - HEADER_SIZE)) result = RESULT_CORRUPT; + } + } + std::fclose(file); + if (result != RESULT_OK) return(result); + + if (Header_CRC(image.data(), image.data() + HEADER_SIZE, header.TableLength) != header.HeaderCRC) { + return(RESULT_CORRUPT); + } + + result = Parse_Fields(image.data() + HEADER_SIZE, header.TableLength); + if (result != RESULT_OK) return(result); + + unsigned char const * const stored = image.data() + header.ContentOffset; + if (Checksum(stored, header.StoredLength) != header.ContentCRC) { + Fields.clear(); + return(RESULT_CORRUPT); + } + + if ((header.Flags & FLAG_LZO) != 0) { + if (!Reserve(Content, header.ContentLength)) { + Fields.clear(); + return(RESULT_NO_MEMORY); + } + + lzo_uint unpacked = (lzo_uint)Content.size(); + int const status = lzo1x_decompress_safe(stored, (lzo_uint)header.StoredLength, + Content.data(), &unpacked, NULL); + + if (status != LZO_E_OK || unpacked != header.ContentLength) { + Fields.clear(); + Content.clear(); + return(RESULT_CORRUPT); + } + } else { + if (header.StoredLength != header.ContentLength) { + Fields.clear(); + return(RESULT_CORRUPT); + } + if (!Reserve(Content, header.StoredLength)) { + Fields.clear(); + return(RESULT_NO_MEMORY); + } + memcpy(Content.data(), stored, header.StoredLength); + } + + return(RESULT_OK); +} + + +// Reads the header and the field table only, so listing a folder of saves touches a +// few hundred bytes of each file. +SaveFileClass::ResultType SaveFileClass::Read_Fields(char const * path) +{ + Fields.clear(); + Content.clear(); + + if (path == NULL) return(RESULT_MISSING); + + std::FILE * const file = std::fopen(path, "rb"); + if (file == NULL) return(RESULT_MISSING); + + unsigned char head[HEADER_SIZE]; + unsigned int const got = (unsigned int)std::fread(head, 1, HEADER_SIZE, file); + bool ok = !std::ferror(file); + + HeaderType header; + ResultType result = ok ? Parse_Header(head, got, header) : RESULT_CORRUPT; + + std::vector table; + if (result == RESULT_OK && header.TableLength > 0) { + long const length = File_Length(file); + unsigned int const size = (unsigned int)length; + if (length < 0 || header.TableLength > size - HEADER_SIZE) { + result = RESULT_CORRUPT; + } else if (!Reserve(table, header.TableLength)) { + result = RESULT_NO_MEMORY; + } else { + if (!Read_Range(file, table.data(), header.TableLength)) result = RESULT_CORRUPT; + } + } + std::fclose(file); + + if (result != RESULT_OK) return(result); + if (Header_CRC(head, table.data(), (unsigned int)table.size()) != header.HeaderCRC) return(RESULT_CORRUPT); + + return(Parse_Fields(table.data(), (unsigned int)table.size())); +} diff --git a/code/savefile.h b/code/savefile.h new file mode 100644 index 000000000..f6b3afa69 --- /dev/null +++ b/code/savefile.h @@ -0,0 +1,78 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#pragma once + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + +#include + +// The file a saved game is kept in: a fixed header, a table of listing fields, and one +// compressed block of game state. docs/SAVE-FORMAT.md records the layout. +class SaveFileClass +{ + public: + enum ResultType { + RESULT_OK, + RESULT_MISSING, // No file under that name. + RESULT_NOT_A_SAVE, // The file does not begin with the signature. + RESULT_UNSUPPORTED_VERSION, // A format version, or a header flag, this build does not read. + RESULT_CORRUPT, // A length, checksum or block that does not add up. + RESULT_WRITE_FAILED, // The file could not be written or moved into place. + RESULT_NO_MEMORY, // The file is within its limits but the process cannot hold it. + RESULT_TOO_LARGE, // The content or a listing field is more than a save can hold. + }; + + enum { + FORMAT_VERSION = 1, + HEADER_SIZE = 32, + }; + + SaveFileClass(void); + + void Set_String(int id, char const * text); + void Set_Int(int id, int value); + void Set_Time(int id, FILETIME const & time); + bool Get_String(int id, char * text, int size) const; + bool Get_Int(int id, int * value) const; + bool Get_Time(int id, FILETIME * time) const; + void Clear_Fields(void); + + ResultType Write(char const * path) const; + ResultType Read(char const * path); + ResultType Read_Fields(char const * path); + + static char const * Result_Text(ResultType result); + static unsigned int Checksum(unsigned char const * data, unsigned int length, unsigned int seed = 0); + + std::vector Content; + + private: + enum FieldKind { + FIELD_STRING = 1, + FIELD_INT = 2, + FIELD_TIME = 3, + }; + + struct FieldType { + int ID; + int Kind; + std::vector Bytes; + }; + + FieldType const * Find(int id, int kind) const; + void Set(int id, int kind, void const * data, unsigned int length); + void Serialize_Fields(std::vector & table) const; + ResultType Parse_Fields(unsigned char const * table, unsigned int length); + + std::vector Fields; +}; diff --git a/code/saveload.cpp b/code/saveload.cpp index 22633874a..ed692b3b3 100644 --- a/code/saveload.cpp +++ b/code/saveload.cpp @@ -42,7 +42,6 @@ * Save_Misc_Values -- saves miscellaneous variables * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "saveload.h" @@ -67,6 +66,7 @@ #include "builtype.h" #include "bullet.h" #include "bullettype.h" +#include "classfactory.h" #include "data.h" #include "dbgprint.h" #include "empulse.h" @@ -77,7 +77,6 @@ #include "globals.h" #include "goptions.h" #include "houstype.h" -#include "ilinkstm.h" #include "infantry.h" #include "infatype.h" #include "init.h" @@ -91,10 +90,12 @@ #include "ovrlight.h" #include "particle.h" #include "partsys.h" +#include "persist.h" #include "psystype.h" #include "ptype.h" #include "revent.h" #include "rules.h" +#include "savefile.h" #include "savemgr.h" #include "savestream.h" #include "savever.h" @@ -141,8 +142,29 @@ #include "objheaps.hh" +#include #include +#include +#include + +/// The save record stamps its times in the Windows epoch: hundred nanosecond ticks since the +/// start of 1601. The host clock counts from 1970, so the difference between the two is added. +static void Fetch_System_File_Time(FILETIME* result) +{ +#ifdef _WIN32 + GetSystemTimeAsFileTime(result); +#else + unsigned long long const epoch_difference = 116444736000000000ULL; + unsigned long long const now = (unsigned long long)std::chrono::duration_cast>>( + std::chrono::system_clock::now().time_since_epoch()).count(); + unsigned long long const ticks = now + epoch_difference; + result->dwLowDateTime = (DWORD)(ticks & 0xFFFFFFFFULL); + result->dwHighDateTime = (DWORD)(ticks >> 32); +#endif +} + + //#define SAVE_BLOCK_SIZE 512 #define SAVE_BLOCK_SIZE 4096 //#define SAVE_BLOCK_SIZE 1024 @@ -152,68 +174,144 @@ */ unsigned int ExpectedGameVersion = LoadOptionsClass::GAMEVER_OPENTS; -_COM_SMARTPTR_TYPEDEF(ILinkStream, __uuidof(ILinkStream)); + +/// +/// Writes one object to the save stream as a record of its own. +/// The record is the class identifier, the length of what follows, and whatever the +/// object's Save writes; a reader that does not consume exactly that length has read a +/// record of a different shape than was written. +/// +/// bool; Was the record written whole? +bool Save_Object(SaveStreamClass & stream, IPersistent * persist) +{ + if (persist == NULL) { + return(false); + } + + ClassID classid = persist->Class_ID(); + stream.Serialize_Bytes(&classid, sizeof(classid)); + unsigned int const lengthat = stream.Offset(); + unsigned int length = 0; + stream.Serialize(length); + unsigned int const start = stream.Offset(); + + bool result = persist->Save(stream, true); + if (!result) { + return(false); + } + + length = stream.Offset() - start; + stream.Overwrite_Bytes(lengthat, &length, sizeof(length)); + return(!stream.Was_Error()); +} + + +bool Save_Object(SaveStreamClass & stream, ILocomotion * locomotion) +{ + IPersistent * const persist = dynamic_cast(locomotion); + if (persist == NULL) { + return(false); + } + return(Save_Object(stream, persist)); +} + + +/// +/// Recreates one object from the save stream. +/// The object is created through the class registered for the identifier the record +/// carries, and reattaches itself to its own heap as it is constructed. +/// +/// The object, or NULL with the stream failed when the identifier names no +/// registered class, the object could not read its record, or the record's length does +/// not match what the object consumed. +IPersistent * Load_Object(SaveStreamClass & stream) +{ + ClassID classid; + unsigned int length = 0; + stream.Serialize_Bytes(&classid, sizeof(classid)); + stream.Serialize(length); + if (stream.Was_Error()) { + return(NULL); + } + + unsigned int const start = stream.Offset(); + if (length > stream.Size() - start) { + DebugString("Save record at %u claims %u bytes, past the end of the save\n", start, length); + stream.Fail(); + return(NULL); + } + + SwizzleManagerClass::MarkType const mark = Swizzler.Mark(); + std::unique_ptr persist(Create_Object(classid)); + if (persist == NULL) { + DebugString("Save record at %u names a class this build does not register\n", start); + stream.Fail(); + return(NULL); + } + + bool ok = persist->Load(stream); + if (ok && stream.Offset() != start + length) { + DebugString("Save record of %s at %u is %u bytes but %u were read\n", + typeid(*persist).name(), start, length, stream.Offset() - start); + ok = false; + } + if (!ok) { + Swizzler.Abandon(mark); + stream.Fail(); + return(NULL); + } + + persist->Post_Load(); + return(persist.release()); +} /// /// Loads a vector of persistent objects from the save game stream. -/// This routine reads the element count and then recreates each object through OLE. The -/// objects are not handed back -- each one reattaches itself to its own heap as it is +/// The objects are not handed back -- each one reattaches itself to its own heap as it is /// constructed, which is what refills the game's vectors. /// -/// Returns with S_OK, or the failure code of the read that went wrong. -__forceinline HRESULT Load_Vector(IStream * stream) +/// bool; Was the record read whole? +static bool Load_Vector(SaveStreamClass & stream) { - int count; - int index; - LPVOID obj; - - HRESULT result = stream->Read(&count, sizeof(count), NULL); - if (FAILED(result)) { - return(result); - } - for (index = 0; index < count; index++) { - result = OleLoadFromStream(stream, IID_IUnknown, &obj); - if (FAILED(result)) { - return(result); + int count = 0; + stream.Serialize(count); + if (stream.Was_Error()) { + return(false); + } + if (count < 0) { + return(false); + } + + for (int index = 0; index < count; index++) { + if (Load_Object(stream) == NULL) { + return(false); } } - return(S_OK); + return(true); } /// /// Saves a vector of persistent objects to the save game stream. -/// This routine writes the element count and then streams out each object in turn through -/// its IPersistStream interface. /// -/// Returns with S_OK, or the failure code of the first object that refused to -/// save. +/// bool; Was the record read whole? template -__forceinline HRESULT Save_Vector(IStream * stream, const DynamicVectorClass &list) +static bool Save_Vector(SaveStreamClass & stream, const DynamicVectorClass &list) { int count = list.Count(); - HRESULT result = stream->Write(&count, sizeof(count), NULL); - if (SUCCEEDED(result)) { - for (int index = 0; index < count; index++) { - LPPERSISTSTREAM lpPS = NULL; - result = list[index]->QueryInterface(IID_IPersistStream, (LPVOID *)&lpPS); - if (FAILED(result)) { - return(result); - } - result = OleSaveToStream(lpPS, stream); - if (FAILED(result)) { - return(result); - } - result = lpPS->Release(); - if (FAILED(result)) { - return(result); - } + stream.Serialize(count); + + for (int index = 0; index < count; index++) { + bool const result = Save_Object(stream, list[index]); + if (!result) { + return(false); } - result = S_OK; } - return(result); + return(!stream.Was_Error()); } + + /// /// Builds a checksum over the whole of the game object state. /// This routine walks the scenario and every object and type heap, folding each one's own @@ -299,7 +397,7 @@ void Print_Heap_CRCs(FILE * fp) * HISTORY: * * 07/08/1996 JLB : Created. * *=============================================================================================*/ -static bool Put_All(IStream *stream, int save_net) +static bool Put_All(SaveStreamClass & stream, int save_net) { /* ** Save the scenario global information. @@ -309,7 +407,7 @@ static bool Put_All(IStream *stream, int save_net) Rule->Save(stream); DebugString("Saving AnimTypes\n"); - if (FAILED(Save_Vector(stream, AnimTypes))) { + if (!Save_Vector(stream, AnimTypes)) { DebugString("\t***** FAILED!\n"); return(false); } @@ -318,13 +416,13 @@ static bool Put_All(IStream *stream, int save_net) ** Save the map. The map must be saved first, since it saves the Theater. */ DebugString("Saving Map\n"); - if (FAILED(Map.Save(stream))) { + if (!Map.Save(stream)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving Tunnels\n"); - if (FAILED(Save_Vector(stream, Tubes))) { + if (!Save_Vector(stream, Tubes)) { DebugString("\t***** FAILED!\n"); return(false); } @@ -333,7 +431,7 @@ static bool Put_All(IStream *stream, int save_net) ** Save miscellaneous variables. */ DebugString("Saving Misc. Values\n"); - if (FAILED(Save_Misc_Values(stream))) { + if (!Save_Misc_Values(stream)) { DebugString("\t***** FAILED!\n"); return(false); } @@ -342,13 +440,13 @@ static bool Put_All(IStream *stream, int save_net) ** Save the Logic & Map layers */ DebugString("Saving Logic\n"); - if (FAILED(Logic.Save(stream))) { + if (!Logic.Save(stream)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving TacticalMap\n"); - if (FAILED(OleSaveToStream(TacticalMap, stream))) { + if (!Save_Object(stream, TacticalMap)) { DebugString("\t***** FAILED!\n"); return(false); } @@ -358,248 +456,248 @@ static bool Put_All(IStream *stream, int save_net) ** TFixedIHeap class. */ DebugString("Saving HouseTypes\n"); - if (FAILED(Save_Vector(stream, HouseTypes))) { + if (!Save_Vector(stream, HouseTypes)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving Houses\n"); - if (FAILED(Save_Vector(stream, Houses))) { + if (!Save_Vector(stream, Houses)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving Units\n"); - if (FAILED(Save_Vector(stream, Units))) { + if (!Save_Vector(stream, Units)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving UnitTypes\n"); - if (FAILED(Save_Vector(stream, UnitTypes))) { + if (!Save_Vector(stream, UnitTypes)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving InfantryTypes\n"); - if (FAILED(Save_Vector(stream, InfantryTypes))) { + if (!Save_Vector(stream, InfantryTypes)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving Infantry\n"); - if (FAILED(Save_Vector(stream, Infantry))) { + if (!Save_Vector(stream, Infantry)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving BuildingTypes\n"); - if (FAILED(Save_Vector(stream, BuildingTypes))) { + if (!Save_Vector(stream, BuildingTypes)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving Buildings\n"); - if (FAILED(Save_Vector(stream, Buildings))) { + if (!Save_Vector(stream, Buildings)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving AircraftTypes\n"); - if (FAILED(Save_Vector(stream, AircraftTypes))) { + if (!Save_Vector(stream, AircraftTypes)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving Aircraft\n"); - if (FAILED(Save_Vector(stream, Aircraft))) { + if (!Save_Vector(stream, Aircraft)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving Anims\n"); - if (FAILED(Save_Vector(stream, Anims))) { + if (!Save_Vector(stream, Anims)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving TaskForces\n"); - if (FAILED(Save_Vector(stream, TaskForces))) { + if (!Save_Vector(stream, TaskForces)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving TeamTypes\n"); - if (FAILED(Save_Vector(stream, TeamTypes))) { + if (!Save_Vector(stream, TeamTypes)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving Teams\n"); - if (FAILED(Save_Vector(stream, Teams))) { + if (!Save_Vector(stream, Teams)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving ScriptTypes\n"); - if (FAILED(Save_Vector(stream, ScriptTypes))) { + if (!Save_Vector(stream, ScriptTypes)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving Scripts\n"); - if (FAILED(Save_Vector(stream, Scripts))) { + if (!Save_Vector(stream, Scripts)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving TagTypes\n"); - if (FAILED(Save_Vector(stream, TagTypes))) { + if (!Save_Vector(stream, TagTypes)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving Tags\n"); - if (FAILED(Save_Vector(stream, Tags))) { + if (!Save_Vector(stream, Tags)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving TriggerTypes\n"); - if (FAILED(Save_Vector(stream, TriggerTypes))) { + if (!Save_Vector(stream, TriggerTypes)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving Triggers\n"); - if (FAILED(Save_Vector(stream, Triggers))) { + if (!Save_Vector(stream, Triggers)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving AITriggerTypes\n"); - if (FAILED(Save_Vector(stream, AITriggerTypes))) { + if (!Save_Vector(stream, AITriggerTypes)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving Actions\n"); - if (FAILED(Save_Vector(stream, Actions))) { + if (!Save_Vector(stream, Actions)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving Events\n"); - if (FAILED(Save_Vector(stream, Events))) { + if (!Save_Vector(stream, Events)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving Factories\n"); - if (FAILED(Save_Vector(stream, Factories))) { + if (!Save_Vector(stream, Factories)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving VoxelAnimTypes\n"); - if (FAILED(Save_Vector(stream, VoxelAnimTypes))) { + if (!Save_Vector(stream, VoxelAnimTypes)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving VoxelAnims\n"); - if (FAILED(Save_Vector(stream, VoxelAnims))) { + if (!Save_Vector(stream, VoxelAnims)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving Warheads\n"); - if (FAILED(Save_Vector(stream, Warheads))) { + if (!Save_Vector(stream, Warheads)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving Weapons\n"); - if (FAILED(Save_Vector(stream, Weapons))) { + if (!Save_Vector(stream, Weapons)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving ParticleTypes\n"); - if (FAILED(Save_Vector(stream, ParticleTypes))) { + if (!Save_Vector(stream, ParticleTypes)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving Particles\n"); - if (FAILED(Save_Vector(stream, Particles))) { + if (!Save_Vector(stream, Particles)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving ParticleSystemTypes\n"); - if (FAILED(Save_Vector(stream, ParticleSystemTypes))) { + if (!Save_Vector(stream, ParticleSystemTypes)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving ParticleSystems\n"); - if (FAILED(Save_Vector(stream, ParticleSystems))) { + if (!Save_Vector(stream, ParticleSystems)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving BulletTypes\n"); - if (FAILED(Save_Vector(stream, BulletTypes))) { + if (!Save_Vector(stream, BulletTypes)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving Bullets\n"); - if (FAILED(Save_Vector(stream, Bullets))) { + if (!Save_Vector(stream, Bullets)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving WaypointPaths\n"); - if (FAILED(Save_Vector(stream, WaypointPaths))) { + if (!Save_Vector(stream, WaypointPaths)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving SmudgeTypes\n"); - if (FAILED(Save_Vector(stream, SmudgeTypes))) { + if (!Save_Vector(stream, SmudgeTypes)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving OverlayTypes\n"); - if (FAILED(Save_Vector(stream, OverlayTypes))) { + if (!Save_Vector(stream, OverlayTypes)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving LightSources\n"); - if (FAILED(Save_Vector(stream, LightSources))) { + if (!Save_Vector(stream, LightSources)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving BuildingLights\n"); - if (FAILED(Save_Vector(stream, BuildingLights))) { + if (!Save_Vector(stream, BuildingLights)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving Sides\n"); - if (FAILED(Save_Vector(stream, Sides))) { + if (!Save_Vector(stream, Sides)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving Tiberiums\n"); - if (FAILED(Save_Vector(stream, Tiberiums))) { + if (!Save_Vector(stream, Tiberiums)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving Empulses\n"); - if (FAILED(Save_Vector(stream, EMPulseClass::EMPulses))) { + if (!Save_Vector(stream, EMPulseClass::EMPulses)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving SuperWeaponTypes\n"); - if (FAILED(Save_Vector(stream, SuperWeaponTypes))) { + if (!Save_Vector(stream, SuperWeaponTypes)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving SuperWeapons\n"); - if (FAILED(Save_Vector(stream, SuperWeapons))) { + if (!Save_Vector(stream, SuperWeapons)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving TerrianTypes\n"); - if (FAILED(Save_Vector(stream, TerrainTypes))) { + if (!Save_Vector(stream, TerrainTypes)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving Terrains\n"); - if (FAILED(Save_Vector(stream, Terrains))) { + if (!Save_Vector(stream, Terrains)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving FoggedObjects\n"); - if (FAILED(Save_Vector(stream, FoggedObjectClass::FoggyObjects))) { + if (!Save_Vector(stream, FoggedObjectClass::FoggyObjects)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving AlphaShapes\n"); - if (FAILED(Save_Vector(stream, AlphaShapes))) { + if (!Save_Vector(stream, AlphaShapes)) { DebugString("\t***** FAILED!\n"); return(false); } DebugString("Saving Waves\n"); - if (FAILED(Save_Vector(stream, Waves))) { + if (!Save_Vector(stream, Waves)) { DebugString("\t***** FAILED!\n"); return(false); } @@ -626,7 +724,7 @@ static bool Put_All(IStream *stream, int save_net) } } - return(true); + return(!stream.Was_Error()); } @@ -638,7 +736,7 @@ static bool Put_All(IStream *stream, int save_net) /// order they were written out. /// /// bool; Was the game state restored? -static bool Get_All(IStream *stream, bool save_net) +static bool Get_All(SaveStreamClass & stream, bool save_net) { Clear_Scenario(); Scen->Load(stream); @@ -683,17 +781,17 @@ static bool Get_All(IStream *stream, bool save_net) return(false); } - if (FAILED(Load_Vector(stream))) { /// AnimTypes + if (!Load_Vector(stream)) { /// AnimTypes return(false); } Map.Load(stream); - if (FAILED(Load_Vector(stream))) { /// Tubes + if (!Load_Vector(stream)) { /// Tubes return(false); } - if (FAILED(Load_Misc_Values(stream))) { + if (!Load_Misc_Values(stream)) { return(false); } @@ -704,156 +802,164 @@ static bool Get_All(IStream *stream, bool save_net) delete TacticalMap; TacticalMap = NULL; } - Tactical * old_tactical; - if (FAILED(OleLoadFromStream(stream, IID_IUnknown, (LPVOID *)&old_tactical))) { + SwizzleManagerClass::MarkType const mark = Swizzler.Mark(); + IPersistent * const object = Load_Object(stream); + Tactical * const old_tactical = dynamic_cast(object); + if (object != NULL && old_tactical == NULL) { + DebugString("Save record of %s at %u is not the tactical map\n", typeid(*object).name(), stream.Offset()); + Swizzler.Abandon(mark); + delete object; + stream.Fail(); + } + if (old_tactical == NULL) { return(false); } - if (FAILED(Load_Vector(stream))) { /// HouseTypes + if (!Load_Vector(stream)) { /// HouseTypes return(false); } - if (FAILED(Load_Vector(stream))) { /// Houses + if (!Load_Vector(stream)) { /// Houses return(false); } - if (FAILED(Load_Vector(stream))) { /// Units + if (!Load_Vector(stream)) { /// Units return(false); } - if (FAILED(Load_Vector(stream))) { /// UnitTypes + if (!Load_Vector(stream)) { /// UnitTypes return(false); } - if (FAILED(Load_Vector(stream))) { /// InfantryTypes + if (!Load_Vector(stream)) { /// InfantryTypes return(false); } - if (FAILED(Load_Vector(stream))) { /// Infantry + if (!Load_Vector(stream)) { /// Infantry return(false); } - if (FAILED(Load_Vector(stream))) { /// BuildingTypes + if (!Load_Vector(stream)) { /// BuildingTypes return(false); } - if (FAILED(Load_Vector(stream))) { /// Buildings + if (!Load_Vector(stream)) { /// Buildings return(false); } - if (FAILED(Load_Vector(stream))) { /// AircraftTypes + if (!Load_Vector(stream)) { /// AircraftTypes return(false); } - if (FAILED(Load_Vector(stream))) { /// Aircraft + if (!Load_Vector(stream)) { /// Aircraft return(false); } - if (FAILED(Load_Vector(stream))) { /// Anims + if (!Load_Vector(stream)) { /// Anims return(false); } - if (FAILED(Load_Vector(stream))) { /// TaskForces + if (!Load_Vector(stream)) { /// TaskForces return(false); } - if (FAILED(Load_Vector(stream))) { /// TeamTypes + if (!Load_Vector(stream)) { /// TeamTypes return(false); } - if (FAILED(Load_Vector(stream))) { /// Teams + if (!Load_Vector(stream)) { /// Teams return(false); } - if (FAILED(Load_Vector(stream))) { /// ScriptTypes + if (!Load_Vector(stream)) { /// ScriptTypes return(false); } - if (FAILED(Load_Vector(stream))) { /// Scripts + if (!Load_Vector(stream)) { /// Scripts return(false); } - if (FAILED(Load_Vector(stream))) { /// TagTypes + if (!Load_Vector(stream)) { /// TagTypes return(false); } - if (FAILED(Load_Vector(stream))) { /// Tags + if (!Load_Vector(stream)) { /// Tags return(false); } - if (FAILED(Load_Vector(stream))) { /// TriggerTypes + if (!Load_Vector(stream)) { /// TriggerTypes return(false); } - if (FAILED(Load_Vector(stream))) { /// Triggers + if (!Load_Vector(stream)) { /// Triggers return(false); } - if (FAILED(Load_Vector(stream))) { /// AITriggerTypes + if (!Load_Vector(stream)) { /// AITriggerTypes return(false); } - if (FAILED(Load_Vector(stream))) { /// Actions + if (!Load_Vector(stream)) { /// Actions return(false); } - if (FAILED(Load_Vector(stream))) { /// Events + if (!Load_Vector(stream)) { /// Events return(false); } - if (FAILED(Load_Vector(stream))) { /// Factories + if (!Load_Vector(stream)) { /// Factories return(false); } - if (FAILED(Load_Vector(stream))) { /// VoxelAnimTypes + if (!Load_Vector(stream)) { /// VoxelAnimTypes return(false); } - if (FAILED(Load_Vector(stream))) { /// VoxelAnims + if (!Load_Vector(stream)) { /// VoxelAnims return(false); } - if (FAILED(Load_Vector(stream))) { /// Warheads + if (!Load_Vector(stream)) { /// Warheads return(false); } - if (FAILED(Load_Vector(stream))) { /// Weapons + if (!Load_Vector(stream)) { /// Weapons return(false); } - if (FAILED(Load_Vector(stream))) { /// ParticleTypes + if (!Load_Vector(stream)) { /// ParticleTypes return(false); } - if (FAILED(Load_Vector(stream))) { /// Particles + if (!Load_Vector(stream)) { /// Particles return(false); } - if (FAILED(Load_Vector(stream))) { /// ParticleSystemTypes + if (!Load_Vector(stream)) { /// ParticleSystemTypes return(false); } - if (FAILED(Load_Vector(stream))) { /// ParticleSystems + if (!Load_Vector(stream)) { /// ParticleSystems return(false); } - if (FAILED(Load_Vector(stream))) { /// BulletTypes + if (!Load_Vector(stream)) { /// BulletTypes return(false); } - if (FAILED(Load_Vector(stream))) { /// Bullets + if (!Load_Vector(stream)) { /// Bullets return(false); } - if (FAILED(Load_Vector(stream))) { /// WaypointPaths + if (!Load_Vector(stream)) { /// WaypointPaths return(false); } - if (FAILED(Load_Vector(stream))) { /// SmudgeTypes + if (!Load_Vector(stream)) { /// SmudgeTypes return(false); } - if (FAILED(Load_Vector(stream))) { /// OverlayTypes + if (!Load_Vector(stream)) { /// OverlayTypes return(false); } - if (FAILED(Load_Vector(stream))) { /// LightSources + if (!Load_Vector(stream)) { /// LightSources return(false); } - if (FAILED(Load_Vector(stream))) { /// BuildingLights + if (!Load_Vector(stream)) { /// BuildingLights return(false); } - if (FAILED(Load_Vector(stream))) { /// Sides + if (!Load_Vector(stream)) { /// Sides return(false); } - if (FAILED(Load_Vector(stream))) { /// Tiberiums + if (!Load_Vector(stream)) { /// Tiberiums return(false); } - if (FAILED(Load_Vector(stream))) { /// EMPulseClass::EMPulses + if (!Load_Vector(stream)) { /// EMPulseClass::EMPulses return(false); } - if (FAILED(Load_Vector(stream))) { /// SuperWeaponTypes + if (!Load_Vector(stream)) { /// SuperWeaponTypes return(false); } - if (FAILED(Load_Vector(stream))) { /// SuperWeapons + if (!Load_Vector(stream)) { /// SuperWeapons return(false); } - if (FAILED(Load_Vector(stream))) { /// TerrainTypes + if (!Load_Vector(stream)) { /// TerrainTypes return(false); } - if (FAILED(Load_Vector(stream))) { /// Terrains + if (!Load_Vector(stream)) { /// Terrains return(false); } - if (FAILED(Load_Vector(stream))) { /// FoggedObjectClass::FoggyObjects + if (!Load_Vector(stream)) { /// FoggedObjectClass::FoggyObjects return(false); } - if (FAILED(Load_Vector(stream))) { /// AlphaShapes + if (!Load_Vector(stream)) { /// AlphaShapes return(false); } - if (FAILED(Load_Vector(stream))) { /// Waves + if (!Load_Vector(stream)) { /// Waves return(false); } if (!VeinholeMonsterClass::Load_All(stream)) { @@ -873,7 +979,7 @@ static bool Get_All(IStream *stream, bool save_net) Map.Flag_To_Redraw(GS_REDRAW_ALL); - return(true); + return(!stream.Was_Error()); } /*************************************************************************** @@ -917,34 +1023,10 @@ static bool Get_All(IStream *stream, bool save_net) *=========================================================================*/ bool Save_Game(const char *file_name, char const * descr) { - WCHAR name[MAX_PATH]; - DebugString("\nSAVING GAME [%s - %s]\n", file_name, descr); Swizzler.Begin_Save(); - MultiByteToWideChar(0,0, Saved_Game_Name(file_name).c_str(), -1, name, sizeof(name)/sizeof(WCHAR)); - - /* - ** Open the file - */ - DebugString("Creating DocFile\n"); - IStoragePtr storage; - if (FAILED(StgCreateDocfile(name, STGM_CREATE|STGM_SHARE_EXCLUSIVE|STGM_READWRITE, 0, &storage))) { - DebugString("\t***** FAILED!\n"); - return(false); - } - - - /* - ** Save the description, scenario #, and house - ** (scenario # & house are saved separately from the actual Scenario & - ** PlayerPtr globals for convenience; we can quickly find out which - ** house & scenario this save-game file is for by reading these values. - ** Also, PlayerPtr is stored in a coded form in Save_Misc_Values(), - ** which may or may not be a HousesType number; so, saving 'house' - ** here ensures we can always pull out the house for this file.) - */ SaveVersionInfo info; info.Set_Internal_Version(ExpectedGameVersion); info.Set_Scenario_Description(descr); @@ -954,66 +1036,37 @@ bool Save_Game(const char *file_name, char const * descr) info.Set_Scenario_Number(Scen->Scenario); info.Set_Executable_Name("SUN.EXE"); info.Set_Game_Type(Session.Type); + FILETIME FileTime; - CoFileTimeNow(&FileTime); + Fetch_System_File_Time(&FileTime); info.Set_Last_Time(FileTime); info.Set_Start_Time(FileTime); info.Set_Play_Time(FileTime); - /* - ** Save the save-game version, for loading verification - */ - DebugString("Saving version information\n"); - if (FAILED(info.Save(storage))) { - DebugString("\t***** FAILED!\n"); - return(false); - } - - DebugString("Creating content stream\n"); - IStreamPtr content; - if (FAILED(storage->CreateStream(L"CONTENTS", STGM_CREATE|STGM_SHARE_EXCLUSIVE|STGM_READWRITE, 0, 0, &content))) { - DebugString("\t***** FAILED!\n"); - return(false); - } - - DebugString("Linking content stream to compressor\n"); - ILinkStreamPtr link; - link.CreateInstance(CLSID_CompressStream, NULL, CLSCTX_INPROC|CLSCTX_LOCAL_SERVER); - if (FAILED(link->Link_Stream(content))) { - DebugString("\t***** FAILED!\n"); - return(false); - } - IStreamPtr stream(link); + SaveFileClass file; + info.Save(file); - /* - ** Dump the save game data to the file. The data is compressed - ** and then encrypted. The message digest is calculated in the - ** process by using the data just as it is written to disk. - */ DebugString("Calling Put_All()\n"); - bool res = Put_All(stream,0); - - DebugString("Unlinking content stream from compressor\n"); - if (FAILED(link->Unlink_Stream(NULL))) { + SaveStreamClass stream(file.Content, SaveStreamClass::MODE_SAVE); + bool res = Put_All(stream, 0); + if (!res) { DebugString("\t***** FAILED!\n"); - return(false); } - DebugString("Releasing content stream\n"); - content.Release(); - - DebugString("Closing DocFile\n"); - if (FAILED(storage->Commit(0))) { - DebugString("\t***** FAILED!\n"); - return(false); + if (res) { + DebugString("Writing %s\n", file_name); + SaveFileClass::ResultType const result = file.Write(Saved_Game_Name(file_name).c_str()); + if (result != SaveFileClass::RESULT_OK) { + DebugString("\t***** FAILED! (%s)\n", SaveFileClass::Result_Text(result)); + res = false; + } } - DebugString("SAVING GAME [%s - %s] - Complete\n\n", file_name, descr); + DebugString("SAVING GAME [%s - %s] - %s\n\n", file_name, descr, res ? "Complete" : "Failed"); if (res) { SaveManager.Autosave.Schedule(Frame); } - return(res); } @@ -1060,62 +1113,42 @@ bool Save_Game(const char *file_name, char const * descr) *=========================================================================*/ bool Load_Game(const char *file_name) { - WCHAR name[MAX_PATH]; - DebugString("\nLOADING GAME [%s]\n", file_name); - /* - ** Read & discard the save-game's header info - */ SaveVersionInfo info; if (!Get_Savefile_Info(file_name, &info)) { return(false); } - - /* - * The load dialog screens the saves it lists, but a network save reaches this routine - * without passing through it, so the stamp is checked here as well. - */ if (info.Get_Internal_Version() != ExpectedGameVersion) { return(false); } - LoadedSaveVersion = info.Get_Internal_Version(); - - Session.Type = (GameType)info.Get_Game_Type(); - Swizzler.Discard(); - - /* - ** Open the file - */ - IStoragePtr storage; - - // Structured storage goes straight to Windows, so the saved game is named in full first. - MultiByteToWideChar(0,0,Saved_Game_Name(file_name).c_str(), -1, name, (sizeof(name)/sizeof(WCHAR))); - if (FAILED(StgOpenStorage(name, 0, STGM_SHARE_DENY_WRITE, 0, 0, &storage))) { + // The whole file is checked before the running game is torn down, so a damaged + // save costs nothing. + SaveFileClass file; + SaveFileClass::ResultType const result = file.Read(Saved_Game_Name(file_name).c_str()); + if (result != SaveFileClass::RESULT_OK) { + DebugString("\t***** FAILED! (%s)\n", SaveFileClass::Result_Text(result)); return(false); } - IStreamPtr content; - if (FAILED(storage->OpenStream(L"CONTENTS", 0, STGM_SHARE_EXCLUSIVE, 0, &content))) { - return(false); - } + LoadedSaveVersion = info.Get_Internal_Version(); + Session.Type = (GameType)info.Get_Game_Type(); - IUnknown *pUnknown = NULL; - ILinkStreamPtr link; - link.CreateInstance(CLSID_CompressStream, pUnknown,CLSCTX_INPROC|CLSCTX_LOCAL_SERVER); - if (FAILED(link->Link_Stream(content))) { - return(false); - } - IStreamPtr stream(link); + Swizzler.Discard(); + SaveStreamClass stream(file.Content, SaveStreamClass::MODE_LOAD); bool res = Get_All(stream, false); - - link->Unlink_Stream(NULL); - if (!res) { + DebugString("\t***** FAILED! (at %u of %u bytes)\n", stream.Offset(), stream.Size()); + // What was loaded stays in the heaps until the next teardown, which must not + // follow the identities still sitting in its pointer slots. + Swizzler.Abandon(); return(false); } + if (stream.Offset() != stream.Size()) { + DebugString("Save carries %u bytes past its last record\n", stream.Size() - stream.Offset()); + } Swizzler.Resolve(); @@ -1209,11 +1242,10 @@ static void Serialize_Misc_Values(SaveStreamClass & stream) } -int Save_Misc_Values(IStream * stream) +int Save_Misc_Values(SaveStreamClass & stream) { - SaveStreamClass savestream(stream, SaveStreamClass::MODE_SAVE); - Serialize_Misc_Values(savestream); - return(savestream.Result()); + Serialize_Misc_Values(stream); + return(!stream.Was_Error()); } @@ -1230,12 +1262,11 @@ int Save_Misc_Values(IStream * stream) * 06/24/1995 BRR : Created. * * 03/12/1996 JLB : Simplified. * *=============================================================================================*/ -int Load_Misc_Values(IStream * stream) +int Load_Misc_Values(SaveStreamClass & stream) { - SaveStreamClass savestream(stream, SaveStreamClass::MODE_LOAD); - savestream.Set_Context("Load_Misc_Values"); - Serialize_Misc_Values(savestream); - return(savestream.Result()); + stream.Set_Context("Load_Misc_Values"); + Serialize_Misc_Values(stream); + return(!stream.Was_Error()); } @@ -1259,23 +1290,20 @@ int Load_Misc_Values(IStream * stream) *=========================================================================*/ bool Get_Savefile_Info(char const * name, SaveVersionInfo * info) { - IStoragePtr storage; - WCHAR wname[MAX_PATH]; - - // Structured storage goes straight to Windows, so the saved game is named in full first. - MultiByteToWideChar(0, 0, Saved_Game_Name(name).c_str(), -1, wname, sizeof(wname) / sizeof(WCHAR)); - - HRESULT result = StgOpenStorage(wname, NULL, STGM_SHARE_EXCLUSIVE|STGM_READWRITE, NULL, 0, &storage); - if (FAILED(result)) { + if (name == NULL || info == NULL) { return(false); } - result = info->Load(storage); - if (FAILED(result)) { + SaveFileClass file; + SaveFileClass::ResultType const result = file.Read_Fields(Saved_Game_Name(name).c_str()); + if (result != SaveFileClass::RESULT_OK) { + if (result != SaveFileClass::RESULT_MISSING) { + DebugString("Saved game %s: %s\n", name, SaveFileClass::Result_Text(result)); + } return(false); } - return(true); + return(info->Load(file)); } diff --git a/code/saveload.h b/code/saveload.h index c92bc5df8..bab276bf6 100644 --- a/code/saveload.h +++ b/code/saveload.h @@ -13,16 +13,25 @@ #pragma once +#include "persist.h" + #include -struct IStream; +class SaveStreamClass; class SaveVersionInfo; +struct ILocomotion; /* ** SAVELOAD.CPP */ -int Load_Misc_Values(IStream * stream); -int Save_Misc_Values(IStream * stream); +int Load_Misc_Values(SaveStreamClass & stream); +int Save_Misc_Values(SaveStreamClass & stream); + +// An object travels as its class identifier, the length of its record, and the record. +// A locomotor loaded this way is handed back unowned; the caller takes it. +bool Save_Object(SaveStreamClass & stream, IPersistent * object); +bool Save_Object(SaveStreamClass & stream, ILocomotion * locomotion); +IPersistent * Load_Object(SaveStreamClass & stream); bool Get_Savefile_Info(char const * name, SaveVersionInfo * info); bool Save_Game(const char *file_name, char const * descr); bool Load_Game(const char *file_name); diff --git a/code/savemgr.cpp b/code/savemgr.cpp index ec62640cd..2047b10e1 100644 --- a/code/savemgr.cpp +++ b/code/savemgr.cpp @@ -7,7 +7,14 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ +#include "hostclock.h" #include "always.h" +#include "conquer.h" +#include "_keyboar.h" +#include "keyboard.h" +#include "msgloop.h" +#include "ui/uiinternal.h" +#include "ui/uimessagebox.h" #include "savemgr.h" @@ -25,7 +32,6 @@ #include "msgbox.h" #include "netdlg.h" #include "netglobal.h" -#include "ownrdraw.h" #include "rawfile.h" #include "rules.h" #include "saveload.h" @@ -150,16 +156,15 @@ void SaveManagerClass::Process_Pending_Save_Game(void) PendingSaveNotice = NoticeType::None; if (MultiplayerSavingAllowed) { - HWND dialog = 0; + bool box = false; if (!quiet) { - dialog = OwnerDraw::Custom_Message_Box(Fetch_String(TXT_SAVING_GAME), NULL, NULL); - } - if (dialog != 0) { - OwnerDraw::Display_Dialog(dialog); + box = UI_Wait_Box_Open(Fetch_String(TXT_SAVING_GAME), NULL, NULL); + Keyboard->Clear(); } bool saved = Save_Game(file_name.c_str(), description.c_str()); - if (dialog != 0) { - OwnerDraw::End_Dialog(dialog); + if (box) { + Keyboard->Clear(); + UI_Wait_Box_Close(); } Record_Save_Outcome(notice, saved); if (saved && SpawnCopyPending) { @@ -314,14 +319,13 @@ void SaveManagerClass::Quick_Save_Service(void) char description[512]; std::snprintf(description, sizeof(description), Fetch_String(TXT_QUICKSAVE_DESCRIPTION), Scen->Description); - HWND dialog = OwnerDraw::Custom_Message_Box(Fetch_String(TXT_SAVING_GAME), NULL, NULL); - if (dialog != 0) { - OwnerDraw::Display_Dialog(dialog); - } + bool const box = UI_Wait_Box_Open(Fetch_String(TXT_SAVING_GAME), NULL, NULL); + Keyboard->Clear(); Request_Save_Game(Quick_Save_File_Name(Single_Player_Kind()).c_str(), description, false, NoticeType::Requested); - if (dialog != 0) { - OwnerDraw::End_Dialog(dialog); + if (box) { + Keyboard->Clear(); + UI_Wait_Box_Close(); } } @@ -593,27 +597,31 @@ void SaveManagerClass::Process_Pending_Load_Game(void) Session.Suspended++; TacticalActive = false; - HWND dialog = OwnerDraw::Custom_Message_Box(Fetch_String(TXT_LOADING_SAVED_GAME), NULL, NULL); - if (dialog != 0) { - OwnerDraw::Display_Dialog(dialog); - } + bool const box = UI_Wait_Box_Open(Fetch_String(TXT_LOADING_SAVED_GAME), NULL, NULL); + Keyboard->Clear(); int shown = -1; while (!MultiplayerLoad.Is_Due(Monotonic_Milliseconds())) { int seconds = MultiplayerLoad.Seconds_Left(Monotonic_Milliseconds()); - if (dialog != 0 && seconds != shown) { + if (box && seconds != shown) { shown = seconds; char buffer[128]; std::snprintf(buffer, sizeof(buffer), Fetch_String(seconds == 1 ? TXT_LOADING_IN_SECOND : TXT_LOADING_IN_SECONDS), seconds); - OwnerDraw::Set_Custom_Message_Box_Text(dialog, buffer); + UI_Wait_Box_Set_Text(buffer); } - OwnerDraw::Dialog_Message_Handler(); - Sleep(10); + + // The countdown runs with the session suspended, which is the branch the dialog + // driver's own pump took here. + Windows_Message_Handler(); + Call_Back(); + UI_Paint_Now(false); + Host_Sleep(10); } - if (dialog != 0) { - OwnerDraw::End_Dialog(dialog); + if (box) { + Keyboard->Clear(); + UI_Wait_Box_Close(); } Session.Suspended--; TacticalActive = true; diff --git a/code/savestream.cpp b/code/savestream.cpp index fd15e72f6..a37a2e55b 100644 --- a/code/savestream.cpp +++ b/code/savestream.cpp @@ -7,26 +7,29 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "savestream.h" #include "saveload.h" +#include + unsigned int LoadedSaveVersion = 0; /// -/// Builds a save stream over the stream given. +/// Builds a save stream over the buffer given, appending to it when saving and reading +/// it from the start when loading. /// -/// The stream the members are to be read from or written to. +/// The bytes of the saved game, which must outlive this stream. /// Is this stream saving or loading? -SaveStreamClass::SaveStreamClass(IStream * stream, ModeType mode) : - Stream(stream), +SaveStreamClass::SaveStreamClass(std::vector & buffer, ModeType mode) : + Buffer(&buffer), + Cursor(mode == MODE_SAVE ? (unsigned int)buffer.size() : 0), Mode(mode), - ErrorCode(stream != NULL ? S_OK : E_POINTER), + Failed(false), FormatVersion(mode == MODE_LOAD ? LoadedSaveVersion : ExpectedGameVersion), OwnerType(NULL), OwnerID(0) @@ -34,51 +37,58 @@ SaveStreamClass::SaveStreamClass(IStream * stream, ModeType mode) : } -/// -/// Stops the pass, as though the stream itself had failed. -/// This is for a record that reads back as something no save could hold -- a length that -/// is negative, or one that does not fit the object waiting for it. An earlier failure is -/// left in place, since it is the one that explains the rest. -/// void SaveStreamClass::Fail(void) { - if (SUCCEEDED(ErrorCode)) { - ErrorCode = E_FAIL; + if (!Failed) { + Failed = true; } } /// -/// Moves a block of bytes between the object and the stream. -/// Every other Serialize reaches the stream through this one. Once something has gone -/// wrong the block is left alone and the failure is kept, so the rest of the pass runs -/// harmlessly and the caller finds out at the end. +/// Moves the bytes of one value between the caller and the stream. +/// A load that runs out of stream in the middle of a value fails the stream rather than +/// hand back a partly read value, and every later call is ignored. A negative length is +/// a count that wrapped, and fails the same way. /// -/// The bytes to write, or the place to read them back into. -/// The number of bytes to move. void SaveStreamClass::Serialize_Bytes(void * data, int length) { - if (FAILED(ErrorCode)) { + if (Failed) { + return; + } + if (length < 0) { + Failed = true; + return; + } + if (length == 0) { return; } - ULONG moved = 0; - HRESULT result; + unsigned char * const bytes = (unsigned char *)data; if (Mode == MODE_SAVE) { - result = Stream->Write(data, length, &moved); + Buffer->insert(Buffer->end(), bytes, bytes + length); + Cursor = (unsigned int)Buffer->size(); } else { - result = Stream->Read(data, length, &moved); + if ((unsigned int)length > Buffer->size() - Cursor) { + Failed = true; + return; + } + memcpy(bytes, Buffer->data() + Cursor, (std::size_t)length); + Cursor += (unsigned int)length; } +} - /* - * A stream that stops early has run out in the middle of an object, which leaves - * the rest of the members holding whatever they held before. Treat it as a failure - * rather than let a half-read object reach the game. - */ - if (SUCCEEDED(result) && moved != (ULONG)length) { - result = E_FAIL; - } - ErrorCode = result; +// A saver patches a length it could not know until the record was written. +void SaveStreamClass::Overwrite_Bytes(unsigned int offset, void const * data, int length) +{ + if (Failed || Mode != MODE_SAVE || length <= 0) { + return; + } + if (offset > Buffer->size() || (unsigned int)length > Buffer->size() - offset) { + Failed = true; + return; + } + memcpy(Buffer->data() + offset, data, (std::size_t)length); } diff --git a/code/savestream.h b/code/savestream.h index 0d7fc56c2..4ccf359b3 100644 --- a/code/savestream.h +++ b/code/savestream.h @@ -73,7 +73,7 @@ class SaveStreamClass MODE_LOAD }; - SaveStreamClass(IStream * stream, ModeType mode); + SaveStreamClass(std::vector & buffer, ModeType mode); bool Is_Saving(void) const {return(Mode == MODE_SAVE);} bool Is_Loading(void) const {return(Mode == MODE_LOAD);} @@ -83,8 +83,7 @@ class SaveStreamClass * does nothing, so a class lists its members without checking each one and the * caller asks once whether the whole pass worked. */ - HRESULT Result(void) const {return(ErrorCode);} - bool Was_Error(void) const {return(FAILED(ErrorCode));} + bool Was_Error(void) const {return(Failed);} /* * Stops the pass here. A container that reads back a length no honest save could @@ -99,11 +98,6 @@ class SaveStreamClass */ unsigned int Version(void) const {return(FormatVersion);} - /* - * The stream underneath, for the sub-objects that are still framed by OLE. - */ - IStream * Get_Stream(void) const {return(Stream);} - /* * Names the record this stream is carrying, so that a pointer which nothing * answers for can be reported against the object that asked for it. @@ -113,9 +107,35 @@ class SaveStreamClass OwnerType = ownertype; OwnerID = ownerid; } + char const * Context_Type(void) const {return(OwnerType);} + SwizzleIDType Context_ID(void) const {return(OwnerID);} + + /* + * Where the next byte goes or comes from, so a record can be framed by its length. + */ + unsigned int Offset(void) const {return(Cursor);} + unsigned int Size(void) const {return((unsigned int)Buffer->size());} + void Overwrite_Bytes(unsigned int offset, void const * data, int length); void Serialize_Bytes(void * data, int length); + /* + * Refuses a count that the bytes left in the stream could not hold, so a damaged + * count fails the load before anything is allocated for it. Nothing serializes + * an element in less than a byte. + */ + bool Fits(int count, std::size_t each) + { + if (Is_Loading()) { + std::size_t const room = (std::size_t)(Buffer->size() - Cursor) / (each > 0 ? each : 1); + if (count < 0 || (std::size_t)count > room) { + Fail(); + return(false); + } + } + return(true); + } + /* * Numbers and enumerations travel as their declared width. */ @@ -203,8 +223,7 @@ class SaveStreamClass Serialize(count); if (Is_Loading()) { - if (count < 0) { - Fail(); + if (!Fits(count, (std::is_arithmetic_v || std::is_enum_v) ? sizeof(T) : 1)) { return; } value.clear(); @@ -254,8 +273,7 @@ class SaveStreamClass Serialize(count); if (Is_Loading()) { - if (count < 0) { - Fail(); + if (!Fits(count, 1)) { return; } value.clear(); @@ -278,8 +296,7 @@ class SaveStreamClass Serialize(count); if (Is_Loading()) { - if (count < 0) { - Fail(); + if (!Fits(count, 1)) { return; } value.assign((std::size_t)count, false); @@ -301,8 +318,7 @@ class SaveStreamClass Serialize(count); if (Is_Loading()) { - if (count < 0) { - Fail(); + if (!Fits(count, 1)) { return; } value.resize(count); @@ -338,9 +354,10 @@ class SaveStreamClass } } - IStream * Stream; + std::vector * Buffer; + unsigned int Cursor; ModeType Mode; - HRESULT ErrorCode; + bool Failed; unsigned int FormatVersion; /* @@ -365,8 +382,7 @@ class SaveStreamClass /* - * The version stamp of the save game currently being read. Each object builds its own - * stream inside IPersistStream::Load, which has no way to be told, so the value is left - * here by the load as a whole. + * The version stamp of the save game currently being read, left here by the load as a + * whole so every stream built during it reports the same version. */ extern unsigned int LoadedSaveVersion; diff --git a/code/savever.cpp b/code/savever.cpp index 414bb7ce6..50d67725e 100644 --- a/code/savever.cpp +++ b/code/savever.cpp @@ -11,13 +11,11 @@ #include "savever.h" -#include "utf8.h" +#include "savefile.h" #include "dbgprint.h" #include "session.h" -#include - /// /// Creates an empty save file information block. @@ -320,796 +318,43 @@ int SaveVersionInfo::Get_Game_Type(void) /// -/// Saves the version information into a save file. -/// This routine is called while a save game is being written. It records every value into -/// the summary information property set and then again as one stream per value, so that a -/// reader which knows only the older layout can still identify the save. -/// -/// Returns with S_OK once every value has been written, otherwise the failure code -/// from the storage layer. -HRESULT SaveVersionInfo::Save(IStorage *storage) -{ - if (storage == NULL) { - return(E_POINTER); - } - - DebugString("Attempting to obtain PropertySetStorage interface\n"); - - IPropertySetStoragePtr storageset; - HRESULT res; - - res = storage->QueryInterface(IID_IPropertySetStorage, (void **)&storageset); - if (SUCCEEDED(res)) { - - DebugString("Saving version information the NEW way.\n"); - - res = Save_String_Set(storageset, PIDSI_SCEN_DESCRIP, ScenarioDescription); - if (FAILED(res)) { - return(res); - } - - res = Save_String_Set(storageset, PIDSI_PLAYER_HOUSE, PlayerHouse); - if (FAILED(res)) { - return(res); - } - - res = Save_Int_Set(storageset, PIDSI_G_VERSION, Version); - if (FAILED(res)) { - return(res); - } - - res = Save_Int_Set(storageset, PIDSI_INTERNAL_VER, InternalVersion); - if (FAILED(res)) { - return(res); - } - - res = Save_Time_Set(storageset, PIDSI_G_START_TIME, &StartTime); - if (FAILED(res)) { - return(res); - } - - res = Save_Time_Set(storageset, PIDSI_LAST_SAVE_TIME, &LastSaveTime); - if (FAILED(res)) { - return(res); - } - - res = Save_Time_Set(storageset, PIDSI_G_PLAY_TIME, &PlayTime); - if (FAILED(res)) { - return(res); - } - - res = Save_String_Set(storageset, PIDSI_EXEC_NAME, ExecutableName); - if (FAILED(res)) { - return(res); - } - - res = Save_String_Set(storageset, PIDSI_PLAYER_NAME1, PlayerName); - if (FAILED(res)) { - return(res); - } - - res = Save_String_Set(storageset, PIDSI_PLAYER_NAME2, PlayerName); - if (FAILED(res)) { - return(res); - } - - res = Save_Int_Set(storageset, PIDSI_SCENARIO_NUM, ScenarioNumber); - if (FAILED(res)) { - return(res); - } - - res = Save_Int_Set(storageset, PIDSI_CAMPAIGN_NUM, CampaignNumber); - if (FAILED(res)) { - return(res); - } - - res = Save_Int_Set(storageset, PIDSI_GAME_TYPE, GameType); - if (FAILED(res)) { - return(res); - } - - } else { - DebugString("\t***** FAILED!\n"); - } - - DebugString("Saving version information the old way.\n"); - - res = Save_String(storage, PIDSI_SCEN_DESCRIP, ScenarioDescription); - if (FAILED(res)) { - return(res); - } - - res = Save_String(storage, PIDSI_PLAYER_HOUSE, PlayerHouse); - if (FAILED(res)) { - return(res); - } - - res = Save_Int(storage, PIDSI_G_VERSION, Version); - if (FAILED(res)) { - return(res); - } - - res = Save_Int(storage, PIDSI_INTERNAL_VER, InternalVersion); - if (FAILED(res)) { - return(res); - } - - res = Save_Time(storage, PIDSI_G_START_TIME, &StartTime); - if (FAILED(res)) { - return(res); - } - - res = Save_Time(storage, PIDSI_LAST_SAVE_TIME, &LastSaveTime); - if (FAILED(res)) { - return(res); - } - - res = Save_Time(storage, PIDSI_G_PLAY_TIME, &PlayTime); - if (FAILED(res)) { - return(res); - } - - res = Save_String(storage, PIDSI_EXEC_NAME, ExecutableName); - if (FAILED(res)) { - return(res); - } - - res = Save_String(storage, PIDSI_PLAYER_NAME1, PlayerName); - if (FAILED(res)) { - return(res); - } - - res = Save_String(storage, PIDSI_PLAYER_NAME2, PlayerName); - if (FAILED(res)) { - return(res); - } - - res = Save_Int(storage, PIDSI_SCENARIO_NUM, ScenarioNumber); - if (FAILED(res)) { - return(res); - } - - res = Save_Int(storage, PIDSI_CAMPAIGN_NUM, CampaignNumber); - if (FAILED(res)) { - return(res); - } - - res = Save_Int(storage, PIDSI_GAME_TYPE, GameType); - if (FAILED(res)) { - return(res); - } - - return(S_OK); -} - - -/// -/// Loads the version information out of a save file. -/// This routine is called when a save game is scanned or restored. It prefers the property -/// set that the current game writes and falls back to the one stream per value layout that -/// older save files use, so that both generations of save file stay readable. -/// -/// Returns with S_OK once every value has been recovered, otherwise the failure -/// code from the storage layer. -HRESULT SaveVersionInfo::Load(IStorage *storage) -{ - if (storage == NULL) { - return(E_POINTER); - } - - IPropertySetStoragePtr storageset; - HRESULT res; - - if (SUCCEEDED(storage->QueryInterface(IID_IPropertySetStorage, (void **)&storageset)) - && SUCCEEDED(Load_String_Set(storageset, PIDSI_SCEN_DESCRIP, ScenarioDescription, sizeof(ScenarioDescription)))) { - - - res = Load_String_Set(storageset, PIDSI_PLAYER_HOUSE, PlayerHouse, sizeof(PlayerHouse)); - if (FAILED(res)) { - return(res); - } - - res = Load_Int_Set(storageset, PIDSI_G_VERSION, &Version); - if (FAILED(res)) { - return(res); - } - - res = Load_Int_Set(storageset, PIDSI_INTERNAL_VER, &InternalVersion); - if (FAILED(res)) { - return(res); - } - - res = Load_Time_Set(storageset, PIDSI_G_START_TIME, &StartTime); - if (FAILED(res)) { - return(res); - } - - res = Load_Time_Set(storageset, PIDSI_LAST_SAVE_TIME, &LastSaveTime); - if (FAILED(res)) { - return(res); - } - - res = Load_Time_Set(storageset, PIDSI_G_PLAY_TIME, &PlayTime); - if (FAILED(res)) { - return(res); - } - - res = Load_String_Set(storageset, PIDSI_EXEC_NAME, ExecutableName, sizeof(ExecutableName)); - if (FAILED(res)) { - return(res); - } - - res = Load_String_Set(storageset, PIDSI_PLAYER_NAME1, PlayerName, sizeof(PlayerName)); - if (FAILED(res)) { - return(res); - } - - res = Load_Int_Set(storageset, PIDSI_SCENARIO_NUM, &ScenarioNumber); - if (FAILED(res)) { - return(res); - } - - res = Load_Int_Set(storageset, PIDSI_CAMPAIGN_NUM, &CampaignNumber); - if (FAILED(res)) { - return(res); - } - - res = Load_Int_Set(storageset, PIDSI_GAME_TYPE, &GameType); - if (FAILED(res)) { - return(res); - } - - } else { - - res = Load_String(storage, PIDSI_SCEN_DESCRIP, ScenarioDescription, sizeof(ScenarioDescription)); - if (FAILED(res)) { - return(res); - } - - - res = Load_String(storage, PIDSI_PLAYER_HOUSE, PlayerHouse, sizeof(PlayerHouse)); - if (FAILED(res)) { - return(res); - } - - - res = Load_Int(storage, PIDSI_G_VERSION, &Version); - if (FAILED(res)) { - return(res); - } - - res = Load_Int(storage, PIDSI_INTERNAL_VER, &InternalVersion); - if (FAILED(res)) { - return(res); - } - - res = Load_Time(storage, PIDSI_G_START_TIME, &StartTime); - if (FAILED(res)) { - return(res); - } - - res = Load_Time(storage, PIDSI_LAST_SAVE_TIME, &LastSaveTime); - if (FAILED(res)) { - return(res); - } - - res = Load_Time(storage, PIDSI_G_PLAY_TIME, &PlayTime); - if (FAILED(res)) { - return(res); - } - - res = Load_String(storage, PIDSI_EXEC_NAME, ExecutableName, sizeof(ExecutableName)); - if (FAILED(res)) { - return(res); - } - - res = Load_String(storage, PIDSI_PLAYER_NAME1, PlayerName, sizeof(PlayerName)); - if (FAILED(res)) { - return(res); - } - - res = Load_Int(storage, PIDSI_SCENARIO_NUM, &ScenarioNumber); - if (FAILED(res)) { - return(res); - } - - res = Load_Int(storage, PIDSI_CAMPAIGN_NUM, &CampaignNumber); - if (FAILED(res)) { - return(res); - } - - res = Load_Int(storage, PIDSI_GAME_TYPE, &GameType); - if (FAILED(res)) { - return(res); - } - } - - return(S_OK); -} - - -/// -/// Reads a string from a stream of its own. -/// The wide text held in the stream is narrowed into the caller's buffer, which is emptied -/// first. This is the old style counterpart of Load_String_Set, used for save files written -/// before the version information moved into a property set. -/// -/// The property identifier naming the stream to open. -/// Returns with the result of the read. A failure means the stream is absent or -/// ended before the text was terminated. -/// The capacity of string; longer text is cut on a character boundary. -HRESULT SaveVersionInfo::Load_String(IStorage *storage, int id, char *string, int size) -{ - *string = '\0'; - - HRESULT res; - IStreamPtr stm; - - res = storage->OpenStream(Stream_Name_From_ID(id), NULL, STGM_SHARE_EXCLUSIVE, 0, &stm); - if (FAILED(res)) { - return(res); - } - - WCHAR buf[128]; - ULONG count; - - int i = 0; - for (; i < ARRAY_SIZE(buf); i++) { - res = stm->Read(&buf[i], sizeof(buf[i]), &count); - if (FAILED(res)) { - return(res); - } - if (res != S_OK || count != sizeof(buf[i])) { - return(E_FAIL); - } - if (buf[i] == '\0') { - break; - } - } - - if (i == ARRAY_SIZE(buf)) { - return(E_FAIL); - } - - char text[512]; - if (WideCharToMultiByte(CP_ACP, 0, buf, -1, text, sizeof(text), 0, 0) == 0) { - text[0] = '\0'; - } - UTF8::Copy(string, size, text); - - return(S_OK); -} - - -/// -/// Reads a string from the save file's property set. -/// The wide text held in the property is narrowed back into the caller's buffer. That buffer -/// is emptied before the read is attempted, so a missing property yields an empty string. -/// -/// The summary information property identifier to read. -/// Returns with the result of the read. A failure means the property set could not -/// be opened. -/// The capacity of string; longer text is cut on a character boundary. -HRESULT SaveVersionInfo::Load_String_Set(IPropertySetStorage *storageset, int id, char *string, int size) -{ - *string = '\0'; - - HRESULT res; - IPropertyStoragePtr storage; - - res = storageset->Open(FMTID_SummaryInformation, STGM_SHARE_EXCLUSIVE|STGM_READWRITE, &storage); - if (FAILED(res)) { - return(res); - } - - PROPSPEC propsec; - propsec.ulKind = PRSPEC_PROPID; - propsec.propid = id; - PROPVARIANT propvar; - - res = storage->ReadMultiple(1, &propsec, &propvar); - if (FAILED(res)) { - return(res); - } - - if (propvar.vt == VT_LPWSTR) { - char text[1024]; - if (WideCharToMultiByte(CP_ACP, 0, propvar.pwszVal, -1, text, sizeof(text), 0, 0) == 0) { - text[0] = '\0'; - } - UTF8::Copy(string, size, text); - } - - return(res); -} - - -/// -/// Reads an integer from a stream of its own. -/// This is the old style counterpart of Load_Int_Set, used for save files written before -/// the version information moved into a property set. The value is cleared first. -/// -/// The property identifier naming the stream to open. -/// Returns with the result of the read. A failure means the stream is absent. -HRESULT SaveVersionInfo::Load_Int(IStorage *storage, int id, int *integer) -{ - *integer = 0; - - HRESULT res; - IStreamPtr stm; - - res = storage->OpenStream(Stream_Name_From_ID(id), NULL, STGM_SHARE_EXCLUSIVE, 0, &stm); - if (FAILED(res)) { - return(res); - } - - res = stm->Read(integer, sizeof(*integer), NULL); - if (FAILED(res)) { - return(res); - } - - return(res); -} - - -/// -/// Reads an integer from the save file's property set. -/// The value is cleared before the read is attempted, so a save file that does not carry -/// the property leaves the caller with zero. -/// -/// The summary information property identifier to read. -/// Returns with the result of the read. A failure means the property set could not -/// be opened. -HRESULT SaveVersionInfo::Load_Int_Set(IPropertySetStorage *storageset, int id, int *integer) -{ - *integer = 0; - - HRESULT res; - IPropertyStoragePtr storage; - - res = storageset->Open(FMTID_SummaryInformation, STGM_SHARE_EXCLUSIVE|STGM_READWRITE, &storage); - if (FAILED(res)) { - return(res); - } - - PROPSPEC propsec; - propsec.ulKind = PRSPEC_PROPID; - propsec.propid = id; - PROPVARIANT propvar; - - res = storage->ReadMultiple(1, &propsec, &propvar); - if (FAILED(res)) { - return(res); - } - - if (propvar.vt == VT_I4) { - *integer = propvar.lVal; - } - - return(res); -} - - -/// -/// Writes a string to a stream of its own. -/// The text is widened before it is written. This is the old style counterpart of -/// Save_String_Set, kept for readers that do not understand property sets. -/// -/// The property identifier naming the stream to create. -/// Returns with the result of the write. A failure means the stream was never -/// committed. -HRESULT SaveVersionInfo::Save_String(IStorage *storage, int id, char *string) -{ - WCHAR buf[260]; - - if (MultiByteToWideChar(CP_ACP, 0, string, -1, buf, ARRAY_SIZE(buf)) == 0) { - buf[0] = L'\0'; - } - - IStreamPtr stm(NULL); - - HRESULT res = storage->CreateStream(Stream_Name_From_ID(id), STGM_SHARE_EXCLUSIVE|STGM_READWRITE, 0, 0, &stm); - if (FAILED(res)) { - return(res); - } - - res = stm->Write(buf, sizeof(WCHAR) * wcslen(buf) + 2, NULL); - if (FAILED(res)) { - return(res); - } - res = stm->Commit(0); - if (FAILED(res)) { - return(res); - } - - return(res); -} - - -/// -/// Writes a string into the save file's property set. -/// The text is widened before it is stored, since the summary information properties are -/// held as wide characters. The property set is created if the save file has none yet. -/// -/// The summary information property identifier to write. -/// Returns with the result of the write. A failure means the property set could -/// neither be opened nor created. -HRESULT SaveVersionInfo::Save_String_Set(IPropertySetStorage *storageset, int id, const char *string) -{ - WCHAR buf[260]; - - if (MultiByteToWideChar(CP_ACP, 0, string, -1, buf, ARRAY_SIZE(buf)) == 0) { - buf[0] = L'\0'; - } - - HRESULT res; - IPropertyStoragePtr storage; - - res = storageset->Open(FMTID_SummaryInformation, STGM_SHARE_EXCLUSIVE|STGM_READWRITE, &storage); - if (FAILED(res)) { - res = storageset->Create(FMTID_SummaryInformation, NULL, PROPSETFLAG_DEFAULT, STGM_SHARE_EXCLUSIVE|STGM_READWRITE|STGM_CREATE, &storage); - if (FAILED(res)) { - return(res); - } - } - - PROPSPEC propsec; - propsec.ulKind = PRSPEC_PROPID; - propsec.propid = id; - PROPVARIANT propvar; - - propvar.vt = VT_LPWSTR; - propvar.pwszVal = buf; - - res = storage->WriteMultiple(1, &propsec, &propvar, PID_FIRST_USABLE); - if (FAILED(res)) { - return(res); - } - - return(res); -} - - -/// -/// Writes an integer to a stream of its own. -/// This is the old style counterpart of Save_Int_Set, kept so that a reader which does not -/// understand property sets can still recover the value. -/// -/// The property identifier naming the stream to create. -/// Returns with the result of the write. A failure means the stream was never -/// committed. -HRESULT SaveVersionInfo::Save_Int(IStorage *storage, int id, int integer) -{ - IStreamPtr stm(NULL); - - HRESULT res = storage->CreateStream(Stream_Name_From_ID(id), STGM_SHARE_EXCLUSIVE|STGM_READWRITE, 0, 0, &stm); - if (FAILED(res)) { - return(res); - } - - res = stm->Write(&integer, sizeof(integer), NULL); - if (FAILED(res)) { - return(res); - } - res = stm->Commit(STGM_READ); - if (FAILED(res)) { - return(res); - } - - return(res); -} - - -/// -/// Writes an integer into the save file's property set. -/// This routine stores the value as a summary information property, creating the property -/// set first if the save file does not carry one yet. +/// Writes every listing field into the file's field table. /// -/// The summary information property identifier to write. -/// Returns with the result of the write. A failure means the property set could -/// neither be opened nor created. -HRESULT SaveVersionInfo::Save_Int_Set(IPropertySetStorage *storageset, int id, int integer) +void SaveVersionInfo::Save(SaveFileClass & file) const { - HRESULT res; - IPropertyStoragePtr storage; - - res = storageset->Open(FMTID_SummaryInformation, STGM_SHARE_EXCLUSIVE|STGM_READWRITE, &storage); - if (FAILED(res)) { - res = storageset->Create(FMTID_SummaryInformation, NULL, PROPSETFLAG_DEFAULT, STGM_SHARE_EXCLUSIVE|STGM_READWRITE|STGM_CREATE, &storage); - if (FAILED(res)) { - return(res); - } - } - - PROPSPEC propsec; - propsec.ulKind = PRSPEC_PROPID; - propsec.propid = id; - PROPVARIANT propvar; - - propvar.vt = VT_I4; - propvar.lVal = integer; - - res = storage->WriteMultiple(1, &propsec, &propvar, PID_FIRST_USABLE); - if (FAILED(res)) { - return(res); - } - - return(res); + file.Set_String(PIDSI_SCEN_DESCRIP, ScenarioDescription); + file.Set_String(PIDSI_PLAYER_HOUSE, PlayerHouse); + file.Set_Int(PIDSI_G_VERSION, Version); + file.Set_Int(PIDSI_INTERNAL_VER, InternalVersion); + file.Set_Time(PIDSI_G_START_TIME, StartTime); + file.Set_Time(PIDSI_LAST_SAVE_TIME, LastSaveTime); + file.Set_Time(PIDSI_G_PLAY_TIME, PlayTime); + file.Set_String(PIDSI_EXEC_NAME, ExecutableName); + file.Set_String(PIDSI_PLAYER_NAME1, PlayerName); + file.Set_String(PIDSI_PLAYER_NAME2, PlayerName); + file.Set_Int(PIDSI_SCENARIO_NUM, ScenarioNumber); + file.Set_Int(PIDSI_CAMPAIGN_NUM, CampaignNumber); + file.Set_Int(PIDSI_GAME_TYPE, GameType); } /// -/// Reads a time stamp from a stream of its own. -/// This is the old style counterpart of Load_Time_Set, used for save files written before -/// the version information moved into a property set. The time is cleared first. +/// Reads the listing fields the file carries; a field the file lacks keeps its default. /// -/// The property identifier naming the stream to open. -/// Returns with the result of the read. A failure means the stream is absent. -HRESULT SaveVersionInfo::Load_Time(IStorage *storage, int id, FILETIME *time) +/// bool; Does the file record an internal version at all? +bool SaveVersionInfo::Load(SaveFileClass const & file) { - time->dwLowDateTime = 0; - time->dwHighDateTime = 0; - - HRESULT res; - IStreamPtr stm; - - res = storage->OpenStream(Stream_Name_From_ID(id), NULL, STGM_SHARE_EXCLUSIVE, 0, &stm); - if (FAILED(res)) { - return(res); - } - - res = stm->Read(time, sizeof(*time), NULL); - if (FAILED(res)) { - return(res); - } - - return(res); -} - + file.Get_String(PIDSI_SCEN_DESCRIP, ScenarioDescription, sizeof(ScenarioDescription)); + file.Get_String(PIDSI_PLAYER_HOUSE, PlayerHouse, sizeof(PlayerHouse)); + file.Get_Int(PIDSI_G_VERSION, &Version); + file.Get_Time(PIDSI_G_START_TIME, &StartTime); + file.Get_Time(PIDSI_LAST_SAVE_TIME, &LastSaveTime); + file.Get_Time(PIDSI_G_PLAY_TIME, &PlayTime); + file.Get_String(PIDSI_EXEC_NAME, ExecutableName, sizeof(ExecutableName)); + file.Get_String(PIDSI_PLAYER_NAME1, PlayerName, sizeof(PlayerName)); + file.Get_Int(PIDSI_SCENARIO_NUM, &ScenarioNumber); + file.Get_Int(PIDSI_CAMPAIGN_NUM, &CampaignNumber); + file.Get_Int(PIDSI_GAME_TYPE, &GameType); -/// -/// Reads a time stamp from the save file's property set. -/// The time is cleared before the read is attempted, so a save file that does not carry the -/// property leaves the caller with a zero time rather than with garbage. -/// -/// The summary information property identifier to read. -/// Returns with the result of the read. A failure means the property set could not -/// be opened. -HRESULT SaveVersionInfo::Load_Time_Set(IPropertySetStorage *storageset, int id, FILETIME *time) -{ - time->dwLowDateTime = 0; - time->dwHighDateTime = 0; - - HRESULT res; - IPropertyStoragePtr storage; - - res = storageset->Open(FMTID_SummaryInformation, STGM_SHARE_EXCLUSIVE|STGM_READWRITE, &storage); - if (FAILED(res)) { - return(res); - } - - PROPSPEC propsec; - propsec.ulKind = PRSPEC_PROPID; - propsec.propid = id; - PROPVARIANT propvar; - - res = storage->ReadMultiple(1, &propsec, &propvar); - if (FAILED(res)) { - return(res); - } - - if (propvar.vt == VT_FILETIME) { - *time = propvar.filetime; - } - - return(res); -} - - -/// -/// Writes a time stamp to a stream of its own. -/// This is the old style counterpart of Save_Time_Set. The save routine records every value -/// this way as well, so that a reader which does not understand property sets can still -/// recover it. -/// -/// The property identifier naming the stream to create. -/// Returns with the result of the write. A failure means the stream was never -/// committed. -HRESULT SaveVersionInfo::Save_Time(IStorage *storage, int id, FILETIME *time) -{ - IStreamPtr stm(NULL); - - HRESULT res = storage->CreateStream(Stream_Name_From_ID(id), STGM_SHARE_EXCLUSIVE|STGM_READWRITE, 0, 0, &stm); - if (FAILED(res)) { - return(res); - } - - res = stm->Write(time, sizeof(*time), NULL); - if (FAILED(res)) { - return(res); - } - res = stm->Commit(STGM_READ); - if (FAILED(res)) { - return(res); - } - - return(res); -} - - -/// -/// Writes a time stamp into the save file's property set. -/// This routine stores the time as a summary information property, creating the property -/// set first if the save file does not carry one yet. -/// -/// The summary information property identifier to write. -/// Returns with the result of the write. A failure means the property set could -/// neither be opened nor created. -HRESULT SaveVersionInfo::Save_Time_Set(IPropertySetStorage *storageset, int id, FILETIME *time) -{ - HRESULT res; - IPropertyStoragePtr storage; - - res = storageset->Open(FMTID_SummaryInformation, STGM_SHARE_EXCLUSIVE|STGM_READWRITE, &storage); - if (FAILED(res)) { - res = storageset->Create(FMTID_SummaryInformation, NULL, PROPSETFLAG_DEFAULT, STGM_SHARE_EXCLUSIVE|STGM_READWRITE|STGM_CREATE, &storage); - if (FAILED(res)) { - return(res); - } - } - - PROPSPEC propsec; - propsec.ulKind = PRSPEC_PROPID; - propsec.propid = id; - PROPVARIANT propvar; - - propvar.vt = VT_FILETIME; - propvar.filetime = *time; - - res = storage->WriteMultiple(1, &propsec, &propvar, PID_FIRST_USABLE); - if (FAILED(res)) { - return(res); - } - - return(res); -} - - -/// -/// Fetches the stream name that a save version property is stored under. -/// This routine maps the summary information property identifiers onto the wide names used -/// by the old style save format, where every value lives in a stream of its own. The stream -/// based save and load helpers call it to name the stream they are about to open. -/// -/// The summary information property identifier to look up. -/// Returns with the stream name for the property, or NULL if the identifier is not -/// one of the recorded version properties. -const WCHAR *Stream_Name_From_ID(int id) -{ - static struct pidsiStruct { - int ID; - WCHAR const *Name; - } _ids[] = { - {PIDSI_SCEN_DESCRIP, L"Scenario Description"}, - {PIDSI_PLAYER_HOUSE, L"Player House"}, - {PIDSI_G_VERSION, L"Version"}, - {PIDSI_INTERNAL_VER, L"Internal Version"}, - {PIDSI_G_START_TIME, L"Start Time"}, - {PIDSI_LAST_SAVE_TIME, L"Last Save Time"}, - {PIDSI_G_PLAY_TIME, L"Play Time"}, - {PIDSI_EXEC_NAME, L"Executable Name"}, - {PIDSI_PLAYER_NAME1, L"Player Name"}, - {PIDSI_PLAYER_NAME2, L"Player Name2"}, - {PIDSI_SCENARIO_NUM, L"Scenario Number"}, - {PIDSI_CAMPAIGN_NUM, L"Campaign"}, - {PIDSI_GAME_TYPE, L"GameType"}, - }; - - for (int i = 0; i < ARRAY_SIZE(_ids); i++) { - if (_ids[i].ID == id) { - return(_ids[i].Name); - } - } - - return(NULL); + return(file.Get_Int(PIDSI_INTERNAL_VER, &InternalVersion)); } diff --git a/code/savever.h b/code/savever.h index 291dd7bde..e6b923015 100644 --- a/code/savever.h +++ b/code/savever.h @@ -11,8 +11,7 @@ #include "win.h" -struct IStorage; -struct IPropertySetStorage; +class SaveFileClass; enum { PIDSI_SCEN_DESCRIP = 2, @@ -75,27 +74,8 @@ class SaveVersionInfo void Set_Game_Type(int id); int Get_Game_Type(void); - HRESULT Save(IStorage *storage); - HRESULT Load(IStorage *storage); - - private: - HRESULT Load_String(IStorage *storage, int id, char *string, int size); - HRESULT Load_String_Set(IPropertySetStorage *storageset, int id, char *string, int size); - - HRESULT Load_Int(IStorage *storage, int id, int *integer); - HRESULT Load_Int_Set(IPropertySetStorage *storageset, int id, int *integer); - - HRESULT Save_String(IStorage *storage, int id, char *string); - HRESULT Save_String_Set(IPropertySetStorage *storageset, int id, const char *string); - - HRESULT Save_Int(IStorage *storage, int id, int integer); - HRESULT Save_Int_Set(IPropertySetStorage *storageset, int id, int integer); - - HRESULT Load_Time(IStorage *storage, int id, FILETIME *time); - HRESULT Load_Time_Set(IPropertySetStorage *storageset, int id, FILETIME *time); - - HRESULT Save_Time(IStorage *storage, int id, FILETIME *time); - HRESULT Save_Time_Set(IPropertySetStorage *storageset, int id, FILETIME *time); + void Save(SaveFileClass & file) const; + bool Load(SaveFileClass const & file); private: /* @@ -160,4 +140,3 @@ class SaveVersionInfo int GameType; }; -const WCHAR *Stream_Name_From_ID(int id); diff --git a/code/scenario.cpp b/code/scenario.cpp index 2e651211a..79a60bdc0 100644 --- a/code/scenario.cpp +++ b/code/scenario.cpp @@ -127,7 +127,6 @@ #include "newmenu.h" #include "overlay.h" #include "overtype.h" -#include "ownrdraw.h" #include "partsys.h" #include "pcx.h" #include "preview.h" @@ -397,10 +396,6 @@ bool Start_Scenario(char const * name, bool briefing, CampaignType campaign) if (briefing && Session.Type == GAME_NORMAL && !has_briefing_movie) { - // No dialog has been put up in a game a client launched, so the artwork it draws with - // is not built yet. - OwnerDraw::Prepare_Resources(MainWindow); - if (Scen->TransitTheme != THEME_NONE) { Theme.Play_Song(Scen->TransitTheme); transit_playing = true; @@ -715,6 +710,7 @@ bool Read_Scenario(char const * fname) } Progress.Set_Graphic_Data((players > 1) ? "PROGBARM.SHP" : "PROGBAR.SHP", background, prog_msg, prog_bar_pos); + Progress.Announce_Milestones(); Progress.Display_Progress(); if (PacketTransport != NULL && Ipx.Transport_Mode() == IPXManagerClass::TRANSPORT_DIRECT && Session.Players.Count() > 1) { @@ -732,7 +728,7 @@ bool Read_Scenario(char const * fname) if (Scen->IsRandom) { if (RandomMapGen.SeedData.Load(name)) { - RandomMapGen.Generate_Random_Map(false, NULL); + RandomMapGen.Generate_Random_Map(false); Multiplayer_Last_Minute_Fixups(); } else { state = ScenarioState::NotRead; @@ -1075,11 +1071,7 @@ void Clear_Scenario(void) LightSourceClass::Recalc = false; while (Objects.Count()) { - if (Objects[0]->RTTI == RTTI_BULLET) { - Objects[0]->Release(); - } else { - delete Objects[0]; - } + delete Objects[0]; } LightSourceClass::Recalc = true; @@ -3334,18 +3326,17 @@ static Cell const Clip_Move(Cell const & cell, FacingType facing, int dist) /// The elapsed mission clock is halted across the write so that the time recorded is the /// one the player will be given back when the game is resumed. /// -void ScenarioClass::Save(IStream * stream) const +void ScenarioClass::Save(SaveStreamClass & stream) const { DebugString("Scenario Save: ElapsedTimer = %d\n", (int)ElapsedTimer); ElapsedTimer.Stop(); - SaveStreamClass savestream(stream, SaveStreamClass::MODE_SAVE); /* * One member list serves both directions, so it cannot be declared const even though * writing changes nothing. */ - const_cast(this)->Serialize(savestream); + const_cast(this)->Serialize(stream); ElapsedTimer.Start(); } @@ -3356,13 +3347,12 @@ void ScenarioClass::Save(IStream * stream) const /// The elapsed mission clock is halted across the read for the same reason it is halted /// across the write, so that it does not advance over the value coming back in. /// -void ScenarioClass::Load(IStream * stream) +void ScenarioClass::Load(SaveStreamClass & stream) { ElapsedTimer.Stop(); - SaveStreamClass savestream(stream, SaveStreamClass::MODE_LOAD); - savestream.Set_Context("ScenarioClass"); - Serialize(savestream); + stream.Set_Context("ScenarioClass"); + Serialize(stream); ElapsedTimer.Start(); DebugString("Scenario Load: ElapsedTimer = %d\n", (int)ElapsedTimer); diff --git a/code/scenario.h b/code/scenario.h index f1bb213d6..eea2cb16b 100644 --- a/code/scenario.h +++ b/code/scenario.h @@ -101,8 +101,8 @@ class ScenarioClass { bool Read_INI(CCINIClass const & ini); bool Write_INI(CCINIClass & ini, bool mplayer=false) const; - void Save(IStream * stream) const; - void Load(IStream * stream); + void Save(SaveStreamClass & stream) const; + void Load(SaveStreamClass & stream); void Serialize(SaveStreamClass & stream); diff --git a/code/scenfile.cpp b/code/scenfile.cpp index b5bef6d6e..73c6ad9e1 100644 --- a/code/scenfile.cpp +++ b/code/scenfile.cpp @@ -7,6 +7,8 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ +#include "always.h" + #include "scenfile.h" #include diff --git a/code/score.cpp b/code/score.cpp index a1ddb96f4..f505e0b41 100644 --- a/code/score.cpp +++ b/code/score.cpp @@ -42,6 +42,7 @@ * ScoreClass::Pulse_Bar_Graph -- Pulses the bargraph color. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ +#include "hostclock.h" #include "always.h" #include "score.h" @@ -72,7 +73,6 @@ #include "surface.h" #include "theme.h" #include "utf8.h" -#include "windlg.h" #include "winstub.h" #include @@ -125,10 +125,6 @@ void ScoreClass::Presentation(void) CCFileClass file; struct Fame hallfame[NUMFAMENAMES]; - while (WS_Destroy_Dialog(NULL, NULL)) { - ; - } - XPos = (HiddenSurface->Get_Width() - 640) / 2; YPos = (HiddenSurface->Get_Height() - 400) / 2; @@ -1058,7 +1054,7 @@ void ScoreClass::Call_Back_Delay(int time) cd.Start(); } - Sleep(0); + Host_Sleep(0); } while (cd > 0); @@ -1119,7 +1115,7 @@ void ScoreClass::Timing(void) } while (!GameInFocus) { - Sleep(500); + Host_Sleep(500); Windows_Message_Handler(); } diff --git a/code/script.cpp b/code/script.cpp index 977dad004..c1be22e65 100644 --- a/code/script.cpp +++ b/code/script.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "script.h" @@ -151,18 +150,9 @@ bool ScriptClass::Has_Missions_Remaining(void) } -/// -/// Fetches the class identifier used to persist this script. -/// This routine is part of the IPersistStream contract that the save game system relies -/// on to recreate objects when a game is loaded. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE ScriptClass::GetClassID(CLSID * retval) +ClassID ScriptClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_ScriptClass; - return(S_OK); + return(ClassID_ScriptClass); } @@ -368,18 +358,9 @@ ScriptTypeClass * ScriptTypeClass::Find_Or_Make(char const * name) } -/// -/// Fetches the class identifier used to persist this script type. -/// This routine is part of the IPersistStream contract that the save game system relies -/// on to recreate objects when a game is loaded. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE ScriptTypeClass::GetClassID(CLSID * retval) +ClassID ScriptTypeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_ScriptTypeClass; - return(S_OK); + return(ClassID_ScriptTypeClass); } diff --git a/code/script.h b/code/script.h index f13faa0d1..cdabf2457 100644 --- a/code/script.h +++ b/code/script.h @@ -29,7 +29,7 @@ class ScriptClass : public AbstractClass ScriptClass(ScriptTypeClass *type = NULL); virtual ~ScriptClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; @@ -76,7 +76,7 @@ class ScriptTypeClass : public AbstractTypeClass static ScriptTypeClass * Find_Or_Make(char const * ininame = NULL); - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; static void Read_All(CCINIClass const & ini, INIScopeType scope); static void Write_All(CCINIClass & ini, INIScopeType scope); diff --git a/code/sendfile.cpp b/code/sendfile.cpp index 118af61f4..4cc06348e 100644 --- a/code/sendfile.cpp +++ b/code/sendfile.cpp @@ -48,8 +48,11 @@ #include "progress.h" #include "session.h" #include "stimer.h" +#include "utf8.h" #include +#include +#include bool Receive_Remote_File ( char *file_name, unsigned int file_length, bool show_progress); bool Send_Remote_File ( char const *file_name ); @@ -120,7 +123,14 @@ bool Get_File_From_Host(char *return_name, bool show_progress) //DebugString ("RA95 - Got packet from host\n"); if (net_receive_packet.Command == NET_FILE_INFO && sender_address == Session.HostAddress) { - strcpy (return_name, net_receive_packet.ScenarioInfo.ShortFileName); + // The field is documented as not necessarily terminated, so the name is taken + // from it by length rather than by a terminator the host may not have sent. + char const * const short_name = net_receive_packet.ScenarioInfo.ShortFileName; + std::size_t const short_length = static_cast( + std::find(short_name, short_name + sizeof(net_receive_packet.ScenarioInfo.ShortFileName), '\0') + - short_name); + std::memcpy(return_name, short_name, short_length); + return_name[short_length] = '\0'; file_length = net_receive_packet.ScenarioInfo.FileLength; DebugString("Host responded with file info\n"); DebugString("File name is %s\n", return_name); @@ -247,14 +257,23 @@ bool Receive_Remote_File ( char *file_name, unsigned int file_length, bool show_ if (receive_packet->Command == NET_FILE_CHUNK && sender_address == Session.HostAddress){ - char *flag = &block_received[receive_packet->BlockNumber]; - if (!block_received[receive_packet->BlockNumber]) { + // The block index and length name where in the reassembly buffer this chunk + // lands, and the host is not trusted to keep either inside it. + std::size_t const block_offset = static_cast(MAX_SEND_FILE_PACKET_SIZE) * receive_packet->BlockNumber; + bool const block_fits = receive_packet->BlockNumber < total_blocks + && receive_packet->BlockLength <= sizeof(receive_packet->RawData) + && block_offset <= file_length + && receive_packet->BlockLength <= file_length - block_offset; + if (!block_fits) { + DebugString("Discarding file chunk %u of length %u\n", (unsigned)receive_packet->BlockNumber, (unsigned)receive_packet->BlockLength); + } else if (!block_received[receive_packet->BlockNumber]) { + char *flag = &block_received[receive_packet->BlockNumber]; *flag = true; received_count++; progress += 100; response_timer = RESPONSE_TIMEOUT/2; DebugString("Received file chunk %d\n", receive_packet->BlockNumber); - memcpy (file_buffer + (MAX_SEND_FILE_PACKET_SIZE) * receive_packet->BlockNumber, + memcpy (file_buffer + block_offset, receive_packet->RawData, receive_packet->BlockLength); if (show_progress){ @@ -358,7 +377,7 @@ bool Send_Remote_File ( char const *file_name, bool send_to_all, bool show_progr ** Send the file info to the remote machine(s) */ net_file_info.Command = NET_FILE_INFO; - strcpy (net_file_info.ScenarioInfo.ShortFileName, file_name); + UTF8::Copy(net_file_info.ScenarioInfo.ShortFileName, sizeof(net_file_info.ScenarioInfo.ShortFileName), file_name); // DebugString( "Uploading '%s'\n", file_name ); // DebugString( "ShortFileName is '%s'\n", net_file_info.ScenarioInfo.ShortFileName ); net_file_info.ScenarioInfo.FileLength = file_length; diff --git a/code/session.cpp b/code/session.cpp index c15304290..93f2dc122 100644 --- a/code/session.cpp +++ b/code/session.cpp @@ -58,6 +58,7 @@ #include "dbgprint.h" #include "gamedirs.h" // for Search_Files. #include "globals.h" +#include "hostclock.h" #include "ipxmgr.h" #include "language/language.h" #include "msgloop.h" @@ -76,6 +77,7 @@ #include #include // for station ID computation #include // for station ID computation +#include // for ntohl /***************************** Globals *************************************/ @@ -1001,7 +1003,7 @@ unsigned int SessionClass::Compute_Unique_ID(void) //------------------------------------------------------------------------ // time(&tm); // id = (unsigned long)tm; - id = timeGetTime(); + id = Host_Milliseconds(); //------------------------------------------------------------------------ // Now add in the free space on the hard drive @@ -1224,7 +1226,7 @@ void SessionClass::Update_Progress(int percent) Call_Back(); while (Ipx.Global_Num_Send() > 5 && timer > 0) { - Sleep(20); + Host_Sleep(20); Windows_Message_Handler(); Call_Back(); } @@ -1331,15 +1333,11 @@ void SessionClass::Init_Fixed_Alliances(void) /// Saves the game options to a save game. /// /// bool; Were the options written successfully? -bool GameOptionsType::Save(IStream * stream) +bool GameOptionsType::Save(SaveStreamClass & stream) { - if (stream == NULL) { - return(false); - } - SaveStreamClass savestream(stream, SaveStreamClass::MODE_SAVE); - Serialize(savestream); - return(SUCCEEDED(savestream.Result())); + Serialize(stream); + return(!stream.Was_Error()); } @@ -1349,17 +1347,13 @@ bool GameOptionsType::Save(IStream * stream) /// scenario with it. /// /// bool; Were the options read back successfully? -bool GameOptionsType::Load(IStream * stream) +bool GameOptionsType::Load(SaveStreamClass & stream) { - if (stream == NULL) { - return(false); - } - SaveStreamClass savestream(stream, SaveStreamClass::MODE_LOAD); - savestream.Set_Context("GameOptionsType"); - Serialize(savestream); + stream.Set_Context("GameOptionsType"); + Serialize(stream); ScenarioIndex = -1; - return(SUCCEEDED(savestream.Result())); + return(!stream.Was_Error()); } diff --git a/code/session.h b/code/session.h index 98b724ab2..86391286b 100644 --- a/code/session.h +++ b/code/session.h @@ -234,6 +234,13 @@ struct NodeNameType { unsigned int LastTime; // last time we heard from this guy unsigned char LastChance; // we're about to remove him from the list int Color; // chat player's color + + /* + * This is the sender's own UniqueID out of the announcement that created this + * node. It identifies the machine whatever address its packets arrive from, and + * it fits in the union's existing slack, so the node's size does not move. + */ + unsigned int ID; } Chat; }; @@ -385,6 +392,12 @@ struct GlobalPacketType { }; #pragma pack() +// These three travel on the network, so their sizes are fixed by the packet format. +static_assert(sizeof(NodeNameType) == 132, "Lobby node layout changed"); +static_assert(sizeof(RemoteFileTransferType) == 487, "Scenario transfer packet layout changed"); +static_assert(sizeof(GlobalPacketType) == 1059, "Global packet layout changed"); +static_assert(offsetof(GlobalPacketType, Name) == 4, "Global packet layout changed"); + //........................................................................... // For finding sync bugs; filled in by the engine when certain conditions // are met; the pointers allow examination of objects in the debugger. @@ -462,8 +475,8 @@ struct GameOptionsType { bool ScrapMetal; // A wreck leaves the animations its type names in ScrapExplosion. char ScenarioDescription [DESCRIP_MAX]; //Used on client machines only - bool Save(IStream * stream); - bool Load(IStream * stream); + bool Save(SaveStreamClass & stream); + bool Load(SaveStreamClass & stream); void Serialize(SaveStreamClass & stream); }; diff --git a/code/sha.cpp b/code/sha.cpp index df04f76e7..85bdbfc4a 100644 --- a/code/sha.cpp +++ b/code/sha.cpp @@ -78,7 +78,7 @@ void SHAEngine::Process_Partial(void const * & data, int & length) ** Attach as many bytes as possible from the source data into ** the staging buffer. */ - int add_count = std::min((int)length, SRC_BLOCK_SIZE - PartialCount); + int add_count = std::min((int)length, SRC_BLOCK_SIZE - PartialCount); memcpy(&Partial[PartialCount], data, add_count); data = ((char const *&)data) + add_count; PartialCount += add_count; diff --git a/code/sha.h b/code/sha.h index 2a2887cef..416fca674 100644 --- a/code/sha.h +++ b/code/sha.h @@ -32,10 +32,11 @@ #pragma once +#include #include #include #include -#include +#include /* @@ -68,10 +69,17 @@ class SHAEngine private: typedef union { - unsigned long Long[5]; + std::uint32_t Long[5]; unsigned char Char[20]; } SHADigest; + /* + ** The digest is a 160 bit value laid out as five 32 bit words, and every + ** routine below indexes it as such. A wider word would silently change the + ** size of the digest, the size of the accumulator and the stride of both. + */ + static_assert(sizeof(SHADigest) == 20, "SHA digest must be 160 bits"); + /* ** This holds the calculated final result. It is cached ** here to avoid the overhead of recalculating it over diff --git a/code/shapeset.h b/code/shapeset.h index e74d3b442..2e58df3b0 100644 --- a/code/shapeset.h +++ b/code/shapeset.h @@ -38,6 +38,8 @@ #include "rect.h" #include "rgb.h" +#include + /* ** This is the header that appears at the beginning of the ShapeSet file. The header @@ -168,6 +170,13 @@ class ShapeSet void Set_Size(short size) {Size = size;} }; + // A shape file is cast straight onto this class, so the frame records that follow the + // header keep their file widths and offsets. + static_assert(sizeof(ShapeRecord) == 24, "Shape frame record layout changed"); + static_assert(offsetof(ShapeRecord, Width) == 4, "Shape frame record layout changed"); + static_assert(offsetof(ShapeRecord, Color) == 12, "Shape frame record layout changed"); + static_assert(offsetof(ShapeRecord, Data) == 20, "Shape frame record layout changed"); + bool Is_Shape_Index_Valid(int index) const {return(unsigned(index) < unsigned(Count));} ShapeRecord const * Fetch_Record_Pointer(int shape) const @@ -187,6 +196,9 @@ class ShapeSet }; #pragma pack(pop) +// A shape file is cast straight onto this header, so its four fields keep their file widths. +static_assert(sizeof(ShapeSet) == 8, "Shape file header layout changed"); + /*********************************************************************************************** * ShapeSet::Get_Data -- Fetches pointer to raw shape data. * diff --git a/code/side.cpp b/code/side.cpp index 1e081a06b..679ba158a 100644 --- a/code/side.cpp +++ b/code/side.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "side.h" @@ -132,18 +131,9 @@ bool SideClass::Read_INI(CCINIClass const & ini) } -/// -/// Fetches the class identifier of this object. -/// This routine is part of the IPersist interface. It is used by the save and load -/// system to recognize what kind of object it is about to create. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE SideClass::GetClassID(CLSID * retval) +ClassID SideClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_SideClass; - return(S_OK); + return(ClassID_SideClass); } diff --git a/code/side.h b/code/side.h index be97d6b84..f93d10c45 100644 --- a/code/side.h +++ b/code/side.h @@ -30,7 +30,7 @@ class SideClass : public AbstractTypeClass SideClass(char const * ininame = NULL); virtual ~SideClass() override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; /* ** Query functions. diff --git a/code/sidebar.cpp b/code/sidebar.cpp index f204c7c5b..297e83edf 100644 --- a/code/sidebar.cpp +++ b/code/sidebar.cpp @@ -837,10 +837,6 @@ bool SidebarClass::Add(RTTIType type, int id) *=============================================================================================*/ bool SidebarClass::Scroll(bool up, int column) { - if (_dialog_count != 0) { - return(false); - } - if (column == -1) { bool scr = false; if (Column[0].Scroll(up)) { diff --git a/code/skirmish.cpp b/code/skirmish.cpp index eb6651352..17e4ea4ee 100644 --- a/code/skirmish.cpp +++ b/code/skirmish.cpp @@ -24,190 +24,11 @@ #include "msgbox.h" #include "netshare.h" #include "newmenu.h" -#include "ownrdraw.h" #include "rules.h" +#include "ui/uiskirmish.h" #include "win.h" -INT_PTR CALLBACK Skirmish_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); -BOOL Skirmish_On_WM_INITDIALOG(HWND window, WPARAM wparam, LPARAM lparam); - - -/// -/// Handles a control notification from the skirmish dialog. -/// This routine services the buttons and check boxes of the setup dialog. When the player -/// accepts the dialog, the slider and combo box settings are harvested into the session -/// options and the local player is added to the player list; when the player cancels, only -/// the handle, side, and color are remembered. -/// -/// The identifier of the control that sent the notification. -/// The notification code that came with the command. -void Skirmish_On_WM_COMMAND(HWND window, int message, WPARAM wparam, LPARAM lparam) -{ - int * rc = (int *)GetWindowLongPtr(window, DWLP_USER); - char buffer[256]; - HWND handle; - - switch (message) { - case IDOK: { - if (lparam == 0) { - EnableWindow(GetDlgItem(window, 1), FALSE); - - int waypoint_count = RandomMapWaypointCount(Session.Options.ScenarioIndex); - int waypoint = 1; - - handle = GetDlgItem(window, IDC_SKIRMISH_AIPLAYERS); - if (handle) waypoint = Slider_GetPos(handle) + 1; - - if (waypoint_count < waypoint) { - sprintf(buffer, Fetch_String(TXT_SCENARIO_TOO_SMALL), waypoint_count); - WWMessageBox().Process(buffer, TXT_OK); - EnableWindow(GetDlgItem(window, 1), TRUE); - return; - } - - GetWindowText(GetDlgItem(window, IDC_SKIRMISH_NAME), Session.Handle, sizeof(Session.Handle)); - - handle = GetDlgItem(window, IDC_SKIRMISH_UNITCOUNT); - if (handle) Session.Options.UnitCount = Slider_GetPos(handle); - - handle = GetDlgItem(window, IDC_SKIRMISH_TECHLEVEL); - if (handle) BuildLevel = Slider_GetPos(handle); - - handle = GetDlgItem(window, IDC_SKIRMISH_CREDITS); - if (handle) Session.Options.Credits = Slider_GetPos(handle); - - handle = GetDlgItem(window, IDC_DIFFICULTY_SLIDER); - if (handle) Session.Options.AIDifficulty = (DiffType)Slider_GetPos(handle); - - handle = GetDlgItem(window, IDC_SKIRMISH_AIPLAYERS); - if (handle) Session.Options.AIPlayers = Slider_GetPos(handle); - - handle = GetDlgItem(window, IDC_GAME_SPEED_SLIDER); - if (handle) { - Session.Options.GameSpeed = 6 - Slider_GetPos(handle); - Options.GameSpeed = Session.Options.GameSpeed; - } - - handle = GetDlgItem(window, IDC_SKIRMISH_SIDE); - if (handle) Session.House = Country_From_Box(handle); - - handle = GetDlgItem(window, IDC_SKIRMISH_COLOR); - if (handle) { - Session.ColorIdx = ComboBox_GetCurSel(handle); - Session.PrefColor = Session.ColorIdx; - } - - NodeNameType * who = new NodeNameType; - if (who) { - strcpy(who->Name, Session.Handle); - who->Player.House = Session.House; - who->Player.Color = Session.ColorIdx; - who->Player.ProcessTime = -1; - Session.Players.Add(who); - } - - handle = GetDlgItem(window, IDC_SKIRMISH_BASES); - if (handle) Session.Options.Bases = Button_GetCheck(handle) == BST_CHECKED; - handle = GetDlgItem(window, IDC_SKIRMISH_CRATES); - if (handle) Session.Options.Goodies = Button_GetCheck(handle) == BST_CHECKED; - handle = GetDlgItem(window, IDC_SKIRMISH_FOG); - if (handle) Session.Options.FogOfWar = Button_GetCheck(handle) == BST_CHECKED; - handle = GetDlgItem(window, IDC_SKIRMISH_BRIDGES); - if (handle) Session.Options.BridgeDestruction = Button_GetCheck(handle) == BST_CHECKED; - handle = GetDlgItem(window, IDC_REDEPLOY_MCV); - if (handle) Session.Options.MCVRedeploy = Button_GetCheck(handle) == BST_CHECKED; - handle = GetDlgItem(window, IDC_SHORT_GAME); - if (handle) Session.Options.ShortGame = Button_GetCheck(handle) == BST_CHECKED; - Session.Options.HarvTruce = false; - handle = GetDlgItem(window, IDC_MULTI_ENGINEER); - if (handle) Session.Options.CrapEngineers = Button_GetCheck(handle) == BST_CHECKED; - - if (MultiplayerMapPreview) { - delete MultiplayerMapPreview; - MultiplayerMapPreview = NULL; - } - *rc = IDOK; - } - } - break; - - case IDCANCEL: - if (!lparam) { - GetWindowText(GetDlgItem(window, IDC_SKIRMISH_NAME), Session.Handle, sizeof(Session.Handle)); - handle = GetDlgItem(window, IDC_SKIRMISH_SIDE); - if (handle) Session.House = Country_From_Box(handle); - handle = GetDlgItem(window, IDC_SKIRMISH_COLOR); - if (handle) { - Session.ColorIdx = ComboBox_GetCurSel(handle); - Session.PrefColor = Session.ColorIdx; - } - *rc = IDCANCEL; - } - break; - - case IDC_SHORT_GAME: - handle = GetDlgItem(window, IDC_SHORT_GAME); - if (handle && Button_GetCheck(handle) == BST_CHECKED) { - SendDlgItemMessage(window, IDC_SKIRMISH_BASES, BM_SETCHECK, TRUE, 0); - } - break; - - case IDC_MULTIMAP: { - int old_scen = Session.Options.ScenarioIndex; - strcpy(buffer, Session.ScenarioFileName); - strcpy(buffer, Session.Options.ScenarioDescription); - ShowWindow(window, SW_HIDE); - if (Scenario_Dialog(MainWindow) == IDCANCEL) { - Session.Options.ScenarioIndex = old_scen; - Set_Scenario_Info_From_Index(old_scen); - Update_Network_Dialog_Preview(window); - ShowWindow(window, SW_SHOW); - if (stricmp(Session.Scenarios[Session.Options.ScenarioIndex]->Get_Filename(), RANDOM_MAP_FILE_NAME) == 0) { - delete MultiplayerMapPreview; - MultiplayerMapPreview = new MapPreviewClass; - MultiplayerMapPreview->Read_PCX_Preview("RandMap.img"); - if (MultiplayerMapPreview->Get_Preview_Surface() == NULL) { - Update_Network_Dialog_Preview(window); - } - InvalidateRect(window, NULL, FALSE); - } else { - Update_Network_Dialog_Preview(window); - } - InvalidateRect(window, NULL, FALSE); - } else { - ShowWindow(window, SW_SHOW); - if (Set_Scenario_Info_From_Index(Session.Options.ScenarioIndex) == true) { - SendDlgItemMessage(window, IDC_SCENARIONAME, WM_SETTEXT, 0, (LPARAM)Session.Options.ScenarioDescription); - if (stricmp(Session.Scenarios[Session.Options.ScenarioIndex]->Get_Filename(), "RandMap.Sed") == 0) { - if (MultiplayerMapPreview != NULL) { - delete MultiplayerMapPreview; - MultiplayerMapPreview = new MapPreviewClass; - MultiplayerMapPreview->Read_PCX_Preview("RandMap.img"); - } - if (MultiplayerMapPreview->Get_Preview_Surface() == NULL) { - Update_Network_Dialog_Preview(window); - } - InvalidateRect(window, NULL, FALSE); - } else { - Update_Network_Dialog_Preview(window); - } - } else { - Session.Options.ScenarioIndex = old_scen; - } - } - } - break; - - case IDC_SKIRMISH_BASES: - handle = GetDlgItem(window, IDC_SKIRMISH_BASES); - if (handle && Button_GetCheck(handle) != 1) { - SendDlgItemMessage(window, IDC_SHORT_GAME, BM_SETCHECK, 0, 0); - } - break; - } -} - /// /// Handles the skirmish game setup dialog. @@ -227,25 +48,18 @@ bool Skirmish_Mode_Dialog(void) Draw_Menu_Background(); Show_Mouse(); - HWND dialog = OwnerDraw::Begin_Dialog(IDD_SKIRMISH, Skirmish_Dialog_Proc); - if (dialog) { - SetWindowLongPtr(dialog, DWLP_USER, (LONG_PTR)&rc); - OwnerDraw::Display_Dialog(dialog); - while (rc != IDOK && rc != IDCANCEL) { - if (OwnerDraw::Dialog_Message_Handler() == IDOK) { - break; - } - Title_Screen_Restore(); - } - OwnerDraw::End_Dialog(dialog); + UISkirmishPresenterClass screen; + screen.Refresh(); + + if (UI_Skirmish_Screen(screen).Outcome != UIResult::OUTCOME_FAILED_TO_OPEN) { + rc = screen.Accepted() ? IDOK : IDCANCEL; } - if (MultiplayerMapPreview != NULL) { - delete MultiplayerMapPreview; - MultiplayerMapPreview = NULL; + if (rc == -1) { + rc = IDCANCEL; } - Session.Write_MultiPlayer_Settings(); + screen.End(); if (rc == IDCANCEL) { Hide_Mouse(); @@ -259,180 +73,3 @@ bool Skirmish_Mode_Dialog(void) return(false); } - - -/// -/// Handles the messages sent to the skirmish setup dialog. -/// The owner draw dialog handler is given first refusal on every message. Anything it -/// leaves alone is dealt with here -- dialog setup, button and slider notifications, and -/// repainting the map preview. -/// -/// Returns with TRUE if the message was handled, otherwise FALSE so that Windows -/// performs its default processing. -INT_PTR CALLBACK Skirmish_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - INT_PTR rc = OwnerDraw::Default_Dialog_Proc(window, message, wparam, lparam); - - if (rc == 0) { - - switch (message) { - case WM_COMMAND: - Skirmish_On_WM_COMMAND(window, LOWORD(wparam), lparam, HIWORD(wparam)); - return(TRUE); - - case WM_INITDIALOG: - return(Skirmish_On_WM_INITDIALOG(window, wparam, lparam)); - - case WM_PAINT: - if (MultiplayerMapPreview) { - MultiplayerMapPreview->Blit_Preview(window); - } - ValidateRect(window, NULL); - break; - - case WM_HSCROLL: { - int code = LOWORD(wparam); - if (code != SB_THUMBPOSITION && code != SB_THUMBTRACK) { - Slider_GetPos((HWND)lparam); - } - switch (GetDlgCtrlID((HWND)lparam)) { - case IDC_SKIRMISH_UNITCOUNT: - GetDlgItem(window, IDC_SKIRMISH_UNITCOUNT_LABEL); - break; - case IDC_SKIRMISH_TECHLEVEL: - GetDlgItem(window, IDC_SKIRMISH_TECHLEVEL_LABEL); - break; - case IDC_DIFFICULTY_SLIDER: - GetDlgItem(window, IDC_SKIRMISH_AILEVEL_LABEL); - break; - case IDC_SKIRMISH_AIPLAYERS: - GetDlgItem(window, IDC_SKIRMISH_AIPLAYERS_LABEL); - break; - } - } - break; - } - return(FALSE); - } - return(rc); -} - - -/// -/// Prepares the skirmish dialog for display. -/// This routine fills the sliders, side and color combo boxes, and option check boxes -/// with the player's current multiplayer settings, selects the starting scenario, and -/// puts up its map preview. -/// -/// Always FALSE, so that Windows leaves the keyboard focus where the dialog -/// template put it. -BOOL Skirmish_On_WM_INITDIALOG(HWND window, WPARAM wparam, LPARAM lparam) -{ - #define MP_MIN_MONEY 2500 - - HWND handle = GetDlgItem(window, IDC_SKIRMISH_UNITCOUNT); - if (handle) { - Slider_SetRange(handle, SessionClass::CountMin[1], SessionClass::CountMax[1]); - Slider_SetPos(handle, Session.Options.UnitCount); - } - - handle = GetDlgItem(window, IDC_SKIRMISH_UNITCOUNT_LABEL); - if (handle) { - // - } - - handle = GetDlgItem(window, IDC_SKIRMISH_TECHLEVEL); - if (handle) { - Slider_SetRange(handle, 1, MPLAYER_BUILD_LEVEL_MAX); - Slider_SetPos(handle, BuildLevel); - } - - handle = GetDlgItem(window, IDC_SKIRMISH_TECHLEVEL_LABEL); - if (handle) { - // - } - - handle = GetDlgItem(window, IDC_DIFFICULTY_SLIDER); - if (handle) { - Slider_SetRange(handle, 0, 2); - Slider_SetPos(handle, Session.Options.AIDifficulty); - } - - handle = GetDlgItem(window, IDC_SKIRMISH_AILEVEL_LABEL); - if (handle) { - // - } - - handle = GetDlgItem(window, IDC_SKIRMISH_CREDITS); - if (handle) { - Slider_SetRange(handle, MP_MIN_MONEY, Rule->MPMaxMoney); - Slider_SetPos(handle, Session.Options.Credits); - SendMessage(handle, OD_SETTRACKSTEP, 0, 250); - } - - handle = GetDlgItem(window, IDC_SKIRMISH_AIPLAYERS); - if (handle) { - Slider_SetRange(handle, 1, 7); - Slider_SetPos(handle, Session.Options.AIPlayers > 1 ? Session.Options.AIPlayers : 1); - } - - handle = GetDlgItem(window, IDC_GAME_SPEED_SLIDER); - if (handle) { - Slider_SetRange(handle, 0, 6); - Slider_SetPos(handle, 6 - Session.Options.GameSpeed); - } - - handle = GetDlgItem(window, IDC_SKIRMISH_NAME); - if (handle) SetWindowText(handle, Session.Handle); - - handle = GetDlgItem(window, IDC_SKIRMISH_SIDE); - if (handle) { - Fill_Country_Box(handle); - Select_Country_In_Box(handle, Session.House); - } - - SendDlgItemMessage(window, IDC_SKIRMISH_COLOR, CB_RESETCONTENT, 0, 0); - SendDlgItemMessage(window, IDC_SKIRMISH_COLOR, CB_INSERTSTRING, -1, (LPARAM)Fetch_String(TXT_GOLD)); - SendDlgItemMessage(window, IDC_SKIRMISH_COLOR, CB_INSERTSTRING, -1, (LPARAM)Fetch_String(TXT_RED)); - SendDlgItemMessage(window, IDC_SKIRMISH_COLOR, CB_INSERTSTRING, -1, (LPARAM)Fetch_String(TXT_BLUE)); - SendDlgItemMessage(window, IDC_SKIRMISH_COLOR, CB_INSERTSTRING, -1, (LPARAM)Fetch_String(TXT_GREEN)); - SendDlgItemMessage(window, IDC_SKIRMISH_COLOR, CB_INSERTSTRING, -1, (LPARAM)Fetch_String(TXT_ORANGE)); - SendDlgItemMessage(window, IDC_SKIRMISH_COLOR, CB_INSERTSTRING, -1, (LPARAM)Fetch_String(TXT_SKY_BLUE)); - SendDlgItemMessage(window, IDC_SKIRMISH_COLOR, CB_INSERTSTRING, -1, (LPARAM)Fetch_String(TXT_PURPLE)); - SendDlgItemMessage(window, IDC_SKIRMISH_COLOR, CB_INSERTSTRING, -1, (LPARAM)Fetch_String(TXT_PINK)); - SendDlgItemMessage(window, IDC_SKIRMISH_COLOR, CB_SETCURSEL, Session.PrefColor, 0); - - for (int player = 0; player < MAX_PLAYERS; player++) { - SendDlgItemMessage(window, IDC_SKIRMISH_COLOR, OD_SETCOLOR, player, (LPARAM)PlayerColorTable[player]); - } - - Set_Scenario_Info_From_Index(0); - Session.Options.ScenarioIndex = 0; - SendDlgItemMessage(window, IDC_SCENARIONAME, WM_SETTEXT, 0, (LPARAM)Session.Options.ScenarioDescription); - Clear_Vector(&Session.Players); - Clear_Vector(&Session.Computers); - - handle = GetDlgItem(window, IDC_SKIRMISH_BASES); - if (handle) Button_SetCheck(handle, Session.Options.Bases ? BST_CHECKED : BST_UNCHECKED); - - handle = GetDlgItem(window, IDC_SKIRMISH_CRATES); - if (handle) Button_SetCheck(handle, Session.Options.Goodies ? BST_CHECKED : BST_UNCHECKED); - - handle = GetDlgItem(window, IDC_SKIRMISH_FOG); - if (handle) Button_SetCheck(handle, Session.Options.FogOfWar ? BST_CHECKED : BST_UNCHECKED); - - handle = GetDlgItem(window, IDC_SKIRMISH_BRIDGES); - if (handle) Button_SetCheck(handle, Session.Options.BridgeDestruction ? BST_CHECKED : BST_UNCHECKED); - - handle = GetDlgItem(window, IDC_REDEPLOY_MCV); - if (handle) Button_SetCheck(handle, Session.Options.MCVRedeploy ? BST_CHECKED : BST_UNCHECKED); - - handle = GetDlgItem(window, IDC_MULTI_ENGINEER); - if (handle) Button_SetCheck(handle, Session.Options.CrapEngineers ? BST_CHECKED : BST_UNCHECKED); - - handle = GetDlgItem(window, IDC_SHORT_GAME); - if (handle) Button_SetCheck(handle, Session.Options.ShortGame ? BST_CHECKED : BST_UNCHECKED); - - Update_Network_Dialog_Preview(window); - return(FALSE); -} diff --git a/code/smudge.cpp b/code/smudge.cpp index dd77b643d..ef0a2c89d 100644 --- a/code/smudge.cpp +++ b/code/smudge.cpp @@ -38,7 +38,6 @@ * SmudgeClass::operator new -- Creator of smudge objects. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "smudge.h" @@ -301,16 +300,7 @@ RTTIType SmudgeClass::Fetch_RTTI(void) const } -/// -/// Fetches the class identifier of this object. -/// This routine is part of the IPersist interface. It is used by the save and load -/// system to recognize what kind of object it is about to create. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE SmudgeClass::GetClassID(CLSID * retval) +ClassID SmudgeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_SmudgeClass; - return(S_OK); + return(ClassID_SmudgeClass); } diff --git a/code/smudge.h b/code/smudge.h index 3fb659f24..f37359599 100644 --- a/code/smudge.h +++ b/code/smudge.h @@ -59,7 +59,7 @@ class SmudgeClass : public ObjectClass SmudgeClass(SmudgeTypeClass const * type, Coord const & pos = COORD_NONE, HousesType = HOUSE_NONE); virtual ~SmudgeClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/smudtype.cpp b/code/smudtype.cpp index afbdf5090..8f3e12cb2 100644 --- a/code/smudtype.cpp +++ b/code/smudtype.cpp @@ -44,7 +44,6 @@ * SmudgetypeClass::Occupy_List -- Determines occupation list for smudge object. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "smudtype.h" @@ -337,17 +336,9 @@ void SmudgeTypeClass::Serialize(SaveStreamClass & stream) } -/// -/// Fetches the class identifier of this object. -/// The save system asks for this so that it knows which class to construct when the object -/// is read back out of a save file. -/// -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE SmudgeTypeClass::GetClassID(CLSID * retval) +ClassID SmudgeTypeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_SmudgeTypeClass; - return(S_OK); + return(ClassID_SmudgeTypeClass); } diff --git a/code/smudtype.h b/code/smudtype.h index 373a0bda7..ad920b460 100644 --- a/code/smudtype.h +++ b/code/smudtype.h @@ -59,7 +59,7 @@ class SmudgeTypeClass : public ObjectTypeClass SmudgeTypeClass(char const * ininame = NULL); virtual ~SmudgeTypeClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; virtual void Post_Load(void) override; diff --git a/code/sounddlg.cpp b/code/sounddlg.cpp index 6a8c817c8..c6053f7f5 100644 --- a/code/sounddlg.cpp +++ b/code/sounddlg.cpp @@ -43,260 +43,26 @@ #include "incdec.h" #include "init.h" #include "language/language.h" -#include "ownrdraw.h" #include "theme.h" +#include "ui/uisound.h" #include "winfix.h" -bool DialogInitialized = false; - /// /// Handles the sound and music options dialog. -/// This routine brings up the sound controls and then services the owner draw dialog -/// handler until the player dismisses them. A cut down version of the dialog is used -/// when there is no game in progress, since the in game options do not apply there. +/// This routine brings up the sound controls and runs them until the player dismisses +/// them. A cut down version of the screen is used when there is no game in progress, since +/// the in game options do not apply there. /// /// This routine will not return until the player closes the dialog. void SoundControlsClass::Dialog(void) { - int rc = -1; DebugString("SoundControls: GameSpeed = %d, ScrollRate = %d, Detail = %d\n", Options.GameSpeed, Options.ScrollRate, Options.DetailLevel); - DialogInitialized = false; - - HWND dialog; - if (!GameActive) { - dialog = OwnerDraw::Begin_Dialog(IDD_SOUND_OPTIONS_DIALOG_LITE, Sound_Option_Dialog_Func); - } else { - dialog = OwnerDraw::Begin_Dialog(IDD_SOUND_OPTIONS_DIALOG, Sound_Option_Dialog_Func); - } - - if (dialog) { - - SetWindowLongPtr(dialog, DWLP_USER, (LONG_PTR)&rc); - - OwnerDraw::Display_Dialog(dialog); - while (rc == -1) { - if (OwnerDraw::Dialog_Message_Handler() == true) { - rc = 2; - } - if (!GameActive) { - Title_Screen_Restore(); - } - } - - OwnerDraw::End_Dialog(dialog); - } + UISoundPresenterClass screen; + screen.Refresh(); + UI_Sound_Screen(screen); DebugString("SoundControls: GameSpeed = %d, ScrollRate = %d, Detail = %d\n", Options.GameSpeed, Options.ScrollRate, Options.DetailLevel); } - -/*********************************************************************************************** - * SoundControlsClass::Process -- Handles all the options graphic interface. * - * * - * This routine is the main control for the visual representation of the options * - * screen. It handles the visual overlay and the player input. * - * * - * INPUT: none * - * * - * OUTPUT: none * - * * - * WARNINGS: none * - * * - * HISTORY: 12/31/1994 MML : Created. * - *=============================================================================================*/ -INT_PTR CALLBACK SoundControlsClass::Sound_Option_Dialog_Func(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - INT_PTR rc = OwnerDraw::Default_Dialog_Proc(window, message, wparam, lparam); - - if (rc == 0) { - switch (message) { - case WM_INITDIALOG: { - DialogInitialized = false; - bool enabled = AudioEngine.Is_Available(); - - /* - ** Music volume slider. - */ - HWND track = GetDlgItem(window, IDC_MUSIC_VOLUME); - if (track) { - SendMessage(track, OD_TRACKSILENT, 0, 0); - Slider_SetRange(track, 0, VOLUME_LEVELS); - Slider_SetPos(track, (int)(Options.ScoreVolume * (double)VOLUME_LEVELS + 0.5)); - EnableWindow(track, enabled); - } - - /* - ** Sound volume slider. - */ - track = GetDlgItem(window, IDC_SOUND_VOLUME); - if (track) { - SendMessage(track, OD_TRACKSILENT, 0, 0); - Slider_SetRange(track, 0, VOLUME_LEVELS); - Slider_SetPos(track, (int)(Options.SoundVolume * (double)VOLUME_LEVELS + 0.5)); - EnableWindow(track, enabled); - } - - track = GetDlgItem(window, IDC_VOICE_VOLUME); - if (track) { - SendMessage(track, OD_TRACKSILENT, 0, 0); - Slider_SetRange(track, 0, VOLUME_LEVELS); - Slider_SetPos(track, (int)(Options.VoiceVolume * (double)VOLUME_LEVELS + 0.5)); - EnableWindow(track, enabled); - } - - if (GameActive) { - - /* - ** Shuffle control. - */ - HWND button = GetDlgItem(window, IDC_SOUND_SHUFFLE); - if (button) { - Button_SetCheck(button, Options.IsScoreShuffle ? BST_CHECKED : BST_UNCHECKED); - EnableWindow(button, enabled); - } - - /* - ** Repeat control. - */ - button = GetDlgItem(window, IDC_SOUND_REPEAT); - if (button) { - Button_SetCheck(button, Options.IsScoreRepeat ? BST_CHECKED : BST_UNCHECKED); - EnableWindow(button, enabled); - } - - /* - ** Add all the themes to the list box. The list box entries are constructed - ** and then stored into allocated EMS memory blocks. - */ - HWND list = GetDlgItem(window, IDC_SOUND_TRACKLIST); - if (list) { - int active_theme = 0; - int visible_num = 1; - - ListBox_ResetContent(list); - - for (ThemeType index = THEME_FIRST; index < Theme.Max_Themes(); index++) { - if (Theme.Is_Allowed(index)) { - char buffer[100]; - int length = Theme.Track_Length(index); - char const * fullname = Theme.Full_Name(index); - - sprintf(buffer, "%02d - %s [%d:%02d]", visible_num, fullname, length / 60, length % 60); - visible_num++; - - int row = ListBox_AddString(list, buffer); - if (row != LB_ERR) { - ListBox_SetItemData(list, row, index); - if (Theme.What_Is_Playing() == index) { - active_theme = row; - } - } - } - } - - ListBox_SetCurSel(list, active_theme); - ListBox_SetTopIndex(list, active_theme); - EnableWindow(list, enabled); - } - } - - DialogInitialized = true; - } - - break; - - case WM_COMMAND: - switch (LOWORD(wparam)) { - - /* - ** Toggle the shuffle button. - */ - case IDC_SOUND_SHUFFLE: - Options.Set_Shuffle(Button_GetCheck((HWND)lparam) == BST_CHECKED); - if (Button_GetCheck((HWND)lparam) == BST_CHECKED) { - SendDlgItemMessage(window, IDC_SOUND_REPEAT, BM_SETCHECK, BST_UNCHECKED, 0); - Options.Set_Repeat(false); - } - break; - - /* - ** Toggle the repeat button. - */ - case IDC_SOUND_REPEAT: - Options.Set_Repeat(Button_GetCheck((HWND)lparam) == BST_CHECKED); - if (Button_GetCheck((HWND)lparam) == BST_CHECKED) { - SendDlgItemMessage(window, IDC_SOUND_SHUFFLE, BM_SETCHECK, BST_UNCHECKED, 0); - Options.Set_Shuffle(false); - } - break; - - /* - ** Stop all themes from playing. - */ - case IDC_SOUND_STOP: - if (HIWORD(wparam) == 0) { - Theme.Queue_Song(THEME_QUIET); - } - break; - - case IDOK: - if (HIWORD(wparam) == 0) { - HWND button = GetDlgItem(window, IDC_MUSIC_VOLUME); - if (button) { - Options.Set_Score_Volume(Slider_GetPos(button) / (double)VOLUME_LEVELS, false); - } - button = GetDlgItem(window, IDC_SOUND_VOLUME); - if (button) { - Options.Set_Sound_Volume(Slider_GetPos(button) / (double)VOLUME_LEVELS, false); - } - button = GetDlgItem(window, IDC_VOICE_VOLUME); - if (button) { - Options.Set_Voice_Volume(Slider_GetPos(button) / (double)VOLUME_LEVELS, false); - } - int * res = (int *)GetWindowLongPtr(window, DWLP_USER); - *res = IDOK; - } - break; - - /* - ** Start the currently selected theme to play. - */ - case IDC_SOUND_PLAY: - if (HIWORD(wparam) == 0) { - HWND list = GetDlgItem(window, IDC_SOUND_TRACKLIST); - if (list) { - int row = ListBox_GetCurSel(list); - if (row != LB_ERR) { - ThemeType theme = (ThemeType)ListBox_GetItemData(list, row); - Theme.Stop(); - Theme.Queue_Song(theme); - } - } - } - break; - } - break; - - /* - * Control volume. - */ - case WM_HSCROLL: - if (DialogInitialized) { - HWND track = (HWND)lparam; - if (track == GetDlgItem(window, IDC_MUSIC_VOLUME)) { - Options.Set_Score_Volume(Slider_GetPos(track) / (double)VOLUME_LEVELS, true); - } else if (track == GetDlgItem(window, IDC_SOUND_VOLUME)) { - Options.Set_Sound_Volume(Slider_GetPos(track) / (double)VOLUME_LEVELS, true); - } else if (track == GetDlgItem(window, IDC_VOICE_VOLUME)) { - Options.Set_Voice_Volume(Slider_GetPos(track) / (double)VOLUME_LEVELS, true); - } - } - break; - } - return(FALSE); - } - - return(rc); -} diff --git a/code/sounddlg.h b/code/sounddlg.h index 55856b1a9..cdeaf7c8b 100644 --- a/code/sounddlg.h +++ b/code/sounddlg.h @@ -43,6 +43,4 @@ class SoundControlsClass public: SoundControlsClass(void) {} void Dialog(void); - - static INT_PTR CALLBACK Sound_Option_Dialog_Func(HWND window, UINT message, WPARAM wparam, LPARAM lparam); }; diff --git a/code/spawnerconfig.cpp b/code/spawnerconfig.cpp index 3b16f7d5d..1e33ec1d0 100644 --- a/code/spawnerconfig.cpp +++ b/code/spawnerconfig.cpp @@ -8,6 +8,8 @@ ******************************************************************************/ +#include "always.h" + #include "spawnerconfig.h" #include "crc.h" diff --git a/code/spawnhouse.cpp b/code/spawnhouse.cpp index 200b140ea..d6019e114 100644 --- a/code/spawnhouse.cpp +++ b/code/spawnhouse.cpp @@ -8,6 +8,8 @@ ******************************************************************************/ +#include "always.h" + #include "spawnhouse.h" #include diff --git a/code/srfcache.cpp b/code/srfcache.cpp index 29ef9484b..6c88236bd 100644 --- a/code/srfcache.cpp +++ b/code/srfcache.cpp @@ -17,7 +17,7 @@ #include "ccfile.h" #include "dsurface.h" -#include "ownrdraw.h" +#include "drawhelp.h" #include "pcx.h" #include @@ -38,6 +38,9 @@ struct MSBitmap }; #pragma pack(pop) +static_assert(sizeof(BITMAPFILEHEADER) == 14, "Bitmap file header layout changed"); +static_assert(offsetof(MSBitmap, info) == 14, "Bitmap image layout changed"); + /// /// Builds a 16-bit pixel from an RGB triple with the channels remapped for the @@ -101,6 +104,9 @@ static unsigned int SurfaceCache_Wstring_Hash(Wstring & string) } +SurfaceCacheClass SurfaceCache; + + /// /// Constructs the cache as a Wstring-keyed dictionary using the surface /// cache hash function. diff --git a/code/startup.cpp b/code/startup.cpp index ced0e7089..962a9e5a4 100644 --- a/code/startup.cpp +++ b/code/startup.cpp @@ -32,7 +32,6 @@ * main -- Initial startup routine (preps library systems). * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "_alpha.h" @@ -64,7 +63,6 @@ #include "classfactory.h" #include "command.h" #include "conquer.h" -#include "cstream.h" #include "data.h" #include "dbgprint.h" #include "deploymentconfig.h" @@ -82,7 +80,6 @@ #include "house.h" #include "houstype.h" #include "hover.h" -#include "iblowfish.h" #include "infantry.h" #include "infatype.h" #include "init.h" @@ -163,7 +160,9 @@ #include #include +#ifdef _WIN32 #include +#endif #include #include #include @@ -174,20 +173,9 @@ extern HINSTANCE LanguageResources; #define AUTOPLAY_GUID "b350c6d2-2f36-11d3-a72c-0090272fa661" -#ifndef NO_BLOWFISH_DLL -const struct RegStruct { - const GUID *clsid; - const char *name; -} RegisterTheseDLLs[] = { - { &CLSID_BlowfishObject, "blowfish.dll" } -}; -#endif - HANDLE AppMutex; HANDLE AutoPlayMutex; -DynamicVectorClass RegisteredClasses; - //WinTimerClass * WinTimer; /// @@ -234,130 +222,80 @@ void Reset_Surfaces(void) } /// -/// Registers the game's COM classes with OLE. -/// This routine is called during startup, before anything that lives in the object -/// database can be created. It first ensures the support DLLs are present, asking any -/// that OLE cannot yet instantiate to register themselves, and then publishes a class -/// factory for every persistent game class so that objects can be created by CLSID. The -/// player is told by way of a message box if a support DLL could not be prepared. +/// Registers every class a saved game or a unit type can name by class identifier. +/// This runs during startup, before anything that lives in the object database can be +/// created. /// -/// bool; Did the preparation fail? Note the sense -- true means trouble. -static bool RegisterClasses(void) +static void RegisterClasses(void) { - - bool failed = false; -#ifndef NO_BLOWFISH_DLL - for (int i = 0; i < ARRAY_SIZE(RegisterTheseDLLs); i++) { - IUnknownPtr ptr; - HRESULT result = ptr.CreateInstance(*RegisterTheseDLLs[i].clsid, NULL, CLSCTX_ALL); - failed = FAILED(result); - if (failed) { - failed = false; - HINSTANCE hModule = LoadLibrary(RegisterTheseDLLs[i].name); - if (hModule != NULL) { - FARPROC fprocDllReg = (FARPROC)GetProcAddress(hModule, "DllRegisterServer"); - if (!fprocDllReg || (fprocDllReg(), FAILED(ptr.CreateInstance(*RegisterTheseDLLs[i].clsid, NULL, CLSCTX_ALL)))) { - failed = true; - } - FreeLibrary(hModule); - } else { - failed = true; - } - } - if (failed) { - break; - } - ptr.Release(); - } -#endif - - DWORD dwRegister; - IClassFactory *t; - - /// Handy macros to easily register the class factories. - - /// Register a class-object with OLE. - #define REGISTER_CLASS(_class, _clsid) \ - { \ - t = new TClassFactory<_class>; \ - CoRegisterClassObject(_clsid, t, CLSCTX_INPROC_SERVER, REGCLS_MULTIPLEUSE, &dwRegister); \ - RegisteredClasses.Add(dwRegister); \ - } \ - - REGISTER_CLASS(CStreamClass, CLSID_CompressStream); - REGISTER_CLASS(WaveClass, CLSID_WaveClass); - REGISTER_CLASS(TerrainTypeClass, CLSID_TerrainTypeClass); - REGISTER_CLASS(TerrainClass, CLSID_TerrainClass); - REGISTER_CLASS(SuperWeaponTypeClass, CLSID_SuperWeaponTypeClass); - REGISTER_CLASS(SuperClass, CLSID_SuperWeaponClass); - REGISTER_CLASS(Tactical, CLSID_TacticalMapClass); - REGISTER_CLASS(CellClass, CLSID_CellClass); - REGISTER_CLASS(EMPulseClass, CLSID_EMPulseClass); - REGISTER_CLASS(LightSourceClass, CLSID_LightSource); - REGISTER_CLASS(SideClass, CLSID_SideClass); - REGISTER_CLASS(TiberiumClass, CLSID_TiberiumClass); - REGISTER_CLASS(TubeClass, CLSID_TubeClass); - REGISTER_CLASS(CampaignClass, CLSID_CampaignClass); - REGISTER_CLASS(BuildingLightClass, CLSID_BuildingLightClass); - REGISTER_CLASS(WaypointPathClass, CLSID_WaypointPath); - REGISTER_CLASS(TEventClass, CLSID_EventClass); - REGISTER_CLASS(VoxelAnimTypeClass, CLSID_VoxelAnimTypeClass); - REGISTER_CLASS(VoxelAnimClass, CLSID_VoxelAnimClass); - REGISTER_CLASS(TActionClass, CLSID_ActionClass); - REGISTER_CLASS(TriggerClass, CLSID_TriggerClass); - REGISTER_CLASS(TriggerTypeClass, CLSID_TriggerTypeClass); - REGISTER_CLASS(ScriptClass, CLSID_ScriptClass); - REGISTER_CLASS(ScriptTypeClass, CLSID_ScriptTypeClass); - REGISTER_CLASS(TagClass, CLSID_TagClass); - REGISTER_CLASS(TagTypeClass, CLSID_TagTypeClass); - REGISTER_CLASS(TeamClass, CLSID_TeamClass); - REGISTER_CLASS(TeamTypeClass, CLSID_TeamTypeClass); - REGISTER_CLASS(TaskForceClass, CLSID_TaskForceClass); - REGISTER_CLASS(UnitTypeClass, CLSID_UnitTypeClass); - REGISTER_CLASS(BuildingTypeClass, CLSID_BuildingTypeClass); - REGISTER_CLASS(AircraftTypeClass, CLSID_AircraftTypeClass); - REGISTER_CLASS(InfantryTypeClass, CLSID_InfantryTypeClass); - REGISTER_CLASS(BulletTypeClass, CLSID_BulletTypeClass); - REGISTER_CLASS(IsometricTileTypeClass, CLSID_IsometricTileTypeClass); - REGISTER_CLASS(OverlayTypeClass, CLSID_OverlayTypeClass); - REGISTER_CLASS(SmudgeTypeClass, CLSID_SmudgeTypeClass); - REGISTER_CLASS(UnitClass, CLSID_UnitClass); - REGISTER_CLASS(BuildingClass, CLSID_BuildingClass); - REGISTER_CLASS(AircraftClass, CLSID_AircraftClass); - REGISTER_CLASS(InfantryClass, CLSID_InfantryClass); - REGISTER_CLASS(AnimClass, CLSID_AnimClass); - REGISTER_CLASS(AnimTypeClass, CLSID_AnimTypeClass); - REGISTER_CLASS(HouseTypeClass, CLSID_HouseTypeClass); - REGISTER_CLASS(HouseClass, CLSID_HouseClass); - REGISTER_CLASS(DriveLocomotionClass, CLSID_DriveLocomotion); - REGISTER_CLASS(JumpjetLocomotionClass, CLSID_JumpjetLocomotion); - REGISTER_CLASS(HoverLocomotionClass, CLSID_HoverLocomotion); - REGISTER_CLASS(TunnelLocomotionClass, CLSID_TunnelLocomotion); - REGISTER_CLASS(WalkLocomotionClass, CLSID_WalkLocomotion); - REGISTER_CLASS(DropPodLocomotionClass, CLSID_BallisticLocomotion); - REGISTER_CLASS(FlyLocomotionClass, CLSID_FlyerLocomotion); - REGISTER_CLASS(TeleportLocomotionClass, CLSID_TeleportLocomotion); - REGISTER_CLASS(MechLocomotionClass, CLSID_MechLocomotion); - REGISTER_CLASS(LevitateLocomotionClass, CLSID_LevitateLocomotion); - REGISTER_CLASS(BulletClass, CLSID_BulletClass); - REGISTER_CLASS(FactoryClass, CLSID_FactoryClass); - REGISTER_CLASS(WarheadTypeClass, CLSID_WarheadTypeClass); - REGISTER_CLASS(WeaponTypeClass, CLSID_WeaponTypeClass); - REGISTER_CLASS(ParticleClass, CLSID_ParticleClass); - REGISTER_CLASS(ParticleTypeClass, CLSID_ParticleTypeClass); - REGISTER_CLASS(ParticleSystemClass, CLSID_ParticleSystemClass); - REGISTER_CLASS(ParticleSystemTypeClass, CLSID_ParticleSystemTypeClass); - REGISTER_CLASS(AITriggerTypeClass, CLSID_AITriggerTypeClass); - REGISTER_CLASS(NeuronClass, CLSID_NeuronClass); - REGISTER_CLASS(FoggedObjectClass, CLSID_FoggedObjectClass); - REGISTER_CLASS(AlphaShapeClass, CLSID_AlphaShapeClass); - - if (failed) { - MessageBox(NULL, Fetch_String(TXT_PREPARECOM_FAILED), Fetch_String(TXT_SHORT_TITLE), MB_ICONEXCLAMATION); - } - - return(failed); - + #define REGISTER_CLASS(_class, _clsid) Register_Class<_class>(_clsid); + + REGISTER_CLASS(WaveClass, ClassID_WaveClass); + REGISTER_CLASS(TerrainTypeClass, ClassID_TerrainTypeClass); + REGISTER_CLASS(TerrainClass, ClassID_TerrainClass); + REGISTER_CLASS(SuperWeaponTypeClass, ClassID_SuperWeaponTypeClass); + REGISTER_CLASS(SuperClass, ClassID_SuperWeaponClass); + REGISTER_CLASS(Tactical, ClassID_TacticalMapClass); + REGISTER_CLASS(CellClass, ClassID_CellClass); + REGISTER_CLASS(EMPulseClass, ClassID_EMPulseClass); + REGISTER_CLASS(LightSourceClass, ClassID_LightSource); + REGISTER_CLASS(SideClass, ClassID_SideClass); + REGISTER_CLASS(TiberiumClass, ClassID_TiberiumClass); + REGISTER_CLASS(TubeClass, ClassID_TubeClass); + REGISTER_CLASS(CampaignClass, ClassID_CampaignClass); + REGISTER_CLASS(BuildingLightClass, ClassID_BuildingLightClass); + REGISTER_CLASS(WaypointPathClass, ClassID_WaypointPath); + REGISTER_CLASS(TEventClass, ClassID_EventClass); + REGISTER_CLASS(VoxelAnimTypeClass, ClassID_VoxelAnimTypeClass); + REGISTER_CLASS(VoxelAnimClass, ClassID_VoxelAnimClass); + REGISTER_CLASS(TActionClass, ClassID_ActionClass); + REGISTER_CLASS(TriggerClass, ClassID_TriggerClass); + REGISTER_CLASS(TriggerTypeClass, ClassID_TriggerTypeClass); + REGISTER_CLASS(ScriptClass, ClassID_ScriptClass); + REGISTER_CLASS(ScriptTypeClass, ClassID_ScriptTypeClass); + REGISTER_CLASS(TagClass, ClassID_TagClass); + REGISTER_CLASS(TagTypeClass, ClassID_TagTypeClass); + REGISTER_CLASS(TeamClass, ClassID_TeamClass); + REGISTER_CLASS(TeamTypeClass, ClassID_TeamTypeClass); + REGISTER_CLASS(TaskForceClass, ClassID_TaskForceClass); + REGISTER_CLASS(UnitTypeClass, ClassID_UnitTypeClass); + REGISTER_CLASS(BuildingTypeClass, ClassID_BuildingTypeClass); + REGISTER_CLASS(AircraftTypeClass, ClassID_AircraftTypeClass); + REGISTER_CLASS(InfantryTypeClass, ClassID_InfantryTypeClass); + REGISTER_CLASS(BulletTypeClass, ClassID_BulletTypeClass); + REGISTER_CLASS(IsometricTileTypeClass, ClassID_IsometricTileTypeClass); + REGISTER_CLASS(OverlayTypeClass, ClassID_OverlayTypeClass); + REGISTER_CLASS(SmudgeTypeClass, ClassID_SmudgeTypeClass); + REGISTER_CLASS(UnitClass, ClassID_UnitClass); + REGISTER_CLASS(BuildingClass, ClassID_BuildingClass); + REGISTER_CLASS(AircraftClass, ClassID_AircraftClass); + REGISTER_CLASS(InfantryClass, ClassID_InfantryClass); + REGISTER_CLASS(AnimClass, ClassID_AnimClass); + REGISTER_CLASS(AnimTypeClass, ClassID_AnimTypeClass); + REGISTER_CLASS(HouseTypeClass, ClassID_HouseTypeClass); + REGISTER_CLASS(HouseClass, ClassID_HouseClass); + REGISTER_CLASS(DriveLocomotionClass, ClassID_DriveLocomotion); + REGISTER_CLASS(JumpjetLocomotionClass, ClassID_JumpjetLocomotion); + REGISTER_CLASS(HoverLocomotionClass, ClassID_HoverLocomotion); + REGISTER_CLASS(TunnelLocomotionClass, ClassID_TunnelLocomotion); + REGISTER_CLASS(WalkLocomotionClass, ClassID_WalkLocomotion); + REGISTER_CLASS(DropPodLocomotionClass, ClassID_BallisticLocomotion); + REGISTER_CLASS(FlyLocomotionClass, ClassID_FlyerLocomotion); + REGISTER_CLASS(TeleportLocomotionClass, ClassID_TeleportLocomotion); + REGISTER_CLASS(MechLocomotionClass, ClassID_MechLocomotion); + REGISTER_CLASS(LevitateLocomotionClass, ClassID_LevitateLocomotion); + REGISTER_CLASS(BulletClass, ClassID_BulletClass); + REGISTER_CLASS(FactoryClass, ClassID_FactoryClass); + REGISTER_CLASS(WarheadTypeClass, ClassID_WarheadTypeClass); + REGISTER_CLASS(WeaponTypeClass, ClassID_WeaponTypeClass); + REGISTER_CLASS(ParticleClass, ClassID_ParticleClass); + REGISTER_CLASS(ParticleTypeClass, ClassID_ParticleTypeClass); + REGISTER_CLASS(ParticleSystemClass, ClassID_ParticleSystemClass); + REGISTER_CLASS(ParticleSystemTypeClass, ClassID_ParticleSystemTypeClass); + REGISTER_CLASS(AITriggerTypeClass, ClassID_AITriggerTypeClass); + REGISTER_CLASS(NeuronClass, ClassID_NeuronClass); + REGISTER_CLASS(FoggedObjectClass, ClassID_FoggedObjectClass); + REGISTER_CLASS(AlphaShapeClass, ClassID_AlphaShapeClass); } /// @@ -433,6 +371,9 @@ int CALLBACK WinMain ( HINSTANCE instance , HINSTANCE , char * , int command_sho ProgramInstance = instance; + // Refuses a build whose type sizes do not match the ones LZO was compiled against. + if (lzo_init() != LZO_E_OK) return(1); + Debug_Init(); // Handed over now because the exception path may not ask the logger for anything: the @@ -525,11 +466,7 @@ int CALLBACK WinMain ( HINSTANCE instance , HINSTANCE , char * , int command_sho return(EXIT_SUCCESS); } - OleInitialize(NULL); - - if (RegisterClasses()) { - exit(EXIT_FAILURE); - } + RegisterClasses(); /* ** Get the full path to the .EXE @@ -603,7 +540,6 @@ int CALLBACK WinMain ( HINSTANCE instance , HINSTANCE , char * , int command_sho wsprintf (buffer, Fetch_String(TXT_CRITICALLY_LOW), (INIT_FREE_DISK_SPACE) / (1024 * 1024)); int reply = MessageBox(NULL, buffer, Fetch_String(TXT_SHORT_TITLE), MB_ICONQUESTION|MB_YESNO); if (reply == IDNO) { - OleUninitialize(); return(EXIT_FAILURE); } } @@ -734,7 +670,6 @@ int CALLBACK WinMain ( HINSTANCE instance , HINSTANCE , char * , int command_sho Debug_Console_Hold(); } - OleUninitialize(); return(error_code); } @@ -1055,10 +990,7 @@ void __cdecl Prog_End(void) Scen = NULL; } - for (i = 0; i < RegisteredClasses.Count(); i++) { - CoRevokeClassObject((DWORD)RegisteredClasses[i]); - } - RegisteredClasses.Clear(); + Unregister_Classes(); if (LanguageResources) { FreeLibrary(LanguageResources); @@ -1106,7 +1038,6 @@ void Emergency_Exit(void) } } - OleUninitialize(); if (MouseCursor) { MouseCursor->Release_Mouse(); diff --git a/code/stats.cpp b/code/stats.cpp index 45628b66e..3f5604e2b 100644 --- a/code/stats.cpp +++ b/code/stats.cpp @@ -62,6 +62,71 @@ int WestwoodOnline_PortNumber = 1234; #include "unittype.h" #include "win.h" +#include "version.h" + +#ifndef _WIN32 +#include +#include +#include +#endif + +namespace { + +/// +/// Reports the machine's installed memory, which the report sends as a byte count. +/// +unsigned long long Physical_Memory_Bytes(void) +{ +#ifdef _WIN32 + MEMORYSTATUS mem_info; + mem_info.dwLength = sizeof(mem_info); + GlobalMemoryStatus(&mem_info); + return((unsigned long long)mem_info.dwTotalPhys); +#elif defined(__APPLE__) + int name[2] = { CTL_HW, HW_MEMSIZE }; + unsigned long long total = 0; + size_t length = sizeof(total); + if (sysctl(name, 2, &total, &length, NULL, 0) == 0) { + return(total); + } + return(0); +#else + long const pages = sysconf(_SC_PHYS_PAGES); + long const page_size = sysconf(_SC_PAGESIZE); + if (pages > 0 && page_size > 0) { + return((unsigned long long)pages * (unsigned long long)page_size); + } + return(0); +#endif +} + +/// +/// Reads a file's last write time in the Windows epoch the report field carries. +/// +/// bool; Was a time read? +bool Program_Write_Time(char const * path, FILETIME & result) +{ +#ifdef _WIN32 + RawFileClass file; + file.Set_Name(path); + file.Open(); + HANDLE handle = file.Get_File_Handle(); + return(handle != INVALID_HANDLE_VALUE && GetFileTime(handle, NULL, NULL, &result) != FALSE); +#else + struct stat info; + if (stat(path, &info) != 0) { + return(false); + } + unsigned long long const ticks = (unsigned long long)info.st_mtime * 10000000ULL + 116444736000000000ULL; + result.dwLowDateTime = (DWORD)(ticks & 0xFFFFFFFFULL); + result.dwHighDateTime = (DWORD)(ticks >> 32); + return(true); +#endif +} + +} + + #define FIELD_GAME_ID "IDNO" #define FIELD_START_CREDITS "CRED" #define FIELD_BASES "BASE" @@ -402,10 +467,7 @@ void Send_Statistics_Packet(void) /* ** Memory */ - MEMORYSTATUS mem_info; - mem_info.dwLength=sizeof(mem_info); - GlobalMemoryStatus(&mem_info); - stats.Add_Field (FIELD_MEMORY, (int)mem_info.dwTotalPhys); + stats.Add_Field (FIELD_MEMORY, (int)Physical_Memory_Bytes()); /* ** Game speed setting. @@ -422,18 +484,10 @@ void Send_Statistics_Packet(void) char path_to_exe[280]; FILETIME write_time; //File time is 64 bits - GetModuleFileName (ProgramInstance, path_to_exe, sizeof(path_to_exe)); - RawFileClass file; - file.Set_Name(path_to_exe); - file.Open(); - HANDLE handle = file.Get_File_Handle(); - - if (handle != INVALID_HANDLE_VALUE) { - if (GetFileTime (handle, NULL, NULL, &write_time)){ - write_time.dwLowDateTime = htonl (write_time.dwLowDateTime); - write_time.dwHighDateTime = htonl (write_time.dwHighDateTime); - stats.Add_Field (FIELD_GAME_BUILD_DATE, (void*)&write_time, sizeof (write_time)); - } + if (Program_File_Name(path_to_exe, sizeof(path_to_exe)) && Program_Write_Time(path_to_exe, write_time)) { + write_time.dwLowDateTime = htonl (write_time.dwLowDateTime); + write_time.dwHighDateTime = htonl (write_time.dwHighDateTime); + stats.Add_Field (FIELD_GAME_BUILD_DATE, (void*)&write_time, sizeof (write_time)); } /* diff --git a/code/stimer.cpp b/code/stimer.cpp index 9574ee3db..1d4dd13dc 100644 --- a/code/stimer.cpp +++ b/code/stimer.cpp @@ -33,6 +33,7 @@ #include "stimer.h" +#include "hostclock.h" #include "win.h" #ifdef _MSC_VER @@ -48,13 +49,13 @@ /// /// Fetches the current system timer value. /// This routine is the clock source that the timer templates are built upon. It scales -/// the Windows multimedia clock down so that timers tick in game sized units rather -/// than in milliseconds. +/// the host clock down so that timers tick in game sized units rather than in +/// milliseconds. /// /// Returns with the current system time, expressed in timer ticks. int SystemTimerClass::operator () (void) const { - return(timeGetTime()/16); + return(Host_Milliseconds()/16); } @@ -66,5 +67,5 @@ int SystemTimerClass::operator () (void) const /// Returns with the current system time, expressed in timer ticks. SystemTimerClass::operator int (void) const { - return(timeGetTime()/16); + return(Host_Milliseconds()/16); } diff --git a/code/sun.h b/code/sun.h index 1364b5222..d5b7885df 100644 --- a/code/sun.h +++ b/code/sun.h @@ -13,9 +13,7 @@ #pragma once -#ifdef INCLUDE_COM -#include "isun.h" -#endif +#include "classids.h" #include /// Everything from here on is the content of defines.h. diff --git a/code/super.cpp b/code/super.cpp index c1339fa81..e9b9117ed 100644 --- a/code/super.cpp +++ b/code/super.cpp @@ -40,7 +40,6 @@ * SuperClass::Suspend -- Suspend the charging of the super weapon. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "super.h" @@ -801,15 +800,9 @@ bool SuperClass::Is_Charging(void) const } -/// -/// Fetches the persistent class identifier for the super weapon. -/// -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE SuperClass::GetClassID(CLSID * retval) +ClassID SuperClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_SuperWeaponClass; - return(S_OK); + return(ClassID_SuperWeaponClass); } diff --git a/code/super.h b/code/super.h index d5dd0954e..2de481e3c 100644 --- a/code/super.h +++ b/code/super.h @@ -48,7 +48,7 @@ class SuperClass : public AbstractClass SuperClass(SuperWeaponTypeClass * type, HouseClass * owner); virtual ~SuperClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/suprtype.cpp b/code/suprtype.cpp index 6c5834955..67a37738d 100644 --- a/code/suprtype.cpp +++ b/code/suprtype.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "suprtype.h" @@ -97,17 +96,9 @@ SuperWeaponTypeClass::~SuperWeaponTypeClass(void) } -/// -/// Fetches the class identifier of this object. -/// The save game system uses this to know which class to construct when the object is -/// read back in. -/// -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE SuperWeaponTypeClass::GetClassID(CLSID * retval) +ClassID SuperWeaponTypeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_SuperWeaponTypeClass; - return(S_OK); + return(ClassID_SuperWeaponTypeClass); } diff --git a/code/suprtype.h b/code/suprtype.h index 8542bcbc9..66ff6b4e0 100644 --- a/code/suprtype.h +++ b/code/suprtype.h @@ -33,7 +33,7 @@ class SuperWeaponTypeClass : public AbstractTypeClass SuperWeaponTypeClass(char const * ininame = NULL); virtual ~SuperWeaponTypeClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; virtual void Post_Load(void) override; diff --git a/code/swizzle.cpp b/code/swizzle.cpp index 48b2ac401..3ac2a94de 100644 --- a/code/swizzle.cpp +++ b/code/swizzle.cpp @@ -229,6 +229,21 @@ void SwizzleManagerClass::Resolve(void) } +/// +/// Takes back everything registered since the mark, clearing each pointer slot it covers. +/// A slot that was registered still holds the identity read from the file, which is not +/// an address; clearing it lets the object it belongs to be destroyed safely. +/// +void SwizzleManagerClass::Abandon(MarkType const & mark) +{ + for (std::size_t index = mark.Requests; index < RequestTable.size(); index++) { + *(void **)RequestTable[index].Pointer = NULL; + } + RequestTable.resize(mark.Requests); + PointerTable.resize(mark.Pointers); +} + + /// /// Throws away every pending request and announcement. /// The load code calls this routine before it starts reading, so that whatever a load that diff --git a/code/swizzle.h b/code/swizzle.h index f49054119..293f31431 100644 --- a/code/swizzle.h +++ b/code/swizzle.h @@ -78,6 +78,18 @@ class SwizzleManagerClass void Resolve(void); void Discard(void); + /* + * The tables' extent at some point of a load, so that a record which fails after + * it can take back what it registered before its object is destroyed. + */ + struct MarkType { + std::size_t Requests; + std::size_t Pointers; + }; + MarkType Mark(void) const {return(MarkType{RequestTable.size(), PointerTable.size()});} + void Abandon(MarkType const & mark); + void Abandon(void) {Abandon(MarkType{0, 0});} + private: /* * These are the pointers read back from the save file that still hold a swizzle ID diff --git a/code/syncrechook.cpp b/code/syncrechook.cpp index 6469adb0f..7ff67a04f 100644 --- a/code/syncrechook.cpp +++ b/code/syncrechook.cpp @@ -246,9 +246,14 @@ void Sync_Recorder_Arm(void) bool const network = (Session.Type == GAME_IPX || Session.Type == GAME_INTERNET); SyncRecorder.Set_Recording(network || Session.Record || Session.Play); - ModuleBase = (uintptr_t)GetModuleHandle(nullptr); + ModuleBase = 0; ModuleSize = 0; MapImageBase = 0; + +#ifdef _WIN32 + // Only a PE image carries the headers this walks, and a caller address is reported as an + // absolute address wherever it does not. + ModuleBase = (uintptr_t)GetModuleHandle(nullptr); if (ModuleBase != 0) { IMAGE_DOS_HEADER const * dos = (IMAGE_DOS_HEADER const *)ModuleBase; if (dos->e_magic == IMAGE_DOS_SIGNATURE) { @@ -258,6 +263,7 @@ void Sync_Recorder_Arm(void) } } } +#endif MapImageBase = Sync_Preferred_Image_Base(); diff --git a/code/syncreport.cpp b/code/syncreport.cpp index 626ffbf2b..fa32e7164 100644 --- a/code/syncreport.cpp +++ b/code/syncreport.cpp @@ -93,6 +93,52 @@ #include #include +#ifndef _WIN32 +#include +#include +#endif + +/// The report stamps its filename with the local wall clock. +static void Fetch_Local_Time(SYSTEMTIME* result) +{ +#ifdef _WIN32 + GetLocalTime(result); +#else + time_t const seconds = time(NULL); + struct tm local; + localtime_r(&seconds, &local); + result->wYear = (WORD)(local.tm_year + 1900); + result->wMonth = (WORD)(local.tm_mon + 1); + result->wDayOfWeek = (WORD)local.tm_wday; + result->wDay = (WORD)local.tm_mday; + result->wHour = (WORD)local.tm_hour; + result->wMinute = (WORD)local.tm_min; + result->wSecond = (WORD)local.tm_sec; + result->wMilliseconds = 0; +#endif +} + +/// The report records the x87 control word so two machines can be compared. Nothing outside +/// x86 has one, so the report says zero rather than inventing a reading. +static unsigned Fetch_FPU_Control_Word(void) +{ +#ifdef _WIN32 + return((unsigned)_controlfp(0, 0)); +#else + return(0); +#endif +} + +static DWORD Fetch_Last_Error(void) +{ +#ifdef _WIN32 + return(GetLastError()); +#else + return((DWORD)errno); +#endif +} + + namespace { int LastReportFrame = -1; @@ -197,7 +243,7 @@ void Print_CRCs(EventClass const * events, int count, unsigned const * crc_ring, char const * debug_dir = Debug_Directory(); if (debug_dir != NULL && debug_dir[0] != '\0') { SYSTEMTIME now; - GetLocalTime(&now); + Fetch_Local_Time(&now); Delete_Files_Older_Than(debug_dir, "SYNC_*.LOG", SYNC_REPORT_MAX_AGE_DAYS); snprintf(filename, sizeof(filename), "%s\\SYNC_H%d_%02u-%02u-%04u_%02u-%02u-%02u_F%d.LOG", debug_dir, PlayerPtr->HeapID, @@ -211,7 +257,7 @@ void Print_CRCs(EventClass const * events, int count, unsigned const * crc_ring, fp = fopen(filename,"wt"); if (fp==NULL) { - DWORD const error = GetLastError(); + DWORD const error = Fetch_Last_Error(); DebugString("Failed to open the out-of-sync report %s. Error %d - %s\n", filename, error, Last_Error_Text(error)); return; } @@ -234,7 +280,7 @@ void Print_CRCs(EventClass const * events, int count, unsigned const * crc_ring, } fprintf(fp, "Seed: %08x\n", Seed); fprintf(fp, "Session type: %d\n", Session.Type); - fprintf(fp, "FPU control word: %x\n", _controlfp(0, 0)); + fprintf(fp, "FPU control word: %x\n", Fetch_FPU_Control_Word()); int cpu_type = PROC_PENTIUM_PRO; char vendor[32]; diff --git a/code/tactical.cpp b/code/tactical.cpp index bca9e834e..1935c14eb 100644 --- a/code/tactical.cpp +++ b/code/tactical.cpp @@ -11,7 +11,6 @@ * disclaimers apply; see LICENSE.md. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "tactical.h" @@ -3724,17 +3723,9 @@ bool Tactical::Draw_3D_Line(Coord const & coord1, Coord const & coord2, int colo } -/// -/// Fetches the class identifier of the tactical map. -/// This routine is used by the persistence system to recognize the object when it is read -/// back out of a save game. -/// -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE Tactical::GetClassID(CLSID * retval) +ClassID Tactical::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_TacticalMapClass; - return(S_OK); + return(ClassID_TacticalMapClass); } diff --git a/code/tactical.h b/code/tactical.h index 9c5a73732..91ba6de7d 100644 --- a/code/tactical.h +++ b/code/tactical.h @@ -103,7 +103,7 @@ class Tactical : public AbstractClass Tactical(void); virtual ~Tactical(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual RTTIType Fetch_RTTI(void) const override {return(RTTI_TACTICALMAP);} diff --git a/code/taction.cpp b/code/taction.cpp index 97d2ff0c6..fa9b68a7d 100644 --- a/code/taction.cpp +++ b/code/taction.cpp @@ -40,7 +40,7 @@ * ActionChoiceClass::Draw_It -- Display the action choice as part of a list box. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM +#include "hostclock.h" #include "always.h" #include "taction.h" @@ -2053,7 +2053,7 @@ bool TActionClass::TAction_ZOOM_IN(HouseClass * , ObjectClass * , TriggerClass * Map.Flag_To_Redraw(); Map.Render(); - Sleep(1000); + Host_Sleep(1000); return(true); } @@ -2961,18 +2961,9 @@ NeedType Action_Needs(TActionType action) } -/// -/// Fetches the class identifier of this object. -/// This routine is used by the persistence machinery to recognize what kind of object it -/// is about to load back. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE TActionClass::GetClassID(CLSID * retval) +ClassID TActionClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_ActionClass; - return(S_OK); + return(ClassID_ActionClass); } diff --git a/code/taction.h b/code/taction.h index 1dd6a0595..c87a3f454 100644 --- a/code/taction.h +++ b/code/taction.h @@ -140,7 +140,7 @@ class TActionClass : public AbstractClass TActionClass(void); virtual ~TActionClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/tag.cpp b/code/tag.cpp index 518b7d56d..83dd8384a 100644 --- a/code/tag.cpp +++ b/code/tag.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "tag.h" @@ -424,16 +423,9 @@ void TagClass::Detach(AbstractClass const * target, bool all) } -/// -/// Fetches the class identifier that this tag persists under. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE TagClass::GetClassID(CLSID * retval) +ClassID TagClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_TagClass; - return(S_OK); + return(ClassID_TagClass); } diff --git a/code/tag.h b/code/tag.h index fe24ffe53..8e04d6ab9 100644 --- a/code/tag.h +++ b/code/tag.h @@ -29,7 +29,7 @@ class TagClass : public AbstractClass TagClass(TagTypeClass * type=NULL); virtual ~TagClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/tagtype.cpp b/code/tagtype.cpp index 95afd121a..26bef7fbf 100644 --- a/code/tagtype.cpp +++ b/code/tagtype.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "tagtype.h" @@ -376,18 +375,9 @@ TagTypeClass * TagTypeClass::Find_Or_Make(char const * name) } -/// -/// Fetches the class identifier of this object. -/// This routine is used by the save and load system so that a tag type can be -/// recognized when it is read back out of a stream. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE TagTypeClass::GetClassID(CLSID * retval) +ClassID TagTypeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_TagTypeClass; - return(S_OK); + return(ClassID_TagTypeClass); } diff --git a/code/tagtype.h b/code/tagtype.h index 1d99464e3..59c60e29a 100644 --- a/code/tagtype.h +++ b/code/tagtype.h @@ -33,7 +33,7 @@ class TagTypeClass : public AbstractTypeClass TagTypeClass(char const * name = NULL); virtual ~TagTypeClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; static TagTypeClass * From_Name(char const * name); diff --git a/code/target.h b/code/target.h index 79ff24d05..f85862729 100644 --- a/code/target.h +++ b/code/target.h @@ -186,6 +186,10 @@ class TargetClass : public xTargetClass }; #pragma pack(pop) +// A target rides inside the network event union, so it stays two 32-bit fields. +static_assert(sizeof(xTargetClass) == 8, "Target layout changed"); +static_assert(sizeof(TargetClass) == 8, "Target layout changed"); + template class IndexClass; extern IndexClass TargetTracker; diff --git a/code/taskforc.cpp b/code/taskforc.cpp index 5e32142cf..6f8a85060 100644 --- a/code/taskforc.cpp +++ b/code/taskforc.cpp @@ -11,7 +11,6 @@ * disclaimers apply; see LICENSE.md. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "taskforc.h" @@ -319,17 +318,9 @@ void TaskForceClass::Serialize(SaveStreamClass & stream) } -/// -/// Fetches the class identifier of this object. -/// This routine is used by the save game code so that an object of the right kind can -/// be created when the game is loaded back in. -/// -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE TaskForceClass::GetClassID(CLSID * retval) +ClassID TaskForceClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_TaskForceClass; - return(S_OK); + return(ClassID_TaskForceClass); } diff --git a/code/taskforc.h b/code/taskforc.h index dc0d30b99..d9ab3d0cd 100644 --- a/code/taskforc.h +++ b/code/taskforc.h @@ -25,7 +25,7 @@ class TaskForceClass : public AbstractTypeClass TaskForceClass(char const *name=NULL); virtual ~TaskForceClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; static TaskForceClass * Find_Or_Make(char const * name); diff --git a/code/team.cpp b/code/team.cpp index 270cecb7a..4c360d150 100644 --- a/code/team.cpp +++ b/code/team.cpp @@ -70,7 +70,6 @@ * _Is_It_Playing -- Determines if unit is active and an initiated team member. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "team.h" @@ -2296,18 +2295,9 @@ void TeamClass::Serialize(SaveStreamClass & stream) } -/// -/// Fetches the class identifier of this object. -/// This routine is part of the persistence contract and is what allows the save game loader -/// to recognize a team when it reads one back in. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE TeamClass::GetClassID(CLSID * retval) +ClassID TeamClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_TeamClass; - return(S_OK); + return(ClassID_TeamClass); } diff --git a/code/team.h b/code/team.h index bc465e36f..c110225ea 100644 --- a/code/team.h +++ b/code/team.h @@ -265,7 +265,7 @@ class TeamClass : public AbstractClass TeamClass(TeamTypeClass const * team=0, HouseClass * owner=0, void * = NULL); virtual ~TeamClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/teamtype.cpp b/code/teamtype.cpp index c406abec7..a4809b702 100644 --- a/code/teamtype.cpp +++ b/code/teamtype.cpp @@ -54,7 +54,6 @@ * TeamTypeClass::~TeamTypeClass -- class destructor * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "teamtype.h" @@ -890,18 +889,9 @@ void TeamTypeClass::Serialize(SaveStreamClass & stream) } -/// -/// Fetches the class identifier of this object. -/// This is part of the persistence contract that the save and load code leans on to -/// recognize what it is reading back. -/// -/// Returns with S_OK and the class identifier filled in, or E_POINTER if no -/// destination was supplied. -HRESULT STDMETHODCALLTYPE TeamTypeClass::GetClassID(CLSID * retval) +ClassID TeamTypeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_TeamTypeClass; - return(S_OK); + return(ClassID_TeamTypeClass); } diff --git a/code/teamtype.h b/code/teamtype.h index 850e04144..d7c369fd1 100644 --- a/code/teamtype.h +++ b/code/teamtype.h @@ -68,7 +68,7 @@ class TeamTypeClass : public AbstractTypeClass TeamTypeClass(char const * name = NULL); virtual ~TeamTypeClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; static TeamTypeClass * Find_Or_Make(char const * ininame = NULL); diff --git a/code/techno.cpp b/code/techno.cpp index 20c2c3ffe..b1c553ad1 100644 --- a/code/techno.cpp +++ b/code/techno.cpp @@ -4064,7 +4064,7 @@ BulletClass * TechnoClass::Fire_At(AbstractClass * target, int which) if (valid_arc) { if (!bullet->Unlimbo(turret_coord, velocity)) { - bullet->Release(); + delete bullet; bullet = NULL; } else { @@ -4187,7 +4187,7 @@ BulletClass * TechnoClass::Fire_At(AbstractClass * target, int which) } } } else { - bullet->Release(); + delete bullet; bullet = NULL; } } diff --git a/code/techtype.cpp b/code/techtype.cpp index a273870cf..27ae39bfe 100644 --- a/code/techtype.cpp +++ b/code/techtype.cpp @@ -27,7 +27,7 @@ #include "combat.h" #include "findmake.h" #include "globals.h" -#include "ilocos.h" +#include "classids.h" #include "infatype.h" #include "mixfile.h" #include "psystype.h" @@ -44,7 +44,7 @@ #include "voc.hh" #include -#include +#include /*************************************************************************** @@ -129,7 +129,7 @@ TechnoTypeClass::TechnoTypeClass(char const * ininame, SpeedType speed) : CloakingSpeed(7), DebrisTypes(), DebrisMaximums(), - Locomotor(CLSID_TeleportLocomotion), + Locomotor(ClassID_TeleportLocomotion), VoxelCenterY(0), VoxelCenterX(0), Weight(1), @@ -565,7 +565,7 @@ bool TechnoTypeClass::Read_INI(CCINIClass const & ini) } PitchSpeed = ini.Get_Float(Name(), "PitchSpeed", PitchSpeed); - Locomotor = ini.Get_CLSID(IniName, "Locomotor", Locomotor); + Locomotor = ini.Get_ClassID(IniName, "Locomotor", Locomotor); CloakingSpeed = ini.Get_Int(Name(), "CloakingSpeed", CloakingSpeed); ThreatAvoidanceCoefficient = ini.Get_Float(Name(), "ThreatAvoidanceCoefficient", ThreatAvoidanceCoefficient); SlowdownDistance = ini.Get_Int(Name(), "SlowdownDistance", SlowdownDistance); diff --git a/code/techtype.h b/code/techtype.h index aeb48195c..5743e3900 100644 --- a/code/techtype.h +++ b/code/techtype.h @@ -14,6 +14,7 @@ #pragma once #include "_weapon.h" +#include "classids.h" #include "objtype.h" #include "typelist.h" @@ -122,7 +123,7 @@ class TechnoTypeClass : public ObjectTypeClass * about. It is what decides whether the object drives, walks, hovers, flies or * tunnels, and an instance of it is created for every object as it is unlimboed. */ - CLSID Locomotor; + ClassID Locomotor; /* * These are the half extents of this object's voxel model, measured off the artwork diff --git a/code/teleport.cpp b/code/teleport.cpp index ab912d72e..5442f1cc6 100644 --- a/code/teleport.cpp +++ b/code/teleport.cpp @@ -36,7 +36,7 @@ TeleportLocomotionClass::TeleportLocomotionClass(void) : /// The object counts as moving from the moment a destination is handed to this /// locomotor until the jump has actually been made. /// -boolean STDMETHODCALLTYPE TeleportLocomotionClass::Is_Moving(void) +bool TeleportLocomotionClass::Is_Moving(void) { if (DestinationCoord != COORD_NONE) { return(true); @@ -50,7 +50,7 @@ boolean STDMETHODCALLTYPE TeleportLocomotionClass::Is_Moving(void) /// This is the plain opposite of Is_Moving. An object with a teleport ordered counts as /// being on the move even though it has not gone anywhere yet. /// -boolean TeleportLocomotionClass::Is_Stationary(void) +bool TeleportLocomotionClass::Is_Stationary(void) { if (Is_Moving() == false) { return(true); @@ -64,7 +64,7 @@ boolean TeleportLocomotionClass::Is_Stationary(void) /// /// Returns with the pending teleport destination, or with the object's current /// position if no teleport has been ordered. -Coord STDMETHODCALLTYPE TeleportLocomotionClass::Destination(void) +Coord TeleportLocomotionClass::Destination(void) { if (Is_Moving()) { return(DestinationCoord); @@ -78,7 +78,7 @@ Coord STDMETHODCALLTYPE TeleportLocomotionClass::Destination(void) /// The jump is not made here. It happens the next time this locomotor is processed. /// /// The coordinate to teleport the object to. -void STDMETHODCALLTYPE TeleportLocomotionClass::Move_To(Coord to) +void TeleportLocomotionClass::Move_To(Coord to) { DestinationCoord = to; } @@ -89,7 +89,7 @@ void STDMETHODCALLTYPE TeleportLocomotionClass::Move_To(Coord to) /// The pending destination is forgotten, so the object stays where it is rather than /// making the jump. /// -void STDMETHODCALLTYPE TeleportLocomotionClass::Stop_Moving(void) +void TeleportLocomotionClass::Stop_Moving(void) { DestinationCoord = COORD_NONE; } @@ -102,7 +102,7 @@ void STDMETHODCALLTYPE TeleportLocomotionClass::Stop_Moving(void) /// it now stands. The whole journey is over by the time this routine returns. /// /// bool; Is there more movement still to process? A teleport never leaves any. -boolean STDMETHODCALLTYPE TeleportLocomotionClass::Process(void) +bool TeleportLocomotionClass::Process(void) { if (Is_Moving()) { LinkedTo->Mark(MARK_UP); @@ -112,22 +112,13 @@ boolean STDMETHODCALLTYPE TeleportLocomotionClass::Process(void) LinkedTo->Per_Cell_Process(PCP_END); LinkedTo->Look(); } - return(VARIANT_FALSE); + return(false); } -/// -/// Fetches the class identifier of this locomotor. -/// This routine is used by the persistence system to record which locomotor was -/// written, so that the right one can be created when the save game is loaded. -/// -/// Pointer to the class identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE TeleportLocomotionClass::GetClassID(CLSID * retval) +ClassID TeleportLocomotionClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_TeleportLocomotion; - return(S_OK); + return(ClassID_TeleportLocomotion); } @@ -149,7 +140,7 @@ void TeleportLocomotionClass::Serialize(SaveStreamClass & stream) /// the way to its destination, so it never rises out of the ground layer. /// /// Returns with the layer the object should be rendered in. -LayerType STDMETHODCALLTYPE TeleportLocomotionClass::In_Which_Layer(void) +LayerType TeleportLocomotionClass::In_Which_Layer(void) { return(LAYER_GROUND); } diff --git a/code/teleport.h b/code/teleport.h index 4efad8853..3e41d3396 100644 --- a/code/teleport.h +++ b/code/teleport.h @@ -19,18 +19,18 @@ class TeleportLocomotionClass : public LocomotionClass public: TeleportLocomotionClass(void); - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; - virtual boolean STDMETHODCALLTYPE Is_Moving(void) override; - virtual Coord STDMETHODCALLTYPE Destination(void) override; - virtual void STDMETHODCALLTYPE Move_To(Coord to) override; - virtual void STDMETHODCALLTYPE Stop_Moving(void) override; - virtual boolean STDMETHODCALLTYPE Process(void) override; - virtual LayerType STDMETHODCALLTYPE In_Which_Layer(void) override; + virtual bool Is_Moving(void) override; + virtual Coord Destination(void) override; + virtual void Move_To(Coord to) override; + virtual void Stop_Moving(void) override; + virtual bool Process(void) override; + virtual LayerType In_Which_Layer(void) override; - virtual boolean Is_Stationary(void); + virtual bool Is_Stationary(void); private: /* diff --git a/code/terrain.cpp b/code/terrain.cpp index 78039fecb..ee4cb3fff 100644 --- a/code/terrain.cpp +++ b/code/terrain.cpp @@ -52,7 +52,6 @@ * TerrainClass::~TerrainClass -- Default destructor for terrain class objects. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "terrain.h" @@ -914,8 +913,8 @@ bool TerrainClass::Render(Rect & cliprect, bool forced, bool extras_only) const /// under the identity it was constructed with is dropped before the members arrive. /// /// The stream to read the object from. -/// Returns with S_OK if the object was read successfully. -HRESULT STDMETHODCALLTYPE TerrainClass::Load(IStream * stream) +/// bool; Was the record read whole? +bool TerrainClass::Load(SaveStreamClass & stream) { TargetTracker.Remove_Index(Fetch_ID()); @@ -1090,16 +1089,7 @@ RTTIType TerrainClass::Fetch_RTTI(void) const } -/// -/// Fetches the class identifier for this object. -/// This routine is part of the IPersistStream implementation. The save system records -/// the identifier so that it knows what to recreate when the game is loaded back in. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE TerrainClass::GetClassID(CLSID * retval) +ClassID TerrainClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_TerrainClass; - return(S_OK); + return(ClassID_TerrainClass); } diff --git a/code/terrain.h b/code/terrain.h index 537160275..83fefb448 100644 --- a/code/terrain.h +++ b/code/terrain.h @@ -59,8 +59,8 @@ class TerrainClass : public ObjectClass, public StageClass TerrainClass(TerrainTypeClass const * type, Cell const & cell); virtual ~TerrainClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; - virtual HRESULT STDMETHODCALLTYPE Load(IStream * stream) override; + virtual ClassID Class_ID(void) const override; + virtual bool Load(SaveStreamClass & stream) override; virtual void Serialize(SaveStreamClass & stream) override; virtual void Post_Load(void) override; diff --git a/code/terrtype.cpp b/code/terrtype.cpp index 0e86c765b..82932b0ff 100644 --- a/code/terrtype.cpp +++ b/code/terrtype.cpp @@ -44,7 +44,6 @@ * TerrainTypeClass::operator new -- Allocates a terrain type object from special pool. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "terrtype.h" @@ -430,18 +429,9 @@ void TerrainTypeClass::Serialize(SaveStreamClass & stream) } -/// -/// Fetches the class identifier of this object. -/// The save system uses this identifier to know what kind of object to create when the -/// save file is loaded back in. -/// -/// Pointer to the buffer to fill in with the class identifier. -/// Returns with S_OK, or E_POINTER if no buffer was supplied. -HRESULT STDMETHODCALLTYPE TerrainTypeClass::GetClassID(CLSID * retval) +ClassID TerrainTypeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_TerrainTypeClass; - return(S_OK); + return(ClassID_TerrainTypeClass); } diff --git a/code/terrtype.h b/code/terrtype.h index 9bf8ec3c0..37b1fdee9 100644 --- a/code/terrtype.h +++ b/code/terrtype.h @@ -111,7 +111,7 @@ class TerrainTypeClass : public ObjectTypeClass TerrainTypeClass(char const * ininame = NULL); virtual ~TerrainTypeClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; virtual void Post_Load(void) override; diff --git a/code/tevent.cpp b/code/tevent.cpp index 167c46731..2b724d56a 100644 --- a/code/tevent.cpp +++ b/code/tevent.cpp @@ -39,7 +39,6 @@ * TEventClass::operator () -- Action operator to see if event is satisfied. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "tevent.h" @@ -839,18 +838,9 @@ void TEventClass::Compute_CRC(CRCEngine & crc) const } -/// -/// Fetches the class identifier of this object. -/// This routine is used by the persistence machinery to recognize what kind of object it -/// is about to load back. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE TEventClass::GetClassID(CLSID * retval) +ClassID TEventClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_EventClass; - return(S_OK); + return(ClassID_EventClass); } diff --git a/code/tevent.h b/code/tevent.h index 6dab13515..c7dc9e1bf 100644 --- a/code/tevent.h +++ b/code/tevent.h @@ -104,7 +104,7 @@ class TEventClass : public AbstractClass TEventClass(void); virtual ~TEventClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/tiberium.cpp b/code/tiberium.cpp index 6bfb58dae..3b01431c8 100644 --- a/code/tiberium.cpp +++ b/code/tiberium.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "tiberium.h" @@ -194,17 +193,9 @@ void TiberiumClass::Compute_CRC(CRCEngine & crc) const } -/// -/// Fetches the class identifier of the tiberium class. -/// This routine tells the save game loader which kind of object to create when this -/// tiberium type is read back in. -/// -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE TiberiumClass::GetClassID(CLSID * retval) +ClassID TiberiumClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_TiberiumClass; - return(S_OK); + return(ClassID_TiberiumClass); } @@ -213,10 +204,10 @@ HRESULT STDMETHODCALLTYPE TiberiumClass::GetClassID(CLSID * retval) /// The spread and growth pools are dropped before the members arrive, since the counts /// they track are about to be replaced with the saved ones. /// -/// Returns with S_OK if the tiberium type was loaded. +/// bool; Was the record read whole? /// The spread and growth systems are not saved, so they come back empty. They /// must be rebuilt once the game has finished loading. -HRESULT STDMETHODCALLTYPE TiberiumClass::Load(IStream * stream) +bool TiberiumClass::Load(SaveStreamClass & stream) { Clear_Spread(); Clear_Growth(); diff --git a/code/tiberium.h b/code/tiberium.h index 650aa07ee..7e02a6f8d 100644 --- a/code/tiberium.h +++ b/code/tiberium.h @@ -37,8 +37,8 @@ class TiberiumClass : public AbstractTypeClass TiberiumClass(char const * ininame = NULL); virtual ~TiberiumClass() override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; - virtual HRESULT STDMETHODCALLTYPE Load(IStream * stream) override; + virtual ClassID Class_ID(void) const override; + virtual bool Load(SaveStreamClass & stream) override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/tracker.cpp b/code/tracker.cpp index 1658bb586..146974e1d 100644 --- a/code/tracker.cpp +++ b/code/tracker.cpp @@ -224,15 +224,13 @@ void Process_Deferred_Deletion(void) break; } } - if (obj->Release()) { - if (typeid(BuildingClass) == typeid(*obj) - || typeid(UnitClass) == typeid(*obj) - || typeid(InfantryClass) == typeid(*obj) - || typeid(AircraftClass) == typeid(*obj)) { - ((ObjectClass *)obj)->IsActive = true; - } - delete obj; + if (typeid(BuildingClass) == typeid(*obj) + || typeid(UnitClass) == typeid(*obj) + || typeid(InfantryClass) == typeid(*obj) + || typeid(AircraftClass) == typeid(*obj)) { + ((ObjectClass *)obj)->IsActive = true; } + delete obj; } else { ++index; } diff --git a/code/trigger.cpp b/code/trigger.cpp index bc1c9720d..2bf8a078a 100644 --- a/code/trigger.cpp +++ b/code/trigger.cpp @@ -42,7 +42,6 @@ * TriggerClass::~TriggerClass -- Destructor for trigger objects. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "trigger.h" @@ -500,18 +499,9 @@ void TriggerClass::Compute_CRC(CRCEngine & crc) const } -/// -/// Fetches the class identifier of this object. -/// This routine is part of the persistence support. The save game system uses the -/// identifier to work out what kind of object to build when the stream is read back in. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE TriggerClass::GetClassID(CLSID * retval) +ClassID TriggerClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_TriggerClass; - return(S_OK); + return(ClassID_TriggerClass); } diff --git a/code/trigger.h b/code/trigger.h index 771341c7d..35bf3233d 100644 --- a/code/trigger.h +++ b/code/trigger.h @@ -65,7 +65,7 @@ class TriggerClass : public AbstractClass TriggerClass(TriggerTypeClass * trigtype=NULL); virtual ~TriggerClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/trigtype.cpp b/code/trigtype.cpp index 9014fe2ae..849f89755 100644 --- a/code/trigtype.cpp +++ b/code/trigtype.cpp @@ -46,7 +46,6 @@ * TriggerTypeClass::~TriggerTypeClass -- Deleting a trigger type deletes associated triggers* * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "trigtype.h" @@ -811,18 +810,9 @@ void TriggerTypeClass::Compute_CRC(CRCEngine & crc) const } -/// -/// Fetches the class identifier of this object. -/// The save game system uses this identifier to know which kind of object to build -/// when the stream is read back in. -/// -/// Pointer to the location to store the class identifier. -/// Returns with S_OK, or E_POINTER if no storage location was supplied. -HRESULT STDMETHODCALLTYPE TriggerTypeClass::GetClassID(CLSID * retval) +ClassID TriggerTypeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_TriggerTypeClass; - return(S_OK); + return(ClassID_TriggerTypeClass); } diff --git a/code/trigtype.h b/code/trigtype.h index f8ba9e5a2..77c95d070 100644 --- a/code/trigtype.h +++ b/code/trigtype.h @@ -55,7 +55,7 @@ class TriggerTypeClass : public AbstractTypeClass static TriggerTypeClass * Find_Or_Make(char const * ininame = NULL); - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; /* ** File I/O routines diff --git a/code/tube.cpp b/code/tube.cpp index 0d67042bc..e0f17408a 100644 --- a/code/tube.cpp +++ b/code/tube.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "tube.h" @@ -267,16 +266,7 @@ RTTIType TubeClass::Fetch_RTTI(void) const } -/// -/// Fetches the class identifier of this object. -/// This routine is used by the save game system so that it knows what kind of object to -/// construct when the stream is read back in. -/// -/// Pointer to the place to store the class identifier. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE TubeClass::GetClassID(CLSID * retval) +ClassID TubeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_TubeClass; - return(S_OK); + return(ClassID_TubeClass); } diff --git a/code/tube.h b/code/tube.h index 71e193c44..f511cd589 100644 --- a/code/tube.h +++ b/code/tube.h @@ -22,7 +22,7 @@ class TubeClass : public AbstractClass { typedef AbstractClass BASECLASS; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/tunnel.cpp b/code/tunnel.cpp index aa3fa9ff1..e4727006e 100644 --- a/code/tunnel.cpp +++ b/code/tunnel.cpp @@ -53,7 +53,7 @@ TunnelLocomotionClass::TunnelLocomotionClass(void) : /// Reports whether the unit is anywhere in the dig cycle (State != STATE_IDLE). /// /// True while dig-moving. -boolean STDMETHODCALLTYPE TunnelLocomotionClass::Is_Moving(void) +bool TunnelLocomotionClass::Is_Moving(void) { if (State != STATE_IDLE) { return(true); @@ -67,7 +67,7 @@ boolean STDMETHODCALLTYPE TunnelLocomotionClass::Is_Moving(void) /// (not STATE_IDLE and not STATE_TURNING). /// /// True while actively moving. -boolean STDMETHODCALLTYPE TunnelLocomotionClass::Is_Moving_Now(void) +bool TunnelLocomotionClass::Is_Moving_Now(void) { if (Is_Moving() && State != STATE_TURNING) { return(true); @@ -80,7 +80,7 @@ boolean STDMETHODCALLTYPE TunnelLocomotionClass::Is_Moving_Now(void) /// Returns the burrow destination while moving, or the current position when idle. /// /// The destination coordinate. -Coord STDMETHODCALLTYPE TunnelLocomotionClass::Destination(void) +Coord TunnelLocomotionClass::Destination(void) { if (Is_Moving()) { return(DestinationCoord); @@ -96,7 +96,7 @@ Coord STDMETHODCALLTYPE TunnelLocomotionClass::Destination(void) /// ignores the order altogether. /// /// The location to travel to. -void STDMETHODCALLTYPE TunnelLocomotionClass::Move_To(Coord to) +void TunnelLocomotionClass::Move_To(Coord to) { if (LinkedTo->StunDuration <= 0) { Coord coord = to; @@ -121,7 +121,7 @@ void STDMETHODCALLTYPE TunnelLocomotionClass::Move_To(Coord to) /// already traveling underground must make for the nearest ground it can surface on. If /// there is no such ground to be had, it stays buried for good. /// -void STDMETHODCALLTYPE TunnelLocomotionClass::Stop_Moving(void) +void TunnelLocomotionClass::Stop_Moving(void) { switch (State) { case STATE_ABORTING: @@ -180,7 +180,7 @@ void STDMETHODCALLTYPE TunnelLocomotionClass::Stop_Moving(void) /// the ground and underground layers. /// /// bool; Is the unit still working through its dig cycle? -boolean STDMETHODCALLTYPE TunnelLocomotionClass::Process(void) +bool TunnelLocomotionClass::Process(void) { if (Is_Moving()) { int agl = LinkedTo->HeightAGL; @@ -256,7 +256,7 @@ boolean STDMETHODCALLTYPE TunnelLocomotionClass::Process(void) /// /// Should the buried unit be hidden outright rather than rippled? /// Returns with the visual treatment to draw the unit with. -VisualType STDMETHODCALLTYPE TunnelLocomotionClass::Visual_Character(boolean flag) +VisualType TunnelLocomotionClass::Visual_Character(bool flag) { if (State == STATE_TUNNELING) { if (flag) { @@ -457,7 +457,7 @@ void TunnelLocomotionClass::Process_Emerging(void) /// /// The shape cache key to fold this pose into. May be NULL. /// Returns with the matrix to draw the unit with. -Matrix3D STDMETHODCALLTYPE TunnelLocomotionClass::Draw_Matrix(int * key) +Matrix3D TunnelLocomotionClass::Draw_Matrix(int * key) { if (State == STATE_IDLE) { int ramp = Map[(Coord const &)(LinkedTo->PositionCoord)].Ramp; @@ -528,7 +528,7 @@ Matrix3D STDMETHODCALLTYPE TunnelLocomotionClass::Draw_Matrix(int * key) /// derived from the terrain-height delta and the rotation progress. /// /// The Z pixel adjustment. -int STDMETHODCALLTYPE TunnelLocomotionClass::Z_Adjust(void) +int TunnelLocomotionClass::Z_Adjust(void) { static int tunnel_Z_Adjust[] = {45, 45}; @@ -583,7 +583,7 @@ int STDMETHODCALLTYPE TunnelLocomotionClass::Z_Adjust(void) /// be shaded down its length rather than across the flat, as the base locomotor would. /// /// Returns with the Z gradient to draw the unit with. -ZGradientType STDMETHODCALLTYPE TunnelLocomotionClass::Z_Gradient(void) +ZGradientType TunnelLocomotionClass::Z_Gradient(void) { if (State == STATE_DESCENDING || State == STATE_DIGGING_IN || State == STATE_ABORTING || State == STATE_EMERGING || State == STATE_ASCENDING) { return(ZGRAD_90DEG); @@ -597,7 +597,7 @@ ZGradientType STDMETHODCALLTYPE TunnelLocomotionClass::Z_Gradient(void) /// dig-in, emerging, aborting), false once it is pitched down or underground. /// /// True if it casts a shadow. -boolean STDMETHODCALLTYPE TunnelLocomotionClass::Is_To_Have_Shadow(void) +bool TunnelLocomotionClass::Is_To_Have_Shadow(void) { if (State == STATE_IDLE || State == STATE_TURNING || State == STATE_ABORTING || State == STATE_DIGGING_IN || State == STATE_EMERGING) { return(true); @@ -612,7 +612,7 @@ boolean STDMETHODCALLTYPE TunnelLocomotionClass::Is_To_Have_Shadow(void) /// /// Cell to test. /// MOVE_OK or MOVE_NO. -MoveType STDMETHODCALLTYPE TunnelLocomotionClass::Can_Enter_Cell(Cell cell) +MoveType TunnelLocomotionClass::Can_Enter_Cell(Cell cell) { if (!Debug_Map && !Map[cell].Can_Burrow_Here()) { return(MOVE_NO); @@ -625,25 +625,16 @@ MoveType STDMETHODCALLTYPE TunnelLocomotionClass::Can_Enter_Cell(Cell cell) /// Sets the unit's desired facing (used while turning to face the dig destination). /// /// Desired facing. -void STDMETHODCALLTYPE TunnelLocomotionClass::Do_Turn(DirType coord) +void TunnelLocomotionClass::Do_Turn(DirType coord) { DirType dir = coord; LinkedTo->PrimaryFacing.Set_Desired(dir); } -/// -/// Fetches the class identifier for this locomotor. -/// This routine is part of the COM persistence support. The save system records the -/// identifier so that the right locomotor can be created again when the game is loaded. -/// -/// The location to store the class identifier in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE TunnelLocomotionClass::GetClassID(CLSID * retval) +ClassID TunnelLocomotionClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_TunnelLocomotion; - return(S_OK); + return(ClassID_TunnelLocomotion); } @@ -666,7 +657,7 @@ void TunnelLocomotionClass::Serialize(SaveStreamClass & stream) /// Returns the render layer: underground while travelling (STATE_TUNNELING), ground otherwise. /// /// The render layer. -LayerType STDMETHODCALLTYPE TunnelLocomotionClass::In_Which_Layer(void) +LayerType TunnelLocomotionClass::In_Which_Layer(void) { if (State != STATE_TUNNELING) { return(LAYER_GROUND); @@ -681,7 +672,7 @@ LayerType STDMETHODCALLTYPE TunnelLocomotionClass::In_Which_Layer(void) /// subterranean unit has no shot while it is lining up, digging, or under the ground. /// /// Returns with the fire error, or FIRE_OK if the unit is free to shoot. -FireErrorType STDMETHODCALLTYPE TunnelLocomotionClass::Can_Fire(void) +FireErrorType TunnelLocomotionClass::Can_Fire(void) { FireErrorType fire = BASECLASS::Can_Fire(); @@ -697,7 +688,7 @@ FireErrorType STDMETHODCALLTYPE TunnelLocomotionClass::Can_Fire(void) /// Reports whether the unit is in the act of surfacing (ascending or emerging). /// /// True while surfacing. -boolean STDMETHODCALLTYPE TunnelLocomotionClass::Is_Surfacing(void) +bool TunnelLocomotionClass::Is_Surfacing(void) { return(State == STATE_ASCENDING || State == STATE_EMERGING); } diff --git a/code/tunnel.h b/code/tunnel.h index 2aaf0ad0a..03ae0794f 100644 --- a/code/tunnel.h +++ b/code/tunnel.h @@ -29,26 +29,26 @@ class TunnelLocomotionClass : public LocomotionClass */ TunnelLocomotionClass(void); - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; - virtual boolean STDMETHODCALLTYPE Is_Moving(void) override; - virtual boolean STDMETHODCALLTYPE Is_Moving_Now(void) override; - virtual Coord STDMETHODCALLTYPE Destination(void) override; - virtual void STDMETHODCALLTYPE Move_To(Coord to) override; - virtual void STDMETHODCALLTYPE Stop_Moving(void) override; - virtual boolean STDMETHODCALLTYPE Process(void) override; - virtual VisualType STDMETHODCALLTYPE Visual_Character(boolean flag) override; - virtual Matrix3D STDMETHODCALLTYPE Draw_Matrix(int *key) override; - virtual int STDMETHODCALLTYPE Z_Adjust(void) override; - virtual ZGradientType STDMETHODCALLTYPE Z_Gradient(void) override; - virtual boolean STDMETHODCALLTYPE Is_To_Have_Shadow(void) override; - virtual MoveType STDMETHODCALLTYPE Can_Enter_Cell(Cell cell) override; - virtual void STDMETHODCALLTYPE Do_Turn(DirType coord) override; - virtual LayerType STDMETHODCALLTYPE In_Which_Layer(void) override; - virtual FireErrorType STDMETHODCALLTYPE Can_Fire(void) override; - virtual boolean STDMETHODCALLTYPE Is_Surfacing(void) override; + virtual bool Is_Moving(void) override; + virtual bool Is_Moving_Now(void) override; + virtual Coord Destination(void) override; + virtual void Move_To(Coord to) override; + virtual void Stop_Moving(void) override; + virtual bool Process(void) override; + virtual VisualType Visual_Character(bool flag) override; + virtual Matrix3D Draw_Matrix(int *key) override; + virtual int Z_Adjust(void) override; + virtual ZGradientType Z_Gradient(void) override; + virtual bool Is_To_Have_Shadow(void) override; + virtual MoveType Can_Enter_Cell(Cell cell) override; + virtual void Do_Turn(DirType coord) override; + virtual LayerType In_Which_Layer(void) override; + virtual FireErrorType Can_Fire(void) override; + virtual bool Is_Surfacing(void) override; void Process_Turning(void); void Process_Digging_In(void); diff --git a/code/typelist.h b/code/typelist.h index f1572a0a1..4dcafb564 100644 --- a/code/typelist.h +++ b/code/typelist.h @@ -19,7 +19,6 @@ #include "win.h" #include -#include template class TypeList : public DynamicVectorClass diff --git a/code/ui/uiabort.cpp b/code/ui/uiabort.cpp new file mode 100644 index 000000000..d1fe666e7 --- /dev/null +++ b/code/ui/uiabort.cpp @@ -0,0 +1,138 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The abort and surrender screen. Behavior traced out of goptions.cpp. +// +// Two things the dialog decided rather than the template: the middle button is relabelled +// a surrender for anything but a solo mission, and it is disabled for a player whose fate +// is already settled, so a defeated player is offered no surrender. Only the surrender +// caption comes from the string table; the restart caption stays the template's, which is +// where its translation lives, so the view-model asks for an override rather than naming +// both. + +#include "always.h" + +#include "uiabort.h" + +#include "uirmlview.h" + +#include "data.h" +#include "house.h" +#include "language/language.h" +#include "session.h" + +#include +#include +#include + + +void UIAbortPresenterClass::Refresh(void) +{ + if (Session.Type == GAME_NORMAL) { + RestartCaption.clear(); + CanRestart = true; + } else { + RestartCaption = Fetch_String(TXT_SURRENDER); + CanRestart = !(PlayerPtr->IsDefeated || PlayerPtr->IsToWin || PlayerPtr->IsToLose || PlayerPtr->IsToDie); + } +} + + +void UIAbortPresenterClass::Execute(UIIntent const & intent) +{ + UIResult result; + + if (intent.Action == UI_ABORT_QUIT) { + Choice = CHOICE_QUIT; + result.Outcome = UIResult::OUTCOME_ACCEPTED; + } else if (intent.Action == UI_ABORT_RESTART) { + Choice = CHOICE_RESTART; + result.Outcome = UIResult::OUTCOME_ACCEPTED; + } else if (intent.Action == UI_ABORT_CANCEL) { + Choice = CHOICE_CANCEL; + result.Outcome = UIResult::OUTCOME_CANCELLED; + } else { + return; + } + + Result = result; +} + + +//--------------------------------------------------------------------------------------- +// The RmlUi view. +//--------------------------------------------------------------------------------------- + +/// +/// The RmlUi half of the abort and surrender screen. +/// +class AbortViewClass : public UIRmlViewClass +{ + public: + AbortViewClass(UIAbortPresenterClass & presenter) : + UIRmlViewClass(presenter, "abort.rml"), + Screen(presenter) + { + } + + virtual void Bind(Rml::DataModelConstructor & model) override; + virtual void Sync(void) override {} + + private: + UIAbortPresenterClass & Screen; + + // The middle button's caption. The document carries the restart wording the + // template carried, and this replaces it only where the screen asks. + Rml::String RestartCaption; +}; + + +void AbortViewClass::Bind(Rml::DataModelConstructor & model) +{ + RestartCaption = Screen.RestartCaption.empty() ? "Restart" : Rml::String(Screen.RestartCaption); + + model.Bind("restartcaption", &RestartCaption); + model.Bind("canrestart", &Screen.CanRestart); + + model.BindEventCallback("press", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{arguments[0].Get(), "", 0}); + }); + + // Escape carries on playing, which is the IDCANCEL the dialog answered with its cancel + // arm. Enter does the same: the template names no default push button, so Windows sent + // IDOK, and the dialog had no arm for it. + model.BindEventCallback("key", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + int const key = event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN); + if (key == Rml::Input::KI_ESCAPE) { + Screen.Queue(UIIntent{UI_ABORT_CANCEL, "", 0}); + } + }); +} + + +/// +/// Shows the abort box and waits for the player to choose. +/// +UIResult UI_Abort_Screen(UIAbortPresenterClass & presenter) +{ + AbortViewClass view(presenter); + + if (!view.Prepare(true)) { + UIResult result; + result.Outcome = UIResult::OUTCOME_FAILED_TO_OPEN; + return(result); + } + + UIResult const result = UI_Run_Modal(presenter, view); + view.Close(); + return(result); +} diff --git a/code/ui/uiabort.h b/code/ui/uiabort.h new file mode 100644 index 000000000..b5fc34480 --- /dev/null +++ b/code/ui/uiabort.h @@ -0,0 +1,60 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The abort and surrender screen's behavior, with no toolkit in it. One screen serves both: +// the middle choice is a restart in a solo mission and a surrender in a session, which is +// what IDD_MISSION_ABORT's own procedure decided at WM_INITDIALOG. +// +// docs/UI_DESIGN.md, "Screens", owns the contract. + +#pragma once + +#include "uiscreen.h" + +#include + + +inline constexpr char const * UI_ABORT_QUIT = "quit"; +inline constexpr char const * UI_ABORT_RESTART = "restart"; +inline constexpr char const * UI_ABORT_CANCEL = "cancel"; + + +class UIAbortPresenterClass : public UIPresenterClass +{ + public: + enum ChoiceType { + CHOICE_NONE, + CHOICE_QUIT, + CHOICE_RESTART, + CHOICE_CANCEL, + }; + + virtual void Execute(UIIntent const & intent) override; + virtual void Refresh(void) override; + + /* + ** The view-model. Plain values, and the only thing a view reads. + */ + + // What the middle button should be relabelled to, or empty to leave the caption the + // view already carries. Only the surrender overrides it; the restart is the caption + // the template holds, and the template is the localized resource. + std::string RestartCaption; + + // Is the middle choice offered at all? A player whose fate is already settled cannot + // surrender, which is what the dialog disabled the button for. + bool CanRestart = true; + + ChoiceType Choice = CHOICE_NONE; +}; + + +// Shows the screen through its RmlUi view. FAILED_TO_OPEN leaves nothing shown and the +// caller falls through to the legacy dialog. +UIResult UI_Abort_Screen(UIAbortPresenterClass & presenter); diff --git a/code/ui/uicampaign.cpp b/code/ui/uicampaign.cpp new file mode 100644 index 000000000..5f8fa053c --- /dev/null +++ b/code/ui/uicampaign.cpp @@ -0,0 +1,272 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The campaign choice screen. Behavior traced out of Choose_Campaign and +// Campaign_Choice_Dialog_Proc in init.cpp, including the availability test that decided +// which campaigns were listed at all. +// +// What the extraction fixes in place: a row carries the campaign it stands for rather than +// its position, because the list skips a campaign the player cannot reach; the difficulty +// is written to the settings only on accept, which is where the dialog read the slider +// back; and the difficulty caption starts as the template's own, because the dialog set it +// only when the slider moved. +// +// docs/UI_DESIGN.md, "Screens", owns the contracts this keeps to. + +#include "always.h" + +#include "uicampaign.h" + +#include "uirmlview.h" + +#include "addon.h" +#include "campaign.h" +#include "data.h" +#include "dbgprint.h" +#include "campaign.hh" +#include "gamedlg.h" +#include "globals.h" +#include "goptions.h" +#include "init.h" +#include "language/language.h" +#include "vector.h" + +#include +#include +#include + + +// A base game campaign is offered only when no addon is running, and an addon's own +// campaign only when that particular addon is running. Moved here from init.cpp, where it +// existed for this screen alone. +static bool Campaign_Available(CampaignClass * campaign) +{ + if (Addon_Enabled(ADDON_ANY) == true) { + if (campaign->RequiredAddon == ADDON_BASE_GAME) { + return(false); + } + if (Addon_Enabled((AddonType)campaign->RequiredAddon)) { + return(true); + } + return(false); + } + + if (campaign->RequiredAddon == ADDON_BASE_GAME) { + return(true); + } + + return(false); +} + + +void UICampaignPresenterClass::Refresh(void) +{ + Campaigns.clear(); + Selected = -1; + + for (int index = 0; index < ::Campaigns.Count(); index++) { + CampaignClass * const campaign = ::Campaigns[index]; + + if (!Campaign_Available(campaign)) { + DebugString("\tSkipping Campaign [%d] - %s\n", index, campaign->Description); + continue; + } + + DebugString("\tAdding Campaign [%d] - %s\n", index, campaign->Description); + + EntryType entry; + entry.Label = campaign->Description; + entry.Campaign = index; + Campaigns.push_back(entry); + } + + // The dialog selected the first row it had listed. + if (!Campaigns.empty()) { + Selected = 0; + } + + Difficulty = Options.Difficulty; + if (Difficulty < 0) Difficulty = 0; + if (Difficulty >= DIFFICULTY_STEPS) Difficulty = DIFFICULTY_STEPS - 1; + + // The template's own caption, which is what the dialog left showing until the slider + // was moved. + DifficultyLabel = "Harder"; +} + + +/// +/// The maintenance the dialog driver ran on every pass of its own loop. +/// +void UICampaignPresenterClass::Service(void) +{ + Title_Screen_Restore(); +} + + +int UICampaignPresenterClass::Chosen(void) const +{ + if (Choice != CHOICE_ACCEPT) { + return(CAMPAIGN_NONE); + } + if (Selected < 0 || Selected >= (int)Campaigns.size()) { + return(CAMPAIGN_NONE); + } + return(Campaigns[Selected].Campaign); +} + + +void UICampaignPresenterClass::Execute(UIIntent const & intent) +{ + if (intent.Action == UI_CAMPAIGN_SELECT) { + if (intent.Value >= 0 && intent.Value < (int)Campaigns.size()) { + Selected = intent.Value; + } + return; + } + + if (intent.Action == UI_CAMPAIGN_DIFFICULTY) { + if (intent.Value >= 0 && intent.Value < DIFFICULTY_STEPS) { + Difficulty = intent.Value; + DifficultyLabel = Fetch_String(GameDifficultyNames[Difficulty]); + } + return; + } + + UIResult result; + + if (intent.Action == UI_CAMPAIGN_ACCEPT) { + Options.Difficulty = Difficulty; + Choice = CHOICE_ACCEPT; + result.Outcome = UIResult::OUTCOME_ACCEPTED; + + } else if (intent.Action == UI_CAMPAIGN_CANCEL) { + Choice = CHOICE_CANCEL; + result.Outcome = UIResult::OUTCOME_CANCELLED; + + } else { + return; + } + + result.Value = Chosen(); + Result = result; +} + + +//--------------------------------------------------------------------------------------- +// The RmlUi view. +//--------------------------------------------------------------------------------------- + +/// +/// The RmlUi half of the campaign choice screen. +/// +class CampaignViewClass : public UIRmlViewClass +{ + public: + CampaignViewClass(UICampaignPresenterClass & presenter) : + UIRmlViewClass(presenter, "campaign.rml"), + Screen(presenter) + { + } + + virtual void Bind(Rml::DataModelConstructor & model) override; + virtual void Sync(void) override; + + // The track bar takes its position from the model as the document loads, and that + // raises a change event of its own. Nothing is queued until this is set. + void Settle(void) { Settled = true; } + + private: + UICampaignPresenterClass & Screen; + bool Settled = false; +}; + + +void CampaignViewClass::Bind(Rml::DataModelConstructor & model) +{ + if (auto entry = model.RegisterStruct()) { + entry.RegisterMember("label", &UICampaignPresenterClass::EntryType::Label); + } + model.RegisterArray>(); + + model.Bind("campaigns", &Screen.Campaigns); + model.Bind("selected", &Screen.Selected); + model.Bind("difficulty", &Screen.Difficulty); + model.Bind("difficultyname", &Screen.DifficultyLabel); + + model.BindEventCallback("pick", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{UI_CAMPAIGN_SELECT, "", (int)arguments[0].Get()}); + }); + + // The track bar is bound one way and a position the screen already holds raises no + // intent, so setting it from the model cannot move a difficulty the player did not. + // The dialog read its slider back at accept because a keyboard or page move raised no + // thumb notification; RmlUi raises a change for every move, so there is nothing left to + // read back. + model.BindEventCallback("slide", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + if (!Settled) return; + + int const step = (int)(event.GetParameter("value", 0.0f) + 0.5f); + if (step == Screen.Difficulty) return; + + Screen.Queue(UIIntent{UI_CAMPAIGN_DIFFICULTY, "", step}); + }); + + model.BindEventCallback("press", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{arguments[0].Get(), "", 0}); + }); + + // Escape cancels and Enter accepts, which is what IsDialogMessage delivered to a dialog + // whose template names no default push button. + model.BindEventCallback("key", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + int const key = event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN); + if (key == Rml::Input::KI_ESCAPE) { + Screen.Queue(UIIntent{UI_CAMPAIGN_CANCEL, "", 0}); + } else if (key == Rml::Input::KI_RETURN || key == Rml::Input::KI_NUMPADENTER) { + Screen.Queue(UIIntent{UI_CAMPAIGN_ACCEPT, "", 0}); + } + }); +} + + +void CampaignViewClass::Sync(void) +{ + if (!Model) return; + + Model.DirtyVariable("campaigns"); + Model.DirtyVariable("selected"); + Model.DirtyVariable("difficultyname"); +} + + +/// +/// Shows the campaign list and waits for the player to choose. +/// +UIResult UI_Campaign_Screen(UICampaignPresenterClass & presenter) +{ + CampaignViewClass view(presenter); + + if (!view.Prepare(true)) { + UIResult result; + result.Outcome = UIResult::OUTCOME_FAILED_TO_OPEN; + return(result); + } + + view.Settle(); + + UIResult const result = UI_Run_Modal(presenter, view); + view.Close(); + return(result); +} diff --git a/code/ui/uicampaign.h b/code/ui/uicampaign.h new file mode 100644 index 000000000..81a014c92 --- /dev/null +++ b/code/ui/uicampaign.h @@ -0,0 +1,73 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The campaign choice screen's behavior, with no toolkit in it. A row carries the campaign +// it stands for rather than its position, because the list skips a campaign the player +// cannot reach and a row number then means nothing on its own. +// +// docs/UI_DESIGN.md, "Screens", owns the contract. + +#pragma once + +#include "uiscreen.h" + +#include +#include + + +inline constexpr char const * UI_CAMPAIGN_SELECT = "select"; // Value: row +inline constexpr char const * UI_CAMPAIGN_DIFFICULTY = "difficulty"; // Value: slider step +inline constexpr char const * UI_CAMPAIGN_ACCEPT = "accept"; +inline constexpr char const * UI_CAMPAIGN_CANCEL = "cancel"; + + +class UICampaignPresenterClass : public UIPresenterClass +{ + public: + // The three positions the difficulty track bar was given. + enum { DIFFICULTY_STEPS = 3 }; + + struct EntryType + { + std::string Label; + int Campaign = 0; + }; + + enum ChoiceType { + CHOICE_NONE, + CHOICE_ACCEPT, + CHOICE_CANCEL, + }; + + virtual void Execute(UIIntent const & intent) override; + virtual void Refresh(void) override; + virtual void Service(void) override; + + // The campaign the player settled on, or the none campaign when they backed out. + int Chosen(void) const; + + /* + ** The view-model. Plain values, and the only thing a view reads. + */ + std::vector Campaigns; + int Selected = -1; + + int Difficulty = 0; + + // What the difficulty caption reads. The dialog left the template's own caption + // showing until the slider was moved, so that caption is where this starts. + std::string DifficultyLabel; + + ChoiceType Choice = CHOICE_NONE; +}; + + +// Shows the screen through its RmlUi view. FAILED_TO_OPEN leaves nothing shown and the +// caller falls through to the legacy dialog. +UIResult UI_Campaign_Screen(UICampaignPresenterClass & presenter); diff --git a/code/ui/uidesync.cpp b/code/ui/uidesync.cpp new file mode 100644 index 000000000..59d1e2e53 --- /dev/null +++ b/code/ui/uidesync.cpp @@ -0,0 +1,624 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#include "hostclock.h" +#include "always.h" + +#include "uidesync.h" + +#include "uirmlview.h" +#include "uishell.h" + +#include "chat.h" +#include "dbgprint.h" +#include "house.h" +#include "ipxmgr.h" +#include "conquer.h" +#include "data.h" +#include "language/language.h" +#include "loaddlg.h" +#include "mpload.h" +#include "msglist.h" +#include "netdlg.h" +#include "netglobal.h" +#include "savemgr.h" +#include "session.h" +#include "syncreport.h" + +#include +#include + +#include +#include + + +static UIDesyncPresenterClass * _DesyncScreen = NULL; + + +UIDesyncPresenterClass * UI_Desync_Screen(void) +{ + return(_DesyncScreen); +} + + +void UI_Set_Desync_Screen(UIDesyncPresenterClass * screen) +{ + _DesyncScreen = screen; +} + + +/// +/// Records the seats, the timers and the standing the screen opens with. +/// +void UIDesyncPresenterClass::Open(void) +{ + IsMaster = Session.Am_I_Master(); + OpenedAt = Monotonic_Milliseconds(); + State.Begin(OpenedAt); + + ContinueReceived = false; + CountdownActive = false; + LastCountdownSecond = -1; + PromptPending = false; + Outcome = OUTCOME_CONTINUE; + Messages.clear(); + + // The master decides and everyone else waits, so only the master's screen carries the + // two decisions; the wait screen's quit comes back after the delay. + CanContinue = IsMaster; + CanLoad = IsMaster && SaveManager.Multiplayer_Load_Is_Allowed() && MultiplayerLoadOptionsClass().Files_Present(); + CanQuit = IsMaster; + + Build_Player_Rows(); +} + + +void UIDesyncPresenterClass::Execute(UIIntent const & intent) +{ + if (intent.Action == UI_DESYNC_LOAD) { + if (!CanLoad || CountdownActive) return; + PromptPending = true; + return; + } + + if (intent.Action == UI_DESYNC_CONTINUE) { + if (!CanContinue || CountdownActive) return; + Send_Continue(); + Answer(OUTCOME_CONTINUE); + return; + } + + if (intent.Action == UI_DESYNC_QUIT) { + if (!CanQuit) return; + Answer(OUTCOME_QUIT); + return; + } + + if (intent.Action == UI_DESYNC_SAY) { + Say(intent.Identity); + return; + } +} + + +void UIDesyncPresenterClass::Refresh(void) +{ + Build_Player_Rows(); +} + + +/// +/// The maintenance the dialog's own loop ran on every pass: heartbeats out, silent seats +/// dropped, the master's decision taken, and the countdown moved. +/// +void UIDesyncPresenterClass::Service(void) +{ + std::int64_t const now = Monotonic_Milliseconds(); + + if (State.Heartbeat_Is_Due(now)) { + Send_Heartbeat(); + State.Heartbeat_Sent(now); + } + Check_Timeouts(); + + // A waiting player's quit comes back once the stall has lasted long enough to be worth + // abandoning, which is what the disabled button stood for. + if (!IsMaster && !CanQuit && now - OpenedAt >= DesyncClass::QUIT_DELAY_MS) { + CanQuit = true; + } + + if (!CountdownActive && SaveManager.MultiplayerLoad.Is_Pending()) { + Start_Countdown(); + } + + if (CountdownActive) { + Update_Countdown(); + if (SaveManager.MultiplayerLoad.Is_Due(now)) { + Answer(OUTCOME_LOAD); + } + return; + } + + if (ContinueReceived) { + Answer(OUTCOME_CONTINUE); + } +} + + +/// +/// Runs the multiplayer save browser with this screen out of the way, the way the dialog +/// disabled itself around the same prompt. +/// +void UIDesyncPresenterClass::Run_Pending(void) +{ + if (!PromptPending) { + return; + } + + PromptPending = false; + SaveManager.Multiplayer_Load_Prompt(); +} + + +void UIDesyncPresenterClass::Record_Chat(char const * name, char const * text) +{ + char buffer[MAX_MESSAGE_LENGTH + MAX_MESSAGE_PREFIX]; + std::snprintf(buffer, sizeof(buffer), "%s: %s", name, text); + Append_Chat_Line(buffer); +} + + +void UIDesyncPresenterClass::Player_Left(int house, char const * name) +{ + State.Mark_Left(house, name); + + if (name != NULL && name[0] != '\0') { + char buffer[128]; + std::snprintf(buffer, sizeof(buffer), Fetch_String(TXT_LEFT_GAME), name); + Append_Chat_Line(buffer); + } + + Build_Player_Rows(); + Master_Changed(); +} + + +void UIDesyncPresenterClass::Master_Decided_To_Continue(void) +{ + DebugString("The master chose to continue without the players out of sync\n"); + ContinueReceived = true; +} + + +void UIDesyncPresenterClass::Heartbeat_Heard(int house) +{ + State.Heard(house, Monotonic_Milliseconds()); +} + + +/// +/// Takes up the master's decisions once this machine has become master, unless a load is +/// already counting down, when there is nothing left to decide. +/// +void UIDesyncPresenterClass::Master_Changed(void) +{ + Build_Player_Rows(); + + if (IsMaster || CountdownActive || !Session.Am_I_Master()) { + return; + } + + DebugString("This machine is the new master; it makes the decision now\n"); + IsMaster = true; + CanContinue = true; + CanQuit = true; + CanLoad = SaveManager.Multiplayer_Load_Is_Allowed() && MultiplayerLoadOptionsClass().Files_Present(); +} + + +void UIDesyncPresenterClass::Build_Player_Rows(void) +{ + Players.clear(); + + int const master = Session.Master_Player_ID(); + + for (int house = 0; house < MAX_PLAYERS && house < Houses.Count(); house++) { + HouseClass const * housep = Houses[house]; + bool const left = State.Has_Left(house); + + // A player who left stays listed, though their seat is no longer human. + if (housep == NULL || (!housep->IsHuman && !left)) { + continue; + } + + PlayerRowType row; + + // The roster entry is gone by now, so the kept name is the only copy while the list + // rebuilds. + row.Name = left && State.Left_Name(house)[0] != '\0' ? State.Left_Name(house) : housep->IniName.c_str(); + row.IsHost = house == master; + + if (left) { + row.Status = STATUS_LEFT; + } else if (Sync_Is_Out_Of_Sync(house)) { + row.Status = STATUS_OUT_OF_SYNC; + } + + Players.push_back(row); + } + + PlayersChanged = true; +} + + +void UIDesyncPresenterClass::Answer(OutcomeType outcome) +{ + Outcome = outcome; + + // A screen answers its driver with a result as well as an outcome, because the runner + // returns on a result. + UIResult result; + result.Outcome = outcome == OUTCOME_QUIT ? UIResult::OUTCOME_CANCELLED : UIResult::OUTCOME_ACCEPTED; + result.Value = (int)outcome; + Result = result; +} + + +void UIDesyncPresenterClass::Say(std::string const & text) +{ + if (text.empty()) { + return; + } + + char buffer[MAX_MESSAGE_LENGTH]; + std::snprintf(buffer, sizeof(buffer), "%s", text.c_str()); + + Session.MessageScope = ChatScopeType::Everyone; + Session.MessageAddress = IPXAddressClass(); + Chat_Send(buffer); +} + + +void UIDesyncPresenterClass::Append_Chat_Line(char const * line) +{ + Messages.emplace_back(line); + if ((int)Messages.size() > MESSAGE_LIMIT) { + Messages.erase(Messages.begin()); + } + MessagesChanged = true; +} + + +void UIDesyncPresenterClass::Send_Heartbeat(void) +{ + if (PlayerPtr == NULL || Session.Players.Count() == 0) { + return; + } + + GlobalPacketType packet; + NetGlobal::Initialize_Packet(packet, NET_DESYNC_HEARTBEAT); + std::snprintf(packet.Name, sizeof(packet.Name), "%s", Session.Players[0]->Name); + + for (int index = 1; index < Session.Players.Count(); index++) { + Ipx.Send_Global_Message(&packet, sizeof(packet), 0, &Session.Players[index]->Address); + } + Ipx.Service(); +} + + +void UIDesyncPresenterClass::Send_Continue(void) +{ + DebugString("Telling every seat to continue without the players out of sync\n"); + + GlobalPacketType packet; + NetGlobal::Initialize_Packet(packet, NET_DESYNC_CONTINUE); + std::snprintf(packet.Name, sizeof(packet.Name), "%s", Session.Players[0]->Name); + + for (int index = 1; index < Session.Players.Count(); index++) { + Ipx.Send_Global_Message(&packet, sizeof(packet), 1, &Session.Players[index]->Address); + Ipx.Service(); + } +} + + +/// +/// Drops the seats that have fallen silent, so a machine that died without a sign-off +/// neither holds up the decision nor lingers in the seats a later load reconciles. +/// +void UIDesyncPresenterClass::Check_Timeouts(void) +{ + std::int64_t const now = Monotonic_Milliseconds(); + + for (int index = Session.Players.Count() - 1; index >= 1; index--) { + int const house = Session.Players[index]->Player.ID; + if (!State.Is_Silent(house, now)) { + continue; + } + + DebugString("No heartbeat from %s (house %d) for %d seconds; dropping the seat\n", + Session.Players[index]->Name, house, (int)(DesyncClass::HEARTBEAT_TIMEOUT_MS / 1000)); + + std::string const name = Session.Players[index]->Name; + Destroy_Connection(house, 1); + Player_Left(house, name.c_str()); + } +} + + +void UIDesyncPresenterClass::Start_Countdown(void) +{ + DebugString("Counting down to the multiplayer load\n"); + + CountdownActive = true; + LastCountdownSecond = -1; + CountdownTotal = (int)MultiplayerLoadClass::COUNTDOWN_MS; + + Append_Chat_Line(Fetch_String(TXT_LOADING_SAVED_GAME)); + + // Nothing is left to decide once the load is scheduled, which is what disabling both + // buttons stood for. + CanLoad = false; + CanContinue = false; + + Update_Countdown(); +} + + +void UIDesyncPresenterClass::Update_Countdown(void) +{ + if (!CountdownActive || !SaveManager.MultiplayerLoad.Is_Pending()) { + return; + } + + std::int64_t const now = Monotonic_Milliseconds(); + CountdownRemaining = std::clamp((int)SaveManager.MultiplayerLoad.Milliseconds_Left(now), 0, CountdownTotal); + + int const seconds = SaveManager.MultiplayerLoad.Seconds_Left(now); + if (seconds == LastCountdownSecond) { + return; + } + LastCountdownSecond = seconds; + + char buffer[128]; + std::snprintf(buffer, sizeof(buffer), + Fetch_String(seconds == 1 ? TXT_LOADING_IN_SECOND : TXT_LOADING_IN_SECONDS), seconds); + CountdownText = buffer; +} + + +/* +** The RmlUi view. The master's screen and the wait screen are the same document family: +** they differ by which of the three buttons exist and by the block of prose beside the +** list, so each carries its own document and its own model name. +*/ +namespace { + + // A seat as the document shows it, with the status text and its color resolved here + // rather than in the presenter. + struct PlayerViewType + { + std::string Name; + std::string Status; + std::string Hex; + std::string Mark; + }; + + + class DesyncViewClass : public UIRmlViewClass + { + public: + DesyncViewClass(UIDesyncPresenterClass & presenter, char const * document) + : UIRmlViewClass(presenter, document), Screen(presenter) {} + + virtual void Bind(Rml::DataModelConstructor & model) override; + virtual void Sync(void) override; + + private: + void Rebuild_Rows(void); + + UIDesyncPresenterClass & Screen; + + std::vector PlayerRows; + + // How much of the countdown bar is left, as a percentage, because a document + // states a width rather than drawing a rectangle, and the color the elapsed + // time gives it. + std::string BarWidth = "0%"; + std::string BarHex = "#00c800"; + }; + + + void DesyncViewClass::Rebuild_Rows(void) + { + PlayerRows.clear(); + + for (UIDesyncPresenterClass::PlayerRowType const & row : Screen.Players) { + PlayerViewType view; + view.Name = row.Name; + + switch (row.Status) { + case UIDesyncPresenterClass::STATUS_LEFT: + view.Status = Fetch_String(TXT_SYNC_STATUS_LEFT); + view.Hex = "#c80000"; + break; + + case UIDesyncPresenterClass::STATUS_OUT_OF_SYNC: + view.Status = Fetch_String(TXT_SYNC_STATUS_OUT); + view.Hex = "#c8c800"; + break; + + default: + view.Status = Fetch_String(TXT_OK); + view.Hex = "#00c800"; + break; + } + + // The master marker, which the list drew as the wolhost.pcx surface. PCX + // decoding is not here yet, so the marker is a character in the same column. + view.Mark = row.IsHost ? "*" : ""; + + PlayerRows.push_back(view); + } + + int const total = Screen.CountdownTotal > 0 ? Screen.CountdownTotal : 1; + int const remaining = std::clamp(Screen.CountdownRemaining, 0, total); + char percent[16]; + std::snprintf(percent, sizeof(percent), "%d%%", remaining * 100 / total); + BarWidth = percent; + + // Green to yellow to red as the load nears, which is what Draw_Countdown_Bar chose + // from the elapsed fraction. + int const elapsed = total - remaining; + BarHex = "#00c800"; + if (elapsed > total * 2 / 5) { + BarHex = elapsed > total * 4 / 5 ? "#c80000" : "#c8c800"; + } + } + + + void DesyncViewClass::Bind(Rml::DataModelConstructor & model) + { + Rebuild_Rows(); + + if (auto row = model.RegisterStruct()) { + row.RegisterMember("name", &PlayerViewType::Name); + row.RegisterMember("status", &PlayerViewType::Status); + row.RegisterMember("hex", &PlayerViewType::Hex); + row.RegisterMember("mark", &PlayerViewType::Mark); + } + model.RegisterArray>(); + model.RegisterArray>(); + + model.Bind("players", &PlayerRows); + model.Bind("messages", &Screen.Messages); + + model.Bind("canload", &Screen.CanLoad); + model.Bind("cancontinue", &Screen.CanContinue); + model.Bind("canquit", &Screen.CanQuit); + + model.Bind("countdown", &Screen.CountdownActive); + model.Bind("countdowntext", &Screen.CountdownText); + model.Bind("barwidth", &BarWidth); + model.Bind("barhex", &BarHex); + + model.BindEventCallback("press", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + + Rml::String const action = arguments[0].Get(); + if (action == UI_DESYNC_LOAD) Screen.Queue(UIIntent{UI_DESYNC_LOAD, "", 0}); + else if (action == UI_DESYNC_CONTINUE) Screen.Queue(UIIntent{UI_DESYNC_CONTINUE, "", 0}); + else if (action == UI_DESYNC_QUIT) Screen.Queue(UIIntent{UI_DESYNC_QUIT, "", 0}); + }); + + // Enter in the chat field sends the line, which is what the dialog's IDOK arm did, + // since it had no default button. + model.BindEventCallback("submit", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + int const key = event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN); + if (key != Rml::Input::KI_RETURN && key != Rml::Input::KI_NUMPADENTER) { + return; + } + + Rml::Element * const field = Element != nullptr ? Element->GetElementById("say") : nullptr; + if (field == nullptr) return; + + Rml::String const text = field->GetAttribute("value", Rml::String()); + field->SetAttribute("value", Rml::String()); + Screen.Queue(UIIntent{UI_DESYNC_SAY, text, 0}); + event.StopPropagation(); + }); + } + + + void DesyncViewClass::Sync(void) + { + if (!Model) return; + + Rebuild_Rows(); + + Model.DirtyVariable("players"); + Model.DirtyVariable("messages"); + Model.DirtyVariable("canload"); + Model.DirtyVariable("cancontinue"); + Model.DirtyVariable("canquit"); + Model.DirtyVariable("countdown"); + Model.DirtyVariable("countdowntext"); + Model.DirtyVariable("barwidth"); + Model.DirtyVariable("barhex"); + + Screen.PlayersChanged = false; + Screen.MessagesChanged = false; + } + + + DesyncViewClass * _View = NULL; + +} // namespace + + +void UI_Desync_Close_View(void) +{ + delete _View; + _View = NULL; +} + + +/// +/// Shows the variant this machine gets and runs it until the decision is made. +/// +UIResult UI_Desync_Run(UIDesyncPresenterClass & presenter) +{ + UIResult failed; + failed.Outcome = UIResult::OUTCOME_FAILED_TO_OPEN; + + // The master's screen replaces the wait screen when this machine is promoted, so the + // document is released rather than kept when the variant moves. + static bool shown_as_master = false; + if (_View != NULL && shown_as_master != presenter.IsMaster) { + UI_Desync_Close_View(); + } + + if (_View == NULL) { + shown_as_master = presenter.IsMaster; + + DesyncViewClass * const view = new DesyncViewClass(presenter, + presenter.IsMaster ? "desynchost.rml" : "desyncwait.rml"); + if (!view->Prepare(true)) { + delete view; + return(failed); + } + + _View = view; + } + + // A family reopened in a loop resets the close mark and the held result, since a close + // marks the presenter closing and a marked presenter drains nothing. + presenter.Result.reset(); + presenter.IsClosing = false; + presenter.Running = presenter.IsMaster ? 1 : 0; + _View->Sync(); + + while (!presenter.Result.has_value()) { + UI_Run_Modal(presenter, *_View); + + if (!presenter.PromptPending) { + break; + } + + // The save browser draws where this screen is, so the document steps aside for it. + _View->Hide(); + presenter.Run_Pending(); + _View->Show(); + _View->Sync(); + } + + presenter.Running = -1; + return(presenter.Result.value_or(UIResult{})); +} diff --git a/code/ui/uidesync.h b/code/ui/uidesync.h new file mode 100644 index 000000000..26ca4db8a --- /dev/null +++ b/code/ui/uidesync.h @@ -0,0 +1,165 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The out-of-sync screen's behavior, with no toolkit in it. The master's decision screen and +// the wait screen everyone else gets are one screen family sharing one model, because they +// differ by which controls exist rather than by what the screen does. +// +// docs/UI_DESIGN.md, "Screens", owns the contract. + +#pragma once + +#include "uiscreen.h" + +#include "desync.h" + +#include +#include +#include + + +inline constexpr char const * UI_DESYNC_LOAD = "load"; +inline constexpr char const * UI_DESYNC_CONTINUE = "continue"; +inline constexpr char const * UI_DESYNC_QUIT = "quit"; +inline constexpr char const * UI_DESYNC_SAY = "say"; + + +class UIDesyncPresenterClass : public UIPresenterClass +{ + public: + // What the screen answers its driver with. These stand where the dialog's own control + // identifiers stood, so a presenter names no control. + enum OutcomeType { + OUTCOME_CONTINUE, + OUTCOME_LOAD, + OUTCOME_QUIT, + }; + + // A seat's standing, which the list drew as colored text in its own column. + enum StatusType { + STATUS_OK, + STATUS_OUT_OF_SYNC, + STATUS_LEFT, + }; + + // A seat and what is known about it. The name is carried rather than the house, + // because a house the computer takes over is renamed and the list would lose it. + struct PlayerRowType + { + std::string Name; + StatusType Status = STATUS_OK; + bool IsHost = false; + }; + + virtual void Execute(UIIntent const & intent) override; + virtual void Refresh(void) override; + virtual void Service(void) override; + + // The multiplayer save browser draws where this screen is, so this screen is stepped + // aside for it rather than run underneath. The family also steps aside when this + // machine is promoted to master, because the runner picks its document once and the + // promotion arrives from inside the service. + virtual bool Suspends(void) const override + { + return(PromptPending || (Running >= 0 && (Running != 0) != IsMaster)); + } + + // Which variant the runner is holding a document open for, which the runner sets and + // clears around itself: -1 for none, 0 for the wait screen, 1 for the master's. + int Running = -1; + + // The rosters and the timers the screen opens with. Called once, before a view is + // prepared, because the wait screen's quit delay is measured from here. + void Open(void); + + // Runs the multiplayer save browser with this screen out of the way. Called by the + // owner between passes, never from an event. + void Run_Pending(void); + + // Records a line of chat for whatever is showing the screen, in the form the dialog + // composed it. + void Record_Chat(char const * name, char const * text); + + // A seat has gone. The name is kept because the roster entry is dropped with it. + void Player_Left(int house, char const * name); + + void Master_Decided_To_Continue(void); + void Heartbeat_Heard(int house); + void Master_Changed(void); + + // Reads the session's seats into the view-model. The master marker is recorded with + // the row rather than while painting it, because it is a fact about the seat. + void Build_Player_Rows(void); + + /* + ** The view-model. + */ + + // Is this machine the master? The master decides and everyone else waits, which is + // what the two templates stood for. + bool IsMaster = false; + + std::vector Players; + + // The chat backlog, kept whole here and wrapped by whatever shows it. + std::vector Messages; + + // The most lines the model keeps, which is what the chat list box was capped at. + enum { MESSAGE_LIMIT = 50 }; + + bool CanLoad = false; + bool CanContinue = false; + + // A waiting player may not quit at once: the dialog left its quit button disabled + // for the first ten seconds so a brief stall is not abandoned by reflex. + bool CanQuit = false; + + bool CountdownActive = false; + std::string CountdownText; + + // How much of the countdown is left, out of MultiplayerLoadClass::COUNTDOWN_MS, which + // is what the shrinking bar was drawn from. + int CountdownRemaining = 0; + int CountdownTotal = 0; + + bool PlayersChanged = false; + bool MessagesChanged = false; + + // Has the save browser been asked for? The owner runs it between passes. + bool PromptPending = false; + + OutcomeType Outcome = OUTCOME_CONTINUE; + + private: + void Answer(OutcomeType outcome); + void Say(std::string const & text); + void Append_Chat_Line(char const * line); + void Send_Heartbeat(void); + void Send_Continue(void); + void Check_Timeouts(void); + void Start_Countdown(void); + void Update_Countdown(void); + + DesyncClass State; + std::int64_t OpenedAt = 0; + bool ContinueReceived = false; + int LastCountdownSecond = -1; +}; + + +// The out-of-sync screen the driver is running, or NULL when none is up. The network code +// reaches the model through this wherever a change is produced away from a screen. +UIDesyncPresenterClass * UI_Desync_Screen(void); +void UI_Set_Desync_Screen(UIDesyncPresenterClass * screen); + + +// Shows the variant the screen says it is, and runs it until the decision is made. The +// document is released when the screen closes, because the screen is not come back to. +UIResult UI_Desync_Run(UIDesyncPresenterClass & presenter); +void UI_Desync_Close_View(void); diff --git a/code/ui/uidev.cpp b/code/ui/uidev.cpp new file mode 100644 index 000000000..6124f4d0f --- /dev/null +++ b/code/ui/uidev.cpp @@ -0,0 +1,165 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The ImGui context and the developer overlays it draws. Tool visibility and frame rate +// never touch deterministic state, and the overlays are armed by a developer key rather +// than by anything a player can reach. +// +// docs/UI_DESIGN.md, "Dear ImGui", owns what belongs here. + +#include "always.h" + +#include "uiinternal.h" + +#include "dbgprint.h" + +#include + + +static ImGuiContext * _Context = nullptr; +static bool _Open = false; +static bool _FrameStarted = false; + + +bool UI_Dev_Init(void) +{ + if (_Context != nullptr) { + return(true); + } + + IMGUI_CHECKVERSION(); + _Context = ImGui::CreateContext(); + if (_Context == nullptr) { + return(false); + } + + ImGuiIO & io = ImGui::GetIO(); + + // The renderer creates and destroys textures on request through the draw data rather + // than from a font atlas it owns, which is the pinned version's backend contract. + io.BackendFlags |= ImGuiBackendFlags_RendererHasTextures; + io.BackendPlatformName = "OpenTS UI shell"; + io.BackendRendererName = "OpenTS bgfx overlay"; + + // Nothing is written beside the executable: the engine keeps its own settings. + io.IniFilename = nullptr; + io.LogFilename = nullptr; + + return(true); +} + + +void UI_Dev_Shutdown(void) +{ + if (_Context == nullptr) { + return; + } + + ImGui::DestroyContext(_Context); + _Context = nullptr; + _Open = false; + _FrameStarted = false; +} + + +bool UI_Dev_Is_Open(void) +{ + return(_Context != nullptr && _Open); +} + + +void UI_Dev_Toggle(void) +{ + if (_Context == nullptr) { + return; + } + + _Open = !_Open; + DebugString("[UI] Developer overlay %s.\n", _Open ? "shown" : "hidden"); +} + + +/// +/// Builds the overlay's frame. Called from the shell's tick, on wall-clock time. +/// +void UI_Dev_New_Frame(int width, int height, double deltaseconds) +{ + if (_Context == nullptr || !_Open || width <= 0 || height <= 0) { + return; + } + + ImGuiIO & io = ImGui::GetIO(); + io.DisplaySize = ImVec2((float)width, (float)height); + io.DeltaTime = deltaseconds > 0.0 ? (float)deltaseconds : (1.0f / 60.0f); + + ImGui::NewFrame(); + _FrameStarted = true; + + ImGui::SetNextWindowPos(ImVec2(16.0f, 16.0f), ImGuiCond_FirstUseEver); + if (ImGui::Begin("OpenTS")) { + ImGui::Text("Overlay %d x %d", width, height); + ImGui::Text("%.1f frames per second", io.Framerate); + } + ImGui::End(); + + ImGui::Render(); +} + + +void UI_Dev_Render(void) +{ + if (_Context == nullptr || !_FrameStarted) { + return; + } + + UI_Render_ImGui(ImGui::GetDrawData()); + _FrameStarted = false; +} + + +bool UI_Dev_Wants_Mouse(void) +{ + return(_Context != nullptr && _Open && ImGui::GetIO().WantCaptureMouse); +} + + +bool UI_Dev_Wants_Keyboard(void) +{ + return(_Context != nullptr && _Open && ImGui::GetIO().WantCaptureKeyboard); +} + + +void UI_Dev_Mouse_Position(float x, float y) +{ + if (_Context == nullptr || !_Open) { + return; + } + + ImGui::GetIO().AddMousePosEvent(x, y); +} + + +void UI_Dev_Mouse_Button(int button, bool down) +{ + if (_Context == nullptr || !_Open || button < 0 || button > 4) { + return; + } + + ImGui::GetIO().AddMouseButtonEvent(button, down); +} + + +void UI_Dev_Mouse_Wheel(float delta) +{ + if (_Context == nullptr || !_Open) { + return; + } + + ImGui::GetIO().AddMouseWheelEvent(0.0f, delta); +} diff --git a/code/ui/uidisplayconfirm.cpp b/code/ui/uidisplayconfirm.cpp new file mode 100644 index 000000000..2bee96c78 --- /dev/null +++ b/code/ui/uidisplayconfirm.cpp @@ -0,0 +1,168 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The display mode confirmation. Behavior traced out of Test_Display_Mode_Dialog and +// Test_Display_Mode_Dialog_Proc in mainopt.cpp. +// +// The timeout was expressed there as a posted WM_COMMAND carrying WM_DESTROY, which is 2, +// which the procedure recorded because it accepted any identifier from one to IDCANCEL, and +// IDCANCEL is also 2. So the timeout was a cancel spelled awkwardly, and it is a cancel +// here. The driver re-armed its timer to five seconds after firing because the posted +// message took another pass to arrive; this produces the result directly, and re-arms for +// the same reason: a caller that keeps servicing must not be handed the answer twice. +// +// docs/UI_DESIGN.md, "Screens", owns the contracts this keeps to. + +#include "always.h" + +#include "uidisplayconfirm.h" + +#include "uirmlview.h" + +#include +#include +#include + + +void UIDisplayConfirmPresenterClass::Refresh(void) +{ + Choice = CHOICE_NONE; + Timer = TIMEOUT_SECONDS * TIMER_SECOND; +} + + +int UIDisplayConfirmPresenterClass::Seconds_Remaining(void) const +{ + int const ticks = (int)Timer.Value(); + if (ticks <= 0) { + return(0); + } + return((ticks + TIMER_SECOND - 1) / TIMER_SECOND); +} + + +/// +/// Takes the mode back when the player says nothing. +/// +void UIDisplayConfirmPresenterClass::Service(void) +{ + if (Result.has_value()) { + return; + } + + if (Timer <= 0) { + Timer = REARM_SECONDS * TIMER_SECOND; + + Choice = CHOICE_CANCEL; + + UIResult result; + result.Outcome = UIResult::OUTCOME_CANCELLED; + Result = result; + } +} + + +void UIDisplayConfirmPresenterClass::Execute(UIIntent const & intent) +{ + UIResult result; + + if (intent.Action == UI_MODECONFIRM_ACCEPT) { + Choice = CHOICE_ACCEPT; + result.Outcome = UIResult::OUTCOME_ACCEPTED; + } else if (intent.Action == UI_MODECONFIRM_CANCEL) { + Choice = CHOICE_CANCEL; + result.Outcome = UIResult::OUTCOME_CANCELLED; + } else { + return; + } + + Result = result; +} + + +//--------------------------------------------------------------------------------------- +// The RmlUi view. +//--------------------------------------------------------------------------------------- + +/// +/// The RmlUi half of the display mode confirmation. +/// +class DisplayConfirmViewClass : public UIRmlViewClass +{ + public: + DisplayConfirmViewClass(UIDisplayConfirmPresenterClass & presenter) : + UIRmlViewClass(presenter, "modeconfirm.rml"), + Screen(presenter) + { + } + + virtual void Bind(Rml::DataModelConstructor & model) override; + virtual void Sync(void) override; + + private: + UIDisplayConfirmPresenterClass & Screen; + int Remaining = 0; +}; + + +void DisplayConfirmViewClass::Bind(Rml::DataModelConstructor & model) +{ + Remaining = Screen.Seconds_Remaining(); + model.Bind("seconds", &Remaining); + + model.BindEventCallback("press", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{arguments[0].Get(), "", 0}); + }); + + // Escape refuses the mode, as does saying nothing. Enter keeps it, because the template + // names no default push button and Windows then sent the dialog IDOK, which its + // procedure recorded and its driver compared against IDOK. + model.BindEventCallback("key", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + int const key = event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN); + if (key == Rml::Input::KI_ESCAPE) { + Screen.Queue(UIIntent{UI_MODECONFIRM_CANCEL, "", 0}); + } else if (key == Rml::Input::KI_RETURN || key == Rml::Input::KI_NUMPADENTER) { + Screen.Queue(UIIntent{UI_MODECONFIRM_ACCEPT, "", 0}); + } + }); +} + + +void DisplayConfirmViewClass::Sync(void) +{ + if (!Model) return; + + int const remaining = Screen.Seconds_Remaining(); + if (remaining != Remaining) { + Remaining = remaining; + Model.DirtyVariable("seconds"); + } +} + + +/// +/// Shows the mode confirmation and waits for the player, or for the timeout. +/// +UIResult UI_Display_Confirm_Screen(UIDisplayConfirmPresenterClass & presenter) +{ + DisplayConfirmViewClass view(presenter); + + if (!view.Prepare(true)) { + UIResult result; + result.Outcome = UIResult::OUTCOME_FAILED_TO_OPEN; + return(result); + } + + UIResult const result = UI_Run_Modal(presenter, view); + view.Close(); + return(result); +} diff --git a/code/ui/uidisplayconfirm.h b/code/ui/uidisplayconfirm.h new file mode 100644 index 000000000..d0cd12818 --- /dev/null +++ b/code/ui/uidisplayconfirm.h @@ -0,0 +1,63 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The display mode confirmation's behavior, with no toolkit in it. The screen exists to +// take back a resolution the player cannot see, so its timeout is the whole point of it and +// belongs here rather than in a view: a mode that leaves the screen unreadable is answered +// by saying nothing, and saying nothing has to mean no. +// +// docs/UI_DESIGN.md, "Screens", owns the contract. + +#pragma once + +#include "uiscreen.h" + +#include "stimer.h" +#include "timer.h" + + +inline constexpr char const * UI_MODECONFIRM_ACCEPT = "accept"; +inline constexpr char const * UI_MODECONFIRM_CANCEL = "cancel"; + + +class UIDisplayConfirmPresenterClass : public UIPresenterClass +{ + public: + enum { + // What the dialog driver's own CDTimerClass was set to, and what it re-armed to + // after firing. + TIMEOUT_SECONDS = 10, + REARM_SECONDS = 5, + }; + + enum ChoiceType { + CHOICE_NONE, + CHOICE_ACCEPT, + CHOICE_CANCEL, + }; + + virtual void Execute(UIIntent const & intent) override; + virtual void Refresh(void) override; + virtual void Service(void) override; + + // Whole seconds left before the mode is taken back, counted down for a view that + // wants to show them. Nothing depends on the number; the rollback is driven by the + // timer itself. + int Seconds_Remaining(void) const; + + ChoiceType Choice = CHOICE_NONE; + + private: + CDTimerClass Timer; +}; + + +// Shows the screen through its RmlUi view. FAILED_TO_OPEN leaves nothing shown and the +// caller falls through to the legacy dialog. +UIResult UI_Display_Confirm_Screen(UIDisplayConfirmPresenterClass & presenter); diff --git a/code/ui/uidisplayoptions.cpp b/code/ui/uidisplayoptions.cpp new file mode 100644 index 000000000..9a14fc59e --- /dev/null +++ b/code/ui/uidisplayoptions.cpp @@ -0,0 +1,243 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The display options screen. Behavior traced out of Display_Options_Dialog_Body in +// mainopt.cpp. +// +// What the extraction fixes in place: the resolution is staged and only a trial the player +// confirms writes it to the settings, while the movie stretching and full screen +// preferences are written straight to the settings at accept and left alone at cancel; and the staged resolution +// moves only when the player leaves the screen on a row other than the one it opened on, so +// re-picking the row already in force stages nothing and skips the trial. +// +// EnumDisplayModes reports nothing on a platform without host mode enumeration, and this +// screen then offers an empty list, which is what the dialog did with the same answer. +// +// docs/UI_DESIGN.md, "Screens", owns the contracts this keeps to. + +#include "always.h" + +#include "uidisplayoptions.h" + +#include "uirmlview.h" + +#include "globals.h" +#include "init.h" +#include "goptions.h" +#include "options.h" +#include "video.h" + +#include + +#include +#include +#include + + +void UIDisplayOptionsPresenterClass::Refresh(void) +{ + Modes.clear(); + Selected = -1; + + StagedWidth = Options.ScreenWidth; + StagedHeight = Options.ScreenHeight; + StretchMovies = Options.StretchMovies; + Fullscreen = Options.Fullscreen; + + int * const modes = EnumDisplayModes(MIN_WIDTH, MIN_HEIGHT, MAX_WIDTH, MAX_HEIGHT); + if (modes != NULL) { + for (int * mode = modes; *mode != 0; mode += 2) { + ModeType entry; + entry.Width = mode[0]; + entry.Height = mode[1]; + + char buffer[64]; + std::snprintf(buffer, sizeof(buffer), "%d x %d", entry.Width, entry.Height); + entry.Label = buffer; + + if (entry.Width == StagedWidth && entry.Height == StagedHeight) { + Selected = (int)Modes.size(); + } + + Modes.push_back(entry); + } + delete [] modes; + } + + Opened = Selected; +} + + +/// +/// The maintenance the dialog driver ran on every pass of its own loop. +/// +void UIDisplayOptionsPresenterClass::Service(void) +{ + Title_Screen_Restore(); +} + + +bool UIDisplayOptionsPresenterClass::Wants_Mode_Change(void) const +{ + return(StagedWidth != Options.ScreenWidth || StagedHeight != Options.ScreenHeight); +} + + +void UIDisplayOptionsPresenterClass::Commit(void) +{ + Options.ScreenWidth = StagedWidth; + Options.ScreenHeight = StagedHeight; +} + + +void UIDisplayOptionsPresenterClass::Execute(UIIntent const & intent) +{ + if (intent.Action == UI_DISPLAY_SELECT) { + if (intent.Value >= 0 && intent.Value < (int)Modes.size()) { + Selected = intent.Value; + } + return; + } + + if (intent.Action == UI_DISPLAY_STRETCH) { + StretchMovies = (intent.Value != 0); + return; + } + + if (intent.Action == UI_DISPLAY_FULLSCREEN) { + Fullscreen = (intent.Value != 0); + return; + } + + UIResult result; + + if (intent.Action == UI_DISPLAY_ACCEPT) { + if (Selected != Opened && Selected >= 0 && Selected < (int)Modes.size()) { + StagedWidth = Modes[Selected].Width; + StagedHeight = Modes[Selected].Height; + } + + // The stretching preference is not staged. The dialog wrote it at IDOK and left it + // alone at IDCANCEL, so it survives a resolution the player then refuses. + Options.StretchMovies = StretchMovies; + + Choice = CHOICE_ACCEPT; + result.Outcome = UIResult::OUTCOME_ACCEPTED; + + } else if (intent.Action == UI_DISPLAY_CANCEL) { + Choice = CHOICE_CANCEL; + result.Outcome = UIResult::OUTCOME_CANCELLED; + + } else { + return; + } + + Result = result; +} + + +//--------------------------------------------------------------------------------------- +// The RmlUi view. +//--------------------------------------------------------------------------------------- + +/// +/// The RmlUi half of the display options screen. +/// +class DisplayOptionsViewClass : public UIRmlViewClass +{ + public: + DisplayOptionsViewClass(UIDisplayOptionsPresenterClass & presenter) : + UIRmlViewClass(presenter, "display.rml"), + Screen(presenter) + { + } + + virtual void Bind(Rml::DataModelConstructor & model) override; + virtual void Sync(void) override; + + private: + UIDisplayOptionsPresenterClass & Screen; +}; + + +void DisplayOptionsViewClass::Bind(Rml::DataModelConstructor & model) +{ + if (auto mode = model.RegisterStruct()) { + mode.RegisterMember("label", &UIDisplayOptionsPresenterClass::ModeType::Label); + } + model.RegisterArray>(); + + model.Bind("modes", &Screen.Modes); + model.Bind("selected", &Screen.Selected); + model.Bind("stretch", &Screen.StretchMovies); + model.Bind("fullscreen", &Screen.Fullscreen); + + model.BindEventCallback("pick", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{UI_DISPLAY_SELECT, "", (int)arguments[0].Get()}); + }); + + model.BindEventCallback("toggle", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const &) { + Screen.Queue(UIIntent{UI_DISPLAY_STRETCH, "", Screen.StretchMovies ? 0 : 1}); + }); + + model.BindEventCallback("togglefull", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const &) { + Screen.Queue(UIIntent{UI_DISPLAY_FULLSCREEN, "", Screen.Fullscreen ? 0 : 1}); + }); + + model.BindEventCallback("press", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{arguments[0].Get(), "", 0}); + }); + + // Escape cancels and Enter accepts, which is what IsDialogMessage delivered to a dialog + // whose template names no default push button. + model.BindEventCallback("key", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + int const key = event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN); + if (key == Rml::Input::KI_ESCAPE) { + Screen.Queue(UIIntent{UI_DISPLAY_CANCEL, "", 0}); + } else if (key == Rml::Input::KI_RETURN || key == Rml::Input::KI_NUMPADENTER) { + Screen.Queue(UIIntent{UI_DISPLAY_ACCEPT, "", 0}); + } + }); +} + + +void DisplayOptionsViewClass::Sync(void) +{ + if (!Model) return; + + Model.DirtyVariable("selected"); + Model.DirtyVariable("stretch"); + Model.DirtyVariable("fullscreen"); +} + + +/// +/// Shows the display options and waits for the player to leave them. +/// +UIResult UI_Display_Options_Screen(UIDisplayOptionsPresenterClass & presenter) +{ + DisplayOptionsViewClass view(presenter); + + if (!view.Prepare(true)) { + UIResult result; + result.Outcome = UIResult::OUTCOME_FAILED_TO_OPEN; + return(result); + } + + UIResult const result = UI_Run_Modal(presenter, view); + view.Close(); + return(result); +} diff --git a/code/ui/uidisplayoptions.h b/code/ui/uidisplayoptions.h new file mode 100644 index 000000000..73b82e310 --- /dev/null +++ b/code/ui/uidisplayoptions.h @@ -0,0 +1,90 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The display options screen's behavior, with no toolkit in it. The resolution it settles +// on is staged rather than applied, because a mode is tried and confirmed before the +// settings remember it; the file-scope TempOptions copy the dialog used for that staging +// lives here now. +// +// docs/UI_DESIGN.md, "Screens", owns the contract. + +#pragma once + +#include "uiscreen.h" + +#include +#include + + +inline constexpr char const * UI_DISPLAY_SELECT = "select"; // Value: row +inline constexpr char const * UI_DISPLAY_STRETCH = "stretch"; // Value: check state +inline constexpr char const * UI_DISPLAY_FULLSCREEN = "fullscreen"; // Value: check state +inline constexpr char const * UI_DISPLAY_ACCEPT = "accept"; +inline constexpr char const * UI_DISPLAY_CANCEL = "cancel"; + + +class UIDisplayOptionsPresenterClass : public UIPresenterClass +{ + public: + // The bounds the dialog asked the display for. + enum { + MIN_WIDTH = 640, + MIN_HEIGHT = 400, + MAX_WIDTH = 4096, + MAX_HEIGHT = 4096, + }; + + struct ModeType + { + std::string Label; + int Width = 0; + int Height = 0; + }; + + enum ChoiceType { + CHOICE_NONE, + CHOICE_ACCEPT, + CHOICE_CANCEL, + }; + + virtual void Execute(UIIntent const & intent) override; + virtual void Refresh(void) override; + virtual void Service(void) override; + + // Is a resolution staged that the game is not already running at? Only one that is + // gets tried, and only a tried one is ever written to the settings. + bool Wants_Mode_Change(void) const; + + // Writes the staged resolution into the settings, once its trial was accepted. + void Commit(void); + + /* + ** The view-model. Plain values, and the only thing a view reads. + */ + std::vector Modes; + int Selected = -1; + bool StretchMovies = false; + bool Fullscreen = false; + + // The resolution the screen is staging. It starts at the one in force and moves + // only when the player accepts a row other than the one the screen opened on, which + // is what the dialog's own previous-against-current comparison amounted to. + int StagedWidth = 0; + int StagedHeight = 0; + + ChoiceType Choice = CHOICE_NONE; + + private: + int Opened = -1; +}; + + +// Shows the screen through its RmlUi view. FAILED_TO_OPEN leaves nothing shown and the +// caller falls through to the legacy dialog. +UIResult UI_Display_Options_Screen(UIDisplayOptionsPresenterClass & presenter); diff --git a/code/ui/uifile.cpp b/code/ui/uifile.cpp new file mode 100644 index 000000000..a28182c46 --- /dev/null +++ b/code/ui/uifile.cpp @@ -0,0 +1,163 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// RmlUi's files, read through the game's own file system so that a document, a style, an +// image or a font loads from a loose ui/ directory or from a mix on equal terms. +// +// Every reference is reduced to its bare name before it is opened. RmlUi joins a relative +// reference with the path of the document that named it, and CDFileClass treats a name +// carrying a directory as a literal path that skips the search order, so a joined name +// would only ever be found on disk. The name alone is the lookup key: the user path, then +// the current directory, then the search paths, then the mix files. That is what lets a +// mod override a document by shipping it earlier in that order. +// +// docs/UI_DESIGN.md, "Assets and strings", owns this. + +#include "always.h" + +#include "uiinternal.h" + +#include "ccfile.h" +#include "dbgprint.h" + +#include + +#include +#include + + +/// +/// Strips every directory a reference carries, leaving the name the archives hold. +/// +static std::string Base_Name(std::string const & path) +{ + std::size_t const mark = path.find_last_of("\\/:"); + return(mark == std::string::npos ? path : path.substr(mark + 1)); +} + + +class UIFileInterface : public Rml::FileInterface +{ + public: + virtual Rml::FileHandle Open(const Rml::String & path) override + { + std::string const name = Base_Name(path); + if (name.empty()) { + return(0); + } + + CCFileClass * file = new CCFileClass(name.c_str()); + if (!file->Is_Available() || !file->Open(FileClass::READ)) { + delete file; + DebugString("[UI] File %s was not found.\n", name.c_str()); + return(0); + } + + return((Rml::FileHandle)file); + } + + virtual void Close(Rml::FileHandle handle) override + { + CCFileClass * file = (CCFileClass *)handle; + if (file != nullptr) { + file->Close(); + delete file; + } + } + + virtual size_t Read(void * buffer, size_t size, Rml::FileHandle handle) override + { + CCFileClass * file = (CCFileClass *)handle; + if (file == nullptr || buffer == nullptr || size == 0) { + return(0); + } + + // The engine counts bytes in an int, so a request larger than that is served in + // pieces rather than truncated silently. + size_t total = 0; + while (total < size) { + size_t const remaining = size - total; + int const chunk = remaining > (size_t)INT_MAX ? INT_MAX : (int)remaining; + + int const read = file->Read((char *)buffer + total, chunk); + if (read <= 0) { + break; + } + + total += (size_t)read; + if (read < chunk) { + break; + } + } + + return(total); + } + + virtual bool Seek(Rml::FileHandle handle, long offset, int origin) override + { + CCFileClass * file = (CCFileClass *)handle; + if (file == nullptr) { + return(false); + } + + int const size = file->Size(); + long target = offset; + + switch (origin) { + case SEEK_CUR: + target = (long)file->Seek(0, SEEK_CUR) + offset; + break; + + case SEEK_END: + target = (long)size + offset; + break; + + default: + break; + } + + // Seek() clamps, so a request past either end would otherwise report success at + // a position the caller never asked for. + if (target < 0 || target > (long)size) { + return(false); + } + + return(file->Seek((int)target, SEEK_SET) == target); + } + + virtual size_t Tell(Rml::FileHandle handle) override + { + CCFileClass * file = (CCFileClass *)handle; + if (file == nullptr) { + return(0); + } + + int const position = file->Seek(0, SEEK_CUR); + return(position < 0 ? 0 : (size_t)position); + } + + virtual size_t Length(Rml::FileHandle handle) override + { + CCFileClass * file = (CCFileClass *)handle; + if (file == nullptr) { + return(0); + } + + int const size = file->Size(); + return(size < 0 ? 0 : (size_t)size); + } +}; + +static UIFileInterface _FileInterface; + + +Rml::FileInterface * UI_File_Interface(void) +{ + return(&_FileInterface); +} diff --git a/code/ui/uifont.cpp b/code/ui/uifont.cpp new file mode 100644 index 000000000..038ffa842 --- /dev/null +++ b/code/ui/uifont.cpp @@ -0,0 +1,454 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// Draws document text with the game's own dlgsys bitmap sheets. +// +// The dialogs never drew their buttons, captions, tabs, check boxes, combo boxes or track +// bar numbers with a scalable face. They drew from a pair of 640x160 sheets, dlgsysi.pcx +// and dlgsysa.pcx, laid out as 16 by 16 cells of 40 by 10 pixels: the first holds palette +// indices, the second the coverage of each pixel. drawhelp.cpp owns both the metrics, which +// are probed out of the ink rather than tabulated anywhere, and the palette shift that +// turns the sheet's own ramp into the colour text is asked for. This file only turns what +// drawhelp hands over into a texture and a quad per glyph. +// +// RmlUi installs one font engine, and the list boxes and tooltips legitimately want the +// shipped TrueType face, so this derives from RmlUi's own engine and answers only for the +// dlgsys family, delegating everything else to it untouched. A face handle of ours is +// recognised by the registry below; anything else is the base engine's and is passed +// straight through. +// +// A document selects the sheets with `font-family: dlgsys`. Its `font-size` is read as the +// height of a glyph cell, so `font-size: 10dp` draws one sheet pixel per authored pixel and +// the text scales with the frame exactly as the artwork around it does. + +#include "always.h" + +#include "uiinternal.h" + +#include "dbgprint.h" +#include "drawhelp.h" +#include "utf8.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + + +// The family a document names to get the sheets, and the base name the sheets are stored +// under. drawhelp appends the "i" and "a" suffixes itself. +static char const _FontFamily[] = "dlgsys"; + + +// One remapped copy of the glyph sheet. The sheet is drawn at its own resolution and scaled +// by the quads, so a colour needs one texture whatever size a document asks for. +struct UIFontSheet +{ + Rml::CallbackTextureSource Source; + int Width = 0; + int Height = 0; +}; + + +// One size of the bitmap face. Everything here is derived from the sheet's own ink. +class UIFontFaceClass +{ + public: + explicit UIFontFaceClass(int size, ODFontMetrics const & metrics); + + int Advance(unsigned char glyph) const { return(_Advance[glyph]); } + int Width(char const * text, std::size_t length) const; + + Rml::FontMetrics const & Metrics(void) const { return(_Metrics); } + int Size(void) const { return(_Size); } + + float Scale(void) const { return(_Scale); } + int CellWidth(void) const { return(_Sheet.leftMargin + _Sheet.glyphWidth); } + int CellHeight(void) const { return(_Sheet.topMargin + _Sheet.glyphHeight); } + int TopMargin(void) const { return(_Sheet.topMargin); } + + private: + ODFontMetrics _Sheet; + Rml::FontMetrics _Metrics = {}; + int _Size = 0; + float _Scale = 1.0f; + int _Advance[256] = {}; +}; + + +UIFontFaceClass::UIFontFaceClass(int size, ODFontMetrics const & metrics) : + _Sheet(metrics), + _Size(size) +{ + int cell_height = _Sheet.topMargin + _Sheet.glyphHeight; + if (cell_height <= 0) { + cell_height = 1; + } + + _Scale = (float)size / (float)cell_height; + if (_Scale <= 0.0f) { + _Scale = 1.0f; + } + + for (int i = 0; i < 256; ++i) { + _Advance[i] = (int)std::lround((double)_Sheet.charWidths[i] * _Scale); + } + + // The sheets give inked extents, not typographic ones. The whole glyph sits above the + // baseline with nothing below it, so a line box centres the ink on itself: RmlUi puts + // the baseline half the leading below the top, which places the ink block exactly where + // OD_DRAW_CHAR_FLAG_VERTICAL_CENTER placed it. + _Metrics.size = size; + _Metrics.ascent = (float)_Sheet.glyphHeight * _Scale; + _Metrics.descent = 0.0f; + _Metrics.line_spacing = (float)cell_height * _Scale; + _Metrics.x_height = _Metrics.ascent * 0.5f; + _Metrics.underline_position = 0.0f; + _Metrics.underline_thickness = std::max(1.0f, _Scale); + _Metrics.has_ellipsis = false; +} + + +int UIFontFaceClass::Width(char const * text, std::size_t length) const +{ + int width = 0; + char const * cursor = text; + char const * end = text + length; + + while (cursor < end) { + char const * before = cursor; + char32_t code = UTF8::Decode(cursor); + if (cursor <= before) { + break; + } + width += _Advance[OD_Font_Glyph(code)]; + } + + return(width); +} + + +static std::map> _Faces; +static std::map _Sheets; +static bool _MetricsRead = false; +static bool _MetricsUsable = false; +static ODFontMetrics _SheetMetrics = {}; +static int _Version = 1; + + +/// +/// Measures the sheets once and remembers whether they could be read at all. +/// The sheets live in a mix file, so the first document shown before the mixes are mounted +/// would find nothing; a later attempt is not made, and the family falls back to whatever +/// the document lists after it. +/// +static bool Sheet_Metrics(ODFontMetrics & metrics) +{ + if (!_MetricsRead) { + _MetricsRead = true; + _MetricsUsable = OD_Font_Metrics(_FontFamily, _SheetMetrics); + + if (_MetricsUsable) { + DebugString("[UI] %s cells %dx%d, ink %dx%d, margins %d,%d.\n", _FontFamily, + _SheetMetrics.leftMargin + _SheetMetrics.glyphWidth, + _SheetMetrics.topMargin + _SheetMetrics.glyphHeight, + _SheetMetrics.glyphWidth, _SheetMetrics.glyphHeight, + _SheetMetrics.leftMargin, _SheetMetrics.topMargin); + } else { + DebugString("[UI] The %s font sheets could not be read.\n", _FontFamily); + } + } + + if (!_MetricsUsable) { + return(false); + } + + metrics = _SheetMetrics; + return(true); +} + + +/// +/// Returns the glyph sheet remapped to one colour, building and uploading it on first use. +/// +static Rml::Texture Sheet_Texture(Rml::RenderManager & manager, unsigned int color, int & width, int & height) +{ + auto found = _Sheets.find(color); + if (found == _Sheets.end()) { + int sheet_width = 0; + int sheet_height = 0; + std::vector pixels; + + if (!OD_Font_Sheet(_FontFamily, (COLORREF)color, sheet_width, sheet_height, pixels)) { + return(Rml::Texture()); + } + + UIFontSheet sheet; + sheet.Width = sheet_width; + sheet.Height = sheet_height; + sheet.Source = Rml::CallbackTextureSource( + [pixels = std::move(pixels), sheet_width, sheet_height](Rml::CallbackTextureInterface const & texture) -> bool { + return(texture.GenerateTexture( + Rml::Span(pixels.data(), pixels.size()), + Rml::Vector2i(sheet_width, sheet_height))); + }); + + found = _Sheets.emplace(color, std::move(sheet)).first; + } + + width = found->second.Width; + height = found->second.Height; + return(found->second.Source.GetTexture(manager)); +} + + +/// +/// Recovers the colour a caller asked for from the premultiplied one RmlUi hands over. +/// +static unsigned int Unpremultiplied_Color(Rml::ColourbPremultiplied colour) +{ + int alpha = colour.alpha; + int red = colour.red; + int green = colour.green; + int blue = colour.blue; + + if (alpha > 0 && alpha < 255) { + red = std::min(255, red * 255 / alpha); + green = std::min(255, green * 255 / alpha); + blue = std::min(255, blue * 255 / alpha); + } + + return((unsigned int)(red | (green << 8) | (blue << 16))); +} + + +// The bitmap engine answers for one family and hands everything else to RmlUi's own engine +// untouched, so a document that asks for the shipped TrueType face is unaffected. +class UIFontEngineClass : public Rml::FontEngineInterfaceDefault +{ + public: + void Shutdown(void) override; + Rml::FontFaceHandle GetFontFaceHandle(Rml::String const & family, Rml::Style::FontStyle style, + Rml::Style::FontWeight weight, int size) override; + Rml::FontEffectsHandle PrepareFontEffects(Rml::FontFaceHandle handle, Rml::FontEffectList const & effects) override; + Rml::FontMetrics const & GetFontMetrics(Rml::FontFaceHandle handle) override; + int GetStringWidth(Rml::FontFaceHandle handle, Rml::StringView string, + Rml::TextShapingContext const & shaping, Rml::Character prior) override; + int GenerateString(Rml::RenderManager & manager, Rml::FontFaceHandle handle, Rml::FontEffectsHandle effects, + Rml::StringView string, Rml::Vector2f position, Rml::ColourbPremultiplied colour, float opacity, + Rml::TextShapingContext const & shaping, Rml::TexturedMeshList & mesh_list) override; + int GetVersion(Rml::FontFaceHandle handle) override; + void ReleaseFontResources(void) override; +}; + + +static UIFontFaceClass * Bitmap_Face(Rml::FontFaceHandle handle) +{ + for (auto const & entry : _Faces) { + if ((Rml::FontFaceHandle)entry.second.get() == handle) { + return(entry.second.get()); + } + } + return(nullptr); +} + + +Rml::FontFaceHandle UIFontEngineClass::GetFontFaceHandle(Rml::String const & family, Rml::Style::FontStyle style, + Rml::Style::FontWeight weight, int size) +{ + if (Rml::StringUtilities::ToLower(family) != _FontFamily) { + return(Rml::FontEngineInterfaceDefault::GetFontFaceHandle(family, style, weight, size)); + } + + ODFontMetrics metrics; + if (size <= 0 || !Sheet_Metrics(metrics)) { + return(0); + } + + auto found = _Faces.find(size); + if (found == _Faces.end()) { + found = _Faces.emplace(size, std::make_unique(size, metrics)).first; + } + + return((Rml::FontFaceHandle)found->second.get()); +} + + +Rml::FontEffectsHandle UIFontEngineClass::PrepareFontEffects(Rml::FontFaceHandle handle, Rml::FontEffectList const & effects) +{ + if (Bitmap_Face(handle) != nullptr) { + return(0); + } + return(Rml::FontEngineInterfaceDefault::PrepareFontEffects(handle, effects)); +} + + +Rml::FontMetrics const & UIFontEngineClass::GetFontMetrics(Rml::FontFaceHandle handle) +{ + UIFontFaceClass const * face = Bitmap_Face(handle); + if (face != nullptr) { + return(face->Metrics()); + } + return(Rml::FontEngineInterfaceDefault::GetFontMetrics(handle)); +} + + +int UIFontEngineClass::GetStringWidth(Rml::FontFaceHandle handle, Rml::StringView string, + Rml::TextShapingContext const & shaping, Rml::Character prior) +{ + UIFontFaceClass const * face = Bitmap_Face(handle); + if (face == nullptr) { + return(Rml::FontEngineInterfaceDefault::GetStringWidth(handle, string, shaping, prior)); + } + + int width = face->Width(string.begin(), string.size()); + width += (int)std::lround((double)shaping.letter_spacing) * (int)string.size(); + return(width); +} + + +int UIFontEngineClass::GenerateString(Rml::RenderManager & manager, Rml::FontFaceHandle handle, + Rml::FontEffectsHandle effects, Rml::StringView string, Rml::Vector2f position, + Rml::ColourbPremultiplied colour, float opacity, Rml::TextShapingContext const & shaping, + Rml::TexturedMeshList & mesh_list) +{ + UIFontFaceClass const * face = Bitmap_Face(handle); + if (face == nullptr) { + return(Rml::FontEngineInterfaceDefault::GenerateString(manager, handle, effects, string, position, + colour, opacity, shaping, mesh_list)); + } + + int sheet_width = 0; + int sheet_height = 0; + Rml::Texture texture = Sheet_Texture(manager, Unpremultiplied_Color(colour), sheet_width, sheet_height); + if (!texture || sheet_width <= 0 || sheet_height <= 0) { + return(0); + } + + mesh_list.resize(1); + mesh_list[0].texture = texture; + Rml::Mesh & mesh = mesh_list[0].mesh; + mesh.vertices.reserve(string.size() * 4); + mesh.indices.reserve(string.size() * 6); + + float const scale = face->Scale(); + int const cell_width = face->CellWidth(); + int const cell_height = face->CellHeight(); + int const columns = (cell_width > 0) ? (sheet_width / cell_width) : 1; + + // The remapped sheet already carries the colour, so the quads only carry the opacity, + // which is what RmlUi's own engine does for a colour glyph. + Rml::ColourbPremultiplied const vertex_colour(colour.alpha, colour.alpha); + + Rml::Vector2f const dimensions((float)cell_width * scale, (float)cell_height * scale); + float const top = position.y - face->Metrics().ascent - (float)face->TopMargin() * scale; + int const spacing = (int)std::lround((double)shaping.letter_spacing); + + int line_width = 0; + char const * cursor = string.begin(); + char const * end = string.end(); + + while (cursor < end) { + char const * before = cursor; + char32_t code = UTF8::Decode(cursor); + if (cursor <= before) { + break; + } + + unsigned char glyph = OD_Font_Glyph(code); + if (glyph > ' ' && columns > 0) { + // Cell zero is blank; the sheet stores the glyph for code n in cell n + 1. + int cell = glyph + 1; + float left = (float)((cell % columns) * cell_width); + float upper = (float)((cell / columns) * cell_height); + + Rml::Vector2f const top_left(left / (float)sheet_width, upper / (float)sheet_height); + Rml::Vector2f const bottom_right((left + cell_width) / (float)sheet_width, + (upper + cell_height) / (float)sheet_height); + + // The blit started one pixel left of the pen, which is what puts the ink at the + // pen once the cell's own left margin is crossed. + Rml::Vector2f const origin(position.x + (float)line_width - scale, top); + + Rml::MeshUtilities::GenerateQuad(mesh, origin.Round(), dimensions, vertex_colour, + top_left, bottom_right); + } + + line_width += face->Advance(glyph) + spacing; + } + + return(std::max(line_width, 0)); +} + + +int UIFontEngineClass::GetVersion(Rml::FontFaceHandle handle) +{ + if (Bitmap_Face(handle) != nullptr) { + return(_Version); + } + return(Rml::FontEngineInterfaceDefault::GetVersion(handle)); +} + + +void UIFontEngineClass::ReleaseFontResources(void) +{ + _Sheets.clear(); + ++_Version; + Rml::FontEngineInterfaceDefault::ReleaseFontResources(); +} + + +/// +/// Gives back the glyph sheets while RmlUi still owns the render manager that has to free +/// them. +/// Rml::Shutdown calls this and then clears its render managers. Releasing a sheet after +/// that point dereferences a destroyed texture database, so this is the last moment a font +/// engine may hold a render resource. Rml::ReleaseFontResources, which also drops them, is +/// a collection entry point the application calls and is not part of shutdown. +/// +void UIFontEngineClass::Shutdown(void) +{ + _Sheets.clear(); + _Faces.clear(); + ++_Version; + Rml::FontEngineInterfaceDefault::Shutdown(); +} + + +static UIFontEngineClass _FontEngine; + + +Rml::FontEngineInterface * UI_Font_Interface(void) +{ + return(&_FontEngine); +} + + +void UI_Font_Shutdown(void) +{ + // The sheets and the faces are gone by now: Rml::Shutdown took them through the engine's + // own Shutdown, which is the only point where a render resource can still be released. + // Nothing here may touch one. What is left is the measurement of the artwork, which the + // next shell start probes again because the surface cache may have been emptied. + _MetricsRead = false; + _MetricsUsable = false; +} diff --git a/code/ui/uigamecontrols.cpp b/code/ui/uigamecontrols.cpp new file mode 100644 index 000000000..f4f8c1640 --- /dev/null +++ b/code/ui/uigamecontrols.cpp @@ -0,0 +1,394 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The game controls screen. Behavior traced out of gamedlg.cpp. +// +// What the extraction fixes in place, none of it obvious from the templates: leaving +// through the sound or the keyboard button applies and saves the settings, because both +// wrote the same IDOK the accept button did; the difficulty is applied only with no game +// running, so the slider the in-game template also carries is never read; a game speed +// change during a network session is issued as an event instead of being written, so every +// player stays in step; and the internet variant carries no game speed slider at all. +// +// docs/UI_DESIGN.md, "Screens", owns the contracts this keeps to. + +#include "always.h" + +#include "uigamecontrols.h" + +#include "uirmlview.h" + +#include "_map.h" +#include "audio/audioengine.h" +#include "_tooltip.h" +#include "cctooltip.h" +#include "data.h" +#include "event.h" +#include "gamedlg.h" +#include "globals.h" +#include "house.h" +#include "init.h" +#include "language/language.h" +#include "options.h" +#include "session.h" +#include "techno.h" + +#include "special.hh" + +#include +#include +#include + + +static void Fill_Labels(std::vector & labels, int const * names, int count) +{ + labels.clear(); + for (int index = 0; index < count; index++) { + labels.push_back(Fetch_String(names[index])); + } +} + + +void UIGameControlsPresenterClass::Refresh(void) +{ + if (GameActive) { + Variant = (Session.Type == GAME_INTERNET) ? VARIANT_INTERNET : VARIANT_SESSION; + } else { + Variant = VARIANT_FRONTEND; + } + + SpeedStep = (OptionsClass::MAX_SPEED_SETTING - 1) - Options.GameSpeed; + ScrollStep = (OptionsClass::MAX_SCROLL_SETTING - 1) - Options.ScrollRate; + DetailStep = Options.DetailLevel; + DifficultyStep = Options.Difficulty; + + CameoText = Options.SidebarCameoText; + ActionLines = Options.ActionLines; + ShowToolTips = Options.ToolTips; + Coasting = (Options.ScrollMethod == 0); + EdgeScroll = Options.AutoScroll; + + SoundAvailable = AudioEngine.Is_Available(); + + Fill_Labels(SpeedLabels, GameSpeedNames, OptionsClass::MAX_SPEED_SETTING); + Fill_Labels(ScrollLabels, GameScrollSpeedNames, OptionsClass::MAX_SCROLL_SETTING); + Fill_Labels(DetailLabels, GameDetailLevelNames, OptionsClass::MAX_DETAIL_SETTING); + Fill_Labels(DifficultyLabels, GameDifficultyNames, OptionsClass::MAX_DIFFICULTY_SETTING); +} + + +/// +/// The maintenance the dialog driver ran on every pass of its own loop. +/// +void UIGameControlsPresenterClass::Service(void) +{ + if (!GameActive) { + Title_Screen_Restore(); + } +} + + +bool UIGameControlsPresenterClass::Commits(void) const +{ + return(Choice == CHOICE_ACCEPT || Choice == CHOICE_SOUND || Choice == CHOICE_KEYBOARD); +} + + +void UIGameControlsPresenterClass::Execute(UIIntent const & intent) +{ + if (intent.Action == UI_GAMECTRL_SPEED) { + SpeedStep = intent.Value; + return; + } + if (intent.Action == UI_GAMECTRL_SCROLL) { + ScrollStep = intent.Value; + return; + } + if (intent.Action == UI_GAMECTRL_DETAIL) { + DetailStep = intent.Value; + return; + } + if (intent.Action == UI_GAMECTRL_DIFFICULTY) { + DifficultyStep = intent.Value; + return; + } + if (intent.Action == UI_GAMECTRL_CAMEO_TEXT) { + CameoText = (intent.Value != 0); + return; + } + if (intent.Action == UI_GAMECTRL_ACTION_LINES) { + ActionLines = (intent.Value != 0); + return; + } + if (intent.Action == UI_GAMECTRL_TOOLTIPS) { + ShowToolTips = (intent.Value != 0); + return; + } + if (intent.Action == UI_GAMECTRL_COASTING) { + Coasting = (intent.Value != 0); + return; + } + if (intent.Action == UI_GAMECTRL_EDGE_SCROLL) { + EdgeScroll = (intent.Value != 0); + return; + } + + UIResult result; + + if (intent.Action == UI_GAMECTRL_ACCEPT) { + Choice = CHOICE_ACCEPT; + result.Outcome = UIResult::OUTCOME_ACCEPTED; + } else if (intent.Action == UI_GAMECTRL_CANCEL) { + Choice = CHOICE_CANCEL; + result.Outcome = UIResult::OUTCOME_CANCELLED; + } else if (intent.Action == UI_GAMECTRL_SOUND) { + if (!Has_Sub_Screens()) { + return; + } + SpecialDialog = SDLG_SOUND; + Choice = CHOICE_SOUND; + result.Outcome = UIResult::OUTCOME_ACCEPTED; + } else if (intent.Action == UI_GAMECTRL_KEYBOARD) { + if (!Has_Sub_Screens()) { + return; + } + SpecialDialog = SDLG_KEYBOARD; + Choice = CHOICE_KEYBOARD; + result.Outcome = UIResult::OUTCOME_ACCEPTED; + } else { + return; + } + + Result = result; +} + + +void UIGameControlsPresenterClass::Apply(void) +{ + if (Has_Speed()) { + int const gamespeed = (OptionsClass::MAX_SPEED_SETTING - 1) - SpeedStep; + if (Options.GameSpeed != gamespeed) { + if (GameActive && Session.Type != GAME_NORMAL && Session.Type != GAME_SKIRMISH) { + OutList.push_back(EventClass(PlayerPtr->HeapID, EventClass::GAMESPEED, gamespeed)); + } else { + Options.GameSpeed = gamespeed; + } + } + } + + Options.ScrollRate = (OptionsClass::MAX_SCROLL_SETTING - 1) - ScrollStep; + + if (Options.DetailLevel != DetailStep) { + Options.DetailLevel = DetailStep; + Map.Reinit_Cell_Drawers(); + } + + if (Options.SidebarCameoText != CameoText) { + Options.SidebarCameoText = CameoText; + Map.Toggle_Cameo_Text(CameoText); + } + + Options.ActionLines = ActionLines; + TechnoClass::Set_Action_Lines(Options.ActionLines); + + Options.ToolTips = ShowToolTips; + if (ToolTips != NULL && GameActive) { + ToolTips->Activate(Options.ToolTips); + } + + Options.ScrollMethod = Coasting ? 0 : 1; + Options.AutoScroll = EdgeScroll; + + if (Has_Difficulty()) { + Options.Difficulty = DifficultyStep; + } +} + + +//--------------------------------------------------------------------------------------- +// The RmlUi view. One document per dialog template, because the three templates differ by +// which controls exist rather than by how one is arranged. +//--------------------------------------------------------------------------------------- + +/// +/// The RmlUi half of the game controls screen. +/// +class GameControlsViewClass : public UIRmlViewClass +{ + public: + GameControlsViewClass(UIGameControlsPresenterClass & presenter, char const * document) : + UIRmlViewClass(presenter, document), + Screen(presenter) + { + } + + virtual void Bind(Rml::DataModelConstructor & model) override; + virtual void Sync(void) override; + + // A slider takes its position from the model as the document loads, and that raises + // a change event of its own. Nothing is queued until this is set. + void Settle(void) { Settled = true; } + + private: + void Move(char const * which, int step); + void Update_Labels(void); + + UIGameControlsPresenterClass & Screen; + bool Settled = false; + + Rml::String SpeedText; + Rml::String ScrollText; + Rml::String DetailText; + Rml::String DifficultyText; +}; + + +static Rml::String Label_At(std::vector const & labels, int step) +{ + if (step < 0 || step >= (int)labels.size()) { + return(Rml::String()); + } + return(Rml::String(labels[step])); +} + + +void GameControlsViewClass::Update_Labels(void) +{ + SpeedText = Label_At(Screen.SpeedLabels, Screen.SpeedStep); + ScrollText = Label_At(Screen.ScrollLabels, Screen.ScrollStep); + DetailText = Label_At(Screen.DetailLabels, Screen.DetailStep); + DifficultyText = Label_At(Screen.DifficultyLabels, Screen.DifficultyStep); +} + + +void GameControlsViewClass::Move(char const * which, int step) +{ + if (!Settled) return; + + if (which == UI_GAMECTRL_SPEED && step == Screen.SpeedStep) return; + if (which == UI_GAMECTRL_SCROLL && step == Screen.ScrollStep) return; + if (which == UI_GAMECTRL_DETAIL && step == Screen.DetailStep) return; + if (which == UI_GAMECTRL_DIFFICULTY && step == Screen.DifficultyStep) return; + + Screen.Queue(UIIntent{which, "", step}); +} + + +void GameControlsViewClass::Bind(Rml::DataModelConstructor & model) +{ + Update_Labels(); + + model.Bind("speed", &Screen.SpeedStep); + model.Bind("scroll", &Screen.ScrollStep); + model.Bind("detail", &Screen.DetailStep); + model.Bind("difficulty", &Screen.DifficultyStep); + model.Bind("speedtext", &SpeedText); + model.Bind("scrolltext", &ScrollText); + model.Bind("detailtext", &DetailText); + model.Bind("difficultytext", &DifficultyText); + + model.Bind("cameotext", &Screen.CameoText); + model.Bind("actionlines", &Screen.ActionLines); + model.Bind("tooltips", &Screen.ShowToolTips); + model.Bind("coasting", &Screen.Coasting); + model.Bind("edgescroll", &Screen.EdgeScroll); + model.Bind("soundavailable", &Screen.SoundAvailable); + + model.BindEventCallback("move", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + + Rml::String const which = arguments[0].Get(); + int const step = (int)(event.GetParameter("value", 0.0f) + 0.5f); + + if (which == UI_GAMECTRL_SPEED) Move(UI_GAMECTRL_SPEED, step); + else if (which == UI_GAMECTRL_SCROLL) Move(UI_GAMECTRL_SCROLL, step); + else if (which == UI_GAMECTRL_DETAIL) Move(UI_GAMECTRL_DETAIL, step); + else if (which == UI_GAMECTRL_DIFFICULTY) Move(UI_GAMECTRL_DIFFICULTY, step); + }); + + // A check box is a class plus a click that queues a toggle, not a two-way bound control. + model.BindEventCallback("toggle", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + + Rml::String const which = arguments[0].Get(); + if (which == UI_GAMECTRL_CAMEO_TEXT) Screen.Queue(UIIntent{UI_GAMECTRL_CAMEO_TEXT, "", Screen.CameoText ? 0 : 1}); + else if (which == UI_GAMECTRL_ACTION_LINES) Screen.Queue(UIIntent{UI_GAMECTRL_ACTION_LINES, "", Screen.ActionLines ? 0 : 1}); + else if (which == UI_GAMECTRL_TOOLTIPS) Screen.Queue(UIIntent{UI_GAMECTRL_TOOLTIPS, "", Screen.ShowToolTips ? 0 : 1}); + else if (which == UI_GAMECTRL_COASTING) Screen.Queue(UIIntent{UI_GAMECTRL_COASTING, "", Screen.Coasting ? 0 : 1}); + else if (which == UI_GAMECTRL_EDGE_SCROLL) Screen.Queue(UIIntent{UI_GAMECTRL_EDGE_SCROLL, "", Screen.EdgeScroll ? 0 : 1}); + }); + + model.BindEventCallback("press", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{arguments[0].Get(), "", 0}); + }); + + // Escape cancels, which is the IDCANCEL the dialog's own cancel arm took. Enter accepts, + // because the template names no default push button and Windows then sent IDOK. + model.BindEventCallback("key", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + int const key = event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN); + if (key == Rml::Input::KI_ESCAPE) { + Screen.Queue(UIIntent{UI_GAMECTRL_CANCEL, "", 0}); + } else if (key == Rml::Input::KI_RETURN || key == Rml::Input::KI_NUMPADENTER) { + Screen.Queue(UIIntent{UI_GAMECTRL_ACCEPT, "", 0}); + } + }); +} + + +void GameControlsViewClass::Sync(void) +{ + if (!Model) return; + + Update_Labels(); + + // The slider positions are not dirtied, because a slider already carries the position + // its own change event reported. + Model.DirtyVariable("speedtext"); + Model.DirtyVariable("scrolltext"); + Model.DirtyVariable("detailtext"); + Model.DirtyVariable("difficultytext"); + Model.DirtyVariable("cameotext"); + Model.DirtyVariable("actionlines"); + Model.DirtyVariable("tooltips"); + Model.DirtyVariable("coasting"); + Model.DirtyVariable("edgescroll"); +} + + +/// +/// Shows the game controls and waits for the player to leave them. +/// +UIResult UI_Game_Controls_Screen(UIGameControlsPresenterClass & presenter) +{ + char const * document = "gamecontrols.rml"; + if (presenter.Variant == UIGameControlsPresenterClass::VARIANT_SESSION) { + document = "gamecontrolsmp.rml"; + } else if (presenter.Variant == UIGameControlsPresenterClass::VARIANT_INTERNET) { + document = "gamecontrolswol.rml"; + } + + GameControlsViewClass view(presenter, document); + + if (!view.Prepare(true)) { + UIResult result; + result.Outcome = UIResult::OUTCOME_FAILED_TO_OPEN; + return(result); + } + + view.Settle(); + + UIResult const result = UI_Run_Modal(presenter, view); + view.Close(); + return(result); +} diff --git a/code/ui/uigamecontrols.h b/code/ui/uigamecontrols.h new file mode 100644 index 000000000..5d6077748 --- /dev/null +++ b/code/ui/uigamecontrols.h @@ -0,0 +1,115 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The game controls screen's behavior, with no toolkit in it. Three templates share it and +// they carry different controls, so the variant is part of the view-model rather than +// something a view works out for itself. +// +// docs/UI_DESIGN.md, "Screens", owns the contract. + +#pragma once + +#include "uiscreen.h" + +#include +#include + + +// What a view raises. Value carries the slider step or the check state. +inline constexpr char const * UI_GAMECTRL_SPEED = "speed"; +inline constexpr char const * UI_GAMECTRL_SCROLL = "scroll"; +inline constexpr char const * UI_GAMECTRL_DETAIL = "detail"; +inline constexpr char const * UI_GAMECTRL_DIFFICULTY = "difficulty"; +inline constexpr char const * UI_GAMECTRL_CAMEO_TEXT = "cameotext"; +inline constexpr char const * UI_GAMECTRL_ACTION_LINES = "actionlines"; +inline constexpr char const * UI_GAMECTRL_TOOLTIPS = "tooltips"; +inline constexpr char const * UI_GAMECTRL_COASTING = "coasting"; +inline constexpr char const * UI_GAMECTRL_EDGE_SCROLL = "edgescroll"; +inline constexpr char const * UI_GAMECTRL_SOUND = "sound"; +inline constexpr char const * UI_GAMECTRL_KEYBOARD = "keyboard"; +inline constexpr char const * UI_GAMECTRL_ACCEPT = "accept"; +inline constexpr char const * UI_GAMECTRL_CANCEL = "cancel"; + + +class UIGameControlsPresenterClass : public UIPresenterClass +{ + public: + // Which of the three templates the state below belongs to. The names are the + // game's own, and they do not mean what they look like: the screen shown with no + // game running is IDD_OPT_CTRL_GAME_SP and the one shown during any local game is + // IDD_OPT_CTRL_GAME_MP. + enum VariantType { + VARIANT_FRONTEND, + VARIANT_SESSION, + VARIANT_INTERNET, + }; + + enum ChoiceType { + CHOICE_NONE, + CHOICE_ACCEPT, + CHOICE_CANCEL, + CHOICE_SOUND, + CHOICE_KEYBOARD, + }; + + virtual void Execute(UIIntent const & intent) override; + virtual void Refresh(void) override; + virtual void Service(void) override; + + // Writes the staged settings back into the game and saves them. The driver calls + // this where the dialog called Set, which is after its loop and before the screen + // comes down. + void Apply(void); + + // Does the way the player left the screen commit the settings? Leaving through the + // sound or keyboard button does, which is not obvious and is the dialog's own + // behavior: both wrote IDOK. + bool Commits(void) const; + + /* + ** The view-model. Plain values, and the only thing a view reads. + */ + VariantType Variant = VARIANT_FRONTEND; + + // Slider steps. Game speed and scroll rate count backward, so the fastest sits at + // step zero; detail and difficulty count forward. A view shows steps; only this + // class knows what they mean. + int SpeedStep = 0; + int ScrollStep = 0; + int DetailStep = 0; + int DifficultyStep = 0; + + bool CameoText = false; + bool ActionLines = false; + bool ShowToolTips = false; + bool Coasting = false; + bool EdgeScroll = false; + + // Is there an audio device to talk to? With none the sound button is disabled, as + // the dialog disabled it. + bool SoundAvailable = false; + + std::vector SpeedLabels; + std::vector ScrollLabels; + std::vector DetailLabels; + std::vector DifficultyLabels; + + // What each variant carries. A setting whose control the template omits is not + // applied, which is what the dialog's own null checks on GetDlgItem amounted to. + bool Has_Speed(void) const { return(Variant != VARIANT_INTERNET); } + bool Has_Difficulty(void) const { return(Variant == VARIANT_FRONTEND); } + bool Has_Sub_Screens(void) const { return(Variant != VARIANT_FRONTEND); } + + ChoiceType Choice = CHOICE_NONE; +}; + + +// Shows the screen through its RmlUi view. FAILED_TO_OPEN leaves nothing shown and the +// caller falls through to the legacy dialog. +UIResult UI_Game_Controls_Screen(UIGameControlsPresenterClass & presenter); diff --git a/code/ui/uigameoptions.cpp b/code/ui/uigameoptions.cpp new file mode 100644 index 000000000..e69acaa85 --- /dev/null +++ b/code/ui/uigameoptions.cpp @@ -0,0 +1,405 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The in-game options screen. Behavior traced out of goptions.cpp and kept where it was, +// with the window handling left behind in the view. +// +// What the extraction fixes in place, each of which the dialog decided rather than the +// template: save and load mean different things in a solo game and in a session, and only +// the solo ones open a browser at all; delete never ends the screen, it just changes what +// is on disk and the screen is refreshed; a skirmish has no briefing to restate; resume is +// where the two sliders are applied, because dragging one only moved its label; abort +// asks for the surrender box rather than the abort box in a tournament session; and the +// screen answers with a choice rather than with a control identifier. +// +// docs/UI_DESIGN.md, "Screens", owns the contracts this keeps to. + +#include "always.h" + +#include "uigameoptions.h" + +#include "uirmlview.h" + +#include "data.h" +#include "dbgprint.h" +#include "event.h" +#include "gamedlg.h" +#include "globals.h" +#include "house.h" +#include "language/language.h" +#include "loaddlg.h" +#include "options.h" +#include "savemgr.h" +#include "scenario.h" +#include "session.h" +#include "stats.h" + +#include "special.hh" + +#include +#include +#include + +#include + + +// The connection quality labels, best first, which is the order the slider counts in. +static int const _ConnectionNames[] = { + TXT_WORST_CONNECTION, + TXT_POOR_CONNECTION, + TXT_GOOD_CONNECTION, + TXT_BEST_CONNECTION +}; + +static int const CONNECTION_STEPS = 4; + + +static bool Is_Solo_Session(void) +{ + return(Session.Type == GAME_NORMAL || Session.Type == GAME_SKIRMISH); +} + + +/// +/// Copies the state the screen shows out of the game. +/// Called at open and again whenever a sub-screen has changed what is on disk, which is what +/// the dialog's second call to its own WM_INITDIALOG handler did. +/// +void UIGameOptionsPresenterClass::Refresh(void) +{ + IsMultiplayer = !Is_Solo_Session(); + HasSliders = (Session.Type == GAME_INTERNET); + + if (Is_Solo_Session()) { + bool const present = LoadOptionsClass().Files_Present(); + CanSave = true; + CanLoad = present; + CanDelete = present; + } else { + CanSave = SaveManager.Is_Multiplayer_Saving_Allowed(); + CanLoad = SaveManager.Multiplayer_Load_Is_Allowed() && MultiplayerLoadOptionsClass().Files_Present(); + CanDelete = true; + } + + CanBrief = (Session.Type != GAME_SKIRMISH); + + SpeedStep = (OptionsClass::MAX_SPEED_SETTING - 1) - Options.GameSpeed; + ConnectionStep = (CONNECTION_STEPS - 1) - Session.LatencyFudge; + + SpeedLabels.clear(); + for (int step = 0; step < OptionsClass::MAX_SPEED_SETTING; step++) { + SpeedLabels.push_back(Fetch_String(GameSpeedNames[step])); + } + + ConnectionLabels.clear(); + for (int step = 0; step < CONNECTION_STEPS; step++) { + ConnectionLabels.push_back(Fetch_String(_ConnectionNames[step])); + } +} + + +void UIGameOptionsPresenterClass::Finish(ChoiceType choice, UIResult::OutcomeType outcome) +{ + Choice = choice; + + UIResult result; + result.Outcome = outcome; + Result = result; +} + + +void UIGameOptionsPresenterClass::Execute(UIIntent const & intent) +{ + if (intent.Action == UI_GAMEOPT_SPEED) { + // Dragging only moves the label. The setting is applied when the player resumes, + // which is where the dialog read the slider back. + SpeedStep = intent.Value; + return; + } + + if (intent.Action == UI_GAMEOPT_CONNECTION) { + ConnectionStep = intent.Value; + return; + } + + // The two checks below are the dialog's own, made when the button was pressed rather + // than when it was enabled, because a session can withdraw permission while the screen + // is up. The view-model's CanSave and CanLoad say what to show, not what to allow. + if (intent.Action == UI_GAMEOPT_SAVE) { + if (Is_Solo_Session()) { + Pending = SUB_SAVE; + } else if (SaveManager.Is_Multiplayer_Saving_Allowed()) { + OutList.push_back(EventClass(PlayerPtr->HeapID, EventClass::SAVEGAME)); + Finish(CHOICE_SAVE_REQUESTED, UIResult::OUTCOME_ACCEPTED); + } + return; + } + + if (intent.Action == UI_GAMEOPT_LOAD) { + if (Is_Solo_Session()) { + Pending = SUB_LOAD; + } else if (SaveManager.Multiplayer_Load_Is_Allowed()) { + // A list opened from in here would sit inside the main loop and stall the match; + // the menu loop opens it between frames instead. + SpecialDialog = SDLG_LOAD; + Finish(CHOICE_LOADED, UIResult::OUTCOME_ACCEPTED); + } + return; + } + + if (intent.Action == UI_GAMEOPT_DELETE) { + Pending = SUB_DELETE; + return; + } + + if (intent.Action == UI_GAMEOPT_BRIEFING) { + Finish(CHOICE_BRIEFING, UIResult::OUTCOME_ACCEPTED); + return; + } + + if (intent.Action == UI_GAMEOPT_RESUME) { + if (Session.Type == GAME_INTERNET) { + int const fudge = (CONNECTION_STEPS - 1) - ConnectionStep; + if (fudge != Session.LatencyFudge) { + OutList.push_back(EventClass(PlayerPtr->HeapID, EventClass::LATENCYFUDGE, fudge)); + DebugString("LATENCYFUDGE event created - %d\n", fudge); + } + + int const speed = (OptionsClass::MAX_SPEED_SETTING - 1) - SpeedStep; + if (Options.GameSpeed != speed) { + OutList.push_back(EventClass(PlayerPtr->HeapID, EventClass::GAMESPEED, speed)); + } + } + Finish(CHOICE_RESUME, UIResult::OUTCOME_ACCEPTED); + return; + } + + if (intent.Action == UI_GAMEOPT_ABORT) { + if (Session.Type == GAME_INTERNET) { + SpecialDialog = WestwoodOnline_Tournament ? SDLG_SURRENDER : SDLG_ABORT; + } else { + SpecialDialog = SDLG_ABORT; + } + Finish(CHOICE_ABORT, UIResult::OUTCOME_CANCELLED); + return; + } + + if (intent.Action == UI_GAMEOPT_SETTINGS) { + SpecialDialog = SDLG_SETTINGS; + Finish(CHOICE_SETTINGS, UIResult::OUTCOME_ACCEPTED); + return; + } +} + + +/// +/// Runs the browser an executed intent asked for. +/// The caller has already got its own presentation out of the way, which is all a view has +/// to do about a screen opening on top of this one. +/// +void UIGameOptionsPresenterClass::Run_Pending(void) +{ + SubScreenType const pending = Pending; + Pending = SUB_NONE; + + switch (pending) { + case SUB_SAVE: { + char description[512]; + std::strcpy(description, Scen->Description); + LoadOptionsClass().Save(description); + Refresh(); + } + break; + + case SUB_LOAD: + if (LoadOptionsClass().Load()) { + Finish(CHOICE_LOADED, UIResult::OUTCOME_ACCEPTED); + } + break; + + case SUB_DELETE: + LoadOptionsClass().Delete(); + Refresh(); + break; + + default: + break; + } +} + + +//--------------------------------------------------------------------------------------- +// The RmlUi view. One document per dialog template, because the three templates differ by +// which controls exist rather than by how one is arranged. +//--------------------------------------------------------------------------------------- + +/// +/// The RmlUi half of the in-game options screen. +/// +class GameOptionsViewClass : public UIRmlViewClass +{ + public: + GameOptionsViewClass(UIGameOptionsPresenterClass & presenter, char const * document) : + UIRmlViewClass(presenter, document), + Screen(presenter) + { + } + + virtual void Bind(Rml::DataModelConstructor & model) override; + virtual void Sync(void) override; + + // A slider takes its position from the model as the document loads, and that raises + // a change event of its own. Nothing is queued until this is set. + void Settle(void) { Settled = true; } + + private: + void Move(char const * which, int step); + void Update_Labels(void); + + UIGameOptionsPresenterClass & Screen; + bool Settled = false; + + // The caption beside each slider. Held here because the labels the presenter + // carries are indexed by step and a document binds a value, not a lookup. + Rml::String SpeedLabel; + Rml::String ConnectionLabel; +}; + + +void GameOptionsViewClass::Update_Labels(void) +{ + SpeedLabel.clear(); + if (Screen.SpeedStep >= 0 && Screen.SpeedStep < (int)Screen.SpeedLabels.size()) { + SpeedLabel = Screen.SpeedLabels[Screen.SpeedStep]; + } + + ConnectionLabel.clear(); + if (Screen.ConnectionStep >= 0 && Screen.ConnectionStep < (int)Screen.ConnectionLabels.size()) { + ConnectionLabel = Screen.ConnectionLabels[Screen.ConnectionStep]; + } +} + + +void GameOptionsViewClass::Move(char const * which, int step) +{ + if (!Settled) return; + + // A position the screen already holds raises no intent, so setting a slider from the + // model cannot look like a move the player did not make. + if (which == UI_GAMEOPT_SPEED && step == Screen.SpeedStep) return; + if (which == UI_GAMEOPT_CONNECTION && step == Screen.ConnectionStep) return; + + Screen.Queue(UIIntent{which, "", step}); +} + + +void GameOptionsViewClass::Bind(Rml::DataModelConstructor & model) +{ + Update_Labels(); + + model.Bind("cansave", &Screen.CanSave); + model.Bind("canload", &Screen.CanLoad); + model.Bind("candelete", &Screen.CanDelete); + model.Bind("canbrief", &Screen.CanBrief); + model.Bind("speed", &Screen.SpeedStep); + model.Bind("connection", &Screen.ConnectionStep); + model.Bind("speedlabel", &SpeedLabel); + model.Bind("connectionlabel", &ConnectionLabel); + + model.BindEventCallback("press", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{arguments[0].Get(), "", 0}); + }); + + model.BindEventCallback("move", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + + Rml::String const which = arguments[0].Get(); + int const step = (int)(event.GetParameter("value", 0.0f) + 0.5f); + + if (which == UI_GAMEOPT_SPEED) Move(UI_GAMEOPT_SPEED, step); + else if (which == UI_GAMEOPT_CONNECTION) Move(UI_GAMEOPT_CONNECTION, step); + }); + + // Escape resumes, which is the IDCANCEL the dialog answered with its resume arm. Enter + // resumes too: the template names no default push button, so Windows sent IDOK, and the + // dialog treated that as the resume button. + model.BindEventCallback("key", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + int const key = event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN); + if (key == Rml::Input::KI_ESCAPE || key == Rml::Input::KI_RETURN || key == Rml::Input::KI_NUMPADENTER) { + Screen.Queue(UIIntent{UI_GAMEOPT_RESUME, "", 0}); + } + }); +} + + +void GameOptionsViewClass::Sync(void) +{ + if (!Model) return; + + Update_Labels(); + + // The slider positions are not dirtied, because a slider already carries the position + // its own change event reported. + Model.DirtyVariable("cansave"); + Model.DirtyVariable("canload"); + Model.DirtyVariable("candelete"); + Model.DirtyVariable("canbrief"); + Model.DirtyVariable("speedlabel"); + Model.DirtyVariable("connectionlabel"); +} + + +/// +/// Shows the in-game options and waits for the player to choose. +/// +UIResult UI_Game_Options_Screen(UIGameOptionsPresenterClass & presenter) +{ + char const * document = "gameoptionsmp.rml"; + if (!presenter.IsMultiplayer) { + document = "gameoptions.rml"; + } else if (presenter.HasSliders) { + document = "gameoptionswol.rml"; + } + + GameOptionsViewClass view(presenter, document); + + if (!view.Prepare(true)) { + UIResult result; + result.Outcome = UIResult::OUTCOME_FAILED_TO_OPEN; + return(result); + } + + view.Settle(); + + // A browser this screen opens is a screen of a different kind, so it nests; getting out + // of the way of it is hiding this document, which is what the dialog's ShowWindow did. + while (!presenter.Result.has_value()) { + UI_Run_Modal(presenter, view); + + if (presenter.Pending == UIGameOptionsPresenterClass::SUB_NONE) { + break; + } + + view.Hide(); + presenter.Run_Pending(); + if (presenter.Result.has_value()) { + break; + } + view.Show(); + view.Sync(); + } + + UIResult const result = presenter.Result.value_or(UIResult{}); + view.Close(); + return(result); +} diff --git a/code/ui/uigameoptions.h b/code/ui/uigameoptions.h new file mode 100644 index 000000000..65344d83a --- /dev/null +++ b/code/ui/uigameoptions.h @@ -0,0 +1,105 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The in-game options screen's behavior, with no toolkit in it. It is the door to save, +// load, the mission briefing, the game settings and abort, so what it decides is worth +// having in one toolkit-free place before either view is written. +// +// docs/UI_DESIGN.md, "Screens", owns the contract. + +#pragma once + +#include "uiscreen.h" + +#include +#include + + +// What a view raises. +inline constexpr char const * UI_GAMEOPT_SAVE = "save"; +inline constexpr char const * UI_GAMEOPT_LOAD = "load"; +inline constexpr char const * UI_GAMEOPT_DELETE = "delete"; +inline constexpr char const * UI_GAMEOPT_BRIEFING = "briefing"; +inline constexpr char const * UI_GAMEOPT_RESUME = "resume"; +inline constexpr char const * UI_GAMEOPT_ABORT = "abort"; +inline constexpr char const * UI_GAMEOPT_SETTINGS = "settings"; +inline constexpr char const * UI_GAMEOPT_SPEED = "speed"; // Value: slider step +inline constexpr char const * UI_GAMEOPT_CONNECTION = "connection"; // Value: slider step + + +class UIGameOptionsPresenterClass : public UIPresenterClass +{ + public: + // What the player settled on. The driver maps this onto the value its own caller + // expects; no control identifier reaches this class. + enum ChoiceType { + CHOICE_NONE, + CHOICE_RESUME, + CHOICE_BRIEFING, + CHOICE_ABORT, + CHOICE_SETTINGS, + CHOICE_SAVE_REQUESTED, + CHOICE_LOADED, + }; + + // A screen this one opens on top of itself. The view hides whatever it has to hide, + // then asks for the pending one to run; only the view knows how to get out of the + // way, and only this class knows what running it means. + enum SubScreenType { + SUB_NONE, + SUB_SAVE, + SUB_LOAD, + SUB_DELETE, + }; + + virtual void Execute(UIIntent const & intent) override; + virtual void Refresh(void) override; + virtual bool Suspends(void) const override { return(Pending != SUB_NONE); } + + // Runs the sub-screen an executed intent asked for and clears the request. Safe to + // call with nothing pending. + void Run_Pending(void); + + /* + ** The view-model. Plain values, and the only thing a view reads. + */ + bool CanSave = false; + bool CanLoad = false; + bool CanDelete = false; + bool CanBrief = false; + + // Does the screen carry the game speed and connection quality sliders? Only the + // template the game shows for an internet session does. + bool HasSliders = false; + + // Is this a session the player saves and loads through the multiplayer path? The two + // paths differ in what the buttons mean, not only in whether they are enabled. + bool IsMultiplayer = false; + + // Slider steps, counted the way the templates count them: the fastest game speed and + // the best connection sit at step zero, so a step is the setting counted backward. A + // view shows steps; only this class knows what they mean. + int SpeedStep = 0; + int ConnectionStep = 0; + + // The label beside each slider, indexed by step. + std::vector SpeedLabels; + std::vector ConnectionLabels; + + ChoiceType Choice = CHOICE_NONE; + SubScreenType Pending = SUB_NONE; + + private: + void Finish(ChoiceType choice, UIResult::OutcomeType outcome); +}; + + +// Shows the screen through its RmlUi view. FAILED_TO_OPEN leaves nothing shown and the +// caller falls through to the legacy dialog. +UIResult UI_Game_Options_Screen(UIGameOptionsPresenterClass & presenter); diff --git a/code/ui/uigametype.cpp b/code/ui/uigametype.cpp new file mode 100644 index 000000000..931121a50 --- /dev/null +++ b/code/ui/uigametype.cpp @@ -0,0 +1,153 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The game type screen. Behavior traced out of Select_Game_Type_Dialog and its procedure in +// addon.cpp. +// +// What the extraction fixes in place: the addon state is cleared before the choice is +// applied and the required addon is set afterwards whichever way the player went, and only +// backing out stops the game carrying on, which is the dialog's own default arm reading any +// identifier that was not Firestorm as the base game. +// +// docs/UI_DESIGN.md, "Screens", owns the contracts this keeps to. + +#include "always.h" + +#include "uigametype.h" + +#include "uirmlview.h" + +#include "addon.h" +#include "init.h" + +#include +#include +#include + + +void UIGameTypePresenterClass::Refresh(void) +{ + Choice = CHOICE_NONE; +} + + +/// +/// The maintenance the dialog driver ran on every pass of its own loop. +/// +void UIGameTypePresenterClass::Service(void) +{ + Title_Screen_Restore(false); +} + + +bool UIGameTypePresenterClass::Apply(int & addon) +{ + // Every addon off before the choice is applied, which is what the dialog did by + // assigning the active set directly. + Disable_Addon(ADDON_ANY); + + if (Choice == CHOICE_BACK) { + return(false); + } + + if (Choice == CHOICE_FIRESTORM) { + Enable_Addon(ADDON_FIRESTORM); + addon = ADDON_FIRESTORM; + } else { + addon = ADDON_BASE_GAME; + } + + Set_Required_Addon((AddonType)addon); + return(true); +} + + +void UIGameTypePresenterClass::Execute(UIIntent const & intent) +{ + UIResult result; + result.Outcome = UIResult::OUTCOME_ACCEPTED; + + if (intent.Action == UI_GAMETYPE_FIRESTORM) { + Choice = CHOICE_FIRESTORM; + } else if (intent.Action == UI_GAMETYPE_ORIGINAL) { + Choice = CHOICE_ORIGINAL; + } else if (intent.Action == UI_GAMETYPE_BACK) { + Choice = CHOICE_BACK; + result.Outcome = UIResult::OUTCOME_CANCELLED; + } else { + return; + } + + Result = result; +} + + +//--------------------------------------------------------------------------------------- +// The RmlUi view. +//--------------------------------------------------------------------------------------- + +/// +/// The RmlUi half of the game type screen. +/// +class GameTypeViewClass : public UIRmlViewClass +{ + public: + GameTypeViewClass(UIGameTypePresenterClass & presenter) : + UIRmlViewClass(presenter, "gametype.rml"), + Screen(presenter) + { + } + + virtual void Bind(Rml::DataModelConstructor & model) override; + virtual void Sync(void) override {} + + private: + UIGameTypePresenterClass & Screen; +}; + + +void GameTypeViewClass::Bind(Rml::DataModelConstructor & model) +{ + model.BindEventCallback("press", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{arguments[0].Get(), "", 0}); + }); + + // The Main Menu button is the template's IDCANCEL, so Escape is what it is, and Enter + // reaches the dialog's default arm, which is the base game. + model.BindEventCallback("key", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + int const key = event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN); + if (key == Rml::Input::KI_ESCAPE) { + Screen.Queue(UIIntent{UI_GAMETYPE_BACK, "", 0}); + } else if (key == Rml::Input::KI_RETURN || key == Rml::Input::KI_NUMPADENTER) { + Screen.Queue(UIIntent{UI_GAMETYPE_ORIGINAL, "", 0}); + } + }); +} + + +/// +/// Shows the game type choice and waits for the player to make it. +/// +UIResult UI_Game_Type_Screen(UIGameTypePresenterClass & presenter) +{ + GameTypeViewClass view(presenter); + + if (!view.Prepare(true)) { + UIResult result; + result.Outcome = UIResult::OUTCOME_FAILED_TO_OPEN; + return(result); + } + + UIResult const result = UI_Run_Modal(presenter, view); + view.Close(); + return(result); +} diff --git a/code/ui/uigametype.h b/code/ui/uigametype.h new file mode 100644 index 000000000..501a8961e --- /dev/null +++ b/code/ui/uigametype.h @@ -0,0 +1,50 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The game type screen's behavior, with no toolkit in it. Two buttons and a way back, shown +// only when an expansion is installed and there is a choice to make. +// +// docs/UI_DESIGN.md, "Screens", owns the contract. + +#pragma once + +#include "uiscreen.h" + + +inline constexpr char const * UI_GAMETYPE_ORIGINAL = "original"; +inline constexpr char const * UI_GAMETYPE_FIRESTORM = "firestorm"; +inline constexpr char const * UI_GAMETYPE_BACK = "back"; + + +class UIGameTypePresenterClass : public UIPresenterClass +{ + public: + enum ChoiceType { + CHOICE_NONE, + CHOICE_ORIGINAL, + CHOICE_FIRESTORM, + CHOICE_BACK, + }; + + virtual void Execute(UIIntent const & intent) override; + virtual void Refresh(void) override; + virtual void Service(void) override; + + // Puts the addon system into the state the choice asks for, and says whether the + // game carries on. Anything but backing out carries on, which is the dialog's own + // default arm. + bool Apply(int & addon); + + ChoiceType Choice = CHOICE_NONE; +}; + + +// Shows the screen through its RmlUi view. FAILED_TO_OPEN leaves nothing shown and the +// caller falls through to the legacy dialog. +UIResult UI_Game_Type_Screen(UIGameTypePresenterClass & presenter); diff --git a/code/ui/uiinternal.h b/code/ui/uiinternal.h new file mode 100644 index 000000000..1ff17a5b2 --- /dev/null +++ b/code/ui/uiinternal.h @@ -0,0 +1,93 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// What the shell's own translation units hand each other. Nothing outside code/ui includes +// this. It names RmlUi types by forward declaration so that uirender.cpp stays the only +// file carrying bgfx and uitexture.cpp the only one carrying the image decoders. + +#pragma once + +#include +#include + +struct ImDrawData; + +namespace Rml { + class RenderInterface; + class SystemInterface; + class FileInterface; + class FontEngineInterface; +} + + +// Decoded image pixels, RGBA8 with premultiplied alpha, top row first. +struct UIImageData +{ + std::vector Pixels; + int Width = 0; + int Height = 0; +}; + + +// uitexture.cpp +bool UI_Decode_Image(char const * source, UIImageData & image); + +// uifont.cpp +Rml::FontEngineInterface * UI_Font_Interface(void); +void UI_Font_Shutdown(void); + +// uirender.cpp +Rml::RenderInterface * UI_Render_Interface(void); +bool UI_Render_Init(void); +void UI_Render_Shutdown(void); + +// Points the overlay views at where the frame landed in the window. Coordinates handed to +// the toolkits afterwards are physical pixels from the frame's top left corner. +void UI_Render_Begin(int destx, int desty, int width, int height); +void UI_Render_End(void); +void UI_Render_ImGui(ImDrawData * data); + +// uimessagebox.cpp +void UI_Message_Box_Service(void); + +// uisurface.cpp +void UI_Surface_Element_Init(void); +void UI_Surface_Element_Shutdown(void); + +// uishell.cpp. Puts what the shell draws on screen, which is the synchronous repaint a +// modeless dialog got from SendMessage(WM_PAINT). Only a screen with no loop of its own +// needs it; a screen inside UI_Run_Modal is presented by every pass. An immediate paint +// ignores the present pacing, which a box that must be seen before a long operation begins +// cannot afford to be skipped by. +void UI_Paint_Now(bool immediate); + +// Turns an identifier RmlUi reports back into the Win32 virtual key it came from. A screen +// that records a keypress needs it, because an RmlUi key event carries the identifier and +// the game's own key encoding is a virtual key with its modifier bits above it. Zero for an +// identifier no key produces. +int UI_Virtual_Key(int identifier); + +// uisystem.cpp +Rml::SystemInterface * UI_System_Interface(void); + +// uifile.cpp +Rml::FileInterface * UI_File_Interface(void); + +// uidev.cpp +bool UI_Dev_Init(void); +void UI_Dev_Shutdown(void); +void UI_Dev_New_Frame(int width, int height, double deltaseconds); +void UI_Dev_Render(void); +bool UI_Dev_Wants_Mouse(void); +bool UI_Dev_Wants_Keyboard(void); +bool UI_Dev_Is_Open(void); +void UI_Dev_Toggle(void); +void UI_Dev_Mouse_Position(float x, float y); +void UI_Dev_Mouse_Button(int button, bool down); +void UI_Dev_Mouse_Wheel(float delta); diff --git a/code/ui/uikeyboard.cpp b/code/ui/uikeyboard.cpp new file mode 100644 index 000000000..6f0f3e5dd --- /dev/null +++ b/code/ui/uikeyboard.cpp @@ -0,0 +1,472 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The keyboard screen. Behavior traced out of Hotkey_Dialog_Proc and its four private +// messages in options.cpp. +// +// What the extraction fixes in place, none of it obvious from the template: assigning with +// nothing captured CLEARS the selected command's key, because the dialog removed the old +// binding before it looked at the new one and only re-added when the key was not zero; +// assigning a key another command already holds TAKES it, because the key is removed from +// the index before it is added; cancel is not a no-op but a reload, since every assignment +// was already made against the live index; and reset deletes only the player's own +// KEYBOARD.INI, so the defaults a deployment ships are what is left. +// +// One inherited defect is preserved rather than repaired: changing the category left the +// command list with no selection, because the dialog handed ListBox_SetCurSel the +// description control instead of the list. The description therefore clears and the +// shortcut, the capture and the assigned-to text all keep whatever they were showing, until +// the player picks a command. +// +// docs/UI_DESIGN.md, "Screens", owns the contracts this keeps to. + +#include "always.h" + +#include "uikeyboard.h" + +#include "uiinternal.h" +#include "uirmlview.h" + +#include "_command.h" +#include "ccfile.h" +#include "ccini.h" +#include "cdfile.h" +#include "command.h" +#include "dbgprint.h" +#include "globals.h" +#include "init.h" +#include "language/language.h" +#include "msgbox.h" +#include "keyboard.h" +#include "vector.h" + +#include "keyboard.h" + +#include +#include +#include + +#include +#include +#include + + +std::string UIKeyboardPresenterClass::Key_Name(int key) +{ + char buffer[64]; + buffer[0] = '\0'; + Build_Hotkey_String((KeyNumType)key, buffer); + return(std::string(buffer)); +} + + +static CommandClass const * Find_Command(std::string const & unique) +{ + for (int index = 0; index < AllCommands.Count(); index++) { + if (unique == AllCommands[index]->Get_Unique_Name()) { + return(AllCommands[index]); + } + } + return(NULL); +} + + +// The key a command answers to now, or zero when it answers to none. +static int Key_Of_Command(CommandClass const * command) +{ + for (int index = 0; index < HotkeyCommands.Count(); index++) { + if (HotkeyCommands.Fetch_By_Position(index) == command) { + return(HotkeyCommands.Fetch_ID_By_Position(index)); + } + } + return(0); +} + + +static bool Less_Ignoring_Case(std::string const & left, std::string const & right) +{ + return(stricmp(left.c_str(), right.c_str()) < 0); +} + + +/// +/// Rebuilds the whole screen from the command list and the live key assignments. +/// +void UIKeyboardPresenterClass::Refresh(void) +{ + Categories.clear(); + + for (int index = 0; index < AllCommands.Count(); index++) { + char const * const category = AllCommands[index]->Get_Category(); + if (category == NULL) continue; + + bool present = false; + for (std::string const & known : Categories) { + if (stricmp(known.c_str(), category) == 0) { + present = true; + break; + } + } + if (!present) { + Categories.push_back(category); + } + } + + // The combo box carried CBS_SORT, so the player sees the categories in order rather + // than in the order the command list was built. + std::sort(Categories.begin(), Categories.end(), Less_Ignoring_Case); + + SelectedCategory = Categories.empty() ? -1 : 0; + + CapturedKey = 0; + CapturedText.clear(); + AssignedTo.clear(); + CurrentShortcut.clear(); + + Fill_Commands(); +} + + +/// +/// The maintenance the dialog driver ran on every pass of its own loop. +/// +void UIKeyboardPresenterClass::Service(void) +{ + if (!GameActive) { + Title_Screen_Restore(); + } +} + + +void UIKeyboardPresenterClass::Fill_Commands(void) +{ + Commands.clear(); + SelectedCommand = -1; + Description.clear(); + + if (SelectedCategory < 0 || SelectedCategory >= (int)Categories.size()) { + return; + } + + std::string const & category = Categories[SelectedCategory]; + + for (int index = 0; index < AllCommands.Count(); index++) { + CommandClass const * const command = AllCommands[index]; + if (command->Get_Category() == NULL) continue; + if (stricmp(command->Get_Category(), category.c_str()) != 0) continue; + + CommandType entry; + entry.Label = (command->Get_Display_Name() != NULL) ? command->Get_Display_Name() : ""; + entry.Description = (command->Get_Description() != NULL) ? command->Get_Description() : ""; + entry.UniqueName = command->Get_Unique_Name(); + Commands.push_back(entry); + } + + // The list box carried LBS_SORT. + std::sort(Commands.begin(), Commands.end(), + [](CommandType const & left, CommandType const & right) { + return(Less_Ignoring_Case(left.Label, right.Label)); + }); +} + + +void UIKeyboardPresenterClass::Show_Command(void) +{ + if (SelectedCommand < 0 || SelectedCommand >= (int)Commands.size()) { + return; + } + + CommandType const & entry = Commands[SelectedCommand]; + CommandClass const * const command = Find_Command(entry.UniqueName); + + Description = entry.Description; + CurrentShortcut = Key_Name(Key_Of_Command(command)); + + // The capture control was emptied every time a command was shown, so a key captured for + // one command cannot be assigned to the next by accident. + CapturedKey = 0; + CapturedText.clear(); + AssignedTo.clear(); +} + + +void UIKeyboardPresenterClass::Apply_Hotkey(void) +{ + if (SelectedCommand < 0 || SelectedCommand >= (int)Commands.size()) { + return; + } + + CommandClass const * const command = Find_Command(Commands[SelectedCommand].UniqueName); + if (command == NULL) { + return; + } + + for (int index = 0; index < HotkeyCommands.Count(); index++) { + if (HotkeyCommands.Fetch_By_Position(index) == command) { + HotkeyCommands.Remove_Index(HotkeyCommands.Fetch_ID_By_Position(index)); + break; + } + } + + if (CapturedKey != 0) { + HotkeyCommands.Remove_Index(CapturedKey); + HotkeyCommands.Add_Index(CapturedKey, command); + } +} + + +void UIKeyboardPresenterClass::Reset_All(void) +{ + DebugString("Deleting users KEYBOARD.INI\n"); + + // Only the player's own file is discarded; the defaults a deployment ships are what the + // reset falls back on. + CCFileClass file("KEYBOARD.INI"); + file.Delete(); + + Init_Hotkeys(); + Refresh(); +} + + +void UIKeyboardPresenterClass::Save_Assignments(void) const +{ + CCINIClass ini; + ini.Clear(); + + for (int index = 0; index < HotkeyCommands.Count(); index++) { + CommandClass const * const command = HotkeyCommands.Fetch_By_Position(index); + int const key = HotkeyCommands.Fetch_ID_By_Position(index); + ini.Put_Int("Hotkey", command->Get_Unique_Name(), key); + } + + CDFileClass file("Keyboard.ini"); + ini.Save(file, false); +} + + +void UIKeyboardPresenterClass::Execute(UIIntent const & intent) +{ + if (intent.Action == UI_KEYBOARD_CATEGORY) { + if (intent.Value >= 0 && intent.Value < (int)Categories.size() && intent.Value != SelectedCategory) { + SelectedCategory = intent.Value; + Fill_Commands(); + } + return; + } + + if (intent.Action == UI_KEYBOARD_COMMAND) { + if (intent.Value >= 0 && intent.Value < (int)Commands.size()) { + SelectedCommand = intent.Value; + Show_Command(); + } + return; + } + + if (intent.Action == UI_KEYBOARD_CAPTURE) { + CapturedKey = intent.Value; + CapturedText = Key_Name(CapturedKey); + + AssignedTo.clear(); + if (HotkeyCommands.Is_Present(CapturedKey)) { + CommandClass const * const holder = HotkeyCommands[CapturedKey]; + if (holder != NULL && holder->Get_Display_Name() != NULL) { + AssignedTo = holder->Get_Display_Name(); + } + } + return; + } + + if (intent.Action == UI_KEYBOARD_ASSIGN) { + Apply_Hotkey(); + Show_Command(); + return; + } + + if (intent.Action == UI_KEYBOARD_RESET) { + // The second argument is the default response, a button position rather than a + // control identifier, and the dialog passed one: Enter answers No. + if (WWMessageBox()._Process(TXT_RESET_HOTKEYS, 1, TXT_YES, TXT_NO, TXT_NONE, false) == 0) { + Reset_All(); + } + return; + } + + UIResult result; + + if (intent.Action == UI_KEYBOARD_ACCEPT) { + Save_Assignments(); + Choice = CHOICE_ACCEPT; + result.Outcome = UIResult::OUTCOME_ACCEPTED; + + } else if (intent.Action == UI_KEYBOARD_CANCEL) { + // Every assignment was made against the live index, so leaving without accepting + // has to put the file's assignments back. + Init_Hotkeys(); + Choice = CHOICE_CANCEL; + result.Outcome = UIResult::OUTCOME_CANCELLED; + + } else { + return; + } + + Result = result; +} + + +//--------------------------------------------------------------------------------------- +// The RmlUi view. +//--------------------------------------------------------------------------------------- + +/// +/// The RmlUi half of the keyboard screen. +/// +class KeyboardViewClass : public UIRmlViewClass +{ + public: + KeyboardViewClass(UIKeyboardPresenterClass & presenter) : + UIRmlViewClass(presenter, "keyboard.rml"), + Screen(presenter) + { + } + + virtual void Bind(Rml::DataModelConstructor & model) override; + virtual void Sync(void) override; + + private: + UIKeyboardPresenterClass & Screen; +}; + + +// Is this the virtual key of a modifier on its own? A hotkey control holds nothing while +// only modifiers are down and takes the binding when a real key arrives, so a modifier +// pressed by itself is not a capture. +static bool Is_Modifier_Key(int key) +{ + return(key == VK_SHIFT || key == VK_CONTROL || key == VK_MENU + || (key >= 0xA0 && key <= 0xA5)); +} + + +void KeyboardViewClass::Bind(Rml::DataModelConstructor & model) +{ + if (auto command = model.RegisterStruct()) { + command.RegisterMember("label", &UIKeyboardPresenterClass::CommandType::Label); + } + model.RegisterArray>(); + model.RegisterArray>(); + + model.Bind("categories", &Screen.Categories); + model.Bind("category", &Screen.SelectedCategory); + model.Bind("commands", &Screen.Commands); + model.Bind("selectedcommand", &Screen.SelectedCommand); + model.Bind("description", &Screen.Description); + model.Bind("shortcut", &Screen.CurrentShortcut); + model.Bind("capturedtext", &Screen.CapturedText); + model.Bind("assignedto", &Screen.AssignedTo); + + // The combo box is bound one way, as every form control in this family is, so the + // category the model holds cannot be re-queued as a change the player did not make. + model.BindEventCallback("choose", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + Rml::String const value = event.GetParameter("value", Rml::String()); + if (value.empty()) return; + + int const row = std::atoi(value.c_str()); + if (row == Screen.SelectedCategory) return; + + Screen.Queue(UIIntent{UI_KEYBOARD_CATEGORY, "", row}); + }); + + model.BindEventCallback("pick", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{UI_KEYBOARD_COMMAND, "", (int)arguments[0].Get()}); + }); + + // The capture control. It stands where msctls_hotkey32 stood, so it takes the key + // itself and leaves the keys IsDialogMessage took from that control alone: Escape and + // Enter still leave the screen and Tab still moves the focus. + model.BindEventCallback("capture", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + int const identifier = event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN); + + if (identifier == Rml::Input::KI_ESCAPE || identifier == Rml::Input::KI_RETURN + || identifier == Rml::Input::KI_NUMPADENTER || identifier == Rml::Input::KI_TAB) { + return; + } + + int const key = UI_Virtual_Key(identifier); + if (key == 0 || Is_Modifier_Key(key)) { + return; + } + + // The game's encoding is the virtual key with its modifier bits above it, and + // those bits are the HOTKEYF_ values the hotkey control reported byte for byte. + int encoded = key; + if (event.GetParameter("shift_key", false)) encoded |= WWKEY_SHIFT_BIT; + if (event.GetParameter("ctrl_key", false)) encoded |= WWKEY_CTRL_BIT; + if (event.GetParameter("alt_key", false)) encoded |= WWKEY_ALT_BIT; + + event.StopPropagation(); + Screen.Queue(UIIntent{UI_KEYBOARD_CAPTURE, "", encoded}); + }); + + model.BindEventCallback("press", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{arguments[0].Get(), "", 0}); + }); + + // Escape cancels and Enter accepts, which is what IsDialogMessage delivered to a dialog + // whose template names no default push button. + model.BindEventCallback("key", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + int const key = event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN); + if (key == Rml::Input::KI_ESCAPE) { + Screen.Queue(UIIntent{UI_KEYBOARD_CANCEL, "", 0}); + } else if (key == Rml::Input::KI_RETURN || key == Rml::Input::KI_NUMPADENTER) { + Screen.Queue(UIIntent{UI_KEYBOARD_ACCEPT, "", 0}); + } + }); +} + + +void KeyboardViewClass::Sync(void) +{ + if (!Model) return; + + Model.DirtyVariable("categories"); + Model.DirtyVariable("category"); + Model.DirtyVariable("commands"); + Model.DirtyVariable("selectedcommand"); + Model.DirtyVariable("description"); + Model.DirtyVariable("shortcut"); + Model.DirtyVariable("capturedtext"); + Model.DirtyVariable("assignedto"); +} + + +/// +/// Shows the keyboard screen and waits for the player to leave it. +/// +UIResult UI_Keyboard_Screen(UIKeyboardPresenterClass & presenter) +{ + KeyboardViewClass view(presenter); + + if (!view.Prepare(true)) { + UIResult result; + result.Outcome = UIResult::OUTCOME_FAILED_TO_OPEN; + return(result); + } + + UIResult const result = UI_Run_Modal(presenter, view); + view.Close(); + return(result); +} diff --git a/code/ui/uikeyboard.h b/code/ui/uikeyboard.h new file mode 100644 index 000000000..d54dc773a --- /dev/null +++ b/code/ui/uikeyboard.h @@ -0,0 +1,96 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The keyboard screen's behavior, with no toolkit in it. The screen rebinds the game's +// commands, so what it holds is a staged view of HotkeyCommands: assignments are made +// against the live index as the player works and are written to KEYBOARD.INI only on +// accept, while cancel throws the index away and loads the file again. +// +// The captured key is a plain integer in the game's own encoding, low byte the virtual key +// and the shift, control and alt bits above it, which is the encoding HotkeyCommands is +// indexed by. A view decides how to capture one; this class never sees the keypress. +// +// docs/UI_DESIGN.md, "Screens", owns the contract. + +#pragma once + +#include "uiscreen.h" + +#include +#include + + +inline constexpr char const * UI_KEYBOARD_CATEGORY = "category"; // Value: row +inline constexpr char const * UI_KEYBOARD_COMMAND = "command"; // Value: row +inline constexpr char const * UI_KEYBOARD_CAPTURE = "capture"; // Value: encoded key +inline constexpr char const * UI_KEYBOARD_ASSIGN = "assign"; +inline constexpr char const * UI_KEYBOARD_RESET = "reset"; +inline constexpr char const * UI_KEYBOARD_ACCEPT = "accept"; +inline constexpr char const * UI_KEYBOARD_CANCEL = "cancel"; + + +class UIKeyboardPresenterClass : public UIPresenterClass +{ + public: + // A command as the screen shows it. The unique name is the identity an intent is + // resolved against, because the command list is rebuilt whenever the category + // changes and a row number outlives nothing. + struct CommandType + { + std::string Label; + std::string Description; + std::string UniqueName; + }; + + enum ChoiceType { + CHOICE_NONE, + CHOICE_ACCEPT, + CHOICE_CANCEL, + }; + + virtual void Execute(UIIntent const & intent) override; + virtual void Refresh(void) override; + virtual void Service(void) override; + + // Spells a key out the way the player's own keyboard layout names it, modifiers + // first. Empty for a key of zero, which is what an unbound command carries. + static std::string Key_Name(int key); + + /* + ** The view-model. Plain values, and the only thing a view reads. + */ + std::vector Categories; + int SelectedCategory = -1; + + std::vector Commands; + int SelectedCommand = -1; + + // The selected command's description and the key it answers to now. + std::string Description; + std::string CurrentShortcut; + + // What the capture control is holding, and the command that key already belongs to. + int CapturedKey = 0; + std::string CapturedText; + std::string AssignedTo; + + ChoiceType Choice = CHOICE_NONE; + + private: + void Fill_Commands(void); + void Show_Command(void); + void Apply_Hotkey(void); + void Reset_All(void); + void Save_Assignments(void) const; +}; + + +// Shows the screen through its RmlUi view. FAILED_TO_OPEN leaves nothing shown and the +// caller falls through to the legacy dialog. +UIResult UI_Keyboard_Screen(UIKeyboardPresenterClass & presenter); diff --git a/code/ui/uilobby.cpp b/code/ui/uilobby.cpp new file mode 100644 index 000000000..0839bc82f --- /dev/null +++ b/code/ui/uilobby.cpp @@ -0,0 +1,1547 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The network lobby. Behavior traced out of MPlayer_Game_List_Dialog_Proc, +// MPlayer_Host_Dialog_Proc, MPlayer_Guest_Dialog_Proc, Net2DisplayGameList, +// _Net2DisplayUsers and Net2Remote_Connect in netdlg2.cpp. +// +// What the extraction fixes in place: the rosters are read into the model where the +// session changes rather than where a list is drawn, so a presentation that draws a +// different number of times cannot lose a change or repeat one; the host's accepted +// status is a fact about the player, so it is recorded with the roster rather than while +// painting a row; a game row carries whether the game is open rather than the bracketed +// caption; and the selection is clamped against a list a host can shorten at any moment, +// which is what the display function did before it drew. +// +// What the host's half fixes in place: a chat line is bounded into the packet field it is +// copied to rather than into a buffer the sender chose; the option track bars carry the +// ranges DisplayGameopts sets rather than a control's default; and the players the host has +// picked out to kick are resolved to names when the kick is executed rather than read back +// off a list box that the roster may have moved underneath. +// +// Packets are untouched. Nothing here changes what goes on the wire, only who owns the +// state the screens show. +// +// docs/UI_DESIGN.md, "Screens", owns the contracts this keeps to. + +#include "always.h" + +#include "uilobby.h" + +#include "uimappreview.h" +#include "uirmlview.h" + +#include "_rand.h" +#include "_rules.h" +#include "_timer.h" +#include "conquer.h" +#include "data.h" +#include "houstype.h" +#include "ipxmgr.h" +#include "language/language.h" +#include "mapgen.h" +#include "mplayer.h" +#include "netdlg.h" +#include "netdlg2.h" +#include "netdlg.h" +#include "netshare.h" +#include "preview.h" +#include "rules.h" +#include "dbgprint.h" +#include "session.h" +#include "utf8.h" + +#include +#include +#include +#include + +#include +#include +#include + + +// The least money a network game may be started with, which is where the credits track bar +// begins. DisplayGameopts names the same figure. +enum { MP_MIN_MONEY = 2500 }; + + +// The lobby the driver is running. A message produced away from a screen reaches the model +// through this, the way PMessagePrintf found the topmost dialog with somewhere to show one. +static UILobbyPresenterClass * _LobbyScreen = NULL; + + +UILobbyPresenterClass * UI_Lobby_Screen(void) +{ + return(_LobbyScreen); +} + + +void UI_Set_Lobby_Screen(UILobbyPresenterClass * screen) +{ + _LobbyScreen = screen; +} + + +void UILobbyPresenterClass::Refresh(void) +{ + Handle = Session.Handle; + Color = Session.ColorIdx; + + Build_Game_Rows(); + Build_User_Rows(); +} + + +/// +/// Puts the lobby's own rosters back the way the game list dialog created them: the +/// player's chat entry first, and an entry standing for the lobby itself at the head of +/// the game list. +/// +void UILobbyPresenterClass::Open(void) +{ + CurGame = 0; + Net2IsGameListActive = true; + + Handle = Session.Handle; + + Session.Options.ScenarioDescription[0] = '\0'; + Session.ColorIdx = Session.PrefColor; + Color = Session.ColorIdx; + + Clear_Vector(&Session.Games); + Clear_Vector(&Session.Players); + Clear_Vector(&Session.Chat); + + NodeNameType * const who = new NodeNameType; + strcpy(who->Name, Session.Handle); + who->Chat.LastTime = 0; + who->Chat.LastChance = 0; + who->Chat.Color = Session.GPacket.PlayerInfo.Color; + Session.Chat.Add(who); + + NodeNameType * const game = new NodeNameType; + strcpy(game->Name, ""); + game->Game.IsOpen = 0; + game->Game.LastTime = 0; + Session.Games.Add(game); + + Send_Join_Queries(true, false, true, true); + + Build_Game_Rows(); + Build_User_Rows(); +} + + +/// +/// Records where a guest stands as its screen opens: it has accepted nothing yet, and the +/// scenario description is cleared because the host has not sent one. +/// +void UILobbyPresenterClass::Open_Guest(void) +{ + Build_Identity_Lists(); + Read_Options(); + + CanAccept = false; + + for (int index = 0; index < Session.Players.Count(); index++) { + if (strcmp(Session.Players[index]->Name, Session.Handle) == 0) { + Session.Players[index]->Player.Status = 0; + } + } + + Session.Options.ScenarioDescription[0] = '\0'; + ScenarioName.clear(); + + Rebuild_Network_Map_Preview(); + PreviewGeneration++; + + Build_User_Rows(); +} + + +/// +/// The host's settings have arrived and been written to the session. +/// The model is read back where they landed rather than where a control is written, which is +/// the same split Fill_List took: a presentation that draws a different number of times +/// cannot lose a change or repeat one. +/// +void UILobbyPresenterClass::Options_Received(void) +{ + Read_Options(); + PreviewGeneration++; + Build_User_Rows(); +} + + +/// +/// Builds the country and color lists both setup screens show, and picks out the ones this +/// player is wearing. A side row carries the country it stands for rather than its position, +/// because the list holds only the countries that may be played. +/// +void UILobbyPresenterClass::Build_Identity_Lists(void) +{ + Sides.clear(); + SelectedSide = 0; + for (int index = 0; index < HouseTypes.Count(); index++) { + HouseTypeClass const * const house = HouseTypes[index]; + if (!house->IsMultiplay) continue; + + if (index == Session.House) { + SelectedSide = (int)Sides.size(); + } + Sides.push_back(SideType{(char const *)house->GivenName, index}); + } + + Colors.clear(); + Colors.push_back(Fetch_String(TXT_GOLD)); + Colors.push_back(Fetch_String(TXT_RED)); + Colors.push_back(Fetch_String(TXT_BLUE)); + Colors.push_back(Fetch_String(TXT_GREEN)); + Colors.push_back(Fetch_String(TXT_ORANGE)); + Colors.push_back(Fetch_String(TXT_SKY_BLUE)); + Colors.push_back(Fetch_String(TXT_PURPLE)); + Colors.push_back(Fetch_String(TXT_PINK)); + + House = Session.House; + Color = Session.ColorIdx; +} + + +/// +/// Reads the session's game options into the view-model, with the ranges the rules give +/// them. The ranges are the ones DisplayGameopts puts on the track bars on its initializing +/// pass, and they belong with the values because a range control clamps a value into the +/// range it is holding. +/// +void UILobbyPresenterClass::Read_Options(void) +{ + UnitCount = SliderType{Session.Options.UnitCount, 1, 10, 1}; + TechLevel = SliderType{BuildLevel, 1, MPLAYER_BUILD_LEVEL_MAX, 1}; + Credits = SliderType{Session.Options.Credits, MP_MIN_MONEY, Rule->MPMaxMoney, 100}; + AIPlayers = SliderType{Session.Options.AIPlayers, 0, 6, 1}; + AILevel = SliderType{(int)Session.Options.AIDifficulty, 0, 2, 1}; + + // The track bar runs the other way round from the setting: its left end is the slowest + // game, and the dialog turned one into the other at both ends. + GameSpeed = SliderType{6 - Session.Options.GameSpeed, 0, 6, 1}; + + Bases = Session.Options.Bases; + Crates = Session.Options.Goodies; + FogOfWar = Session.Options.FogOfWar; + Bridges = Session.Options.BridgeDestruction; + MCVRedeploy = Session.Options.MCVRedeploy; + ShortGame = Session.Options.ShortGame; + MultiEngineer = Session.Options.CrapEngineers; + Allies = Session.Options.AlliesAllowed; + HarvTruce = Session.Options.HarvTruce; + + ScenarioName = Session.Options.ScenarioDescription; + + OptionsChanged = true; +} + + +/// +/// The game the host has just created. The setup opens on the first scenario whatever the +/// session was carrying, seeds the match, and tells the guests what this player is wearing, +/// which is what the dialog did by sending itself its own two selection changes. +/// +void UILobbyPresenterClass::Open_Host(void) +{ + VerNum.Init_Clipping(); + + srand(NonCriticalRandomNumber(1, 0x7FFF)); + Seed = rand(); + + Set_Scenario_Info_From_Index(0); + Session.Options.ScenarioIndex = 0; + + Build_Identity_Lists(); + Read_Options(); + + Rebuild_Network_Map_Preview(); + PreviewGeneration++; + + CanStart = true; + + Host_Side(SelectedSide); + Host_Color(Color); +} + + +/// +/// Records the country the host is showing and tells the guests about it. +/// +void UILobbyPresenterClass::Host_Side(int row) +{ + if (row < 0 || row >= (int)Sides.size()) { + return; + } + + SelectedSide = row; + House = Sides[row].Country; + Session.House = (HousesType)House; + + if (Session.Players.Count() > 0) { + Session.Players[0]->Player.House = Session.House; + } + + PumpGameopts(1, 0); + Build_User_Rows(); +} + + +/// +/// Records the color the host asked for, resolved against the colors already taken. +/// A color somebody else is wearing is bumped forward until a free one is found; if that +/// moved the color away from what the host asked for, the host is told so and the search +/// runs again from the color it was wearing before. +/// +void UILobbyPresenterClass::Host_Color(int color) +{ + if (color < 0 || color >= (int)Colors.size()) { + return; + } + + int old_color = Session.ColorIdx; + + Session.ColorIdx = color; + Session.PrefColor = Session.ColorIdx; + + int newcolor; + int found; + int probe = Session.ColorIdx; + for (;;) { + int count = 0; + newcolor = probe; + found = false; + + while (count < Session.Players.Count()) { + if (count != 0 && Session.Players[count]->Player.Color == probe) { + probe++; + found = true; + } + count++; + } + + if (!found) break; + + probe %= MAX_MPLAYER_COLORS; + } + + int resolved = newcolor; + if (newcolor != Session.ColorIdx) { + Record_Message(ColorSystem, Fetch_String(TXT_COLOR_IN_USE)); + + probe = old_color; + for (;;) { + int count = 0; + old_color = probe; + found = false; + + while (count < Session.Players.Count()) { + if (count != 0 && Session.Players[count]->Player.Color == probe) { + probe++; + found = true; + } + count++; + } + + if (!found) break; + + probe %= MAX_MPLAYER_COLORS; + } + + resolved = old_color; + } + + Session.ColorIdx = resolved; + Color = resolved; + + if (Session.Players.Count() > 0) { + Session.Players[0]->Player.Color = resolved; + } + + Build_User_Rows(); + PumpGameopts(1, 0); +} + + +/// +/// Turns a game option on or off. Short Game needs bases, so turning bases off turns the +/// short game off with it and turning the short game on turns bases on. +/// +void UILobbyPresenterClass::Toggle(std::string const & which) +{ + if (which == UI_LOBBY_BASES) { + Bases = !Bases; + if (!Bases) ShortGame = false; + } else if (which == UI_LOBBY_SHORTGAME) { + ShortGame = !ShortGame; + if (ShortGame) Bases = true; + } else if (which == UI_LOBBY_CRATES) { + Crates = !Crates; + } else if (which == UI_LOBBY_FOG) { + FogOfWar = !FogOfWar; + } else if (which == UI_LOBBY_BRIDGES) { + Bridges = !Bridges; + } else if (which == UI_LOBBY_MCV) { + MCVRedeploy = !MCVRedeploy; + } else if (which == UI_LOBBY_ENGINEER) { + MultiEngineer = !MultiEngineer; + } else if (which == UI_LOBBY_ALLIES) { + Allies = !Allies; + } else if (which == UI_LOBBY_HARVTRUCE) { + HarvTruce = !HarvTruce; + } else { + return; + } + + Session.Options.Bases = Bases; + Session.Options.ShortGame = ShortGame; + Session.Options.Goodies = Crates; + Session.Options.FogOfWar = FogOfWar; + Session.Options.BridgeDestruction = Bridges; + Session.Options.MCVRedeploy = MCVRedeploy; + Session.Options.CrapEngineers = MultiEngineer; + Session.Options.AlliesAllowed = Allies; + Session.Options.HarvTruce = HarvTruce; + + OptionsChanged = true; +} + + +/// +/// Moves a game option's track bar. The dialog re-read every bar on each notification, so +/// the whole set is written through rather than the one that moved. +/// +void UILobbyPresenterClass::Slide(std::string const & which, int value) +{ + SliderType * slider = NULL; + if (which == UI_LOBBY_UNITCOUNT) slider = &UnitCount; + else if (which == UI_LOBBY_CREDITS) slider = &Credits; + else if (which == UI_LOBBY_TECHLEVEL) slider = &TechLevel; + else if (which == UI_LOBBY_AILEVEL) slider = &AILevel; + else if (which == UI_LOBBY_AIPLAYERS) slider = &AIPlayers; + else if (which == UI_LOBBY_GAMESPEED) slider = &GameSpeed; + + if (slider == NULL) { + return; + } + + if (value < slider->Minimum) value = slider->Minimum; + if (value > slider->Maximum) value = slider->Maximum; + slider->Value = value; + + Session.Options.UnitCount = UnitCount.Value; + BuildLevel = TechLevel.Value; + Session.Options.Credits = Credits.Value; + Session.Options.AIPlayers = AIPlayers.Value; + Session.Options.AIDifficulty = (DiffType)AILevel.Value; + Session.Options.GameSpeed = 6 - GameSpeed.Value; +} + + +/// +/// Throws the picked players out of the game. +/// The rows are resolved to names here rather than when they were picked, because the roster +/// can move underneath a selection at any moment, and the host cannot kick itself. +/// +void UILobbyPresenterClass::Kick(void) +{ + for (int const row : PickedUsers) { + if (row < 0 || row >= (int)Users.size()) { + continue; + } + + std::string const & name = Users[row].Name; + if (name == Session.Handle) { + continue; + } + + int index = -1; + for (int i = 0; i < Session.Players.Count(); i++) { + if (name == Session.Players[i]->Name) { + index = i; + break; + } + } + + if (index == -1) { + continue; + } + + memset(&Session.GPacket, 0, sizeof(Session.GPacket)); + Session.GPacket.Command = NET_REJECT_JOIN; + Session.GPacket.Reject.Why = (int)REJECT_BY_OWNER; + Ipx.Send_Global_Message(&Session.GPacket, 455, 1, &Session.Players[index]->Address); + } + + PickedUsers.clear(); + Build_User_Rows(); +} + + +/// +/// Runs the scenario picker with the host screen out of the way, and puts the map it chose +/// on the model. Backing out leaves the scenario the screen was showing, which is what +/// putting the old index back did. +/// +void UILobbyPresenterClass::Run_Pending(void) +{ + if (Pending != SUB_PICK_MAP) { + return; + } + + Pending = SUB_NONE; + + int const previous = Session.Options.ScenarioIndex; + + IsRandomMap = false; + bool const picked = Pick_Scenario_Screen(); + IsRandomMap = true; + + if (!picked) { + Session.Options.ScenarioIndex = previous; + Set_Scenario_Info_From_Index(previous); + + // Backing out puts the settings back on the wire, which is what the cancel arm did + // and the accept arm left to the driver's own pump. + PumpGameopts(1, 0); + } else if (Set_Scenario_Info_From_Index(Session.Options.ScenarioIndex) != true) { + Session.Options.ScenarioIndex = previous; + Set_Scenario_Info_From_Index(previous); + } + + ScenarioName = Session.Options.ScenarioDescription; + + // A generated map has a picture of its own beside it rather than one read out of the map. + int const index = Session.Options.ScenarioIndex; + if (index >= 0 && index < Session.Scenarios.Count() + && stricmp(Session.Scenarios[index]->Get_Filename(), RANDOM_MAP_FILE_NAME) == 0) { + delete MultiplayerMapPreview; + MultiplayerMapPreview = new MapPreviewClass; + MultiplayerMapPreview->Read_PCX_Preview("RandMap.img"); + if (MultiplayerMapPreview->Get_Preview_Surface() == NULL) { + Rebuild_Network_Map_Preview(); + } + } else { + Rebuild_Network_Map_Preview(); + } + + PreviewGeneration++; + OptionsChanged = true; +} + + +/// +/// Records a line of chat or system text for whatever is showing the lobby. +/// The line is kept whole; breaking it to the width it is shown at belongs to the +/// presentation, which is what _DrawMessage did with the list box it was handed. +/// +void UILobbyPresenterClass::Record_Message(int color, char const * text) +{ + if (text == NULL) { + return; + } + + Messages.push_back(ChatLineType{text, color}); + if ((int)Messages.size() > MESSAGE_LIMIT) { + Messages.erase(Messages.begin()); + } + + MessagesChanged = true; +} + + +/// +/// Tells the game that this player has accepted the host's settings. +/// +void UILobbyPresenterClass::Accept(void) +{ + if (Session.Players.Count() == 0) { + return; + } + + Session.Players[0]->Player.Status = 1; + CanAccept = false; + + SendPublicGameopts("A1"); + + Build_User_Rows(); +} + + +/// +/// Tells the host which country and color this player is showing. The country is whatever +/// the model is already holding, because the side is recorded by its own intent ahead of +/// this one, the way the dialog read both of its boxes before sending one packet. +/// +void UILobbyPresenterClass::Change_Identity(int color) +{ + Color = color; + Session.PrefColor = color; + + char options[64]; + std::snprintf(options, sizeof(options), "R%d,%d", House, color); + SendPrivateGameopts(Session.GameName, options); +} + + +/// +/// Reads the advertised games into the view-model and clamps the selection. +/// A game can vanish while the list is open, so a selection past the end is pulled back +/// and the queries are asked again for the game the selection landed on. +/// +void UILobbyPresenterClass::Build_Game_Rows(void) +{ + int const count = Session.Games.Count(); + + if (CurGame >= count) { + CurGame = count - 1; + Send_Join_Queries(0, 1, 0, 0); + } + if (CurGame < 0) { + CurGame = 0; + } + + Games.clear(); + + GameRowType lobby; + lobby.Label = Fetch_String(TXT_LOBBY); + lobby.IsOpen = false; + Games.push_back(lobby); + + for (int index = 1; index < Session.Games.Count(); index++) { + NodeNameType const * const node = Session.Games[index]; + + // The name is the node's first member, which is what the caption format has always + // been handed. A row carries the name and whether the game is open; the bracketed + // caption belongs to the presentation. + GameRowType row; + row.Label = node->Name; + row.IsOpen = node->Game.IsOpen != 0; + Games.push_back(row); + } + + SelectedGame = CurGame; + GamesChanged = true; +} + + +/// +/// Reads the chat roster, or the selected game's player roster, into the view-model. +/// Out in the lobby the rows are the handles of everybody chatting. Inside a game each row +/// carries the player's color, the side its emblem stands for, and whether the player is +/// the host or has accepted the settings. +/// +void UILobbyPresenterClass::Build_User_Rows(void) +{ + Users.clear(); + + if (CurGame == 0) { + UserRowType me; + me.Name = Session.Handle; + Users.push_back(me); + + for (int index = 1; index < Session.Chat.Count(); index++) { + UserRowType row; + row.Name = Session.Chat[index]->Name; + Users.push_back(row); + } + + UsersChanged = true; + return; + } + + for (int index = 0; index < Session.Players.Count(); index++) { + NodeNameType * const node = Session.Players[index]; + + // The host counts as having accepted its own settings, which the list recorded on + // the player rather than on the row it was about to draw. + bool const host = strcmp(node->Name, Session.GameName) == 0; + if (host) { + node->Player.Status = 1; + } + + UserRowType row; + row.Name = node->Name; + row.Color = node->Player.Color; + row.IsHost = host; + row.HasAccepted = node->Player.Status != 0; + + int const country = node->Player.House; + row.Side = (country >= HOUSE_FIRST && country < HouseTypes.Count()) + ? (int)HouseTypes[country]->Side : (int)SIDE_NONE; + + if (row.Side == SIDE_GDI) { + row.SideName = Fetch_String(TXT_GDI); + } else if (row.Side == SIDE_NOD || row.Side == SIDE_NONE) { + row.SideName = Fetch_String(TXT_NOD); + } else { + row.SideName = (char const *)HouseTypes[country]->GivenName; + } + + Users.push_back(row); + } + + UsersChanged = true; +} + + +/// +/// One pass of the maintenance the lobby's driver ran on every turn of its own loop: the +/// transport is serviced, the join protocol is answered, the host's options are broadcast if +/// they moved, and a game or a chat partner that has stopped answering is dropped. +/// +void UILobbyPresenterClass::Service(void) +{ + Net2ServiceLobby(); +} + + +/// +/// Records the name the player is showing and tells the lobby about it. +/// The name is truncated to what the session's buffer holds, and the model carries the +/// truncated name back so the field shows what was actually kept. +/// +void UILobbyPresenterClass::Rename(std::string const & name) +{ + if (name == Session.Handle) { + return; + } + + UTF8::Copy(Session.Handle, sizeof(Session.Handle), name.c_str()); + Handle = Session.Handle; + + Send_Join_Queries(0, 0, 1, 0); + Build_User_Rows(); +} + + +/// +/// Moves the highlight to another advertised game and asks that game who is in it. +/// A player already joined to a game cannot browse away from it, which is what the list +/// refused to do once JoinState left JOIN_NOTHING. +/// +void UILobbyPresenterClass::Pick_Game(int row) +{ + if (JoinState > JOIN_NOTHING) { + return; + } + if (row < 0 || row >= Session.Games.Count() || !Net2IsGameListActive) { + return; + } + + int const previous = CurGame; + + CurGame = row; + strcpy(Session.GameName, Session.Games[row]->Name); + SelectedGame = CurGame; + + Clear_Vector(&Session.Players); + + if (previous != CurGame) { + Send_Join_Queries(1, 1, 1, 0); + } + + Build_User_Rows(); +} + + +/// +/// Sends a line of chat to whoever is listening, which is the game's players once joined +/// and the lobby's chat roster otherwise. A line of two characters or fewer is dropped, +/// which is what the edit control's own handler did. +/// +void UILobbyPresenterClass::Say(std::string const & text) +{ + if (text.size() <= 2) { + return; + } + + PMessagePrintf(ColorMe, "[%s] %s", Session.Handle, text.c_str()); + + GlobalPacketType gpacket; + memset(&gpacket, 0, sizeof(gpacket)); + + gpacket.Command = NET_MESSAGE; + strcpy(gpacket.Name, Session.Handle); + std::snprintf(gpacket.Message.Buf, sizeof(gpacket.Message.Buf), "%s", text.c_str()); + gpacket.Message.Color = Session.ColorIdx; + gpacket.Message.NameCRC = Compute_Name_CRC(Session.GameName); + + DynamicVectorClass & who = + JoinState == JOIN_CONFIRMED ? Session.Players : Session.Chat; + + for (int index = 1; index < who.Count(); index++) { + Ipx.Send_Global_Message(&gpacket, sizeof(gpacket), 1, &who[index]->Address); + Call_Back(); + } +} + + +/// +/// Records what the driver loop is being asked to do next, and ends the pass. +/// The answer is the screen's result as well, because the runner returns on a result and the +/// driver reads the answer after it does; the family's own loop clears both before it shows +/// the next screen. +/// +void UILobbyPresenterClass::Answer(ResponseType response) +{ + Response = response; + + UIResult result; + result.Outcome = response == RESPONSE_CANCEL + ? UIResult::OUTCOME_CANCELLED : UIResult::OUTCOME_ACCEPTED; + Result = result; +} + + +void UILobbyPresenterClass::Execute(UIIntent const & intent) +{ + if (intent.Action == UI_LOBBY_RENAME) { + Rename(intent.Identity); + return; + } + + if (intent.Action == UI_LOBBY_PICK_GAME) { + Pick_Game(intent.Value); + return; + } + + if (intent.Action == UI_LOBBY_SAY) { + Say(intent.Identity); + return; + } + + if (intent.Action == UI_LOBBY_COLOR) { + Session.ColorIdx = intent.Value; + Color = intent.Value; + return; + } + + if (intent.Action == UI_LOBBY_SIDE) { + House = intent.Value; + return; + } + + if (intent.Action == UI_LOBBY_TOGGLE) { + Toggle(intent.Identity); + return; + } + + if (intent.Action == UI_LOBBY_SLIDER) { + Slide(intent.Identity, intent.Value); + return; + } + + if (intent.Action == UI_LOBBY_PICK_USER) { + // A picked row is added rather than replacing the selection, because the player list + // is a multiple-selection one and a second click takes a row back off it. + auto const found = std::find(PickedUsers.begin(), PickedUsers.end(), intent.Value); + if (found != PickedUsers.end()) { + PickedUsers.erase(found); + } else if (intent.Value >= 0 && intent.Value < (int)Users.size()) { + PickedUsers.push_back(intent.Value); + } + UsersChanged = true; + return; + } + + if (intent.Action == UI_LOBBY_KICK) { + Kick(); + return; + } + + if (intent.Action == UI_LOBBY_PICK_MAP) { + Pending = SUB_PICK_MAP; + return; + } + + if (intent.Action == UI_LOBBY_GO) { + // The button goes away until the driver has decided the game may begin, which is what + // disabling the window stood for; the driver puts it back when it refuses. + CanStart = false; + Answer(RESPONSE_GO); + return; + } + + if (intent.Action == UI_LOBBY_IDENTITY) { + Change_Identity(intent.Value); + return; + } + + if (intent.Action == UI_LOBBY_HOST_SIDE) { + Host_Side(intent.Value); + return; + } + + if (intent.Action == UI_LOBBY_HOST_COLOR) { + Host_Color(intent.Value); + return; + } + + if (intent.Action == UI_LOBBY_ACCEPT) { + Accept(); + return; + } + + if (intent.Action == UI_LOBBY_JOIN) { + Answer(RESPONSE_JOIN); + return; + } + + if (intent.Action == UI_LOBBY_NEW) { + Answer(RESPONSE_NEW); + return; + } + + if (intent.Action == UI_LOBBY_CANCEL) { + Answer(RESPONSE_CANCEL); + return; + } +} + + +//--------------------------------------------------------------------------------------- +// The RmlUi views. +//--------------------------------------------------------------------------------------- + +// The picture the preview frame holds, in game logical units. The frame is the setup +// templates' 126 x 73 dialog units, which is 189 by 118.625 at the family's 1.5 across and +// 1.625 down, and the picture sits inside its one pixel border. +enum { PREVIEW_WIDTH = 187, PREVIEW_HEIGHT = 116 }; + +inline constexpr char const * UI_LOBBY_HOST_PREVIEW = "lobbyhostpreview"; +inline constexpr char const * UI_LOBBY_GUEST_PREVIEW = "lobbyguestpreview"; + + +/// +/// The RmlUi half of one of the lobby's three screens. +/// The three documents show overlapping halves of one model, so one view serves them all +/// and binds only what its own document names. Each names its data model after its own +/// file, which is what the base class derives the name from; a document that names another +/// document's model gets no bindings and no events at all. +/// +class LobbyViewClass : public UIRmlViewClass +{ + public: + // A color a player may take, with the swatch the owner-draw combo drew its row in. + struct ColorRowType + { + std::string Name; + std::string Hex; + }; + + // A player row as its document shows it: the model's row plus the color the name is + // drawn in, the marker the list drew as a surface, and whether the host has picked + // the row out to kick. + struct UserViewType + { + std::string Name; + std::string SideName; + std::string Mark; + std::string Hex; + bool Picked = false; + }; + + struct MessageViewType + { + std::string Text; + std::string Hex; + }; + + LobbyViewClass(UILobbyPresenterClass & presenter, char const * document, + UILobbyPresenterClass::ScreenType kind, char const * preview); + virtual ~LobbyViewClass(void) override; + + virtual void Bind(Rml::DataModelConstructor & model) override; + virtual void Sync(void) override; + + // Puts the track bar ranges the rules give this screen on the controls, and lets the + // change handlers start reporting. A range is set before the data binding fills a + // value in, so a value outside a track bar's default range is not clamped away. + void Settle(void); + + UILobbyPresenterClass::ScreenType Kind; + + private: + void Move(char const * which, int value); + void Press(char const * action); + void Set_Range(char const * id, UILobbyPresenterClass::SliderType const & slider); + void Submit_Chat(void); + void Rebuild_Rows(void); + + static std::string Swatch(int color); + + UILobbyPresenterClass & Screen; + + // The view owns the pixels; the presenter carries only the name they answer to. + std::string Preview; + MapPreviewSurfaceClass Picture{PREVIEW_WIDTH, PREVIEW_HEIGHT}; + + std::vector ColorRows; + std::vector UserRows; + std::vector GameRows; + std::vector MessageRows; + + unsigned int Drawn = 0; + bool Settled = false; +}; + + +LobbyViewClass::LobbyViewClass(UILobbyPresenterClass & presenter, char const * document, + UILobbyPresenterClass::ScreenType kind, char const * preview) : + UIRmlViewClass(presenter, document), + Kind(kind), + Screen(presenter), + Preview(preview != NULL ? preview : "") +{ + if (!Preview.empty()) { + UI_Register_Surface(Preview.c_str(), &Picture); + } +} + + +LobbyViewClass::~LobbyViewClass(void) +{ + if (!Preview.empty()) { + UI_Unregister_Surface(Preview.c_str()); + } +} + + +/// +/// Turns a player color into the CSS color the owner-draw list drew its row in, which is +/// what OD_SETCOLOR was handed out of PlayerColorTable. +/// +std::string LobbyViewClass::Swatch(int color) +{ + char hex[8]; + if (color >= 0 && color < MAX_PLAYERS) { + // A COLORREF holds its blue byte highest, which is the order RGB() packs. + unsigned long const packed = (unsigned long)PlayerColorTable[color]; + std::snprintf(hex, sizeof(hex), "#%02x%02x%02x", + (unsigned)(packed & 0xFF), (unsigned)((packed >> 8) & 0xFF), (unsigned)((packed >> 16) & 0xFF)); + } else { + std::snprintf(hex, sizeof(hex), "#b9bcae"); + } + return(hex); +} + + +void LobbyViewClass::Set_Range(char const * id, UILobbyPresenterClass::SliderType const & slider) +{ + if (Element == nullptr) { + return; + } + + Rml::Element * const control = Element->GetElementById(id); + if (control == nullptr) { + return; + } + + // The range is set before the value, because a track bar clamps a value into the range + // it is holding and the default range stops well short of what the rules allow. + control->SetAttribute("min", slider.Minimum); + control->SetAttribute("max", slider.Maximum); + control->SetAttribute("step", slider.Step); + control->SetAttribute("value", slider.Value); +} + + +void LobbyViewClass::Settle(void) +{ + if (Kind != UILobbyPresenterClass::SCREEN_GAME_LIST) { + Set_Range("unitcount", Screen.UnitCount); + Set_Range("credits", Screen.Credits); + Set_Range("techlevel", Screen.TechLevel); + Set_Range("ailevel", Screen.AILevel); + Set_Range("aiplayers", Screen.AIPlayers); + Set_Range("gamespeed", Screen.GameSpeed); + } + + Settled = true; +} + + +/// +/// Reads the chat entry and queues what was typed, then empties the field, which is what the +/// edit control's own handler did once its text had been taken. +/// +void LobbyViewClass::Submit_Chat(void) +{ + if (Element == nullptr) { + return; + } + + Rml::ElementFormControlInput * const field = + rmlui_dynamic_cast(Element->GetElementById("say")); + if (field == nullptr) { + return; + } + + Rml::String const text = field->GetValue(); + field->SetValue(""); + + if (text.empty()) { + return; + } + + Screen.Queue(UIIntent{UI_LOBBY_SAY, text, 0}); +} + + +void LobbyViewClass::Move(char const * which, int value) +{ + if (!Settled) return; + + UILobbyPresenterClass::SliderType const * held = NULL; + if (which == UI_LOBBY_UNITCOUNT) held = &Screen.UnitCount; + else if (which == UI_LOBBY_CREDITS) held = &Screen.Credits; + else if (which == UI_LOBBY_TECHLEVEL) held = &Screen.TechLevel; + else if (which == UI_LOBBY_AILEVEL) held = &Screen.AILevel; + else if (which == UI_LOBBY_AIPLAYERS) held = &Screen.AIPlayers; + else if (which == UI_LOBBY_GAMESPEED) held = &Screen.GameSpeed; + + if (held == NULL || held->Value == value) { + return; + } + + Screen.Queue(UIIntent{UI_LOBBY_SLIDER, which, value}); +} + + +/// +/// Queues what a button or its key stands for. The game list's name field is read here +/// rather than tracked, because that is when the dialog read its edit control. +/// +void LobbyViewClass::Press(char const * action) +{ + if (Kind == UILobbyPresenterClass::SCREEN_GAME_LIST && Element != nullptr) { + Rml::ElementFormControlInput * const field = + rmlui_dynamic_cast(Element->GetElementById("yourname")); + if (field != nullptr) { + Rml::String const text = field->GetValue(); + if (text != Screen.Handle) { + Screen.Queue(UIIntent{UI_LOBBY_RENAME, text, 0}); + } + } + } + + Screen.Queue(UIIntent{action, "", 0}); +} + + +void LobbyViewClass::Rebuild_Rows(void) +{ + GameRows = Screen.Games; + + UserRows.clear(); + for (int index = 0; index < (int)Screen.Users.size(); index++) { + UILobbyPresenterClass::UserRowType const & row = Screen.Users[index]; + + UserViewType view; + view.Name = row.Name; + view.SideName = row.SideName; + view.Hex = Swatch(row.Color); + + // The host and accepted markers, which the list drew as the wolhost.pcx and + // wolacpt.pcx surfaces. + if (row.IsHost) { + view.Mark = "*"; + } else if (row.HasAccepted) { + view.Mark = "+"; + } + + view.Picked = std::find(Screen.PickedUsers.begin(), Screen.PickedUsers.end(), index) + != Screen.PickedUsers.end(); + + UserRows.push_back(view); + } + + MessageRows.clear(); + for (UILobbyPresenterClass::ChatLineType const & line : Screen.Messages) { + MessageViewType view; + view.Text = line.Text; + + if (line.Color < 0) { + view.Hex = "#b9bcae"; + } else { + unsigned long const packed = (unsigned long)line.Color; + char hex[8]; + std::snprintf(hex, sizeof(hex), "#%02x%02x%02x", + (unsigned)(packed & 0xFF), (unsigned)((packed >> 8) & 0xFF), (unsigned)((packed >> 16) & 0xFF)); + view.Hex = hex; + } + + MessageRows.push_back(view); + } +} + + +void LobbyViewClass::Bind(Rml::DataModelConstructor & model) +{ + ColorRows.clear(); + for (int index = 0; index < (int)Screen.Colors.size(); index++) { + ColorRows.push_back(ColorRowType{Screen.Colors[index], Swatch(index)}); + } + + Rebuild_Rows(); + + if (auto row = model.RegisterStruct()) { + row.RegisterMember("label", &UILobbyPresenterClass::GameRowType::Label); + row.RegisterMember("isopen", &UILobbyPresenterClass::GameRowType::IsOpen); + } + model.RegisterArray>(); + + if (auto row = model.RegisterStruct()) { + row.RegisterMember("name", &UserViewType::Name); + row.RegisterMember("sidename", &UserViewType::SideName); + row.RegisterMember("mark", &UserViewType::Mark); + row.RegisterMember("hex", &UserViewType::Hex); + row.RegisterMember("picked", &UserViewType::Picked); + } + model.RegisterArray>(); + + if (auto row = model.RegisterStruct()) { + row.RegisterMember("text", &MessageViewType::Text); + row.RegisterMember("hex", &MessageViewType::Hex); + } + model.RegisterArray>(); + + if (auto swatch = model.RegisterStruct()) { + swatch.RegisterMember("name", &ColorRowType::Name); + swatch.RegisterMember("hex", &ColorRowType::Hex); + } + model.RegisterArray>(); + + if (auto side = model.RegisterStruct()) { + side.RegisterMember("name", &UILobbyPresenterClass::SideType::Name); + } + model.RegisterArray>(); + + if (auto slider = model.RegisterStruct()) { + slider.RegisterMember("value", &UILobbyPresenterClass::SliderType::Value); + slider.RegisterMember("min", &UILobbyPresenterClass::SliderType::Minimum); + slider.RegisterMember("max", &UILobbyPresenterClass::SliderType::Maximum); + slider.RegisterMember("step", &UILobbyPresenterClass::SliderType::Step); + } + + model.Bind("handle", &Screen.Handle); + model.Bind("games", &GameRows); + model.Bind("selectedgame", &Screen.SelectedGame); + model.Bind("users", &UserRows); + model.Bind("messages", &MessageRows); + + model.Bind("sides", &Screen.Sides); + model.Bind("selectedside", &Screen.SelectedSide); + model.Bind("colors", &ColorRows); + model.Bind("selectedcolor", &Screen.Color); + + model.Bind("scenarioname", &Screen.ScenarioName); + model.Bind("preview", &Preview); + + model.Bind("unitcount", &Screen.UnitCount); + model.Bind("credits", &Screen.Credits); + model.Bind("techlevel", &Screen.TechLevel); + model.Bind("ailevel", &Screen.AILevel); + model.Bind("aiplayers", &Screen.AIPlayers); + model.Bind("gamespeed", &Screen.GameSpeed); + + model.Bind("bases", &Screen.Bases); + model.Bind("crates", &Screen.Crates); + model.Bind("fog", &Screen.FogOfWar); + model.Bind("bridges", &Screen.Bridges); + model.Bind("mcv", &Screen.MCVRedeploy); + model.Bind("shortgame", &Screen.ShortGame); + model.Bind("engineer", &Screen.MultiEngineer); + model.Bind("allies", &Screen.Allies); + model.Bind("harvtruce", &Screen.HarvTruce); + + model.Bind("canaccept", &Screen.CanAccept); + model.Bind("canstart", &Screen.CanStart); + + // The field is bound one way, so a value the model already holds is never queued back as + // a change the player did not type. + model.BindEventCallback("rename", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + Rml::String const value = event.GetParameter("value", Rml::String()); + if (value == Screen.Handle) return; + Screen.Queue(UIIntent{UI_LOBBY_RENAME, value, 0}); + }); + + model.BindEventCallback("pickgame", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{UI_LOBBY_PICK_GAME, "", arguments[0].Get()}); + }); + + model.BindEventCallback("pickuser", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{UI_LOBBY_PICK_USER, "", arguments[0].Get()}); + }); + + model.BindEventCallback("chooseside", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + if (!Settled) return; + int const row = (int)(event.GetParameter("value", 0.0f) + 0.5f); + if (row == Screen.SelectedSide) return; + + if (Kind == UILobbyPresenterClass::SCREEN_HOST) { + Screen.Queue(UIIntent{UI_LOBBY_HOST_SIDE, "", row}); + return; + } + + // The guest records the side ahead of the color, because the dialog read both of + // its boxes and sent one packet carrying the pair. + Screen.SelectedSide = row; + if (row >= 0 && row < (int)Screen.Sides.size()) { + Screen.Queue(UIIntent{UI_LOBBY_SIDE, "", Screen.Sides[row].Country}); + } + Screen.Queue(UIIntent{UI_LOBBY_IDENTITY, "", Screen.Color}); + }); + + model.BindEventCallback("choosecolor", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + if (!Settled) return; + int const row = (int)(event.GetParameter("value", 0.0f) + 0.5f); + if (row == Screen.Color) return; + + if (Kind == UILobbyPresenterClass::SCREEN_HOST) { + Screen.Queue(UIIntent{UI_LOBBY_HOST_COLOR, "", row}); + return; + } + + if (Screen.SelectedSide >= 0 && Screen.SelectedSide < (int)Screen.Sides.size()) { + Screen.Queue(UIIntent{UI_LOBBY_SIDE, "", Screen.Sides[Screen.SelectedSide].Country}); + } + Screen.Queue(UIIntent{UI_LOBBY_IDENTITY, "", row}); + }); + + model.BindEventCallback("move", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + + Rml::String const which = arguments[0].Get(); + int const value = (int)(event.GetParameter("value", 0.0f) + 0.5f); + + if (which == UI_LOBBY_UNITCOUNT) Move(UI_LOBBY_UNITCOUNT, value); + else if (which == UI_LOBBY_CREDITS) Move(UI_LOBBY_CREDITS, value); + else if (which == UI_LOBBY_TECHLEVEL) Move(UI_LOBBY_TECHLEVEL, value); + else if (which == UI_LOBBY_AILEVEL) Move(UI_LOBBY_AILEVEL, value); + else if (which == UI_LOBBY_AIPLAYERS) Move(UI_LOBBY_AIPLAYERS, value); + else if (which == UI_LOBBY_GAMESPEED) Move(UI_LOBBY_GAMESPEED, value); + }); + + // A check box is a class plus a click that queues a toggle, not a two-way bound control. + model.BindEventCallback("toggle", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{UI_LOBBY_TOGGLE, arguments[0].Get(), 0}); + }); + + model.BindEventCallback("press", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + + Rml::String const action = arguments[0].Get(); + if (action == UI_LOBBY_JOIN) Press(UI_LOBBY_JOIN); + else if (action == UI_LOBBY_NEW) Press(UI_LOBBY_NEW); + else if (action == UI_LOBBY_CANCEL) Press(UI_LOBBY_CANCEL); + else if (action == UI_LOBBY_ACCEPT) Press(UI_LOBBY_ACCEPT); + else if (action == UI_LOBBY_GO) Press(UI_LOBBY_GO); + else if (action == UI_LOBBY_KICK) Press(UI_LOBBY_KICK); + else if (action == UI_LOBBY_PICK_MAP) Press(UI_LOBBY_PICK_MAP); + }); + + // Enter in the chat field sends the line, which is what EN_MAXTEXT stood for on an + // ES_WANTRETURN edit control. Escape backs out of the screen. + model.BindEventCallback("submit", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + int const key = event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN); + if (key == Rml::Input::KI_RETURN || key == Rml::Input::KI_NUMPADENTER) { + Submit_Chat(); + event.StopPropagation(); + } + }); + + model.BindEventCallback("key", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + int const key = event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN); + if (key == Rml::Input::KI_ESCAPE) { + Press(UI_LOBBY_CANCEL); + } + }); +} + + +void LobbyViewClass::Sync(void) +{ + if (!Model) return; + + Rebuild_Rows(); + + Model.DirtyVariable("games"); + Model.DirtyVariable("selectedgame"); + Model.DirtyVariable("users"); + Model.DirtyVariable("messages"); + Model.DirtyVariable("scenarioname"); + Model.DirtyVariable("canaccept"); + Model.DirtyVariable("canstart"); + + // The track bar, combo box and field values are not dirtied, because each already + // carries what its own change event reported. The options are, because the host's + // coupling and the guest's packets both move them from underneath. + Model.DirtyVariable("bases"); + Model.DirtyVariable("crates"); + Model.DirtyVariable("fog"); + Model.DirtyVariable("bridges"); + Model.DirtyVariable("mcv"); + Model.DirtyVariable("shortgame"); + Model.DirtyVariable("engineer"); + Model.DirtyVariable("allies"); + Model.DirtyVariable("harvtruce"); + + // The guest never moves a track bar -- every one of them is WS_DISABLED on its template + // -- so its values come from the host's packets and have to be dirtied. The host's own + // bars already carry what their change events reported. + if (Kind == UILobbyPresenterClass::SCREEN_GUEST) { + Model.DirtyVariable("unitcount"); + Model.DirtyVariable("credits"); + Model.DirtyVariable("techlevel"); + Model.DirtyVariable("ailevel"); + Model.DirtyVariable("aiplayers"); + Model.DirtyVariable("gamespeed"); + Model.DirtyVariable("selectedside"); + Model.DirtyVariable("selectedcolor"); + } + + // The picture is redrawn where it changed, not every pass, so the element uploads once + // per map rather than once per present. + if (!Preview.empty() && Screen.PreviewGeneration != Drawn) { + Drawn = Screen.PreviewGeneration; + Picture.Redraw(); + } +} + + +// The three documents, kept alive across the driver's passes because the lobby moves +// between them and comes back, the way it kept its host and game list dialogs alive +// together. +static LobbyViewClass * _GameListView = NULL; +static LobbyViewClass * _HostView = NULL; +static LobbyViewClass * _GuestView = NULL; +static LobbyViewClass * _ShownView = NULL; + + +static LobbyViewClass ** Lobby_View_Slot(UILobbyPresenterClass::ScreenType kind) +{ + switch (kind) { + case UILobbyPresenterClass::SCREEN_GAME_LIST: return(&_GameListView); + case UILobbyPresenterClass::SCREEN_HOST: return(&_HostView); + case UILobbyPresenterClass::SCREEN_GUEST: return(&_GuestView); + default: return(NULL); + } +} + + +void UI_Lobby_Close_Views(void) +{ + delete _GameListView; + delete _HostView; + delete _GuestView; + + _GameListView = NULL; + _HostView = NULL; + _GuestView = NULL; + _ShownView = NULL; +} + + +/// +/// Shows whichever of the three documents the screen says it is on, and runs it until the +/// player answers. +/// +UIResult UI_Lobby_Run(UILobbyPresenterClass & presenter) +{ + UIResult failed; + failed.Outcome = UIResult::OUTCOME_FAILED_TO_OPEN; + + LobbyViewClass ** const slot = Lobby_View_Slot(presenter.Showing); + if (slot == NULL) { + return(failed); + } + + if (*slot == NULL) { + char const * document = "gamelist.rml"; + char const * preview = NULL; + if (presenter.Showing == UILobbyPresenterClass::SCREEN_HOST) { + document = "mphost.rml"; + preview = UI_LOBBY_HOST_PREVIEW; + } else if (presenter.Showing == UILobbyPresenterClass::SCREEN_GUEST) { + document = "mpguest.rml"; + preview = UI_LOBBY_GUEST_PREVIEW; + } + + LobbyViewClass * const view = new LobbyViewClass(presenter, document, presenter.Showing, preview); + if (!view->Prepare(true)) { + delete view; + return(failed); + } + + view->Settle(); + *slot = view; + } + + // The screen the lobby moved away from steps aside rather than being torn down, because + // it is come back to and its document is the same one. + if (_ShownView != NULL && _ShownView != *slot) { + _ShownView->Hide(); + } + if (!(*slot)->Is_Visible()) { + (*slot)->Show(); + } + _ShownView = *slot; + + // The ranges go back on the controls every time a screen is shown, because a screen that + // is come back to opens on what the model holds now rather than on what it held when the + // document was first loaded. + (*slot)->Settle(); + + // A family reopened in a loop resets the close mark and the held result, since a close + // marks the presenter closing and a marked presenter drains nothing. + presenter.Result.reset(); + presenter.IsClosing = false; + presenter.Answered = false; + presenter.Running = presenter.Showing; + + (*slot)->Sync(); + + while (!presenter.Result.has_value()) { + UI_Run_Modal(presenter, **slot); + + if (presenter.Pending == UILobbyPresenterClass::SUB_NONE) { + break; + } + + // The scenario picker draws where the host screen is, so the document steps aside + // for it, which is what the dialog's own ShowWindow did. + (*slot)->Hide(); + presenter.Run_Pending(); + (*slot)->Show(); + (*slot)->Sync(); + } + + presenter.Running = UILobbyPresenterClass::SCREEN_NONE; + return(presenter.Result.value_or(UIResult{})); +} diff --git a/code/ui/uilobby.h b/code/ui/uilobby.h new file mode 100644 index 000000000..7187d35c7 --- /dev/null +++ b/code/ui/uilobby.h @@ -0,0 +1,303 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The network lobby's behavior, with no toolkit in it. The game list, the host setup and +// the guest setup are one screen family sharing one model, because they share the session's +// game, player and chat rosters and hand the driver one answer between them. +// +// All three screens are here: the game list, the host setup and the guest setup. +// +// docs/UI_DESIGN.md, "Screens", owns the contract. + +#pragma once + +#include "uiscreen.h" + +#include +#include + + +inline constexpr char const * UI_LOBBY_RENAME = "rename"; +inline constexpr char const * UI_LOBBY_PICK_GAME = "pickgame"; +inline constexpr char const * UI_LOBBY_JOIN = "join"; +inline constexpr char const * UI_LOBBY_NEW = "new"; +inline constexpr char const * UI_LOBBY_CANCEL = "cancel"; +inline constexpr char const * UI_LOBBY_SAY = "say"; +inline constexpr char const * UI_LOBBY_COLOR = "color"; +inline constexpr char const * UI_LOBBY_SIDE = "side"; +inline constexpr char const * UI_LOBBY_IDENTITY = "identity"; +inline constexpr char const * UI_LOBBY_ACCEPT = "accept"; +inline constexpr char const * UI_LOBBY_GO = "go"; +inline constexpr char const * UI_LOBBY_HOST_SIDE = "hostside"; +inline constexpr char const * UI_LOBBY_HOST_COLOR = "hostcolor"; +inline constexpr char const * UI_LOBBY_KICK = "kick"; +inline constexpr char const * UI_LOBBY_PICK_USER = "pickuser"; +inline constexpr char const * UI_LOBBY_PICK_MAP = "pickmap"; +inline constexpr char const * UI_LOBBY_TOGGLE = "toggle"; +inline constexpr char const * UI_LOBBY_SLIDER = "slider"; + +// The game options the host owns. A track bar and a check box name themselves, because an +// intent carries an identity rather than a control. +inline constexpr char const * UI_LOBBY_UNITCOUNT = "unitcount"; +inline constexpr char const * UI_LOBBY_CREDITS = "credits"; +inline constexpr char const * UI_LOBBY_TECHLEVEL = "techlevel"; +inline constexpr char const * UI_LOBBY_AILEVEL = "ailevel"; +inline constexpr char const * UI_LOBBY_AIPLAYERS = "aiplayers"; +inline constexpr char const * UI_LOBBY_GAMESPEED = "gamespeed"; + +inline constexpr char const * UI_LOBBY_BASES = "bases"; +inline constexpr char const * UI_LOBBY_CRATES = "crates"; +inline constexpr char const * UI_LOBBY_FOG = "fog"; +inline constexpr char const * UI_LOBBY_BRIDGES = "bridges"; +inline constexpr char const * UI_LOBBY_MCV = "mcv"; +inline constexpr char const * UI_LOBBY_SHORTGAME = "shortgame"; +inline constexpr char const * UI_LOBBY_ENGINEER = "engineer"; +inline constexpr char const * UI_LOBBY_ALLIES = "allies"; +inline constexpr char const * UI_LOBBY_HARVTRUCE = "harvtruce"; + + +class UILobbyPresenterClass : public UIPresenterClass +{ + public: + // What the driver loop is being asked to do next. These stand where the dialogs' + // own control identifiers stood, so a presenter names no control. + enum ResponseType { + RESPONSE_NONE, + RESPONSE_CANCEL, + RESPONSE_JOIN, + RESPONSE_NEW, + RESPONSE_GO, + }; + + // Which of the family's three screens is being shown. The driver moves between them + // and the presentation follows; a presenter names a screen rather than a window. + enum ScreenType { + SCREEN_NONE, + SCREEN_GAME_LIST, + SCREEN_HOST, + SCREEN_GUEST, + }; + + // A screen the lobby opens and comes back from. The scenario picker draws where the + // host screen is, so its owner takes the host screen off the screen and puts it + // back rather than running it underneath. + enum PendingType { + SUB_NONE, + SUB_PICK_MAP, + }; + + // A country that may be played, carrying the country itself rather than its + // position, because the list holds only the multiplayable countries. + struct SideType + { + std::string Name; + int Country = 0; + }; + + // A track bar and the range the rules give it. A range control clamps a value into + // the range it is holding, so a view sets the range before the value. + struct SliderType + { + int Value = 0; + int Minimum = 0; + int Maximum = 0; + int Step = 1; + }; + + // A line of chat or system text, as PMessagePrintf composed it. Wrapping it to the + // width it is shown at belongs to the presentation. + struct ChatLineType + { + std::string Text; + int Color = -1; + }; + + // A game somebody is advertising. The lobby itself heads the list. + struct GameRowType + { + std::string Label; + bool IsOpen = false; + }; + + // Somebody in the lobby, or a player in the game the list is showing. Out in the + // lobby only the name is known; inside a game the row also carries the color, the + // side its emblem stands for and whether the player is the host or has accepted. + struct UserRowType + { + std::string Name; + std::string SideName; + int Color = 0; + int Side = -1; + bool IsHost = false; + bool HasAccepted = false; + }; + + virtual void Execute(UIIntent const & intent) override; + virtual void Refresh(void) override; + virtual void Service(void) override; + + // The scenario picker draws where the host screen is, so the host screen is stepped + // aside for it rather than run underneath. The family also steps aside when the + // protocol moves it to another of its three screens, because the runner picks the + // document once and a join is confirmed from inside the service. + virtual bool Suspends(void) const override + { + return(Pending != SUB_NONE || Answered || (Running != SCREEN_NONE && Running != Showing)); + } + + // Has the network answered the driver on the player's behalf? A confirmed start, a + // rejected join and a host signing off all answer from inside the service, where + // there is no result to return, so the screen steps aside to let the driver act. + bool Answered = false; + + // The screen the runner is holding a document open for, which the runner sets and + // clears around itself. + ScreenType Running = SCREEN_NONE; + + // The rosters the lobby opens with, which the game list dialog built as it was + // created: the player's own chat entry and the lobby's own game entry. + void Open(void); + + // The rosters and the guest's own standing when the guest screen opens: a guest + // arrives having accepted nothing. + void Open_Guest(void); + + // The game the host has just created: the scenario the setup opens on, the option + // values the rules allow, and the country and color lists both setup screens show. + void Open_Host(void); + + // Runs the scenario picker with the host screen out of the way, and puts the map it + // chose on the model. Called by the owner between passes, never from an event. + void Run_Pending(void); + + // Records a line of chat or system text for whatever is showing the lobby. Called + // from the network code wherever PMessagePrintf composes one. + void Record_Message(int color, char const * text); + + // The host's settings have arrived and been written to the session. Called where the + // options are decoded, so a presentation that is not a window sees them too. + void Options_Received(void); + + // Reads the session's rosters into the view-model. Marking the host as accepted + // happens here rather than while drawing, because it is a fact about the player + // rather than about the row. + void Build_Game_Rows(void); + void Build_User_Rows(void); + + /* + ** The view-model. + */ + ScreenType Showing = SCREEN_NONE; + + std::string Handle; + + // The longest handle the name field accepts, in bytes. Session.Handle is + // MPLAYER_NAME_MAX bytes and travels in a packet field of that size, so anything + // longer is thrown away rather than sent. + enum { HANDLE_LIMIT = 11 }; + + int Color = 0; + + // The country the player is showing, which is a country rather than a row, because + // the side list holds only the countries that may be played. + int House = 0; + + // Is the accept button available? A guest may accept once per change the host + // makes, which is what disabling the button after a press stood for. + bool CanAccept = false; + + std::vector Games; + int SelectedGame = 0; + + std::vector Users; + + // Which player rows the host has picked out to kick. The list is a LBS_MULTIPLESEL + // one, so this is a set of rows rather than a single selection. + std::vector PickedUsers; + + std::vector Messages; + + // The most lines the model keeps, which is what the message list box was capped at. + enum { MESSAGE_LIMIT = 128 }; + + /* + ** The host's half of the model. The guest screen shows the same fields and cannot + ** change them, which is what WS_DISABLED on every one of its controls stood for. + */ + std::vector Sides; + int SelectedSide = 0; + + std::vector Colors; + + std::string ScenarioName; + + SliderType UnitCount; + SliderType Credits; + SliderType TechLevel; + SliderType AILevel; + SliderType AIPlayers; + SliderType GameSpeed; + + bool Bases = false; + bool Crates = false; + bool FogOfWar = false; + bool Bridges = false; + bool MCVRedeploy = false; + bool ShortGame = false; + bool MultiEngineer = false; + bool Allies = false; + bool HarvTruce = false; + + // Is the start button available? Pressing it takes it away until the driver has + // decided the game may begin, which is what disabling the window stood for. + bool CanStart = true; + + PendingType Pending = SUB_NONE; + + // Moves when the map picture changes, so a view uploads once per map rather than + // once per present. + unsigned int PreviewGeneration = 0; + + ResponseType Response = RESPONSE_NONE; + + // Did the last executed intent move a roster? A view redraws only what moved. + bool GamesChanged = false; + bool UsersChanged = false; + bool MessagesChanged = false; + bool OptionsChanged = false; + + private: + void Answer(ResponseType response); + void Rename(std::string const & name); + void Pick_Game(int row); + void Say(std::string const & text); + void Accept(void); + void Change_Identity(int color); + + void Build_Identity_Lists(void); + void Read_Options(void); + void Host_Side(int row); + void Host_Color(int color); + void Toggle(std::string const & which); + void Slide(std::string const & which, int value); + void Kick(void); +}; + + +// The lobby screen the driver is running, or NULL when no lobby is up. The network code +// reaches the model through this wherever a change is produced away from a screen. +UILobbyPresenterClass * UI_Lobby_Screen(void); +void UI_Set_Lobby_Screen(UILobbyPresenterClass * screen); + + +// Shows whichever of the three documents the screen says it is on, and runs it until the +// player answers. The documents outlive one call, because the lobby moves between them and +// comes back; UI_Lobby_Close_Views drops them when the lobby ends. +UIResult UI_Lobby_Run(UILobbyPresenterClass & presenter); +void UI_Lobby_Close_Views(void); diff --git a/code/ui/uimainmenu.cpp b/code/ui/uimainmenu.cpp new file mode 100644 index 000000000..28717f7c3 --- /dev/null +++ b/code/ui/uimainmenu.cpp @@ -0,0 +1,257 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The main menu. Behavior traced out of Main_Menu and Main_Menu_Dialog_Proc in init.cpp. +// +// What the extraction fixes in place: the load button is disabled when there is no saved +// game to offer, and the test is made as the screen opens rather than once at startup; the +// keys the driver watched for beside the buttons belong to the screen, not to the window it +// was drawn in, so a typed character reaches Cheat_Key_Process through an intent and the +// version screen and the credits are choices like any other; and the version screen is a +// screen of a different kind, so it nests and the view steps aside for it, which is what +// the dialog's own ShowWindow did. +// +// docs/UI_DESIGN.md, "Screens", owns the contracts this keeps to. + +#include "always.h" + +#include "uimainmenu.h" + +#include "uirmlview.h" + +#include "voc.h" +#include "globals.h" +#include "init.h" +#include "loaddlg.h" +#include "_rules.h" +#include "rules.h" +#include "scrnsel.hh" + +#include +#include +#include + +void Version_Dialog(void); + + +void UIMainMenuPresenterClass::Refresh(void) +{ + CanLoad = LoadOptionsClass().Files_Present(); +} + + +/// +/// The maintenance the menu driver ran on every pass of its own loop. +/// +void UIMainMenuPresenterClass::Service(void) +{ + Title_Screen_Restore(); +} + + +/// +/// The selection the caller's own switch is written against. +/// +int UIMainMenuPresenterClass::Selection(void) const +{ + switch (Choice) { + case CHOICE_CAMPAIGN: return(SEL_CAMPAIGN_GAME); + case CHOICE_LOAD: return(SEL_LOAD_GAME); + case CHOICE_MULTIPLAYER: return(SEL_MULTIPLAYER_GAME); + case CHOICE_INTRO: return(SEL_INTRO); + case CHOICE_OPTIONS: return(SEL_OPTIONS); + case CHOICE_EXIT: return(SEL_EXIT); + case CHOICE_CREDITS: return(SEL_VIEW_CREDITS); + default: return(SEL_NONE); + } +} + + +/// +/// Runs the screen the last choice asked for, then clears the request. +/// +void UIMainMenuPresenterClass::Run_Pending(void) +{ + if (!VersionPending) { + return; + } + + VersionPending = false; + Version_Dialog(); +} + + +void UIMainMenuPresenterClass::Execute(UIIntent const & intent) +{ + if (intent.Action == UI_MAINMENU_VERSION) { + VersionPending = true; + return; + } + + if (intent.Action == UI_MAINMENU_TYPED) { + // A cheat word is spelled out one character at a time and only the word completing + // it answers, which is why nothing here is a choice. + if (Cheat_Key_Process((char)intent.Value)) { + Sound_Effect(Rule->OptionsChanged); + Title_Screen_Restore(true); + } + return; + } + + UIResult result; + result.Outcome = UIResult::OUTCOME_ACCEPTED; + + if (intent.Action == UI_MAINMENU_CAMPAIGN) { + Choice = CHOICE_CAMPAIGN; + } else if (intent.Action == UI_MAINMENU_LOAD) { + // The permission is checked when the button is pressed rather than when it was + // enabled, because a saved game can arrive or go while the screen is up. + if (!LoadOptionsClass().Files_Present()) { + return; + } + Choice = CHOICE_LOAD; + } else if (intent.Action == UI_MAINMENU_MULTIPLAYER) { + Choice = CHOICE_MULTIPLAYER; + } else if (intent.Action == UI_MAINMENU_INTRO) { + Choice = CHOICE_INTRO; + } else if (intent.Action == UI_MAINMENU_OPTIONS) { + Choice = CHOICE_OPTIONS; + } else if (intent.Action == UI_MAINMENU_CREDITS) { + Choice = CHOICE_CREDITS; + } else if (intent.Action == UI_MAINMENU_EXIT) { + Choice = CHOICE_EXIT; + result.Outcome = UIResult::OUTCOME_CANCELLED; + } else { + return; + } + + result.Value = Selection(); + Result = result; +} + + +//--------------------------------------------------------------------------------------- +// The RmlUi view. +//--------------------------------------------------------------------------------------- + +/// +/// The RmlUi half of the main menu. +/// +class MainMenuViewClass : public UIRmlViewClass +{ + public: + MainMenuViewClass(UIMainMenuPresenterClass & presenter) : + UIRmlViewClass(presenter, "mainmenu.rml"), + Screen(presenter) + { + } + + virtual void Bind(Rml::DataModelConstructor & model) override; + virtual void Sync(void) override; + + private: + UIMainMenuPresenterClass & Screen; + + // Was a modifier held for the key that produced the character now arriving? The + // driver read the version and credits combinations off the queue before anything + // else saw them, so a character they produce is not a character the player typed. + bool Modified = false; +}; + + +void MainMenuViewClass::Bind(Rml::DataModelConstructor & model) +{ + model.Bind("canload", &Screen.CanLoad); + + model.BindEventCallback("press", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{arguments[0].Get(), "", 0}); + }); + + // The keys the driver watched for beside the buttons are the screen's, so the document + // carries them: the shell's modal scope takes every key message before the game's own + // queue sees it, and Keyboard->Check() never fires again while a document is shown. + model.BindEventCallback("key", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + int const key = event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN); + bool const ctrl = event.GetParameter("ctrl_key", false); + bool const alt = event.GetParameter("alt_key", false); + + Modified = ctrl || alt; + + if (key == Rml::Input::KI_V && ctrl && !alt) { + Screen.Queue(UIIntent{UI_MAINMENU_VERSION, "", 0}); + } else if (key == Rml::Input::KI_C && ctrl && alt) { + Screen.Queue(UIIntent{UI_MAINMENU_CREDITS, "", 0}); + } + }); + + // A cheat word is spelled out, so the screen wants the character rather than the key + // that produced it. + model.BindEventCallback("typed", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + // One character follows one key, so the flag answers for that character alone. + bool const modified = Modified; + Modified = false; + if (modified) return; + + Rml::String const text = event.GetParameter("text", Rml::String()); + for (char const letter : text) { + Screen.Queue(UIIntent{UI_MAINMENU_TYPED, "", (int)letter}); + } + }); +} + + +void MainMenuViewClass::Sync(void) +{ + if (!Model) return; + + Model.DirtyVariable("canload"); +} + + +/// +/// Shows the main menu and waits for the player to choose. +/// +UIResult UI_Main_Menu_Screen(UIMainMenuPresenterClass & presenter) +{ + MainMenuViewClass view(presenter); + + if (!view.Prepare(true)) { + UIResult result; + result.Outcome = UIResult::OUTCOME_FAILED_TO_OPEN; + return(result); + } + + // The version screen is a screen of a different kind, so it nests; getting out of the + // way of it is hiding this document, which is what the dialog's ShowWindow did. + while (!presenter.Result.has_value()) { + UIResult const pass = UI_Run_Modal(presenter, view); + + if (pass.GameEnded) { + view.Close(); + return(pass); + } + + if (!presenter.VersionPending) { + break; + } + + view.Hide(); + presenter.Run_Pending(); + view.Show(); + view.Sync(); + } + + UIResult const result = presenter.Result.value_or(UIResult{}); + view.Close(); + return(result); +} diff --git a/code/ui/uimainmenu.h b/code/ui/uimainmenu.h new file mode 100644 index 000000000..fa0f10998 --- /dev/null +++ b/code/ui/uimainmenu.h @@ -0,0 +1,73 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The main menu's behavior, with no toolkit in it. The screen is six buttons and the keys +// the driver watched for beside them, which are part of the screen rather than of the +// window it was drawn in: the version screen, the credits, and the cheat words. +// +// docs/UI_DESIGN.md, "Screens", owns the contract. + +#pragma once + +#include "uiscreen.h" + + +inline constexpr char const * UI_MAINMENU_CAMPAIGN = "campaign"; +inline constexpr char const * UI_MAINMENU_LOAD = "load"; +inline constexpr char const * UI_MAINMENU_MULTIPLAYER = "multiplayer"; +inline constexpr char const * UI_MAINMENU_INTRO = "intro"; +inline constexpr char const * UI_MAINMENU_OPTIONS = "options"; +inline constexpr char const * UI_MAINMENU_EXIT = "exit"; +inline constexpr char const * UI_MAINMENU_VERSION = "version"; +inline constexpr char const * UI_MAINMENU_CREDITS = "credits"; +inline constexpr char const * UI_MAINMENU_TYPED = "typed"; // Value: the character typed + + +class UIMainMenuPresenterClass : public UIPresenterClass +{ + public: + enum ChoiceType { + CHOICE_NONE, + CHOICE_CAMPAIGN, + CHOICE_LOAD, + CHOICE_MULTIPLAYER, + CHOICE_INTRO, + CHOICE_OPTIONS, + CHOICE_EXIT, + CHOICE_CREDITS, + }; + + virtual void Execute(UIIntent const & intent) override; + virtual void Refresh(void) override; + virtual void Service(void) override; + virtual bool Suspends(void) const override { return(VersionPending); } + + // The selection the caller's own switch is written against. + int Selection(void) const; + + // Is the version screen waiting to be run? It is a screen of a different kind, so + // it nests, and the view gets out of its way the way the dialog's ShowWindow did. + bool VersionPending = false; + void Run_Pending(void); + + /* + ** The view-model. Plain values, and the only thing a view reads. + */ + + // Is there a saved game to offer? With none the load button is disabled, as the + // dialog disabled it. + bool CanLoad = false; + + ChoiceType Choice = CHOICE_NONE; +}; + + +// Shows the screen through its RmlUi view. FAILED_TO_OPEN leaves nothing shown and the +// caller falls through to the legacy dialog. +UIResult UI_Main_Menu_Screen(UIMainMenuPresenterClass & presenter); diff --git a/code/ui/uimainoptions.cpp b/code/ui/uimainoptions.cpp new file mode 100644 index 000000000..d05c70b30 --- /dev/null +++ b/code/ui/uimainoptions.cpp @@ -0,0 +1,192 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The main options screen. Behavior traced out of Main_Options_Dialog and +// Main_Options_Dialog_Proc in mainopt.cpp. +// +// What the extraction fixes in place: the driver cleared GameActive around the whole family +// and put it back on the way out, so every screen the family opens sees a game that is not +// running whether or not one is, and both the sound and the game controls screens choose +// their layout from exactly that; and the settings are written once, as the player leaves +// the family, not as each sub-screen closes. +// +// docs/UI_DESIGN.md, "Screens", owns the contracts this keeps to. + +#include "always.h" + +#include "uimainoptions.h" + +#include "uirmlview.h" + +#include "audio/audioengine.h" +#include "gamedlg.h" +#include "globals.h" +#include "init.h" +#include "mainopt.h" +#include "goptions.h" +#include "options.h" +#include "sounddlg.h" + +#include +#include +#include + + +void UIMainOptionsPresenterClass::Refresh(void) +{ + SoundAvailable = AudioEngine.Is_Available(); +} + + +void UIMainOptionsPresenterClass::Begin(void) +{ + WasGameActive = GameActive; + GameActive = false; +} + + +void UIMainOptionsPresenterClass::End(void) +{ + Options.Save_Settings(); + GameActive = WasGameActive; +} + + +/// +/// The maintenance the dialog driver ran on every pass of its own loop. +/// +void UIMainOptionsPresenterClass::Service(void) +{ + Title_Screen_Restore(); +} + + +void UIMainOptionsPresenterClass::Execute(UIIntent const & intent) +{ + UIResult result; + result.Outcome = UIResult::OUTCOME_ACCEPTED; + + if (intent.Action == UI_MAINOPT_SOUND) { + if (!SoundAvailable) { + return; + } + Choice = CHOICE_SOUND; + } else if (intent.Action == UI_MAINOPT_DISPLAY) { + Choice = CHOICE_DISPLAY; + } else if (intent.Action == UI_MAINOPT_KEYBOARD) { + Choice = CHOICE_KEYBOARD; + } else if (intent.Action == UI_MAINOPT_SETTINGS) { + Choice = CHOICE_SETTINGS; + } else { + // Anything else leaves, which is the dialog's own default arm: it took whatever + // identifier arrived, and every identifier that was not a sub-screen ended the + // family. + Choice = CHOICE_EXIT; + result.Outcome = UIResult::OUTCOME_CANCELLED; + } + + Result = result; +} + + +/// +/// Runs the screen the player asked for. +/// +void UIMainOptionsPresenterClass::Run_Pending(void) +{ + ChoiceType const pending = Choice; + Choice = CHOICE_NONE; + + switch (pending) { + case CHOICE_SOUND: + SoundControlsClass().Dialog(); + break; + + case CHOICE_DISPLAY: + Display_Options_Dialog(); + break; + + case CHOICE_KEYBOARD: + Options.Hotkey_Dialog(); + break; + + case CHOICE_SETTINGS: + GameControlsClass().Dialog(); + break; + + default: + break; + } +} + + +//--------------------------------------------------------------------------------------- +// The RmlUi view. +//--------------------------------------------------------------------------------------- + +/// +/// The RmlUi half of the main options screen. +/// +class MainOptionsViewClass : public UIRmlViewClass +{ + public: + MainOptionsViewClass(UIMainOptionsPresenterClass & presenter) : + UIRmlViewClass(presenter, "options.rml"), + Screen(presenter) + { + } + + virtual void Bind(Rml::DataModelConstructor & model) override; + virtual void Sync(void) override {} + + private: + UIMainOptionsPresenterClass & Screen; +}; + + +void MainOptionsViewClass::Bind(Rml::DataModelConstructor & model) +{ + model.Bind("available", &Screen.SoundAvailable); + + model.BindEventCallback("press", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{arguments[0].Get(), "", 0}); + }); + + // Escape leaves, which is the IDCANCEL the dialog's default arm took as an exit. Enter + // leaves too, because the template names no default push button and Windows then sent + // the dialog IDOK, which that same arm did not recognize either. + model.BindEventCallback("key", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + int const key = event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN); + if (key == Rml::Input::KI_ESCAPE || key == Rml::Input::KI_RETURN || key == Rml::Input::KI_NUMPADENTER) { + Screen.Queue(UIIntent{UI_MAINOPT_EXIT, "", 0}); + } + }); +} + + +/// +/// Shows the main options menu and waits for the player to choose. +/// +UIResult UI_Main_Options_Screen(UIMainOptionsPresenterClass & presenter) +{ + MainOptionsViewClass view(presenter); + + if (!view.Prepare(true)) { + UIResult result; + result.Outcome = UIResult::OUTCOME_FAILED_TO_OPEN; + return(result); + } + + UIResult const result = UI_Run_Modal(presenter, view); + view.Close(); + return(result); +} diff --git a/code/ui/uimainoptions.h b/code/ui/uimainoptions.h new file mode 100644 index 000000000..cc2bafc30 --- /dev/null +++ b/code/ui/uimainoptions.h @@ -0,0 +1,78 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The main options screen's behavior, with no toolkit in it. The screen is five buttons and +// almost no state of its own, but it owns the family: it suspends the game while any +// options screen is up and writes the settings out when the player leaves, and the sound +// and game controls screens each pick their own layout from the suspension it holds. +// +// docs/UI_DESIGN.md, "Screens", owns the contract. + +#pragma once + +#include "uiscreen.h" + + +inline constexpr char const * UI_MAINOPT_SOUND = "sound"; +inline constexpr char const * UI_MAINOPT_DISPLAY = "display"; +inline constexpr char const * UI_MAINOPT_KEYBOARD = "keyboard"; +inline constexpr char const * UI_MAINOPT_SETTINGS = "settings"; +inline constexpr char const * UI_MAINOPT_EXIT = "exit"; + + +class UIMainOptionsPresenterClass : public UIPresenterClass +{ + public: + enum ChoiceType { + CHOICE_NONE, + CHOICE_SOUND, + CHOICE_DISPLAY, + CHOICE_KEYBOARD, + CHOICE_SETTINGS, + CHOICE_EXIT, + }; + + virtual void Execute(UIIntent const & intent) override; + virtual void Refresh(void) override; + virtual void Service(void) override; + + // Suspends the game for as long as the options family is up. What this holds is + // read by the screens the family opens, so it is behavior rather than bookkeeping: + // the sound and game controls screens each choose their layout from it. + void Begin(void); + + // Writes the settings out and puts the game back the way Begin found it. + void End(void); + + // Runs the screen the last choice asked for, then clears the request. Safe to call + // with nothing pending. + void Run_Pending(void); + + // Does the last choice leave the options family? Anything the screen does not + // recognize leaves it, which is what the dialog's default arm did. + bool Exits(void) const { return(Choice == CHOICE_EXIT); } + + /* + ** The view-model. Plain values, and the only thing a view reads. + */ + + // Is there an audio device to talk to? With none the sound button is disabled, as + // the dialog disabled it. + bool SoundAvailable = false; + + ChoiceType Choice = CHOICE_NONE; + + private: + bool WasGameActive = false; +}; + + +// Shows the screen through its RmlUi view. FAILED_TO_OPEN leaves nothing shown and the +// caller falls through to the legacy dialog. +UIResult UI_Main_Options_Screen(UIMainOptionsPresenterClass & presenter); diff --git a/code/ui/uimapgen.cpp b/code/ui/uimapgen.cpp new file mode 100644 index 000000000..67d36abe7 --- /dev/null +++ b/code/ui/uimapgen.cpp @@ -0,0 +1,839 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The random map generator screen. What is preserved from IDD_MAPGEN, IDD_MAPGEN_FS and +// IDD_MAPGEN_WDT, and where each came from: the variant is chosen by whether Firestorm is +// enabled and whether the session names a tournament territory, not by the caller; the +// environment and time of day lists are sorted by name and the two size lists are not, +// because only the first two templates carry CBS_SORT; every setting is read off the screen +// when a button is pressed rather than tracked, which is what Get_Settings did; a tournament +// territory narrows each track bar's span and may lock it, and a span with nothing left to +// choose between is shown disabled rather than hidden; the tournament variant fixes the +// teams at two on two and offers no player count; and the seed field, which every template +// declares hidden and disabled, becomes visible only where a territory says the player may +// change the seed. +// +// docs/UI_DESIGN.md, "Screens", owns the contracts this keeps to. + +#include "always.h" + +#include "uimapgen.h" + +#include "uiinternal.h" +#include "uimappreview.h" +#include "uirmlview.h" +#include "uishell.h" +#include "uisurface.h" + +#include "addon.h" +#include "ccrand.h" +#include "data.h" +#include "dbgprint.h" +#include "init.h" +#include "language/language.h" +#include "mapgen.h" +#include "preview.h" +#include "scenario.h" +#include "session.h" +#include "wdtnet.h" +#include "worlddom.h" + +#include +#include + +#include +#include +#include + + +namespace { + + // The strings the three lists are built from, in the order the settings are numbered. + int const _BiomeNames[BIOME_COUNT] = { + TXT_BIOME_TUNDRA, + TXT_BIOME_TAIGA, + TXT_BIOME_TEMPERATE, + TXT_BIOME_DESERT, + TXT_BIOME_MUTATED, + }; + + int const _TimeNames[TIME_OF_DAY_COUNT] = { + TXT_TIME_MORNING, + TXT_TIME_AFTERNOON, + TXT_TIME_DUSK, + TXT_TIME_NIGHT, + }; + + int const _SizeNames[MAPSIZE_COUNT] = { + TXT_MAPSIZE_SMALL, + TXT_MAPSIZE_MEDIUM, + TXT_MAPSIZE_LARGE, + TXT_MAPSIZE_VERY_LARGE, + }; + + + // The territory the session is being fought over, or NULL outside a tournament game. + WDTTerritory * Tournament_Territory(void) + { + if (Session.Type != GAME_INTERNET || !Session.IsWDT) { + return(NULL); + } + return(WDT_Get_Territory(Session.WDTTerritory)); + } + + + // A span with nothing to choose between is shown disabled and left on the full scale, + // which is what Set_Scroll_Bar did with a max no greater than its min. + void Set_Range(UIMapGenPresenterClass::RangeType & range, int min, int max, int value, bool enable) + { + if (max <= min) { + range.Min = 0; + range.Max = 100; + range.Enabled = false; + } else { + range.Min = min; + range.Max = max; + range.Enabled = enable; + } + range.Value = value; + } + +} // namespace + + +static UIMapGenPresenterClass * _MapGenScreen = NULL; + + +UIMapGenPresenterClass * UI_MapGen_Screen(void) +{ + return(_MapGenScreen); +} + + +/// +/// Picks the variant and reads the generator's settings into the view-model. +/// +/// The progress callback the driver ran on every pass of its loop. +void UIMapGenPresenterClass::Open(bool (*callback)()) +{ + Callback = callback; + Pending = PENDING_NONE; + Result.reset(); + IsClosing = false; + + WDTTerritory const * const wdt = Tournament_Territory(); + if (wdt != NULL) { + Variant = VARIANT_WDT; + } else if (Addon_Enabled(ADDON_FIRESTORM)) { + Variant = VARIANT_FIRESTORM; + } else { + Variant = VARIANT_BASE; + } + + // A screen with no seed of its own rolls one, which is what the dialog did as it opened. + if (RandomMapGen.SeedData.Seed == -1) { + RandomMapGen.SeedData.Seed = Sim_Random_Pick(0U, 65535U); + } + + // The preview button is the one control the map debugger takes away, because that build + // generates the map outright rather than previewing it. + CanPreview = !Debug_Map; + + Build_Choices(); + Refresh(); +} + + +void UIMapGenPresenterClass::Execute(UIIntent const & intent) +{ + if (intent.Action == UI_MAPGEN_SET) { + Set_Value(intent.Identity, intent.Value); + return; + } + + if (intent.Action == UI_MAPGEN_TOGGLE) { + if (intent.Identity == "lifeforms" && Lifeforms.Enabled) Lifeforms.State = !Lifeforms.State; + else if (intent.Identity == "ionstorms" && IonStorms.Enabled) IonStorms.State = !IonStorms.State; + else if (intent.Identity == "transitions" && Transitions.Enabled) Transitions.State = !Transitions.State; + return; + } + + if (intent.Action == UI_MAPGEN_SEED) { + SeedText = intent.Identity; + return; + } + + if (intent.Action == UI_MAPGEN_CANCEL) { + Answer(ANSWER_CANCELLED); + return; + } + + if (intent.Action == UI_MAPGEN_OK) { + Apply(); + + if (Debug_Map) { + RandomMapGen.Generate_Random_Map(false); + Scen->Set_Scenario_Name(Fetch_String(TXT_RANDOM_MAP_DESCRIPTION)); + Write_Scenario_INI("RandMap.Map", true); + } else if (RandomMapGen.MapPreview == NULL || RandomMapGen.MapPreview->Get_Preview_Surface() == NULL) { + // A map the player never previewed has to be built before it can be accepted. + RandomMapGen.Generate_Random_Map(true); + } + + Answer(ANSWER_ACCEPTED); + return; + } + + if (intent.Action == UI_MAPGEN_PREVIEW) { + if (!CanPreview) return; + Apply(); + Generate_Preview(); + return; + } + + if (intent.Action == UI_MAPGEN_SURPRISE) { + if (!CanSurprise) return; + Apply(); + RandomMapGen.SeedData.Randomize(); + Refresh(); + return; + } + + if (intent.Action == UI_MAPGEN_LOAD) { + if (!CanLoad) return; + Apply(); + Pending = PENDING_LOAD; + return; + } + + if (intent.Action == UI_MAPGEN_SAVE) { + Apply(); + Pending = PENDING_SAVE; + return; + } + + if (intent.Action == UI_MAPGEN_DELETE) { + if (!CanDelete) return; + Apply(); + Pending = PENDING_DELETE; + return; + } +} + + +/// +/// Reads the generator's settings and the territory's permissions into the view-model. +/// +void UIMapGenPresenterClass::Refresh(void) +{ + MapSeedClass & seed = RandomMapGen.SeedData; + WDTTerritory const * const wdt = Tournament_Territory(); + + seed.Fixup_Settings(); + + Biome = seed.Biome; + Time = seed.Time; + Width = seed.Width; + Height = seed.Height; + + char text[32]; + std::snprintf(text, sizeof(text), "%d", seed.Seed); + SeedText = text; + + BiomeEnabled = wdt == NULL || wdt->UserModBiome; + TimeEnabled = wdt == NULL || wdt->UserModTime; + WidthEnabled = wdt == NULL || wdt->UserModWidth; + HeightEnabled = wdt == NULL || wdt->UserModHeight; + SeedEnabled = wdt == NULL || wdt->UserModSeed; + + if (wdt != NULL) { + Set_Range(Tiberium, wdt->TiberiumAmountMin, wdt->TiberiumAmountMax, seed.Tiberium, wdt->UserModTiberiumAmount); + Set_Range(Hills, wdt->HillsMin, wdt->HillsMax, seed.Hills, wdt->UserModHills); + Set_Range(Water, wdt->WaterMin, wdt->WaterMax, seed.WaterAmount, wdt->UserModWater); + Set_Range(Cliffs, wdt->CliffsMin, wdt->CliffsMax, seed.Cliffs, wdt->UserModCliffs); + Set_Range(Vegetation, wdt->VegetationMin, wdt->VegetationMax, seed.Vegetation, wdt->UserModVegetation); + Set_Range(Cities, wdt->CitiesMin, wdt->CitiesMax, seed.Cities, wdt->UserModCities); + Set_Range(TiberiumFields, wdt->TiberiumFieldsMin, wdt->TiberiumFieldsMax, seed.TiberiumLayout, wdt->UserModTiberiumFields); + Set_Range(Accessibility, wdt->AccessibilityMin, wdt->AccessibilityMax, seed.Accessibility, wdt->UserModAccessability); + Set_Range(Veinholes, 0, 5, seed.VeinholeMonsters, wdt->UserModVeinholeMonsters); + + // The territory fixes the teams, so the boxes say what they are rather than + // offering a choice. + OneOnOne = false; + TwoOnTwo = true; + + Lifeforms.State = seed.TiberiumWildlife > 0; + Lifeforms.Enabled = wdt->UserModTiberiumCreatures; + Transitions.State = seed.UseTransitions; + Transitions.Enabled = wdt->UserModTimeTransitions; + IonStorms.State = seed.UseIonStorms; + IonStorms.Enabled = true; + + // Nothing left to roll leaves the randomize button dead. + CanSurprise = wdt->UserModBiome || wdt->UserModTime || wdt->UserModCliffs + || wdt->UserModAccessability || wdt->UserModHills || wdt->UserModTiberiumAmount + || wdt->UserModTiberiumFields || wdt->UserModWater || wdt->UserModVegetation + || wdt->UserModCities || wdt->UserModWidth || wdt->UserModHeight + || wdt->UserModVeinholeMonsters; + } else { + Set_Range(Tiberium, 1, 100, seed.Tiberium, true); + Set_Range(Players, 2, MAX_PLAYERS, seed.NumPlayers, true); + Set_Range(Hills, 0, 100, seed.Hills, true); + Set_Range(Water, 0, 100, seed.WaterAmount, true); + Set_Range(Cliffs, 0, 100, seed.Cliffs, true); + Set_Range(Vegetation, 0, 100, seed.Vegetation, true); + Set_Range(Cities, 0, 100, seed.Cities, true); + Set_Range(TiberiumFields, 0, 100, seed.TiberiumLayout, true); + Set_Range(Accessibility, 0, 100, seed.Accessibility, true); + Set_Range(Veinholes, 0, 5, seed.VeinholeMonsters, true); + + Lifeforms.State = seed.TiberiumWildlife > 0; + Lifeforms.Enabled = true; + Transitions.State = seed.UseTransitions; + Transitions.Enabled = true; + IonStorms.State = seed.UseIonStorms; + IonStorms.Enabled = true; + + CanSurprise = true; + } + + Refresh_File_Buttons(); + SettingsChanged = true; +} + + +/// +/// The maintenance the driver ran on every pass of its own loop: the caller's progress +/// callback and the title screen behind the screen. +/// +void UIMapGenPresenterClass::Service(void) +{ + if (Callback != NULL) { + Callback(); + } + Title_Screen_Restore(false); +} + + +/// +/// Runs the browser the player asked for with this screen out of the way. +/// +void UIMapGenPresenterClass::Run_Pending(void) +{ + PendingType const pending = Pending; + Pending = PENDING_NONE; + + MapSeedClass & seed = RandomMapGen.SeedData; + + switch (pending) { + case PENDING_LOAD: + if (seed.LoadOptionsClass::Load()) { + Refresh(); + + // A loaded seed is previewed at once, which the dialog did by posting its + // own preview command to itself after it had put the settings back. + Queue(UIIntent{UI_MAPGEN_PREVIEW, "", 0}); + return; + } + Refresh(); + return; + + case PENDING_SAVE: + seed.MapDescription[0] = '\0'; + seed.LoadOptionsClass::Save(seed.MapDescription); + Refresh_File_Buttons(); + return; + + case PENDING_DELETE: + seed.LoadOptionsClass::Delete(); + Refresh_File_Buttons(); + return; + + default: + return; + } +} + + +/// +/// Writes the view-model back into the generator's settings, so that whatever the player +/// has dialed in becomes the seed the generator works from. +/// +void UIMapGenPresenterClass::Apply(void) +{ + MapSeedClass & seed = RandomMapGen.SeedData; + + seed.Biome = Biome; + seed.Time = Time; + seed.Width = Width; + seed.Height = Height; + seed.Seed = std::atoi(SeedText.c_str()); + + seed.Tiberium = Tiberium.Value; + seed.Hills = Hills.Value; + seed.WaterAmount = Water.Value; + seed.Cliffs = Cliffs.Value; + seed.Vegetation = Vegetation.Value; + seed.Cities = Cities.Value; + seed.TiberiumLayout = TiberiumFields.Value; + seed.Accessibility = Accessibility.Value; + + // A tournament territory fixes the player count at four; the variant that shows it has + // no player bar at all. + seed.NumPlayers = Variant == VARIANT_WDT ? 4 : Players.Value; + + seed.TiberiumWildlife = 0; + seed.VeinholeMonsters = 0; + seed.UseIonStorms = false; + seed.UseTransitions = false; + seed.UseBlueTiberium = false; + + if (Addon_Enabled(ADDON_FIRESTORM)) { + seed.TiberiumWildlife = Lifeforms.State ? 30 : 0; + seed.VeinholeMonsters = Veinholes.Value; + seed.UseIonStorms = IonStorms.State; + seed.UseTransitions = Transitions.State; + seed.UseBlueTiberium = (double)seed.Tiberium > 0.75; + } + + seed.Fixup_Settings(); +} + + +/// +/// Reports what the view-model holds for a control, so a view can drop a change that only +/// puts back the value it was given. +/// +int UIMapGenPresenterClass::Value_Of(std::string const & field) const +{ + if (field == "biome") return(Biome); + if (field == "time") return(Time); + if (field == "width") return(Width); + if (field == "height") return(Height); + + if (field == "players") return(Players.Value); + if (field == "accessibility") return(Accessibility.Value); + if (field == "cliffs") return(Cliffs.Value); + if (field == "hills") return(Hills.Value); + if (field == "tiberium") return(Tiberium.Value); + if (field == "tiberiumfields") return(TiberiumFields.Value); + if (field == "water") return(Water.Value); + if (field == "vegetation") return(Vegetation.Value); + if (field == "cities") return(Cities.Value); + if (field == "veinholes") return(Veinholes.Value); + + return(-1); +} + + +void UIMapGenPresenterClass::Set_Value(std::string const & field, int value) +{ + if (field == "biome") { Biome = value; return; } + if (field == "time") { Time = value; return; } + if (field == "width") { Width = value; return; } + if (field == "height") { Height = value; return; } + + if (field == "players") { Players.Value = value; return; } + if (field == "accessibility") { Accessibility.Value = value; return; } + if (field == "cliffs") { Cliffs.Value = value; return; } + if (field == "hills") { Hills.Value = value; return; } + if (field == "tiberium") { Tiberium.Value = value; return; } + if (field == "tiberiumfields") { TiberiumFields.Value = value; return; } + if (field == "water") { Water.Value = value; return; } + if (field == "vegetation") { Vegetation.Value = value; return; } + if (field == "cities") { Cities.Value = value; return; } + if (field == "veinholes") { Veinholes.Value = value; return; } + +} + + +/// +/// Builds the three lists the combo boxes offer. The mutated environment belongs to +/// Firestorm and is left out without it. +/// +void UIMapGenPresenterClass::Build_Choices(void) +{ + Biomes.clear(); + for (int index = BIOME_FIRST; index < BIOME_COUNT; index++) { + if (index != BIOME_MUTATED || Addon_Enabled(ADDON_FIRESTORM)) { + Biomes.push_back(ChoiceType{Fetch_String(_BiomeNames[index]), index}); + } + } + + Times.clear(); + for (int index = TIME_OF_DAY_FIRST; index < TIME_OF_DAY_COUNT; index++) { + Times.push_back(ChoiceType{Fetch_String(_TimeNames[index]), index}); + } + + Sizes.clear(); + for (int index = 0; index < MAPSIZE_COUNT; index++) { + Sizes.push_back(ChoiceType{Fetch_String(_SizeNames[index]), index}); + } + + // Only the environment and time of day combos carry CBS_SORT, so only those two are + // listed by name; the two size lists keep the order their settings are numbered in. + auto by_name = [](ChoiceType const & left, ChoiceType const & right) { return(left.Name < right.Name); }; + std::sort(Biomes.begin(), Biomes.end(), by_name); + std::sort(Times.begin(), Times.end(), by_name); +} + + +void UIMapGenPresenterClass::Answer(int answer) +{ + UIResult result; + result.Outcome = answer == ANSWER_ACCEPTED ? UIResult::OUTCOME_ACCEPTED : UIResult::OUTCOME_CANCELLED; + result.Value = answer; + Result = result; +} + + +/// +/// Builds the map the current settings describe and keeps it as the seed a later accept +/// works from. +/// +void UIMapGenPresenterClass::Generate_Preview(void) +{ + RandomMapGen.Generate_Random_Map(true); + RandomMapGen.MapPreview->Create_Preview(); + + delete RandomMapGen.MapSeeder; + RandomMapGen.MapSeeder = new MapSeedClass; + memcpy(RandomMapGen.MapSeeder, &RandomMapGen.SeedData, sizeof(MapSeedClass)); + + PreviewChanged = true; +} + + +void UIMapGenPresenterClass::Refresh_File_Buttons(void) +{ + bool const present = RandomMapGen.SeedData.Files_Present(); + CanLoad = present; + CanDelete = present; +} + + +/* +** The RmlUi view. The three templates are one document family: they differ by which +** controls exist and where they stand, so each carries its own document and its own model +** name. +*/ +namespace { + + // A combo row as the document shows it. + struct ChoiceViewType + { + std::string Name; + int Value = 0; + }; + + + class MapGenViewClass : public UIRmlViewClass + { + public: + MapGenViewClass(UIMapGenPresenterClass & presenter, char const * document) + : UIRmlViewClass(presenter, document), Screen(presenter) {} + + virtual void Bind(Rml::DataModelConstructor & model) override; + virtual void Sync(void) override; + + // Puts the ranges on the track bars before their values, because a range control + // clamps a value into the range it is holding. + void Arm_Ranges(void); + + void Attach_Preview(void); + void Release_Preview(void); + + private: + void Rebuild_Rows(void); + void Bind_Range(Rml::DataModelConstructor & model, char const * name, + UIMapGenPresenterClass::RangeType & range); + + UIMapGenPresenterClass & Screen; + + std::vector BiomeRows; + std::vector TimeRows; + std::vector SizeRows; + + std::unique_ptr Preview; + + // Have the controls been given their spans yet? A change raised while they are + // being armed is the model settling, not the player moving anything. + bool Settled = false; + }; + + + // The name the document gives the preview's pixels, and the interior of the template's + // preview frame in game logical units. + char const * const PREVIEW_SURFACE = "mapgenpreview"; + int const PREVIEW_WIDTH = 292; + int const PREVIEW_HEIGHT = 214; + + + void MapGenViewClass::Rebuild_Rows(void) + { + BiomeRows.clear(); + for (UIMapGenPresenterClass::ChoiceType const & row : Screen.Biomes) { + BiomeRows.push_back(ChoiceViewType{row.Name, row.Value}); + } + + TimeRows.clear(); + for (UIMapGenPresenterClass::ChoiceType const & row : Screen.Times) { + TimeRows.push_back(ChoiceViewType{row.Name, row.Value}); + } + + SizeRows.clear(); + for (UIMapGenPresenterClass::ChoiceType const & row : Screen.Sizes) { + SizeRows.push_back(ChoiceViewType{row.Name, row.Value}); + } + } + + + void MapGenViewClass::Bind_Range(Rml::DataModelConstructor & model, char const * name, + UIMapGenPresenterClass::RangeType & range) + { + model.Bind(name, &range.Value); + + Rml::String enabled = name; + enabled += "on"; + model.Bind(enabled, &range.Enabled); + } + + + void MapGenViewClass::Bind(Rml::DataModelConstructor & model) + { + Rebuild_Rows(); + + if (auto choice = model.RegisterStruct()) { + choice.RegisterMember("name", &ChoiceViewType::Name); + choice.RegisterMember("value", &ChoiceViewType::Value); + } + model.RegisterArray>(); + + model.Bind("biomes", &BiomeRows); + model.Bind("times", &TimeRows); + model.Bind("sizes", &SizeRows); + + model.Bind("biome", &Screen.Biome); + model.Bind("time", &Screen.Time); + model.Bind("width", &Screen.Width); + model.Bind("height", &Screen.Height); + + model.Bind("biomeon", &Screen.BiomeEnabled); + model.Bind("timeon", &Screen.TimeEnabled); + model.Bind("widthon", &Screen.WidthEnabled); + model.Bind("heighton", &Screen.HeightEnabled); + + model.Bind("seed", &Screen.SeedText); + model.Bind("seedon", &Screen.SeedEnabled); + + Bind_Range(model, "players", Screen.Players); + Bind_Range(model, "accessibility", Screen.Accessibility); + Bind_Range(model, "cliffs", Screen.Cliffs); + Bind_Range(model, "hills", Screen.Hills); + Bind_Range(model, "tiberium", Screen.Tiberium); + Bind_Range(model, "tiberiumfields", Screen.TiberiumFields); + Bind_Range(model, "water", Screen.Water); + Bind_Range(model, "vegetation", Screen.Vegetation); + Bind_Range(model, "cities", Screen.Cities); + Bind_Range(model, "veinholes", Screen.Veinholes); + + model.Bind("lifeforms", &Screen.Lifeforms.State); + model.Bind("lifeformson", &Screen.Lifeforms.Enabled); + model.Bind("ionstorms", &Screen.IonStorms.State); + model.Bind("ionstormson", &Screen.IonStorms.Enabled); + model.Bind("transitions", &Screen.Transitions.State); + model.Bind("transitionson", &Screen.Transitions.Enabled); + + model.Bind("oneonone", &Screen.OneOnOne); + model.Bind("twoontwo", &Screen.TwoOnTwo); + + model.Bind("cansurprise", &Screen.CanSurprise); + model.Bind("canpreview", &Screen.CanPreview); + model.Bind("canload", &Screen.CanLoad); + model.Bind("candelete", &Screen.CanDelete); + + model.BindEventCallback("press", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{arguments[0].Get(), "", 0}); + }); + + // A form control is bound one way and a change matching the value the model holds is + // dropped, so putting the model on a control cannot look like the player moving it. + model.BindEventCallback("change", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const & arguments) { + if (!Settled || arguments.empty()) return; + + Rml::String const field = arguments[0].Get(); + int const value = (int)(event.GetParameter("value", 0.0f) + 0.5f); + if (value == Screen.Value_Of(field)) return; + + Screen.Queue(UIIntent{UI_MAPGEN_SET, field, value}); + }); + + model.BindEventCallback("toggle", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{UI_MAPGEN_TOGGLE, arguments[0].Get(), 0}); + }); + + model.BindEventCallback("typeseed", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + Rml::String const value = event.GetParameter("value", Rml::String()); + if (value == Screen.SeedText) return; + Screen.Queue(UIIntent{UI_MAPGEN_SEED, value, 0}); + }); + + model.BindEventCallback("key", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + int const key = event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN); + if (key == Rml::Input::KI_ESCAPE) { + Screen.Queue(UIIntent{UI_MAPGEN_CANCEL, "", 0}); + } + }); + } + + + /// + /// Puts each track bar's span on the control before its value, because a range control + /// clamps a value into the range it is holding. + /// + void MapGenViewClass::Arm_Ranges(void) + { + if (Element == nullptr) return; + + Settled = false; + + struct { char const * Id; UIMapGenPresenterClass::RangeType const * Range; } const bars[] = { + { "players", &Screen.Players }, + { "accessibility", &Screen.Accessibility }, + { "cliffs", &Screen.Cliffs }, + { "hills", &Screen.Hills }, + { "tiberium", &Screen.Tiberium }, + { "tiberiumfields", &Screen.TiberiumFields }, + { "water", &Screen.Water }, + { "vegetation", &Screen.Vegetation }, + { "cities", &Screen.Cities }, + { "veinholes", &Screen.Veinholes }, + }; + + for (auto const & bar : bars) { + Rml::Element * const element = Element->GetElementById(bar.Id); + if (element == nullptr) continue; + + element->SetAttribute("min", bar.Range->Min); + element->SetAttribute("max", bar.Range->Max); + element->SetAttribute("step", 1); + element->SetAttribute("value", bar.Range->Value); + } + + Settled = true; + } + + + void MapGenViewClass::Attach_Preview(void) + { + Preview = std::make_unique(PREVIEW_WIDTH, PREVIEW_HEIGHT, &RandomMapGen.MapPreview); + UI_Register_Surface(PREVIEW_SURFACE, Preview.get()); + } + + + void MapGenViewClass::Release_Preview(void) + { + UI_Unregister_Surface(PREVIEW_SURFACE); + Preview.reset(); + } + + + void MapGenViewClass::Sync(void) + { + if (!Model) return; + + if (Screen.PreviewChanged && Preview != nullptr) { + Preview->Redraw(); + Screen.PreviewChanged = false; + } + + if (Screen.SettingsChanged) { + Arm_Ranges(); + Screen.SettingsChanged = false; + } + + Model.DirtyAllVariables(); + } + + + MapGenViewClass * _View = NULL; + +} // namespace + + +void UI_MapGen_Preview_Changed(void) +{ + if (_MapGenScreen == NULL || _View == NULL) { + return; + } + + // The generator does not pump between phases, so the picture is put on screen here, the + // way the dialog got a synchronous repaint out of SendMessage. + _MapGenScreen->PreviewChanged = true; + _View->Sync(); + UI_Paint_Now(false); +} + + +/// +/// Shows the variant this session gets and runs it until the player accepts or cancels. +/// +/// What Do_Random_Map_Dialog reports: 1 accepted, 2 cancelled, 0 not shown. +int UI_MapGen_Run(UIMapGenPresenterClass & presenter) +{ + char const * document = "mapgen.rml"; + if (presenter.Variant == UIMapGenPresenterClass::VARIANT_WDT) { + document = "mapgenwdt.rml"; + } else if (presenter.Variant == UIMapGenPresenterClass::VARIANT_FIRESTORM) { + document = "mapgenfs.rml"; + } + + MapGenViewClass * const view = new MapGenViewClass(presenter, document); + if (!view->Prepare(true)) { + delete view; + return(0); + } + + _View = view; + _MapGenScreen = &presenter; + + view->Attach_Preview(); + view->Arm_Ranges(); + view->Sync(); + + while (!presenter.Result.has_value()) { + UI_Run_Modal(presenter, *view); + + if (!presenter.Suspends()) { + break; + } + + // A browser draws where this screen is, so the document steps aside for it. + view->Hide(); + presenter.Run_Pending(); + view->Show(); + view->Sync(); + } + + view->Release_Preview(); + view->Close(); + + _MapGenScreen = NULL; + _View = NULL; + delete view; + + return(presenter.Result.has_value() ? presenter.Result->Value : UIMapGenPresenterClass::ANSWER_CANCELLED); +} diff --git a/code/ui/uimapgen.h b/code/ui/uimapgen.h new file mode 100644 index 000000000..6f3e089c1 --- /dev/null +++ b/code/ui/uimapgen.h @@ -0,0 +1,191 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The random map generator screen. One screen with three variants -- the base game's, the +// Firestorm one and the tournament one -- chosen by what the session is rather than by the +// caller, which is the sound screen's shape. +// +// docs/UI_DESIGN.md, "Screens", owns the contract. + +#pragma once + +#include "uiscreen.h" + +#include +#include + + +inline constexpr char const * UI_MAPGEN_OK = "ok"; +inline constexpr char const * UI_MAPGEN_CANCEL = "cancel"; +inline constexpr char const * UI_MAPGEN_LOAD = "load"; +inline constexpr char const * UI_MAPGEN_SAVE = "save"; +inline constexpr char const * UI_MAPGEN_DELETE = "delete"; +inline constexpr char const * UI_MAPGEN_PREVIEW = "preview"; +inline constexpr char const * UI_MAPGEN_SURPRISE = "surprise"; + +// Carries a control's new value: the identity names the setting and the value carries it. +inline constexpr char const * UI_MAPGEN_SET = "set"; + +// Flips a check box. The identity names it. +inline constexpr char const * UI_MAPGEN_TOGGLE = "toggle"; + +// Carries the seed the player typed, in the identity, because it arrives as text. +inline constexpr char const * UI_MAPGEN_SEED = "seed"; + + +class UIMapGenPresenterClass : public UIPresenterClass +{ + public: + // Which of the three templates the screen is. The addon and the session decide it, + // not the menu that opened the screen. + enum VariantType { + VARIANT_BASE, + VARIANT_FIRESTORM, + VARIANT_WDT, + }; + + // What the screen answers its driver with. Do_Random_Map_Dialog reports 1 for an + // accepted map and 2 for a cancelled screen. + enum { + ANSWER_ACCEPTED = 1, + ANSWER_CANCELLED = 2, + }; + + // A choice a combo box offers. The value is the setting, not the row, because two + // of the lists are sorted by name and a row is not a setting. + struct ChoiceType + { + std::string Name; + int Value = 0; + }; + + // A track bar and the span it is allowed. A tournament territory narrows the span + // and may lock the bar, and a span with nothing to choose between is shown disabled + // rather than hidden, which is what Set_Scroll_Bar did. + struct RangeType + { + int Min = 0; + int Max = 100; + int Value = 0; + bool Enabled = true; + }; + + // A check box and whether the player may change it. + struct SwitchType + { + bool State = false; + bool Enabled = true; + }; + + virtual void Execute(UIIntent const & intent) override; + virtual void Refresh(void) override; + virtual void Service(void) override; + + // The load, save and delete browsers draw where this screen is, so this screen is + // stepped aside for them rather than run underneath. + virtual bool Suspends(void) const override { return(Pending != PENDING_NONE); } + + // Picks the variant and reads the generator's settings into the view-model. Called + // once, before a view is prepared. + void Open(bool (*callback)()); + + // What the view-model holds for a control, so a view can drop a change that only puts + // back the value it was given. + int Value_Of(std::string const & field) const; + + // Runs the browser the player asked for with this screen out of the way. Called by + // the owner between passes, never from an event. + void Run_Pending(void); + + /* + ** The view-model. + */ + + VariantType Variant = VARIANT_BASE; + + std::vector Biomes; + std::vector Times; + std::vector Sizes; + + int Biome = 0; + int Time = 0; + int Width = 0; + int Height = 0; + + bool BiomeEnabled = true; + bool TimeEnabled = true; + bool WidthEnabled = true; + bool HeightEnabled = true; + + std::string SeedText; + bool SeedEnabled = true; + + RangeType Players; + RangeType Accessibility; + RangeType Cliffs; + RangeType Hills; + RangeType Tiberium; + RangeType TiberiumFields; + RangeType Water; + RangeType Vegetation; + RangeType Cities; + RangeType Veinholes; + + SwitchType Lifeforms; + SwitchType IonStorms; + SwitchType Transitions; + + // The tournament variant's two team boxes. The dialog set them and left them + // disabled, so they say what the territory is rather than offering a choice. + bool OneOnOne = false; + bool TwoOnTwo = true; + + bool CanSurprise = true; + bool CanPreview = true; + bool CanLoad = false; + bool CanDelete = false; + + // Has the preview picture moved? The view reads it and clears it, because the + // picture is the view's to own. + bool PreviewChanged = false; + + bool SettingsChanged = false; + + private: + enum PendingType { + PENDING_NONE, + PENDING_LOAD, + PENDING_SAVE, + PENDING_DELETE, + }; + + void Apply(void); + void Set_Value(std::string const & field, int value); + void Build_Choices(void); + void Answer(int answer); + void Generate_Preview(void); + void Refresh_File_Buttons(void); + + PendingType Pending = PENDING_NONE; + + // The maintenance the driver ran on every pass of its own loop. + bool (*Callback)(void) = NULL; +}; + + +// The generator screen the driver is running, or NULL when none is up. The generator reaches +// the picture through this while it is building a map. +UIMapGenPresenterClass * UI_MapGen_Screen(void); + +// The map preview has been redrawn. Called where the generator told the dialog to repaint. +void UI_MapGen_Preview_Changed(void); + +// Shows the variant the screen says it is and runs it until the player accepts or cancels. +// The answer is what Do_Random_Map_Dialog returns; zero means no document was shown. +int UI_MapGen_Run(UIMapGenPresenterClass & presenter); diff --git a/code/ui/uimappreview.cpp b/code/ui/uimappreview.cpp new file mode 100644 index 000000000..5a3774738 --- /dev/null +++ b/code/ui/uimappreview.cpp @@ -0,0 +1,74 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#include "always.h" + +#include "uimappreview.h" + +#include "dsurface.h" +#include "netshare.h" +#include "preview.h" +#include "xsurface.h" + +#include + + +MapPreviewSurfaceClass::MapPreviewSurfaceClass(int width, int height, MapPreviewClass * const * source) : + UISurfaceBufferClass(width, height), + Width(width), + Height(height), + Source(source != NULL ? source : &MultiplayerMapPreview) +{ + Set_Transparent_Color(DSurface::Build_Hicolor_Pixel(255, 0, 255)); + Clear(); +} + + +void MapPreviewSurfaceClass::Redraw(void) +{ + Clear(); + + if (*Source == NULL) { + return; + } + + XSurface * const picture = (*Source)->Get_Preview_Surface(); + if (picture == NULL) { + return; + } + + Rect const source = picture->Get_Rect(); + if (source.Width <= 0 || source.Height <= 0) { + return; + } + + int const scale = std::min(1000 * Width / source.Width, 1000 * Height / source.Height); + + Rect destination; + destination.Width = (scale * source.Width) / 1000; + destination.Height = (scale * source.Height) / 1000; + destination.X = Width / 2 - destination.Width / 2; + destination.Y = Height / 2 - destination.Height / 2; + + // The picture is resampled here rather than blitted. Bit_Blit copies the smaller of the + // two rectangles row for row, so an engine surface blit between rectangles of different + // sizes crops the picture instead of scaling it; only DSurface's own blitter stretches, + // and this buffer is not one. + Surface & buffer = Get_Surface(); + for (int y = 0; y < destination.Height; y++) { + int const sy = source.Y + (y * source.Height) / destination.Height; + for (int x = 0; x < destination.Width; x++) { + int const sx = source.X + (x * source.Width) / destination.Width; + buffer.Put_Pixel(Point2D(destination.X + x, destination.Y + y), + picture->Get_Pixel(Point2D(sx, sy))); + } + } + + Mark_Dirty(); +} diff --git a/code/ui/uimappreview.h b/code/ui/uimappreview.h new file mode 100644 index 000000000..b12775c10 --- /dev/null +++ b/code/ui/uimappreview.h @@ -0,0 +1,42 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The multiplayer map preview as a surface a document can show. The map selection screen +// and the skirmish setup screen both hold one, at the size their own template's preview +// frame gives it. +// +// docs/UI_DESIGN.md, "Assets and strings", owns the routing. + +#pragma once + +#include "uisurface.h" + + +class MapPreviewClass; + + +class MapPreviewSurfaceClass : public UISurfaceBufferClass +{ + public: + // The extents are the interior of the template's preview frame, in game logical + // units, because a provider's pixels are game logical units. The source is the + // variable holding the picture, not the picture, because every owner replaces its + // preview object rather than redrawing one; NULL means the session's own. + MapPreviewSurfaceClass(int width, int height, MapPreviewClass * const * source = NULL); + + // Draws the session's current preview, scaled and centered the way + // MapPreviewClass::Blit_Preview scales it into a dialog's group box. The letterbox + // around a picture of a different shape is left transparent rather than black. + void Redraw(void); + + private: + int Width; + int Height; + MapPreviewClass * const * Source; +}; diff --git a/code/ui/uimessagebox.cpp b/code/ui/uimessagebox.cpp new file mode 100644 index 000000000..8200314e0 --- /dev/null +++ b/code/ui/uimessagebox.cpp @@ -0,0 +1,422 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The message box and the wait box. The message box is the first screen that runs the game +// underneath itself: in a network session the modal runner keeps stepping Main_Loop while +// the box owns the input, which is what WWMessageBox::Process has always done through +// OwnerDraw::Dialog_Message_Handler. +// +// What is preserved from the dialog, and why each of these is here rather than obvious: +// the buttons are laid out first, third, second across the box, which is the order the +// IDD_MSGBOX_3 template places them in; a lone button moves to the middle slot; Enter +// answers with the caller's default response rather than with a button, because the +// template names no default push button and Windows then sends IDOK; Escape answers with +// button two, because Windows sends IDCANCEL whether or not that button exists; and a box +// with no button at all answers with zero without waiting. +// +// docs/UI_DESIGN.md, "Screens" and "Scheduling", own the contracts this keeps to. + +#include "always.h" + +#include "uimessagebox.h" + +#include "uiinternal.h" +#include "uirmlview.h" + +#include "_keyboar.h" +#include "globals.h" +#include "init.h" +#include "keyboard.h" + +#include +#include +#include + +#include +#include + + +// The vocabulary a document has. A press names the button it came from; the other two are +// the keyboard's answers, which are not buttons and do not carry an index of their own. +static char const * const ACTION_PRESS = "press"; +static char const * const ACTION_DEFAULT = "default"; +static char const * const ACTION_CANCEL = "cancel"; + +// The button index Windows produces for the Escape key, which reaches the dialog as +// IDCANCEL and so answers with the second button whether or not one is shown. +static int const ESCAPE_RESPONSE = 1; + + +/// +/// The toolkit-free half of the message box. +/// +class MessageBoxPresenterClass : public UIPresenterClass +{ + public: + // The view-model. Plain values, and the only thing a document reads. + std::string Message; + std::string Buttons[3]; + bool Shown[3] = { false, false, false }; + + // Does the first button sit in the middle slot? It does when it is the only one, as + // the dialog moved it there. + bool FirstIsCentred = false; + + int DefaultResponse = 0; + + // The caller's own per-pass work, polled while the box is up. WS_Wait_Dialog took + // one for the same reason, so a lobby keeps answering the network under a warning. + bool (*ServiceRoutine)(void) = nullptr; + + virtual void Execute(UIIntent const & intent) override; + virtual void Refresh(void) override {} + virtual void Service(void) override; +}; + + +/// +/// Answers an intent the view raised. Every one of them ends the screen, so the mapping +/// from what the player did to what the caller is told is the whole of the behavior. +/// +void MessageBoxPresenterClass::Execute(UIIntent const & intent) +{ + UIResult result; + + if (intent.Action == ACTION_PRESS) { + if (intent.Value < 0 || intent.Value > 2 || !Shown[intent.Value]) { + return; + } + result.Outcome = UIResult::OUTCOME_ACCEPTED; + result.Value = intent.Value; + } else if (intent.Action == ACTION_DEFAULT) { + result.Outcome = UIResult::OUTCOME_ACCEPTED; + result.Value = DefaultResponse; + } else if (intent.Action == ACTION_CANCEL) { + result.Outcome = UIResult::OUTCOME_CANCELLED; + result.Value = ESCAPE_RESPONSE; + } else { + return; + } + + Result = result; +} + + +/// +/// The maintenance the dialog driver ran on every pass of its own loop. +/// +void MessageBoxPresenterClass::Service(void) +{ + if (!GameActive) { + Title_Screen_Restore(); + } + + if (ServiceRoutine != nullptr && ServiceRoutine() && !Result.has_value()) { + UIResult result; + result.Outcome = UIResult::OUTCOME_CANCELLED; + result.Value = ESCAPE_RESPONSE; + Result = result; + } +} + + +/// +/// The RmlUi half of the message box. +/// +class MessageBoxViewClass : public UIRmlViewClass +{ + public: + MessageBoxViewClass(MessageBoxPresenterClass & presenter); + + virtual void Bind(Rml::DataModelConstructor & model) override; + virtual void Sync(void) override; + + private: + void Answer(char const * action, int value); + + MessageBoxPresenterClass & Screen; +}; + + +MessageBoxViewClass::MessageBoxViewClass(MessageBoxPresenterClass & presenter) : + UIRmlViewClass(presenter, "messagebox.rml"), + Screen(presenter) +{ +} + + +void MessageBoxViewClass::Answer(char const * action, int value) +{ + UIIntent intent; + intent.Action = action; + intent.Value = value; + Screen.Queue(intent); +} + + +void MessageBoxViewClass::Bind(Rml::DataModelConstructor & model) +{ + model.Bind("message", &Screen.Message); + model.Bind("button1", &Screen.Buttons[0]); + model.Bind("button2", &Screen.Buttons[1]); + model.Bind("button3", &Screen.Buttons[2]); + model.Bind("shown1", &Screen.Shown[0]); + model.Bind("shown2", &Screen.Shown[1]); + model.Bind("shown3", &Screen.Shown[2]); + model.Bind("centred", &Screen.FirstIsCentred); + + // An event handler never acts: it queues, and the runner executes the queue after + // Context::Update has returned. + model.BindEventCallback("press", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + Answer(ACTION_PRESS, arguments.empty() ? 0 : (int)arguments[0].Get()); + }); + + // Enter and Escape are the box's own hotkeys, and neither answers with a button: Enter + // yields the caller's default response and Escape the second button's index. + model.BindEventCallback("key", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + int const key = event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN); + + if (key == Rml::Input::KI_RETURN || key == Rml::Input::KI_NUMPADENTER) { + Answer(ACTION_DEFAULT, 0); + } else if (key == Rml::Input::KI_ESCAPE) { + Answer(ACTION_CANCEL, 0); + } + }); +} + + +void MessageBoxViewClass::Sync(void) +{ + // Nothing an intent can execute changes the view-model. Every one of them ends the + // screen instead. +} + + +/// +/// Shows a message and waits for the player to answer it. +/// +UIResult UI_Message_Box_Screen(char const * message, int defresponse, + char const * b1txt, char const * b2txt, char const * b3txt, + bool (*service)(void)) +{ + // The presenter is declared first so that it is destroyed last: the data model the view + // binds reads the presenter's view-model, and must not outlive it. + MessageBoxPresenterClass presenter; + MessageBoxViewClass view(presenter); + + char const * const captions[3] = { b1txt, b2txt, b3txt }; + + // The dialog counted its buttons this way: each caption that is present raises the count + // to its own slot, so a box that skips a slot still counts by the highest one filled. + int count = 0; + for (int index = 0; index < 3; index++) { + if (captions[index] != nullptr && captions[index][0] != '\0') { + presenter.Buttons[index] = captions[index]; + presenter.Shown[index] = true; + count = index + 1; + } + } + + // A box with nothing to press is answered for the player, without a pass of the loop and + // so without a frame in which it could be seen. + if (count == 0) { + UIResult result; + result.Outcome = UIResult::OUTCOME_ACCEPTED; + result.Value = 0; + return(result); + } + + presenter.FirstIsCentred = (count == 1); + presenter.DefaultResponse = defresponse; + presenter.ServiceRoutine = service; + + if (message != nullptr) { + presenter.Message = message; + } + + if (!view.Prepare(true)) { + UIResult result; + result.Outcome = UIResult::OUTCOME_FAILED_TO_OPEN; + return(result); + } + + UIResult const result = UI_Run_Modal(presenter, view); + view.Close(); + return(result); +} + + +//--------------------------------------------------------------------------------------- +// The wait box. It stands over a long operation rather than waiting for an answer, so it +// is not modal and the caller's own loop keeps running underneath it. +//--------------------------------------------------------------------------------------- + +class WaitBoxPresenterClass : public UIPresenterClass +{ + public: + std::string Message; + std::string CancelCaption; + bool CanCancel = false; + + // The caller's flag, raised when the player cancels. It is the caller's storage, the + // way the dialog kept it in DWLP_USER, and it outlives the box. + bool * Cancelled = nullptr; + + virtual void Execute(UIIntent const & intent) override; + virtual void Refresh(void) override {} +}; + + +void WaitBoxPresenterClass::Execute(UIIntent const & intent) +{ + if (intent.Action != ACTION_CANCEL || !CanCancel) { + return; + } + + // The escape key is what the operation underneath is watching for; the flag only tells + // the caller which of the two ways out was taken. + if (Keyboard != nullptr) { + Keyboard->Put(KN_ESC); + } + + if (Cancelled != nullptr) { + *Cancelled = true; + } +} + + +class WaitBoxViewClass : public UIRmlViewClass +{ + public: + WaitBoxViewClass(WaitBoxPresenterClass & presenter); + + virtual void Bind(Rml::DataModelConstructor & model) override; + virtual void Sync(void) override; + + private: + WaitBoxPresenterClass & Screen; +}; + + +WaitBoxViewClass::WaitBoxViewClass(WaitBoxPresenterClass & presenter) : + UIRmlViewClass(presenter, "waitbox.rml"), + Screen(presenter) +{ +} + + +void WaitBoxViewClass::Bind(Rml::DataModelConstructor & model) +{ + model.Bind("message", &Screen.Message); + model.Bind("cancelcaption", &Screen.CancelCaption); + model.Bind("cancancel", &Screen.CanCancel); + + model.BindEventCallback("cancel", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const &) { + UIIntent intent; + intent.Action = ACTION_CANCEL; + Screen.Queue(intent); + }); +} + + +void WaitBoxViewClass::Sync(void) +{ + if (Model) { + Model.DirtyVariable("message"); + } +} + + +// The one wait box. Every caller opens one, holds it for the length of an operation and +// closes it, and no caller opens a second while one is up. +static std::unique_ptr _WaitPresenter; +static std::unique_ptr _WaitView; + + +bool UI_Wait_Box_Open(char const * message, char const * cancelcaption, bool * cancelled) +{ + if (_WaitView != nullptr) { + return(false); + } + + // The presenter is created first so that it is destroyed last, for the same reason the + // modal screens declare it first. + _WaitPresenter = std::make_unique(); + _WaitView = std::make_unique(*_WaitPresenter); + + if (message != nullptr) { + _WaitPresenter->Message = message; + } + + if (cancelcaption != nullptr && cancelcaption[0] != '\0') { + _WaitPresenter->CancelCaption = cancelcaption; + _WaitPresenter->CanCancel = true; + } + + _WaitPresenter->Cancelled = cancelled; + + if (!_WaitView->Prepare(false)) { + _WaitView.reset(); + _WaitPresenter.reset(); + return(false); + } + + // The box must be on screen before the operation underneath begins. A caller that saves + // a game and never pumps again would otherwise show nothing at all. + UI_Paint_Now(true); + return(true); +} + + +void UI_Wait_Box_Set_Text(char const * message) +{ + if (_WaitView == nullptr) { + return; + } + + _WaitPresenter->Message = (message != nullptr) ? message : ""; + _WaitView->Sync(); + + // Set_Custom_Message_Box_Text repainted the box before it returned, through UpdateWindow. + UI_Paint_Now(true); +} + + +void UI_Wait_Box_Close(void) +{ + if (_WaitView == nullptr) { + return; + } + + _WaitView->Close(); + _WaitView.reset(); + _WaitPresenter.reset(); +} + + +bool UI_Wait_Box_Is_Open(void) +{ + return(_WaitView != nullptr); +} + + +/// +/// Executes what the wait box's own events queued. +/// The box has no loop of its own, so the shell's tick is its safe point: the queue is +/// drained after Context::Update has returned and never from inside an event handler. +/// +void UI_Message_Box_Service(void) +{ + if (_WaitPresenter != nullptr) { + _WaitPresenter->Drain(); + } +} diff --git a/code/ui/uimessagebox.h b/code/ui/uimessagebox.h new file mode 100644 index 000000000..51cd31fab --- /dev/null +++ b/code/ui/uimessagebox.h @@ -0,0 +1,44 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The message box and the wait box, the two screens every other screen can open. Only the +// result contract crosses this header, so a caller carries no toolkit and no window handle. +// +// docs/UI_DESIGN.md, "Screens", owns the contract. + +#pragma once + +#include "uiscreen.h" + + +// Shows a message with up to three buttons and does not return until one is answered. The +// result's Value is the index of the button the player picked, counted the way +// WWMessageBox::Process counts them; GameEnded says the session ended underneath the box. +// +// The service routine, where one is given, is run once a pass while the box is up. A caller +// whose own work has to keep going underneath the box supplies it, the way the lobby handed +// its network poll to WS_Wait_Dialog; a true return answers the box the way its cancel does. +UIResult UI_Message_Box_Screen(char const * message, int defresponse, + char const * b1txt, char const * b2txt, char const * b3txt, + bool (*service)(void) = nullptr); + + +// Opens the box that stands over a long operation. It is not modal: the caller keeps +// running its own loop underneath and closes the box when the operation finishes. +// +// A caption for the cancel button shows it; cancelling raises the flag and feeds an escape +// key to the game keyboard, as the dialog's cancel button does. The flag is read while the +// box is open, so it must outlive it. +bool UI_Wait_Box_Open(char const * message, char const * cancelcaption, bool * cancelled); + +// Replaces the text of the box that is already showing. +void UI_Wait_Box_Set_Text(char const * message); + +void UI_Wait_Box_Close(void); +bool UI_Wait_Box_Is_Open(void); diff --git a/code/ui/uimpselect.cpp b/code/ui/uimpselect.cpp new file mode 100644 index 000000000..67ce7e912 --- /dev/null +++ b/code/ui/uimpselect.cpp @@ -0,0 +1,163 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The multiplayer game selection screen. Behavior traced out of Select_MPlayer_Game and its +// procedure in mplayer.cpp. +// +// What the extraction fixes in place: only the network and skirmish buttons answer, and +// anything else leaves with no session chosen, which is the dialog's own default arm; the +// internet and world domination buttons stay where the template put them and are disabled, +// because neither the service they led to nor the tour it hosted can be reached; and the +// modem and serial button is on the template but reaches nothing, so it leaves as well. +// +// docs/UI_DESIGN.md, "Screens", owns the contracts this keeps to. + +#include "always.h" + +#include "uimpselect.h" + +#include "uirmlview.h" + +#include "addon.h" +#include "init.h" +#include "session.h" + +#include +#include +#include + + +void UIMPSelectPresenterClass::Refresh(void) +{ + Variant = (Addon_Installed(ADDON_FIRESTORM) == ADDON_FIRESTORM) ? VARIANT_FIRESTORM : VARIANT_BASE; + InternetAvailable = false; + WorldDominationAvailable = false; + Choice = CHOICE_NONE; +} + + +/// +/// The maintenance the dialog driver ran on every pass of its own loop. +/// +void UIMPSelectPresenterClass::Service(void) +{ + Title_Screen_Restore(); +} + + +/// +/// The session type the caller's own switch is written against. +/// +int UIMPSelectPresenterClass::Session_Type(void) const +{ + switch (Choice) { + case CHOICE_NETWORK: return(GAME_IPX); + case CHOICE_SKIRMISH: return(GAME_SKIRMISH); + default: return(GAME_NORMAL); + } +} + + +void UIMPSelectPresenterClass::Execute(UIIntent const & intent) +{ + if (intent.Action == UI_MPSELECT_INTERNET && !InternetAvailable) { + return; + } + if (intent.Action == UI_MPSELECT_WORLDDOM && !WorldDominationAvailable) { + return; + } + + UIResult result; + result.Outcome = UIResult::OUTCOME_ACCEPTED; + + if (intent.Action == UI_MPSELECT_NETWORK) { + Choice = CHOICE_NETWORK; + } else if (intent.Action == UI_MPSELECT_SKIRMISH) { + Choice = CHOICE_SKIRMISH; + } else { + // Modem and serial, and anything else the screen carries, leave with no session + // chosen, which is where the dialog's default arm sent them. + Choice = CHOICE_BACK; + result.Outcome = UIResult::OUTCOME_CANCELLED; + } + + result.Value = Session_Type(); + Result = result; +} + + +//--------------------------------------------------------------------------------------- +// The RmlUi view. One document per template, because the two differ by which buttons exist +// rather than by how one is arranged. +//--------------------------------------------------------------------------------------- + +/// +/// The RmlUi half of the multiplayer game selection screen. +/// +class MPSelectViewClass : public UIRmlViewClass +{ + public: + MPSelectViewClass(UIMPSelectPresenterClass & presenter, char const * document) : + UIRmlViewClass(presenter, document), + Screen(presenter) + { + } + + virtual void Bind(Rml::DataModelConstructor & model) override; + virtual void Sync(void) override {} + + private: + UIMPSelectPresenterClass & Screen; +}; + + +void MPSelectViewClass::Bind(Rml::DataModelConstructor & model) +{ + model.Bind("internet", &Screen.InternetAvailable); + model.Bind("worlddom", &Screen.WorldDominationAvailable); + + model.BindEventCallback("press", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{arguments[0].Get(), "", 0}); + }); + + // The Main Menu button is the template's IDCANCEL, and Enter reaches the same default + // arm the dialog sent every unhandled identifier to. + model.BindEventCallback("key", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + int const key = event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN); + if (key == Rml::Input::KI_ESCAPE || key == Rml::Input::KI_RETURN + || key == Rml::Input::KI_NUMPADENTER) { + Screen.Queue(UIIntent{UI_MPSELECT_BACK, "", 0}); + } + }); +} + + +/// +/// Shows the multiplayer game choices and waits for the player to make one. +/// +UIResult UI_MPlayer_Select_Screen(UIMPSelectPresenterClass & presenter) +{ + char const * const document = + (presenter.Variant == UIMPSelectPresenterClass::VARIANT_FIRESTORM) ? "mpselectfs.rml" : "mpselect.rml"; + + MPSelectViewClass view(presenter, document); + + if (!view.Prepare(true)) { + UIResult result; + result.Outcome = UIResult::OUTCOME_FAILED_TO_OPEN; + return(result); + } + + UIResult const result = UI_Run_Modal(presenter, view); + view.Close(); + return(result); +} diff --git a/code/ui/uimpselect.h b/code/ui/uimpselect.h new file mode 100644 index 000000000..6f490dd0f --- /dev/null +++ b/code/ui/uimpselect.h @@ -0,0 +1,68 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The multiplayer game selection screen's behavior, with no toolkit in it. Two templates +// share it and they differ by which buttons exist, so the variant is part of the view-model +// rather than something a view works out for itself. +// +// docs/UI_DESIGN.md, "Screens", owns the contract. + +#pragma once + +#include "uiscreen.h" + + +inline constexpr char const * UI_MPSELECT_INTERNET = "internet"; +inline constexpr char const * UI_MPSELECT_WORLDDOM = "worlddom"; +inline constexpr char const * UI_MPSELECT_MODEM = "modem"; +inline constexpr char const * UI_MPSELECT_NETWORK = "network"; +inline constexpr char const * UI_MPSELECT_SKIRMISH = "skirmish"; +inline constexpr char const * UI_MPSELECT_BACK = "back"; + + +class UIMPSelectPresenterClass : public UIPresenterClass +{ + public: + enum VariantType { + VARIANT_BASE, + VARIANT_FIRESTORM, + }; + + enum ChoiceType { + CHOICE_NONE, + CHOICE_NETWORK, + CHOICE_SKIRMISH, + CHOICE_BACK, + }; + + virtual void Execute(UIIntent const & intent) override; + virtual void Refresh(void) override; + virtual void Service(void) override; + + // The session type the caller's own switch is written against. + int Session_Type(void) const; + + /* + ** The view-model. Plain values, and the only thing a view reads. + */ + VariantType Variant = VARIANT_BASE; + + // Neither the online service these led to nor the tour it hosted can be reached, so + // the buttons stay on the screen and never answer, which is what the dialog did by + // disabling them. + bool InternetAvailable = false; + bool WorldDominationAvailable = false; + + ChoiceType Choice = CHOICE_NONE; +}; + + +// Shows the screen through its RmlUi view. FAILED_TO_OPEN leaves nothing shown and the +// caller falls through to the legacy dialog. +UIResult UI_MPlayer_Select_Screen(UIMPSelectPresenterClass & presenter); diff --git a/code/ui/uiprogress.cpp b/code/ui/uiprogress.cpp new file mode 100644 index 000000000..5a68fffce --- /dev/null +++ b/code/ui/uiprogress.cpp @@ -0,0 +1,265 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The progress and wait box. It is the first screen whose picture the engine draws: the bar +// is the game's own artwork clipped to the part of the job that is finished, so it reaches +// the document through the element rather than as a file the document names. +// +// What is preserved from IDD_PROGRESS_WAIT, and where each came from: the bar is the first +// frame of the shape the caller named, drawn from its left edge and cut off at the fraction +// finished, which is what ProgressScreenClass::Display_Progress does with +// Shape->Get_Rect(0); the bar is centered on the frame the template calls +// IDC_PROGRESS_BAR_FRAME; and the caption is the template's own literal, because that +// template holds the text rather than a string identifier. +// +// The box is not modal. Its callers -- the map generator and the file transfer -- keep +// running their own loops underneath it and close it when the job ends. +// +// docs/UI_DESIGN.md, "Screens" and "Assets and strings", own the contracts this keeps to. + +#include "always.h" + +#include "uiprogress.h" + +#include "uiinternal.h" +#include "uirmlview.h" +#include "uisurface.h" + +#include "_convert.h" +#include "dbgprint.h" +#include "_mixfile.h" +#include "convert.h" +#include "draw.h" +#include "mixfile.h" +#include "shapeset.h" + +#include + +#include +#include +#include + + +// The caption IDD_PROGRESS_WAIT names. The template carries the text itself rather than a +// string identifier, so there is nothing to look up. +static char const * const DEFAULT_CAPTION = "Working - Please Wait"; + +// The name the document gives the bar's pixels. +static char const * const BAR_SURFACE = "progressbar"; + + +/// +/// The toolkit-free half of the box. It has no actions: the job underneath moves the bar +/// and nothing the player does reaches the screen. +/// +class ProgressWaitPresenterClass : public UIPresenterClass +{ + public: + std::string Caption = DEFAULT_CAPTION; + + // The part of the job that is finished, 0 to 1. + double Fraction = 0.0; + + // The name of the bar artwork, as Set_Graphic_Data names it. + std::string BarShape; + + virtual void Execute(UIIntent const &) override {} + virtual void Refresh(void) override {} +}; + + +/// +/// The bar's pixels. The shape is drawn into an engine surface exactly as the dialog drew +/// it, and the element converts and uploads that when it changes. +/// +class ProgressBarSurfaceClass : public UISurfaceBufferClass +{ + public: + ProgressBarSurfaceClass(ShapeSet * shape, int width, int height) : + UISurfaceBufferClass(width, height), + Shape(shape) + { + } + + void Draw(double fraction); + + private: + ShapeSet * Shape = nullptr; +}; + + +void ProgressBarSurfaceClass::Draw(double fraction) +{ + Clear(); + + if (Shape == nullptr) { + return; + } + + // The dialog cut the bar off at the fraction finished by shrinking the shape's own + // rectangle, which is what keeps a part-drawn bar the same pixels as a full one. + Rect rect = Shape->Get_Rect(0); + rect.Width = (int)(rect.Width * std::clamp(fraction, 0.0, 1.0)); + rect.X = 0; + rect.Y = 0; + + Draw_Shape(Get_Surface(), *NormalDrawer, Shape, 0, Point2D(0, 0), rect, SHAPE_WIN_REL); + Mark_Dirty(); +} + + +class ProgressWaitViewClass : public UIRmlViewClass +{ + public: + ProgressWaitViewClass(ProgressWaitPresenterClass & presenter); + virtual ~ProgressWaitViewClass(void) override; + + virtual void Bind(Rml::DataModelConstructor & model) override; + virtual void Sync(void) override; + + // Attaches the artwork the presenter named and draws the bar at its current value. + void Attach_Bar(void); + + private: + ProgressWaitPresenterClass & Screen; + std::unique_ptr Bar; +}; + + +ProgressWaitViewClass::ProgressWaitViewClass(ProgressWaitPresenterClass & presenter) : + UIRmlViewClass(presenter, "progresswait.rml"), + Screen(presenter) +{ +} + + +ProgressWaitViewClass::~ProgressWaitViewClass(void) +{ + // The registration goes before the provider does, so nothing can be asked for pixels + // that have been freed. + UI_Unregister_Surface(BAR_SURFACE); +} + + +void ProgressWaitViewClass::Bind(Rml::DataModelConstructor & model) +{ + model.Bind("caption", &Screen.Caption); +} + + +void ProgressWaitViewClass::Sync(void) +{ + if (Bar != nullptr) { + Bar->Draw(Screen.Fraction); + } +} + + +void ProgressWaitViewClass::Attach_Bar(void) +{ + UI_Unregister_Surface(BAR_SURFACE); + Bar.reset(); + + if (Screen.BarShape.empty()) { + return; + } + + ShapeSet * const shape = (ShapeSet *)MFCD::Retrieve(Screen.BarShape.c_str()); + if (shape == nullptr) { + DebugString("[UI] The progress bar artwork %s is not in the mix files.\n", Screen.BarShape.c_str()); + return; + } + + Rect const rect = shape->Get_Rect(0); + if (rect.Width <= 0 || rect.Height <= 0) { + return; + } + + Bar = std::make_unique(shape, rect.Width, rect.Height); + Bar->Draw(Screen.Fraction); + + // The element takes its size from the provider, and notices the registration on the next + // context update. + UI_Register_Surface(BAR_SURFACE, Bar.get()); +} + + +// The one progress box. Every caller opens one, holds it for the length of a job and closes +// it, and no caller opens a second while one is up. +static std::unique_ptr _Presenter; +static std::unique_ptr _View; + + +bool UI_Progress_Wait_Open(void) +{ + if (_View != nullptr) { + return(false); + } + + // The presenter is created first so that it is destroyed last, because the data model + // the view binds reads the presenter's view-model. + _Presenter = std::make_unique(); + _View = std::make_unique(*_Presenter); + + if (!_View->Prepare(false)) { + _View.reset(); + _Presenter.reset(); + return(false); + } + + // The box must be on screen before the job underneath begins, or a caller that never + // pumps again shows nothing at all. + UI_Paint_Now(true); + return(true); +} + + +void UI_Progress_Wait_Set_Bar(char const * shape) +{ + if (_View == nullptr) { + return; + } + + _Presenter->BarShape = (shape != nullptr) ? shape : ""; + _View->Attach_Bar(); + UI_Paint_Now(true); +} + + +void UI_Progress_Wait_Set_Progress(double fraction) +{ + if (_View == nullptr) { + return; + } + + _Presenter->Fraction = fraction; + _View->Sync(); + + // The dialog repainted itself where the gauge moved, through a synchronous WM_PAINT. + UI_Paint_Now(false); +} + + +void UI_Progress_Wait_Close(void) +{ + if (_View == nullptr) { + return; + } + + _View->Close(); + _View.reset(); + _Presenter.reset(); +} + + +bool UI_Progress_Wait_Is_Open(void) +{ + return(_View != nullptr); +} + diff --git a/code/ui/uiprogress.h b/code/ui/uiprogress.h new file mode 100644 index 000000000..96443b20d --- /dev/null +++ b/code/ui/uiprogress.h @@ -0,0 +1,31 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The progress and wait box, IDD_PROGRESS_WAIT. It stands over a job the caller drives, so +// it is not modal and the caller's own loop keeps running underneath it, the way the wait +// box does. Only plain values cross this header. +// +// docs/UI_DESIGN.md, "Screens", owns the contract. + +#pragma once + + +// Opens the box. A false return means nothing was shown, which is the caller's cue to open +// the legacy dialog instead. The box carries no bar until one is named. +bool UI_Progress_Wait_Open(void); + +// Names the artwork the bar is drawn from, as ProgressScreenClass::Set_Graphic_Data names +// it. An unknown name leaves the box with no bar rather than failing. +void UI_Progress_Wait_Set_Bar(char const * shape); + +// Moves the bar. The fraction is the part of the job that is finished, 0 to 1. +void UI_Progress_Wait_Set_Progress(double fraction); + +void UI_Progress_Wait_Close(void); +bool UI_Progress_Wait_Is_Open(void); diff --git a/code/ui/uireconnect.cpp b/code/ui/uireconnect.cpp new file mode 100644 index 000000000..dc5230095 --- /dev/null +++ b/code/ui/uireconnect.cpp @@ -0,0 +1,446 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The reconnect and kick-vote screen. What is preserved from IDD_MPLAYER_DISCONNECT, and +// where each came from: a seat gets a button carrying its name and a bar beside it that +// shrinks and turns yellow and then red as the wait on that seat drags on, which is what +// Draw_Sync_Bars painted straight into the surface; pressing a seat's button proposes that +// the seat be kicked, and proposing to kick yourself, or anybody at all in a tournament +// game, earns a line in the message list and nothing else; the message list carries the +// stall's own explanation, which differs between a reconnect and a first-time wait and +// between a LAN game and an internet one; and Cancel gives up on the game. +// +// The screen has no loop of its own. Wait_For_Players keeps servicing the network while the +// game is stalled, and it opens the screen, services it once a pass and closes it, the way +// it created and destroyed a modeless dialog. +// +// docs/UI_DESIGN.md, "Screens", owns the contracts this keeps to. + +#include "always.h" + +#include "uireconnect.h" + +#include "uiinternal.h" +#include "uirmlview.h" +#include "uishell.h" + +#include "data.h" +#include "dbgprint.h" +#include "ipxmgr.h" +#include "language/language.h" +#include "netglobal.h" +#include "queue.h" +#include "session.h" +#include "stats.h" + +#include +#include + +#include +#include + + +bool Cast_Kick_Vote(int kicker, int kickee); + + +/// +/// Records the seats, the stall's explanation and the cleared vote tallies the screen opens +/// with. +/// +/// True when the game is trying to reconnect to somebody, false +/// when it is still waiting for a connection that has never been made. +/// Each connection's reported frame number, used to name the seat the +/// game is furthest behind. +/// How many entries frames carries. +void UIReconnectPresenterClass::Open(bool reconnect, int const * frames, int connections) +{ + Cancelled = false; + Messages.clear(); + TimeText.clear(); + Result.reset(); + IsClosing = false; + + // A stale proposal from an earlier stall is not a vote in this one. + while (Session.KickProposals.Count()) { + delete Session.KickProposals[0]; + Session.KickProposals.Delete_Index(0); + } + memset(Session.KickVoteCount, 0, sizeof(Session.KickVoteCount)); + memset(Session.KickVoteWho, 0xFF, sizeof(Session.KickVoteWho)); + + Refresh(); + + char buffer[256]; + + if (!reconnect) { + Record_Message(Fetch_String(TXT_WAITING_FOR_CONNECTIONS)); + return; + } + + // The seat the game is furthest behind is the one it is trying to reconnect to. + int oldest = 0; + int lowest = 0x7fffffff; + for (int index = 0; index < connections && frames != NULL; index++) { + if (frames[index] < lowest) { + lowest = frames[index]; + oldest = index; + } + } + + char const * name = ""; + if (Session.Type == GAME_IPX || Session.Type == GAME_INTERNET) { + name = Ipx.Connection_Name(Ipx.Connection_ID(oldest)); + } else if (Session.Players.Count() > 1) { + name = Session.Players[1]->Name; + } + + std::snprintf(buffer, sizeof(buffer), Fetch_String(TXT_RECONNECTING_TO), name); + Record_Message(buffer); + Record_Message(""); + + if (Session.Type == GAME_INTERNET) { + Record_Message(Fetch_String(TXT_RECONNECT_HELP3)); + Record_Message(Fetch_String(TXT_RECONNECT_HELP3B)); + Record_Message(Fetch_String(TXT_RECONNECT_HELP3C)); + } + + Record_Message(Fetch_String(TXT_RECONNECT_HELP2)); + if (Session.Type == GAME_INTERNET) { + Record_Message(Fetch_String(TXT_RECONNECT_HELP2B)); + } else if (Session.Type == GAME_IPX) { + Record_Message(Fetch_String(TXT_RECONNECT_HELP4)); + } + + Record_Message(""); + Record_Message(Fetch_String(TXT_RECONNECT_HELP5)); + Record_Message(Fetch_String(TXT_RECONNECT_HELP1)); + Record_Message(""); +} + + +void UIReconnectPresenterClass::Execute(UIIntent const & intent) +{ + if (intent.Action == UI_RECONNECT_KICK) { + Propose_Kick(intent.Value); + return; + } + + if (intent.Action == UI_RECONNECT_CANCEL) { + Cancelled = true; + + UIResult result; + result.Outcome = UIResult::OUTCOME_CANCELLED; + Result = result; + return; + } +} + + +/// +/// Reads the session's seats into the view-model. Only the seats the game holds get a row, +/// which is what destroying the spare buttons stood for. +/// +void UIReconnectPresenterClass::Refresh(void) +{ + Players.clear(); + + for (int index = 0; index < Session.Players.Count() && index < MAX_PLAYERS; index++) { + PlayerRowType row; + row.Name = Session.Players[index]->Name; + Players.push_back(row); + } + + PlayersChanged = true; +} + + +/// +/// Moves every seat's bar. The local seat is never behind, so its bar stays full; every +/// other seat's is the wait since that seat last reported in. +/// +void UIReconnectPresenterClass::Update_Bars(unsigned elapsed, unsigned const * timings, int count) +{ + for (int index = 0; index < (int)Players.size(); index++) { + unsigned progress = 0; + + if (index != 0 && index < Session.Players.Count()) { + // A seat whose connection has already gone away has no timing to read. The + // dialog indexed the array with the -1 it got back for one. + int const connection = Ipx.Connection_Index(Session.Players[index]->Player.ID); + if (timings != NULL && connection >= 0 && connection < count) { + progress = elapsed - timings[connection]; + } + } + + Players[index].Lateness = progress > 480 ? 2 : (progress > 240 ? 1 : 0); + Players[index].Remaining = std::max(100 - (int)(100 * progress / 1200), 0); + } + + PlayersChanged = true; +} + + +void UIReconnectPresenterClass::Set_Time_Remaining(int seconds) +{ + char buffer[256]; + std::snprintf(buffer, sizeof(buffer), Fetch_String(TXT_TIME_ALLOWED), seconds); + + TimeText = buffer; + TimeChanged = true; +} + + +void UIReconnectPresenterClass::Record_Message(char const * line) +{ + Messages.emplace_back(line != NULL ? line : ""); + if ((int)Messages.size() > MESSAGE_LIMIT) { + Messages.erase(Messages.begin()); + } + + MessagesChanged = true; +} + + +/// +/// Proposes that a seat be kicked out of the game, telling every other player and casting +/// this machine's own vote. +/// +/// Index into the session's player list of the seat to be kicked. +void UIReconnectPresenterClass::Propose_Kick(int index) +{ + if (index < 0 || index >= Session.Players.Count()) { + return; + } + + DebugString("Propose_Kick_Player %d - %s. Local id is %d\n", index, Session.Players[index]->Name, + Session.Players[0]->Player.ID); + + if (index == 0) { + Record_Message(Fetch_String(TXT_RECONNECT_KICK_SELF)); + return; + } + + if (Session.Type == GAME_INTERNET && WestwoodOnline_Tournament) { + Record_Message(Fetch_String(TXT_CANT_KICK)); + return; + } + + int const kicker = Session.Players[0]->Player.ID; + int const kickee = Session.Players[index]->Player.ID; + if (!Kick_Vote_Is_Possible(kicker, kickee)) { + return; + } + + GlobalPacketType gpacket; + NetGlobal::Initialize_Packet(gpacket, NET_PROPOSE_KICK); + std::snprintf(gpacket.Name, sizeof(gpacket.Name), "%s", Session.Players[0]->Name); + gpacket.Kick.KickerID = static_cast(kicker); + gpacket.Kick.KickeeID = static_cast(kickee); + + for (int other = 1; other < Session.Players.Count(); other++) { + DebugString("Sending kick proposal to %s\n", Session.Players[other]->Name); + Ipx.Send_Global_Message(&gpacket, sizeof(gpacket), 1, &Session.Players[other]->Address); + } + + Cast_Kick_Vote(kicker, kickee); +} + + +/* +** The RmlUi view. One document: the template has no variants, and the seats it shows are +** the ones the game holds rather than a fixed eight. +*/ +namespace { + + // A seat as the document lays it out. The row's own position is carried here because the + // template puts the eight seats in two columns of four rather than in a list. + struct SeatViewType + { + std::string Name; + std::string Left; + std::string Top; + std::string Width; + std::string Hex; + }; + + + class ReconnectViewClass : public UIRmlViewClass + { + public: + ReconnectViewClass(UIReconnectPresenterClass & presenter) + : UIRmlViewClass(presenter, "reconnect.rml"), Screen(presenter) {} + + virtual void Bind(Rml::DataModelConstructor & model) override; + virtual void Sync(void) override; + + private: + void Rebuild_Rows(void); + + UIReconnectPresenterClass & Screen; + + std::vector Seats; + }; + + + void ReconnectViewClass::Rebuild_Rows(void) + { + Seats.clear(); + + for (int index = 0; index < (int)Screen.Players.size(); index++) { + UIReconnectPresenterClass::PlayerRowType const & row = Screen.Players[index]; + + SeatViewType seat; + seat.Name = row.Name; + + // The template's two columns of four, at 22 and 175 dialog units across and + // every 18 units down from 12. + char position[16]; + std::snprintf(position, sizeof(position), "%gdp", index < 4 ? 33.0 : 262.5); + seat.Left = position; + std::snprintf(position, sizeof(position), "%gdp", 19.5 + (index % 4) * 29.25); + seat.Top = position; + + // The bar keeps a floor of six pixels of the group box it is drawn in, which is + // ten percent of the box's sixty. + std::snprintf(position, sizeof(position), "%d%%", std::max(row.Remaining, 10)); + seat.Width = position; + + seat.Hex = row.Lateness >= 2 ? "#c80000" : (row.Lateness == 1 ? "#c8c800" : "#00c800"); + + Seats.push_back(seat); + } + } + + + void ReconnectViewClass::Bind(Rml::DataModelConstructor & model) + { + Rebuild_Rows(); + + if (auto seat = model.RegisterStruct()) { + seat.RegisterMember("name", &SeatViewType::Name); + seat.RegisterMember("left", &SeatViewType::Left); + seat.RegisterMember("top", &SeatViewType::Top); + seat.RegisterMember("width", &SeatViewType::Width); + seat.RegisterMember("hex", &SeatViewType::Hex); + } + model.RegisterArray>(); + model.RegisterArray>(); + + model.Bind("seats", &Seats); + model.Bind("messages", &Screen.Messages); + model.Bind("timetext", &Screen.TimeText); + + model.BindEventCallback("kick", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{UI_RECONNECT_KICK, "", arguments[0].Get()}); + }); + + model.BindEventCallback("press", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const &) { + Screen.Queue(UIIntent{UI_RECONNECT_CANCEL, "", 0}); + }); + + // Escape gives up on the stalled game, which is the IDCANCEL IsDialogMessage sent + // the dialog whether or not the key reached its cancel button. + model.BindEventCallback("key", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + int const key = event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN); + if (key == Rml::Input::KI_ESCAPE) { + Screen.Queue(UIIntent{UI_RECONNECT_CANCEL, "", 0}); + } + }); + } + + + void ReconnectViewClass::Sync(void) + { + if (!Model) return; + + Rebuild_Rows(); + + Model.DirtyVariable("seats"); + Model.DirtyVariable("messages"); + Model.DirtyVariable("timetext"); + + Screen.PlayersChanged = false; + Screen.MessagesChanged = false; + Screen.TimeChanged = false; + } + + + // The one reconnect screen. Wait_For_Players opens one when the game stalls and closes + // it when the stall ends, and no caller opens a second while one is up. + UIReconnectPresenterClass * _Presenter = NULL; + ReconnectViewClass * _View = NULL; + +} // namespace + + +UIReconnectPresenterClass * UI_Reconnect_Screen(void) +{ + return(_Presenter); +} + + +bool UI_Reconnect_Open(bool reconnect, int const * frames, int connections) +{ + UI_Reconnect_Close(); + + _Presenter = new UIReconnectPresenterClass; + _Presenter->Open(reconnect, frames, connections); + + ReconnectViewClass * const view = new ReconnectViewClass(*_Presenter); + + // The wait loop stops servicing the map's input while this screen is up, so the screen + // owns the input scope the way the dialog did. + if (!view->Prepare(true)) { + delete view; + return(false); + } + + _View = view; + UI_Paint_Now(true); + return(true); +} + + +/// +/// The pass the wait loop gives the screen: what its events queued is executed, the document +/// is brought up to the model, and the result is put on screen. +/// +void UI_Reconnect_Service(void) +{ + if (_View == NULL || _Presenter == NULL) { + return; + } + + _Presenter->Drain(); + _View->Sync(); + UI_Paint_Now(false); +} + + +void UI_Reconnect_Close(void) +{ + if (_View != NULL) { + _View->Close(); + delete _View; + _View = NULL; + } + + delete _Presenter; + _Presenter = NULL; +} + + +bool UI_Reconnect_Has_View(void) +{ + return(_View != NULL); +} diff --git a/code/ui/uireconnect.h b/code/ui/uireconnect.h new file mode 100644 index 000000000..64ec485c3 --- /dev/null +++ b/code/ui/uireconnect.h @@ -0,0 +1,103 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The reconnect and kick-vote screen, IDD_MPLAYER_DISCONNECT. It stands over a stalled +// multiplayer game while Wait_For_Players keeps servicing the network, so it has no loop of +// its own: the wait loop opens it, services it once a pass and closes it. +// +// docs/UI_DESIGN.md, "Screens", owns the contract. + +#pragma once + +#include "uiscreen.h" + +#include +#include + + +inline constexpr char const * UI_RECONNECT_KICK = "kick"; +inline constexpr char const * UI_RECONNECT_CANCEL = "cancel"; + + +class UIReconnectPresenterClass : public UIPresenterClass +{ + public: + // A seat and how far behind it is. The bar's remaining part and its color are + // figures rather than pixels, because a presenter draws nothing. + struct PlayerRowType + { + std::string Name; + + // How much of the seat's bar is left, 0 to 100, which is what Draw_Sync_Bars + // scaled the group box's width by. + int Remaining = 100; + + // 0 while the seat is keeping up, 1 once it is late, 2 once it is very late. + int Lateness = 0; + }; + + virtual void Execute(UIIntent const & intent) override; + virtual void Refresh(void) override; + + // The state the screen opens with: the seats, the prose for the stall it stands + // over, and the discarded kick proposals and vote tallies the dialog cleared on + // creation. + void Open(bool reconnect, int const * frames, int connections); + + // Moves every seat's bar. The timings are indexed by connection, as the wait loop + // holds them, because that is how the dialog read them. + void Update_Bars(unsigned elapsed, unsigned const * timings, int count); + + void Set_Time_Remaining(int seconds); + + // Records a line for whatever is showing the screen. The vote announcements arrive + // here from the wait loop as well as from a button. + void Record_Message(char const * line); + + // Puts a kick to the other players and casts this machine's own vote. The index is + // into the session's player list, as the button that raised it was. + void Propose_Kick(int index); + + /* + ** The view-model. + */ + + std::vector Players; + std::vector Messages; + + // The most lines the model keeps, which is what ListBox_Trim capped the message + // list box at. + enum { MESSAGE_LIMIT = 50 }; + + std::string TimeText; + + // Has the player given up on the stalled game? The wait loop reads this where it + // read IDCANCEL out of the dialog's own result. + bool Cancelled = false; + + bool PlayersChanged = false; + bool MessagesChanged = false; + bool TimeChanged = false; +}; + + +// The screen the wait loop is running, or NULL when none is up. The vote tally reaches the +// model through this, because a vote is counted where no screen is in hand. +UIReconnectPresenterClass * UI_Reconnect_Screen(void); + +// Opens the screen and shows its document. A false return means no document was shown and +// the caller opens the legacy dialog against the same presenter. +bool UI_Reconnect_Open(bool reconnect, int const * frames, int connections); + +// The pass the wait loop gives the screen: the queued intents are executed and the document +// is brought up to the model and put on screen. Does nothing without a document. +void UI_Reconnect_Service(void); + +void UI_Reconnect_Close(void); +bool UI_Reconnect_Has_View(void); diff --git a/code/ui/uirender.cpp b/code/ui/uirender.cpp new file mode 100644 index 000000000..06794781d --- /dev/null +++ b/code/ui/uirender.cpp @@ -0,0 +1,483 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The overlays' side of the renderer. This is the only file in the shell that includes +// bgfx, and every renderer handle the shell owns lives here, the way bgfxbackend.cpp holds +// the frame's handles. It carries RmlUi's render interface and the ImGui renderer, which +// share the program, the views and the blend state. +// +// docs/UI_DESIGN.md, "Rendering", owns the contract. + +#include "uiinternal.h" + +#include "backendviews.hh" +#include "dbgprint.h" + +#include + +#include + +#include +#include + +#include +#include + +#include +#include +#include + + +static const bgfx::EmbeddedShader _EmbeddedShaders[] = { + BGFX_EMBEDDED_SHADER(vs_ocornut_imgui), + BGFX_EMBEDDED_SHADER(fs_ocornut_imgui), + BGFX_EMBEDDED_SHADER_END() +}; + + +static bool _Initialized = false; + +static bgfx::ProgramHandle _Program = BGFX_INVALID_HANDLE; +static bgfx::UniformHandle _TextureSampler = BGFX_INVALID_HANDLE; +static bgfx::TextureHandle _WhiteTexture = BGFX_INVALID_HANDLE; +static bgfx::VertexLayout _RmlLayout; +static bgfx::VertexLayout _ImGuiLayout; + +// Where the frame landed in the window, which is also the overlays' viewport. Scissor +// rectangles arrive relative to this origin and are made absolute before they are set. +static int _OriginX = 0; +static int _OriginY = 0; +static int _Width = 0; +static int _Height = 0; + +// The documents draw the game's own 640x400-era artwork and its bitmap font, magnified by +// whatever the frame scale is. Linear filtering softens both; point sampling keeps the +// pixels the artists drew. Art and text share this so they never disagree. +static const uint64_t _SamplerFlags = + BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP + | BGFX_SAMPLER_MIN_POINT | BGFX_SAMPLER_MAG_POINT | BGFX_SAMPLER_MIP_POINT; + +static const uint64_t _BlendState = + BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A | BGFX_STATE_MSAA + | BGFX_STATE_BLEND_FUNC(BGFX_STATE_BLEND_ONE, BGFX_STATE_BLEND_INV_SRC_ALPHA); + + +// One compiled geometry. RmlUi 6 compiles geometry once and re-submits it, so the indices +// are a static buffer that outlives the frame. +// +// The vertices cannot be, yet. RmlUi re-submits the same geometry at a different +// translation, and the program the overlays share is bgfx's embedded imgui shader, whose +// vertex stage multiplies by u_viewProj alone and so ignores the per-draw model transform +// that bgfx::setTransform sets. Until the shell carries a program with a model transform, +// the translation is applied to a copy of the vertices on the way to a transient buffer. +struct UIGeometry +{ + std::vector Vertices; + bgfx::IndexBufferHandle Indices = BGFX_INVALID_HANDLE; + uint32_t IndexCount = 0; +}; + + +/// +/// Builds an orthographic projection over a target measured in pixels, origin top left. +/// +static void Build_Ortho_Projection(float * result, int width, int height) +{ + const float depthnear = 0.0f; + const float depthfar = 1000.0f; + const bool homogeneous = bgfx::getCaps()->homogeneousDepth; + + std::memset(result, 0, sizeof(float) * 16); + + result[0] = 2.0f / (float)width; + result[5] = -2.0f / (float)height; + result[10] = homogeneous ? 2.0f / (depthfar - depthnear) : 1.0f / (depthfar - depthnear); + result[12] = -1.0f; + result[13] = 1.0f; + result[14] = homogeneous ? -(depthfar + depthnear) / (depthfar - depthnear) : -depthnear / (depthfar - depthnear); + result[15] = 1.0f; +} + + +/// +/// Turns a texture handle the toolkits carry back into the renderer's own. +/// Zero is reserved for "no texture", so the index is stored one higher than it is. +/// +static bgfx::TextureHandle Texture_From_Handle(uintptr_t handle) +{ + bgfx::TextureHandle texture = BGFX_INVALID_HANDLE; + if (handle != 0) { + texture.idx = (uint16_t)(handle - 1); + } + return(texture); +} + + +static uintptr_t Handle_From_Texture(bgfx::TextureHandle texture) +{ + return(bgfx::isValid(texture) ? (uintptr_t)texture.idx + 1 : 0); +} + + +class UIRenderInterface : public Rml::RenderInterface +{ + public: + virtual Rml::CompiledGeometryHandle CompileGeometry(Rml::Span vertices, Rml::Span indices) override; + virtual void RenderGeometry(Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation, Rml::TextureHandle texture) override; + virtual void ReleaseGeometry(Rml::CompiledGeometryHandle geometry) override; + + virtual Rml::TextureHandle LoadTexture(Rml::Vector2i & dimensions, const Rml::String & source) override; + virtual Rml::TextureHandle GenerateTexture(Rml::Span source, Rml::Vector2i dimensions) override; + virtual void ReleaseTexture(Rml::TextureHandle texture) override; + + virtual void EnableScissorRegion(bool enable) override; + virtual void SetScissorRegion(Rml::Rectanglei region) override; + + private: + bool ScissorEnabled = false; + Rml::Rectanglei Scissor = Rml::Rectanglei::FromPosition({0, 0}); +}; + +static UIRenderInterface _RenderInterface; + + +Rml::CompiledGeometryHandle UIRenderInterface::CompileGeometry(Rml::Span vertices, Rml::Span indices) +{ + if (!_Initialized || vertices.empty() || indices.empty()) { + return(0); + } + + UIGeometry * geometry = new UIGeometry; + + geometry->Vertices.assign(vertices.begin(), vertices.end()); + geometry->Indices = bgfx::createIndexBuffer( + bgfx::copy(indices.data(), (uint32_t)(indices.size() * sizeof(int))), BGFX_BUFFER_INDEX32); + geometry->IndexCount = (uint32_t)indices.size(); + + if (!bgfx::isValid(geometry->Indices)) { + ReleaseGeometry((Rml::CompiledGeometryHandle)geometry); + return(0); + } + + return((Rml::CompiledGeometryHandle)geometry); +} + + +void UIRenderInterface::RenderGeometry(Rml::CompiledGeometryHandle handle, Rml::Vector2f translation, Rml::TextureHandle texture) +{ + UIGeometry const * geometry = (UIGeometry const *)handle; + if (!_Initialized || geometry == nullptr || _Width <= 0 || _Height <= 0) { + return; + } + + uint32_t const count = (uint32_t)geometry->Vertices.size(); + if (bgfx::getAvailTransientVertexBuffer(count, _RmlLayout) < count) { + return; + } + + bgfx::TransientVertexBuffer buffer; + bgfx::allocTransientVertexBuffer(&buffer, count, _RmlLayout); + + Rml::Vertex * target = (Rml::Vertex *)buffer.data; + for (uint32_t index = 0; index < count; index++) { + target[index] = geometry->Vertices[index]; + target[index].position += translation; + } + + bgfx::setVertexBuffer(0, &buffer); + bgfx::setIndexBuffer(geometry->Indices, 0, geometry->IndexCount); + + bgfx::TextureHandle bound = Texture_From_Handle((uintptr_t)texture); + bgfx::setTexture(0, _TextureSampler, bgfx::isValid(bound) ? bound : _WhiteTexture, + _SamplerFlags); + + if (ScissorEnabled) { + // RmlUi reports the region relative to the context, which sits at the frame's top + // left corner. bgfx wants it in the target's own coordinates. + int left = std::max(Scissor.Left() + _OriginX, _OriginX); + int top = std::max(Scissor.Top() + _OriginY, _OriginY); + int right = std::min(Scissor.Right() + _OriginX, _OriginX + _Width); + int bottom = std::min(Scissor.Bottom() + _OriginY, _OriginY + _Height); + + if (right <= left || bottom <= top) { + return; + } + + bgfx::setScissor((uint16_t)left, (uint16_t)top, (uint16_t)(right - left), (uint16_t)(bottom - top)); + } + + bgfx::setState(_BlendState); + bgfx::submit(BACKEND_VIEW_UI, _Program); +} + + +void UIRenderInterface::ReleaseGeometry(Rml::CompiledGeometryHandle handle) +{ + UIGeometry * geometry = (UIGeometry *)handle; + if (geometry == nullptr) { + return; + } + + if (bgfx::isValid(geometry->Indices)) { + bgfx::destroy(geometry->Indices); + } + + delete geometry; +} + + +Rml::TextureHandle UIRenderInterface::LoadTexture(Rml::Vector2i & dimensions, const Rml::String & source) +{ + UIImageData image; + if (!UI_Decode_Image(source.c_str(), image)) { + DebugString("[UI] Image %s could not be read.\n", source.c_str()); + return(0); + } + + dimensions.x = image.Width; + dimensions.y = image.Height; + + return(GenerateTexture(Rml::Span(image.Pixels.data(), image.Pixels.size()), + Rml::Vector2i(image.Width, image.Height))); +} + + +Rml::TextureHandle UIRenderInterface::GenerateTexture(Rml::Span source, Rml::Vector2i dimensions) +{ + if (!_Initialized || dimensions.x <= 0 || dimensions.y <= 0) { + return(0); + } + + bgfx::TextureHandle texture = bgfx::createTexture2D( + (uint16_t)dimensions.x, (uint16_t)dimensions.y, false, 1, bgfx::TextureFormat::RGBA8, + _SamplerFlags, + bgfx::copy(source.data(), (uint32_t)source.size())); + + return((Rml::TextureHandle)Handle_From_Texture(texture)); +} + + +void UIRenderInterface::ReleaseTexture(Rml::TextureHandle handle) +{ + bgfx::TextureHandle texture = Texture_From_Handle((uintptr_t)handle); + if (bgfx::isValid(texture)) { + bgfx::destroy(texture); + } +} + + +void UIRenderInterface::EnableScissorRegion(bool enable) +{ + ScissorEnabled = enable; +} + + +void UIRenderInterface::SetScissorRegion(Rml::Rectanglei region) +{ + Scissor = region; +} + + +Rml::RenderInterface * UI_Render_Interface(void) +{ + return(&_RenderInterface); +} + + +/// +/// Creates the program, the sampler and the untextured stand-in the overlays draw with. +/// +/// bool; Is the overlay renderer ready to draw? +bool UI_Render_Init(void) +{ + if (_Initialized) { + return(true); + } + + // RmlUi's vertex is position, then a premultiplied RGBA byte colour, then the texture + // coordinate. bgfx builds the layout in the order the attributes are added, so this + // order is what makes the layout match the structure without a copy. + _RmlLayout.begin() + .add(bgfx::Attrib::Position, 2, bgfx::AttribType::Float) + .add(bgfx::Attrib::Color0, 4, bgfx::AttribType::Uint8, true) + .add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Float) + .end(); + + _ImGuiLayout.begin() + .add(bgfx::Attrib::Position, 2, bgfx::AttribType::Float) + .add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Float) + .add(bgfx::Attrib::Color0, 4, bgfx::AttribType::Uint8, true) + .end(); + + bgfx::RendererType::Enum type = bgfx::getRendererType(); + bgfx::ShaderHandle vertexshader = bgfx::createEmbeddedShader(_EmbeddedShaders, type, "vs_ocornut_imgui"); + bgfx::ShaderHandle fragmentshader = bgfx::createEmbeddedShader(_EmbeddedShaders, type, "fs_ocornut_imgui"); + + if (!bgfx::isValid(vertexshader) || !bgfx::isValid(fragmentshader)) { + return(false); + } + + _Program = bgfx::createProgram(vertexshader, fragmentshader, true); + _TextureSampler = bgfx::createUniform("s_uitex", bgfx::UniformType::Sampler); + + // The program always samples, so untextured geometry is drawn against an opaque white + // pixel and takes its colour from the vertices alone. + const uint32_t white = 0xFFFFFFFF; + _WhiteTexture = bgfx::createTexture2D(1, 1, false, 1, bgfx::TextureFormat::RGBA8, + _SamplerFlags, bgfx::copy(&white, sizeof(white))); + + if (!bgfx::isValid(_Program) || !bgfx::isValid(_TextureSampler) || !bgfx::isValid(_WhiteTexture)) { + UI_Render_Shutdown(); + return(false); + } + + _Initialized = true; + return(true); +} + + +void UI_Render_Shutdown(void) +{ + if (bgfx::isValid(_WhiteTexture)) { + bgfx::destroy(_WhiteTexture); + _WhiteTexture = BGFX_INVALID_HANDLE; + } + if (bgfx::isValid(_TextureSampler)) { + bgfx::destroy(_TextureSampler); + _TextureSampler = BGFX_INVALID_HANDLE; + } + if (bgfx::isValid(_Program)) { + bgfx::destroy(_Program); + _Program = BGFX_INVALID_HANDLE; + } + + _Initialized = false; +} + + +/// +/// Points both overlay views at the rectangle the frame was drawn into. +/// +void UI_Render_Begin(int destx, int desty, int width, int height) +{ + _OriginX = destx; + _OriginY = desty; + _Width = width; + _Height = height; + + if (!_Initialized || width <= 0 || height <= 0) { + return; + } + + float projection[16]; + Build_Ortho_Projection(projection, width, height); + + for (bgfx::ViewId view : {(bgfx::ViewId)BACKEND_VIEW_UI, (bgfx::ViewId)BACKEND_VIEW_DEV}) { + bgfx::setViewFrameBuffer(view, BGFX_INVALID_HANDLE); + bgfx::setViewClear(view, BGFX_CLEAR_NONE); + + // Both toolkits submit back to front and expect that order kept, which bgfx's + // default sorting does not promise. + bgfx::setViewMode(view, bgfx::ViewMode::Sequential); + bgfx::setViewRect(view, (uint16_t)destx, (uint16_t)desty, (uint16_t)width, (uint16_t)height); + bgfx::setViewTransform(view, nullptr, projection); + } +} + + +void UI_Render_End(void) +{ +} + + +/// +/// Draws one ImGui frame on the developer view. +/// The pinned ImGui asks the renderer to create, update and destroy its textures through +/// the draw data rather than owning a font atlas of its own. +/// +void UI_Render_ImGui(ImDrawData * data) +{ + if (!_Initialized || data == nullptr || data->CmdListsCount <= 0) { + return; + } + + for (ImTextureData * texture : *data->Textures) { + if (texture->Status == ImTextureStatus_WantCreate) { + bgfx::TextureHandle created = bgfx::createTexture2D( + (uint16_t)texture->Width, (uint16_t)texture->Height, false, 1, bgfx::TextureFormat::RGBA8, + _SamplerFlags, + bgfx::copy(texture->GetPixels(), (uint32_t)(texture->Width * texture->Height * 4))); + + texture->SetTexID((ImTextureID)Handle_From_Texture(created)); + texture->SetStatus(ImTextureStatus_OK); + } else if (texture->Status == ImTextureStatus_WantUpdates) { + bgfx::TextureHandle existing = Texture_From_Handle((uintptr_t)texture->TexID); + if (bgfx::isValid(existing)) { + bgfx::updateTexture2D(existing, 0, 0, 0, 0, + (uint16_t)texture->Width, (uint16_t)texture->Height, + bgfx::copy(texture->GetPixels(), (uint32_t)(texture->Width * texture->Height * 4)), + (uint16_t)(texture->Width * 4)); + } + texture->SetStatus(ImTextureStatus_OK); + } else if (texture->Status == ImTextureStatus_WantDestroy) { + bgfx::TextureHandle existing = Texture_From_Handle((uintptr_t)texture->TexID); + if (bgfx::isValid(existing)) { + bgfx::destroy(existing); + } + texture->SetTexID(ImTextureID_Invalid); + texture->SetStatus(ImTextureStatus_Destroyed); + } + } + + for (int list = 0; list < data->CmdListsCount; list++) { + ImDrawList const * commands = data->CmdLists[list]; + + const uint32_t vertexcount = (uint32_t)commands->VtxBuffer.size(); + const uint32_t indexcount = (uint32_t)commands->IdxBuffer.size(); + + if (bgfx::getAvailTransientVertexBuffer(vertexcount, _ImGuiLayout) < vertexcount + || bgfx::getAvailTransientIndexBuffer(indexcount) < indexcount) { + break; + } + + bgfx::TransientVertexBuffer vertices; + bgfx::TransientIndexBuffer indices; + bgfx::allocTransientVertexBuffer(&vertices, vertexcount, _ImGuiLayout); + bgfx::allocTransientIndexBuffer(&indices, indexcount); + + std::memcpy(vertices.data, commands->VtxBuffer.begin(), vertexcount * sizeof(ImDrawVert)); + std::memcpy(indices.data, commands->IdxBuffer.begin(), indexcount * sizeof(ImDrawIdx)); + + for (ImDrawCmd const & command : commands->CmdBuffer) { + if (command.ElemCount == 0) { + continue; + } + + int left = std::max((int)command.ClipRect.x + _OriginX, _OriginX); + int top = std::max((int)command.ClipRect.y + _OriginY, _OriginY); + int right = std::min((int)command.ClipRect.z + _OriginX, _OriginX + _Width); + int bottom = std::min((int)command.ClipRect.w + _OriginY, _OriginY + _Height); + + if (right <= left || bottom <= top) { + continue; + } + + bgfx::setScissor((uint16_t)left, (uint16_t)top, (uint16_t)(right - left), (uint16_t)(bottom - top)); + + bgfx::TextureHandle texture = Texture_From_Handle((uintptr_t)command.GetTexID()); + bgfx::setTexture(0, _TextureSampler, bgfx::isValid(texture) ? texture : _WhiteTexture, + _SamplerFlags); + + bgfx::setState(_BlendState); + bgfx::setVertexBuffer(0, &vertices, command.VtxOffset, vertexcount - command.VtxOffset); + bgfx::setIndexBuffer(&indices, command.IdxOffset, command.ElemCount); + bgfx::submit(BACKEND_VIEW_DEV, _Program); + } + } +} diff --git a/code/ui/uirmlview.h b/code/ui/uirmlview.h new file mode 100644 index 000000000..677070b60 --- /dev/null +++ b/code/ui/uirmlview.h @@ -0,0 +1,72 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The RmlUi half of a screen. A view owns its document and its data model and turns the +// toolkit's events into intents on the presenter it was built against. Only files under +// code/ui include this, because it names RmlUi types. +// +// docs/UI_DESIGN.md, "Screens", owns this contract. + +#pragma once + +#include "uiscreen.h" + +#include +#include + + +class UIRmlViewClass +{ + public: + UIRmlViewClass(UIPresenterClass & presenter, char const * document); + virtual ~UIRmlViewClass(void); + + // Loads the document, binds the data model and shows it. A failure leaves nothing + // shown and reports the resource that could not be prepared. + bool Prepare(bool modal); + + // Hides and releases the document in the order docs/UI_DESIGN.md sets out: the + // screen is marked closing first, then focus and capture are dropped, then the + // model is removed while its storage still lives. + void Close(void); + + // Takes the document off the screen without releasing it, and puts it back, so a + // screen opened on top of this one has the region and the input scope to itself. + void Hide(void); + void Show(void); + + bool Is_Visible(void) const; + + // Fills in the view-model's fields and events. Called once, before the document is + // loaded, since a document binds its model as it parses. + virtual void Bind(Rml::DataModelConstructor & model) = 0; + + // Marks whatever the last drained intents changed, so RmlUi redraws only that. + virtual void Sync(void) = 0; + + protected: + UIPresenterClass & Presenter; + Rml::String Document; + + // The document's name without its extension. A document names its model with this, + // and no two live screens may share one. + Rml::String ModelName; + + Rml::ElementDocument * Element = nullptr; + Rml::DataModelHandle Model; + + // Was the document shown exclusively? The shell's input scope is opened and closed + // with it, so this records what to undo rather than being asked again at close. + bool IsModal = false; +}; + + +// Runs a screen to a result the way the legacy dialog drivers do. uishell.cpp owns it; it +// is declared here because it names both halves of a screen. +UIResult UI_Run_Modal(UIPresenterClass & presenter, UIRmlViewClass & view); diff --git a/code/ui/uisavebrowser.cpp b/code/ui/uisavebrowser.cpp new file mode 100644 index 000000000..f479ebfc7 --- /dev/null +++ b/code/ui/uisavebrowser.cpp @@ -0,0 +1,508 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The save game browser. Behavior traced out of LoadOptionsClass::Dialog and the three +// command handlers in loaddlg.cpp. +// +// What the extraction fixes in place: a row carries its entry rather than its position in +// the control; the action button is disabled with an empty list but every check that can +// refuse the operation is made when the button is pressed; a refused load, a refused save +// and a declined deletion all leave the screen standing, which is what putting the state +// back to pending did; and deleting the last game leaves the screen accepted, which is what +// the delete arm did by falling out of its own loop. +// +// docs/UI_DESIGN.md, "Screens", owns the contracts this keeps to. + +#include "always.h" + +#include "uisavebrowser.h" + +#include "uirmlview.h" + +#include "campaign.h" +#include "conquer.h" +#include "data.h" +#include "gamedirs.h" +#include "init.h" +#include "language/language.h" +#include "loaddlg.h" +#include "msgbox.h" +#include "saveload.h" +#include "vector.h" + +#include +#include +#include +#include +#include + +#include +#include + + +// Is a saved game of this name already there? Asked before one is written, since a name the +// folder holds is written over rather than added to. +static bool Saved_Game_Exists(char const * name) +{ + return(GetFileAttributes(Saved_Game_Name(name).c_str()) != INVALID_FILE_ATTRIBUTES); +} + + +UISaveBrowserPresenterClass::UISaveBrowserPresenterClass(LoadOptionsClass & options, StyleType style) : + Style(style), + Options(options) +{ +} + + +/// +/// Is there room on disk to save at all? +/// +bool UISaveBrowserPresenterClass::Can_Open(void) +{ + if (Style != STYLE_SAVE) { + return(true); + } + + if (Disk_Space_Available() >= Options.MinSpaceRequired) { + return(true); + } + + WWMessageBox().Process(TXT_DISKFULL, TXT_OK, TXT_NONE, TXT_NONE); + return(false); +} + + +void UISaveBrowserPresenterClass::Refresh(void) +{ + Options.Build_List(); + + Entries.clear(); + Selected = -1; + + for (int index = 0; index < Options.Files.Count(); index++) { + FileEntryClass const * const file = Options.Files[index]; + + EntryType entry; + entry.Description = file->Descr; + entry.Session = (file->Type != GAME_NORMAL); + entry.Valid = file->Valid; + + if (file->DateTime.dwHighDateTime != (DWORD)-1 && file->DateTime.dwLowDateTime != (DWORD)-1) { + FILETIME local; + SYSTEMTIME stamp; + char buffer[128]; + + FileTimeToLocalFileTime(&file->DateTime, &local); + FileTimeToSystemTime(&local, &stamp); + + GetDateFormat(LANG_USER_DEFAULT, TIME_NOMINUTESORSECONDS, &stamp, NULL, buffer, sizeof(buffer)); + entry.Date = buffer; + GetTimeFormat(LANG_USER_DEFAULT, TIME_NOSECONDS, &stamp, NULL, buffer, sizeof(buffer)); + entry.Time = buffer; + } + + Entries.push_back(entry); + } + + // The load list opens on the first game it could actually read; the other two open on + // their first row, which for a save is the empty slot. + if (!Entries.empty()) { + Selected = 0; + + if (Style == STYLE_LOAD) { + for (size_t index = 0; index < Entries.size(); index++) { + if (Entries[index].Valid) { + Selected = (int)index; + break; + } + } + } + } + + CanAct = !Entries.empty(); + ListChanged = true; + + Description.clear(); + if (Style == STYLE_SAVE && Options.Description != NULL) { + Description = Options.Description; + } +} + + +/// +/// The maintenance the dialog driver ran on every pass of its own loop. +/// +void UISaveBrowserPresenterClass::Service(void) +{ + if (Options.Callback != NULL) { + Options.Callback(); + } + + if (!GameActive) { + Title_Screen_Restore(false); + } +} + + +void UISaveBrowserPresenterClass::Finish(bool accepted) +{ + Outcome = accepted; + + UIResult result; + result.Outcome = accepted ? UIResult::OUTCOME_ACCEPTED : UIResult::OUTCOME_CANCELLED; + Result = result; +} + + +/// +/// Loads the game the player picked, with the screen already out of the way. +/// A load that fails leaves the screen standing so another game can be tried, which is what +/// putting the dialog's state back to pending did. +/// +void UISaveBrowserPresenterClass::Run_Pending(void) +{ + if (Pending != SUB_LOAD) { + return; + } + + Pending = SUB_NONE; + + if (Selected < 0 || Selected >= Options.Files.Count()) { + return; + } + + if (!Options.Load_File(Options.Files[Selected]->Filename)) { + WWMessageBox().Process(TXT_ERROR_LOADING_GAME, TXT_OK, TXT_NONE, TXT_NONE); + return; + } + + Finish(true); +} + + +void UISaveBrowserPresenterClass::Accept(void) +{ + // No row means no operation, which is the LB_ERR the driver tested for. + if (Selected < 0 || Selected >= Options.Files.Count()) { + return; + } + + FileEntryClass * const entry = Options.Files[Selected]; + + switch (Style) { + case STYLE_LOAD: + // The campaign list is read before the screen steps aside, where the dialog + // read it, because the load needs it and a mission save carries a campaign. + if (entry->Num != -1) { + Init_Campaigns(); + } + Pending = SUB_LOAD; + break; + + case STYLE_SAVE: { + if (Description.empty()) { + WWMessageBox().Process(TXT_MUSTENTER_DESCRIPTION, TXT_OK, TXT_NONE, TXT_NONE); + FocusDescription = true; + return; + } + + char picked[256]; + char const * filename = NULL; + + if (entry->Valid) { + filename = entry->Filename; + } else { + Options.Pick_Filename(picked); + filename = picked; + } + + if (filename == NULL) { + return; + } + + // A name the folder already holds is written over, so it is confirmed first. + if (Saved_Game_Exists(filename) + && WWMessageBox()._Process(TXT_CONFIRM_SAVE, 1, TXT_YES, TXT_NO, TXT_NONE)) { + return; + } + + if (!Options.Save_File(filename, Description.c_str())) { + WWMessageBox().Process(TXT_ERROR_SAVING_GAME, TXT_OK, TXT_NONE, TXT_NONE); + return; + } + + int const confirmation = Options.Save_Confirmation(); + if (confirmation != TXT_NONE) { + WWMessageBox().Process(confirmation, TXT_OK, TXT_NONE, TXT_NONE); + } + + if (Options.Description != NULL) { + strcpy(Options.Description, Description.c_str()); + } + + Finish(true); + break; + } + + case STYLE_DELETE: { + char buffer[256]; + sprintf(buffer, "%s\n%s", Fetch_String(TXT_DELETE_FILE_QUERY), entry->Descr); + + if (WWMessageBox()._Process(buffer, 1, TXT_YES, TXT_NO, TXT_NONE)) { + return; + } + + Options.Delete_File(entry->Filename); + + Options.Files.Delete_Index(Selected); + delete entry; + Entries.erase(Entries.begin() + Selected); + + // The list stays open for another deletion; emptying it leaves the screen. + Selected = Entries.empty() ? -1 : 0; + CanAct = !Entries.empty(); + ListChanged = true; + + if (Entries.empty()) { + Finish(true); + } + break; + } + } +} + + +void UISaveBrowserPresenterClass::Execute(UIIntent const & intent) +{ + if (intent.Action == UI_SAVEBROWSER_SELECT) { + if (intent.Value < 0 || intent.Value >= (int)Entries.size()) { + return; + } + + Selected = intent.Value; + + // Picking a row in the save list offers that game's description, so an existing + // game can be written over without typing its name out again; the empty slot offers + // the description the caller suggested. + if (Style == STYLE_SAVE) { + if (Entries[Selected].Valid) { + Description = Entries[Selected].Description; + } else if (Options.Description != NULL) { + Description = Options.Description; + } + FocusDescription = true; + } + return; + } + + if (intent.Action == UI_SAVEBROWSER_DESCRIBE) { + Description = intent.Identity; + if (Description.size() > DESCRIPTION_LIMIT) { + Description.resize(DESCRIPTION_LIMIT); + } + return; + } + + if (intent.Action == UI_SAVEBROWSER_ACCEPT) { + Accept(); + return; + } + + if (intent.Action == UI_SAVEBROWSER_CANCEL) { + Finish(false); + return; + } +} + + +//--------------------------------------------------------------------------------------- +// The RmlUi view. +//--------------------------------------------------------------------------------------- + +/// +/// The RmlUi half of the save game browser. +/// +class SaveBrowserViewClass : public UIRmlViewClass +{ + public: + SaveBrowserViewClass(UISaveBrowserPresenterClass & presenter, char const * document) : + UIRmlViewClass(presenter, document), + Screen(presenter) + { + } + + virtual void Bind(Rml::DataModelConstructor & model) override; + virtual void Sync(void) override; + + private: + void Press(Rml::String const & action); + + // The description the field is holding, which the save screen reads when the action + // button is pressed rather than tracking, as the dialog read its edit control. + std::string Field_Text(void) const; + + Rml::ElementFormControlInput * Field(void) const; + + UISaveBrowserPresenterClass & Screen; +}; + + +Rml::ElementFormControlInput * SaveBrowserViewClass::Field(void) const +{ + if (Element == nullptr) { + return(nullptr); + } + return(rmlui_dynamic_cast(Element->GetElementById("description"))); +} + + +std::string SaveBrowserViewClass::Field_Text(void) const +{ + Rml::ElementFormControlInput * const field = Field(); + if (field == nullptr) { + return(Screen.Description); + } + return(field->GetValue()); +} + + +/// +/// Queues what a button or its key stands for. +/// The save screen reads the description out of the field here rather than tracking it, +/// because that is when the dialog read its edit control, and the read is queued ahead of +/// the action it is read for so the two execute in that order. +/// +void SaveBrowserViewClass::Press(Rml::String const & action) +{ + if (Screen.Style == UISaveBrowserPresenterClass::STYLE_SAVE && action == UI_SAVEBROWSER_ACCEPT) { + std::string const text = Field_Text(); + if (text != Screen.Description) { + Screen.Queue(UIIntent{UI_SAVEBROWSER_DESCRIBE, text, 0}); + } + } + + Screen.Queue(UIIntent{action, "", 0}); +} + + +void SaveBrowserViewClass::Bind(Rml::DataModelConstructor & model) +{ + if (auto entry = model.RegisterStruct()) { + entry.RegisterMember("description", &UISaveBrowserPresenterClass::EntryType::Description); + entry.RegisterMember("date", &UISaveBrowserPresenterClass::EntryType::Date); + entry.RegisterMember("time", &UISaveBrowserPresenterClass::EntryType::Time); + } + model.RegisterArray>(); + + model.Bind("entries", &Screen.Entries); + model.Bind("selected", &Screen.Selected); + model.Bind("description", &Screen.Description); + model.Bind("canact", &Screen.CanAct); + + model.BindEventCallback("pick", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{UI_SAVEBROWSER_SELECT, "", (int)arguments[0].Get()}); + }); + + // The field is bound one way, so a value the model already holds is never queued back as + // a change the player did not type. + model.BindEventCallback("describe", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + Rml::String const value = event.GetParameter("value", Rml::String()); + if (value == Screen.Description) return; + Screen.Queue(UIIntent{UI_SAVEBROWSER_DESCRIBE, value, 0}); + }); + + model.BindEventCallback("press", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Press(arguments[0].Get()); + }); + + // Escape cancels and Enter presses the action button, which is what IsDialogMessage + // delivered to a template that names IDCANCEL and no default push button. + model.BindEventCallback("key", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + int const key = event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN); + if (key == Rml::Input::KI_ESCAPE) { + Press(UI_SAVEBROWSER_CANCEL); + } else if (key == Rml::Input::KI_RETURN || key == Rml::Input::KI_NUMPADENTER) { + Press(UI_SAVEBROWSER_ACCEPT); + } + }); +} + + +void SaveBrowserViewClass::Sync(void) +{ + if (!Model) return; + + Model.DirtyVariable("entries"); + Model.DirtyVariable("selected"); + Model.DirtyVariable("description"); + Model.DirtyVariable("canact"); + + // Picking a row, and a refused empty description, put the focus on the field with its + // text selected, which is what the dialog did with SetFocus and Edit_SetSel. + if (Screen.FocusDescription) { + Screen.FocusDescription = false; + if (Rml::ElementFormControlInput * const field = Field()) { + field->Focus(); + field->Select(); + } + } + + Screen.ListChanged = false; +} + + +/// +/// Shows the browser and waits for the player to leave it. +/// +UIResult UI_Save_Browser_Screen(UISaveBrowserPresenterClass & presenter) +{ + char const * document = "missionload.rml"; + if (presenter.Style == UISaveBrowserPresenterClass::STYLE_SAVE) { + document = "missionsave.rml"; + } else if (presenter.Style == UISaveBrowserPresenterClass::STYLE_DELETE) { + document = "missiondelete.rml"; + } + + SaveBrowserViewClass view(presenter, document); + + if (!view.Prepare(true)) { + UIResult result; + result.Outcome = UIResult::OUTCOME_FAILED_TO_OPEN; + return(result); + } + + // Loading draws where this screen is, so the document steps aside for it, which is what + // the dialog's own ShowWindow did. + while (!presenter.Result.has_value()) { + UI_Run_Modal(presenter, view); + + if (presenter.Pending == UISaveBrowserPresenterClass::SUB_NONE) { + break; + } + + view.Hide(); + presenter.Run_Pending(); + if (presenter.Result.has_value()) { + break; + } + view.Show(); + view.Sync(); + } + + UIResult const result = presenter.Result.value_or(UIResult{}); + view.Close(); + return(result); +} diff --git a/code/ui/uisavebrowser.h b/code/ui/uisavebrowser.h new file mode 100644 index 000000000..b5c1e6711 --- /dev/null +++ b/code/ui/uisavebrowser.h @@ -0,0 +1,123 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The save game browser's behavior, with no toolkit in it. One screen serves loading, +// saving and deleting, because the three templates differ by which controls exist and by +// what the action button does, not by how the list is built. +// +// docs/UI_DESIGN.md, "Screens", owns the contract. + +#pragma once + +#include "uiscreen.h" + +#include +#include + +class LoadOptionsClass; + + +inline constexpr char const * UI_SAVEBROWSER_SELECT = "select"; // Value: row +inline constexpr char const * UI_SAVEBROWSER_DESCRIBE = "describe"; // Identity: the text +inline constexpr char const * UI_SAVEBROWSER_ACCEPT = "accept"; +inline constexpr char const * UI_SAVEBROWSER_CANCEL = "cancel"; + + +class UISaveBrowserPresenterClass : public UIPresenterClass +{ + public: + enum StyleType { + STYLE_LOAD, + STYLE_SAVE, + STYLE_DELETE, + }; + + // A screen this one opens on top of itself. Loading takes a while and draws where + // this screen is, so the view steps aside for it, which is what the dialog's own + // ShowWindow did. + enum SubScreenType { + SUB_NONE, + SUB_LOAD, + }; + + struct EntryType + { + std::string Description; + + // The date and the time the list showed in its own two columns. + std::string Date; + std::string Time; + + // Was the game a multiplayer one? The list marked those with a star. + bool Session = false; + + // Does the row stand for a saved game? The save list opens with one row that + // does not, which is the empty slot. + bool Valid = false; + }; + + UISaveBrowserPresenterClass(LoadOptionsClass & options, StyleType style); + + virtual void Execute(UIIntent const & intent) override; + virtual void Refresh(void) override; + virtual void Service(void) override; + virtual bool Suspends(void) const override { return(Pending != SUB_NONE); } + + // Is there room on disk to save at all? Reports the shortage where the dialog + // reported it, before anything is shown. + bool Can_Open(void); + + // Runs the sub-screen an executed intent asked for and clears the request. Safe to + // call with nothing pending. + void Run_Pending(void); + + // Did the player go through with the operation? This is what the callers read. + bool Accepted(void) const { return(Outcome); } + + /* + ** The view-model. Plain values, and the only thing a view reads. + */ + StyleType Style; + + std::vector Entries; + int Selected = -1; + + // What the description field holds. The save screen is the only style that has one. + std::string Description; + + // The longest description the field accepts, in bytes, which is what the dialog + // capped the edit control at. + enum { DESCRIPTION_LIMIT = 79 }; + + // Is there anything for the action button to act upon? The dialog disabled it with + // an empty list. + bool CanAct = false; + + // Should the description field take the focus with its text selected? The dialog + // did that when a row was picked and when it refused an empty description. + bool FocusDescription = false; + + SubScreenType Pending = SUB_NONE; + + // Has the list itself changed since a view last drew it? Only a deletion moves it, + // so a view that rebuilds a control has one thing to test. + bool ListChanged = false; + + private: + void Accept(void); + void Finish(bool accepted); + + LoadOptionsClass & Options; + bool Outcome = false; +}; + + +// Shows the browser through its RmlUi view. FAILED_TO_OPEN leaves nothing shown and the +// caller falls through to the legacy dialog. +UIResult UI_Save_Browser_Screen(UISaveBrowserPresenterClass & presenter); diff --git a/code/ui/uiscenariopick.cpp b/code/ui/uiscenariopick.cpp new file mode 100644 index 000000000..95a732287 --- /dev/null +++ b/code/ui/uiscenariopick.cpp @@ -0,0 +1,309 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The multiplayer map selection screen. Behavior traced out of Scenario_DlgProc and +// Scenario_Select_Callback in netshare.cpp. +// +// What the extraction fixes in place: the preview follows the highlighted row rather than +// the session's own choice, and the session's choice is put back after every look, so +// browsing the list changes nothing until the player accepts; the random map generator draws +// where this screen is, so the screen steps aside for it; and backing out leaves the +// scenario the screen opened on. +// +// docs/UI_DESIGN.md, "Screens", owns the contracts this keeps to. + +#include "always.h" + +#include "uiscenariopick.h" + +#include "uimappreview.h" +#include "uirmlview.h" + +#include "data.h" +#include "mapgen.h" +#include "netdlg2.h" +#include "netshare.h" +#include "preview.h" +#include "session.h" + +#include +#include +#include + +#include +#include +#include + + +UIScenarioPickPresenterClass::UIScenarioPickPresenterClass(void) +{ +} + + +void UIScenarioPickPresenterClass::Build_List(void) +{ + Scenarios.clear(); + for (int index = 0; index < Session.Scenarios.Count(); index++) { + Scenarios.push_back(Session.Scenarios[index]->Description()); + } + ListChanged = true; +} + + +void UIScenarioPickPresenterClass::Refresh(void) +{ + Build_List(); + + Selected = Session.Options.ScenarioIndex; + Original = Selected; + LastPreviewed = -1; +} + + +/// +/// Builds the preview for the highlighted row and puts the session's own choice back. +/// The list can be walked without committing to anything, so the scenario information the +/// preview needs is loaded, used, and then replaced by the one the screen opened on. +/// +void UIScenarioPickPresenterClass::Preview_Selection(void) +{ + if (Selected == LastPreviewed || Selected < 0 || Selected >= Session.Scenarios.Count()) { + return; + } + + Set_Scenario_Info_From_Index(Selected); + + // A generated map has a picture of its own beside it rather than one read out of the map. + if (stricmp(Session.Scenarios[Selected]->Get_Filename(), RANDOM_MAP_FILE_NAME) == 0) { + delete MultiplayerMapPreview; + MultiplayerMapPreview = new MapPreviewClass; + MultiplayerMapPreview->Read_PCX_Preview("RandMap.img"); + if (MultiplayerMapPreview->Get_Preview_Surface() == NULL) { + Rebuild_Network_Map_Preview(); + } + } else { + Rebuild_Network_Map_Preview(); + } + + LastPreviewed = Selected; + PreviewGeneration++; + + Session.Options.ScenarioIndex = Original; + Set_Scenario_Info_From_Index(Original); +} + + +/// +/// The maintenance the dialog's wait callback ran on every pass of its own loop. +/// +void UIScenarioPickPresenterClass::Service(void) +{ + Preview_Selection(); + + // A game already in the lobby keeps talking while the host browses. The runner services + // a session of its own, so only the network pump the callback added belongs here. + if (Session.Type == GAME_IPX || Session.Type == GAME_INTERNET) { + Net2Callback(); + } +} + + +/// +/// Runs the map generator with the screen already out of the way. +/// +void UIScenarioPickPresenterClass::Run_Pending(void) +{ + if (Pending != SUB_RANDOM_MAP) { + return; + } + + Pending = SUB_NONE; + + int const scenario = CreateRandomMap(); + if (scenario == -1) { + return; + } + + // A generated map joins the list and is highlighted, but the session keeps the scenario + // the screen opened on until the player accepts. + Build_List(); + Selected = scenario; + LastPreviewed = -1; + + Set_Scenario_Info_From_Index(scenario); + if (MultiplayerMapPreview == NULL || MultiplayerMapPreview->Get_Preview_Surface() == NULL) { + Rebuild_Network_Map_Preview(); + } + PreviewGeneration++; + + Session.Options.ScenarioIndex = Original; + Set_Scenario_Info_From_Index(Original); +} + + +void UIScenarioPickPresenterClass::Execute(UIIntent const & intent) +{ + if (intent.Action == UI_SCENARIOPICK_SELECT) { + if (intent.Value < 0 || intent.Value >= (int)Scenarios.size()) { + return; + } + Selected = intent.Value; + return; + } + + if (intent.Action == UI_SCENARIOPICK_RANDOM) { + Pending = SUB_RANDOM_MAP; + return; + } + + if (intent.Action == UI_SCENARIOPICK_ACCEPT) { + // The dialog clamped a list with nothing selected to the first entry. + Selected = Selected > 0 ? Selected : 0; + Session.Options.ScenarioIndex = Selected; + + UIResult result; + result.Outcome = UIResult::OUTCOME_ACCEPTED; + result.Value = Selected; + Result = result; + return; + } + + if (intent.Action == UI_SCENARIOPICK_CANCEL) { + UIResult result; + result.Outcome = UIResult::OUTCOME_CANCELLED; + Result = result; + return; + } +} + + +//--------------------------------------------------------------------------------------- +// The RmlUi view. +//--------------------------------------------------------------------------------------- + +// The picture the preview frame holds, in game logical units. The frame is the template's +// 129 x 80 dialog units, which is 193.5 by 130 at the family's 1.5 across and 1.625 down, +// and the picture sits inside its one pixel border. +enum { PREVIEW_WIDTH = 191, PREVIEW_HEIGHT = 128 }; + + +/// +/// The RmlUi half of the map selection screen. +/// +class ScenarioPickViewClass : public UIRmlViewClass +{ + public: + ScenarioPickViewClass(UIScenarioPickPresenterClass & presenter) : + UIRmlViewClass(presenter, "selectmap.rml"), + Screen(presenter) + { + UI_Register_Surface(Screen.Preview.c_str(), &Picture); + } + + virtual ~ScenarioPickViewClass(void) override + { + UI_Unregister_Surface(Screen.Preview.c_str()); + } + + virtual void Bind(Rml::DataModelConstructor & model) override; + virtual void Sync(void) override; + + private: + UIScenarioPickPresenterClass & Screen; + + // The view owns the pixels; the presenter carries only the name they answer to. + MapPreviewSurfaceClass Picture{PREVIEW_WIDTH, PREVIEW_HEIGHT}; + + unsigned int Drawn = 0; +}; + + +void ScenarioPickViewClass::Bind(Rml::DataModelConstructor & model) +{ + model.RegisterArray>(); + + model.Bind("scenarios", &Screen.Scenarios); + model.Bind("selected", &Screen.Selected); + model.Bind("preview", &Screen.Preview); + + model.BindEventCallback("pick", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{UI_SCENARIOPICK_SELECT, "", (int)arguments[0].Get()}); + }); + + model.BindEventCallback("press", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{arguments[0].Get(), "", 0}); + }); + + // Escape backs out and Enter takes the highlighted map, which is what IsDialogMessage + // delivered to a template that names IDCANCEL and no default push button. + model.BindEventCallback("key", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + int const key = event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN); + if (key == Rml::Input::KI_ESCAPE) { + Screen.Queue(UIIntent{UI_SCENARIOPICK_CANCEL, "", 0}); + } else if (key == Rml::Input::KI_RETURN || key == Rml::Input::KI_NUMPADENTER) { + Screen.Queue(UIIntent{UI_SCENARIOPICK_ACCEPT, "", 0}); + } + }); +} + + +void ScenarioPickViewClass::Sync(void) +{ + if (!Model) return; + + Model.DirtyVariable("scenarios"); + Model.DirtyVariable("selected"); + + // The picture is redrawn where it changed, not every pass, so the element uploads once + // per map rather than once per present. + if (Screen.PreviewGeneration != Drawn) { + Drawn = Screen.PreviewGeneration; + Picture.Redraw(); + } + + Screen.ListChanged = false; +} + + +/// +/// Shows the map selection screen and waits for the player to leave it. +/// +UIResult UI_Scenario_Pick_Screen(UIScenarioPickPresenterClass & presenter) +{ + ScenarioPickViewClass view(presenter); + + if (!view.Prepare(true)) { + UIResult result; + result.Outcome = UIResult::OUTCOME_FAILED_TO_OPEN; + return(result); + } + + // The map generator draws where this screen is, so the document steps aside for it. + while (!presenter.Result.has_value()) { + UI_Run_Modal(presenter, view); + + if (presenter.Pending == UIScenarioPickPresenterClass::SUB_NONE) { + break; + } + + view.Hide(); + presenter.Run_Pending(); + view.Show(); + view.Sync(); + } + + UIResult const result = presenter.Result.value_or(UIResult{}); + view.Close(); + return(result); +} diff --git a/code/ui/uiscenariopick.h b/code/ui/uiscenariopick.h new file mode 100644 index 000000000..bb03ef5bb --- /dev/null +++ b/code/ui/uiscenariopick.h @@ -0,0 +1,87 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The multiplayer map selection screen's behavior, with no toolkit in it. The skirmish +// setup screen and the network lobbies both open it. +// +// docs/UI_DESIGN.md, "Screens", owns the contract. + +#pragma once + +#include "uiscreen.h" + +#include +#include + + +inline constexpr char const * UI_SCENARIOPICK_SELECT = "select"; +inline constexpr char const * UI_SCENARIOPICK_ACCEPT = "accept"; +inline constexpr char const * UI_SCENARIOPICK_CANCEL = "cancel"; +inline constexpr char const * UI_SCENARIOPICK_RANDOM = "random"; + + +// The artwork the map preview is registered under. A presenter carries the name; the view +// owns the provider. +inline constexpr char const * UI_MAP_PREVIEW_SURFACE = "mappreview"; + + +class UIScenarioPickPresenterClass : public UIPresenterClass +{ + public: + // A screen this one opens on top of itself. The map generator draws where this + // screen is, so the view steps aside for it, which is what its ShowWindow did. + enum SubScreenType { + SUB_NONE, + SUB_RANDOM_MAP, + }; + + UIScenarioPickPresenterClass(void); + + virtual void Execute(UIIntent const & intent) override; + virtual void Refresh(void) override; + virtual void Service(void) override; + virtual bool Suspends(void) const override { return(Pending != SUB_NONE); } + + // Runs the sub-screen an executed intent asked for and clears the request. + void Run_Pending(void); + + // Which scenario the player settled on. Only meaningful once the screen accepted. + int Chosen(void) const { return(Selected); } + + /* + ** The view-model. + */ + std::vector Scenarios; + int Selected = 0; + + // The name of the artwork the preview draws into, never a surface. + std::string Preview = UI_MAP_PREVIEW_SURFACE; + + // Moves whenever the preview was rebuilt, so a view marks its provider dirty once + // per change rather than once per pass. + unsigned int PreviewGeneration = 0; + + SubScreenType Pending = SUB_NONE; + + // Has the list itself changed? Only the map generator moves it. + bool ListChanged = false; + + private: + void Build_List(void); + void Preview_Selection(void); + + // The scenario the screen opened on. The preview walks the list without moving the + // session's own choice, so the session is put back after every look. + int Original = 0; + int LastPreviewed = -1; +}; + + +// Shows the picker through its RmlUi view. +UIResult UI_Scenario_Pick_Screen(UIScenarioPickPresenterClass & presenter); diff --git a/code/ui/uiscreen.h b/code/ui/uiscreen.h new file mode 100644 index 000000000..6c550ddc0 --- /dev/null +++ b/code/ui/uiscreen.h @@ -0,0 +1,92 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The toolkit-free half of a screen. A presenter holds the view-model, answers queries and +// executes intents; it never sees a window handle, a surface, an RmlUi element or an ImGui +// call, so the same presenter serves an RmlUi view, a legacy dialog and a test. +// +// docs/UI_DESIGN.md, "Screens", owns this contract. + +#pragma once + +#include +#include +#include + + +// What a view asks the presenter to do. An intent carries identities and copied data only: +// never a document node, a borrowed buffer, a window handle or an engine pointer, because +// it is executed at the owner's next safe point rather than where it was raised. +struct UIIntent +{ + std::string Action; + std::string Identity; + int Value = 0; +}; + + +// What a screen answers with. The outcome maps onto the return values the dialog drivers +// already use, and GameEnded carries what OwnerDraw::Dialog_Message_Handler returns. +struct UIResult +{ + enum OutcomeType { + OUTCOME_ACCEPTED, + OUTCOME_CANCELLED, + OUTCOME_SESSION_ENDED, + OUTCOME_FAILED_TO_OPEN, + }; + + OutcomeType Outcome = OUTCOME_CANCELLED; + int Value = 0; + bool GameEnded = false; +}; + + +class UIPresenterClass +{ + public: + virtual ~UIPresenterClass(void) {} + + // Records an intent for the owner to execute. Safe to call from a toolkit event + // handler, which must never act directly. + void Queue(UIIntent const & intent) { Intents.push_back(intent); } + + // Executes every queued intent in order at the owner's safe point. Intents raised + // while a screen is closing are discarded rather than replayed. + void Drain(void) + { + std::vector pending; + pending.swap(Intents); + + for (UIIntent const & intent : pending) { + if (IsClosing) break; + Execute(intent); + } + } + + void Discard(void) { Intents.clear(); } + + virtual void Execute(UIIntent const & intent) = 0; + virtual void Refresh(void) = 0; + + // The maintenance a screen's own driver ran on every pass of its loop, kept where it + // was rather than moved into the runner. + virtual void Service(void) {} + + // Does the screen want its runner to return before it has a result? A screen of a + // different kind opened over this one draws where this one is, so the owner takes + // this one off the screen and puts it back, which is what a dialog's ShowWindow did. + virtual bool Suspends(void) const { return(false); } + + std::optional Result; + bool IsClosing = false; + + protected: + std::vector Intents; +}; diff --git a/code/ui/uishell.cpp b/code/ui/uishell.cpp new file mode 100644 index 000000000..6c7cf824b --- /dev/null +++ b/code/ui/uishell.cpp @@ -0,0 +1,1010 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The UI shell. It owns the RmlUi context, the ImGui context, the overlay pass, the input +// hook and the modal runner, and it is the only place outside code/ui that any of those +// libraries is reachable from. +// +// docs/UI_DESIGN.md owns the architecture. Nothing here knows what a screen means: it +// knows which presentation owns a region and an input scope, and no more. + +#include "always.h" + +#include "uishell.h" + +#include "drawhelp.h" + +#include "uiinternal.h" +#include "uirmlview.h" + +#include "_keyboar.h" +#include "_xmouse.h" +#include "dbgprint.h" +#include "hostclock.h" +#include "conquer.h" +#include "goptions.h" +#include "keyboard.h" +#include "mainloop.h" +#include "msgloop.h" +#include "session.h" +#include "vidscale.h" +#include "video.h" + +#include + +#include + +#include +#include + +#include + + +static bool _Initialized = false; +static Rml::Context * _Context = nullptr; +static bool _OverlayIsDirty = false; +static unsigned int _LastTickTime = 0; + +// Set while a document is being shown or hidden, so the message pump that the keyboard +// queue's own cleanup runs cannot re-enter the screen it is closing. +static bool _Changing = false; + +// How many exclusive modal documents are shown. A modal takes every mouse and key message +// the way IgnoreInput does around a legacy dialog, and screens nest, so this counts rather +// than flags. +static int _ModalDepth = 0; + +// How many shown documents have handed the mouse pointer to the host. A legacy dialog gave +// the pointer back to Windows for as long as it was up, which is what drew an arrow over it; +// the game's own pointer is a shape it only has while a scenario is running. + +// How many modal runners are on the stack. A runner owns the context between its own +// passes, so the tick that Main_Loop and Call_Back make from inside one is dropped rather +// than updating the context a second time in the same pass. +static int _RunningModal = 0; + + +// The window holds the mouse capture while a gesture a toolkit consumed is in progress. +// The owner of a press owns its release, so a press that crossed into the game or out of +// it still completes where it started. +static int _CaptureButton = -1; + +#ifndef NDEBUG +static Rml::ElementDocument * _TestDocument = nullptr; +#endif + + +// The keys RmlUi names that are not part of one of the four runs above. The pairing is +// read in both directions, so an entry's order matters where two virtual keys share an +// identifier or two identifiers share a virtual key: the first entry naming a virtual key +// is the identifier that key produces, and the first entry naming an identifier is the +// virtual key it converts back to. +// +// Where a modifier has both a general and a sided code the general one comes first, because +// that is what this engine's keyboard queue is written against: keyboard.h has no name for +// 0xA0 to 0xA5 at all, and platform/win32compat reports 0x10, 0x11 and 0x12 for both sides. +struct KeyPairType +{ + unsigned short VirtualKey; + Rml::Input::KeyIdentifier Identifier; +}; + +static KeyPairType const _KeyPairs[] = { + { 0x08, Rml::Input::KI_BACK }, + { 0x09, Rml::Input::KI_TAB }, + { 0x0C, Rml::Input::KI_CLEAR }, + { 0x0D, Rml::Input::KI_RETURN }, + { 0x10, Rml::Input::KI_LSHIFT }, + { 0x11, Rml::Input::KI_LCONTROL }, + { 0x12, Rml::Input::KI_LMENU }, + { 0x13, Rml::Input::KI_PAUSE }, + { 0x14, Rml::Input::KI_CAPITAL }, + { 0x15, Rml::Input::KI_KANA }, + { 0x15, Rml::Input::KI_HANGUL }, + { 0x17, Rml::Input::KI_JUNJA }, + { 0x18, Rml::Input::KI_FINAL }, + { 0x19, Rml::Input::KI_HANJA }, + { 0x19, Rml::Input::KI_KANJI }, + { 0x1B, Rml::Input::KI_ESCAPE }, + { 0x1C, Rml::Input::KI_CONVERT }, + { 0x1D, Rml::Input::KI_NONCONVERT }, + { 0x1E, Rml::Input::KI_ACCEPT }, + { 0x1F, Rml::Input::KI_MODECHANGE }, + { 0x20, Rml::Input::KI_SPACE }, + { 0x21, Rml::Input::KI_PRIOR }, + { 0x22, Rml::Input::KI_NEXT }, + { 0x23, Rml::Input::KI_END }, + { 0x24, Rml::Input::KI_HOME }, + { 0x25, Rml::Input::KI_LEFT }, + { 0x26, Rml::Input::KI_UP }, + { 0x27, Rml::Input::KI_RIGHT }, + { 0x28, Rml::Input::KI_DOWN }, + { 0x29, Rml::Input::KI_SELECT }, + { 0x2A, Rml::Input::KI_PRINT }, + { 0x2B, Rml::Input::KI_EXECUTE }, + { 0x2C, Rml::Input::KI_SNAPSHOT }, + { 0x2D, Rml::Input::KI_INSERT }, + { 0x2E, Rml::Input::KI_DELETE }, + { 0x2F, Rml::Input::KI_HELP }, + { 0x5B, Rml::Input::KI_LWIN }, + { 0x5C, Rml::Input::KI_RWIN }, + { 0x5D, Rml::Input::KI_APPS }, + { 0x5F, Rml::Input::KI_SLEEP }, + { 0x6A, Rml::Input::KI_MULTIPLY }, + { 0x6B, Rml::Input::KI_ADD }, + { 0x6C, Rml::Input::KI_SEPARATOR }, + { 0x6D, Rml::Input::KI_SUBTRACT }, + { 0x6E, Rml::Input::KI_DECIMAL }, + { 0x6F, Rml::Input::KI_DIVIDE }, + { 0x90, Rml::Input::KI_NUMLOCK }, + { 0x91, Rml::Input::KI_SCROLL }, + { 0x92, Rml::Input::KI_OEM_NEC_EQUAL }, + { 0x92, Rml::Input::KI_OEM_FJ_JISHO }, + { 0x93, Rml::Input::KI_OEM_FJ_MASSHOU }, + { 0x94, Rml::Input::KI_OEM_FJ_TOUROKU }, + { 0x95, Rml::Input::KI_OEM_FJ_LOYA }, + { 0x96, Rml::Input::KI_OEM_FJ_ROYA }, + { 0xA0, Rml::Input::KI_LSHIFT }, + { 0xA1, Rml::Input::KI_RSHIFT }, + { 0xA2, Rml::Input::KI_LCONTROL }, + { 0xA3, Rml::Input::KI_RCONTROL }, + { 0xA4, Rml::Input::KI_LMENU }, + { 0xA5, Rml::Input::KI_RMENU }, + { 0xA6, Rml::Input::KI_BROWSER_BACK }, + { 0xA7, Rml::Input::KI_BROWSER_FORWARD }, + { 0xA8, Rml::Input::KI_BROWSER_REFRESH }, + { 0xA9, Rml::Input::KI_BROWSER_STOP }, + { 0xAA, Rml::Input::KI_BROWSER_SEARCH }, + { 0xAB, Rml::Input::KI_BROWSER_FAVORITES }, + { 0xAC, Rml::Input::KI_BROWSER_HOME }, + { 0xAD, Rml::Input::KI_VOLUME_MUTE }, + { 0xAE, Rml::Input::KI_VOLUME_DOWN }, + { 0xAF, Rml::Input::KI_VOLUME_UP }, + { 0xB0, Rml::Input::KI_MEDIA_NEXT_TRACK }, + { 0xB1, Rml::Input::KI_MEDIA_PREV_TRACK }, + { 0xB2, Rml::Input::KI_MEDIA_STOP }, + { 0xB3, Rml::Input::KI_MEDIA_PLAY_PAUSE }, + { 0xB4, Rml::Input::KI_LAUNCH_MAIL }, + { 0xB5, Rml::Input::KI_LAUNCH_MEDIA_SELECT }, + { 0xB6, Rml::Input::KI_LAUNCH_APP1 }, + { 0xB7, Rml::Input::KI_LAUNCH_APP2 }, + { 0xBA, Rml::Input::KI_OEM_1 }, + { 0xBB, Rml::Input::KI_OEM_PLUS }, + { 0xBC, Rml::Input::KI_OEM_COMMA }, + { 0xBD, Rml::Input::KI_OEM_MINUS }, + { 0xBE, Rml::Input::KI_OEM_PERIOD }, + { 0xBF, Rml::Input::KI_OEM_2 }, + { 0xC0, Rml::Input::KI_OEM_3 }, + { 0xDB, Rml::Input::KI_OEM_4 }, + { 0xDC, Rml::Input::KI_OEM_5 }, + { 0xDD, Rml::Input::KI_OEM_6 }, + { 0xDE, Rml::Input::KI_OEM_7 }, + { 0xDF, Rml::Input::KI_OEM_8 }, + { 0xE1, Rml::Input::KI_OEM_AX }, + { 0xE2, Rml::Input::KI_OEM_102 }, + { 0xE3, Rml::Input::KI_ICO_HELP }, + { 0xE4, Rml::Input::KI_ICO_00 }, + { 0xE5, Rml::Input::KI_PROCESSKEY }, + { 0xE6, Rml::Input::KI_ICO_CLEAR }, + { 0xF6, Rml::Input::KI_ATTN }, + { 0xF7, Rml::Input::KI_CRSEL }, + { 0xF8, Rml::Input::KI_EXSEL }, + { 0xF9, Rml::Input::KI_EREOF }, + { 0xFA, Rml::Input::KI_PLAY }, + { 0xFB, Rml::Input::KI_ZOOM }, + { 0xFD, Rml::Input::KI_PA1 }, + { 0xFE, Rml::Input::KI_OEM_CLEAR }, +}; + + +/// +/// Turns a Win32 virtual key into the identifier RmlUi names it by. +/// Every key RmlUi has a name for is mapped, because a key nothing maps is never delivered +/// to a document at all and a screen that binds a shortcut has to see the whole keyboard. +/// +static Rml::Input::KeyIdentifier Key_Identifier(WPARAM key) +{ + using namespace Rml::Input; + + // The four runs where the two enumerations march in step. + if (key >= 'A' && key <= 'Z') { + return((KeyIdentifier)(KI_A + (int)(key - 'A'))); + } + if (key >= '0' && key <= '9') { + return((KeyIdentifier)(KI_0 + (int)(key - '0'))); + } + if (key >= 0x60 && key <= 0x69) { + return((KeyIdentifier)(KI_NUMPAD0 + (int)(key - 0x60))); + } + if (key >= VK_F1 && key <= VK_F24) { + return((KeyIdentifier)(KI_F1 + (int)(key - VK_F1))); + } + + for (KeyPairType const & pair : _KeyPairs) { + if (pair.VirtualKey == key) { + return(pair.Identifier); + } + } + + return(KI_UNKNOWN); +} + + +/// +/// Turns an identifier RmlUi reports back into the Win32 virtual key it came from. +/// A screen that records a keypress needs this, because an RmlUi key event carries the +/// identifier and the game's own encoding is a virtual key. +/// +/// int; The virtual key, or zero for an identifier no key produces. +int UI_Virtual_Key(int identifier) +{ + using namespace Rml::Input; + + if (identifier >= KI_A && identifier <= KI_Z) { + return('A' + (identifier - KI_A)); + } + if (identifier >= KI_0 && identifier <= KI_9) { + return('0' + (identifier - KI_0)); + } + if (identifier >= KI_NUMPAD0 && identifier <= KI_NUMPAD9) { + return(0x60 + (identifier - KI_NUMPAD0)); + } + if (identifier >= KI_F1 && identifier <= KI_F24) { + return(VK_F1 + (identifier - KI_F1)); + } + + // The numeric keypad's Enter is a Return as far as Win32 is concerned; only the + // extended-key bit in a message's own parameters tells them apart, and that bit is + // gone by the time RmlUi has named the key. + if (identifier == KI_NUMPADENTER) { + return(VK_RETURN); + } + + for (KeyPairType const & pair : _KeyPairs) { + if (pair.Identifier == identifier) { + return(pair.VirtualKey); + } + } + + return(0); +} + + +static int Key_Modifiers(void) +{ + int modifiers = 0; + + if ((GetKeyState(VK_CONTROL) & 0x8000) != 0) modifiers |= Rml::Input::KM_CTRL; + if ((GetKeyState(VK_SHIFT) & 0x8000) != 0) modifiers |= Rml::Input::KM_SHIFT; + if ((GetKeyState(VK_MENU) & 0x8000) != 0) modifiers |= Rml::Input::KM_ALT; + + return(modifiers); +} + + +/// +/// Converts a position in the window's client area into the overlay's own space. +/// The overlay is laid out in physical pixels measured from the frame's top left corner, +/// so the letterbox bars fall outside it and never become an edge click. +/// +static void Client_Point_To_Overlay(POINT & point) +{ + VideoScaleInfo const & scale = Video_Get_Scale_Info(); + + point.x -= scale.DestX; + point.y -= scale.DestY; +} + + +/// +/// Reads the position a mouse message carries, in the overlay's space. +/// The wheel reports in screen coordinates and everything else in the client area, which +/// is the one difference the conversion has to make. +/// +static POINT Message_Point(UINT message, LPARAM lparam) +{ + POINT point; + point.x = GET_X_LPARAM(lparam); + point.y = GET_Y_LPARAM(lparam); + + if (message == WM_MOUSEWHEEL) { + ScreenToClient(MainWindow, &point); + } + + Client_Point_To_Overlay(point); + return(point); +} + + +/// +/// Records that something the shell draws has to be put on screen again. +/// Only the overlay's flag is set. The game's frame is left alone so that a present made +/// for a document costs the overlay's draw calls and not the frame's pixels; both resize +/// paths mark the frame themselves, because a new target needs the frame uploaded again. +/// +static void Mark_Overlay_Dirty(void) +{ + _OverlayIsDirty = true; +} + + +/// +/// Points the context at where the frame lands and at the scale it is drawn. +/// One authored density-independent pixel is one game logical unit, so a document authored +/// at a legacy dialog's size keeps that size on screen while its text is rasterized at the +/// physical resolution. +/// +static void Apply_Scale_Info(void) +{ + if (_Context == nullptr) { + return; + } + + VideoScaleInfo const & scale = Video_Get_Scale_Info(); + if (scale.DestWidth <= 0 || scale.DestHeight <= 0) { + return; + } + + _Context->SetDimensions(Rml::Vector2i(scale.DestWidth, scale.DestHeight)); + _Context->SetDensityIndependentPixelRatio(std::min(scale.ScaleX, scale.ScaleY)); + Mark_Overlay_Dirty(); +} + + +/// +/// Starts the shell on the running renderer. +/// +/// bool; Is the shell ready? A false return leaves the game running without one. +bool UI_Init(void) +{ + if (_Initialized) { + return(true); + } + + if (!UI_Render_Init()) { + DebugString("[UI] The overlay renderer could not be started.\n"); + return(false); + } + + Rml::SetSystemInterface(UI_System_Interface()); + Rml::SetFileInterface(UI_File_Interface()); + Rml::SetRenderInterface(UI_Render_Interface()); + + // One font engine serves the whole process. This one answers for the game's own bitmap + // sheets and hands every other family to RmlUi's FreeType engine unchanged. + Rml::SetFontEngineInterface(UI_Font_Interface()); + + if (!Rml::Initialise()) { + DebugString("[UI] RmlUi could not be started.\n"); + UI_Render_Shutdown(); + return(false); + } + + VideoScaleInfo const & scale = Video_Get_Scale_Info(); + _Context = Rml::CreateContext("game", + Rml::Vector2i(std::max(scale.DestWidth, 1), std::max(scale.DestHeight, 1))); + + if (_Context == nullptr) { + DebugString("[UI] The RmlUi context could not be created.\n"); + Rml::Shutdown(); + UI_Render_Shutdown(); + return(false); + } + + // The shipped font loads by bare name, so it is found in ui/ or in a mix on the same + // terms as everything else a document names. + if (!Rml::LoadFontFace("LatoLatin-Regular.ttf")) { + DebugString("[UI] The shipped font could not be loaded.\n"); + } + + UI_Surface_Element_Init(); + UI_Dev_Init(); + + Apply_Scale_Info(); + _LastTickTime = Host_Milliseconds(); + _Initialized = true; + + DebugString("[UI] Shell started at %dx%d.\n", scale.DestWidth, scale.DestHeight); + return(true); +} + + +void UI_Shutdown(void) +{ + if (!_Initialized) { + return; + } + + _Changing = true; + +#ifndef NDEBUG + _TestDocument = nullptr; +#endif + + UI_Dev_Shutdown(); + UI_Surface_Element_Shutdown(); + + _Context = nullptr; + Rml::Shutdown(); + UI_Font_Shutdown(); + UI_Render_Shutdown(); + + _Initialized = false; + _OverlayIsDirty = false; + _CaptureButton = -1; + _Changing = false; +} + + +void UI_On_Resize(void) +{ + if (!_Initialized) { + return; + } + + Apply_Scale_Info(); +} + + +/// +/// Advances layout and animation for documents no modal loop is running. +/// Called from Main_Loop beside Map.Input, on wall-clock time, so nothing here reads or +/// advances a deterministic game timer. +/// +void UI_Tick(void) +{ + + // The service points nest: a dialog driver's Call_Back runs inside a loop that already + // ticked. A nested request is dropped rather than updating the context twice, which is + // what keeps a pump reached from inside an update out of it. + static bool ticking = false; + + if (!_Initialized || _Context == nullptr || _Changing || ticking || _RunningModal > 0) { + return; + } + + ticking = true; + + unsigned int const now = Host_Milliseconds(); + double const elapsed = (double)(now - _LastTickTime) / 1000.0; + _LastTickTime = now; + + _Context->Update(); + UI_Message_Box_Service(); + + // RmlUi cannot say whether it needs redrawing, so anything on screen marks the overlay + // on every tick and the present pacing caps the rate. + if (_Context->GetNumDocuments() > 0 || UI_Dev_Is_Open()) { + Mark_Overlay_Dirty(); + } + + if (UI_Dev_Is_Open()) { + VideoScaleInfo const & scale = Video_Get_Scale_Info(); + UI_Dev_New_Frame(scale.DestWidth, scale.DestHeight, elapsed); + } + + ticking = false; +} + + +void UI_Render_Overlay(void) +{ + if (!_Initialized || _Context == nullptr) { + return; + } + + VideoScaleInfo const & scale = Video_Get_Scale_Info(); + UI_Render_Begin(scale.DestX, scale.DestY, scale.DestWidth, scale.DestHeight); + + _Context->Render(); + UI_Dev_Render(); + + UI_Render_End(); + _OverlayIsDirty = false; +} + + +bool UI_Overlay_Is_Dirty(void) +{ + return(_OverlayIsDirty); +} + + +/// +/// Lays out and presents what the shell draws. +/// A screen that has no loop of its own -- the wait box, the progress box -- is on screen +/// only when something else pumps, so this is the equivalent of the synchronous WM_PAINT +/// those boxes were repainted with. +/// +/// Present whether or not the pacing is ready for another frame. +/// A box that must be seen before a long operation begins gets no second chance. +void UI_Paint_Now(bool immediate) +{ + UI_Tick(); + + if (immediate) { + Video_Present(); + } else { + Video_Present_If_Dirty(); + } +} + + +#ifndef NDEBUG +/// +/// Shows or hides the document that proves the shell renders, clips and takes input. +/// Debug builds only; it exists to be looked at, not to be shipped. +/// +static void Toggle_Test_Document(void) +{ + if (_Context == nullptr) { + return; + } + + _Changing = true; + + if (_TestDocument != nullptr) { + _TestDocument->Close(); + _TestDocument = nullptr; + DebugString("[UI] Test document closed.\n"); + } else { + _TestDocument = _Context->LoadDocument("uitest.rml"); + if (_TestDocument != nullptr) { + _TestDocument->Show(); + DebugString("[UI] Test document shown.\n"); + } else { + DebugString("[UI] Test document uitest.rml could not be loaded.\n"); + } + } + + _Changing = false; + + // Closing marks the overlay too, so the pixels a hidden document left behind go away. + Mark_Overlay_Dirty(); +} + + +/// +/// Answers the developer keys the shell owns. +/// +/// bool; Was the key one of them? +static bool Handle_Developer_Key(WPARAM key) +{ + if ((GetKeyState(VK_CONTROL) & 0x8000) == 0 || (GetKeyState(VK_SHIFT) & 0x8000) == 0) { + return(false); + } + + switch (key) { + case 'U': + Toggle_Test_Document(); + return(true); + + case 'I': + UI_Dev_Toggle(); + Mark_Overlay_Dirty(); + return(true); + + default: + return(false); + } +} +#endif + + +/// +/// Opens an exclusive input scope for a modal document. +/// The keyboard queue is cleared so a key pressed before the screen opened cannot be read +/// by whatever runs underneath it, and the screen is marked changing first so that the +/// message pump inside Keyboard->Clear() cannot re-enter it. +/// +static void Enter_Modal_Scope(void) +{ + bool const changing = _Changing; + _Changing = true; + + _ModalDepth++; + + if (Keyboard != nullptr) { + Keyboard->Clear(); + } + + _Changing = changing; +} + + +/// +/// Closes the input scope a modal document opened, dropping any capture it still holds. +/// +static void Leave_Modal_Scope(void) +{ + if (_ModalDepth <= 0) { + return; + } + + bool const changing = _Changing; + _Changing = true; + + _ModalDepth--; + + if (_CaptureButton != -1) { + _CaptureButton = -1; + if (GetCapture() == MainWindow) { + ReleaseCapture(); + } + } + + if (Keyboard != nullptr) { + Keyboard->Clear(); + } + + _Changing = changing; +} + + +/// +/// Offers a window message to the toolkits before the game sees it. +/// The order follows docs/UI_DESIGN.md: ImGui's capture flags first, then a modal +/// document, then whatever an element under the cursor claims. A modal document takes +/// every mouse and key message, which is what IgnoreInput does around a legacy dialog. +/// With none shown a mouse move is always delivered and never consumed, so the game keeps +/// tracking the cursor underneath. +/// +/// bool; Was the message consumed? The window procedure returns without handling +/// it when so, which is what keeps it out of the keyboard queue. +bool UI_Handle_Window_Message(HWND window, UINT message, WPARAM wparam, LPARAM lparam) +{ + if (!_Initialized || _Context == nullptr || _Changing || window != MainWindow) { + return(false); + } + + int const modifiers = Key_Modifiers(); + + switch (message) { + case WM_MOUSEMOVE: { + POINT const point = Message_Point(message, lparam); + UI_Dev_Mouse_Position((float)point.x, (float)point.y); + _Context->ProcessMouseMove((int)point.x, (int)point.y, modifiers); + return(_ModalDepth > 0); + } + + case WM_LBUTTONDOWN: + case WM_LBUTTONDBLCLK: + case WM_RBUTTONDOWN: + case WM_RBUTTONDBLCLK: + case WM_MBUTTONDOWN: + case WM_MBUTTONDBLCLK: { + int const button = (message == WM_LBUTTONDOWN || message == WM_LBUTTONDBLCLK) ? 0 + : ((message == WM_RBUTTONDOWN || message == WM_RBUTTONDBLCLK) ? 1 : 2); + + POINT const point = Message_Point(message, lparam); + _Context->ProcessMouseMove((int)point.x, (int)point.y, modifiers); + UI_Dev_Mouse_Position((float)point.x, (float)point.y); + UI_Dev_Mouse_Button(button, true); + + if (UI_Dev_Wants_Mouse()) { + return(true); + } + + // A false return means the press reached an element, so the game must not see + // it. The press then owns its release wherever the cursor ends up. + bool const consumed = !_Context->ProcessMouseButtonDown(button, modifiers) || _ModalDepth > 0; + if (consumed) { + _CaptureButton = button; + SetCapture(MainWindow); + } + return(consumed); + } + + case WM_LBUTTONUP: + case WM_RBUTTONUP: + case WM_MBUTTONUP: { + int const button = (message == WM_LBUTTONUP) ? 0 : ((message == WM_RBUTTONUP) ? 1 : 2); + + POINT const point = Message_Point(message, lparam); + _Context->ProcessMouseMove((int)point.x, (int)point.y, modifiers); + UI_Dev_Mouse_Position((float)point.x, (float)point.y); + UI_Dev_Mouse_Button(button, false); + + bool const owned = (_CaptureButton == button); + bool const consumed = !_Context->ProcessMouseButtonUp(button, modifiers); + + if (owned) { + _CaptureButton = -1; + if (GetCapture() == MainWindow) { + ReleaseCapture(); + } + return(true); + } + + return(_ModalDepth > 0 || UI_Dev_Wants_Mouse() || consumed); + } + + case WM_MOUSEWHEEL: { + POINT const point = Message_Point(message, lparam); + float const notches = (float)GET_WHEEL_DELTA_WPARAM(wparam) / (float)WHEEL_DELTA; + + _Context->ProcessMouseMove((int)point.x, (int)point.y, modifiers); + UI_Dev_Mouse_Wheel(notches); + + if (UI_Dev_Wants_Mouse()) { + return(true); + } + + return(_ModalDepth > 0 || !_Context->ProcessMouseWheel(-notches, modifiers)); + } + + case WM_KEYDOWN: + case WM_SYSKEYDOWN: { +#ifndef NDEBUG + if (Handle_Developer_Key(wparam)) { + return(true); + } +#endif + if (UI_Dev_Wants_Keyboard()) { + return(true); + } + + Rml::Input::KeyIdentifier const identifier = Key_Identifier(wparam); + if (identifier == Rml::Input::KI_UNKNOWN) { + return(_ModalDepth > 0); + } + + return(_ModalDepth > 0 || !_Context->ProcessKeyDown(identifier, modifiers)); + } + + case WM_KEYUP: + case WM_SYSKEYUP: { + Rml::Input::KeyIdentifier const identifier = Key_Identifier(wparam); + if (identifier == Rml::Input::KI_UNKNOWN) { + return(_ModalDepth > 0); + } + + bool const consumed = !_Context->ProcessKeyUp(identifier, modifiers); + return(_ModalDepth > 0 || UI_Dev_Wants_Keyboard() || consumed); + } + + case WM_CHAR: { + if (UI_Dev_Wants_Keyboard()) { + return(true); + } + + // Consuming the physical key never suppresses the text it generated, so this + // is decided on its own. + if (wparam < 32) { + return(_ModalDepth > 0); + } + + return(_ModalDepth > 0 || !_Context->ProcessTextInput((Rml::Character)wparam)); + } + + default: + return(false); + } +} + + +//--------------------------------------------------------------------------------------- +// The RmlUi view base. It lives here rather than in a file of its own because the context +// it attaches to is the shell's. +//--------------------------------------------------------------------------------------- + +UIRmlViewClass::UIRmlViewClass(UIPresenterClass & presenter, char const * document) : + Presenter(presenter), + Document(document != nullptr ? document : ""), + ModelName(Document) +{ + Rml::String::size_type const dot = ModelName.rfind('.'); + if (dot != Rml::String::npos) { + ModelName.erase(dot); + } +} + + +UIRmlViewClass::~UIRmlViewClass(void) +{ + Close(); +} + + +/// +/// Loads the document, binds its data model and shows it. +/// Preparation completes before the view becomes interactive, so a failure here leaves +/// nothing shown and the caller opens the legacy view instead. +/// +/// Should the document take focus away from every other one? +/// bool; Is the view ready to be interacted with? +bool UIRmlViewClass::Prepare(bool modal) +{ + if (_Context == nullptr || Element != nullptr) { + return(false); + } + + Rml::DataModelConstructor constructor = _Context->CreateDataModel(ModelName); + if (!constructor) { + DebugString("[UI] The data model for %s could not be created.\n", Document.c_str()); + return(false); + } + + Bind(constructor); + Model = constructor.GetModelHandle(); + + Element = _Context->LoadDocument(Document); + if (Element == nullptr) { + _Context->RemoveDataModel(ModelName); + DebugString("[UI] The document %s could not be loaded.\n", Document.c_str()); + return(false); + } + + Element->Show(modal ? Rml::ModalFlag::Modal : Rml::ModalFlag::None); + + Release_Pointer_To_Host(); + + if (modal) { + IsModal = true; + Enter_Modal_Scope(); + } + + Mark_Overlay_Dirty(); + return(true); +} + + +/// +/// Takes the document off the screen without releasing it. +/// A screen this one opens draws where this one is, and the coexistence rule in +/// docs/UI_DESIGN.md wants only one presentation over a region; the modal scope goes with +/// it, so the screen underneath takes input while it is away. +/// +void UIRmlViewClass::Hide(void) +{ + if (Element == nullptr || !Element->IsVisible()) { + return; + } + + Element->Hide(); + + Recapture_Pointer(); + + if (IsModal) { + Leave_Modal_Scope(); + } + + Mark_Overlay_Dirty(); +} + + +/// +/// Puts the document back on the screen with the scope it had. +/// +void UIRmlViewClass::Show(void) +{ + if (Element == nullptr || Element->IsVisible()) { + return; + } + + Element->Show(IsModal ? Rml::ModalFlag::Modal : Rml::ModalFlag::None); + + Release_Pointer_To_Host(); + + if (IsModal) { + Enter_Modal_Scope(); + } + + Mark_Overlay_Dirty(); +} + + +void UIRmlViewClass::Close(void) +{ + if (_Context == nullptr || Element == nullptr) { + return; + } + + // The order docs/UI_DESIGN.md sets out: mark the screen closing and discard its + // intents, then drop focus and capture, then release the document while the storage its + // data model reads still lives, and only then clear the keyboard queue. + Presenter.IsClosing = true; + Presenter.Discard(); + + bool const wasvisible = Element->IsVisible(); + + Element->Close(); + Element = nullptr; + + if (wasvisible) { + Recapture_Pointer(); + } + + _Context->RemoveDataModel(ModelName); + Model = Rml::DataModelHandle(); + + if (IsModal) { + IsModal = false; + Leave_Modal_Scope(); + } + + Mark_Overlay_Dirty(); +} + + +bool UIRmlViewClass::Is_Visible(void) const +{ + return(Element != nullptr && Element->IsVisible()); +} + + +/// +/// Runs a screen to a result, the way OwnerDraw::Dialog_Message_Handler runs a dialog. +/// The loop keeps the shape the legacy drivers have: it pumps messages, then steps the +/// game where a network session needs stepping and calls back where it does not, and only +/// then updates the toolkit and executes what the screen's events queued. An event handler +/// never acts directly, so a nested screen starts from the queue one level up. +/// +/// The screen to run. It carries the result it produces. +/// The view bound to it, updated after each pass. +/// The screen's result. GameEnded carries what the legacy driver returns when +/// the session ended underneath it. +UIResult UI_Run_Modal(UIPresenterClass & presenter, UIRmlViewClass & view) +{ + UIResult result; + result.Outcome = UIResult::OUTCOME_CANCELLED; + + if (!_Initialized || _Context == nullptr) { + result.Outcome = UIResult::OUTCOME_FAILED_TO_OPEN; + return(result); + } + + // Main_Loop can reach a screen of its own, and the guard is the one + // OwnerDraw::Dialog_Message_Handler keeps for the same reason: the inner driver services + // the game with a callback rather than stepping it twice. + static bool inmainloop = false; + + _RunningModal++; + + while (!presenter.Result.has_value() && !presenter.Suspends()) { + Windows_Message_Handler(); + + if (Session.Type != GAME_NORMAL && Session.Type != GAME_SKIRMISH && !Session.NetOpen && !Session.Suspended) { + if (!inmainloop) { + inmainloop = true; + bool const ended = Main_Loop(); + inmainloop = false; + + if (ended) { + _RunningModal--; + result.Outcome = UIResult::OUTCOME_SESSION_ENDED; + result.GameEnded = true; + return(result); + } + } + } else { + Call_Back(); + } + + presenter.Service(); + + _Context->Update(); + presenter.Drain(); + view.Sync(); + UI_Message_Box_Service(); + + Mark_Overlay_Dirty(); + Video_Present_If_Dirty(); + } + + _RunningModal--; + + // A screen that asked to be stepped aside has no result yet; its owner runs the screen + // it opened and calls back in. + if (!presenter.Result.has_value()) { + return(result); + } + + return(presenter.Result.value()); +} diff --git a/code/ui/uishell.h b/code/ui/uishell.h new file mode 100644 index 000000000..9b534384f --- /dev/null +++ b/code/ui/uishell.h @@ -0,0 +1,48 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The UI shell's interface to the rest of the engine. No RmlUi, ImGui or bgfx type +// appears here, the way no bgfx type appears in bgfxbackend.h, so the engine reaches the +// shell without carrying any of those libraries' headers or build settings. +// +// docs/UI_DESIGN.md owns the architecture this belongs to. + +#pragma once + +#include + + +// Starts and stops the shell. The renderer must already be running, and the shell is torn +// down before it stops. +bool UI_Init(void); +void UI_Shutdown(void); + +// Called after the frame's size or the drawable area's size changed, so the context, the +// coordinate mapping and the clipping move together with the frame. +void UI_On_Resize(void); + +// Advances animation and layout for documents that are not being run by a modal loop. +void UI_Tick(void); + +// Draws the overlays between the frame and the flip. Only video.cpp calls this. +void UI_Render_Overlay(void); + +// Offers a window message to the toolkits before the game sees it. A true return means the +// message was consumed and the window procedure returns without handling it. +bool UI_Handle_Window_Message(HWND window, UINT message, WPARAM wparam, LPARAM lparam); + +// Does anything the shell draws need putting on screen again? True keeps a present +// happening when the game's own frame has not changed. +bool UI_Overlay_Is_Dirty(void); + +// Is an overlay document on screen? The coexistence rule in docs/UI_DESIGN.md forbids +// showing a legacy dialog while one is. + +// Should a migrated screen use its RmlUi view rather than its legacy one? No screen has +// migrated yet, so this answers false until one has. diff --git a/code/ui/uiskirmish.cpp b/code/ui/uiskirmish.cpp new file mode 100644 index 000000000..7502697f8 --- /dev/null +++ b/code/ui/uiskirmish.cpp @@ -0,0 +1,708 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The skirmish setup screen. Behavior traced out of Skirmish_On_WM_INITDIALOG, +// Skirmish_On_WM_COMMAND and Skirmish_Mode_Dialog in skirmish.cpp. +// +// What the extraction fixes in place: a side row carries the country it stands for rather +// than its position, because the list holds only the countries that may be played; the map +// the player asked for is checked for enough start positions when the accept button is +// pressed, and a map with too few leaves the screen standing; Short Game turns Bases on and +// turning Bases off turns Short Game off; and backing out still records the name, side and +// color, which is what the cancel arm read before it answered. +// +// docs/UI_DESIGN.md, "Screens", owns the contracts this keeps to. + +#include "always.h" + +#include "uiskirmish.h" + +#include "uiscenariopick.h" + +#include "uimappreview.h" +#include "uirmlview.h" + +#include "_rules.h" +#include "data.h" +#include "houstype.h" +#include "init.h" +#include "language/language.h" +#include "msgbox.h" +#include "netdlg2.h" +#include "goptions.h" +#include "mapgen.h" +#include "mplayer.h" +#include "netshare.h" +#include "preview.h" +#include "rules.h" +#include "session.h" + +#include +#include +#include +#include + +#include +#include + + +// The least money a skirmish may be started with, which is where the credits track bar +// begins. +enum { MP_MIN_MONEY = 2500 }; + + +UISkirmishPresenterClass::UISkirmishPresenterClass(void) : + Preview(UI_SKIRMISH_PREVIEW_SURFACE) +{ +} + + +void UISkirmishPresenterClass::Refresh(void) +{ + Handle = Session.Handle; + + Sides.clear(); + SelectedSide = 0; + for (int index = 0; index < HouseTypes.Count(); index++) { + HouseTypeClass const * const house = HouseTypes[index]; + if (!house->IsMultiplay) continue; + + if (index == Session.House) { + SelectedSide = (int)Sides.size(); + } + Sides.push_back(SideType{(char const *)house->GivenName, index}); + } + + Colors.clear(); + Colors.push_back(Fetch_String(TXT_GOLD)); + Colors.push_back(Fetch_String(TXT_RED)); + Colors.push_back(Fetch_String(TXT_BLUE)); + Colors.push_back(Fetch_String(TXT_GREEN)); + Colors.push_back(Fetch_String(TXT_ORANGE)); + Colors.push_back(Fetch_String(TXT_SKY_BLUE)); + Colors.push_back(Fetch_String(TXT_PURPLE)); + Colors.push_back(Fetch_String(TXT_PINK)); + SelectedColor = Session.PrefColor; + + UnitCount = SliderType{Session.Options.UnitCount, SessionClass::CountMin[1], SessionClass::CountMax[1], 1}; + Credits = SliderType{Session.Options.Credits, MP_MIN_MONEY, Rule->MPMaxMoney, 250}; + TechLevel = SliderType{BuildLevel, 1, MPLAYER_BUILD_LEVEL_MAX, 1}; + AILevel = SliderType{(int)Session.Options.AIDifficulty, 0, 2, 1}; + AIPlayers = SliderType{Session.Options.AIPlayers > 1 ? Session.Options.AIPlayers : 1, 1, 7, 1}; + + // The track bar runs the other way round from the setting: its left end is the slowest + // game, and the dialog turned one into the other at both ends. + GameSpeed = SliderType{6 - Session.Options.GameSpeed, 0, 6, 1}; + + Bases = Session.Options.Bases; + Crates = Session.Options.Goodies; + FogOfWar = Session.Options.FogOfWar; + Bridges = Session.Options.BridgeDestruction; + MCVRedeploy = Session.Options.MCVRedeploy; + ShortGame = Session.Options.ShortGame; + MultiEngineer = Session.Options.CrapEngineers; + + // The screen opens on the first scenario whatever the session was carrying. + Set_Scenario_Info_From_Index(0); + Session.Options.ScenarioIndex = 0; + ScenarioName = Session.Options.ScenarioDescription; + + Clear_Vector(&Session.Players); + Clear_Vector(&Session.Computers); + + Rebuild_Network_Map_Preview(); + PreviewGeneration++; + + CanAccept = true; + ListChanged = true; +} + + +/// +/// The maintenance the dialog driver ran on every pass of its own loop. +/// +void UISkirmishPresenterClass::Service(void) +{ + Title_Screen_Restore(); +} + + +/// +/// Records the name, side and color the player is showing. +/// Both leaving and backing out read these, because they are the player's own preferences +/// rather than the game's settings. +/// +void UISkirmishPresenterClass::Read_Identity(void) +{ + std::snprintf(Session.Handle, sizeof(Session.Handle), "%s", Handle.c_str()); + + if (SelectedSide >= 0 && SelectedSide < (int)Sides.size()) { + Session.House = (HousesType)Sides[SelectedSide].Country; + } else { + Session.House = (HousesType)HOUSE_FIRST; + } + + Session.ColorIdx = SelectedColor; + Session.PrefColor = SelectedColor; +} + + +/// +/// Writes the player's multiplayer preferences, and drops the preview. +/// +void UISkirmishPresenterClass::End(void) +{ + if (MultiplayerMapPreview != NULL) { + delete MultiplayerMapPreview; + MultiplayerMapPreview = NULL; + } + + Session.Write_MultiPlayer_Settings(); +} + + +/// +/// Runs the map selection screen with this one already out of the way. +/// Backing out of it leaves the scenario this screen was showing, which is what putting the +/// old index back did. +/// +void UISkirmishPresenterClass::Run_Pending(void) +{ + if (Pending != SUB_PICK_MAP) { + return; + } + + Pending = SUB_NONE; + + int const previous = Session.Options.ScenarioIndex; + + bool const picked = Pick_Scenario_Screen(); + + if (picked && Set_Scenario_Info_From_Index(Session.Options.ScenarioIndex) == true) { + ScenarioName = Session.Options.ScenarioDescription; + } else { + Session.Options.ScenarioIndex = previous; + Set_Scenario_Info_From_Index(previous); + } + + // A generated map has a picture of its own beside it rather than one read out of the map. + int const index = Session.Options.ScenarioIndex; + if (index >= 0 && index < Session.Scenarios.Count() + && stricmp(Session.Scenarios[index]->Get_Filename(), RANDOM_MAP_FILE_NAME) == 0) { + delete MultiplayerMapPreview; + MultiplayerMapPreview = new MapPreviewClass; + MultiplayerMapPreview->Read_PCX_Preview("RandMap.img"); + if (MultiplayerMapPreview->Get_Preview_Surface() == NULL) { + Rebuild_Network_Map_Preview(); + } + } else { + Rebuild_Network_Map_Preview(); + } + + PreviewGeneration++; +} + + +/// +/// Starts the game the player set up, if the map has room for it. +/// The start position count is checked here rather than when the slider moved, because the +/// map can change after it did. +/// +void UISkirmishPresenterClass::Accept(void) +{ + CanAccept = false; + + int const waypoints = RandomMapWaypointCount(Session.Options.ScenarioIndex); + if (waypoints < AIPlayers.Value + 1) { + char buffer[256]; + std::snprintf(buffer, sizeof(buffer), Fetch_String(TXT_SCENARIO_TOO_SMALL), waypoints); + WWMessageBox().Process(buffer, TXT_OK); + CanAccept = true; + return; + } + + Read_Identity(); + + Session.Options.UnitCount = UnitCount.Value; + BuildLevel = TechLevel.Value; + Session.Options.Credits = Credits.Value; + Session.Options.AIDifficulty = (DiffType)AILevel.Value; + Session.Options.AIPlayers = AIPlayers.Value; + Session.Options.GameSpeed = 6 - GameSpeed.Value; + Options.GameSpeed = Session.Options.GameSpeed; + + NodeNameType * const who = new NodeNameType; + if (who != NULL) { + strcpy(who->Name, Session.Handle); + who->Player.House = Session.House; + who->Player.Color = Session.ColorIdx; + who->Player.ProcessTime = -1; + Session.Players.Add(who); + } + + Session.Options.Bases = Bases; + Session.Options.Goodies = Crates; + Session.Options.FogOfWar = FogOfWar; + Session.Options.BridgeDestruction = Bridges; + Session.Options.MCVRedeploy = MCVRedeploy; + Session.Options.ShortGame = ShortGame; + Session.Options.HarvTruce = false; + Session.Options.CrapEngineers = MultiEngineer; + + if (MultiplayerMapPreview != NULL) { + delete MultiplayerMapPreview; + MultiplayerMapPreview = NULL; + } + + Outcome = true; + + UIResult result; + result.Outcome = UIResult::OUTCOME_ACCEPTED; + Result = result; +} + + +void UISkirmishPresenterClass::Execute(UIIntent const & intent) +{ + if (intent.Action == UI_SKIRMISH_HANDLE) { + Handle = intent.Identity; + if (Handle.size() > HANDLE_LIMIT) { + Handle.resize(HANDLE_LIMIT); + } + return; + } + + if (intent.Action == UI_SKIRMISH_SIDE) { + if (intent.Value >= 0 && intent.Value < (int)Sides.size()) { + SelectedSide = intent.Value; + } + return; + } + + if (intent.Action == UI_SKIRMISH_COLOR) { + if (intent.Value >= 0 && intent.Value < (int)Colors.size()) { + SelectedColor = intent.Value; + } + return; + } + + if (intent.Action == UI_SKIRMISH_SLIDER) { + SliderType * slider = NULL; + if (intent.Identity == UI_SKIRMISH_UNITCOUNT) slider = &UnitCount; + else if (intent.Identity == UI_SKIRMISH_CREDITS) slider = &Credits; + else if (intent.Identity == UI_SKIRMISH_TECHLEVEL) slider = &TechLevel; + else if (intent.Identity == UI_SKIRMISH_AILEVEL) slider = &AILevel; + else if (intent.Identity == UI_SKIRMISH_AIPLAYERS) slider = &AIPlayers; + else if (intent.Identity == UI_SKIRMISH_GAMESPEED) slider = &GameSpeed; + + if (slider != NULL) { + int value = intent.Value; + if (value < slider->Minimum) value = slider->Minimum; + if (value > slider->Maximum) value = slider->Maximum; + slider->Value = value; + } + return; + } + + if (intent.Action == UI_SKIRMISH_TOGGLE) { + if (intent.Identity == UI_SKIRMISH_BASES) { + Bases = !Bases; + // A short game is decided by what a player still holds, so it needs bases. + if (!Bases) ShortGame = false; + } else if (intent.Identity == UI_SKIRMISH_SHORTGAME) { + ShortGame = !ShortGame; + if (ShortGame) Bases = true; + } else if (intent.Identity == UI_SKIRMISH_CRATES) { + Crates = !Crates; + } else if (intent.Identity == UI_SKIRMISH_FOG) { + FogOfWar = !FogOfWar; + } else if (intent.Identity == UI_SKIRMISH_BRIDGES) { + Bridges = !Bridges; + } else if (intent.Identity == UI_SKIRMISH_MCV) { + MCVRedeploy = !MCVRedeploy; + } else if (intent.Identity == UI_SKIRMISH_ENGINEER) { + MultiEngineer = !MultiEngineer; + } + return; + } + + if (intent.Action == UI_SKIRMISH_PICK_MAP) { + Pending = SUB_PICK_MAP; + return; + } + + if (intent.Action == UI_SKIRMISH_ACCEPT) { + Accept(); + return; + } + + if (intent.Action == UI_SKIRMISH_CANCEL) { + Read_Identity(); + Outcome = false; + + UIResult result; + result.Outcome = UIResult::OUTCOME_CANCELLED; + Result = result; + return; + } +} + + +//--------------------------------------------------------------------------------------- +// The RmlUi view. +//--------------------------------------------------------------------------------------- + +// The picture the preview frame holds, in game logical units. The frame is the template's +// 215 x 106 dialog units, which is 322.5 by 172.25 at the family's 1.5 across and 1.625 +// down, and the picture sits inside its one pixel border. +enum { PREVIEW_WIDTH = 320, PREVIEW_HEIGHT = 170 }; + + +/// +/// The RmlUi half of the skirmish setup screen. +/// +class SkirmishViewClass : public UIRmlViewClass +{ + public: + // A color the player may take, with the swatch the owner-draw combo drew its row in. + struct ColorRowType + { + std::string Name; + std::string Hex; + }; + + SkirmishViewClass(UISkirmishPresenterClass & presenter) : + UIRmlViewClass(presenter, "skirmish.rml"), + Screen(presenter) + { + UI_Register_Surface(Screen.Preview.c_str(), &Picture); + } + + virtual ~SkirmishViewClass(void) override + { + UI_Unregister_Surface(Screen.Preview.c_str()); + } + + virtual void Bind(Rml::DataModelConstructor & model) override; + virtual void Sync(void) override; + + // Puts the track bar ranges the rules give this screen on the controls, and lets + // the change handlers start reporting. A range is set before the data binding fills + // a value in, so a value outside a track bar's default range is not clamped away. + void Settle(void); + + private: + void Move(char const * which, int value); + void Press(char const * action); + void Set_Range(char const * id, UISkirmishPresenterClass::SliderType const & slider); + + std::string Field_Text(void) const; + Rml::ElementFormControlInput * Field(void) const; + + UISkirmishPresenterClass & Screen; + + // The view owns the pixels; the presenter carries only the name they answer to. + MapPreviewSurfaceClass Picture{PREVIEW_WIDTH, PREVIEW_HEIGHT}; + + std::vector ColorRows; + + unsigned int Drawn = 0; + bool Settled = false; +}; + + +Rml::ElementFormControlInput * SkirmishViewClass::Field(void) const +{ + if (Element == nullptr) { + return(nullptr); + } + return(rmlui_dynamic_cast(Element->GetElementById("name"))); +} + + +std::string SkirmishViewClass::Field_Text(void) const +{ + Rml::ElementFormControlInput * const field = Field(); + if (field == nullptr) { + return(Screen.Handle); + } + return(field->GetValue()); +} + + +void SkirmishViewClass::Set_Range(char const * id, UISkirmishPresenterClass::SliderType const & slider) +{ + if (Element == nullptr) { + return; + } + + Rml::Element * const control = Element->GetElementById(id); + if (control == nullptr) { + return; + } + + // The range is set before the value, because a track bar clamps a value into the range + // it is holding and the default range stops well short of what the rules allow. + control->SetAttribute("min", slider.Minimum); + control->SetAttribute("max", slider.Maximum); + control->SetAttribute("step", slider.Step); + control->SetAttribute("value", slider.Value); +} + + +void SkirmishViewClass::Settle(void) +{ + Set_Range("unitcount", Screen.UnitCount); + Set_Range("credits", Screen.Credits); + Set_Range("techlevel", Screen.TechLevel); + Set_Range("ailevel", Screen.AILevel); + Set_Range("aiplayers", Screen.AIPlayers); + Set_Range("gamespeed", Screen.GameSpeed); + + Settled = true; +} + + +/// +/// Queues a track bar's new position, dropping one that matches what the model already +/// holds so that setting a control from the model is not read back as a move. +/// +void SkirmishViewClass::Move(char const * which, int value) +{ + if (!Settled) return; + + UISkirmishPresenterClass::SliderType const * held = NULL; + if (which == UI_SKIRMISH_UNITCOUNT) held = &Screen.UnitCount; + else if (which == UI_SKIRMISH_CREDITS) held = &Screen.Credits; + else if (which == UI_SKIRMISH_TECHLEVEL) held = &Screen.TechLevel; + else if (which == UI_SKIRMISH_AILEVEL) held = &Screen.AILevel; + else if (which == UI_SKIRMISH_AIPLAYERS) held = &Screen.AIPlayers; + else if (which == UI_SKIRMISH_GAMESPEED) held = &Screen.GameSpeed; + + if (held == NULL || held->Value == value) { + return; + } + + Screen.Queue(UIIntent{UI_SKIRMISH_SLIDER, which, value}); +} + + +/// +/// Queues what a button or its key stands for. +/// The name is read out of the field here rather than tracked, because that is when the +/// dialog read its edit control, and the read is queued ahead of the action it is read for +/// so the two execute in that order. Backing out records the name too, which is what the +/// cancel arm did. +/// +void SkirmishViewClass::Press(char const * action) +{ + if (action == UI_SKIRMISH_ACCEPT || action == UI_SKIRMISH_CANCEL) { + std::string const text = Field_Text(); + if (text != Screen.Handle) { + Screen.Queue(UIIntent{UI_SKIRMISH_HANDLE, text, 0}); + } + } + + Screen.Queue(UIIntent{action, "", 0}); +} + + +void SkirmishViewClass::Bind(Rml::DataModelConstructor & model) +{ + ColorRows.clear(); + for (int index = 0; index < (int)Screen.Colors.size(); index++) { + char hex[8]; + if (index < MAX_PLAYERS) { + // A COLORREF holds its blue byte highest, which is the order RGB() packs. + unsigned long const color = (unsigned long)PlayerColorTable[index]; + std::snprintf(hex, sizeof(hex), "#%02x%02x%02x", + (unsigned)(color & 0xFF), (unsigned)((color >> 8) & 0xFF), (unsigned)((color >> 16) & 0xFF)); + } else { + std::snprintf(hex, sizeof(hex), "#b9bcae"); + } + ColorRows.push_back(ColorRowType{Screen.Colors[index], hex}); + } + + if (auto side = model.RegisterStruct()) { + side.RegisterMember("name", &UISkirmishPresenterClass::SideType::Name); + } + model.RegisterArray>(); + + if (auto swatch = model.RegisterStruct()) { + swatch.RegisterMember("name", &ColorRowType::Name); + swatch.RegisterMember("hex", &ColorRowType::Hex); + } + model.RegisterArray>(); + + if (auto slider = model.RegisterStruct()) { + slider.RegisterMember("value", &UISkirmishPresenterClass::SliderType::Value); + slider.RegisterMember("min", &UISkirmishPresenterClass::SliderType::Minimum); + slider.RegisterMember("max", &UISkirmishPresenterClass::SliderType::Maximum); + slider.RegisterMember("step", &UISkirmishPresenterClass::SliderType::Step); + } + + model.Bind("handle", &Screen.Handle); + model.Bind("sides", &Screen.Sides); + model.Bind("selectedside", &Screen.SelectedSide); + model.Bind("colors", &ColorRows); + model.Bind("selectedcolor", &Screen.SelectedColor); + + model.Bind("unitcount", &Screen.UnitCount); + model.Bind("credits", &Screen.Credits); + model.Bind("techlevel", &Screen.TechLevel); + model.Bind("ailevel", &Screen.AILevel); + model.Bind("aiplayers", &Screen.AIPlayers); + model.Bind("gamespeed", &Screen.GameSpeed); + + model.Bind("bases", &Screen.Bases); + model.Bind("crates", &Screen.Crates); + model.Bind("fog", &Screen.FogOfWar); + model.Bind("bridges", &Screen.Bridges); + model.Bind("mcv", &Screen.MCVRedeploy); + model.Bind("shortgame", &Screen.ShortGame); + model.Bind("engineer", &Screen.MultiEngineer); + + model.Bind("scenarioname", &Screen.ScenarioName); + model.Bind("preview", &Screen.Preview); + model.Bind("canaccept", &Screen.CanAccept); + + // The field is bound one way, so a value the model already holds is never queued back + // as a change the player did not type. + model.BindEventCallback("rename", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + Rml::String const value = event.GetParameter("value", Rml::String()); + if (value == Screen.Handle) return; + Screen.Queue(UIIntent{UI_SKIRMISH_HANDLE, value, 0}); + }); + + model.BindEventCallback("chooseside", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + if (!Settled) return; + int const row = (int)(event.GetParameter("value", 0.0f) + 0.5f); + if (row == Screen.SelectedSide) return; + Screen.Queue(UIIntent{UI_SKIRMISH_SIDE, "", row}); + }); + + model.BindEventCallback("choosecolor", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + if (!Settled) return; + int const row = (int)(event.GetParameter("value", 0.0f) + 0.5f); + if (row == Screen.SelectedColor) return; + Screen.Queue(UIIntent{UI_SKIRMISH_COLOR, "", row}); + }); + + model.BindEventCallback("move", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + + Rml::String const which = arguments[0].Get(); + int const value = (int)(event.GetParameter("value", 0.0f) + 0.5f); + + if (which == UI_SKIRMISH_UNITCOUNT) Move(UI_SKIRMISH_UNITCOUNT, value); + else if (which == UI_SKIRMISH_CREDITS) Move(UI_SKIRMISH_CREDITS, value); + else if (which == UI_SKIRMISH_TECHLEVEL) Move(UI_SKIRMISH_TECHLEVEL, value); + else if (which == UI_SKIRMISH_AILEVEL) Move(UI_SKIRMISH_AILEVEL, value); + else if (which == UI_SKIRMISH_AIPLAYERS) Move(UI_SKIRMISH_AIPLAYERS, value); + else if (which == UI_SKIRMISH_GAMESPEED) Move(UI_SKIRMISH_GAMESPEED, value); + }); + + // A check box is a class plus a click that queues a toggle, not a two-way bound control. + model.BindEventCallback("toggle", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{UI_SKIRMISH_TOGGLE, arguments[0].Get(), 0}); + }); + + model.BindEventCallback("press", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + + Rml::String const action = arguments[0].Get(); + if (action == UI_SKIRMISH_ACCEPT) Press(UI_SKIRMISH_ACCEPT); + else if (action == UI_SKIRMISH_CANCEL) Press(UI_SKIRMISH_CANCEL); + else if (action == UI_SKIRMISH_PICK_MAP) Press(UI_SKIRMISH_PICK_MAP); + }); + + // Escape backs out and Enter starts the game, which is what IsDialogMessage delivered to + // a template that names IDCANCEL and no default push button. + model.BindEventCallback("key", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + int const key = event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN); + if (key == Rml::Input::KI_ESCAPE) { + Press(UI_SKIRMISH_CANCEL); + } else if (key == Rml::Input::KI_RETURN || key == Rml::Input::KI_NUMPADENTER) { + Press(UI_SKIRMISH_ACCEPT); + } + }); +} + + +void SkirmishViewClass::Sync(void) +{ + if (!Model) return; + + // The track bar, combo box and field values are not dirtied, because each already + // carries what its own change event reported. + Model.DirtyVariable("sides"); + Model.DirtyVariable("colors"); + Model.DirtyVariable("bases"); + Model.DirtyVariable("crates"); + Model.DirtyVariable("fog"); + Model.DirtyVariable("bridges"); + Model.DirtyVariable("mcv"); + Model.DirtyVariable("shortgame"); + Model.DirtyVariable("engineer"); + Model.DirtyVariable("scenarioname"); + Model.DirtyVariable("canaccept"); + + // The picture is redrawn where it changed, not every pass, so the element uploads once + // per map rather than once per present. + if (Screen.PreviewGeneration != Drawn) { + Drawn = Screen.PreviewGeneration; + Picture.Redraw(); + } + + Screen.ListChanged = false; +} + + +/// +/// Shows the skirmish screen and waits for the player to leave it. +/// +UIResult UI_Skirmish_Screen(UISkirmishPresenterClass & presenter) +{ + SkirmishViewClass view(presenter); + + if (!view.Prepare(true)) { + UIResult result; + result.Outcome = UIResult::OUTCOME_FAILED_TO_OPEN; + return(result); + } + + view.Settle(); + + // The map selection screen draws where this one is, so the document steps aside for it, + // which is what the dialog's own ShowWindow did. + while (!presenter.Result.has_value()) { + UI_Run_Modal(presenter, view); + + if (presenter.Pending == UISkirmishPresenterClass::SUB_NONE) { + break; + } + + view.Hide(); + presenter.Run_Pending(); + view.Show(); + view.Sync(); + } + + UIResult const result = presenter.Result.value_or(UIResult{}); + view.Close(); + return(result); +} diff --git a/code/ui/uiskirmish.h b/code/ui/uiskirmish.h new file mode 100644 index 000000000..7019c36f6 --- /dev/null +++ b/code/ui/uiskirmish.h @@ -0,0 +1,153 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The skirmish setup screen's behavior, with no toolkit in it. +// +// docs/UI_DESIGN.md, "Screens", owns the contract. + +#pragma once + +#include "uiscreen.h" + +#include +#include + + +// The artwork the map preview draws into. The map selection screen this one opens holds a +// preview of its own at a different size, so the two register under different names. +inline constexpr char const * UI_SKIRMISH_PREVIEW_SURFACE = "skirmishpreview"; + +inline constexpr char const * UI_SKIRMISH_HANDLE = "handle"; // Identity: the name typed +inline constexpr char const * UI_SKIRMISH_SIDE = "side"; // Value: row in Sides +inline constexpr char const * UI_SKIRMISH_COLOR = "color"; // Value: row in Colors +inline constexpr char const * UI_SKIRMISH_SLIDER = "slider"; // Identity: which, Value: position +inline constexpr char const * UI_SKIRMISH_TOGGLE = "toggle"; // Identity: which +inline constexpr char const * UI_SKIRMISH_PICK_MAP = "pickmap"; +inline constexpr char const * UI_SKIRMISH_ACCEPT = "accept"; +inline constexpr char const * UI_SKIRMISH_CANCEL = "cancel"; + +// The sliders and check boxes, named rather than numbered, because an intent carries an +// identity and never a control. +inline constexpr char const * UI_SKIRMISH_UNITCOUNT = "unitcount"; +inline constexpr char const * UI_SKIRMISH_CREDITS = "credits"; +inline constexpr char const * UI_SKIRMISH_TECHLEVEL = "techlevel"; +inline constexpr char const * UI_SKIRMISH_AILEVEL = "ailevel"; +inline constexpr char const * UI_SKIRMISH_AIPLAYERS = "aiplayers"; +inline constexpr char const * UI_SKIRMISH_GAMESPEED = "gamespeed"; + +inline constexpr char const * UI_SKIRMISH_BASES = "bases"; +inline constexpr char const * UI_SKIRMISH_CRATES = "crates"; +inline constexpr char const * UI_SKIRMISH_FOG = "fog"; +inline constexpr char const * UI_SKIRMISH_BRIDGES = "bridges"; +inline constexpr char const * UI_SKIRMISH_MCV = "mcv"; +inline constexpr char const * UI_SKIRMISH_SHORTGAME = "shortgame"; +inline constexpr char const * UI_SKIRMISH_ENGINEER = "engineer"; + + +class UISkirmishPresenterClass : public UIPresenterClass +{ + public: + // A screen this one opens on top of itself. + enum SubScreenType { + SUB_NONE, + SUB_PICK_MAP, + }; + + // A track bar, with the range the dialog gave it. + struct SliderType + { + int Value = 0; + int Minimum = 0; + int Maximum = 0; + + // The amount one move covers. Credits move in steps of 250, which is what + // OD_SETTRACKSTEP set on that control. + int Step = 1; + }; + + // A playable side, carrying the country it stands for rather than its position, + // because the list holds only the countries that may be played. + struct SideType + { + std::string Name; + int Country = 0; + }; + + UISkirmishPresenterClass(void); + + virtual void Execute(UIIntent const & intent) override; + virtual void Refresh(void) override; + virtual void Service(void) override; + virtual bool Suspends(void) const override { return(Pending != SUB_NONE); } + + void Run_Pending(void); + + // Did the player ask for the game to start? + bool Accepted(void) const { return(Outcome); } + + // Writes the player's multiplayer preferences, which the driver did on the way out + // whichever way the screen was left. + void End(void); + + /* + ** The view-model. + */ + std::string Handle; + + std::vector Sides; + int SelectedSide = 0; + + std::vector Colors; + int SelectedColor = 0; + + SliderType UnitCount; + SliderType Credits; + SliderType TechLevel; + SliderType AILevel; + SliderType AIPlayers; + SliderType GameSpeed; + + bool Bases = false; + bool Crates = false; + bool FogOfWar = false; + bool Bridges = false; + bool MCVRedeploy = false; + bool ShortGame = false; + bool MultiEngineer = false; + + std::string ScenarioName; + + // The name of the artwork the map preview draws into, never a surface. + std::string Preview; + + // Moves whenever the preview was rebuilt. + unsigned int PreviewGeneration = 0; + + // The longest handle the name field accepts, in bytes, which is the buffer the + // dialog read the control into. + enum { HANDLE_LIMIT = 19 }; + + // Is the accept button available? The dialog disabled it while it checked whether + // the map has room for the computer players asked for. + bool CanAccept = true; + + SubScreenType Pending = SUB_NONE; + + bool ListChanged = false; + + private: + void Accept(void); + void Read_Identity(void); + + bool Outcome = false; +}; + + +// Shows the skirmish screen through its RmlUi view. +UIResult UI_Skirmish_Screen(UISkirmishPresenterClass & presenter); diff --git a/code/ui/uisound.cpp b/code/ui/uisound.cpp new file mode 100644 index 000000000..bc33608bf --- /dev/null +++ b/code/ui/uisound.cpp @@ -0,0 +1,333 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The sound and music screen's behavior, taken out of Sound_Option_Dialog_Func so that the +// dialog procedure and an RmlUi document can drive the same one. +// +// What the dialog did that is not obvious from the controls, and is kept here: +// the "Music Volume" slider sets Options.ScoreVolume; a volume dragged previews itself and +// the same volume is applied again without a preview when the screen is accepted; shuffle +// and repeat exclude one another, the newly checked one clearing the other; the track list +// holds the themes Theme.Is_Allowed admits, numbered from one in that order, and is built +// once when the screen opens, so a song starting later does not move the selection; and the +// screen has no cancel, because the template names no cancel button and the dialog +// procedure ignored the IDCANCEL that Escape produces. +// +// docs/UI_DESIGN.md, "Screens", owns the contract. + +#include "always.h" + +#include "uisound.h" + +#include "uirmlview.h" + +#include "audio/audioengine.h" +#include "globals.h" +#include "goptions.h" +#include "incdec.h" +#include "init.h" +#include "theme.h" + +#include +#include +#include + +#include +#include + + +/// Turns a volume into the slider step that stands for it. +static int Sound_Volume_To_Step(float volume) +{ + return((int)(volume * (double)UISoundPresenterClass::VOLUME_LEVELS + 0.5)); +} + + +/// Turns a slider step back into the volume it stands for. +static float Sound_Step_To_Volume(int step) +{ + int const clamped = std::clamp(step, 0, UISoundPresenterClass::VOLUME_LEVELS); + return((float)(clamped / (double)UISoundPresenterClass::VOLUME_LEVELS)); +} + + +/// +/// Copies the engine's audio state into the view-model. +/// +void UISoundPresenterClass::Refresh(void) +{ + Available = AudioEngine.Is_Available(); + + // The dialog picked its template by this, not by which menu opened it. + HasMusic = (GameActive != false); + + MusicVolume = Sound_Volume_To_Step(Options.ScoreVolume); + SoundVolume = Sound_Volume_To_Step(Options.SoundVolume); + VoiceVolume = Sound_Volume_To_Step(Options.VoiceVolume); + + Shuffle = Options.IsScoreShuffle; + Repeat = Options.IsScoreRepeat; + + Tracks.clear(); + Selected = 0; + + if (!HasMusic) { + return; + } + + int visible = 1; + for (ThemeType index = THEME_FIRST; index < Theme.Max_Themes(); index++) { + if (!Theme.Is_Allowed(index)) continue; + + char buffer[100]; + int const length = Theme.Track_Length(index); + char const * const fullname = Theme.Full_Name(index); + + std::snprintf(buffer, sizeof(buffer), "%02d - %s [%d:%02d]", visible, + (fullname != nullptr) ? fullname : "", length / 60, length % 60); + visible++; + + if (Theme.What_Is_Playing() == index) { + Selected = (int)Tracks.size(); + } + + TrackType track; + track.Label = buffer; + track.Theme = index; + Tracks.push_back(track); + } +} + + +/// +/// Answers an intent a view raised. +/// +void UISoundPresenterClass::Execute(UIIntent const & intent) +{ + if (intent.Action == UI_SOUND_MUSIC) { + MusicVolume = std::clamp(intent.Value, 0, VOLUME_LEVELS); + Options.Set_Score_Volume(Sound_Step_To_Volume(MusicVolume), true); + + } else if (intent.Action == UI_SOUND_SOUND) { + SoundVolume = std::clamp(intent.Value, 0, VOLUME_LEVELS); + Options.Set_Sound_Volume(Sound_Step_To_Volume(SoundVolume), true); + + } else if (intent.Action == UI_SOUND_VOICE) { + VoiceVolume = std::clamp(intent.Value, 0, VOLUME_LEVELS); + Options.Set_Voice_Volume(Sound_Step_To_Volume(VoiceVolume), true); + + } else if (intent.Action == UI_SOUND_SHUFFLE) { + Shuffle = (intent.Value != 0); + Options.Set_Shuffle(Shuffle); + if (Shuffle) { + Repeat = false; + Options.Set_Repeat(false); + } + + } else if (intent.Action == UI_SOUND_REPEAT) { + Repeat = (intent.Value != 0); + Options.Set_Repeat(Repeat); + if (Repeat) { + Shuffle = false; + Options.Set_Shuffle(false); + } + + } else if (intent.Action == UI_SOUND_SELECT) { + if (intent.Value >= 0 && intent.Value < (int)Tracks.size()) { + Selected = intent.Value; + } + + } else if (intent.Action == UI_SOUND_PLAY) { + if (Selected >= 0 && Selected < (int)Tracks.size()) { + // Stopping first is what the dialog did, so the queued song starts rather than + // waiting behind the one already playing. + Theme.Stop(); + Theme.Queue_Song((ThemeType)Tracks[Selected].Theme); + } + + } else if (intent.Action == UI_SOUND_STOP) { + Theme.Queue_Song(THEME_QUIET); + + } else if (intent.Action == UI_SOUND_ACCEPT) { + // The volumes are applied again without a preview, which is what the dialog's OK + // handler did with the positions it read back off the sliders. + Options.Set_Score_Volume(Sound_Step_To_Volume(MusicVolume), false); + Options.Set_Sound_Volume(Sound_Step_To_Volume(SoundVolume), false); + Options.Set_Voice_Volume(Sound_Step_To_Volume(VoiceVolume), false); + + UIResult result; + result.Outcome = UIResult::OUTCOME_ACCEPTED; + Result = result; + } +} + + +/// +/// The maintenance the dialog driver ran on every pass of its own loop. +/// +void UISoundPresenterClass::Service(void) +{ + if (!GameActive) { + Title_Screen_Restore(); + } +} + + +//--------------------------------------------------------------------------------------- +// The RmlUi view. One document per dialog template, because the two templates differ by +// which controls exist rather than by how one is arranged. +//--------------------------------------------------------------------------------------- + +/// +/// The RmlUi half of the sound screen. +/// +class SoundViewClass : public UIRmlViewClass +{ + public: + SoundViewClass(UISoundPresenterClass & presenter, char const * document); + + virtual void Bind(Rml::DataModelConstructor & model) override; + virtual void Sync(void) override; + + // A slider takes its position from the model as the document loads, and that raises + // a change event of its own. Nothing is queued until this is set, which is what + // DialogInitialized did for the dialog's own WM_HSCROLL. + void Settle(void) { Settled = true; } + + private: + void Volume(char const * which, int step); + + UISoundPresenterClass & Screen; + bool Settled = false; +}; + + +SoundViewClass::SoundViewClass(UISoundPresenterClass & presenter, char const * document) : + UIRmlViewClass(presenter, document), + Screen(presenter) +{ +} + + +void SoundViewClass::Volume(char const * which, int step) +{ + if (!Settled) return; + + // A position the screen already holds raises no intent, so setting a slider from the + // model cannot preview a volume the player did not move. + if (which == UI_SOUND_MUSIC && step == Screen.MusicVolume) return; + if (which == UI_SOUND_SOUND && step == Screen.SoundVolume) return; + if (which == UI_SOUND_VOICE && step == Screen.VoiceVolume) return; + + Screen.Queue(UIIntent{which, "", step}); +} + + +void SoundViewClass::Bind(Rml::DataModelConstructor & model) +{ + if (auto track = model.RegisterStruct()) { + track.RegisterMember("label", &UISoundPresenterClass::TrackType::Label); + } + model.RegisterArray>(); + + model.Bind("music", &Screen.MusicVolume); + model.Bind("sound", &Screen.SoundVolume); + model.Bind("voice", &Screen.VoiceVolume); + model.Bind("shuffle", &Screen.Shuffle); + model.Bind("repeat", &Screen.Repeat); + model.Bind("available", &Screen.Available); + model.Bind("tracks", &Screen.Tracks); + model.Bind("selected", &Screen.Selected); + + // An event handler never acts: it queues, and the runner executes the queue after + // Context::Update has returned. + model.BindEventCallback("volume", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + + Rml::String const which = arguments[0].Get(); + int const step = (int)(event.GetParameter("value", 0.0f) + 0.5f); + + if (which == UI_SOUND_MUSIC) Volume(UI_SOUND_MUSIC, step); + else if (which == UI_SOUND_SOUND) Volume(UI_SOUND_SOUND, step); + else if (which == UI_SOUND_VOICE) Volume(UI_SOUND_VOICE, step); + }); + + model.BindEventCallback("toggle", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + + Rml::String const which = arguments[0].Get(); + if (which == UI_SOUND_SHUFFLE) { + Screen.Queue(UIIntent{UI_SOUND_SHUFFLE, "", Screen.Shuffle ? 0 : 1}); + } else if (which == UI_SOUND_REPEAT) { + Screen.Queue(UIIntent{UI_SOUND_REPEAT, "", Screen.Repeat ? 0 : 1}); + } + }); + + model.BindEventCallback("pick", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + Screen.Queue(UIIntent{UI_SOUND_SELECT, "", (int)arguments[0].Get()}); + }); + + model.BindEventCallback("press", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + if (arguments.empty()) return; + + Rml::String const what = arguments[0].Get(); + if (what == UI_SOUND_PLAY) Screen.Queue(UIIntent{UI_SOUND_PLAY, "", 0}); + else if (what == UI_SOUND_STOP) Screen.Queue(UIIntent{UI_SOUND_STOP, "", 0}); + else if (what == UI_SOUND_ACCEPT) Screen.Queue(UIIntent{UI_SOUND_ACCEPT, "", 0}); + }); + + // Enter accepts, because the template names no default push button and Windows then + // sends the dialog IDOK. Escape does nothing, because the dialog procedure ignored the + // IDCANCEL it produces, so this screen has no cancel either. + model.BindEventCallback("key", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + int const key = event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN); + if (key == Rml::Input::KI_RETURN || key == Rml::Input::KI_NUMPADENTER) { + Screen.Queue(UIIntent{UI_SOUND_ACCEPT, "", 0}); + } + }); +} + + +void SoundViewClass::Sync(void) +{ + if (!Model) return; + + // Only what an executed intent can change is dirtied. The volumes are not, because a + // slider already carries the position its own change event reported. + Model.DirtyVariable("shuffle"); + Model.DirtyVariable("repeat"); + Model.DirtyVariable("selected"); +} + + +/// +/// Shows the sound controls and waits for the player to accept them. +/// +UIResult UI_Sound_Screen(UISoundPresenterClass & presenter) +{ + SoundViewClass view(presenter, presenter.Is_Lite() ? "soundlite.rml" : "sound.rml"); + + if (!view.Prepare(true)) { + UIResult result; + result.Outcome = UIResult::OUTCOME_FAILED_TO_OPEN; + return(result); + } + + view.Settle(); + + UIResult const result = UI_Run_Modal(presenter, view); + view.Close(); + return(result); +} diff --git a/code/ui/uisound.h b/code/ui/uisound.h new file mode 100644 index 000000000..02d6591c1 --- /dev/null +++ b/code/ui/uisound.h @@ -0,0 +1,81 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The sound and music screen's behavior, with no toolkit in it. Both views drive this one +// presenter: the OwnerDraw dialog procedure in sounddlg.cpp and the RmlUi document. +// +// docs/UI_DESIGN.md, "Screens", owns the contract. + +#pragma once + +#include "uiscreen.h" + +#include +#include + + +// What a view raises. Value carries the slider step, the check state or the list row. +inline constexpr char const * UI_SOUND_MUSIC = "music"; // Value: slider step +inline constexpr char const * UI_SOUND_SOUND = "sound"; // Value: slider step +inline constexpr char const * UI_SOUND_VOICE = "voice"; // Value: slider step +inline constexpr char const * UI_SOUND_SHUFFLE = "shuffle"; // Value: nonzero to shuffle +inline constexpr char const * UI_SOUND_REPEAT = "repeat"; // Value: nonzero to repeat +inline constexpr char const * UI_SOUND_SELECT = "select"; // Value: track list row +inline constexpr char const * UI_SOUND_PLAY = "play"; +inline constexpr char const * UI_SOUND_STOP = "stop"; +inline constexpr char const * UI_SOUND_ACCEPT = "accept"; + + +class UISoundPresenterClass : public UIPresenterClass +{ + public: + // The steps a volume is expressed in, which is the range the dialog's track bars + // were given. A view shows steps; only this class knows what they mean. + static constexpr int VOLUME_LEVELS = 10; + + struct TrackType + { + std::string Label; + int Theme = 0; + }; + + virtual void Execute(UIIntent const & intent) override; + virtual void Refresh(void) override; + virtual void Service(void) override; + + /* + ** The view-model. Plain values, and the only thing a view reads. + */ + int MusicVolume = 0; + int SoundVolume = 0; + int VoiceVolume = 0; + bool Shuffle = false; + bool Repeat = false; + + // Can anything be heard? With no audio device every control is shown but disabled, + // as the dialog disabled them. + bool Available = false; + + // Does this screen carry the music controls? Only the template the game shows during + // play does; the one it shows with no game running carries the three volumes alone. + bool HasMusic = false; + + std::vector Tracks; + int Selected = 0; + + // Which of the two dialog templates the state above corresponds to, for a view that + // has to choose a document. + bool Is_Lite(void) const { return(!HasMusic); } +}; + + +// Shows the screen for the state the presenter was refreshed into and does not return until +// the player accepts it. A OUTCOME_FAILED_TO_OPEN result means the document could not be +// prepared and nothing was shown, which is the caller's cue to open the legacy dialog. +UIResult UI_Sound_Screen(UISoundPresenterClass & presenter); diff --git a/code/ui/uisurface.cpp b/code/ui/uisurface.cpp new file mode 100644 index 000000000..aed580ccb --- /dev/null +++ b/code/ui/uisurface.cpp @@ -0,0 +1,362 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The element and the providers behind it. This is where engine-drawn pixels +// enter a document: the map preview, the desync host icons and the progress bar are all +// surfaces the game draws for itself, and none of them can be a file a document names. +// +// A document writes . The name is looked up among the live providers +// at render time rather than at parse time, so a document may be shown before the screen +// that owns its pixels has registered them, and an element whose provider went away simply +// draws nothing rather than failing to lay out. +// +// The element uploads only when the provider's generation moves, so a document holding a +// surface costs one quad per present while nothing changes. +// +// docs/UI_DESIGN.md, "Assets and strings" and "Rendering", own the contracts here. + +#include "always.h" + +#include "uisurface.h" + +#include "uiinternal.h" + +#include "bsurface.h" +#include "dsurface.h" +#include "rgb.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + + +UISurfaceProviderClass::~UISurfaceProviderClass(void) +{ +} + + +//--------------------------------------------------------------------------------------- +// The registry. A provider is reached by name and nothing holds a pointer to one past its +// registration, so a screen that closes takes its pixels with it. +//--------------------------------------------------------------------------------------- + +static std::map _Providers; + + +void UI_Register_Surface(char const * name, UISurfaceProviderClass * provider) +{ + if (name == nullptr || name[0] == '\0' || provider == nullptr) { + return; + } + + _Providers[name] = provider; +} + + +void UI_Unregister_Surface(char const * name) +{ + if (name == nullptr) { + return; + } + + _Providers.erase(name); +} + + +static UISurfaceProviderClass * Find_Provider(std::string const & name) +{ + std::map::const_iterator found = _Providers.find(name); + return(found == _Providers.end() ? nullptr : found->second); +} + + +//--------------------------------------------------------------------------------------- +// The surface-backed provider. +//--------------------------------------------------------------------------------------- + +UISurfaceBufferClass::UISurfaceBufferClass(int width, int height) : + Width(width > 0 ? width : 1), + Height(height > 0 ? height : 1) +{ + Buffer = new BSurface(Width, Height, 2); + Buffer->Fill(Transparent); +} + + +UISurfaceBufferClass::~UISurfaceBufferClass(void) +{ + delete Buffer; + Buffer = nullptr; +} + + +void UISurfaceBufferClass::Clear(void) +{ + Buffer->Fill(Transparent); + Mark_Dirty(); +} + + +/// +/// Converts the engine surface into the premultiplied RGBA8 pixels a texture wants. +/// +/// Receives Get_Width() by Get_Height() pixels, top row first. +/// bool; Were the pixels written? +bool UISurfaceBufferClass::Read_Pixels(unsigned char * pixels) const +{ + if (pixels == nullptr || Buffer == nullptr) { + return(false); + } + + unsigned short const * const source = (unsigned short const *)Buffer->Lock(); + if (source == nullptr) { + return(false); + } + + int const stride = Buffer->Stride() / (int)sizeof(unsigned short); + unsigned short const transparent = (unsigned short)Transparent; + + for (int y = 0; y < Height; y++) { + unsigned short const * row = source + (std::size_t)y * stride; + unsigned char * out = pixels + (std::size_t)y * Width * 4; + + for (int x = 0; x < Width; x++) { + unsigned short const pixel = row[x]; + + if (pixel == transparent) { + out[0] = 0; + out[1] = 0; + out[2] = 0; + out[3] = 0; + } else { + // Opaque, so the premultiplied color the render interface expects is the + // color itself. + RGBClass const color = DSurface::Deconstruct_Hicolor_Pixel(pixel); + out[0] = (unsigned char)color.Get_Red(); + out[1] = (unsigned char)color.Get_Green(); + out[2] = (unsigned char)color.Get_Blue(); + out[3] = 255; + } + + out += 4; + } + } + + Buffer->Unlock(); + return(true); +} + + +//--------------------------------------------------------------------------------------- +// The element. +//--------------------------------------------------------------------------------------- + +namespace { + +class UISurfaceElement : public Rml::Element +{ + public: + UISurfaceElement(Rml::String const & tag) : Rml::Element(tag) {} + + virtual bool GetIntrinsicDimensions(Rml::Vector2f & dimensions, float & ratio) override; + + protected: + virtual void OnUpdate(void) override; + virtual void OnDpRatioChange(void) override { DirtyLayout(); } + virtual void OnRender(void) override; + virtual void OnResize(void) override { GeometryIsStale = true; } + virtual void OnAttributeChange(Rml::ElementAttributes const & changed) override; + + private: + void Generate_Geometry(void); + bool Refresh_Texture(UISurfaceProviderClass & provider); + + std::string Source; + Rml::Geometry Quad; + Rml::CallbackTexture Pixels; + + // The provider generation the texture was made from. Zero means there is no texture. + unsigned int Uploaded = 0; + + // The provider extents the last layout was made from. A provider that registers + // after the document was laid out, or one that changes size, is what these catch. + int LaidOutWidth = -1; + int LaidOutHeight = -1; + + bool GeometryIsStale = true; +}; + + +/// +/// Notices a provider arriving, leaving or changing size, none of which the layout would +/// otherwise hear about, and asks for the element to be laid out again. +/// +void UISurfaceElement::OnUpdate(void) +{ + Rml::Element::OnUpdate(); + + UISurfaceProviderClass const * const provider = Find_Provider(GetAttribute("src", "")); + int const width = (provider != nullptr) ? provider->Get_Width() : 0; + int const height = (provider != nullptr) ? provider->Get_Height() : 0; + + if (width != LaidOutWidth || height != LaidOutHeight) { + LaidOutWidth = width; + LaidOutHeight = height; + DirtyLayout(); + } +} + + +bool UISurfaceElement::GetIntrinsicDimensions(Rml::Vector2f & dimensions, float & ratio) +{ + UISurfaceProviderClass const * const provider = Find_Provider(GetAttribute("src", "")); + + // A surface is sized in game logical units, which is what one authored density + // independent pixel is, so the provider's own extents are its intrinsic size. + float const width = (provider != nullptr) ? (float)provider->Get_Width() : 0.0f; + float const height = (provider != nullptr) ? (float)provider->Get_Height() : 0.0f; + + if (height > 0.0f) { + ratio = width / height; + } + + // A surface's pixels are game logical units, and one authored density independent pixel + // is one of those, so the extents follow the document's scale rather than staying at + // their own pixel count. + dimensions = Rml::Vector2f(width, height) * Rml::ElementUtilities::GetDensityIndependentPixelRatio(this); + + return(true); +} + + +void UISurfaceElement::OnAttributeChange(Rml::ElementAttributes const & changed) +{ + Rml::Element::OnAttributeChange(changed); + + if (changed.find("src") != changed.end()) { + Pixels.Release(); + Uploaded = 0; + GeometryIsStale = true; + DirtyLayout(); + } +} + + +void UISurfaceElement::Generate_Geometry(void) +{ + Rml::Mesh mesh = Quad.Release(Rml::Geometry::ReleaseMode::ClearMesh); + + Rml::ComputedValues const & computed = GetComputedValues(); + Rml::ColourbPremultiplied const colour = computed.image_color().ToPremultiplied(computed.opacity()); + Rml::RenderBox const box = GetRenderBox(Rml::BoxArea::Content); + + Rml::MeshUtilities::GenerateQuad(mesh, box.GetFillOffset(), box.GetFillSize(), colour, + Rml::Vector2f(0.0f, 0.0f), Rml::Vector2f(1.0f, 1.0f)); + + if (Rml::RenderManager * manager = GetRenderManager()) { + Quad = manager->MakeGeometry(std::move(mesh)); + } + + GeometryIsStale = false; +} + + +/// +/// Brings the texture up to the provider's current pixels, uploading only when they moved. +/// +/// bool; Is there a texture to draw? +bool UISurfaceElement::Refresh_Texture(UISurfaceProviderClass & provider) +{ + unsigned int const generation = provider.Get_Generation(); + if (Uploaded == generation && Pixels) { + return(true); + } + + Rml::RenderManager * const manager = GetRenderManager(); + if (manager == nullptr) { + return(false); + } + + int const width = provider.Get_Width(); + int const height = provider.Get_Height(); + if (width <= 0 || height <= 0) { + return(false); + } + + // The callback runs when the texture is first needed for drawing, which keeps the + // conversion off the path that only lays the document out. + Pixels.Release(); + Pixels = manager->MakeCallbackTexture( + [&provider, width, height](Rml::CallbackTextureInterface const & texture) -> bool { + std::vector rgba((std::size_t)width * height * 4, 0); + if (!provider.Read_Pixels(rgba.data())) { + return(false); + } + return(texture.GenerateTexture(Rml::Span(rgba.data(), rgba.size()), + Rml::Vector2i(width, height))); + }); + + Uploaded = generation; + return(true); +} + + +void UISurfaceElement::OnRender(void) +{ + std::string const source = GetAttribute("src", ""); + if (source != Source) { + Source = source; + Uploaded = 0; + } + + UISurfaceProviderClass * const provider = Find_Provider(Source); + if (provider == nullptr) { + return; + } + + if (!Refresh_Texture(*provider)) { + return; + } + + if (GeometryIsStale) { + Generate_Geometry(); + } + + Quad.Render(GetAbsoluteOffset(Rml::BoxArea::Border), Pixels); +} + + +static Rml::ElementInstancerGeneric _Instancer; + +} // namespace + + +void UI_Surface_Element_Init(void) +{ + Rml::Factory::RegisterElementInstancer("surface", &_Instancer); +} + + +void UI_Surface_Element_Shutdown(void) +{ + _Providers.clear(); +} diff --git a/code/ui/uisurface.h b/code/ui/uisurface.h new file mode 100644 index 000000000..8f7799f0d --- /dev/null +++ b/code/ui/uisurface.h @@ -0,0 +1,75 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// Pixels the engine draws at run time, shown inside a document by the element. +// A provider is registered under a name and a document names it: . +// No RmlUi type appears here, so a screen's behavior half and the engine can own a provider +// without carrying the toolkit. +// +// docs/UI_DESIGN.md, "Assets and strings", owns the routing. + +#pragma once + +class Surface; + + +class UISurfaceProviderClass +{ + public: + virtual ~UISurfaceProviderClass(void); + + virtual int Get_Width(void) const = 0; + virtual int Get_Height(void) const = 0; + + // Writes Get_Width() by Get_Height() premultiplied RGBA8 pixels, top row first. + virtual bool Read_Pixels(unsigned char * pixels) const = 0; + + // Says the pixels changed. The element uploads them again before it next draws them + // and does nothing at all while this stands still, so a redraw costs no upload. + void Mark_Dirty(void) { Generation++; } + unsigned int Get_Generation(void) const { return(Generation); } + + private: + unsigned int Generation = 1; +}; + + +// A provider backed by an engine surface. A screen draws into it with the engine's ordinary +// drawing calls and marks it dirty; the conversion to what a texture wants happens here. +// Pixels matching the transparent color are written fully transparent, which is how the +// game's own artwork carries its mask. +class UISurfaceBufferClass : public UISurfaceProviderClass +{ + public: + UISurfaceBufferClass(int width, int height); + virtual ~UISurfaceBufferClass(void) override; + + Surface & Get_Surface(void) const { return(*Buffer); } + + void Set_Transparent_Color(int color) { Transparent = color; } + + // Fills the whole buffer with the transparent color and marks it dirty. + void Clear(void); + + virtual int Get_Width(void) const override { return(Width); } + virtual int Get_Height(void) const override { return(Height); } + virtual bool Read_Pixels(unsigned char * pixels) const override; + + private: + Surface * Buffer = nullptr; + int Width = 0; + int Height = 0; + int Transparent = 0; +}; + + +// Names a provider so a document can reach it. A name is unique among live providers, and a +// registration is dropped before its provider is destroyed. +void UI_Register_Surface(char const * name, UISurfaceProviderClass * provider); +void UI_Unregister_Surface(char const * name); diff --git a/code/ui/uisystem.cpp b/code/ui/uisystem.cpp new file mode 100644 index 000000000..02c003480 --- /dev/null +++ b/code/ui/uisystem.cpp @@ -0,0 +1,203 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// What RmlUi asks the host for: the clock its animations run on, where its messages go, +// the cursor it wants shown, the clipboard, and the strings its documents name. +// +// docs/UI_DESIGN.md, "Assets and strings", owns the string half. + +#include "always.h" + +#include "uiinternal.h" + +#include "data.h" +#include "dbgprint.h" +#include "hostclock.h" + +#include + +#include + +#include +#include + + +static unsigned int _StartTime = 0; +static std::string _CursorName; + + +/// +/// Looks a document's string name up in the engine's string table. +/// The name table is generated from language.rc by the script that also builds the portable +/// string table, so the resource script stays the only place a name and a number are paired. +/// An unknown name leaves the reference in the text, which is what makes a missing string +/// visible rather than silent. +/// +/// bool; Was the name resolved? +static bool Lookup_String(std::string const & name, std::string & text) +{ + static std::unordered_map const _names = { +#define OPENTS_STRING_NAME(symbol, id) { symbol, id }, +#include "stringnames.hh" +#undef OPENTS_STRING_NAME + }; + + auto const found = _names.find(name); + if (found == _names.end()) { + return(false); + } + + // Fetch_String already yields UTF-8, so the shell copies the bytes out of its cache and + // hands them to RmlUi unchanged. + text = Fetch_String(found->second); + return(true); +} + + +class UISystemInterface : public Rml::SystemInterface +{ + public: + virtual double GetElapsedTime() override + { + return((double)(Host_Milliseconds() - _StartTime) / 1000.0); + } + + virtual int TranslateString(Rml::String & translated, const Rml::String & input) override; + + virtual bool LogMessage(Rml::Log::Type type, const Rml::String & message) override + { + char const * label = "info"; + switch (type) { + case Rml::Log::LT_ERROR: label = "error"; break; + case Rml::Log::LT_ASSERT: label = "assert"; break; + case Rml::Log::LT_WARNING: label = "warning"; break; + default: break; + } + + DebugString("[UI] %s: %s\n", label, message.c_str()); + return(true); + } + + virtual void SetMouseCursor(const Rml::String & name) override + { + // The game's pointer is built from its own shapes rather than from a system + // cursor, so a document cannot ask for one yet. The request is recorded for + // the screen that first needs a text caret to act on. + _CursorName = name; + } + + virtual void SetClipboardText(const Rml::String & text) override; + virtual void GetClipboardText(Rml::String & text) override; +}; + +static UISystemInterface _SystemInterface; + + +/// +/// Replaces a [[NAME]] reference with the engine string it names. +/// +/// int; How many replacements were made. +int UISystemInterface::TranslateString(Rml::String & translated, const Rml::String & input) +{ + translated.clear(); + int replaced = 0; + + std::size_t position = 0; + while (position < input.size()) { + std::size_t open = input.find("[[", position); + if (open == Rml::String::npos) { + break; + } + + std::size_t close = input.find("]]", open + 2); + if (close == Rml::String::npos) { + break; + } + + std::string text; + std::string const name = input.substr(open + 2, close - open - 2); + + translated.append(input, position, open - position); + + if (Lookup_String(name, text)) { + translated.append(text); + replaced++; + } else { + translated.append(input, open, close + 2 - open); + } + + position = close + 2; + } + + translated.append(input, position, Rml::String::npos); + return(replaced); +} + + +void UISystemInterface::SetClipboardText(const Rml::String & text) +{ +#ifdef _WIN32 + if (!OpenClipboard(NULL)) { + return; + } + + EmptyClipboard(); + + HGLOBAL block = GlobalAlloc(GMEM_MOVEABLE, text.size() + 1); + if (block != NULL) { + char * buffer = (char *)GlobalLock(block); + if (buffer != NULL) { + memcpy(buffer, text.c_str(), text.size() + 1); + GlobalUnlock(block); + SetClipboardData(CF_TEXT, block); + } else { + GlobalFree(block); + } + } + + CloseClipboard(); +#else + // The compatibility layer carries no clipboard yet. An editable screen ships only + // after paste has been exercised, so this has to be supplied before one does. + (void)text; +#endif +} + + +void UISystemInterface::GetClipboardText(Rml::String & text) +{ + text.clear(); + +#ifdef _WIN32 + if (!OpenClipboard(NULL)) { + return; + } + + HANDLE block = GetClipboardData(CF_TEXT); + if (block != NULL) { + char const * buffer = (char const *)GlobalLock(block); + if (buffer != NULL) { + text = buffer; + GlobalUnlock(block); + } + } + + CloseClipboard(); +#endif +} + + +Rml::SystemInterface * UI_System_Interface(void) +{ + if (_StartTime == 0) { + _StartTime = Host_Milliseconds(); + } + + return(&_SystemInterface); +} diff --git a/code/ui/uitexture.cpp b/code/ui/uitexture.cpp new file mode 100644 index 000000000..5ac571281 --- /dev/null +++ b/code/ui/uitexture.cpp @@ -0,0 +1,385 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// Turns an image a document names into the premultiplied RGBA8 pixels the render interface +// uploads. The bytes come through the game's file system, so an image resolves from a mix +// exactly as a document does. +// +// Images resolve by extension. PNG and TGA decode through bimg, which bgfx already carries. +// PCX goes through Read_PCX_File and SHP through ShapeSet, which is how the game's own +// artwork reaches a document. +// +// The source string is a file name followed by up to two '#' arguments: +// +// dbak6440.pcx the picture, through the palette it carries +// dbak6440.pcx#mousepal.pal the picture, through a named palette instead +// mouse.shp#0 one frame, through the game palette +// mouse.shp#0#mousepal.pal one frame, through a named palette +// +// A .pal file holds the six-bit guns the video hardware wanted, so its values are scaled +// the way init.cpp scales the palettes it loads. Shape index zero is transparent. +// +// docs/UI_DESIGN.md, "Assets and strings", owns the routing. + +#include "always.h" + +#include "uiinternal.h" + +#include "_palette.h" +#include "ccfile.h" +#include "dbgprint.h" +#include "dsurface.h" +#include "pcx.h" +#include "shapeset.h" + +#include +#include + +#include +#include +#include +#include +#include + + +static bx::DefaultAllocator _Allocator; + + +static std::string Extension_Of(char const * source) +{ + std::string const name = source != NULL ? source : ""; + std::size_t const dot = name.find_last_of('.'); + if (dot == std::string::npos) { + return(""); + } + + std::string extension = name.substr(dot + 1); + for (char & letter : extension) { + letter = (char)std::tolower((unsigned char)letter); + } + return(extension); +} + + +/// +/// Strips every directory a reference carries, matching how uifile.cpp resolves names. +/// +static std::string Base_Name(char const * source) +{ + std::string const path = source != NULL ? source : ""; + std::size_t const mark = path.find_last_of("\\/:"); + return(mark == std::string::npos ? path : path.substr(mark + 1)); +} + + +static bool Read_Whole_File(char const * name, std::vector & bytes) +{ + CCFileClass file(name); + + if (!file.Is_Available() || !file.Open(FileClass::READ)) { + return(false); + } + + int const size = file.Size(); + if (size <= 0) { + file.Close(); + return(false); + } + + bytes.resize((std::size_t)size); + int const read = file.Read(bytes.data(), size); + file.Close(); + + return(read == size); +} + + +/// +/// Reads one of the game's palette files. +/// +/// bool; Was a full 256 colour palette read? +static bool Read_Palette_File(char const * name, PaletteClass & palette) +{ + std::vector bytes; + + if (!Read_Whole_File(name, bytes) || bytes.size() < 256 * 3) { + return(false); + } + + for (int index = 0; index < 256; index++) { + palette[index] = RGBClass( + (unsigned char)(bytes[index * 3 + 0] << 2), + (unsigned char)(bytes[index * 3 + 1] << 2), + (unsigned char)(bytes[index * 3 + 2] << 2)); + } + + return(true); +} + + +static void Store_Opaque_Pixel(UIImageData & image, std::size_t offset, RGBClass const & color) +{ + image.Pixels[offset + 0] = (unsigned char)color.Get_Red(); + image.Pixels[offset + 1] = (unsigned char)color.Get_Green(); + image.Pixels[offset + 2] = (unsigned char)color.Get_Blue(); + image.Pixels[offset + 3] = 255; +} + + +/// +/// Decodes one of the game's PCX pictures. +/// +/// The file name, resolved through the game's file system. +/// A palette to use instead of the one the file carries, or an +/// empty string for the file's own. +/// bool; Was the picture decoded? +static bool Decode_PCX(std::string const & name, std::string const & palette_name, UIImageData & image) +{ + CCFileClass file(name.c_str()); + PaletteClass palette; + + Surface * picture = Read_PCX_File(file, &palette); + if (picture == NULL) { + return(false); + } + + if (!palette_name.empty() && !Read_Palette_File(palette_name.c_str(), palette)) { + DebugString("[UI] Palette %s could not be read for %s.\n", palette_name.c_str(), name.c_str()); + } + + image.Width = picture->Get_Width(); + image.Height = picture->Get_Height(); + + if (image.Width <= 0 || image.Height <= 0) { + delete picture; + return(false); + } + + image.Pixels.assign((std::size_t)image.Width * image.Height * 4, 0); + + int const stride = picture->Stride(); + unsigned char const * bits = (unsigned char const *)picture->Lock(); + + if (bits == NULL) { + delete picture; + return(false); + } + + if (picture->Bytes_Per_Pixel() == 1) { + for (int y = 0; y < image.Height; y++) { + unsigned char const * row = bits + (std::size_t)y * stride; + for (int x = 0; x < image.Width; x++) { + Store_Opaque_Pixel(image, ((std::size_t)y * image.Width + x) * 4, palette[row[x]]); + } + } + } else { + + // A three plane PCX is decoded straight into the primary's own packing, so the + // component masks the video mode established are what take it apart again. + for (int y = 0; y < image.Height; y++) { + unsigned short const * row = (unsigned short const *)(bits + (std::size_t)y * stride); + for (int x = 0; x < image.Width; x++) { + unsigned short const pixel = row[x]; + RGBClass const color( + (unsigned char)((pixel >> DSurface::RedRight) << DSurface::RedLeft), + (unsigned char)((pixel >> DSurface::GreenRight) << DSurface::GreenLeft), + (unsigned char)((pixel >> DSurface::BlueRight) << DSurface::BlueLeft)); + Store_Opaque_Pixel(image, ((std::size_t)y * image.Width + x) * 4, color); + } + } + } + + picture->Unlock(); + delete picture; + + return(true); +} + + +/// +/// Decodes one frame of a shape file into the shape's logical rectangle. +/// Index zero is transparent, so a frame keeps the position its sub-rectangle gives it and +/// the pixels around it stay clear. +/// +/// bool; Was the frame decoded? +static bool Decode_SHP(std::string const & name, int frame, std::string const & palette_name, UIImageData & image) +{ + std::vector bytes; + + if (!Read_Whole_File(name.c_str(), bytes) || bytes.size() < sizeof(ShapeSet)) { + return(false); + } + + PaletteClass palette = GamePalette; + if (!palette_name.empty() && !Read_Palette_File(palette_name.c_str(), palette)) { + DebugString("[UI] Palette %s could not be read for %s.\n", palette_name.c_str(), name.c_str()); + } + + ShapeSet const * shape = (ShapeSet const *)bytes.data(); + + image.Width = shape->Get_Width(); + image.Height = shape->Get_Height(); + + if (image.Width <= 0 || image.Height <= 0 || frame < 0 || frame >= shape->Get_Count()) { + return(false); + } + + image.Pixels.assign((std::size_t)image.Width * image.Height * 4, 0); + + Rect const rect = shape->Get_Rect(frame); + unsigned char const * data = (unsigned char const *)shape->Get_Data(frame); + + if (data == NULL || rect.Width <= 0 || rect.Height <= 0) { + return(true); + } + + if (rect.X < 0 || rect.Y < 0 + || rect.X + rect.Width > image.Width || rect.Y + rect.Height > image.Height) { + DebugString("[UI] Shape %s frame %d claims a rectangle outside its own bounds.\n", name.c_str(), frame); + return(false); + } + + bool const compressed = shape->Is_RLE_Compressed(frame); + unsigned char const * const end = bytes.data() + bytes.size(); + unsigned char const * line = data; + + for (int y = 0; y < rect.Height; y++) { + + // A compressed line starts with its own byte length and then runs of pixels, where a + // zero introduces a count of transparent ones. + unsigned char const * source = compressed ? line + sizeof(unsigned short) : data + (std::size_t)y * rect.Width; + int x = 0; + + while (x < rect.Width) { + + if (source >= end) { + DebugString("[UI] Shape %s frame %d runs past the end of the file.\n", name.c_str(), frame); + return(false); + } + + unsigned char const index = *source++; + + if (index == 0) { + if (compressed) { + if (source >= end) { + return(false); + } + unsigned char const run = *source++; + + // A zero length run would leave the line where it is, so it counts as one + // pixel rather than as a reason to stop moving. + x += run != 0 ? run : 1; + } else { + x++; + } + continue; + } + + std::size_t const offset = ((std::size_t)(rect.Y + y) * image.Width + (rect.X + x)) * 4; + Store_Opaque_Pixel(image, offset, palette[index]); + x++; + } + + if (compressed) { + if (line + sizeof(unsigned short) > end) { + return(false); + } + unsigned short length; + std::memcpy(&length, line, sizeof(length)); + if (length == 0) { + return(false); + } + line += length; + } + } + + return(true); +} + + +static bool Decode_Through_Bimg(std::string const & name, UIImageData & image) +{ + std::vector bytes; + if (!Read_Whole_File(name.c_str(), bytes)) { + return(false); + } + + bimg::ImageContainer * container = bimg::imageParse(&_Allocator, bytes.data(), + (uint32_t)bytes.size(), bimg::TextureFormat::RGBA8); + + if (container == NULL) { + return(false); + } + + image.Width = (int)container->m_width; + image.Height = (int)container->m_height; + image.Pixels.assign((unsigned char const *)container->m_data, + (unsigned char const *)container->m_data + (std::size_t)image.Width * image.Height * 4); + + bimg::imageFree(container); + + // The render interface uploads premultiplied alpha, which is what RmlUi's texture + // contract specifies, and a decoded file carries straight alpha. + for (std::size_t pixel = 0; pixel + 3 < image.Pixels.size(); pixel += 4) { + unsigned int const alpha = image.Pixels[pixel + 3]; + image.Pixels[pixel + 0] = (unsigned char)((image.Pixels[pixel + 0] * alpha + 127) / 255); + image.Pixels[pixel + 1] = (unsigned char)((image.Pixels[pixel + 1] * alpha + 127) / 255); + image.Pixels[pixel + 2] = (unsigned char)((image.Pixels[pixel + 2] * alpha + 127) / 255); + } + + return(true); +} + + +/// +/// Reads an image a document referenced and hands back its pixels. +/// +/// The image source string as the document wrote it. +/// Receives premultiplied RGBA8 pixels, top row first. +/// bool; Was the image decoded? +bool UI_Decode_Image(char const * source, UIImageData & image) +{ + std::string const request = Base_Name(source); + + std::string name = request; + std::string first; + std::string second; + + std::size_t mark = name.find('#'); + if (mark != std::string::npos) { + first = name.substr(mark + 1); + name = name.substr(0, mark); + + mark = first.find('#'); + if (mark != std::string::npos) { + second = first.substr(mark + 1); + first = first.substr(0, mark); + } + } + + std::string const extension = Extension_Of(name.c_str()); + bool decoded = false; + + if (extension == "pcx") { + decoded = Decode_PCX(name, first, image); + } else if (extension == "shp") { + decoded = Decode_SHP(name, std::atoi(first.c_str()), second, image); + } else if (extension == "png" || extension == "tga") { + decoded = Decode_Through_Bimg(name, image); + } else { + DebugString("[UI] Image %s has no reader.\n", request.c_str()); + return(false); + } + + if (!decoded) { + return(false); + } + + return(image.Width > 0 && image.Height > 0); +} diff --git a/code/ui/uiversion.cpp b/code/ui/uiversion.cpp new file mode 100644 index 000000000..618be01b4 --- /dev/null +++ b/code/ui/uiversion.cpp @@ -0,0 +1,215 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The version information screen: the first screen to migrate, and the pattern the rest +// follow. The presenter gathers the same facts the dialog procedure gathered and holds +// them as plain strings; the view renders them and turns a click or a key into an intent. +// Neither half knows about the other's world. +// +// docs/UI_DESIGN.md, "Screens", owns the contract this keeps to. + +#include "always.h" + +#include "uiversion.h" + +#include "uirmlview.h" + +#include "addon.h" +#include "data.h" +#include "getcpu.h" +#include "globals.h" +#include "language/language.h" +#include "opents_build.h" +#include "version.h" + +#include +#include +#include + +#include + + +// What the view asks for. The strings are the intents' whole vocabulary, so a document can +// name an action without naming a control. +static char const * const ACTION_ACCEPT = "accept"; +static char const * const ACTION_CANCEL = "cancel"; + + +/// +/// The toolkit-free half of the version screen. +/// +class VersionPresenterClass : public UIPresenterClass +{ + public: + // The view-model. Plain values, copied out of the engine by Refresh, and living + // longer than the data model the view binds to them. + std::vector Lines; + + virtual void Execute(UIIntent const & intent) override; + virtual void Refresh(void) override; +}; + + +/// +/// Collects what a player is asked for when they report a problem. +/// These are the same facts, from the same sources and in the same order, that the dialog +/// procedure put into its list box. +/// +void VersionPresenterClass::Refresh(void) +{ + char buffer[256]; + + Lines.clear(); + + if (Addon_Installed(ADDON_FIRESTORM) == true) { + std::string title = Fetch_String(TXT_SHORT_TITLE); + title += ": "; + title += Get_Addon_Title(ADDON_FIRESTORM); + Lines.push_back(title); + } else { + Lines.push_back(Fetch_String(TXT_SHORT_TITLE)); + } + + std::snprintf(buffer, sizeof(buffer), "Version %s", Version_Name()); + Lines.push_back(buffer); + + std::snprintf(buffer, sizeof(buffer), "Internal Version %s", VerNum.Version_Name()); + Lines.push_back(buffer); + +#ifdef _DEBUG + std::snprintf(buffer, sizeof(buffer), "Debug Build: %s - %s", OPENTS_BUILD_DESCRIPTION, OPENTS_COMMIT_DATE); +#else + std::snprintf(buffer, sizeof(buffer), "Release Build: %s - %s", OPENTS_BUILD_DESCRIPTION, OPENTS_COMMIT_DATE); +#endif + Lines.push_back(buffer); + + int cpu_type = 5; + char vendor[32]; + vendor[0] = '\0'; + Get_CPU_Type(cpu_type, vendor, sizeof(vendor) - 1); + std::snprintf(buffer, sizeof(buffer), "CPU vendor: %s", vendor); + Lines.push_back(buffer); + + Get_Language_Version(buffer); + Lines.push_back(buffer); +} + + +/// +/// Answers an intent the view raised. The screen reads nothing and changes nothing, so +/// the only transition it has is the one that ends it. +/// +void VersionPresenterClass::Execute(UIIntent const & intent) +{ + UIResult result; + + if (intent.Action == ACTION_ACCEPT) { + result.Outcome = UIResult::OUTCOME_ACCEPTED; + } else if (intent.Action == ACTION_CANCEL) { + result.Outcome = UIResult::OUTCOME_CANCELLED; + } else { + return; + } + + Result = result; +} + + +/// +/// The RmlUi half. It owns the document, the data model bound to the presenter's +/// view-model, and the mapping from what the player did to what the screen was asked for. +/// +class VersionViewClass : public UIRmlViewClass +{ + public: + VersionViewClass(VersionPresenterClass & presenter); + + virtual void Bind(Rml::DataModelConstructor & model) override; + virtual void Sync(void) override; + + private: + void Dismiss(char const * action); + + VersionPresenterClass & Screen; +}; + + +VersionViewClass::VersionViewClass(VersionPresenterClass & presenter) : + UIRmlViewClass(presenter, "version.rml"), + Screen(presenter) +{ +} + + +void VersionViewClass::Dismiss(char const * action) +{ + UIIntent intent; + intent.Action = action; + Screen.Queue(intent); +} + + +void VersionViewClass::Bind(Rml::DataModelConstructor & model) +{ + model.RegisterArray>(); + model.Bind("lines", &Screen.Lines); + + // An event handler never acts: it queues, and the runner executes the queue after + // Context::Update has returned. + model.BindEventCallback("dismiss", + [this](Rml::DataModelHandle, Rml::Event &, Rml::VariantList const & arguments) { + Dismiss(arguments.empty() ? ACTION_ACCEPT : arguments[0].Get().c_str()); + }); + + // Return accepts and Escape cancels, which is what the dialog's IDOK and IDCANCEL did. + // The document listens rather than the shell, because which keys dismiss a screen is + // the screen's business. + model.BindEventCallback("key", + [this](Rml::DataModelHandle, Rml::Event & event, Rml::VariantList const &) { + int const key = event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN); + + if (key == Rml::Input::KI_RETURN || key == Rml::Input::KI_NUMPADENTER) { + Dismiss(ACTION_ACCEPT); + } else if (key == Rml::Input::KI_ESCAPE) { + Dismiss(ACTION_CANCEL); + } + }); +} + + +void VersionViewClass::Sync(void) +{ + // Nothing an intent can execute changes the view-model, so there is nothing to dirty. + // The screen's only transition ends it. +} + + +/// +/// Shows the version information and waits for the player to dismiss it. +/// +/// The screen's result. OUTCOME_FAILED_TO_OPEN means nothing was shown. +UIResult UI_Version_Screen(void) +{ + // The presenter is declared first so that it is destroyed last: the data model the view + // binds reads the presenter's view-model, and must not outlive it. + VersionPresenterClass presenter; + VersionViewClass view(presenter); + + presenter.Refresh(); + + if (!view.Prepare(true)) { + UIResult result; + result.Outcome = UIResult::OUTCOME_FAILED_TO_OPEN; + return(result); + } + + UIResult const result = UI_Run_Modal(presenter, view); + view.Close(); + return(result); +} diff --git a/code/ui/uiversion.h b/code/ui/uiversion.h new file mode 100644 index 000000000..af4adb6d1 --- /dev/null +++ b/code/ui/uiversion.h @@ -0,0 +1,23 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// The version information screen, reached from the main menu. Only the result contract +// crosses this header, so the caller carries no toolkit of any kind. +// +// docs/UI_DESIGN.md, "Screens", owns the contract. + +#pragma once + +#include "uiscreen.h" + + +// Shows the version information and does not return until the player dismisses it. A +// OUTCOME_FAILED_TO_OPEN result means the documents could not be prepared and nothing was +// shown, which is the caller's cue to open the legacy dialog instead. +UIResult UI_Version_Screen(void); diff --git a/code/unit.cpp b/code/unit.cpp index b7cfb86d3..1012a1309 100644 --- a/code/unit.cpp +++ b/code/unit.cpp @@ -95,7 +95,6 @@ * UnitClass::~UnitClass -- Destructor for unit objects. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "unit.h" @@ -127,7 +126,7 @@ #include "fog.h" #include "house.h" #include "houstype.h" -#include "ilocos.h" +#include "classids.h" #include "incdec.h" #include "infantry.h" #include "infatype.h" @@ -223,7 +222,7 @@ UnitClass::UnitClass(UnitTypeClass const * type, HouseClass * house) : SecondaryFacing.Set(PrimaryFacing.Current()); if (Class != NULL) { - Locomotion = ILocomotionPtr(Class->Locomotor, NULL, CLSCTX_ALL); + Locomotion = Create_Locomotor(Class->Locomotor); Locomotion->Link_To_Object(this); } @@ -2107,10 +2106,8 @@ void UnitClass::Per_Cell_Process(PCPType why) Cell center = Center_Coord(); Cell whom_center = whom->Center_Coord(); if (Center_Coord().As_Cell() == whom->Center_Coord().As_Cell() && whom->RTTI == RTTI_BUILDING) { - IPersistPtr persist(Locomotion); - CLSID clsid; - persist->GetClassID(&clsid); - if (clsid == CLSID_HoverLocomotion && static_cast(whom)->Class->IsCanUnitRepair && NavCom == NULL) { + ClassID const clsid = Locomotion_Class_ID(Locomotion.get()); + if (clsid == ClassID_HoverLocomotion && static_cast(whom)->Class->IsCanUnitRepair && NavCom == NULL) { NavCom = whom; } if (whom == NavCom) { @@ -5252,10 +5249,8 @@ void UnitClass::Assign_Destination(AbstractClass * target, bool immediate) * re-target the nearest reachable cell when driving rather than burrowing. */ if (target != NULL && Class->IsSubterranean && Locomotion->Is_Moving()) { - IPersistPtr persist(Locomotion); - CLSID clsid; - persist->GetClassID(&clsid); - if (clsid == CLSID_DriveLocomotion) { + ClassID const clsid = Locomotion_Class_ID(Locomotion.get()); + if (clsid == ClassID_DriveLocomotion) { NavQueue.Add_Head(target); RouteQueue.Clear(); CellClass * tcell = Get_Target_Cell_Ptr(); @@ -5346,10 +5341,8 @@ void UnitClass::Assign_Destination(AbstractClass * target, bool immediate) * (Mirrors BuildingClass weapons-factory exit, building.cpp:6236-6251.) */ if (target != NULL && !Locomotion->Is_Moving()) { - IPersistPtr persist(Locomotion); - CLSID clsid; - persist->GetClassID(&clsid); - if (clsid == CLSID_TunnelLocomotion && Get_Height_AGL() == 0) { + ClassID const clsid = Locomotion_Class_ID(Locomotion.get()); + if (clsid == ClassID_TunnelLocomotion && Get_Height_AGL() == 0) { Coord tc = target->Center_Coord(); int gl = Map.Get_Height_GL(tc); if (tc.Z < gl) tc.Z = gl; @@ -5370,16 +5363,16 @@ void UnitClass::Assign_Destination(AbstractClass * target, bool immediate) } if (doswap) { - IPiggybackPtr piggy(Locomotion); + IPiggyback * piggy = Piggyback_Of(Locomotion.get()); if (piggy != NULL && piggy->Is_Piggybacking()) { - piggy->End_Piggyback(&Locomotion); + Locomotion = piggy->End_Piggyback(); } - ILocomotionPtr walk(CLSID_DriveLocomotion); + std::unique_ptr walk = Create_Locomotor(ClassID_DriveLocomotion); walk->Link_To_Object(this); - piggy = IPiggybackPtr(walk); + piggy = Piggyback_Of(walk.get()); if (piggy != NULL) { - piggy->Begin_Piggyback(Locomotion); - Locomotion = walk; + piggy->Begin_Piggyback(std::move(Locomotion)); + Locomotion = std::move(walk); Locomotion->Force_New_Slope(Map[Get_Coord()].Ramp); } } @@ -6045,8 +6038,8 @@ bool UnitClass::Ready_To_Commence(void) /// again once that identity has arrived. /// /// The stream to read this unit from. -/// Returns with S_OK if the unit was read successfully. -HRESULT STDMETHODCALLTYPE UnitClass::Load(IStream *stream) +/// bool; Was the record read whole? +bool UnitClass::Load(SaveStreamClass & stream) { TargetTracker.Remove_Index(Fetch_ID()); return(BASECLASS::Load(stream)); @@ -6659,16 +6652,9 @@ bool UnitClass::Is_Immobilized(void) const } -/// -/// Fetches the class identifier used by the save game persistence system. -/// -/// Pointer to the buffer to fill in with the class identifier. -/// Returns with S_OK, or E_POINTER if no buffer was supplied. -HRESULT STDMETHODCALLTYPE UnitClass::GetClassID(CLSID * retval) +ClassID UnitClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_UnitClass; - return(S_OK); + return(ClassID_UnitClass); } diff --git a/code/unit.h b/code/unit.h index 98c0ca05e..bf2d13086 100644 --- a/code/unit.h +++ b/code/unit.h @@ -136,8 +136,8 @@ class UnitClass : public FootClass UnitClass(UnitTypeClass const * type = NULL, HouseClass * house = NULL); virtual ~UnitClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; - virtual HRESULT STDMETHODCALLTYPE Load(IStream * stream) override; + virtual ClassID Class_ID(void) const override; + virtual bool Load(SaveStreamClass & stream) override; virtual void Serialize(SaveStreamClass & stream) override; virtual void Post_Load(void) override; diff --git a/code/unittype.cpp b/code/unittype.cpp index 00723fb19..ecbe34b95 100644 --- a/code/unittype.cpp +++ b/code/unittype.cpp @@ -45,7 +45,6 @@ * UnitTypeClass::operator new -- Allocates an object from the unit type class heap. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "unittype.h" @@ -495,15 +494,9 @@ int UnitTypeClass::Repair_Step(void) const } -/// -/// Fetches the persistent class identifier for the unit type. -/// -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE UnitTypeClass::GetClassID(CLSID * retval) +ClassID UnitTypeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_UnitTypeClass; - return(S_OK); + return(ClassID_UnitTypeClass); } diff --git a/code/unittype.h b/code/unittype.h index b5b5f63d9..acfea5043 100644 --- a/code/unittype.h +++ b/code/unittype.h @@ -313,7 +313,7 @@ class UnitTypeClass : public TechnoTypeClass UnitTypeClass(char const * ininame = NULL); virtual ~UnitTypeClass() override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; virtual void Post_Load(void) override; diff --git a/code/utf8.cpp b/code/utf8.cpp index b3aab4473..53cea240a 100644 --- a/code/utf8.cpp +++ b/code/utf8.cpp @@ -115,7 +115,15 @@ int Best_Fit_Index(unsigned page, short * cache, char32_t code) wchar_t wide = (wchar_t)code; char narrow = 0; BOOL defaulted = FALSE; +#ifdef _WIN32 int written = WideCharToMultiByte(page, 0, &wide, 1, &narrow, 1, NULL, &defaulted); +#else + // No host code page service outside Windows, so nothing above ASCII maps and the + // caller falls back to its own substitute glyph. + (void)page; + (void)wide; + int written = 0; +#endif unsigned char byte = (unsigned char)narrow; slot = (written == 1 && !defaulted && byte >= 0x20 && byte != 0x7F) ? (short)byte : (short)-1; } diff --git a/code/vanim.cpp b/code/vanim.cpp index ea0b2f8d0..afa32b025 100644 --- a/code/vanim.cpp +++ b/code/vanim.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "vanim.h" @@ -546,18 +545,9 @@ void VoxelAnimClass::Compute_CRC(CRCEngine & crc) const } -/// -/// Fetches the class identifier used to persist this object. -/// The save system writes this identifier ahead of the object data so that the loader -/// knows what kind of object to reconstruct. -/// -/// Pointer to the buffer that will receive the class identifier. -/// Returns with S_OK, or E_POINTER if no buffer was supplied. -HRESULT STDMETHODCALLTYPE VoxelAnimClass::GetClassID(CLSID * retval) +ClassID VoxelAnimClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_VoxelAnimClass; - return(S_OK); + return(ClassID_VoxelAnimClass); } diff --git a/code/vanim.h b/code/vanim.h index d7e9f9e4e..8d8af415e 100644 --- a/code/vanim.h +++ b/code/vanim.h @@ -33,7 +33,7 @@ class VoxelAnimClass : public ObjectClass, public BounceClass VoxelAnimClass(VoxelAnimTypeClass const * type, Coord const & coord, HouseClass * house = NULL); virtual ~VoxelAnimClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/vanimtype.cpp b/code/vanimtype.cpp index a2f6646b3..0af776362 100644 --- a/code/vanimtype.cpp +++ b/code/vanimtype.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "vanimtype.h" @@ -247,18 +246,9 @@ void VoxelAnimTypeClass::Compute_CRC(CRCEngine & crc) const } -/// -/// Fetches the class identifier of this object. -/// This routine is used by the persistence system to record what kind of object was -/// written, so that the right class can be created when the save game is loaded. -/// -/// Pointer to the class identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE VoxelAnimTypeClass::GetClassID(CLSID * retval) +ClassID VoxelAnimTypeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_VoxelAnimTypeClass; - return(S_OK); + return(ClassID_VoxelAnimTypeClass); } diff --git a/code/vanimtype.h b/code/vanimtype.h index aa01e9e9a..9abc1adfe 100644 --- a/code/vanimtype.h +++ b/code/vanimtype.h @@ -32,7 +32,7 @@ class VoxelAnimTypeClass : public ObjectTypeClass VoxelAnimTypeClass(char const * ininame = NULL); ~VoxelAnimTypeClass(void); - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; virtual void Post_Load(void) override; diff --git a/code/vector.h b/code/vector.h index f608fa4c0..7466a1fae 100644 --- a/code/vector.h +++ b/code/vector.h @@ -123,8 +123,7 @@ class VectorClass stream.Serialize(count); if (stream.Is_Loading()) { - if (count < 0) { - stream.Fail(); + if (!stream.Fits(count, (std::is_arithmetic_v || std::is_enum_v) ? sizeof(T) : 1)) { return; } Clear(); @@ -518,8 +517,7 @@ class DynamicVectorClass : public VectorClass stream.Serialize(count); if (stream.Is_Loading()) { - if (count < 0) { - stream.Fail(); + if (!stream.Fits(count, (std::is_arithmetic_v || std::is_enum_v) ? sizeof(T) : 1)) { return; } Clear(); diff --git a/code/vein.cpp b/code/vein.cpp index 26ae0a4f8..678663606 100644 --- a/code/vein.cpp +++ b/code/vein.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "vein.h" @@ -881,19 +880,21 @@ void VeinholeMonsterClass::Remove_Dead(void) /// growth records and handed to the swizzler and the target tracker. /// /// bool; Were all the monsters read successfully? -bool VeinholeMonsterClass::Load_All(IStream * stream) +bool VeinholeMonsterClass::Load_All(SaveStreamClass & stream) { Reset(); int cell_count = Map_Cell_Count(); int monster_count; - if (FAILED(stream->Read(&monster_count, sizeof(monster_count), NULL))) { + stream.Serialize(monster_count); + if (stream.Was_Error()) { return(false); } GlobalGrowthState = new bool[cell_count]; - if (FAILED(stream->Read(GlobalGrowthState, cell_count, NULL))) { + stream.Serialize_Bytes(GlobalGrowthState, (int)(cell_count)); + if (stream.Was_Error()) { return(false); } @@ -905,30 +906,32 @@ bool VeinholeMonsterClass::Load_All(IStream * stream) */ VeinholeMonsterClass * monster = new VeinholeMonsterClass(); - SwizzleIDType id; - if (FAILED(stream->Read(&id, sizeof(id), NULL))) { + SwizzleIDType id = 0; + stream.Serialize(id); + if (stream.Was_Error()) { return(false); } Swizzler.Here_I_Am(id, monster); - SaveStreamClass savestream(stream, SaveStreamClass::MODE_LOAD); - savestream.Set_Context(typeid(*monster).name(), id); - monster->Serialize(savestream); - if (FAILED(savestream.Result())) { + stream.Set_Context(typeid(*monster).name(), id); + monster->Serialize(stream); + if (stream.Was_Error()) { return(false); } - if (FAILED(stream->Read(monster->GrowthState, cell_count, NULL))) { + stream.Serialize_Bytes(monster->GrowthState, (int)(cell_count)); + if (stream.Was_Error()) { return(false); } - if (FAILED(stream->Read(monster->GrowthNodes, sizeof(CellNode) * Rule->MaxVeinholeGrowth, NULL))) { + stream.Serialize_Bytes(monster->GrowthNodes, (int)(sizeof(CellNode) * Rule->MaxVeinholeGrowth)); + if (stream.Was_Error()) { return(false); } - monster->GrowthQueue->Serialize(savestream, monster->GrowthNodes); - if (FAILED(savestream.Result())) { + monster->GrowthQueue->Serialize(stream, monster->GrowthNodes); + if (stream.Was_Error()) { return(false); } @@ -972,40 +975,44 @@ void VeinholeMonsterClass::Serialize(SaveStreamClass & stream) /// with its vein growth records so that growth can pick up where it left off. /// /// bool; Were all the monsters written successfully? -bool VeinholeMonsterClass::Save_All(IStream * stream) +bool VeinholeMonsterClass::Save_All(SaveStreamClass & stream) { int monster_count = VeinholeMonsters.Count(); - if (FAILED(stream->Write(&monster_count, sizeof(monster_count), NULL))) { + stream.Serialize(monster_count); + if (stream.Was_Error()) { return(false); } int cell_count = Map_Cell_Count(); - if (FAILED(stream->Write(GlobalGrowthState, cell_count, NULL))) { + stream.Serialize_Bytes(GlobalGrowthState, (int)(cell_count)); + if (stream.Was_Error()) { return(false); } for (int i = 0; i < monster_count; i++) { SwizzleIDType id = Swizzler.ID_Of(VeinholeMonsters[i]); - if (FAILED(stream->Write(&id, sizeof(id), NULL))) { + stream.Serialize(id); + if (stream.Was_Error()) { return(false); } - SaveStreamClass savestream(stream, SaveStreamClass::MODE_SAVE); - VeinholeMonsters[i]->Serialize(savestream); - if (FAILED(savestream.Result())) { + VeinholeMonsters[i]->Serialize(stream); + if (stream.Was_Error()) { return(false); } - if (FAILED(stream->Write(VeinholeMonsters[i]->GrowthState, cell_count, NULL))) { + stream.Serialize_Bytes(VeinholeMonsters[i]->GrowthState, (int)(cell_count)); + if (stream.Was_Error()) { return(false); } - if (FAILED(stream->Write(VeinholeMonsters[i]->GrowthNodes, sizeof(CellNode) * Rule->MaxVeinholeGrowth, NULL))) { + stream.Serialize_Bytes(VeinholeMonsters[i]->GrowthNodes, (int)(sizeof(CellNode) * Rule->MaxVeinholeGrowth)); + if (stream.Was_Error()) { return(false); } - VeinholeMonsters[i]->GrowthQueue->Serialize(savestream, VeinholeMonsters[i]->GrowthNodes); - if (FAILED(savestream.Result())) { + VeinholeMonsters[i]->GrowthQueue->Serialize(stream, VeinholeMonsters[i]->GrowthNodes); + if (stream.Was_Error()) { return(false); } } @@ -1082,15 +1089,7 @@ void VeinholeMonsterClass::Reduce_Veins_At(CellClass * cellptr) } -/// -/// Fetches the class identifier used by the save game system. -/// This routine is called by the persistence layer so that it knows which class to -/// recreate when the saved game is read back in. -/// -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE VeinholeMonsterClass::GetClassID(CLSID * retval) +ClassID VeinholeMonsterClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_VeinholeMonsterClass; - return(S_OK); + return(ClassID_VeinholeMonsterClass); } diff --git a/code/vein.h b/code/vein.h index 92106eb5c..85c456eed 100644 --- a/code/vein.h +++ b/code/vein.h @@ -34,7 +34,7 @@ class VeinholeMonsterClass : public ObjectClass VeinholeMonsterClass(Cell const & cell); ~VeinholeMonsterClass(void); - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; /*--------------------------------------------------------------------- ** Member function prototypes. @@ -61,8 +61,8 @@ class VeinholeMonsterClass : public ObjectClass void Clear_Growth(void); void Destroy_Monster(void); static void Remove_Dead(void); - static bool Load_All(IStream * stream); - static bool Save_All(IStream * stream); + static bool Load_All(SaveStreamClass & stream); + static bool Save_All(SaveStreamClass & stream); void Reduce_Veins_At(CellClass * cellptr); virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/version.cpp b/code/version.cpp index 04d9c3295..a751babd8 100644 --- a/code/version.cpp +++ b/code/version.cpp @@ -40,6 +40,14 @@ * VersionClass::Max_Version -- returns highest version # to connect to * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ +#include "opents_build.h" + +#ifdef __APPLE__ +#include +#elif !defined(_WIN32) +#include +#endif + #include "always.h" #include "version.h" @@ -516,7 +524,14 @@ char const * Version_Name(void) empty = false; - if (GetModuleFileName(ProgramInstance, filename, sizeof(filename)) > 0) { +#ifndef _WIN32 + // No version resource outside Windows, so the generated build stamp answers instead. + (void)size; (void)block; (void)translate_len; (void)handle; (void)translate; + (void)query; (void)filename; + strncpy(buffer, OPENTS_VERSION_DISPLAY, sizeof(buffer) - 1); + buffer[sizeof(buffer) - 1] = '\0'; +#else + if (Program_File_Name(filename, sizeof(filename))) { handle = 1; size = GetFileVersionInfoSize(filename, &handle); if (size > 0) { @@ -535,7 +550,39 @@ char const * Version_Name(void) delete [] block; } } +#endif } return(buffer); } + + +/// +/// Names the file the running program was loaded from. +/// +/// bool; Was a path written into the buffer? +bool Program_File_Name(char * buffer, unsigned int length) +{ + if (buffer == NULL || length == 0) { + return(false); + } + +#ifdef _WIN32 + return(GetModuleFileName(ProgramInstance, buffer, length) > 0); +#elif defined(__APPLE__) + uint32_t size = (uint32_t)length; + if (_NSGetExecutablePath(buffer, &size) != 0) { + buffer[0] = '\0'; + return(false); + } + return(true); +#else + ssize_t const written = readlink("/proc/self/exe", buffer, length - 1); + if (written <= 0) { + buffer[0] = '\0'; + return(false); + } + buffer[written] = '\0'; + return(true); +#endif +} diff --git a/code/version.h b/code/version.h index 2dc230e84..e205dd4d9 100644 --- a/code/version.h +++ b/code/version.h @@ -154,5 +154,6 @@ class VersionClass { }; char const * Version_Name(void); +bool Program_File_Name(char * buffer, unsigned int length); /************************** end of version.h *******************************/ diff --git a/code/video.cpp b/code/video.cpp index f262d5b5d..dd8e4581a 100644 --- a/code/video.cpp +++ b/code/video.cpp @@ -15,14 +15,18 @@ #include "video.h" +#include "drawhelp.h" + #include "_surface.h" #include "bgfxbackend.h" #include "dbgprint.h" #include "dsurface.h" #include "globals.h" #include "goptions.h" +#include "hostclock.h" #include "misc.h" #include "surface.h" +#include "ui/uishell.h" #include "wincursor.h" #include @@ -46,7 +50,8 @@ static VideoScaleInfo _ScaleInfo; // Set whenever the visible surface is written to, and cleared once that frame has been // presented. A frame that is skipped for pacing stays marked, so the next present shows -// the newest content rather than a stale one. +// the newest content rather than a stale one. The shell keeps a second flag for the +// overlays, and a present happens when either is set. static bool _FrameIsDirty = false; static unsigned int _LastPresentTime = 0; static unsigned int _PresentInterval = 16; @@ -166,8 +171,17 @@ bool Video_Init(NativeWindow const & window, int drawablewidth, int drawableheig return(false); } + // The blend masks follow how the display surface packs its pixels. Video_Set_Mode runs + // only when the player changes resolution, so an ordinary session never reached it and + // every blend was left masking with zero, which draws black. + Prepare_Draw_Resources(); + Update_Scale_Info(); Update_Present_Interval(refreshrate); + + // The overlays draw on the renderer this just started, so the shell follows it and is + // torn down before it. A shell that cannot start leaves the game running without one. + UI_Init(); return(true); } @@ -182,6 +196,7 @@ void Video_Shutdown(void) } Win_Cursor_Shutdown(); + UI_Shutdown(); Backend_Shutdown(); _Initialized = false; _FrameIsDirty = false; @@ -209,8 +224,13 @@ bool Video_Set_Mode(int width, int height) VideoModeWidth = width; VideoModeHeight = height; + // The blend masks follow how the display surface packs its pixels, so they are built + // where that becomes known. OwnerDraw's first subclassed control used to do this. + Prepare_Draw_Resources(); + Update_Scale_Info(); Win_Cursor_Refresh(); + UI_On_Resize(); _FrameIsDirty = true; return(true); } @@ -230,6 +250,7 @@ void Video_On_Resize(int drawablewidth, int drawableheight) Backend_On_Resize(drawablewidth, drawableheight); Update_Scale_Info(); Win_Cursor_Refresh(); + UI_On_Resize(); Video_Mark_Dirty(); } @@ -273,12 +294,16 @@ void Video_Present(void) return; } + // The frame is uploaded only when the game drew something. A present made to redraw an + // overlay alone costs a few draw calls rather than the whole frame's pixels. _Presenting = true; - Backend_Present(pixels, surface->Stride(), _ScaleInfo.DestX, _ScaleInfo.DestY, _ScaleInfo.DestWidth, _ScaleInfo.DestHeight, Backend_Scale_Mode()); + Backend_Present(pixels, surface->Stride(), _ScaleInfo.DestX, _ScaleInfo.DestY, _ScaleInfo.DestWidth, _ScaleInfo.DestHeight, Backend_Scale_Mode(), _FrameIsDirty); + UI_Render_Overlay(); + Backend_End_Frame(); _Presenting = false; _FrameIsDirty = false; - _LastPresentTime = timeGetTime(); + _LastPresentTime = Host_Milliseconds(); } @@ -290,11 +315,11 @@ void Video_Present(void) /// void Video_Present_If_Dirty(void) { - if (!_FrameIsDirty) { + if (!_FrameIsDirty && !UI_Overlay_Is_Dirty()) { return; } - unsigned int now = timeGetTime(); + unsigned int now = Host_Milliseconds(); if ((now - _LastPresentTime) < _PresentInterval) { return; } diff --git a/code/visualc.h b/code/visualc.h index 9fa9a9126..f5408a960 100644 --- a/code/visualc.h +++ b/code/visualc.h @@ -116,6 +116,17 @@ // Single precision pi, for the float paths that would otherwise round M_PI at every use. #define M_FPI 3.141592654f +#endif + +// Spellings no C library supplies, so they are needed whatever the compiler. +#ifndef M_SQRT_2 +#define M_SQRT_2 0.707106781186547524401 +#endif +#ifndef M_FPI +#define M_FPI 3.141592654f +#endif + + /* ** Macros to convert between degrees and radians */ @@ -134,6 +145,3 @@ #ifndef DEG_TO_RADF #define DEG_TO_RADF(x) (((float)x)*M_PI/180.0f) #endif - - -#endif diff --git a/code/vocini.cpp b/code/vocini.cpp index 0854e3350..2ae9215a2 100644 --- a/code/vocini.cpp +++ b/code/vocini.cpp @@ -11,6 +11,8 @@ // type. The grammar follows Yuri's Revenge, with defaults that keep the // shipped Tiberian Sun files playing as they did. +#include "always.h" + #include "voc.h" #include "dbgprint.h" diff --git a/code/vqa.cpp b/code/vqa.cpp index bb892ddd4..e97b37b81 100644 --- a/code/vqa.cpp +++ b/code/vqa.cpp @@ -11,6 +11,7 @@ * disclaimers apply; see LICENSE.md. ******************************************************************************/ +#include "hostclock.h" #include "always.h" #include "vqa.h" @@ -529,7 +530,7 @@ int VQAClass::Play_VQA(int last_frame_to_play, bool nobreakout) if (sleeping == true) { if (!GameInFocus) { - Sleep((1000/30)); + Host_Sleep((1000/30)); continue; } else { sleeping = false; diff --git a/code/vqa.h b/code/vqa.h index d9b53a738..86337755a 100644 --- a/code/vqa.h +++ b/code/vqa.h @@ -19,6 +19,7 @@ #include "ccfile.h" +#include #include //========================================================================== diff --git a/code/vqalib/CMakeLists.txt b/code/vqalib/CMakeLists.txt index 5a77067fc..8a162b337 100644 --- a/code/vqalib/CMakeLists.txt +++ b/code/vqalib/CMakeLists.txt @@ -26,11 +26,11 @@ set_target_properties(VQALib PROPERTIES # binary and have to agree on the runtime library and the floating-point model. target_compile_options(VQALib PRIVATE ${OPENTS_COMPILE_OPTIONS}) -target_compile_definitions(VQALib PRIVATE - WIN32 - _WINDOWS - NOMINMAX -) +target_compile_definitions(VQALib PRIVATE NOMINMAX) + +if(WIN32) + target_compile_definitions(VQALib PRIVATE WIN32 _WINDOWS) +endif() # Callers reach the vqaplay.h family through the library, while the player itself reaches # back into the engine for ahandle.h, which owns the sound handle it plays through. diff --git a/code/vqalib/audio.cpp b/code/vqalib/audio.cpp index 6212cb77e..23a7fd6b4 100644 --- a/code/vqalib/audio.cpp +++ b/code/vqalib/audio.cpp @@ -131,8 +131,8 @@ long VQA_OpenAudio(VQAHandleP *vqap) params.SampleRate = vqap->SampleRate; params.Channels = vqap->Channels; params.BitsPerSample = vqap->BitsPerSample; - params.Callback1 = VQA_AudioFillCallback; - params.Callback2 = VQA_AudioDoneCallback; + params.Callback1 = (void *)VQA_AudioFillCallback; + params.Callback2 = (void *)VQA_AudioDoneCallback; rc = (long)vqap->Config.AudioHandler((VQAHandle *)vqap, VQAAUDIO_OPEN, ¶ms, sizeof(params)); if (rc >= VQAERR_OK || rc == VQAERR_NONE) { diff --git a/code/vqalib/cmp.h b/code/vqalib/cmp.h index 954c57f79..06c064ab9 100644 --- a/code/vqalib/cmp.h +++ b/code/vqalib/cmp.h @@ -16,11 +16,12 @@ #pragma once +#include #include -#if defined(__WATCOMC__) || defined(_MSC_VER) +// The ADPCM decoder state is packed on every compiler rather than only on the two the +// original build used, so one build cannot disagree with another about its layout. #pragma pack(push,1) -#endif struct _VQA_SOS_COMPRESS_INFO @@ -33,6 +34,9 @@ struct _VQA_SOS_COMPRESS_INFO typedef _VQA_SOS_COMPRESS_INFO VQASOS; +static_assert(sizeof(VQASOS) == 12, "ADPCM decoder state layout changed"); +static_assert(offsetof(VQASOS, dwPredicted2) == 6, "ADPCM decoder state layout changed"); + extern "C" { void __cdecl VQA_sosCODECInitStream(_VQA_SOS_COMPRESS_INFO *); void __cdecl VQA_sosCODECDecompressData(void *src, void *dst, unsigned short wBitSize, unsigned short wChannels, uint32_t dwUnCompSize, _VQA_SOS_COMPRESS_INFO *sosinfo); @@ -40,8 +44,6 @@ void __cdecl VQA_sosCODECDecompressData(void *src, void *dst, unsigned short wBi //#define VQA_sosCODECDecompressData sosCODECDecompressData -#if defined(__WATCOMC__) || defined(_MSC_VER) #pragma pack(pop) -#endif #endif //VQACMP_H diff --git a/code/vqalib/dstream.cpp b/code/vqalib/dstream.cpp index 456462e0f..88a2ecd44 100644 --- a/code/vqalib/dstream.cpp +++ b/code/vqalib/dstream.cpp @@ -45,9 +45,30 @@ #include "vqaplayp.h" #include #include +#ifdef _WIN32 #include +#else +#include +#endif + +#ifndef O_BINARY +#define O_BINARY 0 +#endif #include +#ifndef _WIN32 +/// The MSVC runtime reports a descriptor's length without disturbing its position. +static long filelength(int handle) +{ + off_t const here = lseek(handle, 0, SEEK_CUR); + if (here < 0) { + return(-1); + } + off_t const end = lseek(handle, 0, SEEK_END); + lseek(handle, here, SEEK_SET); + return((long)end); +} +#endif intptr_t __cdecl Disk_VQA_Stream_Handler(VQAHandle *vqa, long action, void *buffer, long nbytes) { diff --git a/code/vqalib/loader.cpp b/code/vqalib/loader.cpp index 63200ec69..8bc8ac659 100644 --- a/code/vqalib/loader.cpp +++ b/code/vqalib/loader.cpp @@ -186,7 +186,9 @@ long VQA_LoadFrame(VQAHandleP *vqap, long flags) int scan_frame; int tocache; VQA_H_FUNC handler; - unsigned int rc; + // VQAERR_NONE is -1, so a narrower or unsigned holder loses it: it comes back as + // 0xFFFFFFFF from a long return and matches no error code the caller knows. + long rc; int val4; int fsize; VQA_H_FUNC oldhandler; @@ -301,13 +303,16 @@ long VQA_LoadFrame(VQAHandleP *vqap, long flags) */ #pragma pack(push,1) struct VQASN2J { - short index; - long predicted; - short index2; - long predicted2; + std::int16_t index; + std::int32_t predicted; + std::int16_t index2; + std::int32_t predicted2; }; #pragma pack(pop) +static_assert(sizeof(VQASN2J) == 12, "SN2J chunk layout changed"); +static_assert(offsetof(VQASN2J, predicted2) == 8, "SN2J chunk layout changed"); + long VQA_LoadFrame_Internal(VQAHandleP *vqap, long flags) { @@ -1267,7 +1272,7 @@ long VQA_SeekGroup(VQAHandleP *vqap, long framenum, long groupsize, VQABool prel int bytes_per_frame; int preload_frames; int covered_bytes; - unsigned int rc; + long rc; bool bool2; int loadflags; long group_end; @@ -3532,6 +3537,8 @@ long Load_SN2J(VQAHandleP *vqap, unsigned long iffsize) } data; #pragma pack(pop) + static_assert(sizeof(data) == 12, "SN2J chunk layout changed"); + #if(VQAVOC_ON && VQAAUDIO_ON) if (((config->OptionFlags & VQAOPTF_AUDIO) == 0) || (vqap->vocfh != -1) || (audio->Buffer == NULL)) { diff --git a/code/vqalib/vqafile.h b/code/vqalib/vqafile.h index 21e6bd07b..7b1a8927f 100644 --- a/code/vqalib/vqafile.h +++ b/code/vqalib/vqafile.h @@ -38,9 +38,12 @@ #include "iff.h" -#if defined(__WATCOMC__) || defined(_MSC_VER) +#include +#include + +// The structures below name bytes as a .vqa file stores them, so they are packed on every +// compiler rather than only on the two that the original build used. #pragma pack(push,1) -#endif /*--------------------------------------------------------------------------- * STRUCTURE DEFINITIONS AND RELATED DEFINES. @@ -99,7 +102,7 @@ typedef struct _VQAHeader { * expanded size when it is zero, so an old movie that leaves it blank * still allocates correctly. */ - unsigned long MaxCBSize; + std::uint32_t MaxCBSize; /* * Bytes of audio that must be loaded ahead of a seek target to prime the @@ -107,9 +110,15 @@ typedef struct _VQAHeader { * how many frames early to start reading. When the movie carries no * VQAHDF_SNDJUMP flag and this is zero, half a second is assumed. */ - unsigned long AudioPreload; + std::uint32_t AudioPreload; } VQAHeader; +// The VQHD chunk is 42 bytes on disk. MaxCBSize and AudioPreload were written as a 32-bit +// long by the original 32-bit build, so they stay 32 bits wide here. +static_assert(sizeof(VQAHeader) == 42, "VQHD chunk layout changed"); +static_assert(offsetof(VQAHeader, MaxCBSize) == 34, "VQHD chunk layout changed"); +static_assert(offsetof(VQAHeader, AudioPreload) == 38, "VQHD chunk layout changed"); + /* Version type. */ #define VQAHD_VER1 1 #define VQAHD_VER2 2 @@ -236,9 +245,7 @@ typedef struct _VQAHeader { #define ID_VPKZ MAKE_ID('V','P','K','Z') #define ID_VPDZ MAKE_ID('V','P','D','Z') -#if defined(__WATCOMC__) || defined(_MSC_VER) #pragma pack(pop) -#endif #endif /* VQAFILE_H */ diff --git a/code/vqalib/vqaplayp.h b/code/vqalib/vqaplayp.h index 655ae9b98..f156a42c0 100644 --- a/code/vqalib/vqaplayp.h +++ b/code/vqalib/vqaplayp.h @@ -108,10 +108,15 @@ extern char ReqTag[]; * size - Size of chunk. */ typedef struct _ChunkHeader { - unsigned long id; - unsigned long size; + std::uint32_t id; + std::uint32_t size; } ChunkHeader; +// The loader reads a chunk header straight off the file, so its two fields stay the 32-bit +// longs the format was written with. +static_assert(sizeof(ChunkHeader) == 8, "IFF chunk header layout changed"); +static_assert(offsetof(ChunkHeader, size) == 4, "IFF chunk header layout changed"); + /* ZAPHeader: ZAP audio compression header. NOTE: If the uncompressed size * and the compressed size are equal then the audio frame is RAW @@ -121,15 +126,19 @@ typedef struct _ChunkHeader { * CompSize - Compressed size in bytes. */ typedef struct _ZAPHeader { - unsigned short UnCompSize; - unsigned short CompSize; + std::uint16_t UnCompSize; + std::uint16_t CompSize; } ZAPHeader; +static_assert(sizeof(ZAPHeader) == 4, "ZAP audio header layout changed"); + typedef struct _VQAClipper { - unsigned long Width; - unsigned long Height; + std::uint32_t Width; + std::uint32_t Height; } VQAClipper; +static_assert(sizeof(VQAClipper) == 8, "CLIP chunk layout changed"); + /* VQACBNode: A circular list of codebook buffers, used by the load task. * If the data is compressed, it is loaded into the end of the diff --git a/code/walk.cpp b/code/walk.cpp index d3996e10b..3413e895d 100644 --- a/code/walk.cpp +++ b/code/walk.cpp @@ -27,6 +27,7 @@ #include "inline.h" #include "overtype.h" #include "rules.h" +#include "saveload.h" #include "savestream.h" #include "tactical.h" #include "tube.h" @@ -35,7 +36,6 @@ #include "layer.hh" - /// /// Constructs a walking locomotor. /// This is the locomotor used by infantry, who travel on foot between the sub-cell @@ -64,7 +64,7 @@ WalkLocomotionClass::~WalkLocomotionClass(void) /// Is the infantry traveling somewhere? /// /// bool; Does the infantry have somewhere it is trying to get to? -boolean STDMETHODCALLTYPE WalkLocomotionClass::Is_Moving(void) +bool WalkLocomotionClass::Is_Moving(void) { return(IsMoving); } @@ -76,7 +76,7 @@ boolean STDMETHODCALLTYPE WalkLocomotionClass::Is_Moving(void) /// merely under orders to travel but is standing still. /// /// bool; Is the infantry moving right now? -boolean STDMETHODCALLTYPE WalkLocomotionClass::Is_Moving_Now(void) +bool WalkLocomotionClass::Is_Moving_Now(void) { if (Is_Moving() && LinkedTo->Speed > 0 && HeadToCoord != COORD_NONE) { return(true); @@ -90,7 +90,7 @@ boolean STDMETHODCALLTYPE WalkLocomotionClass::Is_Moving_Now(void) /// /// Returns with the coordinate being traveled to, or COORD_NONE if the infantry has /// nowhere it needs to be. -Coord STDMETHODCALLTYPE WalkLocomotionClass::Destination(void) +Coord WalkLocomotionClass::Destination(void) { if (Is_Moving()) { return(DestinationCoord); @@ -104,7 +104,7 @@ Coord STDMETHODCALLTYPE WalkLocomotionClass::Destination(void) /// /// Returns with the immediate destination, or the current position if the infantry /// is not part way between spots. -Coord STDMETHODCALLTYPE WalkLocomotionClass::Head_To_Coord(void) +Coord WalkLocomotionClass::Head_To_Coord(void) { if (HeadToCoord != COORD_NONE) { return(HeadToCoord); @@ -119,7 +119,7 @@ Coord STDMETHODCALLTYPE WalkLocomotionClass::Head_To_Coord(void) /// infantry along its path. /// /// bool; Is the infantry still traveling somewhere? -boolean STDMETHODCALLTYPE WalkLocomotionClass::Process(void) +bool WalkLocomotionClass::Process(void) { IsProcessingMovement = true; Movement_AI(true); @@ -135,7 +135,7 @@ boolean STDMETHODCALLTYPE WalkLocomotionClass::Process(void) /// than beneath it. /// /// The coordinate to travel to, or COORD_NONE to clear the destination. -void STDMETHODCALLTYPE WalkLocomotionClass::Move_To(Coord to) +void WalkLocomotionClass::Move_To(Coord to) { if (LinkedTo->StunDuration <= 0) { DestinationCoord = to; @@ -158,7 +158,7 @@ void STDMETHODCALLTYPE WalkLocomotionClass::Move_To(Coord to) /// The step already under way is allowed to finish; it is the ultimate destination /// that is forgotten. /// -void STDMETHODCALLTYPE WalkLocomotionClass::Stop_Moving(void) +void WalkLocomotionClass::Stop_Moving(void) { DestinationCoord = COORD_NONE; if (HeadToCoord == COORD_NONE) { @@ -172,7 +172,7 @@ void STDMETHODCALLTYPE WalkLocomotionClass::Stop_Moving(void) /// Infantry snap around instantly, so there is no rotation to play out over time. /// /// The direction the infantry should face. -void STDMETHODCALLTYPE WalkLocomotionClass::Do_Turn(DirType dir) +void WalkLocomotionClass::Do_Turn(DirType dir) { LinkedTo->PrimaryFacing.Set(dir); } @@ -184,7 +184,7 @@ void STDMETHODCALLTYPE WalkLocomotionClass::Do_Turn(DirType dir) /// redirected without waiting for the current step to finish. /// /// The coordinate to step to, or COORD_NONE to abandon the step. -void STDMETHODCALLTYPE WalkLocomotionClass::Force_Immediate_Destination(Coord coord) +void WalkLocomotionClass::Force_Immediate_Destination(Coord coord) { HeadToCoord = coord; if (HeadToCoord == COORD_NONE && DestinationCoord == COORD_NONE) { @@ -608,25 +608,16 @@ bool WalkLocomotionClass::Mark_Head_To(Coord const & coord) } -/// -/// Fetches the class ID of this locomotor. -/// The persistence system uses this to recreate the correct locomotor when a saved -/// game is loaded. -/// -/// Pointer to the class ID to fill in. -/// Returns with S_OK if the class ID was fetched, otherwise E_POINTER. -HRESULT STDMETHODCALLTYPE WalkLocomotionClass::GetClassID(CLSID * retval) +ClassID WalkLocomotionClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_WalkLocomotion; - return(S_OK); + return(ClassID_WalkLocomotion); } /// /// Lists the members this walk locomotor carries. /// The locomotor this one was stacked on top of is a separate persistent object rather -/// than a member, so it still travels framed by OLE and is recreated as the class it was +/// than a member, so it travels as a record of its own and is recreated as the class it was /// saved as. /// /// The stream carrying the members. @@ -645,10 +636,9 @@ void WalkLocomotionClass::Serialize(SaveStreamClass & stream) if (haspiggy) { if (stream.Is_Saving()) { - IPersistStreamPtr persist(Piggybacker); - OleSaveToStream(persist, stream.Get_Stream()); + Save_Object(stream, Piggybacker.get()); } else { - OleLoadFromStream(stream.Get_Stream(), IID_ILocomotion, (LPVOID *)&Piggybacker); + Piggybacker = Load_Locomotor(stream); } } } @@ -658,56 +648,26 @@ void WalkLocomotionClass::Serialize(SaveStreamClass & stream) /// Fetches the display layer that walking objects belong in. /// /// Returns with the layer that objects using this locomotor render into. -LayerType STDMETHODCALLTYPE WalkLocomotionClass::In_Which_Layer(void) +LayerType WalkLocomotionClass::In_Which_Layer(void) { return(LAYER_GROUND); } -/// -/// Fetches an interface pointer from this locomotor. -/// This routine extends the base locomotor with the piggyback interface. -/// -/// The interface identifier being asked for. -/// Pointer to the interface pointer to fill in. -/// Returns with S_OK if the interface was supplied, otherwise E_NOINTERFACE. -HRESULT STDMETHODCALLTYPE WalkLocomotionClass::QueryInterface(REFIID riid, LPVOID * ppvObject) -{ - HRESULT result = BASECLASS::QueryInterface(riid, ppvObject); - - if (result == E_NOINTERFACE) { - if (riid == IID_IPiggyback) { - *ppvObject = (IPiggyback*)this; - } - if (*ppvObject == NULL) { - result = E_NOINTERFACE; - } else { - AddRef(); - result = S_OK; - } - } - return(result); -} - - /// /// Attaches a piggybacking locomotor to this one. /// This routine is used when some temporary means of travel, such as being carried /// along, must take over from ordinary walking. /// -/// The locomotor that will ride along on this one. -/// Returns with S_OK if the locomotor was attached, or E_FAIL if one is already -/// piggybacking. -HRESULT STDMETHODCALLTYPE WalkLocomotionClass::Begin_Piggyback(ILocomotion * pointer) +/// The locomotor that is to take over the unit. +/// bool; Was the locomotor taken on? One already carrying a locomotor refuses. +bool WalkLocomotionClass::Begin_Piggyback(std::unique_ptr carried) { - if (pointer == NULL) { - return(E_POINTER); - } - if (Piggybacker == NULL) { - Piggybacker = pointer; - return(S_OK); + if (carried == NULL || Piggybacker != NULL) { + return(false); } - return(E_FAIL); + Piggybacker = std::move(carried); + return(true); } @@ -715,20 +675,10 @@ HRESULT STDMETHODCALLTYPE WalkLocomotionClass::Begin_Piggyback(ILocomotion * poi /// Ends the piggyback session and hands back the locomotor that was riding along. /// Ownership of the piggybacking locomotor passes to the caller. /// -/// Pointer to the locomotor pointer to fill in. -/// Returns with S_OK if a piggybacking locomotor was handed back, or S_FALSE if -/// there was none. -HRESULT STDMETHODCALLTYPE WalkLocomotionClass::End_Piggyback(ILocomotion ** pointer) +/// Returns with the locomotor that was riding, or nothing when none was. +std::unique_ptr WalkLocomotionClass::End_Piggyback(void) { - if (pointer == NULL) { - return(E_POINTER); - } - if (Piggybacker != NULL) { - *pointer = Piggybacker; - Piggybacker.Detach(); - return(S_OK); - } - return(S_FALSE); + return(std::move(Piggybacker)); } @@ -738,7 +688,7 @@ HRESULT STDMETHODCALLTYPE WalkLocomotionClass::End_Piggyback(ILocomotion ** poin /// not resumed part way through a step. /// /// bool; Is it safe to end the piggyback? -boolean STDMETHODCALLTYPE WalkLocomotionClass::Is_Ok_To_End(void) +bool WalkLocomotionClass::Is_Ok_To_End(void) { if (!Is_Moving() && Piggybacker != NULL && !IsProcessingMovement) { return(true); @@ -747,42 +697,13 @@ boolean STDMETHODCALLTYPE WalkLocomotionClass::Is_Ok_To_End(void) } -/// -/// Fetches the class ID of whichever locomotor is in charge. -/// This routine reports the piggybacking locomotor's identity when one has taken -/// over, otherwise it identifies this walking locomotor. -/// -/// Pointer to the class ID to fill in. -/// Returns with S_OK if the class ID was fetched, otherwise an error code. -HRESULT STDMETHODCALLTYPE WalkLocomotionClass::Piggyback_CLSID(GUID * classid) -{ - if (classid == NULL) { - return(E_POINTER); - } - - if (Piggybacker != NULL) { - IPersistPtr ptr(Piggybacker); - if (ptr == NULL) { - return(E_FAIL); - } - return(ptr->GetClassID(classid)); - } - - IPersistPtr ptr(this); - if (ptr == NULL) { - return(E_FAIL); - } - return(ptr->GetClassID(classid)); -} - - /// /// Releases the sub-cell spot that this infantry has reserved. /// This routine is called when the infantry is being lifted off the map so that the /// spot it had claimed becomes available to others again. /// /// The occupancy marking operation being performed. -void STDMETHODCALLTYPE WalkLocomotionClass::Mark_All_Occupation_Bits(int mark) +void WalkLocomotionClass::Mark_All_Occupation_Bits(int mark) { if (mark == MARK_UP) { LinkedTo->Clear_Occupy_Bit(Head_To_Coord()); @@ -797,7 +718,7 @@ void STDMETHODCALLTYPE WalkLocomotionClass::Mark_All_Occupation_Bits(int mark) /// /// The coordinate to test the immediate destination against. /// bool; Is the infantry walking to that spot? -boolean STDMETHODCALLTYPE WalkLocomotionClass::Is_Moving_Here(Coord to) +bool WalkLocomotionClass::Is_Moving_Here(Coord to) { Coord headto = Head_To_Coord(); if (headto.As_Cell() == Coord(to).As_Cell() && abs(headto.Z - to.Z) <= LEVEL_LEPTON_H) { @@ -813,7 +734,7 @@ boolean STDMETHODCALLTYPE WalkLocomotionClass::Is_Moving_Here(Coord to) /// merely holds orders to travel but has yet to take a step. /// /// bool; Is the infantry really moving at this moment? -boolean STDMETHODCALLTYPE WalkLocomotionClass::Is_Really_Moving_Now(void) +bool WalkLocomotionClass::Is_Really_Moving_Now(void) { return(IsReallyMoving); } diff --git a/code/walk.h b/code/walk.h index 9ea0db2b3..f490df50f 100644 --- a/code/walk.h +++ b/code/walk.h @@ -16,6 +16,8 @@ #include "ipiggy.h" #include "loco.h" +#include + class WalkLocomotionClass : public LocomotionClass, public IPiggyback { @@ -29,34 +31,30 @@ class WalkLocomotionClass : public LocomotionClass, public IPiggyback WalkLocomotionClass(void); virtual ~WalkLocomotionClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; - virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID * ppvObject) override; - virtual ULONG STDMETHODCALLTYPE AddRef(void) override {return(BASECLASS::AddRef());} - virtual ULONG STDMETHODCALLTYPE Release(void) override {return(BASECLASS::Release());} - - virtual HRESULT STDMETHODCALLTYPE Begin_Piggyback(ILocomotion * pointer) override; - virtual HRESULT STDMETHODCALLTYPE End_Piggyback(ILocomotion ** pointer) override; - virtual boolean STDMETHODCALLTYPE Is_Ok_To_End(void) override; - virtual HRESULT STDMETHODCALLTYPE Piggyback_CLSID(GUID * classid) override; - virtual boolean STDMETHODCALLTYPE Is_Piggybacking(void) override {return(Piggybacker != NULL);} - - virtual boolean STDMETHODCALLTYPE Is_Moving(void) override; - virtual Coord STDMETHODCALLTYPE Destination(void) override; - virtual Coord STDMETHODCALLTYPE Head_To_Coord(void) override; - virtual boolean STDMETHODCALLTYPE Process(void) override; - virtual void STDMETHODCALLTYPE Move_To(Coord to) override; - virtual void STDMETHODCALLTYPE Stop_Moving(void) override; - virtual void STDMETHODCALLTYPE Do_Turn(DirType dir) override; - virtual LayerType STDMETHODCALLTYPE In_Which_Layer(void) override; - virtual void STDMETHODCALLTYPE Force_Immediate_Destination(Coord coord) override; - virtual boolean STDMETHODCALLTYPE Is_Moving_Now(void) override; - virtual void STDMETHODCALLTYPE Mark_All_Occupation_Bits(int mark) override; - virtual boolean STDMETHODCALLTYPE Is_Moving_Here(Coord to) override; - virtual boolean STDMETHODCALLTYPE Is_Really_Moving_Now(void) override; - virtual void STDMETHODCALLTYPE Stop_Movement_Animation(void) override {IsReallyMoving = false;}; + + virtual bool Begin_Piggyback(std::unique_ptr carried) override; + virtual std::unique_ptr End_Piggyback(void) override; + virtual bool Is_Ok_To_End(void) override; + virtual bool Is_Piggybacking(void) override {return(Piggybacker != NULL);} + + virtual bool Is_Moving(void) override; + virtual Coord Destination(void) override; + virtual Coord Head_To_Coord(void) override; + virtual bool Process(void) override; + virtual void Move_To(Coord to) override; + virtual void Stop_Moving(void) override; + virtual void Do_Turn(DirType dir) override; + virtual LayerType In_Which_Layer(void) override; + virtual void Force_Immediate_Destination(Coord coord) override; + virtual bool Is_Moving_Now(void) override; + virtual void Mark_All_Occupation_Bits(int mark) override; + virtual bool Is_Moving_Here(Coord to) override; + virtual bool Is_Really_Moving_Now(void) override; + virtual void Stop_Movement_Animation(void) override {IsReallyMoving = false;}; void Movement_AI(bool first_pass); bool Mark_Head_To(Coord const & coord); @@ -100,5 +98,5 @@ class WalkLocomotionClass : public LocomotionClass, public IPiggyback * temporarily -- a jump jet coming down to cover the last few cells on foot, say -- * and the suspended locomotor is handed back when the walk is finished. */ - ILocomotionPtr Piggybacker; + std::unique_ptr Piggybacker; }; diff --git a/code/warhead.cpp b/code/warhead.cpp index 11cd19645..39f8e8e02 100644 --- a/code/warhead.cpp +++ b/code/warhead.cpp @@ -35,7 +35,6 @@ * WarheadTypeClass::operator new -- Allocate a warhead object from the special heap. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "warhead.h" @@ -248,18 +247,9 @@ void WarheadTypeClass::Compute_CRC(CRCEngine &crc) const } -/// -/// Fetches the class identifier of this object. -/// This routine is part of the persistence interface. The save code stores the identifier -/// so that the object can be recognized when the game is loaded back in. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE WarheadTypeClass::GetClassID(CLSID * retval) +ClassID WarheadTypeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_WarheadTypeClass; - return(S_OK); + return(ClassID_WarheadTypeClass); } diff --git a/code/warhead.h b/code/warhead.h index 1449ecd30..02e1a2a64 100644 --- a/code/warhead.h +++ b/code/warhead.h @@ -52,7 +52,7 @@ class WarheadTypeClass : public AbstractTypeClass WarheadTypeClass(char const * ininame = NULL); virtual ~WarheadTypeClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/wave.cpp b/code/wave.cpp index d2ae55f06..a9484d2ef 100644 --- a/code/wave.cpp +++ b/code/wave.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "wave.h" @@ -466,18 +465,9 @@ void WaveClass::Post_Load(void) } -/// -/// Fetches the class identifier of this object. -/// This routine is part of the persistence interface. The save game loader uses the -/// identifier to recreate the object as the right kind of class. -/// -/// Pointer to the buffer to store the class identifier in. -/// Returns with S_OK, or E_POINTER if no buffer was supplied. -HRESULT STDMETHODCALLTYPE WaveClass::GetClassID(CLSID * retval) +ClassID WaveClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_WaveClass; - return(S_OK); + return(ClassID_WaveClass); } diff --git a/code/wave.h b/code/wave.h index 8444e4dcc..d1d291d5c 100644 --- a/code/wave.h +++ b/code/wave.h @@ -25,7 +25,7 @@ class WaveClass : public ObjectClass WaveClass(void); virtual ~WaveClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; virtual void Post_Load(void) override; diff --git a/code/waypoint.cpp b/code/waypoint.cpp index a71d3c6f5..59d2f4fb3 100644 --- a/code/waypoint.cpp +++ b/code/waypoint.cpp @@ -7,7 +7,6 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -#define INCLUDE_COM #include "always.h" #include "waypoint.h" @@ -281,18 +280,9 @@ void WaypointPathClass::Compute_CRC(CRCEngine & crc) const } -/// -/// Fetches the class identifier of this object. -/// This routine is used by the persistence machinery, which records the identifier so that -/// it knows what kind of object to create when the game is loaded back. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE WaypointPathClass::GetClassID(CLSID * retval) +ClassID WaypointPathClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_WaypointPath; - return(S_OK); + return(ClassID_WaypointPath); } diff --git a/code/waypoint.h b/code/waypoint.h index ff7f11d10..5fdac3fb5 100644 --- a/code/waypoint.h +++ b/code/waypoint.h @@ -49,7 +49,7 @@ class WaypointPathClass : public AbstractClass WaypointPathClass(int index); virtual ~WaypointPathClass(void) override; - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; virtual void Serialize(SaveStreamClass & stream) override; diff --git a/code/wdtprops.cpp b/code/wdtprops.cpp index d4d5c0c9f..d4489b5c0 100644 --- a/code/wdtprops.cpp +++ b/code/wdtprops.cpp @@ -11,7 +11,6 @@ #include "data.h" #include "language/language.h" -#include "ownrdraw.h" #include "wdtnet.h" diff --git a/code/wdtsel.cpp b/code/wdtsel.cpp index b6f362eac..5a73d5a52 100644 --- a/code/wdtsel.cpp +++ b/code/wdtsel.cpp @@ -22,7 +22,7 @@ #include "msfont.h" #include "newmenu.h" #include "netshare.h" -#include "ownrdraw.h" +#include "drawhelp.h" #include "pcx.h" #include "session.h" #include "theme.h" @@ -1114,7 +1114,7 @@ bool WDT_Select_Campaign(Campaign * campaign, bool vq_anim) /// void Selection::Start(void) { - OwnerDraw::Capture_Mouse(); + Release_Pointer_To_Host(); if (ThemeName != NULL) { Theme.Play_Song(Theme.From_Name(ThemeName)); Theme.Set_Repeat(true); @@ -1144,7 +1144,7 @@ void Selection::End(void) AlternateSurface->Fill(0); Draw_Menu_Background(); Show_Mouse(); - OwnerDraw::Release_Mouse(); + Recapture_Pointer(); } diff --git a/code/weapon.cpp b/code/weapon.cpp index 697c9e14d..736582178 100644 --- a/code/weapon.cpp +++ b/code/weapon.cpp @@ -39,7 +39,6 @@ * WeaponTypeClass::Allowed_Threats -- Determine what threats this weapon can address. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#define INCLUDE_COM #include "always.h" #include "weapon.h" @@ -363,18 +362,9 @@ void WeaponTypeClass::Compute_CRC(CRCEngine &crc) const } -/// -/// Fetches the class identifier of this object. -/// This routine is used by the persistence machinery to recognize what kind of object it -/// is about to load back. -/// -/// Pointer to the identifier to fill in. -/// Returns with S_OK, or E_POINTER if no destination was supplied. -HRESULT STDMETHODCALLTYPE WeaponTypeClass::GetClassID(CLSID * retval) +ClassID WeaponTypeClass::Class_ID(void) const { - if (retval == NULL) return(E_POINTER); - *retval = CLSID_WeaponTypeClass; - return(S_OK); + return(ClassID_WeaponTypeClass); } diff --git a/code/weapon.h b/code/weapon.h index 22df6f3ed..3e1e01f76 100644 --- a/code/weapon.h +++ b/code/weapon.h @@ -60,7 +60,7 @@ class WeaponTypeClass : public AbstractTypeClass WeaponTypeClass(char const * ininame = NULL); ~WeaponTypeClass(void); - virtual HRESULT STDMETHODCALLTYPE GetClassID(CLSID * retval) override; + virtual ClassID Class_ID(void) const override; static WeaponType From_Name(char const * name); diff --git a/code/wincursor.cpp b/code/wincursor.cpp index 1f4a36975..cdb91cd1a 100644 --- a/code/wincursor.cpp +++ b/code/wincursor.cpp @@ -21,6 +21,7 @@ #include "win.h" #include "xmouse.h" +#include #include @@ -120,26 +121,43 @@ static HCURSOR Build_Cursor(ShapeSet const * shape, int frame, int hotx, int hot // what turns one into the other. unsigned short const * table = (unsigned short const *)MouseDrawer->Get_Translate_Table(); + bool const compressed = shape->Is_RLE_Compressed(frame); + unsigned char const * line = data; + for (int y = 0; y < rect.Height; y++) { - for (int x = 0; x < rect.Width; x++) { - unsigned char index = data[y * rect.Width + x]; + // A compressed line starts with its own byte length and then runs of pixels, where + // a zero introduces a count of transparent ones. + unsigned char const * source = compressed ? line + sizeof(unsigned short) : data + y * rect.Width; + int x = 0; + + while (x < rect.Width) { + + unsigned char index = *source++; + if (index == 0) { + x += compressed ? *source++ : 1; continue; } unsigned short pixel = table[index]; - unsigned long red = ((pixel >> 11) & 0x1F) << 3; - unsigned long green = ((pixel >> 5) & 0x3F) << 2; - unsigned long blue = (pixel & 0x1F) << 3; - unsigned long argb = 0xFF000000UL | (red << 16) | (green << 8) | blue; + std::uint32_t red = ((pixel >> 11) & 0x1F) << 3; + std::uint32_t green = ((pixel >> 5) & 0x3F) << 2; + std::uint32_t blue = (pixel & 0x1F) << 3; + std::uint32_t argb = 0xFF000000U | (red << 16) | (green << 8) | blue; for (int suby = 0; suby < scale; suby++) { - unsigned long * row = (unsigned long *)bits + ((rect.Y + y) * scale + suby) * width + (rect.X + x) * scale; + std::uint32_t * row = (std::uint32_t *)bits + ((rect.Y + y) * scale + suby) * width + (rect.X + x) * scale; for (int subx = 0; subx < scale; subx++) { row[subx] = argb; } } + + x++; + } + + if (compressed) { + line += *(unsigned short const *)line; } } diff --git a/code/windlg.cpp b/code/windlg.cpp deleted file mode 100644 index 93ba040cb..000000000 --- a/code/windlg.cpp +++ /dev/null @@ -1,755 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2026 OpenTS contributors - * - * See LICENSE.md for applicable additional terms and warranty disclaimers. - ******************************************************************************/ - -#include "always.h" - -#include "windlg.h" - -#include "arraylist.h" -#include "data.h" -#include "globals.h" -#include "init.h" -#include "msgloop.h" -#include "ownrdraw.h" -#include "video.h" -#include "win.h" - -#include -#include - - -BOOL CALLBACK Resize_Dialog(HWND window, LPARAM lParam); -BOOL CALLBACK Save_Control_Value_Enum_Proc(HWND window, LPARAM lParam); -void WS_Save_Dialog_Values(HWND window); -void WS_Save_Control_Value(int control_id, unsigned char * data, int size); - - -HWND g_TopWindow; -int g_TopWindowID; -int g_LastResponse; - -WSDialogStruct g_Dialogs[64]; -int g_DialogCount; - - -/// -/// Fetches a window's rectangle relative to the main game window. -/// The dialog layout code works in the main window's client space rather than in screen -/// coordinates, so it uses this routine in place of GetWindowRect. -/// -/// Receives the window rectangle, offset into the main window's -/// client area. -/// bool; Was the window rectangle available? -BOOL Get_Display_Rect(HWND window, LPRECT rect) -{ - RECT client; - BOOL res = GetWindowRect(window, rect); - if (!res) { - return(res); - } - GetClientRect(MainWindow, &client); - ClientToScreen(MainWindow, (LPPOINT)&client); - rect->left -= client.left; - rect->right -= client.left; - rect->top -= client.top; - rect->bottom -= client.top; - return(res); -} - - -/// -/// Finds the stack slot a dialog window occupies. -/// The dialog bookkeeping routines use this routine to turn a window handle back into a -/// position in the dialog stack. -/// -/// Returns with the index of the dialog, or -1 if the window is not a tracked -/// dialog. -inline int WS_Dialog_Index(HWND window) -{ - for (int i = 0; i < g_DialogCount; i++) { - if (g_Dialogs[i].handle == window) { - return(i); - } - } - return(-1); -} - - -/// -/// Creates a dialog and pushes it onto the dialog stack. -/// This routine builds the dialog from its resource template, registers it with the -/// message loop so its keystrokes are routed properly, rescales it to the presentation -/// layout, and leaves it as the topmost dialog with the focus. -/// -/// The module instance to load the dialog template from. -/// The resource identifier of the dialog template. -/// The window the dialog is to be parented to. -/// The dialog procedure that messages are routed to. -/// Should the dialog be made visible straight away? -/// Returns with the window handle of the new dialog. If the template is -/// missing or the dialog could not be created, NULL is returned. -HWND WS_Create_Dialog(HINSTANCE instance, int id, HWND parent, DLGPROC proc, BOOL force_show) -{ - WSDialogStruct *slot = &g_Dialogs[g_DialogCount]; - g_Dialogs[g_DialogCount].handle = 0; - g_Dialogs[g_DialogCount].id = 0; - - LPCDLGTEMPLATE templ = (LPCDLGTEMPLATE)Fetch_Resource(MAKEINTRESOURCE(id), (LPCSTR)RT_DIALOG); - - if (templ == NULL) { - return(NULL); - } - - g_DialogCount++; - - HWND window = CreateDialogIndirectParam(instance, (LPCDLGTEMPLATE)templ, parent, proc, 0); - - if (window == NULL) { - g_DialogCount--; - return(NULL); - } - - _dialog_count++; - - Add_Modeless_Dialog(window); - - EnumChildWindows(window, Resize_Dialog, TRUE); - - Resize_Dialog(window, 0); - - OwnerDraw::Capture_Mouse(); - - slot->handle = window; - slot->id = id; - - if (force_show) { - ShowWindow(window, SW_SHOWNORMAL); - } - - SetForegroundWindow(window); - SetFocus(window); - g_TopWindow = window; - g_TopWindowID = id; - return(window); -} - - -/// -/// Closes a dialog along with everything stacked on top of it. -/// This routine records the dialog's control values before it goes, so they can still -/// be read back with WS_Get_Saved_Value, then unregisters and destroys it. Whichever dialog is -/// left underneath becomes the topmost one again and is given back the focus. -/// -/// The dialog to close. If this is NULL, the topmost dialog is -/// closed. -/// The response to report to whoever is waiting on this dialog. -/// bool; Was a dialog found and closed? -bool WS_Destroy_Dialog(HWND window, int id) -{ - if (window == NULL) { - if (g_DialogCount != 0) { - window = g_Dialogs[g_DialogCount - 1].handle; - if (window == NULL) { - return(false); - } - } else { - return(false); - } - } - - int index = WS_Dialog_Index(window); - if (index == -1) { - return(false); - } - - WS_Save_Dialog_Values(window); - - int last = g_DialogCount - 1; - if (g_DialogCount - 1 >= index) { - WSDialogStruct *dlg = &g_Dialogs[last]; - int count = last - index + 1; - do { - Remove_Modeless_Dialog(dlg->handle); - DestroyWindow(dlg->handle); - _dialog_count--; - OwnerDraw::Release_Mouse(); - dlg--; - count--; - } while (count); - } - - g_DialogCount = index; - - if (g_DialogCount != 0) { - HWND hwnd = g_Dialogs[g_DialogCount - 1].handle; - int id = g_Dialogs[g_DialogCount - 1].id; - SetForegroundWindow(hwnd); - InvalidateRect(hwnd, NULL, FALSE); - UpdateWindow(hwnd); - g_TopWindow = hwnd; - g_TopWindowID = id; - SetFocus(hwnd); - SendMessage(g_TopWindow, OD_REFOCUS, 0, 0); - } else { - g_TopWindow = 0; - g_TopWindowID = 0; - SetFocus(MainWindow); - } - g_LastResponse = id; - - return(true); -} - - -/// -/// Is this window one of the dialogs still open? -/// The wait loop uses this routine to tell when the dialog it is watching over has -/// finally been destroyed. -/// -/// bool; Is the window still on the dialog stack? -BOOL WS_Has_Dialog(HWND window) -{ - for (int i = 0; i < g_DialogCount; i++) { - if (g_Dialogs[i].handle == window) { - return(true); - } - } - return(false); -} - - -/// -/// Finds an open dialog by its resource identifier. -/// Should the same dialog happen to be open more than once, the one nearest the top of -/// the stack is the one found. -/// -/// The dialog resource identifier to search for. -/// Returns with the window handle of the dialog, or NULL if no such dialog is -/// open. -HWND WS_Find_Dialog(int id) -{ - for (int i = g_DialogCount - 1; i >= 0; i--) { - if (id == g_Dialogs[i].id) { - return(g_Dialogs[i].handle); - } - } - return(NULL); -} - - -/// -/// Fetches the dialog sitting above the one given. -/// -/// Returns with the window handle of the next dialog up the stack, or NULL if -/// the dialog given is the topmost one or is not tracked at all. -HWND WS_Next_Upper_Dialog(HWND window) -{ - for (int i = 0; i < g_DialogCount; i++) { - if (window == g_Dialogs[i].handle && i < g_DialogCount - 1) { - return(g_Dialogs[i + 1].handle); - } - } - return(NULL); -} - - -/// -/// Fetches the dialog sitting below the one given. -/// -/// Returns with the window handle of the next dialog down the stack, or NULL -/// if the dialog given is the bottom one or is not tracked at all. -HWND WS_Next_Lower_Dialog(HWND window) -{ - for (int i = g_DialogCount - 1; i >= 0; i--) { - if (window == g_Dialogs[i].handle && i > 0) { - return(g_Dialogs[i - 1].handle); - } - } - return(NULL); -} - - -/// -/// Waits until a dialog has been dismissed. -/// Use this routine to make one of these modeless dialogs behave as a modal one. It -/// pumps the message queue on the dialog's behalf, keeps the title screen refreshed, -/// and polls the abort callback, returning only once the dialog is gone. -/// -/// Optional routine polled on every pass. Should it return true, -/// the dialog is cancelled. May be NULL. -/// Should the dialog be forced to the front for the duration -/// of the wait? -/// Returns with the response the dialog was closed with. -int WS_Wait_Dialog(HWND window, bool (*callback)(void), bool, bool place_on_top) -{ - MSG msg; - - if (place_on_top) { - SetForegroundWindow(window); - SendMessage(window, OD_SETTOP, 0, TRUE); - } - - while (true) { - if (!WS_Has_Dialog(window)) { - break; - } - if (callback != NULL) { - if (callback() == TRUE) { - WS_Destroy_Dialog(window, IDCANCEL); - } - } - Title_Screen_Restore(false); - - while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { - TranslateMessage(&msg); - DispatchMessage(&msg); - if (!WS_Has_Dialog(window)) { - break; - } - } - - /* - * This loop pumps messages itself rather than going through the game's handler, - * so anything the dialog drew reaches the screen from here. - */ - Video_Present_If_Dirty(); - - Sleep(0); - } - - if (place_on_top) { - SendMessage(window, OD_SETTOP, 0, FALSE); - } - - return(g_LastResponse); -} - - -/// -/// Fetches the dialog currently on top of the stack. -/// -/// Returns with the window handle of the topmost dialog, or NULL if no dialog -/// is up. -HWND WS_Top_Window(void) -{ - return(g_TopWindow); -} - - -/// -/// Fetches the resource identifier of the topmost dialog. -/// -/// Returns with the dialog identifier, or zero if no dialog is up. -int WS_Top_Window_ID(void) -{ - return(g_TopWindowID); -} - - -/// -/// Fetches the response the last dialog was closed with. -/// -/// Returns with the identifier handed to WS_Destroy_Dialog when the most -/// recently closed dialog went away. -WPARAM WS_Last_Response(void) -{ - return(g_LastResponse); -} - - -/// -/// Records the values of every control in a dialog. -/// This routine is called as a dialog is being closed, so that the caller can still -/// interrogate its controls afterwards with WS_Get_Saved_Value. Whatever was recorded for a -/// previous dialog is discarded first. -/// -void WS_Save_Dialog_Values(HWND window) -{ - WS_Clear_Saved_Values(); - EnumChildWindows(window, Save_Control_Value_Enum_Proc, NULL); -} - - -/// -/// Records the value of one dialog control. -/// This routine is handed to EnumChildWindows by WS_Save_Dialog_Values. An edit box contributes -/// its text, while a button, slider or combo box contributes its current setting. -/// Anything else is passed over without comment. -/// -/// Always TRUE, so that child window enumeration carries on. -BOOL CALLBACK Save_Control_Value_Enum_Proc(HWND window, LPARAM lParam) -{ - char class_name[128]; - - GetClassName(window, class_name, sizeof(class_name)); - unsigned int id = GetWindowLong(window, GWL_ID); - - if (!strcmp(class_name, WC_EDIT)) { - int size = SendMessage(window, WM_GETTEXTLENGTH, 0, 0) + 1; - if (size > 257) { - size = 257; - } - - unsigned char *buf = new unsigned char[size]; - SendMessage(window, WM_GETTEXT, size, (LPARAM)buf); - buf[size - 1] = '\0'; - WS_Save_Control_Value(id, buf, size); - return(TRUE); - } - - if (!strcmp(class_name, WC_BUTTON)) { - int *i = new int; - *i = Button_GetCheck(window); - WS_Save_Control_Value(id, (unsigned char *)i, sizeof(*i)); - return(TRUE); - } - - if (!strcmp(class_name, TRACKBAR_CLASS)) { - int *i = new int; - *i = Slider_GetPos(window); - WS_Save_Control_Value(id, (unsigned char *)i, sizeof(*i)); - return(TRUE); - } - - if (!strcmp(class_name, WC_COMBOBOX)) { - int *i = new int; - *i = ComboBox_GetCurSel(window); - WS_Save_Control_Value(id, (unsigned char *)i, sizeof(*i)); - return(TRUE); - } - - return(TRUE); -} - - -ArrayList g_SavedValueIDs; -ArrayList g_SavedValueSizes; -ArrayList g_SavedValues; - - -/// -/// Records the value of a single dialog control. -/// This routine is the low level record keeper behind WS_Save_Dialog_Values. The value -/// survives the dialog it came from and is handed back later by WS_Get_Saved_Value. -/// -/// The control identifier the value belongs to. -/// Pointer to the value data. -/// The length of the value data, in bytes. -/// The value block is adopted by the record and is freed by -/// WS_Clear_Saved_Values. It must be allocated, never a temporary buffer. -void WS_Save_Control_Value(int control_id, unsigned char * data, int size) -{ - g_SavedValueIDs.addTail(control_id); - g_SavedValues.addTail(data); - g_SavedValueSizes.addTail(size); -} - - -/// -/// Fetches the recorded value of a dialog control. -/// Use this routine after a dialog has been closed, when the caller still needs to know -/// what the user left behind in one of its controls. -/// -/// The control identifier whose value is wanted. -/// Buffer that the recorded value is copied into. -/// The size of the destination buffer, in bytes. -/// Returns with the number of bytes copied. If no value was recorded for that -/// control, -1 is returned. -int WS_Get_Saved_Value(int control_id, unsigned char * dest, int dest_size) -{ - int entry_id = 0; - unsigned char *saved = NULL; - int saved_size = 0; - - for (int index = 0; index < g_SavedValueIDs.length(); index++) { - g_SavedValueIDs.get(entry_id, index); - if (entry_id == control_id) { - if (index >= 0) { - g_SavedValues.get(saved, index); - g_SavedValueSizes.get(saved_size, index); - } - if (saved_size < dest_size) { - dest_size = saved_size; - } - memcpy(dest, saved, dest_size); - return(dest_size); - } - } - return(-1); -} - - -/// -/// Discards every recorded dialog control value. -/// This routine frees the value blocks captured by WS_Save_Dialog_Values and empties the -/// record, leaving it ready for the next dialog that gets torn down. -/// -void WS_Clear_Saved_Values(void) -{ - unsigned char *data = NULL; - - for (int index = 0; index < g_SavedValueIDs.length(); index++) { - g_SavedValues.get(data, index); - delete data; - } - g_SavedValueIDs.clear(); - g_SavedValueSizes.clear(); - g_SavedValues.clear(); -} - - -/// -/// Handles messages for the layout reference dialog. -/// This routine handles nothing whatsoever. The reference dialog is created only to be -/// measured and is destroyed again immediately, so every message is left to the default -/// handling. -/// -INT_PTR CALLBACK Resize_Dialog_Proc(HWND window, UINT message, WPARAM wParam, LPARAM lParam) -{ - // nothing - return(FALSE); -} - - -/// -/// Fetches the client dimensions a dialog template was laid out at. -/// This routine creates the dialog just long enough to measure it and then throws it -/// away. The layout scaling code uses it to discover the resolution a template was -/// designed against. -/// -/// The resource identifier of the dialog template to -/// measure. -/// Receives the width and height of the dialog's client area. -BOOL Get_Dialog_Resolution(unsigned short template_id, DLGPROC dialog_proc, int, POINT &pt) -{ - HWND window; - tagRECT rcl; - - window = CreateDialogParam(ProgramInstance, MAKEINTRESOURCE(template_id), 0, dialog_proc, 0); - GetClientRect(window, &rcl); - DestroyWindow(window); - pt.x = rcl.right; - pt.y = rcl.bottom; - return(1); -} - - -/// -/// Rescales a dialog and every control it holds. -/// Use this routine after a dialog has been created, or whenever its layout has to be -/// rebuilt for the presentation size currently in force. -/// -void Resize_Dialogs(HWND window) -{ - EnumChildWindows(window, Resize_Dialog, 1); - Resize_Dialog(window, 0); -} - - -/// -/// Rescales a dialog or one of its controls to the presentation layout. -/// This routine is handed to EnumChildWindows by the dialog creation code and by -/// Resize_Dialogs, so that every control is carried from the coordinate space its -/// template was designed in over to the one dialogs are actually presented in. -/// -/// Should the window rectangle be taken relative to its parent? -/// This is set when enumerating child controls, and clear for the dialog itself. -/// Always TRUE, so that child window enumeration carries on. -BOOL CALLBACK Resize_Dialog(HWND window, LPARAM lParam) -{ - static int resize_dialog_width; - static int resize_dialog_height; - static int resize_dialog_scale_x = 300; - static int resize_dialog_scale_y = 163; - - LONG w; - LONG wheight; - RECT rcl; - RECT wrcl; - - char class_name[128]; - GetWindowRect(window, &rcl); - GetClassName(window, class_name, sizeof(class_name)); - - if (strcmp(class_name, WC_COMBOBOX) == 0) { - ComboBox_GetDroppedControlRect(window, &rcl); - } - - if (lParam) { - HWND win = (HWND)GetWindowLongPtr(window, GWLP_HWNDPARENT); - GetWindowRect(win, &wrcl); - rcl.left -= wrcl.left; - rcl.right -= wrcl.left; - rcl.top -= wrcl.top; - rcl.bottom -= wrcl.top; - } - - if (resize_dialog_width == 0) { - HWND win = CreateDialogParam(ProgramInstance, MAKEINTRESOURCE(198), NULL, Resize_Dialog_Proc, NULL); - GetClientRect(win, &wrcl); - DestroyWindow(win); - w = wrcl.right; - wheight = wrcl.bottom; - resize_dialog_width = w; - resize_dialog_height = wheight; - } else { - w = resize_dialog_width; - wheight = resize_dialog_height; - } - - int width = resize_dialog_scale_x * (rcl.right - rcl.left + 1) / w; - int height = resize_dialog_scale_y * (rcl.bottom - rcl.top + 1) / wheight; - - int x = resize_dialog_scale_x * rcl.left; - int y = resize_dialog_scale_y * rcl.top; - - rcl.left = x / w; - rcl.top = y / wheight; - rcl.right = width + rcl.left - 1; - rcl.bottom = height + rcl.top - 1; - - MoveWindow(window, rcl.left, rcl.top, width, height, TRUE); - - return(TRUE); -} - - -struct EzFont { - char FaceName[128]; - int DeciPtWidth; - int DeciPtHeight; - int Attributes; - HFONT FontHandle; -}; - -ArrayList g_EzFonts; - - -/// derived from MSDN "Moving Your Game to Windows, Part III" ttfont.cpp - -#define EZ_ATTR_BOLD 1 -#define EZ_ATTR_ITALIC 2 -#define EZ_ATTR_UNDERLINE 4 -#define EZ_ATTR_STRIKEOUT 8 -HFONT Ez_Create_Font (HDC hdc, const char * face_name, int decipt_width, int decipt_height, int attributes); - - -/// -/// Fetches a font of the typeface and point size requested. -/// This routine keeps every font it has built, so repeated requests for the same -/// description hand back the same handle rather than burning another GDI object. -/// The dialog drawing code calls this routine wherever it needs a font. -/// -/// The device context to build the font for. If this is NULL, the -/// font is only looked up and never created. -/// The character width in tenths of a point. -/// The character height in tenths of a point. -/// Bit flags of the EZ_ATTR_ style attributes to apply. -/// Returns with a handle to the font, or NULL if it was neither cached nor -/// able to be created. -/// The returned handle stays owned by the font cache. Do not delete it. -HFONT WS_Get_Font(HDC hdc, const char * face_name, int decipt_width, int decipt_height, int attributes) -{ - EzFont font; - - for (int index = 0; index < g_EzFonts.length(); index++) { - g_EzFonts.get(font, index); - if (!strcmp(font.FaceName, face_name) && font.DeciPtWidth == decipt_width && font.DeciPtHeight == decipt_height && font.Attributes == attributes) { - return(font.FontHandle); - } - } - - if (hdc == NULL) { - return(NULL); - } - - HFONT hFont = Ez_Create_Font(hdc, face_name, decipt_width, decipt_height, attributes); - - if (hFont == NULL) { - return(NULL); - } - - strcpy(font.FaceName, face_name); - font.DeciPtWidth = decipt_width; - font.DeciPtHeight = decipt_height; - font.Attributes = attributes; - font.FontHandle = hFont; - - if (g_EzFonts.addTail(font)) { - return(hFont); - } - - return(NULL); -} - - -/// -/// Creates a font of the typeface and point size requested. -/// This routine maps the requested decipoint dimensions through the device context's -/// current transform, so the font it builds matches the coordinate space the caller -/// draws in. Use WS_Get_Font in preference to this routine -- that one caches its fonts. -/// -/// The device context the font is to be built for. -/// The character width in tenths of a point. Zero lets the -/// typeface choose its own aspect. -/// The character height in tenths of a point. -/// Bit flags of the EZ_ATTR_ style attributes to apply. -/// Returns with a handle to the font created, or NULL if it could not be -/// created. -/// The caller takes ownership of the font handle. -HFONT Ez_Create_Font (HDC hdc, const char * face_name, int decipt_width, - int decipt_height, int attributes) -{ - HFONT hFont ; - LOGFONT lf ; - POINT pt ; - TEXTMETRIC tm ; - - SaveDC (hdc) ; - - SetGraphicsMode (hdc, GM_ADVANCED) ; - ModifyWorldTransform (hdc, NULL, MWT_IDENTITY) ; - SetViewportOrgEx (hdc, 0, 0, NULL) ; - SetWindowOrgEx (hdc, 0, 0, NULL) ; - - pt.x = decipt_width ; - pt.y = decipt_height ; - - DPtoLP (hdc, &pt, 1) ; - - lf.lfHeight = -pt.y ; - lf.lfWidth = 0 ; - lf.lfEscapement = 0 ; - lf.lfOrientation = 0 ; - lf.lfWeight = attributes & EZ_ATTR_BOLD ? 700 : 0 ; - lf.lfItalic = attributes & EZ_ATTR_ITALIC ? 1 : 0 ; - lf.lfUnderline = attributes & EZ_ATTR_UNDERLINE ? 1 : 0 ; - lf.lfStrikeOut = attributes & EZ_ATTR_STRIKEOUT ? 1 : 0 ; - lf.lfCharSet = ANSI_CHARSET ; - lf.lfOutPrecision = 0 ; - lf.lfClipPrecision = 0 ; - lf.lfQuality = 0 ; - lf.lfPitchAndFamily = 0 ; - - strcpy (lf.lfFaceName, face_name) ; - - hFont = CreateFontIndirect (&lf) ; - - if (decipt_width != 0) { - hFont = (HFONT) SelectObject (hdc, hFont) ; - GetTextMetrics (hdc, &tm) ; - DeleteObject (SelectObject (hdc, hFont)) ; - lf.lfWidth = (int) (tm.tmAveCharWidth * - fabs (pt.x) / fabs (pt.y) + 0.5); - hFont = CreateFontIndirect (&lf) ; - } - - RestoreDC (hdc, -1); - return(hFont); -} diff --git a/code/windlg.h b/code/windlg.h deleted file mode 100644 index 3caf8291c..000000000 --- a/code/windlg.h +++ /dev/null @@ -1,57 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2026 OpenTS contributors - * - * See LICENSE.md for applicable additional terms and warranty disclaimers. - ******************************************************************************/ - -#pragma once - -#include "win.h" - - -BOOL Get_Display_Rect(HWND window, LPRECT rect); - -HWND WS_Create_Dialog(HINSTANCE instance, int id, HWND parent, DLGPROC proc, BOOL force_show); -bool WS_Destroy_Dialog(HWND window, int id); - -HWND WS_Find_Dialog(int id); -BOOL WS_Has_Dialog(HWND window); - -int WS_Wait_Dialog(HWND window, bool (*callback)(void), bool=false, bool place_on_top=true); - -HWND WS_Top_Window(void); -int WS_Top_Window_ID(void); -extern HWND g_TopWindow; - -int WS_Get_Saved_Value(int control_id, unsigned char * dest, int dest_size); -void WS_Clear_Saved_Values(void); - -HWND WS_Next_Upper_Dialog(HWND window); -HWND WS_Next_Lower_Dialog(HWND window); - -void Resize_Dialogs(HWND window); - -HFONT WS_Get_Font(HDC hdc, const char * face_name, int decipt_width, int decipt_height, int attributes); - -struct WSDialogStruct { - /* - * This is the window handle of the dialog occupying this slot. It stays zero until the - * dialog has actually been created, so a template that failed to load claims no slot. - */ - HWND handle; - - /* - * This is the resource identifier of the template the dialog was built from. It is what - * lets a dialog be found again by name rather than by handle. - */ - int id; -}; - -extern WSDialogStruct g_Dialogs[64]; -extern int g_DialogCount; -extern HWND g_TopWindow; -extern int g_TopWindowID; -extern int g_LastResponse; diff --git a/code/winfix.cpp b/code/winfix.cpp index c23c43d49..21edec860 100644 --- a/code/winfix.cpp +++ b/code/winfix.cpp @@ -33,11 +33,12 @@ #include "always.h" +#include "_xmouse.h" + #include "winfix.h" #include "ini.h" #include "misc.h" -#include "ownrdraw.h" #include "trim.h" #include diff --git a/code/winstub.cpp b/code/winstub.cpp index e11e6fc62..fb89680d0 100644 --- a/code/winstub.cpp +++ b/code/winstub.cpp @@ -68,10 +68,10 @@ #include "resource.h" #include "session.h" #include "theme.h" +#include "ui/uishell.h" #include "video.h" #include "win.h" #include "wincursor.h" -#include "windlg.h" #include "winfix.h" #include "wwmouse.h" #include "mainopt.h" @@ -151,14 +151,9 @@ void Focus_Restore(void) if (MouseCursor && _MouseCaptured == true && !Debug_Map) { MouseCursor->Capture_Mouse(); } - Heal_Dialog_Controls(); Map.Flag_To_Redraw(GS_REDRAW_ALL); InvalidateRect(MainWindow, 0, 0); Pause_Ingame_Movie(false); - if (WS_Top_Window()) { - SetActiveWindow(WS_Top_Window()); - SetFocus(WS_Top_Window()); - } } @@ -179,6 +174,7 @@ LRESULT CALLBACK /*_export*/ Windows_Procedure(HWND hwnd, UINT message, WPARAM w * The frame may be drawn scaled, so a click has to be matched against where the * player sees the controls rather than where Windows finds them. */ + LPARAM const window_lparam = lParam; { LPARAM translated_lparam; if (Route_Mouse_Message(hwnd, message, wParam, lParam, &translated_lparam)) { @@ -187,6 +183,17 @@ LRESULT CALLBACK /*_export*/ Windows_Procedure(HWND hwnd, UINT message, WPARAM w lParam = translated_lparam; } + /* + * The shell sees mouse, wheel, key and text messages after the routing, which keeps + * legacy child windows working under video scaling, and before the keyboard handler, + * which keeps whatever a toolkit consumed out of the KN_ queue. It reads the position + * Windows delivered rather than the routed one, because the overlays are laid out in + * the window's own pixels. + */ + if (UI_Handle_Window_Message(hwnd, message, wParam, window_lparam)) { + return(0); + } + int low_param = LOWORD(wParam); Map.Message_Handler(hwnd, message, wParam, lParam); @@ -254,6 +261,15 @@ LRESULT CALLBACK /*_export*/ Windows_Procedure(HWND hwnd, UINT message, WPARAM w break; case WM_CLOSE: +#ifndef _WIN32 + /* + ** Windows answers a close request by destroying the window and leaving the + ** program to notice. There is no front end here to notice it and no dialog + ** layer to confirm through, so the request ends the program itself. + */ + Emergency_Exit(); + exit(EXIT_SUCCESS); +#endif break; case WM_CREATE: @@ -355,9 +371,22 @@ LRESULT CALLBACK /*_export*/ Windows_Procedure(HWND hwnd, UINT message, WPARAM w } +#ifndef _WIN32 +// The host toolkit owns the surface the renderer presents into, and hands over the layer +// it created for it. Windows presents into the window handle itself. +extern "C" void * Win32Compat_Native_Window_Handle(HWND window); +extern "C" int Win32Compat_Window_Refresh_Rate(HWND window); +extern "C" BOOL Win32Compat_Set_Window_Fullscreen(HWND window, BOOL fullscreen); +#endif + + NativeWindow Win_Native_Window(HWND window) { +#ifdef _WIN32 return(NativeWindow{ NATIVE_WINDOW_DEFAULT, nullptr, window }); +#else + return(NativeWindow{ NATIVE_WINDOW_DEFAULT, nullptr, Win32Compat_Native_Window_Handle(window) }); +#endif } @@ -375,8 +404,26 @@ bool Win_Window_Drawable_Size(HWND window, int & width, int & height) } +// A borderless window covering the desktop is all a full screen presentation is on +// Windows, so there is nothing further to ask for there. A host that keeps its own +// furniture above an ordinary window has to be told, or it draws over the game. +bool Win_Set_Window_Fullscreen(HWND window, bool fullscreen) +{ +#ifdef _WIN32 + (void)window; + (void)fullscreen; + return(true); +#else + return(Win32Compat_Set_Window_Fullscreen(window, fullscreen ? TRUE : FALSE) != FALSE); +#endif +} + + int Win_Window_Refresh_Rate(HWND window) { +#ifndef _WIN32 + return(Win32Compat_Window_Refresh_Rate(window)); +#else int refreshrate = 0; HDC dc = GetDC(window); @@ -386,6 +433,7 @@ int Win_Window_Refresh_Rate(HWND window) } return(refreshrate); +#endif } @@ -504,6 +552,8 @@ void Create_Main_Window ( HINSTANCE instance , int command_show , int width , in NULL, instance, NULL ); + + Win_Set_Window_Fullscreen(MainWindow, true); } ShowWindow (MainWindow, SW_NORMAL); @@ -521,6 +571,43 @@ void Create_Main_Window ( HINSTANCE instance , int command_show , int width , in } +/// +/// Moves the main window between a full screen presentation and a window. +/// The frame is not resized: it keeps the resolution the display options settled on and is +/// scaled into whichever the window now is, which is what the two creation paths already do. +/// +/// Should the window cover the screen? +/// The resize the host reports rebuilds the presentation, so nothing else needs telling. +void Set_Window_Fullscreen(bool fullscreen) +{ + if (MainWindow == NULL) { + return; + } + + WindowedMode = !fullscreen; + SetWindowLong(MainWindow, GWL_STYLE, fullscreen ? WS_POPUP : WS_OVERLAPPEDWINDOW); + Win_Set_Window_Fullscreen(MainWindow, fullscreen); + + if (fullscreen) { + return; + } + + int clientwidth = (Options.WindowWidth > 0) ? Options.WindowWidth : Options.ScreenWidth; + int clientheight = (Options.WindowHeight > 0) ? Options.WindowHeight : Options.ScreenHeight; + + RECT rect; + SetRect(&rect, 0, 0, clientwidth, clientheight); + AdjustWindowRectEx(&rect, GetWindowLong(MainWindow, GWL_STYLE), FALSE, GetWindowLong(MainWindow, GWL_EXSTYLE)); + + int windowwidth = rect.right - rect.left; + int windowheight = rect.bottom - rect.top; + int x = (GetSystemMetrics(SM_CXSCREEN) - windowwidth) / 2; + int y = (GetSystemMetrics(SM_CYSCREEN) - windowheight) / 2; + + MoveWindow(MainWindow, std::max(x, 0), std::max(y, 0), windowwidth, windowheight, 1); +} + + /// /// Loads a title screen picture and centers it on the surface. /// This routine is used by the startup and scenario loading sequences to put some diff --git a/code/winstub.h b/code/winstub.h index ea3ef4518..86798d28c 100644 --- a/code/winstub.h +++ b/code/winstub.h @@ -23,6 +23,8 @@ void Create_Main_Window ( HINSTANCE instance , int command_show , int width , in NativeWindow Win_Native_Window(HWND window); bool Win_Window_Drawable_Size(HWND window, int & width, int & height); int Win_Window_Refresh_Rate(HWND window); +bool Win_Set_Window_Fullscreen(HWND window, bool fullscreen); +void Set_Window_Fullscreen(bool fullscreen); void Load_Title_Screen(char const * name, Surface * surface, PaletteClass * palette); diff --git a/code/worlddom.cpp b/code/worlddom.cpp index b560a11b3..966f50915 100644 --- a/code/worlddom.cpp +++ b/code/worlddom.cpp @@ -18,7 +18,6 @@ #include "language/language.h" #include "mapgen.h" #include "mixfile.h" -#include "ownrdraw.h" #include "wdtnet.h" @@ -39,51 +38,6 @@ extern WDTPointer g_WDTResumedCampaign; -/// -/// Handles the dialog messages for the tour side choice menu. -/// This routine gives the owner draw default handler first refusal and, for a button it -/// does not consume, records the player's choice in the dialog result. -/// -/// -/// Returns with the result of the owner draw handler, or FALSE if it left the message alone. -/// -INT_PTR CALLBACK WDT_Faction_Choice_Menu_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) -{ - int* retval; - - INT_PTR rc = OwnerDraw::Default_Dialog_Proc(window, message, wparam, lparam); - - if (rc == 0) { - - switch (message) { - case WM_COMMAND: { - retval = (int *)GetWindowLongPtr(window, DWLP_USER); - switch (LOWORD(wparam)) { - case IDC_PICKCLAN_JOIN: - *retval = 1; - break; - - case IDC_PICKCLAN_GDI: - *retval = 2; - break; - - case IDC_PICKCLAN_NOD: - *retval = 3; - break; - - case IDC_CANCEL: - *retval = 4; - break; - } - break; - } - } - return(FALSE); - } - return(rc); -} - - /// /// Asks the player which side to fight for in the tour. /// This routine runs the graphic menu that offers the two sides and tidies it away again. diff --git a/code/wspudp.cpp b/code/wspudp.cpp index dff61927c..35189df74 100644 --- a/code/wspudp.cpp +++ b/code/wspudp.cpp @@ -479,12 +479,19 @@ void UDPInterfaceClass::Receive_Pending(void) /* ** Make sure this packet didn't come from us. If it did then throw it away. */ + // A broadcast is delivered back to the socket that sent it, which is the echo + // this discards. The source port has to match as well as the address, because + // another instance of the game on this machine answers from the same addresses + // and is a peer, not an echo. bool ours = false; - uint32_t const from_ip = source.Get_IP(); - for ( int i=0 ; iBound_Port() : 0; + if (bound != 0 && source.Get_Port() == Socket_Network_Port(bound)) { + uint32_t const from_ip = source.Get_IP(); + for ( int i=0 ; i #include -#include - -#ifndef SEEK_SET -#define SEEK_SET 0 // Seek from start of file. -#define SEEK_CUR 1 // Seek relative from current location. -#define SEEK_END 2 // Seek from end of file. -#endif class FileClass diff --git a/code/zbuffer.h b/code/zbuffer.h index a7ab2e38c..39ee7cfbf 100644 --- a/code/zbuffer.h +++ b/code/zbuffer.h @@ -11,6 +11,8 @@ #include "rect.h" +#include + class Surface; #define ZBUFFER_MAX 0x8000 diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 445516b3c..f2c6ea703 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -39,6 +39,23 @@ The audio layer uses [miniaudio](https://github.com/mackron/miniaudio), vendored through `thirdparty/miniaudio` at a tested tag and compiled as one translation unit from `thirdparty/miniaudio-impl.c`. +The UI shell uses [RmlUi](https://github.com/mikke89/RmlUi), vendored through +`thirdparty/RmlUi` at a tested tag and built static with the FreeType font +engine. Its samples carry their own window and renderer backends, which the +shell replaces, so none of them are built. + +RmlUi's font engine uses [FreeType](https://freetype.org/), vendored through +`thirdparty/freetype` at a tested tag with bzip2, PNG, HarfBuzz, and Brotli +disabled. FreeType's bundled zlib supplies compressed font stream support. + +Developer tooling uses [Dear ImGui](https://github.com/ocornut/imgui), vendored +through `thirdparty/imgui` at a tested tag. Only the core sources are compiled; +the bundled platform and renderer backends are not, because the shell feeds +ImGui through the engine's own message hook and draws it on bgfx. + +`bimg_decode`, which bgfx already carries, decodes the PNG and TGA images UI +documents reference and is built and linked with everything else. + For a fresh clone, use `git clone --recurse-submodules`. Configuration stops with instructions if a submodule is missing. Update a pinned tag in a separate change. @@ -135,6 +152,35 @@ build's saves are not interchangeable with a supported build's. The packed version stamp that saves and network packets carry is the same for both, so nothing rejects a save or a peer on that basis. Configuring the build warns about it. +## Experimental native build + +An unsupported native build for the host platform is available for portability +work. It does not expand the supported build matrix or establish runtime +behavior. + +```bash +cmake -S . -B build/native -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DOPENTS_EXPERIMENTAL_NATIVE=ON +cmake --build build/native +``` + +Run it the same way as a supported build, from `build/native/bin/`: + +```bash +build/native/bin/Game -DATADIR=Run -USERDIR=build/native/user +``` + +This build is the one exception to the data directory being read only: the +shipped UI documents resolve under it, so the build writes `ui/` there. + +Windows supplies the window, the message loop and the cursor itself. Every +other target gets them from `platform/win32compat`, which keeps the Win32 +surface the engine is written against and supplies it from +[SDL](https://github.com/libsdl-org/SDL), pinned as `thirdparty/SDL` and built +with its audio, render and camera subsystems off. Audio stays on miniaudio. +That library is added to the build only when the target is not Windows, so a +Windows configure neither builds nor links it. ## Build from Visual Studio Code diff --git a/docs/README.md b/docs/README.md index d983f12aa..963715287 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,6 +11,8 @@ The developer guides are split by subject: - [Project direction](DIRECTION.md) — long-term architecture. - [UI system design](UI_DESIGN.md) — proposed RmlUi and ImGui integration, screen-level interchangeable views, and the migration from OwnerDraw. +- [The saved game format](SAVE-FORMAT.md) — the layout of a `.SAV` file: its + header, listing fields, compressed content, and object records. See [CONTRIBUTING.md](../CONTRIBUTING.md) for contribution and review rules. Player and modder documentation is under [manual/](../manual/README.md). When a diff --git a/docs/SAVE-FORMAT.md b/docs/SAVE-FORMAT.md new file mode 100644 index 000000000..ea9ddc950 --- /dev/null +++ b/docs/SAVE-FORMAT.md @@ -0,0 +1,161 @@ +# The saved game format + +A saved game is one `.SAV` file written by `code/savefile.cpp` and read back by +it. This document owns the layout. Where the files live, how they are named, +and when they are written is on the manual's +[save games page](../manual/content/formats/save-games.md). + +Every integer is little-endian. Offsets are from the start of the file. + +## Header + +| Offset | Size | Field | +| --- | --- | --- | +| 0 | 4 | Signature, the bytes `OTSV` | +| 4 | 2 | Format version, currently 1 | +| 6 | 2 | Flags; bit 0 set when the content is LZO-compressed | +| 8 | 4 | Length of the field table | +| 12 | 4 | Offset of the content | +| 16 | 4 | Stored length of the content | +| 20 | 4 | Uncompressed length of the content | +| 24 | 4 | CRC-32 of the stored content | +| 28 | 4 | CRC-32 of the first 28 bytes of the header, continued over the field table | + +The header is 32 bytes, the field table follows it directly, and the content +follows the table directly. The content offset is recorded rather than assumed +so a later format version can put something between the two; this version +refuses a file whose offset says otherwise. A field table is refused above +1 MiB, since a listing is a dozen short fields. + +Both checksums are the CRC-32 of IEEE 802.3, polynomial `0xEDB88320` +reflected, initial value and final complement of all ones, as PNG and gzip +use it. The header checksum continues over the field table so a listing can +verify what it shows without reading the content. + +## Field table + +The fields are what the load dialog lists a save by. Each is: + +| Size | Field | +| --- | --- | +| 2 | Identifier | +| 2 | Kind: 1 string, 2 integer, 3 file time | +| 4 | Length of the value | +| | The value: string bytes without a terminator, a 4-byte integer, or an 8-byte `FILETIME` | + +The identifiers are the `PIDSI_` values in `code/savever.h`, the same ones the +compound-document property set carried before this format. A field holds at +most 64 KiB. A reader takes the +first field that matches both identifier and kind and ignores the rest, so a +field it does not know costs nothing. A string longer than the buffer it is +read into is cut on a character boundary, so a shortened description stays +UTF-8. `SaveVersionInfo` in `code/savever.cpp` is the only writer and reader. + +## Content + +The content is the game state: the bytes `Put_All` in `code/saveload.cpp` +writes through `SaveStreamClass`, compressed as one block with LZO1X-1 when +that makes it smaller, and stored as it is otherwise. The reader checks the +stored length and checksum before decompressing, and refuses a block that does +not expand to exactly the recorded length. The decompressor checks every read +and write against its buffers, so a block forged to overrun either is refused +like a damaged one. An uncompressed length above 256 MiB is refused before +anything is allocated for it. + +### Object records + +The state is a sequence of values and object records in the order `Put_All` +names them. An object record is: + +| Size | Field | +| --- | --- | +| 16 | The class identifier of the object | +| 4 | Length of the record body | +| | The body: the swizzle identity, then the members the class's `Serialize` names | + +The swizzle identity is the object's own address, written at the width a +pointer has in the build that wrote it. Every record therefore grows by four +bytes in a 64-bit build, and a save does not cross between builds of different +pointer widths. + +The class identifier is the `ClassID` the object's `Class_ID` reports, the +same one registered in `code/startup.cpp` and, for a locomotor, named by the +`Locomotor=` key. Its sixteen bytes are those of the COM class identifier the +class once registered, so a save written before COM left the engine still +names the same classes. The reader creates the object through that registration, +hands it the stream, checks that it consumed exactly the recorded length, and +only then lets it finish restoring itself, so a refused record never reaches +the map or a side table. A record that comes up short or long fails the load with the object's +type and offset in the debug log, which is what a member added to one build +and not the other looks like. A record read where a locomotor belongs fails +the load the same way when its class is not one. A vector of objects is a +4-byte count followed by that many records, and a locomotor nested inside a +unit's record is a record of its own. A count that the bytes remaining in the +content could not hold fails the load before anything is allocated for it. + +An object whose record fails is destroyed before the load fails. The pointer +slots it had registered are cleared first, since they still hold identities +rather than addresses. The objects loaded before it keep their places in the +heaps and have their slots cleared the same way, so a failed load leaves +nothing that a later teardown cannot delete. + +The body is what each class's `Serialize` produces, member by member, in host +byte order. It is not described here; the classes are the description. + +## Versions + +Two numbers gate a save. The format version in the header says how to parse +the file, and a reader refuses a version above its own. The header flags are +gated the same way: a reader refuses a file with a flag bit it does not know, +so a later version can mark content it stores differently without moving the +format version. The internal version in the field table, +`PIDSI_INTERNAL_VER`, is `ExpectedGameVersion`, the packed project version, +and a save whose value differs from the running build's is not offered to the +player. The format version moves only when the layout in this document +changes; the internal version moves with every release. + +## What the reader refuses + +`SaveFileClass::Read` and `Read_Fields` answer one of: + +| Result | When | +| --- | --- | +| `RESULT_MISSING` | No file under that name | +| `RESULT_NOT_A_SAVE` | The first bytes are not the signature | +| `RESULT_UNSUPPORTED_VERSION` | A format version above the reader's, or a header flag it does not know | +| `RESULT_CORRUPT` | A length, checksum or compressed block that does not add up, including a truncated file, a forged block, a field table above 1 MiB, a content offset that does not follow the table, or a content length above 256 MiB | +| `RESULT_NO_MEMORY` | A file within those limits that the process cannot hold | + +`Read` judges the header before it reads or allocates anything else, so a file +of any size costs the reader no more than the limits above allow, and +`Read_Fields` reads the header and the table only, so listing a folder never +allocates for a file's content. + +`Load_Game` reads and checks the whole file before it tears down the running +game, so a refused file costs nothing. + +A save written before this format is an OLE compound document, which begins +with a signature of its own, so the reader answers `RESULT_NOT_A_SAVE` and the +load dialog leaves the file out of its list. Nothing converts those files. + +## Writing + +`SaveFileClass::Write` builds the whole image in memory, writes it to the +target name with `.tmp` appended, flushes and closes it, and then moves it over +the target with `MoveFileExA` and `MOVEFILE_REPLACE_EXISTING`. A save +interrupted at any point leaves the previous file untouched under its name, +and at most a `.tmp` beside it, which the next successful save replaces. +The reader's limits bind the writer too: content above 256 MiB, a field above +64 KiB or a table above 1 MiB is refused with `RESULT_TOO_LARGE` before +anything is written, so a save this build writes is one it reads, and the +file on disk is left as it was. + +## Checks + +`tests/save` builds `code/savefile.cpp` against the LZO library on every target +covers the round trip, the fields-only read, replacement of an existing file +and of a stale `.tmp`, and each refusal above, including a later version, an +unknown flag, a file cut at every boundary, a byte flipped in the header, the +table and the content, a field table above its limit, a gap before the +content, compressed blocks forged to overrun the reader, and a write above +each limit that leaves the earlier save in place. It reads no game data. diff --git a/docs/UI_DESIGN.md b/docs/UI_DESIGN.md index edebbfcb8..ec8647d16 100644 --- a/docs/UI_DESIGN.md +++ b/docs/UI_DESIGN.md @@ -1,30 +1,111 @@ # UI system design -Status: proposal. Nothing here is implemented, built, or measured. Source -inspection and upstream documentation inform it. This page owns the proposed -UI architecture and migration; [Building OpenTS](BUILDING.md) owns build -support and [Project direction](DIRECTION.md) the wider architecture. +Status: in progress. Steps 1 to 13 of the migration plan have landed. OwnerDraw +is gone; step 14, the sidebar, is the only step left. +Everything outside the migration plan remains a proposal informed by source +inspection and upstream documentation. +This page owns the UI architecture and migration; [Building +OpenTS](BUILDING.md) owns build support and [Project +direction](DIRECTION.md) the wider architecture. + +What step 2 left for later, inside its own files: the renderer keeps compiled +geometry's indices in a static buffer but streams its vertices through a +transient one, because the program the overlays share is bgfx's embedded imgui +shader, whose vertex stage multiplies by `u_viewProj` alone and so ignores the +per-draw model transform; a program with a model transform restores the static +vertex buffer the renderer table describes. `uitexture.cpp` read PNG and TGA +only until the dialog artwork needed PCX and SHP, and the cursor and clipboard +requests are recorded rather than acted on. Step 6 +brought the `` element, which is the other route to game art: pixels +the engine draws rather than a file a document names. + +Step 3 exercised the rest. `UI_Run_Modal` now runs a screen, and the input hook +gained the modal scope its rules always described: while an exclusive document +is shown it takes every mouse and key message, as `IgnoreInput` does around a +legacy dialog, and the keyboard queue is cleared as the scope opens and closes. +The version screen needed no name table, because it composes its own text and +takes its one string-table entry through `Fetch_String`, which already yields +UTF-8 on a build whose active code page is 65001; a document that writes +`[[TXT_OK]]` waited for the name table step 4 brought. The screen drew its +panel rather than blitting `dbak6440.pcx` until PCX decoding arrived with the rest of the +game art; it blits it now. + +Step 4 put the runner under load. `UI_Run_Modal` runs the game as well as a +screen: in a network session it steps `Main_Loop` between passes and reports a +session that ended underneath the box, which is what `WWMessageBox::Process` +has always had through `OwnerDraw::Dialog_Message_Handler`. A presenter gained +`Service`, the maintenance a dialog driver ran on every pass of its own loop, +and the shell drops the tick that `Main_Loop` and `Call_Back` make from inside +a runner, so a pass updates the context once. The `[[NAME]]` table arrived with +it, generated from `language.rc` by the script that already builds the portable +string table. + +Step 5 split a screen in two. `UISoundPresenterClass` holds the sound screen's +whole behavior and both views drive it: the dialog procedure now reads the +view-model and queues intents that its driver executes after the pump, and the +RmlUi documents queue the same intents from their own events. Two facts the +extraction fixed in place: the screen picks its template from `GameActive` +rather than from the menu that opened it, and it has no cancel, because the +templates name no cancel button and the dialog procedure ignored the `IDCANCEL` +that Escape produces. A form control's value is bound one way, with the view +dropping a change that matches the value it already holds, so setting a slider +from the model cannot preview a volume the player did not move; that is what +`DialogInitialized` did for `WM_HSCROLL`. Two live documents may not share a +data-model name, which `Context::CreateDataModel` refuses; a second screen of +the same kind therefore fails preparation rather than opening. + +Step 6 put engine-drawn pixels in a document. The `` element resolves +a provider by name at render time, takes its intrinsic size from that provider +scaled by the document's density-independent pixel ratio, and uploads only +when the provider's generation moves, so a document holding a surface costs a +quad per present while nothing changes. Two boxes without a loop of their own +gained the paint the dialogs got from `SendMessage(WM_PAINT)`: the wait box and +the progress box are presented as they open, unpaced, because the operation +they stand over may never pump again. Screens of different kinds do coexist, +which the progress box opening over the wait box shows; only a second screen of +the same kind is refused, and the coexistence rule still forbids a legacy +dialog underneath either. + +Step 8 made a suspended runner possible. `UI_Run_Modal` returns when the screen +asks to be stepped aside as well as when it has a result, because a screen that +opens another one of a different kind has no result yet and its owner has to +hide it, run the other, and show it again. Without that the in-game options +screen's save and load browsers, and the main menu's version screen, could +never be reached through an RmlUi view. + +Step 7 gave a view the ability to step aside. `UIRmlViewClass` gained `Hide` +and `Show`, which take a document off the screen and put it back with the modal +scope it had, because the in-game options screen opens the save and load +browsers where it is drawn and the legacy dialog got out of their way with +`ShowWindow`. A screen family whose members are opened one after another +resets `IsClosing` and the held result before each pass, since a close marks +the presenter closing and a marked presenter drains nothing. ## Where the UI stands today -OpenTS has four UI systems plus a few bespoke screens. They share the software -frame and the keyboard queue but nothing else. +OpenTS has three UI systems plus a few bespoke screens. They share the software +frame and the keyboard queue but nothing else. OwnerDraw was the fourth and step +13 deleted it. What follows describes what it was, because the screens that +replaced it were converted from its templates and inherit its geometry. | System | Files | Used by | Draws into | | --- | --- | --- | --- | -| OwnerDraw | `ownrdraw.cpp` (7,009 lines), `windlg.cpp`, `msgloop.cpp`, 53 templates in `language.rc` | main menu, options, skirmish, load and save, lobbies, desync, map generator, WDT, message boxes, progress wait | `AlternateSurface`, then `VisibleSurface` | | GadgetClass | `gadget.cpp`, `control.cpp`, `toggle.cpp`, `list.cpp`, `edit.cpp`, `slider.cpp`, ... | sidebar, radar, tactical buttons, message list, checklist, mission restate | `LogicalSurface` (`SidebarSurface`, `HiddenSurface`) | | MSEngine | `msengine.cpp`, `msanim.cpp`, `grphmenu.cpp` | graphic menu, map select, score screens, WDT screens, credits | `AlternateSurface`, `HiddenSurface` | | Bespoke | `progress.cpp`, `score.cpp`, `movies.cpp` | loading screen, score, movies | `HiddenSurface` | -OwnerDraw is the largest and the least portable. Each dialog is a real Win32 +OwnerDraw was the largest and the least portable. Each dialog was a real Win32 child window of `MainWindow`, created from a resource template by `CreateDialogIndirectParam`. Every control is subclassed; its window procedure paints into `AlternateSurface` and blits the result into `VisibleSurface` -itself. `Draw_Dialog_Back` composes `dbak6440.pcx`, the side bars, and sixteen -glow passes into a cached surface and assumes 640x400 art centered on the -screen. Text is GDI "MS Sans Serif" at 14 and 12 pixels through `WS_Get_Font`, -plus the `dlgsys` remap sheets for list text. Tooltips save and restore the +itself. `Draw_Dialog_Back` composes `dbak6440.pcx`, the two side bars, the four +`bar_` corner pieces and sixteen glow passes into a cached surface and assumes +640x400 art centered on the screen. Buttons, check boxes, static captions, tabs +and combo boxes draw their text from the `dlgsys` remap sheets; list boxes, +tooltips and the hotkey control use GDI "MS Sans Serif" at 14 and 12 pixels +through `WS_Get_Font`. Text is `RGB(112,255,0)` and `RGB(144,144,144)` when +disabled; a control that stands over the wallpaper shows it blended 180/255 +toward black. Tooltips save and restore the pixels under them. `Heal_Dialog_Controls` forces every child window to repaint after each `Update_Visible_Surface`, so a dialog repaints once per game frame. The templates hold 322 `CONTROL` entries: 103 owner-draw buttons, 40 track @@ -90,8 +171,10 @@ Facts elsewhere in the tree that bind the design: `Slid`, and `LastSlid` beside `TopIndex` and `Buildables`. - `SidebarClass::Reposition_Sidebar` registers the cameo tooltips itself, independent of gadget registration; `CCToolTip` paints into game surfaces. -- `ProgressScreenClass::Set_Progress_Percent` sends `WM_PAINT` synchronously, - and `Display_Progress` plays the milestone sound from the draw path. +- `ProgressScreenClass::Set_Progress_Percent` sends `WM_PAINT` synchronously. + `Display_Progress` used to print the loading message, play its sound and + clamp the gauge from inside the draw path; step 6 moved all three onto the + progress-changed path. Three consequences shape the design. A new UI must fit the blocking-loop shape, or every driver has to be rewritten in the same change; the loop shape @@ -182,6 +265,7 @@ today. | `uirender.cpp` | RmlUi render interface and the ImGui renderer on bgfx; the only UI file that includes bgfx | | `uisystem.cpp` | RmlUi system interface: time, logging to `DebugString`, cursor, clipboard, string translation | | `uifile.cpp` | RmlUi file interface over `CCFileClass` | +| `uifont.cpp` | RmlUi font engine: the `dlgsys` bitmap sheets, delegating every other family to RmlUi's own engine | | `uitexture.cpp` | image decoding, SHP and PCX conversion, surface-backed textures | | `uiscreen.h`, `uirmlview.h` | presenter, intent, and result contracts; the RmlUi view base | | `uidev.cpp` | ImGui context and developer overlays | @@ -214,6 +298,11 @@ frame's top-left corner. Draw order is the software frame and its scaling passes, RmlUi documents in the context's document order, ImGui, then the hardware cursor. +Every overlay texture is point sampled. The documents draw the game's own +640x400-era artwork and its bitmap font magnified by the frame scale, which at +a 640x400 frame in a 3456x2160 window is 5.4x; linear filtering softens both. +The art and the text share one sampler state so they cannot disagree. + One RmlUi context holds every document. A second context is justified only by an independent coordinate space or lifetime. Data-model names are unique among live screens, binding storage is owned by the view and outlives the @@ -508,26 +597,75 @@ required document, style, or font fails preparation with the name reported. Images resolve by extension. PNG and TGA decode through `bimg_decode`, which is already vendored and needs only linking. PCX goes through `Read_PCX_File` -with the palette named in the source string. SHP frames use a -`name.shp#frame` form with an optional palette, decoded to RGBA with index -zero transparent. Surfaces the engine draws at runtime (the map preview, the +and uses the palette the file carries, or the one a `name.pcx#palette.pal` +source names instead. SHP frames use a `name.shp#frame` form, with +`name.shp#frame#palette.pal` naming a palette and `GamePalette` standing in +when none is named, decoded to RGBA with index zero transparent. A `.pal` file +holds six-bit guns, so its values are scaled the way `init.cpp` scales the +palettes it loads. A source whose file cannot be read leaves the element with +whatever its background and border draw, which is why the dialog panel and +button in `ui/optionsbase.rcss` keep a flat colour under their artwork. Surfaces the engine draws at runtime (the map preview, the desync host icons, a progress bar) reach a document through a `` custom element bound to a named provider; the shell re-uploads the texture -when the provider marks it dirty. Original game art stays local runtime data +when the provider marks it dirty. A document writes `` +and the name is resolved at render time, so a document may be shown before the +screen that owns its pixels registers them and an element whose provider went +away draws nothing rather than failing to lay out. An element with no width or +height of its own takes the provider's extents, scaled by the document's +density-independent pixel ratio, because a provider's pixels are game logical +units. `UISurfaceBufferClass` is the provider a screen wants when the engine +already knows how to draw the thing: it owns a 16-bit surface the screen draws +into with the engine's ordinary calls, and converts it to premultiplied RGBA +with a color key for the mask. Original game art stays local runtime data outside version control; documents receive artwork identities, never engine -pointers. +pointers, and a presenter never holds a provider. ### Fonts -Fonts use RmlUi's FreeType engine with an OFL sans-serif shipped in `ui/`. -The legacy dialogs already draw with a system TrueType face, so this changes -nothing about their look. RmlUi uses one font engine per process, installed -with `SetFontEngineInterface` before `Rml::Initialise`, and the built-in -engine is not reachable from a custom one. In-game text that must match the -bitmap fonts, needed only by the post-migration sidebar view, has two routes: -convert the game's `.fnt` faces to TrueType at build time, or write a bitmap -engine over `WWFontClass` data as RmlUi's `bitmap_font` sample does and -commit every document to bitmap faces. That choice waits for that view. +Documents use RmlUi's FreeType engine with an OFL sans-serif shipped in +`ui/`. RmlUi installs one font engine per process, with +`SetFontEngineInterface` before `Rml::Initialise`, so `uifont.cpp` derives +from `FontEngineInterfaceDefault` rather than replacing it: it answers for +one family and hands every other one to the FreeType engine untouched. That +header lives in RmlUi's `Source` tree rather than its `Include` tree, so the +build puts that one path on that one file. + +The family it answers for is `dlgsys`, the remap sheets the dialogs drew +their buttons, statics, tabs, check boxes, combo boxes and track bar values +from. `drawhelp.cpp` owns the sheets: `OD_Font_Metrics` probes the cell size +and every character's inked width out of the artwork, and `OD_Font_Sheet` +composes the coverage sheet and the palette-shifted index sheet into +premultiplied RGBA for one text colour. `uifont.cpp` uploads that as one +texture per colour and emits a quad per glyph. A document states the height +of a glyph cell as its `font-size`, so `font-size: 18dp` against the 14 by +18 cells of `dlgsys` draws one sheet pixel per authored pixel. The sheets +give inked extents rather than typographic ones, so the face reports the +whole glyph as ascent and nothing as descent, which makes RmlUi's half +leading centre the ink on the line box the way +`OD_DRAW_CHAR_FLAG_VERTICAL_CENTER` centred it on a control. + +`ui/optionsbase.rcss` names which controls draw from it, and every shipped +document links that sheet, so the split is stated once: a push button, a check +box, a static caption, a tab, a combo box and a track bar's value take +`dlgsys`; a list box and its rows, an edit field, a message log and the hotkey +capture control keep the shipped face, which is what the dialogs did. Eight +documents add a rule of their own for a static the templates left without a +class. A static caption does not clip, because a glyph cell is 18dp and the +templates give a static as little as 13dp, while `StaticCtrlProc` had no such +limit; the two captions whose text comes from the game rather than from a +template ask for the clip back. + +A font engine gives its render resources back in `FontEngineInterface::Shutdown`, +which `Rml::Shutdown` calls while the render managers are still alive, and not +in `ReleaseFontResources`. That second method is the entry point behind +`Rml::ReleaseFontResources`, which an application calls to collect memory; no +part of shutdown reaches it. Holding a `CallbackTexture` past +`FontEngineInterface::Shutdown` releases it against a destroyed texture +database. + +In-game text that must match the `WWFontClass` faces, needed only by the +post-migration sidebar view, is a separate problem: those are a different +format and this engine does not read them. ### Strings @@ -544,21 +682,20 @@ byte, which bounds in-game text to the range the transition supports. Documents reference strings by name: `[[TXT_OK]]`. RmlUi passes every text node through `SystemInterface::TranslateString`, where the shell maps the -name to its identifier. The names are `#define`s in `language.h`, so a CMake -script generates the name table into the build's generated directory; no -hand-maintained list. Dynamic text, including player and map names and error +name to its identifier. `cmake/StringTable.cmake` writes that table into the +build's generated directory from the same `language.rc` it reads for the +portable string table, so the resource script stays the only place a name and a +number are paired and no list is hand-maintained. A name the table does not +carry is left in the text rather than replaced with nothing. Dynamic text, including player and map names and error strings, is inserted as text, never as markup. ## Configuration -One transitional key in `SUN.INI`, named by the change that introduces it, -returns every migrated screen to its legacy view while that view exists. -Defaults are decided per screen family in code, so a family switches to RmlUi -by default when its evidence is in without a key per family. The key is -deleted with OwnerDraw. There is no build option: RmlUi and ImGui are always -compiled and linked, so one configuration matrix carries the evidence. -`Options` reads and writes the key where it handles `[Video]` today, and the -key gets a manual page. A sidebar view key follows the sidebar view. +`LegacyDialogs` under `[Options]` in `SUN.INI` returned every migrated screen to +its legacy view while both existed. Step 3 named it; step 13 deleted it with +OwnerDraw, along with `UI_Use_Rml` and its manual page. There is no build +option: RmlUi and ImGui are always compiled and linked, so one configuration +matrix carries the evidence. A sidebar view key follows the sidebar view. ## Dear ImGui @@ -626,7 +763,11 @@ Invariants the split preserves: Progress tracking, clamping, milestone text and sound, and the readiness queries that `scenario.cpp` consumes move out of the draw path into shared behavior, so a repaint cannot repeat a milestone sound and a hidden -presentation cannot lose one. The screen exposes phase, progress, status, and +presentation cannot lose one. Step 6 did the move: +`ProgressScreenClass::Progress_Changed` runs the clamp and +`Announce_Milestones` where the gauge moves, `Display_Progress` draws and +nothing else, and the loading screen announces its first message itself rather +than getting it from a repaint. The screen exposes phase, progress, status, and the operations the loader supports; no cancellation is added to a loader that cannot cancel. Loading stays on its thread with explicit cooperative service points that drain nothing unrelated while scenario objects are being @@ -694,34 +835,280 @@ text beyond an ASCII test document. close; repeated open and close leaks nothing. 3. **Version dialog** (S, leaf). The integration pilot: fonts, clipping, mapping, dismissal by mouse and keyboard, focus return, UI-only redraw, - resize, preparation failure. The main menu keeps hiding around it. + resize, preparation failure. The main menu keeps hiding around it. Landed: + `code/ui/uiversion.cpp` with `ui/version.rml` and `ui/version.rcss`, the + geometry converted from the `IDD_VERSION` template's dialog units. 4. **Modal runner and message boxes** (M, leaf). `WWMessageBox::Process` and `OwnerDraw::Custom_Message_Box` behind the kill switch, preserving button order, default button, Escape, the no-button case, return mappings, and session-end interruption. Evidence includes the multiplayer cases where - `Main_Loop` runs under the box. + `Main_Loop` runs under the box. Landed: `code/ui/uimessagebox.cpp` with + `ui/messagebox.rml` and `ui/waitbox.rml`, the geometry converted from the + `IDD_MSGBOX_3` and `IDD_MSGBOX_1` templates. The wait box is not modal and + answers to a handle no window can have, because its callers hold one and + hand it back to `Display_Dialog`, `Set_Custom_Message_Box_Text` and + `End_Dialog`; the handle goes with OwnerDraw. 5. **Sound** (M, two changes). The behavior pilot: volumes, eligible themes, selection, availability, shuffle and repeat, immediate previews, play and - stop, both templates, frontend and in-game service paths. + stop, both templates, frontend and in-game service paths. Landed: + `code/ui/uisound.{h,cpp}` with `ui/sound.rml` and `ui/soundlite.rml`, the + geometry converted from the `IDD_SOUND_OPTIONS_DIALOG` and + `IDD_SOUND_OPTIONS_DIALOG_LITE` templates. A list row states its width + rather than taking it from the list, because a scrolling container gives its + children no width to be a proportion of. 6. **Progress and wait** (S, leaf). `IDD_PROGRESS_WAIT`, the saving and loading boxes in `savemgr.cpp`, the `` element, milestone effects - moved out of drawing. + moved out of drawing. Landed: `code/ui/uiprogress.{h,cpp}` with + `ui/progresswait.rml` and `ui/progresswait.rcss`, the geometry converted + from the `IDD_PROGRESS_WAIT` template; `code/ui/uisurface.{h,cpp}` with the + element and the provider contract; and the milestone move in + `code/progress.cpp`. The saving and loading boxes reach the wait box step 4 + built, through `OwnerDraw::Custom_Message_Box`, and needed the paint at open + rather than a screen of their own. 7. **Options family** (L, two changes each). Main options, display with its timed rollback, game controls (three variants), keyboard with the hotkey capture control, the display-mode confirmation, abort and surrender. - Evidence: settings round-trip through `SUN.INI` unchanged. + Evidence: settings round-trip through `SUN.INI` unchanged. Every screen in + the family is extracted: `code/ui/uigameoptions.{h,cpp}`, + `code/ui/uiabort.{h,cpp}`, `code/ui/uigamecontrols.{h,cpp}`, + `code/ui/uimainoptions.{h,cpp}`, `code/ui/uidisplayoptions.{h,cpp}`, + `code/ui/uidisplayconfirm.{h,cpp}` and `code/ui/uikeyboard.{h,cpp}`, and each + has its RmlUi view: `ui/options.rml`, `ui/gameoptions.rml` with + its `mp` and `wol` variants, `ui/abort.rml`, `ui/gamecontrols.rml` with its + `mp` and `wol` variants, `ui/display.rml`, `ui/modeconfirm.rml` and + `ui/keyboard.rml`, sharing + the family's look through `ui/optionsbase.rcss` and each carrying its own + geometry. + + The keyboard screen brought the family's two new controls. The category + combo is RmlUi's `select`, sized the way an owner-draw `CBS_DROPDOWNLIST` is + sized, from the item height `ownrdraw.cpp` sets rather than from the + template's dropped extent. The capture control stands where + `msctls_hotkey32` stood: it takes a keypress while it holds the focus and + builds the game's own encoding from it, and it leaves alone the keys + `IsDialogMessage` took from that control, so Escape and Enter still leave + the screen and Tab still moves the focus. Turning a keypress back into that + encoding needs the inverse of the shell's key table, which was a short list + in one direction only; it is now complete and paired, because a key nothing + maps is never delivered to a document at all. Step 9's save-name field wants + the same table. + + The display screen needed the host's mode list. `EnumDisplaySettings` + answered nothing on this platform, so the resolution list was empty and the + trial and its rollback could not be reached through the screen that owns + them; the shim answers from the host's own enumeration now. + + The mode trial's timeout is the presenter's, not a view's: the driver + expressed it as a posted `WM_COMMAND` carrying `WM_DESTROY`, which is two, + which its procedure recorded because `IDCANCEL` is also two, so the timeout + was a cancel spelled awkwardly. `UIDisplayConfirmPresenterClass::Service` + counts a `CDTimerClass` down from ten seconds and produces + the cancel itself, which is what takes an unreadable mode back. + + The templates carry the button captions and the string table does not, so a + document repeats the template's caption where no `TXT_` name exists. That + leaves those captions untranslated until names are added to `language.rc`, + which is where the strings are owned. 8. **Main menu family** (M). `IDD_MAIN_MENU`, campaign choice, game type, multiplayer game selection. The `NewMenuClass` drivers keep their loops. -9. **Load, save, delete** (M, two changes). + Landed: `code/ui/uimainmenu.{h,cpp}`, `uicampaign.{h,cpp}`, + `uigametype.{h,cpp}` and `uimpselect.{h,cpp}` with `ui/mainmenu.rml`, + `ui/campaign.rml`, `ui/gametype.rml`, `ui/mpselect.rml` and + `ui/mpselectfs.rml`, sharing `ui/optionsbase.rcss` and each carrying its own + geometry. `NewMenuClass` is untouched: it is the MSEngine graphic menu. + + The main menu document carries the keys its driver watched for beside the + buttons, because those keys belong to the screen rather than to the window it + was drawn in: Ctrl+V and Ctrl+Alt+C on `keydown`, and the cheat words on + `textinput`, since the shell's modal scope takes every key message before the + `KN_` queue sees it and `Keyboard->Check()` never fires again while a + document is shown. A character that followed a modified key is dropped, the + way the driver's own switch answered those combinations before its default + arm saw them. + + A campaign row carries the campaign it stands for rather than its position, + because the list skips a campaign the player cannot reach. The difficulty + track bar needs no read-back at accept: the dialog read its slider back + because a keyboard or page move raised no thumb notification, and RmlUi + raises a change for every move. The game type screen's default arm is the + behavior, so anything but backing out carries on. + + A view names its data model after its own document, so a variant document + must name its own model rather than the one the base document names; a + document that names another's gets no bindings and no events at all. The + step 7 variants had that fault and it went unseen until a click was driven + through one. +9. **Load, save, delete** (M, two changes). Landed: `code/ui/uisavebrowser.{h,cpp}` + holds all three templates as one screen, because they differ by which controls + exist and by what the action button does rather than by how the list is built, + with `ui/missionload.rml`, `ui/missionsave.rml` and `ui/missiondelete.rml` + sharing `ui/savebrowser.rcss` beside `ui/optionsbase.rcss` and each carrying its + own geometry, converted from the `IDD_MISSION_LOAD`, `IDD_MISSION_SAVE` and + `IDD_MISSION_DELETE` templates. + + A row's cells stand where the owner-draw list put its columns, which the three + dialog procedures register with `OD_ADDCOLUMN` at x 2, 255 and 315. The + multiplayer star is absent from the documents because it was absent from the + dialog: `Fill_List` sends `OD_SETCELL` at x 200, no column is registered there, + and `OD_SETCELL` answers -1 for a column it cannot find. + + The description field states a width. RmlUi moves the caret for the End key by + the length of the formatted line, and a field that formats no line, because it + has no usable width or no font face, reports that length as zero and sends the + caret to the start instead. Home is unaffected, because it asks for index zero + outright, and so is Ctrl+End, which takes the value's own length. Step 12's map + generator screens want the same field. 10. **Skirmish and map selection** (M, two changes). Includes the scenario - picker templates and the preview surface. + picker templates and the preview surface. Landed: both screens are + extracted, `code/ui/uiskirmish.{h,cpp}` and `code/ui/uiscenariopick.{h,cpp}`, + with `skirmish.cpp` and `netshare.cpp` rewired, and both have their RmlUi + view: `ui/skirmish.rml` and `ui/selectmap.rml` with their stylesheets beside + `ui/optionsbase.rcss`, converted from the `IDD_SKIRMISH` and + `IDD_MPLAYER_SELECT_MAP` templates. This is the step that makes a skirmish + reachable from the menu. + + `Update_Network_Dialog_Preview` is split the way `Fill_List` was: a new + `Rebuild_Network_Map_Preview` owns the preview and the old name owns telling + a window to repaint, so a presentation that is not a window can ask for one. + `Pick_Scenario_Screen` is the entry a screen uses, because a presenter names + no window. + + The preview reaches the document through the `` element step 6 + built, with no change to the element. The view owns a `UISurfaceBufferClass` + the size of the template's preview frame, scales the picture into it the way + `MapPreviewClass::Blit_Preview` scales it into the group box, and fills the + letterbox with the buffer's color key; the element takes its size from that + provider and uploads when the provider's generation moves. The presenter + carries only the artwork's name. + + The two screens hold previews of different sizes and the picker opens over + the skirmish screen, so the provider lives in `code/ui/uimappreview.{h,cpp}` + with its extents as constructor arguments and each screen registers under + its own name. A provider name is unique among live providers, so sharing one + would have taken the picture away when the picker closed. + + A track bar's range is set before its value. A range control clamps a value + into the range it is holding, and the default range stops well short of what + the rules allow, so a value written by the data binding before the range was + known opened the credits bar at its minimum instead of at the rules' figure. + The same rule the text field learned at step 9, one control further on. 11. **Network lobbies** (L, two changes). Host, guest, game list, the `WS_` stack, and `netshare.cpp` as one family; then disconnect, desync, and - reconnect. Packets unchanged. -12. **Map generator and WDT** (L). -13. **Retire OwnerDraw** (M). Delete `ownrdraw.cpp`, `windlg.cpp`, the - modeless dialog list, the dialog templates, the kill switch, and the - coexistence assertions. String tables stay. + reconnect. Packets unchanged. All three screens are extracted behind one + presenter, `code/ui/uilobby.{h,cpp}`, because they share the session's game, + player and chat rosters and hand the driver one answer between them, and all + three have their RmlUi view: `ui/gamelist.rml`, `ui/mphost.rml` and + `ui/mpguest.rml`, sharing `ui/lobbybase.rcss` beside `ui/optionsbase.rcss` + and each carrying its own geometry. The out-of-sync screen follows in + `code/ui/uidesync.{h,cpp}` with `ui/desynchost.rml` and `ui/desyncwait.rml` + sharing `ui/desyncbase.rcss`, converted from the `IDD_DESYNC_HOST` and + `IDD_DESYNC_WAIT` templates. The reconnect and kick-vote dialog follows in + `code/ui/uireconnect.{h,cpp}` with `ui/reconnect.rml`, converted from + `IDD_MPLAYER_DISCONNECT`. That screen has no loop of its own: + `Wait_For_Players` keeps servicing the network while the game is stalled, so + it opens the screen, services it once a pass and closes it, the way it created + and destroyed a modeless dialog. + + A list row states its own positioning context as well as its width. The + out-of-sync seat list gives each row three absolutely positioned cells, and + without `position: relative` on the row those cells resolved against the list + instead, so every seat painted over the first and a two-player game listed one + seat. A `std::vector` bound to a data model needs its array type + registered like any other; without that the binding is refused and the list + stays empty. + + A screen answered by the network rather than by a button has to be told to + step aside too. `Get_Join_Responses` writes the driver's answer straight + into `_netresponse` for a confirmed start, a rejected join and a host + signing off, and none of those changes the screen the family is on, so the + runner held the guest's document open and the guest never entered the match + the host had started. The lobby records that it has been answered and + suspends on it, which is the same hook one cause further on. + + A lobby screen changes without a result, so the runner has to be told. The + join protocol moves the family from the game list to the guest screen from + inside the presenter's service, and `UI_Run_Modal` returns only on a result + or a suspension, so the runner held the game list open and the guest + document was never reached. The family suspends when the screen it is + running moves, which is the hook step 8 added for a screen that steps aside. + + A peer is recognised by what it says it is, not by where its packet came + from. Both lobby rosters keyed on the source address, so a machine whose + packets arrive from more than one address was admitted twice, and the second + entry held the color the player had asked for, so the host gave him another + one. A chat announcement carries its sender's identifier and the node keeps + it; a player is matched on the name the join path already refuses to + duplicate. + + `Net2DisplayGameList` and `_Net2DisplayUsers` are split the way `Fill_List` + was: the presenter reads the rosters into the model and the old names put + the model on the controls. The host's accepted status is recorded with the + roster rather than while painting the row, because it is a fact about the + player rather than about the row. That split belongs above the window + check, not below it: a rebuild that happens only when there is a window to + draw into leaves a presentation that is not a window holding a stale model. + `PMessagePrintf` and the game-option decoder take the same split. + + A document has no window, so the driver asks the presenter which of the + three screens it is on rather than asking for the top window. Five places + in the lobby's protocol asked for a window's identifier where they meant + the screen, the load-bearing one being the query a host must stop sending. + + A screen answers its driver with a result as well as a response, because + the runner returns on a result; a family whose members are opened one after + another clears both before it shows the next screen. +12. **Map generator and WDT** (L). Landed: `code/ui/uimapgen.{h,cpp}` holds all + three templates as one screen, with `ui/mapgen.rml`, `ui/mapgenfs.rml` and + `ui/mapgenwdt.rml` sharing `ui/mapgenbase.rcss` beside `ui/optionsbase.rcss` + and each carrying its own geometry, converted from `IDD_MAPGEN`, + `IDD_MAPGEN_FS` and `IDD_MAPGEN_WDT`. The variant is chosen by whether + Firestorm is enabled and whether the session names a tournament territory, + not by the caller, which is step 5's shape. `IDD_WDT_PICK_CLAN` is a template + no code opens, so the WDT half has no OwnerDraw dialog of its own and the rest + of WDT stays with MSEngine. + + The load, save and delete browsers step 9 built draw where this screen is, so + the screen steps aside for them on the hook step 8 added. The seed field keeps + its place and its width but is hidden, because all three templates declare it + `NOT WS_VISIBLE` and nothing ever shows it: it is where the number lives + between `Set_Settings` and `Get_Settings` rather than something a player types + in. The environment and time of day lists are sorted by name and the two size + lists are not, because only the first two combo boxes carry `CBS_SORT`. + + The preview is resampled rather than blitted. `Bit_Blit` copies the smaller of + the two rectangles row for row, so an engine surface blit between rectangles + of different sizes crops the picture instead of scaling it; only `DSurface`'s + own blitter stretches, and a `UISurfaceBufferClass` is not one. + `MapPreviewSurfaceClass` had relied on that blit since step 10, where the two + sizes were close enough to hide it. +13. **Retire OwnerDraw** (M). Landed. `ownrdraw.cpp`, `ownrdraw.h`, + `windlg.cpp` and `windlg.h` are gone, with the legacy view behind every + migrated screen, the modeless dialog list in `msgloop.cpp`, the 53 dialog + templates in `language.rc`, the `LegacyDialogs` key and `UI_Use_Rml`. + `Language.dat` is byte-identical across the template deletion, as it was + across step 4's name table. `UI_Document_Is_Visible` went with the kill + switch: it was the coexistence check and it never had a caller, because a + legacy dialog cannot open on this fork at all. + + Six things in those files had nothing to do with dialogs and are still + wanted by unmigrated MSEngine and bespoke screens, so they moved to + `code/drawhelp.{h,cpp}`: the remapped bitmap text drawing + (`OD_Draw_Text_Remap` and the font metrics behind it), `OD_Draw_Text`, + `OD_Blend_Color` with its component masks, `WS_Get_Font` and its font cache, + `Get_Display_Rect`, and the counted pointer-capture pair. The `OD_` and `WS_` + names are kept because their callers spell them, and the header says why. + `Build_Hotkey_String` went to `keyboard.cpp` instead: it spells a key, not a + control. + + The pointer-capture pair is load-bearing and there is now exactly one + counter. `OwnerDraw::Capture_Mouse` released the game's mouse to the host so + that `WM_SETCURSOR` would fall through and the window class arrow would be + drawn, and the shell had grown a second counter of its own for documents. + Both now call the pair in `drawhelp.cpp`, so a graphic menu and a document + cannot disagree about who holds the pointer. + + `_dialog_count` became dead and took `Heal_Dialog_Controls` and + `SidebarClass::Scroll`'s guard against scrolling under a dialog with it. 14. **Sidebar** (M, then L). The model and view split with the gadget view; later the RmlUi view over the whole column and its selection key. @@ -773,7 +1160,6 @@ geometry memory are recorded on an agreed baseline before defaults change. ## Open decisions - The shipped font. -- The kill-switch key name, fixed by the change that introduces it. - The in-game text route for the sidebar view: TrueType conversions of the game fonts or a bitmap font engine for every document. - The document and binding versioning rules for mods, fixed with the first diff --git a/manual/MAINTAINING.md b/manual/MAINTAINING.md index 01a174911..d16a691ac 100644 --- a/manual/MAINTAINING.md +++ b/manual/MAINTAINING.md @@ -73,8 +73,10 @@ spelling live where it works. Command discovery is also fail-closed. Objects registered through `AllCommands` form the rebindable command catalog. Every discovered direct key handler and launch-parser branch needs exactly one public adapter or one -reasoned exclusion. Command IDs are case-sensitive. Do not infer default -bindings from a declaration or nearby code. +reasoned exclusion. Discovery reads the key names of every layer that delivers +one, so a screen driven by the UI toolkit is scanned for its key identifiers as +the game's own handlers are scanned for theirs. Command IDs are case-sensitive. +Do not infer default bindings from a declaration or nearby code. Enums are authored selections backed by explicit source adapters. Documenting an existing fixed domain is documentation work, not an engine change. Its diff --git a/manual/changes/lobby-packet-validation.md b/manual/changes/lobby-packet-validation.md new file mode 100644 index 000000000..b5c1997dc --- /dev/null +++ b/manual/changes/lobby-packet-validation.md @@ -0,0 +1,12 @@ +--- +title: Check a lobby packet before acting on it +category: fix +release: 0.2.0 +targets: +- type: system + id: network-packet-validation + effect: changed +credit: [OpenTS contributors] +--- + +A global packet that arrives while the network lobby is up is checked for a whole packet and for a terminator on every fixed wire string its handlers read, which is what an in-game packet already got. A packet that fails is counted and dropped. The lobby also refuses a join whose house or color falls outside the tables they index, ignores a game-options string longer than the field it is written into, and keeps a scenario download inside the buffer it was given. Before, a peer could make the lobby read and write past those fields. diff --git a/manual/changes/local-peer-udp-port.md b/manual/changes/local-peer-udp-port.md new file mode 100644 index 000000000..ef6043e3d --- /dev/null +++ b/manual/changes/local-peer-udp-port.md @@ -0,0 +1,12 @@ +--- +title: Hear a peer on the same machine +category: fix +release: 0.2.0 +targets: +- type: system + id: network-packet-validation + effect: changed +credit: [OpenTS contributors] +--- + +A datagram is discarded as this machine's own broadcast coming back only when its source port is the one this game is listening on, as well as its source address being one of this machine's. Before, the address alone was enough, so two copies of the game running on one machine discarded everything the other sent and never found each other. diff --git a/manual/changes/save-file-format.md b/manual/changes/save-file-format.md new file mode 100644 index 000000000..3f7bb20d0 --- /dev/null +++ b/manual/changes/save-file-format.md @@ -0,0 +1,13 @@ +--- +title: Keep saved games in a file of the engine's own +category: feature +release: 0.2.0 +targets: +- type: format + id: save-games + effect: changed +credit: +- Gunnar Beutner +--- + +A saved game is now a file of the engine's own format rather than an OLE compound document: a header, the listing details, and the game state as one compressed block, written under a temporary name and moved into place once complete. The load dialog reads only the header of each file, and a damaged or truncated file is refused before the running game is disturbed. Saved games written before this change are not read and no longer appear in the load dialog; there is no conversion. diff --git a/manual/content/formats/save-games.md b/manual/content/formats/save-games.md index 0e375f94e..f3f3fe50e 100644 --- a/manual/content/formats/save-games.md +++ b/manual/content/formats/save-games.md @@ -1,7 +1,7 @@ --- format_id: save-games title: Save games -summary: Stores versioned OpenTS game state in `.SAV` compound-document files. +summary: Stores versioned OpenTS game state in `.SAV` files of the engine's own format. kind: binary extensions: - .SAV @@ -18,6 +18,7 @@ source_files: - code/mainloop.cpp - code/mpload.cpp - code/netdlg.cpp + - code/savefile.cpp - code/saveload.cpp - code/savemgr.cpp - code/savestream.cpp @@ -30,7 +31,7 @@ source_files: - code/voc.cpp --- -The save dialog creates `.SAV` files. Each file is an OLE compound document: the listing details live in the document's own property set, and the game state goes into a single `CONTENTS` stream that is compressed as it is written. +The save dialog creates `.SAV` files. Each file begins with a fixed header and a table of the details the load dialog lists a save by, followed by the game state as one compressed block. The listing is read from the header and table alone, and a file that is truncated, damaged, or written by a later format version is refused before anything is loaded. A save is written under a temporary name and moved into place once complete, so an interrupted save leaves the previous file intact. ## Where the files are @@ -60,7 +61,7 @@ Timed saves in a game against other machines run only when a launch file set the The [`QuickSave`](/commands/quicksave/) command writes a campaign to `QUICKSAVE.SAV` and a skirmish to `QUICKSAVE_SKIRMISH.SAV`, replacing the previous file of that kind, so a skirmish never writes over a campaign. The save is written at the frame boundary after the key was pressed, once the frame has retired its dead objects, behind the saving box a menu save shows; the message list then reports `Game saved.` or that the game could not be saved. Each file is described as `Quick Save` and the scenario's description, and the load dialog lists it like any other save. A quick save starts the automatic-save interval over like any completed save. -[`QuickLoad`](/commands/quickload/) restores the file for the kind of game being played. It first reads the file's property set and refuses, with a line in the message list, when the file is missing or carries another version's stamp; otherwise the load runs when the frame ends, in the place the options menu runs, and the mission clock resumes in the restored game. A load that fails partway through the restore shows the same error box as the load dialog and leaves the player in the options menu. +[`QuickLoad`](/commands/quickload/) restores the file for the kind of game being played. It first reads the file's listing fields and refuses, with a line in the message list, when the file is missing or carries another version's stamp; otherwise the load runs when the frame ends, in the place the options menu runs, and the mission clock resumes in the restored game. A load that fails partway through the restore shows the same error box as the load dialog and leaves the player in the options menu. Both commands are refused in a game against other machines, during playback, while a scripted sequence has locked input, and once the game is being won or lost. Both arrive unbound. @@ -74,19 +75,21 @@ In a game against other machines the master can load one of the match's saved ga ## What the file holds -The property set carries the description shown in the list, the player's name and house, the campaign and scenario numbers, the game type, three timestamps, the name of the program that wrote the save, and two version stamps — the save format's own version and the build version of the game that wrote it. +The field table carries the description shown in the list, the player's name and house, the campaign and scenario numbers, the game type, three timestamps, the name of the program that wrote the save, and the build version of the game that wrote it. The header carries the format's own version. -The `CONTENTS` stream is a fixed sequence of records — the scenario, the environment, the rules, the map, the loose global values, and every list of type definitions and runtime objects — written and read back in the same order. Among the loose values are the looping sounds left at waypoints by [Play Sound Effect At](/mapping/actions/taction-play-sound-at/) and the sounds attached to objects, each as the sound and its place or object; the playing sound itself is not saved and starts again on the first sound tick after the load. Each list stores its own length ahead of its members, and each member writes out the members its class declares, in the order that class lists them. What a save holds is therefore a field-by-field record of each object rather than a copy of the bytes it occupied in memory. Type definitions travel with the save, so a save carries the rules types it was made with rather than looking them up again on load. Artwork does not travel with it: once a restored type's members have been read, its shape and voxel pointers are released and fetched from the archives again, so a save loaded against a changed set of files gets the current artwork. One piece does not come back. A UnitType drawn from shapes is given a [voxel turret](/formats/vxl-hva/) when the rules are read, by a routine no restore calls; the restore takes the ordinary voxel path instead, which releases that turret along with the body model it could not find. Its voxel barrel is fetched back, and the barrel is what the shape path draws. +The game state is a fixed sequence of records — the scenario, the environment, the rules, the map, the loose global values, and every list of type definitions and runtime objects — written and read back in the same order. Among the loose values are the looping sounds left at waypoints by [Play Sound Effect At](/mapping/actions/taction-play-sound-at/) and the sounds attached to objects, each as the sound and its place or object; the playing sound itself is not saved and starts again on the first sound tick after the load. Each list stores its own length ahead of its members, and each member writes out the members its class declares, in the order that class lists them. What a save holds is therefore a field-by-field record of each object rather than a copy of the bytes it occupied in memory. Type definitions travel with the save, so a save carries the rules types it was made with rather than looking them up again on load. Artwork does not travel with it: once a restored type's members have been read, its shape and voxel pointers are released and fetched from the archives again, so a save loaded against a changed set of files gets the current artwork. One piece does not come back. A UnitType drawn from shapes is given a [voxel turret](/formats/vxl-hva/) when the rules are read, by a routine no restore calls; the restore takes the ordinary voxel path instead, which releases that turret along with the body model it could not find. Its voxel barrel is fetched back, and the barrel is what the shape path draws. The scenario record also holds the scenario file itself, name and bytes, where the deployment's [`CarryScenarioFile`](/formats/opents-ini/#what-a-save-carries) asks for it; the record is written either way, empty when nothing is carried. A [restart or replay](/systems/campaign-progression/#losing-and-restarting) after a load reads that copy, not the file on disk, which a client resuming the save may have replaced. A random map holds no file. ## What is checked The project-version stamp decides whether a file is offered at all, and only -the running version's stamp is accepted. The load dialog reads the property set -of every `.SAV` in the saved-games folder and skips every file stamped by -anything else, including the Tiberian Sun release and another OpenTS -release-cycle version. A save that reaches the engine without passing through +the running version's stamp is accepted. The load dialog reads the header and +field table of every `.SAV` in the saved-games folder and skips every file +stamped by anything else, including another OpenTS release-cycle version. A +file in the compound-document layout that earlier OpenTS releases and Tiberian +Sun wrote is not a saved game to this reader and is skipped as well; there is +no conversion. A save that reaches the engine without passing through the dialog, as a network save or one resumed from a [launch file](/formats/spawn-ini/) does, is checked the same way and refused. Development snapshots within one cycle share the stamp, and their save layouts @@ -95,4 +98,4 @@ leading `*`. Beyond that stamp and the add-on the scenario declares, nothing about a save is measured against the game it is being loaded into. A save made under one set of rules and loaded under another is not detected, and the type definitions stored in the file are simply restored over the ones the rules built. -Reading `CONTENTS` clears the scenario before it starts and gives up at the first record it cannot restore. A load that stops there fails, rather than carrying on into a scenario that was cleared and never refilled. +The file is read and checked in full, checksums included, before the running game is touched, so a truncated or damaged file is refused at no cost. Restoring the game state then clears the scenario before it starts and gives up at the first record it cannot restore. A load that stops there fails, rather than carrying on into a scenario that was cleared and never refilled. diff --git a/manual/content/formats/vqa.md b/manual/content/formats/vqa.md index 0ad066db4..8572824de 100644 --- a/manual/content/formats/vqa.md +++ b/manual/content/formats/vqa.md @@ -90,13 +90,9 @@ The record also carries the block dimensions, a single-color count, the codebook Two entries in that list decide nothing. The drawing position is read only where a movie is placed by offset instead of being centered or given a destination, and nothing asks for that. The second audio track's three fields are read only where that track is selected in place of the first, and nothing selects it, so a movie carrying two tracks plays its first one. -:::caution[Movies in a cached archive do not play] -The offset the archive reader seeks to is measured from the start of the archive file when the archive is not cached, and from the start of the archive's data section when it is. Only the first is a position within the file it then opens, so a movie inside an archive that was cached at startup is read from the wrong place, fails the container check, and is passed over in silence. Among the numbered expansion archives, the `ECACHE` set is cached and the `EXPAND` set is not, and of the two patch archives `PCACHE.MIX` is cached while `PATCH.MIX` is not. [MIX archives](/formats/mix/) covers what caching does. -::: +A movie is read from the archive file itself rather than from a cached copy, so the position the reader seeks to is measured from the start of that file whether or not the archive was cached. [MIX archives](/formats/mix/) covers what caching does. -:::danger[A long movie name overruns the buffer the filename is built in] -The filename is assembled in a fixed twenty-byte buffer. `.VQA` takes four of those bytes and the string terminator a fifth, so a registered name of fifteen characters fills the buffer exactly and a sixteenth character writes one byte past its end. The registry accepts names of up to thirty-one characters, and playing a movie registered at that length writes sixteen bytes over whatever follows the buffer. The names the game ships with are all eight characters or fewer. -::: +The filename is built from the registered name and `.VQA` into a buffer sized for the longest name the registry accepts, so a name of any length the registry admits is truncated rather than written past the end. The names the game ships with are all eight characters or fewer. ## Sound and picture diff --git a/manual/content/internals/class-hierarchy.md b/manual/content/internals/class-hierarchy.md index 0e3097469..97897fc73 100644 --- a/manual/content/internals/class-hierarchy.md +++ b/manual/content/internals/class-hierarchy.md @@ -22,7 +22,7 @@ source_files: `AbstractClass` is the common base for persistent engine entities. Map objects and INI-backed type definitions are separate branches of that hierarchy. A runtime instance stores state for one object in the current match; a type definition stores data shared by every instance with the same INI identifier. -This page covers simulation objects and their definitions. UI controls, file classes, and locomotion COM objects use other hierarchies. +This page covers simulation objects and their definitions. UI controls, file classes, and locomotors use other hierarchies. ## Terms diff --git a/manual/content/internals/locomotion.md b/manual/content/internals/locomotion.md index 2ac0434cb..d2055d75e 100644 --- a/manual/content/internals/locomotion.md +++ b/manual/content/internals/locomotion.md @@ -14,13 +14,13 @@ source_files: - code/droppod.cpp --- -`FootClass::Locomotion` is the current `ILocomotion` COM interface for one mobile runtime instance. The locomotor is a separate object linked to the `FootClass`; it is not a behavioral base class of `FootClass`. +`FootClass::Locomotion` owns the current `ILocomotion` locomotor for one mobile runtime instance. The locomotor is a separate object linked to the `FootClass`; it is not a behavioral base class of `FootClass`. ## Object locomotion -`TechnoTypeClass::Locomotor` stores the CLSID used to create a type's ordinary locomotor. Concrete `FootClass` constructors create that COM object, call `Link_To_Object`, and assign it to `FootClass::Locomotion`. +`TechnoTypeClass::Locomotor` stores the class identifier used to create a type's ordinary locomotor. Concrete `FootClass` constructors create that locomotor, call `Link_To_Object`, and assign it to `FootClass::Locomotion`. -Movement, destination, layer, occupation, and locomotor-specific drawing queries go through the current interface. Code must therefore inspect the runtime `Locomotion` pointer when temporary locomotion is possible; the type's `Locomotor` CLSID describes the ordinary implementation, not necessarily the one currently in control. +Movement, destination, layer, occupation, and locomotor-specific drawing queries go through the current interface. Code must therefore inspect the runtime `Locomotion` pointer when temporary locomotion is possible; the type's `Locomotor` identifier describes the ordinary implementation, not necessarily the one currently in control. ## Piggybacking @@ -28,16 +28,16 @@ Movement, destination, layer, occupation, and locomotor-specific drawing queries | Operation | State transition | | --- | --- | -| `Begin_Piggyback(previous)` | Stores `previous` inside the new locomotor. A null pointer returns `E_POINTER`; an already occupied slot returns `E_FAIL`. | +| `Begin_Piggyback(previous)` | Takes ownership of `previous` and stores it inside the new locomotor. It refuses a null locomotor or an already occupied slot, and a refused locomotor is destroyed rather than returned to the caller. | | Replace `FootClass::Locomotion` | Makes the new locomotor the object's active movement interface. The new locomotor must already be linked to the same object. | -| `End_Piggyback(&FootClass::Locomotion)` | Writes the stored locomotor back into the object member and releases the piggyback slot. No stored locomotor returns `S_FALSE`; a null output pointer returns `E_POINTER`. | +| `End_Piggyback()` | Gives the stored locomotor back to the caller and empties the piggyback slot. It gives back nothing when no locomotor is stored. | -`FootClass::Link_DropPod` applies this sequence with the ballistic locomotor: it retains the passenger's current locomotor through `Begin_Piggyback`, then installs the ballistic interface. Drop-pod touchdown passes the address of `FootClass::Locomotion` to `End_Piggyback` before attempting ground placement. +`FootClass::Link_DropPod` applies this sequence with the ballistic locomotor: it retains the passenger's current locomotor through `Begin_Piggyback`, then installs the ballistic interface. Drop-pod touchdown assigns what `End_Piggyback` gives back to `FootClass::Locomotion` before attempting ground placement. Callers that perform opportunistic restoration first consult `Is_Ok_To_End`. The drop-pod touchdown path calls `End_Piggyback` directly at ground contact because its descent state already establishes the transition. ## Persistence identity -`FootClass::Serialize` writes the active locomotor through `IPersistStream` when saving and restores it through `OleLoadFromStream` when loading. A piggyback-capable locomotor writes whether it carries another locomotor and serializes that nested COM object when present. A save made during a temporary movement state therefore retains both the active locomotor and the one to restore. +`FootClass::Serialize` writes the active locomotor as a record of its own, headed by its class identifier, and recreates it from that identifier when loading. A piggyback-capable locomotor writes whether it carries another locomotor and serializes that nested locomotor when present. A save made during a temporary movement state therefore retains both the active locomotor and the one to restore. -`GetClassID` identifies the active locomotor implementation. `Piggyback_CLSID` returns the carried locomotor's `GetClassID` while piggybacking and the active locomotor's ID otherwise. These identities are distinct while a temporary locomotor is in control. +`Class_ID` identifies the active locomotor implementation, and the carried locomotor keeps its own. These identities are distinct while a temporary locomotor is in control. diff --git a/manual/content/keys/fullscreen.md b/manual/content/keys/fullscreen.md index a28138e2e..b339328f2 100644 --- a/manual/content/keys/fullscreen.md +++ b/manual/content/keys/fullscreen.md @@ -8,7 +8,7 @@ when_omitted: A full-screen game opens a borderless window the size of the desktop. A windowed game opens an ordinary framed window that can be moved, resized, and maximized. Neither one changes the desktop's own resolution: the game always renders at [`ScreenWidth`](/keys/screenwidth/) by [`ScreenHeight`](/keys/screenheight/) and that picture is scaled into whichever window it has, so alt-tabbing away and back does not disturb the rest of the desktop. -This setting is read before the window is created, well before the rest of `SUN.INI`, and it is written back whenever the game saves its options. +This setting is read before the window is created, well before the rest of `SUN.INI`, and it is written back whenever the game saves its options. The display options screen offers it as a check box beside the resolution list and moves the window as soon as the screen is accepted, without the trial and rollback a resolution gets: the frame keeps the resolution it had and the player can see straight away whether the screen is covered. The [`-WIN`](/using/command-line/windowed/) command line option asks for a window regardless of what this setting says. It applies to that run only and is never written back, so a launcher can offer a window without disturbing the player's own preference. diff --git a/manual/content/keys/gamespeed.md b/manual/content/keys/gamespeed.md index 1bb84e16f..1903bd9bd 100644 --- a/manual/content/keys/gamespeed.md +++ b/manual/content/keys/gamespeed.md @@ -7,7 +7,7 @@ when_omitted: value: "3" --- -A frame is not begun until the delay has run out, so a larger figure gives a slower game: `0` runs as fast as the machine manages and `3` holds the game to twenty frames a second at most. The delay governs a single player mission, a skirmish, and a network game still using the older command protocol; a network game on the current protocol turns the figure into a frame rate instead — 60 at `0`, 45 at `1`, and sixty divided by the figure above that — and runs at whichever is lower, that or the rate the machines can sustain. +A frame is not begun until the delay has run out, so a larger figure gives a slower game: `0` holds the game to sixty frames a second and `3` to twenty at most. The delay governs a single player mission, a skirmish, and a network game still using the older command protocol; a network game on the current protocol turns the figure into a frame rate instead — 60 at `0`, 45 at `1`, and sixty divided by the figure above that — and runs at whichever is lower, that or the rate the machines can sustain. `0` names no delay at all, so the pace used to be whatever the display imposed; a machine that draws faster than the display once did ran the simulation away from the player, and the figure is now floored at one sixtieth of a second on the delay path as well. The same figure rescales the delays that have to keep their real-world timing whatever the frame rate is — building animations, infantry sequences and the pauses between EVA reminders — so that lowering it speeds the game up without speeding those up in proportion. diff --git a/manual/content/keys/screenwidth.md b/manual/content/keys/screenwidth.md index 2c26fb653..2dace920b 100644 --- a/manual/content/keys/screenwidth.md +++ b/manual/content/keys/screenwidth.md @@ -2,3 +2,7 @@ key: ScreenWidth summary: The width in pixels of the game screen. --- + +This is the resolution the game renders at, not the size the picture is shown at. Raising it does not enlarge anything: it gives the tactical view more cells and the sidebar more room, while the front-end artwork keeps its own pixel size and sits in the middle of a larger, mostly empty screen. The menus, the score screens and the dialog panels are all drawn at 640 by 400 and none of them is scaled up to a larger screen. + +To fill a larger window with the game as it was drawn, leave this and [`ScreenHeight`](/keys/screenheight/) at 640 by 400 and set [`WindowWidth`](/keys/windowwidth/) and [`WindowHeight`](/keys/windowheight/) to the size wanted; the picture is then scaled to fit. A full-screen game already does this, because it covers the desktop and scales the picture into it. diff --git a/manual/data/command-adapters.yaml b/manual/data/command-adapters.yaml index 3496023ef..30a72383f 100644 --- a/manual/data/command-adapters.yaml +++ b/manual/data/command-adapters.yaml @@ -233,7 +233,7 @@ fixed_controls: context: Classic main menu availability: *all sites: - - { file: code/init.cpp, function: Main_Menu, expression: KN_V+KN_CTRL_BIT } + - { file: code/ui/uimainmenu.cpp, function: BindEventCallback, expression: KI_V } - id: fixed:main-menu-credits title: Open credits description: Opens the credits from the classic main menu. @@ -242,7 +242,7 @@ fixed_controls: context: Classic main menu availability: *all sites: - - { file: code/init.cpp, function: Main_Menu, expression: VK_C+KN_CTRL_BIT+KN_ALT_BIT } + - { file: code/ui/uimainmenu.cpp, function: BindEventCallback, expression: KI_C } - id: fixed:skip-credits title: Exit credits description: Stops the credits and returns to the menu. @@ -349,8 +349,6 @@ fixed_exclusions: reason: Generic graphic-menu polling and activation, not a named application control. - site: { file: code/grphmsct.cpp, function: GM_Build_Key, expression: VK_NONE } reason: Sentinel used while translating a menu accelerator into a virtual key. - - site: { file: code/init.cpp, function: Main_Menu, expression: KN_RLSE_BIT } - reason: Filters release events before the classic main-menu cheat handler. - sites: - { file: code/init.cpp, function: Init_Commands, expression: KN_DELETE } - { file: code/init.cpp, function: Init_Commands, expression: KN_ESC } @@ -389,16 +387,10 @@ fixed_exclusions: - { file: code/msglist.cpp, function: MessageListClass::Input, expression: KN_BACKSPACE } - { file: code/msglist.cpp, function: MessageListClass::Input, expression: KN_ESC } reason: Message editor polling, cursor placement, and text-edit termination. - - site: { file: code/ownrdraw.cpp, function: CtrlProc_Internal, expression: VK_TAB } - reason: Windows dialog focus traversal handled by the shared control procedure. - sites: - - { file: code/ownrdraw.cpp, function: EditBoxCtrlProc, expression: VK_TAB } - - { file: code/ownrdraw.cpp, function: EditBoxCtrlProc, expression: VK_RETURN } - reason: Windows edit-control navigation handled by the shared dialog procedure. - - sites: - - { file: code/ownrdraw.cpp, function: Build_Hotkey_String, expression: VK_MENU } - - { file: code/ownrdraw.cpp, function: Build_Hotkey_String, expression: VK_CONTROL } - - { file: code/ownrdraw.cpp, function: Build_Hotkey_String, expression: VK_SHIFT } + - { file: code/keyboard.cpp, function: Build_Hotkey_String, expression: VK_MENU } + - { file: code/keyboard.cpp, function: Build_Hotkey_String, expression: VK_CONTROL } + - { file: code/keyboard.cpp, function: Build_Hotkey_String, expression: VK_SHIFT } reason: Converts modifier virtual keys to display names; it does not consume input. - sites: - { file: code/restate.cpp, function: RestateMission::User_Input, expression: KN_NONE } @@ -422,6 +414,113 @@ fixed_exclusions: reason: Encoded sidebar gadget IDs and input flags, not physical bindings. - site: { file: code/tab.cpp, function: TabClass::AI, expression: KN_LMOUSE } reason: Tactical-tab hit-testing handled as pointer input. + - sites: + - { file: code/ui/uishell.cpp, function: Key_Identifier, expression: VK_F1 } + - { file: code/ui/uishell.cpp, function: Key_Identifier, expression: VK_F24 } + reason: >- + Bounds of the function-key run in the translation from a Windows virtual key to the + identifier the UI toolkit names it by. The shell hands the key to whichever document + has the input scope; the key's meaning is the document's, so no site here is a game + command. + - sites: + - { file: code/ui/uikeyboard.cpp, function: Is_Modifier_Key, expression: VK_SHIFT } + - { file: code/ui/uikeyboard.cpp, function: Is_Modifier_Key, expression: VK_CONTROL } + - { file: code/ui/uikeyboard.cpp, function: Is_Modifier_Key, expression: VK_MENU } + reason: >- + Recognizes a modifier held on its own so the hotkey capture control waits for the key + it qualifies. It classifies a keypress and dispatches nothing. + - sites: + - { file: code/ui/uishell.cpp, function: Key_Modifiers, expression: VK_CONTROL } + - { file: code/ui/uishell.cpp, function: Key_Modifiers, expression: VK_SHIFT } + - { file: code/ui/uishell.cpp, function: Key_Modifiers, expression: VK_MENU } + reason: Reads the modifier keys held while a UI message is delivered. It reports state and dispatches nothing. + - sites: + - { file: code/ui/uishell.cpp, function: Handle_Developer_Key, expression: VK_CONTROL, guard: "!NDEBUG" } + - { file: code/ui/uishell.cpp, function: Handle_Developer_Key, expression: VK_SHIFT, guard: "!NDEBUG" } + - { file: code/ui/uishell.cpp, function: Handle_Developer_Key, expression: VK_CONTROL+VK_SHIFT, guard: "!NDEBUG" } + reason: >- + Reads the modifiers that qualify the shell's Debug-only developer keys. The keys those + modifiers qualify are the controls; this test is not one of them. + - sites: + - { file: code/ui/uiabort.cpp, function: BindEventCallback, expression: KI_ESCAPE } + - { file: code/ui/uicampaign.cpp, function: BindEventCallback, expression: KI_ESCAPE } + - { file: code/ui/uicampaign.cpp, function: BindEventCallback, expression: KI_RETURN } + - { file: code/ui/uicampaign.cpp, function: BindEventCallback, expression: KI_NUMPADENTER } + - { file: code/ui/uidisplayconfirm.cpp, function: BindEventCallback, expression: KI_ESCAPE } + - { file: code/ui/uidisplayconfirm.cpp, function: BindEventCallback, expression: KI_RETURN } + - { file: code/ui/uidisplayconfirm.cpp, function: BindEventCallback, expression: KI_NUMPADENTER } + - { file: code/ui/uidisplayoptions.cpp, function: BindEventCallback, expression: KI_ESCAPE } + - { file: code/ui/uidisplayoptions.cpp, function: BindEventCallback, expression: KI_RETURN } + - { file: code/ui/uidisplayoptions.cpp, function: BindEventCallback, expression: KI_NUMPADENTER } + - { file: code/ui/uigamecontrols.cpp, function: BindEventCallback, expression: KI_ESCAPE } + - { file: code/ui/uigamecontrols.cpp, function: BindEventCallback, expression: KI_RETURN } + - { file: code/ui/uigamecontrols.cpp, function: BindEventCallback, expression: KI_NUMPADENTER } + - { file: code/ui/uigameoptions.cpp, function: BindEventCallback, expression: KI_ESCAPE } + - { file: code/ui/uigameoptions.cpp, function: BindEventCallback, expression: KI_RETURN } + - { file: code/ui/uigameoptions.cpp, function: BindEventCallback, expression: KI_NUMPADENTER } + - { file: code/ui/uigametype.cpp, function: BindEventCallback, expression: KI_ESCAPE } + - { file: code/ui/uigametype.cpp, function: BindEventCallback, expression: KI_RETURN } + - { file: code/ui/uigametype.cpp, function: BindEventCallback, expression: KI_NUMPADENTER } + - { file: code/ui/uilobby.cpp, function: BindEventCallback, expression: KI_ESCAPE } + - { file: code/ui/uimainoptions.cpp, function: BindEventCallback, expression: KI_ESCAPE } + - { file: code/ui/uimainoptions.cpp, function: BindEventCallback, expression: KI_RETURN } + - { file: code/ui/uimainoptions.cpp, function: BindEventCallback, expression: KI_NUMPADENTER } + - { file: code/ui/uimapgen.cpp, function: BindEventCallback, expression: KI_ESCAPE } + - { file: code/ui/uimessagebox.cpp, function: BindEventCallback, expression: KI_ESCAPE } + - { file: code/ui/uimessagebox.cpp, function: BindEventCallback, expression: KI_RETURN } + - { file: code/ui/uimessagebox.cpp, function: BindEventCallback, expression: KI_NUMPADENTER } + - { file: code/ui/uimpselect.cpp, function: BindEventCallback, expression: KI_ESCAPE } + - { file: code/ui/uimpselect.cpp, function: BindEventCallback, expression: KI_RETURN } + - { file: code/ui/uimpselect.cpp, function: BindEventCallback, expression: KI_NUMPADENTER } + - { file: code/ui/uireconnect.cpp, function: BindEventCallback, expression: KI_ESCAPE } + - { file: code/ui/uisavebrowser.cpp, function: BindEventCallback, expression: KI_ESCAPE } + - { file: code/ui/uisavebrowser.cpp, function: BindEventCallback, expression: KI_RETURN } + - { file: code/ui/uisavebrowser.cpp, function: BindEventCallback, expression: KI_NUMPADENTER } + - { file: code/ui/uiscenariopick.cpp, function: BindEventCallback, expression: KI_ESCAPE } + - { file: code/ui/uiscenariopick.cpp, function: BindEventCallback, expression: KI_RETURN } + - { file: code/ui/uiscenariopick.cpp, function: BindEventCallback, expression: KI_NUMPADENTER } + - { file: code/ui/uiskirmish.cpp, function: BindEventCallback, expression: KI_ESCAPE } + - { file: code/ui/uiskirmish.cpp, function: BindEventCallback, expression: KI_RETURN } + - { file: code/ui/uiskirmish.cpp, function: BindEventCallback, expression: KI_NUMPADENTER } + - { file: code/ui/uisound.cpp, function: BindEventCallback, expression: KI_RETURN } + - { file: code/ui/uisound.cpp, function: BindEventCallback, expression: KI_NUMPADENTER } + - { file: code/ui/uiversion.cpp, function: BindEventCallback, expression: KI_ESCAPE } + - { file: code/ui/uiversion.cpp, function: BindEventCallback, expression: KI_RETURN } + - { file: code/ui/uiversion.cpp, function: BindEventCallback, expression: KI_NUMPADENTER } + reason: >- + A screen's own cancel and accept keys. Escape answers as the template's IDCANCEL did + and Enter as the arm Windows sent IDOK to, so each key presses a button the screen + already shows rather than dispatching a control of its own. + - sites: + - { file: code/ui/uidesync.cpp, function: BindEventCallback, expression: KI_RETURN } + - { file: code/ui/uidesync.cpp, function: BindEventCallback, expression: KI_NUMPADENTER } + - { file: code/ui/uilobby.cpp, function: BindEventCallback, expression: KI_RETURN } + - { file: code/ui/uilobby.cpp, function: BindEventCallback, expression: KI_NUMPADENTER } + reason: Sends the line typed in a chat field, which the edit control's own return handling did. + - sites: + - { file: code/ui/uikeyboard.cpp, function: BindEventCallback, expression: KI_ESCAPE } + - { file: code/ui/uikeyboard.cpp, function: BindEventCallback, expression: KI_RETURN } + - { file: code/ui/uikeyboard.cpp, function: BindEventCallback, expression: KI_NUMPADENTER } + - { file: code/ui/uikeyboard.cpp, function: BindEventCallback, expression: KI_TAB } + reason: >- + Keys the hotkey capture control refuses so they still leave the screen or move the + focus, which is what the control it stands in for left to the dialog manager. + - sites: + - { file: code/ui/uishell.cpp, function: UI_Virtual_Key, expression: KI_A } + - { file: code/ui/uishell.cpp, function: UI_Virtual_Key, expression: KI_Z } + - { file: code/ui/uishell.cpp, function: UI_Virtual_Key, expression: KI_0 } + - { file: code/ui/uishell.cpp, function: UI_Virtual_Key, expression: KI_9 } + - { file: code/ui/uishell.cpp, function: UI_Virtual_Key, expression: KI_NUMPAD0 } + - { file: code/ui/uishell.cpp, function: UI_Virtual_Key, expression: KI_NUMPAD9 } + - { file: code/ui/uishell.cpp, function: UI_Virtual_Key, expression: KI_F1 } + - { file: code/ui/uishell.cpp, function: UI_Virtual_Key, expression: KI_F24 } + - { file: code/ui/uishell.cpp, function: UI_Virtual_Key, expression: KI_NUMPADENTER } + reason: >- + Bounds of the letter, digit, keypad and function-key runs, and the keypad Enter that + folds onto Return, in the translation from a toolkit identifier back to a virtual key. + It names a key for a screen to record; it dispatches nothing. + - site: { file: code/ui/uishell.cpp, function: UI_Handle_Window_Message, expression: KI_UNKNOWN } + reason: Sentinel for a key the toolkit does not name, which the shell drops before any document sees it. - sites: - { file: code/wdtsel.cpp, function: Selection::Pick_Territory, expression: KN_NONE } - { file: code/wdtsel.cpp, function: Selection::Pick_Territory, expression: KN_LMOUSE } diff --git a/manual/data/commands.yaml b/manual/data/commands.yaml index d286e7be0..9778cd0f7 100644 --- a/manual/data/commands.yaml +++ b/manual/data/commands.yaml @@ -2128,7 +2128,7 @@ fixed_controls: - Ctrl+V context: Classic main menu _provenance: - source: code/init.cpp + source: code/ui/uimainmenu.cpp guard: null - id: fixed:main-menu-credits route_id: fixed-main-menu-credits @@ -2141,7 +2141,7 @@ fixed_controls: - Ctrl+Alt+C context: Classic main menu _provenance: - source: code/init.cpp + source: code/ui/uimainmenu.cpp guard: null - id: fixed:skip-credits route_id: fixed-skip-credits diff --git a/manual/data/ini-keys.yaml b/manual/data/ini-keys.yaml index 2f4d3e79a..dd6af0039 100644 --- a/manual/data/ini-keys.yaml +++ b/manual/data/ini-keys.yaml @@ -13123,10 +13123,10 @@ Locomotor: section: kind: identifier source: object-type - value_type: Locomotor CLSID + value_type: classid status: generated _provenance: - default_candidate: the Teleport locomotor + default_candidate: null declared_in: TechnoTypeClass member: Locomotor source: code/techtype.cpp diff --git a/manual/site/scripts/check-render.mjs b/manual/site/scripts/check-render.mjs index 94a46df6e..e6d7b1f77 100644 --- a/manual/site/scripts/check-render.mjs +++ b/manual/site/scripts/check-render.mjs @@ -57,7 +57,7 @@ const cases = [ ['mapping/missions/tmission-loop/index.html', ['Jump to line', 'one-based']], ['internals/class-hierarchy/index.html', ['Object and type system', 'Primary runtime hierarchy', 'Type-definition hierarchy', 'AbstractTypeClass', 'code/abstype.h']], ['internals/radio/index.html', ['Radio contact protocol', 'Contact state', 'Messages and responses', 'Compute_CRC', 'code/radio.cpp']], - ['internals/locomotion/index.html', ['Locomotion and piggybacking', 'FootClass::Locomotion', 'IPiggyback', 'Piggyback_CLSID']], + ['internals/locomotion/index.html', ['Locomotion and piggybacking', 'FootClass::Locomotion', 'IPiggyback', 'Class_ID']], ['reference/enums/mission/index.html', ['data-enum-table', 'MISSION_HUNT', 'Stored value', 'Used by', 'TMISSION_DO']], ['systems/drop-pods/index.html', ['ots-page-subtitle', 'Entry paths', 'Approach and descent', 'Touchdown']], ['systems/base-adjacency/index.html', ['ots-page-subtitle', 'Base placement and adjacency', 'Placement decision order', 'Adjacent']], diff --git a/manual/site/tests/documentation-source-contract.test.mjs b/manual/site/tests/documentation-source-contract.test.mjs index d45716bbd..4dc60e2c6 100644 --- a/manual/site/tests/documentation-source-contract.test.mjs +++ b/manual/site/tests/documentation-source-contract.test.mjs @@ -36,7 +36,7 @@ test('Drop pod approach selection keeps its ordered candidates and unconditional const droppod = source('code/droppod.cpp'); const moveTo = functionBody( droppod, - 'void STDMETHODCALLTYPE DropPodLocomotionClass::Move_To(Coord to)', + 'void DropPodLocomotionClass::Move_To(Coord to)', ); assert.match( @@ -74,7 +74,7 @@ test('Drop pod directions retain their hard-coded airborne and landing-art mappi const drawingCode = functionBody( droppod, - 'int STDMETHODCALLTYPE DropPodLocomotionClass::Drawing_Code(void)', + 'int DropPodLocomotionClass::Drawing_Code(void)', ); assert.match(drawingCode, /Direction\s*%\s*2/); assertOrdered(infantry, [ @@ -84,13 +84,13 @@ test('Drop pod directions retain their hard-coded airborne and landing-art mappi const process = functionBody( droppod, - 'boolean STDMETHODCALLTYPE DropPodLocomotionClass::Process(void)', + 'bool DropPodLocomotionClass::Process(void)', ); assert.match(process, /Rule->DropPod\[Direction\s*%\s*Rule->DropPod\.Count\(\)\]/); const moveTo = functionBody( droppod, - 'void STDMETHODCALLTYPE DropPodLocomotionClass::Move_To(Coord to)', + 'void DropPodLocomotionClass::Move_To(Coord to)', ); assertOrdered(moveTo, [ 'dropcoord.Z += Rule->DropPodHeight;', @@ -102,13 +102,13 @@ test('Drop pod directions retain their hard-coded airborne and landing-art mappi test('Blocked Drop pod touchdown retains its exact damage, animation, and deletion payload', () => { const process = functionBody( source('code/droppod.cpp'), - 'boolean STDMETHODCALLTYPE DropPodLocomotionClass::Process(void)', + 'bool DropPodLocomotionClass::Process(void)', ); assertOrdered(process, [ 'FootClass * linked = LinkedTo;', 'coord = linked->PositionCoord;', 'linked->Limbo();', - 'End_Piggyback(&LinkedTo->Locomotion);', + 'LinkedTo->Locomotion = End_Piggyback();', 'if (!linked->Unlimbo(coord, DIR_N)) {', 'Explosion_Damage(coord, 100, LinkedTo, Rule->C4Warhead);', 'Combat_Anim(100, Rule->C4Warhead, LAND_CLEAR, coord)', @@ -414,15 +414,11 @@ test('A resume is judged before it is loaded, and the save answers for the rest' 'gameloaded = true;', ], 'a network resume seats the players and opens the network before the save is read'); - for (const dialog of ['IDD_OPT_CTRL_WOL']) { - const template = source('code/language/language.rc'); - const body = template.slice(template.indexOf(dialog + ' DIALOG')); - assert.match( - body.slice(0, body.indexOf('END')), - /IDC_SAVE_GAME/, - `${dialog} offers the synchronized save the options handler has always known`, - ); - } + assert.match( + source('ui/gameoptionswol.rml'), + /id="save" data-class-disabled="!cansave" data-event-click="press\('save'\)"/, + 'the internet options offer the synchronized save the options screen has always known', + ); assertOrdered(functionBody(source('code/saveload.cpp'), 'bool Reconcile_Players(void)'), [ 'stricmp(Session.Players[i]->Name, Houses[house]->IniName) == 0', @@ -456,14 +452,14 @@ test('Saved games are named in one folder rather than searched for', () => { assertOrdered(functionBody(gamedirs, 'std::string Saved_Game_Name(char const * filename)'), [ 'UserDirectory + SavedGamesFolder', - 'CreateDirectory(folder.c_str(), NULL);', + 'Make_Directory(folder);', ], 'a saved game is named inside the user directory, and the folder is made on the way'); for (const [file, signature] of [ ['code/saveload.cpp', 'bool Save_Game(const char *file_name, char const * descr)'], ['code/saveload.cpp', 'bool Load_Game(const char *file_name)'], ['code/saveload.cpp', 'bool Get_Savefile_Info(char const * name, SaveVersionInfo * info)'], - ['code/loaddlg.cpp', 'void LoadOptionsClass::Fill_List(HWND window)'], + ['code/loaddlg.cpp', 'void LoadOptionsClass::Build_List(void)'], ['code/loaddlg.cpp', 'bool LoadOptionsClass::Files_Present(void)'], ['code/loaddlg.cpp', 'bool LoadOptionsClass::Delete_File(const char * file_name)'], ]) { @@ -475,7 +471,7 @@ test('Saved games are named in one folder rather than searched for', () => { } assert.doesNotMatch( - functionBody(source('code/loaddlg.cpp'), 'void LoadOptionsClass::Fill_List(HWND window)') + + functionBody(source('code/loaddlg.cpp'), 'void LoadOptionsClass::Build_List(void)') + functionBody(source('code/loaddlg.cpp'), 'bool LoadOptionsClass::Files_Present(void)'), /Search_Files\(/, 'the listing no longer scans the folders the game reads from', @@ -527,20 +523,24 @@ test('A multiplayer load replaces the match around the seats it keeps', () => { 'Reset_Multiplayer_Save_State();', ], 'the old traffic is discarded, the save read, the seats matched, and the connections rebuilt in that order'); - const template = source('code/language/language.rc'); - const body = template.slice(template.indexOf('IDD_OPT_CTRL_WOL DIALOG')); assert.match( - body.slice(0, body.indexOf('END')), - /IDC_LOAD_GAME/, + source('ui/gameoptionswol.rml'), + /id="load" data-class-disabled="!canload" data-event-click="press\('load'\)"/, 'the internet options offer the load the master starts for every machine', ); - assertOrdered(definitionFrom(source('code/goptions.cpp'), 'INT_PTR CALLBACK Game_Options_Dialog_Proc(HWND window, UINT message, WPARAM wparam, LPARAM lparam)'), [ - 'case IDC_LOAD_GAME:', - 'LoadOptionsClass().Load()', - 'Multiplayer_Load_Is_Allowed()', + const gameoptions = source('code/ui/uigameoptions.cpp'); + assertOrdered(functionBody(gameoptions, 'void UIGameOptionsPresenterClass::Execute(UIIntent const & intent)'), [ + 'intent.Action == UI_GAMEOPT_LOAD', + 'Is_Solo_Session()', + 'Pending = SUB_LOAD;', + 'SaveManager.Multiplayer_Load_Is_Allowed()', 'SpecialDialog = SDLG_LOAD;', - ], 'a network game defers the list to the menu loop rather than nesting it in the options dialog'); + ], 'a network game defers the list to the menu loop rather than opening it from the options screen'); + assertOrdered(functionBody(gameoptions, 'void UIGameOptionsPresenterClass::Run_Pending(void)'), [ + 'case SUB_LOAD:', + 'LoadOptionsClass().Load()', + ], 'a solo game opens the list itself'); assertOrdered(definitionFrom(source('code/conquer.cpp'), 'void Ingame_Menu_Dialog(void)'), [ 'case SDLG_OPTIONS:', @@ -817,12 +817,28 @@ test('A computer player draws a country from the lobby roster', () => { }); test('A lobby side entry carries its country', () => { - const netdlg = source('code/netdlg2.cpp'); + const lobby = source('code/ui/uilobby.cpp'); + const skirmish = source('code/ui/uiskirmish.cpp'); + + for (const [screen, text, signature] of [ + ['the network lobby', lobby, 'void UILobbyPresenterClass::Build_Identity_Lists(void)'], + ['the skirmish setup', skirmish, 'void UISkirmishPresenterClass::Refresh(void)'], + ]) { + assertOrdered(functionBody(text, signature), [ + 'if (!house->IsMultiplay) continue;', + 'if (index == Session.House) {', + 'SelectedSide = (int)Sides.size();', + 'Sides.push_back(SideType{(char const *)house->GivenName, index});', + ], `${screen} lists the countries that may be played, and each entry carries its country`); + } + + assert.match(functionBody(lobby, 'void UILobbyPresenterClass::Host_Side(int row)'), /House = Sides\[row\]\.Country;/, 'the selection is read back through its country'); + assert.match(functionBody(skirmish, 'void UISkirmishPresenterClass::Read_Identity(void)'), /Session\.House = \(HousesType\)Sides\[SelectedSide\]\.Country;/, 'the skirmish entry stores a country, not a position'); - assertOrdered(functionBody(netdlg, 'void Fill_Country_Box(HWND combo)'), ['CB_INSERTSTRING', 'CB_SETITEMDATA'], 'each entry carries its country'); - assert.match(functionBody(netdlg, 'int Country_From_Box(HWND combo)'), /CB_GETITEMDATA/, 'the selection is read back through its country'); - assert.doesNotMatch(netdlg, /CB_SETCURSEL, Session\.House/, 'no box is positioned by a country index'); - assert.doesNotMatch(source('code/skirmish.cpp'), /Session\.House = ComboBox_GetCurSel/, 'the skirmish box stores a country, not a position'); + for (const text of [lobby, skirmish]) { + assert.doesNotMatch(text, /SelectedSide = (\(int\))?Session\.House/, 'no list is positioned by a country index'); + assert.doesNotMatch(text, /Session\.House = \(HousesType\)SelectedSide/, 'no position is stored as a country'); + } }); test('A side is declared in the side list alone', () => { diff --git a/manual/tools/commands_engine.py b/manual/tools/commands_engine.py index dccf3b2ce..92510f25c 100644 --- a/manual/tools/commands_engine.py +++ b/manual/tools/commands_engine.py @@ -301,7 +301,10 @@ def _guard_map(text): def _key_expression(value): - tokens = re.findall(r"\b(?:KN|VK)_[A-Z0-9_]+\b", value) + # A screen names a key the way the layer that delivers it does. The game's own + # handlers use KN_ and VK_; a screen driven by the UI toolkit sees its KI_ + # identifiers, and discovery has to reach both or a whole screen's keys go unseen. + tokens = re.findall(r"\b(?:KN|VK|KI)_[A-Z0-9_]+\b", value) return "+".join(dict.fromkeys(tokens)) if tokens else None diff --git a/platform/CMakeLists.txt b/platform/CMakeLists.txt new file mode 100644 index 000000000..2e3aee964 --- /dev/null +++ b/platform/CMakeLists.txt @@ -0,0 +1,34 @@ +# +# --------------------------------------------------------- +# Win32 compatibility layer (non-Windows targets) +# --------------------------------------------------------- +# +# The engine is written against the Win32 window, message, cursor and GDI surface: the +# window procedure, the keyboard queue, the scroll handler and every dialog driver switch +# on WM_ values. Rather than teaching those call sites a second event model, this library +# keeps the surface and supplies it from SDL, so both builds dispatch the same messages. +# +# What it implements is the window, the event loop, the cursor and the keyboard. What it +# refuses is the dialog and GDI surface, which docs/UI_DESIGN.md step 13 replaces rather +# than ports: those entry points report failure so a caller takes its own no-dialog path. +add_library(win32compat STATIC + "${CMAKE_CURRENT_SOURCE_DIR}/win32compat/src/dialog.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/win32compat/src/entry.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/win32compat/src/gdi.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/win32compat/src/input.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/win32compat/src/kernel.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/win32compat/src/message.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/win32compat/src/strings.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/win32compat/src/window.cpp" +) + +target_compile_features(win32compat PRIVATE cxx_std_20) + +# Consumers include and the rest of the surface by their Windows spellings. +target_include_directories(win32compat PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/win32compat/include") + +target_link_libraries(win32compat PUBLIC SDL3::SDL3-static) + +if(APPLE) + target_link_libraries(win32compat PUBLIC "-framework QuartzCore") +endif() diff --git a/platform/win32compat/include/comdef.h b/platform/win32compat/include/comdef.h new file mode 100644 index 000000000..0b00b9e98 --- /dev/null +++ b/platform/win32compat/include/comdef.h @@ -0,0 +1,24 @@ +#pragma once +#include + +// _com_ptr_t and the __declspec(uuid)/__uuidof machinery have no clang equivalent on a +// non-MSVC target. Declaring the template unconditionally lets the surface be counted. +template class _com_ptr_t { +public: + _com_ptr_t() = default; + T *operator->() const; + operator T *() const; + T **operator&(); +}; +#define _COM_SMARTPTR_TYPEDEF(iface, iid) typedef _com_ptr_t iface##Ptr +class _com_error { +public: + HRESULT Error() const; + const char *ErrorMessage() const; +}; + +_COM_SMARTPTR_TYPEDEF(IUnknown, 0); +_COM_SMARTPTR_TYPEDEF(IStream, 0); +_COM_SMARTPTR_TYPEDEF(IPersistStream, 0); +_COM_SMARTPTR_TYPEDEF(IPropertySetStorage, 0); +_COM_SMARTPTR_TYPEDEF(IClassFactory, 0); diff --git a/platform/win32compat/include/commctrl.h b/platform/win32compat/include/commctrl.h new file mode 100644 index 000000000..53c29ba77 --- /dev/null +++ b/platform/win32compat/include/commctrl.h @@ -0,0 +1,113 @@ +#pragma once +#include + +// Common controls exist only in the legacy dialog layer, which UI_DESIGN step 13 replaces. +// The window class names are declared so that a template that names one still compiles. +#define TRACKBAR_CLASS "msctls_trackbar32" +#define PROGRESS_CLASS "msctls_progress32" +#define HOTKEY_CLASS "msctls_hotkey32" +#define WC_TREEVIEW "SysTreeView32" +#define WC_LISTVIEW "SysListView32" +#define WC_TABCONTROL "SysTabControl32" + +#define TBM_GETPOS (WM_USER) +#define TBM_GETRANGEMIN (WM_USER + 1) +#define TBM_GETRANGEMAX (WM_USER + 2) +#define TBM_SETPOS (WM_USER + 5) +#define TBM_SETRANGE (WM_USER + 6) +#define TB_LINEUP 0 +#define TB_LINEDOWN 1 +#define TB_THUMBPOSITION 4 +#define TB_THUMBTRACK 5 +#define TB_ENDTRACK 8 + +#define PBM_SETRANGE (WM_USER + 1) +#define PBM_SETPOS (WM_USER + 2) + +#define HKM_SETHOTKEY (WM_USER + 1) +#define HKM_GETHOTKEY (WM_USER + 2) + +#define TV_FIRST 0x1100 +#define TVM_SELECTITEM (TV_FIRST + 11) +#define TVM_GETNEXTITEM (TV_FIRST + 10) +#define TVM_GETINDENT (TV_FIRST + 6) +#define TVM_GETEDITCONTROL (TV_FIRST + 15) +#define TVGN_ROOT 0 +#define TVGN_NEXT 1 +#define TVGN_PREVIOUS 2 +#define TVGN_CARET 9 +#define TVGN_FIRSTVISIBLE 5 +#define TVGN_NEXTVISIBLE 6 +#define TVGN_PREVIOUSVISIBLE 7 +#define TVGN_DROPHILITE 8 +#define TVE_COLLAPSE 0x0001 +#define TVE_EXPAND 0x0002 +#define TVIF_TEXT 0x0001 +#define TVIF_IMAGE 0x0002 +#define TVIF_PARAM 0x0004 +#define TVIF_STATE 0x0008 +#define TVIF_HANDLE 0x0010 +#define TVIF_SELECTEDIMAGE 0x0020 +#define TVIS_EXPANDED 0x0020 +#define TVIS_SELECTED 0x0002 + +typedef struct tagTVITEMA { + UINT mask; HTREEITEM hItem; UINT state, stateMask; + LPSTR pszText; int cchTextMax, iImage, iSelectedImage, cChildren; LPARAM lParam; +} TVITEMA, TVITEM, TV_ITEM; + +typedef struct tagNMHDR { HWND hwndFrom; UINT_PTR idFrom; UINT code; } NMHDR; +typedef struct tagTVDISPINFOA { NMHDR hdr; TVITEMA item; } NMTVDISPINFOA, NMTVDISPINFO; + +#define TreeView_SelectItem(hwnd, item) ((BOOL)SendMessage((hwnd), TVM_SELECTITEM, TVGN_CARET, (LPARAM)(HTREEITEM)(item))) +#define TreeView_SelectDropTarget(hwnd, item) ((BOOL)SendMessage((hwnd), TVM_SELECTITEM, TVGN_DROPHILITE, (LPARAM)(HTREEITEM)(item))) +#define TreeView_SelectSetFirstVisible(hwnd, item) ((BOOL)SendMessage((hwnd), TVM_SELECTITEM, TVGN_FIRSTVISIBLE, (LPARAM)(HTREEITEM)(item))) +#define TreeView_GetNextItem(hwnd, item, code) ((HTREEITEM)SendMessage((hwnd), TVM_GETNEXTITEM, (WPARAM)(code), (LPARAM)(HTREEITEM)(item))) +#define TreeView_GetRoot(hwnd) TreeView_GetNextItem((hwnd), NULL, TVGN_ROOT) +#define TreeView_GetNextSibling(hwnd, item) TreeView_GetNextItem((hwnd), (item), TVGN_NEXT) +#define TreeView_GetFirstVisible(hwnd) TreeView_GetNextItem((hwnd), NULL, TVGN_FIRSTVISIBLE) +#define TreeView_GetNextVisible(hwnd, item) TreeView_GetNextItem((hwnd), (item), TVGN_NEXTVISIBLE) +#define TreeView_GetPrevVisible(hwnd, item) TreeView_GetNextItem((hwnd), (item), TVGN_PREVIOUSVISIBLE) +#define TreeView_GetIndent(hwnd) ((int)SendMessage((hwnd), TVM_GETINDENT, 0, 0)) +#define TreeView_GetEditControl(hwnd) ((HWND)SendMessage((hwnd), TVM_GETEDITCONTROL, 0, 0)) + +#define LVM_FIRST 0x1000 +#define LVM_GETCOLUMNWIDTH (LVM_FIRST + 29) +#define LVM_SETCOLUMNWIDTH (LVM_FIRST + 30) +#define ListView_GetColumnWidth(hwnd, index) ((int)SendMessage((hwnd), LVM_GETCOLUMNWIDTH, (WPARAM)(int)(index), 0)) +#define ListView_SetColumnWidth(hwnd, index, width) ((BOOL)SendMessage((hwnd), LVM_SETCOLUMNWIDTH, (WPARAM)(int)(index), MAKELPARAM((width), 0))) + +#define TCM_FIRST 0x1300 +#define TCM_GETITEMCOUNT (TCM_FIRST + 4) +#define TCM_GETCURSEL (TCM_FIRST + 11) +#define TCM_GETITEMRECT (TCM_FIRST + 10) +#define TCM_SETITEMSIZE (TCM_FIRST + 41) +#define TCIF_TEXT 0x0001 +typedef struct tagTCITEMA { UINT mask; DWORD dwState, dwStateMask; LPSTR pszText; int cchTextMax, iImage; LPARAM lParam; } TCITEMA, TC_ITEM; +#define TabCtrl_GetItemCount(hwnd) ((int)SendMessage((hwnd), TCM_GETITEMCOUNT, 0, 0)) +#define TabCtrl_GetCurSel(hwnd) ((int)SendMessage((hwnd), TCM_GETCURSEL, 0, 0)) +#define TabCtrl_GetItemRect(hwnd, index, rect) ((BOOL)SendMessage((hwnd), TCM_GETITEMRECT, (WPARAM)(int)(index), (LPARAM)(RECT *)(rect))) + +extern "C" { +void InitCommonControls(void); +BOOL ImageList_BeginDrag(HIMAGELIST list, int image, int x, int y); +BOOL ImageList_DragEnter(HWND lock, int x, int y); +BOOL ImageList_DragMove(int x, int y); +BOOL ImageList_DragShowNolock(BOOL show); +void ImageList_EndDrag(void); +BOOL ImageList_Destroy(HIMAGELIST list); +} + +#define TVM_GETITEM (TV_FIRST + 12) +#define TVM_SETITEM (TV_FIRST + 13) +#define TVM_EXPAND (TV_FIRST + 2) +#define TVM_GETITEMRECT (TV_FIRST + 4) +#define TVM_CREATEDRAGIMAGE (TV_FIRST + 18) +#define TreeView_GetItem(hwnd, item) ((BOOL)SendMessage((hwnd), TVM_GETITEM, 0, (LPARAM)(TVITEM *)(item))) +#define TreeView_SetItem(hwnd, item) ((BOOL)SendMessage((hwnd), TVM_SETITEM, 0, (LPARAM)(TVITEM const *)(item))) +#define TreeView_Expand(hwnd, item, code) ((BOOL)SendMessage((hwnd), TVM_EXPAND, (WPARAM)(code), (LPARAM)(HTREEITEM)(item))) +#define TreeView_GetItemRect(hwnd, item, rect, partial) (*(HTREEITEM *)(rect) = (item), (BOOL)SendMessage((hwnd), TVM_GETITEMRECT, (WPARAM)(BOOL)(partial), (LPARAM)(RECT *)(rect))) +#define TreeView_CreateDragImage(hwnd, item) ((HIMAGELIST)SendMessage((hwnd), TVM_CREATEDRAGIMAGE, 0, (LPARAM)(HTREEITEM)(item))) + +#define TCM_GETITEM (TCM_FIRST + 5) +#define TabCtrl_GetItem(hwnd, index, item) ((BOOL)SendMessage((hwnd), TCM_GETITEM, (WPARAM)(int)(index), (LPARAM)(TC_ITEM *)(item))) diff --git a/platform/win32compat/include/conio.h b/platform/win32compat/include/conio.h new file mode 100644 index 000000000..c9f1f4d1c --- /dev/null +++ b/platform/win32compat/include/conio.h @@ -0,0 +1,4 @@ +#pragma once +#include +#include +extern "C" int _getch(void); diff --git a/platform/win32compat/include/crtdbg.h b/platform/win32compat/include/crtdbg.h new file mode 100644 index 000000000..607852b09 --- /dev/null +++ b/platform/win32compat/include/crtdbg.h @@ -0,0 +1,3 @@ +#pragma once +#include +#include diff --git a/platform/win32compat/include/dbghelp.h b/platform/win32compat/include/dbghelp.h new file mode 100644 index 000000000..720e64a9b --- /dev/null +++ b/platform/win32compat/include/dbghelp.h @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/platform/win32compat/include/direct.h b/platform/win32compat/include/direct.h new file mode 100644 index 000000000..9da0e4dc5 --- /dev/null +++ b/platform/win32compat/include/direct.h @@ -0,0 +1,7 @@ +#pragma once +#include +#include +#define _MAX_DRIVE 3 +#define _MAX_DIR 256 +#define _MAX_FNAME 256 +#define _MAX_EXT 256 diff --git a/platform/win32compat/include/dos.h b/platform/win32compat/include/dos.h new file mode 100644 index 000000000..607852b09 --- /dev/null +++ b/platform/win32compat/include/dos.h @@ -0,0 +1,3 @@ +#pragma once +#include +#include diff --git a/platform/win32compat/include/intrin.h b/platform/win32compat/include/intrin.h new file mode 100644 index 000000000..fccc81c79 --- /dev/null +++ b/platform/win32compat/include/intrin.h @@ -0,0 +1,9 @@ +#pragma once +#include + +// x86 intrinsics with no arm64 equivalent. The bodies are placeholders; the point is to +// let the rest of the translation unit be compiled and counted. +// clang supplies _rotl and _rotr as builtins under -fms-extensions. +static inline unsigned long long __rdtsc() { return 0ULL; } +static inline void __cpuid(int regs[4], int) { regs[0] = regs[1] = regs[2] = regs[3] = 0; } +static inline void __cpuidex(int regs[4], int, int) { regs[0] = regs[1] = regs[2] = regs[3] = 0; } diff --git a/platform/win32compat/include/io.h b/platform/win32compat/include/io.h new file mode 100644 index 000000000..607852b09 --- /dev/null +++ b/platform/win32compat/include/io.h @@ -0,0 +1,3 @@ +#pragma once +#include +#include diff --git a/platform/win32compat/include/iphlpapi.h b/platform/win32compat/include/iphlpapi.h new file mode 100644 index 000000000..720e64a9b --- /dev/null +++ b/platform/win32compat/include/iphlpapi.h @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/platform/win32compat/include/malloc.h b/platform/win32compat/include/malloc.h new file mode 100644 index 000000000..607852b09 --- /dev/null +++ b/platform/win32compat/include/malloc.h @@ -0,0 +1,3 @@ +#pragma once +#include +#include diff --git a/platform/win32compat/include/mmsystem.h b/platform/win32compat/include/mmsystem.h new file mode 100644 index 000000000..720e64a9b --- /dev/null +++ b/platform/win32compat/include/mmsystem.h @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/platform/win32compat/include/objbase.h b/platform/win32compat/include/objbase.h new file mode 100644 index 000000000..a2a5b9a9b --- /dev/null +++ b/platform/win32compat/include/objbase.h @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/platform/win32compat/include/objidl.h b/platform/win32compat/include/objidl.h new file mode 100644 index 000000000..a2a5b9a9b --- /dev/null +++ b/platform/win32compat/include/objidl.h @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/platform/win32compat/include/ole2.h b/platform/win32compat/include/ole2.h new file mode 100644 index 000000000..a2a5b9a9b --- /dev/null +++ b/platform/win32compat/include/ole2.h @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/platform/win32compat/include/process.h b/platform/win32compat/include/process.h new file mode 100644 index 000000000..720e64a9b --- /dev/null +++ b/platform/win32compat/include/process.h @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/platform/win32compat/include/sal.h b/platform/win32compat/include/sal.h new file mode 100644 index 000000000..720e64a9b --- /dev/null +++ b/platform/win32compat/include/sal.h @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/platform/win32compat/include/share.h b/platform/win32compat/include/share.h new file mode 100644 index 000000000..607852b09 --- /dev/null +++ b/platform/win32compat/include/share.h @@ -0,0 +1,3 @@ +#pragma once +#include +#include diff --git a/platform/win32compat/include/shellapi.h b/platform/win32compat/include/shellapi.h new file mode 100644 index 000000000..720e64a9b --- /dev/null +++ b/platform/win32compat/include/shellapi.h @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/platform/win32compat/include/sys/timeb.h b/platform/win32compat/include/sys/timeb.h new file mode 100644 index 000000000..586cfdef2 --- /dev/null +++ b/platform/win32compat/include/sys/timeb.h @@ -0,0 +1,7 @@ +#pragma once +#include + +struct _timeb { long time; unsigned short millitm; short timezone; short dstflag; }; +#define timeb _timeb + +extern "C" void _ftime(struct _timeb * time); diff --git a/platform/win32compat/include/tlhelp32.h b/platform/win32compat/include/tlhelp32.h new file mode 100644 index 000000000..720e64a9b --- /dev/null +++ b/platform/win32compat/include/tlhelp32.h @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/platform/win32compat/include/unknwn.h b/platform/win32compat/include/unknwn.h new file mode 100644 index 000000000..10052c5a5 --- /dev/null +++ b/platform/win32compat/include/unknwn.h @@ -0,0 +1,76 @@ +#pragma once +#include + +#define STDMETHOD(m) virtual HRESULT m +#define STDMETHOD_(t, m) virtual t m +#define STDMETHODIMP HRESULT +#define STDMETHODIMP_(t) t +#define PURE = 0 +#define DECLARE_INTERFACE(i) struct i +#define DECLARE_INTERFACE_(i, b) struct i : public b +#define THIS_ +#define THIS +#define interface struct + +struct IUnknown { + virtual HRESULT QueryInterface(REFIID riid, void **ppv) = 0; + virtual ULONG AddRef() = 0; + virtual ULONG Release() = 0; +}; +typedef IUnknown *LPUNKNOWN; + +#define EXTERN_C extern "C" +#define STDMETHODCALLTYPE +#define STDAPICALLTYPE +#define STDAPI extern "C" HRESULT +#define STDAPI_(t) extern "C" t +#define DECLSPEC_UUID(x) +#define DECLSPEC_NOVTABLE +#define MIDL_INTERFACE(x) struct +#define EXTERN_GUID(n, ...) extern "C" const GUID n + +typedef struct tagSTATSTG { + LPWSTR pwcsName; DWORD type; ULARGE_INTEGER cbSize; + FILETIME mtime, ctime, atime; + DWORD grfMode, grfLocksSupported; + CLSID clsid; DWORD grfStateBits, reserved; +} STATSTG; + +struct ISequentialStream : public IUnknown { + virtual HRESULT Read(void *pv, ULONG cb, ULONG *pcbRead) = 0; + virtual HRESULT Write(const void *pv, ULONG cb, ULONG *pcbWritten) = 0; +}; + +struct IStream : public ISequentialStream { + virtual HRESULT Seek(LARGE_INTEGER, DWORD, ULARGE_INTEGER *) = 0; + virtual HRESULT SetSize(ULARGE_INTEGER) = 0; + virtual HRESULT CopyTo(IStream *, ULARGE_INTEGER, ULARGE_INTEGER *, ULARGE_INTEGER *) = 0; + virtual HRESULT Commit(DWORD) = 0; + virtual HRESULT Revert() = 0; + virtual HRESULT LockRegion(ULARGE_INTEGER, ULARGE_INTEGER, DWORD) = 0; + virtual HRESULT UnlockRegion(ULARGE_INTEGER, ULARGE_INTEGER, DWORD) = 0; + virtual HRESULT Stat(STATSTG *, DWORD) = 0; + virtual HRESULT Clone(IStream **) = 0; +}; +typedef IStream *LPSTREAM; + +typedef unsigned char boolean; + +struct IPersist : public IUnknown { + virtual HRESULT GetClassID(CLSID *pClassID) = 0; +}; + +struct IPersistStream : public IPersist { + virtual HRESULT IsDirty() = 0; + virtual HRESULT Load(IStream *pStm) = 0; + virtual HRESULT Save(IStream *pStm, BOOL fClearDirty) = 0; + virtual HRESULT GetSizeMax(ULARGE_INTEGER *pcbSize) = 0; +}; + +struct IPropertySetStorage : public IUnknown {}; +struct IClassFactory : public IUnknown { + virtual HRESULT CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppv) = 0; + virtual HRESULT LockServer(BOOL fLock) = 0; +}; + + diff --git a/platform/win32compat/include/utime.h b/platform/win32compat/include/utime.h new file mode 100644 index 000000000..3b2a750e0 --- /dev/null +++ b/platform/win32compat/include/utime.h @@ -0,0 +1,4 @@ +#pragma once +#include_next +#include +#include diff --git a/platform/win32compat/include/winbase.h b/platform/win32compat/include/winbase.h new file mode 100644 index 000000000..720e64a9b --- /dev/null +++ b/platform/win32compat/include/winbase.h @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/platform/win32compat/include/windef.h b/platform/win32compat/include/windef.h new file mode 100644 index 000000000..720e64a9b --- /dev/null +++ b/platform/win32compat/include/windef.h @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/platform/win32compat/include/windows.h b/platform/win32compat/include/windows.h new file mode 100644 index 000000000..684d493a8 --- /dev/null +++ b/platform/win32compat/include/windows.h @@ -0,0 +1,962 @@ +#pragma once + +#include +#include +#include + +#ifndef _WINDOWS_ +#define _WINDOWS_ +#endif + +#define WINAPI +#define APIENTRY +#define CALLBACK +#define WINAPIV +#define __cdecl +#define PASCAL + +typedef int BOOL; +typedef unsigned char BYTE; +typedef unsigned short WORD; +typedef unsigned int DWORD; +typedef long LONG; +typedef unsigned long ULONG; +typedef unsigned int UINT; +typedef int INT; +typedef short SHORT; +typedef unsigned short USHORT; +typedef char CHAR; +typedef unsigned char UCHAR; +typedef wchar_t WCHAR; +typedef float FLOAT; +typedef void VOID; +typedef long long LONGLONG; +typedef unsigned long long ULONGLONG; +typedef std::intptr_t INT_PTR; +typedef std::uintptr_t UINT_PTR; +typedef std::intptr_t LONG_PTR; +typedef std::uintptr_t ULONG_PTR; +typedef ULONG_PTR DWORD_PTR; +typedef std::size_t SIZE_T; + +typedef void *LPVOID; +typedef const void *LPCVOID; +typedef char *LPSTR; +typedef const char *LPCSTR; +typedef wchar_t *LPWSTR; +typedef const wchar_t *LPCWSTR; +typedef char *LPTSTR; +typedef const char *LPCTSTR; +typedef BYTE *LPBYTE; +typedef WORD *LPWORD; +typedef DWORD *LPDWORD; +typedef INT *LPINT; +typedef LONG *LPLONG; +typedef BOOL *LPBOOL; + +typedef void *HANDLE; +#define OPENTS_SHIM_HANDLE(name) struct name##__ { int unused; }; typedef struct name##__ *name +OPENTS_SHIM_HANDLE(HWND); +OPENTS_SHIM_HANDLE(HINSTANCE); +OPENTS_SHIM_HANDLE(HDC); +OPENTS_SHIM_HANDLE(HBITMAP); +OPENTS_SHIM_HANDLE(HPALETTE); +OPENTS_SHIM_HANDLE(HBRUSH); +OPENTS_SHIM_HANDLE(HPEN); +OPENTS_SHIM_HANDLE(HFONT); +OPENTS_SHIM_HANDLE(HRGN); +OPENTS_SHIM_HANDLE(HCURSOR); +OPENTS_SHIM_HANDLE(HICON); +OPENTS_SHIM_HANDLE(HMENU); +OPENTS_SHIM_HANDLE(HKEY); +OPENTS_SHIM_HANDLE(HMONITOR); +OPENTS_SHIM_HANDLE(HACCEL); +typedef void *HGDIOBJ; +typedef HINSTANCE HMODULE; +typedef HANDLE HGLOBAL; +typedef HANDLE HLOCAL; + +typedef UINT_PTR WPARAM; +typedef LONG_PTR LPARAM; +typedef LONG_PTR LRESULT; +typedef LONG HRESULT; +typedef DWORD COLORREF; +typedef DWORD *LPCOLORREF; +typedef WORD ATOM; + +typedef LRESULT (CALLBACK *WNDPROC)(HWND, UINT, WPARAM, LPARAM); +typedef INT_PTR (CALLBACK *DLGPROC)(HWND, UINT, WPARAM, LPARAM); +typedef BOOL (CALLBACK *WNDENUMPROC)(HWND, LPARAM); +typedef void (CALLBACK *TIMERPROC)(HWND, UINT, UINT_PTR, DWORD); +typedef DWORD (WINAPI *LPTHREAD_START_ROUTINE)(LPVOID); +typedef int (CALLBACK *FARPROC)(); +typedef int (CALLBACK *PROC)(); + +typedef struct tagPOINT { LONG x, y; } POINT, *LPPOINT, *PPOINT; +typedef struct tagSIZE { LONG cx, cy; } SIZE, *LPSIZE; +typedef struct tagRECT { LONG left, top, right, bottom; } RECT, *LPRECT, *PRECT; +typedef struct tagMSG { HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; } MSG, *LPMSG; +typedef struct tagPAINTSTRUCT { HDC hdc; BOOL fErase; RECT rcPaint; BOOL fRestore; BOOL fIncUpdate; BYTE rgbReserved[32]; } PAINTSTRUCT, *LPPAINTSTRUCT; +typedef union _LARGE_INTEGER { struct { DWORD LowPart; LONG HighPart; }; LONGLONG QuadPart; } LARGE_INTEGER, *PLARGE_INTEGER; +typedef union _ULARGE_INTEGER { struct { DWORD LowPart; DWORD HighPart; }; ULONGLONG QuadPart; } ULARGE_INTEGER; +typedef struct _FILETIME { DWORD dwLowDateTime, dwHighDateTime; } FILETIME, *LPFILETIME; +typedef struct _SYSTEMTIME { WORD wYear, wMonth, wDayOfWeek, wDay, wHour, wMinute, wSecond, wMilliseconds; } SYSTEMTIME, *LPSYSTEMTIME; +typedef struct _SECURITY_ATTRIBUTES { DWORD nLength; LPVOID lpSecurityDescriptor; BOOL bInheritHandle; } SECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES; +typedef struct _OVERLAPPED { ULONG_PTR Internal, InternalHigh; union { struct { DWORD Offset, OffsetHigh; }; void *Pointer; }; HANDLE hEvent; } OVERLAPPED, *LPOVERLAPPED; +typedef struct _RTL_CRITICAL_SECTION { void *opaque[8]; } CRITICAL_SECTION, *LPCRITICAL_SECTION; +typedef struct _GUID { DWORD Data1; WORD Data2; WORD Data3; BYTE Data4[8]; } GUID, IID, CLSID, UUID; +typedef const GUID &REFGUID; +typedef const GUID &REFIID; +typedef const GUID &REFCLSID; + +#define TRUE 1 +#define FALSE 0 +#ifndef NULL +#define NULL 0 +#endif +#define MAX_PATH 260 +#define INVALID_HANDLE_VALUE ((HANDLE)(LONG_PTR)-1) +#define S_OK ((HRESULT)0) +#define S_FALSE ((HRESULT)1) +#define E_FAIL ((HRESULT)0x80004005L) +#define E_NOINTERFACE ((HRESULT)0x80004002L) +#define E_OUTOFMEMORY ((HRESULT)0x8007000EL) +#define E_INVALIDARG ((HRESULT)0x80070057L) +#define SUCCEEDED(hr) (((HRESULT)(hr)) >= 0) +#define FAILED(hr) (((HRESULT)(hr)) < 0) + +#define MAKEWORD(a, b) ((WORD)(((BYTE)(a)) | (((WORD)((BYTE)(b))) << 8))) +#define MAKELONG(a, b) ((LONG)(((WORD)(a)) | (((DWORD)((WORD)(b))) << 16))) +#define LOWORD(l) ((WORD)(((DWORD_PTR)(l)) & 0xffff)) +#define HIWORD(l) ((WORD)((((DWORD_PTR)(l)) >> 16) & 0xffff)) +#define LOBYTE(w) ((BYTE)(((DWORD_PTR)(w)) & 0xff)) +#define HIBYTE(w) ((BYTE)((((DWORD_PTR)(w)) >> 8) & 0xff)) +#define RGB(r, g, b) ((COLORREF)(((BYTE)(r)) | (((WORD)((BYTE)(g))) << 8) | (((DWORD)((BYTE)(b))) << 16))) +#define MAKEINTRESOURCE(i) ((LPSTR)((ULONG_PTR)((WORD)(i)))) + +typedef struct _WIN32_FIND_DATAA { + DWORD dwFileAttributes; FILETIME ftCreationTime, ftLastAccessTime, ftLastWriteTime; + DWORD nFileSizeHigh, nFileSizeLow, dwReserved0, dwReserved1; + CHAR cFileName[MAX_PATH]; CHAR cAlternateFileName[14]; +} WIN32_FIND_DATAA, WIN32_FIND_DATA, *LPWIN32_FIND_DATAA, *LPWIN32_FIND_DATA; + +typedef struct tagDRAWITEMSTRUCT { + UINT CtlType, CtlID; UINT itemID, itemAction, itemState; + HWND hwndItem; HDC hDC; RECT rcItem; ULONG_PTR itemData; +} DRAWITEMSTRUCT, *LPDRAWITEMSTRUCT; + +typedef struct tagMEASUREITEMSTRUCT { UINT CtlType, CtlID, itemID, itemWidth, itemHeight; ULONG_PTR itemData; } MEASUREITEMSTRUCT, *LPMEASUREITEMSTRUCT; + +#pragma pack(push, 1) +typedef struct tagBITMAPFILEHEADER { WORD bfType; DWORD bfSize; WORD bfReserved1, bfReserved2; DWORD bfOffBits; } BITMAPFILEHEADER, *LPBITMAPFILEHEADER; +#pragma pack(pop) +typedef struct tagBITMAPINFOHEADER { + DWORD biSize; LONG biWidth, biHeight; WORD biPlanes, biBitCount; + DWORD biCompression, biSizeImage; LONG biXPelsPerMeter, biYPelsPerMeter; + DWORD biClrUsed, biClrImportant; +} BITMAPINFOHEADER, *LPBITMAPINFOHEADER; +typedef struct tagRGBQUAD { BYTE rgbBlue, rgbGreen, rgbRed, rgbReserved; } RGBQUAD; +typedef struct tagBITMAPINFO { BITMAPINFOHEADER bmiHeader; RGBQUAD bmiColors[1]; } BITMAPINFO, *LPBITMAPINFO; +typedef struct tagBITMAP { LONG bmType, bmWidth, bmHeight, bmWidthBytes; WORD bmPlanes, bmBitsPixel; LPVOID bmBits; } BITMAP; +typedef struct tagDIBSECTION { BITMAP dsBm; BITMAPINFOHEADER dsBmih; DWORD dsBitfields[3]; HANDLE dshSection; DWORD dsOffset; } DIBSECTION; +typedef struct _ICONINFO { BOOL fIcon; DWORD xHotspot, yHotspot; HBITMAP hbmMask, hbmColor; } ICONINFO; +typedef struct tagWNDCLASSA { UINT style; WNDPROC lpfnWndProc; int cbClsExtra, cbWndExtra; HINSTANCE hInstance; HICON hIcon; HCURSOR hCursor; HBRUSH hbrBackground; LPCSTR lpszMenuName, lpszClassName; } WNDCLASSA, WNDCLASS, *LPWNDCLASS; +typedef struct tagMSGBOXPARAMSA { UINT cbSize; HWND hwndOwner; HINSTANCE hInstance; LPCSTR lpszText, lpszCaption; DWORD dwStyle; LPCSTR lpszIcon; DWORD_PTR dwContextHelpId; void *lpfnMsgBoxCallback; DWORD dwLanguageId; } MSGBOXPARAMSA, MSGBOXPARAMS; +typedef struct _devicemodeA { CHAR dmDeviceName[32]; WORD dmSpecVersion, dmDriverVersion, dmSize, dmDriverExtra; DWORD dmFields; DWORD dmPelsWidth, dmPelsHeight, dmBitsPerPel, dmDisplayFrequency; } DEVMODEA, DEVMODE; +typedef struct _RTL_SRWLOCK { void *Ptr; } SRWLOCK; +OPENTS_SHIM_HANDLE(HRSRC); +OPENTS_SHIM_HANDLE(HIMAGELIST); +OPENTS_SHIM_HANDLE(HTREEITEM); +typedef struct _NMHDR_SHIM { HWND hwndFrom; UINT_PTR idFrom; UINT code; } NMHDR_SHIM; +typedef struct _TVITEM_SHIM { UINT mask; HTREEITEM hItem; UINT state, stateMask; LPSTR pszText; int cchTextMax, iImage, iSelectedImage, cChildren; LPARAM lParam; } TVITEM_SHIM; +typedef struct _NMTREEVIEWA { NMHDR_SHIM hdr; UINT action; TVITEM_SHIM itemOld, itemNew; POINT ptDrag; } NMTREEVIEWA, NMTREEVIEW, *LPNMTREEVIEW; +typedef struct DLGTEMPLATE { DWORD style, dwExtendedStyle; WORD cdit; short x, y, cx, cy; } DLGTEMPLATE, *LPDLGTEMPLATE; +typedef const DLGTEMPLATE *LPCDLGTEMPLATE; + +// SAL-style annotations the tree spells out on parameters. +#define IN +#define OUT +#define OPTIONAL +#ifndef CONST +#define CONST const +#endif + +#define E_POINTER ((HRESULT)0x80004003L) +#define E_NOTIMPL ((HRESULT)0x80004001L) +#define CLSCTX_ALL 23 +#define CLSCTX_INPROC_SERVER 1 +#define IDOK 1 +#define IDCANCEL 2 +#define IDABORT 3 +#define IDRETRY 4 +#define IDIGNORE 5 +#define IDYES 6 +#define IDNO 7 +#define MB_OK 0x0 +#define MB_OKCANCEL 0x1 +#define MB_YESNO 0x4 +#define MB_ICONSTOP 0x10 +#define MB_ICONERROR 0x10 +#define MB_ICONQUESTION 0x20 +#define MB_ICONEXCLAMATION 0x30 +#define MB_ICONINFORMATION 0x40 +#define MB_SETFOREGROUND 0x10000 +#define MB_TASKMODAL 0x2000 +#define MB_SYSTEMMODAL 0x1000 +#define MB_APPLMODAL 0x0 + +// --------------------------------------------------------------------------- +// Window messages +// --------------------------------------------------------------------------- +#define WM_NULL 0x0000 +#define WM_CREATE 0x0001 +#define WM_DESTROY 0x0002 +#define WM_MOVE 0x0003 +#define WM_SIZE 0x0005 +#define WM_ACTIVATE 0x0006 +#define WM_SETFOCUS 0x0007 +#define WM_KILLFOCUS 0x0008 +#define WM_ENABLE 0x000A +#define WM_SETREDRAW 0x000B +#define WM_SETTEXT 0x000C +#define WM_GETTEXT 0x000D +#define WM_GETTEXTLENGTH 0x000E +#define WM_PAINT 0x000F +#define WM_CLOSE 0x0010 +#define WM_QUIT 0x0012 +#define WM_ERASEBKGND 0x0014 +#define WM_SHOWWINDOW 0x0018 +#define WM_ACTIVATEAPP 0x001C +#define WM_SETCURSOR 0x0020 +#define WM_MOUSEACTIVATE 0x0021 +#define WM_GETMINMAXINFO 0x0024 +#define WM_SETFONT 0x0030 +#define WM_GETFONT 0x0031 +#define WM_WINDOWPOSCHANGING 0x0046 +#define WM_WINDOWPOSCHANGED 0x0047 +#define WM_CONTEXTMENU 0x007B +#define WM_DISPLAYCHANGE 0x007E +#define WM_NCDESTROY 0x0082 +#define WM_NCHITTEST 0x0084 +#define WM_NCPAINT 0x0085 +#define WM_GETDLGCODE 0x0087 +#define WM_NCMOUSEMOVE 0x00A0 +#define WM_KEYDOWN 0x0100 +#define WM_KEYUP 0x0101 +#define WM_CHAR 0x0102 +#define WM_DEADCHAR 0x0103 +#define WM_SYSKEYDOWN 0x0104 +#define WM_SYSKEYUP 0x0105 +#define WM_SYSCHAR 0x0106 +#define WM_SYSDEADCHAR 0x0107 +#define WM_KEYLAST 0x0109 +#define WM_INITDIALOG 0x0110 +#define WM_COMMAND 0x0111 +#define WM_SYSCOMMAND 0x0112 +#define WM_TIMER 0x0113 +#define WM_HSCROLL 0x0114 +#define WM_VSCROLL 0x0115 +#define WM_CTLCOLORMSGBOX 0x0132 +#define WM_CTLCOLOREDIT 0x0133 +#define WM_CTLCOLORLISTBOX 0x0134 +#define WM_CTLCOLORBTN 0x0135 +#define WM_CTLCOLORDLG 0x0136 +#define WM_CTLCOLORSCROLLBAR 0x0137 +#define WM_CTLCOLORSTATIC 0x0138 +#define WM_MOUSEMOVE 0x0200 +#define WM_LBUTTONDOWN 0x0201 +#define WM_LBUTTONUP 0x0202 +#define WM_LBUTTONDBLCLK 0x0203 +#define WM_RBUTTONDOWN 0x0204 +#define WM_RBUTTONUP 0x0205 +#define WM_RBUTTONDBLCLK 0x0206 +#define WM_MBUTTONDOWN 0x0207 +#define WM_MBUTTONUP 0x0208 +#define WM_MBUTTONDBLCLK 0x0209 +#define WM_MOUSEWHEEL 0x020A +#define WM_XBUTTONDOWN 0x020B +#define WM_XBUTTONUP 0x020C +#define WM_XBUTTONDBLCLK 0x020D +#define WM_MOUSELAST 0x020E +#define WM_MOVING 0x0216 +#define WM_CAPTURECHANGED 0x0215 +#define WM_DRAWITEM 0x002B +#define WM_MEASUREITEM 0x002C +#define WM_DELETEITEM 0x002D +#define WM_COMPAREITEM 0x0039 +#define WM_HELP 0x0053 +#define WM_NOTIFY 0x004E +#define WM_USER 0x0400 +#define WM_APP 0x8000 + +#define HTTRANSPARENT (-1) +#define HTNOWHERE 0 +#define HTCLIENT 1 +#define HTCAPTION 2 + +#define SIZE_RESTORED 0 +#define SIZE_MINIMIZED 1 +#define SIZE_MAXIMIZED 2 + +#define MK_LBUTTON 0x0001 +#define MK_RBUTTON 0x0002 +#define MK_SHIFT 0x0004 +#define MK_CONTROL 0x0008 +#define MK_MBUTTON 0x0010 + +#define SC_SIZE 0xF000 +#define SC_CLOSE 0xF060 +#define SC_SCREENSAVE 0xF140 +#define SC_MONITORPOWER 0xF170 +#define MF_BYCOMMAND 0x00000000 +#define MF_BYPOSITION 0x00000400 +#define MF_GRAYED 0x00000001 + +// --------------------------------------------------------------------------- +// Window and class styles +// --------------------------------------------------------------------------- +#define CS_VREDRAW 0x0001 +#define CS_HREDRAW 0x0002 +#define CS_DBLCLKS 0x0008 +#define CS_OWNDC 0x0020 + +#define WS_OVERLAPPED 0x00000000L +#define WS_POPUP 0x80000000L +#define WS_CHILD 0x40000000L +#define WS_MINIMIZE 0x20000000L +#define WS_VISIBLE 0x10000000L +#define WS_DISABLED 0x08000000L +#define WS_CLIPSIBLINGS 0x04000000L +#define WS_CLIPCHILDREN 0x02000000L +#define WS_MAXIMIZE 0x01000000L +#define WS_CAPTION 0x00C00000L +#define WS_BORDER 0x00800000L +#define WS_DLGFRAME 0x00400000L +#define WS_VSCROLL 0x00200000L +#define WS_HSCROLL 0x00100000L +#define WS_SYSMENU 0x00080000L +#define WS_THICKFRAME 0x00040000L +#define WS_GROUP 0x00020000L +#define WS_TABSTOP 0x00010000L +#define WS_MINIMIZEBOX 0x00020000L +#define WS_MAXIMIZEBOX 0x00010000L +#define WS_OVERLAPPEDWINDOW (WS_OVERLAPPED|WS_CAPTION|WS_SYSMENU|WS_THICKFRAME|WS_MINIMIZEBOX|WS_MAXIMIZEBOX) +#define WS_EX_TOPMOST 0x00000008L +#define WS_EX_TOOLWINDOW 0x00000080L + +#define BS_PUSHBUTTON 0x00000000L +#define BS_CHECKBOX 0x00000002L +#define BS_AUTOCHECKBOX 0x00000003L +#define BS_RADIOBUTTON 0x00000004L +#define BS_GROUPBOX 0x00000007L +#define BS_OWNERDRAW 0x0000000BL +#define ES_MULTILINE 0x0004L +#define ES_PASSWORD 0x0020L +#define SS_CENTER 0x00000001L +#define SS_RIGHT 0x00000002L +#define LBS_NOTIFY 0x0001L +#define LBS_NOSEL 0x4000L +#define LBS_MULTIPLESEL 0x0008L + +#define GWL_STYLE (-16) +#define GWL_EXSTYLE (-20) +#define GWL_ID (-12) +#define GWL_USERDATA (-21) +#define GWL_WNDPROC (-4) +#define GWL_HINSTANCE (-6) +#define GWL_HWNDPARENT (-8) +#define GWLP_WNDPROC (-4) +#define GWLP_HINSTANCE (-6) +#define GWLP_HWNDPARENT (-8) +#define GWLP_USERDATA (-21) +#define GWLP_ID (-12) +#define DWLP_MSGRESULT 0 +#define DWLP_DLGPROC (DWLP_MSGRESULT + sizeof(LRESULT)) +#define DWLP_USER (DWLP_DLGPROC + sizeof(DLGPROC)) + +#define SW_HIDE 0 +#define SW_SHOWNORMAL 1 +#define SW_NORMAL 1 +#define SW_SHOWMINIMIZED 2 +#define SW_SHOWMAXIMIZED 3 +#define SW_SHOW 5 +#define SW_MINIMIZE 6 +#define SW_RESTORE 9 + +#define SWP_NOSIZE 0x0001 +#define SWP_NOMOVE 0x0002 +#define SWP_NOZORDER 0x0004 +#define SWP_NOACTIVATE 0x0010 +#define SWP_SHOWWINDOW 0x0040 +#define SWP_NOOWNERZORDER 0x0200 + +#define GW_HWNDFIRST 0 +#define GW_HWNDLAST 1 +#define GW_HWNDNEXT 2 +#define GW_HWNDPREV 3 +#define GW_OWNER 4 +#define GW_CHILD 5 + +#define HWND_DESKTOP ((HWND)0) +#define HWND_TOP ((HWND)0) +#define HWND_TOPMOST ((HWND)-1) + +#define RDW_INVALIDATE 0x0001 +#define RDW_INTERNALPAINT 0x0002 +#define RDW_ERASE 0x0004 +#define RDW_UPDATENOW 0x0100 +#define RDW_FRAME 0x0400 +#define RDW_ALLCHILDREN 0x0080 + +#define PM_NOREMOVE 0x0000 +#define PM_REMOVE 0x0001 +#define PM_NOYIELD 0x0002 + +#define SM_CXSCREEN 0 +#define SM_CYSCREEN 1 +#define SM_CXBORDER 5 +#define SM_CYBORDER 6 +#define SM_CXFULLSCREEN 16 +#define SM_CYFULLSCREEN 17 +#define SM_SWAPBUTTON 23 +#define SM_CXDRAG 68 +#define SM_CYDRAG 69 + +#define MOD_ALT 0x0001 +#define MOD_CONTROL 0x0002 +#define MOD_SHIFT 0x0004 + +#define IDC_ARROW ((LPCSTR)(ULONG_PTR)32512) +#define IDC_NO ((LPCSTR)(ULONG_PTR)32648) +#define IDI_APPLICATION ((LPCSTR)(ULONG_PTR)32512) +#define RT_DIALOG ((LPCSTR)(ULONG_PTR)5) +#define MB_ICONWARNING 0x30 + +#define HELP_CONTEXTMENU 0x000a +#define HELP_CONTEXTPOPUP 0x0026 + +#define MONITOR_DEFAULTTONULL 0x0 +#define MONITOR_DEFAULTTOPRIMARY 0x1 +#define MONITOR_DEFAULTTONEAREST 0x2 + +// Button, edit, list box, combo box and scroll bar messages +#define BM_GETCHECK 0x00F0 +#define BM_SETCHECK 0x00F1 +#define BM_GETSTATE 0x00F2 +#define BM_SETSTATE 0x00F3 +#define BST_UNCHECKED 0x0000 +#define BST_CHECKED 0x0001 +#define BN_CLICKED 0 +#define BN_DBLCLK 5 +#define ODT_MENU 1 +#define ODT_LISTBOX 2 +#define ODT_COMBOBOX 3 +#define ODT_BUTTON 4 +#define ODT_STATIC 5 + +#define EM_GETSEL 0x00B0 +#define EM_SETSEL 0x00B1 +#define EM_POSFROMCHAR 0x00D6 +#define EM_SETLIMITTEXT 0x00C5 +#define EN_SETFOCUS 0x0100 +#define EN_KILLFOCUS 0x0200 +#define EN_CHANGE 0x0300 +#define EN_MAXTEXT 0x0501 + +#define LB_ADDSTRING 0x0180 +#define LB_INSERTSTRING 0x0181 +#define LB_DELETESTRING 0x0182 +#define LB_RESETCONTENT 0x0184 +#define LB_SETSEL 0x0185 +#define LB_SETCURSEL 0x0186 +#define LB_GETSEL 0x0187 +#define LB_GETCURSEL 0x0188 +#define LB_GETTEXT 0x0189 +#define LB_GETTEXTLEN 0x018A +#define LB_GETCOUNT 0x018B +#define LB_SELECTSTRING 0x018C +#define LB_FINDSTRING 0x018F +#define LB_SELITEMRANGE 0x019B +#define LB_GETSELCOUNT 0x0190 +#define LB_GETSELITEMS 0x0191 +#define LB_SETITEMDATA 0x019A +#define LB_GETITEMDATA 0x0199 +#define LB_GETITEMRECT 0x0198 +#define LB_SETTOPINDEX 0x0197 +#define LB_GETTOPINDEX 0x018E +#define LB_FINDSTRINGEXACT 0x01A2 +#define LB_SETITEMHEIGHT 0x01A0 +#define LB_GETITEMHEIGHT 0x01A1 +#define LB_ERR (-1) +#define LBN_SELCHANGE 1 +#define LBN_DBLCLK 2 + +#define CB_GETEDITSEL 0x0140 +#define CB_ADDSTRING 0x0143 +#define CB_DELETESTRING 0x0144 +#define CB_GETCOUNT 0x0146 +#define CB_GETCURSEL 0x0147 +#define CB_GETLBTEXT 0x0148 +#define CB_INSERTSTRING 0x014A +#define CB_RESETCONTENT 0x014B +#define CB_FINDSTRING 0x014C +#define CB_SETCURSEL 0x014E +#define CB_SHOWDROPDOWN 0x014F +#define CB_GETITEMDATA 0x0150 +#define CB_SETITEMDATA 0x0151 +#define CB_GETDROPPEDCONTROLRECT 0x0152 +#define CB_SETITEMHEIGHT 0x0153 +#define CB_GETITEMHEIGHT 0x0154 +#define CB_GETDROPPEDSTATE 0x0157 +#define CB_GETTOPINDEX 0x015b +#define CB_SETTOPINDEX 0x015c +#define CB_ERR (-1) +#define CBN_SELCHANGE 1 + +#define SBM_SETPOS 0x00E0 +#define SBM_GETPOS 0x00E1 +#define SBM_SETRANGE 0x00E2 +#define SBM_SETSCROLLINFO 0x00E9 +#define SB_LINEUP 0 +#define SB_LINEDOWN 1 +#define SB_THUMBPOSITION 4 +#define SB_THUMBTRACK 5 +#define SB_ENDSCROLL 8 +#define SIF_RANGE 0x0001 +#define SIF_PAGE 0x0002 +#define SIF_POS 0x0004 +#define SIF_ALL 0x0017 + +typedef struct tagSCROLLINFO { UINT cbSize, fMask; int nMin, nMax; UINT nPage; int nPos, nTrackPos; } SCROLLINFO, *LPSCROLLINFO; +typedef struct tagPOINTS { SHORT x, y; } POINTS; +typedef struct tagWINDOWPOS { HWND hwnd, hwndInsertAfter; int x, y, cx, cy; UINT flags; } WINDOWPOS, *LPWINDOWPOS; +typedef struct tagMONITORINFO { DWORD cbSize; RECT rcMonitor, rcWork; DWORD dwFlags; } MONITORINFO, *LPMONITORINFO; +typedef struct tagHELPINFO { UINT cbSize; int iContextType, iCtrlId; HANDLE hItemHandle; DWORD_PTR dwContextId; POINT MousePos; } HELPINFO, *LPHELPINFO; + +#define MAKEPOINTS(l) (*((POINTS *)&(l))) +#define MAKEWPARAM(l, h) ((WPARAM)(DWORD)MAKELONG(l, h)) +#define MAKELPARAM(l, h) ((LPARAM)(DWORD)MAKELONG(l, h)) +#define GET_X_LPARAM(lp) ((int)(short)LOWORD(lp)) +#define GET_Y_LPARAM(lp) ((int)(short)HIWORD(lp)) +#define WHEEL_DELTA 120 +#define GET_WHEEL_DELTA_WPARAM(wp) ((short)HIWORD(wp)) +#define GET_KEYSTATE_WPARAM(wp) ((int)LOWORD(wp)) +#define IS_SURROGATE_PAIR(hi, lo) ((hi) >= 0xd800 && (hi) <= 0xdbff && (lo) >= 0xdc00 && (lo) <= 0xdfff) +#ifndef TEXT +#define TEXT(s) s +#endif + +// --------------------------------------------------------------------------- +// Window management +// --------------------------------------------------------------------------- +extern "C" { +ATOM RegisterClass(WNDCLASS const * cls); +HWND CreateWindowEx(DWORD exstyle, LPCSTR classname, LPCSTR windowname, DWORD style, int x, int y, int width, int height, HWND parent, HMENU menu, HINSTANCE instance, LPVOID param); +BOOL DestroyWindow(HWND window); +BOOL ShowWindow(HWND window, int command); +BOOL ShowWindowAsync(HWND window, int command); +BOOL UpdateWindow(HWND window); +BOOL MoveWindow(HWND window, int x, int y, int width, int height, BOOL repaint); +BOOL SetWindowPos(HWND window, HWND after, int x, int y, int cx, int cy, UINT flags); +BOOL GetClientRect(HWND window, LPRECT rect); +BOOL GetWindowRect(HWND window, LPRECT rect); +BOOL ClientToScreen(HWND window, LPPOINT point); +BOOL ScreenToClient(HWND window, LPPOINT point); +int MapWindowPoints(HWND from, HWND to, LPPOINT points, UINT count); +BOOL AdjustWindowRectEx(LPRECT rect, DWORD style, BOOL menu, DWORD exstyle); +LONG_PTR GetWindowLong(HWND window, int index); +LONG_PTR SetWindowLong(HWND window, int index, LONG_PTR value); +LONG_PTR GetWindowLongPtr(HWND window, int index); +LONG_PTR SetWindowLongPtr(HWND window, int index, LONG_PTR value); +BOOL SetWindowText(HWND window, LPCSTR text); +int GetWindowText(HWND window, LPSTR text, int max); +int GetWindowTextLength(HWND window); +int GetClassName(HWND window, LPSTR name, int max); +BOOL IsWindow(HWND window); +BOOL IsWindowVisible(HWND window); +BOOL IsWindowEnabled(HWND window); +BOOL IsChild(HWND parent, HWND child); +BOOL EnableWindow(HWND window, BOOL enable); +HWND GetParent(HWND window); +HWND GetWindow(HWND window, UINT command); +HWND GetTopWindow(HWND window); +HWND GetDesktopWindow(void); +HWND GetActiveWindow(void); +HWND SetActiveWindow(HWND window); +HWND GetFocus(void); +HWND SetFocus(HWND window); +BOOL SetForegroundWindow(HWND window); +BOOL BringWindowToTop(HWND window); +HWND FindWindow(LPCSTR classname, LPCSTR windowname); +HWND WindowFromPoint(POINT point); +HWND ChildWindowFromPoint(HWND parent, POINT point); +BOOL EnumChildWindows(HWND parent, WNDENUMPROC proc, LPARAM param); +BOOL InvalidateRect(HWND window, RECT const * rect, BOOL erase); +BOOL ValidateRect(HWND window, RECT const * rect); +BOOL GetUpdateRect(HWND window, LPRECT rect, BOOL erase); +BOOL RedrawWindow(HWND window, RECT const * rect, HRGN region, UINT flags); +BOOL CloseWindow(HWND window); +HMENU GetMenu(HWND window); +HMENU GetSystemMenu(HWND window, BOOL revert); +BOOL EnableMenuItem(HMENU menu, UINT item, UINT enable); +BOOL RegisterHotKey(HWND window, int id, UINT modifiers, UINT key); +BOOL SetRect(LPRECT rect, int left, int top, int right, int bottom); +BOOL IntersectRect(LPRECT dest, RECT const * a, RECT const * b); +BOOL PtInRect(RECT const * rect, POINT point); +int GetSystemMetrics(int index); +HMONITOR MonitorFromWindow(HWND window, DWORD flags); +BOOL GetMonitorInfo(HMONITOR monitor, LPMONITORINFO info); +int GetWindowContextHelpId(HWND window); +int MessageBox(HWND window, LPCSTR text, LPCSTR caption, UINT type); +int MessageBoxIndirect(MSGBOXPARAMS const * params); + +// --------------------------------------------------------------------------- +// Messages +// --------------------------------------------------------------------------- +BOOL GetMessage(LPMSG msg, HWND window, UINT filtermin, UINT filtermax); +BOOL PeekMessage(LPMSG msg, HWND window, UINT filtermin, UINT filtermax, UINT remove); +BOOL TranslateMessage(MSG const * msg); +LRESULT DispatchMessage(MSG const * msg); +BOOL PostMessage(HWND window, UINT message, WPARAM wparam, LPARAM lparam); +LRESULT SendMessage(HWND window, UINT message, WPARAM wparam, LPARAM lparam); +void PostQuitMessage(int code); +LRESULT DefWindowProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); +LRESULT CallWindowProc(WNDPROC proc, HWND window, UINT message, WPARAM wparam, LPARAM lparam); +int TranslateAccelerator(HWND window, HACCEL table, LPMSG msg); +UINT_PTR SetTimer(HWND window, UINT_PTR id, UINT elapse, TIMERPROC proc); +BOOL KillTimer(HWND window, UINT_PTR id); + +// --------------------------------------------------------------------------- +// Cursor, keyboard and capture +// --------------------------------------------------------------------------- +BOOL GetCursorPos(LPPOINT point); +BOOL SetCursorPos(int x, int y); +HCURSOR SetCursor(HCURSOR cursor); +int ShowCursor(BOOL show); +BOOL ClipCursor(RECT const * rect); +HWND SetCapture(HWND window); +BOOL ReleaseCapture(void); +HWND GetCapture(void); +SHORT GetAsyncKeyState(int key); +SHORT GetKeyState(int key); +UINT MapVirtualKey(UINT code, UINT type); +int GetKeyNameText(LONG param, LPSTR name, int size); +HCURSOR LoadCursor(HINSTANCE instance, LPCSTR name); +HICON LoadIcon(HINSTANCE instance, LPCSTR name); +HCURSOR CreateIconIndirect(ICONINFO * info); +BOOL DestroyCursor(HCURSOR cursor); +BOOL DestroyIcon(HICON icon); +int LoadString(HINSTANCE instance, UINT id, LPSTR buffer, int max); + +// --------------------------------------------------------------------------- +// Dialogs and controls. Every dialog in the tree is a Win32 resource template, which +// UI_DESIGN step 13 replaces; these report failure so that a caller takes its own +// no-dialog path rather than believing in a window that was never created. +// --------------------------------------------------------------------------- +HWND CreateDialogIndirectParam(HINSTANCE instance, LPCDLGTEMPLATE templ, HWND parent, DLGPROC proc, LPARAM param); +HWND CreateDialogParam(HINSTANCE instance, LPCSTR name, HWND parent, DLGPROC proc, LPARAM param); +INT_PTR DialogBoxParam(HINSTANCE instance, LPCSTR name, HWND parent, DLGPROC proc, LPARAM param); +BOOL EndDialog(HWND dialog, INT_PTR result); +BOOL IsDialogMessage(HWND dialog, LPMSG msg); +HWND GetDlgItem(HWND dialog, int id); +int GetDlgCtrlID(HWND control); +BOOL SetDlgItemText(HWND dialog, int id, LPCSTR text); +UINT GetDlgItemText(HWND dialog, int id, LPSTR text, int max); +BOOL CheckDlgButton(HWND dialog, int id, UINT check); +UINT IsDlgButtonChecked(HWND dialog, int id); +LRESULT SendDlgItemMessage(HWND dialog, int id, UINT message, WPARAM wparam, LPARAM lparam); +} + +#define SetWindowTextA SetWindowText +#define GetWindowTextA GetWindowText +#define LoadStringA LoadString +#define MessageBoxA MessageBox +#define GetClassNameA GetClassName +#define FindWindowA FindWindow + +// --------------------------------------------------------------------------- +// GDI. The engine draws its own frame into system-memory surfaces and presents it +// through bgfx, so the only GDI users left are the legacy dialog layer and the +// tactical font path. Nothing here draws. +// --------------------------------------------------------------------------- +#define BI_RGB 0 +#define BI_BITFIELDS 3 +#define DIB_RGB_COLORS 0 +#define DIB_PAL_COLORS 1 +#define SRCCOPY 0x00CC0020 +#define BLACKNESS 0x00000042 +#define COLORONCOLOR 3 +#define HALFTONE 4 +#define TRANSPARENT 1 +#define OPAQUE 2 +#define TA_LEFT 0 +#define TA_RIGHT 2 +#define TA_CENTER 6 +#define TA_TOP 0 +#define DT_LEFT 0x00000000 +#define DT_CENTER 0x00000001 +#define DT_VCENTER 0x00000004 +#define DT_SINGLELINE 0x00000020 +#define GM_COMPATIBLE 1 +#define GM_ADVANCED 2 +#define MWT_IDENTITY 1 +#define VREFRESH 116 +#define BITSPIXEL 12 +#define WHITE_BRUSH 0 +#define BLACK_BRUSH 4 +#define NULL_BRUSH 5 +#define SYSTEM_FONT 13 +#define FW_NORMAL 400 +#define FW_BOLD 700 +#define ANSI_CHARSET 0 +#define DEFAULT_CHARSET 1 +#define OUT_DEFAULT_PRECIS 0 +#define OUT_RASTER_PRECIS 6 +#define CLIP_DEFAULT_PRECIS 0 +#define DEFAULT_QUALITY 0 +#define PROOF_QUALITY 2 +#define DEFAULT_PITCH 0 +#define FF_DONTCARE 0 +#define FF_SWISS 32 + +typedef struct tagLOGFONTA { + LONG lfHeight, lfWidth, lfEscapement, lfOrientation, lfWeight; + BYTE lfItalic, lfUnderline, lfStrikeOut, lfCharSet, lfOutPrecision, lfClipPrecision, lfQuality, lfPitchAndFamily; + CHAR lfFaceName[32]; +} LOGFONTA, LOGFONT, *LPLOGFONT; + +typedef struct tagTEXTMETRICA { + LONG tmHeight, tmAscent, tmDescent, tmInternalLeading, tmExternalLeading; + LONG tmAveCharWidth, tmMaxCharWidth, tmWeight, tmOverhang, tmDigitizedAspectX, tmDigitizedAspectY; + CHAR tmFirstChar, tmLastChar, tmDefaultChar, tmBreakChar; + BYTE tmItalic, tmUnderlined, tmStruckOut, tmPitchAndFamily, tmCharSet; +} TEXTMETRICA, TEXTMETRIC, *LPTEXTMETRIC; + +extern "C" { +HDC GetDC(HWND window); +int ReleaseDC(HWND window, HDC dc); +HDC CreateCompatibleDC(HDC dc); +BOOL DeleteDC(HDC dc); +int SaveDC(HDC dc); +BOOL RestoreDC(HDC dc, int state); +HGDIOBJ SelectObject(HDC dc, HGDIOBJ object); +BOOL DeleteObject(HGDIOBJ object); +int GetObject(HGDIOBJ object, int size, LPVOID buffer); +HGDIOBJ GetStockObject(int index); +HBRUSH CreateSolidBrush(COLORREF color); +HBITMAP CreateBitmap(int width, int height, UINT planes, UINT bits, void const * data); +HBITMAP CreateDIBSection(HDC dc, BITMAPINFO const * info, UINT usage, void ** bits, HANDLE section, DWORD offset); +HFONT CreateFont(int height, int width, int escapement, int orientation, int weight, DWORD italic, DWORD underline, DWORD strikeout, DWORD charset, DWORD outprecision, DWORD clipprecision, DWORD quality, DWORD pitch, LPCSTR face); +HFONT CreateFontIndirect(LOGFONT const * font); +int GetDeviceCaps(HDC dc, int index); +BOOL BitBlt(HDC dest, int x, int y, int width, int height, HDC source, int sx, int sy, DWORD rop); +BOOL StretchBlt(HDC dest, int x, int y, int width, int height, HDC source, int sx, int sy, int swidth, int sheight, DWORD rop); +int SetStretchBltMode(HDC dc, int mode); +int SetDIBitsToDevice(HDC dc, int x, int y, DWORD width, DWORD height, int sx, int sy, UINT start, UINT lines, void const * bits, BITMAPINFO const * info, UINT usage); +BOOL TextOut(HDC dc, int x, int y, LPCSTR text, int length); +int DrawText(HDC dc, LPCSTR text, int length, LPRECT rect, UINT format); +BOOL GetTextExtentPoint32(HDC dc, LPCSTR text, int length, LPSIZE size); +BOOL GetTextMetrics(HDC dc, LPTEXTMETRIC metrics); +UINT SetTextAlign(HDC dc, UINT align); +COLORREF SetTextColor(HDC dc, COLORREF color); +COLORREF GetTextColor(HDC dc); +COLORREF SetBkColor(HDC dc, COLORREF color); +COLORREF GetBkColor(HDC dc); +int SetBkMode(HDC dc, int mode); +int GetBkMode(HDC dc); +int SetGraphicsMode(HDC dc, int mode); +BOOL SetViewportOrgEx(HDC dc, int x, int y, LPPOINT previous); +BOOL SetWindowOrgEx(HDC dc, int x, int y, LPPOINT previous); +BOOL DPtoLP(HDC dc, LPPOINT points, int count); +BOOL FillRect(HDC dc, RECT const * rect, HBRUSH brush); +BOOL PatBlt(HDC dc, int x, int y, int width, int height, DWORD rop); +void GdiFlush(void); +BOOL EnumDisplaySettings(LPCSTR device, DWORD mode, DEVMODE * settings); +} + +// --------------------------------------------------------------------------- +// Kernel services +// --------------------------------------------------------------------------- +#define ERROR_SUCCESS 0L +#define ERROR_ALREADY_EXISTS 183L +#define ERROR_FILE_NOT_FOUND 2L +#define WAIT_OBJECT_0 0L +#define WAIT_TIMEOUT 258L +#define WAIT_FAILED 0xFFFFFFFFL +#define MUTEX_ALL_ACCESS 0x1F0001L +#define INFINITE 0xFFFFFFFFL +#define GENERIC_READ 0x80000000L +#define GENERIC_WRITE 0x40000000L +#define FILE_SHARE_READ 0x00000001L +#define FILE_SHARE_WRITE 0x00000002L +#define CREATE_NEW 1 +#define CREATE_ALWAYS 2 +#define OPEN_EXISTING 3 +#define OPEN_ALWAYS 4 +#define FILE_BEGIN 0 +#define FILE_CURRENT 1 +#define FILE_END 2 +#define INVALID_SET_FILE_POINTER 0xFFFFFFFFL +#define INVALID_FILE_ATTRIBUTES 0xFFFFFFFFL +#define FILE_ATTRIBUTE_READONLY 0x00000001L +#define FILE_ATTRIBUTE_HIDDEN 0x00000002L +#define FILE_ATTRIBUTE_SYSTEM 0x00000004L +#define FILE_ATTRIBUTE_DIRECTORY 0x00000010L +#define FILE_ATTRIBUTE_ARCHIVE 0x00000020L +#define FILE_ATTRIBUTE_NORMAL 0x00000080L +#define FILE_ATTRIBUTE_TEMPORARY 0x00000100L +#define CP_ACP 0 +#define CP_OEMCP 1 +#define CP_UTF8 65001 +#define STD_INPUT_HANDLE ((DWORD)-10) +#define STD_OUTPUT_HANDLE ((DWORD)-11) +#define STD_ERROR_HANDLE ((DWORD)-12) +#define FORMAT_MESSAGE_FROM_SYSTEM 0x00001000 +#define FORMAT_MESSAGE_ALLOCATE_BUFFER 0x00000100 +#define FORMAT_MESSAGE_IGNORE_INSERTS 0x00000200 +#define LANG_NEUTRAL 0x00 +#define SUBLANG_DEFAULT 0x01 +#define LANG_USER_DEFAULT 0x0400 +#define MAKELANGID(p, s) ((((WORD)(s)) << 10) | (WORD)(p)) +#define TIME_NOSECONDS 0x0002 +#define TIME_NOMINUTESORSECONDS 0x0001 +#define HKEY_LOCAL_MACHINE ((HKEY)(ULONG_PTR)0x80000002) +#define KEY_READ 0x20019 +#define SRWLOCK_INIT { NULL } + +typedef struct _COORD { SHORT X, Y; } COORD; +typedef struct _SMALL_RECT { SHORT Left, Top, Right, Bottom; } SMALL_RECT; +typedef struct _CONSOLE_SCREEN_BUFFER_INFO { COORD dwSize, dwCursorPosition; WORD wAttributes; SMALL_RECT srWindow; COORD dwMaximumWindowSize; } CONSOLE_SCREEN_BUFFER_INFO; +typedef struct _RTL_OSVERSIONINFOW { ULONG dwOSVersionInfoSize, dwMajorVersion, dwMinorVersion, dwBuildNumber, dwPlatformId; WCHAR szCSDVersion[128]; } RTL_OSVERSIONINFOW, *PRTL_OSVERSIONINFOW; +typedef BYTE *PBYTE; + +extern "C" { +DWORD GetLastError(void); +void SetLastError(DWORD code); +HANDLE CreateMutex(LPSECURITY_ATTRIBUTES attributes, BOOL owner, LPCSTR name); +HANDLE OpenMutex(DWORD access, BOOL inherit, LPCSTR name); +DWORD WaitForSingleObject(HANDLE object, DWORD milliseconds); +BOOL ReleaseMutex(HANDLE mutex); +BOOL CloseHandle(HANDLE object); +DWORD GetCurrentProcessId(void); +DWORD GetCurrentThreadId(void); +BOOL IsDebuggerPresent(void); +void OutputDebugString(LPCSTR text); +void Sleep(DWORD milliseconds); +HMODULE GetModuleHandle(LPCSTR name); +HMODULE LoadLibrary(LPCSTR name); +BOOL FreeLibrary(HMODULE module); +FARPROC GetProcAddress(HMODULE module, LPCSTR name); +DWORD GetModuleFileName(HMODULE module, LPSTR name, DWORD size); +HLOCAL LocalFree(HLOCAL memory); +LPWSTR GetCommandLineW(void); +LPWSTR * CommandLineToArgvW(LPCWSTR commandline, int * count); +UINT GetACP(void); +UINT GetOEMCP(void); + +HANDLE CreateFileA(LPCSTR name, DWORD access, DWORD share, LPSECURITY_ATTRIBUTES attributes, DWORD disposition, DWORD flags, HANDLE templatefile); +BOOL ReadFile(HANDLE file, LPVOID buffer, DWORD size, LPDWORD read, LPOVERLAPPED overlapped); +BOOL WriteFile(HANDLE file, LPCVOID buffer, DWORD size, LPDWORD written, LPOVERLAPPED overlapped); +DWORD SetFilePointer(HANDLE file, LONG distance, LONG * distancehigh, DWORD method); +BOOL DeleteFileA(LPCSTR name); +BOOL CopyFile(LPCSTR from, LPCSTR to, BOOL failifexists); +BOOL CreateDirectory(LPCSTR path, LPSECURITY_ATTRIBUTES attributes); +BOOL SetCurrentDirectory(LPCSTR path); +DWORD GetFileAttributesA(LPCSTR name); +HANDLE FindFirstFile(LPCSTR name, LPWIN32_FIND_DATA data); +BOOL FindNextFile(HANDLE find, LPWIN32_FIND_DATA data); +BOOL FindClose(HANDLE find); +LONG CompareFileTime(FILETIME const * a, FILETIME const * b); +BOOL FileTimeToLocalFileTime(FILETIME const * file, LPFILETIME local); +BOOL FileTimeToSystemTime(FILETIME const * file, LPSYSTEMTIME system); +BOOL SystemTimeToFileTime(SYSTEMTIME const * system, LPFILETIME file); +void GetSystemTime(LPSYSTEMTIME system); + +BOOL AllocConsole(void); +HWND GetConsoleWindow(void); +HANDLE GetStdHandle(DWORD which); +BOOL SetConsoleTitle(LPCSTR title); +BOOL WriteConsole(HANDLE console, void const * buffer, DWORD length, LPDWORD written, LPVOID reserved); +BOOL GetConsoleScreenBufferInfo(HANDLE console, CONSOLE_SCREEN_BUFFER_INFO * info); + +void InitializeSRWLock(SRWLOCK * lock); +void AcquireSRWLockExclusive(SRWLOCK * lock); +void ReleaseSRWLockExclusive(SRWLOCK * lock); + +DWORD GetFileVersionInfoSize(LPCSTR name, LPDWORD handle); +BOOL GetFileVersionInfo(LPCSTR name, DWORD handle, DWORD length, LPVOID data); +BOOL VerQueryValue(LPCVOID block, LPCSTR path, LPVOID * buffer, UINT * length); +HRSRC FindResource(HMODULE module, LPCSTR name, LPCSTR type); +HGLOBAL LoadResource(HMODULE module, HRSRC resource); +LPVOID LockResource(HGLOBAL resource); +DWORD SizeofResource(HMODULE module, HRSRC resource); +LONG RegOpenKeyEx(HKEY key, LPCSTR subkey, DWORD options, DWORD access, HKEY * result); +LONG RegQueryValueEx(HKEY key, LPCSTR name, LPDWORD reserved, LPDWORD type, LPBYTE data, LPDWORD size); +LONG RegCloseKey(HKEY key); +} + +#define DeleteFile DeleteFileA +#define GetFileAttributes GetFileAttributesA +#define CreateFile CreateFileA +#define GetFileVersionInfoSizeA GetFileVersionInfoSize +#define GetFileVersionInfoA GetFileVersionInfo +#define VerQueryValueA VerQueryValue +#define GetModuleFileNameA GetModuleFileName +#define LoadLibraryA LoadLibrary +#define GetModuleHandleA GetModuleHandle +#define OutputDebugStringA OutputDebugString +#define SetConsoleTitleA SetConsoleTitle +#define WriteConsoleA WriteConsole +#define RegOpenKeyExA RegOpenKeyEx +#define RegQueryValueExA RegQueryValueEx +#define CreateMutexA CreateMutex +#define OpenMutexA OpenMutex +#define FindFirstFileA FindFirstFile +#define FindNextFileA FindNextFile +#define CreateDirectoryA CreateDirectory +#define SetCurrentDirectoryA SetCurrentDirectory +#define CopyFileA CopyFile + +// --------------------------------------------------------------------------- +// The remaining entry points the tree names, grouped with the ones above by role. +// --------------------------------------------------------------------------- +typedef struct _XFORM { FLOAT eM11, eM12, eM21, eM22, eDx, eDy; } XFORM; + +extern "C" { +int MultiByteToWideChar(UINT codepage, DWORD flags, LPCSTR source, int sourcelength, LPWSTR dest, int destlength); +int WideCharToMultiByte(UINT codepage, DWORD flags, LPCWSTR source, int sourcelength, LPSTR dest, int destlength, LPCSTR defaultchar, LPBOOL useddefault); +void GetLocalTime(LPSYSTEMTIME time); +int GetTimeFormat(DWORD locale, DWORD flags, SYSTEMTIME const * time, LPCSTR format, LPSTR buffer, int size); +int GetDateFormat(DWORD locale, DWORD flags, SYSTEMTIME const * time, LPCSTR format, LPSTR buffer, int size); +DWORD FormatMessage(DWORD flags, LPCVOID source, DWORD id, DWORD language, LPSTR buffer, DWORD size, void * arguments); +BOOL SetStdHandle(DWORD which, HANDLE handle); +BOOL SetConsoleCP(UINT codepage); +BOOL SetConsoleOutputCP(UINT codepage); +BOOL SetConsoleScreenBufferSize(HANDLE console, COORD size); +BOOL DeleteMenu(HMENU menu, UINT position, UINT flags); +BOOL WinHelp(HWND window, LPCSTR help, UINT command, ULONG_PTR data); +int ToUnicode(UINT key, UINT scan, BYTE const * state, LPWSTR buffer, int size, UINT flags); +HWND GetNextDlgTabItem(HWND dialog, HWND control, BOOL previous); +BOOL ModifyWorldTransform(HDC dc, XFORM const * transform, DWORD mode); +} + +#define SNDMSG SendMessage +#define SendMessageA SendMessage +#define PostMessageA PostMessage +#define GetWindowLongPtrA GetWindowLongPtr +#define SetWindowLongPtrA SetWindowLongPtr +#define GetWindowLongA GetWindowLong +#define SetWindowLongA SetWindowLong +#define MultiByteToWideCharA MultiByteToWideChar +#define GetTimeFormatA GetTimeFormat +#define GetDateFormatA GetDateFormat +#define FormatMessageA FormatMessage +#define WinHelpA WinHelp +#define GetKeyNameTextA GetKeyNameText +#define SetDlgItemTextA SetDlgItemText +#define GetDlgItemTextA GetDlgItemText +#define CreateFontA CreateFont +#define TextOutA TextOut +#define DrawTextA DrawText +#define GetTextExtentPoint32A GetTextExtentPoint32 +#define EnumDisplaySettingsA EnumDisplaySettings diff --git a/platform/win32compat/include/windowsx.h b/platform/win32compat/include/windowsx.h new file mode 100644 index 000000000..85f7a1f04 --- /dev/null +++ b/platform/win32compat/include/windowsx.h @@ -0,0 +1,46 @@ +#pragma once +#include + +// The control wrappers Windows spells as macros over SendMessage. Keeping them as macros +// keeps the call sites identical to the supported build. +#define Button_GetCheck(hwnd) ((int)SendMessage((hwnd), BM_GETCHECK, 0, 0)) +#define Button_SetCheck(hwnd, check) ((void)SendMessage((hwnd), BM_SETCHECK, (WPARAM)(int)(check), 0)) +#define Button_Enable(hwnd, enable) EnableWindow((hwnd), (BOOL)(enable)) +#define Button_SetText(hwnd, text) SetWindowText((hwnd), (text)) +#define Static_SetText(hwnd, text) SetWindowText((hwnd), (text)) +#define Edit_SetText(hwnd, text) SetWindowText((hwnd), (text)) +#define Edit_GetText(hwnd, text, max) GetWindowText((hwnd), (text), (max)) + +#define ListBox_AddString(hwnd, text) ((int)SendMessage((hwnd), LB_ADDSTRING, 0, (LPARAM)(LPCSTR)(text))) +#define ListBox_InsertString(hwnd, index, text) ((int)SendMessage((hwnd), LB_INSERTSTRING, (WPARAM)(int)(index), (LPARAM)(LPCSTR)(text))) +#define ListBox_DeleteString(hwnd, index) ((int)SendMessage((hwnd), LB_DELETESTRING, (WPARAM)(int)(index), 0)) +#define ListBox_ResetContent(hwnd) ((BOOL)SendMessage((hwnd), LB_RESETCONTENT, 0, 0)) +#define ListBox_GetCount(hwnd) ((int)SendMessage((hwnd), LB_GETCOUNT, 0, 0)) +#define ListBox_GetCurSel(hwnd) ((int)SendMessage((hwnd), LB_GETCURSEL, 0, 0)) +#define ListBox_SetCurSel(hwnd, index) ((int)SendMessage((hwnd), LB_SETCURSEL, (WPARAM)(int)(index), 0)) +#define ListBox_GetSel(hwnd, index) ((int)SendMessage((hwnd), LB_GETSEL, (WPARAM)(int)(index), 0)) +#define ListBox_SetSel(hwnd, select, index) ((int)SendMessage((hwnd), LB_SETSEL, (WPARAM)(BOOL)(select), (LPARAM)(int)(index))) +#define ListBox_GetText(hwnd, index, text) ((int)SendMessage((hwnd), LB_GETTEXT, (WPARAM)(int)(index), (LPARAM)(LPCSTR)(text))) +#define ListBox_GetItemData(hwnd, index) ((LRESULT)SendMessage((hwnd), LB_GETITEMDATA, (WPARAM)(int)(index), 0)) +#define ListBox_SetItemData(hwnd, index, data) ((int)SendMessage((hwnd), LB_SETITEMDATA, (WPARAM)(int)(index), (LPARAM)(data))) +#define ListBox_SetTopIndex(hwnd, index) ((int)SendMessage((hwnd), LB_SETTOPINDEX, (WPARAM)(int)(index), 0)) +#define ListBox_GetTopIndex(hwnd) ((int)SendMessage((hwnd), LB_GETTOPINDEX, 0, 0)) +#define ListBox_FindStringExact(hwnd, start, text) ((int)SendMessage((hwnd), LB_FINDSTRINGEXACT, (WPARAM)(int)(start), (LPARAM)(LPCSTR)(text))) +#define ListBox_SetItemHeight(hwnd, index, height) ((int)SendMessage((hwnd), LB_SETITEMHEIGHT, (WPARAM)(int)(index), MAKELPARAM((height), 0))) +#define ListBox_GetItemHeight(hwnd, index) ((int)SendMessage((hwnd), LB_GETITEMHEIGHT, (WPARAM)(int)(index), 0)) + +#define ComboBox_AddString(hwnd, text) ((int)SendMessage((hwnd), CB_ADDSTRING, 0, (LPARAM)(LPCSTR)(text))) +#define ComboBox_InsertString(hwnd, index, text) ((int)SendMessage((hwnd), CB_INSERTSTRING, (WPARAM)(int)(index), (LPARAM)(LPCSTR)(text))) +#define ComboBox_DeleteString(hwnd, index) ((int)SendMessage((hwnd), CB_DELETESTRING, (WPARAM)(int)(index), 0)) +#define ComboBox_ResetContent(hwnd) ((int)SendMessage((hwnd), CB_RESETCONTENT, 0, 0)) +#define ComboBox_GetCount(hwnd) ((int)SendMessage((hwnd), CB_GETCOUNT, 0, 0)) +#define ComboBox_GetCurSel(hwnd) ((int)SendMessage((hwnd), CB_GETCURSEL, 0, 0)) +#define ComboBox_SetCurSel(hwnd, index) ((int)SendMessage((hwnd), CB_SETCURSEL, (WPARAM)(int)(index), 0)) +#define ComboBox_FindString(hwnd, start, text) ((int)SendMessage((hwnd), CB_FINDSTRING, (WPARAM)(int)(start), (LPARAM)(LPCSTR)(text))) +#define ComboBox_GetItemData(hwnd, index) ((LRESULT)SendMessage((hwnd), CB_GETITEMDATA, (WPARAM)(int)(index), 0)) +#define ComboBox_SetItemData(hwnd, index, data) ((int)SendMessage((hwnd), CB_SETITEMDATA, (WPARAM)(int)(index), (LPARAM)(data))) +#define ComboBox_GetLBText(hwnd, index, text) ((int)SendMessage((hwnd), CB_GETLBTEXT, (WPARAM)(int)(index), (LPARAM)(LPCSTR)(text))) +#define ComboBox_GetDroppedControlRect(hwnd, rect) ((void)SendMessage((hwnd), CB_GETDROPPEDCONTROLRECT, 0, (LPARAM)(RECT *)(rect))) +#define ComboBox_GetDroppedState(hwnd) ((BOOL)SendMessage((hwnd), CB_GETDROPPEDSTATE, 0, 0)) +#define ComboBox_ShowDropdown(hwnd, show) ((BOOL)SendMessage((hwnd), CB_SHOWDROPDOWN, (WPARAM)(BOOL)(show), 0)) +#define Edit_SetSel(hwnd, start, end) ((void)SendMessage((hwnd), EM_SETSEL, (WPARAM)(int)(start), (LPARAM)(int)(end))) diff --git a/platform/win32compat/include/wingdi.h b/platform/win32compat/include/wingdi.h new file mode 100644 index 000000000..720e64a9b --- /dev/null +++ b/platform/win32compat/include/wingdi.h @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/platform/win32compat/include/winioctl.h b/platform/win32compat/include/winioctl.h new file mode 100644 index 000000000..720e64a9b --- /dev/null +++ b/platform/win32compat/include/winioctl.h @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/platform/win32compat/include/winnt.h b/platform/win32compat/include/winnt.h new file mode 100644 index 000000000..079a447ae --- /dev/null +++ b/platform/win32compat/include/winnt.h @@ -0,0 +1,45 @@ +#pragma once +#include + +// The sync record hook reads its own module's PE headers to recover the build's +// symbol layout. Nothing outside Windows has a PE image to read; the declarations +// exist so the file still compiles and its reader reports that it found nothing. +#define IMAGE_DOS_SIGNATURE 0x5A4D +#define IMAGE_NT_SIGNATURE 0x00004550 +#define IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10B +#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16 + +typedef struct _IMAGE_DOS_HEADER { + WORD e_magic, e_cblp, e_cp, e_crlc, e_cparhdr, e_minalloc, e_maxalloc; + WORD e_ss, e_sp, e_csum, e_ip, e_cs, e_lfarlc, e_ovno, e_res[4]; + WORD e_oemid, e_oeminfo, e_res2[10]; + LONG e_lfanew; +} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER; + +typedef struct _IMAGE_FILE_HEADER { + WORD Machine, NumberOfSections; + DWORD TimeDateStamp, PointerToSymbolTable, NumberOfSymbols; + WORD SizeOfOptionalHeader, Characteristics; +} IMAGE_FILE_HEADER; + +typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress, Size; } IMAGE_DATA_DIRECTORY; + +typedef struct _IMAGE_OPTIONAL_HEADER { + WORD Magic; BYTE MajorLinkerVersion, MinorLinkerVersion; + DWORD SizeOfCode, SizeOfInitializedData, SizeOfUninitializedData; + DWORD AddressOfEntryPoint, BaseOfCode, BaseOfData, ImageBase; + DWORD SectionAlignment, FileAlignment; + WORD MajorOperatingSystemVersion, MinorOperatingSystemVersion; + WORD MajorImageVersion, MinorImageVersion, MajorSubsystemVersion, MinorSubsystemVersion; + DWORD Win32VersionValue, SizeOfImage, SizeOfHeaders, CheckSum; + WORD Subsystem, DllCharacteristics; + DWORD SizeOfStackReserve, SizeOfStackCommit, SizeOfHeapReserve, SizeOfHeapCommit; + DWORD LoaderFlags, NumberOfRvaAndSizes; + IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; +} IMAGE_OPTIONAL_HEADER32; + +typedef struct _IMAGE_NT_HEADERS { + DWORD Signature; + IMAGE_FILE_HEADER FileHeader; + IMAGE_OPTIONAL_HEADER32 OptionalHeader; +} IMAGE_NT_HEADERS32, IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS; diff --git a/platform/win32compat/include/winres.h b/platform/win32compat/include/winres.h new file mode 100644 index 000000000..720e64a9b --- /dev/null +++ b/platform/win32compat/include/winres.h @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/platform/win32compat/include/winsock.h b/platform/win32compat/include/winsock.h new file mode 100644 index 000000000..498e0be92 --- /dev/null +++ b/platform/win32compat/include/winsock.h @@ -0,0 +1,11 @@ +#pragma once +#include +#include +#include +#include +#include + +typedef int SOCKET; +#define INVALID_SOCKET (-1) +#define SOCKET_ERROR (-1) +typedef struct WSAData { WORD wVersion; WORD wHighVersion; char szDescription[257]; char szSystemStatus[129]; unsigned short iMaxSockets; unsigned short iMaxUdpDg; char *lpVendorInfo; } WSADATA, *LPWSADATA; diff --git a/platform/win32compat/include/winsock2.h b/platform/win32compat/include/winsock2.h new file mode 100644 index 000000000..498e0be92 --- /dev/null +++ b/platform/win32compat/include/winsock2.h @@ -0,0 +1,11 @@ +#pragma once +#include +#include +#include +#include +#include + +typedef int SOCKET; +#define INVALID_SOCKET (-1) +#define SOCKET_ERROR (-1) +typedef struct WSAData { WORD wVersion; WORD wHighVersion; char szDescription[257]; char szSystemStatus[129]; unsigned short iMaxSockets; unsigned short iMaxUdpDg; char *lpVendorInfo; } WSADATA, *LPWSADATA; diff --git a/platform/win32compat/include/winuser.h b/platform/win32compat/include/winuser.h new file mode 100644 index 000000000..720e64a9b --- /dev/null +++ b/platform/win32compat/include/winuser.h @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/platform/win32compat/include/ws2tcpip.h b/platform/win32compat/include/ws2tcpip.h new file mode 100644 index 000000000..e6faf3a8f --- /dev/null +++ b/platform/win32compat/include/ws2tcpip.h @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/platform/win32compat/src/dialog.cpp b/platform/win32compat/src/dialog.cpp new file mode 100644 index 000000000..a5d0c8abd --- /dev/null +++ b/platform/win32compat/src/dialog.cpp @@ -0,0 +1,90 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#include "win32compat.h" + +#include + +// Every dialog in the tree is a Win32 resource template driven by OwnerDraw, and +// docs/UI_DESIGN.md step 13 replaces that layer rather than porting it. Creating a dialog +// therefore fails here, and each driver takes the path it already has for a dialog that +// could not be created. Nothing below draws or measures anything: a control that was +// never created answers no message. + +extern "C" HWND CreateDialogIndirectParam(HINSTANCE instance, LPCDLGTEMPLATE templ, HWND parent, + DLGPROC proc, LPARAM param) +{ + (void)instance; + (void)templ; + (void)parent; + (void)proc; + (void)param; + return(NULL); +} + + +extern "C" HWND CreateDialogParam(HINSTANCE instance, LPCSTR name, HWND parent, DLGPROC proc, LPARAM param) +{ + (void)instance; + (void)name; + (void)parent; + (void)proc; + (void)param; + return(NULL); +} + + +extern "C" INT_PTR DialogBoxParam(HINSTANCE instance, LPCSTR name, HWND parent, DLGPROC proc, LPARAM param) +{ + (void)instance; + (void)name; + (void)parent; + (void)proc; + (void)param; + return(IDCANCEL); +} + + +extern "C" BOOL EndDialog(HWND dialog, INT_PTR result) { (void)dialog; (void)result; return(TRUE); } +extern "C" BOOL IsDialogMessage(HWND dialog, LPMSG msg) { (void)dialog; (void)msg; return(FALSE); } +extern "C" HWND GetDlgItem(HWND dialog, int id) { (void)dialog; (void)id; return(NULL); } +extern "C" int GetDlgCtrlID(HWND control) { (void)control; return(0); } +extern "C" HWND GetNextDlgTabItem(HWND dialog, HWND control, BOOL previous) { (void)dialog; (void)control; (void)previous; return(NULL); } +extern "C" BOOL SetDlgItemText(HWND dialog, int id, LPCSTR text) { (void)dialog; (void)id; (void)text; return(FALSE); } +extern "C" BOOL CheckDlgButton(HWND dialog, int id, UINT check) { (void)dialog; (void)id; (void)check; return(FALSE); } +extern "C" UINT IsDlgButtonChecked(HWND dialog, int id) { (void)dialog; (void)id; return(BST_UNCHECKED); } + + +extern "C" UINT GetDlgItemText(HWND dialog, int id, LPSTR text, int max) +{ + (void)dialog; + (void)id; + + if (text != NULL && max > 0) { + text[0] = '\0'; + } + + return(0); +} + + +extern "C" LRESULT SendDlgItemMessage(HWND dialog, int id, UINT message, WPARAM wparam, LPARAM lparam) +{ + return(SendMessage(GetDlgItem(dialog, id), message, wparam, lparam)); +} + + +extern "C" void InitCommonControls(void) {} +extern "C" BOOL ImageList_BeginDrag(HIMAGELIST list, int image, int x, int y) { (void)list; (void)image; (void)x; (void)y; return(FALSE); } +extern "C" BOOL ImageList_DragEnter(HWND lock, int x, int y) { (void)lock; (void)x; (void)y; return(FALSE); } +extern "C" BOOL ImageList_DragMove(int x, int y) { (void)x; (void)y; return(FALSE); } +extern "C" BOOL ImageList_DragShowNolock(BOOL show) { (void)show; return(FALSE); } +extern "C" void ImageList_EndDrag(void) {} +extern "C" BOOL ImageList_Destroy(HIMAGELIST list) { (void)list; return(FALSE); } + diff --git a/platform/win32compat/src/entry.cpp b/platform/win32compat/src/entry.cpp new file mode 100644 index 000000000..6c657725d --- /dev/null +++ b/platform/win32compat/src/entry.cpp @@ -0,0 +1,34 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#include "win32compat.h" + +// Windows enters the game at WinMain. Nothing else does, so the host's entry point records +// the arguments the shell handed over and hands control to the same function the supported +// build starts in. + +int CALLBACK WinMain(HINSTANCE instance, HINSTANCE previous, char * commandline, int show); + +void Win32_Record_Arguments(int argc, char ** argv); + + +int main(int argc, char ** argv) +{ + Win32_Record_Arguments(argc, argv); + + if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) { + SDL_Log("SDL could not start: %s", SDL_GetError()); + return(1); + } + + int const result = WinMain((HINSTANCE)(ULONG_PTR)1, NULL, NULL, 1); + + SDL_Quit(); + return(result); +} diff --git a/platform/win32compat/src/gdi.cpp b/platform/win32compat/src/gdi.cpp new file mode 100644 index 000000000..cf38d79d1 --- /dev/null +++ b/platform/win32compat/src/gdi.cpp @@ -0,0 +1,193 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#include "win32compat.h" + +#include +#include +#include +#include + +// The game composes its own frame in system memory and presents it through bgfx, so GDI +// is reached only by the legacy dialog layer and by the tactical map's text, both of which +// draw with the host font on Windows. Nothing here draws: a device context that does not +// exist refuses every call, and the callers fall back to the engine's own font path. +// +// Bitmaps are the exception. The game builds its mouse cursor by drawing one of its own +// shape frames into a bitmap and handing it to CreateIconIndirect, so a bitmap has to hold +// real pixels for the cursor to exist at all. + +extern "C" HDC GetDC(HWND window) { (void)window; return(NULL); } +extern "C" int ReleaseDC(HWND window, HDC dc) { (void)window; (void)dc; return(0); } +extern "C" HDC CreateCompatibleDC(HDC dc) { (void)dc; return(NULL); } +extern "C" BOOL DeleteDC(HDC dc) { (void)dc; return(FALSE); } +extern "C" int SaveDC(HDC dc) { (void)dc; return(0); } +extern "C" BOOL RestoreDC(HDC dc, int state) { (void)dc; (void)state; return(FALSE); } +extern "C" HGDIOBJ SelectObject(HDC dc, HGDIOBJ object) { (void)dc; (void)object; return(NULL); } +extern "C" int GetObject(HGDIOBJ object, int size, LPVOID buffer) { (void)object; (void)size; (void)buffer; return(0); } +extern "C" HGDIOBJ GetStockObject(int index) { (void)index; return(NULL); } +extern "C" HBRUSH CreateSolidBrush(COLORREF color) { (void)color; return(NULL); } +extern "C" HFONT CreateFont(int height, int width, int escapement, int orientation, int weight, DWORD italic, DWORD underline, DWORD strikeout, DWORD charset, DWORD outprecision, DWORD clipprecision, DWORD quality, DWORD pitch, LPCSTR face) { (void)height; (void)width; (void)escapement; (void)orientation; (void)weight; (void)italic; (void)underline; (void)strikeout; (void)charset; (void)outprecision; (void)clipprecision; (void)quality; (void)pitch; (void)face; return(NULL); } +extern "C" HFONT CreateFontIndirect(LOGFONT const * font) { (void)font; return(NULL); } +extern "C" BOOL BitBlt(HDC dest, int x, int y, int width, int height, HDC source, int sx, int sy, DWORD rop) { (void)dest; (void)x; (void)y; (void)width; (void)height; (void)source; (void)sx; (void)sy; (void)rop; return(FALSE); } +extern "C" BOOL StretchBlt(HDC dest, int x, int y, int width, int height, HDC source, int sx, int sy, int swidth, int sheight, DWORD rop) { (void)dest; (void)x; (void)y; (void)width; (void)height; (void)source; (void)sx; (void)sy; (void)swidth; (void)sheight; (void)rop; return(FALSE); } +extern "C" int SetStretchBltMode(HDC dc, int mode) { (void)dc; (void)mode; return(0); } +extern "C" int SetDIBitsToDevice(HDC dc, int x, int y, DWORD width, DWORD height, int sx, int sy, UINT start, UINT lines, void const * bits, BITMAPINFO const * info, UINT usage) { (void)dc; (void)x; (void)y; (void)width; (void)height; (void)sx; (void)sy; (void)start; (void)lines; (void)bits; (void)info; (void)usage; return(0); } +extern "C" BOOL TextOut(HDC dc, int x, int y, LPCSTR text, int length) { (void)dc; (void)x; (void)y; (void)text; (void)length; return(FALSE); } +extern "C" int DrawText(HDC dc, LPCSTR text, int length, LPRECT rect, UINT format) { (void)dc; (void)text; (void)length; (void)rect; (void)format; return(0); } +extern "C" BOOL GetTextMetrics(HDC dc, LPTEXTMETRIC metrics) { (void)dc; (void)metrics; return(FALSE); } +extern "C" UINT SetTextAlign(HDC dc, UINT align) { (void)dc; (void)align; return(0); } +extern "C" COLORREF SetTextColor(HDC dc, COLORREF color) { (void)dc; (void)color; return(0); } +extern "C" COLORREF GetTextColor(HDC dc) { (void)dc; return(0); } +extern "C" COLORREF SetBkColor(HDC dc, COLORREF color) { (void)dc; (void)color; return(0); } +extern "C" COLORREF GetBkColor(HDC dc) { (void)dc; return(0); } +extern "C" int SetBkMode(HDC dc, int mode) { (void)dc; (void)mode; return(0); } +extern "C" int GetBkMode(HDC dc) { (void)dc; return(0); } +extern "C" int SetGraphicsMode(HDC dc, int mode) { (void)dc; (void)mode; return(0); } +extern "C" BOOL SetViewportOrgEx(HDC dc, int x, int y, LPPOINT previous) { (void)dc; (void)x; (void)y; (void)previous; return(FALSE); } +extern "C" BOOL SetWindowOrgEx(HDC dc, int x, int y, LPPOINT previous) { (void)dc; (void)x; (void)y; (void)previous; return(FALSE); } +extern "C" BOOL DPtoLP(HDC dc, LPPOINT points, int count) { (void)dc; (void)points; (void)count; return(FALSE); } +extern "C" BOOL FillRect(HDC dc, RECT const * rect, HBRUSH brush) { (void)dc; (void)rect; (void)brush; return(FALSE); } +extern "C" BOOL PatBlt(HDC dc, int x, int y, int width, int height, DWORD rop) { (void)dc; (void)x; (void)y; (void)width; (void)height; (void)rop; return(FALSE); } +extern "C" BOOL ModifyWorldTransform(HDC dc, XFORM const * transform, DWORD mode) { (void)dc; (void)transform; (void)mode; return(FALSE); } +extern "C" void GdiFlush(void) {} + + +extern "C" BOOL GetTextExtentPoint32(HDC dc, LPCSTR text, int length, LPSIZE size) +{ + (void)dc; + (void)text; + (void)length; + + if (size != NULL) { + size->cx = 0; + size->cy = 0; + } + + return(FALSE); +} + + +extern "C" int GetDeviceCaps(HDC dc, int index) +{ + (void)dc; + (void)index; + return(0); +} + + +static std::vector _Bitmaps; + + +Win32Bitmap * Win32_Lookup_Bitmap(HBITMAP bitmap) +{ + for (Win32Bitmap * record : _Bitmaps) { + if ((HBITMAP)record == bitmap) { + return(record); + } + } + + return(NULL); +} + + +static Win32Bitmap * Make_Bitmap(int width, int height, int bitcount, int alignment, bool topdown) +{ + if (width <= 0 || height <= 0) { + return(NULL); + } + + // A device-dependent bitmap aligns its scan lines to a word and a device-independent + // one to a double word, and the caller sizes the pixels it hands over to match. + int const bits = alignment * 8; + int const pitch = (((width * bitcount) + bits - 1) / bits) * alignment; + + Win32Bitmap * record = new(std::nothrow) Win32Bitmap; + + if (record == NULL) { + return(NULL); + } + + record->Width = width; + record->Height = height; + record->BitCount = bitcount; + record->Pitch = pitch; + record->TopDown = topdown; + record->Bits = new(std::nothrow) unsigned char[(std::size_t)pitch * (std::size_t)height](); + + if (record->Bits == NULL) { + delete record; + return(NULL); + } + + _Bitmaps.push_back(record); + return(record); +} + + +extern "C" HBITMAP CreateBitmap(int width, int height, UINT planes, UINT bits, void const * data) +{ + if (planes != 1 || (bits != 1 && bits != 32)) { + return(NULL); + } + + Win32Bitmap * record = Make_Bitmap(width, height, (int)bits, 2, true); + + if (record != NULL && data != NULL) { + memcpy(record->Bits, data, (std::size_t)record->Pitch * (std::size_t)record->Height); + } + + return((HBITMAP)record); +} + + +// Only the layout the cursor is drawn in is offered: an uncompressed 32-bit image whose +// rows the caller then writes itself. Anything else is refused the way the rest of GDI is. +extern "C" HBITMAP CreateDIBSection(HDC dc, BITMAPINFO const * info, UINT usage, void ** bits, HANDLE section, DWORD offset) +{ + (void)dc; + (void)usage; + + if (bits != NULL) { + *bits = NULL; + } + + if (info == NULL || section != NULL || offset != 0) { + return(NULL); + } + + if (info->bmiHeader.biPlanes != 1 || info->bmiHeader.biBitCount != 32 || info->bmiHeader.biCompression != BI_RGB) { + return(NULL); + } + + LONG const rawheight = info->bmiHeader.biHeight; + Win32Bitmap * record = Make_Bitmap((int)info->bmiHeader.biWidth, + (int)(rawheight < 0 ? -rawheight : rawheight), 32, 4, rawheight < 0); + + if (record != NULL && bits != NULL) { + *bits = record->Bits; + } + + return((HBITMAP)record); +} + + +extern "C" BOOL DeleteObject(HGDIOBJ object) +{ + for (auto it = _Bitmaps.begin(); it != _Bitmaps.end(); ++it) { + if ((HGDIOBJ)*it == object) { + delete [] (*it)->Bits; + delete *it; + _Bitmaps.erase(it); + return(TRUE); + } + } + + return(FALSE); +} diff --git a/platform/win32compat/src/input.cpp b/platform/win32compat/src/input.cpp new file mode 100644 index 000000000..6a15564e4 --- /dev/null +++ b/platform/win32compat/src/input.cpp @@ -0,0 +1,479 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#include "win32compat.h" + +#include +#include +#include +#include + +// The virtual key codes the engine's keyboard queue is written against. They are stated +// as numbers rather than taken from the engine's own header so that this layer does not +// depend on the engine it serves. +static int const VK_LBUTTON_CODE = 0x01; +static int const VK_RBUTTON_CODE = 0x02; +static int const VK_MBUTTON_CODE = 0x04; + +struct KeyMapping +{ + SDL_Scancode Scancode; + int VirtualKey; +}; + +static KeyMapping const _Keys[] = { + { SDL_SCANCODE_BACKSPACE, 0x08 }, { SDL_SCANCODE_TAB, 0x09 }, + { SDL_SCANCODE_RETURN, 0x0D }, { SDL_SCANCODE_ESCAPE, 0x1B }, + { SDL_SCANCODE_SPACE, 0x20 }, { SDL_SCANCODE_PAGEUP, 0x21 }, + { SDL_SCANCODE_PAGEDOWN, 0x22 }, { SDL_SCANCODE_END, 0x23 }, + { SDL_SCANCODE_HOME, 0x24 }, { SDL_SCANCODE_LEFT, 0x25 }, + { SDL_SCANCODE_UP, 0x26 }, { SDL_SCANCODE_RIGHT, 0x27 }, + { SDL_SCANCODE_DOWN, 0x28 }, { SDL_SCANCODE_INSERT, 0x2D }, + { SDL_SCANCODE_DELETE, 0x2E }, + { SDL_SCANCODE_LSHIFT, 0x10 }, { SDL_SCANCODE_RSHIFT, 0x10 }, + { SDL_SCANCODE_LCTRL, 0x11 }, { SDL_SCANCODE_RCTRL, 0x11 }, + { SDL_SCANCODE_LALT, 0x12 }, { SDL_SCANCODE_RALT, 0x12 }, + { SDL_SCANCODE_CAPSLOCK, 0x14 }, { SDL_SCANCODE_PAUSE, 0x13 }, + { SDL_SCANCODE_PRINTSCREEN, 0x2C }, { SDL_SCANCODE_SCROLLLOCK, 0x91 }, + { SDL_SCANCODE_NUMLOCKCLEAR, 0x90 }, + { SDL_SCANCODE_KP_0, 0x60 }, { SDL_SCANCODE_KP_1, 0x61 }, { SDL_SCANCODE_KP_2, 0x62 }, + { SDL_SCANCODE_KP_3, 0x63 }, { SDL_SCANCODE_KP_4, 0x64 }, { SDL_SCANCODE_KP_5, 0x65 }, + { SDL_SCANCODE_KP_6, 0x66 }, { SDL_SCANCODE_KP_7, 0x67 }, { SDL_SCANCODE_KP_8, 0x68 }, + { SDL_SCANCODE_KP_9, 0x69 }, { SDL_SCANCODE_KP_MULTIPLY, 0x6A }, + { SDL_SCANCODE_KP_PLUS, 0x6B }, { SDL_SCANCODE_KP_MINUS, 0x6D }, + { SDL_SCANCODE_KP_PERIOD, 0x6E }, { SDL_SCANCODE_KP_DIVIDE, 0x6F }, + { SDL_SCANCODE_KP_ENTER, 0x0D }, + { SDL_SCANCODE_F1, 0x70 }, { SDL_SCANCODE_F2, 0x71 }, { SDL_SCANCODE_F3, 0x72 }, + { SDL_SCANCODE_F4, 0x73 }, { SDL_SCANCODE_F5, 0x74 }, { SDL_SCANCODE_F6, 0x75 }, + { SDL_SCANCODE_F7, 0x76 }, { SDL_SCANCODE_F8, 0x77 }, { SDL_SCANCODE_F9, 0x78 }, + { SDL_SCANCODE_F10, 0x79 }, { SDL_SCANCODE_F11, 0x7A }, { SDL_SCANCODE_F12, 0x7B }, + { SDL_SCANCODE_SEMICOLON, 0xBA }, { SDL_SCANCODE_EQUALS, 0xBB }, + { SDL_SCANCODE_COMMA, 0xBC }, { SDL_SCANCODE_MINUS, 0xBD }, + { SDL_SCANCODE_PERIOD, 0xBE }, { SDL_SCANCODE_SLASH, 0xBF }, + { SDL_SCANCODE_GRAVE, 0xC0 }, { SDL_SCANCODE_LEFTBRACKET, 0xDB }, + { SDL_SCANCODE_BACKSLASH, 0xDC }, { SDL_SCANCODE_RIGHTBRACKET, 0xDD }, + { SDL_SCANCODE_APOSTROPHE, 0xDE }, +}; + + +int Win32_Virtual_Key(SDL_Scancode scancode, SDL_Keycode keycode) +{ + if (scancode >= SDL_SCANCODE_A && scancode <= SDL_SCANCODE_Z) { + return('A' + (scancode - SDL_SCANCODE_A)); + } + + if (scancode == SDL_SCANCODE_0) { + return('0'); + } + + if (scancode >= SDL_SCANCODE_1 && scancode <= SDL_SCANCODE_9) { + return('1' + (scancode - SDL_SCANCODE_1)); + } + + for (KeyMapping const & mapping : _Keys) { + if (mapping.Scancode == scancode) { + return(mapping.VirtualKey); + } + } + + if (keycode > 0 && keycode < 128) { + return(SDL_toupper((int)keycode)); + } + + return(0); +} + + +static SDL_Scancode Scancode_For_Virtual_Key(int key) +{ + if (key >= 'A' && key <= 'Z') { + return((SDL_Scancode)(SDL_SCANCODE_A + (key - 'A'))); + } + + if (key == '0') { + return(SDL_SCANCODE_0); + } + + if (key > '0' && key <= '9') { + return((SDL_Scancode)(SDL_SCANCODE_1 + (key - '1'))); + } + + for (KeyMapping const & mapping : _Keys) { + if (mapping.VirtualKey == key) { + return(mapping.Scancode); + } + } + + return(SDL_SCANCODE_UNKNOWN); +} + + +// The engine polls held keys through this rather than through the message queue, so it has +// to answer from the host's live keyboard state and not from anything this layer buffers. +extern "C" SHORT GetAsyncKeyState(int key) +{ + SDL_MouseButtonFlags const buttons = SDL_GetMouseState(NULL, NULL); + + switch (key) { + case VK_LBUTTON_CODE: return((buttons & SDL_BUTTON_LMASK) != 0 ? (SHORT)0x8000 : 0); + case VK_RBUTTON_CODE: return((buttons & SDL_BUTTON_RMASK) != 0 ? (SHORT)0x8000 : 0); + case VK_MBUTTON_CODE: return((buttons & SDL_BUTTON_MMASK) != 0 ? (SHORT)0x8000 : 0); + default: break; + } + + SDL_Scancode const scancode = Scancode_For_Virtual_Key(key); + + if (scancode == SDL_SCANCODE_UNKNOWN) { + return(0); + } + + int count = 0; + bool const * state = SDL_GetKeyboardState(&count); + + if (state == NULL || (int)scancode >= count) { + return(0); + } + + return(state[scancode] ? (SHORT)0x8000 : 0); +} + + +extern "C" SHORT GetKeyState(int key) +{ + SHORT const held = GetAsyncKeyState(key); + + // Only the toggling keys carry a low bit, and the host reports those as modifiers. + SDL_Keymod const modifiers = SDL_GetModState(); + + if (key == 0x14) { + return(held | ((modifiers & SDL_KMOD_CAPS) != 0 ? 1 : 0)); + } + + if (key == 0x90) { + return(held | ((modifiers & SDL_KMOD_NUM) != 0 ? 1 : 0)); + } + + return(held); +} + + +extern "C" UINT MapVirtualKey(UINT code, UINT type) +{ + switch (type) { + // A virtual key to a scan code, and back. + case 0: return((UINT)Scancode_For_Virtual_Key((int)code)); + case 1: return((UINT)Win32_Virtual_Key((SDL_Scancode)code, 0)); + + // A virtual key to the character it types unshifted. + case 2: { + SDL_Scancode const scancode = Scancode_For_Virtual_Key((int)code); + SDL_Keycode const keycode = SDL_GetKeyFromScancode(scancode, SDL_KMOD_NONE, false); + return(keycode < 128 ? (UINT)SDL_toupper((int)keycode) : 0); + } + + default: + return(0); + } +} + + +extern "C" int GetKeyNameText(LONG param, LPSTR name, int size) +{ + if (name == NULL || size <= 0) { + return(0); + } + + name[0] = '\0'; + + SDL_Scancode const scancode = (SDL_Scancode)((param >> 16) & 0xFF); + char const * text = SDL_GetScancodeName(scancode); + + if (text == NULL) { + return(0); + } + + SDL_strlcpy(name, text, (size_t)size); + return((int)strlen(name)); +} + + +extern "C" int ToUnicode(UINT key, UINT scan, BYTE const * state, LPWSTR buffer, int size, UINT flags) +{ + (void)scan; + (void)state; + (void)flags; + + if (buffer == NULL || size <= 0) { + return(0); + } + + SDL_Scancode const scancode = Scancode_For_Virtual_Key((int)key); + SDL_Keymod const modifiers = SDL_GetModState(); + SDL_Keycode const keycode = SDL_GetKeyFromScancode(scancode, modifiers, false); + + if (keycode == 0 || keycode > 0xFFFF) { + return(0); + } + + buffer[0] = (wchar_t)keycode; + return(1); +} + + +extern "C" BOOL GetCursorPos(LPPOINT point) +{ + if (point == NULL) { + return(FALSE); + } + + float x = 0.0f; + float y = 0.0f; + SDL_GetGlobalMouseState(&x, &y); + + float const density = Win32_Pixel_Density(); + point->x = (LONG)(x * density); + point->y = (LONG)(y * density); + return(TRUE); +} + + +extern "C" BOOL SetCursorPos(int x, int y) +{ + float const density = Win32_Pixel_Density(); + return(SDL_WarpMouseGlobal(x / density, y / density) ? TRUE : FALSE); +} + + +static bool _CursorShown = true; +static int _CursorCount; +static HWND _Capture; + + +// One record per cursor the game builds out of its own shape art. The game keeps the +// handles and reselects them as the pointer changes shape, so a record lives until the +// game destroys it. +struct Win32Cursor +{ + SDL_Cursor * Cursor; +}; + +static std::vector _Cursors; +static HCURSOR _CurrentCursor; + + +static Win32Cursor * Lookup_Cursor(HCURSOR cursor) +{ + for (Win32Cursor * record : _Cursors) { + if ((HCURSOR)record == cursor) { + return(record); + } + } + + return(NULL); +} + + +// The pointer the game selects and the counter this API keeps both decide whether anything +// is on screen, so they are applied together. +static void Apply_Cursor(void) +{ + Win32Cursor * record = Lookup_Cursor(_CurrentCursor); + + if (!_CursorShown || _CursorCount < 0) { + SDL_HideCursor(); + return; + } + + // While the mouse is released the game selects no shape of its own, and Windows would + // be drawing the window class's cursor, so the host's own pointer stands in for it. + SDL_SetCursor(record != NULL ? record->Cursor : SDL_GetDefaultCursor()); + SDL_ShowCursor(); +} + + +extern "C" HCURSOR SetCursor(HCURSOR cursor) +{ + HCURSOR const previous = _CurrentCursor; + + _CurrentCursor = Lookup_Cursor(cursor) != NULL ? cursor : NULL; + _CursorShown = _CurrentCursor != NULL; + + Apply_Cursor(); + return(previous); +} + + +extern "C" int ShowCursor(BOOL show) +{ + _CursorCount += show ? 1 : -1; + + if (show) { + _CursorShown = true; + } + + Apply_Cursor(); + return(_CursorCount); +} + + +// Confining the pointer is what the game does at the window's edge while scrolling. SDL +// confines to a window rather than to a desktop rectangle, so the request is honoured at +// window granularity and a rectangle smaller than the window is not. +extern "C" BOOL ClipCursor(RECT const * rect) +{ + Win32Window * main = Win32_Lookup(Win32_Main_Window()); + + if (main == NULL || main->Handle == NULL) { + return(FALSE); + } + + return(SDL_SetWindowMouseGrab(main->Handle, rect != NULL) ? TRUE : FALSE); +} + + +extern "C" HWND SetCapture(HWND window) +{ + HWND const previous = _Capture; + _Capture = window; + SDL_CaptureMouse(true); + return(previous); +} + + +extern "C" BOOL ReleaseCapture(void) +{ + _Capture = NULL; + SDL_CaptureMouse(false); + return(TRUE); +} + + +extern "C" HWND GetCapture(void) +{ + return(_Capture); +} + + +extern "C" HCURSOR LoadCursor(HINSTANCE instance, LPCSTR name) { (void)instance; (void)name; return(NULL); } +extern "C" HICON LoadIcon(HINSTANCE instance, LPCSTR name) { (void)instance; (void)name; return(NULL); } +extern "C" BOOL DestroyIcon(HICON icon) { (void)icon; return(TRUE); } + + +// A 32-bit device-independent bitmap holds its pixels as blue, green, red and alpha in +// memory order, which is what the host calls ARGB8888 on a little-endian machine. +static SDL_Surface * Surface_From_Bitmap(Win32Bitmap const * bitmap) +{ + SDL_Surface * surface = SDL_CreateSurface(bitmap->Width, bitmap->Height, SDL_PIXELFORMAT_ARGB8888); + + if (surface == NULL) { + return(NULL); + } + + for (int y = 0; y < bitmap->Height; y++) { + int const source = bitmap->TopDown ? y : bitmap->Height - 1 - y; + memcpy((unsigned char *)surface->pixels + (std::size_t)y * (std::size_t)surface->pitch, + bitmap->Bits + (std::size_t)source * (std::size_t)bitmap->Pitch, + (std::size_t)bitmap->Width * 4); + } + + return(surface); +} + + +/* + * The game draws its pointer at the scale its frame is presented at, which is measured in + * physical pixels, while the host lays a cursor out in the points its display uses. The + * image is offered at the point size that matches, with the pixels the game drew carried + * alongside it so a dense display still shows all of them. + */ +extern "C" HCURSOR CreateIconIndirect(ICONINFO * info) +{ + if (info == NULL) { + return(NULL); + } + + Win32Bitmap const * color = Win32_Lookup_Bitmap(info->hbmColor); + + if (color == NULL || color->BitCount != 32) { + return(NULL); + } + + SDL_Surface * pixels = Surface_From_Bitmap(color); + + if (pixels == NULL) { + return(NULL); + } + + float const density = Win32_Pixel_Density(); + SDL_Surface * image = pixels; + int hotx = (int)info->xHotspot; + int hoty = (int)info->yHotspot; + + if (density > 1.0f) { + int const width = (int)(pixels->w / density); + int const height = (int)(pixels->h / density); + SDL_Surface * scaled = width > 0 && height > 0 + ? SDL_ScaleSurface(pixels, width, height, SDL_SCALEMODE_NEAREST) : NULL; + + if (scaled != NULL && SDL_AddSurfaceAlternateImage(scaled, pixels)) { + image = scaled; + hotx = (int)(hotx / density); + hoty = (int)(hoty / density); + } else if (scaled != NULL) { + SDL_DestroySurface(scaled); + } + } + + if (hotx >= image->w) hotx = image->w - 1; + if (hoty >= image->h) hoty = image->h - 1; + if (hotx < 0) hotx = 0; + if (hoty < 0) hoty = 0; + + SDL_Cursor * cursor = SDL_CreateColorCursor(image, hotx, hoty); + + if (image != pixels) { + SDL_DestroySurface(image); + } + SDL_DestroySurface(pixels); + + if (cursor == NULL) { + return(NULL); + } + + Win32Cursor * record = new(std::nothrow) Win32Cursor; + + if (record == NULL) { + SDL_DestroyCursor(cursor); + return(NULL); + } + + record->Cursor = cursor; + _Cursors.push_back(record); + return((HCURSOR)record); +} + + +extern "C" BOOL DestroyCursor(HCURSOR cursor) +{ + for (auto it = _Cursors.begin(); it != _Cursors.end(); ++it) { + if ((HCURSOR)*it == cursor) { + if (_CurrentCursor == cursor) { + _CurrentCursor = NULL; + SDL_SetCursor(SDL_GetDefaultCursor()); + } + + SDL_DestroyCursor((*it)->Cursor); + delete *it; + _Cursors.erase(it); + return(TRUE); + } + } + + return(FALSE); +} diff --git a/platform/win32compat/src/kernel.cpp b/platform/win32compat/src/kernel.cpp new file mode 100644 index 000000000..50d375263 --- /dev/null +++ b/platform/win32compat/src/kernel.cpp @@ -0,0 +1,853 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#include "win32compat.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static DWORD _LastError; + + +extern "C" DWORD GetLastError(void) { return(_LastError); } +extern "C" void SetLastError(DWORD code) { _LastError = code; } + + +// The mutexes exist to keep a second copy of the game and the installer's autoplay from +// running at once. A process-local handle answers both callers correctly for a single run +// and refuses nothing, which is the behaviour a first native launch needs. +extern "C" HANDLE CreateMutex(LPSECURITY_ATTRIBUTES attributes, BOOL owner, LPCSTR name) +{ + (void)attributes; + (void)owner; + (void)name; + _LastError = ERROR_SUCCESS; + return((HANDLE)new std::recursive_mutex()); +} + + +extern "C" HANDLE OpenMutex(DWORD access, BOOL inherit, LPCSTR name) +{ + (void)access; + (void)inherit; + (void)name; + _LastError = ERROR_FILE_NOT_FOUND; + return(NULL); +} + + +extern "C" DWORD WaitForSingleObject(HANDLE object, DWORD milliseconds) +{ + (void)object; + (void)milliseconds; + return(WAIT_OBJECT_0); +} + + +extern "C" BOOL ReleaseMutex(HANDLE mutex) { (void)mutex; return(TRUE); } + + +extern "C" DWORD GetCurrentProcessId(void) { return((DWORD)getpid()); } +extern "C" DWORD GetCurrentThreadId(void) { return((DWORD)SDL_GetCurrentThreadID()); } +extern "C" BOOL IsDebuggerPresent(void) { return(FALSE); } +extern "C" void Sleep(DWORD milliseconds) { SDL_Delay(milliseconds); } + + +extern "C" void OutputDebugString(LPCSTR text) +{ + if (text != NULL) { + std::fputs(text, stderr); + } +} + + +// A module named without a path is looked for beside the executable, under the host's own +// library naming: the game asks for "Language.dll" and the build produces +// "libLanguage.dylib" in the same directory. +static std::string Host_Library_Name(char const * name) +{ + std::string stem(name != NULL ? name : ""); + std::string::size_type const dot = stem.find_last_of('.'); + + if (dot != std::string::npos) { + stem = stem.substr(0, dot); + } + +#ifdef __APPLE__ + return("lib" + stem + ".dylib"); +#else + return("lib" + stem + ".so"); +#endif +} + + +// A null name asks for the running program, which is the handle a caller compares against +// rather than one it loads anything from. +extern "C" HMODULE GetModuleHandle(LPCSTR name) +{ + if (name == NULL) { + return((HMODULE)(ULONG_PTR)1); + } + + return((HMODULE)dlopen(Host_Library_Name(name).c_str(), RTLD_LAZY | RTLD_NOLOAD)); +} + + +extern "C" HMODULE LoadLibrary(LPCSTR name) +{ + if (name == NULL) { + return(NULL); + } + + std::string const library = Host_Library_Name(name); + + char executable[MAX_PATH]; + if (GetModuleFileName(NULL, executable, sizeof(executable)) != 0) { + std::filesystem::path beside(executable); + beside.replace_filename(library); + + if (void * handle = dlopen(beside.c_str(), RTLD_LAZY)) { + return((HMODULE)handle); + } + } + + return((HMODULE)dlopen(library.c_str(), RTLD_LAZY)); +} + + +extern "C" BOOL FreeLibrary(HMODULE module) +{ + if (module == NULL || module == (HMODULE)(ULONG_PTR)1) { + return(TRUE); + } + + return(dlclose((void *)module) == 0 ? TRUE : FALSE); +} + + +extern "C" FARPROC GetProcAddress(HMODULE module, LPCSTR name) +{ + if (module == NULL || module == (HMODULE)(ULONG_PTR)1 || name == NULL) { + return(NULL); + } + + return((FARPROC)dlsym((void *)module, name)); +} + + +extern "C" DWORD GetModuleFileName(HMODULE module, LPSTR name, DWORD size) +{ + (void)module; + + if (name == NULL || size == 0) { + return(0); + } + + name[0] = '\0'; + + uint32_t length = size; + if (_NSGetExecutablePath(name, &length) != 0) { + return(0); + } + + return((DWORD)strlen(name)); +} + + +// The engine asks the shell for its command line and then splits it. The real argument +// vector is captured at entry, so the wide line handed back is built from that and split +// back into the same arguments rather than re-parsed by a quoting rule this host lacks. +static std::vector _Arguments; +static std::wstring _CommandLine; + +void Win32_Record_Arguments(int argc, char ** argv) +{ + _Arguments.clear(); + _CommandLine.clear(); + + for (int index = 0; index < argc; index++) { + std::string const argument(argv[index] != NULL ? argv[index] : ""); + _Arguments.push_back(std::wstring(argument.begin(), argument.end())); + + if (index > 0) { + _CommandLine += L' '; + } + _CommandLine += _Arguments.back(); + } +} + + +extern "C" LPWSTR GetCommandLineW(void) +{ + return(const_cast(_CommandLine.c_str())); +} + + +extern "C" LPWSTR * CommandLineToArgvW(LPCWSTR commandline, int * count) +{ + (void)commandline; + + static std::vector pointers; + pointers.clear(); + + for (std::wstring & argument : _Arguments) { + pointers.push_back(const_cast(argument.c_str())); + } + + if (count != NULL) { + *count = (int)pointers.size(); + } + + return(pointers.empty() ? NULL : pointers.data()); +} + + +extern "C" HLOCAL LocalFree(HLOCAL memory) { (void)memory; return(NULL); } + + +// The host has no legacy single-byte code page, so both conversions are UTF-8 only. A +// character with no UTF-8 spelling does not exist, so nothing is lost in that direction; +// the other direction is what the engine's own substitute-glyph path already covers. +extern "C" UINT GetACP(void) { return(CP_UTF8); } +extern "C" UINT GetOEMCP(void) { return(CP_UTF8); } + + +extern "C" int MultiByteToWideChar(UINT codepage, DWORD flags, LPCSTR source, int sourcelength, + LPWSTR dest, int destlength) +{ + (void)codepage; + (void)flags; + + if (source == NULL) { + return(0); + } + + size_t const length = sourcelength < 0 ? strlen(source) + 1 : (size_t)sourcelength; + + if (dest == NULL || destlength == 0) { + return((int)length); + } + + size_t const copied = length < (size_t)destlength ? length : (size_t)destlength; + for (size_t index = 0; index < copied; index++) { + dest[index] = (wchar_t)(unsigned char)source[index]; + } + + return((int)copied); +} + + +extern "C" int WideCharToMultiByte(UINT codepage, DWORD flags, LPCWSTR source, int sourcelength, + LPSTR dest, int destlength, LPCSTR defaultchar, LPBOOL useddefault) +{ + (void)codepage; + (void)flags; + (void)defaultchar; + + if (useddefault != NULL) { + *useddefault = FALSE; + } + + if (source == NULL) { + return(0); + } + + size_t length = 0; + if (sourcelength < 0) { + while (source[length] != 0) length++; + length++; + } else { + length = (size_t)sourcelength; + } + + if (dest == NULL || destlength == 0) { + return((int)length); + } + + size_t const copied = length < (size_t)destlength ? length : (size_t)destlength; + for (size_t index = 0; index < copied; index++) { + dest[index] = source[index] < 128 ? (char)source[index] : '?'; + } + + return((int)copied); +} + + +static FILETIME File_Time_From_Unix(time_t seconds) +{ + // The Windows epoch is 1601, and the count is in hundred-nanosecond units. + unsigned long long const ticks = (unsigned long long)seconds * 10000000ULL + 116444736000000000ULL; + FILETIME time; + time.dwLowDateTime = (DWORD)(ticks & 0xFFFFFFFFULL); + time.dwHighDateTime = (DWORD)(ticks >> 32); + return(time); +} + + +static unsigned long long Ticks_From_File_Time(FILETIME const & time) +{ + return(((unsigned long long)time.dwHighDateTime << 32) | (unsigned long long)time.dwLowDateTime); +} + + +extern "C" void GetSystemTime(LPSYSTEMTIME system) +{ + if (system == NULL) { + return; + } + + timeval now; + gettimeofday(&now, NULL); + + tm parts; + gmtime_r(&now.tv_sec, &parts); + + system->wYear = (WORD)(parts.tm_year + 1900); + system->wMonth = (WORD)(parts.tm_mon + 1); + system->wDayOfWeek = (WORD)parts.tm_wday; + system->wDay = (WORD)parts.tm_mday; + system->wHour = (WORD)parts.tm_hour; + system->wMinute = (WORD)parts.tm_min; + system->wSecond = (WORD)parts.tm_sec; + system->wMilliseconds = (WORD)(now.tv_usec / 1000); +} + + +extern "C" void GetLocalTime(LPSYSTEMTIME system) +{ + if (system == NULL) { + return; + } + + timeval now; + gettimeofday(&now, NULL); + + tm parts; + localtime_r(&now.tv_sec, &parts); + + system->wYear = (WORD)(parts.tm_year + 1900); + system->wMonth = (WORD)(parts.tm_mon + 1); + system->wDayOfWeek = (WORD)parts.tm_wday; + system->wDay = (WORD)parts.tm_mday; + system->wHour = (WORD)parts.tm_hour; + system->wMinute = (WORD)parts.tm_min; + system->wSecond = (WORD)parts.tm_sec; + system->wMilliseconds = (WORD)(now.tv_usec / 1000); +} + + +extern "C" BOOL SystemTimeToFileTime(SYSTEMTIME const * system, LPFILETIME file) +{ + if (system == NULL || file == NULL) { + return(FALSE); + } + + tm parts = {}; + parts.tm_year = system->wYear - 1900; + parts.tm_mon = system->wMonth - 1; + parts.tm_mday = system->wDay; + parts.tm_hour = system->wHour; + parts.tm_min = system->wMinute; + parts.tm_sec = system->wSecond; + + *file = File_Time_From_Unix(timegm(&parts)); + return(TRUE); +} + + +extern "C" BOOL FileTimeToSystemTime(FILETIME const * file, LPSYSTEMTIME system) +{ + if (file == NULL || system == NULL) { + return(FALSE); + } + + time_t const seconds = (time_t)((Ticks_From_File_Time(*file) - 116444736000000000ULL) / 10000000ULL); + + tm parts; + gmtime_r(&seconds, &parts); + + system->wYear = (WORD)(parts.tm_year + 1900); + system->wMonth = (WORD)(parts.tm_mon + 1); + system->wDayOfWeek = (WORD)parts.tm_wday; + system->wDay = (WORD)parts.tm_mday; + system->wHour = (WORD)parts.tm_hour; + system->wMinute = (WORD)parts.tm_min; + system->wSecond = (WORD)parts.tm_sec; + system->wMilliseconds = 0; + return(TRUE); +} + + +extern "C" BOOL FileTimeToLocalFileTime(FILETIME const * file, LPFILETIME local) +{ + if (file == NULL || local == NULL) { + return(FALSE); + } + + *local = *file; + return(TRUE); +} + + +extern "C" LONG CompareFileTime(FILETIME const * a, FILETIME const * b) +{ + if (a == NULL || b == NULL) { + return(0); + } + + unsigned long long const left = Ticks_From_File_Time(*a); + unsigned long long const right = Ticks_From_File_Time(*b); + return(left < right ? -1 : (left > right ? 1 : 0)); +} + + +extern "C" void _ftime(struct _timeb * time) +{ + if (time == NULL) { + return; + } + + timeval now; + gettimeofday(&now, NULL); + time->time = (long)now.tv_sec; + time->millitm = (unsigned short)(now.tv_usec / 1000); + time->timezone = 0; + time->dstflag = 0; +} + + +extern "C" int _getch(void) +{ + return(std::getchar()); +} + + +extern "C" int GetTimeFormat(DWORD locale, DWORD flags, SYSTEMTIME const * time, LPCSTR format, + LPSTR buffer, int size) +{ + (void)locale; + (void)format; + + if (buffer == NULL || size <= 0 || time == NULL) { + return(0); + } + + if ((flags & TIME_NOMINUTESORSECONDS) != 0) { + snprintf(buffer, (size_t)size, "%02u", time->wHour); + } else if ((flags & TIME_NOSECONDS) != 0) { + snprintf(buffer, (size_t)size, "%02u:%02u", time->wHour, time->wMinute); + } else { + snprintf(buffer, (size_t)size, "%02u:%02u:%02u", time->wHour, time->wMinute, time->wSecond); + } + + return((int)strlen(buffer) + 1); +} + + +extern "C" int GetDateFormat(DWORD locale, DWORD flags, SYSTEMTIME const * time, LPCSTR format, + LPSTR buffer, int size) +{ + (void)locale; + (void)flags; + (void)format; + + if (buffer == NULL || size <= 0 || time == NULL) { + return(0); + } + + snprintf(buffer, (size_t)size, "%04u-%02u-%02u", time->wYear, time->wMonth, time->wDay); + return((int)strlen(buffer) + 1); +} + + +extern "C" DWORD FormatMessage(DWORD flags, LPCVOID source, DWORD id, DWORD language, + LPSTR buffer, DWORD size, void * arguments) +{ + (void)flags; + (void)source; + (void)language; + (void)arguments; + + if (buffer == NULL || size == 0) { + return(0); + } + + snprintf(buffer, size, "%s", strerror((int)id)); + return((DWORD)strlen(buffer)); +} + + +// +// --------------------------------------------------------- +// Files +// --------------------------------------------------------- +// +extern "C" HANDLE CreateFileA(LPCSTR name, DWORD access, DWORD share, LPSECURITY_ATTRIBUTES attributes, + DWORD disposition, DWORD flags, HANDLE templatefile) +{ + (void)share; + (void)attributes; + (void)flags; + (void)templatefile; + + if (name == NULL) { + return(INVALID_HANDLE_VALUE); + } + + char const * mode = "rb"; + if ((access & GENERIC_WRITE) != 0) { + mode = disposition == OPEN_EXISTING ? "r+b" : "w+b"; + } + + std::FILE * file = std::fopen(name, mode); + + if (file == NULL) { + _LastError = (DWORD)errno; + return(INVALID_HANDLE_VALUE); + } + + return((HANDLE)file); +} + + +extern "C" BOOL ReadFile(HANDLE handle, LPVOID buffer, DWORD size, LPDWORD read, LPOVERLAPPED overlapped) +{ + (void)overlapped; + + if (handle == INVALID_HANDLE_VALUE || handle == NULL) { + return(FALSE); + } + + size_t const got = std::fread(buffer, 1, size, (std::FILE *)handle); + + if (read != NULL) { + *read = (DWORD)got; + } + + return(TRUE); +} + + +extern "C" BOOL WriteFile(HANDLE handle, LPCVOID buffer, DWORD size, LPDWORD written, LPOVERLAPPED overlapped) +{ + (void)overlapped; + + if (handle == INVALID_HANDLE_VALUE || handle == NULL) { + return(FALSE); + } + + size_t const put = std::fwrite(buffer, 1, size, (std::FILE *)handle); + + /* + * A Windows file handle is not buffered, so a log written through this call + * survives a crash. Match that rather than leaving the last records of a run + * in a standard library buffer that is never drained. + */ + std::fflush((std::FILE *)handle); + + if (written != NULL) { + *written = (DWORD)put; + } + + return(TRUE); +} + + +extern "C" DWORD SetFilePointer(HANDLE handle, LONG distance, LONG * distancehigh, DWORD method) +{ + (void)distancehigh; + + if (handle == INVALID_HANDLE_VALUE || handle == NULL) { + return(INVALID_SET_FILE_POINTER); + } + + int const origin = method == FILE_BEGIN ? SEEK_SET : (method == FILE_END ? SEEK_END : SEEK_CUR); + + if (std::fseek((std::FILE *)handle, distance, origin) != 0) { + return(INVALID_SET_FILE_POINTER); + } + + return((DWORD)std::ftell((std::FILE *)handle)); +} + + +extern "C" BOOL CloseHandle(HANDLE handle) +{ + if (handle == NULL || handle == INVALID_HANDLE_VALUE) { + return(FALSE); + } + + std::fclose((std::FILE *)handle); + return(TRUE); +} + + +extern "C" BOOL DeleteFileA(LPCSTR name) +{ + return(name != NULL && std::remove(name) == 0 ? TRUE : FALSE); +} + + +extern "C" BOOL CopyFile(LPCSTR from, LPCSTR to, BOOL failifexists) +{ + if (from == NULL || to == NULL) { + return(FALSE); + } + + std::error_code error; + auto const options = failifexists + ? std::filesystem::copy_options::none + : std::filesystem::copy_options::overwrite_existing; + std::filesystem::copy_file(from, to, options, error); + return(error ? FALSE : TRUE); +} + + +extern "C" BOOL CreateDirectory(LPCSTR path, LPSECURITY_ATTRIBUTES attributes) +{ + (void)attributes; + + if (path == NULL) { + return(FALSE); + } + + std::error_code error; + + if (std::filesystem::create_directory(path, error)) { + _LastError = ERROR_SUCCESS; + return(TRUE); + } + + // A caller distinguishes "it is already there" from a real failure through the last + // error rather than through the result, so the two cases must not look alike. + _LastError = std::filesystem::is_directory(path, error) ? ERROR_ALREADY_EXISTS : ERROR_FILE_NOT_FOUND; + return(FALSE); +} + + +extern "C" BOOL SetCurrentDirectory(LPCSTR path) +{ + return(path != NULL && chdir(path) == 0 ? TRUE : FALSE); +} + + +extern "C" DWORD GetFileAttributesA(LPCSTR name) +{ + struct stat status; + + if (name == NULL || stat(name, &status) != 0) { + return(INVALID_FILE_ATTRIBUTES); + } + + DWORD attributes = FILE_ATTRIBUTE_NORMAL; + if (S_ISDIR(status.st_mode)) attributes = FILE_ATTRIBUTE_DIRECTORY; + if ((status.st_mode & S_IWUSR) == 0) attributes |= FILE_ATTRIBUTE_READONLY; + return(attributes); +} + + +// Directory enumeration keeps the shape the callers expect: one handle that walks a +// directory and matches each entry against the pattern the caller supplied. +struct Win32Find +{ + DIR * Directory; + std::string Path; + std::string Pattern; +}; + + +static bool Fill_Find_Data(Win32Find * find, LPWIN32_FIND_DATA data) +{ + dirent * entry = NULL; + + while ((entry = readdir(find->Directory)) != NULL) { + if (fnmatch(find->Pattern.c_str(), entry->d_name, FNM_CASEFOLD) != 0) { + continue; + } + + std::string const full = find->Path + "/" + entry->d_name; + + struct stat status; + if (stat(full.c_str(), &status) != 0) { + continue; + } + + memset(data, 0, sizeof(*data)); + data->dwFileAttributes = S_ISDIR(status.st_mode) ? FILE_ATTRIBUTE_DIRECTORY : FILE_ATTRIBUTE_NORMAL; + data->nFileSizeLow = (DWORD)status.st_size; + data->nFileSizeHigh = (DWORD)((unsigned long long)status.st_size >> 32); + data->ftLastWriteTime = File_Time_From_Unix(status.st_mtime); + data->ftCreationTime = data->ftLastWriteTime; + data->ftLastAccessTime = data->ftLastWriteTime; + strncpy(data->cFileName, entry->d_name, sizeof(data->cFileName) - 1); + return(true); + } + + return(false); +} + + +extern "C" HANDLE FindFirstFile(LPCSTR name, LPWIN32_FIND_DATA data) +{ + if (name == NULL || data == NULL) { + return(INVALID_HANDLE_VALUE); + } + + std::string full(name); + std::string::size_type const slash = full.find_last_of("/\\"); + std::string const directory = slash == std::string::npos ? std::string(".") : full.substr(0, slash); + std::string const pattern = slash == std::string::npos ? full : full.substr(slash + 1); + + DIR * handle = opendir(directory.c_str()); + + if (handle == NULL) { + _LastError = (DWORD)errno; + return(INVALID_HANDLE_VALUE); + } + + Win32Find * find = new Win32Find(); + find->Directory = handle; + find->Path = directory; + find->Pattern = pattern; + + if (!Fill_Find_Data(find, data)) { + closedir(handle); + delete find; + return(INVALID_HANDLE_VALUE); + } + + return((HANDLE)find); +} + + +extern "C" BOOL FindNextFile(HANDLE handle, LPWIN32_FIND_DATA data) +{ + if (handle == INVALID_HANDLE_VALUE || handle == NULL || data == NULL) { + return(FALSE); + } + + return(Fill_Find_Data((Win32Find *)handle, data) ? TRUE : FALSE); +} + + +extern "C" BOOL FindClose(HANDLE handle) +{ + if (handle == INVALID_HANDLE_VALUE || handle == NULL) { + return(FALSE); + } + + Win32Find * find = (Win32Find *)handle; + closedir(find->Directory); + delete find; + return(TRUE); +} + + +// +// --------------------------------------------------------- +// Console, locks and the services with no host equivalent +// --------------------------------------------------------- +// +extern "C" BOOL AllocConsole(void) { return(FALSE); } +extern "C" HWND GetConsoleWindow(void) { return(NULL); } +extern "C" HANDLE GetStdHandle(DWORD which) +{ + switch (which) { + case STD_INPUT_HANDLE: return((HANDLE)stdin); + case STD_OUTPUT_HANDLE: return((HANDLE)stdout); + default: return((HANDLE)stderr); + } +} +extern "C" BOOL SetStdHandle(DWORD which, HANDLE handle) { (void)which; (void)handle; return(TRUE); } +extern "C" BOOL SetConsoleTitle(LPCSTR title) { (void)title; return(TRUE); } +extern "C" BOOL SetConsoleCP(UINT codepage) { (void)codepage; return(TRUE); } +extern "C" BOOL SetConsoleOutputCP(UINT codepage) { (void)codepage; return(TRUE); } +extern "C" BOOL SetConsoleScreenBufferSize(HANDLE console, COORD size) { (void)console; (void)size; return(TRUE); } +extern "C" BOOL GetConsoleScreenBufferInfo(HANDLE console, CONSOLE_SCREEN_BUFFER_INFO * info) +{ + (void)console; + (void)info; + return(FALSE); +} + + +extern "C" BOOL WriteConsole(HANDLE console, void const * buffer, DWORD length, LPDWORD written, LPVOID reserved) +{ + (void)reserved; + + std::FILE * stream = console == (HANDLE)stdout ? stdout : stderr; + size_t const put = std::fwrite(buffer, 1, length, stream); + + if (written != NULL) { + *written = (DWORD)put; + } + + return(TRUE); +} + + +// The debug log's lock is a plain mutex; nothing in the tree takes it for reading only. +extern "C" void InitializeSRWLock(SRWLOCK * lock) +{ + if (lock != NULL) { + lock->Ptr = new std::recursive_mutex(); + } +} + + +extern "C" void AcquireSRWLockExclusive(SRWLOCK * lock) +{ + if (lock == NULL) { + return; + } + + if (lock->Ptr == NULL) { + InitializeSRWLock(lock); + } + + ((std::recursive_mutex *)lock->Ptr)->lock(); +} + + +extern "C" void ReleaseSRWLockExclusive(SRWLOCK * lock) +{ + if (lock != NULL && lock->Ptr != NULL) { + ((std::recursive_mutex *)lock->Ptr)->unlock(); + } +} + + +// There is no version resource and no PE image to read outside Windows. Reporting nothing +// is what the callers already handle; inventing a value would be worse. +extern "C" DWORD GetFileVersionInfoSize(LPCSTR name, LPDWORD handle) { (void)name; (void)handle; return(0); } +extern "C" BOOL GetFileVersionInfo(LPCSTR name, DWORD handle, DWORD length, LPVOID data) { (void)name; (void)handle; (void)length; (void)data; return(FALSE); } +extern "C" BOOL VerQueryValue(LPCVOID block, LPCSTR path, LPVOID * buffer, UINT * length) { (void)block; (void)path; (void)buffer; (void)length; return(FALSE); } +extern "C" HRSRC FindResource(HMODULE module, LPCSTR name, LPCSTR type) { (void)module; (void)name; (void)type; return(NULL); } +extern "C" HGLOBAL LoadResource(HMODULE module, HRSRC resource) { (void)module; (void)resource; return(NULL); } +extern "C" LPVOID LockResource(HGLOBAL resource) { (void)resource; return(NULL); } +extern "C" DWORD SizeofResource(HMODULE module, HRSRC resource) { (void)module; (void)resource; return(0); } +extern "C" LONG RegOpenKeyEx(HKEY key, LPCSTR subkey, DWORD options, DWORD access, HKEY * result) { (void)key; (void)subkey; (void)options; (void)access; if (result != NULL) *result = NULL; return(1); } +extern "C" LONG RegQueryValueEx(HKEY key, LPCSTR name, LPDWORD reserved, LPDWORD type, LPBYTE data, LPDWORD size) { (void)key; (void)name; (void)reserved; (void)type; (void)data; (void)size; return(1); } +extern "C" LONG RegCloseKey(HKEY key) { (void)key; return(0); } diff --git a/platform/win32compat/src/message.cpp b/platform/win32compat/src/message.cpp new file mode 100644 index 000000000..7265e7ac2 --- /dev/null +++ b/platform/win32compat/src/message.cpp @@ -0,0 +1,411 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#include "win32compat.h" + +#include +#include + +// The engine speaks Win32 messages everywhere: the window procedure, the keyboard queue, +// the scroll handler and the tooltip timer all switch on WM_ values, and so do the dialog +// drivers. Translating host events into those messages leaves every one of those call +// sites as it is on Windows, and leaves both builds dispatching the same vocabulary. + +static std::deque _Queue; +static bool _Quitting; +static int _QuitCode; + +struct Win32Timer +{ + HWND Window; + UINT_PTR Id; + UINT Interval; + Uint64 Due; + TIMERPROC Procedure; +}; + +static std::vector _Timers; + + +void Win32_Post_Message(HWND window, UINT message, WPARAM wparam, LPARAM lparam) +{ + MSG msg = {}; + msg.hwnd = window; + msg.message = message; + msg.wParam = wparam; + msg.lParam = lparam; + msg.time = (DWORD)SDL_GetTicks(); + _Queue.push_back(msg); +} + + +// The host reports mouse positions in its own logical coordinates; the engine measures its +// client area in physical pixels, so a position crosses the density before it is packed +// into the message the way Windows packs it. +static LPARAM Point_To_LParam(float x, float y) +{ + float const density = Win32_Pixel_Density(); + int const px = (int)(x * density); + int const py = (int)(y * density); + return(MAKELPARAM((short)px, (short)py)); +} + + +static WPARAM Mouse_Key_State(void) +{ + SDL_MouseButtonFlags const buttons = SDL_GetMouseState(NULL, NULL); + SDL_Keymod const modifiers = SDL_GetModState(); + + WPARAM state = 0; + if ((buttons & SDL_BUTTON_LMASK) != 0) state |= MK_LBUTTON; + if ((buttons & SDL_BUTTON_RMASK) != 0) state |= MK_RBUTTON; + if ((buttons & SDL_BUTTON_MMASK) != 0) state |= MK_MBUTTON; + if ((modifiers & SDL_KMOD_SHIFT) != 0) state |= MK_SHIFT; + if ((modifiers & SDL_KMOD_CTRL) != 0) state |= MK_CONTROL; + return(state); +} + + +extern int Win32_Virtual_Key(SDL_Scancode scancode, SDL_Keycode keycode); + + +static void Translate_Event(SDL_Event const & event) +{ + HWND const main = Win32_Main_Window(); + + if (main == NULL) { + return; + } + + switch (event.type) { + case SDL_EVENT_QUIT: + Win32_Post_Message(main, WM_CLOSE, 0, 0); + break; + + case SDL_EVENT_WINDOW_CLOSE_REQUESTED: + Win32_Post_Message(main, WM_CLOSE, 0, 0); + break; + + case SDL_EVENT_WINDOW_FOCUS_GAINED: + Win32_Post_Message(main, WM_ACTIVATEAPP, 1, 0); + Win32_Post_Message(main, WM_SETFOCUS, 0, 0); + break; + + case SDL_EVENT_WINDOW_FOCUS_LOST: + Win32_Post_Message(main, WM_ACTIVATEAPP, 0, 0); + Win32_Post_Message(main, WM_KILLFOCUS, 0, 0); + break; + + case SDL_EVENT_WINDOW_SHOWN: + Win32_Post_Message(main, WM_SHOWWINDOW, 1, 0); + break; + + case SDL_EVENT_WINDOW_HIDDEN: + Win32_Post_Message(main, WM_SHOWWINDOW, 0, 0); + break; + + case SDL_EVENT_WINDOW_MINIMIZED: + Win32_Post_Message(main, WM_SIZE, SIZE_MINIMIZED, 0); + break; + + case SDL_EVENT_WINDOW_RESTORED: + Win32_Post_Message(main, WM_SIZE, SIZE_RESTORED, 0); + break; + + case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: + Win32_Post_Message(main, WM_SIZE, SIZE_RESTORED, + MAKELPARAM((short)event.window.data1, (short)event.window.data2)); + break; + + case SDL_EVENT_WINDOW_MOVED: + Win32_Post_Message(main, WM_MOVE, 0, MAKELPARAM((short)event.window.data1, (short)event.window.data2)); + break; + + case SDL_EVENT_WINDOW_EXPOSED: + Win32_Post_Message(main, WM_PAINT, 0, 0); + break; + + case SDL_EVENT_MOUSE_MOTION: + Win32_Post_Message(main, WM_MOUSEMOVE, Mouse_Key_State(), + Point_To_LParam(event.motion.x, event.motion.y)); + break; + + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: { + bool const down = event.type == SDL_EVENT_MOUSE_BUTTON_DOWN; + bool const doubled = down && event.button.clicks >= 2; + UINT message = 0; + + switch (event.button.button) { + case SDL_BUTTON_LEFT: + message = down ? (doubled ? WM_LBUTTONDBLCLK : WM_LBUTTONDOWN) : WM_LBUTTONUP; + break; + case SDL_BUTTON_RIGHT: + message = down ? (doubled ? WM_RBUTTONDBLCLK : WM_RBUTTONDOWN) : WM_RBUTTONUP; + break; + case SDL_BUTTON_MIDDLE: + message = down ? (doubled ? WM_MBUTTONDBLCLK : WM_MBUTTONDOWN) : WM_MBUTTONUP; + break; + default: + return; + } + + Win32_Post_Message(main, message, Mouse_Key_State(), Point_To_LParam(event.button.x, event.button.y)); + break; + } + + case SDL_EVENT_MOUSE_WHEEL: { + // Windows reports the wheel in notch multiples in the high word, and the + // position in screen rather than client coordinates. + int const notches = (int)(event.wheel.y * 120.0f); + float mx = 0.0f; + float my = 0.0f; + SDL_GetGlobalMouseState(&mx, &my); + Win32_Post_Message(main, WM_MOUSEWHEEL, + MAKEWPARAM((WORD)Mouse_Key_State(), (WORD)(short)notches), Point_To_LParam(mx, my)); + break; + } + + case SDL_EVENT_KEY_DOWN: + case SDL_EVENT_KEY_UP: { + int const key = Win32_Virtual_Key(event.key.scancode, event.key.key); + if (key == 0) { + return; + } + + bool const down = event.type == SDL_EVENT_KEY_DOWN; + bool const system = (event.key.mod & SDL_KMOD_ALT) != 0; + UINT const message = down + ? (system ? WM_SYSKEYDOWN : WM_KEYDOWN) + : (system ? WM_SYSKEYUP : WM_KEYUP); + + // The repeat count, the scan code and the transition bit occupy the same + // places in the parameter that Windows puts them in. + LPARAM lparam = 1; + lparam |= (LPARAM)(event.key.scancode & 0xFF) << 16; + if (!down) lparam |= (LPARAM)3 << 30; + + Win32_Post_Message(main, message, (WPARAM)key, lparam); + break; + } + + case SDL_EVENT_TEXT_INPUT: { + for (char const * cursor = event.text.text; cursor != NULL && *cursor != '\0'; cursor++) { + Win32_Post_Message(main, WM_CHAR, (WPARAM)(unsigned char)*cursor, 1); + } + break; + } + + default: + break; + } +} + + +static void Service_Timers(void) +{ + Uint64 const now = SDL_GetTicks(); + + for (Win32Timer & timer : _Timers) { + if (now >= timer.Due) { + timer.Due = now + timer.Interval; + Win32_Post_Message(timer.Window, WM_TIMER, (WPARAM)timer.Id, (LPARAM)timer.Procedure); + } + } +} + + +void Win32_Pump_Host_Events(void) +{ + if (SDL_WasInit(SDL_INIT_VIDEO) == 0) { + return; + } + + SDL_Event event; + while (SDL_PollEvent(&event)) { + Translate_Event(event); + } + + Service_Timers(); +} + + +static bool Matches_Filter(MSG const & msg, HWND window, UINT filtermin, UINT filtermax) +{ + if (window != NULL && msg.hwnd != window) { + return(false); + } + + if (filtermin == 0 && filtermax == 0) { + return(true); + } + + return(msg.message >= filtermin && msg.message <= filtermax); +} + + +extern "C" BOOL PeekMessage(LPMSG msg, HWND window, UINT filtermin, UINT filtermax, UINT remove) +{ + Win32_Pump_Host_Events(); + + for (auto it = _Queue.begin(); it != _Queue.end(); ++it) { + if (!Matches_Filter(*it, window, filtermin, filtermax)) { + continue; + } + + if (msg != NULL) { + *msg = *it; + } + + if ((remove & PM_REMOVE) != 0) { + _Queue.erase(it); + } + + return(TRUE); + } + + return(FALSE); +} + + +// The engine drives its own frame, so a wait for a message must never block: every caller +// reaches this from inside a loop that also has drawing and simulation to do. +extern "C" BOOL GetMessage(LPMSG msg, HWND window, UINT filtermin, UINT filtermax) +{ + if (!PeekMessage(msg, window, filtermin, filtermax, PM_REMOVE)) { + return(FALSE); + } + + return(msg != NULL && msg->message == WM_QUIT ? FALSE : TRUE); +} + + +extern "C" BOOL TranslateMessage(MSG const * msg) +{ + // Character messages arrive from the host's own text input, so a key message needs no + // second pass to produce one. + (void)msg; + return(FALSE); +} + + +extern "C" LRESULT DispatchMessage(MSG const * msg) +{ + if (msg == NULL) { + return(0); + } + + Win32Window * window = Win32_Lookup(msg->hwnd); + + if (window == NULL || window->Procedure == NULL) { + return(0); + } + + return(window->Procedure(msg->hwnd, msg->message, msg->wParam, msg->lParam)); +} + + +extern "C" BOOL PostMessage(HWND window, UINT message, WPARAM wparam, LPARAM lparam) +{ + Win32_Post_Message(window, message, wparam, lparam); + return(TRUE); +} + + +extern "C" LRESULT SendMessage(HWND window, UINT message, WPARAM wparam, LPARAM lparam) +{ + Win32Window * record = Win32_Lookup(window); + + if (record == NULL || record->Procedure == NULL) { + return(0); + } + + return(record->Procedure(window, message, wparam, lparam)); +} + + +extern "C" void PostQuitMessage(int code) +{ + _Quitting = true; + _QuitCode = code; + Win32_Post_Message(Win32_Main_Window(), WM_QUIT, (WPARAM)code, 0); +} + + +extern "C" LRESULT DefWindowProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) +{ + (void)window; + (void)wparam; + (void)lparam; + + switch (message) { + case WM_NCHITTEST: + return(HTCLIENT); + + case WM_SETCURSOR: + return(TRUE); + + default: + return(0); + } +} + + +extern "C" LRESULT CallWindowProc(WNDPROC proc, HWND window, UINT message, WPARAM wparam, LPARAM lparam) +{ + if (proc == NULL) { + return(DefWindowProc(window, message, wparam, lparam)); + } + + return(proc(window, message, wparam, lparam)); +} + + +extern "C" int TranslateAccelerator(HWND window, HACCEL table, LPMSG msg) +{ + (void)window; + (void)table; + (void)msg; + return(0); +} + + +extern "C" UINT_PTR SetTimer(HWND window, UINT_PTR id, UINT elapse, TIMERPROC proc) +{ + for (Win32Timer & timer : _Timers) { + if (timer.Window == window && timer.Id == id) { + timer.Interval = elapse; + timer.Due = SDL_GetTicks() + elapse; + timer.Procedure = proc; + return(id); + } + } + + Win32Timer timer; + timer.Window = window; + timer.Id = id; + timer.Interval = elapse; + timer.Due = SDL_GetTicks() + elapse; + timer.Procedure = proc; + _Timers.push_back(timer); + return(id); +} + + +extern "C" BOOL KillTimer(HWND window, UINT_PTR id) +{ + for (auto it = _Timers.begin(); it != _Timers.end(); ++it) { + if (it->Window == window && it->Id == id) { + _Timers.erase(it); + return(TRUE); + } + } + + return(FALSE); +} diff --git a/platform/win32compat/src/strings.cpp b/platform/win32compat/src/strings.cpp new file mode 100644 index 000000000..e482acb2c --- /dev/null +++ b/platform/win32compat/src/strings.cpp @@ -0,0 +1,204 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#include "win32compat.h" + +#include +#include +#include +#include +#include +#include +#include + +// Every string a player reads comes through Fetch_String, which asks LoadString for a +// resource compiled from language.rc. Only the resource compiler builds that resource, so +// on a host without one the same strings arrive as a flat data file generated from the same +// script at build time by cmake/StringTable.cmake. The file is UTF-8, because the resource +// script declares code page 65001 and the generator copies its bytes through unchanged. +// +// The format is length-prefixed so that a string containing a newline needs no escaping: +// +// OPENTS-STRINGS 1\n +// \n +// \n\n (repeated times) + +namespace +{ + +char const * const STRING_TABLE_FILE = "Language.dat"; +char const * const STRING_TABLE_MAGIC = "OPENTS-STRINGS 1"; + +typedef std::unordered_map StringMap; + + +// The table sits beside the executable, which is where the language library it replaces is +// looked for as well. A run from elsewhere still finds it through the working directory. +std::vector Table_Candidates(void) +{ + std::vector candidates; + + char executable[MAX_PATH]; + if (GetModuleFileName(NULL, executable, sizeof(executable)) != 0) { + std::filesystem::path beside(executable); + beside.replace_filename(STRING_TABLE_FILE); + candidates.push_back(beside); + } + + candidates.push_back(std::filesystem::path(STRING_TABLE_FILE)); + return(candidates); +} + + +bool Read_Whole_File(std::filesystem::path const & path, std::string & content) +{ + std::FILE * file = std::fopen(path.c_str(), "rb"); + if (file == NULL) { + return(false); + } + + content.clear(); + + char buffer[8192]; + std::size_t got; + while ((got = std::fread(buffer, 1, sizeof(buffer), file)) > 0) { + content.append(buffer, got); + } + + bool const ok = (std::ferror(file) == 0); + std::fclose(file); + return(ok); +} + + +// Reads one line and leaves the cursor past its terminator. A record's text is never read +// this way, since only its declared length says where it ends. +bool Next_Line(std::string const & content, std::size_t & cursor, std::string & line) +{ + if (cursor >= content.size()) { + return(false); + } + + std::size_t const end = content.find('\n', cursor); + if (end == std::string::npos) { + return(false); + } + + line.assign(content, cursor, end - cursor); + cursor = end + 1; + return(true); +} + + +void Load_String_Table(StringMap & strings) +{ + std::string content; + bool found = false; + for (std::filesystem::path const & candidate : Table_Candidates()) { + if (Read_Whole_File(candidate, content)) { + found = true; + break; + } + } + + if (!found) { + return; + } + + std::size_t cursor = 0; + std::string line; + + if (!Next_Line(content, cursor, line) || line != STRING_TABLE_MAGIC) { + return; + } + + if (!Next_Line(content, cursor, line)) { + return; + } + + long const count = std::strtol(line.c_str(), NULL, 10); + + for (long record = 0; record < count; record++) { + if (!Next_Line(content, cursor, line)) { + break; + } + + char * after = NULL; + unsigned long const id = std::strtoul(line.c_str(), &after, 10); + if (after == line.c_str()) { + break; + } + + long const length = std::strtol(after, NULL, 10); + if (length < 0 || cursor + (std::size_t)length > content.size()) { + break; + } + + strings[(unsigned int)id] = content.substr(cursor, (std::size_t)length); + cursor += (std::size_t)length + 1; + } +} + + +// A global constructor in the engine asks for a string before this translation unit's own +// statics would have been built, so the table is built on the first request. It is never torn +// down either, because a string can just as easily be asked for from a static destructor. +StringMap const & Table(void) +{ + static StringMap * strings = NULL; + if (strings == NULL) { + strings = new StringMap(); + Load_String_Table(*strings); + } + + return(*strings); +} + + +// A buffer too small to hold the whole string truncates it, as the Windows call does. The +// text is UTF-8, so the cut is pulled back off a continuation byte rather than left to +// split a code point. +std::size_t Whole_Code_Points(char const * text, std::size_t length) +{ + while (length > 0 && ((unsigned char)text[length] & 0xC0) == 0x80) { + length--; + } + + return(length); +} + +} + + +extern "C" int LoadString(HINSTANCE instance, UINT id, LPSTR buffer, int max) +{ + (void)instance; + + if (buffer == NULL || max <= 0) { + return(0); + } + + buffer[0] = '\0'; + + StringMap const & strings = Table(); + + StringMap::const_iterator const entry = strings.find(id); + if (entry == strings.end()) { + return(0); + } + + std::size_t length = entry->second.size(); + if (length > (std::size_t)(max - 1)) { + length = Whole_Code_Points(entry->second.c_str(), (std::size_t)(max - 1)); + } + + std::memcpy(buffer, entry->second.c_str(), length); + buffer[length] = '\0'; + return((int)length); +} diff --git a/platform/win32compat/src/win32compat.h b/platform/win32compat/src/win32compat.h new file mode 100644 index 000000000..7d0bab01c --- /dev/null +++ b/platform/win32compat/src/win32compat.h @@ -0,0 +1,60 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#pragma once + +#include + +#include + +// One window record per HWND the engine asks for. Only the main window is backed by an +// SDL window; everything the legacy dialog layer would have created is refused, so a +// record without a Handle never exists. +struct Win32Window +{ + SDL_Window * Handle; + WNDPROC Procedure; + char ClassName[64]; + char Title[128]; + DWORD Style; + DWORD ExStyle; + bool Visible; + bool Enabled; +}; + +// The one bitmap a native build creates is the canvas the game draws a mouse cursor onto, +// so a bitmap object carries only what a cursor is built from. +struct Win32Bitmap +{ + int Width; + int Height; + int BitCount; + int Pitch; + bool TopDown; + unsigned char * Bits; +}; + +Win32Bitmap * Win32_Lookup_Bitmap(HBITMAP bitmap); + +Win32Window * Win32_Lookup(HWND window); +HWND Win32_Main_Window(void); +WNDPROC Win32_Class_Procedure(char const * name); + +// Drains the host's event queue into the engine's message queue. Every entry point that +// waits for a message calls this, so the engine keeps its own pump and its own frame pace. +void Win32_Pump_Host_Events(void); +void Win32_Post_Message(HWND window, UINT message, WPARAM wparam, LPARAM lparam); + +// Converts between the host's coordinates and the physical client pixels the engine +// measures its frame in. The two agree on a display whose pixel density is one. +float Win32_Pixel_Density(void); + +// The layer bgfx presents into, which SDL owns and this layer only hands over. +extern "C" void * Win32Compat_Native_Window_Handle(HWND window); +extern "C" int Win32Compat_Window_Refresh_Rate(HWND window); diff --git a/platform/win32compat/src/window.cpp b/platform/win32compat/src/window.cpp new file mode 100644 index 000000000..9eba77731 --- /dev/null +++ b/platform/win32compat/src/window.cpp @@ -0,0 +1,883 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +#include "win32compat.h" + +#include + +#include +#include +#include +#include +#include + +static std::vector _Windows; +static std::unordered_map _Classes; +static HWND _MainWindow; +static SDL_MetalView _MetalView; + + +Win32Window * Win32_Lookup(HWND window) +{ + if (window == NULL) { + return(NULL); + } + + for (Win32Window * candidate : _Windows) { + if ((HWND)candidate == window) { + return(candidate); + } + } + + return(NULL); +} + + +HWND Win32_Main_Window(void) +{ + return(_MainWindow); +} + + +WNDPROC Win32_Class_Procedure(char const * name) +{ + if (name == NULL) { + return(NULL); + } + + auto found = _Classes.find(name); + return(found == _Classes.end() ? NULL : found->second); +} + + +// Every position and size this layer reports is in physical pixels, because that is what +// the engine measures its frame and its client area in. The host works in logical points, +// so one density converts between them. It is read from the display rather than from the +// window so that it answers the same before and after the window exists. +float Win32_Pixel_Density(void) +{ + SDL_DisplayID display = SDL_GetPrimaryDisplay(); + Win32Window * main = Win32_Lookup(_MainWindow); + + if (main != NULL && main->Handle != NULL) { + display = SDL_GetDisplayForWindow(main->Handle); + } + + SDL_DisplayMode const * mode = SDL_GetDesktopDisplayMode(display); + + if (mode == NULL || mode->pixel_density <= 0.0f) { + return(1.0f); + } + + return(mode->pixel_density); +} + + +extern "C" ATOM RegisterClass(WNDCLASS const * cls) +{ + if (cls == NULL || cls->lpszClassName == NULL) { + return(0); + } + + _Classes[cls->lpszClassName] = cls->lpfnWndProc; + return(1); +} + + +extern "C" HWND CreateWindowEx(DWORD exstyle, LPCSTR classname, LPCSTR windowname, DWORD style, + int x, int y, int width, int height, HWND parent, HMENU menu, HINSTANCE instance, LPVOID param) +{ + (void)parent; + (void)menu; + (void)instance; + (void)param; + + if (!SDL_InitSubSystem(SDL_INIT_VIDEO)) { + return(NULL); + } + + Win32Window * window = new Win32Window(); + window->Procedure = Win32_Class_Procedure(classname); + window->Style = style; + window->ExStyle = exstyle; + window->Enabled = true; + SDL_strlcpy(window->ClassName, classname != NULL ? classname : "", sizeof(window->ClassName)); + SDL_strlcpy(window->Title, windowname != NULL ? windowname : "", sizeof(window->Title)); + + // A zero size is what the windowed path asks for before it measures the frame it wants, + // so the window opens at a size SDL accepts and is moved to the real one afterwards. + float const density = Win32_Pixel_Density(); + int const openwidth = width > 0 ? (int)(width / density) : 640; + int const openheight = height > 0 ? (int)(height / density) : 480; + + SDL_WindowFlags flags = SDL_WINDOW_METAL | SDL_WINDOW_HIDDEN | SDL_WINDOW_HIGH_PIXEL_DENSITY; + if ((style & WS_POPUP) != 0) { + flags |= SDL_WINDOW_BORDERLESS; + } + + window->Handle = SDL_CreateWindow(window->Title, openwidth, openheight, flags); + + if (window->Handle == NULL) { + delete window; + return(NULL); + } + + _Windows.push_back(window); + + if (_MainWindow == NULL) { + _MainWindow = (HWND)window; + } + + if (window->Procedure != NULL) { + window->Procedure((HWND)window, WM_CREATE, 0, 0); + } + + return((HWND)window); +} + + +extern "C" BOOL DestroyWindow(HWND handle) +{ + Win32Window * window = Win32_Lookup(handle); + + if (window == NULL) { + return(FALSE); + } + + if (window->Procedure != NULL) { + window->Procedure(handle, WM_DESTROY, 0, 0); + } + + if (window->Handle != NULL) { + SDL_DestroyWindow(window->Handle); + } + + for (auto it = _Windows.begin(); it != _Windows.end(); ++it) { + if (*it == window) { + _Windows.erase(it); + break; + } + } + + if (_MainWindow == handle) { + _MainWindow = NULL; + } + + delete window; + return(TRUE); +} + + +extern "C" BOOL ShowWindow(HWND handle, int command) +{ + Win32Window * window = Win32_Lookup(handle); + + if (window == NULL || window->Handle == NULL) { + return(FALSE); + } + + bool const previous = window->Visible; + + switch (command) { + case SW_HIDE: + SDL_HideWindow(window->Handle); + window->Visible = false; + break; + + case SW_MINIMIZE: + case SW_SHOWMINIMIZED: + SDL_MinimizeWindow(window->Handle); + break; + + default: + SDL_ShowWindow(window->Handle); + SDL_RaiseWindow(window->Handle); + window->Visible = true; + break; + } + + return(previous ? TRUE : FALSE); +} + + +extern "C" BOOL ShowWindowAsync(HWND handle, int command) +{ + return(ShowWindow(handle, command)); +} + + +extern "C" BOOL UpdateWindow(HWND handle) +{ + Win32Window * window = Win32_Lookup(handle); + + if (window == NULL) { + return(FALSE); + } + + if (window->Procedure != NULL) { + window->Procedure(handle, WM_PAINT, 0, 0); + } + + return(TRUE); +} + + +extern "C" BOOL MoveWindow(HWND handle, int x, int y, int width, int height, BOOL repaint) +{ + Win32Window * window = Win32_Lookup(handle); + + if (window == NULL || window->Handle == NULL) { + return(FALSE); + } + + float const density = Win32_Pixel_Density(); + SDL_SetWindowPosition(window->Handle, (int)(x / density), (int)(y / density)); + SDL_SetWindowSize(window->Handle, (int)(width / density), (int)(height / density)); + + if (repaint) { + UpdateWindow(handle); + } + + return(TRUE); +} + + +extern "C" BOOL SetWindowPos(HWND handle, HWND after, int x, int y, int cx, int cy, UINT flags) +{ + (void)after; + + Win32Window * window = Win32_Lookup(handle); + + if (window == NULL || window->Handle == NULL) { + return(FALSE); + } + + float const density = Win32_Pixel_Density(); + + if ((flags & SWP_NOMOVE) == 0) { + SDL_SetWindowPosition(window->Handle, (int)(x / density), (int)(y / density)); + } + + if ((flags & SWP_NOSIZE) == 0) { + SDL_SetWindowSize(window->Handle, (int)(cx / density), (int)(cy / density)); + } + + return(TRUE); +} + + +// The frame is measured in physical pixels because the engine scales it to the drawable +// area itself, so the client rectangle reports pixels and every position the layer +// reports elsewhere is converted to match. +extern "C" BOOL GetClientRect(HWND handle, LPRECT rect) +{ + if (rect == NULL) { + return(FALSE); + } + + rect->left = 0; + rect->top = 0; + rect->right = 0; + rect->bottom = 0; + + Win32Window * window = Win32_Lookup(handle); + + if (window == NULL || window->Handle == NULL) { + return(FALSE); + } + + int width = 0; + int height = 0; + if (!SDL_GetWindowSizeInPixels(window->Handle, &width, &height)) { + return(FALSE); + } + + rect->right = width; + rect->bottom = height; + return(TRUE); +} + + +extern "C" BOOL GetWindowRect(HWND handle, LPRECT rect) +{ + if (rect == NULL) { + return(FALSE); + } + + rect->left = 0; + rect->top = 0; + rect->right = 0; + rect->bottom = 0; + + Win32Window * window = Win32_Lookup(handle); + + if (window == NULL || window->Handle == NULL) { + return(FALSE); + } + + int x = 0; + int y = 0; + int width = 0; + int height = 0; + SDL_GetWindowPosition(window->Handle, &x, &y); + SDL_GetWindowSizeInPixels(window->Handle, &width, &height); + + float const density = Win32_Pixel_Density(); + rect->left = (LONG)(x * density); + rect->top = (LONG)(y * density); + rect->right = rect->left + width; + rect->bottom = rect->top + height; + return(TRUE); +} + + +extern "C" BOOL ClientToScreen(HWND handle, LPPOINT point) +{ + if (point == NULL) { + return(FALSE); + } + + RECT rect; + if (!GetWindowRect(handle, &rect)) { + return(FALSE); + } + + point->x += rect.left; + point->y += rect.top; + return(TRUE); +} + + +extern "C" BOOL ScreenToClient(HWND handle, LPPOINT point) +{ + if (point == NULL) { + return(FALSE); + } + + RECT rect; + if (!GetWindowRect(handle, &rect)) { + return(FALSE); + } + + point->x -= rect.left; + point->y -= rect.top; + return(TRUE); +} + + +extern "C" int MapWindowPoints(HWND from, HWND to, LPPOINT points, UINT count) +{ + for (UINT index = 0; index < count; index++) { + if (from != NULL) ClientToScreen(from, &points[index]); + if (to != NULL) ScreenToClient(to, &points[index]); + } + + return(0); +} + + +// The window is borderless or resizable but never has a client area smaller than the +// frame, so the adjustment the engine asks for is the identity. +extern "C" BOOL AdjustWindowRectEx(LPRECT rect, DWORD style, BOOL menu, DWORD exstyle) +{ + (void)style; + (void)menu; + (void)exstyle; + return(rect != NULL); +} + + +extern "C" LONG_PTR GetWindowLongPtr(HWND handle, int index) +{ + Win32Window * window = Win32_Lookup(handle); + + if (window == NULL) { + return(0); + } + + switch (index) { + case GWL_STYLE: return((LONG_PTR)window->Style); + case GWL_EXSTYLE: return((LONG_PTR)window->ExStyle); + case GWLP_WNDPROC: return((LONG_PTR)window->Procedure); + default: return(0); + } +} + + +extern "C" LONG_PTR SetWindowLongPtr(HWND handle, int index, LONG_PTR value) +{ + Win32Window * window = Win32_Lookup(handle); + + if (window == NULL) { + return(0); + } + + LONG_PTR const previous = GetWindowLongPtr(handle, index); + + switch (index) { + case GWL_STYLE: + window->Style = (DWORD)value; + // Windows leaves the frame alone until a SWP_FRAMECHANGED asks for it. The host + // has no such second step, so the border follows the style as it is set. + if (window->Handle != NULL) { + SDL_SetWindowBordered(window->Handle, (window->Style & WS_POPUP) == 0); + } + break; + case GWL_EXSTYLE: window->ExStyle = (DWORD)value; break; + case GWLP_WNDPROC: window->Procedure = (WNDPROC)value; break; + default: break; + } + + return(previous); +} + + +extern "C" LONG_PTR GetWindowLong(HWND handle, int index) +{ + return(GetWindowLongPtr(handle, index)); +} + + +extern "C" LONG_PTR SetWindowLong(HWND handle, int index, LONG_PTR value) +{ + return(SetWindowLongPtr(handle, index, value)); +} + + +extern "C" BOOL SetWindowText(HWND handle, LPCSTR text) +{ + Win32Window * window = Win32_Lookup(handle); + + if (window == NULL || text == NULL) { + return(FALSE); + } + + SDL_strlcpy(window->Title, text, sizeof(window->Title)); + + if (window->Handle != NULL) { + SDL_SetWindowTitle(window->Handle, window->Title); + } + + return(TRUE); +} + + +extern "C" int GetWindowText(HWND handle, LPSTR text, int max) +{ + if (text == NULL || max <= 0) { + return(0); + } + + text[0] = '\0'; + + Win32Window * window = Win32_Lookup(handle); + + if (window == NULL) { + return(0); + } + + SDL_strlcpy(text, window->Title, (size_t)max); + return((int)strlen(text)); +} + + +extern "C" int GetWindowTextLength(HWND handle) +{ + Win32Window * window = Win32_Lookup(handle); + return(window == NULL ? 0 : (int)strlen(window->Title)); +} + + +extern "C" int GetClassName(HWND handle, LPSTR name, int max) +{ + if (name == NULL || max <= 0) { + return(0); + } + + name[0] = '\0'; + + Win32Window * window = Win32_Lookup(handle); + + if (window == NULL) { + return(0); + } + + SDL_strlcpy(name, window->ClassName, (size_t)max); + return((int)strlen(name)); +} + + +extern "C" BOOL IsWindow(HWND handle) +{ + return(Win32_Lookup(handle) != NULL); +} + + +extern "C" BOOL IsWindowVisible(HWND handle) +{ + Win32Window * window = Win32_Lookup(handle); + return(window != NULL && window->Visible); +} + + +extern "C" BOOL IsWindowEnabled(HWND handle) +{ + Win32Window * window = Win32_Lookup(handle); + return(window != NULL && window->Enabled); +} + + +extern "C" BOOL IsChild(HWND parent, HWND child) +{ + (void)parent; + (void)child; + return(FALSE); +} + + +extern "C" BOOL EnableWindow(HWND handle, BOOL enable) +{ + Win32Window * window = Win32_Lookup(handle); + + if (window == NULL) { + return(FALSE); + } + + BOOL const previous = window->Enabled ? FALSE : TRUE; + window->Enabled = enable != FALSE; + return(previous); +} + + +extern "C" HWND GetParent(HWND handle) { (void)handle; return(NULL); } +extern "C" HWND GetWindow(HWND handle, UINT command) { (void)handle; (void)command; return(NULL); } +extern "C" HWND GetTopWindow(HWND handle) { (void)handle; return(NULL); } +extern "C" HWND GetDesktopWindow(void) { return(NULL); } +extern "C" HWND GetActiveWindow(void) { return(_MainWindow); } +extern "C" HWND SetActiveWindow(HWND handle) { (void)handle; return(_MainWindow); } +extern "C" HWND GetFocus(void) { return(_MainWindow); } +extern "C" HWND SetFocus(HWND handle) { (void)handle; return(_MainWindow); } +extern "C" HWND WindowFromPoint(POINT point) { (void)point; return(_MainWindow); } +extern "C" HWND ChildWindowFromPoint(HWND parent, POINT point) { (void)parent; (void)point; return(NULL); } +extern "C" BOOL EnumChildWindows(HWND parent, WNDENUMPROC proc, LPARAM param) { (void)parent; (void)proc; (void)param; return(TRUE); } +extern "C" HMENU GetMenu(HWND handle) { (void)handle; return(NULL); } +extern "C" HMENU GetSystemMenu(HWND handle, BOOL revert) { (void)handle; (void)revert; return(NULL); } +extern "C" BOOL EnableMenuItem(HMENU menu, UINT item, UINT enable) { (void)menu; (void)item; (void)enable; return(FALSE); } +extern "C" BOOL DeleteMenu(HMENU menu, UINT position, UINT flags) { (void)menu; (void)position; (void)flags; return(FALSE); } +extern "C" BOOL RegisterHotKey(HWND handle, int id, UINT modifiers, UINT key) { (void)handle; (void)id; (void)modifiers; (void)key; return(FALSE); } +extern "C" int GetWindowContextHelpId(HWND handle) { (void)handle; return(0); } +extern "C" BOOL WinHelp(HWND handle, LPCSTR help, UINT command, ULONG_PTR data) { (void)handle; (void)help; (void)command; (void)data; return(FALSE); } + + +extern "C" BOOL SetForegroundWindow(HWND handle) +{ + Win32Window * window = Win32_Lookup(handle); + + if (window == NULL || window->Handle == NULL) { + return(FALSE); + } + + SDL_RaiseWindow(window->Handle); + return(TRUE); +} + + +extern "C" BOOL BringWindowToTop(HWND handle) +{ + return(SetForegroundWindow(handle)); +} + + +extern "C" HWND FindWindow(LPCSTR classname, LPCSTR windowname) +{ + (void)windowname; + + for (Win32Window * candidate : _Windows) { + if (classname == NULL || strcmp(candidate->ClassName, classname) == 0) { + return((HWND)candidate); + } + } + + return(NULL); +} + + +extern "C" BOOL CloseWindow(HWND handle) +{ + return(ShowWindow(handle, SW_MINIMIZE)); +} + + +// The frame is presented every time it changes rather than in answer to a paint request, +// so an invalidation has nothing to record and an update rectangle is always empty. +extern "C" BOOL InvalidateRect(HWND handle, RECT const * rect, BOOL erase) { (void)handle; (void)rect; (void)erase; return(TRUE); } +extern "C" BOOL ValidateRect(HWND handle, RECT const * rect) { (void)handle; (void)rect; return(TRUE); } +extern "C" BOOL RedrawWindow(HWND handle, RECT const * rect, HRGN region, UINT flags) { (void)handle; (void)rect; (void)region; (void)flags; return(TRUE); } + + +extern "C" BOOL GetUpdateRect(HWND handle, LPRECT rect, BOOL erase) +{ + (void)handle; + (void)erase; + + if (rect != NULL) { + rect->left = rect->top = rect->right = rect->bottom = 0; + } + + return(FALSE); +} + + +extern "C" BOOL SetRect(LPRECT rect, int left, int top, int right, int bottom) +{ + if (rect == NULL) { + return(FALSE); + } + + rect->left = left; + rect->top = top; + rect->right = right; + rect->bottom = bottom; + return(TRUE); +} + + +extern "C" BOOL IntersectRect(LPRECT dest, RECT const * a, RECT const * b) +{ + if (dest == NULL || a == NULL || b == NULL) { + return(FALSE); + } + + dest->left = a->left > b->left ? a->left : b->left; + dest->top = a->top > b->top ? a->top : b->top; + dest->right = a->right < b->right ? a->right : b->right; + dest->bottom = a->bottom < b->bottom ? a->bottom : b->bottom; + + if (dest->right <= dest->left || dest->bottom <= dest->top) { + dest->left = dest->top = dest->right = dest->bottom = 0; + return(FALSE); + } + + return(TRUE); +} + + +extern "C" BOOL PtInRect(RECT const * rect, POINT point) +{ + if (rect == NULL) { + return(FALSE); + } + + return(point.x >= rect->left && point.x < rect->right && point.y >= rect->top && point.y < rect->bottom); +} + + +extern "C" int GetSystemMetrics(int index) +{ + SDL_DisplayID const display = SDL_GetPrimaryDisplay(); + SDL_DisplayMode const * mode = SDL_GetDesktopDisplayMode(display); + float const density = Win32_Pixel_Density(); + + switch (index) { + case SM_CXSCREEN: + case SM_CXFULLSCREEN: + return(mode != NULL ? (int)(mode->w * density) : 640); + + case SM_CYSCREEN: + case SM_CYFULLSCREEN: + return(mode != NULL ? (int)(mode->h * density) : 480); + + case SM_CXBORDER: + case SM_CYBORDER: + return(1); + + case SM_CXDRAG: + case SM_CYDRAG: + return(4); + + case SM_SWAPBUTTON: + return(0); + + default: + return(0); + } +} + + +extern "C" HMONITOR MonitorFromWindow(HWND handle, DWORD flags) +{ + (void)handle; + (void)flags; + return((HMONITOR)(ULONG_PTR)SDL_GetPrimaryDisplay()); +} + + +extern "C" BOOL GetMonitorInfo(HMONITOR monitor, LPMONITORINFO info) +{ + (void)monitor; + + if (info == NULL) { + return(FALSE); + } + + SetRect(&info->rcMonitor, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN)); + info->rcWork = info->rcMonitor; + info->dwFlags = 0; + return(TRUE); +} + + +// The host states a display mode in logical points and reports the pixel density beside +// it, which is the unit the engine sizes its frame in on this platform; the density belongs +// to the presentation and the shell's scale information already carries it. A mode is +// therefore reported at the size the caller would ask the display for, not at its pixel +// count, which is what EnumDisplaySettings means on the platform this shim stands in for. +extern "C" BOOL EnumDisplaySettings(LPCSTR device, DWORD mode, DEVMODE * settings) +{ + (void)device; + + if (settings == NULL) { + return(FALSE); + } + + SDL_DisplayID const display = SDL_GetPrimaryDisplay(); + SDL_DisplayMode const * found = NULL; + SDL_DisplayMode ** modes = NULL; + + // ENUM_CURRENT_SETTINGS asks for the mode in force rather than for one of the list. + if (mode == (DWORD)-1 || mode == (DWORD)-2) { + found = SDL_GetDesktopDisplayMode(display); + } else { + int count = 0; + modes = SDL_GetFullscreenDisplayModes(display, &count); + + if (modes != NULL && (int)mode < count) { + found = modes[mode]; + } + } + + if (found == NULL) { + SDL_free(modes); + return(FALSE); + } + + SDL_PixelFormatDetails const * const format = SDL_GetPixelFormatDetails(found->format); + + std::memset(settings, 0, sizeof(*settings)); + settings->dmSize = (WORD)sizeof(*settings); + settings->dmPelsWidth = (DWORD)found->w; + settings->dmPelsHeight = (DWORD)found->h; + settings->dmBitsPerPel = (format != NULL) ? (DWORD)format->bits_per_pixel : 32; + settings->dmDisplayFrequency = (DWORD)(found->refresh_rate + 0.5f); + + SDL_free(modes); + return(TRUE); +} + + +// A message box has no native equivalent that can be shown from inside the engine's own +// loop without a second event source, so the text is reported where a headless run sees +// it and the caller is told the default button was chosen. +extern "C" int MessageBox(HWND handle, LPCSTR text, LPCSTR caption, UINT type) +{ + (void)handle; + + // Written with the C runtime rather than through the host's logger, which drops a + // message that is not valid UTF-8, and several of these carry legacy code page bytes. + std::fprintf(stderr, "%s: %s\n", caption != NULL ? caption : "OpenTS", text != NULL ? text : ""); + + if ((type & MB_YESNO) == MB_YESNO) { + return(IDYES); + } + + return(IDOK); +} + + +extern "C" int MessageBoxIndirect(MSGBOXPARAMS const * params) +{ + if (params == NULL) { + return(IDOK); + } + + return(MessageBox(params->hwndOwner, params->lpszText, params->lpszCaption, params->dwStyle)); +} + + +extern "C" void * Win32Compat_Native_Window_Handle(HWND handle) +{ + Win32Window * window = Win32_Lookup(handle); + + if (window == NULL || window->Handle == NULL) { + return(NULL); + } + + if (_MetalView == NULL) { + _MetalView = SDL_Metal_CreateView(window->Handle); + } + + if (_MetalView == NULL) { + return(NULL); + } + + return(SDL_Metal_GetLayer(_MetalView)); +} + + +extern "C" int Win32Compat_Window_Refresh_Rate(HWND handle) +{ + Win32Window * window = Win32_Lookup(handle); + + if (window == NULL || window->Handle == NULL) { + return(0); + } + + SDL_DisplayMode const * mode = SDL_GetCurrentDisplayMode(SDL_GetDisplayForWindow(window->Handle)); + return(mode != NULL ? (int)(mode->refresh_rate + 0.5f) : 0); +} + + +// The engine expresses a full screen presentation as a borderless window covering the +// desktop, which is what that amounts to on the platform this shim stands in for. Here it +// has to be asked for, or the host's own furniture stays on top of the window. The +// desktop's own mode is kept and the frame is scaled into it, which is what the engine +// already does with the display mode it renders at. +extern "C" BOOL Win32Compat_Set_Window_Fullscreen(HWND handle, BOOL fullscreen) +{ + Win32Window * window = Win32_Lookup(handle); + + if (window == NULL || window->Handle == NULL) { + return(FALSE); + } + + if (!SDL_SetWindowFullscreenMode(window->Handle, NULL)) { + return(FALSE); + } + + if (!SDL_SetWindowFullscreen(window->Handle, fullscreen != FALSE)) { + return(FALSE); + } + + // The size the window reports is read back straight away by the caller, so the change + // has to have landed rather than be waiting in the event queue. + SDL_SyncWindow(window->Handle); + return(TRUE); +} + + +extern "C" BOOL Win32Compat_Window_Is_Fullscreen(HWND handle) +{ + Win32Window * window = Win32_Lookup(handle); + + if (window == NULL || window->Handle == NULL) { + return(FALSE); + } + + return((SDL_GetWindowFlags(window->Handle) & SDL_WINDOW_FULLSCREEN) != 0 ? TRUE : FALSE); +} diff --git a/scripts/get-assets.sh b/scripts/get-assets.sh new file mode 100755 index 000000000..caf645e81 --- /dev/null +++ b/scripts/get-assets.sh @@ -0,0 +1,121 @@ +#!/bin/bash +# Download Tiberian Sun + Firestorm game data from your own Steam account into Run/. +# +# OpenTS supplies the engine, not the game data. This fetches the data files from +# a copy of the game you already own and places them where the engine looks for +# them, which the manual describes under "Game data". +# +# Usage: ./scripts/get-assets.sh +# Steam Guard: you will be prompted for the code on first login. +set -euo pipefail + +if [[ $# -lt 1 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +STEAM_USER="$1" +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DEST="${OPENTS_GAME_DIR:-$REPO_ROOT/Run}" +TMP_DIR="$REPO_ROOT/build/.steamcmd_ts" + +# App 2229880 = "Command & Conquer Tiberian Sun and Firestorm". The depot is +# Windows-only, which does not matter: the archives this copies are data, and the +# Windows executables are excluded below because OpenTS replaces them. +STEAM_APP_ID=2229880 + +if ! command -v steamcmd >/dev/null 2>&1; then + echo "Error: steamcmd is not installed." >&2 + echo " macOS: brew install --cask steamcmd" >&2 + echo " Linux: install the steamcmd package for your distribution" >&2 + exit 1 +fi + +mkdir -p "$TMP_DIR" "$DEST" + +# macOS Gatekeeper quarantines steamcmd's unnotarized bundled frameworks when +# Homebrew installs the cask. On first run that pops a blocking "Apple could not +# verify ... malware" dialog which stops steamcmd dead. Clear the flag up front. +if [[ "$(uname)" == "Darwin" ]] && command -v brew >/dev/null 2>&1; then + STEAMCMD_CASK="$(brew --prefix)/Caskroom/steamcmd" + [[ -d "$STEAMCMD_CASK" ]] && xattr -dr com.apple.quarantine "$STEAMCMD_CASK" 2>/dev/null || true +fi + +# steamcmd and the Steam desktop client share one data directory. A running +# client holds a single-instance lock, so steamcmd stalls forever right after +# "Verifying installation..." with no error at all. Fail fast and say why. +# +# Match the client binary itself, not anything living under the Steam bundle. +# Quitting Steam leaves helpers such as ipcserver and steamwebhelper running for +# a while, and they hold no lock — a broader pattern reports the client as +# running when it is not, and no amount of quitting Steam clears it. +if pgrep -x steam_osx >/dev/null 2>&1 \ + || pgrep -x steam >/dev/null 2>&1; then + echo "Error: the Steam desktop client is running." >&2 + echo "steamcmd shares Steam's data directory, and the running client locks it —" >&2 + echo 'steamcmd would hang forever after "Verifying installation...".' >&2 + echo "Quit Steam completely (Steam > Quit Steam, or Cmd-Q), then re-run this script." >&2 + exit 1 +fi + +echo "==> Downloading app $STEAM_APP_ID from Steam as '$STEAM_USER'" +steamcmd \ + +@sSteamCmdForcePlatformType windows \ + +force_install_dir "$TMP_DIR" \ + +login "$STEAM_USER" \ + +app_update "$STEAM_APP_ID" validate \ + +quit + +echo "==> Copying game data into $DEST" +# Data only. OpenTS replaces the game's own executables, and copying them in +# would leave two things called Game in the same directory. +rsync -a \ + --exclude="*.exe" --exclude="*.dll" --exclude="*.pdb" \ + --exclude="_CommonRedist/" --exclude="installscript.vdf" \ + "$TMP_DIR/" "$DEST/" + +# Only check archives that must exist as their own files. CACHE.MIX, LOCAL.MIX, +# CONQUER.MIX and SOUNDS.MIX are NOT among them: Init_Bootstrap_Mixfiles mounts +# TIBSUN.MIX first and then opens them through it, so in this release they live +# inside TIBSUN.MIX rather than beside it. Checking for them as loose files +# reports a complete download as broken. +echo "==> Verifying the archives startup requires" +missing=0 +for archive in TIBSUN.MIX SCORES.MIX; do + if ! find "$DEST" -maxdepth 2 -iname "$archive" -print -quit | grep -q .; then + echo " MISSING: $archive" >&2 + missing=1 + else + echo " found: $archive" + fi +done + +# A movie archive is required, but its name varies by release and by disc. +if find "$DEST" -maxdepth 2 -iname "MOVIES*.MIX" -print -quit | grep -q .; then + echo " found: $(find "$DEST" -maxdepth 2 -iname 'MOVIES*.MIX' -exec basename {} \; | tr '\n' ' ')" +else + echo " MISSING: a MOVIES*.MIX archive" >&2 + missing=1 +fi + +# Firestorm's speech archive is required only where Firestorm is installed, +# which FIRESTRM.INI identifies. +if find "$DEST" -maxdepth 2 -iname "FIRESTRM.INI" -print -quit | grep -q .; then + if find "$DEST" -maxdepth 2 -iname "SOUNDS01.MIX" -print -quit | grep -q .; then + echo " found: SOUNDS01.MIX (Firestorm)" + else + echo " MISSING: SOUNDS01.MIX, and FIRESTRM.INI says Firestorm is installed" >&2 + missing=1 + fi +fi + +if [[ $missing -ne 0 ]]; then + echo >&2 + echo "Error: the download completed but the archives the engine needs are not present." >&2 + echo "Check that the Steam account owns Tiberian Sun and that app $STEAM_APP_ID installed cleanly." >&2 + exit 1 +fi + +echo +echo "Done. Game data is in $DEST" +echo "Run the engine with: $DEST/Game" diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a7d3ef23a..89e98bbb3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -96,5 +96,5 @@ add_subdirectory(deploymentconfig) add_subdirectory(tutorial) add_subdirectory(utf8) add_subdirectory(shapefacing) -add_subdirectory(cstream) add_subdirectory(zbufring) +add_subdirectory(save) diff --git a/tests/cstream/CMakeLists.txt b/tests/cstream/CMakeLists.txt deleted file mode 100644 index c996e004c..000000000 --- a/tests/cstream/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -opents_add_test(CStreamContract - NAME cstream - SOURCES cstreamcontract.cpp - ENGINE - cstream.cpp - isun_i.c - DEFINITIONS WIN32 _WINDOWS _MBCS NOMINMAX - LIBRARIES lzo - FLOAT -) diff --git a/tests/cstream/cstreamcontract.cpp b/tests/cstream/cstreamcontract.cpp deleted file mode 100644 index c08ad63a0..000000000 --- a/tests/cstream/cstreamcontract.cpp +++ /dev/null @@ -1,148 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2026 OpenTS contributors - * - * See LICENSE.md for applicable additional terms and warranty disclaimers. - ******************************************************************************/ - -#include "cstream.h" - -#include - -#include -#include -#include -#include -#include - -ULONG COMRefCount = 0; - -namespace { - -int Failures = 0; - - -void Report(char const * name, bool ok) -{ - std::printf("%-64s %s\n", name, ok ? "ok" : "FAILED"); - if (!ok) Failures++; -} - - -bool Create_Storage(IStreamPtr & storage) -{ - IStream * stream = nullptr; - if (FAILED(CreateStreamOnHGlobal(nullptr, TRUE, &stream))) { - return(false); - } - storage.Attach(stream, false); - return(true); -} - - -bool Rewind(IStream * storage) -{ - LARGE_INTEGER const start = {}; - return(SUCCEEDED(storage->Seek(start, STREAM_SEEK_SET, nullptr))); -} - - -std::vector Make_Source(ULONG size) -{ - std::vector source(size); - std::uint32_t seed = 123456789; - for (unsigned char & value : source) { - seed ^= seed << 13; - seed ^= seed >> 17; - seed ^= seed << 5; - value = static_cast(seed & 15); - } - return(source); -} - - -void Test_Roundtrip(bool fragmented, ULONG tail) -{ - std::vector const source = Make_Source(CStreamClass::BUFFER_SIZE + tail); - IStreamPtr storage; - bool ok = Create_Storage(storage); - if (ok) { - CStreamClass writer; - ok = SUCCEEDED(writer.Link_Stream(storage)); - for (ULONG offset = 0; ok && offset < source.size();) { - ULONG const count = std::min(static_cast(source.size()) - offset, fragmented ? 997UL : static_cast(source.size())); - ULONG written = 0; - ok = SUCCEEDED(writer.Write(source.data() + offset, count, &written)) && written == count; - offset += count; - } - ok = SUCCEEDED(writer.Unlink_Stream(nullptr)) && ok; - } - - std::array header = {}; - if (ok) { - ULONG read = 0; - ok = Rewind(storage) && SUCCEEDED(storage->Read(header.data(), sizeof(header), &read)) && read == sizeof(header); - ok = ok && header[0] > CStreamClass::BUFFER_SIZE && header[0] <= CStreamClass::STREAM_BUFFER_SIZE; - std::printf("First compressed block: %lu bytes\n", header[0]); - } - - if (ok) { - ok = Rewind(storage); - CStreamClass reader; - ok = SUCCEEDED(reader.Link_Stream(storage)) && ok; - std::vector restored(source.size()); - for (ULONG offset = 0; ok && offset < restored.size();) { - ULONG const count = std::min(static_cast(restored.size()) - offset, fragmented ? 613UL : static_cast(restored.size())); - ULONG read = 0; - ok = SUCCEEDED(reader.Read(restored.data() + offset, count, &read)) && read == count; - offset += count; - } - ok = ok && restored == source; - - unsigned char extra = 0xA5; - ULONG read = 123; - ok = FAILED(reader.Read(&extra, sizeof(extra), &read)) && read == 0 && extra == 0xA5 && ok; - } - - Report(fragmented ? "Fragmented writes and reads with a partial final block" : "Full expanded block and exact end of stream", ok); -} - - -void Test_Read_Bound(void) -{ - IStreamPtr storage; - bool ok = Create_Storage(storage); - if (ok) { - std::array const header = {CStreamClass::STREAM_BUFFER_SIZE + 1, CStreamClass::BUFFER_SIZE}; - unsigned char const payload = 0; - ULONG written = 0; - ok = SUCCEEDED(storage->Write(header.data(), sizeof(header), &written)) && written == sizeof(header); - ok = SUCCEEDED(storage->Write(&payload, sizeof(payload), &written)) && written == sizeof(payload) && ok; - ok = Rewind(storage) && ok; - - CStreamClass reader; - ok = SUCCEEDED(reader.Link_Stream(storage)) && ok; - unsigned char result = 0xA5; - ULONG read = 123; - ok = FAILED(reader.Read(&result, sizeof(result), &read)) && read == 0 && result == 0xA5 && ok; - - LARGE_INTEGER const offset = {}; - ULARGE_INTEGER position = {}; - ok = SUCCEEDED(storage->Seek(offset, STREAM_SEEK_CUR, &position)) && position.QuadPart == sizeof(header) && ok; - } - Report("Oversized compressed header rejected before reading payload", ok); -} - -} - - -int main(void) -{ - Report("LZO initialization", lzo_init() == LZO_E_OK); - Test_Roundtrip(false, 0); - Test_Roundtrip(true, 137); - Test_Read_Bound(); - return(Failures == 0 ? 0 : 1); -} diff --git a/tests/deploymentconfig/CMakeLists.txt b/tests/deploymentconfig/CMakeLists.txt index 7405e28d0..d1621db71 100644 --- a/tests/deploymentconfig/CMakeLists.txt +++ b/tests/deploymentconfig/CMakeLists.txt @@ -10,6 +10,9 @@ opents_add_test(DeploymentConfig _deploymentconfig.cpp bfiofile.cpp rawfile.cpp + file.cpp + file_posix.cpp + file_win.cpp dbgprint.cpp ini.cpp utf8.cpp diff --git a/tests/gamedirs/CMakeLists.txt b/tests/gamedirs/CMakeLists.txt index d34cd8743..bff2374fb 100644 --- a/tests/gamedirs/CMakeLists.txt +++ b/tests/gamedirs/CMakeLists.txt @@ -10,6 +10,9 @@ opents_add_test(GameDirs cdfile.cpp bfiofile.cpp rawfile.cpp + file.cpp + file_posix.cpp + file_win.cpp dbgprint.cpp ini.cpp utf8.cpp diff --git a/tests/save/CMakeLists.txt b/tests/save/CMakeLists.txt new file mode 100644 index 000000000..556327111 --- /dev/null +++ b/tests/save/CMakeLists.txt @@ -0,0 +1,29 @@ +# The harness drives the file a saved game is kept in: savefile.cpp's writer and reader over +# the field table, the compressed content, and every refusal the reader makes. It links the +# same LZO the engine does. +add_executable(SaveTest + "${CMAKE_CURRENT_SOURCE_DIR}/savetest.cpp" + "${CMAKE_SOURCE_DIR}/code/savefile.cpp" +) + +target_compile_features(SaveTest PRIVATE cxx_std_20) + +target_include_directories(SaveTest PRIVATE + "${CMAKE_SOURCE_DIR}/code" +) + +target_compile_definitions(SaveTest PRIVATE WIN32 _WINDOWS _MBCS) + +target_compile_options(SaveTest PRIVATE + $<$:/MTd /EHsc /Zc:__cplusplus> + $<$:/MT /EHsc /Zc:__cplusplus> +) + +target_link_libraries(SaveTest PRIVATE kernel32 lzo) + +set_target_properties(SaveTest PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) + +# The harness writes the files it reads back, so it is given a directory of its own. +add_test(NAME save COMMAND SaveTest "${CMAKE_CURRENT_BINARY_DIR}") diff --git a/tests/save/savetest.cpp b/tests/save/savetest.cpp new file mode 100644 index 000000000..04fae38a3 --- /dev/null +++ b/tests/save/savetest.cpp @@ -0,0 +1,496 @@ +// Exercises the file a saved game is kept in: the field table the load dialog lists from, +// the compressed content block, and every way the reader refuses a file that is not a +// whole, intact save of a version it knows. +// +// Every file it touches it creates itself, in a scratch directory named by the first +// argument, so it reads no game data and leaves nothing behind. + +#include "savefile.h" + +#include + +#include +#include +#include +#include + +static int Failures = 0; +static int Checks = 0; +static std::string Scratch; + + +static void Check(char const * name, bool condition) +{ + Checks++; + if (condition) return; + Failures++; + printf("FAIL %s\n", name); +} + + +static void Check_Result(char const * name, SaveFileClass::ResultType actual, SaveFileClass::ResultType expected) +{ + Checks++; + if (actual == expected) return; + Failures++; + printf("FAIL %s: got \"%s\", expected \"%s\"\n", name, + SaveFileClass::Result_Text(actual), SaveFileClass::Result_Text(expected)); +} + + +static std::string Scratch_Path(char const * name) +{ + return(Scratch + "\\" + name); +} + + +static std::vector Noise(std::size_t length, unsigned int seed) +{ + std::vector data(length); + unsigned int state = seed * 2654435761u + 1u; + for (std::size_t index = 0; index < length; index++) { + state = state * 1103515245u + 12345u; + data[index] = (unsigned char)((state >> 16) & 0xFF); + } + return(data); +} + + +static std::vector Prose(std::size_t length) +{ + static char const text[] = "The quick brown fox jumps over the lazy dog. "; + std::vector data; + while (data.size() < length) { + data.push_back((unsigned char)text[data.size() % (sizeof(text) - 1)]); + } + return(data); +} + + +static std::vector Read_Whole_File(char const * path) +{ + std::vector data; + HANDLE const file = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, NULL); + if (file == INVALID_HANDLE_VALUE) return(data); + DWORD const size = GetFileSize(file, NULL); + if (size != INVALID_FILE_SIZE && size > 0) { + data.resize(size); + DWORD got = 0; + if (!ReadFile(file, data.data(), size, &got, NULL) || got != size) data.clear(); + } + CloseHandle(file); + return(data); +} + + +static bool Write_Whole_File(char const * path, std::vector const & data) +{ + HANDLE const file = CreateFileA(path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, NULL); + if (file == INVALID_HANDLE_VALUE) return(false); + DWORD written = 0; + bool ok = true; + if (!data.empty()) { + ok = WriteFile(file, data.data(), (DWORD)data.size(), &written, NULL) && written == data.size(); + } + CloseHandle(file); + return(ok); +} + + +static bool File_Exists(char const * path) +{ + HANDLE const file = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, NULL); + if (file == INVALID_HANDLE_VALUE) return(false); + CloseHandle(file); + return(true); +} + + +enum { + FIELD_TITLE = 2, + FIELD_HOUSE = 3, + FIELD_VERSION = 16, + FIELD_WHEN = 13, + FIELD_MISSING = 77, +}; + + +static void Fill(SaveFileClass & save, std::vector const & content) +{ + FILETIME when; + when.dwLowDateTime = 0x12345678u; + when.dwHighDateTime = 0x01D2C3B4u; + + save.Set_String(FIELD_TITLE, "GDI 04: Eviction Notice"); + save.Set_String(FIELD_HOUSE, "GDI"); + save.Set_Int(FIELD_VERSION, 0x00010203); + save.Set_Time(FIELD_WHEN, when); + save.Content = content; +} + + +static void Check_Fields(char const * prefix, SaveFileClass const & save) +{ + char text[64]; + int value = 0; + FILETIME when = {}; + + Check((std::string(prefix) + ": title present").c_str(), save.Get_String(FIELD_TITLE, text, sizeof(text))); + Check((std::string(prefix) + ": title text").c_str(), strcmp(text, "GDI 04: Eviction Notice") == 0); + Check((std::string(prefix) + ": house present").c_str(), save.Get_String(FIELD_HOUSE, text, sizeof(text))); + Check((std::string(prefix) + ": house text").c_str(), strcmp(text, "GDI") == 0); + Check((std::string(prefix) + ": version present").c_str(), save.Get_Int(FIELD_VERSION, &value)); + Check((std::string(prefix) + ": version value").c_str(), value == 0x00010203); + Check((std::string(prefix) + ": time present").c_str(), save.Get_Time(FIELD_WHEN, &when)); + Check((std::string(prefix) + ": time value").c_str(), + when.dwLowDateTime == 0x12345678u && when.dwHighDateTime == 0x01D2C3B4u); + Check((std::string(prefix) + ": a missing field is absent").c_str(), !save.Get_String(FIELD_MISSING, text, sizeof(text))); + Check((std::string(prefix) + ": a field is not found under another kind").c_str(), !save.Get_Int(FIELD_TITLE, &value)); + + Check((std::string(prefix) + ": a short buffer is clipped").c_str(), + save.Get_String(FIELD_TITLE, text, 4) && strcmp(text, "GDI") == 0); +} + + +static void Test_Round_Trip(void) +{ + std::string const path = Scratch_Path("ROUNDTRIP.SAV"); + std::vector const content = Prose(300000); + + SaveFileClass written; + Fill(written, content); + Check_Result("round trip: write", written.Write(path.c_str()), SaveFileClass::RESULT_OK); + Check("round trip: no temporary file is left behind", !File_Exists((path + ".tmp").c_str())); + + std::vector const image = Read_Whole_File(path.c_str()); + Check("round trip: the prose was compressed", !image.empty() && image.size() < content.size() / 4); + + SaveFileClass read; + Check_Result("round trip: read", read.Read(path.c_str()), SaveFileClass::RESULT_OK); + Check_Fields("round trip", read); + Check("round trip: content reads back whole", read.Content == content); + + SaveFileClass listed; + Check_Result("round trip: fields alone", listed.Read_Fields(path.c_str()), SaveFileClass::RESULT_OK); + Check_Fields("fields alone", listed); + Check("fields alone: no content is read", listed.Content.empty()); +} + + +static void Test_Cuts(void) +{ + SaveFileClass save; + save.Set_String(FIELD_TITLE, "ab\xC3\xA9" "cd"); + + char text[8]; + Check("cuts: a cut never splits a character", save.Get_String(FIELD_TITLE, text, 4) && strcmp(text, "ab") == 0); + Check("cuts: a cut after a character keeps it whole", save.Get_String(FIELD_TITLE, text, 5) && strcmp(text, "ab\xC3\xA9") == 0); + Check("cuts: a buffer that fits keeps everything", save.Get_String(FIELD_TITLE, text, 8) && strcmp(text, "ab\xC3\xA9" "cd") == 0); +} + + +static void Test_Incompressible(void) +{ + std::string const path = Scratch_Path("NOISE.SAV"); + std::vector const content = Noise(70000, 7); + + SaveFileClass written; + Fill(written, content); + Check_Result("noise: write", written.Write(path.c_str()), SaveFileClass::RESULT_OK); + + std::vector const image = Read_Whole_File(path.c_str()); + Check("noise: stored as it is when compression does not pay", image.size() >= content.size() + SaveFileClass::HEADER_SIZE); + + SaveFileClass read; + Check_Result("noise: read", read.Read(path.c_str()), SaveFileClass::RESULT_OK); + Check("noise: content reads back whole", read.Content == content); +} + + +static void Test_Empty(void) +{ + std::string const path = Scratch_Path("EMPTY.SAV"); + + SaveFileClass written; + Check_Result("empty: write", written.Write(path.c_str()), SaveFileClass::RESULT_OK); + + std::vector const image = Read_Whole_File(path.c_str()); + Check("empty: a header alone", image.size() == SaveFileClass::HEADER_SIZE); + + SaveFileClass read; + Check_Result("empty: read", read.Read(path.c_str()), SaveFileClass::RESULT_OK); + Check("empty: no content", read.Content.empty()); + char text[8]; + Check("empty: no fields", !read.Get_String(FIELD_TITLE, text, sizeof(text))); +} + + +static void Test_Overwrite(void) +{ + std::string const path = Scratch_Path("REPLACE.SAV"); + + SaveFileClass first; + Fill(first, Prose(5000)); + first.Set_String(FIELD_TITLE, "the earlier save"); + Check_Result("replace: first write", first.Write(path.c_str()), SaveFileClass::RESULT_OK); + + Check("replace: a stale temporary is planted", Write_Whole_File((path + ".tmp").c_str(), Noise(100, 3))); + + SaveFileClass second; + Fill(second, Noise(20000, 11)); + second.Set_String(FIELD_TITLE, "the later save"); + Check_Result("replace: second write", second.Write(path.c_str()), SaveFileClass::RESULT_OK); + Check("replace: the stale temporary is gone", !File_Exists((path + ".tmp").c_str())); + + SaveFileClass read; + Check_Result("replace: read", read.Read(path.c_str()), SaveFileClass::RESULT_OK); + char text[64]; + Check("replace: the later save is the one on disk", + read.Get_String(FIELD_TITLE, text, sizeof(text)) && strcmp(text, "the later save") == 0); + Check("replace: the later content is the one on disk", read.Content == second.Content); + + SaveFileClass rewritten; + rewritten.Set_String(FIELD_TITLE, "overwritten field"); + rewritten.Set_String(FIELD_TITLE, "final field"); + Check_Result("replace: field rewrite", rewritten.Write(path.c_str()), SaveFileClass::RESULT_OK); + Check_Result("replace: field rewrite read", read.Read_Fields(path.c_str()), SaveFileClass::RESULT_OK); + Check("replace: a field set twice keeps the last value", + read.Get_String(FIELD_TITLE, text, sizeof(text)) && strcmp(text, "final field") == 0); +} + + +static void Put_U32(std::vector & image, std::size_t at, unsigned int value) +{ + image[at] = (unsigned char)(value & 0xFF); + image[at + 1] = (unsigned char)((value >> 8) & 0xFF); + image[at + 2] = (unsigned char)((value >> 16) & 0xFF); + image[at + 3] = (unsigned char)((value >> 24) & 0xFF); +} + + +static void Test_Limits(void) +{ + std::string const path = Scratch_Path("LIMITS.SAV"); + + SaveFileClass kept; + Fill(kept, Prose(3000)); + kept.Set_String(FIELD_TITLE, "the save that stays"); + Check_Result("limits: the save that stays", kept.Write(path.c_str()), SaveFileClass::RESULT_OK); + + SaveFileClass wide; + Fill(wide, Prose(3000)); + wide.Set_String(FIELD_TITLE, std::string(0x10001, 'x').c_str()); + Check_Result("limits: a field beyond its limit is refused", wide.Write(path.c_str()), SaveFileClass::RESULT_TOO_LARGE); + + SaveFileClass many; + Fill(many, Prose(3000)); + for (int id = 100; id < 117; id++) { + many.Set_String(id, std::string(0x10000, 'y').c_str()); + } + Check_Result("limits: a table beyond its limit is refused", many.Write(path.c_str()), SaveFileClass::RESULT_TOO_LARGE); + + SaveFileClass huge; + huge.Content.resize(0x10000001); + Check_Result("limits: content beyond its limit is refused", huge.Write(path.c_str()), SaveFileClass::RESULT_TOO_LARGE); + + Check("limits: no temporary is left behind", !File_Exists((path + ".tmp").c_str())); + SaveFileClass read; + Check_Result("limits: the earlier save still reads", read.Read(path.c_str()), SaveFileClass::RESULT_OK); + char text[64]; + Check("limits: the earlier save is the one on disk", + read.Get_String(FIELD_TITLE, text, sizeof(text)) && strcmp(text, "the save that stays") == 0); +} + + +// Recomputes the header checksum after a test has changed a header byte on purpose. +static void Reseal_Header(std::vector & image, unsigned int table) +{ + unsigned int crc = SaveFileClass::Checksum(image.data(), SaveFileClass::HEADER_SIZE - 4); + crc = SaveFileClass::Checksum(image.data() + SaveFileClass::HEADER_SIZE, table, crc); + Put_U32(image, 28, crc); +} + + +// Rebuilds a save image around a field table of the test's own making, with the content +// kept and every checksum made good. +static std::vector Forge_Table(std::vector const & image, unsigned int table, + std::vector const & newtable) +{ + std::vector forged(image.begin(), image.begin() + SaveFileClass::HEADER_SIZE); + forged.insert(forged.end(), newtable.begin(), newtable.end()); + forged.insert(forged.end(), image.begin() + SaveFileClass::HEADER_SIZE + table, image.end()); + Put_U32(forged, 8, (unsigned int)newtable.size()); + Put_U32(forged, 12, SaveFileClass::HEADER_SIZE + (unsigned int)newtable.size()); + Reseal_Header(forged, (unsigned int)newtable.size()); + return(forged); +} + + +// Replaces the content of a save image with a compressed block of the test's own making, +// declared as expanding to the length given, with every checksum made good. +static std::vector Forge_Content(std::vector const & image, unsigned int table, + std::vector const & stored, unsigned int expands_to) +{ + std::vector forged(image.begin(), image.begin() + SaveFileClass::HEADER_SIZE + table); + forged.insert(forged.end(), stored.begin(), stored.end()); + forged[6] |= 0x01; + Put_U32(forged, 12, SaveFileClass::HEADER_SIZE + table); + Put_U32(forged, 16, (unsigned int)stored.size()); + Put_U32(forged, 20, expands_to); + Put_U32(forged, 24, SaveFileClass::Checksum(stored.data(), (unsigned int)stored.size())); + Reseal_Header(forged, table); + return(forged); +} + + +static void Test_Refusals(void) +{ + SaveFileClass read; + + std::string const missing = Scratch_Path("MISSING.SAV"); + Check_Result("refuse: a missing file", read.Read(missing.c_str()), SaveFileClass::RESULT_MISSING); + Check_Result("refuse: a missing file's fields", read.Read_Fields(missing.c_str()), SaveFileClass::RESULT_MISSING); + + std::string const plain = Scratch_Path("PLAIN.SAV"); + std::vector hello = { 'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd' }; + Write_Whole_File(plain.c_str(), hello); + Check_Result("refuse: a file that is not a save", read.Read(plain.c_str()), SaveFileClass::RESULT_NOT_A_SAVE); + Check_Result("refuse: its fields", read.Read_Fields(plain.c_str()), SaveFileClass::RESULT_NOT_A_SAVE); + + std::string const good = Scratch_Path("GOOD.SAV"); + SaveFileClass written; + Fill(written, Prose(40000)); + Check_Result("refuse: the reference save", written.Write(good.c_str()), SaveFileClass::RESULT_OK); + std::vector const image = Read_Whole_File(good.c_str()); + Check("refuse: the reference save is readable", !image.empty()); + + std::string const damaged = Scratch_Path("DAMAGED.SAV"); + + unsigned int const table = (unsigned int)image[8] | ((unsigned int)image[9] << 8) + | ((unsigned int)image[10] << 16) | ((unsigned int)image[11] << 24); + + std::vector future = image; + future[4] = 99; + future[5] = 0; + Reseal_Header(future, table); + Write_Whole_File(damaged.c_str(), future); + Check_Result("refuse: a later format version", read.Read(damaged.c_str()), SaveFileClass::RESULT_UNSUPPORTED_VERSION); + Check_Result("refuse: its fields", read.Read_Fields(damaged.c_str()), SaveFileClass::RESULT_UNSUPPORTED_VERSION); + + std::vector flagged = image; + flagged[6] |= 0x02; + Reseal_Header(flagged, table); + Write_Whole_File(damaged.c_str(), flagged); + Check_Result("refuse: a header flag this build does not know", read.Read(damaged.c_str()), SaveFileClass::RESULT_UNSUPPORTED_VERSION); + Check_Result("refuse: its fields", read.Read_Fields(damaged.c_str()), SaveFileClass::RESULT_UNSUPPORTED_VERSION); + + unsigned int const content_offset = (unsigned int)image[12] | ((unsigned int)image[13] << 8) + | ((unsigned int)image[14] << 16) | ((unsigned int)image[15] << 24); + unsigned int const content_length = (unsigned int)image[20] | ((unsigned int)image[21] << 8) + | ((unsigned int)image[22] << 16) | ((unsigned int)image[23] << 24); + Check("refuse: the reference save is compressed", (image[6] & 0x01) != 0); + std::vector const stored(image.begin() + content_offset, image.end()); + + Write_Whole_File(damaged.c_str(), Forge_Content(image, table, stored, content_length - 1)); + Check_Result("refuse: a block that expands past its declared length", read.Read(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); + Write_Whole_File(damaged.c_str(), Forge_Content(image, table, stored, 16)); + Check_Result("refuse: a block that expands far past its declared length", read.Read(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); + Write_Whole_File(damaged.c_str(), Forge_Content(image, table, stored, content_length + 1)); + Check_Result("refuse: a block that ends before its declared length", read.Read(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); + + std::vector const reaching_back = { 18, 'A', 4, 0 }; + Write_Whole_File(damaged.c_str(), Forge_Content(image, table, reaching_back, 3)); + Check_Result("refuse: a block whose match reaches before the start", read.Read(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); + + std::vector const unfinished = { 18, 'A' }; + Write_Whole_File(damaged.c_str(), Forge_Content(image, table, unfinished, 1)); + Check_Result("refuse: a block with no end marker", read.Read(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); + + Write_Whole_File(damaged.c_str(), Forge_Content(image, table, stored, 0x10000001)); + Check_Result("refuse: a block declared larger than any save", read.Read(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); + Check_Result("refuse: its fields", read.Read_Fields(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); + + std::vector const oversized(0x100001, 0); + Write_Whole_File(damaged.c_str(), Forge_Table(image, table, oversized)); + Check_Result("refuse: a field table longer than any listing", read.Read(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); + Check_Result("refuse: its fields", read.Read_Fields(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); + + std::vector gapped = image; + gapped.insert(gapped.begin() + content_offset, 8, 0); + Put_U32(gapped, 12, content_offset + 8); + Reseal_Header(gapped, table); + Check("refuse: the gapped image is longer", gapped.size() == image.size() + 8); + Write_Whole_File(damaged.c_str(), gapped); + Check_Result("refuse: a gap between the table and the content", read.Read(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); + Check_Result("refuse: its fields", read.Read_Fields(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); + + std::vector header_hit = image; + header_hit[9] ^= 0x01; + Write_Whole_File(damaged.c_str(), header_hit); + Check_Result("refuse: a header byte flipped", read.Read(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); + Check_Result("refuse: its fields", read.Read_Fields(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); + + std::vector table_hit = image; + table_hit[SaveFileClass::HEADER_SIZE + 10] ^= 0x20; + Write_Whole_File(damaged.c_str(), table_hit); + Check_Result("refuse: a field byte flipped", read.Read(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); + Check_Result("refuse: its fields", read.Read_Fields(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); + + std::vector content_hit = image; + content_hit[image.size() - 40] ^= 0x80; + Write_Whole_File(damaged.c_str(), content_hit); + Check_Result("refuse: a content byte flipped", read.Read(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); + Check_Result("refuse: a flipped content byte still lists", read.Read_Fields(damaged.c_str()), SaveFileClass::RESULT_OK); + + std::size_t const cuts[] = { 3, 12, SaveFileClass::HEADER_SIZE - 1, SaveFileClass::HEADER_SIZE + 5, + SaveFileClass::HEADER_SIZE + table, image.size() / 2, image.size() - 1 }; + for (std::size_t cut : cuts) { + std::vector truncated(image.begin(), image.begin() + cut); + Write_Whole_File(damaged.c_str(), truncated); + char name[80]; + snprintf(name, sizeof(name), "refuse: a file cut at %u bytes", (unsigned int)cut); + SaveFileClass::ResultType const result = read.Read(damaged.c_str()); + Check(name, result == SaveFileClass::RESULT_CORRUPT || (cut < 4 && result == SaveFileClass::RESULT_NOT_A_SAVE)); + } + + std::vector appended = image; + appended.push_back(0); + Write_Whole_File(damaged.c_str(), appended); + Check_Result("refuse: a file with a trailing byte", read.Read(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); +} + + +int main(int argc, char ** argv) +{ + if (argc < 2) { + printf("usage: SaveTest \n"); + return(2); + } + if (lzo_init() != LZO_E_OK) { + printf("lzo_init failed\n"); + return(2); + } + + Scratch = argv[1]; + CreateDirectoryA(Scratch.c_str(), NULL); + + Test_Round_Trip(); + Test_Cuts(); + Test_Incompressible(); + Test_Empty(); + Test_Overwrite(); + Test_Limits(); + Test_Refusals(); + + char const * const names[] = { "ROUNDTRIP.SAV", "NOISE.SAV", "EMPTY.SAV", "REPLACE.SAV", + "PLAIN.SAV", "GOOD.SAV", "DAMAGED.SAV" }; + for (char const * name : names) { + DeleteFileA(Scratch_Path(name).c_str()); + } + + printf("%d checks, %d failures\n", Checks, Failures); + return(Failures == 0 ? 0 : 1); +} diff --git a/tests/socketudp/socketudp.cpp b/tests/socketudp/socketudp.cpp index dbafee5c8..80e0af72e 100644 --- a/tests/socketudp/socketudp.cpp +++ b/tests/socketudp/socketudp.cpp @@ -216,20 +216,40 @@ void Test_Reset_Does_Not_End_The_Pass(void) } -/// A packet from one of our own addresses is our own broadcast coming back. +/// A packet from one of our own addresses on our own port is our own broadcast +/// coming back. void Test_Own_Address_Is_Discarded(void) { uint32_t const mine = 0x0100007f; Harness harness({{mine, 0}}); - IPXAddressClass const self = Peer(mine, 51000); + IPXAddressClass const self = Peer(mine, 50000); std::vector const wire = harness.Send("echo", self); harness.Socket->Deliver(self, wire.data(), static_cast(wire.size())); harness.Transport.Service(); - Check(harness.Drain().empty(), "a packet from one of our own addresses is thrown away"); + Check(harness.Drain().empty(), "a packet from our own address and port is thrown away"); +} + + +/// Another instance of the game on this machine sends from one of our own +/// addresses and a port of its own. It is a peer, so it must be heard. +void Test_Local_Peer_On_Another_Port_Is_Heard(void) +{ + uint32_t const mine = 0x0100007f; + + Harness harness({{mine, 0}}); + IPXAddressClass const peer = Peer(mine, 50001); + + std::vector const wire = harness.Send("neighbour", peer); + + harness.Socket->Deliver(peer, wire.data(), static_cast(wire.size())); + harness.Transport.Service(); + + std::vector const got = harness.Drain(); + Check(got.size() == 1 && got[0] == "neighbour", "a local address on another port is heard"); } @@ -323,6 +343,7 @@ int main(void) Test_Drain_Is_Bounded(); Test_Reset_Does_Not_End_The_Pass(); Test_Own_Address_Is_Discarded(); + Test_Local_Peer_On_Another_Port_Is_Heard(); Test_Malformed_Is_Rejected(); Test_Tunnel_Framing(); Test_Broadcast_Addresses(); diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index 76dd59ac8..0736609bf 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -20,8 +20,9 @@ set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") add_subdirectory(bgfx.cmake) -# These libraries support the disabled texture tools and are not linked into OpenTS. -set_target_properties(bimg_decode bimg_encode PROPERTIES EXCLUDE_FROM_ALL TRUE) +# The encoder supports the disabled texture tools and is not linked into OpenTS. The decoder +# reads the PNG and TGA files the UI documents reference, so it is built with everything else. +set_target_properties(bimg_encode PROPERTIES EXCLUDE_FROM_ALL TRUE) if(OPENTS_EXPERIMENTAL_CLANG_CL AND CMAKE_SIZEOF_VOID_P EQUAL 4) # bx treats Clang with the MSVC CRT like a non-x86 compiler and erases @@ -101,3 +102,111 @@ target_include_directories(lzo PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/lzo/include" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/lzo/src" ) + +# +# --------------------------------------------------------- +# SDL3 (window, event loop, cursor, keyboard) -- non-Windows only +# --------------------------------------------------------- +# +# Windows supplies the window, the message loop and the cursor itself, so the supported +# build links none of this. Every other target needs one library that also reaches iOS, +# which rules out a desktop-only toolkit and rules out writing AppKit and UIKit twice. +# Audio stays on miniaudio; only the video, event and keyboard subsystems are built. +if(NOT WIN32) + if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/SDL/CMakeLists.txt") + message(FATAL_ERROR + "thirdparty/SDL is empty. Fetch the submodules with:\n" + " git submodule update --init --recursive") + endif() + + set(SDL_SHARED OFF CACHE BOOL "" FORCE) + set(SDL_STATIC ON CACHE BOOL "" FORCE) + set(SDL_TEST_LIBRARY OFF CACHE BOOL "" FORCE) + set(SDL_TESTS OFF CACHE BOOL "" FORCE) + set(SDL_EXAMPLES OFF CACHE BOOL "" FORCE) + set(SDL_INSTALL OFF CACHE BOOL "" FORCE) + set(SDL_AUDIO OFF CACHE BOOL "" FORCE) + set(SDL_RENDER OFF CACHE BOOL "" FORCE) + set(SDL_CAMERA OFF CACHE BOOL "" FORCE) + set(SDL_HAPTIC OFF CACHE BOOL "" FORCE) + set(SDL_SENSOR OFF CACHE BOOL "" FORCE) + set(SDL_POWER OFF CACHE BOOL "" FORCE) + set(SDL_DIALOG OFF CACHE BOOL "" FORCE) + + add_subdirectory(SDL) +endif() + +# +# --------------------------------------------------------- +# FreeType (glyph rasterization for the UI font engine) +# --------------------------------------------------------- +# +# Only RmlUi reaches this library, and only for the shipped sans-serif face. Its optional +# dependencies would each add a library the engine does not otherwise carry, so every one +# of them is refused here rather than left to whatever the build machine happens to have. +if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/freetype/CMakeLists.txt") + message(FATAL_ERROR + "thirdparty/freetype is empty. Fetch the submodules with:\n" + " git submodule update --init --recursive") +endif() + +set(FT_DISABLE_BZIP2 ON CACHE BOOL "" FORCE) +set(FT_DISABLE_PNG ON CACHE BOOL "" FORCE) +set(FT_DISABLE_HARFBUZZ ON CACHE BOOL "" FORCE) +set(FT_DISABLE_BROTLI ON CACHE BOOL "" FORCE) + +add_subdirectory(freetype) + +# FreeType names its export Freetype::Freetype but declares no target under that name in the +# build tree, which is the name RmlUi looks for. +if(NOT TARGET Freetype::Freetype) + add_library(Freetype::Freetype ALIAS freetype) +endif() + +# +# --------------------------------------------------------- +# RmlUi (documents, styling and layout for the UI shell) +# --------------------------------------------------------- +# +# Built as a static library with the FreeType font engine. The samples carry their own +# window and renderer backends, which is what the UI shell replaces, so none of them are +# built and no backend is selected. +if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/RmlUi/CMakeLists.txt") + message(FATAL_ERROR + "thirdparty/RmlUi is empty. Fetch the submodules with:\n" + " git submodule update --init --recursive") +endif() + +set(BUILD_SHARED_LIBS OFF) +set(RMLUI_FONT_ENGINE "freetype" CACHE STRING "" FORCE) +set(RMLUI_SAMPLES OFF CACHE BOOL "" FORCE) +set(RMLUI_LUA_BINDINGS OFF CACHE BOOL "" FORCE) +set(RMLUI_SVG_PLUGIN OFF CACHE BOOL "" FORCE) +set(RMLUI_LOTTIE_PLUGIN OFF CACHE BOOL "" FORCE) +set(RMLUI_HARFBUZZ_SAMPLE OFF CACHE BOOL "" FORCE) +set(RMLUI_TRACY_PROFILING OFF CACHE BOOL "" FORCE) +set(RMLUI_INSTALL_TARGETS_DIR "" CACHE STRING "" FORCE) +set(RMLUI_IS_ROOT_PROJECT FALSE) + +add_subdirectory(RmlUi) + +# +# --------------------------------------------------------- +# Dear ImGui (developer tooling on the same UI shell) +# --------------------------------------------------------- +# +# The core sources only. The bundled platform and renderer backends are left out; the UI +# shell feeds ImGui through the engine's own message hook and draws it on bgfx. +if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/imgui/imgui.cpp") + message(FATAL_ERROR + "thirdparty/imgui is empty. Fetch the submodules with:\n" + " git submodule update --init --recursive") +endif() + +add_library(imgui STATIC + "${CMAKE_CURRENT_SOURCE_DIR}/imgui/imgui.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/imgui/imgui_draw.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/imgui/imgui_tables.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/imgui/imgui_widgets.cpp" +) +target_include_directories(imgui PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/imgui") diff --git a/thirdparty/RmlUi b/thirdparty/RmlUi new file mode 160000 index 000000000..ba95ffe8b --- /dev/null +++ b/thirdparty/RmlUi @@ -0,0 +1 @@ +Subproject commit ba95ffe8bfb6370efb2cdcca927eaad4710c5413 diff --git a/thirdparty/SDL b/thirdparty/SDL new file mode 160000 index 000000000..fa2c02bb6 --- /dev/null +++ b/thirdparty/SDL @@ -0,0 +1 @@ +Subproject commit fa2c02bb6e21974a89ea9824bc53c9932abe5f9c diff --git a/thirdparty/freetype b/thirdparty/freetype new file mode 160000 index 000000000..42608f77f --- /dev/null +++ b/thirdparty/freetype @@ -0,0 +1 @@ +Subproject commit 42608f77f20749dd6ddc9e0536788eaad70ea4b5 diff --git a/thirdparty/imgui b/thirdparty/imgui new file mode 160000 index 000000000..f1cc2ae15 --- /dev/null +++ b/thirdparty/imgui @@ -0,0 +1 @@ +Subproject commit f1cc2ae15e53a861a874c3034aae6798fde194ab diff --git a/thirdparty/licenses/zlib.txt b/thirdparty/licenses/zlib.txt new file mode 100644 index 000000000..1b05130bc --- /dev/null +++ b/thirdparty/licenses/zlib.txt @@ -0,0 +1,24 @@ +zlib (thirdparty/freetype/src/gzip) + +version 1.3, August 18th, 2023 + +Copyright (C) 1995-2023 Jean-loup Gailly and Mark Adler + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + +Jean-loup Gailly Mark Adler +jloup@gzip.org madler@alumni.caltech.edu diff --git a/ui/LICENSE.md b/ui/LICENSE.md new file mode 100644 index 000000000..1ead17558 --- /dev/null +++ b/ui/LICENSE.md @@ -0,0 +1,9 @@ +# Shipped UI files + +`LatoLatin-Regular.ttf` is part of the Lato family by Łukasz Dziedzic, released +under the SIL Open Font License 1.1. The copy here is the one RmlUi vendors in +`thirdparty/RmlUi/Samples/assets/`; that directory's `LICENSE.txt` holds the +license text. + +The documents and styles beside it are OpenTS files under the repository +license. diff --git a/ui/LatoLatin-Regular.ttf b/ui/LatoLatin-Regular.ttf new file mode 100644 index 000000000..bcc57780d Binary files /dev/null and b/ui/LatoLatin-Regular.ttf differ diff --git a/ui/abort.rcss b/ui/abort.rcss new file mode 100644 index 000000000..3920dd2eb --- /dev/null +++ b/ui/abort.rcss @@ -0,0 +1,41 @@ +/* The abort and surrender box. Geometry from the IDD_MISSION_ABORT template, converted from + dialog units at the 8 point MS Sans Serif the template names: 1.5 pixels across and 1.625 + down, with a child's offset taken from the panel's content box and the panel's declared + size taken inside its own border. */ + +/* 256 x 63 dialog units, so 384 x 102.375 pixels, centred on the frame. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -192dp; + margin-top: -51.1875dp; + + width: 380dp; + height: 98.375dp; +} + +/* CTEXT with SS_CENTERIMAGE, 212 x 19 dialog units at 22, 12: centred both ways within its + own extents. */ +#question +{ + left: 31dp; + top: 17.5dp; + width: 318dp; + height: 30.875dp; + line-height: 30.875dp; + text-align: center; +} + +/* Three buttons, all 60 x 14 dialog units on the same row. */ +.button +{ + top: 58.125dp; + width: 90dp; + height: 22.75dp; + line-height: 22.75dp; +} + +#quit { left: 31dp; } +#restart { left: 145dp; } +#cancel { left: 259dp; } diff --git a/ui/abort.rml b/ui/abort.rml new file mode 100644 index 000000000..f39ba21ab --- /dev/null +++ b/ui/abort.rml @@ -0,0 +1,15 @@ + + + Abort mission + + + + +
+
Do you want to abort the mission?
+
Abort
+
{{ restartcaption }}
+
[[TXT_CANCEL]]
+
+ +
diff --git a/ui/campaign.rcss b/ui/campaign.rcss new file mode 100644 index 000000000..16c569509 --- /dev/null +++ b/ui/campaign.rcss @@ -0,0 +1,78 @@ +/* The campaign choice. Geometry from the IDD_CAMPAIGN template, converted from dialog units + at the 8 point MS Sans Serif the template names: 1.5 pixels across and 1.625 down, with a + child's offset taken from the panel's content box and the panel's declared size taken + inside its own border. */ + +/* 246 x 149 dialog units, so 369 x 242.125 pixels. + + The panel is not centred vertically. Its driver moved it with + Move_Dialog(handle, -1, (HiddenSurface->Get_Height() - 400) / 2 + 147), which is half the + frame height less 53, so it sits 53 dp above the middle whatever the frame height is. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -184.5dp; + margin-top: -53dp; + + width: 365dp; + height: 238.125dp; +} + +/* CTEXT with SS_CENTERIMAGE, 206 x 10 dialog units at 22, 12. */ +#question +{ + left: 31dp; + top: 17.5dp; + width: 309dp; + height: 16.25dp; + line-height: 16.25dp; + text-align: center; +} + +/* The campaign list, 206 x 51 dialog units at 22, 28. The template draws it with NOT + WS_BORDER, so it has none. */ +#campaignlist +{ + left: 31dp; + top: 43.5dp; + width: 309dp; + height: 82.875dp; +} + +/* A row spans the list less its scrollbar. */ +#campaignlist .row { width: 297dp; } + +/* The left caption is a static with SS_LEFTNOWORDWRAP and SS_CENTERIMAGE, 136 x 15 dialog + units at 22, 84; the value beside it is RTEXT, 66 x 15 at 162, 84. */ +.caption +{ + height: 24.375dp; + line-height: 24.375dp; +} + +#difficultylabel { left: 31dp; top: 134.5dp; width: 204dp; } +#difficultyvalue { left: 241dp; top: 134.5dp; width: 99dp; text-align: right; } + +/* The difficulty track bar, 206 x 15 dialog units at 22, 98. TBS_NOTICKS, so the bar is a + plain groove with a thumb. */ +#difficulty +{ + left: 31dp; + top: 157.25dp; + width: 309dp; + height: 24.375dp; +} + + +/* OK and Cancel, both 50 x 16 dialog units at y 121. */ +.button +{ + top: 194.625dp; + width: 75dp; + height: 26dp; + line-height: 26dp; +} + +#ok { left: 179.5dp; } +#cancel { left: 265dp; } diff --git a/ui/campaign.rml b/ui/campaign.rml new file mode 100644 index 000000000..4581486ad --- /dev/null +++ b/ui/campaign.rml @@ -0,0 +1,23 @@ + + + Select campaign + + + + +
+
Select Campaign:
+ +
+
{{ entry.label }}
+
+ +
Difficulty
+
{{ difficultyname }}
+ + +
[[TXT_OK]]
+
[[TXT_CANCEL]]
+
+ +
diff --git a/ui/desyncbase.rcss b/ui/desyncbase.rcss new file mode 100644 index 000000000..4b498e453 --- /dev/null +++ b/ui/desyncbase.rcss @@ -0,0 +1,152 @@ +/* What the two out-of-sync documents share. Only the look and the geometry both templates + agree on live here; each document's own stylesheet carries what its variant changes. + + Geometry from the IDD_DESYNC_HOST and IDD_DESYNC_WAIT templates, converted from dialog + units at the 8 point MS Sans Serif they name: 1.5 pixels across and 1.625 down, with a + child's offset taken from the panel's content box and the panel's declared size taken + inside its own border. The two templates are both 360 x 264 dialog units, so 540 x 429 + pixels, and every control they share stands in the same place. */ + +#dialog +{ + left: 50%; + top: 50%; + margin-left: -270dp; + margin-top: -214.5dp; + + width: 536dp; + height: 425dp; +} + +/* The heading, a CTEXT at 30, 10 across 280 units. */ +#header +{ + left: 45dp; + top: 16.25dp; + width: 420dp; + height: 16.25dp; + line-height: 16.25dp; + text-align: center; + color: #70ff00; +} + +/* "Players:" at 30, 23 and the 120 x 105 seat list at 30, 35. */ +#playerslabel { left: 45dp; top: 37.375dp; width: 150dp; height: 16.25dp; line-height: 16.25dp; } +#players { left: 45dp; top: 56.875dp; width: 180dp; height: 170.625dp; } + +/* A scrolling container gives its children no width to be a proportion of, so the row + states its own, and the columns stand where OD_ADDCOLUMN put them: the marker at 2, the + name at 20, and the status against the list's right edge less 56. */ +#players .row +{ + position: relative; + width: 168dp; + height: 16dp; + line-height: 16dp; + padding: 0dp; +} + +#players .mark +{ + display: block; + position: absolute; + left: 2dp; + top: 0dp; + width: 16dp; + text-align: center; + color: #70ff00; +} + +#players .name +{ + display: block; + position: absolute; + left: 20dp; + top: 0dp; + width: 98dp; + white-space: nowrap; + overflow: hidden; +} + +#players .status +{ + display: block; + position: absolute; + left: 124dp; + top: 0dp; + width: 44dp; + white-space: nowrap; + overflow: hidden; +} + +/* The two blocks of prose beside the list, LTEXT at 160, 25 across 180 x 82 units and at + 160, 127 across 180 x 22. Each template writes its own words. */ +.prose +{ + display: block; + position: absolute; + box-sizing: border-box; + left: 240dp; + width: 270dp; + color: #70ff00; + line-height: 16dp; +} + +#prose { top: 40.625dp; height: 133.25dp; } +#footprose { top: 206.375dp; height: 35.75dp; } + +/* The chat list, 300 x 60 at 30, 145, and the chat entry, 299 x 12 at 30, 207. */ +#messages { left: 45dp; top: 235.625dp; width: 450dp; height: 97.5dp; } +#messages .line { width: 438dp; } + +#say { left: 45dp; top: 336.375dp; width: 448.5dp; height: 19.5dp; line-height: 15.5dp; } + +/* The countdown, hidden until a load is scheduled: the text at 30, 223 and the bar's + placeholder group box at 180, 222. */ +#countdowntext +{ + left: 45dp; + top: 362.375dp; + width: 210dp; + height: 16.25dp; + line-height: 16.25dp; +} + +#countdownframe +{ + display: block; + position: absolute; + box-sizing: border-box; + left: 270dp; + top: 360.75dp; + width: 223.5dp; + height: 19.5dp; + + background-color: #000000b4; + border-width: 1dp; + border-color: #ffffff; +} + +/* The bar shrinks as the load nears, which is what Draw_Countdown_Bar drew straight into + the surface. Its color comes from the model, because the dialog changed it with the time + left rather than with a state a stylesheet can name. */ +#countdownbar +{ + display: block; + position: absolute; + left: 0dp; + top: 0dp; + height: 100%; + min-width: 6dp; +} + +/* The three buttons, 70 x 12 dialog units at y 239. */ +.button { top: 388.375dp; width: 105dp; height: 19.5dp; line-height: 19.5dp; } + +/* The dialog's own heading, an LTEXT static. + The dlgsys sheets, as optionsbase.rcss gives every other static. */ +#header +{ + font-family: dlgsys; + font-size: 18dp; +} diff --git a/ui/desynchost.rcss b/ui/desynchost.rcss new file mode 100644 index 000000000..408378030 --- /dev/null +++ b/ui/desynchost.rcss @@ -0,0 +1,5 @@ +/* The master's out-of-sync screen, IDD_DESYNC_HOST. It carries all three buttons, at 30, + 145 and 260 dialog units across the bottom row the base stylesheet places. */ +#load { left: 45dp; } +#continue { left: 217.5dp; } +#quit { left: 390dp; } diff --git a/ui/desynchost.rml b/ui/desynchost.rml new file mode 100644 index 000000000..6a7c15ff6 --- /dev/null +++ b/ui/desynchost.rml @@ -0,0 +1,41 @@ + + + Synchronization error + + + + + + +
+ + +
Players:
+
+
+
{{ entry.mark }}
+
{{ entry.name }}
+
{{ entry.status }}
+
+
+ +
The game has gone out of sync.

Press "Load Game" to load a saved game from this session, re-syncing the game for all players.

Press "Continue" to continue playing without the desynced players. They will continue in a separate game session.
+
Press "Quit" to exit the game.
+ +
+
{{ line }}
+
+ + + +
{{ countdowntext }}
+
+
+
+ +
Load Game
+
Continue
+
Quit
+
+ +
diff --git a/ui/desyncwait.rcss b/ui/desyncwait.rcss new file mode 100644 index 000000000..3ca80e91f --- /dev/null +++ b/ui/desyncwait.rcss @@ -0,0 +1,4 @@ +/* The waiting player's out-of-sync screen, IDD_DESYNC_WAIT. The template gives it the quit + button alone, in the middle position at 145 dialog units, and disables it: the quit comes + back once the stall has lasted long enough to be worth abandoning. */ +#quit { left: 217.5dp; } diff --git a/ui/desyncwait.rml b/ui/desyncwait.rml new file mode 100644 index 000000000..bcf858366 --- /dev/null +++ b/ui/desyncwait.rml @@ -0,0 +1,39 @@ + + + Synchronization error + + + + + + +
+ + +
Players:
+
+
+
{{ entry.mark }}
+
{{ entry.name }}
+
{{ entry.status }}
+
+
+ +
The game has gone out of sync.

If there are saves available from this session, the game host can attempt to load a save to re-sync the game.

Alternatively, the host can choose for the desynced players to continue playing in separate game sessions.
+
Please wait while the host is making a decision.
+ +
+
{{ line }}
+
+ + + +
{{ countdowntext }}
+
+
+
+ +
Quit
+
+ +
diff --git a/ui/display.rcss b/ui/display.rcss new file mode 100644 index 000000000..ca5387e31 --- /dev/null +++ b/ui/display.rcss @@ -0,0 +1,65 @@ +/* The display options. Geometry from the IDD_OPT_DISPLAY template, converted from dialog + units at the 8 point MS Sans Serif the template names: 1.5 pixels across and 1.625 down, + with a child's offset taken from the panel's content box and the panel's declared size + taken inside its own border. */ + +/* 229 x 196 dialog units, so 343.5 x 318.5 pixels, centred on the frame. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -171.75dp; + margin-top: -159.25dp; + + width: 339.5dp; + height: 314.5dp; +} + +/* Two CTEXT captions with SS_CENTERIMAGE, 185 dialog units wide at x 22. */ +.caption +{ + left: 31dp; + width: 277.5dp; + text-align: center; +} + +#title { top: 17.5dp; height: 14.625dp; line-height: 14.625dp; } +#reslabel { top: 38.625dp; height: 16.25dp; line-height: 16.25dp; } + +/* The resolution list, 185 x 110 dialog units at 22, 37, one row shorter to make room for + the full screen check the template has no control for. */ +#reslist +{ + left: 31dp; + top: 58.125dp; + width: 277.5dp; + height: 162.5dp; +} + +/* A row spans the list less its scrollbar. */ +#reslist .row { width: 265.5dp; } + +/* The movie stretching check box, 185 x 10 dialog units at 22, 153, moved up one row, and + the full screen check in the place it used to hold. */ +#stretch, #fullscreen +{ + left: 31dp; + width: 277.5dp; + height: 16.25dp; + line-height: 16.25dp; +} + +#stretch { top: 230.375dp; } +#fullscreen { top: 246.625dp; } + +/* OK and Cancel, both 62 x 14 dialog units on the same row. */ +.button +{ + top: 274.25dp; + width: 93dp; + height: 22.75dp; + line-height: 22.75dp; +} + +#ok { left: 31dp; } +#cancel { left: 215.5dp; } diff --git a/ui/display.rml b/ui/display.rml new file mode 100644 index 000000000..1c1649a44 --- /dev/null +++ b/ui/display.rml @@ -0,0 +1,20 @@ + + + Display options + + + + +
+
Display Options:
+
Resolution Modes
+
+
{{ mode.label }}
+
+
Stretch movies to fit resolution
+
Full screen
+
[[TXT_OK]]
+
[[TXT_CANCEL]]
+
+ +
diff --git a/ui/gamecontrols.rcss b/ui/gamecontrols.rcss new file mode 100644 index 000000000..cfd458e64 --- /dev/null +++ b/ui/gamecontrols.rcss @@ -0,0 +1,79 @@ +/* The game controls shown with no game running. Geometry from the IDD_OPT_CTRL_GAME_SP + template, whose name says single player but which is the screen the front end shows, + converted from dialog units at the 8 point MS Sans Serif the template names: 1.5 pixels + across and 1.625 down. */ + +/* 292 x 179 dialog units, so 438 x 290.875 pixels, centred on the frame. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -219dp; + margin-top: -145.4375dp; + + width: 434dp; + height: 286.875dp; +} + +/* Four track bars, 148 x 13 dialog units at x 80. */ +.slider +{ + left: 118dp; + width: 222dp; + height: 21.125dp; +} + + +#speed { top: 17.5dp; } +#scroll { top: 53.25dp; } +#detail { top: 89dp; } +#difficulty { top: 124.75dp; } + +/* The left captions are LTEXT with SS_CENTERIMAGE, 58 x 13 at x 22; the value captions are + RTEXT, 45 x 13 at x 229. */ +.caption +{ + height: 21.125dp; + line-height: 21.125dp; +} + +#speedlabel, #scrolllabel, #detaillabel, #difficultylabel +{ + left: 31dp; + width: 87dp; +} + +.value +{ + left: 341.5dp; + width: 67.5dp; + text-align: right; +} + +#speedlabel, #speedvalue { top: 17.5dp; } +#scrolllabel, #scrollvalue { top: 53.25dp; } +#detaillabel, #detailvalue { top: 89dp; } +#difficultylabel, #difficultyvalue { top: 124.75dp; } + +/* Five BS_FLAT check boxes in two columns. */ +.check +{ + height: 16.25dp; + line-height: 16.25dp; +} + +#cameotext { left: 31dp; top: 165.375dp; width: 186dp; } +#actionlines { left: 31dp; top: 191.375dp; width: 186dp; } +#edgescroll { left: 31dp; top: 217.375dp; width: 186dp; } +#tooltips { left: 217dp; top: 165.375dp; width: 192dp; } +#coasting { left: 217dp; top: 191.375dp; width: 192dp; } + +/* The one button, 130 x 14 dialog units at 81, 153. */ +#mainmenu +{ + left: 119.5dp; + top: 246.625dp; + width: 195dp; + height: 22.75dp; + line-height: 22.75dp; +} diff --git a/ui/gamecontrols.rml b/ui/gamecontrols.rml new file mode 100644 index 000000000..3ad32b20c --- /dev/null +++ b/ui/gamecontrols.rml @@ -0,0 +1,34 @@ + + + Game controls + + + + +
+
Game Speed
+ +
{{ speedtext }}
+ +
Scroll Rate
+ +
{{ scrolltext }}
+ +
Visual Details
+ +
{{ detailtext }}
+ +
Difficulty
+ +
{{ difficultytext }}
+ +
Cameo Text
+
Target Lines
+
Tooltips
+
Scroll Coasting
+
Edge Scrolling
+ + +
+ +
diff --git a/ui/gamecontrolsmp.rcss b/ui/gamecontrolsmp.rcss new file mode 100644 index 000000000..23e3f7e30 --- /dev/null +++ b/ui/gamecontrolsmp.rcss @@ -0,0 +1,78 @@ +/* The game controls shown during a local game. Geometry from the IDD_OPT_CTRL_GAME_MP + template, converted at 1.5 pixels across and 1.625 down. */ + +/* 292 x 175 dialog units, so 438 x 284.375 pixels, centred on the frame. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -219dp; + margin-top: -142.1875dp; + + width: 434dp; + height: 280.375dp; +} + +/* Three track bars, 128 x 15 dialog units at x 90. */ +.slider +{ + left: 133dp; + width: 192dp; + height: 24.375dp; +} + + +#speed { top: 17.5dp; } +#scroll { top: 67.875dp; } +#detail { top: 118.25dp; } + +/* The left captions are RTEXT, 63 x 15 at x 22; the value captions are LTEXT, 50 x 15 at + x 224. Both carry SS_CENTERIMAGE. */ +.caption +{ + height: 24.375dp; + line-height: 24.375dp; +} + +#speedlabel, #scrolllabel, #detaillabel +{ + left: 31dp; + width: 94.5dp; + text-align: right; +} + +.value +{ + left: 334dp; + width: 75dp; +} + +#speedlabel, #speedvalue { top: 17.5dp; } +#scrolllabel, #scrollvalue { top: 67.875dp; } +#detaillabel, #detailvalue { top: 118.25dp; } + +/* Five BS_FLAT check boxes in two columns. */ +.check +{ + height: 16.25dp; + line-height: 16.25dp; +} + +#cameotext { left: 31dp; top: 150.75dp; width: 178.5dp; } +#actionlines { left: 31dp; top: 180dp; width: 178.5dp; } +#edgescroll { left: 31dp; top: 209.25dp; width: 178.5dp; } +#tooltips { left: 218.5dp; top: 150.75dp; width: 190.5dp; } +#coasting { left: 218.5dp; top: 180dp; width: 190.5dp; } + +/* Three buttons, all 77 x 14 dialog units on the same row. */ +.button +{ + top: 240.125dp; + width: 115.5dp; + height: 22.75dp; + line-height: 22.75dp; +} + +#sound { left: 31dp; } +#keyboard { left: 161.5dp; } +#accept { left: 292dp; } diff --git a/ui/gamecontrolsmp.rml b/ui/gamecontrolsmp.rml new file mode 100644 index 000000000..ad29f12a9 --- /dev/null +++ b/ui/gamecontrolsmp.rml @@ -0,0 +1,32 @@ + + + Game controls + + + + +
+
Game Speed:
+ +
{{ speedtext }}
+ +
Scroll Rate:
+ +
{{ scrolltext }}
+ +
Visual Details:
+ +
{{ detailtext }}
+ +
Sidebar Text
+
Target Lines
+
Tooltips
+
Scroll Coasting
+
Edge Scrolling
+ +
Sound
+
Keyboard
+
[[TXT_OPTIONS_MENU]]
+
+ +
diff --git a/ui/gamecontrolswol.rcss b/ui/gamecontrolswol.rcss new file mode 100644 index 000000000..0dad687cf --- /dev/null +++ b/ui/gamecontrolswol.rcss @@ -0,0 +1,78 @@ +/* The game controls shown during an internet game. Geometry from the IDD_OPT_CTRL_GAME_WOL + template, converted at 1.5 pixels across and 1.625 down. The template carries no game + speed slider, because a speed change in an internet session is an event every player has + to agree on rather than a setting one of them holds. */ + +/* 294 x 144 dialog units, so 441 x 234 pixels, centred on the frame. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -220.5dp; + margin-top: -117dp; + + width: 437dp; + height: 230dp; +} + +/* Two track bars, 128 x 15 dialog units at x 90. */ +.slider +{ + left: 133dp; + width: 192dp; + height: 24.375dp; +} + + +#scroll { top: 17.5dp; } +#detail { top: 67.875dp; } + +/* The left captions are RTEXT, 63 x 15 at x 22; the value captions are LTEXT, 50 x 15 at + x 226. Both carry SS_CENTERIMAGE. */ +.caption +{ + height: 24.375dp; + line-height: 24.375dp; +} + +#scrolllabel, #detaillabel +{ + left: 31dp; + width: 94.5dp; + text-align: right; +} + +.value +{ + left: 337dp; + width: 75dp; +} + +#scrolllabel, #scrollvalue { top: 17.5dp; } +#detaillabel, #detailvalue { top: 67.875dp; } + +/* Five BS_FLAT check boxes in two columns. */ +.check +{ + height: 16.25dp; + line-height: 16.25dp; +} + +#cameotext { left: 31dp; top: 100.375dp; width: 178.5dp; } +#actionlines { left: 31dp; top: 129.625dp; width: 178.5dp; } +#edgescroll { left: 31dp; top: 158.875dp; width: 178.5dp; } +#tooltips { left: 218.5dp; top: 100.375dp; width: 193.5dp; } +#coasting { left: 218.5dp; top: 129.625dp; width: 193.5dp; } + +/* Three buttons, all 77 x 14 dialog units on the same row. */ +.button +{ + top: 189.75dp; + width: 115.5dp; + height: 22.75dp; + line-height: 22.75dp; +} + +#sound { left: 31dp; } +#keyboard { left: 161.5dp; } +#accept { left: 292dp; } diff --git a/ui/gamecontrolswol.rml b/ui/gamecontrolswol.rml new file mode 100644 index 000000000..b34b2fa65 --- /dev/null +++ b/ui/gamecontrolswol.rml @@ -0,0 +1,28 @@ + + + Game controls + + + + +
+
Scroll Rate:
+ +
{{ scrolltext }}
+ +
Visual Details:
+ +
{{ detailtext }}
+ +
Sidebar Text
+
Target Lines
+
Tooltips
+
Scroll Coasting
+
Edge Scrolling
+ +
Sound
+
Keyboard
+
[[TXT_OPTIONS_MENU]]
+
+ +
diff --git a/ui/gamelist.rcss b/ui/gamelist.rcss new file mode 100644 index 000000000..539748609 --- /dev/null +++ b/ui/gamelist.rcss @@ -0,0 +1,51 @@ +/* The network game list. Geometry from the IDD_MPLAYER_GAME_LIST template, converted from + dialog units at the 8 point MS Sans Serif the template names: 1.5 pixels across and 1.625 + down, with a child's offset taken from the panel's content box and the panel's declared + size taken inside its own border. */ + +/* 426 x 240 dialog units, so 639 x 390 pixels, centred on the frame. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -319.5dp; + margin-top: -195dp; + + width: 635dp; + height: 386dp; +} + +/* "Your Name:" at 24, 9 and the EDITTEXT beside it, 68 x 12 dialog units at 89, 8. */ +#namelabel { left: 34dp; top: 12.625dp; width: 93dp; height: 16.25dp; line-height: 16.25dp; } +#yourname { left: 131.5dp; top: 11dp; width: 102dp; height: 19.5dp; line-height: 15.5dp; } + +/* "Games:" at 296, 14 and its 113 x 68 list at 294, 27. */ +#gameslabel { left: 442dp; top: 20.75dp; width: 75dp; height: 16.25dp; line-height: 16.25dp; } +#games { left: 439dp; top: 41.875dp; width: 169.5dp; height: 110.5dp; } + +/* "Players:" at 296, 99 and its 113 x 100 list at 294, 111. */ +#playerslabel { left: 442dp; top: 158.875dp; width: 75dp; height: 16.25dp; line-height: 16.25dp; } +#users { left: 439dp; top: 178.375dp; width: 169.5dp; height: 162.5dp; } + +/* A row states its own width, because a scrolling container gives its children none to be + a proportion of. The lists are 113 dialog units wide, less the scrollbar. */ +#games .row, +#users .row +{ + width: 157.5dp; + height: 16dp; + line-height: 16dp; +} + +/* The message log, 266 x 166 dialog units at 19, 27, and the chat entry below it. */ +#messages { left: 26.5dp; top: 41.875dp; width: 399dp; height: 269.75dp; } +#messages .line { width: 387dp; } + +#say { left: 26.5dp; top: 319.75dp; width: 399dp; height: 19.5dp; line-height: 15.5dp; } + +/* Cancel, Join and New, all 62 x 18 dialog units on the same row. */ +.button { height: 29.25dp; line-height: 29.25dp; width: 93dp; } + +#cancel { left: 269.5dp; top: 345.75dp; } +#join { left: 394dp; top: 345.75dp; } +#new { left: 515.5dp; top: 345.75dp; } diff --git a/ui/gamelist.rml b/ui/gamelist.rml new file mode 100644 index 000000000..feb3fd671 --- /dev/null +++ b/ui/gamelist.rml @@ -0,0 +1,34 @@ + + + Network games + + + + + +
+
Your Name:
+ + +
Games:
+
+
{{ entry.label }}
+
+ +
Players:
+
+
{{ entry.name }}
+
+ +
+
{{ entry.text }}
+
+ + + +
[[TXT_CANCEL]]
+
Join
+
New
+
+ +
diff --git a/ui/gameoptions.rcss b/ui/gameoptions.rcss new file mode 100644 index 000000000..96eae2437 --- /dev/null +++ b/ui/gameoptions.rcss @@ -0,0 +1,33 @@ +/* The in-game options a solo mission or a skirmish shows. Geometry from the IDD_OPT_CTRL_SP + template, converted from dialog units at the 8 point MS Sans Serif the template names: + 1.5 pixels across and 1.625 down, with a child's offset taken from the panel's content + box and the panel's declared size taken inside its own border. */ + +/* 209 x 140 dialog units, so 313.5 x 227.5 pixels, centred on the frame. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -156.75dp; + margin-top: -113.75dp; + + width: 309.5dp; + height: 223.5dp; +} + +/* Seven buttons, all 115 x 14 dialog units at x 47. */ +.button +{ + left: 68.5dp; + width: 172.5dp; + height: 22.75dp; + line-height: 22.75dp; +} + +#settings { top: 17.5dp; } +#briefing { top: 45.125dp; } +#load { top: 72.75dp; } +#save { top: 100.375dp; } +#delete { top: 128dp; } +#abort { top: 155.625dp; } +#resume { top: 183.25dp; } diff --git a/ui/gameoptions.rml b/ui/gameoptions.rml new file mode 100644 index 000000000..1c8d4f7a9 --- /dev/null +++ b/ui/gameoptions.rml @@ -0,0 +1,18 @@ + + + In-game options + + + + +
+
Game Controls
+
Restate Briefing
+
Load Game
+
Save Game
+
Delete Game
+
Abort Mission
+
[[TXT_RESUME_MISSION]]
+
+ +
diff --git a/ui/gameoptionsmp.rcss b/ui/gameoptionsmp.rcss new file mode 100644 index 000000000..04e2b1de5 --- /dev/null +++ b/ui/gameoptionsmp.rcss @@ -0,0 +1,27 @@ +/* The in-game options a local session shows: three buttons and no saving. Geometry from the + IDD_OPT_CTRL_MP template, converted at 1.5 pixels across and 1.625 down. */ + +/* 209 x 75 dialog units, so 313.5 x 121.875 pixels, centred on the frame. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -156.75dp; + margin-top: -60.9375dp; + + width: 309.5dp; + height: 117.875dp; +} + +/* Three buttons, all 99 x 14 dialog units at x 55. */ +.button +{ + left: 80.5dp; + width: 148.5dp; + height: 22.75dp; + line-height: 22.75dp; +} + +#settings { top: 17.5dp; } +#abort { top: 46.75dp; } +#resume { top: 76dp; } diff --git a/ui/gameoptionsmp.rml b/ui/gameoptionsmp.rml new file mode 100644 index 000000000..cc3c74211 --- /dev/null +++ b/ui/gameoptionsmp.rml @@ -0,0 +1,14 @@ + + + In-game options + + + + +
+
Game Controls
+
Abort Mission
+
[[TXT_RESUME_MISSION]]
+
+ +
diff --git a/ui/gameoptionswol.rcss b/ui/gameoptionswol.rcss new file mode 100644 index 000000000..ac8d69b06 --- /dev/null +++ b/ui/gameoptionswol.rcss @@ -0,0 +1,93 @@ +/* The in-game options an internet session shows: the five buttons plus the game speed and + connection quality sliders. Geometry from the IDD_OPT_CTRL_WOL template, converted at 1.5 + pixels across and 1.625 down. */ + +/* 340 x 185 dialog units, so 510 x 300.625 pixels, centred on the frame. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -255dp; + margin-top: -150.3125dp; + + width: 506dp; + height: 296.625dp; +} + +/* Five buttons, all 99 x 14 dialog units at x 120. */ +.button +{ + left: 178dp; + width: 148.5dp; + height: 22.75dp; + line-height: 22.75dp; +} + +#settings { top: 11dp; } +#load { top: 38.625dp; } +#save { top: 66.25dp; } +#abort { top: 93.875dp; } +#resume { top: 121.5dp; } + +/* The group box around the two sliders, 283 x 68 dialog units at 28, 92. Its caption sits + on the top edge, which is how the owner-draw group box drew it. */ +#group +{ + display: block; + position: absolute; + box-sizing: border-box; + left: 40dp; + top: 147.5dp; + width: 424.5dp; + height: 110.5dp; + line-height: 14dp; + padding-left: 8dp; + + color: #70ff00; + border-width: 1dp; + border-color: #ffffff; +} + +/* Two track bars, 148 x 13 dialog units at x 95. */ +.slider +{ + left: 140.5dp; + width: 222dp; + height: 21.125dp; +} + + +#connection { top: 176.75dp; } +#speed { top: 212.5dp; } + +/* The left captions are LTEXT with SS_CENTERIMAGE, 58 x 13 at x 39; the value captions are + RTEXT, 45 x 13 at x 247. */ +.caption +{ + height: 21.125dp; + line-height: 21.125dp; +} + +#connectionlabel, #speedlabel +{ + left: 56.5dp; + width: 87dp; +} + +.value +{ + left: 368.5dp; + width: 67.5dp; + text-align: right; +} + +#connectionlabel, #connectionvalue { top: 176.75dp; } +#speedlabel, #speedvalue { top: 212.5dp; } + +/* The group box caption, a static. + The dlgsys sheets, as optionsbase.rcss gives every other static. */ +#group +{ + font-family: dlgsys; + font-size: 18dp; +} diff --git a/ui/gameoptionswol.rml b/ui/gameoptionswol.rml new file mode 100644 index 000000000..914c1dd38 --- /dev/null +++ b/ui/gameoptionswol.rml @@ -0,0 +1,26 @@ + + + In-game options + + + + +
+
Game Controls
+
Load Game
+
Save Game
+
Abort Mission
+
[[TXT_RESUME_MISSION]]
+ +
Internet Game Controls
+ +
Connection
+ +
{{ connectionlabel }}
+ +
Game Speed
+ +
{{ speedlabel }}
+
+ +
diff --git a/ui/gametype.rcss b/ui/gametype.rcss new file mode 100644 index 000000000..c77366751 --- /dev/null +++ b/ui/gametype.rcss @@ -0,0 +1,41 @@ +/* The game type choice. Geometry from the IDD_SELECT_GAME_TYPE template, converted from + dialog units at the 8 point MS Sans Serif the template names: 1.5 pixels across and 1.625 + down, with a child's offset taken from the panel's content box and the panel's declared + size taken inside its own border. */ + +/* 228 x 108 dialog units, so 342 x 175.5 pixels. This is the one screen of the family its + driver does not move, so it is centred both ways. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -171dp; + margin-top: -87.75dp; + + width: 338dp; + height: 171.5dp; +} + +/* CTEXT with SS_CENTERIMAGE, 184 x 19 dialog units at 22, 12. */ +#question +{ + left: 31dp; + top: 17.5dp; + width: 276dp; + height: 30.875dp; + line-height: 30.875dp; + text-align: center; +} + +/* Three buttons, all 104 x 14 dialog units at x 62. */ +.button +{ + left: 91dp; + width: 156dp; + height: 22.75dp; + line-height: 22.75dp; +} + +#original { top: 66.25dp; } +#firestorm { top: 98.75dp; } +#back { top: 131.25dp; } diff --git a/ui/gametype.rml b/ui/gametype.rml new file mode 100644 index 000000000..73b2d36e4 --- /dev/null +++ b/ui/gametype.rml @@ -0,0 +1,15 @@ + + + Select game type + + + + +
+
Select Game Type
+
Tiberian Sun (Original)
+
Firestorm
+
Main Menu
+
+ +
diff --git a/ui/keyboard.rcss b/ui/keyboard.rcss new file mode 100644 index 000000000..b1d09fb94 --- /dev/null +++ b/ui/keyboard.rcss @@ -0,0 +1,191 @@ +/* The keyboard screen. Geometry from the IDD_OPT_KEYBOARD template, converted from dialog + units at the 8 point MS Sans Serif the template names: 1.5 pixels across and 1.625 down, + with a child's offset taken from the panel's content box and the panel's declared size + taken inside its own border. */ + +/* 336 x 208 dialog units, so 504 x 338 pixels, centred on the frame. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -252dp; + margin-top: -169dp; + + width: 500dp; + height: 334dp; +} + +/* The CTEXT title, 292 x 11 dialog units at 22, 12. */ +#title +{ + left: 31dp; + top: 17.5dp; + width: 438dp; + height: 17.875dp; + line-height: 17.875dp; + text-align: center; +} + +/* The two column captions and the four label and value pairs, all LTEXT. */ +#categorylabel { left: 31dp; top: 41.875dp; width: 219dp; height: 14.625dp; line-height: 14.625dp; } +#commandlabel { left: 250dp; top: 41.875dp; width: 219dp; height: 13dp; line-height: 13dp; } +#capturelabel { left: 31dp; top: 191.375dp; width: 192dp; height: 14.625dp; line-height: 14.625dp; } +#assignedlabel { left: 31dp; top: 243.375dp; width: 219dp; height: 17.875dp; line-height: 17.875dp; } +#shortcutlabel { left: 250dp; top: 243.375dp; width: 219dp; height: 17.875dp; line-height: 17.875dp; } +#assignedto { left: 31dp; top: 267.75dp; width: 219dp; height: 16.25dp; line-height: 16.25dp; } +#shortcut { left: 250dp; top: 267.75dp; width: 219dp; height: 16.25dp; line-height: 16.25dp; } + +/* The category combo box, 138 dialog units wide at 22, 42. The template's 146 is how far + the list drops, not how tall the control is: an owner-draw CBS_DROPDOWNLIST sizes its + closed rectangle from the item height ownrdraw.cpp sets, which is the 14 pixel dialog + font plus two, inside a two pixel border. */ +#category +{ + display: block; + position: absolute; + box-sizing: border-box; + left: 31dp; + top: 66.25dp; + width: 207dp; + height: 20dp; + + color: #70ff00; + background-color: #000000b4; + border-width: 1dp; + border-color: #ffffff; +} + +#category selectvalue +{ + width: auto; + margin-right: 16dp; + height: 16dp; + line-height: 16dp; + padding: 0dp 4dp; + white-space: nowrap; + overflow: hidden; +} + +#category selectarrow +{ + width: 16dp; + height: 16dp; + + decorator: image(dnarrowr.pcx); +} + +/* The dropped list, capped at the 146 dialog units the template gives it. */ +#category selectbox +{ + width: 203dp; + max-height: 237.25dp; + overflow-y: auto; + + background-color: #000000b4; + border-width: 1dp; + border-color: #ffffff; +} + +#category selectbox option +{ + width: auto; + height: 20dp; + line-height: 20dp; + padding: 0dp 4dp; + white-space: nowrap; + color: #70ff00; +} + +#category selectbox option:hover { color: #70ff00; } +#category selectbox option:checked +{ + background-color: #225061; + color: #70ff00; +} + +/* The command list, 146 x 104 dialog units at 168, 41. */ +#commands +{ + left: 250dp; + top: 64.625dp; + width: 219dp; + height: 169dp; +} + +/* A row spans the list less its scrollbar. */ +#commands .row { width: 207dp; } + +/* The description group box, 138 x 57 dialog units at 22, 57, with its caption on the top + edge the way the owner-draw group box drew it, and the description text inside it at + 127 x 42 dialog units at 29, 68. */ +#descriptionbox +{ + display: block; + position: absolute; + box-sizing: border-box; + left: 31dp; + top: 90.625dp; + width: 207dp; + height: 92.625dp; + line-height: 14dp; + padding-left: 8dp; + + color: #70ff00; + border-width: 1dp; + border-color: #ffffff; +} + +#description +{ + display: block; + position: absolute; + box-sizing: border-box; + left: 41.5dp; + top: 108.5dp; + width: 190.5dp; + height: 68.25dp; + overflow: hidden; + color: #70ff00; +} + +/* The capture control, 85 x 14 dialog units at 22, 131. It stands where msctls_hotkey32 + stood: a bordered field that spells out the key it is holding and takes a keypress + whenever it has the focus. */ +#capture +{ + display: block; + position: absolute; + box-sizing: border-box; + left: 31dp; + top: 210.875dp; + width: 127.5dp; + height: 22.75dp; + line-height: 22.75dp; + padding: 0dp 4dp; + white-space: nowrap; + overflow: hidden; + + color: #70ff00; + background-color: #000000b4; + border-width: 1dp; + border-color: #ffffff; +} + +#capture:focus { background-color: #000000b4; } + +/* Assign, Reset All, OK and Cancel. */ +.button { height: 22.75dp; line-height: 22.75dp; } + +#assign { left: 163dp; top: 212.5dp; width: 75dp; } +#reset { left: 31dp; top: 292.125dp; width: 81dp; } +#ok { left: 278.5dp; top: 292.125dp; width: 75dp; } +#cancel { left: 394dp; top: 292.125dp; width: 75dp; } + +/* Both statics. #capture stands where msctls_hotkey32 stood and keeps the system face. + The dlgsys sheets, as optionsbase.rcss gives every other static. */ +#descriptionbox, +#description +{ + font-family: dlgsys; + font-size: 18dp; +} diff --git a/ui/keyboard.rml b/ui/keyboard.rml new file mode 100644 index 000000000..a9cc1c90c --- /dev/null +++ b/ui/keyboard.rml @@ -0,0 +1,38 @@ + + + Customize keyboard + + + + +
+
Customize Keyboard
+ +
Category:
+ + +
Commands:
+
+
{{ command.label }}
+
+ +
Description:
+
{{ description }}
+ +
Press new shortcut key:
+
{{ capturedtext }}
+
Assign
+ +
Currently assigned to:
+
Current shortcut:
+
{{ assignedto }}
+
{{ shortcut }}
+ +
Reset All
+
[[TXT_OK]]
+
[[TXT_CANCEL]]
+
+ +
diff --git a/ui/lobbybase.rcss b/ui/lobbybase.rcss new file mode 100644 index 000000000..e595f6803 --- /dev/null +++ b/ui/lobbybase.rcss @@ -0,0 +1,189 @@ +/* What the three network lobby documents share. Only the look lives here; each document's + own stylesheet carries its geometry, converted from its dialog template's units. + + The palette and the raised and sunken borders are the ones ui/optionsbase.rcss + established for this family of dialogs. The three templates are all 426 x 240 dialog + units, the same as IDD_SKIRMISH, so the conversion is the same 1.5 pixels across and + 1.625 down. + + The lobby is list heavy: the game list, the player list and the message log are all + scrolling containers, and a scrolling container gives its children no width to be a + proportion of, so every row here states its own. */ + +/* The chat and system message log, which the templates give a LBS_NOSEL list box. A line + is kept whole in the model and wrapped here, which is what _DrawMessage did to the width + of the list box it was handed. */ +.log +{ + display: block; + position: absolute; + box-sizing: border-box; + + background-color: #000000b4; + overflow-y: auto; + overflow-x: hidden; +} + +.log .line +{ + display: block; + box-sizing: border-box; + padding: 0dp 2dp; + line-height: 14dp; + color: #70ff00; +} + +/* The player list's own columns. The two setup dialogs register them with OD_ADDCOLUMN at + widths 45, 25 and 5, and the item's own string is column zero. */ +.users .row +{ + position: relative; + height: 16dp; + line-height: 16dp; + padding: 0dp; +} + +.users .name +{ + display: block; + position: absolute; + left: 2dp; + top: 0dp; + width: 108dp; + white-space: nowrap; + overflow: hidden; +} + +.users .side +{ + display: block; + position: absolute; + left: 112dp; + top: 0dp; + width: 37dp; + white-space: nowrap; + overflow: hidden; + color: #70ff00; +} + +/* The host and accepted markers, which the list drew as the wolhost.pcx and wolacpt.pcx + surfaces. PCX decoding is not here yet, so the marker is a character in the same column + the surface stood in. */ +.users .mark +{ + display: block; + position: absolute; + left: 152dp; + top: 0dp; + width: 20dp; + text-align: center; + color: #70ff00; +} + +/* The chat entry, where the templates put an EDITTEXT. It states a width, because a field + with none formats no line and RmlUi's End key then moves the caret to the start of an + empty line rather than to the end of the value. */ +.field +{ + display: block; + position: absolute; + box-sizing: border-box; + white-space: nowrap; + overflow: hidden; + padding: 0dp 4dp; + + font-family: LatoLatin; + color: #70ff00; + background-color: #000000b4; + border-width: 1dp; + border-color: #ffffff; +} + +.field:focus { background-color: #000000b4; } + +/* A CBS_DROPDOWNLIST combo, sized the way ownrdraw.cpp sizes one: the item height, which is + the 14 pixel dialog font plus two, inside a two pixel border. The template's own height is + how far the list drops. */ +.combo +{ + display: block; + position: absolute; + box-sizing: border-box; + height: 20dp; + + color: #70ff00; + background-color: #000000b4; + border-width: 1dp; + border-color: #ffffff; +} + +.combo selectvalue +{ + width: auto; + margin-right: 16dp; + height: 16dp; + line-height: 16dp; + padding: 0dp 4dp; + white-space: nowrap; + overflow: hidden; +} + +.combo selectarrow +{ + width: 16dp; + height: 16dp; + + decorator: image(dnarrowr.pcx); +} + +.combo selectbox +{ + width: 114dp; + overflow-y: auto; + + background-color: #000000b4; + border-width: 1dp; + border-color: #ffffff; +} + +.combo selectbox option +{ + width: auto; + height: 18dp; + line-height: 18dp; + padding: 0dp 4dp; + white-space: nowrap; + color: #70ff00; +} + +.combo selectbox option:hover { color: #70ff00; } +.combo selectbox option:checked { background-color: #225061; } + +/* The preview frame, a GROUPBOX 126 x 73 dialog units at 281, 138 on both setup templates. + The picture inside it is drawn by the screen and reaches the document through the + element, which takes its size from the provider. */ +.previewframe +{ + display: block; + position: absolute; + box-sizing: border-box; + left: 419.5dp; + top: 222.25dp; + width: 189dp; + height: 118.625dp; + + background-color: #000000b4; + border-width: 1dp; + border-color: #ffffff; +} + +.previewframe surface +{ + display: block; + position: absolute; + left: 0dp; + top: 0dp; +} + +/* A caption the template writes right aligned, which every track bar label is. */ +.rcaption { text-align: right; } diff --git a/ui/mainmenu.rcss b/ui/mainmenu.rcss new file mode 100644 index 000000000..f8c6ad4c3 --- /dev/null +++ b/ui/mainmenu.rcss @@ -0,0 +1,36 @@ +/* The main menu. Geometry from the IDD_MAIN_MENU template, converted from dialog units at + the 8 point MS Sans Serif the template names: 1.5 pixels across and 1.625 down, with a + child's offset taken from the panel's content box and the panel's declared size taken + inside its own border. */ + +/* 204 x 147 dialog units, so 306 x 238.875 pixels. + + The panel is not centred vertically. Its driver moved it with + Move_Dialog(handle, -1, (HiddenSurface->Get_Height() - 400) / 2 + 147), which is half the + frame height less 53, so it sits 53 dp above the middle whatever the frame height is. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -153dp; + margin-top: -53dp; + + width: 302dp; + height: 234.875dp; +} + +/* Six buttons, all 130 x 18 dialog units at x 37. */ +.button +{ + left: 53.5dp; + width: 195dp; + height: 29.25dp; + line-height: 29.25dp; +} + +#campaign { top: 17.5dp; } +#load { top: 51.625dp; } +#multiplayer { top: 85.75dp; } +#intro { top: 119.875dp; } +#options { top: 154dp; } +#exit { top: 188.125dp; } diff --git a/ui/mainmenu.rml b/ui/mainmenu.rml new file mode 100644 index 000000000..5b6156f80 --- /dev/null +++ b/ui/mainmenu.rml @@ -0,0 +1,17 @@ + + + Main menu + + + + +
+
New Campaign
+
Load Mission
+
Multiplayer Game
+
Intro / Sneak Peek
+
Options
+
Exit Game
+
+ +
diff --git a/ui/mapgen.rcss b/ui/mapgen.rcss new file mode 100644 index 000000000..9d88b7f48 --- /dev/null +++ b/ui/mapgen.rcss @@ -0,0 +1,60 @@ +/* The base game's map generator, IDD_MAPGEN: 424 x 242 dialog units, so 636 x 393.25 pixels. + It has no Firestorm settings, its two combos start further across than the other two + templates put theirs, and its seed field sits beside the map size lists. */ + +#dialog +{ + left: 50%; + top: 50%; + margin-left: -318dp; + margin-top: -196.625dp; + + width: 632dp; + height: 389.25dp; +} + +#environmentlabel { left: 33dp; top: 13dp; width: 99dp; height: 22.75dp; line-height: 22.75dp; } +#timelabel { left: 33dp; top: 43.875dp; width: 99dp; height: 22.75dp; line-height: 22.75dp; } +#playerslabel { left: 33dp; top: 76.375dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#cliffslabel { left: 33dp; top: 107.25dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#accessibilitylabel { left: 33dp; top: 138.125dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#hillslabel { left: 33dp; top: 169dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#tiberiumlabel { left: 33dp; top: 199.875dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#tiberiumfieldslabel { left: 33dp; top: 230.75dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#waterlabel { left: 33dp; top: 261.625dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#vegetationlabel { left: 33dp; top: 292.5dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#citieslabel { left: 33dp; top: 323.375dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#widthlabel { left: 309dp; top: 13dp; width: 97.5dp; height: 19.5dp; line-height: 19.5dp; } +#heightlabel { left: 307.5dp; top: 40.625dp; width: 97.5dp; height: 19.5dp; line-height: 19.5dp; } + +/* The nine track bars, all at 95 dialog units across. */ +#players { left: 142.5dp; top: 76.375dp; } +#cliffs { left: 142.5dp; top: 107.25dp; } +#accessibility { left: 142.5dp; top: 138.125dp; } +#hills { left: 142.5dp; top: 169dp; } +#tiberium { left: 142.5dp; top: 199.875dp; } +#tiberiumfields { left: 142.5dp; top: 230.75dp; } +#water { left: 142.5dp; top: 261.625dp; } +#vegetation { left: 142.5dp; top: 292.5dp; } +#cities { left: 142.5dp; top: 323.375dp; } + +/* The two sorted combos and the two size combos. */ +#biome { left: 142.5dp; top: 13dp; width: 150dp; } +#time { left: 142.5dp; top: 40.625dp; width: 150dp; } +#width { left: 415.5dp; top: 13dp; width: 94.5dp; } +#height { left: 415.5dp; top: 40.625dp; width: 94.5dp; } + +#seed { left: 517.5dp; top: 40.625dp; width: 85.5dp; height: 19.5dp; line-height: 19.5dp; } + +#previewframe { left: 307.5dp; top: 76.375dp; } +#previewword { top: 87.75dp; height: 26dp; line-height: 26dp; } + +#surprise { left: 307.5dp; top: 313.625dp; } +#previewmap { left: 471dp; top: 313.625dp; } + +/* The bottom row: save, load, delete, then OK and cancel. */ +#save { left: 33dp; } +#load { left: 150dp; } +#delete { left: 267dp; } +#ok { left: 397.5dp; } +#cancel { left: 505.5dp; } diff --git a/ui/mapgen.rml b/ui/mapgen.rml new file mode 100644 index 000000000..08c56128c --- /dev/null +++ b/ui/mapgen.rml @@ -0,0 +1,62 @@ + + + Random map + + + + + +
+
Environment:
+ +
Time of Day:
+ +
Map Width:
+ +
Map Height:
+ + + +
Players:
+ +
Cliffs:
+ +
Accessability:
+ +
Hills:
+ +
Tiberium Amount:
+ +
Tiberium Fields:
+ +
Water:
+ +
Vegetation:
+ +
Cities:
+ + +
+
Preview
+ +
+ +
Surprise Me
+
Preview Map
+ +
Save Map
+
Load Map
+
Delete Map
+
[[TXT_OK]]
+
[[TXT_CANCEL]]
+
+ +
diff --git a/ui/mapgenbase.rcss b/ui/mapgenbase.rcss new file mode 100644 index 000000000..c4f934b2d --- /dev/null +++ b/ui/mapgenbase.rcss @@ -0,0 +1,158 @@ +/* What the three map generator documents share. Only the look lives here; each document's + own stylesheet carries its geometry, converted from its dialog template's units at the + 8 point MS Sans Serif they name: 1.5 pixels across and 1.625 down, with a child's offset + taken from the panel's content box and the panel's declared size taken inside its own + border. + + The palette and the raised and sunken borders are the ones ui/optionsbase.rcss + established for this family of dialogs. */ + +/* A caption the templates give SS_CENTERIMAGE, so its text sits in the middle of the box + the template declares rather than at the top of it. */ +.caption { color: #70ff00; } + +/* The preview frame, a GROUPBOX 197 x 134 dialog units on all three templates. The picture + inside it is drawn by the screen and reaches the document through the element, + which takes its size from the provider. */ +#previewframe +{ + display: block; + position: absolute; + box-sizing: border-box; + width: 295.5dp; + height: 217.75dp; + + background-color: #000000b4; + border-width: 1dp; + border-color: #ffffff; +} + +#previewframe surface +{ + display: block; + position: absolute; + left: 0dp; + top: 0dp; +} + +/* The word the template writes across the middle of an empty frame. */ +#previewword +{ + display: block; + position: absolute; + left: 0dp; + width: 100%; + text-align: center; + color: #909090; +} + +/* The seed field, an ES_NUMBER edit all three templates declare NOT WS_VISIBLE and + WS_DISABLED. Nothing ever shows it: Set_Settings writes the seed into it and Get_Settings + reads the seed back out, so it is where the number lives between the two rather than + something the player types in. It keeps its place and its width here -- a field with no + width formats no line, and RmlUi's End key then moves the caret to the start of an empty + line rather than to the end of the value -- and the document hides it as the template + does. */ +#seed +{ + display: none; + position: absolute; + box-sizing: border-box; + white-space: nowrap; + overflow: hidden; + padding: 0dp 4dp; + + font-family: LatoLatin; + color: #70ff00; + background-color: #000000b4; + border-width: 1dp; + border-color: #ffffff; +} + +#seed:focus { background-color: #000000b4; } + +/* A control the tournament territory has fixed. It is shown rather than hidden, so the + screen keeps its shape whatever the territory allows. */ +.locked +{ + color: #909090; + pointer-events: none; +} + +/* The ten track bars, all 100 x 14 dialog units. */ +.slider { width: 150dp; height: 22.75dp; } + +/* The five combo boxes. A CBS_DROPDOWNLIST is sized the way ownrdraw.cpp sizes one: the + item height, which is the 14 pixel dialog font plus two, inside a two pixel border. The + template's own height is how far the list drops. */ +.combo +{ + display: block; + position: absolute; + box-sizing: border-box; + height: 20dp; + + color: #70ff00; + background-color: #000000b4; + border-width: 1dp; + border-color: #ffffff; +} + +.combo selectvalue +{ + width: auto; + margin-right: 16dp; + height: 16dp; + line-height: 16dp; + padding: 0dp 4dp; + white-space: nowrap; + overflow: hidden; +} + +.combo selectarrow +{ + width: 16dp; + height: 16dp; + + decorator: image(dnarrowr.pcx); +} + +.combo selectbox +{ + width: 100%; + overflow-y: auto; + + background-color: #000000b4; + border-width: 1dp; + border-color: #ffffff; +} + +.combo selectbox option +{ + width: auto; + height: 18dp; + line-height: 18dp; + padding: 0dp 4dp; + white-space: nowrap; + color: #70ff00; +} + +.combo selectbox option:hover { color: #70ff00; } +.combo selectbox option:checked { background-color: #225061; } + +/* The five buttons across the bottom row, 65 x 14 dialog units at y 220, and the two above + the preview, 88 x 14. */ +.footbutton { top: 357.5dp; width: 97.5dp; height: 22.75dp; line-height: 22.75dp; } +.previewbutton { width: 132dp; height: 22.75dp; line-height: 22.75dp; } + +/* A BS_AUTOCHECKBOX, which the Firestorm and tournament templates put down the right hand + side, 84 x 10 dialog units. */ +.check { width: 126dp; height: 16.25dp; line-height: 16.25dp; } + +/* The preview frame's caption, a static. + The dlgsys sheets, as optionsbase.rcss gives every other static. */ +#previewword +{ + font-family: dlgsys; + font-size: 18dp; +} diff --git a/ui/mapgenfs.rcss b/ui/mapgenfs.rcss new file mode 100644 index 000000000..b472d4252 --- /dev/null +++ b/ui/mapgenfs.rcss @@ -0,0 +1,65 @@ +/* The Firestorm map generator, IDD_MAPGEN_FS: 426 x 242 dialog units, so 639 x 393.25 + pixels. It adds the veinhole bar and the three Firestorm check boxes down the right hand + side, moves the two sorted combos left, and puts the seed field under the bars. */ + +#dialog +{ + left: 50%; + top: 50%; + margin-left: -319.5dp; + margin-top: -196.625dp; + + width: 635dp; + height: 389.25dp; +} + +#environmentlabel { left: 33dp; top: 11.375dp; width: 79.5dp; height: 22.75dp; line-height: 22.75dp; } +#timelabel { left: 33dp; top: 39dp; width: 79.5dp; height: 22.75dp; line-height: 22.75dp; } +#playerslabel { left: 33dp; top: 66.625dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#cliffslabel { left: 33dp; top: 94.25dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#accessibilitylabel { left: 33dp; top: 121.875dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#hillslabel { left: 33dp; top: 149.5dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#tiberiumlabel { left: 33dp; top: 177.125dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#tiberiumfieldslabel { left: 33dp; top: 204.75dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#waterlabel { left: 33dp; top: 232.375dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#vegetationlabel { left: 33dp; top: 260dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#citieslabel { left: 33dp; top: 287.625dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#veinholeslabel { left: 33dp; top: 315.25dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#widthlabel { left: 280.5dp; top: 13dp; width: 79.5dp; height: 19.5dp; line-height: 19.5dp; } +#heightlabel { left: 279dp; top: 40.625dp; width: 79.5dp; height: 19.5dp; line-height: 19.5dp; } +#ionstorms { left: 480dp; top: 13dp; width: 126dp; height: 16.25dp; line-height: 16.25dp; } +#transitions { left: 480dp; top: 42.25dp; width: 126dp; height: 16.25dp; line-height: 16.25dp; } +#lifeforms { left: 480dp; top: 71.5dp; width: 126dp; height: 16.25dp; line-height: 16.25dp; } + +/* The track bars. */ +#players { left: 142.5dp; top: 66.625dp; } +#cliffs { left: 142.5dp; top: 94.25dp; } +#accessibility { left: 142.5dp; top: 121.875dp; } +#hills { left: 142.5dp; top: 149.5dp; } +#tiberium { left: 142.5dp; top: 177.125dp; } +#tiberiumfields { left: 142.5dp; top: 204.75dp; } +#water { left: 142.5dp; top: 232.375dp; } +#vegetation { left: 142.5dp; top: 260dp; } +#cities { left: 142.5dp; top: 287.625dp; } +#veinholes { left: 142.5dp; top: 315.25dp; } + +/* The combo boxes. */ +#biome { left: 118.5dp; top: 13dp; width: 150dp; } +#time { left: 118.5dp; top: 40.625dp; width: 150dp; } +#width { left: 372dp; top: 13dp; width: 94.5dp; } +#height { left: 372dp; top: 42.25dp; width: 94.5dp; } + +#seed { left: 33dp; top: 334.75dp; width: 85.5dp; height: 19.5dp; line-height: 19.5dp; } + +#previewframe { left: 310.5dp; top: 92.625dp; } +#previewword { top: 107.25dp; height: 26dp; line-height: 26dp; } + +#surprise { left: 307.5dp; top: 325dp; } +#previewmap { left: 474dp; top: 325dp; } + +/* The bottom row: save, load, delete, then OK and cancel. */ +#save { left: 33dp; } +#load { left: 150dp; } +#delete { left: 267dp; } +#ok { left: 397.5dp; } +#cancel { left: 505.5dp; } diff --git a/ui/mapgenfs.rml b/ui/mapgenfs.rml new file mode 100644 index 000000000..09f77ffc0 --- /dev/null +++ b/ui/mapgenfs.rml @@ -0,0 +1,68 @@ + + + Random map + + + + + +
+
Environment:
+ +
Time of Day:
+ +
Map Width:
+ +
Map Height:
+ + + +
Players:
+ +
Cliffs:
+ +
Accessability:
+ +
Hills:
+ +
Tiberium Amount:
+ +
Tiberium Fields:
+ +
Water:
+ +
Vegetation:
+ +
Cities:
+ +
Veinholes:
+ + +
Ion Storms
+
Transitions
+
Lifeforms
+ +
+
Preview
+ +
+ +
Surprise Me
+
Preview Map
+ +
Save Map
+
Load Map
+
Delete Map
+
[[TXT_OK]]
+
[[TXT_CANCEL]]
+
+ +
diff --git a/ui/mapgenwdt.rcss b/ui/mapgenwdt.rcss new file mode 100644 index 000000000..8466f5783 --- /dev/null +++ b/ui/mapgenwdt.rcss @@ -0,0 +1,65 @@ +/* The tournament map generator, IDD_MAPGEN_WDT: 426 x 242 dialog units, so 639 x 393.25 + pixels. It drops the player count bar the territory fixes and puts the two team boxes in + its place; everything else stands where the Firestorm template puts it, a row or two up. */ + +#dialog +{ + left: 50%; + top: 50%; + margin-left: -319.5dp; + margin-top: -196.625dp; + + width: 635dp; + height: 389.25dp; +} + +#environmentlabel { left: 33dp; top: 11.375dp; width: 79.5dp; height: 22.75dp; line-height: 22.75dp; } +#timelabel { left: 33dp; top: 39dp; width: 79.5dp; height: 22.75dp; line-height: 22.75dp; } +#cliffslabel { left: 33dp; top: 94.25dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#accessibilitylabel { left: 33dp; top: 121.875dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#hillslabel { left: 33dp; top: 149.5dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#tiberiumlabel { left: 33dp; top: 177.125dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#tiberiumfieldslabel { left: 33dp; top: 204.75dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#waterlabel { left: 33dp; top: 232.375dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#vegetationlabel { left: 33dp; top: 260dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#citieslabel { left: 33dp; top: 287.625dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#veinholeslabel { left: 33dp; top: 315.25dp; width: 111dp; height: 22.75dp; line-height: 22.75dp; } +#widthlabel { left: 280.5dp; top: 13dp; width: 79.5dp; height: 19.5dp; line-height: 19.5dp; } +#heightlabel { left: 279dp; top: 40.625dp; width: 79.5dp; height: 19.5dp; line-height: 19.5dp; } +#ionstorms { left: 480dp; top: 13dp; width: 126dp; height: 16.25dp; line-height: 16.25dp; } +#transitions { left: 480dp; top: 40.625dp; width: 126dp; height: 16.25dp; line-height: 16.25dp; } +#lifeforms { left: 480dp; top: 68.25dp; width: 126dp; height: 16.25dp; line-height: 16.25dp; } +#oneonone { left: 33dp; top: 71.5dp; width: 109.5dp; height: 16.25dp; line-height: 16.25dp; } +#twoontwo { left: 153dp; top: 71.5dp; width: 109.5dp; height: 16.25dp; line-height: 16.25dp; } + +/* The track bars. */ +#cliffs { left: 142.5dp; top: 94.25dp; } +#accessibility { left: 142.5dp; top: 121.875dp; } +#hills { left: 142.5dp; top: 149.5dp; } +#tiberium { left: 142.5dp; top: 177.125dp; } +#tiberiumfields { left: 142.5dp; top: 204.75dp; } +#water { left: 142.5dp; top: 232.375dp; } +#vegetation { left: 142.5dp; top: 260dp; } +#cities { left: 142.5dp; top: 287.625dp; } +#veinholes { left: 144dp; top: 315.25dp; } + +/* The combo boxes. */ +#biome { left: 118.5dp; top: 13dp; width: 150dp; } +#time { left: 118.5dp; top: 40.625dp; width: 150dp; } +#width { left: 372dp; top: 13dp; width: 94.5dp; } +#height { left: 372dp; top: 42.25dp; width: 94.5dp; } + +#seed { left: 33dp; top: 334.75dp; width: 85.5dp; height: 19.5dp; line-height: 19.5dp; } + +#previewframe { left: 310.5dp; top: 91dp; } +#previewword { top: 107.25dp; height: 26dp; line-height: 26dp; } + +#surprise { left: 307.5dp; top: 321.75dp; } +#previewmap { left: 474dp; top: 321.75dp; } + +/* The bottom row: save, load, delete, then OK and cancel. */ +#save { left: 33dp; } +#load { left: 150dp; } +#delete { left: 267dp; } +#ok { left: 397.5dp; } +#cancel { left: 505.5dp; } diff --git a/ui/mapgenwdt.rml b/ui/mapgenwdt.rml new file mode 100644 index 000000000..c42cbbdca --- /dev/null +++ b/ui/mapgenwdt.rml @@ -0,0 +1,68 @@ + + + Random map + + + + + +
+
Environment:
+ +
Time of Day:
+ +
Map Width:
+ +
Map Height:
+ + + +
1 on 1
+
2 on 2
+
Cliffs:
+ +
Accessability:
+ +
Hills:
+ +
Tiberium Amount:
+ +
Tiberium Fields:
+ +
Water:
+ +
Vegetation:
+ +
Cities:
+ +
Veinholes:
+ + +
Ion Storms
+
Transitions
+
Lifeforms
+ +
+
Preview
+ +
+ +
Surprise Me
+
Preview Map
+ +
Save Map
+
Load Map
+
Delete Map
+
[[TXT_OK]]
+
[[TXT_CANCEL]]
+
+ +
diff --git a/ui/messagebox.rcss b/ui/messagebox.rcss new file mode 100644 index 000000000..a3671ed31 --- /dev/null +++ b/ui/messagebox.rcss @@ -0,0 +1,75 @@ +/* The message box. Its geometry is the IDD_MSGBOX_3 template's, converted from dialog units + at the 8 point MS Sans Serif the template names: 1.5 pixels across and 1.625 down, with a + child's offset taken from the panel's content box and a bordered element's declared size + taken inside its own border. + + The dialog look, including the panel artwork, comes from ui/optionsbase.rcss. */ + +body +{ + font-family: LatoLatin; + font-size: 12dp; + color: #70ff00; + + width: 100%; + height: 100%; +} + +/* 260 x 84 dialog units. The panel is centred on the frame and re-centres itself when the + frame is resized, because the offsets are a proportion of the context. */ +#dialog +{ + display: block; + position: absolute; + left: 50%; + top: 50%; + margin-left: -195dp; + margin-top: -68.25dp; + + width: 386dp; + height: 132.5dp; +} + +/* The message: 22, 12, 216 x 38 dialog units, centred both ways as the template's CTEXT with + SS_CENTERIMAGE was. A message that carries its own line breaks keeps them. */ +#text +{ + display: block; + position: absolute; + left: 31dp; + top: 17.5dp; + width: 324dp; + height: 61.75dp; + + text-align: center; + white-space: pre-line; + overflow: hidden; +} + +/* The three buttons, 60 x 14 dialog units each, on one row 58 units down. They are laid out + first, third, second across the box, which is where the template puts them. */ +.button +{ + display: block; + position: absolute; + top: 92.25dp; + width: 86dp; + height: 18.75dp; + line-height: 18.75dp; + text-align: center; +} + +.first { left: 31dp; } +.third { left: 148dp; } +.second { left: 265dp; } + +/* A lone button takes the middle slot, as the dialog moved it there. */ +.first.centred { left: 148dp; } + +/* The message, a static. + The dlgsys sheets, as optionsbase.rcss gives every other static. */ +#text +{ + font-family: dlgsys; + font-size: 18dp; +} diff --git a/ui/messagebox.rml b/ui/messagebox.rml new file mode 100644 index 000000000..ebea98c90 --- /dev/null +++ b/ui/messagebox.rml @@ -0,0 +1,15 @@ + + + Message + + + + +
+
{{ message }}
+
{{ button1 }}
+
{{ button3 }}
+
{{ button2 }}
+
+ +
diff --git a/ui/missiondelete.rcss b/ui/missiondelete.rcss new file mode 100644 index 000000000..6b9959fdc --- /dev/null +++ b/ui/missiondelete.rcss @@ -0,0 +1,46 @@ +/* The delete browser. Geometry from the IDD_MISSION_DELETE template, converted from dialog + units at the 8 point MS Sans Serif the template names: 1.5 pixels across and 1.625 down, + with a child's offset taken from the panel's content box and the panel's declared size + taken inside its own border. */ + +/* 302 x 198 dialog units, so 453 x 321.75 pixels, centred on the frame. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -226.5dp; + margin-top: -160.875dp; + + width: 449dp; + height: 317.75dp; +} + +/* The CTEXT title, 258 x 8 dialog units at 22, 12. */ +#title +{ + left: 31dp; + top: 17.5dp; + width: 387dp; + height: 13dp; + line-height: 13dp; + text-align: center; +} + +/* The two visible column headings; the template's "Mission" heading is not visible. */ +#descriptionhead { left: 31dp; top: 40.25dp; width: 181.5dp; height: 13dp; line-height: 13dp; } +#timehead { left: 292dp; top: 40.25dp; width: 106.5dp; height: 13dp; line-height: 13dp; text-align: center; } + +/* The list, 258 x 122 dialog units at 22, 42. */ +#games +{ + left: 31dp; + top: 66.25dp; + width: 387dp; + height: 198.25dp; +} + +/* Delete and Cancel, both 50 x 14 dialog units. */ +.button { height: 22.75dp; line-height: 22.75dp; width: 75dp; } + +#accept { left: 257.5dp; top: 277.5dp; } +#cancel { left: 343dp; top: 277.5dp; } diff --git a/ui/missiondelete.rml b/ui/missiondelete.rml new file mode 100644 index 000000000..76b1dc9f9 --- /dev/null +++ b/ui/missiondelete.rml @@ -0,0 +1,27 @@ + + + Delete mission + + + + + +
+
DELETE
+ +
Description
+
Time Stamp
+ +
+
+
{{ entry.description }}
+
{{ entry.date }}
+
{{ entry.time }}
+
+
+ +
Delete
+
[[TXT_CANCEL]]
+
+ +
diff --git a/ui/missionload.rcss b/ui/missionload.rcss new file mode 100644 index 000000000..92956f850 --- /dev/null +++ b/ui/missionload.rcss @@ -0,0 +1,48 @@ +/* The load browser. Geometry from the IDD_MISSION_LOAD template, converted from dialog + units at the 8 point MS Sans Serif the template names: 1.5 pixels across and 1.625 down, + with a child's offset taken from the panel's content box and the panel's declared size + taken inside its own border. */ + +/* 302 x 198 dialog units, so 453 x 321.75 pixels, centred on the frame: its driver does not + move it. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -226.5dp; + margin-top: -160.875dp; + + width: 449dp; + height: 317.75dp; +} + +/* The CTEXT title, 258 x 8 dialog units at 22, 12. */ +#title +{ + left: 31dp; + top: 17.5dp; + width: 387dp; + height: 13dp; + line-height: 13dp; + text-align: center; +} + +/* The two visible column headings. The template's "Mission" heading carries NOT WS_VISIBLE, + so the dialog never drew it and neither does this. */ +#descriptionhead { left: 31dp; top: 40.25dp; width: 181.5dp; height: 13dp; line-height: 13dp; } +#timehead { left: 292dp; top: 40.25dp; width: 108dp; height: 13dp; line-height: 13dp; text-align: center; } + +/* The list, 258 x 124 dialog units at 22, 40. */ +#games +{ + left: 31dp; + top: 63dp; + width: 387dp; + height: 201.5dp; +} + +/* Load and Cancel, both 50 x 14 dialog units. */ +.button { height: 22.75dp; line-height: 22.75dp; width: 75dp; } + +#accept { left: 254.5dp; top: 275.875dp; } +#cancel { left: 343dp; top: 275.875dp; } diff --git a/ui/missionload.rml b/ui/missionload.rml new file mode 100644 index 000000000..a60864cd2 --- /dev/null +++ b/ui/missionload.rml @@ -0,0 +1,27 @@ + + + Load mission + + + + + +
+
LOAD
+ +
Description
+
Time Stamp
+ +
+
+
{{ entry.description }}
+
{{ entry.date }}
+
{{ entry.time }}
+
+
+ +
Load
+
[[TXT_CANCEL]]
+
+ +
diff --git a/ui/missionsave.rcss b/ui/missionsave.rcss new file mode 100644 index 000000000..4c08a8496 --- /dev/null +++ b/ui/missionsave.rcss @@ -0,0 +1,58 @@ +/* The save browser. Geometry from the IDD_MISSION_SAVE template, converted from dialog + units at the 8 point MS Sans Serif the template names: 1.5 pixels across and 1.625 down, + with a child's offset taken from the panel's content box and the panel's declared size + taken inside its own border. */ + +/* 302 x 198 dialog units, so 453 x 321.75 pixels, centred on the frame. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -226.5dp; + margin-top: -160.875dp; + + width: 449dp; + height: 317.75dp; +} + +/* The CTEXT title, 258 x 8 dialog units at 22, 12. */ +#title +{ + left: 31dp; + top: 17.5dp; + width: 387dp; + height: 13dp; + line-height: 13dp; + text-align: center; +} + +/* The two visible column headings; the template's "Mission" heading is not visible. */ +#descriptionhead { left: 31dp; top: 40.25dp; width: 181.5dp; height: 13dp; line-height: 13dp; } +#timehead { left: 296.5dp; top: 40.25dp; width: 99dp; height: 13dp; line-height: 13dp; text-align: center; } + +/* The list, 258 x 105 dialog units at 22, 42: shorter than the other two, because the + description field sits under it. */ +#games +{ + left: 31dp; + top: 66.25dp; + width: 387dp; + height: 170.625dp; +} + +/* The EDITTEXT, 258 x 14 dialog units at 22, 154. */ +#description +{ + left: 31dp; + top: 248.25dp; + width: 387dp; + height: 22.75dp; + line-height: 18.75dp; + padding: 0dp 4dp; +} + +/* Save and Cancel, both 50 x 14 dialog units. */ +.button { height: 22.75dp; line-height: 22.75dp; width: 75dp; } + +#accept { left: 259dp; top: 277.5dp; } +#cancel { left: 343dp; top: 277.5dp; } diff --git a/ui/missionsave.rml b/ui/missionsave.rml new file mode 100644 index 000000000..cfe039687 --- /dev/null +++ b/ui/missionsave.rml @@ -0,0 +1,29 @@ + + + Save mission + + + + + +
+
SAVE
+ +
Description
+
Time Stamp
+ +
+
+
{{ entry.description }}
+
{{ entry.date }}
+
{{ entry.time }}
+
+
+ + + +
Save
+
[[TXT_CANCEL]]
+
+ +
diff --git a/ui/modeconfirm.rcss b/ui/modeconfirm.rcss new file mode 100644 index 000000000..66caf66d1 --- /dev/null +++ b/ui/modeconfirm.rcss @@ -0,0 +1,42 @@ +/* The display mode confirmation. Geometry from the IDD_OPT_CONFIRM_MODE template, converted + at 1.5 pixels across and 1.625 down. + + Nothing here counts the timeout down. The screen takes the mode back on its own after ten + seconds, because the mode being confirmed may have left this document unreadable, and a + countdown a player cannot see is not what decides it. */ + +/* 239 x 70 dialog units, so 358.5 x 113.75 pixels, centred on the frame. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -179.25dp; + margin-top: -56.875dp; + + width: 354.5dp; + height: 109.75dp; +} + +/* LTEXT with no SS_CENTERIMAGE, 195 x 26 dialog units at 22, 12: left aligned, top aligned + and wrapping, which is how the sentence filled its two lines. */ +#text +{ + left: 31dp; + top: 17.5dp; + width: 292.5dp; + height: 42.25dp; + white-space: normal; + line-height: 16dp; +} + +/* OK and Cancel, both 50 x 14 dialog units on the same row. */ +.button +{ + top: 69.5dp; + width: 75dp; + height: 22.75dp; + line-height: 22.75dp; +} + +#ok { left: 164.5dp; } +#cancel { left: 248.5dp; } diff --git a/ui/modeconfirm.rml b/ui/modeconfirm.rml new file mode 100644 index 000000000..3c2711f23 --- /dev/null +++ b/ui/modeconfirm.rml @@ -0,0 +1,14 @@ + + + Confirm display mode + + + + +
+
Click OK to keep this display mode or wait and your old display settings will be restored.
+
[[TXT_OK]]
+
[[TXT_CANCEL]]
+
+ +
diff --git a/ui/mpguest.rcss b/ui/mpguest.rcss new file mode 100644 index 000000000..157af8600 --- /dev/null +++ b/ui/mpguest.rcss @@ -0,0 +1,109 @@ +/* The guest's game setup. Geometry from the IDD_MPLAYER_GUEST template, converted from + dialog units at the 8 point MS Sans Serif the template names: 1.5 pixels across and 1.625 + down, with a child's offset taken from the panel's content box and the panel's declared + size taken inside its own border. + + The template gives every game option WS_DISABLED on this screen: the guest is shown what + the host has chosen and cannot change any of it. */ + +/* 426 x 240 dialog units, so 639 x 390 pixels, centred on the frame. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -319.5dp; + margin-top: -195dp; + + width: 635dp; + height: 386dp; +} + +/* The player's own settings, top left. The combos are 76 dialog units wide at 66, 7 and + 66, 24; these two are the only controls this screen may touch. */ +#sidelabel { left: 25dp; top: 9.375dp; width: 75dp; height: 19.5dp; line-height: 19.5dp; } +#colorlabel { left: 25dp; top: 37dp; width: 75dp; height: 19.5dp; line-height: 19.5dp; } + +#side, #color { left: 97dp; width: 114dp; } +#side { top: 9.375dp; } +#color { top: 37dp; } + +#side selectbox { max-height: 164.125dp; } +#color selectbox { max-height: 235.625dp; } + +/* "Map:" at 149, 7 and the scenario's own name at 176, 7. */ +#maplabel { left: 221.5dp; top: 9.375dp; width: 39dp; height: 16.25dp; line-height: 16.25dp; } +#scenarioname { left: 262dp; top: 9.375dp; width: 348dp; height: 16.25dp; line-height: 16.25dp; } + +/* "Players:" at 18, 42 and the 150 x 68 player list at 18, 56. */ +#playerslabel { left: 25dp; top: 66.25dp; width: 75dp; height: 19.5dp; line-height: 19.5dp; } +#users { left: 25dp; top: 89dp; width: 225dp; height: 110.5dp; } +#users .row { width: 213dp; } + +/* The message log, 256 x 65 at 18, 130, and the chat entry, 256 x 12 at 18, 199. */ +#messages { left: 25dp; top: 209.25dp; width: 384dp; height: 105.625dp; } +#messages .line { width: 372dp; } + +#say { left: 25dp; top: 321.375dp; width: 384dp; height: 19.5dp; line-height: 15.5dp; } + +/* The six track bars, 66 x 12 dialog units at x 227, and their right aligned captions, + 57 x 10 at x 162. */ +#gamespeedlabel, +#aiplayerslabel, +#ailevellabel, +#unitcountlabel, +#techlevellabel, +#creditslabel +{ + left: 241dp; + width: 85.5dp; + height: 16.25dp; + line-height: 16.25dp; +} + +#gamespeedlabel { top: 43.5dp; } +#aiplayerslabel { top: 69.5dp; } +#ailevellabel { top: 95.5dp; } +#unitcountlabel { top: 121.5dp; } +#techlevellabel { top: 147.5dp; } +#creditslabel { top: 173.5dp; } + +.slider { left: 338.5dp; width: 99dp; height: 19.5dp; } + +#gamespeed { top: 41.875dp; } +#aiplayers { top: 67.875dp; } +#ailevel { top: 93.875dp; } +#unitcount { top: 119.875dp; } +#techlevel { top: 145.875dp; } +#credits { top: 171.875dp; } + +/* The nine check boxes, 108 dialog units wide at x 300 but Allies, 105, and Short Game, 84. */ +.check { left: 448dp; width: 162dp; height: 16.25dp; line-height: 16.25dp; } + +#allies { top: 38.625dp; width: 157.5dp; } +#harvtruce { top: 58.125dp; } +#bases { top: 77.625dp; } +#mcv { top: 97.125dp; } +#fog { top: 116.625dp; } +#bridges { top: 136.125dp; } +#crates { top: 155.625dp; } +#shortgame { top: 175.125dp; width: 126dp; } +#engineer { top: 194.625dp; } + +/* What WS_DISABLED looks like: shown, dimmed and out of reach. */ +.off +{ + color: #909090; + pointer-events: none; +} + +.off sliderbar { background-color: #000000b4; border-color: #ffffff; } + +/* Cancel and Accept, both 58 x 18 dialog units at 279, 215 and 350, 215. */ +.button { height: 29.25dp; line-height: 29.25dp; width: 87dp; } + +#cancel { left: 416.5dp; top: 347.375dp; } +#accept { left: 523dp; top: 347.375dp; } + +/* The map name comes from the scenario, not from a template, so this one keeps the clip + optionsbase.rcss drops for a static caption. At 16.25dp a dlgsys cell is not clipped. */ +#scenarioname { overflow: hidden; } diff --git a/ui/mpguest.rml b/ui/mpguest.rml new file mode 100644 index 000000000..c7b5e1067 --- /dev/null +++ b/ui/mpguest.rml @@ -0,0 +1,76 @@ + + + Join a network game + + + + + +
+
Your Side:
+ + +
Your Color:
+ + +
Map:
+
{{ scenarioname }}
+ +
Players:
+
+
+
{{ entry.name }}
+
{{ entry.sidename }}
+
{{ entry.mark }}
+
+
+ +
+
{{ entry.text }}
+
+ + + + +
Game Speed
+ + +
AI Players
+ + +
AI Level
+ + +
Unit Count
+ + +
Tech Level
+ + +
Credits
+ + +
Allies allowed
+
Harvester Truce
+
Bases
+
Re-Deployable MCV
+
Fog Of War
+
Bridges Destroyable
+
Crates
+
Short Game
+
Multi Engineer
+ +
+ +
+ +
[[TXT_CANCEL]]
+
Accept
+
+ +
diff --git a/ui/mphost.rcss b/ui/mphost.rcss new file mode 100644 index 000000000..89f7735dd --- /dev/null +++ b/ui/mphost.rcss @@ -0,0 +1,100 @@ +/* The host's game setup. Geometry from the IDD_MPLAYER_HOST template, converted from dialog + units at the 8 point MS Sans Serif the template names: 1.5 pixels across and 1.625 down, + with a child's offset taken from the panel's content box and the panel's declared size + taken inside its own border. */ + +/* 426 x 240 dialog units, so 639 x 390 pixels, centred on the frame. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -319.5dp; + margin-top: -195dp; + + width: 635dp; + height: 386dp; +} + +/* The player's own settings, top left. The combos are 76 dialog units wide at 73, 6 and + 73, 22; a closed one stands as tall as the item height rather than as its dropped extent. */ +#sidelabel { left: 25dp; top: 11dp; width: 75dp; height: 19.5dp; line-height: 19.5dp; } +#colorlabel { left: 25dp; top: 35.375dp; width: 78dp; height: 19.5dp; line-height: 19.5dp; } + +#side, #color { left: 107.5dp; width: 114dp; } +#side { top: 7.75dp; } +#color { top: 33.75dp; } + +#side selectbox { max-height: 146.25dp; } +#color selectbox { max-height: 237.25dp; } + +/* Multiplayer Map, 90 x 14 at 160, 5, and the scenario's own name at 255, 8. */ +#multimap { left: 238dp; top: 6.125dp; width: 135dp; } +#scenarioname { left: 380.5dp; top: 11dp; width: 229.5dp; height: 16.25dp; line-height: 16.25dp; } + +/* "Players:" at 18, 41, the 150 x 67 player list at 18, 56 and the kick button under it. */ +#playerslabel { left: 25dp; top: 64.625dp; width: 85.5dp; height: 19.5dp; line-height: 19.5dp; } +#users { left: 25dp; top: 89dp; width: 225dp; height: 108.875dp; } +#users .row { width: 213dp; } + +#kick { left: 25dp; top: 347.375dp; width: 30dp; height: 29.25dp; line-height: 29.25dp; } + +/* The message log, 256 x 65 at 18, 130, and the chat entry, 256 x 12 at 18, 199. */ +#messages { left: 25dp; top: 209.25dp; width: 384dp; height: 105.625dp; } +#messages .line { width: 372dp; } + +#say { left: 25dp; top: 321.375dp; width: 384dp; height: 19.5dp; line-height: 15.5dp; } + +/* The six track bars, 66 x 12 dialog units at x 227, and their right aligned captions, + 58 x 10 at x 166. The captions never change: every WM_HSCROLL case in the dialog fetched + its label and did nothing with it. */ +#gamespeedlabel, +#aiplayerslabel, +#ailevellabel, +#unitcountlabel, +#techlevellabel, +#creditslabel +{ + left: 247dp; + width: 87dp; + height: 16.25dp; + line-height: 16.25dp; +} + +#gamespeedlabel { top: 43.5dp; } +#aiplayerslabel { top: 69.5dp; } +#ailevellabel { top: 95.5dp; } +#unitcountlabel { top: 121.5dp; } +#techlevellabel { top: 147.5dp; } +#creditslabel { top: 173.5dp; } + +.slider { left: 338.5dp; width: 99dp; height: 19.5dp; } + +#gamespeed { top: 41.875dp; } +#aiplayers { top: 67.875dp; } +#ailevel { top: 93.875dp; } +#unitcount { top: 119.875dp; } +#techlevel { top: 145.875dp; } +#credits { top: 171.875dp; } + +/* The nine check boxes, 108 dialog units wide at x 300 but Allies, which is 105. */ +.check { left: 448dp; width: 162dp; height: 16.25dp; line-height: 16.25dp; } + +#allies { top: 38.625dp; width: 157.5dp; } +#harvtruce { top: 58.125dp; } +#bases { top: 77.625dp; } +#mcv { top: 97.125dp; } +#fog { top: 116.625dp; } +#bridges { top: 136.125dp; } +#crates { top: 155.625dp; } +#shortgame { top: 175.125dp; } +#engineer { top: 194.625dp; } + +/* Cancel, 53 x 14 at 291, 219, and Go!, 54 x 14 at 354, 219. */ +.button { height: 22.75dp; line-height: 22.75dp; } + +#cancel { left: 434.5dp; top: 353.875dp; width: 79.5dp; } +#go { left: 529dp; top: 353.875dp; width: 81dp; } + +/* The map name comes from the scenario, not from a template, so this one keeps the clip + optionsbase.rcss drops for a static caption. At 16.25dp a dlgsys cell is not clipped. */ +#scenarioname { overflow: hidden; } diff --git a/ui/mphost.rml b/ui/mphost.rml new file mode 100644 index 000000000..36f34aa2b --- /dev/null +++ b/ui/mphost.rml @@ -0,0 +1,76 @@ + + + Host a network game + + + + + +
+
Your Side:
+ + +
Your Color:
+ + +
Multiplayer Map
+
{{ scenarioname }}
+ +
Players:
+
+
+
{{ entry.name }}
+
{{ entry.sidename }}
+
{{ entry.mark }}
+
+
+ +
Kick
+ +
+
{{ entry.text }}
+
+ + + +
Game Speed:
+ + +
AI Players:
+ + +
AI Level:
+ + +
Unit Count:
+ + +
Tech Level:
+ + +
Credits:
+ + +
Allies allowed
+
Harvester Truce
+
Bases
+
Re-Deployable MCV
+
Fog Of War
+
Bridges Destroyable
+
Crates
+
Short Game
+
Multi Engineer
+ +
+ +
+ +
[[TXT_CANCEL]]
+
Go!
+
+ +
diff --git a/ui/mpselect.rcss b/ui/mpselect.rcss new file mode 100644 index 000000000..fb3692b96 --- /dev/null +++ b/ui/mpselect.rcss @@ -0,0 +1,46 @@ +/* The multiplayer game choice shown with the base game. Geometry from the + IDD_MPLAYER_SELECT_GAME template, converted from dialog units at the 8 point MS Sans + Serif the template names: 1.5 pixels across and 1.625 down, with a child's offset taken + from the panel's content box and the panel's declared size taken inside its own border. */ + +/* 204 x 144 dialog units, so 306 x 234 pixels. + + The panel is not centred vertically. Its driver moved it with + Move_Dialog(handle, -1, (HiddenSurface->Get_Height() - 400) / 2 + 147), which is half the + frame height less 53, so it sits 53 dp above the middle whatever the frame height is. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -153dp; + margin-top: -53dp; + + width: 302dp; + height: 230dp; +} + +/* CTEXT with SS_CENTERIMAGE, 130 x 12 dialog units at 37, 12. */ +#question +{ + left: 53.5dp; + top: 17.5dp; + width: 195dp; + height: 19.5dp; + line-height: 19.5dp; + text-align: center; +} + +/* Five buttons, all 130 x 18 dialog units at x 37. */ +.button +{ + left: 53.5dp; + width: 195dp; + height: 29.25dp; + line-height: 29.25dp; +} + +#internet { top: 43.5dp; } +#modem { top: 77.625dp; } +#network { top: 111.75dp; } +#skirmish { top: 145.875dp; } +#back { top: 180dp; } diff --git a/ui/mpselect.rml b/ui/mpselect.rml new file mode 100644 index 000000000..2149d1e87 --- /dev/null +++ b/ui/mpselect.rml @@ -0,0 +1,17 @@ + + + Select multiplayer game + + + + +
+
Select Multiplayer Game
+
Internet
+
Modem / Serial
+
Network
+
Skirmish
+
Main Menu
+
+ +
diff --git a/ui/mpselectfs.rcss b/ui/mpselectfs.rcss new file mode 100644 index 000000000..8e8565d51 --- /dev/null +++ b/ui/mpselectfs.rcss @@ -0,0 +1,47 @@ +/* The multiplayer game choice shown with the expansion installed. Geometry from the + IDD_MPLAYER_SELECT_GAME_FS template, converted from dialog units at the 8 point MS Sans + Serif the template names: 1.5 pixels across and 1.625 down, with a child's offset taken + from the panel's content box and the panel's declared size taken inside its own border. */ + +/* 197 x 146 dialog units, so 295.5 x 237.25 pixels. + + The panel is not centred vertically. Its driver moved it with + Move_Dialog(handle, -1, (HiddenSurface->Get_Height() - 400) / 2 + 147), which is half the + frame height less 53, so it sits 53 dp above the middle whatever the frame height is. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -147.75dp; + margin-top: -53dp; + + width: 291.5dp; + height: 233.25dp; +} + +/* CTEXT with SS_CENTERIMAGE, 130 x 12 dialog units at 30, 8. */ +#question +{ + left: 43dp; + top: 11dp; + width: 195dp; + height: 19.5dp; + line-height: 19.5dp; + text-align: center; +} + +/* Six buttons, all 130 x 18 dialog units at x 30. */ +.button +{ + left: 43dp; + width: 195dp; + height: 29.25dp; + line-height: 29.25dp; +} + +#internet { top: 33.75dp; } +#worlddom { top: 64.625dp; } +#modem { top: 95.5dp; } +#network { top: 126.375dp; } +#skirmish { top: 157.25dp; } +#back { top: 189.75dp; } diff --git a/ui/mpselectfs.rml b/ui/mpselectfs.rml new file mode 100644 index 000000000..d1891e4e3 --- /dev/null +++ b/ui/mpselectfs.rml @@ -0,0 +1,18 @@ + + + Select multiplayer game + + + + +
+
Select Multiplayer Game
+
Internet
+
World Domination! (Internet)
+
Modem / Serial
+
Network
+
Skirmish
+
Main Menu
+
+ +
diff --git a/ui/options.rcss b/ui/options.rcss new file mode 100644 index 000000000..a9b056a10 --- /dev/null +++ b/ui/options.rcss @@ -0,0 +1,35 @@ +/* The main options menu. Geometry from the IDD_OPT_MAIN template, converted from dialog + units at the 8 point MS Sans Serif the template names: 1.5 pixels across and 1.625 down, + with a child's offset taken from the panel's content box and the panel's declared size + taken inside its own border. */ + +/* 200 x 148 dialog units, so 300 x 240.5 pixels. + + The panel is not centred vertically. Its driver moved it with + Move_Dialog(handle, -1, (HiddenSurface->Get_Height() - 400) / 2 + 147), which is half the + frame height less 53, so it sits 53 dp above the middle whatever the frame height is. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -150dp; + margin-top: -53dp; + + width: 296dp; + height: 236.5dp; +} + +/* Five buttons, all 126 x 18 dialog units at x 37. */ +.button +{ + left: 53.5dp; + width: 189dp; + height: 29.25dp; + line-height: 29.25dp; +} + +#settings { top: 14.25dp; } +#display { top: 50dp; } +#sound { top: 85.75dp; } +#keyboard { top: 121.5dp; } +#exit { top: 193dp; } diff --git a/ui/options.rml b/ui/options.rml new file mode 100644 index 000000000..b43a3c72e --- /dev/null +++ b/ui/options.rml @@ -0,0 +1,16 @@ + + + Options + + + + +
+
Game Settings
+
Display
+
Sound
+
Keyboard
+
Main Menu
+
+ +
diff --git a/ui/optionsbase.rcss b/ui/optionsbase.rcss new file mode 100644 index 000000000..b625d6a5e --- /dev/null +++ b/ui/optionsbase.rcss @@ -0,0 +1,275 @@ +/* What every options family document looks like. Only the look lives here; each document's + own stylesheet carries its geometry, converted from its dialog template's units. + + The artwork is the game's own. A dialog backdrop is the slice of dbak6440.pcx that lies + under the dialog, with leftbar.pcx and rightbar.pcx tiled down its edges and the four + bar_ corner pieces over them, which is what Draw_Dialog_Back composed. A push button is + bue_li24, bue_mi24 and bue_ri24 in a row, and bde_ the same while it is held down. A + check box is cue_i.pcx, or cce_i.pcx when it is ticked, beside its caption. The colours + are the ones OwnerDraw::Initialize set: text is RGB(112,255,0), disabled text is + RGB(144,144,144), a track bar's frame is RGB(78,182,220) and a picked list row is + RGB(34,80,97). A control that stood over the wallpaper -- a list, a field, a combo box, + a track bar -- showed it blended 180/255 toward black. + + Two things the original did that are not reproduced. The sixteen inward glow passes + around a dialog edge need a fade over sixteen pixels, which no decorator in the profile + docs/UI_DESIGN.md declares can express. A disabled button is the enabled artwork under + image-color rather than under a half-black rectangle; the result is the same product. + + Everything here stays inside that profile: text, images, ordinary layout, borders and + basic decorators, with no filter, layer, shader or transform. */ + +body +{ + font-family: LatoLatin; + font-size: 12dp; + color: #70ff00; + + width: 100%; + height: 100%; +} + +/* A dialog panel. A document states its own size and where it sits. The border keeps the + width the templates were converted against; the artwork is painted over all of it. */ +.panel +{ + display: block; + position: absolute; + + border-width: 2dp; + border-color: transparent; + + /* The colour is what the panel is left with if the artwork cannot be read, which is what + a document shown before the mix files are mounted would see. A background paints under + the decorators, so it is invisible whenever they load. */ + background-color: #23261fF2; + + decorator: + image(bar_ul.pcx scale-none left top) border-box, + image(bar_ur.pcx scale-none right top) border-box, + image(bar_ll.pcx scale-none left bottom) border-box, + image(bar_lr.pcx scale-none right bottom) border-box, + image(leftbar.pcx repeat-y left top) border-box, + image(rightbar.pcx repeat-y right top) border-box, + image(dbak6440.pcx scale-none 50% 50%) border-box; +} + +/* A panel the main menu family raises above the middle of the frame. Its wallpaper slice + is taken 147dp above its own top edge, which is where the frame's centred artwork sits + under a panel whose top is 53dp above the middle. */ +.panel.raised +{ + decorator: + image(bar_ul.pcx scale-none left top) border-box, + image(bar_ur.pcx scale-none right top) border-box, + image(bar_ll.pcx scale-none left bottom) border-box, + image(bar_lr.pcx scale-none right bottom) border-box, + image(leftbar.pcx repeat-y left top) border-box, + image(rightbar.pcx repeat-y right top) border-box, + image(dbak6440.pcx scale-none 50% 91%) border-box; +} + +/* An owner-draw push button. */ +.button +{ + display: block; + position: absolute; + + /* A control's declared size is the template's, borders included, because a dialog + control's rectangle includes the frame drawn around it. */ + box-sizing: border-box; + text-align: center; + white-space: nowrap; + overflow: hidden; + + color: #70ff00; + background-color: #33372c; + decorator: tiled-horizontal(bue_li24.pcx, bue_mi24.pcx, bue_ri24.pcx); +} + +.button:active +{ + decorator: tiled-horizontal(bde_li24.pcx, bde_mi24.pcx, bde_ri24.pcx); +} + +.button.disabled, +.disabled .button +{ + color: #909090; + image-color: #808080; + pointer-events: none; +} + +/* A BS_AUTOCHECKBOX with BS_FLAT: an eighteen pixel box at the left edge of the control + with the caption beside it, which is what CheckBoxCtrlProc drew. */ +.check +{ + display: block; + position: absolute; + box-sizing: border-box; + text-align: left; + padding-left: 26dp; + white-space: nowrap; + overflow: hidden; + + color: #70ff00; + decorator: image(cue_i.pcx contain left center); +} + +.check.ticked +{ + decorator: image(cce_i.pcx contain left center); +} + +.check.disabled, +.disabled .check +{ + color: #909090; + image-color: #808080; + pointer-events: none; +} + +/* A static caption. A document says where it sits and how it is aligned. + + It does not clip. A dlgsys glyph cell is 18dp and the templates give a static as little + as 13dp, so a clipping caption would cut the shadow row off a capital and the tail off a + descender; StaticCtrlProc had no such limit, because ODDrawCharRemap blits without + clipping to the control. RmlUi clips both axes together or neither, and no static caption + in any shipped document is wider than its control -- the tightest has 4.5dp to spare -- + so there is nothing for a clip to protect here. A caption whose text comes from the game + rather than the templates asks for the clip back in its own stylesheet. */ +.caption +{ + display: block; + position: absolute; + white-space: nowrap; + overflow: visible; +} + +/* A TBS_NOTICKS track bar. The original drew no groove: the control is the wallpaper + dimmed inside a one pixel frame, with trakgrip.pcx spanning its whole height. */ +.slider +{ + display: block; + position: absolute; + box-sizing: border-box; + + background-color: #000000b4; + border-width: 1dp; + border-color: #4eb6dc; +} + +.slider slider +{ + width: 100%; + height: 100%; +} + +.slider sliderbar +{ + width: 12dp; + height: 100%; + + decorator: image(trakgrip.pcx); +} + +.slider slidertrack +{ + width: 100%; + height: 100%; +} + +.slider sliderarrowdec, +.slider sliderarrowinc +{ + width: 0dp; + height: 0dp; +} + +/* A list box. A row states its own width, because a scrolling container gives its children + no width to be a proportion of. */ +.list +{ + display: block; + position: absolute; + box-sizing: border-box; + + background-color: #000000b4; + border-width: 1dp; + border-color: #ffffff; + overflow-y: auto; + overflow-x: hidden; +} + +.list .row +{ + display: block; + box-sizing: border-box; + padding: 1dp 4dp; + white-space: nowrap; + color: #70ff00; +} + +.list .row.picked +{ + background-color: #225061; +} + +.list scrollbarvertical +{ + width: 18dp; + background-color: #000000b4; +} + +.list scrollbarvertical slidertrack +{ + width: 18dp; + margin-top: 0dp; + background-color: transparent; + border-width: 0dp; +} + +.list scrollbarvertical sliderbar +{ + width: 18dp; + min-height: 20dp; + + decorator: tiled-vertical(sbgript.pcx, sbgripm.pcx, sbgripb.pcx); +} + +.list scrollbarvertical sliderarrowdec, +.list scrollbarvertical sliderarrowinc +{ + width: 18dp; + height: 22dp; +} + +.list scrollbarvertical sliderarrowdec { decorator: image(uparrowr.pcx); } +.list scrollbarvertical sliderarrowinc { decorator: image(dnarrowr.pcx); } +.list scrollbarvertical sliderarrowdec:active { decorator: image(uparrowp.pcx); } +.list scrollbarvertical sliderarrowinc:active { decorator: image(dnarrowp.pcx); } + +/* Which face a control draws with. + + The dialogs drew a push button, a check box, a static caption, a tab, a combo box and a + track bar's value from the dlgsys remap sheets, and reached for the system face only for + a list box, a tooltip and the hotkey control; an edit box went through that face too. + That split is not decoration -- it is most of why the screens read as the original -- so + it is reproduced here rather than applied to everything. + + font-size names the height of a glyph cell. The sheets carry 14 by 18 cells, so 18dp + draws one sheet pixel per authored pixel and the text follows the frame scale exactly as + the artwork around it does. docs/UI_DESIGN.md, "Fonts", owns the split. + + Everything not named here keeps the shipped face body declares: the lists and their rows, + the edit fields, the message logs and the hotkey capture control. */ +.button, +.check, +.caption, +.label, +.prose, +select +{ + font-family: dlgsys; + font-size: 18dp; +} diff --git a/ui/progresswait.rcss b/ui/progresswait.rcss new file mode 100644 index 000000000..e23ffa993 --- /dev/null +++ b/ui/progresswait.rcss @@ -0,0 +1,82 @@ +/* The progress and wait box. Its geometry is the IDD_PROGRESS_WAIT template's, converted + from dialog units at the 8 point MS Sans Serif the template names: 1.5 pixels across and + 1.625 down, with a child's offset taken from the panel's content box and a bordered + element's declared size taken inside its own border. + + The bar itself is not styled here. It is the game's own artwork, drawn into a surface the + engine owns and shown by the element, which takes its size from that surface; + the frame around it centres whatever size arrives. */ + +body +{ + font-family: LatoLatin; + font-size: 12dp; + color: #70ff00; + + width: 100%; + height: 100%; +} + +/* 192 x 53 dialog units. The panel is centred on the frame and re-centres itself when the + frame is resized, because the offsets are a proportion of the context. */ +#dialog +{ + display: block; + position: absolute; + left: 50%; + top: 50%; + margin-left: -144dp; + margin-top: -43.0625dp; + + width: 284dp; + height: 82.125dp; +} + +/* 22, 12, 148 x 11 dialog units. CTEXT with SS_CENTERIMAGE, so the caption is centred both + ways within its own extents. */ +#text +{ + display: block; + position: absolute; + left: 31dp; + top: 17.5dp; + width: 222dp; + height: 17.875dp; + line-height: 17.875dp; + + text-align: center; + white-space: nowrap; + overflow: hidden; +} + +/* 46, 26, 100 x 15 dialog units: the IDC_PROGRESS_BAR_FRAME group box. The bar is centred + in it, which is where Display_Progress put it from the control's own rectangle. */ +#frame +{ + display: block; + position: absolute; + left: 67dp; + top: 40.25dp; + width: 148dp; + height: 22.375dp; + line-height: 22.375dp; + + text-align: center; + + border-width: 1dp; + border-color: #ffffff; +} + +surface +{ + display: inline-block; + vertical-align: middle; +} + +/* The caption, a static. + The dlgsys sheets, as optionsbase.rcss gives every other static. */ +#text +{ + font-family: dlgsys; + font-size: 18dp; +} diff --git a/ui/progresswait.rml b/ui/progresswait.rml new file mode 100644 index 000000000..636f6ab5c --- /dev/null +++ b/ui/progresswait.rml @@ -0,0 +1,13 @@ + + + Working + + + + +
+
{{ caption }}
+
+
+ +
diff --git a/ui/reconnect.rcss b/ui/reconnect.rcss new file mode 100644 index 000000000..2ed7cb3a2 --- /dev/null +++ b/ui/reconnect.rcss @@ -0,0 +1,93 @@ +/* The reconnect and kick-vote screen, IDD_MPLAYER_DISCONNECT. + + Geometry converted from the template's dialog units at the 8 point MS Sans Serif it + names: 1.5 pixels across and 1.625 down, with a child's offset taken from the panel's + content box and the panel's declared size taken inside its own border. The template is + 339 x 220 dialog units, so 508.5 x 357.5 pixels, and its driver centers it. */ + +#dialog +{ + left: 50%; + top: 50%; + margin-left: -254.25dp; + margin-top: -178.75dp; + + width: 504.5dp; + height: 353.5dp; +} + +/* A seat: the button carrying the player's name and the sync bar beside it. The template + puts eight of these in two columns of four, the button 90 x 14 units and the bar's group + box 40 x 10 six units to its right, so a seat spans 136 units and each carries its own + position because the game shows only the seats it holds. */ +.seat +{ + display: block; + position: absolute; + width: 204dp; + height: 22.75dp; +} + +.seat .kick +{ + left: 0dp; + top: 0dp; + width: 135dp; + height: 22.75dp; + line-height: 22.75dp; +} + +.seat .barframe +{ + display: block; + position: absolute; + box-sizing: border-box; + left: 144dp; + top: 0dp; + width: 60dp; + height: 16.25dp; + + background-color: #000000b4; + border-width: 1dp; + border-color: #ffffff; +} + +/* The bar shrinks and changes color as the wait on its seat drags on, which is what + Draw_Sync_Bars filled into the surface. Both come from the model, because the dialog + chose them from elapsed time rather than from a state a stylesheet can name. The floor is + the six pixels the dialog kept of the box's sixty. */ +.seat .bar +{ + display: block; + position: absolute; + left: 0dp; + top: 0dp; + height: 100%; + min-width: 6dp; +} + +/* The time remaining, an LTEXT at 29, 88 across 279 x 8 units. */ +#timeremaining +{ + left: 43.5dp; + top: 143dp; + width: 418.5dp; + height: 13dp; + line-height: 13dp; + color: #70ff00; +} + +/* The message list, 295 x 84 units at 22, 103. It is a LBS_NOSEL list box, which is what + the shared log style stands for. */ +#messages { left: 33dp; top: 167.375dp; width: 442.5dp; height: 136.5dp; } +#messages .line { width: 430.5dp; } + +/* The cancel button, 295 x 14 units at 22, 194. */ +#cancel +{ + left: 33dp; + top: 315.25dp; + width: 442.5dp; + height: 22.75dp; + line-height: 22.75dp; +} diff --git a/ui/reconnect.rml b/ui/reconnect.rml new file mode 100644 index 000000000..5a815257b --- /dev/null +++ b/ui/reconnect.rml @@ -0,0 +1,26 @@ + + + Waiting for players + + + + + +
+
+
{{ entry.name }}
+
+
+
+
+ +
{{ timetext }}
+ +
+
{{ line }}
+
+ +
[[TXT_CANCEL]]
+
+ +
diff --git a/ui/savebrowser.rcss b/ui/savebrowser.rcss new file mode 100644 index 000000000..1e362371e --- /dev/null +++ b/ui/savebrowser.rcss @@ -0,0 +1,75 @@ +/* What the load, save and delete browsers share. Only the look and the list's own columns + live here; each document's stylesheet carries the geometry its template gives it. + + The three templates are the same 302 x 198 dialog units and differ only by the list's + height, where the action button sits and whether a description field exists, so the + family's own sheet is the panel, the column headings and a row. */ + +/* A row's cells stand where the owner-draw list put its columns. The three dialog + procedures register columns with OD_ADDCOLUMN at x 2, 255 and 315, widths 249, 56 and + unbounded, and the item's own string is column zero. + + The multiplayer star is deliberately absent. LoadOptionsClass::Fill_List sends OD_SETCELL + at x 200 for it, no column is registered there, and OD_SETCELL answers -1 for a column it + cannot find, so the legacy list never drew the star either. Preserved rather than + repaired, and reported separately. */ +.list .row +{ + position: relative; + width: 375dp; + height: 14dp; + line-height: 14dp; + padding: 0dp; +} + +.row .description +{ + display: block; + position: absolute; + left: 2dp; + top: 0dp; + width: 249dp; + white-space: nowrap; + overflow: hidden; +} + +.row .date +{ + display: block; + position: absolute; + left: 255dp; + top: 0dp; + width: 56dp; + white-space: nowrap; + overflow: hidden; +} + +.row .time +{ + display: block; + position: absolute; + left: 315dp; + top: 0dp; + width: 60dp; + white-space: nowrap; + overflow: hidden; +} + +/* The description field, where the template puts an EDITTEXT. It states a width, because a + field with none formats no line and RmlUi's End key then moves the caret to the start of + an empty line rather than to the end of the value. */ +.field +{ + display: block; + position: absolute; + box-sizing: border-box; + white-space: nowrap; + overflow: hidden; + + color: #70ff00; + background-color: #000000b4; + border-width: 1dp; + border-color: #ffffff; +} + +.field:focus { background-color: #000000b4; } diff --git a/ui/selectmap.rcss b/ui/selectmap.rcss new file mode 100644 index 000000000..e702957e2 --- /dev/null +++ b/ui/selectmap.rcss @@ -0,0 +1,76 @@ +/* The multiplayer map selection screen. Geometry from the IDD_MPLAYER_SELECT_MAP template, + converted from dialog units at the 8 point MS Sans Serif the template names: 1.5 pixels + across and 1.625 down, with a child's offset taken from the panel's content box and the + panel's declared size taken inside its own border. */ + +/* 360 x 200 dialog units, so 540 x 325 pixels, centred on the frame. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -270dp; + margin-top: -162.5dp; + + width: 536dp; + height: 321dp; +} + +/* The CTEXT title, 316 x 12 dialog units at 22, 12. */ +#title +{ + left: 31dp; + top: 17.5dp; + width: 474dp; + height: 19.5dp; + line-height: 19.5dp; + text-align: center; +} + +/* The map list, 175 x 142 dialog units at 22, 26. */ +#maps +{ + left: 31dp; + top: 40.25dp; + width: 262.5dp; + height: 230.75dp; +} + +/* A row spans the list less its scrollbar, and stands as tall as the owner-draw list's own + item, which is the 12 pixel list font plus two. */ +#maps .row +{ + width: 250.5dp; + height: 14dp; + line-height: 14dp; +} + +/* The preview frame, a GROUPBOX 129 x 80 dialog units at 209, 59. The picture inside it is + drawn by the screen and reaches the document through the element. */ +#previewframe +{ + display: block; + position: absolute; + box-sizing: border-box; + left: 311.5dp; + top: 93.875dp; + width: 193.5dp; + height: 130dp; + + background-color: #000000b4; + border-width: 1dp; + border-color: #ffffff; +} + +/* The picture takes its size from the provider, which is the frame's own extents in game + logical units, so nothing here states one. */ +#previewframe surface +{ + display: block; +} + +/* OK, Create Random Map and Cancel, all 14 dialog units tall at y 174. */ +.button { height: 22.75dp; line-height: 22.75dp; top: 280.75dp; } + +#accept { left: 31dp; width: 75dp; } +#random { left: 196dp; width: 159dp; } +#cancel { left: 430dp; width: 75dp; } diff --git a/ui/selectmap.rml b/ui/selectmap.rml new file mode 100644 index 000000000..f36143f2d --- /dev/null +++ b/ui/selectmap.rml @@ -0,0 +1,24 @@ + + + Select multiplayer map + + + + +
+
Select Multiplayer Map
+ +
+
{{ name }}
+
+ +
+ +
+ +
[[TXT_OK]]
+
Create Random Map
+
[[TXT_CANCEL]]
+
+ +
diff --git a/ui/skirmish.rcss b/ui/skirmish.rcss new file mode 100644 index 000000000..0bbb76ed5 --- /dev/null +++ b/ui/skirmish.rcss @@ -0,0 +1,255 @@ +/* The skirmish setup screen. Geometry from the IDD_SKIRMISH template, converted from dialog + units at the 8 point MS Sans Serif the template names: 1.5 pixels across and 1.625 down, + with a child's offset taken from the panel's content box and the panel's declared size + taken inside its own border. */ + +/* 426 x 240 dialog units, so 639 x 390 pixels, centred on the frame. */ +#dialog +{ + left: 50%; + top: 50%; + margin-left: -319.5dp; + margin-top: -195dp; + + width: 635dp; + height: 386dp; +} + +/* The player's own settings, down the left edge. */ +#namelabel { left: 31dp; top: 17.5dp; width: 117dp; height: 13dp; line-height: 13dp; } +#sidelabel { left: 31dp; top: 66.25dp; width: 117dp; height: 13dp; line-height: 13dp; } +#colorlabel { left: 31dp; top: 113.375dp; width: 117dp; height: 13dp; line-height: 13dp; } +#maplabel { left: 31dp; top: 155.625dp; width: 54dp; height: 16.25dp; line-height: 16.25dp; } + +/* The scenario's own name, which the map selection screen writes. */ +#scenarioname +{ + left: 31dp; + top: 178.375dp; + width: 402dp; + height: 16.25dp; + line-height: 16.25dp; +} + +/* The EDITTEXT, 78 x 12 dialog units at 22, 24. It states a width, because a field with + none formats no line and RmlUi's End key then moves the caret to the start of an empty + line rather than to the end of the value. */ +#name +{ + display: block; + position: absolute; + box-sizing: border-box; + left: 31dp; + top: 37dp; + width: 117dp; + height: 19.5dp; + line-height: 15.5dp; + padding: 0dp 4dp; + white-space: nowrap; + overflow: hidden; + + font-family: LatoLatin; + color: #70ff00; + background-color: #000000b4; + border-width: 1dp; + border-color: #ffffff; +} + +#name:focus { background-color: #000000b4; } + +/* The two CBS_DROPDOWNLIST combo boxes, 78 dialog units wide at 22, 52 and 22, 82. A + closed combo stands as tall as the item height ownrdraw.cpp sets, which is the 14 pixel + dialog font plus two, inside a two pixel border; the template's own height is how far the + list drops. */ +#side, +#color +{ + display: block; + position: absolute; + box-sizing: border-box; + left: 31dp; + width: 117dp; + height: 20dp; + + color: #70ff00; + background-color: #000000b4; + border-width: 1dp; + border-color: #ffffff; +} + +#side { top: 82.5dp; } +#color { top: 131.25dp; } + +#side selectvalue, +#color selectvalue +{ + width: auto; + margin-right: 16dp; + height: 16dp; + line-height: 16dp; + padding: 0dp 4dp; + white-space: nowrap; + overflow: hidden; +} + +#side selectarrow, +#color selectarrow +{ + width: 16dp; + height: 16dp; + + decorator: image(dnarrowr.pcx); +} + +#side selectarrow:active, +/* The dropped lists, capped at the 74 and 73 dialog units the template gives them. */ +#side selectbox, +#color selectbox +{ + width: 113dp; + overflow-y: auto; + + background-color: #000000b4; + border-width: 1dp; + border-color: #ffffff; +} + +#side selectbox { max-height: 120.25dp; } +#color selectbox { max-height: 118.625dp; } + +#side selectbox option, +#color selectbox option +{ + width: auto; + height: 18dp; + line-height: 18dp; + padding: 0dp 4dp; + white-space: nowrap; + color: #70ff00; +} + +#side selectbox option:hover { color: #70ff00; } + +#side selectbox option:checked, +#color selectbox option:checked { background-color: #225061; } + +/* The preview frame, a GROUPBOX 215 x 106 dialog units at 22, 122. The picture inside it is + drawn by the screen and reaches the document through the element. */ +#previewframe +{ + display: block; + position: absolute; + box-sizing: border-box; + left: 31dp; + top: 196.25dp; + width: 322.5dp; + height: 172.25dp; + + background-color: #000000b4; + border-width: 1dp; + border-color: #ffffff; +} + +/* The template's own CTEXT inside the frame, which the picture is drawn over the way the + dialog blitted its preview over the group box. It shows through the letterbox around a + picture of a different shape, because the surface fills that with its color key. */ +#previewtext +{ + display: block; + position: absolute; + left: 0dp; + top: 68.875dp; + width: 320.5dp; + height: 21.125dp; + line-height: 21.125dp; + text-align: center; + color: #909090; +} + +/* The picture takes its size from the provider, which is the frame's interior in game + logical units, so nothing here states one. */ +#previewframe surface +{ + display: block; + position: absolute; + left: 0dp; + top: 0dp; +} + +/* The unnamed GROUPBOX around the check boxes, 167 x 96 dialog units at 117, 12, and the + one around the track bars, 110 x 181 at 294, 12. */ +#optionbox, +#sliderbox +{ + display: block; + position: absolute; + box-sizing: border-box; + + border-width: 1dp; + border-color: #ffffff; +} + +#optionbox { left: 173.5dp; top: 17.5dp; width: 250.5dp; height: 156dp; } +#sliderbox { left: 439dp; top: 17.5dp; width: 165dp; height: 294.125dp; } + +/* The seven check boxes, all 157 dialog units wide but the last, at x 123. */ +.check { left: 182.5dp; width: 235.5dp; height: 16.25dp; line-height: 16.25dp; } + +#bases { top: 32.125dp; } +#crates { top: 51.625dp; } +#fog { top: 71.125dp; } +#bridges { top: 90.625dp; } +#mcv { top: 110.125dp; } +#shortgame { top: 129.625dp; } +#engineer { top: 149.125dp; width: 162dp; } + +/* The six track bars and their captions, all 100 dialog units wide at x 298. The captions + never change: every WM_HSCROLL case in the dialog fetched its label and did nothing with + it, so the template's own words are what the screen has always shown. */ +#unitcountlabel, +#creditslabel, +#techlevellabel, +#ailevellabel, +#aiplayerslabel, +#gamespeedlabel +{ + left: 445dp; + width: 150dp; + height: 13dp; + line-height: 13dp; +} + +#unitcountlabel { top: 27.25dp; } +#creditslabel { top: 74.375dp; } +#techlevellabel { top: 121.5dp; } +#ailevellabel { top: 168.625dp; } +#aiplayerslabel { top: 215.75dp; } +#gamespeedlabel { top: 262.875dp; } + +.slider { left: 445dp; width: 150dp; height: 22.75dp; } + +#unitcount { top: 46.75dp; } +#credits { top: 93.875dp; } +#techlevel { top: 141dp; } +#ailevel { top: 188.125dp; } +#aiplayers { top: 235.25dp; } +#gamespeed { top: 282.375dp; } + +/* Multiplay Map, OK and Cancel, all 14 dialog units tall. */ +.button { height: 22.75dp; line-height: 22.75dp; } + +#multimap { left: 439dp; top: 318.125dp; width: 165dp; } +#accept { left: 439dp; top: 345.75dp; width: 75dp; } +#cancel { left: 529dp; top: 345.75dp; width: 75dp; } + +/* The preview frame's caption, a static. + The dlgsys sheets, as optionsbase.rcss gives every other static. */ +#previewtext +{ + font-family: dlgsys; + font-size: 18dp; +} + +/* The map name comes from the scenario, not from a template, so this one keeps the clip + optionsbase.rcss drops for a static caption. At 16.25dp a dlgsys cell is not clipped. */ +#scenarioname { overflow: hidden; } diff --git a/ui/skirmish.rml b/ui/skirmish.rml new file mode 100644 index 000000000..3f3ed2cb7 --- /dev/null +++ b/ui/skirmish.rml @@ -0,0 +1,63 @@ + + + Skirmish + + + + +
+
Name:
+ + +
Side:
+ + +
Color:
+ + +
Map:
+
{{ scenarioname }}
+ +
+
Preview
+ +
+ +
+
Bases
+
Crates
+
Fog Of War
+
Bridges Destroyable
+
Re-Deployable MCV
+
Short Game
+
Multi Engineer
+ +
+
Unit Count:
+ + +
Credits:
+ + +
Tech Level:
+ + +
AI Level:
+ + +
AI Players:
+ + +
Game Speed
+ + +
Multiplay Map
+
[[TXT_OK]]
+
[[TXT_CANCEL]]
+
+ +
diff --git a/ui/sound.rcss b/ui/sound.rcss new file mode 100644 index 000000000..2961ca776 --- /dev/null +++ b/ui/sound.rcss @@ -0,0 +1,173 @@ +/* The sound controls the game shows during play. The geometry is the + IDD_SOUND_OPTIONS_DIALOG template's, converted from dialog units at the 8 point MS Sans + Serif the template names: 1.5 pixels across and 1.625 down, with a child's offset taken + from the panel's content box and a bordered element's declared size taken inside its own + border. + + The dialog look, including the panel artwork, comes from ui/optionsbase.rcss. */ + +body +{ + font-family: LatoLatin; + font-size: 12dp; + color: #70ff00; + + width: 100%; + height: 100%; +} + +/* 294 x 215 dialog units. The panel is centred on the frame and re-centres itself when the + frame is resized, because the offsets are a proportion of the context. */ +#dialog +{ + display: block; + position: absolute; + left: 50%; + top: 50%; + margin-left: -220.5dp; + margin-top: -174.7dp; + + width: 437dp; + height: 345.4dp; +} + +/* The three right-aligned captions, 70 x 15 dialog units at x 22. */ +.label +{ + display: block; + position: absolute; + left: 33dp; + width: 105dp; + height: 24.4dp; + line-height: 24.4dp; + text-align: right; +} + +#musiclabel { top: 19.5dp; } +#soundlabel { top: 55.3dp; } +#voicelabel { top: 91dp; } + +/* The three track bars, 175 x 15 dialog units at x 97. TBS_NOTICKS, so the bar is a plain + groove with a thumb. */ +.slider +{ + display: block; + position: absolute; + left: 145.5dp; + width: 262.5dp; + height: 24.4dp; +} + +#music { top: 19.5dp; } +#sound { top: 55.3dp; } +#voice { top: 91dp; } + +/* The track list, 175 x 99 dialog units at 97, 82. */ +#tracklist +{ + display: block; + position: absolute; + left: 145.5dp; + top: 133.3dp; + width: 262.5dp; + height: 160.9dp; + + background-color: #000000b4; + overflow-y: auto; + overflow-x: hidden; +} + +/* A row spans the list less the scrollbar. The width is stated rather than left at 100%, + because a scrolling container gives its children no width to be a proportion of. */ +.track +{ + display: block; + box-sizing: border-box; + width: 250.5dp; + padding: 1dp 4dp; + white-space: nowrap; + color: #70ff00; +} + +#tracklist scrollbarvertical +{ + width: 18dp; + background-color: #000000b4; +} + +#tracklist scrollbarvertical slidertrack +{ + width: 18dp; + margin-top: 0dp; + background-color: transparent; + border-width: 0dp; +} + +#tracklist scrollbarvertical sliderbar +{ + width: 18dp; + min-height: 20dp; + + decorator: tiled-vertical(sbgript.pcx, sbgripm.pcx, sbgripb.pcx); +} + +#tracklist scrollbarvertical sliderarrowdec, +#tracklist scrollbarvertical sliderarrowinc +{ + width: 18dp; + height: 22dp; +} + +#tracklist scrollbarvertical sliderarrowdec { decorator: image(uparrowr.pcx); } +#tracklist scrollbarvertical sliderarrowinc { decorator: image(dnarrowr.pcx); } +#tracklist scrollbarvertical sliderarrowdec:active { decorator: image(uparrowp.pcx); } +#tracklist scrollbarvertical sliderarrowinc:active { decorator: image(dnarrowp.pcx); } + +.track:hover { color: #70ff00; } + +.track.picked +{ + background-color: #225061; + color: #70ff00; +} + +/* Play and Stop, 70 x 14 dialog units at x 22, and OK, 62 x 14 at 210, 189. */ +.button +{ + display: block; + position: absolute; + height: 22.8dp; + line-height: 22.8dp; + text-align: center; +} + +#play { left: 33dp; top: 139.8dp; width: 101dp; } +#stop { left: 33dp; top: 183.6dp; width: 101dp; } +#ok { left: 315dp; top: 307.1dp; width: 89dp; } + + +/* Shuffle and Repeat, 70 x 14 dialog units at x 22. */ +.check +{ + display: block; + position: absolute; + left: 33dp; + width: 101dp; + height: 22.8dp; + line-height: 22.8dp; +} + +#shuffle { top: 227.5dp; } +#repeat { top: 271.4dp; } + + +/* With no audio device every control the dialog disabled is dimmed and inert. OK stays + live, because the dialog left it enabled. */ +.unavailable .slider, +.unavailable #tracklist, +.unavailable .check, +.unavailable #play, +.unavailable #stop +{ + pointer-events: none; +} diff --git a/ui/sound.rml b/ui/sound.rml new file mode 100644 index 000000000..43ee7a971 --- /dev/null +++ b/ui/sound.rml @@ -0,0 +1,30 @@ + + + Sound controls + + + + +
+
Music Volume:
+ + +
Sound Volume:
+ + +
Voice Volume:
+ + +
+
{{ track.label }}
+
+ +
Play
+
Stop
+
Shuffle
+
Repeat
+ +
OK
+
+ +
diff --git a/ui/soundlite.rcss b/ui/soundlite.rcss new file mode 100644 index 000000000..029cb61ba --- /dev/null +++ b/ui/soundlite.rcss @@ -0,0 +1,85 @@ +/* The sound controls the game shows with no game running. The geometry is the + IDD_SOUND_OPTIONS_DIALOG_LITE template's, converted from dialog units at the 8 point MS + Sans Serif the template names: 1.5 pixels across and 1.625 down, with a child's offset + taken from the panel's content box and a bordered element's declared size taken inside + its own border. It carries the three volumes alone, as that template does. + + The dialog look, including the panel artwork, comes from ui/optionsbase.rcss. */ + +body +{ + font-family: LatoLatin; + font-size: 12dp; + color: #70ff00; + + width: 100%; + height: 100%; +} + +/* 294 x 112 dialog units. The panel is centred on the frame and re-centres itself when the + frame is resized, because the offsets are a proportion of the context. */ +#dialog +{ + display: block; + position: absolute; + left: 50%; + top: 50%; + margin-left: -220.5dp; + margin-top: -91dp; + + width: 437dp; + height: 178dp; +} + +/* The three right-aligned captions, 70 x 15 dialog units at x 22. */ +.label +{ + display: block; + position: absolute; + left: 33dp; + width: 105dp; + height: 24.4dp; + line-height: 24.4dp; + text-align: right; +} + +#musiclabel { top: 27.6dp; } +#soundlabel { top: 65dp; width: 106.5dp; } +#voicelabel { top: 100.8dp; width: 106.5dp; } + +/* The three track bars, 173 x 15 dialog units at x 99. TBS_NOTICKS, so the bar is a plain + groove with a thumb. */ +.slider +{ + display: block; + position: absolute; + left: 148.5dp; + width: 259.5dp; + height: 24.4dp; +} + +#music { top: 27.6dp; } +#sound { top: 65dp; } +#voice { top: 100.8dp; } + +/* OK, 62 x 14 dialog units at 115, 86, centred across the panel as the template places it. */ +.button +{ + display: block; + position: absolute; + left: 172.5dp; + top: 139.8dp; + width: 89dp; + height: 22.8dp; + line-height: 22.8dp; + text-align: center; +} + + +/* With no audio device the sliders are dimmed and inert. OK stays live, because the dialog + left it enabled. */ +.unavailable .slider +{ + color: #909090; + pointer-events: none; +} diff --git a/ui/soundlite.rml b/ui/soundlite.rml new file mode 100644 index 000000000..700c69117 --- /dev/null +++ b/ui/soundlite.rml @@ -0,0 +1,21 @@ + + + Sound controls + + + + +
+
Music Volume:
+ + +
Sound Volume:
+ + +
Voice Volume:
+ + +
OK
+
+ +
diff --git a/ui/uitest.rcss b/ui/uitest.rcss new file mode 100644 index 000000000..b7c366f0e --- /dev/null +++ b/ui/uitest.rcss @@ -0,0 +1,86 @@ +/* The shell's test document. Debug builds only; it exists to be looked at. It stays + within the styling profile docs/UI_DESIGN.md declares: text, ordinary layout, borders + and basic decorators, with no filter, layer, shader or transform. */ + +body +{ + font-family: LatoLatin; + font-size: 14dp; + color: #e8f0e8; + + /* The body covers the frame so the panel can be placed in it, and passes every + pointer through so that only the panel itself claims a click. */ + width: 100%; + height: 100%; + pointer-events: none; +} + +#panel +{ + display: block; + pointer-events: auto; + + position: absolute; + left: 40dp; + top: 40dp; + width: 320dp; + + padding: 12dp; + background-color: #0c1c10e0; + border: 2dp #57d06a; +} + +#title +{ + display: block; + font-size: 20dp; + color: #8bff9c; + margin-bottom: 8dp; +} + +.row +{ + display: block; + margin-bottom: 4dp; +} + +#swatches +{ + display: block; + margin-top: 10dp; + margin-bottom: 10dp; +} + +.swatch +{ + display: inline-block; + width: 48dp; + height: 24dp; + margin-right: 6dp; + border: 1dp #ffffff; +} + +.red { background-color: #d03a3a; } +.green { background-color: #3ad04a; } +.blue { background-color: #3a6ad0; } +.fade { background-color: #ffffff60; } + +#hit +{ + display: block; + padding: 6dp; + background-color: #1d3a24; + border: 1dp #57d06a; + text-align: center; +} + +#hit:hover +{ + background-color: #2d5f38; +} + +#hit:active +{ + background-color: #57d06a; + color: #0c1c10; +} diff --git a/ui/uitest.rml b/ui/uitest.rml new file mode 100644 index 000000000..7455c9138 --- /dev/null +++ b/ui/uitest.rml @@ -0,0 +1,21 @@ + + + OpenTS UI shell test + + + +
+
OpenTS UI shell
+
This document is drawn by RmlUi on bgfx.
+
A click on the panel is consumed.
+
A click beside it reaches the game.
+
+
+
+
+
+
+
Click the panel
+
+ +
diff --git a/ui/version.rcss b/ui/version.rcss new file mode 100644 index 000000000..238f95b0c --- /dev/null +++ b/ui/version.rcss @@ -0,0 +1,69 @@ +/* The version information screen. Its geometry is the IDD_VERSION template's, converted + from dialog units at the 8 point MS Sans Serif the template names: 1.5 pixels across and + 1.625 down. One authored dp is one game logical unit, so the screen keeps the size the + dialog had while its text is rasterized at the window's own resolution. + + The dialog look, including the panel artwork, comes from ui/optionsbase.rcss. */ + +body +{ + font-family: LatoLatin; + font-size: 12dp; + color: #70ff00; + + width: 100%; + height: 100%; +} + +/* 272 x 106 dialog units. The panel is centred on the frame and re-centres itself when the + frame is resized, because the offsets are a proportion of the context. */ +#dialog +{ + display: block; + position: absolute; + left: 50%; + top: 50%; + margin-left: -204dp; + margin-top: -86dp; + + /* The bevel sits outside the declared size, so the content box is the template's + 408 by 172 pixels less the two device-independent pixels of border on each side. */ + width: 404dp; + height: 168dp; +} + +/* The list box: 22, 12, 228 x 55 dialog units. It clips rather than scrolls, as the + template's list box did, and the clip is what exercises the renderer's scissor. */ +#info +{ + display: block; + position: absolute; + left: 31dp; + top: 18dp; + width: 342dp; + height: 89dp; + overflow: hidden; +} + +.line +{ + display: block; + height: 14dp; + line-height: 14dp; + white-space: nowrap; +} + +/* The OK button: 111, 80, 50 x 14 dialog units, with the bevel inverted while it is held. */ +#ok +{ + display: block; + position: absolute; + left: 165dp; + top: 128dp; + width: 71dp; + height: 19dp; + line-height: 19dp; + text-align: center; +} + + diff --git a/ui/version.rml b/ui/version.rml new file mode 100644 index 000000000..acdf9245c --- /dev/null +++ b/ui/version.rml @@ -0,0 +1,15 @@ + + + Version information + + + + +
+
+
{{ line }}
+
+
OK
+
+ +
diff --git a/ui/waitbox.rcss b/ui/waitbox.rcss new file mode 100644 index 000000000..f47e1b61b --- /dev/null +++ b/ui/waitbox.rcss @@ -0,0 +1,64 @@ +/* The wait box that stands over a long operation. Its geometry is the IDD_MSGBOX_1 + template's, converted from dialog units the way ui/messagebox.rcss describes. The cancel + button is hidden unless the caller supplies a caption, which is the template's own + NOT WS_VISIBLE. */ + +body +{ + font-family: LatoLatin; + font-size: 12dp; + color: #70ff00; + + width: 100%; + height: 100%; +} + +/* 218 x 64 dialog units. */ +#dialog +{ + display: block; + position: absolute; + left: 50%; + top: 50%; + margin-left: -163.5dp; + margin-top: -52dp; + + width: 323dp; + height: 100dp; +} + +/* 22, 12, 174 x 23 dialog units. */ +#text +{ + display: block; + position: absolute; + left: 31dp; + top: 17.5dp; + width: 261dp; + height: 37.375dp; + + text-align: center; + white-space: pre-line; + overflow: hidden; +} + +/* 83, 38, 50 x 14 dialog units. */ +#cancel +{ + display: block; + position: absolute; + left: 122.5dp; + top: 59.75dp; + width: 71dp; + height: 18.75dp; + line-height: 18.75dp; + text-align: center; +} + +/* The message, a static. + The dlgsys sheets, as optionsbase.rcss gives every other static. */ +#text +{ + font-family: dlgsys; + font-size: 18dp; +} diff --git a/ui/waitbox.rml b/ui/waitbox.rml new file mode 100644 index 000000000..4de49e932 --- /dev/null +++ b/ui/waitbox.rml @@ -0,0 +1,13 @@ + + + Please wait + + + + +
+
{{ message }}
+
{{ cancelcaption }}
+
+ +