Skip to content

Commit

Permalink
feat: add ability to see game logs in the UI
Browse files Browse the repository at this point in the history
  • Loading branch information
Umatriz committed Jul 23, 2024
1 parent 443f4e3 commit 40a1db8
Show file tree
Hide file tree
Showing 14 changed files with 214 additions and 25 deletions.
17 changes: 15 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ panic = "abort"

[workspace.dependencies]
async-trait = "0.1.73"
tokio = { version = "1.28.2", features = ["rt", "macros", "process"] }
tokio = { version = "1.38.1", features = ["rt", "macros", "process"] }
tokio-stream = "0.1.15"
tokio-util = "0.7.11"

itertools = "0.13.0"
typed-builder = "0.18.2"
Expand Down
13 changes: 7 additions & 6 deletions crates/client/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ use crate::{
errors_pool::ErrorPoolExt,
states::States,
subscriber::EguiLayer,
views::{self, profiles::ProfilesPage, settings::SettingsPage, ModManager, ProfileInfo, View},
views::{self, profiles::ProfilesPage, settings::SettingsPage, Logs, ModManager, ProfileInfo, View},
Tab, TabKind,
};
use eframe::egui::{self, ScrollArea};
use eframe::egui::{self};
use egui_dock::TabViewer;
use egui_file_dialog::FileDialog;
use egui_task_manager::TaskManager;
Expand Down Expand Up @@ -82,6 +82,7 @@ impl TabViewer for MyContext {
profiles_state: &mut self.states.profiles,
menu_state: &mut self.states.add_profile_menu_state,
tabs_state: &mut self.states.tabs,
logs_state: &self.states.logs_state,

launcher_manifest: self.launcher_manifest,
is_profile_window_open: &mut self.is_profile_window_open,
Expand All @@ -95,11 +96,11 @@ impl TabViewer for MyContext {
file_dialog: &mut self.file_dialog,
}
.ui(ui),
TabKind::Logs => {
ScrollArea::horizontal().show(ui, |ui| {
self.egui_layer.ui(ui);
});
TabKind::Logs => Logs {
egui_layer: &self.egui_layer,
logs_state: &mut self.states.logs_state,
}
.ui(ui),
TabKind::DownloadProgress => {
views::DownloadingProgress {
manager: &self.manager,
Expand Down
6 changes: 4 additions & 2 deletions crates/client/src/states.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ use crate::{
add_tab_menu::TabsState,
profiles::ProfilesState,
settings::{ClientSettingsState, SettingsState},
AddProfileMenuState, ModManagerState, ProfileInfoState, ProfilesConfig,
AddProfileMenuState, LogsState, ModManagerState, ProfileInfoState, ProfilesConfig,
},
};

pub struct States {
pub tabs: TabsState,
pub errors_pool: ErrorsPoolState,

pub logs_state: LogsState,
pub java: JavaState,
pub profiles: ProfilesState,
pub settings: SettingsState,
Expand All @@ -42,8 +43,9 @@ impl Default for States {

Self {
tabs: TabsState::new(),
java: JavaState::new(),
errors_pool: ErrorsPoolState::default(),
logs_state: LogsState::new(),
java: JavaState::new(),
profiles: ProfilesState {
currently_downloading_profiles: HashSet::new(),
profiles: read_toml_config_sync::<ProfilesConfig>(DOT_NOMI_PROFILES_CONFIG).unwrap_or_default(),
Expand Down
2 changes: 2 additions & 0 deletions crates/client/src/views.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use eframe::egui::Ui;
pub mod add_profile_menu;
pub mod add_tab_menu;
pub mod downloading_progress;
pub mod logs;
pub mod mods_manager;
pub mod profile_info;
pub mod profiles;
Expand All @@ -11,6 +12,7 @@ pub mod settings;
pub use add_profile_menu::*;
pub use add_tab_menu::*;
pub use downloading_progress::*;
pub use logs::*;
pub use mods_manager::*;
pub use profile_info::*;
pub use profiles::*;
Expand Down
87 changes: 87 additions & 0 deletions crates/client/src/views/logs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use std::sync::Arc;

use eframe::egui;
use nomi_core::instance::logs::GameLogsWriter;
use parking_lot::Mutex;

use crate::subscriber::EguiLayer;

use super::View;

pub struct Logs<'a> {
pub egui_layer: &'a EguiLayer,
pub logs_state: &'a mut LogsState,
}

#[derive(Default)]
pub struct LogsState {
pub selected_tab: LogsPage,
pub game_logs: Arc<GameLogs>,
}

#[derive(Default, PartialEq)]
pub enum LogsPage {
#[default]
Game,
Launcher,
}

impl LogsState {
pub fn new() -> Self {
Self { ..Default::default() }
}
}

impl View for Logs<'_> {
fn ui(mut self, ui: &mut eframe::egui::Ui) {
egui::TopBottomPanel::top("logs_page_panel").show_inside(ui, |ui| {
ui.horizontal(|ui| {
ui.selectable_value(&mut self.logs_state.selected_tab, LogsPage::Game, "Game");
ui.selectable_value(&mut self.logs_state.selected_tab, LogsPage::Launcher, "Launcher");
});
});

match self.logs_state.selected_tab {
LogsPage::Game => self.game_ui(ui),
LogsPage::Launcher => self.launcher_ui(ui),
}
}
}

impl Logs<'_> {
pub fn game_ui(&mut self, ui: &mut egui::Ui) {
egui::ScrollArea::both().stick_to_bottom(true).show(ui, |ui| {
ui.vertical(|ui| {
let lock = self.logs_state.game_logs.logs.lock();
for message in lock.iter() {
ui.label(message);
}
});
});
}

pub fn launcher_ui(&mut self, ui: &mut egui::Ui) {
egui::ScrollArea::both().stick_to_bottom(true).show(ui, |ui| self.egui_layer.ui(ui));
}
}

#[derive(Default)]
pub struct GameLogs {
logs: Arc<Mutex<Vec<String>>>,
}

impl GameLogs {
pub fn new() -> Self {
Self::default()
}

pub fn clear(&self) {
self.logs.lock().clear();
}
}

impl GameLogsWriter for GameLogs {
fn write(&self, data: nomi_core::instance::logs::GameLogsEvent) {
self.logs.lock().push(data.into_message());
}
}
7 changes: 5 additions & 2 deletions crates/client/src/views/profiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use super::{
add_profile_menu::{AddProfileMenu, AddProfileMenuState},
load_mods,
settings::SettingsState,
ModsConfig, ProfileInfoState, TabsState, View,
LogsState, ModsConfig, ProfileInfoState, TabsState, View,
};

pub struct ProfilesPage<'a> {
Expand All @@ -40,6 +40,7 @@ pub struct ProfilesPage<'a> {

pub is_profile_window_open: &'a mut bool,

pub logs_state: &'a LogsState,
pub tabs_state: &'a mut TabsState,
pub profiles_state: &'a mut ProfilesState,
pub menu_state: &'a mut AddProfileMenuState,
Expand Down Expand Up @@ -185,14 +186,16 @@ impl View for ProfilesPage<'_> {
let should_load_mods = profile.profile.loader().is_fabric();
let profile_id = profile.profile.id;

let game_logs = self.logs_state.game_logs.clone();
game_logs.clear();
let run_game = Task::new(
"Running the game",
Caller::standard(async move {
if should_load_mods {
load_mods(profile_id).await.report_error();
}

instance.launch(user_data, &java_runner).await.report_error()
instance.launch(user_data, &java_runner, &*game_logs).await.report_error()
}),
);

Expand Down
2 changes: 2 additions & 0 deletions crates/nomi-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ repository = "https://github.com/Umatriz/nomi"

[dependencies]
tokio.workspace = true
tokio-stream.workspace = true
tokio-util.workspace = true
anyhow.workspace = true
serde.workspace = true
serde_json.workspace = true
Expand Down
9 changes: 6 additions & 3 deletions crates/nomi-core/src/configs/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ use serde::{Deserialize, Serialize};
use typed_builder::TypedBuilder;

use crate::{
instance::launch::{arguments::UserData, LaunchInstance},
instance::{
launch::{arguments::UserData, LaunchInstance},
logs::GameLogsWriter,
},
repository::{java_runner::JavaRunner, manifest::VersionType},
};

Expand Down Expand Up @@ -71,9 +74,9 @@ pub struct VersionProfile {
}

impl VersionProfile {
pub async fn launch(&self, user_data: UserData, java_runner: &JavaRunner) -> anyhow::Result<()> {
pub async fn launch(&self, user_data: UserData, java_runner: &JavaRunner, logs_writer: &dyn GameLogsWriter) -> anyhow::Result<()> {
match &self.state {
ProfileState::Downloaded(instance) => instance.launch(user_data, java_runner).await,
ProfileState::Downloaded(instance) => instance.launch(user_data, java_runner, logs_writer).await,
ProfileState::NotDownloaded { .. } => Err(anyhow!("This profile is not downloaded!")),
}
}
Expand Down
45 changes: 38 additions & 7 deletions crates/nomi-core/src/instance/launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ use std::{
fs::{File, OpenOptions},
io,
path::{Path, PathBuf},
process::Stdio,
};

use arguments::UserData;
use serde::{Deserialize, Serialize};
use tokio::process::Command;
use tracing::{debug, info, trace, warn};
use tokio_stream::StreamExt;
use tokio_util::codec::{FramedRead, LinesCodec};
use tracing::{debug, error, info, trace, warn};

use crate::{
downloads::Assets,
Expand All @@ -20,7 +23,11 @@ use crate::{

use self::arguments::ArgumentsBuilder;

use super::{profile::LoaderProfile, Undefined};
use super::{
logs::{GameLogsEvent, GameLogsWriter},
profile::LoaderProfile,
Undefined,
};

pub mod arguments;
pub mod rules;
Expand Down Expand Up @@ -141,8 +148,8 @@ impl LaunchInstance {
Ok(())
}

#[tracing::instrument(skip(self), err)]
pub async fn launch(&self, user_data: UserData, java_runner: &JavaRunner) -> anyhow::Result<()> {
#[tracing::instrument(skip(self, logs_writer), err)]
pub async fn launch(&self, user_data: UserData, java_runner: &JavaRunner, logs_writer: &dyn GameLogsWriter) -> anyhow::Result<()> {
let manifest = read_json_config::<Manifest>(&self.settings.manifest_file).await?;

let arguments_builder = ArgumentsBuilder::new(self, &manifest).with_classpath().with_userdata(user_data);
Expand All @@ -163,16 +170,40 @@ impl LaunchInstance {
let mut child = Command::new(java_runner.get())
.args(custom_jvm_arguments)
.args(loader_jvm_arguments)
.args(dbg!(manifest_jvm_arguments))
.args(manifest_jvm_arguments)
.arg(main_class)
.args(dbg!(manifest_game_arguments))
.args(manifest_game_arguments)
.args(loader_game_arguments)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
// Works incorrectly so let's ignore it for now.
// It will work when the instances are implemented.
// .current_dir(std::fs::canonicalize(MINECRAFT_DIR)?)
.spawn()?;

child.wait().await?.code().inspect(|code| info!("Minecraft exit code: {}", code));
let stdout = child.stdout.take().expect("child did not have a handle to stdout");
let stderr = child.stderr.take().expect("child did not have a handle to stdout");

// let mut stdout_reader = BufReader::new(stdout).lines();
// let mut stderr_reader = BufReader::new(stderr).lines();

let stdout = FramedRead::new(stdout, LinesCodec::new());
let stderr = FramedRead::new(stderr, LinesCodec::new());

tokio::spawn(async move {
if let Ok(out) = child.wait().await.inspect_err(|e| error!(error = ?e, "Unable to get the exit code")) {
out.code().inspect(|code| info!("Minecraft exit code: {}", code));
};
});

let mut read = stdout.merge(stderr);

while let Some(line) = read.next().await {
match line {
Ok(line) => logs_writer.write(GameLogsEvent::new(line)),
Err(e) => error!(error = ?e, "Error occurred while decoding game's output"),
}
}

Ok(())
}
Expand Down
Loading

0 comments on commit 40a1db8

Please sign in to comment.