diff --git a/.github/workflows/clang-format-check.yml b/.github/workflows/clang-format-check.yml index 5e16bc1475d..24602f973a6 100644 --- a/.github/workflows/clang-format-check.yml +++ b/.github/workflows/clang-format-check.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - dreamcast paths-ignore: - '*.md' - 'docs/**' diff --git a/.github/workflows/dreamcast.yml b/.github/workflows/dreamcast.yml new file mode 100644 index 00000000000..3c929007ed4 --- /dev/null +++ b/.github/workflows/dreamcast.yml @@ -0,0 +1,133 @@ +--- +name: Sega Dreamcast + +on: # yamllint disable-line rule:truthy + push: + branches: + - master + - dreamcast + 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: Build unpack_and_minify_mpq + run: | + git clone https://github.com/diasurgical/devilutionx-mpq-tools/ && \ + cd devilutionx-mpq-tools && \ + cmake -S. -Bbuild-rel -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF && \ + cmake --build build-rel && \ + cmake --install build-rel + + - name: Download and unpack spawn.mpq + run: | + #devilutionx-assets spawn.mpq fails with unpack_and_minify_mpq + #curl -LO https://github.com/diasurgical/devilutionx-assets/releases/download/v4/spawn.mpq + curl -LO https://raw.githubusercontent.com/d07RiV/diabloweb/3a5a51e84d5dab3cfd4fef661c46977b091aaa9c/spawn.mpq + unpack_and_minify_mpq spawn.mpq + rm spawn.mpq + + - name: Download and unpack fonts.mpq + run: | + curl -LO https://github.com/diasurgical/devilutionx-assets/releases/download/v4/fonts.mpq && \ + unpack_and_minify_mpq fonts.mpq && \ + rm fonts.mpq + + - 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: Configure 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 . \ + -DCMAKE_BUILD_TYPE=Release \ + -B build + + - name: Build DevilutionX + run: | + source /opt/toolchains/dc/kos/environ.sh && cd build && kos-make + + - name: Generate .cdi + run: | + source /opt/toolchains/dc/kos/environ.sh && \ + mv spawn build/data/spawn && \ + mv fonts/fonts/ build/data/fonts/ && \ + mkdcdisc -e build/devilutionx.elf -o build/devilutionx.cdi --name 'Diablo 1' -d build/data/ + + - name: Prepare elf package + run: rm -rf build/data/spawn && rm -rf build/data/fonts/fonts/ + + - 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 6269400c05a..48936347d4e 100644 --- a/3rdParty/libfmt/CMakeLists.txt +++ b/3rdParty/libfmt/CMakeLists.txt @@ -29,7 +29,7 @@ if(DEVILUTIONX_WINDOWS_NO_WCHAR) endif() # Reduces the overall binary size by 8 KiB. -if(TARGET_PLATFORM STREQUAL "rg99") +if(TARGET_PLATFORM STREQUAL "rg99" OR PLATFORM_DREAMCAST) target_compile_definitions(fmt PUBLIC FMT_BUILTIN_TYPES=0) endif() 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..3161c23f131 --- /dev/null +++ b/CMake/platforms/dreamcast.cmake @@ -0,0 +1,50 @@ +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_MPQS 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) + +add_compile_options(-flto=none) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000000..6c327a9fa6f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,60 @@ +FROM azihassan/kallistios:fdffe33635239d46bcccf0d5c4d59bb7d2d91f38 + +RUN echo "Building unpack_and_minify_mpq..." +RUN git clone https://github.com/diasurgical/devilutionx-mpq-tools/ && \ + cd devilutionx-mpq-tools && \ + cmake -S. -Bbuild-rel -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF && \ + cmake --build build-rel && \ + cmake --install build-rel + +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 and unpacking spawn.mpq..." +RUN curl -LO https://raw.githubusercontent.com/d07RiV/diabloweb/3a5a51e84d5dab3cfd4fef661c46977b091aaa9c/spawn.mpq && \ + unpack_and_minify_mpq spawn.mpq && \ + rm spawn.mpq + +RUN echo "Downloading and unpacking fonts.mpq..." +RUN curl -LO https://github.com/diasurgical/devilutionx-assets/releases/download/v4/fonts.mpq && \ + unpack_and_minify_mpq fonts.mpq && \ + rm fonts.mpq + +#WORKDIR /opt/toolchains/dc/kos/devilutionX +#RUN echo "Copying and unpacking diabdat.mpq..." +#COPY DIABDAT.MPQ . +#RUN unpack_and_minify_mpq DIABDAT.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 "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 build/data/spawn && \ + mv fonts/fonts/ build/data/fonts/ && \ + #mv diabdat build/data/diabdat && \ + 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/init.cpp b/Source/init.cpp index 1e6488d58f7..360947149b9 100644 --- a/Source/init.cpp +++ b/Source/init.cpp @@ -123,6 +123,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(); @@ -216,7 +219,18 @@ bool IsDevilutionXMpqOutOfDate(MpqArchive &archive) #ifdef UNPACKED_MPQS bool AreExtraFontsOutOfDate(const std::string &path) { +#ifndef __DREAMCAST__ const std::string versionPath = path + "fonts" DIRECTORY_SEPARATOR_STR "VERSION"; +#else + std::string versionPath = path + "fonts" DIRECTORY_SEPARATOR_STR "VERSION"; + if (!FileExists(versionPath)) { + Log("{} not found, appending trailing period", versionPath); + // handle ISO 9660 trailing period + versionPath += "."; + Log("New versionPath: {}", versionPath); + } +#endif + if (versionPath.size() + 1 > AssetRef::PathBufSize) app_fatal("Path too long"); AssetRef ref; @@ -242,6 +256,7 @@ bool AreExtraFontsOutOfDate(MpqArchive &archive) void init_cleanup() { if (gbIsMultiplayer && gbRunGame) { + Log("init_cleanup() gbIsMultiplayer && gbRunGame"); pfile_write_hero(/*writeGameData=*/false); sfile_write_stash(); } @@ -276,7 +291,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/loadsave.cpp b/Source/loadsave.cpp index df2f2453925..fb485b91461 100644 --- a/Source/loadsave.cpp +++ b/Source/loadsave.cpp @@ -2223,7 +2223,12 @@ size_t HotkeysSize(size_t nHotkeys = NumHotkeys) void LoadHotkeys() { +#ifdef __DREAMCAST__ + // hotkeys => htks to get around VMU filename size limits + LoadHelper file(OpenSaveArchive(gSaveNumber), "htks"); +#else LoadHelper file(OpenSaveArchive(gSaveNumber), "hotkeys"); +#endif if (!file.IsValid()) return; @@ -2265,7 +2270,12 @@ void LoadHotkeys() void SaveHotkeys(SaveWriter &saveWriter, const Player &player) { +#ifdef __DREAMCAST__ + // hotkeys => htks to get around VMU filename size limits + SaveHelper file(saveWriter, "htks", HotkeysSize()); +#else SaveHelper file(saveWriter, "hotkeys", HotkeysSize()); +#endif // Write the number of spell hotkeys file.WriteLE(static_cast(NumHotkeys)); @@ -2285,7 +2295,12 @@ void SaveHotkeys(SaveWriter &saveWriter, const Player &player) void LoadHeroItems(Player &player) { +#ifdef __DREAMCAST__ + // heroitems => hitms to get around VMU filename size limits + LoadHelper file(OpenSaveArchive(gSaveNumber), "hitms"); +#else LoadHelper file(OpenSaveArchive(gSaveNumber), "heroitems"); +#endif if (!file.IsValid()) return; @@ -2564,8 +2579,13 @@ void LoadGame(bool firstflag) void SaveHeroItems(SaveWriter &saveWriter, Player &player) { - size_t itemCount = static_cast(NUM_INVLOC) + InventoryGridCells + MaxBeltItems; + size_t itemCount = static_cast(NUM_INVLOC) + InventoryGridCells + MaxBeltItems; // 7 + 40 + 8 = 55 +#ifdef __DREAMCAST__ + // 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 +#else SaveHelper file(saveWriter, "heroitems", itemCount * (gbIsHellfire ? HellfireItemSaveSize : DiabloItemSaveSize) + sizeof(uint8_t)); +#endif file.WriteLE(gbIsHellfire ? 1 : 0); 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 cbcfd6bcb31..98d2d229ea3 100644 --- a/Source/msg.cpp +++ b/Source/msg.cpp @@ -2655,6 +2655,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 a9d7ac5ebd2..a9bf2ca6e73 100644 --- a/Source/pfile.cpp +++ b/Source/pfile.cpp @@ -32,6 +32,12 @@ #include "utils/str_split.hpp" #include "utils/utf8.hpp" +#ifdef __DREAMCAST__ +#include +#include +#include +#endif + #ifdef UNPACKED_SAVES #include "utils/file_util.h" #else @@ -56,11 +62,24 @@ std::string GetSavePath(uint32_t saveNum, std::string_view savePrefix = {}) { return StrCat(paths::PrefPath(), savePrefix, gbIsSpawn +#ifdef __DREAMCAST__ + // shorter names to get around VMU filename size limits + ? (gbIsMultiplayer ? "M" : "S") + : (gbIsMultiplayer ? "m" : "s"), +#else ? (gbIsMultiplayer ? "share_" : "spawn_") : (gbIsMultiplayer ? "multi_" : "single_"), +#endif saveNum, #ifdef UNPACKED_SAVES +#ifdef __DREAMCAST__ + // flatten directory structure for VMU filesystem compatibility + // for example, /vmu/spawn_sv/hero would become /vmu/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 +91,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 +131,7 @@ bool GetTempSaveNames(uint8_t dwIndex, char *szTemp) void RenameTempToPerm(SaveWriter &saveWriter) { + Log("RenameTempToPerm"); char szTemp[MaxMpqPathSize]; char szPerm[MaxMpqPathSize]; @@ -129,8 +154,9 @@ bool ReadHero(SaveReader &archive, PlayerPack *pPack) size_t read; auto buf = ReadArchive(archive, "hero", &read); - if (buf == nullptr) + if (buf == nullptr) { return false; + } bool ret = false; if (read == sizeof(*pPack)) { @@ -138,11 +164,15 @@ bool ReadHero(SaveReader &archive, PlayerPack *pPack) ret = true; } +#ifdef __DREAMCAST__ + Log("Read player {}", pPack->pName); +#endif 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] }; @@ -232,9 +262,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 vmu, so /vmu/spawn_0_sv/ doesn't exist + // instead, we check for /vmu/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 +543,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 +576,98 @@ 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; +} + +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); + return true; + } +} +#else std::unique_ptr SaveReader::ReadFile(const char *filename, std::size_t &fileSize, int32_t &error) { std::unique_ptr result; @@ -569,16 +708,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) { @@ -596,16 +736,23 @@ std::unique_ptr ReadArchive(SaveReader &archive, const char *pszNam std::size_t length; std::unique_ptr result = archive.ReadFile(pszName, length, error); - if (error != 0) + if (error != 0) { + Log("ReadArchive 0 error = {}", 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"); return nullptr; + } + Log("ReadArchive 2"); if (pdwLen != nullptr) *pdwLen = decodedLength; + Log("ReadArchive 3 {}", decodedLength); return result; } @@ -625,6 +772,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()); @@ -663,6 +811,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++) { @@ -690,6 +839,7 @@ bool pfile_ui_set_hero_infos(bool (*uiAddHeroInfo)(_uiheroinfo *)) } } + Log("pfile_ui_set_hero_infos OK"); return true; } @@ -714,6 +864,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 +874,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 +908,12 @@ void pfile_read_player_from_save(uint32_t saveNum, Player &player) PlayerPack pkplr; { std::optional archive = OpenSaveArchive(saveNum); - if (!archive) + if (!archive) { app_fatal(_("Unable to open archive")); - if (!ReadHero(*archive, &pkplr)) + } + if (!ReadHero(*archive, &pkplr)) { app_fatal(_("Unable to load character")); + } gbValidSaveFile = ArchiveContainsGame(*archive); if (gbValidSaveFile) @@ -770,6 +924,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 +941,7 @@ void pfile_convert_levels() void pfile_remove_temp_files() { + Log("pfile_remove_temp_files"); if (gbIsMultiplayer) return; @@ -801,9 +957,17 @@ void pfile_update(bool forceSave) return; Uint32 tick = SDL_GetTicks(); +#ifdef __DREAMCAST__ + // 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; +#else if (!forceSave && tick - prevTick <= 60000) return; +#endif + Log("pfile_update({})", forceSave); prevTick = tick; pfile_write_hero(); sfile_write_stash(); diff --git a/Source/pfile.h b/Source/pfile.h index 87fbf54d98f..64b7c9d709c 100644 --- a/Source/pfile.h +++ b/Source/pfile.h @@ -19,7 +19,11 @@ namespace devilution { +#ifdef __DREAMCAST__ +#define MAX_CHARACTERS 1 // todo restore me to 99 +#else #define MAX_CHARACTERS 99 +#endif extern bool gbValidSaveFile; 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/stores.cpp b/Source/stores.cpp index 5016e557b62..5fcbb2d97da 100644 --- a/Source/stores.cpp +++ b/Source/stores.cpp @@ -1265,7 +1265,9 @@ void StartBarmaid() AddSText(0, 2, _("Gillian"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSText(0, 9, _("Would you like to:"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSText(0, 12, _("Talk to Gillian"), UiFlags::ColorBlue | UiFlags::AlignCenter, true); +#ifndef __DREAMCAST__ AddSText(0, 14, _("Access Storage"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); +#endif AddSText(0, 18, _("Say goodbye"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); AddSLine(5); CurrentItemIndex = 20; diff --git a/Source/utils/display.cpp b/Source/utils/display.cpp index f15530c436f..67e366dc269 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 */ @@ -307,6 +320,9 @@ bool SpawnWindow(const char *lpWindowName) initFlags |= SDL_INIT_GAMECONTROLLER; SDL_SetHint(SDL_HINT_ORIENTATIONS, "LandscapeLeft LandscapeRight"); +#endif +#ifdef __DREAMCAST__ + enable_dma_driver(); #endif if (SDL_Init(initFlags) <= -1) { ErrSdl(); diff --git a/Source/utils/file_util.cpp b/Source/utils/file_util.cpp index 7dd7de82cc9..0a0114ed976 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,20 @@ bool FileExists(const char *path) return false; } return true; +#elif defined(__DREAMCAST__) + // ramdisk access doesn't work with SDL_RWFromFile or std::filesystem::exists + // todo check to see if this is needed with vmu fs + 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 +184,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 +315,25 @@ 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); + fs_close(fh); + free(contents); + return result != -1; +} +#endif + bool ResizeFile(const char *path, std::uintmax_t size) { #ifdef _WIN32 @@ -350,6 +387,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 546c207d415..807137174bc 100644 --- a/Source/utils/paths.cpp +++ b/Source/utils/paths.cpp @@ -130,6 +130,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 ee47b5d7260..8081b1c460f 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 84ee9c0b4b0..f01825ae886 100644 --- a/Source/utils/sdl_compat.h +++ b/Source/utils/sdl_compat.h @@ -79,9 +79,16 @@ inline int SDLC_SetSurfaceAndPaletteColors(SDL_Surface *surface, SDL_Palette *pa SDL_SetColors(videoSurface, colors, firstcolor, ncolors); if (videoSurface == surface) return 0; #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/) + +