1use pyo3::prelude::*;
9use std::collections::VecDeque;
10use std::sync::{Arc, Mutex};
11
12#[pyclass(name = "AudioEngine", unsendable)]
15struct PyAudioEngine {
16 inner: ling_audio::AudioEngine,
17}
18
19#[pymethods]
20impl PyAudioEngine {
21 #[new]
22 fn new() -> PyResult<Self> {
23 ling_audio::AudioEngine::new()
24 .map(|e| PyAudioEngine { inner: e })
25 .map_err(|e| {
26 pyo3::exceptions::PyRuntimeError::new_err(format!("audio init: {e}"))
27 })
28 }
29
30 #[pyo3(signature = (
35 idx,
36 x=0.0, y=0.0, z=0.0, w=1.0,
37 freq=220.0, amp=0.15,
38 lfo_rate=0.5, lfo_depth=0.02
39 ))]
40 fn set_tone(
41 &self,
42 idx: usize,
43 x: f32, y: f32, z: f32, w: f32,
44 freq: f32, amp: f32,
45 lfo_rate: f32, lfo_depth: f32,
46 ) {
47 self.inner.set_tone(idx, ling_audio::ToneParams {
48 x, y, z, w, freq, amp, lfo_rate, lfo_depth,
49 });
50 }
51
52 fn clear_tone(&self, idx: usize) {
53 self.inner.clear_tone(idx);
54 }
55
56 fn set_listener(&self, cry: f32, sry: f32, crx: f32, srx: f32) {
58 self.inner.set_listener(cry, sry, crx, srx);
59 }
60
61 fn load_bgm(&self, path: &str, vol: f32) {
63 self.inner.load_bgm(path, vol);
64 }
65
66 fn set_bgm_volume(&self, vol: f32) {
67 self.inner.set_bgm_volume(vol);
68 }
69
70 fn set_master_volume(&self, vol: f32) {
71 self.inner.set_master_volume(vol);
72 }
73}
74
75use ling_physics::hyperbolic::HyperbolicSphereWorld;
78use glam::Vec3;
79
80#[pyclass(name = "HyperbolicWorld")]
81struct PyHyperbolicWorld {
82 inner: HyperbolicSphereWorld,
83}
84
85#[pymethods]
86impl PyHyperbolicWorld {
87 #[new]
88 #[pyo3(signature = (radius=100.0, curvature=-1.0, gravity=9.81))]
89 fn new(radius: f32, curvature: f32, gravity: f32) -> Self {
90 PyHyperbolicWorld {
91 inner: HyperbolicSphereWorld { radius, curvature, gravity },
92 }
93 }
94
95 fn gravity_dir(&self, x: f32, y: f32, z: f32) -> (f32, f32, f32) {
97 let v = self.inner.gravity_dir(Vec3::new(x, y, z));
98 (v.x, v.y, v.z)
99 }
100
101 fn gravity_force(&self, x: f32, y: f32, z: f32, mass: f32) -> (f32, f32, f32) {
103 let v = self.inner.gravity_force(Vec3::new(x, y, z), mass);
104 (v.x, v.y, v.z)
105 }
106
107 fn up_at(&self, x: f32, y: f32, z: f32) -> (f32, f32, f32) {
109 let v = self.inner.up_at(Vec3::new(x, y, z));
110 (v.x, v.y, v.z)
111 }
112
113 fn world_distance(
115 &self,
116 ax: f32, ay: f32, az: f32,
117 bx: f32, by: f32, bz: f32,
118 ) -> f32 {
119 self.inner.world_distance(Vec3::new(ax, ay, az), Vec3::new(bx, by, bz))
120 }
121
122 fn to_poincare(&self, x: f32, y: f32, z: f32) -> (f32, f32, f32) {
124 let v = self.inner.to_poincare(Vec3::new(x, y, z));
125 (v.x, v.y, v.z)
126 }
127
128 fn from_poincare(&self, x: f32, y: f32, z: f32) -> (f32, f32, f32) {
130 let v = self.inner.from_poincare(Vec3::new(x, y, z));
131 (v.x, v.y, v.z)
132 }
133}
134
135#[pyfunction]
137fn hyp_distance(ax: f32, ay: f32, az: f32, bx: f32, by: f32, bz: f32) -> f32 {
138 ling_physics::hyperbolic::distance(Vec3::new(ax, ay, az), Vec3::new(bx, by, bz))
139}
140
141#[pyfunction]
142fn exp_map(
143 bx: f32, by: f32, bz: f32,
144 vx: f32, vy: f32, vz: f32,
145) -> (f32, f32, f32) {
146 let v = ling_physics::hyperbolic::exp_map(Vec3::new(bx, by, bz), Vec3::new(vx, vy, vz));
147 (v.x, v.y, v.z)
148}
149
150#[pyfunction]
151fn log_map(
152 bx: f32, by: f32, bz: f32,
153 tx: f32, ty: f32, tz: f32,
154) -> (f32, f32, f32) {
155 let v = ling_physics::hyperbolic::log_map(Vec3::new(bx, by, bz), Vec3::new(tx, ty, tz));
156 (v.x, v.y, v.z)
157}
158
159#[pyclass(name = "NetClient")]
169struct PyNetClient {
170 outbox: Arc<Mutex<Vec<String>>>,
171 inbox: Arc<Mutex<VecDeque<String>>>,
172 connected: Arc<std::sync::atomic::AtomicBool>,
173}
174
175#[pymethods]
176impl PyNetClient {
177 #[new]
178 fn new() -> Self {
179 PyNetClient {
180 outbox: Arc::new(Mutex::new(Vec::new())),
181 inbox: Arc::new(Mutex::new(VecDeque::new())),
182 connected: Arc::new(std::sync::atomic::AtomicBool::new(false)),
183 }
184 }
185
186 fn connect(&self, url: String) {
188 let outbox = Arc::clone(&self.outbox);
189 let inbox = Arc::clone(&self.inbox);
190 let connected = Arc::clone(&self.connected);
191 std::thread::spawn(move || {
192 let rt = tokio::runtime::Builder::new_current_thread()
193 .enable_all()
194 .build()
195 .expect("tokio runtime build");
196 rt.block_on(ws_run(url, outbox, inbox, connected));
197 });
198 }
199
200 fn send(&self, msg: String) {
202 if let Ok(mut ob) = self.outbox.lock() {
203 ob.push(msg);
204 }
205 }
206
207 fn try_recv(&self) -> Option<String> {
209 self.inbox.lock().ok()?.pop_front()
210 }
211
212 fn is_connected(&self) -> bool {
213 self.connected.load(std::sync::atomic::Ordering::Relaxed)
214 }
215}
216
217async fn ws_run(
218 url: String,
219 outbox: Arc<Mutex<Vec<String>>>,
220 inbox: Arc<Mutex<VecDeque<String>>>,
221 connected: Arc<std::sync::atomic::AtomicBool>,
222) {
223 use futures_util::{SinkExt, StreamExt};
224 use tokio::time::{sleep, Duration};
225 use tokio_tungstenite::{connect_async, tungstenite::Message};
226
227 let ws = match connect_async(&url).await {
228 Ok((ws, _)) => ws,
229 Err(e) => { eprintln!("[ling-py netplay] connect failed ({url}): {e}"); return; }
230 };
231 connected.store(true, std::sync::atomic::Ordering::Relaxed);
232 eprintln!("[ling-py netplay] connected to {url}");
233
234 let (mut write, mut read) = ws.split();
235
236 loop {
237 let msgs: Vec<String> = if let Ok(mut ob) = outbox.lock() {
239 std::mem::take(&mut *ob)
240 } else {
241 vec![]
242 };
243 for m in msgs {
244 if write.send(Message::Text(m)).await.is_err() { break; }
245 }
246
247 tokio::select! {
249 frame = read.next() => {
250 match frame {
251 Some(Ok(Message::Text(s))) => {
252 if let Ok(mut ib) = inbox.lock() {
253 ib.push_back(s.to_string());
254 while ib.len() > 128 { ib.pop_front(); }
255 }
256 }
257 Some(Ok(_)) => {} _ => break, }
260 }
261 _ = sleep(Duration::from_millis(1)) => {}
262 }
263 }
264
265 connected.store(false, std::sync::atomic::Ordering::Relaxed);
266 eprintln!("[ling-py netplay] disconnected from {url}");
267}
268
269#[pymodule]
272fn ling_py(m: &Bound<'_, PyModule>) -> PyResult<()> {
273 m.add_class::<PyAudioEngine>()?;
274 m.add_class::<PyHyperbolicWorld>()?;
275 m.add_class::<PyNetClient>()?;
276
277 m.add_function(wrap_pyfunction!(hyp_distance, m)?)?;
278 m.add_function(wrap_pyfunction!(exp_map, m)?)?;
279 m.add_function(wrap_pyfunction!(log_map, m)?)?;
280
281 Ok(())
282}