diff --git a/docs/tr2/CHANGELOG.md b/docs/tr2/CHANGELOG.md index c4edadc718..cb5d272b7c 100644 --- a/docs/tr2/CHANGELOG.md +++ b/docs/tr2/CHANGELOG.md @@ -1,12 +1,15 @@ ## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-0.5...develop) - ××××-××-×× - improved FMV mode appearance - removed black scanlines (#1729) - improved FMV mode behavior - stopped switching screen resolutions (#1729) +- improved screenshots: now saved in the screenshots/ directory with level titles and timestamps as JPG or PNG, similar to TR1X (#1773) - improved switch object names - Switch Type 1 renamed to "Airlock Switch" - Switch Type 2 renamed to "Small Switch" - Switch Type 3 renamed to "Switch Button" - Switch Type 4 renamed to "Lever/Switch" - Switch Type 5 renamed to "Underwater Lever/Switch" +- fixed screenshots not working in windowed mode (#1766) +- fixed screenshots key not getting debounced (#1773) - fixed `/give` not working with weapons (regression from 0.5) - fixed the camera being cut off after using the gong hammer in Ice Palace (#1580) - fixed the audio not being in sync when Lara strikes the gong in Ice Palace (#1725) diff --git a/src/libtrx/game/clock.c b/src/libtrx/game/clock.c new file mode 100644 index 0000000000..e7da84fc3a --- /dev/null +++ b/src/libtrx/game/clock.c @@ -0,0 +1,15 @@ +#include "game/clock.h" + +#include +#include + +size_t Clock_GetDateTime(char *const buffer, const size_t size) +{ + time_t lt = time(0); + struct tm *tptr = localtime(<); + + return snprintf( + buffer, size, "%04d%02d%02d_%02d%02d%02d", tptr->tm_year + 1900, + tptr->tm_mon + 1, tptr->tm_mday, tptr->tm_hour, tptr->tm_min, + tptr->tm_sec); +} diff --git a/src/libtrx/include/libtrx/game/clock.h b/src/libtrx/include/libtrx/game/clock.h index 13e8d78c2e..a74c39c615 100644 --- a/src/libtrx/include/libtrx/game/clock.h +++ b/src/libtrx/include/libtrx/game/clock.h @@ -1,3 +1,6 @@ #pragma once +#include + +size_t Clock_GetDateTime(char *buffer, size_t size); extern double Clock_GetHighPrecisionCounter(void); diff --git a/src/libtrx/include/libtrx/game/game.h b/src/libtrx/include/libtrx/game/game.h index 56b4a7c640..b3709e2d64 100644 --- a/src/libtrx/include/libtrx/game/game.h +++ b/src/libtrx/include/libtrx/game/game.h @@ -6,3 +6,4 @@ extern bool Game_IsPlayable(void); extern GAMEFLOW_LEVEL_TYPE Game_GetCurrentLevelType(void); +extern int32_t Game_GetCurrentLevelNum(void); diff --git a/src/libtrx/include/libtrx/game/output.h b/src/libtrx/include/libtrx/game/output.h new file mode 100644 index 0000000000..cd3d56f535 --- /dev/null +++ b/src/libtrx/include/libtrx/game/output.h @@ -0,0 +1,3 @@ +#pragma once + +extern bool Output_MakeScreenshot(const char *path); diff --git a/src/libtrx/include/libtrx/output.h b/src/libtrx/include/libtrx/output.h new file mode 100644 index 0000000000..6b4d2ba17f --- /dev/null +++ b/src/libtrx/include/libtrx/output.h @@ -0,0 +1,3 @@ +#pragma once + +extern Output_MakeScreenshot(const char const path); diff --git a/src/libtrx/include/libtrx/screenshot.h b/src/libtrx/include/libtrx/screenshot.h new file mode 100644 index 0000000000..81fbaa654e --- /dev/null +++ b/src/libtrx/include/libtrx/screenshot.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +typedef enum { + SCREENSHOT_FORMAT_JPEG, + SCREENSHOT_FORMAT_PNG, +} SCREENSHOT_FORMAT; + +bool Screenshot_Make(SCREENSHOT_FORMAT format); diff --git a/src/libtrx/meson.build b/src/libtrx/meson.build index e1bdc3a632..31939a157e 100644 --- a/src/libtrx/meson.build +++ b/src/libtrx/meson.build @@ -68,6 +68,7 @@ sources = [ 'event_manager.c', 'filesystem.c', 'game/backpack.c', + 'game/clock.c', 'game/console/cmd/config.c', 'game/console/cmd/die.c', 'game/console/cmd/end_level.c', @@ -122,6 +123,7 @@ sources = [ 'json/json_write.c', 'log.c', 'memory.c', + 'screenshot.c', 'strings/common.c', 'strings/fuzzy_match.c', 'vector.c', diff --git a/src/libtrx/screenshot.c b/src/libtrx/screenshot.c new file mode 100644 index 0000000000..2e1e76cb3c --- /dev/null +++ b/src/libtrx/screenshot.c @@ -0,0 +1,141 @@ +#include "screenshot.h" + +#include "filesystem.h" +#include "game/clock.h" +#include "game/game.h" +#include "game/gameflow/common.h" +#include "game/output.h" +#include "memory.h" + +#include +#include + +#define SCREENSHOTS_DIR "screenshots" + +static char *M_GetScreenshotTitle(void); +static char *M_CleanScreenshotTitle(const char *source); +static char *M_GetScreenshotBaseName(void); +static const char *M_GetScreenshotFileExt(SCREENSHOT_FORMAT format); +static char *M_GetScreenshotPath(SCREENSHOT_FORMAT format); + +static char *M_CleanScreenshotTitle(const char *const source) +{ + // Sanitize screenshot title. + // - Replace spaces with underscores + // - Remove all non-alphanumeric characters + // - Merge consecutive underscores together + // - Remove leading underscores + // - Remove trailing underscores + char *result = Memory_Alloc(strlen(source) + 1); + + bool last_was_underscore = false; + char *out = result; + for (size_t i = 0; i < strlen(source); i++) { + if (source[i] == ' ' || source[i] == '_') { + if (!last_was_underscore && out > result) { + *out++ = '_'; + last_was_underscore = true; + } + } else if (((source[i] >= 'A' && source[i] <= 'Z') + || (source[i] >= 'a' && source[i] <= 'z') + || (source[i] >= '0' && source[i] <= '9'))) { + *out++ = source[i]; + last_was_underscore = false; + } + } + *out++ = '\0'; + + // Strip trailing underscores + while (out[-1] == '_' && out >= result) { + out--; + } + *out = '\0'; + + return result; +} + +static char *M_GetScreenshotTitle(void) +{ + const int32_t level_num = Game_GetCurrentLevelNum(); + if (level_num < 0) { + return Memory_DupStr("Intro"); + } + + const char *const level_title = Gameflow_GetLevelTitle(level_num); + if (level_title != NULL && strlen(level_title) > 0) { + char *clean_level_title = M_CleanScreenshotTitle(level_title); + if (clean_level_title != NULL && strlen(clean_level_title) > 0) { + return clean_level_title; + } + Memory_FreePointer(clean_level_title); + } + + // If title totally invalid, name it based on level number + const char *const fmt = "Level_%d"; + const size_t result_size = snprintf(NULL, 0, fmt, level_num) + 1; + char *result = Memory_Alloc(result_size); + snprintf(result, result_size, fmt, level_num); + return result; +} + +static char *M_GetScreenshotBaseName(void) +{ + char *screenshot_title = M_GetScreenshotTitle(); + + // Get timestamp + char date_time[30]; + Clock_GetDateTime(date_time, 30); + + // Full screenshot name + const char *const fmt = "%s_%s"; + const size_t out_size = + snprintf(NULL, 0, fmt, date_time, screenshot_title) + 1; + char *out = Memory_Alloc(out_size); + snprintf(out, out_size, "%s_%s", date_time, screenshot_title); + return out; +} + +static const char *M_GetScreenshotFileExt(const SCREENSHOT_FORMAT format) +{ + switch (format) { + case SCREENSHOT_FORMAT_JPEG: + return "jpg"; + case SCREENSHOT_FORMAT_PNG: + return "png"; + default: + return "jpg"; + } +} + +static char *M_GetScreenshotPath(const SCREENSHOT_FORMAT format) +{ + char *base_name = M_GetScreenshotBaseName(); + const char *const ext = M_GetScreenshotFileExt(format); + + char *full_path = Memory_Alloc( + strlen(SCREENSHOTS_DIR) + strlen(base_name) + strlen(ext) + 6); + sprintf(full_path, "%s/%s.%s", SCREENSHOTS_DIR, base_name, ext); + if (File_Exists(full_path)) { + for (int i = 2; i < 100; i++) { + sprintf( + full_path, "%s/%s_%d.%s", SCREENSHOTS_DIR, base_name, i, ext); + if (!File_Exists(full_path)) { + break; + } + } + } + + Memory_FreePointer(&base_name); + return full_path; +} + +bool Screenshot_Make(const SCREENSHOT_FORMAT format) +{ + File_CreateDirectory(SCREENSHOTS_DIR); + + char *full_path = M_GetScreenshotPath(format); + const bool result = Output_MakeScreenshot(full_path); + Memory_FreePointer(&full_path); + + return result; +} diff --git a/src/tr1/config.h b/src/tr1/config.h index caa7444d72..d1abe5bcce 100644 --- a/src/tr1/config.h +++ b/src/tr1/config.h @@ -4,6 +4,7 @@ #include #include +#include #include #include diff --git a/src/tr1/game/clock.c b/src/tr1/game/clock.c index 2196ee31fa..7a573ecc75 100644 --- a/src/tr1/game/clock.c +++ b/src/tr1/game/clock.c @@ -10,7 +10,6 @@ #include #include #include -#include static double M_GetElapsedUnit(CLOCK_TIMER *const timer, const double unit); static bool M_CheckElapsedUnit( @@ -76,17 +75,6 @@ double Clock_GetSpeedMultiplier(void) } } -void Clock_GetDateTime(char *date_time) -{ - time_t lt = time(0); - struct tm *tptr = localtime(<); - - sprintf( - date_time, "%04d%02d%02d_%02d%02d%02d", tptr->tm_year + 1900, - tptr->tm_mon + 1, tptr->tm_mday, tptr->tm_hour, tptr->tm_min, - tptr->tm_sec); -} - int32_t Clock_GetFrameAdvance(void) { return g_Config.rendering.fps == 30 ? 2 : 1; diff --git a/src/tr1/game/clock.h b/src/tr1/game/clock.h index 1609fb4089..2feefb2ff5 100644 --- a/src/tr1/game/clock.h +++ b/src/tr1/game/clock.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -44,6 +46,4 @@ bool Clock_CheckElapsedMilliseconds(CLOCK_TIMER *timer, int32_t wait); // by the turbo cheat multiplier. bool Clock_CheckElapsedRawMilliseconds(CLOCK_TIMER *timer, int32_t how_often); -void Clock_GetDateTime(char *date_time); - int32_t Clock_GetFrameAdvance(void); diff --git a/src/tr1/game/game/game.c b/src/tr1/game/game/game.c index 9e94e508d1..6ce45ec1ea 100644 --- a/src/tr1/game/game/game.c +++ b/src/tr1/game/game/game.c @@ -252,6 +252,11 @@ GAMEFLOW_LEVEL_TYPE Game_GetCurrentLevelType(void) return g_GameInfo.current_level_type; } +extern int32_t Game_GetCurrentLevelNum(void) +{ + return g_CurrentLevel; +} + bool Game_IsPlayable(void) { if (g_GameInfo.current_level_type == GFL_TITLE diff --git a/src/tr1/game/output.c b/src/tr1/game/output.c index ef8748d1f4..a07c35dcc5 100644 --- a/src/tr1/game/output.c +++ b/src/tr1/game/output.c @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -1364,9 +1365,10 @@ void Output_ApplyTint(float *r, float *g, float *b) } } -bool Output_MakeScreenshot(const char *path) +bool Output_MakeScreenshot(const char *const path) { - return S_Output_MakeScreenshot(path); + GFX_Context_ScheduleScreenshot(path); + return true; } int Output_GetObjectBounds(const BOUNDS_16 *const bounds) diff --git a/src/tr1/game/phase/phase_photo_mode.c b/src/tr1/game/phase/phase_photo_mode.c index 867abed94c..ff09a6ea3c 100644 --- a/src/tr1/game/phase/phase_photo_mode.c +++ b/src/tr1/game/phase/phase_photo_mode.c @@ -73,7 +73,7 @@ static void M_End(void) static PHASE_CONTROL M_Control(int32_t nframes) { if (m_Status == PS_ACTIVE) { - Shell_MakeScreenshot(); + Screenshot_Make(g_Config.screenshot_format); Sound_Effect(SFX_MENU_CHOOSE, NULL, SPM_ALWAYS); m_Status = PS_COOLDOWN; } else if (m_Status == PS_COOLDOWN) { diff --git a/src/tr1/game/savegame/savegame_bson.c b/src/tr1/game/savegame/savegame_bson.c index bc06d1f331..3657222917 100644 --- a/src/tr1/game/savegame/savegame_bson.c +++ b/src/tr1/game/savegame/savegame_bson.c @@ -1248,7 +1248,7 @@ static JSON_OBJECT *M_DumpCurrentMusic(void) { JSON_OBJECT *current_music_obj = JSON_ObjectNew(); JSON_ObjectAppendInt( - current_music_obj, "current_track", Music_GetCurrentTrack()); + current_music_obj, "current_track", Music_GetCurrentPlayingTrack()); JSON_ObjectAppendDouble( current_music_obj, "timestamp", Music_GetTimestamp()); diff --git a/src/tr1/game/shell.c b/src/tr1/game/shell.c index 37bafd3337..ce01a8260c 100644 --- a/src/tr1/game/shell.c +++ b/src/tr1/game/shell.c @@ -33,7 +33,6 @@ #include #include -#define SCREENSHOTS_DIR "screenshots" #define LEVEL_TITLE_SIZE 25 #define TIMESTAMP_SIZE 20 @@ -41,79 +40,6 @@ static const char m_TR1XGameflowPath[] = "cfg/TR1X_gameflow.json5"; static const char m_TR1XGameflowGoldPath[] = "cfg/TR1X_gameflow_ub.json5"; static const char m_TR1XGameflowDemoPath[] = "cfg/TR1X_gameflow_demo_pc.json5"; -static char *M_GetScreenshotName(void); - -static char *M_GetScreenshotName(void) -{ - // Get level title of unknown length - char level_title[100]; - - if (g_CurrentLevel < 0) { - strncpy(level_title, "Intro", LEVEL_TITLE_SIZE - 1); - } else { - strncpy( - level_title, g_GameFlow.levels[g_CurrentLevel].level_title, - LEVEL_TITLE_SIZE - 1); - } - level_title[LEVEL_TITLE_SIZE] = '\0'; - - // Prepare level title for screenshot - char *check = level_title; - bool prev_us = true; // '_' after timestamp before title - int idx = 0; - - while (*check != '\0') { - if (*check == ' ') { - // Replace spaces with a single underscore - if (prev_us) { - memmove( - &level_title[idx], &level_title[idx + 1], - strlen(level_title) - idx); - } else { - *check++ = '_'; - idx++; - prev_us = true; - } - } else if (((*check < 'A' || *check > 'Z') - && (*check < 'a' || *check > 'z') - && (*check < '0' || *check > '9'))) { - // Strip non alphanumeric chars - memmove( - &level_title[idx], &level_title[idx + 1], - strlen(level_title) - idx); - } else { - check++; - idx++; - prev_us = false; - } - } - - // If title totally invalid, name it based on level number - if (strlen(level_title) == 0) { - sprintf(level_title, "Level_%d", g_CurrentLevel); - prev_us = false; - } - - // Strip trailing underscores - if (prev_us) { - check--; - idx--; - memmove( - &level_title[idx], &level_title[idx + 1], strlen(level_title) - 1); - prev_us = false; - } - - // Get timestamp - char date_time[TIMESTAMP_SIZE]; - Clock_GetDateTime(date_time); - - // Full screenshot name - size_t out_size = snprintf(NULL, 0, "%s_%s", date_time, level_title) + 1; - char *out = Memory_Alloc(out_size); - snprintf(out, out_size, "%s_%s", date_time, level_title); - return out; -} - void Shell_Init(const char *gameflow_path) { Text_Init(); @@ -343,45 +269,3 @@ void Shell_ProcessInput(void) Clock_CycleTurboSpeed(!g_Input.slow); } } - -bool Shell_MakeScreenshot(void) -{ - File_CreateDirectory(SCREENSHOTS_DIR); - - char *filename = M_GetScreenshotName(); - - const char *ext; - switch (g_Config.screenshot_format) { - case SCREENSHOT_FORMAT_JPEG: - ext = "jpg"; - break; - case SCREENSHOT_FORMAT_PNG: - ext = "png"; - break; - default: - ext = "jpg"; - break; - } - - bool result = false; - char *full_path = Memory_Alloc( - strlen(SCREENSHOTS_DIR) + strlen(filename) + strlen(ext) + 6); - sprintf(full_path, "%s/%s.%s", SCREENSHOTS_DIR, filename, ext); - if (!File_Exists(full_path)) { - result = Output_MakeScreenshot(full_path); - } else { - // name already exists, so add a number to name - for (int i = 2; i < 100; i++) { - sprintf( - full_path, "%s/%s_%d.%s", SCREENSHOTS_DIR, filename, i, ext); - if (!File_Exists(full_path)) { - result = Output_MakeScreenshot(full_path); - break; - } - } - } - - Memory_FreePointer(&filename); - Memory_FreePointer(&full_path); - return result; -} diff --git a/src/tr1/game/shell.h b/src/tr1/game/shell.h index d88d32f26b..86cad4439f 100644 --- a/src/tr1/game/shell.h +++ b/src/tr1/game/shell.h @@ -8,4 +8,3 @@ void Shell_Shutdown(void); void Shell_Main(void); void Shell_ExitSystem(const char *message); void Shell_ExitSystemFmt(const char *fmt, ...); -bool Shell_MakeScreenshot(void); diff --git a/src/tr1/global/enum_map.c b/src/tr1/global/enum_map.c index c37947cb98..9b95c13b01 100644 --- a/src/tr1/global/enum_map.c +++ b/src/tr1/global/enum_map.c @@ -2,6 +2,7 @@ #include #include +#include void EnumMap_Init(void) { diff --git a/src/tr1/global/types.h b/src/tr1/global/types.h index a5b0601f1a..95c5fdb224 100644 --- a/src/tr1/global/types.h +++ b/src/tr1/global/types.h @@ -942,11 +942,6 @@ typedef enum { TB_ON = 1, } TRISTATE_BOOL; -typedef enum { - SCREENSHOT_FORMAT_JPEG, - SCREENSHOT_FORMAT_PNG, -} SCREENSHOT_FORMAT; - typedef enum { UI_STYLE_PS1, UI_STYLE_PC, diff --git a/src/tr1/specific/s_output.c b/src/tr1/specific/s_output.c index 3bf47d40df..2c7819d6fc 100644 --- a/src/tr1/specific/s_output.c +++ b/src/tr1/specific/s_output.c @@ -1272,12 +1272,6 @@ void S_Output_DownloadTextures(int32_t pages) m_EnvMapTexture = GFX_3D_Renderer_RegisterEnvironmentMap(m_Renderer3D); } -bool S_Output_MakeScreenshot(const char *path) -{ - GFX_Context_ScheduleScreenshot(path); - return true; -} - void S_Output_ScreenBox( int32_t sx, int32_t sy, int32_t w, int32_t h, RGBA_8888 col_dark, RGBA_8888 col_light, float thickness) diff --git a/src/tr1/specific/s_output.h b/src/tr1/specific/s_output.h index f76108f965..2dc5475f65 100644 --- a/src/tr1/specific/s_output.h +++ b/src/tr1/specific/s_output.h @@ -58,8 +58,6 @@ void S_Output_DrawLightningSegment( int x1, int y1, int z1, int thickness1, int x2, int y2, int z2, int thickness2); -bool S_Output_MakeScreenshot(const char *path); - void S_Output_ScreenBox( int32_t sx, int32_t sy, int32_t w, int32_t h, RGBA_8888 col_dark, RGBA_8888 col_light, float thickness); diff --git a/src/tr1/specific/s_shell.c b/src/tr1/specific/s_shell.c index af6eefc7a2..0a7bc1f030 100644 --- a/src/tr1/specific/s_shell.c +++ b/src/tr1/specific/s_shell.c @@ -193,7 +193,7 @@ void S_Shell_SpinMessageLoop(void) case SDL_KEYUP: if (event.key.keysym.sym == SDLK_PRINTSCREEN) { - Shell_MakeScreenshot(); + Screenshot_Make(g_Config.screenshot_format); break; } diff --git a/src/tr2/config.h b/src/tr2/config.h index 2b3229a8bb..b8b244920b 100644 --- a/src/tr2/config.h +++ b/src/tr2/config.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include @@ -10,6 +11,10 @@ typedef struct { struct { bool fix_m16_accuracy; } gameplay; + + struct { + SCREENSHOT_FORMAT screenshot_format; + } rendering; } CONFIG; extern CONFIG g_Config; diff --git a/src/tr2/config_map.def b/src/tr2/config_map.def index be3e668617..addba8f0fc 100644 --- a/src/tr2/config_map.def +++ b/src/tr2/config_map.def @@ -1 +1,2 @@ CFG_BOOL(g_Config, gameplay.fix_m16_accuracy, true) +CFG_ENUM(g_Config, rendering.screenshot_format, SCREENSHOT_FORMAT_JPEG, SCREENSHOT_FORMAT) diff --git a/src/tr2/decomp/decomp.c b/src/tr2/decomp/decomp.c index 990a0e87a2..0cebf59eae 100644 --- a/src/tr2/decomp/decomp.c +++ b/src/tr2/decomp/decomp.c @@ -32,7 +32,11 @@ #include "lib/dinput.h" #include "specific/s_flagged_string.h" +#include +#include #include +#include +#include #include #include @@ -276,245 +280,6 @@ int32_t __cdecl RenderErrorBox(int32_t error_code) return UT_MessageBox(buffer, 0); } -void __cdecl ScreenshotPCX(void) -{ - LPDDS screen = g_SavedAppSettings.render_mode == RM_SOFTWARE - ? g_RenderBufferSurface - : g_PrimaryBufferSurface; - - DDSURFACEDESC desc = { .dwSize = sizeof(DDSURFACEDESC), 0 }; - - int32_t result; - while (true) { - result = IDirectDrawSurface_Lock(screen, 0, &desc, 0, 0); - if (result != DDERR_WASSTILLDRAWING) { - break; - } - } - - if (result == DDERR_SURFACELOST) { - IDirectDrawSurface_Restore(screen); - } - - if (FAILED(result)) { - return; - } - - uint8_t *pcx_data; - int32_t pcx_size = CompPCX( - desc.lpSurface, desc.dwWidth, desc.dwHeight, g_GamePalette8, &pcx_data); - - IDirectDrawSurface_Unlock(screen, &desc); - if (!pcx_size) { - return; - } - - g_ScreenshotCounter++; - if (g_ScreenshotCounter > 9999) { - g_ScreenshotCounter = 1; - } - - char file_name[20]; - sprintf(file_name, "tomb%04d.pcx", g_ScreenshotCounter); - - HANDLE handle = CreateFileA( - file_name, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, - NULL); - DWORD bytes_written; - WriteFile(handle, pcx_data, pcx_size, &bytes_written, 0); - CloseHandle(handle); - GlobalFree(pcx_data); -} - -size_t __cdecl CompPCX( - uint8_t *bitmap, int32_t width, int32_t height, RGB_888 *palette, - uint8_t **pcx_data) -{ - *pcx_data = (uint8_t *)GlobalAlloc( - GMEM_FIXED, - sizeof(PCX_HEADER) + sizeof(RGB_888) * 256 + width * height * 2); - if (*pcx_data == NULL) { - return 0; - } - - PCX_HEADER *pcx_header = (PCX_HEADER *)*pcx_data; - pcx_header->manufacturer = 10; - pcx_header->version = 5; - pcx_header->rle = 1; - pcx_header->bpp = 8; - pcx_header->planes = 1; - pcx_header->x_min = 0; - pcx_header->y_min = 0; - pcx_header->x_max = width - 1; - pcx_header->y_max = height - 1; - pcx_header->h_dpi = width; - pcx_header->v_dpi = height; - pcx_header->bytes_per_line = width; - - uint8_t *pic_data = *pcx_data + sizeof(PCX_HEADER); - for (int32_t y = 0; y < height; y++) { - pic_data += EncodeLinePCX(bitmap, width, pic_data); - bitmap += width; - } - - *pic_data++ = 0x0C; - for (int32_t i = 0; i < 256; i++) { - *pic_data++ = palette[i].red; - *pic_data++ = palette[i].green; - *pic_data++ = palette[i].blue; - } - - return pic_data - *pcx_data + sizeof(RGB_888) * 256; -} - -size_t __cdecl EncodeLinePCX( - const uint8_t *src, const int32_t width, uint8_t *dst) -{ - const uint8_t *const dst_start = dst; - int32_t run_count = 1; - uint8_t last = *src; - - for (int32_t i = 1; i < width; i++) { - uint8_t current = *src++; - if (*src == last) { - run_count++; - if (run_count == 63) { - const size_t add = EncodePutPCX(last, 0x3Fu, dst); - if (add == 0) { - return 0; - } - dst += add; - run_count = 0; - } - } else { - if (run_count != 0) { - const size_t add = EncodePutPCX(last, run_count, dst); - if (add == 0) { - return 0; - } - dst += add; - } - last = current; - run_count = 1; - } - } - - if (run_count != 0) { - const size_t add = EncodePutPCX(last, run_count, dst); - if (add == 0) { - return 0; - } - dst += add; - } - - const size_t total = dst - dst_start; - return total; -} - -size_t __cdecl EncodePutPCX(uint8_t value, uint8_t num, uint8_t *buffer) -{ - if (num == 0 || num > 63) { - return 0; - } - - if (num == 1 && (value & 0xC0) != 0xC0) { - buffer[0] = value; - return 1; - } - - buffer[0] = num | 0xC0; - buffer[1] = value; - return 2; -} - -void __cdecl ScreenshotTGA(IDirectDrawSurface3 *screen, int32_t bpp) -{ - DDSURFACEDESC desc = { - .dwSize = sizeof(DDSURFACEDESC), - 0, - }; - - if (FAILED(WinVidBufferLock(screen, &desc, 0x21u))) { - return; - } - - const int32_t width = desc.dwWidth; - const int32_t height = desc.dwHeight; - - g_ScreenshotCounter++; - - char file_name[20]; - sprintf(file_name, "tomb%04d.tga", g_ScreenshotCounter); - - FILE *handle = fopen(file_name, "wb"); - if (!handle) { - return; - } - - const TGA_HEADER header = { - .id_length = 0, - .color_map_type = 0, - .data_type_code = 2, // Uncompressed, RGB images - .color_map_origin = 0, - .color_map_length = 0, - .color_map_depth = 0, - .x_origin = 0, - .y_origin = 0, - .width = width, - .height = height, - .bpp = 16, - .image_descriptor = 0, - }; - - fwrite(&header, sizeof(TGA_HEADER), 1, handle); - - uint8_t *tga_pic = - (uint8_t *)GlobalAlloc(GMEM_FIXED, width * height * (bpp / 8)); - uint8_t *src = desc.lpSurface + desc.lPitch * (height - 1); - - uint8_t *dst = tga_pic; - for (int32_t y = 0; y < height; y++) { - if (desc.ddpfPixelFormat.dwRBitMask == 0xF800) { - // R5G6B5 - transform - for (int32_t x = 0; x < width; x++) { - const uint16_t sample = ((uint16_t *)src)[x]; - ((uint16_t *)dst)[x] = - ((sample & 0xFFC0) >> 1) | (sample & 0x001F); - } - } else { - // X1R5G5B5 - good - memcpy(dst, src, sizeof(uint16_t) * width); - } - src -= desc.lPitch; - dst += sizeof(uint16_t) * width; - } - fwrite(tga_pic, 2 * height * width, 1, handle); - -cleanup: - if (tga_pic) { - GlobalFree(tga_pic); - } - - if (handle) { - fclose(handle); - } - WinVidBufferUnlock(screen, &desc); -} - -void __cdecl Screenshot(LPDDS screen) -{ - DDSURFACEDESC desc = { 0 }; - desc.dwSize = sizeof(DDSURFACEDESC); - - if (SUCCEEDED(IDirectDrawSurface_GetSurfaceDesc(screen, &desc))) { - if (desc.ddpfPixelFormat.dwRGBBitCount == 8) { - ScreenshotPCX(); - } else if (desc.ddpfPixelFormat.dwRGBBitCount == 16) { - ScreenshotTGA(screen, 16); - } - } -} - bool __cdecl DInputCreate(void) { return SUCCEEDED(DirectInputCreate(g_GameModule, 1280, &g_DInput, NULL)); diff --git a/src/tr2/decomp/decomp.h b/src/tr2/decomp/decomp.h index 453b7b79ad..2d5e70d07c 100644 --- a/src/tr2/decomp/decomp.h +++ b/src/tr2/decomp/decomp.h @@ -11,13 +11,6 @@ int32_t __stdcall WinMain( int32_t nShowCmd); const char *__cdecl DecodeErrorMessage(int32_t error_code); int32_t __cdecl RenderErrorBox(int32_t error_code); -void __cdecl ScreenshotPCX(void); -size_t __cdecl CompPCX( - uint8_t *bitmap, int32_t width, int32_t height, RGB_888 *palette, - uint8_t **pcx_data); -size_t __cdecl EncodeLinePCX(const uint8_t *src, int32_t width, uint8_t *dst); -size_t __cdecl EncodePutPCX(uint8_t value, uint8_t num, uint8_t *buffer); -void __cdecl Screenshot(LPDDS screen); bool __cdecl DInputCreate(void); void __cdecl DInputRelease(void); void __cdecl WinInReadKeyboard(uint8_t *input_data); diff --git a/src/tr2/game/game.c b/src/tr2/game/game.c index 8187e57314..95dec016bd 100644 --- a/src/tr2/game/game.c +++ b/src/tr2/game/game.c @@ -298,6 +298,11 @@ GAMEFLOW_LEVEL_TYPE Game_GetCurrentLevelType(void) return g_GameInfo.current_level.type; } +extern int32_t Game_GetCurrentLevelNum(void) +{ + return g_CurrentLevel; +} + bool Game_IsPlayable(void) { if (g_GameInfo.current_level.type == GFL_TITLE diff --git a/src/tr2/game/output.c b/src/tr2/game/output.c index 842660954f..77311b5d46 100644 --- a/src/tr2/game/output.c +++ b/src/tr2/game/output.c @@ -1,5 +1,6 @@ #include "game/output.h" +#include "decomp/decomp.h" #include "game/hwr.h" #include "game/math.h" #include "game/matrix.h" @@ -8,8 +9,12 @@ #include "global/funcs.h" #include "global/vars.h" +#include +#include #include +#include + #define VBUF_VISIBLE(a, b, c) \ (((a).ys - (b).ys) * ((c).xs - (b).xs) \ >= ((c).ys - (b).ys) * ((a).xs - (b).xs)) @@ -4481,3 +4486,130 @@ void __cdecl Output_DrawScaledSpriteC(const int16_t *const obj_ptr) v_base += v_add; } } + +bool __cdecl Output_MakeScreenshot(const char *const path) +{ + LOG_INFO("Taking screenshot"); + + LPDDS screen = g_SavedAppSettings.render_mode == RM_SOFTWARE + ? g_RenderBufferSurface + : g_PrimaryBufferSurface; + + DDSURFACEDESC desc = { .dwSize = sizeof(DDSURFACEDESC) }; + HRESULT rc; + while (true) { + rc = IDirectDrawSurface_GetSurfaceDesc(screen, &desc); + if (rc != DDERR_WASSTILLDRAWING) { + break; + } + } + if (rc == DDERR_SURFACELOST) { + IDirectDrawSurface_Restore(screen); + } + if (FAILED(rc)) { + LOG_ERROR("Failed to get surface description: %d", GetLastError()); + return false; + } + + if (FAILED( + WinVidBufferLock(screen, &desc, DDLOCK_WRITEONLY | DDLOCK_WAIT))) { + LOG_ERROR("Failed to lock surface: %d", GetLastError()); + return false; + } + + int32_t src_x = g_GameWindowPositionX; + int32_t src_y = g_GameWindowPositionY; + int32_t width = g_GameWindowWidth; + int32_t height = g_GameWindowHeight; + + IMAGE *const image = Image_Create(width, height); + + LOG_DEBUG("bit_count = %d", desc.ddpfPixelFormat.dwRGBBitCount); + LOG_DEBUG("r mask = %08x", desc.ddpfPixelFormat.dwRBitMask); + LOG_DEBUG("g mask = %08x", desc.ddpfPixelFormat.dwGBitMask); + LOG_DEBUG("b mask = %08x", desc.ddpfPixelFormat.dwBBitMask); + for (int32_t y = 0; y < height; y++) { + uint8_t *src = desc.lpSurface + desc.lPitch * (y + src_y); + src += src_x * (desc.ddpfPixelFormat.dwRGBBitCount / 8); + IMAGE_PIXEL *dst = &image->data[width * y]; + + switch (desc.ddpfPixelFormat.dwRGBBitCount) { + case 8: + assert(desc.ddpfPixelFormat.dwFlags & DDPF_PALETTEINDEXED8); + for (int32_t x = 0; x < width; x++) { + dst->r = g_GamePalette8[*src].red; + dst->g = g_GamePalette8[*src].green; + dst->b = g_GamePalette8[*src].blue; + src++; + dst++; + } + break; + + case 16: + if (desc.ddpfPixelFormat.dwRBitMask == 0xF800) { + for (int32_t x = 0; x < width; x++) { + dst->r = (*(uint16_t *)src & 0xF800) >> 8; + dst->g = (*(uint16_t *)src & 0x07E0) >> 3; + dst->b = (*(uint16_t *)src & 0x001F) << 3; + dst++; + src += 2; + } + } else { + for (int32_t x = 0; x < width; x++) { + dst->r = (*(uint16_t *)src & 0x001F) << 3; + dst->g = (*(uint16_t *)src & 0x07E0) >> 3; + dst->b = (*(uint16_t *)src & 0xF800) >> 8; + dst++; + src += 2; + } + } + break; + + case 24: + if (desc.ddpfPixelFormat.dwRBitMask == 255) { + for (int32_t x = 0; x < width; x++) { + dst->r = src[0]; + dst->g = src[1]; + dst->b = src[2]; + dst++; + src += 3; + } + } else { + for (int32_t x = 0; x < width; x++) { + dst->r = src[2]; + dst->g = src[1]; + dst->b = src[0]; + dst++; + src += 3; + } + } + break; + + case 32: + if (desc.ddpfPixelFormat.dwRBitMask == 255) { + for (int32_t x = 0; x < width; x++) { + dst->r = src[1]; + dst->g = src[2]; + dst->b = src[3]; + dst++; + src += 4; + } + } else { + for (int32_t x = 0; x < width; x++) { + dst->r = src[2]; + dst->g = src[1]; + dst->b = src[0]; + dst++; + src += 4; + } + } + } + + src -= desc.lPitch; + } + + const bool ret = Image_SaveToFile(image, path); + Image_Free(image); + WinVidBufferUnlock(screen, &desc); + return ret; +} diff --git a/src/tr2/game/output.h b/src/tr2/game/output.h index b0c26315d8..8dc7c53262 100644 --- a/src/tr2/game/output.h +++ b/src/tr2/game/output.h @@ -171,3 +171,5 @@ void __cdecl Output_DrawScreenSprite( int16_t sprite_idx, int16_t shade, uint16_t flags); void __cdecl Output_DrawScaledSpriteC(const int16_t *obj_ptr); + +bool __cdecl Output_MakeScreenshot(const char *path); diff --git a/src/tr2/global/enum_map.c b/src/tr2/global/enum_map.c index 1acba19e4e..6f5a1f1adf 100644 --- a/src/tr2/global/enum_map.c +++ b/src/tr2/global/enum_map.c @@ -1,5 +1,6 @@ #include #include +#include void EnumMap_Init(void) { diff --git a/src/tr2/global/enum_map.def b/src/tr2/global/enum_map.def index e69de29bb2..12270e03ca 100644 --- a/src/tr2/global/enum_map.def +++ b/src/tr2/global/enum_map.def @@ -0,0 +1,3 @@ +ENUM_MAP_DEFINE(SCREENSHOT_FORMAT, SCREENSHOT_FORMAT_JPEG, "jpg") +ENUM_MAP_DEFINE(SCREENSHOT_FORMAT, SCREENSHOT_FORMAT_JPEG, "jpeg") +ENUM_MAP_DEFINE(SCREENSHOT_FORMAT, SCREENSHOT_FORMAT_PNG, "png") diff --git a/src/tr2/inject_exec.c b/src/tr2/inject_exec.c index 2f8e7d463e..8699d13f93 100644 --- a/src/tr2/inject_exec.c +++ b/src/tr2/inject_exec.c @@ -236,11 +236,6 @@ static void M_DecompGeneral(const bool enable) INJECT(enable, 0x0044E700, GameInit); INJECT(enable, 0x0044E7A0, WinGameStart); INJECT(enable, 0x0044E820, Shell_Shutdown); - INJECT(enable, 0x0044E8E0, ScreenshotPCX); - INJECT(enable, 0x0044E9F0, CompPCX); - INJECT(enable, 0x0044EAA0, EncodeLinePCX); - INJECT(enable, 0x0044EB80, EncodePutPCX); - INJECT(enable, 0x0044EBC0, Screenshot); INJECT(enable, 0x00454C50, TitleSequence); INJECT(enable, 0x004550C0, S_SaveSettings); INJECT(enable, 0x00455140, S_LoadSettings); diff --git a/src/tr2/specific/s_input.c b/src/tr2/specific/s_input.c index 1350889725..f48832c168 100644 --- a/src/tr2/specific/s_input.c +++ b/src/tr2/specific/s_input.c @@ -1,5 +1,6 @@ #include "specific/s_input.h" +#include "config.h" #include "decomp/decomp.h" #include "game/console/common.h" #include "game/gameflow/gameflow_new.h" @@ -10,6 +11,7 @@ #include "global/funcs.h" #include "global/vars.h" +#include #include #include @@ -53,6 +55,7 @@ INPUT_LAYOUT g_Layout[2] = { } } }; +static bool m_IsScreenshotPressed = false; static bool m_IsF3Pressed = false; static bool m_IsF4Pressed = false; static bool m_IsF7Pressed = false; @@ -235,7 +238,12 @@ bool __cdecl S_Input_Update(void) } if (KEY_DOWN(DIK_S)) { - Screenshot(g_PrimaryBufferSurface); + if (!m_IsScreenshotPressed) { + m_IsScreenshotPressed = true; + Screenshot_Make(g_Config.rendering.screenshot_format); + } + } else { + m_IsScreenshotPressed = false; } const bool is_shift_pressed = KEY_DOWN(DIK_LSHIFT) || KEY_DOWN(DIK_RSHIFT); diff --git a/tools/tr2/config/TR2X_ConfigTool/Resources/Lang/en.json b/tools/tr2/config/TR2X_ConfigTool/Resources/Lang/en.json index 9676d128ca..d067a59d86 100644 --- a/tools/tr2/config/TR2X_ConfigTool/Resources/Lang/en.json +++ b/tools/tr2/config/TR2X_ConfigTool/Resources/Lang/en.json @@ -6,10 +6,20 @@ "window_title_about": "About TR2X Config Tool", "label_about_details": "Visit the TR2X GitHub page for further information." }, + "Enums": { + "screenshot_format": { + "jpg": "JPEG", + "png": "PNG" + } + }, "Properties": { "fix_m16_accuracy": { "Title": "Fix M16 accuracy", "Description": "Fixes the accuracy of the M16 while Lara is running." + }, + "screenshot_format": { + "Title": "Screenshot format", + "Description": "Screenshot file format." } } } diff --git a/tools/tr2/config/TR2X_ConfigTool/Resources/specification.json b/tools/tr2/config/TR2X_ConfigTool/Resources/specification.json index 8b24185d9e..b73dac68e3 100644 --- a/tools/tr2/config/TR2X_ConfigTool/Resources/specification.json +++ b/tools/tr2/config/TR2X_ConfigTool/Resources/specification.json @@ -1,4 +1,10 @@ { + "Enums": { + "screenshot_format": [ + "jpg", + "png" + ] + }, "CategorisedProperties": [ { "ID": "controls", @@ -36,7 +42,12 @@ "ID": "graphics", "Image": "Graphics/graphic5.jpg", "Properties": [ - + { + "Field": "screenshot_format", + "DataType": "Enum", + "EnumKey": "screenshot_format", + "DefaultValue": "jpg" + } ] }, {