diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index f450aa87c..a684cbf17 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -79,10 +79,46 @@ jobs: path: bin/ retention-days: 0 + build-flatpak-bundle: + name: "Build flatpak bundle" + runs-on: ubuntu-latest + needs: [build-x86_64-linux] + container: + image: bilelmoussaoui/flatpak-github-actions:freedesktop-23.08 + options: --privileged + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: pd-x86_64-linux + path: dist/linux/bin/ + + - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 + with: + bundle: io.github.fgsfdsfgs.perfect_dark.flatpak + manifest-path: dist/linux/io.github.fgsfdsfgs.perfect_dark.yaml + cache-key: flatpak-builder-${{ github.sha }} + upload-artifact: true + + # - name: Build Flatpak bundle + # run: | + # cd dist/linux/ + # flatpak-builder build --force-clean io.github.fgsfdsfgs.perfect_dark.yaml + # flatpak build-export export build + # flatpak build-bundle export io.github.fgsfdsfgs.perfect_dark.flatpak io.github.fgsfdsfgs.perfect_dark + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: io.github.fgsfdsfgs.perfect_dark.flatpak + path: io.github.fgsfdsfgs.perfect_dark.flatpak + retention-days: 0 + + publish-latest-build: runs-on: ubuntu-latest if: github.ref == 'refs/heads/port-x64' - needs: [build-x86_64-windows, build-x86_64-linux] + needs: [build-x86_64-windows, build-x86_64-linux, build-flatpak-bundle] permissions: contents: write steps: @@ -98,6 +134,7 @@ jobs: pushd ci-artifacts tar czf ../ci-release/pd-x86_64-linux.tar.gz pd-x86_64-linux zip -r ../ci-release/pd-x86_64-windows.zip pd-x86_64-windows + mv io.github.fgsfdsfgs.perfect_dark.flatpak/io.github.fgsfdsfgs.perfect_dark.flatpak ../ci-release/ popd git tag --force ci-dev-build-x64 port-x64 git push --force origin ci-dev-build-x64 diff --git a/.gitignore b/.gitignore index c81aec73a..3d0bebd42 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ *.exe pd.*.z64 build +.flatpak-builder extracted src/assets/*/*.bin src/assets/*/*.ctl diff --git a/Makefile.port b/Makefile.port index 8b3ecfd00..93f71a921 100644 --- a/Makefile.port +++ b/Makefile.port @@ -230,6 +230,10 @@ CXX_O_FILES := $(patsubst %.cpp, $(B_DIR)/%.o, $(CXX_FILES)) O_FILES := $(C_O_FILES) $(CXX_O_FILES) +ifneq (,$(findstring windows,$(TARGET_PLATFORM))) + O_FILES += $(B_DIR)/icon.res +endif + INCLUDES = \ -I include \ -I include/PR \ @@ -247,7 +251,7 @@ else LD := $(CC) endif -$(C_O_FILES): OPT_LVL := $(GCC_OPT_LVL) +$(O_FILES): OPT_LVL := $(GCC_OPT_LVL) COMMON_CFLAGS := $(C_DEFINES) -DAVOID_UB=1 $(INCLUDES) $(TARGET_CFLAGS) -g \ -fno-inline-functions \ @@ -322,6 +326,9 @@ $(B_DIR)/%.o: %.cpp $(JSON_HEADERS) @mkdir -p $(dir $@) $(CXX) -c $(CXXFLAGS) $(OPT_LVL) -o $@ $< +$(B_DIR)/%.res: dist/windows/%.rc + windres $< -O coff -o $@ + ################################################################################ # Clean rules diff --git a/README.md b/README.md index 045a7824a..020f75025 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ There are minor graphics- and gameplay-related issues, and possibly occasional c * experimental high framerate support (up to 240 FPS): * set `Game.TickRateDivisor` to `0` in `pd.ini` to activate; * in practice the game will have issues running faster than ~165 FPS, so use VSync or `Video.FramerateLimit` to cap it. +* emulate the Transfer Pak functionality the game has on the Nintendo 64 to unlock some cheats automatically. Currently only 32-bit platforms are supported, namely x86 Windows and Linux. Note that 32-bit binaries will still work on 64-bit versions of those platforms, @@ -58,6 +59,8 @@ If you want to use a PAL or JPN ROM instead, put them into the `data` directory * PAL: ROM name `pd.pal-final.z64`, EXE name `pd.pal.exe`. * JPN: ROM name `pd.jpn-final.z64`, EXE name `pd.jpn.exe`. +Optionally, you can also put your Perfect Dark for GameBoy Color ROM named `pd.gbc` in the `data` directory if you want to emulate having the Nintendo 64's Transfer Pak and unlock some cheats automatically. + Additional information can be found in the [wiki](https://github.com/fgsfdsfgs/perfect_dark/wiki). A GPU supporting OpenGL 3.0 or above is required to run the port. @@ -134,5 +137,6 @@ It might be possible to build a 32-bit ARM executable, but this has not been tes * [Ship of Harkinian team](https://github.com/Kenix3/libultraship/tree/main/src/graphic/Fast3D), Emill and MaikelChan for the libultraship version of fast3d that this port uses; * lieff for [minimp3](https://github.com/lieff/minimp3); * Mouse Injector and 1964GEPD authors for some of the 60FPS- and mouselook-related fixes; +* NicNamSam for the icon; * everyone who has submitted pull requests and issues to this repository and tested the port; * probably more I'm forgetting. diff --git a/dist/linux/io.github.fgsfdsfgs.perfect_dark.desktop b/dist/linux/io.github.fgsfdsfgs.perfect_dark.desktop new file mode 100755 index 000000000..663933b7e --- /dev/null +++ b/dist/linux/io.github.fgsfdsfgs.perfect_dark.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=Perfect Dark +Comment=Modern source port of Perfect Dark +Exec=pd +Icon=io.github.fgsfdsfgs.perfect_dark +Terminal=false +Type=Application +Categories=Game;ActionGame; +StartupNotify=false diff --git a/dist/linux/io.github.fgsfdsfgs.perfect_dark.metainfo.xml b/dist/linux/io.github.fgsfdsfgs.perfect_dark.metainfo.xml new file mode 100644 index 000000000..10a2f92ee --- /dev/null +++ b/dist/linux/io.github.fgsfdsfgs.perfect_dark.metainfo.xml @@ -0,0 +1,45 @@ + + + io.github.fgsfdsfgs.perfect_dark + Perfect Dark + + Perfect Dark Contributors + https://github.com/fgsfdsfgs/perfect_dark/graphs/contributors + + io.github.fgsfdsfgs.perfect_dark.desktop + Modern cross-platform port of Perfect Dark + CC0-1.0 + MIT + https://github.com/fgsfdsfgs/perfect_dark/ + +

