Skip to content

Commit db32a66

Browse files
author
SirStig
committed
Extract pilot economy logic to shared module
- Move core trade, outfit, ship, and escort economic calculations from `PilotStore` to a new `PilotEconomy` type in `NovaSwiftStory`. - Update `NovaSwiftStory` dependency graph to include `NovaSwiftEngine`. - Add a cross-platform logging shim to `NovaSwiftStory` for non-Apple targets (Linux/Windows). - Expose `PilotEconomy` to the Godot bridge for shared logic consistency. - Miscellaneous UI adjustments for tvOS controller support.
1 parent 385dc9d commit db32a66

10 files changed

Lines changed: 857 additions & 454 deletions

File tree

‎Package.swift‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ let package = Package(
3131
targets: [
3232
.target(name: "NovaSwiftKit", path: "Sources/NovaSwiftKit"),
3333
.target(name: "NovaSwiftEngine", dependencies: ["NovaSwiftKit"], path: "Sources/NovaSwiftEngine"),
34-
.target(name: "NovaSwiftStory", dependencies: ["NovaSwiftKit"], path: "Sources/NovaSwiftStory"),
34+
.target(name: "NovaSwiftStory", dependencies: ["NovaSwiftKit", "NovaSwiftEngine"], path: "Sources/NovaSwiftStory"),
3535
.target(name: "NovaSwiftNet", path: "Sources/NovaSwiftNet"),
3636
.target(
3737
name: "NovaSwiftSync",
Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,56 @@
1+
#if canImport(os)
12
import os
3+
#endif
24

3-
/// Categorized `os.Logger`s for the mission/story runtime (control bits,
4-
/// missions, cron events, pilot persistence). View in Console.app/`log stream`
5-
/// filtered by `subsystem:com.novaswift.story`, or per-category with `category:<name>`.
5+
/// Categorized loggers for the mission/story runtime (control bits, missions,
6+
/// cron events, pilot persistence). On Apple platforms these are real
7+
/// `os.Logger`s (view in Console.app/`log stream` filtered by
8+
/// `subsystem:com.novaswift.story`, or per-category with `category:<name>`);
9+
/// off-Apple (Linux/Windows, e.g. the Godot bridge) they fall back to a tiny
10+
/// stderr-printing shim with the same call surface, since `os.Logger` doesn't
11+
/// exist there.
612
public enum Log {
713
static let subsystem = "com.novaswift.story"
814

15+
#if canImport(os)
916
public static let mission = Logger(subsystem: subsystem, category: "Mission")
1017
public static let ncb = Logger(subsystem: subsystem, category: "NCB")
1118
public static let pilot = Logger(subsystem: subsystem, category: "Pilot")
19+
#else
20+
public static let mission = FallbackLogger(category: "Mission")
21+
public static let ncb = FallbackLogger(category: "NCB")
22+
public static let pilot = FallbackLogger(category: "Pilot")
23+
#endif
1224
}
25+
26+
#if !canImport(os)
27+
/// A string-interpolation type shaped like `os.Logger`'s `OSLogMessage` just
28+
/// enough that call sites written as `"...\(value, privacy: .public)"` — the
29+
/// house style throughout this codebase — keep compiling off-Apple without
30+
/// editing every call site. `privacy:` is accepted and ignored: there's no
31+
/// Console.app redaction story on Linux/Windows, so it always just interpolates.
32+
public struct LogMessage: ExpressibleByStringInterpolation, ExpressibleByStringLiteral, CustomStringConvertible {
33+
public enum Privacy { case `public` }
34+
35+
public struct StringInterpolation: StringInterpolationProtocol {
36+
var value = ""
37+
public init(literalCapacity: Int, interpolationCount: Int) { value.reserveCapacity(literalCapacity) }
38+
public mutating func appendLiteral(_ literal: String) { value += literal }
39+
public mutating func appendInterpolation<T>(_ value: T) { self.value += "\(value)" }
40+
public mutating func appendInterpolation<T>(_ value: T, privacy: Privacy) { self.value += "\(value)" }
41+
}
42+
43+
public let description: String
44+
public init(stringLiteral value: String) { description = value }
45+
public init(stringInterpolation: StringInterpolation) { description = stringInterpolation.value }
46+
}
47+
48+
/// Minimal stand-in for the subset of `os.Logger`'s API this module uses
49+
/// (`.debug`/`.notice`/`.error`). Prints to stderr.
50+
public struct FallbackLogger {
51+
let category: String
52+
public func debug(_ message: LogMessage) { FileHandle.standardError.write("[\(category)] \(message)\n".data(using: .utf8)!) }
53+
public func notice(_ message: LogMessage) { FileHandle.standardError.write("[\(category)] \(message)\n".data(using: .utf8)!) }
54+
public func error(_ message: LogMessage) { FileHandle.standardError.write("[\(category)] \(message)\n".data(using: .utf8)!) }
55+
}
56+
#endif

‎Sources/NovaSwiftStory/PilotEconomy.swift‎

Lines changed: 536 additions & 0 deletions
Large diffs are not rendered by default.

‎app/NovaSwift/App/RootView.swift‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ import SwiftUI
44
struct RootView: View {
55
@EnvironmentObject private var model: AppModel
66
@State private var menuAssets: MainMenuAssets?
7+
#if os(tvOS)
8+
/// Drives the controller-required gate: shows/hides live with pairing.
9+
@ObservedObject private var padState = PadState.shared
10+
/// Dev/automation escape hatch — the simulator has no way to pair a pad.
11+
static let padGateDisabled: Bool = {
12+
#if DEBUG
13+
ProcessInfo.processInfo.environment["NOVASWIFT_NO_PAD_GATE"] != nil
14+
#else
15+
false
16+
#endif
17+
}()
18+
#endif
719

820
var body: some View {
921
ZStack {
@@ -81,6 +93,19 @@ struct RootView: View {
8193
// click into any screen. Inert on tvOS / without a pad.
8294
ControllerCursorOverlay()
8395
.zIndex(50)
96+
97+
#if os(tvOS)
98+
// NovaSwift is controller-required on Apple TV (the Siri Remote
99+
// can't fly a ship, and the whole UI is cursor-driven). Gate the
100+
// app until an extended gamepad pairs; lifts itself the moment
101+
// one connects. Matches the GCSupportedGameControllers Info.plist
102+
// declaration (ExtendedGamepad only).
103+
if !padState.isConnected, !Self.padGateDisabled {
104+
ControllerRequiredView()
105+
.zIndex(60)
106+
.transition(.opacity)
107+
}
108+
#endif
84109
}
85110
.environment(\.novaDebugEnabled, model.settings.uiDebugOverlay)
86111
.environment(\.novaUIScale, model.settings.uiScale) // "Overall UI scale"
@@ -190,3 +215,39 @@ struct RootView: View {
190215
model.commitSettings()
191216
}
192217
}
218+
#if os(tvOS)
219+
/// Full-screen "pair a controller" gate shown on Apple TV until an extended
220+
/// gamepad connects. NovaSwift is a twin-stick game with a cursor-driven UI —
221+
/// the Siri Remote genuinely can't play it, so this is shown up front rather
222+
/// than letting the player wander into an unusable menu.
223+
private struct ControllerRequiredView: View {
224+
var body: some View {
225+
ZStack {
226+
Color.black.opacity(0.92).ignoresSafeArea()
227+
VStack(spacing: 26) {
228+
Image(systemName: "gamecontroller.fill")
229+
.font(.system(size: 96))
230+
.foregroundStyle(LinearGradient(colors: [.white, novaAmber],
231+
startPoint: .top, endPoint: .bottom))
232+
Text("Connect a Game Controller")
233+
.novaFont(.title, weight: .bold, size: 34)
234+
.foregroundStyle(.white)
235+
Text("NovaSwift needs a game controller — an Xbox, PlayStation, or other Bluetooth pad. The Siri Remote can't fly a starship.")
236+
.novaFont(.body, size: 20)
237+
.foregroundStyle(.secondary)
238+
.multilineTextAlignment(.center)
239+
.frame(maxWidth: 760)
240+
.fixedSize(horizontal: false, vertical: true)
241+
Label("Pair one in Settings → Remotes & Devices → Bluetooth", systemImage: "dot.radiowaves.left.and.right")
242+
.novaFont(.caption, size: 17)
243+
.foregroundStyle(novaAmber)
244+
ProgressView()
245+
.tint(novaAmber)
246+
.padding(.top, 6)
247+
}
248+
.padding(60)
249+
}
250+
}
251+
}
252+
#endif
253+

0 commit comments

Comments
 (0)