Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions macos-launcher/src/ActionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ private let logger = Logger(subsystem: "com.stfcmod.startrekpatch", category: "l

struct ActionView: View, XSollaUpdaterDelegate {

@StateObject var gameUpdater = GameUpdaterViewModel()
private let gameUpdater = XsollaUpdater("Star Trek Fleet Command")
@State private var gameVersion: Int = 0
@State private var gameUpdateAvailable: Bool = false
@State private var updating: Bool = false
Expand Down Expand Up @@ -103,8 +103,8 @@ struct ActionView: View, XSollaUpdaterDelegate {
showErrorAlert = true
}
updating = false
gameVersion = gameUpdater.getInstalledGameVersion()
gameUpdateAvailable = await gameUpdater.checkForGameUpdate()
gameVersion = gameUpdater.installedVersion()
gameUpdateAvailable = await gameUpdater.checkForUpdateAvailable()
}
}
}
Expand Down Expand Up @@ -141,10 +141,9 @@ struct ActionView: View, XSollaUpdaterDelegate {
.offset(x: 85, y: 20)
.task {
repeat {
gameVersion = gameUpdater.getInstalledGameVersion()
gameVersion = gameUpdater.installedVersion()
gameInstalled = gameVersion > 0
gameVersion = gameUpdater.getInstalledGameVersion()
gameUpdateAvailable = await gameUpdater.checkForGameUpdate()
gameUpdateAvailable = await gameUpdater.checkForUpdateAvailable()
try? await Task.sleep(for: .seconds(60))
} while !Task.isCancelled

Expand Down
17 changes: 1 addition & 16 deletions macos-launcher/src/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
import Foundation
import SwiftUI

struct ContentView: View, XSollaUpdaterDelegate {
@State private var updateAction: String = ""

struct ContentView: View {
var body: some View {
GeometryReader { geo in
ZStack {
Expand Down Expand Up @@ -158,16 +156,6 @@ struct ContentView: View, XSollaUpdaterDelegate {
}
}

private var buttons: some View {
Grid {
GridRow {

}
}
.frame(width: 300, height: 150)
.offset(x: 85, y: 300)
}

private func commonLabel(prefix: String) -> some View {
HStack {
Spacer()
Expand All @@ -184,9 +172,6 @@ struct ContentView: View, XSollaUpdaterDelegate {
.map { _ in "\(Int.random(in: 0...9))" }
.joined()
}

func updateProgress(progress: XsollaUpdateProgress) {
}
}

extension Color {
Expand Down
16 changes: 0 additions & 16 deletions macos-launcher/src/GitHubLib.swift

This file was deleted.

14 changes: 0 additions & 14 deletions macos-launcher/src/ManagedURL.swift

This file was deleted.

66 changes: 4 additions & 62 deletions macos-launcher/src/PatchEntitlements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ private let logger = Logger(subsystem: "com.stfcmod.startrekpatch", category: "e
/// Entitlements required to enable the loader for STFC.
/// The game ships with these from Scopely's Developer ID signature, so normally
/// no re-signing is needed — we just verify they're present.
let loaderEntitlements: [String: Any] = [
let loaderEntitlements: [String: Bool] = [
"com.apple.security.cs.allow-dyld-environment-variables": true,
"com.apple.security.cs.allow-unsigned-executable-memory": true,
"com.apple.security.cs.disable-library-validation": true,
Expand Down Expand Up @@ -48,10 +48,6 @@ func ensureGameHasLoaderEntitlements(signingIdentity: String = "-") throws {
// typically ships with the entitlements we need from Scopely's signature.
if checkEntitlements(appPath: gamePath, expectedEntitlements: loaderEntitlements) {
logger.info("Game already has the required loader entitlements, no re-signing needed")
// Clear any pending re-application flag since entitlements are valid
if UserDefaults.standard.bool(forKey: "forceEntitlementReapplication") {
UserDefaults.standard.set(false, forKey: "forceEntitlementReapplication")
}
return
}

Expand Down Expand Up @@ -93,7 +89,6 @@ func ensureGameHasLoaderEntitlements(signingIdentity: String = "-") throws {
// We only sign the main executable, preserving nested framework/plugin signatures
logger.info("Applying loader entitlements to the game binary...")
let applySuccess = applyEntitlementsDirectly(
appBundlePath: gameAppPath,
mainExecutablePath: gamePath,
entitlements: loaderEntitlements,
signingIdentity: signingIdentity
Expand All @@ -108,7 +103,6 @@ func ensureGameHasLoaderEntitlements(signingIdentity: String = "-") throws {
}

logger.info("Successfully applied loader entitlements to the game")
UserDefaults.standard.set(false, forKey: "forceEntitlementReapplication")
}

/// Fetches the path to the Star Trek Fleet Command game executable
Expand Down Expand Up @@ -159,7 +153,7 @@ func fetchGamePath() throws -> String? {
/// - appPath: Path to the .app bundle or executable to check
/// - expectedEntitlements: Dictionary of entitlement keys and their expected values
/// - Returns: true if all expected entitlements match, false otherwise
func checkEntitlements(appPath: String, expectedEntitlements: [String: Any]) -> Bool {
func checkEntitlements(appPath: String, expectedEntitlements: [String: Bool]) -> Bool {
// Create process to run codesign command
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/codesign")
Expand Down Expand Up @@ -205,8 +199,7 @@ func checkEntitlements(appPath: String, expectedEntitlements: [String: Any]) ->
return false
}

// Compare values (handling different types)
if !areValuesEqual(actualValue, expectedValue) {
if (actualValue as? Bool) != expectedValue {
let expected = String(describing: expectedValue)
let actual = String(describing: actualValue)
logger.debug("Entitlement '\(key)' mismatch: expected \(expected), got \(actual)")
Expand Down Expand Up @@ -297,16 +290,14 @@ private func requestGameBundleAccessOnMainThread(

/// Applies entitlements to an application using codesign directly (no elevation)
/// - Parameters:
/// - appBundlePath: Path to the .app bundle to sign
/// - mainExecutablePath: Path to the main executable within the bundle
/// - entitlements: Dictionary of entitlement keys and values to apply
/// - signingIdentity: Optional signing identity (defaults to "-" for ad-hoc signing)
/// - Returns: true if signing succeeded, false otherwise
/// - Note: Must be called while security-scoped resource access is active
func applyEntitlementsDirectly(
appBundlePath: String,
mainExecutablePath: String,
entitlements: [String: Any],
entitlements: [String: Bool],
signingIdentity: String = "-"
) -> Bool {
// Create temporary entitlements plist file
Expand All @@ -332,7 +323,6 @@ func applyEntitlementsDirectly(

// Execute codesign directly (no elevation needed - user owns these files)
let success = executeCodesignDirectly(
appBundlePath: appBundlePath,
mainExecutablePath: mainExecutablePath,
entitlementsPath: tempEntitlementsPath,
signingIdentity: signingIdentity
Expand All @@ -356,14 +346,12 @@ func applyEntitlementsDirectly(

/// Executes codesign directly without elevation
/// - Parameters:
/// - appBundlePath: Path to the .app bundle to sign
/// - mainExecutablePath: Path to the main executable within the bundle
/// - entitlementsPath: Path to temporary entitlements plist
/// - signingIdentity: Code signing identity
/// - Returns: true if successful, false otherwise
/// - Note: Must be called while security-scoped resource access is active
func executeCodesignDirectly(
appBundlePath: String,
mainExecutablePath: String,
entitlementsPath: String,
signingIdentity: String
Expand Down Expand Up @@ -456,52 +444,6 @@ func executeCodesignDirectly(
}
}

/// Helper function to compare two values of potentially different types
/// - Parameters:
/// - lhs: First value to compare
/// - rhs: Second value to compare
/// - Returns: true if values are equal, false otherwise
private func areValuesEqual(_ lhs: Any, _ rhs: Any) -> Bool {
// Handle Bool comparison
if let lhsBool = lhs as? Bool, let rhsBool = rhs as? Bool {
return lhsBool == rhsBool
}

// Handle String comparison
if let lhsString = lhs as? String, let rhsString = rhs as? String {
return lhsString == rhsString
}

// Handle Number/Int comparison
if let lhsNum = lhs as? NSNumber, let rhsNum = rhs as? NSNumber {
return lhsNum == rhsNum
}

// Handle Array comparison
if let lhsArray = lhs as? [Any], let rhsArray = rhs as? [Any] {
guard lhsArray.count == rhsArray.count else { return false }
for (index, lhsElement) in lhsArray.enumerated() where !areValuesEqual(lhsElement, rhsArray[index]) {
return false
}
return true
}

// Handle Dictionary comparison
if let lhsDict = lhs as? [String: Any], let rhsDict = rhs as? [String: Any] {
guard lhsDict.keys == rhsDict.keys else { return false }
for (key, lhsValue) in lhsDict {
guard let rhsValue = rhsDict[key] else { return false }
if !areValuesEqual(lhsValue, rhsValue) {
return false
}
}
return true
}

// Fallback: try string comparison
return "\(lhs)" == "\(rhs)"
}

/// Checks if the game is currently running
/// - Parameter bundlePath: Path to the .app bundle
/// - Returns: true if the game is running, false otherwise
Expand Down
2 changes: 1 addition & 1 deletion macos-launcher/src/TemporaryFolderURL.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

public final class TemporaryFolderURL: ManagedURL {
public final class TemporaryFolderURL {
public let contentURL: URL
public init() {
contentURL = URL(fileURLWithPath: NSTemporaryDirectory())
Expand Down
28 changes: 0 additions & 28 deletions macos-launcher/src/ViewModel/GameUpdaterViewModel.swift

This file was deleted.

28 changes: 3 additions & 25 deletions macos-launcher/src/XsollaLib.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,6 @@ func stagedPatchURL(root: URL, relativePath: String) throws -> URL {
}

class XsollaUpdateParser: NSObject, XMLParserDelegate {

var articleNth = 0

var gameVersion = 0

var actions: [XsollaUpdateAction] = []
Expand Down Expand Up @@ -219,6 +216,7 @@ struct XsollaUpdater {
throw NSError(domain: "XsollaUpdater", code: 1, userInfo: nil)
}

@MainActor
func installedVersion() -> Int {
do {
var gamePath = URL(fileURLWithPath: try self.gamePath())
Expand All @@ -231,22 +229,6 @@ struct XsollaUpdater {
}
}

class UpdateAction {
var type: String
var version: Int
var url: String

init(type: String, version: Int, url: String) {
self.type = type
self.version = version
self.url = url
}
}

func latestGameVersion() async throws -> Int {
return try await self.latestGameVersion(installedVersion: self.installedVersion())
}

private func latestGameVersion(installedVersion: Int) async throws -> Int {
let url = URL(
string: String(
Expand All @@ -273,6 +255,7 @@ struct XsollaUpdater {
throw NSError(domain: "XsollaUpdater", code: 2, userInfo: nil)
}

@MainActor
func checkForUpdateAvailable() async -> Bool {
do {
let installedVersion = self.installedVersion()
Expand All @@ -287,6 +270,7 @@ struct XsollaUpdater {
}
}

@MainActor
func updateGame(delegate: XSollaUpdaterDelegate? = nil) async throws {
let installedVersion = self.installedVersion()
logger.info("Starting game update from installed version \(installedVersion, privacy: .public)")
Expand Down Expand Up @@ -530,8 +514,6 @@ struct XsollaUpdater {
logger.warning("Failed to remove Xsolla temp path: \(error.localizedDescription, privacy: .public)")
}

logger.info("Update complete, setting flag to force entitlement re-application")
UserDefaults.standard.set(true, forKey: "forceEntitlementReapplication")
delegate?.updateProgress(progress: XsollaUpdateProgress.Complete)
logger.info(
"Game update completed from \(installedVersion, privacy: .public) to \(pendingGameVersion ?? parser.gameVersion, privacy: .public)")
Expand All @@ -542,7 +524,3 @@ struct XsollaUpdater {
}
}
}

func run_update() {
//
}
26 changes: 17 additions & 9 deletions macos-launcher/src/rsync.cc
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
#include <cstdio>
#include <librsync.h>
#include <memory>

extern "C" {
int rsync_apply(const char* source_file, const char* patch_file, const char* target_file)
{
rs_stats_t stats;
auto basis_file = fopen(source_file, "rb");
auto delta_file = fopen(patch_file, "rb");
auto new_file = fopen(target_file, "wb");
using File = std::unique_ptr<FILE, decltype(&fclose)>;
File basis_file(fopen(source_file, "rb"), fclose);
if (!basis_file)
return RS_IO_ERROR;
File delta_file(fopen(patch_file, "rb"), fclose);
if (!delta_file)
return RS_IO_ERROR;
File new_file(fopen(target_file, "wb"), fclose);
if (!new_file)
return RS_IO_ERROR;

auto result = rs_patch_file(basis_file, delta_file, new_file, &stats);
fclose(basis_file);
fclose(delta_file);
fclose(new_file);
return result;
rs_stats_t stats{};
auto result = rs_patch_file(basis_file.get(), delta_file.get(), new_file.get(), &stats);
if (result != RS_DONE)
return result;
// Buffered writes can fail only when the output is closed.
return fclose(new_file.release()) == 0 ? RS_DONE : RS_IO_ERROR;
}
}
Loading