From fcabb528cfa4a35386dc0518595a6fa13bfad638 Mon Sep 17 00:00:00 2001 From: Julian Benda Date: Wed, 10 Jun 2026 17:52:31 +0200 Subject: [PATCH 1/5] feat(UE): Do not ship inklecate.exe when only bundling sources for UE FAB now forbids bundling executables inside a plugin, so we need an alternataive --- unreal/CMakeLists.txt | 69 +++- .../inkcpp_editor/Private/InkAssetFactory.cpp | 365 +++++++++++++++--- .../Private/InkCppEditorSettings.cpp | 31 ++ .../inkcpp_editor/Private/inklecate_cmd.cpp | 38 ++ .../Private/inklecate_cmd.cpp.in | 87 ++++- .../Public/InkCppEditorSettings.h | 59 +++ .../inkcpp_editor/inkcpp_editor.Build.cs | 30 +- 7 files changed, 576 insertions(+), 103 deletions(-) create mode 100644 unreal/inkcpp/Source/inkcpp_editor/Private/InkCppEditorSettings.cpp create mode 100644 unreal/inkcpp/Source/inkcpp_editor/Private/inklecate_cmd.cpp create mode 100644 unreal/inkcpp/Source/inkcpp_editor/Public/InkCppEditorSettings.h diff --git a/unreal/CMakeLists.txt b/unreal/CMakeLists.txt index 6b304697..8cfc1cfd 100644 --- a/unreal/CMakeLists.txt +++ b/unreal/CMakeLists.txt @@ -15,36 +15,52 @@ if(INKCPP_UNREAL) include(FetchContent) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/inkcpp/inkcpp.uplugin.in" "${CMAKE_CURRENT_BINARY_DIR}/inkcpp/inkcpp.uplugin") - # download inklecate for unreal plugin + + # Try to download inklecate for all platforms. The FetchContent_Declare calls (with URLs and + # hashes) live in the root CMakeLists.txt. For Fab store distributions the bundled binaries are + # stripped; users configure the path themselves via Project Settings > Plugins > InkCPP. FetchContent_MakeAvailable(inklecate_mac inklecate_windows inklecate_linux) set(FETCHCONTENT_QUIET OFF) set(CMAKE_TLS_VERIFY true) - if(NOT inklecate_windows_SOURCE_DIR) - message(WARNING "failed to download inklecate for windows, " - "the unreal plugin will be unable use a .ink file as asset directly") - else() + + # Default all platform paths to empty (= "not bundled"). They are filled in only when the download + # succeeded. + set(INKLECATE_CMD_WIN "") + set(INKLECATE_CMD_MAC "") + set(INKLECATE_CMD_LINUX "") + set(INKCPP_INKLECATE_BUNDLED FALSE) + + if(inklecate_windows_SOURCE_DIR) set(INKLECATE_CMD_WIN "Source/ThirdParty/inklecate/windows/inklecate.exe") file(COPY "${CMAKE_BINARY_DIR}/inklecate/windows" - DESTINATION "inkcpp/Source/ThirdParty/inklecate/") - endif() - if(NOT inklecate_mac_SOURCE_DIR) - message(WARNING "failed to download inklecate for MacOS, " - "the unreal plugin will be unable use a .ink file as asset directly") + DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/inkcpp/Source/ThirdParty/inklecate/") + set(INKCPP_INKLECATE_BUNDLED TRUE) else() - set(INKLECATE_CMD_MAC "Source/ThirdParty/inklecate/mac/inklecate") - file(COPY "${CMAKE_BINARY_DIR}/inklecate/mac" DESTINATION "inkcpp/Source/ThirdParty/inklecate/") + message(WARNING "InkCPP: failed to download inklecate for Windows. " + "A .ink file can still be imported if the user configures the inklecate path " + "in Project Settings > Plugins > InkCPP.") endif() - if(NOT inklecate_linux_SOURCE_DIR) - message(WARNING "failed to download inklecate for linux, " - "the unreal plugin will be unable use a .ink file as asset directly") + if(inklecate_mac_SOURCE_DIR) + set(INKLECATE_CMD_MAC "Source/ThirdParty/inklecate/mac/inklecate") + file(COPY "${CMAKE_BINARY_DIR}/inklecate/mac" + DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/inkcpp/Source/ThirdParty/inklecate/") + set(INKCPP_INKLECATE_BUNDLED TRUE) else() + message(WARNING "InkCPP: failed to download inklecate for macOS. " + "A .ink file can still be imported if the user configures the inklecate path " + "in Project Settings > Plugins > InkCPP.") + endif() + if(inklecate_linux_SOURCE_DIR) set(INKLECATE_CMD_LINUX "Source/ThirdParty/inklecate/linux/inklecate") file(COPY "${CMAKE_BINARY_DIR}/inklecate/linux" - DESTINATION "inkcpp/Source/ThirdParty/inklecate/") + DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/inkcpp/Source/ThirdParty/inklecate/") + set(INKCPP_INKLECATE_BUNDLED TRUE) + else() + message(WARNING "InkCPP: failed to download inklecate for Linux. " + "A .ink file can still be imported if the user configures the inklecate path " + "in Project Settings > Plugins > InkCPP.") endif() - configure_file( - "${CMAKE_CURRENT_SOURCE_DIR}/inkcpp/Source/inkcpp_editor/Private/inklecate_cmd.cpp.in" - "${CMAKE_CURRENT_BINARY_DIR}/inkcpp/Source/inkcpp_editor/Private/inklecate_cmd.cpp") + file( GLOB_RECURSE SOURCE_FILES LIST_DIRECTORIES TRUE @@ -57,6 +73,21 @@ if(INKCPP_UNREAL) "${CMAKE_CURRENT_BINARY_DIR}/${src_file}" COPYONLY) endif() endforeach() + + # When at least one inklecate binary was downloaded, regenerate inklecate_cmd.cpp from the .in + # template so the bundled paths are compiled in. The runtime code will use those paths as a + # default and also auto-fill Project Settings on first use. When no binary was downloaded + # (Fab/offline build) the COPYONLY loop above has already copied the static inklecate_cmd.cpp + # (which reads only from Project Settings), so nothing extra is needed. + if(INKCPP_INKLECATE_BUNDLED) + configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/inkcpp/Source/inkcpp_editor/Private/inklecate_cmd.cpp.in" + "${CMAKE_CURRENT_BINARY_DIR}/inkcpp/Source/inkcpp_editor/Private/inklecate_cmd.cpp") + message(STATUS "InkCPP: inklecate bundled — generated inklecate_cmd.cpp with embedded paths.") + else() + message(STATUS "InkCPP: no inklecate downloaded — using settings-only inklecate_cmd.cpp.") + endif() + install( DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/inkcpp/" DESTINATION "inkcpp" diff --git a/unreal/inkcpp/Source/inkcpp_editor/Private/InkAssetFactory.cpp b/unreal/inkcpp/Source/inkcpp_editor/Private/InkAssetFactory.cpp index 99479728..64230239 100644 --- a/unreal/inkcpp/Source/inkcpp_editor/Private/InkAssetFactory.cpp +++ b/unreal/inkcpp/Source/inkcpp_editor/Private/InkAssetFactory.cpp @@ -9,12 +9,23 @@ #include "EditorFramework/AssetImportData.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" -#include "Interfaces/IPluginManager.h" #include "Internationalization/Regex.h" +#include "Framework/Application/SlateApplication.h" +#include "Widgets/SBoxPanel.h" +#include "Widgets/Text/STextBlock.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Input/SHyperlink.h" +#include "Widgets/Layout/SBorder.h" +#include "Widgets/SWindow.h" +#include "ISettingsModule.h" +#include "Modules/ModuleManager.h" +#include "HAL/PlatformProcess.h" #include "InkAsset.h" +#include "InkCppEditorSettings.h" #include "ink/compiler.h" #include "inklecate_cmd.cpp" +#include "version.h" #include #include @@ -23,30 +34,228 @@ #include #include -UInkAssetFactory::UInkAssetFactory(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) - , object_ptr(this) +// ------------------------------------------------------------------------- +// Platform detection helpers +// ------------------------------------------------------------------------- + +namespace { - // Add ink format - Formats.Add( - FString(TEXT("json;")) - + NSLOCTEXT("UInkAssetFactory", "FormatInkJSON", "Ink JSON File").ToString() - ); - Formats.Add( - FString(TEXT("ink;")) + NSLOCTEXT("UInkAssetFactory", "FormatInk", "Ink File").ToString() + +/** Returns the platform name used by UE (Windows / Mac / Linux). */ +FString GetCurrentPlatform() +{ +#if PLATFORM_WINDOWS + return TEXT("Windows"); +#elif PLATFORM_MAC + return TEXT("Mac"); +#else + return TEXT("Linux"); +#endif +} + +/** + * Returns the inklecate download URL for the host platform. + * The tag is derived from the InkVersion constant so this stays in sync + * automatically when version.h is updated. + */ +FString GetInklecateDownloadUrl() +{ + // Map the ink JSON version to the nearest known inklecate release tag. + // inklecate releases use a "v.." scheme that does NOT + // directly mirror the ink JSON version number; update this string whenever + // a new compatible inklecate release is published. + const FString tag = TEXT("v1.1.1"); + + const FString platform = GetCurrentPlatform(); + if (platform == TEXT("Windows")) { + return FString::Printf( + TEXT("https://github.com/inkle/ink/releases/download/%s/inklecate_windows.zip"), *tag + ); + } else if (platform == TEXT("Mac")) { + return FString::Printf( + TEXT("https://github.com/inkle/ink/releases/download/%s/inklecate_mac.zip"), *tag + ); + } else { + return FString::Printf( + TEXT("https://github.com/inkle/ink/releases/download/%s/inklecate_linux.zip"), *tag + ); + } +} + +/** Returns the recommended path to place inklecate inside the project Content folder. */ +FString GetRecommendedInklecatePath() +{ + const FString platform = GetCurrentPlatform(); + const FString ext = (platform == TEXT("Windows")) ? TEXT(".exe") : TEXT(""); + // FPaths::ProjectContentDir() already ends with a separator + return FPaths::ProjectContentDir() / TEXT("inklecate") / (TEXT("inklecate") + ext); +} + +// ------------------------------------------------------------------------- +// Tutorial / setup dialog +// ------------------------------------------------------------------------- + +/** Opens a modal dialog explaining how to install inklecate and link it in settings. */ +void ShowInklecateSetupDialog() +{ + const FString downloadUrl = GetInklecateDownloadUrl(); + const FString recommendedPath = GetRecommendedInklecatePath(); + const FString platform = GetCurrentPlatform(); + const FString zipName = FPaths::GetCleanFilename(downloadUrl); + const FString binaryName + = (platform == TEXT("Windows")) ? TEXT("inklecate.exe") : TEXT("inklecate"); + const FString contentSubFolder = TEXT("Content/inklecate/"); + + const FText titleText = NSLOCTEXT("InkCPP", "SetupTitle", "InkCPP: Inklecate Setup Required"); + + const FText bodyText = FText::Format( + NSLOCTEXT( + "InkCPP", "SetupBody", + "To import .ink story files directly, InkCPP needs the inklecate compiler.\n\n" + "Quick setup steps:\n" + " 1. Download {0} for {1} using the link below.\n" + " 2. Unzip the archive into your project's Content folder:\n" + " {2}\n" + " 3. Open Project Settings > Plugins > InkCPP and set\n" + " \"Inklecate Executable Path\" to:\n" + " {3}\n\n" + "Tip: import a .ink.json file (exported from Inky) to skip inklecate entirely.\n\n" + "This build of InkCPP expects ink format version {4}." + ), + FText::FromString(zipName), FText::FromString(platform), + FText::FromString(contentSubFolder + binaryName), FText::FromString(recommendedPath), + FText::AsNumber(( int64 ) ink::InkVersion) ); - // Set class - SupportedClass = UInkAsset::StaticClass(); - bCreateNew = false; - bAutomatedReimport = true; - bForceShowDialog = true; - bEditorImport = true; + TSharedPtr DialogPtr; + TSharedRef Dialog = SNew(SWindow) + .Title(titleText) + .SizingRule(ESizingRule::Autosized) + .IsTopmostWindow(true) + .SupportsMaximize(false) + .SupportsMinimize(false); + DialogPtr = Dialog; - ImportPriority = 20; + TSharedRef Content = SNew(SBorder).Padding(FMargin(16.f) + )[SNew(SVerticalBox) + + SVerticalBox::Slot().AutoHeight().Padding( + 0.f, 0.f, 0.f, 12.f + )[SNew(STextBlock).Text(bodyText).AutoWrapText(true)] + + SVerticalBox::Slot().AutoHeight().Padding( + 0.f, 0.f, 0.f, 12.f + )[SNew(SHyperlink) + .Text(FText::Format( + NSLOCTEXT("InkCPP", "DownloadLinkLabel", "Download: {0}"), + FText::FromString(downloadUrl) + )) + .OnNavigate_Lambda([downloadUrl]() { + FPlatformProcess::LaunchURL(*downloadUrl, nullptr, nullptr); + })] + + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Left + )[SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().Padding( + 0.f, 0.f, 8.f, 0.f + )[SNew(SButton) + .Text(NSLOCTEXT("InkCPP", "OpenSettingsBtn", "Open Project Settings")) + .OnClicked_Lambda([DialogPtr]() { + if (ISettingsModule* SettingsModule + = FModuleManager::GetModulePtr("Settings")) { + SettingsModule->ShowViewer(TEXT("Project"), TEXT("Plugins"), TEXT("InkCPP")); + } + if (DialogPtr.IsValid()) { + DialogPtr->RequestDestroyWindow(); + } + return FReply::Handled(); + })] + + SHorizontalBox::Slot().AutoWidth()[SNew(SButton) + .Text(NSLOCTEXT("InkCPP", "CloseBtn", "Close")) + .OnClicked_Lambda([DialogPtr]() { + if (DialogPtr.IsValid()) { + DialogPtr->RequestDestroyWindow(); + } + return FReply::Handled(); + })]]]; + + Dialog->SetContent(Content); + FSlateApplication::Get().AddModalWindow(Dialog, nullptr, false); } -UInkAssetFactory::~UInkAssetFactory() { FReimportManager::Instance()->UnregisterHandler(*this); } +// ------------------------------------------------------------------------- +// Inklecate version check +// ------------------------------------------------------------------------- + +/** + * Runs "inklecate --version", parses the ink JSON version from its output, and + * logs a warning if it does not match ink::InkVersion. + * This check is non-fatal — a version mismatch may still compile correctly. + */ +void CheckInklecateVersion(const std::string& inklecatePath) +{ + const std::string versionCmd = "\"" + inklecatePath + "\" --version 2>&1"; + + FILE* pipe = +#if PLATFORM_WINDOWS + _popen(versionCmd.c_str(), "r"); +#else + popen(versionCmd.c_str(), "r"); +#endif + if (! pipe) { + UE_LOG( + InkCpp, Warning, TEXT("InkCPP: Could not run inklecate --version (version check skipped).") + ); + return; + } + + std::string output; + char buf[256]; + while (fgets(buf, sizeof(buf), pipe)) { + output += buf; + } +#if PLATFORM_WINDOWS + _pclose(pipe); +#else + pclose(pipe); +#endif + + // inklecate prints something like "ink version: 21" or "inkVersion=21" + FString outputFStr(output.c_str()); + FRegexMatcher matcher( + FRegexPattern( + TEXT("(?:ink[Vv]ersion\\s*[=:]\\s*|ink\\s+version\\s*:?\\s*)(\\d+)"), + ERegexPatternFlags{0} + ), + outputFStr + ); + + if (matcher.FindNext()) { + int32 reported = FCString::Atoi(*matcher.GetCaptureGroup(1)); + if (static_cast(reported) != ink::InkVersion) { + UE_LOG( + InkCpp, Warning, + TEXT("InkCPP: inklecate reports ink version %d, but this build of InkCPP expects " + "version %u. Compilation may succeed but runtime behaviour could differ. " + "Download a matching inklecate from https://github.com/inkle/ink/releases"), + reported, ink::InkVersion + ); + } else { + UE_LOG( + InkCpp, Display, TEXT("InkCPP: inklecate version check passed (ink version %d)."), + reported + ); + } + } else { + UE_LOG( + InkCpp, Warning, TEXT("InkCPP: Could not parse ink version from inklecate output: %s"), + *outputFStr + ); + } +} + +} // anonymous namespace + +// ------------------------------------------------------------------------- +// Include traversal +// ------------------------------------------------------------------------- /// @todo only finds first include match? void TraversImports( @@ -60,7 +269,7 @@ void TraversImports( if (visited.find(filepath) != visited.end()) { return; } - int id = visited.size(); + int id = ( int ) visited.size(); visited.insert(filepath); AssetImportData.AddFileName( FString(filepath.string().c_str()), id, id == 0 ? TEXT("MainFile") : TEXT("Include") @@ -86,61 +295,114 @@ void TraversImports( } } +// ------------------------------------------------------------------------- +// Factory implementation +// ------------------------------------------------------------------------- + +UInkAssetFactory::UInkAssetFactory(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , object_ptr(this) +{ + // Add ink format + Formats.Add( + FString(TEXT("json;")) + + NSLOCTEXT("UInkAssetFactory", "FormatInkJSON", "Ink JSON File").ToString() + ); + Formats.Add( + FString(TEXT("ink;")) + NSLOCTEXT("UInkAssetFactory", "FormatInk", "Ink File").ToString() + ); + + // Set class + SupportedClass = UInkAsset::StaticClass(); + bCreateNew = false; + bAutomatedReimport = true; + bForceShowDialog = true; + bEditorImport = true; + + ImportPriority = 20; +} + +UInkAssetFactory::~UInkAssetFactory() { FReimportManager::Instance()->UnregisterHandler(*this); } + UObject* UInkAssetFactory::FactoryCreateFile( UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, const FString& Filename, const TCHAR* Parms, FFeedbackContext* Warn, bool& bOutOperationCanceled ) { std::stringstream output; - std::stringstream cmd{}; - const std::string inklecate_cmd = get_inklecate_cmd(); static const std::string ink_suffix{".ink"}; + try { using path = std::filesystem::path; std::string cFilename = TCHAR_TO_UTF8(*Filename); path story_path(cFilename, path::format::generic_format); story_path.make_preferred(); bool use_temp_file = false; + if (cFilename.size() > ink_suffix.size() && std::equal(ink_suffix.rbegin(), ink_suffix.rend(), cFilename.rbegin())) { - use_temp_file = true; - if (inklecate_cmd.size() == 0) { + // ---- .ink file: needs inklecate ---- + const std::string inklecate_cmd = get_inklecate_cmd(); + + if (inklecate_cmd.empty()) { + // No path configured → show setup tutorial and abort UE_LOG( InkCpp, Warning, - TEXT("Inklecate provided with the plugin, please import ink.json files") + TEXT("InkCPP: No inklecate path configured. " + "Set it in Project Settings > Plugins > InkCPP, " + "or import a .ink.json file directly.") ); + ShowInklecateSetupDialog(); + bOutOperationCanceled = true; return nullptr; } - path path_bin( - TCHAR_TO_UTF8(*IPluginManager::Get().FindPlugin(TEXT("InkCPP"))->GetBaseDir()), - path::format::generic_format - ); - path_bin.make_preferred(); - path_bin /= path(inklecate_cmd, path::format::generic_format).make_preferred(); - char filename[L_tmpnam]; - if (tmpnam_s(filename, sizeof(filename)) != 0) { - UE_LOG(InkCpp, Error, TEXT("Failed to create temporary file")); + + // Verify the binary actually exists on disk + if (! std::filesystem::exists(inklecate_cmd)) { + UE_LOG( + InkCpp, Warning, + TEXT("InkCPP: inklecate not found at '%s'. " + "Update the path in Project Settings > Plugins > InkCPP."), + *FString(inklecate_cmd.c_str()) + ); + ShowInklecateSetupDialog(); + bOutOperationCanceled = true; return nullptr; } - cFilename = filename; + + // Non-fatal version compatibility check + CheckInklecateVersion(inklecate_cmd); + + // Build the inklecate invocation + use_temp_file = true; + char tmp_filename[L_tmpnam]; + if (tmpnam_s(tmp_filename, sizeof(tmp_filename)) != 0) { + UE_LOG(InkCpp, Error, TEXT("InkCPP: Failed to create a temporary file name.")); + return nullptr; + } + cFilename = tmp_filename; path json_path(cFilename, path::format::generic_format); json_path.make_preferred(); - cmd - // if std::system start with a quote, the pair of qoute is removed, which leads to errors - // with pathes with spaces but if the quote is not the first symbol it works fine (a"b" is - // glued to ab from bash) - << path_bin.string()[0] << "\"" << (path_bin.string().c_str() + 1) << "\"" - << " -o \"" << json_path.string() << "\" " << '"' << story_path.string() << "\" 2>&1"; - auto cmd_str = cmd.str(); - int result = std::system(cmd_str.c_str()); + + std::stringstream cmd{}; + // Note: on Windows, if std::system()'s argument begins with '"', the outer + // pair of quotes is stripped, breaking paths with spaces. Emitting the first + // character outside the quoted section works around this. + cmd << inklecate_cmd[0] << "\"" << (inklecate_cmd.c_str() + 1) << "\"" + << " -o \"" << json_path.string() << "\"" + << " \"" << story_path.string() << "\" 2>&1"; + const std::string cmd_str = cmd.str(); + int result = std::system(cmd_str.c_str()); if (result != 0) { UE_LOG( - InkCpp, Warning, TEXT("Inklecate failed with exit code %i, executed was: '%s'"), result, + InkCpp, Warning, TEXT("InkCPP: inklecate failed (exit code %i). Command: '%s'"), result, *FString(cmd_str.c_str()) ); return nullptr; } } + + // ---- JSON → binary compilation (in-process) ---- ink::compiler::run(cFilename.c_str(), output); if (use_temp_file) { std::filesystem::remove(cFilename); @@ -152,22 +414,18 @@ UObject* UInkAssetFactory::FactoryCreateFile( asset->AssetImportData = NewObject(asset, UAssetImportData::StaticClass()); } - // Load it up + // Load compiled binary into the asset std::string data = output.str(); asset->CompiledStory.SetNum(data.length()); FMemory::Memcpy(asset->CompiledStory.GetData(), data.c_str(), data.length()); - // Paths + // Record source file paths for reimport std::unordered_set visited{}; TraversImports(*asset->AssetImportData, visited, story_path); - // Not cancelled bOutOperationCanceled = false; - - // Return return asset; } catch (...) { - // some kind of error? return nullptr; } @@ -182,7 +440,6 @@ bool UInkAssetFactory::FactoryCanImport(const FString& Filename) bool UInkAssetFactory::CanReimport(UObject* Obj, TArray& OutFilenames) { - // Can reimport story assets UInkAsset* InkAsset = Cast(Obj); if (InkAsset != nullptr && InkAsset->AssetImportData) { InkAsset->AssetImportData->ExtractFilenames(OutFilenames); @@ -196,7 +453,7 @@ void UInkAssetFactory::SetReimportPaths(UObject* Obj, const TArray& New UE_LOG(InkCpp, Display, TEXT("SetReimportPaths")); UInkAsset* obj = Cast(Obj); if (obj && NewReimportPaths.Num() > 0) { - for (size_t i = 0; i < NewReimportPaths.Num(); ++i) { + for (int32 i = 0; i < NewReimportPaths.Num(); ++i) { obj->AssetImportData->UpdateFilenameOnly(NewReimportPaths[i], i); } } @@ -222,7 +479,6 @@ EReimportResult::Type UInkAssetFactory::Reimport(UObject* Obj) return EReimportResult::Failed; } - // Run the import again EReimportResult::Type Result = EReimportResult::Failed; bool OutCanceled = false; @@ -231,10 +487,9 @@ EReimportResult::Type UInkAssetFactory::Reimport(UObject* Obj) RF_Public | RF_Standalone, Filename, nullptr, OutCanceled ) != nullptr) { - /// TODO: add aditional pathes + /// @todo add additional paths InkAsset->AssetImportData->Update(Filename); - // Try to find the outer package so we can dirty it up if (InkAsset->GetOuter()) { InkAsset->GetOuter()->MarkPackageDirty(); } else { diff --git a/unreal/inkcpp/Source/inkcpp_editor/Private/InkCppEditorSettings.cpp b/unreal/inkcpp/Source/inkcpp_editor/Private/InkCppEditorSettings.cpp new file mode 100644 index 00000000..b89feac9 --- /dev/null +++ b/unreal/inkcpp/Source/inkcpp_editor/Private/InkCppEditorSettings.cpp @@ -0,0 +1,31 @@ +/* Copyright (c) 2024 Julian Benda + * + * This file is part of inkCPP which is released under MIT license. + * See file LICENSE.txt or go to + * https://github.com/JBenda/inkcpp for full license details. + */ +#include "InkCppEditorSettings.h" + +UInkCppEditorSettings::UInkCppEditorSettings() +{ + CategoryName = TEXT("Plugins"); + SectionName = TEXT("InkCPP"); +} + +FName UInkCppEditorSettings::GetCategoryName() const { return TEXT("Plugins"); } + +FText UInkCppEditorSettings::GetSectionText() const +{ + return NSLOCTEXT("InkCPP", "SettingsSectionText", "InkCPP"); +} + +FText UInkCppEditorSettings::GetSectionDescription() const +{ + return NSLOCTEXT( + "InkCPP", "SettingsSectionDescription", + "Settings for the InkCPP Unreal plugin. " + "Configure the path to the inklecate compiler used to import .ink story files." + ); +} + +FString UInkCppEditorSettings::GetInklecatePath() const { return InklatePath.FilePath; } diff --git a/unreal/inkcpp/Source/inkcpp_editor/Private/inklecate_cmd.cpp b/unreal/inkcpp/Source/inkcpp_editor/Private/inklecate_cmd.cpp new file mode 100644 index 00000000..952dd832 --- /dev/null +++ b/unreal/inkcpp/Source/inkcpp_editor/Private/inklecate_cmd.cpp @@ -0,0 +1,38 @@ +/* Copyright (c) 2024 Julian Benda + * + * This file is part of inkCPP which is released under MIT license. + * See file LICENSE.txt or go to + * https://github.com/JBenda/inkcpp for full license details. + */ +#pragma once + +#include "InkCppEditorSettings.h" + +#include "Misc/Paths.h" + +#include + +/** + * Returns the path to the inklecate executable as a UTF-8 string. + * + * Resolution order: + * 1. Value set in Project Settings > Plugins > InkCPP (InklatePath). + * 2. Empty string — caller interprets this as "inklecate not configured". + * + * A non-empty path does NOT guarantee the file exists; the caller is + * responsible for checking existence and showing the setup dialog if needed. + */ +inline std::string get_inklecate_cmd() +{ + const UInkCppEditorSettings* Settings = GetDefault(); + if (Settings) { + FString ConfiguredPath = Settings->GetInklecatePath().TrimStartAndEnd(); + if (! ConfiguredPath.IsEmpty()) { + // Convert any forward/backward slashes to the OS-preferred separator + FPaths::NormalizeFilename(ConfiguredPath); + return std::string(TCHAR_TO_UTF8(*ConfiguredPath)); + } + } + // No path configured – signal "not found" to the caller + return std::string{}; +} diff --git a/unreal/inkcpp/Source/inkcpp_editor/Private/inklecate_cmd.cpp.in b/unreal/inkcpp/Source/inkcpp_editor/Private/inklecate_cmd.cpp.in index 7a29118a..30f594f6 100644 --- a/unreal/inkcpp/Source/inkcpp_editor/Private/inklecate_cmd.cpp.in +++ b/unreal/inkcpp/Source/inkcpp_editor/Private/inklecate_cmd.cpp.in @@ -1,24 +1,79 @@ /* Copyright (c) 2024 Julian Benda * * This file is part of inkCPP which is released under MIT license. - * See file LICENSE.txt or go to - * https://github.com/JBenda/inkcpp for full license details. + * See file LICENSE.txt or go to + * https://github.com/JBenda/inkcpp for full license details. */ #pragma once -#include "Kismet/GameplayStatics.h" -#include "string" - -inline std::string get_inklecate_cmd() { - FString platform = UGameplayStatics::GetPlatformName(); - if (platform == TEXT("Windows")) { - return std::string("@INKLECATE_CMD_WIN@"); - } else if (platform == TEXT("Mac")) { - return std::string("@INKLECATE_CMD_MAC@"); - } else if (platform == TEXT("Linux")) { - return std::string("@INKLECATE_CMD_LINUX@"); - } else { - // UE_LOG(InkCpp, Warning, TEXT("Platform: '%s' is not know. For compiling a .ink file a system wide 'inklecate' executable wil be tried to use."), platform); - return std::string("inklecate"); +// This file is generated by CMake from inklecate_cmd.cpp.in when inklecate +// binaries are bundled with the local build. The static inklecate_cmd.cpp +// (checked in alongside this template) is used instead when no binaries are +// present (e.g. Fab store distributions). + +#include "InkCppEditorSettings.h" + +#include "Interfaces/IPluginManager.h" +#include "Misc/Paths.h" + +#include + +/** + * Returns the path to the inklecate executable as a UTF-8 std::string. + * + * Resolution order: + * 1. Value set in Project Settings > Plugins > InkCPP. + * 2. Bundled binary inside the plugin's ThirdParty folder (embedded at + * CMake configure time). When this path is used the setting is + * auto-filled so the user can see and override it. + * 3. Empty string — caller shows the setup dialog. + */ +inline std::string get_inklecate_cmd() +{ + // --- 1. User-configured path takes priority --- + if (const UInkCppEditorSettings* Settings = GetDefault()) + { + FString ConfiguredPath = Settings->GetInklecatePath().TrimStartAndEnd(); + if (!ConfiguredPath.IsEmpty()) + { + FPaths::NormalizeFilename(ConfiguredPath); + return std::string(TCHAR_TO_UTF8(*ConfiguredPath)); + } } + + // --- 2. Bundled binary (platform-specific, embedded by CMake) --- + // INKLECATE_CMD_WIN / _MAC / _LINUX are the relative paths inside the + // plugin directory set by unreal/CMakeLists.txt. + FString relPath; +#if PLATFORM_WINDOWS + relPath = TEXT("@INKLECATE_CMD_WIN@"); +#elif PLATFORM_MAC + relPath = TEXT("@INKLECATE_CMD_MAC@"); +#else + relPath = TEXT("@INKLECATE_CMD_LINUX@"); +#endif + + if (!relPath.IsEmpty()) + { + // Resolve relative to the plugin's installation directory + TSharedPtr Plugin = IPluginManager::Get().FindPlugin(TEXT("InkCPP")); + if (Plugin.IsValid()) + { + FString FullPath = Plugin->GetBaseDir() / relPath; + FPaths::NormalizeFilename(FullPath); + + // Auto-fill Project Settings so the user can see and override the path + if (UInkCppEditorSettings* MutableSettings = + GetMutableDefault()) + { + MutableSettings->InklatePath.FilePath = FullPath; + MutableSettings->SaveConfig(); + } + + return std::string(TCHAR_TO_UTF8(*FullPath)); + } + } + + // --- 3. Not found --- + return std::string{}; } diff --git a/unreal/inkcpp/Source/inkcpp_editor/Public/InkCppEditorSettings.h b/unreal/inkcpp/Source/inkcpp_editor/Public/InkCppEditorSettings.h new file mode 100644 index 00000000..1f1e07fd --- /dev/null +++ b/unreal/inkcpp/Source/inkcpp_editor/Public/InkCppEditorSettings.h @@ -0,0 +1,59 @@ +/* Copyright (c) 2024 Julian Benda + * + * This file is part of inkCPP which is released under MIT license. + * See file LICENSE.txt or go to + * https://github.com/JBenda/inkcpp for full license details. + */ +#pragma once + +#include "Engine/DeveloperSettings.h" +#include "InkCppEditorSettings.generated.h" + +/** + * Editor settings for the InkCPP plugin. + * + * Accessible via Edit > Project Settings > Plugins > InkCPP. + * + * @ingroup unreal + */ +UCLASS(config = EditorPerProjectUserSettings, defaultconfig, meta = (DisplayName = "InkCPP")) + +class INKCPP_EDITOR_API UInkCppEditorSettings : public UDeveloperSettings +{ + GENERATED_BODY() + +public: + UInkCppEditorSettings(); + + // ~Begin UDeveloperSettings + virtual FName GetCategoryName() const override; + virtual FText GetSectionText() const override; + virtual FText GetSectionDescription() const override; + // ~End UDeveloperSettings + + /** + * Full path to the inklecate executable used to compile .ink files. + * + * Leave empty to search the system PATH. If no executable is found when importing a .ink file + * a setup guide will be shown. + * + * Recommended location: place the platform-specific inklecate binary inside your project's + * Content folder so it travels with the project, e.g. + * Windows : Content/inklecate/inklecate.exe + * Mac/Linux: Content/inklecate/inklecate + * + * The correct version for this build of InkCPP can be downloaded from: + * https://github.com/inkle/ink/releases + */ + UPROPERTY( + config, EditAnywhere, Category = "Inklecate", + meta + = (DisplayName = "Inklecate Executable Path", + ToolTip = "Absolute path to the inklecate executable (leave empty to use system PATH).", + FilePathFilter = "exe,inklecate,", RelativeToGameDir = false) + ) + FFilePath InklatePath; + + /** Returns the configured path string, or an empty string if not set. */ + FString GetInklecatePath() const; +}; diff --git a/unreal/inkcpp/Source/inkcpp_editor/inkcpp_editor.Build.cs b/unreal/inkcpp/Source/inkcpp_editor/inkcpp_editor.Build.cs index 0b8e75d1..bc0c392f 100644 --- a/unreal/inkcpp/Source/inkcpp_editor/inkcpp_editor.Build.cs +++ b/unreal/inkcpp/Source/inkcpp_editor/inkcpp_editor.Build.cs @@ -11,14 +11,14 @@ public inkcpp_editor(ReadOnlyTargetRules Target) : base(Target) PCHUsage = ModuleRules.PCHUsageMode.NoSharedPCHs; PrivatePCHHeaderFile = "Public/inkcpp_editor.h"; CppStandard = CppStandardVersion.Cpp20; - + PublicIncludePaths.AddRange( new string[] { // ... add public include paths required here ... } ); - - + + PrivateIncludePaths.AddRange( new string[] { Path.Combine(ModuleDirectory, "../shared/Private"), @@ -26,8 +26,8 @@ public inkcpp_editor(ReadOnlyTargetRules Target) : base(Target) Path.Combine(ModuleDirectory, "../ThirdParty/Private"), } ); - - + + PublicDependencyModuleNames.AddRange( new string[] { @@ -36,8 +36,8 @@ public inkcpp_editor(ReadOnlyTargetRules Target) : base(Target) // ... add other public dependencies that you statically link with here ... } ); - - + + PrivateDependencyModuleNames.AddRange( new string[] { @@ -45,12 +45,16 @@ public inkcpp_editor(ReadOnlyTargetRules Target) : base(Target) "Engine", "Slate", "SlateCore", - "inkcpp", - "UnrealEd", - } - ); - - + "inkcpp", + "UnrealEd", + // DeveloperSettings provides UDeveloperSettings (Project Settings integration) + "DeveloperSettings", + // Settings module used to open the Project Settings viewer programmatically + "Settings", + } + ); + + DynamicallyLoadedModuleNames.AddRange( new string[] { From e6f4e768d1598b9f30dcff79fa74e2bd613555ae Mon Sep 17 00:00:00 2001 From: Julian Benda Date: Thu, 11 Jun 2026 00:19:57 +0200 Subject: [PATCH 2/5] fix(UE): polish inklecate selection menue --- CMakeLists.txt | 5 +- README.md | 17 ++-- inkcpp_python/pybind11 | 2 +- unreal/CMakeLists.txt | 9 +- .../inkcpp/Source/inkcpp/Public/InkSnapshot.h | 4 +- .../inkcpp_editor/Private/InkAssetFactory.cpp | 90 ++----------------- .../Private/InkCppEditorSettings.cpp | 14 ++- .../inkcpp_editor/Private/inklecate_cmd.cpp | 4 +- .../Private/inklecate_cmd.cpp.in | 4 +- .../Public/InkCppEditorSettings.h | 11 +-- 10 files changed, 46 insertions(+), 114 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 578d27d7..b9876789 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,7 @@ FetchContent_Declare( URL_HASH SHA256=26f4e188e02536d6e99e73e71d9b13e2c2144187f1368a87e82fd5066176cff8 SOURCE_DIR "${CMAKE_BINARY_DIR}/inklecate/linux") set(FETCHCONTENT_QUIET OFF) +set(CMAKE_TLS_VERIFY ON) mark_as_advanced(FETCHCONTENT_QUIET) set(CMAKE_TLS_VERIFY true) mark_as_advanced(CMAKE_TLS_VERIFY) @@ -58,8 +59,8 @@ option(INKCPP_PY "Build python bindings" OFF) cmake_dependent_option(WHEEL_BUILD "Set for build wheel python lib. (see setup.py for ussage)" OFF "INKCPP_PY" OFF) option(INKCPP_C "Build c library" OFF) -option(INKCPP_TEST "Build inkcpp tests (requires: inklecate in path / env: INKLECATE set \ - / INKCPP_INKLECATE=OS or ALL)" OFF) +option(INKCPP_TEST "Build inkcpp tests (requires: inklecate in path OR env: INKLECATE set \ + OR INKCPP_INKLECATE=OS or ALL)" OFF) set(INKCPP_INKLECATE "NONE" CACHE STRING "If inklecate should be downloaded automatically from the official release page. \ diff --git a/README.md b/README.md index 5ab46ef8..2aad4f66 100644 --- a/README.md +++ b/README.md @@ -44,9 +44,15 @@ KeyFeatures: snapshots, observers, binding ink functions, support ink [function ## Unreal Plugin InkCPP is available via the [UE Marketplace](https://www.unrealengine.com/marketplace/product/inkcpp). +Since the Unrea Marketplace does not allow for bundling executables, you must install inklecate by hand. +If you add the first asset you will get prompted to download the correct version. Alternativly download [inklecate v1.1.1](https://github.com/inkle/ink/releases/tag/v1.1.1) unzip it and set `Project Settings > Plugins > InkCPP > Inklecate Executable Path` to the path. + Alternativly is the latest version of the UE plugin can be downloaded from the [release page](https://github.com/JBenda/inkcpp/releases/latest) (`unreal.zip`). -Place the content of this file at your plugin folder of your UE project and at the next start up it will be intigrated. +Place the content of this file at a location of your choice and run the following command to build the Plugin. +```sh +\PATH\TO\UNREAL_ENGINE\Build\BatchFiles\RunUAT.bat BuildPlugin -plugin=\inkcpp\inkcpp.uplugin -package=GameProject\Plugins\inkcpp -TargetPlatforms=Win64 # compile plugin +``` A example project can be found [here](https://jbenda.github.io/inkcpp/unreal/InkCPP_DEMO.zip). And here the [Documentation](https://jbenda.github.io/inkcpp/html/group__unreal.html). @@ -57,12 +63,11 @@ mkdir build cd build mkdir plugin mkdir plugin-build -cmake -DINKCPP_UNREAL_TARGET_VERSION="5.5" .. -cmake --install . --component unreal --prefix .\plugin # create source files for plugin -\PATH\TO\UNREAL_ENGINE\Build\BatchFiles\RunUAT.bat BuildPlugin -plugin=GIT_REPO\build\plugin\inkcpp\inkcpp.uplugin -package=GIT_REPO\build\plugin-build\inkcpp -TargetPlatforms=Win64 # compile plugin -move plugin-build\inkcpp UE_ENGINE\Engine\Plugins\inkcpp +cmake -DINKCPP_UNREAL_TARGET_VERSION="5.7" -DINKCPP_UNREAL=ON -DINKCPP_INKLECATE=OS -INKCPP_UNREAL_TARGET_PLATFORM=Win64 .. +cmake --build . --target unreal +cmake --install . --componunt unreal_plugin --perifx ./your_project/Plugins # --prefix = path to global Plugins directory of UE or to your GameProject ``` -Adapt `TargetPlatforms` as nessesarry. You might also want to install the Plugin directly into a project or the in UE5.5 introduced external plugin directory. Just adapt the pathets accordendly. +Adapt `TargetPlatforms` as nessesarry. You might also want to install the Plugin directly into a project or the in UE5.5 introduced external plugin directory. Just adapt the pathes accordendly. ## Use standalone diff --git a/inkcpp_python/pybind11 b/inkcpp_python/pybind11 index 1b499083..0c69e1eb 160000 --- a/inkcpp_python/pybind11 +++ b/inkcpp_python/pybind11 @@ -1 +1 @@ -Subproject commit 1b4990838904501de7110d27e96c0a4152029156 +Subproject commit 0c69e1eb2177fa8f8580632c7b1f97fdb606ce8f diff --git a/unreal/CMakeLists.txt b/unreal/CMakeLists.txt index 8cfc1cfd..bede1136 100644 --- a/unreal/CMakeLists.txt +++ b/unreal/CMakeLists.txt @@ -16,13 +16,6 @@ if(INKCPP_UNREAL) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/inkcpp/inkcpp.uplugin.in" "${CMAKE_CURRENT_BINARY_DIR}/inkcpp/inkcpp.uplugin") - # Try to download inklecate for all platforms. The FetchContent_Declare calls (with URLs and - # hashes) live in the root CMakeLists.txt. For Fab store distributions the bundled binaries are - # stripped; users configure the path themselves via Project Settings > Plugins > InkCPP. - FetchContent_MakeAvailable(inklecate_mac inklecate_windows inklecate_linux) - set(FETCHCONTENT_QUIET OFF) - set(CMAKE_TLS_VERIFY true) - # Default all platform paths to empty (= "not bundled"). They are filled in only when the download # succeeded. set(INKLECATE_CMD_WIN "") @@ -101,7 +94,7 @@ if(INKCPP_UNREAL) to build target `unreal` set the filepath with the variable INKCPP_UNREAL_RunUAT_PATH") endif() else() - message(WARNING, "To directly build the plugin with `cmake --build . --target unreal` please \ + message(WARNING "To directly build the plugin with `cmake --build . --target unreal` please \ set INKCPP_UNREAL_RunUAT_PATH to point to unreals RunUAT script.") endif() diff --git a/unreal/inkcpp/Source/inkcpp/Public/InkSnapshot.h b/unreal/inkcpp/Source/inkcpp/Public/InkSnapshot.h index 3207ff66..bd92be9e 100644 --- a/unreal/inkcpp/Source/inkcpp/Public/InkSnapshot.h +++ b/unreal/inkcpp/Source/inkcpp/Public/InkSnapshot.h @@ -30,13 +30,13 @@ struct INKCPP_API FInkSnapshot { , Migratable(migratable) { } - UPROPERTY(BlueprintReadWrite, SaveGame, Category = "ink|SaveGame") + UPROPERTY(BlueprintReadWrite, SaveGame, Category = "Ink|SaveGame") /** Raw data used to restore runtime state. * not needed if a USaveGame is used. */ TArray data; - UPROPERTY(BlueprintReadOnly, SaveGame, Category = "ink|SaveGame") + UPROPERTY(BlueprintReadOnly, SaveGame, Category = "Ink|SaveGame") /** Is true if the snapshot is migratable. */ bool Migratable; diff --git a/unreal/inkcpp/Source/inkcpp_editor/Private/InkAssetFactory.cpp b/unreal/inkcpp/Source/inkcpp_editor/Private/InkAssetFactory.cpp index 64230239..92ff22e2 100644 --- a/unreal/inkcpp/Source/inkcpp_editor/Private/InkAssetFactory.cpp +++ b/unreal/inkcpp/Source/inkcpp_editor/Private/InkAssetFactory.cpp @@ -106,11 +106,11 @@ void ShowInklecateSetupDialog() = (platform == TEXT("Windows")) ? TEXT("inklecate.exe") : TEXT("inklecate"); const FString contentSubFolder = TEXT("Content/inklecate/"); - const FText titleText = NSLOCTEXT("InkCPP", "SetupTitle", "InkCPP: Inklecate Setup Required"); + const FText titleText = NSLOCTEXT("InkCpp", "SetupTitle", "InkCPP: Inklecate Setup Required"); const FText bodyText = FText::Format( NSLOCTEXT( - "InkCPP", "SetupBody", + "InkCpp", "SetupBody", "To import .ink story files directly, InkCPP needs the inklecate compiler.\n\n" "Quick setup steps:\n" " 1. Download {0} for {1} using the link below.\n" @@ -119,12 +119,9 @@ void ShowInklecateSetupDialog() " 3. Open Project Settings > Plugins > InkCPP and set\n" " \"Inklecate Executable Path\" to:\n" " {3}\n\n" - "Tip: import a .ink.json file (exported from Inky) to skip inklecate entirely.\n\n" - "This build of InkCPP expects ink format version {4}." ), FText::FromString(zipName), FText::FromString(platform), - FText::FromString(contentSubFolder + binaryName), FText::FromString(recommendedPath), - FText::AsNumber(( int64 ) ink::InkVersion) + FText::FromString(contentSubFolder + binaryName), FText::FromString(recommendedPath) ); TSharedPtr DialogPtr; @@ -145,7 +142,7 @@ void ShowInklecateSetupDialog() 0.f, 0.f, 0.f, 12.f )[SNew(SHyperlink) .Text(FText::Format( - NSLOCTEXT("InkCPP", "DownloadLinkLabel", "Download: {0}"), + NSLOCTEXT("InkCpp", "DownloadLinkLabel", "Download: {0}"), FText::FromString(downloadUrl) )) .OnNavigate_Lambda([downloadUrl]() { @@ -156,11 +153,11 @@ void ShowInklecateSetupDialog() + SHorizontalBox::Slot().AutoWidth().Padding( 0.f, 0.f, 8.f, 0.f )[SNew(SButton) - .Text(NSLOCTEXT("InkCPP", "OpenSettingsBtn", "Open Project Settings")) + .Text(NSLOCTEXT("InkCpp", "OpenSettingsBtn", "Open Project Settings")) .OnClicked_Lambda([DialogPtr]() { if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr("Settings")) { - SettingsModule->ShowViewer(TEXT("Project"), TEXT("Plugins"), TEXT("InkCPP")); + SettingsModule->ShowViewer(TEXT("Project"), TEXT("Plugins"), TEXT("InkCpp")); } if (DialogPtr.IsValid()) { DialogPtr->RequestDestroyWindow(); @@ -168,7 +165,7 @@ void ShowInklecateSetupDialog() return FReply::Handled(); })] + SHorizontalBox::Slot().AutoWidth()[SNew(SButton) - .Text(NSLOCTEXT("InkCPP", "CloseBtn", "Close")) + .Text(NSLOCTEXT("InkCpp", "CloseBtn", "Close")) .OnClicked_Lambda([DialogPtr]() { if (DialogPtr.IsValid()) { DialogPtr->RequestDestroyWindow(); @@ -180,76 +177,6 @@ void ShowInklecateSetupDialog() FSlateApplication::Get().AddModalWindow(Dialog, nullptr, false); } -// ------------------------------------------------------------------------- -// Inklecate version check -// ------------------------------------------------------------------------- - -/** - * Runs "inklecate --version", parses the ink JSON version from its output, and - * logs a warning if it does not match ink::InkVersion. - * This check is non-fatal — a version mismatch may still compile correctly. - */ -void CheckInklecateVersion(const std::string& inklecatePath) -{ - const std::string versionCmd = "\"" + inklecatePath + "\" --version 2>&1"; - - FILE* pipe = -#if PLATFORM_WINDOWS - _popen(versionCmd.c_str(), "r"); -#else - popen(versionCmd.c_str(), "r"); -#endif - if (! pipe) { - UE_LOG( - InkCpp, Warning, TEXT("InkCPP: Could not run inklecate --version (version check skipped).") - ); - return; - } - - std::string output; - char buf[256]; - while (fgets(buf, sizeof(buf), pipe)) { - output += buf; - } -#if PLATFORM_WINDOWS - _pclose(pipe); -#else - pclose(pipe); -#endif - - // inklecate prints something like "ink version: 21" or "inkVersion=21" - FString outputFStr(output.c_str()); - FRegexMatcher matcher( - FRegexPattern( - TEXT("(?:ink[Vv]ersion\\s*[=:]\\s*|ink\\s+version\\s*:?\\s*)(\\d+)"), - ERegexPatternFlags{0} - ), - outputFStr - ); - - if (matcher.FindNext()) { - int32 reported = FCString::Atoi(*matcher.GetCaptureGroup(1)); - if (static_cast(reported) != ink::InkVersion) { - UE_LOG( - InkCpp, Warning, - TEXT("InkCPP: inklecate reports ink version %d, but this build of InkCPP expects " - "version %u. Compilation may succeed but runtime behaviour could differ. " - "Download a matching inklecate from https://github.com/inkle/ink/releases"), - reported, ink::InkVersion - ); - } else { - UE_LOG( - InkCpp, Display, TEXT("InkCPP: inklecate version check passed (ink version %d)."), - reported - ); - } - } else { - UE_LOG( - InkCpp, Warning, TEXT("InkCPP: Could not parse ink version from inklecate output: %s"), - *outputFStr - ); - } -} } // anonymous namespace @@ -370,9 +297,6 @@ UObject* UInkAssetFactory::FactoryCreateFile( return nullptr; } - // Non-fatal version compatibility check - CheckInklecateVersion(inklecate_cmd); - // Build the inklecate invocation use_temp_file = true; char tmp_filename[L_tmpnam]; diff --git a/unreal/inkcpp/Source/inkcpp_editor/Private/InkCppEditorSettings.cpp b/unreal/inkcpp/Source/inkcpp_editor/Private/InkCppEditorSettings.cpp index b89feac9..2805cc74 100644 --- a/unreal/inkcpp/Source/inkcpp_editor/Private/InkCppEditorSettings.cpp +++ b/unreal/inkcpp/Source/inkcpp_editor/Private/InkCppEditorSettings.cpp @@ -5,27 +5,35 @@ * https://github.com/JBenda/inkcpp for full license details. */ #include "InkCppEditorSettings.h" +#include "inklecate_cmd.cpp" UInkCppEditorSettings::UInkCppEditorSettings() { CategoryName = TEXT("Plugins"); SectionName = TEXT("InkCPP"); + if (InklcatePath.FilePath == TEXT("")) { + InklcatePath.FilePath = *FString(get_inklecate_cmd().c_str()); + } } +FName UInkCppEditorSettings::GetContainerName() const { return TEXT("Project"); } + FName UInkCppEditorSettings::GetCategoryName() const { return TEXT("Plugins"); } +FName UInkCppEditorSettings::GetSectionName() const { return TEXT("InkCPP"); } + FText UInkCppEditorSettings::GetSectionText() const { - return NSLOCTEXT("InkCPP", "SettingsSectionText", "InkCPP"); + return NSLOCTEXT("InkCpp", "SettingsSectionText", "InkCpp"); } FText UInkCppEditorSettings::GetSectionDescription() const { return NSLOCTEXT( - "InkCPP", "SettingsSectionDescription", + "InkCpp", "SettingsSectionDescription", "Settings for the InkCPP Unreal plugin. " "Configure the path to the inklecate compiler used to import .ink story files." ); } -FString UInkCppEditorSettings::GetInklecatePath() const { return InklatePath.FilePath; } +FString UInkCppEditorSettings::GetInklecatePath() const { return InklcatePath.FilePath; } diff --git a/unreal/inkcpp/Source/inkcpp_editor/Private/inklecate_cmd.cpp b/unreal/inkcpp/Source/inkcpp_editor/Private/inklecate_cmd.cpp index 952dd832..8d99b153 100644 --- a/unreal/inkcpp/Source/inkcpp_editor/Private/inklecate_cmd.cpp +++ b/unreal/inkcpp/Source/inkcpp_editor/Private/inklecate_cmd.cpp @@ -16,7 +16,7 @@ * Returns the path to the inklecate executable as a UTF-8 string. * * Resolution order: - * 1. Value set in Project Settings > Plugins > InkCPP (InklatePath). + * 1. Value set in Project Settings > Plugins > InkCPP (InklcatePath). * 2. Empty string — caller interprets this as "inklecate not configured". * * A non-empty path does NOT guarantee the file exists; the caller is @@ -27,7 +27,7 @@ inline std::string get_inklecate_cmd() const UInkCppEditorSettings* Settings = GetDefault(); if (Settings) { FString ConfiguredPath = Settings->GetInklecatePath().TrimStartAndEnd(); - if (! ConfiguredPath.IsEmpty()) { + if (! ConfiguredPath.IsEmpty() && ConfiguredPath != TEXT("")) { // Convert any forward/backward slashes to the OS-preferred separator FPaths::NormalizeFilename(ConfiguredPath); return std::string(TCHAR_TO_UTF8(*ConfiguredPath)); diff --git a/unreal/inkcpp/Source/inkcpp_editor/Private/inklecate_cmd.cpp.in b/unreal/inkcpp/Source/inkcpp_editor/Private/inklecate_cmd.cpp.in index 30f594f6..6d49afca 100644 --- a/unreal/inkcpp/Source/inkcpp_editor/Private/inklecate_cmd.cpp.in +++ b/unreal/inkcpp/Source/inkcpp_editor/Private/inklecate_cmd.cpp.in @@ -34,7 +34,7 @@ inline std::string get_inklecate_cmd() if (const UInkCppEditorSettings* Settings = GetDefault()) { FString ConfiguredPath = Settings->GetInklecatePath().TrimStartAndEnd(); - if (!ConfiguredPath.IsEmpty()) + if (!ConfiguredPath.IsEmpty() && ConfiguredPath != "") { FPaths::NormalizeFilename(ConfiguredPath); return std::string(TCHAR_TO_UTF8(*ConfiguredPath)); @@ -66,7 +66,7 @@ inline std::string get_inklecate_cmd() if (UInkCppEditorSettings* MutableSettings = GetMutableDefault()) { - MutableSettings->InklatePath.FilePath = FullPath; + MutableSettings->InklcatePath.FilePath = FullPath; MutableSettings->SaveConfig(); } diff --git a/unreal/inkcpp/Source/inkcpp_editor/Public/InkCppEditorSettings.h b/unreal/inkcpp/Source/inkcpp_editor/Public/InkCppEditorSettings.h index 1f1e07fd..0bb322fc 100644 --- a/unreal/inkcpp/Source/inkcpp_editor/Public/InkCppEditorSettings.h +++ b/unreal/inkcpp/Source/inkcpp_editor/Public/InkCppEditorSettings.h @@ -16,7 +16,7 @@ * * @ingroup unreal */ -UCLASS(config = EditorPerProjectUserSettings, defaultconfig, meta = (DisplayName = "InkCPP")) +UCLASS(config = Editor, DefaultConfig) class INKCPP_EDITOR_API UInkCppEditorSettings : public UDeveloperSettings { @@ -26,7 +26,9 @@ class INKCPP_EDITOR_API UInkCppEditorSettings : public UDeveloperSettings UInkCppEditorSettings(); // ~Begin UDeveloperSettings + virtual FName GetContainerName() const override; virtual FName GetCategoryName() const override; + virtual FName GetSectionName() const override; virtual FText GetSectionText() const override; virtual FText GetSectionDescription() const override; // ~End UDeveloperSettings @@ -46,13 +48,12 @@ class INKCPP_EDITOR_API UInkCppEditorSettings : public UDeveloperSettings * https://github.com/inkle/ink/releases */ UPROPERTY( - config, EditAnywhere, Category = "Inklecate", + config, EditAnywhere, Category = "Ink", meta = (DisplayName = "Inklecate Executable Path", - ToolTip = "Absolute path to the inklecate executable (leave empty to use system PATH).", - FilePathFilter = "exe,inklecate,", RelativeToGameDir = false) + ToolTip = "Absolute path to the inklecate executable.", RelativeToGameDir = false) ) - FFilePath InklatePath; + FFilePath InklcatePath = TEXT(""); /** Returns the configured path string, or an empty string if not set. */ FString GetInklecatePath() const; From 876d913e27819443dd6813f39c4fd8b052675ebe Mon Sep 17 00:00:00 2001 From: Julian Benda Date: Thu, 11 Jun 2026 00:21:41 +0200 Subject: [PATCH 3/5] build(UE): delete not needed inklecate binaries based on INKCPP_INKLECATE also skip pypi release if a +UEPatch is part of the version tag --- .github/workflows/build.yml | 40 +++++++++++++++++------------------ .github/workflows/release.yml | 3 ++- inkcpp/hungarian_solver.h | 6 ++++++ unreal/CMakeLists.txt | 3 +++ 4 files changed, 31 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7d8eb526..372c0955 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,7 +44,7 @@ jobs: steps: # Checkout project - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true @@ -88,7 +88,7 @@ jobs: shell: bash run: cmake --install . --config $BUILD_TYPE --prefix comp_cl --component cl - name: Upload Cl - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{ matrix.artifact }}-cl path: build/comp_cl/ @@ -98,7 +98,7 @@ jobs: shell: bash run: cmake --install . --config $BUILD_TYPE --prefix comp_lib --component lib - name: Upload Lib - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{ matrix.artifact }}-lib path: build/comp_lib/ @@ -108,7 +108,7 @@ jobs: shell: bash run: cmake --install . --config $BUILD_TYPE --prefix comp_clib --component clib - name: Upload Clib - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{matrix.artifact}}-clib path: build/comp_clib @@ -118,35 +118,35 @@ jobs: working-directory: ${{github.workspace}}/build shell: bash run: | - cmake $GITHUB_WORKSPACE -DINKCPP_UNREAL_TARGET_VERSION="5.7" -DINKCPP_UNREAL=ON + cmake $GITHUB_WORKSPACE -DINKCPP_UNREAL_TARGET_VERSION="5.7" -DINKCPP_UNREAL=ON -DINKCPP_INKLECATE=NONE cmake --install . --config $BUILD_TYPE --prefix comp_unreal_5_7 --component unreal - cmake $GITHUB_WORKSPACE -DINKCPP_UNREAL_TARGET_VERSION="5.6" -DINKCPP_UNREAL=ON + cmake $GITHUB_WORKSPACE -DINKCPP_UNREAL_TARGET_VERSION="5.6" -DINKCPP_UNREAL=ON -DINKCPP_INKLECATE=NONE cmake --install . --config $BUILD_TYPE --prefix comp_unreal_5_6 --component unreal - cmake $GITHUB_WORKSPACE -DINKCPP_UNREAL_TARGET_VERSION="5.5" -DINKCPP_UNREAL=ON + cmake $GITHUB_WORKSPACE -DINKCPP_UNREAL_TARGET_VERSION="5.5" -DINKCPP_UNREAL=ON -DINKCPP_INKLECATE=NONE cmake --install . --config $BUILD_TYPE --prefix comp_unreal_5_5 --component unreal - cmake $GITHUB_WORKSPACE -DINKCPP_UNREAL_TARGET_VERSION="5.4" -DINKCPP_UNREAL=ON + cmake $GITHUB_WORKSPACE -DINKCPP_UNREAL_TARGET_VERSION="5.4" -DINKCPP_UNREAL=ON -DINKCPP_INKLECATE=NONE cmake --install . --config $BUILD_TYPE --prefix comp_unreal_5_4 --component unreal - name: Upload UE 5.7 if: ${{ matrix.unreal }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: unreal_5_7 path: build/comp_unreal_5_7/ - name: Upload UE 5.6 if: ${{ matrix.unreal }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: unreal_5_6 path: build/comp_unreal_5_6/ - name: Upload UE 5.5 if: ${{ matrix.unreal }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: unreal_5_5 path: build/comp_unreal_5_5/ - name: Upload UE 5.4 if: ${{ matrix.unreal }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: unreal_5_4 path: build/comp_unreal_5_4/ @@ -183,7 +183,7 @@ jobs: # Upload results artifact - name: Upload Results Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: result-${{ matrix.artifact }} path: proofing/ink-proof/${{ matrix.artifact }}.txt @@ -191,7 +191,7 @@ jobs: # Upload website artifact - name: Upload Ink-Proof Website Artifact if: ${{ matrix.proof }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{ matrix.artifact }}-www path: proofing/ink-proof/out @@ -203,7 +203,7 @@ jobs: env: DOXYGEN_VERSION: "1.17.0" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - name: Set upt Python @@ -246,7 +246,7 @@ jobs: shell: bash run: cmake --build . --target doc --config Release - name: Upload - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: doxygen path: Documentation/ @@ -256,7 +256,7 @@ jobs: needs: compilation runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - name: Set up Python @@ -296,7 +296,7 @@ jobs: run: | rm dist/*.whl - name: Upload Python files - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: python-package-distribution path: dist/ @@ -306,7 +306,7 @@ jobs: runs-on: ubuntu-latest if: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'master' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Fetch master branch run: | git fetch origin master @@ -329,7 +329,7 @@ jobs: runs-on: ubuntu-latest if: github.ref == 'refs/heads/master' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # Download Ink Proof page for Linux - uses: actions/download-artifact@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0a68d7fe..087f986e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: id-token: write contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Download artifacts env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -35,6 +35,7 @@ jobs: - name: List run: tree - name: Publish to PyPI + if: ${{! contains(github.ref_name, "+UEPatch")}} uses: pypa/gh-action-pypi-publish@release/v1.12 - name: Create release env: diff --git a/inkcpp/hungarian_solver.h b/inkcpp/hungarian_solver.h index e4a53467..3a039c7a 100644 --- a/inkcpp/hungarian_solver.h +++ b/inkcpp/hungarian_solver.h @@ -1,3 +1,9 @@ +/* Copyright (c) 2024 Julian Benda + * + * This file is part of inkCPP which is released under MIT license. + * See file LICENSE.txt or go to + * https://github.com/JBenda/inkcpp for full license details. + */ #pragma once #include "system.h" diff --git a/unreal/CMakeLists.txt b/unreal/CMakeLists.txt index bede1136..0de38884 100644 --- a/unreal/CMakeLists.txt +++ b/unreal/CMakeLists.txt @@ -29,6 +29,7 @@ if(INKCPP_UNREAL) DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/inkcpp/Source/ThirdParty/inklecate/") set(INKCPP_INKLECATE_BUNDLED TRUE) else() + file(REMOVE_RECURSE "${CMAKE_CURRENT_BINARY_DIR}/inkcpp/Source/ThirdParty/inklecate/windows") message(WARNING "InkCPP: failed to download inklecate for Windows. " "A .ink file can still be imported if the user configures the inklecate path " "in Project Settings > Plugins > InkCPP.") @@ -39,6 +40,7 @@ if(INKCPP_UNREAL) DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/inkcpp/Source/ThirdParty/inklecate/") set(INKCPP_INKLECATE_BUNDLED TRUE) else() + file(REMOVE_RECURSE "${CMAKE_CURRENT_BINARY_DIR}/inkcpp/Source/ThirdParty/inklecate/mac") message(WARNING "InkCPP: failed to download inklecate for macOS. " "A .ink file can still be imported if the user configures the inklecate path " "in Project Settings > Plugins > InkCPP.") @@ -49,6 +51,7 @@ if(INKCPP_UNREAL) DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/inkcpp/Source/ThirdParty/inklecate/") set(INKCPP_INKLECATE_BUNDLED TRUE) else() + file(REMOVE_RECURSE "${CMAKE_CURRENT_BINARY_DIR}/inkcpp/Source/ThirdParty/inklecate/linux") message(WARNING "InkCPP: failed to download inklecate for Linux. " "A .ink file can still be imported if the user configures the inklecate path " "in Project Settings > Plugins > InkCPP.") From 506e57c9e74cbda59de59f61f5ac9f39abc65784 Mon Sep 17 00:00:00 2001 From: Julian Benda Date: Thu, 11 Jun 2026 09:18:37 +0200 Subject: [PATCH 4/5] docs(UE): update build command for UnrealEngine Plugin --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2aad4f66..fcbdc0af 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,10 @@ mkdir build cd build mkdir plugin mkdir plugin-build -cmake -DINKCPP_UNREAL_TARGET_VERSION="5.7" -DINKCPP_UNREAL=ON -DINKCPP_INKLECATE=OS -INKCPP_UNREAL_TARGET_PLATFORM=Win64 .. +cmake -DINKCPP_UNREAL_TARGET_VERSION="5.7" -DINKCPP_UNREAL=ON -DINKCPP_INKLECATE=OS -DINKCPP_UNREAL_RunUAT_PATH=\Path\TO\UNREAL_ENGINE\Build\BatchFiles\RunUAT.bat -DINKCPP_UNREAL_TARGET_PLATFORM=Win64 .. +# to set the variables with a GUI use +# cmake .. +# cmage-gui . cmake --build . --target unreal cmake --install . --componunt unreal_plugin --perifx ./your_project/Plugins # --prefix = path to global Plugins directory of UE or to your GameProject ``` From 4e17f0777418ac4543aaeb500bd03ccc0a52c58c Mon Sep 17 00:00:00 2001 From: Julian Benda Date: Thu, 11 Jun 2026 09:32:09 +0200 Subject: [PATCH 5/5] style: formatting --- inkcpp_python/pybind11 | 2 +- .../inkcpp/Source/inkcpp_editor/Private/InkAssetFactory.cpp | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/inkcpp_python/pybind11 b/inkcpp_python/pybind11 index 0c69e1eb..1b499083 160000 --- a/inkcpp_python/pybind11 +++ b/inkcpp_python/pybind11 @@ -1 +1 @@ -Subproject commit 0c69e1eb2177fa8f8580632c7b1f97fdb606ce8f +Subproject commit 1b4990838904501de7110d27e96c0a4152029156 diff --git a/unreal/inkcpp/Source/inkcpp_editor/Private/InkAssetFactory.cpp b/unreal/inkcpp/Source/inkcpp_editor/Private/InkAssetFactory.cpp index 92ff22e2..65d4ed4f 100644 --- a/unreal/inkcpp/Source/inkcpp_editor/Private/InkAssetFactory.cpp +++ b/unreal/inkcpp/Source/inkcpp_editor/Private/InkAssetFactory.cpp @@ -312,9 +312,8 @@ UObject* UInkAssetFactory::FactoryCreateFile( // Note: on Windows, if std::system()'s argument begins with '"', the outer // pair of quotes is stripped, breaking paths with spaces. Emitting the first // character outside the quoted section works around this. - cmd << inklecate_cmd[0] << "\"" << (inklecate_cmd.c_str() + 1) << "\"" - << " -o \"" << json_path.string() << "\"" - << " \"" << story_path.string() << "\" 2>&1"; + cmd << inklecate_cmd[0] << "\"" << (inklecate_cmd.c_str() + 1) << "\"" << " -o \"" + << json_path.string() << "\"" << " \"" << story_path.string() << "\" 2>&1"; const std::string cmd_str = cmd.str(); int result = std::system(cmd_str.c_str()); if (result != 0) {