diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7167c65..af2f2e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,10 @@ jobs: type: sprx extra_args: "" artifact: build/ci_sprx.sprx + - name: ci_ghplugin + type: ghplugin + extra_args: "" + artifact: build/ci_ghplugin.prx - name: ci_pkg type: exe extra_args: "--pkg" @@ -86,6 +90,16 @@ jobs: echo "OO_PS4_TOOLCHAIN=$HOME/OpenOrbis-PS4-Toolchain" >> "$GITHUB_ENV" echo "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1" >> "$GITHUB_ENV" + - name: Install GoldHEN Plugin SDK + if: matrix.type == 'ghplugin' + run: | + set -e + git clone --depth 1 https://github.com/GoldHEN/GoldHEN_Plugins_SDK.git /tmp/goldhen-sdk + make -C /tmp/goldhen-sdk install + # 'make install' ships the headers and libGoldHEN_Hook.a but leaves crtprx.o + # behind in the SDK's own build dir; TYPE GHPLUGIN looks for it in lib/. + cp /tmp/goldhen-sdk/build/crtprx.o "$OO_PS4_TOOLCHAIN/lib/" + - name: Build OrbisForge CLI run: dotnet publish src/OrbisForge.Cli -c Release -r linux-x64 --self-contained -p:PublishSingleFile=true -o publish/linux-x64 diff --git a/README.md b/README.md index 2b416e0..a8da1cd 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,9 @@ OrbisForge generates self-contained CMake projects for the [OpenOrbis PS4 Toolchain](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain), producing `eboot.bin` -executables, `.prx` user libraries, `.sprx` signed system libraries, and (optionally) full -signed/encrypted `.pkg` installers — through a single `add_orbis_target()` CMake function. +executables, `.prx` user libraries, `.sprx` signed system libraries, GoldHEN plugins, and +(optionally) full signed/encrypted `.pkg` installers — through a single `add_orbis_target()` +CMake function. A Windows GUI wizard and a cross-platform CLI both scaffold the same project layout. > The GUI wizard is Windows-only, built on [UIFramework](https://github.com/xeghosted/UIFramework) @@ -19,9 +20,17 @@ A Windows GUI wizard and a cross-platform CLI both scaffold the same project lay ## Features -- **All three artifact types** — EXE (`eboot.bin`), PRX, and SPRX, via one `add_orbis_target()` - call. `TYPE` selects the right CRT object (`crt1.o` vs `crtlib.o`), the right `create-fself` - packaging mode (`--eboot` vs `--lib`), and a sensible default `-ptype`. +- **Every artifact type** — EXE (`eboot.bin`), PRX, SPRX, and GHPLUGIN, via one + `add_orbis_target()` call. `TYPE` selects the right CRT object (`crt1.o` vs `crtlib.o` vs + `crtprx.o`), the right `create-fself` packaging mode (`--eboot` vs `--lib`), and a sensible + default `-ptype`. +- **GoldHEN plugins** — `TYPE GHPLUGIN` builds a plugin the GoldHEN loader actually accepts. + It differs from a plain PRX in three ways that must all be right or the plugin builds + cleanly and then silently never loads: the [GoldHEN Plugin + SDK](https://github.com/GoldHEN/GoldHEN_Plugins_SDK)'s `crtprx.o` instead of `crtlib.o`, + entry at `_init`, and `SceLibcInternal`/`SceSysmodule` instead of `c`/`c++`. Requires the + SDK installed into the toolchain — the generated CMake fails with instructions if it isn't. + The scaffolded starter plugin shows a PS4 notification from `module_start`. - **Optional full `.pkg` installer** — `add_orbis_package()` stages `eboot.bin` and a generated `param.sfo` into a directory tree matching the final package layout, then drives `create-gp4` + `PkgTool.Core pkg_build` to produce a real signed/encrypted `.pkg`. diff --git a/src/OrbisForge.Cli/Program.cs b/src/OrbisForge.Cli/Program.cs index b96ffbe..ea0412e 100644 --- a/src/OrbisForge.Cli/Program.cs +++ b/src/OrbisForge.Cli/Program.cs @@ -5,13 +5,15 @@ var help = """ OrbisForge - project scaffolder for the OpenOrbis PS4 Toolchain (CMake-based) Usage: - orbisforge new --name --type exe|prx|sprx [options] + orbisforge new --name --type exe|prx|sprx|ghplugin [options] orbisforge build [path] [--clean] orbisforge deploy [path] 'new' options: --name Project / target name (required) - --type exe, prx, or sprx (required) + --type exe, prx, sprx, or ghplugin (required). ghplugin builds a + GoldHEN plugin and needs the GoldHEN Plugin SDK installed + into the toolchain (crtprx.o + libGoldHEN_Hook.a in lib/). --output Output directory (default: current directory) --pkg EXE only: also scaffold a full .pkg installer step --ptype Override create-fself -ptype (advanced) @@ -33,6 +35,7 @@ var help = """ Examples: orbisforge new --name hello_cmake --type exe orbisforge new --name my_plugin --type sprx --output E:\Projects\PS4 + orbisforge new --name notify_plugin --type ghplugin --output E:\Projects\PS4 orbisforge new --name my_game --type exe --pkg orbisforge new --name my_game --type exe --deploy-host 192.168.1.50 --deploy-path /data/homebrew/my_game/ orbisforge build @@ -121,7 +124,7 @@ int RunNew(string[] args) if (typeText is null || !OrbisTargetTypeExtensions.TryParse(typeText, out var type)) { - Console.Error.WriteLine("--type is required and must be one of: exe, prx, sprx."); + Console.Error.WriteLine("--type is required and must be one of: exe, prx, sprx, ghplugin."); return 1; } diff --git a/src/OrbisForge.Core/OrbisTargetType.cs b/src/OrbisForge.Core/OrbisTargetType.cs index a03d91b..4931959 100644 --- a/src/OrbisForge.Core/OrbisTargetType.cs +++ b/src/OrbisForge.Core/OrbisTargetType.cs @@ -14,6 +14,10 @@ public enum OrbisTargetType /// Signed system library/module -> <name>.sprx (ptype "system_dynlib"). Sprx, + + /// GoldHEN plugin -> <name>.prx (ptype "fake"). Like Prx, but entered at + /// _init via the GoldHEN SDK's crtprx.o so the loader reaches module_start. + GoldHenPlugin, } public static class OrbisTargetTypeExtensions @@ -23,12 +27,18 @@ public static class OrbisTargetTypeExtensions OrbisTargetType.Exe => "EXE", OrbisTargetType.Prx => "PRX", OrbisTargetType.Sprx => "SPRX", + OrbisTargetType.GoldHenPlugin => "GHPLUGIN", _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), }; public static bool IsLibrary(this OrbisTargetType type) => type is OrbisTargetType.Prx or OrbisTargetType.Sprx; + /// True when the project's primary source is src/<name>.cpp rather than + /// src/main.cpp. Libraries and GoldHEN plugins both have no main(). + public static bool UsesNamedSourceFile(this OrbisTargetType type) => + type.IsLibrary() || type is OrbisTargetType.GoldHenPlugin; + /// Path to the packaged artifact create-fself produces, relative to the /// project root (i.e. under the CMake build directory). public static string ArtifactRelativePath(this OrbisTargetType type, string name) => type switch @@ -36,6 +46,7 @@ public static class OrbisTargetTypeExtensions OrbisTargetType.Exe => "build/eboot.bin", OrbisTargetType.Prx => $"build/{name}.prx", OrbisTargetType.Sprx => $"build/{name}.sprx", + OrbisTargetType.GoldHenPlugin => $"build/{name}.prx", _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), }; @@ -46,6 +57,8 @@ public static class OrbisTargetTypeExtensions case "EXE": case "BIN": type = OrbisTargetType.Exe; return true; case "PRX": type = OrbisTargetType.Prx; return true; case "SPRX": type = OrbisTargetType.Sprx; return true; + case "GHPLUGIN": case "GOLDHEN": case "PLUGIN": + type = OrbisTargetType.GoldHenPlugin; return true; default: type = default; return false; } } diff --git a/src/OrbisForge.Core/ProjectGenerator.cs b/src/OrbisForge.Core/ProjectGenerator.cs index e6a14fe..123277e 100644 --- a/src/OrbisForge.Core/ProjectGenerator.cs +++ b/src/OrbisForge.Core/ProjectGenerator.cs @@ -34,7 +34,13 @@ public static class ProjectGenerator WriteFile(Path.Combine(root, ".vscode", "settings.json"), VsCodeTemplate.SettingsJson, files); WriteFile(Path.Combine(root, ".gitignore"), Unix(GitignoreTemplate.Gitignore), files); - if (options.Type.IsLibrary()) + if (options.Type == OrbisTargetType.GoldHenPlugin) + { + // A plugin is entered at module_start, so it gets neither a main() nor + // the library pair's public header. + WriteFile(Path.Combine(root, "src", $"{name}.cpp"), SourceTemplate.PluginCpp(name), files); + } + else if (options.Type.IsLibrary()) { WriteFile(Path.Combine(root, "src", $"{name}.cpp"), SourceTemplate.LibCpp(name), files); WriteFile(Path.Combine(root, "src", $"{name}.h"), SourceTemplate.LibHeader(name), files); diff --git a/src/OrbisForge.Core/Templates/CMakeListsTemplate.cs b/src/OrbisForge.Core/Templates/CMakeListsTemplate.cs index 43817c1..556afe1 100644 --- a/src/OrbisForge.Core/Templates/CMakeListsTemplate.cs +++ b/src/OrbisForge.Core/Templates/CMakeListsTemplate.cs @@ -17,7 +17,7 @@ internal static class CMakeListsTemplate sb.AppendLine("endif()"); sb.AppendLine(); - var sources = options.Type.IsLibrary() + var sources = options.Type.UsesNamedSourceFile() ? $"src/{name}.cpp" : "src/main.cpp"; diff --git a/src/OrbisForge.Core/Templates/OrbisTargetsTemplate.cs b/src/OrbisForge.Core/Templates/OrbisTargetsTemplate.cs index 87dd998..2a2aff0 100644 --- a/src/OrbisForge.Core/Templates/OrbisTargetsTemplate.cs +++ b/src/OrbisForge.Core/Templates/OrbisTargetsTemplate.cs @@ -6,20 +6,34 @@ internal static class OrbisTargetsTemplate # add_orbis_target() / add_orbis_package() - generated by OrbisForge. # # add_orbis_target( - # TYPE EXE|PRX|SPRX + # TYPE EXE|PRX|SPRX|GHPLUGIN # SOURCES # [LIBS ] # default: c kernel c++ - # [PTYPE ] # default: "fake" (EXE/PRX) or "system_dynlib" (SPRX) + # # (GHPLUGIN: SceLibcInternal kernel SceSysmodule GoldHEN_Hook) + # [PTYPE ] # default: "fake", or "system_dynlib" for SPRX # [PAID ] # default: 0x3800000000000011 - # [LIBNAME ] # PRX/SPRX only, passed to create-fself -libname + # [LIBNAME ] # library types only, passed to create-fself -libname # ) # # Links as a PS4 ELF via the OpenOrbis toolchain, then runs # create-fself as a POST_BUILD step to produce the final artifact: - # EXE -> eboot.bin - # PRX -> .prx - # SPRX -> .sprx + # EXE -> eboot.bin + # PRX -> .prx + # SPRX -> .sprx + # GHPLUGIN -> .prx + # + # GHPLUGIN builds a GoldHEN plugin. It differs from PRX in three ways that + # all have to be right or the plugin builds cleanly and never loads: it + # links the GoldHEN SDK's crtprx.o instead of crtlib.o, enters at _init, + # and links SceLibcInternal/SceSysmodule rather than c/c++. Requires the + # GoldHEN Plugin SDK installed into the toolchain. Entry points are + # module_start/module_stop, which must have C linkage. # readoelf then runs over the packaged .oelf as a build-time sanity check. + # A plain, unsigned .elf (copy of that same validated .oelf) is + # produced alongside it -- raw-ELF loader tools inspect a file's magic + # bytes rather than its extension, so they can't load eboot.bin/*.prx/ + # *.sprx (which are SELF-wrapped, not plain ELF) no matter what it's + # renamed to; point those tools at the .elf instead. function(add_orbis_target name) set(options "") @@ -29,11 +43,6 @@ internal static class OrbisTargetsTemplate if(NOT ORBIS_SOURCES) message(FATAL_ERROR "add_orbis_target(${name}): SOURCES is required") - # A plain, unsigned .elf (copy of that same validated .oelf) is - # produced alongside it -- raw-ELF loader tools inspect a file's magic - # bytes rather than its extension, so they can't load eboot.bin/*.prx/ - # *.sprx (which are SELF-wrapped, not plain ELF) no matter what it's - # renamed to; point those tools at the .elf instead. endif() if(NOT ORBIS_TYPE) @@ -41,12 +50,11 @@ internal static class OrbisTargetsTemplate endif() string(TOUPPER "${ORBIS_TYPE}" ORBIS_TYPE) - if(NOT ORBIS_LIBS) - set(ORBIS_LIBS c kernel c++) - endif() - add_executable(${name} ${ORBIS_SOURCES}) + set(_default_libs c kernel c++) + set(_entry_args "") + if(ORBIS_TYPE STREQUAL "EXE") set(_crt_object "${OO_PS4_TOOLCHAIN}/lib/crt1.o") set(_default_ptype "fake") @@ -62,8 +70,35 @@ internal static class OrbisTargetsTemplate set(_default_ptype "system_dynlib") set(_out_name "${name}.sprx") set(_pkg_flag "--lib") + elseif(ORBIS_TYPE STREQUAL "GHPLUGIN") + # GoldHEN enters a plugin at _init, which the SDK's crtprx.o forwards + # to module_start/module_stop. Built with the stock crtlib.o instead, + # a plugin links without complaint and then never loads on console -- + # so the SDK is checked for here rather than failing silently later. + set(_crt_object "${OO_PS4_TOOLCHAIN}/lib/crtprx.o") + if(NOT EXISTS "${_crt_object}") + message(FATAL_ERROR + "add_orbis_target(${name}): TYPE GHPLUGIN needs the GoldHEN Plugin SDK " + "installed into the toolchain, but ${_crt_object} is missing. " + "Build the SDK (build_static.bat on Windows, make on Linux), then copy " + "crtprx.o and libGoldHEN_Hook.a into ${OO_PS4_TOOLCHAIN}/lib and the SDK's " + "include/* into ${OO_PS4_TOOLCHAIN}/include/GoldHEN.") + endif() + set(_default_ptype "fake") + set(_out_name "${name}.prx") + set(_pkg_flag "--lib") + set(_default_libs SceLibcInternal kernel SceSysmodule GoldHEN_Hook) + set(_entry_args "LINKER:-e,_init") else() - message(FATAL_ERROR "add_orbis_target(${name}): TYPE must be EXE, PRX, or SPRX (got '${ORBIS_TYPE}')") + message(FATAL_ERROR "add_orbis_target(${name}): TYPE must be EXE, PRX, SPRX, or GHPLUGIN (got '${ORBIS_TYPE}')") + endif() + + if(NOT ORBIS_LIBS) + set(ORBIS_LIBS ${_default_libs}) + endif() + + if(_entry_args) + target_link_options(${name} PRIVATE "${_entry_args}") endif() # Order matters here and mirrors the toolchain's own sample Makefiles: @@ -88,6 +123,7 @@ internal static class OrbisTargetsTemplate endif() set(_out_path "${CMAKE_CURRENT_BINARY_DIR}/${_out_name}") + set(_elf_path "${CMAKE_CURRENT_BINARY_DIR}/${name}.elf") add_custom_command(TARGET ${name} POST_BUILD COMMAND "${OO_PS4_TOOLCHAIN_BIN}/create-fself" @@ -108,6 +144,7 @@ internal static class OrbisTargetsTemplate ORBIS_OUTPUT_PATH "${_out_path}" ORBIS_OUTPUT_NAME "${_out_name}" ORBIS_TYPE "${ORBIS_TYPE}" + ORBIS_ELF_PATH "${_elf_path}" ) endfunction() @@ -124,7 +161,6 @@ internal static class OrbisTargetsTemplate set(oneValueArgs TITLE_ID CONTENT_ID TITLE VERSION ASSETS_DIR SCE_MODULE_DIR) cmake_parse_arguments(PKG "" "${oneValueArgs}" "" ${ARGN}) - set(_elf_path "${CMAKE_CURRENT_BINARY_DIR}/${name}.elf") if(NOT PKG_TITLE_ID OR NOT PKG_CONTENT_ID OR NOT PKG_TITLE) message(FATAL_ERROR "add_orbis_package(${target}): TITLE_ID, CONTENT_ID, and TITLE are required") endif() @@ -144,7 +180,6 @@ internal static class OrbisTargetsTemplate endif() set(_sce_sys "${CMAKE_CURRENT_SOURCE_DIR}/sce_sys") - ORBIS_ELF_PATH "${_elf_path}" # create-gp4's --files list has no separate source-vs-target-path syntax: # each entry is a path relative to the working directory it runs from, diff --git a/src/OrbisForge.Core/Templates/SourceTemplate.cs b/src/OrbisForge.Core/Templates/SourceTemplate.cs index 6ab4909..7ff151c 100644 --- a/src/OrbisForge.Core/Templates/SourceTemplate.cs +++ b/src/OrbisForge.Core/Templates/SourceTemplate.cs @@ -41,4 +41,66 @@ internal static class SourceTemplate int {{projectName}}_hello(char *buf, size_t buf_size, int num); """; + + public static string PluginCpp(string projectName) => $$""" + #include + #include + #include + + #include + + #define PLUGIN_NAME "{{projectName}}" + #define PLUGIN_VERSION "1.00" + + // GoldHEN reads these to list the plugin. + #define attr_public __attribute__((visibility("default"))) + + attr_public const char *g_pluginName = PLUGIN_NAME; + attr_public const char *g_pluginDesc = "Generated by OrbisForge"; + attr_public const char *g_pluginAuth = "unknown"; + attr_public const char *g_pluginVersion = PLUGIN_VERSION; + + // This is compiled as C++, so the entry points need C linkage: crtprx.o's + // _init resolves module_start/module_stop by their unmangled names. + extern "C" { + + static void notify(const char *msg) + { + OrbisNotificationRequest req; + + memset(&req, 0, sizeof(req)); + + req.type = NotificationRequest; + req.targetId = -1; + req.useIconImageUri = 1; + + strncpy(req.iconUri, "cxml://psnotification/tex_icon_system", sizeof(req.iconUri) - 1); + strncpy(req.message, msg, sizeof(req.message) - 1); + + // Non-blocking: module_start must not stall the title's startup. + sceKernelSendNotificationRequest(0, &req, sizeof(req), 0); + } + + int module_start(size_t argc, const void *argp) + { + (void)argc; + (void)argp; + + notify(PLUGIN_NAME " v" PLUGIN_VERSION " loaded"); + + return 0; + } + + int module_stop(size_t argc, const void *argp) + { + (void)argc; + (void)argp; + + notify(PLUGIN_NAME " unloaded"); + + return 0; + } + + } + """; } diff --git a/src/OrbisForge.Gui/WizardForm.cs b/src/OrbisForge.Gui/WizardForm.cs index f99c460..88c5584 100644 --- a/src/OrbisForge.Gui/WizardForm.cs +++ b/src/OrbisForge.Gui/WizardForm.cs @@ -71,6 +71,7 @@ namespace OrbisForge.Gui _typeBox.Items.Add("EXE (eboot.bin, homebrew app)"); _typeBox.Items.Add("PRX (user-mode library)"); _typeBox.Items.Add("SPRX (signed system library)"); + _typeBox.Items.Add("GHPLUGIN (GoldHEN plugin)"); _typeBox.SelectedIndex = 0; _typeBox.Location = new Point(left, y); _typeBox.Size = new Size(fieldWidth, 24); @@ -208,6 +209,7 @@ namespace OrbisForge.Gui { case 1: return OrbisTargetType.Prx; case 2: return OrbisTargetType.Sprx; + case 3: return OrbisTargetType.GoldHenPlugin; default: return OrbisTargetType.Exe; } } diff --git a/tests/OrbisForge.Core.Tests/OrbisTargetTypeTests.cs b/tests/OrbisForge.Core.Tests/OrbisTargetTypeTests.cs index 8ce4e6b..a048548 100644 --- a/tests/OrbisForge.Core.Tests/OrbisTargetTypeTests.cs +++ b/tests/OrbisForge.Core.Tests/OrbisTargetTypeTests.cs @@ -9,6 +9,7 @@ public class OrbisTargetTypeTests [InlineData(OrbisTargetType.Exe, "EXE")] [InlineData(OrbisTargetType.Prx, "PRX")] [InlineData(OrbisTargetType.Sprx, "SPRX")] + [InlineData(OrbisTargetType.GoldHenPlugin, "GHPLUGIN")] public void ToCMakeString_ReturnsExpectedToken(OrbisTargetType type, string expected) { Assert.Equal(expected, type.ToCMakeString()); @@ -18,17 +19,33 @@ public class OrbisTargetTypeTests [InlineData(OrbisTargetType.Exe, false)] [InlineData(OrbisTargetType.Prx, true)] [InlineData(OrbisTargetType.Sprx, true)] + [InlineData(OrbisTargetType.GoldHenPlugin, false)] public void IsLibrary_OnlyTrueForPrxAndSprx(OrbisTargetType type, bool expected) { Assert.Equal(expected, type.IsLibrary()); } + // A plugin isn't a "library" in the lib.cpp + public-header sense, but it still + // has no main(), so it shares the src/.cpp layout. + [Theory] + [InlineData(OrbisTargetType.Exe, false)] + [InlineData(OrbisTargetType.Prx, true)] + [InlineData(OrbisTargetType.Sprx, true)] + [InlineData(OrbisTargetType.GoldHenPlugin, true)] + public void UsesNamedSourceFile_TrueForEverythingWithoutMain(OrbisTargetType type, bool expected) + { + Assert.Equal(expected, type.UsesNamedSourceFile()); + } + [Theory] [InlineData("exe", OrbisTargetType.Exe)] [InlineData("EXE", OrbisTargetType.Exe)] [InlineData("bin", OrbisTargetType.Exe)] [InlineData(" prx ", OrbisTargetType.Prx)] [InlineData("SPRX", OrbisTargetType.Sprx)] + [InlineData("ghplugin", OrbisTargetType.GoldHenPlugin)] + [InlineData("GoldHEN", OrbisTargetType.GoldHenPlugin)] + [InlineData(" plugin ", OrbisTargetType.GoldHenPlugin)] public void TryParse_AcceptsKnownAliasesCaseInsensitively(string text, OrbisTargetType expected) { Assert.True(OrbisTargetTypeExtensions.TryParse(text, out var type)); @@ -53,6 +70,7 @@ public class OrbisTargetTypeTests [Theory] [InlineData(OrbisTargetType.Prx, "my_lib", "build/my_lib.prx")] [InlineData(OrbisTargetType.Sprx, "my_plugin", "build/my_plugin.sprx")] + [InlineData(OrbisTargetType.GoldHenPlugin, "notify", "build/notify.prx")] public void ArtifactRelativePath_Libraries_UseNameInFilename(OrbisTargetType type, string name, string expected) { Assert.Equal(expected, type.ArtifactRelativePath(name)); diff --git a/tests/OrbisForge.Core.Tests/ProjectGeneratorTests.cs b/tests/OrbisForge.Core.Tests/ProjectGeneratorTests.cs index 5d03940..252a908 100644 --- a/tests/OrbisForge.Core.Tests/ProjectGeneratorTests.cs +++ b/tests/OrbisForge.Core.Tests/ProjectGeneratorTests.cs @@ -67,10 +67,36 @@ public class ProjectGeneratorTests : IDisposable AssertFileMissing(result, "src/main.cpp"); } + [Fact] + public void Generate_GoldHenPlugin_WritesPluginSourceWithoutHeaderOrMain() + { + var result = ProjectGenerator.Generate(new ProjectOptions + { + Name = "notify_plugin", + Type = OrbisTargetType.GoldHenPlugin, + OutputDirectory = _outputDir, + }); + + AssertFileExists(result, "src/notify_plugin.cpp"); + AssertFileMissing(result, "src/notify_plugin.h"); + AssertFileMissing(result, "src/main.cpp"); + + var source = ReadFile(result, "src/notify_plugin.cpp"); + + // The loader reaches these through crtprx.o's _init, which resolves them by + // their unmangled names -- without extern "C" the C++ compiler would mangle + // them and the plugin would link but never load. + Assert.Contains("extern \"C\"", source); + Assert.Contains("int module_start(", source); + Assert.Contains("int module_stop(", source); + Assert.Contains("sceKernelSendNotificationRequest", source); + } + [Theory] [InlineData(OrbisTargetType.Exe, "TYPE EXE")] [InlineData(OrbisTargetType.Prx, "TYPE PRX")] [InlineData(OrbisTargetType.Sprx, "TYPE SPRX")] + [InlineData(OrbisTargetType.GoldHenPlugin, "TYPE GHPLUGIN")] public void Generate_CMakeLists_DeclaresCorrectType(OrbisTargetType type, string expectedSnippet) { var result = ProjectGenerator.Generate(new ProjectOptions