diff --git a/owmods_core/src/socket.rs b/owmods_core/src/socket.rs index 8f15e4014..5bd7ae347 100644 --- a/owmods_core/src/socket.rs +++ b/owmods_core/src/socket.rs @@ -1,5 +1,5 @@ use anyhow::{anyhow, Result}; -use log::{error, info}; +use log::{error, info, warn}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use tokio::{ @@ -105,9 +105,13 @@ impl LogServer { // Give a log to the tx channel async fn yield_log(tx: &LogServerSender, message: SocketMessage) { - let res = tx.send(message).await; - if let Err(why) = res { - error!("Couldn't Yield Log: {why:?}") + if tx.capacity() == 0 { + warn!("Logs incoming too fast! Logs may be dropped!"); + } else { + let res = tx.send(message).await; + if let Err(why) = res { + error!("Couldn't Yield Log: {why:?}") + } } } diff --git a/owmods_gui/backend/src/commands.rs b/owmods_gui/backend/src/commands.rs index 07039b44d..13a92e17d 100644 --- a/owmods_gui/backend/src/commands.rs +++ b/owmods_gui/backend/src/commands.rs @@ -34,7 +34,10 @@ use tauri::{api::dialog, async_runtime, AppHandle, Manager, WindowEvent}; use time::{macros::format_description, OffsetDateTime}; use tokio::{sync::mpsc, try_join}; -use crate::events::{CustomEventEmitter, CustomEventEmitterAll, CustomEventTriggerGlobal, Event}; +use crate::events::{ + CustomEventEmitter, CustomEventEmitterAll, CustomEventTriggerGlobal, Event, + LogLineCountUpdatePayload, +}; use crate::progress::ProgressBar; use crate::protocol::{ProtocolInstallType, ProtocolPayload}; use crate::{ @@ -721,6 +724,21 @@ pub async fn run_game( let window_handle = window.app_handle(); let mut game_log = state.game_log.write().await; if let Some((lines, writer)) = game_log.get_mut(&port) { + if let Some(last_message) = lines.last_mut() { + if msg == last_message.message { + last_message.amount += 1; + let res = window_handle.typed_emit_all(&Event::LogLineCountUpdate( + LogLineCountUpdatePayload { + port, + line: (lines.len() - 1).try_into().unwrap_or(u32::MAX), + }, + )); + if let Err(why) = res { + error!("Couldn't Emit Game Log: {}", why) + } + continue; + } + } let res = write_log(writer, &msg); if let Err(why) = res { error!("Couldn't Write Game Log: {}", why); @@ -773,7 +791,7 @@ pub async fn get_log_lines( filter_type: Option, search: &str, state: tauri::State<'_, State>, -) -> Result> { +) -> Result> { let logs = state.game_log.read().await; if let Some((lines, _)) = logs.get(&port) { let lines = get_logs_indices(lines, filter_type, search)?; diff --git a/owmods_gui/backend/src/events.rs b/owmods_gui/backend/src/events.rs index b98813d5d..177f25703 100644 --- a/owmods_gui/backend/src/events.rs +++ b/owmods_gui/backend/src/events.rs @@ -14,6 +14,14 @@ fn map_emit_err(e: tauri::Error) -> anyhow::Error { #[typeshare] pub type EmptyParams = (); +// Typeshare doesn't allow tuples, I cry +#[typeshare] +#[derive(Deserialize, Serialize, Clone)] +pub struct LogLineCountUpdatePayload { + pub port: LogPort, + pub line: u32, +} + #[typeshare] #[derive(Deserialize, Serialize, Clone)] #[serde(tag = "name", content = "params", rename_all = "camelCase")] @@ -26,6 +34,7 @@ pub enum Event { OwmlConfigReload(EmptyParams), GameStart(LogPort), LogUpdate(LogPort), + LogLineCountUpdate(LogLineCountUpdatePayload), LogFatal(GameMessage), ProtocolInvoke(ProtocolPayload), ProgressUpdate(EmptyParams), diff --git a/owmods_gui/backend/src/game.rs b/owmods_gui/backend/src/game.rs index 02015591c..99b480dc7 100644 --- a/owmods_gui/backend/src/game.rs +++ b/owmods_gui/backend/src/game.rs @@ -22,11 +22,16 @@ use crate::LogPort; pub struct GameMessage { pub port: LogPort, pub message: SocketMessage, + pub amount: u32, } impl GameMessage { pub fn new(port: LogPort, message: SocketMessage) -> Self { - Self { port, message } + Self { + port, + message, + amount: 1, + } } } @@ -79,43 +84,28 @@ pub fn get_logs_indices( lines: &[GameMessage], filter_type: Option, search: &str, -) -> Result> { +) -> Result> { let mut indices = Vec::with_capacity(lines.len()); let search = search.to_ascii_lowercase(); - let mut count = 1; for (line_number, line) in lines.iter().enumerate() { - let mut same = false; - if let Some(next_line) = lines.get(line_number + 1) { - if next_line.message.message == line.message.message - && next_line.message.message_type == line.message.message_type - && next_line.message.sender_name == line.message.sender_name - { - same = true; + let mut include = true; + if filter_type.is_some() || !search.trim().is_empty() { + let matches_type = filter_type.is_none() + || line.message.message_type == *filter_type.as_ref().unwrap(); + let matches_search = search.is_empty() + || line.message.message.to_ascii_lowercase().contains(&search) + || line + .message + .sender_name + .as_ref() + .map(|v| v.to_ascii_lowercase().contains(&search)) + .unwrap_or(false); + if !(matches_type && matches_search) { + include = false; } } - if same { - count += 1; - } else { - let mut include = true; - if filter_type.is_some() || !search.trim().is_empty() { - let matches_type = filter_type.is_none() - || line.message.message_type == *filter_type.as_ref().unwrap(); - let matches_search = search.is_empty() - || line.message.message.to_ascii_lowercase().contains(&search) - || line - .message - .sender_name - .as_ref() - .map(|v| v.to_ascii_lowercase().contains(&search)) - .unwrap_or(false); - if !(matches_type && matches_search) { - include = false; - } - } - if include { - indices.push((line_number, count)); - } - count = 1; + if include { + indices.push(line_number); } } Ok(indices) diff --git a/owmods_gui/frontend/src/commands.ts b/owmods_gui/frontend/src/commands.ts index db59816fb..841622ad7 100644 --- a/owmods_gui/frontend/src/commands.ts +++ b/owmods_gui/frontend/src/commands.ts @@ -71,7 +71,7 @@ const commandInfo = { filterType?: number | undefined; search: string; }, - [number, number][] + number[] > >("get_log_lines"), exportMods: $>("export_mods"), @@ -111,9 +111,12 @@ const makeInvoke = (key: Command, forceNoDisplayErr?: boolean) => { const makeHook = (key: Command) => { const name = commandInfo[key]; - return (eventName: Event["name"] | Event["name"][], payload?: (typeof name)[0]) => { + return ( + eventName: E | Event["name"][], + payload?: (typeof name)[0] + ) => { const fn = makeInvoke(key, true); - return useTauri<(typeof name)[1]>( + return useTauri<(typeof name)[1], E>( eventName, () => fn(payload ?? {}) as unknown as Promise<(typeof name)[1]>, payload @@ -131,7 +134,8 @@ export type Commands = { export type Hooks = { [T in Command]: ( eventName: Event["name"] | Event["name"][], - payload?: (typeof commandInfo)[T][0] + payload?: (typeof commandInfo)[T][0], + shouldChangeFn?: (params: unknown) => boolean ) => [LoadState, (typeof commandInfo)[T][1] | null, Error | null]; }; diff --git a/owmods_gui/frontend/src/components/logs/LogApp.tsx b/owmods_gui/frontend/src/components/logs/LogApp.tsx index 1c7bb2dc0..15f4ca723 100644 --- a/owmods_gui/frontend/src/components/logs/LogApp.tsx +++ b/owmods_gui/frontend/src/components/logs/LogApp.tsx @@ -13,7 +13,7 @@ import { listen } from "@events"; import { simpleOnError } from "@components/common/StyledErrorBoundary"; export type LogFilter = keyof typeof SocketMessageType | "Any"; -export type LogLines = [number, number][]; +export type LogLines = number[]; const thisWindow = getCurrent(); diff --git a/owmods_gui/frontend/src/components/logs/LogRow.tsx b/owmods_gui/frontend/src/components/logs/LogRow.tsx index dcddb7750..2c5c8990a 100644 --- a/owmods_gui/frontend/src/components/logs/LogRow.tsx +++ b/owmods_gui/frontend/src/components/logs/LogRow.tsx @@ -1,14 +1,13 @@ import { hooks } from "@commands"; import ODTooltip from "@components/common/ODTooltip"; import { Box, Chip, Palette, Skeleton, TableCell, Typography, useTheme } from "@mui/material"; -import { SocketMessageType } from "@types"; +import { LogLineCountUpdatePayload, SocketMessageType } from "@types"; import { Fragment, MutableRefObject, memo, useLayoutEffect, useMemo } from "react"; import { VirtuosoHandle } from "react-virtuoso"; export interface LogRowProps { port: number; index: number; - count: number; virtuosoRef?: MutableRefObject; } @@ -33,10 +32,16 @@ const getColor = (palette: Palette, messageType: SocketMessageType) => { const LogRow = memo(function LogRow(props: LogRowProps) { const theme = useTheme(); - const [status, logLine] = hooks.getLogLine("none", { - port: props.port, - line: props.index - }); + const [status, logLine] = hooks.getLogLine( + "logLineCountUpdate", + { + port: props.port, + line: props.index + }, + (params) => { + return (params as LogLineCountUpdatePayload).line === props.index; + } + ); const messageType = useMemo(() => { return Object.values(SocketMessageType)[ @@ -59,6 +64,8 @@ const LogRow = memo(function LogRow(props: LogRowProps) { props.virtuosoRef?.current?.autoscrollToBottom?.(); }, [status, props.virtuosoRef]); + console.debug(logLine?.amount ?? 1); + return ( <> @@ -95,9 +102,13 @@ const LogRow = memo(function LogRow(props: LogRowProps) { )}{" "} - {props.count > 1 && ( + {(logLine?.amount ?? 1) > 1 && ( - + )} diff --git a/owmods_gui/frontend/src/components/logs/LogTable.tsx b/owmods_gui/frontend/src/components/logs/LogTable.tsx index f7ba10ee9..d04a8e499 100644 --- a/owmods_gui/frontend/src/components/logs/LogTable.tsx +++ b/owmods_gui/frontend/src/components/logs/LogTable.tsx @@ -49,7 +49,7 @@ const LogTable = memo(function LogTable(props: LogTableProps) { `${index}-${props.logLines[index][0]}`} + computeItemKey={(index) => `${index}-${props.logLines[index]}`} increaseViewportBy={500} atBottomThreshold={1000} data={props.logLines} @@ -60,7 +60,7 @@ const LogTable = memo(function LogTable(props: LogTableProps) { )} itemContent={(_, data) => ( - + )} followOutput alignToBottom diff --git a/owmods_gui/frontend/src/events.ts b/owmods_gui/frontend/src/events.ts index 661702d80..ede1d851b 100644 --- a/owmods_gui/frontend/src/events.ts +++ b/owmods_gui/frontend/src/events.ts @@ -2,7 +2,7 @@ import { simpleOnError } from "@components/common/StyledErrorBoundary"; import { listen as tauriListen, emit as tauriEmit } from "@tauri-apps/api/event"; import { Event } from "@types"; -type Params = Extract["params"]; +export type Params = Extract["params"]; type EventSubscriptions = { [E in Event["name"]]: Array<(params: Params) => void>; diff --git a/owmods_gui/frontend/src/hooks.ts b/owmods_gui/frontend/src/hooks.ts index 8123ae7f1..ea09a9f99 100644 --- a/owmods_gui/frontend/src/hooks.ts +++ b/owmods_gui/frontend/src/hooks.ts @@ -6,17 +6,18 @@ import { } from "@components/common/TranslationContext"; import { Event, FailedMod, LocalMod, RemoteMod, UnsafeLocalMod } from "@types"; import { useErrorBoundary } from "react-error-boundary"; -import { listen } from "@events"; +import { Params, listen } from "@events"; export type LoadState = "Loading" | "Done"; /** * Use @commands:hooks if possible */ -export const useTauri = ( +export const useTauri = ( eventName: Event["name"] | Event["name"][], commandFn: () => Promise, - payload: unknown + payload: unknown, + shouldChangeFn?: (params: Params) => boolean ): [LoadState, T | null] => { const [status, setStatus] = useState("Loading"); const [data, setData] = useState(null); @@ -27,7 +28,10 @@ export const useTauri = ( useEffect(() => { if (status !== "Loading") { for (const eventToSubscribe of events) { - listen(eventToSubscribe, () => setStatus("Loading")); + listen(eventToSubscribe, (params) => { + if (shouldChangeFn && !shouldChangeFn(params)) return; + setStatus("Loading"); + }); } } else { commandFn() @@ -40,7 +44,7 @@ export const useTauri = ( }) .finally(() => setStatus("Done")); } - }, [commandFn, errorBound, events, status]); + }, [commandFn, shouldChangeFn, errorBound, events, status]); useEffect(() => { if (status === "Done") { diff --git a/owmods_gui/frontend/src/types.d.ts b/owmods_gui/frontend/src/types.d.ts index 95c7e99ec..c355571cc 100644 --- a/owmods_gui/frontend/src/types.d.ts +++ b/owmods_gui/frontend/src/types.d.ts @@ -1,5 +1,5 @@ /* - Generated by typeshare 1.6.0 + Generated by typeshare 1.2.0 */ export type EmptyParams = undefined; @@ -26,27 +26,19 @@ export interface Config { viewedAlerts: string[]; } -/** Represents an error with a [LocalMod] */ -export type ModValidationError = - /** The mod's manifest was invalid, contains the error encountered when loading it */ - | { errorType: "InvalidManifest"; payload: string } - /** The mod is missing a dependency that needs to be installed, contains the unique name of the missing dep */ - | { errorType: "MissingDep"; payload: string } - /** A dependency of the mod is disabled, contains the unique name of the disabled dep */ - | { errorType: "DisabledDep"; payload: string } - /** There's another enabled mod that conflicts with this one, contains the conflicting mod */ - | { errorType: "ConflictingMod"; payload: string } - /** The DLL the mod specifies in its `manifest.json` doesn't exist, contains the path (if even present) to the DLL specified by the mod */ - | { errorType: "MissingDLL"; payload?: string } - /** There's another mod already in the DB with this mod's unique name, contains the path of the other mod that has the same unique name */ - | { errorType: "DuplicateMod"; payload: string } - /** The mod is outdated, contains the newest version */ - | { errorType: "Outdated"; payload: string }; +/** Represents an installed (and valid) mod */ +export interface LocalMod { + enabled: boolean; + errors: ModValidationError[]; + modPath: string; + manifest: ModManifest; +} -/** Represents a warning a mod wants to show to the user on start */ -export interface ModWarning { - title: string; - body: string; +/** Represents a mod that completely failed to load */ +export interface FailedMod { + error: ModValidationError; + modPath: string; + displayPath: string; } /** Represents a manifest file for a local mod. */ @@ -64,31 +56,10 @@ export interface ModManifest { patcher?: string; } -/** Represents an installed (and valid) mod */ -export interface LocalMod { - enabled: boolean; - errors: ModValidationError[]; - modPath: string; - manifest: ModManifest; -} - -/** Represents a mod that completely failed to load */ -export interface FailedMod { - error: ModValidationError; - modPath: string; - displayPath: string; -} - -/** Contains URLs for a mod's README */ -export interface ModReadMe { - htmlUrl: string; - downloadUrl: string; -} - -/** A prerelease for a mod */ -export interface ModPrerelease { - downloadUrl: string; - version: string; +/** Represents a warning a mod wants to show to the user on start */ +export interface ModWarning { + title: string; + body: string; } /** Represents a mod in the remote database */ @@ -111,6 +82,18 @@ export interface RemoteMod { tags?: string[]; } +/** A prerelease for a mod */ +export interface ModPrerelease { + downloadUrl: string; + version: string; +} + +/** Contains URLs for a mod's README */ +export interface ModReadMe { + htmlUrl: string; + downloadUrl: string; +} + /** Represents the configuration for OWML */ export interface OWMLConfig { gamePath: string; @@ -121,18 +104,6 @@ export interface OWMLConfig { socketPort: number; } -/** Represents the type of message sent from the game */ -export enum SocketMessageType { - Message = "message", - Error = "error", - Warning = "warning", - Info = "info", - Success = "success", - Quit = "quit", - Fatal = "fatal", - Debug = "debug" -} - /** Represents a message sent from the game */ export interface SocketMessage { senderName?: string; @@ -141,24 +112,15 @@ export interface SocketMessage { messageType: SocketMessageType; } -export interface GameMessage { +export interface LogLineCountUpdatePayload { port: LogPort; - message: SocketMessage; + line: number; } -export enum Language { - Wario = "Wario", - English = "English" -} - -export enum Theme { - Blue = "Blue", - Red = "Red", - Pink = "Pink", - Purple = "Purple", - Blurple = "Blurple", - GhostlyGreen = "GhostlyGreen", - Green = "Green" +export interface GameMessage { + port: LogPort; + message: SocketMessage; + amount: number; } export interface GuiConfig { @@ -196,14 +158,6 @@ export interface ProgressBars { counter: number; } -export enum ProtocolInstallType { - InstallMod = "installMod", - InstallURL = "installURL", - InstallPreRelease = "installPreRelease", - InstallZip = "installZip", - Unknown = "unknown" -} - /** * Represents a payload receive by a protocol handler (link from the website) * All URLs should start with owmods:// @@ -228,6 +182,35 @@ export type UnsafeLocalMod = | { loadState: "valid"; mod: LocalMod } | { loadState: "invalid"; mod: FailedMod }; +/** Represents the type of message sent from the game */ +export enum SocketMessageType { + Message = "message", + Error = "error", + Warning = "warning", + Info = "info", + Success = "success", + Quit = "quit", + Fatal = "fatal", + Debug = "debug" +} + +/** Represents an error with a [LocalMod] */ +export type ModValidationError = + /** The mod's manifest was invalid, contains the error encountered when loading it */ + | { errorType: "InvalidManifest"; payload: string } + /** The mod is missing a dependency that needs to be installed, contains the unique name of the missing dep */ + | { errorType: "MissingDep"; payload: string } + /** A dependency of the mod is disabled, contains the unique name of the disabled dep */ + | { errorType: "DisabledDep"; payload: string } + /** There's another enabled mod that conflicts with this one, contains the conflicting mod */ + | { errorType: "ConflictingMod"; payload: string } + /** The DLL the mod specifies in its `manifest.json` doesn't exist, contains the path (if even present) to the DLL specified by the mod */ + | { errorType: "MissingDLL"; payload?: string } + /** There's another mod already in the DB with this mod's unique name, contains the path of the other mod that has the same unique name */ + | { errorType: "DuplicateMod"; payload: string } + /** The mod is outdated, contains the newest version */ + | { errorType: "Outdated"; payload: string }; + export type Event = | { name: "localRefresh"; params: EmptyParams } | { name: "remoteRefresh"; params: EmptyParams } @@ -237,6 +220,7 @@ export type Event = | { name: "owmlConfigReload"; params: EmptyParams } | { name: "gameStart"; params: LogPort } | { name: "logUpdate"; params: LogPort } + | { name: "logLineCountUpdate"; params: LogLineCountUpdatePayload } | { name: "logFatal"; params: GameMessage } | { name: "protocolInvoke"; params: ProtocolPayload } | { name: "progressUpdate"; params: EmptyParams } @@ -247,3 +231,26 @@ export type Event = | { name: "requestReload"; params: string } /** Purposefully never used, some hooks only need to run once */ | { name: "none"; params: EmptyParams }; + +export enum Theme { + Blue = "Blue", + Red = "Red", + Pink = "Pink", + Purple = "Purple", + Blurple = "Blurple", + GhostlyGreen = "GhostlyGreen", + Green = "Green" +} + +export enum Language { + Wario = "Wario", + English = "English" +} + +export enum ProtocolInstallType { + InstallMod = "installMod", + InstallURL = "installURL", + InstallPreRelease = "installPreRelease", + InstallZip = "installZip", + Unknown = "unknown" +} diff --git a/xtask/src/log_spammer.rs b/xtask/src/log_spammer.rs new file mode 100644 index 000000000..e308a340f --- /dev/null +++ b/xtask/src/log_spammer.rs @@ -0,0 +1,23 @@ +use std::io::prelude::*; +use std::net::TcpStream; +use std::time::Duration; + +use anyhow::Result; + +pub fn spam_logs(port: u16) -> Result<()> { + let mut stream = TcpStream::connect(format!("127.0.0.1:{port}"))?; + let mut i = 1; + loop { + println!("Message {i}, {}", i == usize::MAX); + let msg = "{\"type\": 0, \"message\": \"Line 1\\nLine 2\", \"senderName\": \"xtask\", \"senderType\": \"log_spammer\"}\n"; + stream.write_all(msg.as_bytes())?; + i += 1; + std::thread::sleep(Duration::from_secs_f32( + std::env::args() + .nth(3) + .unwrap_or("0.01666666666666667".to_string()) + .parse() + .unwrap(), + )); + } +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 47dcc58d8..b688fbfd1 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -7,6 +7,7 @@ use regex::RegexBuilder; mod cli_tasks; mod gui_tasks; +mod log_spammer; pub fn get_out_dir() -> Result { let out_dir = std::path::PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()) @@ -40,6 +41,13 @@ fn main() -> Result<()> { "cli_pkg_build" => generate_cli_pkg_build()?, "gui_pkg_build" => generate_gui_pkg_build()?, "cli_version" => print_version()?, + "spam_logs" => log_spammer::spam_logs( + std::env::args() + .nth(2) + .expect("Enter port") + .parse() + .unwrap(), + )?, _ => panic!("Invalid Command: {cmd}"), } Ok(())