OneLauncher-Cracked/packages/oneclient_app/src/events.rs
Maciek fb33693f7d
feat: launcher data folder (#767)
* feat: added disk choice on fresh install

* feat: added protection for corrupted/deleted settings.json

* feat: added migration to another drive for already existing users; changed the default directories

* feat: relocation screen

---------

Co-authored-by: LynithDev <61880709+LynithDev@users.noreply.github.com>
2026-09-02 14:07:20 +02:00

376 lines
13 KiB
Rust

//! Runs under freya's `spawn` not `tokio::spawn` radio state is `!Send`
//! Events are batched and folded because a radio write wakes every subscriber
use std::collections::{HashMap, HashSet};
use std::time::Duration;
use freya::prelude::spawn_forever;
use freya::radio::RadioStation;
use oneclient_events::{Event, EventReceiver, GameEvent, LaunchStage, ProgressEvent, Signal};
use tokio::sync::mpsc;
use crate::hooks::PumpSignal;
use crate::notifications::{MESSAGE_TOAST_TTL, PendingPromptView};
use crate::state::{AppChannel, AppState, LoginProgress};
/// Quiet period before log lines are written without it every line wakes the log view
const GAME_LOG_FLUSH: Duration = Duration::from_millis(120);
const COALESCE_BUDGET: usize = 1024;
pub struct EventPump {
pub events: EventReceiver,
pub signals: mpsc::UnboundedReceiver<PumpSignal>,
pub station: RadioStation<AppState, AppChannel>,
}
impl EventPump {
pub async fn run(mut self) {
let mut armed: HashMap<u64, ToastTimer> = HashMap::new();
// Hovering any toast pauses every toast including ones arriving while hovering
let mut paused = false;
let mut game_flush: Option<tokio::time::Instant> = None;
loop {
let next_deadline = armed
.values()
.filter_map(ToastTimer::deadline)
.min()
.into_iter()
.chain(game_flush)
.min();
let timer = async {
match next_deadline {
Some(deadline) => tokio::time::sleep_until(deadline).await,
None => std::future::pending::<()>().await,
}
};
tokio::select! {
() = timer => {
let now = tokio::time::Instant::now();
let due: Vec<u64> = armed
.iter()
.filter(|(_, t)| t.deadline().is_some_and(|d| d <= now))
.map(|(id, _)| *id)
.collect();
if !due.is_empty() {
{
let mut guard = self.station.write_channel(AppChannel::Notifications);
let AppState {
notifications, inbox, ..
} = &mut **guard;
for id in &due {
notifications.expire_toast(inbox, *id);
}
}
for id in due {
armed.remove(&id);
}
reconcile(&self.station, &mut armed, paused);
}
if game_flush.is_some_and(|d| d <= now) {
game_flush = None;
// Lines were already folded in on arrival this only ends the quiet period
self.station.write_channel(AppChannel::Game);
}
}
event = self.events.recv() => {
let Some(first) = event else { break };
let mut batch = vec![first];
while batch.len() < COALESCE_BUDGET {
match self.events.try_recv() {
Ok(next) => batch.push(next),
Err(_) => break,
}
}
let folded = self.apply(batch);
if folded.touched_engine {
reconcile(&self.station, &mut armed, paused);
}
if folded.saw_game_log && game_flush.is_none() {
game_flush = Some(tokio::time::Instant::now() + GAME_LOG_FLUSH);
}
}
signal = self.signals.recv() => {
let Some(signal) = signal else { break };
match signal {
PumpSignal::Reconcile => reconcile(&self.station, &mut armed, paused),
PumpSignal::PauseToasts => {
paused = true;
for timer in armed.values_mut() {
timer.pause();
}
}
PumpSignal::ResumeToasts => {
paused = false;
for timer in armed.values_mut() {
timer.resume();
}
}
}
}
}
}
}
fn apply(&mut self, batch: Vec<Event>) -> Folded {
let mut folded = Folded::default();
let mut engine_events = Vec::new();
let mut stages: Vec<(i64, LaunchStage)> = Vec::new();
let mut logs: Vec<(i64, String)> = Vec::new();
let mut failed: Option<(i64, String)> = None;
let mut login: Option<Option<LoginProgress>> = None;
let mut sync_complete = false;
for event in batch {
match event {
Event::Signal(Signal::ClustersChanged) => folded.clusters = true,
Event::Signal(Signal::JavaChanged) => folded.java = true,
Event::Signal(Signal::SyncComplete) => {
sync_complete = true;
folded.clusters = true;
}
Event::Game(GameEvent::Stage { cluster_id, stage }) => {
stages.push((cluster_id, stage));
}
Event::Game(GameEvent::Log { cluster_id, line }) => {
folded.saw_game_log = true;
logs.push((cluster_id, line));
}
Event::Game(GameEvent::Failed {
cluster_id,
message,
}) => failed = Some((cluster_id, message)),
// Lifted out so it never reaches the engine the sign-in modal renders it inline
Event::Progress(ProgressEvent::Update {
id,
ref label,
current,
total,
}) if id == oneclient_auth::MICROSOFT_LOGIN_PROGRESS => {
login = Some((current < total).then(|| LoginProgress {
label: label.clone(),
current,
total,
}));
}
other => engine_events.push(other),
}
}
if !stages.is_empty() || !logs.is_empty() || failed.is_some() {
let mut guard = self.station.write_channel(AppChannel::Game);
for (cluster_id, stage) in stages {
guard.game.stages.insert(cluster_id, stage);
if stage == LaunchStage::Checking {
guard.game.error = None;
guard.game.logs.insert(cluster_id, std::sync::Arc::new(Vec::new()));
}
}
for (cluster_id, line) in logs {
let buffer = guard.game.logs.entry(cluster_id).or_default();
std::sync::Arc::make_mut(buffer).push(std::sync::Arc::from(line));
}
if let Some((cluster_id, message)) = failed {
guard.game.stages.insert(cluster_id, LaunchStage::Exited);
guard.game.error = Some(message);
}
}
if sync_complete {
self.station
.write_channel(AppChannel::Launcher)
.launcher
.fetching = false;
}
if let Some(progress) = login {
self.station
.write_channel(AppChannel::MicrosoftLogin)
.microsoft_login = progress;
}
if !engine_events.is_empty() {
folded.touched_engine = true;
let mut guard = self.station.write_channel(AppChannel::Notifications);
let AppState {
notifications,
inbox,
prompt,
..
} = &mut **guard;
for event in engine_events {
if let (_timers, Some(pending)) = notifications.dispatch(inbox, event) {
*prompt = Some(pending);
}
}
}
if folded.clusters {
spawn_forever(async move { crate::hooks::invalidate_cluster_queries().await });
}
if folded.java {
spawn_forever(async move { crate::hooks::invalidate_java_queries().await });
}
folded
}
}
#[derive(Default)]
struct Folded {
touched_engine: bool,
saw_game_log: bool,
clusters: bool,
java: bool,
}
fn reconcile(
station: &RadioStation<AppState, AppChannel>,
armed: &mut HashMap<u64, ToastTimer>,
paused: bool,
) {
let state = station.peek();
let want: HashSet<u64> = state
.notifications
.active_toast_entry_ids()
.into_iter()
.filter(|id| state.inbox.iter().any(|e| e.id == *id && !e.is_loading))
.collect();
for id in &want {
armed.entry(*id).or_insert_with(|| ToastTimer::armed(paused));
}
armed.retain(|id, _| want.contains(id));
}
enum ToastTimer {
Running(tokio::time::Instant),
Paused(Duration),
}
impl ToastTimer {
fn armed(paused: bool) -> Self {
if paused {
Self::Paused(MESSAGE_TOAST_TTL)
} else {
Self::Running(tokio::time::Instant::now() + MESSAGE_TOAST_TTL)
}
}
/// `None` while paused
fn deadline(&self) -> Option<tokio::time::Instant> {
match self {
Self::Running(deadline) => Some(*deadline),
Self::Paused(_) => None,
}
}
fn pause(&mut self) {
if let Self::Running(deadline) = self {
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
*self = Self::Paused(remaining);
}
}
fn resume(&mut self) {
if let Self::Paused(remaining) = self {
let remaining = *remaining;
*self = Self::Running(tokio::time::Instant::now() + remaining);
}
}
}
#[must_use]
pub fn prompt_view(state: &AppState) -> Option<PendingPromptView> {
state.prompt.as_ref().map(|prompt| PendingPromptView {
title: prompt.title.clone(),
question: prompt.question.clone(),
choices: prompt.choices.clone(),
dismiss: prompt.dismiss.clone(),
})
}
pub async fn start_launcher(
station: RadioStation<AppState, AppChannel>,
events: oneclient_events::EventBus,
) -> Result<(), anyhow::Error> {
let state = crate::launcher::install(oneclient_core::LauncherState::new(events).await?);
oneclient_net::status::start(state.services.requester.clone());
oneclient_polyplus::start(std::sync::Arc::clone(&state.auth));
oneclient_core::run_startup_tasks(&state);
let data_dir = oneclient_common::paths::data_dir()
.map(|p| p.display().to_string())
.unwrap_or_default();
let settings = state.settings.read().clone();
let auto_update = settings.auto_update;
let mut station = station;
{
let mut guard = station.write_channel(AppChannel::Launcher);
guard.launcher = crate::state::LauncherInit {
ready: true,
fetching: true,
syncing_bundles: true,
error: None,
snapshots: 0,
data_dir,
needs_location: false,
};
}
{
let mut guard = station.write_channel(AppChannel::Settings);
guard.settings.settings = settings;
guard.settings.status = crate::state::AsyncStatus::Ready;
guard.settings.error = None;
}
crate::hooks::invalidate_profile_queries().await;
crate::updater::spawn_update_check(auto_update, state.services.events.clone());
Ok(())
}
pub fn report_startup_failure(
station: &RadioStation<AppState, AppChannel>,
err: &anyhow::Error,
) {
let message = err.to_string();
tracing::error!("launcher init failed: {err:#}");
let data_dir = oneclient_common::paths::data_dir()
.map(|p| p.display().to_string())
.unwrap_or_default();
let mut station = *station;
{
let mut guard = station.write_channel(AppChannel::Launcher);
guard.launcher.error = Some(message.clone());
guard.launcher.data_dir = data_dir;
guard.launcher.snapshots = crate::recovery::snapshots().len();
}
let mut guard = station.write_channel(AppChannel::Notifications);
let AppState {
notifications, inbox, ..
} = &mut **guard;
notifications.dispatch(
inbox,
Event::Notification(oneclient_events::Notification::Message(
oneclient_events::Message {
title: "Launcher failed to start".into(),
body: message,
level: oneclient_events::Level::Error,
},
)),
);
}