diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4a54f53 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true + +[*.{h,cc}] +charset = utf-8 +indent_style = space +indent_size = 4 + +[*.lua] +indent_style = space +indent_size = 4 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6158308..2e46959 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -5,21 +5,21 @@ on: branches: - "**" pull_request: - branches: [ "main", "dev" ] + branches: ["main", "dev"] jobs: - build: + build-win: runs-on: windows-latest steps: - uses: actions/checkout@v4 with: - submodules: 'recursive' + submodules: "recursive" - uses: seanmiddleditch/gha-setup-ninja@master - uses: ilammy/msvc-dev-cmd@v1 with: arch: amd64 - + - uses: xmake-io/github-action-setup-xmake@v1 with: xmake-version: latest @@ -29,12 +29,44 @@ jobs: - name: Build run: xmake -y - + - name: Package run: | mv build/windows/x64/releasedbg/stfc-community-patch.dll version.dll - + - uses: actions/upload-artifact@v2 with: name: stfc-community-patch path: version.dll + + build-mac: + runs-on: macos-13 + steps: + - uses: actions/checkout@v4 + with: + submodules: "recursive" + + - uses: xmake-io/github-action-setup-xmake@v1 + with: + xmake-version: latest + + - name: Set up Homebrew + id: set-up-homebrew + uses: Homebrew/actions/setup-homebrew@master + + - name: Configure + run: xmake f -m release -y -a x86_64 -v + + - name: Build + run: xmake -y + + - name: Install brew things + run: brew install create-dmg + + - name: Create App Bundle + run: scripts/create-mac-dmg.sh + + - uses: actions/upload-artifact@v2 + with: + name: stfc-community-patch-installer.dmg + path: STFC-Community-Patch-Installer.dmg diff --git a/.gitignore b/.gitignore index b9cb8c5..0f2da26 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ version.aps debug.log /app/ /.xmake/ -/vsxmake*/ \ No newline at end of file +/vsxmake*/ +.DS_Store \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index e69de29..adf1ff6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "macos-launcher/deps/PLzmaSDK"] + path = macos-launcher/deps/PLzmaSDK + url = https://github.com/OlehKulykov/PLzmaSDK.git diff --git a/assets/launcher.icns b/assets/launcher.icns new file mode 100644 index 0000000..b6c660b Binary files /dev/null and b/assets/launcher.icns differ diff --git a/assets/launcher.png b/assets/launcher.png new file mode 100644 index 0000000..b0f690b Binary files /dev/null and b/assets/launcher.png differ diff --git a/assets/mac_installer_background.png b/assets/mac_installer_background.png new file mode 100644 index 0000000..afe3969 Binary files /dev/null and b/assets/mac_installer_background.png differ diff --git a/macos-dylib/src/main.cc b/macos-dylib/src/main.cc new file mode 100644 index 0000000..4b3be81 --- /dev/null +++ b/macos-dylib/src/main.cc @@ -0,0 +1,25 @@ +#include +#include +#include +#include + +#include "patches/patches.h" + +__attribute__((constructor)) +void myconstructor(int argc, const char **argv) +{ + syslog(LOG_ERR, "[+] dylib injected in %s\n", argv[0]); + ApplyPatches(); +} + +void* operator new[](size_t size, const char* /*name*/, int /*flags*/, unsigned /*debugFlags*/, const char* /*file*/, + int /*line*/) +{ + return malloc(size); +} + +void* operator new[](size_t size, size_t /*alignment*/, size_t /*alignmentOffset*/, const char* /*name*/, int /*flags*/, + unsigned /*debugFlags*/, const char* /*file*/, int /*line*/) +{ + return malloc(size); +} diff --git a/macos-dylib/xmake.lua b/macos-dylib/xmake.lua new file mode 100644 index 0000000..8477adb --- /dev/null +++ b/macos-dylib/xmake.lua @@ -0,0 +1,8 @@ +target("stfc-community-patch") +do + set_kind("shared") + add_files("src/*.cc") + add_deps("mods") + set_exceptions("cxx") + set_policy("build.optimization.lto", true) +end diff --git a/macos-launcher/deps/PLzmaSDK b/macos-launcher/deps/PLzmaSDK new file mode 160000 index 0000000..9f48b99 --- /dev/null +++ b/macos-launcher/deps/PLzmaSDK @@ -0,0 +1 @@ +Subproject commit 9f48b99d8aee09c98184e4610b40ee87487d1643 diff --git a/macos-launcher/src/ActionView.swift b/macos-launcher/src/ActionView.swift new file mode 100644 index 0000000..4142e28 --- /dev/null +++ b/macos-launcher/src/ActionView.swift @@ -0,0 +1,254 @@ +import Foundation +import SwiftUI + +struct ActionView: View, XSollaUpdaterDelegate { + + @StateObject var gameUpdater = GameUpdaterViewModel() + @State private var gameVersion: Int = 0 + @State private var gameUpdateAvailable: Bool = false + @State private var updating: Bool = false + @State private var updateAction: String = "" + @State private var updateSubAction: String = "" + @State private var updateProgress: Float = 0.0 + @State private var gameInstalled: Bool = false + @State private var gameRunning: Bool = false + private var model = PulsatingViewModel() + + func updateProgress(progress: XsollaUpdateProgress) { + switch progress { + case .Start(_): + break + case .Progress(let currentAction, let totalActions): + updateAction = "Updating \(currentAction) of \(totalActions)" + updateProgress = Float(currentAction) / Float(totalActions) + break + case .Extracting(_): + updateSubAction = "Extracting" + break + case .ExtractComplete(_): + break + case .Downloading(_): + updateSubAction = "Downloading" + break + case .DownloadComplete(_): + break + case .Patching(_): + updateSubAction = "Patching" + break + case .PatchingProgress(_, _): + break + case .PatchStepComplete: + break + case .PatchComplete: + break + case .Waiting: + updateSubAction = "Waiting" + break + case .ApplyVersion: + break + case .VersionApplied: + break + case .Finalizing: + updateSubAction = "Finalizing" + break + case .CleaningUp: + updateSubAction = "Cleaning Up" + break + case .Complete: + updateSubAction = "Complete" + break + } + } + + var body: some View { + GeometryReader { geo in + Grid { + GridRow { + if gameInstalled { + Button { + } label: { + commonButton() + .foregroundColor(.lcarViolet) + }.buttonStyle(PlainButtonStyle()) + Button { + withAnimation { + if gameUpdateAvailable && !updating { + Task { + do { + updating = true + updateAction = "Starting" + updateSubAction = "Planning" + try await gameUpdater.updateGame(delegate: self) + } catch { + print("Error updating game: \(error)") + } + updating = false + gameVersion = gameUpdater.getInstalledGameVersion() + gameUpdateAvailable = await gameUpdater.checkForGameUpdate() + } + } + } + } label: { + if gameUpdateAvailable { + commonButton(text: "Update Game!") + .foregroundColor(.lcarTan) + } else { + commonButton() + .foregroundColor(.lcarTan) + } + }.buttonStyle(PlainButtonStyle()) + Button { + withAnimation { + launchGame() + } + } label: { + commonButton(text: "Engage!") + .foregroundColor(.lcarOrange) + }.buttonStyle(PlainButtonStyle()) + } else { + Text("Game not installed") + .font(.custom("HelveticaNeue-CondensedBold", size: 40)) + .foregroundColor(.lcarTan) + .offset(x: -45) + //Button { + // withAnimation { + // } + //} label: { + // commonButton(text: "Install Game!") + // .foregroundColor(.lcarTan) + //}.buttonStyle(PlainButtonStyle()) + } + + } + .opacity(updating || gameRunning ? 0.5 : 1.0) + .allowsHitTesting(!updating && !gameRunning) + } + .frame(width: geo.size.width, height: 150) + .offset(x: 85, y: 20) + .task { + repeat { + gameVersion = gameUpdater.getInstalledGameVersion() + gameInstalled = gameVersion > 0 + gameVersion = gameUpdater.getInstalledGameVersion() + gameUpdateAvailable = await gameUpdater.checkForGameUpdate() + try? await Task.sleep(for: .seconds(60)) + } while !Task.isCancelled + + } + .overlay(alignment: .bottomTrailing) { + Text("Game Version: \(String(format: "%02d", gameVersion))") + .font(.custom("HelveticaNeue-CondensedBold", size: 17)) + .foregroundColor(.lcarTan) + .offset(x: -10) + } + .overlay(alignment: .bottomLeading) { + if updating { + HStack { + PulsatingView(viewModel: model) + Text("\(updateAction) (\(updateSubAction))") + .font(.custom("HelveticaNeue-CondensedBold", size: 17)) + .foregroundColor(.lcarTan) + .offset(x: -10) + } + .offset(x: 115, y: 10) + } + } + } + } + + private func commonButton(text: String = "") -> some View { + RoundedRectangle(cornerRadius: 20) + .frame(width: 125, height: 50) + .overlay(alignment: .bottomTrailing) { + HStack { + Spacer() + Text(text.count > 0 ? text : "\(randomDigits(4))-\(randomDigits(3))") + .font(.custom("HelveticaNeue-CondensedBold", size: 17)) + .foregroundColor(.black) + } + .scaleEffect(x: 0.7, anchor: .trailing) + .padding(.bottom, 5) + .padding(.trailing, 20) + } + } + + private func randomDigits(_ count: Int) -> String { + (1...count) + .map { _ in "\(Int.random(in: 0...9))" } + .joined() + } + + private func launchGame() { + DispatchQueue.global().async { + DispatchQueue.main.async { + gameRunning = true + } + let process = Process() + let helper = Bundle.main.path(forAuxiliaryExecutable: "stfc-community-patch-loader") + process.executableURL = URL(fileURLWithPath: helper!) + DispatchQueue.global().async { + do { + try process.run() + } catch { + DispatchQueue.main.async { + gameRunning = false + } + return + } + process.waitUntilExit() + DispatchQueue.main.async { + gameRunning = false + } + } + } + } +} + +class PulsatingViewModel: ObservableObject { + @Published var colorIndex = 1 +} + +struct PulsatingView: View { + + @ObservedObject var viewModel: PulsatingViewModel + + func colourToShow() -> Color { + switch viewModel.colorIndex { + case 0: + return Color.lcarOrange + case 1: + return Color.lcarTan + case 2: + return Color.lcarViolet + default: + return Color.lcarPink + } + } + + @State var animate = false + var body: some View { + VStack { + ZStack { + Circle().fill(colourToShow().opacity(0.25)).frame(width: 40, height: 40).scaleEffect( + self.animate ? 1 : 0) + Circle().fill(colourToShow().opacity(0.35)).frame(width: 30, height: 30).scaleEffect( + self.animate ? 1 : 0) + Circle().fill(colourToShow().opacity(0.45)).frame(width: 15, height: 15).scaleEffect( + self.animate ? 1 : 0) + Circle().fill(colourToShow()).frame(width: 16.25, height: 16.25) + } + .onAppear { self.animate = true } + .animation( + animate ? Animation.easeInOut(duration: 1.5).repeatForever(autoreverses: true) : .default, + value: animate + ) + .onChange(of: viewModel.colorIndex) { _ in + self.animate = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.animate = true + } + } + + } + } +} diff --git a/macos-launcher/src/Assets.xcassets/AccentColor.colorset/Contents.json b/macos-launcher/src/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/macos-launcher/src/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos-launcher/src/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos-launcher/src/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..3f00db4 --- /dev/null +++ b/macos-launcher/src/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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 + } +} diff --git a/macos-launcher/src/Assets.xcassets/Contents.json b/macos-launcher/src/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/macos-launcher/src/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos-launcher/src/Assets.xcassets/lcarLightOrange.colorset/Contents.json b/macos-launcher/src/Assets.xcassets/lcarLightOrange.colorset/Contents.json new file mode 100644 index 0000000..6bf7e8c --- /dev/null +++ b/macos-launcher/src/Assets.xcassets/lcarLightOrange.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x99", + "green" : "0xCC", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos-launcher/src/Assets.xcassets/lcarOrange.colorset/Contents.json b/macos-launcher/src/Assets.xcassets/lcarOrange.colorset/Contents.json new file mode 100644 index 0000000..e9ed54d --- /dev/null +++ b/macos-launcher/src/Assets.xcassets/lcarOrange.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0", + "green" : "147", + "red" : "255" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos-launcher/src/Assets.xcassets/lcarPink.colorset/Contents.json b/macos-launcher/src/Assets.xcassets/lcarPink.colorset/Contents.json new file mode 100644 index 0000000..23dd551 --- /dev/null +++ b/macos-launcher/src/Assets.xcassets/lcarPink.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xCC", + "green" : "0x99", + "red" : "0xCC" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos-launcher/src/Assets.xcassets/lcarPlum.colorset/Contents.json b/macos-launcher/src/Assets.xcassets/lcarPlum.colorset/Contents.json new file mode 100644 index 0000000..fc43090 --- /dev/null +++ b/macos-launcher/src/Assets.xcassets/lcarPlum.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x66", + "green" : "0x66", + "red" : "0xCC" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos-launcher/src/Assets.xcassets/lcarTan.colorset/Contents.json b/macos-launcher/src/Assets.xcassets/lcarTan.colorset/Contents.json new file mode 100644 index 0000000..e108918 --- /dev/null +++ b/macos-launcher/src/Assets.xcassets/lcarTan.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x66", + "green" : "0x99", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos-launcher/src/Assets.xcassets/lcarViolet.colorset/Contents.json b/macos-launcher/src/Assets.xcassets/lcarViolet.colorset/Contents.json new file mode 100644 index 0000000..113cb4c --- /dev/null +++ b/macos-launcher/src/Assets.xcassets/lcarViolet.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0x99", + "red" : "0x99" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos-launcher/src/Assets.xcassets/lcarWhite.colorset/Contents.json b/macos-launcher/src/Assets.xcassets/lcarWhite.colorset/Contents.json new file mode 100644 index 0000000..8abb6c6 --- /dev/null +++ b/macos-launcher/src/Assets.xcassets/lcarWhite.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xDB", + "green" : "0xFB", + "red" : "0xF8" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos-launcher/src/Assets.xcassets/ufp-logo.imageset/Contents.json b/macos-launcher/src/Assets.xcassets/ufp-logo.imageset/Contents.json new file mode 100644 index 0000000..074bed6 --- /dev/null +++ b/macos-launcher/src/Assets.xcassets/ufp-logo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ufp-logo.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos-launcher/src/Assets.xcassets/ufp-logo.imageset/ufp-logo.png b/macos-launcher/src/Assets.xcassets/ufp-logo.imageset/ufp-logo.png new file mode 100644 index 0000000..b3d07e2 Binary files /dev/null and b/macos-launcher/src/Assets.xcassets/ufp-logo.imageset/ufp-logo.png differ diff --git a/macos-launcher/src/ContentView.swift b/macos-launcher/src/ContentView.swift new file mode 100644 index 0000000..12fc31e --- /dev/null +++ b/macos-launcher/src/ContentView.swift @@ -0,0 +1,268 @@ +// +// ContentView.swift +// STFC Community Patch Launcher +// +// Created by tashcan on 3/22/24. +// + +import Foundation +import SwiftUI + +struct ContentView: View, XSollaUpdaterDelegate { + @State private var updateAction: String = "" + + var body: some View { + GeometryReader { geo in + ZStack { + VStack(spacing: 10) { + topContent + .frame(height: geo.size.height / 1.5) + bottomContent + .frame(height: geo.size.height * (2 / 2) - 10) + } + ActionView().offset(y: geo.size.height - 150) + } + } + .edgesIgnoringSafeArea(.all) + } + + private var topContent: some View { + GeometryReader { geo in + ZStack { + VStack(spacing: 5) { + Color.lcarPink + Color.lcarViolet + } + .cornerRadius(70, corners: .bottomLeft) + .overlay(alignment: .topTrailing) { + Color.black + .cornerRadius(35, corners: .bottomLeft) + .frame(width: geo.size.width - 100, height: geo.size.height - 20) + } + .overlay(alignment: .topLeading) { + Color.black + .frame(width: 100, height: 50) + } + .overlay(alignment: .bottomTrailing) { + ZStack { + Color.black + HStack(spacing: 5) { + Color.lcarOrange + .frame(width: 20) + .padding(.leading, 5) + Color.lcarPink + .frame(width: 50) + Color.lcarPink + Color.lcarPlum + .frame(width: 20) + } + } + .frame(width: 200, height: 20) + } + .overlay(alignment: .leading) { + VStack(alignment: .trailing, spacing: 15) { + Text("LCARS \(randomDigits(5))") + Text("02-\(randomDigits(6))") + } + .font(.custom("HelveticaNeue-CondensedBold", size: 17)) + .foregroundColor(.black) + .scaleEffect(x: 0.7, anchor: .trailing) + .frame(width: 90) + } + .overlay(alignment: .topTrailing) { + Text("LCARS ACCESS 333") + .font(.custom("HelveticaNeue-CondensedBold", size: 35)) + .padding(.top, 45) + .foregroundColor(.lcarOrange) + .scaleEffect(x: 0.7, anchor: .trailing) + .offset(x: -15, y: 0) + } + .overlay(alignment: .trailing) { + Grid(alignment: .trailing) { + ForEach(0..<7) { row in + GridRow { + ForEach(0..<8) { _ in + Text(randomDigits(Int.random(in: 1...6))) + .foregroundColor((row == 3 || row == 4) ? .lcarWhite : .lcarOrange) + } + } + } + } + .font(.custom("HelveticaNeue-CondensedBold", size: 17)) + .padding(.top, 45) + .offset(x: -15, y: 0) + } + } + } + } + + private var bottomContent: some View { + GeometryReader { geo in + ZStack { + VStack(alignment: .leading, spacing: 5) { + Color.lcarPlum + .frame(height: 100) + .overlay(alignment: .bottomLeading) { + commonLabel(prefix: "03") + .padding(.bottom, 5) + } + Color.lcarPlum + .frame(height: 200) + .overlay(alignment: .bottomLeading) { + commonLabel(prefix: "04") + .padding(.bottom, 5) + } + Color.lcarOrange + .frame(height: 50) + .overlay(alignment: .leading) { + commonLabel(prefix: "05") + } + Color.lcarTan + .overlay(alignment: .topLeading) { + commonLabel(prefix: "06") + .padding(.top, 5) + } + } + .cornerRadius(70, corners: .topLeft) + .overlay(alignment: .bottomTrailing) { + Color.black + .cornerRadius(35, corners: .topLeft) + .frame(width: geo.size.width - 100, height: geo.size.height - 20) + } + .overlay(alignment: .bottomLeading) { + Color.black + .frame(width: 100, height: 50) + } + .overlay(alignment: .topTrailing) { + ZStack { + Color.black + HStack(alignment: .top, spacing: 5) { + Color.lcarLightOrange + .frame(width: 20) + .padding(.leading, 5) + Color.lcarLightOrange + .frame(width: 50, height: 10) + Color.lcarPink + Color.lcarOrange + .frame(width: 20) + } + } + .frame(width: 200, height: 20) + } + .overlay { + Image("ufp-logo") + .opacity(0.15) + .offset(x: 50, y: -200) + } + } + } + } + + 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() + Text("\(prefix)-\(randomDigits(6))") + .font(.custom("HelveticaNeue-CondensedBold", size: 17)) + .foregroundColor(.black) + } + .frame(width: 90) + .scaleEffect(x: 0.7, anchor: .trailing) + } + + private func randomDigits(_ count: Int) -> String { + (1...count) + .map { _ in "\(Int.random(in: 0...9))" } + .joined() + } + + func updateProgress(progress: XsollaUpdateProgress) { + } +} + +extension Color { + static let lcarLightOrange = Color("lcarLightOrange") + static let lcarOrange = Color("lcarOrange") + static let lcarPink = Color("lcarPink") + static let lcarPlum = Color("lcarPlum") + static let lcarTan = Color("lcarTan") + static let lcarViolet = Color("lcarViolet") + static let lcarWhite = Color("lcarWhite") +} + +extension View { + func cornerRadius(_ radius: CGFloat, corners: RectCorner) -> some View { + clipShape(RoundedCornersShape(radius: radius, corners: corners)) + } +} + +// draws shape with specified rounded corners applying corner radius +struct RoundedCornersShape: SwiftUI.Shape { + + var radius: CGFloat = .zero + var corners: RectCorner = .allCorners + + func path(in rect: CGRect) -> SwiftUI.Path { + var path = SwiftUI.Path() + + let p1 = CGPoint(x: rect.minX, y: corners.contains(.topLeft) ? rect.minY + radius : rect.minY) + let p2 = CGPoint(x: corners.contains(.topLeft) ? rect.minX + radius : rect.minX, y: rect.minY) + + let p3 = CGPoint(x: corners.contains(.topRight) ? rect.maxX - radius : rect.maxX, y: rect.minY) + let p4 = CGPoint(x: rect.maxX, y: corners.contains(.topRight) ? rect.minY + radius : rect.minY) + + let p5 = CGPoint( + x: rect.maxX, y: corners.contains(.bottomRight) ? rect.maxY - radius : rect.maxY) + let p6 = CGPoint( + x: corners.contains(.bottomRight) ? rect.maxX - radius : rect.maxX, y: rect.maxY) + + let p7 = CGPoint( + x: corners.contains(.bottomLeft) ? rect.minX + radius : rect.minX, y: rect.maxY) + let p8 = CGPoint( + x: rect.minX, y: corners.contains(.bottomLeft) ? rect.maxY - radius : rect.maxY) + + path.move(to: p1) + path.addArc( + tangent1End: CGPoint(x: rect.minX, y: rect.minY), + tangent2End: p2, + radius: radius) + path.addLine(to: p3) + path.addArc( + tangent1End: CGPoint(x: rect.maxX, y: rect.minY), + tangent2End: p4, + radius: radius) + path.addLine(to: p5) + path.addArc( + tangent1End: CGPoint(x: rect.maxX, y: rect.maxY), + tangent2End: p6, + radius: radius) + path.addLine(to: p7) + path.addArc( + tangent1End: CGPoint(x: rect.minX, y: rect.maxY), + tangent2End: p8, + radius: radius) + path.closeSubpath() + + return path + } +} + +struct RectCorner: OptionSet { + let rawValue: Int + + static let topLeft = RectCorner(rawValue: 1 << 0) + static let topRight = RectCorner(rawValue: 1 << 1) + static let bottomRight = RectCorner(rawValue: 1 << 2) + static let bottomLeft = RectCorner(rawValue: 1 << 3) + + static let allCorners: RectCorner = [.topLeft, topRight, .bottomLeft, .bottomRight] +} diff --git a/macos-launcher/src/GitHubLib.swift b/macos-launcher/src/GitHubLib.swift new file mode 100644 index 0000000..3c15896 --- /dev/null +++ b/macos-launcher/src/GitHubLib.swift @@ -0,0 +1,16 @@ +import Foundation + +struct GitHubUpdater { + func latestPatchVersion() async -> Int { + let url = URL(string: "https://api.github.com/repos/tashcan/bob/releases/latest")! + do { + let (data, _) = try await URLSession.shared.data(from: url) + let json = try JSONSerialization.jsonObject(with: data) as! [String: Any] + let tagName = json["tag_name"] as! String + let versionSegments = tagName.split(separator: ".") + return Int(versionSegments[1]) ?? 0 + } catch { + return 0 + } + } +} diff --git a/macos-launcher/src/Info.plist b/macos-launcher/src/Info.plist new file mode 100644 index 0000000..fc6b04a --- /dev/null +++ b/macos-launcher/src/Info.plist @@ -0,0 +1,40 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + + CFBundleName + STFC Patch + CFBundleDisplayName + Star Trek Fleet Command Community Patch + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + 14.0 + + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSupportedPlatforms + + MacOSX + + CFBundleVersion + 1 + CFBundleIconFile + launcher + + diff --git a/macos-launcher/src/LauncherMain.swift b/macos-launcher/src/LauncherMain.swift new file mode 100644 index 0000000..c86bb69 --- /dev/null +++ b/macos-launcher/src/LauncherMain.swift @@ -0,0 +1,42 @@ +// +// LauncherMain.swift +// STFC Community Patch Launcher +// +// Created by tashcan on 3/22/24. +// + +import AppKit +import Foundation +import SwiftUI + +extension Scene { + func windowResizabilityContentSize() -> some Scene { + if #available(macOS 13.0, *) { + return windowResizability(.contentSize) + } else { + return self + } + } +} + +class AppDelegate: NSObject, NSApplicationDelegate { + func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + NSApplication.shared.terminate(self) + return true + } +} + +@main +struct STFC_Community_Patch_LauncherApp: App { + @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + + var body: some Scene { + WindowGroup { + ContentView() + .frame(width: 600, height: 400) + .fixedSize() + } + .windowResizabilityContentSize() + } + +} diff --git a/macos-launcher/src/ManagedURL.swift b/macos-launcher/src/ManagedURL.swift new file mode 100644 index 0000000..cb919ec --- /dev/null +++ b/macos-launcher/src/ManagedURL.swift @@ -0,0 +1,14 @@ +import Foundation + +public protocol ManagedURL { + var contentURL: URL { get } + func keepAlive() +} + +extension ManagedURL { + public func keepAlive() {} +} + +extension URL: ManagedURL { + public var contentURL: URL { return self } +} diff --git a/macos-launcher/src/TemporaryFolderURL.swift b/macos-launcher/src/TemporaryFolderURL.swift new file mode 100644 index 0000000..2fe3c83 --- /dev/null +++ b/macos-launcher/src/TemporaryFolderURL.swift @@ -0,0 +1,15 @@ +import Foundation + +public final class TemporaryFolderURL: ManagedURL { + public let contentURL: URL + public init() { + contentURL = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent(UUID().uuidString) + } + + deinit { + DispatchQueue.global(qos: .utility).async { [contentURL = self.contentURL] in + try? FileManager.default.removeItem(at: contentURL) + } + } +} diff --git a/macos-launcher/src/ViewModel/GameUpdaterViewModel.swift b/macos-launcher/src/ViewModel/GameUpdaterViewModel.swift new file mode 100644 index 0000000..7a1fe85 --- /dev/null +++ b/macos-launcher/src/ViewModel/GameUpdaterViewModel.swift @@ -0,0 +1,28 @@ +import Foundation + +class GameUpdaterViewModel: ObservableObject { + @Published var latestGameVersion = 0 + + var gameUpdater: XsollaUpdater = XsollaUpdater("Star Trek Fleet Command") + + @MainActor + func fetchLatestGameVersion() async -> Int { + latestGameVersion = try! await gameUpdater.latestGameVersion() + return latestGameVersion + } + + @MainActor + func getInstalledGameVersion() -> Int { + return gameUpdater.installedVersion() + } + + @MainActor + func checkForGameUpdate() async -> Bool { + return await gameUpdater.checkForUpdateAvailable() + } + + @MainActor + func updateGame(delegate: XSollaUpdaterDelegate? = nil) async throws { + try await gameUpdater.updateGame(delegate: delegate) + } +} diff --git a/macos-launcher/src/XsollaLib.swift b/macos-launcher/src/XsollaLib.swift new file mode 100644 index 0000000..81869d9 --- /dev/null +++ b/macos-launcher/src/XsollaLib.swift @@ -0,0 +1,381 @@ +import Foundation + +struct DownloadAction { + var url: String + var size: Int + var to: String +} + +struct ExtractAction { + var file: String + var to: String +} + +struct PatchAction { + var binaries: String + var patch: String +} + +struct VersionAction { + var version: Int +} + +enum XsollaUpdateAction { + case Download(DownloadAction) + case Extract(ExtractAction) + case Patch(PatchAction) + case Version(VersionAction) + case WaitActions +} + +struct PatchRule: Decodable { + var file_size: Optional + var relative_path: String + var rule: String + var sha512: Optional +} + +class XsollaUpdateParser: NSObject, XMLParserDelegate { + + var articleNth = 0 + + var gameVersion = 0 + + var actions: [XsollaUpdateAction] = [] + + func parser( + _ parser: XMLParser, + didStartElement elementName: String, + namespaceURI: String?, + qualifiedName qName: String?, + attributes attributeDict: [String: String] = [:] + ) { + if elementName == "action" { + switch attributeDict["type"] { + case "torrent_download": + actions.append( + XsollaUpdateAction.Download( + DownloadAction( + url: attributeDict["alt_data_link"]!, + size: Int(attributeDict["data_size"]!)!, + to: attributeDict["alt_to"]!))) + break + case "extract": + actions.append( + XsollaUpdateAction.Extract( + ExtractAction( + file: attributeDict["file"]!, + to: attributeDict["to"]!))) + break + case "patch": + actions.append( + XsollaUpdateAction.Patch( + PatchAction(binaries: attributeDict["binaries"]!, patch: attributeDict["patch"]!))) + break + case "wait_actions": + actions.append(XsollaUpdateAction.WaitActions) + break + case "extracted_size": + break + case "version": + gameVersion = Int(attributeDict["version"]!)! + actions.append(XsollaUpdateAction.Version(VersionAction(version: gameVersion))) + break + default: + print("Unknown action type: \(attributeDict["type"]!)") + break + } + } + } +} + +enum XsollaUpdateProgress { + case Start(totalActions: Int) + case Progress(currentAction: Int, totalActions: Int) + case Extracting(currentFile: String) + case ExtractComplete(currentFile: String) + case Downloading(url: String) + case DownloadComplete(url: String) + case Patching(totalFiles: Int) + case PatchingProgress(currentBytes: Int, totalBytes: Int) + case PatchStepComplete + case PatchComplete + case Waiting + case ApplyVersion + case VersionApplied + case Finalizing + case CleaningUp + case Complete +} + +protocol XSollaUpdaterDelegate { + func updateProgress(progress: XsollaUpdateProgress) +} + +struct XsollaUpdater { + var gameName: String + + public init(_ gameName: String) { + self.gameName = gameName + } + + func gamePath() throws -> String { + let library = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first! + let preferences = library.appendingPathComponent("Preferences").appendingPathComponent( + self.gameName) + let settingsIniPath = preferences.appendingPathComponent("launcher_settings.ini") + let settingsIni = parseConfig(settingsIniPath.path) + let gamePath = settingsIni["General"]?["152033..GAME_PATH"] + if gamePath == nil { + throw NSError(domain: "XsollaUpdater", code: 1, userInfo: nil) + } + if gamePath!.starts(with: "//") { + return String(gamePath!.dropFirst()) + } + return gamePath! + } + + func gameTempPath() throws -> String { + let library = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first! + let preferences = library.appendingPathComponent("Preferences").appendingPathComponent( + self.gameName) + let settingsIniPath = preferences.appendingPathComponent("launcher_settings.ini") + let settingsIni = parseConfig(settingsIniPath.path) + let gameTempPath = settingsIni["General"]?["152033..GAME_TEMP_PATH"] + if gameTempPath!.starts(with: "//") { + return String(gameTempPath!.dropFirst()) + } + return gameTempPath! + } + + func installedVersion() -> Int { + do { + var gamePath = URL(fileURLWithPath: try self.gamePath()) + gamePath.appendPathComponent(".version") + let versionFile = try String(contentsOf: gamePath) + let versionSegments = versionFile.split(separator: "=") + return Int(versionSegments[1]) ?? 0 + } catch { + return 0 + } + } + + 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 { + let url = URL( + string: String( + format: + "https://gus.xsolla.com/updates?version=%d&project_id=152033®ion=&platform=mac_os", + self.installedVersion()))! + let (data, _) = try await URLSession.shared.data(from: url) + + let xml = XMLParser(data: data) + let parser = XsollaUpdateParser() + xml.delegate = parser + xml.parse() + return parser.gameVersion + } + + func checkForUpdateAvailable() async -> Bool { + do { + let latestGameVersion = try await self.latestGameVersion() + return self.installedVersion() < latestGameVersion + } catch { + return false + } + } + + func updateGame(delegate: XSollaUpdaterDelegate? = nil) async throws { + defer { delegate?.updateProgress(progress: XsollaUpdateProgress.Complete) } + let url = URL( + string: String( + format: + "https://gus.xsolla.com/updates?version=%d&project_id=152033®ion=&platform=mac_os", + self.installedVersion()))! + let (data, _) = try await URLSession.shared.data(from: url) + + let xml = XMLParser(data: data) + let parser = XsollaUpdateParser() + xml.delegate = parser + xml.parse() + + var tempGamePath = try self.gameTempPath() + if tempGamePath.hasSuffix("/") || tempGamePath.hasSuffix("\\") { + tempGamePath = String(tempGamePath.dropLast()) + } + var gamePath = try self.gamePath() + if gamePath.hasSuffix("/") || gamePath.hasSuffix("\\") { + gamePath = String(gamePath.dropLast()) + } + let tempPath = TemporaryFolderURL() + do { + try FileManager.default.removeItem(atPath: tempGamePath) + } catch {} + try FileManager.default.createDirectory( + atPath: tempGamePath, withIntermediateDirectories: true, attributes: nil) + + delegate?.updateProgress( + progress: XsollaUpdateProgress.Start(totalActions: parser.actions.count)) + + var currentAction = 0 + for action in parser.actions { + currentAction += 1 + delegate?.updateProgress( + progress: XsollaUpdateProgress.Progress( + currentAction: currentAction, totalActions: parser.actions.count)) + switch action { + case .Download(let downloadAction): + delegate?.updateProgress( + progress: XsollaUpdateProgress.Downloading(url: downloadAction.url)) + let (localURL, response) = try await URLSession.shared.download( + from: URL(string: downloadAction.url)!) + let toPath = downloadAction.to.replacingOccurrences(of: "$temp_path", with: tempGamePath) + delegate?.updateProgress( + progress: XsollaUpdateProgress.DownloadComplete(url: downloadAction.url)) + try FileManager.default.moveItem(at: localURL, to: URL(fileURLWithPath: toPath)) + break + case .Extract(let extractAction): + let fromPath = extractAction.file.replacingOccurrences(of: "$temp_path", with: tempGamePath) + let toPath = extractAction.to.replacingOccurrences(of: "$temp_path", with: tempGamePath) + let archivePath = try Path(fromPath) + let archivePathInStream = try InStream(path: archivePath) + let decoder = try Decoder( + stream: archivePathInStream, fileType: .sevenZ) + let _ = try decoder.open() + delegate?.updateProgress(progress: XsollaUpdateProgress.Extracting(currentFile: fromPath)) + let _ = try decoder.extract(to: Path(toPath)) + delegate?.updateProgress( + progress: XsollaUpdateProgress.ExtractComplete(currentFile: fromPath)) + break + case .Patch(let patchAction): + var binaries = patchAction.binaries + binaries = binaries.replacingOccurrences(of: "$temp_path", with: tempGamePath) + binaries = binaries.replacingOccurrences( + of: "$game_path", with: gamePath) + let patch = patchAction.patch.replacingOccurrences(of: "$temp_path", with: tempGamePath) + let patch_rules_json = URL(fileURLWithPath: patch).appendingPathComponent("patchRules.json") + let patch_rules = try JSONDecoder().decode( + [PatchRule].self, from: Data(contentsOf: patch_rules_json)) + delegate?.updateProgress( + progress: XsollaUpdateProgress.Patching(totalFiles: patch_rules.count)) + for rule in patch_rules { + var relativePath = rule.relative_path.trimmingCharacters(in: .whitespacesAndNewlines) + if relativePath.starts(with: "/") || relativePath.starts(with: "\\") { + relativePath = String(relativePath.dropFirst()) + } + + let targetPath = tempPath.contentURL.appendingPathComponent(relativePath) + let sourcePath = URL(fileURLWithPath: gamePath).appendingPathComponent(relativePath) + let patchPath = URL(fileURLWithPath: patch).appendingPathComponent(relativePath) + + //delegate?.updateProgress() + switch rule.rule { + case "patch": + if !FileManager.default.fileExists(atPath: targetPath.path) { + try FileManager.default.createDirectory( + at: targetPath.deletingLastPathComponent(), withIntermediateDirectories: true, + attributes: nil) + try FileManager.default.copyItem(at: sourcePath, to: targetPath) + } + try rsyncApply(source: sourcePath, patch: patchPath, output: targetPath) + break + case "create": + if !FileManager.default.fileExists(atPath: targetPath.path) { + try FileManager.default.createDirectory( + at: targetPath.deletingLastPathComponent(), withIntermediateDirectories: true, + attributes: nil) + try Data().write(to: targetPath) + } + case "delete": + do { + try FileManager.default.removeItem(at: sourcePath) + } catch { + print("Error deleting file \(sourcePath)") + } + case "copy": + if !FileManager.default.fileExists(atPath: targetPath.path) { + try FileManager.default.createDirectory( + at: targetPath.deletingLastPathComponent(), withIntermediateDirectories: true, + attributes: nil) + } + try FileManager.default.copyItem(at: patchPath, to: targetPath) + break + default: + print("Unknown rule \(rule.rule)") + break + } + delegate?.updateProgress(progress: XsollaUpdateProgress.PatchStepComplete) + } + delegate?.updateProgress(progress: XsollaUpdateProgress.PatchComplete) + break + case .WaitActions: + delegate?.updateProgress(progress: XsollaUpdateProgress.Waiting) + break + case .Version(let versionAction): + var versionPath = URL(fileURLWithPath: gamePath) + versionPath.appendPathComponent(".version") + let fileVersion = String(format: "&game=%d", versionAction.version) + delegate?.updateProgress(progress: XsollaUpdateProgress.ApplyVersion) + try fileVersion.write(to: versionPath, atomically: true, encoding: .utf8) + delegate?.updateProgress(progress: XsollaUpdateProgress.VersionApplied) + break + + } + } + delegate?.updateProgress(progress: XsollaUpdateProgress.Finalizing) + copyContentsOfDirectory(from: tempPath.contentURL, to: URL(fileURLWithPath: gamePath)) + delegate?.updateProgress(progress: XsollaUpdateProgress.CleaningUp) + try FileManager.default.removeItem(atPath: tempGamePath) + + } +} + +func copyContentsOfDirectory(from sourceURL: URL, to targetURL: URL) { + let fileManager = FileManager.default + + guard + let sourceContents = try? fileManager.contentsOfDirectory( + at: sourceURL, includingPropertiesForKeys: nil) + else { + print("Error accessing contents of directory at \(sourceURL.path)") + return + } + + for sourceItem in sourceContents { + let destinationItem = targetURL.appendingPathComponent(sourceItem.lastPathComponent) + + do { + if try sourceItem.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false { + // If sourceItem is a directory, create corresponding directory in the target location + try fileManager.createDirectory( + at: destinationItem, withIntermediateDirectories: true, attributes: nil) + copyContentsOfDirectory(from: sourceItem, to: destinationItem) // Recursively copy contents of subdirectory + } else { + // If sourceItem is not a directory, check if a file with the same name exists in the target location + if fileManager.fileExists(atPath: destinationItem.path) { + // If a file with the same name exists, remove it before copying + try fileManager.removeItem(at: destinationItem) + } + // Copy the file from the source directory to the target directory + try fileManager.copyItem(at: sourceItem, to: destinationItem) + } + } catch { + print("Error copying \(sourceItem.lastPathComponent) to \(destinationItem.path): \(error)") + } + } +} + +func run_update() { + // +} diff --git a/macos-launcher/src/ini.swift b/macos-launcher/src/ini.swift new file mode 100644 index 0000000..3c67a74 --- /dev/null +++ b/macos-launcher/src/ini.swift @@ -0,0 +1,53 @@ +import Foundation + +typealias SectionConfig = [String: String] +typealias Config = [String: SectionConfig] + +private func trim(_ s: String) -> String { + let whitespaces = CharacterSet(charactersIn: " \n\r\t") + return s.trimmingCharacters(in: whitespaces) +} + +private func stripComment(_ line: String) -> String { + let parts = line.split( + separator: "#", + maxSplits: 1, + omittingEmptySubsequences: false) + if parts.count > 0 { + return String(parts[0]) + } + return "" +} + +private func parseSectionHeader(_ line: String) -> String { + let from = line.index(after: line.startIndex) + let to = line.index(before: line.endIndex) + return String(line[from.. (String, String)? { + let parts = stripComment(line).split(separator: "=", maxSplits: 1) + if parts.count == 2 { + let k = trim(String(parts[0])) + let v = trim(String(parts[1])) + return (k, v) + } + return nil +} + +func parseConfig(_ filename: String) -> Config { + let f = try! String(contentsOfFile: filename) + var config = Config() + var currentSectionName = "main" + for line in f.components(separatedBy: "\n") { + let line = trim(line) + if line.hasPrefix("[") && line.hasSuffix("]") { + currentSectionName = parseSectionHeader(line) + } else if let (k, v) = parseLine(line) { + var section = config[currentSectionName] ?? [:] + section[k] = v + config[currentSectionName] = section + } + } + return config +} diff --git a/macos-launcher/src/launcher.icns b/macos-launcher/src/launcher.icns new file mode 100644 index 0000000..b8cc492 Binary files /dev/null and b/macos-launcher/src/launcher.icns differ diff --git a/macos-launcher/src/macOSLauncher.entitlements b/macos-launcher/src/macOSLauncher.entitlements new file mode 100644 index 0000000..311b32b --- /dev/null +++ b/macos-launcher/src/macOSLauncher.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/macos-launcher/src/module.modulemap b/macos-launcher/src/module.modulemap new file mode 100644 index 0000000..e29611b --- /dev/null +++ b/macos-launcher/src/module.modulemap @@ -0,0 +1,9 @@ +module libplzma { + header "../deps/PLzmaSDK/libplzma.h" + export * +} + +module librsync { + header "rsync.h" + export * +} diff --git a/macos-launcher/src/rsync.cc b/macos-launcher/src/rsync.cc new file mode 100644 index 0000000..46e50d2 --- /dev/null +++ b/macos-launcher/src/rsync.cc @@ -0,0 +1,18 @@ +#include +#include + +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"); + + auto result = rs_patch_file(basis_file, delta_file, new_file, &stats); + fclose(basis_file); + fclose(delta_file); + fclose(new_file); + return result; +} +} diff --git a/macos-launcher/src/rsync.h b/macos-launcher/src/rsync.h new file mode 100644 index 0000000..726b179 --- /dev/null +++ b/macos-launcher/src/rsync.h @@ -0,0 +1,2 @@ + +int rsync_apply(const char* source_file, const char* patch_file, const char* target_file); diff --git a/macos-launcher/src/rsync.swift b/macos-launcher/src/rsync.swift new file mode 100644 index 0000000..7752e0d --- /dev/null +++ b/macos-launcher/src/rsync.swift @@ -0,0 +1,9 @@ +import Foundation +import librsync + +func rsyncApply(source: URL, patch: URL, output: URL) throws { + let err = rsync_apply(source.path, patch.path, output.path) + if err != 0 { + throw NSError(domain: NSPOSIXErrorDomain, code: Int(err), userInfo: nil) + } +} diff --git a/macos-launcher/xmake.lua b/macos-launcher/xmake.lua new file mode 100644 index 0000000..f31f0e3 --- /dev/null +++ b/macos-launcher/xmake.lua @@ -0,0 +1,20 @@ +add_rules("mode.debug", "mode.release") + +target("macOSLauncher") +do + add_rules("xcode.application") + add_files("src/**/*.swift") + add_files("src/*.swift", "src/*.xcassets") + add_files("src/Info.plist") + add_files("deps/PlzmaSDK/swift/*.swift") + add_files("deps/PlzmaSDK/src/*.cpp") + add_files("deps/PlzmaSDK/src/**/*.c") + add_files("deps/PlzmaSDK/src/**/*.cpp") + add_files("src/*.cc") + add_packages("7z", "lzma", "librsync") + add_ldflags("-lc++") + add_values("xcode.bundle_identifier", "com.tashcan.startrekpatch") + add_scflags("-Xcc -fmodules", "-Xcc -fmodule-map-file=macos-launcher/src/module.modulemap", "-D SWIFT_PACKAGE", + { force = true }) + add_values("xcode.bundle_display_name", "Star Trek Fleet Command Community Patch") +end diff --git a/macos-loader/src/folder_manager.h b/macos-loader/src/folder_manager.h new file mode 100644 index 0000000..96c6d25 --- /dev/null +++ b/macos-loader/src/folder_manager.h @@ -0,0 +1,55 @@ +#pragma once + +namespace fm +{ +enum { + NSApplicationDirectory = 1, + NSDemoApplicationDirectory, + NSDeveloperApplicationDirectory, + NSAdminApplicationDirectory, + NSLibraryDirectory, + NSDeveloperDirectory, + NSUserDirectory, + NSDocumentationDirectory, + NSDocumentDirectory, + NSCoreServiceDirectory, + NSAutosavedInformationDirectory = 11, + NSDesktopDirectory = 12, + NSCachesDirectory = 13, + NSApplicationSupportDirectory = 14, + NSDownloadsDirectory = 15, + NSInputMethodsDirectory = 16, + NSMoviesDirectory = 17, + NSMusicDirectory = 18, + NSPicturesDirectory = 19, + NSPrinterDescriptionDirectory = 20, + NSSharedPublicDirectory = 21, + NSPreferencePanesDirectory = 22, + NSApplicationScriptsDirectory = 23, + NSItemReplacementDirectory = 99, + NSAllApplicationsDirectory = 100, + NSAllLibrariesDirectory = 101, + NSTrashDirectory = 102 +}; +typedef unsigned long SearchPathDirectory; + +enum { + NSUserDomainMask = 1, // user's home directory --- place to install user's personal items (~) + NSLocalDomainMask = + 2, // local to the current machine --- place to install items available to everyone on this machine (/Library) + NSNetworkDomainMask = 4, // publically available location in the local area network --- place to install items + // available on the network (/Network) + NSSystemDomainMask = 8, // provided by Apple, unmodifiable (/System) + NSAllDomainsMask = 0x0ffff // all domains: all of the above and future items +}; +typedef unsigned long SearchPathDomainMask; + +class FolderManager +{ +public: + static const char *pathForDirectory(SearchPathDirectory directory, SearchPathDomainMask domainMask); + static const char *pathForDirectoryAppropriateForItemAtPath(SearchPathDirectory directory, + SearchPathDomainMask domainMask, const char *itemPath, + bool create = false); +}; +}; // namespace fm diff --git a/macos-loader/src/folder_manager.mm b/macos-loader/src/folder_manager.mm new file mode 100644 index 0000000..33ec3ca --- /dev/null +++ b/macos-loader/src/folder_manager.mm @@ -0,0 +1,31 @@ +#include "folder_manager.h" + +#import + +using namespace fm; + +const char * FolderManager::pathForDirectory(SearchPathDirectory directory, SearchPathDomainMask domainMask) { + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSArray *URLs = [fileManager URLsForDirectory:(NSSearchPathDirectory)directory inDomains:domainMask]; + if (URLs.count == 0) return NULL; + + NSURL *URL = [URLs objectAtIndex:0]; + NSString *path = URL.path; + + // `fileSystemRepresentation` on an `NSString` gives a path suitable for POSIX APIs + return path.fileSystemRepresentation; +} + +const char * FolderManager::pathForDirectoryAppropriateForItemAtPath(SearchPathDirectory directory, + SearchPathDomainMask domainMask, const char *itemPath, bool create) { + + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSString *nsPath = [fileManager stringWithFileSystemRepresentation:itemPath length:strlen(itemPath)]; + NSURL *itemURL = (nsPath ? [NSURL fileURLWithPath:nsPath] : nil); + + NSURL *URL = [fileManager URLForDirectory:(NSSearchPathDirectory)directory + inDomain:domainMask + appropriateForURL:itemURL + create:create error:NULL]; + return URL.path.fileSystemRepresentation; +} \ No newline at end of file diff --git a/macos-loader/src/main.cc b/macos-loader/src/main.cc new file mode 100644 index 0000000..eb2bab3 --- /dev/null +++ b/macos-loader/src/main.cc @@ -0,0 +1,78 @@ +#include "folder_manager.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#define LD_LIBRARY_PATH_ENV "DYLD_LIBRARY_PATH" +#define LD_PRELOAD_ENV "DYLD_INSERT_LIBRARIES" + +static char **appArgs = NULL; + +static void die(const char *op) +{ + fprintf(stderr, "cliloader Error: %s\n", op); + exit(1); +} + +int main() +{ + constexpr auto kAppPath = R"(Star Trek Fleet Command.app/Contents/MacOS/Star Trek Fleet Command)"; + + auto ApplicationSupportPath = + (char *)fm::FolderManager::pathForDirectory(fm::NSApplicationSupportDirectory, fm::NSUserDomainMask); + auto LibraryPath = (char *)fm::FolderManager::pathForDirectory(fm::NSLibraryDirectory, fm::NSUserDomainMask); + + std::filesystem::path launcher_settings_ini_path = + std::filesystem::path(LibraryPath) / "Preferences" / "Star Trek Fleet Command" / "launcher_settings.ini"; + + std::ifstream launcher_settings_ini_stream(launcher_settings_ini_path); + ini::IniFile launcher_settings_ini(launcher_settings_ini_stream); + + std::filesystem::path game_install_path = launcher_settings_ini["General"]["152033..GAME_PATH"].as(); + game_install_path /= kAppPath; + auto game_install_path_str = game_install_path.string(); + + char buf[PATH_MAX]; + uint32_t bufsize = PATH_MAX; + _NSGetExecutablePath(buf, &bufsize); + + std::string path = dirname(buf); + +#define SETENV(_name, _value) setenv(_name, _value, 1) + std::string ld_library_path = path; + const char *old_ld_library_path = getenv(LD_LIBRARY_PATH_ENV); + if (old_ld_library_path) { + ld_library_path += ":"; + ld_library_path += old_ld_library_path; + } + std::string ld_preload = path + "/libstfc-community-patch.dylib"; + const char *old_ld_preload = getenv(LD_PRELOAD_ENV); + if (old_ld_preload) { + ld_preload += ":"; + ld_preload += old_ld_preload; + } + + SETENV(LD_PRELOAD_ENV, ld_preload.c_str()); + SETENV(LD_LIBRARY_PATH_ENV, ld_library_path.c_str()); + + char *const argument_list[] = {(char *)game_install_path_str.c_str(), NULL}; + + if (execvp(argument_list[0], argument_list) == -1) { + printf("Failed to launch app %d %s\n", errno, game_install_path_str.c_str()); + return -1; + } + + return 0; +} diff --git a/macos-loader/xmake.lua b/macos-loader/xmake.lua new file mode 100644 index 0000000..c096c90 --- /dev/null +++ b/macos-loader/xmake.lua @@ -0,0 +1,9 @@ +target("stfc-community-patch-loader") +do + set_kind("binary") + add_files("src/*.cc") + add_files("src/*.mm") + add_headerfiles("src/*.h") + add_frameworks("Foundation") + add_packages("inifile-cpp") +end diff --git a/mods/src/config.cc b/mods/src/config.cc index 1e31525..f2bceee 100644 --- a/mods/src/config.cc +++ b/mods/src/config.cc @@ -1,19 +1,43 @@ #include "config.h" -#include "version.h" -#include "str_utils.h" #include "patches/mapkey.h" #include "prime/KeyCode.h" +#include "str_utils.h" +#include "version.h" +#include + +#include +#include #include -#include +#include #include #include #include #include -#include +#if !_WIN32 +#include "folder_manager.h" +#endif -#include +static auto make_config_path(auto filename, bool create_dir = false) +{ +#if !_WIN32 + auto ApplicationSupportPath = + (char*)fm::FolderManager::pathForDirectory(fm::NSApplicationSupportDirectory, fm::NSUserDomainMask); + auto LibraryPath = (char*)fm::FolderManager::pathForDirectory(fm::NSLibraryDirectory, fm::NSUserDomainMask); + + const auto config_dir = std::filesystem::path(LibraryPath) / "Preferences" / "com.tashcan.startrekpatch"; + + if (create_dir) { + std::error_code ec; + std::filesystem::create_directories(config_dir, ec); + } + std::filesystem::path config_path = config_dir / filename; + return config_path.u8string(); +#else + return filename; +#endif +} static const eastl::tuple bannerTypes[] = { {"Standard", ToastState::Standard}, @@ -47,11 +71,14 @@ Config::Config() void Config::Save(toml::table config, std::string_view filename, bool apply_warning) { std::ofstream config_file; - config_file.open(filename); + + auto config_path = make_config_path(filename, true); + + config_file.open(config_path); if (apply_warning) { char buff[44]; snprintf(buff, 44, "%-44s", CONFIG_FILE_DEFAULT); - + config_file << "#######################################################################\n"; config_file << "#######################################################################\n"; config_file << "#### ####\n"; @@ -74,6 +101,7 @@ Config& Config::Get() return config; } +#if _WIN32 static HMONITOR lastMonitor = (HMONITOR)-1; static float dpi = 1.0f; @@ -118,6 +146,17 @@ float Config::GetDPI() return dpi; } +#else +float Config::RefreshDPI() +{ + return Config::GetDPI(); +} + +float Config::GetDPI() +{ + return 1.0f; +} +#endif void Config::AdjustUiScale(bool scaleUp) { @@ -230,7 +269,7 @@ void Config::Load() toml::table parsed; bool write_config = false; try { - config = std::move(toml::parse_file("community_patch_settings.toml")); + config = std::move(toml::parse_file(make_config_path(CONFIG_FILE_DEFAULT))); write_config = true; } catch (const toml::parse_error& e) { spdlog::warn("Failed to load config file, falling back to default settings: {}", e.description()); @@ -277,18 +316,18 @@ void Config::Load() this->use_out_of_dock_power = get_config_or_default(config, parsed, "buffs", "use_out_of_dock_power", false); - this->disable_escape_exit = get_config_or_default(config, parsed, "ui", "disable_escape_exit", false); - this->disable_preview_locate = get_config_or_default(config, parsed, "ui", "disable_preview_locate", false); - this->disable_preview_recall = get_config_or_default(config, parsed, "ui", "disable_preview_recall", false); - this->disable_move_keys = get_config_or_default(config, parsed, "ui", "disable_move_keys", false); - this->disable_toast_banners = get_config_or_default(config, parsed, "ui", "disable_toast_banners", true); - this->extend_donation_slider = get_config_or_default(config, parsed, "ui", "extend_donation_slider", false); - this->disable_galaxy_chat = get_config_or_default(config, parsed, "ui", "disable_galaxy_chat", false); - this->show_cargo_default = get_config_or_default(config, parsed, "ui", "show_cargo_default", false); - this->show_player_cargo = get_config_or_default(config, parsed, "ui", "show_player_cargo", false); - this->show_station_cargo = get_config_or_default(config, parsed, "ui", "show_station_cargo", true); - this->show_hostile_cargo = get_config_or_default(config, parsed, "ui", "show_hostile_cargo", true); - this->show_armada_cargo = get_config_or_default(config, parsed, "ui", "show_armada_cargo", true); + this->disable_escape_exit = get_config_or_default(config, parsed, "ui", "disable_escape_exit", false); + this->disable_preview_locate = get_config_or_default(config, parsed, "ui", "disable_preview_locate", false); + this->disable_preview_recall = get_config_or_default(config, parsed, "ui", "disable_preview_recall", false); + this->disable_move_keys = get_config_or_default(config, parsed, "ui", "disable_move_keys", false); + this->disable_toast_banners = get_config_or_default(config, parsed, "ui", "disable_toast_banners", true); + this->extend_donation_slider = get_config_or_default(config, parsed, "ui", "extend_donation_slider", false); + this->disable_galaxy_chat = get_config_or_default(config, parsed, "ui", "disable_galaxy_chat", false); + this->show_cargo_default = get_config_or_default(config, parsed, "ui", "show_cargo_default", false); + this->show_player_cargo = get_config_or_default(config, parsed, "ui", "show_player_cargo", false); + this->show_station_cargo = get_config_or_default(config, parsed, "ui", "show_station_cargo", true); + this->show_hostile_cargo = get_config_or_default(config, parsed, "ui", "show_hostile_cargo", true); + this->show_armada_cargo = get_config_or_default(config, parsed, "ui", "show_armada_cargo", true); this->always_skip_reveal_sequence = get_config_or_default(config, parsed, "ui", "always_skip_reveal_sequence", false); this->stay_in_bundle_after_summary = @@ -314,7 +353,8 @@ void Config::Load() get_config_or_default(config, parsed, "ui", "disabled_banner_types", ""); this->config_settings_url = get_config_or_default(config, parsed, "config", "settings_url", ""); - this->config_assets_url_override = get_config_or_default(config, parsed, "config", "assets_url_override", ""); + this->config_assets_url_override = + get_config_or_default(config, parsed, "config", "assets_url_override", ""); std::vector types = StrSplit(disabled_banner_types_str, ','); @@ -442,7 +482,7 @@ void Config::Load() parse_config_shortcut(config, parsed, "toggle_cargo_armada", GameFunction::ToggleCargoArmada, "ALT-5"); } - if (!std::filesystem::exists(CONFIG_FILE_DEFAULT)) { + if (!std::filesystem::exists(make_config_path(CONFIG_FILE_RUNTIME))) { message.str(""); message << "Creating " << CONFIG_FILE_DEFAULT << " (default config file)"; spdlog::warn(message.str()); diff --git a/mods/src/config.h b/mods/src/config.h index 6a33ebe..ecc9f99 100644 --- a/mods/src/config.h +++ b/mods/src/config.h @@ -2,7 +2,6 @@ #include -#include "prime/Toast.h" #include #define CONFIG_FILE_DEFAULT "community_patch_settings.toml" diff --git a/mods/src/folder_manager.h b/mods/src/folder_manager.h new file mode 100644 index 0000000..96c6d25 --- /dev/null +++ b/mods/src/folder_manager.h @@ -0,0 +1,55 @@ +#pragma once + +namespace fm +{ +enum { + NSApplicationDirectory = 1, + NSDemoApplicationDirectory, + NSDeveloperApplicationDirectory, + NSAdminApplicationDirectory, + NSLibraryDirectory, + NSDeveloperDirectory, + NSUserDirectory, + NSDocumentationDirectory, + NSDocumentDirectory, + NSCoreServiceDirectory, + NSAutosavedInformationDirectory = 11, + NSDesktopDirectory = 12, + NSCachesDirectory = 13, + NSApplicationSupportDirectory = 14, + NSDownloadsDirectory = 15, + NSInputMethodsDirectory = 16, + NSMoviesDirectory = 17, + NSMusicDirectory = 18, + NSPicturesDirectory = 19, + NSPrinterDescriptionDirectory = 20, + NSSharedPublicDirectory = 21, + NSPreferencePanesDirectory = 22, + NSApplicationScriptsDirectory = 23, + NSItemReplacementDirectory = 99, + NSAllApplicationsDirectory = 100, + NSAllLibrariesDirectory = 101, + NSTrashDirectory = 102 +}; +typedef unsigned long SearchPathDirectory; + +enum { + NSUserDomainMask = 1, // user's home directory --- place to install user's personal items (~) + NSLocalDomainMask = + 2, // local to the current machine --- place to install items available to everyone on this machine (/Library) + NSNetworkDomainMask = 4, // publically available location in the local area network --- place to install items + // available on the network (/Network) + NSSystemDomainMask = 8, // provided by Apple, unmodifiable (/System) + NSAllDomainsMask = 0x0ffff // all domains: all of the above and future items +}; +typedef unsigned long SearchPathDomainMask; + +class FolderManager +{ +public: + static const char *pathForDirectory(SearchPathDirectory directory, SearchPathDomainMask domainMask); + static const char *pathForDirectoryAppropriateForItemAtPath(SearchPathDirectory directory, + SearchPathDomainMask domainMask, const char *itemPath, + bool create = false); +}; +}; // namespace fm diff --git a/mods/src/folder_manager.mm b/mods/src/folder_manager.mm new file mode 100644 index 0000000..33ec3ca --- /dev/null +++ b/mods/src/folder_manager.mm @@ -0,0 +1,31 @@ +#include "folder_manager.h" + +#import + +using namespace fm; + +const char * FolderManager::pathForDirectory(SearchPathDirectory directory, SearchPathDomainMask domainMask) { + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSArray *URLs = [fileManager URLsForDirectory:(NSSearchPathDirectory)directory inDomains:domainMask]; + if (URLs.count == 0) return NULL; + + NSURL *URL = [URLs objectAtIndex:0]; + NSString *path = URL.path; + + // `fileSystemRepresentation` on an `NSString` gives a path suitable for POSIX APIs + return path.fileSystemRepresentation; +} + +const char * FolderManager::pathForDirectoryAppropriateForItemAtPath(SearchPathDirectory directory, + SearchPathDomainMask domainMask, const char *itemPath, bool create) { + + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSString *nsPath = [fileManager stringWithFileSystemRepresentation:itemPath length:strlen(itemPath)]; + NSURL *itemURL = (nsPath ? [NSURL fileURLWithPath:nsPath] : nil); + + NSURL *URL = [fileManager URLForDirectory:(NSSearchPathDirectory)directory + inDomain:domainMask + appropriateForURL:itemURL + create:create error:NULL]; + return URL.path.fileSystemRepresentation; +} \ No newline at end of file diff --git a/mods/src/il2cpp/il2cpp-functions.cc b/mods/src/il2cpp/il2cpp-functions.cc new file mode 100644 index 0000000..0145bce --- /dev/null +++ b/mods/src/il2cpp/il2cpp-functions.cc @@ -0,0 +1,40 @@ +#include "il2cpp-functions.h" + +#include + +#if !_WIN32 +#include +#include +#include + +#define PATH_MAX 1024 +#if defined(__cplusplus) +extern "C" { +#endif // __cplusplus +#define DO_API(r, n, p) n##_t n; +#define DO_API_NO_RETURN(r, n, p) DO_API(r, n, p) +#include "il2cpp-api-functions.h" +#undef DO_API +#undef DO_API_NORETURN +#if defined(__cplusplus) +} +#endif // __cplusplus +#endif + +void init_il2cpp_pointers() +{ +#if !_WIN32 + char buf[PATH_MAX]; + uint32_t bufsize = PATH_MAX; + _NSGetExecutablePath(buf, &bufsize); + + char assembly_path[PATH_MAX]; + snprintf(assembly_path, sizeof(assembly_path), "%s/%s", dirname(buf), "../Frameworks/GameAssembly.dylib"); + auto assembly = dlopen(assembly_path, RTLD_LAZY | RTLD_GLOBAL); +#define DO_API(r, n, p) n = (n##_t)dlsym(assembly, #n); +#define DO_API_NO_RETURN(r, n, p) DO_API(r, n, p) +#include "il2cpp-api-functions.h" +#undef DO_API +#undef DO_API_NORETURN +#endif +} diff --git a/mods/src/il2cpp/il2cpp-functions.h b/mods/src/il2cpp/il2cpp-functions.h index 45ab459..8fc3dd9 100644 --- a/mods/src/il2cpp/il2cpp-functions.h +++ b/mods/src/il2cpp/il2cpp-functions.h @@ -10,11 +10,17 @@ #include #if defined(__cplusplus) -extern "C" -{ +extern "C" { #endif // __cplusplus -#define DO_API(r, n, p) IL2CPP_IMPORT r n p; -#define DO_API_NO_RETURN(r, n, p) IL2CPP_IMPORT NORETURN r n p; +#if _WIN32 +#define DO_API(r, n, p) IL2CPP_IMPORT r n p; +#define DO_API_NO_RETURN(r, n, p) IL2CPP_IMPORT NORETURN r n p; +#else +#define DO_API(r, n, p) \ + using n##_t = r(*) p; \ + extern n##_t n; +#define DO_API_NO_RETURN(r, n, p) DO_API(r, n, p) +#endif #include "il2cpp-api-functions.h" #undef DO_API #undef DO_API_NORETURN @@ -22,3 +28,4 @@ extern "C" } #endif // __cplusplus +void init_il2cpp_pointers(); diff --git a/mods/src/il2cpp/il2cpp_helper.h b/mods/src/il2cpp/il2cpp_helper.h index cad21b6..747b3b6 100644 --- a/mods/src/il2cpp/il2cpp_helper.h +++ b/mods/src/il2cpp/il2cpp_helper.h @@ -1,8 +1,5 @@ #pragma once -#include - -#include "utils.h" #include "il2cpp-functions.h" #include @@ -15,6 +12,11 @@ #include #include +#if !_WIN32 +#include +#include +#endif + class IL2CppPropertyHelper { public: @@ -105,7 +107,7 @@ class IL2CppStaticFieldHelper template inline T Get() const { - T v; + T v; il2cpp_field_static_get_value(this->fieldInfo, &v); return v; } @@ -339,4 +341,9 @@ template class ObjectFinder auto& objects = tracked_objects[T::get_class_helper().get_cls()]; return {reinterpret_cast(objects.data()), reinterpret_cast(objects.data()) + objects.size()}; } -}; \ No newline at end of file +}; + +template T* il2cpp_resolve_icall_typed(const char* name) +{ + return (T*)il2cpp_resolve_icall(name); +} diff --git a/mods/src/patches/gamefunctions.h b/mods/src/patches/gamefunctions.h index 7c9e9e4..e17095d 100644 --- a/mods/src/patches/gamefunctions.h +++ b/mods/src/patches/gamefunctions.h @@ -1,7 +1,5 @@ #pragma once -#include - enum GameFunction { MoveLeft, MoveRight, diff --git a/mods/src/patches/key.cc b/mods/src/patches/key.cc index ad00dd7..8954bbf 100644 --- a/mods/src/patches/key.cc +++ b/mods/src/patches/key.cc @@ -1,17 +1,11 @@ #include "key.h" -#include "config.h" #include "prime/EventSystem.h" -#include "utils.h" #include "str_utils.h" #include -#include - -#include -#include -#include #include #include +#include int Key::cacheInputFocused = 0; int Key::cacheInputModified = 0; @@ -226,7 +220,7 @@ void Key::ResetCache() bool Key::Down(KeyCode key) { static auto GetKeyDownInt = - il2cpp_resolve_icall("UnityEngine.Input::GetKeyDownInt(UnityEngine.KeyCode)"); + il2cpp_resolve_icall_typed("UnityEngine.Input::GetKeyDownInt(UnityEngine.KeyCode)"); if (cacheKeyDown[(int)key] == 0) { cacheKeyDown[(int)key] = GetKeyDownInt(key) ? 1 : -1; @@ -237,7 +231,8 @@ bool Key::Down(KeyCode key) bool Key::Pressed(KeyCode key) { - static auto GetKeyInt = il2cpp_resolve_icall("UnityEngine.Input::GetKeyInt(UnityEngine.KeyCode)"); + static auto GetKeyInt = + il2cpp_resolve_icall_typed("UnityEngine.Input::GetKeyInt(UnityEngine.KeyCode)"); if (cacheKeyPressed[(int)key] == 0) { cacheKeyPressed[(int)key] = GetKeyInt(key) ? 1 : -1; diff --git a/mods/src/patches/key.h b/mods/src/patches/key.h index fc7a7cc..01374e0 100644 --- a/mods/src/patches/key.h +++ b/mods/src/patches/key.h @@ -1,9 +1,10 @@ #pragma once -#include #include -#include + #include +#include +#include class Key { diff --git a/mods/src/patches/mapkey.cc b/mods/src/patches/mapkey.cc index 60142af..51e89b8 100644 --- a/mods/src/patches/mapkey.cc +++ b/mods/src/patches/mapkey.cc @@ -1,23 +1,18 @@ -#include "config.h" -#include "prime/EventSystem.h" -#include "utils.h" -#include "str_utils.h" -#include "key.h" #include "mapkey.h" +#include "gamefunctions.h" #include "modifierkey.h" -#include "prime/TMP_InputField.h" +#include "str_utils.h" +#include -#include +#include #include -#include -#include -#include #include #include +#include MapKey::MapKey() { - this->Key = KeyCode::None; + this->Key = KeyCode::None; this->hasModifiers = false; } diff --git a/mods/src/patches/mapkey.h b/mods/src/patches/mapkey.h index d88ff91..fa3e032 100644 --- a/mods/src/patches/mapkey.h +++ b/mods/src/patches/mapkey.h @@ -1,10 +1,10 @@ #pragma once -#include -#include "key.h" #include "modifierkey.h" #include #include + +#include #include #include @@ -13,12 +13,6 @@ class MapKey public: MapKey(); -private: - static std::array, (int)GameFunction::Max> mappedKeys; - - bool hasModifiers; - -public: static MapKey Parse(std::string_view key); static void AddMappedKey(GameFunction gameFunction, MapKey mappedKey); static bool IsPressed(GameFunction gameFunction); @@ -33,4 +27,9 @@ class MapKey std::vector Shortcuts; KeyCode Key; + +private: + static std::array, (int)GameFunction::Max> mappedKeys; + + bool hasModifiers; }; diff --git a/mods/src/patches/modifierkey.cc b/mods/src/patches/modifierkey.cc index 2c1a7e7..5942abe 100644 --- a/mods/src/patches/modifierkey.cc +++ b/mods/src/patches/modifierkey.cc @@ -1,7 +1,6 @@ #pragma once #include "config.h" -#include "utils.h" #include "str_utils.h" #include "key.h" #include "modifierkey.h" diff --git a/mods/src/patches/modifierkey.h b/mods/src/patches/modifierkey.h index 8d4ee66..d889188 100644 --- a/mods/src/patches/modifierkey.h +++ b/mods/src/patches/modifierkey.h @@ -7,6 +7,7 @@ #include "key.h" #include +#include struct ModifierKey { diff --git a/mods/src/patches/parts/buff_fixes.cc b/mods/src/patches/parts/buff_fixes.cc index e213910..d131833 100644 --- a/mods/src/patches/parts/buff_fixes.cc +++ b/mods/src/patches/parts/buff_fixes.cc @@ -1,12 +1,12 @@ #include "config.h" #include "prime_types.h" -#include - #include #include #include +#include + static bool IsImportantFactionBuffThatNeedsFix(auto original, ClientModifierType modifierType) { return @@ -119,16 +119,17 @@ void InstallBuffFixHooks() screen_manager_helper = il2cpp_get_class_helper("Digit.Client.PrimeLib.Runtime", "Digit.PrimeServer.Services", "BuffService"); - ptr = screen_manager_helper.GetMethodSpecial("ApplyBuffModifiersToCostVal", [](auto count, const Il2CppType** params) { - if (count != 2) { - return false; - } - auto p2 = params[1]->type; - if (p2 == IL2CPP_TYPE_VALUETYPE) { - return true; - } - return false; - }); + ptr = + screen_manager_helper.GetMethodSpecial("ApplyBuffModifiersToCostVal", [](auto count, const Il2CppType **params) { + if (count != 2) { + return false; + } + auto p2 = params[1]->type; + if (p2 == IL2CPP_TYPE_VALUETYPE) { + return true; + } + return false; + }); if (!ptr) { return; } diff --git a/mods/src/patches/parts/chat.cc b/mods/src/patches/parts/chat.cc index d540fa8..d30631b 100644 --- a/mods/src/patches/parts/chat.cc +++ b/mods/src/patches/parts/chat.cc @@ -2,10 +2,10 @@ #include "prime/FullScreenChatViewController.h" #include "prime/GenericButtonContext.h" -#include - #include "config.h" +#include + bool FullScreenChatViewController_AboutToShow(auto original, FullScreenChatViewController* _this3) { original(_this3); diff --git a/mods/src/patches/parts/disable_banners.cc b/mods/src/patches/parts/disable_banners.cc index a160be1..794ffe5 100644 --- a/mods/src/patches/parts/disable_banners.cc +++ b/mods/src/patches/parts/disable_banners.cc @@ -1,10 +1,9 @@ #include "config.h" -#include - #include +#include -#include "prime/Toast.h" +#include struct ToastObserver { }; diff --git a/mods/src/patches/parts/fix_pan.cc b/mods/src/patches/parts/fix_pan.cc index 714877a..128a9f7 100644 --- a/mods/src/patches/parts/fix_pan.cc +++ b/mods/src/patches/parts/fix_pan.cc @@ -1,15 +1,12 @@ #include "config.h" -#include - #include -#include "patches/MapKey.h" -#include "utils.h" - #include #include +#include + TKTouch *TKTouch_populateWithPosition_Hook(auto original, TKTouch *_this, uintptr_t pos, int phase) { auto r = original(_this, pos, phase); @@ -27,9 +24,8 @@ bool NavigationPan_LateUpdate_Hook(auto original, NavigationPan *_this) original(_this); } - static auto GetMouseButton = - il2cpp_resolve_icall("UnityEngine.Input::GetMouseButton(System.Int32)"); - static auto GetTouchCount = il2cpp_resolve_icall("UnityEngine.Input::get_touchCount()"); + static auto GetMouseButton = il2cpp_resolve_icall_typed("UnityEngine.Input::GetMouseButton(System.Int32)"); + static auto GetTouchCount = il2cpp_resolve_icall_typed("UnityEngine.Input::get_touchCount()"); if (_this->BlockPan() || _this->_trackingPOI) { d->x = 0.0f; diff --git a/mods/src/patches/parts/fix_unity_web_request.cc b/mods/src/patches/parts/fix_unity_web_request.cc deleted file mode 100644 index 7cd2685..0000000 --- a/mods/src/patches/parts/fix_unity_web_request.cc +++ /dev/null @@ -1,112 +0,0 @@ -#include "config.h" - -#include - -#include - -#include "utils.h" - -#include "prime/HttpJob.h" -#include "prime/HttpRequest.h" -#include "prime/HttpResponse.h" - -#include - -#include "utils.h" - -static void ProcessSending_Hook(auto original, void *_this, HttpJob *item) -{ - - static auto get_downloadedBytes = il2cpp_resolve_icall( - "UnityEngine.Networking.UnityWebRequest::get_downloadedBytes()"); - static auto get_uploadedBytes = il2cpp_resolve_icall( - "UnityEngine.Networking.UnityWebRequest::get_uploadedBytes()"); - static auto get_uploadHandler = il2cpp_resolve_icall( - "UnityEngine.Networking.UnityWebRequest::get_uploadHandler()"); - static auto get_uploadProgress = il2cpp_resolve_icall( - "UnityEngine.Networking.UnityWebRequest::GetUploadProgress"); - static auto get_IsExecuting = - il2cpp_resolve_icall("UnityEngine.Networking.UnityWebRequest::IsExecuting()"); - static auto get_isNetworkError = il2cpp_resolve_icall( - "UnityEngine.Networking.UnityWebRequest::get_isNetworkError()"); - static auto get_ResponseCode = il2cpp_resolve_icall( - "UnityEngine.Networking.UnityWebRequest::get_responseCode()"); - - if (!get_IsExecuting || !get_ResponseCode || !get_uploadProgress) { - return original(_this, item); - } - - auto unityRequest = item->UnityRequest; - - if (!get_IsExecuting(unityRequest) && get_ResponseCode(unityRequest) == 200) { - item->State = HttpJobState::ResponseReceived; - return; - } - - auto uploadProgress = get_uploadProgress(unityRequest); - if (!unityRequest->get_uploadHandler() || uploadProgress == 1.0f) { - item->State = HttpJobState::ResponseReceiving; - return; - } - - original(_this, item); -} - -static void SendWebRequest_Hook(auto original, UnityWebRequest *_this) -{ - static auto get_use100Continue = il2cpp_resolve_icall( - "UnityEngine.Networking.UnityWebRequest::get_use100Continue"); - static auto set_use100Continue = il2cpp_resolve_icall( - "UnityEngine.Networking.UnityWebRequest::set_use100Continue"); - set_use100Continue(_this, false); - original(_this); -} - -static void *unity_web_request_multi_handle = nullptr; -int64_t curl_multi_cleanup(void *multi_handle); -static decltype(curl_multi_cleanup) *ocurl_multi_cleanup = nullptr; -static int64_t curl_multi_cleanup(void *multi_handle) -{ - if (multi_handle == unity_web_request_multi_handle) { - return 0; - } - return ocurl_multi_cleanup(multi_handle); -} - -void *curl_multi_init(); -static decltype(curl_multi_init) *ocurl_multi_init = nullptr; -static int re_create_count = 0; -static void *curl_multi_init() -{ - // The only requests to this are for the internal UnityWebRequest impl things - // so remember the handle as the `unity_web_request_multi_handle` - if (!unity_web_request_multi_handle) { - unity_web_request_multi_handle = ocurl_multi_init(); - } else { - spdlog::debug("Connection re-create {}, preventing keep-alive drop", re_create_count++); - } - return unity_web_request_multi_handle; -} - -void InstallWebRequestHooks() -{ - return; - - /* - if (!Config::Get().fix_unity_web_requests) { - return; - } - auto screen_manager_helper = - il2cpp_get_class_helper("Digit.Engine.HTTPClient.Runtime", "Digit.Networking.Network", "DigitHttpClient"); - - auto meow = screen_manager_helper.GetMethod("ProcessSending"); - if (!meow) { - return; - } - SPUD_STATIC_DETOUR(meow, ProcessSending_Hook); - - auto send_web_request_ptr = il2cpp_resolve_icall( - "UnityEngine.Networking.UnityWebRequest::BeginWebRequest()"); - SPUD_STATIC_DETOUR(send_web_request_ptr, SendWebRequest_Hook); - */ -} \ No newline at end of file diff --git a/mods/src/patches/parts/free_resize.cc b/mods/src/patches/parts/free_resize.cc index 57ca447..9cd2889 100644 --- a/mods/src/patches/parts/free_resize.cc +++ b/mods/src/patches/parts/free_resize.cc @@ -1,7 +1,7 @@ +#if _WIN32 #include #include "config.h" -#include "utils.h" #include @@ -104,16 +104,16 @@ struct ResolutionArray { void AspectRatioConstraintHandler_Update(auto original, void* _this) { - static auto get_fullscreen = il2cpp_resolve_icall("UnityEngine.Screen::get_fullScreen()"); - static auto get_height = il2cpp_resolve_icall("UnityEngine.Screen::get_height()"); - static auto get_width = il2cpp_resolve_icall("UnityEngine.Screen::get_width()"); - static auto get_resolutions = il2cpp_resolve_icall("UnityEngine.Screen::get_resolutions()"); - static auto SetResolution = il2cpp_resolve_icall( + static auto get_fullscreen = il2cpp_resolve_icall_typed("UnityEngine.Screen::get_fullScreen()"); + static auto get_height = il2cpp_resolve_icall_typed("UnityEngine.Screen::get_height()"); + static auto get_width = il2cpp_resolve_icall_typed("UnityEngine.Screen::get_width()"); + static auto get_resolutions = il2cpp_resolve_icall_typed("UnityEngine.Screen::get_resolutions()"); + static auto SetResolution = il2cpp_resolve_icall_typed( "UnityEngine.Screen::SetResolution(System.Int32,System.Int32,UnityEngine.FullScreenMode,System.Int32)"); if (unityWindow) { if (get_fullscreen()) { - static auto get_currentResolution_Injected = il2cpp_resolve_icall( + static auto get_currentResolution_Injected = il2cpp_resolve_icall_typed( "UnityEngine.Screen::get_currentResolution_Injected(UnityEngine.Resolution&)"); auto height = get_height(); auto width = get_width(); @@ -147,8 +147,8 @@ void InstallFreeResizeHooks() { auto AspectRatioConstraintHandler_helper = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Client.Utils", "AspectRatioConstraintHandler"); - auto ptr_update = AspectRatioConstraintHandler_helper.GetMethod("Update"); - auto ptr_wndproc = AspectRatioConstraintHandler_helper.GetMethod("WndProc"); + auto ptr_update = AspectRatioConstraintHandler_helper.GetMethod("Update"); + auto ptr_wndproc = AspectRatioConstraintHandler_helper.GetMethod("WndProc"); if (!ptr_update || !ptr_wndproc) { return; @@ -157,3 +157,4 @@ void InstallFreeResizeHooks() SPUD_STATIC_DETOUR(ptr_update, AspectRatioConstraintHandler_Update); SPUD_STATIC_DETOUR(ptr_wndproc, AspectRatioConstraintHandler_WndProc); } +#endif diff --git a/mods/src/patches/parts/hotkeys.cc b/mods/src/patches/parts/hotkeys.cc index 7f15cef..7d8fb9d 100644 --- a/mods/src/patches/parts/hotkeys.cc +++ b/mods/src/patches/parts/hotkeys.cc @@ -35,7 +35,6 @@ #include "patches/key.h" #include "patches/mapkey.h" -#include "utils.h" #include #include @@ -43,8 +42,6 @@ #include #include -extern HWND unityWindow; - static bool reset_focus_next_frame = false; static int show_info_pending = 0; @@ -81,7 +78,7 @@ void ScreenManager_Update_Hook(auto original, ScreenManager* _this) return; } - static auto GetDeltaTime = il2cpp_resolve_icall("UnityEngine.Time::get_deltaTime()"); + static auto GetDeltaTime = il2cpp_resolve_icall_typed("UnityEngine.Time::get_deltaTime()"); const auto is_in_chat = Hub::IsInChat(); const auto config = &Config::Get(); @@ -355,6 +352,8 @@ void ScreenManager_Update_Hook(auto original, ScreenManager* _this) return; } + // config->Load(); + return original(_this); } diff --git a/mods/src/patches/parts/improve_responsiveness.cc b/mods/src/patches/parts/improve_responsiveness.cc index 4ce518b..b10c4b4 100644 --- a/mods/src/patches/parts/improve_responsiveness.cc +++ b/mods/src/patches/parts/improve_responsiveness.cc @@ -1,19 +1,10 @@ #include "config.h" -#include "prime_types.h" - -#include - #include "prime/TransitionManager.h" -#include "utils.h" - #include -#include -#include -#include - -#include "spdlog/spdlog.h" +#include +#include int64_t TransitionManager_Awake(auto original, TransitionManager* a1) { diff --git a/mods/src/patches/parts/misc.cc b/mods/src/patches/parts/misc.cc index 92eac75..3e4a537 100644 --- a/mods/src/patches/parts/misc.cc +++ b/mods/src/patches/parts/misc.cc @@ -1,24 +1,21 @@ #include "config.h" -#include "prime_types.h" -#include +#include +#include +#include +#include +#include +#include -#include "utils.h" +#include -#include "prime/BundleDataWidget.h" -#include "prime/ClientModifierType.h" -#include "prime/Hub.h" -#include "prime/IList.h" -#include "prime/InventoryForPopup.h" -#include "prime/ShopSummaryDirector.h" +#include -#include +#if _WIN32 +#include +#endif #include -#include -#include - -#include "spdlog/spdlog.h" int64_t InventoryForPopup_set_MaxItemsToUse(auto original, InventoryForPopup* a1, int64_t a2) { @@ -76,6 +73,7 @@ ResolutionArray* GetResolutions_Hook(auto original) { auto resolutions = original(); +#if _WIN32 // Modify auto screenWidth = GetSystemMetrics(SM_CXSCREEN); auto screenHeight = GetSystemMetrics(SM_CYSCREEN); @@ -107,13 +105,14 @@ ResolutionArray* GetResolutions_Hook(auto original) ++i; } resolutions->maxlength = res.size(); +#endif return resolutions; } void InstallResolutionListFix() { - SPUD_STATIC_DETOUR(il2cpp_resolve_icall("UnityEngine.Screen::get_resolutions()"), + SPUD_STATIC_DETOUR(il2cpp_resolve_icall_typed("UnityEngine.Screen::get_resolutions()"), GetResolutions_Hook); } diff --git a/mods/src/patches/parts/sync.cc b/mods/src/patches/parts/sync.cc index d651954..f81d871 100644 --- a/mods/src/patches/parts/sync.cc +++ b/mods/src/patches/parts/sync.cc @@ -1,4 +1,5 @@ -#include +#include "config.h" +#include "il2cpp-api-types.h" #include @@ -10,13 +11,19 @@ #include -#include "config.h" - #include #include +#include + +#include +#include +#if _WIN32 #include #include +#endif + +#include #include #include @@ -25,45 +32,31 @@ #include #include -#include -#include +static std::string to_string(const std::wstring& str); +static std::wstring to_wstring(const std::string& str); namespace http { -using namespace winrt; -using namespace Windows::Foundation; -static auto get_client(std::wstring sessionid = L"") -{ - Windows::Web::Http::HttpClient httpClient; - auto headers{httpClient.DefaultRequestHeaders()}; - std::wstring header = L"stfc community patch"; - if (!headers.UserAgent().TryParseAdd(header)) { - throw L"Invalid header value: " + header; - } - auto token = to_hstring(Config::Get().sync_token); - if (!token.empty() && sessionid.empty()) { - headers.Append(L"stfc-sync-token", token); - } - if (!sessionid.empty()) { - headers.Append(L"X-AUTH-SESSION-ID", sessionid); - headers.Append(L"X-PRIME-VERSION", L"meh"); - headers.Append(L"X-Api-Key", L"meh"); - headers.Append(L"X-PRIME-SYNC", L"0"); +struct CURLClient { + CURLClient(CURL* handle) + : handle_(handle) + { } - return httpClient; -} -static void write_data(std::string file_data) -{ - if (!Config::Get().sync_file.empty()) { - std::ofstream sync_file; - sync_file.open(Config::Get().sync_file, std::ios_base::app); + operator CURL*() const + { + return this->handle_; + } - sync_file << file_data << "\n\n"; - sync_file.close(); + ~CURLClient() + { + curl_easy_cleanup(handle_); } -} + +private: + CURL* handle_; +}; static void send_data(std::wstring post_data) { @@ -71,61 +64,174 @@ static void send_data(std::wstring post_data) return; } - using namespace Windows::Storage::Streams; - try { - Windows::Web::Http::HttpResponseMessage httpResponseMessage; - std::wstring httpResponseBody; + std::wstring httpResponseBody; + CURL* httpClient = curl_easy_init(); + + curl_easy_setopt(httpClient, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS); + curl_easy_setopt(httpClient, CURLOPT_USERAGENT, "stfc community patch"); + + struct curl_slist* list = NULL; - auto httpClient = get_client(); - Uri requestUri{winrt::to_hstring(Config::Get().sync_url)}; + list = curl_slist_append(list, "Content-Type: application/json"); - Windows::Web::Http::HttpStringContent jsonContent(post_data, UnicodeEncoding::Utf8, L"application/json"); - httpResponseMessage = httpClient.PostAsync(requestUri, jsonContent).get(); - httpResponseMessage.EnsureSuccessStatusCode(); - } catch (winrt::hresult_error const& ex) { - spdlog::error("Failed to send sync data: {}", winrt::to_string(ex.message()).c_str()); + auto token = Config::Get().sync_token; + auto sync_token_header = "stfc-sync-token: " + token; + if (!token.empty()) { + list = curl_slist_append(list, sync_token_header.c_str()); + } + + if (list) { + curl_easy_setopt(httpClient, CURLOPT_HTTPHEADER, list); + } + + auto url = Config::Get().sync_url; + if (auto r = curl_easy_setopt(httpClient, CURLOPT_URL, url.c_str()); r != CURLE_OK) { + auto text = "Curl failed with: " + std::to_string((int)r); + text = text; + throw std::runtime_error("Failed to send data"); + } + + auto post_data_str = to_string(post_data); + if (curl_easy_setopt(httpClient, CURLOPT_POSTFIELDS, post_data_str.c_str()) != CURLE_OK) { + throw std::runtime_error("Failed to send data"); + } + + auto res = curl_easy_perform(httpClient); + if (res != CURLE_OK) { + throw std::runtime_error("Failed to send data"); } } +static size_t curl_write_to_string(void* contents, size_t size, size_t nmemb, std::string* s) +{ + size_t newLength = size * nmemb; + s->append((char*)contents, newLength); + return newLength; +} + static std::wstring get_data_data(std::wstring session, std::wstring url, std::wstring path, std::wstring post_data) { if (Config::Get().sync_url.empty()) { return {}; } - using namespace Windows::Storage::Streams; - try { - Windows::Web::Http::HttpResponseMessage httpResponseMessage; - std::wstring httpResponseBody; - - auto httpClient = get_client(session); - if (url.ends_with(L"/")) { - url = url.substr(0, url.length() - 1); - url += path; - } else { - url += path; - } - Uri requestUri{url}; - - Windows::Web::Http::HttpStringContent jsonContent(post_data, UnicodeEncoding::Utf8, L"application/json"); - jsonContent.Headers().ContentType().CharSet(L""); - httpResponseMessage = httpClient.PostAsync(requestUri, jsonContent).get(); - httpResponseMessage.EnsureSuccessStatusCode(); - return httpResponseMessage.Content().ReadAsStringAsync().get().c_str(); - } catch (winrt::hresult_error const& ex) { - spdlog::error("Failed to send sync data: {}", winrt::to_string(ex.message()).c_str()); + CURL* httpClient = curl_easy_init(); + + curl_easy_setopt(httpClient, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS); + curl_easy_setopt(httpClient, CURLOPT_USERAGENT, "stfc community patch"); + + struct curl_slist* list = NULL; + + list = curl_slist_append(list, "Content-Type: application/json"); + + auto token = Config::Get().sync_token; + auto sync_token_header = "stfc-sync-token: " + token; + if (!token.empty() && session.empty()) { + list = curl_slist_append(list, sync_token_header.c_str()); + } + + auto session_id_header = "X-AUTH-SESSION-ID: " + to_string(session); + if (!session.empty()) { + list = curl_slist_append(list, session_id_header.c_str()); + list = curl_slist_append(list, "X-PRIME-VERSION: meh"); + list = curl_slist_append(list, "X-Api-Key: meh"); + list = curl_slist_append(list, "X-PRIME-SYNC: 0"); + } + + if (list) { + curl_easy_setopt(httpClient, CURLOPT_HTTPHEADER, list); + } + + if (url.ends_with(L"/")) { + url = url.substr(0, url.length() - 1); + url += path; + } else { + url += path; + } + + curl_easy_setopt(httpClient, CURLOPT_URL, to_string(url).c_str()); + curl_easy_setopt(httpClient, CURLOPT_POSTFIELDS, to_string(post_data).c_str()); + + std::string s; + + curl_easy_setopt(httpClient, CURLOPT_WRITEFUNCTION, curl_write_to_string); + curl_easy_setopt(httpClient, CURLOPT_WRITEDATA, &s); + + auto res = curl_easy_perform(httpClient); + if (res != CURLE_OK) { + throw std::runtime_error("Failed to send data"); + } + + long http_code = 0; + curl_easy_getinfo(httpClient, CURLINFO_RESPONSE_CODE, &http_code); + if (http_code != 200) { + throw std::runtime_error("Failed to send data"); } + return to_wstring(s); + return {}; } static void send_data(std::string post_data) { - return send_data(std::wstring(winrt::to_hstring(post_data))); + return send_data(to_wstring(post_data)); +} + +static void write_data(std::string file_data) +{ + if (!Config::Get().sync_file.empty()) { + std::ofstream sync_file; + sync_file.open(Config::Get().sync_file, std::ios_base::app); + + sync_file << file_data << "\n\n"; + sync_file.close(); + } } } // namespace http +#include + +static std::wstring to_wstring(const std::string& str) +{ +#if _WIN32 + return winrt::to_hstring(str).c_str(); +#else + size_t expected_utf32words = simdutf::utf32_length_from_utf8(str.data(), str.length()); + std::unique_ptr utf32_output{new char32_t[expected_utf32words]}; + size_t utf16words = simdutf::convert_utf8_to_utf32(str.data(), str.length(), utf32_output.get()); + return std::wstring(utf32_output.get(), utf32_output.get() + utf16words); +#endif +} + +static std::wstring to_wstring(Il2CppString* str) +{ +#if _WIN32 + return str->chars; +#else + size_t expected_utf32words = simdutf::utf32_length_from_utf16(str->chars, str->length); + std::unique_ptr utf32_output{new char32_t[expected_utf32words]}; + size_t utf16words = simdutf::convert_utf16_to_utf32(str->chars, str->length, utf32_output.get()); + return std::wstring(utf32_output.get(), utf32_output.get() + utf16words); +#endif +} + +static std::string to_string(const std::wstring& str) +{ +#if _WIN32 + size_t expected_utf16words = simdutf::utf8_length_from_utf16((char16_t*)str.data(), str.length()); + std::unique_ptr utf8_output{new char8_t[expected_utf16words]}; + size_t utf16words = simdutf::convert_utf16_to_utf8((char16_t*)str.data(), str.length(), (char*)utf8_output.get()); + return std::string(utf8_output.get(), utf8_output.get() + utf16words); +#else + size_t expected_utf32words = simdutf::utf8_length_from_utf32((char32_t*)str.data(), str.length()); + std::unique_ptr utf32_output{new char8_t[expected_utf32words]}; + size_t utf16words = simdutf::convert_utf32_to_utf8((char32_t*)str.data(), str.length(), (char*)utf32_output.get()); + return std::string(utf32_output.get(), utf32_output.get() + utf16words); +#endif +} + std::mutex m; std::condition_variable cv; std::queue sync_data_queue; @@ -451,7 +557,9 @@ static std::wstring gameServerUrl; void ship_sync_data() { +#if _WIN32 winrt::init_apartment(); +#endif for (;;) { { @@ -466,17 +574,31 @@ void ship_sync_data() })(); try { http::send_data(sync_data); +#if _WIN32 } catch (winrt::hresult_error const& ex) { spdlog::error("Failed to send sync data: {}", winrt::to_string(ex.message()).c_str()); } catch (const std::wstring& sz) { spdlog::error("Failed to send sync data: {}", winrt::to_string(sz).c_str()); } +#else + } catch (const std::wstring& sz) { + spdlog::error("Failed to send sync data: {}", to_string(sz)); + } +#endif + catch (const std::runtime_error& e) { + spdlog::error("Failed to send sync data: {}", e.what()); + } } +#if _WIN32 + winrt::uninit_apartment(); +#endif } void ship_combat_log_data() { +#if _WIN32 winrt::init_apartment(); +#endif for (;;) { { @@ -485,14 +607,15 @@ void ship_combat_log_data() } try { - const auto sync_data = ([&] { + const auto sync_data = ([&] { std::lock_guard lk(m2); auto data = combat_log_data_queue.front(); combat_log_data_queue.pop(); return data; })(); - auto body = std::format(L"{{\"journal_id\":{}}}", sync_data); - auto battle_log = http::get_data_data(instanceSessionId, gameServerUrl, L"/journals/get", body); + + auto body = L"{{\"journal_id\":" + std::to_wstring(sync_data) + L"}}"; + auto battle_log = http::get_data_data(instanceSessionId, gameServerUrl, L"/journals/get", body); using json = nlohmann::json; @@ -524,11 +647,11 @@ void ship_combat_log_data() [](const std::string& a, const std::string& b) -> std::string { return a + (a.length() > 0 ? "," : "") + b; }); - auto profiles_body = std::format("{{\"user_ids\":[{}]}}", profiles_joined); - auto profiles = http::get_data_data(instanceSessionId, gameServerUrl, L"/user_profile/profiles", - winrt::to_hstring(profiles_body).c_str()); - auto profiles_json = json::parse(profiles); - auto names = json::object(); + auto profiles_body = "{{\"user_ids\":[" + profiles_joined + "]}}"; + auto profiles = + http::get_data_data(instanceSessionId, gameServerUrl, L"/user_profile/profiles", to_wstring(profiles_body)); + auto profiles_json = json::parse(profiles); + auto names = json::object(); for (const auto& profile : profiles_json["user_profiles"].get()) { names[profile.first] = profile.second["name"]; } @@ -538,26 +661,41 @@ void ship_combat_log_data() auto ship_data = ship_array.dump(); http::write_data(ship_data); http::send_data(ship_data); +#if _WIN32 } catch (winrt::hresult_error const& ex) { spdlog::error("Failed to send sync data: {}", winrt::to_string(ex.message()).c_str()); } catch (const std::wstring& sz) { spdlog::error("Failed to send sync data: {}", winrt::to_string(sz).c_str()); } +#else + } catch (const std::wstring& sz) { + spdlog::error("Failed to send sync data: {}", to_string(sz)); + } +#endif + catch (const std::runtime_error& e) { + spdlog::error("Failed to send sync data: {}", e.what()); + } } catch (...) { } } + +#if _WIN32 + winrt::uninit_apartment(); +#endif } void PrimeApp_InitPrimeServer(auto original, void* _this, Il2CppString* gameServerUrl, Il2CppString* gatewayServerUrl, Il2CppString* sessionId) { original(_this, gameServerUrl, gatewayServerUrl, sessionId); - ::instanceSessionId = sessionId->chars; - ::gameServerUrl = gameServerUrl->chars; + ::instanceSessionId = to_wstring(sessionId); + ::gameServerUrl = to_wstring(gameServerUrl); } void InstallSyncPatches() { + curl_global_init(CURL_GLOBAL_ALL); + auto missions_data_container = il2cpp_get_class_helper("Digit.Client.PrimeLib.Runtime", "Digit.PrimeServer.Models", "MissionsDataContainer"); auto ptr = missions_data_container.GetMethod("ParseBinaryObject"); diff --git a/mods/src/patches/parts/testing.cc b/mods/src/patches/parts/testing.cc index 48eb1ea..d0f2383 100644 --- a/mods/src/patches/parts/testing.cc +++ b/mods/src/patches/parts/testing.cc @@ -1,10 +1,4 @@ -#include "prime_types.h" - -#include -#include - #include "config.h" -#include "utils.h" #include "prime/ActionRequirement.h" #include "prime/AllianceStarbaseObjectViewerWidget.h" @@ -37,8 +31,11 @@ #include "prime/SceneManager.h" #include "prime/ScreenManager.h" #include "prime/StarNodeObjectViewerWidget.h" +#include #include +#include +#include #include #include @@ -46,7 +43,6 @@ #include #include -#include static int i = 0; @@ -116,12 +112,6 @@ void FactionsListDirector_Hook(void* _this, void // } //} -LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter_Hook(LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter) -{ - return nullptr; - return SetUnhandledExceptionFilter(lpTopLevelExceptionFilter); -} - void RequestDispatcherBase_SetDefaultHeader(void* _this, Il2CppString* key, Il2CppString* value); decltype(RequestDispatcherBase_SetDefaultHeader)* oRequestDispatcherBase_SetDefaultHeader = nullptr; void RequestDispatcherBase_SetDefaultHeader(void* _this, Il2CppString* key, Il2CppString* value) @@ -212,12 +202,6 @@ class Model } }; -void* AppConfig_LoadConfig(auto original) -{ - auto app_config = original(); - return app_config; -} - AppConfig* Model_LoadConfigs(auto original, Model* _this) { original(_this); @@ -298,7 +282,7 @@ void* track_ctor(auto original, void* _this) std::scoped_lock lk{tracked_objects_mutex}; auto cls = (Il2CppObject*)_this; spdlog::trace("Tracking {}({})", _this, cls->klass->name); - typedef void (*FinalizerCallback)(void* object, void* client_data); + typedef void (*FinalizerCallback)(void* object, void* client_data); FinalizerCallback oldCallback = nullptr; void* oldData = nullptr; GC_register_finalizer_inner((intptr_t)_this, track_finalizer, nullptr, &oldCallback, &oldData); @@ -381,7 +365,7 @@ template void TrackObject() void SetActive_hook(auto original, void* _this, bool active) { - static auto IsActiveSelf = il2cpp_resolve_icall("UnityEngine.GameObject::get_activeSelf()"); + static auto IsActiveSelf = il2cpp_resolve_icall_typed("UnityEngine.GameObject::get_activeSelf()"); if (active && IsActiveSelf(_this)) { return; @@ -392,10 +376,6 @@ void SetActive_hook(auto original, void* _this, bool active) void InstallTestPatches() { - auto app_config = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Client.Core", "AppConfig"); - auto load_config_ptr = app_config.GetMethod("LoadConfig"); - SPUD_STATIC_DETOUR(load_config_ptr, AppConfig_LoadConfig); - auto model = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Client.Core", "Model"); auto load_configs_ptr = model.GetMethod("LoadConfigs"); SPUD_STATIC_DETOUR(load_configs_ptr, Model_LoadConfigs); @@ -420,12 +400,17 @@ void InstallTestPatches() SPUD_STATIC_DETOUR(il2cpp_unity_liveness_finalize, calc_liveness_hook); - static auto SetActive = il2cpp_resolve_icall("UnityEngine.GameObject::SetActive(System.Boolean)"); + static auto SetActive = + il2cpp_resolve_icall_typed("UnityEngine.GameObject::SetActive(System.Boolean)"); SPUD_STATIC_DETOUR(SetActive, SetActive_hook); +#if _WIN32 auto GC_register_finalizer_inner_matches = spud::find_in_module("40 56 57 41 57 48 83 EC ? 83 3D", "GameAssembly.dll"); - +#else + auto GC_register_finalizer_inner_matches = spud::find_in_module( + "55 48 89 E5 41 57 41 56 41 55 41 54 53 48 83 EC ? 4C 89 45 ? 48 89 4D ? 83 3D", "GameAssembly.dylib"); +#endif auto GC_register_finalizer_inner_matche = GC_register_finalizer_inner_matches.get(0); GC_register_finalizer_inner = (decltype(GC_register_finalizer_inner))GC_register_finalizer_inner_matche.address(); -} \ No newline at end of file +} diff --git a/mods/src/patches/parts/ui_scale.cc b/mods/src/patches/parts/ui_scale.cc index f3e52a7..356be9c 100644 --- a/mods/src/patches/parts/ui_scale.cc +++ b/mods/src/patches/parts/ui_scale.cc @@ -1,21 +1,12 @@ #include "config.h" -#include "prime_types.h" -#include "utils.h" - -#include - -#include - -#include #include -#include "prime/CanvasScaler.h" -#include "prime/ScreenManager.h" - -#include +#include +#include -#include "spdlog/spdlog.h" +#include +#include void SetResolution_Hook(auto original, int x, int y, int mode, int unk) { @@ -28,8 +19,8 @@ void ScreenManager_UpdateCanvasRootScaleFactor_Hook(auto original, ScreenManager original(_this); if (Config::Get().ui_scale != 0.0f) { - static auto get_height_method = il2cpp_resolve_icall("UnityEngine.Screen::get_height()"); - static auto get_width_method = il2cpp_resolve_icall("UnityEngine.Screen::get_width()"); + static auto get_height_method = il2cpp_resolve_icall_typed("UnityEngine.Screen::get_height()"); + static auto get_width_method = il2cpp_resolve_icall_typed("UnityEngine.Screen::get_width()"); static auto ref_height = 1080; static auto ref_width = 1920; @@ -54,12 +45,6 @@ void ScreenManager_UpdateCanvasRootScaleFactor_Hook(auto original, ScreenManager } } -BOOL SetWindowPos_Hook(HWND hWnd, HWND hWndInsertAfter, int X, int Y, int cx, int cy, UINT uFlags) -{ - spdlog::trace("Window size/position {} (x) {} (y) {} (cx) {} (cy)", X, Y, cx, cy); - return SetWindowPos(hWnd, hWndInsertAfter, X, Y, cx, cy, uFlags); -} - void InstallUiScaleHooks() { auto screen_manager_helper = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Client.UI", "ScreenManager"); @@ -69,7 +54,7 @@ void InstallUiScaleHooks() } SPUD_STATIC_DETOUR(ptr_update_scale, ScreenManager_UpdateCanvasRootScaleFactor_Hook); - static auto SetResolution = il2cpp_resolve_icall( + static auto SetResolution = il2cpp_resolve_icall_typed( "UnityEngine.Screen::SetResolution(System.Int32,System.Int32,UnityEngine.FullScreenMode,System.Int32)"); SPUD_STATIC_DETOUR(SetResolution, SetResolution_Hook); diff --git a/mods/src/patches/parts/zoom.cc b/mods/src/patches/parts/zoom.cc index 9bd5763..5545041 100644 --- a/mods/src/patches/parts/zoom.cc +++ b/mods/src/patches/parts/zoom.cc @@ -1,25 +1,19 @@ #include "config.h" -#include "prime_types.h" -#include "patches/mapkey.h" -#include "utils.h" -#include -#include +#include #include -#include "prime/EventSystem.h" -#include "prime/Hub.h" -#include "prime/KeyCode.h" -#include "prime/NavigationPan.h" -#include "prime/NavigationZoom.h" -#include "prime/ScreenManager.h" -#include "prime/TMP_InputField.h" +#include +#include + +#include +#include vec3 GetMouseWorldPos(void *cam, vec3 *pos) { static auto class_helper = il2cpp_get_class_helper("Digit.Client.PrimeLib.Runtime", "Digit.Client.Core", "MathUtils"); - static auto fn = class_helper.GetMethodInfo("GetMouseWorldPos"); + static auto fn = class_helper.GetMethodInfo("GetMouseWorldPos"); void *args[2] = {cam, (void *)pos}; Il2CppException *exception = NULL; @@ -39,8 +33,8 @@ inline void StoreZoom(std::string label, float &zoom, NavigationZoom *_this) void NavigationZoom_Update_Hook(auto original, NavigationZoom *_this) { static auto GetMousePosition = - il2cpp_resolve_icall("UnityEngine.Input::get_mousePosition_Injected(UnityEngine.Vector3&)"); - static auto GetDeltaTime = il2cpp_resolve_icall("UnityEngine.Time::get_deltaTime()"); + il2cpp_resolve_icall_typed("UnityEngine.Input::get_mousePosition_Injected(UnityEngine.Vector3&)"); + static auto GetDeltaTime = il2cpp_resolve_icall_typed("UnityEngine.Time::get_deltaTime()"); const auto dt = GetDeltaTime(); auto zoomDelta = 0.0f; @@ -147,14 +141,67 @@ void NavigationZoom_SetViewParameters_Hook(auto original, NavigationZoom *_this, } } +void NavigationZoom_ApplyRangeChanges_Hook(auto original, NavigationZoom *_this) +{ + if (_this->_depth == NodeDepth::SolarSystem) { + auto ratio = (Config::Get().zoom / _this->_viewRadius); + _this->_farRatioSystemNormal = 0.55f * ratio; + _this->_farRatioSystemExtended = 1 * ratio; + original(_this); + _this->_sceneCamera->farClipPlane = Config::Get().zoom * 2.75f; + do_default_zoom = true; + } else { + original(_this); + } +} + +void NavigationZoom_SetDepth_Hook(auto original, NavigationZoom *_this, NodeDepth depth) +{ + if (depth == NodeDepth::SolarSystem) { + auto ratio = (Config::Get().zoom / _this->_viewRadius); + _this->_farRatioSystemNormal = 0.55f * ratio; + _this->_farRatioSystemExtended = 1 * ratio; + _this->_sceneCamera->farClipPlane = Config::Get().zoom * 3.75f; + original(_this, depth); + _this->_sceneCamera->farClipPlane = Config::Get().zoom * 3.75f; + do_default_zoom = true; + } else { + original(_this, depth); + } +} + +void NavigationCamera_SetSystemViewSizeData_Hook(auto original, uint8_t *_this_cam, float radius, Vector3 *systemPos, + NodeDepth depth) +{ + if (depth == NodeDepth::SolarSystem) { + auto _this = *(NavigationZoom **)(_this_cam + 0x20); + auto ratio = (Config::Get().zoom / radius); + _this->_farRatioSystemNormal = 0.55f * ratio; + _this->_farRatioSystemExtended = 1 * ratio; + original(_this_cam, radius, systemPos, depth); + _this->_sceneCamera->farClipPlane = Config::Get().zoom * 2.75f; + do_default_zoom = true; + } else { + original(_this_cam, radius, systemPos, depth); + } +} + void InstallZoomHooks() { auto screen_manager_helper = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.Navigation", "NavigationZoom"); auto ptr_set_view_parameters = screen_manager_helper.GetMethod("SetViewParameters"); auto ptr_update = screen_manager_helper.GetMethod("Update"); - if (!ptr_set_view_parameters || !ptr_update) { - return; - } + auto ptr_apply_range_changes = screen_manager_helper.GetMethod("ApplyRangeChanges"); + auto ptr_set_depth = screen_manager_helper.GetMethod("SetDepth"); SPUD_STATIC_DETOUR(ptr_update, NavigationZoom_Update_Hook); + SPUD_STATIC_DETOUR(ptr_set_depth, NavigationZoom_SetDepth_Hook); + +#if _WIN32 SPUD_STATIC_DETOUR(ptr_set_view_parameters, NavigationZoom_SetViewParameters_Hook); + // SPUD_STATIC_DETOUR(ptr_apply_range_changes, NavigationZoom_ApplyRangeChanges_Hook); +#endif + + // auto navigation_camera = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.Navigation", "NavigationCamera"); + // auto ptr_set_system_view_size_data = navigation_camera.GetMethod("SetSystemViewSizeData"); + // SPUD_STATIC_DETOUR(ptr_set_system_view_size_data, NavigationCamera_SetSystemViewSizeData_Hook); } diff --git a/mods/src/patches/patches.cc b/mods/src/patches/patches.cc index a4312f9..564bf1e 100644 --- a/mods/src/patches/patches.cc +++ b/mods/src/patches/patches.cc @@ -1,30 +1,32 @@ #include "patches.h" +#include "version.h" -#include - -#include +#include #include -#include "il2cpp/il2cpp_helper.h" - -#include "spdlog/sinks/basic_file_sink.h" -#include "spdlog/sinks/stdout_color_sinks.h" -#include "spdlog/spdlog.h" +#include +#include +#include -#include "Dbghelp.h" - -#include "version.h" +#if _WIN32 +#include +#else +#include +#include +#include +#endif -#include +#include void InstallUiScaleHooks(); void InstallZoomHooks(); void InstallBuffFixHooks(); +#if _WIN32 void InstallFreeResizeHooks(); +#endif void InstallToastBannerHooks(); void InstallPanHooks(); -void InstallWebRequestHooks(); void InstallImproveResponsivenessHooks(); void InstallHotkeyHooks(); void InstallTestPatches(); @@ -36,12 +38,16 @@ void InstallSyncPatches(); __int64 __fastcall il2cpp_init_hook(auto original, const char* domain_name) { + printf("il2cpp_init_hook(%s)\n", domain_name); + auto r = original(domain_name); +#if _WIN32 #ifndef NDEBUG AllocConsole(); FILE* fp; freopen_s(&fp, "CONOUT$", "w", stdout); +#endif #endif auto file_logger = spdlog::basic_logger_mt("default", "community_patch.log", true); @@ -51,37 +57,37 @@ __int64 __fastcall il2cpp_init_hook(auto original, const char* domain_name) spdlog::info("Initializing STFC Community Patch ({})", VER_PRODUCT_VERSION_STR); - const std::map patches = { - {"UiScaleHooks", InstallUiScaleHooks}, - {"ZoomHooks", InstallZoomHooks}, - {"BuffFixHooks", InstallBuffFixHooks}, - {"ToastBannerHooks", InstallToastBannerHooks}, - {"PanHooks", InstallPanHooks}, - {"WebRequestHooks", InstallWebRequestHooks}, - {"ImproveResponsivenessHooks", InstallImproveResponsivenessHooks}, - {"HotkeyHooks", InstallHotkeyHooks}, - {"FreeResizeHooks", InstallFreeResizeHooks}, - {"TempCrashFixes", InstallTempCrashFixes}, - {"TestPatches", InstallTestPatches}, - {"MiscPatches", InstallMiscPatches}, - {"ChatPatches", InstallChatPatches}, - {"ResolutionListFix", InstallResolutionListFix}, - {"SyncPatches", InstallSyncPatches}, + const std::pair patches[] = { + {"UiScaleHooks", InstallUiScaleHooks}, + {"ZoomHooks", InstallZoomHooks}, + {"BuffFixHooks", InstallBuffFixHooks}, + {"ToastBannerHooks", InstallToastBannerHooks}, + {"PanHooks", InstallPanHooks}, + {"ImproveResponsivenessHooks", InstallImproveResponsivenessHooks}, + {"HotkeyHooks", InstallHotkeyHooks}, +#if _WIN32 + {"FreeResizeHooks", InstallFreeResizeHooks}, +#endif + {"TempCrashFixes", InstallTempCrashFixes}, + {"TestPatches", InstallTestPatches}, + {"MiscPatches", InstallMiscPatches}, + {"ChatPatches", InstallChatPatches}, + {"ResolutionListFix", InstallResolutionListFix}, + {"SyncPatches", InstallSyncPatches}, }; auto patch_count = 0; - auto patch_total = patches.size(); - for (auto& kv : patches) { - auto patch_name = kv.first; - auto patch_func = kv.second; + auto patch_total = sizeof(patches) / sizeof(patches[0]); + for (const auto& [patch_name, patch_func] : patches) { patch_count++; spdlog::info("Patching {:>2} of {} ({})", patch_count, patch_total, patch_name); - reinterpret_cast(patch_func)(); + patch_func(); } spdlog::info(""); #if VERSION_PATCH - spdlog::info("Loaded beta version {}.{}.{} (Patch {})", VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION, VERSION_PATCH); + spdlog::info("Loaded beta version {}.{}.{} (Patch {})", VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION, + VERSION_PATCH); spdlog::info(""); spdlog::info("NOTE: Beta versions may have unexpected bugs and issues"); #else @@ -96,45 +102,24 @@ __int64 __fastcall il2cpp_init_hook(auto original, const char* domain_name) return r; } -void CreateMiniDump(EXCEPTION_POINTERS* pep) +void ApplyPatches() { - // Open the file - typedef BOOL (*PDUMPFN)(HANDLE hProcess, DWORD ProcessId, HANDLE hFile, MINIDUMP_TYPE DumpType, - PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, - PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, - PMINIDUMP_CALLBACK_INFORMATION CallbackParam); - - HANDLE hFile = - CreateFileW(L"Minidump.dmp", GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); - - HMODULE h = ::LoadLibraryW(L"DbgHelp.dll"); - PDUMPFN pFn = (PDUMPFN)GetProcAddress(h, "MiniDumpWriteDump"); - - if ((hFile != NULL) && (hFile != INVALID_HANDLE_VALUE)) { - MINIDUMP_EXCEPTION_INFORMATION mdei; - - mdei.ThreadId = GetCurrentThreadId(); - mdei.ExceptionPointers = pep; - mdei.ClientPointers = TRUE; - - MINIDUMP_TYPE mdt = MiniDumpNormal; - - (*pFn)(GetCurrentProcess(), GetCurrentProcessId(), hFile, mdt, (pep != 0) ? &mdei : 0, 0, 0); +#if _WIN32 + auto assembly = LoadLibraryA("GameAssembly.dll"); +#else + char buf[PATH_MAX]; + uint32_t bufsize = PATH_MAX; + _NSGetExecutablePath(buf, &bufsize); - CloseHandle(hFile); - } -} + char assembly_path[PATH_MAX]; + sprintf(assembly_path, "%s/%s", dirname(buf), "../Frameworks/GameAssembly.dylib"); + printf("Loading %s\n", assembly_path); + auto assembly = dlopen(assembly_path, RTLD_LAZY | RTLD_GLOBAL); -LONG WINAPI CrashHandler(struct _EXCEPTION_POINTERS* ExceptionInfo) -{ - CreateMiniDump(ExceptionInfo); - return EXCEPTION_EXECUTE_HANDLER; -} - -void Patches::Apply() -{ - auto assembly = LoadLibraryA("GameAssembly.dll"); + init_il2cpp_pointers(); +#endif + printf("Got assembly %p\n", assembly); try { #ifndef NDEBUG const auto log_level = spdlog::level::trace; @@ -145,7 +130,12 @@ void Patches::Apply() spdlog::set_level(log_level); spdlog::flush_on(log_level); +#if _WIN32 auto n = GetProcAddress(assembly, "il2cpp_init"); +#else + auto n = dlsym(assembly, "il2cpp_init"); +#endif + printf("Got il2cpp_init %p\n", n); SPUD_STATIC_DETOUR(n, il2cpp_init_hook); } catch (...) { // Failed to Apply at least some patches diff --git a/mods/src/patches/patches.h b/mods/src/patches/patches.h index 111a2c3..8a3f2c1 100644 --- a/mods/src/patches/patches.h +++ b/mods/src/patches/patches.h @@ -1,5 +1,3 @@ #pragma once -struct Patches { - static void Apply(); -}; \ No newline at end of file +void ApplyPatches(); diff --git a/mods/src/prime/FleetsManager.h b/mods/src/prime/FleetsManager.h index adf9079..86681a7 100644 --- a/mods/src/prime/FleetsManager.h +++ b/mods/src/prime/FleetsManager.h @@ -24,7 +24,7 @@ struct FleetsManager : MonoSingleton { private: static IL2CppClassHelper& get_class_helper() { - auto class_helper = FleetsManager::get_class_helper().GetNestedType("d__170"); + static auto class_helper = FleetsManager::get_class_helper().GetNestedType("d__170"); return class_helper; } }; @@ -64,4 +64,4 @@ struct FleetsManager : MonoSingleton { il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.FleetManagement", "FleetsManager"); return class_helper; } -}; \ No newline at end of file +}; diff --git a/mods/src/prime/HttpRequest.h b/mods/src/prime/HttpRequest.h index 0a321f7..81733b2 100644 --- a/mods/src/prime/HttpRequest.h +++ b/mods/src/prime/HttpRequest.h @@ -4,7 +4,7 @@ struct HttpRequest { public: - const wchar_t* get_URL() + const Il2CppChar* get_URL() { static auto prop = get_class_helper().GetProperty("URL"); auto s = prop.Get((void*)this); diff --git a/mods/src/prime/NavigationZoom.h b/mods/src/prime/NavigationZoom.h index 34e57eb..0af7280 100644 --- a/mods/src/prime/NavigationZoom.h +++ b/mods/src/prime/NavigationZoom.h @@ -32,7 +32,7 @@ struct NavigationZoom { __declspec(property(get = __get_Distance, put = __set_Distance)) float Distance; - __declspec(property(get = __get__depth, put = __set_depth)) int _depth; + __declspec(property(get = __get__depth, put = __set_depth)) NodeDepth _depth; // __declspec(property(get = __get__storedZoomDistanceSystem, put = __set__storedZoomDistanceSystem)) float _storedZoomDistanceSystem; @@ -74,16 +74,16 @@ struct NavigationZoom { return field.SetRaw(this, depth); } - int __get__depth() + NodeDepth __get__depth() { static auto field = get_class_helper().GetField("_depth"); - return *(int*)((ptrdiff_t)this + field.offset()); + return *(NodeDepth*)((ptrdiff_t)this + field.offset()); } - void __set_depth(int depth) + void __set_depth(NodeDepth depth) { - static auto field = get_class_helper().GetField("_depth"); - *(int*)((ptrdiff_t)this + field.offset()) = depth; + static auto field = get_class_helper().GetField("_depth"); + *(NodeDepth*)((ptrdiff_t)this + field.offset()) = depth; } float __get__viewRadius() @@ -247,4 +247,4 @@ struct NavigationZoom { static auto field = get_class_helper().GetField("_farRatioSystemExtended").offset(); *(float*)((ptrdiff_t)this + field) = v; } -}; \ No newline at end of file +}; diff --git a/mods/src/prime/SceneManager.h b/mods/src/prime/SceneManager.h index 983de88..c80559b 100644 --- a/mods/src/prime/SceneManager.h +++ b/mods/src/prime/SceneManager.h @@ -2,21 +2,19 @@ #include -#include "utils.h" - struct SceneManager { public: - static const wchar_t* GetSceneName(void* scene) + static const Il2CppChar* GetSceneName(void* scene) { - static auto GetSceneName = - il2cpp_resolve_icall("UnityEngine.SceneManagement.Scene::GetNameInternal(System.Int32)"); + static auto GetSceneName = il2cpp_resolve_icall_typed( + "UnityEngine.SceneManagement.Scene::GetNameInternal(System.Int32)"); auto name = GetSceneName(scene); return il2cpp_string_chars(name); } static void* GetActiveScene() { - static auto GetActiveScene_Injected = il2cpp_resolve_icall( + static auto GetActiveScene_Injected = il2cpp_resolve_icall_typed( "UnityEngine.SceneManagement.SceneManager::GetActiveScene_Injected(UnityEngine.SceneManagement.Scene&)"); void* scene = nullptr; GetActiveScene_Injected(&scene); @@ -30,4 +28,4 @@ struct SceneManager { il2cpp_get_class_helper("UnityEngine.CoreModule", "UnityEngine.SceneManagement", "SceneManager"); return class_helper; } -}; \ No newline at end of file +}; diff --git a/mods/src/str_utils.h b/mods/src/str_utils.h index 7d21b87..5c70a42 100644 --- a/mods/src/str_utils.h +++ b/mods/src/str_utils.h @@ -1,10 +1,10 @@ #pragma once +#include +#include #include #include -#include #include -#include inline bool ascii_isspace(unsigned char c) { diff --git a/mods/src/utils.cc b/mods/src/utils.cc deleted file mode 100644 index 5cb5a15..0000000 --- a/mods/src/utils.cc +++ /dev/null @@ -1,94 +0,0 @@ -#include "utils.h" - -#include - -#include - -bool set_import(const std::string module, const std::string &name, uintptr_t func) -{ - static uint64_t image_base = 0; - - if (image_base == 0) - { - image_base = uint64_t(GetModuleHandleA(module.size() > 0 ? module.c_str() : NULL)); - } - - bool result = false; - auto dosHeader = (PIMAGE_DOS_HEADER)(image_base); - if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) - { - throw std::runtime_error("Invalid DOS Signature"); - } - - auto header = (PIMAGE_NT_HEADERS)((image_base + (dosHeader->e_lfanew * sizeof(char)))); - if (header->Signature != IMAGE_NT_SIGNATURE) - { - throw std::runtime_error("Invalid NT Signature"); - } - - // BuildImportTable - PIMAGE_DATA_DIRECTORY directory = - &header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; - - if (directory->Size > 0) - { - auto importDesc = (PIMAGE_IMPORT_DESCRIPTOR)(header->OptionalHeader.ImageBase + directory->VirtualAddress); - for (; !IsBadReadPtr(importDesc, sizeof(IMAGE_IMPORT_DESCRIPTOR)) && importDesc->Name; - importDesc++) - { - wchar_t buf[0xFFF] = {}; - auto name2 = (LPCSTR)(header->OptionalHeader.ImageBase + importDesc->Name); - std::wstring sname; - size_t converted; - mbstowcs_s(&converted, buf, name2, sizeof(buf)); - sname = buf; - auto csname = sname.c_str(); - - HMODULE handle = LoadLibraryW(csname); - - if (handle == nullptr) - { - SetLastError(ERROR_MOD_NOT_FOUND); - break; - } - - auto *thunkRef = - (uintptr_t *)(header->OptionalHeader.ImageBase + importDesc->OriginalFirstThunk); - auto *funcRef = (FARPROC *)(header->OptionalHeader.ImageBase + importDesc->FirstThunk); - - if (!importDesc->OriginalFirstThunk) // no hint table - { - thunkRef = (uintptr_t *)(header->OptionalHeader.ImageBase + importDesc->FirstThunk); - } - - for (; *thunkRef, *funcRef; thunkRef++, (void)funcRef++) - { - if (!IMAGE_SNAP_BY_ORDINAL(*thunkRef)) - { - std::string import = - (LPCSTR) & ((PIMAGE_IMPORT_BY_NAME)(header->OptionalHeader.ImageBase + (*thunkRef))) - ->Name; - - if (import == name) - { - DWORD oldProtect; - VirtualProtect((void *)funcRef, sizeof(FARPROC), PAGE_EXECUTE_READWRITE, - &oldProtect); - - *funcRef = (FARPROC)func; - - VirtualProtect((void *)funcRef, sizeof(FARPROC), oldProtect, &oldProtect); - result = true; - } - } - } - } - } - return result; -} - -void *il2cpp_resolve_icall_internal(const char *name) -{ - static auto addr = (decltype(il2cpp_resolve_icall_internal) *)(GetProcAddress(GetModuleHandle("GameAssembly.dll"), "il2cpp_resolve_icall")); - return addr(name); -} \ No newline at end of file diff --git a/mods/src/utils.h b/mods/src/utils.h deleted file mode 100644 index 31f69d5..0000000 --- a/mods/src/utils.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include - -bool set_import(const std::string module, const std::string &name, uintptr_t func); - -void * il2cpp_resolve_icall_internal(const char*); - -template -T *il2cpp_resolve_icall(const char* name) { - return (T*)il2cpp_resolve_icall_internal(name); -} diff --git a/mods/xmake.lua b/mods/xmake.lua index e55117b..e3d24f6 100644 --- a/mods/xmake.lua +++ b/mods/xmake.lua @@ -1,14 +1,22 @@ target("mods") +do + add_ldflags("-v") set_kind("static") add_files("src/**.cc") add_headerfiles("src/**.h") - add_includedirs("src", {public = true}) - add_packages("spud", "nlohmann_json", "protobuf-cpp", "libil2cpp", "eastl", "toml++", "spdlog") + add_includedirs("src", { public = true }) + add_packages("spud", "nlohmann_json", "protobuf-cpp", "libil2cpp", "eastl", "toml++", "spdlog", "simdutf", "libcurl") add_rules("protobuf.cpp") add_files("src/prime/proto/*.proto") set_exceptions("cxx") + add_defines("NOMINMAX") if is_plat("windows") then add_cxflags("/bigobj") add_linkdirs("src/il2cpp") end - set_policy("build.optimization.lto", true) \ No newline at end of file + if is_plat("macosx") then + add_cxflags("-fms-extensions") + add_files("src/*.mm") + end + set_policy("build.optimization.lto", true) +end diff --git a/scripts/create-mac-dmg.sh b/scripts/create-mac-dmg.sh new file mode 100755 index 0000000..cd2fbfb --- /dev/null +++ b/scripts/create-mac-dmg.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +set -xe + +xmake clean +xmake f -y -a x86_64 -m release +xmake + +rm build/macosx/x86_64/release/libmods.a +rm build/macosx/x86_64/release/macOSLauncher +mv build/macosx/x86_64/release/stfc-community-patch-loader build/macosx/x86_64/release/macOSLauncher.app/Contents +mv build/macosx/x86_64/release/libstfc-community-patch.dylib build/macosx/x86_64/release/macOSLauncher.app/Contents +cp assets/launcher.icns build/macosx/x86_64/release/macOSLauncher.app/Contents/Resources +rm -r build/macosx/x86_64/release/STFC\ Community\ Patch.app || true +mv build/macosx/x86_64/release/macOSLauncher.app build/macosx/x86_64/release/STFC\ Community\ Patch.app + +codesign --force --verify --verbose --deep --sign "-" build/macosx/x86_64/release/STFC\ Community\ Patch.app + +rm STFC-Community-Patch-Installer.dmg || true +create-dmg \ + --volname "STFC Community Patch Installer" \ + --background "assets/mac_installer_background.png" \ + --window-pos 200 120 \ + --window-size 800 400 \ + --icon-size 100 \ + --icon "STFC Community Patch.app" 200 190 \ + --hide-extension "STFC Community Patch.app" \ + --app-drop-link 600 185 \ + "STFC-Community-Patch-Installer.dmg" \ + "build/macosx/x86_64/release/" diff --git a/win-proxy-dll/src/main.cc b/win-proxy-dll/src/main.cc index 750c6e2..f4c0e07 100644 --- a/win-proxy-dll/src/main.cc +++ b/win-proxy-dll/src/main.cc @@ -1,8 +1,5 @@ #include -#include -#include - #include #include "patches/patches.h" @@ -28,7 +25,7 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID /*lpReserved*/) // Since we are replacing version.dll, need the proper forwards VersionDllInit(); - Patches::Apply(); + ApplyPatches(); break; case DLL_THREAD_ATTACH: break; @@ -50,4 +47,4 @@ void* operator new[](size_t size, size_t /*alignment*/, size_t /*alignmentOffset unsigned /*debugFlags*/, const char* /*file*/, int /*line*/) { return malloc(size); -} \ No newline at end of file +} diff --git a/win-proxy-dll/xmake.lua b/win-proxy-dll/xmake.lua index 04a3cc4..ab666c7 100644 --- a/win-proxy-dll/xmake.lua +++ b/win-proxy-dll/xmake.lua @@ -1,4 +1,5 @@ target("stfc-community-patch") +do set_kind("shared") add_files("src/*.cc") add_files("src/*.rc") @@ -10,4 +11,5 @@ target("stfc-community-patch") add_cxflags("/bigobj") end set_policy("build.optimization.lto", true) - add_links("User32.lib", "Ole32.lib", "OleAut32.lib") \ No newline at end of file + add_links("User32.lib", "Ole32.lib", "OleAut32.lib") +end diff --git a/xmake-packages/packages/l/librsync/patches/v2.3.4/rsync_cmake.patch b/xmake-packages/packages/l/librsync/patches/v2.3.4/rsync_cmake.patch new file mode 100644 index 0000000..b686018 --- /dev/null +++ b/xmake-packages/packages/l/librsync/patches/v2.3.4/rsync_cmake.patch @@ -0,0 +1,19 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index c9861d4..7fef403 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -34,14 +34,6 @@ set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + # Turn on generating compile_commands.json for clang-tidy and iwyu. + set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +-if (NOT CMAKE_SYSTEM_PROCESSOR) +- message(FATAL_ERROR "No target CPU architecture set") +-endif() +- +-if (NOT CMAKE_SYSTEM_NAME) +- message(FATAL_ERROR "No target OS set") +-endif() +- + # Set CMAKE_BUILD_TYPE if unset. + include(BuildType) + message (STATUS "CMAKE_BUILD_TYPE = ${CMAKE_BUILD_TYPE}") diff --git a/xmake-packages/packages/l/librsync/xmake.lua b/xmake-packages/packages/l/librsync/xmake.lua new file mode 100644 index 0000000..17928e6 --- /dev/null +++ b/xmake-packages/packages/l/librsync/xmake.lua @@ -0,0 +1,13 @@ +package("librsync") +add_deps("cmake") +add_urls("https://github.com/librsync/librsync/archive/refs/tags/$(version).tar.gz", + "https://github.com/librsync/librsync.git") +add_versions("v2.3.4", "a0dedf9fff66d8e29e7c25d23c1f42beda2089fb4eac1b36e6acd8a29edfbd1f") +add_patches("v2.3.4", path.join(os.scriptdir(), "patches", "v2.3.4", "rsync_cmake.patch")) +on_install(function(package) + local configs = {} + table.insert(configs, "-DCMAKE_BUILD_TYPE=" .. (package:debug() and "Debug" or "Release")) + table.insert(configs, "-DBUILD_SHARED_LIBS=" .. (package:config("shared") and "ON" or "OFF")) + table.insert(configs, "-DCMAKEY_SYSTEM_PROCESSOR=x86_64") + import("package.tools.cmake").install(package, configs) +end) diff --git a/xmake-packages/packages/s/spud/xmake.lua b/xmake-packages/packages/s/spud/xmake.lua new file mode 100644 index 0000000..d7a5b3c --- /dev/null +++ b/xmake-packages/packages/s/spud/xmake.lua @@ -0,0 +1,18 @@ +package("spud") +add_deps("cmake") +add_urls("https://github.com/tashcan/spud/archive/refs/tags/$(version).tar.gz", + "https://github.com/tashcan/spud.git") +add_versions("v0.1.1", "4298ec14727080166a959051d647a2acfcdceb0170bd1d269c1c76c8e51c1dca") +add_versions("v0.2.0.alpha.1", "30df1499b5e4a51ae7ddd9fbda410dcbe6fed2725d6097dfbb2990e9a9ba2ece") +add_versions("v0.2.0.alpha.2", "414db66510c3410ea1e90207fff1be15c72b362983c04b14c63edc5d7067b4f1") +add_versions("v0.2.0.alpha.3", "a0e0d810a6dfd3919267581766663fa28d1980b8d4a6f111fa770d0d3e6ce211") +add_versions("v0.2.0.alpha.4", "f758c0a2403256837d84df7b103612d7ad4360917310f3dd6a04f77bbfd22bfb") +on_install(function(package) + local configs = {} + table.insert(configs, "-DCMAKE_BUILD_TYPE=" .. (package:debug() and "Debug" or "Release")) + table.insert(configs, "-DBUILD_SHARED_LIBS=" .. (package:config("shared") and "ON" or "OFF")) + table.insert(configs, "-DSPUD_BUILD_TESTS=OFF") + table.insert(configs, "-DSPUD_NO_LTO=ON") + table.insert(configs, "-DCMAKE_OSX_ARCHITECTURES=x86_64") + import("package.tools.cmake").install(package, configs) +end) diff --git a/xmake.lua b/xmake.lua index 7859c1d..819d3bc 100644 --- a/xmake.lua +++ b/xmake.lua @@ -1,56 +1,43 @@ +set_project("stfc-community-patch") + set_languages("c++23") add_requires("eastl") add_requires("spdlog") -add_requires("imgui") add_requires("protobuf-cpp") add_requires("toml++") add_requires("nlohmann_json") +add_requires("inifile-cpp") +add_requires("7z") +add_requires("lzma") +add_requires("librsync") +add_requires("libcurl", { configs = { zlib = true }}) if is_plat("windows") then includes("win-proxy-dll") end if is_plat("macosx") then + includes("macos-dylib") includes("macos-loader") + includes("macos-launcher") end add_rules("mode.debug") add_rules("mode.releasedbg") -package("spud") - add_deps("cmake") - add_urls("https://github.com/tashcan/spud/archive/refs/tags/$(version).tar.gz", - "https://github.com/tashcan/spud.git") - add_versions("v0.1.1", "4298ec14727080166a959051d647a2acfcdceb0170bd1d269c1c76c8e51c1dca") - add_versions("v0.2.0.alpha.1", "30df1499b5e4a51ae7ddd9fbda410dcbe6fed2725d6097dfbb2990e9a9ba2ece") - on_install(function (package) - local configs = {} - table.insert(configs, "-DCMAKE_BUILD_TYPE=" .. (package:debug() and "Debug" or "Release")) - table.insert(configs, "-DBUILD_SHARED_LIBS=" .. (package:config("shared") and "ON" or "OFF")) - table.insert(configs, "-DSPUD_BUILD_TESTS=OFF") - table.insert(configs, "-DSPUD_NO_LTO=ON") - import("package.tools.cmake").install(package, configs) - end) - on_test(function (package) - assert(package:check_cxxsnippets({test = [[ - void test() { - auto memory = spud::alloc_executable_memory(128); - (void)memory; - } - ]]}, {configs = {languages = "c++17"}, includes = "spud/utils.h"})) - end) -package_end() - package("libil2cpp") - on_fetch(function (package, opt) - return {includedirs = path.join(os.scriptdir(), "third_party/libil2cpp")} - end) +on_fetch(function(package, opt) + return { includedirs = path.join(os.scriptdir(), "third_party/libil2cpp") } +end) package_end() -add_requires("spud v0.2.0.alpha.1") +add_requires("spud v0.2.0.alpha.4") add_requires("libil2cpp") +add_requires("simdutf") -- includes("launcher") includes("mods") + +add_repositories("stfc-community-patch-repo xmake-packages")