返回 CodeWhale
PetNativeCore.swift
根目录 / pet / swift / PetNativeCore.swift
1 import Foundation
2 import JavaScriptCore
3
4 public struct PetVoice: Codable {
5 public let id: String, start: Double, duration: Double, frequency: Double, gain: Double, pan: Double, kind: String
6 }
7 public struct PetPeer: Decodable {
8 public let id: String, slot: Int, phase: Double, present: Bool
9 }
10 public struct PetFood: Decodable { public let x: Double, y: Double, life: Double }
11 public struct PetWorldFrame: Decodable {
12 public let timeMs: Double, behaviour: String, state: PetState, needs: String
13 public let pod: [PetPeer], surface: Double, caustic: Double, food: PetFood?
14 public let voices: [PetVoice], digest: String
15 }
16 public enum PetCoreError: Error, LocalizedError {
17 case invalid(String)
18 public var errorDescription: String? { switch self { case .invalid(let s): return s } }
19 }
20
21 /// JavaScriptCore runs the same event-independent world and score bundle as web.
22 /// Swift owns rasterization and audio output. No native telemetry reclassification.
23 @MainActor public final class PetNativeCore {
24 public let sim: PetSim
25 public private(set) var frame: PetWorldFrame
26 private let context: JSContext
27 private let world: JSValue
28 private var failure: String?
29
30 public init(points: [(Double, Double)], bundle: URL, tape: String = "", interactions: String = "[]", live: Bool = false, saved: Data? = nil, expressionVersion: Int = 2) throws {
31 guard let context = JSContext() else { throw PetCoreError.invalid("Unable to create the pet runtime.") }
32 self.context = context
33 let script = try String(contentsOf: bundle, encoding: .utf8)
34 context.evaluateScript(script)
35 if let error = context.exception { throw PetCoreError.invalid(error.toString()) }
36 let pointData = try JSONSerialization.data(withJSONObject: points.map { [$0.0, $0.1] })
37 guard let constructor = context.objectForKeyedSubscript("PetNative"),
38 let world = constructor.construct(withArguments: [String(decoding: pointData, as: UTF8.self), tape, interactions, live, expressionVersion]), context.exception == nil
39 else { throw PetCoreError.invalid(context.exception?.toString() ?? "Invalid pet recording.") }
40 if let saved {
41 guard saved.count <= 8 * 1024 * 1024, let text = String(data: saved, encoding: .utf8) else { throw PetCoreError.invalid("Invalid pet habitat file.") }
42 world.invokeMethod("restoreRecording", withArguments: [text])
43 if let error = context.exception { throw PetCoreError.invalid(error.toString()) }
44 if live {
45 world.invokeMethod("resumeEngine", withArguments: [])
46 if let error = context.exception { throw PetCoreError.invalid(error.toString()) }
47 }
48 }
49 guard let text = world.invokeMethod("snapshot", withArguments: [])?.toString(), context.exception == nil
50 else { throw PetCoreError.invalid(context.exception?.toString() ?? "Invalid pet recording.") }
51 self.world = world
52 self.frame = try JSONDecoder().decode(PetWorldFrame.self, from: Data(text.utf8))
53 self.sim = PetSim(points: points, expressionVersion: expressionVersion)
54 if saved != nil {
55 try restoreProjection()
56 }
57 context.exceptionHandler = { [weak self] _, error in self?.failure = error?.toString() ?? "Pet runtime failed." }
58 }
59
60 @discardableResult public func tick(motion: Bool) throws -> PetWorldFrame {
61 failure = nil
62 guard let text = world.invokeMethod("step", withArguments: [1.0 / 30, motion])?.toString(), failure == nil
63 else { throw PetCoreError.invalid(failure ?? "Unable to advance the pet.") }
64 frame = try JSONDecoder().decode(PetWorldFrame.self, from: Data(text.utf8))
65 let peers = frame.pod.filter { $0.present }
66 let slots = peers.count >= 3 ? peers.map { ([0, 2, 4, 1, 3, 5][$0.slot], $0.phase) } : nil
67 sim.step(dt: 1.0 / 30, state: frame.state, motion: motion, podSlots: slots)
68 return frame
69 }
70 public func interact(food: Bool, x: Double = 0.2, y: Double = -0.15) throws {
71 failure = nil
72 world.invokeMethod("interact", withArguments: [food ? "food" : "attention", x, y])
73 if let failure { throw PetCoreError.invalid(failure) }
74 }
75 public func interactions() throws -> String {
76 failure = nil
77 let value = world.invokeMethod("interactions", withArguments: [])?.toString()
78 guard let value, failure == nil else { throw PetCoreError.invalid(failure ?? "Unable to save pet interactions.") }
79 return value
80 }
81 public func accept(packet: String) throws {
82 failure = nil
83 world.invokeMethod("accept", withArguments: [packet])
84 if let failure { throw PetCoreError.invalid(failure) }
85 }
86 @discardableResult public func acceptLiveTail(_ text: String) throws -> Bool {
87 failure = nil
88 guard let value = world.invokeMethod("acceptLiveTail", withArguments: [text]), failure == nil else { throw PetCoreError.invalid(failure ?? "Unable to read local telemetry.") }
89 return value.toBool()
90 }
91 public func resumeLiveInput() throws {
92 failure = nil
93 world.invokeMethod("resetLiveInput", withArguments: [])
94 guard failure == nil, let text = world.invokeMethod("snapshot", withArguments: [])?.toString(), failure == nil else { throw PetCoreError.invalid(failure ?? "Unable to resume live telemetry.") }
95 frame = try JSONDecoder().decode(PetWorldFrame.self, from: Data(text.utf8))
96 try restoreProjection()
97 }
98 private func restoreProjection() throws {
99 struct Projection: Decodable { let sim: PetParticleCheckpoint }
100 guard let text = world.invokeMethod("checkpoint", withArguments: [])?.toString(), context.exception == nil
101 else { throw PetCoreError.invalid(context.exception?.toString() ?? "Unable to restore the particle renderer.") }
102 try sim.restoreValidated(JSONDecoder().decode(Projection.self, from: Data(text.utf8)).sim)
103 guard petDigest(sim) == frame.digest else { throw PetCoreError.invalid("The restored particle renderer differs from the shared world.") }
104 }
105 public func recording(checkpoint: Bool = false) throws -> Data {
106 failure = nil
107 guard let value = world.invokeMethod("recording", withArguments: [checkpoint])?.toString(), failure == nil else {
108 throw PetCoreError.invalid(failure ?? "Unable to save the pet recording.")
109 }
110 return Data(value.utf8)
111 }
112 public func prepareSegment() throws -> Data? {
113 failure = nil
114 guard let needed = world.invokeMethod("needsSegment", withArguments: []), failure == nil else { throw PetCoreError.invalid(failure ?? "Unable to inspect recording history.") }
115 if !needed.toBool() { return nil }
116 guard let text = world.invokeMethod("prepareSegment", withArguments: [])?.toString(), failure == nil else { throw PetCoreError.invalid(failure ?? "Unable to prepare recording history.") }
117 let data = Data(text.utf8)
118 guard data.count <= 8 * 1024 * 1024 else { throw PetCoreError.invalid("The active recording exceeds 8 MiB.") }
119 return data
120 }
121 public func commitSegment() throws {
122 failure = nil; world.invokeMethod("commitSegment", withArguments: [])
123 if let failure { throw PetCoreError.invalid(failure) }
124 }
125 /// The host owns the world for this entire export. The private autosave and
126 /// native import remain bounded to 8 MiB; larger exports open in the browser.
127 public func exportRecording(completed: Bool = false) throws -> Data {
128 var data = Data(), index = 0
129 while true {
130 failure = nil
131 guard let value = world.invokeMethod("recordingChunk", withArguments: [index, completed]), failure == nil else {
132 throw PetCoreError.invalid(failure ?? "Unable to export the pet recording.")
133 }
134 if value.isNull { return data }
135 guard value.isString, let text = value.toString() else { throw PetCoreError.invalid("Invalid pet export chunk.") }
136 let bytes = Data(text.utf8)
137 guard data.count + bytes.count <= 64 * 1024 * 1024 else { throw PetCoreError.invalid("Recording exceeds the 64 MiB export limit. The current world was kept.") }
138 data.append(bytes); index += 1
139 }
140 }
141 public func pcm(voice: PetVoice, startSample: Int, length: Int, rate: Int) throws -> [[Float]] {
142 let voices = String(decoding: try JSONEncoder().encode([voice]), as: UTF8.self)
143 failure = nil
144 guard let channels = world.invokeMethod("pcmChannels", withArguments: [voices, startSample, length, rate]), failure == nil,
145 channels.isArray, channels.forProperty("length")?.toInt32() == 2
146 else { throw PetCoreError.invalid(failure ?? "Unable to render pet audio.") }
147 return try (0..<2).map { index in
148 guard let channel = channels.atIndex(index) else { throw PetCoreError.invalid("Missing pet audio channel.") }
149 let ctx = context.jsGlobalContextRef
150 var exception: JSValueRef?
151 guard JSValueGetTypedArrayType(ctx, channel.jsValueRef, &exception) == kJSTypedArrayTypeFloat32Array,
152 exception == nil, let object = JSValueToObject(ctx, channel.jsValueRef, &exception), exception == nil,
153 JSObjectGetTypedArrayLength(ctx, object, &exception) == length, exception == nil
154 else { throw PetCoreError.invalid("Invalid pet PCM buffer.") }
155 if length == 0 { return [] }
156 guard let bytes = JSObjectGetTypedArrayBytesPtr(ctx, object, &exception), exception == nil
157 else { throw PetCoreError.invalid("Pet PCM buffer is unavailable.") }
158 // JavaScriptCore guarantees this pointer only until its next API
159 // call. Copy immediately; no JS-backed memory escapes the method.
160 let samples = withExtendedLifetime(channel) {
161 Array(UnsafeBufferPointer(start: bytes.assumingMemoryBound(to: Float.self), count: length))
162 }
163 guard samples.allSatisfy(\.isFinite) else { throw PetCoreError.invalid("Invalid pet PCM samples.") }
164 return samples
165 }
166 }
167 }
168
168 lines Plain Text