1mod arrow;
2mod directory;
3mod mount;
4mod node;
5pub use arrow::query::{Query, SearchDescriptionResult};
6pub use mount::{Mount, Root};
8
9use std::cell::RefCell;
10use std::sync::LazyLock;
11
12use anyhow::{Result, anyhow};
13use dynamic::{Dynamic, MsgPack, MsgUnpack, Type};
14use rand::RngExt;
15
16use tokio::sync::mpsc;
17
18pub type Msg<T> = (T, Option<mpsc::Sender<T>>);
19pub type MsgSender<T> = mpsc::Sender<Msg<T>>;
20pub type MsgReceiver<T> = mpsc::Receiver<Msg<T>>;
21
22pub fn tx_rx<T: Send>() -> (MsgSender<T>, MsgReceiver<T>) {
23 let (tx, rx) = mpsc::channel(1024);
24 (tx, rx)
25}
26
27pub fn block_on_async<F, T>(f: F) -> T
28where
29 F: FnOnce() -> std::pin::Pin<Box<dyn Future<Output = T> + Send>> + 'static + Send,
30 T: Send + 'static,
31{
32 if tokio::runtime::Handle::try_current().is_ok() {
33 let (tx, rx) = tokio::sync::oneshot::channel();
34 tokio::task::spawn(async move {
35 let result = f().await;
36 let _ = tx.send(result);
37 });
38 tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(async { rx.await.unwrap() }))
39 } else {
40 let rt = tokio::runtime::Runtime::new().unwrap();
41 rt.block_on(f())
42 }
43}
44
45use smol_str::SmolStr;
46pub fn start_task<F>(info: Dynamic, f: F) -> Dynamic
47where
48 F: FnOnce() -> std::pin::Pin<Box<dyn Future<Output = Result<()>> + Send>> + 'static + Send,
49{
50 let id = uuid::Uuid::new_v4().to_string();
51 let task = SmolStr::new(format!("local/tasks/{}", id));
52 let r = if tokio::runtime::Handle::try_current().is_ok() {
53 Object::Task(tokio::task::spawn(async move { f().await }), info)
54 } else {
55 Object::ThreadTask(
56 std::thread::spawn(move || {
57 let rt = tokio::runtime::Runtime::new().unwrap();
58 rt.block_on(f())
59 }),
60 info,
61 )
62 };
63 let _ = add(&task, r);
64 log::info!("start task {:?}", task);
65 id.into()
66}
67
68#[macro_export]
69macro_rules! sync_await {
70 ($future:expr) => {
71 $crate::block_on_async(move || Box::pin($future))
72 };
73}
74
75pub fn send<T: Send + 'static>(tx: &MsgSender<T>, msg: T) -> Result<()> {
76 tx.try_send((msg, None)).map_err(|e| anyhow!("发送失败: {}", e))
77}
78
79pub fn call<T: Send + 'static>(tx: &MsgSender<T>, msg: T) -> Result<T> {
80 let (reply_tx, mut reply_rx) = mpsc::channel::<T>(1024);
81 tx.try_send((msg, Some(reply_tx))).map_err(|e| anyhow!("发送失败: {}", e))?;
82 reply_rx.try_recv().map_err(|e| anyhow!("接收回复失败: {}", e))
83}
84
85use tokio::task::JoinHandle;
86
87use crate::node::Node;
88#[derive(Debug)]
89pub enum Object {
90 Value(Dynamic), Native(fn(Dynamic) -> Dynamic), Func(i64, Type), Tx(MsgSender<Dynamic>, Dynamic), Task(JoinHandle<Result<()>>, Dynamic), ThreadTask(std::thread::JoinHandle<Result<()>>, Dynamic), }
97
98impl Into<Object> for Dynamic {
99 fn into(self) -> Object {
100 Object::Value(self)
101 }
102}
103
104impl Object {
105 pub fn value(&self) -> Dynamic {
106 match self {
107 Self::Value(v) => v.deep_clone(),
108 Self::Task(_, info) => info.deep_clone(),
109 Self::ThreadTask(_, info) => info.deep_clone(),
110 Self::Tx(_, info) => info.deep_clone(),
111 _ => Dynamic::Null,
112 }
113 }
114}
115
116impl Default for Object {
117 fn default() -> Self {
118 Self::Value(Dynamic::Null)
119 }
120}
121
122impl MsgPack for Object {
123 fn encode(&self, buf: &mut Vec<u8>) {
124 match self {
125 Self::Value(v) => v.encode(buf),
126 _ => {}
127 }
128 }
129}
130
131impl MsgUnpack for Object {
132 fn decode(buf: &[u8]) -> Result<(Self, usize)> {
133 let (v, len) = Dynamic::decode(buf)?;
134 Ok((Self::Value(v), len))
135 }
136}
137
138static ROOT: LazyLock<Root<Object>> = LazyLock::new(|| {
139 let root = Root::<Object>::new();
140 if let Ok((mount, name)) = root.get_mount("local/fight") {
141 mount.add(name, Object::Native(fight));
142 }
143 root
144});
145
146thread_local! {
147 static SEND_STACK: RefCell<Vec<String>> = RefCell::new(Vec::new());
148}
149
150fn builtin_native(name: &str) -> Option<fn(Dynamic) -> Dynamic> {
151 match name {
152 "fight" | "native.fight" | "native::fight" => Some(fight),
153 _ => None,
154 }
155}
156
157fn get_value(input: &Dynamic, keys: &[&str]) -> Option<Dynamic> {
158 keys.iter().find_map(|key| input.get_dynamic(key))
159}
160
161fn get_string(input: &Dynamic, keys: &[&str]) -> Option<String> {
162 get_value(input, keys).map(|value| value.as_str().to_string())
163}
164
165fn get_i64(input: &Dynamic, keys: &[&str]) -> Option<i64> {
166 get_value(input, keys).and_then(|value| value.as_int())
167}
168
169fn get_f64(input: &Dynamic, keys: &[&str]) -> Option<f64> {
170 get_value(input, keys).and_then(|value| value.as_float())
171}
172
173fn get_range(input: &Dynamic, range_keys: &[&str], min_keys: &[&str], max_keys: &[&str], default_min: i64, default_max: i64) -> (i64, i64) {
174 if let Some(range) = get_value(input, range_keys) {
175 let min = range.get_idx(0).and_then(|v| v.as_int()).unwrap_or(default_min);
176 let max = range.get_idx(1).and_then(|v| v.as_int()).unwrap_or(default_max);
177 return normalize_range(min, max, default_min, default_max);
178 }
179
180 let min = get_i64(input, min_keys).unwrap_or(default_min);
181 let max = get_i64(input, max_keys).unwrap_or(default_max);
182 normalize_range(min, max, default_min, default_max)
183}
184
185fn normalize_range(min: i64, max: i64, default_min: i64, default_max: i64) -> (i64, i64) {
186 let min = min.max(0);
187 let max = max.max(0);
188 if min == 0 && max == 0 {
189 (default_min, default_max)
190 } else if min <= max {
191 (min, max)
192 } else {
193 (max, min)
194 }
195}
196
197fn role_name(role: &Dynamic) -> String {
198 get_string(role, &["name", "名称"]).unwrap_or_else(|| "未命名".to_string())
199}
200
201fn role_hp(role: &Dynamic) -> i64 {
202 get_i64(role, &["hp", "health", "生命"]).unwrap_or(0).max(0)
203}
204
205fn role_max_hp(role: &Dynamic) -> i64 {
206 get_i64(role, &["max_hp", "max_health", "最大生命"]).unwrap_or_else(|| role_hp(role)).max(1)
207}
208
209fn role_speed(role: &Dynamic) -> i64 {
210 get_i64(role, &["speed", "initiative", "速度"]).unwrap_or(0)
211}
212
213fn role_attack_range(role: &Dynamic) -> (i64, i64) {
214 get_range(role, &["attack", "攻击"], &["attack_min", "min_attack", "攻击下限"], &["attack_max", "max_attack", "攻击上限"], 8, 15)
215}
216
217fn role_defense_range(role: &Dynamic) -> (i64, i64) {
218 get_range(role, &["defense", "防御"], &["defense_min", "min_defense", "防御下限"], &["defense_max", "max_defense", "防御上限"], 1, 4)
219}
220
221fn role_block_chance(role: &Dynamic) -> f64 {
222 get_f64(role, &["block_chance", "block", "格挡几率"]).unwrap_or(0.15).clamp(0.0, 0.95)
223}
224
225fn role_block_ratio(role: &Dynamic) -> f64 {
226 get_f64(role, &["block_ratio", "格挡减伤"]).unwrap_or(0.5).clamp(0.0, 1.0)
227}
228
229fn role_crit_chance(role: &Dynamic) -> f64 {
230 get_f64(role, &["crit_chance", "crit", "暴击几率"]).unwrap_or(0.1).clamp(0.0, 1.0)
231}
232
233fn role_crit_ratio(role: &Dynamic) -> f64 {
234 get_f64(role, &["crit_ratio", "暴击倍率"]).unwrap_or(1.5).max(1.0)
235}
236
237fn set_i64(role: &Dynamic, key: &str, value: i64) {
238 role.set_dynamic(key.into(), value);
239}
240
241fn set_bool(role: &Dynamic, key: &str, value: bool) {
242 role.set_dynamic(key.into(), value);
243}
244
245fn normalize_role(input: &Dynamic, side: &'static str, default_name: &str) -> Dynamic {
246 let role = if input.is_map() { input.deep_clone() } else { Dynamic::map(Default::default()) };
247 let name = get_string(&role, &["name", "名称"]).unwrap_or_else(|| default_name.to_string());
248 let hp = get_i64(&role, &["hp", "health", "生命"]).unwrap_or(100).max(1);
249 let (attack_min, attack_max) = role_attack_range(&role);
250 let (defense_min, defense_max) = role_defense_range(&role);
251
252 role.set_dynamic("name".into(), name);
253 role.set_dynamic("side".into(), side);
254 set_i64(&role, "hp", hp);
255 set_i64(&role, "max_hp", role_max_hp(&role).max(hp));
256 set_i64(&role, "attack_min", attack_min);
257 set_i64(&role, "attack_max", attack_max);
258 set_i64(&role, "defense_min", defense_min);
259 set_i64(&role, "defense_max", defense_max);
260 role.set_dynamic("block_chance".into(), role_block_chance(&role));
261 role.set_dynamic("block_ratio".into(), role_block_ratio(&role));
262 role.set_dynamic("crit_chance".into(), role_crit_chance(&role));
263 role.set_dynamic("crit_ratio".into(), role_crit_ratio(&role));
264 set_i64(&role, "speed", role_speed(&role));
265 set_bool(&role, "alive", hp > 0);
266 role
267}
268
269fn make_fight_record(entries: [(&str, Dynamic); 10]) -> Dynamic {
270 let mut map = std::collections::BTreeMap::<SmolStr, Dynamic>::new();
271 for (key, value) in entries {
272 map.insert(key.into(), value);
273 }
274 Dynamic::map(map)
275}
276
277fn fight(msg: Dynamic) -> Dynamic {
278 let left_input = msg.get_dynamic("left").or_else(|| msg.get_dynamic("a")).unwrap_or(Dynamic::Null);
279 let right_input = msg.get_dynamic("right").or_else(|| msg.get_dynamic("b")).unwrap_or(Dynamic::Null);
280 if !left_input.is_map() || !right_input.is_map() {
281 let mut error = std::collections::BTreeMap::new();
282 error.insert("error".into(), "fight expects {left: {...}, right: {...}}".into());
283 return Dynamic::map(error);
284 }
285
286 let left = normalize_role(&left_input, "left", "左侧");
287 let right = normalize_role(&right_input, "right", "右侧");
288 let max_rounds = get_i64(&msg, &["max_rounds", "最大回合"]).unwrap_or(50).clamp(1, 500) as usize;
289
290 let mut rng = rand::rng();
291 let mut process = Vec::new();
292 let mut records = Vec::new();
293 let mut left_turn = if role_speed(&left) == role_speed(&right) { rng.random_bool(0.5) } else { role_speed(&left) > role_speed(&right) };
294
295 let mut round_count = 0usize;
296 while role_hp(&left) > 0 && role_hp(&right) > 0 && round_count < max_rounds {
297 round_count += 1;
298 let (attacker, defender) = if left_turn { (&left, &right) } else { (&right, &left) };
299
300 let (attack_min, attack_max) = role_attack_range(attacker);
301 let (defense_min, defense_max) = role_defense_range(defender);
302 let attack_roll = rng.random_range(attack_min..=attack_max);
303 let defense_roll = rng.random_range(defense_min..=defense_max);
304 let blocked = rng.random_bool(role_block_chance(defender));
305 let crit = rng.random_bool(role_crit_chance(attacker));
306
307 let mut damage = (attack_roll - defense_roll).max(1);
308 if crit {
309 damage = ((damage as f64) * role_crit_ratio(attacker)).round() as i64;
310 }
311 if blocked {
312 damage = ((damage as f64) * (1.0 - role_block_ratio(defender))).round() as i64;
313 }
314 damage = damage.max(if blocked { 0 } else { 1 });
315
316 let defender_hp = (role_hp(defender) - damage).max(0);
317 let defender_max_hp = role_max_hp(defender);
318 set_i64(defender, "hp", defender_hp);
319 set_bool(defender, "alive", defender_hp > 0);
320
321 let line = format!(
322 "第{}回合 {} 攻击 {},攻击 {},防御 {},{}{}造成 {} 点伤害,{} 剩余 {} / {}",
323 round_count,
324 role_name(attacker),
325 role_name(defender),
326 attack_roll,
327 defense_roll,
328 if crit { "触发暴击," } else { "" },
329 if blocked { "被格挡后," } else { "" },
330 damage,
331 role_name(defender),
332 defender_hp,
333 defender_max_hp,
334 );
335 process.push(line.clone().into());
336 records.push(make_fight_record([
337 ("round", (round_count as i64).into()),
338 ("attacker", role_name(attacker).into()),
339 ("attacker_side", get_string(attacker, &["side"]).unwrap_or_default().into()),
340 ("defender", role_name(defender).into()),
341 ("defender_side", get_string(defender, &["side"]).unwrap_or_default().into()),
342 ("attack_roll", attack_roll.into()),
343 ("defense_roll", defense_roll.into()),
344 ("blocked", blocked.into()),
345 ("critical", crit.into()),
346 ("damage", damage.into()),
347 ]));
348 if let Some(last) = records.last() {
349 last.insert("defender_hp", defender_hp);
350 last.insert("text", line);
351 }
352
353 left_turn = !left_turn;
354 }
355
356 let left_hp = role_hp(&left);
357 let right_hp = role_hp(&right);
358 let left_name = role_name(&left);
359 let right_name = role_name(&right);
360 let winner = if left_hp == right_hp {
361 "draw".to_string()
362 } else if left_hp > right_hp {
363 left_name.clone()
364 } else {
365 right_name.clone()
366 };
367 let loser = if winner == "draw" {
368 String::new()
369 } else if winner == left_name {
370 right_name
371 } else {
372 left_name
373 };
374
375 let mut result = std::collections::BTreeMap::new();
376 result.insert("winner".into(), winner.into());
377 result.insert("loser".into(), loser.into());
378 result.insert("draw".into(), (left_hp == right_hp).into());
379 result.insert("round_count".into(), (round_count as i64).into());
380 result.insert("process".into(), Dynamic::list(process));
381 result.insert("records".into(), Dynamic::list(records));
382 result.insert("left".into(), left);
383 result.insert("right".into(), right);
384 Dynamic::map(result)
385}
386
387pub fn mount_memory(name: &str) -> bool {
388 ROOT.mount_memory(name)
389}
390
391pub fn mount_redis(name: &str, url: &str) -> Result<bool> {
392 ROOT.mount_redis(name, url)
393}
394
395pub fn mount_fjall(name: &str, data_dir: &str) -> Result<bool> {
396 ROOT.mount_fjall(name, data_dir)
397}
398
399pub fn get_mount<'a>(name: &'a str) -> Result<(Mount<Object>, &'a str)> {
400 ROOT.get_mount(name)
401}
402
403pub fn add(name: &str, obj: Object) -> Result<bool> {
404 let (m, name) = get_mount(name)?;
405 let mut obj = obj;
406 let expire = take_object_expire(&mut obj);
407 let added = m.add(name, obj);
408 if added {
409 apply_redis_expire(&m, name, expire);
410 }
411 Ok(added)
412}
413
414pub fn add_native(name: &str, native_name: &str) -> Result<bool> {
415 let Some(handler) = builtin_native(native_name) else {
416 return Ok(false);
417 };
418 let (m, name) = get_mount(name)?;
419 Ok(m.add(name, Object::Native(handler)))
420}
421
422pub fn add_value<T: Into<Dynamic>>(name: &str, val: T) -> Result<bool> {
423 let (m, name) = get_mount(name)?;
424 let mut value = val.into();
425 let expire = take_dynamic_expire(&mut value);
426 let added = m.add(name, Object::Value(value));
427 if added {
428 apply_redis_expire(&m, name, expire);
429 }
430 Ok(added)
431}
432
433pub fn get(name: &str) -> Result<Dynamic> {
434 let (m, name) = get_mount(name)?;
435 m.get(name, |obj| obj.value())
436}
437
438pub fn dir(name: &str) -> Result<Dynamic> {
439 let (m, name) = get_mount(name)?;
440 m.dir(name).map(Into::into)
441}
442
443pub fn keys(name: &str) -> Result<Dynamic> {
444 let (m, name) = get_mount(name)?;
445 m.keys(name).map(Into::into)
446}
447
448pub fn contains(name: &str) -> bool {
449 if let Ok((m, name)) = get_mount(name) { m.contains(name) } else { false }
450}
451
452pub fn remove(name: &str) -> Result<Dynamic> {
453 let (m, name) = get_mount(name)?;
454 match m.remove(name) {
455 Ok(Object::Value(v)) => Ok(v),
456 _ => Err(anyhow!("没有删除对象")),
457 }
458}
459
460pub fn add_list(name: &str) -> Result<()> {
461 let (m, name) = get_mount(name)?;
462 m.add_list(name);
463 Ok(())
464}
465
466pub fn push(name: &str, value: Dynamic) -> Result<usize> {
467 let (m, name) = get_mount(name)?;
468 let mut value = value;
469 let expire = take_dynamic_expire(&mut value);
470 let len = m.push(name, Object::Value(value))?;
471 apply_redis_expire(&m, name, expire);
472 Ok(len)
473}
474
475pub fn add_map(name: &str) -> Result<()> {
476 let (m, name) = get_mount(name)?;
477 m.add_map(name);
478 Ok(())
479}
480
481pub fn insert(name: &str, key: &str, value: Dynamic) -> Result<()> {
482 let (m, name) = get_mount(name)?;
483 let mut value = value;
484 let expire = take_dynamic_expire(&mut value);
485 let _ = m.insert(name, key, Object::Value(value));
486 apply_redis_expire(&m, name, expire);
487 Ok(())
488}
489
490fn take_object_expire(obj: &mut Object) -> Option<i64> {
491 if let Object::Value(value) = obj { take_dynamic_expire(value) } else { None }
492}
493
494fn take_dynamic_expire(value: &mut Dynamic) -> Option<i64> {
495 value.remove_dynamic("@expire").and_then(|expire| expire.as_int()).filter(|expire| *expire > 0)
496}
497
498fn apply_redis_expire(mount: &Mount<Object>, name: &str, expire: Option<i64>) {
499 let Some(expire) = expire else {
500 return;
501 };
502 if let Mount::Redis { client, rl: _ } = mount
503 && let Ok(mut conn) = client.get_connection()
504 {
505 let _ = conn.expire::<&str, ()>(name, expire);
506 }
507}
508
509pub fn get_key(name: &str, key: &str) -> Result<Dynamic> {
510 let (m, name) = get_mount(name)?;
511 m.get_key(name, key, |obj| obj.value())
512}
513
514pub fn update<F>(name: &str, mut f: F) -> Result<Dynamic>
517where
518 F: FnMut(Dynamic) -> Dynamic + Send + 'static,
519{
520 let (m, name) = get_mount(name)?;
521 m.get_mut(name, move |obj| match obj {
522 Object::Value(v) => {
523 let current = std::mem::take(v);
524 let next = f(current);
525 let result = next.deep_clone();
526 *v = next;
527 result
528 }
529 _ => Dynamic::Null,
530 })
531}
532
533pub fn update_key<F>(name: &str, key: &str, mut f: F) -> Result<Dynamic>
535where
536 F: FnMut(Dynamic) -> Dynamic + Send + 'static,
537{
538 let (m, name) = get_mount(name)?;
539 m.get_key_mut(name, key, move |obj| match obj {
540 Object::Value(v) => {
541 let current = std::mem::take(v);
542 let next = f(current);
543 let result = next.deep_clone();
544 *v = next;
545 result
546 }
547 _ => Dynamic::Null,
548 })
549}
550
551use redis::Commands;
552pub fn get_list(name: &str) -> Result<Vec<Dynamic>> {
553 let (m, name) = get_mount(name)?;
554 match m {
555 Mount::Memory(m) => m
556 .read_sync(name, |_, v| match v {
557 Node::List(l) => l.iter().map(|(_, item)| item.value()).collect(),
558 _ => Vec::new(),
559 })
560 .ok_or(anyhow!("未发现 {}", name)),
561 Mount::Redis { client, rl: _ } => {
562 let mut conn = client.get_connection()?;
563 let items: Vec<Vec<u8>> = conn.lrange(name, 0, -1)?;
564 let items: Vec<Dynamic> = items.into_iter().map(|buf| Dynamic::decode(buf.as_slice()).map(|(v, _)| v).unwrap_or(Dynamic::Null)).collect();
565 Ok(items)
566 }
567 Mount::Fjall { .. } => {
568 let len = m.len(name)?;
569 let mut items = Vec::new();
570 for idx in 0..len {
571 if let Ok(value) = m.get_idx(name, idx, |obj| obj.value()) {
572 items.push(value);
573 }
574 }
575 Ok(items)
576 }
577 }
578}
579
580#[derive(Debug)]
581enum MyFn {
582 Null,
583 Sender(MsgSender<Dynamic>),
584 Native(fn(Dynamic) -> Dynamic),
585 Script(i64, Type),
586}
587
588impl MyFn {
589 fn call(&self, msg: Dynamic) -> Result<Dynamic> {
590 match self {
591 MyFn::Sender(tx) => call(tx, msg),
592 MyFn::Native(f) => Ok(f(msg)),
593 MyFn::Script(ptr, ty) => dynamic::call_fn(*ptr, ty.clone(), Box::new(msg)).map(|r| r.as_ref().clone()),
594 MyFn::Null => Ok(Dynamic::Null),
595 }
596 }
597}
598
599impl From<&Object> for MyFn {
600 fn from(obj: &Object) -> Self {
601 match obj {
602 Object::Tx(tx, _) => MyFn::Sender(tx.clone()),
603 Object::Task(t, _) => {
604 t.abort();
605 MyFn::Null
606 }
607 Object::Func(ptr, ty) => MyFn::Script(*ptr, ty.clone()),
608 Object::Native(f) => MyFn::Native(*f),
609 _ => MyFn::Null,
610 }
611 }
612}
613
614pub fn send_msg(name: &str, msg: Dynamic) -> Result<Dynamic> {
615 let entered = SEND_STACK.with(|stack| {
616 let mut stack = stack.borrow_mut();
617 if stack.len() >= 64 {
618 return Err(anyhow!("root::send call depth exceeded at {}", name));
619 }
620 if stack.iter().any(|item| item == name) {
621 return Err(anyhow!("root::send recursive path detected: {}", name));
622 }
623 stack.push(name.to_string());
624 Ok(())
625 });
626 entered?;
627 let result = (|| {
628 let (m, name) = get_mount(name)?;
629 let f: MyFn = m.get(name, |obj| obj.into())?;
630 f.call(msg)
631 })();
632 SEND_STACK.with(|stack| {
633 let _ = stack.borrow_mut().pop();
634 });
635 result
636}
637
638pub fn send_idx_msg(name: &str, idx: usize, msg: Dynamic) -> Result<Dynamic> {
639 let stack_name = format!("{}[{}]", name, idx);
640 let entered = SEND_STACK.with(|stack| {
641 let mut stack = stack.borrow_mut();
642 if stack.len() >= 64 {
643 return Err(anyhow!("root::send_idx call depth exceeded at {}", stack_name));
644 }
645 if stack.iter().any(|item| item == &stack_name) {
646 return Err(anyhow!("root::send_idx recursive path detected: {}", stack_name));
647 }
648 stack.push(stack_name);
649 Ok(())
650 });
651 entered?;
652 let result = (|| {
653 let (m, name) = get_mount(name)?;
654 let f: MyFn = m.get_idx(name, idx, |obj| obj.into())?;
655 f.call(msg)
656 })();
657 SEND_STACK.with(|stack| {
658 let _ = stack.borrow_mut().pop();
659 });
660 result
661}
662
663#[cfg(test)]
664mod tests {
665 use super::*;
666
667 fn fight_request() -> Dynamic {
668 let left = Dynamic::map(Default::default());
669 left.insert("name", "战士");
670 left.insert("hp", 30);
671 left.insert("attack_min", 8);
672 left.insert("attack_max", 12);
673 left.insert("defense_min", 1);
674 left.insert("defense_max", 3);
675 left.insert("block_chance", 0.2);
676
677 let right = Dynamic::map(Default::default());
678 right.insert("name", "盗贼");
679 right.insert("hp", 24);
680 right.insert("attack_min", 6);
681 right.insert("attack_max", 10);
682 right.insert("defense_min", 0);
683 right.insert("defense_max", 2);
684 right.insert("block_chance", 0.1);
685
686 let msg = Dynamic::map(Default::default());
687 msg.insert("left", left);
688 msg.insert("right", right);
689 msg
690 }
691
692 #[test]
693 fn add_native_registers_builtin_fight_handler() {
694 assert!(add_native("local/test/fight_handler", "native.fight").unwrap());
695 let result = send_msg("local/test/fight_handler", fight_request()).unwrap();
696 assert!(result.is_map());
697 assert!(result.get_dynamic("winner").is_some());
698 assert!(result.get_dynamic("process").is_some_and(|v| v.is_list()));
699 }
700
701 #[test]
702 fn local_fight_handler_is_available_by_default() {
703 assert!(contains("local/fight"));
704 let result = send_msg("local/fight", fight_request()).unwrap();
705 let rounds = result.get_dynamic("round_count").and_then(|v| v.as_int()).unwrap_or_default();
706 assert!(rounds > 0);
707 }
708
709 #[test]
710 fn take_dynamic_expire_strips_positive_expire_metadata() {
711 let mut value = dynamic::map!("@expire"=> 30, "name"=> "zust");
712
713 assert_eq!(take_dynamic_expire(&mut value), Some(30));
714 assert!(!value.contains("@expire"));
715 assert_eq!(value.get_dynamic("name").unwrap().as_str(), "zust");
716 }
717
718 #[test]
719 fn take_dynamic_expire_ignores_non_positive_expire_metadata() {
720 let mut value = dynamic::map!("@expire"=> 0, "name"=> "zust");
721
722 assert_eq!(take_dynamic_expire(&mut value), None);
723 assert!(!value.contains("@expire"));
724 }
725
726 #[test]
727 fn update_increments_value_atomically() {
728 let path = format!("local/test/update_inc/{}", uuid::Uuid::new_v4());
729 add_value(&path, 0_i64).unwrap();
730
731 let next = update(&path, |v| v + Dynamic::I64(1)).unwrap();
732 assert_eq!(next.as_int(), Some(1));
733 assert_eq!(get(&path).unwrap().as_int(), Some(1));
734
735 let next = update(&path, |v| v + Dynamic::I64(41)).unwrap();
736 assert_eq!(next.as_int(), Some(42));
737 assert_eq!(get(&path).unwrap().as_int(), Some(42));
738 }
739
740 #[test]
741 fn update_concurrent_threads_no_lost_update() {
742 let path = format!("local/test/update_concurrent/{}", uuid::Uuid::new_v4());
743 add_value(&path, 0_i64).unwrap();
744
745 const THREADS: i64 = 8;
746 const ITERS: i64 = 1000;
747
748 let mut handles = Vec::new();
749 for _ in 0..THREADS {
750 let p = path.clone();
751 handles.push(std::thread::spawn(move || {
752 for _ in 0..ITERS {
753 update(&p, |v| v + Dynamic::I64(1)).unwrap();
754 }
755 }));
756 }
757 for h in handles {
758 h.join().unwrap();
759 }
760
761 assert_eq!(get(&path).unwrap().as_int(), Some(THREADS * ITERS));
762 }
763
764 #[test]
765 fn update_key_increments_map_value() {
766 let path = format!("local/test/update_key/{}", uuid::Uuid::new_v4());
767 add_map(&path).unwrap();
768 insert(&path, "n", 0_i64.into()).unwrap();
769
770 const THREADS: i64 = 8;
771 const ITERS: i64 = 500;
772
773 let mut handles = Vec::new();
774 for _ in 0..THREADS {
775 let p = path.clone();
776 handles.push(std::thread::spawn(move || {
777 for _ in 0..ITERS {
778 update_key(&p, "n", |v| v + Dynamic::I64(1)).unwrap();
779 }
780 }));
781 }
782 for h in handles {
783 h.join().unwrap();
784 }
785
786 assert_eq!(get_key(&path, "n").unwrap().as_int(), Some(THREADS * ITERS));
787 }
788
789 #[test]
790 fn keys_returns_memory_map_keys() {
791 let path = format!("local/test/keys/{}", uuid::Uuid::new_v4());
792 add_map(&path).unwrap();
793 insert(&path, "0", "zero".into()).unwrap();
794 insert(&path, "1", "one".into()).unwrap();
795
796 let key_list = keys(&path).unwrap();
797 let mut keys = (0..key_list.len()).map(|idx| key_list.get_idx(idx).unwrap().as_str().to_string()).collect::<Vec<_>>();
798 keys.sort();
799 assert_eq!(keys, vec!["0".to_string(), "1".to_string()]);
800 }
801
802 #[test]
803 fn dir_returns_immediate_child_names() {
804 let path = format!("local/test/public_dir/{}", uuid::Uuid::new_v4());
805 add_value(&format!("{path}/a"), 1_i64).unwrap();
806 add_value(&format!("{path}/sub/item"), 2_i64).unwrap();
807
808 let entries = dir(&path).unwrap();
809 let mut entries = (0..entries.len()).map(|idx| entries.get_idx(idx).unwrap().as_str().to_string()).collect::<Vec<_>>();
810 entries.sort();
811 assert_eq!(entries, vec!["a".to_string(), "sub".to_string()]);
812 }
813
814 #[test]
815 fn mount_fjall_uses_explicit_mount_name() {
816 let data_dir = std::env::temp_dir().join(format!("zust-root-fjall-named-{}", uuid::Uuid::new_v4()));
817 let data_dir_str = data_dir.to_str().unwrap();
818 let mount_name = format!("fjall_{}", uuid::Uuid::new_v4().simple());
819
820 assert!(mount_fjall(&mount_name, data_dir_str).unwrap());
821 add_value(&format!("{mount_name}/item"), 9_i64).unwrap();
822 assert_eq!(get(&format!("{mount_name}/item")).unwrap().as_int(), Some(9));
823 assert!(!contains("fjall/item"));
824
825 let _ = std::fs::remove_dir_all(data_dir);
826 }
827
828 #[test]
829 fn update_returns_err_when_missing() {
830 let path = format!("local/test/update_missing/{}", uuid::Uuid::new_v4());
831 let result = update(&path, |v| v + Dynamic::I64(1));
832 assert!(result.is_err());
833 }
834
835 #[test]
836 fn root_get_returns_independent_dynamic_values() {
837 let path = format!("local/test/root_get_clone/{}", uuid::Uuid::new_v4());
838 let nested = dynamic::map!("score"=> 1);
839 let value = dynamic::map!("nested"=> nested.clone(), "items"=> Dynamic::list(vec![nested]));
840
841 add_value(&path, value).unwrap();
842
843 let first = get(&path).unwrap();
844 first.get_dynamic("nested").unwrap().insert("score", 2);
845 first.get_dynamic("items").unwrap().get_idx(0).unwrap().insert("score", 3);
846
847 let second = get(&path).unwrap();
848 assert_eq!(second.get_dynamic("nested").unwrap().get_dynamic("score").and_then(|v| v.as_int()), Some(1));
849 assert_eq!(second.get_dynamic("items").unwrap().get_idx(0).unwrap().get_dynamic("score").and_then(|v| v.as_int()), Some(1));
850 }
851}