First commit
First commit contains v1 of PS Classics fPKG Builder for macOS.
This commit is contained in:
@@ -60,3 +60,5 @@ fastlane/report.xml
|
|||||||
fastlane/Preview.html
|
fastlane/Preview.html
|
||||||
fastlane/screenshots/**/*.png
|
fastlane/screenshots/**/*.png
|
||||||
fastlane/test_output
|
fastlane/test_output
|
||||||
|
|
||||||
|
**/.DS_Store
|
||||||
|
|||||||
@@ -0,0 +1,395 @@
|
|||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 56;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
C2DC09452C6D2F45009897FF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2DC09442C6D2F45009897FF /* AppDelegate.swift */; };
|
||||||
|
C2DC09492C6D2F46009897FF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C2DC09482C6D2F46009897FF /* Assets.xcassets */; };
|
||||||
|
C2DC094C2C6D2F46009897FF /* Base in Resources */ = {isa = PBXBuildFile; fileRef = C2DC094B2C6D2F46009897FF /* Base */; };
|
||||||
|
C2DC09572C6D3068009897FF /* PSClassicsWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2DC09532C6D3068009897FF /* PSClassicsWindowController.swift */; };
|
||||||
|
C2DC09582C6D3068009897FF /* PS2ClassicsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2DC09542C6D3068009897FF /* PS2ClassicsViewController.swift */; };
|
||||||
|
C2DC09592C6D3068009897FF /* PSPClassicsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2DC09552C6D3068009897FF /* PSPClassicsViewController.swift */; };
|
||||||
|
C2DC095A2C6D3068009897FF /* PS1ClassicsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2DC09562C6D3068009897FF /* PS1ClassicsViewController.swift */; };
|
||||||
|
C2E921542C70FAE600DEC0FC /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E921532C70FAE600DEC0FC /* Utils.swift */; };
|
||||||
|
C2E921562C7110C800DEC0FC /* isoinfo in Resources */ = {isa = PBXBuildFile; fileRef = C2E921552C7110C800DEC0FC /* isoinfo */; };
|
||||||
|
C2E921582C71219700DEC0FC /* unar in Resources */ = {isa = PBXBuildFile; fileRef = C2E921572C71219700DEC0FC /* unar */; };
|
||||||
|
C2E9215A2C71E96C00DEC0FC /* pspdecrypt in Resources */ = {isa = PBXBuildFile; fileRef = C2E921592C71E96C00DEC0FC /* pspdecrypt */; };
|
||||||
|
C2E9215E2C71F6F000DEC0FC /* sfoutil in Resources */ = {isa = PBXBuildFile; fileRef = C2E9215D2C71F6F000DEC0FC /* sfoutil */; };
|
||||||
|
C2E921602C72298400DEC0FC /* Preloader.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C2E9215F2C72298400DEC0FC /* Preloader.storyboard */; };
|
||||||
|
C2E921622C722C1200DEC0FC /* PreloaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E921612C722C1200DEC0FC /* PreloaderViewController.swift */; };
|
||||||
|
C2E921642C72409200DEC0FC /* PSClassicsTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E921632C72409200DEC0FC /* PSClassicsTabViewController.swift */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
C2DC09412C6D2F45009897FF /* fPKG Builder.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "fPKG Builder.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
C2DC09442C6D2F45009897FF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
C2DC09482C6D2F46009897FF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
C2DC094B2C6D2F46009897FF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||||
|
C2DC094D2C6D2F46009897FF /* fPKG_Builder.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = fPKG_Builder.entitlements; sourceTree = "<group>"; };
|
||||||
|
C2DC09532C6D3068009897FF /* PSClassicsWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PSClassicsWindowController.swift; sourceTree = "<group>"; };
|
||||||
|
C2DC09542C6D3068009897FF /* PS2ClassicsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PS2ClassicsViewController.swift; sourceTree = "<group>"; };
|
||||||
|
C2DC09552C6D3068009897FF /* PSPClassicsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PSPClassicsViewController.swift; sourceTree = "<group>"; };
|
||||||
|
C2DC09562C6D3068009897FF /* PS1ClassicsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PS1ClassicsViewController.swift; sourceTree = "<group>"; };
|
||||||
|
C2E921532C70FAE600DEC0FC /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; };
|
||||||
|
C2E921552C7110C800DEC0FC /* isoinfo */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = isoinfo; sourceTree = "<group>"; };
|
||||||
|
C2E921572C71219700DEC0FC /* unar */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = unar; sourceTree = "<group>"; };
|
||||||
|
C2E921592C71E96C00DEC0FC /* pspdecrypt */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = pspdecrypt; sourceTree = "<group>"; };
|
||||||
|
C2E9215D2C71F6F000DEC0FC /* sfoutil */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = sfoutil; sourceTree = "<group>"; };
|
||||||
|
C2E9215F2C72298400DEC0FC /* Preloader.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Preloader.storyboard; sourceTree = "<group>"; };
|
||||||
|
C2E921612C722C1200DEC0FC /* PreloaderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreloaderViewController.swift; sourceTree = "<group>"; };
|
||||||
|
C2E921632C72409200DEC0FC /* PSClassicsTabViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PSClassicsTabViewController.swift; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
C2DC093E2C6D2F45009897FF /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
C2DC09382C6D2F45009897FF = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
C2DC09432C6D2F45009897FF /* fPKG Builder */,
|
||||||
|
C2DC09422C6D2F45009897FF /* Products */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
C2DC09422C6D2F45009897FF /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
C2DC09412C6D2F45009897FF /* fPKG Builder.app */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
C2DC09432C6D2F45009897FF /* fPKG Builder */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
C2E9215D2C71F6F000DEC0FC /* sfoutil */,
|
||||||
|
C2E921592C71E96C00DEC0FC /* pspdecrypt */,
|
||||||
|
C2E921572C71219700DEC0FC /* unar */,
|
||||||
|
C2E921552C7110C800DEC0FC /* isoinfo */,
|
||||||
|
C2DC09442C6D2F45009897FF /* AppDelegate.swift */,
|
||||||
|
C2E921612C722C1200DEC0FC /* PreloaderViewController.swift */,
|
||||||
|
C2DC09562C6D3068009897FF /* PS1ClassicsViewController.swift */,
|
||||||
|
C2DC09542C6D3068009897FF /* PS2ClassicsViewController.swift */,
|
||||||
|
C2E921632C72409200DEC0FC /* PSClassicsTabViewController.swift */,
|
||||||
|
C2DC09532C6D3068009897FF /* PSClassicsWindowController.swift */,
|
||||||
|
C2DC09552C6D3068009897FF /* PSPClassicsViewController.swift */,
|
||||||
|
C2E921532C70FAE600DEC0FC /* Utils.swift */,
|
||||||
|
C2DC09482C6D2F46009897FF /* Assets.xcassets */,
|
||||||
|
C2DC094A2C6D2F46009897FF /* Main.storyboard */,
|
||||||
|
C2E9215F2C72298400DEC0FC /* Preloader.storyboard */,
|
||||||
|
C2DC094D2C6D2F46009897FF /* fPKG_Builder.entitlements */,
|
||||||
|
);
|
||||||
|
path = "fPKG Builder";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
C2DC09402C6D2F45009897FF /* fPKG Builder */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = C2DC09502C6D2F46009897FF /* Build configuration list for PBXNativeTarget "fPKG Builder" */;
|
||||||
|
buildPhases = (
|
||||||
|
C2DC093D2C6D2F45009897FF /* Sources */,
|
||||||
|
C2DC093E2C6D2F45009897FF /* Frameworks */,
|
||||||
|
C2DC093F2C6D2F45009897FF /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = "fPKG Builder";
|
||||||
|
productName = "fPKG Builder";
|
||||||
|
productReference = C2DC09412C6D2F45009897FF /* fPKG Builder.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
C2DC09392C6D2F45009897FF /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
BuildIndependentTargetsInParallel = 1;
|
||||||
|
LastSwiftUpdateCheck = 1540;
|
||||||
|
LastUpgradeCheck = 1540;
|
||||||
|
TargetAttributes = {
|
||||||
|
C2DC09402C6D2F45009897FF = {
|
||||||
|
CreatedOnToolsVersion = 15.4;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = C2DC093C2C6D2F45009897FF /* Build configuration list for PBXProject "fPKG Builder" */;
|
||||||
|
compatibilityVersion = "Xcode 14.0";
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = C2DC09382C6D2F45009897FF;
|
||||||
|
productRefGroup = C2DC09422C6D2F45009897FF /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
C2DC09402C6D2F45009897FF /* fPKG Builder */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
C2DC093F2C6D2F45009897FF /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
C2E9215A2C71E96C00DEC0FC /* pspdecrypt in Resources */,
|
||||||
|
C2E921562C7110C800DEC0FC /* isoinfo in Resources */,
|
||||||
|
C2DC09492C6D2F46009897FF /* Assets.xcassets in Resources */,
|
||||||
|
C2E9215E2C71F6F000DEC0FC /* sfoutil in Resources */,
|
||||||
|
C2E921582C71219700DEC0FC /* unar in Resources */,
|
||||||
|
C2E921602C72298400DEC0FC /* Preloader.storyboard in Resources */,
|
||||||
|
C2DC094C2C6D2F46009897FF /* Base in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
C2DC093D2C6D2F45009897FF /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
C2DC09582C6D3068009897FF /* PS2ClassicsViewController.swift in Sources */,
|
||||||
|
C2DC09592C6D3068009897FF /* PSPClassicsViewController.swift in Sources */,
|
||||||
|
C2DC09452C6D2F45009897FF /* AppDelegate.swift in Sources */,
|
||||||
|
C2DC095A2C6D3068009897FF /* PS1ClassicsViewController.swift in Sources */,
|
||||||
|
C2DC09572C6D3068009897FF /* PSClassicsWindowController.swift in Sources */,
|
||||||
|
C2E921622C722C1200DEC0FC /* PreloaderViewController.swift in Sources */,
|
||||||
|
C2E921642C72409200DEC0FC /* PSClassicsTabViewController.swift in Sources */,
|
||||||
|
C2E921542C70FAE600DEC0FC /* Utils.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXVariantGroup section */
|
||||||
|
C2DC094A2C6D2F46009897FF /* Main.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
C2DC094B2C6D2F46009897FF /* Base */,
|
||||||
|
);
|
||||||
|
name = Main.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
C2DC094E2C6D2F46009897FF /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = macosx;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
C2DC094F2C6D2F46009897FF /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
SDKROOT = macosx;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
C2DC09512C6D2F46009897FF /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = "fPKG Builder/fPKG_Builder.entitlements";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "PS Classics fPKG Builder";
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
|
INFOPLIST_KEY_NSMainStoryboardFile = Preloader;
|
||||||
|
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/../Frameworks",
|
||||||
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "SvenGDK.fPKG-Builder";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
C2DC09522C6D2F46009897FF /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = "fPKG Builder/fPKG_Builder.entitlements";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "PS Classics fPKG Builder";
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
|
INFOPLIST_KEY_NSMainStoryboardFile = Preloader;
|
||||||
|
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/../Frameworks",
|
||||||
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "SvenGDK.fPKG-Builder";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
C2DC093C2C6D2F45009897FF /* Build configuration list for PBXProject "fPKG Builder" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
C2DC094E2C6D2F46009897FF /* Debug */,
|
||||||
|
C2DC094F2C6D2F46009897FF /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
C2DC09502C6D2F46009897FF /* Build configuration list for PBXNativeTarget "fPKG Builder" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
C2DC09512C6D2F46009897FF /* Debug */,
|
||||||
|
C2DC09522C6D2F46009897FF /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
};
|
||||||
|
rootObject = C2DC09392C6D2F45009897FF /* Project object */;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// AppDelegate.swift
|
||||||
|
// fPKG Builder
|
||||||
|
//
|
||||||
|
// Created by SvenGDK on 14/08/2024.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Cocoa
|
||||||
|
|
||||||
|
@main
|
||||||
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||||
|
// Insert code here to initialize your application
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationWillTerminate(_ aNotification: Notification) {
|
||||||
|
// Insert code here to tear down your application
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "16x16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "16x16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "128x128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "128x128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "256x256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "256x256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "512x512"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "PS1.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "PS2.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "psp.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,838 @@
|
|||||||
|
//
|
||||||
|
// PS1ClassicsViewController.swift
|
||||||
|
// PS Mac Tools
|
||||||
|
//
|
||||||
|
// Created by SvenGDK on 14/08/2024.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AppKit
|
||||||
|
|
||||||
|
class PS1ClassicsViewController: NSViewController {
|
||||||
|
|
||||||
|
var CurrentGameID: String = ""
|
||||||
|
var Disc1CueFile: String = ""
|
||||||
|
var Disc2CueFile: String = ""
|
||||||
|
var Disc3CueFile: String = ""
|
||||||
|
var Disc4CueFile: String = ""
|
||||||
|
|
||||||
|
@IBOutlet weak var BuildfPKGButton: NSButton!
|
||||||
|
@IBOutlet weak var ProgressTextField: NSTextField!
|
||||||
|
@IBOutlet weak var fPKGBuilderProgressIndicator: NSProgressIndicator!
|
||||||
|
@IBOutlet weak var fPKGBuilderSpinningIndicator: NSProgressIndicator!
|
||||||
|
@IBOutlet weak var fPKGBuilderStatusTextField: NSTextField!
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
// Select 6x upscaling by default
|
||||||
|
UpscalingComboBox.selectItem(at: 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
override var representedObject: Any? {
|
||||||
|
didSet {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBOutlet weak var GameTitleTextField: NSTextField!
|
||||||
|
@IBOutlet weak var NPTitleTextField: NSTextField!
|
||||||
|
@IBOutlet weak var IconTextField: NSTextField!
|
||||||
|
@IBOutlet weak var BackgroundTextField: NSTextField!
|
||||||
|
@IBOutlet weak var Disc1TextField: NSTextField!
|
||||||
|
@IBOutlet weak var Disc2TextField: NSTextField!
|
||||||
|
@IBOutlet weak var Disc3TextField: NSTextField!
|
||||||
|
@IBOutlet weak var Disc4TextField: NSTextField!
|
||||||
|
@IBOutlet weak var TXTConfigTextField: NSTextField!
|
||||||
|
@IBOutlet weak var LUAConfigTextField: NSTextField!
|
||||||
|
@IBOutlet weak var SkipBootlogoCheckBox: NSButton!
|
||||||
|
@IBOutlet weak var Force60HzCheckBox: NSButton!
|
||||||
|
@IBOutlet weak var EmulateAnalogSticksCheckBox: NSButton!
|
||||||
|
@IBOutlet weak var EnableGunconCheckBox: NSButton!
|
||||||
|
@IBOutlet weak var UpscalingComboBox: NSComboBox!
|
||||||
|
|
||||||
|
@IBAction func BrowseIcon(_ sender: NSButton) {
|
||||||
|
let dialog = NSOpenPanel()
|
||||||
|
|
||||||
|
dialog.title = "Select an icon for your game"
|
||||||
|
dialog.showsResizeIndicator = true
|
||||||
|
dialog.showsHiddenFiles = false
|
||||||
|
dialog.canChooseFiles = true
|
||||||
|
dialog.canChooseDirectories = false
|
||||||
|
dialog.allowsMultipleSelection = false
|
||||||
|
dialog.allowedContentTypes = [Utils().PNGType]
|
||||||
|
|
||||||
|
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = dialog.url
|
||||||
|
IconTextField.stringValue = result!.path
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func BrowseBackground(_ sender: NSButton) {
|
||||||
|
let dialog = NSOpenPanel()
|
||||||
|
|
||||||
|
dialog.title = "Select a background for your game"
|
||||||
|
dialog.showsResizeIndicator = true
|
||||||
|
dialog.showsHiddenFiles = false
|
||||||
|
dialog.canChooseFiles = true
|
||||||
|
dialog.canChooseDirectories = false
|
||||||
|
dialog.allowsMultipleSelection = false
|
||||||
|
dialog.allowedContentTypes = [Utils().PNGType]
|
||||||
|
|
||||||
|
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = dialog.url
|
||||||
|
BackgroundTextField.stringValue = result!.path
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func BrowseDisc1(_ sender: NSButton) {
|
||||||
|
let dialog = NSOpenPanel()
|
||||||
|
|
||||||
|
dialog.title = "Select your PS1 BIN file"
|
||||||
|
dialog.showsResizeIndicator = true
|
||||||
|
dialog.showsHiddenFiles = false
|
||||||
|
dialog.canChooseFiles = true
|
||||||
|
dialog.canChooseDirectories = false
|
||||||
|
dialog.allowsMultipleSelection = false
|
||||||
|
dialog.allowedContentTypes = [Utils().BINType]
|
||||||
|
|
||||||
|
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = dialog.url
|
||||||
|
Disc1TextField.stringValue = result!.path
|
||||||
|
checkPS1Game(inputPath: result!.path)
|
||||||
|
BuildfPKGButton.isEnabled = true
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func BrowseDisc2(_ sender: NSButton) {
|
||||||
|
let dialog = NSOpenPanel()
|
||||||
|
|
||||||
|
dialog.title = "Select the Disc 2 BIN file"
|
||||||
|
dialog.showsResizeIndicator = true
|
||||||
|
dialog.showsHiddenFiles = false
|
||||||
|
dialog.canChooseFiles = true
|
||||||
|
dialog.canChooseDirectories = false
|
||||||
|
dialog.allowsMultipleSelection = false
|
||||||
|
dialog.allowedContentTypes = [Utils().BINType]
|
||||||
|
|
||||||
|
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = dialog.url
|
||||||
|
Disc2TextField.stringValue = result!.path
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func BrowseDisc3(_ sender: NSButton) {
|
||||||
|
let dialog = NSOpenPanel()
|
||||||
|
|
||||||
|
dialog.title = "Select the Disc 3 BIN file"
|
||||||
|
dialog.showsResizeIndicator = true
|
||||||
|
dialog.showsHiddenFiles = false
|
||||||
|
dialog.canChooseFiles = true
|
||||||
|
dialog.canChooseDirectories = false
|
||||||
|
dialog.allowsMultipleSelection = false
|
||||||
|
dialog.allowedContentTypes = [Utils().BINType]
|
||||||
|
|
||||||
|
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = dialog.url
|
||||||
|
Disc3TextField.stringValue = result!.path
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func BrowseDisc4(_ sender: NSButton) {
|
||||||
|
let dialog = NSOpenPanel()
|
||||||
|
|
||||||
|
dialog.title = "Select the Disc 4 BIN file"
|
||||||
|
dialog.showsResizeIndicator = true
|
||||||
|
dialog.showsHiddenFiles = false
|
||||||
|
dialog.canChooseFiles = true
|
||||||
|
dialog.canChooseDirectories = false
|
||||||
|
dialog.allowsMultipleSelection = false
|
||||||
|
dialog.allowedContentTypes = [Utils().BINType]
|
||||||
|
|
||||||
|
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = dialog.url
|
||||||
|
Disc4TextField.stringValue = result!.path
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func BrowseTXTConfig(_ sender: NSButton) {
|
||||||
|
let dialog = NSOpenPanel()
|
||||||
|
|
||||||
|
dialog.title = "Select a TXT config file"
|
||||||
|
dialog.showsResizeIndicator = true
|
||||||
|
dialog.showsHiddenFiles = false
|
||||||
|
dialog.canChooseFiles = true
|
||||||
|
dialog.canChooseDirectories = false
|
||||||
|
dialog.allowsMultipleSelection = false
|
||||||
|
dialog.allowedContentTypes = [Utils().TXTType]
|
||||||
|
|
||||||
|
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = dialog.url
|
||||||
|
TXTConfigTextField.stringValue = result!.path
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func BrowseLUAConfig(_ sender: NSButton) {
|
||||||
|
let dialog = NSOpenPanel()
|
||||||
|
|
||||||
|
dialog.title = "Select a LUA config file"
|
||||||
|
dialog.showsResizeIndicator = true
|
||||||
|
dialog.showsHiddenFiles = false
|
||||||
|
dialog.canChooseFiles = true
|
||||||
|
dialog.canChooseDirectories = false
|
||||||
|
dialog.allowsMultipleSelection = false
|
||||||
|
dialog.allowedContentTypes = [Utils().TXTType]
|
||||||
|
|
||||||
|
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = dialog.url
|
||||||
|
LUAConfigTextField.stringValue = result!.path
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkPS1Game(inputPath: String) {
|
||||||
|
|
||||||
|
// Read the BIN file and try to get the game ID
|
||||||
|
let BINReaderOutput = ReadBINFile(fileInput: inputPath).output
|
||||||
|
var BINGameTitleID = BINReaderOutput.filter({(item: String) -> Bool in
|
||||||
|
let stringMatch = item.lowercased().range(of: "BOOT = ".lowercased())
|
||||||
|
let stringMatch2 = item.lowercased().range(of: "BOOT=".lowercased())
|
||||||
|
if stringMatch == nil {
|
||||||
|
return stringMatch2 != nil ? true : false
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return stringMatch != nil ? true : false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if BINGameTitleID.count == 0 {
|
||||||
|
BINGameTitleID = ["Title ID not found", "Title ID not found"]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if BINGameTitleID[0].starts(with: "BOOT=") {
|
||||||
|
NPTitleTextField.stringValue = BINGameTitleID[0].components(separatedBy: "=")[1]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
NPTitleTextField.stringValue = BINGameTitleID[0].components(separatedBy: " = ")[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentGameID = BINGameTitleID[0].replacingOccurrences(of: "BOOT = cdrom:\\", with: "").replacingOccurrences(of: "BOOT=cdrom:\\", with: "").replacingOccurrences(of: "BOOT = cdrom:", with: "").replacingOccurrences(of: ";1", with: "").replacingOccurrences(of: "_", with: "").replacingOccurrences(of: ".", with: "").replacingOccurrences(of: "MGS\"", with: "")
|
||||||
|
|
||||||
|
NPTitleTextField.stringValue = CurrentGameID
|
||||||
|
|
||||||
|
// Check for a game title if we have a game ID
|
||||||
|
if !CurrentGameID.isEmpty {
|
||||||
|
var AdjustedGameID: String = CurrentGameID
|
||||||
|
AdjustedGameID.insert("-", at: AdjustedGameID.index(AdjustedGameID.startIndex, offsetBy: 4))
|
||||||
|
|
||||||
|
print(AdjustedGameID)
|
||||||
|
|
||||||
|
let GameTitle: String = FindGameTitle(GameID: AdjustedGameID)
|
||||||
|
if !GameTitle.isEmpty {
|
||||||
|
GameTitleTextField.stringValue = GameTitle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadBINFile(fileInput: String) -> (output: [String], error: [String], exitCode: Int32) {
|
||||||
|
var output : [String] = []
|
||||||
|
var error : [String] = []
|
||||||
|
|
||||||
|
let task = Process()
|
||||||
|
task.launchPath = "/bin/sh"
|
||||||
|
task.arguments = ["-c", "strings '" + fileInput + "' | LANG=C fgrep 'BOOT'"]
|
||||||
|
|
||||||
|
let outpipe = Pipe()
|
||||||
|
task.standardOutput = outpipe
|
||||||
|
let errpipe = Pipe()
|
||||||
|
task.standardError = errpipe
|
||||||
|
|
||||||
|
task.launch()
|
||||||
|
|
||||||
|
let outdata = outpipe.fileHandleForReading.readDataToEndOfFile()
|
||||||
|
if var string = String(data: outdata, encoding: .utf8) {
|
||||||
|
string = string.trimmingCharacters(in: .newlines)
|
||||||
|
output = string.components(separatedBy: "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
let errdata = errpipe.fileHandleForReading.readDataToEndOfFile()
|
||||||
|
if var string = String(data: errdata, encoding: .utf8) {
|
||||||
|
string = string.trimmingCharacters(in: .newlines)
|
||||||
|
error = string.components(separatedBy: "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
task.waitUntilExit()
|
||||||
|
let status = task.terminationStatus
|
||||||
|
|
||||||
|
return (output, error, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func BuildfPKG(_ sender: NSButton) {
|
||||||
|
|
||||||
|
let MissingInfoAlert = NSAlert()
|
||||||
|
MissingInfoAlert.addButton(withTitle: "Close")
|
||||||
|
|
||||||
|
if Disc1TextField.stringValue.isEmpty {
|
||||||
|
MissingInfoAlert.messageText = "Disc 1 is missing, cannot continue."
|
||||||
|
MissingInfoAlert.runModal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if GameTitleTextField.stringValue.isEmpty {
|
||||||
|
MissingInfoAlert.messageText = "The Game Title is missing, cannot continue."
|
||||||
|
MissingInfoAlert.runModal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if NPTitleTextField.stringValue.isEmpty {
|
||||||
|
MissingInfoAlert.messageText = "The NP Title is missing, cannot continue."
|
||||||
|
MissingInfoAlert.runModal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ProgressTextField.isHidden = false
|
||||||
|
fPKGBuilderProgressIndicator.isHidden = false
|
||||||
|
fPKGBuilderSpinningIndicator.isHidden = false
|
||||||
|
fPKGBuilderStatusTextField.isHidden = false
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.isIndeterminate = true
|
||||||
|
fPKGBuilderProgressIndicator.startAnimation(nil)
|
||||||
|
fPKGBuilderSpinningIndicator.startAnimation(nil)
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Waiting for user input"
|
||||||
|
|
||||||
|
CreatefPKG()
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreatefPKG() {
|
||||||
|
|
||||||
|
var PKGOutputPath: String = ""
|
||||||
|
let FolderBrowserOpenPanel = NSOpenPanel()
|
||||||
|
|
||||||
|
FolderBrowserOpenPanel.title = "Select an output folder"
|
||||||
|
FolderBrowserOpenPanel.showsResizeIndicator = true
|
||||||
|
FolderBrowserOpenPanel.showsHiddenFiles = false
|
||||||
|
FolderBrowserOpenPanel.canChooseFiles = false
|
||||||
|
FolderBrowserOpenPanel.canChooseDirectories = true
|
||||||
|
FolderBrowserOpenPanel.allowsMultipleSelection = false
|
||||||
|
|
||||||
|
// Set destination folder where the pkg file will be moved into
|
||||||
|
if (FolderBrowserOpenPanel.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = FolderBrowserOpenPanel.url
|
||||||
|
PKGOutputPath = result!.path
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.stopAnimation(nil)
|
||||||
|
fPKGBuilderProgressIndicator.isIndeterminate = false
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue = 0
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Removing old fPKG build folder"
|
||||||
|
usleep(150000)
|
||||||
|
|
||||||
|
// Remove previous fPKG GP4 project
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().CDrivePath + "/Cache/PS1fPKG.gp4") {
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(atPath: Utils().CDrivePath + "/Cache/PS1fPKG.gp4")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove previous fPKG project folder
|
||||||
|
var isDir: ObjCBool = true
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().CDrivePath + "/Cache/PS1fPKG", isDirectory: &isDir) {
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(atPath: Utils().CDrivePath + "/Cache/PS1fPKG")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue += 10
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Preparing the PS1 emulator"
|
||||||
|
usleep(150000)
|
||||||
|
|
||||||
|
// Copy the PS1 emulator to the new project folder
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().CDrivePath + "/PS4/emus/ps1hd", isDirectory: &isDir) {
|
||||||
|
do {
|
||||||
|
try FileManager.default.copyItem(atPath: Utils().CDrivePath + "/PS4/emus/ps1hd", toPath: Utils().CDrivePath + "/Cache/PS1fPKG")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue += 10
|
||||||
|
|
||||||
|
// Copy the selected icon to the project folder
|
||||||
|
if !IconTextField.stringValue.isEmpty {
|
||||||
|
do {
|
||||||
|
try FileManager.default.copyItem(atPath: IconTextField.stringValue, toPath: Utils().CDrivePath + "/Cache/PS1fPKG/sce_sys/icon0.png")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the selected background to the project folder
|
||||||
|
if !BackgroundTextField.stringValue.isEmpty {
|
||||||
|
do {
|
||||||
|
try FileManager.default.copyItem(atPath: IconTextField.stringValue, toPath: Utils().CDrivePath + "/Cache/PS1fPKG/sce_sys/pic0.png")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If config already exists, remove it
|
||||||
|
let ConfigDestinationPath: String = Utils().CDrivePath + "/Cache/PS1fPKG/config-title.txt"
|
||||||
|
if FileManager.default.fileExists(atPath: ConfigDestinationPath)
|
||||||
|
{
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(atPath: ConfigDestinationPath)
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new config file
|
||||||
|
FileManager.default.createFile(atPath: ConfigDestinationPath, contents:Data(" ".utf8), attributes: nil)
|
||||||
|
let ConfigWriter = FileHandle(forWritingAtPath: ConfigDestinationPath)
|
||||||
|
if ConfigWriter == nil { }
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ConfigWriter!.write("--ps4-trophies=0\n".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--ps5-uds=0\n".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--trophies=0\n".data(using: .utf8)!)
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue += 10
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Preparing selected disc(s)"
|
||||||
|
usleep(150000)
|
||||||
|
|
||||||
|
// Discs Setup
|
||||||
|
Disc1CueFile = Disc1TextField.stringValue.replacingOccurrences(of: ".bin", with: ".cue")
|
||||||
|
ConfigWriter!.write(#"--image="data/disc1.bin"\#n"#.data(using: .utf8)!)
|
||||||
|
|
||||||
|
// Copy selected discs to the inner Games folder (if not already exists)
|
||||||
|
let Disc1BINFileName: String = URL(string: Disc1TextField.stringValue)!.lastPathComponent
|
||||||
|
let Disc1CUEFileName: String = URL(string: Disc1CueFile)!.lastPathComponent
|
||||||
|
|
||||||
|
if FileManager.default.fileExists(atPath: Disc1TextField.stringValue) && !FileManager.default.fileExists(atPath: Utils().CDrivePath + "/Games/" + Disc1BINFileName) {
|
||||||
|
|
||||||
|
do {
|
||||||
|
try FileManager.default.copyItem(atPath: Disc1TextField.stringValue, toPath: Utils().CDrivePath + "/Games/" + Disc1BINFileName)
|
||||||
|
try FileManager.default.copyItem(atPath: Disc1CueFile, toPath: Utils().CDrivePath + "/Games/" + Disc1CUEFileName)
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Disc2TextField.stringValue.isEmpty {
|
||||||
|
Disc2CueFile = Disc2TextField.stringValue.replacingOccurrences(of: ".bin", with: ".cue")
|
||||||
|
ConfigWriter!.write(#"--image="data/disc2.bin"\#n"#.data(using: .utf8)!)
|
||||||
|
|
||||||
|
if FileManager.default.fileExists(atPath: Disc2TextField.stringValue) {
|
||||||
|
do {
|
||||||
|
let Disc2FileName: String = URL(string: Disc2TextField.stringValue)!.lastPathComponent
|
||||||
|
let Disc2CUEFileName: String = URL(string: Disc2CueFile)!.lastPathComponent
|
||||||
|
try FileManager.default.copyItem(atPath: Disc2TextField.stringValue, toPath: Utils().CDrivePath + "/Games/" + Disc2FileName)
|
||||||
|
try FileManager.default.copyItem(atPath: Disc2CueFile, toPath: Utils().CDrivePath + "/Games/" + Disc2CUEFileName)
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !Disc3TextField.stringValue.isEmpty {
|
||||||
|
Disc3CueFile = Disc3TextField.stringValue.replacingOccurrences(of: ".bin", with: ".cue")
|
||||||
|
ConfigWriter!.write(#"--image="data/disc3.bin"\#n"#.data(using: .utf8)!)
|
||||||
|
|
||||||
|
if FileManager.default.fileExists(atPath: Disc3TextField.stringValue) {
|
||||||
|
do {
|
||||||
|
let Disc3FileName: String = URL(string: Disc3TextField.stringValue)!.lastPathComponent
|
||||||
|
let Disc3CUEFileName: String = URL(string: Disc3CueFile)!.lastPathComponent
|
||||||
|
try FileManager.default.copyItem(atPath: Disc3TextField.stringValue, toPath: Utils().CDrivePath + "/Games/" + Disc3FileName)
|
||||||
|
try FileManager.default.copyItem(atPath: Disc3CueFile, toPath: Utils().CDrivePath + "/Games/" + Disc3CUEFileName)
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !Disc4TextField.stringValue.isEmpty {
|
||||||
|
Disc4CueFile = Disc4TextField.stringValue.replacingOccurrences(of: ".bin", with: ".cue")
|
||||||
|
ConfigWriter!.write(#"--image="data/disc4.bin"\#n"#.data(using: .utf8)!)
|
||||||
|
|
||||||
|
if FileManager.default.fileExists(atPath: Disc4TextField.stringValue) {
|
||||||
|
do {
|
||||||
|
let Disc4FileName: String = URL(string: Disc4TextField.stringValue)!.lastPathComponent
|
||||||
|
let Disc4CUEFileName: String = URL(string: Disc4CueFile)!.lastPathComponent
|
||||||
|
try FileManager.default.copyItem(atPath: Disc4TextField.stringValue, toPath: Utils().CDrivePath + "/Games/" + Disc4FileName)
|
||||||
|
try FileManager.default.copyItem(atPath: Disc4CueFile, toPath: Utils().CDrivePath + "/Games/" + Disc4CUEFileName)
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue += 10
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Checking for game protection"
|
||||||
|
usleep(150000)
|
||||||
|
|
||||||
|
// Check for game protection
|
||||||
|
var AdjustedGameID: String = CurrentGameID
|
||||||
|
AdjustedGameID.insert("_", at: AdjustedGameID.index(AdjustedGameID.startIndex, offsetBy: 4))
|
||||||
|
AdjustedGameID.insert(".", at: AdjustedGameID.index(AdjustedGameID.startIndex, offsetBy: 8))
|
||||||
|
var GameProtectionPatch: String = IsGameProtected(GameID: AdjustedGameID)
|
||||||
|
if !GameProtectionPatch.isEmpty {
|
||||||
|
let GameProtectionAlert = NSAlert()
|
||||||
|
GameProtectionAlert.messageText = "Please confirm"
|
||||||
|
GameProtectionAlert.informativeText = "This game is LibCrypt protected. Do you want to patch it automatically ?"
|
||||||
|
GameProtectionAlert.addButton(withTitle: "Yes")
|
||||||
|
GameProtectionAlert.addButton(withTitle: "No")
|
||||||
|
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Waiting for user input"
|
||||||
|
|
||||||
|
if GameProtectionAlert.runModal() == NSApplication.ModalResponse.alertFirstButtonReturn
|
||||||
|
{
|
||||||
|
GameProtectionPatch = "--libcrypt=" + GameProtectionPatch + "\n"
|
||||||
|
ConfigWriter!.write(GameProtectionPatch.data(using: .utf8)!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LUA config
|
||||||
|
if !LUAConfigTextField.stringValue.isEmpty {
|
||||||
|
let LUATitleID: String = "--ps1-title-id=" + NPTitleTextField.stringValue + "\n"
|
||||||
|
ConfigWriter!.write(LUATitleID.data(using: .utf8)!)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Graphics config
|
||||||
|
let UpscaleValue: String = "--scale=" + UpscalingComboBox.stringValue + "\n"
|
||||||
|
ConfigWriter!.write(UpscaleValue.data(using: .utf8)!)
|
||||||
|
|
||||||
|
if SkipBootlogoCheckBox.state == .on {
|
||||||
|
ConfigWriter!.write("--bios-hide-sce-osd=1\n".data(using: .utf8)!)
|
||||||
|
}
|
||||||
|
if EnableGunconCheckBox.state == .on {
|
||||||
|
ConfigWriter!.write("--guncon\n".data(using: .utf8)!)
|
||||||
|
}
|
||||||
|
if Force60HzCheckBox.state == .on {
|
||||||
|
ConfigWriter!.write("--gpu-scanout-fps-override=60\n".data(using: .utf8)!)
|
||||||
|
}
|
||||||
|
if EmulateAnalogSticksCheckBox.state == .on {
|
||||||
|
ConfigWriter!.write("--sim-analog-pad=0x2020\n".data(using: .utf8)!)
|
||||||
|
}
|
||||||
|
|
||||||
|
// User config
|
||||||
|
if !TXTConfigTextField.stringValue.isEmpty {
|
||||||
|
ConfigWriter!.write("#User imported config".data(using: .utf8)!)
|
||||||
|
do {
|
||||||
|
let UserConfigData = try String(contentsOfFile: TXTConfigTextField.stringValue, encoding: .utf8)
|
||||||
|
if !UserConfigData.isEmpty {
|
||||||
|
ConfigWriter!.write(UserConfigData.data(using: .utf8)!)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save config
|
||||||
|
ConfigWriter!.closeFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue += 10 //50
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "PS1 emulator configuration done. Creating now the param.sfo file, please wait"
|
||||||
|
usleep(150000)
|
||||||
|
|
||||||
|
// Create a new param.sfo file
|
||||||
|
CreateParamSFO(GameTitle: GameTitleTextField.stringValue)
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().ToolsPath + "/param.sfo") {
|
||||||
|
|
||||||
|
//Delete default param.sfo if exists
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().CDrivePath + "/Cache/PS1fPKG/sce_sys/param.sfo") {
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(atPath: Utils().CDrivePath + "/Cache/PS1fPKG/sce_sys/param.sfo")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the new param.sfo to the sce_sys folder
|
||||||
|
do {
|
||||||
|
try FileManager.default.moveItem(atPath: Utils().ToolsPath + "/param.sfo", toPath: Utils().CDrivePath + "/Cache/PS1fPKG/sce_sys/param.sfo")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue += 10 //60
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Param.sfo file created. Creating disc(s) TOC"
|
||||||
|
usleep(150000)
|
||||||
|
|
||||||
|
// Create a TOC file
|
||||||
|
CreateTOCFile()
|
||||||
|
|
||||||
|
let CU2Path: String = Utils().ToolsPath + "/" + URL(fileURLWithPath: Disc1TextField.stringValue).deletingPathExtension().lastPathComponent + ".cu2"
|
||||||
|
let TOCPath: String = Utils().ToolsPath + "/" + URL(fileURLWithPath: Disc1TextField.stringValue).deletingPathExtension().lastPathComponent + ".TOC"
|
||||||
|
|
||||||
|
if FileManager.default.fileExists(atPath: TOCPath) {
|
||||||
|
do {
|
||||||
|
try FileManager.default.moveItem(atPath: TOCPath, toPath: Utils().CDrivePath + "/Cache/PS1fPKG/data/disc1.toc")
|
||||||
|
try FileManager.default.removeItem(atPath: CU2Path)
|
||||||
|
} catch {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue += 10 //70
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Disc(s) TOC created. Proceeding to GP4 project creation"
|
||||||
|
usleep(150000)
|
||||||
|
|
||||||
|
// Generate a GP4 project and modify it
|
||||||
|
CreateGP4Project()
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().CDrivePath + "/Cache/PS1fPKG.gp4") {
|
||||||
|
|
||||||
|
let BaseFileNameWithExt: String = URL(fileURLWithPath: Disc1TextField.stringValue).lastPathComponent
|
||||||
|
let BaseFileName: String = BaseFileNameWithExt.replacingOccurrences(of: ".bin", with: "")
|
||||||
|
let Disc1CUEFilePath: String = "C:\\Games\\" + BaseFileName + ".cue"
|
||||||
|
let Disc1BINFilePath: String = "C:\\Games\\" + BaseFileName + ".bin"
|
||||||
|
|
||||||
|
let XMLDisc1CuePath = "\n <file targ_path=\"data/disc1.cue\" orig_path=\"\(Disc1CUEFilePath)\" pfs_compression=\"enable\"/>"
|
||||||
|
let XMLDisc1BinPath = "\n <file targ_path=\"data/disc1.bin\" orig_path=\"\(Disc1BINFilePath)\" pfs_compression=\"enable\"/>"
|
||||||
|
|
||||||
|
do {
|
||||||
|
var fileContents = try String(contentsOfFile: Utils().CDrivePath + "/Cache/PS1fPKG.gp4", encoding: .utf8)
|
||||||
|
fileContents = fileContents.replacingOccurrences(of: "<?xml version=\"1.1\"", with: "<?xml version=\"1.0\"")
|
||||||
|
fileContents = fileContents.replacingOccurrences(of: "<scenarios default_id=\"1\">", with: "<scenarios default_id=\"0\">")
|
||||||
|
fileContents = fileContents.replacingOccurrences(of: "</files>", with: "\(XMLDisc1CuePath)\(XMLDisc1BinPath)\n</files>")
|
||||||
|
|
||||||
|
try fileContents.write(toFile: Utils().CDrivePath + "/Cache/PS1fPKG.gp4", atomically: false, encoding: .utf8)
|
||||||
|
} catch {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Disc2TextField.stringValue.isEmpty {
|
||||||
|
let BaseFileNameWithExt: String = URL(fileURLWithPath: Disc2TextField.stringValue).lastPathComponent
|
||||||
|
let BaseFileName: String = BaseFileNameWithExt.replacingOccurrences(of: ".bin", with: "")
|
||||||
|
let Disc2CUEFilePath: String = "C:\\Games\\" + BaseFileName + ".cue"
|
||||||
|
let Disc2BINFilePath: String = "C:\\Games\\" + BaseFileName + ".bin"
|
||||||
|
|
||||||
|
let XMLDisc2CuePath = "\n <file targ_path=\"data/disc2.cue\" orig_path=\"\(Disc2CUEFilePath)\" pfs_compression=\"enable\"/>"
|
||||||
|
let XMLDisc2BinPath = "\n <file targ_path=\"data/disc2.bin\" orig_path=\"\(Disc2BINFilePath)\" pfs_compression=\"enable\"/>"
|
||||||
|
|
||||||
|
do {
|
||||||
|
var fileContents = try String(contentsOfFile: Utils().CDrivePath + "/Cache/PS1fPKG.gp4", encoding: .utf8)
|
||||||
|
fileContents = fileContents.replacingOccurrences(of: "</files>", with: "\(XMLDisc2CuePath)\(XMLDisc2BinPath)\n</files>")
|
||||||
|
try fileContents.write(toFile: Utils().CDrivePath + "/Cache/PS1fPKG.gp4", atomically: true, encoding: .utf8)
|
||||||
|
} catch {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Disc3TextField.stringValue.isEmpty {
|
||||||
|
let BaseFileNameWithExt: String = URL(fileURLWithPath: Disc3TextField.stringValue).lastPathComponent
|
||||||
|
let BaseFileName: String = BaseFileNameWithExt.replacingOccurrences(of: ".bin", with: "")
|
||||||
|
let Disc3CUEFilePath: String = "C:\\Games\\" + BaseFileName + ".cue"
|
||||||
|
let Disc3BINFilePath: String = "C:\\Games\\" + BaseFileName + ".bin"
|
||||||
|
|
||||||
|
let XMLDisc3CuePath = "\n <file targ_path=\"data/disc3.cue\" orig_path=\"\(Disc3CUEFilePath)\" pfs_compression=\"enable\"/>"
|
||||||
|
let XMLDisc3BinPath = "\n <file targ_path=\"data/disc3.bin\" orig_path=\"\(Disc3BINFilePath)\" pfs_compression=\"enable\"/>"
|
||||||
|
|
||||||
|
do {
|
||||||
|
var fileContents = try String(contentsOfFile: Utils().CDrivePath + "/Cache/PS1fPKG.gp4", encoding: .utf8)
|
||||||
|
fileContents = fileContents.replacingOccurrences(of: "</files>", with: "\(XMLDisc3CuePath)\(XMLDisc3BinPath)\n</files>")
|
||||||
|
try fileContents.write(toFile: Utils().CDrivePath + "/Cache/PS1fPKG.gp4", atomically: true, encoding: .utf8)
|
||||||
|
} catch {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Disc4TextField.stringValue.isEmpty {
|
||||||
|
let BaseFileNameWithExt: String = URL(fileURLWithPath: Disc4TextField.stringValue).lastPathComponent
|
||||||
|
let BaseFileName: String = BaseFileNameWithExt.replacingOccurrences(of: ".bin", with: "")
|
||||||
|
let Disc4CUEFilePath: String = "C:\\Games\\" + BaseFileName + ".cue"
|
||||||
|
let Disc4BINFilePath: String = "C:\\Games\\" + BaseFileName + ".bin"
|
||||||
|
|
||||||
|
let XMLDisc4CuePath = "\n <file targ_path=\"data/disc4.cue\" orig_path=\"\(Disc4CUEFilePath)\" pfs_compression=\"enable\"/>"
|
||||||
|
let XMLDisc4BinPath = "\n <file targ_path=\"data/disc4.bin\" orig_path=\"\(Disc4BINFilePath)\" pfs_compression=\"enable\"/>"
|
||||||
|
|
||||||
|
do {
|
||||||
|
var fileContents = try String(contentsOfFile: Utils().CDrivePath + "/Cache/PS1fPKG.gp4", encoding: .utf8)
|
||||||
|
fileContents = fileContents.replacingOccurrences(of: "</files>", with: "\(XMLDisc4CuePath)\(XMLDisc4BinPath)\n</files>")
|
||||||
|
try fileContents.write(toFile: Utils().CDrivePath + "/Cache/PS1fPKG.gp4", atomically: true, encoding: .utf8)
|
||||||
|
} catch {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue += 10 //80
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "GP4 project created. fPKG will be built after user confirmation"
|
||||||
|
usleep(100000)
|
||||||
|
|
||||||
|
let PKGReadyAlert = NSAlert()
|
||||||
|
PKGReadyAlert.messageText = "Files are ready for fPKG creation."
|
||||||
|
PKGReadyAlert.informativeText = "fPKG Ready"
|
||||||
|
PKGReadyAlert.addButton(withTitle: "Continue")
|
||||||
|
PKGReadyAlert.runModal()
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.isIndeterminate = true
|
||||||
|
fPKGBuilderProgressIndicator.startAnimation(nil)
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "The fPKG is building, please wait until done"
|
||||||
|
usleep(150000)
|
||||||
|
|
||||||
|
// Create the fPKG
|
||||||
|
BuildPKG()
|
||||||
|
|
||||||
|
// Check build
|
||||||
|
let PKGFileName: String = "UP9000-" + CurrentGameID + "_00-" + CurrentGameID + "PS1FPKG-A0100-V0100.pkg"
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().CDrivePath + "/Cache/" + PKGFileName) {
|
||||||
|
|
||||||
|
// Move the created fPKG into the selected output folder
|
||||||
|
do {
|
||||||
|
try FileManager.default.moveItem(atPath: Utils().CDrivePath + "/Cache/" + PKGFileName, toPath: PKGOutputPath + "/" + PKGFileName)
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.stopAnimation(nil)
|
||||||
|
fPKGBuilderProgressIndicator.isIndeterminate = false
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue = 100 //100
|
||||||
|
fPKGBuilderSpinningIndicator.stopAnimation(nil)
|
||||||
|
fPKGBuilderSpinningIndicator.isHidden = true
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Done"
|
||||||
|
|
||||||
|
let SuccessAlert = NSAlert()
|
||||||
|
SuccessAlert.messageText = "fPKG created with success!"
|
||||||
|
SuccessAlert.informativeText = "Done"
|
||||||
|
SuccessAlert.addButton(withTitle: "Continue")
|
||||||
|
SuccessAlert.runModal()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindGameTitle(GameID: String) -> String {
|
||||||
|
if !GameID.isEmpty {
|
||||||
|
do {
|
||||||
|
let PS1DBContent = try String(contentsOfFile: Utils().CDrivePath + "/PS4/ps1ids.txt", encoding: .utf8)
|
||||||
|
let GameIDs = PS1DBContent.components(separatedBy: .newlines)
|
||||||
|
|
||||||
|
var FoundTitle: String = ""
|
||||||
|
for FoundGameID in GameIDs {
|
||||||
|
if FoundGameID.contains(GameID) {
|
||||||
|
FoundTitle = FoundGameID.components(separatedBy: ";")[1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if FoundTitle.isEmpty { return "" }
|
||||||
|
else { return FoundTitle }
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
print(error)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsGameProtected(GameID: String) -> String {
|
||||||
|
if !GameID.isEmpty {
|
||||||
|
do {
|
||||||
|
let LibCrData = try String(contentsOfFile: Utils().CDrivePath + "/PS4/libcrypt.txt", encoding: .utf8)
|
||||||
|
let GameIDs = LibCrData.components(separatedBy: .newlines)
|
||||||
|
|
||||||
|
var FoundPatch: String = ""
|
||||||
|
for FoundGameID in GameIDs {
|
||||||
|
if FoundGameID.contains(GameID) {
|
||||||
|
FoundPatch = FoundGameID.components(separatedBy: " ")[1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if FoundPatch.isEmpty { return "" }
|
||||||
|
else { return FoundPatch }
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
print(error)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateParamSFO(GameTitle: String) {
|
||||||
|
let SFOUtil = Bundle.main.path(forResource: "sfoutil", ofType: "")
|
||||||
|
let ContentID: String = "UP9000-" + CurrentGameID + "_00-" + CurrentGameID + "PS1FPKG"
|
||||||
|
let DestinationPath: String = "'" + Utils().ToolsPath + "/param.sfo'"
|
||||||
|
let task = Process()
|
||||||
|
|
||||||
|
task.launchPath = "/bin/sh"
|
||||||
|
task.arguments = ["-c", "'" + SFOUtil! + "' --force --new-file " + DestinationPath + " --add int APP_TYPE 1 --add str APP_VER \"01.00\" --add int ATTRIBUTE 0 --add str CATEGORY \"gd\" --add str CONTENT_ID \"\(ContentID)\" --add int DOWNLOAD_DATA_SIZE 0 --add str FORMAT \"obs\" --add int PARENTAL_LEVEL 5 --add int SYSTEM_VER 0 --add str TITLE \"" + GameTitle + "\" --add str TITLE_ID \"\(NPTitleTextField.stringValue)\" --add str VERSION \"01.00\""]
|
||||||
|
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateTOCFile() {
|
||||||
|
let task = Process()
|
||||||
|
|
||||||
|
let GameBINFileName: String = Disc1CueFile.replacingOccurrences(of: ".cue", with: ".bin")
|
||||||
|
var GameBINFileSize: String = ""
|
||||||
|
if let GameFileAttributes = try? FileManager.default.attributesOfItem(atPath: GameBINFileName) {
|
||||||
|
if let GameFileSizeAsBytes = GameFileAttributes[.size] as? Int64 {
|
||||||
|
let NewByteCountFormatter = ByteCountFormatter()
|
||||||
|
NewByteCountFormatter.allowedUnits = [.useBytes]
|
||||||
|
NewByteCountFormatter.countStyle = .file
|
||||||
|
NewByteCountFormatter.includesUnit = false
|
||||||
|
NewByteCountFormatter.zeroPadsFractionDigits = true
|
||||||
|
let FileSizeInBytes = NewByteCountFormatter.string(fromByteCount: GameFileSizeAsBytes)
|
||||||
|
GameBINFileSize = FileSizeInBytes.replacingOccurrences(of: ".", with: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task.launchPath = "/bin/sh"
|
||||||
|
task.currentDirectoryPath = Utils().ToolsPath
|
||||||
|
task.arguments = ["-c", "'" + Utils().ToolsPath + "/cue2toc' '" + Disc1CueFile + "' --size " + GameBINFileSize]
|
||||||
|
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateGP4Project() {
|
||||||
|
let task = Process()
|
||||||
|
|
||||||
|
task.launchPath = "/bin/sh"
|
||||||
|
task.arguments = ["-c", "'" + Utils().ToolsPath + "/gengp4_ps1'"]
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildPKG() {
|
||||||
|
let task = Process()
|
||||||
|
|
||||||
|
task.launchPath = "/bin/sh"
|
||||||
|
task.arguments = ["-c", "'" + Utils().ToolsPath + "/make_fPKG_PS1'"]
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,838 @@
|
|||||||
|
//
|
||||||
|
// PS2ClassicsViewController.swift
|
||||||
|
// PS Mac Tools
|
||||||
|
//
|
||||||
|
// Created by SvenGDK on 14/08/2024.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AppKit
|
||||||
|
|
||||||
|
class PS2ClassicsViewController: NSViewController {
|
||||||
|
|
||||||
|
var CurrentGameID: String = ""
|
||||||
|
var CurrentGameCRC: String = ""
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
// Set default values
|
||||||
|
EmulatorsComboBox.selectItem(at: 0)
|
||||||
|
MultitapComboBox.selectItem(at: 0)
|
||||||
|
UpscalingComboBox.selectItem(at: 0)
|
||||||
|
UprenderComboBox.selectItem(at: 1)
|
||||||
|
DisplayModeComboBox.selectItem(at: 0)
|
||||||
|
ShaderComboBox.selectItem(at: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override var representedObject: Any? {
|
||||||
|
didSet {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBOutlet weak var BuildfPKGButton: NSButton!
|
||||||
|
@IBOutlet weak var fPKGBuilderProgressIndicator: NSProgressIndicator!
|
||||||
|
@IBOutlet weak var fPKGBuilderSpinningIndicator: NSProgressIndicator!
|
||||||
|
@IBOutlet weak var fPKGBuilderStatusTextField: NSTextField!
|
||||||
|
|
||||||
|
|
||||||
|
@IBOutlet weak var GameTitleTextField: NSTextField!
|
||||||
|
@IBOutlet weak var NPTitleTextField: NSTextField!
|
||||||
|
@IBOutlet weak var IconTextField: NSTextField!
|
||||||
|
@IBOutlet weak var BackgroundTextField: NSTextField!
|
||||||
|
@IBOutlet weak var Disc1TextField: NSTextField!
|
||||||
|
@IBOutlet weak var Disc2TextField: NSTextField!
|
||||||
|
@IBOutlet weak var Disc3TextField: NSTextField!
|
||||||
|
@IBOutlet weak var Disc4TextField: NSTextField!
|
||||||
|
@IBOutlet weak var Disc5TextField: NSTextField!
|
||||||
|
@IBOutlet weak var TXTConfigTextField: NSTextField!
|
||||||
|
@IBOutlet weak var LUAConfigTextField: NSTextField!
|
||||||
|
@IBOutlet weak var UpscalingComboBox: NSComboBox!
|
||||||
|
@IBOutlet weak var UprenderComboBox: NSComboBox!
|
||||||
|
@IBOutlet weak var ShaderComboBox: NSComboBox!
|
||||||
|
@IBOutlet weak var DisplayModeComboBox: NSComboBox!
|
||||||
|
@IBOutlet weak var FixGraphicsCheckBox: NSButton!
|
||||||
|
@IBOutlet weak var ImproveSpeedCheckBox: NSButton!
|
||||||
|
@IBOutlet weak var DisableMTVUCheckBox: NSButton!
|
||||||
|
@IBOutlet weak var DisableInstantVIF1TransferCheckBox: NSButton!
|
||||||
|
@IBOutlet weak var PS2MemoryCardTextField: NSTextField!
|
||||||
|
@IBOutlet weak var EmulatorsComboBox: NSComboBox!
|
||||||
|
@IBOutlet weak var MultitapComboBox: NSComboBox!
|
||||||
|
@IBOutlet weak var RestartEmuOnDiscChangeCheckBox: NSButton!
|
||||||
|
|
||||||
|
@IBAction func BrowseIcon(_ sender: NSButton) {
|
||||||
|
let dialog = NSOpenPanel()
|
||||||
|
|
||||||
|
dialog.title = "Select an icon for your game"
|
||||||
|
dialog.showsResizeIndicator = true
|
||||||
|
dialog.showsHiddenFiles = false
|
||||||
|
dialog.canChooseFiles = true
|
||||||
|
dialog.canChooseDirectories = false
|
||||||
|
dialog.allowsMultipleSelection = false
|
||||||
|
dialog.allowedContentTypes = [Utils().PNGType]
|
||||||
|
|
||||||
|
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = dialog.url
|
||||||
|
IconTextField.stringValue = result!.path
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func BrowseBackground(_ sender: NSButton) {
|
||||||
|
let dialog = NSOpenPanel()
|
||||||
|
|
||||||
|
dialog.title = "Select a background for your game"
|
||||||
|
dialog.showsResizeIndicator = true
|
||||||
|
dialog.showsHiddenFiles = false
|
||||||
|
dialog.canChooseFiles = true
|
||||||
|
dialog.canChooseDirectories = false
|
||||||
|
dialog.allowsMultipleSelection = false
|
||||||
|
dialog.allowedContentTypes = [Utils().PNGType]
|
||||||
|
|
||||||
|
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = dialog.url
|
||||||
|
BackgroundTextField.stringValue = result!.path
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func BrowseDisc1(_ sender: NSButton) {
|
||||||
|
let dialog = NSOpenPanel()
|
||||||
|
|
||||||
|
dialog.title = "Select your PS2 ISO file"
|
||||||
|
dialog.showsResizeIndicator = true
|
||||||
|
dialog.showsHiddenFiles = false
|
||||||
|
dialog.canChooseFiles = true
|
||||||
|
dialog.canChooseDirectories = false
|
||||||
|
dialog.allowsMultipleSelection = false
|
||||||
|
dialog.allowedContentTypes = [Utils().ISOType]
|
||||||
|
|
||||||
|
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = dialog.url
|
||||||
|
Disc1TextField.stringValue = result!.path
|
||||||
|
|
||||||
|
// Read ISO file using isoinfo
|
||||||
|
let ISOReaderOutput = CheckPS2Game(ISOFile: result!.path).output
|
||||||
|
|
||||||
|
// Get the game ID
|
||||||
|
let GameID = ISOReaderOutput.filter({(item: String) -> Bool in
|
||||||
|
let stringMatch = item.lowercased().range(of: "BOOT2 = ".lowercased())
|
||||||
|
let stringMatch2 = item.lowercased().range(of: "BOOT2=".lowercased())
|
||||||
|
if stringMatch == nil {
|
||||||
|
return stringMatch2 != nil ? true : false
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return stringMatch != nil ? true : false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get the game title if a game ID has been found
|
||||||
|
if !GameID.isEmpty {
|
||||||
|
if GameID.count > 0 {
|
||||||
|
CurrentGameID = FormatGameID(GameID: GameID[0].components(separatedBy: "=")[1])
|
||||||
|
let GameTitle: String = FindGameTitle(GameID: CurrentGameID)
|
||||||
|
NPTitleTextField.stringValue = CurrentGameID
|
||||||
|
GameTitleTextField.stringValue = GameTitle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildfPKGButton.isEnabled = true
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func BrowseDisc2(_ sender: NSButton) {
|
||||||
|
let dialog = NSOpenPanel()
|
||||||
|
|
||||||
|
dialog.title = "Select the Disc 2 ISO file"
|
||||||
|
dialog.showsResizeIndicator = true
|
||||||
|
dialog.showsHiddenFiles = false
|
||||||
|
dialog.canChooseFiles = true
|
||||||
|
dialog.canChooseDirectories = false
|
||||||
|
dialog.allowsMultipleSelection = false
|
||||||
|
dialog.allowedContentTypes = [Utils().ISOType]
|
||||||
|
|
||||||
|
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = dialog.url
|
||||||
|
Disc2TextField.stringValue = result!.path
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func BrowseDisc3(_ sender: NSButton) {
|
||||||
|
let dialog = NSOpenPanel()
|
||||||
|
|
||||||
|
dialog.title = "Select the Disc 3 ISO file"
|
||||||
|
dialog.showsResizeIndicator = true
|
||||||
|
dialog.showsHiddenFiles = false
|
||||||
|
dialog.canChooseFiles = true
|
||||||
|
dialog.canChooseDirectories = false
|
||||||
|
dialog.allowsMultipleSelection = false
|
||||||
|
dialog.allowedContentTypes = [Utils().ISOType]
|
||||||
|
|
||||||
|
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = dialog.url
|
||||||
|
Disc3TextField.stringValue = result!.path
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func BrowseDisc4(_ sender: NSButton) {
|
||||||
|
let dialog = NSOpenPanel()
|
||||||
|
|
||||||
|
dialog.title = "Select the Disc 4 ISO file"
|
||||||
|
dialog.showsResizeIndicator = true
|
||||||
|
dialog.showsHiddenFiles = false
|
||||||
|
dialog.canChooseFiles = true
|
||||||
|
dialog.canChooseDirectories = false
|
||||||
|
dialog.allowsMultipleSelection = false
|
||||||
|
dialog.allowedContentTypes = [Utils().ISOType]
|
||||||
|
|
||||||
|
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = dialog.url
|
||||||
|
Disc4TextField.stringValue = result!.path
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func BrowseDisc5(_ sender: NSButton) {
|
||||||
|
let dialog = NSOpenPanel()
|
||||||
|
|
||||||
|
dialog.title = "Select the Disc 5 ISO file"
|
||||||
|
dialog.showsResizeIndicator = true
|
||||||
|
dialog.showsHiddenFiles = false
|
||||||
|
dialog.canChooseFiles = true
|
||||||
|
dialog.canChooseDirectories = false
|
||||||
|
dialog.allowsMultipleSelection = false
|
||||||
|
dialog.allowedContentTypes = [Utils().ISOType]
|
||||||
|
|
||||||
|
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = dialog.url
|
||||||
|
Disc5TextField.stringValue = result!.path
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func BrowseTXTConfig(_ sender: NSButton) {
|
||||||
|
let dialog = NSOpenPanel()
|
||||||
|
|
||||||
|
dialog.title = "Select a TXT config file"
|
||||||
|
dialog.showsResizeIndicator = true
|
||||||
|
dialog.showsHiddenFiles = false
|
||||||
|
dialog.canChooseFiles = true
|
||||||
|
dialog.canChooseDirectories = false
|
||||||
|
dialog.allowsMultipleSelection = false
|
||||||
|
dialog.allowedContentTypes = [Utils().TXTType]
|
||||||
|
|
||||||
|
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = dialog.url
|
||||||
|
TXTConfigTextField.stringValue = result!.path
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func BrowseLUAConfig(_ sender: NSButton) {
|
||||||
|
let dialog = NSOpenPanel()
|
||||||
|
|
||||||
|
dialog.title = "Select a LUA config file"
|
||||||
|
dialog.showsResizeIndicator = true
|
||||||
|
dialog.showsHiddenFiles = false
|
||||||
|
dialog.canChooseFiles = true
|
||||||
|
dialog.canChooseDirectories = false
|
||||||
|
dialog.allowsMultipleSelection = false
|
||||||
|
dialog.allowedContentTypes = [Utils().TXTType]
|
||||||
|
|
||||||
|
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = dialog.url
|
||||||
|
LUAConfigTextField.stringValue = result!.path
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func BrowseMemoryCard(_ sender: NSButton) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckPS2Game(ISOFile: String) -> (output: [String], error: [String], exitCode: Int32) {
|
||||||
|
var output : [String] = []
|
||||||
|
var error : [String] = []
|
||||||
|
|
||||||
|
let isoutil = Bundle.main.path(forResource: "isoinfo", ofType: "")
|
||||||
|
let task = Process()
|
||||||
|
task.launchPath = "/bin/sh"
|
||||||
|
task.arguments = ["-c", "'" + isoutil! + "' -i '" + ISOFile + "' -x '/SYSTEM.CNF;1'"]
|
||||||
|
|
||||||
|
let outpipe = Pipe()
|
||||||
|
task.standardOutput = outpipe
|
||||||
|
let errpipe = Pipe()
|
||||||
|
task.standardError = errpipe
|
||||||
|
|
||||||
|
task.launch()
|
||||||
|
|
||||||
|
let outdata = outpipe.fileHandleForReading.readDataToEndOfFile()
|
||||||
|
if var string = String(data: outdata, encoding: .utf8) {
|
||||||
|
string = string.trimmingCharacters(in: .newlines)
|
||||||
|
output = string.components(separatedBy: "\n")
|
||||||
|
print(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
let errdata = errpipe.fileHandleForReading.readDataToEndOfFile()
|
||||||
|
if var string = String(data: errdata, encoding: .utf8) {
|
||||||
|
string = string.trimmingCharacters(in: .newlines)
|
||||||
|
error = string.components(separatedBy: "\n")
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
task.waitUntilExit()
|
||||||
|
let status = task.terminationStatus
|
||||||
|
return (output, error, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindGameTitle(GameID: String) -> String {
|
||||||
|
if !GameID.isEmpty {
|
||||||
|
do {
|
||||||
|
let PS2DBContent = try String(contentsOfFile: Utils().CDrivePath + "/PS4/ps2ids.txt", encoding: .utf8)
|
||||||
|
let GameIDs = PS2DBContent.components(separatedBy: .newlines)
|
||||||
|
|
||||||
|
var FoundTitle: String = ""
|
||||||
|
for FoundGameID in GameIDs {
|
||||||
|
if FoundGameID.contains(GameID) {
|
||||||
|
FoundTitle = FoundGameID.components(separatedBy: ";")[1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if FoundTitle.isEmpty { return "" }
|
||||||
|
else { return FoundTitle }
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
print(error)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormatGameID(GameID: String) -> String {
|
||||||
|
return GameID.components(separatedBy: ":\\")[1].replacingOccurrences(of: ".", with: "", options: NSString.CompareOptions.literal, range: nil).replacingOccurrences(of: "_", with: "", options: NSString.CompareOptions.literal, range: nil).replacingOccurrences(of: ";1", with: "", options: NSString.CompareOptions.literal, range: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func BuildfPKG(_ sender: NSButton) {
|
||||||
|
|
||||||
|
let MissingInfoAlert = NSAlert()
|
||||||
|
MissingInfoAlert.addButton(withTitle: "Close")
|
||||||
|
|
||||||
|
if Disc1TextField.stringValue.isEmpty {
|
||||||
|
MissingInfoAlert.messageText = "Disc 1 is missing, cannot continue."
|
||||||
|
MissingInfoAlert.runModal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if GameTitleTextField.stringValue.isEmpty {
|
||||||
|
MissingInfoAlert.messageText = "The Game Title is missing, cannot continue."
|
||||||
|
MissingInfoAlert.runModal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if NPTitleTextField.stringValue.isEmpty {
|
||||||
|
MissingInfoAlert.messageText = "The NP Title is missing, cannot continue."
|
||||||
|
MissingInfoAlert.runModal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.isHidden = false
|
||||||
|
fPKGBuilderSpinningIndicator.isHidden = false
|
||||||
|
fPKGBuilderStatusTextField.isHidden = false
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.isIndeterminate = true
|
||||||
|
fPKGBuilderProgressIndicator.startAnimation(nil)
|
||||||
|
fPKGBuilderSpinningIndicator.startAnimation(nil)
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Waiting for user input"
|
||||||
|
|
||||||
|
CreatefPKG()
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreatefPKG() {
|
||||||
|
|
||||||
|
var PKGOutputPath: String = ""
|
||||||
|
let FolderBrowserOpenPanel = NSOpenPanel()
|
||||||
|
|
||||||
|
FolderBrowserOpenPanel.title = "Select an output folder"
|
||||||
|
FolderBrowserOpenPanel.showsResizeIndicator = true
|
||||||
|
FolderBrowserOpenPanel.showsHiddenFiles = false
|
||||||
|
FolderBrowserOpenPanel.canChooseFiles = false
|
||||||
|
FolderBrowserOpenPanel.canChooseDirectories = true
|
||||||
|
FolderBrowserOpenPanel.allowsMultipleSelection = false
|
||||||
|
|
||||||
|
// Set destination folder where the pkg file will be moved into
|
||||||
|
if (FolderBrowserOpenPanel.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = FolderBrowserOpenPanel.url
|
||||||
|
PKGOutputPath = result!.path
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set disc count
|
||||||
|
var DiscCount: Int = 1
|
||||||
|
if !Disc2TextField.stringValue.isEmpty {
|
||||||
|
DiscCount += 1
|
||||||
|
}
|
||||||
|
if !Disc3TextField.stringValue.isEmpty {
|
||||||
|
DiscCount += 1
|
||||||
|
}
|
||||||
|
if !Disc4TextField.stringValue.isEmpty {
|
||||||
|
DiscCount += 1
|
||||||
|
}
|
||||||
|
if !Disc5TextField.stringValue.isEmpty {
|
||||||
|
DiscCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.stopAnimation(nil)
|
||||||
|
fPKGBuilderProgressIndicator.isIndeterminate = false
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue = 0
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Removing old fPKG build folder"
|
||||||
|
usleep(150000)
|
||||||
|
|
||||||
|
// Remove previous fPKG GP4 project
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().CDrivePath + "/Cache/PS2fPKG.gp4") {
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(atPath: Utils().CDrivePath + "/Cache/PS2fPKG.gp4")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove previous fPKG project folder
|
||||||
|
var isDir: ObjCBool = true
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().CDrivePath + "/Cache/PS2fPKG", isDirectory: &isDir) {
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(atPath: Utils().CDrivePath + "/Cache/PS2fPKG")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue += 10
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Preparing the PS2 emulator"
|
||||||
|
usleep(150000)
|
||||||
|
|
||||||
|
// Copy the selected PS2 emulator to the new project folder
|
||||||
|
if EmulatorsComboBox.stringValue == "Jak v2" {
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().CDrivePath + "/PS4/emus/Jakv2", isDirectory: &isDir) {
|
||||||
|
do {
|
||||||
|
try FileManager.default.copyItem(atPath: Utils().CDrivePath + "/PS4/emus/Jakv2", toPath: Utils().CDrivePath + "/Cache/PS2fPKG")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if EmulatorsComboBox.stringValue == "Rogue v1" {
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().CDrivePath + "/PS4/emus/Roguev1", isDirectory: &isDir) {
|
||||||
|
do {
|
||||||
|
try FileManager.default.copyItem(atPath: Utils().CDrivePath + "/PS4/emus/Roguev1", toPath: Utils().CDrivePath + "/Cache/PS2fPKG")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the selected icon to the project folder
|
||||||
|
if !IconTextField.stringValue.isEmpty {
|
||||||
|
do {
|
||||||
|
try FileManager.default.copyItem(atPath: IconTextField.stringValue, toPath: Utils().CDrivePath + "/Cache/PS2fPKG/sce_sys/icon0.png")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the selected background to the project folder
|
||||||
|
if !BackgroundTextField.stringValue.isEmpty {
|
||||||
|
do {
|
||||||
|
try FileManager.default.copyItem(atPath: IconTextField.stringValue, toPath: Utils().CDrivePath + "/Cache/PS2fPKG/sce_sys/pic0.png")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue += 10 //20
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Creating param.sfo file, please wait"
|
||||||
|
usleep(150000)
|
||||||
|
|
||||||
|
// Create a new param.sfo file
|
||||||
|
CreateParamSFO(GameTitle: GameTitleTextField.stringValue)
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().ToolsPath + "/param.sfo") {
|
||||||
|
|
||||||
|
//Delete default param.sfo if exists
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().CDrivePath + "/Cache/PS2fPKG/sce_sys/param.sfo") {
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(atPath: Utils().CDrivePath + "/Cache/PS2fPKG/sce_sys/param.sfo")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the new param.sfo to the sce_sys folder
|
||||||
|
do {
|
||||||
|
try FileManager.default.moveItem(atPath: Utils().ToolsPath + "/param.sfo", toPath: Utils().CDrivePath + "/Cache/PS2fPKG/sce_sys/param.sfo")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue += 10 //30
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Configurating PS2 Emulator, please wait"
|
||||||
|
usleep(150000)
|
||||||
|
|
||||||
|
// If config already exists, remove it
|
||||||
|
let ConfigDestinationPath: String = Utils().CDrivePath + "/Cache/PS2fPKG/config-emu-ps4.txt"
|
||||||
|
if FileManager.default.fileExists(atPath: ConfigDestinationPath)
|
||||||
|
{
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(atPath: ConfigDestinationPath)
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new config file
|
||||||
|
FileManager.default.createFile(atPath: ConfigDestinationPath, contents:Data(" ".utf8), attributes: nil)
|
||||||
|
let ConfigWriter = FileHandle(forWritingAtPath: ConfigDestinationPath)
|
||||||
|
if ConfigWriter == nil { }
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Required config
|
||||||
|
let NewPS2EmulatorConfig: [String] = [
|
||||||
|
"--path-vmc=\"/tmp/vmc\"\n",
|
||||||
|
"--config-local-lua=\"\"\n",
|
||||||
|
"--ps2-title-id=" + CurrentGameID,
|
||||||
|
"\n--max-disc-num=" + String(DiscCount),
|
||||||
|
"\n--gs-uprender=" + UprenderComboBox.stringValue.lowercased(),
|
||||||
|
"\n--gs-upscale=" + UpscalingComboBox.stringValue.lowercased(),
|
||||||
|
"\n--host-audio=1\n",
|
||||||
|
"--rom=\"PS20220WD20050620.crack\"\n",
|
||||||
|
"--verbose-cdvd-reads=0\n",
|
||||||
|
"--host-display-mode=" + DisplayModeComboBox.stringValue.lowercased()
|
||||||
|
]
|
||||||
|
let NewPS2EmulatorConfigContent: String = NewPS2EmulatorConfig.joined()
|
||||||
|
ConfigWriter!.write(NewPS2EmulatorConfigContent.data(using: .utf8)!)
|
||||||
|
|
||||||
|
// Restart emulator on disc change config
|
||||||
|
if RestartEmuOnDiscChangeCheckBox.state == .on {
|
||||||
|
ConfigWriter!.write("#Disable emu reset on disc change".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--switch-disc-reset=0".data(using: .utf8)!)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multitap config
|
||||||
|
if MultitapComboBox.indexOfSelectedItem == 1 {
|
||||||
|
ConfigWriter!.write("#Enable Multitap on port 1".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--mtap1=always".data(using: .utf8)!)
|
||||||
|
}
|
||||||
|
else if MultitapComboBox.indexOfSelectedItem == 2 {
|
||||||
|
ConfigWriter!.write("#Enable Multitap on port 2".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--mtap2=always".data(using: .utf8)!)
|
||||||
|
}
|
||||||
|
else if MultitapComboBox.indexOfSelectedItem == 3 {
|
||||||
|
ConfigWriter!.write("#Enable Multitap on both ports".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--mtap1=always".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--mtap2=always".data(using: .utf8)!)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emulator fixes
|
||||||
|
if ImproveSpeedCheckBox.state == .on {
|
||||||
|
ConfigWriter!.write("#Improve Speed".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("-vu0-opt-flags=1".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--vu1-opt-flags=1".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--cop2-opt-flags=1".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--vu0-const-prop=0".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--vu1-const-prop=0".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--vu1-jr-cache-policy=newprog".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--vu1-jalr-cache-policy=newprog".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--vu0-jr-cache-policy=newprog".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--vu0-jalr-cache-policy=newprog".data(using: .utf8)!)
|
||||||
|
}
|
||||||
|
if FixGraphicsCheckBox.state == .on {
|
||||||
|
ConfigWriter!.write("#Fix Graphics".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--fpu-no-clamping=0".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--fpu-clamp-results=1".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--vu0-no-clamping=0".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--vu0-clamp-results=1".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--vu1-no-clamping=0".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--vu1-clamp-results=1".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--cop2-no-clamping=0".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--cop2-clamp-results=1".data(using: .utf8)!)
|
||||||
|
}
|
||||||
|
if DisableMTVUCheckBox.state == .on {
|
||||||
|
ConfigWriter!.write("#Disable MTVU".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--vu1=jit-sync".data(using: .utf8)!)
|
||||||
|
}
|
||||||
|
if DisableInstantVIF1TransferCheckBox.state == .on {
|
||||||
|
ConfigWriter!.write("#Disable Instant VIF1 Transfer".data(using: .utf8)!)
|
||||||
|
ConfigWriter!.write("--vif1-instant-xfer=0".data(using: .utf8)!)
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigWriter!.closeFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append custom config
|
||||||
|
if !TXTConfigTextField.stringValue.isEmpty {
|
||||||
|
let AdditionalConfig = try? String(contentsOfFile: TXTConfigTextField.stringValue, encoding: .utf8)
|
||||||
|
if let NewFileHandle = FileHandle(forWritingAtPath: ConfigDestinationPath) {
|
||||||
|
NewFileHandle.seekToEndOfFile()
|
||||||
|
if let DataToWrite = AdditionalConfig!.data(using: .utf8) {
|
||||||
|
NewFileHandle.write("#User Config".data(using: .utf8)!)
|
||||||
|
NewFileHandle.write(DataToWrite)
|
||||||
|
}
|
||||||
|
NewFileHandle.closeFile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Memory Card if selected
|
||||||
|
if !PS2MemoryCardTextField.stringValue.isEmpty {
|
||||||
|
|
||||||
|
var AdjustedGameID: String = CurrentGameID
|
||||||
|
AdjustedGameID.insert("-", at: AdjustedGameID.index(AdjustedGameID.startIndex, offsetBy: 4))
|
||||||
|
|
||||||
|
// Create feature_data folder
|
||||||
|
if !FileManager.default.fileExists(atPath: Utils().CDrivePath + "/Cache/PS2fPKG/feature_data", isDirectory: &isDir) {
|
||||||
|
do {
|
||||||
|
try FileManager.default.createDirectory(atPath: Utils().CDrivePath + "/Cache/PS2fPKG/feature_data", withIntermediateDirectories: false)
|
||||||
|
try FileManager.default.createDirectory(atPath: Utils().CDrivePath + "/Cache/PS2fPKG/feature_data/" + AdjustedGameID, withIntermediateDirectories: false)
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy Memory Card to feature_data
|
||||||
|
if FileManager.default.fileExists(atPath: PS2MemoryCardTextField.stringValue) {
|
||||||
|
do {
|
||||||
|
try FileManager.default.copyItem(atPath: PS2MemoryCardTextField.stringValue, toPath: Utils().CDrivePath + "/Cache/PS2fPKG/feature_data/" + AdjustedGameID + "/custom.card")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if !FileManager.default.fileExists(atPath: Utils().CDrivePath + "/Cache/PS2fPKG/image", isDirectory: &isDir) {
|
||||||
|
do {
|
||||||
|
try FileManager.default.createDirectory(atPath: Utils().CDrivePath + "/Cache/PS2fPKG/image", withIntermediateDirectories: false)
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue += 10 //40
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "PS2 emulator configuration done. Creating now the GP4 project"
|
||||||
|
usleep(150000)
|
||||||
|
|
||||||
|
// Generate a GP4 project and modify it
|
||||||
|
CreateGP4Project()
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().CDrivePath + "/Cache/PS2fPKG.gp4") {
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue += 20 //60
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "GP4 project created. Preparing disc(s)"
|
||||||
|
usleep(100000)
|
||||||
|
|
||||||
|
var FullDiscInfo: String = ""
|
||||||
|
|
||||||
|
// Copy selected discs to the inner Games folder (if not already exists) & add to GP4 project file
|
||||||
|
let Disc1BaseFileNameWithExt: String = URL(fileURLWithPath: Disc1TextField.stringValue).lastPathComponent
|
||||||
|
let Disc1BaseFileName: String = Disc1BaseFileNameWithExt.replacingOccurrences(of: ".iso", with: "")
|
||||||
|
let Disc1ISOFilePath: String = "C:\\Games\\" + Disc1BaseFileName + ".iso"
|
||||||
|
let Disc1ISOFileName: String = URL(string: Disc1TextField.stringValue)!.lastPathComponent
|
||||||
|
if FileManager.default.fileExists(atPath: Disc1TextField.stringValue) && !FileManager.default.fileExists(atPath: Utils().CDrivePath + "/Games/" + Disc1ISOFileName) {
|
||||||
|
do {
|
||||||
|
try FileManager.default.copyItem(atPath: Disc1TextField.stringValue, toPath: Utils().CDrivePath + "/Games/" + Disc1ISOFileName)
|
||||||
|
let NewDiscInfo = [FullDiscInfo, "\n <file targ_path=\"image/disc01.iso\" orig_path=\"\(Disc1ISOFilePath)\" pfs_compression=\"enable\"/>"]
|
||||||
|
FullDiscInfo = NewDiscInfo.joined()
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !Disc2TextField.stringValue.isEmpty {
|
||||||
|
let Disc2BaseFileNameWithExt: String = URL(fileURLWithPath: Disc2TextField.stringValue).lastPathComponent
|
||||||
|
let Disc2BaseFileName: String = Disc2BaseFileNameWithExt.replacingOccurrences(of: ".iso", with: "")
|
||||||
|
let Disc2ISOFilePath: String = "C:\\Games\\" + Disc2BaseFileName + ".iso"
|
||||||
|
let Disc2ISOFileName: String = URL(string: Disc2TextField.stringValue)!.lastPathComponent
|
||||||
|
if FileManager.default.fileExists(atPath: Disc2TextField.stringValue) && !FileManager.default.fileExists(atPath: Utils().CDrivePath + "/Games/" + Disc2ISOFileName) {
|
||||||
|
do {
|
||||||
|
try FileManager.default.copyItem(atPath: Disc2TextField.stringValue, toPath: Utils().CDrivePath + "/Games/" + Disc2ISOFileName)
|
||||||
|
let NewDiscInfo = [FullDiscInfo, "\n <file targ_path=\"image/disc02.iso\" orig_path=\"\(Disc2ISOFilePath)\" pfs_compression=\"enable\"/>"]
|
||||||
|
FullDiscInfo = NewDiscInfo.joined()
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !Disc3TextField.stringValue.isEmpty {
|
||||||
|
let Disc3BaseFileNameWithExt: String = URL(fileURLWithPath: Disc3TextField.stringValue).lastPathComponent
|
||||||
|
let Disc3BaseFileName: String = Disc3BaseFileNameWithExt.replacingOccurrences(of: ".iso", with: "")
|
||||||
|
let Disc3ISOFilePath: String = "C:\\Games\\" + Disc3BaseFileName + ".iso"
|
||||||
|
let Disc3ISOFileName: String = URL(string: Disc3TextField.stringValue)!.lastPathComponent
|
||||||
|
if FileManager.default.fileExists(atPath: Disc3TextField.stringValue) && !FileManager.default.fileExists(atPath: Utils().CDrivePath + "/Games/" + Disc3ISOFileName) {
|
||||||
|
do {
|
||||||
|
try FileManager.default.copyItem(atPath: Disc3TextField.stringValue, toPath: Utils().CDrivePath + "/Games/" + Disc3ISOFileName)
|
||||||
|
let NewDiscInfo = [FullDiscInfo, "\n <file targ_path=\"image/disc03.iso\" orig_path=\"\(Disc3ISOFilePath)\" pfs_compression=\"enable\"/>"]
|
||||||
|
FullDiscInfo = NewDiscInfo.joined()
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !Disc4TextField.stringValue.isEmpty {
|
||||||
|
let Disc4BaseFileNameWithExt: String = URL(fileURLWithPath: Disc4TextField.stringValue).lastPathComponent
|
||||||
|
let Disc4BaseFileName: String = Disc4BaseFileNameWithExt.replacingOccurrences(of: ".iso", with: "")
|
||||||
|
let Disc4ISOFilePath: String = "C:\\Games\\" + Disc4BaseFileName + ".iso"
|
||||||
|
let Disc4ISOFileName: String = URL(string: Disc4TextField.stringValue)!.lastPathComponent
|
||||||
|
if FileManager.default.fileExists(atPath: Disc4TextField.stringValue) && !FileManager.default.fileExists(atPath: Utils().CDrivePath + "/Games/" + Disc4ISOFileName) {
|
||||||
|
do {
|
||||||
|
try FileManager.default.copyItem(atPath: Disc4TextField.stringValue, toPath: Utils().CDrivePath + "/Games/" + Disc4ISOFileName)
|
||||||
|
let NewDiscInfo = [FullDiscInfo, "\n <file targ_path=\"image/disc04.iso\" orig_path=\"\(Disc4ISOFilePath)\" pfs_compression=\"enable\"/>"]
|
||||||
|
FullDiscInfo = NewDiscInfo.joined()
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !Disc5TextField.stringValue.isEmpty {
|
||||||
|
let Disc5BaseFileNameWithExt: String = URL(fileURLWithPath: Disc5TextField.stringValue).lastPathComponent
|
||||||
|
let Disc5BaseFileName: String = Disc5BaseFileNameWithExt.replacingOccurrences(of: ".iso", with: "")
|
||||||
|
let Disc5ISOFilePath: String = "C:\\Games\\" + Disc5BaseFileName + ".iso"
|
||||||
|
let Disc5ISOFileName: String = URL(string: Disc5TextField.stringValue)!.lastPathComponent
|
||||||
|
if FileManager.default.fileExists(atPath: Disc5TextField.stringValue) && !FileManager.default.fileExists(atPath: Utils().CDrivePath + "/Games/" + Disc5ISOFileName) {
|
||||||
|
do {
|
||||||
|
try FileManager.default.copyItem(atPath: Disc5TextField.stringValue, toPath: Utils().CDrivePath + "/Games/" + Disc5ISOFileName)
|
||||||
|
let NewDiscInfo = [FullDiscInfo, "\n <file targ_path=\"image/disc05.iso\" orig_path=\"\(Disc5ISOFilePath)\" pfs_compression=\"enable\"/>"]
|
||||||
|
FullDiscInfo = NewDiscInfo.joined()
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify the GP4 project
|
||||||
|
do {
|
||||||
|
var fileContents = try String(contentsOfFile: Utils().CDrivePath + "/Cache/PS2fPKG.gp4", encoding: .utf8)
|
||||||
|
fileContents = fileContents.replacingOccurrences(of: "<?xml version=\"1.1\"", with: "<?xml version=\"1.0\"")
|
||||||
|
fileContents = fileContents.replacingOccurrences(of: "<scenarios default_id=\"1\">", with: "<scenarios default_id=\"0\">")
|
||||||
|
fileContents = fileContents.replacingOccurrences(of: "</files>", with: "\(FullDiscInfo)\n</files>")
|
||||||
|
|
||||||
|
try fileContents.write(toFile: Utils().CDrivePath + "/Cache/PS2fPKG.gp4", atomically: false, encoding: .utf8)
|
||||||
|
} catch {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue += 20 //80
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Disc(s) copied & GP4 project modified. fPKG will be built after user confirmation"
|
||||||
|
usleep(100000)
|
||||||
|
|
||||||
|
let PKGReadyAlert = NSAlert()
|
||||||
|
PKGReadyAlert.messageText = "Files are ready for fPKG creation."
|
||||||
|
PKGReadyAlert.informativeText = "fPKG Ready"
|
||||||
|
PKGReadyAlert.addButton(withTitle: "Continue")
|
||||||
|
PKGReadyAlert.runModal()
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.isIndeterminate = true
|
||||||
|
fPKGBuilderProgressIndicator.startAnimation(nil)
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "The fPKG is building, please wait until done"
|
||||||
|
usleep(150000)
|
||||||
|
|
||||||
|
// Create the fPKG
|
||||||
|
BuildPKG()
|
||||||
|
|
||||||
|
// Check build
|
||||||
|
let PKGFileName: String = "UP9000-" + CurrentGameID + "_00-" + CurrentGameID + "PS2FPKG-A0100-V0100.pkg"
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().CDrivePath + "/Cache/" + PKGFileName) {
|
||||||
|
|
||||||
|
// Move the created fPKG into the selected output folder
|
||||||
|
do {
|
||||||
|
try FileManager.default.moveItem(atPath: Utils().CDrivePath + "/Cache/" + PKGFileName, toPath: PKGOutputPath + "/" + PKGFileName)
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.stopAnimation(nil)
|
||||||
|
fPKGBuilderProgressIndicator.isIndeterminate = false
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue = 100 //100
|
||||||
|
fPKGBuilderSpinningIndicator.stopAnimation(nil)
|
||||||
|
fPKGBuilderSpinningIndicator.isHidden = true
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Done"
|
||||||
|
|
||||||
|
let SuccessAlert = NSAlert()
|
||||||
|
SuccessAlert.messageText = "fPKG created with success!"
|
||||||
|
SuccessAlert.informativeText = "Done"
|
||||||
|
SuccessAlert.addButton(withTitle: "Continue")
|
||||||
|
SuccessAlert.runModal()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExtractFileFromISO(ISOFile: String, FileToExtract: String) -> String {
|
||||||
|
// Extracts the specified file into the Tools directory
|
||||||
|
let unAr = Bundle.main.path(forResource: "unar", ofType: "")
|
||||||
|
let task = Process()
|
||||||
|
|
||||||
|
task.launchPath = "/bin/sh"
|
||||||
|
task.arguments = ["-c", "'" + unAr! + "' -f -o '" + Utils().ToolsPath + "/' '" + ISOFile + "' " + FileToExtract]
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
|
||||||
|
let OutputFolderName: String = URL(string: ISOFile)!.deletingPathExtension().lastPathComponent
|
||||||
|
// Check if file has been extracted successfully
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().ToolsPath + "/" + OutputFolderName + "/" + FileToExtract) {
|
||||||
|
return Utils().ToolsPath + "/" + OutputFolderName + "/" + FileToExtract
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateParamSFO(GameTitle: String) {
|
||||||
|
let SFOUtil = Bundle.main.path(forResource: "sfoutil", ofType: "")
|
||||||
|
let ContentID: String = "UP9000-" + CurrentGameID + "_00-" + CurrentGameID + "PS2FPKG"
|
||||||
|
let DestinationPath: String = "'" + Utils().ToolsPath + "/param.sfo'"
|
||||||
|
let task = Process()
|
||||||
|
|
||||||
|
task.launchPath = "/bin/sh"
|
||||||
|
task.arguments = ["-c", "'" + SFOUtil! + "' --force --new-file " + DestinationPath + " --add int APP_TYPE 1 --add str APP_VER \"01.00\" --add int ATTRIBUTE 0 --add str CATEGORY \"gd\" --add str CONTENT_ID \"\(ContentID)\" --add int DOWNLOAD_DATA_SIZE 0 --add str FORMAT \"obs\" --add int PARENTAL_LEVEL 5 --add int SYSTEM_VER 0 --add str TITLE \"" + GameTitle + "\" --add str TITLE_ID \"\(NPTitleTextField.stringValue)\" --add str VERSION \"01.00\""]
|
||||||
|
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateGP4Project() {
|
||||||
|
let task = Process()
|
||||||
|
|
||||||
|
task.launchPath = "/bin/sh"
|
||||||
|
task.arguments = ["-c", "'" + Utils().ToolsPath + "/gengp4_ps2'"]
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildPKG() {
|
||||||
|
let task = Process()
|
||||||
|
|
||||||
|
task.launchPath = "/bin/sh"
|
||||||
|
task.arguments = ["-c", "'" + Utils().ToolsPath + "/make_fPKG_PS2'"]
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// PSClassicsTabViewController.swift
|
||||||
|
// fPKG Builder
|
||||||
|
//
|
||||||
|
// Created by SvenGDK on 18/08/2024.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Cocoa
|
||||||
|
|
||||||
|
class PSClassicsTabViewController: NSTabViewController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var representedObject: Any? {
|
||||||
|
didSet {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// PPPwnWindowController.swift
|
||||||
|
// PS Mac Tools
|
||||||
|
//
|
||||||
|
// Created by SvenGDK on 14/08/2024.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AppKit
|
||||||
|
|
||||||
|
class PSClassicsWindowController: NSWindowController {
|
||||||
|
|
||||||
|
override func windowDidLoad() {
|
||||||
|
super.windowDidLoad()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,656 @@
|
|||||||
|
//
|
||||||
|
// PSPClassicsViewController.swift
|
||||||
|
// PS Mac Tools
|
||||||
|
//
|
||||||
|
// Created by SvenGDK on 14/08/2024.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AppKit
|
||||||
|
|
||||||
|
class PSPClassicsViewController: NSViewController {
|
||||||
|
|
||||||
|
var CurrentGameID: String = ""
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var representedObject: Any? {
|
||||||
|
didSet {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBOutlet weak var BuildfPKGButton: NSButton!
|
||||||
|
@IBOutlet weak var ProgressTextField: NSTextField!
|
||||||
|
@IBOutlet weak var fPKGBuilderProgressIndicator: NSProgressIndicator!
|
||||||
|
@IBOutlet weak var fPKGBuilderSpinningIndicator: NSProgressIndicator!
|
||||||
|
@IBOutlet weak var fPKGBuilderStatusTextField: NSTextField!
|
||||||
|
|
||||||
|
@IBOutlet weak var GameTitleTextField: NSTextField!
|
||||||
|
@IBOutlet weak var NPTitleTextField: NSTextField!
|
||||||
|
@IBOutlet weak var IconTextField: NSTextField!
|
||||||
|
@IBOutlet weak var BackgroundTextField: NSTextField!
|
||||||
|
@IBOutlet weak var GameISOTextField: NSTextField!
|
||||||
|
@IBOutlet weak var CustomConfigTextField: NSTextField!
|
||||||
|
|
||||||
|
@IBAction func BrowseIcon(_ sender: NSButton) {
|
||||||
|
let dialog = NSOpenPanel()
|
||||||
|
|
||||||
|
dialog.title = "Select an icon for your game"
|
||||||
|
dialog.showsResizeIndicator = true
|
||||||
|
dialog.showsHiddenFiles = false
|
||||||
|
dialog.canChooseFiles = true
|
||||||
|
dialog.canChooseDirectories = false
|
||||||
|
dialog.allowsMultipleSelection = false
|
||||||
|
dialog.allowedContentTypes = [Utils().PNGType]
|
||||||
|
|
||||||
|
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = dialog.url
|
||||||
|
IconTextField.stringValue = result!.path
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func BrowseBackground(_ sender: NSButton) {
|
||||||
|
let dialog = NSOpenPanel()
|
||||||
|
|
||||||
|
dialog.title = "Select a background for your game"
|
||||||
|
dialog.showsResizeIndicator = true
|
||||||
|
dialog.showsHiddenFiles = false
|
||||||
|
dialog.canChooseFiles = true
|
||||||
|
dialog.canChooseDirectories = false
|
||||||
|
dialog.allowsMultipleSelection = false
|
||||||
|
dialog.allowedContentTypes = [Utils().PNGType]
|
||||||
|
|
||||||
|
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = dialog.url
|
||||||
|
BackgroundTextField.stringValue = result!.path
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func BrowseGameISO(_ sender: NSButton) {
|
||||||
|
let dialog = NSOpenPanel()
|
||||||
|
|
||||||
|
dialog.title = "Select your PSP ISO file"
|
||||||
|
dialog.showsResizeIndicator = true
|
||||||
|
dialog.showsHiddenFiles = false
|
||||||
|
dialog.canChooseFiles = true
|
||||||
|
dialog.canChooseDirectories = false
|
||||||
|
dialog.allowsMultipleSelection = false
|
||||||
|
dialog.allowedContentTypes = [Utils().ISOType]
|
||||||
|
|
||||||
|
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = dialog.url
|
||||||
|
let FilePath: String = result!.path
|
||||||
|
|
||||||
|
// Check if game is a PSP ISO (error output needs to be checked here)
|
||||||
|
let ISOReaderOutput = CheckPSPGame(ISOFile: FilePath).error
|
||||||
|
if !ISOReaderOutput.isEmpty {
|
||||||
|
|
||||||
|
// Extract UMD_DATA.BIN
|
||||||
|
let ExtractedUMDDataPath: String = ExtractFileFromISO(ISOFile: FilePath, FileToExtract: "UMD_DATA.BIN")
|
||||||
|
|
||||||
|
// Read UMD_DATA.BIN
|
||||||
|
if FileManager.default.fileExists(atPath: ExtractedUMDDataPath) {
|
||||||
|
|
||||||
|
let UMDData = ReadUMDData(FileToRead: ExtractedUMDDataPath, Offset: 0, Length: 10)
|
||||||
|
let NPTitle = String(bytes: UMDData!, encoding: .ascii)?.replacingOccurrences(of: "-", with: "") ?? ""
|
||||||
|
let GameTitle = result!.deletingPathExtension().lastPathComponent.replacingOccurrences(of: "(.*?)", with: "", options: .regularExpression).replacingOccurrences(of: " {2,}", with: "", options: .regularExpression).replacingOccurrences(of: "_", with: " ")
|
||||||
|
|
||||||
|
NPTitleTextField.stringValue = NPTitle
|
||||||
|
GameTitleTextField.stringValue = GameTitle
|
||||||
|
CurrentGameID = NPTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
GameISOTextField.stringValue = result!.path
|
||||||
|
BuildfPKGButton.isEnabled = true
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No valid PSP ISO
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func BrowseCustomConfig(_ sender: NSButton) {
|
||||||
|
let dialog = NSOpenPanel()
|
||||||
|
|
||||||
|
dialog.title = "Select a custom config file"
|
||||||
|
dialog.showsResizeIndicator = true
|
||||||
|
dialog.showsHiddenFiles = false
|
||||||
|
dialog.canChooseFiles = true
|
||||||
|
dialog.canChooseDirectories = false
|
||||||
|
dialog.allowsMultipleSelection = false
|
||||||
|
dialog.allowedContentTypes = [Utils().TXTType]
|
||||||
|
|
||||||
|
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = dialog.url
|
||||||
|
CustomConfigTextField.stringValue = result!.path
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckPSPGame(ISOFile: String) -> (output: [String], error: [String], exitCode: Int32) {
|
||||||
|
var output : [String] = []
|
||||||
|
var error : [String] = []
|
||||||
|
|
||||||
|
let isoutil = Bundle.main.path(forResource: "isoinfo", ofType: "")
|
||||||
|
let task = Process()
|
||||||
|
task.launchPath = "/bin/sh"
|
||||||
|
task.arguments = ["-c", "'" + isoutil! + "' -i '" + ISOFile + "' -x 'PSP_GAME/PARAM.SFO'"]
|
||||||
|
|
||||||
|
let outpipe = Pipe()
|
||||||
|
task.standardOutput = outpipe
|
||||||
|
let errpipe = Pipe()
|
||||||
|
task.standardError = errpipe
|
||||||
|
|
||||||
|
task.launch()
|
||||||
|
|
||||||
|
let outdata = outpipe.fileHandleForReading.readDataToEndOfFile()
|
||||||
|
if var string = String(data: outdata, encoding: .utf8) {
|
||||||
|
string = string.trimmingCharacters(in: .newlines)
|
||||||
|
output = string.components(separatedBy: "\n")
|
||||||
|
print(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
let errdata = errpipe.fileHandleForReading.readDataToEndOfFile()
|
||||||
|
if var string = String(data: errdata, encoding: .utf8) {
|
||||||
|
string = string.trimmingCharacters(in: .newlines)
|
||||||
|
error = string.components(separatedBy: "\n")
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
task.waitUntilExit()
|
||||||
|
let status = task.terminationStatus
|
||||||
|
return (output, error, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExtractFileFromISO(ISOFile: String, FileToExtract: String) -> String {
|
||||||
|
// Extracts the specified file into the Tools directory
|
||||||
|
let unAr = Bundle.main.path(forResource: "unar", ofType: "")
|
||||||
|
let task = Process()
|
||||||
|
|
||||||
|
task.launchPath = "/bin/sh"
|
||||||
|
task.arguments = ["-c", "'" + unAr! + "' -f -o '" + Utils().ToolsPath + "/' '" + ISOFile + "' " + FileToExtract]
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
|
||||||
|
let OutputFolderName: String = URL(string: ISOFile)!.deletingPathExtension().lastPathComponent
|
||||||
|
// Check if file has been extracted successfully
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().ToolsPath + "/" + OutputFolderName + "/" + FileToExtract) {
|
||||||
|
return Utils().ToolsPath + "/" + OutputFolderName + "/" + FileToExtract
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadUMDData(FileToRead: String, Offset: Int64, Length: Int) -> [UInt8]? {
|
||||||
|
var offset: Int64 = Offset
|
||||||
|
var newByte = [UInt8](repeating: 0, count: Length)
|
||||||
|
if let fileHandle = FileHandle(forReadingAtPath: FileToRead) {
|
||||||
|
let baseStreamLength = fileHandle.seekToEndOfFile()
|
||||||
|
fileHandle.seek(toFileOffset: UInt64(offset))
|
||||||
|
var num = 0
|
||||||
|
|
||||||
|
while offset < baseStreamLength && num < Length {
|
||||||
|
let byte = fileHandle.readData(ofLength: 1)
|
||||||
|
if let byteValue = byte.first {
|
||||||
|
newByte[num] = byteValue
|
||||||
|
offset += 1
|
||||||
|
num += 1
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileHandle.closeFile()
|
||||||
|
}
|
||||||
|
return newByte
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteData(FileToWrite: String, Offset: Int64, DataToWrite: String) {
|
||||||
|
if let fileHandle = FileHandle(forWritingAtPath: FileToWrite) {
|
||||||
|
let newStringArray = DataToWrite.split(separator: "-").map { String($0) }
|
||||||
|
fileHandle.seek(toFileOffset: UInt64(Offset))
|
||||||
|
for str in newStringArray {
|
||||||
|
if let intValue = Int(str, radix: 16) {
|
||||||
|
fileHandle.write(Data([UInt8(intValue)]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileHandle.closeFile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindOffset(FileName: String, Query: [UInt8]) -> Int64? {
|
||||||
|
var returnLength: Int64 = -1
|
||||||
|
if let fileHandle = FileHandle(forReadingAtPath: FileName) {
|
||||||
|
let baseStreamLength = fileHandle.seekToEndOfFile()
|
||||||
|
fileHandle.seek(toFileOffset: 0)
|
||||||
|
|
||||||
|
if Query.count <= baseStreamLength {
|
||||||
|
var newByteArray = [UInt8](repeating: 0, count: Query.count)
|
||||||
|
let bytesRead = fileHandle.readData(ofLength: Query.count)
|
||||||
|
newByteArray = Array(bytesRead)
|
||||||
|
var flag = false
|
||||||
|
let newQueryLength = Query.count - 1
|
||||||
|
var whileInt = 0
|
||||||
|
|
||||||
|
while whileInt <= newQueryLength {
|
||||||
|
if newByteArray[whileInt] == Query[whileInt] {
|
||||||
|
flag = true
|
||||||
|
whileInt += 1
|
||||||
|
} else {
|
||||||
|
flag = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !flag {
|
||||||
|
let newBaseStreamLength = baseStreamLength - 1
|
||||||
|
let queryLength = Query.count
|
||||||
|
var queryOffset = queryLength
|
||||||
|
|
||||||
|
while queryOffset <= newBaseStreamLength {
|
||||||
|
newByteArray.removeFirst()
|
||||||
|
newByteArray.append(fileHandle.readData(ofLength: 1).first ?? 0)
|
||||||
|
var num3 = 0
|
||||||
|
while num3 <= newQueryLength {
|
||||||
|
if newByteArray[num3] == Query[num3] {
|
||||||
|
flag = true
|
||||||
|
num3 += 1
|
||||||
|
} else {
|
||||||
|
flag = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !flag {
|
||||||
|
queryOffset += 1
|
||||||
|
} else {
|
||||||
|
returnLength = Int64(queryOffset) - Int64(queryLength - 1)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
returnLength = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileHandle.closeFile()
|
||||||
|
}
|
||||||
|
return returnLength
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecryptEBOOT(EBOOTFile: String) {
|
||||||
|
// Extracts the specified file into the Tools directory
|
||||||
|
let pspdecrypt = Bundle.main.path(forResource: "pspdecrypt", ofType: "")
|
||||||
|
let task = Process()
|
||||||
|
|
||||||
|
task.launchPath = "/bin/sh"
|
||||||
|
task.arguments = ["-c", "'" + pspdecrypt! + "' '" + EBOOTFile + "'"]
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreatefPKG() {
|
||||||
|
var PKGOutputPath: String = ""
|
||||||
|
let FolderBrowserOpenPanel = NSOpenPanel()
|
||||||
|
|
||||||
|
FolderBrowserOpenPanel.title = "Select an output folder"
|
||||||
|
FolderBrowserOpenPanel.showsResizeIndicator = true
|
||||||
|
FolderBrowserOpenPanel.showsHiddenFiles = false
|
||||||
|
FolderBrowserOpenPanel.canChooseFiles = false
|
||||||
|
FolderBrowserOpenPanel.canChooseDirectories = true
|
||||||
|
FolderBrowserOpenPanel.allowsMultipleSelection = false
|
||||||
|
|
||||||
|
// Set destination folder where the pkg file will be moved into
|
||||||
|
if (FolderBrowserOpenPanel.runModal() == NSApplication.ModalResponse.OK) {
|
||||||
|
let result = FolderBrowserOpenPanel.url
|
||||||
|
PKGOutputPath = result!.path
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.stopAnimation(nil)
|
||||||
|
fPKGBuilderProgressIndicator.isIndeterminate = false
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue = 0
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Removing old fPKG build folder"
|
||||||
|
usleep(150000)
|
||||||
|
|
||||||
|
// Remove previous fPKG GP4 project
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().CDrivePath + "/Cache/PSPfPKG.gp4") {
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(atPath: Utils().CDrivePath + "/Cache/PSPfPKG.gp4")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove previous fPKG project folder
|
||||||
|
var isDir: ObjCBool = true
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().CDrivePath + "/Cache/PSPfPKG", isDirectory: &isDir) {
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(atPath: Utils().CDrivePath + "/Cache/PSPfPKG")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue += 10
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Preparing the PSP emulator"
|
||||||
|
usleep(150000)
|
||||||
|
|
||||||
|
// Copy the PS1 emulator to the new project folder
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().CDrivePath + "/PS4/emus/psphd", isDirectory: &isDir) {
|
||||||
|
do {
|
||||||
|
try FileManager.default.copyItem(atPath: Utils().CDrivePath + "/PS4/emus/psphd", toPath: Utils().CDrivePath + "/Cache/PSPfPKG")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue += 10 //20
|
||||||
|
|
||||||
|
// Copy the selected icon to the project folder
|
||||||
|
if !IconTextField.stringValue.isEmpty {
|
||||||
|
do {
|
||||||
|
try FileManager.default.copyItem(atPath: IconTextField.stringValue, toPath: Utils().CDrivePath + "/Cache/PSPfPKG/sce_sys/icon0.png")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the selected background to the project folder
|
||||||
|
if !BackgroundTextField.stringValue.isEmpty {
|
||||||
|
do {
|
||||||
|
try FileManager.default.copyItem(atPath: IconTextField.stringValue, toPath: Utils().CDrivePath + "/Cache/PSPfPKG/sce_sys/pic0.png")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get PSP EBOOT
|
||||||
|
let SelectedISOFile: String = GameISOTextField.stringValue
|
||||||
|
let ExtractedEBOOTPath: String = ExtractFileFromISO(ISOFile: SelectedISOFile, FileToExtract: "PSP_GAME/SYSDIR/EBOOT.BIN")
|
||||||
|
let DecryptedEBOOTPath: String = ExtractedEBOOTPath.appending(".dec")
|
||||||
|
|
||||||
|
if !FileManager.default.fileExists(atPath: ExtractedEBOOTPath) {
|
||||||
|
do {
|
||||||
|
// Copy ISO file to fPKG cache
|
||||||
|
try FileManager.default.copyItem(atPath: SelectedISOFile, toPath: Utils().CDrivePath + "/Cache/PSPfPKG/data/USER_L0.IMG")
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue += 10 //30
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Waiting for user input"
|
||||||
|
usleep(150000)
|
||||||
|
|
||||||
|
let IncompatibilityAlert = NSAlert()
|
||||||
|
IncompatibilityAlert.informativeText = "Warning: This game may not work!"
|
||||||
|
IncompatibilityAlert.messageText = "Cannot read the EBOOT.BIN file from the ISO."
|
||||||
|
IncompatibilityAlert.runModal()
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue += 10 //30
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Decrypting PSP EBOOT & modifying final image"
|
||||||
|
usleep(150000)
|
||||||
|
|
||||||
|
// Decrypt extracted EBOOT file
|
||||||
|
DecryptEBOOT(EBOOTFile: ExtractedEBOOTPath)
|
||||||
|
|
||||||
|
// Copy ISO file to fPKG cache
|
||||||
|
do {
|
||||||
|
try FileManager.default.copyItem(atPath: SelectedISOFile, toPath: Utils().CDrivePath + "/Cache/PSPfPKG/data/USER_L0.IMG")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
let NewFileInfo = try? FileManager.default.attributesOfItem(atPath: ExtractedEBOOTPath)
|
||||||
|
let FileLength = (NewFileInfo?[.size] as? NSNumber)?.int64Value ?? 0
|
||||||
|
let CappedFileLength = min(FileLength, 512320)
|
||||||
|
|
||||||
|
if let TempEBOOTByteArray = ReadUMDData(FileToRead: ExtractedEBOOTPath, Offset: 0, Length: Int(CappedFileLength)),
|
||||||
|
let OffsetValue = FindOffset(FileName: Utils().CDrivePath + "/Cache/PSPfPKG/data/USER_L0.IMG", Query: TempEBOOTByteArray) {
|
||||||
|
let DecTempEBOOTByteArray = ReadUMDData(FileToRead: DecryptedEBOOTPath, Offset: 0, Length: Int(FileLength))
|
||||||
|
let DataString = DecTempEBOOTByteArray?.map { String(format: "%02X", $0) }.joined(separator: "-") ?? ""
|
||||||
|
|
||||||
|
// Modify ISO
|
||||||
|
WriteData(FileToWrite: Utils().CDrivePath + "/Cache/PSPfPKG/data/USER_L0.IMG", Offset: OffsetValue, DataToWrite: DataString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue += 10 //40
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Removing temporary files"
|
||||||
|
usleep(150000)
|
||||||
|
|
||||||
|
// Remove temporary files
|
||||||
|
if FileManager.default.fileExists(atPath: ExtractedEBOOTPath) {
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(atPath: ExtractedEBOOTPath)
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if FileManager.default.fileExists(atPath: DecryptedEBOOTPath) {
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(atPath: DecryptedEBOOTPath)
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If config already exists, remove it
|
||||||
|
let ConfigDestinationPath: String = Utils().CDrivePath + "/Cache/PSPfPKG/config-title.txt"
|
||||||
|
if FileManager.default.fileExists(atPath: ConfigDestinationPath)
|
||||||
|
{
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(atPath: ConfigDestinationPath)
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue += 10 //50
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Configurating PSP Emulator, please wait"
|
||||||
|
usleep(150000)
|
||||||
|
|
||||||
|
// Create a new config file
|
||||||
|
FileManager.default.createFile(atPath: ConfigDestinationPath, contents:Data(" ".utf8), attributes: nil)
|
||||||
|
let ConfigWriter = FileHandle(forWritingAtPath: ConfigDestinationPath)
|
||||||
|
if ConfigWriter == nil { }
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Required config
|
||||||
|
let PSPEmulatorConfig: String = """
|
||||||
|
--ps4-trophies=0
|
||||||
|
--ps5-uds=0
|
||||||
|
--trophies=0
|
||||||
|
--image="data/USER_L0.IMG"
|
||||||
|
--antialias=SSAA4x
|
||||||
|
--multisaves=true
|
||||||
|
--notrophies=true
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
ConfigWriter!.write(PSPEmulatorConfig.data(using: .utf8)!)
|
||||||
|
ConfigWriter!.closeFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append custom config
|
||||||
|
if !CustomConfigTextField.stringValue.isEmpty {
|
||||||
|
let AdditionalConfig = try? String(contentsOfFile: CustomConfigTextField.stringValue, encoding: .utf8)
|
||||||
|
if let NewFileHandle = FileHandle(forWritingAtPath: ConfigDestinationPath) {
|
||||||
|
NewFileHandle.seekToEndOfFile()
|
||||||
|
if let DataToWrite = AdditionalConfig!.data(using: .utf8) {
|
||||||
|
NewFileHandle.write(DataToWrite)
|
||||||
|
}
|
||||||
|
NewFileHandle.closeFile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue += 10 //60
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "PSP emulator configuration done. Creating now the param.sfo file, please wait"
|
||||||
|
usleep(150000)
|
||||||
|
|
||||||
|
// Create a new param.sfo file
|
||||||
|
CreateParamSFO(GameTitle: GameTitleTextField.stringValue)
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().ToolsPath + "/param.sfo") {
|
||||||
|
|
||||||
|
//Delete default param.sfo if exists
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().CDrivePath + "/Cache/PSPfPKG/sce_sys/param.sfo") {
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(atPath: Utils().CDrivePath + "/Cache/PSPfPKG/sce_sys/param.sfo")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the new param.sfo to the sce_sys folder
|
||||||
|
do {
|
||||||
|
try FileManager.default.moveItem(atPath: Utils().ToolsPath + "/param.sfo", toPath: Utils().CDrivePath + "/Cache/PSPfPKG/sce_sys/param.sfo")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue += 10 //70
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Param.sfo file created. Creating GP4 project"
|
||||||
|
usleep(150000)
|
||||||
|
|
||||||
|
// Generate a GP4 project and modify it
|
||||||
|
CreateGP4Project()
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().CDrivePath + "/Cache/PSPfPKG.gp4") {
|
||||||
|
|
||||||
|
// Modify the GP4 project
|
||||||
|
do {
|
||||||
|
var fileContents = try String(contentsOfFile: Utils().CDrivePath + "/Cache/PSPfPKG.gp4", encoding: .utf8)
|
||||||
|
fileContents = fileContents.replacingOccurrences(of: "<?xml version=\"1.1\"", with: "<?xml version=\"1.0\"")
|
||||||
|
fileContents = fileContents.replacingOccurrences(of: "<scenarios default_id=\"1\">", with: "<scenarios default_id=\"0\">")
|
||||||
|
try fileContents.write(toFile: Utils().CDrivePath + "/Cache/PSPfPKG.gp4", atomically: false, encoding: .utf8)
|
||||||
|
} catch {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue += 10 //80
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "GP4 project created. fPKG will be built after user confirmation"
|
||||||
|
usleep(100000)
|
||||||
|
|
||||||
|
let PKGReadyAlert = NSAlert()
|
||||||
|
PKGReadyAlert.messageText = "Files are ready for fPKG creation."
|
||||||
|
PKGReadyAlert.informativeText = "fPKG Ready"
|
||||||
|
PKGReadyAlert.addButton(withTitle: "Continue")
|
||||||
|
PKGReadyAlert.runModal()
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.isIndeterminate = true
|
||||||
|
fPKGBuilderProgressIndicator.startAnimation(nil)
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "The fPKG is building, please wait until done"
|
||||||
|
usleep(150000)
|
||||||
|
|
||||||
|
// Create the fPKG
|
||||||
|
BuildPKG()
|
||||||
|
|
||||||
|
// Check build
|
||||||
|
let PKGFileName: String = "UP9000-" + CurrentGameID + "_00-" + CurrentGameID + "PSPFPKG-A0100-V0100.pkg"
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().CDrivePath + "/Cache/" + PKGFileName) {
|
||||||
|
|
||||||
|
// Move the created fPKG into the selected output folder
|
||||||
|
do {
|
||||||
|
try FileManager.default.moveItem(atPath: Utils().CDrivePath + "/Cache/" + PKGFileName, toPath: PKGOutputPath + "/" + PKGFileName)
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.stopAnimation(nil)
|
||||||
|
fPKGBuilderProgressIndicator.isIndeterminate = false
|
||||||
|
fPKGBuilderProgressIndicator.doubleValue = 100 //100
|
||||||
|
fPKGBuilderSpinningIndicator.stopAnimation(nil)
|
||||||
|
fPKGBuilderSpinningIndicator.isHidden = true
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Done"
|
||||||
|
|
||||||
|
let SuccessAlert = NSAlert()
|
||||||
|
SuccessAlert.messageText = "fPKG created with success!"
|
||||||
|
SuccessAlert.informativeText = "Done"
|
||||||
|
SuccessAlert.addButton(withTitle: "Continue")
|
||||||
|
SuccessAlert.runModal()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func BuildfPKG(_ sender: NSButton) {
|
||||||
|
|
||||||
|
let MissingInfoAlert = NSAlert()
|
||||||
|
MissingInfoAlert.addButton(withTitle: "Close")
|
||||||
|
|
||||||
|
if GameISOTextField.stringValue.isEmpty {
|
||||||
|
MissingInfoAlert.messageText = "Game ISO is missing, cannot continue."
|
||||||
|
MissingInfoAlert.runModal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if GameTitleTextField.stringValue.isEmpty {
|
||||||
|
MissingInfoAlert.messageText = "The Game Title is missing, cannot continue."
|
||||||
|
MissingInfoAlert.runModal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if NPTitleTextField.stringValue.isEmpty {
|
||||||
|
MissingInfoAlert.messageText = "The NP Title is missing, cannot continue."
|
||||||
|
MissingInfoAlert.runModal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ProgressTextField.isHidden = false
|
||||||
|
fPKGBuilderProgressIndicator.isHidden = false
|
||||||
|
fPKGBuilderSpinningIndicator.isHidden = false
|
||||||
|
fPKGBuilderStatusTextField.isHidden = false
|
||||||
|
|
||||||
|
fPKGBuilderProgressIndicator.isIndeterminate = true
|
||||||
|
fPKGBuilderProgressIndicator.startAnimation(nil)
|
||||||
|
fPKGBuilderSpinningIndicator.startAnimation(nil)
|
||||||
|
fPKGBuilderStatusTextField.stringValue = "Waiting for user input"
|
||||||
|
|
||||||
|
CreatefPKG()
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateParamSFO(GameTitle: String) {
|
||||||
|
let SFOUtil = Bundle.main.path(forResource: "sfoutil", ofType: "")
|
||||||
|
let ContentID: String = "UP9000-" + CurrentGameID + "_00-" + CurrentGameID + "PSPFPKG"
|
||||||
|
let DestinationPath: String = "'" + Utils().ToolsPath + "/param.sfo'"
|
||||||
|
let task = Process()
|
||||||
|
|
||||||
|
task.launchPath = "/bin/sh"
|
||||||
|
task.arguments = ["-c", "'" + SFOUtil! + "' --force --new-file " + DestinationPath + " --add int APP_TYPE 1 --add str APP_VER \"01.00\" --add int ATTRIBUTE 0 --add str CATEGORY \"gd\" --add str CONTENT_ID \"\(ContentID)\" --add int DOWNLOAD_DATA_SIZE 0 --add str FORMAT \"obs\" --add int PARENTAL_LEVEL 5 --add int SYSTEM_VER 0 --add str TITLE \"" + GameTitle + "\" --add str TITLE_ID \"\(NPTitleTextField.stringValue)\" --add str VERSION \"01.00\""]
|
||||||
|
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateGP4Project() {
|
||||||
|
let task = Process()
|
||||||
|
|
||||||
|
task.launchPath = "/bin/sh"
|
||||||
|
task.arguments = ["-c", "'" + Utils().ToolsPath + "/gengp4_psp'"]
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildPKG() {
|
||||||
|
let task = Process()
|
||||||
|
|
||||||
|
task.launchPath = "/bin/sh"
|
||||||
|
task.arguments = ["-c", "'" + Utils().ToolsPath + "/make_fPKG_PSP'"]
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="RgM-zw-Kyg">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="macosx"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22690"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--Window Controller-->
|
||||||
|
<scene sceneID="RhC-Y2-v22">
|
||||||
|
<objects>
|
||||||
|
<windowController id="RgM-zw-Kyg" sceneMemberID="viewController">
|
||||||
|
<window key="window" title="PS Classics fPKG Builder for macOS [by SvenGDK]" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="bKe-r2-B5n">
|
||||||
|
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||||
|
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||||
|
<rect key="contentRect" x="425" y="462" width="480" height="270"/>
|
||||||
|
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
|
||||||
|
<view key="contentView" id="gBb-NI-ZjB">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
</view>
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="RgM-zw-Kyg" id="V4I-jq-e5I"/>
|
||||||
|
</connections>
|
||||||
|
</window>
|
||||||
|
<connections>
|
||||||
|
<segue destination="E2r-yS-ssk" kind="relationship" relationship="window.shadowedContentViewController" id="uVo-8X-puB"/>
|
||||||
|
</connections>
|
||||||
|
</windowController>
|
||||||
|
<customObject id="siQ-VN-W1N" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="-18" y="18"/>
|
||||||
|
</scene>
|
||||||
|
<!--Preloader View Controller-->
|
||||||
|
<scene sceneID="EBV-j7-AZH">
|
||||||
|
<objects>
|
||||||
|
<viewController id="E2r-yS-ssk" customClass="PreloaderViewController" customModule="fPKG_Builder" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
|
<view key="view" id="dgd-yP-Q5T">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<progressIndicator fixedFrame="YES" maxValue="100" style="bar" translatesAutoresizingMaskIntoConstraints="NO" id="fS8-oT-1Mi">
|
||||||
|
<rect key="frame" x="48" y="121" width="384" height="20"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
|
</progressIndicator>
|
||||||
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Xco-oh-R2J">
|
||||||
|
<rect key="frame" x="46" y="148" width="388" height="16"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
|
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="Please wait until the fPKG Builder finished initializing" id="slo-vB-YAj">
|
||||||
|
<font key="font" usesAppearanceFont="YES"/>
|
||||||
|
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
</textField>
|
||||||
|
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DyG-lw-2YN">
|
||||||
|
<rect key="frame" x="46" y="106" width="388" height="14"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
|
<textFieldCell key="cell" controlSize="small" lineBreakMode="clipping" alignment="center" title="Checking ..." id="gX3-mg-cFx">
|
||||||
|
<font key="font" metaFont="smallSystem"/>
|
||||||
|
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
</textField>
|
||||||
|
</subviews>
|
||||||
|
</view>
|
||||||
|
<connections>
|
||||||
|
<outlet property="LoadingProgressIndicator" destination="fS8-oT-1Mi" id="ezC-ki-YvN"/>
|
||||||
|
<outlet property="LoadingStatusTextField" destination="DyG-lw-2YN" id="05b-c0-H9l"/>
|
||||||
|
</connections>
|
||||||
|
</viewController>
|
||||||
|
<customObject id="ea9-8Z-f5x" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="586" y="10"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
//
|
||||||
|
// PreloaderViewController.swift
|
||||||
|
// fPKG Builder
|
||||||
|
//
|
||||||
|
// Created by SvenGDK on 18/08/2024.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Cocoa
|
||||||
|
|
||||||
|
class PreloaderViewController: NSViewController {
|
||||||
|
|
||||||
|
@IBOutlet weak var LoadingProgressIndicator: NSProgressIndicator!
|
||||||
|
@IBOutlet weak var LoadingStatusTextField: NSTextField!
|
||||||
|
|
||||||
|
var isDir: ObjCBool = true
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||||
|
self.SetupfPKGBuilder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var representedObject: Any? {
|
||||||
|
didSet {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetupfPKGBuilder() {
|
||||||
|
let ErrorAlert = NSAlert()
|
||||||
|
ErrorAlert.addButton(withTitle: "Continue")
|
||||||
|
|
||||||
|
LoadingProgressIndicator.doubleValue = 0
|
||||||
|
LoadingStatusTextField.stringValue = "Checking if wine has been installed ..."
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
// Setup wine if app still exists and wine-home/usr folder is missing
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().ToolsPath + "/Wine Stable.app") && !FileManager.default.fileExists(atPath: Utils().ToolsPath + "/Resources/wine-home/usr", isDirectory: &isDir) {
|
||||||
|
if CheckWineQuarantine() == true {
|
||||||
|
|
||||||
|
LoadingProgressIndicator.doubleValue += 25
|
||||||
|
LoadingStatusTextField.stringValue = "Wine is not installed & quarantined, please wait ..."
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
DequarantineWine()
|
||||||
|
|
||||||
|
if CheckWineQuarantine() == false {
|
||||||
|
|
||||||
|
LoadingProgressIndicator.doubleValue += 25
|
||||||
|
LoadingStatusTextField.stringValue = "Wine dequarantined, installing now please wait ..."
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
MoveDequarantinedWine()
|
||||||
|
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().ToolsPath + "/Resources/wine-home/usr", isDirectory: &isDir) {
|
||||||
|
|
||||||
|
// Remove wine app
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(atPath: Utils().ToolsPath + "/Wine Stable.app")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make shell scripts in the Tools folder executable
|
||||||
|
MakeBinariesExecutable()
|
||||||
|
|
||||||
|
LoadingProgressIndicator.doubleValue += 50
|
||||||
|
LoadingStatusTextField.stringValue = "PS Classics fPKG Builder is ready. Starting ..."
|
||||||
|
sleep(2)
|
||||||
|
|
||||||
|
// Open the main window and close the preloader
|
||||||
|
let MainStoryBoard = NSStoryboard(name: "Main", bundle: nil)
|
||||||
|
let PSClassicsTabVC = MainStoryBoard.instantiateController(withIdentifier: "PSClassicsTabViewController") as! PSClassicsTabViewController
|
||||||
|
let MainWC = MainStoryBoard.instantiateController(withIdentifier: "MainWindowController") as! PSClassicsWindowController
|
||||||
|
|
||||||
|
MainWC.contentViewController = PSClassicsTabVC
|
||||||
|
MainWC.window?.setContentSize(NSSize(width: 805, height: 650))
|
||||||
|
MainWC.showWindow(self)
|
||||||
|
|
||||||
|
self.view.window?.close()
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ErrorAlert.messageText = "Could not move wine into the final destination. Errors will happen if you continue."
|
||||||
|
ErrorAlert.runModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ErrorAlert.messageText = "Could not dequarantine wine. Errors will happen if you continue."
|
||||||
|
ErrorAlert.runModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ErrorAlert.messageText = "Could not check quarantine status of wine. Errors will happen if you continue."
|
||||||
|
ErrorAlert.runModal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Assuming wine has been installed before
|
||||||
|
// Open the main window and close the preloader
|
||||||
|
let MainStoryBoard = NSStoryboard(name: "Main", bundle: nil)
|
||||||
|
let PSClassicsTabVC = MainStoryBoard.instantiateController(withIdentifier: "PSClassicsTabViewController") as! PSClassicsTabViewController
|
||||||
|
let MainWC = MainStoryBoard.instantiateController(withIdentifier: "MainWindowController") as! PSClassicsWindowController
|
||||||
|
|
||||||
|
MainWC.contentViewController = PSClassicsTabVC
|
||||||
|
MainWC.window?.setContentSize(NSSize(width: 805, height: 650))
|
||||||
|
MainWC.showWindow(self)
|
||||||
|
|
||||||
|
self.view.window?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckWineQuarantine() -> Bool {
|
||||||
|
var output : [String] = []
|
||||||
|
|
||||||
|
let task = Process()
|
||||||
|
task.launchPath = "/bin/sh"
|
||||||
|
task.arguments = ["-c", "xattr '" + Utils().ToolsPath + "/Wine Stable.app'"]
|
||||||
|
|
||||||
|
let outpipe = Pipe()
|
||||||
|
task.standardOutput = outpipe
|
||||||
|
|
||||||
|
task.launch()
|
||||||
|
|
||||||
|
let outdata = outpipe.fileHandleForReading.readDataToEndOfFile()
|
||||||
|
if var string = String(data: outdata, encoding: .utf8) {
|
||||||
|
string = string.trimmingCharacters(in: .newlines)
|
||||||
|
output = string.components(separatedBy: "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
task.waitUntilExit()
|
||||||
|
|
||||||
|
if !output.isEmpty {
|
||||||
|
if output.contains("com.apple.quarantine") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DequarantineWine() {
|
||||||
|
let task = Process()
|
||||||
|
task.launchPath = "/bin/sh"
|
||||||
|
task.arguments = ["-c", "xattr -r -d com.apple.quarantine '" + Utils().ToolsPath + "/Wine Stable.app'"]
|
||||||
|
|
||||||
|
let outpipe = Pipe()
|
||||||
|
task.standardOutput = outpipe
|
||||||
|
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func MoveDequarantinedWine() {
|
||||||
|
// Create wine-home folder
|
||||||
|
if !FileManager.default.fileExists(atPath: Utils().ToolsPath + "/Resources/wine-home", isDirectory: &isDir) {
|
||||||
|
do {
|
||||||
|
try FileManager.default.createDirectory(atPath: Utils().ToolsPath + "/Resources/wine-home", withIntermediateDirectories: false)
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy wine files to the wine-home/usr folder
|
||||||
|
if FileManager.default.fileExists(atPath: Utils().ToolsPath + "/Wine Stable.app/Contents/Resources/wine", isDirectory: &isDir) {
|
||||||
|
do {
|
||||||
|
try FileManager.default.copyItem(atPath: Utils().ToolsPath + "/Wine Stable.app/Contents/Resources/wine", toPath: Utils().ToolsPath + "/Resources/wine-home/usr")
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeBinariesExecutable() {
|
||||||
|
let task = Process()
|
||||||
|
task.launchPath = "/bin/sh"
|
||||||
|
task.arguments = ["-c", "chmod +x '" + Utils().ToolsPath + "/cue2toc' '" + Utils().ToolsPath + "/gengp4_ps1' '" + Utils().ToolsPath + "/gengp4_ps2' '" + Utils().ToolsPath + "/gengp4_psp' '" + Utils().ToolsPath + "/make_fPKG_PS1' '" + Utils().ToolsPath + "/make_fPKG_PS2' '" + Utils().ToolsPath + "/make_fPKG_PSP'"]
|
||||||
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// Utils.swift
|
||||||
|
// fPKG Builder
|
||||||
|
//
|
||||||
|
// Created by SvenGDK on 17/08/2024.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
class Utils {
|
||||||
|
|
||||||
|
// UTTypes for file selection filters
|
||||||
|
public let BINType: UTType = UTType(tag: "bin", tagClass: .filenameExtension, conformingTo: .archive)!
|
||||||
|
public let ISOType: UTType = UTType(tag: "iso", tagClass: .filenameExtension, conformingTo: .diskImage)!
|
||||||
|
public let PNGType: UTType = UTType(tag: "png", tagClass: .filenameExtension, conformingTo: .png)!
|
||||||
|
public let TXTType: UTType = UTType(tag: "txt", tagClass: .filenameExtension, conformingTo: .text)!
|
||||||
|
|
||||||
|
// Frequently used paths
|
||||||
|
public let ToolsPath: String = Bundle.main.bundleURL.deletingLastPathComponent().path + "/Tools"
|
||||||
|
public let CDrivePath: String = Bundle.main.bundleURL.deletingLastPathComponent().path + "/Tools/Resources/wine-prefix/drive_c"
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict/>
|
||||||
|
</plist>
|
||||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Reference in New Issue
Block a user