checkpoint

This commit is contained in:
John Smith 2023-06-07 21:55:23 -04:00
parent 317f036598
commit 59c14f3b22
8 changed files with 340 additions and 286 deletions

4
Cargo.lock generated
View File

@ -6160,6 +6160,7 @@ dependencies = [
"cursive_table_view",
"directories",
"flexi_logger",
"flume",
"futures",
"hex",
"json",
@ -6168,10 +6169,11 @@ dependencies = [
"serde",
"serde_derive",
"serial_test",
"stop-token",
"thiserror",
"tokio 1.28.2",
"tokio-util",
"veilid-core",
"veilid-tools",
]
[[package]]

View File

@ -12,8 +12,8 @@ path = "src/main.rs"
[features]
default = [ "rt-tokio" ]
macos = [ "cursive/ncurses-backend" ]
rt-async-std = [ "async-std", "veilid-core/rt-async-std", "cursive/rt-async-std" ]
rt-tokio = [ "tokio", "tokio-util", "veilid-core/rt-tokio", "cursive/rt-tokio" ]
rt-async-std = [ "async-std", "veilid-tools/rt-async-std", "cursive/rt-async-std" ]
rt-tokio = [ "tokio", "tokio-util", "veilid-tools/rt-tokio", "cursive/rt-tokio" ]
[dependencies]
async-std = { version = "^1.9", features = ["unstable", "attributes"], optional = true }
@ -41,8 +41,10 @@ flexi_logger = { version = "^0", features = ["use_chrono_for_offset"] }
thiserror = "^1"
crossbeam-channel = "^0"
hex = "^0"
veilid-core = { path = "../veilid-core" }
veilid-tools = { path = "../veilid-tools", default-features = false }
json = "^0"
stop-token = { version = "^0", default-features = false }
flume = { version = "^0", features = ["async"] }
[dev-dependencies]
serial_test = "^0"

View File

@ -1,94 +1,66 @@
use crate::command_processor::*;
use crate::tools::*;
use futures::future::FutureExt;
use serde::de::DeserializeOwned;
use std::cell::RefCell;
use std::net::SocketAddr;
use std::rc::Rc;
use veilid_core::tools::*;
use veilid_core::*;
use stop_token::{future::FutureExt as _, StopSource, StopToken};
fn map_to_internal_error<T: ToString>(e: T) -> VeilidAPIError {
VeilidAPIError::Internal {
message: e.to_string(),
use veilid_tools::*;
cfg_if! {
if #[cfg(feature="rt-async-std")] {
use async_std::io::prelude::BufReadExt;
use async_std::io::WriteExt;
use async_std::io::BufReader;
} else if #[cfg(feature="rt-tokio")] {
use tokio::io::AsyncBufReadExt;
use tokio::io::AsyncWriteExt;
use tokio::io::BufReader;
}
}
fn decode_api_result<T: DeserializeOwned + fmt::Debug>(
reader: &api_result::Reader,
) -> VeilidAPIResult<T> {
match reader.which().map_err(map_to_internal_error)? {
api_result::Which::Ok(v) => {
let ok_val = v.map_err(map_to_internal_error)?;
let res: T = veilid_core::deserialize_json(ok_val).map_err(map_to_internal_error)?;
Ok(res)
}
api_result::Which::Err(e) => {
let err_val = e.map_err(map_to_internal_error)?;
let res: VeilidAPIError =
veilid_core::deserialize_json(err_val).map_err(map_to_internal_error)?;
Err(res)
}
}
}
// fn map_to_internal_error<T: ToString>(e: T) -> VeilidAPIError {
// VeilidAPIError::Internal {
// message: e.to_string(),
// }
// }
struct VeilidClientImpl {
comproc: CommandProcessor,
}
// fn decode_api_result<T: DeserializeOwned + fmt::Debug>(
// reader: &api_result::Reader,
// ) -> VeilidAPIResult<T> {
// match reader.which().map_err(map_to_internal_error)? {
// api_result::Which::Ok(v) => {
// let ok_val = v.map_err(map_to_internal_error)?;
// let res: T = veilid_core::deserialize_json(ok_val).map_err(map_to_internal_error)?;
// Ok(res)
// }
// api_result::Which::Err(e) => {
// let err_val = e.map_err(map_to_internal_error)?;
// let res: VeilidAPIError =
// veilid_core::deserialize_json(err_val).map_err(map_to_internal_error)?;
// Err(res)
// }
// }
// }
impl VeilidClientImpl {
pub fn new(comproc: CommandProcessor) -> Self {
Self { comproc }
}
}
// struct VeilidClientImpl {
// comproc: CommandProcessor,
// }
impl veilid_client::Server for VeilidClientImpl {
fn update(
&mut self,
params: veilid_client::UpdateParams,
_results: veilid_client::UpdateResults,
) -> Promise<(), ::capnp::Error> {
let veilid_update = pry!(pry!(params.get()).get_veilid_update());
let veilid_update: VeilidUpdate = pry_result!(deserialize_json(veilid_update));
// impl VeilidClientImpl {
// pub fn new(comproc: CommandProcessor) -> Self {
// Self { comproc }
// }
// }
match veilid_update {
VeilidUpdate::Log(log) => {
self.comproc.update_log(log);
}
VeilidUpdate::AppMessage(msg) => {
self.comproc.update_app_message(msg);
}
VeilidUpdate::AppCall(call) => {
self.comproc.update_app_call(call);
}
VeilidUpdate::Attachment(attachment) => {
self.comproc.update_attachment(attachment);
}
VeilidUpdate::Network(network) => {
self.comproc.update_network_status(network);
}
VeilidUpdate::Config(config) => {
self.comproc.update_config(config);
}
VeilidUpdate::RouteChange(route) => {
self.comproc.update_route(route);
}
VeilidUpdate::Shutdown => self.comproc.update_shutdown(),
VeilidUpdate::ValueChange(value_change) => {
self.comproc.update_value_change(value_change);
}
}
Promise::ok(())
}
}
// }
struct ClientApiConnectionInner {
comproc: CommandProcessor,
connect_addr: Option<SocketAddr>,
disconnector: Option<Disconnector<rpc_twoparty_capnp::Side>>,
server: Option<Rc<RefCell<veilid_server::Client>>>,
server: Option<flume::Sender<String>>,
server_settings: Option<String>,
disconnector: Option<StopSource>,
disconnect_requested: bool,
cancel_eventual: Eventual,
}
@ -106,9 +78,9 @@ impl ClientApiConnection {
inner: Rc::new(RefCell::new(ClientApiConnectionInner {
comproc,
connect_addr: None,
disconnector: None,
server: None,
server_settings: None,
disconnector: None,
disconnect_requested: false,
cancel_eventual: Eventual::new(),
})),
@ -123,195 +95,271 @@ impl ClientApiConnection {
eventual.resolve(); // don't need to await this
}
async fn process_veilid_state<'a>(
&'a mut self,
veilid_state: VeilidState,
) -> Result<(), String> {
let mut inner = self.inner.borrow_mut();
inner.comproc.update_attachment(veilid_state.attachment);
inner.comproc.update_network_status(veilid_state.network);
inner.comproc.update_config(veilid_state.config);
Ok(())
}
// async fn process_veilid_state<'a>(
// &'a mut self,
// veilid_state: VeilidState,
// ) -> Result<(), String> {
// let mut inner = self.inner.borrow_mut();
// inner.comproc.update_attachment(veilid_state.attachment);
// inner.comproc.update_network_status(veilid_state.network);
// inner.comproc.update_config(veilid_state.config);
// Ok(())
// }
async fn spawn_rpc_system(
&mut self,
connect_addr: SocketAddr,
mut rpc_system: RpcSystem<rpc_twoparty_capnp::Side>,
) -> Result<(), String> {
let mut request;
{
let mut inner = self.inner.borrow_mut();
// Get the bootstrap server connection object
inner.server = Some(Rc::new(RefCell::new(
rpc_system.bootstrap(rpc_twoparty_capnp::Side::Server),
)));
// Store our disconnector future for later (must happen after bootstrap, contrary to documentation)
inner.disconnector = Some(rpc_system.get_disconnector());
// Get a client object to pass to the server for status update callbacks
let client = capnp_rpc::new_client(VeilidClientImpl::new(inner.comproc.clone()));
// Register our client and get a registration object back
request = inner
.server
.as_ref()
.unwrap()
.borrow_mut()
.register_request();
request.get().set_veilid_client(client);
inner
.comproc
.set_connection_state(ConnectionState::Connected(
connect_addr,
std::time::SystemTime::now(),
));
}
let rpc_jh = spawn_local(rpc_system);
let reg_res: Result<registration::Client, String> = (async {
// Send the request and get the state object and the registration object
let response = request
.send()
.promise
.await
.map_err(|e| format!("failed to send register request: {}", e))?;
let response = response
.get()
.map_err(|e| format!("failed to get register response: {}", e))?;
// Get the registration object, which drops our connection when it is dropped
let registration = response
.get_registration()
.map_err(|e| format!("failed to get registration object: {}", e))?;
// Get the initial veilid state
let veilid_state = response
.get_state()
.map_err(|e| format!("failed to get initial veilid state: {}", e))?;
// Set up our state for the first time
let veilid_state: VeilidState = deserialize_json(veilid_state)
.map_err(|e| format!("failed to get deserialize veilid state: {}", e))?;
self.process_veilid_state(veilid_state).await?;
// Save server settings
let server_settings = response
.get_settings()
.map_err(|e| format!("failed to get initial veilid server settings: {}", e))?
.to_owned();
self.inner.borrow_mut().server_settings = Some(server_settings.clone());
// Don't drop the registration, doing so will remove the client
// object mapping from the server which we need for the update backchannel
Ok(registration)
})
.await;
let _registration = match reg_res {
Ok(v) => v,
Err(e) => {
rpc_jh.abort().await;
return Err(e);
}
async fn process_update(&self, update: json::JsonValue) {
let comproc = self.inner.borrow().comproc.clone();
let Some(kind) = update["kind"].as_str() else {
comproc.log_message(format!("missing update kind: {}", update));
return;
};
// Wait until rpc system completion or disconnect was requested
let res = rpc_jh.await;
res.map_err(|e| format!("client RPC system error: {}", e))
match kind {
"Log" => {
comproc.update_log(update);
}
"AppMessage" => {
comproc.update_app_message(update);
}
"AppCall" => {
comproc.update_app_call(update);
}
"Attachment" => {
comproc.update_attachment(update);
}
"Network" => {
comproc.update_network_status(update);
}
"Config" => {
comproc.update_config(update);
}
"RouteChange" => {
comproc.update_route(update);
}
"Shutdown" => comproc.update_shutdown(),
"ValueChange" => {
comproc.update_value_change(update);
}
_ => {
comproc.log_message(format!("unknown update kind: {}", update));
}
}
}
// async fn spawn_rpc_system(
// &mut self,
// connect_addr: SocketAddr,
// mut rpc_system: RpcSystem<rpc_twoparty_capnp::Side>,
// ) -> Result<(), String> {
// let mut request;
// {
// let mut inner = self.inner.borrow_mut();
// // Get the bootstrap server connection object
// inner.server = Some(Rc::new(RefCell::new(
// rpc_system.bootstrap(rpc_twoparty_capnp::Side::Server),
// )));
// // Store our disconnector future for later (must happen after bootstrap, contrary to documentation)
// inner.disconnector = Some(rpc_system.get_disconnector());
// // Get a client object to pass to the server for status update callbacks
// let client = capnp_rpc::new_client(VeilidClientImpl::new(inner.comproc.clone()));
// // Register our client and get a registration object back
// request = inner
// .server
// .as_ref()
// .unwrap()
// .borrow_mut()
// .register_request();
// request.get().set_veilid_client(client);
// inner
// .comproc
// .set_connection_state(ConnectionState::Connected(
// connect_addr,
// std::time::SystemTime::now(),
// ));
// }
// let rpc_jh = spawn_local(rpc_system);
// let reg_res: Result<registration::Client, String> = (async {
// // Send the request and get the state object and the registration object
// let response = request
// .send()
// .promise
// .await
// .map_err(|e| format!("failed to send register request: {}", e))?;
// let response = response
// .get()
// .map_err(|e| format!("failed to get register response: {}", e))?;
// // Get the registration object, which drops our connection when it is dropped
// let registration = response
// .get_registration()
// .map_err(|e| format!("failed to get registration object: {}", e))?;
// // Get the initial veilid state
// let veilid_state = response
// .get_state()
// .map_err(|e| format!("failed to get initial veilid state: {}", e))?;
// // Set up our state for the first time
// let veilid_state: VeilidState = deserialize_json(veilid_state)
// .map_err(|e| format!("failed to get deserialize veilid state: {}", e))?;
// self.process_veilid_state(veilid_state).await?;
// // Save server settings
// let server_settings = response
// .get_settings()
// .map_err(|e| format!("failed to get initial veilid server settings: {}", e))?
// .to_owned();
// self.inner.borrow_mut().server_settings = Some(server_settings.clone());
// // Don't drop the registration, doing so will remove the client
// // object mapping from the server which we need for the update backchannel
// Ok(registration)
// })
// .await;
// let _registration = match reg_res {
// Ok(v) => v,
// Err(e) => {
// rpc_jh.abort().await;
// return Err(e);
// }
// };
// // Wait until rpc system completion or disconnect was requested
// let res = rpc_jh.await;
// res.map_err(|e| format!("client RPC system error: {}", e))
// }
async fn handle_connection(&mut self, connect_addr: SocketAddr) -> Result<(), String> {
trace!("ClientApiConnection::handle_connection");
self.inner.borrow_mut().connect_addr = Some(connect_addr);
let stop_token = {
let stop_source = StopSource::new();
let token = stop_source.token();
let mut inner = self.inner.borrow_mut();
inner.connect_addr = Some(connect_addr);
inner.disconnector = Some(stop_source);
token
};
// Connect the TCP socket
let stream = TcpStream::connect(connect_addr)
.await
.map_err(map_to_string)?;
// If it succeed, disable nagle algorithm
stream.set_nodelay(true).map_err(map_to_string)?;
// Create the VAT network
// Split the stream
cfg_if! {
if #[cfg(feature="rt-async-std")] {
use futures::AsyncReadExt;
let (reader, writer) = stream.split();
let mut reader = BufReader::new(reader);
} else if #[cfg(feature="rt-tokio")] {
pub use tokio_util::compat::*;
let (reader, writer) = stream.into_split();
let reader = reader.compat();
let writer = writer.compat_write();
let mut reader = BufReader::new(reader);
}
}
let rpc_network = Box::new(twoparty::VatNetwork::new(
reader,
writer,
rpc_twoparty_capnp::Side::Client,
Default::default(),
));
// Create the rpc system
let rpc_system = RpcSystem::new(rpc_network, None);
// Process the rpc system until we decide we're done
match self.spawn_rpc_system(connect_addr, rpc_system).await {
Ok(()) => {}
// Process lines
let mut line = String::new();
while let Ok(r) = reader
.read_line(&mut line)
.timeout_at(stop_token.clone())
.await
{
match r {
Ok(size) => {
// Exit on EOF
if size == 0 {
// Disconnected
return Err("Connection closed".to_owned());
}
}
Err(e) => {
error!("Failed to spawn client RPC system: {}", e);
// Disconnected
return Err("Connection lost".to_owned());
}
}
// Drop the server and disconnector too (if we still have it)
let mut inner = self.inner.borrow_mut();
let disconnect_requested = inner.disconnect_requested;
inner.server_settings = None;
inner.server = None;
inner.disconnector = None;
inner.disconnect_requested = false;
inner.connect_addr = None;
// Unmarshal json
let j = match json::parse(line.trim()) {
Ok(v) => v,
Err(e) => {
error!("failed to parse server response: {}", e);
continue;
}
};
if j["type"] == "Update" {
self.process_update(j).await;
}
}
if !disconnect_requested {
// Connection lost
Err("Connection lost".to_owned())
} else {
// Connection finished
Ok(())
}
// let rpc_network = Box::new(twoparty::VatNetwork::new(
// reader,
// writer,
// rpc_twoparty_capnp::Side::Client,
// Default::default(),
// ));
// // Create the rpc system
// let rpc_system = RpcSystem::new(rpc_network, None);
// // Process the rpc system until we decide we're done
// match self.spawn_rpc_system(connect_addr, rpc_system).await {
// Ok(()) => {}
// Err(e) => {
// error!("Failed to spawn client RPC system: {}", e);
// }
// }
// // Drop the server and disconnector too (if we still have it)
// let mut inner = self.inner.borrow_mut();
// let disconnect_requested = inner.disconnect_requested;
// inner.server_settings = None;
// inner.server = None;
// inner.disconnector = None;
// inner.disconnect_requested = false;
// inner.connect_addr = None;
}
pub fn cancellable<T>(&mut self, p: Promise<T, capnp::Error>) -> Promise<T, capnp::Error>
where
T: 'static,
{
let (mut cancel_instance, cancel_eventual) = {
let inner = self.inner.borrow();
(
inner.cancel_eventual.instance_empty().fuse(),
inner.cancel_eventual.clone(),
)
};
let mut p = p.fuse();
// pub fn cancellable<T>(&mut self, p: Promise<T, capnp::Error>) -> Promise<T, capnp::Error>
// where
// T: 'static,
// {
// let (mut cancel_instance, cancel_eventual) = {
// let inner = self.inner.borrow();
// (
// inner.cancel_eventual.instance_empty().fuse(),
// inner.cancel_eventual.clone(),
// )
// };
// let mut p = p.fuse();
Promise::from_future(async move {
let out = select! {
a = p => {
a
},
_ = cancel_instance => {
Err(capnp::Error::failed("cancelled".into()))
}
};
drop(cancel_instance);
cancel_eventual.reset();
out
})
}
// Promise::from_future(async move {
// let out = select! {
// a = p => {
// a
// },
// _ = cancel_instance => {
// Err(capnp::Error::failed("cancelled".into()))
// }
// };
// drop(cancel_instance);
// cancel_eventual.reset();
// out
// })
// }
pub async fn server_attach(&mut self) -> Result<(), String> {
trace!("ClientApiConnection::server_attach");

View File

@ -5,8 +5,7 @@ use std::cell::*;
use std::net::SocketAddr;
use std::rc::Rc;
use std::time::SystemTime;
use veilid_core::tools::*;
use veilid_core::*;
use veilid_tools::*;
pub fn convert_loglevel(s: &str) -> Result<VeilidConfigLogLevel, String> {
match s.to_ascii_lowercase().as_str() {
@ -387,7 +386,11 @@ reply - reply to an AppCall not handled directly by the server
// calls into ui
////////////////////////////////////////////
pub fn update_attachment(&mut self, attachment: veilid_core::VeilidStateAttachment) {
pub fn log_message(&mut self, message: String) {
self.inner().ui.add_node_event(message);
}
pub fn update_attachment(&mut self, attachment: json::JsonValue) {
self.inner_mut().ui.set_attachment_state(
attachment.state,
attachment.public_internet_ready,
@ -424,17 +427,17 @@ reply - reply to an AppCall not handled directly by the server
self.inner().ui.add_node_event(out);
}
}
pub fn update_value_change(&mut self, value_change: veilid_core::VeilidValueChange) {
let out = format!("Value change: {:?}", value_change);
pub fn update_value_change(&mut self, value_change: json::JsonValue) {
let out = format!("Value change: {:?}", value_change.as_str().unwrap_or("???"));
self.inner().ui.add_node_event(out);
}
pub fn update_log(&mut self, log: veilid_core::VeilidLog) {
pub fn update_log(&mut self, log: json::JsonValue) {
self.inner().ui.add_node_event(format!(
"{}: {}{}",
log.log_level,
log.message,
if let Some(bt) = log.backtrace {
log["log_level"].as_str().unwrap_or("???"),
log["message"].as_str().unwrap_or("???"),
if let Some(bt) = log["backtrace"].as_str() {
format!("\nBacktrace:\n{}", bt)
} else {
"".to_owned()
@ -442,7 +445,7 @@ reply - reply to an AppCall not handled directly by the server
));
}
pub fn update_app_message(&mut self, msg: veilid_core::VeilidAppMessage) {
pub fn update_app_message(&mut self, msg: json::JsonValue) {
// check is message body is ascii printable
let mut printable = true;
for c in msg.message() {

View File

@ -3,7 +3,7 @@
#![recursion_limit = "256"]
use crate::tools::*;
use veilid_core::tools::*;
use veilid_tools::*;
use clap::{Arg, ColorChoice, Command};
use flexi_logger::*;

View File

@ -1,7 +1,6 @@
use super::*;
use cursive_table_view::*;
use std::cmp::Ordering;
use veilid_core::*;
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub enum PeerTableColumn {

View File

@ -12,12 +12,11 @@ use cursive::Cursive;
use cursive::CursiveRunnable;
use cursive_flexi_logger_view::{CursiveLogWriter, FlexiLoggerView};
//use cursive_multiplex::*;
use log::*;
use std::cell::RefCell;
use std::collections::{HashMap, VecDeque};
use std::rc::Rc;
use thiserror::Error;
use veilid_core::*;
use veilid_tools::*;
//////////////////////////////////////////////////////////////
///
@ -50,20 +49,20 @@ impl<T> Dirty<T> {
pub type UICallback = Box<dyn Fn(&mut Cursive) + Send>;
struct UIState {
attachment_state: Dirty<AttachmentState>,
attachment_state: Dirty<String>,
public_internet_ready: Dirty<bool>,
local_network_ready: Dirty<bool>,
network_started: Dirty<bool>,
network_down_up: Dirty<(f32, f32)>,
connection_state: Dirty<ConnectionState>,
peers_state: Dirty<Vec<PeerTableData>>,
peers_state: Dirty<Vec<json::JsonValue>>,
node_id: Dirty<String>,
}
impl UIState {
pub fn new() -> Self {
Self {
attachment_state: Dirty::new(AttachmentState::Detached),
attachment_state: Dirty::new("Detached".to_owned()),
public_internet_ready: Dirty::new(false),
local_network_ready: Dirty::new(false),
network_started: Dirty::new(false),
@ -239,15 +238,16 @@ impl UI {
s.find_name("peers").unwrap()
}
fn render_attachment_state(inner: &mut UIInner) -> String {
let att = match inner.ui_state.attachment_state.get() {
AttachmentState::Detached => "[----]",
AttachmentState::Attaching => "[/ ]",
AttachmentState::AttachedWeak => "[| ]",
AttachmentState::AttachedGood => "[|| ]",
AttachmentState::AttachedStrong => "[||| ]",
AttachmentState::FullyAttached => "[||||]",
AttachmentState::OverAttached => "[++++]",
AttachmentState::Detaching => "[////]",
let att = match inner.ui_state.attachment_state.get().as_str() {
"Detached" => "[----]",
"Attaching" => "[/ ]",
"AttachedWeak" => "[| ]",
"AttachedGood" => "[|| ]",
"AttachedStrong" => "[||| ]",
"FullyAttached" => "[||||]",
"OverAttached" => "[++++]",
"Detaching" => "[////]",
_ => "[????]",
};
let pi = if *inner.ui_state.public_internet_ready.get() {
"+P"
@ -272,15 +272,16 @@ impl UI {
}
fn render_button_attach<'a>(inner: &mut UIInner) -> (&'a str, bool) {
if let ConnectionState::Connected(_, _) = inner.ui_state.connection_state.get() {
match inner.ui_state.attachment_state.get() {
AttachmentState::Detached => ("Attach", true),
AttachmentState::Attaching => ("Detach", true),
AttachmentState::AttachedWeak => ("Detach", true),
AttachmentState::AttachedGood => ("Detach", true),
AttachmentState::AttachedStrong => ("Detach", true),
AttachmentState::FullyAttached => ("Detach", true),
AttachmentState::OverAttached => ("Detach", true),
AttachmentState::Detaching => ("Detach", false),
match inner.ui_state.attachment_state.get().as_str() {
"Detached" => ("Attach", true),
"Attaching" => ("Detach", true),
"AttachedWeak" => ("Detach", true),
"AttachedGood" => ("Detach", true),
"AttachedStrong" => ("Detach", true),
"FullyAttached" => ("Detach", true),
"OverAttached" => ("Detach", true),
"Detaching" => ("Detach", false),
_ => ("???", false),
}
} else {
(" ---- ", false)
@ -412,15 +413,17 @@ impl UI {
}
fn on_button_attach_pressed(s: &mut Cursive) {
let action: Option<bool> = match Self::inner_mut(s).ui_state.attachment_state.get() {
AttachmentState::Detached => Some(true),
AttachmentState::Attaching => Some(false),
AttachmentState::AttachedWeak => Some(false),
AttachmentState::AttachedGood => Some(false),
AttachmentState::AttachedStrong => Some(false),
AttachmentState::FullyAttached => Some(false),
AttachmentState::OverAttached => Some(false),
AttachmentState::Detaching => None,
let action: Option<bool> = match Self::inner_mut(s).ui_state.attachment_state.get().as_str()
{
"Detached" => Some(true),
"Attaching" => Some(false),
"AttachedWeak" => Some(false),
"AttachedGood" => Some(false),
"AttachedStrong" => Some(false),
"FullyAttached" => Some(false),
"OverAttached" => Some(false),
"Detaching" => None,
_ => None,
};
let mut cmdproc = Self::command_processor(s);
if let Some(a) = action {

View File

@ -140,8 +140,5 @@ pub use wasm::*;
pub mod tests;
// For iOS tests
#[no_mangle]
pub extern "C" fn main_rs() {
// start game code here
}
pub extern "C" fn main_rs() {}