From 70ff515f4858f8daeaecbfe58bfd7642f29ccec1 Mon Sep 17 00:00:00 2001 From: Gleb Mazovetskiy Date: Sat, 2 Sep 2023 10:22:19 +0100 Subject: [PATCH] Stream demo messages This entirely eliminates the demo memory overhead. --- Packaging/OpenDingux/profile-generate.sh | 2 + Packaging/OpenDingux/rg99-pgo.md | 55 ++++ Source/engine/demomode.cpp | 304 ++++++++++------------- Source/pfile.cpp | 11 + 4 files changed, 205 insertions(+), 167 deletions(-) diff --git a/Packaging/OpenDingux/profile-generate.sh b/Packaging/OpenDingux/profile-generate.sh index ee3067f4513..a0110cee084 100755 --- a/Packaging/OpenDingux/profile-generate.sh +++ b/Packaging/OpenDingux/profile-generate.sh @@ -6,5 +6,7 @@ SAVE_DIR="$(mktemp -d)" ln -s "${PWD}/demo_0_reference_spawn_0_sv" "${SAVE_DIR}/" ln -s "${PWD}/demo_0.dmo" "${SAVE_DIR}/" cp -r "${PWD}/spawn_0_sv" "${SAVE_DIR}/" +rm -rf "${HOME}/devilutionx-profile" +mkdir -p "${HOME}/devilutionx-profile" ./devilutionx --diablo --spawn --demo 0 --timedemo --save-dir "$SAVE_DIR" --data-dir ~/.local/share/diasurgical/devilution rm -rf "$SAVE_DIR" diff --git a/Packaging/OpenDingux/rg99-pgo.md b/Packaging/OpenDingux/rg99-pgo.md index f03e0492ef8..d092f2741ba 100644 --- a/Packaging/OpenDingux/rg99-pgo.md +++ b/Packaging/OpenDingux/rg99-pgo.md @@ -35,3 +35,58 @@ Here are the instructions for producing a PGO'd build. ``` 7. The final package is at `build-rg99/devilutionx-rg99.opk`. + +## Remote Debugging with VS Code + +If the demo crashes and you cannot reproduce this on PC, you can +use a remote debugger to diagnose the issue. + +Unpack the package and copy it to the RG99: + +```bash +cd build-rg99 +rm -rf squashfs-root +unsquashfs devilutionx-rg99.opk +ssh rg99 'rm -rf /media/data/local/home/squashfs-root' +scp -r -O squashfs-root/ rg99:/media/data/local/home/squashfs-root +``` + +Then, on RG99, prepare the demo files and run `gdbserver`: + +```bash +mkdir -p demo +cp -r squashfs-root/demo_0* demo +cp -r squashfs-root/spawn_0_sv demo +cd squashfs-root +gdbserver 10.1.1.1:8001 devilutionx --diablo --spawn --demo 0 --timedemo \ + --save-dir ~/demo --data-dir ~/.local/share/diasurgical/devilution +``` + +Then, on the PC, add the following VS Code configuration to `.vscode/launch.json`: + +```json +{ + "name": "rg99 remote debug", + "type": "cppdbg", + "request": "launch", + "program": "build-rg99/devilutionx", + "stopAtEntry": true, + "miDebuggerPath": "/opt/rs90-toolchain/bin/mipsel-linux-gdb", + "miDebuggerArgs": "-ix /opt/rs90-toolchain/mipsel-rs90-linux-musl/sysroot/usr/share/buildroot/gdbinit", + "MIMode": "gdb", + "miDebuggerServerAddress": "10.1.1.3:8001", + "targetArchitecture": "mips", + "additionalSOLibSearchPath": "/opt/rs90-toolchain/mipsel-rs90-linux-musl/sysroot", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ], + "externalConsole": false, + "cwd": "${workspaceFolder}" +} +``` + +Finally, run the configuration from the "Run and Debug" VS Code tab. diff --git a/Source/engine/demomode.cpp b/Source/engine/demomode.cpp index 69555b442b0..5a0a658b089 100644 --- a/Source/engine/demomode.cpp +++ b/Source/engine/demomode.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -65,13 +64,6 @@ struct KeyEventData { uint16_t mod; }; -union DemoMsgEventData { - MouseMotionEventData motion; - MouseButtonEventData button; - MouseWheelEventData wheel; - KeyEventData key; -}; - struct DemoMsg { enum EventType : uint8_t { GameTick = 0, @@ -93,6 +85,12 @@ struct DemoMsg { EventType type; uint8_t progressToNextGameTick; + union { + MouseMotionEventData motion; + MouseButtonEventData button; + MouseWheelEventData wheel; + KeyEventData key; + }; [[nodiscard]] bool isEvent() const { @@ -100,7 +98,11 @@ struct DemoMsg { } }; +FILE *DemoFile; +int DemoFileVersion; int DemoNumber = -1; +std::optional CurrentDemoMessage; + bool Timedemo = false; int RecordNumber = -1; bool CreateDemoReference = false; @@ -142,46 +144,6 @@ uint32_t StartTime = 0; uint16_t DemoGraphicsWidth = 640; uint16_t DemoGraphicsHeight = 480; -std::deque DemoMessageQueue; -std::deque MouseMotionEventDataQueue; -std::deque MouseButtonEventDataQueue; -std::deque MouseWheelEventDataQueue; -std::deque KeyEventDataQueue; - -struct DemoMessageAndData { - DemoMsg message; - DemoMsgEventData data; -}; -DemoMessageAndData PopDemoMessage() -{ - DemoMessageAndData result; - result.message = DemoMessageQueue.front(); - DemoMessageQueue.pop_front(); - switch (result.message.type) { - case DemoMsg::MouseMotionEvent: - result.data.motion = MouseMotionEventDataQueue.front(); - MouseMotionEventDataQueue.pop_front(); - break; - case DemoMsg::MouseButtonDownEvent: - case DemoMsg::MouseButtonUpEvent: - result.data.button = MouseButtonEventDataQueue.front(); - MouseButtonEventDataQueue.pop_front(); - break; - case DemoMsg::MouseWheelEvent: - result.data.wheel = MouseWheelEventDataQueue.front(); - MouseWheelEventDataQueue.pop_front(); - break; - case DemoMsg::KeyDownEvent: - case DemoMsg::KeyUpEvent: - result.data.key = KeyEventDataQueue.front(); - KeyEventDataQueue.pop_front(); - break; - default: - break; - } - return result; -} - void ReadSettings(FILE *in, uint8_t version) // NOLINT(readability-identifier-length) { DemoGraphicsWidth = ReadLE16(in); @@ -277,39 +239,39 @@ void WriteSettings(FILE *out) } #if SDL_VERSION_ATLEAST(2, 0, 0) -bool CreateSdlEvent(const DemoMsg &dmsg, const DemoMsgEventData &data, SDL_Event &event, uint16_t &modState) +bool CreateSdlEvent(const DemoMsg &dmsg, SDL_Event &event, uint16_t &modState) { const uint8_t type = dmsg.type; switch (type) { case DemoMsg::MouseMotionEvent: event.type = SDL_MOUSEMOTION; event.motion.which = 0; - event.motion.x = data.motion.x; - event.motion.y = data.motion.y; + event.motion.x = dmsg.motion.x; + event.motion.y = dmsg.motion.y; return true; case DemoMsg::MouseButtonDownEvent: case DemoMsg::MouseButtonUpEvent: event.type = type == DemoMsg::MouseButtonDownEvent ? SDL_MOUSEBUTTONDOWN : SDL_MOUSEBUTTONUP; event.button.which = 0; - event.button.button = data.button.button; + event.button.button = dmsg.button.button; event.button.state = type == DemoMsg::MouseButtonDownEvent ? SDL_PRESSED : SDL_RELEASED; - event.button.x = data.button.x; - event.button.y = data.button.y; - modState = data.button.mod; + event.button.x = dmsg.button.x; + event.button.y = dmsg.button.y; + modState = dmsg.button.mod; return true; case DemoMsg::MouseWheelEvent: event.type = SDL_MOUSEWHEEL; event.wheel.which = 0; - event.wheel.x = data.wheel.x; - event.wheel.y = data.wheel.y; - modState = data.wheel.mod; + event.wheel.x = dmsg.wheel.x; + event.wheel.y = dmsg.wheel.y; + modState = dmsg.wheel.mod; return true; case DemoMsg::KeyDownEvent: case DemoMsg::KeyUpEvent: event.type = type == DemoMsg::KeyDownEvent ? SDL_KEYDOWN : SDL_KEYUP; event.key.state = type == DemoMsg::KeyDownEvent ? SDL_PRESSED : SDL_RELEASED; - event.key.keysym.sym = data.key.sym; - event.key.keysym.mod = data.key.mod; + event.key.keysym.sym = dmsg.key.sym; + event.key.keysym.mod = dmsg.key.mod; return true; default: if (type >= DemoMsg::MinCustomEvent) { @@ -370,43 +332,43 @@ uint8_t Sdl2ToSdl1MouseButton(uint8_t button) } } -bool CreateSdlEvent(const DemoMsg &dmsg, const DemoMsgEventData &data, SDL_Event &event, uint16_t &modState) +bool CreateSdlEvent(const DemoMsg &dmsg, SDL_Event &event, uint16_t &modState) { const uint8_t type = dmsg.type; switch (type) { case DemoMsg::MouseMotionEvent: event.type = SDL_MOUSEMOTION; event.motion.which = 0; - event.motion.x = data.motion.x; - event.motion.y = data.motion.y; + event.motion.x = dmsg.motion.x; + event.motion.y = dmsg.motion.y; return true; case DemoMsg::MouseButtonDownEvent: case DemoMsg::MouseButtonUpEvent: event.type = type == DemoMsg::MouseButtonDownEvent ? SDL_MOUSEBUTTONDOWN : SDL_MOUSEBUTTONUP; event.button.which = 0; - event.button.button = Sdl2ToSdl1MouseButton(data.button.button); + event.button.button = Sdl2ToSdl1MouseButton(dmsg.button.button); event.button.state = type == DemoMsg::MouseButtonDownEvent ? SDL_PRESSED : SDL_RELEASED; - event.button.x = data.button.x; - event.button.y = data.button.y; - modState = data.button.mod; + event.button.x = dmsg.button.x; + event.button.y = dmsg.button.y; + modState = dmsg.button.mod; return true; case DemoMsg::MouseWheelEvent: - if (data.wheel.y == 0) { + if (dmsg.wheel.y == 0) { LogWarn("Demo: unsupported event (mouse wheel y == 0)"); return false; } event.type = SDL_MOUSEBUTTONDOWN; event.button.which = 0; - event.button.button = data.wheel.y > 0 ? SDL_BUTTON_WHEELUP : SDL_BUTTON_WHEELDOWN; - modState = data.wheel.mod; + event.button.button = dmsg.wheel.y > 0 ? SDL_BUTTON_WHEELUP : SDL_BUTTON_WHEELDOWN; + modState = dmsg.wheel.mod; return true; case DemoMsg::KeyDownEvent: case DemoMsg::KeyUpEvent: event.type = type == DemoMsg::KeyDownEvent ? SDL_KEYDOWN : SDL_KEYUP; event.key.which = 0; event.key.state = type == DemoMsg::KeyDownEvent ? SDL_PRESSED : SDL_RELEASED; - event.key.keysym.sym = Sdl2ToSdl1Key(data.key.sym); - event.key.keysym.mod = static_cast(data.key.mod); + event.key.keysym.sym = Sdl2ToSdl1Key(dmsg.key.sym); + event.key.keysym.mod = static_cast(dmsg.key.mod); return true; default: if (type >= DemoMsg::MinCustomEvent) { @@ -446,11 +408,11 @@ uint8_t MapPreV2DemoMsgEventType(uint16_t type) } } -void LogDemoMessage(const DemoMsg &msg, const DemoMsgEventData &data = DemoMsgEventData {}) +void LogDemoMessage(const DemoMsg &dmsg) { #ifdef LOG_DEMOMODE_MESSAGES - const uint8_t progressToNextGameTick = msg.progressToNextGameTick; - switch (msg.type) { + const uint8_t progressToNextGameTick = dmsg.progressToNextGameTick; + switch (dmsg.type) { case DemoMsg::GameTick: #ifdef LOG_DEMOMODE_MESSAGES_GAMETICK Log("⏲️ GameTick {:>3}", progressToNextGameTick); @@ -464,119 +426,122 @@ void LogDemoMessage(const DemoMsg &msg, const DemoMsgEventData &data = DemoMsgEv case DemoMsg::MouseMotionEvent: #ifdef LOG_DEMOMODE_MESSAGES_MOUSEMOTION Log("🖱️ Message {:>3} MOUSEMOTION {} {}", progressToNextGameTick, - data.motion.x, data.motion.y); + dmsg.motion.x, dmsg.motion.y); #endif break; case DemoMsg::MouseButtonDownEvent: case DemoMsg::MouseButtonUpEvent: Log("🖱️ Message {:>3} {} {} {} {} 0x{:x}", progressToNextGameTick, - msg.type == DemoMsg::MouseButtonDownEvent ? "MOUSEBUTTONDOWN" : "MOUSEBUTTONUP", - data.button.button, data.button.x, data.button.y, data.button.mod); + dmsg.type == DemoMsg::MouseButtonDownEvent ? "MOUSEBUTTONDOWN" : "MOUSEBUTTONUP", + dmsg.button.button, dmsg.button.x, dmsg.button.y, dmsg.button.mod); break; case DemoMsg::MouseWheelEvent: Log("🖱️ Message {:>3} MOUSEWHEEL {} {} 0x{:x}", progressToNextGameTick, - data.wheel.x, data.wheel.y, data.wheel.mod); + dmsg.wheel.x, dmsg.wheel.y, dmsg.wheel.mod); break; case DemoMsg::KeyDownEvent: case DemoMsg::KeyUpEvent: Log("🔤 Message {:>3} {} 0x{:x} 0x{:x}", progressToNextGameTick, - msg.type == DemoMsg::KeyDownEvent ? "KEYDOWN" : "KEYUP", - data.key.sym, data.key.mod); + dmsg.type == DemoMsg::KeyDownEvent ? "KEYDOWN" : "KEYUP", + dmsg.key.sym, dmsg.key.mod); break; case DemoMsg::QuitEvent: Log("❎ Message {:>3} QUIT", progressToNextGameTick); break; default: - Log("📨 Message {:>3} USEREVENT {}", progressToNextGameTick, static_cast(msg.type)); + Log("📨 Message {:>3} USEREVENT {}", progressToNextGameTick, static_cast(dmsg.type)); break; } #endif // LOG_DEMOMODE_MESSAGES } -LoadingStatus LoadDemoMessages(int demoNumber) +void CloseDemoFile() +{ + if (DemoFile != nullptr) { + std::fclose(DemoFile); + DemoFile = nullptr; + } +} + +LoadingStatus OpenDemoFile(int demoNumber) { + CloseDemoFile(); const std::string path = StrCat(paths::PrefPath(), "demo_", demoNumber, ".dmo"); - FILE *demofile = OpenFile(path.c_str(), "rb"); - if (demofile == nullptr) { + DemoFile = OpenFile(path.c_str(), "rb"); + if (DemoFile == nullptr) { return LoadingStatus::FileNotFound; } - - const uint8_t version = ReadByte(demofile); - if (version > Version) { + DemoFileVersion = ReadByte(DemoFile); + if (DemoFileVersion > Version) { return LoadingStatus::UnsupportedVersion; } + DemoNumber = demoNumber; - gSaveNumber = ReadLE32(demofile); - ReadSettings(demofile, version); + gSaveNumber = ReadLE32(DemoFile); + ReadSettings(DemoFile, DemoFileVersion); - while (true) { - const uint8_t typeNum = version >= 2 ? ReadByte(demofile) : ReadLE32(demofile); - if (std::feof(demofile) != 0) - break; + return LoadingStatus::Success; +} - // Events with the high bit 1 are Rendering events with the rest of the bits used - // to encode `progressToNextGameTick` inline. - if ((typeNum & 0b10000000) != 0) { - DemoMessageQueue.push_back(DemoMsg { DemoMsg::Rendering, static_cast(typeNum & 0b01111111u) }); - continue; - } - const uint8_t progressToNextGameTick = ReadByte(demofile); +std::optional ReadDemoMessage() +{ + const uint8_t typeNum = DemoFileVersion >= 2 ? ReadByte(DemoFile) : ReadLE32(DemoFile); - switch (typeNum) { - case DemoMsg::GameTick: - case DemoMsg::Rendering: - DemoMessageQueue.push_back(DemoMsg { static_cast(typeNum), progressToNextGameTick }); + if (std::feof(DemoFile) != 0) { + CloseDemoFile(); + return std::nullopt; + } + + // Events with the high bit 1 are Rendering events with the rest of the bits used + // to encode `progressToNextGameTick` inline. + if ((typeNum & 0b10000000) != 0) { + DemoModeLastTick = SDL_GetTicks(); + return DemoMsg { DemoMsg::Rendering, static_cast(typeNum & 0b01111111u), {} }; + } + const uint8_t progressToNextGameTick = ReadByte(DemoFile); + + switch (typeNum) { + case DemoMsg::GameTick: + case DemoMsg::Rendering: + DemoModeLastTick = SDL_GetTicks(); + return DemoMsg { static_cast(typeNum), progressToNextGameTick, {} }; + default: { + const uint8_t eventType = DemoFileVersion >= 2 ? typeNum : MapPreV2DemoMsgEventType(static_cast(ReadLE32(DemoFile))); + DemoMsg result { static_cast(eventType), progressToNextGameTick, {} }; + switch (eventType) { + case DemoMsg::MouseMotionEvent: { + result.motion.x = ReadLE16(DemoFile); + result.motion.y = ReadLE16(DemoFile); + } break; + case DemoMsg::MouseButtonDownEvent: + case DemoMsg::MouseButtonUpEvent: { + result.button.button = ReadByte(DemoFile); + result.button.x = ReadLE16(DemoFile); + result.button.y = ReadLE16(DemoFile); + result.button.mod = ReadLE16(DemoFile); + } break; + case DemoMsg::MouseWheelEvent: { + result.wheel.x = DemoFileVersion >= 2 ? ReadLE16(DemoFile) : static_cast(ReadLE32(DemoFile)); + result.wheel.y = DemoFileVersion >= 2 ? ReadLE16(DemoFile) : static_cast(ReadLE32(DemoFile)); + result.wheel.mod = ReadLE16(DemoFile); + } break; + case DemoMsg::KeyDownEvent: + case DemoMsg::KeyUpEvent: { + result.key.sym = static_cast(ReadLE32(DemoFile)); + result.key.mod = static_cast(ReadLE16(DemoFile)); + } break; + case DemoMsg::QuitEvent: // SDL_QUIT break; - default: { - const uint8_t eventType = version >= 2 ? typeNum : MapPreV2DemoMsgEventType(static_cast(ReadLE32(demofile))); - DemoMessageQueue.push_back(DemoMsg { static_cast(eventType), progressToNextGameTick }); - switch (eventType) { - case DemoMsg::MouseMotionEvent: { - MouseMotionEventData motion; - motion.x = ReadLE16(demofile); - motion.y = ReadLE16(demofile); - MouseMotionEventDataQueue.push_back(motion); - } break; - case DemoMsg::MouseButtonDownEvent: - case DemoMsg::MouseButtonUpEvent: { - MouseButtonEventData button; - button.button = ReadByte(demofile); - button.x = ReadLE16(demofile); - button.y = ReadLE16(demofile); - button.mod = ReadLE16(demofile); - MouseButtonEventDataQueue.push_back(button); - } break; - case DemoMsg::MouseWheelEvent: { - MouseWheelEventData wheel; - wheel.x = version >= 2 ? ReadLE16(demofile) : static_cast(ReadLE32(demofile)); - wheel.y = version >= 2 ? ReadLE16(demofile) : static_cast(ReadLE32(demofile)); - wheel.mod = ReadLE16(demofile); - MouseWheelEventDataQueue.push_back(wheel); - } break; - case DemoMsg::KeyDownEvent: - case DemoMsg::KeyUpEvent: { - KeyEventData key; - key.sym = static_cast(ReadLE32(demofile)); - key.mod = static_cast(ReadLE16(demofile)); - KeyEventDataQueue.push_back(key); - } break; - case DemoMsg::QuitEvent: // SDL_QUIT - break; - default: - if (eventType < DemoMsg::MinCustomEvent) { - app_fatal(StrCat("Unknown event ", eventType)); - } - break; + default: + if (eventType < DemoMsg::MinCustomEvent) { + app_fatal(StrCat("Unknown event ", eventType)); } - } break; + break; } + DemoModeLastTick = SDL_GetTicks(); + return result; + } break; } - - std::fclose(demofile); - - DemoModeLastTick = SDL_GetTicks(); - - return LoadingStatus::Success; } void WriteDemoMsgHeader(DemoMsg::EventType type) @@ -595,11 +560,10 @@ namespace demo { void InitPlayBack(int demoNumber, bool timedemo) { - DemoNumber = demoNumber; Timedemo = timedemo; ControlMode = ControlTypes::KeyboardAndMouse; - const LoadingStatus status = LoadDemoMessages(demoNumber); + const LoadingStatus status = OpenDemoFile(demoNumber); switch (status) { case LoadingStatus::Success: return; @@ -672,10 +636,14 @@ bool IsRecording() bool GetRunGameLoop(bool &drawGame, bool &processInput) { - if (DemoMessageQueue.empty()) + if (CurrentDemoMessage == std::nullopt && DemoFile != nullptr) + CurrentDemoMessage = ReadDemoMessage(); + if (CurrentDemoMessage == std::nullopt) app_fatal("Demo queue empty"); - const DemoMsg &dmsg = DemoMessageQueue.front(); - if (dmsg.isEvent()) + + const DemoMsg &dmsg = *CurrentDemoMessage; + + if (CurrentDemoMessage->isEvent()) app_fatal("Unexpected event demo message in GetRunGameLoop"); LogDemoMessage(dmsg); if (Timedemo) { @@ -706,7 +674,7 @@ bool GetRunGameLoop(bool &drawGame, bool &processInput) } ProgressToNextGameTick = dmsg.progressToNextGameTick; const bool isGameTick = dmsg.type == DemoMsg::GameTick; - DemoMessageQueue.pop_front(); + CurrentDemoMessage = std::nullopt; if (isGameTick) LogicTick++; return isGameTick; @@ -724,7 +692,8 @@ bool FetchMessage(SDL_Event *event, uint16_t *modState) return true; } if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE) { - DemoMessageQueue.clear(); + CloseDemoFile(); + CurrentDemoMessage = std::nullopt; DemoNumber = -1; Timedemo = false; last_tick = SDL_GetTicks(); @@ -741,15 +710,16 @@ bool FetchMessage(SDL_Event *event, uint16_t *modState) } } - if (!DemoMessageQueue.empty()) { - if (DemoMessageQueue.front().isEvent()) { - const DemoMessageAndData dmsg = PopDemoMessage(); - LogDemoMessage(dmsg.message, dmsg.data); - const bool hasEvent = CreateSdlEvent(dmsg.message, dmsg.data, *event, *modState); - ProgressToNextGameTick = dmsg.message.progressToNextGameTick; + if (CurrentDemoMessage == std::nullopt && DemoFile != nullptr) + CurrentDemoMessage = ReadDemoMessage(); + if (CurrentDemoMessage != std::nullopt) { + const DemoMsg &dmsg = *CurrentDemoMessage; + LogDemoMessage(dmsg); + if (dmsg.isEvent()) { + const bool hasEvent = CreateSdlEvent(dmsg, *event, *modState); + ProgressToNextGameTick = dmsg.progressToNextGameTick; + CurrentDemoMessage = std::nullopt; return hasEvent; - } else { - LogDemoMessage(DemoMessageQueue.front()); } } diff --git a/Source/pfile.cpp b/Source/pfile.cpp index f03f4f2a275..02f6ccd8d0f 100644 --- a/Source/pfile.cpp +++ b/Source/pfile.cpp @@ -27,6 +27,7 @@ #include "utils/language.h" #include "utils/parse_int.hpp" #include "utils/paths.h" +#include "utils/stdcompat/filesystem.hpp" #include "utils/str_cat.hpp" #include "utils/str_split.hpp" #include "utils/utf8.hpp" @@ -164,7 +165,17 @@ SaveWriter GetStashWriter() void CopySaveFile(uint32_t saveNum, std::string targetPath) { const std::string savePath = GetSavePath(saveNum); +#if defined(UNPACKED_SAVES) +#ifdef DVL_NO_FILESYSTEM +#error "UNPACKED_SAVES requires either DISABLE_DEMOMODE or C++17 " +#endif + CreateDir(targetPath.c_str()); + for (const std::filesystem::directory_entry &entry : std::filesystem::directory_iterator(savePath)) { + CopyFileOverwrite(entry.path().string().c_str(), (targetPath + entry.path().filename().string()).c_str()); + } +#else CopyFileOverwrite(savePath.c_str(), targetPath.c_str()); +#endif } #endif