mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-07-18 00:10:45 +00:00
feat: 1.5.0 drop 2 (#122)
Co-authored-by: str1k3r <115313679+S1l3ntStr1ke87@users.noreply.github.com>
This commit is contained in:
BIN
public/images/lce_online.png
Normal file
BIN
public/images/lce_online.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 MiB After Width: | Height: | Size: 364 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 MiB After Width: | Height: | Size: 364 KiB |
@@ -23,7 +23,6 @@ use commands::runners;
|
||||
use commands::skin;
|
||||
use commands::steam;
|
||||
use commands::workshop;
|
||||
use networking::direct;
|
||||
use networking::relay;
|
||||
use networking::stun;
|
||||
use state::{DownloadState, GameState, ProxyGuard};
|
||||
@@ -108,7 +107,6 @@ pub fn run() {
|
||||
game::restore_instance,
|
||||
commands::console2lce::import_world,
|
||||
stun::stun_discover,
|
||||
direct::start_direct_proxy,
|
||||
relay::start_relay_proxy,
|
||||
relay::start_host_relay,
|
||||
relay::stop_proxy,
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
use tauri::State;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use crate::state::ProxyGuard;
|
||||
async fn run_direct_proxy(
|
||||
proxy_state: &ProxyGuard,
|
||||
target_ip: &str,
|
||||
target_port: u16,
|
||||
cancel: CancellationToken,
|
||||
) -> Result<u16, String> {
|
||||
let remote = tokio::net::TcpStream::connect(format!("{}:{}", target_ip, target_port))
|
||||
.await
|
||||
.map_err(|e| format!("Direct TCP connect failed: {}", e))?;
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:0")
|
||||
.await
|
||||
.map_err(|e| format!("Bind failed: {}", e))?;
|
||||
let local_port = listener.local_addr().map_err(|e| e.to_string())?.port();
|
||||
{
|
||||
let mut port = proxy_state.local_port.lock().await;
|
||||
*port = Some(local_port);
|
||||
}
|
||||
|
||||
let (local_stream, _) = tokio::select! {
|
||||
result = listener.accept() => result.map_err(|e| format!("Accept failed: {}", e))?,
|
||||
_ = cancel.cancelled() => return Err("Proxy cancelled".into()),
|
||||
};
|
||||
|
||||
let (mut a_read, mut a_write) = remote.into_split();
|
||||
let (mut b_read, mut b_write) = local_stream.into_split();
|
||||
let cancel_a = cancel.clone();
|
||||
let task_a = tokio::spawn(async move {
|
||||
let mut buf = [0u8; 65536];
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = a_read.read(&mut buf) => {
|
||||
match result {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(n) => { if b_write.write_all(&buf[..n]).await.is_err() { break; } }
|
||||
}
|
||||
}
|
||||
_ = cancel_a.cancelled() => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let cancel_b = cancel.clone();
|
||||
let task_b = tokio::spawn(async move {
|
||||
let mut buf = [0u8; 65536];
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = b_read.read(&mut buf) => {
|
||||
match result {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(n) => { if a_write.write_all(&buf[..n]).await.is_err() { break; } }
|
||||
}
|
||||
}
|
||||
_ = cancel_b.cancelled() => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tokio::select! {
|
||||
_ = task_a => {},
|
||||
_ = task_b => {},
|
||||
_ = cancel.cancelled() => {},
|
||||
}
|
||||
|
||||
Ok(local_port)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn start_direct_proxy(
|
||||
proxy_state: State<'_, ProxyGuard>,
|
||||
target_ip: String,
|
||||
target_port: u16,
|
||||
) -> Result<u16, String> {
|
||||
let cancel = CancellationToken::new();
|
||||
let session_id = "__direct__".to_string();
|
||||
{
|
||||
let mut tokens = proxy_state.cancel_tokens.lock().await;
|
||||
tokens.insert(session_id.clone(), cancel.clone());
|
||||
}
|
||||
|
||||
let local_port = run_direct_proxy(&proxy_state, &target_ip, target_port, cancel).await?;
|
||||
|
||||
{
|
||||
let mut tokens = proxy_state.cancel_tokens.lock().await;
|
||||
tokens.remove(&session_id);
|
||||
}
|
||||
|
||||
Ok(local_port)
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
pub mod stun;
|
||||
pub mod relay;
|
||||
pub mod direct;
|
||||
|
||||
@@ -1,268 +1,176 @@
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use tauri::State;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use crate::state::ProxyGuard;
|
||||
use crate::util;
|
||||
async fn run_relay_proxy(
|
||||
proxy_state: &ProxyGuard,
|
||||
ws_url: &str,
|
||||
auth_token: &str,
|
||||
cancel: CancellationToken,
|
||||
) -> Result<u16, String> {
|
||||
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
|
||||
eprintln!("[Emerald] Joiner relay: connecting WS...");
|
||||
let mut request = ws_url
|
||||
.into_client_request()
|
||||
.map_err(|e| format!("Failed to build WS request: {}", e))?;
|
||||
request.headers_mut().insert(
|
||||
http::header::AUTHORIZATION,
|
||||
format!("Bearer {}", auth_token)
|
||||
.parse()
|
||||
.map_err(|_| "Invalid auth header value".to_string())?,
|
||||
);
|
||||
request.headers_mut().insert(
|
||||
http::header::USER_AGENT,
|
||||
"MCLCE-LceLive/1.0"
|
||||
.parse()
|
||||
.map_err(|_| "Invalid UA header value".to_string())?,
|
||||
);
|
||||
|
||||
let (ws_stream, _) = tokio_tungstenite::connect_async(request)
|
||||
.await
|
||||
.map_err(|e| format!("Relay WS connect failed: {}", e))?;
|
||||
eprintln!("[Emerald] Joiner relay: WS connected");
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:61000")
|
||||
.await
|
||||
.map_err(|e| format!("Bind failed: {}", e))?;
|
||||
let local_port = listener.local_addr().map_err(|e| e.to_string())?.port();
|
||||
eprintln!("[Emerald] Joiner relay: bound on 0.0.0.0:{}", local_port);
|
||||
|
||||
{
|
||||
let mut port = proxy_state.local_port.lock().await;
|
||||
*port = Some(local_port);
|
||||
const PROXY_ADDR: &str = "proxy.mclegacyedition.xyz:2052"; //neo: yeah bro im hardcoding it
|
||||
async fn read_line(stream: &mut TcpStream) -> Result<String, String> {
|
||||
let mut buf = Vec::new();
|
||||
let mut byte = [0u8; 1];
|
||||
loop {
|
||||
stream.read_exact(&mut byte).await.map_err(|e| e.to_string())?;
|
||||
if byte[0] == b'\n' { break; }
|
||||
buf.push(byte[0]);
|
||||
}
|
||||
String::from_utf8(buf).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
eprintln!("[Emerald] Joiner relay: waiting for TCP accept on port {}...", local_port);
|
||||
let (tcp_stream, _) = tokio::select! {
|
||||
result = listener.accept() => {
|
||||
eprintln!("[Emerald] Joiner relay: TCP accepted");
|
||||
result.map_err(|e| format!("Accept failed: {}", e)).unwrap()
|
||||
},
|
||||
_ = cancel.cancelled() => {
|
||||
eprintln!("[Emerald] Joiner relay: cancelled before accept");
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
eprintln!("[Emerald] Joiner relay: starting forwarders");
|
||||
let (tcp_read, tcp_write) = tcp_stream.into_split();
|
||||
let (ws_write, ws_read) = ws_stream.split();
|
||||
let cancel_ws = cancel.clone();
|
||||
let forward_tcp = tokio::spawn(async move {
|
||||
let mut ws_write = ws_write;
|
||||
let mut tcp_read = tcp_read;
|
||||
let mut buf = [0u8; 65536];
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = tcp_read.read(&mut buf) => {
|
||||
match result {
|
||||
Ok(0) => { eprintln!("[Emerald] Joiner relay: TCP→WS EOF"); break; },
|
||||
Err(e) => { eprintln!("[Emerald] Joiner relay: TCP→WS read error: {e}"); break; },
|
||||
Ok(n) => {
|
||||
eprintln!("[Emerald] Joiner relay: TCP→WS forwarding {} bytes", n);
|
||||
if ws_write.send(tokio_tungstenite::tungstenite::Message::Binary(buf[..n].to_vec())).await.is_err() {
|
||||
eprintln!("[Emerald] Joiner relay: TCP→WS send error"); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = cancel_ws.cancelled() => { eprintln!("[Emerald] Joiner relay: TCP→WS cancelled"); break; },
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let cancel_tcp = cancel.clone();
|
||||
let forward_ws = tokio::spawn(async move {
|
||||
let ws_read = ws_read;
|
||||
let mut tcp_write = tcp_write;
|
||||
tokio::pin!(ws_read);
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = ws_read.next() => {
|
||||
match result {
|
||||
Some(Ok(tokio_tungstenite::tungstenite::Message::Binary(data))) => {
|
||||
eprintln!("[Emerald] Joiner relay: WS→TCP forwarding {} bytes", data.len());
|
||||
if tcp_write.write_all(&data).await.is_err() { break; }
|
||||
}
|
||||
Some(Ok(tokio_tungstenite::tungstenite::Message::Close(_))) => { break; }
|
||||
None => { break; }
|
||||
Some(Err(e)) => { eprintln!("[Emerald] Joiner relay: WS→TCP error: {e}"); break; }
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ = cancel_tcp.cancelled() => { break; },
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tokio::select! {
|
||||
_ = forward_tcp => eprintln!("[Emerald] Joiner relay: forward_tcp done"),
|
||||
_ = forward_ws => eprintln!("[Emerald] Joiner relay: forward_ws done"),
|
||||
_ = cancel.cancelled() => eprintln!("[Emerald] Joiner relay: cancelled"),
|
||||
}
|
||||
eprintln!("[Emerald] Joiner relay: relay task ended");
|
||||
});
|
||||
|
||||
Ok(local_port)
|
||||
async fn write_line(stream: &mut TcpStream, line: &str) -> Result<(), String> {
|
||||
let data = format!("{}\n", line);
|
||||
stream.write_all(data.as_bytes()).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
async fn run_host_relay(
|
||||
_proxy_state: &ProxyGuard,
|
||||
ws_url: &str,
|
||||
proxy_addr: &str,
|
||||
auth_token: &str,
|
||||
game_port: u16,
|
||||
cancel: CancellationToken,
|
||||
) -> Result<(), String> {
|
||||
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
|
||||
eprintln!("[Emerald] Host relay: connecting WS...");
|
||||
let mut request = ws_url
|
||||
.into_client_request()
|
||||
.map_err(|e| format!("Failed to build WS request: {}", e))?;
|
||||
request.headers_mut().insert(
|
||||
http::header::AUTHORIZATION,
|
||||
format!("Bearer {}", auth_token)
|
||||
.parse()
|
||||
.map_err(|_| "Invalid auth header value".to_string())?,
|
||||
);
|
||||
request.headers_mut().insert(
|
||||
http::header::USER_AGENT,
|
||||
"MCLCE-LceLive/1.0"
|
||||
.parse()
|
||||
.map_err(|_| "Invalid UA header value".to_string())?,
|
||||
);
|
||||
|
||||
let (ws_stream, _) = tokio_tungstenite::connect_async(request)
|
||||
let mut host_conn = TcpStream::connect(proxy_addr)
|
||||
.await
|
||||
.map_err(|e| format!("Host relay WS connect failed: {}", e))?;
|
||||
eprintln!("[Emerald] Host relay: WS connected");
|
||||
eprintln!("[Emerald] Host relay: connecting to game 127.0.0.1:{}...", game_port);
|
||||
.map_err(|e| format!("Proxy connect failed: {}", e))?;
|
||||
|
||||
write_line(&mut host_conn, &format!("HOST {} 0", auth_token)).await?;
|
||||
let game_stream = loop {
|
||||
match tokio::net::TcpStream::connect(format!("127.0.0.1:{}", game_port)).await {
|
||||
Ok(stream) => { eprintln!("[Emerald] Host relay: connected to game"); break stream; },
|
||||
Err(e) => {
|
||||
eprintln!("[Emerald] Host relay: game connect failed (retrying): {e}");
|
||||
match TcpStream::connect(format!("127.0.0.1:{}", game_port)).await {
|
||||
Ok(s) => break s,
|
||||
Err(_) => {
|
||||
tokio::select! {
|
||||
_ = cancel.cancelled() => return Err("Host relay cancelled".into()),
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(2)) => {}
|
||||
_ = cancel.cancelled() => return Err("Cancelled".into()),
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(1)) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
eprintln!("[Emerald] Host relay: starting forwarders");
|
||||
let (game_read, game_write) = game_stream.into_split();
|
||||
let (ws_write, ws_read) = ws_stream.split();
|
||||
let cancel_ws = cancel.clone();
|
||||
let forward_game = tokio::spawn(async move {
|
||||
let mut ws_write = ws_write;
|
||||
let mut game_read = game_read;
|
||||
let client_line = read_line(&mut host_conn).await?;
|
||||
let client_parts: Vec<&str> = client_line.split_whitespace().collect();
|
||||
if client_parts.len() < 2 || client_parts[0] != "CLIENT" {
|
||||
return Err(format!("Expected CLIENT, got: {}", client_line));
|
||||
}
|
||||
let joiner_id = client_parts[1];
|
||||
let mut accept_conn = TcpStream::connect(proxy_addr)
|
||||
.await
|
||||
.map_err(|e| format!("Proxy connect failed: {}", e))?;
|
||||
write_line(&mut accept_conn, &format!("ACCEPT {} 0 {}", auth_token, joiner_id)).await?;
|
||||
let (mut g_read, mut g_write) = game_stream.into_split();
|
||||
let (mut a_read, mut a_write) = accept_conn.into_split();
|
||||
let c1 = cancel.clone();
|
||||
let c2 = cancel.clone();
|
||||
let t1 = tokio::spawn(async move {
|
||||
let mut buf = [0u8; 65536];
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = game_read.read(&mut buf) => {
|
||||
match result {
|
||||
Ok(0) => { eprintln!("[Emerald] Host relay: game→WS EOF"); break; },
|
||||
Err(e) => { eprintln!("[Emerald] Host relay: game→WS read error: {e}"); break; },
|
||||
Ok(n) => {
|
||||
eprintln!("[Emerald] Host relay: game→WS forwarding {} bytes", n);
|
||||
if ws_write.send(tokio_tungstenite::tungstenite::Message::Binary(buf[..n].to_vec())).await.is_err() { break; }
|
||||
}
|
||||
r = g_read.read(&mut buf) => {
|
||||
match r {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(n) => { if a_write.write_all(&buf[..n]).await.is_err() { break; } }
|
||||
}
|
||||
}
|
||||
_ = cancel_ws.cancelled() => { break; },
|
||||
_ = c1.cancelled() => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let cancel_ws2 = cancel.clone();
|
||||
let forward_ws = tokio::spawn(async move {
|
||||
let ws_read = ws_read;
|
||||
let mut game_write = game_write;
|
||||
tokio::pin!(ws_read);
|
||||
let t2 = tokio::spawn(async move {
|
||||
let mut buf = [0u8; 65536];
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = ws_read.next() => {
|
||||
match result {
|
||||
Some(Ok(tokio_tungstenite::tungstenite::Message::Binary(data))) => {
|
||||
eprintln!("[Emerald] Host relay: WS→game forwarding {} bytes", data.len());
|
||||
if game_write.write_all(&data).await.is_err() { break; }
|
||||
}
|
||||
Some(Ok(tokio_tungstenite::tungstenite::Message::Close(_))) => { break; }
|
||||
None => { break; }
|
||||
Some(Err(e)) => { eprintln!("[Emerald] Host relay: WS→game error: {e}"); break; }
|
||||
_ => {}
|
||||
r = a_read.read(&mut buf) => {
|
||||
match r {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(n) => { if g_write.write_all(&buf[..n]).await.is_err() { break; } }
|
||||
}
|
||||
}
|
||||
_ = cancel_ws2.cancelled() => { break; },
|
||||
_ = c2.cancelled() => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tokio::select! {
|
||||
_ = forward_game => eprintln!("[Emerald] Host relay: forward_game done"),
|
||||
_ = forward_ws => eprintln!("[Emerald] Host relay: forward_ws done"),
|
||||
_ = cancel.cancelled() => eprintln!("[Emerald] Host relay: cancelled"),
|
||||
}
|
||||
|
||||
let _ = tokio::join!(t1, t2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn start_relay_proxy(
|
||||
proxy_state: State<'_, ProxyGuard>,
|
||||
api_base_url: String,
|
||||
access_token: String,
|
||||
session_id: String,
|
||||
async fn run_relay_proxy(
|
||||
proxy_state: &ProxyGuard,
|
||||
proxy_addr: &str,
|
||||
auth_token: &str,
|
||||
target_session: &str,
|
||||
cancel: CancellationToken,
|
||||
) -> Result<u16, String> {
|
||||
let ws_base = util::ws_base_url(&api_base_url);
|
||||
let ws_url = format!("{}/api/relay/ws?sessionId={}&role=joiner", ws_base, session_id);
|
||||
let cancel = CancellationToken::new();
|
||||
let mut stream = TcpStream::connect(proxy_addr)
|
||||
.await
|
||||
.map_err(|e| format!("Proxy connect failed: {}", e))?;
|
||||
|
||||
write_line(&mut stream, &format!("JOIN {} 0 {}", auth_token, target_session)).await?;
|
||||
let listener = TcpListener::bind("0.0.0.0:61000")
|
||||
.await
|
||||
.map_err(|e| format!("Bind failed: {}", e))?;
|
||||
let local_port = listener.local_addr().map_err(|e| e.to_string())?.port();
|
||||
{
|
||||
let mut tokens = proxy_state.cancel_tokens.lock().await;
|
||||
tokens.insert(session_id.clone(), cancel.clone());
|
||||
let mut port = proxy_state.local_port.lock().await;
|
||||
*port = Some(local_port);
|
||||
}
|
||||
|
||||
let local_port = run_relay_proxy(&proxy_state, &ws_url, &access_token, cancel).await?;
|
||||
let (local_stream, _) = tokio::select! {
|
||||
r = listener.accept() => r.map_err(|e| format!("Accept failed: {}", e))?,
|
||||
_ = cancel.cancelled() => return Err("Cancelled".into()),
|
||||
};
|
||||
|
||||
{
|
||||
let mut tokens = proxy_state.cancel_tokens.lock().await;
|
||||
tokens.remove(&session_id);
|
||||
}
|
||||
let (mut l_read, mut l_write) = local_stream.into_split();
|
||||
let (mut s_read, mut s_write) = stream.into_split();
|
||||
let c1 = cancel.clone();
|
||||
let c2 = cancel.clone();
|
||||
let t1 = tokio::spawn(async move {
|
||||
let mut buf = [0u8; 65536];
|
||||
loop {
|
||||
tokio::select! {
|
||||
r = l_read.read(&mut buf) => {
|
||||
match r {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(n) => { if s_write.write_all(&buf[..n]).await.is_err() { break; } }
|
||||
}
|
||||
}
|
||||
_ = c1.cancelled() => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let t2 = tokio::spawn(async move {
|
||||
let mut buf = [0u8; 65536];
|
||||
loop {
|
||||
tokio::select! {
|
||||
r = s_read.read(&mut buf) => {
|
||||
match r {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(n) => { if l_write.write_all(&buf[..n]).await.is_err() { break; } }
|
||||
}
|
||||
}
|
||||
_ = c2.cancelled() => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let _ = tokio::join!(t1, t2);
|
||||
Ok(local_port)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn start_host_relay(
|
||||
proxy_state: State<'_, ProxyGuard>,
|
||||
api_base_url: String,
|
||||
access_token: String,
|
||||
session_id: String,
|
||||
auth_token: String,
|
||||
game_port: u16,
|
||||
) -> Result<(), String> {
|
||||
let ws_base = util::ws_base_url(&api_base_url);
|
||||
let ws_url = format!("{}/api/relay/ws?sessionId={}&role=host", ws_base, session_id);
|
||||
let addr = PROXY_ADDR;
|
||||
let cancel = CancellationToken::new();
|
||||
let session_id = "__host__".to_string();
|
||||
{
|
||||
let mut tokens = proxy_state.cancel_tokens.lock().await;
|
||||
tokens.insert(session_id.clone(), cancel.clone());
|
||||
}
|
||||
|
||||
let result = run_host_relay(&proxy_state, &ws_url, &access_token, game_port, cancel).await;
|
||||
|
||||
let result = run_host_relay(&proxy_state, &addr, &auth_token, game_port, cancel).await;
|
||||
{
|
||||
let mut tokens = proxy_state.cancel_tokens.lock().await;
|
||||
tokens.remove(&session_id);
|
||||
@@ -271,6 +179,29 @@ pub async fn start_host_relay(
|
||||
result
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn start_relay_proxy(
|
||||
proxy_state: State<'_, ProxyGuard>,
|
||||
auth_token: String,
|
||||
target_session: String,
|
||||
) -> Result<u16, String> {
|
||||
let addr = PROXY_ADDR;
|
||||
let cancel = CancellationToken::new();
|
||||
let session_id = target_session.clone();
|
||||
{
|
||||
let mut tokens = proxy_state.cancel_tokens.lock().await;
|
||||
tokens.insert(session_id.clone(), cancel.clone());
|
||||
}
|
||||
|
||||
let local_port = run_relay_proxy(&proxy_state, &addr, &auth_token, &target_session, cancel).await?;
|
||||
{
|
||||
let mut tokens = proxy_state.cancel_tokens.lock().await;
|
||||
tokens.remove(&session_id);
|
||||
}
|
||||
|
||||
Ok(local_port)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn stop_proxy(proxy_state: State<'_, ProxyGuard>, session_id: String) -> Result<(), String> {
|
||||
let mut tokens = proxy_state.cancel_tokens.lock().await;
|
||||
@@ -299,10 +230,10 @@ pub async fn join_game(
|
||||
game_state: State<'_, crate::state::GameState>,
|
||||
_proxy_state: State<'_, ProxyGuard>,
|
||||
_api_base_url: String,
|
||||
_access_token: String,
|
||||
_auth_token: String,
|
||||
host_ip: String,
|
||||
host_port: u16,
|
||||
_session_id: String,
|
||||
_target_session: String,
|
||||
instance_id: String,
|
||||
) -> Result<(), String> {
|
||||
let server = crate::types::McServer {
|
||||
|
||||
@@ -44,14 +44,6 @@ pub fn copy_dir_all(src: impl AsRef<std::path::Path>, dst: impl AsRef<std::path:
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ws_base_url(api_base_url: &str) -> String {
|
||||
if api_base_url.starts_with("https") {
|
||||
api_base_url.replace("https", "wss")
|
||||
} else {
|
||||
api_base_url.replace("http", "ws")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn unix_path_to_wine_z_path(unix_path: &PathBuf) -> String {
|
||||
let p = unix_path.to_string_lossy();
|
||||
|
||||
@@ -507,7 +507,7 @@ const SkinViewer = memo(function SkinViewer({
|
||||
setActiveView("screenshots");
|
||||
} else if (focusIndex === 3) {
|
||||
playPressSound();
|
||||
setActiveView("lcelive");
|
||||
setActiveView("lceonline");
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -749,7 +749,7 @@ const SkinViewer = memo(function SkinViewer({
|
||||
onMouseEnter={() => isFocusedSection && setFocusIndex(3)}
|
||||
onClick={() => {
|
||||
playPressSound();
|
||||
setActiveView("lcelive");
|
||||
setActiveView("lceonline");
|
||||
}}
|
||||
className={`mc-sq-btn w-12 h-12 flex items-center justify-center outline-none border-none ${isFocusedSection && focusIndex === 3 ? "" : ""}`}
|
||||
style={
|
||||
@@ -760,11 +760,11 @@ const SkinViewer = memo(function SkinViewer({
|
||||
}
|
||||
: {}
|
||||
}
|
||||
title="LCELive"
|
||||
title="LCE Online"
|
||||
>
|
||||
<img
|
||||
src="/images/friends.png"
|
||||
alt="LCELive"
|
||||
alt="LCE Online"
|
||||
className="w-8 h-8 object-contain"
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { TauriService } from "../../services/TauriService";
|
||||
import { lceLiveService, GameInvite } from "../../services/LceLiveService";
|
||||
import { lceOnlineService } from "../../services/LceOnlineService";
|
||||
import type { Edition } from "../../types/edition";
|
||||
interface GameInvite {
|
||||
inviteId: string;
|
||||
from: string;
|
||||
hostIp: string;
|
||||
hostPort: number;
|
||||
hostName: string;
|
||||
sessionId?: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export default function ChooseInstanceModal({
|
||||
isOpen,
|
||||
@@ -26,9 +35,8 @@ export default function ChooseInstanceModal({
|
||||
const [error, setError] = useState<string>("");
|
||||
const [isJoining, setIsJoining] = useState(false);
|
||||
const [focusIndex, setFocusIndex] = useState(0);
|
||||
|
||||
const validInstances = editions.filter((e: Edition) =>
|
||||
installs.includes(e.instanceId)
|
||||
installs.includes(e.instanceId),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -52,30 +60,37 @@ export default function ChooseInstanceModal({
|
||||
playPressSound();
|
||||
setIsJoining(true);
|
||||
setError("");
|
||||
setStatus("Accepting invite...");
|
||||
setStatus("Launching game...");
|
||||
try {
|
||||
const inviteData = await lceLiveService.acceptGameInvite(invite.inviteId) as Record<string, unknown>;
|
||||
const fromIp = typeof invite.from !== 'string' ? (invite.from as unknown as Record<string, string>).hostIp : undefined;
|
||||
const hostIp: string = (inviteData.hostIp as string) || fromIp || invite.hostIp;
|
||||
const hostPort: number = (inviteData.hostPort as number) || invite.hostPort;
|
||||
const sessionId = (inviteData.signalingSessionId as string) || invite.signalingSessionId || "";
|
||||
|
||||
const sessionId = invite.sessionId || "";
|
||||
if (sessionId) {
|
||||
setStatus("Connecting via relay...");
|
||||
const baseUrl = lceLiveService.apiBaseUrl;
|
||||
const accessToken = lceLiveService.accessToken ?? "";
|
||||
await TauriService.startRelayProxy(baseUrl, accessToken, sessionId);
|
||||
const accessToken = lceOnlineService.accessToken ?? "";
|
||||
const relayPromise = TauriService.startRelayProxy(
|
||||
accessToken,
|
||||
sessionId,
|
||||
);
|
||||
setStatus("Launching game...");
|
||||
await TauriService.launchGame(
|
||||
selectedInstance,
|
||||
[{ name: invite.hostName || "LCELive Game", ip: "127.0.0.1", port: 61000 }],
|
||||
["-ip", "127.0.0.1", "-port", "61000", "-quitondisconnect"]
|
||||
[
|
||||
{
|
||||
name: invite.hostName || "LCE Online Game",
|
||||
ip: "127.0.0.1",
|
||||
port: 61000,
|
||||
},
|
||||
],
|
||||
["-ip", "127.0.0.1", "-port", "61000", "-quitondisconnect"],
|
||||
);
|
||||
await relayPromise;
|
||||
} else {
|
||||
setStatus("Launching game...");
|
||||
await TauriService.stopAllProxies();
|
||||
await TauriService.launchGame(selectedInstance, [
|
||||
{ name: invite.hostName || "LCELive Game", ip: hostIp, port: hostPort }
|
||||
{
|
||||
name: invite.hostName || "LCE Online Game",
|
||||
ip: invite.hostIp,
|
||||
port: invite.hostPort,
|
||||
},
|
||||
]);
|
||||
}
|
||||
onClose();
|
||||
@@ -103,13 +118,18 @@ export default function ChooseInstanceModal({
|
||||
setFocusIndex((prev) => (prev - 1 + max) % max);
|
||||
} else if (e.key === "Enter") {
|
||||
if (focusIndex === 0 && validInstances.length > 0) {
|
||||
const currentIdx = validInstances.findIndex((i: Edition) => i.instanceId === selectedInstance);
|
||||
const currentIdx = validInstances.findIndex(
|
||||
(i: Edition) => i.instanceId === selectedInstance,
|
||||
);
|
||||
const next = (currentIdx + 1) % validInstances.length;
|
||||
setSelectedInstance(validInstances[next].instanceId);
|
||||
playPressSound();
|
||||
} else if (focusIndex === 1 && !isJoining) {
|
||||
handleJoin();
|
||||
} else if (focusIndex === (validInstances.length > 0 ? 2 : 1) && !isJoining) {
|
||||
} else if (
|
||||
focusIndex === (validInstances.length > 0 ? 2 : 1) &&
|
||||
!isJoining
|
||||
) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
@@ -119,8 +139,11 @@ export default function ChooseInstanceModal({
|
||||
}, [isOpen, focusIndex, selectedInstance, validInstances, isJoining]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const hostName = invite ? (typeof invite.from === 'string' ? invite.from : invite.from.displayName) : "";
|
||||
const hostName = invite
|
||||
? typeof invite.from === "string"
|
||||
? invite.from
|
||||
: invite.hostName
|
||||
: "";
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
@@ -147,25 +170,41 @@ export default function ChooseInstanceModal({
|
||||
</p>
|
||||
|
||||
{validInstances.length > 0 ? (
|
||||
<div className="w-full mb-4 flex flex-col gap-2 max-h-[300px] overflow-y-auto"
|
||||
style={{ scrollbarWidth: "thin", scrollbarColor: "#373737 transparent" }}>
|
||||
<div
|
||||
className="w-full mb-4 flex flex-col gap-2 max-h-[300px] overflow-y-auto"
|
||||
style={{
|
||||
scrollbarWidth: "thin",
|
||||
scrollbarColor: "#373737 transparent",
|
||||
}}
|
||||
>
|
||||
{validInstances.map((inst: Edition) => {
|
||||
const isSelected = selectedInstance === inst.instanceId;
|
||||
return (
|
||||
<div
|
||||
key={inst.instanceId}
|
||||
onClick={() => { playPressSound(); setSelectedInstance(inst.instanceId); }}
|
||||
onClick={() => {
|
||||
playPressSound();
|
||||
setSelectedInstance(inst.instanceId);
|
||||
}}
|
||||
onMouseEnter={() => setFocusIndex(0)}
|
||||
className={`w-full px-4 py-3 cursor-pointer flex items-center gap-3 transition-all outline-none border-none ${isSelected ? "bg-white/15 border-l-4 border-[#FFFF55]" : "bg-black/20 hover:bg-black/30 border-l-4 border-transparent"}`}
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
>
|
||||
<div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${isSelected ? "border-[#FFFF55]" : "border-gray-500"}`}>
|
||||
{isSelected && <div className="w-2 h-2 rounded-full bg-[#FFFF55]" />}
|
||||
<div
|
||||
className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${isSelected ? "border-[#FFFF55]" : "border-gray-500"}`}
|
||||
>
|
||||
{isSelected && (
|
||||
<div className="w-2 h-2 rounded-full bg-[#FFFF55]" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-white text-lg font-bold mc-text-shadow">{inst.name}</span>
|
||||
<span className="text-white text-lg font-bold mc-text-shadow">
|
||||
{inst.name}
|
||||
</span>
|
||||
{inst.selectedBranch && (
|
||||
<span className="text-gray-400 text-xs">{inst.selectedBranch}</span>
|
||||
<span className="text-gray-400 text-xs">
|
||||
{inst.selectedBranch}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -190,10 +229,15 @@ export default function ChooseInstanceModal({
|
||||
const cancelIdx = validInstances.length > 0 ? 2 : 1;
|
||||
setFocusIndex(cancelIdx);
|
||||
}}
|
||||
onClick={() => { playBackSound(); onClose(); }}
|
||||
onClick={() => {
|
||||
playBackSound();
|
||||
onClose();
|
||||
}}
|
||||
className={`w-32 h-10 flex items-center justify-center text-xl mc-text-shadow transition-colors outline-none border-none ${(() => {
|
||||
const cancelIdx = validInstances.length > 0 ? 2 : 1;
|
||||
return focusIndex === cancelIdx ? "text-[#FFFF55]" : "text-white";
|
||||
return focusIndex === cancelIdx
|
||||
? "text-[#FFFF55]"
|
||||
: "text-white";
|
||||
})()}`}
|
||||
style={{
|
||||
backgroundImage: (() => {
|
||||
@@ -214,9 +258,10 @@ export default function ChooseInstanceModal({
|
||||
onClick={handleJoin}
|
||||
className={`w-32 h-10 flex items-center justify-center text-xl mc-text-shadow transition-colors outline-none border-none ${focusIndex === 1 ? "text-[#FFFF55]" : "text-white"}`}
|
||||
style={{
|
||||
backgroundImage: focusIndex === 1
|
||||
? "url('/images/button_highlighted.png')"
|
||||
: "url('/images/Button_Background.png')",
|
||||
backgroundImage:
|
||||
focusIndex === 1
|
||||
? "url('/images/button_highlighted.png')"
|
||||
: "url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
@@ -233,7 +278,9 @@ export default function ChooseInstanceModal({
|
||||
</h2>
|
||||
<div className="flex flex-col items-center gap-4 py-8">
|
||||
<div className="w-12 h-12 border-4 border-[#FFFF55] border-t-transparent rounded-full animate-spin" />
|
||||
<p className="text-white text-lg mc-text-shadow text-center">{status}</p>
|
||||
<p className="text-white text-lg mc-text-shadow text-center">
|
||||
{status}
|
||||
</p>
|
||||
</div>
|
||||
{error && (
|
||||
<div className="text-red-500 text-center mc-text-shadow uppercase text-xs tracking-widest mb-3">
|
||||
|
||||
@@ -209,6 +209,7 @@ const CreditsView = memo(function CreditsView() {
|
||||
{ name: "dr.av", url: "#" },
|
||||
{ name: "alreadywarned", url: "#" },
|
||||
{ name: "andrewjcf", url: "#" },
|
||||
{ name: "dille", url: "#" },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -427,4 +428,4 @@ const CreditsView = memo(function CreditsView() {
|
||||
);
|
||||
});
|
||||
|
||||
export default CreditsView;
|
||||
export default CreditsView;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef, useMemo, useCallback, memo } from "react";
|
||||
import { useState, useEffect, useRef, useMemo, memo } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import {
|
||||
useUI,
|
||||
@@ -6,69 +6,55 @@ import {
|
||||
useAudio,
|
||||
useGame,
|
||||
} from "../../context/LauncherContext";
|
||||
import {
|
||||
lceLiveService,
|
||||
LceLiveAccount,
|
||||
FriendRequest,
|
||||
GameInvite,
|
||||
DeviceLinkStartResponse,
|
||||
} from "../../services/LceLiveService";
|
||||
import { TauriService } from "../../services/TauriService";
|
||||
import ChooseInstanceModal from "../modals/ChooseInstanceModal";
|
||||
import QRCode from "qrcode";
|
||||
|
||||
interface LceLiveViewProps {
|
||||
import { lceOnlineService } from "../../services/LceOnlineService";
|
||||
import { TauriService } from "../../services/TauriService";
|
||||
interface LceOnlineViewProps {
|
||||
addFriendTarget?: string | null;
|
||||
onClearAddFriendTarget?: () => void;
|
||||
invites?: Array<{
|
||||
inviteid: string;
|
||||
from: { uuid: string; username: string };
|
||||
sessionid: string;
|
||||
}>;
|
||||
}
|
||||
const LceLiveView = memo(function LceLiveView({
|
||||
const LceOnlineView = memo(function LceOnlineView({
|
||||
addFriendTarget,
|
||||
onClearAddFriendTarget,
|
||||
}: LceLiveViewProps) {
|
||||
invites: invitesProp,
|
||||
}: LceOnlineViewProps) {
|
||||
const { setActiveView } = useUI();
|
||||
const { animationsEnabled } = useConfig();
|
||||
const { playPressSound, playBackSound } = useAudio();
|
||||
const { editions, installs } = useGame();
|
||||
const [isSignedIn, setIsSignedIn] = useState(lceLiveService.signedIn);
|
||||
const game = useGame();
|
||||
const [isSignedIn, setIsSignedIn] = useState(lceOnlineService.signedIn);
|
||||
const [currentTab, setCurrentTab] = useState<
|
||||
"friends" | "requests" | "invites" | "device_link"
|
||||
"friends" | "requests" | "invites"
|
||||
>("friends");
|
||||
const [focusIndex, setFocusIndex] = useState<number | null>(0);
|
||||
const [acceptInvite, setAcceptInvite] = useState<GameInvite | null>(null);
|
||||
const [friends, setFriends] = useState<LceLiveAccount[]>([]);
|
||||
const [incomingReqs, setIncomingReqs] = useState<FriendRequest[]>([]);
|
||||
const [outgoingReqs, setOutgoingReqs] = useState<FriendRequest[]>([]);
|
||||
const [invites, setInvites] = useState<GameInvite[]>([]);
|
||||
const [linkData, setLinkData] = useState<DeviceLinkStartResponse | null>(
|
||||
null,
|
||||
);
|
||||
const [linkError, setLinkError] = useState<string | null>(null);
|
||||
const [friends, setFriends] = useState<string[]>([]);
|
||||
const [incomingReqs, setIncomingReqs] = useState<string[]>([]);
|
||||
const [outgoingReqs, setOutgoingReqs] = useState<string[]>([]);
|
||||
const invites = invitesProp ?? [];
|
||||
const [isHosting, setIsHosting] = useState(false);
|
||||
const [_hostStatus, setHostStatus] = useState("");
|
||||
const [hostIp, setHostIp] = useState("");
|
||||
const [hostPort, setHostPort] = useState(19132);
|
||||
const [isDiscovering, setIsDiscovering] = useState(false);
|
||||
const [invitedFriends, setInvitedFriends] = useState<Set<string>>(new Set());
|
||||
const [showHostMethodPicker, setShowHostMethodPicker] = useState(false);
|
||||
const [isAddingFriend, setIsAddingFriend] = useState(false);
|
||||
const [addFriendUsername, setAddFriendUsername] = useState("");
|
||||
const addFriendInputRef = useRef<HTMLInputElement>(null);
|
||||
const [errorModal, setErrorModal] = useState<string | null>(null);
|
||||
const [qrDataUrl, setQrDataUrl] = useState<string>("");
|
||||
const [joinTarget, setJoinTarget] = useState<{
|
||||
inviteid: string;
|
||||
sessionId: string;
|
||||
hostName: string;
|
||||
} | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const fetchSocialData = async () => {
|
||||
if (!lceLiveService.signedIn) return;
|
||||
if (!lceOnlineService.signedIn) return;
|
||||
try {
|
||||
const [f, reqs, invs] = await Promise.all([
|
||||
lceLiveService.getFriends(),
|
||||
lceLiveService.getPendingRequests(),
|
||||
lceLiveService.getGameInvites(),
|
||||
]);
|
||||
setFriends(f);
|
||||
setIncomingReqs(reqs.incoming);
|
||||
setOutgoingReqs(reqs.outgoing);
|
||||
setInvites(invs.filter((i: GameInvite) => i.status === "pending"));
|
||||
const lists = await lceOnlineService.getSocialLists();
|
||||
setFriends(lists.friends);
|
||||
setIncomingReqs(lists.requests);
|
||||
setOutgoingReqs([]);
|
||||
} catch (e: unknown) {
|
||||
console.error(e);
|
||||
}
|
||||
@@ -76,84 +62,57 @@ const LceLiveView = memo(function LceLiveView({
|
||||
|
||||
useEffect(() => {
|
||||
if (isSignedIn) {
|
||||
if (currentTab === "device_link") setCurrentTab("friends");
|
||||
fetchSocialData();
|
||||
const pollInvites = setInterval(async () => {
|
||||
try {
|
||||
const invs = await lceLiveService.getGameInvites();
|
||||
setInvites(invs.filter((i: GameInvite) => i.status === "pending"));
|
||||
} catch (e) {
|
||||
console.warn("Failed to poll invites", e);
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(pollInvites);
|
||||
} else {
|
||||
setCurrentTab("device_link");
|
||||
}
|
||||
}, [isSignedIn, currentTab]);
|
||||
}, [isSignedIn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentTab !== "device_link") return;
|
||||
let mounted = true;
|
||||
let pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||
const startLink = async () => {
|
||||
try {
|
||||
if (!linkData) {
|
||||
const data = await lceLiveService.startDeviceLink();
|
||||
if (mounted) setLinkData(data);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (mounted) setLinkError(e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
};
|
||||
return lceOnlineService.onSessionChange(() => {
|
||||
setIsSignedIn(lceOnlineService.signedIn);
|
||||
});
|
||||
}, []);
|
||||
|
||||
startLink();
|
||||
if (linkData?.deviceCode) {
|
||||
pollInterval = setInterval(
|
||||
async () => {
|
||||
try {
|
||||
const res = await lceLiveService.pollDeviceLink(
|
||||
linkData.deviceCode,
|
||||
);
|
||||
if (res.isLinked && mounted) {
|
||||
setIsSignedIn(true);
|
||||
setLinkData(null);
|
||||
if (pollInterval !== null) clearInterval(pollInterval);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
console.warn("Poll failed", e);
|
||||
}
|
||||
},
|
||||
Math.max(linkData.intervalSeconds * 1000, 2000),
|
||||
useEffect(() => {
|
||||
if (!isSignedIn) {
|
||||
TauriService.openUrl(
|
||||
"https://mclegacyedition.xyz/internal/auth?appId=emerald_launcher",
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
if (pollInterval !== null) clearInterval(pollInterval);
|
||||
};
|
||||
}, [currentTab, linkData]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!linkData?.verificationUri || !linkData?.userCode) return;
|
||||
const authUrl = `${linkData.verificationUri}?code=${linkData.userCode}`;
|
||||
QRCode.toDataURL(authUrl, { width: 200, margin: 1 }, (err, url) => {
|
||||
if (!err) setQrDataUrl(url);
|
||||
});
|
||||
}, [linkData]);
|
||||
|
||||
const openAuthUrl = useCallback(() => {
|
||||
if (!linkData?.verificationUri || !linkData?.userCode) return;
|
||||
const authUrl = `${linkData.verificationUri}?code=${linkData.userCode}`;
|
||||
TauriService.openUrl(authUrl);
|
||||
}, [linkData]);
|
||||
if (!addFriendTarget) return;
|
||||
setCurrentTab("friends");
|
||||
handleAction(() => lceOnlineService.sendFriendRequest(addFriendTarget));
|
||||
onClearAddFriendTarget?.();
|
||||
}, [addFriendTarget, onClearAddFriendTarget]);
|
||||
|
||||
const handleLogout = () => {
|
||||
playPressSound();
|
||||
lceLiveService.logoutLocal();
|
||||
lceOnlineService.logoutLocal();
|
||||
setIsSignedIn(false);
|
||||
setLinkData(null);
|
||||
};
|
||||
|
||||
const handleStartHosting = async () => {
|
||||
playPressSound();
|
||||
try {
|
||||
const token = lceOnlineService.accessToken ?? "";
|
||||
if (!token) return;
|
||||
TauriService.startHostRelay(token, 25565).catch(() => {});
|
||||
setIsHosting(true);
|
||||
} catch (e: unknown) {
|
||||
setErrorModal(e instanceof Error ? e.message : "Failed to start hosting");
|
||||
}
|
||||
};
|
||||
|
||||
const handleStopHosting = async () => {
|
||||
playPressSound();
|
||||
try {
|
||||
await TauriService.stopAllProxies();
|
||||
} catch (e: unknown) {
|
||||
console.warn("Stop hosting failed", e);
|
||||
}
|
||||
setIsHosting(false);
|
||||
};
|
||||
|
||||
const handleAction = async (action: () => Promise<void>) => {
|
||||
@@ -166,113 +125,6 @@ const LceLiveView = memo(function LceLiveView({
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartHosting = () => {
|
||||
playPressSound();
|
||||
setShowHostMethodPicker(true);
|
||||
setFocusIndex(0);
|
||||
};
|
||||
|
||||
const handleHostDirect = async () => {
|
||||
setShowHostMethodPicker(false);
|
||||
setFocusIndex(0);
|
||||
setIsDiscovering(true);
|
||||
setHostStatus("Discovering external IP...");
|
||||
try {
|
||||
const endpoint = await TauriService.stunDiscover();
|
||||
setHostIp(endpoint.ip);
|
||||
setHostPort(25565);
|
||||
setIsHosting(true);
|
||||
setHostStatus(`Hosting at ${endpoint.ip}:25565`);
|
||||
setInvitedFriends(new Set());
|
||||
} catch (e: unknown) {
|
||||
const msg =
|
||||
e instanceof Error
|
||||
? e.message
|
||||
: typeof e === "string"
|
||||
? e
|
||||
: "Unknown error";
|
||||
setErrorModal("STUN discovery failed: " + msg);
|
||||
setHostStatus("");
|
||||
} finally {
|
||||
setIsDiscovering(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleHostRelay = async () => {
|
||||
setShowHostMethodPicker(false);
|
||||
setFocusIndex(0);
|
||||
setIsDiscovering(true);
|
||||
setHostStatus("Discovering external IP for invite...");
|
||||
try {
|
||||
const endpoint = await TauriService.stunDiscover();
|
||||
setHostIp(endpoint.ip);
|
||||
setHostPort(25565);
|
||||
} catch {
|
||||
setHostIp("127.0.0.1");
|
||||
setHostPort(25565);
|
||||
}
|
||||
setIsHosting(true);
|
||||
setHostStatus("Relay ready - invite friends to activate");
|
||||
setInvitedFriends(new Set());
|
||||
setIsDiscovering(false);
|
||||
};
|
||||
|
||||
const handleStopHosting = async () => {
|
||||
playPressSound();
|
||||
try {
|
||||
await TauriService.stopAllProxies();
|
||||
await lceLiveService.deactivateGameInvites();
|
||||
} catch (e: unknown) {
|
||||
console.warn("Stop hosting failed", e);
|
||||
}
|
||||
setIsHosting(false);
|
||||
setHostStatus("");
|
||||
setHostIp("");
|
||||
setInvitedFriends(new Set());
|
||||
};
|
||||
|
||||
const handleInviteFriend = async (friend: LceLiveAccount) => {
|
||||
playPressSound();
|
||||
const name = lceLiveService.displayUsername;
|
||||
const sessionId = crypto.randomUUID();
|
||||
try {
|
||||
await lceLiveService.sendGameInvite(
|
||||
friend.accountId,
|
||||
hostIp,
|
||||
hostPort,
|
||||
name,
|
||||
sessionId,
|
||||
);
|
||||
setInvitedFriends((prev) => new Set(prev).add(friend.accountId));
|
||||
setHostStatus("Connecting relay...");
|
||||
TauriService.startHostRelay(
|
||||
lceLiveService.apiBaseUrl,
|
||||
lceLiveService.accessToken ?? "",
|
||||
sessionId,
|
||||
25565,
|
||||
)
|
||||
.then(() => setHostStatus("Relay active"))
|
||||
.catch((relayErr: unknown) => {
|
||||
const relayMsg =
|
||||
relayErr instanceof Error
|
||||
? relayErr.message
|
||||
: typeof relayErr === "string"
|
||||
? relayErr
|
||||
: "Unknown error";
|
||||
console.warn("Relay failed:", relayMsg);
|
||||
setHostStatus("Relay disconnected");
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
const msg =
|
||||
e instanceof Error
|
||||
? e.message
|
||||
: typeof e === "string"
|
||||
? e
|
||||
: "Unknown error";
|
||||
setErrorModal("Failed to send invite: " + msg);
|
||||
}
|
||||
};
|
||||
|
||||
type MenuItem = {
|
||||
id: string;
|
||||
type: "button" | "friend" | "request_in" | "request_out" | "invite";
|
||||
@@ -283,35 +135,21 @@ const LceLiveView = memo(function LceLiveView({
|
||||
|
||||
const menuItems = useMemo<MenuItem[]>(() => {
|
||||
const items: MenuItem[] = [];
|
||||
if (currentTab === "device_link") {
|
||||
if (linkData) {
|
||||
if (currentTab === "friends") {
|
||||
if (!isHosting) {
|
||||
items.push({
|
||||
id: "link_retry",
|
||||
id: "host_game",
|
||||
type: "button",
|
||||
label: "Restart Link",
|
||||
onClick: () => {
|
||||
setLinkData(null);
|
||||
playPressSound();
|
||||
},
|
||||
label: "Host Game",
|
||||
onClick: handleStartHosting,
|
||||
});
|
||||
} else {
|
||||
items.push({
|
||||
id: "stop_hosting",
|
||||
type: "button",
|
||||
label: "Stop Hosting",
|
||||
onClick: handleStopHosting,
|
||||
});
|
||||
}
|
||||
} else if (currentTab === "friends") {
|
||||
if (!isDiscovering && !showHostMethodPicker) {
|
||||
if (!isHosting) {
|
||||
items.push({
|
||||
id: "host_game",
|
||||
type: "button",
|
||||
label: "Host Game",
|
||||
onClick: handleStartHosting,
|
||||
});
|
||||
} else {
|
||||
items.push({
|
||||
id: "stop_hosting",
|
||||
type: "button",
|
||||
label: "Stop Hosting",
|
||||
onClick: handleStopHosting,
|
||||
});
|
||||
}
|
||||
}
|
||||
items.push({
|
||||
id: "add_friend",
|
||||
@@ -331,56 +169,57 @@ const LceLiveView = memo(function LceLiveView({
|
||||
});
|
||||
friends.forEach((f) => {
|
||||
items.push({
|
||||
id: `friend_${f.accountId}`,
|
||||
id: `friend_${f}`,
|
||||
type: "friend",
|
||||
label: f.displayName,
|
||||
onClick: isHosting
|
||||
? () => handleInviteFriend(f)
|
||||
: () =>
|
||||
handleAction(() => lceLiveService.removeFriend(f.accountId)),
|
||||
label: f,
|
||||
onClick: () => handleAction(() => lceOnlineService.removeFriend(f)),
|
||||
onClickSecondary: isHosting
|
||||
? () => handleAction(() => lceLiveService.removeFriend(f.accountId))
|
||||
? () => handleAction(() => lceOnlineService.sendInvite(f))
|
||||
: undefined,
|
||||
});
|
||||
});
|
||||
} else if (currentTab === "requests") {
|
||||
incomingReqs.forEach((r) => {
|
||||
items.push({
|
||||
id: `req_in_${r.username}`,
|
||||
id: `req_in_${r}`,
|
||||
type: "request_in",
|
||||
label: r.displayName,
|
||||
label: r,
|
||||
onClick: () =>
|
||||
handleAction(() => lceLiveService.sendFriendRequest(r.username)),
|
||||
handleAction(() => lceOnlineService.acceptFriendRequest(r)),
|
||||
onClickSecondary: () =>
|
||||
handleAction(() =>
|
||||
lceLiveService.declineFriendRequest(r.accountId),
|
||||
),
|
||||
handleAction(() => lceOnlineService.declineFriendRequest(r)),
|
||||
});
|
||||
});
|
||||
outgoingReqs.forEach((r) => {
|
||||
items.push({
|
||||
id: `req_out_${r.username}`,
|
||||
id: `req_out_${r}`,
|
||||
type: "request_out",
|
||||
label: r.displayName,
|
||||
label: r,
|
||||
onClick: () =>
|
||||
handleAction(() =>
|
||||
lceLiveService.declineFriendRequest(r.accountId),
|
||||
),
|
||||
handleAction(() => lceOnlineService.declineFriendRequest(r)),
|
||||
});
|
||||
});
|
||||
} else if (currentTab === "invites") {
|
||||
invites.forEach((inv) => {
|
||||
items.push({
|
||||
id: `inv_${inv.inviteId}`,
|
||||
id: `invite_${inv.inviteid}`,
|
||||
type: "invite",
|
||||
label:
|
||||
typeof inv.from === "string" ? "Unknown" : inv.from.displayName,
|
||||
onClick: () => {
|
||||
playPressSound();
|
||||
setAcceptInvite(inv);
|
||||
},
|
||||
label: inv.from.username,
|
||||
onClick: () =>
|
||||
handleAction(async () => {
|
||||
const sessionId = await lceOnlineService.acceptInvite(
|
||||
inv.from.username,
|
||||
);
|
||||
setJoinTarget({
|
||||
inviteid: inv.inviteid,
|
||||
sessionId,
|
||||
hostName: inv.from.username,
|
||||
});
|
||||
}),
|
||||
onClickSecondary: () =>
|
||||
handleAction(() => lceLiveService.declineGameInvite(inv.inviteId)),
|
||||
handleAction(() =>
|
||||
lceOnlineService.declineInvite(inv.from.username),
|
||||
),
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -392,22 +231,10 @@ const LceLiveView = memo(function LceLiveView({
|
||||
incomingReqs,
|
||||
outgoingReqs,
|
||||
invites,
|
||||
linkData,
|
||||
playPressSound,
|
||||
isHosting,
|
||||
isDiscovering,
|
||||
showHostMethodPicker,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!addFriendTarget) return;
|
||||
setCurrentTab("friends");
|
||||
handleAction(() =>
|
||||
lceLiveService.sendFriendRequest(addFriendTarget),
|
||||
);
|
||||
onClearAddFriendTarget?.();
|
||||
}, [addFriendTarget, onClearAddFriendTarget]);
|
||||
|
||||
const tabs: ("friends" | "requests" | "invites")[] = [
|
||||
"friends",
|
||||
"requests",
|
||||
@@ -429,7 +256,7 @@ const LceLiveView = memo(function LceLiveView({
|
||||
} else if (e.key === "Enter") {
|
||||
if (addFriendUsername.trim() !== "") {
|
||||
handleAction(() =>
|
||||
lceLiveService.sendFriendRequest(addFriendUsername.trim()),
|
||||
lceOnlineService.sendFriendRequest(addFriendUsername.trim()),
|
||||
);
|
||||
setIsAddingFriend(false);
|
||||
}
|
||||
@@ -437,23 +264,11 @@ const LceLiveView = memo(function LceLiveView({
|
||||
return;
|
||||
}
|
||||
|
||||
if (showHostMethodPicker) {
|
||||
if (!isSignedIn) {
|
||||
if (e.key === "Escape" || e.key === "Backspace") {
|
||||
setShowHostMethodPicker(false);
|
||||
setFocusIndex(0);
|
||||
playBackSound();
|
||||
} else if (e.key === "ArrowDown") {
|
||||
setFocusIndex((prev) => (prev === null || prev >= 2 ? 0 : prev + 1));
|
||||
} else if (e.key === "ArrowUp") {
|
||||
setFocusIndex((prev) => (prev === null || prev <= 0 ? 2 : prev - 1));
|
||||
} else if (e.key === "Enter") {
|
||||
if (focusIndex === 0) handleHostRelay();
|
||||
else if (focusIndex === 1) handleHostDirect();
|
||||
else if (focusIndex === 2) {
|
||||
setShowHostMethodPicker(false);
|
||||
setFocusIndex(0);
|
||||
playBackSound();
|
||||
}
|
||||
setActiveView("main");
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -464,22 +279,20 @@ const LceLiveView = memo(function LceLiveView({
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentTab !== "device_link") {
|
||||
const curIdx = tabs.indexOf(currentTab);
|
||||
if (e.key === "q" || e.key === "Q" || e.key === "ArrowLeft") {
|
||||
const next = curIdx > 0 ? tabs[curIdx - 1] : tabs[tabs.length - 1];
|
||||
setCurrentTab(next);
|
||||
setFocusIndex(0);
|
||||
playPressSound();
|
||||
return;
|
||||
}
|
||||
if (e.key === "e" || e.key === "E" || e.key === "ArrowRight") {
|
||||
const next = curIdx < tabs.length - 1 ? tabs[curIdx + 1] : tabs[0];
|
||||
setCurrentTab(next);
|
||||
setFocusIndex(0);
|
||||
playPressSound();
|
||||
return;
|
||||
}
|
||||
const curIdx = tabs.indexOf(currentTab);
|
||||
if (e.key === "q" || e.key === "Q" || e.key === "ArrowLeft") {
|
||||
const next = curIdx > 0 ? tabs[curIdx - 1] : tabs[tabs.length - 1];
|
||||
setCurrentTab(next);
|
||||
setFocusIndex(0);
|
||||
playPressSound();
|
||||
return;
|
||||
}
|
||||
if (e.key === "e" || e.key === "E" || e.key === "ArrowRight") {
|
||||
const next = curIdx < tabs.length - 1 ? tabs[curIdx + 1] : tabs[0];
|
||||
setCurrentTab(next);
|
||||
setFocusIndex(0);
|
||||
playPressSound();
|
||||
return;
|
||||
}
|
||||
|
||||
const itemCount = menuItems.length;
|
||||
@@ -508,9 +321,7 @@ const LceLiveView = memo(function LceLiveView({
|
||||
isAddingFriend,
|
||||
addFriendUsername,
|
||||
errorModal,
|
||||
showHostMethodPicker,
|
||||
handleHostDirect,
|
||||
handleHostRelay,
|
||||
isSignedIn,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -532,52 +343,21 @@ const LceLiveView = memo(function LceLiveView({
|
||||
}
|
||||
}
|
||||
}, [focusIndex, isAddingFriend]);
|
||||
|
||||
const renderContent = () => {
|
||||
if (currentTab === "device_link") {
|
||||
if (!isSignedIn) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center flex-1 text-center py-12">
|
||||
{!linkData ? (
|
||||
<p className="text-lg text-[#2a2a2a] font-bold">
|
||||
{linkError || "Starting device link..."}
|
||||
</p>
|
||||
) : (
|
||||
<div className="flex items-start justify-center gap-10 w-full max-w-2xl">
|
||||
<div className="flex flex-col items-center space-y-4 flex-1 min-w-0">
|
||||
<p className="text-lg text-[#2a2a2a] font-bold">
|
||||
Open this link in your browser:
|
||||
</p>
|
||||
<p className="text-[#111] text-base font-bold tracking-widest break-all bg-black/10 px-4 py-2 rounded shadow-inner">
|
||||
{linkData.verificationUri}
|
||||
</p>
|
||||
<p className="text-lg text-[#2a2a2a] font-bold">
|
||||
And enter the code:
|
||||
</p>
|
||||
<p className="text-[#111] text-4xl tracking-[0.2em] font-bold bg-black/10 px-6 py-3 rounded shadow-inner">
|
||||
{linkData.userCode}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-3 shrink-0">
|
||||
{qrDataUrl && (
|
||||
<img
|
||||
src={qrDataUrl}
|
||||
alt="QR Code"
|
||||
className="w-48 h-48 image-rendering-pixelated"
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
onClick={openAuthUrl}
|
||||
className="h-10 px-6 flex items-center justify-center text-white mc-text-shadow text-base font-bold uppercase tracking-widest outline-none border-none hover:text-[#FFFF55] transition-colors"
|
||||
style={{
|
||||
backgroundImage: "url('/images/button_highlighted.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
Open in Browser
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<h2 className="text-[#FFFF55] text-3xl mc-text-shadow mb-8 pb-2 w-full text-center uppercase tracking-widest">
|
||||
<img
|
||||
src="/images/lceonline.png"
|
||||
alt="LCE Online"
|
||||
className="h-16 mx-auto"
|
||||
/>
|
||||
</h2>
|
||||
<p className="text-white text-lg mc-text-shadow mb-8 max-w-sm">
|
||||
Awaiting authentication...
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -617,10 +397,10 @@ const LceLiveView = memo(function LceLiveView({
|
||||
<div className="bg-black/10 px-4 py-3 text-[#2a2a2a] font-bold tracking-widest uppercase border-b-4 border-[#222] flex justify-between shadow-sm z-10">
|
||||
<span>
|
||||
{currentTab === "friends"
|
||||
? "Joinable Friends"
|
||||
: currentTab === "requests"
|
||||
? "Pending Requests"
|
||||
: "Game Invites"}
|
||||
? "Friends"
|
||||
: currentTab === "invites"
|
||||
? "Invites"
|
||||
: "Pending Requests"}
|
||||
</span>
|
||||
<span className="text-[#111]">{listItems.length}</span>
|
||||
</div>
|
||||
@@ -648,59 +428,28 @@ const LceLiveView = memo(function LceLiveView({
|
||||
<span className="text-[#2a2a2a] font-bold text-2xl truncate pr-4">
|
||||
{item.label}
|
||||
</span>
|
||||
<span className="text-[#555] text-base font-bold truncate">
|
||||
@
|
||||
{menuItems.find((m) => m.id === item.id)?.type ===
|
||||
"friend"
|
||||
? friends.find(
|
||||
(f) => `friend_${f.accountId}` === item.id,
|
||||
)?.username
|
||||
: item.type === "request_in"
|
||||
? incomingReqs.find(
|
||||
(f) => `req_in_${f.accountId}` === item.id,
|
||||
)?.username
|
||||
: item.type === "request_out"
|
||||
? outgoingReqs.find(
|
||||
(f) =>
|
||||
`req_out_${f.accountId}` === item.id,
|
||||
)?.username
|
||||
: "Invite"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-3 pr-2 shrink-0">
|
||||
{item.type === "friend" && !isHosting && (
|
||||
<button
|
||||
className={`px-6 h-12 flex items-center justify-center font-bold text-base outline-none uppercase tracking-widest mc-text-shadow ${isFocused ? "text-white shadow-md" : "text-gray-300"}`}
|
||||
style={{
|
||||
backgroundImage:
|
||||
"url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
onClick={item.onClick}
|
||||
>
|
||||
REMOVE
|
||||
</button>
|
||||
)}
|
||||
{item.type === "friend" && isHosting && (
|
||||
{item.type === "friend" && (
|
||||
<>
|
||||
<button
|
||||
className={`px-6 h-12 flex items-center justify-center font-bold text-base outline-none uppercase tracking-widest mc-text-shadow ${isFocused ? "text-white shadow-md" : "text-gray-300"}`}
|
||||
style={{
|
||||
backgroundImage:
|
||||
"url('/images/button_highlighted.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
onClick={item.onClick}
|
||||
>
|
||||
{invitedFriends.has(
|
||||
item.id.replace("friend_", ""),
|
||||
)
|
||||
? "INVITED"
|
||||
: "INVITE"}
|
||||
</button>
|
||||
{item.onClickSecondary && (
|
||||
<button
|
||||
className={`px-6 h-12 flex items-center justify-center font-bold text-base outline-none uppercase tracking-widest mc-text-shadow ${isFocused ? "text-white shadow-md" : "text-gray-300"}`}
|
||||
style={{
|
||||
backgroundImage:
|
||||
"url('/images/button_highlighted.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
item.onClickSecondary?.();
|
||||
}}
|
||||
>
|
||||
INVITE
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={`px-6 h-12 flex items-center justify-center font-bold text-base outline-none uppercase tracking-widest mc-text-shadow ${isFocused ? "text-white shadow-md" : "text-gray-300"}`}
|
||||
style={{
|
||||
@@ -709,10 +458,7 @@ const LceLiveView = memo(function LceLiveView({
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
item.onClickSecondary?.();
|
||||
}}
|
||||
onClick={item.onClick}
|
||||
>
|
||||
REMOVE
|
||||
</button>
|
||||
@@ -732,8 +478,38 @@ const LceLiveView = memo(function LceLiveView({
|
||||
CANCEL
|
||||
</button>
|
||||
)}
|
||||
{(item.type === "request_in" ||
|
||||
item.type === "invite") && (
|
||||
{item.type === "invite" && (
|
||||
<>
|
||||
<button
|
||||
className={`px-6 h-12 flex items-center justify-center font-bold text-base outline-none uppercase tracking-widest mc-text-shadow ${isFocused ? "text-white shadow-md" : "text-gray-300"}`}
|
||||
style={{
|
||||
backgroundImage:
|
||||
"url('/images/button_highlighted.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
onClick={item.onClick}
|
||||
>
|
||||
ACCEPT
|
||||
</button>
|
||||
<button
|
||||
className={`px-6 h-12 flex items-center justify-center font-bold text-base outline-none uppercase tracking-widest mc-text-shadow ${isFocused ? "text-white shadow-md" : "text-gray-300"}`}
|
||||
style={{
|
||||
backgroundImage:
|
||||
"url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
item.onClickSecondary?.();
|
||||
}}
|
||||
>
|
||||
DECLINE
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{item.type === "request_in" && (
|
||||
<>
|
||||
<button
|
||||
className={`px-6 h-12 flex items-center justify-center font-bold text-base outline-none uppercase tracking-widest mc-text-shadow ${isFocused ? "text-white shadow-md" : "text-gray-300"}`}
|
||||
@@ -775,6 +551,7 @@ const LceLiveView = memo(function LceLiveView({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={containerRef}
|
||||
@@ -786,7 +563,7 @@ const LceLiveView = memo(function LceLiveView({
|
||||
className="flex flex-col items-center justify-center w-full h-full absolute inset-0 outline-none p-12"
|
||||
>
|
||||
<div className="w-full max-w-5xl h-full flex flex-col mt-[4vh] mb-[4vh] relative drop-shadow-2xl">
|
||||
{currentTab !== "device_link" && (
|
||||
{isSignedIn && (
|
||||
<div
|
||||
className="flex z-10 space-x-2 px-12 relative w-full items-end"
|
||||
style={{ marginBottom: "-4px" }}
|
||||
@@ -819,7 +596,7 @@ const LceLiveView = memo(function LceLiveView({
|
||||
)}
|
||||
{t === "invites" && invites.length > 0 && (
|
||||
<span
|
||||
className={`ml-3 text-white text-base px-3 py-1 rounded-full shadow-inner border-2 font-normal ${currentTab === t ? "bg-[#30872a] border-[#1b5e16]" : "bg-[#23681d] border-[#111]"}`}
|
||||
className={`ml-3 text-white text-base px-3 py-1 rounded-full shadow-inner border-2 font-normal ${currentTab === t ? "bg-[#d72f2f] border-[#8a1a1a]" : "bg-[#a81f1f] border-[#111]"}`}
|
||||
>
|
||||
{invites.length}
|
||||
</span>
|
||||
@@ -842,14 +619,6 @@ const LceLiveView = memo(function LceLiveView({
|
||||
>
|
||||
{renderContent()}
|
||||
</div>
|
||||
<div className="flex justify-center pt-4 pb-2">
|
||||
<img
|
||||
src="/images/lcelive.png"
|
||||
alt="LCELive"
|
||||
className="h-5 opacity-70 cursor-pointer"
|
||||
onClick={() => TauriService.openUrl("https://lcelive.co.uk")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
@@ -891,7 +660,7 @@ const LceLiveView = memo(function LceLiveView({
|
||||
playPressSound();
|
||||
if (addFriendUsername.trim() !== "") {
|
||||
handleAction(() =>
|
||||
lceLiveService.sendFriendRequest(
|
||||
lceOnlineService.sendFriendRequest(
|
||||
addFriendUsername.trim(),
|
||||
),
|
||||
);
|
||||
@@ -920,83 +689,6 @@ const LceLiveView = memo(function LceLiveView({
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
{showHostMethodPicker && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-[105] flex items-center justify-center bg-black/80 backdrop-blur-sm outline-none border-none"
|
||||
>
|
||||
<div
|
||||
className="relative w-[420px] p-8 flex flex-col items-center shadow-2xl gap-4"
|
||||
style={{
|
||||
backgroundImage: "url('/images/frame_background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
<h2 className="text-[#FFFF55] text-3xl mc-text-shadow mb-2 border-b-2 border-[#373737] pb-2 w-full text-center uppercase tracking-widest">
|
||||
Host Game
|
||||
</h2>
|
||||
<button
|
||||
data-index={0}
|
||||
onMouseEnter={() => setFocusIndex(0)}
|
||||
onClick={handleHostRelay}
|
||||
className={`w-full h-14 flex items-center justify-center text-xl font-bold uppercase tracking-widest outline-none border-none ${focusIndex === 0 ? "text-[#FFFF55] mc-text-shadow" : "text-white mc-text-shadow hover:text-[#FFFF55]"}`}
|
||||
style={{
|
||||
backgroundImage:
|
||||
focusIndex === 0
|
||||
? "url('/images/button_highlighted.png')"
|
||||
: "url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
Relay
|
||||
</button>
|
||||
<button
|
||||
data-index={1}
|
||||
onMouseEnter={() => setFocusIndex(1)}
|
||||
onClick={handleHostDirect}
|
||||
className={`w-full h-14 flex items-center justify-center text-xl font-bold uppercase tracking-widest outline-none border-none ${focusIndex === 1 ? "text-[#FFFF55] mc-text-shadow" : "text-white mc-text-shadow hover:text-[#FFFF55]"}`}
|
||||
style={{
|
||||
backgroundImage:
|
||||
focusIndex === 1
|
||||
? "url('/images/button_highlighted.png')"
|
||||
: "url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
Direct (STUN)
|
||||
</button>
|
||||
<button
|
||||
data-index={2}
|
||||
onMouseEnter={() => setFocusIndex(2)}
|
||||
onClick={() => {
|
||||
setShowHostMethodPicker(false);
|
||||
setFocusIndex(0);
|
||||
playBackSound();
|
||||
}}
|
||||
className={`w-full h-14 flex items-center justify-center text-xl font-bold uppercase tracking-widest outline-none border-none ${focusIndex === 2 ? "text-[#FFFF55] mc-text-shadow" : "text-white mc-text-shadow hover:text-[#FFFF55]"}`}
|
||||
style={{
|
||||
backgroundImage:
|
||||
focusIndex === 2
|
||||
? "url('/images/button_highlighted.png')"
|
||||
: "url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
{errorModal && (
|
||||
<motion.div
|
||||
@@ -1034,21 +726,27 @@ const LceLiveView = memo(function LceLiveView({
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<ChooseInstanceModal
|
||||
isOpen={acceptInvite !== null}
|
||||
onClose={() => {
|
||||
setAcceptInvite(null);
|
||||
fetchSocialData();
|
||||
}}
|
||||
playPressSound={playPressSound}
|
||||
playBackSound={playBackSound}
|
||||
editions={editions}
|
||||
installs={installs}
|
||||
invite={acceptInvite}
|
||||
/>
|
||||
{joinTarget && (
|
||||
<ChooseInstanceModal
|
||||
isOpen={true}
|
||||
onClose={() => setJoinTarget(null)}
|
||||
playPressSound={playPressSound}
|
||||
playBackSound={playBackSound}
|
||||
editions={game.editions}
|
||||
installs={game.installs}
|
||||
invite={{
|
||||
inviteId: joinTarget.inviteid,
|
||||
from: joinTarget.hostName,
|
||||
hostIp: "",
|
||||
hostPort: 0,
|
||||
hostName: joinTarget.hostName,
|
||||
sessionId: joinTarget.sessionId,
|
||||
status: "pending",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
});
|
||||
|
||||
export default LceLiveView;
|
||||
export default LceOnlineView;
|
||||
@@ -83,7 +83,7 @@ interface ServerListing {
|
||||
id: string;
|
||||
owner: string;
|
||||
name: string;
|
||||
short_descripton: string
|
||||
short_descripton: string;
|
||||
description: string;
|
||||
discord?: string;
|
||||
version: string;
|
||||
@@ -510,17 +510,10 @@ const WorkshopView = memo(function WorkshopView({
|
||||
useEffect(() => {
|
||||
if (!workshopTarget || loading) return;
|
||||
|
||||
if (
|
||||
workshopTarget.type === "lceonline" && serverPlugins.length === 0
|
||||
)
|
||||
if (workshopTarget.type === "lceonline" && serverPlugins.length === 0)
|
||||
return;
|
||||
if (
|
||||
workshopTarget.type === "plugin" && pluginPackages.length === 0
|
||||
)
|
||||
return;
|
||||
if (
|
||||
workshopTarget.type === "version" && versionPackages.length === 0
|
||||
)
|
||||
if (workshopTarget.type === "plugin" && pluginPackages.length === 0) return;
|
||||
if (workshopTarget.type === "version" && versionPackages.length === 0)
|
||||
return;
|
||||
if (!workshopTarget.type && allPackages.length === 0) return;
|
||||
|
||||
@@ -1292,9 +1285,10 @@ function PackageModal({
|
||||
const needsUpdate =
|
||||
hasInstalled && installedEntries.some((e) => e.version !== pkg.version);
|
||||
const unresolvedDeps = useMemo(
|
||||
() => (pkg.dependencies || []).filter(
|
||||
(depId) => !installedPkgs.some((p) => p.packageId === depId),
|
||||
),
|
||||
() =>
|
||||
(pkg.dependencies || []).filter(
|
||||
(depId) => !installedPkgs.some((p) => p.packageId === depId),
|
||||
),
|
||||
[pkg.dependencies, installedPkgs],
|
||||
);
|
||||
const focusOptions: Array<"install" | "uninstall" | "close"> = isGameServerTab
|
||||
@@ -1351,9 +1345,7 @@ function PackageModal({
|
||||
["*.dll", "*"],
|
||||
);
|
||||
if (!path) return;
|
||||
const response = await fetch(
|
||||
`${pkg.download_url}`,
|
||||
);
|
||||
const response = await fetch(`${pkg.download_url}`);
|
||||
const blob = await response.blob();
|
||||
const buffer = await blob.arrayBuffer();
|
||||
await TauriService.writeBinaryFile(path, new Uint8Array(buffer));
|
||||
@@ -1498,7 +1490,13 @@ function PackageModal({
|
||||
Details
|
||||
</span>
|
||||
<div className="text-sm text-white mc-text-shadow leading-relaxed workshop-markdown">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw, [rehypeSanitize, defaultSchema]]}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[
|
||||
rehypeRaw,
|
||||
[rehypeSanitize, defaultSchema],
|
||||
]}
|
||||
>
|
||||
{pkg.extended_description}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
@@ -1685,7 +1683,9 @@ function PackageModal({
|
||||
<div className="flex flex-wrap gap-1.5 mt-1">
|
||||
{pkg.dependencies.map((depId) => {
|
||||
const dep = allPackages.find((p) => p.id === depId);
|
||||
const isDepInstalled = installedPkgs.some((p) => p.packageId === depId);
|
||||
const isDepInstalled = installedPkgs.some(
|
||||
(p) => p.packageId === depId,
|
||||
);
|
||||
return (
|
||||
<span
|
||||
key={depId}
|
||||
@@ -1711,9 +1711,12 @@ function PackageModal({
|
||||
<div className="flex flex-wrap gap-1.5 mt-1">
|
||||
{pkg.required_versions.map((verId) => {
|
||||
const builtin = BASE_EDITIONS.find((e) => e.id === verId);
|
||||
const versionPkg = versionPackages.find((v) => v.id === verId);
|
||||
const versionPkg = versionPackages.find(
|
||||
(v) => v.id === verId,
|
||||
);
|
||||
const customEd = customEditions.find((e) => e.id === verId);
|
||||
const displayName = builtin?.name || customEd?.name || versionPkg?.name;
|
||||
const displayName =
|
||||
builtin?.name || customEd?.name || versionPkg?.name;
|
||||
if (!displayName) return null;
|
||||
return (
|
||||
<span
|
||||
@@ -1937,8 +1940,16 @@ function InstallModal({
|
||||
isPluginTab?: boolean;
|
||||
}) {
|
||||
const game = useContext(GameContext);
|
||||
const availableEditions =
|
||||
const allInstalled =
|
||||
game?.editions.filter((e) => game.installs.includes(e.id)) || [];
|
||||
const availableEditions =
|
||||
pkg.required_versions && pkg.required_versions.length > 0
|
||||
? allInstalled.filter((e) =>
|
||||
pkg.required_versions!.some(
|
||||
(rv) => e.id === rv || e.id.startsWith(rv + "_"),
|
||||
),
|
||||
)
|
||||
: allInstalled;
|
||||
const [focusedIdx, setFocusedIdx] = useState(0);
|
||||
const [status, setStatus] = useState<
|
||||
"idle" | "installing" | "success" | "error"
|
||||
|
||||
@@ -230,4 +230,5 @@ export const SPLASHES = [
|
||||
"You're using asterisks wrong*",
|
||||
"Edgy humor is soo edgy",
|
||||
"I LOVE YURI!!!!!!! oh and yaoi ig",
|
||||
"RIP PrismaChunk0's Dog 2010-2026",
|
||||
];
|
||||
@@ -62,7 +62,7 @@ export function useDiscordRPC({
|
||||
devtools: "Developing for LCE",
|
||||
skins: "Changing Skins",
|
||||
workshop: "Browsing Workshop",
|
||||
lcelive: "Browsing Friends",
|
||||
lceonline: "Browsing Friends",
|
||||
"pck-editor": "Editing a PCK file",
|
||||
"options-editor": "Editing Options Files",
|
||||
"arc-editor": "Editing an ARC file",
|
||||
|
||||
@@ -71,12 +71,23 @@ export const BASE_EDITIONS = [
|
||||
id: "moon_edition",
|
||||
name: "Minecraft: Moon Edition",
|
||||
desc: "Galacticraft LCE port (Modded build!)",
|
||||
url: "https://INTENTIONALLY_INVALID_LINK_BECAUSE_FUCK_YOU_THATS_WHY", //neo: placeholder for now
|
||||
url: "https://github.com/blazin-blaze/moon-edition/releases/download/v1.0.1/moonEditionWindows64.zip",
|
||||
titleImage: "/images/minecraft_title_moon.png",
|
||||
supportsSlimSkins: false,
|
||||
logo: "/images/moonEdition.png",
|
||||
panorama: "moonedition",
|
||||
},
|
||||
{
|
||||
id: "lceonline",
|
||||
name: "LCE Online Client",
|
||||
desc: "Restoring the classic LCE online experience with friends, world hosting, leaderboards & more.",
|
||||
url: "https://github.com/lceonline/MCLEClient/releases/download/v1.0.0b/LCENWindows64.zip",
|
||||
titleImage: "/images/lceonline.png",
|
||||
supportsSlimSkins: false,
|
||||
logo: "/images/lce_online.png",
|
||||
panorama: "vanilla_tu19",
|
||||
lceOnline: true,
|
||||
},
|
||||
];
|
||||
|
||||
const PARTNERSHIP_SERVERS = [
|
||||
@@ -481,10 +492,18 @@ export function useGameManager({
|
||||
setIsGameRunning(true);
|
||||
try {
|
||||
getCurrentWindow().minimize();
|
||||
const currentEdition = editions.find((e) => e.instanceId === profile);
|
||||
await TauriService.launchGame(
|
||||
profile,
|
||||
PARTNERSHIP_SERVERS,
|
||||
extraLaunchArgs,
|
||||
currentEdition?.lceOnline
|
||||
? extraLaunchArgs!.concat([
|
||||
"-token",
|
||||
localStorage.getItem("lceonline_session")
|
||||
? JSON.parse(localStorage.getItem("lceonline_session")!).accessToken
|
||||
: "",
|
||||
])
|
||||
: extraLaunchArgs,
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
console.error(e);
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { lceLiveService, type FriendRequest, type GameInvite } from "../services/LceLiveService";
|
||||
export function useLceLiveNotifications() {
|
||||
const [friendRequestMessage, setFriendRequestMessage] = useState<string | null>(null);
|
||||
const [gameInviteMessage, setGameInviteMessage] = useState<string | null>(null);
|
||||
const seenRequests = useRef<Set<string>>(new Set());
|
||||
const seenInvites = useRef<Set<string>>(new Set());
|
||||
useEffect(() => {
|
||||
let pollInterval: ReturnType<typeof setInterval>;
|
||||
const init = async () => {
|
||||
if (lceLiveService.signedIn) {
|
||||
try {
|
||||
await lceLiveService.refreshSession();
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
if (lceLiveService.signedIn) {
|
||||
try {
|
||||
const [reqs, invs] = await Promise.all([
|
||||
lceLiveService.getPendingRequests(),
|
||||
lceLiveService.getGameInvites()
|
||||
]);
|
||||
reqs.incoming.forEach((r: FriendRequest) => seenRequests.current.add(r.accountId));
|
||||
invs.filter((i: GameInvite) => i.status === "pending").forEach((i: GameInvite) => seenInvites.current.add(i.inviteId));
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
pollInterval = setInterval(async () => {
|
||||
if (!lceLiveService.signedIn) return;
|
||||
try {
|
||||
const [reqs, invs] = await Promise.all([
|
||||
lceLiveService.getPendingRequests(),
|
||||
lceLiveService.getGameInvites()
|
||||
]);
|
||||
|
||||
reqs.incoming.forEach((r: FriendRequest) => {
|
||||
if (!seenRequests.current.has(r.accountId)) {
|
||||
seenRequests.current.add(r.accountId);
|
||||
setFriendRequestMessage(`New request from ${r.displayName}`);
|
||||
}
|
||||
});
|
||||
|
||||
invs.filter((i: GameInvite) => i.status === "pending").forEach((i: GameInvite) => {
|
||||
if (!seenInvites.current.has(i.inviteId)) {
|
||||
seenInvites.current.add(i.inviteId);
|
||||
const fromName = typeof i.from === 'string' ? "Unknown" : i.from.displayName;
|
||||
setGameInviteMessage(`${fromName} invited you to play!`);
|
||||
}
|
||||
});
|
||||
} catch (e) { }
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
init();
|
||||
return () => {
|
||||
if (pollInterval) clearInterval(pollInterval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
friendRequestMessage,
|
||||
gameInviteMessage,
|
||||
clearFriendRequestMessage: () => setFriendRequestMessage(null),
|
||||
clearGameInviteMessage: () => setGameInviteMessage(null),
|
||||
};
|
||||
}
|
||||
81
src/hooks/useLceOnlineNotifications.ts
Normal file
81
src/hooks/useLceOnlineNotifications.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { lceOnlineService } from "../services/LceOnlineService";
|
||||
export function useLceOnlineNotifications() {
|
||||
const [friendRequestMessage, setFriendRequestMessage] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [InviteMessage, setInviteMessage] = useState<string | null>(null);
|
||||
const [invites, setInvites] = useState<
|
||||
Array<{
|
||||
inviteid: string;
|
||||
from: { uuid: string; username: string };
|
||||
sessionid: string;
|
||||
}>
|
||||
>([]);
|
||||
const seenRequests = useRef<Set<string>>(new Set());
|
||||
const seenInvites = useRef<Set<string>>(new Set());
|
||||
useEffect(() => {
|
||||
let pollInterval: ReturnType<typeof setInterval>;
|
||||
|
||||
const poll = async () => {
|
||||
if (!lceOnlineService.signedIn) return;
|
||||
try {
|
||||
const lists = await lceOnlineService.getSocialLists();
|
||||
lists.requests.forEach((r: string) => {
|
||||
if (!seenRequests.current.has(r)) {
|
||||
seenRequests.current.add(r);
|
||||
setFriendRequestMessage(`New Friend request from ${r}`);
|
||||
}
|
||||
});
|
||||
} catch (e) {}
|
||||
try {
|
||||
const invitesData = await lceOnlineService.getInvites();
|
||||
setInvites(invitesData);
|
||||
invitesData.forEach((i) => {
|
||||
if (!seenInvites.current.has(i.inviteid)) {
|
||||
seenInvites.current.add(i.inviteid);
|
||||
setInviteMessage(`New invite from ${i.from.username}`);
|
||||
}
|
||||
});
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const init = async () => {
|
||||
if (lceOnlineService.signedIn) {
|
||||
try {
|
||||
const lists = await lceOnlineService.getSocialLists();
|
||||
lists.requests.forEach((r: string) => {
|
||||
if (!seenRequests.current.has(r)) {
|
||||
seenRequests.current.add(r);
|
||||
setFriendRequestMessage(`New Friend request from ${r}`);
|
||||
}
|
||||
});
|
||||
} catch (e) {}
|
||||
try {
|
||||
const invitesData = await lceOnlineService.getInvites();
|
||||
setInvites(invitesData);
|
||||
invitesData.forEach((i) => {
|
||||
if (!seenInvites.current.has(i.inviteid)) {
|
||||
seenInvites.current.add(i.inviteid);
|
||||
setInviteMessage(`New invite from ${i.from.username}`);
|
||||
}
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
pollInterval = setInterval(poll, 3000);
|
||||
};
|
||||
|
||||
init();
|
||||
return () => {
|
||||
if (pollInterval) clearInterval(pollInterval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
friendRequestMessage,
|
||||
InviteMessage,
|
||||
clearFriendRequestMessage: () => setFriendRequestMessage(null),
|
||||
clearInviteMessage: () => setInviteMessage(null),
|
||||
invites,
|
||||
};
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import OptionsEditorView from "../components/views/OptionsEditorView";
|
||||
import ModelEditorView from "../components/views/ModelEditorView";
|
||||
import ScreenshotsView from "../components/views/ScreenshotsView";
|
||||
import SwfView from "../components/views/SwfView";
|
||||
import LceLiveView from "../components/views/LceLiveView";
|
||||
import LceOnlineView from "../components/views/LceOnlineView";
|
||||
import CreditsView from "../components/views/CreditsView";
|
||||
import SkinViewer from "../components/common/SkinViewer";
|
||||
import PanoramaBackground from "../components/common/PanoramaBackground";
|
||||
@@ -35,7 +35,8 @@ import {
|
||||
useSkin,
|
||||
} from "../context/LauncherContext";
|
||||
import { TauriService } from "../services/TauriService";
|
||||
import { useLceLiveNotifications } from "../hooks/useLceLiveNotifications";
|
||||
import { lceOnlineService } from "../services/LceOnlineService";
|
||||
import { useLceOnlineNotifications } from "../hooks/useLceOnlineNotifications";
|
||||
import { usePluginViews } from "../plugins/PluginContext";
|
||||
import { usePlatform } from "../hooks/usePlatform";
|
||||
import { PluginManager } from "../plugins/PluginManager";
|
||||
@@ -67,12 +68,13 @@ export default function App() {
|
||||
const game = useGame();
|
||||
const skin = useSkin();
|
||||
const { skinUrl, setSkinUrl, capeUrl } = skin;
|
||||
const notifications = useLceLiveNotifications();
|
||||
const notifications = useLceOnlineNotifications();
|
||||
const {
|
||||
friendRequestMessage,
|
||||
gameInviteMessage,
|
||||
InviteMessage,
|
||||
clearFriendRequestMessage,
|
||||
clearGameInviteMessage,
|
||||
clearInviteMessage,
|
||||
invites
|
||||
} = notifications;
|
||||
const [showSetup, setShowSetup] = useState(false);
|
||||
const [isSetupChecked, setIsSetupChecked] = useState(false);
|
||||
@@ -157,16 +159,22 @@ export default function App() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
action === "lcelive" &&
|
||||
parts.length >= 2 &&
|
||||
parts[1] === "addfriend"
|
||||
) {
|
||||
const username = parsed.searchParams.get("username");
|
||||
if (username) {
|
||||
setActiveView("lcelive");
|
||||
setAddFriendTarget(username);
|
||||
return;
|
||||
if (action === "lceonline" && parts.length >= 2) {
|
||||
if (parts[1] === "auth") {
|
||||
const token = parsed.searchParams.get("token");
|
||||
if (token) {
|
||||
lceOnlineService.loginWithTokenAndFetchAccount(token);
|
||||
setActiveView("lceonline");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (parts[1] === "addfriend") {
|
||||
const username = parsed.searchParams.get("username");
|
||||
if (username) {
|
||||
setActiveView("lceonline");
|
||||
setAddFriendTarget(username);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -614,11 +622,12 @@ export default function App() {
|
||||
{activeView === "swf-editor" && (
|
||||
<SwfView key="swf-editor-view" />
|
||||
)}
|
||||
{activeView === "lcelive" && (
|
||||
<LceLiveView
|
||||
key="lcelive-view"
|
||||
{activeView === "lceonline" && (
|
||||
<LceOnlineView
|
||||
key="lceonline-view"
|
||||
addFriendTarget={addFriendTarget}
|
||||
onClearAddFriendTarget={() => setAddFriendTarget(null)}
|
||||
invites={invites}
|
||||
/>
|
||||
)}
|
||||
{activeView === "skins" && <SkinsView key="skins-view" />}
|
||||
@@ -662,18 +671,18 @@ export default function App() {
|
||||
onClose={clearFriendRequestMessage}
|
||||
onClick={() => {
|
||||
clearFriendRequestMessage();
|
||||
setActiveView("lcelive");
|
||||
setActiveView("lceonline");
|
||||
}}
|
||||
title="Friend Request"
|
||||
variant="update"
|
||||
/>
|
||||
|
||||
<AchievementToast
|
||||
message={gameInviteMessage}
|
||||
onClose={clearGameInviteMessage}
|
||||
message={InviteMessage}
|
||||
onClose={clearInviteMessage}
|
||||
onClick={() => {
|
||||
clearGameInviteMessage();
|
||||
setActiveView("lcelive");
|
||||
clearInviteMessage();
|
||||
setActiveView("lceonline");
|
||||
}}
|
||||
title="Game Invite"
|
||||
variant="update"
|
||||
|
||||
@@ -1,384 +0,0 @@
|
||||
const LOCAL_STORAGE_KEY = "lcelive_session";
|
||||
const DEFAULT_BASE_URL = "https://api.lcelive.co.uk";
|
||||
const MAX_REFRESH_RETRIES = 1;
|
||||
import { TauriService } from "./TauriService";
|
||||
export interface LceLiveAccount {
|
||||
accountId: string;
|
||||
username: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export interface SessionData {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
account: LceLiveAccount;
|
||||
}
|
||||
|
||||
export interface DeviceLinkStartResponse {
|
||||
deviceCode: string;
|
||||
userCode: string;
|
||||
verificationUri: string;
|
||||
verificationUriComplete?: string;
|
||||
intervalSeconds: number;
|
||||
expiresInSeconds?: number;
|
||||
}
|
||||
|
||||
export interface DeviceLinkPollResponse {
|
||||
status: string;
|
||||
isLinked?: boolean;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
account?: LceLiveAccount;
|
||||
}
|
||||
|
||||
export interface GameInvite {
|
||||
inviteId: string;
|
||||
from: LceLiveAccount | string;
|
||||
hostIp: string;
|
||||
hostPort: number;
|
||||
hostName: string;
|
||||
signalingSessionId?: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface FriendRequest {
|
||||
accountId: string;
|
||||
username: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export interface PendingRequests {
|
||||
incoming: FriendRequest[];
|
||||
outgoing: FriendRequest[];
|
||||
}
|
||||
|
||||
export class LceLiveService {
|
||||
private _session: SessionData | null = null;
|
||||
private baseUrl: string = DEFAULT_BASE_URL;
|
||||
private _refreshPromise: Promise<void> | null = null;
|
||||
|
||||
constructor() {
|
||||
this.loadSession();
|
||||
}
|
||||
|
||||
setBaseUrl(url: string) {
|
||||
this.baseUrl = url.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
get signedIn(): boolean {
|
||||
return this._session !== null;
|
||||
}
|
||||
|
||||
get account(): LceLiveAccount | null {
|
||||
return this._session?.account || null;
|
||||
}
|
||||
|
||||
get displayUsername(): string {
|
||||
if (!this._session) return "Not signed in";
|
||||
return (
|
||||
this._session.account.displayName ||
|
||||
this._session.account.username ||
|
||||
"Unknown"
|
||||
);
|
||||
}
|
||||
|
||||
get apiBaseUrl(): string {
|
||||
return this.baseUrl;
|
||||
}
|
||||
|
||||
get accessToken(): string | null {
|
||||
return this._session?.accessToken || null;
|
||||
}
|
||||
|
||||
public generateDeviceId(): string {
|
||||
let id = localStorage.getItem("lcelive_device_id");
|
||||
if (!id) {
|
||||
id = crypto.randomUUID();
|
||||
localStorage.setItem("lcelive_device_id", id);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
public getDeviceName(): string {
|
||||
return "LCE Emerald Launcher";
|
||||
}
|
||||
|
||||
private loadSession() {
|
||||
try {
|
||||
const data = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||
if (data) {
|
||||
this._session = JSON.parse(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to load LceLive session", e);
|
||||
}
|
||||
}
|
||||
|
||||
private saveSession() {
|
||||
if (this._session) {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(this._session));
|
||||
} else {
|
||||
localStorage.removeItem(LOCAL_STORAGE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
private async request<T = any>(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: unknown,
|
||||
authed: boolean = true,
|
||||
retryCount: number = 0,
|
||||
): Promise<T> {
|
||||
if (authed && this._session?.refreshToken && retryCount === 0) {
|
||||
try {
|
||||
await this.refreshSession(); //neo: i do this on every request only because it doesnt always return 401
|
||||
} catch (err) {
|
||||
this.logoutLocal();
|
||||
throw new Error("Session expired. Please log in again.");
|
||||
}
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Accept: "application/json",
|
||||
"User-Agent": "MCLCE-LceLive/1.0",
|
||||
};
|
||||
|
||||
if (body) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
|
||||
if (authed && this._session?.accessToken) {
|
||||
headers["Authorization"] = `Bearer ${this._session.accessToken}`;
|
||||
}
|
||||
|
||||
const bodyStr = body ? JSON.stringify(body) : null;
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await TauriService.httpProxyRequest(
|
||||
method,
|
||||
`${this.baseUrl}${path}`,
|
||||
bodyStr,
|
||||
headers,
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(`Network error when calling ${path}: ${e}`);
|
||||
}
|
||||
|
||||
if (
|
||||
res.status === 401 &&
|
||||
authed &&
|
||||
this._session?.refreshToken &&
|
||||
retryCount < MAX_REFRESH_RETRIES
|
||||
) {
|
||||
try {
|
||||
await this.refreshSession();
|
||||
return this.request(method, path, body, authed, retryCount + 1);
|
||||
} catch (err) {
|
||||
this.logoutLocal();
|
||||
throw new Error("Session expired. Please log in again.");
|
||||
}
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = res.body ? JSON.parse(res.body) : {};
|
||||
} catch {
|
||||
data = { message: res.body };
|
||||
}
|
||||
|
||||
if (res.status >= 400) {
|
||||
const errorMsg =
|
||||
data.message ||
|
||||
data.detail ||
|
||||
data.title ||
|
||||
data.error ||
|
||||
`HTTP ${res.status}`;
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async startDeviceLink(): Promise<DeviceLinkStartResponse> {
|
||||
return this.request<DeviceLinkStartResponse>(
|
||||
"POST",
|
||||
"/api/auth/device/start",
|
||||
{
|
||||
deviceId: this.generateDeviceId(),
|
||||
deviceName: this.getDeviceName(),
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async pollDeviceLink(deviceCode: string): Promise<DeviceLinkPollResponse> {
|
||||
const data = await this.request<DeviceLinkPollResponse>(
|
||||
"GET",
|
||||
`/api/auth/device/poll/${deviceCode}`,
|
||||
null,
|
||||
false,
|
||||
);
|
||||
if (data.isLinked && data.accessToken) {
|
||||
this._session = {
|
||||
accessToken: data.accessToken,
|
||||
refreshToken: data.refreshToken ?? "",
|
||||
account: data.account ?? {
|
||||
accountId: "",
|
||||
username: "",
|
||||
displayName: "",
|
||||
},
|
||||
};
|
||||
this.saveSession();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async refreshSession(): Promise<void> {
|
||||
if (!this._session?.refreshToken) throw new Error("No refresh token");
|
||||
|
||||
if (this._refreshPromise) {
|
||||
return this._refreshPromise;
|
||||
}
|
||||
|
||||
this._refreshPromise = (async () => {
|
||||
try {
|
||||
const data = await this.request(
|
||||
"POST",
|
||||
"/api/auth/refresh",
|
||||
{
|
||||
refreshToken: this._session!.refreshToken,
|
||||
},
|
||||
false,
|
||||
);
|
||||
this._session = {
|
||||
accessToken: data.accessToken,
|
||||
refreshToken: data.refreshToken,
|
||||
account: data.account,
|
||||
};
|
||||
this.saveSession();
|
||||
} finally {
|
||||
this._refreshPromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return this._refreshPromise;
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
if (this._session?.refreshToken) {
|
||||
try {
|
||||
await this.request(
|
||||
"POST",
|
||||
"/api/auth/logout",
|
||||
{
|
||||
refreshToken: this._session.refreshToken,
|
||||
},
|
||||
true,
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn("Server logout failed", e);
|
||||
}
|
||||
}
|
||||
this.logoutLocal();
|
||||
}
|
||||
|
||||
logoutLocal(): void {
|
||||
this._session = null;
|
||||
this.saveSession();
|
||||
}
|
||||
|
||||
async getFriends(): Promise<LceLiveAccount[]> {
|
||||
const data = await this.request("GET", "/api/social/friends");
|
||||
return data.friends || [];
|
||||
}
|
||||
|
||||
async removeFriend(accountId: string): Promise<void> {
|
||||
await this.request("DELETE", `/api/social/friends/${accountId}`);
|
||||
}
|
||||
|
||||
async sendFriendRequest(username: string): Promise<void> {
|
||||
await this.request("POST", "/api/social/request", { username });
|
||||
}
|
||||
|
||||
async getPendingRequests(): Promise<PendingRequests> {
|
||||
const data = await this.request("GET", "/api/social/requests");
|
||||
return {
|
||||
incoming: (data.incoming || []).map((r: Record<string, string>) => ({
|
||||
accountId: r.requesterUserId || r.accountId || r.userId,
|
||||
username: r.requesterUsername || r.username,
|
||||
displayName: r.requesterDisplayName || r.displayName,
|
||||
})),
|
||||
outgoing: (data.outgoing || []).map((r: Record<string, string>) => ({
|
||||
accountId: r.targetUserId || r.accountId || r.userId,
|
||||
username: r.targetUsername || r.username,
|
||||
displayName: r.targetDisplayName || r.displayName,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async acceptFriendRequest(accountId: string): Promise<void> {
|
||||
await this.request("POST", `/api/social/requests/${accountId}/accept`, {});
|
||||
}
|
||||
|
||||
async declineFriendRequest(accountId: string): Promise<void> {
|
||||
await this.request("POST", `/api/social/requests/${accountId}/decline`, {});
|
||||
}
|
||||
|
||||
async getGameInvites(): Promise<GameInvite[]> {
|
||||
const data = await this.request("GET", "/api/sessions/invites");
|
||||
const incoming = data.incoming || [];
|
||||
return incoming.map((inv: Record<string, unknown>) => ({
|
||||
inviteId: inv.inviteId,
|
||||
from: {
|
||||
accountId: inv.senderAccountId,
|
||||
username: inv.senderUsername,
|
||||
displayName: inv.senderDisplayName,
|
||||
},
|
||||
hostIp: inv.hostIp,
|
||||
hostPort: inv.hostPort,
|
||||
hostName: inv.hostName,
|
||||
status: inv.status,
|
||||
signalingSessionId: inv.signalingSessionId,
|
||||
}));
|
||||
}
|
||||
|
||||
async sendGameInvite(
|
||||
recipientAccountId: string,
|
||||
hostIp: string,
|
||||
hostPort: number,
|
||||
hostName: string,
|
||||
signalingSessionId?: string,
|
||||
): Promise<void> {
|
||||
await this.request("POST", "/api/sessions/invites", {
|
||||
recipientAccountId,
|
||||
hostIp,
|
||||
hostPort,
|
||||
hostName,
|
||||
signalingSessionId,
|
||||
});
|
||||
}
|
||||
|
||||
async acceptGameInvite(inviteId: string): Promise<Record<string, unknown>> {
|
||||
return this.request("POST", `/api/sessions/invites/${inviteId}/accept`, {});
|
||||
}
|
||||
|
||||
async declineGameInvite(inviteId: string): Promise<void> {
|
||||
await this.request("POST", `/api/sessions/invites/${inviteId}/decline`, {});
|
||||
}
|
||||
|
||||
async deactivateGameInvites(): Promise<void> {
|
||||
await this.request("POST", "/api/sessions/invites/deactivate", {});
|
||||
}
|
||||
|
||||
async requestJoinTicket(): Promise<string> {
|
||||
const data = await this.request("POST", "/api/sessions/ticket", {});
|
||||
return data.ticket;
|
||||
}
|
||||
|
||||
async validateJoinTicket(ticket: string): Promise<LceLiveAccount> {
|
||||
return this.request("POST", "/api/sessions/validate", { ticket }, false);
|
||||
}
|
||||
}
|
||||
|
||||
export const lceLiveService = new LceLiveService();
|
||||
282
src/services/LceOnlineService.ts
Normal file
282
src/services/LceOnlineService.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
const SESSION_KEY = "lceonline_session";
|
||||
const SOCIAL_BASE_URL = "https://social.mclegacyedition.xyz";
|
||||
const AUTH_BASE_URL = "https://auth.mclegacyedition.xyz"; //neo: yeah bro im hardcoding all three
|
||||
import { TauriService } from "./TauriService";
|
||||
export interface LceOnlineAccount {
|
||||
username: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export interface SessionData {
|
||||
accessToken: string;
|
||||
account: LceOnlineAccount;
|
||||
}
|
||||
|
||||
export interface FriendRequest {
|
||||
username: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export class LceOnlineService {
|
||||
private _session: SessionData | null = null;
|
||||
private baseUrl: string = SOCIAL_BASE_URL;
|
||||
private _listeners: Array<() => void> = [];
|
||||
constructor() {
|
||||
this.loadSession();
|
||||
}
|
||||
|
||||
onSessionChange(listener: () => void): () => void {
|
||||
this._listeners.push(listener);
|
||||
return () => {
|
||||
this._listeners = this._listeners.filter((l) => l !== listener);
|
||||
};
|
||||
}
|
||||
|
||||
private _notify() {
|
||||
for (const l of this._listeners) l();
|
||||
}
|
||||
|
||||
get signedIn(): boolean {
|
||||
return this._session !== null;
|
||||
}
|
||||
|
||||
get account(): LceOnlineAccount | null {
|
||||
return this._session?.account || null;
|
||||
}
|
||||
|
||||
get displayUsername(): string {
|
||||
if (!this._session) return "Not signed in";
|
||||
return (
|
||||
this._session.account.displayName ||
|
||||
this._session.account.username ||
|
||||
"Unknown"
|
||||
);
|
||||
}
|
||||
|
||||
get accessToken(): string | null {
|
||||
return this._session?.accessToken || null;
|
||||
}
|
||||
|
||||
logoutLocal(): void {
|
||||
this._session = null;
|
||||
this.saveSession();
|
||||
this._notify();
|
||||
}
|
||||
|
||||
async login(username: string, password: string): Promise<void> {
|
||||
const res = await this.request<string>(
|
||||
"POST",
|
||||
"/login",
|
||||
`${username}:${password}`,
|
||||
AUTH_BASE_URL,
|
||||
);
|
||||
const text = typeof res === "string" ? res : "";
|
||||
if (!text.startsWith("-")) {
|
||||
throw new Error(text || "Login failed");
|
||||
}
|
||||
const token = text.split(":")[1];
|
||||
await this.loginWithTokenAndFetchAccount(token);
|
||||
}
|
||||
|
||||
async register(username: string, password: string): Promise<void> {
|
||||
const res = await this.request<string>(
|
||||
"POST",
|
||||
"/register",
|
||||
`${username}:${password}`,
|
||||
AUTH_BASE_URL,
|
||||
);
|
||||
const text = typeof res === "string" ? res : "";
|
||||
if (!text.startsWith("-")) {
|
||||
throw new Error(text || "Registration failed");
|
||||
}
|
||||
const token = text.split(":")[1];
|
||||
await this.loginWithTokenAndFetchAccount(token);
|
||||
}
|
||||
|
||||
loginWithToken(token: string, username?: string) {
|
||||
const name = username || "Player";
|
||||
this._session = {
|
||||
accessToken: token,
|
||||
account: { username: name, displayName: name },
|
||||
};
|
||||
this.saveSession();
|
||||
this._notify();
|
||||
}
|
||||
|
||||
async loginWithTokenAndFetchAccount(token: string): Promise<void> {
|
||||
this._session = {
|
||||
accessToken: token,
|
||||
account: { username: "Player", displayName: "Player" },
|
||||
};
|
||||
this.saveSession();
|
||||
this._notify();
|
||||
try {
|
||||
const raw: string = await this.request<string>("POST", "/accountinfo");
|
||||
if (typeof raw === "string" && raw.startsWith("-")) {
|
||||
const username = raw.slice(1);
|
||||
this._session!.account = { username, displayName: username };
|
||||
this.saveSession();
|
||||
this._notify();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to fetch account info", e);
|
||||
}
|
||||
}
|
||||
|
||||
private loadSession() {
|
||||
try {
|
||||
const data = localStorage.getItem(SESSION_KEY);
|
||||
if (data) {
|
||||
this._session = JSON.parse(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to load LCE Online session", e);
|
||||
}
|
||||
}
|
||||
|
||||
private saveSession() {
|
||||
if (this._session) {
|
||||
localStorage.setItem(SESSION_KEY, JSON.stringify(this._session));
|
||||
} else {
|
||||
localStorage.removeItem(SESSION_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
private async request<T = any>(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: string | null,
|
||||
baseUrl?: string,
|
||||
): Promise<T> {
|
||||
const headers: Record<string, string> = {
|
||||
Accept: "text/plain, application/json",
|
||||
"User-Agent": "MCLCE-LCEOnline/1.0",
|
||||
};
|
||||
|
||||
if (body) {
|
||||
headers["Content-Type"] = "text/plain";
|
||||
}
|
||||
|
||||
if (this._session?.accessToken) {
|
||||
headers["Authorization"] = `Bearer ${this._session.accessToken}`;
|
||||
}
|
||||
|
||||
const url = `${baseUrl || this.baseUrl}${path}`;
|
||||
let res;
|
||||
try {
|
||||
res = await TauriService.httpProxyRequest(
|
||||
method,
|
||||
url,
|
||||
body ?? null,
|
||||
headers,
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(`Network error when calling ${path}: ${e}`);
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = res.body ? JSON.parse(res.body) : {};
|
||||
} catch {
|
||||
data = res.body ?? {};
|
||||
}
|
||||
|
||||
if (res.status >= 400) {
|
||||
const errorMsg =
|
||||
data.message ||
|
||||
data.detail ||
|
||||
data.title ||
|
||||
data.error ||
|
||||
data ||
|
||||
`HTTP ${res.status}`;
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async getSocialLists(): Promise<{
|
||||
friends: string[];
|
||||
requests: string[];
|
||||
blocked: string[];
|
||||
}> {
|
||||
const raw: string = await this.request<string>(
|
||||
"POST",
|
||||
"/getSocialLists",
|
||||
null,
|
||||
);
|
||||
if (typeof raw !== "string") {
|
||||
return { friends: [], requests: [], blocked: [] };
|
||||
}
|
||||
const withoutPrefix = raw.startsWith("-") ? raw.slice(1) : raw;
|
||||
const parts = withoutPrefix.split("|");
|
||||
return {
|
||||
friends: parts[0] ? parts[0].split(",").filter(Boolean) : [],
|
||||
requests: parts[1] ? parts[1].split(",").filter(Boolean) : [],
|
||||
blocked: parts[2] ? parts[2].split(",").filter(Boolean) : [],
|
||||
};
|
||||
}
|
||||
|
||||
async sendFriendRequest(target: string): Promise<void> {
|
||||
const res = await this.request<string>("POST", "/sendrequest", target);
|
||||
if (typeof res === "string" && res !== "Successfully Sent Friend Request") {
|
||||
throw new Error(res);
|
||||
}
|
||||
}
|
||||
|
||||
async acceptFriendRequest(from: string): Promise<void> {
|
||||
const res = await this.request<string>("POST", "/acceptrequest", from);
|
||||
if (typeof res === "string" && res !== "1") {
|
||||
throw new Error(res);
|
||||
}
|
||||
}
|
||||
|
||||
async declineFriendRequest(from: string): Promise<void> {
|
||||
const res = await this.request<string>("POST", "/declinerequest", from);
|
||||
if (typeof res === "string" && res !== "1") {
|
||||
throw new Error(res);
|
||||
}
|
||||
}
|
||||
|
||||
async removeFriend(from: string): Promise<void> {
|
||||
const res = await this.request<string>("POST", "/removefriend", from);
|
||||
if (typeof res === "string") {
|
||||
throw new Error(res);
|
||||
}
|
||||
}
|
||||
|
||||
async sendInvite(target: string): Promise<void> {
|
||||
const res = await this.request<string>("POST", "/invite", target);
|
||||
if (typeof res === "string" && res !== "Invite Sent") {
|
||||
throw new Error(res);
|
||||
}
|
||||
}
|
||||
|
||||
async acceptInvite(from: string): Promise<string> {
|
||||
const res = await this.request<string>("POST", "/acceptinvite", from);
|
||||
if (typeof res !== "string") throw new Error("Failed to accept invite");
|
||||
return res;
|
||||
}
|
||||
|
||||
async declineInvite(from: string): Promise<void> {
|
||||
try {
|
||||
await this.request("POST", "/declineinvite", from);
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : "";
|
||||
if (msg !== "Declined Invite") throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async getInvites(): Promise<
|
||||
Array<{
|
||||
inviteid: string;
|
||||
from: { uuid: string; username: string };
|
||||
sessionid: string;
|
||||
}>
|
||||
> {
|
||||
const res = await this.request<string>("GET", "/getinvites", null);
|
||||
return Array.isArray(res) ? res : [];
|
||||
}
|
||||
}
|
||||
|
||||
export const lceOnlineService = new LceOnlineService();
|
||||
@@ -300,31 +300,19 @@ export class TauriService {
|
||||
return invoke("stun_discover");
|
||||
}
|
||||
|
||||
static async startDirectProxy(
|
||||
targetIp: string,
|
||||
targetPort: number,
|
||||
): Promise<number> {
|
||||
return invoke("start_direct_proxy", { targetIp, targetPort });
|
||||
}
|
||||
|
||||
static async startRelayProxy(
|
||||
apiBaseUrl: string,
|
||||
accessToken: string,
|
||||
sessionId: string,
|
||||
authToken: string,
|
||||
targetSession: string,
|
||||
): Promise<number> {
|
||||
return invoke("start_relay_proxy", { apiBaseUrl, accessToken, sessionId });
|
||||
return invoke("start_relay_proxy", { authToken, targetSession });
|
||||
}
|
||||
|
||||
static async startHostRelay(
|
||||
apiBaseUrl: string,
|
||||
accessToken: string,
|
||||
sessionId: string,
|
||||
authToken: string,
|
||||
gamePort: number,
|
||||
): Promise<void> {
|
||||
return invoke("start_host_relay", {
|
||||
apiBaseUrl,
|
||||
accessToken,
|
||||
sessionId,
|
||||
authToken,
|
||||
gamePort,
|
||||
});
|
||||
}
|
||||
@@ -339,18 +327,18 @@ export class TauriService {
|
||||
|
||||
static async joinGame(
|
||||
apiBaseUrl: string,
|
||||
accessToken: string,
|
||||
authToken: string,
|
||||
hostIp: string,
|
||||
hostPort: number,
|
||||
sessionId: string,
|
||||
targetSession: string,
|
||||
instanceId: string,
|
||||
): Promise<void> {
|
||||
return invoke("join_game", {
|
||||
apiBaseUrl,
|
||||
accessToken,
|
||||
authToken,
|
||||
hostIp,
|
||||
hostPort,
|
||||
sessionId,
|
||||
targetSession,
|
||||
instanceId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface Edition {
|
||||
comingSoon?: boolean;
|
||||
category?: string[];
|
||||
officialDLC?: string;
|
||||
lceOnline?: boolean;
|
||||
}
|
||||
|
||||
export interface CustomEditionInput {
|
||||
|
||||
Reference in New Issue
Block a user