diff --git a/.github/workflows/dreamcast.yml b/.github/workflows/dreamcast.yml new file mode 100644 index 00000000000..bf5d09f8465 --- /dev/null +++ b/.github/workflows/dreamcast.yml @@ -0,0 +1,123 @@ +--- +name: Sega Dreamcast + +on: # yamllint disable-line rule:truthy + push: + branches: + - master + - dreamcast + - fix/ISSUE-8-libfmt-patch + paths-ignore: + - '*.md' + - 'docs/**' + pull_request: + types: [opened, synchronize] + paths-ignore: + - '*.md' + - 'docs/**' + release: + types: [published] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + container: azihassan/kallistios:docker + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Uninstall kos-ports SDL 1.2 + run: | + source /opt/toolchains/dc/kos/environ.sh && \ + cd /opt/toolchains/dc/kos-ports/SDL && \ + make uninstall || echo 'SDL 1.2 uninstall finished with non zero status, proceding anyway' + + - name: Install GPF SDL 1.2 + run: | + git clone -b SDL-dreamhal--GLDC https://github.com/GPF/SDL-1.2 && \ + cd SDL-1.2 && \ + source /opt/toolchains/dc/kos/environ.sh && \ + make -f Makefile.dc && \ + cp /opt/toolchains/dc/kos/addons/lib/dreamcast/libSDL.a /usr/lib/ && \ + cp include/* /usr/include/SDL/ + + - name: Download spawn.mpq + run: | + curl -LO https://github.com/diasurgical/devilutionx-assets/releases/download/v4/spawn.mpq + + - name: Download fonts.mpq + run: | + curl -LO https://github.com/diasurgical/devilutionx-assets/releases/download/v4/fonts.mpq + + - name: Configure CMake + run: | + source /opt/toolchains/dc/kos/environ.sh && \ + #uncomment when using packed mpq or save files + #without this, cmake can't find the kos-ports bzip2 & zlib libraries + export CMAKE_PREFIX_PATH=/opt/toolchains/dc/kos-ports/libbz2/inst/:/opt/toolchains/dc/kos-ports/zlib/inst/ && \ + kos-cmake \ + -S . \ + -DCMAKE_BUILD_TYPE=Release \ + -B build + + - name: Build DevilutionX + run: | + # patches libfmt to support long double + # patch build/_deps/libfmt-src/include/fmt/format.h -l -p0 < libfmt-long-double.patch + source /opt/toolchains/dc/kos/environ.sh && cd build && kos-make + + - name: Generate .cdi + run: | + source /opt/toolchains/dc/kos/environ.sh && \ + mv spawn.mpq build/data/ && \ + mv fonts.mpq build/data/fonts/ && \ + mkdcdisc -e build/devilutionx.elf -o build/devilutionx.cdi --name 'Diablo 1' -d build/data/ + + - name: Prepare elf package + run: rm build/data/spawn.mpq && rm build/data/fonts/fonts.mpq + + - name: Upload .elf Package + if: ${{ !env.ACT }} + uses: actions/upload-artifact@v4 + with: + name: devilutionx + path: | + build/data + build/devilutionx.elf + + - name: Upload .cdi Package + if: ${{ !env.ACT }} + uses: actions/upload-artifact@v4 + with: + name: devilutionx.cdi + path: ./build/devilutionx.cdi + + - name: Prepare Releases + if: ${{ github.event_name == 'release' && !env.ACT }} + run: | + apk add zip && \ + cd build && \ + zip -r devilutionx-dreamcast.zip data/ devilutionx.elf && \ + zip -r devilutionx-dreamcast.cdi.zip devilutionx.cdi + + - name: Update Release .cdi + if: ${{ github.event_name == 'release' && !env.ACT }} + uses: svenstaro/upload-release-action@v2 + with: + file: ./build/devilutionx-dreamcast.cdi.zip + overwrite: true + + - name: Update Release .elf + if: ${{ github.event_name == 'release' && !env.ACT }} + uses: svenstaro/upload-release-action@v2 + with: + file: ./build/devilutionx-dreamcast.zip + overwrite: true +... diff --git a/3rdParty/libfmt/CMakeLists.txt b/3rdParty/libfmt/CMakeLists.txt index 69b3b1cbb41..47a38b10f1c 100644 --- a/3rdParty/libfmt/CMakeLists.txt +++ b/3rdParty/libfmt/CMakeLists.txt @@ -16,11 +16,13 @@ else() endif() include(FetchContent) FetchContent_Declare(libfmt - URL https://github.com/fmtlib/fmt/archive/44f3d8a77cd7e05e9da92d68635abdb4da626e9e.tar.gz - URL_HASH MD5=b6eb4573962586cfbfb6e5e2986d292b + URL https://github.com/fmtlib/fmt/archive/ff9ee0461a8dd02cbaf9458aa13301aa1099c0f7.tar.gz + URL_HASH MD5=19ff2a3abbbbdf25eff6bbc7afe539e7 ) FetchContent_MakeAvailableExcludeFromAll(libfmt) if(DEVILUTIONX_WINDOWS_NO_WCHAR) target_compile_definitions(fmt PUBLIC FMT_WINDOWS_NO_WCHAR) endif() + +target_compile_definitions(fmt PUBLIC FMT_BUILTIN_TYPES=0) diff --git a/CMake/Platforms.cmake b/CMake/Platforms.cmake index 1c3d1e927b4..fa0d07d614b 100644 --- a/CMake/Platforms.cmake +++ b/CMake/Platforms.cmake @@ -75,3 +75,7 @@ endif() if(NXDK) include(platforms/xbox_nxdk) endif() + +if(PLATFORM_DREAMCAST) + include(platforms/dreamcast) +endif() diff --git a/CMake/platforms/dreamcast.cmake b/CMake/platforms/dreamcast.cmake new file mode 100644 index 00000000000..4b97cc3a253 --- /dev/null +++ b/CMake/platforms/dreamcast.cmake @@ -0,0 +1,47 @@ +set(BUILD_TESTING OFF) +set(NONET ON) +set(ASAN OFF) +set(UBSAN OFF) + +set(USE_SDL1 ON) +set(SDL1_VIDEO_MODE_BPP 8) +set(SDL1_VIDEO_MODE_FLAGS SDL_FULLSCREEN|SDL_DOUBLEBUF|SDL_HWSURFACE|SDL_HWPALETTE) +set(DEFAULT_WIDTH 640) +set(DEFAULT_HEIGHT 480) +set(DEVILUTIONX_GAMEPAD_TYPE Nintendo) + +set(NOSOUND ON) +set(DEVILUTIONX_STATIC_ZLIB ON) +set(UNPACKED_SAVES ON) +set(DEVILUTIONX_SYSTEM_LIBFMT OFF) +set(DEVILUTIONX_STATIC_LUA ON) +set(DEVILUTIONX_DISABLE_STRIP ON) + +set(DEVILUTIONX_ASSETS_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/data/") +set(BUILD_ASSETS_MPQ OFF) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/threads-stub") +list(APPEND DEVILUTIONX_PLATFORM_COMPILE_DEFINITIONS __DREAMCAST__) +add_compile_options(-fpermissive) + +#SDL Joystick hat mapping (D-pad) +set(JOY_HAT_DPAD_UP_HAT 0) +set(JOY_HAT_DPAD_RIGHT_HAT 0) +set(JOY_HAT_DPAD_DOWN_HAT 0) +set(JOY_HAT_DPAD_LEFT_HAT 0) +set(JOY_HAT_DPAD_UP 1) +set(JOY_HAT_DPAD_RIGHT 2) +set(JOY_HAT_DPAD_DOWN 4) +set(JOY_HAT_DPAD_LEFT 8) + +#SDL Joystick button mapping +set(JOY_BUTTON_A 2) +set(JOY_BUTTON_B 1) +set(JOY_BUTTON_X 5) +set(JOY_BUTTON_Y 6) + +set(JOY_BUTTON_START 3) + +#GPF SDL files +set(SDL_INCLUDE_DIR /usr/include/SDL/) +set(SDL_LIBRARY /usr/lib/libSDL.a) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000000..24f1bff714e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,46 @@ +FROM alpine-kallistios:no-gdb + +RUN echo "Cloning project..." +WORKDIR /opt/toolchains/dc/kos/ +RUN git clone -b dreamcast https://github.com/azihassan/devilutionX.git + +RUN echo "Uninstall kos-ports SDL 1.2..." +RUN source /opt/toolchains/dc/kos/environ.sh && \ + cd /opt/toolchains/dc/kos-ports/SDL && \ + make uninstall || echo 'SDL 1.2 uninstall finished with non zero status, proceding anyway' + +RUN echo "Install GPF SDL 1.2..." +RUN git clone -b SDL-dreamhal--GLDC https://github.com/GPF/SDL-1.2 && \ + cd SDL-1.2 && \ + source /opt/toolchains/dc/kos/environ.sh && \ + make -f Makefile.dc && \ + cp /opt/toolchains/dc/kos/addons/lib/dreamcast/libSDL.a /usr/lib/ && \ + cp include/* /usr/include/SDL/ + +WORKDIR /opt/toolchains/dc/kos/devilutionX +RUN echo "Downloading spawn.mpq..." +RUN curl -LO https://github.com/diasurgical/devilutionx-assets/releases/download/v4/spawn.mpq + +RUN echo "Downloading fonts.mpq..." +RUN curl -LO https://github.com/diasurgical/devilutionx-assets/releases/download/v4/fonts.mpq + +RUN echo "Configuring CMake..." +RUN source /opt/toolchains/dc/kos/environ.sh && \ + #uncomment when using packed save files + #without this, cmake can't find the kos-ports bzip2 & zlib libraries + export CMAKE_PREFIX_PATH=/opt/toolchains/dc/kos-ports/libbz2/inst/:/opt/toolchains/dc/kos-ports/zlib/inst/ && \ + kos-cmake -S. -Bbuild + +# RUN echo "Patching fmt to support long double..." +# RUN patch build/_deps/libfmt-src/include/fmt/format.h -l -p0 < libfmt-long-double.patch + +RUN echo "Compiling..." +RUN source /opt/toolchains/dc/kos/environ.sh && cd build && kos-make + +RUN echo "Generating CDI" +RUN source /opt/toolchains/dc/kos/environ.sh && \ + mv spawn.mpq build/data/ && \ + mv fonts.mpq build/data/fonts && \ + mkdcdisc -e build/devilutionx.elf -o build/devilutionx.cdi --name 'Diablo 1' -d build/data/ + +ENTRYPOINT ["sh", "-c", "source /opt/toolchains/dc/kos/environ.sh && \"$@\"", "-s"] diff --git a/Source/DiabloUI/mainmenu.cpp b/Source/DiabloUI/mainmenu.cpp index f2592038615..5b1449b7320 100644 --- a/Source/DiabloUI/mainmenu.cpp +++ b/Source/DiabloUI/mainmenu.cpp @@ -35,7 +35,11 @@ void MainmenuEsc() void MainmenuLoad(const char *name) { +#ifndef __DREAMCAST__ + // single player save files are too big for the VMU + // todo reactivate when SD card saving is implemented vecMenuItems.push_back(std::make_unique(_("Single Player"), MAINMENU_SINGLE_PLAYER)); +#endif vecMenuItems.push_back(std::make_unique(_("Multi Player"), MAINMENU_MULTIPLAYER)); vecMenuItems.push_back(std::make_unique(_("Settings"), MAINMENU_SETTINGS)); vecMenuItems.push_back(std::make_unique(_("Support"), MAINMENU_SHOW_SUPPORT)); diff --git a/Source/appfat.h b/Source/appfat.h index 9c98572f7fe..378399254aa 100644 --- a/Source/appfat.h +++ b/Source/appfat.h @@ -20,7 +20,7 @@ namespace devilution { #ifndef _DEBUG #define assert(exp) #else -#define assert(exp) (void)((exp) || (assert_fail(__LINE__, __FILE__, #exp), 0)) +#define assert(exp) (void)((exp) || (devilution::assert_fail(__LINE__, __FILE__, #exp), 0)) #endif /** diff --git a/Source/controls/devices/joystick.cpp b/Source/controls/devices/joystick.cpp index 3770e595cfd..af62cb395ea 100644 --- a/Source/controls/devices/joystick.cpp +++ b/Source/controls/devices/joystick.cpp @@ -46,8 +46,10 @@ StaticVector Joystick::ToControllerButtonEvents(const return { ControllerButtonEvent { ControllerButton_BUTTON_RIGHTSTICK, up } }; #endif #ifdef JOY_BUTTON_LEFTSHOULDER - case JOY_BUTTON_LEFTSHOULDER: + case JOY_BUTTON_LEFTSHOULDER: { + Log("ToControllerButtonEvents JOY_BUTTON_LEFTSHOULDER pressed"); return { ControllerButtonEvent { ControllerButton_BUTTON_LEFTSHOULDER, up } }; + } #endif #ifdef JOY_BUTTON_RIGHTSHOULDER case JOY_BUTTON_RIGHTSHOULDER: @@ -101,6 +103,20 @@ StaticVector Joystick::ToControllerButtonEvents(const } case SDL_JOYAXISMOTION: case SDL_JOYBALLMOTION: +#ifdef __DREAMCAST__ + if (event.jaxis.axis == 3) { + Log("BUTTON_LEFTSHOULDER detected"); + Log("event.jbutton.button = {}", event.jbutton.button); + Log("event.jbutton.state == SDL_RELEASED = {}", event.jbutton.state == SDL_RELEASED); + return { ControllerButtonEvent { ControllerButton_BUTTON_LEFTSHOULDER, event.jaxis.value < 255 } }; + } + if (event.jaxis.axis == 2) { + Log("BUTTON_RIGHTSHOULDER detected"); + Log("event.jbutton.button = {}", event.jbutton.button); + Log("event.jbutton.state == SDL_RELEASED = {}", event.jbutton.state == SDL_RELEASED); + return { ControllerButtonEvent { ControllerButton_BUTTON_RIGHTSHOULDER, event.jaxis.value < 255 } }; + } +#endif // ProcessAxisMotion() requires a ControllerButtonEvent parameter // so provide one here using ControllerButton_NONE return { ControllerButtonEvent { ControllerButton_NONE, false } }; @@ -211,8 +227,10 @@ int Joystick::ToSdlJoyButton(ControllerButton button) return JOY_BUTTON_RIGHTSTICK; #endif #ifdef JOY_BUTTON_LEFTSHOULDER - case ControllerButton_BUTTON_LEFTSHOULDER: + case ControllerButton_BUTTON_LEFTSHOULDER: { + Log("ToSdlJoyButton JOY_BUTTON_LEFTSHOULDER pressed"); return JOY_BUTTON_LEFTSHOULDER; + } #endif #ifdef JOY_BUTTON_RIGHTSHOULDER case ControllerButton_BUTTON_RIGHTSHOULDER: @@ -292,6 +310,31 @@ bool Joystick::IsPressed(ControllerButton button) const return joyButton < numButtons && SDL_JoystickGetButton(sdl_joystick_, joyButton) != 0; } +#ifdef __DREAMCAST__ +bool Joystick::ProcessAxisMotion(const SDL_Event &event) +{ + if (event.type != SDL_JOYAXISMOTION) + return false; + + Log("ProcessAxisMotion event.jaxis.axis = {}", event.jaxis.axis); + Log("ProcessAxisMotion event.jaxis.value = {}", event.jaxis.value); + Log("ProcessAxisMotion event.jbutton.button = {}", event.jbutton.button); + Log("event.jbutton.state == SDL_RELEASED = {}", event.jbutton.state == SDL_RELEASED); + + switch (event.jaxis.axis) { + case 0: // horizontal + leftStickXUnscaled = event.jaxis.value; + leftStickNeedsScaling = true; + return true; + case 1: // vertical + leftStickYUnscaled = event.jaxis.value; + leftStickNeedsScaling = true; + return true; + default: + return false; + } +} +#else //! ifdef __DREAMCAST__ bool Joystick::ProcessAxisMotion(const SDL_Event &event) { if (event.type != SDL_JOYAXISMOTION) @@ -330,6 +373,7 @@ bool Joystick::ProcessAxisMotion(const SDL_Event &event) return false; #endif } +#endif void Joystick::Add(int deviceIndex) { diff --git a/Source/diablo.cpp b/Source/diablo.cpp index eebe60ba1ba..8de62493e08 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -787,8 +787,11 @@ void GameEventHandler(const SDL_Event &event, uint16_t modState) #endif default: if (IsCustomEvent(event.type)) { - if (gbIsMultiplayer) + if (gbIsMultiplayer) { + Log("IsCustomEvent({}) = true", event.type); + Log("pfile_write_hero"); pfile_write_hero(); + } nthread_ignore_mutex(true); PaletteFadeOut(8); sound_stop(); @@ -908,6 +911,7 @@ void RunGameLoop(interface_mode uMsg) demo::NotifyGameLoopEnd(); if (gbIsMultiplayer) { + Log("gbRunGame = {}, pfile_write_hero(/*writeGameData=*/false)", gbRunGame); pfile_write_hero(/*writeGameData=*/false); sfile_write_stash(); } @@ -2847,6 +2851,7 @@ void DisableInputEventHandler(const SDL_Event &event, uint16_t modState) void LoadGameLevel(bool firstflag, lvl_entry lvldir) { + Log("LoadGameLevel(firstflag = {}, lvldir = {})", firstflag, static_cast(lvldir)); _music_id neededTrack = GetLevelMusic(leveltype); ClearFloatingNumbers(); diff --git a/Source/init.cpp b/Source/init.cpp index 1e145b78f85..c922016ecd7 100644 --- a/Source/init.cpp +++ b/Source/init.cpp @@ -122,6 +122,9 @@ std::vector GetMPQSearchPaths() { std::vector paths; paths.push_back(paths::BasePath()); +#if defined(__DREAMCAST__) + return paths; +#endif paths.push_back(paths::PrefPath()); if (paths[0] == paths[1]) paths.pop_back(); @@ -193,7 +196,13 @@ bool CheckExtraFontsVersion(AssetRef &&ref) #ifdef UNPACKED_MPQS bool AreExtraFontsOutOfDate(const std::string &path) { +#ifdef __DREAMCAST__ + // handle ISO 9660 trailing period + const std::string versionPath = path + "fonts" DIRECTORY_SEPARATOR_STR "VERSION."; +#else const std::string versionPath = path + "fonts" DIRECTORY_SEPARATOR_STR "VERSION"; +#endif + if (versionPath.size() + 1 > AssetRef::PathBufSize) app_fatal("Path too long"); AssetRef ref; @@ -219,6 +228,7 @@ bool AreExtraFontsOutOfDate(MpqArchive &archive) void init_cleanup() { if (gbIsMultiplayer && gbRunGame) { + Log("init_cleanup() gbIsMultiplayer && gbRunGame"); pfile_write_hero(/*writeGameData=*/false); sfile_write_stash(); } @@ -253,7 +263,7 @@ void LoadCoreArchives() #ifdef UNPACKED_MPQS font_data_path = FindUnpackedMpqData(paths, "fonts"); #else // !UNPACKED_MPQS -#if !defined(__ANDROID__) && !defined(__APPLE__) && !defined(__3DS__) && !defined(__SWITCH__) +#if !defined(__ANDROID__) && !defined(__APPLE__) && !defined(__3DS__) && !defined(__SWITCH__) && !defined(__DREAMCAST__) // Load devilutionx.mpq first to get the font file for error messages devilutionx_mpq = LoadMPQ(paths, "devilutionx.mpq"); #endif diff --git a/Source/interfac.cpp b/Source/interfac.cpp index 601473e5d2b..51ed9b1f7b7 100644 --- a/Source/interfac.cpp +++ b/Source/interfac.cpp @@ -73,6 +73,7 @@ Cutscenes GetCutSceneFromLevelType(dungeon_type type) Cutscenes PickCutscene(interface_mode uMsg) { + Log("MyPlayer->plrlevel = {}", MyPlayer->plrlevel); switch (uMsg) { case WM_DIABLOADGAME: case WM_DIABNEWGAME: @@ -351,6 +352,7 @@ void ShowProgress(interface_mode uMsg) case WM_DIABNEXTLVL: IncProgress(); if (!gbIsMultiplayer) { + Log("pfile_save_level()"); pfile_save_level(); } else { DeltaSaveLevel(); @@ -361,12 +363,14 @@ void ShowProgress(interface_mode uMsg) currlevel = myPlayer.plrlevel; leveltype = GetLevelType(currlevel); IncProgress(); + Log("LoadGameLevel(false, ENTRY_MAIN)"); LoadGameLevel(false, ENTRY_MAIN); IncProgress(); break; case WM_DIABPREVLVL: IncProgress(); if (!gbIsMultiplayer) { + Log("pfile_save_level()"); pfile_save_level(); } else { DeltaSaveLevel(); @@ -377,6 +381,7 @@ void ShowProgress(interface_mode uMsg) leveltype = GetLevelType(currlevel); assert(myPlayer.isOnActiveLevel()); IncProgress(); + Log("LoadGameLevel(false, ENTRY_PREV)"); LoadGameLevel(false, ENTRY_PREV); IncProgress(); break; diff --git a/Source/loadsave.cpp b/Source/loadsave.cpp index 0c0dd1533ad..ce4f7fb7b8d 100644 --- a/Source/loadsave.cpp +++ b/Source/loadsave.cpp @@ -179,6 +179,8 @@ class SaveHelper { , m_buffer_(new std::byte[codec_get_encoded_len(bufferLen)]) , m_capacity_(bufferLen) { + Log("SaveHelper instantiated: this(mpqWriter, \"{}\", {})", szFileName, bufferLen); + Log("SaveHelper allocated byte array of {} bytes", codec_get_encoded_len(bufferLen)); } bool IsValid(size_t len = 1) @@ -224,9 +226,12 @@ class SaveHelper { ~SaveHelper() { + // const auto encodedLen = m_cur_; const auto encodedLen = codec_get_encoded_len(m_cur_); const char *const password = pfile_get_password(); + Log("codec_encode(m_buffer_.get(), {}, {})", m_cur_, encodedLen); codec_encode(m_buffer_.get(), m_cur_, encodedLen, password); + Log("~SaveHelper WriteFile(\"{}\", m_buffer_.get(), {})", m_szFileName_, encodedLen); m_mpqWriter.WriteFile(m_szFileName_, m_buffer_.get(), encodedLen); } }; @@ -1832,6 +1837,7 @@ void LoadLevelSeeds() void SaveLevel(SaveWriter &saveWriter, LevelConversionData *levelConversionData) { + Log("SaveLevel(SaveWriter &saveWriter, LevelConversionData *levelConversionData)"); Player &myPlayer = *MyPlayer; DoUnVision(myPlayer.position.tile, myPlayer._pLightRad); // fix for vision staying on the level @@ -1841,6 +1847,7 @@ void SaveLevel(SaveWriter &saveWriter, LevelConversionData *levelConversionData) char szName[MaxMpqPathSize]; GetTempLevelNames(szName); + Log("SaveHelper file(saveWriter, \"{}\", 256 * 1024)", szName); SaveHelper file(saveWriter, szName, 256 * 1024); if (leveltype != DTYPE_TOWN) { @@ -1911,11 +1918,18 @@ void SaveLevel(SaveWriter &saveWriter, LevelConversionData *levelConversionData) void LoadLevel(LevelConversionData *levelConversionData) { + Log("LoadLevel"); char szName[MaxMpqPathSize]; std::optional archive = OpenSaveArchive(gSaveNumber); + Log("OpenSaveArchive({})", gSaveNumber); GetTempLevelNames(szName); - if (!archive || !archive->HasFile(szName)) + Log("szName = '{}'", szName); + Log("!archive = {}", !archive); + Log("!archive->HasFile(\"{}\") = {}", szName, !archive->HasFile(szName)); + if (!archive || !archive->HasFile(szName)) { + Log("{} not found in archive, calling GetPermLevelNames(\"{}\")", szName, szName); GetPermLevelNames(szName); + } LoadHelper file(std::move(archive), szName); if (!file.IsValid()) app_fatal(_("Unable to open save file archive")); @@ -2221,7 +2235,8 @@ size_t HotkeysSize(size_t nHotkeys = NumHotkeys) void LoadHotkeys() { - LoadHelper file(OpenSaveArchive(gSaveNumber), "hotkeys"); + // hotkeys => htks to get around VMU filename size limits + LoadHelper file(OpenSaveArchive(gSaveNumber), "htks"); if (!file.IsValid()) return; @@ -2263,7 +2278,8 @@ void LoadHotkeys() void SaveHotkeys(SaveWriter &saveWriter, const Player &player) { - SaveHelper file(saveWriter, "hotkeys", HotkeysSize()); + // hotkeys => htks to get around VMU filename size limits + SaveHelper file(saveWriter, "htks", HotkeysSize()); // Write the number of spell hotkeys file.WriteLE(static_cast(NumHotkeys)); @@ -2283,7 +2299,8 @@ void SaveHotkeys(SaveWriter &saveWriter, const Player &player) void LoadHeroItems(Player &player) { - LoadHelper file(OpenSaveArchive(gSaveNumber), "heroitems"); + // heroitems => hitms to get around VMU filename size limits + LoadHelper file(OpenSaveArchive(gSaveNumber), "hitms"); if (!file.IsValid()) return; @@ -2349,6 +2366,7 @@ void RemoveEmptyInventory(Player &player) void LoadGame(bool firstflag) { + Log("LoadGame(firstflag = {})", firstflag); FreeGameMem(); LoadHelper file(OpenSaveArchive(gSaveNumber), "game"); @@ -2369,6 +2387,7 @@ void LoadGame(bool firstflag) giNumberOfSmithPremiumItems = 6; } + Log("pfile_remove_temp_files"); pfile_remove_temp_files(); setlevel = file.NextBool8(); @@ -2398,13 +2417,16 @@ void LoadGame(bool firstflag) Player &myPlayer = *MyPlayer; + Log("LoadPlayer(file, myPlayer);"); LoadPlayer(file, myPlayer); if (sgGameInitInfo.nDifficulty < DIFF_NORMAL || sgGameInitInfo.nDifficulty > DIFF_HELL) sgGameInitInfo.nDifficulty = DIFF_NORMAL; + Log("LoadQuests {}", giNumberQuests); for (int i = 0; i < giNumberQuests; i++) LoadQuest(&file, i); + Log("LoadPortals", MAXPORTAL); for (int i = 0; i < MAXPORTAL; i++) LoadPortal(&file, i); @@ -2413,6 +2435,7 @@ void LoadGame(bool firstflag) RemoveEmptyInventory(myPlayer); } + Log("LoadGameLevel"); LoadGameLevel(firstflag, ENTRY_LOAD); SetPlrAnims(myPlayer); SyncPlrAnim(myPlayer); @@ -2429,6 +2452,7 @@ void LoadGame(bool firstflag) if (leveltype != DTYPE_TOWN) { for (unsigned &monsterId : ActiveMonsters) monsterId = file.NextBE(); + Log("LoadMonsters {}", ActiveMonsterCount); for (size_t i = 0; i < ActiveMonsterCount; i++) LoadMonster(&file, Monsters[ActiveMonsters[i]]); for (size_t i = 0; i < ActiveMonsterCount; i++) @@ -2437,6 +2461,7 @@ void LoadGame(bool firstflag) file.Skip(MaxMissilesForSaveGame); // Skip AvailableMissiles file.Skip(MaxMissilesForSaveGame); + Log("LoadMissiles {}", tmpNummissiles); for (int i = 0; i < tmpNummissiles; i++) LoadMissile(&file); // For petrified monsters, the data in missile.var1 must be used to @@ -2560,10 +2585,12 @@ void LoadGame(bool firstflag) gbIsHellfireSaveGame = gbIsHellfire; } +// todo restore saving of inventory body void SaveHeroItems(SaveWriter &saveWriter, Player &player) { - size_t itemCount = static_cast(NUM_INVLOC) + InventoryGridCells + MaxBeltItems; - SaveHelper file(saveWriter, "heroitems", itemCount * (gbIsHellfire ? HellfireItemSaveSize : DiabloItemSaveSize) + sizeof(uint8_t)); + size_t itemCount = static_cast(NUM_INVLOC) + InventoryGridCells + MaxBeltItems; // 7 + 40 + 8 = 55 + // heroitems => hitms to get around VMU filename size limits + SaveHelper file(saveWriter, "hitms", itemCount * (gbIsHellfire ? HellfireItemSaveSize : DiabloItemSaveSize) + sizeof(uint8_t)); // 55 * 368 + 1 = 20241 bytes file.WriteLE(gbIsHellfire ? 1 : 0); @@ -2612,6 +2639,7 @@ void SaveStash(SaveWriter &stashWriter) } }; + Log("Saving {} pages of stash", pagesToSave.size()); // Current stash size is 100 pages. Will definitely fit in a 32 bit value. file.WriteLE(static_cast(pagesToSave.size())); for (const auto &page : pagesToSave) { @@ -2619,6 +2647,7 @@ void SaveStash(SaveWriter &stashWriter) for (const auto &row : Stash.stashGrids[page]) { for (uint16_t cell : row) { file.WriteLE(cell); + Log("\t\tSaving stash item {}", cell); } } } @@ -2626,6 +2655,7 @@ void SaveStash(SaveWriter &stashWriter) // 100 pages of 100 items is still only 10 000, as with the page count will definitely fit in 32 bits even in the worst case. file.WriteLE(static_cast(Stash.stashList.size())); for (const Item &item : Stash.stashList) { + Log("SaveItem(file, item)"); SaveItem(file, item); } @@ -2634,6 +2664,7 @@ void SaveStash(SaveWriter &stashWriter) void SaveGameData(SaveWriter &saveWriter) { + Log("SaveHelper file(saveWriter, \"game\", 320 * 1024)"); SaveHelper file(saveWriter, "game", 320 * 1024); if (gbIsSpawn && !gbIsHellfire) @@ -2799,6 +2830,7 @@ void SaveGameData(SaveWriter &saveWriter) void SaveGame() { + Log("SaveGame()"); gbValidSaveFile = true; pfile_write_hero(/*writeGameData=*/true); sfile_write_stash(); diff --git a/Source/main.cpp b/Source/main.cpp index af8960a8350..9fedd1b8c41 100644 --- a/Source/main.cpp +++ b/Source/main.cpp @@ -22,6 +22,14 @@ #include "diablo.h" +#ifdef __DREAMCAST__ +// fchmod fails to link on the dreamcast, this stub is provided as a workaround +extern "C" int fchmod(int fd, mode_t mode) +{ + return 0; +} +#endif + #if !defined(__APPLE__) extern "C" const char *__asan_default_options() // NOLINT(bugprone-reserved-identifier, readability-identifier-naming) { diff --git a/Source/msg.cpp b/Source/msg.cpp index f3d90d0095a..6dcf063bd13 100644 --- a/Source/msg.cpp +++ b/Source/msg.cpp @@ -2652,6 +2652,7 @@ void DeltaSaveLevel() if (!gbIsMultiplayer) return; + Log("DeltaSaveLevel"); for (Player &player : Players) { if (&player != MyPlayer) ResetPlayerGFX(player); diff --git a/Source/pfile.cpp b/Source/pfile.cpp index f2baa6c75ce..97dc14c52d8 100644 --- a/Source/pfile.cpp +++ b/Source/pfile.cpp @@ -32,6 +32,13 @@ #include "utils/str_split.hpp" #include "utils/utf8.hpp" +#ifdef __DREAMCAST__ +#include +#include +#include +#include +#endif + #ifdef UNPACKED_SAVES #include "utils/file_util.h" #else @@ -47,6 +54,26 @@ namespace devilution { bool gbValidSaveFile; +void listdir(const char *dir, int depth) +{ + file_t d = fs_open(dir, O_RDONLY | O_DIR); + dirent_t *entry; + printf("============ %s ============\n", dir); + while (NULL != (entry = fs_readdir(d))) { + char absolutePath[1024]; + strcpy(absolutePath, dir); + strcat(absolutePath, "/"); + strcat(absolutePath, entry->name); + bool isDir = entry->size == -1; + printf("[%s]\t%.2f kB\t%s\n", isDir ? "DIR" : "FIL", entry->size / 1024.0, entry->name); + if (isDir) { + printf("absolutePath = %s, depth = %d\n", absolutePath, depth); + listdir(absolutePath, depth + 1); + } + } + fs_close(d); + printf("============ %s ============\n\n\n", dir); +} namespace { /** List of character names for the character selection screen. */ @@ -54,13 +81,21 @@ char hero_names[MAX_CHARACTERS][PlayerNameLength]; std::string GetSavePath(uint32_t saveNum, std::string_view savePrefix = {}) { + // shorter names to get around VMU filename size limits return StrCat(paths::PrefPath(), savePrefix, gbIsSpawn - ? (gbIsMultiplayer ? "share_" : "spawn_") - : (gbIsMultiplayer ? "multi_" : "single_"), + ? (gbIsMultiplayer ? "M" : "S") + : (gbIsMultiplayer ? "m" : "s"), saveNum, #ifdef UNPACKED_SAVES +#ifdef __DREAMCAST__ + // flatten directory structure for easier fs_ramdisk_* usage + // for example, /ram/spawn_sv/hero would become /ram/spawn_sv_hero + + gbIsHellfire ? "_hsv" DIRECTORY_SEPARATOR_STR : "_sv_" +#else gbIsHellfire ? "_hsv" DIRECTORY_SEPARATOR_STR : "_sv" DIRECTORY_SEPARATOR_STR +#endif #else gbIsHellfire ? ".hsv" : ".sv" #endif @@ -72,7 +107,12 @@ std::string GetStashSavePath() return StrCat(paths::PrefPath(), gbIsSpawn ? "stash_spawn" : "stash", #ifdef UNPACKED_SAVES +#ifdef __DREAMCAST__ + // same as above + gbIsHellfire ? "_hsv" DIRECTORY_SEPARATOR_STR : "_sv_" +#else gbIsHellfire ? "_hsv" DIRECTORY_SEPARATOR_STR : "_sv" DIRECTORY_SEPARATOR_STR +#endif #else gbIsHellfire ? ".hsv" : ".sv" #endif @@ -107,6 +147,7 @@ bool GetTempSaveNames(uint8_t dwIndex, char *szTemp) void RenameTempToPerm(SaveWriter &saveWriter) { + Log("RenameTempToPerm"); char szTemp[MaxMpqPathSize]; char szPerm[MaxMpqPathSize]; @@ -115,9 +156,14 @@ void RenameTempToPerm(SaveWriter &saveWriter) [[maybe_unused]] bool result = GetPermSaveNames(dwIndex, szPerm); // DO NOT PUT DIRECTLY INTO ASSERT! assert(result); dwIndex++; + Log("GetPermSaveNames({}, \"{}\")", dwIndex, szTemp); if (saveWriter.HasFile(szTemp)) { - if (saveWriter.HasFile(szPerm)) + Log("saveWriter.HasFile(\"{}\") = true", szTemp); + if (saveWriter.HasFile(szPerm)) { + Log("saveWriter.HasFile(\"{}\") = true => RemoveHashEntry", szPerm); saveWriter.RemoveHashEntry(szPerm); + } + Log("saveWriter.RenameFile(\"{}\", {})", szTemp, szPerm); saveWriter.RenameFile(szTemp, szPerm); } } @@ -129,26 +175,38 @@ bool ReadHero(SaveReader &archive, PlayerPack *pPack) size_t read; auto buf = ReadArchive(archive, "hero", &read); - if (buf == nullptr) + if (buf == nullptr) { + Log("ReadArchive(archive, \"hero\", {}) = false", read); return false; + } bool ret = false; if (read == sizeof(*pPack)) { memcpy(pPack, buf.get(), sizeof(*pPack)); ret = true; } + Log("{} == sizeof(*pPack) ({}) = {}", read, sizeof(*pPack), read == sizeof(*pPack)); + Log("Read player {}", pPack->pName); + // Log("\tpHPBase = {}", pPack->pHPBase); + listdir("/ram", 0); + listdir("/vmu/a1", 0); return ret; } void EncodeHero(SaveWriter &saveWriter, const PlayerPack *pack) { + Log("EncodeHero"); size_t packedLen = codec_get_encoded_len(sizeof(*pack)); std::unique_ptr packed { new std::byte[packedLen] }; + Log("memcpy(packed.get(), pack, {})", sizeof(*pack)); memcpy(packed.get(), pack, sizeof(*pack)); codec_encode(packed.get(), sizeof(*pack), packedLen, pfile_get_password()); - saveWriter.WriteFile("hero", packed.get(), packedLen); + Log("Saving player {}", pack->pName); + // Log("\tpHPBase = {}", pack->pHPBase); + bool result = saveWriter.WriteFile("hero", packed.get(), packedLen /* sizeof(*pack) */); + Log("saveWriter.WriteFile(\"hero\", packed.get(), {}) = {}", packedLen, result); } SaveWriter GetSaveWriter(uint32_t saveNum) @@ -232,9 +290,22 @@ bool ArchiveContainsGame(SaveReader &hsArchive) std::optional CreateSaveReader(std::string &&path) { #ifdef UNPACKED_SAVES +#ifdef __DREAMCAST__ + Log("\tAttempting to load save file {}", path); + // no notion of directories in ramdisk, so /ram/spawn_0_sv/ doesn't exist + // instead, we check for /ram/spawn_0_sv_hero which was previously created + std::string heroFile = path + "hero"; + if (!FileExists(heroFile)) { + Log("\tFailed ):"); + return std::nullopt; + } + Log("\tFound save path {} (:", path); + return SaveReader(std::move(path)); +#else if (!FileExists(path)) return std::nullopt; return SaveReader(std::move(path)); +#endif #else std::int32_t error; return MpqArchive::Open(path.c_str(), error); @@ -500,15 +571,19 @@ HeroCompareResult CompareSaves(const std::string &actualSavePath, const std::str void pfile_write_hero(SaveWriter &saveWriter, bool writeGameData) { + Log("pfile_write_hero with writeGameData = {}", writeGameData); if (writeGameData) { SaveGameData(saveWriter); RenameTempToPerm(saveWriter); + Log("Game data saved"); } PlayerPack pkplr; Player &myPlayer = *MyPlayer; PackPlayer(pkplr, myPlayer); + Log("Player data packed"); EncodeHero(saveWriter, &pkplr); + Log("Player data saved"); if (!gbVanilla) { SaveHotkeys(saveWriter, myPlayer); SaveHeroItems(saveWriter, myPlayer); @@ -529,6 +604,146 @@ void RemoveAllInvalidItems(Player &player) } // namespace #ifdef UNPACKED_SAVES +#ifdef __DREAMCAST__ +std::unique_ptr SaveReader::ReadFile(const char *filename, std::size_t &fileSize, int32_t &error) +{ + Log("SaveReader::ReadFile(\"{}\", fileSize, error)", filename); + error = 0; + const std::string path = dir_ + filename; + Log("path = \"{}\"", path); + size_t size = 0; + uint8 *contents; + if (fs_load(path.c_str(), &contents) == -1) { + error = 1; + LogError("fs_load(\"{}\", &contents) = -1", path); + app_fatal("SaveReader::ReadFile " + path + " KO"); + return nullptr; + } + vmu_pkg_t package; + if (vmu_pkg_parse(contents, &package) < 0) { + error = 1; + free(contents); + LogError("vmu_pkg_parse = -1"); + app_fatal("vmu_pkg_parse failed"); + return nullptr; + } + Log("Parsed package {} ({})", package.desc_short, package.desc_long); + fileSize = package.data_len; + std::unique_ptr result; + result.reset(new std::byte[fileSize]); + memcpy(result.get(), package.data, fileSize); + // free(package.data); + free(contents); + return result; +} + +/* + * todo: add bzip compression to the inventory data (hitms) +std::byte* compressHeroItems(std::byte *data, size_t size) +{ + int bzBuffToBuffCompress( char* dest, + unsigned int* destLen, + char* source, + unsigned int sourceLen, + int blockSize100k, + int verbosity, + int workFactor ); + + char *compressed = malloc(sizeof(std::byte) * size); + size_t compressedLength; + if(BZ_OK != bzBuffToBuffCompress( + compressed, + &compressedLength, + data, + size, + + )) { + free(compressed); + } +}*/ + +bool SaveWriter::WriteFile(const char *filename, const std::byte *data, size_t size) +{ + Log("SaveWriter::WriteFile(\"{}\", data[], {})", filename, size); + const std::string path = dir_ + filename; + Log("dir_ = {}", dir_); + Log("path = {}", path); + const char *baseName = basename(path.c_str()); + // vmu code + if (dir_.starts_with("/vmu")) { + vmu_pkg_t package; + strcpy(package.app_id, "DevilutionX"); + strncpy(package.desc_short, filename, 20); + strcpy(package.desc_long, "Diablo 1 save data"); + package.icon_cnt = 0; + package.icon_anim_speed = 0; + package.eyecatch_type = VMUPKG_EC_NONE; + package.data_len = size; + package.data = new uint8[size]; + memcpy(package.data, data, size); + + uint8 *contents; + size_t packageSize; + if (vmu_pkg_build(&package, &contents, &packageSize) < 0) { + delete[] package.data; + LogError("vmu_pkg_build failed"); + app_fatal("vmu_pkg_build failed"); + return false; + } + FILE *file = OpenFile(path.c_str(), "wb"); + if (file == nullptr) { + delete[] package.data; + free(contents); + LogError("fopen(\"{}\", \"wb\") = nullptr", path); + app_fatal("SaveReader::WriteFile KO"); + return false; + } + size_t written = std::fwrite(contents, sizeof(uint8), packageSize, file); + if (written != packageSize) { + delete[] package.data; + free(contents); + std::fclose(file); + LogError("fwrite(data, {}, {}, file) = {} != -1", sizeof(uint8), packageSize, written); + app_fatal("vmu fwrite call failed"); + return false; + } + if (std::fclose(file) != 0) { + delete[] package.data; + free(contents); + LogError("fclose(file) = 0"); + app_fatal("fclose(file) = 0"); + return false; + } + delete[] package.data; + free(contents); + listdir("/vmu/a1", 0); + return true; + } + + // ramdisk code + bool exists = FileExists(baseName); + if (exists) { + Log("{} exists, removing it", path); + void *toFree; + size_t ignore; + int detach_result = fs_ramdisk_detach(baseName, &toFree, &ignore); + free(toFree); + Log("fs_ramdisk_detach result = {}", detach_result); + if (detach_result == -1) { + return false; + } + } + Log("\tAllocating {} bytes for path {}", size, baseName); + void *buffer = malloc(size); + memcpy(buffer, data, size); + Log("\tMallocation succeeded ? {}", buffer != NULL); + int attach_result = fs_ramdisk_attach(baseName, buffer, size); + Log("\tAttach result: {}", attach_result); + Log("Current ramdisk contents:"); + listdir("/ram", 0); + return attach_result != -1; +} +#else std::unique_ptr SaveReader::ReadFile(const char *filename, std::size_t &fileSize, int32_t &error) { std::unique_ptr result; @@ -569,16 +784,17 @@ bool SaveWriter::WriteFile(const char *filename, const std::byte *data, size_t s std::fclose(file); return true; } - +#endif // def __DREAMCAST__ void SaveWriter::RemoveHashEntries(bool (*fnGetName)(uint8_t, char *)) { char pszFileName[MaxMpqPathSize]; for (uint8_t i = 0; fnGetName(i, pszFileName); i++) { + Log("RemoveHashEntry(\"{}\")", pszFileName); RemoveHashEntry(pszFileName); } } -#endif +#endif // def UNPACKED_SAVES std::optional OpenSaveArchive(uint32_t saveNum) { @@ -595,17 +811,34 @@ std::unique_ptr ReadArchive(SaveReader &archive, const char *pszNam int32_t error; std::size_t length; + Log("ReadArchive(archive, \"{}\", {})", pszName, *pdwLen); + Log("ReadArchive 0"); std::unique_ptr result = archive.ReadFile(pszName, length, error); - if (error != 0) + if (error != 0) { + Log("ReadArchive 0 error = {}", error); + app_fatal("ReadArchive 0 = " + error); return nullptr; + } + Log("ReadArchive 1, length = {}", length); std::size_t decodedLength = codec_decode(result.get(), length, pfile_get_password()); - if (decodedLength == 0) + if (decodedLength == 0) { + Log("ReadArchive nullptr"); + app_fatal("decodedLength = 0"); return nullptr; + } + if (strcmp(pszName, "hero") == 0) { + PlayerPack pPack; + memcpy(&pPack, result.get(), decodedLength); + Log("ReadArchive player {}", pPack.pName); + // Log("\tpHPBase = {}", pPack.pHPBase); + } + Log("ReadArchive 2"); if (pdwLen != nullptr) *pdwLen = decodedLength; + Log("ReadArchive 3 {}", decodedLength); return result; } @@ -625,6 +858,7 @@ void pfile_write_hero(bool writeGameData) #ifndef DISABLE_DEMOMODE void pfile_write_hero_demo(int demo) { + Log("pfile_write_hero_demo({})", demo); std::string savePath = GetSavePath(gSaveNumber, StrCat("demo_", demo, "_reference_")); CopySaveFile(gSaveNumber, savePath); auto saveWriter = SaveWriter(savePath.c_str()); @@ -633,6 +867,7 @@ void pfile_write_hero_demo(int demo) HeroCompareResult pfile_compare_hero_demo(int demo, bool logDetails) { + Log("pfile_compare_hero_demo({}, {})", demo, logDetails); std::string referenceSavePath = GetSavePath(gSaveNumber, StrCat("demo_", demo, "_reference_")); if (!FileExists(referenceSavePath.c_str())) @@ -663,6 +898,7 @@ void sfile_write_stash() bool pfile_ui_set_hero_infos(bool (*uiAddHeroInfo)(_uiheroinfo *)) { + Log("pfile_ui_set_hero_infos"); memset(hero_names, 0, sizeof(hero_names)); for (uint32_t i = 0; i < MAX_CHARACTERS; i++) { @@ -670,9 +906,13 @@ bool pfile_ui_set_hero_infos(bool (*uiAddHeroInfo)(_uiheroinfo *)) if (archive) { PlayerPack pkplr; if (ReadHero(*archive, &pkplr)) { + Log("ReadHero OK"); + Log("Player {}", pkplr.pName); + // Log("Player {}, HP = {}", pkplr.pName, pkplr.pHPBase); _uiheroinfo uihero; uihero.saveNumber = i; strcpy(hero_names[i], pkplr.pName); + Log("hero_names[{}] = {}", i, pkplr.pName); bool hasSaveGame = ArchiveContainsGame(*archive); if (hasSaveGame) pkplr.bIsHellfire = gbIsHellfireSaveGame ? 1 : 0; @@ -686,10 +926,14 @@ bool pfile_ui_set_hero_infos(bool (*uiAddHeroInfo)(_uiheroinfo *)) Game2UiPlayer(player, &uihero, hasSaveGame); uiAddHeroInfo(&uihero); + } else { + Log("ReadHero(*archive, &pkplr) failed"); + app_fatal("ReadHero(*archive, &pkplr) failed"); } } } + Log("pfile_ui_set_hero_infos OK"); return true; } @@ -714,6 +958,7 @@ uint32_t pfile_ui_get_first_unused_save_num() bool pfile_ui_save_create(_uiheroinfo *heroinfo) { + Log("pfile_ui_save_create"); PlayerPack pkplr; uint32_t saveNum = heroinfo->saveNumber; @@ -723,6 +968,7 @@ bool pfile_ui_save_create(_uiheroinfo *heroinfo) giNumberOfLevels = gbIsHellfire ? 25 : 17; + Log("GetSaveWriter({})", saveNum); SaveWriter saveWriter = GetSaveWriter(saveNum); saveWriter.RemoveHashEntries(GetFileName); CopyUtf8(hero_names[saveNum], heroinfo->name, sizeof(hero_names[saveNum])); @@ -756,10 +1002,16 @@ void pfile_read_player_from_save(uint32_t saveNum, Player &player) PlayerPack pkplr; { std::optional archive = OpenSaveArchive(saveNum); - if (!archive) + if (!archive) { + listdir("/ram", 0); + listdir("/vmu/a1", 0); app_fatal(_("Unable to open archive")); - if (!ReadHero(*archive, &pkplr)) + } + if (!ReadHero(*archive, &pkplr)) { + listdir("/ram", 0); + listdir("/vmu/a1", 0); app_fatal(_("Unable to load character")); + } gbValidSaveFile = ArchiveContainsGame(*archive); if (gbValidSaveFile) @@ -770,6 +1022,7 @@ void pfile_read_player_from_save(uint32_t saveNum, Player &player) LoadHeroItems(player); RemoveAllInvalidItems(player); CalcPlrInv(player, false); + Log("pfile_read_player_from_save OK"); } void pfile_save_level() @@ -786,6 +1039,7 @@ void pfile_convert_levels() void pfile_remove_temp_files() { + Log("pfile_remove_temp_files"); if (gbIsMultiplayer) return; @@ -801,9 +1055,12 @@ void pfile_update(bool forceSave) return; Uint32 tick = SDL_GetTicks(); - if (!forceSave && tick - prevTick <= 60000) + // 600000 instead of 60000 + // 60000 ms is too frequent for the VMU, the game hangs too often and too long + if (!forceSave && tick - prevTick <= 600000) return; + Log("pfile_update({})", forceSave); prevTick = tick; pfile_write_hero(); sfile_write_stash(); diff --git a/Source/pfile.h b/Source/pfile.h index c025f0c9349..6643be31ec4 100644 --- a/Source/pfile.h +++ b/Source/pfile.h @@ -19,7 +19,7 @@ namespace devilution { -#define MAX_CHARACTERS 99 +#define MAX_CHARACTERS 1 // todo restore me to 99 extern bool gbValidSaveFile; @@ -28,6 +28,7 @@ struct SaveReader { explicit SaveReader(std::string &&dir) : dir_(std::move(dir)) { + Log("new SaveReader(\"{}\");", dir); } const std::string &dir() const @@ -50,6 +51,7 @@ struct SaveWriter { explicit SaveWriter(std::string &&dir) : dir_(std::move(dir)) { + Log("new SaveWriter(\"{}\");", dir); } bool WriteFile(const char *filename, const std::byte *data, size_t size); diff --git a/Source/platform/locale.cpp b/Source/platform/locale.cpp index dba90787dde..c410032d53c 100644 --- a/Source/platform/locale.cpp +++ b/Source/platform/locale.cpp @@ -22,7 +22,7 @@ #include #include // clang-format on -#elif defined(__APPLE__) and defined(USE_COREFOUNDATION) +#elif defined(__APPLE__) && defined(USE_COREFOUNDATION) #include #else #include diff --git a/Source/utils/display.cpp b/Source/utils/display.cpp index 41c1bae8e3b..7d8b6ca1204 100644 --- a/Source/utils/display.cpp +++ b/Source/utils/display.cpp @@ -43,6 +43,19 @@ #endif #endif +#ifdef __DREAMCAST__ +#include + +void enable_dma_driver() +{ + SDL_DC_VerticalWait(SDL_FALSE); + SDL_DC_ShowAskHz(SDL_TRUE); + SDL_DC_EmulateKeyboard(SDL_FALSE); + SDL_DC_EmulateMouse(SDL_FALSE); + SDL_DC_SetVideoDriver(SDL_DC_DMA_VIDEO); +} +#endif + namespace devilution { extern SDLSurfaceUniquePtr RendererTextureSurface; /** defined in dx.cpp */ @@ -232,6 +245,9 @@ float GetDpiScalingFactor() void SetVideoMode(int width, int height, int bpp, uint32_t flags) { Log("Setting video mode {}x{} bpp={} flags=0x{:08X}", width, height, bpp, flags); +#ifdef __DREAMCAST__ + enable_dma_driver(); +#endif ghMainWnd = SDL_SetVideoMode(width, height, bpp, flags); if (ghMainWnd == nullptr) { ErrSdl(); @@ -460,6 +476,9 @@ void SetFullscreenMode() if (*sgOptions.Graphics.fullscreen) { flags |= SDL_FULLSCREEN; } +#ifdef __DREAMCAST__ + enable_dma_driver(); +#endif ghMainWnd = SDL_SetVideoMode(0, 0, 0, flags); if (ghMainWnd == NULL) { ErrSdl(); diff --git a/Source/utils/file_util.cpp b/Source/utils/file_util.cpp index 5796fd25b5d..ac83269b509 100644 --- a/Source/utils/file_util.cpp +++ b/Source/utils/file_util.cpp @@ -47,6 +47,10 @@ #endif #endif +#ifdef __DREAMCAST__ +#include +#endif + namespace devilution { #if defined(_WIN32) && !defined(DEVILUTIONX_WINDOWS_NO_WCHAR) @@ -102,6 +106,19 @@ bool FileExists(const char *path) return false; } return true; +#elif defined(__DREAMCAST__) + // ramdisk access doesn't work with SDL_RWFromFile or std::filesystem::exists + int file = fs_open(path, O_RDONLY); + if (file != -1) { + fs_close(file); + return true; + } + file = fs_open(path, O_RDONLY | O_DIR); + if (file != -1) { + fs_close(file); + return true; + } + return false; #elif (_POSIX_C_SOURCE >= 200112L || defined(_BSD_SOURCE) || defined(__APPLE__)) && !defined(__ANDROID__) return ::access(path, F_OK) == 0; #elif defined(DVL_HAS_FILESYSTEM) @@ -166,7 +183,7 @@ bool FileExistsAndIsWriteable(const char *path) #ifdef _WIN32 const DWORD attr = WindowsGetFileAttributes(path); return attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_READONLY) == 0; -#elif (_POSIX_C_SOURCE >= 200112L || defined(_BSD_SOURCE) || defined(__APPLE__)) && !defined(__ANDROID__) +#elif (_POSIX_C_SOURCE >= 200112L || defined(_BSD_SOURCE) || defined(__APPLE__)) && !defined(__ANDROID__) && !defined(__DREAMCAST__) return ::access(path, W_OK) == 0; #else if (!FileExists(path)) @@ -297,6 +314,24 @@ void RecursivelyCreateDir(const char *path) #endif } +#ifdef __DREAMCAST__ +bool TruncateFile(const char *path, off_t size) +{ + Log("TruncateFile(\"{}\", {})", path, size); + void *contents; + size_t read = fs_load(path, &contents); + if (read == -1) { + return false; + } + + fs_unlink(path); + file_t fh = fs_open(path, O_WRONLY); + int result = fs_write(fh, contents, size); + free(contents); + return result != -1; +} +#endif + bool ResizeFile(const char *path, std::uintmax_t size) { #ifdef _WIN32 @@ -350,6 +385,8 @@ bool ResizeFile(const char *path, std::uintmax_t size) } ::CloseHandle(file); return true; +#elif __DREAMCAST__ + return TruncateFile(path, static_cast(size)); #elif _POSIX_C_SOURCE >= 200112L || defined(_BSD_SOURCE) || defined(__APPLE__) return ::truncate(path, static_cast(size)) == 0; #else diff --git a/Source/utils/paths.cpp b/Source/utils/paths.cpp index 2f6415bea11..22bb7b5eedb 100644 --- a/Source/utils/paths.cpp +++ b/Source/utils/paths.cpp @@ -120,6 +120,8 @@ const std::string &AssetsPath() assetsPath.emplace("D:\\assets\\"); #elif defined(__3DS__) || defined(__SWITCH__) assetsPath.emplace("romfs:/"); +#elif defined(__DREAMCAST__) + assetsPath.emplace("/cd/"); #else assetsPath.emplace(FromSDL(SDL_GetBasePath()) + ("assets" DIRECTORY_SEPARATOR_STR)); #endif diff --git a/Source/utils/sdl2_to_1_2_backports.cpp b/Source/utils/sdl2_to_1_2_backports.cpp index af280f13334..3a758c14792 100644 --- a/Source/utils/sdl2_to_1_2_backports.cpp +++ b/Source/utils/sdl2_to_1_2_backports.cpp @@ -800,6 +800,8 @@ char *SDL_GetBasePath() retval = SDL_strdup("file:sdmc:/3ds/devilutionx/"); #elif defined(__amigaos__) retval = SDL_strdup("PROGDIR:"); +#elif defined(__DREAMCAST__) + retval = SDL_strdup("/cd/"); #else /* is a Linux-style /proc filesystem available? */ @@ -879,6 +881,9 @@ char *SDL_GetPrefPath(const char *org, const char *app) #elif defined(__amigaos__) retval = SDL_strdup("PROGDIR:"); return retval; +#elif defined(__DREAMCAST__) + retval = SDL_strdup("/vmu/a1/"); + return retval; #endif if (!app) { diff --git a/Source/utils/sdl_compat.h b/Source/utils/sdl_compat.h index bf1d79d4ff5..7c5da623487 100644 --- a/Source/utils/sdl_compat.h +++ b/Source/utils/sdl_compat.h @@ -77,9 +77,16 @@ inline int SDLC_SetSurfaceAndPaletteColors(SDL_Surface *surface, SDL_Palette *pa // When the video surface is 8bit, we need to set the output palette as well. SDL_SetColors(SDL_GetVideoSurface(), colors, firstcolor, ncolors); #endif + +#ifdef __DREAMCAST__ + // todo figure out why the SDL_SetPalette call crashes on dreamcast + return 0; +#else // In SDL1, the surface always has its own distinct palette, so we need to // update it as well. return SDL_SetPalette(surface, SDL_LOGPAL, colors, firstcolor, ncolors) - 1; +#endif // defined(__DREAMCAST__) + #else // !USE_SDL1 if (SDL_SetPaletteColors(palette, colors, firstcolor, ncolors) < 0) return -1; diff --git a/docs/installing.md b/docs/installing.md index 4c721046739..fa2e71e3e06 100644 --- a/docs/installing.md +++ b/docs/installing.md @@ -225,3 +225,19 @@ If you'd like to use this option, scan the QR code below. - Copy the contents of the released .zip-file onto the root of your SD card - Copy the MPQ files to `/Emu/PORTS/Binaries/Diablo.port/FILES_HERE/` + +
Sega Dreamcast + +**Shareware version** + +- Download and extract [devilutionx-dreamcast.cdi.zip](https://github.com/diasurgical/devilutionX/releases/latest/download/devilutionx-dreamcast.cdi.zip) +- Burn it to a CD using a tool like [IMGBURN](https://www.imgburn.com/index.php?act=download) or [dcdib](https://alex-free.github.io/dcdib/) + +**Full version** (requires that you provide diabdat.mpq) + +- Download [devilutionx-dreamcast.zip](https://github.com/azihassan/devilutionX/releases/download/latest/devilutionx-dreamcast.zip) +- Extract it and copy diabdat.mpq in the data/ directory +- Package it into a .cdi file using [mkdcdisc](https://gitlab.com/simulant/mkdcdisc) with the following command: `mkdcdisc -e devilutionx.elf -o devilutionx.cdi --name 'Diablo 1' -d data/` +- Burn it to a CD using a tool like [IMGBURN](https://www.imgburn.com/index.php?act=download) or [dcdib](https://alex-free.github.io/dcdib/) + +
diff --git a/libfmt-long-double.patch b/libfmt-long-double.patch new file mode 100644 index 00000000000..40b520e3922 --- /dev/null +++ b/libfmt-long-double.patch @@ -0,0 +1,12 @@ +diff --git a/build/_deps/libfmt-src/include/fmt/format.h b/build/_deps/libfmt-src/include/fmt/format.h +index 7637c8a0d..92b2131ef 100644 +--- a/build/_deps/libfmt-src/include/fmt/format.h ++++ b/build/_deps/libfmt-src/include/fmt/format.h +@@ -1542,6 +1542,7 @@ template <> struct float_info { + template + struct float_info::digits == 64 || ++ std::numeric_limits::digits == 53 || + std::numeric_limits::digits == 113 || + is_float128::value>> { + using carrier_uint = detail::uint128_t; +