This is a port of Ryan Dywer's decompilation of classic N64 shooter Perfect Dark to modern systems.

+
+ + + Game + ActionGame + Shooter + + + n64 + scifi + cyberpunk + retro + sourceport + port + + + + + + moderate + mild + + https://github.com/fgsfdsfgs/perfect_dark/ +
diff --git a/dist/linux/io.github.fgsfdsfgs.perfect_dark.png b/dist/linux/io.github.fgsfdsfgs.perfect_dark.png new file mode 100644 index 000000000..bd236a116 Binary files /dev/null and b/dist/linux/io.github.fgsfdsfgs.perfect_dark.png differ diff --git a/dist/linux/io.github.fgsfdsfgs.perfect_dark.sh b/dist/linux/io.github.fgsfdsfgs.perfect_dark.sh new file mode 100644 index 000000000..0e163c2a9 --- /dev/null +++ b/dist/linux/io.github.fgsfdsfgs.perfect_dark.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# Create user data directories if necessary +userdirs=( roms saves mods ) +for path in "${userdirs[@]}"; do + if [ ! -d "${XDG_DATA_HOME}/${path}" ]; then + mkdir -p "${XDG_DATA_HOME}/${path}" + fi +done + +# Default to pd as executable but switch to jpn/pal if the NTSC rom +# is missing but JPN/PAL are present. +executable="pd" +if [ -f ${XDG_DATA_HOME}/roms/pd.ntsc-final.z64 ] | \ + [ -f ${XDG_DATA_HOME}/roms/pd.ntsc-1.0.z64 ]; then + executable="pd" +elif [ -f ${XDG_DATA_HOME}/roms/pd.jpn-final.z64 ]; then + executable="pd.jpn" +elif [ -f ${XDG_DATA_HOME}/roms/pd.pal-final.z64 ]; then + executable="pd.pal" +fi + +# If first parameter passed to this script is one of the pd executables, +# run that regardless of ROM auto-detection above and pass the remaining +# parameters to it; otherwise pass all arguments to the executable selected +# above +case $1 in + "pd."*) + executable=$1 + params="${@:2}" + ;; + *) + params="$@" + ;; +esac + +# Run game +$executable --basedir ${XDG_DATA_HOME}/roms --savedir ${XDG_DATA_HOME}/saves $params diff --git a/dist/linux/io.github.fgsfdsfgs.perfect_dark.yaml b/dist/linux/io.github.fgsfdsfgs.perfect_dark.yaml new file mode 100644 index 000000000..7ef407946 --- /dev/null +++ b/dist/linux/io.github.fgsfdsfgs.perfect_dark.yaml @@ -0,0 +1,93 @@ +app-id: io.github.fgsfdsfgs.perfect_dark +runtime: org.freedesktop.Platform +runtime-version: &runtime-version '23.08' +sdk: org.freedesktop.Sdk +command: io.github.fgsfdsfgs.perfect_dark.sh + +finish-args: + # multiarch is required for 32bit binaries + - --allow=multiarch + # hardware 3D and controller access + - --device=all + # X11 + XShm access + - --share=ipc + - --socket=x11 + # Audio + - --socket=pulseaudio + # Enable network for multiplayer + - --share=network + +# Parameters for 32-bit extensions +x-gl-version: &gl-version '1.4' +x-gl-versions: &gl-versions 23.08;1.4 +x-gl-merge-dirs: &gl-merge-dirs vulkan/icd.d;glvnd/egl_vendor.d;egl/egl_external_platform.d;OpenCL/vendors;lib/dri;lib/d3d;lib/gbm;vulkan/explicit_layer.d;vulkan/implicit_layer.d + +add-extensions: + org.freedesktop.Platform.Compat.i386: + directory: lib/i386-linux-gnu + version: '23.08' + + org.freedesktop.Platform.GL32: + directory: lib/i386-linux-gnu/GL + version: *gl-version + versions: *gl-versions + subdirectories: true + no-autodownload: true + autodelete: false + add-ld-path: lib + merge-dirs: *gl-merge-dirs + download-if: active-gl-driver + enable-if: active-gl-driver + autoprune-unless: active-gl-driver + + org.freedesktop.Platform.GL32.Debug: + directory: lib/debug/lib/i386-linux-gnu/GL + version: *gl-version + versions: *gl-versions + subdirectories: true + no-autodownload: true + merge-dirs: *gl-merge-dirs + enable-if: active-gl-driver + autoprune-unless: active-gl-driver + + org.freedesktop.Platform.VAAPI.Intel.i386: + directory: lib/i386-linux-gnu/dri/intel-vaapi-driver + version: *runtime-version + versions: *runtime-version + autodelete: false + no-autodownload: true + add-ld-path: lib + download-if: have-intel-gpu + autoprune-unless: have-intel-gpu + +modules: + - name: perfect_dark + buildsystem: simple + sources: + - type: file + path: bin/pd + - type: file + path: bin/pd.jpn + - type: file + path: bin/pd.pal + - type: file + path: io.github.fgsfdsfgs.perfect_dark.sh + - type: file + path: io.github.fgsfdsfgs.perfect_dark.png + - type: file + path: io.github.fgsfdsfgs.perfect_dark.metainfo.xml + - type: file + path: io.github.fgsfdsfgs.perfect_dark.desktop + build-commands: [] + post-install: + # create extension mountpoints + - mkdir -p /app/lib/i386-linux-gnu + - mkdir -p /app/lib/i386-linux-gnu/GL + - mkdir -p /app/lib/debug/lib/i386-linux-gnu/GL + - mkdir -p /app/lib/i386-linux-gnu/dri/intel-vaapi-driver + # port + - install -Dm 755 -t /app/bin/ pd* + - install -Dm 755 ${FLATPAK_ID}.sh /app/bin/${FLATPAK_ID}.sh + - install -Dm 644 ${FLATPAK_ID}.desktop /app/share/applications/${FLATPAK_ID}.desktop + - install -Dm 644 ${FLATPAK_ID}.metainfo.xml /app/share/metainfo/${FLATPAK_ID}.metainfo.xml + - install -Dm 644 ${FLATPAK_ID}.png /app/share/icons/hicolor/256x256/apps/${FLATPAK_ID}.png diff --git a/dist/windows/icon.ico b/dist/windows/icon.ico new file mode 100644 index 000000000..c69fb2fee Binary files /dev/null and b/dist/windows/icon.ico differ diff --git a/dist/windows/icon.rc b/dist/windows/icon.rc new file mode 100644 index 000000000..f2e2d4d79 --- /dev/null +++ b/dist/windows/icon.rc @@ -0,0 +1 @@ +id ICON icon.ico diff --git a/port/include/romdata.h b/port/include/romdata.h index 6d604803f..545190eb8 100644 --- a/port/include/romdata.h +++ b/port/include/romdata.h @@ -32,4 +32,6 @@ u8 *romdataSegGetDataEnd(const char *segName); u32 romdataSegGetSize(const char *segName); u32 romdataFileGetEstimatedSize(u32 size, FileType filetype); +s32 romdataCheckGbcRom(void); + #endif diff --git a/port/src/main.c b/port/src/main.c index e31e090a2..5e404426d 100644 --- a/port/src/main.c +++ b/port/src/main.c @@ -108,6 +108,8 @@ int main(int argc, const char **argv) audioInit(); romdataInit(); + g_ValidGbcRomFound = romdataCheckGbcRom(); + gameInit(); if (fsGetModDir()) { diff --git a/port/src/romdata.c b/port/src/romdata.c index 03b617b21..e16315714 100644 --- a/port/src/romdata.c +++ b/port/src/romdata.c @@ -50,6 +50,9 @@ #define ROMDATA_MAX_FILES 2048 +#define GBC_ROM_NAME "pd.gbc" +#define GBC_ROM_SIZE 4194304 + u8 *g_RomFile; u32 g_RomFileSize; @@ -398,6 +401,53 @@ s32 romdataInit(void) return 0; } +static inline bool romdataCheckGbcRomContents(const u8 *gbcRomFile, const u32 gbcRomSize) +{ + if (gbcRomSize != GBC_ROM_SIZE) { + return false; + } + + // ROM title + if (memcmp(gbcRomFile + 0x134, "PerfDark VPDE", 15) != 0) { + return false; + } + + // Licensee code + if (memcmp(gbcRomFile + 0x144, "4Y", 2) != 0) { + return false; + } + + // Header and global checksums + if (gbcRomFile[0x14D] != 0xA1 || gbcRomFile[0x14E] != 0xAD || gbcRomFile[0x14F] != 0x0F) { + return false; + } + + return true; +} + +s32 romdataCheckGbcRom(void) +{ + if (fsFileSize(GBC_ROM_NAME) < 0) { + // bail early if it doesn't exist to avoid generating error messages + return false; + } + + u32 gbcRomSize = 0; + u8 *gbcRomFile = fsFileLoad(GBC_ROM_NAME, &gbcRomSize); + if (!gbcRomFile) { + return false; + } + + const bool ret = romdataCheckGbcRomContents(gbcRomFile, gbcRomSize); + sysMemFree(gbcRomFile); + + if (ret) { + sysLogPrintf(LOG_NOTE, "romdataCheckGbcRom: valid GBC rom found"); + } + + return ret; +} + s32 romdataFileGetSize(s32 fileNum) { if (fileNum < 1 || fileNum >= ROMDATA_MAX_FILES) { diff --git a/src/game/mplayer/mplayer.c b/src/game/mplayer/mplayer.c index 80b9a4c1f..142d94028 100644 --- a/src/game/mplayer/mplayer.c +++ b/src/game/mplayer/mplayer.c @@ -3146,6 +3146,21 @@ void mpRemoveSimulant(s32 index) mpGenerateBotNames(); } +#ifndef PLATFORM_N64 +void mpCopySimulant(s32 index) +{ + s32 dest = mpGetSlotForNewBot(); + + g_MpSetup.chrslots |= 1 << (dest + 4); + g_BotConfigsArray[dest].base.name[0] = g_BotConfigsArray[index].base.name[0]; + g_BotConfigsArray[dest].base.mpheadnum = g_BotConfigsArray[index].base.mpheadnum; + g_BotConfigsArray[dest].base.mpbodynum = g_BotConfigsArray[index].base.mpbodynum; + g_BotConfigsArray[dest].type = g_BotConfigsArray[index].type; + g_BotConfigsArray[dest].difficulty = g_BotConfigsArray[index].difficulty; + mpGenerateBotNames(); +} +#endif + bool mpHasSimulants(void) { if ((g_MpSetup.chrslots & 0xfff0) != 0) { diff --git a/src/game/mplayer/setup.c b/src/game/mplayer/setup.c index 646319f34..ba9800e83 100644 --- a/src/game/mplayer/setup.c +++ b/src/game/mplayer/setup.c @@ -2933,6 +2933,24 @@ MenuItemHandlerResult menuhandlerMpDeleteSimulant(s32 operation, struct menuitem return 0; } +#ifndef PLATFORM_N64 +MenuItemHandlerResult menuhandlerMpCopySimulant(s32 operation, struct menuitem *item, union handlerdata *data) +{ + switch (operation) { + case MENUOP_SET: + mpCopySimulant(g_Menus[g_MpPlayerNum].mpsetup.slotindex); + menuPopDialog(); + break; + case MENUOP_CHECKDISABLED: + if (mpHasUnusedBotSlots() == 0) { + return true; + } + } + + return 0; +} +#endif + char *mpMenuTitleEditSimulant(struct menudialogdef *dialogdef) { sprintf(g_StringPointer, "%s", &g_BotConfigsArray[g_Menus[g_MpPlayerNum].mpsetup.slotindex].base.name); @@ -3151,6 +3169,16 @@ struct menuitem g_MpEditSimulantMenuItems[] = { 0, NULL, }, +#ifndef PLATFORM_N64 + { + MENUITEMTYPE_SELECTABLE, + 0, + MENUITEMFLAG_LOCKABLEMINOR | MENUITEMFLAG_LITERAL_TEXT, + (uintptr_t)"Copy Simulant\n", + 0, + menuhandlerMpCopySimulant, + }, +#endif { MENUITEMTYPE_SELECTABLE, 0, diff --git a/src/game/pak.c b/src/game/pak.c index c58b59de3..5747f74ee 100644 --- a/src/game/pak.c +++ b/src/game/pak.c @@ -329,6 +329,10 @@ u8 g_PaksPlugged = 0; bool var80075d14 = true; #endif +#ifndef PLATFORM_N64 +bool g_ValidGbcRomFound = false; +#endif + u32 pakGetBlockSize(s8 device) { return device == SAVEDEVICE_GAMEPAK ? 0x10 : 0x20; @@ -5982,6 +5986,7 @@ s32 pak0f11e750(s8 device) bool gbpakIsAnyPerfectDark(void) { +#ifdef PLATFORM_N64 s8 i; for (i = 0; i < MAX_PLAYERS; i++) { @@ -5991,6 +5996,9 @@ bool gbpakIsAnyPerfectDark(void) } return false; +#else + return g_ValidGbcRomFound; +#endif } /** diff --git a/src/include/bss.h b/src/include/bss.h index e59e90b29..5382431b1 100644 --- a/src/include/bss.h +++ b/src/include/bss.h @@ -293,5 +293,8 @@ extern struct bossfile g_BossFile; extern struct chrdata *g_MpBotChrPtrs[MAX_BOTS]; extern s32 g_JpnMaxCacheItems; extern s32 var8009d370jf; +#ifndef PLATFORM_N64 +extern bool g_ValidGbcRomFound; +#endif #endif diff --git a/src/include/game/mplayer/mplayer.h b/src/include/game/mplayer/mplayer.h index bb4b17e56..b2ab42f95 100644 --- a/src/include/game/mplayer/mplayer.h +++ b/src/include/game/mplayer/mplayer.h @@ -90,6 +90,9 @@ void mpCreateBotFromProfile(s32 botnum, u8 difficulty); void mpSetBotDifficulty(s32 botnum, s32 difficulty); s32 mpGetSlotForNewBot(void); void mpRemoveSimulant(s32 index); +#ifndef PLATFORM_N64 +void mpCopySimulant(s32 index); +#endif bool mpHasSimulants(void); bool mpHasUnusedBotSlots(void); bool mpIsSimSlotEnabled(s32 slot); diff --git a/src/include/game/mplayer/setup.h b/src/include/game/mplayer/setup.h index ebb915747..b92f76c6e 100644 --- a/src/include/game/mplayer/setup.h +++ b/src/include/game/mplayer/setup.h @@ -144,6 +144,9 @@ MenuDialogHandlerResult menudialogMpSimulant(s32 operation, struct menudialogdef MenuItemHandlerResult menuhandlerMpSimulantHead(s32 operation, struct menuitem *item, union handlerdata *data); MenuItemHandlerResult menuhandlerMpSimulantBody(s32 operation, struct menuitem *item, union handlerdata *data); MenuItemHandlerResult menuhandlerMpDeleteSimulant(s32 operation, struct menuitem *item, union handlerdata *data); +#ifndef PLATFORM_N64 +MenuItemHandlerResult menuhandlerMpCopySimulant(s32 operation, struct menuitem *item, union handlerdata *data); +#endif MenuItemHandlerResult menuhandlerMpChangeSimulantType(s32 operation, struct menuitem *item, union handlerdata *data); MenuItemHandlerResult menuhandlerMpClearAllSimulants(s32 operation, struct menuitem *item, union handlerdata *data); MenuItemHandlerResult menuhandlerMpAddSimulant(s32 operation, struct menuitem *item, union handlerdata *data);